diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/AUTHORS b/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/AUTHORS new file mode 100644 index 0000000000000000000000000000000000000000..1062f3bf743c0a37bcde05b1afc1ba5e98a95822 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/AUTHORS @@ -0,0 +1,1379 @@ +All people who contributed to SymPy by sending at least a patch or +more (in the order of the date of their first contribution), except +those who explicitly didn't want to be mentioned. People with a * next +to their names are not found in the metadata of the git history. This +file is generated automatically by running `./bin/authors_update.py`. + +There are a total of 1371 authors. + +Ondřej Čertík +Fabian Pedregosa +Jurjen N.E. Bos +Mateusz Paprocki +*Marc-Etienne M.Leveille +Brian Jorgensen +Jason Gedge +Robert Schwarz +Pearu Peterson +Fredrik Johansson +Chris Wu +*Ulrich Hecht +Goutham Lakshminarayan +David Lawrence +Jaroslaw Tworek +David Marek +Bernhard R. Link +Andrej Tokarčík +Or Dvory +Saroj Adhikari +Pauli Virtanen +Robert Kern +James Aspnes +Nimish Telang +Abderrahim Kitouni +Pan Peng +Friedrich Hagedorn +Elrond der Elbenfuerst +Rizgar Mella +Felix Kaiser +Roberto Nobrega +David Roberts +Sebastian Krämer +Vinzent Steinberg +Riccardo Gori +Case Van Horsen +Stepan Roucka +Ali Raza Syed +Stefano Maggiolo +Robert Cimrman +Bastian Weber +Sebastian Krause +Sebastian Kreft +*Dan +Alan Bromborsky +Boris Timokhin +Robert +Andy R. Terrel +Hubert Tsang +Konrad Meyer +Henrik Johansson +Priit Laes +Freddie Witherden +Brian E. Granger +Andrew Straw +Kaifeng Zhu +Ted Horst +Andrew Docherty +Akshay Srinivasan +Aaron Meurer +Barry Wardell +Tomasz Buchert +Vinay Kumar +Johann Cohen-Tanugi +Jochen Voss +Luke Peterson +Chris Smith +Thomas Sidoti +Florian Mickler +Nicolas Pourcelot +Ben Goodrich +Toon Verstraelen +Ronan Lamy +James Abbatiello +Ryan Krauss +Bill Flynn +Kevin Goodsell +Jorn Baayen +Eh Tan +Renato Coutinho +Oscar Benjamin +Øyvind Jensen +Julio Idichekop Filho +Łukasz Pankowski +*Chu-Ching Huang +Fernando Perez +Raffaele De Feo +Christian Muise +Matt Curry +Kazuo Thow +Christian Schubert +Jezreel Ng +James Pearson +Matthew Brett +Addison Cugini +Nicholas J.S. Kinar +Harold Erbin +Thomas Dixon +Cristóvão Sousa +Andre de Fortier Smit +Mark Dewing +Alexey U. Gudchenko +Gary Kerr +Sherjil Ozair +Oleksandr Gituliar +Sean Vig +Prafullkumar P. Tale +Vladimir Perić +Tom Bachmann +Yuri Karadzhov +Vladimir Lagunov +Matthew Rocklin +Saptarshi Mandal +Gilbert Gede +Anatolii Koval +Tomo Lazovich +Pavel Fedotov +Jack McCaffery +Jeremias Yehdegho +Kibeom Kim +Gregory Ksionda +Tomáš Bambas +Raymond Wong +Luca Weihs +Shai 'Deshe' Wyborski +Thomas Wiecki +Óscar Nájera +Mario Pernici +Benjamin McDonald +Sam Magura +Stefan Krastanov +Bradley Froehle +Min Ragan-Kelley +Emma Hogan +Nikhil Sarda +Julien Rioux +Roberto Colistete, Jr. +Raoul Bourquin +Gert-Ludwig Ingold +Srinivas Vasudevan +Jason Moore +Miha Marolt +Tim Lahey +Luis Garcia +Matt Rajca +David Li +Alexandr Gudulin +Bilal Akhtar +Grzegorz Świrski +Matt Habel +David Ju +Nichita Utiu +Nikolay Lazarov +Steve Anton +Imran Ahmed Manzoor +Ljubiša Moćić <3rdslasher@gmail.com> +Piotr Korgul +Jim Zhang +Sam Sleight +tborisova +Chancellor Arkantos +Stepan Simsa +Tobias Lenz +Siddhanathan Shanmugam +Tiffany Zhu +Tristan Hume +Alexey Subach +Joan Creus +Geoffry Song +Puneeth Chaganti +Marcin Kostrzewa <> +Natalia Nawara +vishal +Shruti Mangipudi +Davy Mao +Swapnil Agarwal +Dhia Kennouche +jerryma1121 +Joachim Durchholz +Martin Povišer +Siddhant Jain +Kevin Hunter +Michael Mayorov +Nathan Alison +Christian Bühler +Carsten Knoll +Bharath M R +Matthias Toews +Sergiu Ivanov +Jorge E. Cardona +Sanket Agarwal +Manoj Babu K. +Sai Nikhil +Aleksandar Makelov +Sachin Irukula +Raphael Michel +Ashwini Oruganti +Andreas Klöckner +Prateek Papriwal +Arpit Goyal +Angadh Nanjangud +Comer Duncan +Jens H. Nielsen +Joseph Dougherty +Elliot Marshall +Guru Devanla +George Waksman +Alexandr Popov +Tarun Gaba +Takafumi Arakaki +Saurabh Jha +Rom le Clair +Angus Griffith <16sn6uv@gmail.com> +Timothy Reluga +Brian Stephanik +Alexander Eberspächer +Sachin Joglekar +Tyler Pirtle +Vasily Povalyaev +Colleen Lee +Matthew Hoff +Niklas Thörne +Huijun Mai +Marek Šuppa +Ramana Venkata +Prasoon Shukla +Stefen Yin +Thomas Hisch +Madeleine Ball +Mary Clark +Rishabh Dixit +Manoj Kumar +Akshit Agarwal +CJ Carey +Patrick Lacasse +Ananya H +Tarang Patel +Christopher Dembia +Benjamin Fishbein +Sean Ge +Amit Jamadagni +Ankit Agrawal +Björn Dahlgren +Christophe Saint-Jean +Demian Wassermann +Khagesh Patel +Stephen Loo +hm +Patrick Poitras +Katja Sophie Hotz +Varun Joshi +Chetna Gupta +Thilina Rathnayake +Max Hutchinson +Shravas K Rao +Matthew Tadd +Alexander Hirzel +Randy Heydon +Oliver Lee +Seshagiri Prabhu +Pradyumna +Erik Welch +Eric Nelson +Roland Puntaier +Chris Conley +Tim Swast +Dmitry Batkovich +Francesco Bonazzi +Yuriy Demidov +Rick Muller +Manish Gill +Markus Müller +Amit Saha +Jeremy +QuaBoo +Stefan van der Walt +David Joyner +Lars Buitinck +Alkiviadis G. Akritas +Vinit Ravishankar +Michael Boyle +Heiner Kirchhoffer +Pablo Puente +James Fiedler +Harsh Gupta +Tuomas Airaksinen +Paul Strickland +James Goppert +rathmann +Avichal Dayal +Paul Scott +Shipra Banga +Pramod Ch +Akshay +Buck Shlegeris +Jonathan Miller +Edward Schembor +Rajath Shashidhara +Zamrath Nizam +Aditya Shah +Rajat Aggarwal +Sambuddha Basu +Zeel Shah +Abhinav Chanda +Jim Crist +Sudhanshu Mishra +Anurag Sharma +Soumya Dipta Biswas +Sushant Hiray +Ben Lucato +Kunal Arora +Henry Gebhardt +Dammina Sahabandu +Manish Shukla +Ralph Bean +richierichrawr +John Connor +Juan Luis Cano Rodríguez +Sahil Shekhawat +Kundan Kumar +Stas Kelvich +sevaader +Dhruvesh Vijay Parikh +Venkatesh Halli +Lennart Fricke +Vlad Seghete +Shashank Agarwal +carstimon +Pierre Haessig +Maciej Baranski +Benjamin Gudehus +Faisal Anees +Mark Shoulson +Robert Johansson +Kalevi Suominen +Kaushik Varanasi +Fawaz Alazemi +Ambar Mehrotra +David P. Sanders +Peter Brady +John V. Siratt +Sarwar Chahal +Nathan Woods +Colin B. Macdonald +Marcus Näslund +Clemens Novak +Mridul Seth +Craig A. Stoudt +Raj +Mihai A. Ionescu +immerrr +Chai Wah Wu +Leonid Blouvshtein +Peleg Michaeli +ck Lux +zsc347 +Hamish Dickson +Michael Gallaspy +Roman Inflianskas +Duane Nykamp +Ted Dokos +Sunny Aggarwal +Victor Brebenar +Akshat Jain +Shivam Vats +Longqi Wang +Juan Felipe Osorio +Ray Cathcart +Lukas Zorich +Eric Miller +Cody Herbst +Nishith Shah +Amit Kumar +Yury G. Kudryashov +Guillaume Gay +Mihir Wadwekar +Tuan Manh Lai +Asish Panda +Darshan Chaudhary +Alec Kalinin +Ralf Stephan +Aaditya Nair +Jayesh Lahori +Harshil Goel +Luv Agarwal +Jason Ly +Lokesh Sharma +Sartaj Singh +Chris Swierczewski +Konstantin Togoi +Param Singh +Sumith Kulal +Juha Remes +Philippe Bouafia +Peter Schmidt +Jiaxing Liang +Lucas Jones +Gregory Ashton +Jennifer White +Renato Orsino +Alistair Lynn +Govind Sahai +Adam Bloomston +Kyle McDaniel +Nguyen Truong Duy +Alex Lindsay +Mathew Chong +Jason Siefken +Gaurav Dhingra +Gao, Xiang +Kevin Ventullo +mao8 +Isuru Fernando +Shivam Tyagi +Richard Otis +Rich LaSota +dustyrockpyle +Anton Akhmerov +Michael Zingale +Chak-Pong Chung +David T +Phil Ruffwind +Sebastian Koslowski +Kumar Krishna Agrawal +Dustin Gadal +João Moura +Yu Kobayashi +Shashank Kumar +Timothy Cyrus +Devyani Kota +Keval Shah +Dzhelil Rufat +Pastafarianist +Sourav Singh +Jacob Garber +Vinay Singh +GolimarOurHero +Prashant Tyagi +Matthew Davis +Tschijnmo TSCHAU +Alexander Bentkamp +Jack Kemp +Kshitij Saraogi +Thomas Baruchel +Nicolás Guarín-Zapata +Jens Jørgen Mortensen +Sampad Kumar Saha +Eva Charlotte Mayer +Laura Domine +Justin Blythe +Meghana Madhyastha +Tanu Hari Dixit +Shekhar Prasad Rajak +Aqnouch Mohammed +Arafat Dad Khan +Boris Atamanovskiy +Sam Tygier +Jai Luthra +Guo Xingjian +Sandeep Veethu +Archit Verma +Shubham Tibra +Ashutosh Saboo +Michael S. Hansen +Anish Shah +Guillaume Jacquenot +Bhautik Mavani +Michał Radwański +Jerry Li +Pablo Zubieta +Shivam Agarwal +Chaitanya Sai Alaparthi +Arihant Parsoya +Ruslan Pisarev +Akash Trehan +Nishant Nikhil +Vladimir Poluhsin +Akshay Nagar +James Brandon Milam +Abhinav Agarwal +Rishabh Daal +Sanya Khurana +Aman Deep +Aravind Reddy +Abhishek Verma +Matthew Parnell +Thomas Hickman +Akshay Siramdas +YiDing Jiang +Jatin Yadav +Matthew Thomas +Rehas Sachdeva +Michael Mueller +Srajan Garg +Prabhjot Singh +Haruki Moriguchi +Tom Gijselinck +Nitin Chaudhary +Alex Argunov +Nathan Musoke +Abhishek Garg +Dana Jacobsen +Vasiliy Dommes +Phillip Berndt +Haimo Zhang +Anthony Scopatz +bluebrook +Leonid Kovalev +Josh Burkart +Dimitra Konomi +Christina Zografou +Fiach Antaw +Langston Barrett +Krit Karan +G. D. McBain +Prempal Singh +Gabriel Orisaka +Matthias Bussonnier +rahuldan +Colin Marquardt +Andrew Taber +Yash Reddy +Peter Stangl +elvis-sik +Nikos Karagiannakis +Jainul Vaghasia +Dennis Meckel +Harshil Meena +Micky +Nick Curtis +Michele Zaffalon +Martha Giannoudovardi +Devang Kulshreshtha +Steph Papanik +Mohammad Sadeq Dousti +Arif Ahmed +Abdullah Javed Nesar +Lakshya Agrawal +shruti +Rohit Rango +Hong Xu +Ivan Petuhov +Alsheh +Marcel Stimberg +Alexey Pakhocmhik +Tommy Olofsson +Zulfikar +Blair Azzopardi +Danny Hermes +Sergey Pestov +Mohit Chandra +Karthik Chintapalli +Marcin Briański +andreo +Flamy Owl +Yicong Guo +Varun Garg +Rishabh Madan +Aditya Kapoor +Karan Sharma +Vedant Rathore +Johan Blåbäck +Pranjal Tale +Jason Tokayer +Raghav Jajodia +Rajat Thakur +Dhruv Bhanushali +Anjul Kumar Tyagi +Barun Parruck +Bao Chau +Tanay Agrawal +Ranjith Kumar +Shikhar Makhija +Yathartha Joshi +Valeriia Gladkova +Sagar Bharadwaj +Daniel Mahler +Ka Yi +Rishat Iskhakov +Szymon Mieszczak +Sachin Agarwal +Priyank Patel +Satya Prakash Dwibedi +tools4origins +Nico Schlömer +Fermi Paradox +Ekansh Purohit +Vedarth Sharma +Peeyush Kushwaha +Jayjayyy +Christopher J. Wright +Jakub Wilk +Mauro Garavello +Chris Tefer +Shikhar Jaiswal +Chiu-Hsiang Hsu +Carlos Cordoba +Fabian Ball +Yerniyaz +Christiano Anderson +Robin Neatherway +Thomas Hunt +Theodore Han +Duc-Minh Phan +Lejla Metohajrova +Samyak Jain +Aditya Rohan +Vincent Delecroix +Michael Sparapany +Harsh Jain +Nathan Goldbaum +latot +Kenneth Lyons +Stan Schymanski +David Daly +Ayush Shridhar +Javed Nissar +Jiri Kuncar +vedantc98 +Rupesh Harode +Rob Zinkov +James Harrop +James Taylor +Ishan Joshi +Marco Mancini +Boris Ettinger +Micah Fitch +Daniel Wennberg +ylemkimon +Akash Vaish +Peter Enenkel +Waldir Pimenta +Jithin D. George +Lev Chelyadinov +Lucas Wiman +Rhea Parekh +James Cotton +Robert Pollak +anca-mc +Sourav Ghosh +Jonathan Allan +Nikhil Pappu +Ethan Ward +Cezary Marczak +dps7ud +Nilabja Bhattacharya +Itay4 <31018228+Itay4@users.noreply.github.com> +Poom Chiarawongse +Yang Yang +Cavendish McKay +Bradley Gannon +B McG +Rob Drynkin +Seth Ebner +Akash Kundu +Mark Jeromin +Roberto Díaz Pérez +Gleb Siroki +Segev Finer +Alex Lubbock +Ayodeji Ige +Matthew Wardrop +Hugo van Kemenade +Austin Palmer +der-blaue-elefant +Filip Gokstorp +Yuki Matsuda +Aaron Miller +Salil Vishnu Kapur +Atharva Khare +Shubham Maheshwari +Pavel Tkachenko +Ashish Kumar Gaurav +Rajeev Singh +Keno Goertz +Lucas Gallindo +Himanshu +David Menéndez Hurtado +Amit Manchanda +Rohit Jain +Jonathan A. Gross +Unknown +Sayan Goswami +Subhash Saurabh +Rastislav Rabatin +Vishal +Jeremey Gluck +Akshat Maheshwari +symbolique +Saloni Jain +Arighna Chakrabarty +Abhigyan Khaund +Jashanpreet Singh +Saurabh Agarwal +luzpaz +P. Sai Prasanth +Nirmal Sarswat +Cristian Di Pietrantonio +Ravi charan +Nityananda Gohain +Cédric Travelletti +Nicholas Bollweg +Himanshu Ladia +Adwait Baokar +Mihail Tarigradschi +Saketh +rushyam +sfoo +Rahil Hastu +Zach Raines +Sidhant Nagpal +Gagandeep Singh +Rishav Chakraborty +Malkhan Singh +Joaquim Monserrat +Mayank Singh +Rémy Léone +Maxence Mayrand <35958639+maxencemayrand@users.noreply.github.com> +Nikoleta Glynatsi +helo9 +Ken Wakita +Carl Sandrock +Fredrik Eriksson +Ian Swire +Bulat +Ehren Metcalfe +Dmitry Savransky +Kiyohito Yamazaki +Caley Finn +Zhi-Qiang Zhou +Alexander Pozdneev +Wes Turner <50891+westurner@users.noreply.github.com> +JMSS-Unknown <31131631+JMSS-Unknown@users.noreply.github.com> +Arshdeep Singh +cym1 <16437732+cym1@users.noreply.github.com> +Stewart Wadsworth +Jared Lumpe +Avi Shrivastava +ramvenkat98 +Bilal Ahmed +Dimas Abreu Archanjo Dutra +Yatna Verma +S.Y. Lee +Miro Hrončok +Sudarshan Kamath +Ayushman Koul +Robert Dougherty-Bliss +Andrey Grozin +Bavish Kulur +Arun Singh +sirnicolaf <43586954+sirnicolaf@users.noreply.github.com> +Zachariah Etienne +Prayush Dawda <35144226+iamprayush@users.noreply.github.com> +2torus +Faisal Riyaz +Martin Roelfs +SirJohnFranklin +Anthony Sottile +ViacheslavP +Safiya03 +Alexander Dunlap +Rohit Sharma <31184621+rohitx007@users.noreply.github.com> +Jonathan Warner +Mohit Balwani +Marduk Bolaños +amsuhane +Matthias Geier +klaasvanaarsen <44929042+klaasvanaarsen@users.noreply.github.com> +Shubham Kumar Jha +rationa-kunal +Animesh Sinha +Gaurang Tandon <1gaurangtandon@gmail.com> +Matthew Craven +Daniel Ingram +Jogi Miglani +Takumasa Nakamura +Ritu Raj Singh +Rajiv Ranjan Singh +Vera Lozhkina +adhoc-king <46354827+adhoc-king@users.noreply.github.com> +Mikel Rouco +Oscar Gustafsson +damianos +Supreet Agrawal +shiksha11 +Martin Ueding +sharma-kunal +Divyanshu Thakur +Susumu Ishizuka +Samnan Rahee +Fredrik Andersson +Bhavya Srivastava +Alpesh Jamgade +Shubham Abhang +Vishesh Mangla +Nicko van Someren +dandiez <47832466+dandiez@users.noreply.github.com> +Frédéric Chapoton +jhanwar +Noumbissi valere Gille Geovan +Salmista-94 +Shivani Kohli +Parker Berry +Pragyan Mehrotra +Nabanita Dash +Gaetano Guerriero +Ankit Raj Pandey +Ritesh Kumar +kangzhiq <709563092@qq.com> +Jun Lin +Petr Kungurtsev +Anway De +znxftw +Denis Ivanenko +Orestis Vaggelis +Nikhil Maan +Abhinav Anand +Qingsha Shi +Juan Barbosa +Prionti Nasir +Bharat Raghunathan +arooshiverma +Christoph Gohle +Charalampos Tsiagkalis +Daniel Sears +Megan Ly +Sean P. Cornelius +Erik R. Gomez +Riccardo Magliocchetti +Henry Metlov +pekochun +Bendik Samseth +Vighnesh Shenoy +Versus Void +Denys Rybalka +Mark Dickinson +Rimi +rimibis <33387803+rimibis@users.noreply.github.com> +Steven Lee +Gilles Schintgen +Abhi58 +Tomasz Pytel +Aadit Kamat +Samesh +Velibor Zeli +Gabriel Bernardino +Joseph Redfern +Evelyn King +Miguel Marco +David Hagen +Hannah Kari +Soniya Nayak +Harsh Agarwal +Enric Florit +Yogesh Mishra +Denis Rykov +Ivan Tkachenko +Kenneth Emeka Odoh +Stephan Seitz +Yeshwanth N +Oscar Gerardo Lazo Arjona +Srinivasa Arun Yeragudipati +Kirtan Mali +TitanSnow +Pengning Chao <8857165+PengningChao@users.noreply.github.com> +Louis Abraham +Morten Olsen Lysgaard +Akash Nagaraj (akasnaga) +Akash Nagaraj +Lauren Glattly +Hou-Rui +George Korepanov +dranknight09 +aditisingh2362 +Gina +gregmedlock +Georgios Giapitzakis Tzintanos +Eric Wieser +Bradley Dowling <34559056+btdow@users.noreply.github.com> +Maria Marginean <33810762+mmargin@users.noreply.github.com> +Akash Agrawall +jgulian +Sourav Goyal +Zlatan Vasović +Alex Meiburg +Smit Lunagariya +Naman Gera +Julien Palard +Dhruv Mendiratta +erdOne <36414270+erdOne@users.noreply.github.com> +risubaba +abhinav28071999 <41710346+abhinav28071999@users.noreply.github.com> +Jisoo Song +Jaime R <38530589+Jaime02@users.noreply.github.com> +Vikrant Malik +Hardik Saini <43683678+Guardianofgotham@users.noreply.github.com> +Abhishek +Johannes Hartung +Milan Jolly +faizan2700 +mohit <39158356+mohitacecode@users.noreply.github.com> +Mohit Gupta +Psycho-Pirate +Chanakya-Ekbote +Rashmi Shehana +Jonty16117 +Anubhav Gupta +Michal Grňo +vezeli <37907135+vezeli@users.noreply.github.com> +Tim Gates +Sandeep Murthy +Neil +V1krant <46847915+V1krant@users.noreply.github.com> +alejandro +Riyan Dhiman +sbt4104 +Seth Troisi +Bhaskar Gupta +Smit Gajjar +rbl +Ilya Pchelintsev +Omar Wagih +prshnt19 +Johan Guzman +Vasileios Kalos +BasileiosKal <61801875+BasileiosKal@users.noreply.github.com> +Shubham Thorat <37049710+sbt4104@users.noreply.github.com> +Arpan Chattopadhyay +Ashutosh Hathidara +Moses Paul R +Saanidhya vats +tnzl +Vatsal Srivastava +Jean-Luc Herren +Dhruv Kothari +seadavis <45022599+seadavis@users.noreply.github.com> +kamimura +slacker404 +Jaime Resano +Ebrahim Byagowi +wuyudi +Akira Kyle +Calvin Jay Ross +Martin Thoma +Thomas A Caswell +Lagaras Stelios +Jerry James +Jan Kruse +Nathan Taylor +Vaishnav Damani +Mohit Shah +Mathias Louboutin +Marijan Smetko +Dave Witte Morris +soumi7 +Zhongshi +Wes Galbraith +KaustubhDamania +w495 +Akhil Rajput +Markus Mohrhard +Benjamin Wolba +彭于斌 <1931127624@qq.com> +Rudr Tiwari +Aaryan Dewan +Benedikt Placke +Sneha Goddu +goddus <39923708+goddus@users.noreply.github.com> +Shivang Dubey +Michael Greminger +Peter Cock +Willem Melching +Elias Basler +Brandon David +Abhay_Dhiman +Tasha Kim +Ayush Malik +Devesh Sawant +Wolfgang Stöcher +Sudeep Sidhu +foice +Ben Payne +Muskan Kumar <31043527+muskanvk@users.noreply.github.com> +noam simcha finkelstein +Garrett Folbe +Islam Mansour +Sayandip Halder +Shubham Agrawal +numbermaniac <5206120+numbermaniac@users.noreply.github.com> +Sakirul Alam +Mohammed Bilal +Chris du Plessis +Coder-RG +Ansh Mishra +Alex Malins +Lorenzo Contento +Naveen Sai +Shital Mule +Amanda Dsouza +Nijso Beishuizen +Harry Zheng +Felix Yan +Constantin Mateescu +Eva Tiwari +Aditya Kumar Sinha +Soumi Bardhan <51290447+Soumi7@users.noreply.github.com> +Kaustubh Chaudhari +Kristian Brünn +Neel Gorasiya +Akshat Sood <68052998+akshatsood2249@users.noreply.github.com> +Jose M. Gomez +Stefan Petrea +Praveen Sahu +Mark Bell +AlexCQY +Fabian Froehlich +Nikhil Gopalam +Kartik Sethi +Muhammed Abdul Quadir Owais +Harshit Yadav +Sidharth Mundhra +Suryam Arnav Kalra +Prince Gupta +Kunal Singh +Mayank Raj +Achal Jain <2achaljain@gmail.com> +Mario Maio +Aaron Stiff <69512633+AaronStiff@users.noreply.github.com> +Wyatt Peak +Bhaskar Joshi +Aditya Jindal +Vaibhav Bhat +Priyansh Rathi +Saket Kumar Singh +Yukai Chou +Qijia Liu +Paul Mandel +Nisarg Chaudhari <54911392+Nisarg-Chaudhari@users.noreply.github.com> +Dominik Stańczak +Rodrigo Luger +Marco Antônio Habitzreuter +Ayush Bisht +Akshansh Bhatt +Brandon T. Willard +Thomas Aarholt +Hiren Chalodiya +Roland Dixon +dimasvq +Sagar231 +Michael Chu +Abby Ng +Angad Sandhu <55819847+angadsinghsandhu@users.noreply.github.com> +Alexander Cockburn +Yaser AlOsh +Davide Sandonà +Jonathan Gutow +Nihir Agarwal +Lee Johnston +Zach Carmichael <20629897+craymichael@users.noreply.github.com> +Vijairam Ganesh Moorthy +Hanspeter Schmid +Ben Oostendorp +Nikita +Aman +Shashank KS +Aman Sharma +Anup Parikh +Lucy Mountain +Miguel Torres Costa +Rikard Nordgren +Arun sanganal <74652697+ArunSanganal@users.noreply.github.com> +Kamlesh Joshi <72374645+kamleshjoshi8102@users.noreply.github.com> +Joseph Rance <56409230+Joseph-Rance@users.noreply.github.com> +Huangduirong +Nils Schulte <47043622+Schnilz@users.noreply.github.com> +Matt Bogosian +Elisha Hollander +Aditya Ravuri +Mamidi Ratna Praneeth +Jeffrey Ryan +Jonathan Daniel <36337649+jond01@users.noreply.github.com> +Robin Richard +Gautam Menghani +Remco de Boer <29308176+redeboer@users.noreply.github.com> +Sebastian East +Evani Balasubramanyam +Rahil Parikh +Jason Ross +Joannah Nanjekye +Ayush Kumar +Kshitij +Daniel Hyams +alijosephine +Matthias Köppe +mohajain +Anibal M. Medina-Mardones +Travis Ens +Evgenia Karunus +Risiraj Dey +lastcodestanding +Andrey Lekar +Abbas Mohammed <42001049+iam-abbas@users.noreply.github.com> +Anutosh Bhat +Steve Kieffer +Paul Spiering +Pieter Gijsbers +Wang Ran (汪然) +naelsondouglas +Aman Thakur +S. Hanko +Dennis Sweeney +Gurpartap Singh +Hampus Malmberg +scimax +Nikhil Date +Kuldeep Borkar Jr +AkuBrain <76952313+Franck2111@users.noreply.github.com> +Leo Battle +Advait Pote +Anurag Bhat +Jeremy Monat +Diane Tchuindjo +Tom Fryers <61272761+TomFryers@users.noreply.github.com> +Zouhair +zzj <29055749+zjzh@users.noreply.github.com> +shubhayu09 +Siddhant Jain +Tirthankar Mazumder <63574588+wermos@users.noreply.github.com> +Sumit Kumar +Shivam Sagar +Gaurav Jain +Andrii Oriekhov +Luis Talavera +Arie Bovenberg +Carson McManus +Jack Schmidt <1107865+jackschmidt@users.noreply.github.com> +Riley Britten +Georges Khaznadar +Donald Wilson +Timo Stienstra +dispasha +Saksham Alok +Varenyam Bhardwaj +oittaa <8972248+oittaa@users.noreply.github.com> +Omkaar <79257339+Pysics@users.noreply.github.com> +Islem BOUZENIA +extraymond +Alexander Behrens +user202729 <25191436+user202729@users.noreply.github.com> +Pieter Eendebak +Zaz Brown +ritikBhandari +viocha <66580331+viocha@users.noreply.github.com> +Arthur Ryman +Xiang Wu +tttc3 +Seth Poulsen +cocolato +Anton Golovanov +Gareth Ma +Clément M.T. Robert +Glenn Horton-Smith +Karan +Stefan Behnle <84378403+behnle@users.noreply.github.com> +Shreyash Mishra <72146041+Shreyash-cyber@users.noreply.github.com> +Arthur Milchior +NotWearingPants <26556598+NotWearingPants@users.noreply.github.com> +Ishan Pandhare +Carlos García Montoro +Parcly Taxel +Saicharan +Kunal Sheth +Biswadeep Purkayastha <98874428+metabiswadeep@users.noreply.github.com> +Jyn Spring 琴春 +Phil LeMaitre +Chris Kerr +José Senart +Uwe L. Korn +ForeverHaibara <69423537+ForeverHaibara@users.noreply.github.com> +Yves Tumushimire +wookie184 +Costor +Klaus Rettinghaus +Sam Brockie +Abhishek Patidar <1e9abhi1e10@gmail.com> +Eric Demer +Pontus von Brömssen +Victor Immanuel +Evandro Bernardes +Michele Ceccacci +Ayush Aryan +Kishore Gopalakrishnan +Jan-Philipp Hoffmann +Daiki Takahashi +Sayan Mitra +Aman Kumar Shukla +Zoufiné Lauer-Baré +Charles Harris +Tejaswini Sanapathi +Devansh +Aaron Gokaslan +Daan Koning (he/him) +Steven Burns +Jay Patankar +Vivek Soni +Le Cong Minh Hieu +Sam Ritchie +Maciej Skórski +Tilo Reneau-Cardoso +Laurence Warne +Lukas Molleman +Konstantinos Riganas +Grace Su +Pedro Rosa +Abhinav Cillanki +Baiyuan Qiu <1061688677@qq.com> +Liwei Cai +Daniel Weindl +Isidora Araya +Seb Tiburzio +Victory Omole +Abhishek Chaudhary +Alexander Zhura +Shuai Zhou +Martin Manns +John Möller +zzc <1378113190@qq.com> +Pablo Galindo Salgado +Johannes Kasimir +Theodore Dias +Kaustubh <90597818+kaustubh-765@users.noreply.github.com> +Idan Pazi +Bobby Palmer +Saikat Das +Suman mondal +Taylan Sahin +Fabio Luporini +Oriel Malihi +Geetika Vadali +Matthias Rettl +Mikhail Remnev +philwillnyc <56197213+philwillnyc@users.noreply.github.com> +Raphael Lehner +Harry Mountain +Bhavik Sachdev +袁野 (Yuan Ye) +fazledyn-or +mohammedouahman +K. Kraus +Zac Hatfield-Dodds +platypus +codecruisader +James Whitehead +atharvParlikar +Ivan Petukhov +Augusto Borges +Han Wei Ang +Congxu Yang +Saicharan <62512681+saicharan2804@users.noreply.github.com> +Arnab Nandi +Harrison Oates <48871176+HarrisonOates@users.noreply.github.com> +Corey Cerovsek +Harsh Kasat +omahs <73983677+omahs@users.noreply.github.com> +Pascal Gitz +Ravindu-Hirimuthugoda +Sophia Pustova +George Pittock +Warren Jacinto +Sachin Singh +Zedmat <104870914+harshkasat@users.noreply.github.com> +Soumendra Ganguly +Samith Karunathilake <55777141+samithkavishke@users.noreply.github.com> +Viraj Vekaria +Shishir Kushwaha +Ankit Kumar Singh +Abhishek Kumar +Mohak Malviya +Matthias Liesenfeld <116307294+maliesen@users.noreply.github.com> +dodo +Mohamed Rezk +Tommaso Vaccari <05-gesto-follemente@icloud.com> +Alexis Schotte +Lauren Yim <31467609+cherryblossom000@users.noreply.github.com> +Prey Patel +Riccardo Di Girolamo +Abhishek kumar +Sam Lubelsky +Henrique Soares +Vladimir Sereda +Hwayeon Kang +Raj Sapale +Gerald Teschl +Richard Samuel <98638849+samuelard7@users.noreply.github.com> +HeeJae Chang +Nick Harder +Ethan DeGuire +Lorenz Winkler +Richard Rodenbusch +Zhenxu Zhu +Mark van Gelder +Mark van Gelder +Ishan Pandhare <91841626+Ishanned@users.noreply.github.com> +James A. Preiss +Emile Fourcini +Alberto Jiménez Ruiz +João Bravo +Dean Price +Edward Z. Yang +James Titus +Zhuoyuan Li +Hugo Kerstens +Jan Jancar +Andrew Mosson +Marek Madejski +Gonzalo Tornaría +Peter Stahlecker +Jean-François B <2589111+jfbu@users.noreply.github.com> +Zexuan Zhou (Bruce) +George Frolov +Corbet Elkins +Håkon Kvernmoen +Muhammad Maaz +Shishir Kushwaha <138311586+shishir-11@users.noreply.github.com> +Matt Wang +bharatAmeria <21001019007@jcboseust.ac.in> +Amir Ebrahimi +Steven Esquea +Rishabh Kamboj <111004091+VectorNd@users.noreply.github.com> +Aasim Ali +Ivan A. Melnikov +Borek Saheli +Guido Roncarolo +Quek Zi Yao +Roelof Rietbroek +MostafaGalal1 +Au Huishan +Kris Katterjohn +Shiyao Guo +Rushabh Mehta +Temiloluwa Yusuf ytemiloluwa@gmail.com ytemiloluwa +Davi Laerte +Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> +Harshit Gupta +Praveen Perumal +Kevin McWhirter +Prayag V +Lucas Kletzander +Pratyksh Gupta +Leonardo Mangani +Karan Anand +Gagan Mishra +Krishnav Bajoria +Matt Ord +Jatin Bhardwaj +Prashant Tandon +Paramjit Singh +João Rodrigues +Alejandro García Prada <114813960+AlexGarciaPrada@users.noreply.github.com> +Matthew Treinish +Clayton Rabideau +Victoria Koval +Voaides Negustor Robert <134785947+voaidesr@users.noreply.github.com> +Ovsk Mendov +David Brooks +Nicholas Laustrup <124007393+nicklaustrup@users.noreply.github.com> +Harikrishna Srinivasan +Mathis Cros +Arnav Mummineni <45217840+RCoder01@users.noreply.github.com> +Thangaraju Sibiraj <85477603+t-sibiraj@users.noreply.github.com> +KJaybhaye diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/INSTALLER b/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/LICENSE b/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..0744f229d697ca3ed1b1b257bfdb70e3eecf0b9e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/LICENSE @@ -0,0 +1,153 @@ +Copyright (c) 2006-2023 SymPy Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + a. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + b. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + c. Neither the name of SymPy nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +-------------------------------------------------------------------------------- + +Patches that were taken from the Diofant project (https://github.com/diofant/diofant) +are licensed as: + +Copyright (c) 2006-2018 SymPy Development Team, + 2013-2023 Sergey B Kirpichev + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + a. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + b. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + c. Neither the name of Diofant or SymPy nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +-------------------------------------------------------------------------------- + +Submodules taken from the multipledispatch project (https://github.com/mrocklin/multipledispatch) +are licensed as: + +Copyright (c) 2014 Matthew Rocklin + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + a. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + b. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + c. Neither the name of multipledispatch nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +-------------------------------------------------------------------------------- + +The files under the directory sympy/parsing/autolev/tests/pydy-example-repo +are directly copied from PyDy project and are licensed as: + +Copyright (c) 2009-2023, PyDy Authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of this project nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL PYDY AUTHORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +The files under the directory sympy/parsing/latex +are directly copied from latex2sympy project and are licensed as: + +Copyright 2016, latex2sympy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/METADATA b/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..02b6c7ac100d28e6f7c16a3c7a048c41fc59680a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/METADATA @@ -0,0 +1,34 @@ +Metadata-Version: 2.2 +Name: sympy +Version: 1.14.0+computecanada +Summary: Computer algebra system (CAS) in Python +Description-Content-Type: text/markdown +Keywords: Math CAS +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Scientific/Engineering +Classifier: Topic :: Scientific/Engineering :: Mathematics +Classifier: Topic :: Scientific/Engineering :: Physics +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Author: SymPy development team +Author-email: sympy@googlegroups.com +Home-page: https://sympy.org +Project-URL: Source, https://github.com/sympy/sympy +Requires-Python: >=3.9 +Requires-Dist: mpmath<1.4,>=1.1.0 +Requires-Dist: pytest>=7.1.0; extra == "dev" +Requires-Dist: hypothesis>=6.70.0; extra == "dev" +Provides-Extra: dev +License-File: LICENSE +License-File: AUTHORS + +BSD \ No newline at end of file diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/RECORD b/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..5d06c4989271542d251e814c82a6546e2f73c2f8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/RECORD @@ -0,0 +1,3105 @@ +../../../bin/isympy,sha256=akWVio1Q7hBm9ujgVl6Rh1ZXVx3WhKoERrqrDoieBmg,256 +../../../share/man/man1/isympy.1,sha256=9DZdSOIQLikrATHlbkdDZ04LBQigZDUE0_oCXBDvdBs,6659 +__pycache__/isympy.cpython-311.pyc,, +isympy.py,sha256=haEArczb_Ny-LyjOCwx2FNGal_S0Mo4d0deiWL2g_Gc,11220 +sympy-1.14.0+computecanada.dist-info/AUTHORS,sha256=grEDhSGwaWxjyQsx7UoKLJxMFoccZIsVewfKRLbxj40,55779 +sympy-1.14.0+computecanada.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +sympy-1.14.0+computecanada.dist-info/LICENSE,sha256=B6XpgZ9ye0mGrSgpx6KaYyDUJXX3IOsk1xt_71c6AoY,7885 +sympy-1.14.0+computecanada.dist-info/METADATA,sha256=53beh8Vwoj7nvcPr8_d5SFoIa5kcrCPvCXQbJUBirSo,1339 +sympy-1.14.0+computecanada.dist-info/RECORD,, +sympy-1.14.0+computecanada.dist-info/WHEEL,sha256=E4Ta9GSW8Vg2e11uZktRc054ObD9JXJ5hm6vpxg0WJE,87 +sympy-1.14.0+computecanada.dist-info/entry_points.txt,sha256=Sp-vLJom4PRlhGfY6RpUre7SjYm33JNq9NCwCGeW-fQ,39 +sympy-1.14.0+computecanada.dist-info/top_level.txt,sha256=elXb5xfjLdjgSSoQFk4_2Qu3lp2CIaglF9MQtfIoH7o,13 +sympy/__init__.py,sha256=TpR2NIuhBf6rKNgvW89s26Lj6E3m4Fm7_noTcowKSrA,29422 +sympy/__pycache__/__init__.cpython-311.pyc,, +sympy/__pycache__/abc.cpython-311.pyc,, +sympy/__pycache__/conftest.cpython-311.pyc,, +sympy/__pycache__/galgebra.cpython-311.pyc,, +sympy/__pycache__/release.cpython-311.pyc,, +sympy/__pycache__/this.cpython-311.pyc,, +sympy/abc.py,sha256=-RYLRKQ6SZMJGXDIAOP9UWPDAyvYH0zPbp9ZFg0YKBY,3764 +sympy/algebras/__init__.py,sha256=7PRGOW30nlMOTeUPR7iy8l5xGoE2yCBEfRbjqDKWOgU,62 +sympy/algebras/__pycache__/__init__.cpython-311.pyc,, +sympy/algebras/__pycache__/quaternion.cpython-311.pyc,, +sympy/algebras/quaternion.py,sha256=i2Fkvjj14vA6GtXgcSkpDwVmIYc7T5zT1hw49sb__tM,47483 +sympy/algebras/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/algebras/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/algebras/tests/__pycache__/test_quaternion.cpython-311.pyc,, +sympy/algebras/tests/test_quaternion.py,sha256=un4I6IKE2v6YEfUsFBSY5N6qaZ0agn1puX0LElb1k4E,16809 +sympy/assumptions/__init__.py,sha256=PFS8djTqiNbGVMjg7PaPjEfwmjyZVfioXiRVzqqA3E0,550 +sympy/assumptions/__pycache__/__init__.cpython-311.pyc,, +sympy/assumptions/__pycache__/ask.cpython-311.pyc,, +sympy/assumptions/__pycache__/ask_generated.cpython-311.pyc,, +sympy/assumptions/__pycache__/assume.cpython-311.pyc,, +sympy/assumptions/__pycache__/cnf.cpython-311.pyc,, +sympy/assumptions/__pycache__/facts.cpython-311.pyc,, +sympy/assumptions/__pycache__/lra_satask.cpython-311.pyc,, +sympy/assumptions/__pycache__/refine.cpython-311.pyc,, +sympy/assumptions/__pycache__/satask.cpython-311.pyc,, +sympy/assumptions/__pycache__/sathandlers.cpython-311.pyc,, +sympy/assumptions/__pycache__/wrapper.cpython-311.pyc,, +sympy/assumptions/ask.py,sha256=B2W8CW-9ynvYqfBX9_4qCLhCSwE0g_gUGqq7lM50wUk,19376 +sympy/assumptions/ask_generated.py,sha256=whNIU5tj2UkEGHPAfk-_89ahvVERs4npOB0JYVbIQJc,23558 +sympy/assumptions/assume.py,sha256=_gcFc4h_YGs9-tshoD0gmLl_RtPivDQWMWhWWLX9seo,14606 +sympy/assumptions/cnf.py,sha256=mfthFXL8Jhpn7Vgj1IfG5acFzfBgaV02xztQ7KpoLmg,12509 +sympy/assumptions/facts.py,sha256=fimPoHEyusSUr0uI4kiDb4mzxHjEBglvLQW0DGdNFAs,8391 +sympy/assumptions/handlers/__init__.py,sha256=lvjAfPdz0MDjTxjuzbBSGBco2OmpZRiGixSG0oaiZi0,330 +sympy/assumptions/handlers/__pycache__/__init__.cpython-311.pyc,, +sympy/assumptions/handlers/__pycache__/calculus.cpython-311.pyc,, +sympy/assumptions/handlers/__pycache__/common.cpython-311.pyc,, +sympy/assumptions/handlers/__pycache__/matrices.cpython-311.pyc,, +sympy/assumptions/handlers/__pycache__/ntheory.cpython-311.pyc,, +sympy/assumptions/handlers/__pycache__/order.cpython-311.pyc,, +sympy/assumptions/handlers/__pycache__/sets.cpython-311.pyc,, +sympy/assumptions/handlers/calculus.py,sha256=WF3gysxZa2frvd1kHxdDN7xd0PwYemER8dWutfDN-w8,7799 +sympy/assumptions/handlers/common.py,sha256=xgUR3xHPNgGFkLGyMqLV_nUIfMsQJLP8p6boy8Rc6JI,4314 +sympy/assumptions/handlers/matrices.py,sha256=Gdauk2xk1hKPRr4i6RpvOMHtDnyVD34x1OyhL-Oh8Hc,22321 +sympy/assumptions/handlers/ntheory.py,sha256=lRhW2ceJPK0gNqR00NAwkEq1FBjtRrxz1u6WevjRvjc,7660 +sympy/assumptions/handlers/order.py,sha256=-NCLn8ab7Fo_UzZLZi7m3UwtYGN-XiU1qvJDhi5z7ZQ,12280 +sympy/assumptions/handlers/sets.py,sha256=9P8X6ucARJIJA3jG_gj736L71ZQi5hfLCKL--0_ifrw,25841 +sympy/assumptions/lra_satask.py,sha256=FlmiLERsj6J9w6vygwEEEn7pyGPnD0JkPEFEdoE7bfM,9563 +sympy/assumptions/predicates/__init__.py,sha256=q1C7iWpvdDymEUZNyzJvZLsLtgwSkYtCixME-fYyIDw,110 +sympy/assumptions/predicates/__pycache__/__init__.cpython-311.pyc,, +sympy/assumptions/predicates/__pycache__/calculus.cpython-311.pyc,, +sympy/assumptions/predicates/__pycache__/common.cpython-311.pyc,, +sympy/assumptions/predicates/__pycache__/matrices.cpython-311.pyc,, +sympy/assumptions/predicates/__pycache__/ntheory.cpython-311.pyc,, +sympy/assumptions/predicates/__pycache__/order.cpython-311.pyc,, +sympy/assumptions/predicates/__pycache__/sets.cpython-311.pyc,, +sympy/assumptions/predicates/calculus.py,sha256=vFnlYVYZVd6D9OwA7-3bDK_Q0jf2iCZCZiMlWenw0Vg,1889 +sympy/assumptions/predicates/common.py,sha256=zpByACpa_tF0nVNB0J_rJehnXkHtkxhchn1DvkVVS-s,2279 +sympy/assumptions/predicates/matrices.py,sha256=X3vbkEf3zwJLyanEjf6ijYXuRfFfSv-yatl1tJ25wDk,12142 +sympy/assumptions/predicates/ntheory.py,sha256=wvFNFSf0S4egbY7REw0V0ANC03CuiRU9PLmdi16VfHo,2546 +sympy/assumptions/predicates/order.py,sha256=ez1UZ824KDtimLssUASCZHD_KEQmo8Pv-qofVLhZUrk,9511 +sympy/assumptions/predicates/sets.py,sha256=-bTVXa-X1-yfXlIKzMBW_JxIqueS5PdEwEzChzIne38,9238 +sympy/assumptions/refine.py,sha256=FNv5neAYJh-MgvnHDZ8-tjC9RIKcIxarVj5WiE4EnYg,11946 +sympy/assumptions/relation/__init__.py,sha256=t2tZNEIK7w-xXshRQIRL8tIyiNe1W5fMhN7QNRPnQFo,261 +sympy/assumptions/relation/__pycache__/__init__.cpython-311.pyc,, +sympy/assumptions/relation/__pycache__/binrel.cpython-311.pyc,, +sympy/assumptions/relation/__pycache__/equality.cpython-311.pyc,, +sympy/assumptions/relation/binrel.py,sha256=3iwnSEE53-vRsPv-bOnjydgOkCpbB12FTFR_sQ3CwvE,6313 +sympy/assumptions/relation/equality.py,sha256=ZBnSFpctNeroYy1nvam0kzSJCNpsUR3SlBeqFcAMM0U,7148 +sympy/assumptions/satask.py,sha256=P3iprPjuOyhT5Fwr0hX61xTOcD98M_bzXSAV-pXYhN4,11745 +sympy/assumptions/sathandlers.py,sha256=jx8B0u_N73fMoVoLKIfmXMdtSLz7-ZIKhJrxYl84AJk,9418 +sympy/assumptions/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/assumptions/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/assumptions/tests/__pycache__/test_assumptions_2.cpython-311.pyc,, +sympy/assumptions/tests/__pycache__/test_context.cpython-311.pyc,, +sympy/assumptions/tests/__pycache__/test_matrices.cpython-311.pyc,, +sympy/assumptions/tests/__pycache__/test_query.cpython-311.pyc,, +sympy/assumptions/tests/__pycache__/test_refine.cpython-311.pyc,, +sympy/assumptions/tests/__pycache__/test_rel_queries.cpython-311.pyc,, +sympy/assumptions/tests/__pycache__/test_satask.cpython-311.pyc,, +sympy/assumptions/tests/__pycache__/test_sathandlers.cpython-311.pyc,, +sympy/assumptions/tests/__pycache__/test_wrapper.cpython-311.pyc,, +sympy/assumptions/tests/test_assumptions_2.py,sha256=oNgIDOoW-GpBbXxbtw05SWnE8I7sGislYmB3MDogwB4,1070 +sympy/assumptions/tests/test_context.py,sha256=I5gES7AY9_vz1-CEaCchy4MXABtX85ncNkvoRuLskG8,1153 +sympy/assumptions/tests/test_matrices.py,sha256=nzSofuawc18hNe9Nj0dN_lTeDwa2KbPjt4K2rvb3xmw,12258 +sympy/assumptions/tests/test_query.py,sha256=ENB3XoNNi7j0p1_-j9Xd9C5YYpRbu1dcAQe46foV8cU,104495 +sympy/assumptions/tests/test_refine.py,sha256=bHxYUnCOEIzA1yPU3B2xbU9JZfhDv6RkmPm8esetisQ,8834 +sympy/assumptions/tests/test_rel_queries.py,sha256=fRn_IMMb942GfEH8CNk6z50YCyDlA_GMXv2xOVFdSdk,6677 +sympy/assumptions/tests/test_satask.py,sha256=IIqqIxzkLfANpTNBKEsCGCp3Bm8zmDnYd23woqKh9EE,15741 +sympy/assumptions/tests/test_sathandlers.py,sha256=jMCZQb3G6pVQ5MHaSTWV_0eULHaCF8Mowu12Ll72rgs,1842 +sympy/assumptions/tests/test_wrapper.py,sha256=iE32j83rrerCz85HHt2hTolgJkqb44KddfEpI3H1Fb8,1159 +sympy/assumptions/wrapper.py,sha256=7GXR39zPCCfV-pcs8ph9KRRwZF3i_T5Lzv156vKFf_I,5434 +sympy/benchmarks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/benchmarks/__pycache__/__init__.cpython-311.pyc,, +sympy/benchmarks/__pycache__/bench_discrete_log.cpython-311.pyc,, +sympy/benchmarks/__pycache__/bench_meijerint.cpython-311.pyc,, +sympy/benchmarks/__pycache__/bench_symbench.cpython-311.pyc,, +sympy/benchmarks/bench_discrete_log.py,sha256=CNchIJ5HFMPpNlVZh2vOU0GgQ3bse6hqyqDovpDHlKE,2473 +sympy/benchmarks/bench_meijerint.py,sha256=dSNdZhoc8a4h50wRtbOxLwpmgUiuMFpe6ytTLURcplY,11610 +sympy/benchmarks/bench_symbench.py,sha256=UMD3eYf_Poht0qxjdH2_axGwwON6cZo1Sp700Ci1M1M,2997 +sympy/calculus/__init__.py,sha256=IWDc6qPbEcWyTm9QM6V8vSAs-5OtGNijimykoWz3Clc,828 +sympy/calculus/__pycache__/__init__.cpython-311.pyc,, +sympy/calculus/__pycache__/accumulationbounds.cpython-311.pyc,, +sympy/calculus/__pycache__/euler.cpython-311.pyc,, +sympy/calculus/__pycache__/finite_diff.cpython-311.pyc,, +sympy/calculus/__pycache__/singularities.cpython-311.pyc,, +sympy/calculus/__pycache__/util.cpython-311.pyc,, +sympy/calculus/accumulationbounds.py,sha256=4nJD5JzBarlB5l6yPpVADX3iDamnHVwsor_nQJw9CAM,28682 +sympy/calculus/euler.py,sha256=0QrHD9TYKlSZuO8drnU3bUFJrSu8v5SncqtkRSWLjGM,3436 +sympy/calculus/finite_diff.py,sha256=X7qZJ5GmHlHKokUUMFoaQqrqX2jLRq4b7W2G5aWntzM,17053 +sympy/calculus/singularities.py,sha256=wBQ7WiJ1amuZStBJ-iMTiIHJexjzJHHwrc0tU2XVT10,12184 +sympy/calculus/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/calculus/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/calculus/tests/__pycache__/test_accumulationbounds.cpython-311.pyc,, +sympy/calculus/tests/__pycache__/test_euler.cpython-311.pyc,, +sympy/calculus/tests/__pycache__/test_finite_diff.cpython-311.pyc,, +sympy/calculus/tests/__pycache__/test_singularities.cpython-311.pyc,, +sympy/calculus/tests/__pycache__/test_util.cpython-311.pyc,, +sympy/calculus/tests/test_accumulationbounds.py,sha256=a_Ry2nKX5WbhSe1Bk2k0W6-VWOpVTg0FnA9u8rNSIV4,11195 +sympy/calculus/tests/test_euler.py,sha256=YWpts4pWSiYEwRsi5DLQ16JgC9109-9NKZIL_IO6_Aw,2683 +sympy/calculus/tests/test_finite_diff.py,sha256=9mIvXB8DaxJh2gzmhwi5eE3eBRYTFxyqYG5uIUpYg2w,7670 +sympy/calculus/tests/test_singularities.py,sha256=Zj4WPkT-KlXo7TF3Ir3ug3IkS_qhnFBS7VXAMnIuCso,5272 +sympy/calculus/tests/test_util.py,sha256=IJIhgudR9dym1VRAdu33G2tfSDoexNdWdz-Pgf-kh4o,18557 +sympy/calculus/util.py,sha256=qBDTlb5VP-xJPfh2Ndz37zNMVpseXW4d77hxBCLX5AA,28880 +sympy/categories/__init__.py,sha256=XiKBVC6pbDED-OVtNlSH-fGB8dB_jWLqwCEO7wBTAyA,984 +sympy/categories/__pycache__/__init__.cpython-311.pyc,, +sympy/categories/__pycache__/baseclasses.cpython-311.pyc,, +sympy/categories/__pycache__/diagram_drawing.cpython-311.pyc,, +sympy/categories/baseclasses.py,sha256=1Kn7PIegQCbF78s0rhf1Bx1mbxwfQcfQi6v-QqloSoE,31360 +sympy/categories/diagram_drawing.py,sha256=lfR29f6QX-rK-cCAom-2mRJ4xLyhi55ch91CyEPCXAQ,95173 +sympy/categories/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/categories/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/categories/tests/__pycache__/test_baseclasses.cpython-311.pyc,, +sympy/categories/tests/__pycache__/test_drawing.cpython-311.pyc,, +sympy/categories/tests/test_baseclasses.py,sha256=SwD6QsfSlrEdpD2dbkcN62CPVIRP5SadjCplLrMAoa8,5767 +sympy/categories/tests/test_drawing.py,sha256=IELPpadmnQyQ2x5a5qHC8ioq5kfT1UnAl4h1vO3gbqg,27848 +sympy/codegen/__init__.py,sha256=sQcJsyLyoRh9ccOPhv2eZ-wHjQrArByOON9ndj-MYgQ,974 +sympy/codegen/__pycache__/__init__.cpython-311.pyc,, +sympy/codegen/__pycache__/abstract_nodes.cpython-311.pyc,, +sympy/codegen/__pycache__/algorithms.cpython-311.pyc,, +sympy/codegen/__pycache__/approximations.cpython-311.pyc,, +sympy/codegen/__pycache__/ast.cpython-311.pyc,, +sympy/codegen/__pycache__/cfunctions.cpython-311.pyc,, +sympy/codegen/__pycache__/cnodes.cpython-311.pyc,, +sympy/codegen/__pycache__/cutils.cpython-311.pyc,, +sympy/codegen/__pycache__/cxxnodes.cpython-311.pyc,, +sympy/codegen/__pycache__/fnodes.cpython-311.pyc,, +sympy/codegen/__pycache__/futils.cpython-311.pyc,, +sympy/codegen/__pycache__/matrix_nodes.cpython-311.pyc,, +sympy/codegen/__pycache__/numpy_nodes.cpython-311.pyc,, +sympy/codegen/__pycache__/pynodes.cpython-311.pyc,, +sympy/codegen/__pycache__/pyutils.cpython-311.pyc,, +sympy/codegen/__pycache__/rewriting.cpython-311.pyc,, +sympy/codegen/__pycache__/scipy_nodes.cpython-311.pyc,, +sympy/codegen/abstract_nodes.py,sha256=TY4ecftqnym5viYInnb59zGPPFXdeSGQwi--xTz6Pvo,490 +sympy/codegen/algorithms.py,sha256=GEvnadOTZ3afVrbN5WE52OhCP8gzMII_AN4oHQxvEpo,6383 +sympy/codegen/approximations.py,sha256=r7zyShIWp40QngshFQwqKVKWcA5KmHmQcJarXVtOATQ,6448 +sympy/codegen/ast.py,sha256=R_JhC-uryx9NoJ1ISQU6fb3aV4aRL1SModmV_Dc2z2U,56788 +sympy/codegen/cfunctions.py,sha256=jrFWaRjrQnpHzu6-mwFtd6cRspK8Jo7B3RDo1AOoTLo,12331 +sympy/codegen/cnodes.py,sha256=lsqy-JeRvr9WCk2fwDiRqPhAMFk0snInF7WlAlk9-Zg,3409 +sympy/codegen/cutils.py,sha256=vlzMs8OkC5Bu4sIP-AF2mYf_tIo7Uo4r2DAI_LNhZzM,383 +sympy/codegen/cxxnodes.py,sha256=Om-EBfYduFF97tgXOF68rr8zYbngem9kBRm9SJiKLSM,342 +sympy/codegen/fnodes.py,sha256=mdOPkMVmjjNp22XfHscvx8uMtUxbUqTl66Wki7ORMa8,18936 +sympy/codegen/futils.py,sha256=k-mxMJKr_Q_afTy6NrKNl_N2XQLBmSdZAssO5hBonNY,1792 +sympy/codegen/matrix_nodes.py,sha256=0d3qXy2zaq3isyklE48lP7NP5LTF7SkLXMHMbweVGXU,2284 +sympy/codegen/numpy_nodes.py,sha256=H6FoBnarEuO7R1sbX8Qa9hK4jCju90R2qN66_-1N6Ug,4543 +sympy/codegen/pynodes.py,sha256=Neo1gFQ9kC31T-gH8TeeCaDDNaDe5deIP97MRZFgMHk,243 +sympy/codegen/pyutils.py,sha256=HfF6SP710Y7yExZcSesI0usVaDiWdEPEmMtyMD3JtOY,838 +sympy/codegen/rewriting.py,sha256=8JtiMFgv0sA7uGu2MUU7L3uzldGw5xG1ksuk4zh2ZDE,11585 +sympy/codegen/scipy_nodes.py,sha256=hYlxtGyTM0Z64Nazm1TeMZ3Y8dMsiD_HNhNvbU9eiQY,2508 +sympy/codegen/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/codegen/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/codegen/tests/__pycache__/test_abstract_nodes.cpython-311.pyc,, +sympy/codegen/tests/__pycache__/test_algorithms.cpython-311.pyc,, +sympy/codegen/tests/__pycache__/test_applications.cpython-311.pyc,, +sympy/codegen/tests/__pycache__/test_approximations.cpython-311.pyc,, +sympy/codegen/tests/__pycache__/test_ast.cpython-311.pyc,, +sympy/codegen/tests/__pycache__/test_cfunctions.cpython-311.pyc,, +sympy/codegen/tests/__pycache__/test_cnodes.cpython-311.pyc,, +sympy/codegen/tests/__pycache__/test_cxxnodes.cpython-311.pyc,, +sympy/codegen/tests/__pycache__/test_fnodes.cpython-311.pyc,, +sympy/codegen/tests/__pycache__/test_matrix_nodes.cpython-311.pyc,, +sympy/codegen/tests/__pycache__/test_numpy_nodes.cpython-311.pyc,, +sympy/codegen/tests/__pycache__/test_pynodes.cpython-311.pyc,, +sympy/codegen/tests/__pycache__/test_pyutils.cpython-311.pyc,, +sympy/codegen/tests/__pycache__/test_rewriting.cpython-311.pyc,, +sympy/codegen/tests/__pycache__/test_scipy_nodes.cpython-311.pyc,, +sympy/codegen/tests/test_abstract_nodes.py,sha256=a_GKf3FpeNN8zfMc-V8AaSrQtEI1oiLfJOco2VKiSKI,451 +sympy/codegen/tests/test_algorithms.py,sha256=lgl6w3d7WaT8LJrp716wWicxbgnR4hQk4alzBCdDrIw,6919 +sympy/codegen/tests/test_applications.py,sha256=RFWS7EoE3DytUrr4HzkPaXbOtnaStpzJalVKrY1rVRE,2277 +sympy/codegen/tests/test_approximations.py,sha256=SZpOUzahb_bJOceD0DLdmeiw-jN37OPmf5TRp1dyRgM,2035 +sympy/codegen/tests/test_ast.py,sha256=aAWk-yAVVNAmFMkyUlYBbVA8mPlTFqULOtmXMEi3LO8,21688 +sympy/codegen/tests/test_cfunctions.py,sha256=4jJM7dCLPbfEU3CB1ZUKGC1pRuRvjqZfDkFfYSkrzsY,5119 +sympy/codegen/tests/test_cnodes.py,sha256=FlI5XP39K3kC1QWKQ-QKkzNQw8TROjj5mKXJhK1UU2c,3039 +sympy/codegen/tests/test_cxxnodes.py,sha256=5OwN8D_ZtKN9z5uNeUwbUkyAGzNLrTgIKUlcRWmOSpE,366 +sympy/codegen/tests/test_fnodes.py,sha256=dKSw6w7i4Z8NHVDu_ug4V9Fa49CfnSX1FVahDIdNPgI,6650 +sympy/codegen/tests/test_matrix_nodes.py,sha256=CTMSwQkW0JHaMgHR9Lys8kDk5UgQidTl4VQhWI8gw7s,1896 +sympy/codegen/tests/test_numpy_nodes.py,sha256=tEeyF9vDmw-EyKnHV2dYM6MWGfA2I2fmf3kBsYxqad0,2199 +sympy/codegen/tests/test_pynodes.py,sha256=Gso18KKzSwA-1AHC55SgHPAfH1GrGUCGaN6QR7iuEO0,432 +sympy/codegen/tests/test_pyutils.py,sha256=yvqif7d6EpsnaBjP8XXjVo3wEENBxI6Vm01I1Wow-js,299 +sympy/codegen/tests/test_rewriting.py,sha256=ELPziNI3CsJ4VS7mUbk4QWyG_94FbgZCdBKieMN20Vc,15852 +sympy/codegen/tests/test_scipy_nodes.py,sha256=LBWpjTRfgWN5NLTchLZEp6m7IMtu7HbiKoztLc6KNGY,1495 +sympy/combinatorics/__init__.py,sha256=Dx9xakpHuTIgy4G8zVjAY6pTu8J9_K3d_jKPizRMdVo,1500 +sympy/combinatorics/__pycache__/__init__.cpython-311.pyc,, +sympy/combinatorics/__pycache__/coset_table.cpython-311.pyc,, +sympy/combinatorics/__pycache__/fp_groups.cpython-311.pyc,, +sympy/combinatorics/__pycache__/free_groups.cpython-311.pyc,, +sympy/combinatorics/__pycache__/galois.cpython-311.pyc,, +sympy/combinatorics/__pycache__/generators.cpython-311.pyc,, +sympy/combinatorics/__pycache__/graycode.cpython-311.pyc,, +sympy/combinatorics/__pycache__/group_constructs.cpython-311.pyc,, +sympy/combinatorics/__pycache__/group_numbers.cpython-311.pyc,, +sympy/combinatorics/__pycache__/homomorphisms.cpython-311.pyc,, +sympy/combinatorics/__pycache__/named_groups.cpython-311.pyc,, +sympy/combinatorics/__pycache__/partitions.cpython-311.pyc,, +sympy/combinatorics/__pycache__/pc_groups.cpython-311.pyc,, +sympy/combinatorics/__pycache__/perm_groups.cpython-311.pyc,, +sympy/combinatorics/__pycache__/permutations.cpython-311.pyc,, +sympy/combinatorics/__pycache__/polyhedron.cpython-311.pyc,, +sympy/combinatorics/__pycache__/prufer.cpython-311.pyc,, +sympy/combinatorics/__pycache__/rewritingsystem.cpython-311.pyc,, +sympy/combinatorics/__pycache__/rewritingsystem_fsm.cpython-311.pyc,, +sympy/combinatorics/__pycache__/schur_number.cpython-311.pyc,, +sympy/combinatorics/__pycache__/subsets.cpython-311.pyc,, +sympy/combinatorics/__pycache__/tensor_can.cpython-311.pyc,, +sympy/combinatorics/__pycache__/testutil.cpython-311.pyc,, +sympy/combinatorics/__pycache__/util.cpython-311.pyc,, +sympy/combinatorics/coset_table.py,sha256=soppBd8i6UV-sppdOzykfNE8evMeclU4l1TwfL5UlXI,43296 +sympy/combinatorics/fp_groups.py,sha256=0iLx7mtwxn1kjYFysgAzILAdtlS9e1VWPtWnszubhd8,47779 +sympy/combinatorics/free_groups.py,sha256=gt33RoIByKDZNaf661ay4pQMilPPMPY4hFNGIKi2ho0,39740 +sympy/combinatorics/galois.py,sha256=cpd2iHaSdim5-3-cqvk58YP-AuS6CvJeCdUWpiOdIZI,17867 +sympy/combinatorics/generators.py,sha256=UAdv0bvqtN6i9bfFgB873g-A-zssc0ZoiHckDOEI0M0,7479 +sympy/combinatorics/graycode.py,sha256=xbtr8AaFYb4SMmwUi7mf7913U87jH-XEYF_3pGZfj0o,11207 +sympy/combinatorics/group_constructs.py,sha256=IKx12_yWJqEQ7g-oBuAWd5VRLbCOWyL0LG4PQu43BS8,2021 +sympy/combinatorics/group_numbers.py,sha256=eOiK0RyVHewiGiPXHihfM7MIKlAlg7AhGnBL4f1D54M,9163 +sympy/combinatorics/homomorphisms.py,sha256=dCpmPM3V2ReRuYDdXDfdMTU3pt7zkjKBkYSF6X2MfE8,18844 +sympy/combinatorics/named_groups.py,sha256=zd_C9epKDrMG0drafGUcHuuJJkcMaDt1Nf2ik4NXNq8,8378 +sympy/combinatorics/partitions.py,sha256=ZXqVmVNjmauhMeiTWtCCqOP38b9MJg7UlBdZa-7aICQ,20841 +sympy/combinatorics/pc_groups.py,sha256=5AwEeYQCMdYcybhlEykvJZqEYtJQLgYbRxAnNQeajfE,21414 +sympy/combinatorics/perm_groups.py,sha256=sf0FfxmJ5uWCwlvzRD5m5HHduG5qqHzh3Y0n1Dax5Yo,184825 +sympy/combinatorics/permutations.py,sha256=nQoc4YgTeoT80eHkqetTBUxwAQQW40US2AnR2u4-E_k,87737 +sympy/combinatorics/polyhedron.py,sha256=-1y5GhorUK62_gJpn4tXTLye7BcG0hAup74waDQ8y2I,35928 +sympy/combinatorics/prufer.py,sha256=yN6d4w_ZVXNFhBoevA84gor4Xb5ttG529xbVgHxzKDo,12061 +sympy/combinatorics/rewritingsystem.py,sha256=cT1JrAuKj9rWI3IhaHekYYt0rdG56pwFLg32pcGC9aI,17095 +sympy/combinatorics/rewritingsystem_fsm.py,sha256=CKGhLqyvxY0mlmy8_Hb4WzkSdWYPUaU2yZYhz-0iZ5w,2433 +sympy/combinatorics/schur_number.py,sha256=YdsyA7n_z9tyfRTSRfIjEjtnGo5EuDGBMUS09AQ2MxU,4437 +sympy/combinatorics/subsets.py,sha256=oxuExuGyFnvunkmktl-vBYiLbiN66A2Q2MyzwWfy46A,16047 +sympy/combinatorics/tensor_can.py,sha256=JywytVypkN6aWdCpeEYletMW4vxyUvU29RfKq9-Lu0c,40738 +sympy/combinatorics/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/combinatorics/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_coset_table.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_fp_groups.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_free_groups.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_galois.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_generators.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_graycode.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_group_constructs.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_group_numbers.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_homomorphisms.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_named_groups.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_partitions.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_pc_groups.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_perm_groups.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_permutations.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_polyhedron.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_prufer.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_rewriting.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_schur_number.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_subsets.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_tensor_can.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_testutil.cpython-311.pyc,, +sympy/combinatorics/tests/__pycache__/test_util.cpython-311.pyc,, +sympy/combinatorics/tests/test_coset_table.py,sha256=cEUF0OH6SNhN_kh069wMsq6h4eSVqbDLghrg2r9Ht48,28474 +sympy/combinatorics/tests/test_fp_groups.py,sha256=C4rWnSJyk7L6uZnI-kAYvtbfFXxba0o1XjpOTkWr97s,10203 +sympy/combinatorics/tests/test_free_groups.py,sha256=g47dnRsNXVG9lFcqppJyrGryGIUCw9dRASkcMa4QujA,6411 +sympy/combinatorics/tests/test_galois.py,sha256=w35JRx8lmlXCdzUBNdocgATPYWBOEZ6LH-tAxOPwCQ8,2763 +sympy/combinatorics/tests/test_generators.py,sha256=6YpOp0i5PRGtySPNZseQ8mjSXbwpfGfz0hDB4kfk40Q,3567 +sympy/combinatorics/tests/test_graycode.py,sha256=pI4e7Y615d5Bmmxui6fdEeyca6j6KSD0YmeychV6ORk,2800 +sympy/combinatorics/tests/test_group_constructs.py,sha256=jJLwMdhuUalKv4Aql9SzV2utK8Ex-IYdMecggr95pi8,450 +sympy/combinatorics/tests/test_group_numbers.py,sha256=spHwsvjzEDXsdAFxkrQYOnioTgtWoVF5K7k7FbgMvfg,4149 +sympy/combinatorics/tests/test_homomorphisms.py,sha256=UwBj5loCuZAiuvmqy5VAbwhCQTph8o6BzTaGrH0rzB4,3745 +sympy/combinatorics/tests/test_named_groups.py,sha256=tsuDVGv4iHGEZ0BVR87_ENhyAfZvFIl0M6Dv_HX1VoY,1931 +sympy/combinatorics/tests/test_partitions.py,sha256=oppszKJLLSpcEzHgespIveSmEC3fDZ0qkus1k7MBt4E,4097 +sympy/combinatorics/tests/test_pc_groups.py,sha256=wfkY_ilpG0XWrhaWMVK6r7yWMeXfM8WNTyti5oE9bdk,2728 +sympy/combinatorics/tests/test_perm_groups.py,sha256=70_AedMpr-YxiYDHHjw6fjvmYW7-YoznEGwUeGtJo0E,41195 +sympy/combinatorics/tests/test_permutations.py,sha256=E8J-WLCW2z9hTyTJcm1dsFgFXh8YAesIppYsXRu5pAs,20189 +sympy/combinatorics/tests/test_polyhedron.py,sha256=3SWkFQKeF-p1QWP4Iu9NIA1oTxAFo1BLRrrLerBFAhw,4180 +sympy/combinatorics/tests/test_prufer.py,sha256=OTJp0NxjiVswWkOuCIlnGFU2Gw4noRsrPpUJtp2XhEs,2649 +sympy/combinatorics/tests/test_rewriting.py,sha256=3COHq74k6knt2rqE7hfd4ZP_6whf0Kg14tYxFmTtYrI,1787 +sympy/combinatorics/tests/test_schur_number.py,sha256=wg13uTumFltWIGbVg_PEr6nhXIru19UWitsEZiakoRI,1727 +sympy/combinatorics/tests/test_subsets.py,sha256=6pyhLYV5HuXvx63r-gGVHr8LSrGRXcpDudhFn9fBqX8,2635 +sympy/combinatorics/tests/test_tensor_can.py,sha256=olH5D5wwTBOkZXjtqvLO6RKbvCG9KoMVK4__wDe95N4,24676 +sympy/combinatorics/tests/test_testutil.py,sha256=NYEI8X2qLWf7Gv1O7UthOHUSyWpbNCRrGmG_vdyEgF8,1733 +sympy/combinatorics/tests/test_util.py,sha256=sOYMWHxlbM0mqalqA7jNrYMm8DKcf_GwL5YBjs96_C4,4499 +sympy/combinatorics/testutil.py,sha256=GNnqy0mb6yPMa3zpGEzz2p6uxY7VtobPtwUialhfYEQ,11142 +sympy/combinatorics/util.py,sha256=lkOaITBImqB9yyLvN8DU0G-vraw27cSx2XaPdAPVBhg,16296 +sympy/concrete/__init__.py,sha256=2HDmg3VyLgM_ZPw3XsGpkOClGiQnyTlUNHSwVTtizA0,144 +sympy/concrete/__pycache__/__init__.cpython-311.pyc,, +sympy/concrete/__pycache__/delta.cpython-311.pyc,, +sympy/concrete/__pycache__/expr_with_intlimits.cpython-311.pyc,, +sympy/concrete/__pycache__/expr_with_limits.cpython-311.pyc,, +sympy/concrete/__pycache__/gosper.cpython-311.pyc,, +sympy/concrete/__pycache__/guess.cpython-311.pyc,, +sympy/concrete/__pycache__/products.cpython-311.pyc,, +sympy/concrete/__pycache__/summations.cpython-311.pyc,, +sympy/concrete/delta.py,sha256=38M0E24b6V-dn706WXnylM4DAvvyb6tFy7eRE4oR5Bo,9870 +sympy/concrete/expr_with_intlimits.py,sha256=vj4PjttB9xE5aUYu37R1A4_KtGgxcPa65jzjv8-krsc,11352 +sympy/concrete/expr_with_limits.py,sha256=dL5u-b_CzwghTWIhNsGc4md8jPWfhXOvna5Lig6XVr0,21834 +sympy/concrete/gosper.py,sha256=JUFbnOWSls8Xo0Zj198Xm4adopE-rAJjXv6M2q9x7us,5499 +sympy/concrete/guess.py,sha256=6EejdhNgqjMK7lVTXms8-_xGmmpI3vtliLW7-L2eaDM,17474 +sympy/concrete/products.py,sha256=I_aGMeKtSuFU07PGfZPHHTwS1WxDRNbnjedNC_m1k24,18456 +sympy/concrete/summations.py,sha256=D5n4x-fzqhgRwTIWpdN3Xun4I8u847tB7Dcex7lh7lI,55998 +sympy/concrete/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/concrete/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/concrete/tests/__pycache__/test_delta.cpython-311.pyc,, +sympy/concrete/tests/__pycache__/test_gosper.cpython-311.pyc,, +sympy/concrete/tests/__pycache__/test_guess.cpython-311.pyc,, +sympy/concrete/tests/__pycache__/test_products.cpython-311.pyc,, +sympy/concrete/tests/__pycache__/test_sums_products.cpython-311.pyc,, +sympy/concrete/tests/test_delta.py,sha256=uI7xjMx7JuVb3kkN7cLR6_pGsKS4Ulq22p-Z9oti5Jc,23869 +sympy/concrete/tests/test_gosper.py,sha256=ZHiZfYGCeCS9I-0oqN6sFbiYa-284GeFoGsNbhIWq4I,7987 +sympy/concrete/tests/test_guess.py,sha256=TPW6Hy11Po6VLZG_dx95x3sMBYl5kcQH8wjJ6TOtu-k,3370 +sympy/concrete/tests/test_products.py,sha256=caYc-xlEIrX9I_A-KPQdwp5oDprVJSbfcOaKg_qUnsM,14521 +sympy/concrete/tests/test_sums_products.py,sha256=rg0cSziPNgkvyehA0wCf-3jj8fSmNT7ECOOkiXWN64o,66390 +sympy/conftest.py,sha256=wvibr2FiWTlUcLTYAFEL8tSAKWPnv_Q6Cj4lARBCM0U,2775 +sympy/core/__init__.py,sha256=0L72TGngrIg2JknW3elaPSIDmkpPjWGNVfNk33wKXJ0,3123 +sympy/core/__pycache__/__init__.cpython-311.pyc,, +sympy/core/__pycache__/_print_helpers.cpython-311.pyc,, +sympy/core/__pycache__/add.cpython-311.pyc,, +sympy/core/__pycache__/alphabets.cpython-311.pyc,, +sympy/core/__pycache__/assumptions.cpython-311.pyc,, +sympy/core/__pycache__/assumptions_generated.cpython-311.pyc,, +sympy/core/__pycache__/backend.cpython-311.pyc,, +sympy/core/__pycache__/basic.cpython-311.pyc,, +sympy/core/__pycache__/cache.cpython-311.pyc,, +sympy/core/__pycache__/compatibility.cpython-311.pyc,, +sympy/core/__pycache__/containers.cpython-311.pyc,, +sympy/core/__pycache__/core.cpython-311.pyc,, +sympy/core/__pycache__/coreerrors.cpython-311.pyc,, +sympy/core/__pycache__/decorators.cpython-311.pyc,, +sympy/core/__pycache__/evalf.cpython-311.pyc,, +sympy/core/__pycache__/expr.cpython-311.pyc,, +sympy/core/__pycache__/exprtools.cpython-311.pyc,, +sympy/core/__pycache__/facts.cpython-311.pyc,, +sympy/core/__pycache__/function.cpython-311.pyc,, +sympy/core/__pycache__/intfunc.cpython-311.pyc,, +sympy/core/__pycache__/kind.cpython-311.pyc,, +sympy/core/__pycache__/logic.cpython-311.pyc,, +sympy/core/__pycache__/mod.cpython-311.pyc,, +sympy/core/__pycache__/mul.cpython-311.pyc,, +sympy/core/__pycache__/multidimensional.cpython-311.pyc,, +sympy/core/__pycache__/numbers.cpython-311.pyc,, +sympy/core/__pycache__/operations.cpython-311.pyc,, +sympy/core/__pycache__/parameters.cpython-311.pyc,, +sympy/core/__pycache__/power.cpython-311.pyc,, +sympy/core/__pycache__/random.cpython-311.pyc,, +sympy/core/__pycache__/relational.cpython-311.pyc,, +sympy/core/__pycache__/rules.cpython-311.pyc,, +sympy/core/__pycache__/singleton.cpython-311.pyc,, +sympy/core/__pycache__/sorting.cpython-311.pyc,, +sympy/core/__pycache__/symbol.cpython-311.pyc,, +sympy/core/__pycache__/sympify.cpython-311.pyc,, +sympy/core/__pycache__/trace.cpython-311.pyc,, +sympy/core/__pycache__/traversal.cpython-311.pyc,, +sympy/core/_print_helpers.py,sha256=GQo9dI_BvAJtYHVFFfmroNr0L8d71UeI-tU7SGJgctk,2388 +sympy/core/add.py,sha256=TddwsicZFR4yEtVTkZmkzFtn9ndAlhfad7vJiECr6o4,43407 +sympy/core/alphabets.py,sha256=vWBs2atOvfRK6Xfg6hc5IKiB7s_0sZIiVJpcCUJL0N4,266 +sympy/core/assumptions.py,sha256=8K9rhYtT-kqS7hmc9ltkYC-wujYT3pe0BaUJNrOr8fg,23595 +sympy/core/assumptions_generated.py,sha256=0TJKYIHSIFyQcVHZdIHZ19b7tqst_sY7iZwjKzcvZBM,42817 +sympy/core/backend.py,sha256=hF9cftjzB92QEcwimekj0gQ9CEMXTH-leH3l8QcguGo,5273 +sympy/core/basic.py,sha256=IzZvs7iIcMvV8_athdOBnIvBYJg38khuO2fAJtd7uTE,76789 +sympy/core/benchmarks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/core/benchmarks/__pycache__/__init__.cpython-311.pyc,, +sympy/core/benchmarks/__pycache__/bench_arit.cpython-311.pyc,, +sympy/core/benchmarks/__pycache__/bench_assumptions.cpython-311.pyc,, +sympy/core/benchmarks/__pycache__/bench_basic.cpython-311.pyc,, +sympy/core/benchmarks/__pycache__/bench_expand.cpython-311.pyc,, +sympy/core/benchmarks/__pycache__/bench_numbers.cpython-311.pyc,, +sympy/core/benchmarks/__pycache__/bench_sympify.cpython-311.pyc,, +sympy/core/benchmarks/bench_arit.py,sha256=gfrnvKSXLCaUoFFxMgJhnLUp7rG9Pa_YT7OKgOrPP8E,412 +sympy/core/benchmarks/bench_assumptions.py,sha256=evfZzTgOUUvvvlK0DRdDZQRqxIlGLfJYzKu8QDMxSks,177 +sympy/core/benchmarks/bench_basic.py,sha256=YF0tTJ_AN_Wz11qidzM4bIhlwEhEqVc-IGVGrUx6SaA,210 +sympy/core/benchmarks/bench_expand.py,sha256=xgQYQMwqgXJtKajM4JVhuL-7AW8TLY-vdBpO6uyMDoQ,427 +sympy/core/benchmarks/bench_numbers.py,sha256=_1yHSbYmxriiTTzKtxFh7kJm-rXPL6roGs8MW1E5-sg,1135 +sympy/core/benchmarks/bench_sympify.py,sha256=G5iGInhhbkkxSY2pS08BNG945m9m4eZlNT1aJutGt5M,138 +sympy/core/cache.py,sha256=AyG7kganyV0jVx-aNBEUFogqRLHQqqFn8xU3ZSfJoaM,6172 +sympy/core/compatibility.py,sha256=XQH7ezmRi6l3R23qMHN2wfA-YMRWbh2YYjPY7LRo3lo,1145 +sympy/core/containers.py,sha256=IA8PWQeCl78Pn_1WczoYUzOhBoT9X2hM3_hJ232gyKc,11411 +sympy/core/core.py,sha256=lX3Af31Qyff-gwPiAH-vijjuOzBFSuQtN06nUqIcSTQ,547 +sympy/core/coreerrors.py,sha256=imD9TCL-UEM5hrHcqGQbbfLeyZCSJMx6vB1NBQITQIQ,642 +sympy/core/decorators.py,sha256=NO3oe7yIO4q-6Vvy2vTpIAS8GGI-JHjEOEGeAv97voY,8759 +sympy/core/evalf.py,sha256=X0BinrUYeCKyHPHJQnu3FGASTUB8iz5dweVvqeArBrU,62089 +sympy/core/expr.py,sha256=HR_xzh6273FBmdfNbogGzkWoqaI_k_CeIOnpmyzwCrQ,143770 +sympy/core/exprtools.py,sha256=Ft7C1K2kD4_EdpRrGxii_WDub4pK_rYnLTdeVhlBoeo,51544 +sympy/core/facts.py,sha256=54pFKhJwEzU8LkO7rL25TwGjIb5y5CvZleHEy_TpD68,19546 +sympy/core/function.py,sha256=y0vEdXaUD8thP3XasZVkkogqXOGB0cs2JnfHEa4DNjQ,117102 +sympy/core/intfunc.py,sha256=TigJI9bvsb4aN0aZJgCzxIHlv_eh0rElWeTurier3yI,14219 +sympy/core/kind.py,sha256=9kQvtDxm-SSRGi-155XsBl_rs-oN_7dw7fNNT3mDu2Q,11540 +sympy/core/logic.py,sha256=Qbj2WUh5lS0GjxvtPaNmcQXz7mbWXuWj5ZNFZ-N-YPg,10827 +sympy/core/mod.py,sha256=XDfpth2r4AWc04oLqtDWwFuYRLzI0l0MhLMwGSMu99g,8363 +sympy/core/mul.py,sha256=rjCiyOqxMgUTWR8sAcmilRfQLup9wv8Ij7s7JIfUkVM,79201 +sympy/core/multidimensional.py,sha256=NWX1okybO_nZCl9IhIOE8QYalY1WoC0zlzsvBg_E1eE,4233 +sympy/core/numbers.py,sha256=WEkS1JJ9ov-n4hXvxwrH1NPikNlyBCjLN5v-jOZxvsw,136741 +sympy/core/operations.py,sha256=_wjhMX-MD6cyUNKCc9K_QxyxpeG4WQehdypo0Fp76GM,25828 +sympy/core/parameters.py,sha256=EoT2S3W1dS2-HoV6WN7szBexXvn5_w43e2JFouKuvkU,3854 +sympy/core/power.py,sha256=FJSYRf0S3pk1JGkVy3kdWWQENhmpVme-Pf9SS2Rc6SE,73244 +sympy/core/random.py,sha256=miFdVpNKfutbkpYiIOzG9kVNUm5GTk-_nnmQqUhVDZs,6647 +sympy/core/relational.py,sha256=htbGT0uvzoYD07Q6_hdT0iDDsns0NDI2aml0W8mtAgM,51896 +sympy/core/rules.py,sha256=AJuZztmYKZ_yUITLZB6rhZjDy6ROBCtajcYqPa50sjc,1496 +sympy/core/singleton.py,sha256=2Ueja8GJ-0-cvBzlqPwL9hB9eOtjrJFXV6mkDZO1Bp4,7115 +sympy/core/sorting.py,sha256=PoL2-MtVeuYTu-DISGlbvqW2mt787BmWfzrwV1ibavE,10827 +sympy/core/symbol.py,sha256=0xwgxT7ZfhPjq87glw2CQq5MtIEO-DOrAoXu7bHwWFo,29820 +sympy/core/sympify.py,sha256=1vWheH469HqICHrADMY_k4T4eDQlGORNjEh7Re4WbmE,20546 +sympy/core/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/core/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_args.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_arit.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_assumptions.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_basic.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_cache.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_compatibility.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_complex.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_constructor_postprocessor.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_containers.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_count_ops.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_diff.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_equal.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_eval.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_evalf.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_expand.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_expr.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_exprtools.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_facts.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_function.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_kind.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_logic.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_match.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_multidimensional.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_noncommutative.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_numbers.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_operations.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_parameters.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_power.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_priority.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_random.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_relational.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_rules.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_singleton.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_sorting.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_subs.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_symbol.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_sympify.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_traversal.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_truediv.cpython-311.pyc,, +sympy/core/tests/__pycache__/test_var.cpython-311.pyc,, +sympy/core/tests/test_args.py,sha256=gsvYkFoqSWM0G8sjwBkwwNpT_9jc-io1SyDcchI3fA4,184252 +sympy/core/tests/test_arit.py,sha256=TH4b9Z_sMGWlYqiU5MqpY5LmhzYRJSTHWtTNIrYv7fk,78611 +sympy/core/tests/test_assumptions.py,sha256=MjJdF_ymVL6mtgQx-aSr_rsNNxaTi2pHFLjyaPCBq5Q,41573 +sympy/core/tests/test_basic.py,sha256=skFOWZRNqDujmQ8GAskWH7OYcsrO0miBdM92Gy01k0U,10230 +sympy/core/tests/test_cache.py,sha256=p6Ci75a_T-bBXE_5HVxRKla62uSay_0Vuf57gUuH6sI,2001 +sympy/core/tests/test_compatibility.py,sha256=7pvNUEGIcRrfWl3doqHlm3AdNkGlcChO69gos3Fk09A,240 +sympy/core/tests/test_complex.py,sha256=koNGFMt6UMmzahJADSja_eD24gr-GG5gGCtyDgCRtPI,21906 +sympy/core/tests/test_constructor_postprocessor.py,sha256=0d7vbVuKi3GCm3PKLtiNqv_Au7v6RYt1rzRdHiD08tM,2441 +sympy/core/tests/test_containers.py,sha256=ijteBC6cjqzejfxXZZIELZJfMrgTQa1n6AlxHGCEQJs,7432 +sympy/core/tests/test_count_ops.py,sha256=eIA2WvCuWKXVBJEGfWoJrn6WfUshX_NXttrrfyLbNnI,5665 +sympy/core/tests/test_diff.py,sha256=6j4Vk9UCNRv8Oyx_4iv1ePjocwBg7_-3ftrSJ8u0cPo,5421 +sympy/core/tests/test_equal.py,sha256=RoOJuu4kMe4Rkk7eNyVOJov5S1770YHiVAiziNIKd2o,1678 +sympy/core/tests/test_eval.py,sha256=o0kZn3oaMidVYdNjeZYtx4uUKBoE3A2tWn2NS4hu72Q,2366 +sympy/core/tests/test_evalf.py,sha256=aIf2xMuFomr828s4qv6Q16xLHWNmBnDfC4FUv9g619M,28574 +sympy/core/tests/test_expand.py,sha256=jbIjBEmdsPPArsxJ9206YzMS7mPUVZo7j-7alM795eU,13886 +sympy/core/tests/test_expr.py,sha256=249jMUV5GnkjmVVcgPSkwWvzrT00ppWeygkK_9RO6kc,78428 +sympy/core/tests/test_exprtools.py,sha256=L7fi319z1EeFag6pH8myqDQYQ32H193QLKMdqlxACsY,19021 +sympy/core/tests/test_facts.py,sha256=YEZMZ-116VFnFqJ48h9bQsF2flhiB65trnZvJsRSh_o,11579 +sympy/core/tests/test_function.py,sha256=_YRRGTDthS85Bq4zoLo8bHMHNESPLUZW-4_VQaClCLs,51450 +sympy/core/tests/test_kind.py,sha256=NLJbwCpugzlNbaSyUlbb6NHoT_9dHuoXj023EDQMrNI,2048 +sympy/core/tests/test_logic.py,sha256=_YKSIod6Q0oIz9lDs78UQQrv9LU-uKaztd7w8LWwuwY,5634 +sympy/core/tests/test_match.py,sha256=yjVNccCLutvov8Ru_p38Hl_HDS0ltdsWierSi54m5Ts,22714 +sympy/core/tests/test_multidimensional.py,sha256=Fr-lagme3lwLrBpdaWP7O7oPezhIatn5X8fYYs-8bN8,848 +sympy/core/tests/test_noncommutative.py,sha256=IkGPcvLO4ACVj5LMT2IUgyj68F1RBvMKbm01iqTOK04,4436 +sympy/core/tests/test_numbers.py,sha256=SvTVZUpMWfeGvaqyJUp4e9FYIO8lFJLcbRcmBNWO8m0,78001 +sympy/core/tests/test_operations.py,sha256=mRxftKlrxxrn3zS3UPwqkF6Nr15l5Cv6j3c2RJX46s4,2859 +sympy/core/tests/test_parameters.py,sha256=wO9D-LcMMEyf5u5-EmDwVeQ02YzYbYwtFFR_o-M4ybQ,3560 +sympy/core/tests/test_power.py,sha256=H7SFxpWVnF4vgpF95ad-Fo5xHhIvKhtZJuGBg0J2SAk,24862 +sympy/core/tests/test_priority.py,sha256=6QlWz52qWwOU0CknBb4KErs4R87jp4wH3hVSQNAgdVI,3252 +sympy/core/tests/test_random.py,sha256=H58NfH5BYeQ3RIscbDct6SZkHQVRJjichVUSuSrhvAU,1233 +sympy/core/tests/test_relational.py,sha256=7ne7GIG_i0uVT8FfpCJ4ixnuIyQoVy-PwJm3kYNWbKI,43525 +sympy/core/tests/test_rules.py,sha256=iwmMX7hxC_73CuX9BizeAci-cO4JDq-y1sicKBXEGA4,349 +sympy/core/tests/test_singleton.py,sha256=xLJJgXwmkbKhsot_qTs-o4dniMjHUh3_va0xsA5h-KA,3036 +sympy/core/tests/test_sorting.py,sha256=6BZKYqUedAR-jeHcIgsJelJHFWuougml2c1NNilxGZg,902 +sympy/core/tests/test_subs.py,sha256=7ITJFDplgWBRImkcHfjRdnHqaKgjTxWb4j4WoRysvR8,30106 +sympy/core/tests/test_symbol.py,sha256=zYhPWsdyQp7_NiLVthpoCB1RyP9pmJcNlTdTN2kMdfY,13043 +sympy/core/tests/test_sympify.py,sha256=FEOsRduYX3EqE9EhMILwE9wu6jDj_o2WU9XEZHsXFUo,28195 +sympy/core/tests/test_traversal.py,sha256=cmgvMW8G-LZ20ZXy-wg5Vz5ogI_oq2p2bJSwMy9IMF0,4311 +sympy/core/tests/test_truediv.py,sha256=RYfJX39-mNhekRE3sj5TGFZXKra4ML9vGvObsRYuD3k,854 +sympy/core/tests/test_var.py,sha256=hexP-0q2nN9h_dyhKLCuvqFXgLC9e_Hroni8Ldb16Ko,1594 +sympy/core/trace.py,sha256=9WC8p3OpBL6TdHmZWMDK9jaCG-16f4uZV2VptduVH98,348 +sympy/core/traversal.py,sha256=PP4HUp-2gtsoCtGZwqxpbqZ33vtoRX-yYTbIqOQiaR0,8860 +sympy/crypto/__init__.py,sha256=i8GcbScXhIPbMEe7uuMgXqh_cU2mZm2f6hspIgmW5uM,2158 +sympy/crypto/__pycache__/__init__.cpython-311.pyc,, +sympy/crypto/__pycache__/crypto.cpython-311.pyc,, +sympy/crypto/crypto.py,sha256=iAERU5Qf9GizEUMUyxTgTUBI0U9QpSkbRir2nvKIKUA,89685 +sympy/crypto/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/crypto/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/crypto/tests/__pycache__/test_crypto.cpython-311.pyc,, +sympy/crypto/tests/test_crypto.py,sha256=gPsRBNNHwc4QDBjgT5tvWTCuC5IA5d4aZRTblxXjngk,19758 +sympy/diffgeom/__init__.py,sha256=cWj4N7AfNgrYcGIBexX-UrWxfd1bP9DTNqUmLWUJ9nA,991 +sympy/diffgeom/__pycache__/__init__.cpython-311.pyc,, +sympy/diffgeom/__pycache__/diffgeom.cpython-311.pyc,, +sympy/diffgeom/__pycache__/rn.cpython-311.pyc,, +sympy/diffgeom/diffgeom.py,sha256=DgxgxP9B7NT-M1Hb1-1AwHYHBvXZk_Y6_MqZcGPsmOg,72274 +sympy/diffgeom/rn.py,sha256=kvgth6rNJWt94kzVospZwiH53C-s4VSiorktQNmMobQ,6264 +sympy/diffgeom/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/diffgeom/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/diffgeom/tests/__pycache__/test_class_structure.cpython-311.pyc,, +sympy/diffgeom/tests/__pycache__/test_diffgeom.cpython-311.pyc,, +sympy/diffgeom/tests/__pycache__/test_function_diffgeom_book.cpython-311.pyc,, +sympy/diffgeom/tests/__pycache__/test_hyperbolic_space.cpython-311.pyc,, +sympy/diffgeom/tests/test_class_structure.py,sha256=LbRyxhhp-NnnfJ2gTn1SdlgCBQn2rhyB7xApOgcd_rM,1048 +sympy/diffgeom/tests/test_diffgeom.py,sha256=3BepCr6ned-4C_3me4zScu06HXG9Qx_dBBxIpiXAvy4,14145 +sympy/diffgeom/tests/test_function_diffgeom_book.py,sha256=GwhUAiAPtUv5I9oghdElMhtc6KX184tySgLSpVHhT0Q,5254 +sympy/diffgeom/tests/test_hyperbolic_space.py,sha256=c4xQJ_bBS4xrMj3pfx1Ms3oC2_LwuJuNYXNZxs-cVG8,2598 +sympy/discrete/__init__.py,sha256=A_Seud0IRr2gPYlz6JMQZa3sBhRL3O7gVqhIvMRRvE0,772 +sympy/discrete/__pycache__/__init__.cpython-311.pyc,, +sympy/discrete/__pycache__/convolutions.cpython-311.pyc,, +sympy/discrete/__pycache__/recurrences.cpython-311.pyc,, +sympy/discrete/__pycache__/transforms.cpython-311.pyc,, +sympy/discrete/convolutions.py,sha256=9L2d2Rrn6jqGfV2lBxCV6LmcTNBZUuOIqP_fuMIzPzk,18341 +sympy/discrete/recurrences.py,sha256=FqU5QG4qNNLSVBqcpL7HtKa7rQOlmHMXDQRzHZ_P_s0,5124 +sympy/discrete/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/discrete/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/discrete/tests/__pycache__/test_convolutions.cpython-311.pyc,, +sympy/discrete/tests/__pycache__/test_recurrences.cpython-311.pyc,, +sympy/discrete/tests/__pycache__/test_transforms.cpython-311.pyc,, +sympy/discrete/tests/test_convolutions.py,sha256=K4S9bA1E2tg0VcFQ8SYtlxjL6HB666RxD5rriTuAK4k,17856 +sympy/discrete/tests/test_recurrences.py,sha256=s5ZEZQ262gcnBLpCjJVmeKlTKQByRTQBrc-N9p_4W8c,3019 +sympy/discrete/tests/test_transforms.py,sha256=vEORFaPvxmPSsw0f4Z2hLEN1wD0FdyQOYHDEY9aVm5A,5546 +sympy/discrete/transforms.py,sha256=lf-n6IN881uCfTUAxPNjdUaSguiRbYW0omuR96vKNlE,11681 +sympy/external/__init__.py,sha256=C6s4654Elc_X-D9UgI2cUQWiQyGDt9LG3IKUc8qqzuo,578 +sympy/external/__pycache__/__init__.cpython-311.pyc,, +sympy/external/__pycache__/gmpy.cpython-311.pyc,, +sympy/external/__pycache__/importtools.cpython-311.pyc,, +sympy/external/__pycache__/ntheory.cpython-311.pyc,, +sympy/external/__pycache__/pythonmpq.cpython-311.pyc,, +sympy/external/gmpy.py,sha256=89vXc1KD5TiQCTwlL-98r01KdqsOsJpzc8XN1R7TwM4,9768 +sympy/external/importtools.py,sha256=Q7tS2cdGZ9a4NI_1sgGuoVcSDv_rIk-Av0BpFTa6EzA,7671 +sympy/external/ntheory.py,sha256=jnm13KGII1p6X42T-u2jVoBxiOCXZ8XGxjzhCuWXthE,16555 +sympy/external/pythonmpq.py,sha256=DBQYdZsCUajqIjV5ECYpzpCSbe4RR9qr4D1x_ZTuZaY,11259 +sympy/external/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/external/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/external/tests/__pycache__/test_autowrap.cpython-311.pyc,, +sympy/external/tests/__pycache__/test_codegen.cpython-311.pyc,, +sympy/external/tests/__pycache__/test_gmpy.cpython-311.pyc,, +sympy/external/tests/__pycache__/test_importtools.cpython-311.pyc,, +sympy/external/tests/__pycache__/test_ntheory.cpython-311.pyc,, +sympy/external/tests/__pycache__/test_numpy.cpython-311.pyc,, +sympy/external/tests/__pycache__/test_pythonmpq.cpython-311.pyc,, +sympy/external/tests/__pycache__/test_scipy.cpython-311.pyc,, +sympy/external/tests/test_autowrap.py,sha256=nUWYeQd1QF-Ou4A63iWypsn6pJMi0uMLyba8aRCOhnM,9759 +sympy/external/tests/test_codegen.py,sha256=t991vYFfQC6loodk7Vp1oKTCklBn8SwY7qLshVVou4w,12598 +sympy/external/tests/test_gmpy.py,sha256=8ITpuWYeitCymWwuQLpOOVBmRb3CJsO5sbqiZcVDulE,397 +sympy/external/tests/test_importtools.py,sha256=KrfontKYv11UvpazQ0vS1qyhxIvgZrCOXh1JFeACjeo,1394 +sympy/external/tests/test_ntheory.py,sha256=BJWirDnX7Y7McBoXreMomGQx33YlAjiuBTYQQLhobLU,8881 +sympy/external/tests/test_numpy.py,sha256=tuEji5l6GqbNjv74T6a3e8LDzI2zKUaLzvfluNXOFE0,10291 +sympy/external/tests/test_pythonmpq.py,sha256=L_FdZmmk5N-VEivE_O_qZa98BZhT1WSxRfdmG817bA0,5797 +sympy/external/tests/test_scipy.py,sha256=CVaw7D0-6DORgg78Q6b35SNKn05PlKwWJuqXOuU-qdY,1172 +sympy/functions/__init__.py,sha256=-u5IzcQAPk9emytXfMK22EVXqpXUtSau3pQtevZYmFo,5565 +sympy/functions/__pycache__/__init__.cpython-311.pyc,, +sympy/functions/combinatorial/__init__.py,sha256=WqXI3qU_TTJ7nJA8m3Z-7ZAYKoApT8f9Xs0u2bTwy_c,53 +sympy/functions/combinatorial/__pycache__/__init__.cpython-311.pyc,, +sympy/functions/combinatorial/__pycache__/factorials.cpython-311.pyc,, +sympy/functions/combinatorial/__pycache__/numbers.cpython-311.pyc,, +sympy/functions/combinatorial/factorials.py,sha256=cYMN1Bz76XninzSxzdLYxTN7-Mt1mzfUcSWIU-qpYAw,39184 +sympy/functions/combinatorial/numbers.py,sha256=PcW4rvWQ66POd1FcL8SJav2iUmGv2Ip_Vbbp_3qYIAY,101088 +sympy/functions/combinatorial/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/functions/combinatorial/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/functions/combinatorial/tests/__pycache__/test_comb_factorials.cpython-311.pyc,, +sympy/functions/combinatorial/tests/__pycache__/test_comb_numbers.cpython-311.pyc,, +sympy/functions/combinatorial/tests/test_comb_factorials.py,sha256=B59sfZxTUmKtnG62LW9sPn7HnRxFBP6x_rnbKqbaaZ0,26317 +sympy/functions/combinatorial/tests/test_comb_numbers.py,sha256=GWeIeb9zJo3WJr9NGW7VD-PmCIt13ce-VufICHs1-A8,47593 +sympy/functions/elementary/__init__.py,sha256=Fj8p5qE-Rr1lqAyHI0aSgC3RYX56O-gWwo6wu-eUQYA,50 +sympy/functions/elementary/__pycache__/__init__.cpython-311.pyc,, +sympy/functions/elementary/__pycache__/_trigonometric_special.cpython-311.pyc,, +sympy/functions/elementary/__pycache__/complexes.cpython-311.pyc,, +sympy/functions/elementary/__pycache__/exponential.cpython-311.pyc,, +sympy/functions/elementary/__pycache__/hyperbolic.cpython-311.pyc,, +sympy/functions/elementary/__pycache__/integers.cpython-311.pyc,, +sympy/functions/elementary/__pycache__/miscellaneous.cpython-311.pyc,, +sympy/functions/elementary/__pycache__/piecewise.cpython-311.pyc,, +sympy/functions/elementary/__pycache__/trigonometric.cpython-311.pyc,, +sympy/functions/elementary/_trigonometric_special.py,sha256=FvrgSXisxjXjyBC4-NLLya6q2YyTMNMAUqqYzuYl34g,7271 +sympy/functions/elementary/benchmarks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/functions/elementary/benchmarks/__pycache__/__init__.cpython-311.pyc,, +sympy/functions/elementary/benchmarks/__pycache__/bench_exp.cpython-311.pyc,, +sympy/functions/elementary/benchmarks/bench_exp.py,sha256=PFBYa9eMovH5XOFN5XTxWr1VDj1EBoKwn4mAtj-_DdM,185 +sympy/functions/elementary/complexes.py,sha256=HQm4ViBWN5OkCtJZQPdLdChTnmzbk4Cb2A--MUzTgUs,44168 +sympy/functions/elementary/exponential.py,sha256=GoDlUBPcmpRjYehRZOooF0bISSK_oxpJlkqaE_KNTeQ,42610 +sympy/functions/elementary/hyperbolic.py,sha256=zewRSHNzxyvSFD9sP9XBKX-evG6Rds4k9jAAcfmDbIk,69329 +sympy/functions/elementary/integers.py,sha256=Dc5Bq6ds85NJYWkfHJPJccUeVTxbhzVloTeMo5HGG98,22394 +sympy/functions/elementary/miscellaneous.py,sha256=cPpv71po3HCQGsWIHgcQEql6RNb4Y3RrL24ujgtdTIQ,27944 +sympy/functions/elementary/piecewise.py,sha256=Nv32I9lktMPHF3k0JpifbcgCgIz7CyQ8KwI_t1BaVsI,58285 +sympy/functions/elementary/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/functions/elementary/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/functions/elementary/tests/__pycache__/test_complexes.cpython-311.pyc,, +sympy/functions/elementary/tests/__pycache__/test_exponential.cpython-311.pyc,, +sympy/functions/elementary/tests/__pycache__/test_hyperbolic.cpython-311.pyc,, +sympy/functions/elementary/tests/__pycache__/test_integers.cpython-311.pyc,, +sympy/functions/elementary/tests/__pycache__/test_interface.cpython-311.pyc,, +sympy/functions/elementary/tests/__pycache__/test_miscellaneous.cpython-311.pyc,, +sympy/functions/elementary/tests/__pycache__/test_piecewise.cpython-311.pyc,, +sympy/functions/elementary/tests/__pycache__/test_trigonometric.cpython-311.pyc,, +sympy/functions/elementary/tests/test_complexes.py,sha256=BaPIrUSkoguP74x7uLSt9bA9kxmB58fJ4wgNtx5RnCg,34016 +sympy/functions/elementary/tests/test_exponential.py,sha256=ojjC0aafWaQegZVYV7bgxyvWAES_erpeYTb3R0m9V5I,29733 +sympy/functions/elementary/tests/test_hyperbolic.py,sha256=TBJZbiV-o9c2uVUqnkaRuHr5FdSGBOpqKK5_2bWg92s,56898 +sympy/functions/elementary/tests/test_integers.py,sha256=_QaECu3JQRQa9uPi10jXlu9AohvtLTKB4rSe920g0tY,23266 +sympy/functions/elementary/tests/test_interface.py,sha256=dW0aHvLR5UPyyXzBS-tgKrQIp-H6Vu8sKjlQrBQgtZ4,2513 +sympy/functions/elementary/tests/test_miscellaneous.py,sha256=eCL30UmsusBhjvqICQNmToa1aJTML8fXav1L1J6b7FU,17148 +sympy/functions/elementary/tests/test_piecewise.py,sha256=b1c0IMbXHDLjbi_ESs7fQhJiMaEro30GEu9XYreicYA,62876 +sympy/functions/elementary/tests/test_trigonometric.py,sha256=ebRorj-WThNp56qvZOAGkvgpvWFnIlB6kFZyV1gCii4,90340 +sympy/functions/elementary/trigonometric.py,sha256=b2mE2SpNj0LZM3AcipW0FrjturAi9ozijK3PhqyfCbM,115945 +sympy/functions/special/__init__.py,sha256=5pjIq_RVCMsuCe1b-FlwIty30KxoUowZYKLmpIT9KHQ,59 +sympy/functions/special/__pycache__/__init__.cpython-311.pyc,, +sympy/functions/special/__pycache__/bessel.cpython-311.pyc,, +sympy/functions/special/__pycache__/beta_functions.cpython-311.pyc,, +sympy/functions/special/__pycache__/bsplines.cpython-311.pyc,, +sympy/functions/special/__pycache__/delta_functions.cpython-311.pyc,, +sympy/functions/special/__pycache__/elliptic_integrals.cpython-311.pyc,, +sympy/functions/special/__pycache__/error_functions.cpython-311.pyc,, +sympy/functions/special/__pycache__/gamma_functions.cpython-311.pyc,, +sympy/functions/special/__pycache__/hyper.cpython-311.pyc,, +sympy/functions/special/__pycache__/mathieu_functions.cpython-311.pyc,, +sympy/functions/special/__pycache__/polynomials.cpython-311.pyc,, +sympy/functions/special/__pycache__/singularity_functions.cpython-311.pyc,, +sympy/functions/special/__pycache__/spherical_harmonics.cpython-311.pyc,, +sympy/functions/special/__pycache__/tensor_functions.cpython-311.pyc,, +sympy/functions/special/__pycache__/zeta_functions.cpython-311.pyc,, +sympy/functions/special/benchmarks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/functions/special/benchmarks/__pycache__/__init__.cpython-311.pyc,, +sympy/functions/special/benchmarks/__pycache__/bench_special.cpython-311.pyc,, +sympy/functions/special/benchmarks/bench_special.py,sha256=wzAoKTccuEaG4xrEYTlYfIJuLi3kUTMTEJ9iA113Wog,164 +sympy/functions/special/bessel.py,sha256=r-jJETBLp8a4JOKV9dW5EWr_yvERL0HMn0su328bug8,68656 +sympy/functions/special/beta_functions.py,sha256=de-ypTWZEjvFkq9RgPq3ghGOvFOfPmb9JfUDAPKQoXk,12777 +sympy/functions/special/bsplines.py,sha256=q7iZSeEMlRrx5gpXOzlk4uSTPIollduNZuGwMHB6fRM,10060 +sympy/functions/special/delta_functions.py,sha256=jopb1BX803pZtDAtLmKfFqRVjo3mySekyC80Lu-sJkM,19887 +sympy/functions/special/elliptic_integrals.py,sha256=YpCU-xyTvrmBxdO7Rj2pNYDbS2-bGfCvCaOHon9nSq0,14921 +sympy/functions/special/error_functions.py,sha256=GvKrm5fVMpat0Iierh9UsxsTeQknJyiHgMrWS-e7fhw,79556 +sympy/functions/special/gamma_functions.py,sha256=gkPQdMNR7k5uY1vSUu6zzyoJKMoQZfC90YjLRcyft5c,42943 +sympy/functions/special/hyper.py,sha256=oqeAhFALAN_H6eS0Fs_axdfL9jIYQ-aRFGGgQky2HYk,38829 +sympy/functions/special/mathieu_functions.py,sha256=kFmt0RUA70vhGphI7lhwUsOQ7KIy8Y95DU6gGkVQU5Y,6620 +sympy/functions/special/polynomials.py,sha256=IdxRf8D1iFA-oYfLyTcUmsSCeuWja4h2ExNb8UShwxg,46753 +sympy/functions/special/singularity_functions.py,sha256=ounzMgFQiOW-e1Rb5y_dCSopJGegXMdkj-EpjrP8tzU,8346 +sympy/functions/special/spherical_harmonics.py,sha256=MNAJ4ABTy_EIZe2XSK_MZ1SPnk1OeDnJAtrICAst4yw,11018 +sympy/functions/special/tensor_functions.py,sha256=UNoDpLu1EBR-2tX_dLoRpKhN0Yhm-z_hh8EtW50dNSE,12303 +sympy/functions/special/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/functions/special/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/functions/special/tests/__pycache__/test_bessel.cpython-311.pyc,, +sympy/functions/special/tests/__pycache__/test_beta_functions.cpython-311.pyc,, +sympy/functions/special/tests/__pycache__/test_bsplines.cpython-311.pyc,, +sympy/functions/special/tests/__pycache__/test_delta_functions.cpython-311.pyc,, +sympy/functions/special/tests/__pycache__/test_elliptic_integrals.cpython-311.pyc,, +sympy/functions/special/tests/__pycache__/test_error_functions.cpython-311.pyc,, +sympy/functions/special/tests/__pycache__/test_gamma_functions.cpython-311.pyc,, +sympy/functions/special/tests/__pycache__/test_hyper.cpython-311.pyc,, +sympy/functions/special/tests/__pycache__/test_mathieu.cpython-311.pyc,, +sympy/functions/special/tests/__pycache__/test_singularity_functions.cpython-311.pyc,, +sympy/functions/special/tests/__pycache__/test_spec_polynomials.cpython-311.pyc,, +sympy/functions/special/tests/__pycache__/test_spherical_harmonics.cpython-311.pyc,, +sympy/functions/special/tests/__pycache__/test_tensor_functions.cpython-311.pyc,, +sympy/functions/special/tests/__pycache__/test_zeta_functions.cpython-311.pyc,, +sympy/functions/special/tests/test_bessel.py,sha256=c6OKhj4MCjv4kpDG7udueaCOLykblsKHMOyP4CdFToA,37373 +sympy/functions/special/tests/test_beta_functions.py,sha256=yxfgu-wmNEeMfaFABiDHYmuZpZup9FTp0ZYerlc6hhc,3786 +sympy/functions/special/tests/test_bsplines.py,sha256=6UYg7IqXTi8fcSOut8TEzNVkxIA4ff-CyG22qJnbIYA,7145 +sympy/functions/special/tests/test_delta_functions.py,sha256=8xhSWG4SLL86z1QKFfLk_3b--bCrxjvCaxHlODBVToE,7138 +sympy/functions/special/tests/test_elliptic_integrals.py,sha256=scu7KemJ7Q2nsRcZtckQNruuthI-vla9M1sDVkLcbKM,6991 +sympy/functions/special/tests/test_error_functions.py,sha256=TkhkIKiuRZS78-F4ZTt5rvr3hfknp654IEhmHhRXT48,33565 +sympy/functions/special/tests/test_gamma_functions.py,sha256=aveGPdG1X9OdlC6xpdZbSsJ3S4M4DaG2osyOhm1fAYo,29908 +sympy/functions/special/tests/test_hyper.py,sha256=SEnQ9TtyO_dSQRc94AHrbQai6Q7-tmRfDIh40gNE3FE,16694 +sympy/functions/special/tests/test_mathieu.py,sha256=pqoFbnC84NDL6EQkigFtx5OQ1RFYppckTjzsm9XT0PY,1282 +sympy/functions/special/tests/test_singularity_functions.py,sha256=GEXzHHeR5F0swrZzhF7pEGR0tk9Vp3Ve2c-sE4WSpsY,6249 +sympy/functions/special/tests/test_spec_polynomials.py,sha256=wuiZaR_LwaM8SlNuGl3B1p4eOHC_-zZVSXMPNfzKRB4,19561 +sympy/functions/special/tests/test_spherical_harmonics.py,sha256=pUFtFpNPBnJTdnqou0jniSchijyh1rdzKv8H24RT9FU,3850 +sympy/functions/special/tests/test_tensor_functions.py,sha256=bblSDkPABZ6N1j1Rb2Bb5TZIzZoK1D8ks3fHizi69ZI,5546 +sympy/functions/special/tests/test_zeta_functions.py,sha256=2r59_aC0QOXQsBNXqxsHPr2PkJExusI6qvSydZBPbfw,10474 +sympy/functions/special/zeta_functions.py,sha256=QPRYVp0-WKs02CUSQiRIJUWD4N_3PmqrOsk1MGzBUfU,24091 +sympy/galgebra.py,sha256=yEosUPSnhLp9a1NWXvpCLoU20J6TQ58XNIvw07POkVk,123 +sympy/geometry/__init__.py,sha256=BU2MiKm8qJyZJ_hz1qC-3nFJTPEcuvx4hYd02jHjqSM,1240 +sympy/geometry/__pycache__/__init__.cpython-311.pyc,, +sympy/geometry/__pycache__/curve.cpython-311.pyc,, +sympy/geometry/__pycache__/ellipse.cpython-311.pyc,, +sympy/geometry/__pycache__/entity.cpython-311.pyc,, +sympy/geometry/__pycache__/exceptions.cpython-311.pyc,, +sympy/geometry/__pycache__/line.cpython-311.pyc,, +sympy/geometry/__pycache__/parabola.cpython-311.pyc,, +sympy/geometry/__pycache__/plane.cpython-311.pyc,, +sympy/geometry/__pycache__/point.cpython-311.pyc,, +sympy/geometry/__pycache__/polygon.cpython-311.pyc,, +sympy/geometry/__pycache__/util.cpython-311.pyc,, +sympy/geometry/curve.py,sha256=F7b6XrlhUZ0QWLDoZJVojWfC5LeyOU-69OTFnYAREg8,10170 +sympy/geometry/ellipse.py,sha256=So0gH9e-1Oh3HGzrXoLqANyP4lTTBfGHHVo5vCdbfdk,50305 +sympy/geometry/entity.py,sha256=fvHhtSb6RvE6v-8yMyCNvm0ekLPoO7EO9J8TEsGyQGU,20668 +sympy/geometry/exceptions.py,sha256=XtUMA44UTdrBWt771jegFC-TXsobhDiI-10TDH_WNFM,131 +sympy/geometry/line.py,sha256=Yw8ns_w8rn6FEak6MmfsdccgNNjMQHhodDik2UX7jJM,80397 +sympy/geometry/parabola.py,sha256=YhtQ9C5Yco_iSQ6ZYRs0lZBWHmDkcvKUr-Y8RghY3M8,10716 +sympy/geometry/plane.py,sha256=z3hjEf3ibO735DV0Mb4DVjvthYlDyiPfekCeePSCDMM,26770 +sympy/geometry/point.py,sha256=vQB40V1fvhZze3D_D54HlMSS8gXeMIWerd2hBozmPFA,36661 +sympy/geometry/polygon.py,sha256=aooJyJVwf6ZPuxStYgTc-2jNjVaM2YHSvpVY3XRjAuo,82027 +sympy/geometry/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/geometry/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/geometry/tests/__pycache__/test_curve.cpython-311.pyc,, +sympy/geometry/tests/__pycache__/test_ellipse.cpython-311.pyc,, +sympy/geometry/tests/__pycache__/test_entity.cpython-311.pyc,, +sympy/geometry/tests/__pycache__/test_geometrysets.cpython-311.pyc,, +sympy/geometry/tests/__pycache__/test_line.cpython-311.pyc,, +sympy/geometry/tests/__pycache__/test_parabola.cpython-311.pyc,, +sympy/geometry/tests/__pycache__/test_plane.cpython-311.pyc,, +sympy/geometry/tests/__pycache__/test_point.cpython-311.pyc,, +sympy/geometry/tests/__pycache__/test_polygon.cpython-311.pyc,, +sympy/geometry/tests/__pycache__/test_util.cpython-311.pyc,, +sympy/geometry/tests/test_curve.py,sha256=xL4uRWAal4mXZxuQhcs9QOhs6MheCbFNyH1asq_a2IQ,4479 +sympy/geometry/tests/test_ellipse.py,sha256=f-EorUkNl_cg63bDvm4Qrbe1PWrBaCCRg8jAmdIYBuU,26509 +sympy/geometry/tests/test_entity.py,sha256=KSsncs7afLpqUAYTs9b0VH1dt6FhPrX5rrj2rdLpBUo,3896 +sympy/geometry/tests/test_geometrysets.py,sha256=vvOWrFrJuNAFgbrVh1wPY94o-H-85FWlnIyyo2Kst9c,1911 +sympy/geometry/tests/test_line.py,sha256=fLWGfHQNC3YqjxtT7zQbP--Cu0mjcPwSBfji4DA7BP0,38054 +sympy/geometry/tests/test_parabola.py,sha256=kd0RU5sGOcfp6jgwgXMtvT2B6kG1-M3-iGOLnUJfZOw,6150 +sympy/geometry/tests/test_plane.py,sha256=QRcfoDsJtCtcvjFb18hBEHupycLgAT2OohF6GpNShyQ,12525 +sympy/geometry/tests/test_point.py,sha256=YKXQdlBTQWsIVf9l3G6iPMq0OqMTo8nZlibhJDsq_Ic,16410 +sympy/geometry/tests/test_polygon.py,sha256=4D0t98SELQF42dYR-CbFqDU5yJds0LAE2RE8xqEdqag,27602 +sympy/geometry/tests/test_util.py,sha256=sbh1QvkQG1OqvE-kt4fNNIkMWnOFi5EpaBmnZS3pzNc,7044 +sympy/geometry/util.py,sha256=zF6Xx0ciab6Rxm4ekK5RawWXPWAk5oug8eDvgfNKoRc,20671 +sympy/holonomic/__init__.py,sha256=BgHIokaSOo3nwJlGO_caJHz37n6yoA8GeM9Xjn4zMpc,784 +sympy/holonomic/__pycache__/__init__.cpython-311.pyc,, +sympy/holonomic/__pycache__/holonomic.cpython-311.pyc,, +sympy/holonomic/__pycache__/holonomicerrors.cpython-311.pyc,, +sympy/holonomic/__pycache__/numerical.cpython-311.pyc,, +sympy/holonomic/__pycache__/recurrence.cpython-311.pyc,, +sympy/holonomic/holonomic.py,sha256=vP5XabVIezsAURbK65p8P_QfPINQwhol0d62B7Br3s0,91887 +sympy/holonomic/holonomicerrors.py,sha256=qDyUoGbrRjPtVax4SeEEf_o6-264mASEZO_rZETXH5o,1193 +sympy/holonomic/numerical.py,sha256=MlP8xTBqjVQqM1-jwviS_yPICKj6tEVsa9f6wyMCSfo,2625 +sympy/holonomic/recurrence.py,sha256=HWSMokhjcdGTLW_oku0Xi8W8RHxWYS28bv8V7eubC58,10377 +sympy/holonomic/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/holonomic/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/holonomic/tests/__pycache__/test_holonomic.cpython-311.pyc,, +sympy/holonomic/tests/__pycache__/test_recurrence.cpython-311.pyc,, +sympy/holonomic/tests/test_holonomic.py,sha256=NuoJtJi3RlpMYhMU_bGH1tuFuFoOZZrrSdMpgdHGhf0,35333 +sympy/holonomic/tests/test_recurrence.py,sha256=qHv0kn1Q4-aCD7XmbDK2xIdkjF0XkeZUKD2yeLajiq0,1342 +sympy/integrals/__init__.py,sha256=pZ-C3tDP_8woKActNoS7IwyW-9AuB5PMHB5B0ohlpw8,1970 +sympy/integrals/__pycache__/__init__.cpython-311.pyc,, +sympy/integrals/__pycache__/deltafunctions.cpython-311.pyc,, +sympy/integrals/__pycache__/heurisch.cpython-311.pyc,, +sympy/integrals/__pycache__/integrals.cpython-311.pyc,, +sympy/integrals/__pycache__/intpoly.cpython-311.pyc,, +sympy/integrals/__pycache__/laplace.cpython-311.pyc,, +sympy/integrals/__pycache__/manualintegrate.cpython-311.pyc,, +sympy/integrals/__pycache__/meijerint.cpython-311.pyc,, +sympy/integrals/__pycache__/meijerint_doc.cpython-311.pyc,, +sympy/integrals/__pycache__/prde.cpython-311.pyc,, +sympy/integrals/__pycache__/quadrature.cpython-311.pyc,, +sympy/integrals/__pycache__/rationaltools.cpython-311.pyc,, +sympy/integrals/__pycache__/rde.cpython-311.pyc,, +sympy/integrals/__pycache__/risch.cpython-311.pyc,, +sympy/integrals/__pycache__/singularityfunctions.cpython-311.pyc,, +sympy/integrals/__pycache__/transforms.cpython-311.pyc,, +sympy/integrals/__pycache__/trigonometry.cpython-311.pyc,, +sympy/integrals/benchmarks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/integrals/benchmarks/__pycache__/__init__.cpython-311.pyc,, +sympy/integrals/benchmarks/__pycache__/bench_integrate.cpython-311.pyc,, +sympy/integrals/benchmarks/__pycache__/bench_trigintegrate.cpython-311.pyc,, +sympy/integrals/benchmarks/bench_integrate.py,sha256=vk6wAO1bqzFT9oW4qsW7nKGfc_gP0XaB5PMYKx5339Q,396 +sympy/integrals/benchmarks/bench_trigintegrate.py,sha256=8XU3uB3mcavigvzHQZA7H1sHI32zgT-9RkSnLa-Y3Vc,305 +sympy/integrals/deltafunctions.py,sha256=ysIQLdRBcG_YR-bVDoxt-sxEVU8TG77oSgM-J0gI0mE,7435 +sympy/integrals/heurisch.py,sha256=Huq3dBJEXvYGzkBboXnG6xFCGorc5Uftf5ssbiltyIs,26706 +sympy/integrals/integrals.py,sha256=DV_ditazAyVBpUCzsUFNiYvvQRqJfx3UGjnSEB2HKcg,64887 +sympy/integrals/intpoly.py,sha256=SXjd_f295YrYsvoQpzE2EQM5xaQnnj0zvHdYW5KEdn0,43266 +sympy/integrals/laplace.py,sha256=y5NxZ3_nNtc-NETYWw4iha7LTVM9Ok9NWPtmMUXtZwg,87038 +sympy/integrals/manualintegrate.py,sha256=sO_5-9pbeRKhZSuEwXm6okxpvRsbpQZerbLBdRdDtp4,75721 +sympy/integrals/meijerint.py,sha256=yOsrHJA7doo1uDsmsiTqK2Tly4JF30sA5RElT92lfec,80775 +sympy/integrals/meijerint_doc.py,sha256=Sis9bZvwciU2_BZzr3GCvXLkBcXVUhP9ycqGIDu99xU,1204 +sympy/integrals/prde.py,sha256=3VqBQOX7SZv_lETmhoVgb9gRjb1z8hiZAJ6vv5AgNEo,52073 +sympy/integrals/quadrature.py,sha256=6Bg3JmlIjIduIfaGfNVcwNfSrgEiLOszcN8WPzsXNqE,17064 +sympy/integrals/rationaltools.py,sha256=ZAizq9unSt-_wQkCIAhl_FaemzbQcfBcE9A3mDiU788,11694 +sympy/integrals/rde.py,sha256=EKXK6bf4m-2O0NZ-rtHq0Ye2a_o9LmUrkoV8gNxZKVk,27393 +sympy/integrals/risch.py,sha256=4UsTHizZtAMe3VUmRHugdOuPMilw8sKiVOlSUMWj9oE,67352 +sympy/integrals/singularityfunctions.py,sha256=ONI8x-ed-IcqOF4K2l0LVUvEUN2_dHztvL4auRsi67U,2235 +sympy/integrals/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/integrals/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/integrals/tests/__pycache__/test_deltafunctions.cpython-311.pyc,, +sympy/integrals/tests/__pycache__/test_failing_integrals.cpython-311.pyc,, +sympy/integrals/tests/__pycache__/test_heurisch.cpython-311.pyc,, +sympy/integrals/tests/__pycache__/test_integrals.cpython-311.pyc,, +sympy/integrals/tests/__pycache__/test_intpoly.cpython-311.pyc,, +sympy/integrals/tests/__pycache__/test_laplace.cpython-311.pyc,, +sympy/integrals/tests/__pycache__/test_lineintegrals.cpython-311.pyc,, +sympy/integrals/tests/__pycache__/test_manual.cpython-311.pyc,, +sympy/integrals/tests/__pycache__/test_meijerint.cpython-311.pyc,, +sympy/integrals/tests/__pycache__/test_prde.cpython-311.pyc,, +sympy/integrals/tests/__pycache__/test_quadrature.cpython-311.pyc,, +sympy/integrals/tests/__pycache__/test_rationaltools.cpython-311.pyc,, +sympy/integrals/tests/__pycache__/test_rde.cpython-311.pyc,, +sympy/integrals/tests/__pycache__/test_risch.cpython-311.pyc,, +sympy/integrals/tests/__pycache__/test_singularityfunctions.cpython-311.pyc,, +sympy/integrals/tests/__pycache__/test_transforms.cpython-311.pyc,, +sympy/integrals/tests/__pycache__/test_trigonometry.cpython-311.pyc,, +sympy/integrals/tests/test_deltafunctions.py,sha256=ivFjS-WlLQ4aMqjVS7ZzMChP2Mmw_JUPnwI9otiLnvs,3709 +sympy/integrals/tests/test_failing_integrals.py,sha256=HKMA6O26exCIRWQ43KyKugOvJL3gOei3r9JObcKo8p0,7438 +sympy/integrals/tests/test_heurisch.py,sha256=-btpZS_LI9EO-2EmA0eHV6l7xykQjkHIcpDU_GziTlo,14299 +sympy/integrals/tests/test_integrals.py,sha256=8bVIN4ztTwOvpLrlDIk1SURQMtiNXoqu-LLMkZp-Wrk,81192 +sympy/integrals/tests/test_intpoly.py,sha256=NzGhkR2pUMfd8lIU2cFR9bFa0J89RzpHs3zDggAWtXo,37445 +sympy/integrals/tests/test_laplace.py,sha256=pWn5UEm15gJv_hD35lvJh03Uuw4o79rbCQYvbXQS2xk,38186 +sympy/integrals/tests/test_lineintegrals.py,sha256=zcPJ2n7DYt9KsgAe38t0gq3ARApUlb-kBahLThuRcq8,450 +sympy/integrals/tests/test_manual.py,sha256=unQ6Ew81SCHvVMva_aAOndh7hWpLeDwBtXs6W-yC0jE,34551 +sympy/integrals/tests/test_meijerint.py,sha256=G22dppQGMFU3JGexHZs65nce8UAK9wTilZn0D1XvZu0,32594 +sympy/integrals/tests/test_prde.py,sha256=2BZmEDasdx_3l64-9hioArysDj6Nl520GpQN2xnEE_A,16360 +sympy/integrals/tests/test_quadrature.py,sha256=iFMdqck36gkL-yksLflawIOYmw-0PzO2tFj_qdK6Hjg,19919 +sympy/integrals/tests/test_rationaltools.py,sha256=7QiPnpBXl7lo32RmhXo7ED6FYj5I1gjEFOziJYlqPtI,5669 +sympy/integrals/tests/test_rde.py,sha256=4d3vJupa-hRN4yNDISY8IC3rSI_cZW5BbtxoZm14y-Y,9571 +sympy/integrals/tests/test_risch.py,sha256=HaWg0JnErdrNzNmVfyz2Zz4XAgZPVVpZPt6Map3sQ58,38630 +sympy/integrals/tests/test_singularityfunctions.py,sha256=CSrHie59_NjNZ9B2GaHzKPNsMzxm5Kh6GuxlYk8zTuI,1266 +sympy/integrals/tests/test_transforms.py,sha256=mLl5RfuENf11oenV1OXgg6TfWdQTEY23txiGl7grCyo,27152 +sympy/integrals/tests/test_trigonometry.py,sha256=moMYr_Prc7gaYPjBK0McLjRpTEes2veUlN0vGv9UyEA,3869 +sympy/integrals/transforms.py,sha256=_rwRkrOnV4zby27MZJ9dpJrtQFG4gTbL7naLpFLXKDo,51750 +sympy/integrals/trigonometry.py,sha256=iOoBDGFDZx8PNbgL3XeZEd80I8ro0WAizNuC4P-u8x0,11083 +sympy/interactive/__init__.py,sha256=yokwEO2HF3eN2Xu65JSpUUsN4iYmPvvU4m_64f3Q33o,251 +sympy/interactive/__pycache__/__init__.cpython-311.pyc,, +sympy/interactive/__pycache__/printing.cpython-311.pyc,, +sympy/interactive/__pycache__/session.cpython-311.pyc,, +sympy/interactive/__pycache__/traversal.cpython-311.pyc,, +sympy/interactive/printing.py,sha256=EZBO3YCga2dXBI1EfFHVDmUKBkF9UmrLMgwI76B0nyE,21562 +sympy/interactive/session.py,sha256=sG546e0mAtT0OrFkYNVM7QGvkWrDhAQZ5E1hfx03iBQ,15329 +sympy/interactive/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/interactive/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/interactive/tests/__pycache__/test_interactive.cpython-311.pyc,, +sympy/interactive/tests/__pycache__/test_ipython.cpython-311.pyc,, +sympy/interactive/tests/test_interactive.py,sha256=Pbopy9lODrd_P46_xxlWxLwqPfG6_4J3CWWC4IqfDL4,485 +sympy/interactive/tests/test_ipython.py,sha256=QZUvr77TxJHjdgQVNpihAuoglg1ghVHOTDFOVXJ9pcE,11797 +sympy/interactive/traversal.py,sha256=XbccdO6msNAvrG6FFJl2n4XmIiRISnvda4QflfEPg7U,3189 +sympy/liealgebras/__init__.py,sha256=K8tw7JqG33_y6mYl1LTr8ZNtKH5L21BqkjCHfLhP4aA,79 +sympy/liealgebras/__pycache__/__init__.cpython-311.pyc,, +sympy/liealgebras/__pycache__/cartan_matrix.cpython-311.pyc,, +sympy/liealgebras/__pycache__/cartan_type.cpython-311.pyc,, +sympy/liealgebras/__pycache__/dynkin_diagram.cpython-311.pyc,, +sympy/liealgebras/__pycache__/root_system.cpython-311.pyc,, +sympy/liealgebras/__pycache__/type_a.cpython-311.pyc,, +sympy/liealgebras/__pycache__/type_b.cpython-311.pyc,, +sympy/liealgebras/__pycache__/type_c.cpython-311.pyc,, +sympy/liealgebras/__pycache__/type_d.cpython-311.pyc,, +sympy/liealgebras/__pycache__/type_e.cpython-311.pyc,, +sympy/liealgebras/__pycache__/type_f.cpython-311.pyc,, +sympy/liealgebras/__pycache__/type_g.cpython-311.pyc,, +sympy/liealgebras/__pycache__/weyl_group.cpython-311.pyc,, +sympy/liealgebras/cartan_matrix.py,sha256=yr2LoZi_Gxmu-EMKgFuPOPNMYPOsxucLAS6oRpSYi2U,524 +sympy/liealgebras/cartan_type.py,sha256=xLklg8Y5s40je6sXwmLmG9iyYi9YEk9KoxTSFz1GtdI,1790 +sympy/liealgebras/dynkin_diagram.py,sha256=ZzGuBGNOJ3lPDdJDs4n8hvGbz6wLhC5mwb8zFkDmyPw,535 +sympy/liealgebras/root_system.py,sha256=n1TKAZyWDPSlUDmjAga9Sa2Cm9qeRXddYPbl2XKyFq4,6673 +sympy/liealgebras/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/liealgebras/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/liealgebras/tests/__pycache__/test_cartan_matrix.cpython-311.pyc,, +sympy/liealgebras/tests/__pycache__/test_cartan_type.cpython-311.pyc,, +sympy/liealgebras/tests/__pycache__/test_dynkin_diagram.cpython-311.pyc,, +sympy/liealgebras/tests/__pycache__/test_root_system.cpython-311.pyc,, +sympy/liealgebras/tests/__pycache__/test_type_A.cpython-311.pyc,, +sympy/liealgebras/tests/__pycache__/test_type_B.cpython-311.pyc,, +sympy/liealgebras/tests/__pycache__/test_type_C.cpython-311.pyc,, +sympy/liealgebras/tests/__pycache__/test_type_D.cpython-311.pyc,, +sympy/liealgebras/tests/__pycache__/test_type_E.cpython-311.pyc,, +sympy/liealgebras/tests/__pycache__/test_type_F.cpython-311.pyc,, +sympy/liealgebras/tests/__pycache__/test_type_G.cpython-311.pyc,, +sympy/liealgebras/tests/__pycache__/test_weyl_group.cpython-311.pyc,, +sympy/liealgebras/tests/test_cartan_matrix.py,sha256=KCsakn0fHKHRbIUcrUkHBIKkudl3_ISUdHrfJy-UOd4,303 +sympy/liealgebras/tests/test_cartan_type.py,sha256=t5PvYYDXbNIFL3CV59Je7SBIAeLLf-W3mOINPUoHK6E,339 +sympy/liealgebras/tests/test_dynkin_diagram.py,sha256=DSixbnt_yd0zrhKzXW_XqkXWXYe1Dk2MmXN-Rjb1dGg,260 +sympy/liealgebras/tests/test_root_system.py,sha256=YmGBdUeJ4PkLSfAfRgTF7GW62RCEd5nH27FSX9UaG5Q,927 +sympy/liealgebras/tests/test_type_A.py,sha256=x7QmpjxsGmXol-IYVtN1lmIOmM3HLYwpX1tSG5h6FMM,657 +sympy/liealgebras/tests/test_type_B.py,sha256=Gw0GP24wP2rPn38Wwla9W7BwWH4JtCGpaprZb5W6JVY,642 +sympy/liealgebras/tests/test_type_C.py,sha256=ysSy-vzE9lNwzAunrmvnFkLBoJwF7W2On7QpqS6RI1s,927 +sympy/liealgebras/tests/test_type_D.py,sha256=qrO4oCjrjkp1uDvrNtbgANVyaOExqOLNtIpIxD1uH0U,764 +sympy/liealgebras/tests/test_type_E.py,sha256=IQ46Bo75Kivz0O45mNKNvDO8jen7X2NwxVBTwvr5lQo,987 +sympy/liealgebras/tests/test_type_F.py,sha256=yUQJ7LzTemv4Cd1XW_dr3x7KEI07BahsWAyJfXLS1eA,1378 +sympy/liealgebras/tests/test_type_G.py,sha256=wVa6qcAHbdrc9dA63samexHL35cWWJS606pom-6mH2Q,548 +sympy/liealgebras/tests/test_weyl_group.py,sha256=HrzojRECbhNUsdLFQAXYnJEt8LfktOSJZuqVE45aRnc,1501 +sympy/liealgebras/type_a.py,sha256=Etn_aeyE5FdYrUXrGnjNiTL5k4r5-O_CysaO-_UKASA,4294 +sympy/liealgebras/type_b.py,sha256=ERH1aM38eHZUuTJYRsWLKFDm74G4y1MQtweSfkDFQYc,4547 +sympy/liealgebras/type_c.py,sha256=Al1pA5cw5IKjvBzx9LdMORd5-JZXgA0kaMv8aHhnYJ0,4426 +sympy/liealgebras/type_d.py,sha256=Cagn4GXrYuUP32W6jauIPnEv8IcD3H88sbn0fKS7pmU,4681 +sympy/liealgebras/type_e.py,sha256=KR84bp4Ga7d9itKRCsD3AYrarVewGiNNe7AHWNUfTAM,8280 +sympy/liealgebras/type_f.py,sha256=gIyk4zA04uIpwizcYdvKsZ_xkcpPLId6LIHR6tp38dk,4423 +sympy/liealgebras/type_g.py,sha256=Ife98dGPtarGd-ii8hJbXdB0SMsct4okDkSX2wLN8XI,2965 +sympy/liealgebras/weyl_group.py,sha256=5YFA8qC4GWDM0WLNR_6VgpuNFZDfyDA7fBFjBcZaLgA,14557 +sympy/logic/__init__.py,sha256=RfoXrq9MESnXdL7PkwpYEfWeaxH6wBPHiE4zCgLKvk0,456 +sympy/logic/__pycache__/__init__.cpython-311.pyc,, +sympy/logic/__pycache__/boolalg.cpython-311.pyc,, +sympy/logic/__pycache__/inference.cpython-311.pyc,, +sympy/logic/algorithms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/logic/algorithms/__pycache__/__init__.cpython-311.pyc,, +sympy/logic/algorithms/__pycache__/dpll.cpython-311.pyc,, +sympy/logic/algorithms/__pycache__/dpll2.cpython-311.pyc,, +sympy/logic/algorithms/__pycache__/lra_theory.cpython-311.pyc,, +sympy/logic/algorithms/__pycache__/minisat22_wrapper.cpython-311.pyc,, +sympy/logic/algorithms/__pycache__/pycosat_wrapper.cpython-311.pyc,, +sympy/logic/algorithms/__pycache__/z3_wrapper.cpython-311.pyc,, +sympy/logic/algorithms/dpll.py,sha256=zqiZDm1oD5sNxFqm_0Hen6NjfILIDp5uRgEOad1vYXI,9188 +sympy/logic/algorithms/dpll2.py,sha256=PEVvFUrEf-IwTwfnTYtAFLPLPjjqA1nmm39nASgsM1I,21497 +sympy/logic/algorithms/lra_theory.py,sha256=6JZAZMJhhHSbUXZuc0ddYwp_x7efVuthIYV7Dc9eUyc,31769 +sympy/logic/algorithms/minisat22_wrapper.py,sha256=uINcvkIHGWYJb8u-Q0OgnSgaHfVUd9tYYFbBAVNiASo,1317 +sympy/logic/algorithms/pycosat_wrapper.py,sha256=0vNFTbu9-YhSfjwYTsZsP_Z4HM8WpL11-xujLBS1kYg,1207 +sympy/logic/algorithms/z3_wrapper.py,sha256=mFmf7DWDV0Zu7006EdFK4qEDTP7sfmcXrfGZkMK97vo,3747 +sympy/logic/boolalg.py,sha256=GqxLtqUangGoSjsncKDzlLEE7iYVe5qsMtPhztniI9c,114972 +sympy/logic/inference.py,sha256=J2D8t9iHCSdotzS9iq6g3EvLPsI2B10kiNbmHsIz_oY,8983 +sympy/logic/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/logic/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/logic/tests/__pycache__/test_boolalg.cpython-311.pyc,, +sympy/logic/tests/__pycache__/test_dimacs.cpython-311.pyc,, +sympy/logic/tests/__pycache__/test_inference.cpython-311.pyc,, +sympy/logic/tests/__pycache__/test_lra_theory.cpython-311.pyc,, +sympy/logic/tests/test_boolalg.py,sha256=0ZjG56WpIhIwFLedUzg5IC-c0VF_g9qoU9aAbFydPYU,49748 +sympy/logic/tests/test_dimacs.py,sha256=EK_mA_k9zBLcQLTOKTZVrGhnGuQNza5mwXDQD_f-X1c,3886 +sympy/logic/tests/test_inference.py,sha256=9BXfPbJs6sBEgx1nwkmg2HcLl82lcfFXngKJL_ByzUI,16116 +sympy/logic/tests/test_lra_theory.py,sha256=hNCH66hP_32x5ioyqM6ltd0DbEl72kdctCBu-H6egG0,16834 +sympy/logic/utilities/__init__.py,sha256=WTn2vBgHcmhONRWI79PdMYNk8UxYDzsxRlZWuc-wtNI,55 +sympy/logic/utilities/__pycache__/__init__.cpython-311.pyc,, +sympy/logic/utilities/__pycache__/dimacs.cpython-311.pyc,, +sympy/logic/utilities/dimacs.py,sha256=gfrGWQpn4F7gLJH1zHAnLB8-CMGa1XtXl9NCGhwAW9k,1671 +sympy/matrices/__init__.py,sha256=i2a37WlcCj8-AKG_Yy8BBdOHFgAuWois_2IcD_Ih00s,2634 +sympy/matrices/__pycache__/__init__.cpython-311.pyc,, +sympy/matrices/__pycache__/common.cpython-311.pyc,, +sympy/matrices/__pycache__/decompositions.cpython-311.pyc,, +sympy/matrices/__pycache__/dense.cpython-311.pyc,, +sympy/matrices/__pycache__/determinant.cpython-311.pyc,, +sympy/matrices/__pycache__/eigen.cpython-311.pyc,, +sympy/matrices/__pycache__/exceptions.cpython-311.pyc,, +sympy/matrices/__pycache__/graph.cpython-311.pyc,, +sympy/matrices/__pycache__/immutable.cpython-311.pyc,, +sympy/matrices/__pycache__/inverse.cpython-311.pyc,, +sympy/matrices/__pycache__/kind.cpython-311.pyc,, +sympy/matrices/__pycache__/matrices.cpython-311.pyc,, +sympy/matrices/__pycache__/matrixbase.cpython-311.pyc,, +sympy/matrices/__pycache__/normalforms.cpython-311.pyc,, +sympy/matrices/__pycache__/reductions.cpython-311.pyc,, +sympy/matrices/__pycache__/repmatrix.cpython-311.pyc,, +sympy/matrices/__pycache__/solvers.cpython-311.pyc,, +sympy/matrices/__pycache__/sparse.cpython-311.pyc,, +sympy/matrices/__pycache__/sparsetools.cpython-311.pyc,, +sympy/matrices/__pycache__/subspaces.cpython-311.pyc,, +sympy/matrices/__pycache__/utilities.cpython-311.pyc,, +sympy/matrices/benchmarks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/matrices/benchmarks/__pycache__/__init__.cpython-311.pyc,, +sympy/matrices/benchmarks/__pycache__/bench_matrix.cpython-311.pyc,, +sympy/matrices/benchmarks/bench_matrix.py,sha256=vGMlg-2il2cFeAWrf0NJ6pzPX3Yd3ZQMxFgQ4q5ILQE,306 +sympy/matrices/common.py,sha256=v_jMjH9t6mrHl7Z5QluhO-G07QUaJSdiioF2YC1xlhY,95395 +sympy/matrices/decompositions.py,sha256=5OjwFe5Z_eM2XoZPqrNFY9-Esq3cO_8yqFxT9QBPHgQ,47865 +sympy/matrices/dense.py,sha256=HTPNMKbi2M0W0nHImNtHib9JM5VC8si99ldL_FylKwQ,30461 +sympy/matrices/determinant.py,sha256=pDTDYCLBSWJov_j_B-ZZHdqH3-WQ_uwhZawrX3EDE9k,34550 +sympy/matrices/eigen.py,sha256=v09mqb2hZ0HqGbGzYpELOatIHnGTP5XADE-1RGcua-g,39811 +sympy/matrices/exceptions.py,sha256=8diN_ojMGC93XpqvZeS5ow4crlFxSkm6WqtnIK5j81E,503 +sympy/matrices/expressions/__init__.py,sha256=IMqXCSsPh0Vp_MC9HZTudA5DGM4WBq_yB-Bst0azyM8,1692 +sympy/matrices/expressions/__pycache__/__init__.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/_shape.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/adjoint.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/applyfunc.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/blockmatrix.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/companion.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/determinant.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/diagonal.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/dotproduct.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/factorizations.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/fourier.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/funcmatrix.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/hadamard.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/inverse.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/kronecker.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/matadd.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/matexpr.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/matmul.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/matpow.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/permutation.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/sets.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/slice.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/special.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/trace.cpython-311.pyc,, +sympy/matrices/expressions/__pycache__/transpose.cpython-311.pyc,, +sympy/matrices/expressions/_shape.py,sha256=TyQSwGx41aaMyAYs5Q7Er6atKVAdWK7DJ6YIVsiEAZg,3062 +sympy/matrices/expressions/adjoint.py,sha256=HV4OIWgmS2f_1_4PHRhQpgNmKJVCoX0eyHmrM6gfN5g,1515 +sympy/matrices/expressions/applyfunc.py,sha256=8scpWjZp7yzuvUNr0mxN03KgyTzLRa5NkGl9s81YPgY,6751 +sympy/matrices/expressions/blockmatrix.py,sha256=TPR1ZDTSzVGtT-Qo23XNzjnDVl1Js8NCi_2qV3fQA9U,31643 +sympy/matrices/expressions/companion.py,sha256=lXUJRbjQR6e1mdHQdJwNIJXMW80XmKbOVqNvUXjB57U,1705 +sympy/matrices/expressions/determinant.py,sha256=RyQXgUgqJkv_rvUPrn1_rOY45wzp5zYH6ZOf9S8NK8s,3281 +sympy/matrices/expressions/diagonal.py,sha256=XHWoT-Jv5QwJVsGNbfxHnNG2sygPy1CeR_t6zr8oUoM,6328 +sympy/matrices/expressions/dotproduct.py,sha256=sKdUhwVKTB3LEvd8xMwCDexNoQ1Dz43DCYsmm3UwFWw,1911 +sympy/matrices/expressions/factorizations.py,sha256=zFNjMBsJqhsIcDD8Me4W8-Q-TV89WptfG3Dd9yK_tPE,1456 +sympy/matrices/expressions/fourier.py,sha256=dvaftgB9jgkR_8ETyhzyVLtf1ZJu_wQC-ZbpTYMXZGE,2094 +sympy/matrices/expressions/funcmatrix.py,sha256=q6R75wLn0UdV4xJdVJUrNaofV1k1egXLLQdBeZcPtiY,3520 +sympy/matrices/expressions/hadamard.py,sha256=feXSZy0vbqxTzg0JeLmkSegiF4T2v5dOdcv0UQczK38,13920 +sympy/matrices/expressions/inverse.py,sha256=4UwgHWSIHgEoKOniObkClMYN9DrO2xNyvOSVToXSpj8,2963 +sympy/matrices/expressions/kronecker.py,sha256=kzCHqXDtcZGVQPln521lfN5redNwj6IjXJwjbv_Dkhg,13404 +sympy/matrices/expressions/matadd.py,sha256=0MSanal1HKVEuCBEpehKwfUX4fuM9UMy6Fg2H5noA0s,4773 +sympy/matrices/expressions/matexpr.py,sha256=nfV9MhDNBR9HkOP7zoXOtv9wWR4OFxNczprik4S3Uh8,27549 +sympy/matrices/expressions/matmul.py,sha256=n9LcrgYoEhEQuq2htALHuboIWN0p8P_3lZdKNo92Rf0,15509 +sympy/matrices/expressions/matpow.py,sha256=puuYB49wr1WzFxhT9DcTWtF2bGDFWcbJ7oh14ATQARs,5140 +sympy/matrices/expressions/permutation.py,sha256=Xe7yOx-EgeD6JrqWc4L-ApdN-3ZiV8XS_LQPmc1lhGw,8050 +sympy/matrices/expressions/sets.py,sha256=3zase_rDn2QdaXETX78BgkfKiWcRC7FmwVjjIU-WmdY,2033 +sympy/matrices/expressions/slice.py,sha256=aNdY1Ey4VJR-UCvoORX2kh2DmA6QjOp-waENvWg8WVE,3355 +sympy/matrices/expressions/special.py,sha256=hywkygOQjcJEGQn2fG4dF8oUqXL3N42U1TxbdJr4-6E,7499 +sympy/matrices/expressions/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/matrices/expressions/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_adjoint.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_applyfunc.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_blockmatrix.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_companion.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_derivatives.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_determinant.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_diagonal.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_dotproduct.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_factorizations.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_fourier.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_funcmatrix.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_hadamard.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_indexing.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_inverse.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_kronecker.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_matadd.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_matexpr.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_matmul.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_matpow.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_permutation.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_sets.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_slice.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_special.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_trace.cpython-311.pyc,, +sympy/matrices/expressions/tests/__pycache__/test_transpose.cpython-311.pyc,, +sympy/matrices/expressions/tests/test_adjoint.py,sha256=cxOc334yNSI9MazhG9HT8s1OCXjkDWr3Zj2JnyHS3Z4,1065 +sympy/matrices/expressions/tests/test_applyfunc.py,sha256=mxTJaoB4Ze50lk-2TgVopmrrbuQbEqUsZwc3K1H8w-Q,3522 +sympy/matrices/expressions/tests/test_blockmatrix.py,sha256=ANNR7e2eiGIvabMNezxRVzipmA8oUwmDTrBTz5ALMzU,16541 +sympy/matrices/expressions/tests/test_companion.py,sha256=Lam6r-cSOokjhSlJws55Kq-gL5_pHfeV_Xuvmn5PkRU,1657 +sympy/matrices/expressions/tests/test_derivatives.py,sha256=9mBeaAZDX7-JbYs6tMClNuGDygETVN_dCXSlHmyAhwg,15991 +sympy/matrices/expressions/tests/test_determinant.py,sha256=JSgptLz9KNC4_X27qnuFq-rscgHk6144s5TEUQpLxr0,2067 +sympy/matrices/expressions/tests/test_diagonal.py,sha256=3L6Vs_Yr36a8dgIqAeIcNEf0xcVyeyGhANNu0dlIpwI,4516 +sympy/matrices/expressions/tests/test_dotproduct.py,sha256=Zkv2N6oRPm0-sN4PFwsVFrM5Y_qv4x2gWqQQQD86hBY,1171 +sympy/matrices/expressions/tests/test_factorizations.py,sha256=6UPA_UhCL5JPbaQCOatMnxhGnQ-aIHmb3lXqbwrSoIE,786 +sympy/matrices/expressions/tests/test_fourier.py,sha256=0eD69faoHXBcuQ7g2Q31fqs-gyR_Xfe-gv-7DXhJh_c,1638 +sympy/matrices/expressions/tests/test_funcmatrix.py,sha256=uN9r0ECMIBqsIzOezg_n9uDYNs6ebYS8Yf5yexUjmAM,2230 +sympy/matrices/expressions/tests/test_hadamard.py,sha256=rR0l1howrI8SaJOnLb0fsCXS5cIx1rzahwFTfGldp3Y,4614 +sympy/matrices/expressions/tests/test_indexing.py,sha256=wwYQa7LNlzhBA5fU50gPyE8cqaJf0s3O70PUx4eNCEA,12038 +sympy/matrices/expressions/tests/test_inverse.py,sha256=n4gwv-GH0LPXZDVgzEB0lA_fk8MmNFK_BVNJb0FEUfY,2320 +sympy/matrices/expressions/tests/test_kronecker.py,sha256=e5H6av3ioOn8jkjyDBrT3NEmCkyHbN6ZEHOlyB9OYLk,5366 +sympy/matrices/expressions/tests/test_matadd.py,sha256=U1fL5YLP_cYEOsdi2uaGGrzm8qOsKcXn69BC1UV6RMM,1866 +sympy/matrices/expressions/tests/test_matexpr.py,sha256=pQkxhi8okFDbF6Uea-dbeRrjNfmlc6nzJdIXOJxAqUI,18441 +sympy/matrices/expressions/tests/test_matmul.py,sha256=2ofS4YHHIcI9DamZNU-8RhBQTnNogqGknKxqkyKXO8U,6187 +sympy/matrices/expressions/tests/test_matpow.py,sha256=dRbwvZ3vxwnqw09lnRqOu475f46xDPNo7V3oc1d_P2U,7308 +sympy/matrices/expressions/tests/test_permutation.py,sha256=93Cqjj2k3aoR3ayMJLdJUa5h1u87bRRxT3I8B4FQsvU,5607 +sympy/matrices/expressions/tests/test_sets.py,sha256=DfFGe6W1ppUs6bgo3vB3DSJvFemrT68s0F3QbyoIJiE,1408 +sympy/matrices/expressions/tests/test_slice.py,sha256=C7OGAQQTz0YZxZCa7g0m8_0Bqq8jaPRa22JHVSqK7tY,2027 +sympy/matrices/expressions/tests/test_special.py,sha256=Mhg71vnjjb4fm0jZgjDoWW8rAJMBeh8aDCM75gjEpKQ,6496 +sympy/matrices/expressions/tests/test_trace.py,sha256=fRlrw9CfdO3z3SI4TQb1fCUb_zVAndbtyOErEeCTCQ0,3383 +sympy/matrices/expressions/tests/test_transpose.py,sha256=P3wPPRywKnrAppX6gssgD66v0RIcolxqDkCaKGGPVcM,1987 +sympy/matrices/expressions/trace.py,sha256=skr53LvstLV5Yg9hkaRb0yWrTdxC9u95G-YzIc80aTs,5362 +sympy/matrices/expressions/transpose.py,sha256=QGQ1bgqvYmRNs6QiVolhtFlbluPYpwW3UNvkRZUqUHU,2645 +sympy/matrices/graph.py,sha256=4UBv9SI5Z8Xjc5jPJVOoPVGQDtfbyOmS8f0ENrPOuU8,9080 +sympy/matrices/immutable.py,sha256=okpJZ41FnaHp1PpwnPNCnB_o3afAU7DgRsr2NqBKtvg,5530 +sympy/matrices/inverse.py,sha256=aVjDn_SjZUfi-jdM_1uJ6u6lVItuvbnb3CppayHQ-Gs,13166 +sympy/matrices/kind.py,sha256=EJxDdD4gFgvVfC3lRex-bczhcGjwBhglf1hPDk2WzXE,2843 +sympy/matrices/matrices.py,sha256=iqgi7x7cjnLT6OuRp6TYjoObJtUtbcPpeGTOC-Enh9Q,23536 +sympy/matrices/matrixbase.py,sha256=snpv-ZBFi-CRtfMwY9lukSTPpELVXmJF16Gn8xPV3yI,165154 +sympy/matrices/normalforms.py,sha256=mX19wvJqQAirRiPY-WdSfEdljFOhst18LcDX4MvPM8A,4636 +sympy/matrices/reductions.py,sha256=MZ8LtryawSm1jXsWzRPsYBmir867lRUWX4LFNGVIHwI,12500 +sympy/matrices/repmatrix.py,sha256=d2vne9qNn1cu7dQbCdkftMPadA87evGMaoE7LkxaYXE,29914 +sympy/matrices/solvers.py,sha256=kWkUdy0XoWbn7RY4S1N0qNgxKAcUrHiTlicb25rR9z4,25163 +sympy/matrices/sparse.py,sha256=ER4tfnpvAMeockzWi10mKdKZJSYJNPISyoUL_m29UJ8,14673 +sympy/matrices/sparsetools.py,sha256=tzI541P8QW_v1eVJAXgOlo_KK1Xp6u1geawX_tdlBxY,9182 +sympy/matrices/subspaces.py,sha256=uLo4qnP0xvFcFo5hhf6g7pHSHiRbcQ1ATDKwGBxW7CE,3761 +sympy/matrices/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/matrices/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/matrices/tests/__pycache__/test_commonmatrix.cpython-311.pyc,, +sympy/matrices/tests/__pycache__/test_decompositions.cpython-311.pyc,, +sympy/matrices/tests/__pycache__/test_determinant.cpython-311.pyc,, +sympy/matrices/tests/__pycache__/test_domains.cpython-311.pyc,, +sympy/matrices/tests/__pycache__/test_eigen.cpython-311.pyc,, +sympy/matrices/tests/__pycache__/test_graph.cpython-311.pyc,, +sympy/matrices/tests/__pycache__/test_immutable.cpython-311.pyc,, +sympy/matrices/tests/__pycache__/test_interactions.cpython-311.pyc,, +sympy/matrices/tests/__pycache__/test_matrices.cpython-311.pyc,, +sympy/matrices/tests/__pycache__/test_matrixbase.cpython-311.pyc,, +sympy/matrices/tests/__pycache__/test_normalforms.cpython-311.pyc,, +sympy/matrices/tests/__pycache__/test_reductions.cpython-311.pyc,, +sympy/matrices/tests/__pycache__/test_repmatrix.cpython-311.pyc,, +sympy/matrices/tests/__pycache__/test_solvers.cpython-311.pyc,, +sympy/matrices/tests/__pycache__/test_sparse.cpython-311.pyc,, +sympy/matrices/tests/__pycache__/test_sparsetools.cpython-311.pyc,, +sympy/matrices/tests/__pycache__/test_subspaces.cpython-311.pyc,, +sympy/matrices/tests/test_commonmatrix.py,sha256=CEYYa5frGDGPAVz78uYVTBC3IHoQLG18VRnq289XtBQ,40772 +sympy/matrices/tests/test_decompositions.py,sha256=LTm3KjUHh0QkGmR8UA1CwGWzpFu8o-qcl4sv2VKBi8g,14419 +sympy/matrices/tests/test_determinant.py,sha256=FxbLYbiO1wj12YYfsbQPoEOcgWpRGVu1CzVKLx4e8oQ,9560 +sympy/matrices/tests/test_domains.py,sha256=gUnMLr_LeEsN5unFjIwHCZje0URC9uVBN_Q76JbX9f4,3276 +sympy/matrices/tests/test_eigen.py,sha256=Xm-Xe7qGACLpqwOx5W2mDjve2KyJf-q8PyEi40jMgAk,22722 +sympy/matrices/tests/test_graph.py,sha256=ckfGDCg2M6gluv9XFnfURga8gxd2HTL7aX281s6wy6c,3213 +sympy/matrices/tests/test_immutable.py,sha256=JSu6YlGtPP-5iialCeatCKbZ4ScLDUzhQ-TMGhsalp8,4616 +sympy/matrices/tests/test_interactions.py,sha256=6T6wkHyTW5v2fwg0rz2HULoDElfA_NttApU2t-pZFKI,2070 +sympy/matrices/tests/test_matrices.py,sha256=YrwrczVsTuLoPAAkh4dlZYbfUcVEUe88wyM53fmUOtw,163196 +sympy/matrices/tests/test_matrixbase.py,sha256=2EUoz0Dl3hPFNvWU--ls6RPGfllZJbVvZSsDA9D6jPo,168868 +sympy/matrices/tests/test_normalforms.py,sha256=leFZ2rJGrWKv8Zz8aMaHQiNrCEQWOG1RqQ87omvlqIk,3730 +sympy/matrices/tests/test_reductions.py,sha256=tKv_KufpVc6qwH-MDz8XLpgVS4z6snnlNdd1ECAQSXM,13385 +sympy/matrices/tests/test_repmatrix.py,sha256=enu29IYEPzvKvGFeN6Bi9KYKqcXdy0EucifN9fV420Q,2084 +sympy/matrices/tests/test_solvers.py,sha256=55Zmvp2KC6OpdXFHBkXZRBRls_8wHfYNWO2UOQh9AoE,22311 +sympy/matrices/tests/test_sparse.py,sha256=GvXN6kBVldjqoR8WN8I_PjblKhRmyRWvVuLUgZEgugY,23281 +sympy/matrices/tests/test_sparsetools.py,sha256=pjQR6UaEMR92NolB_IGZ9Umk6FPZjvI0vk1Fd4H_C5I,4877 +sympy/matrices/tests/test_subspaces.py,sha256=poY6k6l2LSL7OCixQNGzrauLZIYbrjDul7J-yEE02S8,3465 +sympy/matrices/utilities.py,sha256=mMnNsDTxGKqiG0JATsM4W9b5jglhacy-vmRw2aZojgY,2117 +sympy/multipledispatch/__init__.py,sha256=aV2NC2cO_KmD6QFiwy4oC1D8fm3pFuPbaiTMeWmNWak,259 +sympy/multipledispatch/__pycache__/__init__.cpython-311.pyc,, +sympy/multipledispatch/__pycache__/conflict.cpython-311.pyc,, +sympy/multipledispatch/__pycache__/core.cpython-311.pyc,, +sympy/multipledispatch/__pycache__/dispatcher.cpython-311.pyc,, +sympy/multipledispatch/__pycache__/utils.cpython-311.pyc,, +sympy/multipledispatch/conflict.py,sha256=rR6tKn58MfhMMKZ4ZrhVduylXd9f5PjT2TpzM9LMB6o,2117 +sympy/multipledispatch/core.py,sha256=I4WOnmu1VtlaCnn2oD9R2-xckkYLRZPNFEWtCOTAYfM,2261 +sympy/multipledispatch/dispatcher.py,sha256=A2I4upt4qNollXGpwzrqg7M0oKHJhZx1BUMIBnjRIow,12226 +sympy/multipledispatch/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/multipledispatch/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/multipledispatch/tests/__pycache__/test_conflict.cpython-311.pyc,, +sympy/multipledispatch/tests/__pycache__/test_core.cpython-311.pyc,, +sympy/multipledispatch/tests/__pycache__/test_dispatcher.cpython-311.pyc,, +sympy/multipledispatch/tests/test_conflict.py,sha256=msNVSiikuPOqsEm_MMGmjsNbA2CAR0F1FZaHskzzo04,1786 +sympy/multipledispatch/tests/test_core.py,sha256=UfH_7cyvZ6PHjdH8vmLG49CG7E30W8uxm3FthuMc1Jk,4048 +sympy/multipledispatch/tests/test_dispatcher.py,sha256=saJPpGXLpLOuRfw-ekzZGzY-Rys0NsS5ke0n33i9j0U,6228 +sympy/multipledispatch/utils.py,sha256=39wB9i8jNhlLFZyCTFnioLx5N_CNWv4r5VZwKrxswIE,3097 +sympy/ntheory/__init__.py,sha256=WpYPsusKb9OQ1xmXxZFla7kXacIWv4lNIpFIEVVzTLA,2810 +sympy/ntheory/__pycache__/__init__.cpython-311.pyc,, +sympy/ntheory/__pycache__/bbp_pi.cpython-311.pyc,, +sympy/ntheory/__pycache__/continued_fraction.cpython-311.pyc,, +sympy/ntheory/__pycache__/digits.cpython-311.pyc,, +sympy/ntheory/__pycache__/ecm.cpython-311.pyc,, +sympy/ntheory/__pycache__/egyptian_fraction.cpython-311.pyc,, +sympy/ntheory/__pycache__/elliptic_curve.cpython-311.pyc,, +sympy/ntheory/__pycache__/factor_.cpython-311.pyc,, +sympy/ntheory/__pycache__/generate.cpython-311.pyc,, +sympy/ntheory/__pycache__/modular.cpython-311.pyc,, +sympy/ntheory/__pycache__/multinomial.cpython-311.pyc,, +sympy/ntheory/__pycache__/partitions_.cpython-311.pyc,, +sympy/ntheory/__pycache__/primetest.cpython-311.pyc,, +sympy/ntheory/__pycache__/qs.cpython-311.pyc,, +sympy/ntheory/__pycache__/residue_ntheory.cpython-311.pyc,, +sympy/ntheory/bbp_pi.py,sha256=ILur1c9Ja-1F_blgnInUx-WopQ_WSvK-2OvNLEe2Zx8,5998 +sympy/ntheory/continued_fraction.py,sha256=bQW7PvdgDtWnbpCmkOwyz3mNYPOXh9_ehq3_ZpJO8Rw,10717 +sympy/ntheory/digits.py,sha256=ea3xSLy8RMaMHsekg8nq2gnVTug7SOvaDjw0lmB6NMU,3831 +sympy/ntheory/ecm.py,sha256=zg_GhL0XSyG5ZVSdGv3YceCguPZrFaS-hC-vXqOVOGQ,11793 +sympy/ntheory/egyptian_fraction.py,sha256=hW886hPWJtARqgZIrH1WjZFC0uvf9CHxMIn0X9MWZro,6923 +sympy/ntheory/elliptic_curve.py,sha256=ZT677EHi26BkZdRTLs1Tf4VSyMxDzJrwk1xECoMT7Qg,11544 +sympy/ntheory/factor_.py,sha256=MQQJSnjSpz-B9ex9fqMCnrrNTfqFOlhJV9RAWy3gJTw,83551 +sympy/ntheory/generate.py,sha256=B6LVkK627xWzQ1UE5XoeMxNM7Q1O8EwcP7fwKs5vkZk,33376 +sympy/ntheory/modular.py,sha256=wYNfNr5S8DqipQNBLVMR7cPUNg7twM25xcLhXBD14I4,8474 +sympy/ntheory/multinomial.py,sha256=rbm3STjgfRbNVbcPeH69qtWktthSCk0sC373NuDM6fU,5073 +sympy/ntheory/partitions_.py,sha256=S81zrWeTVHmYAphcP6yUGLz5CrlUwW_JFtkQRoeLlV4,8995 +sympy/ntheory/primetest.py,sha256=9EVL1yeBkOHDfa2phHiDwUnmidSJJrTl6Wzo0y1Sck0,25423 +sympy/ntheory/qs.py,sha256=8ZKxvqc-oGPqZNbEH-XcPsjXLy5-5aygVB5rR1BkSO8,14938 +sympy/ntheory/residue_ntheory.py,sha256=dDBaogSqroJHlyXRqnLmuzNVyXfy4rWB_hlhOCNQHpk,54359 +sympy/ntheory/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/ntheory/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/ntheory/tests/__pycache__/test_bbp_pi.cpython-311.pyc,, +sympy/ntheory/tests/__pycache__/test_continued_fraction.cpython-311.pyc,, +sympy/ntheory/tests/__pycache__/test_digits.cpython-311.pyc,, +sympy/ntheory/tests/__pycache__/test_ecm.cpython-311.pyc,, +sympy/ntheory/tests/__pycache__/test_egyptian_fraction.cpython-311.pyc,, +sympy/ntheory/tests/__pycache__/test_elliptic_curve.cpython-311.pyc,, +sympy/ntheory/tests/__pycache__/test_factor_.cpython-311.pyc,, +sympy/ntheory/tests/__pycache__/test_generate.cpython-311.pyc,, +sympy/ntheory/tests/__pycache__/test_hypothesis.cpython-311.pyc,, +sympy/ntheory/tests/__pycache__/test_modular.cpython-311.pyc,, +sympy/ntheory/tests/__pycache__/test_multinomial.cpython-311.pyc,, +sympy/ntheory/tests/__pycache__/test_partitions.cpython-311.pyc,, +sympy/ntheory/tests/__pycache__/test_primetest.cpython-311.pyc,, +sympy/ntheory/tests/__pycache__/test_qs.cpython-311.pyc,, +sympy/ntheory/tests/__pycache__/test_residue.cpython-311.pyc,, +sympy/ntheory/tests/test_bbp_pi.py,sha256=TgNpZOtKAfU-IVzO62Ko7Oy3LLNfdzEo9gWMTTDZL6c,9486 +sympy/ntheory/tests/test_continued_fraction.py,sha256=JJsyEXatgjNxYZTKkKubvymyVs0WDpLfbEiAY5SYk8g,3583 +sympy/ntheory/tests/test_digits.py,sha256=mrTfwboMCQkiOEpMgYg8Nrk12WE5pEtpmbQthRVt4Xc,1968 +sympy/ntheory/tests/test_ecm.py,sha256=yco77gknWe6co4VKTCoRNKHzd3jdqGKQWQFwuziYNWI,2290 +sympy/ntheory/tests/test_egyptian_fraction.py,sha256=tpHcwteuuQAahcPqvgBm4Mwq-efzcHOn8mldijynjlE,2378 +sympy/ntheory/tests/test_elliptic_curve.py,sha256=wc0EOsGo-qGpdevRq1o64htwTOT_YSUzUfyhJC-JVbg,624 +sympy/ntheory/tests/test_factor_.py,sha256=Z4B33C0blhcG9KuQzmJcDw7TtrTYJ6jAsWQd8Oi3YZw,26382 +sympy/ntheory/tests/test_generate.py,sha256=lhnJPjlz1TYrDHJ4Jq0F64P4KV8C7ngKmm3Jxtz7Wsk,9868 +sympy/ntheory/tests/test_hypothesis.py,sha256=Ztg-QoiBxpUp6euPy1RcPbF6yaLK_ij-Jcl637GGhNY,728 +sympy/ntheory/tests/test_modular.py,sha256=g73sUXtYNxzbDcq5UnMWT8NodAU8unwRj_E-PpvJqDs,1425 +sympy/ntheory/tests/test_multinomial.py,sha256=8uuj6XlatNyIILOpjJap13CMZmDwrCyGKn9LiIUiLV0,2344 +sympy/ntheory/tests/test_partitions.py,sha256=qkd-84AO0rpJv-MQW0lnXtIPtGTAfplFbu2ezKSfEQI,1088 +sympy/ntheory/tests/test_primetest.py,sha256=Lp4elAnnBDYSIPfVXENOUjZYuwAynsNQrE_CEiYDErU,9495 +sympy/ntheory/tests/test_qs.py,sha256=zbwQ1k5ywsUVNZzPewWul_yUnoqWxxvsSWOBAU5Albc,3956 +sympy/ntheory/tests/test_residue.py,sha256=7VGXsslZWVd3R7tAtIcLjYyh0TjZpb5BtG2GQehlAUY,16809 +sympy/parsing/__init__.py,sha256=KHuyDeHY1ifpVxT4aTOhomazCBYVIrKWd28jqp6YNJ8,125 +sympy/parsing/__pycache__/__init__.cpython-311.pyc,, +sympy/parsing/__pycache__/ast_parser.cpython-311.pyc,, +sympy/parsing/__pycache__/mathematica.cpython-311.pyc,, +sympy/parsing/__pycache__/maxima.cpython-311.pyc,, +sympy/parsing/__pycache__/sym_expr.cpython-311.pyc,, +sympy/parsing/__pycache__/sympy_parser.cpython-311.pyc,, +sympy/parsing/ast_parser.py,sha256=iJvr6bhm1RjM5rhWzZA4c4LGTH5lAFazN5zu8y8q-aY,2734 +sympy/parsing/autolev/Autolev.g4,sha256=980mo25mLWrQFmhRIg-aqIalUuwktYYaBGTXZ5_XZwA,4195 +sympy/parsing/autolev/__init__.py,sha256=sp5hzv5siVW3xUmhkp0S0iaA0Cz-PVB0HO1zC04pxYs,3611 +sympy/parsing/autolev/__pycache__/__init__.cpython-311.pyc,, +sympy/parsing/autolev/__pycache__/_build_autolev_antlr.cpython-311.pyc,, +sympy/parsing/autolev/__pycache__/_listener_autolev_antlr.cpython-311.pyc,, +sympy/parsing/autolev/__pycache__/_parse_autolev_antlr.cpython-311.pyc,, +sympy/parsing/autolev/_antlr/__init__.py,sha256=MQ4ZacpTuP-NmruFXKdWLQatoeVJQ8SaBQ2DnYvtyE8,203 +sympy/parsing/autolev/_antlr/__pycache__/__init__.cpython-311.pyc,, +sympy/parsing/autolev/_antlr/__pycache__/autolevlexer.cpython-311.pyc,, +sympy/parsing/autolev/_antlr/__pycache__/autolevlistener.cpython-311.pyc,, +sympy/parsing/autolev/_antlr/__pycache__/autolevparser.cpython-311.pyc,, +sympy/parsing/autolev/_antlr/autolevlexer.py,sha256=K7HF_-5dUyAIv1_7GkhTmxqSCanEhCpzJG8fayAEB3Q,13609 +sympy/parsing/autolev/_antlr/autolevlistener.py,sha256=EDb3XkH9Y7CLzxGM-tY-nGqxMGfBHVkqKdVCPxABgRE,12821 +sympy/parsing/autolev/_antlr/autolevparser.py,sha256=BZYJ7IkurRmm44S50pYp_9JHCjT8fr1w5HeksAEPjtg,106291 +sympy/parsing/autolev/_build_autolev_antlr.py,sha256=0oIrC3sWUDe18zydeNyRmQrGGwPtRYQzIul8YcmMpk4,2578 +sympy/parsing/autolev/_listener_autolev_antlr.py,sha256=jv1jYvmrGmom6F3fhYTHG1sqLK6FFCO1-jrvDW-nPEM,104760 +sympy/parsing/autolev/_parse_autolev_antlr.py,sha256=b9hIaluJUd1V2XIAp1erak6U-c-CwKyDLH1UkYQuvKE,1736 +sympy/parsing/autolev/test-examples/README.txt,sha256=0C4m_nLROeV5J8nMfm3RYEfYgQJqmlHZaCpVD24boQY,528 +sympy/parsing/autolev/test-examples/__pycache__/ruletest1.cpython-311.pyc,, +sympy/parsing/autolev/test-examples/__pycache__/ruletest10.cpython-311.pyc,, +sympy/parsing/autolev/test-examples/__pycache__/ruletest11.cpython-311.pyc,, +sympy/parsing/autolev/test-examples/__pycache__/ruletest12.cpython-311.pyc,, +sympy/parsing/autolev/test-examples/__pycache__/ruletest2.cpython-311.pyc,, +sympy/parsing/autolev/test-examples/__pycache__/ruletest3.cpython-311.pyc,, +sympy/parsing/autolev/test-examples/__pycache__/ruletest4.cpython-311.pyc,, +sympy/parsing/autolev/test-examples/__pycache__/ruletest5.cpython-311.pyc,, +sympy/parsing/autolev/test-examples/__pycache__/ruletest6.cpython-311.pyc,, +sympy/parsing/autolev/test-examples/__pycache__/ruletest7.cpython-311.pyc,, +sympy/parsing/autolev/test-examples/__pycache__/ruletest8.cpython-311.pyc,, +sympy/parsing/autolev/test-examples/__pycache__/ruletest9.cpython-311.pyc,, +sympy/parsing/autolev/test-examples/pydy-example-repo/__pycache__/chaos_pendulum.cpython-311.pyc,, +sympy/parsing/autolev/test-examples/pydy-example-repo/__pycache__/double_pendulum.cpython-311.pyc,, +sympy/parsing/autolev/test-examples/pydy-example-repo/__pycache__/mass_spring_damper.cpython-311.pyc,, +sympy/parsing/autolev/test-examples/pydy-example-repo/__pycache__/non_min_pendulum.cpython-311.pyc,, +sympy/parsing/autolev/test-examples/pydy-example-repo/chaos_pendulum.al,sha256=HpTcX2wXzLqmgpp8fcSqNweKjxljk43iYK0wQmBbCDI,690 +sympy/parsing/autolev/test-examples/pydy-example-repo/chaos_pendulum.py,sha256=FSu4TP2BDTQjzYhMkcpRhXbb3kAD27XCyO_EoL55Ack,2274 +sympy/parsing/autolev/test-examples/pydy-example-repo/double_pendulum.al,sha256=wjeeRdCS3Es6ldX9Ug5Du1uaijUTyoXpfTqmhL0uYfk,427 +sympy/parsing/autolev/test-examples/pydy-example-repo/double_pendulum.py,sha256=uU9azTUGrY15BSDtw5T_V-7gmjyhHbXslzkmwBvFjGk,1583 +sympy/parsing/autolev/test-examples/pydy-example-repo/mass_spring_damper.al,sha256=Gf7OhgRlwqUEXq7rkfbf89yWA23u4uIUJ-buXTyOuXM,505 +sympy/parsing/autolev/test-examples/pydy-example-repo/mass_spring_damper.py,sha256=9ReCAqcUH5HYBgHmop9h5Zx54mfScWZN5L5F6rCHk4w,1366 +sympy/parsing/autolev/test-examples/pydy-example-repo/non_min_pendulum.al,sha256=p5v40h1nVFrWNqnB0K7GiNQT0b-MqwayYjZxXOY4M8M,362 +sympy/parsing/autolev/test-examples/pydy-example-repo/non_min_pendulum.py,sha256=DdxcWrm3HMQuyyY3Pk6sKHb4RXhQEM_EKY3HYZCP8ec,1503 +sympy/parsing/autolev/test-examples/ruletest1.al,sha256=mDJ02Q1Qm-ShVmGoyjzSfgDJHUOuDrsUg3YMnkpKdUw,176 +sympy/parsing/autolev/test-examples/ruletest1.py,sha256=eIKEFzEwkCFhPF0GTmf6SLuxXT384GqdCJnhiL2U0BQ,555 +sympy/parsing/autolev/test-examples/ruletest10.al,sha256=jKpV8BgX91iQsQDLFOJyaS396AyE5YQlUMxih5o9RK0,781 +sympy/parsing/autolev/test-examples/ruletest10.py,sha256=I1tsQcSAW6wqIguF-7lwlj9D4YZ8kCZqPqTKPUHR9oI,2726 +sympy/parsing/autolev/test-examples/ruletest11.al,sha256=j_q7giq2KIuXVRLWwNlwIlpbhNO6SqBMnLGLcxIkzwk,188 +sympy/parsing/autolev/test-examples/ruletest11.py,sha256=dYTRtXvMDXHiKzXHD2Sh0fcEukob3wr_GbSeqaZrrO8,475 +sympy/parsing/autolev/test-examples/ruletest12.al,sha256=drr2NLrK1ewn4FjMppXycpAUNbZEQ0IAMsdVx8nxk6I,185 +sympy/parsing/autolev/test-examples/ruletest12.py,sha256=ZG36s3PnkT0aKBM9Nx6H0sdJrtoLwaebU9386YSUql8,472 +sympy/parsing/autolev/test-examples/ruletest2.al,sha256=d-QjPpW0lzugaGBg8F6pDl_5sZHOR_EDJ8EvWLcz4FY,237 +sympy/parsing/autolev/test-examples/ruletest2.py,sha256=jrJfb0Jk2FP4GS5pDa0UB5ph0ijEVd1X8meKeZrTVng,820 +sympy/parsing/autolev/test-examples/ruletest3.al,sha256=1TAaOe8GI8-yBWJddfIxwnvScHNmOjSzSaQn0RS_v5k,308 +sympy/parsing/autolev/test-examples/ruletest3.py,sha256=O3K3IQo-HCjAIOSkfz3bDlst7dVUiRwhOZ0q_3jb5LU,1574 +sympy/parsing/autolev/test-examples/ruletest4.al,sha256=qPGlPbdDRrzTDUBeWydAIa7mbjs2o3uX938QAsWJ7Qk,302 +sympy/parsing/autolev/test-examples/ruletest4.py,sha256=WHod5yzKF4TNbEf4Yfxmx9WnimA7NOXqtTjZXR8FsP0,682 +sympy/parsing/autolev/test-examples/ruletest5.al,sha256=VuiKjiFmLK3uEdho0m3pk-n0qm4SNLoLPMRJqjMJ4GY,516 +sympy/parsing/autolev/test-examples/ruletest5.py,sha256=WvUtno1D3BrmFNPYYIBKR_gOA-PaHoxLlSTNDX67dcQ,1991 +sympy/parsing/autolev/test-examples/ruletest6.al,sha256=-HwgTmh_6X3wHjo3PQi7378t8YdizRJClc5Eb5DmjhE,703 +sympy/parsing/autolev/test-examples/ruletest6.py,sha256=vEO0jMOD-KIevAcVexmpvac0MGjN7O_dNipOBJJNzF0,1473 +sympy/parsing/autolev/test-examples/ruletest7.al,sha256=wR9S9rTzO9fyKL6Ofgwzw8XCFCV_p2hBpYotC8TvADI,773 +sympy/parsing/autolev/test-examples/ruletest7.py,sha256=_XvMrMe5r9RLopTrIqMGLhaYvHL1qjteWz9CKcotCL8,1696 +sympy/parsing/autolev/test-examples/ruletest8.al,sha256=P7Nu3Pq2R1mKcuFRc9dRO5jJ1_e5fwWdtqYG8NHVVds,682 +sympy/parsing/autolev/test-examples/ruletest8.py,sha256=8tgbwJ-ir0wiOCsgIFCAu4uD8SieYRrLoLzEfae5YQY,2690 +sympy/parsing/autolev/test-examples/ruletest9.al,sha256=txtZ5RH2p1FvAe6etwetSCH8rLktnpk5z0W72sCOdAA,755 +sympy/parsing/autolev/test-examples/ruletest9.py,sha256=GtqV-Wq2GGJzfblMscAz-KXCzs0P_4XqvA3FIdlPe04,1965 +sympy/parsing/c/__init__.py,sha256=J9CvkNRY-qy6CA06GZYuwTuxdnqas6oUP2g0qLztGro,65 +sympy/parsing/c/__pycache__/__init__.cpython-311.pyc,, +sympy/parsing/c/__pycache__/c_parser.cpython-311.pyc,, +sympy/parsing/c/c_parser.py,sha256=fM1GGRIbjzrVQ7ErfkWz6Xzfjvym030BOoWlQLx37Oc,38124 +sympy/parsing/fortran/__init__.py,sha256=KraiVw2qxIgYeMRTFjs1vkMi-hqqDkxUBv8Rc2gwkCI,73 +sympy/parsing/fortran/__pycache__/__init__.cpython-311.pyc,, +sympy/parsing/fortran/__pycache__/fortran_parser.cpython-311.pyc,, +sympy/parsing/fortran/fortran_parser.py,sha256=RpNQR3eNx5vgfzdt0nEZDCB56kF__SnYMaqWN3zla00,11483 +sympy/parsing/latex/LICENSE.txt,sha256=AHvDClj6QKmW53IEcSDeTq8x9REOT5w7X5P8374urKE,1075 +sympy/parsing/latex/LaTeX.g4,sha256=fG0ZUQPwYQOIbcyaPDAkGvcfGs3ZwwMB8ZnKW5yHUDY,5821 +sympy/parsing/latex/__init__.py,sha256=CfRRwZZo2qMF2HksyjGer8qSwhx0T4wnXsa2HKxCvUE,8968 +sympy/parsing/latex/__pycache__/__init__.cpython-311.pyc,, +sympy/parsing/latex/__pycache__/_build_latex_antlr.cpython-311.pyc,, +sympy/parsing/latex/__pycache__/_parse_latex_antlr.cpython-311.pyc,, +sympy/parsing/latex/__pycache__/errors.cpython-311.pyc,, +sympy/parsing/latex/_antlr/__init__.py,sha256=TAb79senorEsoYLCLwUa8wg8AUCHzmmZ7tLdi0XGNaE,384 +sympy/parsing/latex/_antlr/__pycache__/__init__.cpython-311.pyc,, +sympy/parsing/latex/_antlr/__pycache__/latexlexer.cpython-311.pyc,, +sympy/parsing/latex/_antlr/__pycache__/latexparser.cpython-311.pyc,, +sympy/parsing/latex/_antlr/latexlexer.py,sha256=Y1hmY1VGL5FTSSlToTRQydPnyaLLNy1mDSWx76HaYwM,30502 +sympy/parsing/latex/_antlr/latexparser.py,sha256=ZvonpvTS3vLSOVpas88M3CfNnUhPUDsCCPPk4wBYUGE,123655 +sympy/parsing/latex/_build_latex_antlr.py,sha256=3Mwip9f_yXUNhiwwUTCG5Nk8l2uzAw8oZHU0HSl5gkE,2765 +sympy/parsing/latex/_parse_latex_antlr.py,sha256=sVaO04oSeHe_TaMeM-6toheCR88G_RmJYpUIx-Sef1g,20712 +sympy/parsing/latex/errors.py,sha256=adSpvQyWjTLsbN_2KHJ4HuXpY7_U9noeWiG0lskYLgE,45 +sympy/parsing/latex/lark/__init__.py,sha256=hhhvfKRGP3ON36wRJwVfxMWw_GA6rl0JKsIzjuaUX38,120 +sympy/parsing/latex/lark/__pycache__/__init__.cpython-311.pyc,, +sympy/parsing/latex/lark/__pycache__/latex_parser.cpython-311.pyc,, +sympy/parsing/latex/lark/__pycache__/transformer.cpython-311.pyc,, +sympy/parsing/latex/lark/grammar/greek_symbols.lark,sha256=-G8JGrBredhWAzCaurr1UmqgRMRrAJfs_pANub8kXyA,937 +sympy/parsing/latex/lark/grammar/latex.lark,sha256=yz3DXIHllvRDGHeBmpz49PzDCU92pKexg2RTKX4KhTE,13221 +sympy/parsing/latex/lark/latex_parser.py,sha256=lGFEo_RxSKXYmjVEb_E3DiZ-aXl5tAZyfHklzEuJ9VA,4430 +sympy/parsing/latex/lark/transformer.py,sha256=RuXe8enUnLrzUFC9ZfKQyQ1_IlgA-qPINNng6UnY1x4,25754 +sympy/parsing/mathematica.py,sha256=P2vWEgx_HXv4I_C-6vPE3RXtbpXNT2iIJQBrA6QfqkU,39614 +sympy/parsing/maxima.py,sha256=DhTnXRSAceijyA1OAm86c6TyW9-aeUVoZEELGu0oZtY,1835 +sympy/parsing/sym_expr.py,sha256=-hxarp961eyLtuwUhbg3D3qzy06HrEPZEYpGVcJzAv0,8895 +sympy/parsing/sympy_parser.py,sha256=PRRhNS_0LKBVcHFmg7LGygzxRyQt6-vyCSMisXkRhVE,43832 +sympy/parsing/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/parsing/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/parsing/tests/__pycache__/test_ast_parser.cpython-311.pyc,, +sympy/parsing/tests/__pycache__/test_autolev.cpython-311.pyc,, +sympy/parsing/tests/__pycache__/test_c_parser.cpython-311.pyc,, +sympy/parsing/tests/__pycache__/test_custom_latex.cpython-311.pyc,, +sympy/parsing/tests/__pycache__/test_fortran_parser.cpython-311.pyc,, +sympy/parsing/tests/__pycache__/test_implicit_multiplication_application.cpython-311.pyc,, +sympy/parsing/tests/__pycache__/test_latex.cpython-311.pyc,, +sympy/parsing/tests/__pycache__/test_latex_deps.cpython-311.pyc,, +sympy/parsing/tests/__pycache__/test_latex_lark.cpython-311.pyc,, +sympy/parsing/tests/__pycache__/test_mathematica.cpython-311.pyc,, +sympy/parsing/tests/__pycache__/test_maxima.cpython-311.pyc,, +sympy/parsing/tests/__pycache__/test_sym_expr.cpython-311.pyc,, +sympy/parsing/tests/__pycache__/test_sympy_parser.cpython-311.pyc,, +sympy/parsing/tests/test_ast_parser.py,sha256=lcT8w7mn6UEZ8T-xfA4TqG4Mt7JxY00oHhOW7JtHQfY,803 +sympy/parsing/tests/test_autolev.py,sha256=tQuUFa8YqVdsHPOcUhAwlMKB8Uk08HejDhDCda8lXs0,6647 +sympy/parsing/tests/test_c_parser.py,sha256=VYl3K4if_23iIS-Be8MBSG0OKZo-6xgxHiN22laeAyo,155354 +sympy/parsing/tests/test_custom_latex.py,sha256=vX5uVHw9-UgEcRl0XVNOMgrjBbb1sXDlPLL4D8AeOiQ,2060 +sympy/parsing/tests/test_fortran_parser.py,sha256=SGbawrJ4a780TJAFVMONc7Y3Y8VYgVqsIHxVGaicbxE,11828 +sympy/parsing/tests/test_implicit_multiplication_application.py,sha256=xUlc9TKH4HjimhnvWMwCtTabHAJcHCupFdsuiOsZhUs,7389 +sympy/parsing/tests/test_latex.py,sha256=WvKNJ5mtxfzl-rBiE9hc2aEz81b5BmI3SKO7sRZiNbI,11765 +sympy/parsing/tests/test_latex_deps.py,sha256=oe5vm2eIKn05ZiCcXUaO8X6HCcRmN1qCuTsz6tB7Qrk,426 +sympy/parsing/tests/test_latex_lark.py,sha256=kXmZP-gMLwa5XAcNsd-rF_zrPok0RZuJsa6Ny6zfoLw,36059 +sympy/parsing/tests/test_mathematica.py,sha256=vAxwquc8ArTQE9UNbsO21FqSa6J17sCF-A4vhTPXLY0,13395 +sympy/parsing/tests/test_maxima.py,sha256=iIwnFm0lYD0-JcraUIymogqEMN3ji0c-0JeNFFGTEDs,1987 +sympy/parsing/tests/test_sym_expr.py,sha256=-wNR7GwvJHVmPSZxSuAuoX1_FJk83O0tcDi09qYY6Jk,5668 +sympy/parsing/tests/test_sympy_parser.py,sha256=H_dDL89ASex3sI8F92ceXIJy5Qd137rxTa20Rj-9l2A,12946 +sympy/physics/__init__.py,sha256=F_yvUMCuBq3HR-3Ai6W4oktBsXRg8KdutFLwT9FFJlY,220 +sympy/physics/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/__pycache__/hydrogen.cpython-311.pyc,, +sympy/physics/__pycache__/matrices.cpython-311.pyc,, +sympy/physics/__pycache__/paulialgebra.cpython-311.pyc,, +sympy/physics/__pycache__/pring.cpython-311.pyc,, +sympy/physics/__pycache__/qho_1d.cpython-311.pyc,, +sympy/physics/__pycache__/secondquant.cpython-311.pyc,, +sympy/physics/__pycache__/sho.cpython-311.pyc,, +sympy/physics/__pycache__/wigner.cpython-311.pyc,, +sympy/physics/biomechanics/__init__.py,sha256=dG1IoRAnmfXvSyPciqJVrPn5LLnuvbVnBt78hBG0maQ,1520 +sympy/physics/biomechanics/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/biomechanics/__pycache__/_mixin.cpython-311.pyc,, +sympy/physics/biomechanics/__pycache__/activation.cpython-311.pyc,, +sympy/physics/biomechanics/__pycache__/curve.cpython-311.pyc,, +sympy/physics/biomechanics/__pycache__/musculotendon.cpython-311.pyc,, +sympy/physics/biomechanics/_mixin.py,sha256=0D3iBqlCRmR4HXKMxyC2LvYpKGHreOuZY5dXMJioQ4A,1493 +sympy/physics/biomechanics/activation.py,sha256=rUJegXrdO1LPHnzJpVRAYnA6DsvF46ndctkBt1JHDwI,25522 +sympy/physics/biomechanics/curve.py,sha256=PezKOUytiC15oLV11GC4M6u7ndXHpwtbilqdbIiqvgw,63154 +sympy/physics/biomechanics/musculotendon.py,sha256=LrYwFnwkShDWWZQtCgtSUqvc1H5IlTj_BNVKxfFNhE0,58274 +sympy/physics/biomechanics/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/physics/biomechanics/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/biomechanics/tests/__pycache__/test_activation.cpython-311.pyc,, +sympy/physics/biomechanics/tests/__pycache__/test_curve.cpython-311.pyc,, +sympy/physics/biomechanics/tests/__pycache__/test_mixin.cpython-311.pyc,, +sympy/physics/biomechanics/tests/__pycache__/test_musculotendon.cpython-311.pyc,, +sympy/physics/biomechanics/tests/test_activation.py,sha256=hdnMsFBLjloJylu8-cLZ44oamORG3kSsI0q2-eLQ_-I,13395 +sympy/physics/biomechanics/tests/test_curve.py,sha256=3jbWEHr8ieZYCfVlwJ1KTA1WJ9dcTCyQ1p3r8G2jp1I,75800 +sympy/physics/biomechanics/tests/test_mixin.py,sha256=ds-EoUCvfiSjVGnC_mBwjn5mI7z5W5wi2UTZZ4-pIIQ,1322 +sympy/physics/biomechanics/tests/test_musculotendon.py,sha256=Ls59mtJQ83W0fdpDGFGNeTgtxL8yAZ6ODW96N9mvFtM,32906 +sympy/physics/continuum_mechanics/__init__.py,sha256=GzYypKq3so15xfvY4qFerHYCNGFzZthfQ1aOcgu8yCM,191 +sympy/physics/continuum_mechanics/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/continuum_mechanics/__pycache__/arch.cpython-311.pyc,, +sympy/physics/continuum_mechanics/__pycache__/beam.cpython-311.pyc,, +sympy/physics/continuum_mechanics/__pycache__/cable.cpython-311.pyc,, +sympy/physics/continuum_mechanics/__pycache__/truss.cpython-311.pyc,, +sympy/physics/continuum_mechanics/arch.py,sha256=TKaoHk7-ToNP0n6ZGFF_cW6x965C-D3iENiqFo74ZJU,39243 +sympy/physics/continuum_mechanics/beam.py,sha256=Vq1xJHrjatQEUigMsxlAk8qoZSyp_Z_0GiJ5y5AIMIg,159946 +sympy/physics/continuum_mechanics/cable.py,sha256=1IOpa_eLYwdyixNkDhLXJQSg4fQhwOH7Ybm1jWR4aQA,31420 +sympy/physics/continuum_mechanics/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/physics/continuum_mechanics/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/continuum_mechanics/tests/__pycache__/test_arch.cpython-311.pyc,, +sympy/physics/continuum_mechanics/tests/__pycache__/test_beam.cpython-311.pyc,, +sympy/physics/continuum_mechanics/tests/__pycache__/test_cable.cpython-311.pyc,, +sympy/physics/continuum_mechanics/tests/__pycache__/test_truss.cpython-311.pyc,, +sympy/physics/continuum_mechanics/tests/test_arch.py,sha256=t_V-Pt0fxoPqYDZl5LB6ehQ4hBBVlveRWLsEXdyo_3k,2571 +sympy/physics/continuum_mechanics/tests/test_beam.py,sha256=Wmi8BCju21H1RaomKDqFATvCUqsrca_43o5IyA5s4ok,42085 +sympy/physics/continuum_mechanics/tests/test_cable.py,sha256=aANZVJxfihRkjAauCvFPGkXKD_zxMcJFb7vXU443dsQ,3832 +sympy/physics/continuum_mechanics/tests/test_truss.py,sha256=wgtF1GbQX5hzx20UrBg2ZyEvplVpsio3DiU8CS_bAk8,3269 +sympy/physics/continuum_mechanics/truss.py,sha256=hkLLnXA9aRaE2YvA0ods7T40zJQXUc_jLI108NjhtTI,44962 +sympy/physics/control/__init__.py,sha256=U0BeOmFRXOw-cpZ_F-RzNuEh78Tqzx31nuvZMZW2Fo8,1334 +sympy/physics/control/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/control/__pycache__/control_plots.cpython-311.pyc,, +sympy/physics/control/__pycache__/lti.cpython-311.pyc,, +sympy/physics/control/control_plots.py,sha256=lYa42ATTrSXMV44Q3POfEyYQCFSTV1W5Rb4UejYFdT8,37515 +sympy/physics/control/lti.py,sha256=o6n-qFXriBeETr4anCgOuYViUpQXDtBejckIdYdlxx0,177322 +sympy/physics/control/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/physics/control/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/control/tests/__pycache__/test_control_plots.cpython-311.pyc,, +sympy/physics/control/tests/__pycache__/test_lti.cpython-311.pyc,, +sympy/physics/control/tests/test_control_plots.py,sha256=dsXkabVUMzU6b1FmptRz2sGgXfe6y_lu3Mm0KN8is90,16899 +sympy/physics/control/tests/test_lti.py,sha256=YAvwA11zSKRcHmgLPuCJhBJXbFrPfjUNytaQsnEsEr8,104043 +sympy/physics/hep/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/physics/hep/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/hep/__pycache__/gamma_matrices.cpython-311.pyc,, +sympy/physics/hep/gamma_matrices.py,sha256=WlSHLUtMU7NrgLyKEvTntMSYxMZq1r_6o2kqUEAdPaA,24253 +sympy/physics/hep/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/physics/hep/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/hep/tests/__pycache__/test_gamma_matrices.cpython-311.pyc,, +sympy/physics/hep/tests/test_gamma_matrices.py,sha256=iKqICj0bP7EK0sSuYFsPdPkDTbHGa6J_LMPZAzv1j4o,14722 +sympy/physics/hydrogen.py,sha256=R2wnNi1xB-WTQ8Z9aPUhX9Z8mQ8TdhCM1JAZIkyXgjw,7594 +sympy/physics/matrices.py,sha256=jHfbWkzL2myFt-39kodQo5wPubBxNZKXlljuSxZL4bE,3836 +sympy/physics/mechanics/__init__.py,sha256=7rgPJp1YItAMOc29gWqkRp68AKCdZLu4tFQG-CWgjOQ,2874 +sympy/physics/mechanics/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/mechanics/__pycache__/actuator.cpython-311.pyc,, +sympy/physics/mechanics/__pycache__/body.cpython-311.pyc,, +sympy/physics/mechanics/__pycache__/body_base.cpython-311.pyc,, +sympy/physics/mechanics/__pycache__/functions.cpython-311.pyc,, +sympy/physics/mechanics/__pycache__/inertia.cpython-311.pyc,, +sympy/physics/mechanics/__pycache__/joint.cpython-311.pyc,, +sympy/physics/mechanics/__pycache__/jointsmethod.cpython-311.pyc,, +sympy/physics/mechanics/__pycache__/kane.cpython-311.pyc,, +sympy/physics/mechanics/__pycache__/lagrange.cpython-311.pyc,, +sympy/physics/mechanics/__pycache__/linearize.cpython-311.pyc,, +sympy/physics/mechanics/__pycache__/loads.cpython-311.pyc,, +sympy/physics/mechanics/__pycache__/method.cpython-311.pyc,, +sympy/physics/mechanics/__pycache__/models.cpython-311.pyc,, +sympy/physics/mechanics/__pycache__/particle.cpython-311.pyc,, +sympy/physics/mechanics/__pycache__/pathway.cpython-311.pyc,, +sympy/physics/mechanics/__pycache__/rigidbody.cpython-311.pyc,, +sympy/physics/mechanics/__pycache__/system.cpython-311.pyc,, +sympy/physics/mechanics/__pycache__/wrapping_geometry.cpython-311.pyc,, +sympy/physics/mechanics/actuator.py,sha256=d7Ffb9vo1Tmrcfiy71_g0r1btscfvvabE-vznku3Auk,43633 +sympy/physics/mechanics/body.py,sha256=Z9ZOReEab9FHEDsyv2RfIBQM4_ripXhFXmQtu1OmN_Y,24617 +sympy/physics/mechanics/body_base.py,sha256=bwP04lWmD0iY_T0Vsn6NWMbnWyzUMFFqkAoAyKsow_c,2491 +sympy/physics/mechanics/functions.py,sha256=KsX3z33rZdMCifwyVlyKIHVJh0Fve--zs5wd1rkGMl0,25190 +sympy/physics/mechanics/inertia.py,sha256=FKcEbpYPx_26LS9sSp8lcsJ83spqwSYIyxM5q_z1mYA,6172 +sympy/physics/mechanics/joint.py,sha256=o9SWJ9pXDO-sVh0gJ6VTI0LgY019gFB5w5FP1PN--UI,84621 +sympy/physics/mechanics/jointsmethod.py,sha256=nqkXawtuxeyP0D8DEAYURCJlDYS4Eka_vXx6UZP7y74,10415 +sympy/physics/mechanics/kane.py,sha256=XyvEDNF09i5DnHf-YlCecEHDdKK9j486N2P5YD8_XaM,37118 +sympy/physics/mechanics/lagrange.py,sha256=UTmClOP-PkY8S-LcPlcOAUmshndlq6mpmZailbvy4E4,20202 +sympy/physics/mechanics/linearize.py,sha256=YtAap6TzsZJd794cdDCEDJVlMOXd_wFAMaCZZacHwbE,17242 +sympy/physics/mechanics/loads.py,sha256=jnajZOg631Aqtd0-BplehohUL991C3Cji2OZZ3MVHdk,5406 +sympy/physics/mechanics/method.py,sha256=2vFRhA79ra4HR6AzVBHMr3oNncrcqgLLMRqdyif0DrI,660 +sympy/physics/mechanics/models.py,sha256=9q1g3I2xYpuTMi-v9geswEqxJWTP3RjcOquRfzMhHzM,6463 +sympy/physics/mechanics/particle.py,sha256=YKiEBkPLVRI9foXlEe8eu8Ys5W1GyQO1oOBf0T8fHFg,5985 +sympy/physics/mechanics/pathway.py,sha256=2T71x06X8CZ20NUyXkMnP83gKsx2wJJEbPJS-1gMXFA,26555 +sympy/physics/mechanics/rigidbody.py,sha256=s5b7ynsdcMsq9PN2nM1yozNcZd08RA-MgzMjcs7jnBI,10287 +sympy/physics/mechanics/system.py,sha256=VsPPQfJs0gBLKJYNQ2ZLmD98JWLd1IZy0QGRwI21_TM,59457 +sympy/physics/mechanics/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/physics/mechanics/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_actuator.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_body.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_functions.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_inertia.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_joint.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_jointsmethod.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_kane.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_kane2.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_kane3.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_kane4.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_kane5.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_lagrange.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_lagrange2.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_linearity_of_velocity_constraints.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_linearize.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_loads.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_method.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_models.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_particle.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_pathway.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_rigidbody.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_system.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_system_class.cpython-311.pyc,, +sympy/physics/mechanics/tests/__pycache__/test_wrapping_geometry.cpython-311.pyc,, +sympy/physics/mechanics/tests/test_actuator.py,sha256=WoTnD_IGhewVOzcL7ybJXf_mrK0_E71gDcnon-3Mayg,41081 +sympy/physics/mechanics/tests/test_body.py,sha256=ygHfWeL6-3LW9gqXHfzdvqQhJWxg2-U2KQIHKqGNqKQ,12067 +sympy/physics/mechanics/tests/test_functions.py,sha256=Cl1aT3qT-0Ik6vJgS6at6PUaeWRQfpEG88QvEIxz1Bk,10447 +sympy/physics/mechanics/tests/test_inertia.py,sha256=On4K78tmq4tyr_QFs3Uo01efdlVj5R6yfn6CVs7ksnE,2726 +sympy/physics/mechanics/tests/test_joint.py,sha256=ojhQdBsWN6nR7egEhpw-LAyDRdFIPfSEsDoFL1bhzqs,57922 +sympy/physics/mechanics/tests/test_jointsmethod.py,sha256=8IUO7ntLPsydDT4YgA1FPyO5TXNpo4CIWC7O_JdWg9Q,10226 +sympy/physics/mechanics/tests/test_kane.py,sha256=K1HOm7cuPzXeO9ZmW7oAHgu-jxOg1IFqyKB3YheAbus,21545 +sympy/physics/mechanics/tests/test_kane2.py,sha256=qNLkbSV6UYclQzMDoRi_TJrJEz0-MUi7yf1e7XW4ba4,19256 +sympy/physics/mechanics/tests/test_kane3.py,sha256=-9MbXLfYUDhA_9D2o4NrNLgDTOnKDLLsgryQW3AHacs,14959 +sympy/physics/mechanics/tests/test_kane4.py,sha256=qJYUfvnq1F5UN_AQqTu_3BT5XqGceqxnCyP3d2gE04A,4709 +sympy/physics/mechanics/tests/test_kane5.py,sha256=gZvAyxJ8fkkLtH60xHlb_Gxrbt9d7VmJTckXSGmF0j4,5693 +sympy/physics/mechanics/tests/test_lagrange.py,sha256=iuHomulBF8MafLeorKGaLHUEF8CvFhXcxEtN0hk1akM,10119 +sympy/physics/mechanics/tests/test_lagrange2.py,sha256=VBonbWg0uLduE4AwNUWCsGGa_2KmaDbg9jROZL3DTWE,1402 +sympy/physics/mechanics/tests/test_linearity_of_velocity_constraints.py,sha256=Y9JYoaob8yyL7PS-VN7g-DC8YEfCxBGdFSSkQAKlOBI,1341 +sympy/physics/mechanics/tests/test_linearize.py,sha256=6yFFGEhJW60fx9Ny1duc6eyvGDg6rtmJVo_V1mgHgGk,13273 +sympy/physics/mechanics/tests/test_loads.py,sha256=Kw94kP0tfwKsV-jCDHGTQsyc-1dKQl3ABJfqJtR8AJg,2698 +sympy/physics/mechanics/tests/test_method.py,sha256=L7CnsvbQC-U7ijbSZdu7DEr03p88OLj4IPvFJ_3kCDo,154 +sympy/physics/mechanics/tests/test_models.py,sha256=GcsfCm5G4PPYQXsHCiAKI1dEW42RaZOh-x6aEouTYo4,5078 +sympy/physics/mechanics/tests/test_particle.py,sha256=JL6QAA6T3POQkSutUnungrVkR3xt6ZVX-hp75-EufQw,2682 +sympy/physics/mechanics/tests/test_pathway.py,sha256=oGCxlUOviyNc1GBMvhk5sYVZfu8C4o7lJMbqatBie3A,24944 +sympy/physics/mechanics/tests/test_rigidbody.py,sha256=ezMW5BWt9cWdNeY1B9aYcL4NsPcVkaKZuUS1C7S1qPk,6188 +sympy/physics/mechanics/tests/test_system.py,sha256=Dihu77qM5_QkDQg-zavHbVhh_nvaGEVztXgPNl2_enk,8700 +sympy/physics/mechanics/tests/test_system_class.py,sha256=Xe4VLrxWaYL3oRwP2SBaanWf5DY46d2yPlwf9H1XJ4M,38219 +sympy/physics/mechanics/tests/test_wrapping_geometry.py,sha256=aXwuEaprstnSW7BwLr3OKUyxSagRPO58L-tUMaq3I9s,10502 +sympy/physics/mechanics/wrapping_geometry.py,sha256=XkI331oQcBme4jDmH0RqQDWh_pq49o38nbRsXwT9O5E,21599 +sympy/physics/optics/__init__.py,sha256=0UmqIt2-u8WwNkAqsnOVt9VlkB9K0CRIJYiQaltJ73w,1647 +sympy/physics/optics/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/optics/__pycache__/gaussopt.cpython-311.pyc,, +sympy/physics/optics/__pycache__/medium.cpython-311.pyc,, +sympy/physics/optics/__pycache__/polarization.cpython-311.pyc,, +sympy/physics/optics/__pycache__/utils.cpython-311.pyc,, +sympy/physics/optics/__pycache__/waves.cpython-311.pyc,, +sympy/physics/optics/gaussopt.py,sha256=mVQ-JX7xmAp9XbNOYIlwsPAxkUukTw_QjbjIxuKWZW8,20898 +sympy/physics/optics/medium.py,sha256=cys0tWGi1VCPWMTZuKadcN_bToz_bqKsDHSEVzuV3CE,7124 +sympy/physics/optics/polarization.py,sha256=mIrZiOVXetGtKkLxl8Llaf2Z9coWenf6JKrClh4W8yU,21434 +sympy/physics/optics/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/physics/optics/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/optics/tests/__pycache__/test_gaussopt.cpython-311.pyc,, +sympy/physics/optics/tests/__pycache__/test_medium.cpython-311.pyc,, +sympy/physics/optics/tests/__pycache__/test_polarization.cpython-311.pyc,, +sympy/physics/optics/tests/__pycache__/test_utils.cpython-311.pyc,, +sympy/physics/optics/tests/__pycache__/test_waves.cpython-311.pyc,, +sympy/physics/optics/tests/test_gaussopt.py,sha256=QMXJw_6mFCC3918b-pc_4b_zgO8Hsk7_SBvMupbEi5I,4222 +sympy/physics/optics/tests/test_medium.py,sha256=RxG7N3lzmCO_8hIoKyPnDKffmk8QFzA9yamu1_mr_dE,2194 +sympy/physics/optics/tests/test_polarization.py,sha256=81MzyA29HZckg_Ss-88-5o0g9augDqCr_LwcJIiXuA0,2605 +sympy/physics/optics/tests/test_utils.py,sha256=SjicjAptcZGwuX-ib_Lq7PlGONotvo2XJ4p3JA9iNVI,8553 +sympy/physics/optics/tests/test_waves.py,sha256=PeFfrl7MBkWBHdc796sDDYDuhGepat3DQk7PmyTXVnw,3397 +sympy/physics/optics/utils.py,sha256=BqfuvtrjO3PEcDQ1DecNyt2Th9Yps6xued1tEY4ysvk,22172 +sympy/physics/optics/waves.py,sha256=Iw-9gGksvWhPmQ_VepmI90ekKyzHdPlq6U41wdM4ikI,10042 +sympy/physics/paulialgebra.py,sha256=1r_qDBbVyl836qIXlVDdoF89Z9wedGvWIkHAbwQaK-4,6002 +sympy/physics/pring.py,sha256=SCMGGIcEhVoD7dwhY7_NWL1iKwo7OfgKdmm2Ok_9Xl0,2240 +sympy/physics/qho_1d.py,sha256=ZXemUsa_b0rLtPVTUkgAkZQ1Ecu2eIZxaiNSSXW0PDk,2005 +sympy/physics/quantum/__init__.py,sha256=vzWc2d2aXbMdZGJRx4tyS6FDCtjbtl7SqCJE7nSXN9A,1952 +sympy/physics/quantum/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/anticommutator.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/boson.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/cartesian.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/cg.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/circuitplot.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/circuitutils.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/commutator.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/constants.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/dagger.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/density.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/fermion.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/gate.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/grover.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/hilbert.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/identitysearch.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/innerproduct.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/kind.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/matrixcache.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/matrixutils.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/operator.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/operatorordering.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/operatorset.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/pauli.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/piab.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/qapply.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/qasm.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/qexpr.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/qft.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/qubit.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/represent.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/sho1d.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/shor.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/spin.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/state.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/tensorproduct.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/trace.cpython-311.pyc,, +sympy/physics/quantum/__pycache__/transforms.cpython-311.pyc,, +sympy/physics/quantum/anticommutator.py,sha256=kXr7DWZZy0Sqcnxx-QEeA58mUCRXTL_VHDzQBjNV0DI,5072 +sympy/physics/quantum/boson.py,sha256=Nn4TN917TQv4tdFNZddlT1eEGg7O1uybnU9K5NI6AyM,5834 +sympy/physics/quantum/cartesian.py,sha256=9R9VDYLV1Xe-GkA9TQbj8PVlBLaD0fF6KXfHJ1ze5as,9092 +sympy/physics/quantum/cg.py,sha256=WK7HkAIRFejQQLjRsCC7rH0L--0fmXAoeL1JdTHb3GA,23319 +sympy/physics/quantum/circuitplot.py,sha256=SacQMhPyDhizKmGRNEs1vtXph8lR6bMn5bVJI4rJiXg,11799 +sympy/physics/quantum/circuitutils.py,sha256=mrQNUDbwM3LV1NZ1EqVpXyOY2mOXCBVZW7cQTiCxUaM,13882 +sympy/physics/quantum/commutator.py,sha256=MKk8OqwIi8LCzxkD8URmKCFM9OthAAxS8xFNgP_i09U,8139 +sympy/physics/quantum/constants.py,sha256=20VRATCkSprSnGFR5ejvMEYlWwEcv1B-dE3RPqPTQ9k,1420 +sympy/physics/quantum/dagger.py,sha256=P58FL8fLFfMhtu79jGsG69oEMjLjutRDrXsLwBat-9U,2484 +sympy/physics/quantum/density.py,sha256=ie_NJO-YNJ3PydAMp3jlmhC5H8bN3ldhn2SiCKY5N0s,9546 +sympy/physics/quantum/fermion.py,sha256=1ipn3FItUJ_ruLnflpp9MN_6t5w8CgHAJRJCOsukGGI,4983 +sympy/physics/quantum/gate.py,sha256=Iv7-qhSCe_An9qaJcYRDgwr8ClNraP47E75UlS5fCoQ,42588 +sympy/physics/quantum/grover.py,sha256=17KC5MJR3cCavmzqRqi9dB5OFTOpsYjfrTZuv03HiuE,10452 +sympy/physics/quantum/hilbert.py,sha256=qrja92vF7BUeSyHOLKVX8-XKcPGT7QaQMWrqWXjRNus,19632 +sympy/physics/quantum/identitysearch.py,sha256=Zh_ji5J0YeAy2AezsQcHV9W2icWoaa3ZwTbfjCCQmJo,27607 +sympy/physics/quantum/innerproduct.py,sha256=NKlJufh068C3MeKBORJG8k7zdH_vGJeo7jlH67PqU7E,4303 +sympy/physics/quantum/kind.py,sha256=odJyriPZKyievddvlY-kks8iBCSi1yZ7JEsaLANHoe4,2831 +sympy/physics/quantum/matrixcache.py,sha256=S6fPkkYmfX8ELBOc9EST-8XnQ1gtpSOBfd2KwLGKdYo,3587 +sympy/physics/quantum/matrixutils.py,sha256=53VGeUNYNpU1eA1sNcYFaX1_45I9SVabCfpTyHUDFog,8214 +sympy/physics/quantum/operator.py,sha256=Q-uiUjAZgMOXHKoBFe2oW_qJsJSjC8fM246yzxMmZ0A,19657 +sympy/physics/quantum/operatorordering.py,sha256=byAyZCNKTCeFWIFThmNx0NgdI4u32O4ydodYSa6Wrr8,10296 +sympy/physics/quantum/operatorset.py,sha256=h8nkScpQcUzCO3zemqKpgQfJDWiBbfj33IJzcl4J2_4,9563 +sympy/physics/quantum/pauli.py,sha256=lzxWFHXqxKWRiYK99QCo9zuVG9eVXiB8vFya7TvrVxQ,17250 +sympy/physics/quantum/piab.py,sha256=Zjb2cRGniVDV6e35gjP4uEpI4w0C7YGQIEXReaq_z-E,1912 +sympy/physics/quantum/qapply.py,sha256=5vqWe__WRvJuD6sWC2NTDe1qEcToBGTGaye9lzirgf4,9494 +sympy/physics/quantum/qasm.py,sha256=UWpcUIBgkK55SmEBZlpmz-1KGHZvW7dNeSVG8tHr44A,6288 +sympy/physics/quantum/qexpr.py,sha256=N87sU5AqFbGEXgHJof-B7ua46rk4SeysK16abU1GfYI,13940 +sympy/physics/quantum/qft.py,sha256=ua4qBQAm-gi923lRRxOgAebTsTCoR93pz8ZHPXBdcus,6425 +sympy/physics/quantum/qubit.py,sha256=mE0xebz8zqeKEc4xww9-Iv7tZzxHjm9Klsr2WgUQcVM,25997 +sympy/physics/quantum/represent.py,sha256=dBY9RLD21V0WuIBbigagjaLSUCiloDzitv8Wl6fU9dE,18729 +sympy/physics/quantum/sho1d.py,sha256=ZroR_FjxmjOmDcd0Fm04vWKTGCpvLaEu4NiuplKm708,20867 +sympy/physics/quantum/shor.py,sha256=DVwPxLAPSr8t3F3aXJIPe4o5XSuQiE6a6eA6OYmdZFw,5504 +sympy/physics/quantum/spin.py,sha256=l6168fTEZEC4FeaMS8iLqo3b4YMvUlPTZvq9N8iSV08,72986 +sympy/physics/quantum/state.py,sha256=618YaDGAtpUeuWCVYs6lDuO3mD87_f8KIUGUXaN9L4s,29971 +sympy/physics/quantum/tensorproduct.py,sha256=PvpDSuLku1IdIV3T2dK3fVv6AdMNVcc5eR8n34AB3OQ,12599 +sympy/physics/quantum/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/physics/quantum/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_anticommutator.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_boson.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_cartesian.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_cg.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_circuitplot.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_circuitutils.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_commutator.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_constants.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_dagger.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_density.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_fermion.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_gate.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_grover.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_hilbert.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_identitysearch.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_innerproduct.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_kind.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_matrixutils.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_operator.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_operatorordering.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_operatorset.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_pauli.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_piab.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_printing.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_qapply.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_qasm.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_qexpr.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_qft.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_qubit.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_represent.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_sho1d.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_shor.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_spin.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_state.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_tensorproduct.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_trace.cpython-311.pyc,, +sympy/physics/quantum/tests/__pycache__/test_transforms.cpython-311.pyc,, +sympy/physics/quantum/tests/test_anticommutator.py,sha256=ckWHKwQFiAMWcDaYSa_26vi_GIsvs32_0O62I5lGsr8,1304 +sympy/physics/quantum/tests/test_boson.py,sha256=BZjdrZ-F1QhyhDqfK4Zc1VEFBJi1PeiPjMpfBcHekfo,1676 +sympy/physics/quantum/tests/test_cartesian.py,sha256=4szaB4w555oDUkhaRtBF8mOfIxiODNCqOlDHEVcUFOo,4417 +sympy/physics/quantum/tests/test_cg.py,sha256=BF-2ybLhoAYOb0wfWFlgnobMzH20zTYJvW5Z7v46SYI,9159 +sympy/physics/quantum/tests/test_circuitplot.py,sha256=c3v9wUzLHUH-eBVGj6_broVhHkioNwpaaApTDAJEflU,2096 +sympy/physics/quantum/tests/test_circuitutils.py,sha256=GrJAWRQVH_l8EIHrj1ve2jtxske72IriQ3lo94fqrVQ,13187 +sympy/physics/quantum/tests/test_commutator.py,sha256=keBstGDpNITFRr06uVFrka_Lje56g6oFoJQEpZXmnYw,2727 +sympy/physics/quantum/tests/test_constants.py,sha256=KBmYPIF49Sq34lbzbFCZRYWSyIdhnR3AK3q-VbU6grU,338 +sympy/physics/quantum/tests/test_dagger.py,sha256=pi2lhL5EaE8FuIuwM9gOce2NpDCP4S1JptqI-mx8FGM,2632 +sympy/physics/quantum/tests/test_density.py,sha256=EyxiEgyc0nDSweJwI0JUwta7gZ81TVHCl7YDEosTrvI,9718 +sympy/physics/quantum/tests/test_fermion.py,sha256=RK-J3nV1UO_9R5UyrBIp_qfWX-5iZ152aoyEllKWSIc,1636 +sympy/physics/quantum/tests/test_gate.py,sha256=7oBX1HoWnrYtHjABRoqv_wQDB9B829E99fdcJzaqawM,12496 +sympy/physics/quantum/tests/test_grover.py,sha256=uze62AG6H4x2MYJJA-EY3NtkqwvrDIQ2kONuvIRQiZ4,3640 +sympy/physics/quantum/tests/test_hilbert.py,sha256=IGP6rc2-b3we9dRDbpRniFAhQwp_TYtMfFzxusAprx0,2643 +sympy/physics/quantum/tests/test_identitysearch.py,sha256=3YGrXCsFLhLtN5MRyT5ZF8ELrSdkvDKTv6xKM4i2ims,17745 +sympy/physics/quantum/tests/test_innerproduct.py,sha256=37tT8p6MhHjAYeoay1Zyv7gCs-DeZQi4VdwUH2IffDE,1483 +sympy/physics/quantum/tests/test_kind.py,sha256=MCc0jobaf-fZmi99ir2VpZM1Lah5w9ZMxqygVQZfleU,2515 +sympy/physics/quantum/tests/test_matrixutils.py,sha256=wXc2jYLYaR07MgbVBpUU0cEsy2YQhQygbHV6M55u_ss,4115 +sympy/physics/quantum/tests/test_operator.py,sha256=kmNGQzjtdsko6PlUNcTT0kjOYX73LOMzHGuncS2SjSw,8417 +sympy/physics/quantum/tests/test_operatorordering.py,sha256=SFvJfrBxreMgMB3PEpXGcTvO_113Pi1O-Jco-A9_aVI,2003 +sympy/physics/quantum/tests/test_operatorset.py,sha256=DNfBeYBa_58kSG7PM5Ilo6xnzek8lSiAGX01uMFRYqI,2628 +sympy/physics/quantum/tests/test_pauli.py,sha256=Bhsx_gj5cpYv4BhVJRQohxlKk_rcp4jHtSRlTP-m_xs,4940 +sympy/physics/quantum/tests/test_piab.py,sha256=8ndnzyIsjF4AOu_9k6Yqap_1XUDTbiGnv7onJdrZBWA,1086 +sympy/physics/quantum/tests/test_printing.py,sha256=Qg34sHUAgKeh7DhHNp3KQX0_bhfdkMPCKJUvL08ufz8,30332 +sympy/physics/quantum/tests/test_qapply.py,sha256=ol5IJuuPand4r-P2cBv_Wycv2wnkMsW3dlJxDAp_PSs,6086 +sympy/physics/quantum/tests/test_qasm.py,sha256=ZvMjiheWBceSmIM9LHOL5fiFUl6HsUo8puqdzywrhkc,2976 +sympy/physics/quantum/tests/test_qexpr.py,sha256=0a80r2d31yxuJn11CcemrMn855o2ge5CH8j73ZCwuKI,1830 +sympy/physics/quantum/tests/test_qft.py,sha256=v-sGTaW9S-gcGTDAUPvjwd1kINF6rlI_u5Sf-Gso0r8,1931 +sympy/physics/quantum/tests/test_qubit.py,sha256=rj5RNjLWjZ8514fQwEsVp0wuUlI1SKAIomL0wZ-9PqA,9271 +sympy/physics/quantum/tests/test_represent.py,sha256=lEwzpL0fGxDGkojZ4_WoBAtCcA7aq2-S-i0Z0QrnTXg,5177 +sympy/physics/quantum/tests/test_sho1d.py,sha256=1QCoRXwhcK-P1oUrREgAroPHJPzPgWEBSrHomjS3meA,6893 +sympy/physics/quantum/tests/test_shor.py,sha256=3a3GCg6V5_mlJ2bltoXinGMGvlSxpq7GluapD_3SZaQ,666 +sympy/physics/quantum/tests/test_spin.py,sha256=p6F0cfQkPYwXlzE29mTL-dpsEYZmFU0EfAwm2MUA9xE,345698 +sympy/physics/quantum/tests/test_state.py,sha256=pjNwjEQN1zMXzC6NbL60ZLzpqBfZD5RscjuyiFuhTaE,6748 +sympy/physics/quantum/tests/test_tensorproduct.py,sha256=CL8xJyUl7plFU7TYsHHkLDYt9PtS4SM_H-SG4lKM5aE,5314 +sympy/physics/quantum/tests/test_trace.py,sha256=dbpTXcJArWRR_Hh5JTuy2GJIfgjVo6zS20o5mdVEGH4,3057 +sympy/physics/quantum/tests/test_transforms.py,sha256=w1fLTJO07CWPOQp5jFVU83MatsaoNBkxOI_YbM6I1w8,2346 +sympy/physics/quantum/trace.py,sha256=2ZqN9IEsz3LKHTLV8ZDwTK0sM5PfwL0p2sYet0N7Gis,6397 +sympy/physics/quantum/transforms.py,sha256=XLnWmyoWW8oXJ8I7dUIF7cMxFt9xr736HfNstD_tnK0,10929 +sympy/physics/secondquant.py,sha256=1XDsbnUpFC7zmJ4VC9DXEsK_smSqNMaDfsi6slIkws0,90974 +sympy/physics/sho.py,sha256=K8P9FAdZr6UfQKYZO9TlhDUqUd3YsMekXCsKy2HhaY0,2480 +sympy/physics/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/physics/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/tests/__pycache__/test_clebsch_gordan.cpython-311.pyc,, +sympy/physics/tests/__pycache__/test_hydrogen.cpython-311.pyc,, +sympy/physics/tests/__pycache__/test_paulialgebra.cpython-311.pyc,, +sympy/physics/tests/__pycache__/test_physics_matrices.cpython-311.pyc,, +sympy/physics/tests/__pycache__/test_pring.cpython-311.pyc,, +sympy/physics/tests/__pycache__/test_qho_1d.cpython-311.pyc,, +sympy/physics/tests/__pycache__/test_secondquant.cpython-311.pyc,, +sympy/physics/tests/__pycache__/test_sho.cpython-311.pyc,, +sympy/physics/tests/test_clebsch_gordan.py,sha256=gHMZnYkigoVWeDV7bwD4YARplb_L1AOMppzcLxSmWnI,9921 +sympy/physics/tests/test_hydrogen.py,sha256=kohRIR6JojE_GWYnlzLsMMgdhoKd8whazs0mq7cCTQc,4987 +sympy/physics/tests/test_paulialgebra.py,sha256=tyshEMsLNPR4iYzoAbPGZRZ-e_8t7GDP_xyjRyhepeQ,1477 +sympy/physics/tests/test_physics_matrices.py,sha256=Dha8iQRhzxLcl7TKSA6QP0pnEcBoqtj_Ob6tx01SMwI,2948 +sympy/physics/tests/test_pring.py,sha256=XScQQO9RhRrlqSII_ZyyOUpE-zs-7wphSFCZq2OuFnE,1261 +sympy/physics/tests/test_qho_1d.py,sha256=LD9WU-Y5lW7bVM7MyCkSGW9MU2FZhVjMB5Zk848_q1M,1775 +sympy/physics/tests/test_secondquant.py,sha256=vI9EuCMd1PMYNbZH2bHW9xHPYS5DS92QOmAk1kC12cQ,49401 +sympy/physics/tests/test_sho.py,sha256=aIs1f3eo6hb4ErRU8xrr_h_yhTmRx-fQgv9n27SfsLM,693 +sympy/physics/units/__init__.py,sha256=DVvWy9qNRm742NFGcBpybFY20ZK3BU7DWNbLMTXYiFo,12386 +sympy/physics/units/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/units/__pycache__/dimensions.cpython-311.pyc,, +sympy/physics/units/__pycache__/prefixes.cpython-311.pyc,, +sympy/physics/units/__pycache__/quantities.cpython-311.pyc,, +sympy/physics/units/__pycache__/unitsystem.cpython-311.pyc,, +sympy/physics/units/__pycache__/util.cpython-311.pyc,, +sympy/physics/units/definitions/__init__.py,sha256=F3RyZc1AjM2Ch5b27Tt-VYdZ1HAIWvhgtQQQTfMiN6w,7470 +sympy/physics/units/definitions/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/units/definitions/__pycache__/dimension_definitions.cpython-311.pyc,, +sympy/physics/units/definitions/__pycache__/unit_definitions.cpython-311.pyc,, +sympy/physics/units/definitions/dimension_definitions.py,sha256=QmPLCy7GvLx1z_YEZ5KFl8QUXg4rcuu3PuNI5wLpPdY,1633 +sympy/physics/units/definitions/unit_definitions.py,sha256=05wpHmAtyQvuJBeuzWm3cDQ6UYviNtsi4kVc0hv8VHw,14680 +sympy/physics/units/dimensions.py,sha256=GMuoSF1vqKDs_e3JJ7Y0lpFrneXMZLZk_qIC5d6Q5UU,20899 +sympy/physics/units/prefixes.py,sha256=_q2f8gA-kckBG7TutTFQazTf15PCZqNnaTR1gKXRfsk,6260 +sympy/physics/units/quantities.py,sha256=r5E231CULmsSEM7Rh7zfcTPuR85_X0CwRCVU_nDsek0,4671 +sympy/physics/units/systems/__init__.py,sha256=jJuvdc15c83yl11IuvhyjijwOZ9m1JGgZOgKwKv2e2o,244 +sympy/physics/units/systems/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/units/systems/__pycache__/cgs.cpython-311.pyc,, +sympy/physics/units/systems/__pycache__/length_weight_time.cpython-311.pyc,, +sympy/physics/units/systems/__pycache__/mks.cpython-311.pyc,, +sympy/physics/units/systems/__pycache__/mksa.cpython-311.pyc,, +sympy/physics/units/systems/__pycache__/natural.cpython-311.pyc,, +sympy/physics/units/systems/__pycache__/si.cpython-311.pyc,, +sympy/physics/units/systems/cgs.py,sha256=gXbX8uuZo7lcYIENA-CpAnyS9WVQy-vRisxlQm-198A,3702 +sympy/physics/units/systems/length_weight_time.py,sha256=DXIDSWdhjfxGLA0ldOziWhwQjzTAs7-VQTNCHzDvCgY,7004 +sympy/physics/units/systems/mks.py,sha256=Z3eX9yWK9BdvEosCROK2qRKtKFYOjtQ50Jk6vFT7AQY,1546 +sympy/physics/units/systems/mksa.py,sha256=U8cSI-maIuLJRvpKLBuZA8V19LDRYVc2I40Rao-wvjk,2002 +sympy/physics/units/systems/natural.py,sha256=43Odvmtxdpbz8UcW_xoRE9ArJVVdF7dgdAN2ByDAXx4,909 +sympy/physics/units/systems/si.py,sha256=YBPUuovW3-JBDZYuStXXRaC8cfzE3En3K5MjNy5pLJk,14478 +sympy/physics/units/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/physics/units/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/units/tests/__pycache__/test_dimensions.cpython-311.pyc,, +sympy/physics/units/tests/__pycache__/test_dimensionsystem.cpython-311.pyc,, +sympy/physics/units/tests/__pycache__/test_prefixes.cpython-311.pyc,, +sympy/physics/units/tests/__pycache__/test_quantities.cpython-311.pyc,, +sympy/physics/units/tests/__pycache__/test_unit_system_cgs_gauss.cpython-311.pyc,, +sympy/physics/units/tests/__pycache__/test_unitsystem.cpython-311.pyc,, +sympy/physics/units/tests/__pycache__/test_util.cpython-311.pyc,, +sympy/physics/units/tests/test_dimensions.py,sha256=lzkgGfEXMHxB8Izv7nRTN2uOEPh65LXPYaG8Kr5H05o,6122 +sympy/physics/units/tests/test_dimensionsystem.py,sha256=s2_2RAJwOaPOTvyIiAO9SYap374ytZqWbatWkLCnbSU,2717 +sympy/physics/units/tests/test_prefixes.py,sha256=Y3vlIReWGu8bwwZTrNGZSVoWYjhzgUZC33CDeyIvw48,2238 +sympy/physics/units/tests/test_quantities.py,sha256=XPuM6ul7XrUHvQE7F8rvpoCaxT9N6TM2gX99qUM4gTA,19758 +sympy/physics/units/tests/test_unit_system_cgs_gauss.py,sha256=JepTWt8yGdtv5dQ2AKUKb9fxpuYqLWOp0oOmzov9vfY,3173 +sympy/physics/units/tests/test_unitsystem.py,sha256=1Xh78_8hbv-yP4ICWI_dUrOnk3cimlvP_VhO-EXOa7Q,3254 +sympy/physics/units/tests/test_util.py,sha256=76PaLp_Cd9BAiry6VcWUD4Hrr68D6lTOkScWcLyhmL0,9355 +sympy/physics/units/unitsystem.py,sha256=mLuXftt3003OLk3knUGdYb1XfGbQa6vp-pHcenL11-o,7593 +sympy/physics/units/util.py,sha256=GU43MjXBRl5fm0CrU1fhFsZi9Q7wPG0NSJVJb1Oc_Y8,9885 +sympy/physics/vector/__init__.py,sha256=jZmrNB6ZfY7NOP8nx8GWcfI2Ixb2mv7lXuGHn63kyOw,985 +sympy/physics/vector/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/vector/__pycache__/dyadic.cpython-311.pyc,, +sympy/physics/vector/__pycache__/fieldfunctions.cpython-311.pyc,, +sympy/physics/vector/__pycache__/frame.cpython-311.pyc,, +sympy/physics/vector/__pycache__/functions.cpython-311.pyc,, +sympy/physics/vector/__pycache__/point.cpython-311.pyc,, +sympy/physics/vector/__pycache__/printing.cpython-311.pyc,, +sympy/physics/vector/__pycache__/vector.cpython-311.pyc,, +sympy/physics/vector/dyadic.py,sha256=CuAkSBgnatfnZQ2gBazZQC1DBVXbksiFWWqxu2w3C2U,18042 +sympy/physics/vector/fieldfunctions.py,sha256=H3S7oeLHaInZns9ko04BPOcSSOK2M5hBK71jB813ExY,8610 +sympy/physics/vector/frame.py,sha256=J1sY-qaAamcZFKP26x54Q9TWITXBgjmE3v5PiEaFri4,57260 +sympy/physics/vector/functions.py,sha256=b4dHZOxbvS77SMcSlCV2GTaOzWj0TQiUtDaO_dhj3Uw,24810 +sympy/physics/vector/point.py,sha256=W1H6Jd-ZAIHY8yKO3YWj-vdEp7qI2yoglzk5HuT-y2E,20569 +sympy/physics/vector/printing.py,sha256=a1N-wziCnt4gHtY9luqe-CDW9aAtpZ0FcvWwQ0hMEEo,11790 +sympy/physics/vector/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/physics/vector/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/physics/vector/tests/__pycache__/test_dyadic.cpython-311.pyc,, +sympy/physics/vector/tests/__pycache__/test_fieldfunctions.cpython-311.pyc,, +sympy/physics/vector/tests/__pycache__/test_frame.cpython-311.pyc,, +sympy/physics/vector/tests/__pycache__/test_functions.cpython-311.pyc,, +sympy/physics/vector/tests/__pycache__/test_output.cpython-311.pyc,, +sympy/physics/vector/tests/__pycache__/test_point.cpython-311.pyc,, +sympy/physics/vector/tests/__pycache__/test_printing.cpython-311.pyc,, +sympy/physics/vector/tests/__pycache__/test_vector.cpython-311.pyc,, +sympy/physics/vector/tests/test_dyadic.py,sha256=vCSLxo0pynrHrCCo7S4925lgRhoSm_Mr5YhIh_1bYcw,4265 +sympy/physics/vector/tests/test_fieldfunctions.py,sha256=FUjh18QzB6dXSau9iHutb36o28faSa7T9sB0icpja-M,5825 +sympy/physics/vector/tests/test_frame.py,sha256=S5xn5Q9dMj_ACIS9ff7ACTBS7sFFovi9DIov-crUsko,29683 +sympy/physics/vector/tests/test_functions.py,sha256=qHGT0RR-vkAp1oF30TFNgaeuOGvXdZnK2aKZuRgrZHg,21127 +sympy/physics/vector/tests/test_output.py,sha256=hgqlE-_zN_EPE_gIZ_v2uXB1y2auo39hReWpvFUm2QQ,2612 +sympy/physics/vector/tests/test_point.py,sha256=5CTzT3a-igd33auAra4uusm0PYUc_whPtV7KAoZ4g5w,12373 +sympy/physics/vector/tests/test_printing.py,sha256=qVBjz4f3TtDrduUYLNDrvlrzBVMBDqLo27JWsFHdX18,10967 +sympy/physics/vector/tests/test_vector.py,sha256=bqU1ltS6UGbSo74KXMtvP1mOpqKQ6XSV19Wjw2QoNFc,10259 +sympy/physics/vector/vector.py,sha256=R7P63QPZee0lfHHVDU7Uq91ZJs5ewjVK5HVj9HHNeQo,24874 +sympy/physics/wigner.py,sha256=L5QyzD4yH4SyTk7-WCCd07XK0_Pd9nz7sHwo2zOWV7I,39588 +sympy/plotting/__init__.py,sha256=hAdOjai8-laj79rLJ2HZbiW1okXlz0p1ck-CoeNU6m8,526 +sympy/plotting/__pycache__/__init__.cpython-311.pyc,, +sympy/plotting/__pycache__/experimental_lambdify.cpython-311.pyc,, +sympy/plotting/__pycache__/plot.cpython-311.pyc,, +sympy/plotting/__pycache__/plot_implicit.cpython-311.pyc,, +sympy/plotting/__pycache__/plotgrid.cpython-311.pyc,, +sympy/plotting/__pycache__/series.cpython-311.pyc,, +sympy/plotting/__pycache__/textplot.cpython-311.pyc,, +sympy/plotting/__pycache__/utils.cpython-311.pyc,, +sympy/plotting/backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/plotting/backends/__pycache__/__init__.cpython-311.pyc,, +sympy/plotting/backends/__pycache__/base_backend.cpython-311.pyc,, +sympy/plotting/backends/base_backend.py,sha256=aof5cs8mWG6fl87DadR5sZBjY8ssMelOwKFWi27-FO0,14911 +sympy/plotting/backends/matplotlibbackend/__init__.py,sha256=Mzsz43gkanid12G8qnOcwJNbbvqf6eGppn-IZyJCMto,162 +sympy/plotting/backends/matplotlibbackend/__pycache__/__init__.cpython-311.pyc,, +sympy/plotting/backends/matplotlibbackend/__pycache__/matplotlib.cpython-311.pyc,, +sympy/plotting/backends/matplotlibbackend/matplotlib.py,sha256=9efu7ST_D3M-_fepZCPTcy8TdYEIX0PCkyocDGVfbPE,12548 +sympy/plotting/backends/textbackend/__init__.py,sha256=nnV_C7JJ_kwGcQe2C-tpnK635W8vTuIf5Grvvmur0rQ,92 +sympy/plotting/backends/textbackend/__pycache__/__init__.cpython-311.pyc,, +sympy/plotting/backends/textbackend/__pycache__/text.cpython-311.pyc,, +sympy/plotting/backends/textbackend/text.py,sha256=1ukArjwwQWUED9SB-1dmkB6YL5EcJ2rUosUf_NcBpXs,803 +sympy/plotting/experimental_lambdify.py,sha256=xcBhlvZ2h20aI1MpUN6qAEpO075Dv132AWbQJ7l3Wzg,22828 +sympy/plotting/intervalmath/__init__.py,sha256=fQV7sLZ9NHpZO5XGl2ZfqX56x-mdq-sYhtWEKLngHlU,479 +sympy/plotting/intervalmath/__pycache__/__init__.cpython-311.pyc,, +sympy/plotting/intervalmath/__pycache__/interval_arithmetic.cpython-311.pyc,, +sympy/plotting/intervalmath/__pycache__/interval_membership.cpython-311.pyc,, +sympy/plotting/intervalmath/__pycache__/lib_interval.cpython-311.pyc,, +sympy/plotting/intervalmath/interval_arithmetic.py,sha256=jv5YolNs6pOawIhuSsTBVwgkgmdOFwPrGN_1KtjfcIs,15570 +sympy/plotting/intervalmath/interval_membership.py,sha256=1VpO1T7UjvPxcMySC5GhZl8-VM_DxIirSWC3ZGmxIAY,2385 +sympy/plotting/intervalmath/lib_interval.py,sha256=WY1qRtyub4MDJaZizw6cXQI5NMEIXBO9UEWPEI80aW8,14809 +sympy/plotting/intervalmath/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/plotting/intervalmath/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/plotting/intervalmath/tests/__pycache__/test_interval_functions.cpython-311.pyc,, +sympy/plotting/intervalmath/tests/__pycache__/test_interval_membership.cpython-311.pyc,, +sympy/plotting/intervalmath/tests/__pycache__/test_intervalmath.cpython-311.pyc,, +sympy/plotting/intervalmath/tests/test_interval_functions.py,sha256=gdIo5z54tIbG8hDaGd3I8rBDP67oetMZWWdM-uvt1ec,9862 +sympy/plotting/intervalmath/tests/test_interval_membership.py,sha256=D1KjcrLxAwOmDEUqA-8TCqkFWGtmeerR9KwmzS7tyjk,4216 +sympy/plotting/intervalmath/tests/test_intervalmath.py,sha256=ndBMczrs6xYMN5RGnyCL9yq7pNUxrXHTSU1mdUsp5tU,9034 +sympy/plotting/plot.py,sha256=umvG47eIqOdEUo5qki3jDkS6LYKnTWFTZ5qzxZZBgAw,40882 +sympy/plotting/plot_implicit.py,sha256=1xIIr4eV1gIU4SkQ2k54PBZGUgYMCgsPttY-9uZ2eZM,7330 +sympy/plotting/plotgrid.py,sha256=QZmbxY-fcgPuseYLnKVDFsoH6m57Cosvy4W7l4jIqWw,6115 +sympy/plotting/pygletplot/__init__.py,sha256=DM7GURQbdSfcddHz23MxOShatBFc26tP_sd3G8pGCQE,3732 +sympy/plotting/pygletplot/__pycache__/__init__.cpython-311.pyc,, +sympy/plotting/pygletplot/__pycache__/color_scheme.cpython-311.pyc,, +sympy/plotting/pygletplot/__pycache__/managed_window.cpython-311.pyc,, +sympy/plotting/pygletplot/__pycache__/plot.cpython-311.pyc,, +sympy/plotting/pygletplot/__pycache__/plot_axes.cpython-311.pyc,, +sympy/plotting/pygletplot/__pycache__/plot_camera.cpython-311.pyc,, +sympy/plotting/pygletplot/__pycache__/plot_controller.cpython-311.pyc,, +sympy/plotting/pygletplot/__pycache__/plot_curve.cpython-311.pyc,, +sympy/plotting/pygletplot/__pycache__/plot_interval.cpython-311.pyc,, +sympy/plotting/pygletplot/__pycache__/plot_mode.cpython-311.pyc,, +sympy/plotting/pygletplot/__pycache__/plot_mode_base.cpython-311.pyc,, +sympy/plotting/pygletplot/__pycache__/plot_modes.cpython-311.pyc,, +sympy/plotting/pygletplot/__pycache__/plot_object.cpython-311.pyc,, +sympy/plotting/pygletplot/__pycache__/plot_rotation.cpython-311.pyc,, +sympy/plotting/pygletplot/__pycache__/plot_surface.cpython-311.pyc,, +sympy/plotting/pygletplot/__pycache__/plot_window.cpython-311.pyc,, +sympy/plotting/pygletplot/__pycache__/util.cpython-311.pyc,, +sympy/plotting/pygletplot/color_scheme.py,sha256=NgPUamkldygfrIPj0LvC_1AzhscVtg18FSudElvFYB8,12522 +sympy/plotting/pygletplot/managed_window.py,sha256=N7AKtM7ELfIJLie6zvI-J6-OQRBnMZu6AL1USz7hFEk,3072 +sympy/plotting/pygletplot/plot.py,sha256=s-5AJB0KelHs9WGoFIVIdYrOoMXfdpnM5-G2cF8xzDQ,13352 +sympy/plotting/pygletplot/plot_axes.py,sha256=Q9YN8W0Hd1PeflHLvOvSZ-hxeLU4Kq3nUFLYDC0x0E8,8655 +sympy/plotting/pygletplot/plot_camera.py,sha256=myYtKbv1ov_ltgR34hf8BR76t3AwTSu4QFUY5YY-e1E,3928 +sympy/plotting/pygletplot/plot_controller.py,sha256=MroJJSPCbBDT8gGs_GdqpV_KHsllMNJpxx0MU3vKJV8,6941 +sympy/plotting/pygletplot/plot_curve.py,sha256=YwKA2lYC7IwCOQJaOVnww8AAG4P36cArgbC1iLV9OFI,2838 +sympy/plotting/pygletplot/plot_interval.py,sha256=doqr2wxnrED4MJDlkxQ07GFvaagX36HUb77ly_vIuKQ,5431 +sympy/plotting/pygletplot/plot_mode.py,sha256=Djq-ewVms_JoSriDpolDhhtttBJQdJO8BD4E0nyOWcQ,14156 +sympy/plotting/pygletplot/plot_mode_base.py,sha256=3z3WjeN7TTslHJevhr3X_7HRHPgUleYSngu6285lR6k,11502 +sympy/plotting/pygletplot/plot_modes.py,sha256=gKzJShz6OXa6EHKar8SuHWrELVznxg_s2d5IBQkkeYE,5352 +sympy/plotting/pygletplot/plot_object.py,sha256=qGtzcKup4It1CqZ2jxA7FnorCua4S9I-B_7I3SHBjcQ,330 +sympy/plotting/pygletplot/plot_rotation.py,sha256=KGMCTyaSh5_UQD9FKGCIpoI3-CbbOhXxTcjAc1o5-PU,1445 +sympy/plotting/pygletplot/plot_surface.py,sha256=C0q9tzDmxzC1IpWiNKY4llzcopx6dhotGOLpK1N9m3s,3803 +sympy/plotting/pygletplot/plot_window.py,sha256=5boC2Fkmk46-gWGqWzdTkPmTMNHHOpA0CnB9q946Hwc,4643 +sympy/plotting/pygletplot/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/plotting/pygletplot/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/plotting/pygletplot/tests/__pycache__/test_plotting.cpython-311.pyc,, +sympy/plotting/pygletplot/tests/test_plotting.py,sha256=NisjR-yuBRJfQvjcb20skTR3yid2U3MhKHW6sy8RE10,2720 +sympy/plotting/pygletplot/util.py,sha256=mzQQgDDbp04B03KyJrossLp8Yq72RJzjp-3ArfjbMH8,4621 +sympy/plotting/series.py,sha256=hPkMicAi29RjoJkFpMCt1CRn0xdBVYU1wLniigHgCaM,96568 +sympy/plotting/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/plotting/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/plotting/tests/__pycache__/test_experimental_lambdify.cpython-311.pyc,, +sympy/plotting/tests/__pycache__/test_plot.cpython-311.pyc,, +sympy/plotting/tests/__pycache__/test_plot_implicit.cpython-311.pyc,, +sympy/plotting/tests/__pycache__/test_series.cpython-311.pyc,, +sympy/plotting/tests/__pycache__/test_textplot.cpython-311.pyc,, +sympy/plotting/tests/__pycache__/test_utils.cpython-311.pyc,, +sympy/plotting/tests/test_experimental_lambdify.py,sha256=EYshdXA5tAGWolaDX-nHAolp7xIJN4Oqb1Uc1C1IhJI,3127 +sympy/plotting/tests/test_plot.py,sha256=mXiNdG-dHkK1iJ_moNlehZQU3i8mCtUKhVJdTxSxjdA,48217 +sympy/plotting/tests/test_plot_implicit.py,sha256=wurrO7ntsHb4tWYPVs5VogRrtcylevu0EceCSEwiWQg,5799 +sympy/plotting/tests/test_region_and.png,sha256=EV0Lm4HtQPk_6eIWtPY4TPcQk-O7tkpdZIuLmFjGRaA,6864 +sympy/plotting/tests/test_region_not.png,sha256=3O_9_nPW149FMULEcT5RqI2-k2H3nHELbfJADt2cO8k,7939 +sympy/plotting/tests/test_region_or.png,sha256=5Bug09vyog-Cu3mky7pbtFjew5bMvbpe0ZXWsgDKfy4,8809 +sympy/plotting/tests/test_region_xor.png,sha256=kucVWBA9A98OpcR4did5aLXUyoq4z0O4C3PM6dliBSw,10002 +sympy/plotting/tests/test_series.py,sha256=Nwa6__YMoWCiY0mYSlvJPPnlvtfOYLACHR1RbkopMc0,65638 +sympy/plotting/tests/test_textplot.py,sha256=VurTGeMjUfBLpLdoMqzJK9gbcShNb7f1OrAcRNyrtag,12761 +sympy/plotting/tests/test_utils.py,sha256=FkcYZAFT8TnHRIbkknx9NvA3-LgR0ua7WFyzQEPsVIE,3602 +sympy/plotting/textplot.py,sha256=Xc-bWvzVOq8QUn_BxDlJv9bryCpgzsgV73XoQQwVj0Q,5075 +sympy/plotting/utils.py,sha256=Cimno9MQGjcbe4rQ6TN86r4rM2t62CgARqPtgF0mhR8,12259 +sympy/polys/__init__.py,sha256=qXmDNr7noCUAtIs0rJJRHHFbHjy3uHIvXZDajcSePc4,5577 +sympy/polys/__pycache__/__init__.cpython-311.pyc,, +sympy/polys/__pycache__/appellseqs.cpython-311.pyc,, +sympy/polys/__pycache__/compatibility.cpython-311.pyc,, +sympy/polys/__pycache__/constructor.cpython-311.pyc,, +sympy/polys/__pycache__/densearith.cpython-311.pyc,, +sympy/polys/__pycache__/densebasic.cpython-311.pyc,, +sympy/polys/__pycache__/densetools.cpython-311.pyc,, +sympy/polys/__pycache__/dispersion.cpython-311.pyc,, +sympy/polys/__pycache__/distributedmodules.cpython-311.pyc,, +sympy/polys/__pycache__/domainmatrix.cpython-311.pyc,, +sympy/polys/__pycache__/euclidtools.cpython-311.pyc,, +sympy/polys/__pycache__/factortools.cpython-311.pyc,, +sympy/polys/__pycache__/fglmtools.cpython-311.pyc,, +sympy/polys/__pycache__/fields.cpython-311.pyc,, +sympy/polys/__pycache__/galoistools.cpython-311.pyc,, +sympy/polys/__pycache__/groebnertools.cpython-311.pyc,, +sympy/polys/__pycache__/heuristicgcd.cpython-311.pyc,, +sympy/polys/__pycache__/modulargcd.cpython-311.pyc,, +sympy/polys/__pycache__/monomials.cpython-311.pyc,, +sympy/polys/__pycache__/multivariate_resultants.cpython-311.pyc,, +sympy/polys/__pycache__/orderings.cpython-311.pyc,, +sympy/polys/__pycache__/orthopolys.cpython-311.pyc,, +sympy/polys/__pycache__/partfrac.cpython-311.pyc,, +sympy/polys/__pycache__/polyclasses.cpython-311.pyc,, +sympy/polys/__pycache__/polyconfig.cpython-311.pyc,, +sympy/polys/__pycache__/polyerrors.cpython-311.pyc,, +sympy/polys/__pycache__/polyfuncs.cpython-311.pyc,, +sympy/polys/__pycache__/polymatrix.cpython-311.pyc,, +sympy/polys/__pycache__/polyoptions.cpython-311.pyc,, +sympy/polys/__pycache__/polyquinticconst.cpython-311.pyc,, +sympy/polys/__pycache__/polyroots.cpython-311.pyc,, +sympy/polys/__pycache__/polytools.cpython-311.pyc,, +sympy/polys/__pycache__/polyutils.cpython-311.pyc,, +sympy/polys/__pycache__/puiseux.cpython-311.pyc,, +sympy/polys/__pycache__/rationaltools.cpython-311.pyc,, +sympy/polys/__pycache__/ring_series.cpython-311.pyc,, +sympy/polys/__pycache__/rings.cpython-311.pyc,, +sympy/polys/__pycache__/rootisolation.cpython-311.pyc,, +sympy/polys/__pycache__/rootoftools.cpython-311.pyc,, +sympy/polys/__pycache__/solvers.cpython-311.pyc,, +sympy/polys/__pycache__/specialpolys.cpython-311.pyc,, +sympy/polys/__pycache__/sqfreetools.cpython-311.pyc,, +sympy/polys/__pycache__/subresultants_qq_zz.cpython-311.pyc,, +sympy/polys/agca/__init__.py,sha256=fahpWoG_0LgoqOXBnDBJS16Jj1fE1_VKG7edM3qZ2HE,130 +sympy/polys/agca/__pycache__/__init__.cpython-311.pyc,, +sympy/polys/agca/__pycache__/extensions.cpython-311.pyc,, +sympy/polys/agca/__pycache__/homomorphisms.cpython-311.pyc,, +sympy/polys/agca/__pycache__/ideals.cpython-311.pyc,, +sympy/polys/agca/__pycache__/modules.cpython-311.pyc,, +sympy/polys/agca/extensions.py,sha256=YmtFs9C0s-4DNMXFtdX1hYVNlby18mAJzhJ5Aqickrw,9388 +sympy/polys/agca/homomorphisms.py,sha256=gaMNV96pKUuYHZ8Bd7QOs27J1IbbJgkEjyWcTLe8GFI,21937 +sympy/polys/agca/ideals.py,sha256=S6rBl3H-hdeI44ZbELwjjt1rKlrhK11AKb8Aas-OtCE,11073 +sympy/polys/agca/modules.py,sha256=PeA138FHsCfUn8vkdrjPa92xmzq7Qqk0PrdYwVoNqf0,47240 +sympy/polys/agca/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/polys/agca/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/polys/agca/tests/__pycache__/test_extensions.cpython-311.pyc,, +sympy/polys/agca/tests/__pycache__/test_homomorphisms.cpython-311.pyc,, +sympy/polys/agca/tests/__pycache__/test_ideals.cpython-311.pyc,, +sympy/polys/agca/tests/__pycache__/test_modules.cpython-311.pyc,, +sympy/polys/agca/tests/test_extensions.py,sha256=i3IHQNXQByFMCvjjyd_hwwJSCiUj0z1rRwS9WFK2AFc,6455 +sympy/polys/agca/tests/test_homomorphisms.py,sha256=m0hFmcTzvZ8sZbbnWeENwzKyufpE9zWwZR-WCI4kdpU,4224 +sympy/polys/agca/tests/test_ideals.py,sha256=w76qXO-_HN6LQbV7l3h7gJZsM-DZ2io2X-kPWiHYRNw,3788 +sympy/polys/agca/tests/test_modules.py,sha256=HdfmcxdEVucEbtfmzVq8i_1wGojT5b5DE5VIfbTMx3k,13552 +sympy/polys/appellseqs.py,sha256=hWeDKsKnJuAuPN_5IU6m1okurAq9xMt3LQgMehcvBKQ,8305 +sympy/polys/benchmarks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/polys/benchmarks/__pycache__/__init__.cpython-311.pyc,, +sympy/polys/benchmarks/__pycache__/bench_galoispolys.cpython-311.pyc,, +sympy/polys/benchmarks/__pycache__/bench_groebnertools.cpython-311.pyc,, +sympy/polys/benchmarks/__pycache__/bench_solvers.cpython-311.pyc,, +sympy/polys/benchmarks/bench_galoispolys.py,sha256=8RtN9ZQga2oxscVPPkMGB29Dz8UbskMS2szYtqZ69u0,1502 +sympy/polys/benchmarks/bench_groebnertools.py,sha256=YqGDCzewRszCye_GnneDXMRNB38ORSpVu_Jn0ELIySo,803 +sympy/polys/benchmarks/bench_solvers.py,sha256=gLrZguh6pE0E4_vM2GeOS5bHnrcSUQXqD0Qz9tItfmo,446778 +sympy/polys/compatibility.py,sha256=ti_gJSz8zB1sf-oIyfXAw4xkDGkQaZs_34RSC-6HldY,58373 +sympy/polys/constructor.py,sha256=rv8hQgE8-P9QXUtOQmlEiyA4XD-fb1Qzd8bvH3UHQls,11373 +sympy/polys/densearith.py,sha256=6wB6DJWKNB0RA7Tdg7jMgRIERjZdtAp32OQhIQ6wmV4,34114 +sympy/polys/densebasic.py,sha256=Kag6cqlxHhHUMYUXpr9GHLWh4E1Mnw2Esoh6zx9r14E,35954 +sympy/polys/densetools.py,sha256=A0Z1E16eVx577zw84m8KZgT5o6dbDXXonW8TqfISJZY,29746 +sympy/polys/dispersion.py,sha256=s6GIYnGA6U9jhGP7YXQQS8G3byG4-kPbr55BR6p-iz4,5740 +sympy/polys/distributedmodules.py,sha256=t8pLIgDQs_dMecGXwybVYoLavofEy2DXhFS8N5gj5SU,21827 +sympy/polys/domainmatrix.py,sha256=FmNqklNFQR1WrQYtP2r7jypw2IQadNKGP14EaUaxUqI,310 +sympy/polys/domains/__init__.py,sha256=T6qPNkU1EJ6D5BnvyJSXJv4zeJ5MUT5RLsovMkkXS9E,1872 +sympy/polys/domains/__pycache__/__init__.cpython-311.pyc,, +sympy/polys/domains/__pycache__/algebraicfield.cpython-311.pyc,, +sympy/polys/domains/__pycache__/characteristiczero.cpython-311.pyc,, +sympy/polys/domains/__pycache__/complexfield.cpython-311.pyc,, +sympy/polys/domains/__pycache__/compositedomain.cpython-311.pyc,, +sympy/polys/domains/__pycache__/domain.cpython-311.pyc,, +sympy/polys/domains/__pycache__/domainelement.cpython-311.pyc,, +sympy/polys/domains/__pycache__/expressiondomain.cpython-311.pyc,, +sympy/polys/domains/__pycache__/expressionrawdomain.cpython-311.pyc,, +sympy/polys/domains/__pycache__/field.cpython-311.pyc,, +sympy/polys/domains/__pycache__/finitefield.cpython-311.pyc,, +sympy/polys/domains/__pycache__/fractionfield.cpython-311.pyc,, +sympy/polys/domains/__pycache__/gaussiandomains.cpython-311.pyc,, +sympy/polys/domains/__pycache__/gmpyfinitefield.cpython-311.pyc,, +sympy/polys/domains/__pycache__/gmpyintegerring.cpython-311.pyc,, +sympy/polys/domains/__pycache__/gmpyrationalfield.cpython-311.pyc,, +sympy/polys/domains/__pycache__/groundtypes.cpython-311.pyc,, +sympy/polys/domains/__pycache__/integerring.cpython-311.pyc,, +sympy/polys/domains/__pycache__/modularinteger.cpython-311.pyc,, +sympy/polys/domains/__pycache__/mpelements.cpython-311.pyc,, +sympy/polys/domains/__pycache__/old_fractionfield.cpython-311.pyc,, +sympy/polys/domains/__pycache__/old_polynomialring.cpython-311.pyc,, +sympy/polys/domains/__pycache__/polynomialring.cpython-311.pyc,, +sympy/polys/domains/__pycache__/pythonfinitefield.cpython-311.pyc,, +sympy/polys/domains/__pycache__/pythonintegerring.cpython-311.pyc,, +sympy/polys/domains/__pycache__/pythonrational.cpython-311.pyc,, +sympy/polys/domains/__pycache__/pythonrationalfield.cpython-311.pyc,, +sympy/polys/domains/__pycache__/quotientring.cpython-311.pyc,, +sympy/polys/domains/__pycache__/rationalfield.cpython-311.pyc,, +sympy/polys/domains/__pycache__/realfield.cpython-311.pyc,, +sympy/polys/domains/__pycache__/ring.cpython-311.pyc,, +sympy/polys/domains/__pycache__/simpledomain.cpython-311.pyc,, +sympy/polys/domains/algebraicfield.py,sha256=JXK_o6BdQbYT_0gOLALNOLsmqgfV46DRFJb7cANJgAM,23051 +sympy/polys/domains/characteristiczero.py,sha256=vHYRUXPrfJzDF8wrd1KSFqG8WzwfITP_eweA-SHPVYA,382 +sympy/polys/domains/complexfield.py,sha256=VbhX6yigmU2OLhof8qEpPp1XZhzzkfXfcuZevx1LZsk,6159 +sympy/polys/domains/compositedomain.py,sha256=DAo2ISA9XdOnYzFu8azuPIQAT9fyVwSM8Pe425vmvww,1642 +sympy/polys/domains/domain.py,sha256=aj9HOp98G2m25wRzSDj4lWzoRLGyXDsuevzScdHjV6o,40850 +sympy/polys/domains/domainelement.py,sha256=IrG-Mzv_VlCAmE-hmJVH_d77TrsfyaGGfJVmU8FFvlY,860 +sympy/polys/domains/expressiondomain.py,sha256=AB7Mnd6kOLaS_yG4lMXZTpVPUuAHGcrd6RAjjFSVxNc,7593 +sympy/polys/domains/expressionrawdomain.py,sha256=cXarD2jXi97FGNiqNiDqQlX0g764EW2M1PEbrveImnY,1448 +sympy/polys/domains/field.py,sha256=fR0l6wZIv02UMJ5rioEP910jGUViVx38EdwtJuJX5vg,2959 +sympy/polys/domains/finitefield.py,sha256=1DHkfrCz08KXLEz7in4IK71B8udHPZZAWv50-jV5_vQ,10654 +sympy/polys/domains/fractionfield.py,sha256=BoqUCbVggwaoqGCVy_r42MIVHuAV-P3gONFVZIlWojU,5848 +sympy/polys/domains/gaussiandomains.py,sha256=YrcHg4QSPgIRW-Ft8J84q1zwmU-DVJuAscWXnVCQ9CI,19598 +sympy/polys/domains/gmpyfinitefield.py,sha256=WgSLnALNOVVKH931WpVT28ZWDilsrTDG8DyMee2xR94,437 +sympy/polys/domains/gmpyintegerring.py,sha256=qJ7w8K40cfzhztZtOuWlIL2DXa642xJcKIIxoAOlcSs,3037 +sympy/polys/domains/gmpyrationalfield.py,sha256=dZjrfcWaUA-BHUtutzLOWPlOSNLYzBqSFeukER6L_bA,3178 +sympy/polys/domains/groundtypes.py,sha256=hAla27w5ekoJR_8c-1Yo8vrEgIIggPc615tfe1udc9A,2102 +sympy/polys/domains/integerring.py,sha256=4oy49xTi8hV6qh8CWUAoBcgn2aJqgqwyh7bZBsjGfwI,7442 +sympy/polys/domains/modularinteger.py,sha256=k6gskb0eypXdrKJRxR3l_75mVjmICGnaOy7FdRMwG8E,6042 +sympy/polys/domains/mpelements.py,sha256=PREDWq_WA5er2zzftt5Lg0opnWzyFBsIkLKcyGXUVz0,5042 +sympy/polys/domains/old_fractionfield.py,sha256=TUVxyL2fS4QF3kgyW5EGfkl91ir3S1klu08UfZr3GuI,6226 +sympy/polys/domains/old_polynomialring.py,sha256=KQcH58oHnHzOpDdWojZiLlHDqrAiUd4OAaBIZigqpyc,15982 +sympy/polys/domains/polynomialring.py,sha256=OLPcwkyrbkt5pNZ342o3Ziv9TCrlpU3h18s9R-cxqXI,6223 +sympy/polys/domains/pythonfinitefield.py,sha256=lWp266ErnuUPuo7k8ju3z2S5IresFInpJAl4Ihsq0pI,453 +sympy/polys/domains/pythonintegerring.py,sha256=EH5s6nwaxmeaScLER_FfqPdhyuJnbjtBslHmgyOyEPs,2962 +sympy/polys/domains/pythonrational.py,sha256=M3VUGODh3MLElePjYtjt9b02ReMThw-XXpuQTkohgNs,548 +sympy/polys/domains/pythonrationalfield.py,sha256=x8BPkGKj0WPuwJzN2py5l9aAjHaY4djv65c4tzUTr3Y,2295 +sympy/polys/domains/quotientring.py,sha256=2NOUkkbFj4514qkDUszl6hl1EQuPikn3QEoQ1sDobGI,5911 +sympy/polys/domains/rationalfield.py,sha256=D-pA-iHDHbOi6fivqSrJnfmH2JTrheKJQ9ZFXXN5ftM,5982 +sympy/polys/domains/realfield.py,sha256=OdLfE_9OVxvQlnQiT4faJxuB6dZwnXdIt-o-5wGYroA,6456 +sympy/polys/domains/ring.py,sha256=p66U2X58acSHLHxOTU6aJZ0Umdcu1qiGIUDtV8iJCD0,3236 +sympy/polys/domains/simpledomain.py,sha256=_K-Zz8Opf505r3eHSrbPAlnGiGSjY_O4Cwa4OTeOSoY,369 +sympy/polys/domains/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/polys/domains/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/polys/domains/tests/__pycache__/test_domains.cpython-311.pyc,, +sympy/polys/domains/tests/__pycache__/test_polynomialring.cpython-311.pyc,, +sympy/polys/domains/tests/__pycache__/test_quotientring.cpython-311.pyc,, +sympy/polys/domains/tests/test_domains.py,sha256=XJwTGPM-8W4Rn76-cec99rpUIDDjP-7HTFs7AKGNrEw,51028 +sympy/polys/domains/tests/test_polynomialring.py,sha256=8nNFKuQeicwiBk6pnqPLNk0cnI61iK8Dpl-TZifvORc,2895 +sympy/polys/domains/tests/test_quotientring.py,sha256=BYoq1CqI76RDSm0xQdp1v7Dv1n5sdcmes-b_y_AfW-0,1459 +sympy/polys/euclidtools.py,sha256=hAsNupzfR-07fTwFvTF2yDabyKMm_PLgFC8KX9c2CAo,41808 +sympy/polys/factortools.py,sha256=CupycipegP1IGCYPY9eY2T8f_6h4gmr6qId1A4UDJvk,43034 +sympy/polys/fglmtools.py,sha256=Z3VS_IKsZu1o3g22KPTIH1feNL8cTpieaNWvLuILmK4,4298 +sympy/polys/fields.py,sha256=RdSDkOls6n87yvHG1KXFnGng0JS3SAcUAlz4tTb1RZE,21250 +sympy/polys/galoistools.py,sha256=Z3ed_A0Nohj9RzuM3kom7-ay4MnzIHppwgV2QpONuTo,57238 +sympy/polys/groebnertools.py,sha256=NhK-XcFR9e4chDDJJ-diXb7XYuw9zcixFA_riomThPM,23342 +sympy/polys/heuristicgcd.py,sha256=rD3intgKCtAAMH3sqlgqbJL1XSq9QjfeG_MYzwCOek0,3732 +sympy/polys/matrices/__init__.py,sha256=ZaPJMi8l22d3F3rudS4NqzSt0xwxbs3uwnQwlhhR91o,397 +sympy/polys/matrices/__pycache__/__init__.cpython-311.pyc,, +sympy/polys/matrices/__pycache__/_dfm.cpython-311.pyc,, +sympy/polys/matrices/__pycache__/_typing.cpython-311.pyc,, +sympy/polys/matrices/__pycache__/ddm.cpython-311.pyc,, +sympy/polys/matrices/__pycache__/dense.cpython-311.pyc,, +sympy/polys/matrices/__pycache__/dfm.cpython-311.pyc,, +sympy/polys/matrices/__pycache__/domainmatrix.cpython-311.pyc,, +sympy/polys/matrices/__pycache__/domainscalar.cpython-311.pyc,, +sympy/polys/matrices/__pycache__/eigen.cpython-311.pyc,, +sympy/polys/matrices/__pycache__/exceptions.cpython-311.pyc,, +sympy/polys/matrices/__pycache__/linsolve.cpython-311.pyc,, +sympy/polys/matrices/__pycache__/lll.cpython-311.pyc,, +sympy/polys/matrices/__pycache__/normalforms.cpython-311.pyc,, +sympy/polys/matrices/__pycache__/rref.cpython-311.pyc,, +sympy/polys/matrices/__pycache__/sdm.cpython-311.pyc,, +sympy/polys/matrices/_dfm.py,sha256=_3p5Hb9e86uBG_r__Ee6BAZxj4KiRhcf2uN3XnjHxwQ,32971 +sympy/polys/matrices/_typing.py,sha256=Egp2nMOaq-oJCW0bgNDxy1Bx6evTl8eMTpf4mzIw1s4,407 +sympy/polys/matrices/ddm.py,sha256=eMKUy8X1kQTEG1l7Fx_L4LG9Jqs9l0KEwlYQwlcl6Do,32362 +sympy/polys/matrices/dense.py,sha256=RYdogVHeWZoeO94Oal1mf1sJvj-JmyS_baRi8-d9Rms,23217 +sympy/polys/matrices/dfm.py,sha256=Fj_uF4FskrwcBDNuRSV--AozCP2cYkUz-BMitcMlkRE,1241 +sympy/polys/matrices/domainmatrix.py,sha256=MvjWRzQ4lKzX2rCl8mRtpiIXSe641_x6OMrTr8iscPA,115928 +sympy/polys/matrices/domainscalar.py,sha256=sPXC-it46yun2r_tJkun4MIuVQQyQMQTVsTedId8Rj4,3778 +sympy/polys/matrices/eigen.py,sha256=glArxs9_rTrE_ssz2fd2gKCTsguSyEHwoeQP82tmIcM,3015 +sympy/polys/matrices/exceptions.py,sha256=ay3Lv21X3QqszysBN71xdr9KGQuC5kDBl90a2Sjx6pM,1351 +sympy/polys/matrices/linsolve.py,sha256=p86jlJP9h3CxZX92xk2sh1WAvKBkB5_YD55k1oYpmGI,7534 +sympy/polys/matrices/lll.py,sha256=p5q_rb5QvAuHsGRZAzP2Jf_h2vdC_Be4Q62lkaYxW-c,3550 +sympy/polys/matrices/normalforms.py,sha256=6MJfVbiwlZuCk-0fYiyOMuqkPdVpyzTNdDP2s6jkRw0,17122 +sympy/polys/matrices/rref.py,sha256=5YK_phB382K9JFVIydlH2IYpziirEDUnKzyl5BfS2_o,15532 +sympy/polys/matrices/sdm.py,sha256=bb2fIO42KjyJuEuMBkiDFn_qLJVjD4gsRgWZ6GOmTHw,64293 +sympy/polys/matrices/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/polys/matrices/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/polys/matrices/tests/__pycache__/test_ddm.cpython-311.pyc,, +sympy/polys/matrices/tests/__pycache__/test_dense.cpython-311.pyc,, +sympy/polys/matrices/tests/__pycache__/test_domainmatrix.cpython-311.pyc,, +sympy/polys/matrices/tests/__pycache__/test_domainscalar.cpython-311.pyc,, +sympy/polys/matrices/tests/__pycache__/test_eigen.cpython-311.pyc,, +sympy/polys/matrices/tests/__pycache__/test_fflu.cpython-311.pyc,, +sympy/polys/matrices/tests/__pycache__/test_inverse.cpython-311.pyc,, +sympy/polys/matrices/tests/__pycache__/test_linsolve.cpython-311.pyc,, +sympy/polys/matrices/tests/__pycache__/test_lll.cpython-311.pyc,, +sympy/polys/matrices/tests/__pycache__/test_normalforms.cpython-311.pyc,, +sympy/polys/matrices/tests/__pycache__/test_nullspace.cpython-311.pyc,, +sympy/polys/matrices/tests/__pycache__/test_rref.cpython-311.pyc,, +sympy/polys/matrices/tests/__pycache__/test_sdm.cpython-311.pyc,, +sympy/polys/matrices/tests/__pycache__/test_xxm.cpython-311.pyc,, +sympy/polys/matrices/tests/test_ddm.py,sha256=BcnUntz3XR2Xyff02l4Mk2EsxwYlIBzM4lFSRo9oZT8,16699 +sympy/polys/matrices/tests/test_dense.py,sha256=ETC2h5yLKsPJ5GAgx_RUcWyR63iZx81pbtOZiWty1r0,9516 +sympy/polys/matrices/tests/test_domainmatrix.py,sha256=4QttFR5PYJg6Xn37VXdxmv1w9Xkm82fF-M-KRQIe0RU,50006 +sympy/polys/matrices/tests/test_domainscalar.py,sha256=HEFEKX5tP6SZ83_91nvLmFqgHxbVdpqOP4ZwzfFbHnc,3740 +sympy/polys/matrices/tests/test_eigen.py,sha256=T1lYZeW-0NwDxDOG6ZJLr-OICfxY2wa0fVHV2V6EXSk,3200 +sympy/polys/matrices/tests/test_fflu.py,sha256=7En_75QemdtHvR7lUyZrcHiV9Dv2rswBuuKVOPWi1pk,8542 +sympy/polys/matrices/tests/test_inverse.py,sha256=5DT5qvfS3MlrgNgeHnG8GLjgLnmhsxj94yR3cbtmEO8,5247 +sympy/polys/matrices/tests/test_linsolve.py,sha256=ur1BFzlIQyBrO_-aL71lXsLC67nfec9G5o-mzW_TFY4,3373 +sympy/polys/matrices/tests/test_lll.py,sha256=RGYTDGbLfFvAMTESv-S1kqSWzwtOIjmguqXO3yGCjH4,6624 +sympy/polys/matrices/tests/test_normalforms.py,sha256=x1-vkjXq3LGblUSJFz7z73IVmaxqZbVzWoGj9XS-yXw,5106 +sympy/polys/matrices/tests/test_nullspace.py,sha256=eAEDPlnVkKfFM9jn1gztOUQTos1Sm9qwH10C-t9uLUE,5418 +sympy/polys/matrices/tests/test_rref.py,sha256=mWTIfKAUP3vkGKhffCrPHuC_0DdF-iH41cchlSN8Pqc,25982 +sympy/polys/matrices/tests/test_sdm.py,sha256=fSE3bQlDU-atzTFTgp4AgAS3QL-7rvb_61stj3QGBnU,13415 +sympy/polys/matrices/tests/test_xxm.py,sha256=X9sP_VfPi4bUR7V1ZLq6ZHXPOBUA7tE5-gQULang_Ag,29778 +sympy/polys/modulargcd.py,sha256=gOwojISiV8IncaEHZJZMHOC1odN7xRKN5L1Ls4szROI,58729 +sympy/polys/monomials.py,sha256=6cDCTVNNPDeee5bt0RQnGuYnsO_fefioHA5mEDKHSuA,18458 +sympy/polys/multivariate_resultants.py,sha256=6bwdF-lcUqtKoVDrnOKhm_PyPfo8w0Yyy_GtESatQFw,15248 +sympy/polys/numberfields/__init__.py,sha256=ZfhC9MyfGfGUz_DT_rXasB-M_P2zUiZXOJUNh_Gtm8c,538 +sympy/polys/numberfields/__pycache__/__init__.cpython-311.pyc,, +sympy/polys/numberfields/__pycache__/basis.cpython-311.pyc,, +sympy/polys/numberfields/__pycache__/exceptions.cpython-311.pyc,, +sympy/polys/numberfields/__pycache__/galois_resolvents.cpython-311.pyc,, +sympy/polys/numberfields/__pycache__/galoisgroups.cpython-311.pyc,, +sympy/polys/numberfields/__pycache__/minpoly.cpython-311.pyc,, +sympy/polys/numberfields/__pycache__/modules.cpython-311.pyc,, +sympy/polys/numberfields/__pycache__/primes.cpython-311.pyc,, +sympy/polys/numberfields/__pycache__/resolvent_lookup.cpython-311.pyc,, +sympy/polys/numberfields/__pycache__/subfield.cpython-311.pyc,, +sympy/polys/numberfields/__pycache__/utilities.cpython-311.pyc,, +sympy/polys/numberfields/basis.py,sha256=IPA6cSwz-53ClQwo-wkmRzfx9pRX4iBhiggdLMVSgJ0,8261 +sympy/polys/numberfields/exceptions.py,sha256=kHb-aB4eBzG3SsIpYtL5wLExDSb8lIOpNq0tk3guFIA,1594 +sympy/polys/numberfields/galois_resolvents.py,sha256=OL3u-G6sCwvZuBuuYQO0QpL-wWxtjxFaBjVQhtiQ_Z8,25006 +sympy/polys/numberfields/galoisgroups.py,sha256=c4s6z_mEUkoWKezFrfjKiU2tZnEnxR4m1LYaagicVno,20671 +sympy/polys/numberfields/minpoly.py,sha256=0F7QVSOk8WqViD50jKtT-94C6kQyuXPBCyNZPTf0ScA,27791 +sympy/polys/numberfields/modules.py,sha256=4MJykT6gtT_LC033LWsHT_CM7nEydLISAdQ0yA_bhkQ,69243 +sympy/polys/numberfields/primes.py,sha256=UXOkuMdnamVvHPQZxwilCbhdwNNNS7zQ4bwSFc5xGgk,23984 +sympy/polys/numberfields/resolvent_lookup.py,sha256=qfLNKOz_WjtXwpVlfzy8EkD4gw12epx9npE9HsjyIdg,40411 +sympy/polys/numberfields/subfield.py,sha256=Huh9iq2CDQN8p0fs2bSe-yEoB_V1OVr8rH58rJT4crk,16668 +sympy/polys/numberfields/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/polys/numberfields/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/polys/numberfields/tests/__pycache__/test_basis.cpython-311.pyc,, +sympy/polys/numberfields/tests/__pycache__/test_galoisgroups.cpython-311.pyc,, +sympy/polys/numberfields/tests/__pycache__/test_minpoly.cpython-311.pyc,, +sympy/polys/numberfields/tests/__pycache__/test_modules.cpython-311.pyc,, +sympy/polys/numberfields/tests/__pycache__/test_numbers.cpython-311.pyc,, +sympy/polys/numberfields/tests/__pycache__/test_primes.cpython-311.pyc,, +sympy/polys/numberfields/tests/__pycache__/test_subfield.cpython-311.pyc,, +sympy/polys/numberfields/tests/__pycache__/test_utilities.cpython-311.pyc,, +sympy/polys/numberfields/tests/test_basis.py,sha256=96BJ7e4oPDKXyvlRrUkiQxmHyjRGpOkAC7R3ln-jgNE,4580 +sympy/polys/numberfields/tests/test_galoisgroups.py,sha256=3LFuMbV92VBFlqqEqjh37oQvmG8cgZ0pFxDCXUoYRL4,5036 +sympy/polys/numberfields/tests/test_minpoly.py,sha256=HjlmPWX90Cb-XK5G1H0-uirOiHdgTu7R8qpcG5ZI5iw,23115 +sympy/polys/numberfields/tests/test_modules.py,sha256=GU4166j_hMlB22uWxxIjV_ON8RsyvpaN7Ly3eK8_m8Y,22926 +sympy/polys/numberfields/tests/test_numbers.py,sha256=M0vZIBnjPBHV4vFUnPBILaqiR_cgSuU50kFB-v7l1gA,5988 +sympy/polys/numberfields/tests/test_primes.py,sha256=JhcAkaQMgjkOSziQ2jZApJ8b8oviil5cUy0hfFqNmZg,9779 +sympy/polys/numberfields/tests/test_subfield.py,sha256=EpBexToc17YwLB4-n6nE_HaleU8AcFGi5OpDReW7q0k,12776 +sympy/polys/numberfields/tests/test_utilities.py,sha256=rQGEJWctcfzjUtMwRuyCHerSqEsoP5C3z1bnddJA17o,3661 +sympy/polys/numberfields/utilities.py,sha256=IHKmfafE9tMGammfdtkyR4_IHb0mON-OBwitE0Ss1R0,13087 +sympy/polys/orderings.py,sha256=IFieyj4LkFa7NDiGTZD3VwUY7mSN3GEjThKk0z5WJ1s,8500 +sympy/polys/orthopolys.py,sha256=PIJg_Kml8MxmJs5QPCjGmRLjjlE_4QKhsWhU7-an7x0,10181 +sympy/polys/partfrac.py,sha256=ptYpXsp8dYyyeiPnLU-rdQ-uv3-GAvDnN79E81C68y8,14695 +sympy/polys/polyclasses.py,sha256=Zc9sjiwVxsM6wM8h9KCFP2Pjlfl4XA3MjC6NlguQzqM,96529 +sympy/polys/polyconfig.py,sha256=mgfFpp9SU159tA_PM2o04WZyzMoWfOtWZugRcHnP42c,1598 +sympy/polys/polyerrors.py,sha256=xByI-fqIHVYsYRm63NmHXlSSRCwSI9vZUoO-1Mf5Wlk,4744 +sympy/polys/polyfuncs.py,sha256=OEZpdYeHQADBJYqMw8JAyN4sw-jsJ6lzVH6m-CCoK8g,8547 +sympy/polys/polymatrix.py,sha256=iNa-EyIrzv7ZeXMaP_PjojgNd29AnIotZE3NeF2te44,9771 +sympy/polys/polyoptions.py,sha256=yG1k4ZPN2ufwpZ4lI4Sn8DSIHvd7EwDUHUaz64HxlUM,21806 +sympy/polys/polyquinticconst.py,sha256=mYLFWSBq3H3Y0I8cx76Z_xauLx1YeViC4xF6yWsSTPQ,96035 +sympy/polys/polyroots.py,sha256=bJToZlR6WZNl9fNxkIJxiPt3XoDQ_K6Pb3UOCBPfsKM,37025 +sympy/polys/polytools.py,sha256=BUwK8wJApazWeKQ3t27yg-HTMnwiv9ScPiavk2n02Rk,212876 +sympy/polys/polyutils.py,sha256=M8V-D4NkfMlsJe4LeAZzljrM4BKp10Qvuuutd8kmp3E,17248 +sympy/polys/puiseux.py,sha256=wc_5w5RyXHfAwC9Dde-6ecPm9y1z5DAmfthtSZ53CqA,26518 +sympy/polys/rationaltools.py,sha256=8vbkg3nuBxbd4ztR7lOj2jTF9taKUKTPah_fD38Ex6c,2838 +sympy/polys/ring_series.py,sha256=7DijmljyAaQrupWB0JmaNwGm3wNTstnEsBln-o1uX1M,59877 +sympy/polys/rings.py,sha256=KVdA3C04G2H3YN81rM-8ORsUHqmktIrz0S0tfwLwN4g,86080 +sympy/polys/rootisolation.py,sha256=QKCYgD4kMNBtSLYkUim8MXHa7louMtlO0riwKTiXRRI,64341 +sympy/polys/rootoftools.py,sha256=bMcVUcIwqaM643UU1WTCNmPmDFRepZEq9Xfdr9mm63Q,43092 +sympy/polys/solvers.py,sha256=FareKqDVVC6P2I4yjudi_3CS3kx0SbmAz3k7zHkETgE,13520 +sympy/polys/specialpolys.py,sha256=B2vijl75zgUKUTY1HCqjB9BTDFf3FM8ugwkKGTB83XA,11038 +sympy/polys/sqfreetools.py,sha256=oRFlOLhcXz5OKjc9SQtOuEhMvacvkHyTSEqhk6Snh1Y,19876 +sympy/polys/subresultants_qq_zz.py,sha256=VFKTAe3iGMCimLkc9GeuwG5o4SB0x2T5kRwxe4A5BDo,88241 +sympy/polys/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/polys/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_appellseqs.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_constructor.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_densearith.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_densebasic.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_densetools.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_dispersion.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_distributedmodules.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_euclidtools.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_factortools.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_fields.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_galoistools.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_groebnertools.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_heuristicgcd.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_hypothesis.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_injections.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_modulargcd.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_monomials.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_multivariate_resultants.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_orderings.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_orthopolys.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_partfrac.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_polyclasses.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_polyfuncs.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_polymatrix.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_polyoptions.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_polyroots.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_polytools.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_polyutils.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_puiseux.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_pythonrational.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_rationaltools.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_ring_series.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_rings.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_rootisolation.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_rootoftools.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_solvers.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_specialpolys.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_sqfreetools.cpython-311.pyc,, +sympy/polys/tests/__pycache__/test_subresultants_qq_zz.cpython-311.pyc,, +sympy/polys/tests/test_appellseqs.py,sha256=YTERuRr30QtfxYR0erXvJG8D-INe9RaMFAF0ZM-H4Ks,3820 +sympy/polys/tests/test_constructor.py,sha256=t6JpLbIdy2hcHF4_s_Xiav0kVc4OOsH5EFg6IXOiwmI,7220 +sympy/polys/tests/test_densearith.py,sha256=ptFGN7ovPH3dqT5khr6Wm8CR6a1WOR7Fi8dj8SuBvJs,40827 +sympy/polys/tests/test_densebasic.py,sha256=FcjAfZngYRQSWJ__BrBPITAUYjMNkS_pOtGyuJv_Bs0,21590 +sympy/polys/tests/test_densetools.py,sha256=o7HZfOw4Xn71RpCDhk98srNipA3zXfikAP_lV0gShlM,26241 +sympy/polys/tests/test_dispersion.py,sha256=S3diTgz7WKH88ulGNT3ijiQ8Y53Rpul6jATBJMGyQdw,3182 +sympy/polys/tests/test_distributedmodules.py,sha256=dXmjhozX5Yzb7DsrtbdFTqAxi9Z1UZNJvGxj-vHM7cM,7639 +sympy/polys/tests/test_euclidtools.py,sha256=vEyj48eIjm6-KRQtThNfI4ic_VDNB6l7jMouxJAF9HE,19482 +sympy/polys/tests/test_factortools.py,sha256=K5_R-01Muc_45XOtolk_tQswE2l6tGrlOJ9zA_aNVYE,24903 +sympy/polys/tests/test_fields.py,sha256=DUvcax7WGtydkLDXq0aQ9Ksc6PkdV67NPZCvqTYN2w8,9869 +sympy/polys/tests/test_galoistools.py,sha256=0oN3eSWvV99juZSXco6Ek9n6s6BFOmOE4UIEhyZnQQs,28532 +sympy/polys/tests/test_groebnertools.py,sha256=ZWHBcCCOVNwDxuJWg1WPo0krTHx1m1wTPi2cOYPsAT4,18584 +sympy/polys/tests/test_heuristicgcd.py,sha256=87Yc0on955VExFyOgJuxBZhsIMFz1Vq4vclIVkQ--cE,4297 +sympy/polys/tests/test_hypothesis.py,sha256=LdokFa3JrQ59_umY15x21QSScPjJoCl8RxHRfn1NUOc,1109 +sympy/polys/tests/test_injections.py,sha256=EONGggBUNWaVSwi817CzLBYJgkTehFq8-m-Qdqes984,1286 +sympy/polys/tests/test_modulargcd.py,sha256=2SjCEjCPkf1-u-Co5vuVg5sJmfBLSY3gkFLicLl4g6E,9043 +sympy/polys/tests/test_monomials.py,sha256=YcMBU1qe6g7EJojtV89dfJZmdXsXVTSly2J5cF6SD6o,11083 +sympy/polys/tests/test_multivariate_resultants.py,sha256=DJu8CcZ3xwx8njpjDeSOyhyxeqZYmhfb7dkSCU-ll7Y,9501 +sympy/polys/tests/test_orderings.py,sha256=bdsIsqJTFJCVyZNRMAGVDXVk79ldw9rmAGejS_lwKP0,4254 +sympy/polys/tests/test_orthopolys.py,sha256=Ob5Osxd8hwebEjK3dDMarZ2VDXzl3pdsEdz_-kMa80M,6560 +sympy/polys/tests/test_partfrac.py,sha256=VfWOx30fZu_avMbWqxtbLDcLi-9x6iuQmLfyatnFc3Y,7838 +sympy/polys/tests/test_polyclasses.py,sha256=EuoT3b9a9KRTQFe95MjGrQIqAhRgLG5qUo42FtkM-Js,14765 +sympy/polys/tests/test_polyfuncs.py,sha256=VbgCgCRE06dtSY9I9GSdPH9T52ETYYoxk4J3N1WBtd4,4520 +sympy/polys/tests/test_polymatrix.py,sha256=pl2VrN_d2XGOVHvvAnaNQzkdFTdQgjt9ePgo41soBRs,7353 +sympy/polys/tests/test_polyoptions.py,sha256=z9DUdt8K3lYkm4IyLH1Cv-TKe76HP-EyaRkZVsfWb6U,12416 +sympy/polys/tests/test_polyroots.py,sha256=m0rnjXMqX59XdFn9X4rRtZJjI39vBN-MAt9cpQszq-I,26803 +sympy/polys/tests/test_polytools.py,sha256=JfaZlcguRIh6ZVNpkIUc2sSRJX_Hq6aluW7HxjCDWO4,139212 +sympy/polys/tests/test_polyutils.py,sha256=Qs3QQl0WYmTnkYE2ovTxdLeu6DYnWO_OoUmLwNDZzSw,11547 +sympy/polys/tests/test_puiseux.py,sha256=4PnBdJuYDWku8l6Jg2Dsn7umg14ldYBdvHqtVaaELbE,7304 +sympy/polys/tests/test_pythonrational.py,sha256=vYMlOTuYvf-15P0nKTFm-uRrhUc-nCFEkqYFAPLxg08,4143 +sympy/polys/tests/test_rationaltools.py,sha256=wkvjzNP1IH-SdubNk5JJ7OWcY-zNF6z3t32kfp9Ncs0,2397 +sympy/polys/tests/test_ring_series.py,sha256=j88HGfiJ9ZL114UezdJtC5aWtox8RG0I2Qyq9gLnkCE,31697 +sympy/polys/tests/test_rings.py,sha256=q8A_CeerrzGqVf1Ol-O-Jc-R7q-2X119YSNMnds3Ccs,48735 +sympy/polys/tests/test_rootisolation.py,sha256=x-n-T-Con-8phelNa05BPszkC_UCW1C0yAOwz658I60,32724 +sympy/polys/tests/test_rootoftools.py,sha256=1cv7M6EQ3mK7NhcwKqbIvour1l_QYtW_zr6vyZ3rzs0,23414 +sympy/polys/tests/test_solvers.py,sha256=kKyEvmqt91HHuAV42DS26B8fPDFQ2Ghtb_Nf3-1eWyI,13644 +sympy/polys/tests/test_specialpolys.py,sha256=v1i0RMNlixxe2EbwqoE-UlV7a2KY-md4T7thhUhWSx0,5031 +sympy/polys/tests/test_sqfreetools.py,sha256=m8ipJE28YC9eOrsVVAqxduCsw4iOW-s_nw76y0E8wUs,4886 +sympy/polys/tests/test_subresultants_qq_zz.py,sha256=bSFOjbtyIClbJHLDvvzgmOgoZyC_5Vdpe7vtW9VGU_s,13219 +sympy/printing/__init__.py,sha256=Hh-z1idh92pk17XjdXkjYwb8cJ5coEr4ZJQGcpE3d9I,2215 +sympy/printing/__pycache__/__init__.cpython-311.pyc,, +sympy/printing/__pycache__/aesaracode.cpython-311.pyc,, +sympy/printing/__pycache__/c.cpython-311.pyc,, +sympy/printing/__pycache__/codeprinter.cpython-311.pyc,, +sympy/printing/__pycache__/conventions.cpython-311.pyc,, +sympy/printing/__pycache__/cxx.cpython-311.pyc,, +sympy/printing/__pycache__/defaults.cpython-311.pyc,, +sympy/printing/__pycache__/dot.cpython-311.pyc,, +sympy/printing/__pycache__/fortran.cpython-311.pyc,, +sympy/printing/__pycache__/glsl.cpython-311.pyc,, +sympy/printing/__pycache__/gtk.cpython-311.pyc,, +sympy/printing/__pycache__/jscode.cpython-311.pyc,, +sympy/printing/__pycache__/julia.cpython-311.pyc,, +sympy/printing/__pycache__/lambdarepr.cpython-311.pyc,, +sympy/printing/__pycache__/latex.cpython-311.pyc,, +sympy/printing/__pycache__/llvmjitcode.cpython-311.pyc,, +sympy/printing/__pycache__/maple.cpython-311.pyc,, +sympy/printing/__pycache__/mathematica.cpython-311.pyc,, +sympy/printing/__pycache__/mathml.cpython-311.pyc,, +sympy/printing/__pycache__/numpy.cpython-311.pyc,, +sympy/printing/__pycache__/octave.cpython-311.pyc,, +sympy/printing/__pycache__/precedence.cpython-311.pyc,, +sympy/printing/__pycache__/preview.cpython-311.pyc,, +sympy/printing/__pycache__/printer.cpython-311.pyc,, +sympy/printing/__pycache__/pycode.cpython-311.pyc,, +sympy/printing/__pycache__/python.cpython-311.pyc,, +sympy/printing/__pycache__/pytorch.cpython-311.pyc,, +sympy/printing/__pycache__/rcode.cpython-311.pyc,, +sympy/printing/__pycache__/repr.cpython-311.pyc,, +sympy/printing/__pycache__/rust.cpython-311.pyc,, +sympy/printing/__pycache__/smtlib.cpython-311.pyc,, +sympy/printing/__pycache__/str.cpython-311.pyc,, +sympy/printing/__pycache__/tableform.cpython-311.pyc,, +sympy/printing/__pycache__/tensorflow.cpython-311.pyc,, +sympy/printing/__pycache__/theanocode.cpython-311.pyc,, +sympy/printing/__pycache__/tree.cpython-311.pyc,, +sympy/printing/aesaracode.py,sha256=_iHvF9avR9MGiOnxLIwog2M5rUwhLRt-bdsbcJHeO_s,18921 +sympy/printing/c.py,sha256=qX8EkxVT5Z2Hncz8DJh8iar_jBuVcRDEiIt1d6jcerE,27011 +sympy/printing/codeprinter.py,sha256=QUAWM6H45YBkd7fX6jXXO3j-tfVaHhzxbXn7OnSm0Es,42371 +sympy/printing/conventions.py,sha256=hvN1VMZE5krMj8FL4AAuxAO0xCwhAugBFWrFQ0355AY,2586 +sympy/printing/cxx.py,sha256=vCQyzT-1eNLLDJy4NBwCp5X5OCoPOZ9icsx8YifaPsc,6123 +sympy/printing/defaults.py,sha256=YitLfIRfFH8ltNd18Y6YtBgq5H2te0wFKlHuIO4cvo8,135 +sympy/printing/dot.py,sha256=W0J798ZxBdlJercffBGnNDTp7J2tMdIYQkE_KIiyi3s,8274 +sympy/printing/fortran.py,sha256=zHVQMphFeeIGl-1x29Hw5Lekxi5NWou-V3Kn4O0oHQQ,28635 +sympy/printing/glsl.py,sha256=wsjPcpyzk92m4I2_re0cn3JwEjPmRlmlVWX9owhzM5w,20310 +sympy/printing/gtk.py,sha256=ptnwYxJr5ox3LG4TCDbRIgxsCikaVvEzWBaqIpITUXc,466 +sympy/printing/jscode.py,sha256=BORODo9Aio62FKh1HYlluG-IxtJC26_-IWrxAiEtmr8,11981 +sympy/printing/julia.py,sha256=V_Qbld0MOebIkT4VnmyP6VdIbgrPaqllgjkv7eSq8bk,23541 +sympy/printing/lambdarepr.py,sha256=BCx4eSdG8MQ8ZSUV1lWEd3CzbZ4IiMid-TTxPoV6FHU,8305 +sympy/printing/latex.py,sha256=jCqXe1XOWLNUcRNzsHOY3CMvbkPBKjf8tD4ii6PVKxU,123669 +sympy/printing/llvmjitcode.py,sha256=NjjHbgheH4j6SboFIDfrrIogkcrJRkW_kjlzM8DpDmQ,17127 +sympy/printing/maple.py,sha256=aG7t4RUEVojSFBVA14GDBzU702KtTcolTCelXozNiN0,10571 +sympy/printing/mathematica.py,sha256=to8LmNkhiP0ACh-Vun-hfaMnLFSEc0mF2OmZnQmihE8,12720 +sympy/printing/mathml.py,sha256=UT7g45oLTqvnrC1IUZeqywHTV6-LUClfuOF_U4AMlaI,76634 +sympy/printing/numpy.py,sha256=Kpq-ySkQSRKYCxtgQZEi_4lj3EmK7FgvbDJTzvB77Tc,21049 +sympy/printing/octave.py,sha256=1ofeBaIcoJfgzImezfPed9Z4dZ6Tk5gVbEQbr02TcLI,25469 +sympy/printing/precedence.py,sha256=hKIin20F4LJp2l5yhSQ-WAegTRHYQLhccABC1_ciI-U,5522 +sympy/printing/pretty/__init__.py,sha256=pJTe-DO4ctTlnjg1UvqyoeBY50B5znFjcGvivXRhM2U,344 +sympy/printing/pretty/__pycache__/__init__.cpython-311.pyc,, +sympy/printing/pretty/__pycache__/pretty.cpython-311.pyc,, +sympy/printing/pretty/__pycache__/pretty_symbology.cpython-311.pyc,, +sympy/printing/pretty/__pycache__/stringpict.cpython-311.pyc,, +sympy/printing/pretty/pretty.py,sha256=KmEXRb6XEtI5tiQYssueibKKrJVF1F1mwrF1uHGxmpM,105024 +sympy/printing/pretty/pretty_symbology.py,sha256=v4FyDz_03mbSGX7tGni8Zw2L2qKTRGvmCIL85ttXg0s,24112 +sympy/printing/pretty/stringpict.py,sha256=UbG55kC4ve8-Dby_v0cm8AXUdLcu4YDLXVnA8F_iV8Y,19158 +sympy/printing/pretty/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/printing/pretty/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/printing/pretty/tests/__pycache__/test_pretty.cpython-311.pyc,, +sympy/printing/pretty/tests/test_pretty.py,sha256=K2Xt--ptdxWLuUmtYJnuyh81ZrGIDRqNM_iOUADAdjA,187716 +sympy/printing/preview.py,sha256=3KacgHEch3KKJZeKc7kvD-L4pfftI8Gm62fx9-Vipak,14089 +sympy/printing/printer.py,sha256=z1phdIHrrcU2cyD-1GaBXREmhkcVl8bxeIWZrGAlczg,15868 +sympy/printing/pycode.py,sha256=LsDnREZEJVw0zsy_AjRSkYER-acomwQiDicrWBmnoW8,27048 +sympy/printing/python.py,sha256=oxGJhNQUXRKjHSEgqroGAyB-A4ZgJQqA_o7C8uXA1U8,3347 +sympy/printing/pytorch.py,sha256=HXEAoLbgbuE-SYj0hvDHeJ0zgzOVDBkK5qWErLCJhNo,10620 +sympy/printing/rcode.py,sha256=uBazXZwSRd95UUQjNoD6qLlpun7Xwlm8cfLG4qYhy3Y,14101 +sympy/printing/repr.py,sha256=GLO6WFOunZ8k0Om0g1VbGW2Y5uOP0IVEo_pIGO8HoNw,11545 +sympy/printing/rust.py,sha256=5Dh_zFxXZRVzbfHGowRQd9moUjzJZ7G2PXjxqsmBBw0,20831 +sympy/printing/smtlib.py,sha256=u2rVmprPITJdLq79FTYm7Fl-osteadr1JyXeQXlZW68,21752 +sympy/printing/str.py,sha256=dTK_CdT-Mm_zRoOL-Mwb4ocBA__h2wBFAAg6VvAiebQ,33239 +sympy/printing/tableform.py,sha256=pz4hXDLZ1IlqU8eOo0XBX1xaALYMqe4qHPUnEVq0mAE,11797 +sympy/printing/tensorflow.py,sha256=QmWoLUKpefBYJZ-J0Ei0NHHrU7Livp8Td4trWTDFzy8,8161 +sympy/printing/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/printing/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_aesaracode.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_c.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_codeprinter.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_conventions.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_cupy.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_cxx.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_dot.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_fortran.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_glsl.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_gtk.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_jax.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_jscode.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_julia.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_lambdarepr.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_latex.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_llvmjit.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_maple.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_mathematica.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_mathml.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_numpy.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_octave.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_precedence.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_preview.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_pycode.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_python.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_rcode.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_repr.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_rust.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_smtlib.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_str.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_tableform.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_tensorflow.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_theanocode.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_torch.cpython-311.pyc,, +sympy/printing/tests/__pycache__/test_tree.cpython-311.pyc,, +sympy/printing/tests/test_aesaracode.py,sha256=dX-SKJHfx9m91NSlt7pQ5Gul0x7fig3XZsHCt2kvtRc,21340 +sympy/printing/tests/test_c.py,sha256=etIEgJwlGWxtH6VRYg3Zkjyw_ktYRYj_-mvEMQehmjI,31227 +sympy/printing/tests/test_codeprinter.py,sha256=rVimgQTdJchy7PsjdZupQmXfqi6FBme2BPwnz7Z5WeQ,2142 +sympy/printing/tests/test_conventions.py,sha256=yqPpU3F0WcbxImPBBAHd3YEZpkFGfcq_TLK4WN_gtP4,5257 +sympy/printing/tests/test_cupy.py,sha256=E2T4a82XITbWAssmRZD4zCQdMCrpaZFCAjGXthUp8cY,1882 +sympy/printing/tests/test_cxx.py,sha256=0XQPQdtDCgEKVfpuelEB-79f7d2fe9QPgEKCCYVAI_w,3183 +sympy/printing/tests/test_dot.py,sha256=TSAtgGIgK_JbY-RMbQgUvnAI87SJqeJOqzcLjAobhKM,4648 +sympy/printing/tests/test_fortran.py,sha256=d5uy1zp0IgaBPupzXpNuk169YZYygnodPWhNRcvUtAQ,35456 +sympy/printing/tests/test_glsl.py,sha256=cfog9fp_EOFm_piJwqUcSvAIJ78bRwkFjecwr3ocCak,28421 +sympy/printing/tests/test_gtk.py,sha256=94gp1xRlPrFiALQGuqHnmh9xKrMxR52RQVkN0MXbUdA,500 +sympy/printing/tests/test_jax.py,sha256=5sOhmFNadFAKT2FumDuk5UqgOc0e0cYkRnJTczClnG8,11062 +sympy/printing/tests/test_jscode.py,sha256=ObahZne9lQbBiXyJZLohjQGdHsG2CnWCFOB8KbFOAqQ,11369 +sympy/printing/tests/test_julia.py,sha256=7KSmbPi_0J2ZSL2fI_xLWqoR9Gy73LfNtzFANHS8EiM,14014 +sympy/printing/tests/test_lambdarepr.py,sha256=DMoIfRNmUkF0KUXoNnJ_skNPU42tJ2xlEvqCT2GxwaA,6947 +sympy/printing/tests/test_latex.py,sha256=xHG_gSlEsD-QkOIiHUyWE4zb0brJxwgwGs1jJBvvOA0,138061 +sympy/printing/tests/test_llvmjit.py,sha256=EGPeRisM60_TIVgnk7PTLSm5F-Aod_88zLjHPZwfyZ8,5344 +sympy/printing/tests/test_maple.py,sha256=dhcz2bqapB_qMyBLMgsgnvVRIcHQ-HmxvAxC_Y0Z3q8,13078 +sympy/printing/tests/test_mathematica.py,sha256=oNNTHZzHviNi_ckua2issBb05IS7GaYyluBJiCjhGoM,10954 +sympy/printing/tests/test_mathml.py,sha256=yVIJYHlFNLbt1qtbKrqjxB2P88dsD6AtS9EVsDi_vEw,99434 +sympy/printing/tests/test_numpy.py,sha256=JZwyiFUS7Vfiwn3j1ogF31PJLJRzH57dtEHq_ujV9wQ,11715 +sympy/printing/tests/test_octave.py,sha256=WOJpsuGhMYLw2ikjwdTV6kVac6-I3xkjZ7qvUVheFbU,18545 +sympy/printing/tests/test_precedence.py,sha256=Za6JCTVvRGZhNY2dqiA8XpUpR5YGck7Op2cXiV78dZA,4067 +sympy/printing/tests/test_preview.py,sha256=dSVxiGqdNR6gbF40V4J2tGhQ-T4RDvSyGypHvYcPDYM,988 +sympy/printing/tests/test_pycode.py,sha256=txwQ1jKkQNZSOXoKw9w2n-_IeIdd-QUjh7WYvOgi1rA,18363 +sympy/printing/tests/test_python.py,sha256=HN7JkzQcKSnB6718i7kaEJZ5pYMqu56z1mSmHQGzY4k,8128 +sympy/printing/tests/test_rcode.py,sha256=TXyl449eCO4J-P3DQb3w9FWvMqOdXmfuiXgyBg_inQQ,13781 +sympy/printing/tests/test_repr.py,sha256=5XmDdIDlQlCWckWq8H95Fw82h-oDxrRpMWZePb6hHa4,12980 +sympy/printing/tests/test_rust.py,sha256=ClZ30PUmZj1ZoBWL2zSt4SsWQS0WB8FrFSQE-w3bxjU,11988 +sympy/printing/tests/test_smtlib.py,sha256=S-42OTOUGkZ-k17qCQ36DYgT-tyOeYqDCvtXzrL2Kzo,22553 +sympy/printing/tests/test_str.py,sha256=QhRuH-hwpXRcPTmKClxqBv8HQdlpNjR4dMXf_XJ3jCs,43523 +sympy/printing/tests/test_tableform.py,sha256=Ff5l1QL2HxN32WS_TdFhUAVqzop8YoWY3Uz1TThvVIM,5692 +sympy/printing/tests/test_tensorflow.py,sha256=-56L5IdYszZy9CGNxS8Ix8UaFStZudFuKQlyyz6Rzzs,17100 +sympy/printing/tests/test_theanocode.py,sha256=E36Fj72HxMK0e1pKTkoTpv9wI4UvwHdVufo-JA6dYq0,21394 +sympy/printing/tests/test_torch.py,sha256=yOtB4yl1DjbTbb8I_t2bdW8fAafVC5G8ubeIJpVLPus,16842 +sympy/printing/tests/test_tree.py,sha256=_8PGAhWMQ_A0f2DQLdDeMrpxY19889P5Ih9H41RZn8s,6080 +sympy/printing/theanocode.py,sha256=d1Aznwfqz6lFR680RGrIf5ht2AH0Ei7pz-7yGZt-CLY,19094 +sympy/printing/tree.py,sha256=tQNjLV3_c3QUUce068AbzAuCiUIJb47l_7F_TYxxFUQ,3868 +sympy/release.py,sha256=oitamisYLp3lrFI3uGv-5ZCJIeYOT9gZLcszUmVNhw8,23 +sympy/sandbox/__init__.py,sha256=IaEVOYHaZ97OHEuto1UGthFuO35c0uvAZFZU23YyEaU,189 +sympy/sandbox/__pycache__/__init__.cpython-311.pyc,, +sympy/sandbox/__pycache__/indexed_integrals.cpython-311.pyc,, +sympy/sandbox/indexed_integrals.py,sha256=svh4xDIa8nGpDeH4TeRb49gG8miMvXpCzEarbor58EE,2141 +sympy/sandbox/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/sandbox/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/sandbox/tests/__pycache__/test_indexed_integrals.cpython-311.pyc,, +sympy/sandbox/tests/test_indexed_integrals.py,sha256=UK2E2wg9EMwda4Vwpzyj3rmXs6ni33HqcbyaqAww6ww,1179 +sympy/series/__init__.py,sha256=DYG9oisjzYeS55dIUpQpbAFcoDz7Q81fZJw36PRGu14,766 +sympy/series/__pycache__/__init__.cpython-311.pyc,, +sympy/series/__pycache__/acceleration.cpython-311.pyc,, +sympy/series/__pycache__/approximants.cpython-311.pyc,, +sympy/series/__pycache__/aseries.cpython-311.pyc,, +sympy/series/__pycache__/formal.cpython-311.pyc,, +sympy/series/__pycache__/fourier.cpython-311.pyc,, +sympy/series/__pycache__/gruntz.cpython-311.pyc,, +sympy/series/__pycache__/kauers.cpython-311.pyc,, +sympy/series/__pycache__/limits.cpython-311.pyc,, +sympy/series/__pycache__/limitseq.cpython-311.pyc,, +sympy/series/__pycache__/order.cpython-311.pyc,, +sympy/series/__pycache__/residues.cpython-311.pyc,, +sympy/series/__pycache__/sequences.cpython-311.pyc,, +sympy/series/__pycache__/series.cpython-311.pyc,, +sympy/series/__pycache__/series_class.cpython-311.pyc,, +sympy/series/acceleration.py,sha256=Phe8jpZ4uiS4vYAzh8PiUoHKVUjyu5K9MoAnZX3pV_w,3365 +sympy/series/approximants.py,sha256=tE-hHuoW62QJHDA3WhRlXaTkokCAODs1vXgjirhOYiQ,3181 +sympy/series/aseries.py,sha256=cHVGRQaza4ayqI6ji6OHNkdQEMV7Bko4f4vug2buEQY,255 +sympy/series/benchmarks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/series/benchmarks/__pycache__/__init__.cpython-311.pyc,, +sympy/series/benchmarks/__pycache__/bench_limit.cpython-311.pyc,, +sympy/series/benchmarks/__pycache__/bench_order.cpython-311.pyc,, +sympy/series/benchmarks/bench_limit.py,sha256=2PtdeeJtD6qyEvt9HFNvyTnMM8phFZRjscgnb4fHndU,173 +sympy/series/benchmarks/bench_order.py,sha256=iC8sQJ0lLlTgiXltAyLzSCQ-3490cf-c6NFiIU44JSk,207 +sympy/series/formal.py,sha256=9HJp9fcNX-MxcIfTtMptdKEyAw6cQQ3LyyzMyEt-9aw,51668 +sympy/series/fourier.py,sha256=H3zDg_S5D0Qeewp-o87_t_30TIg3rad83bzAoz9yp_E,22941 +sympy/series/gruntz.py,sha256=fhHr8w8SeMd-6GKjHmlwajd4FUmzeYo97AA3C_q0lKg,23497 +sympy/series/kauers.py,sha256=PzD0MATMNjLjPi9GW5GQGL6Uqc2UT-uPwnzhi7TkJH8,1720 +sympy/series/limits.py,sha256=hoSFWORWR-DodSf6Po93uK_09JUePhNvI60YsH6V4CI,13307 +sympy/series/limitseq.py,sha256=WM1Lh3RXhSZM1gQaJrhWnUtYEgJunLujIEw1gmtVhYw,7752 +sympy/series/order.py,sha256=1JjdsH2zvP8X1rf75qpfIB-iI70fkcH9CmJsXsRqSxY,19558 +sympy/series/residues.py,sha256=k46s_fFfIHdJZqfst-B_-X1R-SAWs_rR9MQH7a9JLtg,2213 +sympy/series/sequences.py,sha256=WLubAZr5AevpllgI4b6WmIfXRX88QNVCpMGBofGkcqo,35543 +sympy/series/series.py,sha256=PveJ6LibsrDX-mymTbV7teNggnsX0cfaoGMLAP0KAIo,1863 +sympy/series/series_class.py,sha256=033NJ5Re8AS4eq-chmfct3-Lz2vBqdFqXtnrbxswTx0,2918 +sympy/series/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/series/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/series/tests/__pycache__/test_approximants.cpython-311.pyc,, +sympy/series/tests/__pycache__/test_aseries.cpython-311.pyc,, +sympy/series/tests/__pycache__/test_demidovich.cpython-311.pyc,, +sympy/series/tests/__pycache__/test_formal.cpython-311.pyc,, +sympy/series/tests/__pycache__/test_fourier.cpython-311.pyc,, +sympy/series/tests/__pycache__/test_gruntz.cpython-311.pyc,, +sympy/series/tests/__pycache__/test_kauers.cpython-311.pyc,, +sympy/series/tests/__pycache__/test_limits.cpython-311.pyc,, +sympy/series/tests/__pycache__/test_limitseq.cpython-311.pyc,, +sympy/series/tests/__pycache__/test_lseries.cpython-311.pyc,, +sympy/series/tests/__pycache__/test_nseries.cpython-311.pyc,, +sympy/series/tests/__pycache__/test_order.cpython-311.pyc,, +sympy/series/tests/__pycache__/test_residues.cpython-311.pyc,, +sympy/series/tests/__pycache__/test_sequences.cpython-311.pyc,, +sympy/series/tests/__pycache__/test_series.cpython-311.pyc,, +sympy/series/tests/test_approximants.py,sha256=KViHMW1dPXn7xaPYhtTQ9L_WtLLkoIic6yfFnwZ8Q70,1012 +sympy/series/tests/test_aseries.py,sha256=5yWntog5ogRZrmjF3ZhY4e-d2mHYC1Ocs7IXRRFQrtI,2377 +sympy/series/tests/test_demidovich.py,sha256=JGYacqJMEqHS6oT2AYs9d7iutIEb32PkJs9EJqOHxcQ,4947 +sympy/series/tests/test_formal.py,sha256=TpMws7tgQHaokLrzEKZdlOcn6f04WIZhUqreaOOJ0kU,22496 +sympy/series/tests/test_fourier.py,sha256=-RnlhZUZJuctdE3pEtVMeGzCfyWzR1TFeojDhpJqyWo,5908 +sympy/series/tests/test_gruntz.py,sha256=3yPq8ksMGM7x4VQIfsFkk3JIgHlj-g1kJJBwIL76uPg,16445 +sympy/series/tests/test_kauers.py,sha256=Z85FhfXOOVki0HNGeK5BEBZOpkuB6SnKK3FqfK1-aLQ,1102 +sympy/series/tests/test_limits.py,sha256=YuwcBy8Atf-UGP8Li2sFU0guV_ZsjHqx-IdPwI6M3EA,48638 +sympy/series/tests/test_limitseq.py,sha256=QjEF99sYEDqfY7ULz1qjQTo6e0lIRUCflEOBgiDYRVA,5691 +sympy/series/tests/test_lseries.py,sha256=GlQvlBlD9wh02PPBP6zU83wmhurvGUFTuCRp44B4uI4,1875 +sympy/series/tests/test_nseries.py,sha256=uzhzYswSOe9Gh_nWKeO69tvGPMLd-9tqk4HBYX8JIm4,18284 +sympy/series/tests/test_order.py,sha256=AUlRkYKCUIs4DyT6g94ZGmjzT2fqHutPXOUGhpz4mb0,17901 +sympy/series/tests/test_residues.py,sha256=pT9xzPqtmfKGSbLLAxgDVZLTSy3TOxyfq3thTJs2VLw,3178 +sympy/series/tests/test_sequences.py,sha256=Oyq32yQZnGNQDS2uJ3by3bZ-y4G9c9BFfdQTcVuW2RM,11161 +sympy/series/tests/test_series.py,sha256=yPqSIfTytdiIYlYR9yvK7uS4IwF1fctpDiiktEnwvec,17006 +sympy/sets/__init__.py,sha256=3vjCm4v2esbpsVPY0ROwTXMETxns_66bG4FCIFZ96oM,1026 +sympy/sets/__pycache__/__init__.cpython-311.pyc,, +sympy/sets/__pycache__/conditionset.cpython-311.pyc,, +sympy/sets/__pycache__/contains.cpython-311.pyc,, +sympy/sets/__pycache__/fancysets.cpython-311.pyc,, +sympy/sets/__pycache__/ordinals.cpython-311.pyc,, +sympy/sets/__pycache__/powerset.cpython-311.pyc,, +sympy/sets/__pycache__/setexpr.cpython-311.pyc,, +sympy/sets/__pycache__/sets.cpython-311.pyc,, +sympy/sets/conditionset.py,sha256=mhodBVrMqJ6W5H8CuaFhO3FO9VdJifOEPjjFmV9lT2I,7792 +sympy/sets/contains.py,sha256=AlsfOc_6V0TH_6G7XTPIXhDywgtg3ECdjCTfSOmPC-E,1829 +sympy/sets/fancysets.py,sha256=tZ4wVasc937Yr0VkQuLVHe75Falcr72Nrkhk89178QQ,48187 +sympy/sets/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/sets/handlers/__pycache__/__init__.cpython-311.pyc,, +sympy/sets/handlers/__pycache__/add.cpython-311.pyc,, +sympy/sets/handlers/__pycache__/comparison.cpython-311.pyc,, +sympy/sets/handlers/__pycache__/functions.cpython-311.pyc,, +sympy/sets/handlers/__pycache__/intersection.cpython-311.pyc,, +sympy/sets/handlers/__pycache__/issubset.cpython-311.pyc,, +sympy/sets/handlers/__pycache__/mul.cpython-311.pyc,, +sympy/sets/handlers/__pycache__/power.cpython-311.pyc,, +sympy/sets/handlers/__pycache__/union.cpython-311.pyc,, +sympy/sets/handlers/add.py,sha256=_ucFvxuDv9wsmKxGkCDUERtYk3I_tQxjZjY3ZkroWs0,1863 +sympy/sets/handlers/comparison.py,sha256=WfT_vLrOkvPqRg2mf7gziVs_6cLg0kOTEFv-Nb1zIvo,1601 +sympy/sets/handlers/functions.py,sha256=jYSFqFNH6mXbKFPgvIAIGY8BhbLPo1dAvcNg4MxmCaI,8381 +sympy/sets/handlers/intersection.py,sha256=oYPmrx3FAkGLVWT3EjSimeMfsGqPsqVnpJm5UxQzuCM,17514 +sympy/sets/handlers/issubset.py,sha256=azka_5eOaUro3r3v72PmET0oY8-aaoJkzVEK7kuqXCA,4739 +sympy/sets/handlers/mul.py,sha256=XFbkOw4PDQumaOEUlHeQLvjhIom0f3iniSYv_Kau-xw,1842 +sympy/sets/handlers/power.py,sha256=84N3dIus7r09XV7PF_RiEpFRw1y5tOGD34WKzSM9F-4,3186 +sympy/sets/handlers/union.py,sha256=lrAdydqExnALUjM0dnoM-7JAZqtbgLb46Y2GGmFtQdw,4225 +sympy/sets/ordinals.py,sha256=GSyaBq7BHJC3pvgoCDoUKZQ0IE2VXyHtx6_g5OS64W4,7641 +sympy/sets/powerset.py,sha256=vIGnSYKngEPEt6V-6beDOXAOY9ugDLJ8fXOx5H9JJck,2913 +sympy/sets/setexpr.py,sha256=jMOQigDscLTrFPXvHqo1ODVRG9BqC4yn38Ej4m6WPa0,3019 +sympy/sets/sets.py,sha256=-iuLnPWLunCa0oOErHELO9EkLZXs6L2AFWQgkCFbJcA,81202 +sympy/sets/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/sets/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/sets/tests/__pycache__/test_conditionset.cpython-311.pyc,, +sympy/sets/tests/__pycache__/test_contains.cpython-311.pyc,, +sympy/sets/tests/__pycache__/test_fancysets.cpython-311.pyc,, +sympy/sets/tests/__pycache__/test_ordinals.cpython-311.pyc,, +sympy/sets/tests/__pycache__/test_powerset.cpython-311.pyc,, +sympy/sets/tests/__pycache__/test_setexpr.cpython-311.pyc,, +sympy/sets/tests/__pycache__/test_sets.cpython-311.pyc,, +sympy/sets/tests/test_conditionset.py,sha256=UeutWuBCT5PZicNkkl9E94pREnU5CvLGWUfmRS29PcU,11370 +sympy/sets/tests/test_contains.py,sha256=ec6WRzriwV9nurz3jS9IXEqtfL1pZedtJFp--fSsBvY,1561 +sympy/sets/tests/test_fancysets.py,sha256=oiuu5PKNrDeDO-NtgsE4CpkwQaS7JNgCYuSyc1ykKKE,51938 +sympy/sets/tests/test_ordinals.py,sha256=L4DYc6ByQMDwJGFzJC3YhfSrVk5auW7pf4QYpJ5xY7w,2637 +sympy/sets/tests/test_powerset.py,sha256=nFvDGlhAf0wG-pZnPkgJjfwDHrTwdro3MYIinwyxn94,4805 +sympy/sets/tests/test_setexpr.py,sha256=E--SjYVzrmau0EbD8g4NTqp6aLD8qHzIuI7sAfuWxpY,14797 +sympy/sets/tests/test_sets.py,sha256=oMtqcr9gr68itfWH4j18POID8tD0hAZUV8HCz7-KKj0,68544 +sympy/simplify/__init__.py,sha256=MH1vkwHq0J5tNm7ss8V6v-mjrDGUXwfOsariIwfi38c,1274 +sympy/simplify/__pycache__/__init__.cpython-311.pyc,, +sympy/simplify/__pycache__/_cse_diff.cpython-311.pyc,, +sympy/simplify/__pycache__/combsimp.cpython-311.pyc,, +sympy/simplify/__pycache__/cse_main.cpython-311.pyc,, +sympy/simplify/__pycache__/cse_opts.cpython-311.pyc,, +sympy/simplify/__pycache__/epathtools.cpython-311.pyc,, +sympy/simplify/__pycache__/fu.cpython-311.pyc,, +sympy/simplify/__pycache__/gammasimp.cpython-311.pyc,, +sympy/simplify/__pycache__/hyperexpand.cpython-311.pyc,, +sympy/simplify/__pycache__/hyperexpand_doc.cpython-311.pyc,, +sympy/simplify/__pycache__/powsimp.cpython-311.pyc,, +sympy/simplify/__pycache__/radsimp.cpython-311.pyc,, +sympy/simplify/__pycache__/ratsimp.cpython-311.pyc,, +sympy/simplify/__pycache__/simplify.cpython-311.pyc,, +sympy/simplify/__pycache__/sqrtdenest.cpython-311.pyc,, +sympy/simplify/__pycache__/traversaltools.cpython-311.pyc,, +sympy/simplify/__pycache__/trigsimp.cpython-311.pyc,, +sympy/simplify/_cse_diff.py,sha256=Nq1gXn27o1v83t__SQUuacnOohhywa39ucPaR2jD518,10479 +sympy/simplify/combsimp.py,sha256=UzgG6eC_eIzieMS3j0oQFJnS5XkZ1gxNqOSzBqJcIFQ,3604 +sympy/simplify/cse_main.py,sha256=tcUmXXFGIZyoX4PvBEmZ-YYm-CBLXoAyGLHverxP23o,31310 +sympy/simplify/cse_opts.py,sha256=ZTCaOdOrgtifWxQmFzyngrLq9uwzByBdiSS5mE-DDoE,1618 +sympy/simplify/epathtools.py,sha256=fV3oeD2J3LJRQ4YoTV6gq3yFT_NSZi-7xxKnrX-2KJA,10122 +sympy/simplify/fu.py,sha256=exevmk1p4YgEAO-MmTgh7cG7cyVMMF8p7vZ1ymwwKiI,62208 +sympy/simplify/gammasimp.py,sha256=yzfkD_q1Zxu1eMCZuprmA3VzZXhecX3K0xdZlUdYlcY,18485 +sympy/simplify/hyperexpand.py,sha256=sTWd1-VeLr0NTr-cm6nftjmO_gVPqa1P9ud_rvSZS54,84420 +sympy/simplify/hyperexpand_doc.py,sha256=E8AD0mj8ULtelDSUkmJKJY7kYm5fVfCL4QH_DX65qEw,521 +sympy/simplify/powsimp.py,sha256=aCkX2zxgOm1S9oWc260KrzhSwcJw8p3JEMZ-vkmUapM,26754 +sympy/simplify/radsimp.py,sha256=VP83Ufl_9DUtmeHCRXSmOHqdg9zDsPoQJ_7eUyCiwpI,41649 +sympy/simplify/ratsimp.py,sha256=vTu0t0k1FDFTofl4mwcK9NuTMNZ20IsL8fIyFJRFNkg,7682 +sympy/simplify/simplify.py,sha256=3rK7cCfkh1svliMVsCuTv9_aQDz3nmZvA_HRAvmNkSo,73330 +sympy/simplify/sqrtdenest.py,sha256=MsTyPUcEhiMf51fpk6e5grlqudU-LMAHOuatPCxo5uA,21600 +sympy/simplify/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/simplify/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/simplify/tests/__pycache__/test_combsimp.cpython-311.pyc,, +sympy/simplify/tests/__pycache__/test_cse.cpython-311.pyc,, +sympy/simplify/tests/__pycache__/test_cse_diff.cpython-311.pyc,, +sympy/simplify/tests/__pycache__/test_epathtools.cpython-311.pyc,, +sympy/simplify/tests/__pycache__/test_fu.cpython-311.pyc,, +sympy/simplify/tests/__pycache__/test_function.cpython-311.pyc,, +sympy/simplify/tests/__pycache__/test_gammasimp.cpython-311.pyc,, +sympy/simplify/tests/__pycache__/test_hyperexpand.cpython-311.pyc,, +sympy/simplify/tests/__pycache__/test_powsimp.cpython-311.pyc,, +sympy/simplify/tests/__pycache__/test_radsimp.cpython-311.pyc,, +sympy/simplify/tests/__pycache__/test_ratsimp.cpython-311.pyc,, +sympy/simplify/tests/__pycache__/test_rewrite.cpython-311.pyc,, +sympy/simplify/tests/__pycache__/test_simplify.cpython-311.pyc,, +sympy/simplify/tests/__pycache__/test_sqrtdenest.cpython-311.pyc,, +sympy/simplify/tests/__pycache__/test_trigsimp.cpython-311.pyc,, +sympy/simplify/tests/test_combsimp.py,sha256=O95WSxCvo2fDQs-UlarAcSf0_8M3PuTR76lhREDoNA8,2958 +sympy/simplify/tests/test_cse.py,sha256=JOjH5G6GPfeaKpKdeeKL1vvHgbt8TGxTkqmH2e1ovY8,25516 +sympy/simplify/tests/test_cse_diff.py,sha256=Y_zYZpldwjUoWiTTpFMlw4PXkqNp9r9Zwrw46gkmNoY,7275 +sympy/simplify/tests/test_epathtools.py,sha256=ugsQlfuK6POiixdeit63QovsVAlG5JyCaPlPp0j35LE,3525 +sympy/simplify/tests/test_fu.py,sha256=X2NNsynwefg2aH5GhyxQItL80fKPZ9md6nydMEedWbQ,19410 +sympy/simplify/tests/test_function.py,sha256=gzdcSFObuDzVFJDdAgmERtZJvG38WNSmclPAdG8OaPQ,2199 +sympy/simplify/tests/test_gammasimp.py,sha256=32cPRmtG-_Mz9g02lmmn-PWDD3J_Ku6sxLxIUU7WqxE,5320 +sympy/simplify/tests/test_hyperexpand.py,sha256=y3lxd97UZ1BkrrZe0r2l6MM8zNABTE_L4KtxFwmdzeQ,41043 +sympy/simplify/tests/test_powsimp.py,sha256=FWI_K8nozwYzhrhY1Q0CZkf6n88hFR0jjuYRvQnMius,14375 +sympy/simplify/tests/test_radsimp.py,sha256=Gf6Tf7-lb5G8gqF6hJA6kPrZNFES6fCz7_-ls7TFUfY,19128 +sympy/simplify/tests/test_ratsimp.py,sha256=bv-K60A7m2on-U9szzaeYO7Hlp1EK5P0HvBBO59oyas,2198 +sympy/simplify/tests/test_rewrite.py,sha256=LZj4V6a95GJj1o3NlKRoHMk7sWGPASFlw24nsm4z43k,1127 +sympy/simplify/tests/test_simplify.py,sha256=QkiaVVj0nqHXvu1lUUYZfYZ6_Ods6DfiBwFiqAJKSoo,41928 +sympy/simplify/tests/test_sqrtdenest.py,sha256=4zRtDQVGpKRRBYSAnEF5pSM0AR_fAMumONu2Ocb3tqg,7470 +sympy/simplify/tests/test_trigsimp.py,sha256=vG5PDTDNOuFypT7H9DSMjIollPqkKdNhWv5FBj6vFnE,19949 +sympy/simplify/traversaltools.py,sha256=pn_t9Yrk_SL1X0vl-zVR6yZaxkY25D4MwTBv4ywnD1Y,409 +sympy/simplify/trigsimp.py,sha256=t36QKVf_0yqVCNo1hUBlBp5locL0sMH1AlsAwQCzpLQ,46857 +sympy/solvers/__init__.py,sha256=nb1LceF2MwvULXsCN8VUq7X2tVgZOPsmWYHGFszIdW4,2333 +sympy/solvers/__pycache__/__init__.cpython-311.pyc,, +sympy/solvers/__pycache__/bivariate.cpython-311.pyc,, +sympy/solvers/__pycache__/decompogen.cpython-311.pyc,, +sympy/solvers/__pycache__/deutils.cpython-311.pyc,, +sympy/solvers/__pycache__/inequalities.cpython-311.pyc,, +sympy/solvers/__pycache__/pde.cpython-311.pyc,, +sympy/solvers/__pycache__/polysys.cpython-311.pyc,, +sympy/solvers/__pycache__/recurr.cpython-311.pyc,, +sympy/solvers/__pycache__/simplex.cpython-311.pyc,, +sympy/solvers/__pycache__/solvers.cpython-311.pyc,, +sympy/solvers/__pycache__/solveset.cpython-311.pyc,, +sympy/solvers/benchmarks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/solvers/benchmarks/__pycache__/__init__.cpython-311.pyc,, +sympy/solvers/benchmarks/__pycache__/bench_solvers.cpython-311.pyc,, +sympy/solvers/benchmarks/bench_solvers.py,sha256=ZVK2TIW0XjWRDBex054ymmVlSBQw-RIBhEL1wS2ZAmU,288 +sympy/solvers/bivariate.py,sha256=Ku4Ttid3yU3CIc4IPfJ7bedLtXVaGKvFOonEIlZrXXw,17870 +sympy/solvers/decompogen.py,sha256=dWQla7hp7A4RqI2a0qRNQLWNPEuur68lD3dVTyktdBU,3757 +sympy/solvers/deutils.py,sha256=jukqDw6JGZfl36HRFjZIX_A0_iarmTawI0Up7z5tQDc,10315 +sympy/solvers/diophantine/__init__.py,sha256=I1p3uj3kFQv20cbsZ34K5rNCx1_pDS7JwHUCFstpBgs,128 +sympy/solvers/diophantine/__pycache__/__init__.cpython-311.pyc,, +sympy/solvers/diophantine/__pycache__/diophantine.cpython-311.pyc,, +sympy/solvers/diophantine/diophantine.py,sha256=n74XrMIW6ZwrAfaVVks7nwB01GClI0SYdvTe5K90Jnc,122482 +sympy/solvers/diophantine/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/solvers/diophantine/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/solvers/diophantine/tests/__pycache__/test_diophantine.cpython-311.pyc,, +sympy/solvers/diophantine/tests/test_diophantine.py,sha256=N7ZCyNVfSV0JzAKbnc_8YbUFq5ZkiDLnO6MxxMxCG90,43335 +sympy/solvers/inequalities.py,sha256=kz6HzAytg4V2eqSKwGzbJuYyFfgj6bFSUgk1i15zVp0,33189 +sympy/solvers/ode/__init__.py,sha256=I7RKwCcaoerflUm5i3ZDJgBIOnkhBjb83BCHcVcFqfM,468 +sympy/solvers/ode/__pycache__/__init__.cpython-311.pyc,, +sympy/solvers/ode/__pycache__/hypergeometric.cpython-311.pyc,, +sympy/solvers/ode/__pycache__/lie_group.cpython-311.pyc,, +sympy/solvers/ode/__pycache__/nonhomogeneous.cpython-311.pyc,, +sympy/solvers/ode/__pycache__/ode.cpython-311.pyc,, +sympy/solvers/ode/__pycache__/riccati.cpython-311.pyc,, +sympy/solvers/ode/__pycache__/single.cpython-311.pyc,, +sympy/solvers/ode/__pycache__/subscheck.cpython-311.pyc,, +sympy/solvers/ode/__pycache__/systems.cpython-311.pyc,, +sympy/solvers/ode/hypergeometric.py,sha256=t9KN5AdB7yF-rRoatEBzUYBcrwvIrKm83KBDa5BJYqY,10048 +sympy/solvers/ode/lie_group.py,sha256=C2HoOVtDbIT2ryx4TzEvQU46Yqc8LurTBBtjv5m2-BQ,39175 +sympy/solvers/ode/nonhomogeneous.py,sha256=Wi6UxjYNAPcfJsEzKHNvIJ8zhJ7-ZOkqccCR93MpUuM,17875 +sympy/solvers/ode/ode.py,sha256=9UQ0bqcrFOsGuSxGfRJpd2CllIWfiUYZPC2-A7Crefo,145379 +sympy/solvers/ode/riccati.py,sha256=3ZrkCy3ufXCyCx8gJfAuxyCOgbOLEHtvTh2VgJhA7Mk,30736 +sympy/solvers/ode/single.py,sha256=yBpPVFK3LsCf07dDb9_x6tzhp5d4ln_xIpMZpP2Ryrc,109465 +sympy/solvers/ode/subscheck.py,sha256=CIPca_qTxL9z5oaD2e2NrgME0eVQgF9PabZndcVqHZM,16130 +sympy/solvers/ode/systems.py,sha256=Kok2AMO4YEKX38fi2LrxLm5zpTcNXz398b7OBvrUezU,71467 +sympy/solvers/ode/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/solvers/ode/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/solvers/ode/tests/__pycache__/test_lie_group.cpython-311.pyc,, +sympy/solvers/ode/tests/__pycache__/test_ode.cpython-311.pyc,, +sympy/solvers/ode/tests/__pycache__/test_riccati.cpython-311.pyc,, +sympy/solvers/ode/tests/__pycache__/test_single.cpython-311.pyc,, +sympy/solvers/ode/tests/__pycache__/test_subscheck.cpython-311.pyc,, +sympy/solvers/ode/tests/__pycache__/test_systems.cpython-311.pyc,, +sympy/solvers/ode/tests/test_lie_group.py,sha256=vg1yy_-a5x1Xm2IcVkEi5cD2uA5wE5gjqpfBwkV1vZc,5319 +sympy/solvers/ode/tests/test_ode.py,sha256=bEcTi9zA5TooKOE9duFtd6TAVRvLPrO92arX7fF85-Q,48508 +sympy/solvers/ode/tests/test_riccati.py,sha256=a2_pXzCFb9WDtT_kdxYkfLd-PG5xvs4rQn_vpFk-O9s,29348 +sympy/solvers/ode/tests/test_single.py,sha256=sEvhXws920Oe_IuvmBIYM5iPViR6I3njZNgJlc8sMC4,100200 +sympy/solvers/ode/tests/test_subscheck.py,sha256=Gzwc9h9n6zlNOhJ8Qh6fQDeB8ghaRmgv3ktBAfPJx-U,12468 +sympy/solvers/ode/tests/test_systems.py,sha256=PeIEHOx8-eZQNuqOfhjTvGEeFSVriLlHShPmy84mde4,129087 +sympy/solvers/pde.py,sha256=V2jxgmmepng2ofFFDVOsBHWqf-IUQCgIz4lXzhBUeyg,34913 +sympy/solvers/polysys.py,sha256=4CCK3FNhl-K6B4bYF_goSrAq1JOMubl6KsmaB_5R2f4,27168 +sympy/solvers/recurr.py,sha256=DyssZuOyemoC6J1cWq635O7zkg1WLHrR7KGoM-gNy0g,25389 +sympy/solvers/simplex.py,sha256=peRv44aDM30U_0wk8xZv3t6M_cpm_a9hBALiyr7zndY,35276 +sympy/solvers/solvers.py,sha256=xsSlQaj0XaunexmhfQZZI_uiRXHETmaUab9hgZrmmbU,138454 +sympy/solvers/solveset.py,sha256=6VxsiKcn4fwdJGYXA8k1si5zvYs6vNrSTtLcmSKpjMk,151683 +sympy/solvers/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/solvers/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/solvers/tests/__pycache__/test_constantsimp.cpython-311.pyc,, +sympy/solvers/tests/__pycache__/test_decompogen.cpython-311.pyc,, +sympy/solvers/tests/__pycache__/test_inequalities.cpython-311.pyc,, +sympy/solvers/tests/__pycache__/test_numeric.cpython-311.pyc,, +sympy/solvers/tests/__pycache__/test_pde.cpython-311.pyc,, +sympy/solvers/tests/__pycache__/test_polysys.cpython-311.pyc,, +sympy/solvers/tests/__pycache__/test_recurr.cpython-311.pyc,, +sympy/solvers/tests/__pycache__/test_simplex.cpython-311.pyc,, +sympy/solvers/tests/__pycache__/test_solvers.cpython-311.pyc,, +sympy/solvers/tests/__pycache__/test_solveset.cpython-311.pyc,, +sympy/solvers/tests/test_constantsimp.py,sha256=9Feugsg9jD2BwQiG4EFpb9fORyst6JdBmZqq2GaOgH8,8707 +sympy/solvers/tests/test_decompogen.py,sha256=7GUsDQQZtYbZIK0p0UxsOuNEJxEt4IHeOSsem_k-k0U,2943 +sympy/solvers/tests/test_inequalities.py,sha256=whg3vGXEYxeIHNQS4yBeB9VQpoYWnfw5NBS4xLiqDJ8,21025 +sympy/solvers/tests/test_numeric.py,sha256=pKLBJuf4lGCewf5DgBjvH5T9phf6ilkyW1gB2R-5SzA,4734 +sympy/solvers/tests/test_pde.py,sha256=UGP3uWjF8pKQgfPifmdfvS5URVmzSg6m2NkS7LGzmio,9257 +sympy/solvers/tests/test_polysys.py,sha256=bK2ckIhQvbJCc6_qsUOAuAEyTc5CR8J2yiqN4MFOgpM,15266 +sympy/solvers/tests/test_recurr.py,sha256=-OeghSg16GFN70y_RUXC6CF6VU_b7NXaKDbejtRSocg,11418 +sympy/solvers/tests/test_simplex.py,sha256=pG7j7KxXrlK8K_fF04XH_mvMzwEOopSBpJZiagKjyNs,9037 +sympy/solvers/tests/test_solvers.py,sha256=eXM6AT2upxlRC7S2l1YxUSS6jJNM42PxzYCTz4C4vmc,107765 +sympy/solvers/tests/test_solveset.py,sha256=lOQ4hG7Dg6yuDgFriKIlvWdzEumehgL08Srsh4JwDHY,148480 +sympy/stats/__init__.py,sha256=9pY3OMdvAIeJ_Q1ZqynOZ8hHWFFrrxOx0qBHKkGfHCg,8487 +sympy/stats/__pycache__/__init__.cpython-311.pyc,, +sympy/stats/__pycache__/compound_rv.cpython-311.pyc,, +sympy/stats/__pycache__/crv.cpython-311.pyc,, +sympy/stats/__pycache__/crv_types.cpython-311.pyc,, +sympy/stats/__pycache__/drv.cpython-311.pyc,, +sympy/stats/__pycache__/drv_types.cpython-311.pyc,, +sympy/stats/__pycache__/error_prop.cpython-311.pyc,, +sympy/stats/__pycache__/frv.cpython-311.pyc,, +sympy/stats/__pycache__/frv_types.cpython-311.pyc,, +sympy/stats/__pycache__/joint_rv.cpython-311.pyc,, +sympy/stats/__pycache__/joint_rv_types.cpython-311.pyc,, +sympy/stats/__pycache__/matrix_distributions.cpython-311.pyc,, +sympy/stats/__pycache__/random_matrix.cpython-311.pyc,, +sympy/stats/__pycache__/random_matrix_models.cpython-311.pyc,, +sympy/stats/__pycache__/rv.cpython-311.pyc,, +sympy/stats/__pycache__/rv_interface.cpython-311.pyc,, +sympy/stats/__pycache__/stochastic_process.cpython-311.pyc,, +sympy/stats/__pycache__/stochastic_process_types.cpython-311.pyc,, +sympy/stats/__pycache__/symbolic_multivariate_probability.cpython-311.pyc,, +sympy/stats/__pycache__/symbolic_probability.cpython-311.pyc,, +sympy/stats/compound_rv.py,sha256=SO1KXJ0aHGbD5y9QA8o6qOHbio3ua8wyO2Rsh0Hnw48,7965 +sympy/stats/crv.py,sha256=jd8iemE41aW-byXgFDYKaMv2VOOUnyUtiMx_QAXI-n4,21028 +sympy/stats/crv_types.py,sha256=qXYxtvJPg4ORJI7AxIkRhwnJ6lcz3sVBWf4toQy9vvA,122371 +sympy/stats/drv.py,sha256=Zu8IK9yt69XJFKt-ShoJfZbvZ-CGMwvrKNiXvi6q_n4,11994 +sympy/stats/drv_types.py,sha256=2jL6QYa9lAlbvTxvx9BXdfwU6PRt0uzV3CVXRIeZ5k0,19865 +sympy/stats/error_prop.py,sha256=a-H6GZEidsiP_4-iNw7nSD99AMyN6DNHsSl0IUZGIAs,3315 +sympy/stats/frv.py,sha256=vZROeD6DSVGX0kfOL_yOds3pZgjSiXFX-bMZtSUkVMA,16874 +sympy/stats/frv_types.py,sha256=Xkc3MU5dFLajPajIStW_lV4qiuiHg86rBo45zX_oGLk,23348 +sympy/stats/joint_rv.py,sha256=DcixlO2Ml4gnwMmZk2VTegiHVq88DkLdQlOTQ57SQtc,15963 +sympy/stats/joint_rv_types.py,sha256=PUatR4WcPHmAHadt8iRh5xYh5NJigzYh-EoAMR5blDw,30575 +sympy/stats/matrix_distributions.py,sha256=3OricwEMM_NU8b2lJxoiSTml7kvqrNQ6IUIn9Xy_DsY,21953 +sympy/stats/random_matrix.py,sha256=NmzLC5JMDWI2TvH8tY6go8lYyHmqcZ-B7sSIO7z7oAk,1028 +sympy/stats/random_matrix_models.py,sha256=7i5XAUYxt-ekmP5KDMaytUlmCvxglEspoWbswSf82tE,15328 +sympy/stats/rv.py,sha256=rmjdVpsvJrqtMgnreuT_7lU5S_V2nkvlrpUvA7zCWLM,54579 +sympy/stats/rv_interface.py,sha256=0ZSplwmkEtio_UTlh7bqSK_1aILgvk2JTfZ-CkOcfAc,13935 +sympy/stats/sampling/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/stats/sampling/__pycache__/__init__.cpython-311.pyc,, +sympy/stats/sampling/__pycache__/sample_numpy.cpython-311.pyc,, +sympy/stats/sampling/__pycache__/sample_pymc.cpython-311.pyc,, +sympy/stats/sampling/__pycache__/sample_scipy.cpython-311.pyc,, +sympy/stats/sampling/sample_numpy.py,sha256=B4ZC7ZBrSD6ICQT468rOy-xrOgQDuecsHa0zJesAeYE,4229 +sympy/stats/sampling/sample_pymc.py,sha256=9g-n04aXSFc6F7FJ5zTYtHHL6W8-26g1nrgtamJc3Hw,2995 +sympy/stats/sampling/sample_scipy.py,sha256=ysqpDy8bp1RMH0g5FFgMmp2SQuXGFkcSH7JDZEpiZ8w,6329 +sympy/stats/sampling/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/stats/sampling/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/stats/sampling/tests/__pycache__/test_sample_continuous_rv.cpython-311.pyc,, +sympy/stats/sampling/tests/__pycache__/test_sample_discrete_rv.cpython-311.pyc,, +sympy/stats/sampling/tests/__pycache__/test_sample_finite_rv.cpython-311.pyc,, +sympy/stats/sampling/tests/test_sample_continuous_rv.py,sha256=Gh8hFN1hFFsthEv9wP2ZdgghQfaEnE8n7HlmyXXhN1E,5708 +sympy/stats/sampling/tests/test_sample_discrete_rv.py,sha256=Z3hqjHEFaIoB0hrhpy7YYL_7Dtqzmy5YMWAYrlqPu38,3360 +sympy/stats/sampling/tests/test_sample_finite_rv.py,sha256=dWwrFePw8eX2rBheAXi1AVxr_gqBD63VZKfW81hNoQc,3061 +sympy/stats/stochastic_process.py,sha256=pDz0rbKXTiaNmMmmz70dP3F_KWL_XhoCKFHYBNt1QeU,2312 +sympy/stats/stochastic_process_types.py,sha256=YEUA-saTh8WF6Pn7GcgrcyclRCb_SC13YnpgOIpu7-Q,88553 +sympy/stats/symbolic_multivariate_probability.py,sha256=R6Co7XCcxLoOtTqC6ZSnGuylZNUBrC5AD0DrJr2jE1A,10450 +sympy/stats/symbolic_probability.py,sha256=HkWUiH9EM_DQ1aM1FxNmBm_8xtqeviIsyUpqFl2RYnI,23266 +sympy/stats/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/stats/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/stats/tests/__pycache__/test_compound_rv.cpython-311.pyc,, +sympy/stats/tests/__pycache__/test_continuous_rv.cpython-311.pyc,, +sympy/stats/tests/__pycache__/test_discrete_rv.cpython-311.pyc,, +sympy/stats/tests/__pycache__/test_error_prop.cpython-311.pyc,, +sympy/stats/tests/__pycache__/test_finite_rv.cpython-311.pyc,, +sympy/stats/tests/__pycache__/test_joint_rv.cpython-311.pyc,, +sympy/stats/tests/__pycache__/test_matrix_distributions.cpython-311.pyc,, +sympy/stats/tests/__pycache__/test_mix.cpython-311.pyc,, +sympy/stats/tests/__pycache__/test_random_matrix.cpython-311.pyc,, +sympy/stats/tests/__pycache__/test_rv.cpython-311.pyc,, +sympy/stats/tests/__pycache__/test_stochastic_process.cpython-311.pyc,, +sympy/stats/tests/__pycache__/test_symbolic_multivariate.cpython-311.pyc,, +sympy/stats/tests/__pycache__/test_symbolic_probability.cpython-311.pyc,, +sympy/stats/tests/test_compound_rv.py,sha256=2927chbHTThA34Ki-ji319QT7ajQ1ueC640Mga-18ZA,6263 +sympy/stats/tests/test_continuous_rv.py,sha256=BcBkiyX7e1QiwS6xnD-qLzXnijvqbsebTzXEn3IfGyE,56140 +sympy/stats/tests/test_discrete_rv.py,sha256=bLqMtKpH5fDjuP7uuRAtAjakbahuFz9x3dY-PPKFsGk,11230 +sympy/stats/tests/test_error_prop.py,sha256=xKAkw3F5XJ72xiDREI7PkyReWNVW_89CD_mjOY_diDY,1933 +sympy/stats/tests/test_finite_rv.py,sha256=JHYgY4snFF5t9qcnQfKaN5zaGsO7_SuNR7Tq234W4No,20413 +sympy/stats/tests/test_joint_rv.py,sha256=W28rCRYczv5Jax7k-bj7OveT-y-AP4q-kRR0-LNaWX0,18653 +sympy/stats/tests/test_matrix_distributions.py,sha256=9daJUiSGaLq34TeZfB-xPqC8xz6vECGrm0DdBZaQPyY,8857 +sympy/stats/tests/test_mix.py,sha256=Cplnw06Ki96Y_4fx6Bu7lUXjxoIfX7tNJasm9SOz5wQ,3991 +sympy/stats/tests/test_random_matrix.py,sha256=CiD1hV25MGHwTfHGaoaehGD3iJ4lqNYi-ZiwReO6CVk,5842 +sympy/stats/tests/test_rv.py,sha256=Bp7UwffIMO7oc8UnFV11yYGcXUjSa0NhsuOgQaNRMt8,12959 +sympy/stats/tests/test_stochastic_process.py,sha256=i-VCOZrjpJtvyTBm9xgniTCkk_iUYIuFnkiyxzkn6Ig,39323 +sympy/stats/tests/test_symbolic_multivariate.py,sha256=G3AgbRbt0DQ-p0DYXYDjbx4e4f5FIgd31F34e0NO2n8,5580 +sympy/stats/tests/test_symbolic_probability.py,sha256=k5trScMiwSgl9dzJt30BV-t0KuYcyD-s9HtT2-hVhQ0,9398 +sympy/strategies/__init__.py,sha256=XaTAPqDoi6527juvR8LLN1mv6ZcslDrGloTTBMjJzxA,1402 +sympy/strategies/__pycache__/__init__.cpython-311.pyc,, +sympy/strategies/__pycache__/core.cpython-311.pyc,, +sympy/strategies/__pycache__/rl.cpython-311.pyc,, +sympy/strategies/__pycache__/tools.cpython-311.pyc,, +sympy/strategies/__pycache__/traverse.cpython-311.pyc,, +sympy/strategies/__pycache__/tree.cpython-311.pyc,, +sympy/strategies/__pycache__/util.cpython-311.pyc,, +sympy/strategies/branch/__init__.py,sha256=xxbMwR2LzLcQWsH9ss8ddE99VHFJTY-cYiR6xhO3tj0,356 +sympy/strategies/branch/__pycache__/__init__.cpython-311.pyc,, +sympy/strategies/branch/__pycache__/core.cpython-311.pyc,, +sympy/strategies/branch/__pycache__/tools.cpython-311.pyc,, +sympy/strategies/branch/__pycache__/traverse.cpython-311.pyc,, +sympy/strategies/branch/core.py,sha256=QiXSa7uhvmUBTLyUwBQHrYkWlOceKh5p4kVD90VnCKM,2759 +sympy/strategies/branch/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/strategies/branch/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/strategies/branch/tests/__pycache__/test_core.cpython-311.pyc,, +sympy/strategies/branch/tests/__pycache__/test_tools.cpython-311.pyc,, +sympy/strategies/branch/tests/__pycache__/test_traverse.cpython-311.pyc,, +sympy/strategies/branch/tests/test_core.py,sha256=23KQWJxC_2T1arwMAkt9pY1ZtG59avlxTZcVTn81UPI,2246 +sympy/strategies/branch/tests/test_tools.py,sha256=4BDkqVqrTlsivQ0PldQr6PjVZsAikc39tSxGAQA3ir8,942 +sympy/strategies/branch/tests/test_traverse.py,sha256=6rikMnZdamSzww1sSiM-aQwqa4lQrpM-DpOU9XCbiOQ,1322 +sympy/strategies/branch/tools.py,sha256=tvv3IjmQGNYbo-slCbbDf_rylZd537wvLcpdBtT-bbY,357 +sympy/strategies/branch/traverse.py,sha256=7iBViQdNpKu-AHoFED7_C9KBSyYcQBfLGopEJQbNtvk,799 +sympy/strategies/core.py,sha256=nsH6LZgyc_aslv4Na5XvJMEizC6uSzscRlVW91k1pu4,3956 +sympy/strategies/rl.py,sha256=-M5lfrtcREkvbW4YfoiB19wfYyMLKz6SmL887ymKeoI,4404 +sympy/strategies/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/strategies/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/strategies/tests/__pycache__/test_core.cpython-311.pyc,, +sympy/strategies/tests/__pycache__/test_rl.cpython-311.pyc,, +sympy/strategies/tests/__pycache__/test_tools.cpython-311.pyc,, +sympy/strategies/tests/__pycache__/test_traverse.cpython-311.pyc,, +sympy/strategies/tests/__pycache__/test_tree.cpython-311.pyc,, +sympy/strategies/tests/test_core.py,sha256=42XHlv1hN1S1QPEf2r9pddZ2EQL6o4FEPQvfo-UmXcw,2152 +sympy/strategies/tests/test_rl.py,sha256=wm0L6pdvddBgRcwhpiSk-nCgyzVGickfnOCkmHWS0j4,1949 +sympy/strategies/tests/test_tools.py,sha256=UdMojFIn3f1b2x2iRGv1Wfnwdso-Kl57GTyjCU_DjzQ,875 +sympy/strategies/tests/test_traverse.py,sha256=jWuZhYEt-F18_rxEMhn6OgGQ1GNs-dM_GFZ2F5nHs2I,2082 +sympy/strategies/tests/test_tree.py,sha256=9NL948rt6i9tYU6CQz9VNxE6l1begQs-MxP2euzE3Sc,2400 +sympy/strategies/tools.py,sha256=ERASzEP2SP-EcJ8p-4XyREYB15q3t81x1cyamJ-M880,1368 +sympy/strategies/traverse.py,sha256=DhPnBJ5Rw_xzhGiBtSciTyV-H2zhlxgjYVjrNH-gLyk,1183 +sympy/strategies/tree.py,sha256=ggnP9l3NIpJsssBMVKr4-yM_m8uCkrkm191ZC6MfZjc,3770 +sympy/strategies/util.py,sha256=2fbR813IY4IYco5mBoGJLu5z88OhXmwuIxgOO9IvZO4,361 +sympy/tensor/__init__.py,sha256=VMNXCRSayigQT6a3cvf5M_M-wdV-KSil_JbAmHcuUQc,870 +sympy/tensor/__pycache__/__init__.cpython-311.pyc,, +sympy/tensor/__pycache__/functions.cpython-311.pyc,, +sympy/tensor/__pycache__/index_methods.cpython-311.pyc,, +sympy/tensor/__pycache__/indexed.cpython-311.pyc,, +sympy/tensor/__pycache__/tensor.cpython-311.pyc,, +sympy/tensor/__pycache__/toperators.cpython-311.pyc,, +sympy/tensor/array/__init__.py,sha256=lTT1EwV5tb3WAvmmS_mIjhCSWSLiB0NNPW4n9_3fu0k,8244 +sympy/tensor/array/__pycache__/__init__.cpython-311.pyc,, +sympy/tensor/array/__pycache__/array_comprehension.cpython-311.pyc,, +sympy/tensor/array/__pycache__/array_derivatives.cpython-311.pyc,, +sympy/tensor/array/__pycache__/arrayop.cpython-311.pyc,, +sympy/tensor/array/__pycache__/dense_ndim_array.cpython-311.pyc,, +sympy/tensor/array/__pycache__/mutable_ndim_array.cpython-311.pyc,, +sympy/tensor/array/__pycache__/ndim_array.cpython-311.pyc,, +sympy/tensor/array/__pycache__/sparse_ndim_array.cpython-311.pyc,, +sympy/tensor/array/array_comprehension.py,sha256=01PTIbkAGaq0CDcaI_2KsaMnYm1nxQ8sFAiHHcc__gw,12262 +sympy/tensor/array/array_derivatives.py,sha256=c-gYeA_qpXOY3aexyz7psSqmTVIGVBrcGDvSkW5dZV0,4796 +sympy/tensor/array/arrayop.py,sha256=KvFGYWcYvChWkThtVAotlaSrcfjHogAxvpxWvp6dSgo,18397 +sympy/tensor/array/dense_ndim_array.py,sha256=Ie8qVMJyp2Tsq7aVhmZpPX8X-KTlF9uaxkQfTzCZ9z8,6433 +sympy/tensor/array/expressions/__init__.py,sha256=OUMJjZY7HtWJL0ygqkdWC8LdCqibJZhHCfYeXu-eB4E,7045 +sympy/tensor/array/expressions/__pycache__/__init__.cpython-311.pyc,, +sympy/tensor/array/expressions/__pycache__/array_expressions.cpython-311.pyc,, +sympy/tensor/array/expressions/__pycache__/arrayexpr_derivatives.cpython-311.pyc,, +sympy/tensor/array/expressions/__pycache__/conv_array_to_indexed.cpython-311.pyc,, +sympy/tensor/array/expressions/__pycache__/conv_array_to_matrix.cpython-311.pyc,, +sympy/tensor/array/expressions/__pycache__/conv_indexed_to_array.cpython-311.pyc,, +sympy/tensor/array/expressions/__pycache__/conv_matrix_to_array.cpython-311.pyc,, +sympy/tensor/array/expressions/__pycache__/from_array_to_indexed.cpython-311.pyc,, +sympy/tensor/array/expressions/__pycache__/from_array_to_matrix.cpython-311.pyc,, +sympy/tensor/array/expressions/__pycache__/from_indexed_to_array.cpython-311.pyc,, +sympy/tensor/array/expressions/__pycache__/from_matrix_to_array.cpython-311.pyc,, +sympy/tensor/array/expressions/__pycache__/utils.cpython-311.pyc,, +sympy/tensor/array/expressions/array_expressions.py,sha256=28TI8cwwQ4Q-QCI9X_j6X-lG0kR6amjAe9-h9JKb5EU,76961 +sympy/tensor/array/expressions/arrayexpr_derivatives.py,sha256=W9-bY2LL83lLSNHXItzqjOgvf-HIDbUXPoVw8uOymcg,6249 +sympy/tensor/array/expressions/conv_array_to_indexed.py,sha256=BIwlQr7RKC8bZN3mR8ICC5TYOC9uasYcV0Zc1VNKmiE,445 +sympy/tensor/array/expressions/conv_array_to_matrix.py,sha256=85YZBTZI4o9dJtKDJXXug_lJVLG8dT_22AT7l7DKoyE,416 +sympy/tensor/array/expressions/conv_indexed_to_array.py,sha256=EyW52TplBxIx25mUDvI_5Tzc8LD6Mnp6XNW9wIw9pH4,254 +sympy/tensor/array/expressions/conv_matrix_to_array.py,sha256=XYyqt0NsQSrgNpEkr8xTGeUhR7ZYeNljVFfVEF1K7vA,250 +sympy/tensor/array/expressions/from_array_to_indexed.py,sha256=3YIcsAzWVWQRJYQS90uPvSl2dM7ZqLV_qt7E9-uYU28,3936 +sympy/tensor/array/expressions/from_array_to_matrix.py,sha256=4HUbCfR2BHd54Ijws9DCcSs6aAJ-k8RwT195NTQImis,41435 +sympy/tensor/array/expressions/from_indexed_to_array.py,sha256=RUcKemmrwuK5RFRr19YSPVMCOkZfLAWlbbB56u8Wi0g,11187 +sympy/tensor/array/expressions/from_matrix_to_array.py,sha256=yIY1RupF9-FVV3jZLsqWxZ1ckoE1-HkQyM8cQIm4_Gs,3929 +sympy/tensor/array/expressions/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/tensor/array/expressions/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/tensor/array/expressions/tests/__pycache__/test_array_expressions.cpython-311.pyc,, +sympy/tensor/array/expressions/tests/__pycache__/test_arrayexpr_derivatives.cpython-311.pyc,, +sympy/tensor/array/expressions/tests/__pycache__/test_as_explicit.cpython-311.pyc,, +sympy/tensor/array/expressions/tests/__pycache__/test_convert_array_to_indexed.cpython-311.pyc,, +sympy/tensor/array/expressions/tests/__pycache__/test_convert_array_to_matrix.cpython-311.pyc,, +sympy/tensor/array/expressions/tests/__pycache__/test_convert_indexed_to_array.cpython-311.pyc,, +sympy/tensor/array/expressions/tests/__pycache__/test_convert_matrix_to_array.cpython-311.pyc,, +sympy/tensor/array/expressions/tests/__pycache__/test_deprecated_conv_modules.cpython-311.pyc,, +sympy/tensor/array/expressions/tests/test_array_expressions.py,sha256=QUAdxQ9TvBpDEAZoJpLSWwbqjmuflPe3xBRP30lFZr0,31262 +sympy/tensor/array/expressions/tests/test_arrayexpr_derivatives.py,sha256=lpC4ly6MJLDRBcVt3GcP3H6ke9bI-o3VULw0xyF5QbY,2470 +sympy/tensor/array/expressions/tests/test_as_explicit.py,sha256=nOjFKXCqYNu2O7Szc1TD1x1bsUchPRAG3nGlNGEd1Yg,2568 +sympy/tensor/array/expressions/tests/test_convert_array_to_indexed.py,sha256=6yNxGXH6BX5607FTjMkwR2t9wNVlEhV8JMSh4UIWux8,2500 +sympy/tensor/array/expressions/tests/test_convert_array_to_matrix.py,sha256=2vkSep9CPKYrQQS0u8Ayn_sc7yek1zwzjjCWK5cfYe8,29311 +sympy/tensor/array/expressions/tests/test_convert_indexed_to_array.py,sha256=RVEG_qUsXiBH9gHtWp2-9pMC4J2aLc4iUdzBFM0QyTw,8615 +sympy/tensor/array/expressions/tests/test_convert_matrix_to_array.py,sha256=G2g5E0l-FABwYyQowbKKvLcEI8NViJXaYLW3eUEcvjw,4595 +sympy/tensor/array/expressions/tests/test_deprecated_conv_modules.py,sha256=DG8IoUtxCy2acWjUHUUKu4bRsTxXbeFLFjKMLA2GdLY,1216 +sympy/tensor/array/expressions/utils.py,sha256=Rn58boHHUEoBZFtinDpruLWFBkNBwgkVQ4c9m7Nym1o,3939 +sympy/tensor/array/mutable_ndim_array.py,sha256=M0PTt8IOIcVXqQPWe2N50sm4Eq2bodRXV4Vkd08crXk,277 +sympy/tensor/array/ndim_array.py,sha256=bZvA21fSTMc2sQLJN4AovQspddzmnINBWFPr-TeGmm0,19138 +sympy/tensor/array/sparse_ndim_array.py,sha256=4nD_Hg-JdC_1mYQTohmKFfL5M1Ugdq0fpnDUILkTtq8,6387 +sympy/tensor/array/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/tensor/array/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/tensor/array/tests/__pycache__/test_array_comprehension.cpython-311.pyc,, +sympy/tensor/array/tests/__pycache__/test_array_derivatives.cpython-311.pyc,, +sympy/tensor/array/tests/__pycache__/test_arrayop.cpython-311.pyc,, +sympy/tensor/array/tests/__pycache__/test_immutable_ndim_array.cpython-311.pyc,, +sympy/tensor/array/tests/__pycache__/test_mutable_ndim_array.cpython-311.pyc,, +sympy/tensor/array/tests/__pycache__/test_ndim_array.cpython-311.pyc,, +sympy/tensor/array/tests/__pycache__/test_ndim_array_conversions.cpython-311.pyc,, +sympy/tensor/array/tests/test_array_comprehension.py,sha256=e8MsWbvwmr-HxTfaM7i8HoIVofa8InLzF9PTCIVvzjU,4529 +sympy/tensor/array/tests/test_array_derivatives.py,sha256=hS10Bkb3F2kWoFoxk2ucr21p0r5DfwHDYPI8HcmAl0o,1601 +sympy/tensor/array/tests/test_arrayop.py,sha256=WahGcUnArsAo9eaMqGT7_AjKons0WgFzLOWTtNvnSEI,25844 +sympy/tensor/array/tests/test_immutable_ndim_array.py,sha256=9ji_14szn-qoL6DQ5muzIFNaXefT7n55PFigXoFwk50,15823 +sympy/tensor/array/tests/test_mutable_ndim_array.py,sha256=rFFa0o0AJYgPNnpqijl91Vb9EW2kgHGQc6cu9f1fIvY,13070 +sympy/tensor/array/tests/test_ndim_array.py,sha256=KH-9LAME3ldVIu5n7Vd_Xr36dN4frCdiF9qZdBWETu0,2232 +sympy/tensor/array/tests/test_ndim_array_conversions.py,sha256=CUGDCbCcslACy3Ngq-zoig9JnO4yHTw3IPcKy0FnRpw,648 +sympy/tensor/functions.py,sha256=FNZ2M0HGVaASJ_1AkPu1vF5NHNLmBXKLsj0Q5wKMX90,4168 +sympy/tensor/index_methods.py,sha256=dcX9kNKLHi_XXkFHBPS-fcM-PaeYKkX80jmzxC0siiQ,15434 +sympy/tensor/indexed.py,sha256=3qiQehMvXSDbr_RDXg6l_aEu6dHZXEfXrMoXCJj7Cqo,24515 +sympy/tensor/tensor.py,sha256=EQ9qJjtvij4gG101jB1eOuTtaSmvm1Da3IHVCBn1Flo,181094 +sympy/tensor/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/tensor/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/tensor/tests/__pycache__/test_functions.cpython-311.pyc,, +sympy/tensor/tests/__pycache__/test_index_methods.cpython-311.pyc,, +sympy/tensor/tests/__pycache__/test_indexed.cpython-311.pyc,, +sympy/tensor/tests/__pycache__/test_printing.cpython-311.pyc,, +sympy/tensor/tests/__pycache__/test_tensor.cpython-311.pyc,, +sympy/tensor/tests/__pycache__/test_tensor_element.cpython-311.pyc,, +sympy/tensor/tests/__pycache__/test_tensor_operators.cpython-311.pyc,, +sympy/tensor/tests/test_functions.py,sha256=rBBHjJIUA2oR83UgEJ_GIASDWfTZXDzOllmcO90XYDU,1552 +sympy/tensor/tests/test_index_methods.py,sha256=Pu951z4yYYMOXBKcNteH63hTAxmNX8702nSQH_pciFE,7112 +sympy/tensor/tests/test_indexed.py,sha256=EB2-t0gYkaTOvrCkhyW2cEL_MVi4xBUZxbf35NA-puI,16376 +sympy/tensor/tests/test_printing.py,sha256=sUx_rChNTWFKPNwVl296QXO-d4-yemDJnkEHFislsmc,424 +sympy/tensor/tests/test_tensor.py,sha256=XQiLkOtyLLFKX_lWEqpCr2lXxQ6LK_hShj0rz7RrWpk,81236 +sympy/tensor/tests/test_tensor_element.py,sha256=1dF96FtqUGaJzethw23vJIj3H5KdxsU1Xyd4DU54EB4,908 +sympy/tensor/tests/test_tensor_operators.py,sha256=n4md5Dv7QoVSMR4cHbkEKSFDxgTTpEyxMiCAqoqyJyk,17966 +sympy/tensor/toperators.py,sha256=fniTUpdYz0OvtNnFgrHINedX86FxVcxfKj9l_l1p9Rw,8840 +sympy/testing/__init__.py,sha256=IN9aHvoksdZywonAYE0cExFnuPQs9z1E4P742aYZNnE,169 +sympy/testing/__pycache__/__init__.cpython-311.pyc,, +sympy/testing/__pycache__/matrices.cpython-311.pyc,, +sympy/testing/__pycache__/pytest.cpython-311.pyc,, +sympy/testing/__pycache__/quality_unicode.cpython-311.pyc,, +sympy/testing/__pycache__/randtest.cpython-311.pyc,, +sympy/testing/__pycache__/runtests.cpython-311.pyc,, +sympy/testing/__pycache__/runtests_pytest.cpython-311.pyc,, +sympy/testing/__pycache__/tmpfiles.cpython-311.pyc,, +sympy/testing/matrices.py,sha256=VWBPdjIUYNHE7fdbYcmQwQTYcIWpOP9tFn9A0rGCBmE,216 +sympy/testing/pytest.py,sha256=xiKL67rD4sKYVZ1Xqci4arPJQzrm3xWfg-m6I7eWNBM,13363 +sympy/testing/quality_unicode.py,sha256=quKFUpEUPftVGNpsj71WpRoeKZUzc8sgLjODRmHPf-w,3482 +sympy/testing/randtest.py,sha256=IKDFAm8b72Z1OkT7vpgnZjaW5LsSU_wf6g35sCkq9I0,562 +sympy/testing/runtests.py,sha256=dD9_0xtvG0wMgQtdvxDi7ePc2CL2ybNRqHKgHjJomzE,89921 +sympy/testing/runtests_pytest.py,sha256=KSqhzaRis7sx2AwctWsP6VjCbDcg6C5UCApp_LUNRuc,17801 +sympy/testing/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/testing/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/testing/tests/__pycache__/diagnose_imports.cpython-311.pyc,, +sympy/testing/tests/__pycache__/test_code_quality.cpython-311.pyc,, +sympy/testing/tests/__pycache__/test_deprecated.cpython-311.pyc,, +sympy/testing/tests/__pycache__/test_module_imports.cpython-311.pyc,, +sympy/testing/tests/__pycache__/test_pytest.cpython-311.pyc,, +sympy/testing/tests/__pycache__/test_runtests_pytest.cpython-311.pyc,, +sympy/testing/tests/diagnose_imports.py,sha256=VQAc0WLHgQgksu6kcQLmEVW4O98v7YgjLMzCLNZ6JfE,9508 +sympy/testing/tests/test_code_quality.py,sha256=Yzb1CAIAGTO29Ay2fsPn0-OqstAoy9FMiAMIed6iVZE,19214 +sympy/testing/tests/test_deprecated.py,sha256=wQZHs4wDNuK4flaKKLsJW6XRMtrVjMv_5rUP3WspgPA,183 +sympy/testing/tests/test_module_imports.py,sha256=5w6F6JW6K7lgpbB4X9Tj0Vw8AcNVlfaSuvbwKXJKD6c,1459 +sympy/testing/tests/test_pytest.py,sha256=iKO10Tvua1Xem6a22IWH4SDrpFfr-bM-rXx039Ua7YA,6778 +sympy/testing/tests/test_runtests_pytest.py,sha256=C4bo-SOx8BsxvT4M_0OPUiwSEXShaMqpYRoedMmbR1g,6083 +sympy/testing/tmpfiles.py,sha256=bF8ktKC9lDhS65gahB9hOewsZ378UkhLgq3QHiqWYXU,1042 +sympy/this.py,sha256=XfOkN5EIM2RuDxSm_q6k_R_WtkIoSy6PXWKp3aAXvoc,550 +sympy/unify/__init__.py,sha256=Upa9h7SSr9W1PXo0WkNESsGsMZ85rcWkeruBtkAi3Fg,293 +sympy/unify/__pycache__/__init__.cpython-311.pyc,, +sympy/unify/__pycache__/core.cpython-311.pyc,, +sympy/unify/__pycache__/rewrite.cpython-311.pyc,, +sympy/unify/__pycache__/usympy.cpython-311.pyc,, +sympy/unify/core.py,sha256=-BCNPPMdfZuhhIWqyn9pYJoO8yFPGDX78Hn2551ABuE,7037 +sympy/unify/rewrite.py,sha256=Emr8Uoum3gxKpMDqFHJIjx3xChArUIN6XIy6NPfCS8I,1798 +sympy/unify/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/unify/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/unify/tests/__pycache__/test_rewrite.cpython-311.pyc,, +sympy/unify/tests/__pycache__/test_sympy.cpython-311.pyc,, +sympy/unify/tests/__pycache__/test_unify.cpython-311.pyc,, +sympy/unify/tests/test_rewrite.py,sha256=BgA8zmdz9Nw-Xbu4-w3UABeWypqLvmy9VzL744EmYtE,2002 +sympy/unify/tests/test_sympy.py,sha256=UCItZJNAx9dG5F7O27pyXUF1-e6aOwkZ-cVdB6SZFZc,5922 +sympy/unify/tests/test_unify.py,sha256=4TlgchV6NWuBekJx9RGlMjx3-UwonzgIYXDytb7sBRU,3029 +sympy/unify/usympy.py,sha256=6Kxx96FXSdqXimLseVK_FkYwy2vqWhNnxMVPMRShvy4,3964 +sympy/utilities/__init__.py,sha256=nbQhzII8dw5zd4hQJ2SUyriK5dOrqf-bbjy10XKQXPw,840 +sympy/utilities/__pycache__/__init__.cpython-311.pyc,, +sympy/utilities/__pycache__/autowrap.cpython-311.pyc,, +sympy/utilities/__pycache__/codegen.cpython-311.pyc,, +sympy/utilities/__pycache__/decorator.cpython-311.pyc,, +sympy/utilities/__pycache__/enumerative.cpython-311.pyc,, +sympy/utilities/__pycache__/exceptions.cpython-311.pyc,, +sympy/utilities/__pycache__/iterables.cpython-311.pyc,, +sympy/utilities/__pycache__/lambdify.cpython-311.pyc,, +sympy/utilities/__pycache__/magic.cpython-311.pyc,, +sympy/utilities/__pycache__/matchpy_connector.cpython-311.pyc,, +sympy/utilities/__pycache__/memoization.cpython-311.pyc,, +sympy/utilities/__pycache__/misc.cpython-311.pyc,, +sympy/utilities/__pycache__/pkgdata.cpython-311.pyc,, +sympy/utilities/__pycache__/pytest.cpython-311.pyc,, +sympy/utilities/__pycache__/randtest.cpython-311.pyc,, +sympy/utilities/__pycache__/runtests.cpython-311.pyc,, +sympy/utilities/__pycache__/source.cpython-311.pyc,, +sympy/utilities/__pycache__/timeutils.cpython-311.pyc,, +sympy/utilities/__pycache__/tmpfiles.cpython-311.pyc,, +sympy/utilities/_compilation/__init__.py,sha256=uYUDPbwrMTbGEMVuago32EN_ix8fsi5M0SvcLOtwMOk,751 +sympy/utilities/_compilation/__pycache__/__init__.cpython-311.pyc,, +sympy/utilities/_compilation/__pycache__/availability.cpython-311.pyc,, +sympy/utilities/_compilation/__pycache__/compilation.cpython-311.pyc,, +sympy/utilities/_compilation/__pycache__/runners.cpython-311.pyc,, +sympy/utilities/_compilation/__pycache__/util.cpython-311.pyc,, +sympy/utilities/_compilation/availability.py,sha256=ybxp3mboH5772JHTWKBN1D-cs6QxATQiaL4zJVV4RE0,2884 +sympy/utilities/_compilation/compilation.py,sha256=qkfDtSgSeRJ-97E5hN1asUqoDXS4nnNgYoHCA8WAmHA,22156 +sympy/utilities/_compilation/runners.py,sha256=KAinYF0m55dRdDFO5pvHBlvTxDY0ldoImqTOwX7VmL4,10237 +sympy/utilities/_compilation/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/utilities/_compilation/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/utilities/_compilation/tests/__pycache__/test_compilation.cpython-311.pyc,, +sympy/utilities/_compilation/tests/test_compilation.py,sha256=DmdxCPlWaqgtNZPWzMVPwqRuqqJcgWBWxXfOEMZ21D0,3189 +sympy/utilities/_compilation/util.py,sha256=otSIKhM5YSYzbUNYycagG2xSj6owlQVrwIVl0rv7Owc,8611 +sympy/utilities/autowrap.py,sha256=l8noWRKmA-B814S92RK3O64YWAkARnMfqY1AQqi2S8g,42561 +sympy/utilities/codegen.py,sha256=1ieFSlxGgd9utMSPzN5NraTfoSSzlUM9V_BnGYMuMc8,81670 +sympy/utilities/decorator.py,sha256=NaZBNCDdMhG4_3esEOLZCbVQcjltvi3lrMIRCqewfR4,11184 +sympy/utilities/enumerative.py,sha256=ah4Td5KhqGdPR7EQHcUDOsYLDH64Sh2z6xm5gDpJSFg,43585 +sympy/utilities/exceptions.py,sha256=RoKY7jDIq6OsZbNSCyneWbVQb1Cw2MtOuioJlCKmBec,10570 +sympy/utilities/iterables.py,sha256=noIMRQmdSPfbg1LorRPHreozS265u4IHUmDkOvMB2po,91103 +sympy/utilities/lambdify.py,sha256=lepieExI19QvZYPrGl_WZaFwta8YZBo6AgxDnwq01ow,57816 +sympy/utilities/magic.py,sha256=ofrwi1-xwMWb4VCQOEIwe4J1QAwxOscigDq26uSn3iY,400 +sympy/utilities/matchpy_connector.py,sha256=7dSDOPbN5Y_XW6bIGVNK3dJ-gVdTB_liJ8O5rIqd28c,11948 +sympy/utilities/mathml/__init__.py,sha256=74VhxNJlvCZTm2Jh3t69N8QZRklTQjqMvqFMLVwCKQk,3388 +sympy/utilities/mathml/__pycache__/__init__.cpython-311.pyc,, +sympy/utilities/mathml/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/utilities/mathml/data/__pycache__/__init__.cpython-311.pyc,, +sympy/utilities/mathml/data/mmlctop.xsl,sha256=fi3CTNyg-mSscOGYBXLJv8veE_ItR_YTFMJ4jmjp6aE,114444 +sympy/utilities/mathml/data/mmltex.xsl,sha256=haX7emZOfD6_nbn5BjK93F-C85mSS8KogAbIBsW1aBA,137304 +sympy/utilities/mathml/data/simple_mmlctop.xsl,sha256=OM-Vge1satH-MAYwWhraeXcorn1KGtuqBK-PDddaOrk,114433 +sympy/utilities/memoization.py,sha256=jD6RjVMZkGpNZYaJ9481vTiqvmwyu1IKDpsF5PYIvf4,1838 +sympy/utilities/misc.py,sha256=C6C3Em9YUjqg4gUhVdu97K0Wt_gRxuhNHS0TW6MlrxQ,16000 +sympy/utilities/pkgdata.py,sha256=BiBonyObCsS6EnLUII_1kDtchDN7Tur0T4GMzoUo06M,935 +sympy/utilities/pytest.py,sha256=ifo5fkOQ94HKXjy_U__mLFSohNRGP2ft8Z4P5gAi4yw,440 +sympy/utilities/randtest.py,sha256=Qlr4MMe0ade6jUH6pIEE0-v4g4qGswJhKpdaQFS_GDw,435 +sympy/utilities/runtests.py,sha256=Js3GGuzxQDZw5Tr5Kge0DCYx9gi3eya4DZxfwib6e3Y,451 +sympy/utilities/source.py,sha256=ShIXRNtplSEfZNi5VDYD3yi6305eRz4TmchEOEvcicw,1127 +sympy/utilities/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/utilities/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/utilities/tests/__pycache__/test_autowrap.cpython-311.pyc,, +sympy/utilities/tests/__pycache__/test_codegen.cpython-311.pyc,, +sympy/utilities/tests/__pycache__/test_codegen_julia.cpython-311.pyc,, +sympy/utilities/tests/__pycache__/test_codegen_octave.cpython-311.pyc,, +sympy/utilities/tests/__pycache__/test_codegen_rust.cpython-311.pyc,, +sympy/utilities/tests/__pycache__/test_decorator.cpython-311.pyc,, +sympy/utilities/tests/__pycache__/test_deprecated.cpython-311.pyc,, +sympy/utilities/tests/__pycache__/test_enumerative.cpython-311.pyc,, +sympy/utilities/tests/__pycache__/test_exceptions.cpython-311.pyc,, +sympy/utilities/tests/__pycache__/test_iterables.cpython-311.pyc,, +sympy/utilities/tests/__pycache__/test_lambdify.cpython-311.pyc,, +sympy/utilities/tests/__pycache__/test_matchpy_connector.cpython-311.pyc,, +sympy/utilities/tests/__pycache__/test_mathml.cpython-311.pyc,, +sympy/utilities/tests/__pycache__/test_misc.cpython-311.pyc,, +sympy/utilities/tests/__pycache__/test_pickling.cpython-311.pyc,, +sympy/utilities/tests/__pycache__/test_source.cpython-311.pyc,, +sympy/utilities/tests/__pycache__/test_timeutils.cpython-311.pyc,, +sympy/utilities/tests/__pycache__/test_wester.cpython-311.pyc,, +sympy/utilities/tests/__pycache__/test_xxe.cpython-311.pyc,, +sympy/utilities/tests/test_autowrap.py,sha256=PA6zaRVrIWyQHq6bGKezqNB6YWTfcnnB9VH2vzaEfaQ,14865 +sympy/utilities/tests/test_codegen.py,sha256=pjfhZTa5LI9gdrakDfLtBBrIxgWjWORR_NBDW9J8Cq8,56130 +sympy/utilities/tests/test_codegen_julia.py,sha256=QYqSluVhmxq_BAagEKBb6eJIseFFAweMjpuLgNPMUWI,18540 +sympy/utilities/tests/test_codegen_octave.py,sha256=GUQIj6YjsMsj8ZLmVngWa9ovoyqfwvikWXdeS7m_c7I,17830 +sympy/utilities/tests/test_codegen_rust.py,sha256=seq9o51dsa09MmcqwKRkGiurbWLs1uAIjaz7xGOKNE4,12326 +sympy/utilities/tests/test_decorator.py,sha256=VYUvzUrVI7I7MK0YZxLLEmEu4pV5dqaB1CLEJ8Ocav4,3705 +sympy/utilities/tests/test_deprecated.py,sha256=LRrZ2UxuXnK6Jwxl8vT0EdLT-q-7jLkTC69U9JjuYYU,489 +sympy/utilities/tests/test_enumerative.py,sha256=nw-1r0MgxbzYkScmxv7HUAIAQycg8pUVOH_slDRLg7E,6097 +sympy/utilities/tests/test_exceptions.py,sha256=OKRa2yuHMtnVcnisu-xcaedi2RKsH9QrgU9exgoOK30,716 +sympy/utilities/tests/test_iterables.py,sha256=xYWyoDtVkYfnIOdA-5yUYZsxo4iTsExaNpVmjMuwpTc,35288 +sympy/utilities/tests/test_lambdify.py,sha256=emTSDwHdGprENO3ugK86k1L6m8Y3IamPj1oUNIKaYrQ,73531 +sympy/utilities/tests/test_matchpy_connector.py,sha256=mBrAev2Hxe4jg_1ny3ZaGIfh2xvWbr6BDRVjB9owJFM,4850 +sympy/utilities/tests/test_mathml.py,sha256=-6z1MRYEH4eYQi2_wt8zmdjwtt5Cn483zqsvD-o_r70,836 +sympy/utilities/tests/test_misc.py,sha256=xZh6ux9o2iZpWvfX_ivp4lD3WgJVeP5k6ipppNGHDsw,4643 +sympy/utilities/tests/test_pickling.py,sha256=ZXGzB_gQ9juws_N_n2vf6QPVojdfP4Mbl7uqdtkPb0E,23600 +sympy/utilities/tests/test_source.py,sha256=ObjrJxZFVhLgXjVmFHUy7bti9UPPgOh5Cptw8lHW9mM,289 +sympy/utilities/tests/test_timeutils.py,sha256=sCRC6BCSho1e9n4clke3QXHx4a3qYLru-bddS_sEmFA,337 +sympy/utilities/tests/test_wester.py,sha256=S79UNsGhEW5z6hDriQB8OY_Vhaz1j0Q7yv31HynyDfk,94866 +sympy/utilities/tests/test_xxe.py,sha256=xk1j0Dd96wsGYKRNDzXTW0hTQejGCfiZcEhYcYiqojg,66 +sympy/utilities/timeutils.py,sha256=PNHyvuEM66UEXLF8ye6PMilcEDK75B9wab3ai8a9cyo,1941 +sympy/utilities/tmpfiles.py,sha256=tQP3g7LTrnXTGqUgGNoIZtjug6O6lq6EYLrKPJcUGEU,450 +sympy/vector/__init__.py,sha256=bgKutVqUPWuvHWBpiEm6XWE6iGfAPJw3pt19NRn2n10,1969 +sympy/vector/__pycache__/__init__.cpython-311.pyc,, +sympy/vector/__pycache__/basisdependent.cpython-311.pyc,, +sympy/vector/__pycache__/coordsysrect.cpython-311.pyc,, +sympy/vector/__pycache__/deloperator.cpython-311.pyc,, +sympy/vector/__pycache__/dyadic.cpython-311.pyc,, +sympy/vector/__pycache__/functions.cpython-311.pyc,, +sympy/vector/__pycache__/implicitregion.cpython-311.pyc,, +sympy/vector/__pycache__/integrals.cpython-311.pyc,, +sympy/vector/__pycache__/kind.cpython-311.pyc,, +sympy/vector/__pycache__/operators.cpython-311.pyc,, +sympy/vector/__pycache__/orienters.cpython-311.pyc,, +sympy/vector/__pycache__/parametricregion.cpython-311.pyc,, +sympy/vector/__pycache__/point.cpython-311.pyc,, +sympy/vector/__pycache__/scalar.cpython-311.pyc,, +sympy/vector/__pycache__/vector.cpython-311.pyc,, +sympy/vector/basisdependent.py,sha256=P7LogUck6NWjEzAOH3dYr4A8M5QCOPaCW75lmiQpBqU,11857 +sympy/vector/coordsysrect.py,sha256=0BKdr8AG8BUJnShIIyyCvkU5OVdjtZsKcBuGgdbvdHw,36833 +sympy/vector/deloperator.py,sha256=4BJNjmI342HkVRmeQkqauqvibKsf2HOuzknQTfQMkpg,3191 +sympy/vector/dyadic.py,sha256=IOyrgONyGDHPtG0RINcMgetAVMSOmYI5a99s-OwXBTA,8571 +sympy/vector/functions.py,sha256=rmEBaV0XpZLkCPcJFojHS9CNHirV55v5sDwNqTPwqGM,15454 +sympy/vector/implicitregion.py,sha256=4-M_-shL4EJ9nfjAZ_xg00IXZVQghcCZ3bv8QGbkopI,16158 +sympy/vector/integrals.py,sha256=x8DrvKXPznE05JgnZ7I3IWLWrvFl9SEghGaFmHrBaE4,6837 +sympy/vector/kind.py,sha256=U6qxciZY7-ACzBSpGM0iSTI5tqflpPBvm4_J9ujyxyE,1812 +sympy/vector/operators.py,sha256=dNC7mNXZb3KxX5GyjFNDTLTkzSnYjOohZtBXuQ1keZE,9579 +sympy/vector/orienters.py,sha256=EtWNWfOvAuy_wipam9SA7_muKSrsP-43UPRCCz56sb0,11798 +sympy/vector/parametricregion.py,sha256=gAarunY8q3D8WVvryZEbJQ4g-nlXo4FnO-CbyEpVUXc,5932 +sympy/vector/point.py,sha256=-JFOibsQclOKC2m2qZRiGGB4oK9ieVZRd2ll5g4k3bE,4488 +sympy/vector/scalar.py,sha256=WAb25BC0TEOZ-_bEtL1gdySuM1adJzK47kJYTrC4Y4w,2024 +sympy/vector/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sympy/vector/tests/__pycache__/__init__.cpython-311.pyc,, +sympy/vector/tests/__pycache__/test_coordsysrect.cpython-311.pyc,, +sympy/vector/tests/__pycache__/test_dyadic.cpython-311.pyc,, +sympy/vector/tests/__pycache__/test_field_functions.cpython-311.pyc,, +sympy/vector/tests/__pycache__/test_functions.cpython-311.pyc,, +sympy/vector/tests/__pycache__/test_implicitregion.cpython-311.pyc,, +sympy/vector/tests/__pycache__/test_integrals.cpython-311.pyc,, +sympy/vector/tests/__pycache__/test_operators.cpython-311.pyc,, +sympy/vector/tests/__pycache__/test_parametricregion.cpython-311.pyc,, +sympy/vector/tests/__pycache__/test_printing.cpython-311.pyc,, +sympy/vector/tests/__pycache__/test_vector.cpython-311.pyc,, +sympy/vector/tests/test_coordsysrect.py,sha256=q9n9OIG_CpD4KQN20dzwRZIXoMv7VSgp8fHmVnkZfr0,19595 +sympy/vector/tests/test_dyadic.py,sha256=f1R-BL_63VBbc0XgEX_LYzV_3OupYd4hp5RzRk6dAbI,4949 +sympy/vector/tests/test_field_functions.py,sha256=v9l8Ex8K2MsPGxqAPhpEgu6WAo6wS6qvdWLKQMxgE4A,14094 +sympy/vector/tests/test_functions.py,sha256=Bs2sekdDJyw_wrUpG7vZQGH0y0S4C4AbxGSpeU_8C2s,8050 +sympy/vector/tests/test_implicitregion.py,sha256=wVilD5H-MhHiW58QT6P5U7uT79JdKHm9D7JgZoi6BE4,4028 +sympy/vector/tests/test_integrals.py,sha256=iDq3Wpr8I8EXAGkXqMpA-P9i1zEnV7lU8WR6xBOhEK0,5087 +sympy/vector/tests/test_operators.py,sha256=KexUWvc_Nwp2HWrEbhxiO7MeaFxYlckrp__Tkwg-wmU,1613 +sympy/vector/tests/test_parametricregion.py,sha256=OfKapF9A_g9X6JxgYc0UfxIhwXzRERzaj-EijQCJONw,4009 +sympy/vector/tests/test_printing.py,sha256=3BeW55iQ4qXdfDTFqptE2ufJPJIBOzdfIYVx84n_EwA,7708 +sympy/vector/tests/test_vector.py,sha256=rJpMlUWiezrY6drHUNdJc8_FDYMwGCG4lBy8zS4XSZk,10238 +sympy/vector/vector.py,sha256=aOSfyfRvkatzWHWuulY6n0_iqElI5mntAUjNPptpBiM,20322 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/WHEEL b/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..b270261c9bb44553b2f648368d1bf5cb7baf8416 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: wheelfile 0.0.8 +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/entry_points.txt b/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/entry_points.txt new file mode 100644 index 0000000000000000000000000000000000000000..42a12f960335556fcee728e5754c346447d4e89c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +isympy = isympy:main diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/top_level.txt b/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..0aa85c253508b68bb075f556be3c3f76dc4467ad --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy-1.14.0+computecanada.dist-info/top_level.txt @@ -0,0 +1,2 @@ +isympy +sympy diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_mathematica.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_mathematica.py new file mode 100644 index 0000000000000000000000000000000000000000..aaf6b537677442ae59a4f1bbd2b5774d6646f4e2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_mathematica.py @@ -0,0 +1,287 @@ +from sympy.core import (S, pi, oo, symbols, Function, Rational, Integer, Tuple, + Derivative, Eq, Ne, Le, Lt, Gt, Ge) +from sympy.integrals import Integral +from sympy.concrete import Sum +from sympy.functions import (exp, sin, cos, fresnelc, fresnels, conjugate, Max, + Min, gamma, polygamma, loggamma, erf, erfi, erfc, + erf2, expint, erfinv, erfcinv, Ei, Si, Ci, li, + Shi, Chi, uppergamma, beta, subfactorial, erf2inv, + factorial, factorial2, catalan, RisingFactorial, + FallingFactorial, harmonic, atan2, sec, acsc, + hermite, laguerre, assoc_laguerre, jacobi, + gegenbauer, chebyshevt, chebyshevu, legendre, + assoc_legendre, Li, LambertW) + +from sympy.printing.mathematica import mathematica_code as mcode + +x, y, z, w = symbols('x,y,z,w') +f = Function('f') + + +def test_Integer(): + assert mcode(Integer(67)) == "67" + assert mcode(Integer(-1)) == "-1" + + +def test_Rational(): + assert mcode(Rational(3, 7)) == "3/7" + assert mcode(Rational(18, 9)) == "2" + assert mcode(Rational(3, -7)) == "-3/7" + assert mcode(Rational(-3, -7)) == "3/7" + assert mcode(x + Rational(3, 7)) == "x + 3/7" + assert mcode(Rational(3, 7)*x) == "(3/7)*x" + + +def test_Relational(): + assert mcode(Eq(x, y)) == "x == y" + assert mcode(Ne(x, y)) == "x != y" + assert mcode(Le(x, y)) == "x <= y" + assert mcode(Lt(x, y)) == "x < y" + assert mcode(Gt(x, y)) == "x > y" + assert mcode(Ge(x, y)) == "x >= y" + + +def test_Function(): + assert mcode(f(x, y, z)) == "f[x, y, z]" + assert mcode(sin(x) ** cos(x)) == "Sin[x]^Cos[x]" + assert mcode(sec(x) * acsc(x)) == "ArcCsc[x]*Sec[x]" + assert mcode(atan2(y, x)) == "ArcTan[x, y]" + assert mcode(conjugate(x)) == "Conjugate[x]" + assert mcode(Max(x, y, z)*Min(y, z)) == "Max[x, y, z]*Min[y, z]" + assert mcode(fresnelc(x)) == "FresnelC[x]" + assert mcode(fresnels(x)) == "FresnelS[x]" + assert mcode(gamma(x)) == "Gamma[x]" + assert mcode(uppergamma(x, y)) == "Gamma[x, y]" + assert mcode(polygamma(x, y)) == "PolyGamma[x, y]" + assert mcode(loggamma(x)) == "LogGamma[x]" + assert mcode(erf(x)) == "Erf[x]" + assert mcode(erfc(x)) == "Erfc[x]" + assert mcode(erfi(x)) == "Erfi[x]" + assert mcode(erf2(x, y)) == "Erf[x, y]" + assert mcode(expint(x, y)) == "ExpIntegralE[x, y]" + assert mcode(erfcinv(x)) == "InverseErfc[x]" + assert mcode(erfinv(x)) == "InverseErf[x]" + assert mcode(erf2inv(x, y)) == "InverseErf[x, y]" + assert mcode(Ei(x)) == "ExpIntegralEi[x]" + assert mcode(Ci(x)) == "CosIntegral[x]" + assert mcode(li(x)) == "LogIntegral[x]" + assert mcode(Si(x)) == "SinIntegral[x]" + assert mcode(Shi(x)) == "SinhIntegral[x]" + assert mcode(Chi(x)) == "CoshIntegral[x]" + assert mcode(beta(x, y)) == "Beta[x, y]" + assert mcode(factorial(x)) == "Factorial[x]" + assert mcode(factorial2(x)) == "Factorial2[x]" + assert mcode(subfactorial(x)) == "Subfactorial[x]" + assert mcode(FallingFactorial(x, y)) == "FactorialPower[x, y]" + assert mcode(RisingFactorial(x, y)) == "Pochhammer[x, y]" + assert mcode(catalan(x)) == "CatalanNumber[x]" + assert mcode(harmonic(x)) == "HarmonicNumber[x]" + assert mcode(harmonic(x, y)) == "HarmonicNumber[x, y]" + assert mcode(Li(x)) == "LogIntegral[x] - LogIntegral[2]" + assert mcode(LambertW(x)) == "ProductLog[x]" + assert mcode(LambertW(x, -1)) == "ProductLog[-1, x]" + assert mcode(LambertW(x, y)) == "ProductLog[y, x]" + + +def test_special_polynomials(): + assert mcode(hermite(x, y)) == "HermiteH[x, y]" + assert mcode(laguerre(x, y)) == "LaguerreL[x, y]" + assert mcode(assoc_laguerre(x, y, z)) == "LaguerreL[x, y, z]" + assert mcode(jacobi(x, y, z, w)) == "JacobiP[x, y, z, w]" + assert mcode(gegenbauer(x, y, z)) == "GegenbauerC[x, y, z]" + assert mcode(chebyshevt(x, y)) == "ChebyshevT[x, y]" + assert mcode(chebyshevu(x, y)) == "ChebyshevU[x, y]" + assert mcode(legendre(x, y)) == "LegendreP[x, y]" + assert mcode(assoc_legendre(x, y, z)) == "LegendreP[x, y, z]" + + +def test_Pow(): + assert mcode(x**3) == "x^3" + assert mcode(x**(y**3)) == "x^(y^3)" + assert mcode(1/(f(x)*3.5)**(x - y**x)/(x**2 + y)) == \ + "(3.5*f[x])^(-x + y^x)/(x^2 + y)" + assert mcode(x**-1.0) == 'x^(-1.0)' + assert mcode(x**Rational(2, 3)) == 'x^(2/3)' + + +def test_Mul(): + A, B, C, D = symbols('A B C D', commutative=False) + assert mcode(x*y*z) == "x*y*z" + assert mcode(x*y*A) == "x*y*A" + assert mcode(x*y*A*B) == "x*y*A**B" + assert mcode(x*y*A*B*C) == "x*y*A**B**C" + assert mcode(x*A*B*(C + D)*A*y) == "x*y*A**B**(C + D)**A" + + +def test_constants(): + assert mcode(S.Zero) == "0" + assert mcode(S.One) == "1" + assert mcode(S.NegativeOne) == "-1" + assert mcode(S.Half) == "1/2" + assert mcode(S.ImaginaryUnit) == "I" + + assert mcode(oo) == "Infinity" + assert mcode(S.NegativeInfinity) == "-Infinity" + assert mcode(S.ComplexInfinity) == "ComplexInfinity" + assert mcode(S.NaN) == "Indeterminate" + + assert mcode(S.Exp1) == "E" + assert mcode(pi) == "Pi" + assert mcode(S.GoldenRatio) == "GoldenRatio" + assert mcode(S.TribonacciConstant) == \ + "(1/3 + (1/3)*(19 - 3*33^(1/2))^(1/3) + " \ + "(1/3)*(3*33^(1/2) + 19)^(1/3))" + assert mcode(2*S.TribonacciConstant) == \ + "2*(1/3 + (1/3)*(19 - 3*33^(1/2))^(1/3) + " \ + "(1/3)*(3*33^(1/2) + 19)^(1/3))" + assert mcode(S.EulerGamma) == "EulerGamma" + assert mcode(S.Catalan) == "Catalan" + + +def test_containers(): + assert mcode([1, 2, 3, [4, 5, [6, 7]], 8, [9, 10], 11]) == \ + "{1, 2, 3, {4, 5, {6, 7}}, 8, {9, 10}, 11}" + assert mcode((1, 2, (3, 4))) == "{1, 2, {3, 4}}" + assert mcode([1]) == "{1}" + assert mcode((1,)) == "{1}" + assert mcode(Tuple(*[1, 2, 3])) == "{1, 2, 3}" + + +def test_matrices(): + from sympy.matrices import MutableDenseMatrix, MutableSparseMatrix, \ + ImmutableDenseMatrix, ImmutableSparseMatrix + A = MutableDenseMatrix( + [[1, -1, 0, 0], + [0, 1, -1, 0], + [0, 0, 1, -1], + [0, 0, 0, 1]] + ) + B = MutableSparseMatrix(A) + C = ImmutableDenseMatrix(A) + D = ImmutableSparseMatrix(A) + + assert mcode(C) == mcode(A) == \ + "{{1, -1, 0, 0}, " \ + "{0, 1, -1, 0}, " \ + "{0, 0, 1, -1}, " \ + "{0, 0, 0, 1}}" + + assert mcode(D) == mcode(B) == \ + "SparseArray[{" \ + "{1, 1} -> 1, {1, 2} -> -1, {2, 2} -> 1, {2, 3} -> -1, " \ + "{3, 3} -> 1, {3, 4} -> -1, {4, 4} -> 1" \ + "}, {4, 4}]" + + # Trivial cases of matrices + assert mcode(MutableDenseMatrix(0, 0, [])) == '{}' + assert mcode(MutableSparseMatrix(0, 0, [])) == 'SparseArray[{}, {0, 0}]' + assert mcode(MutableDenseMatrix(0, 3, [])) == '{}' + assert mcode(MutableSparseMatrix(0, 3, [])) == 'SparseArray[{}, {0, 3}]' + assert mcode(MutableDenseMatrix(3, 0, [])) == '{{}, {}, {}}' + assert mcode(MutableSparseMatrix(3, 0, [])) == 'SparseArray[{}, {3, 0}]' + +def test_NDArray(): + from sympy.tensor.array import ( + MutableDenseNDimArray, ImmutableDenseNDimArray, + MutableSparseNDimArray, ImmutableSparseNDimArray) + + example = MutableDenseNDimArray( + [[[1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12]], + [[13, 14, 15, 16], + [17, 18, 19, 20], + [21, 22, 23, 24]]] + ) + + assert mcode(example) == \ + "{{{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}}, " \ + "{{13, 14, 15, 16}, {17, 18, 19, 20}, {21, 22, 23, 24}}}" + + example = ImmutableDenseNDimArray(example) + + assert mcode(example) == \ + "{{{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}}, " \ + "{{13, 14, 15, 16}, {17, 18, 19, 20}, {21, 22, 23, 24}}}" + + example = MutableSparseNDimArray(example) + + assert mcode(example) == \ + "SparseArray[{" \ + "{1, 1, 1} -> 1, {1, 1, 2} -> 2, {1, 1, 3} -> 3, " \ + "{1, 1, 4} -> 4, {1, 2, 1} -> 5, {1, 2, 2} -> 6, " \ + "{1, 2, 3} -> 7, {1, 2, 4} -> 8, {1, 3, 1} -> 9, " \ + "{1, 3, 2} -> 10, {1, 3, 3} -> 11, {1, 3, 4} -> 12, " \ + "{2, 1, 1} -> 13, {2, 1, 2} -> 14, {2, 1, 3} -> 15, " \ + "{2, 1, 4} -> 16, {2, 2, 1} -> 17, {2, 2, 2} -> 18, " \ + "{2, 2, 3} -> 19, {2, 2, 4} -> 20, {2, 3, 1} -> 21, " \ + "{2, 3, 2} -> 22, {2, 3, 3} -> 23, {2, 3, 4} -> 24" \ + "}, {2, 3, 4}]" + + example = ImmutableSparseNDimArray(example) + + assert mcode(example) == \ + "SparseArray[{" \ + "{1, 1, 1} -> 1, {1, 1, 2} -> 2, {1, 1, 3} -> 3, " \ + "{1, 1, 4} -> 4, {1, 2, 1} -> 5, {1, 2, 2} -> 6, " \ + "{1, 2, 3} -> 7, {1, 2, 4} -> 8, {1, 3, 1} -> 9, " \ + "{1, 3, 2} -> 10, {1, 3, 3} -> 11, {1, 3, 4} -> 12, " \ + "{2, 1, 1} -> 13, {2, 1, 2} -> 14, {2, 1, 3} -> 15, " \ + "{2, 1, 4} -> 16, {2, 2, 1} -> 17, {2, 2, 2} -> 18, " \ + "{2, 2, 3} -> 19, {2, 2, 4} -> 20, {2, 3, 1} -> 21, " \ + "{2, 3, 2} -> 22, {2, 3, 3} -> 23, {2, 3, 4} -> 24" \ + "}, {2, 3, 4}]" + + +def test_Integral(): + assert mcode(Integral(sin(sin(x)), x)) == "Hold[Integrate[Sin[Sin[x]], x]]" + assert mcode(Integral(exp(-x**2 - y**2), + (x, -oo, oo), + (y, -oo, oo))) == \ + "Hold[Integrate[Exp[-x^2 - y^2], {x, -Infinity, Infinity}, " \ + "{y, -Infinity, Infinity}]]" + + +def test_Derivative(): + assert mcode(Derivative(sin(x), x)) == "Hold[D[Sin[x], x]]" + assert mcode(Derivative(x, x)) == "Hold[D[x, x]]" + assert mcode(Derivative(sin(x)*y**4, x, 2)) == "Hold[D[y^4*Sin[x], {x, 2}]]" + assert mcode(Derivative(sin(x)*y**4, x, y, x)) == "Hold[D[y^4*Sin[x], x, y, x]]" + assert mcode(Derivative(sin(x)*y**4, x, y, 3, x)) == "Hold[D[y^4*Sin[x], x, {y, 3}, x]]" + + +def test_Sum(): + assert mcode(Sum(sin(x), (x, 0, 10))) == "Hold[Sum[Sin[x], {x, 0, 10}]]" + assert mcode(Sum(exp(-x**2 - y**2), + (x, -oo, oo), + (y, -oo, oo))) == \ + "Hold[Sum[Exp[-x^2 - y^2], {x, -Infinity, Infinity}, " \ + "{y, -Infinity, Infinity}]]" + + +def test_comment(): + from sympy.printing.mathematica import MCodePrinter + assert MCodePrinter()._get_comment("Hello World") == \ + "(* Hello World *)" + + +def test_userfuncs(): + # Dictionary mutation test + some_function = symbols("some_function", cls=Function) + my_user_functions = {"some_function": "SomeFunction"} + assert mcode( + some_function(z), + user_functions=my_user_functions) == \ + 'SomeFunction[z]' + assert mcode( + some_function(z), + user_functions=my_user_functions) == \ + 'SomeFunction[z]' + + # List argument test + my_user_functions = \ + {"some_function": [(lambda x: True, "SomeOtherFunction")]} + assert mcode( + some_function(z), + user_functions=my_user_functions) == \ + 'SomeOtherFunction[z]' diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_mathml.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_mathml.py new file mode 100644 index 0000000000000000000000000000000000000000..4e7c2253c98fb1a4e99375774ad158df9b80b439 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_mathml.py @@ -0,0 +1,2048 @@ +from sympy.calculus.accumulationbounds import AccumBounds +from sympy.concrete.summations import Sum +from sympy.core.basic import Basic +from sympy.core.containers import Tuple +from sympy.core.function import Derivative, Lambda, diff, Function +from sympy.core.numbers import (zoo, Float, Integer, I, oo, pi, E, + Rational) +from sympy.core.relational import Lt, Ge, Ne, Eq +from sympy.core.singleton import S +from sympy.core.symbol import symbols, Symbol +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import (factorial2, + binomial, factorial) +from sympy.functions.combinatorial.numbers import (lucas, bell, + catalan, euler, tribonacci, fibonacci, bernoulli, primenu, primeomega, + totient, reduced_totient) +from sympy.functions.elementary.complexes import re, im, conjugate, Abs +from sympy.functions.elementary.exponential import exp, LambertW, log +from sympy.functions.elementary.hyperbolic import (tanh, acoth, atanh, + coth, asinh, acsch, asech, acosh, csch, sinh, cosh, sech) +from sympy.functions.elementary.integers import ceiling, floor +from sympy.functions.elementary.miscellaneous import Max, Min +from sympy.functions.elementary.trigonometric import (csc, sec, tan, + atan, sin, asec, cot, cos, acot, acsc, asin, acos) +from sympy.functions.special.delta_functions import Heaviside +from sympy.functions.special.elliptic_integrals import (elliptic_pi, + elliptic_f, elliptic_k, elliptic_e) +from sympy.functions.special.error_functions import (fresnelc, + fresnels, Ei, expint) +from sympy.functions.special.gamma_functions import (gamma, uppergamma, + lowergamma) +from sympy.functions.special.mathieu_functions import (mathieusprime, + mathieus, mathieucprime, mathieuc) +from sympy.functions.special.polynomials import (jacobi, chebyshevu, + chebyshevt, hermite, assoc_legendre, gegenbauer, assoc_laguerre, + legendre, laguerre) +from sympy.functions.special.singularity_functions import SingularityFunction +from sympy.functions.special.zeta_functions import (polylog, stieltjes, + lerchphi, dirichlet_eta, zeta) +from sympy.integrals.integrals import Integral +from sympy.logic.boolalg import (Xor, Or, false, true, And, Equivalent, + Implies, Not) +from sympy.matrices.dense import Matrix +from sympy.matrices.expressions.determinant import Determinant +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.physics.quantum import (ComplexSpace, FockSpace, hbar, + HilbertSpace, Dagger) +from sympy.printing.mathml import (MathMLPresentationPrinter, + MathMLPrinter, MathMLContentPrinter, mathml) +from sympy.series.limits import Limit +from sympy.sets.contains import Contains +from sympy.sets.fancysets import Range +from sympy.sets.sets import (Interval, Union, SymmetricDifference, + Complement, FiniteSet, Intersection, ProductSet) +from sympy.stats.rv import RandomSymbol +from sympy.tensor.indexed import IndexedBase +from sympy.vector import (Divergence, CoordSys3D, Cross, Curl, Dot, + Laplacian, Gradient) +from sympy.testing.pytest import raises, XFAIL + +x, y, z, a, b, c, d, e, n = symbols('x:z a:e n') +mp = MathMLContentPrinter() +mpp = MathMLPresentationPrinter() + + +def test_mathml_printer(): + m = MathMLPrinter() + assert m.doprint(1+x) == mp.doprint(1+x) + + +def test_content_printmethod(): + assert mp.doprint(1 + x) == 'x1' + + +def test_content_mathml_core(): + mml_1 = mp._print(1 + x) + assert mml_1.nodeName == 'apply' + nodes = mml_1.childNodes + assert len(nodes) == 3 + assert nodes[0].nodeName == 'plus' + assert nodes[0].hasChildNodes() is False + assert nodes[0].nodeValue is None + assert nodes[1].nodeName in ['cn', 'ci'] + if nodes[1].nodeName == 'cn': + assert nodes[1].childNodes[0].nodeValue == '1' + assert nodes[2].childNodes[0].nodeValue == 'x' + else: + assert nodes[1].childNodes[0].nodeValue == 'x' + assert nodes[2].childNodes[0].nodeValue == '1' + + mml_2 = mp._print(x**2) + assert mml_2.nodeName == 'apply' + nodes = mml_2.childNodes + assert nodes[1].childNodes[0].nodeValue == 'x' + assert nodes[2].childNodes[0].nodeValue == '2' + + mml_3 = mp._print(2*x) + assert mml_3.nodeName == 'apply' + nodes = mml_3.childNodes + assert nodes[0].nodeName == 'times' + assert nodes[1].childNodes[0].nodeValue == '2' + assert nodes[2].childNodes[0].nodeValue == 'x' + + mml = mp._print(Float(1.0, 2)*x) + assert mml.nodeName == 'apply' + nodes = mml.childNodes + assert nodes[0].nodeName == 'times' + assert nodes[1].childNodes[0].nodeValue == '1.0' + assert nodes[2].childNodes[0].nodeValue == 'x' + + +def test_content_mathml_functions(): + mml_1 = mp._print(sin(x)) + assert mml_1.nodeName == 'apply' + assert mml_1.childNodes[0].nodeName == 'sin' + assert mml_1.childNodes[1].nodeName == 'ci' + + mml_2 = mp._print(diff(sin(x), x, evaluate=False)) + assert mml_2.nodeName == 'apply' + assert mml_2.childNodes[0].nodeName == 'diff' + assert mml_2.childNodes[1].nodeName == 'bvar' + assert mml_2.childNodes[1].childNodes[ + 0].nodeName == 'ci' # below bvar there's x/ci> + + mml_3 = mp._print(diff(cos(x*y), x, evaluate=False)) + assert mml_3.nodeName == 'apply' + assert mml_3.childNodes[0].nodeName == 'partialdiff' + assert mml_3.childNodes[1].nodeName == 'bvar' + assert mml_3.childNodes[1].childNodes[ + 0].nodeName == 'ci' # below bvar there's x/ci> + + mml_4 = mp._print(Lambda((x, y), x * y)) + assert mml_4.nodeName == 'lambda' + assert mml_4.childNodes[0].nodeName == 'bvar' + assert mml_4.childNodes[0].childNodes[ + 0].nodeName == 'ci' # below bvar there's x/ci> + assert mml_4.childNodes[1].nodeName == 'bvar' + assert mml_4.childNodes[1].childNodes[ + 0].nodeName == 'ci' # below bvar there's y/ci> + assert mml_4.childNodes[2].nodeName == 'apply' + + +def test_content_mathml_limits(): + # XXX No unevaluated limits + lim_fun = sin(x)/x + mml_1 = mp._print(Limit(lim_fun, x, 0)) + assert mml_1.childNodes[0].nodeName == 'limit' + assert mml_1.childNodes[1].nodeName == 'bvar' + assert mml_1.childNodes[2].nodeName == 'lowlimit' + assert mml_1.childNodes[3].toxml() == mp._print(lim_fun).toxml() + + +def test_content_mathml_integrals(): + integrand = x + mml_1 = mp._print(Integral(integrand, (x, 0, 1))) + assert mml_1.childNodes[0].nodeName == 'int' + assert mml_1.childNodes[1].nodeName == 'bvar' + assert mml_1.childNodes[2].nodeName == 'lowlimit' + assert mml_1.childNodes[3].nodeName == 'uplimit' + assert mml_1.childNodes[4].toxml() == mp._print(integrand).toxml() + + +def test_content_mathml_matrices(): + A = Matrix([1, 2, 3]) + B = Matrix([[0, 5, 4], [2, 3, 1], [9, 7, 9]]) + mll_1 = mp._print(A) + assert mll_1.childNodes[0].nodeName == 'matrixrow' + assert mll_1.childNodes[0].childNodes[0].nodeName == 'cn' + assert mll_1.childNodes[0].childNodes[0].childNodes[0].nodeValue == '1' + assert mll_1.childNodes[1].nodeName == 'matrixrow' + assert mll_1.childNodes[1].childNodes[0].nodeName == 'cn' + assert mll_1.childNodes[1].childNodes[0].childNodes[0].nodeValue == '2' + assert mll_1.childNodes[2].nodeName == 'matrixrow' + assert mll_1.childNodes[2].childNodes[0].nodeName == 'cn' + assert mll_1.childNodes[2].childNodes[0].childNodes[0].nodeValue == '3' + mll_2 = mp._print(B) + assert mll_2.childNodes[0].nodeName == 'matrixrow' + assert mll_2.childNodes[0].childNodes[0].nodeName == 'cn' + assert mll_2.childNodes[0].childNodes[0].childNodes[0].nodeValue == '0' + assert mll_2.childNodes[0].childNodes[1].nodeName == 'cn' + assert mll_2.childNodes[0].childNodes[1].childNodes[0].nodeValue == '5' + assert mll_2.childNodes[0].childNodes[2].nodeName == 'cn' + assert mll_2.childNodes[0].childNodes[2].childNodes[0].nodeValue == '4' + assert mll_2.childNodes[1].nodeName == 'matrixrow' + assert mll_2.childNodes[1].childNodes[0].nodeName == 'cn' + assert mll_2.childNodes[1].childNodes[0].childNodes[0].nodeValue == '2' + assert mll_2.childNodes[1].childNodes[1].nodeName == 'cn' + assert mll_2.childNodes[1].childNodes[1].childNodes[0].nodeValue == '3' + assert mll_2.childNodes[1].childNodes[2].nodeName == 'cn' + assert mll_2.childNodes[1].childNodes[2].childNodes[0].nodeValue == '1' + assert mll_2.childNodes[2].nodeName == 'matrixrow' + assert mll_2.childNodes[2].childNodes[0].nodeName == 'cn' + assert mll_2.childNodes[2].childNodes[0].childNodes[0].nodeValue == '9' + assert mll_2.childNodes[2].childNodes[1].nodeName == 'cn' + assert mll_2.childNodes[2].childNodes[1].childNodes[0].nodeValue == '7' + assert mll_2.childNodes[2].childNodes[2].nodeName == 'cn' + assert mll_2.childNodes[2].childNodes[2].childNodes[0].nodeValue == '9' + + +def test_content_mathml_sums(): + summand = x + mml_1 = mp._print(Sum(summand, (x, 1, 10))) + assert mml_1.childNodes[0].nodeName == 'sum' + assert mml_1.childNodes[1].nodeName == 'bvar' + assert mml_1.childNodes[2].nodeName == 'lowlimit' + assert mml_1.childNodes[3].nodeName == 'uplimit' + assert mml_1.childNodes[4].toxml() == mp._print(summand).toxml() + + +def test_content_mathml_tuples(): + mml_1 = mp._print([2]) + assert mml_1.nodeName == 'list' + assert mml_1.childNodes[0].nodeName == 'cn' + assert len(mml_1.childNodes) == 1 + + mml_2 = mp._print([2, Integer(1)]) + assert mml_2.nodeName == 'list' + assert mml_2.childNodes[0].nodeName == 'cn' + assert mml_2.childNodes[1].nodeName == 'cn' + assert len(mml_2.childNodes) == 2 + + +def test_content_mathml_add(): + mml = mp._print(x**5 - x**4 + x) + assert mml.childNodes[0].nodeName == 'plus' + assert mml.childNodes[1].childNodes[0].nodeName == 'minus' + assert mml.childNodes[1].childNodes[1].nodeName == 'apply' + + +def test_content_mathml_Rational(): + mml_1 = mp._print(Rational(1, 1)) + """should just return a number""" + assert mml_1.nodeName == 'cn' + + mml_2 = mp._print(Rational(2, 5)) + assert mml_2.childNodes[0].nodeName == 'divide' + + +def test_content_mathml_constants(): + mml = mp._print(I) + assert mml.nodeName == 'imaginaryi' + + mml = mp._print(E) + assert mml.nodeName == 'exponentiale' + + mml = mp._print(oo) + assert mml.nodeName == 'infinity' + + mml = mp._print(pi) + assert mml.nodeName == 'pi' + + assert mathml(hbar) == '' + assert mathml(S.TribonacciConstant) == '' + assert mathml(S.GoldenRatio) == 'φ' + mml = mathml(S.EulerGamma) + assert mml == '' + + mml = mathml(S.EmptySet) + assert mml == '' + + mml = mathml(S.true) + assert mml == '' + + mml = mathml(S.false) + assert mml == '' + + mml = mathml(S.NaN) + assert mml == '' + + +def test_content_mathml_trig(): + mml = mp._print(sin(x)) + assert mml.childNodes[0].nodeName == 'sin' + + mml = mp._print(cos(x)) + assert mml.childNodes[0].nodeName == 'cos' + + mml = mp._print(tan(x)) + assert mml.childNodes[0].nodeName == 'tan' + + mml = mp._print(cot(x)) + assert mml.childNodes[0].nodeName == 'cot' + + mml = mp._print(csc(x)) + assert mml.childNodes[0].nodeName == 'csc' + + mml = mp._print(sec(x)) + assert mml.childNodes[0].nodeName == 'sec' + + mml = mp._print(asin(x)) + assert mml.childNodes[0].nodeName == 'arcsin' + + mml = mp._print(acos(x)) + assert mml.childNodes[0].nodeName == 'arccos' + + mml = mp._print(atan(x)) + assert mml.childNodes[0].nodeName == 'arctan' + + mml = mp._print(acot(x)) + assert mml.childNodes[0].nodeName == 'arccot' + + mml = mp._print(acsc(x)) + assert mml.childNodes[0].nodeName == 'arccsc' + + mml = mp._print(asec(x)) + assert mml.childNodes[0].nodeName == 'arcsec' + + mml = mp._print(sinh(x)) + assert mml.childNodes[0].nodeName == 'sinh' + + mml = mp._print(cosh(x)) + assert mml.childNodes[0].nodeName == 'cosh' + + mml = mp._print(tanh(x)) + assert mml.childNodes[0].nodeName == 'tanh' + + mml = mp._print(coth(x)) + assert mml.childNodes[0].nodeName == 'coth' + + mml = mp._print(csch(x)) + assert mml.childNodes[0].nodeName == 'csch' + + mml = mp._print(sech(x)) + assert mml.childNodes[0].nodeName == 'sech' + + mml = mp._print(asinh(x)) + assert mml.childNodes[0].nodeName == 'arcsinh' + + mml = mp._print(atanh(x)) + assert mml.childNodes[0].nodeName == 'arctanh' + + mml = mp._print(acosh(x)) + assert mml.childNodes[0].nodeName == 'arccosh' + + mml = mp._print(acoth(x)) + assert mml.childNodes[0].nodeName == 'arccoth' + + mml = mp._print(acsch(x)) + assert mml.childNodes[0].nodeName == 'arccsch' + + mml = mp._print(asech(x)) + assert mml.childNodes[0].nodeName == 'arcsech' + + +def test_content_mathml_relational(): + mml_1 = mp._print(Eq(x, 1)) + assert mml_1.nodeName == 'apply' + assert mml_1.childNodes[0].nodeName == 'eq' + assert mml_1.childNodes[1].nodeName == 'ci' + assert mml_1.childNodes[1].childNodes[0].nodeValue == 'x' + assert mml_1.childNodes[2].nodeName == 'cn' + assert mml_1.childNodes[2].childNodes[0].nodeValue == '1' + + mml_2 = mp._print(Ne(1, x)) + assert mml_2.nodeName == 'apply' + assert mml_2.childNodes[0].nodeName == 'neq' + assert mml_2.childNodes[1].nodeName == 'cn' + assert mml_2.childNodes[1].childNodes[0].nodeValue == '1' + assert mml_2.childNodes[2].nodeName == 'ci' + assert mml_2.childNodes[2].childNodes[0].nodeValue == 'x' + + mml_3 = mp._print(Ge(1, x)) + assert mml_3.nodeName == 'apply' + assert mml_3.childNodes[0].nodeName == 'geq' + assert mml_3.childNodes[1].nodeName == 'cn' + assert mml_3.childNodes[1].childNodes[0].nodeValue == '1' + assert mml_3.childNodes[2].nodeName == 'ci' + assert mml_3.childNodes[2].childNodes[0].nodeValue == 'x' + + mml_4 = mp._print(Lt(1, x)) + assert mml_4.nodeName == 'apply' + assert mml_4.childNodes[0].nodeName == 'lt' + assert mml_4.childNodes[1].nodeName == 'cn' + assert mml_4.childNodes[1].childNodes[0].nodeValue == '1' + assert mml_4.childNodes[2].nodeName == 'ci' + assert mml_4.childNodes[2].childNodes[0].nodeValue == 'x' + + +def test_content_symbol(): + mml = mp._print(x) + assert mml.nodeName == 'ci' + assert mml.childNodes[0].nodeValue == 'x' + del mml + + mml = mp._print(Symbol("x^2")) + assert mml.nodeName == 'ci' + assert mml.childNodes[0].nodeName == 'mml:msup' + assert mml.childNodes[0].childNodes[0].nodeName == 'mml:mi' + assert mml.childNodes[0].childNodes[0].childNodes[0].nodeValue == 'x' + assert mml.childNodes[0].childNodes[1].nodeName == 'mml:mi' + assert mml.childNodes[0].childNodes[1].childNodes[0].nodeValue == '2' + del mml + + mml = mp._print(Symbol("x__2")) + assert mml.nodeName == 'ci' + assert mml.childNodes[0].nodeName == 'mml:msup' + assert mml.childNodes[0].childNodes[0].nodeName == 'mml:mi' + assert mml.childNodes[0].childNodes[0].childNodes[0].nodeValue == 'x' + assert mml.childNodes[0].childNodes[1].nodeName == 'mml:mi' + assert mml.childNodes[0].childNodes[1].childNodes[0].nodeValue == '2' + del mml + + mml = mp._print(Symbol("x_2")) + assert mml.nodeName == 'ci' + assert mml.childNodes[0].nodeName == 'mml:msub' + assert mml.childNodes[0].childNodes[0].nodeName == 'mml:mi' + assert mml.childNodes[0].childNodes[0].childNodes[0].nodeValue == 'x' + assert mml.childNodes[0].childNodes[1].nodeName == 'mml:mi' + assert mml.childNodes[0].childNodes[1].childNodes[0].nodeValue == '2' + del mml + + mml = mp._print(Symbol("x^3_2")) + assert mml.nodeName == 'ci' + assert mml.childNodes[0].nodeName == 'mml:msubsup' + assert mml.childNodes[0].childNodes[0].nodeName == 'mml:mi' + assert mml.childNodes[0].childNodes[0].childNodes[0].nodeValue == 'x' + assert mml.childNodes[0].childNodes[1].nodeName == 'mml:mi' + assert mml.childNodes[0].childNodes[1].childNodes[0].nodeValue == '2' + assert mml.childNodes[0].childNodes[2].nodeName == 'mml:mi' + assert mml.childNodes[0].childNodes[2].childNodes[0].nodeValue == '3' + del mml + + mml = mp._print(Symbol("x__3_2")) + assert mml.nodeName == 'ci' + assert mml.childNodes[0].nodeName == 'mml:msubsup' + assert mml.childNodes[0].childNodes[0].nodeName == 'mml:mi' + assert mml.childNodes[0].childNodes[0].childNodes[0].nodeValue == 'x' + assert mml.childNodes[0].childNodes[1].nodeName == 'mml:mi' + assert mml.childNodes[0].childNodes[1].childNodes[0].nodeValue == '2' + assert mml.childNodes[0].childNodes[2].nodeName == 'mml:mi' + assert mml.childNodes[0].childNodes[2].childNodes[0].nodeValue == '3' + del mml + + mml = mp._print(Symbol("x_2_a")) + assert mml.nodeName == 'ci' + assert mml.childNodes[0].nodeName == 'mml:msub' + assert mml.childNodes[0].childNodes[0].nodeName == 'mml:mi' + assert mml.childNodes[0].childNodes[0].childNodes[0].nodeValue == 'x' + assert mml.childNodes[0].childNodes[1].nodeName == 'mml:mrow' + assert mml.childNodes[0].childNodes[1].childNodes[0].nodeName == 'mml:mi' + assert mml.childNodes[0].childNodes[1].childNodes[0].childNodes[ + 0].nodeValue == '2' + assert mml.childNodes[0].childNodes[1].childNodes[1].nodeName == 'mml:mo' + assert mml.childNodes[0].childNodes[1].childNodes[1].childNodes[ + 0].nodeValue == ' ' + assert mml.childNodes[0].childNodes[1].childNodes[2].nodeName == 'mml:mi' + assert mml.childNodes[0].childNodes[1].childNodes[2].childNodes[ + 0].nodeValue == 'a' + del mml + + mml = mp._print(Symbol("x^2^a")) + assert mml.nodeName == 'ci' + assert mml.childNodes[0].nodeName == 'mml:msup' + assert mml.childNodes[0].childNodes[0].nodeName == 'mml:mi' + assert mml.childNodes[0].childNodes[0].childNodes[0].nodeValue == 'x' + assert mml.childNodes[0].childNodes[1].nodeName == 'mml:mrow' + assert mml.childNodes[0].childNodes[1].childNodes[0].nodeName == 'mml:mi' + assert mml.childNodes[0].childNodes[1].childNodes[0].childNodes[ + 0].nodeValue == '2' + assert mml.childNodes[0].childNodes[1].childNodes[1].nodeName == 'mml:mo' + assert mml.childNodes[0].childNodes[1].childNodes[1].childNodes[ + 0].nodeValue == ' ' + assert mml.childNodes[0].childNodes[1].childNodes[2].nodeName == 'mml:mi' + assert mml.childNodes[0].childNodes[1].childNodes[2].childNodes[ + 0].nodeValue == 'a' + del mml + + mml = mp._print(Symbol("x__2__a")) + assert mml.nodeName == 'ci' + assert mml.childNodes[0].nodeName == 'mml:msup' + assert mml.childNodes[0].childNodes[0].nodeName == 'mml:mi' + assert mml.childNodes[0].childNodes[0].childNodes[0].nodeValue == 'x' + assert mml.childNodes[0].childNodes[1].nodeName == 'mml:mrow' + assert mml.childNodes[0].childNodes[1].childNodes[0].nodeName == 'mml:mi' + assert mml.childNodes[0].childNodes[1].childNodes[0].childNodes[ + 0].nodeValue == '2' + assert mml.childNodes[0].childNodes[1].childNodes[1].nodeName == 'mml:mo' + assert mml.childNodes[0].childNodes[1].childNodes[1].childNodes[ + 0].nodeValue == ' ' + assert mml.childNodes[0].childNodes[1].childNodes[2].nodeName == 'mml:mi' + assert mml.childNodes[0].childNodes[1].childNodes[2].childNodes[ + 0].nodeValue == 'a' + del mml + + +def test_content_mathml_greek(): + mml = mp._print(Symbol('alpha')) + assert mml.nodeName == 'ci' + assert mml.childNodes[0].nodeValue == '\N{GREEK SMALL LETTER ALPHA}' + + assert mp.doprint(Symbol('alpha')) == 'α' + assert mp.doprint(Symbol('beta')) == 'β' + assert mp.doprint(Symbol('gamma')) == 'γ' + assert mp.doprint(Symbol('delta')) == 'δ' + assert mp.doprint(Symbol('epsilon')) == 'ε' + assert mp.doprint(Symbol('zeta')) == 'ζ' + assert mp.doprint(Symbol('eta')) == 'η' + assert mp.doprint(Symbol('theta')) == 'θ' + assert mp.doprint(Symbol('iota')) == 'ι' + assert mp.doprint(Symbol('kappa')) == 'κ' + assert mp.doprint(Symbol('lambda')) == 'λ' + assert mp.doprint(Symbol('mu')) == 'μ' + assert mp.doprint(Symbol('nu')) == 'ν' + assert mp.doprint(Symbol('xi')) == 'ξ' + assert mp.doprint(Symbol('omicron')) == 'ο' + assert mp.doprint(Symbol('pi')) == 'π' + assert mp.doprint(Symbol('rho')) == 'ρ' + assert mp.doprint(Symbol('varsigma')) == 'ς' + assert mp.doprint(Symbol('sigma')) == 'σ' + assert mp.doprint(Symbol('tau')) == 'τ' + assert mp.doprint(Symbol('upsilon')) == 'υ' + assert mp.doprint(Symbol('phi')) == 'φ' + assert mp.doprint(Symbol('chi')) == 'χ' + assert mp.doprint(Symbol('psi')) == 'ψ' + assert mp.doprint(Symbol('omega')) == 'ω' + + assert mp.doprint(Symbol('Alpha')) == 'Α' + assert mp.doprint(Symbol('Beta')) == 'Β' + assert mp.doprint(Symbol('Gamma')) == 'Γ' + assert mp.doprint(Symbol('Delta')) == 'Δ' + assert mp.doprint(Symbol('Epsilon')) == 'Ε' + assert mp.doprint(Symbol('Zeta')) == 'Ζ' + assert mp.doprint(Symbol('Eta')) == 'Η' + assert mp.doprint(Symbol('Theta')) == 'Θ' + assert mp.doprint(Symbol('Iota')) == 'Ι' + assert mp.doprint(Symbol('Kappa')) == 'Κ' + assert mp.doprint(Symbol('Lambda')) == 'Λ' + assert mp.doprint(Symbol('Mu')) == 'Μ' + assert mp.doprint(Symbol('Nu')) == 'Ν' + assert mp.doprint(Symbol('Xi')) == 'Ξ' + assert mp.doprint(Symbol('Omicron')) == 'Ο' + assert mp.doprint(Symbol('Pi')) == 'Π' + assert mp.doprint(Symbol('Rho')) == 'Ρ' + assert mp.doprint(Symbol('Sigma')) == 'Σ' + assert mp.doprint(Symbol('Tau')) == 'Τ' + assert mp.doprint(Symbol('Upsilon')) == 'Υ' + assert mp.doprint(Symbol('Phi')) == 'Φ' + assert mp.doprint(Symbol('Chi')) == 'Χ' + assert mp.doprint(Symbol('Psi')) == 'Ψ' + assert mp.doprint(Symbol('Omega')) == 'Ω' + + +def test_content_mathml_order(): + expr = x**3 + x**2*y + 3*x*y**3 + y**4 + + mp = MathMLContentPrinter({'order': 'lex'}) + mml = mp._print(expr) + + assert mml.childNodes[1].childNodes[0].nodeName == 'power' + assert mml.childNodes[1].childNodes[1].childNodes[0].data == 'x' + assert mml.childNodes[1].childNodes[2].childNodes[0].data == '3' + + assert mml.childNodes[4].childNodes[0].nodeName == 'power' + assert mml.childNodes[4].childNodes[1].childNodes[0].data == 'y' + assert mml.childNodes[4].childNodes[2].childNodes[0].data == '4' + + mp = MathMLContentPrinter({'order': 'rev-lex'}) + mml = mp._print(expr) + + assert mml.childNodes[1].childNodes[0].nodeName == 'power' + assert mml.childNodes[1].childNodes[1].childNodes[0].data == 'y' + assert mml.childNodes[1].childNodes[2].childNodes[0].data == '4' + + assert mml.childNodes[4].childNodes[0].nodeName == 'power' + assert mml.childNodes[4].childNodes[1].childNodes[0].data == 'x' + assert mml.childNodes[4].childNodes[2].childNodes[0].data == '3' + + +def test_content_settings(): + raises(TypeError, lambda: mathml(x, method="garbage")) + + +def test_content_mathml_logic(): + assert mathml(And(x, y)) == 'xy' + assert mathml(Or(x, y)) == 'xy' + assert mathml(Xor(x, y)) == 'xy' + assert mathml(Implies(x, y)) == 'xy' + assert mathml(Not(x)) == 'x' + + +def test_content_finite_sets(): + assert mathml(FiniteSet(a)) == 'a' + assert mathml(FiniteSet(a, b)) == 'ab' + assert mathml(FiniteSet(FiniteSet(a, b), c)) == \ + 'cab' + + A = FiniteSet(a) + B = FiniteSet(b) + C = FiniteSet(c) + D = FiniteSet(d) + + U1 = Union(A, B, evaluate=False) + U2 = Union(C, D, evaluate=False) + I1 = Intersection(A, B, evaluate=False) + I2 = Intersection(C, D, evaluate=False) + C1 = Complement(A, B, evaluate=False) + C2 = Complement(C, D, evaluate=False) + # XXX ProductSet does not support evaluate keyword + P1 = ProductSet(A, B) + P2 = ProductSet(C, D) + + assert mathml(U1) == \ + 'ab' + assert mathml(I1) == \ + 'ab' \ + '' + assert mathml(C1) == \ + 'ab' + assert mathml(P1) == \ + 'ab' \ + '' + + assert mathml(Intersection(A, U2, evaluate=False)) == \ + 'a' \ + 'cd' + assert mathml(Intersection(U1, U2, evaluate=False)) == \ + 'a' \ + 'bc' \ + 'd' + + # XXX Does the parenthesis appear correctly for these examples in mathjax? + assert mathml(Intersection(C1, C2, evaluate=False)) == \ + 'a' \ + 'bc' \ + 'd' + assert mathml(Intersection(P1, P2, evaluate=False)) == \ + 'a' \ + 'b' \ + 'cd' + + assert mathml(Union(A, I2, evaluate=False)) == \ + 'a' \ + 'cd' + assert mathml(Union(I1, I2, evaluate=False)) == \ + 'a' \ + 'bc' \ + 'd' + assert mathml(Union(C1, C2, evaluate=False)) == \ + 'a' \ + 'bc' \ + 'd' + assert mathml(Union(P1, P2, evaluate=False)) == \ + 'a' \ + 'b' \ + 'cd' + + assert mathml(Complement(A, C2, evaluate=False)) == \ + 'a' \ + 'cd' + assert mathml(Complement(U1, U2, evaluate=False)) == \ + 'a' \ + 'bc' \ + 'd' + assert mathml(Complement(I1, I2, evaluate=False)) == \ + 'a' \ + 'bc' \ + 'd' + assert mathml(Complement(P1, P2, evaluate=False)) == \ + 'a' \ + 'b' \ + 'cd' + + assert mathml(ProductSet(A, P2)) == \ + 'a' \ + 'c' \ + 'd' + assert mathml(ProductSet(U1, U2)) == \ + 'a' \ + 'bc' \ + 'd' + assert mathml(ProductSet(I1, I2)) == \ + 'a' \ + 'b' \ + 'cd' + assert mathml(ProductSet(C1, C2)) == \ + 'a' \ + 'b' \ + 'cd' + + +def test_presentation_printmethod(): + assert mpp.doprint(1 + x) == 'x+1' + assert mpp.doprint(x**2) == 'x2' + assert mpp.doprint(x**-1) == '1x' + assert mpp.doprint(x**-2) == \ + '1x2' + assert mpp.doprint(2*x) == \ + '2⁢x' + + +def test_presentation_mathml_core(): + mml_1 = mpp._print(1 + x) + assert mml_1.nodeName == 'mrow' + nodes = mml_1.childNodes + assert len(nodes) == 3 + assert nodes[0].nodeName in ['mi', 'mn'] + assert nodes[1].nodeName == 'mo' + if nodes[0].nodeName == 'mn': + assert nodes[0].childNodes[0].nodeValue == '1' + assert nodes[2].childNodes[0].nodeValue == 'x' + else: + assert nodes[0].childNodes[0].nodeValue == 'x' + assert nodes[2].childNodes[0].nodeValue == '1' + + mml_2 = mpp._print(x**2) + assert mml_2.nodeName == 'msup' + nodes = mml_2.childNodes + assert nodes[0].childNodes[0].nodeValue == 'x' + assert nodes[1].childNodes[0].nodeValue == '2' + + mml_3 = mpp._print(2*x) + assert mml_3.nodeName == 'mrow' + nodes = mml_3.childNodes + assert nodes[0].childNodes[0].nodeValue == '2' + assert nodes[1].childNodes[0].nodeValue == '⁢' + assert nodes[2].childNodes[0].nodeValue == 'x' + + mml = mpp._print(Float(1.0, 2)*x) + assert mml.nodeName == 'mrow' + nodes = mml.childNodes + assert nodes[0].childNodes[0].nodeValue == '1.0' + assert nodes[1].childNodes[0].nodeValue == '⁢' + assert nodes[2].childNodes[0].nodeValue == 'x' + + +def test_presentation_mathml_functions(): + mml_1 = mpp._print(sin(x)) + assert mml_1.childNodes[0].childNodes[0 + ].nodeValue == 'sin' + assert mml_1.childNodes[1].childNodes[1 + ].childNodes[0].nodeValue == 'x' + + mml_2 = mpp._print(diff(sin(x), x, evaluate=False)) + assert mml_2.nodeName == 'mrow' + assert mml_2.childNodes[0].childNodes[0 + ].childNodes[0].childNodes[0].nodeValue == 'ⅆ' + assert mml_2.childNodes[1].childNodes[1 + ].nodeName == 'mrow' + assert mml_2.childNodes[0].childNodes[1 + ].childNodes[0].childNodes[0].nodeValue == 'ⅆ' + + mml_3 = mpp._print(diff(cos(x*y), x, evaluate=False)) + assert mml_3.childNodes[0].nodeName == 'mfrac' + assert mml_3.childNodes[0].childNodes[0 + ].childNodes[0].childNodes[0].nodeValue == '∂' + assert mml_3.childNodes[1].childNodes[0 + ].childNodes[0].nodeValue == 'cos' + + +def test_print_derivative(): + f = Function('f') + d = Derivative(f(x, y, z), x, z, x, z, z, y) + assert mathml(d) == \ + 'yz2xzxxyz' + assert mathml(d, printer='presentation') == \ + '∂6∂y∂2z∂x∂z∂xf(x,y,z)' + + +def test_presentation_mathml_limits(): + lim_fun = sin(x)/x + mml_1 = mpp._print(Limit(lim_fun, x, 0)) + assert mml_1.childNodes[0].nodeName == 'munder' + assert mml_1.childNodes[0].childNodes[0 + ].childNodes[0].nodeValue == 'lim' + assert mml_1.childNodes[0].childNodes[1 + ].childNodes[0].childNodes[0 + ].nodeValue == 'x' + assert mml_1.childNodes[0].childNodes[1 + ].childNodes[1].childNodes[0 + ].nodeValue == '→' + assert mml_1.childNodes[0].childNodes[1 + ].childNodes[2].childNodes[0 + ].nodeValue == '0' + + +def test_presentation_mathml_integrals(): + assert mpp.doprint(Integral(x, (x, 0, 1))) == \ + '∫01'\ + 'xⅆx' + assert mpp.doprint(Integral(log(x), x)) == \ + '∫log(x' \ + ')ⅆx' + assert mpp.doprint(Integral(x*y, x, y)) == \ + '∬x⁢'\ + 'yⅆyⅆx' + z, w = symbols('z w') + assert mpp.doprint(Integral(x*y*z, x, y, z)) == \ + '∭x⁢'\ + 'y⁢zⅆ'\ + 'zⅆyⅆx' + assert mpp.doprint(Integral(x*y*z*w, x, y, z, w)) == \ + '∫∫∫'\ + '∫w⁢'\ + 'x⁢y'\ + '⁢zⅆw'\ + 'ⅆzⅆyⅆx' + assert mpp.doprint(Integral(x, x, y, (z, 0, 1))) == \ + '∫01'\ + '∫∫xⅆz'\ + 'ⅆyⅆx' + assert mpp.doprint(Integral(x, (x, 0))) == \ + '∫0xⅆ'\ + 'x' + + +def test_presentation_mathml_matrices(): + A = Matrix([1, 2, 3]) + B = Matrix([[0, 5, 4], [2, 3, 1], [9, 7, 9]]) + mll_1 = mpp._print(A) + assert mll_1.childNodes[1].nodeName == 'mtable' + assert mll_1.childNodes[1].childNodes[0].nodeName == 'mtr' + assert len(mll_1.childNodes[1].childNodes) == 3 + assert mll_1.childNodes[1].childNodes[0].childNodes[0].nodeName == 'mtd' + assert len(mll_1.childNodes[1].childNodes[0].childNodes) == 1 + assert mll_1.childNodes[1].childNodes[0].childNodes[0 + ].childNodes[0].childNodes[0].nodeValue == '1' + assert mll_1.childNodes[1].childNodes[1].childNodes[0 + ].childNodes[0].childNodes[0].nodeValue == '2' + assert mll_1.childNodes[1].childNodes[2].childNodes[0 + ].childNodes[0].childNodes[0].nodeValue == '3' + mll_2 = mpp._print(B) + assert mll_2.childNodes[1].nodeName == 'mtable' + assert mll_2.childNodes[1].childNodes[0].nodeName == 'mtr' + assert len(mll_2.childNodes[1].childNodes) == 3 + assert mll_2.childNodes[1].childNodes[0].childNodes[0].nodeName == 'mtd' + assert len(mll_2.childNodes[1].childNodes[0].childNodes) == 3 + assert mll_2.childNodes[1].childNodes[0].childNodes[0 + ].childNodes[0].childNodes[0].nodeValue == '0' + assert mll_2.childNodes[1].childNodes[0].childNodes[1 + ].childNodes[0].childNodes[0].nodeValue == '5' + assert mll_2.childNodes[1].childNodes[0].childNodes[2 + ].childNodes[0].childNodes[0].nodeValue == '4' + assert mll_2.childNodes[1].childNodes[1].childNodes[0 + ].childNodes[0].childNodes[0].nodeValue == '2' + assert mll_2.childNodes[1].childNodes[1].childNodes[1 + ].childNodes[0].childNodes[0].nodeValue == '3' + assert mll_2.childNodes[1].childNodes[1].childNodes[2 + ].childNodes[0].childNodes[0].nodeValue == '1' + assert mll_2.childNodes[1].childNodes[2].childNodes[0 + ].childNodes[0].childNodes[0].nodeValue == '9' + assert mll_2.childNodes[1].childNodes[2].childNodes[1 + ].childNodes[0].childNodes[0].nodeValue == '7' + assert mll_2.childNodes[1].childNodes[2].childNodes[2 + ].childNodes[0].childNodes[0].nodeValue == '9' + + +def test_presentation_mathml_sums(): + mml_1 = mpp._print(Sum(x, (x, 1, 10))) + assert mml_1.childNodes[0].nodeName == 'munderover' + assert len(mml_1.childNodes[0].childNodes) == 3 + assert mml_1.childNodes[0].childNodes[0].childNodes[0 + ].nodeValue == '∑' + assert len(mml_1.childNodes[0].childNodes[1].childNodes) == 3 + assert mml_1.childNodes[0].childNodes[2].childNodes[0 + ].nodeValue == '10' + assert mml_1.childNodes[1].childNodes[0].nodeValue == 'x' + + assert mpp.doprint(Sum(x, (x, 1, 10))) == \ + '∑x=110x' + assert mpp.doprint(Sum(x + y, (x, 1, 10))) == \ + '∑x=110(x+y)' + + +def test_presentation_mathml_add(): + mml = mpp._print(x**5 - x**4 + x) + assert len(mml.childNodes) == 5 + assert mml.childNodes[0].childNodes[0].childNodes[0 + ].nodeValue == 'x' + assert mml.childNodes[0].childNodes[1].childNodes[0 + ].nodeValue == '5' + assert mml.childNodes[1].childNodes[0].nodeValue == '-' + assert mml.childNodes[2].childNodes[0].childNodes[0 + ].nodeValue == 'x' + assert mml.childNodes[2].childNodes[1].childNodes[0 + ].nodeValue == '4' + assert mml.childNodes[3].childNodes[0].nodeValue == '+' + assert mml.childNodes[4].childNodes[0].nodeValue == 'x' + + +def test_presentation_mathml_Rational(): + mml_1 = mpp._print(Rational(1, 1)) + assert mml_1.nodeName == 'mn' + + mml_2 = mpp._print(Rational(2, 5)) + assert mml_2.nodeName == 'mfrac' + assert mml_2.childNodes[0].childNodes[0].nodeValue == '2' + assert mml_2.childNodes[1].childNodes[0].nodeValue == '5' + + +def test_presentation_mathml_constants(): + mml = mpp._print(I) + assert mml.childNodes[0].nodeValue == 'ⅈ' + + mml = mpp._print(E) + assert mml.childNodes[0].nodeValue == 'ⅇ' + + mml = mpp._print(oo) + assert mml.childNodes[0].nodeValue == '∞' + + mml = mpp._print(pi) + assert mml.childNodes[0].nodeValue == 'π' + + assert mathml(hbar, printer='presentation') == 'ℏ' + assert mathml(S.TribonacciConstant, printer='presentation' + ) == 'TribonacciConstant' + assert mathml(S.EulerGamma, printer='presentation' + ) == 'γ' + assert mathml(S.GoldenRatio, printer='presentation' + ) == 'Φ' + + assert mathml(zoo, printer='presentation') == \ + '∞~' + + assert mathml(S.NaN, printer='presentation') == 'NaN' + +def test_presentation_mathml_trig(): + mml = mpp._print(sin(x)) + assert mml.childNodes[0].childNodes[0].nodeValue == 'sin' + + mml = mpp._print(cos(x)) + assert mml.childNodes[0].childNodes[0].nodeValue == 'cos' + + mml = mpp._print(tan(x)) + assert mml.childNodes[0].childNodes[0].nodeValue == 'tan' + + mml = mpp._print(asin(x)) + assert mml.childNodes[0].childNodes[0].nodeValue == 'arcsin' + + mml = mpp._print(acos(x)) + assert mml.childNodes[0].childNodes[0].nodeValue == 'arccos' + + mml = mpp._print(atan(x)) + assert mml.childNodes[0].childNodes[0].nodeValue == 'arctan' + + mml = mpp._print(sinh(x)) + assert mml.childNodes[0].childNodes[0].nodeValue == 'sinh' + + mml = mpp._print(cosh(x)) + assert mml.childNodes[0].childNodes[0].nodeValue == 'cosh' + + mml = mpp._print(tanh(x)) + assert mml.childNodes[0].childNodes[0].nodeValue == 'tanh' + + mml = mpp._print(asinh(x)) + assert mml.childNodes[0].childNodes[0].nodeValue == 'arcsinh' + + mml = mpp._print(atanh(x)) + assert mml.childNodes[0].childNodes[0].nodeValue == 'arctanh' + + mml = mpp._print(acosh(x)) + assert mml.childNodes[0].childNodes[0].nodeValue == 'arccosh' + + +def test_presentation_mathml_relational(): + mml_1 = mpp._print(Eq(x, 1)) + assert len(mml_1.childNodes) == 3 + assert mml_1.childNodes[0].nodeName == 'mi' + assert mml_1.childNodes[0].childNodes[0].nodeValue == 'x' + assert mml_1.childNodes[1].nodeName == 'mo' + assert mml_1.childNodes[1].childNodes[0].nodeValue == '=' + assert mml_1.childNodes[2].nodeName == 'mn' + assert mml_1.childNodes[2].childNodes[0].nodeValue == '1' + + mml_2 = mpp._print(Ne(1, x)) + assert len(mml_2.childNodes) == 3 + assert mml_2.childNodes[0].nodeName == 'mn' + assert mml_2.childNodes[0].childNodes[0].nodeValue == '1' + assert mml_2.childNodes[1].nodeName == 'mo' + assert mml_2.childNodes[1].childNodes[0].nodeValue == '≠' + assert mml_2.childNodes[2].nodeName == 'mi' + assert mml_2.childNodes[2].childNodes[0].nodeValue == 'x' + + mml_3 = mpp._print(Ge(1, x)) + assert len(mml_3.childNodes) == 3 + assert mml_3.childNodes[0].nodeName == 'mn' + assert mml_3.childNodes[0].childNodes[0].nodeValue == '1' + assert mml_3.childNodes[1].nodeName == 'mo' + assert mml_3.childNodes[1].childNodes[0].nodeValue == '≥' + assert mml_3.childNodes[2].nodeName == 'mi' + assert mml_3.childNodes[2].childNodes[0].nodeValue == 'x' + + mml_4 = mpp._print(Lt(1, x)) + assert len(mml_4.childNodes) == 3 + assert mml_4.childNodes[0].nodeName == 'mn' + assert mml_4.childNodes[0].childNodes[0].nodeValue == '1' + assert mml_4.childNodes[1].nodeName == 'mo' + assert mml_4.childNodes[1].childNodes[0].nodeValue == '<' + assert mml_4.childNodes[2].nodeName == 'mi' + assert mml_4.childNodes[2].childNodes[0].nodeValue == 'x' + + +def test_presentation_symbol(): + mml = mpp._print(x) + assert mml.nodeName == 'mi' + assert mml.childNodes[0].nodeValue == 'x' + del mml + + mml = mpp._print(Symbol("x^2")) + assert mml.nodeName == 'msup' + assert mml.childNodes[0].nodeName == 'mi' + assert mml.childNodes[0].childNodes[0].nodeValue == 'x' + assert mml.childNodes[1].nodeName == 'mi' + assert mml.childNodes[1].childNodes[0].nodeValue == '2' + del mml + + mml = mpp._print(Symbol("x__2")) + assert mml.nodeName == 'msup' + assert mml.childNodes[0].nodeName == 'mi' + assert mml.childNodes[0].childNodes[0].nodeValue == 'x' + assert mml.childNodes[1].nodeName == 'mi' + assert mml.childNodes[1].childNodes[0].nodeValue == '2' + del mml + + mml = mpp._print(Symbol("x_2")) + assert mml.nodeName == 'msub' + assert mml.childNodes[0].nodeName == 'mi' + assert mml.childNodes[0].childNodes[0].nodeValue == 'x' + assert mml.childNodes[1].nodeName == 'mi' + assert mml.childNodes[1].childNodes[0].nodeValue == '2' + del mml + + mml = mpp._print(Symbol("x^3_2")) + assert mml.nodeName == 'msubsup' + assert mml.childNodes[0].nodeName == 'mi' + assert mml.childNodes[0].childNodes[0].nodeValue == 'x' + assert mml.childNodes[1].nodeName == 'mi' + assert mml.childNodes[1].childNodes[0].nodeValue == '2' + assert mml.childNodes[2].nodeName == 'mi' + assert mml.childNodes[2].childNodes[0].nodeValue == '3' + del mml + + mml = mpp._print(Symbol("x__3_2")) + assert mml.nodeName == 'msubsup' + assert mml.childNodes[0].nodeName == 'mi' + assert mml.childNodes[0].childNodes[0].nodeValue == 'x' + assert mml.childNodes[1].nodeName == 'mi' + assert mml.childNodes[1].childNodes[0].nodeValue == '2' + assert mml.childNodes[2].nodeName == 'mi' + assert mml.childNodes[2].childNodes[0].nodeValue == '3' + del mml + + mml = mpp._print(Symbol("x_2_a")) + assert mml.nodeName == 'msub' + assert mml.childNodes[0].nodeName == 'mi' + assert mml.childNodes[0].childNodes[0].nodeValue == 'x' + assert mml.childNodes[1].nodeName == 'mrow' + assert mml.childNodes[1].childNodes[0].nodeName == 'mi' + assert mml.childNodes[1].childNodes[0].childNodes[0].nodeValue == '2' + assert mml.childNodes[1].childNodes[1].nodeName == 'mo' + assert mml.childNodes[1].childNodes[1].childNodes[0].nodeValue == ' ' + assert mml.childNodes[1].childNodes[2].nodeName == 'mi' + assert mml.childNodes[1].childNodes[2].childNodes[0].nodeValue == 'a' + del mml + + mml = mpp._print(Symbol("x^2^a")) + assert mml.nodeName == 'msup' + assert mml.childNodes[0].nodeName == 'mi' + assert mml.childNodes[0].childNodes[0].nodeValue == 'x' + assert mml.childNodes[1].nodeName == 'mrow' + assert mml.childNodes[1].childNodes[0].nodeName == 'mi' + assert mml.childNodes[1].childNodes[0].childNodes[0].nodeValue == '2' + assert mml.childNodes[1].childNodes[1].nodeName == 'mo' + assert mml.childNodes[1].childNodes[1].childNodes[0].nodeValue == ' ' + assert mml.childNodes[1].childNodes[2].nodeName == 'mi' + assert mml.childNodes[1].childNodes[2].childNodes[0].nodeValue == 'a' + del mml + + mml = mpp._print(Symbol("x__2__a")) + assert mml.nodeName == 'msup' + assert mml.childNodes[0].nodeName == 'mi' + assert mml.childNodes[0].childNodes[0].nodeValue == 'x' + assert mml.childNodes[1].nodeName == 'mrow' + assert mml.childNodes[1].childNodes[0].nodeName == 'mi' + assert mml.childNodes[1].childNodes[0].childNodes[0].nodeValue == '2' + assert mml.childNodes[1].childNodes[1].nodeName == 'mo' + assert mml.childNodes[1].childNodes[1].childNodes[0].nodeValue == ' ' + assert mml.childNodes[1].childNodes[2].nodeName == 'mi' + assert mml.childNodes[1].childNodes[2].childNodes[0].nodeValue == 'a' + del mml + + +def test_presentation_mathml_greek(): + mml = mpp._print(Symbol('alpha')) + assert mml.nodeName == 'mi' + assert mml.childNodes[0].nodeValue == '\N{GREEK SMALL LETTER ALPHA}' + + assert mpp.doprint(Symbol('alpha')) == 'α' + assert mpp.doprint(Symbol('beta')) == 'β' + assert mpp.doprint(Symbol('gamma')) == 'γ' + assert mpp.doprint(Symbol('delta')) == 'δ' + assert mpp.doprint(Symbol('epsilon')) == 'ε' + assert mpp.doprint(Symbol('zeta')) == 'ζ' + assert mpp.doprint(Symbol('eta')) == 'η' + assert mpp.doprint(Symbol('theta')) == 'θ' + assert mpp.doprint(Symbol('iota')) == 'ι' + assert mpp.doprint(Symbol('kappa')) == 'κ' + assert mpp.doprint(Symbol('lambda')) == 'λ' + assert mpp.doprint(Symbol('mu')) == 'μ' + assert mpp.doprint(Symbol('nu')) == 'ν' + assert mpp.doprint(Symbol('xi')) == 'ξ' + assert mpp.doprint(Symbol('omicron')) == 'ο' + assert mpp.doprint(Symbol('pi')) == 'π' + assert mpp.doprint(Symbol('rho')) == 'ρ' + assert mpp.doprint(Symbol('varsigma')) == 'ς' + assert mpp.doprint(Symbol('sigma')) == 'σ' + assert mpp.doprint(Symbol('tau')) == 'τ' + assert mpp.doprint(Symbol('upsilon')) == 'υ' + assert mpp.doprint(Symbol('phi')) == 'φ' + assert mpp.doprint(Symbol('chi')) == 'χ' + assert mpp.doprint(Symbol('psi')) == 'ψ' + assert mpp.doprint(Symbol('omega')) == 'ω' + + assert mpp.doprint(Symbol('Alpha')) == 'Α' + assert mpp.doprint(Symbol('Beta')) == 'Β' + assert mpp.doprint(Symbol('Gamma')) == 'Γ' + assert mpp.doprint(Symbol('Delta')) == 'Δ' + assert mpp.doprint(Symbol('Epsilon')) == 'Ε' + assert mpp.doprint(Symbol('Zeta')) == 'Ζ' + assert mpp.doprint(Symbol('Eta')) == 'Η' + assert mpp.doprint(Symbol('Theta')) == 'Θ' + assert mpp.doprint(Symbol('Iota')) == 'Ι' + assert mpp.doprint(Symbol('Kappa')) == 'Κ' + assert mpp.doprint(Symbol('Lambda')) == 'Λ' + assert mpp.doprint(Symbol('Mu')) == 'Μ' + assert mpp.doprint(Symbol('Nu')) == 'Ν' + assert mpp.doprint(Symbol('Xi')) == 'Ξ' + assert mpp.doprint(Symbol('Omicron')) == 'Ο' + assert mpp.doprint(Symbol('Pi')) == 'Π' + assert mpp.doprint(Symbol('Rho')) == 'Ρ' + assert mpp.doprint(Symbol('Sigma')) == 'Σ' + assert mpp.doprint(Symbol('Tau')) == 'Τ' + assert mpp.doprint(Symbol('Upsilon')) == 'Υ' + assert mpp.doprint(Symbol('Phi')) == 'Φ' + assert mpp.doprint(Symbol('Chi')) == 'Χ' + assert mpp.doprint(Symbol('Psi')) == 'Ψ' + assert mpp.doprint(Symbol('Omega')) == 'Ω' + + +def test_presentation_mathml_order(): + expr = x**3 + x**2*y + 3*x*y**3 + y**4 + + mp = MathMLPresentationPrinter({'order': 'lex'}) + mml = mp._print(expr) + assert mml.childNodes[0].nodeName == 'msup' + assert mml.childNodes[0].childNodes[0].childNodes[0].nodeValue == 'x' + assert mml.childNodes[0].childNodes[1].childNodes[0].nodeValue == '3' + + assert mml.childNodes[6].nodeName == 'msup' + assert mml.childNodes[6].childNodes[0].childNodes[0].nodeValue == 'y' + assert mml.childNodes[6].childNodes[1].childNodes[0].nodeValue == '4' + + mp = MathMLPresentationPrinter({'order': 'rev-lex'}) + mml = mp._print(expr) + + assert mml.childNodes[0].nodeName == 'msup' + assert mml.childNodes[0].childNodes[0].childNodes[0].nodeValue == 'y' + assert mml.childNodes[0].childNodes[1].childNodes[0].nodeValue == '4' + + assert mml.childNodes[6].nodeName == 'msup' + assert mml.childNodes[6].childNodes[0].childNodes[0].nodeValue == 'x' + assert mml.childNodes[6].childNodes[1].childNodes[0].nodeValue == '3' + + +def test_print_intervals(): + a = Symbol('a', real=True) + assert mpp.doprint(Interval(0, a)) == \ + '[0,a]' + assert mpp.doprint(Interval(0, a, False, False)) == \ + '[0,a]' + assert mpp.doprint(Interval(0, a, True, False)) == \ + '(0,a]' + assert mpp.doprint(Interval(0, a, False, True)) == \ + '[0,a)' + assert mpp.doprint(Interval(0, a, True, True)) == \ + '(0,a)' + + +def test_print_tuples(): + assert mpp.doprint(Tuple(0,)) == \ + '(0)' + assert mpp.doprint(Tuple(0, a)) == \ + '(0,a)' + assert mpp.doprint(Tuple(0, a, a)) == \ + '(0,a,a)' + assert mpp.doprint(Tuple(0, 1, 2, 3, 4)) == \ + '(0,1,2,3,4)' + assert mpp.doprint(Tuple(0, 1, Tuple(2, 3, 4))) == \ + '(0,1,(2,3'\ + ',4))' + + +def test_print_re_im(): + assert mpp.doprint(re(x)) == \ + 'ℜ(x)' + assert mpp.doprint(im(x)) == \ + 'ℑ(x)' + assert mpp.doprint(re(x + 1, evaluate=False)) == \ + 'ℜ(x+1)' + assert mpp.doprint(im(x + 1, evaluate=False)) == \ + 'ℑ(x+1)' + + +def test_print_Abs(): + assert mpp.doprint(Abs(x)) == \ + '|x|' + assert mpp.doprint(Abs(x + 1)) == \ + '|x+1|' + + +def test_print_Determinant(): + assert mpp.doprint(Determinant(Matrix([[1, 2], [3, 4]]))) == \ + '|[1234]|' + + +def test_presentation_settings(): + raises(TypeError, lambda: mathml(x, printer='presentation', + method="garbage")) + + +def test_print_domains(): + from sympy.sets import Integers, Naturals, Naturals0, Reals, Complexes + + assert mpp.doprint(Complexes) == 'ℂ' + assert mpp.doprint(Integers) == 'ℤ' + assert mpp.doprint(Naturals) == 'ℕ' + assert mpp.doprint(Naturals0) == \ + 'ℕ0' + assert mpp.doprint(Reals) == 'ℝ' + + +def test_print_expression_with_minus(): + assert mpp.doprint(-x) == '-x' + assert mpp.doprint(-x/y) == \ + '-xy' + assert mpp.doprint(-Rational(1, 2)) == \ + '-12' + + +def test_print_AssocOp(): + from sympy.core.operations import AssocOp + + class TestAssocOp(AssocOp): + identity = 0 + + expr = TestAssocOp(1, 2) + assert mpp.doprint(expr) == \ + 'testassocop12' + + +def test_print_basic(): + expr = Basic(S(1), S(2)) + assert mpp.doprint(expr) == \ + 'basic(1,2)' + assert mp.doprint(expr) == '12' + + +def test_mat_delim_print(): + expr = Matrix([[1, 2], [3, 4]]) + assert mathml(expr, printer='presentation', mat_delim='[') == \ + '[1'\ + '234'\ + ']' + assert mathml(expr, printer='presentation', mat_delim='(') == \ + '(12'\ + '34)' + assert mathml(expr, printer='presentation', mat_delim='') == \ + '12'\ + '34' + + +def test_ln_notation_print(): + expr = log(x) + assert mathml(expr, printer='presentation') == \ + 'log(x)' + assert mathml(expr, printer='presentation', ln_notation=False) == \ + 'log(x)' + assert mathml(expr, printer='presentation', ln_notation=True) == \ + 'ln(x)' + + +def test_mul_symbol_print(): + expr = x * y + assert mathml(expr, printer='presentation') == \ + 'x⁢y' + assert mathml(expr, printer='presentation', mul_symbol=None) == \ + 'x⁢y' + assert mathml(expr, printer='presentation', mul_symbol='dot') == \ + 'x·y' + assert mathml(expr, printer='presentation', mul_symbol='ldot') == \ + 'x․y' + assert mathml(expr, printer='presentation', mul_symbol='times') == \ + 'x×y' + + +def test_print_lerchphi(): + assert mpp.doprint(lerchphi(1, 2, 3)) == \ + 'Φ(1,2,3)' + + +def test_print_polylog(): + assert mp.doprint(polylog(x, y)) == \ + 'xy' + assert mpp.doprint(polylog(x, y)) == \ + 'Lix(y)' + + +def test_print_set_frozenset(): + f = frozenset({1, 5, 3}) + assert mpp.doprint(f) == \ + '{1,3,5}' + s = set({1, 2, 3}) + assert mpp.doprint(s) == \ + '{1,2,3}' + + +def test_print_FiniteSet(): + f1 = FiniteSet(x, 1, 3) + assert mpp.doprint(f1) == \ + '{1,3,x}' + + +def test_print_LambertW(): + assert mpp.doprint(LambertW(x)) == 'W(x)' + assert mpp.doprint(LambertW(x, y)) == 'W(x,y)' + + +def test_print_EmptySet(): + assert mpp.doprint(S.EmptySet) == '∅' + + +def test_print_UniversalSet(): + assert mpp.doprint(S.UniversalSet) == '𝕌' + + +def test_print_spaces(): + assert mpp.doprint(HilbertSpace()) == 'ℋ' + assert mpp.doprint(ComplexSpace(2)) == '𝒞2' + assert mpp.doprint(FockSpace()) == 'ℱ' + + +def test_print_constants(): + assert mpp.doprint(hbar) == 'ℏ' + assert mpp.doprint(S.TribonacciConstant) == 'TribonacciConstant' + assert mpp.doprint(S.GoldenRatio) == 'Φ' + assert mpp.doprint(S.EulerGamma) == 'γ' + + +def test_print_Contains(): + assert mpp.doprint(Contains(x, S.Naturals)) == \ + 'x∈ℕ' + + +def test_print_Dagger(): + x = symbols('x', commutative=False) + assert mpp.doprint(Dagger(x)) == 'x†' + + +def test_print_SetOp(): + f1 = FiniteSet(x, 1, 3) + f2 = FiniteSet(y, 2, 4) + + prntr = lambda x: mathml(x, printer='presentation') + + assert prntr(Union(f1, f2, evaluate=False)) == \ + '{1,3,x'\ + '}∪{2,'\ + '4,y}' + assert prntr(Intersection(f1, f2, evaluate=False)) == \ + '{1,3,x'\ + '}∩{2'\ + ',4,y}' + assert prntr(Complement(f1, f2, evaluate=False)) == \ + '{1,3,x'\ + '}∖{2'\ + ',4,y}' + assert prntr(SymmetricDifference(f1, f2, evaluate=False)) == \ + '{1,3,x'\ + '}∆{2'\ + ',4,y}' + + A = FiniteSet(a) + C = FiniteSet(c) + D = FiniteSet(d) + + U1 = Union(C, D, evaluate=False) + I1 = Intersection(C, D, evaluate=False) + C1 = Complement(C, D, evaluate=False) + D1 = SymmetricDifference(C, D, evaluate=False) + # XXX ProductSet does not support evaluate keyword + P1 = ProductSet(C, D) + + assert prntr(Union(A, I1, evaluate=False)) == \ + '{a}' \ + '∪({' \ + 'c}∩{' \ + 'd})' + assert prntr(Intersection(A, C1, evaluate=False)) == \ + '{a}' \ + '∩({' \ + 'c}∖{' \ + 'd})' + assert prntr(Complement(A, D1, evaluate=False)) == \ + '{a}' \ + '∖({' \ + 'c}∆{' \ + 'd})' + assert prntr(SymmetricDifference(A, P1, evaluate=False)) == \ + '{a}' \ + '∆({' \ + 'c}×{' \ + 'd})' + assert prntr(ProductSet(A, U1)) == \ + '{a}' \ + '×({' \ + 'c}∪{' \ + 'd})' + + +def test_print_logic(): + assert mpp.doprint(And(x, y)) == \ + 'x∧y' + assert mpp.doprint(Or(x, y)) == \ + 'x∨y' + assert mpp.doprint(Xor(x, y)) == \ + 'x⊻y' + assert mpp.doprint(Implies(x, y)) == \ + 'x⇒y' + assert mpp.doprint(Equivalent(x, y)) == \ + 'x⇔y' + + assert mpp.doprint(And(Eq(x, y), x > 4)) == \ + 'x=y∧'\ + 'x>4' + assert mpp.doprint(And(Eq(x, 3), y < 3, x > y + 1)) == \ + 'x=3∧'\ + 'x>y+1'\ + '∧y<3' + assert mpp.doprint(Or(Eq(x, y), x > 4)) == \ + 'x=y∨'\ + 'x>4' + assert mpp.doprint(And(Eq(x, 3), Or(y < 3, x > y + 1))) == \ + 'x=3'\ + '∧(x>'\ + 'y+1∨'\ + 'y<3)' + + assert mpp.doprint(Not(x)) == '¬x' + assert mpp.doprint(Not(And(x, y))) == \ + '¬(x∧y)' + + +def test_root_notation_print(): + assert mathml(x**(S.One/3), printer='presentation') == \ + 'x3' + assert mathml(x**(S.One/3), printer='presentation', root_notation=False) ==\ + 'x13' + assert mathml(x**(S.One/3), printer='content') == \ + '3x' + assert mathml(x**(S.One/3), printer='content', root_notation=False) == \ + 'x13' + assert mathml(x**(Rational(-1, 3)), printer='presentation') == \ + '1x3' + assert mathml(x**(Rational(-1, 3)), printer='presentation', root_notation=False) \ + == '1x13' + + +def test_fold_frac_powers_print(): + expr = x ** Rational(5, 2) + assert mathml(expr, printer='presentation') == \ + 'x52' + assert mathml(expr, printer='presentation', fold_frac_powers=True) == \ + 'x52' + assert mathml(expr, printer='presentation', fold_frac_powers=False) == \ + 'x52' + + +def test_fold_short_frac_print(): + expr = Rational(2, 5) + assert mathml(expr, printer='presentation') == \ + '25' + assert mathml(expr, printer='presentation', fold_short_frac=True) == \ + '25' + assert mathml(expr, printer='presentation', fold_short_frac=False) == \ + '25' + + +def test_print_factorials(): + assert mpp.doprint(factorial(x)) == 'x!' + assert mpp.doprint(factorial(x + 1)) == \ + '(x+1)!' + assert mpp.doprint(factorial2(x)) == 'x!!' + assert mpp.doprint(factorial2(x + 1)) == \ + '(x+1)!!' + assert mpp.doprint(binomial(x, y)) == \ + '(xy)' + assert mpp.doprint(binomial(4, x + y)) == \ + '(4x'\ + '+y)' + + +def test_print_floor(): + expr = floor(x) + assert mathml(expr, printer='presentation') == \ + '⌊x⌋' + + +def test_print_ceiling(): + expr = ceiling(x) + assert mathml(expr, printer='presentation') == \ + '⌈x⌉' + + +def test_print_Lambda(): + expr = Lambda(x, x+1) + assert mathml(expr, printer='presentation') == \ + '(x↦x+1)' + expr = Lambda((x, y), x + y) + assert mathml(expr, printer='presentation') == \ + '((x,y)↦x+y)' + + +def test_print_conjugate(): + assert mpp.doprint(conjugate(x)) == \ + 'x' + assert mpp.doprint(conjugate(x + 1)) == \ + 'x+1' + + +def test_print_AccumBounds(): + a = Symbol('a', real=True) + assert mpp.doprint(AccumBounds(0, 1)) == '⟨0,1⟩' + assert mpp.doprint(AccumBounds(0, a)) == '⟨0,a⟩' + assert mpp.doprint(AccumBounds(a + 1, a + 2)) == '⟨a+1,a+2⟩' + + +def test_print_Float(): + assert mpp.doprint(Float(1e100)) == '1.0·10100' + assert mpp.doprint(Float(1e-100)) == '1.0·10-100' + assert mpp.doprint(Float(-1e100)) == '-1.0·10100' + assert mpp.doprint(Float(1.0*oo)) == '∞' + assert mpp.doprint(Float(-1.0*oo)) == '-∞' + + +def test_print_different_functions(): + assert mpp.doprint(gamma(x)) == 'Γ(x)' + assert mpp.doprint(lowergamma(x, y)) == 'γ(x,y)' + assert mpp.doprint(uppergamma(x, y)) == 'Γ(x,y)' + assert mpp.doprint(zeta(x)) == 'ζ(x)' + assert mpp.doprint(zeta(x, y)) == 'ζ(x,y)' + assert mpp.doprint(dirichlet_eta(x)) == 'η(x)' + assert mpp.doprint(elliptic_k(x)) == 'Κ(x)' + assert mpp.doprint(totient(x)) == 'ϕ(x)' + assert mpp.doprint(reduced_totient(x)) == 'λ(x)' + assert mpp.doprint(primenu(x)) == 'ν(x)' + assert mpp.doprint(primeomega(x)) == 'Ω(x)' + assert mpp.doprint(fresnels(x)) == 'S(x)' + assert mpp.doprint(fresnelc(x)) == 'C(x)' + assert mpp.doprint(Heaviside(x)) == 'Θ(x,12)' + + +def test_mathml_builtins(): + assert mpp.doprint(None) == 'None' + assert mpp.doprint(true) == 'True' + assert mpp.doprint(false) == 'False' + + +def test_mathml_Range(): + assert mpp.doprint(Range(1, 51)) == \ + '{1,2,…,50}' + assert mpp.doprint(Range(1, 4)) == \ + '{1,2,3}' + assert mpp.doprint(Range(0, 3, 1)) == \ + '{0,1,2}' + assert mpp.doprint(Range(0, 30, 1)) == \ + '{0,1,…,29}' + assert mpp.doprint(Range(30, 1, -1)) == \ + '{30,29,…,2}' + assert mpp.doprint(Range(0, oo, 2)) == \ + '{0,2,…}' + assert mpp.doprint(Range(oo, -2, -2)) == \ + '{…,2,0}' + assert mpp.doprint(Range(-2, -oo, -1)) == \ + '{-2,-3,…}' + + +def test_print_exp(): + assert mpp.doprint(exp(x)) == \ + 'ⅇx' + assert mpp.doprint(exp(1) + exp(2)) == \ + 'ⅇ+ⅇ2' + + +def test_print_MinMax(): + assert mpp.doprint(Min(x, y)) == \ + 'min(x,y)' + assert mpp.doprint(Min(x, 2, x**3)) == \ + 'min(2,x,x3)' + assert mpp.doprint(Max(x, y)) == \ + 'max(x,y)' + assert mpp.doprint(Max(x, 2, x**3)) == \ + 'max(2,x,x3)' + + +def test_mathml_presentation_numbers(): + n = Symbol('n') + assert mathml(catalan(n), printer='presentation') == \ + 'Cn' + assert mathml(bernoulli(n), printer='presentation') == \ + 'Bn' + assert mathml(bell(n), printer='presentation') == \ + 'Bn' + assert mathml(euler(n), printer='presentation') == \ + 'En' + assert mathml(fibonacci(n), printer='presentation') == \ + 'Fn' + assert mathml(lucas(n), printer='presentation') == \ + 'Ln' + assert mathml(tribonacci(n), printer='presentation') == \ + 'Tn' + assert mathml(bernoulli(n, x), printer='presentation') == \ + mathml(bell(n, x), printer='presentation') == \ + 'Bn(x)' + assert mathml(euler(n, x), printer='presentation') == \ + 'En(x)' + assert mathml(fibonacci(n, x), printer='presentation') == \ + 'Fn(x)' + assert mathml(tribonacci(n, x), printer='presentation') == \ + 'Tn(x)' + + +def test_mathml_presentation_mathieu(): + assert mathml(mathieuc(x, y, z), printer='presentation') == \ + 'C(x,y,z)' + assert mathml(mathieus(x, y, z), printer='presentation') == \ + 'S(x,y,z)' + assert mathml(mathieucprime(x, y, z), printer='presentation') == \ + 'C′(x,y,z)' + assert mathml(mathieusprime(x, y, z), printer='presentation') == \ + 'S′(x,y,z)' + + +def test_mathml_presentation_stieltjes(): + assert mathml(stieltjes(n), printer='presentation') == \ + 'γn' + assert mathml(stieltjes(n, x), printer='presentation') == \ + 'γn(x)' + + +def test_print_matrix_symbol(): + A = MatrixSymbol('A', 1, 2) + assert mpp.doprint(A) == 'A' + assert mp.doprint(A) == 'A' + assert mathml(A, printer='presentation', mat_symbol_style="bold") == \ + 'A' + # No effect in content printer + assert mathml(A, mat_symbol_style="bold") == 'A' + + +def test_print_hadamard(): + from sympy.matrices.expressions import HadamardProduct + from sympy.matrices.expressions import Transpose + + X = MatrixSymbol('X', 2, 2) + Y = MatrixSymbol('Y', 2, 2) + + assert mathml(HadamardProduct(X, Y*Y), printer="presentation") == \ + '' \ + 'X' \ + '∘' \ + 'Y2' \ + '' + + assert mathml(HadamardProduct(X, Y)*Y, printer="presentation") == \ + '' \ + '(' \ + 'X∘Y' \ + ')' \ + '⁢Y' \ + '' + + assert mathml(HadamardProduct(X, Y, Y), printer="presentation") == \ + '' \ + 'X∘' \ + 'Y∘' \ + 'Y' \ + '' + + assert mathml( + Transpose(HadamardProduct(X, Y)), printer="presentation") == \ + '' \ + '(' \ + 'X∘Y' \ + ')' \ + 'T' \ + '' + + +def test_print_random_symbol(): + R = RandomSymbol(Symbol('R')) + assert mpp.doprint(R) == 'R' + assert mp.doprint(R) == 'R' + + +def test_print_IndexedBase(): + assert mathml(IndexedBase(a)[b], printer='presentation') == \ + 'ab' + assert mathml(IndexedBase(a)[b, c, d], printer='presentation') == \ + 'a(b,c,d)' + assert mathml(IndexedBase(a)[b]*IndexedBase(c)[d]*IndexedBase(e), + printer='presentation') == \ + 'ab⁢'\ + 'cd⁢e' + + +def test_print_Indexed(): + assert mathml(IndexedBase(a), printer='presentation') == 'a' + assert mathml(IndexedBase(a/b), printer='presentation') == \ + 'ab' + assert mathml(IndexedBase((a, b)), printer='presentation') == \ + '(a,b)' + +def test_print_MatrixElement(): + i, j = symbols('i j') + A = MatrixSymbol('A', i, j) + assert mathml(A[0,0],printer = 'presentation') == \ + 'A0,0' + assert mathml(A[i,j], printer = 'presentation') == \ + 'Ai,j' + assert mathml(A[i*j,0], printer = 'presentation') == \ + 'Ai⁢j,0' + + +def test_print_Vector(): + ACS = CoordSys3D('A') + assert mathml(Cross(ACS.i, ACS.j*ACS.x*3 + ACS.k), printer='presentation') == \ + 'i^'\ + 'A×('\ + '(3⁢'\ + 'xA'\ + ')⁢'\ + 'j^'\ + 'A+'\ + 'k^'\ + 'A)' + assert mathml(Cross(ACS.i, ACS.j), printer='presentation') == \ + 'i^'\ + 'A×'\ + 'j^'\ + 'A' + assert mathml(x*Cross(ACS.i, ACS.j), printer='presentation') == \ + 'x⁢('\ + 'i^'\ + 'A×'\ + 'j^'\ + 'A)' + assert mathml(Cross(x*ACS.i, ACS.j), printer='presentation') == \ + '-j'\ + '^A'\ + '×((x)'\ + '⁢i'\ + '^A'\ + ')' + assert mathml(Curl(3*ACS.x*ACS.j), printer='presentation') == \ + '∇×(('\ + '3⁢x'\ + 'A)⁢'\ + 'j^'\ + 'A)' + assert mathml(Curl(3*x*ACS.x*ACS.j), printer='presentation') == \ + '∇×(('\ + '3⁢x'\ + 'A⁢'\ + 'x)⁢'\ + 'j^'\ + 'A)' + assert mathml(x*Curl(3*ACS.x*ACS.j), printer='presentation') == \ + 'x⁢(∇'\ + '×((3'\ + '⁢x'\ + 'A)'\ + '⁢j'\ + '^A)'\ + ')' + assert mathml(Curl(3*x*ACS.x*ACS.j + ACS.i), printer='presentation') == \ + '∇×('\ + 'i^'\ + 'A+('\ + '3⁢x'\ + 'A⁢'\ + 'x)⁢'\ + 'j^'\ + 'A)' + assert mathml(Divergence(3*ACS.x*ACS.j), printer='presentation') == \ + '∇·(('\ + '3⁢x'\ + 'A)'\ + '⁢j'\ + '^A)' + assert mathml(x*Divergence(3*ACS.x*ACS.j), printer='presentation') == \ + 'x⁢(∇'\ + '·((3'\ + '⁢x'\ + 'A)'\ + '⁢j'\ + '^A'\ + '))' + assert mathml(Divergence(3*x*ACS.x*ACS.j + ACS.i), printer='presentation') == \ + '∇·('\ + 'i^'\ + 'A+('\ + '3⁢'\ + 'xA'\ + '⁢x)'\ + '⁢j'\ + '^A'\ + ')' + assert mathml(Dot(ACS.i, ACS.j*ACS.x*3+ACS.k), printer='presentation') == \ + 'i^'\ + 'A·('\ + '(3⁢'\ + 'xA'\ + ')⁢'\ + 'j^'\ + 'A+'\ + 'k^'\ + 'A)' + assert mathml(Dot(ACS.i, ACS.j), printer='presentation') == \ + 'i^'\ + 'A·'\ + 'j^'\ + 'A' + assert mathml(Dot(x*ACS.i, ACS.j), printer='presentation') == \ + 'j^'\ + 'A·('\ + '(x)⁢'\ + 'i^'\ + 'A)' + assert mathml(x*Dot(ACS.i, ACS.j), printer='presentation') == \ + 'x⁢('\ + 'i^'\ + 'A·'\ + 'j^'\ + 'A)' + assert mathml(Gradient(ACS.x), printer='presentation') == \ + '∇x'\ + 'A' + assert mathml(Gradient(ACS.x + 3*ACS.y), printer='presentation') == \ + '∇('\ + 'xA+3'\ + '⁢y'\ + 'A)' + assert mathml(x*Gradient(ACS.x), printer='presentation') == \ + 'x⁢(∇'\ + 'xA'\ + ')' + assert mathml(Gradient(x*ACS.x), printer='presentation') == \ + '∇('\ + 'xA⁢'\ + 'x)' + assert mathml(Cross(ACS.z, ACS.x), printer='presentation') == \ + '-x'\ + 'A×'\ + 'zA' + assert mathml(Laplacian(ACS.x), printer='presentation') == \ + '∆x'\ + 'A' + assert mathml(Laplacian(ACS.x + 3*ACS.y), printer='presentation') == \ + '∆('\ + 'xA+3'\ + '⁢y'\ + 'A)' + assert mathml(x*Laplacian(ACS.x), printer='presentation') == \ + 'x⁢(∆'\ + 'xA'\ + ')' + assert mathml(Laplacian(x*ACS.x), printer='presentation') == \ + '∆('\ + 'xA⁢'\ + 'x)' + +@XFAIL +def test_vector_cross_xfail(): + ACS = CoordSys3D('A') + assert mathml(Cross(ACS.x, ACS.z) + Cross(ACS.z, ACS.x), printer='presentation') == \ + '0^' + +def test_print_elliptic_f(): + assert mathml(elliptic_f(x, y), printer = 'presentation') == \ + '𝖥(x|y)' + assert mathml(elliptic_f(x/y, y), printer = 'presentation') == \ + '𝖥(xy|y)' + +def test_print_elliptic_e(): + assert mathml(elliptic_e(x), printer = 'presentation') == \ + '𝖤(x)' + assert mathml(elliptic_e(x, y), printer = 'presentation') == \ + '𝖤(x|y)' + +def test_print_elliptic_pi(): + assert mathml(elliptic_pi(x, y), printer = 'presentation') == \ + '𝛱(x|y)' + assert mathml(elliptic_pi(x, y, z), printer = 'presentation') == \ + '𝛱(x;y|z)' + +def test_print_Ei(): + assert mathml(Ei(x), printer = 'presentation') == \ + 'Ei(x)' + assert mathml(Ei(x**y), printer = 'presentation') == \ + 'Ei(xy)' + +def test_print_expint(): + assert mathml(expint(x, y), printer = 'presentation') == \ + 'Ex(y)' + assert mathml(expint(IndexedBase(x)[1], IndexedBase(x)[2]), printer = 'presentation') == \ + 'Ex1(x2)' + +def test_print_jacobi(): + assert mathml(jacobi(n, a, b, x), printer = 'presentation') == \ + 'Pn(a,b)(x)' + +def test_print_gegenbauer(): + assert mathml(gegenbauer(n, a, x), printer = 'presentation') == \ + 'Cn(a)(x)' + +def test_print_chebyshevt(): + assert mathml(chebyshevt(n, x), printer = 'presentation') == \ + 'Tn(x)' + +def test_print_chebyshevu(): + assert mathml(chebyshevu(n, x), printer = 'presentation') == \ + 'Un(x)' + +def test_print_legendre(): + assert mathml(legendre(n, x), printer = 'presentation') == \ + 'Pn(x)' + +def test_print_assoc_legendre(): + assert mathml(assoc_legendre(n, a, x), printer = 'presentation') == \ + 'Pn(a)(x)' + +def test_print_laguerre(): + assert mathml(laguerre(n, x), printer = 'presentation') == \ + 'Ln(x)' + +def test_print_assoc_laguerre(): + assert mathml(assoc_laguerre(n, a, x), printer = 'presentation') == \ + 'Ln(a)(x)' + +def test_print_hermite(): + assert mathml(hermite(n, x), printer = 'presentation') == \ + 'Hn(x)' + +def test_mathml_SingularityFunction(): + assert mathml(SingularityFunction(x, 4, 5), printer='presentation') == \ + '⟨x-4⟩5' + assert mathml(SingularityFunction(x, -3, 4), printer='presentation') == \ + '⟨x+3⟩4' + assert mathml(SingularityFunction(x, 0, 4), printer='presentation') == \ + '⟨x⟩4' + assert mathml(SingularityFunction(x, a, n), printer='presentation') == \ + '⟨-a+x⟩n' + assert mathml(SingularityFunction(x, 4, -2), printer='presentation') == \ + '⟨x-4⟩-2' + assert mathml(SingularityFunction(x, 4, -1), printer='presentation') == \ + '⟨x-4⟩-1' + + +def test_mathml_matrix_functions(): + from sympy.matrices import Adjoint, Inverse, Transpose + X = MatrixSymbol('X', 2, 2) + Y = MatrixSymbol('Y', 2, 2) + assert mathml(Adjoint(X), printer='presentation') == \ + 'X†' + assert mathml(Adjoint(X + Y), printer='presentation') == \ + '(X+Y)†' + assert mathml(Adjoint(X) + Adjoint(Y), printer='presentation') == \ + 'X†+' \ + 'Y†' + assert mathml(Adjoint(X*Y), printer='presentation') == \ + '(X⁢' \ + 'Y)†' + assert mathml(Adjoint(Y)*Adjoint(X), printer='presentation') == \ + 'Y†⁢' \ + 'X†' + assert mathml(Adjoint(X**2), printer='presentation') == \ + '(X2)†' + assert mathml(Adjoint(X)**2, printer='presentation') == \ + '(X†)2' + assert mathml(Adjoint(Inverse(X)), printer='presentation') == \ + '(X-1)†' + assert mathml(Inverse(Adjoint(X)), printer='presentation') == \ + '(X†)-1' + assert mathml(Adjoint(Transpose(X)), printer='presentation') == \ + '(XT)†' + assert mathml(Transpose(Adjoint(X)), printer='presentation') == \ + '(X†)T' + assert mathml(Transpose(Adjoint(X) + Y), printer='presentation') == \ + '(X†' \ + '+Y)T' + assert mathml(Transpose(X), printer='presentation') == \ + 'XT' + assert mathml(Transpose(X + Y), printer='presentation') == \ + '(X+Y)T' + + +def test_mathml_special_matrices(): + from sympy.matrices import Identity, ZeroMatrix, OneMatrix + assert mathml(Identity(4), printer='presentation') == '𝕀' + assert mathml(ZeroMatrix(2, 2), printer='presentation') == '𝟘' + assert mathml(OneMatrix(2, 2), printer='presentation') == '𝟙' + +def test_mathml_piecewise(): + from sympy.functions.elementary.piecewise import Piecewise + # Content MathML + assert mathml(Piecewise((x, x <= 1), (x**2, True))) == \ + 'xx1x2' + + raises(ValueError, lambda: mathml(Piecewise((x, x <= 1)))) + + +def test_issue_17857(): + assert mathml(Range(-oo, oo), printer='presentation') == \ + '{…,-1,0,1,…}' + assert mathml(Range(oo, -oo, -1), printer='presentation') == \ + '{…,1,0,-1,…}' + + +def test_float_roundtrip(): + x = sympify(0.8975979010256552) + y = float(mp.doprint(x).strip('')) + assert x == y + + +def test_content_mathml_disable_split_super_sub(): + mp = MathMLContentPrinter() + assert mp.doprint(Symbol('u_b')) == 'ub' + mp = MathMLContentPrinter({'disable_split_super_sub': False}) + assert mp.doprint(Symbol('u_b')) == 'ub' + mp = MathMLContentPrinter({'disable_split_super_sub': True}) + assert mp.doprint(Symbol('u_b')) == 'u_b' + +def test_presentation_mathml_disable_split_super_sub(): + mpp = MathMLPresentationPrinter() + assert mpp.doprint(Symbol('u_b')) == 'ub' + mpp = MathMLPresentationPrinter({'disable_split_super_sub': False}) + assert mpp.doprint(Symbol('u_b')) == 'ub' + mpp = MathMLPresentationPrinter({'disable_split_super_sub': True}) + assert mpp.doprint(Symbol('u_b')) == 'u_b' diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_numpy.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_numpy.py new file mode 100644 index 0000000000000000000000000000000000000000..fee1c6bd95e54790a048220f37b8e5de79017d2f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_numpy.py @@ -0,0 +1,381 @@ +from sympy.concrete.summations import Sum +from sympy.core.mod import Mod +from sympy.core.relational import (Equality, Unequality) +from sympy.core.symbol import Symbol +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.special.gamma_functions import polygamma +from sympy.functions.special.error_functions import (Si, Ci) +from sympy.matrices import Matrix +from sympy.matrices.expressions.blockmatrix import BlockMatrix +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.matrices.expressions.special import Identity +from sympy.utilities.lambdify import lambdify +from sympy import symbols, Min, Max + +from sympy.abc import x, i, j, a, b, c, d +from sympy.core import Pow +from sympy.codegen.matrix_nodes import MatrixSolve +from sympy.codegen.numpy_nodes import logaddexp, logaddexp2 +from sympy.codegen.cfunctions import log1p, expm1, hypot, log10, exp2, log2, Sqrt +from sympy.tensor.array import Array +from sympy.tensor.array.expressions.array_expressions import ArrayTensorProduct, ArrayAdd, \ + PermuteDims, ArrayDiagonal +from sympy.printing.numpy import NumPyPrinter, SciPyPrinter, _numpy_known_constants, \ + _numpy_known_functions, _scipy_known_constants, _scipy_known_functions +from sympy.tensor.array.expressions.from_matrix_to_array import convert_matrix_to_array + +from sympy.testing.pytest import skip, raises +from sympy.external import import_module + +np = import_module('numpy') +jax = import_module('jax') + +if np: + deafult_float_info = np.finfo(np.array([]).dtype) + NUMPY_DEFAULT_EPSILON = deafult_float_info.eps + +def test_numpy_piecewise_regression(): + """ + NumPyPrinter needs to print Piecewise()'s choicelist as a list to avoid + breaking compatibility with numpy 1.8. This is not necessary in numpy 1.9+. + See gh-9747 and gh-9749 for details. + """ + printer = NumPyPrinter() + p = Piecewise((1, x < 0), (0, True)) + assert printer.doprint(p) == \ + 'numpy.select([numpy.less(x, 0),True], [1,0], default=numpy.nan)' + assert printer.module_imports == {'numpy': {'select', 'less', 'nan'}} + +def test_numpy_logaddexp(): + lae = logaddexp(a, b) + assert NumPyPrinter().doprint(lae) == 'numpy.logaddexp(a, b)' + lae2 = logaddexp2(a, b) + assert NumPyPrinter().doprint(lae2) == 'numpy.logaddexp2(a, b)' + + +def test_sum(): + if not np: + skip("NumPy not installed") + + s = Sum(x ** i, (i, a, b)) + f = lambdify((a, b, x), s, 'numpy') + + a_, b_ = 0, 10 + x_ = np.linspace(-1, +1, 10) + assert np.allclose(f(a_, b_, x_), sum(x_ ** i_ for i_ in range(a_, b_ + 1))) + + s = Sum(i * x, (i, a, b)) + f = lambdify((a, b, x), s, 'numpy') + + a_, b_ = 0, 10 + x_ = np.linspace(-1, +1, 10) + assert np.allclose(f(a_, b_, x_), sum(i_ * x_ for i_ in range(a_, b_ + 1))) + + +def test_multiple_sums(): + if not np: + skip("NumPy not installed") + + s = Sum((x + j) * i, (i, a, b), (j, c, d)) + f = lambdify((a, b, c, d, x), s, 'numpy') + + a_, b_ = 0, 10 + c_, d_ = 11, 21 + x_ = np.linspace(-1, +1, 10) + assert np.allclose(f(a_, b_, c_, d_, x_), + sum((x_ + j_) * i_ for i_ in range(a_, b_ + 1) for j_ in range(c_, d_ + 1))) + + +def test_codegen_einsum(): + if not np: + skip("NumPy not installed") + + M = MatrixSymbol("M", 2, 2) + N = MatrixSymbol("N", 2, 2) + + cg = convert_matrix_to_array(M * N) + f = lambdify((M, N), cg, 'numpy') + + ma = np.array([[1, 2], [3, 4]]) + mb = np.array([[1,-2], [-1, 3]]) + assert (f(ma, mb) == np.matmul(ma, mb)).all() + + +def test_codegen_extra(): + if not np: + skip("NumPy not installed") + + M = MatrixSymbol("M", 2, 2) + N = MatrixSymbol("N", 2, 2) + P = MatrixSymbol("P", 2, 2) + Q = MatrixSymbol("Q", 2, 2) + ma = np.array([[1, 2], [3, 4]]) + mb = np.array([[1,-2], [-1, 3]]) + mc = np.array([[2, 0], [1, 2]]) + md = np.array([[1,-1], [4, 7]]) + + cg = ArrayTensorProduct(M, N) + f = lambdify((M, N), cg, 'numpy') + assert (f(ma, mb) == np.einsum(ma, [0, 1], mb, [2, 3])).all() + + cg = ArrayAdd(M, N) + f = lambdify((M, N), cg, 'numpy') + assert (f(ma, mb) == ma+mb).all() + + cg = ArrayAdd(M, N, P) + f = lambdify((M, N, P), cg, 'numpy') + assert (f(ma, mb, mc) == ma+mb+mc).all() + + cg = ArrayAdd(M, N, P, Q) + f = lambdify((M, N, P, Q), cg, 'numpy') + assert (f(ma, mb, mc, md) == ma+mb+mc+md).all() + + cg = PermuteDims(M, [1, 0]) + f = lambdify((M,), cg, 'numpy') + assert (f(ma) == ma.T).all() + + cg = PermuteDims(ArrayTensorProduct(M, N), [1, 2, 3, 0]) + f = lambdify((M, N), cg, 'numpy') + assert (f(ma, mb) == np.transpose(np.einsum(ma, [0, 1], mb, [2, 3]), (1, 2, 3, 0))).all() + + cg = ArrayDiagonal(ArrayTensorProduct(M, N), (1, 2)) + f = lambdify((M, N), cg, 'numpy') + assert (f(ma, mb) == np.diagonal(np.einsum(ma, [0, 1], mb, [2, 3]), axis1=1, axis2=2)).all() + + +def test_relational(): + if not np: + skip("NumPy not installed") + + e = Equality(x, 1) + + f = lambdify((x,), e) + x_ = np.array([0, 1, 2]) + assert np.array_equal(f(x_), [False, True, False]) + + e = Unequality(x, 1) + + f = lambdify((x,), e) + x_ = np.array([0, 1, 2]) + assert np.array_equal(f(x_), [True, False, True]) + + e = (x < 1) + + f = lambdify((x,), e) + x_ = np.array([0, 1, 2]) + assert np.array_equal(f(x_), [True, False, False]) + + e = (x <= 1) + + f = lambdify((x,), e) + x_ = np.array([0, 1, 2]) + assert np.array_equal(f(x_), [True, True, False]) + + e = (x > 1) + + f = lambdify((x,), e) + x_ = np.array([0, 1, 2]) + assert np.array_equal(f(x_), [False, False, True]) + + e = (x >= 1) + + f = lambdify((x,), e) + x_ = np.array([0, 1, 2]) + assert np.array_equal(f(x_), [False, True, True]) + + +def test_mod(): + if not np: + skip("NumPy not installed") + + e = Mod(a, b) + f = lambdify((a, b), e) + + a_ = np.array([0, 1, 2, 3]) + b_ = 2 + assert np.array_equal(f(a_, b_), [0, 1, 0, 1]) + + a_ = np.array([0, 1, 2, 3]) + b_ = np.array([2, 2, 2, 2]) + assert np.array_equal(f(a_, b_), [0, 1, 0, 1]) + + a_ = np.array([2, 3, 4, 5]) + b_ = np.array([2, 3, 4, 5]) + assert np.array_equal(f(a_, b_), [0, 0, 0, 0]) + + +def test_pow(): + if not np: + skip('NumPy not installed') + + expr = Pow(2, -1, evaluate=False) + f = lambdify([], expr, 'numpy') + assert f() == 0.5 + + +def test_expm1(): + if not np: + skip("NumPy not installed") + + f = lambdify((a,), expm1(a), 'numpy') + assert abs(f(1e-10) - 1e-10 - 5e-21) <= 1e-10 * NUMPY_DEFAULT_EPSILON + + +def test_log1p(): + if not np: + skip("NumPy not installed") + + f = lambdify((a,), log1p(a), 'numpy') + assert abs(f(1e-99) - 1e-99) <= 1e-99 * NUMPY_DEFAULT_EPSILON + +def test_hypot(): + if not np: + skip("NumPy not installed") + assert abs(lambdify((a, b), hypot(a, b), 'numpy')(3, 4) - 5) <= NUMPY_DEFAULT_EPSILON + +def test_log10(): + if not np: + skip("NumPy not installed") + assert abs(lambdify((a,), log10(a), 'numpy')(100) - 2) <= NUMPY_DEFAULT_EPSILON + + +def test_exp2(): + if not np: + skip("NumPy not installed") + assert abs(lambdify((a,), exp2(a), 'numpy')(5) - 32) <= NUMPY_DEFAULT_EPSILON + + +def test_log2(): + if not np: + skip("NumPy not installed") + assert abs(lambdify((a,), log2(a), 'numpy')(256) - 8) <= NUMPY_DEFAULT_EPSILON + + +def test_Sqrt(): + if not np: + skip("NumPy not installed") + assert abs(lambdify((a,), Sqrt(a), 'numpy')(4) - 2) <= NUMPY_DEFAULT_EPSILON + + +def test_sqrt(): + if not np: + skip("NumPy not installed") + assert abs(lambdify((a,), sqrt(a), 'numpy')(4) - 2) <= NUMPY_DEFAULT_EPSILON + + +def test_matsolve(): + if not np: + skip("NumPy not installed") + + M = MatrixSymbol("M", 3, 3) + x = MatrixSymbol("x", 3, 1) + + expr = M**(-1) * x + x + matsolve_expr = MatrixSolve(M, x) + x + + f = lambdify((M, x), expr) + f_matsolve = lambdify((M, x), matsolve_expr) + + m0 = np.array([[1, 2, 3], [3, 2, 5], [5, 6, 7]]) + assert np.linalg.matrix_rank(m0) == 3 + + x0 = np.array([3, 4, 5]) + + assert np.allclose(f_matsolve(m0, x0), f(m0, x0)) + + +def test_16857(): + if not np: + skip("NumPy not installed") + + a_1 = MatrixSymbol('a_1', 10, 3) + a_2 = MatrixSymbol('a_2', 10, 3) + a_3 = MatrixSymbol('a_3', 10, 3) + a_4 = MatrixSymbol('a_4', 10, 3) + A = BlockMatrix([[a_1, a_2], [a_3, a_4]]) + assert A.shape == (20, 6) + + printer = NumPyPrinter() + assert printer.doprint(A) == 'numpy.block([[a_1, a_2], [a_3, a_4]])' + + +def test_issue_17006(): + if not np: + skip("NumPy not installed") + + M = MatrixSymbol("M", 2, 2) + + f = lambdify(M, M + Identity(2)) + ma = np.array([[1, 2], [3, 4]]) + mr = np.array([[2, 2], [3, 5]]) + + assert (f(ma) == mr).all() + + from sympy.core.symbol import symbols + n = symbols('n', integer=True) + N = MatrixSymbol("M", n, n) + raises(NotImplementedError, lambda: lambdify(N, N + Identity(n))) + +def test_jax_tuple_compatibility(): + if not jax: + skip("Jax not installed") + + x, y, z = symbols('x y z') + expr = Max(x, y, z) + Min(x, y, z) + func = lambdify((x, y, z), expr, 'jax') + input_tuple1, input_tuple2 = (1, 2, 3), (4, 5, 6) + input_array1, input_array2 = jax.numpy.asarray(input_tuple1), jax.numpy.asarray(input_tuple2) + assert np.allclose(func(*input_tuple1), func(*input_array1)) + assert np.allclose(func(*input_tuple2), func(*input_array2)) + +def test_numpy_array(): + p = NumPyPrinter() + assert p.doprint(Array([[1, 2], [3, 5]])) == 'numpy.array([[1, 2], [3, 5]])' + assert p.doprint(Array([1, 2])) == 'numpy.array([1, 2])' + assert p.doprint(Array([[[1, 2, 3]]])) == 'numpy.array([[[1, 2, 3]]])' + assert p.doprint(Array([], (0,))) == 'numpy.zeros((0,))' + assert p.doprint(Array([], (0, 0))) == 'numpy.zeros((0, 0))' + assert p.doprint(Array([], (0, 1))) == 'numpy.zeros((0, 1))' + assert p.doprint(Array([], (1, 0))) == 'numpy.zeros((1, 0))' + assert p.doprint(Array([1], ())) == 'numpy.array(1)' + +def test_numpy_matrix(): + p = NumPyPrinter() + assert p.doprint(Matrix([[1, 2], [3, 5]])) == 'numpy.array([[1, 2], [3, 5]])' + assert p.doprint(Matrix([1, 2])) == 'numpy.array([[1], [2]])' + assert p.doprint(Matrix(0, 0, [])) == 'numpy.zeros((0, 0))' + assert p.doprint(Matrix(0, 1, [])) == 'numpy.zeros((0, 1))' + assert p.doprint(Matrix(1, 0, [])) == 'numpy.zeros((1, 0))' + +def test_numpy_known_funcs_consts(): + assert _numpy_known_constants['NaN'] == 'numpy.nan' + assert _numpy_known_constants['EulerGamma'] == 'numpy.euler_gamma' + + assert _numpy_known_functions['acos'] == 'numpy.arccos' + assert _numpy_known_functions['log'] == 'numpy.log' + +def test_scipy_known_funcs_consts(): + assert _scipy_known_constants['GoldenRatio'] == 'scipy.constants.golden_ratio' + assert _scipy_known_constants['Pi'] == 'scipy.constants.pi' + + assert _scipy_known_functions['erf'] == 'scipy.special.erf' + assert _scipy_known_functions['factorial'] == 'scipy.special.factorial' + +def test_numpy_print_methods(): + prntr = NumPyPrinter() + assert hasattr(prntr, '_print_acos') + assert hasattr(prntr, '_print_log') + +def test_scipy_print_methods(): + prntr = SciPyPrinter() + assert hasattr(prntr, '_print_acos') + assert hasattr(prntr, '_print_log') + assert hasattr(prntr, '_print_erf') + assert hasattr(prntr, '_print_factorial') + assert hasattr(prntr, '_print_chebyshevt') + k = Symbol('k', integer=True, nonnegative=True) + x = Symbol('x', real=True) + assert prntr.doprint(polygamma(k, x)) == "scipy.special.polygamma(k, x)" + assert prntr.doprint(Si(x)) == "scipy.special.sici(x)[0]" + assert prntr.doprint(Ci(x)) == "scipy.special.sici(x)[1]" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_octave.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_octave.py new file mode 100644 index 0000000000000000000000000000000000000000..1aba318f873c48ec702f1b4e3a6cc047f75d647d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_octave.py @@ -0,0 +1,515 @@ +from sympy.core import (S, pi, oo, symbols, Function, Rational, Integer, + Tuple, Symbol, EulerGamma, GoldenRatio, Catalan, + Lambda, Mul, Pow, Mod, Eq, Ne, Le, Lt, Gt, Ge) +from sympy.codegen.matrix_nodes import MatrixSolve +from sympy.functions import (arg, atan2, bernoulli, beta, ceiling, chebyshevu, + chebyshevt, conjugate, DiracDelta, exp, expint, + factorial, floor, harmonic, Heaviside, im, + laguerre, LambertW, log, Max, Min, Piecewise, + polylog, re, RisingFactorial, sign, sinc, sqrt, + zeta, binomial, legendre, dirichlet_eta, + riemann_xi) +from sympy.functions import (sin, cos, tan, cot, sec, csc, asin, acos, acot, + atan, asec, acsc, sinh, cosh, tanh, coth, csch, + sech, asinh, acosh, atanh, acoth, asech, acsch) +from sympy.testing.pytest import raises, XFAIL +from sympy.utilities.lambdify import implemented_function +from sympy.matrices import (eye, Matrix, MatrixSymbol, Identity, + HadamardProduct, SparseMatrix, HadamardPower) +from sympy.functions.special.bessel import (jn, yn, besselj, bessely, besseli, + besselk, hankel1, hankel2, airyai, + airybi, airyaiprime, airybiprime) +from sympy.functions.special.gamma_functions import (gamma, lowergamma, + uppergamma, loggamma, + polygamma) +from sympy.functions.special.error_functions import (Chi, Ci, erf, erfc, erfi, + erfcinv, erfinv, fresnelc, + fresnels, li, Shi, Si, Li, + erf2, Ei) +from sympy.printing.octave import octave_code, octave_code as mcode + +x, y, z = symbols('x,y,z') + + +def test_Integer(): + assert mcode(Integer(67)) == "67" + assert mcode(Integer(-1)) == "-1" + + +def test_Rational(): + assert mcode(Rational(3, 7)) == "3/7" + assert mcode(Rational(18, 9)) == "2" + assert mcode(Rational(3, -7)) == "-3/7" + assert mcode(Rational(-3, -7)) == "3/7" + assert mcode(x + Rational(3, 7)) == "x + 3/7" + assert mcode(Rational(3, 7)*x) == "3*x/7" + + +def test_Relational(): + assert mcode(Eq(x, y)) == "x == y" + assert mcode(Ne(x, y)) == "x != y" + assert mcode(Le(x, y)) == "x <= y" + assert mcode(Lt(x, y)) == "x < y" + assert mcode(Gt(x, y)) == "x > y" + assert mcode(Ge(x, y)) == "x >= y" + + +def test_Function(): + assert mcode(sin(x) ** cos(x)) == "sin(x).^cos(x)" + assert mcode(sign(x)) == "sign(x)" + assert mcode(exp(x)) == "exp(x)" + assert mcode(log(x)) == "log(x)" + assert mcode(factorial(x)) == "factorial(x)" + assert mcode(floor(x)) == "floor(x)" + assert mcode(atan2(y, x)) == "atan2(y, x)" + assert mcode(beta(x, y)) == 'beta(x, y)' + assert mcode(polylog(x, y)) == 'polylog(x, y)' + assert mcode(harmonic(x)) == 'harmonic(x)' + assert mcode(bernoulli(x)) == "bernoulli(x)" + assert mcode(bernoulli(x, y)) == "bernoulli(x, y)" + assert mcode(legendre(x, y)) == "legendre(x, y)" + + +def test_Function_change_name(): + assert mcode(abs(x)) == "abs(x)" + assert mcode(ceiling(x)) == "ceil(x)" + assert mcode(arg(x)) == "angle(x)" + assert mcode(im(x)) == "imag(x)" + assert mcode(re(x)) == "real(x)" + assert mcode(conjugate(x)) == "conj(x)" + assert mcode(chebyshevt(y, x)) == "chebyshevT(y, x)" + assert mcode(chebyshevu(y, x)) == "chebyshevU(y, x)" + assert mcode(laguerre(x, y)) == "laguerreL(x, y)" + assert mcode(Chi(x)) == "coshint(x)" + assert mcode(Shi(x)) == "sinhint(x)" + assert mcode(Ci(x)) == "cosint(x)" + assert mcode(Si(x)) == "sinint(x)" + assert mcode(li(x)) == "logint(x)" + assert mcode(loggamma(x)) == "gammaln(x)" + assert mcode(polygamma(x, y)) == "psi(x, y)" + assert mcode(RisingFactorial(x, y)) == "pochhammer(x, y)" + assert mcode(DiracDelta(x)) == "dirac(x)" + assert mcode(DiracDelta(x, 3)) == "dirac(3, x)" + assert mcode(Heaviside(x)) == "heaviside(x, 1/2)" + assert mcode(Heaviside(x, y)) == "heaviside(x, y)" + assert mcode(binomial(x, y)) == "bincoeff(x, y)" + assert mcode(Mod(x, y)) == "mod(x, y)" + + +def test_minmax(): + assert mcode(Max(x, y) + Min(x, y)) == "max(x, y) + min(x, y)" + assert mcode(Max(x, y, z)) == "max(x, max(y, z))" + assert mcode(Min(x, y, z)) == "min(x, min(y, z))" + + +def test_Pow(): + assert mcode(x**3) == "x.^3" + assert mcode(x**(y**3)) == "x.^(y.^3)" + assert mcode(x**Rational(2, 3)) == 'x.^(2/3)' + g = implemented_function('g', Lambda(x, 2*x)) + assert mcode(1/(g(x)*3.5)**(x - y**x)/(x**2 + y)) == \ + "(3.5*2*x).^(-x + y.^x)./(x.^2 + y)" + # For issue 14160 + assert mcode(Mul(-2, x, Pow(Mul(y,y,evaluate=False), -1, evaluate=False), + evaluate=False)) == '-2*x./(y.*y)' + + +def test_basic_ops(): + assert mcode(x*y) == "x.*y" + assert mcode(x + y) == "x + y" + assert mcode(x - y) == "x - y" + assert mcode(-x) == "-x" + + +def test_1_over_x_and_sqrt(): + # 1.0 and 0.5 would do something different in regular StrPrinter, + # but these are exact in IEEE floating point so no different here. + assert mcode(1/x) == '1./x' + assert mcode(x**-1) == mcode(x**-1.0) == '1./x' + assert mcode(1/sqrt(x)) == '1./sqrt(x)' + assert mcode(x**-S.Half) == mcode(x**-0.5) == '1./sqrt(x)' + assert mcode(sqrt(x)) == 'sqrt(x)' + assert mcode(x**S.Half) == mcode(x**0.5) == 'sqrt(x)' + assert mcode(1/pi) == '1/pi' + assert mcode(pi**-1) == mcode(pi**-1.0) == '1/pi' + assert mcode(pi**-0.5) == '1/sqrt(pi)' + + +def test_mix_number_mult_symbols(): + assert mcode(3*x) == "3*x" + assert mcode(pi*x) == "pi*x" + assert mcode(3/x) == "3./x" + assert mcode(pi/x) == "pi./x" + assert mcode(x/3) == "x/3" + assert mcode(x/pi) == "x/pi" + assert mcode(x*y) == "x.*y" + assert mcode(3*x*y) == "3*x.*y" + assert mcode(3*pi*x*y) == "3*pi*x.*y" + assert mcode(x/y) == "x./y" + assert mcode(3*x/y) == "3*x./y" + assert mcode(x*y/z) == "x.*y./z" + assert mcode(x/y*z) == "x.*z./y" + assert mcode(1/x/y) == "1./(x.*y)" + assert mcode(2*pi*x/y/z) == "2*pi*x./(y.*z)" + assert mcode(3*pi/x) == "3*pi./x" + assert mcode(S(3)/5) == "3/5" + assert mcode(S(3)/5*x) == "3*x/5" + assert mcode(x/y/z) == "x./(y.*z)" + assert mcode((x+y)/z) == "(x + y)./z" + assert mcode((x+y)/(z+x)) == "(x + y)./(x + z)" + assert mcode((x+y)/EulerGamma) == "(x + y)/%s" % EulerGamma.evalf(17) + assert mcode(x/3/pi) == "x/(3*pi)" + assert mcode(S(3)/5*x*y/pi) == "3*x.*y/(5*pi)" + + +def test_mix_number_pow_symbols(): + assert mcode(pi**3) == 'pi^3' + assert mcode(x**2) == 'x.^2' + assert mcode(x**(pi**3)) == 'x.^(pi^3)' + assert mcode(x**y) == 'x.^y' + assert mcode(x**(y**z)) == 'x.^(y.^z)' + assert mcode((x**y)**z) == '(x.^y).^z' + + +def test_imag(): + I = S('I') + assert mcode(I) == "1i" + assert mcode(5*I) == "5i" + assert mcode((S(3)/2)*I) == "3*1i/2" + assert mcode(3+4*I) == "3 + 4i" + assert mcode(sqrt(3)*I) == "sqrt(3)*1i" + + +def test_constants(): + assert mcode(pi) == "pi" + assert mcode(oo) == "inf" + assert mcode(-oo) == "-inf" + assert mcode(S.NegativeInfinity) == "-inf" + assert mcode(S.NaN) == "NaN" + assert mcode(S.Exp1) == "exp(1)" + assert mcode(exp(1)) == "exp(1)" + + +def test_constants_other(): + assert mcode(2*GoldenRatio) == "2*(1+sqrt(5))/2" + assert mcode(2*Catalan) == "2*%s" % Catalan.evalf(17) + assert mcode(2*EulerGamma) == "2*%s" % EulerGamma.evalf(17) + + +def test_boolean(): + assert mcode(x & y) == "x & y" + assert mcode(x | y) == "x | y" + assert mcode(~x) == "~x" + assert mcode(x & y & z) == "x & y & z" + assert mcode(x | y | z) == "x | y | z" + assert mcode((x & y) | z) == "z | x & y" + assert mcode((x | y) & z) == "z & (x | y)" + + +def test_KroneckerDelta(): + from sympy.functions import KroneckerDelta + assert mcode(KroneckerDelta(x, y)) == "double(x == y)" + assert mcode(KroneckerDelta(x, y + 1)) == "double(x == (y + 1))" + assert mcode(KroneckerDelta(2**x, y)) == "double((2.^x) == y)" + + +def test_Matrices(): + assert mcode(Matrix(1, 1, [10])) == "10" + A = Matrix([[1, sin(x/2), abs(x)], + [0, 1, pi], + [0, exp(1), ceiling(x)]]) + expected = "[1 sin(x/2) abs(x); 0 1 pi; 0 exp(1) ceil(x)]" + assert mcode(A) == expected + # row and columns + assert mcode(A[:,0]) == "[1; 0; 0]" + assert mcode(A[0,:]) == "[1 sin(x/2) abs(x)]" + # empty matrices + assert mcode(Matrix(0, 0, [])) == '[]' + assert mcode(Matrix(0, 3, [])) == 'zeros(0, 3)' + # annoying to read but correct + assert mcode(Matrix([[x, x - y, -y]])) == "[x x - y -y]" + + +def test_vector_entries_hadamard(): + # For a row or column, user might to use the other dimension + A = Matrix([[1, sin(2/x), 3*pi/x/5]]) + assert mcode(A) == "[1 sin(2./x) 3*pi./(5*x)]" + assert mcode(A.T) == "[1; sin(2./x); 3*pi./(5*x)]" + + +@XFAIL +def test_Matrices_entries_not_hadamard(): + # For Matrix with col >= 2, row >= 2, they need to be scalars + # FIXME: is it worth worrying about this? Its not wrong, just + # leave it user's responsibility to put scalar data for x. + A = Matrix([[1, sin(2/x), 3*pi/x/5], [1, 2, x*y]]) + expected = ("[1 sin(2/x) 3*pi/(5*x);\n" + "1 2 x*y]") # <- we give x.*y + assert mcode(A) == expected + + +def test_MatrixSymbol(): + n = Symbol('n', integer=True) + A = MatrixSymbol('A', n, n) + B = MatrixSymbol('B', n, n) + assert mcode(A*B) == "A*B" + assert mcode(B*A) == "B*A" + assert mcode(2*A*B) == "2*A*B" + assert mcode(B*2*A) == "2*B*A" + assert mcode(A*(B + 3*Identity(n))) == "A*(3*eye(n) + B)" + assert mcode(A**(x**2)) == "A^(x.^2)" + assert mcode(A**3) == "A^3" + assert mcode(A**S.Half) == "A^(1/2)" + + +def test_MatrixSolve(): + n = Symbol('n', integer=True) + A = MatrixSymbol('A', n, n) + x = MatrixSymbol('x', n, 1) + assert mcode(MatrixSolve(A, x)) == "A \\ x" + +def test_special_matrices(): + assert mcode(6*Identity(3)) == "6*eye(3)" + + +def test_containers(): + assert mcode([1, 2, 3, [4, 5, [6, 7]], 8, [9, 10], 11]) == \ + "{1, 2, 3, {4, 5, {6, 7}}, 8, {9, 10}, 11}" + assert mcode((1, 2, (3, 4))) == "{1, 2, {3, 4}}" + assert mcode([1]) == "{1}" + assert mcode((1,)) == "{1}" + assert mcode(Tuple(*[1, 2, 3])) == "{1, 2, 3}" + assert mcode((1, x*y, (3, x**2))) == "{1, x.*y, {3, x.^2}}" + # scalar, matrix, empty matrix and empty list + assert mcode((1, eye(3), Matrix(0, 0, []), [])) == "{1, [1 0 0; 0 1 0; 0 0 1], [], {}}" + + +def test_octave_noninline(): + source = mcode((x+y)/Catalan, assign_to='me', inline=False) + expected = ( + "Catalan = %s;\n" + "me = (x + y)/Catalan;" + ) % Catalan.evalf(17) + assert source == expected + + +def test_octave_piecewise(): + expr = Piecewise((x, x < 1), (x**2, True)) + assert mcode(expr) == "((x < 1).*(x) + (~(x < 1)).*(x.^2))" + assert mcode(expr, assign_to="r") == ( + "r = ((x < 1).*(x) + (~(x < 1)).*(x.^2));") + assert mcode(expr, assign_to="r", inline=False) == ( + "if (x < 1)\n" + " r = x;\n" + "else\n" + " r = x.^2;\n" + "end") + expr = Piecewise((x**2, x < 1), (x**3, x < 2), (x**4, x < 3), (x**5, True)) + expected = ("((x < 1).*(x.^2) + (~(x < 1)).*( ...\n" + "(x < 2).*(x.^3) + (~(x < 2)).*( ...\n" + "(x < 3).*(x.^4) + (~(x < 3)).*(x.^5))))") + assert mcode(expr) == expected + assert mcode(expr, assign_to="r") == "r = " + expected + ";" + assert mcode(expr, assign_to="r", inline=False) == ( + "if (x < 1)\n" + " r = x.^2;\n" + "elseif (x < 2)\n" + " r = x.^3;\n" + "elseif (x < 3)\n" + " r = x.^4;\n" + "else\n" + " r = x.^5;\n" + "end") + # Check that Piecewise without a True (default) condition error + expr = Piecewise((x, x < 1), (x**2, x > 1), (sin(x), x > 0)) + raises(ValueError, lambda: mcode(expr)) + + +def test_octave_piecewise_times_const(): + pw = Piecewise((x, x < 1), (x**2, True)) + assert mcode(2*pw) == "2*((x < 1).*(x) + (~(x < 1)).*(x.^2))" + assert mcode(pw/x) == "((x < 1).*(x) + (~(x < 1)).*(x.^2))./x" + assert mcode(pw/(x*y)) == "((x < 1).*(x) + (~(x < 1)).*(x.^2))./(x.*y)" + assert mcode(pw/3) == "((x < 1).*(x) + (~(x < 1)).*(x.^2))/3" + + +def test_octave_matrix_assign_to(): + A = Matrix([[1, 2, 3]]) + assert mcode(A, assign_to='a') == "a = [1 2 3];" + A = Matrix([[1, 2], [3, 4]]) + assert mcode(A, assign_to='A') == "A = [1 2; 3 4];" + + +def test_octave_matrix_assign_to_more(): + # assigning to Symbol or MatrixSymbol requires lhs/rhs match + A = Matrix([[1, 2, 3]]) + B = MatrixSymbol('B', 1, 3) + C = MatrixSymbol('C', 2, 3) + assert mcode(A, assign_to=B) == "B = [1 2 3];" + raises(ValueError, lambda: mcode(A, assign_to=x)) + raises(ValueError, lambda: mcode(A, assign_to=C)) + + +def test_octave_matrix_1x1(): + A = Matrix([[3]]) + B = MatrixSymbol('B', 1, 1) + C = MatrixSymbol('C', 1, 2) + assert mcode(A, assign_to=B) == "B = 3;" + # FIXME? + #assert mcode(A, assign_to=x) == "x = 3;" + raises(ValueError, lambda: mcode(A, assign_to=C)) + + +def test_octave_matrix_elements(): + A = Matrix([[x, 2, x*y]]) + assert mcode(A[0, 0]**2 + A[0, 1] + A[0, 2]) == "x.^2 + x.*y + 2" + A = MatrixSymbol('AA', 1, 3) + assert mcode(A) == "AA" + assert mcode(A[0, 0]**2 + sin(A[0,1]) + A[0,2]) == \ + "sin(AA(1, 2)) + AA(1, 1).^2 + AA(1, 3)" + assert mcode(sum(A)) == "AA(1, 1) + AA(1, 2) + AA(1, 3)" + + +def test_octave_boolean(): + assert mcode(True) == "true" + assert mcode(S.true) == "true" + assert mcode(False) == "false" + assert mcode(S.false) == "false" + + +def test_octave_not_supported(): + with raises(NotImplementedError): + mcode(S.ComplexInfinity) + f = Function('f') + assert mcode(f(x).diff(x), strict=False) == ( + "% Not supported in Octave:\n" + "% Derivative\n" + "Derivative(f(x), x)" + ) + + +def test_octave_not_supported_not_on_whitelist(): + from sympy.functions.special.polynomials import assoc_laguerre + with raises(NotImplementedError): + mcode(assoc_laguerre(x, y, z)) + + +def test_octave_expint(): + assert mcode(expint(1, x)) == "expint(x)" + with raises(NotImplementedError): + mcode(expint(2, x)) + assert mcode(expint(y, x), strict=False) == ( + "% Not supported in Octave:\n" + "% expint\n" + "expint(y, x)" + ) + + +def test_trick_indent_with_end_else_words(): + # words starting with "end" or "else" do not confuse the indenter + t1 = S('endless') + t2 = S('elsewhere') + pw = Piecewise((t1, x < 0), (t2, x <= 1), (1, True)) + assert mcode(pw, inline=False) == ( + "if (x < 0)\n" + " endless\n" + "elseif (x <= 1)\n" + " elsewhere\n" + "else\n" + " 1\n" + "end") + + +def test_hadamard(): + A = MatrixSymbol('A', 3, 3) + B = MatrixSymbol('B', 3, 3) + v = MatrixSymbol('v', 3, 1) + h = MatrixSymbol('h', 1, 3) + C = HadamardProduct(A, B) + n = Symbol('n') + assert mcode(C) == "A.*B" + assert mcode(C*v) == "(A.*B)*v" + assert mcode(h*C*v) == "h*(A.*B)*v" + assert mcode(C*A) == "(A.*B)*A" + # mixing Hadamard and scalar strange b/c we vectorize scalars + assert mcode(C*x*y) == "(x.*y)*(A.*B)" + + # Testing HadamardPower: + assert mcode(HadamardPower(A, n)) == "A.**n" + assert mcode(HadamardPower(A, 1+n)) == "A.**(n + 1)" + assert mcode(HadamardPower(A*B.T, 1+n)) == "(A*B.T).**(n + 1)" + + +def test_sparse(): + M = SparseMatrix(5, 6, {}) + M[2, 2] = 10 + M[1, 2] = 20 + M[1, 3] = 22 + M[0, 3] = 30 + M[3, 0] = x*y + assert mcode(M) == ( + "sparse([4 2 3 1 2], [1 3 3 4 4], [x.*y 20 10 30 22], 5, 6)" + ) + + +def test_sinc(): + assert mcode(sinc(x)) == 'sinc(x/pi)' + assert mcode(sinc(x + 3)) == 'sinc((x + 3)/pi)' + assert mcode(sinc(pi*(x + 3))) == 'sinc(x + 3)' + + +def test_trigfun(): + for f in (sin, cos, tan, cot, sec, csc, asin, acos, acot, atan, asec, acsc, + sinh, cosh, tanh, coth, csch, sech, asinh, acosh, atanh, acoth, + asech, acsch): + assert octave_code(f(x) == f.__name__ + '(x)') + + +def test_specfun(): + n = Symbol('n') + for f in [besselj, bessely, besseli, besselk]: + assert octave_code(f(n, x)) == f.__name__ + '(n, x)' + for f in (erfc, erfi, erf, erfinv, erfcinv, fresnelc, fresnels, gamma): + assert octave_code(f(x)) == f.__name__ + '(x)' + assert octave_code(hankel1(n, x)) == 'besselh(n, 1, x)' + assert octave_code(hankel2(n, x)) == 'besselh(n, 2, x)' + assert octave_code(airyai(x)) == 'airy(0, x)' + assert octave_code(airyaiprime(x)) == 'airy(1, x)' + assert octave_code(airybi(x)) == 'airy(2, x)' + assert octave_code(airybiprime(x)) == 'airy(3, x)' + assert octave_code(uppergamma(n, x)) == '(gammainc(x, n, \'upper\').*gamma(n))' + assert octave_code(lowergamma(n, x)) == '(gammainc(x, n).*gamma(n))' + assert octave_code(z**lowergamma(n, x)) == 'z.^(gammainc(x, n).*gamma(n))' + assert octave_code(jn(n, x)) == 'sqrt(2)*sqrt(pi)*sqrt(1./x).*besselj(n + 1/2, x)/2' + assert octave_code(yn(n, x)) == 'sqrt(2)*sqrt(pi)*sqrt(1./x).*bessely(n + 1/2, x)/2' + assert octave_code(LambertW(x)) == 'lambertw(x)' + assert octave_code(LambertW(x, n)) == 'lambertw(n, x)' + + # Automatic rewrite + assert octave_code(Ei(x)) == '(logint(exp(x)))' + assert octave_code(dirichlet_eta(x)) == '(((x == 1).*(log(2)) + (~(x == 1)).*((1 - 2.^(1 - x)).*zeta(x))))' + assert octave_code(riemann_xi(x)) == '(pi.^(-x/2).*x.*(x - 1).*gamma(x/2).*zeta(x)/2)' + + +def test_MatrixElement_printing(): + # test cases for issue #11821 + A = MatrixSymbol("A", 1, 3) + B = MatrixSymbol("B", 1, 3) + C = MatrixSymbol("C", 1, 3) + + assert mcode(A[0, 0]) == "A(1, 1)" + assert mcode(3 * A[0, 0]) == "3*A(1, 1)" + + F = C[0, 0].subs(C, A - B) + assert mcode(F) == "(A - B)(1, 1)" + + +def test_zeta_printing_issue_14820(): + assert octave_code(zeta(x)) == 'zeta(x)' + with raises(NotImplementedError): + octave_code(zeta(x, y)) + + +def test_automatic_rewrite(): + assert octave_code(Li(x)) == '(logint(x) - logint(2))' + assert octave_code(erf2(x, y)) == '(-erf(x) + erf(y))' diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_precedence.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_precedence.py new file mode 100644 index 0000000000000000000000000000000000000000..d08ea07483857e8c2ee7f930aa53d2dacdc58193 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_precedence.py @@ -0,0 +1,128 @@ +from sympy.concrete.products import Product +from sympy.concrete.summations import Sum +from sympy.core.function import Derivative, Function +from sympy.core.numbers import Integer, Rational, Float, oo +from sympy.core.relational import Rel +from sympy.core.symbol import symbols +from sympy.functions import sin +from sympy.integrals.integrals import Integral +from sympy.series.order import Order + +from sympy.printing.precedence import precedence, PRECEDENCE + +x, y = symbols("x,y") + + +def test_Add(): + assert precedence(x + y) == PRECEDENCE["Add"] + assert precedence(x*y + 1) == PRECEDENCE["Add"] + + +def test_Function(): + assert precedence(sin(x)) == PRECEDENCE["Func"] + +def test_Derivative(): + assert precedence(Derivative(x, y)) == PRECEDENCE["Atom"] + +def test_Integral(): + assert precedence(Integral(x, y)) == PRECEDENCE["Atom"] + + +def test_Mul(): + assert precedence(x*y) == PRECEDENCE["Mul"] + assert precedence(-x*y) == PRECEDENCE["Add"] + + +def test_Number(): + assert precedence(Integer(0)) == PRECEDENCE["Atom"] + assert precedence(Integer(1)) == PRECEDENCE["Atom"] + assert precedence(Integer(-1)) == PRECEDENCE["Add"] + assert precedence(Integer(10)) == PRECEDENCE["Atom"] + assert precedence(Rational(5, 2)) == PRECEDENCE["Mul"] + assert precedence(Rational(-5, 2)) == PRECEDENCE["Add"] + assert precedence(Float(5)) == PRECEDENCE["Atom"] + assert precedence(Float(-5)) == PRECEDENCE["Add"] + assert precedence(oo) == PRECEDENCE["Atom"] + assert precedence(-oo) == PRECEDENCE["Add"] + + +def test_Order(): + assert precedence(Order(x)) == PRECEDENCE["Atom"] + + +def test_Pow(): + assert precedence(x**y) == PRECEDENCE["Pow"] + assert precedence(-x**y) == PRECEDENCE["Add"] + assert precedence(x**-y) == PRECEDENCE["Pow"] + + +def test_Product(): + assert precedence(Product(x, (x, y, y + 1))) == PRECEDENCE["Atom"] + + +def test_Relational(): + assert precedence(Rel(x + y, y, "<")) == PRECEDENCE["Relational"] + + +def test_Sum(): + assert precedence(Sum(x, (x, y, y + 1))) == PRECEDENCE["Atom"] + + +def test_Symbol(): + assert precedence(x) == PRECEDENCE["Atom"] + + +def test_And_Or(): + # precedence relations between logical operators, ... + assert precedence(x & y) > precedence(x | y) + assert precedence(~y) > precedence(x & y) + # ... and with other operators (cfr. other programming languages) + assert precedence(x + y) > precedence(x | y) + assert precedence(x + y) > precedence(x & y) + assert precedence(x*y) > precedence(x | y) + assert precedence(x*y) > precedence(x & y) + assert precedence(~y) > precedence(x*y) + assert precedence(~y) > precedence(x - y) + # double checks + assert precedence(x & y) == PRECEDENCE["And"] + assert precedence(x | y) == PRECEDENCE["Or"] + assert precedence(~y) == PRECEDENCE["Not"] + + +def test_custom_function_precedence_comparison(): + """ + Test cases for custom functions with different precedence values, + specifically handling: + 1. Functions with precedence < PRECEDENCE["Mul"] (50) + 2. Functions with precedence = Func (70) + + Key distinction: + 1. Lower precedence functions (45) need parentheses: -2*(x F y) + 2. Higher precedence functions (70) don't: -2*x F y + """ + class LowPrecedenceF(Function): + precedence = PRECEDENCE["Mul"] - 5 + def _sympystr(self, printer): + return f"{printer._print(self.args[0])} F {printer._print(self.args[1])}" + + class HighPrecedenceF(Function): + precedence = PRECEDENCE["Func"] + def _sympystr(self, printer): + return f"{printer._print(self.args[0])} F {printer._print(self.args[1])}" + + def test_low_precedence(): + expr1 = 2 * LowPrecedenceF(x, y) + assert str(expr1) == "2*(x F y)" + + expr2 = -2 * LowPrecedenceF(x, y) + assert str(expr2) == "-2*(x F y)" + + def test_high_precedence(): + expr1 = 2 * HighPrecedenceF(x, y) + assert str(expr1) == "2*x F y" + + expr2 = -2 * HighPrecedenceF(x, y) + assert str(expr2) == "-2*x F y" + + test_low_precedence() + test_high_precedence() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_preview.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_preview.py new file mode 100644 index 0000000000000000000000000000000000000000..91771ceb0466d6b0fee00570426713d02da14872 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_preview.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +from sympy.core.relational import Eq +from sympy.core.symbol import Symbol +from sympy.functions.elementary.piecewise import Piecewise +from sympy.printing.preview import preview + +from io import BytesIO + + +def test_preview(): + x = Symbol('x') + obj = BytesIO() + try: + preview(x, output='png', viewer='BytesIO', outputbuffer=obj) + except RuntimeError: + pass # latex not installed on CI server + + +def test_preview_unicode_symbol(): + # issue 9107 + a = Symbol('α') + obj = BytesIO() + try: + preview(a, output='png', viewer='BytesIO', outputbuffer=obj) + except RuntimeError: + pass # latex not installed on CI server + + +def test_preview_latex_construct_in_expr(): + # see PR 9801 + x = Symbol('x') + pw = Piecewise((1, Eq(x, 0)), (0, True)) + obj = BytesIO() + try: + preview(pw, output='png', viewer='BytesIO', outputbuffer=obj) + except RuntimeError: + pass # latex not installed on CI server diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_pycode.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_pycode.py new file mode 100644 index 0000000000000000000000000000000000000000..2c38fe81d830149cdce6b55f15e6e07513fdd146 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_pycode.py @@ -0,0 +1,493 @@ +from sympy import Not +from sympy.codegen import Assignment +from sympy.codegen.ast import none +from sympy.codegen.cfunctions import expm1, log1p +from sympy.codegen.scipy_nodes import cosm1 +from sympy.codegen.matrix_nodes import MatrixSolve +from sympy.core import Expr, Mod, symbols, Eq, Le, Gt, zoo, oo, Rational, Pow +from sympy.core.function import Derivative +from sympy.core.numbers import pi +from sympy.core.singleton import S +from sympy.functions import acos, KroneckerDelta, Piecewise, sign, sqrt, Min, Max, cot, acsch, asec, coth, sec, log, sin, cos, tan, asin, atan, sinh, cosh, tanh, asinh, acosh, atanh +from sympy.functions.elementary.trigonometric import atan2 +from sympy.logic import And, Or +from sympy.matrices import SparseMatrix, MatrixSymbol, Identity +from sympy.printing.codeprinter import PrintMethodNotImplementedError +from sympy.printing.pycode import ( + MpmathPrinter, CmathPrinter, PythonCodePrinter, pycode, SymPyPrinter +) +from sympy.printing.tensorflow import TensorflowPrinter +from sympy.printing.numpy import NumPyPrinter, SciPyPrinter +from sympy.testing.pytest import raises, skip +from sympy.tensor import IndexedBase, Idx +from sympy.tensor.array.expressions.array_expressions import ArraySymbol, ArrayDiagonal, ArrayContraction, ZeroArray, OneArray +from sympy.external import import_module +from sympy.functions.special.gamma_functions import loggamma + + + +x, y, z = symbols('x y z') +p = IndexedBase("p") + + +def test_PythonCodePrinter(): + prntr = PythonCodePrinter() + + assert not prntr.module_imports + + assert prntr.doprint(x**y) == 'x**y' + assert prntr.doprint(Mod(x, 2)) == 'x % 2' + assert prntr.doprint(-Mod(x, y)) == '-(x % y)' + assert prntr.doprint(Mod(-x, y)) == '(-x) % y' + assert prntr.doprint(And(x, y)) == 'x and y' + assert prntr.doprint(Or(x, y)) == 'x or y' + assert prntr.doprint(1/(x+y)) == '1/(x + y)' + assert prntr.doprint(Not(x)) == 'not x' + assert not prntr.module_imports + + assert prntr.doprint(pi) == 'math.pi' + assert prntr.module_imports == {'math': {'pi'}} + + assert prntr.doprint(x**Rational(1, 2)) == 'math.sqrt(x)' + assert prntr.doprint(sqrt(x)) == 'math.sqrt(x)' + assert prntr.module_imports == {'math': {'pi', 'sqrt'}} + + assert prntr.doprint(acos(x)) == 'math.acos(x)' + assert prntr.doprint(cot(x)) == '(1/math.tan(x))' + assert prntr.doprint(coth(x)) == '((math.exp(x) + math.exp(-x))/(math.exp(x) - math.exp(-x)))' + assert prntr.doprint(asec(x)) == '(math.acos(1/x))' + assert prntr.doprint(acsch(x)) == '(math.log(math.sqrt(1 + x**(-2)) + 1/x))' + + assert prntr.doprint(Assignment(x, 2)) == 'x = 2' + assert prntr.doprint(Piecewise((1, Eq(x, 0)), + (2, x>6))) == '((1) if (x == 0) else (2) if (x > 6) else None)' + assert prntr.doprint(Piecewise((2, Le(x, 0)), + (3, Gt(x, 0)), evaluate=False)) == '((2) if (x <= 0) else'\ + ' (3) if (x > 0) else None)' + assert prntr.doprint(sign(x)) == '(0.0 if x == 0 else math.copysign(1, x))' + assert prntr.doprint(p[0, 1]) == 'p[0, 1]' + assert prntr.doprint(KroneckerDelta(x,y)) == '(1 if x == y else 0)' + + assert prntr.doprint((2,3)) == "(2, 3)" + assert prntr.doprint([2,3]) == "[2, 3]" + + assert prntr.doprint(Min(x, y)) == "min(x, y)" + assert prntr.doprint(Max(x, y)) == "max(x, y)" + + +def test_PythonCodePrinter_standard(): + prntr = PythonCodePrinter() + + assert prntr.standard == 'python3' + + raises(ValueError, lambda: PythonCodePrinter({'standard':'python4'})) + + +def test_CmathPrinter(): + p = CmathPrinter() + + assert p.doprint(sqrt(x)) == 'cmath.sqrt(x)' + assert p.doprint(log(x)) == 'cmath.log(x)' + + assert p.doprint(sin(x)) == 'cmath.sin(x)' + assert p.doprint(cos(x)) == 'cmath.cos(x)' + assert p.doprint(tan(x)) == 'cmath.tan(x)' + + assert p.doprint(asin(x)) == 'cmath.asin(x)' + assert p.doprint(acos(x)) == 'cmath.acos(x)' + assert p.doprint(atan(x)) == 'cmath.atan(x)' + + assert p.doprint(sinh(x)) == 'cmath.sinh(x)' + assert p.doprint(cosh(x)) == 'cmath.cosh(x)' + assert p.doprint(tanh(x)) == 'cmath.tanh(x)' + + assert p.doprint(asinh(x)) == 'cmath.asinh(x)' + assert p.doprint(acosh(x)) == 'cmath.acosh(x)' + assert p.doprint(atanh(x)) == 'cmath.atanh(x)' + + +def test_MpmathPrinter(): + p = MpmathPrinter() + assert p.doprint(sign(x)) == 'mpmath.sign(x)' + assert p.doprint(Rational(1, 2)) == 'mpmath.mpf(1)/mpmath.mpf(2)' + + assert p.doprint(S.Exp1) == 'mpmath.e' + assert p.doprint(S.Pi) == 'mpmath.pi' + assert p.doprint(S.GoldenRatio) == 'mpmath.phi' + assert p.doprint(S.EulerGamma) == 'mpmath.euler' + assert p.doprint(S.NaN) == 'mpmath.nan' + assert p.doprint(S.Infinity) == 'mpmath.inf' + assert p.doprint(S.NegativeInfinity) == 'mpmath.ninf' + assert p.doprint(loggamma(x)) == 'mpmath.loggamma(x)' + + +def test_NumPyPrinter(): + from sympy.core.function import Lambda + from sympy.matrices.expressions.adjoint import Adjoint + from sympy.matrices.expressions.diagonal import (DiagMatrix, DiagonalMatrix, DiagonalOf) + from sympy.matrices.expressions.funcmatrix import FunctionMatrix + from sympy.matrices.expressions.hadamard import HadamardProduct + from sympy.matrices.expressions.kronecker import KroneckerProduct + from sympy.matrices.expressions.special import (OneMatrix, ZeroMatrix) + from sympy.abc import a, b + p = NumPyPrinter() + assert p.doprint(sign(x)) == 'numpy.sign(x)' + A = MatrixSymbol("A", 2, 2) + B = MatrixSymbol("B", 2, 2) + C = MatrixSymbol("C", 1, 5) + D = MatrixSymbol("D", 3, 4) + assert p.doprint(A**(-1)) == "numpy.linalg.inv(A)" + assert p.doprint(A**5) == "numpy.linalg.matrix_power(A, 5)" + assert p.doprint(Identity(3)) == "numpy.eye(3)" + + u = MatrixSymbol('x', 2, 1) + v = MatrixSymbol('y', 2, 1) + assert p.doprint(MatrixSolve(A, u)) == 'numpy.linalg.solve(A, x)' + assert p.doprint(MatrixSolve(A, u) + v) == 'numpy.linalg.solve(A, x) + y' + + assert p.doprint(ZeroMatrix(2, 3)) == "numpy.zeros((2, 3))" + assert p.doprint(OneMatrix(2, 3)) == "numpy.ones((2, 3))" + assert p.doprint(FunctionMatrix(4, 5, Lambda((a, b), a + b))) == \ + "numpy.fromfunction(lambda a, b: a + b, (4, 5))" + assert p.doprint(HadamardProduct(A, B)) == "numpy.multiply(A, B)" + assert p.doprint(KroneckerProduct(A, B)) == "numpy.kron(A, B)" + assert p.doprint(Adjoint(A)) == "numpy.conjugate(numpy.transpose(A))" + assert p.doprint(DiagonalOf(A)) == "numpy.reshape(numpy.diag(A), (-1, 1))" + assert p.doprint(DiagMatrix(C)) == "numpy.diagflat(C)" + assert p.doprint(DiagonalMatrix(D)) == "numpy.multiply(D, numpy.eye(3, 4))" + + # Workaround for numpy negative integer power errors + assert p.doprint(x**-1) == 'x**(-1.0)' + assert p.doprint(x**-2) == 'x**(-2.0)' + + expr = Pow(2, -1, evaluate=False) + assert p.doprint(expr) == "2**(-1.0)" + + assert p.doprint(S.Exp1) == 'numpy.e' + assert p.doprint(S.Pi) == 'numpy.pi' + assert p.doprint(S.EulerGamma) == 'numpy.euler_gamma' + assert p.doprint(S.NaN) == 'numpy.nan' + assert p.doprint(S.Infinity) == 'numpy.inf' + assert p.doprint(S.NegativeInfinity) == '-numpy.inf' + + # Function rewriting operator precedence fix + assert p.doprint(sec(x)**2) == '(numpy.cos(x)**(-1.0))**2' + + +def test_issue_18770(): + numpy = import_module('numpy') + if not numpy: + skip("numpy not installed.") + + from sympy.functions.elementary.miscellaneous import (Max, Min) + from sympy.utilities.lambdify import lambdify + + expr1 = Min(0.1*x + 3, x + 1, 0.5*x + 1) + func = lambdify(x, expr1, "numpy") + assert (func(numpy.linspace(0, 3, 3)) == [1.0, 1.75, 2.5 ]).all() + assert func(4) == 3 + + expr1 = Max(x**2, x**3) + func = lambdify(x,expr1, "numpy") + assert (func(numpy.linspace(-1, 2, 4)) == [1, 0, 1, 8] ).all() + assert func(4) == 64 + + +def test_SciPyPrinter(): + p = SciPyPrinter() + expr = acos(x) + assert 'numpy' not in p.module_imports + assert p.doprint(expr) == 'numpy.arccos(x)' + assert 'numpy' in p.module_imports + assert not any(m.startswith('scipy') for m in p.module_imports) + smat = SparseMatrix(2, 5, {(0, 1): 3}) + assert p.doprint(smat) == \ + 'scipy.sparse.coo_matrix(([3], ([0], [1])), shape=(2, 5))' + assert 'scipy.sparse' in p.module_imports + + assert p.doprint(S.GoldenRatio) == 'scipy.constants.golden_ratio' + assert p.doprint(S.Pi) == 'scipy.constants.pi' + assert p.doprint(S.Exp1) == 'numpy.e' + + +def test_pycode_reserved_words(): + s1, s2 = symbols('if else') + raises(ValueError, lambda: pycode(s1 + s2, error_on_reserved=True)) + py_str = pycode(s1 + s2) + assert py_str in ('else_ + if_', 'if_ + else_') + + +def test_issue_20762(): + # Make sure pycode removes curly braces from subscripted variables + a_b, b, a_11 = symbols('a_{b} b a_{11}') + expr = a_b*b + assert pycode(expr) == 'a_b*b' + expr = a_11*b + assert pycode(expr) == 'a_11*b' + + +def test_sqrt(): + prntr = PythonCodePrinter() + assert prntr._print_Pow(sqrt(x), rational=False) == 'math.sqrt(x)' + assert prntr._print_Pow(1/sqrt(x), rational=False) == '1/math.sqrt(x)' + + prntr = PythonCodePrinter({'standard' : 'python3'}) + assert prntr._print_Pow(sqrt(x), rational=True) == 'x**(1/2)' + assert prntr._print_Pow(1/sqrt(x), rational=True) == 'x**(-1/2)' + + prntr = MpmathPrinter() + assert prntr._print_Pow(sqrt(x), rational=False) == 'mpmath.sqrt(x)' + assert prntr._print_Pow(sqrt(x), rational=True) == \ + "x**(mpmath.mpf(1)/mpmath.mpf(2))" + + prntr = NumPyPrinter() + assert prntr._print_Pow(sqrt(x), rational=False) == 'numpy.sqrt(x)' + assert prntr._print_Pow(sqrt(x), rational=True) == 'x**(1/2)' + + prntr = SciPyPrinter() + assert prntr._print_Pow(sqrt(x), rational=False) == 'numpy.sqrt(x)' + assert prntr._print_Pow(sqrt(x), rational=True) == 'x**(1/2)' + + prntr = SymPyPrinter() + assert prntr._print_Pow(sqrt(x), rational=False) == 'sympy.sqrt(x)' + assert prntr._print_Pow(sqrt(x), rational=True) == 'x**(1/2)' + + +def test_frac(): + from sympy.functions.elementary.integers import frac + + expr = frac(x) + prntr = NumPyPrinter() + assert prntr.doprint(expr) == 'numpy.mod(x, 1)' + + prntr = SciPyPrinter() + assert prntr.doprint(expr) == 'numpy.mod(x, 1)' + + prntr = PythonCodePrinter() + assert prntr.doprint(expr) == 'x % 1' + + prntr = MpmathPrinter() + assert prntr.doprint(expr) == 'mpmath.frac(x)' + + prntr = SymPyPrinter() + assert prntr.doprint(expr) == 'sympy.functions.elementary.integers.frac(x)' + + +class CustomPrintedObject(Expr): + def _numpycode(self, printer): + return 'numpy' + + def _mpmathcode(self, printer): + return 'mpmath' + + +def test_printmethod(): + obj = CustomPrintedObject() + assert NumPyPrinter().doprint(obj) == 'numpy' + assert MpmathPrinter().doprint(obj) == 'mpmath' + + +def test_codegen_ast_nodes(): + assert pycode(none) == 'None' + + +def test_issue_14283(): + prntr = PythonCodePrinter() + + assert prntr.doprint(zoo) == "math.nan" + assert prntr.doprint(-oo) == "float('-inf')" + + +def test_NumPyPrinter_print_seq(): + n = NumPyPrinter() + + assert n._print_seq(range(2)) == '(0, 1,)' + + +def test_issue_16535_16536(): + from sympy.functions.special.gamma_functions import (lowergamma, uppergamma) + + a = symbols('a') + expr1 = lowergamma(a, x) + expr2 = uppergamma(a, x) + + prntr = SciPyPrinter() + assert prntr.doprint(expr1) == 'scipy.special.gamma(a)*scipy.special.gammainc(a, x)' + assert prntr.doprint(expr2) == 'scipy.special.gamma(a)*scipy.special.gammaincc(a, x)' + + p_numpy = NumPyPrinter() + p_pycode = PythonCodePrinter({'strict': False}) + + for expr in [expr1, expr2]: + with raises(NotImplementedError): + p_numpy.doprint(expr1) + assert "Not supported" in p_pycode.doprint(expr) + + +def test_Integral(): + from sympy.functions.elementary.exponential import exp + from sympy.integrals.integrals import Integral + + single = Integral(exp(-x), (x, 0, oo)) + double = Integral(x**2*exp(x*y), (x, -z, z), (y, 0, z)) + indefinite = Integral(x**2, x) + evaluateat = Integral(x**2, (x, 1)) + + prntr = SciPyPrinter() + assert prntr.doprint(single) == 'scipy.integrate.quad(lambda x: numpy.exp(-x), 0, numpy.inf)[0]' + assert prntr.doprint(double) == 'scipy.integrate.nquad(lambda x, y: x**2*numpy.exp(x*y), ((-z, z), (0, z)))[0]' + raises(NotImplementedError, lambda: prntr.doprint(indefinite)) + raises(NotImplementedError, lambda: prntr.doprint(evaluateat)) + + prntr = MpmathPrinter() + assert prntr.doprint(single) == 'mpmath.quad(lambda x: mpmath.exp(-x), (0, mpmath.inf))' + assert prntr.doprint(double) == 'mpmath.quad(lambda x, y: x**2*mpmath.exp(x*y), (-z, z), (0, z))' + raises(NotImplementedError, lambda: prntr.doprint(indefinite)) + raises(NotImplementedError, lambda: prntr.doprint(evaluateat)) + + +def test_fresnel_integrals(): + from sympy.functions.special.error_functions import (fresnelc, fresnels) + + expr1 = fresnelc(x) + expr2 = fresnels(x) + + prntr = SciPyPrinter() + assert prntr.doprint(expr1) == 'scipy.special.fresnel(x)[1]' + assert prntr.doprint(expr2) == 'scipy.special.fresnel(x)[0]' + + p_numpy = NumPyPrinter() + p_pycode = PythonCodePrinter() + p_mpmath = MpmathPrinter() + for expr in [expr1, expr2]: + with raises(NotImplementedError): + p_numpy.doprint(expr) + with raises(NotImplementedError): + p_pycode.doprint(expr) + + assert p_mpmath.doprint(expr1) == 'mpmath.fresnelc(x)' + assert p_mpmath.doprint(expr2) == 'mpmath.fresnels(x)' + + +def test_beta(): + from sympy.functions.special.beta_functions import beta + + expr = beta(x, y) + + prntr = SciPyPrinter() + assert prntr.doprint(expr) == 'scipy.special.beta(x, y)' + + prntr = NumPyPrinter() + assert prntr.doprint(expr) == '(math.gamma(x)*math.gamma(y)/math.gamma(x + y))' + + prntr = PythonCodePrinter() + assert prntr.doprint(expr) == '(math.gamma(x)*math.gamma(y)/math.gamma(x + y))' + + prntr = PythonCodePrinter({'allow_unknown_functions': True}) + assert prntr.doprint(expr) == '(math.gamma(x)*math.gamma(y)/math.gamma(x + y))' + + prntr = MpmathPrinter() + assert prntr.doprint(expr) == 'mpmath.beta(x, y)' + +def test_airy(): + from sympy.functions.special.bessel import (airyai, airybi) + + expr1 = airyai(x) + expr2 = airybi(x) + + prntr = SciPyPrinter() + assert prntr.doprint(expr1) == 'scipy.special.airy(x)[0]' + assert prntr.doprint(expr2) == 'scipy.special.airy(x)[2]' + + prntr = NumPyPrinter({'strict': False}) + assert "Not supported" in prntr.doprint(expr1) + assert "Not supported" in prntr.doprint(expr2) + + prntr = PythonCodePrinter({'strict': False}) + assert "Not supported" in prntr.doprint(expr1) + assert "Not supported" in prntr.doprint(expr2) + +def test_airy_prime(): + from sympy.functions.special.bessel import (airyaiprime, airybiprime) + + expr1 = airyaiprime(x) + expr2 = airybiprime(x) + + prntr = SciPyPrinter() + assert prntr.doprint(expr1) == 'scipy.special.airy(x)[1]' + assert prntr.doprint(expr2) == 'scipy.special.airy(x)[3]' + + prntr = NumPyPrinter({'strict': False}) + assert "Not supported" in prntr.doprint(expr1) + assert "Not supported" in prntr.doprint(expr2) + + prntr = PythonCodePrinter({'strict': False}) + assert "Not supported" in prntr.doprint(expr1) + assert "Not supported" in prntr.doprint(expr2) + + +def test_numerical_accuracy_functions(): + prntr = SciPyPrinter() + assert prntr.doprint(expm1(x)) == 'numpy.expm1(x)' + assert prntr.doprint(log1p(x)) == 'numpy.log1p(x)' + assert prntr.doprint(cosm1(x)) == 'scipy.special.cosm1(x)' + +def test_array_printer(): + A = ArraySymbol('A', (4,4,6,6,6)) + I = IndexedBase('I') + i,j,k = Idx('i', (0,1)), Idx('j', (2,3)), Idx('k', (4,5)) + + prntr = NumPyPrinter() + assert prntr.doprint(ZeroArray(5)) == 'numpy.zeros((5,))' + assert prntr.doprint(OneArray(5)) == 'numpy.ones((5,))' + assert prntr.doprint(ArrayContraction(A, [2,3])) == 'numpy.einsum("abccd->abd", A)' + assert prntr.doprint(I) == 'I' + assert prntr.doprint(ArrayDiagonal(A, [2,3,4])) == 'numpy.einsum("abccc->abc", A)' + assert prntr.doprint(ArrayDiagonal(A, [0,1], [2,3])) == 'numpy.einsum("aabbc->cab", A)' + assert prntr.doprint(ArrayContraction(A, [2], [3])) == 'numpy.einsum("abcde->abe", A)' + assert prntr.doprint(Assignment(I[i,j,k], I[i,j,k])) == 'I = I' + + prntr = TensorflowPrinter() + assert prntr.doprint(ZeroArray(5)) == 'tensorflow.zeros((5,))' + assert prntr.doprint(OneArray(5)) == 'tensorflow.ones((5,))' + assert prntr.doprint(ArrayContraction(A, [2,3])) == 'tensorflow.linalg.einsum("abccd->abd", A)' + assert prntr.doprint(I) == 'I' + assert prntr.doprint(ArrayDiagonal(A, [2,3,4])) == 'tensorflow.linalg.einsum("abccc->abc", A)' + assert prntr.doprint(ArrayDiagonal(A, [0,1], [2,3])) == 'tensorflow.linalg.einsum("aabbc->cab", A)' + assert prntr.doprint(ArrayContraction(A, [2], [3])) == 'tensorflow.linalg.einsum("abcde->abe", A)' + assert prntr.doprint(Assignment(I[i,j,k], I[i,j,k])) == 'I = I' + + +def test_custom_Derivative_methods(): + class MyPrinter(SciPyPrinter): + def _print_Derivative_cosm1(self, args, seq_orders): + arg, = args + order, = seq_orders + return 'my_custom_cosm1(%s, deriv_order=%d)' % (self._print(arg), order) + + def _print_Derivative_atan2(self, args, seq_orders): + arg1, arg2 = args + ord1, ord2 = seq_orders + return 'my_custom_atan2(%s, %s, deriv1=%d, deriv2=%d)' % ( + self._print(arg1), self._print(arg2), ord1, ord2 + ) + + p = MyPrinter() + cosm1_1 = cosm1(x).diff(x, evaluate=False) + assert p.doprint(cosm1_1) == 'my_custom_cosm1(x, deriv_order=1)' + atan2_2_3 = atan2(x, y).diff(x, 2, y, 3, evaluate=False) + assert p.doprint(atan2_2_3) == 'my_custom_atan2(x, y, deriv1=2, deriv2=3)' + + try: + p.doprint(expm1(x).diff(x, evaluate=False)) + except PrintMethodNotImplementedError as e: + assert '_print_Derivative_expm1' in repr(e) + else: + assert False # should have thrown + + try: + p.doprint(Derivative(cosm1(x**2),x)) + except ValueError as e: + assert '_print_Derivative(' in repr(e) + else: + assert False # should have thrown diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_python.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_python.py new file mode 100644 index 0000000000000000000000000000000000000000..fb94a662be90934a672d08b3de44a22e2580d8b6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_python.py @@ -0,0 +1,203 @@ +from sympy.core.function import (Derivative, Function) +from sympy.core.numbers import (I, Rational, oo, pi) +from sympy.core.relational import (Eq, Ge, Gt, Le, Lt, Ne) +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.complexes import (Abs, conjugate) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import sin +from sympy.integrals.integrals import Integral +from sympy.matrices.dense import Matrix +from sympy.series.limits import limit + +from sympy.printing.python import python + +from sympy.testing.pytest import raises, XFAIL + +x, y = symbols('x,y') +th = Symbol('theta') +ph = Symbol('phi') + + +def test_python_basic(): + # Simple numbers/symbols + assert python(-Rational(1)/2) == "e = Rational(-1, 2)" + assert python(-Rational(13)/22) == "e = Rational(-13, 22)" + assert python(oo) == "e = oo" + + # Powers + assert python(x**2) == "x = Symbol(\'x\')\ne = x**2" + assert python(1/x) == "x = Symbol('x')\ne = 1/x" + assert python(y*x**-2) == "y = Symbol('y')\nx = Symbol('x')\ne = y/x**2" + assert python( + x**Rational(-5, 2)) == "x = Symbol('x')\ne = x**Rational(-5, 2)" + + # Sums of terms + assert python(x**2 + x + 1) in [ + "x = Symbol('x')\ne = 1 + x + x**2", + "x = Symbol('x')\ne = x + x**2 + 1", + "x = Symbol('x')\ne = x**2 + x + 1", ] + assert python(1 - x) in [ + "x = Symbol('x')\ne = 1 - x", + "x = Symbol('x')\ne = -x + 1"] + assert python(1 - 2*x) in [ + "x = Symbol('x')\ne = 1 - 2*x", + "x = Symbol('x')\ne = -2*x + 1"] + assert python(1 - Rational(3, 2)*y/x) in [ + "y = Symbol('y')\nx = Symbol('x')\ne = 1 - 3/2*y/x", + "y = Symbol('y')\nx = Symbol('x')\ne = -3/2*y/x + 1", + "y = Symbol('y')\nx = Symbol('x')\ne = 1 - 3*y/(2*x)"] + + # Multiplication + assert python(x/y) == "x = Symbol('x')\ny = Symbol('y')\ne = x/y" + assert python(-x/y) == "x = Symbol('x')\ny = Symbol('y')\ne = -x/y" + assert python((x + 2)/y) in [ + "y = Symbol('y')\nx = Symbol('x')\ne = 1/y*(2 + x)", + "y = Symbol('y')\nx = Symbol('x')\ne = 1/y*(x + 2)", + "x = Symbol('x')\ny = Symbol('y')\ne = 1/y*(2 + x)", + "x = Symbol('x')\ny = Symbol('y')\ne = (2 + x)/y", + "x = Symbol('x')\ny = Symbol('y')\ne = (x + 2)/y"] + assert python((1 + x)*y) in [ + "y = Symbol('y')\nx = Symbol('x')\ne = y*(1 + x)", + "y = Symbol('y')\nx = Symbol('x')\ne = y*(x + 1)", ] + + # Check for proper placement of negative sign + assert python(-5*x/(x + 10)) == "x = Symbol('x')\ne = -5*x/(x + 10)" + assert python(1 - Rational(3, 2)*(x + 1)) in [ + "x = Symbol('x')\ne = Rational(-3, 2)*x + Rational(-1, 2)", + "x = Symbol('x')\ne = -3*x/2 + Rational(-1, 2)", + "x = Symbol('x')\ne = -3*x/2 + Rational(-1, 2)" + ] + + +def test_python_keyword_symbol_name_escaping(): + # Check for escaping of keywords + assert python( + 5*Symbol("lambda")) == "lambda_ = Symbol('lambda')\ne = 5*lambda_" + assert (python(5*Symbol("lambda") + 7*Symbol("lambda_")) == + "lambda__ = Symbol('lambda')\nlambda_ = Symbol('lambda_')\ne = 7*lambda_ + 5*lambda__") + assert (python(5*Symbol("for") + Function("for_")(8)) == + "for__ = Symbol('for')\nfor_ = Function('for_')\ne = 5*for__ + for_(8)") + + +def test_python_keyword_function_name_escaping(): + assert python( + 5*Function("for")(8)) == "for_ = Function('for')\ne = 5*for_(8)" + + +def test_python_relational(): + assert python(Eq(x, y)) == "x = Symbol('x')\ny = Symbol('y')\ne = Eq(x, y)" + assert python(Ge(x, y)) == "x = Symbol('x')\ny = Symbol('y')\ne = x >= y" + assert python(Le(x, y)) == "x = Symbol('x')\ny = Symbol('y')\ne = x <= y" + assert python(Gt(x, y)) == "x = Symbol('x')\ny = Symbol('y')\ne = x > y" + assert python(Lt(x, y)) == "x = Symbol('x')\ny = Symbol('y')\ne = x < y" + assert python(Ne(x/(y + 1), y**2)) in [ + "x = Symbol('x')\ny = Symbol('y')\ne = Ne(x/(1 + y), y**2)", + "x = Symbol('x')\ny = Symbol('y')\ne = Ne(x/(y + 1), y**2)"] + + +def test_python_functions(): + # Simple + assert python(2*x + exp(x)) in "x = Symbol('x')\ne = 2*x + exp(x)" + assert python(sqrt(2)) == 'e = sqrt(2)' + assert python(2**Rational(1, 3)) == 'e = 2**Rational(1, 3)' + assert python(sqrt(2 + pi)) == 'e = sqrt(2 + pi)' + assert python((2 + pi)**Rational(1, 3)) == 'e = (2 + pi)**Rational(1, 3)' + assert python(2**Rational(1, 4)) == 'e = 2**Rational(1, 4)' + assert python(Abs(x)) == "x = Symbol('x')\ne = Abs(x)" + assert python( + Abs(x/(x**2 + 1))) in ["x = Symbol('x')\ne = Abs(x/(1 + x**2))", + "x = Symbol('x')\ne = Abs(x/(x**2 + 1))"] + + # Univariate/Multivariate functions + f = Function('f') + assert python(f(x)) == "x = Symbol('x')\nf = Function('f')\ne = f(x)" + assert python(f(x, y)) == "x = Symbol('x')\ny = Symbol('y')\nf = Function('f')\ne = f(x, y)" + assert python(f(x/(y + 1), y)) in [ + "x = Symbol('x')\ny = Symbol('y')\nf = Function('f')\ne = f(x/(1 + y), y)", + "x = Symbol('x')\ny = Symbol('y')\nf = Function('f')\ne = f(x/(y + 1), y)"] + + # Nesting of square roots + assert python(sqrt((sqrt(x + 1)) + 1)) in [ + "x = Symbol('x')\ne = sqrt(1 + sqrt(1 + x))", + "x = Symbol('x')\ne = sqrt(sqrt(x + 1) + 1)"] + + # Nesting of powers + assert python((((x + 1)**Rational(1, 3)) + 1)**Rational(1, 3)) in [ + "x = Symbol('x')\ne = (1 + (1 + x)**Rational(1, 3))**Rational(1, 3)", + "x = Symbol('x')\ne = ((x + 1)**Rational(1, 3) + 1)**Rational(1, 3)"] + + # Function powers + assert python(sin(x)**2) == "x = Symbol('x')\ne = sin(x)**2" + + +@XFAIL +def test_python_functions_conjugates(): + a, b = map(Symbol, 'ab') + assert python( conjugate(a + b*I) ) == '_ _\na - I*b' + assert python( conjugate(exp(a + b*I)) ) == ' _ _\n a - I*b\ne ' + + +def test_python_derivatives(): + # Simple + f_1 = Derivative(log(x), x, evaluate=False) + assert python(f_1) == "x = Symbol('x')\ne = Derivative(log(x), x)" + + f_2 = Derivative(log(x), x, evaluate=False) + x + assert python(f_2) == "x = Symbol('x')\ne = x + Derivative(log(x), x)" + + # Multiple symbols + f_3 = Derivative(log(x) + x**2, x, y, evaluate=False) + assert python(f_3) == \ + "x = Symbol('x')\ny = Symbol('y')\ne = Derivative(x**2 + log(x), x, y)" + + f_4 = Derivative(2*x*y, y, x, evaluate=False) + x**2 + assert python(f_4) in [ + "x = Symbol('x')\ny = Symbol('y')\ne = x**2 + Derivative(2*x*y, y, x)", + "x = Symbol('x')\ny = Symbol('y')\ne = Derivative(2*x*y, y, x) + x**2"] + + +def test_python_integrals(): + # Simple + f_1 = Integral(log(x), x) + assert python(f_1) == "x = Symbol('x')\ne = Integral(log(x), x)" + + f_2 = Integral(x**2, x) + assert python(f_2) == "x = Symbol('x')\ne = Integral(x**2, x)" + + # Double nesting of pow + f_3 = Integral(x**(2**x), x) + assert python(f_3) == "x = Symbol('x')\ne = Integral(x**(2**x), x)" + + # Definite integrals + f_4 = Integral(x**2, (x, 1, 2)) + assert python(f_4) == "x = Symbol('x')\ne = Integral(x**2, (x, 1, 2))" + + f_5 = Integral(x**2, (x, Rational(1, 2), 10)) + assert python( + f_5) == "x = Symbol('x')\ne = Integral(x**2, (x, Rational(1, 2), 10))" + + # Nested integrals + f_6 = Integral(x**2*y**2, x, y) + assert python(f_6) == "x = Symbol('x')\ny = Symbol('y')\ne = Integral(x**2*y**2, x, y)" + + +def test_python_matrix(): + p = python(Matrix([[x**2+1, 1], [y, x+y]])) + s = "x = Symbol('x')\ny = Symbol('y')\ne = MutableDenseMatrix([[x**2 + 1, 1], [y, x + y]])" + assert p == s + +def test_python_limits(): + assert python(limit(x, x, oo)) == 'e = oo' + assert python(limit(x**2, x, 0)) == 'e = 0' + +def test_issue_20762(): + # Make sure Python removes curly braces from subscripted variables + a_b = Symbol('a_{b}') + b = Symbol('b') + expr = a_b*b + assert python(expr) == "a_b = Symbol('a_{b}')\nb = Symbol('b')\ne = a_b*b" + + +def test_settings(): + raises(TypeError, lambda: python(x, method="garbage")) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_rcode.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_rcode.py new file mode 100644 index 0000000000000000000000000000000000000000..a83235b0654c6bf24c30846dbf68678d29cd3c80 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_rcode.py @@ -0,0 +1,476 @@ +from sympy.core import (S, pi, oo, Symbol, symbols, Rational, Integer, + GoldenRatio, EulerGamma, Catalan, Lambda, Dummy) +from sympy.functions import (Piecewise, sin, cos, Abs, exp, ceiling, sqrt, + gamma, sign, Max, Min, factorial, beta) +from sympy.core.relational import (Eq, Ge, Gt, Le, Lt, Ne) +from sympy.sets import Range +from sympy.logic import ITE +from sympy.codegen import For, aug_assign, Assignment +from sympy.testing.pytest import raises +from sympy.printing.rcode import RCodePrinter +from sympy.utilities.lambdify import implemented_function +from sympy.tensor import IndexedBase, Idx +from sympy.matrices import Matrix, MatrixSymbol + +from sympy.printing.rcode import rcode + +x, y, z = symbols('x,y,z') + + +def test_printmethod(): + class fabs(Abs): + def _rcode(self, printer): + return "abs(%s)" % printer._print(self.args[0]) + + assert rcode(fabs(x)) == "abs(x)" + + +def test_rcode_sqrt(): + assert rcode(sqrt(x)) == "sqrt(x)" + assert rcode(x**0.5) == "sqrt(x)" + assert rcode(sqrt(x)) == "sqrt(x)" + + +def test_rcode_Pow(): + assert rcode(x**3) == "x^3" + assert rcode(x**(y**3)) == "x^(y^3)" + g = implemented_function('g', Lambda(x, 2*x)) + assert rcode(1/(g(x)*3.5)**(x - y**x)/(x**2 + y)) == \ + "(3.5*2*x)^(-x + y^x)/(x^2 + y)" + assert rcode(x**-1.0) == '1.0/x' + assert rcode(x**Rational(2, 3)) == 'x^(2.0/3.0)' + _cond_cfunc = [(lambda base, exp: exp.is_integer, "dpowi"), + (lambda base, exp: not exp.is_integer, "pow")] + assert rcode(x**3, user_functions={'Pow': _cond_cfunc}) == 'dpowi(x, 3)' + assert rcode(x**3.2, user_functions={'Pow': _cond_cfunc}) == 'pow(x, 3.2)' + + +def test_rcode_Max(): + # Test for gh-11926 + assert rcode(Max(x,x*x),user_functions={"Max":"my_max", "Pow":"my_pow"}) == 'my_max(x, my_pow(x, 2))' + + +def test_rcode_constants_mathh(): + assert rcode(exp(1)) == "exp(1)" + assert rcode(pi) == "pi" + assert rcode(oo) == "Inf" + assert rcode(-oo) == "-Inf" + + +def test_rcode_constants_other(): + assert rcode(2*GoldenRatio) == "GoldenRatio = 1.61803398874989;\n2*GoldenRatio" + assert rcode( + 2*Catalan) == "Catalan = 0.915965594177219;\n2*Catalan" + assert rcode(2*EulerGamma) == "EulerGamma = 0.577215664901533;\n2*EulerGamma" + + +def test_rcode_Rational(): + assert rcode(Rational(3, 7)) == "3.0/7.0" + assert rcode(Rational(18, 9)) == "2" + assert rcode(Rational(3, -7)) == "-3.0/7.0" + assert rcode(Rational(-3, -7)) == "3.0/7.0" + assert rcode(x + Rational(3, 7)) == "x + 3.0/7.0" + assert rcode(Rational(3, 7)*x) == "(3.0/7.0)*x" + + +def test_rcode_Integer(): + assert rcode(Integer(67)) == "67" + assert rcode(Integer(-1)) == "-1" + + +def test_rcode_functions(): + assert rcode(sin(x) ** cos(x)) == "sin(x)^cos(x)" + assert rcode(factorial(x) + gamma(y)) == "factorial(x) + gamma(y)" + assert rcode(beta(Min(x, y), Max(x, y))) == "beta(min(x, y), max(x, y))" + + +def test_rcode_inline_function(): + x = symbols('x') + g = implemented_function('g', Lambda(x, 2*x)) + assert rcode(g(x)) == "2*x" + g = implemented_function('g', Lambda(x, 2*x/Catalan)) + assert rcode( + g(x)) == "Catalan = %s;\n2*x/Catalan" % Catalan.n() + A = IndexedBase('A') + i = Idx('i', symbols('n', integer=True)) + g = implemented_function('g', Lambda(x, x*(1 + x)*(2 + x))) + res=rcode(g(A[i]), assign_to=A[i]) + ref=( + "for (i in 1:n){\n" + " A[i] = (A[i] + 1)*(A[i] + 2)*A[i];\n" + "}" + ) + assert res == ref + + +def test_rcode_exceptions(): + assert rcode(ceiling(x)) == "ceiling(x)" + assert rcode(Abs(x)) == "abs(x)" + assert rcode(gamma(x)) == "gamma(x)" + + +def test_rcode_user_functions(): + x = symbols('x', integer=False) + n = symbols('n', integer=True) + custom_functions = { + "ceiling": "myceil", + "Abs": [(lambda x: not x.is_integer, "fabs"), (lambda x: x.is_integer, "abs")], + } + assert rcode(ceiling(x), user_functions=custom_functions) == "myceil(x)" + assert rcode(Abs(x), user_functions=custom_functions) == "fabs(x)" + assert rcode(Abs(n), user_functions=custom_functions) == "abs(n)" + + +def test_rcode_boolean(): + assert rcode(True) == "True" + assert rcode(S.true) == "True" + assert rcode(False) == "False" + assert rcode(S.false) == "False" + assert rcode(x & y) == "x & y" + assert rcode(x | y) == "x | y" + assert rcode(~x) == "!x" + assert rcode(x & y & z) == "x & y & z" + assert rcode(x | y | z) == "x | y | z" + assert rcode((x & y) | z) == "z | x & y" + assert rcode((x | y) & z) == "z & (x | y)" + +def test_rcode_Relational(): + assert rcode(Eq(x, y)) == "x == y" + assert rcode(Ne(x, y)) == "x != y" + assert rcode(Le(x, y)) == "x <= y" + assert rcode(Lt(x, y)) == "x < y" + assert rcode(Gt(x, y)) == "x > y" + assert rcode(Ge(x, y)) == "x >= y" + + +def test_rcode_Piecewise(): + expr = Piecewise((x, x < 1), (x**2, True)) + res=rcode(expr) + ref="ifelse(x < 1,x,x^2)" + assert res == ref + tau=Symbol("tau") + res=rcode(expr,tau) + ref="tau = ifelse(x < 1,x,x^2);" + assert res == ref + + expr = 2*Piecewise((x, x < 1), (x**2, x<2), (x**3,True)) + assert rcode(expr) == "2*ifelse(x < 1,x,ifelse(x < 2,x^2,x^3))" + res = rcode(expr, assign_to='c') + assert res == "c = 2*ifelse(x < 1,x,ifelse(x < 2,x^2,x^3));" + + # Check that Piecewise without a True (default) condition error + #expr = Piecewise((x, x < 1), (x**2, x > 1), (sin(x), x > 0)) + #raises(ValueError, lambda: rcode(expr)) + expr = 2*Piecewise((x, x < 1), (x**2, x<2)) + assert(rcode(expr))== "2*ifelse(x < 1,x,ifelse(x < 2,x^2,NA))" + + +def test_rcode_sinc(): + from sympy.functions.elementary.trigonometric import sinc + expr = sinc(x) + res = rcode(expr) + ref = "(ifelse(x != 0,sin(x)/x,1))" + assert res == ref + + +def test_rcode_Piecewise_deep(): + p = rcode(2*Piecewise((x, x < 1), (x + 1, x < 2), (x**2, True))) + assert p == "2*ifelse(x < 1,x,ifelse(x < 2,x + 1,x^2))" + expr = x*y*z + x**2 + y**2 + Piecewise((0, x < 0.5), (1, True)) + cos(z) - 1 + p = rcode(expr) + ref="x^2 + x*y*z + y^2 + ifelse(x < 0.5,0,1) + cos(z) - 1" + assert p == ref + + ref="c = x^2 + x*y*z + y^2 + ifelse(x < 0.5,0,1) + cos(z) - 1;" + p = rcode(expr, assign_to='c') + assert p == ref + + +def test_rcode_ITE(): + expr = ITE(x < 1, y, z) + p = rcode(expr) + ref="ifelse(x < 1,y,z)" + assert p == ref + + +def test_rcode_settings(): + raises(TypeError, lambda: rcode(sin(x), method="garbage")) + + +def test_rcode_Indexed(): + n, m, o = symbols('n m o', integer=True) + i, j, k = Idx('i', n), Idx('j', m), Idx('k', o) + p = RCodePrinter() + p._not_r = set() + + x = IndexedBase('x')[j] + assert p._print_Indexed(x) == 'x[j]' + A = IndexedBase('A')[i, j] + assert p._print_Indexed(A) == 'A[i, j]' + B = IndexedBase('B')[i, j, k] + assert p._print_Indexed(B) == 'B[i, j, k]' + + assert p._not_r == set() + +def test_rcode_Indexed_without_looking_for_contraction(): + len_y = 5 + y = IndexedBase('y', shape=(len_y,)) + x = IndexedBase('x', shape=(len_y,)) + Dy = IndexedBase('Dy', shape=(len_y-1,)) + i = Idx('i', len_y-1) + e=Eq(Dy[i], (y[i+1]-y[i])/(x[i+1]-x[i])) + code0 = rcode(e.rhs, assign_to=e.lhs, contract=False) + assert code0 == 'Dy[i] = (y[%s] - y[i])/(x[%s] - x[i]);' % (i + 1, i + 1) + + +def test_rcode_loops_matrix_vector(): + n, m = symbols('n m', integer=True) + A = IndexedBase('A') + x = IndexedBase('x') + y = IndexedBase('y') + i = Idx('i', m) + j = Idx('j', n) + + s = ( + 'for (i in 1:m){\n' + ' y[i] = 0;\n' + '}\n' + 'for (i in 1:m){\n' + ' for (j in 1:n){\n' + ' y[i] = A[i, j]*x[j] + y[i];\n' + ' }\n' + '}' + ) + c = rcode(A[i, j]*x[j], assign_to=y[i]) + assert c == s + + +def test_dummy_loops(): + # the following line could also be + # [Dummy(s, integer=True) for s in 'im'] + # or [Dummy(integer=True) for s in 'im'] + i, m = symbols('i m', integer=True, cls=Dummy) + x = IndexedBase('x') + y = IndexedBase('y') + i = Idx(i, m) + + expected = ( + 'for (i_%(icount)i in 1:m_%(mcount)i){\n' + ' y[i_%(icount)i] = x[i_%(icount)i];\n' + '}' + ) % {'icount': i.label.dummy_index, 'mcount': m.dummy_index} + code = rcode(x[i], assign_to=y[i]) + assert code == expected + + +def test_rcode_loops_add(): + n, m = symbols('n m', integer=True) + A = IndexedBase('A') + x = IndexedBase('x') + y = IndexedBase('y') + z = IndexedBase('z') + i = Idx('i', m) + j = Idx('j', n) + + s = ( + 'for (i in 1:m){\n' + ' y[i] = x[i] + z[i];\n' + '}\n' + 'for (i in 1:m){\n' + ' for (j in 1:n){\n' + ' y[i] = A[i, j]*x[j] + y[i];\n' + ' }\n' + '}' + ) + c = rcode(A[i, j]*x[j] + x[i] + z[i], assign_to=y[i]) + assert c == s + + +def test_rcode_loops_multiple_contractions(): + n, m, o, p = symbols('n m o p', integer=True) + a = IndexedBase('a') + b = IndexedBase('b') + y = IndexedBase('y') + i = Idx('i', m) + j = Idx('j', n) + k = Idx('k', o) + l = Idx('l', p) + + s = ( + 'for (i in 1:m){\n' + ' y[i] = 0;\n' + '}\n' + 'for (i in 1:m){\n' + ' for (j in 1:n){\n' + ' for (k in 1:o){\n' + ' for (l in 1:p){\n' + ' y[i] = a[i, j, k, l]*b[j, k, l] + y[i];\n' + ' }\n' + ' }\n' + ' }\n' + '}' + ) + c = rcode(b[j, k, l]*a[i, j, k, l], assign_to=y[i]) + assert c == s + + +def test_rcode_loops_addfactor(): + n, m, o, p = symbols('n m o p', integer=True) + a = IndexedBase('a') + b = IndexedBase('b') + c = IndexedBase('c') + y = IndexedBase('y') + i = Idx('i', m) + j = Idx('j', n) + k = Idx('k', o) + l = Idx('l', p) + + s = ( + 'for (i in 1:m){\n' + ' y[i] = 0;\n' + '}\n' + 'for (i in 1:m){\n' + ' for (j in 1:n){\n' + ' for (k in 1:o){\n' + ' for (l in 1:p){\n' + ' y[i] = (a[i, j, k, l] + b[i, j, k, l])*c[j, k, l] + y[i];\n' + ' }\n' + ' }\n' + ' }\n' + '}' + ) + c = rcode((a[i, j, k, l] + b[i, j, k, l])*c[j, k, l], assign_to=y[i]) + assert c == s + + +def test_rcode_loops_multiple_terms(): + n, m, o, p = symbols('n m o p', integer=True) + a = IndexedBase('a') + b = IndexedBase('b') + c = IndexedBase('c') + y = IndexedBase('y') + i = Idx('i', m) + j = Idx('j', n) + k = Idx('k', o) + + s0 = ( + 'for (i in 1:m){\n' + ' y[i] = 0;\n' + '}\n' + ) + s1 = ( + 'for (i in 1:m){\n' + ' for (j in 1:n){\n' + ' for (k in 1:o){\n' + ' y[i] = b[j]*b[k]*c[i, j, k] + y[i];\n' + ' }\n' + ' }\n' + '}\n' + ) + s2 = ( + 'for (i in 1:m){\n' + ' for (k in 1:o){\n' + ' y[i] = a[i, k]*b[k] + y[i];\n' + ' }\n' + '}\n' + ) + s3 = ( + 'for (i in 1:m){\n' + ' for (j in 1:n){\n' + ' y[i] = a[i, j]*b[j] + y[i];\n' + ' }\n' + '}\n' + ) + c = rcode( + b[j]*a[i, j] + b[k]*a[i, k] + b[j]*b[k]*c[i, j, k], assign_to=y[i]) + + ref={} + ref[0] = s0 + s1 + s2 + s3[:-1] + ref[1] = s0 + s1 + s3 + s2[:-1] + ref[2] = s0 + s2 + s1 + s3[:-1] + ref[3] = s0 + s2 + s3 + s1[:-1] + ref[4] = s0 + s3 + s1 + s2[:-1] + ref[5] = s0 + s3 + s2 + s1[:-1] + + assert (c == ref[0] or + c == ref[1] or + c == ref[2] or + c == ref[3] or + c == ref[4] or + c == ref[5]) + + +def test_dereference_printing(): + expr = x + y + sin(z) + z + assert rcode(expr, dereference=[z]) == "x + y + (*z) + sin((*z))" + + +def test_Matrix_printing(): + # Test returning a Matrix + mat = Matrix([x*y, Piecewise((2 + x, y>0), (y, True)), sin(z)]) + A = MatrixSymbol('A', 3, 1) + p = rcode(mat, A) + assert p == ( + "A[0] = x*y;\n" + "A[1] = ifelse(y > 0,x + 2,y);\n" + "A[2] = sin(z);") + # Test using MatrixElements in expressions + expr = Piecewise((2*A[2, 0], x > 0), (A[2, 0], True)) + sin(A[1, 0]) + A[0, 0] + p = rcode(expr) + assert p == ("ifelse(x > 0,2*A[2],A[2]) + sin(A[1]) + A[0]") + # Test using MatrixElements in a Matrix + q = MatrixSymbol('q', 5, 1) + M = MatrixSymbol('M', 3, 3) + m = Matrix([[sin(q[1,0]), 0, cos(q[2,0])], + [q[1,0] + q[2,0], q[3, 0], 5], + [2*q[4, 0]/q[1,0], sqrt(q[0,0]) + 4, 0]]) + assert rcode(m, M) == ( + "M[0] = sin(q[1]);\n" + "M[1] = 0;\n" + "M[2] = cos(q[2]);\n" + "M[3] = q[1] + q[2];\n" + "M[4] = q[3];\n" + "M[5] = 5;\n" + "M[6] = 2*q[4]/q[1];\n" + "M[7] = sqrt(q[0]) + 4;\n" + "M[8] = 0;") + + +def test_rcode_sgn(): + + expr = sign(x) * y + assert rcode(expr) == 'y*sign(x)' + p = rcode(expr, 'z') + assert p == 'z = y*sign(x);' + + p = rcode(sign(2 * x + x**2) * x + x**2) + assert p == "x^2 + x*sign(x^2 + 2*x)" + + expr = sign(cos(x)) + p = rcode(expr) + assert p == 'sign(cos(x))' + +def test_rcode_Assignment(): + assert rcode(Assignment(x, y + z)) == 'x = y + z;' + assert rcode(aug_assign(x, '+', y + z)) == 'x += y + z;' + + +def test_rcode_For(): + f = For(x, Range(0, 10, 2), [aug_assign(y, '*', x)]) + sol = rcode(f) + assert sol == ("for(x in seq(from=0, to=9, by=2){\n" + " y *= x;\n" + "}") + + +def test_MatrixElement_printing(): + # test cases for issue #11821 + A = MatrixSymbol("A", 1, 3) + B = MatrixSymbol("B", 1, 3) + C = MatrixSymbol("C", 1, 3) + + assert(rcode(A[0, 0]) == "A[0]") + assert(rcode(3 * A[0, 0]) == "3*A[0]") + + F = C[0, 0].subs(C, A - B) + assert(rcode(F) == "(A - B)[0]") diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_repr.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_repr.py new file mode 100644 index 0000000000000000000000000000000000000000..da58883b4fb027ed82db842a0a1ce5f76a49a8bb --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_repr.py @@ -0,0 +1,382 @@ +from __future__ import annotations +from typing import Any + +from sympy.external.gmpy import GROUND_TYPES +from sympy.testing.pytest import raises, warns_deprecated_sympy +from sympy.assumptions.ask import Q +from sympy.core.function import (Function, WildFunction) +from sympy.core.numbers import (AlgebraicNumber, Float, Integer, Rational) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol, Wild, symbols) +from sympy.core.sympify import sympify +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.miscellaneous import (root, sqrt) +from sympy.functions.elementary.trigonometric import sin +from sympy.functions.special.delta_functions import Heaviside +from sympy.logic.boolalg import (false, true) +from sympy.matrices.dense import (Matrix, ones) +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.matrices.immutable import ImmutableDenseMatrix +from sympy.combinatorics import Cycle, Permutation +from sympy.core.symbol import Str +from sympy.geometry import Point, Ellipse +from sympy.printing import srepr +from sympy.polys import ring, field, ZZ, QQ, lex, grlex, Poly +from sympy.polys.polyclasses import DMP +from sympy.polys.agca.extensions import FiniteExtension + +x, y = symbols('x,y') + +# eval(srepr(expr)) == expr has to succeed in the right environment. The right +# environment is the scope of "from sympy import *" for most cases. +ENV: dict[str, Any] = {"Str": Str} +exec("from sympy import *", ENV) + + +def sT(expr, string, import_stmt=None, **kwargs): + """ + sT := sreprTest + + Tests that srepr delivers the expected string and that + the condition eval(srepr(expr))==expr holds. + """ + if import_stmt is None: + ENV2 = ENV + else: + ENV2 = ENV.copy() + exec(import_stmt, ENV2) + + assert srepr(expr, **kwargs) == string + assert eval(string, ENV2) == expr + + +def test_printmethod(): + class R(Abs): + def _sympyrepr(self, printer): + return "foo(%s)" % printer._print(self.args[0]) + assert srepr(R(x)) == "foo(Symbol('x'))" + + +def test_Add(): + sT(x + y, "Add(Symbol('x'), Symbol('y'))") + assert srepr(x**2 + 1, order='lex') == "Add(Pow(Symbol('x'), Integer(2)), Integer(1))" + assert srepr(x**2 + 1, order='old') == "Add(Integer(1), Pow(Symbol('x'), Integer(2)))" + assert srepr(sympify('x + 3 - 2', evaluate=False), order='none') == "Add(Symbol('x'), Integer(3), Mul(Integer(-1), Integer(2)))" + + +def test_more_than_255_args_issue_10259(): + from sympy.core.add import Add + from sympy.core.mul import Mul + for op in (Add, Mul): + expr = op(*symbols('x:256')) + assert eval(srepr(expr)) == expr + + +def test_Function(): + sT(Function("f")(x), "Function('f')(Symbol('x'))") + # test unapplied Function + sT(Function('f'), "Function('f')") + + sT(sin(x), "sin(Symbol('x'))") + sT(sin, "sin") + + +def test_Heaviside(): + sT(Heaviside(x), "Heaviside(Symbol('x'))") + sT(Heaviside(x, 1), "Heaviside(Symbol('x'), Integer(1))") + + +def test_Geometry(): + sT(Point(0, 0), "Point2D(Integer(0), Integer(0))") + sT(Ellipse(Point(0, 0), 5, 1), + "Ellipse(Point2D(Integer(0), Integer(0)), Integer(5), Integer(1))") + # TODO more tests + + +def test_Singletons(): + sT(S.Catalan, 'Catalan') + sT(S.ComplexInfinity, 'zoo') + sT(S.EulerGamma, 'EulerGamma') + sT(S.Exp1, 'E') + sT(S.GoldenRatio, 'GoldenRatio') + sT(S.TribonacciConstant, 'TribonacciConstant') + sT(S.Half, 'Rational(1, 2)') + sT(S.ImaginaryUnit, 'I') + sT(S.Infinity, 'oo') + sT(S.NaN, 'nan') + sT(S.NegativeInfinity, '-oo') + sT(S.NegativeOne, 'Integer(-1)') + sT(S.One, 'Integer(1)') + sT(S.Pi, 'pi') + sT(S.Zero, 'Integer(0)') + sT(S.Complexes, 'Complexes') + sT(S.EmptySequence, 'EmptySequence') + sT(S.EmptySet, 'EmptySet') + # sT(S.IdentityFunction, 'Lambda(_x, _x)') + sT(S.Naturals, 'Naturals') + sT(S.Naturals0, 'Naturals0') + sT(S.Rationals, 'Rationals') + sT(S.Reals, 'Reals') + sT(S.UniversalSet, 'UniversalSet') + + +def test_Integer(): + sT(Integer(4), "Integer(4)") + + +def test_list(): + sT([x, Integer(4)], "[Symbol('x'), Integer(4)]") + + +def test_Matrix(): + for cls, name in [(Matrix, "MutableDenseMatrix"), (ImmutableDenseMatrix, "ImmutableDenseMatrix")]: + sT(cls([[x**+1, 1], [y, x + y]]), + "%s([[Symbol('x'), Integer(1)], [Symbol('y'), Add(Symbol('x'), Symbol('y'))]])" % name) + + sT(cls(), "%s([])" % name) + + sT(cls([[x**+1, 1], [y, x + y]]), "%s([[Symbol('x'), Integer(1)], [Symbol('y'), Add(Symbol('x'), Symbol('y'))]])" % name) + + +def test_empty_Matrix(): + sT(ones(0, 3), "MutableDenseMatrix(0, 3, [])") + sT(ones(4, 0), "MutableDenseMatrix(4, 0, [])") + sT(ones(0, 0), "MutableDenseMatrix([])") + + +def test_Rational(): + sT(Rational(1, 3), "Rational(1, 3)") + sT(Rational(-1, 3), "Rational(-1, 3)") + + +def test_Float(): + sT(Float('1.23', dps=3), "Float('1.22998', precision=13)") + sT(Float('1.23456789', dps=9), "Float('1.23456788994', precision=33)") + sT(Float('1.234567890123456789', dps=19), + "Float('1.234567890123456789013', precision=66)") + sT(Float('0.60038617995049726', dps=15), + "Float('0.60038617995049726', precision=53)") + + sT(Float('1.23', precision=13), "Float('1.22998', precision=13)") + sT(Float('1.23456789', precision=33), + "Float('1.23456788994', precision=33)") + sT(Float('1.234567890123456789', precision=66), + "Float('1.234567890123456789013', precision=66)") + sT(Float('0.60038617995049726', precision=53), + "Float('0.60038617995049726', precision=53)") + + sT(Float('0.60038617995049726', 15), + "Float('0.60038617995049726', precision=53)") + + +def test_Symbol(): + sT(x, "Symbol('x')") + sT(y, "Symbol('y')") + sT(Symbol('x', negative=True), "Symbol('x', negative=True)") + + +def test_Symbol_two_assumptions(): + x = Symbol('x', negative=0, integer=1) + # order could vary + s1 = "Symbol('x', integer=True, negative=False)" + s2 = "Symbol('x', negative=False, integer=True)" + assert srepr(x) in (s1, s2) + assert eval(srepr(x), ENV) == x + + +def test_Symbol_no_special_commutative_treatment(): + sT(Symbol('x'), "Symbol('x')") + sT(Symbol('x', commutative=False), "Symbol('x', commutative=False)") + sT(Symbol('x', commutative=0), "Symbol('x', commutative=False)") + sT(Symbol('x', commutative=True), "Symbol('x', commutative=True)") + sT(Symbol('x', commutative=1), "Symbol('x', commutative=True)") + + +def test_Wild(): + sT(Wild('x', even=True), "Wild('x', even=True)") + + +def test_Dummy(): + d = Dummy('d') + sT(d, "Dummy('d', dummy_index=%s)" % str(d.dummy_index)) + + +def test_Dummy_assumption(): + d = Dummy('d', nonzero=True) + assert d == eval(srepr(d)) + s1 = "Dummy('d', dummy_index=%s, nonzero=True)" % str(d.dummy_index) + s2 = "Dummy('d', nonzero=True, dummy_index=%s)" % str(d.dummy_index) + assert srepr(d) in (s1, s2) + + +def test_Dummy_from_Symbol(): + # should not get the full dictionary of assumptions + n = Symbol('n', integer=True) + d = n.as_dummy() + assert srepr(d + ) == "Dummy('n', dummy_index=%s)" % str(d.dummy_index) + + +def test_tuple(): + sT((x,), "(Symbol('x'),)") + sT((x, y), "(Symbol('x'), Symbol('y'))") + + +def test_WildFunction(): + sT(WildFunction('w'), "WildFunction('w')") + + +def test_settins(): + raises(TypeError, lambda: srepr(x, method="garbage")) + + +def test_Mul(): + sT(3*x**3*y, "Mul(Integer(3), Pow(Symbol('x'), Integer(3)), Symbol('y'))") + assert srepr(3*x**3*y, order='old') == "Mul(Integer(3), Symbol('y'), Pow(Symbol('x'), Integer(3)))" + assert srepr(sympify('(x+4)*2*x*7', evaluate=False), order='none') == "Mul(Add(Symbol('x'), Integer(4)), Integer(2), Symbol('x'), Integer(7))" + + +def test_AlgebraicNumber(): + a = AlgebraicNumber(sqrt(2)) + sT(a, "AlgebraicNumber(Pow(Integer(2), Rational(1, 2)), [Integer(1), Integer(0)])") + a = AlgebraicNumber(root(-2, 3)) + sT(a, "AlgebraicNumber(Pow(Integer(-2), Rational(1, 3)), [Integer(1), Integer(0)])") + + +def test_PolyRing(): + assert srepr(ring("x", ZZ, lex)[0]) == "PolyRing((Symbol('x'),), ZZ, lex)" + assert srepr(ring("x,y", QQ, grlex)[0]) == "PolyRing((Symbol('x'), Symbol('y')), QQ, grlex)" + assert srepr(ring("x,y,z", ZZ["t"], lex)[0]) == "PolyRing((Symbol('x'), Symbol('y'), Symbol('z')), ZZ[t], lex)" + + +def test_FracField(): + assert srepr(field("x", ZZ, lex)[0]) == "FracField((Symbol('x'),), ZZ, lex)" + assert srepr(field("x,y", QQ, grlex)[0]) == "FracField((Symbol('x'), Symbol('y')), QQ, grlex)" + assert srepr(field("x,y,z", ZZ["t"], lex)[0]) == "FracField((Symbol('x'), Symbol('y'), Symbol('z')), ZZ[t], lex)" + + +def test_PolyElement(): + R, x, y = ring("x,y", ZZ) + assert srepr(3*x**2*y + 1) == "PolyElement(PolyRing((Symbol('x'), Symbol('y')), ZZ, lex), [((2, 1), 3), ((0, 0), 1)])" + + +def test_FracElement(): + F, x, y = field("x,y", ZZ) + assert srepr((3*x**2*y + 1)/(x - y**2)) == "FracElement(FracField((Symbol('x'), Symbol('y')), ZZ, lex), [((2, 1), 3), ((0, 0), 1)], [((1, 0), 1), ((0, 2), -1)])" + + +def test_FractionField(): + assert srepr(QQ.frac_field(x)) == \ + "FractionField(FracField((Symbol('x'),), QQ, lex))" + assert srepr(QQ.frac_field(x, y, order=grlex)) == \ + "FractionField(FracField((Symbol('x'), Symbol('y')), QQ, grlex))" + + +def test_PolynomialRingBase(): + assert srepr(ZZ.old_poly_ring(x)) == \ + "GlobalPolynomialRing(ZZ, Symbol('x'))" + assert srepr(ZZ[x].old_poly_ring(y)) == \ + "GlobalPolynomialRing(ZZ[x], Symbol('y'))" + assert srepr(QQ.frac_field(x).old_poly_ring(y)) == \ + "GlobalPolynomialRing(FractionField(FracField((Symbol('x'),), QQ, lex)), Symbol('y'))" + + +def test_DMP(): + p1 = DMP([1, 2], ZZ) + p2 = ZZ.old_poly_ring(x)([1, 2]) + if GROUND_TYPES != 'flint': + assert srepr(p1) == "DMP_Python([1, 2], ZZ)" + assert srepr(p2) == "DMP_Python([1, 2], ZZ)" + else: + assert srepr(p1) == "DUP_Flint([1, 2], ZZ)" + assert srepr(p2) == "DUP_Flint([1, 2], ZZ)" + + +def test_FiniteExtension(): + assert srepr(FiniteExtension(Poly(x**2 + 1, x))) == \ + "FiniteExtension(Poly(x**2 + 1, x, domain='ZZ'))" + + +def test_ExtensionElement(): + A = FiniteExtension(Poly(x**2 + 1, x)) + if GROUND_TYPES != 'flint': + ans = "ExtElem(DMP_Python([1, 0], ZZ), FiniteExtension(Poly(x**2 + 1, x, domain='ZZ')))" + else: + ans = "ExtElem(DUP_Flint([1, 0], ZZ), FiniteExtension(Poly(x**2 + 1, x, domain='ZZ')))" + assert srepr(A.generator) == ans + +def test_BooleanAtom(): + assert srepr(true) == "true" + assert srepr(false) == "false" + + +def test_Integers(): + sT(S.Integers, "Integers") + + +def test_Naturals(): + sT(S.Naturals, "Naturals") + + +def test_Naturals0(): + sT(S.Naturals0, "Naturals0") + + +def test_Reals(): + sT(S.Reals, "Reals") + + +def test_matrix_expressions(): + n = symbols('n', integer=True) + A = MatrixSymbol("A", n, n) + B = MatrixSymbol("B", n, n) + sT(A, "MatrixSymbol(Str('A'), Symbol('n', integer=True), Symbol('n', integer=True))") + sT(A*B, "MatMul(MatrixSymbol(Str('A'), Symbol('n', integer=True), Symbol('n', integer=True)), MatrixSymbol(Str('B'), Symbol('n', integer=True), Symbol('n', integer=True)))") + sT(A + B, "MatAdd(MatrixSymbol(Str('A'), Symbol('n', integer=True), Symbol('n', integer=True)), MatrixSymbol(Str('B'), Symbol('n', integer=True), Symbol('n', integer=True)))") + + +def test_Cycle(): + # FIXME: sT fails because Cycle is not immutable and calling srepr(Cycle(1, 2)) + # adds keys to the Cycle dict (GH-17661) + #import_stmt = "from sympy.combinatorics import Cycle" + #sT(Cycle(1, 2), "Cycle(1, 2)", import_stmt) + assert srepr(Cycle(1, 2)) == "Cycle(1, 2)" + + +def test_Permutation(): + import_stmt = "from sympy.combinatorics import Permutation" + sT(Permutation(1, 2)(3, 4), "Permutation([0, 2, 1, 4, 3])", import_stmt, perm_cyclic=False) + sT(Permutation(1, 2)(3, 4), "Permutation(1, 2)(3, 4)", import_stmt, perm_cyclic=True) + + with warns_deprecated_sympy(): + old_print_cyclic = Permutation.print_cyclic + Permutation.print_cyclic = False + sT(Permutation(1, 2)(3, 4), "Permutation([0, 2, 1, 4, 3])", import_stmt) + Permutation.print_cyclic = old_print_cyclic + +def test_dict(): + from sympy.abc import x, y, z + d = {} + assert srepr(d) == "{}" + d = {x: y} + assert srepr(d) == "{Symbol('x'): Symbol('y')}" + d = {x: y, y: z} + assert srepr(d) in ( + "{Symbol('x'): Symbol('y'), Symbol('y'): Symbol('z')}", + "{Symbol('y'): Symbol('z'), Symbol('x'): Symbol('y')}", + ) + d = {x: {y: z}} + assert srepr(d) == "{Symbol('x'): {Symbol('y'): Symbol('z')}}" + +def test_set(): + from sympy.abc import x, y + s = set() + assert srepr(s) == "set()" + s = {x, y} + assert srepr(s) in ("{Symbol('x'), Symbol('y')}", "{Symbol('y'), Symbol('x')}") + +def test_Predicate(): + sT(Q.even, "Q.even") + +def test_AppliedPredicate(): + sT(Q.even(Symbol('z')), "AppliedPredicate(Q.even, Symbol('z'))") diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_rust.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_rust.py new file mode 100644 index 0000000000000000000000000000000000000000..c81d592faca0d4a31e5a9618a48d67cb19ca94d8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_rust.py @@ -0,0 +1,363 @@ +from sympy.core import (S, pi, oo, symbols, Rational, Integer, + GoldenRatio, EulerGamma, Catalan, Lambda, Dummy, + Eq, Ne, Le, Lt, Gt, Ge, Mod) +from sympy.functions import (Piecewise, sin, cos, Abs, exp, ceiling, sqrt, + sign, floor) +from sympy.logic import ITE +from sympy.testing.pytest import raises +from sympy.utilities.lambdify import implemented_function +from sympy.tensor import IndexedBase, Idx +from sympy.matrices import MatrixSymbol, SparseMatrix, Matrix + +from sympy.printing.codeprinter import rust_code + +x, y, z = symbols('x,y,z', integer=False, real=True) +k, m, n = symbols('k,m,n', integer=True) + + +def test_Integer(): + assert rust_code(Integer(42)) == "42" + assert rust_code(Integer(-56)) == "-56" + + +def test_Relational(): + assert rust_code(Eq(x, y)) == "x == y" + assert rust_code(Ne(x, y)) == "x != y" + assert rust_code(Le(x, y)) == "x <= y" + assert rust_code(Lt(x, y)) == "x < y" + assert rust_code(Gt(x, y)) == "x > y" + assert rust_code(Ge(x, y)) == "x >= y" + + +def test_Rational(): + assert rust_code(Rational(3, 7)) == "3_f64/7.0" + assert rust_code(Rational(18, 9)) == "2" + assert rust_code(Rational(3, -7)) == "-3_f64/7.0" + assert rust_code(Rational(-3, -7)) == "3_f64/7.0" + assert rust_code(x + Rational(3, 7)) == "x + 3_f64/7.0" + assert rust_code(Rational(3, 7)*x) == "(3_f64/7.0)*x" + + +def test_basic_ops(): + assert rust_code(x + y) == "x + y" + assert rust_code(x - y) == "x - y" + assert rust_code(x * y) == "x*y" + assert rust_code(x / y) == "x*y.recip()" + assert rust_code(-x) == "-x" + assert rust_code(2 * x) == "2.0*x" + assert rust_code(y + 2) == "y + 2.0" + assert rust_code(x + n) == "n as f64 + x" + +def test_printmethod(): + class fabs(Abs): + def _rust_code(self, printer): + return "%s.fabs()" % printer._print(self.args[0]) + assert rust_code(fabs(x)) == "x.fabs()" + a = MatrixSymbol("a", 1, 3) + assert rust_code(a[0,0]) == 'a[0]' + + +def test_Functions(): + assert rust_code(sin(x) ** cos(x)) == "x.sin().powf(x.cos())" + assert rust_code(abs(x)) == "x.abs()" + assert rust_code(ceiling(x)) == "x.ceil()" + assert rust_code(floor(x)) == "x.floor()" + + # Automatic rewrite + assert rust_code(Mod(x, 3)) == 'x - 3.0*((1_f64/3.0)*x).floor()' + + +def test_Pow(): + assert rust_code(1/x) == "x.recip()" + assert rust_code(x**-1) == rust_code(x**-1.0) == "x.recip()" + assert rust_code(sqrt(x)) == "x.sqrt()" + assert rust_code(x**S.Half) == rust_code(x**0.5) == "x.sqrt()" + + assert rust_code(1/sqrt(x)) == "x.sqrt().recip()" + assert rust_code(x**-S.Half) == rust_code(x**-0.5) == "x.sqrt().recip()" + + assert rust_code(1/pi) == "PI.recip()" + assert rust_code(pi**-1) == rust_code(pi**-1.0) == "PI.recip()" + assert rust_code(pi**-0.5) == "PI.sqrt().recip()" + + assert rust_code(x**Rational(1, 3)) == "x.cbrt()" + assert rust_code(2**x) == "x.exp2()" + assert rust_code(exp(x)) == "x.exp()" + assert rust_code(x**3) == "x.powi(3)" + assert rust_code(x**(y**3)) == "x.powf(y.powi(3))" + assert rust_code(x**Rational(2, 3)) == "x.powf(2_f64/3.0)" + + g = implemented_function('g', Lambda(x, 2*x)) + assert rust_code(1/(g(x)*3.5)**(x - y**x)/(x**2 + y)) == \ + "(3.5*2.0*x).powf(-x + y.powf(x))/(x.powi(2) + y)" + _cond_cfunc = [(lambda base, exp: exp.is_integer, "dpowi", 1), + (lambda base, exp: not exp.is_integer, "pow", 1)] + assert rust_code(x**3, user_functions={'Pow': _cond_cfunc}) == 'x.dpowi(3)' + assert rust_code(x**3.2, user_functions={'Pow': _cond_cfunc}) == 'x.pow(3.2)' + + +def test_constants(): + assert rust_code(pi) == "PI" + assert rust_code(oo) == "INFINITY" + assert rust_code(S.Infinity) == "INFINITY" + assert rust_code(-oo) == "NEG_INFINITY" + assert rust_code(S.NegativeInfinity) == "NEG_INFINITY" + assert rust_code(S.NaN) == "NAN" + assert rust_code(exp(1)) == "E" + assert rust_code(S.Exp1) == "E" + + +def test_constants_other(): + assert rust_code(2*GoldenRatio) == "const GoldenRatio: f64 = %s;\n2.0*GoldenRatio" % GoldenRatio.evalf(17) + assert rust_code( + 2*Catalan) == "const Catalan: f64 = %s;\n2.0*Catalan" % Catalan.evalf(17) + assert rust_code(2*EulerGamma) == "const EulerGamma: f64 = %s;\n2.0*EulerGamma" % EulerGamma.evalf(17) + + +def test_boolean(): + assert rust_code(True) == "true" + assert rust_code(S.true) == "true" + assert rust_code(False) == "false" + assert rust_code(S.false) == "false" + assert rust_code(k & m) == "k && m" + assert rust_code(k | m) == "k || m" + assert rust_code(~k) == "!k" + assert rust_code(k & m & n) == "k && m && n" + assert rust_code(k | m | n) == "k || m || n" + assert rust_code((k & m) | n) == "n || k && m" + assert rust_code((k | m) & n) == "n && (k || m)" + + +def test_Piecewise(): + expr = Piecewise((x, x < 1), (x + 2, True)) + assert rust_code(expr) == ( + "if (x < 1.0) {\n" + " x\n" + "} else {\n" + " x + 2.0\n" + "}") + assert rust_code(expr, assign_to="r") == ( + "r = if (x < 1.0) {\n" + " x\n" + "} else {\n" + " x + 2.0\n" + "};") + assert rust_code(expr, assign_to="r", inline=True) == ( + "r = if (x < 1.0) { x } else { x + 2.0 };") + expr = Piecewise((x, x < 1), (x + 1, x < 5), (x + 2, True)) + assert rust_code(expr, inline=True) == ( + "if (x < 1.0) { x } else if (x < 5.0) { x + 1.0 } else { x + 2.0 }") + assert rust_code(expr, assign_to="r", inline=True) == ( + "r = if (x < 1.0) { x } else if (x < 5.0) { x + 1.0 } else { x + 2.0 };") + assert rust_code(expr, assign_to="r") == ( + "r = if (x < 1.0) {\n" + " x\n" + "} else if (x < 5.0) {\n" + " x + 1.0\n" + "} else {\n" + " x + 2.0\n" + "};") + expr = 2*Piecewise((x, x < 1), (x + 1, x < 5), (x + 2, True)) + assert rust_code(expr, inline=True) == ( + "2.0*if (x < 1.0) { x } else if (x < 5.0) { x + 1.0 } else { x + 2.0 }") + expr = 2*Piecewise((x, x < 1), (x + 1, x < 5), (x + 2, True)) - 42 + assert rust_code(expr, inline=True) == ( + "2.0*if (x < 1.0) { x } else if (x < 5.0) { x + 1.0 } else { x + 2.0 } - 42.0") + # Check that Piecewise without a True (default) condition error + expr = Piecewise((x, x < 1), (x**2, x > 1), (sin(x), x > 0)) + raises(ValueError, lambda: rust_code(expr)) + + +def test_dereference_printing(): + expr = x + y + sin(z) + z + assert rust_code(expr, dereference=[z]) == "x + y + (*z) + (*z).sin()" + + +def test_sign(): + expr = sign(x) * y + assert rust_code(expr) == "y*(if (x == 0.0) { 0.0 } else { (x).signum() }) as f64" + assert rust_code(expr, assign_to='r') == "r = y*(if (x == 0.0) { 0.0 } else { (x).signum() }) as f64;" + + expr = sign(x + y) + 42 + assert rust_code(expr) == "(if (x + y == 0.0) { 0.0 } else { (x + y).signum() }) + 42" + assert rust_code(expr, assign_to='r') == "r = (if (x + y == 0.0) { 0.0 } else { (x + y).signum() }) + 42;" + + expr = sign(cos(x)) + assert rust_code(expr) == "(if (x.cos() == 0.0) { 0.0 } else { (x.cos()).signum() })" + + +def test_reserved_words(): + + x, y = symbols("x if") + + expr = sin(y) + assert rust_code(expr) == "if_.sin()" + assert rust_code(expr, dereference=[y]) == "(*if_).sin()" + assert rust_code(expr, reserved_word_suffix='_unreserved') == "if_unreserved.sin()" + + with raises(ValueError): + rust_code(expr, error_on_reserved=True) + + +def test_ITE(): + ekpr = ITE(k < 1, m, n) + assert rust_code(ekpr) == ( + "if (k < 1) {\n" + " m\n" + "} else {\n" + " n\n" + "}") + + +def test_Indexed(): + n, m, o = symbols('n m o', integer=True) + i, j, k = Idx('i', n), Idx('j', m), Idx('k', o) + + x = IndexedBase('x')[j] + assert rust_code(x) == "x[j]" + + A = IndexedBase('A')[i, j] + assert rust_code(A) == "A[m*i + j]" + + B = IndexedBase('B')[i, j, k] + assert rust_code(B) == "B[m*o*i + o*j + k]" + + +def test_dummy_loops(): + i, m = symbols('i m', integer=True, cls=Dummy) + x = IndexedBase('x') + y = IndexedBase('y') + i = Idx(i, m) + + assert rust_code(x[i], assign_to=y[i]) == ( + "for i in 0..m {\n" + " y[i] = x[i];\n" + "}") + + +def test_loops(): + m, n = symbols('m n', integer=True) + A = IndexedBase('A') + x = IndexedBase('x') + y = IndexedBase('y') + z = IndexedBase('z') + i = Idx('i', m) + j = Idx('j', n) + + assert rust_code(A[i, j]*x[j], assign_to=y[i]) == ( + "for i in 0..m {\n" + " y[i] = 0;\n" + "}\n" + "for i in 0..m {\n" + " for j in 0..n {\n" + " y[i] = A[n*i + j]*x[j] + y[i];\n" + " }\n" + "}") + + assert rust_code(A[i, j]*x[j] + x[i] + z[i], assign_to=y[i]) == ( + "for i in 0..m {\n" + " y[i] = x[i] + z[i];\n" + "}\n" + "for i in 0..m {\n" + " for j in 0..n {\n" + " y[i] = A[n*i + j]*x[j] + y[i];\n" + " }\n" + "}") + + +def test_loops_multiple_contractions(): + n, m, o, p = symbols('n m o p', integer=True) + a = IndexedBase('a') + b = IndexedBase('b') + y = IndexedBase('y') + i = Idx('i', m) + j = Idx('j', n) + k = Idx('k', o) + l = Idx('l', p) + + assert rust_code(b[j, k, l]*a[i, j, k, l], assign_to=y[i]) == ( + "for i in 0..m {\n" + " y[i] = 0;\n" + "}\n" + "for i in 0..m {\n" + " for j in 0..n {\n" + " for k in 0..o {\n" + " for l in 0..p {\n" + " y[i] = a[%s]*b[%s] + y[i];\n" % (i*n*o*p + j*o*p + k*p + l, j*o*p + k*p + l) +\ + " }\n" + " }\n" + " }\n" + "}") + + +def test_loops_addfactor(): + m, n, o, p = symbols('m n o p', integer=True) + a = IndexedBase('a') + b = IndexedBase('b') + c = IndexedBase('c') + y = IndexedBase('y') + i = Idx('i', m) + j = Idx('j', n) + k = Idx('k', o) + l = Idx('l', p) + + code = rust_code((a[i, j, k, l] + b[i, j, k, l])*c[j, k, l], assign_to=y[i]) + assert code == ( + "for i in 0..m {\n" + " y[i] = 0;\n" + "}\n" + "for i in 0..m {\n" + " for j in 0..n {\n" + " for k in 0..o {\n" + " for l in 0..p {\n" + " y[i] = (a[%s] + b[%s])*c[%s] + y[i];\n" % (i*n*o*p + j*o*p + k*p + l, i*n*o*p + j*o*p + k*p + l, j*o*p + k*p + l) +\ + " }\n" + " }\n" + " }\n" + "}") + + +def test_settings(): + raises(TypeError, lambda: rust_code(sin(x), method="garbage")) + + +def test_inline_function(): + x = symbols('x') + g = implemented_function('g', Lambda(x, 2*x)) + assert rust_code(g(x)) == "2*x" + + g = implemented_function('g', Lambda(x, 2*x/Catalan)) + assert rust_code(g(x)) == ( + "const Catalan: f64 = %s;\n2.0*x/Catalan" % Catalan.evalf(17)) + + A = IndexedBase('A') + i = Idx('i', symbols('n', integer=True)) + g = implemented_function('g', Lambda(x, x*(1 + x)*(2 + x))) + assert rust_code(g(A[i]), assign_to=A[i]) == ( + "for i in 0..n {\n" + " A[i] = (A[i] + 1)*(A[i] + 2)*A[i];\n" + "}") + + +def test_user_functions(): + x = symbols('x', integer=False) + n = symbols('n', integer=True) + custom_functions = { + "ceiling": "ceil", + "Abs": [(lambda x: not x.is_integer, "fabs", 4), (lambda x: x.is_integer, "abs", 4)], + } + assert rust_code(ceiling(x), user_functions=custom_functions) == "x.ceil()" + assert rust_code(Abs(x), user_functions=custom_functions) == "fabs(x)" + assert rust_code(Abs(n), user_functions=custom_functions) == "abs(n)" + + +def test_matrix(): + assert rust_code(Matrix([1, 2, 3])) == '[1, 2, 3]' + with raises(ValueError): + rust_code(Matrix([[1, 2, 3]])) + + +def test_sparse_matrix(): + # gh-15791 + with raises(NotImplementedError): + rust_code(SparseMatrix([[1, 2, 3]])) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_smtlib.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_smtlib.py new file mode 100644 index 0000000000000000000000000000000000000000..48ff3d432d9042bf178f4e52dc46c787059937a3 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_smtlib.py @@ -0,0 +1,553 @@ +import contextlib +import itertools +import re +import typing +from enum import Enum +from typing import Callable + +import sympy +from sympy import Add, Implies, sqrt +from sympy.core import Mul, Pow +from sympy.core import (S, pi, symbols, Function, Rational, Integer, + Symbol, Eq, Ne, Le, Lt, Gt, Ge) +from sympy.functions import Piecewise, exp, sin, cos +from sympy.assumptions.ask import Q +from sympy.printing.smtlib import smtlib_code +from sympy.testing.pytest import raises, Failed + +x, y, z = symbols('x,y,z') + + +class _W(Enum): + DEFAULTING_TO_FLOAT = re.compile("Could not infer type of `.+`. Defaulting to float.", re.IGNORECASE) + WILL_NOT_DECLARE = re.compile("Non-Symbol/Function `.+` will not be declared.", re.IGNORECASE) + WILL_NOT_ASSERT = re.compile("Non-Boolean expression `.+` will not be asserted. Converting to SMTLib verbatim.", re.IGNORECASE) + + +@contextlib.contextmanager +def _check_warns(expected: typing.Iterable[_W]): + warns: typing.List[str] = [] + log_warn = warns.append + yield log_warn + + errors = [] + for i, (w, e) in enumerate(itertools.zip_longest(warns, expected)): + if not e: + errors += [f"[{i}] Received unexpected warning `{w}`."] + elif not w: + errors += [f"[{i}] Did not receive expected warning `{e.name}`."] + elif not e.value.match(w): + errors += [f"[{i}] Warning `{w}` does not match expected {e.name}."] + + if errors: raise Failed('\n'.join(errors)) + + +def test_Integer(): + with _check_warns([_W.WILL_NOT_ASSERT] * 2) as w: + assert smtlib_code(Integer(67), log_warn=w) == "67" + assert smtlib_code(Integer(-1), log_warn=w) == "-1" + with _check_warns([]) as w: + assert smtlib_code(Integer(67)) == "67" + assert smtlib_code(Integer(-1)) == "-1" + + +def test_Rational(): + with _check_warns([_W.WILL_NOT_ASSERT] * 4) as w: + assert smtlib_code(Rational(3, 7), log_warn=w) == "(/ 3 7)" + assert smtlib_code(Rational(18, 9), log_warn=w) == "2" + assert smtlib_code(Rational(3, -7), log_warn=w) == "(/ -3 7)" + assert smtlib_code(Rational(-3, -7), log_warn=w) == "(/ 3 7)" + + with _check_warns([_W.DEFAULTING_TO_FLOAT, _W.WILL_NOT_ASSERT] * 2) as w: + assert smtlib_code(x + Rational(3, 7), auto_declare=False, log_warn=w) == "(+ (/ 3 7) x)" + assert smtlib_code(Rational(3, 7) * x, log_warn=w) == "(declare-const x Real)\n" \ + "(* (/ 3 7) x)" + + +def test_Relational(): + with _check_warns([_W.DEFAULTING_TO_FLOAT] * 12) as w: + assert smtlib_code(Eq(x, y), auto_declare=False, log_warn=w) == "(assert (= x y))" + assert smtlib_code(Ne(x, y), auto_declare=False, log_warn=w) == "(assert (not (= x y)))" + assert smtlib_code(Le(x, y), auto_declare=False, log_warn=w) == "(assert (<= x y))" + assert smtlib_code(Lt(x, y), auto_declare=False, log_warn=w) == "(assert (< x y))" + assert smtlib_code(Gt(x, y), auto_declare=False, log_warn=w) == "(assert (> x y))" + assert smtlib_code(Ge(x, y), auto_declare=False, log_warn=w) == "(assert (>= x y))" + + +def test_AppliedBinaryRelation(): + with _check_warns([_W.DEFAULTING_TO_FLOAT] * 12) as w: + assert smtlib_code(Q.eq(x, y), auto_declare=False, log_warn=w) == "(assert (= x y))" + assert smtlib_code(Q.ne(x, y), auto_declare=False, log_warn=w) == "(assert (not (= x y)))" + assert smtlib_code(Q.lt(x, y), auto_declare=False, log_warn=w) == "(assert (< x y))" + assert smtlib_code(Q.le(x, y), auto_declare=False, log_warn=w) == "(assert (<= x y))" + assert smtlib_code(Q.gt(x, y), auto_declare=False, log_warn=w) == "(assert (> x y))" + assert smtlib_code(Q.ge(x, y), auto_declare=False, log_warn=w) == "(assert (>= x y))" + + raises(ValueError, lambda: smtlib_code(Q.complex(x), log_warn=w)) + + +def test_AppliedPredicate(): + with _check_warns([_W.DEFAULTING_TO_FLOAT] * 6) as w: + assert smtlib_code(Q.positive(x), auto_declare=False, log_warn=w) == "(assert (> x 0))" + assert smtlib_code(Q.negative(x), auto_declare=False, log_warn=w) == "(assert (< x 0))" + assert smtlib_code(Q.zero(x), auto_declare=False, log_warn=w) == "(assert (= x 0))" + assert smtlib_code(Q.nonpositive(x), auto_declare=False, log_warn=w) == "(assert (<= x 0))" + assert smtlib_code(Q.nonnegative(x), auto_declare=False, log_warn=w) == "(assert (>= x 0))" + assert smtlib_code(Q.nonzero(x), auto_declare=False, log_warn=w) == "(assert (not (= x 0)))" + +def test_Function(): + with _check_warns([_W.DEFAULTING_TO_FLOAT, _W.WILL_NOT_ASSERT]) as w: + assert smtlib_code(sin(x) ** cos(x), auto_declare=False, log_warn=w) == "(pow (sin x) (cos x))" + + with _check_warns([_W.WILL_NOT_ASSERT]) as w: + assert smtlib_code( + abs(x), + symbol_table={x: int, y: bool}, + known_types={int: "INTEGER_TYPE"}, + known_functions={sympy.Abs: "ABSOLUTE_VALUE_OF"}, + log_warn=w + ) == "(declare-const x INTEGER_TYPE)\n" \ + "(ABSOLUTE_VALUE_OF x)" + + my_fun1 = Function('f1') + with _check_warns([_W.WILL_NOT_ASSERT]) as w: + assert smtlib_code( + my_fun1(x), + symbol_table={my_fun1: Callable[[bool], float]}, + log_warn=w + ) == "(declare-const x Bool)\n" \ + "(declare-fun f1 (Bool) Real)\n" \ + "(f1 x)" + + with _check_warns([]) as w: + assert smtlib_code( + my_fun1(x), + symbol_table={my_fun1: Callable[[bool], bool]}, + log_warn=w + ) == "(declare-const x Bool)\n" \ + "(declare-fun f1 (Bool) Bool)\n" \ + "(assert (f1 x))" + + assert smtlib_code( + Eq(my_fun1(x, z), y), + symbol_table={my_fun1: Callable[[int, bool], bool]}, + log_warn=w + ) == "(declare-const x Int)\n" \ + "(declare-const y Bool)\n" \ + "(declare-const z Bool)\n" \ + "(declare-fun f1 (Int Bool) Bool)\n" \ + "(assert (= (f1 x z) y))" + + assert smtlib_code( + Eq(my_fun1(x, z), y), + symbol_table={my_fun1: Callable[[int, bool], bool]}, + known_functions={my_fun1: "MY_KNOWN_FUN", Eq: '=='}, + log_warn=w + ) == "(declare-const x Int)\n" \ + "(declare-const y Bool)\n" \ + "(declare-const z Bool)\n" \ + "(assert (== (MY_KNOWN_FUN x z) y))" + + with _check_warns([_W.DEFAULTING_TO_FLOAT] * 3) as w: + assert smtlib_code( + Eq(my_fun1(x, z), y), + known_functions={my_fun1: "MY_KNOWN_FUN", Eq: '=='}, + log_warn=w + ) == "(declare-const x Real)\n" \ + "(declare-const y Real)\n" \ + "(declare-const z Real)\n" \ + "(assert (== (MY_KNOWN_FUN x z) y))" + + +def test_Pow(): + with _check_warns([_W.DEFAULTING_TO_FLOAT, _W.WILL_NOT_ASSERT]) as w: + assert smtlib_code(x ** 3, auto_declare=False, log_warn=w) == "(pow x 3)" + with _check_warns([_W.DEFAULTING_TO_FLOAT, _W.DEFAULTING_TO_FLOAT, _W.WILL_NOT_ASSERT]) as w: + assert smtlib_code(x ** (y ** 3), auto_declare=False, log_warn=w) == "(pow x (pow y 3))" + with _check_warns([_W.DEFAULTING_TO_FLOAT, _W.WILL_NOT_ASSERT]) as w: + assert smtlib_code(x ** Rational(2, 3), auto_declare=False, log_warn=w) == '(pow x (/ 2 3))' + + a = Symbol('a', integer=True) + b = Symbol('b', real=True) + c = Symbol('c') + + def g(x): return 2 * x + + # if x=1, y=2, then expr=2.333... + expr = 1 / (g(a) * 3.5) ** (a - b ** a) / (a ** 2 + b) + + with _check_warns([]) as w: + assert smtlib_code( + [ + Eq(a < 2, c), + Eq(b > a, c), + c & True, + Eq(expr, 2 + Rational(1, 3)) + ], + log_warn=w + ) == '(declare-const a Int)\n' \ + '(declare-const b Real)\n' \ + '(declare-const c Bool)\n' \ + '(assert (= (< a 2) c))\n' \ + '(assert (= (> b a) c))\n' \ + '(assert c)\n' \ + '(assert (= ' \ + '(* (pow (* 7.0 a) (+ (pow b a) (* -1 a))) (pow (+ b (pow a 2)) -1)) ' \ + '(/ 7 3)' \ + '))' + + with _check_warns([_W.DEFAULTING_TO_FLOAT, _W.WILL_NOT_ASSERT]) as w: + assert smtlib_code( + Mul(-2, c, Pow(Mul(b, b, evaluate=False), -1, evaluate=False), evaluate=False), + log_warn=w + ) == '(declare-const b Real)\n' \ + '(declare-const c Real)\n' \ + '(* -2 c (pow (* b b) -1))' + + +def test_basic_ops(): + with _check_warns([_W.DEFAULTING_TO_FLOAT, _W.DEFAULTING_TO_FLOAT, _W.WILL_NOT_ASSERT]) as w: + assert smtlib_code(x * y, auto_declare=False, log_warn=w) == "(* x y)" + + with _check_warns([_W.DEFAULTING_TO_FLOAT, _W.DEFAULTING_TO_FLOAT, _W.WILL_NOT_ASSERT]) as w: + assert smtlib_code(x + y, auto_declare=False, log_warn=w) == "(+ x y)" + + # with _check_warns([_SmtlibWarnings.DEFAULTING_TO_FLOAT, _SmtlibWarnings.DEFAULTING_TO_FLOAT, _SmtlibWarnings.WILL_NOT_ASSERT]) as w: + # todo: implement re-write, currently does '(+ x (* -1 y))' instead + # assert smtlib_code(x - y, auto_declare=False, log_warn=w) == "(- x y)" + + with _check_warns([_W.DEFAULTING_TO_FLOAT, _W.WILL_NOT_ASSERT]) as w: + assert smtlib_code(-x, auto_declare=False, log_warn=w) == "(* -1 x)" + + +def test_quantifier_extensions(): + from sympy.logic.boolalg import Boolean + from sympy import Interval, Tuple, sympify + + # start For-all quantifier class example + class ForAll(Boolean): + def _smtlib(self, printer): + bound_symbol_declarations = [ + printer._s_expr(sym.name, [ + printer._known_types[printer.symbol_table[sym]], + Interval(start, end) + ]) for sym, start, end in self.limits + ] + return printer._s_expr('forall', [ + printer._s_expr('', bound_symbol_declarations), + self.function + ]) + + @property + def bound_symbols(self): + return {s for s, _, _ in self.limits} + + @property + def free_symbols(self): + bound_symbol_names = {s.name for s in self.bound_symbols} + return { + s for s in self.function.free_symbols + if s.name not in bound_symbol_names + } + + def __new__(cls, *args): + limits = [sympify(a) for a in args if isinstance(a, (tuple, Tuple))] + function = [sympify(a) for a in args if isinstance(a, Boolean)] + assert len(limits) + len(function) == len(args) + assert len(function) == 1 + function = function[0] + + if isinstance(function, ForAll): return ForAll.__new__( + ForAll, *(limits + function.limits), function.function + ) + inst = Boolean.__new__(cls) + inst._args = tuple(limits + [function]) + inst.limits = limits + inst.function = function + return inst + + # end For-All Quantifier class example + + f = Function('f') + with _check_warns([_W.DEFAULTING_TO_FLOAT]) as w: + assert smtlib_code( + ForAll((x, -42, +21), Eq(f(x), f(x))), + symbol_table={f: Callable[[float], float]}, + log_warn=w + ) == '(assert (forall ( (x Real [-42, 21])) true))' + + with _check_warns([_W.DEFAULTING_TO_FLOAT] * 2) as w: + assert smtlib_code( + ForAll( + (x, -42, +21), (y, -100, 3), + Implies(Eq(x, y), Eq(f(x), f(y))) + ), + symbol_table={f: Callable[[float], float]}, + log_warn=w + ) == '(declare-fun f (Real) Real)\n' \ + '(assert (' \ + 'forall ( (x Real [-42, 21]) (y Real [-100, 3])) ' \ + '(=> (= x y) (= (f x) (f y)))' \ + '))' + + a = Symbol('a', integer=True) + b = Symbol('b', real=True) + c = Symbol('c') + + with _check_warns([]) as w: + assert smtlib_code( + ForAll( + (a, 2, 100), ForAll( + (b, 2, 100), + Implies(a < b, sqrt(a) < b) | c + )), + log_warn=w + ) == '(declare-const c Bool)\n' \ + '(assert (forall ( (a Int [2, 100]) (b Real [2, 100])) ' \ + '(or c (=> (< a b) (< (pow a (/ 1 2)) b)))' \ + '))' + + +def test_mix_number_mult_symbols(): + with _check_warns([_W.WILL_NOT_ASSERT]) as w: + assert smtlib_code( + 1 / pi, + known_constants={pi: "MY_PI"}, + log_warn=w + ) == '(pow MY_PI -1)' + + with _check_warns([_W.WILL_NOT_ASSERT]) as w: + assert smtlib_code( + [ + Eq(pi, 3.14, evaluate=False), + 1 / pi, + ], + known_constants={pi: "MY_PI"}, + log_warn=w + ) == '(assert (= MY_PI 3.14))\n' \ + '(pow MY_PI -1)' + + with _check_warns([_W.WILL_NOT_ASSERT]) as w: + assert smtlib_code( + Add(S.Zero, S.One, S.NegativeOne, S.Half, + S.Exp1, S.Pi, S.GoldenRatio, evaluate=False), + known_constants={ + S.Pi: 'p', S.GoldenRatio: 'g', + S.Exp1: 'e' + }, + known_functions={ + Add: 'plus', + exp: 'exp' + }, + precision=3, + log_warn=w + ) == '(plus 0 1 -1 (/ 1 2) (exp 1) p g)' + + with _check_warns([_W.WILL_NOT_ASSERT]) as w: + assert smtlib_code( + Add(S.Zero, S.One, S.NegativeOne, S.Half, + S.Exp1, S.Pi, S.GoldenRatio, evaluate=False), + known_constants={ + S.Pi: 'p' + }, + known_functions={ + Add: 'plus', + exp: 'exp' + }, + precision=3, + log_warn=w + ) == '(plus 0 1 -1 (/ 1 2) (exp 1) p 1.62)' + + with _check_warns([_W.WILL_NOT_ASSERT]) as w: + assert smtlib_code( + Add(S.Zero, S.One, S.NegativeOne, S.Half, + S.Exp1, S.Pi, S.GoldenRatio, evaluate=False), + known_functions={Add: 'plus'}, + precision=3, + log_warn=w + ) == '(plus 0 1 -1 (/ 1 2) 2.72 3.14 1.62)' + + with _check_warns([_W.WILL_NOT_ASSERT]) as w: + assert smtlib_code( + Add(S.Zero, S.One, S.NegativeOne, S.Half, + S.Exp1, S.Pi, S.GoldenRatio, evaluate=False), + known_constants={S.Exp1: 'e'}, + known_functions={Add: 'plus'}, + precision=3, + log_warn=w + ) == '(plus 0 1 -1 (/ 1 2) e 3.14 1.62)' + + +def test_boolean(): + with _check_warns([]) as w: + assert smtlib_code(x & y, log_warn=w) == '(declare-const x Bool)\n' \ + '(declare-const y Bool)\n' \ + '(assert (and x y))' + assert smtlib_code(x | y, log_warn=w) == '(declare-const x Bool)\n' \ + '(declare-const y Bool)\n' \ + '(assert (or x y))' + assert smtlib_code(~x, log_warn=w) == '(declare-const x Bool)\n' \ + '(assert (not x))' + assert smtlib_code(x & y & z, log_warn=w) == '(declare-const x Bool)\n' \ + '(declare-const y Bool)\n' \ + '(declare-const z Bool)\n' \ + '(assert (and x y z))' + + with _check_warns([_W.DEFAULTING_TO_FLOAT]) as w: + assert smtlib_code((x & ~y) | (z > 3), log_warn=w) == '(declare-const x Bool)\n' \ + '(declare-const y Bool)\n' \ + '(declare-const z Real)\n' \ + '(assert (or (> z 3) (and x (not y))))' + + f = Function('f') + g = Function('g') + h = Function('h') + with _check_warns([_W.DEFAULTING_TO_FLOAT]) as w: + assert smtlib_code( + [Gt(f(x), y), + Lt(y, g(z))], + symbol_table={ + f: Callable[[bool], int], g: Callable[[bool], int], + }, log_warn=w + ) == '(declare-const x Bool)\n' \ + '(declare-const y Real)\n' \ + '(declare-const z Bool)\n' \ + '(declare-fun f (Bool) Int)\n' \ + '(declare-fun g (Bool) Int)\n' \ + '(assert (> (f x) y))\n' \ + '(assert (< y (g z)))' + + with _check_warns([]) as w: + assert smtlib_code( + [Eq(f(x), y), + Lt(y, g(z))], + symbol_table={ + f: Callable[[bool], int], g: Callable[[bool], int], + }, log_warn=w + ) == '(declare-const x Bool)\n' \ + '(declare-const y Int)\n' \ + '(declare-const z Bool)\n' \ + '(declare-fun f (Bool) Int)\n' \ + '(declare-fun g (Bool) Int)\n' \ + '(assert (= (f x) y))\n' \ + '(assert (< y (g z)))' + + with _check_warns([]) as w: + assert smtlib_code( + [Eq(f(x), y), + Eq(g(f(x)), z), + Eq(h(g(f(x))), x)], + symbol_table={ + f: Callable[[float], int], + g: Callable[[int], bool], + h: Callable[[bool], float] + }, + log_warn=w + ) == '(declare-const x Real)\n' \ + '(declare-const y Int)\n' \ + '(declare-const z Bool)\n' \ + '(declare-fun f (Real) Int)\n' \ + '(declare-fun g (Int) Bool)\n' \ + '(declare-fun h (Bool) Real)\n' \ + '(assert (= (f x) y))\n' \ + '(assert (= (g (f x)) z))\n' \ + '(assert (= (h (g (f x))) x))' + + +# todo: make smtlib_code support arrays +# def test_containers(): +# assert julia_code([1, 2, 3, [4, 5, [6, 7]], 8, [9, 10], 11]) == \ +# "Any[1, 2, 3, Any[4, 5, Any[6, 7]], 8, Any[9, 10], 11]" +# assert julia_code((1, 2, (3, 4))) == "(1, 2, (3, 4))" +# assert julia_code([1]) == "Any[1]" +# assert julia_code((1,)) == "(1,)" +# assert julia_code(Tuple(*[1, 2, 3])) == "(1, 2, 3)" +# assert julia_code((1, x * y, (3, x ** 2))) == "(1, x .* y, (3, x .^ 2))" +# # scalar, matrix, empty matrix and empty list +# assert julia_code((1, eye(3), Matrix(0, 0, []), [])) == "(1, [1 0 0;\n0 1 0;\n0 0 1], zeros(0, 0), Any[])" + +def test_smtlib_piecewise(): + with _check_warns([_W.DEFAULTING_TO_FLOAT, _W.WILL_NOT_ASSERT]) as w: + assert smtlib_code( + Piecewise((x, x < 1), + (x ** 2, True)), + auto_declare=False, + log_warn=w + ) == '(ite (< x 1) x (pow x 2))' + + with _check_warns([_W.DEFAULTING_TO_FLOAT, _W.WILL_NOT_ASSERT]) as w: + assert smtlib_code( + Piecewise((x ** 2, x < 1), + (x ** 3, x < 2), + (x ** 4, x < 3), + (x ** 5, True)), + auto_declare=False, + log_warn=w + ) == '(ite (< x 1) (pow x 2) ' \ + '(ite (< x 2) (pow x 3) ' \ + '(ite (< x 3) (pow x 4) ' \ + '(pow x 5))))' + + # Check that Piecewise without a True (default) condition error + expr = Piecewise((x, x < 1), (x ** 2, x > 1), (sin(x), x > 0)) + with _check_warns([_W.DEFAULTING_TO_FLOAT, _W.WILL_NOT_ASSERT]) as w: + raises(AssertionError, lambda: smtlib_code(expr, log_warn=w)) + + +def test_smtlib_piecewise_times_const(): + pw = Piecewise((x, x < 1), (x ** 2, True)) + with _check_warns([_W.DEFAULTING_TO_FLOAT, _W.WILL_NOT_ASSERT]) as w: + assert smtlib_code(2 * pw, log_warn=w) == '(declare-const x Real)\n(* 2 (ite (< x 1) x (pow x 2)))' + with _check_warns([_W.DEFAULTING_TO_FLOAT, _W.WILL_NOT_ASSERT]) as w: + assert smtlib_code(pw / x, log_warn=w) == '(declare-const x Real)\n(* (pow x -1) (ite (< x 1) x (pow x 2)))' + with _check_warns([_W.DEFAULTING_TO_FLOAT, _W.DEFAULTING_TO_FLOAT, _W.WILL_NOT_ASSERT]) as w: + assert smtlib_code(pw / (x * y), log_warn=w) == '(declare-const x Real)\n(declare-const y Real)\n(* (pow x -1) (pow y -1) (ite (< x 1) x (pow x 2)))' + with _check_warns([_W.DEFAULTING_TO_FLOAT, _W.WILL_NOT_ASSERT]) as w: + assert smtlib_code(pw / 3, log_warn=w) == '(declare-const x Real)\n(* (/ 1 3) (ite (< x 1) x (pow x 2)))' + + +# todo: make smtlib_code support arrays / matrices ? +# def test_smtlib_matrix_assign_to(): +# A = Matrix([[1, 2, 3]]) +# assert smtlib_code(A, assign_to='a') == "a = [1 2 3]" +# A = Matrix([[1, 2], [3, 4]]) +# assert smtlib_code(A, assign_to='A') == "A = [1 2;\n3 4]" + +# def test_julia_matrix_1x1(): +# A = Matrix([[3]]) +# B = MatrixSymbol('B', 1, 1) +# C = MatrixSymbol('C', 1, 2) +# assert julia_code(A, assign_to=B) == "B = [3]" +# raises(ValueError, lambda: julia_code(A, assign_to=C)) + +# def test_julia_matrix_elements(): +# A = Matrix([[x, 2, x * y]]) +# assert julia_code(A[0, 0] ** 2 + A[0, 1] + A[0, 2]) == "x .^ 2 + x .* y + 2" +# A = MatrixSymbol('AA', 1, 3) +# assert julia_code(A) == "AA" +# assert julia_code(A[0, 0] ** 2 + sin(A[0, 1]) + A[0, 2]) == \ +# "sin(AA[1,2]) + AA[1,1] .^ 2 + AA[1,3]" +# assert julia_code(sum(A)) == "AA[1,1] + AA[1,2] + AA[1,3]" + +def test_smtlib_boolean(): + with _check_warns([]) as w: + assert smtlib_code(True, auto_assert=False, log_warn=w) == 'true' + assert smtlib_code(True, log_warn=w) == '(assert true)' + assert smtlib_code(S.true, log_warn=w) == '(assert true)' + assert smtlib_code(S.false, log_warn=w) == '(assert false)' + assert smtlib_code(False, log_warn=w) == '(assert false)' + assert smtlib_code(False, auto_assert=False, log_warn=w) == 'false' + + +def test_not_supported(): + f = Function('f') + with _check_warns([_W.DEFAULTING_TO_FLOAT, _W.WILL_NOT_ASSERT]) as w: + raises(KeyError, lambda: smtlib_code(f(x).diff(x), symbol_table={f: Callable[[float], float]}, log_warn=w)) + with _check_warns([_W.WILL_NOT_ASSERT]) as w: + raises(KeyError, lambda: smtlib_code(S.ComplexInfinity, log_warn=w)) + + +def test_Float(): + assert smtlib_code(0.0) == "0.0" + assert smtlib_code(0.000000000000000003) == '(* 3.0 (pow 10 -18))' + assert smtlib_code(5.3) == "5.3" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_str.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_str.py new file mode 100644 index 0000000000000000000000000000000000000000..675212964b03bf9a9806088225c28d7f70971ca7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_str.py @@ -0,0 +1,1206 @@ +from sympy import MatAdd +from sympy.algebras.quaternion import Quaternion +from sympy.assumptions.ask import Q +from sympy.calculus.accumulationbounds import AccumBounds +from sympy.combinatorics.partitions import Partition +from sympy.concrete.summations import (Sum, summation) +from sympy.core.add import Add +from sympy.core.containers import (Dict, Tuple) +from sympy.core.expr import UnevaluatedExpr, Expr +from sympy.core.function import (Derivative, Function, Lambda, Subs, WildFunction) +from sympy.core.mul import Mul +from sympy.core import (Catalan, EulerGamma, GoldenRatio, TribonacciConstant) +from sympy.core.numbers import (E, Float, I, Integer, Rational, nan, oo, pi, zoo) +from sympy.core.parameters import _exp_is_pow +from sympy.core.power import Pow +from sympy.core.relational import (Eq, Rel, Ne) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol, Wild, symbols) +from sympy.functions.combinatorial.factorials import (factorial, factorial2, subfactorial) +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.functions.special.delta_functions import Heaviside +from sympy.functions.special.zeta_functions import zeta +from sympy.integrals.integrals import Integral +from sympy.logic.boolalg import (Equivalent, false, true, Xor) +from sympy.matrices.dense import Matrix +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.matrices.expressions import Identity +from sympy.matrices.expressions.slice import MatrixSlice +from sympy.matrices import SparseMatrix +from sympy.polys.polytools import factor +from sympy.series.limits import Limit +from sympy.series.order import O +from sympy.sets.sets import (Complement, FiniteSet, Interval, SymmetricDifference) +from sympy.stats import (Covariance, Expectation, Probability, Variance) +from sympy.stats.rv import RandomSymbol +from sympy.external import import_module +from sympy.physics.control.lti import TransferFunction, Series, Parallel, \ + Feedback, TransferFunctionMatrix, MIMOSeries, MIMOParallel, MIMOFeedback +from sympy.physics.units import second, joule +from sympy.polys import (Poly, rootof, RootSum, groebner, ring, field, ZZ, QQ, + ZZ_I, QQ_I, lex, grlex) +from sympy.geometry import Point, Circle, Polygon, Ellipse, Triangle +from sympy.tensor import NDimArray +from sympy.tensor.array.expressions.array_expressions import ArraySymbol, ArrayElement + +from sympy.testing.pytest import raises, warns_deprecated_sympy + +from sympy.printing import sstr, sstrrepr, StrPrinter +from sympy.physics.quantum.trace import Tr + +x, y, z, w, t = symbols('x,y,z,w,t') +d = Dummy('d') + + +def test_printmethod(): + class R(Abs): + def _sympystr(self, printer): + return "foo(%s)" % printer._print(self.args[0]) + assert sstr(R(x)) == "foo(x)" + + class R(Abs): + def _sympystr(self, printer): + return "foo" + assert sstr(R(x)) == "foo" + + +def test_Abs(): + assert str(Abs(x)) == "Abs(x)" + assert str(Abs(Rational(1, 6))) == "1/6" + assert str(Abs(Rational(-1, 6))) == "1/6" + + +def test_Add(): + assert str(x + y) == "x + y" + assert str(x + 1) == "x + 1" + assert str(x + x**2) == "x**2 + x" + assert str(Add(0, 1, evaluate=False)) == "0 + 1" + assert str(Add(0, 0, 1, evaluate=False)) == "0 + 0 + 1" + assert str(1.0*x) == "1.0*x" + assert str(5 + x + y + x*y + x**2 + y**2) == "x**2 + x*y + x + y**2 + y + 5" + assert str(1 + x + x**2/2 + x**3/3) == "x**3/3 + x**2/2 + x + 1" + assert str(2*x - 7*x**2 + 2 + 3*y) == "-7*x**2 + 2*x + 3*y + 2" + assert str(x - y) == "x - y" + assert str(2 - x) == "2 - x" + assert str(x - 2) == "x - 2" + assert str(x - y - z - w) == "-w + x - y - z" + assert str(x - z*y**2*z*w) == "-w*y**2*z**2 + x" + assert str(x - 1*y*x*y) == "-x*y**2 + x" + assert str(sin(x).series(x, 0, 15)) == "x - x**3/6 + x**5/120 - x**7/5040 + x**9/362880 - x**11/39916800 + x**13/6227020800 + O(x**15)" + assert str(Add(Add(-w, x, evaluate=False), Add(-y, z, evaluate=False), evaluate=False)) == "(-w + x) + (-y + z)" + assert str(Add(Add(-x, -y, evaluate=False), -z, evaluate=False)) == "-z + (-x - y)" + assert str(Add(Add(Add(-x, -y, evaluate=False), -z, evaluate=False), -t, evaluate=False)) == "-t + (-z + (-x - y))" + + +def test_Catalan(): + assert str(Catalan) == "Catalan" + + +def test_ComplexInfinity(): + assert str(zoo) == "zoo" + + +def test_Derivative(): + assert str(Derivative(x, y)) == "Derivative(x, y)" + assert str(Derivative(x**2, x, evaluate=False)) == "Derivative(x**2, x)" + assert str(Derivative( + x**2/y, x, y, evaluate=False)) == "Derivative(x**2/y, x, y)" + + +def test_dict(): + assert str({1: 1 + x}) == sstr({1: 1 + x}) == "{1: x + 1}" + assert str({1: x**2, 2: y*x}) in ("{1: x**2, 2: x*y}", "{2: x*y, 1: x**2}") + assert sstr({1: x**2, 2: y*x}) == "{1: x**2, 2: x*y}" + + +def test_Dict(): + assert str(Dict({1: 1 + x})) == sstr({1: 1 + x}) == "{1: x + 1}" + assert str(Dict({1: x**2, 2: y*x})) in ( + "{1: x**2, 2: x*y}", "{2: x*y, 1: x**2}") + assert sstr(Dict({1: x**2, 2: y*x})) == "{1: x**2, 2: x*y}" + + +def test_Dummy(): + assert str(d) == "_d" + assert str(d + x) == "_d + x" + + +def test_EulerGamma(): + assert str(EulerGamma) == "EulerGamma" + + +def test_Exp(): + assert str(E) == "E" + with _exp_is_pow(True): + assert str(exp(x)) == "E**x" + + +def test_factorial(): + n = Symbol('n', integer=True) + assert str(factorial(-2)) == "zoo" + assert str(factorial(0)) == "1" + assert str(factorial(7)) == "5040" + assert str(factorial(n)) == "factorial(n)" + assert str(factorial(2*n)) == "factorial(2*n)" + assert str(factorial(factorial(n))) == 'factorial(factorial(n))' + assert str(factorial(factorial2(n))) == 'factorial(factorial2(n))' + assert str(factorial2(factorial(n))) == 'factorial2(factorial(n))' + assert str(factorial2(factorial2(n))) == 'factorial2(factorial2(n))' + assert str(subfactorial(3)) == "2" + assert str(subfactorial(n)) == "subfactorial(n)" + assert str(subfactorial(2*n)) == "subfactorial(2*n)" + + +def test_Function(): + f = Function('f') + fx = f(x) + w = WildFunction('w') + assert str(f) == "f" + assert str(fx) == "f(x)" + assert str(w) == "w_" + + +def test_Geometry(): + assert sstr(Point(0, 0)) == 'Point2D(0, 0)' + assert sstr(Circle(Point(0, 0), 3)) == 'Circle(Point2D(0, 0), 3)' + assert sstr(Ellipse(Point(1, 2), 3, 4)) == 'Ellipse(Point2D(1, 2), 3, 4)' + assert sstr(Triangle(Point(1, 1), Point(7, 8), Point(0, -1))) == \ + 'Triangle(Point2D(1, 1), Point2D(7, 8), Point2D(0, -1))' + assert sstr(Polygon(Point(5, 6), Point(-2, -3), Point(0, 0), Point(4, 7))) == \ + 'Polygon(Point2D(5, 6), Point2D(-2, -3), Point2D(0, 0), Point2D(4, 7))' + assert sstr(Triangle(Point(0, 0), Point(1, 0), Point(0, 1)), sympy_integers=True) == \ + 'Triangle(Point2D(S(0), S(0)), Point2D(S(1), S(0)), Point2D(S(0), S(1)))' + assert sstr(Ellipse(Point(1, 2), 3, 4), sympy_integers=True) == \ + 'Ellipse(Point2D(S(1), S(2)), S(3), S(4))' + + +def test_GoldenRatio(): + assert str(GoldenRatio) == "GoldenRatio" + + +def test_Heaviside(): + assert str(Heaviside(x)) == str(Heaviside(x, S.Half)) == "Heaviside(x)" + assert str(Heaviside(x, 1)) == "Heaviside(x, 1)" + + +def test_TribonacciConstant(): + assert str(TribonacciConstant) == "TribonacciConstant" + + +def test_ImaginaryUnit(): + assert str(I) == "I" + + +def test_Infinity(): + assert str(oo) == "oo" + assert str(oo*I) == "oo*I" + + +def test_Integer(): + assert str(Integer(-1)) == "-1" + assert str(Integer(1)) == "1" + assert str(Integer(-3)) == "-3" + assert str(Integer(0)) == "0" + assert str(Integer(25)) == "25" + + +def test_Integral(): + assert str(Integral(sin(x), y)) == "Integral(sin(x), y)" + assert str(Integral(sin(x), (y, 0, 1))) == "Integral(sin(x), (y, 0, 1))" + + +def test_Interval(): + n = (S.NegativeInfinity, 1, 2, S.Infinity) + for i in range(len(n)): + for j in range(i + 1, len(n)): + for l in (True, False): + for r in (True, False): + ival = Interval(n[i], n[j], l, r) + assert S(str(ival)) == ival + + +def test_AccumBounds(): + a = Symbol('a', real=True) + assert str(AccumBounds(0, a)) == "AccumBounds(0, a)" + assert str(AccumBounds(0, 1)) == "AccumBounds(0, 1)" + + +def test_Lambda(): + assert str(Lambda(d, d**2)) == "Lambda(_d, _d**2)" + # issue 2908 + assert str(Lambda((), 1)) == "Lambda((), 1)" + assert str(Lambda((), x)) == "Lambda((), x)" + assert str(Lambda((x, y), x+y)) == "Lambda((x, y), x + y)" + assert str(Lambda(((x, y),), x+y)) == "Lambda(((x, y),), x + y)" + + +def test_Limit(): + assert str(Limit(sin(x)/x, x, y)) == "Limit(sin(x)/x, x, y, dir='+')" + assert str(Limit(1/x, x, 0)) == "Limit(1/x, x, 0, dir='+')" + assert str( + Limit(sin(x)/x, x, y, dir="-")) == "Limit(sin(x)/x, x, y, dir='-')" + + +def test_list(): + assert str([x]) == sstr([x]) == "[x]" + assert str([x**2, x*y + 1]) == sstr([x**2, x*y + 1]) == "[x**2, x*y + 1]" + assert str([x**2, [y + x]]) == sstr([x**2, [y + x]]) == "[x**2, [x + y]]" + + +def test_Matrix_str(): + M = Matrix([[x**+1, 1], [y, x + y]]) + assert str(M) == "Matrix([[x, 1], [y, x + y]])" + assert sstr(M) == "Matrix([\n[x, 1],\n[y, x + y]])" + M = Matrix([[1]]) + assert str(M) == sstr(M) == "Matrix([[1]])" + M = Matrix([[1, 2]]) + assert str(M) == sstr(M) == "Matrix([[1, 2]])" + M = Matrix() + assert str(M) == sstr(M) == "Matrix(0, 0, [])" + M = Matrix(0, 1, lambda i, j: 0) + assert str(M) == sstr(M) == "Matrix(0, 1, [])" + + +def test_Mul(): + assert str(x/y) == "x/y" + assert str(y/x) == "y/x" + assert str(x/y/z) == "x/(y*z)" + assert str((x + 1)/(y + 2)) == "(x + 1)/(y + 2)" + assert str(2*x/3) == '2*x/3' + assert str(-2*x/3) == '-2*x/3' + assert str(-1.0*x) == '-1.0*x' + assert str(1.0*x) == '1.0*x' + assert str(Mul(0, 1, evaluate=False)) == '0*1' + assert str(Mul(1, 0, evaluate=False)) == '1*0' + assert str(Mul(1, 1, evaluate=False)) == '1*1' + assert str(Mul(1, 1, 1, evaluate=False)) == '1*1*1' + assert str(Mul(1, 2, evaluate=False)) == '1*2' + assert str(Mul(1, S.Half, evaluate=False)) == '1*(1/2)' + assert str(Mul(1, 1, S.Half, evaluate=False)) == '1*1*(1/2)' + assert str(Mul(1, 1, 2, 3, x, evaluate=False)) == '1*1*2*3*x' + assert str(Mul(1, -1, evaluate=False)) == '1*(-1)' + assert str(Mul(-1, 1, evaluate=False)) == '-1*1' + assert str(Mul(4, 3, 2, 1, 0, y, x, evaluate=False)) == '4*3*2*1*0*y*x' + assert str(Mul(4, 3, 2, 1+z, 0, y, x, evaluate=False)) == '4*3*2*(z + 1)*0*y*x' + assert str(Mul(Rational(2, 3), Rational(5, 7), evaluate=False)) == '(2/3)*(5/7)' + # For issue 14160 + assert str(Mul(-2, x, Pow(Mul(y,y,evaluate=False), -1, evaluate=False), + evaluate=False)) == '-2*x/(y*y)' + # issue 21537 + assert str(Mul(x, Pow(1/y, -1, evaluate=False), evaluate=False)) == 'x/(1/y)' + + # Issue 24108 + from sympy.core.parameters import evaluate + with evaluate(False): + assert str(Mul(Pow(Integer(2), Integer(-1)), Add(Integer(-1), Mul(Integer(-1), Integer(1))))) == "(-1 - 1*1)/2" + + class CustomClass1(Expr): + is_commutative = True + + class CustomClass2(Expr): + is_commutative = True + cc1 = CustomClass1() + cc2 = CustomClass2() + assert str(Rational(2)*cc1) == '2*CustomClass1()' + assert str(cc1*Rational(2)) == '2*CustomClass1()' + assert str(cc1*Float("1.5")) == '1.5*CustomClass1()' + assert str(cc2*Rational(2)) == '2*CustomClass2()' + assert str(cc2*Rational(2)*cc1) == '2*CustomClass1()*CustomClass2()' + assert str(cc1*Rational(2)*cc2) == '2*CustomClass1()*CustomClass2()' + + +def test_NaN(): + assert str(nan) == "nan" + + +def test_NegativeInfinity(): + assert str(-oo) == "-oo" + +def test_Order(): + assert str(O(x)) == "O(x)" + assert str(O(x**2)) == "O(x**2)" + assert str(O(x*y)) == "O(x*y, x, y)" + assert str(O(x, x)) == "O(x)" + assert str(O(x, (x, 0))) == "O(x)" + assert str(O(x, (x, oo))) == "O(x, (x, oo))" + assert str(O(x, x, y)) == "O(x, x, y)" + assert str(O(x, x, y)) == "O(x, x, y)" + assert str(O(x, (x, oo), (y, oo))) == "O(x, (x, oo), (y, oo))" + + +def test_Permutation_Cycle(): + from sympy.combinatorics import Permutation, Cycle + + # general principle: economically, canonically show all moved elements + # and the size of the permutation. + + for p, s in [ + (Cycle(), + '()'), + (Cycle(2), + '(2)'), + (Cycle(2, 1), + '(1 2)'), + (Cycle(1, 2)(5)(6, 7)(10), + '(1 2)(6 7)(10)'), + (Cycle(3, 4)(1, 2)(3, 4), + '(1 2)(4)'), + ]: + assert sstr(p) == s + + for p, s in [ + (Permutation([]), + 'Permutation([])'), + (Permutation([], size=1), + 'Permutation([0])'), + (Permutation([], size=2), + 'Permutation([0, 1])'), + (Permutation([], size=10), + 'Permutation([], size=10)'), + (Permutation([1, 0, 2]), + 'Permutation([1, 0, 2])'), + (Permutation([1, 0, 2, 3, 4, 5]), + 'Permutation([1, 0], size=6)'), + (Permutation([1, 0, 2, 3, 4, 5], size=10), + 'Permutation([1, 0], size=10)'), + ]: + assert sstr(p, perm_cyclic=False) == s + + for p, s in [ + (Permutation([]), + '()'), + (Permutation([], size=1), + '(0)'), + (Permutation([], size=2), + '(1)'), + (Permutation([], size=10), + '(9)'), + (Permutation([1, 0, 2]), + '(2)(0 1)'), + (Permutation([1, 0, 2, 3, 4, 5]), + '(5)(0 1)'), + (Permutation([1, 0, 2, 3, 4, 5], size=10), + '(9)(0 1)'), + (Permutation([0, 1, 3, 2, 4, 5], size=10), + '(9)(2 3)'), + ]: + assert sstr(p) == s + + + with warns_deprecated_sympy(): + old_print_cyclic = Permutation.print_cyclic + Permutation.print_cyclic = False + assert sstr(Permutation([1, 0, 2])) == 'Permutation([1, 0, 2])' + Permutation.print_cyclic = old_print_cyclic + +def test_Pi(): + assert str(pi) == "pi" + + +def test_Poly(): + assert str(Poly(0, x)) == "Poly(0, x, domain='ZZ')" + assert str(Poly(1, x)) == "Poly(1, x, domain='ZZ')" + assert str(Poly(x, x)) == "Poly(x, x, domain='ZZ')" + + assert str(Poly(2*x + 1, x)) == "Poly(2*x + 1, x, domain='ZZ')" + assert str(Poly(2*x - 1, x)) == "Poly(2*x - 1, x, domain='ZZ')" + + assert str(Poly(-1, x)) == "Poly(-1, x, domain='ZZ')" + assert str(Poly(-x, x)) == "Poly(-x, x, domain='ZZ')" + + assert str(Poly(-2*x + 1, x)) == "Poly(-2*x + 1, x, domain='ZZ')" + assert str(Poly(-2*x - 1, x)) == "Poly(-2*x - 1, x, domain='ZZ')" + + assert str(Poly(x - 1, x)) == "Poly(x - 1, x, domain='ZZ')" + assert str(Poly(2*x + x**5, x)) == "Poly(x**5 + 2*x, x, domain='ZZ')" + + assert str(Poly(3**(2*x), 3**x)) == "Poly((3**x)**2, 3**x, domain='ZZ')" + assert str(Poly((x**2)**x)) == "Poly(((x**2)**x), (x**2)**x, domain='ZZ')" + + assert str(Poly((x + y)**3, (x + y), expand=False) + ) == "Poly((x + y)**3, x + y, domain='ZZ')" + assert str(Poly((x - 1)**2, (x - 1), expand=False) + ) == "Poly((x - 1)**2, x - 1, domain='ZZ')" + + assert str( + Poly(x**2 + 1 + y, x)) == "Poly(x**2 + y + 1, x, domain='ZZ[y]')" + assert str( + Poly(x**2 - 1 + y, x)) == "Poly(x**2 + y - 1, x, domain='ZZ[y]')" + + assert str(Poly(x**2 + I*x, x)) == "Poly(x**2 + I*x, x, domain='ZZ_I')" + assert str(Poly(x**2 - I*x, x)) == "Poly(x**2 - I*x, x, domain='ZZ_I')" + + assert str(Poly(-x*y*z + x*y - 1, x, y, z) + ) == "Poly(-x*y*z + x*y - 1, x, y, z, domain='ZZ')" + assert str(Poly(-w*x**21*y**7*z + (1 + w)*z**3 - 2*x*z + 1, x, y, z)) == \ + "Poly(-w*x**21*y**7*z - 2*x*z + (w + 1)*z**3 + 1, x, y, z, domain='ZZ[w]')" + + assert str(Poly(x**2 + 1, x, modulus=2)) == "Poly(x**2 + 1, x, modulus=2)" + assert str(Poly(2*x**2 + 3*x + 4, x, modulus=17)) == "Poly(2*x**2 + 3*x + 4, x, modulus=17)" + + +def test_PolyRing(): + assert str(ring("x", ZZ, lex)[0]) == "Polynomial ring in x over ZZ with lex order" + assert str(ring("x,y", QQ, grlex)[0]) == "Polynomial ring in x, y over QQ with grlex order" + assert str(ring("x,y,z", ZZ["t"], lex)[0]) == "Polynomial ring in x, y, z over ZZ[t] with lex order" + + +def test_FracField(): + assert str(field("x", ZZ, lex)[0]) == "Rational function field in x over ZZ with lex order" + assert str(field("x,y", QQ, grlex)[0]) == "Rational function field in x, y over QQ with grlex order" + assert str(field("x,y,z", ZZ["t"], lex)[0]) == "Rational function field in x, y, z over ZZ[t] with lex order" + + +def test_PolyElement(): + Ruv, u,v = ring("u,v", ZZ) + Rxyz, x,y,z = ring("x,y,z", Ruv) + Rx_zzi, xz = ring("x", ZZ_I) + + assert str(x - x) == "0" + assert str(x - 1) == "x - 1" + assert str(x + 1) == "x + 1" + assert str(x**2) == "x**2" + + assert str((u**2 + 3*u*v + 1)*x**2*y + u + 1) == "(u**2 + 3*u*v + 1)*x**2*y + u + 1" + assert str((u**2 + 3*u*v + 1)*x**2*y + (u + 1)*x) == "(u**2 + 3*u*v + 1)*x**2*y + (u + 1)*x" + assert str((u**2 + 3*u*v + 1)*x**2*y + (u + 1)*x + 1) == "(u**2 + 3*u*v + 1)*x**2*y + (u + 1)*x + 1" + assert str((-u**2 + 3*u*v - 1)*x**2*y - (u + 1)*x - 1) == "-(u**2 - 3*u*v + 1)*x**2*y - (u + 1)*x - 1" + + assert str(-(v**2 + v + 1)*x + 3*u*v + 1) == "-(v**2 + v + 1)*x + 3*u*v + 1" + assert str(-(v**2 + v + 1)*x - 3*u*v + 1) == "-(v**2 + v + 1)*x - 3*u*v + 1" + + assert str((1+I)*xz + 2) == "(1 + 1*I)*x + (2 + 0*I)" + + +def test_FracElement(): + Fuv, u,v = field("u,v", ZZ) + Fxyzt, x,y,z,t = field("x,y,z,t", Fuv) + Rx_zzi, xz = field("x", QQ_I) + i = QQ_I(0, 1) + + assert str(x - x) == "0" + assert str(x - 1) == "x - 1" + assert str(x + 1) == "x + 1" + + assert str(x/3) == "x/3" + assert str(x/z) == "x/z" + assert str(x*y/z) == "x*y/z" + assert str(x/(z*t)) == "x/(z*t)" + assert str(x*y/(z*t)) == "x*y/(z*t)" + + assert str((x - 1)/y) == "(x - 1)/y" + assert str((x + 1)/y) == "(x + 1)/y" + assert str((-x - 1)/y) == "(-x - 1)/y" + assert str((x + 1)/(y*z)) == "(x + 1)/(y*z)" + assert str(-y/(x + 1)) == "-y/(x + 1)" + assert str(y*z/(x + 1)) == "y*z/(x + 1)" + + assert str(((u + 1)*x*y + 1)/((v - 1)*z - 1)) == "((u + 1)*x*y + 1)/((v - 1)*z - 1)" + assert str(((u + 1)*x*y + 1)/((v - 1)*z - t*u*v - 1)) == "((u + 1)*x*y + 1)/((v - 1)*z - u*v*t - 1)" + + assert str((1+i)/xz) == "(1 + 1*I)/x" + assert str(((1+i)*xz - i)/xz) == "((1 + 1*I)*x + (0 + -1*I))/x" + + +def test_GaussianInteger(): + assert str(ZZ_I(1, 0)) == "1" + assert str(ZZ_I(-1, 0)) == "-1" + assert str(ZZ_I(0, 1)) == "I" + assert str(ZZ_I(0, -1)) == "-I" + assert str(ZZ_I(0, 2)) == "2*I" + assert str(ZZ_I(0, -2)) == "-2*I" + assert str(ZZ_I(1, 1)) == "1 + I" + assert str(ZZ_I(-1, -1)) == "-1 - I" + assert str(ZZ_I(-1, -2)) == "-1 - 2*I" + + +def test_GaussianRational(): + assert str(QQ_I(1, 0)) == "1" + assert str(QQ_I(QQ(2, 3), 0)) == "2/3" + assert str(QQ_I(0, QQ(2, 3))) == "2*I/3" + assert str(QQ_I(QQ(1, 2), QQ(-2, 3))) == "1/2 - 2*I/3" + + +def test_Pow(): + assert str(x**-1) == "1/x" + assert str(x**-2) == "x**(-2)" + assert str(x**2) == "x**2" + assert str((x + y)**-1) == "1/(x + y)" + assert str((x + y)**-2) == "(x + y)**(-2)" + assert str((x + y)**2) == "(x + y)**2" + assert str((x + y)**(1 + x)) == "(x + y)**(x + 1)" + assert str(x**Rational(1, 3)) == "x**(1/3)" + assert str(1/x**Rational(1, 3)) == "x**(-1/3)" + assert str(sqrt(sqrt(x))) == "x**(1/4)" + # not the same as x**-1 + assert str(x**-1.0) == 'x**(-1.0)' + # see issue #2860 + assert str(Pow(S(2), -1.0, evaluate=False)) == '2**(-1.0)' + + +def test_sqrt(): + assert str(sqrt(x)) == "sqrt(x)" + assert str(sqrt(x**2)) == "sqrt(x**2)" + assert str(1/sqrt(x)) == "1/sqrt(x)" + assert str(1/sqrt(x**2)) == "1/sqrt(x**2)" + assert str(y/sqrt(x)) == "y/sqrt(x)" + assert str(x**0.5) == "x**0.5" + assert str(1/x**0.5) == "x**(-0.5)" + + +def test_Rational(): + n1 = Rational(1, 4) + n2 = Rational(1, 3) + n3 = Rational(2, 4) + n4 = Rational(2, -4) + n5 = Rational(0) + n7 = Rational(3) + n8 = Rational(-3) + assert str(n1*n2) == "1/12" + assert str(n1*n2) == "1/12" + assert str(n3) == "1/2" + assert str(n1*n3) == "1/8" + assert str(n1 + n3) == "3/4" + assert str(n1 + n2) == "7/12" + assert str(n1 + n4) == "-1/4" + assert str(n4*n4) == "1/4" + assert str(n4 + n2) == "-1/6" + assert str(n4 + n5) == "-1/2" + assert str(n4*n5) == "0" + assert str(n3 + n4) == "0" + assert str(n1**n7) == "1/64" + assert str(n2**n7) == "1/27" + assert str(n2**n8) == "27" + assert str(n7**n8) == "1/27" + assert str(Rational("-25")) == "-25" + assert str(Rational("1.25")) == "5/4" + assert str(Rational("-2.6e-2")) == "-13/500" + assert str(S("25/7")) == "25/7" + assert str(S("-123/569")) == "-123/569" + assert str(S("0.1[23]", rational=1)) == "61/495" + assert str(S("5.1[666]", rational=1)) == "31/6" + assert str(S("-5.1[666]", rational=1)) == "-31/6" + assert str(S("0.[9]", rational=1)) == "1" + assert str(S("-0.[9]", rational=1)) == "-1" + + assert str(sqrt(Rational(1, 4))) == "1/2" + assert str(sqrt(Rational(1, 36))) == "1/6" + + assert str((123**25) ** Rational(1, 25)) == "123" + assert str((123**25 + 1)**Rational(1, 25)) != "123" + assert str((123**25 - 1)**Rational(1, 25)) != "123" + assert str((123**25 - 1)**Rational(1, 25)) != "122" + + assert str(sqrt(Rational(81, 36))**3) == "27/8" + assert str(1/sqrt(Rational(81, 36))**3) == "8/27" + + assert str(sqrt(-4)) == str(2*I) + assert str(2**Rational(1, 10**10)) == "2**(1/10000000000)" + + assert sstr(Rational(2, 3), sympy_integers=True) == "S(2)/3" + x = Symbol("x") + assert sstr(x**Rational(2, 3), sympy_integers=True) == "x**(S(2)/3)" + assert sstr(Eq(x, Rational(2, 3)), sympy_integers=True) == "Eq(x, S(2)/3)" + assert sstr(Limit(x, x, Rational(7, 2)), sympy_integers=True) == \ + "Limit(x, x, S(7)/2, dir='+')" + + +def test_Float(): + # NOTE dps is the whole number of decimal digits + assert str(Float('1.23', dps=1 + 2)) == '1.23' + assert str(Float('1.23456789', dps=1 + 8)) == '1.23456789' + assert str( + Float('1.234567890123456789', dps=1 + 18)) == '1.234567890123456789' + assert str(pi.evalf(1 + 2)) == '3.14' + assert str(pi.evalf(1 + 14)) == '3.14159265358979' + assert str(pi.evalf(1 + 64)) == ('3.141592653589793238462643383279' + '5028841971693993751058209749445923') + assert str(pi.round(-1)) == '0.0' + assert str((pi**400 - (pi**400).round(1)).n(2)) == '-0.e+88' + assert sstr(Float("100"), full_prec=False, min=-2, max=2) == '1.0e+2' + assert sstr(Float("100"), full_prec=False, min=-2, max=3) == '100.0' + assert sstr(Float("0.1"), full_prec=False, min=-2, max=3) == '0.1' + assert sstr(Float("0.099"), min=-2, max=3) == '9.90000000000000e-2' + + +def test_Relational(): + assert str(Rel(x, y, "<")) == "x < y" + assert str(Rel(x + y, y, "==")) == "Eq(x + y, y)" + assert str(Rel(x, y, "!=")) == "Ne(x, y)" + assert str(Eq(x, 1) | Eq(x, 2)) == "Eq(x, 1) | Eq(x, 2)" + assert str(Ne(x, 1) & Ne(x, 2)) == "Ne(x, 1) & Ne(x, 2)" + + +def test_AppliedBinaryRelation(): + assert str(Q.eq(x, y)) == "Q.eq(x, y)" + assert str(Q.ne(x, y)) == "Q.ne(x, y)" + + +def test_CRootOf(): + assert str(rootof(x**5 + 2*x - 1, 0)) == "CRootOf(x**5 + 2*x - 1, 0)" + + +def test_RootSum(): + f = x**5 + 2*x - 1 + + assert str( + RootSum(f, Lambda(z, z), auto=False)) == "RootSum(x**5 + 2*x - 1)" + assert str(RootSum(f, Lambda( + z, z**2), auto=False)) == "RootSum(x**5 + 2*x - 1, Lambda(z, z**2))" + + +def test_GroebnerBasis(): + assert str(groebner( + [], x, y)) == "GroebnerBasis([], x, y, domain='ZZ', order='lex')" + + F = [x**2 - 3*y - x + 1, y**2 - 2*x + y - 1] + + assert str(groebner(F, order='grlex')) == \ + "GroebnerBasis([x**2 - x - 3*y + 1, y**2 - 2*x + y - 1], x, y, domain='ZZ', order='grlex')" + assert str(groebner(F, order='lex')) == \ + "GroebnerBasis([2*x - y**2 - y + 1, y**4 + 2*y**3 - 3*y**2 - 16*y + 7], x, y, domain='ZZ', order='lex')" + +def test_set(): + assert sstr(set()) == 'set()' + assert sstr(frozenset()) == 'frozenset()' + + assert sstr({1}) == '{1}' + assert sstr(frozenset([1])) == 'frozenset({1})' + assert sstr({1, 2, 3}) == '{1, 2, 3}' + assert sstr(frozenset([1, 2, 3])) == 'frozenset({1, 2, 3})' + + assert sstr( + {1, x, x**2, x**3, x**4}) == '{1, x, x**2, x**3, x**4}' + assert sstr( + frozenset([1, x, x**2, x**3, x**4])) == 'frozenset({1, x, x**2, x**3, x**4})' + + +def test_SparseMatrix(): + M = SparseMatrix([[x**+1, 1], [y, x + y]]) + assert str(M) == "Matrix([[x, 1], [y, x + y]])" + assert sstr(M) == "Matrix([\n[x, 1],\n[y, x + y]])" + + +def test_Sum(): + assert str(summation(cos(3*z), (z, x, y))) == "Sum(cos(3*z), (z, x, y))" + assert str(Sum(x*y**2, (x, -2, 2), (y, -5, 5))) == \ + "Sum(x*y**2, (x, -2, 2), (y, -5, 5))" + + +def test_Symbol(): + assert str(y) == "y" + assert str(x) == "x" + e = x + assert str(e) == "x" + + +def test_tuple(): + assert str((x,)) == sstr((x,)) == "(x,)" + assert str((x + y, 1 + x)) == sstr((x + y, 1 + x)) == "(x + y, x + 1)" + assert str((x + y, ( + 1 + x, x**2))) == sstr((x + y, (1 + x, x**2))) == "(x + y, (x + 1, x**2))" + + +def test_Series_str(): + tf1 = TransferFunction(x*y**2 - z, y**3 - t**3, y) + tf2 = TransferFunction(x - y, x + y, y) + tf3 = TransferFunction(t*x**2 - t**w*x + w, t - y, y) + assert str(Series(tf1, tf2)) == \ + "Series(TransferFunction(x*y**2 - z, -t**3 + y**3, y), TransferFunction(x - y, x + y, y))" + assert str(Series(tf1, tf2, tf3)) == \ + "Series(TransferFunction(x*y**2 - z, -t**3 + y**3, y), TransferFunction(x - y, x + y, y), TransferFunction(t*x**2 - t**w*x + w, t - y, y))" + assert str(Series(-tf2, tf1)) == \ + "Series(TransferFunction(-x + y, x + y, y), TransferFunction(x*y**2 - z, -t**3 + y**3, y))" + + +def test_MIMOSeries_str(): + tf1 = TransferFunction(x*y**2 - z, y**3 - t**3, y) + tf2 = TransferFunction(x - y, x + y, y) + tfm_1 = TransferFunctionMatrix([[tf1, tf2], [tf2, tf1]]) + tfm_2 = TransferFunctionMatrix([[tf2, tf1], [tf1, tf2]]) + assert str(MIMOSeries(tfm_1, tfm_2)) == \ + "MIMOSeries(TransferFunctionMatrix(((TransferFunction(x*y**2 - z, -t**3 + y**3, y), TransferFunction(x - y, x + y, y)), "\ + "(TransferFunction(x - y, x + y, y), TransferFunction(x*y**2 - z, -t**3 + y**3, y)))), "\ + "TransferFunctionMatrix(((TransferFunction(x - y, x + y, y), TransferFunction(x*y**2 - z, -t**3 + y**3, y)), "\ + "(TransferFunction(x*y**2 - z, -t**3 + y**3, y), TransferFunction(x - y, x + y, y)))))" + + +def test_TransferFunction_str(): + tf1 = TransferFunction(x - 1, x + 1, x) + assert str(tf1) == "TransferFunction(x - 1, x + 1, x)" + tf2 = TransferFunction(x + 1, 2 - y, x) + assert str(tf2) == "TransferFunction(x + 1, 2 - y, x)" + tf3 = TransferFunction(y, y**2 + 2*y + 3, y) + assert str(tf3) == "TransferFunction(y, y**2 + 2*y + 3, y)" + + +def test_Parallel_str(): + tf1 = TransferFunction(x*y**2 - z, y**3 - t**3, y) + tf2 = TransferFunction(x - y, x + y, y) + tf3 = TransferFunction(t*x**2 - t**w*x + w, t - y, y) + assert str(Parallel(tf1, tf2)) == \ + "Parallel(TransferFunction(x*y**2 - z, -t**3 + y**3, y), TransferFunction(x - y, x + y, y))" + assert str(Parallel(tf1, tf2, tf3)) == \ + "Parallel(TransferFunction(x*y**2 - z, -t**3 + y**3, y), TransferFunction(x - y, x + y, y), TransferFunction(t*x**2 - t**w*x + w, t - y, y))" + assert str(Parallel(-tf2, tf1)) == \ + "Parallel(TransferFunction(-x + y, x + y, y), TransferFunction(x*y**2 - z, -t**3 + y**3, y))" + + +def test_MIMOParallel_str(): + tf1 = TransferFunction(x*y**2 - z, y**3 - t**3, y) + tf2 = TransferFunction(x - y, x + y, y) + tfm_1 = TransferFunctionMatrix([[tf1, tf2], [tf2, tf1]]) + tfm_2 = TransferFunctionMatrix([[tf2, tf1], [tf1, tf2]]) + assert str(MIMOParallel(tfm_1, tfm_2)) == \ + "MIMOParallel(TransferFunctionMatrix(((TransferFunction(x*y**2 - z, -t**3 + y**3, y), TransferFunction(x - y, x + y, y)), "\ + "(TransferFunction(x - y, x + y, y), TransferFunction(x*y**2 - z, -t**3 + y**3, y)))), "\ + "TransferFunctionMatrix(((TransferFunction(x - y, x + y, y), TransferFunction(x*y**2 - z, -t**3 + y**3, y)), "\ + "(TransferFunction(x*y**2 - z, -t**3 + y**3, y), TransferFunction(x - y, x + y, y)))))" + + +def test_Feedback_str(): + tf1 = TransferFunction(x*y**2 - z, y**3 - t**3, y) + tf2 = TransferFunction(x - y, x + y, y) + tf3 = TransferFunction(t*x**2 - t**w*x + w, t - y, y) + assert str(Feedback(tf1*tf2, tf3)) == \ + "Feedback(Series(TransferFunction(x*y**2 - z, -t**3 + y**3, y), TransferFunction(x - y, x + y, y)), " \ + "TransferFunction(t*x**2 - t**w*x + w, t - y, y), -1)" + assert str(Feedback(tf1, TransferFunction(1, 1, y), 1)) == \ + "Feedback(TransferFunction(x*y**2 - z, -t**3 + y**3, y), TransferFunction(1, 1, y), 1)" + + +def test_MIMOFeedback_str(): + tf1 = TransferFunction(x**2 - y**3, y - z, x) + tf2 = TransferFunction(y - x, z + y, x) + tfm_1 = TransferFunctionMatrix([[tf2, tf1], [tf1, tf2]]) + tfm_2 = TransferFunctionMatrix([[tf1, tf2], [tf2, tf1]]) + assert (str(MIMOFeedback(tfm_1, tfm_2)) \ + == "MIMOFeedback(TransferFunctionMatrix(((TransferFunction(-x + y, y + z, x), TransferFunction(x**2 - y**3, y - z, x))," \ + " (TransferFunction(x**2 - y**3, y - z, x), TransferFunction(-x + y, y + z, x)))), " \ + "TransferFunctionMatrix(((TransferFunction(x**2 - y**3, y - z, x), " \ + "TransferFunction(-x + y, y + z, x)), (TransferFunction(-x + y, y + z, x), TransferFunction(x**2 - y**3, y - z, x)))), -1)") + assert (str(MIMOFeedback(tfm_1, tfm_2, 1)) \ + == "MIMOFeedback(TransferFunctionMatrix(((TransferFunction(-x + y, y + z, x), TransferFunction(x**2 - y**3, y - z, x)), " \ + "(TransferFunction(x**2 - y**3, y - z, x), TransferFunction(-x + y, y + z, x)))), " \ + "TransferFunctionMatrix(((TransferFunction(x**2 - y**3, y - z, x), TransferFunction(-x + y, y + z, x)), "\ + "(TransferFunction(-x + y, y + z, x), TransferFunction(x**2 - y**3, y - z, x)))), 1)") + + +def test_TransferFunctionMatrix_str(): + tf1 = TransferFunction(x*y**2 - z, y**3 - t**3, y) + tf2 = TransferFunction(x - y, x + y, y) + tf3 = TransferFunction(t*x**2 - t**w*x + w, t - y, y) + assert str(TransferFunctionMatrix([[tf1], [tf2]])) == \ + "TransferFunctionMatrix(((TransferFunction(x*y**2 - z, -t**3 + y**3, y),), (TransferFunction(x - y, x + y, y),)))" + assert str(TransferFunctionMatrix([[tf1, tf2], [tf3, tf2]])) == \ + "TransferFunctionMatrix(((TransferFunction(x*y**2 - z, -t**3 + y**3, y), TransferFunction(x - y, x + y, y)), (TransferFunction(t*x**2 - t**w*x + w, t - y, y), TransferFunction(x - y, x + y, y))))" + + +def test_Quaternion_str_printer(): + q = Quaternion(x, y, z, t) + assert str(q) == "x + y*i + z*j + t*k" + q = Quaternion(x,y,z,x*t) + assert str(q) == "x + y*i + z*j + t*x*k" + q = Quaternion(x,y,z,x+t) + assert str(q) == "x + y*i + z*j + (t + x)*k" + + +def test_Quantity_str(): + assert sstr(second, abbrev=True) == "s" + assert sstr(joule, abbrev=True) == "J" + assert str(second) == "second" + assert str(joule) == "joule" + + +def test_wild_str(): + # Check expressions containing Wild not causing infinite recursion + w = Wild('x') + assert str(w + 1) == 'x_ + 1' + assert str(exp(2**w) + 5) == 'exp(2**x_) + 5' + assert str(3*w + 1) == '3*x_ + 1' + assert str(1/w + 1) == '1 + 1/x_' + assert str(w**2 + 1) == 'x_**2 + 1' + assert str(1/(1 - w)) == '1/(1 - x_)' + + +def test_wild_matchpy(): + from sympy.utilities.matchpy_connector import WildDot, WildPlus, WildStar + + matchpy = import_module("matchpy") + + if matchpy is None: + return + + wd = WildDot('w_') + wp = WildPlus('w__') + ws = WildStar('w___') + + assert str(wd) == 'w_' + assert str(wp) == 'w__' + assert str(ws) == 'w___' + + assert str(wp/ws + 2**wd) == '2**w_ + w__/w___' + assert str(sin(wd)*cos(wp)*sqrt(ws)) == 'sqrt(w___)*sin(w_)*cos(w__)' + + +def test_zeta(): + assert str(zeta(3)) == "zeta(3)" + + +def test_issue_3101(): + e = x - y + a = str(e) + b = str(e) + assert a == b + + +def test_issue_3103(): + e = -2*sqrt(x) - y/sqrt(x)/2 + assert str(e) not in ["(-2)*x**1/2(-1/2)*x**(-1/2)*y", + "-2*x**1/2(-1/2)*x**(-1/2)*y", "-2*x**1/2-1/2*x**-1/2*w"] + assert str(e) == "-2*sqrt(x) - y/(2*sqrt(x))" + + +def test_issue_4021(): + e = Integral(x, x) + 1 + assert str(e) == 'Integral(x, x) + 1' + + +def test_sstrrepr(): + assert sstr('abc') == 'abc' + assert sstrrepr('abc') == "'abc'" + + e = ['a', 'b', 'c', x] + assert sstr(e) == "[a, b, c, x]" + assert sstrrepr(e) == "['a', 'b', 'c', x]" + + +def test_infinity(): + assert sstr(oo*I) == "oo*I" + + +def test_full_prec(): + assert sstr(S("0.3"), full_prec=True) == "0.300000000000000" + assert sstr(S("0.3"), full_prec="auto") == "0.300000000000000" + assert sstr(S("0.3"), full_prec=False) == "0.3" + assert sstr(S("0.3")*x, full_prec=True) in [ + "0.300000000000000*x", + "x*0.300000000000000" + ] + assert sstr(S("0.3")*x, full_prec="auto") in [ + "0.3*x", + "x*0.3" + ] + assert sstr(S("0.3")*x, full_prec=False) in [ + "0.3*x", + "x*0.3" + ] + + +def test_noncommutative(): + A, B, C = symbols('A,B,C', commutative=False) + + assert sstr(A*B*C**-1) == "A*B*C**(-1)" + assert sstr(C**-1*A*B) == "C**(-1)*A*B" + assert sstr(A*C**-1*B) == "A*C**(-1)*B" + assert sstr(sqrt(A)) == "sqrt(A)" + assert sstr(1/sqrt(A)) == "A**(-1/2)" + + +def test_empty_printer(): + str_printer = StrPrinter() + assert str_printer.emptyPrinter("foo") == "foo" + assert str_printer.emptyPrinter(x*y) == "x*y" + assert str_printer.emptyPrinter(32) == "32" + +def test_decimal_printer(): + dec_printer = StrPrinter(settings={"dps":3}) + f = Function('f') + assert dec_printer.doprint(f(1.329294)) == "f(1.33)" + + +def test_settings(): + raises(TypeError, lambda: sstr(S(4), method="garbage")) + + +def test_RandomDomain(): + from sympy.stats import Normal, Die, Exponential, pspace, where + X = Normal('x1', 0, 1) + assert str(where(X > 0)) == "Domain: (0 < x1) & (x1 < oo)" + + D = Die('d1', 6) + assert str(where(D > 4)) == "Domain: Eq(d1, 5) | Eq(d1, 6)" + + A = Exponential('a', 1) + B = Exponential('b', 1) + assert str(pspace(Tuple(A, B)).domain) == "Domain: (0 <= a) & (0 <= b) & (a < oo) & (b < oo)" + + +def test_FiniteSet(): + assert str(FiniteSet(*range(1, 51))) == ( + '{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,' + ' 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34,' + ' 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50}' + ) + assert str(FiniteSet(*range(1, 6))) == '{1, 2, 3, 4, 5}' + assert str(FiniteSet(*[x*y, x**2])) == '{x**2, x*y}' + assert str(FiniteSet(FiniteSet(FiniteSet(x, y), 5), FiniteSet(x,y), 5) + ) == 'FiniteSet(5, FiniteSet(5, {x, y}), {x, y})' + + +def test_Partition(): + assert str(Partition(FiniteSet(x, y), {z})) == 'Partition({z}, {x, y})' + +def test_UniversalSet(): + assert str(S.UniversalSet) == 'UniversalSet' + + +def test_PrettyPoly(): + F = QQ.frac_field(x, y) + R = QQ[x, y] + assert sstr(F.convert(x/(x + y))) == sstr(x/(x + y)) + assert sstr(R.convert(x + y)) == sstr(x + y) + + +def test_categories(): + from sympy.categories import (Object, NamedMorphism, + IdentityMorphism, Category) + + A = Object("A") + B = Object("B") + + f = NamedMorphism(A, B, "f") + id_A = IdentityMorphism(A) + + K = Category("K") + + assert str(A) == 'Object("A")' + assert str(f) == 'NamedMorphism(Object("A"), Object("B"), "f")' + assert str(id_A) == 'IdentityMorphism(Object("A"))' + + assert str(K) == 'Category("K")' + + +def test_Tr(): + A, B = symbols('A B', commutative=False) + t = Tr(A*B) + assert str(t) == 'Tr(A*B)' + + +def test_issue_6387(): + assert str(factor(-3.0*z + 3)) == '-3.0*(1.0*z - 1.0)' + + +def test_MatMul_MatAdd(): + X, Y = MatrixSymbol("X", 2, 2), MatrixSymbol("Y", 2, 2) + assert str(2*(X + Y)) == "2*X + 2*Y" + + assert str(I*X) == "I*X" + assert str(-I*X) == "-I*X" + assert str((1 + I)*X) == '(1 + I)*X' + assert str(-(1 + I)*X) == '(-1 - I)*X' + assert str(MatAdd(MatAdd(X, Y), MatAdd(X, Y))) == '(X + Y) + (X + Y)' + + +def test_MatrixSlice(): + n = Symbol('n', integer=True) + X = MatrixSymbol('X', n, n) + Y = MatrixSymbol('Y', 10, 10) + Z = MatrixSymbol('Z', 10, 10) + + assert str(MatrixSlice(X, (None, None, None), (None, None, None))) == 'X[:, :]' + assert str(X[x:x + 1, y:y + 1]) == 'X[x:x + 1, y:y + 1]' + assert str(X[x:x + 1:2, y:y + 1:2]) == 'X[x:x + 1:2, y:y + 1:2]' + assert str(X[:x, y:]) == 'X[:x, y:]' + assert str(X[:x, y:]) == 'X[:x, y:]' + assert str(X[x:, :y]) == 'X[x:, :y]' + assert str(X[x:y, z:w]) == 'X[x:y, z:w]' + assert str(X[x:y:t, w:t:x]) == 'X[x:y:t, w:t:x]' + assert str(X[x::y, t::w]) == 'X[x::y, t::w]' + assert str(X[:x:y, :t:w]) == 'X[:x:y, :t:w]' + assert str(X[::x, ::y]) == 'X[::x, ::y]' + assert str(MatrixSlice(X, (0, None, None), (0, None, None))) == 'X[:, :]' + assert str(MatrixSlice(X, (None, n, None), (None, n, None))) == 'X[:, :]' + assert str(MatrixSlice(X, (0, n, None), (0, n, None))) == 'X[:, :]' + assert str(MatrixSlice(X, (0, n, 2), (0, n, 2))) == 'X[::2, ::2]' + assert str(X[1:2:3, 4:5:6]) == 'X[1:2:3, 4:5:6]' + assert str(X[1:3:5, 4:6:8]) == 'X[1:3:5, 4:6:8]' + assert str(X[1:10:2]) == 'X[1:10:2, :]' + assert str(Y[:5, 1:9:2]) == 'Y[:5, 1:9:2]' + assert str(Y[:5, 1:10:2]) == 'Y[:5, 1::2]' + assert str(Y[5, :5:2]) == 'Y[5:6, :5:2]' + assert str(X[0:1, 0:1]) == 'X[:1, :1]' + assert str(X[0:1:2, 0:1:2]) == 'X[:1:2, :1:2]' + assert str((Y + Z)[2:, 2:]) == '(Y + Z)[2:, 2:]' + +def test_true_false(): + assert str(true) == repr(true) == sstr(true) == "True" + assert str(false) == repr(false) == sstr(false) == "False" + +def test_Equivalent(): + assert str(Equivalent(y, x)) == "Equivalent(x, y)" + +def test_Xor(): + assert str(Xor(y, x, evaluate=False)) == "x ^ y" + +def test_Complement(): + assert str(Complement(S.Reals, S.Naturals)) == 'Complement(Reals, Naturals)' + +def test_SymmetricDifference(): + assert str(SymmetricDifference(Interval(2, 3), Interval(3, 4),evaluate=False)) == \ + 'SymmetricDifference(Interval(2, 3), Interval(3, 4))' + + +def test_UnevaluatedExpr(): + a, b = symbols("a b") + expr1 = 2*UnevaluatedExpr(a+b) + assert str(expr1) == "2*(a + b)" + + +def test_MatrixElement_printing(): + # test cases for issue #11821 + A = MatrixSymbol("A", 1, 3) + B = MatrixSymbol("B", 1, 3) + C = MatrixSymbol("C", 1, 3) + + assert(str(A[0, 0]) == "A[0, 0]") + assert(str(3 * A[0, 0]) == "3*A[0, 0]") + + F = C[0, 0].subs(C, A - B) + assert str(F) == "(A - B)[0, 0]" + + +def test_MatrixSymbol_printing(): + A = MatrixSymbol("A", 3, 3) + B = MatrixSymbol("B", 3, 3) + + assert str(A - A*B - B) == "A - A*B - B" + assert str(A*B - (A+B)) == "-A + A*B - B" + assert str(A**(-1)) == "A**(-1)" + assert str(A**3) == "A**3" + + +def test_MatrixExpressions(): + n = Symbol('n', integer=True) + X = MatrixSymbol('X', n, n) + + assert str(X) == "X" + + # Apply function elementwise (`ElementwiseApplyFunc`): + + expr = (X.T*X).applyfunc(sin) + assert str(expr) == 'Lambda(_d, sin(_d)).(X.T*X)' + + lamda = Lambda(x, 1/x) + expr = (n*X).applyfunc(lamda) + assert str(expr) == 'Lambda(x, 1/x).(n*X)' + + +def test_Subs_printing(): + assert str(Subs(x, (x,), (1,))) == 'Subs(x, x, 1)' + assert str(Subs(x + y, (x, y), (1, 2))) == 'Subs(x + y, (x, y), (1, 2))' + + +def test_issue_15716(): + e = Integral(factorial(x), (x, -oo, oo)) + assert e.as_terms() == ([(e, ((1.0, 0.0), (1,), ()))], [e]) + + +def test_str_special_matrices(): + from sympy.matrices import Identity, ZeroMatrix, OneMatrix + assert str(Identity(4)) == 'I' + assert str(ZeroMatrix(2, 2)) == '0' + assert str(OneMatrix(2, 2)) == '1' + + +def test_issue_14567(): + assert factorial(Sum(-1, (x, 0, 0))) + y # doesn't raise an error + + +def test_issue_21823(): + assert str(Partition([1, 2])) == 'Partition({1, 2})' + assert str(Partition({1, 2})) == 'Partition({1, 2})' + + +def test_issue_22689(): + assert str(Mul(Pow(x,-2, evaluate=False), Pow(3,-1,evaluate=False), evaluate=False)) == "1/(x**2*3)" + + +def test_issue_21119_21460(): + ss = lambda x: str(S(x, evaluate=False)) + assert ss('4/2') == '4/2' + assert ss('4/-2') == '4/(-2)' + assert ss('-4/2') == '-4/2' + assert ss('-4/-2') == '-4/(-2)' + assert ss('-2*3/-1') == '-2*3/(-1)' + assert ss('-2*3/-1/2') == '-2*3/(-1*2)' + assert ss('4/2/1') == '4/(2*1)' + assert ss('-2/-1/2') == '-2/(-1*2)' + assert ss('2*3*4**(-2*3)') == '2*3/4**(2*3)' + assert ss('2*3*1*4**(-2*3)') == '2*3*1/4**(2*3)' + + +def test_Str(): + from sympy.core.symbol import Str + assert str(Str('x')) == 'x' + assert sstrrepr(Str('x')) == "Str('x')" + + +def test_diffgeom(): + from sympy.diffgeom import Manifold, Patch, CoordSystem, BaseScalarField + x,y = symbols('x y', real=True) + m = Manifold('M', 2) + assert str(m) == "M" + p = Patch('P', m) + assert str(p) == "P" + rect = CoordSystem('rect', p, [x, y]) + assert str(rect) == "rect" + b = BaseScalarField(rect, 0) + assert str(b) == "x" + +def test_NDimArray(): + assert sstr(NDimArray(1.0), full_prec=True) == '1.00000000000000' + assert sstr(NDimArray(1.0), full_prec=False) == '1.0' + assert sstr(NDimArray([1.0, 2.0]), full_prec=True) == '[1.00000000000000, 2.00000000000000]' + assert sstr(NDimArray([1.0, 2.0]), full_prec=False) == '[1.0, 2.0]' + assert sstr(NDimArray([], (0,))) == 'ImmutableDenseNDimArray([], (0,))' + assert sstr(NDimArray([], (0, 0))) == 'ImmutableDenseNDimArray([], (0, 0))' + assert sstr(NDimArray([], (0, 1))) == 'ImmutableDenseNDimArray([], (0, 1))' + assert sstr(NDimArray([], (1, 0))) == 'ImmutableDenseNDimArray([], (1, 0))' + +def test_Predicate(): + assert sstr(Q.even) == 'Q.even' + +def test_AppliedPredicate(): + assert sstr(Q.even(x)) == 'Q.even(x)' + +def test_printing_str_array_expressions(): + assert sstr(ArraySymbol("A", (2, 3, 4))) == "A" + assert sstr(ArrayElement("A", (2, 1/(1-x), 0))) == "A[2, 1/(1 - x), 0]" + M = MatrixSymbol("M", 3, 3) + N = MatrixSymbol("N", 3, 3) + assert sstr(ArrayElement(M*N, [x, 0])) == "(M*N)[x, 0]" + +def test_printing_stats(): + # issue 24132 + x = RandomSymbol("x") + y = RandomSymbol("y") + z1 = Probability(x > 0)*Identity(2) + z2 = Expectation(x)*Identity(2) + z3 = Variance(x)*Identity(2) + z4 = Covariance(x, y) * Identity(2) + + assert str(z1) == "Probability(x > 0)*I" + assert str(z2) == "Expectation(x)*I" + assert str(z3) == "Variance(x)*I" + assert str(z4) == "Covariance(x, y)*I" + assert z1.is_commutative == False + assert z2.is_commutative == False + assert z3.is_commutative == False + assert z4.is_commutative == False + assert z2._eval_is_commutative() == False + assert z3._eval_is_commutative() == False + assert z4._eval_is_commutative() == False diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_tableform.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_tableform.py new file mode 100644 index 0000000000000000000000000000000000000000..05802dd104a12f2f53d137167ecf31d201ff8dfc --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_tableform.py @@ -0,0 +1,182 @@ +from sympy.core.singleton import S +from sympy.printing.tableform import TableForm +from sympy.printing.latex import latex +from sympy.abc import x +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import sin +from sympy.testing.pytest import raises + +from textwrap import dedent + + +def test_TableForm(): + s = str(TableForm([["a", "b"], ["c", "d"], ["e", 0]], + headings="automatic")) + assert s == ( + ' | 1 2\n' + '-------\n' + '1 | a b\n' + '2 | c d\n' + '3 | e ' + ) + s = str(TableForm([["a", "b"], ["c", "d"], ["e", 0]], + headings="automatic", wipe_zeros=False)) + assert s == dedent('''\ + | 1 2 + ------- + 1 | a b + 2 | c d + 3 | e 0''') + s = str(TableForm([[x**2, "b"], ["c", x**2], ["e", "f"]], + headings=("automatic", None))) + assert s == ( + '1 | x**2 b \n' + '2 | c x**2\n' + '3 | e f ' + ) + s = str(TableForm([["a", "b"], ["c", "d"], ["e", "f"]], + headings=(None, "automatic"))) + assert s == dedent('''\ + 1 2 + --- + a b + c d + e f''') + s = str(TableForm([[5, 7], [4, 2], [10, 3]], + headings=[["Group A", "Group B", "Group C"], ["y1", "y2"]])) + assert s == ( + ' | y1 y2\n' + '---------------\n' + 'Group A | 5 7 \n' + 'Group B | 4 2 \n' + 'Group C | 10 3 ' + ) + raises( + ValueError, + lambda: + TableForm( + [[5, 7], [4, 2], [10, 3]], + headings=[["Group A", "Group B", "Group C"], ["y1", "y2"]], + alignments="middle") + ) + s = str(TableForm([[5, 7], [4, 2], [10, 3]], + headings=[["Group A", "Group B", "Group C"], ["y1", "y2"]], + alignments="right")) + assert s == dedent('''\ + | y1 y2 + --------------- + Group A | 5 7 + Group B | 4 2 + Group C | 10 3''') + + # other alignment permutations + d = [[1, 100], [100, 1]] + s = TableForm(d, headings=(('xxx', 'x'), None), alignments='l') + assert str(s) == ( + 'xxx | 1 100\n' + ' x | 100 1 ' + ) + s = TableForm(d, headings=(('xxx', 'x'), None), alignments='lr') + assert str(s) == dedent('''\ + xxx | 1 100 + x | 100 1''') + s = TableForm(d, headings=(('xxx', 'x'), None), alignments='clr') + assert str(s) == dedent('''\ + xxx | 1 100 + x | 100 1''') + + s = TableForm(d, headings=(('xxx', 'x'), None)) + assert str(s) == ( + 'xxx | 1 100\n' + ' x | 100 1 ' + ) + + raises(ValueError, lambda: TableForm(d, alignments='clr')) + + #pad + s = str(TableForm([[None, "-", 2], [1]], pad='?')) + assert s == dedent('''\ + ? - 2 + 1 ? ?''') + + +def test_TableForm_latex(): + s = latex(TableForm([[0, x**3], ["c", S.One/4], [sqrt(x), sin(x**2)]], + wipe_zeros=True, headings=("automatic", "automatic"))) + assert s == ( + '\\begin{tabular}{r l l}\n' + ' & 1 & 2 \\\\\n' + '\\hline\n' + '1 & & $x^{3}$ \\\\\n' + '2 & $c$ & $\\frac{1}{4}$ \\\\\n' + '3 & $\\sqrt{x}$ & $\\sin{\\left(x^{2} \\right)}$ \\\\\n' + '\\end{tabular}' + ) + s = latex(TableForm([[0, x**3], ["c", S.One/4], [sqrt(x), sin(x**2)]], + wipe_zeros=True, headings=("automatic", "automatic"), alignments='l')) + assert s == ( + '\\begin{tabular}{r l l}\n' + ' & 1 & 2 \\\\\n' + '\\hline\n' + '1 & & $x^{3}$ \\\\\n' + '2 & $c$ & $\\frac{1}{4}$ \\\\\n' + '3 & $\\sqrt{x}$ & $\\sin{\\left(x^{2} \\right)}$ \\\\\n' + '\\end{tabular}' + ) + s = latex(TableForm([[0, x**3], ["c", S.One/4], [sqrt(x), sin(x**2)]], + wipe_zeros=True, headings=("automatic", "automatic"), alignments='l'*3)) + assert s == ( + '\\begin{tabular}{l l l}\n' + ' & 1 & 2 \\\\\n' + '\\hline\n' + '1 & & $x^{3}$ \\\\\n' + '2 & $c$ & $\\frac{1}{4}$ \\\\\n' + '3 & $\\sqrt{x}$ & $\\sin{\\left(x^{2} \\right)}$ \\\\\n' + '\\end{tabular}' + ) + s = latex(TableForm([["a", x**3], ["c", S.One/4], [sqrt(x), sin(x**2)]], + headings=("automatic", "automatic"))) + assert s == ( + '\\begin{tabular}{r l l}\n' + ' & 1 & 2 \\\\\n' + '\\hline\n' + '1 & $a$ & $x^{3}$ \\\\\n' + '2 & $c$ & $\\frac{1}{4}$ \\\\\n' + '3 & $\\sqrt{x}$ & $\\sin{\\left(x^{2} \\right)}$ \\\\\n' + '\\end{tabular}' + ) + s = latex(TableForm([["a", x**3], ["c", S.One/4], [sqrt(x), sin(x**2)]], + formats=['(%s)', None], headings=("automatic", "automatic"))) + assert s == ( + '\\begin{tabular}{r l l}\n' + ' & 1 & 2 \\\\\n' + '\\hline\n' + '1 & (a) & $x^{3}$ \\\\\n' + '2 & (c) & $\\frac{1}{4}$ \\\\\n' + '3 & (sqrt(x)) & $\\sin{\\left(x^{2} \\right)}$ \\\\\n' + '\\end{tabular}' + ) + + def neg_in_paren(x, i, j): + if i % 2: + return ('(%s)' if x < 0 else '%s') % x + else: + pass # use default print + s = latex(TableForm([[-1, 2], [-3, 4]], + formats=[neg_in_paren]*2, headings=("automatic", "automatic"))) + assert s == ( + '\\begin{tabular}{r l l}\n' + ' & 1 & 2 \\\\\n' + '\\hline\n' + '1 & -1 & 2 \\\\\n' + '2 & (-3) & 4 \\\\\n' + '\\end{tabular}' + ) + s = latex(TableForm([["a", x**3], ["c", S.One/4], [sqrt(x), sin(x**2)]])) + assert s == ( + '\\begin{tabular}{l l}\n' + '$a$ & $x^{3}$ \\\\\n' + '$c$ & $\\frac{1}{4}$ \\\\\n' + '$\\sqrt{x}$ & $\\sin{\\left(x^{2} \\right)}$ \\\\\n' + '\\end{tabular}' + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_tensorflow.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_tensorflow.py new file mode 100644 index 0000000000000000000000000000000000000000..e9c92cd17b13e1148ebf83f13f66854b983491fe --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_tensorflow.py @@ -0,0 +1,493 @@ +import random +from sympy.core.function import Derivative +from sympy.core.symbol import symbols +from sympy import Piecewise +from sympy.tensor.array.expressions.array_expressions import ArrayTensorProduct, ArrayAdd, \ + PermuteDims, ArrayDiagonal +from sympy.core.relational import Eq, Ne, Ge, Gt, Le, Lt +from sympy.external import import_module +from sympy.functions import \ + Abs, ceiling, exp, floor, sign, sin, asin, sqrt, cos, \ + acos, tan, atan, atan2, cosh, acosh, sinh, asinh, tanh, atanh, \ + re, im, arg, erf, loggamma, log +from sympy.codegen.cfunctions import isnan, isinf +from sympy.matrices import Matrix, MatrixBase, eye, randMatrix +from sympy.matrices.expressions import \ + Determinant, HadamardProduct, Inverse, MatrixSymbol, Trace +from sympy.printing.tensorflow import tensorflow_code +from sympy.tensor.array.expressions.from_matrix_to_array import convert_matrix_to_array +from sympy.utilities.lambdify import lambdify +from sympy.testing.pytest import skip +from sympy.testing.pytest import XFAIL + + +tf = tensorflow = import_module("tensorflow") + +if tensorflow: + # Hide Tensorflow warnings + import os + os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' + + +M = MatrixSymbol("M", 3, 3) +N = MatrixSymbol("N", 3, 3) +P = MatrixSymbol("P", 3, 3) +Q = MatrixSymbol("Q", 3, 3) + +x, y, z, t = symbols("x y z t") + +if tf is not None: + llo = [list(range(i, i+3)) for i in range(0, 9, 3)] + m3x3 = tf.constant(llo) + m3x3sympy = Matrix(llo) + + +def _compare_tensorflow_matrix(variables, expr, use_float=False): + f = lambdify(variables, expr, 'tensorflow') + if not use_float: + random_matrices = [randMatrix(v.rows, v.cols) for v in variables] + else: + random_matrices = [randMatrix(v.rows, v.cols)/100. for v in variables] + + graph = tf.Graph() + r = None + with graph.as_default(): + random_variables = [eval(tensorflow_code(i)) for i in random_matrices] + session = tf.compat.v1.Session(graph=graph) + r = session.run(f(*random_variables)) + + e = expr.subs(dict(zip(variables, random_matrices))) + e = e.doit() + if e.is_Matrix: + if not isinstance(e, MatrixBase): + e = e.as_explicit() + e = e.tolist() + + if not use_float: + assert (r == e).all() + else: + r = [i for row in r for i in row] + e = [i for row in e for i in row] + assert all( + abs(a-b) < 10**-(4-int(log(abs(a), 10))) for a, b in zip(r, e)) + + +# Creating a custom inverse test. +# See https://github.com/sympy/sympy/issues/18469 +def _compare_tensorflow_matrix_inverse(variables, expr, use_float=False): + f = lambdify(variables, expr, 'tensorflow') + if not use_float: + random_matrices = [eye(v.rows, v.cols)*4 for v in variables] + else: + random_matrices = [eye(v.rows, v.cols)*3.14 for v in variables] + + graph = tf.Graph() + r = None + with graph.as_default(): + random_variables = [eval(tensorflow_code(i)) for i in random_matrices] + session = tf.compat.v1.Session(graph=graph) + r = session.run(f(*random_variables)) + + e = expr.subs(dict(zip(variables, random_matrices))) + e = e.doit() + if e.is_Matrix: + if not isinstance(e, MatrixBase): + e = e.as_explicit() + e = e.tolist() + + if not use_float: + assert (r == e).all() + else: + r = [i for row in r for i in row] + e = [i for row in e for i in row] + assert all( + abs(a-b) < 10**-(4-int(log(abs(a), 10))) for a, b in zip(r, e)) + + +def _compare_tensorflow_matrix_scalar(variables, expr): + f = lambdify(variables, expr, 'tensorflow') + random_matrices = [ + randMatrix(v.rows, v.cols).evalf() / 100 for v in variables] + + graph = tf.Graph() + r = None + with graph.as_default(): + random_variables = [eval(tensorflow_code(i)) for i in random_matrices] + session = tf.compat.v1.Session(graph=graph) + r = session.run(f(*random_variables)) + + e = expr.subs(dict(zip(variables, random_matrices))) + e = e.doit() + assert abs(r-e) < 10**-6 + + +def _compare_tensorflow_scalar( + variables, expr, rng=lambda: random.randint(0, 10)): + f = lambdify(variables, expr, 'tensorflow') + rvs = [rng() for v in variables] + + graph = tf.Graph() + r = None + with graph.as_default(): + tf_rvs = [eval(tensorflow_code(i)) for i in rvs] + session = tf.compat.v1.Session(graph=graph) + r = session.run(f(*tf_rvs)) + + e = expr.subs(dict(zip(variables, rvs))).evalf().doit() + assert abs(r-e) < 10**-6 + + +def _compare_tensorflow_relational( + variables, expr, rng=lambda: random.randint(0, 10)): + f = lambdify(variables, expr, 'tensorflow') + rvs = [rng() for v in variables] + + graph = tf.Graph() + r = None + with graph.as_default(): + tf_rvs = [eval(tensorflow_code(i)) for i in rvs] + session = tf.compat.v1.Session(graph=graph) + r = session.run(f(*tf_rvs)) + + e = expr.subs(dict(zip(variables, rvs))).doit() + assert r == e + + +def test_tensorflow_printing(): + assert tensorflow_code(eye(3)) == \ + "tensorflow.constant([[1, 0, 0], [0, 1, 0], [0, 0, 1]])" + + expr = Matrix([[x, sin(y)], [exp(z), -t]]) + assert tensorflow_code(expr) == \ + "tensorflow.Variable(" \ + "[[x, tensorflow.math.sin(y)]," \ + " [tensorflow.math.exp(z), -t]])" + + +# This (random) test is XFAIL because it fails occasionally +# See https://github.com/sympy/sympy/issues/18469 +@XFAIL +def test_tensorflow_math(): + if not tf: + skip("TensorFlow not installed") + + expr = Abs(x) + assert tensorflow_code(expr) == "tensorflow.math.abs(x)" + _compare_tensorflow_scalar((x,), expr) + + expr = sign(x) + assert tensorflow_code(expr) == "tensorflow.math.sign(x)" + _compare_tensorflow_scalar((x,), expr) + + expr = ceiling(x) + assert tensorflow_code(expr) == "tensorflow.math.ceil(x)" + _compare_tensorflow_scalar((x,), expr, rng=lambda: random.random()) + + expr = floor(x) + assert tensorflow_code(expr) == "tensorflow.math.floor(x)" + _compare_tensorflow_scalar((x,), expr, rng=lambda: random.random()) + + expr = exp(x) + assert tensorflow_code(expr) == "tensorflow.math.exp(x)" + _compare_tensorflow_scalar((x,), expr, rng=lambda: random.random()) + + expr = sqrt(x) + assert tensorflow_code(expr) == "tensorflow.math.sqrt(x)" + _compare_tensorflow_scalar((x,), expr, rng=lambda: random.random()) + + expr = x ** 4 + assert tensorflow_code(expr) == "tensorflow.math.pow(x, 4)" + _compare_tensorflow_scalar((x,), expr, rng=lambda: random.random()) + + expr = cos(x) + assert tensorflow_code(expr) == "tensorflow.math.cos(x)" + _compare_tensorflow_scalar((x,), expr, rng=lambda: random.random()) + + expr = acos(x) + assert tensorflow_code(expr) == "tensorflow.math.acos(x)" + _compare_tensorflow_scalar((x,), expr, rng=lambda: random.uniform(0, 0.95)) + + expr = sin(x) + assert tensorflow_code(expr) == "tensorflow.math.sin(x)" + _compare_tensorflow_scalar((x,), expr, rng=lambda: random.random()) + + expr = asin(x) + assert tensorflow_code(expr) == "tensorflow.math.asin(x)" + _compare_tensorflow_scalar((x,), expr, rng=lambda: random.random()) + + expr = tan(x) + assert tensorflow_code(expr) == "tensorflow.math.tan(x)" + _compare_tensorflow_scalar((x,), expr, rng=lambda: random.random()) + + expr = atan(x) + assert tensorflow_code(expr) == "tensorflow.math.atan(x)" + _compare_tensorflow_scalar((x,), expr, rng=lambda: random.random()) + + expr = atan2(y, x) + assert tensorflow_code(expr) == "tensorflow.math.atan2(y, x)" + _compare_tensorflow_scalar((y, x), expr, rng=lambda: random.random()) + + expr = cosh(x) + assert tensorflow_code(expr) == "tensorflow.math.cosh(x)" + _compare_tensorflow_scalar((x,), expr, rng=lambda: random.random()) + + expr = acosh(x) + assert tensorflow_code(expr) == "tensorflow.math.acosh(x)" + _compare_tensorflow_scalar((x,), expr, rng=lambda: random.uniform(1, 2)) + + expr = sinh(x) + assert tensorflow_code(expr) == "tensorflow.math.sinh(x)" + _compare_tensorflow_scalar((x,), expr, rng=lambda: random.uniform(1, 2)) + + expr = asinh(x) + assert tensorflow_code(expr) == "tensorflow.math.asinh(x)" + _compare_tensorflow_scalar((x,), expr, rng=lambda: random.uniform(1, 2)) + + expr = tanh(x) + assert tensorflow_code(expr) == "tensorflow.math.tanh(x)" + _compare_tensorflow_scalar((x,), expr, rng=lambda: random.uniform(1, 2)) + + expr = atanh(x) + assert tensorflow_code(expr) == "tensorflow.math.atanh(x)" + _compare_tensorflow_scalar( + (x,), expr, rng=lambda: random.uniform(-.5, .5)) + + expr = erf(x) + assert tensorflow_code(expr) == "tensorflow.math.erf(x)" + _compare_tensorflow_scalar( + (x,), expr, rng=lambda: random.random()) + + expr = loggamma(x) + assert tensorflow_code(expr) == "tensorflow.math.lgamma(x)" + _compare_tensorflow_scalar( + (x,), expr, rng=lambda: random.random()) + + +def test_tensorflow_complexes(): + assert tensorflow_code(re(x)) == "tensorflow.math.real(x)" + assert tensorflow_code(im(x)) == "tensorflow.math.imag(x)" + assert tensorflow_code(arg(x)) == "tensorflow.math.angle(x)" + + +def test_tensorflow_relational(): + if not tf: + skip("TensorFlow not installed") + + expr = Eq(x, y) + assert tensorflow_code(expr) == "tensorflow.math.equal(x, y)" + _compare_tensorflow_relational((x, y), expr) + + expr = Ne(x, y) + assert tensorflow_code(expr) == "tensorflow.math.not_equal(x, y)" + _compare_tensorflow_relational((x, y), expr) + + expr = Ge(x, y) + assert tensorflow_code(expr) == "tensorflow.math.greater_equal(x, y)" + _compare_tensorflow_relational((x, y), expr) + + expr = Gt(x, y) + assert tensorflow_code(expr) == "tensorflow.math.greater(x, y)" + _compare_tensorflow_relational((x, y), expr) + + expr = Le(x, y) + assert tensorflow_code(expr) == "tensorflow.math.less_equal(x, y)" + _compare_tensorflow_relational((x, y), expr) + + expr = Lt(x, y) + assert tensorflow_code(expr) == "tensorflow.math.less(x, y)" + _compare_tensorflow_relational((x, y), expr) + + +# This (random) test is XFAIL because it fails occasionally +# See https://github.com/sympy/sympy/issues/18469 +@XFAIL +def test_tensorflow_matrices(): + if not tf: + skip("TensorFlow not installed") + + expr = M + assert tensorflow_code(expr) == "M" + _compare_tensorflow_matrix((M,), expr) + + expr = M + N + assert tensorflow_code(expr) == "tensorflow.math.add(M, N)" + _compare_tensorflow_matrix((M, N), expr) + + expr = M * N + assert tensorflow_code(expr) == "tensorflow.linalg.matmul(M, N)" + _compare_tensorflow_matrix((M, N), expr) + + expr = HadamardProduct(M, N) + assert tensorflow_code(expr) == "tensorflow.math.multiply(M, N)" + _compare_tensorflow_matrix((M, N), expr) + + expr = M*N*P*Q + assert tensorflow_code(expr) == \ + "tensorflow.linalg.matmul(" \ + "tensorflow.linalg.matmul(" \ + "tensorflow.linalg.matmul(M, N), P), Q)" + _compare_tensorflow_matrix((M, N, P, Q), expr) + + expr = M**3 + assert tensorflow_code(expr) == \ + "tensorflow.linalg.matmul(tensorflow.linalg.matmul(M, M), M)" + _compare_tensorflow_matrix((M,), expr) + + expr = Trace(M) + assert tensorflow_code(expr) == "tensorflow.linalg.trace(M)" + _compare_tensorflow_matrix((M,), expr) + + expr = Determinant(M) + assert tensorflow_code(expr) == "tensorflow.linalg.det(M)" + _compare_tensorflow_matrix_scalar((M,), expr) + + expr = Inverse(M) + assert tensorflow_code(expr) == "tensorflow.linalg.inv(M)" + _compare_tensorflow_matrix_inverse((M,), expr, use_float=True) + + expr = M.T + assert tensorflow_code(expr, tensorflow_version='1.14') == \ + "tensorflow.linalg.matrix_transpose(M)" + assert tensorflow_code(expr, tensorflow_version='1.13') == \ + "tensorflow.matrix_transpose(M)" + + _compare_tensorflow_matrix((M,), expr) + + +def test_codegen_einsum(): + if not tf: + skip("TensorFlow not installed") + + graph = tf.Graph() + with graph.as_default(): + session = tf.compat.v1.Session(graph=graph) + + M = MatrixSymbol("M", 2, 2) + N = MatrixSymbol("N", 2, 2) + + cg = convert_matrix_to_array(M * N) + f = lambdify((M, N), cg, 'tensorflow') + + ma = tf.constant([[1, 2], [3, 4]]) + mb = tf.constant([[1,-2], [-1, 3]]) + y = session.run(f(ma, mb)) + c = session.run(tf.matmul(ma, mb)) + assert (y == c).all() + + +def test_codegen_extra(): + if not tf: + skip("TensorFlow not installed") + + graph = tf.Graph() + with graph.as_default(): + session = tf.compat.v1.Session() + + M = MatrixSymbol("M", 2, 2) + N = MatrixSymbol("N", 2, 2) + P = MatrixSymbol("P", 2, 2) + Q = MatrixSymbol("Q", 2, 2) + ma = tf.constant([[1, 2], [3, 4]]) + mb = tf.constant([[1,-2], [-1, 3]]) + mc = tf.constant([[2, 0], [1, 2]]) + md = tf.constant([[1,-1], [4, 7]]) + + cg = ArrayTensorProduct(M, N) + assert tensorflow_code(cg) == \ + 'tensorflow.linalg.einsum("ab,cd", M, N)' + f = lambdify((M, N), cg, 'tensorflow') + y = session.run(f(ma, mb)) + c = session.run(tf.einsum("ij,kl", ma, mb)) + assert (y == c).all() + + cg = ArrayAdd(M, N) + assert tensorflow_code(cg) == 'tensorflow.math.add(M, N)' + f = lambdify((M, N), cg, 'tensorflow') + y = session.run(f(ma, mb)) + c = session.run(ma + mb) + assert (y == c).all() + + cg = ArrayAdd(M, N, P) + assert tensorflow_code(cg) == \ + 'tensorflow.math.add(tensorflow.math.add(M, N), P)' + f = lambdify((M, N, P), cg, 'tensorflow') + y = session.run(f(ma, mb, mc)) + c = session.run(ma + mb + mc) + assert (y == c).all() + + cg = ArrayAdd(M, N, P, Q) + assert tensorflow_code(cg) == \ + 'tensorflow.math.add(' \ + 'tensorflow.math.add(tensorflow.math.add(M, N), P), Q)' + f = lambdify((M, N, P, Q), cg, 'tensorflow') + y = session.run(f(ma, mb, mc, md)) + c = session.run(ma + mb + mc + md) + assert (y == c).all() + + cg = PermuteDims(M, [1, 0]) + assert tensorflow_code(cg) == 'tensorflow.transpose(M, [1, 0])' + f = lambdify((M,), cg, 'tensorflow') + y = session.run(f(ma)) + c = session.run(tf.transpose(ma)) + assert (y == c).all() + + cg = PermuteDims(ArrayTensorProduct(M, N), [1, 2, 3, 0]) + assert tensorflow_code(cg) == \ + 'tensorflow.transpose(' \ + 'tensorflow.linalg.einsum("ab,cd", M, N), [1, 2, 3, 0])' + f = lambdify((M, N), cg, 'tensorflow') + y = session.run(f(ma, mb)) + c = session.run(tf.transpose(tf.einsum("ab,cd", ma, mb), [1, 2, 3, 0])) + assert (y == c).all() + + cg = ArrayDiagonal(ArrayTensorProduct(M, N), (1, 2)) + assert tensorflow_code(cg) == \ + 'tensorflow.linalg.einsum("ab,bc->acb", M, N)' + f = lambdify((M, N), cg, 'tensorflow') + y = session.run(f(ma, mb)) + c = session.run(tf.einsum("ab,bc->acb", ma, mb)) + assert (y == c).all() + + +def test_MatrixElement_printing(): + A = MatrixSymbol("A", 1, 3) + B = MatrixSymbol("B", 1, 3) + C = MatrixSymbol("C", 1, 3) + + assert tensorflow_code(A[0, 0]) == "A[0, 0]" + assert tensorflow_code(3 * A[0, 0]) == "3*A[0, 0]" + + F = C[0, 0].subs(C, A - B) + assert tensorflow_code(F) == "(tensorflow.math.add((-1)*B, A))[0, 0]" + + +def test_tensorflow_Derivative(): + expr = Derivative(sin(x), x) + assert tensorflow_code(expr) == \ + "tensorflow.gradients(tensorflow.math.sin(x), x)[0]" + +def test_tensorflow_isnan_isinf(): + if not tf: + skip("TensorFlow not installed") + + # Test for isnan + x = symbols("x") + # Return 0 if x is of nan value, and 1 otherwise + expression = Piecewise((0.0, isnan(x)), (1.0, True)) + printed_code = tensorflow_code(expression) + expected_printed_code = "tensorflow.where(tensorflow.math.is_nan(x), 0.0, 1.0)" + assert tensorflow_code(expression) == expected_printed_code, f"Incorrect printed result {printed_code}, expected {expected_printed_code}" + for _input, _expected in [(float('nan'), 0.0), (float('inf'), 1.0), (float('-inf'), 1.0), (1.0, 1.0)]: + _output = lambdify((x), expression, modules="tensorflow")(x=tf.constant([_input])) + assert (_output == _expected).numpy().all() + + # Test for isinf + x = symbols("x") + # Return 0 if x is of nan value, and 1 otherwise + expression = Piecewise((0.0, isinf(x)), (1.0, True)) + printed_code = tensorflow_code(expression) + expected_printed_code = "tensorflow.where(tensorflow.math.is_inf(x), 0.0, 1.0)" + assert tensorflow_code(expression) == expected_printed_code, f"Incorrect printed result {printed_code}, expected {expected_printed_code}" + for _input, _expected in [(float('inf'), 0.0), (float('-inf'), 0.0), (float('nan'), 1.0), (1.0, 1.0)]: + _output = lambdify((x), expression, modules="tensorflow")(x=tf.constant([_input])) + assert (_output == _expected).numpy().all() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_theanocode.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_theanocode.py new file mode 100644 index 0000000000000000000000000000000000000000..6ff40f78cb4de16149cb5e780756b7e32b574b71 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_theanocode.py @@ -0,0 +1,639 @@ +""" +Important note on tests in this module - the Theano printing functions use a +global cache by default, which means that tests using it will modify global +state and thus not be independent from each other. Instead of using the "cache" +keyword argument each time, this module uses the theano_code_ and +theano_function_ functions defined below which default to using a new, empty +cache instead. +""" + +import logging + +from sympy.external import import_module +from sympy.testing.pytest import raises, SKIP, warns_deprecated_sympy + +theanologger = logging.getLogger('theano.configdefaults') +theanologger.setLevel(logging.CRITICAL) +theano = import_module('theano') +theanologger.setLevel(logging.WARNING) + + +if theano: + import numpy as np + ts = theano.scalar + tt = theano.tensor + xt, yt, zt = [tt.scalar(name, 'floatX') for name in 'xyz'] + Xt, Yt, Zt = [tt.tensor('floatX', (False, False), name=n) for n in 'XYZ'] +else: + #bin/test will not execute any tests now + disabled = True + +import sympy as sy +from sympy.core.singleton import S +from sympy.abc import x, y, z, t +from sympy.printing.theanocode import (theano_code, dim_handling, + theano_function) + + +# Default set of matrix symbols for testing - make square so we can both +# multiply and perform elementwise operations between them. +X, Y, Z = [sy.MatrixSymbol(n, 4, 4) for n in 'XYZ'] + +# For testing AppliedUndef +f_t = sy.Function('f')(t) + + +def theano_code_(expr, **kwargs): + """ Wrapper for theano_code that uses a new, empty cache by default. """ + kwargs.setdefault('cache', {}) + with warns_deprecated_sympy(): + return theano_code(expr, **kwargs) + +def theano_function_(inputs, outputs, **kwargs): + """ Wrapper for theano_function that uses a new, empty cache by default. """ + kwargs.setdefault('cache', {}) + with warns_deprecated_sympy(): + return theano_function(inputs, outputs, **kwargs) + + +def fgraph_of(*exprs): + """ Transform SymPy expressions into Theano Computation. + + Parameters + ========== + exprs + SymPy expressions + + Returns + ======= + theano.gof.FunctionGraph + """ + outs = list(map(theano_code_, exprs)) + ins = theano.gof.graph.inputs(outs) + ins, outs = theano.gof.graph.clone(ins, outs) + return theano.gof.FunctionGraph(ins, outs) + + +def theano_simplify(fgraph): + """ Simplify a Theano Computation. + + Parameters + ========== + fgraph : theano.gof.FunctionGraph + + Returns + ======= + theano.gof.FunctionGraph + """ + mode = theano.compile.get_default_mode().excluding("fusion") + fgraph = fgraph.clone() + mode.optimizer.optimize(fgraph) + return fgraph + + +def theq(a, b): + """ Test two Theano objects for equality. + + Also accepts numeric types and lists/tuples of supported types. + + Note - debugprint() has a bug where it will accept numeric types but does + not respect the "file" argument and in this case and instead prints the number + to stdout and returns an empty string. This can lead to tests passing where + they should fail because any two numbers will always compare as equal. To + prevent this we treat numbers as a separate case. + """ + numeric_types = (int, float, np.number) + a_is_num = isinstance(a, numeric_types) + b_is_num = isinstance(b, numeric_types) + + # Compare numeric types using regular equality + if a_is_num or b_is_num: + if not (a_is_num and b_is_num): + return False + + return a == b + + # Compare sequences element-wise + a_is_seq = isinstance(a, (tuple, list)) + b_is_seq = isinstance(b, (tuple, list)) + + if a_is_seq or b_is_seq: + if not (a_is_seq and b_is_seq) or type(a) != type(b): + return False + + return list(map(theq, a)) == list(map(theq, b)) + + # Otherwise, assume debugprint() can handle it + astr = theano.printing.debugprint(a, file='str') + bstr = theano.printing.debugprint(b, file='str') + + # Check for bug mentioned above + for argname, argval, argstr in [('a', a, astr), ('b', b, bstr)]: + if argstr == '': + raise TypeError( + 'theano.printing.debugprint(%s) returned empty string ' + '(%s is instance of %r)' + % (argname, argname, type(argval)) + ) + + return astr == bstr + + +def test_example_symbols(): + """ + Check that the example symbols in this module print to their Theano + equivalents, as many of the other tests depend on this. + """ + assert theq(xt, theano_code_(x)) + assert theq(yt, theano_code_(y)) + assert theq(zt, theano_code_(z)) + assert theq(Xt, theano_code_(X)) + assert theq(Yt, theano_code_(Y)) + assert theq(Zt, theano_code_(Z)) + + +def test_Symbol(): + """ Test printing a Symbol to a theano variable. """ + xx = theano_code_(x) + assert isinstance(xx, (tt.TensorVariable, ts.ScalarVariable)) + assert xx.broadcastable == () + assert xx.name == x.name + + xx2 = theano_code_(x, broadcastables={x: (False,)}) + assert xx2.broadcastable == (False,) + assert xx2.name == x.name + +def test_MatrixSymbol(): + """ Test printing a MatrixSymbol to a theano variable. """ + XX = theano_code_(X) + assert isinstance(XX, tt.TensorVariable) + assert XX.broadcastable == (False, False) + +@SKIP # TODO - this is currently not checked but should be implemented +def test_MatrixSymbol_wrong_dims(): + """ Test MatrixSymbol with invalid broadcastable. """ + bcs = [(), (False,), (True,), (True, False), (False, True,), (True, True)] + for bc in bcs: + with raises(ValueError): + theano_code_(X, broadcastables={X: bc}) + +def test_AppliedUndef(): + """ Test printing AppliedUndef instance, which works similarly to Symbol. """ + ftt = theano_code_(f_t) + assert isinstance(ftt, tt.TensorVariable) + assert ftt.broadcastable == () + assert ftt.name == 'f_t' + + +def test_add(): + expr = x + y + comp = theano_code_(expr) + assert comp.owner.op == theano.tensor.add + +def test_trig(): + assert theq(theano_code_(sy.sin(x)), tt.sin(xt)) + assert theq(theano_code_(sy.tan(x)), tt.tan(xt)) + +def test_many(): + """ Test printing a complex expression with multiple symbols. """ + expr = sy.exp(x**2 + sy.cos(y)) * sy.log(2*z) + comp = theano_code_(expr) + expected = tt.exp(xt**2 + tt.cos(yt)) * tt.log(2*zt) + assert theq(comp, expected) + + +def test_dtype(): + """ Test specifying specific data types through the dtype argument. """ + for dtype in ['float32', 'float64', 'int8', 'int16', 'int32', 'int64']: + assert theano_code_(x, dtypes={x: dtype}).type.dtype == dtype + + # "floatX" type + assert theano_code_(x, dtypes={x: 'floatX'}).type.dtype in ('float32', 'float64') + + # Type promotion + assert theano_code_(x + 1, dtypes={x: 'float32'}).type.dtype == 'float32' + assert theano_code_(x + y, dtypes={x: 'float64', y: 'float32'}).type.dtype == 'float64' + + +def test_broadcastables(): + """ Test the "broadcastables" argument when printing symbol-like objects. """ + + # No restrictions on shape + for s in [x, f_t]: + for bc in [(), (False,), (True,), (False, False), (True, False)]: + assert theano_code_(s, broadcastables={s: bc}).broadcastable == bc + + # TODO - matrix broadcasting? + +def test_broadcasting(): + """ Test "broadcastable" attribute after applying element-wise binary op. """ + + expr = x + y + + cases = [ + [(), (), ()], + [(False,), (False,), (False,)], + [(True,), (False,), (False,)], + [(False, True), (False, False), (False, False)], + [(True, False), (False, False), (False, False)], + ] + + for bc1, bc2, bc3 in cases: + comp = theano_code_(expr, broadcastables={x: bc1, y: bc2}) + assert comp.broadcastable == bc3 + + +def test_MatMul(): + expr = X*Y*Z + expr_t = theano_code_(expr) + assert isinstance(expr_t.owner.op, tt.Dot) + assert theq(expr_t, Xt.dot(Yt).dot(Zt)) + +def test_Transpose(): + assert isinstance(theano_code_(X.T).owner.op, tt.DimShuffle) + +def test_MatAdd(): + expr = X+Y+Z + assert isinstance(theano_code_(expr).owner.op, tt.Elemwise) + + +def test_Rationals(): + assert theq(theano_code_(sy.Integer(2) / 3), tt.true_div(2, 3)) + assert theq(theano_code_(S.Half), tt.true_div(1, 2)) + +def test_Integers(): + assert theano_code_(sy.Integer(3)) == 3 + +def test_factorial(): + n = sy.Symbol('n') + assert theano_code_(sy.factorial(n)) + +def test_Derivative(): + simp = lambda expr: theano_simplify(fgraph_of(expr)) + assert theq(simp(theano_code_(sy.Derivative(sy.sin(x), x, evaluate=False))), + simp(theano.grad(tt.sin(xt), xt))) + + +def test_theano_function_simple(): + """ Test theano_function() with single output. """ + f = theano_function_([x, y], [x+y]) + assert f(2, 3) == 5 + +def test_theano_function_multi(): + """ Test theano_function() with multiple outputs. """ + f = theano_function_([x, y], [x+y, x-y]) + o1, o2 = f(2, 3) + assert o1 == 5 + assert o2 == -1 + +def test_theano_function_numpy(): + """ Test theano_function() vs Numpy implementation. """ + f = theano_function_([x, y], [x+y], dim=1, + dtypes={x: 'float64', y: 'float64'}) + assert np.linalg.norm(f([1, 2], [3, 4]) - np.asarray([4, 6])) < 1e-9 + + f = theano_function_([x, y], [x+y], dtypes={x: 'float64', y: 'float64'}, + dim=1) + xx = np.arange(3).astype('float64') + yy = 2*np.arange(3).astype('float64') + assert np.linalg.norm(f(xx, yy) - 3*np.arange(3)) < 1e-9 + + +def test_theano_function_matrix(): + m = sy.Matrix([[x, y], [z, x + y + z]]) + expected = np.array([[1.0, 2.0], [3.0, 1.0 + 2.0 + 3.0]]) + f = theano_function_([x, y, z], [m]) + np.testing.assert_allclose(f(1.0, 2.0, 3.0), expected) + f = theano_function_([x, y, z], [m], scalar=True) + np.testing.assert_allclose(f(1.0, 2.0, 3.0), expected) + f = theano_function_([x, y, z], [m, m]) + assert isinstance(f(1.0, 2.0, 3.0), type([])) + np.testing.assert_allclose(f(1.0, 2.0, 3.0)[0], expected) + np.testing.assert_allclose(f(1.0, 2.0, 3.0)[1], expected) + +def test_dim_handling(): + assert dim_handling([x], dim=2) == {x: (False, False)} + assert dim_handling([x, y], dims={x: 1, y: 2}) == {x: (False, True), + y: (False, False)} + assert dim_handling([x], broadcastables={x: (False,)}) == {x: (False,)} + +def test_theano_function_kwargs(): + """ + Test passing additional kwargs from theano_function() to theano.function(). + """ + import numpy as np + f = theano_function_([x, y, z], [x+y], dim=1, on_unused_input='ignore', + dtypes={x: 'float64', y: 'float64', z: 'float64'}) + assert np.linalg.norm(f([1, 2], [3, 4], [0, 0]) - np.asarray([4, 6])) < 1e-9 + + f = theano_function_([x, y, z], [x+y], + dtypes={x: 'float64', y: 'float64', z: 'float64'}, + dim=1, on_unused_input='ignore') + xx = np.arange(3).astype('float64') + yy = 2*np.arange(3).astype('float64') + zz = 2*np.arange(3).astype('float64') + assert np.linalg.norm(f(xx, yy, zz) - 3*np.arange(3)) < 1e-9 + +def test_theano_function_scalar(): + """ Test the "scalar" argument to theano_function(). """ + + args = [ + ([x, y], [x + y], None, [0]), # Single 0d output + ([X, Y], [X + Y], None, [2]), # Single 2d output + ([x, y], [x + y], {x: 0, y: 1}, [1]), # Single 1d output + ([x, y], [x + y, x - y], None, [0, 0]), # Two 0d outputs + ([x, y, X, Y], [x + y, X + Y], None, [0, 2]), # One 0d output, one 2d + ] + + # Create and test functions with and without the scalar setting + for inputs, outputs, in_dims, out_dims in args: + for scalar in [False, True]: + + f = theano_function_(inputs, outputs, dims=in_dims, scalar=scalar) + + # Check the theano_function attribute is set whether wrapped or not + assert isinstance(f.theano_function, theano.compile.function_module.Function) + + # Feed in inputs of the appropriate size and get outputs + in_values = [ + np.ones([1 if bc else 5 for bc in i.type.broadcastable]) + for i in f.theano_function.input_storage + ] + out_values = f(*in_values) + if not isinstance(out_values, list): + out_values = [out_values] + + # Check output types and shapes + assert len(out_dims) == len(out_values) + for d, value in zip(out_dims, out_values): + + if scalar and d == 0: + # Should have been converted to a scalar value + assert isinstance(value, np.number) + + else: + # Otherwise should be an array + assert isinstance(value, np.ndarray) + assert value.ndim == d + +def test_theano_function_bad_kwarg(): + """ + Passing an unknown keyword argument to theano_function() should raise an + exception. + """ + raises(Exception, lambda : theano_function_([x], [x+1], foobar=3)) + + +def test_slice(): + assert theano_code_(slice(1, 2, 3)) == slice(1, 2, 3) + + def theq_slice(s1, s2): + for attr in ['start', 'stop', 'step']: + a1 = getattr(s1, attr) + a2 = getattr(s2, attr) + if a1 is None or a2 is None: + if not (a1 is None or a2 is None): + return False + elif not theq(a1, a2): + return False + return True + + dtypes = {x: 'int32', y: 'int32'} + assert theq_slice(theano_code_(slice(x, y), dtypes=dtypes), slice(xt, yt)) + assert theq_slice(theano_code_(slice(1, x, 3), dtypes=dtypes), slice(1, xt, 3)) + +def test_MatrixSlice(): + from theano import Constant + + cache = {} + + n = sy.Symbol('n', integer=True) + X = sy.MatrixSymbol('X', n, n) + + Y = X[1:2:3, 4:5:6] + Yt = theano_code_(Y, cache=cache) + + s = ts.Scalar('int64') + assert tuple(Yt.owner.op.idx_list) == (slice(s, s, s), slice(s, s, s)) + assert Yt.owner.inputs[0] == theano_code_(X, cache=cache) + # == doesn't work in theano like it does in SymPy. You have to use + # equals. + assert all(Yt.owner.inputs[i].equals(Constant(s, i)) for i in range(1, 7)) + + k = sy.Symbol('k') + theano_code_(k, dtypes={k: 'int32'}) + start, stop, step = 4, k, 2 + Y = X[start:stop:step] + Yt = theano_code_(Y, dtypes={n: 'int32', k: 'int32'}) + # assert Yt.owner.op.idx_list[0].stop == kt + +def test_BlockMatrix(): + n = sy.Symbol('n', integer=True) + A, B, C, D = [sy.MatrixSymbol(name, n, n) for name in 'ABCD'] + At, Bt, Ct, Dt = map(theano_code_, (A, B, C, D)) + Block = sy.BlockMatrix([[A, B], [C, D]]) + Blockt = theano_code_(Block) + solutions = [tt.join(0, tt.join(1, At, Bt), tt.join(1, Ct, Dt)), + tt.join(1, tt.join(0, At, Ct), tt.join(0, Bt, Dt))] + assert any(theq(Blockt, solution) for solution in solutions) + +@SKIP +def test_BlockMatrix_Inverse_execution(): + k, n = 2, 4 + dtype = 'float32' + A = sy.MatrixSymbol('A', n, k) + B = sy.MatrixSymbol('B', n, n) + inputs = A, B + output = B.I*A + + cutsizes = {A: [(n//2, n//2), (k//2, k//2)], + B: [(n//2, n//2), (n//2, n//2)]} + cutinputs = [sy.blockcut(i, *cutsizes[i]) for i in inputs] + cutoutput = output.subs(dict(zip(inputs, cutinputs))) + + dtypes = dict(zip(inputs, [dtype]*len(inputs))) + f = theano_function_(inputs, [output], dtypes=dtypes, cache={}) + fblocked = theano_function_(inputs, [sy.block_collapse(cutoutput)], + dtypes=dtypes, cache={}) + + ninputs = [np.random.rand(*x.shape).astype(dtype) for x in inputs] + ninputs = [np.arange(n*k).reshape(A.shape).astype(dtype), + np.eye(n).astype(dtype)] + ninputs[1] += np.ones(B.shape)*1e-5 + + assert np.allclose(f(*ninputs), fblocked(*ninputs), rtol=1e-5) + +def test_DenseMatrix(): + t = sy.Symbol('theta') + for MatrixType in [sy.Matrix, sy.ImmutableMatrix]: + X = MatrixType([[sy.cos(t), -sy.sin(t)], [sy.sin(t), sy.cos(t)]]) + tX = theano_code_(X) + assert isinstance(tX, tt.TensorVariable) + assert tX.owner.op == tt.join_ + + +def test_cache_basic(): + """ Test single symbol-like objects are cached when printed by themselves. """ + + # Pairs of objects which should be considered equivalent with respect to caching + pairs = [ + (x, sy.Symbol('x')), + (X, sy.MatrixSymbol('X', *X.shape)), + (f_t, sy.Function('f')(sy.Symbol('t'))), + ] + + for s1, s2 in pairs: + cache = {} + st = theano_code_(s1, cache=cache) + + # Test hit with same instance + assert theano_code_(s1, cache=cache) is st + + # Test miss with same instance but new cache + assert theano_code_(s1, cache={}) is not st + + # Test hit with different but equivalent instance + assert theano_code_(s2, cache=cache) is st + +def test_global_cache(): + """ Test use of the global cache. """ + from sympy.printing.theanocode import global_cache + + backup = dict(global_cache) + try: + # Temporarily empty global cache + global_cache.clear() + + for s in [x, X, f_t]: + with warns_deprecated_sympy(): + st = theano_code(s) + assert theano_code(s) is st + + finally: + # Restore global cache + global_cache.update(backup) + +def test_cache_types_distinct(): + """ + Test that symbol-like objects of different types (Symbol, MatrixSymbol, + AppliedUndef) are distinguished by the cache even if they have the same + name. + """ + symbols = [sy.Symbol('f_t'), sy.MatrixSymbol('f_t', 4, 4), f_t] + + cache = {} # Single shared cache + printed = {} + + for s in symbols: + st = theano_code_(s, cache=cache) + assert st not in printed.values() + printed[s] = st + + # Check all printed objects are distinct + assert len(set(map(id, printed.values()))) == len(symbols) + + # Check retrieving + for s, st in printed.items(): + with warns_deprecated_sympy(): + assert theano_code(s, cache=cache) is st + +def test_symbols_are_created_once(): + """ + Test that a symbol is cached and reused when it appears in an expression + more than once. + """ + expr = sy.Add(x, x, evaluate=False) + comp = theano_code_(expr) + + assert theq(comp, xt + xt) + assert not theq(comp, xt + theano_code_(x)) + +def test_cache_complex(): + """ + Test caching on a complicated expression with multiple symbols appearing + multiple times. + """ + expr = x ** 2 + (y - sy.exp(x)) * sy.sin(z - x * y) + symbol_names = {s.name for s in expr.free_symbols} + expr_t = theano_code_(expr) + + # Iterate through variables in the Theano computational graph that the + # printed expression depends on + seen = set() + for v in theano.gof.graph.ancestors([expr_t]): + # Owner-less, non-constant variables should be our symbols + if v.owner is None and not isinstance(v, theano.gof.graph.Constant): + # Check it corresponds to a symbol and appears only once + assert v.name in symbol_names + assert v.name not in seen + seen.add(v.name) + + # Check all were present + assert seen == symbol_names + + +def test_Piecewise(): + # A piecewise linear + expr = sy.Piecewise((0, x<0), (x, x<2), (1, True)) # ___/III + result = theano_code_(expr) + assert result.owner.op == tt.switch + + expected = tt.switch(xt<0, 0, tt.switch(xt<2, xt, 1)) + assert theq(result, expected) + + expr = sy.Piecewise((x, x < 0)) + result = theano_code_(expr) + expected = tt.switch(xt < 0, xt, np.nan) + assert theq(result, expected) + + expr = sy.Piecewise((0, sy.And(x>0, x<2)), \ + (x, sy.Or(x>2, x<0))) + result = theano_code_(expr) + expected = tt.switch(tt.and_(xt>0,xt<2), 0, \ + tt.switch(tt.or_(xt>2, xt<0), xt, np.nan)) + assert theq(result, expected) + + +def test_Relationals(): + assert theq(theano_code_(sy.Eq(x, y)), tt.eq(xt, yt)) + # assert theq(theano_code_(sy.Ne(x, y)), tt.neq(xt, yt)) # TODO - implement + assert theq(theano_code_(x > y), xt > yt) + assert theq(theano_code_(x < y), xt < yt) + assert theq(theano_code_(x >= y), xt >= yt) + assert theq(theano_code_(x <= y), xt <= yt) + + +def test_complexfunctions(): + with warns_deprecated_sympy(): + xt, yt = theano_code_(x, dtypes={x:'complex128'}), theano_code_(y, dtypes={y: 'complex128'}) + from sympy.functions.elementary.complexes import conjugate + from theano.tensor import as_tensor_variable as atv + from theano.tensor import complex as cplx + with warns_deprecated_sympy(): + assert theq(theano_code_(y*conjugate(x)), yt*(xt.conj())) + assert theq(theano_code_((1+2j)*x), xt*(atv(1.0)+atv(2.0)*cplx(0,1))) + + +def test_constantfunctions(): + with warns_deprecated_sympy(): + tf = theano_function_([],[1+1j]) + assert(tf()==1+1j) + + +def test_Exp1(): + """ + Test that exp(1) prints without error and evaluates close to SymPy's E + """ + # sy.exp(1) should yield same instance of E as sy.E (singleton), but extra + # check added for sanity + e_a = sy.exp(1) + e_b = sy.E + + np.testing.assert_allclose(float(e_a), np.e) + np.testing.assert_allclose(float(e_b), np.e) + + e = theano_code_(e_a) + np.testing.assert_allclose(float(e_a), e.eval()) + + e = theano_code_(e_b) + np.testing.assert_allclose(float(e_b), e.eval()) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_torch.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_torch.py new file mode 100644 index 0000000000000000000000000000000000000000..8ce2c6cec75e03264f93b472a79eb073742e3486 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_torch.py @@ -0,0 +1,531 @@ +import random +import math + +from sympy import symbols, Derivative +from sympy.printing.pytorch import torch_code +from sympy import (eye, MatrixSymbol, Matrix) +from sympy.tensor.array import NDimArray +from sympy.tensor.array.expressions.array_expressions import ( + ArrayTensorProduct, ArrayAdd, + PermuteDims, ArrayDiagonal, _CodegenArrayAbstract) +from sympy.utilities.lambdify import lambdify +from sympy.core.relational import Eq, Ne, Ge, Gt, Le, Lt +from sympy.functions import \ + Abs, ceiling, exp, floor, sign, sin, asin, cos, \ + acos, tan, atan, atan2, cosh, acosh, sinh, asinh, tanh, atanh, \ + re, im, arg, erf, loggamma, sqrt +from sympy.testing.pytest import skip +from sympy.external import import_module +from sympy.matrices.expressions import \ + Determinant, HadamardProduct, Inverse, Trace +from sympy.matrices import randMatrix +from sympy.matrices import Identity, ZeroMatrix, OneMatrix +from sympy import conjugate, I +from sympy import Heaviside, gamma, polygamma + + + +torch = import_module("torch") + +M = MatrixSymbol("M", 3, 3) +N = MatrixSymbol("N", 3, 3) +P = MatrixSymbol("P", 3, 3) +Q = MatrixSymbol("Q", 3, 3) + +x, y, z, t = symbols("x y z t") + +if torch is not None: + llo = [list(range(i, i + 3)) for i in range(0, 9, 3)] + m3x3 = torch.tensor(llo, dtype=torch.float64) + m3x3sympy = Matrix(llo) + + +def _compare_torch_matrix(variables, expr): + f = lambdify(variables, expr, 'torch') + + random_matrices = [randMatrix(i.shape[0], i.shape[1]) for i in variables] + random_variables = [torch.tensor(i.tolist(), dtype=torch.float64) for i in random_matrices] + r = f(*random_variables) + e = expr.subs(dict(zip(variables, random_matrices))).doit() + + if isinstance(e, _CodegenArrayAbstract): + e = e.doit() + + if hasattr(e, 'is_number') and e.is_number: + if isinstance(r, torch.Tensor) and r.dim() == 0: + r = r.item() + e = float(e) + assert abs(r - e) < 1e-6 + return + + if e.is_Matrix or isinstance(e, NDimArray): + e = torch.tensor(e.tolist(), dtype=torch.float64) + assert torch.allclose(r, e, atol=1e-6) + else: + raise TypeError(f"Cannot compare {type(r)} with {type(e)}") + + +def _compare_torch_scalar(variables, expr, rng=lambda: random.uniform(-5, 5)): + f = lambdify(variables, expr, 'torch') + rvs = [rng() for v in variables] + t_rvs = [torch.tensor(i, dtype=torch.float64) for i in rvs] + r = f(*t_rvs) + if isinstance(r, torch.Tensor): + r = r.item() + e = expr.subs(dict(zip(variables, rvs))).doit() + assert abs(r - e) < 1e-6 + + +def _compare_torch_relational(variables, expr, rng=lambda: random.randint(0, 10)): + f = lambdify(variables, expr, 'torch') + rvs = [rng() for v in variables] + t_rvs = [torch.tensor(i, dtype=torch.float64) for i in rvs] + r = f(*t_rvs) + e = bool(expr.subs(dict(zip(variables, rvs))).doit()) + assert r.item() == e + + +def test_torch_math(): + if not torch: + skip("PyTorch not installed") + + expr = Abs(x) + assert torch_code(expr) == "torch.abs(x)" + f = lambdify(x, expr, 'torch') + ma = torch.tensor([[-1, 2, -3, -4]], dtype=torch.float64) + y_abs = f(ma) + c = torch.abs(ma) + assert torch.all(y_abs == c) + + expr = sign(x) + assert torch_code(expr) == "torch.sign(x)" + _compare_torch_scalar((x,), expr, rng=lambda: random.uniform(-10, 10)) + + expr = ceiling(x) + assert torch_code(expr) == "torch.ceil(x)" + _compare_torch_scalar((x,), expr, rng=lambda: random.random()) + + expr = floor(x) + assert torch_code(expr) == "torch.floor(x)" + _compare_torch_scalar((x,), expr, rng=lambda: random.random()) + + expr = exp(x) + assert torch_code(expr) == "torch.exp(x)" + _compare_torch_scalar((x,), expr, rng=lambda: random.uniform(-2, 2)) + + expr = sqrt(x) + assert torch_code(expr) == "torch.sqrt(x)" + _compare_torch_scalar((x,), expr, rng=lambda: random.random()) + + expr = x ** 4 + assert torch_code(expr) == "torch.pow(x, 4)" + _compare_torch_scalar((x,), expr, rng=lambda: random.random()) + + expr = cos(x) + assert torch_code(expr) == "torch.cos(x)" + _compare_torch_scalar((x,), expr, rng=lambda: random.random()) + + expr = acos(x) + assert torch_code(expr) == "torch.acos(x)" + _compare_torch_scalar((x,), expr, rng=lambda: random.uniform(-0.99, 0.99)) + + expr = sin(x) + assert torch_code(expr) == "torch.sin(x)" + _compare_torch_scalar((x,), expr, rng=lambda: random.random()) + + expr = asin(x) + assert torch_code(expr) == "torch.asin(x)" + _compare_torch_scalar((x,), expr, rng=lambda: random.uniform(-0.99, 0.99)) + + expr = tan(x) + assert torch_code(expr) == "torch.tan(x)" + _compare_torch_scalar((x,), expr, rng=lambda: random.uniform(-1.5, 1.5)) + + expr = atan(x) + assert torch_code(expr) == "torch.atan(x)" + _compare_torch_scalar((x,), expr, rng=lambda: random.uniform(-5, 5)) + + expr = atan2(y, x) + assert torch_code(expr) == "torch.atan2(y, x)" + _compare_torch_scalar((y, x), expr, rng=lambda: random.uniform(-5, 5)) + + expr = cosh(x) + assert torch_code(expr) == "torch.cosh(x)" + _compare_torch_scalar((x,), expr, rng=lambda: random.uniform(-2, 2)) + + expr = acosh(x) + assert torch_code(expr) == "torch.acosh(x)" + _compare_torch_scalar((x,), expr, rng=lambda: random.uniform(1.1, 5)) + + expr = sinh(x) + assert torch_code(expr) == "torch.sinh(x)" + _compare_torch_scalar((x,), expr, rng=lambda: random.uniform(-2, 2)) + + expr = asinh(x) + assert torch_code(expr) == "torch.asinh(x)" + _compare_torch_scalar((x,), expr, rng=lambda: random.uniform(-5, 5)) + + expr = tanh(x) + assert torch_code(expr) == "torch.tanh(x)" + _compare_torch_scalar((x,), expr, rng=lambda: random.uniform(-2, 2)) + + expr = atanh(x) + assert torch_code(expr) == "torch.atanh(x)" + _compare_torch_scalar((x,), expr, rng=lambda: random.uniform(-0.9, 0.9)) + + expr = erf(x) + assert torch_code(expr) == "torch.erf(x)" + _compare_torch_scalar((x,), expr, rng=lambda: random.uniform(-2, 2)) + + expr = loggamma(x) + assert torch_code(expr) == "torch.lgamma(x)" + _compare_torch_scalar((x,), expr, rng=lambda: random.uniform(0.5, 5)) + + +def test_torch_complexes(): + assert torch_code(re(x)) == "torch.real(x)" + assert torch_code(im(x)) == "torch.imag(x)" + assert torch_code(arg(x)) == "torch.angle(x)" + + +def test_torch_relational(): + if not torch: + skip("PyTorch not installed") + + expr = Eq(x, y) + assert torch_code(expr) == "torch.eq(x, y)" + _compare_torch_relational((x, y), expr) + + expr = Ne(x, y) + assert torch_code(expr) == "torch.ne(x, y)" + _compare_torch_relational((x, y), expr) + + expr = Ge(x, y) + assert torch_code(expr) == "torch.ge(x, y)" + _compare_torch_relational((x, y), expr) + + expr = Gt(x, y) + assert torch_code(expr) == "torch.gt(x, y)" + _compare_torch_relational((x, y), expr) + + expr = Le(x, y) + assert torch_code(expr) == "torch.le(x, y)" + _compare_torch_relational((x, y), expr) + + expr = Lt(x, y) + assert torch_code(expr) == "torch.lt(x, y)" + _compare_torch_relational((x, y), expr) + + +def test_torch_matrix(): + if torch is None: + skip("PyTorch not installed") + + expr = M + assert torch_code(expr) == "M" + f = lambdify((M,), expr, "torch") + eye_mat = eye(3) + eye_tensor = torch.tensor(eye_mat.tolist(), dtype=torch.float64) + assert torch.allclose(f(eye_tensor), eye_tensor) + + expr = M * N + assert torch_code(expr) == "torch.matmul(M, N)" + _compare_torch_matrix((M, N), expr) + + expr = M ** 3 + assert torch_code(expr) == "torch.mm(torch.mm(M, M), M)" + _compare_torch_matrix((M,), expr) + + expr = M * N * P * Q + assert torch_code(expr) == "torch.matmul(torch.matmul(torch.matmul(M, N), P), Q)" + _compare_torch_matrix((M, N, P, Q), expr) + + expr = Trace(M) + assert torch_code(expr) == "torch.trace(M)" + _compare_torch_matrix((M,), expr) + + expr = Determinant(M) + assert torch_code(expr) == "torch.det(M)" + _compare_torch_matrix((M,), expr) + + expr = HadamardProduct(M, N) + assert torch_code(expr) == "torch.mul(M, N)" + _compare_torch_matrix((M, N), expr) + + expr = Inverse(M) + assert torch_code(expr) == "torch.linalg.inv(M)" + + # For inverse, use a matrix that's guaranteed to be invertible + eye_mat = eye(3) + eye_tensor = torch.tensor(eye_mat.tolist(), dtype=torch.float64) + f = lambdify((M,), expr, "torch") + result = f(eye_tensor) + expected = torch.linalg.inv(eye_tensor) + assert torch.allclose(result, expected) + + +def test_torch_array_operations(): + if not torch: + skip("PyTorch not installed") + + M = MatrixSymbol("M", 2, 2) + N = MatrixSymbol("N", 2, 2) + P = MatrixSymbol("P", 2, 2) + Q = MatrixSymbol("Q", 2, 2) + + ma = torch.tensor([[1., 2.], [3., 4.]], dtype=torch.float64) + mb = torch.tensor([[1., -2.], [-1., 3.]], dtype=torch.float64) + mc = torch.tensor([[2., 0.], [1., 2.]], dtype=torch.float64) + md = torch.tensor([[1., -1.], [4., 7.]], dtype=torch.float64) + + cg = ArrayTensorProduct(M, N) + assert torch_code(cg) == 'torch.einsum("ab,cd", M, N)' + f = lambdify((M, N), cg, 'torch') + y = f(ma, mb) + c = torch.einsum("ij,kl", ma, mb) + assert torch.allclose(y, c) + + cg = ArrayAdd(M, N) + assert torch_code(cg) == 'torch.add(M, N)' + f = lambdify((M, N), cg, 'torch') + y = f(ma, mb) + c = ma + mb + assert torch.allclose(y, c) + + cg = ArrayAdd(M, N, P) + assert torch_code(cg) == 'torch.add(torch.add(M, N), P)' + f = lambdify((M, N, P), cg, 'torch') + y = f(ma, mb, mc) + c = ma + mb + mc + assert torch.allclose(y, c) + + cg = ArrayAdd(M, N, P, Q) + assert torch_code(cg) == 'torch.add(torch.add(torch.add(M, N), P), Q)' + f = lambdify((M, N, P, Q), cg, 'torch') + y = f(ma, mb, mc, md) + c = ma + mb + mc + md + assert torch.allclose(y, c) + + cg = PermuteDims(M, [1, 0]) + assert torch_code(cg) == 'M.permute(1, 0)' + f = lambdify((M,), cg, 'torch') + y = f(ma) + c = ma.T + assert torch.allclose(y, c) + + cg = PermuteDims(ArrayTensorProduct(M, N), [1, 2, 3, 0]) + assert torch_code(cg) == 'torch.einsum("ab,cd", M, N).permute(1, 2, 3, 0)' + f = lambdify((M, N), cg, 'torch') + y = f(ma, mb) + c = torch.einsum("ab,cd", ma, mb).permute(1, 2, 3, 0) + assert torch.allclose(y, c) + + cg = ArrayDiagonal(ArrayTensorProduct(M, N), (1, 2)) + assert torch_code(cg) == 'torch.einsum("ab,bc->acb", M, N)' + f = lambdify((M, N), cg, 'torch') + y = f(ma, mb) + c = torch.einsum("ab,bc->acb", ma, mb) + assert torch.allclose(y, c) + + +def test_torch_derivative(): + """Test derivative handling.""" + expr = Derivative(sin(x), x) + assert torch_code(expr) == 'torch.autograd.grad(torch.sin(x), x)[0]' + + +def test_torch_printing_dtype(): + if not torch: + skip("PyTorch not installed") + + # matrix printing with default dtype + expr = Matrix([[x, sin(y)], [exp(z), -t]]) + assert "dtype=torch.float64" in torch_code(expr) + + # explicit dtype + assert "dtype=torch.float32" in torch_code(expr, dtype="torch.float32") + + # with requires_grad + result = torch_code(expr, requires_grad=True) + assert "requires_grad=True" in result + assert "dtype=torch.float64" in result + + # both + result = torch_code(expr, requires_grad=True, dtype="torch.float32") + assert "requires_grad=True" in result + assert "dtype=torch.float32" in result + + +def test_requires_grad(): + if not torch: + skip("PyTorch not installed") + + expr = sin(x) + cos(y) + f = lambdify([x, y], expr, 'torch') + + # make sure the gradients flow + x_val = torch.tensor(1.0, requires_grad=True) + y_val = torch.tensor(2.0, requires_grad=True) + result = f(x_val, y_val) + assert result.requires_grad + result.backward() + + # x_val.grad should be cos(x_val) which is close to cos(1.0) + assert abs(x_val.grad.item() - float(cos(1.0).evalf())) < 1e-6 + + # y_val.grad should be -sin(y_val) which is close to -sin(2.0) + assert abs(y_val.grad.item() - float(-sin(2.0).evalf())) < 1e-6 + + +def test_torch_multi_variable_derivatives(): + if not torch: + skip("PyTorch not installed") + + x, y, z = symbols("x y z") + + expr = Derivative(sin(x), x) + assert torch_code(expr) == "torch.autograd.grad(torch.sin(x), x)[0]" + + expr = Derivative(sin(x), (x, 2)) + assert torch_code( + expr) == "torch.autograd.grad(torch.autograd.grad(torch.sin(x), x, create_graph=True)[0], x, create_graph=True)[0]" + + expr = Derivative(sin(x * y), x, y) + result = torch_code(expr) + expected = "torch.autograd.grad(torch.autograd.grad(torch.sin(x*y), x, create_graph=True)[0], y, create_graph=True)[0]" + normalized_result = result.replace(" ", "") + normalized_expected = expected.replace(" ", "") + assert normalized_result == normalized_expected + + expr = Derivative(sin(x), x, x) + result = torch_code(expr) + expected = "torch.autograd.grad(torch.autograd.grad(torch.sin(x), x, create_graph=True)[0], x, create_graph=True)[0]" + assert result == expected + + expr = Derivative(sin(x * y * z), x, (y, 2), z) + result = torch_code(expr) + expected = "torch.autograd.grad(torch.autograd.grad(torch.autograd.grad(torch.autograd.grad(torch.sin(x*y*z), x, create_graph=True)[0], y, create_graph=True)[0], y, create_graph=True)[0], z, create_graph=True)[0]" + normalized_result = result.replace(" ", "") + normalized_expected = expected.replace(" ", "") + assert normalized_result == normalized_expected + + +def test_torch_derivative_lambdify(): + if not torch: + skip("PyTorch not installed") + + x = symbols("x") + y = symbols("y") + + expr = Derivative(x ** 2, x) + f = lambdify(x, expr, 'torch') + x_val = torch.tensor(2.0, requires_grad=True) + result = f(x_val) + assert torch.isclose(result, torch.tensor(4.0)) + + expr = Derivative(sin(x), (x, 2)) + f = lambdify(x, expr, 'torch') + # Second derivative of sin(x) at x=0 is 0, not -1 + x_val = torch.tensor(0.0, requires_grad=True) + result = f(x_val) + assert torch.isclose(result, torch.tensor(0.0), atol=1e-5) + + x_val = torch.tensor(math.pi / 2, requires_grad=True) + result = f(x_val) + assert torch.isclose(result, torch.tensor(-1.0), atol=1e-5) + + expr = Derivative(x * y ** 2, x, y) + f = lambdify((x, y), expr, 'torch') + x_val = torch.tensor(2.0, requires_grad=True) + y_val = torch.tensor(3.0, requires_grad=True) + result = f(x_val, y_val) + assert torch.isclose(result, torch.tensor(6.0)) + + +def test_torch_special_matrices(): + if not torch: + skip("PyTorch not installed") + + expr = Identity(3) + assert torch_code(expr) == "torch.eye(3)" + + n = symbols("n") + expr = Identity(n) + assert torch_code(expr) == "torch.eye(n, n)" + + expr = ZeroMatrix(2, 3) + assert torch_code(expr) == "torch.zeros((2, 3))" + + m, n = symbols("m n") + expr = ZeroMatrix(m, n) + assert torch_code(expr) == "torch.zeros((m, n))" + + expr = OneMatrix(2, 3) + assert torch_code(expr) == "torch.ones((2, 3))" + + expr = OneMatrix(m, n) + assert torch_code(expr) == "torch.ones((m, n))" + + +def test_torch_special_matrices_lambdify(): + if not torch: + skip("PyTorch not installed") + + expr = Identity(3) + f = lambdify([], expr, 'torch') + result = f() + expected = torch.eye(3) + assert torch.allclose(result, expected) + + expr = ZeroMatrix(2, 3) + f = lambdify([], expr, 'torch') + result = f() + expected = torch.zeros((2, 3)) + assert torch.allclose(result, expected) + + expr = OneMatrix(2, 3) + f = lambdify([], expr, 'torch') + result = f() + expected = torch.ones((2, 3)) + assert torch.allclose(result, expected) + + +def test_torch_complex_operations(): + if not torch: + skip("PyTorch not installed") + + expr = conjugate(x) + assert torch_code(expr) == "torch.conj(x)" + + # SymPy distributes conjugate over addition and applies specific rules for each term + expr = conjugate(sin(x) + I * cos(y)) + assert torch_code(expr) == "torch.sin(torch.conj(x)) - 1j*torch.cos(torch.conj(y))" + + expr = I + assert torch_code(expr) == "1j" + + expr = 2 * I + x + assert torch_code(expr) == "x + 2*1j" + + expr = exp(I * x) + assert torch_code(expr) == "torch.exp(1j*x)" + + +def test_torch_special_functions(): + if not torch: + skip("PyTorch not installed") + + expr = Heaviside(x) + assert torch_code(expr) == "torch.heaviside(x, 1/2)" + + expr = Heaviside(x, 0) + assert torch_code(expr) == "torch.heaviside(x, 0)" + + expr = gamma(x) + assert torch_code(expr) == "torch.special.gamma(x)" + + expr = polygamma(0, x) # Use polygamma instead of digamma because sympy will default to that anyway + assert torch_code(expr) == "torch.special.digamma(x)" + + expr = gamma(sin(x)) + assert torch_code(expr) == "torch.special.gamma(torch.sin(x))" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_tree.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_tree.py new file mode 100644 index 0000000000000000000000000000000000000000..cf116d0cac5d38f225815fcd2d4ac90cd0dd96d7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tests/test_tree.py @@ -0,0 +1,196 @@ +from sympy.printing.tree import tree +from sympy.testing.pytest import XFAIL + + +# Remove this flag after making _assumptions cache deterministic. +@XFAIL +def test_print_tree_MatAdd(): + from sympy.matrices.expressions import MatrixSymbol + A = MatrixSymbol('A', 3, 3) + B = MatrixSymbol('B', 3, 3) + + test_str = [ + 'MatAdd: A + B\n', + 'algebraic: False\n', + 'commutative: False\n', + 'complex: False\n', + 'composite: False\n', + 'even: False\n', + 'extended_negative: False\n', + 'extended_nonnegative: False\n', + 'extended_nonpositive: False\n', + 'extended_nonzero: False\n', + 'extended_positive: False\n', + 'extended_real: False\n', + 'imaginary: False\n', + 'integer: False\n', + 'irrational: False\n', + 'negative: False\n', + 'noninteger: False\n', + 'nonnegative: False\n', + 'nonpositive: False\n', + 'nonzero: False\n', + 'odd: False\n', + 'positive: False\n', + 'prime: False\n', + 'rational: False\n', + 'real: False\n', + 'transcendental: False\n', + 'zero: False\n', + '+-MatrixSymbol: A\n', + '| algebraic: False\n', + '| commutative: False\n', + '| complex: False\n', + '| composite: False\n', + '| even: False\n', + '| extended_negative: False\n', + '| extended_nonnegative: False\n', + '| extended_nonpositive: False\n', + '| extended_nonzero: False\n', + '| extended_positive: False\n', + '| extended_real: False\n', + '| imaginary: False\n', + '| integer: False\n', + '| irrational: False\n', + '| negative: False\n', + '| noninteger: False\n', + '| nonnegative: False\n', + '| nonpositive: False\n', + '| nonzero: False\n', + '| odd: False\n', + '| positive: False\n', + '| prime: False\n', + '| rational: False\n', + '| real: False\n', + '| transcendental: False\n', + '| zero: False\n', + '| +-Symbol: A\n', + '| | commutative: True\n', + '| +-Integer: 3\n', + '| | algebraic: True\n', + '| | commutative: True\n', + '| | complex: True\n', + '| | extended_negative: False\n', + '| | extended_nonnegative: True\n', + '| | extended_real: True\n', + '| | finite: True\n', + '| | hermitian: True\n', + '| | imaginary: False\n', + '| | infinite: False\n', + '| | integer: True\n', + '| | irrational: False\n', + '| | negative: False\n', + '| | noninteger: False\n', + '| | nonnegative: True\n', + '| | rational: True\n', + '| | real: True\n', + '| | transcendental: False\n', + '| +-Integer: 3\n', + '| algebraic: True\n', + '| commutative: True\n', + '| complex: True\n', + '| extended_negative: False\n', + '| extended_nonnegative: True\n', + '| extended_real: True\n', + '| finite: True\n', + '| hermitian: True\n', + '| imaginary: False\n', + '| infinite: False\n', + '| integer: True\n', + '| irrational: False\n', + '| negative: False\n', + '| noninteger: False\n', + '| nonnegative: True\n', + '| rational: True\n', + '| real: True\n', + '| transcendental: False\n', + '+-MatrixSymbol: B\n', + ' algebraic: False\n', + ' commutative: False\n', + ' complex: False\n', + ' composite: False\n', + ' even: False\n', + ' extended_negative: False\n', + ' extended_nonnegative: False\n', + ' extended_nonpositive: False\n', + ' extended_nonzero: False\n', + ' extended_positive: False\n', + ' extended_real: False\n', + ' imaginary: False\n', + ' integer: False\n', + ' irrational: False\n', + ' negative: False\n', + ' noninteger: False\n', + ' nonnegative: False\n', + ' nonpositive: False\n', + ' nonzero: False\n', + ' odd: False\n', + ' positive: False\n', + ' prime: False\n', + ' rational: False\n', + ' real: False\n', + ' transcendental: False\n', + ' zero: False\n', + ' +-Symbol: B\n', + ' | commutative: True\n', + ' +-Integer: 3\n', + ' | algebraic: True\n', + ' | commutative: True\n', + ' | complex: True\n', + ' | extended_negative: False\n', + ' | extended_nonnegative: True\n', + ' | extended_real: True\n', + ' | finite: True\n', + ' | hermitian: True\n', + ' | imaginary: False\n', + ' | infinite: False\n', + ' | integer: True\n', + ' | irrational: False\n', + ' | negative: False\n', + ' | noninteger: False\n', + ' | nonnegative: True\n', + ' | rational: True\n', + ' | real: True\n', + ' | transcendental: False\n', + ' +-Integer: 3\n', + ' algebraic: True\n', + ' commutative: True\n', + ' complex: True\n', + ' extended_negative: False\n', + ' extended_nonnegative: True\n', + ' extended_real: True\n', + ' finite: True\n', + ' hermitian: True\n', + ' imaginary: False\n', + ' infinite: False\n', + ' integer: True\n', + ' irrational: False\n', + ' negative: False\n', + ' noninteger: False\n', + ' nonnegative: True\n', + ' rational: True\n', + ' real: True\n', + ' transcendental: False\n' + ] + + assert tree(A + B) == "".join(test_str) + + +def test_print_tree_MatAdd_noassumptions(): + from sympy.matrices.expressions import MatrixSymbol + A = MatrixSymbol('A', 3, 3) + B = MatrixSymbol('B', 3, 3) + + test_str = \ +"""MatAdd: A + B ++-MatrixSymbol: A +| +-Str: A +| +-Integer: 3 +| +-Integer: 3 ++-MatrixSymbol: B + +-Str: B + +-Integer: 3 + +-Integer: 3 +""" + + assert tree(A + B, assumptions=False) == test_str diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/theanocode.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/theanocode.py new file mode 100644 index 0000000000000000000000000000000000000000..dce908865d426dabede2b6749ad944e5a420e4cf --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/theanocode.py @@ -0,0 +1,571 @@ +""" +.. deprecated:: 1.8 + + ``sympy.printing.theanocode`` is deprecated. Theano has been renamed to + Aesara. Use ``sympy.printing.aesaracode`` instead. See + :ref:`theanocode-deprecated` for more information. + +""" +from __future__ import annotations +import math +from typing import Any + +from sympy.external import import_module +from sympy.printing.printer import Printer +from sympy.utilities.iterables import is_sequence +import sympy +from functools import partial + +from sympy.utilities.decorator import doctest_depends_on +from sympy.utilities.exceptions import sympy_deprecation_warning + + +__doctest_requires__ = {('theano_function',): ['theano']} + + +theano = import_module('theano') + + +if theano: + ts = theano.scalar + tt = theano.tensor + from theano.sandbox import linalg as tlinalg + + mapping = { + sympy.Add: tt.add, + sympy.Mul: tt.mul, + sympy.Abs: tt.abs_, + sympy.sign: tt.sgn, + sympy.ceiling: tt.ceil, + sympy.floor: tt.floor, + sympy.log: tt.log, + sympy.exp: tt.exp, + sympy.sqrt: tt.sqrt, + sympy.cos: tt.cos, + sympy.acos: tt.arccos, + sympy.sin: tt.sin, + sympy.asin: tt.arcsin, + sympy.tan: tt.tan, + sympy.atan: tt.arctan, + sympy.atan2: tt.arctan2, + sympy.cosh: tt.cosh, + sympy.acosh: tt.arccosh, + sympy.sinh: tt.sinh, + sympy.asinh: tt.arcsinh, + sympy.tanh: tt.tanh, + sympy.atanh: tt.arctanh, + sympy.re: tt.real, + sympy.im: tt.imag, + sympy.arg: tt.angle, + sympy.erf: tt.erf, + sympy.gamma: tt.gamma, + sympy.loggamma: tt.gammaln, + sympy.Pow: tt.pow, + sympy.Eq: tt.eq, + sympy.StrictGreaterThan: tt.gt, + sympy.StrictLessThan: tt.lt, + sympy.LessThan: tt.le, + sympy.GreaterThan: tt.ge, + sympy.And: tt.and_, + sympy.Or: tt.or_, + sympy.Max: tt.maximum, # SymPy accept >2 inputs, Theano only 2 + sympy.Min: tt.minimum, # SymPy accept >2 inputs, Theano only 2 + sympy.conjugate: tt.conj, + sympy.core.numbers.ImaginaryUnit: lambda:tt.complex(0,1), + # Matrices + sympy.MatAdd: tt.Elemwise(ts.add), + sympy.HadamardProduct: tt.Elemwise(ts.mul), + sympy.Trace: tlinalg.trace, + sympy.Determinant : tlinalg.det, + sympy.Inverse: tlinalg.matrix_inverse, + sympy.Transpose: tt.DimShuffle((False, False), [1, 0]), + } + + +class TheanoPrinter(Printer): + """ Code printer which creates Theano symbolic expression graphs. + + Parameters + ========== + + cache : dict + Cache dictionary to use. If None (default) will use + the global cache. To create a printer which does not depend on or alter + global state pass an empty dictionary. Note: the dictionary is not + copied on initialization of the printer and will be updated in-place, + so using the same dict object when creating multiple printers or making + multiple calls to :func:`.theano_code` or :func:`.theano_function` means + the cache is shared between all these applications. + + Attributes + ========== + + cache : dict + A cache of Theano variables which have been created for SymPy + symbol-like objects (e.g. :class:`sympy.core.symbol.Symbol` or + :class:`sympy.matrices.expressions.MatrixSymbol`). This is used to + ensure that all references to a given symbol in an expression (or + multiple expressions) are printed as the same Theano variable, which is + created only once. Symbols are differentiated only by name and type. The + format of the cache's contents should be considered opaque to the user. + """ + printmethod = "_theano" + + def __init__(self, *args, **kwargs): + self.cache = kwargs.pop('cache', {}) + super().__init__(*args, **kwargs) + + def _get_key(self, s, name=None, dtype=None, broadcastable=None): + """ Get the cache key for a SymPy object. + + Parameters + ========== + + s : sympy.core.basic.Basic + SymPy object to get key for. + + name : str + Name of object, if it does not have a ``name`` attribute. + """ + + if name is None: + name = s.name + + return (name, type(s), s.args, dtype, broadcastable) + + def _get_or_create(self, s, name=None, dtype=None, broadcastable=None): + """ + Get the Theano variable for a SymPy symbol from the cache, or create it + if it does not exist. + """ + + # Defaults + if name is None: + name = s.name + if dtype is None: + dtype = 'floatX' + if broadcastable is None: + broadcastable = () + + key = self._get_key(s, name, dtype=dtype, broadcastable=broadcastable) + + if key in self.cache: + return self.cache[key] + + value = tt.tensor(name=name, dtype=dtype, broadcastable=broadcastable) + self.cache[key] = value + return value + + def _print_Symbol(self, s, **kwargs): + dtype = kwargs.get('dtypes', {}).get(s) + bc = kwargs.get('broadcastables', {}).get(s) + return self._get_or_create(s, dtype=dtype, broadcastable=bc) + + def _print_AppliedUndef(self, s, **kwargs): + name = str(type(s)) + '_' + str(s.args[0]) + dtype = kwargs.get('dtypes', {}).get(s) + bc = kwargs.get('broadcastables', {}).get(s) + return self._get_or_create(s, name=name, dtype=dtype, broadcastable=bc) + + def _print_Basic(self, expr, **kwargs): + op = mapping[type(expr)] + children = [self._print(arg, **kwargs) for arg in expr.args] + return op(*children) + + def _print_Number(self, n, **kwargs): + # Integers already taken care of below, interpret as float + return float(n.evalf()) + + def _print_MatrixSymbol(self, X, **kwargs): + dtype = kwargs.get('dtypes', {}).get(X) + return self._get_or_create(X, dtype=dtype, broadcastable=(None, None)) + + def _print_DenseMatrix(self, X, **kwargs): + if not hasattr(tt, 'stacklists'): + raise NotImplementedError( + "Matrix translation not yet supported in this version of Theano") + + return tt.stacklists([ + [self._print(arg, **kwargs) for arg in L] + for L in X.tolist() + ]) + + _print_ImmutableMatrix = _print_ImmutableDenseMatrix = _print_DenseMatrix + + def _print_MatMul(self, expr, **kwargs): + children = [self._print(arg, **kwargs) for arg in expr.args] + result = children[0] + for child in children[1:]: + result = tt.dot(result, child) + return result + + def _print_MatPow(self, expr, **kwargs): + children = [self._print(arg, **kwargs) for arg in expr.args] + result = 1 + if isinstance(children[1], int) and children[1] > 0: + for i in range(children[1]): + result = tt.dot(result, children[0]) + else: + raise NotImplementedError('''Only non-negative integer + powers of matrices can be handled by Theano at the moment''') + return result + + def _print_MatrixSlice(self, expr, **kwargs): + parent = self._print(expr.parent, **kwargs) + rowslice = self._print(slice(*expr.rowslice), **kwargs) + colslice = self._print(slice(*expr.colslice), **kwargs) + return parent[rowslice, colslice] + + def _print_BlockMatrix(self, expr, **kwargs): + nrows, ncols = expr.blocks.shape + blocks = [[self._print(expr.blocks[r, c], **kwargs) + for c in range(ncols)] + for r in range(nrows)] + return tt.join(0, *[tt.join(1, *row) for row in blocks]) + + + def _print_slice(self, expr, **kwargs): + return slice(*[self._print(i, **kwargs) + if isinstance(i, sympy.Basic) else i + for i in (expr.start, expr.stop, expr.step)]) + + def _print_Pi(self, expr, **kwargs): + return math.pi + + def _print_Exp1(self, expr, **kwargs): + return ts.exp(1) + + def _print_Piecewise(self, expr, **kwargs): + import numpy as np + e, cond = expr.args[0].args # First condition and corresponding value + + # Print conditional expression and value for first condition + p_cond = self._print(cond, **kwargs) + p_e = self._print(e, **kwargs) + + # One condition only + if len(expr.args) == 1: + # Return value if condition else NaN + return tt.switch(p_cond, p_e, np.nan) + + # Return value_1 if condition_1 else evaluate remaining conditions + p_remaining = self._print(sympy.Piecewise(*expr.args[1:]), **kwargs) + return tt.switch(p_cond, p_e, p_remaining) + + def _print_Rational(self, expr, **kwargs): + return tt.true_div(self._print(expr.p, **kwargs), + self._print(expr.q, **kwargs)) + + def _print_Integer(self, expr, **kwargs): + return expr.p + + def _print_factorial(self, expr, **kwargs): + return self._print(sympy.gamma(expr.args[0] + 1), **kwargs) + + def _print_Derivative(self, deriv, **kwargs): + rv = self._print(deriv.expr, **kwargs) + for var in deriv.variables: + var = self._print(var, **kwargs) + rv = tt.Rop(rv, var, tt.ones_like(var)) + return rv + + def emptyPrinter(self, expr): + return expr + + def doprint(self, expr, dtypes=None, broadcastables=None): + """ Convert a SymPy expression to a Theano graph variable. + + The ``dtypes`` and ``broadcastables`` arguments are used to specify the + data type, dimension, and broadcasting behavior of the Theano variables + corresponding to the free symbols in ``expr``. Each is a mapping from + SymPy symbols to the value of the corresponding argument to + ``theano.tensor.Tensor``. + + See the corresponding `documentation page`__ for more information on + broadcasting in Theano. + + .. __: http://deeplearning.net/software/theano/tutorial/broadcasting.html + + Parameters + ========== + + expr : sympy.core.expr.Expr + SymPy expression to print. + + dtypes : dict + Mapping from SymPy symbols to Theano datatypes to use when creating + new Theano variables for those symbols. Corresponds to the ``dtype`` + argument to ``theano.tensor.Tensor``. Defaults to ``'floatX'`` + for symbols not included in the mapping. + + broadcastables : dict + Mapping from SymPy symbols to the value of the ``broadcastable`` + argument to ``theano.tensor.Tensor`` to use when creating Theano + variables for those symbols. Defaults to the empty tuple for symbols + not included in the mapping (resulting in a scalar). + + Returns + ======= + + theano.gof.graph.Variable + A variable corresponding to the expression's value in a Theano + symbolic expression graph. + + """ + if dtypes is None: + dtypes = {} + if broadcastables is None: + broadcastables = {} + + return self._print(expr, dtypes=dtypes, broadcastables=broadcastables) + + +global_cache: dict[Any, Any] = {} + + +def theano_code(expr, cache=None, **kwargs): + """ + Convert a SymPy expression into a Theano graph variable. + + .. deprecated:: 1.8 + + ``sympy.printing.theanocode`` is deprecated. Theano has been renamed to + Aesara. Use ``sympy.printing.aesaracode`` instead. See + :ref:`theanocode-deprecated` for more information. + + Parameters + ========== + + expr : sympy.core.expr.Expr + SymPy expression object to convert. + + cache : dict + Cached Theano variables (see :class:`TheanoPrinter.cache + `). Defaults to the module-level global cache. + + dtypes : dict + Passed to :meth:`.TheanoPrinter.doprint`. + + broadcastables : dict + Passed to :meth:`.TheanoPrinter.doprint`. + + Returns + ======= + + theano.gof.graph.Variable + A variable corresponding to the expression's value in a Theano symbolic + expression graph. + + """ + sympy_deprecation_warning( + """ + sympy.printing.theanocode is deprecated. Theano has been renamed to + Aesara. Use sympy.printing.aesaracode instead.""", + deprecated_since_version="1.8", + active_deprecations_target='theanocode-deprecated') + + if not theano: + raise ImportError("theano is required for theano_code") + + if cache is None: + cache = global_cache + + return TheanoPrinter(cache=cache, settings={}).doprint(expr, **kwargs) + + +def dim_handling(inputs, dim=None, dims=None, broadcastables=None): + r""" + Get value of ``broadcastables`` argument to :func:`.theano_code` from + keyword arguments to :func:`.theano_function`. + + Included for backwards compatibility. + + Parameters + ========== + + inputs + Sequence of input symbols. + + dim : int + Common number of dimensions for all inputs. Overrides other arguments + if given. + + dims : dict + Mapping from input symbols to number of dimensions. Overrides + ``broadcastables`` argument if given. + + broadcastables : dict + Explicit value of ``broadcastables`` argument to + :meth:`.TheanoPrinter.doprint`. If not None function will return this value unchanged. + + Returns + ======= + dict + Dictionary mapping elements of ``inputs`` to their "broadcastable" + values (tuple of ``bool``\ s). + """ + if dim is not None: + return dict.fromkeys(inputs, (False,) * dim) + + if dims is not None: + maxdim = max(dims.values()) + return { + s: (False,) * d + (True,) * (maxdim - d) + for s, d in dims.items() + } + + if broadcastables is not None: + return broadcastables + + return {} + + +@doctest_depends_on(modules=('theano',)) +def theano_function(inputs, outputs, scalar=False, *, + dim=None, dims=None, broadcastables=None, **kwargs): + """ + Create a Theano function from SymPy expressions. + + .. deprecated:: 1.8 + + ``sympy.printing.theanocode`` is deprecated. Theano has been renamed to + Aesara. Use ``sympy.printing.aesaracode`` instead. See + :ref:`theanocode-deprecated` for more information. + + The inputs and outputs are converted to Theano variables using + :func:`.theano_code` and then passed to ``theano.function``. + + Parameters + ========== + + inputs + Sequence of symbols which constitute the inputs of the function. + + outputs + Sequence of expressions which constitute the outputs(s) of the + function. The free symbols of each expression must be a subset of + ``inputs``. + + scalar : bool + Convert 0-dimensional arrays in output to scalars. This will return a + Python wrapper function around the Theano function object. + + cache : dict + Cached Theano variables (see :class:`TheanoPrinter.cache + `). Defaults to the module-level global cache. + + dtypes : dict + Passed to :meth:`.TheanoPrinter.doprint`. + + broadcastables : dict + Passed to :meth:`.TheanoPrinter.doprint`. + + dims : dict + Alternative to ``broadcastables`` argument. Mapping from elements of + ``inputs`` to integers indicating the dimension of their associated + arrays/tensors. Overrides ``broadcastables`` argument if given. + + dim : int + Another alternative to the ``broadcastables`` argument. Common number of + dimensions to use for all arrays/tensors. + ``theano_function([x, y], [...], dim=2)`` is equivalent to using + ``broadcastables={x: (False, False), y: (False, False)}``. + + Returns + ======= + callable + A callable object which takes values of ``inputs`` as positional + arguments and returns an output array for each of the expressions + in ``outputs``. If ``outputs`` is a single expression the function will + return a Numpy array, if it is a list of multiple expressions the + function will return a list of arrays. See description of the ``squeeze`` + argument above for the behavior when a single output is passed in a list. + The returned object will either be an instance of + ``theano.compile.function_module.Function`` or a Python wrapper + function around one. In both cases, the returned value will have a + ``theano_function`` attribute which points to the return value of + ``theano.function``. + + Examples + ======== + + >>> from sympy.abc import x, y, z + >>> from sympy.printing.theanocode import theano_function + + A simple function with one input and one output: + + >>> f1 = theano_function([x], [x**2 - 1], scalar=True) + >>> f1(3) + 8.0 + + A function with multiple inputs and one output: + + >>> f2 = theano_function([x, y, z], [(x**z + y**z)**(1/z)], scalar=True) + >>> f2(3, 4, 2) + 5.0 + + A function with multiple inputs and multiple outputs: + + >>> f3 = theano_function([x, y], [x**2 + y**2, x**2 - y**2], scalar=True) + >>> f3(2, 3) + [13.0, -5.0] + + See also + ======== + + dim_handling + + """ + sympy_deprecation_warning( + """ + sympy.printing.theanocode is deprecated. Theano has been renamed to Aesara. Use sympy.printing.aesaracode instead""", + deprecated_since_version="1.8", + active_deprecations_target='theanocode-deprecated') + + if not theano: + raise ImportError("theano is required for theano_function") + + # Pop off non-theano keyword args + cache = kwargs.pop('cache', {}) + dtypes = kwargs.pop('dtypes', {}) + + broadcastables = dim_handling( + inputs, dim=dim, dims=dims, broadcastables=broadcastables, + ) + + # Print inputs/outputs + code = partial(theano_code, cache=cache, dtypes=dtypes, + broadcastables=broadcastables) + tinputs = list(map(code, inputs)) + toutputs = list(map(code, outputs)) + + #fix constant expressions as variables + toutputs = [output if isinstance(output, theano.Variable) else tt.as_tensor_variable(output) for output in toutputs] + + if len(toutputs) == 1: + toutputs = toutputs[0] + + # Compile theano func + func = theano.function(tinputs, toutputs, **kwargs) + + is_0d = [len(o.variable.broadcastable) == 0 for o in func.outputs] + + # No wrapper required + if not scalar or not any(is_0d): + func.theano_function = func + return func + + # Create wrapper to convert 0-dimensional outputs to scalars + def wrapper(*args): + out = func(*args) + # out can be array(1.0) or [array(1.0), array(2.0)] + + if is_sequence(out): + return [o[()] if is_0d[i] else o for i, o in enumerate(out)] + else: + return out[()] + + wrapper.__wrapped__ = func + wrapper.__doc__ = func.__doc__ + wrapper.theano_function = func + return wrapper diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tree.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tree.py new file mode 100644 index 0000000000000000000000000000000000000000..82dac013419fbe93f63dcf5b90b3a529d72a32bc --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/printing/tree.py @@ -0,0 +1,175 @@ +def pprint_nodes(subtrees): + """ + Prettyprints systems of nodes. + + Examples + ======== + + >>> from sympy.printing.tree import pprint_nodes + >>> print(pprint_nodes(["a", "b1\\nb2", "c"])) + +-a + +-b1 + | b2 + +-c + + """ + def indent(s, type=1): + x = s.split("\n") + r = "+-%s\n" % x[0] + for a in x[1:]: + if a == "": + continue + if type == 1: + r += "| %s\n" % a + else: + r += " %s\n" % a + return r + if not subtrees: + return "" + f = "" + for a in subtrees[:-1]: + f += indent(a) + f += indent(subtrees[-1], 2) + return f + + +def print_node(node, assumptions=True): + """ + Returns information about the "node". + + This includes class name, string representation and assumptions. + + Parameters + ========== + + assumptions : bool, optional + See the ``assumptions`` keyword in ``tree`` + """ + s = "%s: %s\n" % (node.__class__.__name__, str(node)) + + if assumptions: + d = node._assumptions + else: + d = None + + if d: + for a in sorted(d): + v = d[a] + if v is None: + continue + s += "%s: %s\n" % (a, v) + + return s + + +def tree(node, assumptions=True): + """ + Returns a tree representation of "node" as a string. + + It uses print_node() together with pprint_nodes() on node.args recursively. + + Parameters + ========== + + assumptions : bool, optional + The flag to decide whether to print out all the assumption data + (such as ``is_integer`, ``is_real``) associated with the + expression or not. + + Enabling the flag makes the result verbose, and the printed + result may not be deterministic because of the randomness used + in backtracing the assumptions. + + See Also + ======== + + print_tree + + """ + subtrees = [] + for arg in node.args: + subtrees.append(tree(arg, assumptions=assumptions)) + s = print_node(node, assumptions=assumptions) + pprint_nodes(subtrees) + return s + + +def print_tree(node, assumptions=True): + """ + Prints a tree representation of "node". + + Parameters + ========== + + assumptions : bool, optional + The flag to decide whether to print out all the assumption data + (such as ``is_integer`, ``is_real``) associated with the + expression or not. + + Enabling the flag makes the result verbose, and the printed + result may not be deterministic because of the randomness used + in backtracing the assumptions. + + Examples + ======== + + >>> from sympy.printing import print_tree + >>> from sympy import Symbol + >>> x = Symbol('x', odd=True) + >>> y = Symbol('y', even=True) + + Printing with full assumptions information: + + >>> print_tree(y**x) + Pow: y**x + +-Symbol: y + | algebraic: True + | commutative: True + | complex: True + | even: True + | extended_real: True + | finite: True + | hermitian: True + | imaginary: False + | infinite: False + | integer: True + | irrational: False + | noninteger: False + | odd: False + | rational: True + | real: True + | transcendental: False + +-Symbol: x + algebraic: True + commutative: True + complex: True + even: False + extended_nonzero: True + extended_real: True + finite: True + hermitian: True + imaginary: False + infinite: False + integer: True + irrational: False + noninteger: False + nonzero: True + odd: True + rational: True + real: True + transcendental: False + zero: False + + Hiding the assumptions: + + >>> print_tree(y**x, assumptions=False) + Pow: y**x + +-Symbol: y + +-Symbol: x + + See Also + ======== + + tree + + """ + print(tree(node, assumptions=assumptions)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/release.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/release.py new file mode 100644 index 0000000000000000000000000000000000000000..b9f68edb2f571e7bfaad9472d36a2d357b5574b2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/release.py @@ -0,0 +1 @@ +__version__ = "1.14.0" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sandbox/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sandbox/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3a84b7517819bb2fc9886274e09d955a74cabca1 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sandbox/__init__.py @@ -0,0 +1,8 @@ +""" +Sandbox module of SymPy. + +This module contains experimental code, use at your own risk! + +There is no warranty that this code will still be located here in future +versions of SymPy. +""" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sandbox/indexed_integrals.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sandbox/indexed_integrals.py new file mode 100644 index 0000000000000000000000000000000000000000..c0c17d141448b5a71cb814bff76a710a5bd43f88 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sandbox/indexed_integrals.py @@ -0,0 +1,72 @@ +from sympy.tensor import Indexed +from sympy.core.containers import Tuple +from sympy.core.symbol import Dummy +from sympy.core.sympify import sympify +from sympy.integrals.integrals import Integral + + +class IndexedIntegral(Integral): + """ + Experimental class to test integration by indexed variables. + + Usage is analogue to ``Integral``, it simply adds awareness of + integration over indices. + + Contraction of non-identical index symbols referring to the same + ``IndexedBase`` is not yet supported. + + Examples + ======== + + >>> from sympy.sandbox.indexed_integrals import IndexedIntegral + >>> from sympy import IndexedBase, symbols + >>> A = IndexedBase('A') + >>> i, j = symbols('i j', integer=True) + >>> ii = IndexedIntegral(A[i], A[i]) + >>> ii + Integral(_A[i], _A[i]) + >>> ii.doit() + A[i]**2/2 + + If the indices are different, indexed objects are considered to be + different variables: + + >>> i2 = IndexedIntegral(A[j], A[i]) + >>> i2 + Integral(A[j], _A[i]) + >>> i2.doit() + A[i]*A[j] + """ + + def __new__(cls, function, *limits, **assumptions): + repl, limits = IndexedIntegral._indexed_process_limits(limits) + function = sympify(function) + function = function.xreplace(repl) + obj = Integral.__new__(cls, function, *limits, **assumptions) + obj._indexed_repl = repl + obj._indexed_reverse_repl = {val: key for key, val in repl.items()} + return obj + + def doit(self): + res = super().doit() + return res.xreplace(self._indexed_reverse_repl) + + @staticmethod + def _indexed_process_limits(limits): + repl = {} + newlimits = [] + for i in limits: + if isinstance(i, (tuple, list, Tuple)): + v = i[0] + vrest = i[1:] + else: + v = i + vrest = () + if isinstance(v, Indexed): + if v not in repl: + r = Dummy(str(v)) + repl[v] = r + newlimits.append((r,)+vrest) + else: + newlimits.append(i) + return repl, newlimits diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sandbox/tests/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sandbox/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sandbox/tests/test_indexed_integrals.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sandbox/tests/test_indexed_integrals.py new file mode 100644 index 0000000000000000000000000000000000000000..61b98f0ffec29e026f6dfe8e16fde8b5818b0b09 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sandbox/tests/test_indexed_integrals.py @@ -0,0 +1,25 @@ +from sympy.sandbox.indexed_integrals import IndexedIntegral +from sympy.core.symbol import symbols +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.tensor.indexed import (Idx, IndexedBase) + + +def test_indexed_integrals(): + A = IndexedBase('A') + i, j = symbols('i j', integer=True) + a1, a2 = symbols('a1:3', cls=Idx) + assert isinstance(a1, Idx) + + assert IndexedIntegral(1, A[i]).doit() == A[i] + assert IndexedIntegral(A[i], A[i]).doit() == A[i] ** 2 / 2 + assert IndexedIntegral(A[j], A[i]).doit() == A[i] * A[j] + assert IndexedIntegral(A[i] * A[j], A[i]).doit() == A[i] ** 2 * A[j] / 2 + assert IndexedIntegral(sin(A[i]), A[i]).doit() == -cos(A[i]) + assert IndexedIntegral(sin(A[j]), A[i]).doit() == sin(A[j]) * A[i] + + assert IndexedIntegral(1, A[a1]).doit() == A[a1] + assert IndexedIntegral(A[a1], A[a1]).doit() == A[a1] ** 2 / 2 + assert IndexedIntegral(A[a2], A[a1]).doit() == A[a1] * A[a2] + assert IndexedIntegral(A[a1] * A[a2], A[a1]).doit() == A[a1] ** 2 * A[a2] / 2 + assert IndexedIntegral(sin(A[a1]), A[a1]).doit() == -cos(A[a1]) + assert IndexedIntegral(sin(A[a2]), A[a1]).doit() == sin(A[a2]) * A[a1] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..953653e21856b82bc0b708ccd922efb728a084ed --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/__init__.py @@ -0,0 +1,23 @@ +"""A module that handles series: find a limit, order the series etc. +""" +from .order import Order +from .limits import limit, Limit +from .gruntz import gruntz +from .series import series +from .approximants import approximants +from .residues import residue +from .sequences import SeqPer, SeqFormula, sequence, SeqAdd, SeqMul +from .fourier import fourier_series +from .formal import fps +from .limitseq import difference_delta, limit_seq + +from sympy.core.singleton import S +EmptySequence = S.EmptySequence + +O = Order + +__all__ = ['Order', 'O', 'limit', 'Limit', 'gruntz', 'series', 'approximants', + 'residue', 'EmptySequence', 'SeqPer', 'SeqFormula', 'sequence', + 'SeqAdd', 'SeqMul', 'fourier_series', 'fps', 'difference_delta', + 'limit_seq' + ] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/acceleration.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/acceleration.py new file mode 100644 index 0000000000000000000000000000000000000000..e2c7c1629a4b0d52e2aa33bd415886dfed515693 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/acceleration.py @@ -0,0 +1,101 @@ +""" +Convergence acceleration / extrapolation methods for series and +sequences. + +References: +Carl M. Bender & Steven A. Orszag, "Advanced Mathematical Methods for +Scientists and Engineers: Asymptotic Methods and Perturbation Theory", +Springer 1999. (Shanks transformation: pp. 368-375, Richardson +extrapolation: pp. 375-377.) +""" + +from sympy.core.numbers import Integer +from sympy.core.singleton import S +from sympy.functions.combinatorial.factorials import factorial + + +def richardson(A, k, n, N): + """ + Calculate an approximation for lim k->oo A(k) using Richardson + extrapolation with the terms A(n), A(n+1), ..., A(n+N+1). + Choosing N ~= 2*n often gives good results. + + Examples + ======== + + A simple example is to calculate exp(1) using the limit definition. + This limit converges slowly; n = 100 only produces two accurate + digits: + + >>> from sympy.abc import n + >>> e = (1 + 1/n)**n + >>> print(round(e.subs(n, 100).evalf(), 10)) + 2.7048138294 + + Richardson extrapolation with 11 appropriately chosen terms gives + a value that is accurate to the indicated precision: + + >>> from sympy import E + >>> from sympy.series.acceleration import richardson + >>> print(round(richardson(e, n, 10, 20).evalf(), 10)) + 2.7182818285 + >>> print(round(E.evalf(), 10)) + 2.7182818285 + + Another useful application is to speed up convergence of series. + Computing 100 terms of the zeta(2) series 1/k**2 yields only + two accurate digits: + + >>> from sympy.abc import k, n + >>> from sympy import Sum + >>> A = Sum(k**-2, (k, 1, n)) + >>> print(round(A.subs(n, 100).evalf(), 10)) + 1.6349839002 + + Richardson extrapolation performs much better: + + >>> from sympy import pi + >>> print(round(richardson(A, n, 10, 20).evalf(), 10)) + 1.6449340668 + >>> print(round(((pi**2)/6).evalf(), 10)) # Exact value + 1.6449340668 + + """ + s = S.Zero + for j in range(0, N + 1): + s += (A.subs(k, Integer(n + j)).doit() * (n + j)**N * + S.NegativeOne**(j + N) / (factorial(j) * factorial(N - j))) + return s + + +def shanks(A, k, n, m=1): + """ + Calculate an approximation for lim k->oo A(k) using the n-term Shanks + transformation S(A)(n). With m > 1, calculate the m-fold recursive + Shanks transformation S(S(...S(A)...))(n). + + The Shanks transformation is useful for summing Taylor series that + converge slowly near a pole or singularity, e.g. for log(2): + + >>> from sympy.abc import k, n + >>> from sympy import Sum, Integer + >>> from sympy.series.acceleration import shanks + >>> A = Sum(Integer(-1)**(k+1) / k, (k, 1, n)) + >>> print(round(A.subs(n, 100).doit().evalf(), 10)) + 0.6881721793 + >>> print(round(shanks(A, n, 25).evalf(), 10)) + 0.6931396564 + >>> print(round(shanks(A, n, 25, 5).evalf(), 10)) + 0.6931471806 + + The correct value is 0.6931471805599453094172321215. + """ + table = [A.subs(k, Integer(j)).doit() for j in range(n + m + 2)] + table2 = table.copy() + + for i in range(1, m + 1): + for j in range(i, n + m + 1): + x, y, z = table[j - 1], table[j], table[j + 1] + table2[j] = (z*x - y**2) / (z + x - 2*y) + table = table2.copy() + return table[n] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/approximants.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/approximants.py new file mode 100644 index 0000000000000000000000000000000000000000..3d54ce41bc7367606ae6260f8e9ac00149cedc0f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/approximants.py @@ -0,0 +1,103 @@ +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.polys.polytools import lcm +from sympy.utilities import public + +@public +def approximants(l, X=Symbol('x'), simplify=False): + """ + Return a generator for consecutive Pade approximants for a series. + It can also be used for computing the rational generating function of a + series when possible, since the last approximant returned by the generator + will be the generating function (if any). + + Explanation + =========== + + The input list can contain more complex expressions than integer or rational + numbers; symbols may also be involved in the computation. An example below + show how to compute the generating function of the whole Pascal triangle. + + The generator can be asked to apply the sympy.simplify function on each + generated term, which will make the computation slower; however it may be + useful when symbols are involved in the expressions. + + Examples + ======== + + >>> from sympy.series import approximants + >>> from sympy import lucas, fibonacci, symbols, binomial + >>> g = [lucas(k) for k in range(16)] + >>> [e for e in approximants(g)] + [2, -4/(x - 2), (5*x - 2)/(3*x - 1), (x - 2)/(x**2 + x - 1)] + + >>> h = [fibonacci(k) for k in range(16)] + >>> [e for e in approximants(h)] + [x, -x/(x - 1), (x**2 - x)/(2*x - 1), -x/(x**2 + x - 1)] + + >>> x, t = symbols("x,t") + >>> p=[sum(binomial(k,i)*x**i for i in range(k+1)) for k in range(16)] + >>> y = approximants(p, t) + >>> for k in range(3): print(next(y)) + 1 + (x + 1)/((-x - 1)*(t*(x + 1) + (x + 1)/(-x - 1))) + nan + + >>> y = approximants(p, t, simplify=True) + >>> for k in range(3): print(next(y)) + 1 + -1/(t*(x + 1) - 1) + nan + + See Also + ======== + + sympy.concrete.guess.guess_generating_function_rational + mpmath.pade + """ + from sympy.simplify import simplify as simp + from sympy.simplify.radsimp import denom + p1, q1 = [S.One], [S.Zero] + p2, q2 = [S.Zero], [S.One] + while len(l): + b = 0 + while l[b]==0: + b += 1 + if b == len(l): + return + m = [S.One/l[b]] + for k in range(b+1, len(l)): + s = 0 + for j in range(b, k): + s -= l[j+1] * m[b-j-1] + m.append(s/l[b]) + l = m + a, l[0] = l[0], 0 + p = [0] * max(len(p2), b+len(p1)) + q = [0] * max(len(q2), b+len(q1)) + for k in range(len(p2)): + p[k] = a*p2[k] + for k in range(b, b+len(p1)): + p[k] += p1[k-b] + for k in range(len(q2)): + q[k] = a*q2[k] + for k in range(b, b+len(q1)): + q[k] += q1[k-b] + while p[-1]==0: p.pop() + while q[-1]==0: q.pop() + p1, p2 = p2, p + q1, q2 = q2, q + + # yield result + c = 1 + for x in p: + c = lcm(c, denom(x)) + for x in q: + c = lcm(c, denom(x)) + out = ( sum(c*e*X**k for k, e in enumerate(p)) + / sum(c*e*X**k for k, e in enumerate(q)) ) + if simplify: + yield(simp(out)) + else: + yield out + return diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/aseries.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/aseries.py new file mode 100644 index 0000000000000000000000000000000000000000..dbbe0664e6d43a9329f37789c16c48143eda5413 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/aseries.py @@ -0,0 +1,10 @@ +from sympy.core.sympify import sympify + + +def aseries(expr, x=None, n=6, bound=0, hir=False): + """ + See the docstring of Expr.aseries() for complete details of this wrapper. + + """ + expr = sympify(expr) + return expr.aseries(x, n, bound, hir) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/benchmarks/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/benchmarks/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/benchmarks/bench_limit.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/benchmarks/bench_limit.py new file mode 100644 index 0000000000000000000000000000000000000000..eafc28328848dad4b3ea433537971f5785253afe --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/benchmarks/bench_limit.py @@ -0,0 +1,9 @@ +from sympy.core.numbers import oo +from sympy.core.symbol import Symbol +from sympy.series.limits import limit + +x = Symbol('x') + + +def timeit_limit_1x(): + limit(1/x, x, oo) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/benchmarks/bench_order.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/benchmarks/bench_order.py new file mode 100644 index 0000000000000000000000000000000000000000..1c85fa173dfc2a478792de8ab816c23ba9d408ef --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/benchmarks/bench_order.py @@ -0,0 +1,10 @@ +from sympy.core.add import Add +from sympy.core.symbol import Symbol +from sympy.series.order import O + +x = Symbol('x') +l = [x**i for i in range(1000)] +l.append(O(x**1001)) + +def timeit_order_1x(): + Add(*l) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/formal.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/formal.py new file mode 100644 index 0000000000000000000000000000000000000000..ada591e03dd607daf8f8a5da1cf38cf283a3ed86 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/formal.py @@ -0,0 +1,1863 @@ +"""Formal Power Series""" + +from collections import defaultdict + +from sympy.core.numbers import (nan, oo, zoo) +from sympy.core.add import Add +from sympy.core.expr import Expr +from sympy.core.function import Derivative, Function, expand +from sympy.core.mul import Mul +from sympy.core.numbers import Rational +from sympy.core.relational import Eq +from sympy.sets.sets import Interval +from sympy.core.singleton import S +from sympy.core.symbol import Wild, Dummy, symbols, Symbol +from sympy.core.sympify import sympify +from sympy.discrete.convolutions import convolution +from sympy.functions.combinatorial.factorials import binomial, factorial, rf +from sympy.functions.combinatorial.numbers import bell +from sympy.functions.elementary.integers import floor, frac, ceiling +from sympy.functions.elementary.miscellaneous import Min, Max +from sympy.functions.elementary.piecewise import Piecewise +from sympy.series.limits import Limit +from sympy.series.order import Order +from sympy.series.sequences import sequence +from sympy.series.series_class import SeriesBase +from sympy.utilities.iterables import iterable + + + +def rational_algorithm(f, x, k, order=4, full=False): + """ + Rational algorithm for computing + formula of coefficients of Formal Power Series + of a function. + + Explanation + =========== + + Applicable when f(x) or some derivative of f(x) + is a rational function in x. + + :func:`rational_algorithm` uses :func:`~.apart` function for partial fraction + decomposition. :func:`~.apart` by default uses 'undetermined coefficients + method'. By setting ``full=True``, 'Bronstein's algorithm' can be used + instead. + + Looks for derivative of a function up to 4'th order (by default). + This can be overridden using order option. + + Parameters + ========== + + x : Symbol + order : int, optional + Order of the derivative of ``f``, Default is 4. + full : bool + + Returns + ======= + + formula : Expr + ind : Expr + Independent terms. + order : int + full : bool + + Examples + ======== + + >>> from sympy import log, atan + >>> from sympy.series.formal import rational_algorithm as ra + >>> from sympy.abc import x, k + + >>> ra(1 / (1 - x), x, k) + (1, 0, 0) + >>> ra(log(1 + x), x, k) + (-1/((-1)**k*k), 0, 1) + + >>> ra(atan(x), x, k, full=True) + ((-I/(2*(-I)**k) + I/(2*I**k))/k, 0, 1) + + Notes + ===== + + By setting ``full=True``, range of admissible functions to be solved using + ``rational_algorithm`` can be increased. This option should be used + carefully as it can significantly slow down the computation as ``doit`` is + performed on the :class:`~.RootSum` object returned by the :func:`~.apart` + function. Use ``full=False`` whenever possible. + + See Also + ======== + + sympy.polys.partfrac.apart + + References + ========== + + .. [1] Formal Power Series - Dominik Gruntz, Wolfram Koepf + .. [2] Power Series in Computer Algebra - Wolfram Koepf + + """ + from sympy.polys import RootSum, apart + from sympy.integrals import integrate + + diff = f + ds = [] # list of diff + + for i in range(order + 1): + if i: + diff = diff.diff(x) + + if diff.is_rational_function(x): + coeff, sep = S.Zero, S.Zero + + terms = apart(diff, x, full=full) + if terms.has(RootSum): + terms = terms.doit() + + for t in Add.make_args(terms): + num, den = t.as_numer_denom() + if not den.has(x): + sep += t + else: + if isinstance(den, Mul): + # m*(n*x - a)**j -> (n*x - a)**j + ind = den.as_independent(x) + den = ind[1] + num /= ind[0] + + # (n*x - a)**j -> (x - b) + den, j = den.as_base_exp() + a, xterm = den.as_coeff_add(x) + + # term -> m/x**n + if not a: + sep += t + continue + + xc = xterm[0].coeff(x) + a /= -xc + num /= xc**j + + ak = ((-1)**j * num * + binomial(j + k - 1, k).rewrite(factorial) / + a**(j + k)) + coeff += ak + + # Hacky, better way? + if coeff.is_zero: + return None + if (coeff.has(x) or coeff.has(zoo) or coeff.has(oo) or + coeff.has(nan)): + return None + + for j in range(i): + coeff = (coeff / (k + j + 1)) + sep = integrate(sep, x) + sep += (ds.pop() - sep).limit(x, 0) # constant of integration + return (coeff.subs(k, k - i), sep, i) + + else: + ds.append(diff) + + return None + + +def rational_independent(terms, x): + """ + Returns a list of all the rationally independent terms. + + Examples + ======== + + >>> from sympy import sin, cos + >>> from sympy.series.formal import rational_independent + >>> from sympy.abc import x + + >>> rational_independent([cos(x), sin(x)], x) + [cos(x), sin(x)] + >>> rational_independent([x**2, sin(x), x*sin(x), x**3], x) + [x**3 + x**2, x*sin(x) + sin(x)] + """ + if not terms: + return [] + + ind = terms[0:1] + + for t in terms[1:]: + n = t.as_independent(x)[1] + for i, term in enumerate(ind): + d = term.as_independent(x)[1] + q = (n / d).cancel() + if q.is_rational_function(x): + ind[i] += t + break + else: + ind.append(t) + return ind + + +def simpleDE(f, x, g, order=4): + r""" + Generates simple DE. + + Explanation + =========== + + DE is of the form + + .. math:: + f^k(x) + \sum\limits_{j=0}^{k-1} A_j f^j(x) = 0 + + where :math:`A_j` should be rational function in x. + + Generates DE's upto order 4 (default). DE's can also have free parameters. + + By increasing order, higher order DE's can be found. + + Yields a tuple of (DE, order). + """ + from sympy.solvers.solveset import linsolve + + a = symbols('a:%d' % (order)) + + def _makeDE(k): + eq = f.diff(x, k) + Add(*[a[i]*f.diff(x, i) for i in range(0, k)]) + DE = g(x).diff(x, k) + Add(*[a[i]*g(x).diff(x, i) for i in range(0, k)]) + return eq, DE + + found = False + for k in range(1, order + 1): + eq, DE = _makeDE(k) + eq = eq.expand() + terms = eq.as_ordered_terms() + ind = rational_independent(terms, x) + if found or len(ind) == k: + sol = dict(zip(a, (i for s in linsolve(ind, a[:k]) for i in s))) + if sol: + found = True + DE = DE.subs(sol) + DE = DE.as_numer_denom()[0] + DE = DE.factor().as_coeff_mul(Derivative)[1][0] + yield DE.collect(Derivative(g(x))), k + + +def exp_re(DE, r, k): + """Converts a DE with constant coefficients (explike) into a RE. + + Explanation + =========== + + Performs the substitution: + + .. math:: + f^j(x) \\to r(k + j) + + Normalises the terms so that lowest order of a term is always r(k). + + Examples + ======== + + >>> from sympy import Function, Derivative + >>> from sympy.series.formal import exp_re + >>> from sympy.abc import x, k + >>> f, r = Function('f'), Function('r') + + >>> exp_re(-f(x) + Derivative(f(x)), r, k) + -r(k) + r(k + 1) + >>> exp_re(Derivative(f(x), x) + Derivative(f(x), (x, 2)), r, k) + r(k) + r(k + 1) + + See Also + ======== + + sympy.series.formal.hyper_re + """ + RE = S.Zero + + g = DE.atoms(Function).pop() + + mini = None + for t in Add.make_args(DE): + coeff, d = t.as_independent(g) + if isinstance(d, Derivative): + j = d.derivative_count + else: + j = 0 + if mini is None or j < mini: + mini = j + RE += coeff * r(k + j) + if mini: + RE = RE.subs(k, k - mini) + return RE + + +def hyper_re(DE, r, k): + """ + Converts a DE into a RE. + + Explanation + =========== + + Performs the substitution: + + .. math:: + x^l f^j(x) \\to (k + 1 - l)_j . a_{k + j - l} + + Normalises the terms so that lowest order of a term is always r(k). + + Examples + ======== + + >>> from sympy import Function, Derivative + >>> from sympy.series.formal import hyper_re + >>> from sympy.abc import x, k + >>> f, r = Function('f'), Function('r') + + >>> hyper_re(-f(x) + Derivative(f(x)), r, k) + (k + 1)*r(k + 1) - r(k) + >>> hyper_re(-x*f(x) + Derivative(f(x), (x, 2)), r, k) + (k + 2)*(k + 3)*r(k + 3) - r(k) + + See Also + ======== + + sympy.series.formal.exp_re + """ + RE = S.Zero + + g = DE.atoms(Function).pop() + x = g.atoms(Symbol).pop() + + mini = None + for t in Add.make_args(DE.expand()): + coeff, d = t.as_independent(g) + c, v = coeff.as_independent(x) + l = v.as_coeff_exponent(x)[1] + if isinstance(d, Derivative): + j = d.derivative_count + else: + j = 0 + RE += c * rf(k + 1 - l, j) * r(k + j - l) + if mini is None or j - l < mini: + mini = j - l + + RE = RE.subs(k, k - mini) + + m = Wild('m') + return RE.collect(r(k + m)) + + +def _transformation_a(f, x, P, Q, k, m, shift): + f *= x**(-shift) + P = P.subs(k, k + shift) + Q = Q.subs(k, k + shift) + return f, P, Q, m + + +def _transformation_c(f, x, P, Q, k, m, scale): + f = f.subs(x, x**scale) + P = P.subs(k, k / scale) + Q = Q.subs(k, k / scale) + m *= scale + return f, P, Q, m + + +def _transformation_e(f, x, P, Q, k, m): + f = f.diff(x) + P = P.subs(k, k + 1) * (k + m + 1) + Q = Q.subs(k, k + 1) * (k + 1) + return f, P, Q, m + + +def _apply_shift(sol, shift): + return [(res, cond + shift) for res, cond in sol] + + +def _apply_scale(sol, scale): + return [(res, cond / scale) for res, cond in sol] + + +def _apply_integrate(sol, x, k): + return [(res / ((cond + 1)*(cond.as_coeff_Add()[1].coeff(k))), cond + 1) + for res, cond in sol] + + +def _compute_formula(f, x, P, Q, k, m, k_max): + """Computes the formula for f.""" + from sympy.polys import roots + + sol = [] + for i in range(k_max + 1, k_max + m + 1): + if (i < 0) == True: + continue + r = f.diff(x, i).limit(x, 0) / factorial(i) + if r.is_zero: + continue + + kterm = m*k + i + res = r + + p = P.subs(k, kterm) + q = Q.subs(k, kterm) + c1 = p.subs(k, 1/k).leadterm(k)[0] + c2 = q.subs(k, 1/k).leadterm(k)[0] + res *= (-c1 / c2)**k + + res *= Mul(*[rf(-r, k)**mul for r, mul in roots(p, k).items()]) + res /= Mul(*[rf(-r, k)**mul for r, mul in roots(q, k).items()]) + + sol.append((res, kterm)) + + return sol + + +def _rsolve_hypergeometric(f, x, P, Q, k, m): + """ + Recursive wrapper to rsolve_hypergeometric. + + Explanation + =========== + + Returns a Tuple of (formula, series independent terms, + maximum power of x in independent terms) if successful + otherwise ``None``. + + See :func:`rsolve_hypergeometric` for details. + """ + from sympy.polys import lcm, roots + from sympy.integrals import integrate + + # transformation - c + proots, qroots = roots(P, k), roots(Q, k) + all_roots = dict(proots) + all_roots.update(qroots) + scale = lcm([r.as_numer_denom()[1] for r, t in all_roots.items() + if r.is_rational]) + f, P, Q, m = _transformation_c(f, x, P, Q, k, m, scale) + + # transformation - a + qroots = roots(Q, k) + if qroots: + k_min = Min(*qroots.keys()) + else: + k_min = S.Zero + shift = k_min + m + f, P, Q, m = _transformation_a(f, x, P, Q, k, m, shift) + + l = (x*f).limit(x, 0) + if not isinstance(l, Limit) and l != 0: # Ideally should only be l != 0 + return None + + qroots = roots(Q, k) + if qroots: + k_max = Max(*qroots.keys()) + else: + k_max = S.Zero + + ind, mp = S.Zero, -oo + for i in range(k_max + m + 1): + r = f.diff(x, i).limit(x, 0) / factorial(i) + if r.is_finite is False: + old_f = f + f, P, Q, m = _transformation_a(f, x, P, Q, k, m, i) + f, P, Q, m = _transformation_e(f, x, P, Q, k, m) + sol, ind, mp = _rsolve_hypergeometric(f, x, P, Q, k, m) + sol = _apply_integrate(sol, x, k) + sol = _apply_shift(sol, i) + ind = integrate(ind, x) + ind += (old_f - ind).limit(x, 0) # constant of integration + mp += 1 + return sol, ind, mp + elif r: + ind += r*x**(i + shift) + pow_x = Rational((i + shift), scale) + if pow_x > mp: + mp = pow_x # maximum power of x + ind = ind.subs(x, x**(1/scale)) + + sol = _compute_formula(f, x, P, Q, k, m, k_max) + sol = _apply_shift(sol, shift) + sol = _apply_scale(sol, scale) + + return sol, ind, mp + + +def rsolve_hypergeometric(f, x, P, Q, k, m): + """ + Solves RE of hypergeometric type. + + Explanation + =========== + + Attempts to solve RE of the form + + Q(k)*a(k + m) - P(k)*a(k) + + Transformations that preserve Hypergeometric type: + + a. x**n*f(x): b(k + m) = R(k - n)*b(k) + b. f(A*x): b(k + m) = A**m*R(k)*b(k) + c. f(x**n): b(k + n*m) = R(k/n)*b(k) + d. f(x**(1/m)): b(k + 1) = R(k*m)*b(k) + e. f'(x): b(k + m) = ((k + m + 1)/(k + 1))*R(k + 1)*b(k) + + Some of these transformations have been used to solve the RE. + + Returns + ======= + + formula : Expr + ind : Expr + Independent terms. + order : int + + Examples + ======== + + >>> from sympy import exp, ln, S + >>> from sympy.series.formal import rsolve_hypergeometric as rh + >>> from sympy.abc import x, k + + >>> rh(exp(x), x, -S.One, (k + 1), k, 1) + (Piecewise((1/factorial(k), Eq(Mod(k, 1), 0)), (0, True)), 1, 1) + + >>> rh(ln(1 + x), x, k**2, k*(k + 1), k, 1) + (Piecewise(((-1)**(k - 1)*factorial(k - 1)/RisingFactorial(2, k - 1), + Eq(Mod(k, 1), 0)), (0, True)), x, 2) + + References + ========== + + .. [1] Formal Power Series - Dominik Gruntz, Wolfram Koepf + .. [2] Power Series in Computer Algebra - Wolfram Koepf + """ + result = _rsolve_hypergeometric(f, x, P, Q, k, m) + + if result is None: + return None + + sol_list, ind, mp = result + + sol_dict = defaultdict(lambda: S.Zero) + for res, cond in sol_list: + j, mk = cond.as_coeff_Add() + c = mk.coeff(k) + + if j.is_integer is False: + res *= x**frac(j) + j = floor(j) + + res = res.subs(k, (k - j) / c) + cond = Eq(k % c, j % c) + sol_dict[cond] += res # Group together formula for same conditions + + sol = [(res, cond) for cond, res in sol_dict.items()] + sol.append((S.Zero, True)) + sol = Piecewise(*sol) + + if mp is -oo: + s = S.Zero + elif mp.is_integer is False: + s = ceiling(mp) + else: + s = mp + 1 + + # save all the terms of + # form 1/x**k in ind + if s < 0: + ind += sum(sequence(sol * x**k, (k, s, -1))) + s = S.Zero + + return (sol, ind, s) + + +def _solve_hyper_RE(f, x, RE, g, k): + """See docstring of :func:`rsolve_hypergeometric` for details.""" + terms = Add.make_args(RE) + + if len(terms) == 2: + gs = list(RE.atoms(Function)) + P, Q = map(RE.coeff, gs) + m = gs[1].args[0] - gs[0].args[0] + if m < 0: + P, Q = Q, P + m = abs(m) + return rsolve_hypergeometric(f, x, P, Q, k, m) + + +def _solve_explike_DE(f, x, DE, g, k): + """Solves DE with constant coefficients.""" + from sympy.solvers import rsolve + + for t in Add.make_args(DE): + coeff, d = t.as_independent(g) + if coeff.free_symbols: + return + + RE = exp_re(DE, g, k) + + init = {} + for i in range(len(Add.make_args(RE))): + if i: + f = f.diff(x) + init[g(k).subs(k, i)] = f.limit(x, 0) + + sol = rsolve(RE, g(k), init) + + if sol: + return (sol / factorial(k), S.Zero, S.Zero) + + +def _solve_simple(f, x, DE, g, k): + """Converts DE into RE and solves using :func:`rsolve`.""" + from sympy.solvers import rsolve + + RE = hyper_re(DE, g, k) + + init = {} + for i in range(len(Add.make_args(RE))): + if i: + f = f.diff(x) + init[g(k).subs(k, i)] = f.limit(x, 0) / factorial(i) + + sol = rsolve(RE, g(k), init) + + if sol: + return (sol, S.Zero, S.Zero) + + +def _transform_explike_DE(DE, g, x, order, syms): + """Converts DE with free parameters into DE with constant coefficients.""" + from sympy.solvers.solveset import linsolve + + eq = [] + highest_coeff = DE.coeff(Derivative(g(x), x, order)) + for i in range(order): + coeff = DE.coeff(Derivative(g(x), x, i)) + coeff = (coeff / highest_coeff).expand().collect(x) + eq.extend(Add.make_args(coeff)) + temp = [] + for e in eq: + if e.has(x): + break + elif e.has(Symbol): + temp.append(e) + else: + eq = temp + if eq: + sol = dict(zip(syms, (i for s in linsolve(eq, list(syms)) for i in s))) + if sol: + DE = DE.subs(sol) + DE = DE.factor().as_coeff_mul(Derivative)[1][0] + DE = DE.collect(Derivative(g(x))) + return DE + + +def _transform_DE_RE(DE, g, k, order, syms): + """Converts DE with free parameters into RE of hypergeometric type.""" + from sympy.solvers.solveset import linsolve + + RE = hyper_re(DE, g, k) + + eq = [RE.coeff(g(k + i)) for i in range(1, order)] + sol = dict(zip(syms, (i for s in linsolve(eq, list(syms)) for i in s))) + if sol: + m = Wild('m') + RE = RE.subs(sol) + RE = RE.factor().as_numer_denom()[0].collect(g(k + m)) + RE = RE.as_coeff_mul(g)[1][0] + for i in range(order): # smallest order should be g(k) + if RE.coeff(g(k + i)) and i: + RE = RE.subs(k, k - i) + break + return RE + + +def solve_de(f, x, DE, order, g, k): + """ + Solves the DE. + + Explanation + =========== + + Tries to solve DE by either converting into a RE containing two terms or + converting into a DE having constant coefficients. + + Returns + ======= + + formula : Expr + ind : Expr + Independent terms. + order : int + + Examples + ======== + + >>> from sympy import Derivative as D, Function + >>> from sympy import exp, ln + >>> from sympy.series.formal import solve_de + >>> from sympy.abc import x, k + >>> f = Function('f') + + >>> solve_de(exp(x), x, D(f(x), x) - f(x), 1, f, k) + (Piecewise((1/factorial(k), Eq(Mod(k, 1), 0)), (0, True)), 1, 1) + + >>> solve_de(ln(1 + x), x, (x + 1)*D(f(x), x, 2) + D(f(x)), 2, f, k) + (Piecewise(((-1)**(k - 1)*factorial(k - 1)/RisingFactorial(2, k - 1), + Eq(Mod(k, 1), 0)), (0, True)), x, 2) + """ + sol = None + syms = DE.free_symbols.difference({g, x}) + + if syms: + RE = _transform_DE_RE(DE, g, k, order, syms) + else: + RE = hyper_re(DE, g, k) + if not RE.free_symbols.difference({k}): + sol = _solve_hyper_RE(f, x, RE, g, k) + + if sol: + return sol + + if syms: + DE = _transform_explike_DE(DE, g, x, order, syms) + if not DE.free_symbols.difference({x}): + sol = _solve_explike_DE(f, x, DE, g, k) + + if sol: + return sol + + +def hyper_algorithm(f, x, k, order=4): + """ + Hypergeometric algorithm for computing Formal Power Series. + + Explanation + =========== + + Steps: + * Generates DE + * Convert the DE into RE + * Solves the RE + + Examples + ======== + + >>> from sympy import exp, ln + >>> from sympy.series.formal import hyper_algorithm + + >>> from sympy.abc import x, k + + >>> hyper_algorithm(exp(x), x, k) + (Piecewise((1/factorial(k), Eq(Mod(k, 1), 0)), (0, True)), 1, 1) + + >>> hyper_algorithm(ln(1 + x), x, k) + (Piecewise(((-1)**(k - 1)*factorial(k - 1)/RisingFactorial(2, k - 1), + Eq(Mod(k, 1), 0)), (0, True)), x, 2) + + See Also + ======== + + sympy.series.formal.simpleDE + sympy.series.formal.solve_de + """ + g = Function('g') + + des = [] # list of DE's + sol = None + for DE, i in simpleDE(f, x, g, order): + if DE is not None: + sol = solve_de(f, x, DE, i, g, k) + if sol: + return sol + if not DE.free_symbols.difference({x}): + des.append(DE) + + # If nothing works + # Try plain rsolve + for DE in des: + sol = _solve_simple(f, x, DE, g, k) + if sol: + return sol + + +def _compute_fps(f, x, x0, dir, hyper, order, rational, full): + """Recursive wrapper to compute fps. + + See :func:`compute_fps` for details. + """ + if x0 in [S.Infinity, S.NegativeInfinity]: + dir = S.One if x0 is S.Infinity else -S.One + temp = f.subs(x, 1/x) + result = _compute_fps(temp, x, 0, dir, hyper, order, rational, full) + if result is None: + return None + return (result[0], result[1].subs(x, 1/x), result[2].subs(x, 1/x)) + elif x0 or dir == -S.One: + if dir == -S.One: + rep = -x + x0 + rep2 = -x + rep2b = x0 + else: + rep = x + x0 + rep2 = x + rep2b = -x0 + temp = f.subs(x, rep) + result = _compute_fps(temp, x, 0, S.One, hyper, order, rational, full) + if result is None: + return None + return (result[0], result[1].subs(x, rep2 + rep2b), + result[2].subs(x, rep2 + rep2b)) + + if f.is_polynomial(x): + k = Dummy('k') + ak = sequence(Coeff(f, x, k), (k, 1, oo)) + xk = sequence(x**k, (k, 0, oo)) + ind = f.coeff(x, 0) + return ak, xk, ind + + # Break instances of Add + # this allows application of different + # algorithms on different terms increasing the + # range of admissible functions. + if isinstance(f, Add): + result = False + ak = sequence(S.Zero, (0, oo)) + ind, xk = S.Zero, None + for t in Add.make_args(f): + res = _compute_fps(t, x, 0, S.One, hyper, order, rational, full) + if res: + if not result: + result = True + xk = res[1] + if res[0].start > ak.start: + seq = ak + s, f = ak.start, res[0].start + else: + seq = res[0] + s, f = res[0].start, ak.start + save = Add(*[z[0]*z[1] for z in zip(seq[0:(f - s)], xk[s:f])]) + ak += res[0] + ind += res[2] + save + else: + ind += t + if result: + return ak, xk, ind + return None + + # The symbolic term - symb, if present, is being separated from the function + # Otherwise symb is being set to S.One + syms = f.free_symbols.difference({x}) + (f, symb) = expand(f).as_independent(*syms) + + result = None + + # from here on it's x0=0 and dir=1 handling + k = Dummy('k') + if rational: + result = rational_algorithm(f, x, k, order, full) + + if result is None and hyper: + result = hyper_algorithm(f, x, k, order) + + if result is None: + return None + + from sympy.simplify.powsimp import powsimp + if symb.is_zero: + symb = S.One + else: + symb = powsimp(symb) + ak = sequence(result[0], (k, result[2], oo)) + xk_formula = powsimp(x**k * symb) + xk = sequence(xk_formula, (k, 0, oo)) + ind = powsimp(result[1] * symb) + + return ak, xk, ind + + +def compute_fps(f, x, x0=0, dir=1, hyper=True, order=4, rational=True, + full=False): + """ + Computes the formula for Formal Power Series of a function. + + Explanation + =========== + + Tries to compute the formula by applying the following techniques + (in order): + + * rational_algorithm + * Hypergeometric algorithm + + Parameters + ========== + + x : Symbol + x0 : number, optional + Point to perform series expansion about. Default is 0. + dir : {1, -1, '+', '-'}, optional + If dir is 1 or '+' the series is calculated from the right and + for -1 or '-' the series is calculated from the left. For smooth + functions this flag will not alter the results. Default is 1. + hyper : {True, False}, optional + Set hyper to False to skip the hypergeometric algorithm. + By default it is set to False. + order : int, optional + Order of the derivative of ``f``, Default is 4. + rational : {True, False}, optional + Set rational to False to skip rational algorithm. By default it is set + to True. + full : {True, False}, optional + Set full to True to increase the range of rational algorithm. + See :func:`rational_algorithm` for details. By default it is set to + False. + + Returns + ======= + + ak : sequence + Sequence of coefficients. + xk : sequence + Sequence of powers of x. + ind : Expr + Independent terms. + mul : Pow + Common terms. + + See Also + ======== + + sympy.series.formal.rational_algorithm + sympy.series.formal.hyper_algorithm + """ + f = sympify(f) + x = sympify(x) + + if not f.has(x): + return None + + x0 = sympify(x0) + + if dir == '+': + dir = S.One + elif dir == '-': + dir = -S.One + elif dir not in [S.One, -S.One]: + raise ValueError("Dir must be '+' or '-'") + else: + dir = sympify(dir) + + return _compute_fps(f, x, x0, dir, hyper, order, rational, full) + + +class Coeff(Function): + """ + Coeff(p, x, n) represents the nth coefficient of the polynomial p in x + """ + @classmethod + def eval(cls, p, x, n): + if p.is_polynomial(x) and n.is_integer: + return p.coeff(x, n) + + +class FormalPowerSeries(SeriesBase): + """ + Represents Formal Power Series of a function. + + Explanation + =========== + + No computation is performed. This class should only to be used to represent + a series. No checks are performed. + + For computing a series use :func:`fps`. + + See Also + ======== + + sympy.series.formal.fps + """ + def __new__(cls, *args): + args = map(sympify, args) + return Expr.__new__(cls, *args) + + def __init__(self, *args): + ak = args[4][0] + k = ak.variables[0] + self.ak_seq = sequence(ak.formula, (k, 1, oo)) + self.fact_seq = sequence(factorial(k), (k, 1, oo)) + self.bell_coeff_seq = self.ak_seq * self.fact_seq + self.sign_seq = sequence((-1, 1), (k, 1, oo)) + + @property + def function(self): + return self.args[0] + + @property + def x(self): + return self.args[1] + + @property + def x0(self): + return self.args[2] + + @property + def dir(self): + return self.args[3] + + @property + def ak(self): + return self.args[4][0] + + @property + def xk(self): + return self.args[4][1] + + @property + def ind(self): + return self.args[4][2] + + @property + def interval(self): + return Interval(0, oo) + + @property + def start(self): + return self.interval.inf + + @property + def stop(self): + return self.interval.sup + + @property + def length(self): + return oo + + @property + def infinite(self): + """Returns an infinite representation of the series""" + from sympy.concrete import Sum + ak, xk = self.ak, self.xk + k = ak.variables[0] + inf_sum = Sum(ak.formula * xk.formula, (k, ak.start, ak.stop)) + + return self.ind + inf_sum + + def _get_pow_x(self, term): + """Returns the power of x in a term.""" + xterm, pow_x = term.as_independent(self.x)[1].as_base_exp() + if not xterm.has(self.x): + return S.Zero + return pow_x + + def polynomial(self, n=6): + """ + Truncated series as polynomial. + + Explanation + =========== + + Returns series expansion of ``f`` upto order ``O(x**n)`` + as a polynomial(without ``O`` term). + """ + terms = [] + sym = self.free_symbols + for i, t in enumerate(self): + xp = self._get_pow_x(t) + if xp.has(*sym): + xp = xp.as_coeff_add(*sym)[0] + if xp >= n: + break + elif xp.is_integer is True and i == n + 1: + break + elif t is not S.Zero: + terms.append(t) + + return Add(*terms) + + def truncate(self, n=6): + """ + Truncated series. + + Explanation + =========== + + Returns truncated series expansion of f upto + order ``O(x**n)``. + + If n is ``None``, returns an infinite iterator. + """ + if n is None: + return iter(self) + + x, x0 = self.x, self.x0 + pt_xk = self.xk.coeff(n) + if x0 is S.NegativeInfinity: + x0 = S.Infinity + + return self.polynomial(n) + Order(pt_xk, (x, x0)) + + def zero_coeff(self): + return self._eval_term(0) + + def _eval_term(self, pt): + try: + pt_xk = self.xk.coeff(pt) + pt_ak = self.ak.coeff(pt).simplify() # Simplify the coefficients + except IndexError: + term = S.Zero + else: + term = (pt_ak * pt_xk) + + if self.ind: + ind = S.Zero + sym = self.free_symbols + for t in Add.make_args(self.ind): + pow_x = self._get_pow_x(t) + if pow_x.has(*sym): + pow_x = pow_x.as_coeff_add(*sym)[0] + if pt == 0 and pow_x < 1: + ind += t + elif pow_x >= pt and pow_x < pt + 1: + ind += t + term += ind + + return term.collect(self.x) + + def _eval_subs(self, old, new): + x = self.x + if old.has(x): + return self + + def _eval_as_leading_term(self, x, logx, cdir): + for t in self: + if t is not S.Zero: + return t + + def _eval_derivative(self, x): + f = self.function.diff(x) + ind = self.ind.diff(x) + + pow_xk = self._get_pow_x(self.xk.formula) + ak = self.ak + k = ak.variables[0] + if ak.formula.has(x): + form = [] + for e, c in ak.formula.args: + temp = S.Zero + for t in Add.make_args(e): + pow_x = self._get_pow_x(t) + temp += t * (pow_xk + pow_x) + form.append((temp, c)) + form = Piecewise(*form) + ak = sequence(form.subs(k, k + 1), (k, ak.start - 1, ak.stop)) + else: + ak = sequence((ak.formula * pow_xk).subs(k, k + 1), + (k, ak.start - 1, ak.stop)) + + return self.func(f, self.x, self.x0, self.dir, (ak, self.xk, ind)) + + def integrate(self, x=None, **kwargs): + """ + Integrate Formal Power Series. + + Examples + ======== + + >>> from sympy import fps, sin, integrate + >>> from sympy.abc import x + >>> f = fps(sin(x)) + >>> f.integrate(x).truncate() + -1 + x**2/2 - x**4/24 + O(x**6) + >>> integrate(f, (x, 0, 1)) + 1 - cos(1) + """ + from sympy.integrals import integrate + + if x is None: + x = self.x + elif iterable(x): + return integrate(self.function, x) + + f = integrate(self.function, x) + ind = integrate(self.ind, x) + ind += (f - ind).limit(x, 0) # constant of integration + + pow_xk = self._get_pow_x(self.xk.formula) + ak = self.ak + k = ak.variables[0] + if ak.formula.has(x): + form = [] + for e, c in ak.formula.args: + temp = S.Zero + for t in Add.make_args(e): + pow_x = self._get_pow_x(t) + temp += t / (pow_xk + pow_x + 1) + form.append((temp, c)) + form = Piecewise(*form) + ak = sequence(form.subs(k, k - 1), (k, ak.start + 1, ak.stop)) + else: + ak = sequence((ak.formula / (pow_xk + 1)).subs(k, k - 1), + (k, ak.start + 1, ak.stop)) + + return self.func(f, self.x, self.x0, self.dir, (ak, self.xk, ind)) + + def product(self, other, x=None, n=6): + """ + Multiplies two Formal Power Series, using discrete convolution and + return the truncated terms upto specified order. + + Parameters + ========== + + n : Number, optional + Specifies the order of the term up to which the polynomial should + be truncated. + + Examples + ======== + + >>> from sympy import fps, sin, exp + >>> from sympy.abc import x + >>> f1 = fps(sin(x)) + >>> f2 = fps(exp(x)) + + >>> f1.product(f2, x).truncate(4) + x + x**2 + x**3/3 + O(x**4) + + See Also + ======== + + sympy.discrete.convolutions + sympy.series.formal.FormalPowerSeriesProduct + + """ + + if n is None: + return iter(self) + + other = sympify(other) + + if not isinstance(other, FormalPowerSeries): + raise ValueError("Both series should be an instance of FormalPowerSeries" + " class.") + + if self.dir != other.dir: + raise ValueError("Both series should be calculated from the" + " same direction.") + elif self.x0 != other.x0: + raise ValueError("Both series should be calculated about the" + " same point.") + + elif self.x != other.x: + raise ValueError("Both series should have the same symbol.") + + return FormalPowerSeriesProduct(self, other) + + def coeff_bell(self, n): + r""" + self.coeff_bell(n) returns a sequence of Bell polynomials of the second kind. + Note that ``n`` should be a integer. + + The second kind of Bell polynomials (are sometimes called "partial" Bell + polynomials or incomplete Bell polynomials) are defined as + + .. math:: + B_{n,k}(x_1, x_2,\dotsc x_{n-k+1}) = + \sum_{j_1+j_2+j_2+\dotsb=k \atop j_1+2j_2+3j_2+\dotsb=n} + \frac{n!}{j_1!j_2!\dotsb j_{n-k+1}!} + \left(\frac{x_1}{1!} \right)^{j_1} + \left(\frac{x_2}{2!} \right)^{j_2} \dotsb + \left(\frac{x_{n-k+1}}{(n-k+1)!} \right) ^{j_{n-k+1}}. + + * ``bell(n, k, (x1, x2, ...))`` gives Bell polynomials of the second kind, + `B_{n,k}(x_1, x_2, \dotsc, x_{n-k+1})`. + + See Also + ======== + + sympy.functions.combinatorial.numbers.bell + + """ + + inner_coeffs = [bell(n, j, tuple(self.bell_coeff_seq[:n-j+1])) for j in range(1, n+1)] + + k = Dummy('k') + return sequence(tuple(inner_coeffs), (k, 1, oo)) + + def compose(self, other, x=None, n=6): + r""" + Returns the truncated terms of the formal power series of the composed function, + up to specified ``n``. + + Explanation + =========== + + If ``f`` and ``g`` are two formal power series of two different functions, + then the coefficient sequence ``ak`` of the composed formal power series `fp` + will be as follows. + + .. math:: + \sum\limits_{k=0}^{n} b_k B_{n,k}(x_1, x_2, \dotsc, x_{n-k+1}) + + Parameters + ========== + + n : Number, optional + Specifies the order of the term up to which the polynomial should + be truncated. + + Examples + ======== + + >>> from sympy import fps, sin, exp + >>> from sympy.abc import x + >>> f1 = fps(exp(x)) + >>> f2 = fps(sin(x)) + + >>> f1.compose(f2, x).truncate() + 1 + x + x**2/2 - x**4/8 - x**5/15 + O(x**6) + + >>> f1.compose(f2, x).truncate(8) + 1 + x + x**2/2 - x**4/8 - x**5/15 - x**6/240 + x**7/90 + O(x**8) + + See Also + ======== + + sympy.functions.combinatorial.numbers.bell + sympy.series.formal.FormalPowerSeriesCompose + + References + ========== + + .. [1] Comtet, Louis: Advanced combinatorics; the art of finite and infinite expansions. Reidel, 1974. + + """ + + if n is None: + return iter(self) + + other = sympify(other) + + if not isinstance(other, FormalPowerSeries): + raise ValueError("Both series should be an instance of FormalPowerSeries" + " class.") + + if self.dir != other.dir: + raise ValueError("Both series should be calculated from the" + " same direction.") + elif self.x0 != other.x0: + raise ValueError("Both series should be calculated about the" + " same point.") + + elif self.x != other.x: + raise ValueError("Both series should have the same symbol.") + + if other._eval_term(0).as_coeff_mul(other.x)[0] is not S.Zero: + raise ValueError("The formal power series of the inner function should not have any " + "constant coefficient term.") + + return FormalPowerSeriesCompose(self, other) + + def inverse(self, x=None, n=6): + r""" + Returns the truncated terms of the inverse of the formal power series, + up to specified ``n``. + + Explanation + =========== + + If ``f`` and ``g`` are two formal power series of two different functions, + then the coefficient sequence ``ak`` of the composed formal power series ``fp`` + will be as follows. + + .. math:: + \sum\limits_{k=0}^{n} (-1)^{k} x_0^{-k-1} B_{n,k}(x_1, x_2, \dotsc, x_{n-k+1}) + + Parameters + ========== + + n : Number, optional + Specifies the order of the term up to which the polynomial should + be truncated. + + Examples + ======== + + >>> from sympy import fps, exp, cos + >>> from sympy.abc import x + >>> f1 = fps(exp(x)) + >>> f2 = fps(cos(x)) + + >>> f1.inverse(x).truncate() + 1 - x + x**2/2 - x**3/6 + x**4/24 - x**5/120 + O(x**6) + + >>> f2.inverse(x).truncate(8) + 1 + x**2/2 + 5*x**4/24 + 61*x**6/720 + O(x**8) + + See Also + ======== + + sympy.functions.combinatorial.numbers.bell + sympy.series.formal.FormalPowerSeriesInverse + + References + ========== + + .. [1] Comtet, Louis: Advanced combinatorics; the art of finite and infinite expansions. Reidel, 1974. + + """ + + if n is None: + return iter(self) + + if self._eval_term(0).is_zero: + raise ValueError("Constant coefficient should exist for an inverse of a formal" + " power series to exist.") + + return FormalPowerSeriesInverse(self) + + def __add__(self, other): + other = sympify(other) + + if isinstance(other, FormalPowerSeries): + if self.dir != other.dir: + raise ValueError("Both series should be calculated from the" + " same direction.") + elif self.x0 != other.x0: + raise ValueError("Both series should be calculated about the" + " same point.") + + x, y = self.x, other.x + f = self.function + other.function.subs(y, x) + + if self.x not in f.free_symbols: + return f + + ak = self.ak + other.ak + if self.ak.start > other.ak.start: + seq = other.ak + s, e = other.ak.start, self.ak.start + else: + seq = self.ak + s, e = self.ak.start, other.ak.start + save = Add(*[z[0]*z[1] for z in zip(seq[0:(e - s)], self.xk[s:e])]) + ind = self.ind + other.ind + save + + return self.func(f, x, self.x0, self.dir, (ak, self.xk, ind)) + + elif not other.has(self.x): + f = self.function + other + ind = self.ind + other + + return self.func(f, self.x, self.x0, self.dir, + (self.ak, self.xk, ind)) + + return Add(self, other) + + def __radd__(self, other): + return self.__add__(other) + + def __neg__(self): + return self.func(-self.function, self.x, self.x0, self.dir, + (-self.ak, self.xk, -self.ind)) + + def __sub__(self, other): + return self.__add__(-other) + + def __rsub__(self, other): + return (-self).__add__(other) + + def __mul__(self, other): + other = sympify(other) + + if other.has(self.x): + return Mul(self, other) + + f = self.function * other + ak = self.ak.coeff_mul(other) + ind = self.ind * other + + return self.func(f, self.x, self.x0, self.dir, (ak, self.xk, ind)) + + def __rmul__(self, other): + return self.__mul__(other) + + +class FiniteFormalPowerSeries(FormalPowerSeries): + """Base Class for Product, Compose and Inverse classes""" + + def __init__(self, *args): + pass + + @property + def ffps(self): + return self.args[0] + + @property + def gfps(self): + return self.args[1] + + @property + def f(self): + return self.ffps.function + + @property + def g(self): + return self.gfps.function + + @property + def infinite(self): + raise NotImplementedError("No infinite version for an object of" + " FiniteFormalPowerSeries class.") + + def _eval_terms(self, n): + raise NotImplementedError("(%s)._eval_terms()" % self) + + def _eval_term(self, pt): + raise NotImplementedError("By the current logic, one can get terms" + "upto a certain order, instead of getting term by term.") + + def polynomial(self, n): + return self._eval_terms(n) + + def truncate(self, n=6): + ffps = self.ffps + pt_xk = ffps.xk.coeff(n) + x, x0 = ffps.x, ffps.x0 + + return self.polynomial(n) + Order(pt_xk, (x, x0)) + + def _eval_derivative(self, x): + raise NotImplementedError + + def integrate(self, x): + raise NotImplementedError + + +class FormalPowerSeriesProduct(FiniteFormalPowerSeries): + """Represents the product of two formal power series of two functions. + + Explanation + =========== + + No computation is performed. Terms are calculated using a term by term logic, + instead of a point by point logic. + + There are two differences between a :obj:`FormalPowerSeries` object and a + :obj:`FormalPowerSeriesProduct` object. The first argument contains the two + functions involved in the product. Also, the coefficient sequence contains + both the coefficient sequence of the formal power series of the involved functions. + + See Also + ======== + + sympy.series.formal.FormalPowerSeries + sympy.series.formal.FiniteFormalPowerSeries + + """ + + def __init__(self, *args): + ffps, gfps = self.ffps, self.gfps + + k = ffps.ak.variables[0] + self.coeff1 = sequence(ffps.ak.formula, (k, 0, oo)) + + k = gfps.ak.variables[0] + self.coeff2 = sequence(gfps.ak.formula, (k, 0, oo)) + + @property + def function(self): + """Function of the product of two formal power series.""" + return self.f * self.g + + def _eval_terms(self, n): + """ + Returns the first ``n`` terms of the product formal power series. + Term by term logic is implemented here. + + Examples + ======== + + >>> from sympy import fps, sin, exp + >>> from sympy.abc import x + >>> f1 = fps(sin(x)) + >>> f2 = fps(exp(x)) + >>> fprod = f1.product(f2, x) + + >>> fprod._eval_terms(4) + x**3/3 + x**2 + x + + See Also + ======== + + sympy.series.formal.FormalPowerSeries.product + + """ + coeff1, coeff2 = self.coeff1, self.coeff2 + + aks = convolution(coeff1[:n], coeff2[:n]) + + terms = [] + for i in range(0, n): + terms.append(aks[i] * self.ffps.xk.coeff(i)) + + return Add(*terms) + + +class FormalPowerSeriesCompose(FiniteFormalPowerSeries): + """ + Represents the composed formal power series of two functions. + + Explanation + =========== + + No computation is performed. Terms are calculated using a term by term logic, + instead of a point by point logic. + + There are two differences between a :obj:`FormalPowerSeries` object and a + :obj:`FormalPowerSeriesCompose` object. The first argument contains the outer + function and the inner function involved in the omposition. Also, the + coefficient sequence contains the generic sequence which is to be multiplied + by a custom ``bell_seq`` finite sequence. The finite terms will then be added up to + get the final terms. + + See Also + ======== + + sympy.series.formal.FormalPowerSeries + sympy.series.formal.FiniteFormalPowerSeries + + """ + + @property + def function(self): + """Function for the composed formal power series.""" + f, g, x = self.f, self.g, self.ffps.x + return f.subs(x, g) + + def _eval_terms(self, n): + """ + Returns the first `n` terms of the composed formal power series. + Term by term logic is implemented here. + + Explanation + =========== + + The coefficient sequence of the :obj:`FormalPowerSeriesCompose` object is the generic sequence. + It is multiplied by ``bell_seq`` to get a sequence, whose terms are added up to get + the final terms for the polynomial. + + Examples + ======== + + >>> from sympy import fps, sin, exp + >>> from sympy.abc import x + >>> f1 = fps(exp(x)) + >>> f2 = fps(sin(x)) + >>> fcomp = f1.compose(f2, x) + + >>> fcomp._eval_terms(6) + -x**5/15 - x**4/8 + x**2/2 + x + 1 + + >>> fcomp._eval_terms(8) + x**7/90 - x**6/240 - x**5/15 - x**4/8 + x**2/2 + x + 1 + + See Also + ======== + + sympy.series.formal.FormalPowerSeries.compose + sympy.series.formal.FormalPowerSeries.coeff_bell + + """ + + ffps, gfps = self.ffps, self.gfps + terms = [ffps.zero_coeff()] + + for i in range(1, n): + bell_seq = gfps.coeff_bell(i) + seq = (ffps.bell_coeff_seq * bell_seq) + terms.append(Add(*(seq[:i])) / ffps.fact_seq[i-1] * ffps.xk.coeff(i)) + + return Add(*terms) + + +class FormalPowerSeriesInverse(FiniteFormalPowerSeries): + """ + Represents the Inverse of a formal power series. + + Explanation + =========== + + No computation is performed. Terms are calculated using a term by term logic, + instead of a point by point logic. + + There is a single difference between a :obj:`FormalPowerSeries` object and a + :obj:`FormalPowerSeriesInverse` object. The coefficient sequence contains the + generic sequence which is to be multiplied by a custom ``bell_seq`` finite sequence. + The finite terms will then be added up to get the final terms. + + See Also + ======== + + sympy.series.formal.FormalPowerSeries + sympy.series.formal.FiniteFormalPowerSeries + + """ + def __init__(self, *args): + ffps = self.ffps + k = ffps.xk.variables[0] + + inv = ffps.zero_coeff() + inv_seq = sequence(inv ** (-(k + 1)), (k, 1, oo)) + self.aux_seq = ffps.sign_seq * ffps.fact_seq * inv_seq + + @property + def function(self): + """Function for the inverse of a formal power series.""" + f = self.f + return 1 / f + + @property + def g(self): + raise ValueError("Only one function is considered while performing" + "inverse of a formal power series.") + + @property + def gfps(self): + raise ValueError("Only one function is considered while performing" + "inverse of a formal power series.") + + def _eval_terms(self, n): + """ + Returns the first ``n`` terms of the composed formal power series. + Term by term logic is implemented here. + + Explanation + =========== + + The coefficient sequence of the `FormalPowerSeriesInverse` object is the generic sequence. + It is multiplied by ``bell_seq`` to get a sequence, whose terms are added up to get + the final terms for the polynomial. + + Examples + ======== + + >>> from sympy import fps, exp, cos + >>> from sympy.abc import x + >>> f1 = fps(exp(x)) + >>> f2 = fps(cos(x)) + >>> finv1, finv2 = f1.inverse(), f2.inverse() + + >>> finv1._eval_terms(6) + -x**5/120 + x**4/24 - x**3/6 + x**2/2 - x + 1 + + >>> finv2._eval_terms(8) + 61*x**6/720 + 5*x**4/24 + x**2/2 + 1 + + See Also + ======== + + sympy.series.formal.FormalPowerSeries.inverse + sympy.series.formal.FormalPowerSeries.coeff_bell + + """ + ffps = self.ffps + terms = [ffps.zero_coeff()] + + for i in range(1, n): + bell_seq = ffps.coeff_bell(i) + seq = (self.aux_seq * bell_seq) + terms.append(Add(*(seq[:i])) / ffps.fact_seq[i-1] * ffps.xk.coeff(i)) + + return Add(*terms) + + +def fps(f, x=None, x0=0, dir=1, hyper=True, order=4, rational=True, full=False): + """ + Generates Formal Power Series of ``f``. + + Explanation + =========== + + Returns the formal series expansion of ``f`` around ``x = x0`` + with respect to ``x`` in the form of a ``FormalPowerSeries`` object. + + Formal Power Series is represented using an explicit formula + computed using different algorithms. + + See :func:`compute_fps` for the more details regarding the computation + of formula. + + Parameters + ========== + + x : Symbol, optional + If x is None and ``f`` is univariate, the univariate symbols will be + supplied, otherwise an error will be raised. + x0 : number, optional + Point to perform series expansion about. Default is 0. + dir : {1, -1, '+', '-'}, optional + If dir is 1 or '+' the series is calculated from the right and + for -1 or '-' the series is calculated from the left. For smooth + functions this flag will not alter the results. Default is 1. + hyper : {True, False}, optional + Set hyper to False to skip the hypergeometric algorithm. + By default it is set to False. + order : int, optional + Order of the derivative of ``f``, Default is 4. + rational : {True, False}, optional + Set rational to False to skip rational algorithm. By default it is set + to True. + full : {True, False}, optional + Set full to True to increase the range of rational algorithm. + See :func:`rational_algorithm` for details. By default it is set to + False. + + Examples + ======== + + >>> from sympy import fps, ln, atan, sin + >>> from sympy.abc import x, n + + Rational Functions + + >>> fps(ln(1 + x)).truncate() + x - x**2/2 + x**3/3 - x**4/4 + x**5/5 + O(x**6) + + >>> fps(atan(x), full=True).truncate() + x - x**3/3 + x**5/5 + O(x**6) + + Symbolic Functions + + >>> fps(x**n*sin(x**2), x).truncate(8) + -x**(n + 6)/6 + x**(n + 2) + O(x**(n + 8)) + + See Also + ======== + + sympy.series.formal.FormalPowerSeries + sympy.series.formal.compute_fps + """ + f = sympify(f) + + if x is None: + free = f.free_symbols + if len(free) == 1: + x = free.pop() + elif not free: + return f + else: + raise NotImplementedError("multivariate formal power series") + + result = compute_fps(f, x, x0, dir, hyper, order, rational, full) + + if result is None: + return f + + return FormalPowerSeries(f, x, x0, dir, result) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/fourier.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/fourier.py new file mode 100644 index 0000000000000000000000000000000000000000..c4ad43a75d0e1d24bfc591383f68965fd8b504c7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/fourier.py @@ -0,0 +1,811 @@ +"""Fourier Series""" + +from sympy.core.numbers import (oo, pi) +from sympy.core.symbol import Wild +from sympy.core.expr import Expr +from sympy.core.add import Add +from sympy.core.containers import Tuple +from sympy.core.singleton import S +from sympy.core.symbol import Dummy, Symbol +from sympy.core.sympify import sympify +from sympy.functions.elementary.trigonometric import sin, cos, sinc +from sympy.series.series_class import SeriesBase +from sympy.series.sequences import SeqFormula +from sympy.sets.sets import Interval +from sympy.utilities.iterables import is_sequence + + +__doctest_requires__ = {('fourier_series',): ['matplotlib']} + + +def fourier_cos_seq(func, limits, n): + """Returns the cos sequence in a Fourier series""" + from sympy.integrals import integrate + x, L = limits[0], limits[2] - limits[1] + cos_term = cos(2*n*pi*x / L) + formula = 2 * cos_term * integrate(func * cos_term, limits) / L + a0 = formula.subs(n, S.Zero) / 2 + return a0, SeqFormula(2 * cos_term * integrate(func * cos_term, limits) + / L, (n, 1, oo)) + + +def fourier_sin_seq(func, limits, n): + """Returns the sin sequence in a Fourier series""" + from sympy.integrals import integrate + x, L = limits[0], limits[2] - limits[1] + sin_term = sin(2*n*pi*x / L) + return SeqFormula(2 * sin_term * integrate(func * sin_term, limits) + / L, (n, 1, oo)) + + +def _process_limits(func, limits): + """ + Limits should be of the form (x, start, stop). + x should be a symbol. Both start and stop should be bounded. + + Explanation + =========== + + * If x is not given, x is determined from func. + * If limits is None. Limit of the form (x, -pi, pi) is returned. + + Examples + ======== + + >>> from sympy.series.fourier import _process_limits as pari + >>> from sympy.abc import x + >>> pari(x**2, (x, -2, 2)) + (x, -2, 2) + >>> pari(x**2, (-2, 2)) + (x, -2, 2) + >>> pari(x**2, None) + (x, -pi, pi) + """ + def _find_x(func): + free = func.free_symbols + if len(free) == 1: + return free.pop() + elif not free: + return Dummy('k') + else: + raise ValueError( + " specify dummy variables for %s. If the function contains" + " more than one free symbol, a dummy variable should be" + " supplied explicitly e.g. FourierSeries(m*n**2, (n, -pi, pi))" + % func) + + x, start, stop = None, None, None + if limits is None: + x, start, stop = _find_x(func), -pi, pi + if is_sequence(limits, Tuple): + if len(limits) == 3: + x, start, stop = limits + elif len(limits) == 2: + x = _find_x(func) + start, stop = limits + + if not isinstance(x, Symbol) or start is None or stop is None: + raise ValueError('Invalid limits given: %s' % str(limits)) + + unbounded = [S.NegativeInfinity, S.Infinity] + if start in unbounded or stop in unbounded: + raise ValueError("Both the start and end value should be bounded") + + return sympify((x, start, stop)) + + +def finite_check(f, x, L): + + def check_fx(exprs, x): + return x not in exprs.free_symbols + + def check_sincos(_expr, x, L): + if isinstance(_expr, (sin, cos)): + sincos_args = _expr.args[0] + + if sincos_args.match(a*(pi/L)*x + b) is not None: + return True + else: + return False + + from sympy.simplify.fu import TR2, TR1, sincos_to_sum + _expr = sincos_to_sum(TR2(TR1(f))) + add_coeff = _expr.as_coeff_add() + + a = Wild('a', properties=[lambda k: k.is_Integer, lambda k: k != S.Zero, ]) + b = Wild('b', properties=[lambda k: x not in k.free_symbols, ]) + + for s in add_coeff[1]: + mul_coeffs = s.as_coeff_mul()[1] + for t in mul_coeffs: + if not (check_fx(t, x) or check_sincos(t, x, L)): + return False, f + + return True, _expr + + +class FourierSeries(SeriesBase): + r"""Represents Fourier sine/cosine series. + + Explanation + =========== + + This class only represents a fourier series. + No computation is performed. + + For how to compute Fourier series, see the :func:`fourier_series` + docstring. + + See Also + ======== + + sympy.series.fourier.fourier_series + """ + def __new__(cls, *args): + args = map(sympify, args) + return Expr.__new__(cls, *args) + + @property + def function(self): + return self.args[0] + + @property + def x(self): + return self.args[1][0] + + @property + def period(self): + return (self.args[1][1], self.args[1][2]) + + @property + def a0(self): + return self.args[2][0] + + @property + def an(self): + return self.args[2][1] + + @property + def bn(self): + return self.args[2][2] + + @property + def interval(self): + return Interval(0, oo) + + @property + def start(self): + return self.interval.inf + + @property + def stop(self): + return self.interval.sup + + @property + def length(self): + return oo + + @property + def L(self): + return abs(self.period[1] - self.period[0]) / 2 + + def _eval_subs(self, old, new): + x = self.x + if old.has(x): + return self + + def truncate(self, n=3): + """ + Return the first n nonzero terms of the series. + + If ``n`` is None return an iterator. + + Parameters + ========== + + n : int or None + Amount of non-zero terms in approximation or None. + + Returns + ======= + + Expr or iterator : + Approximation of function expanded into Fourier series. + + Examples + ======== + + >>> from sympy import fourier_series, pi + >>> from sympy.abc import x + >>> s = fourier_series(x, (x, -pi, pi)) + >>> s.truncate(4) + 2*sin(x) - sin(2*x) + 2*sin(3*x)/3 - sin(4*x)/2 + + See Also + ======== + + sympy.series.fourier.FourierSeries.sigma_approximation + """ + if n is None: + return iter(self) + + terms = [] + for t in self: + if len(terms) == n: + break + if t is not S.Zero: + terms.append(t) + + return Add(*terms) + + def sigma_approximation(self, n=3): + r""" + Return :math:`\sigma`-approximation of Fourier series with respect + to order n. + + Explanation + =========== + + Sigma approximation adjusts a Fourier summation to eliminate the Gibbs + phenomenon which would otherwise occur at discontinuities. + A sigma-approximated summation for a Fourier series of a T-periodical + function can be written as + + .. math:: + s(\theta) = \frac{1}{2} a_0 + \sum _{k=1}^{m-1} + \operatorname{sinc} \Bigl( \frac{k}{m} \Bigr) \cdot + \left[ a_k \cos \Bigl( \frac{2\pi k}{T} \theta \Bigr) + + b_k \sin \Bigl( \frac{2\pi k}{T} \theta \Bigr) \right], + + where :math:`a_0, a_k, b_k, k=1,\ldots,{m-1}` are standard Fourier + series coefficients and + :math:`\operatorname{sinc} \Bigl( \frac{k}{m} \Bigr)` is a Lanczos + :math:`\sigma` factor (expressed in terms of normalized + :math:`\operatorname{sinc}` function). + + Parameters + ========== + + n : int + Highest order of the terms taken into account in approximation. + + Returns + ======= + + Expr : + Sigma approximation of function expanded into Fourier series. + + Examples + ======== + + >>> from sympy import fourier_series, pi + >>> from sympy.abc import x + >>> s = fourier_series(x, (x, -pi, pi)) + >>> s.sigma_approximation(4) + 2*sin(x)*sinc(pi/4) - 2*sin(2*x)/pi + 2*sin(3*x)*sinc(3*pi/4)/3 + + See Also + ======== + + sympy.series.fourier.FourierSeries.truncate + + Notes + ===== + + The behaviour of + :meth:`~sympy.series.fourier.FourierSeries.sigma_approximation` + is different from :meth:`~sympy.series.fourier.FourierSeries.truncate` + - it takes all nonzero terms of degree smaller than n, rather than + first n nonzero ones. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Gibbs_phenomenon + .. [2] https://en.wikipedia.org/wiki/Sigma_approximation + """ + terms = [sinc(pi * i / n) * t for i, t in enumerate(self[:n]) + if t is not S.Zero] + return Add(*terms) + + def shift(self, s): + """ + Shift the function by a term independent of x. + + Explanation + =========== + + f(x) -> f(x) + s + + This is fast, if Fourier series of f(x) is already + computed. + + Examples + ======== + + >>> from sympy import fourier_series, pi + >>> from sympy.abc import x + >>> s = fourier_series(x**2, (x, -pi, pi)) + >>> s.shift(1).truncate() + -4*cos(x) + cos(2*x) + 1 + pi**2/3 + """ + s, x = sympify(s), self.x + + if x in s.free_symbols: + raise ValueError("'%s' should be independent of %s" % (s, x)) + + a0 = self.a0 + s + sfunc = self.function + s + + return self.func(sfunc, self.args[1], (a0, self.an, self.bn)) + + def shiftx(self, s): + """ + Shift x by a term independent of x. + + Explanation + =========== + + f(x) -> f(x + s) + + This is fast, if Fourier series of f(x) is already + computed. + + Examples + ======== + + >>> from sympy import fourier_series, pi + >>> from sympy.abc import x + >>> s = fourier_series(x**2, (x, -pi, pi)) + >>> s.shiftx(1).truncate() + -4*cos(x + 1) + cos(2*x + 2) + pi**2/3 + """ + s, x = sympify(s), self.x + + if x in s.free_symbols: + raise ValueError("'%s' should be independent of %s" % (s, x)) + + an = self.an.subs(x, x + s) + bn = self.bn.subs(x, x + s) + sfunc = self.function.subs(x, x + s) + + return self.func(sfunc, self.args[1], (self.a0, an, bn)) + + def scale(self, s): + """ + Scale the function by a term independent of x. + + Explanation + =========== + + f(x) -> s * f(x) + + This is fast, if Fourier series of f(x) is already + computed. + + Examples + ======== + + >>> from sympy import fourier_series, pi + >>> from sympy.abc import x + >>> s = fourier_series(x**2, (x, -pi, pi)) + >>> s.scale(2).truncate() + -8*cos(x) + 2*cos(2*x) + 2*pi**2/3 + """ + s, x = sympify(s), self.x + + if x in s.free_symbols: + raise ValueError("'%s' should be independent of %s" % (s, x)) + + an = self.an.coeff_mul(s) + bn = self.bn.coeff_mul(s) + a0 = self.a0 * s + sfunc = self.args[0] * s + + return self.func(sfunc, self.args[1], (a0, an, bn)) + + def scalex(self, s): + """ + Scale x by a term independent of x. + + Explanation + =========== + + f(x) -> f(s*x) + + This is fast, if Fourier series of f(x) is already + computed. + + Examples + ======== + + >>> from sympy import fourier_series, pi + >>> from sympy.abc import x + >>> s = fourier_series(x**2, (x, -pi, pi)) + >>> s.scalex(2).truncate() + -4*cos(2*x) + cos(4*x) + pi**2/3 + """ + s, x = sympify(s), self.x + + if x in s.free_symbols: + raise ValueError("'%s' should be independent of %s" % (s, x)) + + an = self.an.subs(x, x * s) + bn = self.bn.subs(x, x * s) + sfunc = self.function.subs(x, x * s) + + return self.func(sfunc, self.args[1], (self.a0, an, bn)) + + def _eval_as_leading_term(self, x, logx, cdir): + for t in self: + if t is not S.Zero: + return t + + def _eval_term(self, pt): + if pt == 0: + return self.a0 + return self.an.coeff(pt) + self.bn.coeff(pt) + + def __neg__(self): + return self.scale(-1) + + def __add__(self, other): + if isinstance(other, FourierSeries): + if self.period != other.period: + raise ValueError("Both the series should have same periods") + + x, y = self.x, other.x + function = self.function + other.function.subs(y, x) + + if self.x not in function.free_symbols: + return function + + an = self.an + other.an + bn = self.bn + other.bn + a0 = self.a0 + other.a0 + + return self.func(function, self.args[1], (a0, an, bn)) + + return Add(self, other) + + def __sub__(self, other): + return self.__add__(-other) + + +class FiniteFourierSeries(FourierSeries): + r"""Represents Finite Fourier sine/cosine series. + + For how to compute Fourier series, see the :func:`fourier_series` + docstring. + + Parameters + ========== + + f : Expr + Expression for finding fourier_series + + limits : ( x, start, stop) + x is the independent variable for the expression f + (start, stop) is the period of the fourier series + + exprs: (a0, an, bn) or Expr + a0 is the constant term a0 of the fourier series + an is a dictionary of coefficients of cos terms + an[k] = coefficient of cos(pi*(k/L)*x) + bn is a dictionary of coefficients of sin terms + bn[k] = coefficient of sin(pi*(k/L)*x) + + or exprs can be an expression to be converted to fourier form + + Methods + ======= + + This class is an extension of FourierSeries class. + Please refer to sympy.series.fourier.FourierSeries for + further information. + + See Also + ======== + + sympy.series.fourier.FourierSeries + sympy.series.fourier.fourier_series + """ + + def __new__(cls, f, limits, exprs): + f = sympify(f) + limits = sympify(limits) + exprs = sympify(exprs) + + if not (isinstance(exprs, Tuple) and len(exprs) == 3): # exprs is not of form (a0, an, bn) + # Converts the expression to fourier form + c, e = exprs.as_coeff_add() + from sympy.simplify.fu import TR10 + rexpr = c + Add(*[TR10(i) for i in e]) + a0, exp_ls = rexpr.expand(trig=False, power_base=False, power_exp=False, log=False).as_coeff_add() + + x = limits[0] + L = abs(limits[2] - limits[1]) / 2 + + a = Wild('a', properties=[lambda k: k.is_Integer, lambda k: k is not S.Zero, ]) + b = Wild('b', properties=[lambda k: x not in k.free_symbols, ]) + + an = {} + bn = {} + + # separates the coefficients of sin and cos terms in dictionaries an, and bn + for p in exp_ls: + t = p.match(b * cos(a * (pi / L) * x)) + q = p.match(b * sin(a * (pi / L) * x)) + if t: + an[t[a]] = t[b] + an.get(t[a], S.Zero) + elif q: + bn[q[a]] = q[b] + bn.get(q[a], S.Zero) + else: + a0 += p + + exprs = Tuple(a0, an, bn) + + return Expr.__new__(cls, f, limits, exprs) + + @property + def interval(self): + _length = 1 if self.a0 else 0 + _length += max(set(self.an.keys()).union(set(self.bn.keys()))) + 1 + return Interval(0, _length) + + @property + def length(self): + return self.stop - self.start + + def shiftx(self, s): + s, x = sympify(s), self.x + + if x in s.free_symbols: + raise ValueError("'%s' should be independent of %s" % (s, x)) + + _expr = self.truncate().subs(x, x + s) + sfunc = self.function.subs(x, x + s) + + return self.func(sfunc, self.args[1], _expr) + + def scale(self, s): + s, x = sympify(s), self.x + + if x in s.free_symbols: + raise ValueError("'%s' should be independent of %s" % (s, x)) + + _expr = self.truncate() * s + sfunc = self.function * s + + return self.func(sfunc, self.args[1], _expr) + + def scalex(self, s): + s, x = sympify(s), self.x + + if x in s.free_symbols: + raise ValueError("'%s' should be independent of %s" % (s, x)) + + _expr = self.truncate().subs(x, x * s) + sfunc = self.function.subs(x, x * s) + + return self.func(sfunc, self.args[1], _expr) + + def _eval_term(self, pt): + if pt == 0: + return self.a0 + + _term = self.an.get(pt, S.Zero) * cos(pt * (pi / self.L) * self.x) \ + + self.bn.get(pt, S.Zero) * sin(pt * (pi / self.L) * self.x) + return _term + + def __add__(self, other): + if isinstance(other, FourierSeries): + return other.__add__(fourier_series(self.function, self.args[1],\ + finite=False)) + elif isinstance(other, FiniteFourierSeries): + if self.period != other.period: + raise ValueError("Both the series should have same periods") + + x, y = self.x, other.x + function = self.function + other.function.subs(y, x) + + if self.x not in function.free_symbols: + return function + + return fourier_series(function, limits=self.args[1]) + + +def fourier_series(f, limits=None, finite=True): + r"""Computes the Fourier trigonometric series expansion. + + Explanation + =========== + + Fourier trigonometric series of $f(x)$ over the interval $(a, b)$ + is defined as: + + .. math:: + \frac{a_0}{2} + \sum_{n=1}^{\infty} + (a_n \cos(\frac{2n \pi x}{L}) + b_n \sin(\frac{2n \pi x}{L})) + + where the coefficients are: + + .. math:: + L = b - a + + .. math:: + a_0 = \frac{2}{L} \int_{a}^{b}{f(x) dx} + + .. math:: + a_n = \frac{2}{L} \int_{a}^{b}{f(x) \cos(\frac{2n \pi x}{L}) dx} + + .. math:: + b_n = \frac{2}{L} \int_{a}^{b}{f(x) \sin(\frac{2n \pi x}{L}) dx} + + The condition whether the function $f(x)$ given should be periodic + or not is more than necessary, because it is sufficient to consider + the series to be converging to $f(x)$ only in the given interval, + not throughout the whole real line. + + This also brings a lot of ease for the computation because + you do not have to make $f(x)$ artificially periodic by + wrapping it with piecewise, modulo operations, + but you can shape the function to look like the desired periodic + function only in the interval $(a, b)$, and the computed series will + automatically become the series of the periodic version of $f(x)$. + + This property is illustrated in the examples section below. + + Parameters + ========== + + limits : (sym, start, end), optional + *sym* denotes the symbol the series is computed with respect to. + + *start* and *end* denotes the start and the end of the interval + where the fourier series converges to the given function. + + Default range is specified as $-\pi$ and $\pi$. + + Returns + ======= + + FourierSeries + A symbolic object representing the Fourier trigonometric series. + + Examples + ======== + + Computing the Fourier series of $f(x) = x^2$: + + >>> from sympy import fourier_series, pi + >>> from sympy.abc import x + >>> f = x**2 + >>> s = fourier_series(f, (x, -pi, pi)) + >>> s1 = s.truncate(n=3) + >>> s1 + -4*cos(x) + cos(2*x) + pi**2/3 + + Shifting of the Fourier series: + + >>> s.shift(1).truncate() + -4*cos(x) + cos(2*x) + 1 + pi**2/3 + >>> s.shiftx(1).truncate() + -4*cos(x + 1) + cos(2*x + 2) + pi**2/3 + + Scaling of the Fourier series: + + >>> s.scale(2).truncate() + -8*cos(x) + 2*cos(2*x) + 2*pi**2/3 + >>> s.scalex(2).truncate() + -4*cos(2*x) + cos(4*x) + pi**2/3 + + Computing the Fourier series of $f(x) = x$: + + This illustrates how truncating to the higher order gives better + convergence. + + .. plot:: + :context: reset + :format: doctest + :include-source: True + + >>> from sympy import fourier_series, pi, plot + >>> from sympy.abc import x + >>> f = x + >>> s = fourier_series(f, (x, -pi, pi)) + >>> s1 = s.truncate(n = 3) + >>> s2 = s.truncate(n = 5) + >>> s3 = s.truncate(n = 7) + >>> p = plot(f, s1, s2, s3, (x, -pi, pi), show=False, legend=True) + + >>> p[0].line_color = (0, 0, 0) + >>> p[0].label = 'x' + >>> p[1].line_color = (0.7, 0.7, 0.7) + >>> p[1].label = 'n=3' + >>> p[2].line_color = (0.5, 0.5, 0.5) + >>> p[2].label = 'n=5' + >>> p[3].line_color = (0.3, 0.3, 0.3) + >>> p[3].label = 'n=7' + + >>> p.show() + + This illustrates how the series converges to different sawtooth + waves if the different ranges are specified. + + .. plot:: + :context: close-figs + :format: doctest + :include-source: True + + >>> s1 = fourier_series(x, (x, -1, 1)).truncate(10) + >>> s2 = fourier_series(x, (x, -pi, pi)).truncate(10) + >>> s3 = fourier_series(x, (x, 0, 1)).truncate(10) + >>> p = plot(x, s1, s2, s3, (x, -5, 5), show=False, legend=True) + + >>> p[0].line_color = (0, 0, 0) + >>> p[0].label = 'x' + >>> p[1].line_color = (0.7, 0.7, 0.7) + >>> p[1].label = '[-1, 1]' + >>> p[2].line_color = (0.5, 0.5, 0.5) + >>> p[2].label = '[-pi, pi]' + >>> p[3].line_color = (0.3, 0.3, 0.3) + >>> p[3].label = '[0, 1]' + + >>> p.show() + + Notes + ===== + + Computing Fourier series can be slow + due to the integration required in computing + an, bn. + + It is faster to compute Fourier series of a function + by using shifting and scaling on an already + computed Fourier series rather than computing + again. + + e.g. If the Fourier series of ``x**2`` is known + the Fourier series of ``x**2 - 1`` can be found by shifting by ``-1``. + + See Also + ======== + + sympy.series.fourier.FourierSeries + + References + ========== + + .. [1] https://mathworld.wolfram.com/FourierSeries.html + """ + f = sympify(f) + + limits = _process_limits(f, limits) + x = limits[0] + + if x not in f.free_symbols: + return f + + if finite: + L = abs(limits[2] - limits[1]) / 2 + is_finite, res_f = finite_check(f, x, L) + if is_finite: + return FiniteFourierSeries(f, limits, res_f) + + n = Dummy('n') + center = (limits[1] + limits[2]) / 2 + if center.is_zero: + neg_f = f.subs(x, -x) + if f == neg_f: + a0, an = fourier_cos_seq(f, limits, n) + bn = SeqFormula(0, (1, oo)) + return FourierSeries(f, limits, (a0, an, bn)) + elif f == -neg_f: + a0 = S.Zero + an = SeqFormula(0, (1, oo)) + bn = fourier_sin_seq(f, limits, n) + return FourierSeries(f, limits, (a0, an, bn)) + a0, an = fourier_cos_seq(f, limits, n) + bn = fourier_sin_seq(f, limits, n) + return FourierSeries(f, limits, (a0, an, bn)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/gruntz.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/gruntz.py new file mode 100644 index 0000000000000000000000000000000000000000..20ba3150e9918384141e755a39f5bacb0e76db55 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/gruntz.py @@ -0,0 +1,701 @@ +""" +Limits +====== + +Implemented according to the PhD thesis +https://www.cybertester.com/data/gruntz.pdf, which contains very thorough +descriptions of the algorithm including many examples. We summarize here +the gist of it. + +All functions are sorted according to how rapidly varying they are at +infinity using the following rules. Any two functions f and g can be +compared using the properties of L: + +L=lim log|f(x)| / log|g(x)| (for x -> oo) + +We define >, < ~ according to:: + + 1. f > g .... L=+-oo + + we say that: + - f is greater than any power of g + - f is more rapidly varying than g + - f goes to infinity/zero faster than g + + 2. f < g .... L=0 + + we say that: + - f is lower than any power of g + + 3. f ~ g .... L!=0, +-oo + + we say that: + - both f and g are bounded from above and below by suitable integral + powers of the other + +Examples +======== +:: + 2 < x < exp(x) < exp(x**2) < exp(exp(x)) + 2 ~ 3 ~ -5 + x ~ x**2 ~ x**3 ~ 1/x ~ x**m ~ -x + exp(x) ~ exp(-x) ~ exp(2x) ~ exp(x)**2 ~ exp(x+exp(-x)) + f ~ 1/f + +So we can divide all the functions into comparability classes (x and x^2 +belong to one class, exp(x) and exp(-x) belong to some other class). In +principle, we could compare any two functions, but in our algorithm, we +do not compare anything below the class 2~3~-5 (for example log(x) is +below this), so we set 2~3~-5 as the lowest comparability class. + +Given the function f, we find the list of most rapidly varying (mrv set) +subexpressions of it. This list belongs to the same comparability class. +Let's say it is {exp(x), exp(2x)}. Using the rule f ~ 1/f we find an +element "w" (either from the list or a new one) from the same +comparability class which goes to zero at infinity. In our example we +set w=exp(-x) (but we could also set w=exp(-2x) or w=exp(-3x) ...). We +rewrite the mrv set using w, in our case {1/w, 1/w^2}, and substitute it +into f. Then we expand f into a series in w:: + + f = c0*w^e0 + c1*w^e1 + ... + O(w^en), where e0oo, lim f = lim c0*w^e0, because all the other terms go to zero, +because w goes to zero faster than the ci and ei. So:: + + for e0>0, lim f = 0 + for e0<0, lim f = +-oo (the sign depends on the sign of c0) + for e0=0, lim f = lim c0 + +We need to recursively compute limits at several places of the algorithm, but +as is shown in the PhD thesis, it always finishes. + +Important functions from the implementation: + +compare(a, b, x) compares "a" and "b" by computing the limit L. +mrv(e, x) returns list of most rapidly varying (mrv) subexpressions of "e" +rewrite(e, Omega, x, wsym) rewrites "e" in terms of w +leadterm(f, x) returns the lowest power term in the series of f +mrv_leadterm(e, x) returns the lead term (c0, e0) for e +limitinf(e, x) computes lim e (for x->oo) +limit(e, z, z0) computes any limit by converting it to the case x->oo + +All the functions are really simple and straightforward except +rewrite(), which is the most difficult/complex part of the algorithm. +When the algorithm fails, the bugs are usually in the series expansion +(i.e. in SymPy) or in rewrite. + +This code is almost exact rewrite of the Maple code inside the Gruntz +thesis. + +Debugging +--------- + +Because the gruntz algorithm is highly recursive, it's difficult to +figure out what went wrong inside a debugger. Instead, turn on nice +debug prints by defining the environment variable SYMPY_DEBUG. For +example: + +[user@localhost]: SYMPY_DEBUG=True ./bin/isympy + +In [1]: limit(sin(x)/x, x, 0) +limitinf(_x*sin(1/_x), _x) = 1 ++-mrv_leadterm(_x*sin(1/_x), _x) = (1, 0) +| +-mrv(_x*sin(1/_x), _x) = set([_x]) +| | +-mrv(_x, _x) = set([_x]) +| | +-mrv(sin(1/_x), _x) = set([_x]) +| | +-mrv(1/_x, _x) = set([_x]) +| | +-mrv(_x, _x) = set([_x]) +| +-mrv_leadterm(exp(_x)*sin(exp(-_x)), _x, set([exp(_x)])) = (1, 0) +| +-rewrite(exp(_x)*sin(exp(-_x)), set([exp(_x)]), _x, _w) = (1/_w*sin(_w), -_x) +| +-sign(_x, _x) = 1 +| +-mrv_leadterm(1, _x) = (1, 0) ++-sign(0, _x) = 0 ++-limitinf(1, _x) = 1 + +And check manually which line is wrong. Then go to the source code and +debug this function to figure out the exact problem. + +""" +from functools import reduce + +from sympy.core import Basic, S, Mul, PoleError +from sympy.core.cache import cacheit +from sympy.core.function import AppliedUndef +from sympy.core.intfunc import ilcm +from sympy.core.numbers import I, oo +from sympy.core.symbol import Dummy, Wild +from sympy.core.traversal import bottom_up + +from sympy.functions import log, exp, sign as _sign +from sympy.series.order import Order +from sympy.utilities.misc import debug_decorator as debug +from sympy.utilities.timeutils import timethis + +timeit = timethis('gruntz') + + +def compare(a, b, x): + """Returns "<" if a" for a>b""" + # log(exp(...)) must always be simplified here for termination + la, lb = log(a), log(b) + if isinstance(a, Basic) and (isinstance(a, exp) or (a.is_Pow and a.base == S.Exp1)): + la = a.exp + if isinstance(b, Basic) and (isinstance(b, exp) or (b.is_Pow and b.base == S.Exp1)): + lb = b.exp + + c = limitinf(la/lb, x) + if c == 0: + return "<" + elif c.is_infinite: + return ">" + else: + return "=" + + +class SubsSet(dict): + """ + Stores (expr, dummy) pairs, and how to rewrite expr-s. + + Explanation + =========== + + The gruntz algorithm needs to rewrite certain expressions in term of a new + variable w. We cannot use subs, because it is just too smart for us. For + example:: + + > Omega=[exp(exp(_p - exp(-_p))/(1 - 1/_p)), exp(exp(_p))] + > O2=[exp(-exp(_p) + exp(-exp(-_p))*exp(_p)/(1 - 1/_p))/_w, 1/_w] + > e = exp(exp(_p - exp(-_p))/(1 - 1/_p)) - exp(exp(_p)) + > e.subs(Omega[0],O2[0]).subs(Omega[1],O2[1]) + -1/w + exp(exp(p)*exp(-exp(-p))/(1 - 1/p)) + + is really not what we want! + + So we do it the hard way and keep track of all the things we potentially + want to substitute by dummy variables. Consider the expression:: + + exp(x - exp(-x)) + exp(x) + x. + + The mrv set is {exp(x), exp(-x), exp(x - exp(-x))}. + We introduce corresponding dummy variables d1, d2, d3 and rewrite:: + + d3 + d1 + x. + + This class first of all keeps track of the mapping expr->variable, i.e. + will at this stage be a dictionary:: + + {exp(x): d1, exp(-x): d2, exp(x - exp(-x)): d3}. + + [It turns out to be more convenient this way round.] + But sometimes expressions in the mrv set have other expressions from the + mrv set as subexpressions, and we need to keep track of that as well. In + this case, d3 is really exp(x - d2), so rewrites at this stage is:: + + {d3: exp(x-d2)}. + + The function rewrite uses all this information to correctly rewrite our + expression in terms of w. In this case w can be chosen to be exp(-x), + i.e. d2. The correct rewriting then is:: + + exp(-w)/w + 1/w + x. + """ + def __init__(self): + self.rewrites = {} + + def __repr__(self): + return super().__repr__() + ', ' + self.rewrites.__repr__() + + def __getitem__(self, key): + if key not in self: + self[key] = Dummy() + return dict.__getitem__(self, key) + + def do_subs(self, e): + """Substitute the variables with expressions""" + for expr, var in self.items(): + e = e.xreplace({var: expr}) + return e + + def meets(self, s2): + """Tell whether or not self and s2 have non-empty intersection""" + return set(self.keys()).intersection(list(s2.keys())) != set() + + def union(self, s2, exps=None): + """Compute the union of self and s2, adjusting exps""" + res = self.copy() + tr = {} + for expr, var in s2.items(): + if expr in self: + if exps: + exps = exps.xreplace({var: res[expr]}) + tr[var] = res[expr] + else: + res[expr] = var + for var, rewr in s2.rewrites.items(): + res.rewrites[var] = rewr.xreplace(tr) + return res, exps + + def copy(self): + """Create a shallow copy of SubsSet""" + r = SubsSet() + r.rewrites = self.rewrites.copy() + for expr, var in self.items(): + r[expr] = var + return r + + +@debug +def mrv(e, x): + """Returns a SubsSet of most rapidly varying (mrv) subexpressions of 'e', + and e rewritten in terms of these""" + from sympy.simplify.powsimp import powsimp + e = powsimp(e, deep=True, combine='exp') + if not isinstance(e, Basic): + raise TypeError("e should be an instance of Basic") + if not e.has(x): + return SubsSet(), e + elif e == x: + s = SubsSet() + return s, s[x] + elif e.is_Mul or e.is_Add: + i, d = e.as_independent(x) # throw away x-independent terms + if d.func != e.func: + s, expr = mrv(d, x) + return s, e.func(i, expr) + a, b = d.as_two_terms() + s1, e1 = mrv(a, x) + s2, e2 = mrv(b, x) + return mrv_max1(s1, s2, e.func(i, e1, e2), x) + elif e.is_Pow and e.base != S.Exp1: + e1 = S.One + while e.is_Pow: + b1 = e.base + e1 *= e.exp + e = b1 + if b1 == 1: + return SubsSet(), b1 + if e1.has(x): + return mrv(exp(e1*log(b1)), x) + else: + s, expr = mrv(b1, x) + return s, expr**e1 + elif isinstance(e, log): + s, expr = mrv(e.args[0], x) + return s, log(expr) + elif isinstance(e, exp) or (e.is_Pow and e.base == S.Exp1): + # We know from the theory of this algorithm that exp(log(...)) may always + # be simplified here, and doing so is vital for termination. + if isinstance(e.exp, log): + return mrv(e.exp.args[0], x) + # if a product has an infinite factor the result will be + # infinite if there is no zero, otherwise NaN; here, we + # consider the result infinite if any factor is infinite + li = limitinf(e.exp, x) + if any(_.is_infinite for _ in Mul.make_args(li)): + s1 = SubsSet() + e1 = s1[e] + s2, e2 = mrv(e.exp, x) + su = s1.union(s2)[0] + su.rewrites[e1] = exp(e2) + return mrv_max3(s1, e1, s2, exp(e2), su, e1, x) + else: + s, expr = mrv(e.exp, x) + return s, exp(expr) + elif isinstance(e, AppliedUndef): + raise ValueError("MRV set computation for UndefinedFunction is not allowed") + elif e.is_Function: + l = [mrv(a, x) for a in e.args] + l2 = [s for (s, _) in l if s != SubsSet()] + if len(l2) != 1: + # e.g. something like BesselJ(x, x) + raise NotImplementedError("MRV set computation for functions in" + " several variables not implemented.") + s, ss = l2[0], SubsSet() + args = [ss.do_subs(x[1]) for x in l] + return s, e.func(*args) + elif e.is_Derivative: + raise NotImplementedError("MRV set computation for derivatives" + " not implemented yet.") + raise NotImplementedError( + "Don't know how to calculate the mrv of '%s'" % e) + + +def mrv_max3(f, expsf, g, expsg, union, expsboth, x): + """ + Computes the maximum of two sets of expressions f and g, which + are in the same comparability class, i.e. max() compares (two elements of) + f and g and returns either (f, expsf) [if f is larger], (g, expsg) + [if g is larger] or (union, expsboth) [if f, g are of the same class]. + """ + if not isinstance(f, SubsSet): + raise TypeError("f should be an instance of SubsSet") + if not isinstance(g, SubsSet): + raise TypeError("g should be an instance of SubsSet") + if f == SubsSet(): + return g, expsg + elif g == SubsSet(): + return f, expsf + elif f.meets(g): + return union, expsboth + + c = compare(list(f.keys())[0], list(g.keys())[0], x) + if c == ">": + return f, expsf + elif c == "<": + return g, expsg + else: + if c != "=": + raise ValueError("c should be =") + return union, expsboth + + +def mrv_max1(f, g, exps, x): + """Computes the maximum of two sets of expressions f and g, which + are in the same comparability class, i.e. mrv_max1() compares (two elements of) + f and g and returns the set, which is in the higher comparability class + of the union of both, if they have the same order of variation. + Also returns exps, with the appropriate substitutions made. + """ + u, b = f.union(g, exps) + return mrv_max3(f, g.do_subs(exps), g, f.do_subs(exps), + u, b, x) + + +@debug +@cacheit +@timeit +def sign(e, x): + """ + Returns a sign of an expression e(x) for x->oo. + + :: + + e > 0 for x sufficiently large ... 1 + e == 0 for x sufficiently large ... 0 + e < 0 for x sufficiently large ... -1 + + The result of this function is currently undefined if e changes sign + arbitrarily often for arbitrarily large x (e.g. sin(x)). + + Note that this returns zero only if e is *constantly* zero + for x sufficiently large. [If e is constant, of course, this is just + the same thing as the sign of e.] + """ + if not isinstance(e, Basic): + raise TypeError("e should be an instance of Basic") + + if e.is_positive: + return 1 + elif e.is_negative: + return -1 + elif e.is_zero: + return 0 + + elif not e.has(x): + from sympy.simplify import logcombine + e = logcombine(e) + return _sign(e) + elif e == x: + return 1 + elif e.is_Mul: + a, b = e.as_two_terms() + sa = sign(a, x) + if not sa: + return 0 + return sa * sign(b, x) + elif isinstance(e, exp): + return 1 + elif e.is_Pow: + if e.base == S.Exp1: + return 1 + s = sign(e.base, x) + if s == 1: + return 1 + if e.exp.is_Integer: + return s**e.exp + elif isinstance(e, log) and e.args[0].is_positive: + return sign(e.args[0] - 1, x) + + # if all else fails, do it the hard way + c0, e0 = mrv_leadterm(e, x) + return sign(c0, x) + + +@debug +@timeit +@cacheit +def limitinf(e, x): + """Limit e(x) for x-> oo.""" + # rewrite e in terms of tractable functions only + + old = e + if not e.has(x): + return e # e is a constant + from sympy.simplify.powsimp import powdenest + from sympy.calculus.util import AccumBounds + if e.has(Order): + e = e.expand().removeO() + if not x.is_positive or x.is_integer: + # We make sure that x.is_positive is True and x.is_integer is None + # so we get all the correct mathematical behavior from the expression. + # We need a fresh variable. + p = Dummy('p', positive=True) + e = e.subs(x, p) + x = p + e = e.rewrite('tractable', deep=True, limitvar=x) + e = powdenest(e) + if isinstance(e, AccumBounds): + if mrv_leadterm(e.min, x) != mrv_leadterm(e.max, x): + raise NotImplementedError + c0, e0 = mrv_leadterm(e.min, x) + else: + c0, e0 = mrv_leadterm(e, x) + sig = sign(e0, x) + if sig == 1: + return S.Zero # e0>0: lim f = 0 + elif sig == -1: # e0<0: lim f = +-oo (the sign depends on the sign of c0) + if c0.match(I*Wild("a", exclude=[I])): + return c0*oo + s = sign(c0, x) + # the leading term shouldn't be 0: + if s == 0: + raise ValueError("Leading term should not be 0") + return s*oo + elif sig == 0: + if c0 == old: + c0 = c0.cancel() + return limitinf(c0, x) # e0=0: lim f = lim c0 + else: + raise ValueError("{} could not be evaluated".format(sig)) + + +def moveup2(s, x): + r = SubsSet() + for expr, var in s.items(): + r[expr.xreplace({x: exp(x)})] = var + for var, expr in s.rewrites.items(): + r.rewrites[var] = s.rewrites[var].xreplace({x: exp(x)}) + return r + + +def moveup(l, x): + return [e.xreplace({x: exp(x)}) for e in l] + + +@debug +@timeit +@cacheit +def mrv_leadterm(e, x): + """Returns (c0, e0) for e.""" + Omega = SubsSet() + if not e.has(x): + return (e, S.Zero) + if Omega == SubsSet(): + Omega, exps = mrv(e, x) + if not Omega: + # e really does not depend on x after simplification + return exps, S.Zero + if x in Omega: + # move the whole omega up (exponentiate each term): + Omega_up = moveup2(Omega, x) + exps_up = moveup([exps], x)[0] + # NOTE: there is no need to move this down! + Omega = Omega_up + exps = exps_up + # + # The positive dummy, w, is used here so log(w*2) etc. will expand; + # a unique dummy is needed in this algorithm + # + # For limits of complex functions, the algorithm would have to be + # improved, or just find limits of Re and Im components separately. + # + w = Dummy("w", positive=True) + f, logw = rewrite(exps, Omega, x, w) + + # Ensure expressions of the form exp(log(...)) don't get simplified automatically in the previous steps. + # see: https://github.com/sympy/sympy/issues/15323#issuecomment-478639399 + f = f.replace(lambda f: f.is_Pow and f.has(x), lambda f: exp(log(f.base)*f.exp)) + + try: + lt = f.leadterm(w, logx=logw) + except (NotImplementedError, PoleError, ValueError): + n0 = 1 + _series = Order(1) + incr = S.One + while _series.is_Order: + _series = f._eval_nseries(w, n=n0+incr, logx=logw) + incr *= 2 + series = _series.expand().removeO() + try: + lt = series.leadterm(w, logx=logw) + except (NotImplementedError, PoleError, ValueError): + lt = f.as_coeff_exponent(w) + if lt[0].has(w): + base = f.as_base_exp()[0].as_coeff_exponent(w) + ex = f.as_base_exp()[1] + lt = (base[0]**ex, base[1]*ex) + return (lt[0].subs(log(w), logw), lt[1]) + + +def build_expression_tree(Omega, rewrites): + r""" Helper function for rewrite. + + We need to sort Omega (mrv set) so that we replace an expression before + we replace any expression in terms of which it has to be rewritten:: + + e1 ---> e2 ---> e3 + \ + -> e4 + + Here we can do e1, e2, e3, e4 or e1, e2, e4, e3. + To do this we assemble the nodes into a tree, and sort them by height. + + This function builds the tree, rewrites then sorts the nodes. + """ + class Node: + def __init__(self): + self.before = [] + self.expr = None + self.var = None + def ht(self): + return reduce(lambda x, y: x + y, + [x.ht() for x in self.before], 1) + nodes = {} + for expr, v in Omega: + n = Node() + n.var = v + n.expr = expr + nodes[v] = n + for _, v in Omega: + if v in rewrites: + n = nodes[v] + r = rewrites[v] + for _, v2 in Omega: + if r.has(v2): + n.before.append(nodes[v2]) + + return nodes + + +@debug +@timeit +def rewrite(e, Omega, x, wsym): + """e(x) ... the function + Omega ... the mrv set + wsym ... the symbol which is going to be used for w + + Returns the rewritten e in terms of w and log(w). See test_rewrite1() + for examples and correct results. + """ + + from sympy import AccumBounds + if not isinstance(Omega, SubsSet): + raise TypeError("Omega should be an instance of SubsSet") + if len(Omega) == 0: + raise ValueError("Length cannot be 0") + # all items in Omega must be exponentials + for t in Omega.keys(): + if not isinstance(t, exp): + raise ValueError("Value should be exp") + rewrites = Omega.rewrites + Omega = list(Omega.items()) + + nodes = build_expression_tree(Omega, rewrites) + Omega.sort(key=lambda x: nodes[x[1]].ht(), reverse=True) + + # make sure we know the sign of each exp() term; after the loop, + # g is going to be the "w" - the simplest one in the mrv set + for g, _ in Omega: + sig = sign(g.exp, x) + if sig != 1 and sig != -1 and not sig.has(AccumBounds): + raise NotImplementedError('Result depends on the sign of %s' % sig) + if sig == 1: + wsym = 1/wsym # if g goes to oo, substitute 1/w + # O2 is a list, which results by rewriting each item in Omega using "w" + O2 = [] + denominators = [] + for f, var in Omega: + c = limitinf(f.exp/g.exp, x) + if c.is_Rational: + denominators.append(c.q) + arg = f.exp + if var in rewrites: + if not isinstance(rewrites[var], exp): + raise ValueError("Value should be exp") + arg = rewrites[var].args[0] + O2.append((var, exp((arg - c*g.exp))*wsym**c)) + + # Remember that Omega contains subexpressions of "e". So now we find + # them in "e" and substitute them for our rewriting, stored in O2 + + # the following powsimp is necessary to automatically combine exponentials, + # so that the .xreplace() below succeeds: + # TODO this should not be necessary + from sympy.simplify.powsimp import powsimp + f = powsimp(e, deep=True, combine='exp') + for a, b in O2: + f = f.xreplace({a: b}) + + for _, var in Omega: + assert not f.has(var) + + # finally compute the logarithm of w (logw). + logw = g.exp + if sig == 1: + logw = -logw # log(w)->log(1/w)=-log(w) + + # Some parts of SymPy have difficulty computing series expansions with + # non-integral exponents. The following heuristic improves the situation: + exponent = reduce(ilcm, denominators, 1) + f = f.subs({wsym: wsym**exponent}) + logw /= exponent + + # bottom_up function is required for a specific case - when f is + # -exp(p/(p + 1)) + exp(-p**2/(p + 1) + p). No current simplification + # methods reduce this to 0 while not expanding polynomials. + f = bottom_up(f, lambda w: getattr(w, 'normal', lambda: w)()) + + return f, logw + + +def gruntz(e, z, z0, dir="+"): + """ + Compute the limit of e(z) at the point z0 using the Gruntz algorithm. + + Explanation + =========== + + ``z0`` can be any expression, including oo and -oo. + + For ``dir="+"`` (default) it calculates the limit from the right + (z->z0+) and for ``dir="-"`` the limit from the left (z->z0-). For infinite z0 + (oo or -oo), the dir argument does not matter. + + This algorithm is fully described in the module docstring in the gruntz.py + file. It relies heavily on the series expansion. Most frequently, gruntz() + is only used if the faster limit() function (which uses heuristics) fails. + """ + if not z.is_symbol: + raise NotImplementedError("Second argument must be a Symbol") + + # convert all limits to the limit z->oo; sign of z is handled in limitinf + r = None + if z0 in (oo, I*oo): + e0 = e + elif z0 in (-oo, -I*oo): + e0 = e.subs(z, -z) + else: + if str(dir) == "-": + e0 = e.subs(z, z0 - 1/z) + elif str(dir) == "+": + e0 = e.subs(z, z0 + 1/z) + else: + raise NotImplementedError("dir must be '+' or '-'") + + r = limitinf(e0, z) + + # This is a bit of a heuristic for nice results... we always rewrite + # tractable functions in terms of familiar intractable ones. + # It might be nicer to rewrite the exactly to what they were initially, + # but that would take some work to implement. + return r.rewrite('intractable', deep=True) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/kauers.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/kauers.py new file mode 100644 index 0000000000000000000000000000000000000000..9e9645ff15ee5ae3c1d1c8709f76aed1b366f50a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/kauers.py @@ -0,0 +1,51 @@ +def finite_diff(expression, variable, increment=1): + """ + Takes as input a polynomial expression and the variable used to construct + it and returns the difference between function's value when the input is + incremented to 1 and the original function value. If you want an increment + other than one supply it as a third argument. + + Examples + ======== + + >>> from sympy.abc import x, y, z + >>> from sympy.series.kauers import finite_diff + >>> finite_diff(x**2, x) + 2*x + 1 + >>> finite_diff(y**3 + 2*y**2 + 3*y + 4, y) + 3*y**2 + 7*y + 6 + >>> finite_diff(x**2 + 3*x + 8, x, 2) + 4*x + 10 + >>> finite_diff(z**3 + 8*z, z, 3) + 9*z**2 + 27*z + 51 + """ + expression = expression.expand() + expression2 = expression.subs(variable, variable + increment) + expression2 = expression2.expand() + return expression2 - expression + +def finite_diff_kauers(sum): + """ + Takes as input a Sum instance and returns the difference between the sum + with the upper index incremented by 1 and the original sum. For example, + if S(n) is a sum, then finite_diff_kauers will return S(n + 1) - S(n). + + Examples + ======== + + >>> from sympy.series.kauers import finite_diff_kauers + >>> from sympy import Sum + >>> from sympy.abc import x, y, m, n, k + >>> finite_diff_kauers(Sum(k, (k, 1, n))) + n + 1 + >>> finite_diff_kauers(Sum(1/k, (k, 1, n))) + 1/(n + 1) + >>> finite_diff_kauers(Sum((x*y**2), (x, 1, n), (y, 1, m))) + (m + 1)**2*(n + 1) + >>> finite_diff_kauers(Sum((x*y), (x, 1, m), (y, 1, n))) + (m + 1)*(n + 1) + """ + function = sum.function + for l in sum.limits: + function = function.subs(l[0], l[- 1] + 1) + return function diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/limits.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/limits.py new file mode 100644 index 0000000000000000000000000000000000000000..e15f7a1243452075a76553903cabf60e43942d8c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/limits.py @@ -0,0 +1,394 @@ +from sympy.calculus.accumulationbounds import AccumBounds +from sympy.core import S, Symbol, Add, sympify, Expr, PoleError, Mul +from sympy.core.exprtools import factor_terms +from sympy.core.numbers import Float, _illegal +from sympy.core.function import AppliedUndef +from sympy.core.symbol import Dummy +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.complexes import (Abs, sign, arg, re) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.special.gamma_functions import gamma +from sympy.polys import PolynomialError, factor +from sympy.series.order import Order +from .gruntz import gruntz + +def limit(e, z, z0, dir="+"): + """Computes the limit of ``e(z)`` at the point ``z0``. + + Parameters + ========== + + e : expression, the limit of which is to be taken + + z : symbol representing the variable in the limit. + Other symbols are treated as constants. Multivariate limits + are not supported. + + z0 : the value toward which ``z`` tends. Can be any expression, + including ``oo`` and ``-oo``. + + dir : string, optional (default: "+") + The limit is bi-directional if ``dir="+-"``, from the right + (z->z0+) if ``dir="+"``, and from the left (z->z0-) if + ``dir="-"``. For infinite ``z0`` (``oo`` or ``-oo``), the ``dir`` + argument is determined from the direction of the infinity + (i.e., ``dir="-"`` for ``oo``). + + Examples + ======== + + >>> from sympy import limit, sin, oo + >>> from sympy.abc import x + >>> limit(sin(x)/x, x, 0) + 1 + >>> limit(1/x, x, 0) # default dir='+' + oo + >>> limit(1/x, x, 0, dir="-") + -oo + >>> limit(1/x, x, 0, dir='+-') + zoo + >>> limit(1/x, x, oo) + 0 + + Notes + ===== + + First we try some heuristics for easy and frequent cases like "x", "1/x", + "x**2" and similar, so that it's fast. For all other cases, we use the + Gruntz algorithm (see the gruntz() function). + + See Also + ======== + + limit_seq : returns the limit of a sequence. + """ + + return Limit(e, z, z0, dir).doit(deep=False) + + +def heuristics(e, z, z0, dir): + """Computes the limit of an expression term-wise. + Parameters are the same as for the ``limit`` function. + Works with the arguments of expression ``e`` one by one, computing + the limit of each and then combining the results. This approach + works only for simple limits, but it is fast. + """ + + rv = None + if z0 is S.Infinity: + rv = limit(e.subs(z, 1/z), z, S.Zero, "+") + if isinstance(rv, Limit): + return + elif (e.is_Mul or e.is_Add or e.is_Pow or (e.is_Function and not isinstance(e, AppliedUndef))): + r = [] + from sympy.simplify.simplify import together + for a in e.args: + l = limit(a, z, z0, dir) + if l.has(S.Infinity) and l.is_finite is None: + if isinstance(e, Add): + m = factor_terms(e) + if not isinstance(m, Mul): # try together + m = together(m) + if not isinstance(m, Mul): # try factor if the previous methods failed + m = factor(e) + if isinstance(m, Mul): + return heuristics(m, z, z0, dir) + return + return + elif isinstance(l, Limit): + return + elif l is S.NaN: + return + else: + r.append(l) + if r: + rv = e.func(*r) + if rv is S.NaN and e.is_Mul and any(isinstance(rr, AccumBounds) for rr in r): + r2 = [] + e2 = [] + for ii, rval in enumerate(r): + if isinstance(rval, AccumBounds): + r2.append(rval) + else: + e2.append(e.args[ii]) + + if len(e2) > 0: + e3 = Mul(*e2).simplify() + l = limit(e3, z, z0, dir) + rv = l * Mul(*r2) + + if rv is S.NaN: + try: + from sympy.simplify.ratsimp import ratsimp + rat_e = ratsimp(e) + except PolynomialError: + return + if rat_e is S.NaN or rat_e == e: + return + return limit(rat_e, z, z0, dir) + return rv + + +class Limit(Expr): + """Represents an unevaluated limit. + + Examples + ======== + + >>> from sympy import Limit, sin + >>> from sympy.abc import x + >>> Limit(sin(x)/x, x, 0) + Limit(sin(x)/x, x, 0, dir='+') + >>> Limit(1/x, x, 0, dir="-") + Limit(1/x, x, 0, dir='-') + + """ + + def __new__(cls, e, z, z0, dir="+"): + e = sympify(e) + z = sympify(z) + z0 = sympify(z0) + + if z0 in (S.Infinity, S.ImaginaryUnit*S.Infinity): + dir = "-" + elif z0 in (S.NegativeInfinity, S.ImaginaryUnit*S.NegativeInfinity): + dir = "+" + + if(z0.has(z)): + raise NotImplementedError("Limits approaching a variable point are" + " not supported (%s -> %s)" % (z, z0)) + if isinstance(dir, str): + dir = Symbol(dir) + elif not isinstance(dir, Symbol): + raise TypeError("direction must be of type basestring or " + "Symbol, not %s" % type(dir)) + if str(dir) not in ('+', '-', '+-'): + raise ValueError("direction must be one of '+', '-' " + "or '+-', not %s" % dir) + + obj = Expr.__new__(cls) + obj._args = (e, z, z0, dir) + return obj + + + @property + def free_symbols(self): + e = self.args[0] + isyms = e.free_symbols + isyms.difference_update(self.args[1].free_symbols) + isyms.update(self.args[2].free_symbols) + return isyms + + + def pow_heuristics(self, e): + _, z, z0, _ = self.args + b1, e1 = e.base, e.exp + if not b1.has(z): + res = limit(e1*log(b1), z, z0) + return exp(res) + + ex_lim = limit(e1, z, z0) + base_lim = limit(b1, z, z0) + + if base_lim is S.One: + if ex_lim in (S.Infinity, S.NegativeInfinity): + res = limit(e1*(b1 - 1), z, z0) + return exp(res) + if base_lim is S.NegativeInfinity and ex_lim is S.Infinity: + return S.ComplexInfinity + + + def doit(self, **hints): + """Evaluates the limit. + + Parameters + ========== + + deep : bool, optional (default: True) + Invoke the ``doit`` method of the expressions involved before + taking the limit. + + hints : optional keyword arguments + To be passed to ``doit`` methods; only used if deep is True. + """ + + e, z, z0, dir = self.args + + if str(dir) == '+-': + r = limit(e, z, z0, dir='+') + l = limit(e, z, z0, dir='-') + if isinstance(r, Limit) and isinstance(l, Limit): + if r.args[0] == l.args[0]: + return self + if r == l: + return l + if r.is_infinite and l.is_infinite: + return S.ComplexInfinity + raise ValueError("The limit does not exist since " + "left hand limit = %s and right hand limit = %s" + % (l, r)) + + if z0 is S.ComplexInfinity: + raise NotImplementedError("Limits at complex " + "infinity are not implemented") + + if z0.is_infinite: + cdir = sign(z0) + cdir = cdir/abs(cdir) + e = e.subs(z, cdir*z) + dir = "-" + z0 = S.Infinity + + if hints.get('deep', True): + e = e.doit(**hints) + z = z.doit(**hints) + z0 = z0.doit(**hints) + + if e == z: + return z0 + + if not e.has(z): + return e + + if z0 is S.NaN: + return S.NaN + + if e.has(*_illegal): + return self + + if e.is_Order: + return Order(limit(e.expr, z, z0), *e.args[1:]) + + cdir = S.Zero + if str(dir) == "+": + cdir = S.One + elif str(dir) == "-": + cdir = S.NegativeOne + + def set_signs(expr): + if not expr.args: + return expr + newargs = tuple(set_signs(arg) for arg in expr.args) + if newargs != expr.args: + expr = expr.func(*newargs) + abs_flag = isinstance(expr, Abs) + arg_flag = isinstance(expr, arg) + sign_flag = isinstance(expr, sign) + if abs_flag or sign_flag or arg_flag: + try: + sig = limit(expr.args[0], z, z0, dir) + if sig.is_zero: + sig = limit(1/expr.args[0], z, z0, dir) + except NotImplementedError: + return expr + else: + if sig.is_extended_real: + if (sig < 0) == True: + return (-expr.args[0] if abs_flag else + S.NegativeOne if sign_flag else S.Pi) + elif (sig > 0) == True: + return (expr.args[0] if abs_flag else + S.One if sign_flag else S.Zero) + return expr + + if e.has(Float): + # Convert floats like 0.5 to exact SymPy numbers like S.Half, to + # prevent rounding errors which can lead to unexpected execution + # of conditional blocks that work on comparisons + # Also see comments in https://github.com/sympy/sympy/issues/19453 + from sympy.simplify.simplify import nsimplify + e = nsimplify(e) + e = set_signs(e) + + + if e.is_meromorphic(z, z0): + if z0 is S.Infinity: + newe = e.subs(z, 1/z) + # cdir changes sign as oo- should become 0+ + cdir = -cdir + else: + newe = e.subs(z, z + z0) + try: + coeff, ex = newe.leadterm(z, cdir=cdir) + except ValueError: + pass + else: + if ex > 0: + return S.Zero + elif ex == 0: + return coeff + if cdir == 1 or not(int(ex) & 1): + return S.Infinity*sign(coeff) + elif cdir == -1: + return S.NegativeInfinity*sign(coeff) + else: + return S.ComplexInfinity + + if z0 is S.Infinity: + if e.is_Mul: + e = factor_terms(e) + dummy = Dummy('z', positive=z.is_positive, negative=z.is_negative, real=z.is_real) + newe = e.subs(z, 1/dummy) + # cdir changes sign as oo- should become 0+ + cdir = -cdir + newz = dummy + else: + newe = e.subs(z, z + z0) + newz = z + try: + coeff, ex = newe.leadterm(newz, cdir=cdir) + except (ValueError, NotImplementedError, PoleError): + # The NotImplementedError catching is for custom functions + from sympy.simplify.powsimp import powsimp + e = powsimp(e) + if e.is_Pow: + r = self.pow_heuristics(e) + if r is not None: + return r + try: + coeff = newe.as_leading_term(newz, cdir=cdir) + if coeff != newe and (coeff.has(exp) or coeff.has(S.Exp1)): + return gruntz(coeff, newz, 0, "-" if re(cdir).is_negative else "+") + except (ValueError, NotImplementedError, PoleError): + pass + else: + if isinstance(coeff, AccumBounds) and ex == S.Zero: + return coeff + if coeff.has(S.Infinity, S.NegativeInfinity, S.ComplexInfinity, S.NaN): + return self + if not coeff.has(newz): + if ex.is_positive: + return S.Zero + elif ex == 0: + return coeff + elif ex.is_negative: + if cdir == 1: + return S.Infinity*sign(coeff) + elif cdir == -1: + return S.NegativeInfinity*sign(coeff)*S.NegativeOne**(S.One + ex) + else: + return S.ComplexInfinity + else: + raise NotImplementedError("Not sure of sign of %s" % ex) + + # gruntz fails on factorials but works with the gamma function + # If no factorial term is present, e should remain unchanged. + # factorial is defined to be zero for negative inputs (which + # differs from gamma) so only rewrite for non-negative z0. + if z0.is_extended_nonnegative: + e = e.rewrite(factorial, gamma) + + l = None + + try: + r = gruntz(e, z, z0, dir) + if r is S.NaN or l is S.NaN: + raise PoleError() + except (PoleError, ValueError): + if l is not None: + raise + r = heuristics(e, z, z0, dir) + if r is None: + return self + + return r diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/limitseq.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/limitseq.py new file mode 100644 index 0000000000000000000000000000000000000000..ceac4e7b63bfc09d9dfc26c12c7d2acc8b8d44da --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/limitseq.py @@ -0,0 +1,257 @@ +"""Limits of sequences""" + +from sympy.calculus.accumulationbounds import AccumulationBounds +from sympy.core.add import Add +from sympy.core.function import PoleError +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import Dummy +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.numbers import fibonacci +from sympy.functions.combinatorial.factorials import factorial, subfactorial +from sympy.functions.special.gamma_functions import gamma +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.miscellaneous import Max, Min +from sympy.functions.elementary.trigonometric import cos, sin +from sympy.series.limits import Limit + + +def difference_delta(expr, n=None, step=1): + """Difference Operator. + + Explanation + =========== + + Discrete analog of differential operator. Given a sequence x[n], + returns the sequence x[n + step] - x[n]. + + Examples + ======== + + >>> from sympy import difference_delta as dd + >>> from sympy.abc import n + >>> dd(n*(n + 1), n) + 2*n + 2 + >>> dd(n*(n + 1), n, 2) + 4*n + 6 + + References + ========== + + .. [1] https://reference.wolfram.com/language/ref/DifferenceDelta.html + """ + expr = sympify(expr) + + if n is None: + f = expr.free_symbols + if len(f) == 1: + n = f.pop() + elif len(f) == 0: + return S.Zero + else: + raise ValueError("Since there is more than one variable in the" + " expression, a variable must be supplied to" + " take the difference of %s" % expr) + step = sympify(step) + if step.is_number is False or step.is_finite is False: + raise ValueError("Step should be a finite number.") + + if hasattr(expr, '_eval_difference_delta'): + result = expr._eval_difference_delta(n, step) + if result: + return result + + return expr.subs(n, n + step) - expr + + +def dominant(expr, n): + """Finds the dominant term in a sum, that is a term that dominates + every other term. + + Explanation + =========== + + If limit(a/b, n, oo) is oo then a dominates b. + If limit(a/b, n, oo) is 0 then b dominates a. + Otherwise, a and b are comparable. + + If there is no unique dominant term, then returns ``None``. + + Examples + ======== + + >>> from sympy import Sum + >>> from sympy.series.limitseq import dominant + >>> from sympy.abc import n, k + >>> dominant(5*n**3 + 4*n**2 + n + 1, n) + 5*n**3 + >>> dominant(2**n + Sum(k, (k, 0, n)), n) + 2**n + + See Also + ======== + + sympy.series.limitseq.dominant + """ + terms = Add.make_args(expr.expand(func=True)) + term0 = terms[-1] + comp = [term0] # comparable terms + for t in terms[:-1]: + r = term0/t + e = r.gammasimp() + if e == r: + e = r.factor() + l = limit_seq(e, n) + if l is None: + return None + elif l.is_zero: + term0 = t + comp = [term0] + elif l not in [S.Infinity, S.NegativeInfinity]: + comp.append(t) + if len(comp) > 1: + return None + return term0 + + +def _limit_inf(expr, n): + try: + return Limit(expr, n, S.Infinity).doit(deep=False) + except (NotImplementedError, PoleError): + return None + + +def _limit_seq(expr, n, trials): + from sympy.concrete.summations import Sum + + for i in range(trials): + if not expr.has(Sum): + result = _limit_inf(expr, n) + if result is not None: + return result + + num, den = expr.as_numer_denom() + if not den.has(n) or not num.has(n): + result = _limit_inf(expr.doit(), n) + if result is not None: + return result + return None + + num, den = (difference_delta(t.expand(), n) for t in [num, den]) + expr = (num / den).gammasimp() + + if not expr.has(Sum): + result = _limit_inf(expr, n) + if result is not None: + return result + + num, den = expr.as_numer_denom() + + num = dominant(num, n) + if num is None: + return None + + den = dominant(den, n) + if den is None: + return None + + expr = (num / den).gammasimp() + + +def limit_seq(expr, n=None, trials=5): + """Finds the limit of a sequence as index ``n`` tends to infinity. + + Parameters + ========== + + expr : Expr + SymPy expression for the ``n-th`` term of the sequence + n : Symbol, optional + The index of the sequence, an integer that tends to positive + infinity. If None, inferred from the expression unless it has + multiple symbols. + trials: int, optional + The algorithm is highly recursive. ``trials`` is a safeguard from + infinite recursion in case the limit is not easily computed by the + algorithm. Try increasing ``trials`` if the algorithm returns ``None``. + + Admissible Terms + ================ + + The algorithm is designed for sequences built from rational functions, + indefinite sums, and indefinite products over an indeterminate n. Terms of + alternating sign are also allowed, but more complex oscillatory behavior is + not supported. + + Examples + ======== + + >>> from sympy import limit_seq, Sum, binomial + >>> from sympy.abc import n, k, m + >>> limit_seq((5*n**3 + 3*n**2 + 4) / (3*n**3 + 4*n - 5), n) + 5/3 + >>> limit_seq(binomial(2*n, n) / Sum(binomial(2*k, k), (k, 1, n)), n) + 3/4 + >>> limit_seq(Sum(k**2 * Sum(2**m/m, (m, 1, k)), (k, 1, n)) / (2**n*n), n) + 4 + + See Also + ======== + + sympy.series.limitseq.dominant + + References + ========== + + .. [1] Computing Limits of Sequences - Manuel Kauers + """ + + from sympy.concrete.summations import Sum + if n is None: + free = expr.free_symbols + if len(free) == 1: + n = free.pop() + elif not free: + return expr + else: + raise ValueError("Expression has more than one variable. " + "Please specify a variable.") + elif n not in expr.free_symbols: + return expr + + expr = expr.rewrite(fibonacci, S.GoldenRatio) + expr = expr.rewrite(factorial, subfactorial, gamma) + n_ = Dummy("n", integer=True, positive=True) + n1 = Dummy("n", odd=True, positive=True) + n2 = Dummy("n", even=True, positive=True) + + # If there is a negative term raised to a power involving n, or a + # trigonometric function, then consider even and odd n separately. + powers = (p.as_base_exp() for p in expr.atoms(Pow)) + if (any(b.is_negative and e.has(n) for b, e in powers) or + expr.has(cos, sin)): + L1 = _limit_seq(expr.xreplace({n: n1}), n1, trials) + if L1 is not None: + L2 = _limit_seq(expr.xreplace({n: n2}), n2, trials) + if L1 != L2: + if L1.is_comparable and L2.is_comparable: + return AccumulationBounds(Min(L1, L2), Max(L1, L2)) + else: + return None + else: + L1 = _limit_seq(expr.xreplace({n: n_}), n_, trials) + if L1 is not None: + return L1 + else: + if expr.is_Add: + limits = [limit_seq(term, n, trials) for term in expr.args] + if any(result is None for result in limits): + return None + else: + return Add(*limits) + # Maybe the absolute value is easier to deal with (though not if + # it has a Sum). If it tends to 0, the limit is 0. + elif not expr.has(Sum): + lim = _limit_seq(Abs(expr.xreplace({n: n_})), n_, trials) + if lim is not None and lim.is_zero: + return S.Zero diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/order.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/order.py new file mode 100644 index 0000000000000000000000000000000000000000..9cfd4309c2b7094ce02feab129e5f051c442d8cd --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/order.py @@ -0,0 +1,522 @@ +from sympy.core import S, sympify, Expr, Dummy, Add, Mul +from sympy.core.cache import cacheit +from sympy.core.containers import Tuple +from sympy.core.function import Function, PoleError, expand_power_base, expand_log +from sympy.core.sorting import default_sort_key +from sympy.functions.elementary.exponential import exp, log +from sympy.sets.sets import Complement +from sympy.utilities.iterables import uniq, is_sequence + + +class Order(Expr): + r""" Represents the limiting behavior of some function. + + Explanation + =========== + + The order of a function characterizes the function based on the limiting + behavior of the function as it goes to some limit. Only taking the limit + point to be a number is currently supported. This is expressed in + big O notation [1]_. + + The formal definition for the order of a function `g(x)` about a point `a` + is such that `g(x) = O(f(x))` as `x \rightarrow a` if and only if there + exists a `\delta > 0` and an `M > 0` such that `|g(x)| \leq M|f(x)|` for + `|x-a| < \delta`. This is equivalent to `\limsup_{x \rightarrow a} + |g(x)/f(x)| < \infty`. + + Let's illustrate it on the following example by taking the expansion of + `\sin(x)` about 0: + + .. math :: + \sin(x) = x - x^3/3! + O(x^5) + + where in this case `O(x^5) = x^5/5! - x^7/7! + \cdots`. By the definition + of `O`, there is a `\delta > 0` and an `M` such that: + + .. math :: + |x^5/5! - x^7/7! + ....| <= M|x^5| \text{ for } |x| < \delta + + or by the alternate definition: + + .. math :: + \lim_{x \rightarrow 0} | (x^5/5! - x^7/7! + ....) / x^5| < \infty + + which surely is true, because + + .. math :: + \lim_{x \rightarrow 0} | (x^5/5! - x^7/7! + ....) / x^5| = 1/5! + + + As it is usually used, the order of a function can be intuitively thought + of representing all terms of powers greater than the one specified. For + example, `O(x^3)` corresponds to any terms proportional to `x^3, + x^4,\ldots` and any higher power. For a polynomial, this leaves terms + proportional to `x^2`, `x` and constants. + + Examples + ======== + + >>> from sympy import O, oo, cos, pi + >>> from sympy.abc import x, y + + >>> O(x + x**2) + O(x) + >>> O(x + x**2, (x, 0)) + O(x) + >>> O(x + x**2, (x, oo)) + O(x**2, (x, oo)) + + >>> O(1 + x*y) + O(1, x, y) + >>> O(1 + x*y, (x, 0), (y, 0)) + O(1, x, y) + >>> O(1 + x*y, (x, oo), (y, oo)) + O(x*y, (x, oo), (y, oo)) + + >>> O(1) in O(1, x) + True + >>> O(1, x) in O(1) + False + >>> O(x) in O(1, x) + True + >>> O(x**2) in O(x) + True + + >>> O(x)*x + O(x**2) + >>> O(x) - O(x) + O(x) + >>> O(cos(x)) + O(1) + >>> O(cos(x), (x, pi/2)) + O(x - pi/2, (x, pi/2)) + + References + ========== + + .. [1] `Big O notation `_ + + Notes + ===== + + In ``O(f(x), x)`` the expression ``f(x)`` is assumed to have a leading + term. ``O(f(x), x)`` is automatically transformed to + ``O(f(x).as_leading_term(x),x)``. + + ``O(expr*f(x), x)`` is ``O(f(x), x)`` + + ``O(expr, x)`` is ``O(1)`` + + ``O(0, x)`` is 0. + + Multivariate O is also supported: + + ``O(f(x, y), x, y)`` is transformed to + ``O(f(x, y).as_leading_term(x,y).as_leading_term(y), x, y)`` + + In the multivariate case, it is assumed the limits w.r.t. the various + symbols commute. + + If no symbols are passed then all symbols in the expression are used + and the limit point is assumed to be zero. + + """ + + is_Order = True + + __slots__ = () + + @cacheit + def __new__(cls, expr, *args, **kwargs): + expr = sympify(expr) + + if not args: + if expr.is_Order: + variables = expr.variables + point = expr.point + else: + variables = list(expr.free_symbols) + point = [S.Zero]*len(variables) + else: + args = list(args if is_sequence(args) else [args]) + variables, point = [], [] + if is_sequence(args[0]): + for a in args: + v, p = list(map(sympify, a)) + variables.append(v) + point.append(p) + else: + variables = list(map(sympify, args)) + point = [S.Zero]*len(variables) + + if not all(v.is_symbol for v in variables): + raise TypeError('Variables are not symbols, got %s' % variables) + + if len(list(uniq(variables))) != len(variables): + raise ValueError('Variables are supposed to be unique symbols, got %s' % variables) + + if expr.is_Order: + expr_vp = dict(expr.args[1:]) + new_vp = dict(expr_vp) + vp = dict(zip(variables, point)) + for v, p in vp.items(): + if v in new_vp.keys(): + if p != new_vp[v]: + raise NotImplementedError( + "Mixing Order at different points is not supported.") + else: + new_vp[v] = p + if set(expr_vp.keys()) == set(new_vp.keys()): + return expr + else: + variables = list(new_vp.keys()) + point = [new_vp[v] for v in variables] + + if expr is S.NaN: + return S.NaN + + if any(x in p.free_symbols for x in variables for p in point): + raise ValueError('Got %s as a point.' % point) + + if variables: + if any(p != point[0] for p in point): + raise NotImplementedError( + "Multivariable orders at different points are not supported.") + if point[0] in (S.Infinity, S.Infinity*S.ImaginaryUnit): + s = {k: 1/Dummy() for k in variables} + rs = {1/v: 1/k for k, v in s.items()} + ps = [S.Zero for p in point] + elif point[0] in (S.NegativeInfinity, S.NegativeInfinity*S.ImaginaryUnit): + s = {k: -1/Dummy() for k in variables} + rs = {-1/v: -1/k for k, v in s.items()} + ps = [S.Zero for p in point] + elif point[0] is not S.Zero: + s = {k: Dummy() + point[0] for k in variables} + rs = {(v - point[0]).together(): k - point[0] for k, v in s.items()} + ps = [S.Zero for p in point] + else: + s = () + rs = () + ps = list(point) + + expr = expr.subs(s) + + if expr.is_Add: + expr = expr.factor() + + if s: + args = tuple([r[0] for r in rs.items()]) + else: + args = tuple(variables) + + if len(variables) > 1: + # XXX: better way? We need this expand() to + # workaround e.g: expr = x*(x + y). + # (x*(x + y)).as_leading_term(x, y) currently returns + # x*y (wrong order term!). That's why we want to deal with + # expand()'ed expr (handled in "if expr.is_Add" branch below). + expr = expr.expand() + + old_expr = None + while old_expr != expr: + old_expr = expr + if expr.is_Add: + lst = expr.extract_leading_order(args) + expr = Add(*[f.expr for (e, f) in lst]) + + elif expr: + try: + expr = expr.as_leading_term(*args) + except PoleError: + if isinstance(expr, Function) or\ + all(isinstance(arg, Function) for arg in expr.args): + # It is not possible to simplify an expression + # containing only functions (which raise error on + # call to leading term) further + pass + else: + orders = [] + pts = tuple(zip(args, ps)) + for arg in expr.args: + try: + lt = arg.as_leading_term(*args) + except PoleError: + lt = arg + if lt not in args: + order = Order(lt) + else: + order = Order(lt, *pts) + orders.append(order) + if expr.is_Add: + new_expr = Order(Add(*orders), *pts) + if new_expr.is_Add: + new_expr = Order(Add(*[a.expr for a in new_expr.args]), *pts) + expr = new_expr.expr + elif expr.is_Mul: + expr = Mul(*[a.expr for a in orders]) + elif expr.is_Pow: + e = expr.exp + b = expr.base + expr = exp(e * log(b)) + + # It would probably be better to handle this somewhere + # else. This is needed for a testcase in which there is a + # symbol with the assumptions zero=True. + if expr.is_zero: + expr = S.Zero + else: + expr = expr.as_independent(*args, as_Add=False)[1] + + expr = expand_power_base(expr) + expr = expand_log(expr) + + if len(args) == 1: + # The definition of O(f(x)) symbol explicitly stated that + # the argument of f(x) is irrelevant. That's why we can + # combine some power exponents (only "on top" of the + # expression tree for f(x)), e.g.: + # x**p * (-x)**q -> x**(p+q) for real p, q. + x = args[0] + margs = list(Mul.make_args( + expr.as_independent(x, as_Add=False)[1])) + + for i, t in enumerate(margs): + if t.is_Pow: + b, q = t.args + if b in (x, -x) and q.is_real and not q.has(x): + margs[i] = x**q + elif b.is_Pow and not b.exp.has(x): + b, r = b.args + if b in (x, -x) and r.is_real: + margs[i] = x**(r*q) + elif b.is_Mul and b.args[0] is S.NegativeOne: + b = -b + if b.is_Pow and not b.exp.has(x): + b, r = b.args + if b in (x, -x) and r.is_real: + margs[i] = x**(r*q) + + expr = Mul(*margs) + + expr = expr.subs(rs) + + if expr.is_Order: + expr = expr.expr + + if not expr.has(*variables) and not expr.is_zero: + expr = S.One + + # create Order instance: + vp = dict(zip(variables, point)) + variables.sort(key=default_sort_key) + point = [vp[v] for v in variables] + args = (expr,) + Tuple(*zip(variables, point)) + obj = Expr.__new__(cls, *args) + return obj + + def _eval_nseries(self, x, n, logx, cdir=0): + return self + + @property + def expr(self): + return self.args[0] + + @property + def variables(self): + if self.args[1:]: + return tuple(x[0] for x in self.args[1:]) + else: + return () + + @property + def point(self): + if self.args[1:]: + return tuple(x[1] for x in self.args[1:]) + else: + return () + + @property + def free_symbols(self): + return self.expr.free_symbols | set(self.variables) + + def _eval_power(b, e): + if e.is_Number and e.is_nonnegative: + return b.func(b.expr ** e, *b.args[1:]) + if e == O(1): + return b + return + + def as_expr_variables(self, order_symbols): + if order_symbols is None: + order_symbols = self.args[1:] + else: + if (not all(o[1] == order_symbols[0][1] for o in order_symbols) and + not all(p == self.point[0] for p in self.point)): # pragma: no cover + raise NotImplementedError('Order at points other than 0 ' + 'or oo not supported, got %s as a point.' % self.point) + if order_symbols and order_symbols[0][1] != self.point[0]: + raise NotImplementedError( + "Multiplying Order at different points is not supported.") + order_symbols = dict(order_symbols) + for s, p in dict(self.args[1:]).items(): + if s not in order_symbols.keys(): + order_symbols[s] = p + order_symbols = sorted(order_symbols.items(), key=lambda x: default_sort_key(x[0])) + return self.expr, tuple(order_symbols) + + def removeO(self): + return S.Zero + + def getO(self): + return self + + @cacheit + def contains(self, expr): + r""" + Return True if expr belongs to Order(self.expr, \*self.variables). + Return False if self belongs to expr. + Return None if the inclusion relation cannot be determined + (e.g. when self and expr have different symbols). + """ + expr = sympify(expr) + if expr.is_zero: + return True + if expr is S.NaN: + return False + point = self.point[0] if self.point else S.Zero + if expr.is_Order: + if (any(p != point for p in expr.point) or + any(p != point for p in self.point)): + return None + if expr.expr == self.expr: + # O(1) + O(1), O(1) + O(1, x), etc. + return all(x in self.args[1:] for x in expr.args[1:]) + if expr.expr.is_Add: + return all(self.contains(x) for x in expr.expr.args) + if self.expr.is_Add and point.is_zero: + return any(self.func(x, *self.args[1:]).contains(expr) + for x in self.expr.args) + if self.variables and expr.variables: + common_symbols = tuple( + [s for s in self.variables if s in expr.variables]) + elif self.variables: + common_symbols = self.variables + else: + common_symbols = expr.variables + if not common_symbols: + return None + if (self.expr.is_Pow and len(self.variables) == 1 + and self.variables == expr.variables): + symbol = self.variables[0] + other = expr.expr.as_independent(symbol, as_Add=False)[1] + if (other.is_Pow and other.base == symbol and + self.expr.base == symbol): + if point.is_zero: + rv = (self.expr.exp - other.exp).is_nonpositive + if point.is_infinite: + rv = (self.expr.exp - other.exp).is_nonnegative + if rv is not None: + return rv + + from sympy.simplify.powsimp import powsimp + r = None + ratio = self.expr/expr.expr + ratio = powsimp(ratio, deep=True, combine='exp') + for s in common_symbols: + from sympy.series.limits import Limit + l = Limit(ratio, s, point).doit(heuristics=False) + if not isinstance(l, Limit): + l = l != 0 + else: + l = None + if r is None: + r = l + else: + if r != l: + return + return r + + if self.expr.is_Pow and len(self.variables) == 1: + symbol = self.variables[0] + other = expr.as_independent(symbol, as_Add=False)[1] + if (other.is_Pow and other.base == symbol and + self.expr.base == symbol): + if point.is_zero: + rv = (self.expr.exp - other.exp).is_nonpositive + if point.is_infinite: + rv = (self.expr.exp - other.exp).is_nonnegative + if rv is not None: + return rv + + obj = self.func(expr, *self.args[1:]) + return self.contains(obj) + + def __contains__(self, other): + result = self.contains(other) + if result is None: + raise TypeError('contains did not evaluate to a bool') + return result + + def _eval_subs(self, old, new): + if old in self.variables: + newexpr = self.expr.subs(old, new) + i = self.variables.index(old) + newvars = list(self.variables) + newpt = list(self.point) + if new.is_symbol: + newvars[i] = new + else: + syms = new.free_symbols + if len(syms) == 1 or old in syms: + if old in syms: + var = self.variables[i] + else: + var = syms.pop() + # First, try to substitute self.point in the "new" + # expr to see if this is a fixed point. + # E.g. O(y).subs(y, sin(x)) + from sympy import limit + if new.has(Order) and limit(new.getO().expr, var, new.getO().point[0]) == self.point[i]: + point = new.getO().point[0] + return Order(newexpr, *zip([var], [point])) + else: + point = new.subs(var, self.point[i]) + if point != self.point[i]: + from sympy.solvers.solveset import solveset + d = Dummy() + sol = solveset(old - new.subs(var, d), d) + if isinstance(sol, Complement): + e1 = sol.args[0] + e2 = sol.args[1] + sol = set(e1) - set(e2) + res = [dict(zip((d, ), sol))] + point = d.subs(res[0]).limit(old, self.point[i]) + newvars[i] = var + newpt[i] = point + elif old not in syms: + del newvars[i], newpt[i] + if not syms and new == self.point[i]: + newvars.extend(syms) + newpt.extend([S.Zero]*len(syms)) + else: + return + return Order(newexpr, *zip(newvars, newpt)) + + def _eval_conjugate(self): + expr = self.expr._eval_conjugate() + if expr is not None: + return self.func(expr, *self.args[1:]) + + def _eval_derivative(self, x): + return self.func(self.expr.diff(x), *self.args[1:]) or self + + def _eval_transpose(self): + expr = self.expr._eval_transpose() + if expr is not None: + return self.func(expr, *self.args[1:]) + + def __neg__(self): + return self + +O = Order diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/residues.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/residues.py new file mode 100644 index 0000000000000000000000000000000000000000..a426f9e799bd040eea5124f718c2fa43e5de026b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/residues.py @@ -0,0 +1,73 @@ +""" +This module implements the Residue function and related tools for working +with residues. +""" + +from sympy.core.mul import Mul +from sympy.core.singleton import S +from sympy.core.sympify import sympify +from sympy.utilities.timeutils import timethis + + +@timethis('residue') +def residue(expr, x, x0): + """ + Finds the residue of ``expr`` at the point x=x0. + + The residue is defined as the coefficient of ``1/(x-x0)`` in the power series + expansion about ``x=x0``. + + Examples + ======== + + >>> from sympy import Symbol, residue, sin + >>> x = Symbol("x") + >>> residue(1/x, x, 0) + 1 + >>> residue(1/x**2, x, 0) + 0 + >>> residue(2/sin(x), x, 0) + 2 + + This function is essential for the Residue Theorem [1]. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Residue_theorem + """ + # The current implementation uses series expansion to + # calculate it. A more general implementation is explained in + # the section 5.6 of the Bronstein's book {M. Bronstein: + # Symbolic Integration I, Springer Verlag (2005)}. For purely + # rational functions, the algorithm is much easier. See + # sections 2.4, 2.5, and 2.7 (this section actually gives an + # algorithm for computing any Laurent series coefficient for + # a rational function). The theory in section 2.4 will help to + # understand why the resultant works in the general algorithm. + # For the definition of a resultant, see section 1.4 (and any + # previous sections for more review). + + from sympy.series.order import Order + from sympy.simplify.radsimp import collect + expr = sympify(expr) + if x0 != 0: + expr = expr.subs(x, x + x0) + for n in (0, 1, 2, 4, 8, 16, 32): + s = expr.nseries(x, n=n) + if not s.has(Order) or s.getn() >= 0: + break + s = collect(s.removeO(), x) + if s.is_Add: + args = s.args + else: + args = [s] + res = S.Zero + for arg in args: + c, m = arg.as_coeff_mul(x) + m = Mul(*m) + if not (m in (S.One, x) or (m.is_Pow and m.exp.is_Integer)): + raise NotImplementedError('term of unexpected form: %s' % m) + if m == 1/x: + res += c + return res diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/sequences.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/sequences.py new file mode 100644 index 0000000000000000000000000000000000000000..7787515ddb05afaf34751bf451544935723d0921 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/sequences.py @@ -0,0 +1,1239 @@ +from sympy.core.basic import Basic +from sympy.core.cache import cacheit +from sympy.core.containers import Tuple +from sympy.core.decorators import call_highest_priority +from sympy.core.parameters import global_parameters +from sympy.core.function import AppliedUndef, expand +from sympy.core.mul import Mul +from sympy.core.numbers import Integer +from sympy.core.relational import Eq +from sympy.core.singleton import S, Singleton +from sympy.core.sorting import ordered +from sympy.core.symbol import Dummy, Symbol, Wild +from sympy.core.sympify import sympify +from sympy.matrices import Matrix +from sympy.polys import lcm, factor +from sympy.sets.sets import Interval, Intersection +from sympy.tensor.indexed import Idx +from sympy.utilities.iterables import flatten, is_sequence, iterable + + +############################################################################### +# SEQUENCES # +############################################################################### + + +class SeqBase(Basic): + """Base class for sequences""" + + is_commutative = True + _op_priority = 15 + + @staticmethod + def _start_key(expr): + """Return start (if possible) else S.Infinity. + + adapted from Set._infimum_key + """ + try: + start = expr.start + except NotImplementedError: + start = S.Infinity + return start + + def _intersect_interval(self, other): + """Returns start and stop. + + Takes intersection over the two intervals. + """ + interval = Intersection(self.interval, other.interval) + return interval.inf, interval.sup + + @property + def gen(self): + """Returns the generator for the sequence""" + raise NotImplementedError("(%s).gen" % self) + + @property + def interval(self): + """The interval on which the sequence is defined""" + raise NotImplementedError("(%s).interval" % self) + + @property + def start(self): + """The starting point of the sequence. This point is included""" + raise NotImplementedError("(%s).start" % self) + + @property + def stop(self): + """The ending point of the sequence. This point is included""" + raise NotImplementedError("(%s).stop" % self) + + @property + def length(self): + """Length of the sequence""" + raise NotImplementedError("(%s).length" % self) + + @property + def variables(self): + """Returns a tuple of variables that are bounded""" + return () + + @property + def free_symbols(self): + """ + This method returns the symbols in the object, excluding those + that take on a specific value (i.e. the dummy symbols). + + Examples + ======== + + >>> from sympy import SeqFormula + >>> from sympy.abc import n, m + >>> SeqFormula(m*n**2, (n, 0, 5)).free_symbols + {m} + """ + return ({j for i in self.args for j in i.free_symbols + .difference(self.variables)}) + + @cacheit + def coeff(self, pt): + """Returns the coefficient at point pt""" + if pt < self.start or pt > self.stop: + raise IndexError("Index %s out of bounds %s" % (pt, self.interval)) + return self._eval_coeff(pt) + + def _eval_coeff(self, pt): + raise NotImplementedError("The _eval_coeff method should be added to" + "%s to return coefficient so it is available" + "when coeff calls it." + % self.func) + + def _ith_point(self, i): + """Returns the i'th point of a sequence. + + Explanation + =========== + + If start point is negative infinity, point is returned from the end. + Assumes the first point to be indexed zero. + + Examples + ========= + + >>> from sympy import oo + >>> from sympy.series.sequences import SeqPer + + bounded + + >>> SeqPer((1, 2, 3), (-10, 10))._ith_point(0) + -10 + >>> SeqPer((1, 2, 3), (-10, 10))._ith_point(5) + -5 + + End is at infinity + + >>> SeqPer((1, 2, 3), (0, oo))._ith_point(5) + 5 + + Starts at negative infinity + + >>> SeqPer((1, 2, 3), (-oo, 0))._ith_point(5) + -5 + """ + if self.start is S.NegativeInfinity: + initial = self.stop + else: + initial = self.start + + if self.start is S.NegativeInfinity: + step = -1 + else: + step = 1 + + return initial + i*step + + def _add(self, other): + """ + Should only be used internally. + + Explanation + =========== + + self._add(other) returns a new, term-wise added sequence if self + knows how to add with other, otherwise it returns ``None``. + + ``other`` should only be a sequence object. + + Used within :class:`SeqAdd` class. + """ + return None + + def _mul(self, other): + """ + Should only be used internally. + + Explanation + =========== + + self._mul(other) returns a new, term-wise multiplied sequence if self + knows how to multiply with other, otherwise it returns ``None``. + + ``other`` should only be a sequence object. + + Used within :class:`SeqMul` class. + """ + return None + + def coeff_mul(self, other): + """ + Should be used when ``other`` is not a sequence. Should be + defined to define custom behaviour. + + Examples + ======== + + >>> from sympy import SeqFormula + >>> from sympy.abc import n + >>> SeqFormula(n**2).coeff_mul(2) + SeqFormula(2*n**2, (n, 0, oo)) + + Notes + ===== + + '*' defines multiplication of sequences with sequences only. + """ + return Mul(self, other) + + def __add__(self, other): + """Returns the term-wise addition of 'self' and 'other'. + + ``other`` should be a sequence. + + Examples + ======== + + >>> from sympy import SeqFormula + >>> from sympy.abc import n + >>> SeqFormula(n**2) + SeqFormula(n**3) + SeqFormula(n**3 + n**2, (n, 0, oo)) + """ + if not isinstance(other, SeqBase): + raise TypeError('cannot add sequence and %s' % type(other)) + return SeqAdd(self, other) + + @call_highest_priority('__add__') + def __radd__(self, other): + return self + other + + def __sub__(self, other): + """Returns the term-wise subtraction of ``self`` and ``other``. + + ``other`` should be a sequence. + + Examples + ======== + + >>> from sympy import SeqFormula + >>> from sympy.abc import n + >>> SeqFormula(n**2) - (SeqFormula(n)) + SeqFormula(n**2 - n, (n, 0, oo)) + """ + if not isinstance(other, SeqBase): + raise TypeError('cannot subtract sequence and %s' % type(other)) + return SeqAdd(self, -other) + + @call_highest_priority('__sub__') + def __rsub__(self, other): + return (-self) + other + + def __neg__(self): + """Negates the sequence. + + Examples + ======== + + >>> from sympy import SeqFormula + >>> from sympy.abc import n + >>> -SeqFormula(n**2) + SeqFormula(-n**2, (n, 0, oo)) + """ + return self.coeff_mul(-1) + + def __mul__(self, other): + """Returns the term-wise multiplication of 'self' and 'other'. + + ``other`` should be a sequence. For ``other`` not being a + sequence see :func:`coeff_mul` method. + + Examples + ======== + + >>> from sympy import SeqFormula + >>> from sympy.abc import n + >>> SeqFormula(n**2) * (SeqFormula(n)) + SeqFormula(n**3, (n, 0, oo)) + """ + if not isinstance(other, SeqBase): + raise TypeError('cannot multiply sequence and %s' % type(other)) + return SeqMul(self, other) + + @call_highest_priority('__mul__') + def __rmul__(self, other): + return self * other + + def __iter__(self): + for i in range(self.length): + pt = self._ith_point(i) + yield self.coeff(pt) + + def __getitem__(self, index): + if isinstance(index, int): + index = self._ith_point(index) + return self.coeff(index) + elif isinstance(index, slice): + start, stop = index.start, index.stop + if start is None: + start = 0 + if stop is None: + stop = self.length + return [self.coeff(self._ith_point(i)) for i in + range(start, stop, index.step or 1)] + + def find_linear_recurrence(self,n,d=None,gfvar=None): + r""" + Finds the shortest linear recurrence that satisfies the first n + terms of sequence of order `\leq` ``n/2`` if possible. + If ``d`` is specified, find shortest linear recurrence of order + `\leq` min(d, n/2) if possible. + Returns list of coefficients ``[b(1), b(2), ...]`` corresponding to the + recurrence relation ``x(n) = b(1)*x(n-1) + b(2)*x(n-2) + ...`` + Returns ``[]`` if no recurrence is found. + If gfvar is specified, also returns ordinary generating function as a + function of gfvar. + + Examples + ======== + + >>> from sympy import sequence, sqrt, oo, lucas + >>> from sympy.abc import n, x, y + >>> sequence(n**2).find_linear_recurrence(10, 2) + [] + >>> sequence(n**2).find_linear_recurrence(10) + [3, -3, 1] + >>> sequence(2**n).find_linear_recurrence(10) + [2] + >>> sequence(23*n**4+91*n**2).find_linear_recurrence(10) + [5, -10, 10, -5, 1] + >>> sequence(sqrt(5)*(((1 + sqrt(5))/2)**n - (-(1 + sqrt(5))/2)**(-n))/5).find_linear_recurrence(10) + [1, 1] + >>> sequence(x+y*(-2)**(-n), (n, 0, oo)).find_linear_recurrence(30) + [1/2, 1/2] + >>> sequence(3*5**n + 12).find_linear_recurrence(20,gfvar=x) + ([6, -5], 3*(5 - 21*x)/((x - 1)*(5*x - 1))) + >>> sequence(lucas(n)).find_linear_recurrence(15,gfvar=x) + ([1, 1], (x - 2)/(x**2 + x - 1)) + """ + from sympy.simplify import simplify + x = [simplify(expand(t)) for t in self[:n]] + lx = len(x) + if d is None: + r = lx//2 + else: + r = min(d,lx//2) + coeffs = [] + for l in range(1, r+1): + l2 = 2*l + mlist = [] + for k in range(l): + mlist.append(x[k:k+l]) + m = Matrix(mlist) + if m.det() != 0: + y = simplify(m.LUsolve(Matrix(x[l:l2]))) + if lx == l2: + coeffs = flatten(y[::-1]) + break + mlist = [] + for k in range(l,lx-l): + mlist.append(x[k:k+l]) + m = Matrix(mlist) + if m*y == Matrix(x[l2:]): + coeffs = flatten(y[::-1]) + break + if gfvar is None: + return coeffs + else: + l = len(coeffs) + if l == 0: + return [], None + else: + n, d = x[l-1]*gfvar**(l-1), 1 - coeffs[l-1]*gfvar**l + for i in range(l-1): + n += x[i]*gfvar**i + for j in range(l-i-1): + n -= coeffs[i]*x[j]*gfvar**(i+j+1) + d -= coeffs[i]*gfvar**(i+1) + return coeffs, simplify(factor(n)/factor(d)) + +class EmptySequence(SeqBase, metaclass=Singleton): + """Represents an empty sequence. + + The empty sequence is also available as a singleton as + ``S.EmptySequence``. + + Examples + ======== + + >>> from sympy import EmptySequence, SeqPer + >>> from sympy.abc import x + >>> EmptySequence + EmptySequence + >>> SeqPer((1, 2), (x, 0, 10)) + EmptySequence + SeqPer((1, 2), (x, 0, 10)) + >>> SeqPer((1, 2)) * EmptySequence + EmptySequence + >>> EmptySequence.coeff_mul(-1) + EmptySequence + """ + + @property + def interval(self): + return S.EmptySet + + @property + def length(self): + return S.Zero + + def coeff_mul(self, coeff): + """See docstring of SeqBase.coeff_mul""" + return self + + def __iter__(self): + return iter([]) + + +class SeqExpr(SeqBase): + """Sequence expression class. + + Various sequences should inherit from this class. + + Examples + ======== + + >>> from sympy.series.sequences import SeqExpr + >>> from sympy.abc import x + >>> from sympy import Tuple + >>> s = SeqExpr(Tuple(1, 2, 3), Tuple(x, 0, 10)) + >>> s.gen + (1, 2, 3) + >>> s.interval + Interval(0, 10) + >>> s.length + 11 + + See Also + ======== + + sympy.series.sequences.SeqPer + sympy.series.sequences.SeqFormula + """ + + @property + def gen(self): + return self.args[0] + + @property + def interval(self): + return Interval(self.args[1][1], self.args[1][2]) + + @property + def start(self): + return self.interval.inf + + @property + def stop(self): + return self.interval.sup + + @property + def length(self): + return self.stop - self.start + 1 + + @property + def variables(self): + return (self.args[1][0],) + + +class SeqPer(SeqExpr): + """ + Represents a periodic sequence. + + The elements are repeated after a given period. + + Examples + ======== + + >>> from sympy import SeqPer, oo + >>> from sympy.abc import k + + >>> s = SeqPer((1, 2, 3), (0, 5)) + >>> s.periodical + (1, 2, 3) + >>> s.period + 3 + + For value at a particular point + + >>> s.coeff(3) + 1 + + supports slicing + + >>> s[:] + [1, 2, 3, 1, 2, 3] + + iterable + + >>> list(s) + [1, 2, 3, 1, 2, 3] + + sequence starts from negative infinity + + >>> SeqPer((1, 2, 3), (-oo, 0))[0:6] + [1, 2, 3, 1, 2, 3] + + Periodic formulas + + >>> SeqPer((k, k**2, k**3), (k, 0, oo))[0:6] + [0, 1, 8, 3, 16, 125] + + See Also + ======== + + sympy.series.sequences.SeqFormula + """ + + def __new__(cls, periodical, limits=None): + periodical = sympify(periodical) + + def _find_x(periodical): + free = periodical.free_symbols + if len(periodical.free_symbols) == 1: + return free.pop() + else: + return Dummy('k') + + x, start, stop = None, None, None + if limits is None: + x, start, stop = _find_x(periodical), 0, S.Infinity + if is_sequence(limits, Tuple): + if len(limits) == 3: + x, start, stop = limits + elif len(limits) == 2: + x = _find_x(periodical) + start, stop = limits + + if not isinstance(x, (Symbol, Idx)) or start is None or stop is None: + raise ValueError('Invalid limits given: %s' % str(limits)) + + if start is S.NegativeInfinity and stop is S.Infinity: + raise ValueError("Both the start and end value" + "cannot be unbounded") + + limits = sympify((x, start, stop)) + + if is_sequence(periodical, Tuple): + periodical = sympify(tuple(flatten(periodical))) + else: + raise ValueError("invalid period %s should be something " + "like e.g (1, 2) " % periodical) + + if Interval(limits[1], limits[2]) is S.EmptySet: + return S.EmptySequence + + return Basic.__new__(cls, periodical, limits) + + @property + def period(self): + return len(self.gen) + + @property + def periodical(self): + return self.gen + + def _eval_coeff(self, pt): + if self.start is S.NegativeInfinity: + idx = (self.stop - pt) % self.period + else: + idx = (pt - self.start) % self.period + return self.periodical[idx].subs(self.variables[0], pt) + + def _add(self, other): + """See docstring of SeqBase._add""" + if isinstance(other, SeqPer): + per1, lper1 = self.periodical, self.period + per2, lper2 = other.periodical, other.period + + per_length = lcm(lper1, lper2) + + new_per = [] + for x in range(per_length): + ele1 = per1[x % lper1] + ele2 = per2[x % lper2] + new_per.append(ele1 + ele2) + + start, stop = self._intersect_interval(other) + return SeqPer(new_per, (self.variables[0], start, stop)) + + def _mul(self, other): + """See docstring of SeqBase._mul""" + if isinstance(other, SeqPer): + per1, lper1 = self.periodical, self.period + per2, lper2 = other.periodical, other.period + + per_length = lcm(lper1, lper2) + + new_per = [] + for x in range(per_length): + ele1 = per1[x % lper1] + ele2 = per2[x % lper2] + new_per.append(ele1 * ele2) + + start, stop = self._intersect_interval(other) + return SeqPer(new_per, (self.variables[0], start, stop)) + + def coeff_mul(self, coeff): + """See docstring of SeqBase.coeff_mul""" + coeff = sympify(coeff) + per = [x * coeff for x in self.periodical] + return SeqPer(per, self.args[1]) + + +class SeqFormula(SeqExpr): + """ + Represents sequence based on a formula. + + Elements are generated using a formula. + + Examples + ======== + + >>> from sympy import SeqFormula, oo, Symbol + >>> n = Symbol('n') + >>> s = SeqFormula(n**2, (n, 0, 5)) + >>> s.formula + n**2 + + For value at a particular point + + >>> s.coeff(3) + 9 + + supports slicing + + >>> s[:] + [0, 1, 4, 9, 16, 25] + + iterable + + >>> list(s) + [0, 1, 4, 9, 16, 25] + + sequence starts from negative infinity + + >>> SeqFormula(n**2, (-oo, 0))[0:6] + [0, 1, 4, 9, 16, 25] + + See Also + ======== + + sympy.series.sequences.SeqPer + """ + + def __new__(cls, formula, limits=None): + formula = sympify(formula) + + def _find_x(formula): + free = formula.free_symbols + if len(free) == 1: + return free.pop() + elif not free: + return Dummy('k') + else: + raise ValueError( + " specify dummy variables for %s. If the formula contains" + " more than one free symbol, a dummy variable should be" + " supplied explicitly e.g., SeqFormula(m*n**2, (n, 0, 5))" + % formula) + + x, start, stop = None, None, None + if limits is None: + x, start, stop = _find_x(formula), 0, S.Infinity + if is_sequence(limits, Tuple): + if len(limits) == 3: + x, start, stop = limits + elif len(limits) == 2: + x = _find_x(formula) + start, stop = limits + + if not isinstance(x, (Symbol, Idx)) or start is None or stop is None: + raise ValueError('Invalid limits given: %s' % str(limits)) + + if start is S.NegativeInfinity and stop is S.Infinity: + raise ValueError("Both the start and end value " + "cannot be unbounded") + limits = sympify((x, start, stop)) + + if Interval(limits[1], limits[2]) is S.EmptySet: + return S.EmptySequence + + return Basic.__new__(cls, formula, limits) + + @property + def formula(self): + return self.gen + + def _eval_coeff(self, pt): + d = self.variables[0] + return self.formula.subs(d, pt) + + def _add(self, other): + """See docstring of SeqBase._add""" + if isinstance(other, SeqFormula): + form1, v1 = self.formula, self.variables[0] + form2, v2 = other.formula, other.variables[0] + formula = form1 + form2.subs(v2, v1) + start, stop = self._intersect_interval(other) + return SeqFormula(formula, (v1, start, stop)) + + def _mul(self, other): + """See docstring of SeqBase._mul""" + if isinstance(other, SeqFormula): + form1, v1 = self.formula, self.variables[0] + form2, v2 = other.formula, other.variables[0] + formula = form1 * form2.subs(v2, v1) + start, stop = self._intersect_interval(other) + return SeqFormula(formula, (v1, start, stop)) + + def coeff_mul(self, coeff): + """See docstring of SeqBase.coeff_mul""" + coeff = sympify(coeff) + formula = self.formula * coeff + return SeqFormula(formula, self.args[1]) + + def expand(self, *args, **kwargs): + return SeqFormula(expand(self.formula, *args, **kwargs), self.args[1]) + +class RecursiveSeq(SeqBase): + """ + A finite degree recursive sequence. + + Explanation + =========== + + That is, a sequence a(n) that depends on a fixed, finite number of its + previous values. The general form is + + a(n) = f(a(n - 1), a(n - 2), ..., a(n - d)) + + for some fixed, positive integer d, where f is some function defined by a + SymPy expression. + + Parameters + ========== + + recurrence : SymPy expression defining recurrence + This is *not* an equality, only the expression that the nth term is + equal to. For example, if :code:`a(n) = f(a(n - 1), ..., a(n - d))`, + then the expression should be :code:`f(a(n - 1), ..., a(n - d))`. + + yn : applied undefined function + Represents the nth term of the sequence as e.g. :code:`y(n)` where + :code:`y` is an undefined function and `n` is the sequence index. + + n : symbolic argument + The name of the variable that the recurrence is in, e.g., :code:`n` if + the recurrence function is :code:`y(n)`. + + initial : iterable with length equal to the degree of the recurrence + The initial values of the recurrence. + + start : start value of sequence (inclusive) + + Examples + ======== + + >>> from sympy import Function, symbols + >>> from sympy.series.sequences import RecursiveSeq + >>> y = Function("y") + >>> n = symbols("n") + >>> fib = RecursiveSeq(y(n - 1) + y(n - 2), y(n), n, [0, 1]) + + >>> fib.coeff(3) # Value at a particular point + 2 + + >>> fib[:6] # supports slicing + [0, 1, 1, 2, 3, 5] + + >>> fib.recurrence # inspect recurrence + Eq(y(n), y(n - 2) + y(n - 1)) + + >>> fib.degree # automatically determine degree + 2 + + >>> for x in zip(range(10), fib): # supports iteration + ... print(x) + (0, 0) + (1, 1) + (2, 1) + (3, 2) + (4, 3) + (5, 5) + (6, 8) + (7, 13) + (8, 21) + (9, 34) + + See Also + ======== + + sympy.series.sequences.SeqFormula + + """ + + def __new__(cls, recurrence, yn, n, initial=None, start=0): + if not isinstance(yn, AppliedUndef): + raise TypeError("recurrence sequence must be an applied undefined function" + ", found `{}`".format(yn)) + + if not isinstance(n, Basic) or not n.is_symbol: + raise TypeError("recurrence variable must be a symbol" + ", found `{}`".format(n)) + + if yn.args != (n,): + raise TypeError("recurrence sequence does not match symbol") + + y = yn.func + + k = Wild("k", exclude=(n,)) + degree = 0 + + # Find all applications of y in the recurrence and check that: + # 1. The function y is only being used with a single argument; and + # 2. All arguments are n + k for constant negative integers k. + + prev_ys = recurrence.find(y) + for prev_y in prev_ys: + if len(prev_y.args) != 1: + raise TypeError("Recurrence should be in a single variable") + + shift = prev_y.args[0].match(n + k)[k] + if not (shift.is_constant() and shift.is_integer and shift < 0): + raise TypeError("Recurrence should have constant," + " negative, integer shifts" + " (found {})".format(prev_y)) + + if -shift > degree: + degree = -shift + + if not initial: + initial = [Dummy("c_{}".format(k)) for k in range(degree)] + + if len(initial) != degree: + raise ValueError("Number of initial terms must equal degree") + + degree = Integer(degree) + start = sympify(start) + + initial = Tuple(*(sympify(x) for x in initial)) + + seq = Basic.__new__(cls, recurrence, yn, n, initial, start) + + seq.cache = {y(start + k): init for k, init in enumerate(initial)} + seq.degree = degree + + return seq + + @property + def _recurrence(self): + """Equation defining recurrence.""" + return self.args[0] + + @property + def recurrence(self): + """Equation defining recurrence.""" + return Eq(self.yn, self.args[0]) + + @property + def yn(self): + """Applied function representing the nth term""" + return self.args[1] + + @property + def y(self): + """Undefined function for the nth term of the sequence""" + return self.yn.func + + @property + def n(self): + """Sequence index symbol""" + return self.args[2] + + @property + def initial(self): + """The initial values of the sequence""" + return self.args[3] + + @property + def start(self): + """The starting point of the sequence. This point is included""" + return self.args[4] + + @property + def stop(self): + """The ending point of the sequence. (oo)""" + return S.Infinity + + @property + def interval(self): + """Interval on which sequence is defined.""" + return (self.start, S.Infinity) + + def _eval_coeff(self, index): + if index - self.start < len(self.cache): + return self.cache[self.y(index)] + + for current in range(len(self.cache), index + 1): + # Use xreplace over subs for performance. + # See issue #10697. + seq_index = self.start + current + current_recurrence = self._recurrence.xreplace({self.n: seq_index}) + new_term = current_recurrence.xreplace(self.cache) + + self.cache[self.y(seq_index)] = new_term + + return self.cache[self.y(self.start + current)] + + def __iter__(self): + index = self.start + while True: + yield self._eval_coeff(index) + index += 1 + + +def sequence(seq, limits=None): + """ + Returns appropriate sequence object. + + Explanation + =========== + + If ``seq`` is a SymPy sequence, returns :class:`SeqPer` object + otherwise returns :class:`SeqFormula` object. + + Examples + ======== + + >>> from sympy import sequence + >>> from sympy.abc import n + >>> sequence(n**2, (n, 0, 5)) + SeqFormula(n**2, (n, 0, 5)) + >>> sequence((1, 2, 3), (n, 0, 5)) + SeqPer((1, 2, 3), (n, 0, 5)) + + See Also + ======== + + sympy.series.sequences.SeqPer + sympy.series.sequences.SeqFormula + """ + seq = sympify(seq) + + if is_sequence(seq, Tuple): + return SeqPer(seq, limits) + else: + return SeqFormula(seq, limits) + + +############################################################################### +# OPERATIONS # +############################################################################### + + +class SeqExprOp(SeqBase): + """ + Base class for operations on sequences. + + Examples + ======== + + >>> from sympy.series.sequences import SeqExprOp, sequence + >>> from sympy.abc import n + >>> s1 = sequence(n**2, (n, 0, 10)) + >>> s2 = sequence((1, 2, 3), (n, 5, 10)) + >>> s = SeqExprOp(s1, s2) + >>> s.gen + (n**2, (1, 2, 3)) + >>> s.interval + Interval(5, 10) + >>> s.length + 6 + + See Also + ======== + + sympy.series.sequences.SeqAdd + sympy.series.sequences.SeqMul + """ + @property + def gen(self): + """Generator for the sequence. + + returns a tuple of generators of all the argument sequences. + """ + return tuple(a.gen for a in self.args) + + @property + def interval(self): + """Sequence is defined on the intersection + of all the intervals of respective sequences + """ + return Intersection(*(a.interval for a in self.args)) + + @property + def start(self): + return self.interval.inf + + @property + def stop(self): + return self.interval.sup + + @property + def variables(self): + """Cumulative of all the bound variables""" + return tuple(flatten([a.variables for a in self.args])) + + @property + def length(self): + return self.stop - self.start + 1 + + +class SeqAdd(SeqExprOp): + """Represents term-wise addition of sequences. + + Rules: + * The interval on which sequence is defined is the intersection + of respective intervals of sequences. + * Anything + :class:`EmptySequence` remains unchanged. + * Other rules are defined in ``_add`` methods of sequence classes. + + Examples + ======== + + >>> from sympy import EmptySequence, oo, SeqAdd, SeqPer, SeqFormula + >>> from sympy.abc import n + >>> SeqAdd(SeqPer((1, 2), (n, 0, oo)), EmptySequence) + SeqPer((1, 2), (n, 0, oo)) + >>> SeqAdd(SeqPer((1, 2), (n, 0, 5)), SeqPer((1, 2), (n, 6, 10))) + EmptySequence + >>> SeqAdd(SeqPer((1, 2), (n, 0, oo)), SeqFormula(n**2, (n, 0, oo))) + SeqAdd(SeqFormula(n**2, (n, 0, oo)), SeqPer((1, 2), (n, 0, oo))) + >>> SeqAdd(SeqFormula(n**3), SeqFormula(n**2)) + SeqFormula(n**3 + n**2, (n, 0, oo)) + + See Also + ======== + + sympy.series.sequences.SeqMul + """ + + def __new__(cls, *args, **kwargs): + evaluate = kwargs.get('evaluate', global_parameters.evaluate) + + # flatten inputs + args = list(args) + + # adapted from sympy.sets.sets.Union + def _flatten(arg): + if isinstance(arg, SeqBase): + if isinstance(arg, SeqAdd): + return sum(map(_flatten, arg.args), []) + else: + return [arg] + if iterable(arg): + return sum(map(_flatten, arg), []) + raise TypeError("Input must be Sequences or " + " iterables of Sequences") + args = _flatten(args) + + args = [a for a in args if a is not S.EmptySequence] + + # Addition of no sequences is EmptySequence + if not args: + return S.EmptySequence + + if Intersection(*(a.interval for a in args)) is S.EmptySet: + return S.EmptySequence + + # reduce using known rules + if evaluate: + return SeqAdd.reduce(args) + + args = list(ordered(args, SeqBase._start_key)) + + return Basic.__new__(cls, *args) + + @staticmethod + def reduce(args): + """Simplify :class:`SeqAdd` using known rules. + + Iterates through all pairs and ask the constituent + sequences if they can simplify themselves with any other constituent. + + Notes + ===== + + adapted from ``Union.reduce`` + + """ + new_args = True + while new_args: + for id1, s in enumerate(args): + new_args = False + for id2, t in enumerate(args): + if id1 == id2: + continue + new_seq = s._add(t) + # This returns None if s does not know how to add + # with t. Returns the newly added sequence otherwise + if new_seq is not None: + new_args = [a for a in args if a not in (s, t)] + new_args.append(new_seq) + break + if new_args: + args = new_args + break + + if len(args) == 1: + return args.pop() + else: + return SeqAdd(args, evaluate=False) + + def _eval_coeff(self, pt): + """adds up the coefficients of all the sequences at point pt""" + return sum(a.coeff(pt) for a in self.args) + + +class SeqMul(SeqExprOp): + r"""Represents term-wise multiplication of sequences. + + Explanation + =========== + + Handles multiplication of sequences only. For multiplication + with other objects see :func:`SeqBase.coeff_mul`. + + Rules: + * The interval on which sequence is defined is the intersection + of respective intervals of sequences. + * Anything \* :class:`EmptySequence` returns :class:`EmptySequence`. + * Other rules are defined in ``_mul`` methods of sequence classes. + + Examples + ======== + + >>> from sympy import EmptySequence, oo, SeqMul, SeqPer, SeqFormula + >>> from sympy.abc import n + >>> SeqMul(SeqPer((1, 2), (n, 0, oo)), EmptySequence) + EmptySequence + >>> SeqMul(SeqPer((1, 2), (n, 0, 5)), SeqPer((1, 2), (n, 6, 10))) + EmptySequence + >>> SeqMul(SeqPer((1, 2), (n, 0, oo)), SeqFormula(n**2)) + SeqMul(SeqFormula(n**2, (n, 0, oo)), SeqPer((1, 2), (n, 0, oo))) + >>> SeqMul(SeqFormula(n**3), SeqFormula(n**2)) + SeqFormula(n**5, (n, 0, oo)) + + See Also + ======== + + sympy.series.sequences.SeqAdd + """ + + def __new__(cls, *args, **kwargs): + evaluate = kwargs.get('evaluate', global_parameters.evaluate) + + # flatten inputs + args = list(args) + + # adapted from sympy.sets.sets.Union + def _flatten(arg): + if isinstance(arg, SeqBase): + if isinstance(arg, SeqMul): + return sum(map(_flatten, arg.args), []) + else: + return [arg] + elif iterable(arg): + return sum(map(_flatten, arg), []) + raise TypeError("Input must be Sequences or " + " iterables of Sequences") + args = _flatten(args) + + # Multiplication of no sequences is EmptySequence + if not args: + return S.EmptySequence + + if Intersection(*(a.interval for a in args)) is S.EmptySet: + return S.EmptySequence + + # reduce using known rules + if evaluate: + return SeqMul.reduce(args) + + args = list(ordered(args, SeqBase._start_key)) + + return Basic.__new__(cls, *args) + + @staticmethod + def reduce(args): + """Simplify a :class:`SeqMul` using known rules. + + Explanation + =========== + + Iterates through all pairs and ask the constituent + sequences if they can simplify themselves with any other constituent. + + Notes + ===== + + adapted from ``Union.reduce`` + + """ + new_args = True + while new_args: + for id1, s in enumerate(args): + new_args = False + for id2, t in enumerate(args): + if id1 == id2: + continue + new_seq = s._mul(t) + # This returns None if s does not know how to multiply + # with t. Returns the newly multiplied sequence otherwise + if new_seq is not None: + new_args = [a for a in args if a not in (s, t)] + new_args.append(new_seq) + break + if new_args: + args = new_args + break + + if len(args) == 1: + return args.pop() + else: + return SeqMul(args, evaluate=False) + + def _eval_coeff(self, pt): + """multiplies the coefficients of all the sequences at point pt""" + val = 1 + for a in self.args: + val *= a.coeff(pt) + return val diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/series.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/series.py new file mode 100644 index 0000000000000000000000000000000000000000..e9feec7d3b1987bfaa5238969f531e9f98b88b25 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/series.py @@ -0,0 +1,63 @@ +from sympy.core.sympify import sympify + + +def series(expr, x=None, x0=0, n=6, dir="+"): + """Series expansion of expr around point `x = x0`. + + Parameters + ========== + + expr : Expression + The expression whose series is to be expanded. + + x : Symbol + It is the variable of the expression to be calculated. + + x0 : Value + The value around which ``x`` is calculated. Can be any value + from ``-oo`` to ``oo``. + + n : Value + The number of terms upto which the series is to be expanded. + + dir : String, optional + The series-expansion can be bi-directional. If ``dir="+"``, + then (x->x0+). If ``dir="-"``, then (x->x0-). For infinite + ``x0`` (``oo`` or ``-oo``), the ``dir`` argument is determined + from the direction of the infinity (i.e., ``dir="-"`` for + ``oo``). + + Examples + ======== + + >>> from sympy import series, tan, oo + >>> from sympy.abc import x + >>> f = tan(x) + >>> series(f, x, 2, 6, "+") + tan(2) + (1 + tan(2)**2)*(x - 2) + (x - 2)**2*(tan(2)**3 + tan(2)) + + (x - 2)**3*(1/3 + 4*tan(2)**2/3 + tan(2)**4) + (x - 2)**4*(tan(2)**5 + + 5*tan(2)**3/3 + 2*tan(2)/3) + (x - 2)**5*(2/15 + 17*tan(2)**2/15 + + 2*tan(2)**4 + tan(2)**6) + O((x - 2)**6, (x, 2)) + + >>> series(f, x, 2, 3, "-") + tan(2) + (2 - x)*(-tan(2)**2 - 1) + (2 - x)**2*(tan(2)**3 + tan(2)) + + O((x - 2)**3, (x, 2)) + + >>> series(f, x, 2, oo, "+") + Traceback (most recent call last): + ... + TypeError: 'Infinity' object cannot be interpreted as an integer + + Returns + ======= + + Expr + Series expansion of the expression about x0 + + See Also + ======== + + sympy.core.expr.Expr.series: See the docstring of Expr.series() for complete details of this wrapper. + """ + expr = sympify(expr) + return expr.series(x, x0, n, dir) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/series_class.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/series_class.py new file mode 100644 index 0000000000000000000000000000000000000000..ff04993b266a3cbd3f767042d4325fb11edb2168 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/series_class.py @@ -0,0 +1,99 @@ +""" +Contains the base class for series +Made using sequences in mind +""" + +from sympy.core.expr import Expr +from sympy.core.singleton import S +from sympy.core.cache import cacheit + + +class SeriesBase(Expr): + """Base Class for series""" + + @property + def interval(self): + """The interval on which the series is defined""" + raise NotImplementedError("(%s).interval" % self) + + @property + def start(self): + """The starting point of the series. This point is included""" + raise NotImplementedError("(%s).start" % self) + + @property + def stop(self): + """The ending point of the series. This point is included""" + raise NotImplementedError("(%s).stop" % self) + + @property + def length(self): + """Length of the series expansion""" + raise NotImplementedError("(%s).length" % self) + + @property + def variables(self): + """Returns a tuple of variables that are bounded""" + return () + + @property + def free_symbols(self): + """ + This method returns the symbols in the object, excluding those + that take on a specific value (i.e. the dummy symbols). + """ + return ({j for i in self.args for j in i.free_symbols} + .difference(self.variables)) + + @cacheit + def term(self, pt): + """Term at point pt of a series""" + if pt < self.start or pt > self.stop: + raise IndexError("Index %s out of bounds %s" % (pt, self.interval)) + return self._eval_term(pt) + + def _eval_term(self, pt): + raise NotImplementedError("The _eval_term method should be added to" + "%s to return series term so it is available" + "when 'term' calls it." + % self.func) + + def _ith_point(self, i): + """ + Returns the i'th point of a series + If start point is negative infinity, point is returned from the end. + Assumes the first point to be indexed zero. + + Examples + ======== + + TODO + """ + if self.start is S.NegativeInfinity: + initial = self.stop + step = -1 + else: + initial = self.start + step = 1 + + return initial + i*step + + def __iter__(self): + i = 0 + while i < self.length: + pt = self._ith_point(i) + yield self.term(pt) + i += 1 + + def __getitem__(self, index): + if isinstance(index, int): + index = self._ith_point(index) + return self.term(index) + elif isinstance(index, slice): + start, stop = index.start, index.stop + if start is None: + start = 0 + if stop is None: + stop = self.length + return [self.term(self._ith_point(i)) for i in + range(start, stop, index.step or 1)] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_approximants.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_approximants.py new file mode 100644 index 0000000000000000000000000000000000000000..9c03d2ce38add99b0dce8725b6c8d8844b31f76b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_approximants.py @@ -0,0 +1,23 @@ +from sympy.series import approximants +from sympy.core.symbol import symbols +from sympy.functions.combinatorial.factorials import binomial +from sympy.functions.combinatorial.numbers import (fibonacci, lucas) + + +def test_approximants(): + x, t = symbols("x,t") + g = [lucas(k) for k in range(16)] + assert list(approximants(g)) == ( + [2, -4/(x - 2), (5*x - 2)/(3*x - 1), (x - 2)/(x**2 + x - 1)] ) + g = [lucas(k)+fibonacci(k+2) for k in range(16)] + assert list(approximants(g)) == ( + [3, -3/(x - 1), (3*x - 3)/(2*x - 1), -3/(x**2 + x - 1)] ) + g = [lucas(k)**2 for k in range(16)] + assert list(approximants(g)) == ( + [4, -16/(x - 4), (35*x - 4)/(9*x - 1), (37*x - 28)/(13*x**2 + 11*x - 7), + (50*x**2 + 63*x - 52)/(37*x**2 + 19*x - 13), + (-x**2 - 7*x + 4)/(x**3 - 2*x**2 - 2*x + 1)] ) + p = [sum(binomial(k,i)*x**i for i in range(k+1)) for k in range(16)] + y = approximants(p, t, simplify=True) + assert next(y) == 1 + assert next(y) == -1/(t*(x + 1) - 1) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_aseries.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_aseries.py new file mode 100644 index 0000000000000000000000000000000000000000..cae0ac0a43f2406dd96e45c6a31939ac6b4cdcaa --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_aseries.py @@ -0,0 +1,55 @@ +from sympy.core.function import PoleError +from sympy.core.numbers import oo +from sympy.core.symbol import Symbol +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.series.order import O +from sympy.abc import x + +from sympy.testing.pytest import raises + +def test_simple(): + # Gruntz' theses pp. 91 to 96 + # 6.6 + e = sin(1/x + exp(-x)) - sin(1/x) + assert e.aseries(x) == (1/(24*x**4) - 1/(2*x**2) + 1 + O(x**(-6), (x, oo)))*exp(-x) + + e = exp(x) * (exp(1/x + exp(-x)) - exp(1/x)) + assert e.aseries(x, n=4) == 1/(6*x**3) + 1/(2*x**2) + 1/x + 1 + O(x**(-4), (x, oo)) + + e = exp(exp(x) / (1 - 1/x)) + assert e.aseries(x) == exp(exp(x) / (1 - 1/x)) + + # The implementation of bound in aseries is incorrect currently. This test + # should be commented out when that is fixed. + # assert e.aseries(x, bound=3) == exp(exp(x) / x**2)*exp(exp(x) / x)*exp(-exp(x) + exp(x)/(1 - 1/x) - \ + # exp(x) / x - exp(x) / x**2) * exp(exp(x)) + + e = exp(sin(1/x + exp(-exp(x)))) - exp(sin(1/x)) + assert e.aseries(x, n=4) == (-1/(2*x**3) + 1/x + 1 + O(x**(-4), (x, oo)))*exp(-exp(x)) + + e3 = lambda x:exp(exp(exp(x))) + e = e3(x)/e3(x - 1/e3(x)) + assert e.aseries(x, n=3) == 1 + exp(2*x + 2*exp(x))*exp(-2*exp(exp(x)))/2\ + - exp(2*x + exp(x))*exp(-2*exp(exp(x)))/2 - exp(x + exp(x))*exp(-2*exp(exp(x)))/2\ + + exp(x + exp(x))*exp(-exp(exp(x))) + O(exp(-3*exp(exp(x))), (x, oo)) + + e = exp(exp(x)) * (exp(sin(1/x + 1/exp(exp(x)))) - exp(sin(1/x))) + assert e.aseries(x, n=4) == -1/(2*x**3) + 1/x + 1 + O(x**(-4), (x, oo)) + + n = Symbol('n', integer=True) + e = (sqrt(n)*log(n)**2*exp(sqrt(log(n))*log(log(n))**2*exp(sqrt(log(log(n)))*log(log(log(n)))**3)))/n + assert e.aseries(n) == \ + exp(exp(sqrt(log(log(n)))*log(log(log(n)))**3)*sqrt(log(n))*log(log(n))**2)*log(n)**2/sqrt(n) + + +def test_hierarchical(): + e = sin(1/x + exp(-x)) + assert e.aseries(x, n=3, hir=True) == -exp(-2*x)*sin(1/x)/2 + \ + exp(-x)*cos(1/x) + sin(1/x) + O(exp(-3*x), (x, oo)) + + e = sin(x) * cos(exp(-x)) + assert e.aseries(x, hir=True) == exp(-4*x)*sin(x)/24 - \ + exp(-2*x)*sin(x)/2 + sin(x) + O(exp(-6*x), (x, oo)) + raises(PoleError, lambda: e.aseries(x)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_demidovich.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_demidovich.py new file mode 100644 index 0000000000000000000000000000000000000000..98cafbae6f019dd3d97d306099d5780ed2f37f04 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_demidovich.py @@ -0,0 +1,143 @@ +from sympy.core.numbers import (Rational, oo, pi) +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import (root, sqrt) +from sympy.functions.elementary.trigonometric import (asin, cos, sin, tan) +from sympy.polys.rationaltools import together +from sympy.series.limits import limit + +# Numbers listed with the tests refer to problem numbers in the book +# "Anti-demidovich, problemas resueltos, Ed. URSS" + +x = Symbol("x") + + +def test_leadterm(): + assert (3 + 2*x**(log(3)/log(2) - 1)).leadterm(x) == (3, 0) + + +def root3(x): + return root(x, 3) + + +def root4(x): + return root(x, 4) + + +def test_Limits_simple_0(): + assert limit((2**(x + 1) + 3**(x + 1))/(2**x + 3**x), x, oo) == 3 # 175 + + +def test_Limits_simple_1(): + assert limit((x + 1)*(x + 2)*(x + 3)/x**3, x, oo) == 1 # 172 + assert limit(sqrt(x + 1) - sqrt(x), x, oo) == 0 # 179 + assert limit((2*x - 3)*(3*x + 5)*(4*x - 6)/(3*x**3 + x - 1), x, oo) == 8 # Primjer 1 + assert limit(x/root3(x**3 + 10), x, oo) == 1 # Primjer 2 + assert limit((x + 1)**2/(x**2 + 1), x, oo) == 1 # 181 + + +def test_Limits_simple_2(): + assert limit(1000*x/(x**2 - 1), x, oo) == 0 # 182 + assert limit((x**2 - 5*x + 1)/(3*x + 7), x, oo) is oo # 183 + assert limit((2*x**2 - x + 3)/(x**3 - 8*x + 5), x, oo) == 0 # 184 + assert limit((2*x**2 - 3*x - 4)/sqrt(x**4 + 1), x, oo) == 2 # 186 + assert limit((2*x + 3)/(x + root3(x)), x, oo) == 2 # 187 + assert limit(x**2/(10 + x*sqrt(x)), x, oo) is oo # 188 + assert limit(root3(x**2 + 1)/(x + 1), x, oo) == 0 # 189 + assert limit(sqrt(x)/sqrt(x + sqrt(x + sqrt(x))), x, oo) == 1 # 190 + + +def test_Limits_simple_3a(): + a = Symbol('a') + #issue 3513 + assert together(limit((x**2 - (a + 1)*x + a)/(x**3 - a**3), x, a)) == \ + (a - 1)/(3*a**2) # 196 + + +def test_Limits_simple_3b(): + h = Symbol("h") + assert limit(((x + h)**3 - x**3)/h, h, 0) == 3*x**2 # 197 + assert limit((1/(1 - x) - 3/(1 - x**3)), x, 1) == -1 # 198 + assert limit((sqrt(1 + x) - 1)/(root3(1 + x) - 1), x, 0) == Rational(3)/2 # Primer 4 + assert limit((sqrt(x) - 1)/(x - 1), x, 1) == Rational(1)/2 # 199 + assert limit((sqrt(x) - 8)/(root3(x) - 4), x, 64) == 3 # 200 + assert limit((root3(x) - 1)/(root4(x) - 1), x, 1) == Rational(4)/3 # 201 + assert limit( + (root3(x**2) - 2*root3(x) + 1)/(x - 1)**2, x, 1) == Rational(1)/9 # 202 + + +def test_Limits_simple_4a(): + a = Symbol('a') + assert limit((sqrt(x) - sqrt(a))/(x - a), x, a) == 1/(2*sqrt(a)) # Primer 5 + assert limit((sqrt(x) - 1)/(root3(x) - 1), x, 1) == Rational(3, 2) # 205 + assert limit((sqrt(1 + x) - sqrt(1 - x))/x, x, 0) == 1 # 207 + assert limit(sqrt(x**2 - 5*x + 6) - x, x, oo) == Rational(-5, 2) # 213 + + +def test_limits_simple_4aa(): + assert limit(x*(sqrt(x**2 + 1) - x), x, oo) == Rational(1)/2 # 214 + + +def test_Limits_simple_4b(): + #issue 3511 + assert limit(x - root3(x**3 - 1), x, oo) == 0 # 215 + + +def test_Limits_simple_4c(): + assert limit(log(1 + exp(x))/x, x, -oo) == 0 # 267a + assert limit(log(1 + exp(x))/x, x, oo) == 1 # 267b + + +def test_bounded(): + assert limit(sin(x)/x, x, oo) == 0 # 216b + assert limit(x*sin(1/x), x, 0) == 0 # 227a + + +def test_f1a(): + #issue 3508: + assert limit((sin(2*x)/x)**(1 + x), x, 0) == 2 # Primer 7 + + +def test_f1a2(): + #issue 3509: + assert limit(((x - 1)/(x + 1))**x, x, oo) == exp(-2) # Primer 9 + + +def test_f1b(): + m = Symbol("m") + n = Symbol("n") + h = Symbol("h") + a = Symbol("a") + assert limit(sin(x)/x, x, 2) == sin(2)/2 # 216a + assert limit(sin(3*x)/x, x, 0) == 3 # 217 + assert limit(sin(5*x)/sin(2*x), x, 0) == Rational(5, 2) # 218 + assert limit(sin(pi*x)/sin(3*pi*x), x, 0) == Rational(1, 3) # 219 + assert limit(x*sin(pi/x), x, oo) == pi # 220 + assert limit((1 - cos(x))/x**2, x, 0) == S.Half # 221 + assert limit(x*sin(1/x), x, oo) == 1 # 227b + assert limit((cos(m*x) - cos(n*x))/x**2, x, 0) == -m**2/2 + n**2/2 # 232 + assert limit((tan(x) - sin(x))/x**3, x, 0) == S.Half # 233 + assert limit((x - sin(2*x))/(x + sin(3*x)), x, 0) == -Rational(1, 4) # 237 + assert limit((1 - sqrt(cos(x)))/x**2, x, 0) == Rational(1, 4) # 239 + assert limit((sqrt(1 + sin(x)) - sqrt(1 - sin(x)))/x, x, 0) == 1 # 240 + + assert limit((1 + h/x)**x, x, oo) == exp(h) # Primer 9 + assert limit((sin(x) - sin(a))/(x - a), x, a) == cos(a) # 222, *176 + assert limit((cos(x) - cos(a))/(x - a), x, a) == -sin(a) # 223 + assert limit((sin(x + h) - sin(x))/h, h, 0) == cos(x) # 225 + + +def test_f2a(): + assert limit(((x + 1)/(2*x + 1))**(x**2), x, oo) == 0 # Primer 8 + + +def test_f2(): + assert limit((sqrt( + cos(x)) - root3(cos(x)))/(sin(x)**2), x, 0) == -Rational(1, 12) # *184 + + +def test_f3(): + a = Symbol('a') + #issue 3504 + assert limit(asin(a*x)/x, x, 0) == a diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_formal.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_formal.py new file mode 100644 index 0000000000000000000000000000000000000000..cac60b12534152a5783bb8f0faab2c06da6691fb --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_formal.py @@ -0,0 +1,618 @@ +from sympy.concrete.summations import Sum +from sympy.core.add import Add +from sympy.core.function import (Derivative, Function) +from sympy.core.mul import Mul +from sympy.core.numbers import (I, Rational, oo, pi) +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.hyperbolic import (acosh, asech) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (acos, asin, atan, cos, sin) +from sympy.functions.special.bessel import airyai +from sympy.functions.special.error_functions import erf +from sympy.functions.special.gamma_functions import gamma +from sympy.integrals.integrals import integrate +from sympy.series.formal import fps +from sympy.series.order import O +from sympy.series.formal import (rational_algorithm, FormalPowerSeries, + FormalPowerSeriesProduct, FormalPowerSeriesCompose, + FormalPowerSeriesInverse, simpleDE, + rational_independent, exp_re, hyper_re) +from sympy.testing.pytest import raises, XFAIL, slow + +x, y, z = symbols('x y z') +n, m, k = symbols('n m k', integer=True) +f, r = Function('f'), Function('r') + + +def test_rational_algorithm(): + f = 1 / ((x - 1)**2 * (x - 2)) + assert rational_algorithm(f, x, k) == \ + (-2**(-k - 1) + 1 - (factorial(k + 1) / factorial(k)), 0, 0) + + f = (1 + x + x**2 + x**3) / ((x - 1) * (x - 2)) + assert rational_algorithm(f, x, k) == \ + (-15*2**(-k - 1) + 4, x + 4, 0) + + f = z / (y*m - m*x - y*x + x**2) + assert rational_algorithm(f, x, k) == \ + (((-y**(-k - 1)*z) / (y - m)) + ((m**(-k - 1)*z) / (y - m)), 0, 0) + + f = x / (1 - x - x**2) + assert rational_algorithm(f, x, k) is None + assert rational_algorithm(f, x, k, full=True) == \ + (((Rational(-1, 2) + sqrt(5)/2)**(-k - 1) * + (-sqrt(5)/10 + S.Half)) + + ((-sqrt(5)/2 - S.Half)**(-k - 1) * + (sqrt(5)/10 + S.Half)), 0, 0) + + f = 1 / (x**2 + 2*x + 2) + assert rational_algorithm(f, x, k) is None + assert rational_algorithm(f, x, k, full=True) == \ + ((I*(-1 + I)**(-k - 1)) / 2 - (I*(-1 - I)**(-k - 1)) / 2, 0, 0) + + f = log(1 + x) + assert rational_algorithm(f, x, k) == \ + (-(-1)**(-k) / k, 0, 1) + + f = atan(x) + assert rational_algorithm(f, x, k) is None + assert rational_algorithm(f, x, k, full=True) == \ + (((I*I**(-k)) / 2 - (I*(-I)**(-k)) / 2) / k, 0, 1) + + f = x*atan(x) - log(1 + x**2) / 2 + assert rational_algorithm(f, x, k) is None + assert rational_algorithm(f, x, k, full=True) == \ + (((I*I**(-k + 1)) / 2 - (I*(-I)**(-k + 1)) / 2) / + (k*(k - 1)), 0, 2) + + f = log((1 + x) / (1 - x)) / 2 - atan(x) + assert rational_algorithm(f, x, k) is None + assert rational_algorithm(f, x, k, full=True) == \ + ((-(-1)**(-k) / 2 - (I*I**(-k)) / 2 + (I*(-I)**(-k)) / 2 + + S.Half) / k, 0, 1) + + assert rational_algorithm(cos(x), x, k) is None + + +def test_rational_independent(): + ri = rational_independent + assert ri([], x) == [] + assert ri([cos(x), sin(x)], x) == [cos(x), sin(x)] + assert ri([x**2, sin(x), x*sin(x), x**3], x) == \ + [x**3 + x**2, x*sin(x) + sin(x)] + assert ri([S.One, x*log(x), log(x), sin(x)/x, cos(x), sin(x), x], x) == \ + [x + 1, x*log(x) + log(x), sin(x)/x + sin(x), cos(x)] + + +def test_simpleDE(): + # Tests just the first valid DE + for DE in simpleDE(exp(x), x, f): + assert DE == (-f(x) + Derivative(f(x), x), 1) + break + for DE in simpleDE(sin(x), x, f): + assert DE == (f(x) + Derivative(f(x), x, x), 2) + break + for DE in simpleDE(log(1 + x), x, f): + assert DE == ((x + 1)*Derivative(f(x), x, 2) + Derivative(f(x), x), 2) + break + for DE in simpleDE(asin(x), x, f): + assert DE == (x*Derivative(f(x), x) + (x**2 - 1)*Derivative(f(x), x, x), + 2) + break + for DE in simpleDE(exp(x)*sin(x), x, f): + assert DE == (2*f(x) - 2*Derivative(f(x)) + Derivative(f(x), x, x), 2) + break + for DE in simpleDE(((1 + x)/(1 - x))**n, x, f): + assert DE == (2*n*f(x) + (x**2 - 1)*Derivative(f(x), x), 1) + break + for DE in simpleDE(airyai(x), x, f): + assert DE == (-x*f(x) + Derivative(f(x), x, x), 2) + break + + +def test_exp_re(): + d = -f(x) + Derivative(f(x), x) + assert exp_re(d, r, k) == -r(k) + r(k + 1) + + d = f(x) + Derivative(f(x), x, x) + assert exp_re(d, r, k) == r(k) + r(k + 2) + + d = f(x) + Derivative(f(x), x) + Derivative(f(x), x, x) + assert exp_re(d, r, k) == r(k) + r(k + 1) + r(k + 2) + + d = Derivative(f(x), x) + Derivative(f(x), x, x) + assert exp_re(d, r, k) == r(k) + r(k + 1) + + d = Derivative(f(x), x, 3) + Derivative(f(x), x, 4) + Derivative(f(x)) + assert exp_re(d, r, k) == r(k) + r(k + 2) + r(k + 3) + + +def test_hyper_re(): + d = f(x) + Derivative(f(x), x, x) + assert hyper_re(d, r, k) == r(k) + (k+1)*(k+2)*r(k + 2) + + d = -x*f(x) + Derivative(f(x), x, x) + assert hyper_re(d, r, k) == (k + 2)*(k + 3)*r(k + 3) - r(k) + + d = 2*f(x) - 2*Derivative(f(x), x) + Derivative(f(x), x, x) + assert hyper_re(d, r, k) == \ + (-2*k - 2)*r(k + 1) + (k + 1)*(k + 2)*r(k + 2) + 2*r(k) + + d = 2*n*f(x) + (x**2 - 1)*Derivative(f(x), x) + assert hyper_re(d, r, k) == \ + k*r(k) + 2*n*r(k + 1) + (-k - 2)*r(k + 2) + + d = (x**10 + 4)*Derivative(f(x), x) + x*(x**10 - 1)*Derivative(f(x), x, x) + assert hyper_re(d, r, k) == \ + (k*(k - 1) + k)*r(k) + (4*k - (k + 9)*(k + 10) + 40)*r(k + 10) + + d = ((x**2 - 1)*Derivative(f(x), x, 3) + 3*x*Derivative(f(x), x, x) + + Derivative(f(x), x)) + assert hyper_re(d, r, k) == \ + ((k*(k - 2)*(k - 1) + 3*k*(k - 1) + k)*r(k) + + (-k*(k + 1)*(k + 2))*r(k + 2)) + + +def test_fps(): + assert fps(1) == 1 + assert fps(2, x) == 2 + assert fps(2, x, dir='+') == 2 + assert fps(2, x, dir='-') == 2 + assert fps(1/x + 1/x**2) == 1/x + 1/x**2 + assert fps(log(1 + x), hyper=False, rational=False) == log(1 + x) + + f = fps(x**2 + x + 1) + assert isinstance(f, FormalPowerSeries) + assert f.function == x**2 + x + 1 + assert f[0] == 1 + assert f[2] == x**2 + assert f.truncate(4) == x**2 + x + 1 + O(x**4) + assert f.polynomial() == x**2 + x + 1 + + f = fps(log(1 + x)) + assert isinstance(f, FormalPowerSeries) + assert f.function == log(1 + x) + assert f.subs(x, y) == f + assert f[:5] == [0, x, -x**2/2, x**3/3, -x**4/4] + assert f.as_leading_term(x) == x + assert f.polynomial(6) == x - x**2/2 + x**3/3 - x**4/4 + x**5/5 + + k = f.ak.variables[0] + assert f.infinite == Sum((-(-1)**(-k)*x**k)/k, (k, 1, oo)) + + ft, s = f.truncate(n=None), f[:5] + for i, t in enumerate(ft): + if i == 5: + break + assert s[i] == t + + f = sin(x).fps(x) + assert isinstance(f, FormalPowerSeries) + assert f.truncate() == x - x**3/6 + x**5/120 + O(x**6) + + raises(NotImplementedError, lambda: fps(y*x)) + raises(ValueError, lambda: fps(x, dir=0)) + + +@slow +def test_fps__rational(): + assert fps(1/x) == (1/x) + assert fps((x**2 + x + 1) / x**3, dir=-1) == (x**2 + x + 1) / x**3 + + f = 1 / ((x - 1)**2 * (x - 2)) + assert fps(f, x).truncate() == \ + (Rational(-1, 2) - x*Rational(5, 4) - 17*x**2/8 - 49*x**3/16 - 129*x**4/32 - + 321*x**5/64 + O(x**6)) + + f = (1 + x + x**2 + x**3) / ((x - 1) * (x - 2)) + assert fps(f, x).truncate() == \ + (S.Half + x*Rational(5, 4) + 17*x**2/8 + 49*x**3/16 + 113*x**4/32 + + 241*x**5/64 + O(x**6)) + + f = x / (1 - x - x**2) + assert fps(f, x, full=True).truncate() == \ + x + x**2 + 2*x**3 + 3*x**4 + 5*x**5 + O(x**6) + + f = 1 / (x**2 + 2*x + 2) + assert fps(f, x, full=True).truncate() == \ + S.Half - x/2 + x**2/4 - x**4/8 + x**5/8 + O(x**6) + + f = log(1 + x) + assert fps(f, x).truncate() == \ + x - x**2/2 + x**3/3 - x**4/4 + x**5/5 + O(x**6) + assert fps(f, x, dir=1).truncate() == fps(f, x, dir=-1).truncate() + assert fps(f, x, 2).truncate() == \ + (log(3) - Rational(2, 3) - (x - 2)**2/18 + (x - 2)**3/81 - + (x - 2)**4/324 + (x - 2)**5/1215 + x/3 + O((x - 2)**6, (x, 2))) + assert fps(f, x, 2, dir=-1).truncate() == \ + (log(3) - Rational(2, 3) - (-x + 2)**2/18 - (-x + 2)**3/81 - + (-x + 2)**4/324 - (-x + 2)**5/1215 + x/3 + O((x - 2)**6, (x, 2))) + + f = atan(x) + assert fps(f, x, full=True).truncate() == x - x**3/3 + x**5/5 + O(x**6) + assert fps(f, x, full=True, dir=1).truncate() == \ + fps(f, x, full=True, dir=-1).truncate() + assert fps(f, x, 2, full=True).truncate() == \ + (atan(2) - Rational(2, 5) - 2*(x - 2)**2/25 + 11*(x - 2)**3/375 - + 6*(x - 2)**4/625 + 41*(x - 2)**5/15625 + x/5 + O((x - 2)**6, (x, 2))) + assert fps(f, x, 2, full=True, dir=-1).truncate() == \ + (atan(2) - Rational(2, 5) - 2*(-x + 2)**2/25 - 11*(-x + 2)**3/375 - + 6*(-x + 2)**4/625 - 41*(-x + 2)**5/15625 + x/5 + O((x - 2)**6, (x, 2))) + + f = x*atan(x) - log(1 + x**2) / 2 + assert fps(f, x, full=True).truncate() == x**2/2 - x**4/12 + O(x**6) + + f = log((1 + x) / (1 - x)) / 2 - atan(x) + assert fps(f, x, full=True).truncate(n=10) == 2*x**3/3 + 2*x**7/7 + O(x**10) + + +@slow +def test_fps__hyper(): + f = sin(x) + assert fps(f, x).truncate() == x - x**3/6 + x**5/120 + O(x**6) + + f = cos(x) + assert fps(f, x).truncate() == 1 - x**2/2 + x**4/24 + O(x**6) + + f = exp(x) + assert fps(f, x).truncate() == \ + 1 + x + x**2/2 + x**3/6 + x**4/24 + x**5/120 + O(x**6) + + f = atan(x) + assert fps(f, x).truncate() == x - x**3/3 + x**5/5 + O(x**6) + + f = exp(acos(x)) + assert fps(f, x).truncate() == \ + (exp(pi/2) - x*exp(pi/2) + x**2*exp(pi/2)/2 - x**3*exp(pi/2)/3 + + 5*x**4*exp(pi/2)/24 - x**5*exp(pi/2)/6 + O(x**6)) + + f = exp(acosh(x)) + assert fps(f, x).truncate() == I + x - I*x**2/2 - I*x**4/8 + O(x**6) + + f = atan(1/x) + assert fps(f, x).truncate() == pi/2 - x + x**3/3 - x**5/5 + O(x**6) + + f = x*atan(x) - log(1 + x**2) / 2 + assert fps(f, x, rational=False).truncate() == x**2/2 - x**4/12 + O(x**6) + + f = log(1 + x) + assert fps(f, x, rational=False).truncate() == \ + x - x**2/2 + x**3/3 - x**4/4 + x**5/5 + O(x**6) + + f = airyai(x**2) + assert fps(f, x).truncate() == \ + (3**Rational(5, 6)*gamma(Rational(1, 3))/(6*pi) - + 3**Rational(2, 3)*x**2/(3*gamma(Rational(1, 3))) + O(x**6)) + + f = exp(x)*sin(x) + assert fps(f, x).truncate() == x + x**2 + x**3/3 - x**5/30 + O(x**6) + + f = exp(x)*sin(x)/x + assert fps(f, x).truncate() == 1 + x + x**2/3 - x**4/30 - x**5/90 + O(x**6) + + f = sin(x) * cos(x) + assert fps(f, x).truncate() == x - 2*x**3/3 + 2*x**5/15 + O(x**6) + + +def test_fps_shift(): + f = x**-5*sin(x) + assert fps(f, x).truncate() == \ + 1/x**4 - 1/(6*x**2) + Rational(1, 120) - x**2/5040 + x**4/362880 + O(x**6) + + f = x**2*atan(x) + assert fps(f, x, rational=False).truncate() == \ + x**3 - x**5/3 + O(x**6) + + f = cos(sqrt(x))*x + assert fps(f, x).truncate() == \ + x - x**2/2 + x**3/24 - x**4/720 + x**5/40320 + O(x**6) + + f = x**2*cos(sqrt(x)) + assert fps(f, x).truncate() == \ + x**2 - x**3/2 + x**4/24 - x**5/720 + O(x**6) + + +def test_fps__Add_expr(): + f = x*atan(x) - log(1 + x**2) / 2 + assert fps(f, x).truncate() == x**2/2 - x**4/12 + O(x**6) + + f = sin(x) + cos(x) - exp(x) + log(1 + x) + assert fps(f, x).truncate() == x - 3*x**2/2 - x**4/4 + x**5/5 + O(x**6) + + f = 1/x + sin(x) + assert fps(f, x).truncate() == 1/x + x - x**3/6 + x**5/120 + O(x**6) + + f = sin(x) - cos(x) + 1/(x - 1) + assert fps(f, x).truncate() == \ + -2 - x**2/2 - 7*x**3/6 - 25*x**4/24 - 119*x**5/120 + O(x**6) + + +def test_fps__asymptotic(): + f = exp(x) + assert fps(f, x, oo) == f + assert fps(f, x, -oo).truncate() == O(1/x**6, (x, oo)) + + f = erf(x) + assert fps(f, x, oo).truncate() == 1 + O(1/x**6, (x, oo)) + assert fps(f, x, -oo).truncate() == -1 + O(1/x**6, (x, oo)) + + f = atan(x) + assert fps(f, x, oo, full=True).truncate() == \ + -1/(5*x**5) + 1/(3*x**3) - 1/x + pi/2 + O(1/x**6, (x, oo)) + assert fps(f, x, -oo, full=True).truncate() == \ + -1/(5*x**5) + 1/(3*x**3) - 1/x - pi/2 + O(1/x**6, (x, oo)) + + f = log(1 + x) + assert fps(f, x, oo) != \ + (-1/(5*x**5) - 1/(4*x**4) + 1/(3*x**3) - 1/(2*x**2) + 1/x - log(1/x) + + O(1/x**6, (x, oo))) + assert fps(f, x, -oo) != \ + (-1/(5*x**5) - 1/(4*x**4) + 1/(3*x**3) - 1/(2*x**2) + 1/x + I*pi - + log(-1/x) + O(1/x**6, (x, oo))) + + +def test_fps__fractional(): + f = sin(sqrt(x)) / x + assert fps(f, x).truncate() == \ + (1/sqrt(x) - sqrt(x)/6 + x**Rational(3, 2)/120 - + x**Rational(5, 2)/5040 + x**Rational(7, 2)/362880 - + x**Rational(9, 2)/39916800 + x**Rational(11, 2)/6227020800 + O(x**6)) + + f = sin(sqrt(x)) * x + assert fps(f, x).truncate() == \ + (x**Rational(3, 2) - x**Rational(5, 2)/6 + x**Rational(7, 2)/120 - + x**Rational(9, 2)/5040 + x**Rational(11, 2)/362880 + O(x**6)) + + f = atan(sqrt(x)) / x**2 + assert fps(f, x).truncate() == \ + (x**Rational(-3, 2) - x**Rational(-1, 2)/3 + x**S.Half/5 - + x**Rational(3, 2)/7 + x**Rational(5, 2)/9 - x**Rational(7, 2)/11 + + x**Rational(9, 2)/13 - x**Rational(11, 2)/15 + O(x**6)) + + f = exp(sqrt(x)) + assert fps(f, x).truncate().expand() == \ + (1 + x/2 + x**2/24 + x**3/720 + x**4/40320 + x**5/3628800 + sqrt(x) + + x**Rational(3, 2)/6 + x**Rational(5, 2)/120 + x**Rational(7, 2)/5040 + + x**Rational(9, 2)/362880 + x**Rational(11, 2)/39916800 + O(x**6)) + + f = exp(sqrt(x))*x + assert fps(f, x).truncate().expand() == \ + (x + x**2/2 + x**3/24 + x**4/720 + x**5/40320 + x**Rational(3, 2) + + x**Rational(5, 2)/6 + x**Rational(7, 2)/120 + x**Rational(9, 2)/5040 + + x**Rational(11, 2)/362880 + O(x**6)) + + +def test_fps__logarithmic_singularity(): + f = log(1 + 1/x) + assert fps(f, x) != \ + -log(x) + x - x**2/2 + x**3/3 - x**4/4 + x**5/5 + O(x**6) + assert fps(f, x, rational=False) != \ + -log(x) + x - x**2/2 + x**3/3 - x**4/4 + x**5/5 + O(x**6) + + +@XFAIL +def test_fps__logarithmic_singularity_fail(): + f = asech(x) # Algorithms for computing limits probably needs improvements + assert fps(f, x) == log(2) - log(x) - x**2/4 - 3*x**4/64 + O(x**6) + + +def test_fps_symbolic(): + f = x**n*sin(x**2) + assert fps(f, x).truncate(8) == x**(n + 2) - x**(n + 6)/6 + O(x**(n + 8), x) + + f = x**n*log(1 + x) + fp = fps(f, x) + k = fp.ak.variables[0] + assert fp.infinite == \ + Sum((-(-1)**(-k)*x**(k + n))/k, (k, 1, oo)) + + f = (x - 2)**n*log(1 + x) + assert fps(f, x, 2).truncate() == \ + ((x - 2)**n*log(3) + (x - 2)**(n + 1)/3 - (x - 2)**(n + 2)/18 + (x - 2)**(n + 3)/81 - + (x - 2)**(n + 4)/324 + (x - 2)**(n + 5)/1215 + O((x - 2)**(n + 6), (x, 2))) + + f = x**(n - 2)*cos(x) + assert fps(f, x).truncate() == \ + (x**(n - 2) - x**n/2 + x**(n + 2)/24 + O(x**(n + 4), x)) + + f = x**(n - 2)*sin(x) + x**n*exp(x) + assert fps(f, x).truncate() == \ + (x**(n - 1) + x**(n + 1) + x**(n + 2)/2 + x**n + + x**(n + 4)/24 + x**(n + 5)/60 + O(x**(n + 6), x)) + + f = x**n*atan(x) + assert fps(f, x, oo).truncate() == \ + (-x**(n - 5)/5 + x**(n - 3)/3 + x**n*(pi/2 - 1/x) + + O((1/x)**(-n)/x**6, (x, oo))) + + f = x**(n/2)*cos(x) + assert fps(f, x).truncate() == \ + x**(n/2) - x**(n/2 + 2)/2 + x**(n/2 + 4)/24 + O(x**(n/2 + 6), x) + + f = x**(n + m)*sin(x) + assert fps(f, x).truncate() == \ + x**(m + n + 1) - x**(m + n + 3)/6 + x**(m + n + 5)/120 + O(x**(m + n + 6), x) + + +def test_fps__slow(): + f = x*exp(x)*sin(2*x) # TODO: rsolve needs improvement + assert fps(f, x).truncate() == 2*x**2 + 2*x**3 - x**4/3 - x**5 + O(x**6) + + +def test_fps__operations(): + f1, f2 = fps(sin(x)), fps(cos(x)) + + fsum = f1 + f2 + assert fsum.function == sin(x) + cos(x) + assert fsum.truncate() == \ + 1 + x - x**2/2 - x**3/6 + x**4/24 + x**5/120 + O(x**6) + + fsum = f1 + 1 + assert fsum.function == sin(x) + 1 + assert fsum.truncate() == 1 + x - x**3/6 + x**5/120 + O(x**6) + + fsum = 1 + f2 + assert fsum.function == cos(x) + 1 + assert fsum.truncate() == 2 - x**2/2 + x**4/24 + O(x**6) + + assert (f1 + x) == Add(f1, x) + + assert -f2.truncate() == -1 + x**2/2 - x**4/24 + O(x**6) + assert (f1 - f1) is S.Zero + + fsub = f1 - f2 + assert fsub.function == sin(x) - cos(x) + assert fsub.truncate() == \ + -1 + x + x**2/2 - x**3/6 - x**4/24 + x**5/120 + O(x**6) + + fsub = f1 - 1 + assert fsub.function == sin(x) - 1 + assert fsub.truncate() == -1 + x - x**3/6 + x**5/120 + O(x**6) + + fsub = 1 - f2 + assert fsub.function == -cos(x) + 1 + assert fsub.truncate() == x**2/2 - x**4/24 + O(x**6) + + raises(ValueError, lambda: f1 + fps(exp(x), dir=-1)) + raises(ValueError, lambda: f1 + fps(exp(x), x0=1)) + + fm = f1 * 3 + + assert fm.function == 3*sin(x) + assert fm.truncate() == 3*x - x**3/2 + x**5/40 + O(x**6) + + fm = 3 * f2 + + assert fm.function == 3*cos(x) + assert fm.truncate() == 3 - 3*x**2/2 + x**4/8 + O(x**6) + + assert (f1 * f2) == Mul(f1, f2) + assert (f1 * x) == Mul(f1, x) + + fd = f1.diff() + assert fd.function == cos(x) + assert fd.truncate() == 1 - x**2/2 + x**4/24 + O(x**6) + + fd = f2.diff() + assert fd.function == -sin(x) + assert fd.truncate() == -x + x**3/6 - x**5/120 + O(x**6) + + fd = f2.diff().diff() + assert fd.function == -cos(x) + assert fd.truncate() == -1 + x**2/2 - x**4/24 + O(x**6) + + f3 = fps(exp(sqrt(x))) + fd = f3.diff() + assert fd.truncate().expand() == \ + (1/(2*sqrt(x)) + S.Half + x/12 + x**2/240 + x**3/10080 + x**4/725760 + + x**5/79833600 + sqrt(x)/4 + x**Rational(3, 2)/48 + x**Rational(5, 2)/1440 + + x**Rational(7, 2)/80640 + x**Rational(9, 2)/7257600 + x**Rational(11, 2)/958003200 + + O(x**6)) + + assert f1.integrate((x, 0, 1)) == -cos(1) + 1 + assert integrate(f1, (x, 0, 1)) == -cos(1) + 1 + + fi = integrate(f1, x) + assert fi.function == -cos(x) + assert fi.truncate() == -1 + x**2/2 - x**4/24 + O(x**6) + + fi = f2.integrate(x) + assert fi.function == sin(x) + assert fi.truncate() == x - x**3/6 + x**5/120 + O(x**6) + +def test_fps__product(): + f1, f2, f3 = fps(sin(x)), fps(exp(x)), fps(cos(x)) + + raises(ValueError, lambda: f1.product(exp(x), x)) + raises(ValueError, lambda: f1.product(fps(exp(x), dir=-1), x, 4)) + raises(ValueError, lambda: f1.product(fps(exp(x), x0=1), x, 4)) + raises(ValueError, lambda: f1.product(fps(exp(y)), x, 4)) + + fprod = f1.product(f2, x) + assert isinstance(fprod, FormalPowerSeriesProduct) + assert isinstance(fprod.ffps, FormalPowerSeries) + assert isinstance(fprod.gfps, FormalPowerSeries) + assert fprod.f == sin(x) + assert fprod.g == exp(x) + assert fprod.function == sin(x) * exp(x) + assert fprod._eval_terms(4) == x + x**2 + x**3/3 + assert fprod.truncate(4) == x + x**2 + x**3/3 + O(x**4) + assert fprod.polynomial(4) == x + x**2 + x**3/3 + + raises(NotImplementedError, lambda: fprod._eval_term(5)) + raises(NotImplementedError, lambda: fprod.infinite) + raises(NotImplementedError, lambda: fprod._eval_derivative(x)) + raises(NotImplementedError, lambda: fprod.integrate(x)) + + assert f1.product(f3, x)._eval_terms(4) == x - 2*x**3/3 + assert f1.product(f3, x).truncate(4) == x - 2*x**3/3 + O(x**4) + + +def test_fps__compose(): + f1, f2, f3 = fps(exp(x)), fps(sin(x)), fps(cos(x)) + + raises(ValueError, lambda: f1.compose(sin(x), x)) + raises(ValueError, lambda: f1.compose(fps(sin(x), dir=-1), x, 4)) + raises(ValueError, lambda: f1.compose(fps(sin(x), x0=1), x, 4)) + raises(ValueError, lambda: f1.compose(fps(sin(y)), x, 4)) + + raises(ValueError, lambda: f1.compose(f3, x)) + raises(ValueError, lambda: f2.compose(f3, x)) + + fcomp = f1.compose(f2, x) + assert isinstance(fcomp, FormalPowerSeriesCompose) + assert isinstance(fcomp.ffps, FormalPowerSeries) + assert isinstance(fcomp.gfps, FormalPowerSeries) + assert fcomp.f == exp(x) + assert fcomp.g == sin(x) + assert fcomp.function == exp(sin(x)) + assert fcomp._eval_terms(6) == 1 + x + x**2/2 - x**4/8 - x**5/15 + assert fcomp.truncate() == 1 + x + x**2/2 - x**4/8 - x**5/15 + O(x**6) + assert fcomp.truncate(5) == 1 + x + x**2/2 - x**4/8 + O(x**5) + + raises(NotImplementedError, lambda: fcomp._eval_term(5)) + raises(NotImplementedError, lambda: fcomp.infinite) + raises(NotImplementedError, lambda: fcomp._eval_derivative(x)) + raises(NotImplementedError, lambda: fcomp.integrate(x)) + + assert f1.compose(f2, x).truncate(4) == 1 + x + x**2/2 + O(x**4) + assert f1.compose(f2, x).truncate(8) == \ + 1 + x + x**2/2 - x**4/8 - x**5/15 - x**6/240 + x**7/90 + O(x**8) + assert f1.compose(f2, x).truncate(6) == \ + 1 + x + x**2/2 - x**4/8 - x**5/15 + O(x**6) + + assert f2.compose(f2, x).truncate(4) == x - x**3/3 + O(x**4) + assert f2.compose(f2, x).truncate(8) == x - x**3/3 + x**5/10 - 8*x**7/315 + O(x**8) + assert f2.compose(f2, x).truncate(6) == x - x**3/3 + x**5/10 + O(x**6) + + +def test_fps__inverse(): + f1, f2, f3 = fps(sin(x)), fps(exp(x)), fps(cos(x)) + + raises(ValueError, lambda: f1.inverse(x)) + + finv = f2.inverse(x) + assert isinstance(finv, FormalPowerSeriesInverse) + assert isinstance(finv.ffps, FormalPowerSeries) + raises(ValueError, lambda: finv.gfps) + + assert finv.f == exp(x) + assert finv.function == exp(-x) + assert finv._eval_terms(5) == 1 - x + x**2/2 - x**3/6 + x**4/24 + assert finv.truncate() == 1 - x + x**2/2 - x**3/6 + x**4/24 - x**5/120 + O(x**6) + assert finv.truncate(5) == 1 - x + x**2/2 - x**3/6 + x**4/24 + O(x**5) + + raises(NotImplementedError, lambda: finv._eval_term(5)) + raises(ValueError, lambda: finv.g) + raises(NotImplementedError, lambda: finv.infinite) + raises(NotImplementedError, lambda: finv._eval_derivative(x)) + raises(NotImplementedError, lambda: finv.integrate(x)) + + assert f2.inverse(x).truncate(8) == \ + 1 - x + x**2/2 - x**3/6 + x**4/24 - x**5/120 + x**6/720 - x**7/5040 + O(x**8) + + assert f3.inverse(x).truncate() == 1 + x**2/2 + 5*x**4/24 + O(x**6) + assert f3.inverse(x).truncate(8) == 1 + x**2/2 + 5*x**4/24 + 61*x**6/720 + O(x**8) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_fourier.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_fourier.py new file mode 100644 index 0000000000000000000000000000000000000000..994f182088b09b038e0e1b3885fec1c27f69f2b0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_fourier.py @@ -0,0 +1,165 @@ +from sympy.core.add import Add +from sympy.core.numbers import (Rational, oo, pi) +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (cos, sin, sinc, tan) +from sympy.series.fourier import fourier_series +from sympy.series.fourier import FourierSeries +from sympy.testing.pytest import raises +from functools import lru_cache + +x, y, z = symbols('x y z') + +# Don't declare these during import because they are slow +@lru_cache() +def _get_examples(): + fo = fourier_series(x, (x, -pi, pi)) + fe = fourier_series(x**2, (-pi, pi)) + fp = fourier_series(Piecewise((0, x < 0), (pi, True)), (x, -pi, pi)) + return fo, fe, fp + + +def test_FourierSeries(): + fo, fe, fp = _get_examples() + + assert fourier_series(1, (-pi, pi)) == 1 + assert (Piecewise((0, x < 0), (pi, True)). + fourier_series((x, -pi, pi)).truncate()) == fp.truncate() + assert isinstance(fo, FourierSeries) + assert fo.function == x + assert fo.x == x + assert fo.period == (-pi, pi) + + assert fo.term(3) == 2*sin(3*x) / 3 + assert fe.term(3) == -4*cos(3*x) / 9 + assert fp.term(3) == 2*sin(3*x) / 3 + + assert fo.as_leading_term(x) == 2*sin(x) + assert fe.as_leading_term(x) == pi**2 / 3 + assert fp.as_leading_term(x) == pi / 2 + + assert fo.truncate() == 2*sin(x) - sin(2*x) + (2*sin(3*x) / 3) + assert fe.truncate() == -4*cos(x) + cos(2*x) + pi**2 / 3 + assert fp.truncate() == 2*sin(x) + (2*sin(3*x) / 3) + pi / 2 + + fot = fo.truncate(n=None) + s = [0, 2*sin(x), -sin(2*x)] + for i, t in enumerate(fot): + if i == 3: + break + assert s[i] == t + + def _check_iter(f, i): + for ind, t in enumerate(f): + assert t == f[ind] # noqa: PLR1736 + if ind == i: + break + + _check_iter(fo, 3) + _check_iter(fe, 3) + _check_iter(fp, 3) + + assert fo.subs(x, x**2) == fo + + raises(ValueError, lambda: fourier_series(x, (0, 1, 2))) + raises(ValueError, lambda: fourier_series(x, (x, 0, oo))) + raises(ValueError, lambda: fourier_series(x*y, (0, oo))) + + +def test_FourierSeries_2(): + p = Piecewise((0, x < 0), (x, True)) + f = fourier_series(p, (x, -2, 2)) + + assert f.term(3) == (2*sin(3*pi*x / 2) / (3*pi) - + 4*cos(3*pi*x / 2) / (9*pi**2)) + assert f.truncate() == (2*sin(pi*x / 2) / pi - sin(pi*x) / pi - + 4*cos(pi*x / 2) / pi**2 + S.Half) + + +def test_square_wave(): + """Test if fourier_series approximates discontinuous function correctly.""" + square_wave = Piecewise((1, x < pi), (-1, True)) + s = fourier_series(square_wave, (x, 0, 2*pi)) + + assert s.truncate(3) == 4 / pi * sin(x) + 4 / (3 * pi) * sin(3 * x) + \ + 4 / (5 * pi) * sin(5 * x) + assert s.sigma_approximation(4) == 4 / pi * sin(x) * sinc(pi / 4) + \ + 4 / (3 * pi) * sin(3 * x) * sinc(3 * pi / 4) + + +def test_sawtooth_wave(): + s = fourier_series(x, (x, 0, pi)) + assert s.truncate(4) == \ + pi/2 - sin(2*x) - sin(4*x)/2 - sin(6*x)/3 + s = fourier_series(x, (x, 0, 1)) + assert s.truncate(4) == \ + S.Half - sin(2*pi*x)/pi - sin(4*pi*x)/(2*pi) - sin(6*pi*x)/(3*pi) + + +def test_FourierSeries__operations(): + fo, fe, fp = _get_examples() + + fes = fe.scale(-1).shift(pi**2) + assert fes.truncate() == 4*cos(x) - cos(2*x) + 2*pi**2 / 3 + + assert fp.shift(-pi/2).truncate() == (2*sin(x) + (2*sin(3*x) / 3) + + (2*sin(5*x) / 5)) + + fos = fo.scale(3) + assert fos.truncate() == 6*sin(x) - 3*sin(2*x) + 2*sin(3*x) + + fx = fe.scalex(2).shiftx(1) + assert fx.truncate() == -4*cos(2*x + 2) + cos(4*x + 4) + pi**2 / 3 + + fl = fe.scalex(3).shift(-pi).scalex(2).shiftx(1).scale(4) + assert fl.truncate() == (-16*cos(6*x + 6) + 4*cos(12*x + 12) - + 4*pi + 4*pi**2 / 3) + + raises(ValueError, lambda: fo.shift(x)) + raises(ValueError, lambda: fo.shiftx(sin(x))) + raises(ValueError, lambda: fo.scale(x*y)) + raises(ValueError, lambda: fo.scalex(x**2)) + + +def test_FourierSeries__neg(): + fo, fe, fp = _get_examples() + + assert (-fo).truncate() == -2*sin(x) + sin(2*x) - (2*sin(3*x) / 3) + assert (-fe).truncate() == +4*cos(x) - cos(2*x) - pi**2 / 3 + + +def test_FourierSeries__add__sub(): + fo, fe, fp = _get_examples() + + assert fo + fo == fo.scale(2) + assert fo - fo == 0 + assert -fe - fe == fe.scale(-2) + + assert (fo + fe).truncate() == 2*sin(x) - sin(2*x) - 4*cos(x) + cos(2*x) \ + + pi**2 / 3 + assert (fo - fe).truncate() == 2*sin(x) - sin(2*x) + 4*cos(x) - cos(2*x) \ + - pi**2 / 3 + + assert isinstance(fo + 1, Add) + + raises(ValueError, lambda: fo + fourier_series(x, (x, 0, 2))) + + +def test_FourierSeries_finite(): + + assert fourier_series(sin(x)).truncate(1) == sin(x) + # assert type(fourier_series(sin(x)*log(x))).truncate() == FourierSeries + # assert type(fourier_series(sin(x**2+6))).truncate() == FourierSeries + assert fourier_series(sin(x)*log(y)*exp(z),(x,pi,-pi)).truncate() == sin(x)*log(y)*exp(z) + assert fourier_series(sin(x)**6).truncate(oo) == -15*cos(2*x)/32 + 3*cos(4*x)/16 - cos(6*x)/32 \ + + Rational(5, 16) + assert fourier_series(sin(x) ** 6).truncate() == -15 * cos(2 * x) / 32 + 3 * cos(4 * x) / 16 \ + + Rational(5, 16) + assert fourier_series(sin(4*x+3) + cos(3*x+4)).truncate(oo) == -sin(4)*sin(3*x) + sin(4*x)*cos(3) \ + + cos(4)*cos(3*x) + sin(3)*cos(4*x) + assert fourier_series(sin(x)+cos(x)*tan(x)).truncate(oo) == 2*sin(x) + assert fourier_series(cos(pi*x), (x, -1, 1)).truncate(oo) == cos(pi*x) + assert fourier_series(cos(3*pi*x + 4) - sin(4*pi*x)*log(pi*y), (x, -1, 1)).truncate(oo) == -log(pi*y)*sin(4*pi*x)\ + - sin(4)*sin(3*pi*x) + cos(4)*cos(3*pi*x) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_gruntz.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_gruntz.py new file mode 100644 index 0000000000000000000000000000000000000000..4cae15297048bc52a69a3d9ca57a7614cfcdc61c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_gruntz.py @@ -0,0 +1,490 @@ +from sympy.core import EulerGamma +from sympy.core.function import Function +from sympy.core.numbers import (E, I, Integer, Rational, oo, pi) +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (acot, atan, cos, sin) +from sympy.functions.special.error_functions import (Ei, erf) +from sympy.functions.special.gamma_functions import (digamma, gamma, loggamma) +from sympy.functions.special.zeta_functions import zeta +from sympy.polys.polytools import cancel +from sympy.functions.elementary.hyperbolic import cosh, coth, sinh, tanh +from sympy.series.gruntz import compare, mrv, rewrite, mrv_leadterm, gruntz, \ + sign +from sympy.testing.pytest import XFAIL, raises, skip, slow + +""" +This test suite is testing the limit algorithm using the bottom up approach. +See the documentation in limits2.py. The algorithm itself is highly recursive +by nature, so "compare" is logically the lowest part of the algorithm, yet in +some sense it's the most complex part, because it needs to calculate a limit +to return the result. + +Nevertheless, the rest of the algorithm depends on compare working correctly. +""" + +x = Symbol('x', real=True) +m = Symbol('m', real=True) + + +runslow = False + + +def _sskip(): + if not runslow: + skip("slow") + + +@slow +def test_gruntz_evaluation(): + # Gruntz' thesis pp. 122 to 123 + # 8.1 + assert gruntz(exp(x)*(exp(1/x - exp(-x)) - exp(1/x)), x, oo) == -1 + # 8.2 + assert gruntz(exp(x)*(exp(1/x + exp(-x) + exp(-x**2)) + - exp(1/x - exp(-exp(x)))), x, oo) == 1 + # 8.3 + assert gruntz(exp(exp(x - exp(-x))/(1 - 1/x)) - exp(exp(x)), x, oo) is oo + # 8.5 + assert gruntz(exp(exp(exp(x + exp(-x)))) / exp(exp(exp(x))), x, oo) is oo + # 8.6 + assert gruntz(exp(exp(exp(x))) / exp(exp(exp(x - exp(-exp(x))))), + x, oo) is oo + # 8.7 + assert gruntz(exp(exp(exp(x))) / exp(exp(exp(x - exp(-exp(exp(x)))))), + x, oo) == 1 + # 8.8 + assert gruntz(exp(exp(x)) / exp(exp(x - exp(-exp(exp(x))))), x, oo) == 1 + # 8.9 + assert gruntz(log(x)**2 * exp(sqrt(log(x))*(log(log(x)))**2 + * exp(sqrt(log(log(x))) * (log(log(log(x))))**3)) / sqrt(x), + x, oo) == 0 + # 8.10 + assert gruntz((x*log(x)*(log(x*exp(x) - x**2))**2) + / (log(log(x**2 + 2*exp(exp(3*x**3*log(x)))))), x, oo) == Rational(1, 3) + # 8.11 + assert gruntz((exp(x*exp(-x)/(exp(-x) + exp(-2*x**2/(x + 1)))) - exp(x))/x, + x, oo) == -exp(2) + # 8.12 + assert gruntz((3**x + 5**x)**(1/x), x, oo) == 5 + # 8.13 + assert gruntz(x/log(x**(log(x**(log(2)/log(x))))), x, oo) is oo + # 8.14 + assert gruntz(exp(exp(2*log(x**5 + x)*log(log(x)))) + / exp(exp(10*log(x)*log(log(x)))), x, oo) is oo + # 8.15 + assert gruntz(exp(exp(Rational(5, 2)*x**Rational(-5, 7) + Rational(21, 8)*x**Rational(6, 11) + + 2*x**(-8) + Rational(54, 17)*x**Rational(49, 45)))**8 + / log(log(-log(Rational(4, 3)*x**Rational(-5, 14))))**Rational(7, 6), x, oo) is oo + # 8.16 + assert gruntz((exp(4*x*exp(-x)/(1/exp(x) + 1/exp(2*x**2/(x + 1)))) - exp(x)) + / exp(x)**4, x, oo) == 1 + # 8.17 + assert gruntz(exp(x*exp(-x)/(exp(-x) + exp(-2*x**2/(x + 1))))/exp(x), x, oo) \ + == 1 + # 8.19 + assert gruntz(log(x)*(log(log(x) + log(log(x))) - log(log(x))) + / (log(log(x) + log(log(log(x))))), x, oo) == 1 + # 8.20 + assert gruntz(exp((log(log(x + exp(log(x)*log(log(x)))))) + / (log(log(log(exp(x) + x + log(x)))))), x, oo) == E + # Another + assert gruntz(exp(exp(exp(x + exp(-x)))) / exp(exp(x)), x, oo) is oo + + +def test_gruntz_evaluation_slow(): + _sskip() + # 8.4 + assert gruntz(exp(exp(exp(x)/(1 - 1/x))) + - exp(exp(exp(x)/(1 - 1/x - log(x)**(-log(x))))), x, oo) is -oo + # 8.18 + assert gruntz((exp(exp(-x/(1 + exp(-x))))*exp(-x/(1 + exp(-x/(1 + exp(-x))))) + *exp(exp(-x + exp(-x/(1 + exp(-x)))))) + / (exp(-x/(1 + exp(-x))))**2 - exp(x) + x, x, oo) == 2 + + +@slow +def test_gruntz_eval_special(): + # Gruntz, p. 126 + assert gruntz(exp(x)*(sin(1/x + exp(-x)) - sin(1/x + exp(-x**2))), x, oo) == 1 + assert gruntz((erf(x - exp(-exp(x))) - erf(x)) * exp(exp(x)) * exp(x**2), + x, oo) == -2/sqrt(pi) + assert gruntz(exp(exp(x)) * (exp(sin(1/x + exp(-exp(x)))) - exp(sin(1/x))), + x, oo) == 1 + assert gruntz(exp(x)*(gamma(x + exp(-x)) - gamma(x)), x, oo) is oo + assert gruntz(exp(exp(digamma(digamma(x))))/x, x, oo) == exp(Rational(-1, 2)) + assert gruntz(exp(exp(digamma(log(x))))/x, x, oo) == exp(Rational(-1, 2)) + assert gruntz(digamma(digamma(digamma(x))), x, oo) is oo + assert gruntz(loggamma(loggamma(x)), x, oo) is oo + assert gruntz(((gamma(x + 1/gamma(x)) - gamma(x))/log(x) - cos(1/x)) + * x*log(x), x, oo) == Rational(-1, 2) + assert gruntz(x * (gamma(x - 1/gamma(x)) - gamma(x) + log(x)), x, oo) \ + == S.Half + assert gruntz((gamma(x + 1/gamma(x)) - gamma(x)) / log(x), x, oo) == 1 + + +def test_gruntz_eval_special_slow(): + _sskip() + assert gruntz(gamma(x + 1)/sqrt(2*pi) + - exp(-x)*(x**(x + S.Half) + x**(x - S.Half)/12), x, oo) is oo + assert gruntz(exp(exp(exp(digamma(digamma(digamma(x))))))/x, x, oo) == 0 + + +@XFAIL +def test_grunts_eval_special_slow_sometimes_fail(): + _sskip() + # XXX This sometimes fails!!! + assert gruntz(exp(gamma(x - exp(-x))*exp(1/x)) - exp(gamma(x)), x, oo) is oo + + +def test_gruntz_Ei(): + assert gruntz((Ei(x - exp(-exp(x))) - Ei(x)) *exp(-x)*exp(exp(x))*x, x, oo) == -1 + + +@XFAIL +def test_gruntz_eval_special_fail(): + # TODO zeta function series + assert gruntz( + exp((log(2) + 1)*x) * (zeta(x + exp(-x)) - zeta(x)), x, oo) == -log(2) + + # TODO 8.35 - 8.37 (bessel, max-min) + + +def test_gruntz_hyperbolic(): + assert gruntz(cosh(x), x, oo) is oo + assert gruntz(cosh(x), x, -oo) is oo + assert gruntz(sinh(x), x, oo) is oo + assert gruntz(sinh(x), x, -oo) is -oo + assert gruntz(2*cosh(x)*exp(x), x, oo) is oo + assert gruntz(2*cosh(x)*exp(x), x, -oo) == 1 + assert gruntz(2*sinh(x)*exp(x), x, oo) is oo + assert gruntz(2*sinh(x)*exp(x), x, -oo) == -1 + assert gruntz(tanh(x), x, oo) == 1 + assert gruntz(tanh(x), x, -oo) == -1 + assert gruntz(coth(x), x, oo) == 1 + assert gruntz(coth(x), x, -oo) == -1 + + +def test_compare1(): + assert compare(2, x, x) == "<" + assert compare(x, exp(x), x) == "<" + assert compare(exp(x), exp(x**2), x) == "<" + assert compare(exp(x**2), exp(exp(x)), x) == "<" + assert compare(1, exp(exp(x)), x) == "<" + + assert compare(x, 2, x) == ">" + assert compare(exp(x), x, x) == ">" + assert compare(exp(x**2), exp(x), x) == ">" + assert compare(exp(exp(x)), exp(x**2), x) == ">" + assert compare(exp(exp(x)), 1, x) == ">" + + assert compare(2, 3, x) == "=" + assert compare(3, -5, x) == "=" + assert compare(2, -5, x) == "=" + + assert compare(x, x**2, x) == "=" + assert compare(x**2, x**3, x) == "=" + assert compare(x**3, 1/x, x) == "=" + assert compare(1/x, x**m, x) == "=" + assert compare(x**m, -x, x) == "=" + + assert compare(exp(x), exp(-x), x) == "=" + assert compare(exp(-x), exp(2*x), x) == "=" + assert compare(exp(2*x), exp(x)**2, x) == "=" + assert compare(exp(x)**2, exp(x + exp(-x)), x) == "=" + assert compare(exp(x), exp(x + exp(-x)), x) == "=" + + assert compare(exp(x**2), 1/exp(x**2), x) == "=" + + +def test_compare2(): + assert compare(exp(x), x**5, x) == ">" + assert compare(exp(x**2), exp(x)**2, x) == ">" + assert compare(exp(x), exp(x + exp(-x)), x) == "=" + assert compare(exp(x + exp(-x)), exp(x), x) == "=" + assert compare(exp(x + exp(-x)), exp(-x), x) == "=" + assert compare(exp(-x), x, x) == ">" + assert compare(x, exp(-x), x) == "<" + assert compare(exp(x + 1/x), x, x) == ">" + assert compare(exp(-exp(x)), exp(x), x) == ">" + assert compare(exp(exp(-exp(x)) + x), exp(-exp(x)), x) == "<" + + +def test_compare3(): + assert compare(exp(exp(x)), exp(x + exp(-exp(x))), x) == ">" + + +def test_sign1(): + assert sign(Rational(0), x) == 0 + assert sign(Rational(3), x) == 1 + assert sign(Rational(-5), x) == -1 + assert sign(log(x), x) == 1 + assert sign(exp(-x), x) == 1 + assert sign(exp(x), x) == 1 + assert sign(-exp(x), x) == -1 + assert sign(3 - 1/x, x) == 1 + assert sign(-3 - 1/x, x) == -1 + assert sign(sin(1/x), x) == 1 + assert sign((x**Integer(2)), x) == 1 + assert sign(x**2, x) == 1 + assert sign(x**5, x) == 1 + + +def test_sign2(): + assert sign(x, x) == 1 + assert sign(-x, x) == -1 + y = Symbol("y", positive=True) + assert sign(y, x) == 1 + assert sign(-y, x) == -1 + assert sign(y*x, x) == 1 + assert sign(-y*x, x) == -1 + + +def mmrv(a, b): + return set(mrv(a, b)[0].keys()) + + +def test_mrv1(): + assert mmrv(x, x) == {x} + assert mmrv(x + 1/x, x) == {x} + assert mmrv(x**2, x) == {x} + assert mmrv(log(x), x) == {x} + assert mmrv(exp(x), x) == {exp(x)} + assert mmrv(exp(-x), x) == {exp(-x)} + assert mmrv(exp(x**2), x) == {exp(x**2)} + assert mmrv(-exp(1/x), x) == {x} + assert mmrv(exp(x + 1/x), x) == {exp(x + 1/x)} + + +def test_mrv2a(): + assert mmrv(exp(x + exp(-exp(x))), x) == {exp(-exp(x))} + assert mmrv(exp(x + exp(-x)), x) == {exp(x + exp(-x)), exp(-x)} + assert mmrv(exp(1/x + exp(-x)), x) == {exp(-x)} + +#sometimes infinite recursion due to log(exp(x**2)) not simplifying + + +def test_mrv2b(): + assert mmrv(exp(x + exp(-x**2)), x) == {exp(-x**2)} + +#sometimes infinite recursion due to log(exp(x**2)) not simplifying + + +def test_mrv2c(): + assert mmrv( + exp(-x + 1/x**2) - exp(x + 1/x), x) == {exp(x + 1/x), exp(1/x**2 - x)} + +#sometimes infinite recursion due to log(exp(x**2)) not simplifying + + +def test_mrv3(): + assert mmrv(exp(x**2) + x*exp(x) + log(x)**x/x, x) == {exp(x**2)} + assert mmrv( + exp(x)*(exp(1/x + exp(-x)) - exp(1/x)), x) == {exp(x), exp(-x)} + assert mmrv(log( + x**2 + 2*exp(exp(3*x**3*log(x)))), x) == {exp(exp(3*x**3*log(x)))} + assert mmrv(log(x - log(x))/log(x), x) == {x} + assert mmrv( + (exp(1/x - exp(-x)) - exp(1/x))*exp(x), x) == {exp(x), exp(-x)} + assert mmrv( + 1/exp(-x + exp(-x)) - exp(x), x) == {exp(x), exp(-x), exp(x - exp(-x))} + assert mmrv(log(log(x*exp(x*exp(x)) + 1)), x) == {exp(x*exp(x))} + assert mmrv(exp(exp(log(log(x) + 1/x))), x) == {x} + + +def test_mrv4(): + ln = log + assert mmrv((ln(ln(x) + ln(ln(x))) - ln(ln(x)))/ln(ln(x) + ln(ln(ln(x))))*ln(x), + x) == {x} + assert mmrv(log(log(x*exp(x*exp(x)) + 1)) - exp(exp(log(log(x) + 1/x))), x) == \ + {exp(x*exp(x))} + + +def mrewrite(a, b, c): + return rewrite(a[1], a[0], b, c) + + +def test_rewrite1(): + e = exp(x) + assert mrewrite(mrv(e, x), x, m) == (1/m, -x) + e = exp(x**2) + assert mrewrite(mrv(e, x), x, m) == (1/m, -x**2) + e = exp(x + 1/x) + assert mrewrite(mrv(e, x), x, m) == (1/m, -x - 1/x) + e = 1/exp(-x + exp(-x)) - exp(x) + assert mrewrite(mrv(e, x), x, m) == ((-m*exp(m) + m)*exp(-m)/m**2, -x) + + +def test_rewrite2(): + e = exp(x)*log(log(exp(x))) + assert mmrv(e, x) == {exp(x)} + assert mrewrite(mrv(e, x), x, m) == (1/m*log(x), -x) + +#sometimes infinite recursion due to log(exp(x**2)) not simplifying + + +def test_rewrite3(): + e = exp(-x + 1/x**2) - exp(x + 1/x) + #both of these are correct and should be equivalent: + assert mrewrite(mrv(e, x), x, m) in [(-1/m + m*exp( + (x**2 + x)/x**3), -x - 1/x), ((m**2 - exp((x**2 + x)/x**3))/m, x**(-2) - x)] + + +def test_mrv_leadterm1(): + assert mrv_leadterm(-exp(1/x), x) == (-1, 0) + assert mrv_leadterm(1/exp(-x + exp(-x)) - exp(x), x) == (-1, 0) + assert mrv_leadterm( + (exp(1/x - exp(-x)) - exp(1/x))*exp(x), x) == (-exp(1/x), 0) + + +def test_mrv_leadterm2(): + #Gruntz: p51, 3.25 + assert mrv_leadterm((log(exp(x) + x) - x)/log(exp(x) + log(x))*exp(x), x) == \ + (1, 0) + + +def test_mrv_leadterm3(): + #Gruntz: p56, 3.27 + assert mmrv(exp(-x + exp(-x)*exp(-x*log(x))), x) == {exp(-x - x*log(x))} + assert mrv_leadterm(exp(-x + exp(-x)*exp(-x*log(x))), x) == (exp(-x), 0) + + +def test_limit1(): + assert gruntz(x, x, oo) is oo + assert gruntz(x, x, -oo) is -oo + assert gruntz(-x, x, oo) is -oo + assert gruntz(x**2, x, -oo) is oo + assert gruntz(-x**2, x, oo) is -oo + assert gruntz(x*log(x), x, 0, dir="+") == 0 + assert gruntz(1/x, x, oo) == 0 + assert gruntz(exp(x), x, oo) is oo + assert gruntz(-exp(x), x, oo) is -oo + assert gruntz(exp(x)/x, x, oo) is oo + assert gruntz(1/x - exp(-x), x, oo) == 0 + assert gruntz(x + 1/x, x, oo) is oo + + +def test_limit2(): + assert gruntz(x**x, x, 0, dir="+") == 1 + assert gruntz((exp(x) - 1)/x, x, 0) == 1 + assert gruntz(1 + 1/x, x, oo) == 1 + assert gruntz(-exp(1/x), x, oo) == -1 + assert gruntz(x + exp(-x), x, oo) is oo + assert gruntz(x + exp(-x**2), x, oo) is oo + assert gruntz(x + exp(-exp(x)), x, oo) is oo + assert gruntz(13 + 1/x - exp(-x), x, oo) == 13 + + +def test_limit3(): + a = Symbol('a') + assert gruntz(x - log(1 + exp(x)), x, oo) == 0 + assert gruntz(x - log(a + exp(x)), x, oo) == 0 + assert gruntz(exp(x)/(1 + exp(x)), x, oo) == 1 + assert gruntz(exp(x)/(a + exp(x)), x, oo) == 1 + + +def test_limit4(): + #issue 3463 + assert gruntz((3**x + 5**x)**(1/x), x, oo) == 5 + #issue 3463 + assert gruntz((3**(1/x) + 5**(1/x))**x, x, 0) == 5 + + +@XFAIL +def test_MrvTestCase_page47_ex3_21(): + h = exp(-x/(1 + exp(-x))) + expr = exp(h)*exp(-x/(1 + h))*exp(exp(-x + h))/h**2 - exp(x) + x + assert mmrv(expr, x) == {1/h, exp(-x), exp(x), exp(x - h), exp(x/(1 + h))} + + +def test_gruntz_I(): + y = Symbol("y") + assert gruntz(I*x, x, oo) == I*oo + assert gruntz(y*I*x, x, oo) == y*I*oo + assert gruntz(y*3*I*x, x, oo) == y*I*oo + assert gruntz(y*3*sin(I)*x, x, oo) == y*I*oo + + +def test_issue_4814(): + assert gruntz((x + 1)**(1/log(x + 1)), x, oo) == E + + +def test_intractable(): + assert gruntz(1/gamma(x), x, oo) == 0 + assert gruntz(1/loggamma(x), x, oo) == 0 + assert gruntz(gamma(x)/loggamma(x), x, oo) is oo + assert gruntz(exp(gamma(x))/gamma(x), x, oo) is oo + assert gruntz(gamma(x), x, 3) == 2 + assert gruntz(gamma(Rational(1, 7) + 1/x), x, oo) == gamma(Rational(1, 7)) + assert gruntz(log(x**x)/log(gamma(x)), x, oo) == 1 + assert gruntz(log(gamma(gamma(x)))/exp(x), x, oo) is oo + + +def test_aseries_trig(): + assert cancel(gruntz(1/log(atan(x)), x, oo) + - 1/(log(pi) + log(S.Half))) == 0 + assert gruntz(1/acot(x), x, -oo) is -oo + + +def test_exp_log_series(): + assert gruntz(x/log(log(x*exp(x))), x, oo) is oo + + +def test_issue_3644(): + assert gruntz(((x**7 + x + 1)/(2**x + x**2))**(-1/x), x, oo) == 2 + + +def test_issue_6843(): + n = Symbol('n', integer=True, positive=True) + r = (n + 1)*x**(n + 1)/(x**(n + 1) - 1) - x/(x - 1) + assert gruntz(r, x, 1).simplify() == n/2 + + +def test_issue_4190(): + assert gruntz(x - gamma(1/x), x, oo) == S.EulerGamma + + +@XFAIL +def test_issue_5172(): + n = Symbol('n') + r = Symbol('r', positive=True) + c = Symbol('c') + p = Symbol('p', positive=True) + m = Symbol('m', negative=True) + expr = ((2*n*(n - r + 1)/(n + r*(n - r + 1)))**c + \ + (r - 1)*(n*(n - r + 2)/(n + r*(n - r + 1)))**c - n)/(n**c - n) + expr = expr.subs(c, c + 1) + assert gruntz(expr.subs(c, m), n, oo) == 1 + # fail: + assert gruntz(expr.subs(c, p), n, oo).simplify() == \ + (2**(p + 1) + r - 1)/(r + 1)**(p + 1) + + +def test_issue_4109(): + assert gruntz(1/gamma(x), x, 0) == 0 + assert gruntz(x*gamma(x), x, 0) == 1 + + +def test_issue_6682(): + assert gruntz(exp(2*Ei(-x))/x**2, x, 0) == exp(2*EulerGamma) + + +def test_issue_7096(): + from sympy.functions import sign + assert gruntz(x**-pi, x, 0, dir='-') == oo*sign((-1)**(-pi)) + + +def test_issue_7391_8166(): + f = Function('f') + # limit should depend on the continuity of the expression at the point passed + raises(ValueError, lambda: gruntz(f(x), x, 4)) + raises(ValueError, lambda: gruntz(x*f(x)**2/(x**2 + f(x)**4), x, 0)) + + +def test_issue_24210_25885(): + eq = exp(x)/(1+1/x)**x**2 + ans = sqrt(E) + assert gruntz(eq, x, oo) == ans + assert gruntz(1/eq, x, oo) == 1/ans diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_kauers.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_kauers.py new file mode 100644 index 0000000000000000000000000000000000000000..bfb9044b33416bc38879649b258150ba2906250c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_kauers.py @@ -0,0 +1,23 @@ +from sympy.series.kauers import finite_diff +from sympy.series.kauers import finite_diff_kauers +from sympy.abc import x, y, z, m, n, w +from sympy.core.numbers import pi +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.concrete.summations import Sum + + +def test_finite_diff(): + assert finite_diff(x**2 + 2*x + 1, x) == 2*x + 3 + assert finite_diff(y**3 + 2*y**2 + 3*y + 5, y) == 3*y**2 + 7*y + 6 + assert finite_diff(z**2 - 2*z + 3, z) == 2*z - 1 + assert finite_diff(w**2 + 3*w - 2, w) == 2*w + 4 + assert finite_diff(sin(x), x, pi/6) == -sin(x) + sin(x + pi/6) + assert finite_diff(cos(y), y, pi/3) == -cos(y) + cos(y + pi/3) + assert finite_diff(x**2 - 2*x + 3, x, 2) == 4*x + assert finite_diff(n**2 - 2*n + 3, n, 3) == 6*n + 3 + +def test_finite_diff_kauers(): + assert finite_diff_kauers(Sum(x**2, (x, 1, n))) == (n + 1)**2 + assert finite_diff_kauers(Sum(y, (y, 1, m))) == (m + 1) + assert finite_diff_kauers(Sum((x*y), (x, 1, m), (y, 1, n))) == (m + 1)*(n + 1) + assert finite_diff_kauers(Sum((x*y**2), (x, 1, m), (y, 1, n))) == (n + 1)**2*(m + 1) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_limits.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_limits.py new file mode 100644 index 0000000000000000000000000000000000000000..aa3ab7683f057424f1c3215a06381d27687710dc --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_limits.py @@ -0,0 +1,1440 @@ +from itertools import product + +from sympy.concrete.summations import Sum +from sympy.core.function import (Function, diff) +from sympy.core import EulerGamma, GoldenRatio +from sympy.core.mod import Mod +from sympy.core.numbers import (E, I, Rational, oo, pi, zoo) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.combinatorial.numbers import fibonacci +from sympy.functions.combinatorial.factorials import (binomial, factorial, subfactorial) +from sympy.functions.elementary.complexes import (Abs, re, sign) +from sympy.functions.elementary.exponential import (LambertW, exp, log) +from sympy.functions.elementary.hyperbolic import (atanh, asinh, acosh, acoth, acsch, asech, tanh, sinh) +from sympy.functions.elementary.integers import (ceiling, floor, frac) +from sympy.functions.elementary.miscellaneous import (cbrt, real_root, sqrt) +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (acos, acot, acsc, asec, asin, + atan, cos, cot, csc, sec, sin, tan) +from sympy.functions.special.bessel import (besseli, bessely, besselj, besselk) +from sympy.functions.special.error_functions import (Ei, erf, erfc, erfi, fresnelc, fresnels) +from sympy.functions.special.gamma_functions import (digamma, gamma, uppergamma) +from sympy.functions.special.hyper import meijerg +from sympy.integrals.integrals import (Integral, integrate) +from sympy.series.limits import (Limit, limit) +from sympy.simplify.simplify import (logcombine, simplify) +from sympy.simplify.hyperexpand import hyperexpand + +from sympy.calculus.accumulationbounds import AccumBounds +from sympy.core.mul import Mul +from sympy.series.limits import heuristics +from sympy.series.order import Order +from sympy.testing.pytest import XFAIL, raises + +from sympy import elliptic_e, elliptic_k + +from sympy.abc import x, y, z, k +n = Symbol('n', integer=True, positive=True) + + +def test_basic1(): + assert limit(x, x, oo) is oo + assert limit(x, x, -oo) is -oo + assert limit(-x, x, oo) is -oo + assert limit(x**2, x, -oo) is oo + assert limit(-x**2, x, oo) is -oo + assert limit(x*log(x), x, 0, dir="+") == 0 + assert limit(1/x, x, oo) == 0 + assert limit(exp(x), x, oo) is oo + assert limit(-exp(x), x, oo) is -oo + assert limit(exp(x)/x, x, oo) is oo + assert limit(1/x - exp(-x), x, oo) == 0 + assert limit(x + 1/x, x, oo) is oo + assert limit(x - x**2, x, oo) is -oo + assert limit((1 + x)**(1 + sqrt(2)), x, 0) == 1 + assert limit((1 + x)**oo, x, 0) == Limit((x + 1)**oo, x, 0) + assert limit((1 + x)**oo, x, 0, dir='-') == Limit((x + 1)**oo, x, 0, dir='-') + assert limit((1 + x + y)**oo, x, 0, dir='-') == Limit((1 + x + y)**oo, x, 0, dir='-') + assert limit(y/x/log(x), x, 0) == -oo*sign(y) + assert limit(cos(x + y)/x, x, 0) == sign(cos(y))*oo + assert limit(gamma(1/x + 3), x, oo) == 2 + assert limit(S.NaN, x, -oo) is S.NaN + assert limit(Order(2)*x, x, S.NaN) is S.NaN + assert limit(1/(x - 1), x, 1, dir="+") is oo + assert limit(1/(x - 1), x, 1, dir="-") is -oo + assert limit(1/(5 - x)**3, x, 5, dir="+") is -oo + assert limit(1/(5 - x)**3, x, 5, dir="-") is oo + assert limit(1/sin(x), x, pi, dir="+") is -oo + assert limit(1/sin(x), x, pi, dir="-") is oo + assert limit(1/cos(x), x, pi/2, dir="+") is -oo + assert limit(1/cos(x), x, pi/2, dir="-") is oo + assert limit(1/tan(x**3), x, (2*pi)**Rational(1, 3), dir="+") is oo + assert limit(1/tan(x**3), x, (2*pi)**Rational(1, 3), dir="-") is -oo + assert limit(1/cot(x)**3, x, (pi*Rational(3, 2)), dir="+") is -oo + assert limit(1/cot(x)**3, x, (pi*Rational(3, 2)), dir="-") is oo + assert limit(tan(x), x, oo) == AccumBounds(S.NegativeInfinity, S.Infinity) + assert limit(cot(x), x, oo) == AccumBounds(S.NegativeInfinity, S.Infinity) + assert limit(sec(x), x, oo) == AccumBounds(S.NegativeInfinity, S.Infinity) + assert limit(csc(x), x, oo) == AccumBounds(S.NegativeInfinity, S.Infinity) + + # test bi-directional limits + assert limit(sin(x)/x, x, 0, dir="+-") == 1 + assert limit(x**2, x, 0, dir="+-") == 0 + assert limit(1/x**2, x, 0, dir="+-") is oo + + # test failing bi-directional limits + assert limit(1/x, x, 0, dir="+-") is zoo + # approaching 0 + # from dir="+" + assert limit(1 + 1/x, x, 0) is oo + # from dir='-' + # Add + assert limit(1 + 1/x, x, 0, dir='-') is -oo + # Pow + assert limit(x**(-2), x, 0, dir='-') is oo + assert limit(x**(-3), x, 0, dir='-') is -oo + assert limit(1/sqrt(x), x, 0, dir='-') == (-oo)*I + assert limit(x**2, x, 0, dir='-') == 0 + assert limit(sqrt(x), x, 0, dir='-') == 0 + assert limit(x**-pi, x, 0, dir='-') == -oo*(-1)**(1 - pi) + assert limit((1 + cos(x))**oo, x, 0) == Limit((cos(x) + 1)**oo, x, 0) + + # test pull request 22491 + assert limit(1/asin(x), x, 0, dir = '+') == oo + assert limit(1/asin(x), x, 0, dir = '-') == -oo + assert limit(1/sinh(x), x, 0, dir = '+') == oo + assert limit(1/sinh(x), x, 0, dir = '-') == -oo + assert limit(log(1/x) + 1/sin(x), x, 0, dir = '+') == oo + assert limit(log(1/x) + 1/x, x, 0, dir = '+') == oo + + +def test_basic2(): + assert limit(x**x, x, 0, dir="+") == 1 + assert limit((exp(x) - 1)/x, x, 0) == 1 + assert limit(1 + 1/x, x, oo) == 1 + assert limit(-exp(1/x), x, oo) == -1 + assert limit(x + exp(-x), x, oo) is oo + assert limit(x + exp(-x**2), x, oo) is oo + assert limit(x + exp(-exp(x)), x, oo) is oo + assert limit(13 + 1/x - exp(-x), x, oo) == 13 + + +def test_basic3(): + assert limit(1/x, x, 0, dir="+") is oo + assert limit(1/x, x, 0, dir="-") is -oo + + +def test_basic4(): + assert limit(2*x + y*x, x, 0) == 0 + assert limit(2*x + y*x, x, 1) == 2 + y + assert limit(2*x**8 + y*x**(-3), x, -2) == 512 - y/8 + assert limit(sqrt(x + 1) - sqrt(x), x, oo) == 0 + assert integrate(1/(x**3 + 1), (x, 0, oo)) == 2*pi*sqrt(3)/9 + + +def test_log(): + # https://github.com/sympy/sympy/issues/21598 + a, b, c = symbols('a b c', positive=True) + A = log(a/b) - (log(a) - log(b)) + assert A.limit(a, oo) == 0 + assert (A * c).limit(a, oo) == 0 + + tau, x = symbols('tau x', positive=True) + # The value of manualintegrate in the issue + expr = tau**2*((tau - 1)*(tau + 1)*log(x + 1)/(tau**2 + 1)**2 + 1/((tau**2\ + + 1)*(x + 1)) - (-2*tau*atan(x/tau) + (tau**2/2 - 1/2)*log(tau**2\ + + x**2))/(tau**2 + 1)**2) + assert limit(expr, x, oo) == pi*tau**3/(tau**2 + 1)**2 + + +def test_piecewise(): + # https://github.com/sympy/sympy/issues/18363 + assert limit((real_root(x - 6, 3) + 2)/(x + 2), x, -2, '+') == Rational(1, 12) + + +def test_piecewise2(): + func1 = 2*sqrt(x)*Piecewise(((4*x - 2)/Abs(sqrt(4 - 4*(2*x - 1)**2)), 4*x - 2\ + >= 0), ((2 - 4*x)/Abs(sqrt(4 - 4*(2*x - 1)**2)), True)) + func2 = Piecewise((x**2/2, x <= 0.5), (x/2 - 0.125, True)) + func3 = Piecewise(((x - 9) / 5, x < -1), ((x - 9) / 5, x > 4), (sqrt(Abs(x - 3)), True)) + assert limit(func1, x, 0) == 1 + assert limit(func2, x, 0) == 0 + assert limit(func3, x, -1) == 2 + + +def test_basic5(): + class my(Function): + @classmethod + def eval(cls, arg): + if arg is S.Infinity: + return S.NaN + assert limit(my(x), x, oo) == Limit(my(x), x, oo) + + +def test_issue_3885(): + assert limit(x*y + x*z, z, 2) == x*y + 2*x + + +def test_Limit(): + assert Limit(sin(x)/x, x, 0) != 1 + assert Limit(sin(x)/x, x, 0).doit() == 1 + assert Limit(x, x, 0, dir='+-').args == (x, x, 0, Symbol('+-')) + + +def test_floor(): + assert limit(floor(x), x, -2, "+") == -2 + assert limit(floor(x), x, -2, "-") == -3 + assert limit(floor(x), x, -1, "+") == -1 + assert limit(floor(x), x, -1, "-") == -2 + assert limit(floor(x), x, 0, "+") == 0 + assert limit(floor(x), x, 0, "-") == -1 + assert limit(floor(x), x, 1, "+") == 1 + assert limit(floor(x), x, 1, "-") == 0 + assert limit(floor(x), x, 2, "+") == 2 + assert limit(floor(x), x, 2, "-") == 1 + assert limit(floor(x), x, 248, "+") == 248 + assert limit(floor(x), x, 248, "-") == 247 + + # https://github.com/sympy/sympy/issues/14478 + assert limit(x*floor(3/x)/2, x, 0, '+') == Rational(3, 2) + assert limit(floor(x + 1/2) - floor(x), x, oo) == AccumBounds(-S.Half, S(3)/2) + + # test issue 9158 + assert limit(floor(atan(x)), x, oo) == 1 + assert limit(floor(atan(x)), x, -oo) == -2 + assert limit(ceiling(atan(x)), x, oo) == 2 + assert limit(ceiling(atan(x)), x, -oo) == -1 + + +def test_floor_requires_robust_assumptions(): + assert limit(floor(sin(x)), x, 0, "+") == 0 + assert limit(floor(sin(x)), x, 0, "-") == -1 + assert limit(floor(cos(x)), x, 0, "+") == 0 + assert limit(floor(cos(x)), x, 0, "-") == 0 + assert limit(floor(5 + sin(x)), x, 0, "+") == 5 + assert limit(floor(5 + sin(x)), x, 0, "-") == 4 + assert limit(floor(5 + cos(x)), x, 0, "+") == 5 + assert limit(floor(5 + cos(x)), x, 0, "-") == 5 + + +def test_ceiling(): + assert limit(ceiling(x), x, -2, "+") == -1 + assert limit(ceiling(x), x, -2, "-") == -2 + assert limit(ceiling(x), x, -1, "+") == 0 + assert limit(ceiling(x), x, -1, "-") == -1 + assert limit(ceiling(x), x, 0, "+") == 1 + assert limit(ceiling(x), x, 0, "-") == 0 + assert limit(ceiling(x), x, 1, "+") == 2 + assert limit(ceiling(x), x, 1, "-") == 1 + assert limit(ceiling(x), x, 2, "+") == 3 + assert limit(ceiling(x), x, 2, "-") == 2 + assert limit(ceiling(x), x, 248, "+") == 249 + assert limit(ceiling(x), x, 248, "-") == 248 + + # https://github.com/sympy/sympy/issues/14478 + assert limit(x*ceiling(3/x)/2, x, 0, '+') == Rational(3, 2) + assert limit(ceiling(x + 1/2) - ceiling(x), x, oo) == AccumBounds(-S.Half, S(3)/2) + + +def test_ceiling_requires_robust_assumptions(): + assert limit(ceiling(sin(x)), x, 0, "+") == 1 + assert limit(ceiling(sin(x)), x, 0, "-") == 0 + assert limit(ceiling(cos(x)), x, 0, "+") == 1 + assert limit(ceiling(cos(x)), x, 0, "-") == 1 + assert limit(ceiling(5 + sin(x)), x, 0, "+") == 6 + assert limit(ceiling(5 + sin(x)), x, 0, "-") == 5 + assert limit(ceiling(5 + cos(x)), x, 0, "+") == 6 + assert limit(ceiling(5 + cos(x)), x, 0, "-") == 6 + + +def test_frac(): + assert limit(frac(x), x, oo) == AccumBounds(0, 1) + assert limit(frac(x)**(1/x), x, oo) == AccumBounds(0, 1) + assert limit(frac(x)**(1/x), x, -oo) == AccumBounds(1, oo) + assert limit(frac(x)**x, x, oo) == AccumBounds(0, oo) # wolfram gives (0, 1) + assert limit(frac(sin(x)), x, 0, "+") == 0 + assert limit(frac(sin(x)), x, 0, "-") == 1 + assert limit(frac(cos(x)), x, 0, "+-") == 1 + assert limit(frac(x**2), x, 0, "+-") == 0 + raises(ValueError, lambda: limit(frac(x), x, 0, '+-')) + assert limit(frac(-2*x + 1), x, 0, "+") == 1 + assert limit(frac(-2*x + 1), x, 0, "-") == 0 + assert limit(frac(x + S.Half), x, 0, "+-") == S(1)/2 + assert limit(frac(1/x), x, 0) == AccumBounds(0, 1) + + +def test_issue_14355(): + assert limit(floor(sin(x)/x), x, 0, '+') == 0 + assert limit(floor(sin(x)/x), x, 0, '-') == 0 + # test comment https://github.com/sympy/sympy/issues/14355#issuecomment-372121314 + assert limit(floor(-tan(x)/x), x, 0, '+') == -2 + assert limit(floor(-tan(x)/x), x, 0, '-') == -2 + + +def test_atan(): + x = Symbol("x", real=True) + assert limit(atan(x)*sin(1/x), x, 0) == 0 + assert limit(atan(x) + sqrt(x + 1) - sqrt(x), x, oo) == pi/2 + + +def test_set_signs(): + assert limit(abs(x), x, 0) == 0 + assert limit(abs(sin(x)), x, 0) == 0 + assert limit(abs(cos(x)), x, 0) == 1 + assert limit(abs(sin(x + 1)), x, 0) == sin(1) + + # https://github.com/sympy/sympy/issues/9449 + assert limit((Abs(x + y) - Abs(x - y))/(2*x), x, 0) == sign(y) + + # https://github.com/sympy/sympy/issues/12398 + assert limit(Abs(log(x)/x**3), x, oo) == 0 + assert limit(x*(Abs(log(x)/x**3)/Abs(log(x + 1)/(x + 1)**3) - 1), x, oo) == 3 + + # https://github.com/sympy/sympy/issues/18501 + assert limit(Abs(log(x - 1)**3 - 1), x, 1, '+') == oo + + # https://github.com/sympy/sympy/issues/18997 + assert limit(Abs(log(x)), x, 0) == oo + assert limit(Abs(log(Abs(x))), x, 0) == oo + + # https://github.com/sympy/sympy/issues/19026 + z = Symbol('z', positive=True) + assert limit(Abs(log(z) + 1)/log(z), z, oo) == 1 + + # https://github.com/sympy/sympy/issues/20704 + assert limit(z*(Abs(1/z + y) - Abs(y - 1/z))/2, z, 0) == 0 + + # https://github.com/sympy/sympy/issues/21606 + assert limit(cos(z)/sign(z), z, pi, '-') == -1 + + +def test_heuristic(): + x = Symbol("x", real=True) + assert heuristics(sin(1/x) + atan(x), x, 0, '+') == AccumBounds(-1, 1) + assert limit(log(2 + sqrt(atan(x))*sqrt(sin(1/x))), x, 0) == log(2) + + +def test_issue_3871(): + z = Symbol("z", positive=True) + f = -1/z*exp(-z*x) + assert limit(f, x, oo) == 0 + assert f.limit(x, oo) == 0 + + +def test_exponential(): + n = Symbol('n') + x = Symbol('x', real=True) + assert limit((1 + x/n)**n, n, oo) == exp(x) + assert limit((1 + x/(2*n))**n, n, oo) == exp(x/2) + assert limit((1 + x/(2*n + 1))**n, n, oo) == exp(x/2) + assert limit(((x - 1)/(x + 1))**x, x, oo) == exp(-2) + assert limit(1 + (1 + 1/x)**x, x, oo) == 1 + S.Exp1 + assert limit((2 + 6*x)**x/(6*x)**x, x, oo) == exp(S('1/3')) + + +def test_exponential2(): + n = Symbol('n') + assert limit((1 + x/(n + sin(n)))**n, n, oo) == exp(x) + + +def test_doit(): + f = Integral(2 * x, x) + l = Limit(f, x, oo) + assert l.doit() is oo + + +def test_series_AccumBounds(): + assert limit(sin(k) - sin(k + 1), k, oo) == AccumBounds(-2, 2) + assert limit(cos(k) - cos(k + 1) + 1, k, oo) == AccumBounds(-1, 3) + + # not the exact bound + assert limit(sin(k) - sin(k)*cos(k), k, oo) == AccumBounds(-2, 2) + + # test for issue #9934 + lo = (-3 + cos(1))/2 + hi = (1 + cos(1))/2 + t1 = Mul(AccumBounds(lo, hi), 1/(-1 + cos(1)), evaluate=False) + assert limit(simplify(Sum(cos(n).rewrite(exp), (n, 0, k)).doit().rewrite(sin)), k, oo) == t1 + + t2 = Mul(AccumBounds(-1 + sin(1)/2, sin(1)/2 + 1), 1/(1 - cos(1))) + assert limit(simplify(Sum(sin(n).rewrite(exp), (n, 0, k)).doit().rewrite(sin)), k, oo) == t2 + + assert limit(((sin(x) + 1)/2)**x, x, oo) == AccumBounds(0, oo) # wolfram says 0 + + # https://github.com/sympy/sympy/issues/12312 + e = 2**(-x)*(sin(x) + 1)**x + assert limit(e, x, oo) == AccumBounds(0, oo) + + +def test_bessel_functions_at_infinity(): + # Pull Request 23844 implements limits for all bessel and modified bessel + # functions approaching infinity along any direction i.e. abs(z0) tends to oo + + assert limit(besselj(1, x), x, oo) == 0 + assert limit(besselj(1, x), x, -oo) == 0 + assert limit(besselj(1, x), x, I*oo) == oo*I + assert limit(besselj(1, x), x, -I*oo) == -oo*I + assert limit(bessely(1, x), x, oo) == 0 + assert limit(bessely(1, x), x, -oo) == 0 + assert limit(bessely(1, x), x, I*oo) == -oo + assert limit(bessely(1, x), x, -I*oo) == -oo + assert limit(besseli(1, x), x, oo) == oo + assert limit(besseli(1, x), x, -oo) == -oo + assert limit(besseli(1, x), x, I*oo) == 0 + assert limit(besseli(1, x), x, -I*oo) == 0 + assert limit(besselk(1, x), x, oo) == 0 + assert limit(besselk(1, x), x, -oo) == -oo*I + assert limit(besselk(1, x), x, I*oo) == 0 + assert limit(besselk(1, x), x, -I*oo) == 0 + + # test issue 14874 + assert limit(besselk(0, x), x, oo) == 0 + + +@XFAIL +def test_doit2(): + f = Integral(2 * x, x) + l = Limit(f, x, oo) + # limit() breaks on the contained Integral. + assert l.doit(deep=False) == l + + +def test_issue_2929(): + assert limit((x * exp(x))/(exp(x) - 1), x, -oo) == 0 + + +def test_issue_3792(): + assert limit((1 - cos(x))/x**2, x, S.Half) == 4 - 4*cos(S.Half) + assert limit(sin(sin(x + 1) + 1), x, 0) == sin(1 + sin(1)) + assert limit(abs(sin(x + 1) + 1), x, 0) == 1 + sin(1) + + +def test_issue_4090(): + assert limit(1/(x + 3), x, 2) == Rational(1, 5) + assert limit(1/(x + pi), x, 2) == S.One/(2 + pi) + assert limit(log(x)/(x**2 + 3), x, 2) == log(2)/7 + assert limit(log(x)/(x**2 + pi), x, 2) == log(2)/(4 + pi) + + +def test_issue_4547(): + assert limit(cot(x), x, 0, dir='+') is oo + assert limit(cot(x), x, pi/2, dir='+') == 0 + + +def test_issue_5164(): + assert limit(x**0.5, x, oo) == oo**0.5 is oo + assert limit(x**0.5, x, 16) == 4 # Should this be a float? + assert limit(x**0.5, x, 0) == 0 + assert limit(x**(-0.5), x, oo) == 0 + assert limit(x**(-0.5), x, 4) == S.Half # Should this be a float? + + +def test_issue_5383(): + func = (1.0 * 1 + 1.0 * x)**(1.0 * 1 / x) + assert limit(func, x, 0) == E + + +def test_issue_14793(): + expr = ((x + S(1)/2) * log(x) - x + log(2*pi)/2 - \ + log(factorial(x)) + S(1)/(12*x))*x**3 + assert limit(expr, x, oo) == S(1)/360 + + +def test_issue_5183(): + # using list(...) so py.test can recalculate values + tests = list(product([x, -x], + [-1, 1], + [2, 3, S.Half, Rational(2, 3)], + ['-', '+'])) + results = (oo, oo, -oo, oo, -oo*I, oo, -oo*(-1)**Rational(1, 3), oo, + 0, 0, 0, 0, 0, 0, 0, 0, + oo, oo, oo, -oo, oo, -oo*I, oo, -oo*(-1)**Rational(1, 3), + 0, 0, 0, 0, 0, 0, 0, 0) + assert len(tests) == len(results) + for i, (args, res) in enumerate(zip(tests, results)): + y, s, e, d = args + eq = y**(s*e) + try: + assert limit(eq, x, 0, dir=d) == res + except AssertionError: + if 0: # change to 1 if you want to see the failing tests + print() + print(i, res, eq, d, limit(eq, x, 0, dir=d)) + else: + assert None + + +def test_issue_5184(): + assert limit(sin(x)/x, x, oo) == 0 + assert limit(atan(x), x, oo) == pi/2 + assert limit(gamma(x), x, oo) is oo + assert limit(cos(x)/x, x, oo) == 0 + assert limit(gamma(x), x, S.Half) == sqrt(pi) + + r = Symbol('r', real=True) + assert limit(r*sin(1/r), r, 0) == 0 + + +def test_issue_5229(): + assert limit((1 + y)**(1/y) - S.Exp1, y, 0) == 0 + + +def test_issue_4546(): + # using list(...) so py.test can recalculate values + tests = list(product([cot, tan], + [-pi/2, 0, pi/2, pi, pi*Rational(3, 2)], + ['-', '+'])) + results = (0, 0, -oo, oo, 0, 0, -oo, oo, 0, 0, + oo, -oo, 0, 0, oo, -oo, 0, 0, oo, -oo) + assert len(tests) == len(results) + for i, (args, res) in enumerate(zip(tests, results)): + f, l, d = args + eq = f(x) + try: + assert limit(eq, x, l, dir=d) == res + except AssertionError: + if 0: # change to 1 if you want to see the failing tests + print() + print(i, res, eq, l, d, limit(eq, x, l, dir=d)) + else: + assert None + + +def test_issue_3934(): + assert limit((1 + x**log(3))**(1/x), x, 0) == 1 + assert limit((5**(1/x) + 3**(1/x))**x, x, 0) == 5 + + +def test_issue_5955(): + assert limit((x**16)/(1 + x**16), x, oo) == 1 + assert limit((x**100)/(1 + x**100), x, oo) == 1 + assert limit((x**1885)/(1 + x**1885), x, oo) == 1 + assert limit((x**1000/((x + 1)**1000 + exp(-x))), x, oo) == 1 + + +def test_newissue(): + assert limit(exp(1/sin(x))/exp(cot(x)), x, 0) == 1 + + +def test_extended_real_line(): + assert limit(x - oo, x, oo) == Limit(x - oo, x, oo) + assert limit(1/(x + sin(x)) - oo, x, 0) == Limit(1/(x + sin(x)) - oo, x, 0) + assert limit(oo/x, x, oo) == Limit(oo/x, x, oo) + assert limit(x - oo + 1/x, x, oo) == Limit(x - oo + 1/x, x, oo) + + +@XFAIL +def test_order_oo(): + x = Symbol('x', positive=True) + assert Order(x)*oo != Order(1, x) + assert limit(oo/(x**2 - 4), x, oo) is oo + + +def test_issue_5436(): + raises(NotImplementedError, lambda: limit(exp(x*y), x, oo)) + raises(NotImplementedError, lambda: limit(exp(-x*y), x, oo)) + + +def test_Limit_dir(): + raises(TypeError, lambda: Limit(x, x, 0, dir=0)) + raises(ValueError, lambda: Limit(x, x, 0, dir='0')) + + +def test_polynomial(): + assert limit((x + 1)**1000/((x + 1)**1000 + 1), x, oo) == 1 + assert limit((x + 1)**1000/((x + 1)**1000 + 1), x, -oo) == 1 + assert limit(x ** Rational(77, 3) / (1 + x ** Rational(77, 3)), x, oo) == 1 + assert limit(x ** 101.1 / (1 + x ** 101.1), x, oo) == 1 + + +def test_rational(): + assert limit(1/y - (1/(y + x) + x/(y + x)/y)/z, x, oo) == (z - 1)/(y*z) + assert limit(1/y - (1/(y + x) + x/(y + x)/y)/z, x, -oo) == (z - 1)/(y*z) + + +def test_issue_5740(): + assert limit(log(x)*z - log(2*x)*y, x, 0) == oo*sign(y - z) + + +def test_issue_6366(): + n = Symbol('n', integer=True, positive=True) + r = (n + 1)*x**(n + 1)/(x**(n + 1) - 1) - x/(x - 1) + assert limit(r, x, 1).cancel() == n/2 + + +def test_factorial(): + f = factorial(x) + assert limit(f, x, oo) is oo + assert limit(x/f, x, oo) == 0 + # see Stirling's approximation: + # https://en.wikipedia.org/wiki/Stirling's_approximation + assert limit(f/(sqrt(2*pi*x)*(x/E)**x), x, oo) == 1 + assert limit(f, x, -oo) == gamma(-oo) + + +def test_issue_6560(): + e = (5*x**3/4 - x*Rational(3, 4) + (y*(3*x**2/2 - S.Half) + + 35*x**4/8 - 15*x**2/4 + Rational(3, 8))/(2*(y + 1))) + assert limit(e, y, oo) == 5*x**3/4 + 3*x**2/4 - 3*x/4 - Rational(1, 4) + +@XFAIL +def test_issue_5172(): + n = Symbol('n') + r = Symbol('r', positive=True) + c = Symbol('c') + p = Symbol('p', positive=True) + m = Symbol('m', negative=True) + expr = ((2*n*(n - r + 1)/(n + r*(n - r + 1)))**c + + (r - 1)*(n*(n - r + 2)/(n + r*(n - r + 1)))**c - n)/(n**c - n) + expr = expr.subs(c, c + 1) + raises(NotImplementedError, lambda: limit(expr, n, oo)) + assert limit(expr.subs(c, m), n, oo) == 1 + assert limit(expr.subs(c, p), n, oo).simplify() == \ + (2**(p + 1) + r - 1)/(r + 1)**(p + 1) + + +def test_issue_7088(): + a = Symbol('a') + assert limit(sqrt(x/(x + a)), x, oo) == 1 + + +def test_branch_cuts(): + assert limit(asin(I*x + 2), x, 0) == pi - asin(2) + assert limit(asin(I*x + 2), x, 0, '-') == asin(2) + assert limit(asin(I*x - 2), x, 0) == -asin(2) + assert limit(asin(I*x - 2), x, 0, '-') == -pi + asin(2) + assert limit(acos(I*x + 2), x, 0) == -acos(2) + assert limit(acos(I*x + 2), x, 0, '-') == acos(2) + assert limit(acos(I*x - 2), x, 0) == acos(-2) + assert limit(acos(I*x - 2), x, 0, '-') == 2*pi - acos(-2) + assert limit(atan(x + 2*I), x, 0) == I*atanh(2) + assert limit(atan(x + 2*I), x, 0, '-') == -pi + I*atanh(2) + assert limit(atan(x - 2*I), x, 0) == pi - I*atanh(2) + assert limit(atan(x - 2*I), x, 0, '-') == -I*atanh(2) + assert limit(atan(1/x), x, 0) == pi/2 + assert limit(atan(1/x), x, 0, '-') == -pi/2 + assert limit(atan(x), x, oo) == pi/2 + assert limit(atan(x), x, -oo) == -pi/2 + assert limit(acot(x + S(1)/2*I), x, 0) == pi - I*acoth(S(1)/2) + assert limit(acot(x + S(1)/2*I), x, 0, '-') == -I*acoth(S(1)/2) + assert limit(acot(x - S(1)/2*I), x, 0) == I*acoth(S(1)/2) + assert limit(acot(x - S(1)/2*I), x, 0, '-') == -pi + I*acoth(S(1)/2) + assert limit(acot(x), x, 0) == pi/2 + assert limit(acot(x), x, 0, '-') == -pi/2 + assert limit(asec(I*x + S(1)/2), x, 0) == asec(S(1)/2) + assert limit(asec(I*x + S(1)/2), x, 0, '-') == -asec(S(1)/2) + assert limit(asec(I*x - S(1)/2), x, 0) == 2*pi - asec(-S(1)/2) + assert limit(asec(I*x - S(1)/2), x, 0, '-') == asec(-S(1)/2) + assert limit(acsc(I*x + S(1)/2), x, 0) == acsc(S(1)/2) + assert limit(acsc(I*x + S(1)/2), x, 0, '-') == pi - acsc(S(1)/2) + assert limit(acsc(I*x - S(1)/2), x, 0) == -pi + acsc(S(1)/2) + assert limit(acsc(I*x - S(1)/2), x, 0, '-') == -acsc(S(1)/2) + + assert limit(log(I*x - 1), x, 0) == I*pi + assert limit(log(I*x - 1), x, 0, '-') == -I*pi + assert limit(log(-I*x - 1), x, 0) == -I*pi + assert limit(log(-I*x - 1), x, 0, '-') == I*pi + + assert limit(sqrt(I*x - 1), x, 0) == I + assert limit(sqrt(I*x - 1), x, 0, '-') == -I + assert limit(sqrt(-I*x - 1), x, 0) == -I + assert limit(sqrt(-I*x - 1), x, 0, '-') == I + + assert limit(cbrt(I*x - 1), x, 0) == (-1)**(S(1)/3) + assert limit(cbrt(I*x - 1), x, 0, '-') == -(-1)**(S(2)/3) + assert limit(cbrt(-I*x - 1), x, 0) == -(-1)**(S(2)/3) + assert limit(cbrt(-I*x - 1), x, 0, '-') == (-1)**(S(1)/3) + + +def test_issue_6364(): + a = Symbol('a') + e = z/(1 - sqrt(1 + z)*sin(a)**2 - sqrt(1 - z)*cos(a)**2) + assert limit(e, z, 0) == 1/(cos(a)**2 - S.Half) + + +def test_issue_6682(): + assert limit(exp(2*Ei(-x))/x**2, x, 0) == exp(2*EulerGamma) + + +def test_issue_4099(): + a = Symbol('a') + assert limit(a/x, x, 0) == oo*sign(a) + assert limit(-a/x, x, 0) == -oo*sign(a) + assert limit(-a*x, x, oo) == -oo*sign(a) + assert limit(a*x, x, oo) == oo*sign(a) + + +def test_issue_4503(): + dx = Symbol('dx') + assert limit((sqrt(1 + exp(x + dx)) - sqrt(1 + exp(x)))/dx, dx, 0) == \ + exp(x)/(2*sqrt(exp(x) + 1)) + + +def test_issue_6052(): + G = meijerg((), (), (1,), (0,), -x) + g = hyperexpand(G) + assert limit(g, x, 0, '+-') == 0 + assert limit(g, x, oo) == -oo + + +def test_issue_7224(): + expr = sqrt(x)*besseli(1,sqrt(8*x)) + assert limit(x*diff(expr, x, x)/expr, x, 0) == 2 + assert limit(x*diff(expr, x, x)/expr, x, 1).evalf() == 2.0 + + +def test_issue_7391_8166(): + f = Function('f') + # limit should depend on the continuity of the expression at the point passed + assert limit(f(x), x, 4) == Limit(f(x), x, 4, dir='+') + assert limit(x*f(x)**2/(x**2 + f(x)**4), x, 0) == Limit(x*f(x)**2/(x**2 + f(x)**4), x, 0, dir='+') + + +def test_issue_8208(): + assert limit(n**(Rational(1, 1e9) - 1), n, oo) == 0 + + +def test_issue_8229(): + assert limit((x**Rational(1, 4) - 2)/(sqrt(x) - 4)**Rational(2, 3), x, 16) == 0 + + +def test_issue_8433(): + d, t = symbols('d t', positive=True) + assert limit(erf(1 - t/d), t, oo) == -1 + + +def test_issue_8481(): + k = Symbol('k', integer=True, nonnegative=True) + lamda = Symbol('lamda', positive=True) + assert limit(lamda**k * exp(-lamda) / factorial(k), k, oo) == 0 + + +def test_issue_8462(): + assert limit(binomial(n, n/2), n, oo) == oo + assert limit(binomial(n, n/2) * 3 ** (-n), n, oo) == 0 + + +def test_issue_8634(): + n = Symbol('n', integer=True, positive=True) + x = Symbol('x') + assert limit(x**n, x, -oo) == oo*sign((-1)**n) + + +def test_issue_8635_18176(): + x = Symbol('x', real=True) + k = Symbol('k', positive=True) + assert limit(x**n - x**(n - 0), x, oo) == 0 + assert limit(x**n - x**(n - 5), x, oo) == oo + assert limit(x**n - x**(n - 2.5), x, oo) == oo + assert limit(x**n - x**(n - k - 1), x, oo) == oo + x = Symbol('x', positive=True) + assert limit(x**n - x**(n - 1), x, oo) == oo + assert limit(x**n - x**(n + 2), x, oo) == -oo + + +def test_issue_8730(): + assert limit(subfactorial(x), x, oo) is oo + + +def test_issue_9252(): + n = Symbol('n', integer=True) + c = Symbol('c', positive=True) + assert limit((log(n))**(n/log(n)) / (1 + c)**n, n, oo) == 0 + # limit should depend on the value of c + raises(NotImplementedError, lambda: limit((log(n))**(n/log(n)) / c**n, n, oo)) + + +def test_issue_9558(): + assert limit(sin(x)**15, x, 0, '-') == 0 + + +def test_issue_10801(): + # make sure limits work with binomial + assert limit(16**k / (k * binomial(2*k, k)**2), k, oo) == pi + + +def test_issue_10976(): + s, x = symbols('s x', real=True) + assert limit(erf(s*x)/erf(s), s, 0) == x + + +def test_issue_9041(): + assert limit(factorial(n) / ((n/exp(1))**n * sqrt(2*pi*n)), n, oo) == 1 + + +def test_issue_9205(): + x, y, a = symbols('x, y, a') + assert Limit(x, x, a).free_symbols == {a} + assert Limit(x, x, a, '-').free_symbols == {a} + assert Limit(x + y, x + y, a).free_symbols == {a} + assert Limit(-x**2 + y, x**2, a).free_symbols == {y, a} + + +def test_issue_9471(): + assert limit(((27**(log(n,3)))/n**3),n,oo) == 1 + assert limit(((27**(log(n,3)+1))/n**3),n,oo) == 27 + + +def test_issue_10382(): + assert limit(fibonacci(n + 1)/fibonacci(n), n, oo) == GoldenRatio + + +def test_issue_11496(): + assert limit(erfc(log(1/x)), x, oo) == 2 + + +def test_issue_11879(): + assert simplify(limit(((x+y)**n-x**n)/y, y, 0)) == n*x**(n-1) + + +def test_limit_with_Float(): + k = symbols("k") + assert limit(1.0 ** k, k, oo) == 1 + assert limit(0.3*1.0**k, k, oo) == Rational(3, 10) + + +def test_issue_10610(): + assert limit(3**x*3**(-x - 1)*(x + 1)**2/x**2, x, oo) == Rational(1, 3) + + +def test_issue_10868(): + assert limit(log(x) + asech(x), x, 0, '+') == log(2) + assert limit(log(x) + asech(x), x, 0, '-') == log(2) + 2*I*pi + raises(ValueError, lambda: limit(log(x) + asech(x), x, 0, '+-')) + assert limit(log(x) + asech(x), x, oo) == oo + assert limit(log(x) + acsch(x), x, 0, '+') == log(2) + assert limit(log(x) + acsch(x), x, 0, '-') == -oo + raises(ValueError, lambda: limit(log(x) + acsch(x), x, 0, '+-')) + assert limit(log(x) + acsch(x), x, oo) == oo + + +def test_issue_6599(): + assert limit((n + cos(n))/n, n, oo) == 1 + + +def test_issue_12555(): + assert limit((3**x + 2* x**10) / (x**10 + exp(x)), x, -oo) == 2 + assert limit((3**x + 2* x**10) / (x**10 + exp(x)), x, oo) is oo + + +def test_issue_12769(): + r, z, x = symbols('r z x', real=True) + a, b, s0, K, F0, s, T = symbols('a b s0 K F0 s T', positive=True, real=True) + fx = (F0**b*K**b*r*s0 - sqrt((F0**2*K**(2*b)*a**2*(b - 1) + \ + F0**(2*b)*K**2*a**2*(b - 1) + F0**(2*b)*K**(2*b)*s0**2*(b - 1)*(b**2 - 2*b + 1) - \ + 2*F0**(2*b)*K**(b + 1)*a*r*s0*(b**2 - 2*b + 1) + \ + 2*F0**(b + 1)*K**(2*b)*a*r*s0*(b**2 - 2*b + 1) - \ + 2*F0**(b + 1)*K**(b + 1)*a**2*(b - 1))/((b - 1)*(b**2 - 2*b + 1))))*(b*r - b - r + 1) + + assert fx.subs(K, F0).factor(deep=True) == limit(fx, K, F0).factor(deep=True) + + +def test_issue_13332(): + assert limit(sqrt(30)*5**(-5*x - 1)*(46656*x)**x*(5*x + 2)**(5*x + 5*S.Half) * + (6*x + 2)**(-6*x - 5*S.Half), x, oo) == Rational(25, 36) + + +def test_issue_12564(): + assert limit(x**2 + x*sin(x) + cos(x), x, -oo) is oo + assert limit(x**2 + x*sin(x) + cos(x), x, oo) is oo + assert limit(((x + cos(x))**2).expand(), x, oo) is oo + assert limit(((x + sin(x))**2).expand(), x, oo) is oo + assert limit(((x + cos(x))**2).expand(), x, -oo) is oo + assert limit(((x + sin(x))**2).expand(), x, -oo) is oo + + +def test_issue_14456(): + raises(NotImplementedError, lambda: Limit(exp(x), x, zoo).doit()) + raises(NotImplementedError, lambda: Limit(x**2/(x+1), x, zoo).doit()) + + +def test_issue_14411(): + assert limit(3*sec(4*pi*x - x/3), x, 3*pi/(24*pi - 2)) is -oo + + +def test_issue_13382(): + assert limit(x*(((x + 1)**2 + 1)/(x**2 + 1) - 1), x, oo) == 2 + + +def test_issue_13403(): + assert limit(x*(-1 + (x + log(x + 1) + 1)/(x + log(x))), x, oo) == 1 + + +def test_issue_13416(): + assert limit((-x**3*log(x)**3 + (x - 1)*(x + 1)**2*log(x + 1)**3)/(x**2*log(x)**3), x, oo) == 1 + + +def test_issue_13462(): + assert limit(n**2*(2*n*(-(1 - 1/(2*n))**x + 1) - x - (-x**2/4 + x/4)/n), n, oo) == x**3/24 - x**2/8 + x/12 + + +def test_issue_13750(): + a = Symbol('a') + assert limit(erf(a - x), x, oo) == -1 + assert limit(erf(sqrt(x) - x), x, oo) == -1 + + +def test_issue_14276(): + assert isinstance(limit(sin(x)**log(x), x, oo), Limit) + assert isinstance(limit(sin(x)**cos(x), x, oo), Limit) + assert isinstance(limit(sin(log(cos(x))), x, oo), Limit) + assert limit((1 + 1/(x**2 + cos(x)))**(x**2 + x), x, oo) == E + + +def test_issue_14514(): + assert limit((1/(log(x)**log(x)))**(1/x), x, oo) == 1 + + +def test_issues_14525(): + assert limit(sin(x)**2 - cos(x) + tan(x)*csc(x), x, oo) == AccumBounds(S.NegativeInfinity, S.Infinity) + assert limit(sin(x)**2 - cos(x) + sin(x)*cot(x), x, oo) == AccumBounds(S.NegativeInfinity, S.Infinity) + assert limit(cot(x) - tan(x)**2, x, oo) == AccumBounds(S.NegativeInfinity, S.Infinity) + assert limit(cos(x) - tan(x)**2, x, oo) == AccumBounds(S.NegativeInfinity, S.One) + assert limit(sin(x) - tan(x)**2, x, oo) == AccumBounds(S.NegativeInfinity, S.One) + assert limit(cos(x)**2 - tan(x)**2, x, oo) == AccumBounds(S.NegativeInfinity, S.One) + assert limit(tan(x)**2 + sin(x)**2 - cos(x), x, oo) == AccumBounds(-S.One, S.Infinity) + + +def test_issue_14574(): + assert limit(sqrt(x)*cos(x - x**2) / (x + 1), x, oo) == 0 + + +def test_issue_10102(): + assert limit(fresnels(x), x, oo) == S.Half + assert limit(3 + fresnels(x), x, oo) == 3 + S.Half + assert limit(5*fresnels(x), x, oo) == Rational(5, 2) + assert limit(fresnelc(x), x, oo) == S.Half + assert limit(fresnels(x), x, -oo) == Rational(-1, 2) + assert limit(4*fresnelc(x), x, -oo) == -2 + + +def test_issue_14377(): + raises(NotImplementedError, lambda: limit(exp(I*x)*sin(pi*x), x, oo)) + + +def test_issue_15146(): + e = (x/2) * (-2*x**3 - 2*(x**3 - 1) * x**2 * digamma(x**3 + 1) + \ + 2*(x**3 - 1) * x**2 * digamma(x**3 + x + 1) + x + 3) + assert limit(e, x, oo) == S(1)/3 + + +def test_issue_15202(): + e = (2**x*(2 + 2**(-x)*(-2*2**x + x + 2))/(x + 1))**(x + 1) + assert limit(e, x, oo) == exp(1) + + e = (log(x, 2)**7 + 10*x*factorial(x) + 5**x) / (factorial(x + 1) + 3*factorial(x) + 10**x) + assert limit(e, x, oo) == 10 + + +def test_issue_15282(): + assert limit((x**2000 - (x + 1)**2000) / x**1999, x, oo) == -2000 + + +def test_issue_15984(): + assert limit((-x + log(exp(x) + 1))/x, x, oo, dir='-') == 0 + + +def test_issue_13571(): + assert limit(uppergamma(x, 1) / gamma(x), x, oo) == 1 + + +def test_issue_13575(): + assert limit(acos(erfi(x)), x, 1) == acos(erfi(S.One)) + + +def test_issue_17325(): + assert Limit(sin(x)/x, x, 0, dir="+-").doit() == 1 + assert Limit(x**2, x, 0, dir="+-").doit() == 0 + assert Limit(1/x**2, x, 0, dir="+-").doit() is oo + assert Limit(1/x, x, 0, dir="+-").doit() is zoo + + +def test_issue_10978(): + assert LambertW(x).limit(x, 0) == 0 + + +def test_issue_14313_comment(): + assert limit(floor(n/2), n, oo) is oo + + +def test_issue_15323(): + d = ((1 - 1/x)**x).diff(x) + assert limit(d, x, 1, dir='+') == 1 + + +def test_issue_12571(): + assert limit(-LambertW(-log(x))/log(x), x, 1) == 1 + + +def test_issue_14590(): + assert limit((x**3*((x + 1)/x)**x)/((x + 1)*(x + 2)*(x + 3)), x, oo) == exp(1) + + +def test_issue_14393(): + a, b = symbols('a b') + assert limit((x**b - y**b)/(x**a - y**a), x, y) == b*y**(-a + b)/a + + +def test_issue_14556(): + assert limit(factorial(n + 1)**(1/(n + 1)) - factorial(n)**(1/n), n, oo) == exp(-1) + + +def test_issue_14811(): + assert limit(((1 + ((S(2)/3)**(x + 1)))**(2**x))/(2**((S(4)/3)**(x - 1))), x, oo) == oo + + +def test_issue_16222(): + assert limit(exp(x), x, 1000000000) == exp(1000000000) + + +def test_issue_16714(): + assert limit(((x**(x + 1) + (x + 1)**x) / x**(x + 1))**x, x, oo) == exp(exp(1)) + + +def test_issue_16722(): + z = symbols('z', positive=True) + assert limit(binomial(n + z, n)*n**-z, n, oo) == 1/gamma(z + 1) + z = symbols('z', positive=True, integer=True) + assert limit(binomial(n + z, n)*n**-z, n, oo) == 1/gamma(z + 1) + + +def test_issue_17431(): + assert limit(((n + 1) + 1) / (((n + 1) + 2) * factorial(n + 1)) * + (n + 2) * factorial(n) / (n + 1), n, oo) == 0 + assert limit((n + 2)**2*factorial(n)/((n + 1)*(n + 3)*factorial(n + 1)) + , n, oo) == 0 + assert limit((n + 1) * factorial(n) / (n * factorial(n + 1)), n, oo) == 0 + + +def test_issue_17671(): + assert limit(Ei(-log(x)) - log(log(x))/x, x, 1) == EulerGamma + + +def test_issue_17751(): + a, b, c, x = symbols('a b c x', positive=True) + assert limit((a + 1)*x - sqrt((a + 1)**2*x**2 + b*x + c), x, oo) == -b/(2*a + 2) + + +def test_issue_17792(): + assert limit(factorial(n)/sqrt(n)*(exp(1)/n)**n, n, oo) == sqrt(2)*sqrt(pi) + + +def test_issue_18118(): + assert limit(sign(sin(x)), x, 0, "-") == -1 + assert limit(sign(sin(x)), x, 0, "+") == 1 + + +def test_issue_18306(): + assert limit(sin(sqrt(x))/sqrt(sin(x)), x, 0, '+') == 1 + + +def test_issue_18378(): + assert limit(log(exp(3*x) + x)/log(exp(x) + x**100), x, oo) == 3 + + +def test_issue_18399(): + assert limit((1 - S(1)/2*x)**(3*x), x, oo) is zoo + assert limit((-x)**x, x, oo) is zoo + + +def test_issue_18442(): + assert limit(tan(x)**(2**(sqrt(pi))), x, oo, dir='-') == Limit(tan(x)**(2**(sqrt(pi))), x, oo, dir='-') + + +def test_issue_18452(): + assert limit(abs(log(x))**x, x, 0) == 1 + assert limit(abs(log(x))**x, x, 0, "-") == 1 + + +def test_issue_18473(): + assert limit(sin(x)**(1/x), x, oo) == Limit(sin(x)**(1/x), x, oo, dir='-') + assert limit(cos(x)**(1/x), x, oo) == Limit(cos(x)**(1/x), x, oo, dir='-') + assert limit(tan(x)**(1/x), x, oo) == Limit(tan(x)**(1/x), x, oo, dir='-') + assert limit((cos(x) + 2)**(1/x), x, oo) == 1 + assert limit((sin(x) + 10)**(1/x), x, oo) == 1 + assert limit((cos(x) - 2)**(1/x), x, oo) == Limit((cos(x) - 2)**(1/x), x, oo, dir='-') + assert limit((cos(x) + 1)**(1/x), x, oo) == AccumBounds(0, 1) + assert limit((tan(x)**2)**(2/x) , x, oo) == AccumBounds(0, oo) + assert limit((sin(x)**2)**(1/x), x, oo) == AccumBounds(0, 1) + # Tests for issue #23751 + assert limit((cos(x) + 1)**(1/x), x, -oo) == AccumBounds(1, oo) + assert limit((sin(x)**2)**(1/x), x, -oo) == AccumBounds(1, oo) + assert limit((tan(x)**2)**(2/x) , x, -oo) == AccumBounds(0, oo) + + +def test_issue_18482(): + assert limit((2*exp(3*x)/(exp(2*x) + 1))**(1/x), x, oo) == exp(1) + + +def test_issue_18508(): + assert limit(sin(x)/sqrt(1-cos(x)), x, 0) == sqrt(2) + assert limit(sin(x)/sqrt(1-cos(x)), x, 0, dir='+') == sqrt(2) + assert limit(sin(x)/sqrt(1-cos(x)), x, 0, dir='-') == -sqrt(2) + + +def test_issue_18521(): + raises(NotImplementedError, lambda: limit(exp((2 - n) * x), x, oo)) + + +def test_issue_18969(): + a, b = symbols('a b', positive=True) + assert limit(LambertW(a), a, b) == LambertW(b) + assert limit(exp(LambertW(a)), a, b) == exp(LambertW(b)) + + +def test_issue_18992(): + assert limit(n/(factorial(n)**(1/n)), n, oo) == exp(1) + + +def test_issue_19067(): + x = Symbol('x') + assert limit(gamma(x)/(gamma(x - 1)*gamma(x + 2)), x, 0) == -1 + + +def test_issue_19586(): + assert limit(x**(2**x*3**(-x)), x, oo) == 1 + + +def test_issue_13715(): + n = Symbol('n') + p = Symbol('p', zero=True) + assert limit(n + p, n, 0) == 0 + + +def test_issue_15055(): + assert limit(n**3*((-n - 1)*sin(1/n) + (n + 2)*sin(1/(n + 1)))/(-n + 1), n, oo) == 1 + + +def test_issue_16708(): + m, vi = symbols('m vi', positive=True) + B, ti, d = symbols('B ti d') + assert limit((B*ti*vi - sqrt(m)*sqrt(-2*B*d*vi + m*(vi)**2) + m*vi)/(B*vi), B, 0) == (d + ti*vi)/vi + + +def test_issue_19154(): + assert limit(besseli(1, 3 *x)/(x *besseli(1, x)**3), x , oo) == 2*sqrt(3)*pi/3 + assert limit(besseli(1, 3 *x)/(x *besseli(1, x)**3), x , -oo) == -2*sqrt(3)*pi/3 + + +def test_issue_19453(): + beta = Symbol("beta", positive=True) + h = Symbol("h", positive=True) + m = Symbol("m", positive=True) + w = Symbol("omega", positive=True) + g = Symbol("g", positive=True) + + e = exp(1) + q = 3*h**2*beta*g*e**(0.5*h*beta*w) + p = m**2*w**2 + s = e**(h*beta*w) - 1 + Z = -q/(4*p*s) - q/(2*p*s**2) - q*(e**(h*beta*w) + 1)/(2*p*s**3)\ + + e**(0.5*h*beta*w)/s + E = -diff(log(Z), beta) + + assert limit(E - 0.5*h*w, beta, oo) == 0 + assert limit(E.simplify() - 0.5*h*w, beta, oo) == 0 + + +def test_issue_19739(): + assert limit((-S(1)/4)**x, x, oo) == 0 + + +def test_issue_19766(): + assert limit(2**(-x)*sqrt(4**(x + 1) + 1), x, oo) == 2 + + +def test_issue_19770(): + m = Symbol('m') + # the result is not 0 for non-real m + assert limit(cos(m*x)/x, x, oo) == Limit(cos(m*x)/x, x, oo, dir='-') + m = Symbol('m', real=True) + # can be improved to give the correct result 0 + assert limit(cos(m*x)/x, x, oo) == Limit(cos(m*x)/x, x, oo, dir='-') + m = Symbol('m', nonzero=True) + assert limit(cos(m*x), x, oo) == AccumBounds(-1, 1) + assert limit(cos(m*x)/x, x, oo) == 0 + + +def test_issue_7535(): + assert limit(tan(x)/sin(tan(x)), x, pi/2) == Limit(tan(x)/sin(tan(x)), x, pi/2, dir='+') + assert limit(tan(x)/sin(tan(x)), x, pi/2, dir='-') == Limit(tan(x)/sin(tan(x)), x, pi/2, dir='-') + assert limit(tan(x)/sin(tan(x)), x, pi/2, dir='+-') == Limit(tan(x)/sin(tan(x)), x, pi/2, dir='+-') + assert limit(sin(tan(x)),x,pi/2) == AccumBounds(-1, 1) + assert -oo*(1/sin(-oo)) == AccumBounds(-oo, oo) + assert oo*(1/sin(oo)) == AccumBounds(-oo, oo) + assert oo*(1/sin(-oo)) == AccumBounds(-oo, oo) + assert -oo*(1/sin(oo)) == AccumBounds(-oo, oo) + + +def test_issue_20365(): + assert limit(((x + 1)**(1/x) - E)/x, x, 0) == -E/2 + + +def test_issue_21031(): + assert limit(((1 + x)**(1/x) - (1 + 2*x)**(1/(2*x)))/asin(x), x, 0) == E/2 + + +def test_issue_21038(): + assert limit(sin(pi*x)/(3*x - 12), x, 4) == pi/3 + + +def test_issue_20578(): + expr = abs(x) * sin(1/x) + assert limit(expr,x,0,'+') == 0 + assert limit(expr,x,0,'-') == 0 + assert limit(expr,x,0,'+-') == 0 + + +def test_issue_21227(): + f = log(x) + + assert f.nseries(x, logx=y) == y + assert f.nseries(x, logx=-x) == -x + + f = log(-log(x)) + + assert f.nseries(x, logx=y) == log(-y) + assert f.nseries(x, logx=-x) == log(x) + + f = log(log(x)) + + assert f.nseries(x, logx=y) == log(y) + assert f.nseries(x, logx=-x) == log(-x) + assert f.nseries(x, logx=x) == log(x) + + f = log(log(log(1/x))) + + assert f.nseries(x, logx=y) == log(log(-y)) + assert f.nseries(x, logx=-y) == log(log(y)) + assert f.nseries(x, logx=x) == log(log(-x)) + assert f.nseries(x, logx=-x) == log(log(x)) + + +def test_issue_21415(): + exp = (x-1)*cos(1/(x-1)) + assert exp.limit(x,1) == 0 + assert exp.expand().limit(x,1) == 0 + + +def test_issue_21530(): + assert limit(sinh(n + 1)/sinh(n), n, oo) == E + + +def test_issue_21550(): + r = (sqrt(5) - 1)/2 + assert limit((x - r)/(x**2 + x - 1), x, r) == sqrt(5)/5 + + +def test_issue_21661(): + out = limit((x**(x + 1) * (log(x) + 1) + 1) / x, x, 11) + assert out == S(3138428376722)/11 + 285311670611*log(11) + + +def test_issue_21701(): + assert limit((besselj(z, x)/x**z).subs(z, 7), x, 0) == S(1)/645120 + + +def test_issue_21721(): + a = Symbol('a', real=True) + I = integrate(1/(pi*(1 + (x - a)**2)), x) + assert I.limit(x, oo) == S.Half + + +def test_issue_21756(): + term = (1 - exp(-2*I*pi*z))/(1 - exp(-2*I*pi*z/5)) + assert term.limit(z, 0) == 5 + assert re(term).limit(z, 0) == 5 + + +def test_issue_21785(): + a = Symbol('a') + assert sqrt((-a**2 + x**2)/(1 - x**2)).limit(a, 1, '-') == I + + +def test_issue_22181(): + assert limit((-1)**x * 2**(-x), x, oo) == 0 + + +def test_issue_22220(): + e1 = sqrt(30)*atan(sqrt(30)*tan(x/2)/6)/30 + e2 = sqrt(30)*I*(-log(sqrt(2)*tan(x/2) - 2*sqrt(15)*I/5) + + +log(sqrt(2)*tan(x/2) + 2*sqrt(15)*I/5))/60 + + assert limit(e1, x, -pi) == -sqrt(30)*pi/60 + assert limit(e2, x, -pi) == -sqrt(30)*pi/30 + + assert limit(e1, x, -pi, '-') == sqrt(30)*pi/60 + assert limit(e2, x, -pi, '-') == 0 + + # test https://github.com/sympy/sympy/issues/22220#issuecomment-972727694 + expr = log(x - I) - log(-x - I) + expr2 = logcombine(expr, force=True) + assert limit(expr, x, oo) == limit(expr2, x, oo) == I*pi + + # test https://github.com/sympy/sympy/issues/22220#issuecomment-1077618340 + expr = expr = (-log(tan(x/2) - I) +log(tan(x/2) + I)) + assert limit(expr, x, pi, '+') == 2*I*pi + assert limit(expr, x, pi, '-') == 0 + + +def test_issue_22334(): + k, n = symbols('k, n', positive=True) + assert limit((n+1)**k/((n+1)**(k+1) - (n)**(k+1)), n, oo) == 1/(k + 1) + assert limit((n+1)**k/((n+1)**(k+1) - (n)**(k+1)).expand(), n, oo) == 1/(k + 1) + assert limit((n+1)**k/(n*(-n**k + (n + 1)**k) + (n + 1)**k), n, oo) == 1/(k + 1) + + +def test_issue_22836_limit(): + assert limit(2**(1/x)/factorial(1/(x)), x, 0) == S.Zero + + +def test_sympyissue_22986(): + assert limit(acosh(1 + 1/x)*sqrt(x), x, oo) == sqrt(2) + + +def test_issue_23231(): + f = (2**x - 2**(-x))/(2**x + 2**(-x)) + assert limit(f, x, -oo) == -1 + + +def test_issue_23596(): + assert integrate(((1 + x)/x**2)*exp(-1/x), (x, 0, oo)) == oo + + +def test_issue_23752(): + expr1 = sqrt(-I*x**2 + x - 3) + expr2 = sqrt(-I*x**2 + I*x - 3) + assert limit(expr1, x, 0, '+') == -sqrt(3)*I + assert limit(expr1, x, 0, '-') == -sqrt(3)*I + assert limit(expr2, x, 0, '+') == sqrt(3)*I + assert limit(expr2, x, 0, '-') == -sqrt(3)*I + + +def test_issue_24276(): + fx = log(tan(pi/2*tanh(x))).diff(x) + assert fx.limit(x, oo) == 2 + assert fx.simplify().limit(x, oo) == 2 + assert fx.rewrite(sin).limit(x, oo) == 2 + assert fx.rewrite(sin).simplify().limit(x, oo) == 2 + +def test_issue_25230(): + a = Symbol('a', real = True) + b = Symbol('b', positive = True) + c = Symbol('c', negative = True) + n = Symbol('n', integer = True) + raises(NotImplementedError, lambda: limit(Mod(x, a), x, a)) + assert limit(Mod(x, b), x, n*b, '+') == 0 + assert limit(Mod(x, b), x, n*b, '-') == b + assert limit(Mod(x, c), x, n*c, '+') == c + assert limit(Mod(x, c), x, n*c, '-') == 0 + + +def test_issue_25582(): + + assert limit(asin(exp(x)), x, oo, '-') == -oo*I + assert limit(acos(exp(x)), x, oo, '-') == oo*I + assert limit(atan(exp(x)), x, oo, '-') == pi/2 + assert limit(acot(exp(x)), x, oo, '-') == 0 + assert limit(asec(exp(x)), x, oo, '-') == pi/2 + assert limit(acsc(exp(x)), x, oo, '-') == 0 + + +def test_issue_25847(): + #atan + assert limit(atan(sin(x)/x), x, 0, '+-') == pi/4 + assert limit(atan(exp(1/x)), x, 0, '+') == pi/2 + assert limit(atan(exp(1/x)), x, 0, '-') == 0 + + #asin + assert limit(asin(sin(x)/x), x, 0, '+-') == pi/2 + assert limit(asin(exp(1/x)), x, 0, '+') == -oo*I + assert limit(asin(exp(1/x)), x, 0, '-') == 0 + + #acos + assert limit(acos(sin(x)/x), x, 0, '+-') == 0 + assert limit(acos(exp(1/x)), x, 0, '+') == oo*I + assert limit(acos(exp(1/x)), x, 0, '-') == pi/2 + + #acot + assert limit(acot(sin(x)/x), x, 0, '+-') == pi/4 + assert limit(acot(exp(1/x)), x, 0, '+') == 0 + assert limit(acot(exp(1/x)), x, 0, '-') == pi/2 + + #asec + assert limit(asec(sin(x)/x), x, 0, '+-') == 0 + assert limit(asec(exp(1/x)), x, 0, '+') == pi/2 + assert limit(asec(exp(1/x)), x, 0, '-') == oo*I + + #acsc + assert limit(acsc(sin(x)/x), x, 0, '+-') == pi/2 + assert limit(acsc(exp(1/x)), x, 0, '+') == 0 + assert limit(acsc(exp(1/x)), x, 0, '-') == -oo*I + + #atanh + assert limit(atanh(sin(x)/x), x, 0, '+-') == oo + assert limit(atanh(exp(1/x)), x, 0, '+') == -I*pi/2 + assert limit(atanh(exp(1/x)), x, 0, '-') == 0 + + #asinh + assert limit(asinh(sin(x)/x), x, 0, '+-') == log(1 + sqrt(2)) + assert limit(asinh(exp(1/x)), x, 0, '+') == oo + assert limit(asinh(exp(1/x)), x, 0, '-') == 0 + + #acosh + assert limit(acosh(sin(x)/x), x, 0, '+-') == 0 + assert limit(acosh(exp(1/x)), x, 0, '+') == oo + assert limit(acosh(exp(1/x)), x, 0, '-') == I*pi/2 + + #acoth + assert limit(acoth(sin(x)/x), x, 0, '+-') == oo + assert limit(acoth(exp(1/x)), x, 0, '+') == 0 + assert limit(acoth(exp(1/x)), x, 0, '-') == -I*pi/2 + + #asech + assert limit(asech(sin(x)/x), x, 0, '+-') == 0 + assert limit(asech(exp(1/x)), x, 0, '+') == I*pi/2 + assert limit(asech(exp(1/x)), x, 0, '-') == oo + + #acsch + assert limit(acsch(sin(x)/x), x, 0, '+-') == log(1 + sqrt(2)) + assert limit(acsch(exp(1/x)), x, 0, '+') == 0 + assert limit(acsch(exp(1/x)), x, 0, '-') == oo + + +def test_issue_26040(): + assert limit(besseli(0, x + 1)/besseli(0, x), x, oo) == S.Exp1 + + +def test_issue_26250(): + e = elliptic_e(4*x/(x**2 + 2*x + 1)) + k = elliptic_k(4*x/(x**2 + 2*x + 1)) + e1 = ((1-3*x**2)*e**2/2 - (x**2-2*x+1)*e*k/2) + e2 = pi**2*(x**8 - 2*x**7 - x**6 + 4*x**5 - x**4 - 2*x**3 + x**2) + assert limit(e1/e2, x, 0) == -S(1)/8 + + +def test_issue_26513(): + assert limit(abs((-x/(x+1))**x), x ,oo) == exp(-1) + assert limit((x/(x + 1))**x, x, oo) == exp(-1) + raises (NotImplementedError, lambda: limit((-x/(x+1))**x, x, oo)) + + +def test_issue_26916(): + assert limit(Ei(x)*exp(-x), x, +oo) == 0 + assert limit(Ei(x)*exp(-x), x, -oo) == 0 + + +def test_issue_22982_15323(): + assert limit((log(E + 1/x) - 1)**(1 - sqrt(E + 1/x)), x, oo) == oo + assert limit((1 - 1/x)**x*(log(1 - 1/x) + 1/(x*(1 - 1/x))), x, 1, dir='+') == 1 + assert limit((log(E + 1/x) )**(1 - sqrt(E + 1/x)), x, oo) == 1 + assert limit((log(E + 1/x) - 1)**(- sqrt(E + 1/x)), x, oo) == oo + + +def test_issue_26991(): + assert limit(x/((x - 6)*sinh(tanh(0.03*x)) + tanh(x) - 0.5), x, oo) == 1/sinh(1) + +def test_issue_27278(): + expr = (1/(x*log((x + 3)/x)))**x*((x + 1)*log((x + 4)/(x + 1)))**(x + 1)/3 + assert limit(expr, x, oo) == 1 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_limitseq.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_limitseq.py new file mode 100644 index 0000000000000000000000000000000000000000..362bb0397feb0ec63929920855c81279eca0bd6a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_limitseq.py @@ -0,0 +1,177 @@ +from sympy.concrete.summations import Sum +from sympy.core.add import Add +from sympy.core.numbers import (I, Rational, oo, pi) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.combinatorial.factorials import (binomial, factorial, subfactorial) +from sympy.functions.combinatorial.numbers import (fibonacci, harmonic) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.functions.special.gamma_functions import gamma +from sympy.series.limitseq import limit_seq +from sympy.series.limitseq import difference_delta as dd +from sympy.testing.pytest import raises, XFAIL +from sympy.calculus.accumulationbounds import AccumulationBounds + +n, m, k = symbols('n m k', integer=True) + + +def test_difference_delta(): + e = n*(n + 1) + e2 = e * k + + assert dd(e) == 2*n + 2 + assert dd(e2, n, 2) == k*(4*n + 6) + + raises(ValueError, lambda: dd(e2)) + raises(ValueError, lambda: dd(e2, n, oo)) + + +def test_difference_delta__Sum(): + e = Sum(1/k, (k, 1, n)) + assert dd(e, n) == 1/(n + 1) + assert dd(e, n, 5) == Add(*[1/(i + n + 1) for i in range(5)]) + + e = Sum(1/k, (k, 1, 3*n)) + assert dd(e, n) == Add(*[1/(i + 3*n + 1) for i in range(3)]) + + e = n * Sum(1/k, (k, 1, n)) + assert dd(e, n) == 1 + Sum(1/k, (k, 1, n)) + + e = Sum(1/k, (k, 1, n), (m, 1, n)) + assert dd(e, n) == harmonic(n) + + +def test_difference_delta__Add(): + e = n + n*(n + 1) + assert dd(e, n) == 2*n + 3 + assert dd(e, n, 2) == 4*n + 8 + + e = n + Sum(1/k, (k, 1, n)) + assert dd(e, n) == 1 + 1/(n + 1) + assert dd(e, n, 5) == 5 + Add(*[1/(i + n + 1) for i in range(5)]) + + +def test_difference_delta__Pow(): + e = 4**n + assert dd(e, n) == 3*4**n + assert dd(e, n, 2) == 15*4**n + + e = 4**(2*n) + assert dd(e, n) == 15*4**(2*n) + assert dd(e, n, 2) == 255*4**(2*n) + + e = n**4 + assert dd(e, n) == (n + 1)**4 - n**4 + + e = n**n + assert dd(e, n) == (n + 1)**(n + 1) - n**n + + +def test_limit_seq(): + e = binomial(2*n, n) / Sum(binomial(2*k, k), (k, 1, n)) + assert limit_seq(e) == S(3) / 4 + assert limit_seq(e, m) == e + + e = (5*n**3 + 3*n**2 + 4) / (3*n**3 + 4*n - 5) + assert limit_seq(e, n) == S(5) / 3 + + e = (harmonic(n) * Sum(harmonic(k), (k, 1, n))) / (n * harmonic(2*n)**2) + assert limit_seq(e, n) == 1 + + e = Sum(k**2 * Sum(2**m/m, (m, 1, k)), (k, 1, n)) / (2**n*n) + assert limit_seq(e, n) == 4 + + e = (Sum(binomial(3*k, k) * binomial(5*k, k), (k, 1, n)) / + (binomial(3*n, n) * binomial(5*n, n))) + assert limit_seq(e, n) == S(84375) / 83351 + + e = Sum(harmonic(k)**2/k, (k, 1, 2*n)) / harmonic(n)**3 + assert limit_seq(e, n) == S.One / 3 + + raises(ValueError, lambda: limit_seq(e * m)) + + +def test_alternating_sign(): + assert limit_seq((-1)**n/n**2, n) == 0 + assert limit_seq((-2)**(n+1)/(n + 3**n), n) == 0 + assert limit_seq((2*n + (-1)**n)/(n + 1), n) == 2 + assert limit_seq(sin(pi*n), n) == 0 + assert limit_seq(cos(2*pi*n), n) == 1 + assert limit_seq((S.NegativeOne/5)**n, n) == 0 + assert limit_seq((Rational(-1, 5))**n, n) == 0 + assert limit_seq((I/3)**n, n) == 0 + assert limit_seq(sqrt(n)*(I/2)**n, n) == 0 + assert limit_seq(n**7*(I/3)**n, n) == 0 + assert limit_seq(n/(n + 1) + (I/2)**n, n) == 1 + + +def test_accum_bounds(): + assert limit_seq((-1)**n, n) == AccumulationBounds(-1, 1) + assert limit_seq(cos(pi*n), n) == AccumulationBounds(-1, 1) + assert limit_seq(sin(pi*n/2)**2, n) == AccumulationBounds(0, 1) + assert limit_seq(2*(-3)**n/(n + 3**n), n) == AccumulationBounds(-2, 2) + assert limit_seq(3*n/(n + 1) + 2*(-1)**n, n) == AccumulationBounds(1, 5) + + +def test_limitseq_sum(): + from sympy.abc import x, y, z + assert limit_seq(Sum(1/x, (x, 1, y)) - log(y), y) == S.EulerGamma + assert limit_seq(Sum(1/x, (x, 1, y)) - 1/y, y) is S.Infinity + assert (limit_seq(binomial(2*x, x) / Sum(binomial(2*y, y), (y, 1, x)), x) == + S(3) / 4) + assert (limit_seq(Sum(y**2 * Sum(2**z/z, (z, 1, y)), (y, 1, x)) / + (2**x*x), x) == 4) + + +def test_issue_9308(): + assert limit_seq(subfactorial(n)/factorial(n), n) == exp(-1) + + +def test_issue_10382(): + n = Symbol('n', integer=True) + assert limit_seq(fibonacci(n+1)/fibonacci(n), n).together() == S.GoldenRatio + + +def test_issue_11672(): + assert limit_seq(Rational(-1, 2)**n, n) == 0 + + +def test_issue_14196(): + k, n = symbols('k, n', positive=True) + m = Symbol('m') + assert limit_seq(Sum(m**k, (m, 1, n)).doit()/(n**(k + 1)), n) == 1/(k + 1) + + +def test_issue_16735(): + assert limit_seq(5**n/factorial(n), n) == 0 + + +def test_issue_19868(): + assert limit_seq(1/gamma(n + S.One/2), n) == 0 + + +@XFAIL +def test_limit_seq_fail(): + # improve Summation algorithm or add ad-hoc criteria + e = (harmonic(n)**3 * Sum(1/harmonic(k), (k, 1, n)) / + (n * Sum(harmonic(k)/k, (k, 1, n)))) + assert limit_seq(e, n) == 2 + + # No unique dominant term + e = (Sum(2**k * binomial(2*k, k) / k**2, (k, 1, n)) / + (Sum(2**k/k*2, (k, 1, n)) * Sum(binomial(2*k, k), (k, 1, n)))) + assert limit_seq(e, n) == S(3) / 7 + + # Simplifications of summations needs to be improved. + e = n**3*Sum(2**k/k**2, (k, 1, n))**2 / (2**n * Sum(2**k/k, (k, 1, n))) + assert limit_seq(e, n) == 2 + + e = (harmonic(n) * Sum(2**k/k, (k, 1, n)) / + (n * Sum(2**k*harmonic(k)/k**2, (k, 1, n)))) + assert limit_seq(e, n) == 1 + + e = (Sum(2**k*factorial(k) / k**2, (k, 1, 2*n)) / + (Sum(4**k/k**2, (k, 1, n)) * Sum(factorial(k), (k, 1, 2*n)))) + assert limit_seq(e, n) == S(3) / 16 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_lseries.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_lseries.py new file mode 100644 index 0000000000000000000000000000000000000000..42d327bf60c76eebdc4570d631efef4bc84b58e3 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_lseries.py @@ -0,0 +1,65 @@ +from sympy.core.numbers import E +from sympy.core.singleton import S +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.hyperbolic import tanh +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.series.order import Order +from sympy.abc import x, y + + +def test_sin(): + e = sin(x).lseries(x) + assert next(e) == x + assert next(e) == -x**3/6 + assert next(e) == x**5/120 + + +def test_cos(): + e = cos(x).lseries(x) + assert next(e) == 1 + assert next(e) == -x**2/2 + assert next(e) == x**4/24 + + +def test_exp(): + e = exp(x).lseries(x) + assert next(e) == 1 + assert next(e) == x + assert next(e) == x**2/2 + assert next(e) == x**3/6 + + +def test_exp2(): + e = exp(cos(x)).lseries(x) + assert next(e) == E + assert next(e) == -E*x**2/2 + assert next(e) == E*x**4/6 + assert next(e) == -31*E*x**6/720 + + +def test_simple(): + assert list(x.lseries()) == [x] + assert list(S.One.lseries(x)) == [1] + assert not next((x/(x + y)).lseries(y)).has(Order) + + +def test_issue_5183(): + s = (x + 1/x).lseries() + assert list(s) == [1/x, x] + assert next((x + x**2).lseries()) == x + assert next(((1 + x)**7).lseries(x)) == 1 + assert next((sin(x + y)).series(x, n=3).lseries(y)) == x + # it would be nice if all terms were grouped, but in the + # following case that would mean that all the terms would have + # to be known since, for example, every term has a constant in it. + s = ((1 + x)**7).series(x, 1, n=None) + assert [next(s) for i in range(2)] == [128, -448 + 448*x] + + +def test_issue_6999(): + s = tanh(x).lseries(x, 1) + assert next(s) == tanh(1) + assert next(s) == x - (x - 1)*tanh(1)**2 - 1 + assert next(s) == -(x - 1)**2*tanh(1) + (x - 1)**2*tanh(1)**3 + assert next(s) == -(x - 1)**3*tanh(1)**4 - (x - 1)**3/3 + \ + 4*(x - 1)**3*tanh(1)**2/3 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_nseries.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_nseries.py new file mode 100644 index 0000000000000000000000000000000000000000..a2f20add82d3e858e2ce145fc9fcd4a6548a48cc --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_nseries.py @@ -0,0 +1,557 @@ +from sympy.calculus.util import AccumBounds +from sympy.core.function import (Derivative, PoleError) +from sympy.core.numbers import (E, I, Integer, Rational, pi) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.complexes import sign +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.hyperbolic import (acosh, acoth, asinh, atanh, cosh, coth, sinh, tanh) +from sympy.functions.elementary.integers import (ceiling, floor, frac) +from sympy.functions.elementary.miscellaneous import (cbrt, sqrt) +from sympy.functions.elementary.trigonometric import (asin, cos, cot, sin, tan) +from sympy.series.limits import limit +from sympy.series.order import O +from sympy.abc import x, y, z + +from sympy.testing.pytest import raises, XFAIL + + +def test_simple_1(): + assert x.nseries(x, n=5) == x + assert y.nseries(x, n=5) == y + assert (1/(x*y)).nseries(y, n=5) == 1/(x*y) + assert Rational(3, 4).nseries(x, n=5) == Rational(3, 4) + assert x.nseries() == x + + +def test_mul_0(): + assert (x*log(x)).nseries(x, n=5) == x*log(x) + + +def test_mul_1(): + assert (x*log(2 + x)).nseries(x, n=5) == x*log(2) + x**2/2 - x**3/8 + \ + x**4/24 + O(x**5) + assert (x*log(1 + x)).nseries( + x, n=5) == x**2 - x**3/2 + x**4/3 + O(x**5) + + +def test_pow_0(): + assert (x**2).nseries(x, n=5) == x**2 + assert (1/x).nseries(x, n=5) == 1/x + assert (1/x**2).nseries(x, n=5) == 1/x**2 + assert (x**Rational(2, 3)).nseries(x, n=5) == (x**Rational(2, 3)) + assert (sqrt(x)**3).nseries(x, n=5) == (sqrt(x)**3) + + +def test_pow_1(): + assert ((1 + x)**2).nseries(x, n=5) == x**2 + 2*x + 1 + + # https://github.com/sympy/sympy/issues/21075 + assert ((sqrt(x) + 1)**2).nseries(x) == 2*sqrt(x) + x + 1 + assert ((sqrt(x) + cbrt(x))**2).nseries(x) == 2*x**Rational(5, 6)\ + + x**Rational(2, 3) + x + + +def test_geometric_1(): + assert (1/(1 - x)).nseries(x, n=5) == 1 + x + x**2 + x**3 + x**4 + O(x**5) + assert (x/(1 - x)).nseries(x, n=6) == x + x**2 + x**3 + x**4 + x**5 + O(x**6) + assert (x**3/(1 - x)).nseries(x, n=8) == x**3 + x**4 + x**5 + x**6 + \ + x**7 + O(x**8) + + +def test_sqrt_1(): + assert sqrt(1 + x).nseries(x, n=5) == 1 + x/2 - x**2/8 + x**3/16 - 5*x**4/128 + O(x**5) + + +def test_exp_1(): + assert exp(x).nseries(x, n=5) == 1 + x + x**2/2 + x**3/6 + x**4/24 + O(x**5) + assert exp(x).nseries(x, n=12) == 1 + x + x**2/2 + x**3/6 + x**4/24 + x**5/120 + \ + x**6/720 + x**7/5040 + x**8/40320 + x**9/362880 + x**10/3628800 + \ + x**11/39916800 + O(x**12) + assert exp(1/x).nseries(x, n=5) == exp(1/x) + assert exp(1/(1 + x)).nseries(x, n=4) == \ + (E*(1 - x - 13*x**3/6 + 3*x**2/2)).expand() + O(x**4) + assert exp(2 + x).nseries(x, n=5) == \ + (exp(2)*(1 + x + x**2/2 + x**3/6 + x**4/24)).expand() + O(x**5) + + +def test_exp_sqrt_1(): + assert exp(1 + sqrt(x)).nseries(x, n=3) == \ + (exp(1)*(1 + sqrt(x) + x/2 + sqrt(x)*x/6)).expand() + O(sqrt(x)**3) + + +def test_power_x_x1(): + assert (exp(x*log(x))).nseries(x, n=4) == \ + 1 + x*log(x) + x**2*log(x)**2/2 + x**3*log(x)**3/6 + O(x**4*log(x)**4) + + +def test_power_x_x2(): + assert (x**x).nseries(x, n=4) == \ + 1 + x*log(x) + x**2*log(x)**2/2 + x**3*log(x)**3/6 + O(x**4*log(x)**4) + + +def test_log_singular1(): + assert log(1 + 1/x).nseries(x, n=5) == x - log(x) - x**2/2 + x**3/3 - \ + x**4/4 + O(x**5) + + +def test_log_power1(): + e = 1 / (1/x + x ** (log(3)/log(2))) + assert e.nseries(x, n=5) == -x**(log(3)/log(2) + 2) + x + O(x**5) + + +def test_log_series(): + l = Symbol('l') + e = 1/(1 - log(x)) + assert e.nseries(x, n=5, logx=l) == 1/(1 - l) + + +def test_log2(): + e = log(-1/x) + assert e.nseries(x, n=5) == -log(x) + log(-1) + + +def test_log3(): + l = Symbol('l') + e = 1/log(-1/x) + assert e.nseries(x, n=4, logx=l) == 1/(-l + log(-1)) + + +def test_series1(): + e = sin(x) + assert e.nseries(x, 0, 0) != 0 + assert e.nseries(x, 0, 0) == O(1, x) + assert e.nseries(x, 0, 1) == O(x, x) + assert e.nseries(x, 0, 2) == x + O(x**2, x) + assert e.nseries(x, 0, 3) == x + O(x**3, x) + assert e.nseries(x, 0, 4) == x - x**3/6 + O(x**4, x) + + e = (exp(x) - 1)/x + assert e.nseries(x, 0, 3) == 1 + x/2 + x**2/6 + O(x**3) + + assert x.nseries(x, 0, 2) == x + + +@XFAIL +def test_series1_failing(): + assert x.nseries(x, 0, 0) == O(1, x) + assert x.nseries(x, 0, 1) == O(x, x) + + +def test_seriesbug1(): + assert (1/x).nseries(x, 0, 3) == 1/x + assert (x + 1/x).nseries(x, 0, 3) == x + 1/x + + +def test_series2x(): + assert ((x + 1)**(-2)).nseries(x, 0, 4) == 1 - 2*x + 3*x**2 - 4*x**3 + O(x**4, x) + assert ((x + 1)**(-1)).nseries(x, 0, 4) == 1 - x + x**2 - x**3 + O(x**4, x) + assert ((x + 1)**0).nseries(x, 0, 3) == 1 + assert ((x + 1)**1).nseries(x, 0, 3) == 1 + x + assert ((x + 1)**2).nseries(x, 0, 3) == x**2 + 2*x + 1 + assert ((x + 1)**3).nseries(x, 0, 3) == 1 + 3*x + 3*x**2 + O(x**3) + + assert (1/(1 + x)).nseries(x, 0, 4) == 1 - x + x**2 - x**3 + O(x**4, x) + assert (x + 3/(1 + 2*x)).nseries(x, 0, 4) == 3 - 5*x + 12*x**2 - 24*x**3 + O(x**4, x) + + assert ((1/x + 1)**3).nseries(x, 0, 3) == 1 + 3/x + 3/x**2 + x**(-3) + assert (1/(1 + 1/x)).nseries(x, 0, 4) == x - x**2 + x**3 - O(x**4, x) + assert (1/(1 + 1/x**2)).nseries(x, 0, 6) == x**2 - x**4 + O(x**6, x) + + +def test_bug2(): # 1/log(0)*log(0) problem + w = Symbol("w") + e = (w**(-1) + w**( + -log(3)*log(2)**(-1)))**(-1)*(3*w**(-log(3)*log(2)**(-1)) + 2*w**(-1)) + e = e.expand() + assert e.nseries(w, 0, 4).subs(w, 0) == 3 + + +def test_exp(): + e = (1 + x)**(1/x) + assert e.nseries(x, n=3) == exp(1) - x*exp(1)/2 + 11*exp(1)*x**2/24 + O(x**3) + + +def test_exp2(): + w = Symbol("w") + e = w**(1 - log(x)/(log(2) + log(x))) + logw = Symbol("logw") + assert e.nseries( + w, 0, 1, logx=logw) == exp(logw*log(2)/(log(x) + log(2))) + + +def test_bug3(): + e = (2/x + 3/x**2)/(1/x + 1/x**2) + assert e.nseries(x, n=3) == 3 - x + x**2 + O(x**3) + + +def test_generalexponent(): + p = 2 + e = (2/x + 3/x**p)/(1/x + 1/x**p) + assert e.nseries(x, 0, 3) == 3 - x + x**2 + O(x**3) + p = S.Half + e = (2/x + 3/x**p)/(1/x + 1/x**p) + assert e.nseries(x, 0, 2) == 2 - x + sqrt(x) + x**(S(3)/2) + O(x**2) + + e = 1 + sqrt(x) + assert e.nseries(x, 0, 4) == 1 + sqrt(x) + +# more complicated example + + +def test_genexp_x(): + e = 1/(1 + sqrt(x)) + assert e.nseries(x, 0, 2) == \ + 1 + x - sqrt(x) - sqrt(x)**3 + O(x**2, x) + +# more complicated example + + +def test_genexp_x2(): + p = Rational(3, 2) + e = (2/x + 3/x**p)/(1/x + 1/x**p) + assert e.nseries(x, 0, 3) == 3 + x + x**2 - sqrt(x) - x**(S(3)/2) - x**(S(5)/2) + O(x**3) + + +def test_seriesbug2(): + w = Symbol("w") + #simple case (1): + e = ((2*w)/w)**(1 + w) + assert e.nseries(w, 0, 1) == 2 + O(w, w) + assert e.nseries(w, 0, 1).subs(w, 0) == 2 + + +def test_seriesbug2b(): + w = Symbol("w") + #test sin + e = sin(2*w)/w + assert e.nseries(w, 0, 3) == 2 - 4*w**2/3 + O(w**3) + + +def test_seriesbug2d(): + w = Symbol("w", real=True) + e = log(sin(2*w)/w) + assert e.series(w, n=5) == log(2) - 2*w**2/3 - 4*w**4/45 + O(w**5) + + +def test_seriesbug2c(): + w = Symbol("w", real=True) + #more complicated case, but sin(x)~x, so the result is the same as in (1) + e = (sin(2*w)/w)**(1 + w) + assert e.series(w, 0, 1) == 2 + O(w) + assert e.series(w, 0, 3) == 2 + 2*w*log(2) + \ + w**2*(Rational(-4, 3) + log(2)**2) + O(w**3) + assert e.series(w, 0, 2).subs(w, 0) == 2 + + +def test_expbug4(): + x = Symbol("x", real=True) + assert (log( + sin(2*x)/x)*(1 + x)).series(x, 0, 2) == log(2) + x*log(2) + O(x**2, x) + assert exp( + log(sin(2*x)/x)*(1 + x)).series(x, 0, 2) == 2 + 2*x*log(2) + O(x**2) + + assert exp(log(2) + O(x)).nseries(x, 0, 2) == 2 + O(x) + assert ((2 + O(x))**(1 + x)).nseries(x, 0, 2) == 2 + O(x) + + +def test_logbug4(): + assert log(2 + O(x)).nseries(x, 0, 2) == log(2) + O(x, x) + + +def test_expbug5(): + assert exp(log(1 + x)/x).nseries(x, n=3) == exp(1) + -exp(1)*x/2 + 11*exp(1)*x**2/24 + O(x**3) + + assert exp(O(x)).nseries(x, 0, 2) == 1 + O(x) + + +def test_sinsinbug(): + assert sin(sin(x)).nseries(x, 0, 8) == x - x**3/3 + x**5/10 - 8*x**7/315 + O(x**8) + + +def test_issue_3258(): + a = x/(exp(x) - 1) + assert a.nseries(x, 0, 5) == 1 - x/2 - x**4/720 + x**2/12 + O(x**5) + + +def test_issue_3204(): + x = Symbol("x", nonnegative=True) + f = sin(x**3)**Rational(1, 3) + assert f.nseries(x, 0, 17) == x - x**7/18 - x**13/3240 + O(x**17) + + +def test_issue_3224(): + f = sqrt(1 - sqrt(y)) + assert f.nseries(y, 0, 2) == 1 - sqrt(y)/2 - y/8 - sqrt(y)**3/16 + O(y**2) + + +def test_issue_3463(): + w, i = symbols('w,i') + r = log(5)/log(3) + p = w**(-1 + r) + e = 1/x*(-log(w**(1 + r)) + log(w + w**r)) + e_ser = -r*log(w)/x + p/x - p**2/(2*x) + O(w) + assert e.nseries(w, n=1) == e_ser + + +def test_sin(): + assert sin(8*x).nseries(x, n=4) == 8*x - 256*x**3/3 + O(x**4) + assert sin(x + y).nseries(x, n=1) == sin(y) + O(x) + assert sin(x + y).nseries(x, n=2) == sin(y) + cos(y)*x + O(x**2) + assert sin(x + y).nseries(x, n=5) == sin(y) + cos(y)*x - sin(y)*x**2/2 - \ + cos(y)*x**3/6 + sin(y)*x**4/24 + O(x**5) + + +def test_issue_3515(): + e = sin(8*x)/x + assert e.nseries(x, n=6) == 8 - 256*x**2/3 + 4096*x**4/15 + O(x**6) + + +def test_issue_3505(): + e = sin(x)**(-4)*(sqrt(cos(x))*sin(x)**2 - + cos(x)**Rational(1, 3)*sin(x)**2) + assert e.nseries(x, n=9) == Rational(-1, 12) - 7*x**2/288 - \ + 43*x**4/10368 - 1123*x**6/2488320 + 377*x**8/29859840 + O(x**9) + + +def test_issue_3501(): + a = Symbol("a") + e = x**(-2)*(x*sin(a + x) - x*sin(a)) + assert e.nseries(x, n=6) == cos(a) - sin(a)*x/2 - cos(a)*x**2/6 + \ + x**3*sin(a)/24 + x**4*cos(a)/120 - x**5*sin(a)/720 + O(x**6) + e = x**(-2)*(x*cos(a + x) - x*cos(a)) + assert e.nseries(x, n=6) == -sin(a) - cos(a)*x/2 + sin(a)*x**2/6 + \ + cos(a)*x**3/24 - x**4*sin(a)/120 - x**5*cos(a)/720 + O(x**6) + + +def test_issue_3502(): + e = sin(5*x)/sin(2*x) + assert e.nseries(x, n=2) == Rational(5, 2) + O(x**2) + assert e.nseries(x, n=6) == \ + Rational(5, 2) - 35*x**2/4 + 329*x**4/48 + O(x**6) + + +def test_issue_3503(): + e = sin(2 + x)/(2 + x) + assert e.nseries(x, n=2) == sin(2)/2 + x*cos(2)/2 - x*sin(2)/4 + O(x**2) + + +def test_issue_3506(): + e = (x + sin(3*x))**(-2)*(x*(x + sin(3*x)) - (x + sin(3*x))*sin(2*x)) + assert e.nseries(x, n=7) == \ + Rational(-1, 4) + 5*x**2/96 + 91*x**4/768 + 11117*x**6/129024 + O(x**7) + + +def test_issue_3508(): + x = Symbol("x", real=True) + assert log(sin(x)).series(x, n=5) == log(x) - x**2/6 - x**4/180 + O(x**5) + e = -log(x) + x*(-log(x) + log(sin(2*x))) + log(sin(2*x)) + assert e.series(x, n=5) == \ + log(2) + log(2)*x - 2*x**2/3 - 2*x**3/3 - 4*x**4/45 + O(x**5) + + +def test_issue_3507(): + e = x**(-4)*(x**2 - x**2*sqrt(cos(x))) + assert e.nseries(x, n=9) == \ + Rational(1, 4) + x**2/96 + 19*x**4/5760 + 559*x**6/645120 + 29161*x**8/116121600 + O(x**9) + + +def test_issue_3639(): + assert sin(cos(x)).nseries(x, n=5) == \ + sin(1) - x**2*cos(1)/2 - x**4*sin(1)/8 + x**4*cos(1)/24 + O(x**5) + + +def test_hyperbolic(): + assert sinh(x).nseries(x, n=6) == x + x**3/6 + x**5/120 + O(x**6) + assert cosh(x).nseries(x, n=5) == 1 + x**2/2 + x**4/24 + O(x**5) + assert tanh(x).nseries(x, n=6) == x - x**3/3 + 2*x**5/15 + O(x**6) + assert coth(x).nseries(x, n=6) == \ + 1/x - x**3/45 + x/3 + 2*x**5/945 + O(x**6) + assert asinh(x).nseries(x, n=6) == x - x**3/6 + 3*x**5/40 + O(x**6) + assert acosh(x).nseries(x, n=6) == \ + pi*I/2 - I*x - 3*I*x**5/40 - I*x**3/6 + O(x**6) + assert atanh(x).nseries(x, n=6) == x + x**3/3 + x**5/5 + O(x**6) + assert acoth(x).nseries(x, n=6) == -I*pi/2 + x + x**3/3 + x**5/5 + O(x**6) + + +def test_series2(): + w = Symbol("w", real=True) + x = Symbol("x", real=True) + e = w**(-2)*(w*exp(1/x - w) - w*exp(1/x)) + assert e.nseries(w, n=4) == -exp(1/x) + w*exp(1/x)/2 - w**2*exp(1/x)/6 + w**3*exp(1/x)/24 + O(w**4) + + +def test_series3(): + w = Symbol("w", real=True) + e = w**(-6)*(w**3*tan(w) - w**3*sin(w)) + assert e.nseries(w, n=8) == Integer(1)/2 + w**2/8 + 13*w**4/240 + 529*w**6/24192 + O(w**8) + + +def test_bug4(): + w = Symbol("w") + e = x/(w**4 + x**2*w**4 + 2*x*w**4)*w**4 + assert e.nseries(w, n=2).removeO().expand() in [x/(1 + 2*x + x**2), + 1/(1 + x/2 + 1/x/2)/2, 1/x/(1 + 2/x + x**(-2))] + + +def test_bug5(): + w = Symbol("w") + l = Symbol('l') + e = (-log(w) + log(1 + w*log(x)))**(-2)*w**(-2)*((-log(w) + + log(1 + x*w))*(-log(w) + log(1 + w*log(x)))*w - x*(-log(w) + + log(1 + w*log(x)))*w) + assert e.nseries(w, n=0, logx=l) == x/w/l + 1/w + O(1, w) + assert e.nseries(w, n=1, logx=l) == x/w/l + 1/w - x/l + 1/l*log(x) \ + + x*log(x)/l**2 + O(w) + + +def test_issue_4115(): + assert (sin(x)/(1 - cos(x))).nseries(x, n=1) == 2/x + O(x) + assert (sin(x)**2/(1 - cos(x))).nseries(x, n=1) == 2 + O(x) + + +def test_pole(): + raises(PoleError, lambda: sin(1/x).series(x, 0, 5)) + raises(PoleError, lambda: sin(1 + 1/x).series(x, 0, 5)) + raises(PoleError, lambda: (x*sin(1/x)).series(x, 0, 5)) + + +def test_expsinbug(): + assert exp(sin(x)).series(x, 0, 0) == O(1, x) + assert exp(sin(x)).series(x, 0, 1) == 1 + O(x) + assert exp(sin(x)).series(x, 0, 2) == 1 + x + O(x**2) + assert exp(sin(x)).series(x, 0, 3) == 1 + x + x**2/2 + O(x**3) + assert exp(sin(x)).series(x, 0, 4) == 1 + x + x**2/2 + O(x**4) + assert exp(sin(x)).series(x, 0, 5) == 1 + x + x**2/2 - x**4/8 + O(x**5) + + +def test_floor(): + x = Symbol('x') + assert floor(x).series(x) == 0 + assert floor(-x).series(x) == -1 + assert floor(sin(x)).series(x) == 0 + assert floor(sin(-x)).series(x) == -1 + assert floor(x**3).series(x) == 0 + assert floor(-x**3).series(x) == -1 + assert floor(cos(x)).series(x) == 0 + assert floor(cos(-x)).series(x) == 0 + assert floor(5 + sin(x)).series(x) == 5 + assert floor(5 + sin(-x)).series(x) == 4 + + assert floor(x).series(x, 2) == 2 + assert floor(-x).series(x, 2) == -3 + + x = Symbol('x', negative=True) + assert floor(x + 1.5).series(x) == 1 + + +def test_frac(): + assert frac(x).series(x, cdir=1) == x + assert frac(x).series(x, cdir=-1) == 1 + x + assert frac(2*x + 1).series(x, cdir=1) == 2*x + assert frac(2*x + 1).series(x, cdir=-1) == 1 + 2*x + assert frac(x**2).series(x, cdir=1) == x**2 + assert frac(x**2).series(x, cdir=-1) == x**2 + assert frac(sin(x) + 5).series(x, cdir=1) == x - x**3/6 + x**5/120 + O(x**6) + assert frac(sin(x) + 5).series(x, cdir=-1) == 1 + x - x**3/6 + x**5/120 + O(x**6) + assert frac(sin(x) + S.Half).series(x) == S.Half + x - x**3/6 + x**5/120 + O(x**6) + assert frac(x**8).series(x, cdir=1) == O(x**6) + assert frac(1/x).series(x) == AccumBounds(0, 1) + O(x**6) + + +def test_ceiling(): + assert ceiling(x).series(x) == 1 + assert ceiling(-x).series(x) == 0 + assert ceiling(sin(x)).series(x) == 1 + assert ceiling(sin(-x)).series(x) == 0 + assert ceiling(1 - cos(x)).series(x) == 1 + assert ceiling(1 - cos(-x)).series(x) == 1 + assert ceiling(x).series(x, 2) == 3 + assert ceiling(-x).series(x, 2) == -2 + + +def test_abs(): + a = Symbol('a') + assert abs(x).nseries(x, n=4) == x + assert abs(-x).nseries(x, n=4) == x + assert abs(x + 1).nseries(x, n=4) == x + 1 + assert abs(sin(x)).nseries(x, n=4) == x - Rational(1, 6)*x**3 + O(x**4) + assert abs(sin(-x)).nseries(x, n=4) == x - Rational(1, 6)*x**3 + O(x**4) + assert abs(x - a).nseries(x, 1) == -a*sign(1 - a) + (x - 1)*sign(1 - a) + sign(1 - a) + + +def test_dir(): + assert abs(x).series(x, 0, dir="+") == x + assert abs(x).series(x, 0, dir="-") == -x + assert floor(x + 2).series(x, 0, dir='+') == 2 + assert floor(x + 2).series(x, 0, dir='-') == 1 + assert floor(x + 2.2).series(x, 0, dir='-') == 2 + assert ceiling(x + 2.2).series(x, 0, dir='-') == 3 + assert sin(x + y).series(x, 0, dir='-') == sin(x + y).series(x, 0, dir='+') + + +def test_cdir(): + assert abs(x).series(x, 0, cdir=1) == x + assert abs(x).series(x, 0, cdir=-1) == -x + assert floor(x + 2).series(x, 0, cdir=1) == 2 + assert floor(x + 2).series(x, 0, cdir=-1) == 1 + assert floor(x + 2.2).series(x, 0, cdir=1) == 2 + assert ceiling(x + 2.2).series(x, 0, cdir=-1) == 3 + assert sin(x + y).series(x, 0, cdir=-1) == sin(x + y).series(x, 0, cdir=1) + + +def test_issue_3504(): + a = Symbol("a") + e = asin(a*x)/x + assert e.series(x, 4, n=2).removeO() == \ + (x - 4)*(a/(4*sqrt(-16*a**2 + 1)) - asin(4*a)/16) + asin(4*a)/4 + + +def test_issue_4441(): + a, b = symbols('a,b') + f = 1/(1 + a*x) + assert f.series(x, 0, 5) == 1 - a*x + a**2*x**2 - a**3*x**3 + \ + a**4*x**4 + O(x**5) + f = 1/(1 + (a + b)*x) + assert f.series(x, 0, 3) == 1 + x*(-a - b)\ + + x**2*(a + b)**2 + O(x**3) + + +def test_issue_4329(): + assert tan(x).series(x, pi/2, n=3).removeO() == \ + -pi/6 + x/3 - 1/(x - pi/2) + assert cot(x).series(x, pi, n=3).removeO() == \ + -x/3 + pi/3 + 1/(x - pi) + assert limit(tan(x)**tan(2*x), x, pi/4) == exp(-1) + + +def test_issue_5183(): + assert abs(x + x**2).series(n=1) == O(x) + assert abs(x + x**2).series(n=2) == x + O(x**2) + assert ((1 + x)**2).series(x, n=6) == x**2 + 2*x + 1 + assert (1 + 1/x).series() == 1 + 1/x + assert Derivative(exp(x).series(), x).doit() == \ + 1 + x + x**2/2 + x**3/6 + x**4/24 + O(x**5) + + +def test_issue_5654(): + a = Symbol('a') + assert (1/(x**2+a**2)**2).nseries(x, x0=I*a, n=0) == \ + -I/(4*a**3*(-I*a + x)) - 1/(4*a**2*(-I*a + x)**2) + O(1, (x, I*a)) + assert (1/(x**2+a**2)**2).nseries(x, x0=I*a, n=1) == 3/(16*a**4) \ + -I/(4*a**3*(-I*a + x)) - 1/(4*a**2*(-I*a + x)**2) + O(-I*a + x, (x, I*a)) + + +def test_issue_5925(): + sx = sqrt(x + z).series(z, 0, 1) + sxy = sqrt(x + y + z).series(z, 0, 1) + s1, s2 = sx.subs(x, x + y), sxy + assert (s1 - s2).expand().removeO().simplify() == 0 + + sx = sqrt(x + z).series(z, 0, 1) + sxy = sqrt(x + y + z).series(z, 0, 1) + assert sxy.subs({x:1, y:2}) == sx.subs(x, 3) + + +def test_exp_2(): + assert exp(x**3).nseries(x, 0, 14) == 1 + x**3 + x**6/2 + x**9/6 + x**12/24 + O(x**14) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_order.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_order.py new file mode 100644 index 0000000000000000000000000000000000000000..50fcb861ee2a76c730baae6d26cc1e7a00347176 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_order.py @@ -0,0 +1,503 @@ +from sympy.core.add import Add +from sympy.core.function import (Function, expand) +from sympy.core.numbers import (I, Rational, nan, oo, pi) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.complexes import (conjugate, transpose) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.integrals.integrals import Integral +from sympy.series.order import O, Order +from sympy.core.expr import unchanged +from sympy.testing.pytest import raises +from sympy.abc import w, x, y, z +from sympy.testing.pytest import XFAIL + + +def test_caching_bug(): + #needs to be a first test, so that all caches are clean + #cache it + O(w) + #and test that this won't raise an exception + O(w**(-1/x/log(3)*log(5)), w) + + +def test_free_symbols(): + assert Order(1).free_symbols == set() + assert Order(x).free_symbols == {x} + assert Order(1, x).free_symbols == {x} + assert Order(x*y).free_symbols == {x, y} + assert Order(x, x, y).free_symbols == {x, y} + + +def test_simple_1(): + o = Rational(0) + assert Order(2*x) == Order(x) + assert Order(x)*3 == Order(x) + assert -28*Order(x) == Order(x) + assert Order(Order(x)) == Order(x) + assert Order(Order(x), y) == Order(Order(x), x, y) + assert Order(-23) == Order(1) + assert Order(exp(x)) == Order(1, x) + assert Order(exp(1/x)).expr == exp(1/x) + assert Order(x*exp(1/x)).expr == x*exp(1/x) + assert Order(x**(o/3)).expr == x**(o/3) + assert Order(x**(o*Rational(5, 3))).expr == x**(o*Rational(5, 3)) + assert Order(x**2 + x + y, x) == O(1, x) + assert Order(x**2 + x + y, y) == O(1, y) + raises(ValueError, lambda: Order(exp(x), x, x)) + raises(TypeError, lambda: Order(x, 2 - x)) + + +def test_simple_2(): + assert Order(2*x)*x == Order(x**2) + assert Order(2*x)/x == Order(1, x) + assert Order(2*x)*x*exp(1/x) == Order(x**2*exp(1/x)) + assert (Order(2*x)*x*exp(1/x)/log(x)**3).expr == x**2*exp(1/x)*log(x)**-3 + + +def test_simple_3(): + assert Order(x) + x == Order(x) + assert Order(x) + 2 == 2 + Order(x) + assert Order(x) + x**2 == Order(x) + assert Order(x) + 1/x == 1/x + Order(x) + assert Order(1/x) + 1/x**2 == 1/x**2 + Order(1/x) + assert Order(x) + exp(1/x) == Order(x) + exp(1/x) + + +def test_simple_4(): + assert Order(x)**2 == Order(x**2) + + +def test_simple_5(): + assert Order(x) + Order(x**2) == Order(x) + assert Order(x) + Order(x**-2) == Order(x**-2) + assert Order(x) + Order(1/x) == Order(1/x) + + +def test_simple_6(): + assert Order(x) - Order(x) == Order(x) + assert Order(x) + Order(1) == Order(1) + assert Order(x) + Order(x**2) == Order(x) + assert Order(1/x) + Order(1) == Order(1/x) + assert Order(x) + Order(exp(1/x)) == Order(exp(1/x)) + assert Order(x**3) + Order(exp(2/x)) == Order(exp(2/x)) + assert Order(x**-3) + Order(exp(2/x)) == Order(exp(2/x)) + + +def test_simple_7(): + assert 1 + O(1) == O(1) + assert 2 + O(1) == O(1) + assert x + O(1) == O(1) + assert 1/x + O(1) == 1/x + O(1) + + +def test_simple_8(): + assert O(sqrt(-x)) == O(sqrt(x)) + assert O(x**2*sqrt(x)) == O(x**Rational(5, 2)) + assert O(x**3*sqrt(-(-x)**3)) == O(x**Rational(9, 2)) + assert O(x**Rational(3, 2)*sqrt((-x)**3)) == O(x**3) + assert O(x*(-2*x)**(I/2)) == O(x*(-x)**(I/2)) + + +def test_as_expr_variables(): + assert Order(x).as_expr_variables(None) == (x, ((x, 0),)) + assert Order(x).as_expr_variables(((x, 0),)) == (x, ((x, 0),)) + assert Order(y).as_expr_variables(((x, 0),)) == (y, ((x, 0), (y, 0))) + assert Order(y).as_expr_variables(((x, 0), (y, 0))) == (y, ((x, 0), (y, 0))) + + +def test_contains_0(): + assert Order(1, x).contains(Order(1, x)) + assert Order(1, x).contains(Order(1)) + assert Order(1).contains(Order(1, x)) is False + + +def test_contains_1(): + assert Order(x).contains(Order(x)) + assert Order(x).contains(Order(x**2)) + assert not Order(x**2).contains(Order(x)) + assert not Order(x).contains(Order(1/x)) + assert not Order(1/x).contains(Order(exp(1/x))) + assert not Order(x).contains(Order(exp(1/x))) + assert Order(1/x).contains(Order(x)) + assert Order(exp(1/x)).contains(Order(x)) + assert Order(exp(1/x)).contains(Order(1/x)) + assert Order(exp(1/x)).contains(Order(exp(1/x))) + assert Order(exp(2/x)).contains(Order(exp(1/x))) + assert not Order(exp(1/x)).contains(Order(exp(2/x))) + + +def test_contains_2(): + assert Order(x).contains(Order(y)) is None + assert Order(x).contains(Order(y*x)) + assert Order(y*x).contains(Order(x)) + assert Order(y).contains(Order(x*y)) + assert Order(x).contains(Order(y**2*x)) + + +def test_contains_3(): + assert Order(x*y**2).contains(Order(x**2*y)) is None + assert Order(x**2*y).contains(Order(x*y**2)) is None + + +def test_contains_4(): + assert Order(sin(1/x**2)).contains(Order(cos(1/x**2))) is True + assert Order(cos(1/x**2)).contains(Order(sin(1/x**2))) is True + + +def test_contains(): + assert Order(1, x) not in Order(1) + assert Order(1) in Order(1, x) + raises(TypeError, lambda: Order(x*y**2) in Order(x**2*y)) + + +def test_add_1(): + assert Order(x + x) == Order(x) + assert Order(3*x - 2*x**2) == Order(x) + assert Order(1 + x) == Order(1, x) + assert Order(1 + 1/x) == Order(1/x) + # TODO : A better output for Order(log(x) + 1/log(x)) + # could be Order(log(x)). Currently Order for expressions + # where all arguments would involve a log term would fall + # in this category and outputs for these should be improved. + assert Order(log(x) + 1/log(x)) == Order((log(x)**2 + 1)/log(x)) + assert Order(exp(1/x) + x) == Order(exp(1/x)) + assert Order(exp(1/x) + 1/x**20) == Order(exp(1/x)) + + +def test_ln_args(): + assert O(log(x)) + O(log(2*x)) == O(log(x)) + assert O(log(x)) + O(log(x**3)) == O(log(x)) + assert O(log(x*y)) + O(log(x) + log(y)) == O(log(x) + log(y), x, y) + + +def test_multivar_0(): + assert Order(x*y).expr == x*y + assert Order(x*y**2).expr == x*y**2 + assert Order(x*y, x).expr == x + assert Order(x*y**2, y).expr == y**2 + assert Order(x*y*z).expr == x*y*z + assert Order(x/y).expr == x/y + assert Order(x*exp(1/y)).expr == x*exp(1/y) + assert Order(exp(x)*exp(1/y)).expr == exp(x)*exp(1/y) + + +def test_multivar_0a(): + assert Order(exp(1/x)*exp(1/y)).expr == exp(1/x)*exp(1/y) + + +def test_multivar_1(): + assert Order(x + y).expr == x + y + assert Order(x + 2*y).expr == x + y + assert (Order(x + y) + x).expr == (x + y) + assert (Order(x + y) + x**2) == Order(x + y) + assert (Order(x + y) + 1/x) == 1/x + Order(x + y) + assert Order(x**2 + y*x).expr == x**2 + y*x + + +def test_multivar_2(): + assert Order(x**2*y + y**2*x, x, y).expr == x**2*y + y**2*x + + +def test_multivar_mul_1(): + assert Order(x + y)*x == Order(x**2 + y*x, x, y) + + +def test_multivar_3(): + assert (Order(x) + Order(y)).args in [ + (Order(x), Order(y)), + (Order(y), Order(x))] + assert Order(x) + Order(y) + Order(x + y) == Order(x + y) + assert (Order(x**2*y) + Order(y**2*x)).args in [ + (Order(x*y**2), Order(y*x**2)), + (Order(y*x**2), Order(x*y**2))] + assert (Order(x**2*y) + Order(y*x)) == Order(x*y) + + +def test_issue_3468(): + y = Symbol('y', negative=True) + z = Symbol('z', complex=True) + + # check that Order does not modify assumptions about symbols + Order(x) + Order(y) + Order(z) + + assert x.is_positive is None + assert y.is_positive is False + assert z.is_positive is None + + +def test_leading_order(): + assert (x + 1 + 1/x**5).extract_leading_order(x) == ((1/x**5, O(1/x**5)),) + assert (1 + 1/x).extract_leading_order(x) == ((1/x, O(1/x)),) + assert (1 + x).extract_leading_order(x) == ((1, O(1, x)),) + assert (1 + x**2).extract_leading_order(x) == ((1, O(1, x)),) + assert (2 + x**2).extract_leading_order(x) == ((2, O(1, x)),) + assert (x + x**2).extract_leading_order(x) == ((x, O(x)),) + + +def test_leading_order2(): + assert set((2 + pi + x**2).extract_leading_order(x)) == {(pi, O(1, x)), + (S(2), O(1, x))} + assert set((2*x + pi*x + x**2).extract_leading_order(x)) == {(2*x, O(x)), + (x*pi, O(x))} + + +def test_order_leadterm(): + assert O(x**2)._eval_as_leading_term(x, None, 1) == O(x**2) + + +def test_order_symbols(): + e = x*y*sin(x)*Integral(x, (x, 1, 2)) + assert O(e) == O(x**2*y, x, y) + assert O(e, x) == O(x**2) + + +def test_nan(): + assert O(nan) is nan + assert not O(x).contains(nan) + + +def test_O1(): + assert O(1, x) * x == O(x) + assert O(1, y) * x == O(1, y) + + +def test_getn(): + # other lines are tested incidentally by the suite + assert O(x).getn() == 1 + assert O(x/log(x)).getn() == 1 + assert O(x**2/log(x)**2).getn() == 2 + assert O(x*log(x)).getn() == 1 + raises(NotImplementedError, lambda: (O(x) + O(y)).getn()) + + +def test_diff(): + assert O(x**2).diff(x) == O(x) + + +def test_getO(): + assert (x).getO() is None + assert (x).removeO() == x + assert (O(x)).getO() == O(x) + assert (O(x)).removeO() == 0 + assert (z + O(x) + O(y)).getO() == O(x) + O(y) + assert (z + O(x) + O(y)).removeO() == z + raises(NotImplementedError, lambda: (O(x) + O(y)).getn()) + + +def test_leading_term(): + from sympy.functions.special.gamma_functions import digamma + assert O(1/digamma(1/x)) == O(1/log(x)) + + +def test_eval(): + assert Order(x).subs(Order(x), 1) == 1 + assert Order(x).subs(x, y) == Order(y) + assert Order(x).subs(y, x) == Order(x) + assert Order(x).subs(x, x + y) == Order(x + y, (x, -y)) + assert (O(1)**x).is_Pow + + +def test_issue_4279(): + a, b = symbols('a b') + assert O(a, a, b) + O(1, a, b) == O(1, a, b) + assert O(b, a, b) + O(1, a, b) == O(1, a, b) + assert O(a + b, a, b) + O(1, a, b) == O(1, a, b) + assert O(1, a, b) + O(a, a, b) == O(1, a, b) + assert O(1, a, b) + O(b, a, b) == O(1, a, b) + assert O(1, a, b) + O(a + b, a, b) == O(1, a, b) + + +def test_issue_4855(): + assert 1/O(1) != O(1) + assert 1/O(x) != O(1/x) + assert 1/O(x, (x, oo)) != O(1/x, (x, oo)) + + f = Function('f') + assert 1/O(f(x)) != O(1/x) + + +def test_order_conjugate_transpose(): + x = Symbol('x', real=True) + y = Symbol('y', imaginary=True) + assert conjugate(Order(x)) == Order(conjugate(x)) + assert conjugate(Order(y)) == Order(conjugate(y)) + assert conjugate(Order(x**2)) == Order(conjugate(x)**2) + assert conjugate(Order(y**2)) == Order(conjugate(y)**2) + assert transpose(Order(x)) == Order(transpose(x)) + assert transpose(Order(y)) == Order(transpose(y)) + assert transpose(Order(x**2)) == Order(transpose(x)**2) + assert transpose(Order(y**2)) == Order(transpose(y)**2) + + +def test_order_noncommutative(): + A = Symbol('A', commutative=False) + assert Order(A + A*x, x) == Order(1, x) + assert (A + A*x)*Order(x) == Order(x) + assert (A*x)*Order(x) == Order(x**2, x) + assert expand((1 + Order(x))*A*A*x) == A*A*x + Order(x**2, x) + assert expand((A*A + Order(x))*x) == A*A*x + Order(x**2, x) + assert expand((A + Order(x))*A*x) == A*A*x + Order(x**2, x) + + +def test_issue_6753(): + assert (1 + x**2)**10000*O(x) == O(x) + + +def test_order_at_infinity(): + assert Order(1 + x, (x, oo)) == Order(x, (x, oo)) + assert Order(3*x, (x, oo)) == Order(x, (x, oo)) + assert Order(x, (x, oo))*3 == Order(x, (x, oo)) + assert -28*Order(x, (x, oo)) == Order(x, (x, oo)) + assert Order(Order(x, (x, oo)), (x, oo)) == Order(x, (x, oo)) + assert Order(Order(x, (x, oo)), (y, oo)) == Order(x, (x, oo), (y, oo)) + assert Order(3, (x, oo)) == Order(1, (x, oo)) + assert Order(x**2 + x + y, (x, oo)) == O(x**2, (x, oo)) + assert Order(x**2 + x + y, (y, oo)) == O(y, (y, oo)) + + assert Order(2*x, (x, oo))*x == Order(x**2, (x, oo)) + assert Order(2*x, (x, oo))/x == Order(1, (x, oo)) + assert Order(2*x, (x, oo))*x*exp(1/x) == Order(x**2*exp(1/x), (x, oo)) + assert Order(2*x, (x, oo))*x*exp(1/x)/log(x)**3 == Order(x**2*exp(1/x)*log(x)**-3, (x, oo)) + + assert Order(x, (x, oo)) + 1/x == 1/x + Order(x, (x, oo)) == Order(x, (x, oo)) + assert Order(x, (x, oo)) + 1 == 1 + Order(x, (x, oo)) == Order(x, (x, oo)) + assert Order(x, (x, oo)) + x == x + Order(x, (x, oo)) == Order(x, (x, oo)) + assert Order(x, (x, oo)) + x**2 == x**2 + Order(x, (x, oo)) + assert Order(1/x, (x, oo)) + 1/x**2 == 1/x**2 + Order(1/x, (x, oo)) == Order(1/x, (x, oo)) + assert Order(x, (x, oo)) + exp(1/x) == exp(1/x) + Order(x, (x, oo)) + + assert Order(x, (x, oo))**2 == Order(x**2, (x, oo)) + + assert Order(x, (x, oo)) + Order(x**2, (x, oo)) == Order(x**2, (x, oo)) + assert Order(x, (x, oo)) + Order(x**-2, (x, oo)) == Order(x, (x, oo)) + assert Order(x, (x, oo)) + Order(1/x, (x, oo)) == Order(x, (x, oo)) + + assert Order(x, (x, oo)) - Order(x, (x, oo)) == Order(x, (x, oo)) + assert Order(x, (x, oo)) + Order(1, (x, oo)) == Order(x, (x, oo)) + assert Order(x, (x, oo)) + Order(x**2, (x, oo)) == Order(x**2, (x, oo)) + assert Order(1/x, (x, oo)) + Order(1, (x, oo)) == Order(1, (x, oo)) + assert Order(x, (x, oo)) + Order(exp(1/x), (x, oo)) == Order(x, (x, oo)) + assert Order(x**3, (x, oo)) + Order(exp(2/x), (x, oo)) == Order(x**3, (x, oo)) + assert Order(x**-3, (x, oo)) + Order(exp(2/x), (x, oo)) == Order(exp(2/x), (x, oo)) + + # issue 7207 + assert Order(exp(x), (x, oo)).expr == Order(2*exp(x), (x, oo)).expr == exp(x) + assert Order(y**x, (x, oo)).expr == Order(2*y**x, (x, oo)).expr == exp(x*log(y)) + + # issue 19545 + assert Order(1/x - 3/(3*x + 2), (x, oo)).expr == x**(-2) + +def test_mixing_order_at_zero_and_infinity(): + assert (Order(x, (x, 0)) + Order(x, (x, oo))).is_Add + assert Order(x, (x, 0)) + Order(x, (x, oo)) == Order(x, (x, oo)) + Order(x, (x, 0)) + assert Order(Order(x, (x, oo))) == Order(x, (x, oo)) + + # not supported (yet) + raises(NotImplementedError, lambda: Order(x, (x, 0))*Order(x, (x, oo))) + raises(NotImplementedError, lambda: Order(x, (x, oo))*Order(x, (x, 0))) + raises(NotImplementedError, lambda: Order(Order(x, (x, oo)), y)) + raises(NotImplementedError, lambda: Order(Order(x), (x, oo))) + + +def test_order_at_some_point(): + assert Order(x, (x, 1)) == Order(1, (x, 1)) + assert Order(2*x - 2, (x, 1)) == Order(x - 1, (x, 1)) + assert Order(-x + 1, (x, 1)) == Order(x - 1, (x, 1)) + assert Order(x - 1, (x, 1))**2 == Order((x - 1)**2, (x, 1)) + assert Order(x - 2, (x, 2)) - O(x - 2, (x, 2)) == Order(x - 2, (x, 2)) + + +def test_order_subs_limits(): + # issue 3333 + assert (1 + Order(x)).subs(x, 1/x) == 1 + Order(1/x, (x, oo)) + assert (1 + Order(x)).limit(x, 0) == 1 + # issue 5769 + assert ((x + Order(x**2))/x).limit(x, 0) == 1 + + assert Order(x**2).subs(x, y - 1) == Order((y - 1)**2, (y, 1)) + assert Order(10*x**2, (x, 2)).subs(x, y - 1) == Order(1, (y, 3)) + + #issue 19120 + assert O(x).subs(x, O(x)) == O(x) + assert O(x**2).subs(x, x + O(x)) == O(x**2) + assert O(x, (x, oo)).subs(x, O(x, (x, oo))) == O(x, (x, oo)) + assert O(x**2, (x, oo)).subs(x, x + O(x, (x, oo))) == O(x**2, (x, oo)) + assert (x + O(x**2)).subs(x, x + O(x**2)) == x + O(x**2) + assert (x**2 + O(x**2) + 1/x**2).subs(x, x + O(x**2)) == (x + O(x**2))**(-2) + O(x**2) + assert (x**2 + O(x**2) + 1).subs(x, x + O(x**2)) == 1 + O(x**2) + assert O(x, (x, oo)).subs(x, x + O(x**2, (x, oo))) == O(x**2, (x, oo)) + assert sin(x).series(n=8).subs(x,sin(x).series(n=8)).expand() == x - x**3/3 + x**5/10 - 8*x**7/315 + O(x**8) + assert cos(x).series(n=8).subs(x,sin(x).series(n=8)).expand() == 1 - x**2/2 + 5*x**4/24 - 37*x**6/720 + O(x**8) + assert O(x).subs(x, O(1/x, (x, oo))) == O(1/x, (x, oo)) + +@XFAIL +def test_order_failing_due_to_solveset(): + assert O(x**3).subs(x, exp(-x**2)) == O(exp(-3*x**2), (x, -oo)) + raises(NotImplementedError, lambda: O(x).subs(x, O(1/x))) # mixing of order at different points + + +def test_issue_9351(): + assert exp(x).series(x, 10, 1) == exp(10) + Order(x - 10, (x, 10)) + + +def test_issue_9192(): + assert O(1)*O(1) == O(1) + assert O(1)**O(1) == O(1) + + +def test_issue_9910(): + assert O(x*log(x) + sin(x), (x, oo)) == O(x*log(x), (x, oo)) + + +def test_performance_of_adding_order(): + l = [x**i for i in range(1000)] + l.append(O(x**1001)) + assert Add(*l).subs(x,1) == O(1) + +def test_issue_14622(): + assert (x**(-4) + x**(-3) + x**(-1) + O(x**(-6), (x, oo))).as_numer_denom() == ( + x**4 + x**5 + x**7 + O(x**2, (x, oo)), x**8) + assert (x**3 + O(x**2, (x, oo))).is_Add + assert O(x**2, (x, oo)).contains(x**3) is False + assert O(x, (x, oo)).contains(O(x, (x, 0))) is None + assert O(x, (x, 0)).contains(O(x, (x, oo))) is None + raises(NotImplementedError, lambda: O(x**3).contains(x**w)) + + +def test_issue_15539(): + assert O(1/x**2 + 1/x**4, (x, -oo)) == O(1/x**2, (x, -oo)) + assert O(1/x**4 + exp(x), (x, -oo)) == O(1/x**4, (x, -oo)) + assert O(1/x**4 + exp(-x), (x, -oo)) == O(exp(-x), (x, -oo)) + assert O(1/x, (x, oo)).subs(x, -x) == O(-1/x, (x, -oo)) + +def test_issue_18606(): + assert unchanged(Order, 0) + + +def test_issue_22165(): + assert O(log(x)).contains(2) + + +def test_issue_23231(): + # This test checks Order for expressions having + # arguments containing variables in exponents/powers. + assert O(x**x + 2**x, (x, oo)) == O(exp(x*log(x)), (x, oo)) + assert O(x**x + x**2, (x, oo)) == O(exp(x*log(x)), (x, oo)) + assert O(x**x + 1/x**2, (x, oo)) == O(exp(x*log(x)), (x, oo)) + assert O(2**x + 3**x , (x, oo)) == O(exp(x*log(3)), (x, oo)) + + +def test_issue_9917(): + assert O(x*sin(x) + 1, (x, oo)) == O(x, (x, oo)) + + +def test_issue_22836(): + assert O(2**x + factorial(x), (x, oo)) == O(factorial(x), (x, oo)) + assert O(2**x + factorial(x) + x**x, (x, oo)) == O(exp(x*log(x)), (x, oo)) + assert O(x + factorial(x), (x, oo)) == O(factorial(x), (x, oo)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_residues.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_residues.py new file mode 100644 index 0000000000000000000000000000000000000000..9f7d075a56500d008e3c8b46c1fda5db890fd76a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_residues.py @@ -0,0 +1,101 @@ +from sympy.core.function import Function +from sympy.core.numbers import (I, Rational, pi) +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.hyperbolic import tanh +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cot, sin, tan) +from sympy.series.residues import residue +from sympy.testing.pytest import XFAIL, raises +from sympy.abc import x, z, a, s, k + + +def test_basic1(): + assert residue(1/x, x, 0) == 1 + assert residue(-2/x, x, 0) == -2 + assert residue(81/x, x, 0) == 81 + assert residue(1/x**2, x, 0) == 0 + assert residue(0, x, 0) == 0 + assert residue(5, x, 0) == 0 + assert residue(x, x, 0) == 0 + assert residue(x**2, x, 0) == 0 + + +def test_basic2(): + assert residue(1/x, x, 1) == 0 + assert residue(-2/x, x, 1) == 0 + assert residue(81/x, x, -1) == 0 + assert residue(1/x**2, x, 1) == 0 + assert residue(0, x, 1) == 0 + assert residue(5, x, 1) == 0 + assert residue(x, x, 1) == 0 + assert residue(x**2, x, 5) == 0 + + +def test_f(): + f = Function("f") + assert residue(f(x)/x**5, x, 0) == f(x).diff(x, 4).subs(x, 0)/24 + + +def test_functions(): + assert residue(1/sin(x), x, 0) == 1 + assert residue(2/sin(x), x, 0) == 2 + assert residue(1/sin(x)**2, x, 0) == 0 + assert residue(1/sin(x)**5, x, 0) == Rational(3, 8) + + +def test_expressions(): + assert residue(1/(x + 1), x, 0) == 0 + assert residue(1/(x + 1), x, -1) == 1 + assert residue(1/(x**2 + 1), x, -1) == 0 + assert residue(1/(x**2 + 1), x, I) == -I/2 + assert residue(1/(x**2 + 1), x, -I) == I/2 + assert residue(1/(x**4 + 1), x, 0) == 0 + assert residue(1/(x**4 + 1), x, exp(I*pi/4)).equals(-(Rational(1, 4) + I/4)/sqrt(2)) + assert residue(1/(x**2 + a**2)**2, x, a*I) == -I/4/a**3 + + +@XFAIL +def test_expressions_failing(): + n = Symbol('n', integer=True, positive=True) + assert residue(exp(z)/(z - pi*I/4*a)**n, z, I*pi*a) == \ + exp(I*pi*a/4)/factorial(n - 1) + + +def test_NotImplemented(): + raises(NotImplementedError, lambda: residue(exp(1/z), z, 0)) + + +def test_bug(): + assert residue(2**(z)*(s + z)*(1 - s - z)/z**2, z, 0) == \ + 1 + s*log(2) - s**2*log(2) - 2*s + + +def test_issue_5654(): + assert residue(1/(x**2 + a**2)**2, x, a*I) == -I/(4*a**3) + assert residue(1/s*1/(z - exp(s)), s, 0) == 1/(z - 1) + assert residue((1 + k)/s*1/(z - exp(s)), s, 0) == k/(z - 1) + 1/(z - 1) + + +def test_issue_6499(): + assert residue(1/(exp(z) - 1), z, 0) == 1 + + +def test_issue_14037(): + assert residue(sin(x**50)/x**51, x, 0) == 1 + + +def test_issue_21176(): + f = x**2*cot(pi*x)/(x**4 + 1) + assert residue(f, x, -sqrt(2)/2 - sqrt(2)*I/2).cancel().together(deep=True)\ + == sqrt(2)*(1 - I)/(8*tan(sqrt(2)*pi*(1 + I)/2)) + + +def test_issue_21177(): + r = -sqrt(3)*tanh(sqrt(3)*pi/2)/3 + a = residue(cot(pi*x)/((x - 1)*(x - 2) + 1), x, S(3)/2 - sqrt(3)*I/2) + b = residue(cot(pi*x)/(x**2 - 3*x + 3), x, S(3)/2 - sqrt(3)*I/2) + assert a == r + assert (b - a).cancel() == 0 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_sequences.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_sequences.py new file mode 100644 index 0000000000000000000000000000000000000000..61e276ad67982f0a9877de3548d70238976d28a5 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_sequences.py @@ -0,0 +1,312 @@ +from sympy.core.containers import Tuple +from sympy.core.function import Function +from sympy.core.numbers import oo, Rational +from sympy.core.singleton import S +from sympy.core.symbol import symbols, Symbol +from sympy.functions.combinatorial.numbers import tribonacci, fibonacci +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import cos, sin +from sympy.series import EmptySequence +from sympy.series.sequences import (SeqMul, SeqAdd, SeqPer, SeqFormula, + sequence) +from sympy.sets.sets import Interval +from sympy.tensor.indexed import Indexed, Idx +from sympy.series.sequences import SeqExpr, SeqExprOp, RecursiveSeq +from sympy.testing.pytest import raises, slow + +x, y, z = symbols('x y z') +n, m = symbols('n m') + + +def test_EmptySequence(): + assert S.EmptySequence is EmptySequence + + assert S.EmptySequence.interval is S.EmptySet + assert S.EmptySequence.length is S.Zero + + assert list(S.EmptySequence) == [] + + +def test_SeqExpr(): + #SeqExpr is a baseclass and does not take care of + #ensuring all arguments are Basics hence the use of + #Tuple(...) here. + s = SeqExpr(Tuple(1, n, y), Tuple(x, 0, 10)) + + assert isinstance(s, SeqExpr) + assert s.gen == (1, n, y) + assert s.interval == Interval(0, 10) + assert s.start == 0 + assert s.stop == 10 + assert s.length == 11 + assert s.variables == (x,) + + assert SeqExpr(Tuple(1, 2, 3), Tuple(x, 0, oo)).length is oo + + +def test_SeqPer(): + s = SeqPer((1, n, 3), (x, 0, 5)) + + assert isinstance(s, SeqPer) + assert s.periodical == Tuple(1, n, 3) + assert s.period == 3 + assert s.coeff(3) == 1 + assert s.free_symbols == {n} + + assert list(s) == [1, n, 3, 1, n, 3] + assert s[:] == [1, n, 3, 1, n, 3] + assert SeqPer((1, n, 3), (x, -oo, 0))[0:6] == [1, n, 3, 1, n, 3] + + raises(ValueError, lambda: SeqPer((1, 2, 3), (0, 1, 2))) + raises(ValueError, lambda: SeqPer((1, 2, 3), (x, -oo, oo))) + raises(ValueError, lambda: SeqPer(n**2, (0, oo))) + + assert SeqPer((n, n**2, n**3), (m, 0, oo))[:6] == \ + [n, n**2, n**3, n, n**2, n**3] + assert SeqPer((n, n**2, n**3), (n, 0, oo))[:6] == [0, 1, 8, 3, 16, 125] + assert SeqPer((n, m), (n, 0, oo))[:6] == [0, m, 2, m, 4, m] + + +def test_SeqFormula(): + s = SeqFormula(n**2, (n, 0, 5)) + + assert isinstance(s, SeqFormula) + assert s.formula == n**2 + assert s.coeff(3) == 9 + + assert list(s) == [i**2 for i in range(6)] + assert s[:] == [i**2 for i in range(6)] + assert SeqFormula(n**2, (n, -oo, 0))[0:6] == [i**2 for i in range(6)] + + assert SeqFormula(n**2, (0, oo)) == SeqFormula(n**2, (n, 0, oo)) + + assert SeqFormula(n**2, (0, m)).subs(m, x) == SeqFormula(n**2, (0, x)) + assert SeqFormula(m*n**2, (n, 0, oo)).subs(m, x) == \ + SeqFormula(x*n**2, (n, 0, oo)) + + raises(ValueError, lambda: SeqFormula(n**2, (0, 1, 2))) + raises(ValueError, lambda: SeqFormula(n**2, (n, -oo, oo))) + raises(ValueError, lambda: SeqFormula(m*n**2, (0, oo))) + + seq = SeqFormula(x*(y**2 + z), (z, 1, 100)) + assert seq.expand() == SeqFormula(x*y**2 + x*z, (z, 1, 100)) + seq = SeqFormula(sin(x*(y**2 + z)),(z, 1, 100)) + assert seq.expand(trig=True) == SeqFormula(sin(x*y**2)*cos(x*z) + sin(x*z)*cos(x*y**2), (z, 1, 100)) + assert seq.expand() == SeqFormula(sin(x*y**2 + x*z), (z, 1, 100)) + assert seq.expand(trig=False) == SeqFormula(sin(x*y**2 + x*z), (z, 1, 100)) + seq = SeqFormula(exp(x*(y**2 + z)), (z, 1, 100)) + assert seq.expand() == SeqFormula(exp(x*y**2)*exp(x*z), (z, 1, 100)) + assert seq.expand(power_exp=False) == SeqFormula(exp(x*y**2 + x*z), (z, 1, 100)) + assert seq.expand(mul=False, power_exp=False) == SeqFormula(exp(x*(y**2 + z)), (z, 1, 100)) + +def test_sequence(): + form = SeqFormula(n**2, (n, 0, 5)) + per = SeqPer((1, 2, 3), (n, 0, 5)) + inter = SeqFormula(n**2) + + assert sequence(n**2, (n, 0, 5)) == form + assert sequence((1, 2, 3), (n, 0, 5)) == per + assert sequence(n**2) == inter + + +def test_SeqExprOp(): + form = SeqFormula(n**2, (n, 0, 10)) + per = SeqPer((1, 2, 3), (m, 5, 10)) + + s = SeqExprOp(form, per) + assert s.gen == (n**2, (1, 2, 3)) + assert s.interval == Interval(5, 10) + assert s.start == 5 + assert s.stop == 10 + assert s.length == 6 + assert s.variables == (n, m) + + +def test_SeqAdd(): + per = SeqPer((1, 2, 3), (n, 0, oo)) + form = SeqFormula(n**2) + + per_bou = SeqPer((1, 2), (n, 1, 5)) + form_bou = SeqFormula(n**2, (6, 10)) + form_bou2 = SeqFormula(n**2, (1, 5)) + + assert SeqAdd() == S.EmptySequence + assert SeqAdd(S.EmptySequence) == S.EmptySequence + assert SeqAdd(per) == per + assert SeqAdd(per, S.EmptySequence) == per + assert SeqAdd(per_bou, form_bou) == S.EmptySequence + + s = SeqAdd(per_bou, form_bou2, evaluate=False) + assert s.args == (form_bou2, per_bou) + assert s[:] == [2, 6, 10, 18, 26] + assert list(s) == [2, 6, 10, 18, 26] + + assert isinstance(SeqAdd(per, per_bou, evaluate=False), SeqAdd) + + s1 = SeqAdd(per, per_bou) + assert isinstance(s1, SeqPer) + assert s1 == SeqPer((2, 4, 4, 3, 3, 5), (n, 1, 5)) + s2 = SeqAdd(form, form_bou) + assert isinstance(s2, SeqFormula) + assert s2 == SeqFormula(2*n**2, (6, 10)) + + assert SeqAdd(form, form_bou, per) == \ + SeqAdd(per, SeqFormula(2*n**2, (6, 10))) + assert SeqAdd(form, SeqAdd(form_bou, per)) == \ + SeqAdd(per, SeqFormula(2*n**2, (6, 10))) + assert SeqAdd(per, SeqAdd(form, form_bou), evaluate=False) == \ + SeqAdd(per, SeqFormula(2*n**2, (6, 10))) + + assert SeqAdd(SeqPer((1, 2), (n, 0, oo)), SeqPer((1, 2), (m, 0, oo))) == \ + SeqPer((2, 4), (n, 0, oo)) + + +def test_SeqMul(): + per = SeqPer((1, 2, 3), (n, 0, oo)) + form = SeqFormula(n**2) + + per_bou = SeqPer((1, 2), (n, 1, 5)) + form_bou = SeqFormula(n**2, (n, 6, 10)) + form_bou2 = SeqFormula(n**2, (1, 5)) + + assert SeqMul() == S.EmptySequence + assert SeqMul(S.EmptySequence) == S.EmptySequence + assert SeqMul(per) == per + assert SeqMul(per, S.EmptySequence) == S.EmptySequence + assert SeqMul(per_bou, form_bou) == S.EmptySequence + + s = SeqMul(per_bou, form_bou2, evaluate=False) + assert s.args == (form_bou2, per_bou) + assert s[:] == [1, 8, 9, 32, 25] + assert list(s) == [1, 8, 9, 32, 25] + + assert isinstance(SeqMul(per, per_bou, evaluate=False), SeqMul) + + s1 = SeqMul(per, per_bou) + assert isinstance(s1, SeqPer) + assert s1 == SeqPer((1, 4, 3, 2, 2, 6), (n, 1, 5)) + s2 = SeqMul(form, form_bou) + assert isinstance(s2, SeqFormula) + assert s2 == SeqFormula(n**4, (6, 10)) + + assert SeqMul(form, form_bou, per) == \ + SeqMul(per, SeqFormula(n**4, (6, 10))) + assert SeqMul(form, SeqMul(form_bou, per)) == \ + SeqMul(per, SeqFormula(n**4, (6, 10))) + assert SeqMul(per, SeqMul(form, form_bou2, + evaluate=False), evaluate=False) == \ + SeqMul(form, per, form_bou2, evaluate=False) + + assert SeqMul(SeqPer((1, 2), (n, 0, oo)), SeqPer((1, 2), (n, 0, oo))) == \ + SeqPer((1, 4), (n, 0, oo)) + + +def test_add(): + per = SeqPer((1, 2), (n, 0, oo)) + form = SeqFormula(n**2) + + assert per + (SeqPer((2, 3))) == SeqPer((3, 5), (n, 0, oo)) + assert form + SeqFormula(n**3) == SeqFormula(n**2 + n**3) + + assert per + form == SeqAdd(per, form) + + raises(TypeError, lambda: per + n) + raises(TypeError, lambda: n + per) + + +def test_sub(): + per = SeqPer((1, 2), (n, 0, oo)) + form = SeqFormula(n**2) + + assert per - (SeqPer((2, 3))) == SeqPer((-1, -1), (n, 0, oo)) + assert form - (SeqFormula(n**3)) == SeqFormula(n**2 - n**3) + + assert per - form == SeqAdd(per, -form) + + raises(TypeError, lambda: per - n) + raises(TypeError, lambda: n - per) + + +def test_mul__coeff_mul(): + assert SeqPer((1, 2), (n, 0, oo)).coeff_mul(2) == SeqPer((2, 4), (n, 0, oo)) + assert SeqFormula(n**2).coeff_mul(2) == SeqFormula(2*n**2) + assert S.EmptySequence.coeff_mul(100) == S.EmptySequence + + assert SeqPer((1, 2), (n, 0, oo)) * (SeqPer((2, 3))) == \ + SeqPer((2, 6), (n, 0, oo)) + assert SeqFormula(n**2) * SeqFormula(n**3) == SeqFormula(n**5) + + assert S.EmptySequence * SeqFormula(n**2) == S.EmptySequence + assert SeqFormula(n**2) * S.EmptySequence == S.EmptySequence + + raises(TypeError, lambda: sequence(n**2) * n) + raises(TypeError, lambda: n * sequence(n**2)) + + +def test_neg(): + assert -SeqPer((1, -2), (n, 0, oo)) == SeqPer((-1, 2), (n, 0, oo)) + assert -SeqFormula(n**2) == SeqFormula(-n**2) + + +def test_operations(): + per = SeqPer((1, 2), (n, 0, oo)) + per2 = SeqPer((2, 4), (n, 0, oo)) + form = SeqFormula(n**2) + form2 = SeqFormula(n**3) + + assert per + form + form2 == SeqAdd(per, form, form2) + assert per + form - form2 == SeqAdd(per, form, -form2) + assert per + form - S.EmptySequence == SeqAdd(per, form) + assert per + per2 + form == SeqAdd(SeqPer((3, 6), (n, 0, oo)), form) + assert S.EmptySequence - per == -per + assert form + form == SeqFormula(2*n**2) + + assert per * form * form2 == SeqMul(per, form, form2) + assert form * form == SeqFormula(n**4) + assert form * -form == SeqFormula(-n**4) + + assert form * (per + form2) == SeqMul(form, SeqAdd(per, form2)) + assert form * (per + per) == SeqMul(form, per2) + + assert form.coeff_mul(m) == SeqFormula(m*n**2, (n, 0, oo)) + assert per.coeff_mul(m) == SeqPer((m, 2*m), (n, 0, oo)) + + +def test_Idx_limits(): + i = symbols('i', cls=Idx) + r = Indexed('r', i) + + assert SeqFormula(r, (i, 0, 5))[:] == [r.subs(i, j) for j in range(6)] + assert SeqPer((1, 2), (i, 0, 5))[:] == [1, 2, 1, 2, 1, 2] + + +@slow +def test_find_linear_recurrence(): + assert sequence((0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55), \ + (n, 0, 10)).find_linear_recurrence(11) == [1, 1] + assert sequence((1, 2, 4, 7, 28, 128, 582, 2745, 13021, 61699, 292521, \ + 1387138), (n, 0, 11)).find_linear_recurrence(12) == [5, -2, 6, -11] + assert sequence(x*n**3+y*n, (n, 0, oo)).find_linear_recurrence(10) \ + == [4, -6, 4, -1] + assert sequence(x**n, (n,0,20)).find_linear_recurrence(21) == [x] + assert sequence((1,2,3)).find_linear_recurrence(10, 5) == [0, 0, 1] + assert sequence(((1 + sqrt(5))/2)**n + \ + (-(1 + sqrt(5))/2)**(-n)).find_linear_recurrence(10) == [1, 1] + assert sequence(x*((1 + sqrt(5))/2)**n + y*(-(1 + sqrt(5))/2)**(-n), \ + (n,0,oo)).find_linear_recurrence(10) == [1, 1] + assert sequence((1,2,3,4,6),(n, 0, 4)).find_linear_recurrence(5) == [] + assert sequence((2,3,4,5,6,79),(n, 0, 5)).find_linear_recurrence(6,gfvar=x) \ + == ([], None) + assert sequence((2,3,4,5,8,30),(n, 0, 5)).find_linear_recurrence(6,gfvar=x) \ + == ([Rational(19, 2), -20, Rational(27, 2)], (-31*x**2 + 32*x - 4)/(27*x**3 - 40*x**2 + 19*x -2)) + assert sequence(fibonacci(n)).find_linear_recurrence(30,gfvar=x) \ + == ([1, 1], -x/(x**2 + x - 1)) + assert sequence(tribonacci(n)).find_linear_recurrence(30,gfvar=x) \ + == ([1, 1, 1], -x/(x**3 + x**2 + x - 1)) + +def test_RecursiveSeq(): + y = Function('y') + n = Symbol('n') + fib = RecursiveSeq(y(n - 1) + y(n - 2), y(n), n, [0, 1]) + assert fib.coeff(3) == 2 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_series.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_series.py new file mode 100644 index 0000000000000000000000000000000000000000..e3f3c122b98c14a58c6d5c6636cbb53e1e66a75d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/series/tests/test_series.py @@ -0,0 +1,421 @@ +from sympy.core.evalf import N +from sympy.core.function import (Derivative, Function, PoleError, Subs) +from sympy.core.numbers import (E, Float, Rational, oo, pi, I) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.exponential import (LambertW, exp, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (atan, cos, sin) +from sympy.functions.special.gamma_functions import gamma +from sympy.integrals.integrals import Integral, integrate +from sympy.series.order import O +from sympy.series.series import series +from sympy.abc import x, y, n, k +from sympy.testing.pytest import raises +from sympy.core import EulerGamma + + +def test_sin(): + e1 = sin(x).series(x, 0) + e2 = series(sin(x), x, 0) + assert e1 == e2 + + +def test_cos(): + e1 = cos(x).series(x, 0) + e2 = series(cos(x), x, 0) + assert e1 == e2 + + +def test_exp(): + e1 = exp(x).series(x, 0) + e2 = series(exp(x), x, 0) + assert e1 == e2 + + +def test_exp2(): + e1 = exp(cos(x)).series(x, 0) + e2 = series(exp(cos(x)), x, 0) + assert e1 == e2 + + +def test_issue_5223(): + assert series(1, x) == 1 + assert next(S.Zero.lseries(x)) == 0 + assert cos(x).series() == cos(x).series(x) + raises(ValueError, lambda: cos(x + y).series()) + raises(ValueError, lambda: x.series(dir="")) + + assert (cos(x).series(x, 1) - + cos(x + 1).series(x).subs(x, x - 1)).removeO() == 0 + e = cos(x).series(x, 1, n=None) + assert [next(e) for i in range(2)] == [cos(1), -((x - 1)*sin(1))] + e = cos(x).series(x, 1, n=None, dir='-') + assert [next(e) for i in range(2)] == [cos(1), (1 - x)*sin(1)] + # the following test is exact so no need for x -> x - 1 replacement + assert abs(x).series(x, 1, dir='-') == x + assert exp(x).series(x, 1, dir='-', n=3).removeO() == \ + E - E*(-x + 1) + E*(-x + 1)**2/2 + + D = Derivative + assert D(x**2 + x**3*y**2, x, 2, y, 1).series(x).doit() == 12*x*y + assert next(D(cos(x), x).lseries()) == D(1, x) + assert D( + exp(x), x).series(n=3) == D(1, x) + D(x, x) + D(x**2/2, x) + D(x**3/6, x) + O(x**3) + + assert Integral(x, (x, 1, 3), (y, 1, x)).series(x) == -4 + 4*x + + assert (1 + x + O(x**2)).getn() == 2 + assert (1 + x).getn() is None + + raises(PoleError, lambda: ((1/sin(x))**oo).series()) + logx = Symbol('logx') + assert ((sin(x))**y).nseries(x, n=1, logx=logx) == \ + exp(y*logx) + O(x*exp(y*logx), x) + + assert sin(1/x).series(x, oo, n=5) == 1/x - 1/(6*x**3) + O(x**(-5), (x, oo)) + assert abs(x).series(x, oo, n=5, dir='+') == x + assert abs(x).series(x, -oo, n=5, dir='-') == -x + assert abs(-x).series(x, oo, n=5, dir='+') == x + assert abs(-x).series(x, -oo, n=5, dir='-') == -x + + assert exp(x*log(x)).series(n=3) == \ + 1 + x*log(x) + x**2*log(x)**2/2 + O(x**3*log(x)**3) + # XXX is this right? If not, fix "ngot > n" handling in expr. + p = Symbol('p', positive=True) + assert exp(sqrt(p)**3*log(p)).series(n=3) == \ + 1 + p**S('3/2')*log(p) + O(p**3*log(p)**3) + + assert exp(sin(x)*log(x)).series(n=2) == 1 + x*log(x) + O(x**2*log(x)**2) + + +def test_issue_6350(): + expr = integrate(exp(k*(y**3 - 3*y)), (y, 0, oo), conds='none') + assert expr.series(k, 0, 3) == -(-1)**(S(2)/3)*sqrt(3)*gamma(S(1)/3)**2*gamma(S(2)/3)/(6*pi*k**(S(1)/3)) - \ + sqrt(3)*k*gamma(-S(2)/3)*gamma(-S(1)/3)/(6*pi) - \ + (-1)**(S(1)/3)*sqrt(3)*k**(S(1)/3)*gamma(-S(1)/3)*gamma(S(1)/3)*gamma(S(2)/3)/(6*pi) - \ + (-1)**(S(2)/3)*sqrt(3)*k**(S(5)/3)*gamma(S(1)/3)**2*gamma(S(2)/3)/(4*pi) - \ + (-1)**(S(1)/3)*sqrt(3)*k**(S(7)/3)*gamma(-S(1)/3)*gamma(S(1)/3)*gamma(S(2)/3)/(8*pi) + O(k**3) + + +def test_issue_11313(): + assert Integral(cos(x), x).series(x) == sin(x).series(x) + assert Derivative(sin(x), x).series(x, n=3).doit() == cos(x).series(x, n=3) + + assert Derivative(x**3, x).as_leading_term(x) == 3*x**2 + assert Derivative(x**3, y).as_leading_term(x) == 0 + assert Derivative(sin(x), x).as_leading_term(x) == 1 + assert Derivative(cos(x), x).as_leading_term(x) == -x + + # This result is equivalent to zero, zero is not return because + # `Expr.series` doesn't currently detect an `x` in its `free_symbol`s. + assert Derivative(1, x).as_leading_term(x) == Derivative(1, x) + + assert Derivative(exp(x), x).series(x).doit() == exp(x).series(x) + assert 1 + Integral(exp(x), x).series(x) == exp(x).series(x) + + assert Derivative(log(x), x).series(x).doit() == (1/x).series(x) + assert Integral(log(x), x).series(x) == Integral(log(x), x).doit().series(x).removeO() + + +def test_series_of_Subs(): + from sympy.abc import z + + subs1 = Subs(sin(x), x, y) + subs2 = Subs(sin(x) * cos(z), x, y) + subs3 = Subs(sin(x * z), (x, z), (y, x)) + + assert subs1.series(x) == subs1 + subs1_series = (Subs(x, x, y) + Subs(-x**3/6, x, y) + + Subs(x**5/120, x, y) + O(y**6)) + assert subs1.series() == subs1_series + assert subs1.series(y) == subs1_series + assert subs1.series(z) == subs1 + assert subs2.series(z) == (Subs(z**4*sin(x)/24, x, y) + + Subs(-z**2*sin(x)/2, x, y) + Subs(sin(x), x, y) + O(z**6)) + assert subs3.series(x).doit() == subs3.doit().series(x) + assert subs3.series(z).doit() == sin(x*y) + + raises(ValueError, lambda: Subs(x + 2*y, y, z).series()) + assert Subs(x + y, y, z).series(x).doit() == x + z + + +def test_issue_3978(): + f = Function('f') + assert f(x).series(x, 0, 3, dir='-') == \ + f(0) + x*Subs(Derivative(f(x), x), x, 0) + \ + x**2*Subs(Derivative(f(x), x, x), x, 0)/2 + O(x**3) + assert f(x).series(x, 0, 3) == \ + f(0) + x*Subs(Derivative(f(x), x), x, 0) + \ + x**2*Subs(Derivative(f(x), x, x), x, 0)/2 + O(x**3) + assert f(x**2).series(x, 0, 3) == \ + f(0) + x**2*Subs(Derivative(f(x), x), x, 0) + O(x**3) + assert f(x**2+1).series(x, 0, 3) == \ + f(1) + x**2*Subs(Derivative(f(x), x), x, 1) + O(x**3) + + class TestF(Function): + pass + + assert TestF(x).series(x, 0, 3) == TestF(0) + \ + x*Subs(Derivative(TestF(x), x), x, 0) + \ + x**2*Subs(Derivative(TestF(x), x, x), x, 0)/2 + O(x**3) + +from sympy.series.acceleration import richardson, shanks +from sympy.concrete.summations import Sum +from sympy.core.numbers import Integer + + +def test_acceleration(): + e = (1 + 1/n)**n + assert round(richardson(e, n, 10, 20).evalf(), 10) == round(E.evalf(), 10) + + A = Sum(Integer(-1)**(k + 1) / k, (k, 1, n)) + assert round(shanks(A, n, 25).evalf(), 4) == round(log(2).evalf(), 4) + assert round(shanks(A, n, 25, 5).evalf(), 10) == round(log(2).evalf(), 10) + + +def test_issue_5852(): + assert series(1/cos(x/log(x)), x, 0) == 1 + x**2/(2*log(x)**2) + \ + 5*x**4/(24*log(x)**4) + O(x**6) + + +def test_issue_4583(): + assert cos(1 + x + x**2).series(x, 0, 5) == cos(1) - x*sin(1) + \ + x**2*(-sin(1) - cos(1)/2) + x**3*(-cos(1) + sin(1)/6) + \ + x**4*(-11*cos(1)/24 + sin(1)/2) + O(x**5) + + +def test_issue_6318(): + eq = (1/x)**Rational(2, 3) + assert (eq + 1).as_leading_term(x) == eq + + +def test_x_is_base_detection(): + eq = (x**2)**Rational(2, 3) + assert eq.series() == x**Rational(4, 3) + + +def test_issue_7203(): + assert series(cos(x), x, pi, 3) == \ + -1 + (x - pi)**2/2 + O((x - pi)**3, (x, pi)) + + +def test_exp_product_positive_factors(): + a, b = symbols('a, b', positive=True) + x = a * b + assert series(exp(x), x, n=8) == 1 + a*b + a**2*b**2/2 + \ + a**3*b**3/6 + a**4*b**4/24 + a**5*b**5/120 + a**6*b**6/720 + \ + a**7*b**7/5040 + O(a**8*b**8, a, b) + + +def test_issue_8805(): + assert series(1, n=8) == 1 + + +def test_issue_9173(): + p0,p1,p2,p3,b0,b1,b2=symbols('p0 p1 p2 p3 b0 b1 b2') + Q=(p0+(p1+(p2+p3/y)/y)/y)/(1+((p3/(b0*y)+(b0*p2-b1*p3)/b0**2)/y+\ + (b0**2*p1-b0*b1*p2-p3*(b0*b2-b1**2))/b0**3)/y) + + series = Q.series(y,n=3) + + assert series == y*(b0*p2/p3+b0*(-p2/p3+b1/b0))+y**2*(b0*p1/p3+b0*p2*\ + (-p2/p3+b1/b0)/p3+b0*(-p1/p3+(p2/p3-b1/b0)**2+b1*p2/(b0*p3)+\ + b2/b0-b1**2/b0**2))+b0+O(y**3) + assert series.simplify() == b2*y**2 + b1*y + b0 + O(y**3) + + +def test_issue_9549(): + y = (x**2 + x + 1) / (x**3 + x**2) + assert series(y, x, oo) == x**(-5) - 1/x**4 + x**(-3) + 1/x + O(x**(-6), (x, oo)) + + +def test_issue_10761(): + assert series(1/(x**-2 + x**-3), x, 0) == x**3 - x**4 + x**5 + O(x**6) + + +def test_issue_12578(): + y = (1 - 1/(x/2 - 1/(2*x))**4)**(S(1)/8) + assert y.series(x, 0, n=17) == 1 - 2*x**4 - 8*x**6 - 34*x**8 - 152*x**10 - 714*x**12 - \ + 3472*x**14 - 17318*x**16 + O(x**17) + + +def test_issue_12791(): + beta = symbols('beta', positive=True) + theta, varphi = symbols('theta varphi', real=True) + + expr = (-beta**2*varphi*sin(theta) + beta**2*cos(theta) + \ + beta*varphi*sin(theta) - beta*cos(theta) - beta + 1)/(beta*cos(theta) - 1)**2 + + sol = (0.5/(0.5*cos(theta) - 1.0)**2 - 0.25*cos(theta)/(0.5*cos(theta) - 1.0)**2 + + (beta - 0.5)*(-0.25*varphi*sin(2*theta) - 1.5*cos(theta) + + 0.25*cos(2*theta) + 1.25)/((0.5*cos(theta) - 1.0)**2*(0.5*cos(theta) - 1.0)) + + 0.25*varphi*sin(theta)/(0.5*cos(theta) - 1.0)**2 + + O((beta - S.Half)**2, (beta, S.Half))) + + assert expr.series(beta, 0.5, 2).trigsimp() == sol + + +def test_issue_14384(): + x, a = symbols('x a') + assert series(x**a, x) == x**a + assert series(x**(-2*a), x) == x**(-2*a) + assert series(exp(a*log(x)), x) == exp(a*log(x)) + raises(PoleError, lambda: series(x**I, x)) + raises(PoleError, lambda: series(x**(I + 1), x)) + raises(PoleError, lambda: series(exp(I*log(x)), x)) + + +def test_issue_14885(): + assert series(x**Rational(-3, 2)*exp(x), x, 0) == (x**Rational(-3, 2) + 1/sqrt(x) + + sqrt(x)/2 + x**Rational(3, 2)/6 + x**Rational(5, 2)/24 + x**Rational(7, 2)/120 + + x**Rational(9, 2)/720 + x**Rational(11, 2)/5040 + O(x**6)) + + +def test_issue_15539(): + assert series(atan(x), x, -oo) == (-1/(5*x**5) + 1/(3*x**3) - 1/x - pi/2 + + O(x**(-6), (x, -oo))) + assert series(atan(x), x, oo) == (-1/(5*x**5) + 1/(3*x**3) - 1/x + pi/2 + + O(x**(-6), (x, oo))) + + +def test_issue_7259(): + assert series(LambertW(x), x) == x - x**2 + 3*x**3/2 - 8*x**4/3 + 125*x**5/24 + O(x**6) + assert series(LambertW(x**2), x, n=8) == x**2 - x**4 + 3*x**6/2 + O(x**8) + assert series(LambertW(sin(x)), x, n=4) == x - x**2 + 4*x**3/3 + O(x**4) + +def test_issue_11884(): + assert cos(x).series(x, 1, n=1) == cos(1) + O(x - 1, (x, 1)) + + +def test_issue_18008(): + y = x*(1 + x*(1 - x))/((1 + x*(1 - x)) - (1 - x)*(1 - x)) + assert y.series(x, oo, n=4) == -9/(32*x**3) - 3/(16*x**2) - 1/(8*x) + S(1)/4 + x/2 + \ + O(x**(-4), (x, oo)) + + +def test_issue_18842(): + f = log(x/(1 - x)) + assert f.series(x, 0.491, n=1).removeO().nsimplify() == \ + -S(180019443780011)/5000000000000000 + + +def test_issue_19534(): + dt = symbols('dt', real=True) + expr = 16*dt*(0.125*dt*(2.0*dt + 1.0) + 0.875*dt + 1.0)/45 + \ + 49*dt*(-0.049335189898860408029*dt*(2.0*dt + 1.0) + \ + 0.29601113939316244817*dt*(0.125*dt*(2.0*dt + 1.0) + 0.875*dt + 1.0) - \ + 0.12564355335492979587*dt*(0.074074074074074074074*dt*(2.0*dt + 1.0) + \ + 0.2962962962962962963*dt*(0.125*dt*(2.0*dt + 1.0) + 0.875*dt + 1.0) + \ + 0.96296296296296296296*dt + 1.0) + 0.051640768506639183825*dt + \ + dt*(1/2 - sqrt(21)/14) + 1.0)/180 + 49*dt*(-0.23637909581542530626*dt*(2.0*dt + 1.0) - \ + 0.74817562366625959291*dt*(0.125*dt*(2.0*dt + 1.0) + 0.875*dt + 1.0) + \ + 0.88085458023927036857*dt*(0.074074074074074074074*dt*(2.0*dt + 1.0) + \ + 0.2962962962962962963*dt*(0.125*dt*(2.0*dt + 1.0) + 0.875*dt + 1.0) + \ + 0.96296296296296296296*dt + 1.0) + \ + 2.1165151389911680013*dt*(-0.049335189898860408029*dt*(2.0*dt + 1.0) + \ + 0.29601113939316244817*dt*(0.125*dt*(2.0*dt + 1.0) + 0.875*dt + 1.0) - \ + 0.12564355335492979587*dt*(0.074074074074074074074*dt*(2.0*dt + 1.0) + \ + 0.2962962962962962963*dt*(0.125*dt*(2.0*dt + 1.0) + 0.875*dt + 1.0) + \ + 0.96296296296296296296*dt + 1.0) + 0.22431393315265061193*dt + 1.0) - \ + 1.1854881643947648988*dt + dt*(sqrt(21)/14 + 1/2) + 1.0)/180 + \ + dt*(0.66666666666666666667*dt*(2.0*dt + 1.0) + \ + 6.0173399699313066769*dt*(0.125*dt*(2.0*dt + 1.0) + 0.875*dt + 1.0) - \ + 4.1117044797036320069*dt*(0.074074074074074074074*dt*(2.0*dt + 1.0) + \ + 0.2962962962962962963*dt*(0.125*dt*(2.0*dt + 1.0) + 0.875*dt + 1.0) + \ + 0.96296296296296296296*dt + 1.0) - \ + 7.0189140975801991157*dt*(-0.049335189898860408029*dt*(2.0*dt + 1.0) + \ + 0.29601113939316244817*dt*(0.125*dt*(2.0*dt + 1.0) + 0.875*dt + 1.0) - \ + 0.12564355335492979587*dt*(0.074074074074074074074*dt*(2.0*dt + 1.0) + \ + 0.2962962962962962963*dt*(0.125*dt*(2.0*dt + 1.0) + 0.875*dt + 1.0) + \ + 0.96296296296296296296*dt + 1.0) + 0.22431393315265061193*dt + 1.0) + \ + 0.94010945196161777522*dt*(-0.23637909581542530626*dt*(2.0*dt + 1.0) - \ + 0.74817562366625959291*dt*(0.125*dt*(2.0*dt + 1.0) + 0.875*dt + 1.0) + \ + 0.88085458023927036857*dt*(0.074074074074074074074*dt*(2.0*dt + 1.0) + \ + 0.2962962962962962963*dt*(0.125*dt*(2.0*dt + 1.0) + 0.875*dt + 1.0) + \ + 0.96296296296296296296*dt + 1.0) + \ + 2.1165151389911680013*dt*(-0.049335189898860408029*dt*(2.0*dt + 1.0) + \ + 0.29601113939316244817*dt*(0.125*dt*(2.0*dt + 1.0) + 0.875*dt + 1.0) - \ + 0.12564355335492979587*dt*(0.074074074074074074074*dt*(2.0*dt + 1.0) + \ + 0.2962962962962962963*dt*(0.125*dt*(2.0*dt + 1.0) + 0.875*dt + 1.0) + \ + 0.96296296296296296296*dt + 1.0) + 0.22431393315265061193*dt + 1.0) - \ + 0.35816132904077632692*dt + 1.0) + 5.5065024887242400038*dt + 1.0)/20 + dt/20 + 1 + + assert N(expr.series(dt, 0, 8), 20) == ( + - Float('0.00092592592592592596126289', precision=70) * dt**7 + + Float('0.0027777777777777783174695', precision=70) * dt**6 + + Float('0.016666666666666656027029', precision=70) * dt**5 + + Float('0.083333333333333300951828', precision=70) * dt**4 + + Float('0.33333333333333337034077', precision=70) * dt**3 + + Float('1.0', precision=70) * dt**2 + + Float('1.0', precision=70) * dt + + Float('1.0', precision=70) + ) + + +def test_issue_11407(): + a, b, c, x = symbols('a b c x') + assert series(sqrt(a + b + c*x), x, 0, 1) == sqrt(a + b) + O(x) + assert series(sqrt(a + b + c + c*x), x, 0, 1) == sqrt(a + b + c) + O(x) + + +def test_issue_14037(): + assert (sin(x**50)/x**51).series(x, n=0) == 1/x + O(1, x) + + +def test_issue_20551(): + expr = (exp(x)/x).series(x, n=None) + terms = [ next(expr) for i in range(3) ] + assert terms == [1/x, 1, x/2] + + +def test_issue_20697(): + p_0, p_1, p_2, p_3, b_0, b_1, b_2 = symbols('p_0 p_1 p_2 p_3 b_0 b_1 b_2') + Q = (p_0 + (p_1 + (p_2 + p_3/y)/y)/y)/(1 + ((p_3/(b_0*y) + (b_0*p_2\ + - b_1*p_3)/b_0**2)/y + (b_0**2*p_1 - b_0*b_1*p_2 - p_3*(b_0*b_2\ + - b_1**2))/b_0**3)/y) + assert Q.series(y, n=3).ratsimp() == b_2*y**2 + b_1*y + b_0 + O(y**3) + + +def test_issue_21245(): + fi = (1 + sqrt(5))/2 + assert (1/(1 - x - x**2)).series(x, 1/fi, 1).factor() == \ + (-37*sqrt(5) - 83 + 13*sqrt(5)*x + 29*x + O((x - 2/(1 + sqrt(5)))**2, (x\ + , 2/(1 + sqrt(5)))))/((2*sqrt(5) + 5)**2*(x + sqrt(5)*x - 2)) + + + +def test_issue_21938(): + expr = sin(1/x + exp(-x)) - sin(1/x) + assert expr.series(x, oo) == (1/(24*x**4) - 1/(2*x**2) + 1 + O(x**(-6), (x, oo)))*exp(-x) + + +def test_issue_23432(): + expr = 1/sqrt(1 - x**2) + result = expr.series(x, 0.5) + assert result.is_Add and len(result.args) == 7 + + +def test_issue_23727(): + res = series(sqrt(1 - x**2), x, 0.1) + assert res.is_Add == True + + +def test_issue_24266(): + #type1: exp(f(x)) + assert (exp(-I*pi*(2*x+1))).series(x, 0, 3) == -1 + 2*I*pi*x + 2*pi**2*x**2 + O(x**3) + assert (exp(-I*pi*(2*x+1))*gamma(1+x)).series(x, 0, 3) == -1 + x*(EulerGamma + 2*I*pi) + \ + x**2*(-EulerGamma**2/2 + 23*pi**2/12 - 2*EulerGamma*I*pi) + O(x**3) + + #type2: c**f(x) + assert ((2*I)**(-I*pi*(2*x+1))).series(x, 0, 2) == exp(pi**2/2 - I*pi*log(2)) + \ + x*(pi**2*exp(pi**2/2 - I*pi*log(2)) - 2*I*pi*exp(pi**2/2 - I*pi*log(2))*log(2)) + O(x**2) + assert ((2)**(-I*pi*(2*x+1))).series(x, 0, 2) == exp(-I*pi*log(2)) - 2*I*pi*x*exp(-I*pi*log(2))*log(2) + O(x**2) + + #type3: f(y)**g(x) + assert ((y)**(I*pi*(2*x+1))).series(x, 0, 2) == exp(I*pi*log(y)) + 2*I*pi*x*exp(I*pi*log(y))*log(y) + O(x**2) + assert ((I*y)**(I*pi*(2*x+1))).series(x, 0, 2) == exp(I*pi*log(I*y)) + 2*I*pi*x*exp(I*pi*log(I*y))*log(I*y) + O(x**2) + + +def test_issue_26856(): + raises(ValueError, lambda: (2**x).series(x, oo, -1)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b909c0b5ef03b1e1e76dfbf4288f61860575da7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/__init__.py @@ -0,0 +1,36 @@ +from .sets import (Set, Interval, Union, FiniteSet, ProductSet, + Intersection, imageset, Complement, SymmetricDifference, + DisjointUnion) + +from .fancysets import ImageSet, Range, ComplexRegion +from .contains import Contains +from .conditionset import ConditionSet +from .ordinals import Ordinal, OmegaPower, ord0 +from .powerset import PowerSet +from ..core.singleton import S +from .handlers.comparison import _eval_is_eq # noqa:F401 +Complexes = S.Complexes +EmptySet = S.EmptySet +Integers = S.Integers +Naturals = S.Naturals +Naturals0 = S.Naturals0 +Rationals = S.Rationals +Reals = S.Reals +UniversalSet = S.UniversalSet + +__all__ = [ + 'Set', 'Interval', 'Union', 'EmptySet', 'FiniteSet', 'ProductSet', + 'Intersection', 'imageset', 'Complement', 'SymmetricDifference', 'DisjointUnion', + + 'ImageSet', 'Range', 'ComplexRegion', 'Reals', + + 'Contains', + + 'ConditionSet', + + 'Ordinal', 'OmegaPower', 'ord0', + + 'PowerSet', + + 'Reals', 'Naturals', 'Naturals0', 'UniversalSet', 'Integers', 'Rationals', +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/conditionset.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/conditionset.py new file mode 100644 index 0000000000000000000000000000000000000000..e847e60ce97d7e9922ce907042ace941838b0ab1 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/conditionset.py @@ -0,0 +1,246 @@ +from sympy.core.singleton import S +from sympy.core.basic import Basic +from sympy.core.containers import Tuple +from sympy.core.function import Lambda, BadSignatureError +from sympy.core.logic import fuzzy_bool +from sympy.core.relational import Eq +from sympy.core.symbol import Dummy +from sympy.core.sympify import _sympify +from sympy.logic.boolalg import And, as_Boolean +from sympy.utilities.iterables import sift, flatten, has_dups +from sympy.utilities.exceptions import sympy_deprecation_warning +from .contains import Contains +from .sets import Set, Union, FiniteSet, SetKind + + +adummy = Dummy('conditionset') + + +class ConditionSet(Set): + r""" + Set of elements which satisfies a given condition. + + .. math:: \{x \mid \textrm{condition}(x) = \texttt{True}, x \in S\} + + Examples + ======== + + >>> from sympy import Symbol, S, ConditionSet, pi, Eq, sin, Interval + >>> from sympy.abc import x, y, z + + >>> sin_sols = ConditionSet(x, Eq(sin(x), 0), Interval(0, 2*pi)) + >>> 2*pi in sin_sols + True + >>> pi/2 in sin_sols + False + >>> 3*pi in sin_sols + False + >>> 5 in ConditionSet(x, x**2 > 4, S.Reals) + True + + If the value is not in the base set, the result is false: + + >>> 5 in ConditionSet(x, x**2 > 4, Interval(2, 4)) + False + + Notes + ===== + + Symbols with assumptions should be avoided or else the + condition may evaluate without consideration of the set: + + >>> n = Symbol('n', negative=True) + >>> cond = (n > 0); cond + False + >>> ConditionSet(n, cond, S.Integers) + EmptySet + + Only free symbols can be changed by using `subs`: + + >>> c = ConditionSet(x, x < 1, {x, z}) + >>> c.subs(x, y) + ConditionSet(x, x < 1, {y, z}) + + To check if ``pi`` is in ``c`` use: + + >>> pi in c + False + + If no base set is specified, the universal set is implied: + + >>> ConditionSet(x, x < 1).base_set + UniversalSet + + Only symbols or symbol-like expressions can be used: + + >>> ConditionSet(x + 1, x + 1 < 1, S.Integers) + Traceback (most recent call last): + ... + ValueError: non-symbol dummy not recognized in condition + + When the base set is a ConditionSet, the symbols will be + unified if possible with preference for the outermost symbols: + + >>> ConditionSet(x, x < y, ConditionSet(z, z + y < 2, S.Integers)) + ConditionSet(x, (x < y) & (x + y < 2), Integers) + + """ + def __new__(cls, sym, condition, base_set=S.UniversalSet): + sym = _sympify(sym) + flat = flatten([sym]) + if has_dups(flat): + raise BadSignatureError("Duplicate symbols detected") + base_set = _sympify(base_set) + if not isinstance(base_set, Set): + raise TypeError( + 'base set should be a Set object, not %s' % base_set) + condition = _sympify(condition) + + if isinstance(condition, FiniteSet): + condition_orig = condition + temp = (Eq(lhs, 0) for lhs in condition) + condition = And(*temp) + sympy_deprecation_warning( + f""" +Using a set for the condition in ConditionSet is deprecated. Use a boolean +instead. + +In this case, replace + + {condition_orig} + +with + + {condition} +""", + deprecated_since_version='1.5', + active_deprecations_target="deprecated-conditionset-set", + ) + + condition = as_Boolean(condition) + + if condition is S.true: + return base_set + + if condition is S.false: + return S.EmptySet + + if base_set is S.EmptySet: + return S.EmptySet + + # no simple answers, so now check syms + for i in flat: + if not getattr(i, '_diff_wrt', False): + raise ValueError('`%s` is not symbol-like' % i) + + if base_set.contains(sym) is S.false: + raise TypeError('sym `%s` is not in base_set `%s`' % (sym, base_set)) + + know = None + if isinstance(base_set, FiniteSet): + sifted = sift( + base_set, lambda _: fuzzy_bool(condition.subs(sym, _))) + if sifted[None]: + know = FiniteSet(*sifted[True]) + base_set = FiniteSet(*sifted[None]) + else: + return FiniteSet(*sifted[True]) + + if isinstance(base_set, cls): + s, c, b = base_set.args + def sig(s): + return cls(s, Eq(adummy, 0)).as_dummy().sym + sa, sb = map(sig, (sym, s)) + if sa != sb: + raise BadSignatureError('sym does not match sym of base set') + reps = dict(zip(flatten([sym]), flatten([s]))) + if s == sym: + condition = And(condition, c) + base_set = b + elif not c.free_symbols & sym.free_symbols: + reps = {v: k for k, v in reps.items()} + condition = And(condition, c.xreplace(reps)) + base_set = b + elif not condition.free_symbols & s.free_symbols: + sym = sym.xreplace(reps) + condition = And(condition.xreplace(reps), c) + base_set = b + + # flatten ConditionSet(Contains(ConditionSet())) expressions + if isinstance(condition, Contains) and (sym == condition.args[0]): + if isinstance(condition.args[1], Set): + return condition.args[1].intersect(base_set) + + rv = Basic.__new__(cls, sym, condition, base_set) + return rv if know is None else Union(know, rv) + + sym = property(lambda self: self.args[0]) + condition = property(lambda self: self.args[1]) + base_set = property(lambda self: self.args[2]) + + @property + def free_symbols(self): + cond_syms = self.condition.free_symbols - self.sym.free_symbols + return cond_syms | self.base_set.free_symbols + + @property + def bound_symbols(self): + return flatten([self.sym]) + + def _contains(self, other): + def ok_sig(a, b): + tuples = [isinstance(i, Tuple) for i in (a, b)] + c = tuples.count(True) + if c == 1: + return False + if c == 0: + return True + return len(a) == len(b) and all( + ok_sig(i, j) for i, j in zip(a, b)) + if not ok_sig(self.sym, other): + return S.false + + # try doing base_cond first and return + # False immediately if it is False + base_cond = Contains(other, self.base_set) + if base_cond is S.false: + return S.false + + # Substitute other into condition. This could raise e.g. for + # ConditionSet(x, 1/x >= 0, Reals).contains(0) + lamda = Lambda((self.sym,), self.condition) + try: + lambda_cond = lamda(other) + except TypeError: + return None + else: + return And(base_cond, lambda_cond) + + def as_relational(self, other): + f = Lambda(self.sym, self.condition) + if isinstance(self.sym, Tuple): + f = f(*other) + else: + f = f(other) + return And(f, self.base_set.contains(other)) + + def _eval_subs(self, old, new): + sym, cond, base = self.args + dsym = sym.subs(old, adummy) + insym = dsym.has(adummy) + # prioritize changing a symbol in the base + newbase = base.subs(old, new) + if newbase != base: + if not insym: + cond = cond.subs(old, new) + return self.func(sym, cond, newbase) + if insym: + pass # no change of bound symbols via subs + elif getattr(new, '_diff_wrt', False): + cond = cond.subs(old, new) + else: + pass # let error about the symbol raise from __new__ + return self.func(sym, cond, base) + + def _kind(self): + return SetKind(self.sym.kind) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/contains.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/contains.py new file mode 100644 index 0000000000000000000000000000000000000000..403d4875279d718724a898efa5cba41bc7bed6ea --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/contains.py @@ -0,0 +1,63 @@ +from sympy.core import S +from sympy.core.sympify import sympify +from sympy.core.relational import Eq, Ne +from sympy.core.parameters import global_parameters +from sympy.logic.boolalg import Boolean +from sympy.utilities.misc import func_name +from .sets import Set + + +class Contains(Boolean): + """ + Asserts that x is an element of the set S. + + Examples + ======== + + >>> from sympy import Symbol, Integer, S, Contains + >>> Contains(Integer(2), S.Integers) + True + >>> Contains(Integer(-2), S.Naturals) + False + >>> i = Symbol('i', integer=True) + >>> Contains(i, S.Naturals) + Contains(i, Naturals) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Element_%28mathematics%29 + """ + def __new__(cls, x, s, evaluate=None): + x = sympify(x) + s = sympify(s) + + if evaluate is None: + evaluate = global_parameters.evaluate + + if not isinstance(s, Set): + raise TypeError('expecting Set, not %s' % func_name(s)) + + if evaluate: + # _contains can return symbolic booleans that would be returned by + # s.contains(x) but here for Contains(x, s) we only evaluate to + # true, false or return the unevaluated Contains. + result = s._contains(x) + + if isinstance(result, Boolean): + if result in (S.true, S.false): + return result + elif result is not None: + raise TypeError("_contains() should return Boolean or None") + + return super().__new__(cls, x, s) + + @property + def binary_symbols(self): + return set().union(*[i.binary_symbols + for i in self.args[1].args + if i.is_Boolean or i.is_Symbol or + isinstance(i, (Eq, Ne))]) + + def as_set(self): + return self.args[1] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/fancysets.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/fancysets.py new file mode 100644 index 0000000000000000000000000000000000000000..e0e24a2a864222d16ba1a697558b5211127fb2ad --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/fancysets.py @@ -0,0 +1,1523 @@ +from functools import reduce +from itertools import product + +from sympy.core.basic import Basic +from sympy.core.containers import Tuple +from sympy.core.expr import Expr +from sympy.core.function import Lambda +from sympy.core.logic import fuzzy_not, fuzzy_or, fuzzy_and +from sympy.core.mod import Mod +from sympy.core.intfunc import igcd +from sympy.core.numbers import oo, Rational, Integer +from sympy.core.relational import Eq, is_eq +from sympy.core.kind import NumberKind +from sympy.core.singleton import Singleton, S +from sympy.core.symbol import Dummy, symbols, Symbol +from sympy.core.sympify import _sympify, sympify, _sympy_converter +from sympy.functions.elementary.integers import ceiling, floor +from sympy.functions.elementary.trigonometric import sin, cos +from sympy.logic.boolalg import And, Or +from .sets import tfn, Set, Interval, Union, FiniteSet, ProductSet, SetKind +from sympy.utilities.misc import filldedent + + +class Rationals(Set, metaclass=Singleton): + """ + Represents the rational numbers. This set is also available as + the singleton ``S.Rationals``. + + Examples + ======== + + >>> from sympy import S + >>> S.Half in S.Rationals + True + >>> iterable = iter(S.Rationals) + >>> [next(iterable) for i in range(12)] + [0, 1, -1, 1/2, 2, -1/2, -2, 1/3, 3, -1/3, -3, 2/3] + """ + + is_iterable = True + _inf = S.NegativeInfinity + _sup = S.Infinity + is_empty = False + is_finite_set = False + + def _contains(self, other): + if not isinstance(other, Expr): + return S.false + return tfn[other.is_rational] + + def __iter__(self): + yield S.Zero + yield S.One + yield S.NegativeOne + d = 2 + while True: + for n in range(d): + if igcd(n, d) == 1: + yield Rational(n, d) + yield Rational(d, n) + yield Rational(-n, d) + yield Rational(-d, n) + d += 1 + + @property + def _boundary(self): + return S.Reals + + def _kind(self): + return SetKind(NumberKind) + + +class Naturals(Set, metaclass=Singleton): + """ + Represents the natural numbers (or counting numbers) which are all + positive integers starting from 1. This set is also available as + the singleton ``S.Naturals``. + + Examples + ======== + + >>> from sympy import S, Interval, pprint + >>> 5 in S.Naturals + True + >>> iterable = iter(S.Naturals) + >>> next(iterable) + 1 + >>> next(iterable) + 2 + >>> next(iterable) + 3 + >>> pprint(S.Naturals.intersect(Interval(0, 10))) + {1, 2, ..., 10} + + See Also + ======== + + Naturals0 : non-negative integers (i.e. includes 0, too) + Integers : also includes negative integers + """ + + is_iterable = True + _inf: Integer = S.One + _sup = S.Infinity + is_empty = False + is_finite_set = False + + def _contains(self, other): + if not isinstance(other, Expr): + return S.false + elif other.is_positive and other.is_integer: + return S.true + elif other.is_integer is False or other.is_positive is False: + return S.false + + def _eval_is_subset(self, other): + return Range(1, oo).is_subset(other) + + def _eval_is_superset(self, other): + return Range(1, oo).is_superset(other) + + def __iter__(self): + i = self._inf + while True: + yield i + i = i + 1 + + @property + def _boundary(self): + return self + + def as_relational(self, x): + return And(Eq(floor(x), x), x >= self.inf, x < oo) + + def _kind(self): + return SetKind(NumberKind) + + +class Naturals0(Naturals): + """Represents the whole numbers which are all the non-negative integers, + inclusive of zero. + + See Also + ======== + + Naturals : positive integers; does not include 0 + Integers : also includes the negative integers + """ + _inf = S.Zero + + def _contains(self, other): + if not isinstance(other, Expr): + return S.false + elif other.is_integer and other.is_nonnegative: + return S.true + elif other.is_integer is False or other.is_nonnegative is False: + return S.false + + def _eval_is_subset(self, other): + return Range(oo).is_subset(other) + + def _eval_is_superset(self, other): + return Range(oo).is_superset(other) + + +class Integers(Set, metaclass=Singleton): + """ + Represents all integers: positive, negative and zero. This set is also + available as the singleton ``S.Integers``. + + Examples + ======== + + >>> from sympy import S, Interval, pprint + >>> 5 in S.Naturals + True + >>> iterable = iter(S.Integers) + >>> next(iterable) + 0 + >>> next(iterable) + 1 + >>> next(iterable) + -1 + >>> next(iterable) + 2 + + >>> pprint(S.Integers.intersect(Interval(-4, 4))) + {-4, -3, ..., 4} + + See Also + ======== + + Naturals0 : non-negative integers + Integers : positive and negative integers and zero + """ + + is_iterable = True + is_empty = False + is_finite_set = False + + def _contains(self, other): + if not isinstance(other, Expr): + return S.false + return tfn[other.is_integer] + + def __iter__(self): + yield S.Zero + i = S.One + while True: + yield i + yield -i + i = i + 1 + + @property + def _inf(self): + return S.NegativeInfinity + + @property + def _sup(self): + return S.Infinity + + @property + def _boundary(self): + return self + + def _kind(self): + return SetKind(NumberKind) + + def as_relational(self, x): + return And(Eq(floor(x), x), -oo < x, x < oo) + + def _eval_is_subset(self, other): + return Range(-oo, oo).is_subset(other) + + def _eval_is_superset(self, other): + return Range(-oo, oo).is_superset(other) + + +class Reals(Interval, metaclass=Singleton): + """ + Represents all real numbers + from negative infinity to positive infinity, + including all integer, rational and irrational numbers. + This set is also available as the singleton ``S.Reals``. + + + Examples + ======== + + >>> from sympy import S, Rational, pi, I + >>> 5 in S.Reals + True + >>> Rational(-1, 2) in S.Reals + True + >>> pi in S.Reals + True + >>> 3*I in S.Reals + False + >>> S.Reals.contains(pi) + True + + + See Also + ======== + + ComplexRegion + """ + @property + def start(self): + return S.NegativeInfinity + + @property + def end(self): + return S.Infinity + + @property + def left_open(self): + return True + + @property + def right_open(self): + return True + + def __eq__(self, other): + return other == Interval(S.NegativeInfinity, S.Infinity) + + def __hash__(self): + return hash(Interval(S.NegativeInfinity, S.Infinity)) + + +class ImageSet(Set): + """ + Image of a set under a mathematical function. The transformation + must be given as a Lambda function which has as many arguments + as the elements of the set upon which it operates, e.g. 1 argument + when acting on the set of integers or 2 arguments when acting on + a complex region. + + This function is not normally called directly, but is called + from ``imageset``. + + + Examples + ======== + + >>> from sympy import Symbol, S, pi, Dummy, Lambda + >>> from sympy import FiniteSet, ImageSet, Interval + + >>> x = Symbol('x') + >>> N = S.Naturals + >>> squares = ImageSet(Lambda(x, x**2), N) # {x**2 for x in N} + >>> 4 in squares + True + >>> 5 in squares + False + + >>> FiniteSet(0, 1, 2, 3, 4, 5, 6, 7, 9, 10).intersect(squares) + {1, 4, 9} + + >>> square_iterable = iter(squares) + >>> for i in range(4): + ... next(square_iterable) + 1 + 4 + 9 + 16 + + If you want to get value for `x` = 2, 1/2 etc. (Please check whether the + `x` value is in ``base_set`` or not before passing it as args) + + >>> squares.lamda(2) + 4 + >>> squares.lamda(S(1)/2) + 1/4 + + >>> n = Dummy('n') + >>> solutions = ImageSet(Lambda(n, n*pi), S.Integers) # solutions of sin(x) = 0 + >>> dom = Interval(-1, 1) + >>> dom.intersect(solutions) + {0} + + See Also + ======== + + sympy.sets.sets.imageset + """ + def __new__(cls, flambda, *sets): + if not isinstance(flambda, Lambda): + raise ValueError('First argument must be a Lambda') + + signature = flambda.signature + + if len(signature) != len(sets): + raise ValueError('Incompatible signature') + + sets = [_sympify(s) for s in sets] + + if not all(isinstance(s, Set) for s in sets): + raise TypeError("Set arguments to ImageSet should of type Set") + + if not all(cls._check_sig(sg, st) for sg, st in zip(signature, sets)): + raise ValueError("Signature %s does not match sets %s" % (signature, sets)) + + if flambda is S.IdentityFunction and len(sets) == 1: + return sets[0] + + if not set(flambda.variables) & flambda.expr.free_symbols: + is_empty = fuzzy_or(s.is_empty for s in sets) + if is_empty == True: + return S.EmptySet + elif is_empty == False: + return FiniteSet(flambda.expr) + + return Basic.__new__(cls, flambda, *sets) + + lamda = property(lambda self: self.args[0]) + base_sets = property(lambda self: self.args[1:]) + + @property + def base_set(self): + # XXX: Maybe deprecate this? It is poorly defined in handling + # the multivariate case... + sets = self.base_sets + if len(sets) == 1: + return sets[0] + else: + return ProductSet(*sets).flatten() + + @property + def base_pset(self): + return ProductSet(*self.base_sets) + + @classmethod + def _check_sig(cls, sig_i, set_i): + if sig_i.is_symbol: + return True + elif isinstance(set_i, ProductSet): + sets = set_i.sets + if len(sig_i) != len(sets): + return False + # Recurse through the signature for nested tuples: + return all(cls._check_sig(ts, ps) for ts, ps in zip(sig_i, sets)) + else: + # XXX: Need a better way of checking whether a set is a set of + # Tuples or not. For example a FiniteSet can contain Tuples + # but so can an ImageSet or a ConditionSet. Others like + # Integers, Reals etc can not contain Tuples. We could just + # list the possibilities here... Current code for e.g. + # _contains probably only works for ProductSet. + return True # Give the benefit of the doubt + + def __iter__(self): + already_seen = set() + for i in self.base_pset: + val = self.lamda(*i) + if val in already_seen: + continue + else: + already_seen.add(val) + yield val + + def _is_multivariate(self): + return len(self.lamda.variables) > 1 + + def _contains(self, other): + from sympy.solvers.solveset import _solveset_multi + + def get_symsetmap(signature, base_sets): + '''Attempt to get a map of symbols to base_sets''' + queue = list(zip(signature, base_sets)) + symsetmap = {} + for sig, base_set in queue: + if sig.is_symbol: + symsetmap[sig] = base_set + elif base_set.is_ProductSet: + sets = base_set.sets + if len(sig) != len(sets): + raise ValueError("Incompatible signature") + # Recurse + queue.extend(zip(sig, sets)) + else: + # If we get here then we have something like sig = (x, y) and + # base_set = {(1, 2), (3, 4)}. For now we give up. + return None + + return symsetmap + + def get_equations(expr, candidate): + '''Find the equations relating symbols in expr and candidate.''' + queue = [(expr, candidate)] + for e, c in queue: + if not isinstance(e, Tuple): + yield Eq(e, c) + elif not isinstance(c, Tuple) or len(e) != len(c): + yield False + return + else: + queue.extend(zip(e, c)) + + # Get the basic objects together: + other = _sympify(other) + expr = self.lamda.expr + sig = self.lamda.signature + variables = self.lamda.variables + base_sets = self.base_sets + + # Use dummy symbols for ImageSet parameters so they don't match + # anything in other + rep = {v: Dummy(v.name) for v in variables} + variables = [v.subs(rep) for v in variables] + sig = sig.subs(rep) + expr = expr.subs(rep) + + # Map the parts of other to those in the Lambda expr + equations = [] + for eq in get_equations(expr, other): + # Unsatisfiable equation? + if eq is False: + return S.false + equations.append(eq) + + # Map the symbols in the signature to the corresponding domains + symsetmap = get_symsetmap(sig, base_sets) + if symsetmap is None: + # Can't factor the base sets to a ProductSet + return None + + # Which of the variables in the Lambda signature need to be solved for? + symss = (eq.free_symbols for eq in equations) + variables = set(variables) & reduce(set.union, symss, set()) + + # Use internal multivariate solveset + variables = tuple(variables) + base_sets = [symsetmap[v] for v in variables] + solnset = _solveset_multi(equations, variables, base_sets) + if solnset is None: + return None + return tfn[fuzzy_not(solnset.is_empty)] + + @property + def is_iterable(self): + return all(s.is_iterable for s in self.base_sets) + + def doit(self, **hints): + from sympy.sets.setexpr import SetExpr + f = self.lamda + sig = f.signature + if len(sig) == 1 and sig[0].is_symbol and isinstance(f.expr, Expr): + base_set = self.base_sets[0] + return SetExpr(base_set)._eval_func(f).set + if all(s.is_FiniteSet for s in self.base_sets): + return FiniteSet(*(f(*a) for a in product(*self.base_sets))) + return self + + def _kind(self): + return SetKind(self.lamda.expr.kind) + + +class Range(Set): + """ + Represents a range of integers. Can be called as ``Range(stop)``, + ``Range(start, stop)``, or ``Range(start, stop, step)``; when ``step`` is + not given it defaults to 1. + + ``Range(stop)`` is the same as ``Range(0, stop, 1)`` and the stop value + (just as for Python ranges) is not included in the Range values. + + >>> from sympy import Range + >>> list(Range(3)) + [0, 1, 2] + + The step can also be negative: + + >>> list(Range(10, 0, -2)) + [10, 8, 6, 4, 2] + + The stop value is made canonical so equivalent ranges always + have the same args: + + >>> Range(0, 10, 3) + Range(0, 12, 3) + + Infinite ranges are allowed. ``oo`` and ``-oo`` are never included in the + set (``Range`` is always a subset of ``Integers``). If the starting point + is infinite, then the final value is ``stop - step``. To iterate such a + range, it needs to be reversed: + + >>> from sympy import oo + >>> r = Range(-oo, 1) + >>> r[-1] + 0 + >>> next(iter(r)) + Traceback (most recent call last): + ... + TypeError: Cannot iterate over Range with infinite start + >>> next(iter(r.reversed)) + 0 + + Although ``Range`` is a :class:`Set` (and supports the normal set + operations) it maintains the order of the elements and can + be used in contexts where ``range`` would be used. + + >>> from sympy import Interval + >>> Range(0, 10, 2).intersect(Interval(3, 7)) + Range(4, 8, 2) + >>> list(_) + [4, 6] + + Although slicing of a Range will always return a Range -- possibly + empty -- an empty set will be returned from any intersection that + is empty: + + >>> Range(3)[:0] + Range(0, 0, 1) + >>> Range(3).intersect(Interval(4, oo)) + EmptySet + >>> Range(3).intersect(Range(4, oo)) + EmptySet + + Range will accept symbolic arguments but has very limited support + for doing anything other than displaying the Range: + + >>> from sympy import Symbol, pprint + >>> from sympy.abc import i, j, k + >>> Range(i, j, k).start + i + >>> Range(i, j, k).inf + Traceback (most recent call last): + ... + ValueError: invalid method for symbolic range + + Better success will be had when using integer symbols: + + >>> n = Symbol('n', integer=True) + >>> r = Range(n, n + 20, 3) + >>> r.inf + n + >>> pprint(r) + {n, n + 3, ..., n + 18} + """ + + def __new__(cls, *args): + if len(args) == 1: + if isinstance(args[0], range): + raise TypeError( + 'use sympify(%s) to convert range to Range' % args[0]) + + # expand range + slc = slice(*args) + + if slc.step == 0: + raise ValueError("step cannot be 0") + + start, stop, step = slc.start or 0, slc.stop, slc.step or 1 + try: + ok = [] + for w in (start, stop, step): + w = sympify(w) + if w in [S.NegativeInfinity, S.Infinity] or ( + w.has(Symbol) and w.is_integer != False): + ok.append(w) + elif not w.is_Integer: + if w.is_infinite: + raise ValueError('infinite symbols not allowed') + raise ValueError + else: + ok.append(w) + except ValueError: + raise ValueError(filldedent(''' + Finite arguments to Range must be integers; `imageset` can define + other cases, e.g. use `imageset(i, i/10, Range(3))` to give + [0, 1/10, 1/5].''')) + start, stop, step = ok + + null = False + if any(i.has(Symbol) for i in (start, stop, step)): + dif = stop - start + n = dif/step + if n.is_Rational: + if dif == 0: + null = True + else: # (x, x + 5, 2) or (x, 3*x, x) + n = floor(n) + end = start + n*step + if dif.is_Rational: # (x, x + 5, 2) + if (end - stop).is_negative: + end += step + else: # (x, 3*x, x) + if (end/stop - 1).is_negative: + end += step + elif n.is_extended_negative: + null = True + else: + end = stop # other methods like sup and reversed must fail + elif start.is_infinite: + span = step*(stop - start) + if span is S.NaN or span <= 0: + null = True + elif step.is_Integer and stop.is_infinite and abs(step) != 1: + raise ValueError(filldedent(''' + Step size must be %s in this case.''' % (1 if step > 0 else -1))) + else: + end = stop + else: + oostep = step.is_infinite + if oostep: + step = S.One if step > 0 else S.NegativeOne + n = ceiling((stop - start)/step) + if n <= 0: + null = True + elif oostep: + step = S.One # make it canonical + end = start + step + else: + end = start + n*step + if null: + start = end = S.Zero + step = S.One + return Basic.__new__(cls, start, end, step) + + start = property(lambda self: self.args[0]) + stop = property(lambda self: self.args[1]) + step = property(lambda self: self.args[2]) + + @property + def reversed(self): + """Return an equivalent Range in the opposite order. + + Examples + ======== + + >>> from sympy import Range + >>> Range(10).reversed + Range(9, -1, -1) + """ + if self.has(Symbol): + n = (self.stop - self.start)/self.step + if not n.is_extended_positive or not all( + i.is_integer or i.is_infinite for i in self.args): + raise ValueError('invalid method for symbolic range') + if self.start == self.stop: + return self + return self.func( + self.stop - self.step, self.start - self.step, -self.step) + + def _kind(self): + return SetKind(NumberKind) + + def _contains(self, other): + if self.start == self.stop: + return S.false + if other.is_infinite: + return S.false + if not other.is_integer: + return tfn[other.is_integer] + if self.has(Symbol): + n = (self.stop - self.start)/self.step + if not n.is_extended_positive or not all( + i.is_integer or i.is_infinite for i in self.args): + return + else: + n = self.size + if self.start.is_finite: + ref = self.start + elif self.stop.is_finite: + ref = self.stop + else: # both infinite; step is +/- 1 (enforced by __new__) + return S.true + if n == 1: + return Eq(other, self[0]) + res = (ref - other) % self.step + if res == S.Zero: + if self.has(Symbol): + d = Dummy('i') + return self.as_relational(d).subs(d, other) + return And(other >= self.inf, other <= self.sup) + elif res.is_Integer: # off sequence + return S.false + else: # symbolic/unsimplified residue modulo step + return None + + def __iter__(self): + n = self.size # validate + if not (n.has(S.Infinity) or n.has(S.NegativeInfinity) or n.is_Integer): + raise TypeError("Cannot iterate over symbolic Range") + if self.start in [S.NegativeInfinity, S.Infinity]: + raise TypeError("Cannot iterate over Range with infinite start") + elif self.start != self.stop: + i = self.start + if n.is_infinite: + while True: + yield i + i += self.step + else: + for _ in range(n): + yield i + i += self.step + + @property + def is_iterable(self): + # Check that size can be determined, used by __iter__ + dif = self.stop - self.start + n = dif/self.step + if not (n.has(S.Infinity) or n.has(S.NegativeInfinity) or n.is_Integer): + return False + if self.start in [S.NegativeInfinity, S.Infinity]: + return False + if not (n.is_extended_nonnegative and all(i.is_integer for i in self.args)): + return False + return True + + def __len__(self): + rv = self.size + if rv is S.Infinity: + raise ValueError('Use .size to get the length of an infinite Range') + return int(rv) + + @property + def size(self): + if self.start == self.stop: + return S.Zero + dif = self.stop - self.start + n = dif/self.step + if n.is_infinite: + return S.Infinity + if n.is_extended_nonnegative and all(i.is_integer for i in self.args): + return abs(floor(n)) + raise ValueError('Invalid method for symbolic Range') + + @property + def is_finite_set(self): + if self.start.is_integer and self.stop.is_integer: + return True + return self.size.is_finite + + @property + def is_empty(self): + try: + return self.size.is_zero + except ValueError: + return None + + def __bool__(self): + # this only distinguishes between definite null range + # and non-null/unknown null; getting True doesn't mean + # that it actually is not null + b = is_eq(self.start, self.stop) + if b is None: + raise ValueError('cannot tell if Range is null or not') + return not bool(b) + + def __getitem__(self, i): + ooslice = "cannot slice from the end with an infinite value" + zerostep = "slice step cannot be zero" + infinite = "slicing not possible on range with infinite start" + # if we had to take every other element in the following + # oo, ..., 6, 4, 2, 0 + # we might get oo, ..., 4, 0 or oo, ..., 6, 2 + ambiguous = "cannot unambiguously re-stride from the end " + \ + "with an infinite value" + if isinstance(i, slice): + if self.size.is_finite: # validates, too + if self.start == self.stop: + return Range(0) + start, stop, step = i.indices(self.size) + n = ceiling((stop - start)/step) + if n <= 0: + return Range(0) + canonical_stop = start + n*step + end = canonical_stop - step + ss = step*self.step + return Range(self[start], self[end] + ss, ss) + else: # infinite Range + start = i.start + stop = i.stop + if i.step == 0: + raise ValueError(zerostep) + step = i.step or 1 + ss = step*self.step + #--------------------- + # handle infinite Range + # i.e. Range(-oo, oo) or Range(oo, -oo, -1) + # -------------------- + if self.start.is_infinite and self.stop.is_infinite: + raise ValueError(infinite) + #--------------------- + # handle infinite on right + # e.g. Range(0, oo) or Range(0, -oo, -1) + # -------------------- + if self.stop.is_infinite: + # start and stop are not interdependent -- + # they only depend on step --so we use the + # equivalent reversed values + return self.reversed[ + stop if stop is None else -stop + 1: + start if start is None else -start: + step].reversed + #--------------------- + # handle infinite on the left + # e.g. Range(oo, 0, -1) or Range(-oo, 0) + # -------------------- + # consider combinations of + # start/stop {== None, < 0, == 0, > 0} and + # step {< 0, > 0} + if start is None: + if stop is None: + if step < 0: + return Range(self[-1], self.start, ss) + elif step > 1: + raise ValueError(ambiguous) + else: # == 1 + return self + elif stop < 0: + if step < 0: + return Range(self[-1], self[stop], ss) + else: # > 0 + return Range(self.start, self[stop], ss) + elif stop == 0: + if step > 0: + return Range(0) + else: # < 0 + raise ValueError(ooslice) + elif stop == 1: + if step > 0: + raise ValueError(ooslice) # infinite singleton + else: # < 0 + raise ValueError(ooslice) + else: # > 1 + raise ValueError(ooslice) + elif start < 0: + if stop is None: + if step < 0: + return Range(self[start], self.start, ss) + else: # > 0 + return Range(self[start], self.stop, ss) + elif stop < 0: + return Range(self[start], self[stop], ss) + elif stop == 0: + if step < 0: + raise ValueError(ooslice) + else: # > 0 + return Range(0) + elif stop > 0: + raise ValueError(ooslice) + elif start == 0: + if stop is None: + if step < 0: + raise ValueError(ooslice) # infinite singleton + elif step > 1: + raise ValueError(ambiguous) + else: # == 1 + return self + elif stop < 0: + if step > 1: + raise ValueError(ambiguous) + elif step == 1: + return Range(self.start, self[stop], ss) + else: # < 0 + return Range(0) + else: # >= 0 + raise ValueError(ooslice) + elif start > 0: + raise ValueError(ooslice) + else: + if self.start == self.stop: + raise IndexError('Range index out of range') + if not (all(i.is_integer or i.is_infinite + for i in self.args) and ((self.stop - self.start)/ + self.step).is_extended_positive): + raise ValueError('Invalid method for symbolic Range') + if i == 0: + if self.start.is_infinite: + raise ValueError(ooslice) + return self.start + if i == -1: + if self.stop.is_infinite: + raise ValueError(ooslice) + return self.stop - self.step + n = self.size # must be known for any other index + rv = (self.stop if i < 0 else self.start) + i*self.step + if rv.is_infinite: + raise ValueError(ooslice) + val = (rv - self.start)/self.step + rel = fuzzy_or([val.is_infinite, + fuzzy_and([val.is_nonnegative, (n-val).is_nonnegative])]) + if rel: + return rv + if rel is None: + raise ValueError('Invalid method for symbolic Range') + raise IndexError("Range index out of range") + + @property + def _inf(self): + if not self: + return S.EmptySet.inf + if self.has(Symbol): + if all(i.is_integer or i.is_infinite for i in self.args): + dif = self.stop - self.start + if self.step.is_positive and dif.is_positive: + return self.start + elif self.step.is_negative and dif.is_negative: + return self.stop - self.step + raise ValueError('invalid method for symbolic range') + if self.step > 0: + return self.start + else: + return self.stop - self.step + + @property + def _sup(self): + if not self: + return S.EmptySet.sup + if self.has(Symbol): + if all(i.is_integer or i.is_infinite for i in self.args): + dif = self.stop - self.start + if self.step.is_positive and dif.is_positive: + return self.stop - self.step + elif self.step.is_negative and dif.is_negative: + return self.start + raise ValueError('invalid method for symbolic range') + if self.step > 0: + return self.stop - self.step + else: + return self.start + + @property + def _boundary(self): + return self + + def as_relational(self, x): + """Rewrite a Range in terms of equalities and logic operators. """ + if self.start.is_infinite: + assert not self.stop.is_infinite # by instantiation + a = self.reversed.start + else: + a = self.start + step = self.step + in_seq = Eq(Mod(x - a, step), 0) + ints = And(Eq(Mod(a, 1), 0), Eq(Mod(step, 1), 0)) + n = (self.stop - self.start)/self.step + if n == 0: + return S.EmptySet.as_relational(x) + if n == 1: + return And(Eq(x, a), ints) + try: + a, b = self.inf, self.sup + except ValueError: + a = None + if a is not None: + range_cond = And( + x > a if a.is_infinite else x >= a, + x < b if b.is_infinite else x <= b) + else: + a, b = self.start, self.stop - self.step + range_cond = Or( + And(self.step >= 1, x > a if a.is_infinite else x >= a, + x < b if b.is_infinite else x <= b), + And(self.step <= -1, x < a if a.is_infinite else x <= a, + x > b if b.is_infinite else x >= b)) + return And(in_seq, ints, range_cond) + + +_sympy_converter[range] = lambda r: Range(r.start, r.stop, r.step) + +def normalize_theta_set(theta): + r""" + Normalize a Real Set `theta` in the interval `[0, 2\pi)`. It returns + a normalized value of theta in the Set. For Interval, a maximum of + one cycle $[0, 2\pi]$, is returned i.e. for theta equal to $[0, 10\pi]$, + returned normalized value would be $[0, 2\pi)$. As of now intervals + with end points as non-multiples of ``pi`` is not supported. + + Raises + ====== + + NotImplementedError + The algorithms for Normalizing theta Set are not yet + implemented. + ValueError + The input is not valid, i.e. the input is not a real set. + RuntimeError + It is a bug, please report to the github issue tracker. + + Examples + ======== + + >>> from sympy.sets.fancysets import normalize_theta_set + >>> from sympy import Interval, FiniteSet, pi + >>> normalize_theta_set(Interval(9*pi/2, 5*pi)) + Interval(pi/2, pi) + >>> normalize_theta_set(Interval(-3*pi/2, pi/2)) + Interval.Ropen(0, 2*pi) + >>> normalize_theta_set(Interval(-pi/2, pi/2)) + Union(Interval(0, pi/2), Interval.Ropen(3*pi/2, 2*pi)) + >>> normalize_theta_set(Interval(-4*pi, 3*pi)) + Interval.Ropen(0, 2*pi) + >>> normalize_theta_set(Interval(-3*pi/2, -pi/2)) + Interval(pi/2, 3*pi/2) + >>> normalize_theta_set(FiniteSet(0, pi, 3*pi)) + {0, pi} + + """ + from sympy.functions.elementary.trigonometric import _pi_coeff + + if theta.is_Interval: + interval_len = theta.measure + # one complete circle + if interval_len >= 2*S.Pi: + if interval_len == 2*S.Pi and theta.left_open and theta.right_open: + k = _pi_coeff(theta.start) + return Union(Interval(0, k*S.Pi, False, True), + Interval(k*S.Pi, 2*S.Pi, True, True)) + return Interval(0, 2*S.Pi, False, True) + + k_start, k_end = _pi_coeff(theta.start), _pi_coeff(theta.end) + + if k_start is None or k_end is None: + raise NotImplementedError("Normalizing theta without pi as coefficient is " + "not yet implemented") + new_start = k_start*S.Pi + new_end = k_end*S.Pi + + if new_start > new_end: + return Union(Interval(S.Zero, new_end, False, theta.right_open), + Interval(new_start, 2*S.Pi, theta.left_open, True)) + else: + return Interval(new_start, new_end, theta.left_open, theta.right_open) + + elif theta.is_FiniteSet: + new_theta = [] + for element in theta: + k = _pi_coeff(element) + if k is None: + raise NotImplementedError('Normalizing theta without pi as ' + 'coefficient, is not Implemented.') + else: + new_theta.append(k*S.Pi) + return FiniteSet(*new_theta) + + elif theta.is_Union: + return Union(*[normalize_theta_set(interval) for interval in theta.args]) + + elif theta.is_subset(S.Reals): + raise NotImplementedError("Normalizing theta when, it is of type %s is not " + "implemented" % type(theta)) + else: + raise ValueError(" %s is not a real set" % (theta)) + + +class ComplexRegion(Set): + r""" + Represents the Set of all Complex Numbers. It can represent a + region of Complex Plane in both the standard forms Polar and + Rectangular coordinates. + + * Polar Form + Input is in the form of the ProductSet or Union of ProductSets + of the intervals of ``r`` and ``theta``, and use the flag ``polar=True``. + + .. math:: Z = \{z \in \mathbb{C} \mid z = r\times (\cos(\theta) + I\sin(\theta)), r \in [\texttt{r}], \theta \in [\texttt{theta}]\} + + * Rectangular Form + Input is in the form of the ProductSet or Union of ProductSets + of interval of x and y, the real and imaginary parts of the Complex numbers in a plane. + Default input type is in rectangular form. + + .. math:: Z = \{z \in \mathbb{C} \mid z = x + Iy, x \in [\operatorname{re}(z)], y \in [\operatorname{im}(z)]\} + + Examples + ======== + + >>> from sympy import ComplexRegion, Interval, S, I, Union + >>> a = Interval(2, 3) + >>> b = Interval(4, 6) + >>> c1 = ComplexRegion(a*b) # Rectangular Form + >>> c1 + CartesianComplexRegion(ProductSet(Interval(2, 3), Interval(4, 6))) + + * c1 represents the rectangular region in complex plane + surrounded by the coordinates (2, 4), (3, 4), (3, 6) and + (2, 6), of the four vertices. + + >>> c = Interval(1, 8) + >>> c2 = ComplexRegion(Union(a*b, b*c)) + >>> c2 + CartesianComplexRegion(Union(ProductSet(Interval(2, 3), Interval(4, 6)), ProductSet(Interval(4, 6), Interval(1, 8)))) + + * c2 represents the Union of two rectangular regions in complex + plane. One of them surrounded by the coordinates of c1 and + other surrounded by the coordinates (4, 1), (6, 1), (6, 8) and + (4, 8). + + >>> 2.5 + 4.5*I in c1 + True + >>> 2.5 + 6.5*I in c1 + False + + >>> r = Interval(0, 1) + >>> theta = Interval(0, 2*S.Pi) + >>> c2 = ComplexRegion(r*theta, polar=True) # Polar Form + >>> c2 # unit Disk + PolarComplexRegion(ProductSet(Interval(0, 1), Interval.Ropen(0, 2*pi))) + + * c2 represents the region in complex plane inside the + Unit Disk centered at the origin. + + >>> 0.5 + 0.5*I in c2 + True + >>> 1 + 2*I in c2 + False + + >>> unit_disk = ComplexRegion(Interval(0, 1)*Interval(0, 2*S.Pi), polar=True) + >>> upper_half_unit_disk = ComplexRegion(Interval(0, 1)*Interval(0, S.Pi), polar=True) + >>> intersection = unit_disk.intersect(upper_half_unit_disk) + >>> intersection + PolarComplexRegion(ProductSet(Interval(0, 1), Interval(0, pi))) + >>> intersection == upper_half_unit_disk + True + + See Also + ======== + + CartesianComplexRegion + PolarComplexRegion + Complexes + + """ + is_ComplexRegion = True + + def __new__(cls, sets, polar=False): + if polar is False: + return CartesianComplexRegion(sets) + elif polar is True: + return PolarComplexRegion(sets) + else: + raise ValueError("polar should be either True or False") + + @property + def sets(self): + """ + Return raw input sets to the self. + + Examples + ======== + + >>> from sympy import Interval, ComplexRegion, Union + >>> a = Interval(2, 3) + >>> b = Interval(4, 5) + >>> c = Interval(1, 7) + >>> C1 = ComplexRegion(a*b) + >>> C1.sets + ProductSet(Interval(2, 3), Interval(4, 5)) + >>> C2 = ComplexRegion(Union(a*b, b*c)) + >>> C2.sets + Union(ProductSet(Interval(2, 3), Interval(4, 5)), ProductSet(Interval(4, 5), Interval(1, 7))) + + """ + return self.args[0] + + @property + def psets(self): + """ + Return a tuple of sets (ProductSets) input of the self. + + Examples + ======== + + >>> from sympy import Interval, ComplexRegion, Union + >>> a = Interval(2, 3) + >>> b = Interval(4, 5) + >>> c = Interval(1, 7) + >>> C1 = ComplexRegion(a*b) + >>> C1.psets + (ProductSet(Interval(2, 3), Interval(4, 5)),) + >>> C2 = ComplexRegion(Union(a*b, b*c)) + >>> C2.psets + (ProductSet(Interval(2, 3), Interval(4, 5)), ProductSet(Interval(4, 5), Interval(1, 7))) + + """ + if self.sets.is_ProductSet: + psets = () + psets = psets + (self.sets, ) + else: + psets = self.sets.args + return psets + + @property + def a_interval(self): + """ + Return the union of intervals of `x` when, self is in + rectangular form, or the union of intervals of `r` when + self is in polar form. + + Examples + ======== + + >>> from sympy import Interval, ComplexRegion, Union + >>> a = Interval(2, 3) + >>> b = Interval(4, 5) + >>> c = Interval(1, 7) + >>> C1 = ComplexRegion(a*b) + >>> C1.a_interval + Interval(2, 3) + >>> C2 = ComplexRegion(Union(a*b, b*c)) + >>> C2.a_interval + Union(Interval(2, 3), Interval(4, 5)) + + """ + a_interval = [] + for element in self.psets: + a_interval.append(element.args[0]) + + a_interval = Union(*a_interval) + return a_interval + + @property + def b_interval(self): + """ + Return the union of intervals of `y` when, self is in + rectangular form, or the union of intervals of `theta` + when self is in polar form. + + Examples + ======== + + >>> from sympy import Interval, ComplexRegion, Union + >>> a = Interval(2, 3) + >>> b = Interval(4, 5) + >>> c = Interval(1, 7) + >>> C1 = ComplexRegion(a*b) + >>> C1.b_interval + Interval(4, 5) + >>> C2 = ComplexRegion(Union(a*b, b*c)) + >>> C2.b_interval + Interval(1, 7) + + """ + b_interval = [] + for element in self.psets: + b_interval.append(element.args[1]) + + b_interval = Union(*b_interval) + return b_interval + + @property + def _measure(self): + """ + The measure of self.sets. + + Examples + ======== + + >>> from sympy import Interval, ComplexRegion, S + >>> a, b = Interval(2, 5), Interval(4, 8) + >>> c = Interval(0, 2*S.Pi) + >>> c1 = ComplexRegion(a*b) + >>> c1.measure + 12 + >>> c2 = ComplexRegion(a*c, polar=True) + >>> c2.measure + 6*pi + + """ + return self.sets._measure + + def _kind(self): + return self.args[0].kind + + @classmethod + def from_real(cls, sets): + """ + Converts given subset of real numbers to a complex region. + + Examples + ======== + + >>> from sympy import Interval, ComplexRegion + >>> unit = Interval(0,1) + >>> ComplexRegion.from_real(unit) + CartesianComplexRegion(ProductSet(Interval(0, 1), {0})) + + """ + if not sets.is_subset(S.Reals): + raise ValueError("sets must be a subset of the real line") + + return CartesianComplexRegion(sets * FiniteSet(0)) + + def _contains(self, other): + from sympy.functions import arg, Abs + + isTuple = isinstance(other, Tuple) + if isTuple and len(other) != 2: + raise ValueError('expecting Tuple of length 2') + + # If the other is not an Expression, and neither a Tuple + if not isinstance(other, (Expr, Tuple)): + return S.false + + # self in rectangular form + if not self.polar: + re, im = other if isTuple else other.as_real_imag() + return tfn[fuzzy_or(fuzzy_and([ + pset.args[0]._contains(re), + pset.args[1]._contains(im)]) + for pset in self.psets)] + + # self in polar form + elif self.polar: + if other.is_zero: + # ignore undefined complex argument + return tfn[fuzzy_or(pset.args[0]._contains(S.Zero) + for pset in self.psets)] + if isTuple: + r, theta = other + else: + r, theta = Abs(other), arg(other) + if theta.is_real and theta.is_number: + # angles in psets are normalized to [0, 2pi) + theta %= 2*S.Pi + return tfn[fuzzy_or(fuzzy_and([ + pset.args[0]._contains(r), + pset.args[1]._contains(theta)]) + for pset in self.psets)] + + +class CartesianComplexRegion(ComplexRegion): + r""" + Set representing a square region of the complex plane. + + .. math:: Z = \{z \in \mathbb{C} \mid z = x + Iy, x \in [\operatorname{re}(z)], y \in [\operatorname{im}(z)]\} + + Examples + ======== + + >>> from sympy import ComplexRegion, I, Interval + >>> region = ComplexRegion(Interval(1, 3) * Interval(4, 6)) + >>> 2 + 5*I in region + True + >>> 5*I in region + False + + See also + ======== + + ComplexRegion + PolarComplexRegion + Complexes + """ + + polar = False + variables = symbols('x, y', cls=Dummy) + + def __new__(cls, sets): + + if sets == S.Reals*S.Reals: + return S.Complexes + + if all(_a.is_FiniteSet for _a in sets.args) and (len(sets.args) == 2): + + # ** ProductSet of FiniteSets in the Complex Plane. ** + # For Cases like ComplexRegion({2, 4}*{3}), It + # would return {2 + 3*I, 4 + 3*I} + + # FIXME: This should probably be handled with something like: + # return ImageSet(Lambda((x, y), x+I*y), sets).rewrite(FiniteSet) + complex_num = [] + for x in sets.args[0]: + for y in sets.args[1]: + complex_num.append(x + S.ImaginaryUnit*y) + return FiniteSet(*complex_num) + else: + return Set.__new__(cls, sets) + + @property + def expr(self): + x, y = self.variables + return x + S.ImaginaryUnit*y + + +class PolarComplexRegion(ComplexRegion): + r""" + Set representing a polar region of the complex plane. + + .. math:: Z = \{z \in \mathbb{C} \mid z = r\times (\cos(\theta) + I\sin(\theta)), r \in [\texttt{r}], \theta \in [\texttt{theta}]\} + + Examples + ======== + + >>> from sympy import ComplexRegion, Interval, oo, pi, I + >>> rset = Interval(0, oo) + >>> thetaset = Interval(0, pi) + >>> upper_half_plane = ComplexRegion(rset * thetaset, polar=True) + >>> 1 + I in upper_half_plane + True + >>> 1 - I in upper_half_plane + False + + See also + ======== + + ComplexRegion + CartesianComplexRegion + Complexes + + """ + + polar = True + variables = symbols('r, theta', cls=Dummy) + + def __new__(cls, sets): + + new_sets = [] + # sets is Union of ProductSets + if not sets.is_ProductSet: + for k in sets.args: + new_sets.append(k) + # sets is ProductSets + else: + new_sets.append(sets) + # Normalize input theta + for k, v in enumerate(new_sets): + new_sets[k] = ProductSet(v.args[0], + normalize_theta_set(v.args[1])) + sets = Union(*new_sets) + return Set.__new__(cls, sets) + + @property + def expr(self): + r, theta = self.variables + return r*(cos(theta) + S.ImaginaryUnit*sin(theta)) + + +class Complexes(CartesianComplexRegion, metaclass=Singleton): + """ + The :class:`Set` of all complex numbers + + Examples + ======== + + >>> from sympy import S, I + >>> S.Complexes + Complexes + >>> 1 + I in S.Complexes + True + + See also + ======== + + Reals + ComplexRegion + + """ + + is_empty = False + is_finite_set = False + + # Override property from superclass since Complexes has no args + @property + def sets(self): + return ProductSet(S.Reals, S.Reals) + + def __new__(cls): + return Set.__new__(cls) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/add.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/add.py new file mode 100644 index 0000000000000000000000000000000000000000..8c07b25ed19d21febffd6b23a92b34b787179f44 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/add.py @@ -0,0 +1,79 @@ +from sympy.core.numbers import oo, Infinity, NegativeInfinity +from sympy.core.singleton import S +from sympy.core import Basic, Expr +from sympy.multipledispatch import Dispatcher +from sympy.sets import Interval, FiniteSet + + + +# XXX: The functions in this module are clearly not tested and are broken in a +# number of ways. + +_set_add = Dispatcher('_set_add') +_set_sub = Dispatcher('_set_sub') + + +@_set_add.register(Basic, Basic) +def _(x, y): + return None + + +@_set_add.register(Expr, Expr) +def _(x, y): + return x+y + + +@_set_add.register(Interval, Interval) +def _(x, y): + """ + Additions in interval arithmetic + https://en.wikipedia.org/wiki/Interval_arithmetic + """ + return Interval(x.start + y.start, x.end + y.end, + x.left_open or y.left_open, x.right_open or y.right_open) + + +@_set_add.register(Interval, Infinity) +def _(x, y): + if x.start is S.NegativeInfinity: + return Interval(-oo, oo) + return FiniteSet({S.Infinity}) + +@_set_add.register(Interval, NegativeInfinity) +def _(x, y): + if x.end is S.Infinity: + return Interval(-oo, oo) + return FiniteSet({S.NegativeInfinity}) + + +@_set_sub.register(Basic, Basic) +def _(x, y): + return None + + +@_set_sub.register(Expr, Expr) +def _(x, y): + return x-y + + +@_set_sub.register(Interval, Interval) +def _(x, y): + """ + Subtractions in interval arithmetic + https://en.wikipedia.org/wiki/Interval_arithmetic + """ + return Interval(x.start - y.end, x.end - y.start, + x.left_open or y.right_open, x.right_open or y.left_open) + + +@_set_sub.register(Interval, Infinity) +def _(x, y): + if x.start is S.NegativeInfinity: + return Interval(-oo, oo) + return FiniteSet(-oo) + +@_set_sub.register(Interval, NegativeInfinity) +def _(x, y): + if x.start is S.NegativeInfinity: + return Interval(-oo, oo) + return FiniteSet(-oo) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/comparison.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/comparison.py new file mode 100644 index 0000000000000000000000000000000000000000..b64d1a2a22e15d09f6f10fb4fef730163d468d45 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/comparison.py @@ -0,0 +1,53 @@ +from sympy.core.relational import Eq, is_eq +from sympy.core.basic import Basic +from sympy.core.logic import fuzzy_and, fuzzy_bool +from sympy.logic.boolalg import And +from sympy.multipledispatch import dispatch +from sympy.sets.sets import tfn, ProductSet, Interval, FiniteSet, Set + + +@dispatch(Interval, FiniteSet) # type:ignore +def _eval_is_eq(lhs, rhs): # noqa: F811 + return False + + +@dispatch(FiniteSet, Interval) # type:ignore +def _eval_is_eq(lhs, rhs): # noqa: F811 + return False + + +@dispatch(Interval, Interval) # type:ignore +def _eval_is_eq(lhs, rhs): # noqa: F811 + return And(Eq(lhs.left, rhs.left), + Eq(lhs.right, rhs.right), + lhs.left_open == rhs.left_open, + lhs.right_open == rhs.right_open) + +@dispatch(FiniteSet, FiniteSet) # type:ignore +def _eval_is_eq(lhs, rhs): # noqa: F811 + def all_in_both(): + s_set = set(lhs.args) + o_set = set(rhs.args) + yield fuzzy_and(lhs._contains(e) for e in o_set - s_set) + yield fuzzy_and(rhs._contains(e) for e in s_set - o_set) + + return tfn[fuzzy_and(all_in_both())] + + +@dispatch(ProductSet, ProductSet) # type:ignore +def _eval_is_eq(lhs, rhs): # noqa: F811 + if len(lhs.sets) != len(rhs.sets): + return False + + eqs = (is_eq(x, y) for x, y in zip(lhs.sets, rhs.sets)) + return tfn[fuzzy_and(map(fuzzy_bool, eqs))] + + +@dispatch(Set, Basic) # type:ignore +def _eval_is_eq(lhs, rhs): # noqa: F811 + return False + + +@dispatch(Set, Set) # type:ignore +def _eval_is_eq(lhs, rhs): # noqa: F811 + return tfn[fuzzy_and(a.is_subset(b) for a, b in [(lhs, rhs), (rhs, lhs)])] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/functions.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/functions.py new file mode 100644 index 0000000000000000000000000000000000000000..2529dbfd458451d7d09e91c717b170df77b1d9fe --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/functions.py @@ -0,0 +1,262 @@ +from sympy.core.singleton import S +from sympy.sets.sets import Set +from sympy.calculus.singularities import singularities +from sympy.core import Expr, Add +from sympy.core.function import Lambda, FunctionClass, diff, expand_mul +from sympy.core.numbers import Float, oo +from sympy.core.symbol import Dummy, symbols, Wild +from sympy.functions.elementary.exponential import exp, log +from sympy.functions.elementary.miscellaneous import Min, Max +from sympy.logic.boolalg import true +from sympy.multipledispatch import Dispatcher +from sympy.sets import (imageset, Interval, FiniteSet, Union, ImageSet, + Intersection, Range, Complement) +from sympy.sets.sets import EmptySet, is_function_invertible_in_set +from sympy.sets.fancysets import Integers, Naturals, Reals +from sympy.functions.elementary.exponential import match_real_imag + + +_x, _y = symbols("x y") + +FunctionUnion = (FunctionClass, Lambda) + +_set_function = Dispatcher('_set_function') + + +@_set_function.register(FunctionClass, Set) +def _(f, x): + return None + +@_set_function.register(FunctionUnion, FiniteSet) +def _(f, x): + return FiniteSet(*map(f, x)) + +@_set_function.register(Lambda, Interval) +def _(f, x): + from sympy.solvers.solveset import solveset + from sympy.series import limit + # TODO: handle functions with infinitely many solutions (eg, sin, tan) + # TODO: handle multivariate functions + + expr = f.expr + if len(expr.free_symbols) > 1 or len(f.variables) != 1: + return + var = f.variables[0] + if not var.is_real: + if expr.subs(var, Dummy(real=True)).is_real is False: + return + + if expr.is_Piecewise: + result = S.EmptySet + domain_set = x + for (p_expr, p_cond) in expr.args: + if p_cond is true: + intrvl = domain_set + else: + intrvl = p_cond.as_set() + intrvl = Intersection(domain_set, intrvl) + + if p_expr.is_Number: + image = FiniteSet(p_expr) + else: + image = imageset(Lambda(var, p_expr), intrvl) + result = Union(result, image) + + # remove the part which has been `imaged` + domain_set = Complement(domain_set, intrvl) + if domain_set is S.EmptySet: + break + return result + + if not x.start.is_comparable or not x.end.is_comparable: + return + + try: + from sympy.polys.polyutils import _nsort + sing = list(singularities(expr, var, x)) + if len(sing) > 1: + sing = _nsort(sing) + except NotImplementedError: + return + + if x.left_open: + _start = limit(expr, var, x.start, dir="+") + elif x.start not in sing: + _start = f(x.start) + if x.right_open: + _end = limit(expr, var, x.end, dir="-") + elif x.end not in sing: + _end = f(x.end) + + if len(sing) == 0: + soln_expr = solveset(diff(expr, var), var) + if not (isinstance(soln_expr, FiniteSet) + or soln_expr is S.EmptySet): + return + solns = list(soln_expr) + + extr = [_start, _end] + [f(i) for i in solns + if i.is_real and i in x] + start, end = Min(*extr), Max(*extr) + + left_open, right_open = False, False + if _start <= _end: + # the minimum or maximum value can occur simultaneously + # on both the edge of the interval and in some interior + # point + if start == _start and start not in solns: + left_open = x.left_open + if end == _end and end not in solns: + right_open = x.right_open + else: + if start == _end and start not in solns: + left_open = x.right_open + if end == _start and end not in solns: + right_open = x.left_open + + return Interval(start, end, left_open, right_open) + else: + return imageset(f, Interval(x.start, sing[0], + x.left_open, True)) + \ + Union(*[imageset(f, Interval(sing[i], sing[i + 1], True, True)) + for i in range(0, len(sing) - 1)]) + \ + imageset(f, Interval(sing[-1], x.end, True, x.right_open)) + +@_set_function.register(FunctionClass, Interval) +def _(f, x): + if f == exp: + return Interval(exp(x.start), exp(x.end), x.left_open, x.right_open) + elif f == log: + return Interval(log(x.start), log(x.end), x.left_open, x.right_open) + return ImageSet(Lambda(_x, f(_x)), x) + +@_set_function.register(FunctionUnion, Union) +def _(f, x): + return Union(*(imageset(f, arg) for arg in x.args)) + +@_set_function.register(FunctionUnion, Intersection) +def _(f, x): + # If the function is invertible, intersect the maps of the sets. + if is_function_invertible_in_set(f, x): + return Intersection(*(imageset(f, arg) for arg in x.args)) + else: + return ImageSet(Lambda(_x, f(_x)), x) + +@_set_function.register(FunctionUnion, EmptySet) +def _(f, x): + return x + +@_set_function.register(FunctionUnion, Set) +def _(f, x): + return ImageSet(Lambda(_x, f(_x)), x) + +@_set_function.register(FunctionUnion, Range) +def _(f, self): + if not self: + return S.EmptySet + if not isinstance(f.expr, Expr): + return + if self.size == 1: + return FiniteSet(f(self[0])) + if f is S.IdentityFunction: + return self + + x = f.variables[0] + expr = f.expr + # handle f that is linear in f's variable + if x not in expr.free_symbols or x in expr.diff(x).free_symbols: + return + if self.start.is_finite: + F = f(self.step*x + self.start) # for i in range(len(self)) + else: + F = f(-self.step*x + self[-1]) + F = expand_mul(F) + if F != expr: + return imageset(x, F, Range(self.size)) + +@_set_function.register(FunctionUnion, Integers) +def _(f, self): + expr = f.expr + if not isinstance(expr, Expr): + return + + n = f.variables[0] + if expr == abs(n): + return S.Naturals0 + + # f(x) + c and f(-x) + c cover the same integers + # so choose the form that has the fewest negatives + c = f(0) + fx = f(n) - c + f_x = f(-n) - c + neg_count = lambda e: sum(_.could_extract_minus_sign() + for _ in Add.make_args(e)) + if neg_count(f_x) < neg_count(fx): + expr = f_x + c + + a = Wild('a', exclude=[n]) + b = Wild('b', exclude=[n]) + match = expr.match(a*n + b) + if match and match[a] and ( + not match[a].atoms(Float) and + not match[b].atoms(Float)): + # canonical shift + a, b = match[a], match[b] + if a in [1, -1]: + # drop integer addends in b + nonint = [] + for bi in Add.make_args(b): + if not bi.is_integer: + nonint.append(bi) + b = Add(*nonint) + if b.is_number and a.is_real: + # avoid Mod for complex numbers, #11391 + br, bi = match_real_imag(b) + if br and br.is_comparable and a.is_comparable: + br %= a + b = br + S.ImaginaryUnit*bi + elif b.is_number and a.is_imaginary: + br, bi = match_real_imag(b) + ai = a/S.ImaginaryUnit + if bi and bi.is_comparable and ai.is_comparable: + bi %= ai + b = br + S.ImaginaryUnit*bi + expr = a*n + b + + if expr != f.expr: + return ImageSet(Lambda(n, expr), S.Integers) + + +@_set_function.register(FunctionUnion, Naturals) +def _(f, self): + expr = f.expr + if not isinstance(expr, Expr): + return + + x = f.variables[0] + if not expr.free_symbols - {x}: + if expr == abs(x): + if self is S.Naturals: + return self + return S.Naturals0 + step = expr.coeff(x) + c = expr.subs(x, 0) + if c.is_Integer and step.is_Integer and expr == step*x + c: + if self is S.Naturals: + c += step + if step > 0: + if step == 1: + if c == 0: + return S.Naturals0 + elif c == 1: + return S.Naturals + return Range(c, oo, step) + return Range(c, -oo, step) + + +@_set_function.register(FunctionUnion, Reals) +def _(f, self): + expr = f.expr + if not isinstance(expr, Expr): + return + return _set_function(f, Interval(-oo, oo)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/intersection.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/intersection.py new file mode 100644 index 0000000000000000000000000000000000000000..fcb9309ef3e9d2722ab1bfe664f1d1644f17da5d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/intersection.py @@ -0,0 +1,533 @@ +from sympy.core.basic import _aresame +from sympy.core.function import Lambda, expand_complex +from sympy.core.mul import Mul +from sympy.core.numbers import ilcm, Float +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, symbols) +from sympy.core.sorting import ordered +from sympy.functions.elementary.complexes import sign +from sympy.functions.elementary.integers import floor, ceiling +from sympy.sets.fancysets import ComplexRegion +from sympy.sets.sets import (FiniteSet, Intersection, Interval, Set, Union) +from sympy.multipledispatch import Dispatcher +from sympy.sets.conditionset import ConditionSet +from sympy.sets.fancysets import (Integers, Naturals, Reals, Range, + ImageSet, Rationals) +from sympy.sets.sets import EmptySet, UniversalSet, imageset, ProductSet +from sympy.simplify.radsimp import numer + + +intersection_sets = Dispatcher('intersection_sets') + + +@intersection_sets.register(ConditionSet, ConditionSet) +def _(a, b): + return None + +@intersection_sets.register(ConditionSet, Set) +def _(a, b): + return ConditionSet(a.sym, a.condition, Intersection(a.base_set, b)) + +@intersection_sets.register(Naturals, Integers) +def _(a, b): + return a + +@intersection_sets.register(Naturals, Naturals) +def _(a, b): + return a if a is S.Naturals else b + +@intersection_sets.register(Interval, Naturals) +def _(a, b): + return intersection_sets(b, a) + +@intersection_sets.register(ComplexRegion, Set) +def _(self, other): + if other.is_ComplexRegion: + # self in rectangular form + if (not self.polar) and (not other.polar): + return ComplexRegion(Intersection(self.sets, other.sets)) + + # self in polar form + elif self.polar and other.polar: + r1, theta1 = self.a_interval, self.b_interval + r2, theta2 = other.a_interval, other.b_interval + new_r_interval = Intersection(r1, r2) + new_theta_interval = Intersection(theta1, theta2) + + # 0 and 2*Pi means the same + if ((2*S.Pi in theta1 and S.Zero in theta2) or + (2*S.Pi in theta2 and S.Zero in theta1)): + new_theta_interval = Union(new_theta_interval, + FiniteSet(0)) + return ComplexRegion(new_r_interval*new_theta_interval, + polar=True) + + + if other.is_subset(S.Reals): + new_interval = [] + x = symbols("x", cls=Dummy, real=True) + + # self in rectangular form + if not self.polar: + for element in self.psets: + if S.Zero in element.args[1]: + new_interval.append(element.args[0]) + new_interval = Union(*new_interval) + return Intersection(new_interval, other) + + # self in polar form + elif self.polar: + for element in self.psets: + if S.Zero in element.args[1]: + new_interval.append(element.args[0]) + if S.Pi in element.args[1]: + new_interval.append(ImageSet(Lambda(x, -x), element.args[0])) + if S.Zero in element.args[0]: + new_interval.append(FiniteSet(0)) + new_interval = Union(*new_interval) + return Intersection(new_interval, other) + +@intersection_sets.register(Integers, Reals) +def _(a, b): + return a + +@intersection_sets.register(Range, Interval) +def _(a, b): + # Check that there are no symbolic arguments + if not all(i.is_number for i in a.args + b.args[:2]): + return + + # In case of null Range, return an EmptySet. + if a.size == 0: + return S.EmptySet + + # trim down to self's size, and represent + # as a Range with step 1. + start = ceiling(max(b.inf, a.inf)) + if start not in b: + start += 1 + end = floor(min(b.sup, a.sup)) + if end not in b: + end -= 1 + return intersection_sets(a, Range(start, end + 1)) + +@intersection_sets.register(Range, Naturals) +def _(a, b): + return intersection_sets(a, Interval(b.inf, S.Infinity)) + +@intersection_sets.register(Range, Range) +def _(a, b): + # Check that there are no symbolic range arguments + if not all(all(v.is_number for v in r.args) for r in [a, b]): + return None + + # non-overlap quick exits + if not b: + return S.EmptySet + if not a: + return S.EmptySet + if b.sup < a.inf: + return S.EmptySet + if b.inf > a.sup: + return S.EmptySet + + # work with finite end at the start + r1 = a + if r1.start.is_infinite: + r1 = r1.reversed + r2 = b + if r2.start.is_infinite: + r2 = r2.reversed + + # If both ends are infinite then it means that one Range is just the set + # of all integers (the step must be 1). + if r1.start.is_infinite: + return b + if r2.start.is_infinite: + return a + + from sympy.solvers.diophantine.diophantine import diop_linear + + # this equation represents the values of the Range; + # it's a linear equation + eq = lambda r, i: r.start + i*r.step + + # we want to know when the two equations might + # have integer solutions so we use the diophantine + # solver + va, vb = diop_linear(eq(r1, Dummy('a')) - eq(r2, Dummy('b'))) + + # check for no solution + no_solution = va is None and vb is None + if no_solution: + return S.EmptySet + + # there is a solution + # ------------------- + + # find the coincident point, c + a0 = va.as_coeff_Add()[0] + c = eq(r1, a0) + + # find the first point, if possible, in each range + # since c may not be that point + def _first_finite_point(r1, c): + if c == r1.start: + return c + # st is the signed step we need to take to + # get from c to r1.start + st = sign(r1.start - c)*step + # use Range to calculate the first point: + # we want to get as close as possible to + # r1.start; the Range will not be null since + # it will at least contain c + s1 = Range(c, r1.start + st, st)[-1] + if s1 == r1.start: + pass + else: + # if we didn't hit r1.start then, if the + # sign of st didn't match the sign of r1.step + # we are off by one and s1 is not in r1 + if sign(r1.step) != sign(st): + s1 -= st + if s1 not in r1: + return + return s1 + + # calculate the step size of the new Range + step = abs(ilcm(r1.step, r2.step)) + s1 = _first_finite_point(r1, c) + if s1 is None: + return S.EmptySet + s2 = _first_finite_point(r2, c) + if s2 is None: + return S.EmptySet + + # replace the corresponding start or stop in + # the original Ranges with these points; the + # result must have at least one point since + # we know that s1 and s2 are in the Ranges + def _updated_range(r, first): + st = sign(r.step)*step + if r.start.is_finite: + rv = Range(first, r.stop, st) + else: + rv = Range(r.start, first + st, st) + return rv + r1 = _updated_range(a, s1) + r2 = _updated_range(b, s2) + + # work with them both in the increasing direction + if sign(r1.step) < 0: + r1 = r1.reversed + if sign(r2.step) < 0: + r2 = r2.reversed + + # return clipped Range with positive step; it + # can't be empty at this point + start = max(r1.start, r2.start) + stop = min(r1.stop, r2.stop) + return Range(start, stop, step) + + +@intersection_sets.register(Range, Integers) +def _(a, b): + return a + + +@intersection_sets.register(Range, Rationals) +def _(a, b): + return a + + +@intersection_sets.register(ImageSet, Set) +def _(self, other): + from sympy.solvers.diophantine import diophantine + + # Only handle the straight-forward univariate case + if (len(self.lamda.variables) > 1 + or self.lamda.signature != self.lamda.variables): + return None + base_set = self.base_sets[0] + + # Intersection between ImageSets with Integers as base set + # For {f(n) : n in Integers} & {g(m) : m in Integers} we solve the + # diophantine equations f(n)=g(m). + # If the solutions for n are {h(t) : t in Integers} then we return + # {f(h(t)) : t in integers}. + # If the solutions for n are {n_1, n_2, ..., n_k} then we return + # {f(n_i) : 1 <= i <= k}. + if base_set is S.Integers: + gm = None + if isinstance(other, ImageSet) and other.base_sets == (S.Integers,): + gm = other.lamda.expr + var = other.lamda.variables[0] + # Symbol of second ImageSet lambda must be distinct from first + m = Dummy('m') + gm = gm.subs(var, m) + elif other is S.Integers: + m = gm = Dummy('m') + if gm is not None: + fn = self.lamda.expr + n = self.lamda.variables[0] + try: + solns = list(diophantine(fn - gm, syms=(n, m), permute=True)) + except (TypeError, NotImplementedError): + # TypeError if equation not polynomial with rational coeff. + # NotImplementedError if correct format but no solver. + return + # 3 cases are possible for solns: + # - empty set, + # - one or more parametric (infinite) solutions, + # - a finite number of (non-parametric) solution couples. + # Among those, there is one type of solution set that is + # not helpful here: multiple parametric solutions. + if len(solns) == 0: + return S.EmptySet + elif any(s.free_symbols for tupl in solns for s in tupl): + if len(solns) == 1: + soln, solm = solns[0] + (t,) = soln.free_symbols + expr = fn.subs(n, soln.subs(t, n)).expand() + return imageset(Lambda(n, expr), S.Integers) + else: + return + else: + return FiniteSet(*(fn.subs(n, s[0]) for s in solns)) + + if other == S.Reals: + from sympy.solvers.solvers import denoms, solve_linear + + def _solution_union(exprs, sym): + # return a union of linear solutions to i in expr; + # if i cannot be solved, use a ConditionSet for solution + sols = [] + for i in exprs: + x, xis = solve_linear(i, 0, [sym]) + if x == sym: + sols.append(FiniteSet(xis)) + else: + sols.append(ConditionSet(sym, Eq(i, 0))) + return Union(*sols) + + f = self.lamda.expr + n = self.lamda.variables[0] + + n_ = Dummy(n.name, real=True) + f_ = f.subs(n, n_) + + re, im = f_.as_real_imag() + im = expand_complex(im) + + re = re.subs(n_, n) + im = im.subs(n_, n) + ifree = im.free_symbols + lam = Lambda(n, re) + if im.is_zero: + # allow re-evaluation + # of self in this case to make + # the result canonical + pass + elif im.is_zero is False: + return S.EmptySet + elif ifree != {n}: + return None + else: + # univarite imaginary part in same variable; + # use numer instead of as_numer_denom to keep + # this as fast as possible while still handling + # simple cases + base_set &= _solution_union( + Mul.make_args(numer(im)), n) + # exclude values that make denominators 0 + base_set -= _solution_union(denoms(f), n) + return imageset(lam, base_set) + + elif isinstance(other, Interval): + from sympy.solvers.solveset import (invert_real, invert_complex, + solveset) + + f = self.lamda.expr + n = self.lamda.variables[0] + new_inf, new_sup = None, None + new_lopen, new_ropen = other.left_open, other.right_open + + if f.is_real: + inverter = invert_real + else: + inverter = invert_complex + + g1, h1 = inverter(f, other.inf, n) + g2, h2 = inverter(f, other.sup, n) + + if all(isinstance(i, FiniteSet) for i in (h1, h2)): + if g1 == n: + if len(h1) == 1: + new_inf = h1.args[0] + if g2 == n: + if len(h2) == 1: + new_sup = h2.args[0] + # TODO: Design a technique to handle multiple-inverse + # functions + + # Any of the new boundary values cannot be determined + if any(i is None for i in (new_sup, new_inf)): + return + + + range_set = S.EmptySet + + if all(i.is_real for i in (new_sup, new_inf)): + # this assumes continuity of underlying function + # however fixes the case when it is decreasing + if new_inf > new_sup: + new_inf, new_sup = new_sup, new_inf + new_interval = Interval(new_inf, new_sup, new_lopen, new_ropen) + range_set = base_set.intersect(new_interval) + else: + if other.is_subset(S.Reals): + solutions = solveset(f, n, S.Reals) + if not isinstance(range_set, (ImageSet, ConditionSet)): + range_set = solutions.intersect(other) + else: + return + + if range_set is S.EmptySet: + return S.EmptySet + elif isinstance(range_set, Range) and range_set.size is not S.Infinity: + range_set = FiniteSet(*list(range_set)) + + if range_set is not None: + return imageset(Lambda(n, f), range_set) + return + else: + return + + +@intersection_sets.register(ProductSet, ProductSet) +def _(a, b): + if len(b.args) != len(a.args): + return S.EmptySet + return ProductSet(*(i.intersect(j) for i, j in zip(a.sets, b.sets))) + + +@intersection_sets.register(Interval, Interval) +def _(a, b): + # handle (-oo, oo) + infty = S.NegativeInfinity, S.Infinity + if a == Interval(*infty): + l, r = a.left, a.right + if l.is_real or l in infty or r.is_real or r in infty: + return b + + # We can't intersect [0,3] with [x,6] -- we don't know if x>0 or x<0 + if not a._is_comparable(b): + return None + + empty = False + + if a.start <= b.end and b.start <= a.end: + # Get topology right. + if a.start < b.start: + start = b.start + left_open = b.left_open + elif a.start > b.start: + start = a.start + left_open = a.left_open + else: + start = a.start + if not _aresame(a.start, b.start): + # For example Integer(2) != Float(2) + # Prefer the Float boundary because Floats should be + # contagious in calculations. + if b.start.has(Float) and not a.start.has(Float): + start = b.start + elif a.start.has(Float) and not b.start.has(Float): + start = a.start + else: + #this is to ensure that if Eq(a.start, b.start) but + #type(a.start) != type(b.start) the order of a and b + #does not matter for the result + start = list(ordered([a,b]))[0].start + left_open = a.left_open or b.left_open + + if a.end < b.end: + end = a.end + right_open = a.right_open + elif a.end > b.end: + end = b.end + right_open = b.right_open + else: + # see above for logic with start + end = a.end + if not _aresame(a.end, b.end): + if b.end.has(Float) and not a.end.has(Float): + end = b.end + elif a.end.has(Float) and not b.end.has(Float): + end = a.end + else: + end = list(ordered([a,b]))[0].end + right_open = a.right_open or b.right_open + + if end - start == 0 and (left_open or right_open): + empty = True + else: + empty = True + + if empty: + return S.EmptySet + + return Interval(start, end, left_open, right_open) + +@intersection_sets.register(EmptySet, Set) +def _(a, b): + return S.EmptySet + +@intersection_sets.register(UniversalSet, Set) +def _(a, b): + return b + +@intersection_sets.register(FiniteSet, FiniteSet) +def _(a, b): + return FiniteSet(*(a._elements & b._elements)) + +@intersection_sets.register(FiniteSet, Set) +def _(a, b): + try: + return FiniteSet(*[el for el in a if el in b]) + except TypeError: + return None # could not evaluate `el in b` due to symbolic ranges. + +@intersection_sets.register(Set, Set) +def _(a, b): + return None + +@intersection_sets.register(Integers, Rationals) +def _(a, b): + return a + +@intersection_sets.register(Naturals, Rationals) +def _(a, b): + return a + +@intersection_sets.register(Rationals, Reals) +def _(a, b): + return a + +def _intlike_interval(a, b): + try: + if b._inf is S.NegativeInfinity and b._sup is S.Infinity: + return a + s = Range(max(a.inf, ceiling(b.left)), floor(b.right) + 1) + return intersection_sets(s, b) # take out endpoints if open interval + except ValueError: + return None + +@intersection_sets.register(Integers, Interval) +def _(a, b): + return _intlike_interval(a, b) + +@intersection_sets.register(Naturals, Interval) +def _(a, b): + return _intlike_interval(a, b) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/issubset.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/issubset.py new file mode 100644 index 0000000000000000000000000000000000000000..cc23e8bf56f1743cd7f08452dd09a0acf981f5da --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/issubset.py @@ -0,0 +1,144 @@ +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.core.logic import fuzzy_and, fuzzy_bool, fuzzy_not, fuzzy_or +from sympy.core.relational import Eq +from sympy.sets.sets import FiniteSet, Interval, Set, Union, ProductSet +from sympy.sets.fancysets import Complexes, Reals, Range, Rationals +from sympy.multipledispatch import Dispatcher + + +_inf_sets = [S.Naturals, S.Naturals0, S.Integers, S.Rationals, S.Reals, S.Complexes] + + +is_subset_sets = Dispatcher('is_subset_sets') + + +@is_subset_sets.register(Set, Set) +def _(a, b): + return None + +@is_subset_sets.register(Interval, Interval) +def _(a, b): + # This is correct but can be made more comprehensive... + if fuzzy_bool(a.start < b.start): + return False + if fuzzy_bool(a.end > b.end): + return False + if (b.left_open and not a.left_open and fuzzy_bool(Eq(a.start, b.start))): + return False + if (b.right_open and not a.right_open and fuzzy_bool(Eq(a.end, b.end))): + return False + +@is_subset_sets.register(Interval, FiniteSet) +def _(a_interval, b_fs): + # An Interval can only be a subset of a finite set if it is finite + # which can only happen if it has zero measure. + if fuzzy_not(a_interval.measure.is_zero): + return False + +@is_subset_sets.register(Interval, Union) +def _(a_interval, b_u): + if all(isinstance(s, (Interval, FiniteSet)) for s in b_u.args): + intervals = [s for s in b_u.args if isinstance(s, Interval)] + if all(fuzzy_bool(a_interval.start < s.start) for s in intervals): + return False + if all(fuzzy_bool(a_interval.end > s.end) for s in intervals): + return False + if a_interval.measure.is_nonzero: + no_overlap = lambda s1, s2: fuzzy_or([ + fuzzy_bool(s1.end <= s2.start), + fuzzy_bool(s1.start >= s2.end), + ]) + if all(no_overlap(s, a_interval) for s in intervals): + return False + +@is_subset_sets.register(Range, Range) +def _(a, b): + if a.step == b.step == 1: + return fuzzy_and([fuzzy_bool(a.start >= b.start), + fuzzy_bool(a.stop <= b.stop)]) + +@is_subset_sets.register(Range, Interval) +def _(a_range, b_interval): + if a_range.step.is_positive: + if b_interval.left_open and a_range.inf.is_finite: + cond_left = a_range.inf > b_interval.left + else: + cond_left = a_range.inf >= b_interval.left + if b_interval.right_open and a_range.sup.is_finite: + cond_right = a_range.sup < b_interval.right + else: + cond_right = a_range.sup <= b_interval.right + return fuzzy_and([cond_left, cond_right]) + +@is_subset_sets.register(Range, FiniteSet) +def _(a_range, b_finiteset): + try: + a_size = a_range.size + except ValueError: + # symbolic Range of unknown size + return None + if a_size > len(b_finiteset): + return False + elif any(arg.has(Symbol) for arg in a_range.args): + return fuzzy_and(b_finiteset.contains(x) for x in a_range) + else: + # Checking A \ B == EmptySet is more efficient than repeated naive + # membership checks on an arbitrary FiniteSet. + a_set = set(a_range) + b_remaining = len(b_finiteset) + # Symbolic expressions and numbers of unknown type (integer or not) are + # all counted as "candidates", i.e. *potentially* matching some a in + # a_range. + cnt_candidate = 0 + for b in b_finiteset: + if b.is_Integer: + a_set.discard(b) + elif fuzzy_not(b.is_integer): + pass + else: + cnt_candidate += 1 + b_remaining -= 1 + if len(a_set) > b_remaining + cnt_candidate: + return False + if len(a_set) == 0: + return True + return None + +@is_subset_sets.register(Interval, Range) +def _(a_interval, b_range): + if a_interval.measure.is_extended_nonzero: + return False + +@is_subset_sets.register(Interval, Rationals) +def _(a_interval, b_rationals): + if a_interval.measure.is_extended_nonzero: + return False + +@is_subset_sets.register(Range, Complexes) +def _(a, b): + return True + +@is_subset_sets.register(Complexes, Interval) +def _(a, b): + return False + +@is_subset_sets.register(Complexes, Range) +def _(a, b): + return False + +@is_subset_sets.register(Complexes, Rationals) +def _(a, b): + return False + +@is_subset_sets.register(Rationals, Reals) +def _(a, b): + return True + +@is_subset_sets.register(Rationals, Range) +def _(a, b): + return False + +@is_subset_sets.register(ProductSet, FiniteSet) +def _(a_ps, b_fs): + return fuzzy_and(b_fs.contains(x) for x in a_ps) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/mul.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/mul.py new file mode 100644 index 0000000000000000000000000000000000000000..0dedc8068b7973fd4cb6fbf2854e5fa671d188de --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/mul.py @@ -0,0 +1,79 @@ +from sympy.core import Basic, Expr +from sympy.core.numbers import oo +from sympy.core.symbol import symbols +from sympy.multipledispatch import Dispatcher +from sympy.sets.setexpr import set_mul +from sympy.sets.sets import Interval, Set + + +_x, _y = symbols("x y") + + +_set_mul = Dispatcher('_set_mul') +_set_div = Dispatcher('_set_div') + + +@_set_mul.register(Basic, Basic) +def _(x, y): + return None + +@_set_mul.register(Set, Set) +def _(x, y): + return None + +@_set_mul.register(Expr, Expr) +def _(x, y): + return x*y + +@_set_mul.register(Interval, Interval) +def _(x, y): + """ + Multiplications in interval arithmetic + https://en.wikipedia.org/wiki/Interval_arithmetic + """ + # TODO: some intervals containing 0 and oo will fail as 0*oo returns nan. + comvals = ( + (x.start * y.start, bool(x.left_open or y.left_open)), + (x.start * y.end, bool(x.left_open or y.right_open)), + (x.end * y.start, bool(x.right_open or y.left_open)), + (x.end * y.end, bool(x.right_open or y.right_open)), + ) + # TODO: handle symbolic intervals + minval, minopen = min(comvals) + maxval, maxopen = max(comvals) + return Interval( + minval, + maxval, + minopen, + maxopen + ) + +@_set_div.register(Basic, Basic) +def _(x, y): + return None + +@_set_div.register(Expr, Expr) +def _(x, y): + return x/y + +@_set_div.register(Set, Set) +def _(x, y): + return None + +@_set_div.register(Interval, Interval) +def _(x, y): + """ + Divisions in interval arithmetic + https://en.wikipedia.org/wiki/Interval_arithmetic + """ + if (y.start*y.end).is_negative: + return Interval(-oo, oo) + if y.start == 0: + s2 = oo + else: + s2 = 1/y.start + if y.end == 0: + s1 = -oo + else: + s1 = 1/y.end + return set_mul(x, Interval(s1, s2, y.right_open, y.left_open)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/power.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/power.py new file mode 100644 index 0000000000000000000000000000000000000000..3cad4ee49ab27770143bc121d1fbcd024bf01548 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/power.py @@ -0,0 +1,107 @@ +from sympy.core import Basic, Expr +from sympy.core.function import Lambda +from sympy.core.numbers import oo, Infinity, NegativeInfinity, Zero, Integer +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.miscellaneous import (Max, Min) +from sympy.sets.fancysets import ImageSet +from sympy.sets.setexpr import set_div +from sympy.sets.sets import Set, Interval, FiniteSet, Union +from sympy.multipledispatch import Dispatcher + + +_x, _y = symbols("x y") + + +_set_pow = Dispatcher('_set_pow') + + +@_set_pow.register(Basic, Basic) +def _(x, y): + return None + +@_set_pow.register(Set, Set) +def _(x, y): + return ImageSet(Lambda((_x, _y), (_x ** _y)), x, y) + +@_set_pow.register(Expr, Expr) +def _(x, y): + return x**y + +@_set_pow.register(Interval, Zero) +def _(x, z): + return FiniteSet(S.One) + +@_set_pow.register(Interval, Integer) +def _(x, exponent): + """ + Powers in interval arithmetic + https://en.wikipedia.org/wiki/Interval_arithmetic + """ + s1 = x.start**exponent + s2 = x.end**exponent + if ((s2 > s1) if exponent > 0 else (x.end > -x.start)) == True: + left_open = x.left_open + right_open = x.right_open + # TODO: handle unevaluated condition. + sleft = s2 + else: + # TODO: `s2 > s1` could be unevaluated. + left_open = x.right_open + right_open = x.left_open + sleft = s1 + + if x.start.is_positive: + return Interval( + Min(s1, s2), + Max(s1, s2), left_open, right_open) + elif x.end.is_negative: + return Interval( + Min(s1, s2), + Max(s1, s2), left_open, right_open) + + # Case where x.start < 0 and x.end > 0: + if exponent.is_odd: + if exponent.is_negative: + if x.start.is_zero: + return Interval(s2, oo, x.right_open) + if x.end.is_zero: + return Interval(-oo, s1, True, x.left_open) + return Union(Interval(-oo, s1, True, x.left_open), Interval(s2, oo, x.right_open)) + else: + return Interval(s1, s2, x.left_open, x.right_open) + elif exponent.is_even: + if exponent.is_negative: + if x.start.is_zero: + return Interval(s2, oo, x.right_open) + if x.end.is_zero: + return Interval(s1, oo, x.left_open) + return Interval(0, oo) + else: + return Interval(S.Zero, sleft, S.Zero not in x, left_open) + +@_set_pow.register(Interval, Infinity) +def _(b, e): + # TODO: add logic for open intervals? + if b.start.is_nonnegative: + if b.end < 1: + return FiniteSet(S.Zero) + if b.start > 1: + return FiniteSet(S.Infinity) + return Interval(0, oo) + elif b.end.is_negative: + if b.start > -1: + return FiniteSet(S.Zero) + if b.end < -1: + return FiniteSet(-oo, oo) + return Interval(-oo, oo) + else: + if b.start > -1: + if b.end < 1: + return FiniteSet(S.Zero) + return Interval(0, oo) + return Interval(-oo, oo) + +@_set_pow.register(Interval, NegativeInfinity) +def _(b, e): + return _set_pow(set_div(S.One, b), oo) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/union.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/union.py new file mode 100644 index 0000000000000000000000000000000000000000..75d867b49969ae2aeea76155dbaae7e05c1a6847 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/handlers/union.py @@ -0,0 +1,147 @@ +from sympy.core.singleton import S +from sympy.core.sympify import sympify +from sympy.functions.elementary.miscellaneous import Min, Max +from sympy.sets.sets import (EmptySet, FiniteSet, Intersection, + Interval, ProductSet, Set, Union, UniversalSet) +from sympy.sets.fancysets import (ComplexRegion, Naturals, Naturals0, + Integers, Rationals, Reals) +from sympy.multipledispatch import Dispatcher + + +union_sets = Dispatcher('union_sets') + + +@union_sets.register(Naturals0, Naturals) +def _(a, b): + return a + +@union_sets.register(Rationals, Naturals) +def _(a, b): + return a + +@union_sets.register(Rationals, Naturals0) +def _(a, b): + return a + +@union_sets.register(Reals, Naturals) +def _(a, b): + return a + +@union_sets.register(Reals, Naturals0) +def _(a, b): + return a + +@union_sets.register(Reals, Rationals) +def _(a, b): + return a + +@union_sets.register(Integers, Set) +def _(a, b): + intersect = Intersection(a, b) + if intersect == a: + return b + elif intersect == b: + return a + +@union_sets.register(ComplexRegion, Set) +def _(a, b): + if b.is_subset(S.Reals): + # treat a subset of reals as a complex region + b = ComplexRegion.from_real(b) + + if b.is_ComplexRegion: + # a in rectangular form + if (not a.polar) and (not b.polar): + return ComplexRegion(Union(a.sets, b.sets)) + # a in polar form + elif a.polar and b.polar: + return ComplexRegion(Union(a.sets, b.sets), polar=True) + return None + +@union_sets.register(EmptySet, Set) +def _(a, b): + return b + + +@union_sets.register(UniversalSet, Set) +def _(a, b): + return a + +@union_sets.register(ProductSet, ProductSet) +def _(a, b): + if b.is_subset(a): + return a + if len(b.sets) != len(a.sets): + return None + if len(a.sets) == 2: + a1, a2 = a.sets + b1, b2 = b.sets + if a1 == b1: + return a1 * Union(a2, b2) + if a2 == b2: + return Union(a1, b1) * a2 + return None + +@union_sets.register(ProductSet, Set) +def _(a, b): + if b.is_subset(a): + return a + return None + +@union_sets.register(Interval, Interval) +def _(a, b): + if a._is_comparable(b): + # Non-overlapping intervals + end = Min(a.end, b.end) + start = Max(a.start, b.start) + if (end < start or + (end == start and (end not in a and end not in b))): + return None + else: + start = Min(a.start, b.start) + end = Max(a.end, b.end) + + left_open = ((a.start != start or a.left_open) and + (b.start != start or b.left_open)) + right_open = ((a.end != end or a.right_open) and + (b.end != end or b.right_open)) + return Interval(start, end, left_open, right_open) + +@union_sets.register(Interval, UniversalSet) +def _(a, b): + return S.UniversalSet + +@union_sets.register(Interval, Set) +def _(a, b): + # If I have open end points and these endpoints are contained in b + # But only in case, when endpoints are finite. Because + # interval does not contain oo or -oo. + open_left_in_b_and_finite = (a.left_open and + sympify(b.contains(a.start)) is S.true and + a.start.is_finite) + open_right_in_b_and_finite = (a.right_open and + sympify(b.contains(a.end)) is S.true and + a.end.is_finite) + if open_left_in_b_and_finite or open_right_in_b_and_finite: + # Fill in my end points and return + open_left = a.left_open and a.start not in b + open_right = a.right_open and a.end not in b + new_a = Interval(a.start, a.end, open_left, open_right) + return {new_a, b} + return None + +@union_sets.register(FiniteSet, FiniteSet) +def _(a, b): + return FiniteSet(*(a._elements | b._elements)) + +@union_sets.register(FiniteSet, Set) +def _(a, b): + # If `b` set contains one of my elements, remove it from `a` + if any(b.contains(x) == True for x in a): + return { + FiniteSet(*[x for x in a if b.contains(x) != True]), b} + return None + +@union_sets.register(Set, Set) +def _(a, b): + return None diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/ordinals.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/ordinals.py new file mode 100644 index 0000000000000000000000000000000000000000..cfe062354cfe58a4747998e51fa0d261e67576cc --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/ordinals.py @@ -0,0 +1,282 @@ +from sympy.core import Basic, Integer +import operator + + +class OmegaPower(Basic): + """ + Represents ordinal exponential and multiplication terms one of the + building blocks of the :class:`Ordinal` class. + In ``OmegaPower(a, b)``, ``a`` represents exponent and ``b`` represents multiplicity. + """ + def __new__(cls, a, b): + if isinstance(b, int): + b = Integer(b) + if not isinstance(b, Integer) or b <= 0: + raise TypeError("multiplicity must be a positive integer") + + if not isinstance(a, Ordinal): + a = Ordinal.convert(a) + + return Basic.__new__(cls, a, b) + + @property + def exp(self): + return self.args[0] + + @property + def mult(self): + return self.args[1] + + def _compare_term(self, other, op): + if self.exp == other.exp: + return op(self.mult, other.mult) + else: + return op(self.exp, other.exp) + + def __eq__(self, other): + if not isinstance(other, OmegaPower): + try: + other = OmegaPower(0, other) + except TypeError: + return NotImplemented + return self.args == other.args + + def __hash__(self): + return Basic.__hash__(self) + + def __lt__(self, other): + if not isinstance(other, OmegaPower): + try: + other = OmegaPower(0, other) + except TypeError: + return NotImplemented + return self._compare_term(other, operator.lt) + + +class Ordinal(Basic): + """ + Represents ordinals in Cantor normal form. + + Internally, this class is just a list of instances of OmegaPower. + + Examples + ======== + >>> from sympy import Ordinal, OmegaPower + >>> from sympy.sets.ordinals import omega + >>> w = omega + >>> w.is_limit_ordinal + True + >>> Ordinal(OmegaPower(w + 1, 1), OmegaPower(3, 2)) + w**(w + 1) + w**3*2 + >>> 3 + w + w + >>> (w + 1) * w + w**2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Ordinal_arithmetic + """ + def __new__(cls, *terms): + obj = super().__new__(cls, *terms) + powers = [i.exp for i in obj.args] + if not all(powers[i] >= powers[i+1] for i in range(len(powers) - 1)): + raise ValueError("powers must be in decreasing order") + return obj + + @property + def terms(self): + return self.args + + @property + def leading_term(self): + if self == ord0: + raise ValueError("ordinal zero has no leading term") + return self.terms[0] + + @property + def trailing_term(self): + if self == ord0: + raise ValueError("ordinal zero has no trailing term") + return self.terms[-1] + + @property + def is_successor_ordinal(self): + try: + return self.trailing_term.exp == ord0 + except ValueError: + return False + + @property + def is_limit_ordinal(self): + try: + return not self.trailing_term.exp == ord0 + except ValueError: + return False + + @property + def degree(self): + return self.leading_term.exp + + @classmethod + def convert(cls, integer_value): + if integer_value == 0: + return ord0 + return Ordinal(OmegaPower(0, integer_value)) + + def __eq__(self, other): + if not isinstance(other, Ordinal): + try: + other = Ordinal.convert(other) + except TypeError: + return NotImplemented + return self.terms == other.terms + + def __hash__(self): + return hash(self.args) + + def __lt__(self, other): + if not isinstance(other, Ordinal): + try: + other = Ordinal.convert(other) + except TypeError: + return NotImplemented + for term_self, term_other in zip(self.terms, other.terms): + if term_self != term_other: + return term_self < term_other + return len(self.terms) < len(other.terms) + + def __le__(self, other): + return (self == other or self < other) + + def __gt__(self, other): + return not self <= other + + def __ge__(self, other): + return not self < other + + def __str__(self): + net_str = "" + plus_count = 0 + if self == ord0: + return 'ord0' + for i in self.terms: + if plus_count: + net_str += " + " + + if i.exp == ord0: + net_str += str(i.mult) + elif i.exp == 1: + net_str += 'w' + elif len(i.exp.terms) > 1 or i.exp.is_limit_ordinal: + net_str += 'w**(%s)'%i.exp + else: + net_str += 'w**%s'%i.exp + + if not i.mult == 1 and not i.exp == ord0: + net_str += '*%s'%i.mult + + plus_count += 1 + return(net_str) + + __repr__ = __str__ + + def __add__(self, other): + if not isinstance(other, Ordinal): + try: + other = Ordinal.convert(other) + except TypeError: + return NotImplemented + if other == ord0: + return self + a_terms = list(self.terms) + b_terms = list(other.terms) + r = len(a_terms) - 1 + b_exp = other.degree + while r >= 0 and a_terms[r].exp < b_exp: + r -= 1 + if r < 0: + terms = b_terms + elif a_terms[r].exp == b_exp: + sum_term = OmegaPower(b_exp, a_terms[r].mult + other.leading_term.mult) + terms = a_terms[:r] + [sum_term] + b_terms[1:] + else: + terms = a_terms[:r+1] + b_terms + return Ordinal(*terms) + + def __radd__(self, other): + if not isinstance(other, Ordinal): + try: + other = Ordinal.convert(other) + except TypeError: + return NotImplemented + return other + self + + def __mul__(self, other): + if not isinstance(other, Ordinal): + try: + other = Ordinal.convert(other) + except TypeError: + return NotImplemented + if ord0 in (self, other): + return ord0 + a_exp = self.degree + a_mult = self.leading_term.mult + summation = [] + if other.is_limit_ordinal: + for arg in other.terms: + summation.append(OmegaPower(a_exp + arg.exp, arg.mult)) + + else: + for arg in other.terms[:-1]: + summation.append(OmegaPower(a_exp + arg.exp, arg.mult)) + b_mult = other.trailing_term.mult + summation.append(OmegaPower(a_exp, a_mult*b_mult)) + summation += list(self.terms[1:]) + return Ordinal(*summation) + + def __rmul__(self, other): + if not isinstance(other, Ordinal): + try: + other = Ordinal.convert(other) + except TypeError: + return NotImplemented + return other * self + + def __pow__(self, other): + if not self == omega: + return NotImplemented + return Ordinal(OmegaPower(other, 1)) + + +class OrdinalZero(Ordinal): + """The ordinal zero. + + OrdinalZero can be imported as ``ord0``. + """ + pass + + +class OrdinalOmega(Ordinal): + """The ordinal omega which forms the base of all ordinals in cantor normal form. + + OrdinalOmega can be imported as ``omega``. + + Examples + ======== + + >>> from sympy.sets.ordinals import omega + >>> omega + omega + w*2 + """ + def __new__(cls): + return Ordinal.__new__(cls) + + @property + def terms(self): + return (OmegaPower(1, 1),) + + +ord0 = OrdinalZero() +omega = OrdinalOmega() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/powerset.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/powerset.py new file mode 100644 index 0000000000000000000000000000000000000000..2eb3b41b9859281480bc9517a1cad0abe7a5683f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/powerset.py @@ -0,0 +1,119 @@ +from sympy.core.decorators import _sympifyit +from sympy.core.parameters import global_parameters +from sympy.core.logic import fuzzy_bool +from sympy.core.singleton import S +from sympy.core.sympify import _sympify + +from .sets import Set, FiniteSet, SetKind + + +class PowerSet(Set): + r"""A symbolic object representing a power set. + + Parameters + ========== + + arg : Set + The set to take power of. + + evaluate : bool + The flag to control evaluation. + + If the evaluation is disabled for finite sets, it can take + advantage of using subset test as a membership test. + + Notes + ===== + + Power set `\mathcal{P}(S)` is defined as a set containing all the + subsets of `S`. + + If the set `S` is a finite set, its power set would have + `2^{\left| S \right|}` elements, where `\left| S \right|` denotes + the cardinality of `S`. + + Examples + ======== + + >>> from sympy import PowerSet, S, FiniteSet + + A power set of a finite set: + + >>> PowerSet(FiniteSet(1, 2, 3)) + PowerSet({1, 2, 3}) + + A power set of an empty set: + + >>> PowerSet(S.EmptySet) + PowerSet(EmptySet) + >>> PowerSet(PowerSet(S.EmptySet)) + PowerSet(PowerSet(EmptySet)) + + A power set of an infinite set: + + >>> PowerSet(S.Reals) + PowerSet(Reals) + + Evaluating the power set of a finite set to its explicit form: + + >>> PowerSet(FiniteSet(1, 2, 3)).rewrite(FiniteSet) + FiniteSet(EmptySet, {1}, {2}, {3}, {1, 2}, {1, 3}, {2, 3}, {1, 2, 3}) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Power_set + + .. [2] https://en.wikipedia.org/wiki/Axiom_of_power_set + """ + def __new__(cls, arg, evaluate=None): + if evaluate is None: + evaluate=global_parameters.evaluate + + arg = _sympify(arg) + + if not isinstance(arg, Set): + raise ValueError('{} must be a set.'.format(arg)) + + return super().__new__(cls, arg) + + @property + def arg(self): + return self.args[0] + + def _eval_rewrite_as_FiniteSet(self, *args, **kwargs): + arg = self.arg + if arg.is_FiniteSet: + return arg.powerset() + return None + + @_sympifyit('other', NotImplemented) + def _contains(self, other): + if not isinstance(other, Set): + return None + + return fuzzy_bool(self.arg.is_superset(other)) + + def _eval_is_subset(self, other): + if isinstance(other, PowerSet): + return self.arg.is_subset(other.arg) + + def __len__(self): + return 2 ** len(self.arg) + + def __iter__(self): + found = [S.EmptySet] + yield S.EmptySet + + for x in self.arg: + temp = [] + x = FiniteSet(x) + for y in found: + new = x + y + yield new + temp.append(new) + found.extend(temp) + + @property + def kind(self): + return SetKind(self.arg.kind) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/setexpr.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/setexpr.py new file mode 100644 index 0000000000000000000000000000000000000000..94d77d5293617a620b70a945888987ce6cc61157 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/setexpr.py @@ -0,0 +1,97 @@ +from sympy.core import Expr +from sympy.core.decorators import call_highest_priority, _sympifyit +from .fancysets import ImageSet +from .sets import set_add, set_sub, set_mul, set_div, set_pow, set_function + + +class SetExpr(Expr): + """An expression that can take on values of a set. + + Examples + ======== + + >>> from sympy import Interval, FiniteSet + >>> from sympy.sets.setexpr import SetExpr + + >>> a = SetExpr(Interval(0, 5)) + >>> b = SetExpr(FiniteSet(1, 10)) + >>> (a + b).set + Union(Interval(1, 6), Interval(10, 15)) + >>> (2*a + b).set + Interval(1, 20) + """ + _op_priority = 11.0 + + def __new__(cls, setarg): + return Expr.__new__(cls, setarg) + + set = property(lambda self: self.args[0]) + + def _latex(self, printer): + return r"SetExpr\left({}\right)".format(printer._print(self.set)) + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__radd__') + def __add__(self, other): + return _setexpr_apply_operation(set_add, self, other) + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__add__') + def __radd__(self, other): + return _setexpr_apply_operation(set_add, other, self) + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__rmul__') + def __mul__(self, other): + return _setexpr_apply_operation(set_mul, self, other) + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__mul__') + def __rmul__(self, other): + return _setexpr_apply_operation(set_mul, other, self) + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__rsub__') + def __sub__(self, other): + return _setexpr_apply_operation(set_sub, self, other) + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__sub__') + def __rsub__(self, other): + return _setexpr_apply_operation(set_sub, other, self) + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__rpow__') + def __pow__(self, other): + return _setexpr_apply_operation(set_pow, self, other) + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__pow__') + def __rpow__(self, other): + return _setexpr_apply_operation(set_pow, other, self) + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__rtruediv__') + def __truediv__(self, other): + return _setexpr_apply_operation(set_div, self, other) + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__truediv__') + def __rtruediv__(self, other): + return _setexpr_apply_operation(set_div, other, self) + + def _eval_func(self, func): + # TODO: this could be implemented straight into `imageset`: + res = set_function(func, self.set) + if res is None: + return SetExpr(ImageSet(func, self.set)) + return SetExpr(res) + + +def _setexpr_apply_operation(op, x, y): + if isinstance(x, SetExpr): + x = x.set + if isinstance(y, SetExpr): + y = y.set + out = op(x, y) + return SetExpr(out) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/sets.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/sets.py new file mode 100644 index 0000000000000000000000000000000000000000..3c85ce87c515cfd4520dcc6b9265fe76d8c6163f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/sets.py @@ -0,0 +1,2804 @@ +from __future__ import annotations + +from typing import Any, Callable, TYPE_CHECKING, overload +from functools import reduce +from collections import defaultdict +from collections.abc import Mapping, Iterable +import inspect + +from sympy.core.kind import Kind, UndefinedKind, NumberKind +from sympy.core.basic import Basic +from sympy.core.containers import Tuple, TupleKind +from sympy.core.decorators import sympify_method_args, sympify_return +from sympy.core.evalf import EvalfMixin +from sympy.core.expr import Expr +from sympy.core.function import Lambda +from sympy.core.logic import (FuzzyBool, fuzzy_bool, fuzzy_or, fuzzy_and, + fuzzy_not) +from sympy.core.numbers import Float, Integer +from sympy.core.operations import LatticeOp +from sympy.core.parameters import global_parameters +from sympy.core.relational import Eq, Ne, is_lt +from sympy.core.singleton import Singleton, S +from sympy.core.sorting import ordered +from sympy.core.symbol import symbols, Symbol, Dummy, uniquely_named_symbol +from sympy.core.sympify import _sympify, sympify, _sympy_converter +from sympy.functions.elementary.exponential import exp, log +from sympy.functions.elementary.miscellaneous import Max, Min +from sympy.logic.boolalg import And, Or, Not, Xor, true, false +from sympy.utilities.decorator import deprecated +from sympy.utilities.exceptions import sympy_deprecation_warning +from sympy.utilities.iterables import (iproduct, sift, roundrobin, iterable, + subsets) +from sympy.utilities.misc import func_name, filldedent + +from mpmath import mpi, mpf + +from mpmath.libmp.libmpf import prec_to_dps + + +tfn = defaultdict(lambda: None, { + True: S.true, + S.true: S.true, + False: S.false, + S.false: S.false}) + + +@sympify_method_args +class Set(Basic, EvalfMixin): + """ + The base class for any kind of set. + + Explanation + =========== + + This is not meant to be used directly as a container of items. It does not + behave like the builtin ``set``; see :class:`FiniteSet` for that. + + Real intervals are represented by the :class:`Interval` class and unions of + sets by the :class:`Union` class. The empty set is represented by the + :class:`EmptySet` class and available as a singleton as ``S.EmptySet``. + """ + + __slots__: tuple[()] = () + + is_number = False + is_iterable = False + is_interval = False + + is_FiniteSet = False + is_Interval = False + is_ProductSet = False + is_Union = False + is_Intersection: FuzzyBool = None + is_UniversalSet: FuzzyBool = None + is_Complement: FuzzyBool = None + is_ComplexRegion = False + + is_empty: FuzzyBool = None + is_finite_set: FuzzyBool = None + + @property # type: ignore + @deprecated( + """ + The is_EmptySet attribute of Set objects is deprecated. + Use 's is S.EmptySet" or 's.is_empty' instead. + """, + deprecated_since_version="1.5", + active_deprecations_target="deprecated-is-emptyset", + ) + def is_EmptySet(self): + return None + + if TYPE_CHECKING: + + def __new__(cls, *args: Basic | complex) -> Set: + ... + + @overload # type: ignore + def subs(self, arg1: Mapping[Basic | complex, Set | complex], arg2: None=None) -> Set: ... + @overload + def subs(self, arg1: Iterable[tuple[Basic | complex, Set | complex]], arg2: None=None, **kwargs: Any) -> Set: ... + @overload + def subs(self, arg1: Set | complex, arg2: Set | complex) -> Set: ... + @overload + def subs(self, arg1: Mapping[Basic | complex, Basic | complex], arg2: None=None, **kwargs: Any) -> Basic: ... + @overload + def subs(self, arg1: Iterable[tuple[Basic | complex, Basic | complex]], arg2: None=None, **kwargs: Any) -> Basic: ... + @overload + def subs(self, arg1: Basic | complex, arg2: Basic | complex, **kwargs: Any) -> Basic: ... + + def subs(self, arg1: Mapping[Basic | complex, Basic | complex] | Basic | complex, # type: ignore + arg2: Basic | complex | None = None, **kwargs: Any) -> Basic: + ... + + def simplify(self, **kwargs) -> Set: + assert False + + def evalf(self, n: int = 15, subs: dict[Basic, Basic | float] | None = None, + maxn: int = 100, chop: bool = False, strict: bool = False, + quad: str | None = None, verbose: bool = False) -> Set: + ... + + n = evalf + + @staticmethod + def _infimum_key(expr): + """ + Return infimum (if possible) else S.Infinity. + """ + try: + infimum = expr.inf + assert infimum.is_comparable + infimum = infimum.evalf() # issue #18505 + except (NotImplementedError, + AttributeError, AssertionError, ValueError): + infimum = S.Infinity + return infimum + + def union(self, other): + """ + Returns the union of ``self`` and ``other``. + + Examples + ======== + + As a shortcut it is possible to use the ``+`` operator: + + >>> from sympy import Interval, FiniteSet + >>> Interval(0, 1).union(Interval(2, 3)) + Union(Interval(0, 1), Interval(2, 3)) + >>> Interval(0, 1) + Interval(2, 3) + Union(Interval(0, 1), Interval(2, 3)) + >>> Interval(1, 2, True, True) + FiniteSet(2, 3) + Union({3}, Interval.Lopen(1, 2)) + + Similarly it is possible to use the ``-`` operator for set differences: + + >>> Interval(0, 2) - Interval(0, 1) + Interval.Lopen(1, 2) + >>> Interval(1, 3) - FiniteSet(2) + Union(Interval.Ropen(1, 2), Interval.Lopen(2, 3)) + + """ + return Union(self, other) + + def intersect(self, other): + """ + Returns the intersection of 'self' and 'other'. + + Examples + ======== + + >>> from sympy import Interval + + >>> Interval(1, 3).intersect(Interval(1, 2)) + Interval(1, 2) + + >>> from sympy import imageset, Lambda, symbols, S + >>> n, m = symbols('n m') + >>> a = imageset(Lambda(n, 2*n), S.Integers) + >>> a.intersect(imageset(Lambda(m, 2*m + 1), S.Integers)) + EmptySet + + """ + return Intersection(self, other) + + def intersection(self, other): + """ + Alias for :meth:`intersect()` + """ + return self.intersect(other) + + def is_disjoint(self, other): + """ + Returns True if ``self`` and ``other`` are disjoint. + + Examples + ======== + + >>> from sympy import Interval + >>> Interval(0, 2).is_disjoint(Interval(1, 2)) + False + >>> Interval(0, 2).is_disjoint(Interval(3, 4)) + True + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Disjoint_sets + """ + return self.intersect(other) == S.EmptySet + + def isdisjoint(self, other): + """ + Alias for :meth:`is_disjoint()` + """ + return self.is_disjoint(other) + + def complement(self, universe): + r""" + The complement of 'self' w.r.t the given universe. + + Examples + ======== + + >>> from sympy import Interval, S + >>> Interval(0, 1).complement(S.Reals) + Union(Interval.open(-oo, 0), Interval.open(1, oo)) + + >>> Interval(0, 1).complement(S.UniversalSet) + Complement(UniversalSet, Interval(0, 1)) + + """ + return Complement(universe, self) + + def _complement(self, other): + # this behaves as other - self + if isinstance(self, ProductSet) and isinstance(other, ProductSet): + # If self and other are disjoint then other - self == self + if len(self.sets) != len(other.sets): + return other + + # There can be other ways to represent this but this gives: + # (A x B) - (C x D) = ((A - C) x B) U (A x (B - D)) + overlaps = [] + pairs = list(zip(self.sets, other.sets)) + for n in range(len(pairs)): + sets = (o if i != n else o-s for i, (s, o) in enumerate(pairs)) + overlaps.append(ProductSet(*sets)) + return Union(*overlaps) + + elif isinstance(other, Interval): + if isinstance(self, (Interval, FiniteSet)): + return Intersection(other, self.complement(S.Reals)) + + elif isinstance(other, Union): + return Union(*(o - self for o in other.args)) + + elif isinstance(other, Complement): + return Complement(other.args[0], Union(other.args[1], self), evaluate=False) + + elif other is S.EmptySet: + return S.EmptySet + + elif isinstance(other, FiniteSet): + sifted = sift(other, lambda x: fuzzy_bool(self.contains(x))) + # ignore those that are contained in self + return Union(FiniteSet(*(sifted[False])), + Complement(FiniteSet(*(sifted[None])), self, evaluate=False) + if sifted[None] else S.EmptySet) + + def symmetric_difference(self, other): + """ + Returns symmetric difference of ``self`` and ``other``. + + Examples + ======== + + >>> from sympy import Interval, S + >>> Interval(1, 3).symmetric_difference(S.Reals) + Union(Interval.open(-oo, 1), Interval.open(3, oo)) + >>> Interval(1, 10).symmetric_difference(S.Reals) + Union(Interval.open(-oo, 1), Interval.open(10, oo)) + + >>> from sympy import S, EmptySet + >>> S.Reals.symmetric_difference(EmptySet) + Reals + + References + ========== + .. [1] https://en.wikipedia.org/wiki/Symmetric_difference + + """ + return SymmetricDifference(self, other) + + def _symmetric_difference(self, other): + return Union(Complement(self, other), Complement(other, self)) + + @property + def inf(self): + """ + The infimum of ``self``. + + Examples + ======== + + >>> from sympy import Interval, Union + >>> Interval(0, 1).inf + 0 + >>> Union(Interval(0, 1), Interval(2, 3)).inf + 0 + + """ + return self._inf + + @property + def _inf(self): + raise NotImplementedError("(%s)._inf" % self) + + @property + def sup(self): + """ + The supremum of ``self``. + + Examples + ======== + + >>> from sympy import Interval, Union + >>> Interval(0, 1).sup + 1 + >>> Union(Interval(0, 1), Interval(2, 3)).sup + 3 + + """ + return self._sup + + @property + def _sup(self): + raise NotImplementedError("(%s)._sup" % self) + + def contains(self, other): + """ + Returns a SymPy value indicating whether ``other`` is contained + in ``self``: ``true`` if it is, ``false`` if it is not, else + an unevaluated ``Contains`` expression (or, as in the case of + ConditionSet and a union of FiniteSet/Intervals, an expression + indicating the conditions for containment). + + Examples + ======== + + >>> from sympy import Interval, S + >>> from sympy.abc import x + + >>> Interval(0, 1).contains(0.5) + True + + As a shortcut it is possible to use the ``in`` operator, but that + will raise an error unless an affirmative true or false is not + obtained. + + >>> Interval(0, 1).contains(x) + (0 <= x) & (x <= 1) + >>> x in Interval(0, 1) + Traceback (most recent call last): + ... + TypeError: did not evaluate to a bool: None + + The result of 'in' is a bool, not a SymPy value + + >>> 1 in Interval(0, 2) + True + >>> _ is S.true + False + """ + from .contains import Contains + other = sympify(other, strict=True) + + c = self._contains(other) + if isinstance(c, Contains): + return c + if c is None: + return Contains(other, self, evaluate=False) + b = tfn[c] + if b is None: + return c + return b + + def _contains(self, other): + """Test if ``other`` is an element of the set ``self``. + + This is an internal method that is expected to be overridden by + subclasses of ``Set`` and will be called by the public + :func:`Set.contains` method or the :class:`Contains` expression. + + Parameters + ========== + + other: Sympified :class:`Basic` instance + The object whose membership in ``self`` is to be tested. + + Returns + ======= + + Symbolic :class:`Boolean` or ``None``. + + A return value of ``None`` indicates that it is unknown whether + ``other`` is contained in ``self``. Returning ``None`` from here + ensures that ``self.contains(other)`` or ``Contains(self, other)`` will + return an unevaluated :class:`Contains` expression. + + If not ``None`` then the returned value is a :class:`Boolean` that is + logically equivalent to the statement that ``other`` is an element of + ``self``. Usually this would be either ``S.true`` or ``S.false`` but + not always. + """ + raise NotImplementedError(f"{type(self).__name__}._contains") + + def is_subset(self, other): + """ + Returns True if ``self`` is a subset of ``other``. + + Examples + ======== + + >>> from sympy import Interval + >>> Interval(0, 0.5).is_subset(Interval(0, 1)) + True + >>> Interval(0, 1).is_subset(Interval(0, 1, left_open=True)) + False + + """ + if not isinstance(other, Set): + raise ValueError("Unknown argument '%s'" % other) + + # Handle the trivial cases + if self == other: + return True + is_empty = self.is_empty + if is_empty is True: + return True + elif fuzzy_not(is_empty) and other.is_empty: + return False + if self.is_finite_set is False and other.is_finite_set: + return False + + # Dispatch on subclass rules + ret = self._eval_is_subset(other) + if ret is not None: + return ret + ret = other._eval_is_superset(self) + if ret is not None: + return ret + + # Use pairwise rules from multiple dispatch + from sympy.sets.handlers.issubset import is_subset_sets + ret = is_subset_sets(self, other) + if ret is not None: + return ret + + # Fall back on computing the intersection + # XXX: We shouldn't do this. A query like this should be handled + # without evaluating new Set objects. It should be the other way round + # so that the intersect method uses is_subset for evaluation. + if self.intersect(other) == self: + return True + + def _eval_is_subset(self, other): + '''Returns a fuzzy bool for whether self is a subset of other.''' + return None + + def _eval_is_superset(self, other): + '''Returns a fuzzy bool for whether self is a subset of other.''' + return None + + # This should be deprecated: + def issubset(self, other): + """ + Alias for :meth:`is_subset()` + """ + return self.is_subset(other) + + def is_proper_subset(self, other): + """ + Returns True if ``self`` is a proper subset of ``other``. + + Examples + ======== + + >>> from sympy import Interval + >>> Interval(0, 0.5).is_proper_subset(Interval(0, 1)) + True + >>> Interval(0, 1).is_proper_subset(Interval(0, 1)) + False + + """ + if isinstance(other, Set): + return self != other and self.is_subset(other) + else: + raise ValueError("Unknown argument '%s'" % other) + + def is_superset(self, other): + """ + Returns True if ``self`` is a superset of ``other``. + + Examples + ======== + + >>> from sympy import Interval + >>> Interval(0, 0.5).is_superset(Interval(0, 1)) + False + >>> Interval(0, 1).is_superset(Interval(0, 1, left_open=True)) + True + + """ + if isinstance(other, Set): + return other.is_subset(self) + else: + raise ValueError("Unknown argument '%s'" % other) + + # This should be deprecated: + def issuperset(self, other): + """ + Alias for :meth:`is_superset()` + """ + return self.is_superset(other) + + def is_proper_superset(self, other): + """ + Returns True if ``self`` is a proper superset of ``other``. + + Examples + ======== + + >>> from sympy import Interval + >>> Interval(0, 1).is_proper_superset(Interval(0, 0.5)) + True + >>> Interval(0, 1).is_proper_superset(Interval(0, 1)) + False + + """ + if isinstance(other, Set): + return self != other and self.is_superset(other) + else: + raise ValueError("Unknown argument '%s'" % other) + + def _eval_powerset(self): + from .powerset import PowerSet + return PowerSet(self) + + def powerset(self): + """ + Find the Power set of ``self``. + + Examples + ======== + + >>> from sympy import EmptySet, FiniteSet, Interval + + A power set of an empty set: + + >>> A = EmptySet + >>> A.powerset() + {EmptySet} + + A power set of a finite set: + + >>> A = FiniteSet(1, 2) + >>> a, b, c = FiniteSet(1), FiniteSet(2), FiniteSet(1, 2) + >>> A.powerset() == FiniteSet(a, b, c, EmptySet) + True + + A power set of an interval: + + >>> Interval(1, 2).powerset() + PowerSet(Interval(1, 2)) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Power_set + + """ + return self._eval_powerset() + + @property + def measure(self): + """ + The (Lebesgue) measure of ``self``. + + Examples + ======== + + >>> from sympy import Interval, Union + >>> Interval(0, 1).measure + 1 + >>> Union(Interval(0, 1), Interval(2, 3)).measure + 2 + + """ + return self._measure + + @property + def kind(self): + """ + The kind of a Set + + Explanation + =========== + + Any :class:`Set` will have kind :class:`SetKind` which is + parametrised by the kind of the elements of the set. For example + most sets are sets of numbers and will have kind + ``SetKind(NumberKind)``. If elements of sets are different in kind than + their kind will ``SetKind(UndefinedKind)``. See + :class:`sympy.core.kind.Kind` for an explanation of the kind system. + + Examples + ======== + + >>> from sympy import Interval, Matrix, FiniteSet, EmptySet, ProductSet, PowerSet + + >>> FiniteSet(Matrix([1, 2])).kind + SetKind(MatrixKind(NumberKind)) + + >>> Interval(1, 2).kind + SetKind(NumberKind) + + >>> EmptySet.kind + SetKind() + + A :class:`sympy.sets.powerset.PowerSet` is a set of sets: + + >>> PowerSet({1, 2, 3}).kind + SetKind(SetKind(NumberKind)) + + A :class:`ProductSet` represents the set of tuples of elements of + other sets. Its kind is :class:`sympy.core.containers.TupleKind` + parametrised by the kinds of the elements of those sets: + + >>> p = ProductSet(FiniteSet(1, 2), FiniteSet(3, 4)) + >>> list(p) + [(1, 3), (2, 3), (1, 4), (2, 4)] + >>> p.kind + SetKind(TupleKind(NumberKind, NumberKind)) + + When all elements of the set do not have same kind, the kind + will be returned as ``SetKind(UndefinedKind)``: + + >>> FiniteSet(0, Matrix([1, 2])).kind + SetKind(UndefinedKind) + + The kind of the elements of a set are given by the ``element_kind`` + attribute of ``SetKind``: + + >>> Interval(1, 2).kind.element_kind + NumberKind + + See Also + ======== + + NumberKind + sympy.core.kind.UndefinedKind + sympy.core.containers.TupleKind + MatrixKind + sympy.matrices.expressions.sets.MatrixSet + sympy.sets.conditionset.ConditionSet + Rationals + Naturals + Integers + sympy.sets.fancysets.ImageSet + sympy.sets.fancysets.Range + sympy.sets.fancysets.ComplexRegion + sympy.sets.powerset.PowerSet + sympy.sets.sets.ProductSet + sympy.sets.sets.Interval + sympy.sets.sets.Union + sympy.sets.sets.Intersection + sympy.sets.sets.Complement + sympy.sets.sets.EmptySet + sympy.sets.sets.UniversalSet + sympy.sets.sets.FiniteSet + sympy.sets.sets.SymmetricDifference + sympy.sets.sets.DisjointUnion + """ + return self._kind() + + @property + def boundary(self): + """ + The boundary or frontier of a set. + + Explanation + =========== + + A point x is on the boundary of a set S if + + 1. x is in the closure of S. + I.e. Every neighborhood of x contains a point in S. + 2. x is not in the interior of S. + I.e. There does not exist an open set centered on x contained + entirely within S. + + There are the points on the outer rim of S. If S is open then these + points need not actually be contained within S. + + For example, the boundary of an interval is its start and end points. + This is true regardless of whether or not the interval is open. + + Examples + ======== + + >>> from sympy import Interval + >>> Interval(0, 1).boundary + {0, 1} + >>> Interval(0, 1, True, False).boundary + {0, 1} + """ + return self._boundary + + @property + def is_open(self): + """ + Property method to check whether a set is open. + + Explanation + =========== + + A set is open if and only if it has an empty intersection with its + boundary. In particular, a subset A of the reals is open if and only + if each one of its points is contained in an open interval that is a + subset of A. + + Examples + ======== + >>> from sympy import S + >>> S.Reals.is_open + True + >>> S.Rationals.is_open + False + """ + return Intersection(self, self.boundary).is_empty + + @property + def is_closed(self): + """ + A property method to check whether a set is closed. + + Explanation + =========== + + A set is closed if its complement is an open set. The closedness of a + subset of the reals is determined with respect to R and its standard + topology. + + Examples + ======== + >>> from sympy import Interval + >>> Interval(0, 1).is_closed + True + """ + return self.boundary.is_subset(self) + + @property + def closure(self): + """ + Property method which returns the closure of a set. + The closure is defined as the union of the set itself and its + boundary. + + Examples + ======== + >>> from sympy import S, Interval + >>> S.Reals.closure + Reals + >>> Interval(0, 1).closure + Interval(0, 1) + """ + return self + self.boundary + + @property + def interior(self): + """ + Property method which returns the interior of a set. + The interior of a set S consists all points of S that do not + belong to the boundary of S. + + Examples + ======== + >>> from sympy import Interval + >>> Interval(0, 1).interior + Interval.open(0, 1) + >>> Interval(0, 1).boundary.interior + EmptySet + """ + return self - self.boundary + + @property + def _boundary(self): + raise NotImplementedError() + + @property + def _measure(self): + raise NotImplementedError("(%s)._measure" % self) + + def _kind(self): + return SetKind(UndefinedKind) + + def _eval_evalf(self, prec): + dps = prec_to_dps(prec) + return self.func(*[arg.evalf(n=dps) for arg in self.args]) + + @sympify_return([('other', 'Set')], NotImplemented) + def __add__(self, other): + return self.union(other) + + @sympify_return([('other', 'Set')], NotImplemented) + def __or__(self, other): + return self.union(other) + + @sympify_return([('other', 'Set')], NotImplemented) + def __and__(self, other): + return self.intersect(other) + + @sympify_return([('other', 'Set')], NotImplemented) + def __mul__(self, other): + return ProductSet(self, other) + + @sympify_return([('other', 'Set')], NotImplemented) + def __xor__(self, other): + return SymmetricDifference(self, other) + + @sympify_return([('exp', Expr)], NotImplemented) + def __pow__(self, exp): + if not (exp.is_Integer and exp >= 0): + raise ValueError("%s: Exponent must be a positive Integer" % exp) + return ProductSet(*[self]*exp) + + @sympify_return([('other', 'Set')], NotImplemented) + def __sub__(self, other): + return Complement(self, other) + + def __contains__(self, other): + other = _sympify(other) + c = self._contains(other) + b = tfn[c] + if b is None: + # x in y must evaluate to T or F; to entertain a None + # result with Set use y.contains(x) + raise TypeError('did not evaluate to a bool: %r' % c) + return b + + +class ProductSet(Set): + """ + Represents a Cartesian Product of Sets. + + Explanation + =========== + + Returns a Cartesian product given several sets as either an iterable + or individual arguments. + + Can use ``*`` operator on any sets for convenient shorthand. + + Examples + ======== + + >>> from sympy import Interval, FiniteSet, ProductSet + >>> I = Interval(0, 5); S = FiniteSet(1, 2, 3) + >>> ProductSet(I, S) + ProductSet(Interval(0, 5), {1, 2, 3}) + + >>> (2, 2) in ProductSet(I, S) + True + + >>> Interval(0, 1) * Interval(0, 1) # The unit square + ProductSet(Interval(0, 1), Interval(0, 1)) + + >>> coin = FiniteSet('H', 'T') + >>> set(coin**2) + {(H, H), (H, T), (T, H), (T, T)} + + The Cartesian product is not commutative or associative e.g.: + + >>> I*S == S*I + False + >>> (I*I)*I == I*(I*I) + False + + Notes + ===== + + - Passes most operations down to the argument sets + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Cartesian_product + """ + is_ProductSet = True + + def __new__(cls, *sets, **assumptions): + if len(sets) == 1 and iterable(sets[0]) and not isinstance(sets[0], (Set, set)): + sympy_deprecation_warning( + """ +ProductSet(iterable) is deprecated. Use ProductSet(*iterable) instead. + """, + deprecated_since_version="1.5", + active_deprecations_target="deprecated-productset-iterable", + ) + sets = tuple(sets[0]) + + sets = [sympify(s) for s in sets] + + if not all(isinstance(s, Set) for s in sets): + raise TypeError("Arguments to ProductSet should be of type Set") + + # Nullary product of sets is *not* the empty set + if len(sets) == 0: + return FiniteSet(()) + + if S.EmptySet in sets: + return S.EmptySet + + return Basic.__new__(cls, *sets, **assumptions) + + @property + def sets(self): + return self.args + + def flatten(self): + def _flatten(sets): + for s in sets: + if s.is_ProductSet: + yield from _flatten(s.sets) + else: + yield s + return ProductSet(*_flatten(self.sets)) + + + + def _contains(self, element): + """ + ``in`` operator for ProductSets. + + Examples + ======== + + >>> from sympy import Interval + >>> (2, 3) in Interval(0, 5) * Interval(0, 5) + True + + >>> (10, 10) in Interval(0, 5) * Interval(0, 5) + False + + Passes operation on to constituent sets + """ + if element.is_Symbol: + return None + + if not isinstance(element, Tuple) or len(element) != len(self.sets): + return S.false + + return And(*[s.contains(e) for s, e in zip(self.sets, element)]) + + def as_relational(self, *symbols): + symbols = [_sympify(s) for s in symbols] + if len(symbols) != len(self.sets) or not all( + i.is_Symbol for i in symbols): + raise ValueError( + 'number of symbols must match the number of sets') + return And(*[s.as_relational(i) for s, i in zip(self.sets, symbols)]) + + @property + def _boundary(self): + return Union(*(ProductSet(*(b + b.boundary if i != j else b.boundary + for j, b in enumerate(self.sets))) + for i, a in enumerate(self.sets))) + + @property + def is_iterable(self): + """ + A property method which tests whether a set is iterable or not. + Returns True if set is iterable, otherwise returns False. + + Examples + ======== + + >>> from sympy import FiniteSet, Interval + >>> I = Interval(0, 1) + >>> A = FiniteSet(1, 2, 3, 4, 5) + >>> I.is_iterable + False + >>> A.is_iterable + True + + """ + return all(set.is_iterable for set in self.sets) + + def __iter__(self): + """ + A method which implements is_iterable property method. + If self.is_iterable returns True (both constituent sets are iterable), + then return the Cartesian Product. Otherwise, raise TypeError. + """ + return iproduct(*self.sets) + + @property + def is_empty(self): + return fuzzy_or(s.is_empty for s in self.sets) + + @property + def is_finite_set(self): + all_finite = fuzzy_and(s.is_finite_set for s in self.sets) + return fuzzy_or([self.is_empty, all_finite]) + + @property + def _measure(self): + measure = 1 + for s in self.sets: + measure *= s.measure + return measure + + def _kind(self): + return SetKind(TupleKind(*(i.kind.element_kind for i in self.args))) + + def __len__(self): + return reduce(lambda a, b: a*b, (len(s) for s in self.args)) + + def __bool__(self): + return all(self.sets) + + +class Interval(Set): + """ + Represents a real interval as a Set. + + Usage: + Returns an interval with end points ``start`` and ``end``. + + For ``left_open=True`` (default ``left_open`` is ``False``) the interval + will be open on the left. Similarly, for ``right_open=True`` the interval + will be open on the right. + + Examples + ======== + + >>> from sympy import Symbol, Interval + >>> Interval(0, 1) + Interval(0, 1) + >>> Interval.Ropen(0, 1) + Interval.Ropen(0, 1) + >>> Interval.Ropen(0, 1) + Interval.Ropen(0, 1) + >>> Interval.Lopen(0, 1) + Interval.Lopen(0, 1) + >>> Interval.open(0, 1) + Interval.open(0, 1) + + >>> a = Symbol('a', real=True) + >>> Interval(0, a) + Interval(0, a) + + Notes + ===== + - Only real end points are supported + - ``Interval(a, b)`` with $a > b$ will return the empty set + - Use the ``evalf()`` method to turn an Interval into an mpmath + ``mpi`` interval instance + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Interval_%28mathematics%29 + """ + is_Interval = True + + def __new__(cls, start, end, left_open=False, right_open=False): + + start = _sympify(start) + end = _sympify(end) + left_open = _sympify(left_open) + right_open = _sympify(right_open) + + if not all(isinstance(a, (type(true), type(false))) + for a in [left_open, right_open]): + raise NotImplementedError( + "left_open and right_open can have only true/false values, " + "got %s and %s" % (left_open, right_open)) + + # Only allow real intervals + if fuzzy_not(fuzzy_and(i.is_extended_real for i in (start, end, end-start))): + raise ValueError("Non-real intervals are not supported") + + # evaluate if possible + if is_lt(end, start): + return S.EmptySet + elif (end - start).is_negative: + return S.EmptySet + + if end == start and (left_open or right_open): + return S.EmptySet + if end == start and not (left_open or right_open): + if start is S.Infinity or start is S.NegativeInfinity: + return S.EmptySet + return FiniteSet(end) + + # Make sure infinite interval end points are open. + if start is S.NegativeInfinity: + left_open = true + if end is S.Infinity: + right_open = true + if start == S.Infinity or end == S.NegativeInfinity: + return S.EmptySet + + return Basic.__new__(cls, start, end, left_open, right_open) + + @property + def start(self): + """ + The left end point of the interval. + + This property takes the same value as the ``inf`` property. + + Examples + ======== + + >>> from sympy import Interval + >>> Interval(0, 1).start + 0 + + """ + return self._args[0] + + @property + def end(self): + """ + The right end point of the interval. + + This property takes the same value as the ``sup`` property. + + Examples + ======== + + >>> from sympy import Interval + >>> Interval(0, 1).end + 1 + + """ + return self._args[1] + + @property + def left_open(self): + """ + True if interval is left-open. + + Examples + ======== + + >>> from sympy import Interval + >>> Interval(0, 1, left_open=True).left_open + True + >>> Interval(0, 1, left_open=False).left_open + False + + """ + return self._args[2] + + @property + def right_open(self): + """ + True if interval is right-open. + + Examples + ======== + + >>> from sympy import Interval + >>> Interval(0, 1, right_open=True).right_open + True + >>> Interval(0, 1, right_open=False).right_open + False + + """ + return self._args[3] + + @classmethod + def open(cls, a, b): + """Return an interval including neither boundary.""" + return cls(a, b, True, True) + + @classmethod + def Lopen(cls, a, b): + """Return an interval not including the left boundary.""" + return cls(a, b, True, False) + + @classmethod + def Ropen(cls, a, b): + """Return an interval not including the right boundary.""" + return cls(a, b, False, True) + + @property + def _inf(self): + return self.start + + @property + def _sup(self): + return self.end + + @property + def left(self): + return self.start + + @property + def right(self): + return self.end + + @property + def is_empty(self): + if self.left_open or self.right_open: + cond = self.start >= self.end # One/both bounds open + else: + cond = self.start > self.end # Both bounds closed + return fuzzy_bool(cond) + + @property + def is_finite_set(self): + return self.measure.is_zero + + def _complement(self, other): + if other == S.Reals: + a = Interval(S.NegativeInfinity, self.start, + True, not self.left_open) + b = Interval(self.end, S.Infinity, not self.right_open, True) + return Union(a, b) + + if isinstance(other, FiniteSet): + nums = [m for m in other.args if m.is_number] + if nums == []: + return None + + return Set._complement(self, other) + + @property + def _boundary(self): + finite_points = [p for p in (self.start, self.end) + if abs(p) != S.Infinity] + return FiniteSet(*finite_points) + + def _contains(self, other): + if (not isinstance(other, Expr) or other is S.NaN + or other.is_real is False or other.has(S.ComplexInfinity)): + # if an expression has zoo it will be zoo or nan + # and neither of those is real + return false + + if self.start is S.NegativeInfinity and self.end is S.Infinity: + if other.is_real is not None: + return tfn[other.is_real] + + d = Dummy() + return self.as_relational(d).subs(d, other) + + def as_relational(self, x): + """Rewrite an interval in terms of inequalities and logic operators.""" + x = sympify(x) + if self.right_open: + right = x < self.end + else: + right = x <= self.end + if self.left_open: + left = self.start < x + else: + left = self.start <= x + return And(left, right) + + @property + def _measure(self): + return self.end - self.start + + def _kind(self): + return SetKind(NumberKind) + + def to_mpi(self, prec=53): + return mpi(mpf(self.start._eval_evalf(prec)), + mpf(self.end._eval_evalf(prec))) + + def _eval_evalf(self, prec): + return Interval(self.left._evalf(prec), self.right._evalf(prec), + left_open=self.left_open, right_open=self.right_open) + + def _is_comparable(self, other): + is_comparable = self.start.is_comparable + is_comparable &= self.end.is_comparable + is_comparable &= other.start.is_comparable + is_comparable &= other.end.is_comparable + + return is_comparable + + @property + def is_left_unbounded(self): + """Return ``True`` if the left endpoint is negative infinity. """ + return self.left is S.NegativeInfinity or self.left == Float("-inf") + + @property + def is_right_unbounded(self): + """Return ``True`` if the right endpoint is positive infinity. """ + return self.right is S.Infinity or self.right == Float("+inf") + + def _eval_Eq(self, other): + if not isinstance(other, Interval): + if isinstance(other, FiniteSet): + return false + elif isinstance(other, Set): + return None + return false + + +class Union(Set, LatticeOp): + """ + Represents a union of sets as a :class:`Set`. + + Examples + ======== + + >>> from sympy import Union, Interval + >>> Union(Interval(1, 2), Interval(3, 4)) + Union(Interval(1, 2), Interval(3, 4)) + + The Union constructor will always try to merge overlapping intervals, + if possible. For example: + + >>> Union(Interval(1, 2), Interval(2, 3)) + Interval(1, 3) + + See Also + ======== + + Intersection + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Union_%28set_theory%29 + """ + is_Union = True + + @property + def identity(self): + return S.EmptySet + + @property + def zero(self): + return S.UniversalSet + + def __new__(cls, *args, **kwargs): + evaluate = kwargs.get('evaluate', global_parameters.evaluate) + + # flatten inputs to merge intersections and iterables + args = _sympify(args) + + # Reduce sets using known rules + if evaluate: + args = list(cls._new_args_filter(args)) + return simplify_union(args) + + args = list(ordered(args, Set._infimum_key)) + + obj = Basic.__new__(cls, *args) + obj._argset = frozenset(args) + return obj + + @property + def args(self): + return self._args + + def _complement(self, universe): + # DeMorgan's Law + return Intersection(s.complement(universe) for s in self.args) + + @property + def _inf(self): + # We use Min so that sup is meaningful in combination with symbolic + # interval end points. + return Min(*[set.inf for set in self.args]) + + @property + def _sup(self): + # We use Max so that sup is meaningful in combination with symbolic + # end points. + return Max(*[set.sup for set in self.args]) + + @property + def is_empty(self): + return fuzzy_and(set.is_empty for set in self.args) + + @property + def is_finite_set(self): + return fuzzy_and(set.is_finite_set for set in self.args) + + @property + def _measure(self): + # Measure of a union is the sum of the measures of the sets minus + # the sum of their pairwise intersections plus the sum of their + # triple-wise intersections minus ... etc... + + # Sets is a collection of intersections and a set of elementary + # sets which made up those intersections (called "sos" for set of sets) + # An example element might of this list might be: + # ( {A,B,C}, A.intersect(B).intersect(C) ) + + # Start with just elementary sets ( ({A}, A), ({B}, B), ... ) + # Then get and subtract ( ({A,B}, (A int B), ... ) while non-zero + sets = [(FiniteSet(s), s) for s in self.args] + measure = 0 + parity = 1 + while sets: + # Add up the measure of these sets and add or subtract it to total + measure += parity * sum(inter.measure for sos, inter in sets) + + # For each intersection in sets, compute the intersection with every + # other set not already part of the intersection. + sets = ((sos + FiniteSet(newset), newset.intersect(intersection)) + for sos, intersection in sets for newset in self.args + if newset not in sos) + + # Clear out sets with no measure + sets = [(sos, inter) for sos, inter in sets if inter.measure != 0] + + # Clear out duplicates + sos_list = [] + sets_list = [] + for _set in sets: + if _set[0] in sos_list: + continue + else: + sos_list.append(_set[0]) + sets_list.append(_set) + sets = sets_list + + # Flip Parity - next time subtract/add if we added/subtracted here + parity *= -1 + return measure + + def _kind(self): + kinds = tuple(arg.kind for arg in self.args if arg is not S.EmptySet) + if not kinds: + return SetKind() + elif all(i == kinds[0] for i in kinds): + return kinds[0] + else: + return SetKind(UndefinedKind) + + @property + def _boundary(self): + def boundary_of_set(i): + """ The boundary of set i minus interior of all other sets """ + b = self.args[i].boundary + for j, a in enumerate(self.args): + if j != i: + b = b - a.interior + return b + return Union(*map(boundary_of_set, range(len(self.args)))) + + def _contains(self, other): + return Or(*[s.contains(other) for s in self.args]) + + def is_subset(self, other): + return fuzzy_and(s.is_subset(other) for s in self.args) + + def as_relational(self, symbol): + """Rewrite a Union in terms of equalities and logic operators. """ + if (len(self.args) == 2 and + all(isinstance(i, Interval) for i in self.args)): + # optimization to give 3 args as (x > 1) & (x < 5) & Ne(x, 3) + # instead of as 4, ((1 <= x) & (x < 3)) | ((x <= 5) & (3 < x)) + # XXX: This should be ideally be improved to handle any number of + # intervals and also not to assume that the intervals are in any + # particular sorted order. + a, b = self.args + if a.sup == b.inf and a.right_open and b.left_open: + mincond = symbol > a.inf if a.left_open else symbol >= a.inf + maxcond = symbol < b.sup if b.right_open else symbol <= b.sup + necond = Ne(symbol, a.sup) + return And(necond, mincond, maxcond) + return Or(*[i.as_relational(symbol) for i in self.args]) + + @property + def is_iterable(self): + return all(arg.is_iterable for arg in self.args) + + def __iter__(self): + return roundrobin(*(iter(arg) for arg in self.args)) + + +class Intersection(Set, LatticeOp): + """ + Represents an intersection of sets as a :class:`Set`. + + Examples + ======== + + >>> from sympy import Intersection, Interval + >>> Intersection(Interval(1, 3), Interval(2, 4)) + Interval(2, 3) + + We often use the .intersect method + + >>> Interval(1,3).intersect(Interval(2,4)) + Interval(2, 3) + + See Also + ======== + + Union + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Intersection_%28set_theory%29 + """ + is_Intersection = True + + @property + def identity(self): + return S.UniversalSet + + @property + def zero(self): + return S.EmptySet + + def __new__(cls, *args , evaluate=None): + if evaluate is None: + evaluate = global_parameters.evaluate + + # flatten inputs to merge intersections and iterables + args = list(ordered(set(_sympify(args)))) + + # Reduce sets using known rules + if evaluate: + args = list(cls._new_args_filter(args)) + return simplify_intersection(args) + + args = list(ordered(args, Set._infimum_key)) + + obj = Basic.__new__(cls, *args) + obj._argset = frozenset(args) + return obj + + @property + def args(self): + return self._args + + @property + def is_iterable(self): + return any(arg.is_iterable for arg in self.args) + + @property + def is_finite_set(self): + if fuzzy_or(arg.is_finite_set for arg in self.args): + return True + + def _kind(self): + kinds = tuple(arg.kind for arg in self.args if arg is not S.UniversalSet) + if not kinds: + return SetKind(UndefinedKind) + elif all(i == kinds[0] for i in kinds): + return kinds[0] + else: + return SetKind() + + @property + def _inf(self): + raise NotImplementedError() + + @property + def _sup(self): + raise NotImplementedError() + + def _contains(self, other): + return And(*[set.contains(other) for set in self.args]) + + def __iter__(self): + sets_sift = sift(self.args, lambda x: x.is_iterable) + + completed = False + candidates = sets_sift[True] + sets_sift[None] + + finite_candidates, others = [], [] + for candidate in candidates: + length = None + try: + length = len(candidate) + except TypeError: + others.append(candidate) + + if length is not None: + finite_candidates.append(candidate) + finite_candidates.sort(key=len) + + for s in finite_candidates + others: + other_sets = set(self.args) - {s} + other = Intersection(*other_sets, evaluate=False) + completed = True + for x in s: + try: + if x in other: + yield x + except TypeError: + completed = False + if completed: + return + + if not completed: + if not candidates: + raise TypeError("None of the constituent sets are iterable") + raise TypeError( + "The computation had not completed because of the " + "undecidable set membership is found in every candidates.") + + @staticmethod + def _handle_finite_sets(args): + '''Simplify intersection of one or more FiniteSets and other sets''' + + # First separate the FiniteSets from the others + fs_args, others = sift(args, lambda x: x.is_FiniteSet, binary=True) + + # Let the caller handle intersection of non-FiniteSets + if not fs_args: + return + + # Convert to Python sets and build the set of all elements + fs_sets = [set(fs) for fs in fs_args] + all_elements = reduce(lambda a, b: a | b, fs_sets, set()) + + # Extract elements that are definitely in or definitely not in the + # intersection. Here we check contains for all of args. + definite = set() + for e in all_elements: + inall = fuzzy_and(s.contains(e) for s in args) + if inall is True: + definite.add(e) + if inall is not None: + for s in fs_sets: + s.discard(e) + + # At this point all elements in all of fs_sets are possibly in the + # intersection. In some cases this is because they are definitely in + # the intersection of the finite sets but it's not clear if they are + # members of others. We might have {m, n}, {m}, and Reals where we + # don't know if m or n is real. We want to remove n here but it is + # possibly in because it might be equal to m. So what we do now is + # extract the elements that are definitely in the remaining finite + # sets iteratively until we end up with {n}, {}. At that point if we + # get any empty set all remaining elements are discarded. + + fs_elements = reduce(lambda a, b: a | b, fs_sets, set()) + + # Need fuzzy containment testing + fs_symsets = [FiniteSet(*s) for s in fs_sets] + + while fs_elements: + for e in fs_elements: + infs = fuzzy_and(s.contains(e) for s in fs_symsets) + if infs is True: + definite.add(e) + if infs is not None: + for n, s in enumerate(fs_sets): + # Update Python set and FiniteSet + if e in s: + s.remove(e) + fs_symsets[n] = FiniteSet(*s) + fs_elements.remove(e) + break + # If we completed the for loop without removing anything we are + # done so quit the outer while loop + else: + break + + # If any of the sets of remainder elements is empty then we discard + # all of them for the intersection. + if not all(fs_sets): + fs_sets = [set()] + + # Here we fold back the definitely included elements into each fs. + # Since they are definitely included they must have been members of + # each FiniteSet to begin with. We could instead fold these in with a + # Union at the end to get e.g. {3}|({x}&{y}) rather than {3,x}&{3,y}. + if definite: + fs_sets = [fs | definite for fs in fs_sets] + + if fs_sets == [set()]: + return S.EmptySet + + sets = [FiniteSet(*s) for s in fs_sets] + + # Any set in others is redundant if it contains all the elements that + # are in the finite sets so we don't need it in the Intersection + all_elements = reduce(lambda a, b: a | b, fs_sets, set()) + is_redundant = lambda o: all(fuzzy_bool(o.contains(e)) for e in all_elements) + others = [o for o in others if not is_redundant(o)] + + if others: + rest = Intersection(*others) + # XXX: Maybe this shortcut should be at the beginning. For large + # FiniteSets it could much more efficient to process the other + # sets first... + if rest is S.EmptySet: + return S.EmptySet + # Flatten the Intersection + if rest.is_Intersection: + sets.extend(rest.args) + else: + sets.append(rest) + + if len(sets) == 1: + return sets[0] + else: + return Intersection(*sets, evaluate=False) + + def as_relational(self, symbol): + """Rewrite an Intersection in terms of equalities and logic operators""" + return And(*[set.as_relational(symbol) for set in self.args]) + + +class Complement(Set): + r"""Represents the set difference or relative complement of a set with + another set. + + $$A - B = \{x \in A \mid x \notin B\}$$ + + + Examples + ======== + + >>> from sympy import Complement, FiniteSet + >>> Complement(FiniteSet(0, 1, 2), FiniteSet(1)) + {0, 2} + + See Also + ========= + + Intersection, Union + + References + ========== + + .. [1] https://mathworld.wolfram.com/ComplementSet.html + """ + + is_Complement = True + + def __new__(cls, a, b, evaluate=True): + a, b = map(_sympify, (a, b)) + if evaluate: + return Complement.reduce(a, b) + + return Basic.__new__(cls, a, b) + + @staticmethod + def reduce(A, B): + """ + Simplify a :class:`Complement`. + + """ + if B == S.UniversalSet or A.is_subset(B): + return S.EmptySet + + if isinstance(B, Union): + return Intersection(*(s.complement(A) for s in B.args)) + + result = B._complement(A) + if result is not None: + return result + else: + return Complement(A, B, evaluate=False) + + def _contains(self, other): + A = self.args[0] + B = self.args[1] + return And(A.contains(other), Not(B.contains(other))) + + def as_relational(self, symbol): + """Rewrite a complement in terms of equalities and logic + operators""" + A, B = self.args + + A_rel = A.as_relational(symbol) + B_rel = Not(B.as_relational(symbol)) + + return And(A_rel, B_rel) + + def _kind(self): + return self.args[0].kind + + @property + def is_iterable(self): + if self.args[0].is_iterable: + return True + + @property + def is_finite_set(self): + A, B = self.args + a_finite = A.is_finite_set + if a_finite is True: + return True + elif a_finite is False and B.is_finite_set: + return False + + def __iter__(self): + A, B = self.args + for a in A: + if a not in B: + yield a + else: + continue + + +class EmptySet(Set, metaclass=Singleton): + """ + Represents the empty set. The empty set is available as a singleton + as ``S.EmptySet``. + + Examples + ======== + + >>> from sympy import S, Interval + >>> S.EmptySet + EmptySet + + >>> Interval(1, 2).intersect(S.EmptySet) + EmptySet + + See Also + ======== + + UniversalSet + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Empty_set + """ + is_empty = True + is_finite_set = True + is_FiniteSet = True + + @property # type: ignore + @deprecated( + """ + The is_EmptySet attribute of Set objects is deprecated. + Use 's is S.EmptySet" or 's.is_empty' instead. + """, + deprecated_since_version="1.5", + active_deprecations_target="deprecated-is-emptyset", + ) + def is_EmptySet(self): + return True + + @property + def _measure(self): + return 0 + + def _contains(self, other): + return false + + def as_relational(self, symbol): + return false + + def __len__(self): + return 0 + + def __iter__(self): + return iter([]) + + def _eval_powerset(self): + return FiniteSet(self) + + @property + def _boundary(self): + return self + + def _complement(self, other): + return other + + def _kind(self): + return SetKind() + + def _symmetric_difference(self, other): + return other + + +class UniversalSet(Set, metaclass=Singleton): + """ + Represents the set of all things. + The universal set is available as a singleton as ``S.UniversalSet``. + + Examples + ======== + + >>> from sympy import S, Interval + >>> S.UniversalSet + UniversalSet + + >>> Interval(1, 2).intersect(S.UniversalSet) + Interval(1, 2) + + See Also + ======== + + EmptySet + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Universal_set + """ + + is_UniversalSet = True + is_empty = False + is_finite_set = False + + def _complement(self, other): + return S.EmptySet + + def _symmetric_difference(self, other): + return other + + @property + def _measure(self): + return S.Infinity + + def _kind(self): + return SetKind(UndefinedKind) + + def _contains(self, other): + return true + + def as_relational(self, symbol): + return true + + @property + def _boundary(self): + return S.EmptySet + + +class FiniteSet(Set): + """ + Represents a finite set of Sympy expressions. + + Examples + ======== + + >>> from sympy import FiniteSet, Symbol, Interval, Naturals0 + >>> FiniteSet(1, 2, 3, 4) + {1, 2, 3, 4} + >>> 3 in FiniteSet(1, 2, 3, 4) + True + >>> FiniteSet(1, (1, 2), Symbol('x')) + {1, x, (1, 2)} + >>> FiniteSet(Interval(1, 2), Naturals0, {1, 2}) + FiniteSet({1, 2}, Interval(1, 2), Naturals0) + >>> members = [1, 2, 3, 4] + >>> f = FiniteSet(*members) + >>> f + {1, 2, 3, 4} + >>> f - FiniteSet(2) + {1, 3, 4} + >>> f + FiniteSet(2, 5) + {1, 2, 3, 4, 5} + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Finite_set + """ + is_FiniteSet = True + is_iterable = True + is_empty = False + is_finite_set = True + + def __new__(cls, *args, **kwargs): + evaluate = kwargs.get('evaluate', global_parameters.evaluate) + if evaluate: + args = list(map(sympify, args)) + + if len(args) == 0: + return S.EmptySet + else: + args = list(map(sympify, args)) + + # keep the form of the first canonical arg + dargs = {} + for i in reversed(list(ordered(args))): + if i.is_Symbol: + dargs[i] = i + else: + try: + dargs[i.as_dummy()] = i + except TypeError: + # e.g. i = class without args like `Interval` + dargs[i] = i + _args_set = set(dargs.values()) + args = list(ordered(_args_set, Set._infimum_key)) + obj = Basic.__new__(cls, *args) + obj._args_set = _args_set + return obj + + + def __iter__(self): + return iter(self.args) + + def _complement(self, other): + if isinstance(other, Interval): + # Splitting in sub-intervals is only done for S.Reals; + # other cases that need splitting will first pass through + # Set._complement(). + nums, syms = [], [] + for m in self.args: + if m.is_number and m.is_real: + nums.append(m) + elif m.is_real == False: + pass # drop non-reals + else: + syms.append(m) # various symbolic expressions + if other == S.Reals and nums != []: + nums.sort() + intervals = [] # Build up a list of intervals between the elements + intervals += [Interval(S.NegativeInfinity, nums[0], True, True)] + for a, b in zip(nums[:-1], nums[1:]): + intervals.append(Interval(a, b, True, True)) # both open + intervals.append(Interval(nums[-1], S.Infinity, True, True)) + if syms != []: + return Complement(Union(*intervals, evaluate=False), + FiniteSet(*syms), evaluate=False) + else: + return Union(*intervals, evaluate=False) + elif nums == []: # no splitting necessary or possible: + if syms: + return Complement(other, FiniteSet(*syms), evaluate=False) + else: + return other + + elif isinstance(other, FiniteSet): + unk = [] + for i in self: + c = sympify(other.contains(i)) + if c is not S.true and c is not S.false: + unk.append(i) + unk = FiniteSet(*unk) + if unk == self: + return + not_true = [] + for i in other: + c = sympify(self.contains(i)) + if c is not S.true: + not_true.append(i) + return Complement(FiniteSet(*not_true), unk) + + return Set._complement(self, other) + + def _contains(self, other): + """ + Tests whether an element, other, is in the set. + + Explanation + =========== + + The actual test is for mathematical equality (as opposed to + syntactical equality). In the worst case all elements of the + set must be checked. + + Examples + ======== + + >>> from sympy import FiniteSet + >>> 1 in FiniteSet(1, 2) + True + >>> 5 in FiniteSet(1, 2) + False + + """ + if other in self._args_set: + return S.true + else: + # evaluate=True is needed to override evaluate=False context; + # we need Eq to do the evaluation + return Or(*[Eq(e, other, evaluate=True) for e in self.args]) + + def _eval_is_subset(self, other): + return fuzzy_and(other._contains(e) for e in self.args) + + @property + def _boundary(self): + return self + + @property + def _inf(self): + return Min(*self) + + @property + def _sup(self): + return Max(*self) + + @property + def measure(self): + return 0 + + def _kind(self): + if not self.args: + return SetKind() + elif all(i.kind == self.args[0].kind for i in self.args): + return SetKind(self.args[0].kind) + else: + return SetKind(UndefinedKind) + + def __len__(self): + return len(self.args) + + def as_relational(self, symbol): + """Rewrite a FiniteSet in terms of equalities and logic operators. """ + return Or(*[Eq(symbol, elem) for elem in self]) + + def compare(self, other): + return (hash(self) - hash(other)) + + def _eval_evalf(self, prec): + dps = prec_to_dps(prec) + return FiniteSet(*[elem.evalf(n=dps) for elem in self]) + + def _eval_simplify(self, **kwargs): + from sympy.simplify import simplify + return FiniteSet(*[simplify(elem, **kwargs) for elem in self]) + + @property + def _sorted_args(self): + return self.args + + def _eval_powerset(self): + return self.func(*[self.func(*s) for s in subsets(self.args)]) + + def _eval_rewrite_as_PowerSet(self, *args, **kwargs): + """Rewriting method for a finite set to a power set.""" + from .powerset import PowerSet + + is2pow = lambda n: bool(n and not n & (n - 1)) + if not is2pow(len(self)): + return None + + fs_test = lambda arg: isinstance(arg, Set) and arg.is_FiniteSet + if not all(fs_test(arg) for arg in args): + return None + + biggest = max(args, key=len) + for arg in subsets(biggest.args): + arg_set = FiniteSet(*arg) + if arg_set not in args: + return None + return PowerSet(biggest) + + def __ge__(self, other): + if not isinstance(other, Set): + raise TypeError("Invalid comparison of set with %s" % func_name(other)) + return other.is_subset(self) + + def __gt__(self, other): + if not isinstance(other, Set): + raise TypeError("Invalid comparison of set with %s" % func_name(other)) + return self.is_proper_superset(other) + + def __le__(self, other): + if not isinstance(other, Set): + raise TypeError("Invalid comparison of set with %s" % func_name(other)) + return self.is_subset(other) + + def __lt__(self, other): + if not isinstance(other, Set): + raise TypeError("Invalid comparison of set with %s" % func_name(other)) + return self.is_proper_subset(other) + + def __eq__(self, other): + if isinstance(other, (set, frozenset)): + return self._args_set == other + return super().__eq__(other) + + __hash__ : Callable[[Basic], Any] = Basic.__hash__ + +_sympy_converter[set] = lambda x: FiniteSet(*x) +_sympy_converter[frozenset] = lambda x: FiniteSet(*x) + + +class SymmetricDifference(Set): + """Represents the set of elements which are in either of the + sets and not in their intersection. + + Examples + ======== + + >>> from sympy import SymmetricDifference, FiniteSet + >>> SymmetricDifference(FiniteSet(1, 2, 3), FiniteSet(3, 4, 5)) + {1, 2, 4, 5} + + See Also + ======== + + Complement, Union + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Symmetric_difference + """ + + is_SymmetricDifference = True + + def __new__(cls, a, b, evaluate=True): + if evaluate: + return SymmetricDifference.reduce(a, b) + + return Basic.__new__(cls, a, b) + + @staticmethod + def reduce(A, B): + result = B._symmetric_difference(A) + if result is not None: + return result + else: + return SymmetricDifference(A, B, evaluate=False) + + def as_relational(self, symbol): + """Rewrite a symmetric_difference in terms of equalities and + logic operators""" + A, B = self.args + + A_rel = A.as_relational(symbol) + B_rel = B.as_relational(symbol) + + return Xor(A_rel, B_rel) + + @property + def is_iterable(self): + if all(arg.is_iterable for arg in self.args): + return True + + def __iter__(self): + + args = self.args + union = roundrobin(*(iter(arg) for arg in args)) + + for item in union: + count = 0 + for s in args: + if item in s: + count += 1 + + if count % 2 == 1: + yield item + + + +class DisjointUnion(Set): + """ Represents the disjoint union (also known as the external disjoint union) + of a finite number of sets. + + Examples + ======== + + >>> from sympy import DisjointUnion, FiniteSet, Interval, Union, Symbol + >>> A = FiniteSet(1, 2, 3) + >>> B = Interval(0, 5) + >>> DisjointUnion(A, B) + DisjointUnion({1, 2, 3}, Interval(0, 5)) + >>> DisjointUnion(A, B).rewrite(Union) + Union(ProductSet({1, 2, 3}, {0}), ProductSet(Interval(0, 5), {1})) + >>> C = FiniteSet(Symbol('x'), Symbol('y'), Symbol('z')) + >>> DisjointUnion(C, C) + DisjointUnion({x, y, z}, {x, y, z}) + >>> DisjointUnion(C, C).rewrite(Union) + ProductSet({x, y, z}, {0, 1}) + + References + ========== + + https://en.wikipedia.org/wiki/Disjoint_union + """ + + def __new__(cls, *sets): + dj_collection = [] + for set_i in sets: + if isinstance(set_i, Set): + dj_collection.append(set_i) + else: + raise TypeError("Invalid input: '%s', input args \ + to DisjointUnion must be Sets" % set_i) + obj = Basic.__new__(cls, *dj_collection) + return obj + + @property + def sets(self): + return self.args + + @property + def is_empty(self): + return fuzzy_and(s.is_empty for s in self.sets) + + @property + def is_finite_set(self): + all_finite = fuzzy_and(s.is_finite_set for s in self.sets) + return fuzzy_or([self.is_empty, all_finite]) + + @property + def is_iterable(self): + if self.is_empty: + return False + iter_flag = True + for set_i in self.sets: + if not set_i.is_empty: + iter_flag = iter_flag and set_i.is_iterable + return iter_flag + + def _eval_rewrite_as_Union(self, *sets, **kwargs): + """ + Rewrites the disjoint union as the union of (``set`` x {``i``}) + where ``set`` is the element in ``sets`` at index = ``i`` + """ + + dj_union = S.EmptySet + index = 0 + for set_i in sets: + if isinstance(set_i, Set): + cross = ProductSet(set_i, FiniteSet(index)) + dj_union = Union(dj_union, cross) + index = index + 1 + return dj_union + + def _contains(self, element): + """ + ``in`` operator for DisjointUnion + + Examples + ======== + + >>> from sympy import Interval, DisjointUnion + >>> D = DisjointUnion(Interval(0, 1), Interval(0, 2)) + >>> (0.5, 0) in D + True + >>> (0.5, 1) in D + True + >>> (1.5, 0) in D + False + >>> (1.5, 1) in D + True + + Passes operation on to constituent sets + """ + if not isinstance(element, Tuple) or len(element) != 2: + return S.false + + if not element[1].is_Integer: + return S.false + + if element[1] >= len(self.sets) or element[1] < 0: + return S.false + + return self.sets[element[1]]._contains(element[0]) + + def _kind(self): + if not self.args: + return SetKind() + elif all(i.kind == self.args[0].kind for i in self.args): + return self.args[0].kind + else: + return SetKind(UndefinedKind) + + def __iter__(self): + if self.is_iterable: + + iters = [] + for i, s in enumerate(self.sets): + iters.append(iproduct(s, {Integer(i)})) + + return iter(roundrobin(*iters)) + else: + raise ValueError("'%s' is not iterable." % self) + + def __len__(self): + """ + Returns the length of the disjoint union, i.e., the number of elements in the set. + + Examples + ======== + + >>> from sympy import FiniteSet, DisjointUnion, EmptySet + >>> D1 = DisjointUnion(FiniteSet(1, 2, 3, 4), EmptySet, FiniteSet(3, 4, 5)) + >>> len(D1) + 7 + >>> D2 = DisjointUnion(FiniteSet(3, 5, 7), EmptySet, FiniteSet(3, 5, 7)) + >>> len(D2) + 6 + >>> D3 = DisjointUnion(EmptySet, EmptySet) + >>> len(D3) + 0 + + Adds up the lengths of the constituent sets. + """ + + if self.is_finite_set: + size = 0 + for set in self.sets: + size += len(set) + return size + else: + raise ValueError("'%s' is not a finite set." % self) + + +def imageset(*args): + r""" + Return an image of the set under transformation ``f``. + + Explanation + =========== + + If this function cannot compute the image, it returns an + unevaluated ImageSet object. + + .. math:: + \{ f(x) \mid x \in \mathrm{self} \} + + Examples + ======== + + >>> from sympy import S, Interval, imageset, sin, Lambda + >>> from sympy.abc import x + + >>> imageset(x, 2*x, Interval(0, 2)) + Interval(0, 4) + + >>> imageset(lambda x: 2*x, Interval(0, 2)) + Interval(0, 4) + + >>> imageset(Lambda(x, sin(x)), Interval(-2, 1)) + ImageSet(Lambda(x, sin(x)), Interval(-2, 1)) + + >>> imageset(sin, Interval(-2, 1)) + ImageSet(Lambda(x, sin(x)), Interval(-2, 1)) + >>> imageset(lambda y: x + y, Interval(-2, 1)) + ImageSet(Lambda(y, x + y), Interval(-2, 1)) + + Expressions applied to the set of Integers are simplified + to show as few negatives as possible and linear expressions + are converted to a canonical form. If this is not desirable + then the unevaluated ImageSet should be used. + + >>> imageset(x, -2*x + 5, S.Integers) + ImageSet(Lambda(x, 2*x + 1), Integers) + + See Also + ======== + + sympy.sets.fancysets.ImageSet + + """ + from .fancysets import ImageSet + from .setexpr import set_function + + if len(args) < 2: + raise ValueError('imageset expects at least 2 args, got: %s' % len(args)) + + if isinstance(args[0], (Symbol, tuple)) and len(args) > 2: + f = Lambda(args[0], args[1]) + set_list = args[2:] + else: + f = args[0] + set_list = args[1:] + + if isinstance(f, Lambda): + pass + elif callable(f): + nargs = getattr(f, 'nargs', {}) + if nargs: + if len(nargs) != 1: + raise NotImplementedError(filldedent(''' + This function can take more than 1 arg + but the potentially complicated set input + has not been analyzed at this point to + know its dimensions. TODO + ''')) + N = nargs.args[0] + if N == 1: + s = 'x' + else: + s = [Symbol('x%i' % i) for i in range(1, N + 1)] + else: + s = inspect.signature(f).parameters + + dexpr = _sympify(f(*[Dummy() for i in s])) + var = tuple(uniquely_named_symbol( + Symbol(i), dexpr) for i in s) + f = Lambda(var, f(*var)) + else: + raise TypeError(filldedent(''' + expecting lambda, Lambda, or FunctionClass, + not \'%s\'.''' % func_name(f))) + + if any(not isinstance(s, Set) for s in set_list): + name = [func_name(s) for s in set_list] + raise ValueError( + 'arguments after mapping should be sets, not %s' % name) + + if len(set_list) == 1: + set = set_list[0] + try: + # TypeError if arg count != set dimensions + r = set_function(f, set) + if r is None: + raise TypeError + if not r: + return r + except TypeError: + r = ImageSet(f, set) + if isinstance(r, ImageSet): + f, set = r.args + + if f.variables[0] == f.expr: + return set + + if isinstance(set, ImageSet): + # XXX: Maybe this should just be: + # f2 = set.lambda + # fun = Lambda(f2.signature, f(*f2.expr)) + # return imageset(fun, *set.base_sets) + if len(set.lamda.variables) == 1 and len(f.variables) == 1: + x = set.lamda.variables[0] + y = f.variables[0] + return imageset( + Lambda(x, f.expr.subs(y, set.lamda.expr)), *set.base_sets) + + if r is not None: + return r + + return ImageSet(f, *set_list) + + +def is_function_invertible_in_set(func, setv): + """ + Checks whether function ``func`` is invertible when the domain is + restricted to set ``setv``. + """ + # Functions known to always be invertible: + if func in (exp, log): + return True + u = Dummy("u") + fdiff = func(u).diff(u) + # monotonous functions: + # TODO: check subsets (`func` in `setv`) + if (fdiff > 0) == True or (fdiff < 0) == True: + return True + # TODO: support more + return None + + +def simplify_union(args): + """ + Simplify a :class:`Union` using known rules. + + Explanation + =========== + + We first start with global rules like 'Merge all FiniteSets' + + Then we iterate through all pairs and ask the constituent sets if they + can simplify themselves with any other constituent. This process depends + on ``union_sets(a, b)`` functions. + """ + from sympy.sets.handlers.union import union_sets + + # ===== Global Rules ===== + if not args: + return S.EmptySet + + for arg in args: + if not isinstance(arg, Set): + raise TypeError("Input args to Union must be Sets") + + # Merge all finite sets + finite_sets = [x for x in args if x.is_FiniteSet] + if len(finite_sets) > 1: + a = (x for set in finite_sets for x in set) + finite_set = FiniteSet(*a) + args = [finite_set] + [x for x in args if not x.is_FiniteSet] + + # ===== Pair-wise Rules ===== + # Here we depend on rules built into the constituent sets + args = set(args) + new_args = True + while new_args: + for s in args: + new_args = False + for t in args - {s}: + new_set = union_sets(s, t) + # This returns None if s does not know how to intersect + # with t. Returns the newly intersected set otherwise + if new_set is not None: + if not isinstance(new_set, set): + new_set = {new_set} + new_args = (args - {s, t}).union(new_set) + break + if new_args: + args = new_args + break + + if len(args) == 1: + return args.pop() + else: + return Union(*args, evaluate=False) + + +def simplify_intersection(args): + """ + Simplify an intersection using known rules. + + Explanation + =========== + + We first start with global rules like + 'if any empty sets return empty set' and 'distribute any unions' + + Then we iterate through all pairs and ask the constituent sets if they + can simplify themselves with any other constituent + """ + + # ===== Global Rules ===== + if not args: + return S.UniversalSet + + for arg in args: + if not isinstance(arg, Set): + raise TypeError("Input args to Union must be Sets") + + # If any EmptySets return EmptySet + if S.EmptySet in args: + return S.EmptySet + + # Handle Finite sets + rv = Intersection._handle_finite_sets(args) + + if rv is not None: + return rv + + # If any of the sets are unions, return a Union of Intersections + for s in args: + if s.is_Union: + other_sets = set(args) - {s} + if len(other_sets) > 0: + other = Intersection(*other_sets) + return Union(*(Intersection(arg, other) for arg in s.args)) + else: + return Union(*s.args) + + for s in args: + if s.is_Complement: + args.remove(s) + other_sets = args + [s.args[0]] + return Complement(Intersection(*other_sets), s.args[1]) + + from sympy.sets.handlers.intersection import intersection_sets + + # At this stage we are guaranteed not to have any + # EmptySets, FiniteSets, or Unions in the intersection + + # ===== Pair-wise Rules ===== + # Here we depend on rules built into the constituent sets + args = set(args) + new_args = True + while new_args: + for s in args: + new_args = False + for t in args - {s}: + new_set = intersection_sets(s, t) + # This returns None if s does not know how to intersect + # with t. Returns the newly intersected set otherwise + + if new_set is not None: + new_args = (args - {s, t}).union({new_set}) + break + if new_args: + args = new_args + break + + if len(args) == 1: + return args.pop() + else: + return Intersection(*args, evaluate=False) + + +def _handle_finite_sets(op, x, y, commutative): + # Handle finite sets: + fs_args, other = sift([x, y], lambda x: isinstance(x, FiniteSet), binary=True) + if len(fs_args) == 2: + return FiniteSet(*[op(i, j) for i in fs_args[0] for j in fs_args[1]]) + elif len(fs_args) == 1: + sets = [_apply_operation(op, other[0], i, commutative) for i in fs_args[0]] + return Union(*sets) + else: + return None + + +def _apply_operation(op, x, y, commutative): + from .fancysets import ImageSet + d = Dummy('d') + + out = _handle_finite_sets(op, x, y, commutative) + if out is None: + out = op(x, y) + + if out is None and commutative: + out = op(y, x) + if out is None: + _x, _y = symbols("x y") + if isinstance(x, Set) and not isinstance(y, Set): + out = ImageSet(Lambda(d, op(d, y)), x).doit() + elif not isinstance(x, Set) and isinstance(y, Set): + out = ImageSet(Lambda(d, op(x, d)), y).doit() + else: + out = ImageSet(Lambda((_x, _y), op(_x, _y)), x, y) + return out + + +def set_add(x, y): + from sympy.sets.handlers.add import _set_add + return _apply_operation(_set_add, x, y, commutative=True) + + +def set_sub(x, y): + from sympy.sets.handlers.add import _set_sub + return _apply_operation(_set_sub, x, y, commutative=False) + + +def set_mul(x, y): + from sympy.sets.handlers.mul import _set_mul + return _apply_operation(_set_mul, x, y, commutative=True) + + +def set_div(x, y): + from sympy.sets.handlers.mul import _set_div + return _apply_operation(_set_div, x, y, commutative=False) + + +def set_pow(x, y): + from sympy.sets.handlers.power import _set_pow + return _apply_operation(_set_pow, x, y, commutative=False) + + +def set_function(f, x): + from sympy.sets.handlers.functions import _set_function + return _set_function(f, x) + + +class SetKind(Kind): + """ + SetKind is kind for all Sets + + Every instance of Set will have kind ``SetKind`` parametrised by the kind + of the elements of the ``Set``. The kind of the elements might be + ``NumberKind``, or ``TupleKind`` or something else. When not all elements + have the same kind then the kind of the elements will be given as + ``UndefinedKind``. + + Parameters + ========== + + element_kind: Kind (optional) + The kind of the elements of the set. In a well defined set all elements + will have the same kind. Otherwise the kind should + :class:`sympy.core.kind.UndefinedKind`. The ``element_kind`` argument is optional but + should only be omitted in the case of ``EmptySet`` whose kind is simply + ``SetKind()`` + + Examples + ======== + + >>> from sympy import Interval + >>> Interval(1, 2).kind + SetKind(NumberKind) + >>> Interval(1,2).kind.element_kind + NumberKind + + See Also + ======== + + sympy.core.kind.NumberKind + sympy.matrices.kind.MatrixKind + sympy.core.containers.TupleKind + """ + def __new__(cls, element_kind=None): + obj = super().__new__(cls, element_kind) + obj.element_kind = element_kind + return obj + + def __repr__(self): + if not self.element_kind: + return "SetKind()" + else: + return "SetKind(%s)" % self.element_kind diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/test_conditionset.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/test_conditionset.py new file mode 100644 index 0000000000000000000000000000000000000000..4818246f306afd46a09a2cbea1faab858a9e7806 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/test_conditionset.py @@ -0,0 +1,294 @@ +from sympy.core.expr import unchanged +from sympy.sets import (ConditionSet, Intersection, FiniteSet, + EmptySet, Union, Contains, ImageSet) +from sympy.sets.sets import SetKind +from sympy.core.function import (Function, Lambda) +from sympy.core.mod import Mod +from sympy.core.kind import NumberKind +from sympy.core.numbers import (oo, pi) +from sympy.core.relational import (Eq, Ne) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.trigonometric import (asin, sin) +from sympy.logic.boolalg import And +from sympy.matrices.dense import Matrix +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.sets.sets import Interval +from sympy.testing.pytest import raises, warns_deprecated_sympy + + +w = Symbol('w') +x = Symbol('x') +y = Symbol('y') +z = Symbol('z') +f = Function('f') + + +def test_CondSet(): + sin_sols_principal = ConditionSet(x, Eq(sin(x), 0), + Interval(0, 2*pi, False, True)) + assert pi in sin_sols_principal + assert pi/2 not in sin_sols_principal + assert 3*pi not in sin_sols_principal + assert oo not in sin_sols_principal + assert 5 in ConditionSet(x, x**2 > 4, S.Reals) + assert 1 not in ConditionSet(x, x**2 > 4, S.Reals) + # in this case, 0 is not part of the base set so + # it can't be in any subset selected by the condition + assert 0 not in ConditionSet(x, y > 5, Interval(1, 7)) + # since 'in' requires a true/false, the following raises + # an error because the given value provides no information + # for the condition to evaluate (since the condition does + # not depend on the dummy symbol): the result is `y > 5`. + # In this case, ConditionSet is just acting like + # Piecewise((Interval(1, 7), y > 5), (S.EmptySet, True)). + raises(TypeError, lambda: 6 in ConditionSet(x, y > 5, + Interval(1, 7))) + + X = MatrixSymbol('X', 2, 2) + matrix_set = ConditionSet(X, Eq(X*Matrix([[1, 1], [1, 1]]), X)) + Y = Matrix([[0, 0], [0, 0]]) + assert matrix_set.contains(Y).doit() is S.true + Z = Matrix([[1, 2], [3, 4]]) + assert matrix_set.contains(Z).doit() is S.false + + assert isinstance(ConditionSet(x, x < 1, {x, y}).base_set, + FiniteSet) + raises(TypeError, lambda: ConditionSet(x, x + 1, {x, y})) + raises(TypeError, lambda: ConditionSet(x, x, 1)) + + I = S.Integers + U = S.UniversalSet + C = ConditionSet + assert C(x, False, I) is S.EmptySet + assert C(x, True, I) is I + assert C(x, x < 1, C(x, x < 2, I) + ) == C(x, (x < 1) & (x < 2), I) + assert C(y, y < 1, C(x, y < 2, I) + ) == C(x, (x < 1) & (y < 2), I), C(y, y < 1, C(x, y < 2, I)) + assert C(y, y < 1, C(x, x < 2, I) + ) == C(y, (y < 1) & (y < 2), I) + assert C(y, y < 1, C(x, y < x, I) + ) == C(x, (x < 1) & (y < x), I) + assert unchanged(C, y, x < 1, C(x, y < x, I)) + assert ConditionSet(x, x < 1).base_set is U + # arg checking is not done at instantiation but this + # will raise an error when containment is tested + assert ConditionSet((x,), x < 1).base_set is U + + c = ConditionSet((x, y), x < y, I**2) + assert (1, 2) in c + assert (1, pi) not in c + + raises(TypeError, lambda: C(x, x > 1, C((x, y), x > 1, I**2))) + # signature mismatch since only 3 args are accepted + raises(TypeError, lambda: C((x, y), x + y < 2, U, U)) + + +def test_CondSet_intersect(): + input_conditionset = ConditionSet(x, x**2 > 4, Interval(1, 4, False, + False)) + other_domain = Interval(0, 3, False, False) + output_conditionset = ConditionSet(x, x**2 > 4, Interval( + 1, 3, False, False)) + assert Intersection(input_conditionset, other_domain + ) == output_conditionset + + +def test_issue_9849(): + assert ConditionSet(x, Eq(x, x), S.Naturals + ) is S.Naturals + assert ConditionSet(x, Eq(Abs(sin(x)), -1), S.Naturals + ) == S.EmptySet + + +def test_simplified_FiniteSet_in_CondSet(): + assert ConditionSet(x, And(x < 1, x > -3), FiniteSet(0, 1, 2) + ) == FiniteSet(0) + assert ConditionSet(x, x < 0, FiniteSet(0, 1, 2)) == EmptySet + assert ConditionSet(x, And(x < -3), EmptySet) == EmptySet + y = Symbol('y') + assert (ConditionSet(x, And(x > 0), FiniteSet(-1, 0, 1, y)) == + Union(FiniteSet(1), ConditionSet(x, And(x > 0), FiniteSet(y)))) + assert (ConditionSet(x, Eq(Mod(x, 3), 1), FiniteSet(1, 4, 2, y)) == + Union(FiniteSet(1, 4), ConditionSet(x, Eq(Mod(x, 3), 1), + FiniteSet(y)))) + + +def test_free_symbols(): + assert ConditionSet(x, Eq(y, 0), FiniteSet(z) + ).free_symbols == {y, z} + assert ConditionSet(x, Eq(x, 0), FiniteSet(z) + ).free_symbols == {z} + assert ConditionSet(x, Eq(x, 0), FiniteSet(x, z) + ).free_symbols == {x, z} + assert ConditionSet(x, Eq(x, 0), ImageSet(Lambda(y, y**2), + S.Integers)).free_symbols == set() + + +def test_bound_symbols(): + assert ConditionSet(x, Eq(y, 0), FiniteSet(z) + ).bound_symbols == [x] + assert ConditionSet(x, Eq(x, 0), FiniteSet(x, y) + ).bound_symbols == [x] + assert ConditionSet(x, x < 10, ImageSet(Lambda(y, y**2), S.Integers) + ).bound_symbols == [x] + assert ConditionSet(x, x < 10, ConditionSet(y, y > 1, S.Integers) + ).bound_symbols == [x] + + +def test_as_dummy(): + _0, _1 = symbols('_0 _1') + assert ConditionSet(x, x < 1, Interval(y, oo) + ).as_dummy() == ConditionSet(_0, _0 < 1, Interval(y, oo)) + assert ConditionSet(x, x < 1, Interval(x, oo) + ).as_dummy() == ConditionSet(_0, _0 < 1, Interval(x, oo)) + assert ConditionSet(x, x < 1, ImageSet(Lambda(y, y**2), S.Integers) + ).as_dummy() == ConditionSet( + _0, _0 < 1, ImageSet(Lambda(_0, _0**2), S.Integers)) + e = ConditionSet((x, y), x <= y, S.Reals**2) + assert e.bound_symbols == [x, y] + assert e.as_dummy() == ConditionSet((_0, _1), _0 <= _1, S.Reals**2) + assert e.as_dummy() == ConditionSet((y, x), y <= x, S.Reals**2 + ).as_dummy() + + +def test_subs_CondSet(): + s = FiniteSet(z, y) + c = ConditionSet(x, x < 2, s) + assert c.subs(x, y) == c + assert c.subs(z, y) == ConditionSet(x, x < 2, FiniteSet(y)) + assert c.xreplace({x: y}) == ConditionSet(y, y < 2, s) + + assert ConditionSet(x, x < y, s + ).subs(y, w) == ConditionSet(x, x < w, s.subs(y, w)) + # if the user uses assumptions that cause the condition + # to evaluate, that can't be helped from SymPy's end + n = Symbol('n', negative=True) + assert ConditionSet(n, 0 < n, S.Integers) is S.EmptySet + p = Symbol('p', positive=True) + assert ConditionSet(n, n < y, S.Integers + ).subs(n, x) == ConditionSet(n, n < y, S.Integers) + raises(ValueError, lambda: ConditionSet( + x + 1, x < 1, S.Integers)) + assert ConditionSet( + p, n < x, Interval(-5, 5)).subs(x, p) == Interval(-5, 5), ConditionSet( + p, n < x, Interval(-5, 5)).subs(x, p) + assert ConditionSet( + n, n < x, Interval(-oo, 0)).subs(x, p + ) == Interval(-oo, 0) + + assert ConditionSet(f(x), f(x) < 1, {w, z} + ).subs(f(x), y) == ConditionSet(f(x), f(x) < 1, {w, z}) + + # issue 17341 + k = Symbol('k') + img1 = ImageSet(Lambda(k, 2*k*pi + asin(y)), S.Integers) + img2 = ImageSet(Lambda(k, 2*k*pi + asin(S.One/3)), S.Integers) + assert ConditionSet(x, Contains( + y, Interval(-1,1)), img1).subs(y, S.One/3).dummy_eq(img2) + + assert (0, 1) in ConditionSet((x, y), x + y < 3, S.Integers**2) + + raises(TypeError, lambda: ConditionSet(n, n < -10, Interval(0, 10))) + + +def test_subs_CondSet_tebr(): + with warns_deprecated_sympy(): + assert ConditionSet((x, y), {x + 1, x + y}, S.Reals**2) == \ + ConditionSet((x, y), Eq(x + 1, 0) & Eq(x + y, 0), S.Reals**2) + + +def test_dummy_eq(): + C = ConditionSet + I = S.Integers + c = C(x, x < 1, I) + assert c.dummy_eq(C(y, y < 1, I)) + assert c.dummy_eq(1) == False + assert c.dummy_eq(C(x, x < 1, S.Reals)) == False + + c1 = ConditionSet((x, y), Eq(x + 1, 0) & Eq(x + y, 0), S.Reals**2) + c2 = ConditionSet((x, y), Eq(x + 1, 0) & Eq(x + y, 0), S.Reals**2) + c3 = ConditionSet((x, y), Eq(x + 1, 0) & Eq(x + y, 0), S.Complexes**2) + assert c1.dummy_eq(c2) + assert c1.dummy_eq(c3) is False + assert c.dummy_eq(c1) is False + assert c1.dummy_eq(c) is False + + # issue 19496 + m = Symbol('m') + n = Symbol('n') + a = Symbol('a') + d1 = ImageSet(Lambda(m, m*pi), S.Integers) + d2 = ImageSet(Lambda(n, n*pi), S.Integers) + c1 = ConditionSet(x, Ne(a, 0), d1) + c2 = ConditionSet(x, Ne(a, 0), d2) + assert c1.dummy_eq(c2) + + +def test_contains(): + assert 6 in ConditionSet(x, x > 5, Interval(1, 7)) + assert (8 in ConditionSet(x, y > 5, Interval(1, 7))) is False + # `in` should give True or False; in this case there is not + # enough information for that result + raises(TypeError, + lambda: 6 in ConditionSet(x, y > 5, Interval(1, 7))) + # here, there is enough information but the comparison is + # not defined + raises(TypeError, lambda: 0 in ConditionSet(x, 1/x >= 0, S.Reals)) + assert ConditionSet(x, y > 5, Interval(1, 7) + ).contains(6) == (y > 5) + assert ConditionSet(x, y > 5, Interval(1, 7) + ).contains(8) is S.false + assert ConditionSet(x, y > 5, Interval(1, 7) + ).contains(w) == And(Contains(w, Interval(1, 7)), y > 5) + # This returns an unevaluated Contains object + # because 1/0 should not be defined for 1 and 0 in the context of + # reals. + assert ConditionSet(x, 1/x >= 0, S.Reals).contains(0) == \ + Contains(0, ConditionSet(x, 1/x >= 0, S.Reals), evaluate=False) + c = ConditionSet((x, y), x + y > 1, S.Integers**2) + assert not c.contains(1) + assert c.contains((2, 1)) + assert not c.contains((0, 1)) + c = ConditionSet((w, (x, y)), w + x + y > 1, S.Integers*S.Integers**2) + assert not c.contains(1) + assert not c.contains((1, 2)) + assert not c.contains(((1, 2), 3)) + assert not c.contains(((1, 2), (3, 4))) + assert c.contains((1, (3, 4))) + + +def test_as_relational(): + assert ConditionSet((x, y), x > 1, S.Integers**2).as_relational((x, y) + ) == (x > 1) & Contains(x, S.Integers) & Contains(y, S.Integers) + assert ConditionSet(x, x > 1, S.Integers).as_relational(x + ) == Contains(x, S.Integers) & (x > 1) + + +def test_flatten(): + """Tests whether there is basic denesting functionality""" + inner = ConditionSet(x, sin(x) + x > 0) + outer = ConditionSet(x, Contains(x, inner), S.Reals) + assert outer == ConditionSet(x, sin(x) + x > 0, S.Reals) + + inner = ConditionSet(y, sin(y) + y > 0) + outer = ConditionSet(x, Contains(y, inner), S.Reals) + assert outer != ConditionSet(x, sin(x) + x > 0, S.Reals) + + inner = ConditionSet(x, sin(x) + x > 0).intersect(Interval(-1, 1)) + outer = ConditionSet(x, Contains(x, inner), S.Reals) + assert outer == ConditionSet(x, sin(x) + x > 0, Interval(-1, 1)) + + +def test_duplicate(): + from sympy.core.function import BadSignatureError + # test coverage for line 95 in conditionset.py, check for duplicates in symbols + dup = symbols('a,a') + raises(BadSignatureError, lambda: ConditionSet(dup, x < 0)) + + +def test_SetKind_ConditionSet(): + assert ConditionSet(x, Eq(sin(x), 0), Interval(0, 2*pi)).kind is SetKind(NumberKind) + assert ConditionSet(x, x < 0).kind is SetKind(NumberKind) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/test_contains.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/test_contains.py new file mode 100644 index 0000000000000000000000000000000000000000..bb6b98940946f98bf377aad6810f5b32eb6dd069 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/test_contains.py @@ -0,0 +1,52 @@ +from sympy.core.expr import unchanged +from sympy.core.numbers import oo +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.sets.contains import Contains +from sympy.sets.sets import (FiniteSet, Interval) +from sympy.testing.pytest import raises + + +def test_contains_basic(): + raises(TypeError, lambda: Contains(S.Integers, 1)) + assert Contains(2, S.Integers) is S.true + assert Contains(-2, S.Naturals) is S.false + + i = Symbol('i', integer=True) + assert Contains(i, S.Naturals) == Contains(i, S.Naturals, evaluate=False) + + +def test_issue_6194(): + x = Symbol('x') + assert unchanged(Contains, x, Interval(0, 1)) + assert Interval(0, 1).contains(x) == (S.Zero <= x) & (x <= 1) + assert Contains(x, FiniteSet(0)) != S.false + assert Contains(x, Interval(1, 1)) != S.false + assert Contains(x, S.Integers) != S.false + + +def test_issue_10326(): + assert Contains(oo, Interval(-oo, oo)) == False + assert Contains(-oo, Interval(-oo, oo)) == False + + +def test_binary_symbols(): + x = Symbol('x') + y = Symbol('y') + z = Symbol('z') + assert Contains(x, FiniteSet(y, Eq(z, True)) + ).binary_symbols == {y, z} + + +def test_as_set(): + x = Symbol('x') + y = Symbol('y') + assert Contains(x, FiniteSet(y)).as_set() == FiniteSet(y) + assert Contains(x, S.Integers).as_set() == S.Integers + assert Contains(x, S.Reals).as_set() == S.Reals + + +def test_type_error(): + # Pass in a parameter not of type "set" + raises(TypeError, lambda: Contains(2, None)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/test_fancysets.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/test_fancysets.py new file mode 100644 index 0000000000000000000000000000000000000000..b23c2a99fce0af5bfe7c667185465ee417de19ce --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/test_fancysets.py @@ -0,0 +1,1313 @@ + +from sympy.core.expr import unchanged +from sympy.sets.contains import Contains +from sympy.sets.fancysets import (ImageSet, Range, normalize_theta_set, + ComplexRegion) +from sympy.sets.sets import (FiniteSet, Interval, Union, imageset, + Intersection, ProductSet, SetKind) +from sympy.sets.conditionset import ConditionSet +from sympy.simplify.simplify import simplify +from sympy.core.basic import Basic +from sympy.core.containers import Tuple, TupleKind +from sympy.core.function import Lambda +from sympy.core.kind import NumberKind +from sympy.core.numbers import (I, Rational, oo, pi) +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol, symbols) +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.integers import floor +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin, tan) +from sympy.logic.boolalg import And +from sympy.matrices.dense import eye +from sympy.testing.pytest import XFAIL, raises +from sympy.abc import x, y, t, z +from sympy.core.mod import Mod + +import itertools + + +def test_naturals(): + N = S.Naturals + assert 5 in N + assert -5 not in N + assert 5.5 not in N + ni = iter(N) + a, b, c, d = next(ni), next(ni), next(ni), next(ni) + assert (a, b, c, d) == (1, 2, 3, 4) + assert isinstance(a, Basic) + + assert N.intersect(Interval(-5, 5)) == Range(1, 6) + assert N.intersect(Interval(-5, 5, True, True)) == Range(1, 5) + + assert N.boundary == N + assert N.is_open == False + assert N.is_closed == True + + assert N.inf == 1 + assert N.sup is oo + assert not N.contains(oo) + for s in (S.Naturals0, S.Naturals): + assert s.intersection(S.Reals) is s + assert s.is_subset(S.Reals) + + assert N.as_relational(x) == And(Eq(floor(x), x), x >= 1, x < oo) + + +def test_naturals0(): + N = S.Naturals0 + assert 0 in N + assert -1 not in N + assert next(iter(N)) == 0 + assert not N.contains(oo) + assert N.contains(sin(x)) == Contains(sin(x), N) + + +def test_integers(): + Z = S.Integers + assert 5 in Z + assert -5 in Z + assert 5.5 not in Z + assert not Z.contains(oo) + assert not Z.contains(-oo) + + zi = iter(Z) + a, b, c, d = next(zi), next(zi), next(zi), next(zi) + assert (a, b, c, d) == (0, 1, -1, 2) + assert isinstance(a, Basic) + + assert Z.intersect(Interval(-5, 5)) == Range(-5, 6) + assert Z.intersect(Interval(-5, 5, True, True)) == Range(-4, 5) + assert Z.intersect(Interval(5, S.Infinity)) == Range(5, S.Infinity) + assert Z.intersect(Interval.Lopen(5, S.Infinity)) == Range(6, S.Infinity) + + assert Z.inf is -oo + assert Z.sup is oo + + assert Z.boundary == Z + assert Z.is_open == False + assert Z.is_closed == True + + assert Z.as_relational(x) == And(Eq(floor(x), x), -oo < x, x < oo) + + +def test_ImageSet(): + raises(ValueError, lambda: ImageSet(x, S.Integers)) + assert ImageSet(Lambda(x, 1), S.Integers) == FiniteSet(1) + assert ImageSet(Lambda(x, y), S.Integers) == {y} + assert ImageSet(Lambda(x, 1), S.EmptySet) == S.EmptySet + empty = Intersection(FiniteSet(log(2)/pi), S.Integers) + assert unchanged(ImageSet, Lambda(x, 1), empty) # issue #17471 + squares = ImageSet(Lambda(x, x**2), S.Naturals) + assert 4 in squares + assert 5 not in squares + assert FiniteSet(*range(10)).intersect(squares) == FiniteSet(1, 4, 9) + + assert 16 not in squares.intersect(Interval(0, 10)) + + si = iter(squares) + a, b, c, d = next(si), next(si), next(si), next(si) + assert (a, b, c, d) == (1, 4, 9, 16) + + harmonics = ImageSet(Lambda(x, 1/x), S.Naturals) + assert Rational(1, 5) in harmonics + assert Rational(.25) in harmonics + assert harmonics.contains(.25) == Contains( + 0.25, ImageSet(Lambda(x, 1/x), S.Naturals), evaluate=False) + assert Rational(.3) not in harmonics + assert (1, 2) not in harmonics + + assert harmonics.is_iterable + + assert imageset(x, -x, Interval(0, 1)) == Interval(-1, 0) + + assert ImageSet(Lambda(x, x**2), Interval(0, 2)).doit() == Interval(0, 4) + assert ImageSet(Lambda((x, y), 2*x), {4}, {3}).doit() == FiniteSet(8) + assert (ImageSet(Lambda((x, y), x+y), {1, 2, 3}, {10, 20, 30}).doit() == + FiniteSet(11, 12, 13, 21, 22, 23, 31, 32, 33)) + + c = Interval(1, 3) * Interval(1, 3) + assert Tuple(2, 6) in ImageSet(Lambda(((x, y),), (x, 2*y)), c) + assert Tuple(2, S.Half) in ImageSet(Lambda(((x, y),), (x, 1/y)), c) + assert Tuple(2, -2) not in ImageSet(Lambda(((x, y),), (x, y**2)), c) + assert Tuple(2, -2) in ImageSet(Lambda(((x, y),), (x, -2)), c) + c3 = ProductSet(Interval(3, 7), Interval(8, 11), Interval(5, 9)) + assert Tuple(8, 3, 9) in ImageSet(Lambda(((t, y, x),), (y, t, x)), c3) + assert Tuple(Rational(1, 8), 3, 9) in ImageSet(Lambda(((t, y, x),), (1/y, t, x)), c3) + assert 2/pi not in ImageSet(Lambda(((x, y),), 2/x), c) + assert 2/S(100) not in ImageSet(Lambda(((x, y),), 2/x), c) + assert Rational(2, 3) in ImageSet(Lambda(((x, y),), 2/x), c) + + S1 = imageset(lambda x, y: x + y, S.Integers, S.Naturals) + assert S1.base_pset == ProductSet(S.Integers, S.Naturals) + assert S1.base_sets == (S.Integers, S.Naturals) + + # Passing a set instead of a FiniteSet shouldn't raise + assert unchanged(ImageSet, Lambda(x, x**2), {1, 2, 3}) + + S2 = ImageSet(Lambda(((x, y),), x+y), {(1, 2), (3, 4)}) + assert 3 in S2.doit() + # FIXME: This doesn't yet work: + #assert 3 in S2 + assert S2._contains(3) is None + + raises(TypeError, lambda: ImageSet(Lambda(x, x**2), 1)) + + +def test_image_is_ImageSet(): + assert isinstance(imageset(x, sqrt(sin(x)), Range(5)), ImageSet) + + +def test_halfcircle(): + r, th = symbols('r, theta', real=True) + L = Lambda(((r, th),), (r*cos(th), r*sin(th))) + halfcircle = ImageSet(L, Interval(0, 1)*Interval(0, pi)) + + assert (1, 0) in halfcircle + assert (0, -1) not in halfcircle + assert (0, 0) in halfcircle + assert halfcircle._contains((r, 0)) is None + assert not halfcircle.is_iterable + + +@XFAIL +def test_halfcircle_fail(): + r, th = symbols('r, theta', real=True) + L = Lambda(((r, th),), (r*cos(th), r*sin(th))) + halfcircle = ImageSet(L, Interval(0, 1)*Interval(0, pi)) + assert (r, 2*pi) not in halfcircle + + +def test_ImageSet_iterator_not_injective(): + L = Lambda(x, x - x % 2) # produces 0, 2, 2, 4, 4, 6, 6, ... + evens = ImageSet(L, S.Naturals) + i = iter(evens) + # No repeats here + assert (next(i), next(i), next(i), next(i)) == (0, 2, 4, 6) + + +def test_inf_Range_len(): + raises(ValueError, lambda: len(Range(0, oo, 2))) + assert Range(0, oo, 2).size is S.Infinity + assert Range(0, -oo, -2).size is S.Infinity + assert Range(oo, 0, -2).size is S.Infinity + assert Range(-oo, 0, 2).size is S.Infinity + + +def test_Range_set(): + empty = Range(0) + + assert Range(5) == Range(0, 5) == Range(0, 5, 1) + + r = Range(10, 20, 2) + assert 12 in r + assert 8 not in r + assert 11 not in r + assert 30 not in r + + assert list(Range(0, 5)) == list(range(5)) + assert list(Range(5, 0, -1)) == list(range(5, 0, -1)) + + + assert Range(5, 15).sup == 14 + assert Range(5, 15).inf == 5 + assert Range(15, 5, -1).sup == 15 + assert Range(15, 5, -1).inf == 6 + assert Range(10, 67, 10).sup == 60 + assert Range(60, 7, -10).inf == 10 + + assert len(Range(10, 38, 10)) == 3 + + assert Range(0, 0, 5) == empty + assert Range(oo, oo, 1) == empty + assert Range(oo, 1, 1) == empty + assert Range(-oo, 1, -1) == empty + assert Range(1, oo, -1) == empty + assert Range(1, -oo, 1) == empty + assert Range(1, -4, oo) == empty + ip = symbols('ip', positive=True) + assert Range(0, ip, -1) == empty + assert Range(0, -ip, 1) == empty + assert Range(1, -4, -oo) == Range(1, 2) + assert Range(1, 4, oo) == Range(1, 2) + assert Range(-oo, oo).size == oo + assert Range(oo, -oo, -1).size == oo + raises(ValueError, lambda: Range(-oo, oo, 2)) + raises(ValueError, lambda: Range(x, pi, y)) + raises(ValueError, lambda: Range(x, y, 0)) + + assert 5 in Range(0, oo, 5) + assert -5 in Range(-oo, 0, 5) + assert oo not in Range(0, oo) + ni = symbols('ni', integer=False) + assert ni not in Range(oo) + u = symbols('u', integer=None) + assert Range(oo).contains(u) is not False + inf = symbols('inf', infinite=True) + assert inf not in Range(-oo, oo) + raises(ValueError, lambda: Range(0, oo, 2)[-1]) + raises(ValueError, lambda: Range(0, -oo, -2)[-1]) + assert Range(-oo, 1, 1)[-1] is S.Zero + assert Range(oo, 1, -1)[-1] == 2 + assert inf not in Range(oo) + assert Range(1, 10, 1)[-1] == 9 + assert all(i.is_Integer for i in Range(0, -1, 1)) + it = iter(Range(-oo, 0, 2)) + raises(TypeError, lambda: next(it)) + + assert empty.intersect(S.Integers) == empty + assert Range(-1, 10, 1).intersect(S.Complexes) == Range(-1, 10, 1) + assert Range(-1, 10, 1).intersect(S.Reals) == Range(-1, 10, 1) + assert Range(-1, 10, 1).intersect(S.Rationals) == Range(-1, 10, 1) + assert Range(-1, 10, 1).intersect(S.Integers) == Range(-1, 10, 1) + assert Range(-1, 10, 1).intersect(S.Naturals) == Range(1, 10, 1) + assert Range(-1, 10, 1).intersect(S.Naturals0) == Range(0, 10, 1) + + # test slicing + assert Range(1, 10, 1)[5] == 6 + assert Range(1, 12, 2)[5] == 11 + assert Range(1, 10, 1)[-1] == 9 + assert Range(1, 10, 3)[-1] == 7 + raises(ValueError, lambda: Range(oo,0,-1)[1:3:0]) + raises(ValueError, lambda: Range(oo,0,-1)[:1]) + raises(ValueError, lambda: Range(1, oo)[-2]) + raises(ValueError, lambda: Range(-oo, 1)[2]) + raises(IndexError, lambda: Range(10)[-20]) + raises(IndexError, lambda: Range(10)[20]) + raises(ValueError, lambda: Range(2, -oo, -2)[2:2:0]) + assert Range(2, -oo, -2)[2:2:2] == empty + assert Range(2, -oo, -2)[:2:2] == Range(2, -2, -4) + raises(ValueError, lambda: Range(-oo, 4, 2)[:2:2]) + assert Range(-oo, 4, 2)[::-2] == Range(2, -oo, -4) + raises(ValueError, lambda: Range(-oo, 4, 2)[::2]) + assert Range(oo, 2, -2)[::] == Range(oo, 2, -2) + assert Range(-oo, 4, 2)[:-2:-2] == Range(2, 0, -4) + assert Range(-oo, 4, 2)[:-2:2] == Range(-oo, 0, 4) + raises(ValueError, lambda: Range(-oo, 4, 2)[:0:-2]) + raises(ValueError, lambda: Range(-oo, 4, 2)[:2:-2]) + assert Range(-oo, 4, 2)[-2::-2] == Range(0, -oo, -4) + raises(ValueError, lambda: Range(-oo, 4, 2)[-2:0:-2]) + raises(ValueError, lambda: Range(-oo, 4, 2)[0::2]) + assert Range(oo, 2, -2)[0::] == Range(oo, 2, -2) + raises(ValueError, lambda: Range(-oo, 4, 2)[0:-2:2]) + assert Range(oo, 2, -2)[0:-2:] == Range(oo, 6, -2) + raises(ValueError, lambda: Range(oo, 2, -2)[0:2:]) + raises(ValueError, lambda: Range(-oo, 4, 2)[2::-1]) + assert Range(-oo, 4, 2)[-2::2] == Range(0, 4, 4) + assert Range(oo, 0, -2)[-10:0:2] == empty + raises(ValueError, lambda: Range(oo, 0, -2)[0]) + raises(ValueError, lambda: Range(oo, 0, -2)[-10:10:2]) + raises(ValueError, lambda: Range(oo, 0, -2)[0::-2]) + assert Range(oo, 0, -2)[0:-4:-2] == empty + assert Range(oo, 0, -2)[:0:2] == empty + raises(ValueError, lambda: Range(oo, 0, -2)[:1:-1]) + + # test empty Range + assert Range(x, x, y) == empty + assert empty.reversed == empty + assert 0 not in empty + assert list(empty) == [] + assert len(empty) == 0 + assert empty.size is S.Zero + assert empty.intersect(FiniteSet(0)) is S.EmptySet + assert bool(empty) is False + raises(IndexError, lambda: empty[0]) + assert empty[:0] == empty + raises(NotImplementedError, lambda: empty.inf) + raises(NotImplementedError, lambda: empty.sup) + assert empty.as_relational(x) is S.false + + AB = [None] + list(range(12)) + for R in [ + Range(1, 10), + Range(1, 10, 2), + ]: + r = list(R) + for a, b, c in itertools.product(AB, AB, [-3, -1, None, 1, 3]): + for reverse in range(2): + r = list(reversed(r)) + R = R.reversed + result = list(R[a:b:c]) + ans = r[a:b:c] + txt = ('\n%s[%s:%s:%s] = %s -> %s' % ( + R, a, b, c, result, ans)) + check = ans == result + assert check, txt + + assert Range(1, 10, 1).boundary == Range(1, 10, 1) + + for r in (Range(1, 10, 2), Range(1, oo, 2)): + rev = r.reversed + assert r.inf == rev.inf and r.sup == rev.sup + assert r.step == -rev.step + + builtin_range = range + + raises(TypeError, lambda: Range(builtin_range(1))) + assert S(builtin_range(10)) == Range(10) + assert S(builtin_range(1000000000000)) == Range(1000000000000) + + # test Range.as_relational + assert Range(1, 4).as_relational(x) == (x >= 1) & (x <= 3) & Eq(Mod(x, 1), 0) + assert Range(oo, 1, -2).as_relational(x) == (x >= 3) & (x < oo) & Eq(Mod(x + 1, -2), 0) + + +def test_Range_symbolic(): + # symbolic Range + xr = Range(x, x + 4, 5) + sr = Range(x, y, t) + i = Symbol('i', integer=True) + ip = Symbol('i', integer=True, positive=True) + ipr = Range(ip) + inr = Range(0, -ip, -1) + ir = Range(i, i + 19, 2) + ir2 = Range(i, i*8, 3*i) + i = Symbol('i', integer=True) + inf = symbols('inf', infinite=True) + raises(ValueError, lambda: Range(inf)) + raises(ValueError, lambda: Range(inf, 0, -1)) + raises(ValueError, lambda: Range(inf, inf, 1)) + raises(ValueError, lambda: Range(1, 1, inf)) + # args + assert xr.args == (x, x + 5, 5) + assert sr.args == (x, y, t) + assert ir.args == (i, i + 20, 2) + assert ir2.args == (i, 10*i, 3*i) + # reversed + raises(ValueError, lambda: xr.reversed) + raises(ValueError, lambda: sr.reversed) + assert ipr.reversed.args == (ip - 1, -1, -1) + assert inr.reversed.args == (-ip + 1, 1, 1) + assert ir.reversed.args == (i + 18, i - 2, -2) + assert ir2.reversed.args == (7*i, -2*i, -3*i) + # contains + assert inf not in sr + assert inf not in ir + assert 0 in ipr + assert 0 in inr + raises(TypeError, lambda: 1 in ipr) + raises(TypeError, lambda: -1 in inr) + assert .1 not in sr + assert .1 not in ir + assert i + 1 not in ir + assert i + 2 in ir + raises(TypeError, lambda: x in xr) # XXX is this what contains is supposed to do? + raises(TypeError, lambda: 1 in sr) # XXX is this what contains is supposed to do? + # iter + raises(ValueError, lambda: next(iter(xr))) + raises(ValueError, lambda: next(iter(sr))) + assert next(iter(ir)) == i + assert next(iter(ir2)) == i + assert sr.intersect(S.Integers) == sr + assert sr.intersect(FiniteSet(x)) == Intersection({x}, sr) + raises(ValueError, lambda: sr[:2]) + raises(ValueError, lambda: xr[0]) + raises(ValueError, lambda: sr[0]) + # len + assert len(ir) == ir.size == 10 + assert len(ir2) == ir2.size == 3 + raises(ValueError, lambda: len(xr)) + raises(ValueError, lambda: xr.size) + raises(ValueError, lambda: len(sr)) + raises(ValueError, lambda: sr.size) + # bool + assert bool(Range(0)) == False + assert bool(xr) + assert bool(ir) + assert bool(ipr) + assert bool(inr) + raises(ValueError, lambda: bool(sr)) + raises(ValueError, lambda: bool(ir2)) + # inf + raises(ValueError, lambda: xr.inf) + raises(ValueError, lambda: sr.inf) + assert ipr.inf == 0 + assert inr.inf == -ip + 1 + assert ir.inf == i + raises(ValueError, lambda: ir2.inf) + # sup + raises(ValueError, lambda: xr.sup) + raises(ValueError, lambda: sr.sup) + assert ipr.sup == ip - 1 + assert inr.sup == 0 + assert ir.inf == i + raises(ValueError, lambda: ir2.sup) + # getitem + raises(ValueError, lambda: xr[0]) + raises(ValueError, lambda: sr[0]) + raises(ValueError, lambda: sr[-1]) + raises(ValueError, lambda: sr[:2]) + assert ir[:2] == Range(i, i + 4, 2) + assert ir[0] == i + assert ir[-2] == i + 16 + assert ir[-1] == i + 18 + assert ir2[:2] == Range(i, 7*i, 3*i) + assert ir2[0] == i + assert ir2[-2] == 4*i + assert ir2[-1] == 7*i + raises(ValueError, lambda: Range(i)[-1]) + assert ipr[0] == ipr.inf == 0 + assert ipr[-1] == ipr.sup == ip - 1 + assert inr[0] == inr.sup == 0 + assert inr[-1] == inr.inf == -ip + 1 + raises(ValueError, lambda: ipr[-2]) + assert ir.inf == i + assert ir.sup == i + 18 + raises(ValueError, lambda: Range(i).inf) + # as_relational + assert ir.as_relational(x) == ((x >= i) & (x <= i + 18) & + Eq(Mod(-i + x, 2), 0)) + assert ir2.as_relational(x) == Eq( + Mod(-i + x, 3*i), 0) & (((x >= i) & (x <= 7*i) & (3*i >= 1)) | + ((x <= i) & (x >= 7*i) & (3*i <= -1))) + assert Range(i, i + 1).as_relational(x) == Eq(x, i) + assert sr.as_relational(z) == Eq( + Mod(t, 1), 0) & Eq(Mod(x, 1), 0) & Eq(Mod(-x + z, t), 0 + ) & (((z >= x) & (z <= -t + y) & (t >= 1)) | + ((z <= x) & (z >= -t + y) & (t <= -1))) + assert xr.as_relational(z) == Eq(z, x) & Eq(Mod(x, 1), 0) + # symbols can clash if user wants (but it must be integer) + assert xr.as_relational(x) == Eq(Mod(x, 1), 0) + # contains() for symbolic values (issue #18146) + e = Symbol('e', integer=True, even=True) + o = Symbol('o', integer=True, odd=True) + assert Range(5).contains(i) == And(i >= 0, i <= 4) + assert Range(1).contains(i) == Eq(i, 0) + assert Range(-oo, 5, 1).contains(i) == (i <= 4) + assert Range(-oo, oo).contains(i) == True + assert Range(0, 8, 2).contains(i) == Contains(i, Range(0, 8, 2)) + assert Range(0, 8, 2).contains(e) == And(e >= 0, e <= 6) + assert Range(0, 8, 2).contains(2*i) == And(2*i >= 0, 2*i <= 6) + assert Range(0, 8, 2).contains(o) == False + assert Range(1, 9, 2).contains(e) == False + assert Range(1, 9, 2).contains(o) == And(o >= 1, o <= 7) + assert Range(8, 0, -2).contains(o) == False + assert Range(9, 1, -2).contains(o) == And(o >= 3, o <= 9) + assert Range(-oo, 8, 2).contains(i) == Contains(i, Range(-oo, 8, 2)) + + +def test_range_range_intersection(): + for a, b, r in [ + (Range(0), Range(1), S.EmptySet), + (Range(3), Range(4, oo), S.EmptySet), + (Range(3), Range(-3, -1), S.EmptySet), + (Range(1, 3), Range(0, 3), Range(1, 3)), + (Range(1, 3), Range(1, 4), Range(1, 3)), + (Range(1, oo, 2), Range(2, oo, 2), S.EmptySet), + (Range(0, oo, 2), Range(oo), Range(0, oo, 2)), + (Range(0, oo, 2), Range(100), Range(0, 100, 2)), + (Range(2, oo, 2), Range(oo), Range(2, oo, 2)), + (Range(0, oo, 2), Range(5, 6), S.EmptySet), + (Range(2, 80, 1), Range(55, 71, 4), Range(55, 71, 4)), + (Range(0, 6, 3), Range(-oo, 5, 3), S.EmptySet), + (Range(0, oo, 2), Range(5, oo, 3), Range(8, oo, 6)), + (Range(4, 6, 2), Range(2, 16, 7), S.EmptySet),]: + assert a.intersect(b) == r + assert a.intersect(b.reversed) == r + assert a.reversed.intersect(b) == r + assert a.reversed.intersect(b.reversed) == r + a, b = b, a + assert a.intersect(b) == r + assert a.intersect(b.reversed) == r + assert a.reversed.intersect(b) == r + assert a.reversed.intersect(b.reversed) == r + + +def test_range_interval_intersection(): + p = symbols('p', positive=True) + assert isinstance(Range(3).intersect(Interval(p, p + 2)), Intersection) + assert Range(4).intersect(Interval(0, 3)) == Range(4) + assert Range(4).intersect(Interval(-oo, oo)) == Range(4) + assert Range(4).intersect(Interval(1, oo)) == Range(1, 4) + assert Range(4).intersect(Interval(1.1, oo)) == Range(2, 4) + assert Range(4).intersect(Interval(0.1, 3)) == Range(1, 4) + assert Range(4).intersect(Interval(0.1, 3.1)) == Range(1, 4) + assert Range(4).intersect(Interval.open(0, 3)) == Range(1, 3) + assert Range(4).intersect(Interval.open(0.1, 0.5)) is S.EmptySet + assert Interval(-1, 5).intersect(S.Complexes) == Interval(-1, 5) + assert Interval(-1, 5).intersect(S.Reals) == Interval(-1, 5) + assert Interval(-1, 5).intersect(S.Integers) == Range(-1, 6) + assert Interval(-1, 5).intersect(S.Naturals) == Range(1, 6) + assert Interval(-1, 5).intersect(S.Naturals0) == Range(0, 6) + + # Null Range intersections + assert Range(0).intersect(Interval(0.2, 0.8)) is S.EmptySet + assert Range(0).intersect(Interval(-oo, oo)) is S.EmptySet + + +def test_range_is_finite_set(): + assert Range(-100, 100).is_finite_set is True + assert Range(2, oo).is_finite_set is False + assert Range(-oo, 50).is_finite_set is False + assert Range(-oo, oo).is_finite_set is False + assert Range(oo, -oo).is_finite_set is True + assert Range(0, 0).is_finite_set is True + assert Range(oo, oo).is_finite_set is True + assert Range(-oo, -oo).is_finite_set is True + n = Symbol('n', integer=True) + m = Symbol('m', integer=True) + assert Range(n, n + 49).is_finite_set is True + assert Range(n, 0).is_finite_set is True + assert Range(-3, n + 7).is_finite_set is True + assert Range(n, m).is_finite_set is True + assert Range(n + m, m - n).is_finite_set is True + assert Range(n, n + m + n).is_finite_set is True + assert Range(n, oo).is_finite_set is False + assert Range(-oo, n).is_finite_set is False + assert Range(n, -oo).is_finite_set is True + assert Range(oo, n).is_finite_set is True + + +def test_Range_is_iterable(): + assert Range(-100, 100).is_iterable is True + assert Range(2, oo).is_iterable is False + assert Range(-oo, 50).is_iterable is False + assert Range(-oo, oo).is_iterable is False + assert Range(oo, -oo).is_iterable is True + assert Range(0, 0).is_iterable is True + assert Range(oo, oo).is_iterable is True + assert Range(-oo, -oo).is_iterable is True + n = Symbol('n', integer=True) + m = Symbol('m', integer=True) + p = Symbol('p', integer=True, positive=True) + assert Range(n, n + 49).is_iterable is True + assert Range(n, 0).is_iterable is False + assert Range(-3, n + 7).is_iterable is False + assert Range(-3, p + 7).is_iterable is False # Should work with better __iter__ + assert Range(n, m).is_iterable is False + assert Range(n + m, m - n).is_iterable is False + assert Range(n, n + m + n).is_iterable is False + assert Range(n, oo).is_iterable is False + assert Range(-oo, n).is_iterable is False + x = Symbol('x') + assert Range(x, x + 49).is_iterable is False + assert Range(x, 0).is_iterable is False + assert Range(-3, x + 7).is_iterable is False + assert Range(x, m).is_iterable is False + assert Range(x + m, m - x).is_iterable is False + assert Range(x, x + m + x).is_iterable is False + assert Range(x, oo).is_iterable is False + assert Range(-oo, x).is_iterable is False + + +def test_Integers_eval_imageset(): + ans = ImageSet(Lambda(x, 2*x + Rational(3, 7)), S.Integers) + im = imageset(Lambda(x, -2*x + Rational(3, 7)), S.Integers) + assert im == ans + im = imageset(Lambda(x, -2*x - Rational(11, 7)), S.Integers) + assert im == ans + y = Symbol('y') + L = imageset(x, 2*x + y, S.Integers) + assert y + 4 in L + a, b, c = 0.092, 0.433, 0.341 + assert a in imageset(x, a + c*x, S.Integers) + assert b in imageset(x, b + c*x, S.Integers) + + _x = symbols('x', negative=True) + eq = _x**2 - _x + 1 + assert imageset(_x, eq, S.Integers).lamda.expr == _x**2 + _x + 1 + eq = 3*_x - 1 + assert imageset(_x, eq, S.Integers).lamda.expr == 3*_x + 2 + + assert imageset(x, (x, 1/x), S.Integers) == \ + ImageSet(Lambda(x, (x, 1/x)), S.Integers) + + +def test_Range_eval_imageset(): + a, b, c = symbols('a b c') + assert imageset(x, a*(x + b) + c, Range(3)) == \ + imageset(x, a*x + a*b + c, Range(3)) + eq = (x + 1)**2 + assert imageset(x, eq, Range(3)).lamda.expr == eq + eq = a*(x + b) + c + r = Range(3, -3, -2) + imset = imageset(x, eq, r) + assert imset.lamda.expr != eq + assert list(imset) == [eq.subs(x, i).expand() for i in list(r)] + + +def test_fun(): + assert (FiniteSet(*ImageSet(Lambda(x, sin(pi*x/4)), + Range(-10, 11))) == FiniteSet(-1, -sqrt(2)/2, 0, sqrt(2)/2, 1)) + + +def test_Range_is_empty(): + i = Symbol('i', integer=True) + n = Symbol('n', negative=True, integer=True) + p = Symbol('p', positive=True, integer=True) + + assert Range(0).is_empty + assert not Range(1).is_empty + assert Range(1, 0).is_empty + assert not Range(-1, 0).is_empty + assert Range(i).is_empty is None + assert Range(n).is_empty + assert Range(p).is_empty is False + assert Range(n, 0).is_empty is False + assert Range(n, p).is_empty is False + assert Range(p, n).is_empty + assert Range(n, -1).is_empty is None + assert Range(p, n, -1).is_empty is False + + +def test_Reals(): + assert 5 in S.Reals + assert S.Pi in S.Reals + assert -sqrt(2) in S.Reals + assert (2, 5) not in S.Reals + assert sqrt(-1) not in S.Reals + assert S.Reals == Interval(-oo, oo) + assert S.Reals != Interval(0, oo) + assert S.Reals.is_subset(Interval(-oo, oo)) + assert S.Reals.intersect(Range(-oo, oo)) == Range(-oo, oo) + assert S.ComplexInfinity not in S.Reals + assert S.NaN not in S.Reals + assert x + S.ComplexInfinity not in S.Reals + + +def test_Complex(): + assert 5 in S.Complexes + assert 5 + 4*I in S.Complexes + assert S.Pi in S.Complexes + assert -sqrt(2) in S.Complexes + assert -I in S.Complexes + assert sqrt(-1) in S.Complexes + assert S.Complexes.intersect(S.Reals) == S.Reals + assert S.Complexes.union(S.Reals) == S.Complexes + assert S.Complexes == ComplexRegion(S.Reals*S.Reals) + assert (S.Complexes == ComplexRegion(Interval(1, 2)*Interval(3, 4))) == False + assert str(S.Complexes) == "Complexes" + assert repr(S.Complexes) == "Complexes" + + +def take(n, iterable): + "Return first n items of the iterable as a list" + return list(itertools.islice(iterable, n)) + + +def test_intersections(): + assert S.Integers.intersect(S.Reals) == S.Integers + assert 5 in S.Integers.intersect(S.Reals) + assert 5 in S.Integers.intersect(S.Reals) + assert -5 not in S.Naturals.intersect(S.Reals) + assert 5.5 not in S.Integers.intersect(S.Reals) + assert 5 in S.Integers.intersect(Interval(3, oo)) + assert -5 in S.Integers.intersect(Interval(-oo, 3)) + assert all(x.is_Integer + for x in take(10, S.Integers.intersect(Interval(3, oo)) )) + + +def test_infinitely_indexed_set_1(): + from sympy.abc import n, m + assert imageset(Lambda(n, n), S.Integers) == imageset(Lambda(m, m), S.Integers) + + assert imageset(Lambda(n, 2*n), S.Integers).intersect( + imageset(Lambda(m, 2*m + 1), S.Integers)) is S.EmptySet + + assert imageset(Lambda(n, 2*n), S.Integers).intersect( + imageset(Lambda(n, 2*n + 1), S.Integers)) is S.EmptySet + + assert imageset(Lambda(m, 2*m), S.Integers).intersect( + imageset(Lambda(n, 3*n), S.Integers)).dummy_eq( + ImageSet(Lambda(t, 6*t), S.Integers)) + + assert imageset(x, x/2 + Rational(1, 3), S.Integers).intersect(S.Integers) is S.EmptySet + assert imageset(x, x/2 + S.Half, S.Integers).intersect(S.Integers) is S.Integers + + # https://github.com/sympy/sympy/issues/17355 + S53 = ImageSet(Lambda(n, 5*n + 3), S.Integers) + assert S53.intersect(S.Integers) == S53 + + +def test_infinitely_indexed_set_2(): + from sympy.abc import n + a = Symbol('a', integer=True) + assert imageset(Lambda(n, n), S.Integers) == \ + imageset(Lambda(n, n + a), S.Integers) + assert imageset(Lambda(n, n + pi), S.Integers) == \ + imageset(Lambda(n, n + a + pi), S.Integers) + assert imageset(Lambda(n, n), S.Integers) == \ + imageset(Lambda(n, -n + a), S.Integers) + assert imageset(Lambda(n, -6*n), S.Integers) == \ + ImageSet(Lambda(n, 6*n), S.Integers) + assert imageset(Lambda(n, 2*n + pi), S.Integers) == \ + ImageSet(Lambda(n, 2*n + pi - 2), S.Integers) + + +def test_imageset_intersect_real(): + from sympy.abc import n + assert imageset(Lambda(n, n + (n - 1)*(n + 1)*I), S.Integers).intersect(S.Reals) == FiniteSet(-1, 1) + im = (n - 1)*(n + S.Half) + assert imageset(Lambda(n, n + im*I), S.Integers + ).intersect(S.Reals) == FiniteSet(1) + assert imageset(Lambda(n, n + im*(n + 1)*I), S.Naturals0 + ).intersect(S.Reals) == FiniteSet(1) + assert imageset(Lambda(n, n/2 + im.expand()*I), S.Integers + ).intersect(S.Reals) == ImageSet(Lambda(x, x/2), ConditionSet( + n, Eq(n**2 - n/2 - S(1)/2, 0), S.Integers)) + assert imageset(Lambda(n, n/(1/n - 1) + im*(n + 1)*I), S.Integers + ).intersect(S.Reals) == FiniteSet(S.Half) + assert imageset(Lambda(n, n/(n - 6) + + (n - 3)*(n + 1)*I/(2*n + 2)), S.Integers).intersect( + S.Reals) == FiniteSet(-1) + assert imageset(Lambda(n, n/(n**2 - 9) + + (n - 3)*(n + 1)*I/(2*n + 2)), S.Integers).intersect( + S.Reals) is S.EmptySet + s = ImageSet( + Lambda(n, -I*(I*(2*pi*n - pi/4) + log(Abs(sqrt(-I))))), + S.Integers) + # s is unevaluated, but after intersection the result + # should be canonical + assert s.intersect(S.Reals) == imageset( + Lambda(n, 2*n*pi - pi/4), S.Integers) == ImageSet( + Lambda(n, 2*pi*n + pi*Rational(7, 4)), S.Integers) + + +def test_imageset_intersect_interval(): + from sympy.abc import n + f1 = ImageSet(Lambda(n, n*pi), S.Integers) + f2 = ImageSet(Lambda(n, 2*n), Interval(0, pi)) + f3 = ImageSet(Lambda(n, 2*n*pi + pi/2), S.Integers) + # complex expressions + f4 = ImageSet(Lambda(n, n*I*pi), S.Integers) + f5 = ImageSet(Lambda(n, 2*I*n*pi + pi/2), S.Integers) + # non-linear expressions + f6 = ImageSet(Lambda(n, log(n)), S.Integers) + f7 = ImageSet(Lambda(n, n**2), S.Integers) + f8 = ImageSet(Lambda(n, Abs(n)), S.Integers) + f9 = ImageSet(Lambda(n, exp(n)), S.Naturals0) + + assert f1.intersect(Interval(-1, 1)) == FiniteSet(0) + assert f1.intersect(Interval(0, 2*pi, False, True)) == FiniteSet(0, pi) + assert f2.intersect(Interval(1, 2)) == Interval(1, 2) + assert f3.intersect(Interval(-1, 1)) == S.EmptySet + assert f3.intersect(Interval(-5, 5)) == FiniteSet(pi*Rational(-3, 2), pi/2) + assert f4.intersect(Interval(-1, 1)) == FiniteSet(0) + assert f4.intersect(Interval(1, 2)) == S.EmptySet + assert f5.intersect(Interval(0, 1)) == S.EmptySet + assert f6.intersect(Interval(0, 1)) == FiniteSet(S.Zero, log(2)) + assert f7.intersect(Interval(0, 10)) == Intersection(f7, Interval(0, 10)) + assert f8.intersect(Interval(0, 2)) == Intersection(f8, Interval(0, 2)) + assert f9.intersect(Interval(1, 2)) == Intersection(f9, Interval(1, 2)) + + +def test_imageset_intersect_diophantine(): + from sympy.abc import m, n + # Check that same lambda variable for both ImageSets is handled correctly + img1 = ImageSet(Lambda(n, 2*n + 1), S.Integers) + img2 = ImageSet(Lambda(n, 4*n + 1), S.Integers) + assert img1.intersect(img2) == img2 + # Empty solution set returned by diophantine: + assert ImageSet(Lambda(n, 2*n), S.Integers).intersect( + ImageSet(Lambda(n, 2*n + 1), S.Integers)) == S.EmptySet + # Check intersection with S.Integers: + assert ImageSet(Lambda(n, 9/n + 20*n/3), S.Integers).intersect( + S.Integers) == FiniteSet(-61, -23, 23, 61) + # Single solution (2, 3) for diophantine solution: + assert ImageSet(Lambda(n, (n - 2)**2), S.Integers).intersect( + ImageSet(Lambda(n, -(n - 3)**2), S.Integers)) == FiniteSet(0) + # Single parametric solution for diophantine solution: + assert ImageSet(Lambda(n, n**2 + 5), S.Integers).intersect( + ImageSet(Lambda(m, 2*m), S.Integers)).dummy_eq(ImageSet( + Lambda(n, 4*n**2 + 4*n + 6), S.Integers)) + # 4 non-parametric solution couples for dioph. equation: + assert ImageSet(Lambda(n, n**2 - 9), S.Integers).intersect( + ImageSet(Lambda(m, -m**2), S.Integers)) == FiniteSet(-9, 0) + # Double parametric solution for diophantine solution: + assert ImageSet(Lambda(m, m**2 + 40), S.Integers).intersect( + ImageSet(Lambda(n, 41*n), S.Integers)).dummy_eq(Intersection( + ImageSet(Lambda(m, m**2 + 40), S.Integers), + ImageSet(Lambda(n, 41*n), S.Integers))) + # Check that diophantine returns *all* (8) solutions (permute=True) + assert ImageSet(Lambda(n, n**4 - 2**4), S.Integers).intersect( + ImageSet(Lambda(m, -m**4 + 3**4), S.Integers)) == FiniteSet(0, 65) + assert ImageSet(Lambda(n, pi/12 + n*5*pi/12), S.Integers).intersect( + ImageSet(Lambda(n, 7*pi/12 + n*11*pi/12), S.Integers)).dummy_eq(ImageSet( + Lambda(n, 55*pi*n/12 + 17*pi/4), S.Integers)) + # TypeError raised by diophantine (#18081) + assert ImageSet(Lambda(n, n*log(2)), S.Integers).intersection( + S.Integers).dummy_eq(Intersection(ImageSet( + Lambda(n, n*log(2)), S.Integers), S.Integers)) + # NotImplementedError raised by diophantine (no solver for cubic_thue) + assert ImageSet(Lambda(n, n**3 + 1), S.Integers).intersect( + ImageSet(Lambda(n, n**3), S.Integers)).dummy_eq(Intersection( + ImageSet(Lambda(n, n**3 + 1), S.Integers), + ImageSet(Lambda(n, n**3), S.Integers))) + + +def test_infinitely_indexed_set_3(): + from sympy.abc import n, m + assert imageset(Lambda(m, 2*pi*m), S.Integers).intersect( + imageset(Lambda(n, 3*pi*n), S.Integers)).dummy_eq( + ImageSet(Lambda(t, 6*pi*t), S.Integers)) + assert imageset(Lambda(n, 2*n + 1), S.Integers) == \ + imageset(Lambda(n, 2*n - 1), S.Integers) + assert imageset(Lambda(n, 3*n + 2), S.Integers) == \ + imageset(Lambda(n, 3*n - 1), S.Integers) + + +def test_ImageSet_simplification(): + from sympy.abc import n, m + assert imageset(Lambda(n, n), S.Integers) == S.Integers + assert imageset(Lambda(n, sin(n)), + imageset(Lambda(m, tan(m)), S.Integers)) == \ + imageset(Lambda(m, sin(tan(m))), S.Integers) + assert imageset(n, 1 + 2*n, S.Naturals) == Range(3, oo, 2) + assert imageset(n, 1 + 2*n, S.Naturals0) == Range(1, oo, 2) + assert imageset(n, 1 - 2*n, S.Naturals) == Range(-1, -oo, -2) + + +def test_ImageSet_contains(): + assert (2, S.Half) in imageset(x, (x, 1/x), S.Integers) + assert imageset(x, x + I*3, S.Integers).intersection(S.Reals) is S.EmptySet + i = Dummy(integer=True) + q = imageset(x, x + I*y, S.Integers).intersection(S.Reals) + assert q.subs(y, I*i).intersection(S.Integers) is S.Integers + q = imageset(x, x + I*y/x, S.Integers).intersection(S.Reals) + assert q.subs(y, 0) is S.Integers + assert q.subs(y, I*i*x).intersection(S.Integers) is S.Integers + z = cos(1)**2 + sin(1)**2 - 1 + q = imageset(x, x + I*z, S.Integers).intersection(S.Reals) + assert q is not S.EmptySet + + +def test_ComplexRegion_contains(): + r = Symbol('r', real=True) + # contains in ComplexRegion + a = Interval(2, 3) + b = Interval(4, 6) + c = Interval(7, 9) + c1 = ComplexRegion(a*b) + c2 = ComplexRegion(Union(a*b, c*a)) + assert 2.5 + 4.5*I in c1 + assert 2 + 4*I in c1 + assert 3 + 4*I in c1 + assert 8 + 2.5*I in c2 + assert 2.5 + 6.1*I not in c1 + assert 4.5 + 3.2*I not in c1 + assert c1.contains(x) == Contains(x, c1, evaluate=False) + assert c1.contains(r) == False + assert c2.contains(x) == Contains(x, c2, evaluate=False) + assert c2.contains(r) == False + + r1 = Interval(0, 1) + theta1 = Interval(0, 2*S.Pi) + c3 = ComplexRegion(r1*theta1, polar=True) + assert (0.5 + I*6/10) in c3 + assert (S.Half + I*6/10) in c3 + assert (S.Half + .6*I) in c3 + assert (0.5 + .6*I) in c3 + assert I in c3 + assert 1 in c3 + assert 0 in c3 + assert 1 + I not in c3 + assert 1 - I not in c3 + assert c3.contains(x) == Contains(x, c3, evaluate=False) + assert c3.contains(r + 2*I) == Contains( + r + 2*I, c3, evaluate=False) # is in fact False + assert c3.contains(1/(1 + r**2)) == Contains( + 1/(1 + r**2), c3, evaluate=False) # is in fact True + + r2 = Interval(0, 3) + theta2 = Interval(pi, 2*pi, left_open=True) + c4 = ComplexRegion(r2*theta2, polar=True) + assert c4.contains(0) == True + assert c4.contains(2 + I) == False + assert c4.contains(-2 + I) == False + assert c4.contains(-2 - I) == True + assert c4.contains(2 - I) == True + assert c4.contains(-2) == False + assert c4.contains(2) == True + assert c4.contains(x) == Contains(x, c4, evaluate=False) + assert c4.contains(3/(1 + r**2)) == Contains( + 3/(1 + r**2), c4, evaluate=False) # is in fact True + + raises(ValueError, lambda: ComplexRegion(r1*theta1, polar=2)) + + +def test_symbolic_Range(): + n = Symbol('n') + raises(ValueError, lambda: Range(n)[0]) + raises(IndexError, lambda: Range(n, n)[0]) + raises(ValueError, lambda: Range(n, n+1)[0]) + raises(ValueError, lambda: Range(n).size) + + n = Symbol('n', integer=True) + raises(ValueError, lambda: Range(n)[0]) + raises(IndexError, lambda: Range(n, n)[0]) + assert Range(n, n+1)[0] == n + raises(ValueError, lambda: Range(n).size) + assert Range(n, n+1).size == 1 + + n = Symbol('n', integer=True, nonnegative=True) + raises(ValueError, lambda: Range(n)[0]) + raises(IndexError, lambda: Range(n, n)[0]) + assert Range(n+1)[0] == 0 + assert Range(n, n+1)[0] == n + assert Range(n).size == n + assert Range(n+1).size == n+1 + assert Range(n, n+1).size == 1 + + n = Symbol('n', integer=True, positive=True) + assert Range(n)[0] == 0 + assert Range(n, n+1)[0] == n + assert Range(n).size == n + assert Range(n, n+1).size == 1 + + m = Symbol('m', integer=True, positive=True) + + assert Range(n, n+m)[0] == n + assert Range(n, n+m).size == m + assert Range(n, n+1).size == 1 + assert Range(n, n+m, 2).size == floor(m/2) + + m = Symbol('m', integer=True, positive=True, even=True) + assert Range(n, n+m, 2).size == m/2 + + +def test_issue_18400(): + n = Symbol('n', integer=True) + raises(ValueError, lambda: imageset(lambda x: x*2, Range(n))) + + n = Symbol('n', integer=True, positive=True) + # No exception + assert imageset(lambda x: x*2, Range(n)) == imageset(lambda x: x*2, Range(n)) + + +def test_ComplexRegion_intersect(): + # Polar form + X_axis = ComplexRegion(Interval(0, oo)*FiniteSet(0, S.Pi), polar=True) + + unit_disk = ComplexRegion(Interval(0, 1)*Interval(0, 2*S.Pi), polar=True) + upper_half_unit_disk = ComplexRegion(Interval(0, 1)*Interval(0, S.Pi), polar=True) + upper_half_disk = ComplexRegion(Interval(0, oo)*Interval(0, S.Pi), polar=True) + lower_half_disk = ComplexRegion(Interval(0, oo)*Interval(S.Pi, 2*S.Pi), polar=True) + right_half_disk = ComplexRegion(Interval(0, oo)*Interval(-S.Pi/2, S.Pi/2), polar=True) + first_quad_disk = ComplexRegion(Interval(0, oo)*Interval(0, S.Pi/2), polar=True) + + assert upper_half_disk.intersect(unit_disk) == upper_half_unit_disk + assert right_half_disk.intersect(first_quad_disk) == first_quad_disk + assert upper_half_disk.intersect(right_half_disk) == first_quad_disk + assert upper_half_disk.intersect(lower_half_disk) == X_axis + + c1 = ComplexRegion(Interval(0, 4)*Interval(0, 2*S.Pi), polar=True) + assert c1.intersect(Interval(1, 5)) == Interval(1, 4) + assert c1.intersect(Interval(4, 9)) == FiniteSet(4) + assert c1.intersect(Interval(5, 12)) is S.EmptySet + + # Rectangular form + X_axis = ComplexRegion(Interval(-oo, oo)*FiniteSet(0)) + + unit_square = ComplexRegion(Interval(-1, 1)*Interval(-1, 1)) + upper_half_unit_square = ComplexRegion(Interval(-1, 1)*Interval(0, 1)) + upper_half_plane = ComplexRegion(Interval(-oo, oo)*Interval(0, oo)) + lower_half_plane = ComplexRegion(Interval(-oo, oo)*Interval(-oo, 0)) + right_half_plane = ComplexRegion(Interval(0, oo)*Interval(-oo, oo)) + first_quad_plane = ComplexRegion(Interval(0, oo)*Interval(0, oo)) + + assert upper_half_plane.intersect(unit_square) == upper_half_unit_square + assert right_half_plane.intersect(first_quad_plane) == first_quad_plane + assert upper_half_plane.intersect(right_half_plane) == first_quad_plane + assert upper_half_plane.intersect(lower_half_plane) == X_axis + + c1 = ComplexRegion(Interval(-5, 5)*Interval(-10, 10)) + assert c1.intersect(Interval(2, 7)) == Interval(2, 5) + assert c1.intersect(Interval(5, 7)) == FiniteSet(5) + assert c1.intersect(Interval(6, 9)) is S.EmptySet + + # unevaluated object + C1 = ComplexRegion(Interval(0, 1)*Interval(0, 2*S.Pi), polar=True) + C2 = ComplexRegion(Interval(-1, 1)*Interval(-1, 1)) + assert C1.intersect(C2) == Intersection(C1, C2, evaluate=False) + + +def test_ComplexRegion_union(): + # Polar form + c1 = ComplexRegion(Interval(0, 1)*Interval(0, 2*S.Pi), polar=True) + c2 = ComplexRegion(Interval(0, 1)*Interval(0, S.Pi), polar=True) + c3 = ComplexRegion(Interval(0, oo)*Interval(0, S.Pi), polar=True) + c4 = ComplexRegion(Interval(0, oo)*Interval(S.Pi, 2*S.Pi), polar=True) + + p1 = Union(Interval(0, 1)*Interval(0, 2*S.Pi), Interval(0, 1)*Interval(0, S.Pi)) + p2 = Union(Interval(0, oo)*Interval(0, S.Pi), Interval(0, oo)*Interval(S.Pi, 2*S.Pi)) + + assert c1.union(c2) == ComplexRegion(p1, polar=True) + assert c3.union(c4) == ComplexRegion(p2, polar=True) + + # Rectangular form + c5 = ComplexRegion(Interval(2, 5)*Interval(6, 9)) + c6 = ComplexRegion(Interval(4, 6)*Interval(10, 12)) + c7 = ComplexRegion(Interval(0, 10)*Interval(-10, 0)) + c8 = ComplexRegion(Interval(12, 16)*Interval(14, 20)) + + p3 = Union(Interval(2, 5)*Interval(6, 9), Interval(4, 6)*Interval(10, 12)) + p4 = Union(Interval(0, 10)*Interval(-10, 0), Interval(12, 16)*Interval(14, 20)) + + assert c5.union(c6) == ComplexRegion(p3) + assert c7.union(c8) == ComplexRegion(p4) + + assert c1.union(Interval(2, 4)) == Union(c1, Interval(2, 4), evaluate=False) + assert c5.union(Interval(2, 4)) == Union(c5, ComplexRegion.from_real(Interval(2, 4))) + + +def test_ComplexRegion_from_real(): + c1 = ComplexRegion(Interval(0, 1) * Interval(0, 2 * S.Pi), polar=True) + + raises(ValueError, lambda: c1.from_real(c1)) + assert c1.from_real(Interval(-1, 1)) == ComplexRegion(Interval(-1, 1) * FiniteSet(0), False) + + +def test_ComplexRegion_measure(): + a, b = Interval(2, 5), Interval(4, 8) + theta1, theta2 = Interval(0, 2*S.Pi), Interval(0, S.Pi) + c1 = ComplexRegion(a*b) + c2 = ComplexRegion(Union(a*theta1, b*theta2), polar=True) + + assert c1.measure == 12 + assert c2.measure == 9*pi + + +def test_normalize_theta_set(): + # Interval + assert normalize_theta_set(Interval(pi, 2*pi)) == \ + Union(FiniteSet(0), Interval.Ropen(pi, 2*pi)) + assert normalize_theta_set(Interval(pi*Rational(9, 2), 5*pi)) == Interval(pi/2, pi) + assert normalize_theta_set(Interval(pi*Rational(-3, 2), pi/2)) == Interval.Ropen(0, 2*pi) + assert normalize_theta_set(Interval.open(pi*Rational(-3, 2), pi/2)) == \ + Union(Interval.Ropen(0, pi/2), Interval.open(pi/2, 2*pi)) + assert normalize_theta_set(Interval.open(pi*Rational(-7, 2), pi*Rational(-3, 2))) == \ + Union(Interval.Ropen(0, pi/2), Interval.open(pi/2, 2*pi)) + assert normalize_theta_set(Interval(-pi/2, pi/2)) == \ + Union(Interval(0, pi/2), Interval.Ropen(pi*Rational(3, 2), 2*pi)) + assert normalize_theta_set(Interval.open(-pi/2, pi/2)) == \ + Union(Interval.Ropen(0, pi/2), Interval.open(pi*Rational(3, 2), 2*pi)) + assert normalize_theta_set(Interval(-4*pi, 3*pi)) == Interval.Ropen(0, 2*pi) + assert normalize_theta_set(Interval(pi*Rational(-3, 2), -pi/2)) == Interval(pi/2, pi*Rational(3, 2)) + assert normalize_theta_set(Interval.open(0, 2*pi)) == Interval.open(0, 2*pi) + assert normalize_theta_set(Interval.Ropen(-pi/2, pi/2)) == \ + Union(Interval.Ropen(0, pi/2), Interval.Ropen(pi*Rational(3, 2), 2*pi)) + assert normalize_theta_set(Interval.Lopen(-pi/2, pi/2)) == \ + Union(Interval(0, pi/2), Interval.open(pi*Rational(3, 2), 2*pi)) + assert normalize_theta_set(Interval(-pi/2, pi/2)) == \ + Union(Interval(0, pi/2), Interval.Ropen(pi*Rational(3, 2), 2*pi)) + assert normalize_theta_set(Interval.open(4*pi, pi*Rational(9, 2))) == Interval.open(0, pi/2) + assert normalize_theta_set(Interval.Lopen(4*pi, pi*Rational(9, 2))) == Interval.Lopen(0, pi/2) + assert normalize_theta_set(Interval.Ropen(4*pi, pi*Rational(9, 2))) == Interval.Ropen(0, pi/2) + assert normalize_theta_set(Interval.open(3*pi, 5*pi)) == \ + Union(Interval.Ropen(0, pi), Interval.open(pi, 2*pi)) + + # FiniteSet + assert normalize_theta_set(FiniteSet(0, pi, 3*pi)) == FiniteSet(0, pi) + assert normalize_theta_set(FiniteSet(0, pi/2, pi, 2*pi)) == FiniteSet(0, pi/2, pi) + assert normalize_theta_set(FiniteSet(0, -pi/2, -pi, -2*pi)) == FiniteSet(0, pi, pi*Rational(3, 2)) + assert normalize_theta_set(FiniteSet(pi*Rational(-3, 2), pi/2)) == \ + FiniteSet(pi/2) + assert normalize_theta_set(FiniteSet(2*pi)) == FiniteSet(0) + + # Unions + assert normalize_theta_set(Union(Interval(0, pi/3), Interval(pi/2, pi))) == \ + Union(Interval(0, pi/3), Interval(pi/2, pi)) + assert normalize_theta_set(Union(Interval(0, pi), Interval(2*pi, pi*Rational(7, 3)))) == \ + Interval(0, pi) + + # ValueError for non-real sets + raises(ValueError, lambda: normalize_theta_set(S.Complexes)) + + # NotImplementedError for subset of reals + raises(NotImplementedError, lambda: normalize_theta_set(Interval(0, 1))) + + # NotImplementedError without pi as coefficient + raises(NotImplementedError, lambda: normalize_theta_set(Interval(1, 2*pi))) + raises(NotImplementedError, lambda: normalize_theta_set(Interval(2*pi, 10))) + raises(NotImplementedError, lambda: normalize_theta_set(FiniteSet(0, 3, 3*pi))) + + +def test_ComplexRegion_FiniteSet(): + x, y, z, a, b, c = symbols('x y z a b c') + + # Issue #9669 + assert ComplexRegion(FiniteSet(a, b, c)*FiniteSet(x, y, z)) == \ + FiniteSet(a + I*x, a + I*y, a + I*z, b + I*x, b + I*y, + b + I*z, c + I*x, c + I*y, c + I*z) + assert ComplexRegion(FiniteSet(2)*FiniteSet(3)) == FiniteSet(2 + 3*I) + + +def test_union_RealSubSet(): + assert (S.Complexes).union(Interval(1, 2)) == S.Complexes + assert (S.Complexes).union(S.Integers) == S.Complexes + + +def test_SetKind_fancySet(): + G = lambda *args: ImageSet(Lambda(x, x ** 2), *args) + assert G(Interval(1, 4)).kind is SetKind(NumberKind) + assert G(FiniteSet(1, 4)).kind is SetKind(NumberKind) + assert S.Rationals.kind is SetKind(NumberKind) + assert S.Naturals.kind is SetKind(NumberKind) + assert S.Integers.kind is SetKind(NumberKind) + assert Range(3).kind is SetKind(NumberKind) + a = Interval(2, 3) + b = Interval(4, 6) + c1 = ComplexRegion(a*b) + assert c1.kind is SetKind(TupleKind(NumberKind, NumberKind)) + + +def test_issue_9980(): + c1 = ComplexRegion(Interval(1, 2)*Interval(2, 3)) + c2 = ComplexRegion(Interval(1, 5)*Interval(1, 3)) + R = Union(c1, c2) + assert simplify(R) == ComplexRegion(Union(Interval(1, 2)*Interval(2, 3), \ + Interval(1, 5)*Interval(1, 3)), False) + assert c1.func(*c1.args) == c1 + assert R.func(*R.args) == R + + +def test_issue_11732(): + interval12 = Interval(1, 2) + finiteset1234 = FiniteSet(1, 2, 3, 4) + pointComplex = Tuple(1, 5) + + assert (interval12 in S.Naturals) == False + assert (interval12 in S.Naturals0) == False + assert (interval12 in S.Integers) == False + assert (interval12 in S.Complexes) == False + + assert (finiteset1234 in S.Naturals) == False + assert (finiteset1234 in S.Naturals0) == False + assert (finiteset1234 in S.Integers) == False + assert (finiteset1234 in S.Complexes) == False + + assert (pointComplex in S.Naturals) == False + assert (pointComplex in S.Naturals0) == False + assert (pointComplex in S.Integers) == False + assert (pointComplex in S.Complexes) == True + + +def test_issue_11730(): + unit = Interval(0, 1) + square = ComplexRegion(unit ** 2) + + assert Union(S.Complexes, FiniteSet(oo)) != S.Complexes + assert Union(S.Complexes, FiniteSet(eye(4))) != S.Complexes + assert Union(unit, square) == square + assert Intersection(S.Reals, square) == unit + + +def test_issue_11938(): + unit = Interval(0, 1) + ival = Interval(1, 2) + cr1 = ComplexRegion(ival * unit) + + assert Intersection(cr1, S.Reals) == ival + assert Intersection(cr1, unit) == FiniteSet(1) + + arg1 = Interval(0, S.Pi) + arg2 = FiniteSet(S.Pi) + arg3 = Interval(S.Pi / 4, 3 * S.Pi / 4) + cp1 = ComplexRegion(unit * arg1, polar=True) + cp2 = ComplexRegion(unit * arg2, polar=True) + cp3 = ComplexRegion(unit * arg3, polar=True) + + assert Intersection(cp1, S.Reals) == Interval(-1, 1) + assert Intersection(cp2, S.Reals) == Interval(-1, 0) + assert Intersection(cp3, S.Reals) == FiniteSet(0) + + +def test_issue_11914(): + a, b = Interval(0, 1), Interval(0, pi) + c, d = Interval(2, 3), Interval(pi, 3 * pi / 2) + cp1 = ComplexRegion(a * b, polar=True) + cp2 = ComplexRegion(c * d, polar=True) + + assert -3 in cp1.union(cp2) + assert -3 in cp2.union(cp1) + assert -5 not in cp1.union(cp2) + + +def test_issue_9543(): + assert ImageSet(Lambda(x, x**2), S.Naturals).is_subset(S.Reals) + + +def test_issue_16871(): + assert ImageSet(Lambda(x, x), FiniteSet(1)) == {1} + assert ImageSet(Lambda(x, x - 3), S.Integers + ).intersection(S.Integers) is S.Integers + + +@XFAIL +def test_issue_16871b(): + assert ImageSet(Lambda(x, x - 3), S.Integers).is_subset(S.Integers) + + +def test_issue_18050(): + assert imageset(Lambda(x, I*x + 1), S.Integers + ) == ImageSet(Lambda(x, I*x + 1), S.Integers) + assert imageset(Lambda(x, 3*I*x + 4 + 8*I), S.Integers + ) == ImageSet(Lambda(x, 3*I*x + 4 + 2*I), S.Integers) + # no 'Mod' for next 2 tests: + assert imageset(Lambda(x, 2*x + 3*I), S.Integers + ) == ImageSet(Lambda(x, 2*x + 3*I), S.Integers) + r = Symbol('r', positive=True) + assert imageset(Lambda(x, r*x + 10), S.Integers + ) == ImageSet(Lambda(x, r*x + 10), S.Integers) + # reduce real part: + assert imageset(Lambda(x, 3*x + 8 + 5*I), S.Integers + ) == ImageSet(Lambda(x, 3*x + 2 + 5*I), S.Integers) + + +def test_Rationals(): + assert S.Integers.is_subset(S.Rationals) + assert S.Naturals.is_subset(S.Rationals) + assert S.Naturals0.is_subset(S.Rationals) + assert S.Rationals.is_subset(S.Reals) + assert S.Rationals.inf is -oo + assert S.Rationals.sup is oo + it = iter(S.Rationals) + assert [next(it) for i in range(12)] == [ + 0, 1, -1, S.Half, 2, Rational(-1, 2), -2, + Rational(1, 3), 3, Rational(-1, 3), -3, Rational(2, 3)] + assert Basic() not in S.Rationals + assert S.Half in S.Rationals + assert S.Rationals.contains(0.5) == Contains( + 0.5, S.Rationals, evaluate=False) + assert 2 in S.Rationals + r = symbols('r', rational=True) + assert r in S.Rationals + raises(TypeError, lambda: x in S.Rationals) + # issue #18134: + assert S.Rationals.boundary == S.Reals + assert S.Rationals.closure == S.Reals + assert S.Rationals.is_open == False + assert S.Rationals.is_closed == False + + +def test_NZQRC_unions(): + # check that all trivial number set unions are simplified: + nbrsets = (S.Naturals, S.Naturals0, S.Integers, S.Rationals, + S.Reals, S.Complexes) + unions = (Union(a, b) for a in nbrsets for b in nbrsets) + assert all(u.is_Union is False for u in unions) + + +def test_imageset_intersection(): + n = Dummy() + s = ImageSet(Lambda(n, -I*(I*(2*pi*n - pi/4) + + log(Abs(sqrt(-I))))), S.Integers) + assert s.intersect(S.Reals) == ImageSet( + Lambda(n, 2*pi*n + pi*Rational(7, 4)), S.Integers) + + +def test_issue_17858(): + assert 1 in Range(-oo, oo) + assert 0 in Range(oo, -oo, -1) + assert oo not in Range(-oo, oo) + assert -oo not in Range(-oo, oo) + +def test_issue_17859(): + r = Range(-oo,oo) + raises(ValueError,lambda: r[::2]) + raises(ValueError, lambda: r[::-2]) + r = Range(oo,-oo,-1) + raises(ValueError,lambda: r[::2]) + raises(ValueError, lambda: r[::-2]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/test_ordinals.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/test_ordinals.py new file mode 100644 index 0000000000000000000000000000000000000000..973ca329586f3e904f9377c44022c266f81c805c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/test_ordinals.py @@ -0,0 +1,67 @@ +from sympy.sets.ordinals import Ordinal, OmegaPower, ord0, omega +from sympy.testing.pytest import raises + +def test_string_ordinals(): + assert str(omega) == 'w' + assert str(Ordinal(OmegaPower(5, 3), OmegaPower(3, 2))) == 'w**5*3 + w**3*2' + assert str(Ordinal(OmegaPower(5, 3), OmegaPower(0, 5))) == 'w**5*3 + 5' + assert str(Ordinal(OmegaPower(1, 3), OmegaPower(0, 5))) == 'w*3 + 5' + assert str(Ordinal(OmegaPower(omega + 1, 1), OmegaPower(3, 2))) == 'w**(w + 1) + w**3*2' + +def test_addition_with_integers(): + assert 3 + Ordinal(OmegaPower(5, 3)) == Ordinal(OmegaPower(5, 3)) + assert Ordinal(OmegaPower(5, 3))+3 == Ordinal(OmegaPower(5, 3), OmegaPower(0, 3)) + assert Ordinal(OmegaPower(5, 3), OmegaPower(0, 2))+3 == \ + Ordinal(OmegaPower(5, 3), OmegaPower(0, 5)) + + +def test_addition_with_ordinals(): + assert Ordinal(OmegaPower(5, 3), OmegaPower(3, 2)) + Ordinal(OmegaPower(3, 3)) == \ + Ordinal(OmegaPower(5, 3), OmegaPower(3, 5)) + assert Ordinal(OmegaPower(5, 3), OmegaPower(3, 2)) + Ordinal(OmegaPower(4, 2)) == \ + Ordinal(OmegaPower(5, 3), OmegaPower(4, 2)) + assert Ordinal(OmegaPower(omega, 2), OmegaPower(3, 2)) + Ordinal(OmegaPower(4, 2)) == \ + Ordinal(OmegaPower(omega, 2), OmegaPower(4, 2)) + +def test_comparison(): + assert Ordinal(OmegaPower(5, 3)) > Ordinal(OmegaPower(4, 3), OmegaPower(2, 1)) + assert Ordinal(OmegaPower(5, 3), OmegaPower(3, 2)) < Ordinal(OmegaPower(5, 4)) + assert Ordinal(OmegaPower(5, 4)) < Ordinal(OmegaPower(5, 5), OmegaPower(4, 1)) + + assert Ordinal(OmegaPower(5, 3), OmegaPower(3, 2)) == \ + Ordinal(OmegaPower(5, 3), OmegaPower(3, 2)) + assert not Ordinal(OmegaPower(5, 3), OmegaPower(3, 2)) == Ordinal(OmegaPower(5, 3)) + assert Ordinal(OmegaPower(omega, 3)) > Ordinal(OmegaPower(5, 3)) + +def test_multiplication_with_integers(): + w = omega + assert 3*w == w + assert w*9 == Ordinal(OmegaPower(1, 9)) + +def test_multiplication(): + w = omega + assert w*(w + 1) == w*w + w + assert (w + 1)*(w + 1) == w*w + w + 1 + assert w*1 == w + assert 1*w == w + assert w*ord0 == ord0 + assert ord0*w == ord0 + assert w**w == w * w**w + assert (w**w)*w*w == w**(w + 2) + +def test_exponentiation(): + w = omega + assert w**2 == w*w + assert w**3 == w*w*w + assert w**(w + 1) == Ordinal(OmegaPower(omega + 1, 1)) + assert (w**w)*(w**w) == w**(w*2) + +def test_comapre_not_instance(): + w = OmegaPower(omega + 1, 1) + assert(not (w == None)) + assert(not (w < 5)) + raises(TypeError, lambda: w < 6.66) + +def test_is_successort(): + w = Ordinal(OmegaPower(5, 1)) + assert not w.is_successor_ordinal diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/test_powerset.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/test_powerset.py new file mode 100644 index 0000000000000000000000000000000000000000..2e3a407d565f6b9537a296af103ec0a4e137cff9 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/test_powerset.py @@ -0,0 +1,141 @@ +from sympy.core.expr import unchanged +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.sets.contains import Contains +from sympy.sets.fancysets import Interval +from sympy.sets.powerset import PowerSet +from sympy.sets.sets import FiniteSet +from sympy.testing.pytest import raises, XFAIL + + +def test_powerset_creation(): + assert unchanged(PowerSet, FiniteSet(1, 2)) + assert unchanged(PowerSet, S.EmptySet) + raises(ValueError, lambda: PowerSet(123)) + assert unchanged(PowerSet, S.Reals) + assert unchanged(PowerSet, S.Integers) + + +def test_powerset_rewrite_FiniteSet(): + assert PowerSet(FiniteSet(1, 2)).rewrite(FiniteSet) == \ + FiniteSet(S.EmptySet, FiniteSet(1), FiniteSet(2), FiniteSet(1, 2)) + assert PowerSet(S.EmptySet).rewrite(FiniteSet) == FiniteSet(S.EmptySet) + assert PowerSet(S.Naturals).rewrite(FiniteSet) == PowerSet(S.Naturals) + + +def test_finiteset_rewrite_powerset(): + assert FiniteSet(S.EmptySet).rewrite(PowerSet) == PowerSet(S.EmptySet) + assert FiniteSet( + S.EmptySet, FiniteSet(1), + FiniteSet(2), FiniteSet(1, 2)).rewrite(PowerSet) == \ + PowerSet(FiniteSet(1, 2)) + assert FiniteSet(1, 2, 3).rewrite(PowerSet) == FiniteSet(1, 2, 3) + + +def test_powerset__contains__(): + subset_series = [ + S.EmptySet, + FiniteSet(1, 2), + S.Naturals, + S.Naturals0, + S.Integers, + S.Rationals, + S.Reals, + S.Complexes] + + l = len(subset_series) + for i in range(l): + for j in range(l): + if i <= j: + assert subset_series[i] in \ + PowerSet(subset_series[j], evaluate=False) + else: + assert subset_series[i] not in \ + PowerSet(subset_series[j], evaluate=False) + + +@XFAIL +def test_failing_powerset__contains__(): + # XXX These are failing when evaluate=True, + # but using unevaluated PowerSet works fine. + assert FiniteSet(1, 2) not in PowerSet(S.EmptySet).rewrite(FiniteSet) + assert S.Naturals not in PowerSet(S.EmptySet).rewrite(FiniteSet) + assert S.Naturals not in PowerSet(FiniteSet(1, 2)).rewrite(FiniteSet) + assert S.Naturals0 not in PowerSet(S.EmptySet).rewrite(FiniteSet) + assert S.Naturals0 not in PowerSet(FiniteSet(1, 2)).rewrite(FiniteSet) + assert S.Integers not in PowerSet(S.EmptySet).rewrite(FiniteSet) + assert S.Integers not in PowerSet(FiniteSet(1, 2)).rewrite(FiniteSet) + assert S.Rationals not in PowerSet(S.EmptySet).rewrite(FiniteSet) + assert S.Rationals not in PowerSet(FiniteSet(1, 2)).rewrite(FiniteSet) + assert S.Reals not in PowerSet(S.EmptySet).rewrite(FiniteSet) + assert S.Reals not in PowerSet(FiniteSet(1, 2)).rewrite(FiniteSet) + assert S.Complexes not in PowerSet(S.EmptySet).rewrite(FiniteSet) + assert S.Complexes not in PowerSet(FiniteSet(1, 2)).rewrite(FiniteSet) + + +def test_powerset__len__(): + A = PowerSet(S.EmptySet, evaluate=False) + assert len(A) == 1 + A = PowerSet(A, evaluate=False) + assert len(A) == 2 + A = PowerSet(A, evaluate=False) + assert len(A) == 4 + A = PowerSet(A, evaluate=False) + assert len(A) == 16 + + +def test_powerset__iter__(): + a = PowerSet(FiniteSet(1, 2)).__iter__() + assert next(a) == S.EmptySet + assert next(a) == FiniteSet(1) + assert next(a) == FiniteSet(2) + assert next(a) == FiniteSet(1, 2) + + a = PowerSet(S.Naturals).__iter__() + assert next(a) == S.EmptySet + assert next(a) == FiniteSet(1) + assert next(a) == FiniteSet(2) + assert next(a) == FiniteSet(1, 2) + assert next(a) == FiniteSet(3) + assert next(a) == FiniteSet(1, 3) + assert next(a) == FiniteSet(2, 3) + assert next(a) == FiniteSet(1, 2, 3) + + +def test_powerset_contains(): + A = PowerSet(FiniteSet(1), evaluate=False) + assert A.contains(2) == Contains(2, A) + + x = Symbol('x') + + A = PowerSet(FiniteSet(x), evaluate=False) + assert A.contains(FiniteSet(1)) == Contains(FiniteSet(1), A) + + +def test_powerset_method(): + # EmptySet + A = FiniteSet() + pset = A.powerset() + assert len(pset) == 1 + assert pset == FiniteSet(S.EmptySet) + + # FiniteSets + A = FiniteSet(1, 2) + pset = A.powerset() + assert len(pset) == 2**len(A) + assert pset == FiniteSet(FiniteSet(), FiniteSet(1), + FiniteSet(2), A) + # Not finite sets + A = Interval(0, 1) + assert A.powerset() == PowerSet(A) + +def test_is_subset(): + # covers line 101-102 + # initialize powerset(1), which is a subset of powerset(1,2) + subset = PowerSet(FiniteSet(1)) + pset = PowerSet(FiniteSet(1, 2)) + bad_set = PowerSet(FiniteSet(2, 3)) + # assert "subset" is subset of pset == True + assert subset.is_subset(pset) + # assert "bad_set" is subset of pset == False + assert not pset.is_subset(bad_set) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/test_setexpr.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/test_setexpr.py new file mode 100644 index 0000000000000000000000000000000000000000..faab1261c8d3e86901b04d30e8bc94de31642b93 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/test_setexpr.py @@ -0,0 +1,317 @@ +from sympy.sets.setexpr import SetExpr +from sympy.sets import Interval, FiniteSet, Intersection, ImageSet, Union + +from sympy.core.expr import Expr +from sympy.core.function import Lambda +from sympy.core.numbers import (I, Rational, oo) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol, symbols) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import (Max, Min, sqrt) +from sympy.functions.elementary.trigonometric import cos +from sympy.sets.sets import Set + + +a, x = symbols("a, x") +_d = Dummy("d") + + +def test_setexpr(): + se = SetExpr(Interval(0, 1)) + assert isinstance(se.set, Set) + assert isinstance(se, Expr) + + +def test_scalar_funcs(): + assert SetExpr(Interval(0, 1)).set == Interval(0, 1) + a, b = Symbol('a', real=True), Symbol('b', real=True) + a, b = 1, 2 + # TODO: add support for more functions in the future: + for f in [exp, log]: + input_se = f(SetExpr(Interval(a, b))) + output = input_se.set + expected = Interval(Min(f(a), f(b)), Max(f(a), f(b))) + assert output == expected + + +def test_Add_Mul(): + assert (SetExpr(Interval(0, 1)) + 1).set == Interval(1, 2) + assert (SetExpr(Interval(0, 1))*2).set == Interval(0, 2) + + +def test_Pow(): + assert (SetExpr(Interval(0, 2))**2).set == Interval(0, 4) + + +def test_compound(): + assert (exp(SetExpr(Interval(0, 1))*2 + 1)).set == \ + Interval(exp(1), exp(3)) + + +def test_Interval_Interval(): + assert (SetExpr(Interval(1, 2)) + SetExpr(Interval(10, 20))).set == \ + Interval(11, 22) + assert (SetExpr(Interval(1, 2))*SetExpr(Interval(10, 20))).set == \ + Interval(10, 40) + + +def test_FiniteSet_FiniteSet(): + assert (SetExpr(FiniteSet(1, 2, 3)) + SetExpr(FiniteSet(1, 2))).set == \ + FiniteSet(2, 3, 4, 5) + assert (SetExpr(FiniteSet(1, 2, 3))*SetExpr(FiniteSet(1, 2))).set == \ + FiniteSet(1, 2, 3, 4, 6) + + +def test_Interval_FiniteSet(): + assert (SetExpr(FiniteSet(1, 2)) + SetExpr(Interval(0, 10))).set == \ + Interval(1, 12) + + +def test_Many_Sets(): + assert (SetExpr(Interval(0, 1)) + + SetExpr(Interval(2, 3)) + + SetExpr(FiniteSet(10, 11, 12))).set == Interval(12, 16) + + +def test_same_setexprs_are_not_identical(): + a = SetExpr(FiniteSet(0, 1)) + b = SetExpr(FiniteSet(0, 1)) + assert (a + b).set == FiniteSet(0, 1, 2) + + # Cannot detect the set being the same: + # assert (a + a).set == FiniteSet(0, 2) + + +def test_Interval_arithmetic(): + i12cc = SetExpr(Interval(1, 2)) + i12lo = SetExpr(Interval.Lopen(1, 2)) + i12ro = SetExpr(Interval.Ropen(1, 2)) + i12o = SetExpr(Interval.open(1, 2)) + + n23cc = SetExpr(Interval(-2, 3)) + n23lo = SetExpr(Interval.Lopen(-2, 3)) + n23ro = SetExpr(Interval.Ropen(-2, 3)) + n23o = SetExpr(Interval.open(-2, 3)) + + n3n2cc = SetExpr(Interval(-3, -2)) + + assert i12cc + i12cc == SetExpr(Interval(2, 4)) + assert i12cc - i12cc == SetExpr(Interval(-1, 1)) + assert i12cc*i12cc == SetExpr(Interval(1, 4)) + assert i12cc/i12cc == SetExpr(Interval(S.Half, 2)) + assert i12cc**2 == SetExpr(Interval(1, 4)) + assert i12cc**3 == SetExpr(Interval(1, 8)) + + assert i12lo + i12ro == SetExpr(Interval.open(2, 4)) + assert i12lo - i12ro == SetExpr(Interval.Lopen(-1, 1)) + assert i12lo*i12ro == SetExpr(Interval.open(1, 4)) + assert i12lo/i12ro == SetExpr(Interval.Lopen(S.Half, 2)) + assert i12lo + i12lo == SetExpr(Interval.Lopen(2, 4)) + assert i12lo - i12lo == SetExpr(Interval.open(-1, 1)) + assert i12lo*i12lo == SetExpr(Interval.Lopen(1, 4)) + assert i12lo/i12lo == SetExpr(Interval.open(S.Half, 2)) + assert i12lo + i12cc == SetExpr(Interval.Lopen(2, 4)) + assert i12lo - i12cc == SetExpr(Interval.Lopen(-1, 1)) + assert i12lo*i12cc == SetExpr(Interval.Lopen(1, 4)) + assert i12lo/i12cc == SetExpr(Interval.Lopen(S.Half, 2)) + assert i12lo + i12o == SetExpr(Interval.open(2, 4)) + assert i12lo - i12o == SetExpr(Interval.open(-1, 1)) + assert i12lo*i12o == SetExpr(Interval.open(1, 4)) + assert i12lo/i12o == SetExpr(Interval.open(S.Half, 2)) + assert i12lo**2 == SetExpr(Interval.Lopen(1, 4)) + assert i12lo**3 == SetExpr(Interval.Lopen(1, 8)) + + assert i12ro + i12ro == SetExpr(Interval.Ropen(2, 4)) + assert i12ro - i12ro == SetExpr(Interval.open(-1, 1)) + assert i12ro*i12ro == SetExpr(Interval.Ropen(1, 4)) + assert i12ro/i12ro == SetExpr(Interval.open(S.Half, 2)) + assert i12ro + i12cc == SetExpr(Interval.Ropen(2, 4)) + assert i12ro - i12cc == SetExpr(Interval.Ropen(-1, 1)) + assert i12ro*i12cc == SetExpr(Interval.Ropen(1, 4)) + assert i12ro/i12cc == SetExpr(Interval.Ropen(S.Half, 2)) + assert i12ro + i12o == SetExpr(Interval.open(2, 4)) + assert i12ro - i12o == SetExpr(Interval.open(-1, 1)) + assert i12ro*i12o == SetExpr(Interval.open(1, 4)) + assert i12ro/i12o == SetExpr(Interval.open(S.Half, 2)) + assert i12ro**2 == SetExpr(Interval.Ropen(1, 4)) + assert i12ro**3 == SetExpr(Interval.Ropen(1, 8)) + + assert i12o + i12lo == SetExpr(Interval.open(2, 4)) + assert i12o - i12lo == SetExpr(Interval.open(-1, 1)) + assert i12o*i12lo == SetExpr(Interval.open(1, 4)) + assert i12o/i12lo == SetExpr(Interval.open(S.Half, 2)) + assert i12o + i12ro == SetExpr(Interval.open(2, 4)) + assert i12o - i12ro == SetExpr(Interval.open(-1, 1)) + assert i12o*i12ro == SetExpr(Interval.open(1, 4)) + assert i12o/i12ro == SetExpr(Interval.open(S.Half, 2)) + assert i12o + i12cc == SetExpr(Interval.open(2, 4)) + assert i12o - i12cc == SetExpr(Interval.open(-1, 1)) + assert i12o*i12cc == SetExpr(Interval.open(1, 4)) + assert i12o/i12cc == SetExpr(Interval.open(S.Half, 2)) + assert i12o**2 == SetExpr(Interval.open(1, 4)) + assert i12o**3 == SetExpr(Interval.open(1, 8)) + + assert n23cc + n23cc == SetExpr(Interval(-4, 6)) + assert n23cc - n23cc == SetExpr(Interval(-5, 5)) + assert n23cc*n23cc == SetExpr(Interval(-6, 9)) + assert n23cc/n23cc == SetExpr(Interval.open(-oo, oo)) + assert n23cc + n23ro == SetExpr(Interval.Ropen(-4, 6)) + assert n23cc - n23ro == SetExpr(Interval.Lopen(-5, 5)) + assert n23cc*n23ro == SetExpr(Interval.Ropen(-6, 9)) + assert n23cc/n23ro == SetExpr(Interval.Lopen(-oo, oo)) + assert n23cc + n23lo == SetExpr(Interval.Lopen(-4, 6)) + assert n23cc - n23lo == SetExpr(Interval.Ropen(-5, 5)) + assert n23cc*n23lo == SetExpr(Interval(-6, 9)) + assert n23cc/n23lo == SetExpr(Interval.open(-oo, oo)) + assert n23cc + n23o == SetExpr(Interval.open(-4, 6)) + assert n23cc - n23o == SetExpr(Interval.open(-5, 5)) + assert n23cc*n23o == SetExpr(Interval.open(-6, 9)) + assert n23cc/n23o == SetExpr(Interval.open(-oo, oo)) + assert n23cc**2 == SetExpr(Interval(0, 9)) + assert n23cc**3 == SetExpr(Interval(-8, 27)) + + n32cc = SetExpr(Interval(-3, 2)) + n32lo = SetExpr(Interval.Lopen(-3, 2)) + n32ro = SetExpr(Interval.Ropen(-3, 2)) + assert n32cc*n32lo == SetExpr(Interval.Ropen(-6, 9)) + assert n32cc*n32cc == SetExpr(Interval(-6, 9)) + assert n32lo*n32cc == SetExpr(Interval.Ropen(-6, 9)) + assert n32cc*n32ro == SetExpr(Interval(-6, 9)) + assert n32lo*n32ro == SetExpr(Interval.Ropen(-6, 9)) + assert n32cc/n32lo == SetExpr(Interval.Ropen(-oo, oo)) + assert i12cc/n32lo == SetExpr(Interval.Ropen(-oo, oo)) + + assert n3n2cc**2 == SetExpr(Interval(4, 9)) + assert n3n2cc**3 == SetExpr(Interval(-27, -8)) + + assert n23cc + i12cc == SetExpr(Interval(-1, 5)) + assert n23cc - i12cc == SetExpr(Interval(-4, 2)) + assert n23cc*i12cc == SetExpr(Interval(-4, 6)) + assert n23cc/i12cc == SetExpr(Interval(-2, 3)) + + +def test_SetExpr_Intersection(): + x, y, z, w = symbols("x y z w") + set1 = Interval(x, y) + set2 = Interval(w, z) + inter = Intersection(set1, set2) + se = SetExpr(inter) + assert exp(se).set == Intersection( + ImageSet(Lambda(x, exp(x)), set1), + ImageSet(Lambda(x, exp(x)), set2)) + assert cos(se).set == ImageSet(Lambda(x, cos(x)), inter) + + +def test_SetExpr_Interval_div(): + # TODO: some expressions cannot be calculated due to bugs (currently + # commented): + assert SetExpr(Interval(-3, -2))/SetExpr(Interval(-2, 1)) == SetExpr(Interval(-oo, oo)) + assert SetExpr(Interval(2, 3))/SetExpr(Interval(-2, 2)) == SetExpr(Interval(-oo, oo)) + + assert SetExpr(Interval(-3, -2))/SetExpr(Interval(0, 4)) == SetExpr(Interval(-oo, Rational(-1, 2))) + assert SetExpr(Interval(2, 4))/SetExpr(Interval(-3, 0)) == SetExpr(Interval(-oo, Rational(-2, 3))) + assert SetExpr(Interval(2, 4))/SetExpr(Interval(0, 3)) == SetExpr(Interval(Rational(2, 3), oo)) + + # assert SetExpr(Interval(0, 1))/SetExpr(Interval(0, 1)) == SetExpr(Interval(0, oo)) + # assert SetExpr(Interval(-1, 0))/SetExpr(Interval(0, 1)) == SetExpr(Interval(-oo, 0)) + assert SetExpr(Interval(-1, 2))/SetExpr(Interval(-2, 2)) == SetExpr(Interval(-oo, oo)) + + assert 1/SetExpr(Interval(-1, 2)) == SetExpr(Union(Interval(-oo, -1), Interval(S.Half, oo))) + + assert 1/SetExpr(Interval(0, 2)) == SetExpr(Interval(S.Half, oo)) + assert (-1)/SetExpr(Interval(0, 2)) == SetExpr(Interval(-oo, Rational(-1, 2))) + assert 1/SetExpr(Interval(-oo, 0)) == SetExpr(Interval.open(-oo, 0)) + assert 1/SetExpr(Interval(-1, 0)) == SetExpr(Interval(-oo, -1)) + # assert (-2)/SetExpr(Interval(-oo, 0)) == SetExpr(Interval(0, oo)) + # assert 1/SetExpr(Interval(-oo, -1)) == SetExpr(Interval(-1, 0)) + + # assert SetExpr(Interval(1, 2))/a == Mul(SetExpr(Interval(1, 2)), 1/a, evaluate=False) + + # assert SetExpr(Interval(1, 2))/0 == SetExpr(Interval(1, 2))*zoo + # assert SetExpr(Interval(1, oo))/oo == SetExpr(Interval(0, oo)) + # assert SetExpr(Interval(1, oo))/(-oo) == SetExpr(Interval(-oo, 0)) + # assert SetExpr(Interval(-oo, -1))/oo == SetExpr(Interval(-oo, 0)) + # assert SetExpr(Interval(-oo, -1))/(-oo) == SetExpr(Interval(0, oo)) + # assert SetExpr(Interval(-oo, oo))/oo == SetExpr(Interval(-oo, oo)) + # assert SetExpr(Interval(-oo, oo))/(-oo) == SetExpr(Interval(-oo, oo)) + # assert SetExpr(Interval(-1, oo))/oo == SetExpr(Interval(0, oo)) + # assert SetExpr(Interval(-1, oo))/(-oo) == SetExpr(Interval(-oo, 0)) + # assert SetExpr(Interval(-oo, 1))/oo == SetExpr(Interval(-oo, 0)) + # assert SetExpr(Interval(-oo, 1))/(-oo) == SetExpr(Interval(0, oo)) + + +def test_SetExpr_Interval_pow(): + assert SetExpr(Interval(0, 2))**2 == SetExpr(Interval(0, 4)) + assert SetExpr(Interval(-1, 1))**2 == SetExpr(Interval(0, 1)) + assert SetExpr(Interval(1, 2))**2 == SetExpr(Interval(1, 4)) + assert SetExpr(Interval(-1, 2))**3 == SetExpr(Interval(-1, 8)) + assert SetExpr(Interval(-1, 1))**0 == SetExpr(FiniteSet(1)) + + + assert SetExpr(Interval(1, 2))**Rational(5, 2) == SetExpr(Interval(1, 4*sqrt(2))) + #assert SetExpr(Interval(-1, 2))**Rational(1, 3) == SetExpr(Interval(-1, 2**Rational(1, 3))) + #assert SetExpr(Interval(0, 2))**S.Half == SetExpr(Interval(0, sqrt(2))) + + #assert SetExpr(Interval(-4, 2))**Rational(2, 3) == SetExpr(Interval(0, 2*2**Rational(1, 3))) + + #assert SetExpr(Interval(-1, 5))**S.Half == SetExpr(Interval(0, sqrt(5))) + #assert SetExpr(Interval(-oo, 2))**S.Half == SetExpr(Interval(0, sqrt(2))) + #assert SetExpr(Interval(-2, 3))**(Rational(-1, 4)) == SetExpr(Interval(0, oo)) + + assert SetExpr(Interval(1, 5))**(-2) == SetExpr(Interval(Rational(1, 25), 1)) + assert SetExpr(Interval(-1, 3))**(-2) == SetExpr(Interval(0, oo)) + + assert SetExpr(Interval(0, 2))**(-2) == SetExpr(Interval(Rational(1, 4), oo)) + assert SetExpr(Interval(-1, 2))**(-3) == SetExpr(Union(Interval(-oo, -1), Interval(Rational(1, 8), oo))) + assert SetExpr(Interval(-3, -2))**(-3) == SetExpr(Interval(Rational(-1, 8), Rational(-1, 27))) + assert SetExpr(Interval(-3, -2))**(-2) == SetExpr(Interval(Rational(1, 9), Rational(1, 4))) + #assert SetExpr(Interval(0, oo))**S.Half == SetExpr(Interval(0, oo)) + #assert SetExpr(Interval(-oo, -1))**Rational(1, 3) == SetExpr(Interval(-oo, -1)) + #assert SetExpr(Interval(-2, 3))**(Rational(-1, 3)) == SetExpr(Interval(-oo, oo)) + + assert SetExpr(Interval(-oo, 0))**(-2) == SetExpr(Interval.open(0, oo)) + assert SetExpr(Interval(-2, 0))**(-2) == SetExpr(Interval(Rational(1, 4), oo)) + + assert SetExpr(Interval(Rational(1, 3), S.Half))**oo == SetExpr(FiniteSet(0)) + assert SetExpr(Interval(0, S.Half))**oo == SetExpr(FiniteSet(0)) + assert SetExpr(Interval(S.Half, 1))**oo == SetExpr(Interval(0, oo)) + assert SetExpr(Interval(0, 1))**oo == SetExpr(Interval(0, oo)) + assert SetExpr(Interval(2, 3))**oo == SetExpr(FiniteSet(oo)) + assert SetExpr(Interval(1, 2))**oo == SetExpr(Interval(0, oo)) + assert SetExpr(Interval(S.Half, 3))**oo == SetExpr(Interval(0, oo)) + assert SetExpr(Interval(Rational(-1, 3), Rational(-1, 4)))**oo == SetExpr(FiniteSet(0)) + assert SetExpr(Interval(-1, Rational(-1, 2)))**oo == SetExpr(Interval(-oo, oo)) + assert SetExpr(Interval(-3, -2))**oo == SetExpr(FiniteSet(-oo, oo)) + assert SetExpr(Interval(-2, -1))**oo == SetExpr(Interval(-oo, oo)) + assert SetExpr(Interval(-2, Rational(-1, 2)))**oo == SetExpr(Interval(-oo, oo)) + assert SetExpr(Interval(Rational(-1, 2), S.Half))**oo == SetExpr(FiniteSet(0)) + assert SetExpr(Interval(Rational(-1, 2), 1))**oo == SetExpr(Interval(0, oo)) + assert SetExpr(Interval(Rational(-2, 3), 2))**oo == SetExpr(Interval(0, oo)) + assert SetExpr(Interval(-1, 1))**oo == SetExpr(Interval(-oo, oo)) + assert SetExpr(Interval(-1, S.Half))**oo == SetExpr(Interval(-oo, oo)) + assert SetExpr(Interval(-1, 2))**oo == SetExpr(Interval(-oo, oo)) + assert SetExpr(Interval(-2, S.Half))**oo == SetExpr(Interval(-oo, oo)) + + assert (SetExpr(Interval(1, 2))**x).dummy_eq(SetExpr(ImageSet(Lambda(_d, _d**x), Interval(1, 2)))) + + assert SetExpr(Interval(2, 3))**(-oo) == SetExpr(FiniteSet(0)) + assert SetExpr(Interval(0, 2))**(-oo) == SetExpr(Interval(0, oo)) + assert (SetExpr(Interval(-1, 2))**(-oo)).dummy_eq(SetExpr(ImageSet(Lambda(_d, _d**(-oo)), Interval(-1, 2)))) + + +def test_SetExpr_Integers(): + assert SetExpr(S.Integers) + 1 == SetExpr(S.Integers) + assert (SetExpr(S.Integers) + I).dummy_eq( + SetExpr(ImageSet(Lambda(_d, _d + I), S.Integers))) + assert SetExpr(S.Integers)*(-1) == SetExpr(S.Integers) + assert (SetExpr(S.Integers)*2).dummy_eq( + SetExpr(ImageSet(Lambda(_d, 2*_d), S.Integers))) + assert (SetExpr(S.Integers)*I).dummy_eq( + SetExpr(ImageSet(Lambda(_d, I*_d), S.Integers))) + # issue #18050: + assert SetExpr(S.Integers)._eval_func(Lambda(x, I*x + 1)).dummy_eq( + SetExpr(ImageSet(Lambda(_d, I*_d + 1), S.Integers))) + # needs improvement: + assert (SetExpr(S.Integers)*I + 1).dummy_eq( + SetExpr(ImageSet(Lambda(x, x + 1), + ImageSet(Lambda(_d, _d*I), S.Integers)))) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/test_sets.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/test_sets.py new file mode 100644 index 0000000000000000000000000000000000000000..657ab19a90eb88ca48f266f7a5cf050504caed43 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/sets/tests/test_sets.py @@ -0,0 +1,1753 @@ +from sympy.concrete.summations import Sum +from sympy.core.add import Add +from sympy.core.containers import TupleKind +from sympy.core.function import Lambda +from sympy.core.kind import NumberKind, UndefinedKind +from sympy.core.numbers import (Float, I, Rational, nan, oo, pi, zoo) +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.core.sympify import sympify +from sympy.functions.elementary.miscellaneous import (Max, Min, sqrt) +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.logic.boolalg import (false, true) +from sympy.matrices.kind import MatrixKind +from sympy.matrices.dense import Matrix +from sympy.polys.rootoftools import rootof +from sympy.sets.contains import Contains +from sympy.sets.fancysets import (ImageSet, Range) +from sympy.sets.sets import (Complement, DisjointUnion, FiniteSet, Intersection, Interval, ProductSet, Set, SymmetricDifference, Union, imageset, SetKind) +from mpmath import mpi + +from sympy.core.expr import unchanged +from sympy.core.relational import Eq, Ne, Le, Lt, LessThan +from sympy.logic import And, Or, Xor +from sympy.testing.pytest import raises, XFAIL, warns_deprecated_sympy +from sympy.utilities.iterables import cartes + +from sympy.abc import x, y, z, m, n + +EmptySet = S.EmptySet + +def test_imageset(): + ints = S.Integers + assert imageset(x, x - 1, S.Naturals) is S.Naturals0 + assert imageset(x, x + 1, S.Naturals0) is S.Naturals + assert imageset(x, abs(x), S.Naturals0) is S.Naturals0 + assert imageset(x, abs(x), S.Naturals) is S.Naturals + assert imageset(x, abs(x), S.Integers) is S.Naturals0 + # issue 16878a + r = symbols('r', real=True) + assert imageset(x, (x, x), S.Reals)._contains((1, r)) == None + assert imageset(x, (x, x), S.Reals)._contains((1, 2)) == False + assert (r, r) in imageset(x, (x, x), S.Reals) + assert 1 + I in imageset(x, x + I, S.Reals) + assert {1} not in imageset(x, (x,), S.Reals) + assert (1, 1) not in imageset(x, (x,), S.Reals) + raises(TypeError, lambda: imageset(x, ints)) + raises(ValueError, lambda: imageset(x, y, z, ints)) + raises(ValueError, lambda: imageset(Lambda(x, cos(x)), y)) + assert (1, 2) in imageset(Lambda((x, y), (x, y)), ints, ints) + raises(ValueError, lambda: imageset(Lambda(x, x), ints, ints)) + assert imageset(cos, ints) == ImageSet(Lambda(x, cos(x)), ints) + def f(x): + return cos(x) + assert imageset(f, ints) == imageset(x, cos(x), ints) + f = lambda x: cos(x) + assert imageset(f, ints) == ImageSet(Lambda(x, cos(x)), ints) + assert imageset(x, 1, ints) == FiniteSet(1) + assert imageset(x, y, ints) == {y} + assert imageset((x, y), (1, z), ints, S.Reals) == {(1, z)} + clash = Symbol('x', integer=true) + assert (str(imageset(lambda x: x + clash, Interval(-2, 1)).lamda.expr) + in ('x0 + x', 'x + x0')) + x1, x2 = symbols("x1, x2") + assert imageset(lambda x, y: + Add(x, y), Interval(1, 2), Interval(2, 3)).dummy_eq( + ImageSet(Lambda((x1, x2), x1 + x2), + Interval(1, 2), Interval(2, 3))) + + +def test_is_empty(): + for s in [S.Naturals, S.Naturals0, S.Integers, S.Rationals, S.Reals, + S.UniversalSet]: + assert s.is_empty is False + + assert S.EmptySet.is_empty is True + + +def test_is_finiteset(): + for s in [S.Naturals, S.Naturals0, S.Integers, S.Rationals, S.Reals, + S.UniversalSet]: + assert s.is_finite_set is False + + assert S.EmptySet.is_finite_set is True + + assert FiniteSet(1, 2).is_finite_set is True + assert Interval(1, 2).is_finite_set is False + assert Interval(x, y).is_finite_set is None + assert ProductSet(FiniteSet(1), FiniteSet(2)).is_finite_set is True + assert ProductSet(FiniteSet(1), Interval(1, 2)).is_finite_set is False + assert ProductSet(FiniteSet(1), Interval(x, y)).is_finite_set is None + assert Union(Interval(0, 1), Interval(2, 3)).is_finite_set is False + assert Union(FiniteSet(1), Interval(2, 3)).is_finite_set is False + assert Union(FiniteSet(1), FiniteSet(2)).is_finite_set is True + assert Union(FiniteSet(1), Interval(x, y)).is_finite_set is None + assert Intersection(Interval(x, y), FiniteSet(1)).is_finite_set is True + assert Intersection(Interval(x, y), Interval(1, 2)).is_finite_set is None + assert Intersection(FiniteSet(x), FiniteSet(y)).is_finite_set is True + assert Complement(FiniteSet(1), Interval(x, y)).is_finite_set is True + assert Complement(Interval(x, y), FiniteSet(1)).is_finite_set is None + assert Complement(Interval(1, 2), FiniteSet(x)).is_finite_set is False + assert DisjointUnion(Interval(-5, 3), FiniteSet(x, y)).is_finite_set is False + assert DisjointUnion(S.EmptySet, FiniteSet(x, y), S.EmptySet).is_finite_set is True + + +def test_deprecated_is_EmptySet(): + with warns_deprecated_sympy(): + S.EmptySet.is_EmptySet + + with warns_deprecated_sympy(): + FiniteSet(1).is_EmptySet + + +def test_interval_arguments(): + assert Interval(0, oo) == Interval(0, oo, False, True) + assert Interval(0, oo).right_open is true + assert Interval(-oo, 0) == Interval(-oo, 0, True, False) + assert Interval(-oo, 0).left_open is true + assert Interval(oo, -oo) == S.EmptySet + assert Interval(oo, oo) == S.EmptySet + assert Interval(-oo, -oo) == S.EmptySet + assert Interval(oo, x) == S.EmptySet + assert Interval(oo, oo) == S.EmptySet + assert Interval(x, -oo) == S.EmptySet + assert Interval(x, x) == {x} + + assert isinstance(Interval(1, 1), FiniteSet) + e = Sum(x, (x, 1, 3)) + assert isinstance(Interval(e, e), FiniteSet) + + assert Interval(1, 0) == S.EmptySet + assert Interval(1, 1).measure == 0 + + assert Interval(1, 1, False, True) == S.EmptySet + assert Interval(1, 1, True, False) == S.EmptySet + assert Interval(1, 1, True, True) == S.EmptySet + + + assert isinstance(Interval(0, Symbol('a')), Interval) + assert Interval(Symbol('a', positive=True), 0) == S.EmptySet + raises(ValueError, lambda: Interval(0, S.ImaginaryUnit)) + raises(ValueError, lambda: Interval(0, Symbol('z', extended_real=False))) + raises(ValueError, lambda: Interval(x, x + S.ImaginaryUnit)) + + raises(NotImplementedError, lambda: Interval(0, 1, And(x, y))) + raises(NotImplementedError, lambda: Interval(0, 1, False, And(x, y))) + raises(NotImplementedError, lambda: Interval(0, 1, z, And(x, y))) + + +def test_interval_symbolic_end_points(): + a = Symbol('a', real=True) + + assert Union(Interval(0, a), Interval(0, 3)).sup == Max(a, 3) + assert Union(Interval(a, 0), Interval(-3, 0)).inf == Min(-3, a) + + assert Interval(0, a).contains(1) == LessThan(1, a) + + +def test_interval_is_empty(): + x, y = symbols('x, y') + r = Symbol('r', real=True) + p = Symbol('p', positive=True) + n = Symbol('n', negative=True) + nn = Symbol('nn', nonnegative=True) + assert Interval(1, 2).is_empty == False + assert Interval(3, 3).is_empty == False # FiniteSet + assert Interval(r, r).is_empty == False # FiniteSet + assert Interval(r, r + nn).is_empty == False + assert Interval(x, x).is_empty == False + assert Interval(1, oo).is_empty == False + assert Interval(-oo, oo).is_empty == False + assert Interval(-oo, 1).is_empty == False + assert Interval(x, y).is_empty == None + assert Interval(r, oo).is_empty == False # real implies finite + assert Interval(n, 0).is_empty == False + assert Interval(n, 0, left_open=True).is_empty == False + assert Interval(p, 0).is_empty == True # EmptySet + assert Interval(nn, 0).is_empty == None + assert Interval(n, p).is_empty == False + assert Interval(0, p, left_open=True).is_empty == False + assert Interval(0, p, right_open=True).is_empty == False + assert Interval(0, nn, left_open=True).is_empty == None + assert Interval(0, nn, right_open=True).is_empty == None + + +def test_union(): + assert Union(Interval(1, 2), Interval(2, 3)) == Interval(1, 3) + assert Union(Interval(1, 2), Interval(2, 3, True)) == Interval(1, 3) + assert Union(Interval(1, 3), Interval(2, 4)) == Interval(1, 4) + assert Union(Interval(1, 2), Interval(1, 3)) == Interval(1, 3) + assert Union(Interval(1, 3), Interval(1, 2)) == Interval(1, 3) + assert Union(Interval(1, 3, False, True), Interval(1, 2)) == \ + Interval(1, 3, False, True) + assert Union(Interval(1, 3), Interval(1, 2, False, True)) == Interval(1, 3) + assert Union(Interval(1, 2, True), Interval(1, 3)) == Interval(1, 3) + assert Union(Interval(1, 2, True), Interval(1, 3, True)) == \ + Interval(1, 3, True) + assert Union(Interval(1, 2, True), Interval(1, 3, True, True)) == \ + Interval(1, 3, True, True) + assert Union(Interval(1, 2, True, True), Interval(1, 3, True)) == \ + Interval(1, 3, True) + assert Union(Interval(1, 3), Interval(2, 3)) == Interval(1, 3) + assert Union(Interval(1, 3, False, True), Interval(2, 3)) == \ + Interval(1, 3) + assert Union(Interval(1, 2, False, True), Interval(2, 3, True)) != \ + Interval(1, 3) + assert Union(Interval(1, 2), S.EmptySet) == Interval(1, 2) + assert Union(S.EmptySet) == S.EmptySet + + assert Union(Interval(0, 1), *[FiniteSet(1.0/n) for n in range(1, 10)]) == \ + Interval(0, 1) + # issue #18241: + x = Symbol('x') + assert Union(Interval(0, 1), FiniteSet(1, x)) == Union( + Interval(0, 1), FiniteSet(x)) + assert unchanged(Union, Interval(0, 1), FiniteSet(2, x)) + + assert Interval(1, 2).union(Interval(2, 3)) == \ + Interval(1, 2) + Interval(2, 3) + + assert Interval(1, 2).union(Interval(2, 3)) == Interval(1, 3) + + assert Union(Set()) == Set() + + assert FiniteSet(1) + FiniteSet(2) + FiniteSet(3) == FiniteSet(1, 2, 3) + assert FiniteSet('ham') + FiniteSet('eggs') == FiniteSet('ham', 'eggs') + assert FiniteSet(1, 2, 3) + S.EmptySet == FiniteSet(1, 2, 3) + + assert FiniteSet(1, 2, 3) & FiniteSet(2, 3, 4) == FiniteSet(2, 3) + assert FiniteSet(1, 2, 3) | FiniteSet(2, 3, 4) == FiniteSet(1, 2, 3, 4) + + assert FiniteSet(1, 2, 3) & S.EmptySet == S.EmptySet + assert FiniteSet(1, 2, 3) | S.EmptySet == FiniteSet(1, 2, 3) + + x = Symbol("x") + y = Symbol("y") + z = Symbol("z") + assert S.EmptySet | FiniteSet(x, FiniteSet(y, z)) == \ + FiniteSet(x, FiniteSet(y, z)) + + # Test that Intervals and FiniteSets play nicely + assert Interval(1, 3) + FiniteSet(2) == Interval(1, 3) + assert Interval(1, 3, True, True) + FiniteSet(3) == \ + Interval(1, 3, True, False) + X = Interval(1, 3) + FiniteSet(5) + Y = Interval(1, 2) + FiniteSet(3) + XandY = X.intersect(Y) + assert 2 in X and 3 in X and 3 in XandY + assert XandY.is_subset(X) and XandY.is_subset(Y) + + raises(TypeError, lambda: Union(1, 2, 3)) + + assert X.is_iterable is False + + # issue 7843 + assert Union(S.EmptySet, FiniteSet(-sqrt(-I), sqrt(-I))) == \ + FiniteSet(-sqrt(-I), sqrt(-I)) + + assert Union(S.Reals, S.Integers) == S.Reals + + +def test_union_iter(): + # Use Range because it is ordered + u = Union(Range(3), Range(5), Range(4), evaluate=False) + + # Round robin + assert list(u) == [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 4] + + +def test_union_is_empty(): + assert (Interval(x, y) + FiniteSet(1)).is_empty == False + assert (Interval(x, y) + Interval(-x, y)).is_empty == None + + +def test_difference(): + assert Interval(1, 3) - Interval(1, 2) == Interval(2, 3, True) + assert Interval(1, 3) - Interval(2, 3) == Interval(1, 2, False, True) + assert Interval(1, 3, True) - Interval(2, 3) == Interval(1, 2, True, True) + assert Interval(1, 3, True) - Interval(2, 3, True) == \ + Interval(1, 2, True, False) + assert Interval(0, 2) - FiniteSet(1) == \ + Union(Interval(0, 1, False, True), Interval(1, 2, True, False)) + + # issue #18119 + assert S.Reals - FiniteSet(I) == S.Reals + assert S.Reals - FiniteSet(-I, I) == S.Reals + assert Interval(0, 10) - FiniteSet(-I, I) == Interval(0, 10) + assert Interval(0, 10) - FiniteSet(1, I) == Union( + Interval.Ropen(0, 1), Interval.Lopen(1, 10)) + assert S.Reals - FiniteSet(1, 2 + I, x, y**2) == Complement( + Union(Interval.open(-oo, 1), Interval.open(1, oo)), FiniteSet(x, y**2), + evaluate=False) + + assert FiniteSet(1, 2, 3) - FiniteSet(2) == FiniteSet(1, 3) + assert FiniteSet('ham', 'eggs') - FiniteSet('eggs') == FiniteSet('ham') + assert FiniteSet(1, 2, 3, 4) - Interval(2, 10, True, False) == \ + FiniteSet(1, 2) + assert FiniteSet(1, 2, 3, 4) - S.EmptySet == FiniteSet(1, 2, 3, 4) + assert Union(Interval(0, 2), FiniteSet(2, 3, 4)) - Interval(1, 3) == \ + Union(Interval(0, 1, False, True), FiniteSet(4)) + + assert -1 in S.Reals - S.Naturals + + +def test_Complement(): + A = FiniteSet(1, 3, 4) + B = FiniteSet(3, 4) + C = Interval(1, 3) + D = Interval(1, 2) + + assert Complement(A, B, evaluate=False).is_iterable is True + assert Complement(A, C, evaluate=False).is_iterable is True + assert Complement(C, D, evaluate=False).is_iterable is None + + assert FiniteSet(*Complement(A, B, evaluate=False)) == FiniteSet(1) + assert FiniteSet(*Complement(A, C, evaluate=False)) == FiniteSet(4) + raises(TypeError, lambda: FiniteSet(*Complement(C, A, evaluate=False))) + + assert Complement(Interval(1, 3), Interval(1, 2)) == Interval(2, 3, True) + assert Complement(FiniteSet(1, 3, 4), FiniteSet(3, 4)) == FiniteSet(1) + assert Complement(Union(Interval(0, 2), FiniteSet(2, 3, 4)), + Interval(1, 3)) == \ + Union(Interval(0, 1, False, True), FiniteSet(4)) + + assert 3 not in Complement(Interval(0, 5), Interval(1, 4), evaluate=False) + assert -1 in Complement(S.Reals, S.Naturals, evaluate=False) + assert 1 not in Complement(S.Reals, S.Naturals, evaluate=False) + + assert Complement(S.Integers, S.UniversalSet) == EmptySet + assert S.UniversalSet.complement(S.Integers) == EmptySet + + assert (0 not in S.Reals.intersect(S.Integers - FiniteSet(0))) + + assert S.EmptySet - S.Integers == S.EmptySet + + assert (S.Integers - FiniteSet(0)) - FiniteSet(1) == S.Integers - FiniteSet(0, 1) + + assert S.Reals - Union(S.Naturals, FiniteSet(pi)) == \ + Intersection(S.Reals - S.Naturals, S.Reals - FiniteSet(pi)) + # issue 12712 + assert Complement(FiniteSet(x, y, 2), Interval(-10, 10)) == \ + Complement(FiniteSet(x, y), Interval(-10, 10)) + + A = FiniteSet(*symbols('a:c')) + B = FiniteSet(*symbols('d:f')) + assert unchanged(Complement, ProductSet(A, A), B) + + A2 = ProductSet(A, A) + B3 = ProductSet(B, B, B) + assert A2 - B3 == A2 + assert B3 - A2 == B3 + + +def test_set_operations_nonsets(): + '''Tests that e.g. FiniteSet(1) * 2 raises TypeError''' + ops = [ + lambda a, b: a + b, + lambda a, b: a - b, + lambda a, b: a * b, + lambda a, b: a / b, + lambda a, b: a // b, + lambda a, b: a | b, + lambda a, b: a & b, + lambda a, b: a ^ b, + # FiniteSet(1) ** 2 gives a ProductSet + #lambda a, b: a ** b, + ] + Sx = FiniteSet(x) + Sy = FiniteSet(y) + sets = [ + {1}, + FiniteSet(1), + Interval(1, 2), + Union(Sx, Interval(1, 2)), + Intersection(Sx, Sy), + Complement(Sx, Sy), + ProductSet(Sx, Sy), + S.EmptySet, + ] + nums = [0, 1, 2, S(0), S(1), S(2)] + + for si in sets: + for ni in nums: + for op in ops: + raises(TypeError, lambda : op(si, ni)) + raises(TypeError, lambda : op(ni, si)) + raises(TypeError, lambda: si ** object()) + raises(TypeError, lambda: si ** {1}) + + +def test_complement(): + assert Complement({1, 2}, {1}) == {2} + assert Interval(0, 1).complement(S.Reals) == \ + Union(Interval(-oo, 0, True, True), Interval(1, oo, True, True)) + assert Interval(0, 1, True, False).complement(S.Reals) == \ + Union(Interval(-oo, 0, True, False), Interval(1, oo, True, True)) + assert Interval(0, 1, False, True).complement(S.Reals) == \ + Union(Interval(-oo, 0, True, True), Interval(1, oo, False, True)) + assert Interval(0, 1, True, True).complement(S.Reals) == \ + Union(Interval(-oo, 0, True, False), Interval(1, oo, False, True)) + + assert S.UniversalSet.complement(S.EmptySet) == S.EmptySet + assert S.UniversalSet.complement(S.Reals) == S.EmptySet + assert S.UniversalSet.complement(S.UniversalSet) == S.EmptySet + + assert S.EmptySet.complement(S.Reals) == S.Reals + + assert Union(Interval(0, 1), Interval(2, 3)).complement(S.Reals) == \ + Union(Interval(-oo, 0, True, True), Interval(1, 2, True, True), + Interval(3, oo, True, True)) + + assert FiniteSet(0).complement(S.Reals) == \ + Union(Interval(-oo, 0, True, True), Interval(0, oo, True, True)) + + assert (FiniteSet(5) + Interval(S.NegativeInfinity, + 0)).complement(S.Reals) == \ + Interval(0, 5, True, True) + Interval(5, S.Infinity, True, True) + + assert FiniteSet(1, 2, 3).complement(S.Reals) == \ + Interval(S.NegativeInfinity, 1, True, True) + \ + Interval(1, 2, True, True) + Interval(2, 3, True, True) +\ + Interval(3, S.Infinity, True, True) + + assert FiniteSet(x).complement(S.Reals) == Complement(S.Reals, FiniteSet(x)) + + assert FiniteSet(0, x).complement(S.Reals) == Complement(Interval(-oo, 0, True, True) + + Interval(0, oo, True, True) + , FiniteSet(x), evaluate=False) + + square = Interval(0, 1) * Interval(0, 1) + notsquare = square.complement(S.Reals*S.Reals) + + assert all(pt in square for pt in [(0, 0), (.5, .5), (1, 0), (1, 1)]) + assert not any( + pt in notsquare for pt in [(0, 0), (.5, .5), (1, 0), (1, 1)]) + assert not any(pt in square for pt in [(-1, 0), (1.5, .5), (10, 10)]) + assert all(pt in notsquare for pt in [(-1, 0), (1.5, .5), (10, 10)]) + + +def test_intersect1(): + assert all(S.Integers.intersection(i) is i for i in + (S.Naturals, S.Naturals0)) + assert all(i.intersection(S.Integers) is i for i in + (S.Naturals, S.Naturals0)) + s = S.Naturals0 + assert S.Naturals.intersection(s) is S.Naturals + assert s.intersection(S.Naturals) is S.Naturals + x = Symbol('x') + assert Interval(0, 2).intersect(Interval(1, 2)) == Interval(1, 2) + assert Interval(0, 2).intersect(Interval(1, 2, True)) == \ + Interval(1, 2, True) + assert Interval(0, 2, True).intersect(Interval(1, 2)) == \ + Interval(1, 2, False, False) + assert Interval(0, 2, True, True).intersect(Interval(1, 2)) == \ + Interval(1, 2, False, True) + assert Interval(0, 2).intersect(Union(Interval(0, 1), Interval(2, 3))) == \ + Union(Interval(0, 1), Interval(2, 2)) + + assert FiniteSet(1, 2).intersect(FiniteSet(1, 2, 3)) == FiniteSet(1, 2) + assert FiniteSet(1, 2, x).intersect(FiniteSet(x)) == FiniteSet(x) + assert FiniteSet('ham', 'eggs').intersect(FiniteSet('ham')) == \ + FiniteSet('ham') + assert FiniteSet(1, 2, 3, 4, 5).intersect(S.EmptySet) == S.EmptySet + + assert Interval(0, 5).intersect(FiniteSet(1, 3)) == FiniteSet(1, 3) + assert Interval(0, 1, True, True).intersect(FiniteSet(1)) == S.EmptySet + + assert Union(Interval(0, 1), Interval(2, 3)).intersect(Interval(1, 2)) == \ + Union(Interval(1, 1), Interval(2, 2)) + assert Union(Interval(0, 1), Interval(2, 3)).intersect(Interval(0, 2)) == \ + Union(Interval(0, 1), Interval(2, 2)) + assert Union(Interval(0, 1), Interval(2, 3)).intersect(Interval(1, 2, True, True)) == \ + S.EmptySet + assert Union(Interval(0, 1), Interval(2, 3)).intersect(S.EmptySet) == \ + S.EmptySet + assert Union(Interval(0, 5), FiniteSet('ham')).intersect(FiniteSet(2, 3, 4, 5, 6)) == \ + Intersection(FiniteSet(2, 3, 4, 5, 6), Union(FiniteSet('ham'), Interval(0, 5))) + assert Intersection(FiniteSet(1, 2, 3), Interval(2, x), Interval(3, y)) == \ + Intersection(FiniteSet(3), Interval(2, x), Interval(3, y), evaluate=False) + assert Intersection(FiniteSet(1, 2), Interval(0, 3), Interval(x, y)) == \ + Intersection({1, 2}, Interval(x, y), evaluate=False) + assert Intersection(FiniteSet(1, 2, 4), Interval(0, 3), Interval(x, y)) == \ + Intersection({1, 2}, Interval(x, y), evaluate=False) + # XXX: Is the real=True necessary here? + # https://github.com/sympy/sympy/issues/17532 + m, n = symbols('m, n', real=True) + assert Intersection(FiniteSet(m), FiniteSet(m, n), Interval(m, m+1)) == \ + FiniteSet(m) + + # issue 8217 + assert Intersection(FiniteSet(x), FiniteSet(y)) == \ + Intersection(FiniteSet(x), FiniteSet(y), evaluate=False) + assert FiniteSet(x).intersect(S.Reals) == \ + Intersection(S.Reals, FiniteSet(x), evaluate=False) + + # tests for the intersection alias + assert Interval(0, 5).intersection(FiniteSet(1, 3)) == FiniteSet(1, 3) + assert Interval(0, 1, True, True).intersection(FiniteSet(1)) == S.EmptySet + + assert Union(Interval(0, 1), Interval(2, 3)).intersection(Interval(1, 2)) == \ + Union(Interval(1, 1), Interval(2, 2)) + + # canonical boundary selected + a = sqrt(2*sqrt(6) + 5) + b = sqrt(2) + sqrt(3) + assert Interval(a, 4).intersection(Interval(b, 5)) == Interval(b, 4) + assert Interval(1, a).intersection(Interval(0, b)) == Interval(1, b) + + +def test_intersection_interval_float(): + # intersection of Intervals with mixed Rational/Float boundaries should + # lead to Float boundaries in all cases regardless of which Interval is + # open or closed. + typs = [ + (Interval, Interval, Interval), + (Interval, Interval.open, Interval.open), + (Interval, Interval.Lopen, Interval.Lopen), + (Interval, Interval.Ropen, Interval.Ropen), + (Interval.open, Interval.open, Interval.open), + (Interval.open, Interval.Lopen, Interval.open), + (Interval.open, Interval.Ropen, Interval.open), + (Interval.Lopen, Interval.Lopen, Interval.Lopen), + (Interval.Lopen, Interval.Ropen, Interval.open), + (Interval.Ropen, Interval.Ropen, Interval.Ropen), + ] + + as_float = lambda a1, a2: a2 if isinstance(a2, float) else a1 + + for t1, t2, t3 in typs: + for t1i, t2i in [(t1, t2), (t2, t1)]: + for a1, a2, b1, b2 in cartes([2, 2.0], [2, 2.0], [3, 3.0], [3, 3.0]): + I1 = t1(a1, b1) + I2 = t2(a2, b2) + I3 = t3(as_float(a1, a2), as_float(b1, b2)) + assert I1.intersect(I2) == I3 + + +def test_intersection(): + # iterable + i = Intersection(FiniteSet(1, 2, 3), Interval(2, 5), evaluate=False) + assert i.is_iterable + assert set(i) == {S(2), S(3)} + + # challenging intervals + x = Symbol('x', real=True) + i = Intersection(Interval(0, 3), Interval(x, 6)) + assert (5 in i) is False + raises(TypeError, lambda: 2 in i) + + # Singleton special cases + assert Intersection(Interval(0, 1), S.EmptySet) == S.EmptySet + assert Intersection(Interval(-oo, oo), Interval(-oo, x)) == Interval(-oo, x) + + # Products + line = Interval(0, 5) + i = Intersection(line**2, line**3, evaluate=False) + assert (2, 2) not in i + assert (2, 2, 2) not in i + raises(TypeError, lambda: list(i)) + + a = Intersection(Intersection(S.Integers, S.Naturals, evaluate=False), S.Reals, evaluate=False) + assert a._argset == frozenset([Intersection(S.Naturals, S.Integers, evaluate=False), S.Reals]) + + assert Intersection(S.Complexes, FiniteSet(S.ComplexInfinity)) == S.EmptySet + + # issue 12178 + assert Intersection() == S.UniversalSet + + # issue 16987 + assert Intersection({1}, {1}, {x}) == Intersection({1}, {x}) + + +def test_issue_9623(): + n = Symbol('n') + + a = S.Reals + b = Interval(0, oo) + c = FiniteSet(n) + + assert Intersection(a, b, c) == Intersection(b, c) + assert Intersection(Interval(1, 2), Interval(3, 4), FiniteSet(n)) == EmptySet + + +def test_is_disjoint(): + assert Interval(0, 2).is_disjoint(Interval(1, 2)) == False + assert Interval(0, 2).is_disjoint(Interval(3, 4)) == True + + +def test_ProductSet__len__(): + A = FiniteSet(1, 2) + B = FiniteSet(1, 2, 3) + assert ProductSet(A).__len__() == 2 + assert ProductSet(A).__len__() is not S(2) + assert ProductSet(A, B).__len__() == 6 + assert ProductSet(A, B).__len__() is not S(6) + + +def test_ProductSet(): + # ProductSet is always a set of Tuples + assert ProductSet(S.Reals) == S.Reals ** 1 + assert ProductSet(S.Reals, S.Reals) == S.Reals ** 2 + assert ProductSet(S.Reals, S.Reals, S.Reals) == S.Reals ** 3 + + assert ProductSet(S.Reals) != S.Reals + assert ProductSet(S.Reals, S.Reals) == S.Reals * S.Reals + assert ProductSet(S.Reals, S.Reals, S.Reals) != S.Reals * S.Reals * S.Reals + assert ProductSet(S.Reals, S.Reals, S.Reals) == (S.Reals * S.Reals * S.Reals).flatten() + + assert 1 not in ProductSet(S.Reals) + assert (1,) in ProductSet(S.Reals) + + assert 1 not in ProductSet(S.Reals, S.Reals) + assert (1, 2) in ProductSet(S.Reals, S.Reals) + assert (1, I) not in ProductSet(S.Reals, S.Reals) + + assert (1, 2, 3) in ProductSet(S.Reals, S.Reals, S.Reals) + assert (1, 2, 3) in S.Reals ** 3 + assert (1, 2, 3) not in S.Reals * S.Reals * S.Reals + assert ((1, 2), 3) in S.Reals * S.Reals * S.Reals + assert (1, (2, 3)) not in S.Reals * S.Reals * S.Reals + assert (1, (2, 3)) in S.Reals * (S.Reals * S.Reals) + + assert ProductSet() == FiniteSet(()) + assert ProductSet(S.Reals, S.EmptySet) == S.EmptySet + + # See GH-17458 + + for ni in range(5): + Rn = ProductSet(*(S.Reals,) * ni) + assert (1,) * ni in Rn + assert 1 not in Rn + + assert (S.Reals * S.Reals) * S.Reals != S.Reals * (S.Reals * S.Reals) + + S1 = S.Reals + S2 = S.Integers + x1 = pi + x2 = 3 + assert x1 in S1 + assert x2 in S2 + assert (x1, x2) in S1 * S2 + S3 = S1 * S2 + x3 = (x1, x2) + assert x3 in S3 + assert (x3, x3) in S3 * S3 + assert x3 + x3 not in S3 * S3 + + raises(ValueError, lambda: S.Reals**-1) + with warns_deprecated_sympy(): + ProductSet(FiniteSet(s) for s in range(2)) + raises(TypeError, lambda: ProductSet(None)) + + S1 = FiniteSet(1, 2) + S2 = FiniteSet(3, 4) + S3 = ProductSet(S1, S2) + assert (S3.as_relational(x, y) + == And(S1.as_relational(x), S2.as_relational(y)) + == And(Or(Eq(x, 1), Eq(x, 2)), Or(Eq(y, 3), Eq(y, 4)))) + raises(ValueError, lambda: S3.as_relational(x)) + raises(ValueError, lambda: S3.as_relational(x, 1)) + raises(ValueError, lambda: ProductSet(Interval(0, 1)).as_relational(x, y)) + + Z2 = ProductSet(S.Integers, S.Integers) + assert Z2.contains((1, 2)) is S.true + assert Z2.contains((1,)) is S.false + assert Z2.contains(x) == Contains(x, Z2, evaluate=False) + assert Z2.contains(x).subs(x, 1) is S.false + assert Z2.contains((x, 1)).subs(x, 2) is S.true + assert Z2.contains((x, y)) == Contains(x, S.Integers) & Contains(y, S.Integers) + assert unchanged(Contains, (x, y), Z2) + assert Contains((1, 2), Z2) is S.true + + +def test_ProductSet_of_single_arg_is_not_arg(): + assert unchanged(ProductSet, Interval(0, 1)) + assert unchanged(ProductSet, ProductSet(Interval(0, 1))) + + +def test_ProductSet_is_empty(): + assert ProductSet(S.Integers, S.Reals).is_empty == False + assert ProductSet(Interval(x, 1), S.Reals).is_empty == None + + +def test_interval_subs(): + a = Symbol('a', real=True) + + assert Interval(0, a).subs(a, 2) == Interval(0, 2) + assert Interval(a, 0).subs(a, 2) == S.EmptySet + + +def test_interval_to_mpi(): + assert Interval(0, 1).to_mpi() == mpi(0, 1) + assert Interval(0, 1, True, False).to_mpi() == mpi(0, 1) + assert type(Interval(0, 1).to_mpi()) == type(mpi(0, 1)) + + +def test_set_evalf(): + assert Interval(S(11)/64, S.Half).evalf() == Interval( + Float('0.171875'), Float('0.5')) + assert Interval(x, S.Half, right_open=True).evalf() == Interval( + x, Float('0.5'), right_open=True) + assert Interval(-oo, S.Half).evalf() == Interval(-oo, Float('0.5')) + assert FiniteSet(2, x).evalf() == FiniteSet(Float('2.0'), x) + + +def test_measure(): + a = Symbol('a', real=True) + + assert Interval(1, 3).measure == 2 + assert Interval(0, a).measure == a + assert Interval(1, a).measure == a - 1 + + assert Union(Interval(1, 2), Interval(3, 4)).measure == 2 + assert Union(Interval(1, 2), Interval(3, 4), FiniteSet(5, 6, 7)).measure \ + == 2 + + assert FiniteSet(1, 2, oo, a, -oo, -5).measure == 0 + + assert S.EmptySet.measure == 0 + + square = Interval(0, 10) * Interval(0, 10) + offsetsquare = Interval(5, 15) * Interval(5, 15) + band = Interval(-oo, oo) * Interval(2, 4) + + assert square.measure == offsetsquare.measure == 100 + assert (square + offsetsquare).measure == 175 # there is some overlap + assert (square - offsetsquare).measure == 75 + assert (square * FiniteSet(1, 2, 3)).measure == 0 + assert (square.intersect(band)).measure == 20 + assert (square + band).measure is oo + assert (band * FiniteSet(1, 2, 3)).measure is nan + + +def test_is_subset(): + assert Interval(0, 1).is_subset(Interval(0, 2)) is True + assert Interval(0, 3).is_subset(Interval(0, 2)) is False + assert Interval(0, 1).is_subset(FiniteSet(0, 1)) is False + + assert FiniteSet(1, 2).is_subset(FiniteSet(1, 2, 3, 4)) + assert FiniteSet(4, 5).is_subset(FiniteSet(1, 2, 3, 4)) is False + assert FiniteSet(1).is_subset(Interval(0, 2)) + assert FiniteSet(1, 2).is_subset(Interval(0, 2, True, True)) is False + assert (Interval(1, 2) + FiniteSet(3)).is_subset( + Interval(0, 2, False, True) + FiniteSet(2, 3)) + + assert Interval(3, 4).is_subset(Union(Interval(0, 1), Interval(2, 5))) is True + assert Interval(3, 6).is_subset(Union(Interval(0, 1), Interval(2, 5))) is False + + assert FiniteSet(1, 2, 3, 4).is_subset(Interval(0, 5)) is True + assert S.EmptySet.is_subset(FiniteSet(1, 2, 3)) is True + + assert Interval(0, 1).is_subset(S.EmptySet) is False + assert S.EmptySet.is_subset(S.EmptySet) is True + + raises(ValueError, lambda: S.EmptySet.is_subset(1)) + + # tests for the issubset alias + assert FiniteSet(1, 2, 3, 4).issubset(Interval(0, 5)) is True + assert S.EmptySet.issubset(FiniteSet(1, 2, 3)) is True + + assert S.Naturals.is_subset(S.Integers) + assert S.Naturals0.is_subset(S.Integers) + + assert FiniteSet(x).is_subset(FiniteSet(y)) is None + assert FiniteSet(x).is_subset(FiniteSet(y).subs(y, x)) is True + assert FiniteSet(x).is_subset(FiniteSet(y).subs(y, x+1)) is False + + assert Interval(0, 1).is_subset(Interval(0, 1, left_open=True)) is False + assert Interval(-2, 3).is_subset(Union(Interval(-oo, -2), Interval(3, oo))) is False + + n = Symbol('n', integer=True) + assert Range(-3, 4, 1).is_subset(FiniteSet(-10, 10)) is False + assert Range(S(10)**100).is_subset(FiniteSet(0, 1, 2)) is False + assert Range(6, 0, -2).is_subset(FiniteSet(2, 4, 6)) is True + assert Range(1, oo).is_subset(FiniteSet(1, 2)) is False + assert Range(-oo, 1).is_subset(FiniteSet(1)) is False + assert Range(3).is_subset(FiniteSet(0, 1, n)) is None + assert Range(n, n + 2).is_subset(FiniteSet(n, n + 1)) is True + assert Range(5).is_subset(Interval(0, 4, right_open=True)) is False + #issue 19513 + assert imageset(Lambda(n, 1/n), S.Integers).is_subset(S.Reals) is None + +def test_is_proper_subset(): + assert Interval(0, 1).is_proper_subset(Interval(0, 2)) is True + assert Interval(0, 3).is_proper_subset(Interval(0, 2)) is False + assert S.EmptySet.is_proper_subset(FiniteSet(1, 2, 3)) is True + + raises(ValueError, lambda: Interval(0, 1).is_proper_subset(0)) + + +def test_is_superset(): + assert Interval(0, 1).is_superset(Interval(0, 2)) == False + assert Interval(0, 3).is_superset(Interval(0, 2)) + + assert FiniteSet(1, 2).is_superset(FiniteSet(1, 2, 3, 4)) == False + assert FiniteSet(4, 5).is_superset(FiniteSet(1, 2, 3, 4)) == False + assert FiniteSet(1).is_superset(Interval(0, 2)) == False + assert FiniteSet(1, 2).is_superset(Interval(0, 2, True, True)) == False + assert (Interval(1, 2) + FiniteSet(3)).is_superset( + Interval(0, 2, False, True) + FiniteSet(2, 3)) == False + + assert Interval(3, 4).is_superset(Union(Interval(0, 1), Interval(2, 5))) == False + + assert FiniteSet(1, 2, 3, 4).is_superset(Interval(0, 5)) == False + assert S.EmptySet.is_superset(FiniteSet(1, 2, 3)) == False + + assert Interval(0, 1).is_superset(S.EmptySet) == True + assert S.EmptySet.is_superset(S.EmptySet) == True + + raises(ValueError, lambda: S.EmptySet.is_superset(1)) + + # tests for the issuperset alias + assert Interval(0, 1).issuperset(S.EmptySet) == True + assert S.EmptySet.issuperset(S.EmptySet) == True + + +def test_is_proper_superset(): + assert Interval(0, 1).is_proper_superset(Interval(0, 2)) is False + assert Interval(0, 3).is_proper_superset(Interval(0, 2)) is True + assert FiniteSet(1, 2, 3).is_proper_superset(S.EmptySet) is True + + raises(ValueError, lambda: Interval(0, 1).is_proper_superset(0)) + + +def test_contains(): + assert Interval(0, 2).contains(1) is S.true + assert Interval(0, 2).contains(3) is S.false + assert Interval(0, 2, True, False).contains(0) is S.false + assert Interval(0, 2, True, False).contains(2) is S.true + assert Interval(0, 2, False, True).contains(0) is S.true + assert Interval(0, 2, False, True).contains(2) is S.false + assert Interval(0, 2, True, True).contains(0) is S.false + assert Interval(0, 2, True, True).contains(2) is S.false + + assert (Interval(0, 2) in Interval(0, 2)) is False + + assert FiniteSet(1, 2, 3).contains(2) is S.true + assert FiniteSet(1, 2, Symbol('x')).contains(Symbol('x')) is S.true + + assert FiniteSet(y)._contains(x) == Eq(y, x, evaluate=False) + raises(TypeError, lambda: x in FiniteSet(y)) + assert FiniteSet({x, y})._contains({x}) == Eq({x, y}, {x}, evaluate=False) + assert FiniteSet({x, y}).subs(y, x)._contains({x}) is S.true + assert FiniteSet({x, y}).subs(y, x+1)._contains({x}) is S.false + + # issue 8197 + from sympy.abc import a, b + assert FiniteSet(b).contains(-a) == Eq(b, -a) + assert FiniteSet(b).contains(a) == Eq(b, a) + assert FiniteSet(a).contains(1) == Eq(a, 1) + raises(TypeError, lambda: 1 in FiniteSet(a)) + + # issue 8209 + rad1 = Pow(Pow(2, Rational(1, 3)) - 1, Rational(1, 3)) + rad2 = Pow(Rational(1, 9), Rational(1, 3)) - Pow(Rational(2, 9), Rational(1, 3)) + Pow(Rational(4, 9), Rational(1, 3)) + s1 = FiniteSet(rad1) + s2 = FiniteSet(rad2) + assert s1 - s2 == S.EmptySet + + items = [1, 2, S.Infinity, S('ham'), -1.1] + fset = FiniteSet(*items) + assert all(item in fset for item in items) + assert all(fset.contains(item) is S.true for item in items) + + assert Union(Interval(0, 1), Interval(2, 5)).contains(3) is S.true + assert Union(Interval(0, 1), Interval(2, 5)).contains(6) is S.false + assert Union(Interval(0, 1), FiniteSet(2, 5)).contains(3) is S.false + + assert S.EmptySet.contains(1) is S.false + assert FiniteSet(rootof(x**3 + x - 1, 0)).contains(S.Infinity) is S.false + + assert rootof(x**5 + x**3 + 1, 0) in S.Reals + assert not rootof(x**5 + x**3 + 1, 1) in S.Reals + + # non-bool results + assert Union(Interval(1, 2), Interval(3, 4)).contains(x) == \ + Or(And(S.One <= x, x <= 2), And(S(3) <= x, x <= 4)) + assert Intersection(Interval(1, x), Interval(2, 3)).contains(y) == \ + And(y <= 3, y <= x, S.One <= y, S(2) <= y) + + assert (S.Complexes).contains(S.ComplexInfinity) == S.false + + +def test_interval_symbolic(): + x = Symbol('x') + e = Interval(0, 1) + assert e.contains(x) == And(S.Zero <= x, x <= 1) + raises(TypeError, lambda: x in e) + e = Interval(0, 1, True, True) + assert e.contains(x) == And(S.Zero < x, x < 1) + c = Symbol('c', real=False) + assert Interval(x, x + 1).contains(c) == False + e = Symbol('e', extended_real=True) + assert Interval(-oo, oo).contains(e) == And( + S.NegativeInfinity < e, e < S.Infinity) + + +def test_union_contains(): + x = Symbol('x') + i1 = Interval(0, 1) + i2 = Interval(2, 3) + i3 = Union(i1, i2) + assert i3.as_relational(x) == Or(And(S.Zero <= x, x <= 1), And(S(2) <= x, x <= 3)) + raises(TypeError, lambda: x in i3) + e = i3.contains(x) + assert e == i3.as_relational(x) + assert e.subs(x, -0.5) is false + assert e.subs(x, 0.5) is true + assert e.subs(x, 1.5) is false + assert e.subs(x, 2.5) is true + assert e.subs(x, 3.5) is false + + U = Interval(0, 2, True, True) + Interval(10, oo) + FiniteSet(-1, 2, 5, 6) + assert all(el not in U for el in [0, 4, -oo]) + assert all(el in U for el in [2, 5, 10]) + + +def test_is_number(): + assert Interval(0, 1).is_number is False + assert Set().is_number is False + + +def test_Interval_is_left_unbounded(): + assert Interval(3, 4).is_left_unbounded is False + assert Interval(-oo, 3).is_left_unbounded is True + assert Interval(Float("-inf"), 3).is_left_unbounded is True + + +def test_Interval_is_right_unbounded(): + assert Interval(3, 4).is_right_unbounded is False + assert Interval(3, oo).is_right_unbounded is True + assert Interval(3, Float("+inf")).is_right_unbounded is True + + +def test_Interval_as_relational(): + x = Symbol('x') + + assert Interval(-1, 2, False, False).as_relational(x) == \ + And(Le(-1, x), Le(x, 2)) + assert Interval(-1, 2, True, False).as_relational(x) == \ + And(Lt(-1, x), Le(x, 2)) + assert Interval(-1, 2, False, True).as_relational(x) == \ + And(Le(-1, x), Lt(x, 2)) + assert Interval(-1, 2, True, True).as_relational(x) == \ + And(Lt(-1, x), Lt(x, 2)) + + assert Interval(-oo, 2, right_open=False).as_relational(x) == And(Lt(-oo, x), Le(x, 2)) + assert Interval(-oo, 2, right_open=True).as_relational(x) == And(Lt(-oo, x), Lt(x, 2)) + + assert Interval(-2, oo, left_open=False).as_relational(x) == And(Le(-2, x), Lt(x, oo)) + assert Interval(-2, oo, left_open=True).as_relational(x) == And(Lt(-2, x), Lt(x, oo)) + + assert Interval(-oo, oo).as_relational(x) == And(Lt(-oo, x), Lt(x, oo)) + x = Symbol('x', real=True) + y = Symbol('y', real=True) + assert Interval(x, y).as_relational(x) == (x <= y) + assert Interval(y, x).as_relational(x) == (y <= x) + + +def test_Finite_as_relational(): + x = Symbol('x') + y = Symbol('y') + + assert FiniteSet(1, 2).as_relational(x) == Or(Eq(x, 1), Eq(x, 2)) + assert FiniteSet(y, -5).as_relational(x) == Or(Eq(x, y), Eq(x, -5)) + + +def test_Union_as_relational(): + x = Symbol('x') + assert (Interval(0, 1) + FiniteSet(2)).as_relational(x) == \ + Or(And(Le(0, x), Le(x, 1)), Eq(x, 2)) + assert (Interval(0, 1, True, True) + FiniteSet(1)).as_relational(x) == \ + And(Lt(0, x), Le(x, 1)) + assert Or(x < 0, x > 0).as_set().as_relational(x) == \ + And((x > -oo), (x < oo), Ne(x, 0)) + assert (Interval.Ropen(1, 3) + Interval.Lopen(3, 5) + ).as_relational(x) == And(Ne(x,3),(x>=1),(x<=5)) + + +def test_Intersection_as_relational(): + x = Symbol('x') + assert (Intersection(Interval(0, 1), FiniteSet(2), + evaluate=False).as_relational(x) + == And(And(Le(0, x), Le(x, 1)), Eq(x, 2))) + + +def test_Complement_as_relational(): + x = Symbol('x') + expr = Complement(Interval(0, 1), FiniteSet(2), evaluate=False) + assert expr.as_relational(x) == \ + And(Le(0, x), Le(x, 1), Ne(x, 2)) + + +@XFAIL +def test_Complement_as_relational_fail(): + x = Symbol('x') + expr = Complement(Interval(0, 1), FiniteSet(2), evaluate=False) + # XXX This example fails because 0 <= x changes to x >= 0 + # during the evaluation. + assert expr.as_relational(x) == \ + (0 <= x) & (x <= 1) & Ne(x, 2) + + +def test_SymmetricDifference_as_relational(): + x = Symbol('x') + expr = SymmetricDifference(Interval(0, 1), FiniteSet(2), evaluate=False) + assert expr.as_relational(x) == Xor(Eq(x, 2), Le(0, x) & Le(x, 1)) + + +def test_EmptySet(): + assert S.EmptySet.as_relational(Symbol('x')) is S.false + assert S.EmptySet.intersect(S.UniversalSet) == S.EmptySet + assert S.EmptySet.boundary == S.EmptySet + + +def test_finite_basic(): + x = Symbol('x') + A = FiniteSet(1, 2, 3) + B = FiniteSet(3, 4, 5) + AorB = Union(A, B) + AandB = A.intersect(B) + assert A.is_subset(AorB) and B.is_subset(AorB) + assert AandB.is_subset(A) + assert AandB == FiniteSet(3) + + assert A.inf == 1 and A.sup == 3 + assert AorB.inf == 1 and AorB.sup == 5 + assert FiniteSet(x, 1, 5).sup == Max(x, 5) + assert FiniteSet(x, 1, 5).inf == Min(x, 1) + + # issue 7335 + assert FiniteSet(S.EmptySet) != S.EmptySet + assert FiniteSet(FiniteSet(1, 2, 3)) != FiniteSet(1, 2, 3) + assert FiniteSet((1, 2, 3)) != FiniteSet(1, 2, 3) + + # Ensure a variety of types can exist in a FiniteSet + assert FiniteSet((1, 2), A, -5, x, 'eggs', x**2) + + assert (A > B) is False + assert (A >= B) is False + assert (A < B) is False + assert (A <= B) is False + assert AorB > A and AorB > B + assert AorB >= A and AorB >= B + assert A >= A and A <= A + assert A >= AandB and B >= AandB + assert A > AandB and B > AandB + + +def test_product_basic(): + H, T = 'H', 'T' + unit_line = Interval(0, 1) + d6 = FiniteSet(1, 2, 3, 4, 5, 6) + d4 = FiniteSet(1, 2, 3, 4) + coin = FiniteSet(H, T) + + square = unit_line * unit_line + + assert (0, 0) in square + assert 0 not in square + assert (H, T) in coin ** 2 + assert (.5, .5, .5) in (square * unit_line).flatten() + assert ((.5, .5), .5) in square * unit_line + assert (H, 3, 3) in (coin * d6 * d6).flatten() + assert ((H, 3), 3) in coin * d6 * d6 + HH, TT = sympify(H), sympify(T) + assert set(coin**2) == {(HH, HH), (HH, TT), (TT, HH), (TT, TT)} + + assert (d4*d4).is_subset(d6*d6) + + assert square.complement(Interval(-oo, oo)*Interval(-oo, oo)) == Union( + (Interval(-oo, 0, True, True) + + Interval(1, oo, True, True))*Interval(-oo, oo), + Interval(-oo, oo)*(Interval(-oo, 0, True, True) + + Interval(1, oo, True, True))) + + assert (Interval(-5, 5)**3).is_subset(Interval(-10, 10)**3) + assert not (Interval(-10, 10)**3).is_subset(Interval(-5, 5)**3) + assert not (Interval(-5, 5)**2).is_subset(Interval(-10, 10)**3) + + assert (Interval(.2, .5)*FiniteSet(.5)).is_subset(square) # segment in square + + assert len(coin*coin*coin) == 8 + assert len(S.EmptySet*S.EmptySet) == 0 + assert len(S.EmptySet*coin) == 0 + raises(TypeError, lambda: len(coin*Interval(0, 2))) + + +def test_real(): + x = Symbol('x', real=True) + + I = Interval(0, 5) + J = Interval(10, 20) + A = FiniteSet(1, 2, 30, x, S.Pi) + B = FiniteSet(-4, 0) + C = FiniteSet(100) + D = FiniteSet('Ham', 'Eggs') + + assert all(s.is_subset(S.Reals) for s in [I, J, A, B, C]) + assert not D.is_subset(S.Reals) + assert all((a + b).is_subset(S.Reals) for a in [I, J, A, B, C] for b in [I, J, A, B, C]) + assert not any((a + D).is_subset(S.Reals) for a in [I, J, A, B, C, D]) + + assert not (I + A + D).is_subset(S.Reals) + + +def test_supinf(): + x = Symbol('x', real=True) + y = Symbol('y', real=True) + + assert (Interval(0, 1) + FiniteSet(2)).sup == 2 + assert (Interval(0, 1) + FiniteSet(2)).inf == 0 + assert (Interval(0, 1) + FiniteSet(x)).sup == Max(1, x) + assert (Interval(0, 1) + FiniteSet(x)).inf == Min(0, x) + assert FiniteSet(5, 1, x).sup == Max(5, x) + assert FiniteSet(5, 1, x).inf == Min(1, x) + assert FiniteSet(5, 1, x, y).sup == Max(5, x, y) + assert FiniteSet(5, 1, x, y).inf == Min(1, x, y) + assert FiniteSet(5, 1, x, y, S.Infinity, S.NegativeInfinity).sup == \ + S.Infinity + assert FiniteSet(5, 1, x, y, S.Infinity, S.NegativeInfinity).inf == \ + S.NegativeInfinity + assert FiniteSet('Ham', 'Eggs').sup == Max('Ham', 'Eggs') + + +def test_universalset(): + U = S.UniversalSet + x = Symbol('x') + assert U.as_relational(x) is S.true + assert U.union(Interval(2, 4)) == U + + assert U.intersect(Interval(2, 4)) == Interval(2, 4) + assert U.measure is S.Infinity + assert U.boundary == S.EmptySet + assert U.contains(0) is S.true + + +def test_Union_of_ProductSets_shares(): + line = Interval(0, 2) + points = FiniteSet(0, 1, 2) + assert Union(line * line, line * points) == line * line + + +def test_Interval_free_symbols(): + # issue 6211 + assert Interval(0, 1).free_symbols == set() + x = Symbol('x', real=True) + assert Interval(0, x).free_symbols == {x} + + +def test_image_interval(): + x = Symbol('x', real=True) + a = Symbol('a', real=True) + assert imageset(x, 2*x, Interval(-2, 1)) == Interval(-4, 2) + assert imageset(x, 2*x, Interval(-2, 1, True, False)) == \ + Interval(-4, 2, True, False) + assert imageset(x, x**2, Interval(-2, 1, True, False)) == \ + Interval(0, 4, False, True) + assert imageset(x, x**2, Interval(-2, 1)) == Interval(0, 4) + assert imageset(x, x**2, Interval(-2, 1, True, False)) == \ + Interval(0, 4, False, True) + assert imageset(x, x**2, Interval(-2, 1, True, True)) == \ + Interval(0, 4, False, True) + assert imageset(x, (x - 2)**2, Interval(1, 3)) == Interval(0, 1) + assert imageset(x, 3*x**4 - 26*x**3 + 78*x**2 - 90*x, Interval(0, 4)) == \ + Interval(-35, 0) # Multiple Maxima + assert imageset(x, x + 1/x, Interval(-oo, oo)) == Interval(-oo, -2) \ + + Interval(2, oo) # Single Infinite discontinuity + assert imageset(x, 1/x + 1/(x-1)**2, Interval(0, 2, True, False)) == \ + Interval(Rational(3, 2), oo, False) # Multiple Infinite discontinuities + + # Test for Python lambda + assert imageset(lambda x: 2*x, Interval(-2, 1)) == Interval(-4, 2) + + assert imageset(Lambda(x, a*x), Interval(0, 1)) == \ + ImageSet(Lambda(x, a*x), Interval(0, 1)) + + assert imageset(Lambda(x, sin(cos(x))), Interval(0, 1)) == \ + ImageSet(Lambda(x, sin(cos(x))), Interval(0, 1)) + + +def test_image_piecewise(): + f = Piecewise((x, x <= -1), (1/x**2, x <= 5), (x**3, True)) + f1 = Piecewise((0, x <= 1), (1, x <= 2), (2, True)) + assert imageset(x, f, Interval(-5, 5)) == Union(Interval(-5, -1), Interval(Rational(1, 25), oo)) + assert imageset(x, f1, Interval(1, 2)) == FiniteSet(0, 1) + + +@XFAIL # See: https://github.com/sympy/sympy/pull/2723#discussion_r8659826 +def test_image_Intersection(): + x = Symbol('x', real=True) + y = Symbol('y', real=True) + assert imageset(x, x**2, Interval(-2, 0).intersect(Interval(x, y))) == \ + Interval(0, 4).intersect(Interval(Min(x**2, y**2), Max(x**2, y**2))) + + +def test_image_FiniteSet(): + x = Symbol('x', real=True) + assert imageset(x, 2*x, FiniteSet(1, 2, 3)) == FiniteSet(2, 4, 6) + + +def test_image_Union(): + x = Symbol('x', real=True) + assert imageset(x, x**2, Interval(-2, 0) + FiniteSet(1, 2, 3)) == \ + (Interval(0, 4) + FiniteSet(9)) + + +def test_image_EmptySet(): + x = Symbol('x', real=True) + assert imageset(x, 2*x, S.EmptySet) == S.EmptySet + + +def test_issue_5724_7680(): + assert I not in S.Reals # issue 7680 + assert Interval(-oo, oo).contains(I) is S.false + + +def test_boundary(): + assert FiniteSet(1).boundary == FiniteSet(1) + assert all(Interval(0, 1, left_open, right_open).boundary == FiniteSet(0, 1) + for left_open in (true, false) for right_open in (true, false)) + + +def test_boundary_Union(): + assert (Interval(0, 1) + Interval(2, 3)).boundary == FiniteSet(0, 1, 2, 3) + assert ((Interval(0, 1, False, True) + + Interval(1, 2, True, False)).boundary == FiniteSet(0, 1, 2)) + + assert (Interval(0, 1) + FiniteSet(2)).boundary == FiniteSet(0, 1, 2) + assert Union(Interval(0, 10), Interval(5, 15), evaluate=False).boundary \ + == FiniteSet(0, 15) + + assert Union(Interval(0, 10), Interval(0, 1), evaluate=False).boundary \ + == FiniteSet(0, 10) + assert Union(Interval(0, 10, True, True), + Interval(10, 15, True, True), evaluate=False).boundary \ + == FiniteSet(0, 10, 15) + + +@XFAIL +def test_union_boundary_of_joining_sets(): + """ Testing the boundary of unions is a hard problem """ + assert Union(Interval(0, 10), Interval(10, 15), evaluate=False).boundary \ + == FiniteSet(0, 15) + + +def test_boundary_ProductSet(): + open_square = Interval(0, 1, True, True) ** 2 + assert open_square.boundary == (FiniteSet(0, 1) * Interval(0, 1) + + Interval(0, 1) * FiniteSet(0, 1)) + + second_square = Interval(1, 2, True, True) * Interval(0, 1, True, True) + assert (open_square + second_square).boundary == ( + FiniteSet(0, 1) * Interval(0, 1) + + FiniteSet(1, 2) * Interval(0, 1) + + Interval(0, 1) * FiniteSet(0, 1) + + Interval(1, 2) * FiniteSet(0, 1)) + + +def test_boundary_ProductSet_line(): + line_in_r2 = Interval(0, 1) * FiniteSet(0) + assert line_in_r2.boundary == line_in_r2 + + +def test_is_open(): + assert Interval(0, 1, False, False).is_open is False + assert Interval(0, 1, True, False).is_open is False + assert Interval(0, 1, True, True).is_open is True + assert FiniteSet(1, 2, 3).is_open is False + + +def test_is_closed(): + assert Interval(0, 1, False, False).is_closed is True + assert Interval(0, 1, True, False).is_closed is False + assert FiniteSet(1, 2, 3).is_closed is True + + +def test_closure(): + assert Interval(0, 1, False, True).closure == Interval(0, 1, False, False) + + +def test_interior(): + assert Interval(0, 1, False, True).interior == Interval(0, 1, True, True) + + +def test_issue_7841(): + raises(TypeError, lambda: x in S.Reals) + + +def test_Eq(): + assert Eq(Interval(0, 1), Interval(0, 1)) + assert Eq(Interval(0, 1), Interval(0, 2)) == False + + s1 = FiniteSet(0, 1) + s2 = FiniteSet(1, 2) + + assert Eq(s1, s1) + assert Eq(s1, s2) == False + + assert Eq(s1*s2, s1*s2) + assert Eq(s1*s2, s2*s1) == False + + assert unchanged(Eq, FiniteSet({x, y}), FiniteSet({x})) + assert Eq(FiniteSet({x, y}).subs(y, x), FiniteSet({x})) is S.true + assert Eq(FiniteSet({x, y}), FiniteSet({x})).subs(y, x) is S.true + assert Eq(FiniteSet({x, y}).subs(y, x+1), FiniteSet({x})) is S.false + assert Eq(FiniteSet({x, y}), FiniteSet({x})).subs(y, x+1) is S.false + + assert Eq(ProductSet({1}, {2}), Interval(1, 2)) is S.false + assert Eq(ProductSet({1}), ProductSet({1}, {2})) is S.false + + assert Eq(FiniteSet(()), FiniteSet(1)) is S.false + assert Eq(ProductSet(), FiniteSet(1)) is S.false + + i1 = Interval(0, 1) + i2 = Interval(x, y) + assert unchanged(Eq, ProductSet(i1, i1), ProductSet(i2, i2)) + + +def test_SymmetricDifference(): + A = FiniteSet(0, 1, 2, 3, 4, 5) + B = FiniteSet(2, 4, 6, 8, 10) + C = Interval(8, 10) + + assert SymmetricDifference(A, B, evaluate=False).is_iterable is True + assert SymmetricDifference(A, C, evaluate=False).is_iterable is None + assert FiniteSet(*SymmetricDifference(A, B, evaluate=False)) == \ + FiniteSet(0, 1, 3, 5, 6, 8, 10) + raises(TypeError, + lambda: FiniteSet(*SymmetricDifference(A, C, evaluate=False))) + + assert SymmetricDifference(FiniteSet(0, 1, 2, 3, 4, 5), \ + FiniteSet(2, 4, 6, 8, 10)) == FiniteSet(0, 1, 3, 5, 6, 8, 10) + assert SymmetricDifference(FiniteSet(2, 3, 4), FiniteSet(2, 3, 4 ,5)) \ + == FiniteSet(5) + assert FiniteSet(1, 2, 3, 4, 5) ^ FiniteSet(1, 2, 5, 6) == \ + FiniteSet(3, 4, 6) + assert Set(S(1), S(2), S(3)) ^ Set(S(2), S(3), S(4)) == Union(Set(S(1), S(2), S(3)) - Set(S(2), S(3), S(4)), \ + Set(S(2), S(3), S(4)) - Set(S(1), S(2), S(3))) + assert Interval(0, 4) ^ Interval(2, 5) == Union(Interval(0, 4) - \ + Interval(2, 5), Interval(2, 5) - Interval(0, 4)) + + +def test_issue_9536(): + from sympy.functions.elementary.exponential import log + a = Symbol('a', real=True) + assert FiniteSet(log(a)).intersect(S.Reals) == Intersection(S.Reals, FiniteSet(log(a))) + + +def test_issue_9637(): + n = Symbol('n') + a = FiniteSet(n) + b = FiniteSet(2, n) + assert Complement(S.Reals, a) == Complement(S.Reals, a, evaluate=False) + assert Complement(Interval(1, 3), a) == Complement(Interval(1, 3), a, evaluate=False) + assert Complement(Interval(1, 3), b) == \ + Complement(Union(Interval(1, 2, False, True), Interval(2, 3, True, False)), a) + assert Complement(a, S.Reals) == Complement(a, S.Reals, evaluate=False) + assert Complement(a, Interval(1, 3)) == Complement(a, Interval(1, 3), evaluate=False) + + +def test_issue_9808(): + # See https://github.com/sympy/sympy/issues/16342 + assert Complement(FiniteSet(y), FiniteSet(1)) == Complement(FiniteSet(y), FiniteSet(1), evaluate=False) + assert Complement(FiniteSet(1, 2, x), FiniteSet(x, y, 2, 3)) == \ + Complement(FiniteSet(1), FiniteSet(y), evaluate=False) + + +def test_issue_9956(): + assert Union(Interval(-oo, oo), FiniteSet(1)) == Interval(-oo, oo) + assert Interval(-oo, oo).contains(1) is S.true + + +def test_issue_Symbol_inter(): + i = Interval(0, oo) + r = S.Reals + mat = Matrix([0, 0, 0]) + assert Intersection(r, i, FiniteSet(m), FiniteSet(m, n)) == \ + Intersection(i, FiniteSet(m)) + assert Intersection(FiniteSet(1, m, n), FiniteSet(m, n, 2), i) == \ + Intersection(i, FiniteSet(m, n)) + assert Intersection(FiniteSet(m, n, x), FiniteSet(m, z), r) == \ + Intersection(Intersection({m, z}, {m, n, x}), r) + assert Intersection(FiniteSet(m, n, 3), FiniteSet(m, n, x), r) == \ + Intersection(FiniteSet(3, m, n), FiniteSet(m, n, x), r, evaluate=False) + assert Intersection(FiniteSet(m, n, 3), FiniteSet(m, n, 2, 3), r) == \ + Intersection(FiniteSet(3, m, n), r) + assert Intersection(r, FiniteSet(mat, 2, n), FiniteSet(0, mat, n)) == \ + Intersection(r, FiniteSet(n)) + assert Intersection(FiniteSet(sin(x), cos(x)), FiniteSet(sin(x), cos(x), 1), r) == \ + Intersection(r, FiniteSet(sin(x), cos(x))) + assert Intersection(FiniteSet(x**2, 1, sin(x)), FiniteSet(x**2, 2, sin(x)), r) == \ + Intersection(r, FiniteSet(x**2, sin(x))) + + +def test_issue_11827(): + assert S.Naturals0**4 + + +def test_issue_10113(): + f = x**2/(x**2 - 4) + assert imageset(x, f, S.Reals) == Union(Interval(-oo, 0), Interval(1, oo, True, True)) + assert imageset(x, f, Interval(-2, 2)) == Interval(-oo, 0) + assert imageset(x, f, Interval(-2, 3)) == Union(Interval(-oo, 0), Interval(Rational(9, 5), oo)) + + +def test_issue_10248(): + raises( + TypeError, lambda: list(Intersection(S.Reals, FiniteSet(x))) + ) + A = Symbol('A', real=True) + assert list(Intersection(S.Reals, FiniteSet(A))) == [A] + + +def test_issue_9447(): + a = Interval(0, 1) + Interval(2, 3) + assert Complement(S.UniversalSet, a) == Complement( + S.UniversalSet, Union(Interval(0, 1), Interval(2, 3)), evaluate=False) + assert Complement(S.Naturals, a) == Complement( + S.Naturals, Union(Interval(0, 1), Interval(2, 3)), evaluate=False) + + +def test_issue_10337(): + assert (FiniteSet(2) == 3) is False + assert (FiniteSet(2) != 3) is True + raises(TypeError, lambda: FiniteSet(2) < 3) + raises(TypeError, lambda: FiniteSet(2) <= 3) + raises(TypeError, lambda: FiniteSet(2) > 3) + raises(TypeError, lambda: FiniteSet(2) >= 3) + + +def test_issue_10326(): + bad = [ + EmptySet, + FiniteSet(1), + Interval(1, 2), + S.ComplexInfinity, + S.ImaginaryUnit, + S.Infinity, + S.NaN, + S.NegativeInfinity, + ] + interval = Interval(0, 5) + for i in bad: + assert i not in interval + + x = Symbol('x', real=True) + nr = Symbol('nr', extended_real=False) + assert x + 1 in Interval(x, x + 4) + assert nr not in Interval(x, x + 4) + assert Interval(1, 2) in FiniteSet(Interval(0, 5), Interval(1, 2)) + assert Interval(-oo, oo).contains(oo) is S.false + assert Interval(-oo, oo).contains(-oo) is S.false + + +def test_issue_2799(): + U = S.UniversalSet + a = Symbol('a', real=True) + inf_interval = Interval(a, oo) + R = S.Reals + + assert U + inf_interval == inf_interval + U + assert U + R == R + U + assert R + inf_interval == inf_interval + R + + +def test_issue_9706(): + assert Interval(-oo, 0).closure == Interval(-oo, 0, True, False) + assert Interval(0, oo).closure == Interval(0, oo, False, True) + assert Interval(-oo, oo).closure == Interval(-oo, oo) + + +def test_issue_8257(): + reals_plus_infinity = Union(Interval(-oo, oo), FiniteSet(oo)) + reals_plus_negativeinfinity = Union(Interval(-oo, oo), FiniteSet(-oo)) + assert Interval(-oo, oo) + FiniteSet(oo) == reals_plus_infinity + assert FiniteSet(oo) + Interval(-oo, oo) == reals_plus_infinity + assert Interval(-oo, oo) + FiniteSet(-oo) == reals_plus_negativeinfinity + assert FiniteSet(-oo) + Interval(-oo, oo) == reals_plus_negativeinfinity + + +def test_issue_10931(): + assert S.Integers - S.Integers == EmptySet + assert S.Integers - S.Reals == EmptySet + + +def test_issue_11174(): + soln = Intersection(Interval(-oo, oo), FiniteSet(-x), evaluate=False) + assert Intersection(FiniteSet(-x), S.Reals) == soln + + soln = Intersection(S.Reals, FiniteSet(x), evaluate=False) + assert Intersection(FiniteSet(x), S.Reals) == soln + + +def test_issue_18505(): + assert ImageSet(Lambda(n, sqrt(pi*n/2 - 1 + pi/2)), S.Integers).contains(0) == \ + Contains(0, ImageSet(Lambda(n, sqrt(pi*n/2 - 1 + pi/2)), S.Integers)) + + +def test_finite_set_intersection(): + # The following should not produce recursion errors + # Note: some of these are not completely correct. See + # https://github.com/sympy/sympy/issues/16342. + assert Intersection(FiniteSet(-oo, x), FiniteSet(x)) == FiniteSet(x) + assert Intersection._handle_finite_sets([FiniteSet(-oo, x), FiniteSet(0, x)]) == FiniteSet(x) + + assert Intersection._handle_finite_sets([FiniteSet(-oo, x), FiniteSet(x)]) == FiniteSet(x) + assert Intersection._handle_finite_sets([FiniteSet(2, 3, x, y), FiniteSet(1, 2, x)]) == \ + Intersection._handle_finite_sets([FiniteSet(1, 2, x), FiniteSet(2, 3, x, y)]) == \ + Intersection(FiniteSet(1, 2, x), FiniteSet(2, 3, x, y)) == \ + Intersection(FiniteSet(1, 2, x), FiniteSet(2, x, y)) + + assert FiniteSet(1+x-y) & FiniteSet(1) == \ + FiniteSet(1) & FiniteSet(1+x-y) == \ + Intersection(FiniteSet(1+x-y), FiniteSet(1), evaluate=False) + + assert FiniteSet(1) & FiniteSet(x) == FiniteSet(x) & FiniteSet(1) == \ + Intersection(FiniteSet(1), FiniteSet(x), evaluate=False) + + assert FiniteSet({x}) & FiniteSet({x, y}) == \ + Intersection(FiniteSet({x}), FiniteSet({x, y}), evaluate=False) + + +def test_union_intersection_constructor(): + # The actual exception does not matter here, so long as these fail + sets = [FiniteSet(1), FiniteSet(2)] + raises(Exception, lambda: Union(sets)) + raises(Exception, lambda: Intersection(sets)) + raises(Exception, lambda: Union(tuple(sets))) + raises(Exception, lambda: Intersection(tuple(sets))) + raises(Exception, lambda: Union(i for i in sets)) + raises(Exception, lambda: Intersection(i for i in sets)) + + # Python sets are treated the same as FiniteSet + # The union of a single set (of sets) is the set (of sets) itself + assert Union(set(sets)) == FiniteSet(*sets) + assert Intersection(set(sets)) == FiniteSet(*sets) + + assert Union({1}, {2}) == FiniteSet(1, 2) + assert Intersection({1, 2}, {2, 3}) == FiniteSet(2) + + +def test_Union_contains(): + assert zoo not in Union( + Interval.open(-oo, 0), Interval.open(0, oo)) + + +@XFAIL +def test_issue_16878b(): + # in intersection_sets for (ImageSet, Set) there is no code + # that handles the base_set of S.Reals like there is + # for Integers + assert imageset(x, (x, x), S.Reals).is_subset(S.Reals**2) is True + +def test_DisjointUnion(): + assert DisjointUnion(FiniteSet(1, 2, 3), FiniteSet(1, 2, 3), FiniteSet(1, 2, 3)).rewrite(Union) == (FiniteSet(1, 2, 3) * FiniteSet(0, 1, 2)) + assert DisjointUnion(Interval(1, 3), Interval(2, 4)).rewrite(Union) == Union(Interval(1, 3) * FiniteSet(0), Interval(2, 4) * FiniteSet(1)) + assert DisjointUnion(Interval(0, 5), Interval(0, 5)).rewrite(Union) == Union(Interval(0, 5) * FiniteSet(0), Interval(0, 5) * FiniteSet(1)) + assert DisjointUnion(Interval(-1, 2), S.EmptySet, S.EmptySet).rewrite(Union) == Interval(-1, 2) * FiniteSet(0) + assert DisjointUnion(Interval(-1, 2)).rewrite(Union) == Interval(-1, 2) * FiniteSet(0) + assert DisjointUnion(S.EmptySet, Interval(-1, 2), S.EmptySet).rewrite(Union) == Interval(-1, 2) * FiniteSet(1) + assert DisjointUnion(Interval(-oo, oo)).rewrite(Union) == Interval(-oo, oo) * FiniteSet(0) + assert DisjointUnion(S.EmptySet).rewrite(Union) == S.EmptySet + assert DisjointUnion().rewrite(Union) == S.EmptySet + raises(TypeError, lambda: DisjointUnion(Symbol('n'))) + + x = Symbol("x") + y = Symbol("y") + z = Symbol("z") + assert DisjointUnion(FiniteSet(x), FiniteSet(y, z)).rewrite(Union) == (FiniteSet(x) * FiniteSet(0)) + (FiniteSet(y, z) * FiniteSet(1)) + +def test_DisjointUnion_is_empty(): + assert DisjointUnion(S.EmptySet).is_empty is True + assert DisjointUnion(S.EmptySet, S.EmptySet).is_empty is True + assert DisjointUnion(S.EmptySet, FiniteSet(1, 2, 3)).is_empty is False + +def test_DisjointUnion_is_iterable(): + assert DisjointUnion(S.Integers, S.Naturals, S.Rationals).is_iterable is True + assert DisjointUnion(S.EmptySet, S.Reals).is_iterable is False + assert DisjointUnion(FiniteSet(1, 2, 3), S.EmptySet, FiniteSet(x, y)).is_iterable is True + assert DisjointUnion(S.EmptySet, S.EmptySet).is_iterable is False + +def test_DisjointUnion_contains(): + assert (0, 0) in DisjointUnion(FiniteSet(0, 1, 2), FiniteSet(0, 1, 2), FiniteSet(0, 1, 2)) + assert (0, 1) in DisjointUnion(FiniteSet(0, 1, 2), FiniteSet(0, 1, 2), FiniteSet(0, 1, 2)) + assert (0, 2) in DisjointUnion(FiniteSet(0, 1, 2), FiniteSet(0, 1, 2), FiniteSet(0, 1, 2)) + assert (1, 0) in DisjointUnion(FiniteSet(0, 1, 2), FiniteSet(0, 1, 2), FiniteSet(0, 1, 2)) + assert (1, 1) in DisjointUnion(FiniteSet(0, 1, 2), FiniteSet(0, 1, 2), FiniteSet(0, 1, 2)) + assert (1, 2) in DisjointUnion(FiniteSet(0, 1, 2), FiniteSet(0, 1, 2), FiniteSet(0, 1, 2)) + assert (2, 0) in DisjointUnion(FiniteSet(0, 1, 2), FiniteSet(0, 1, 2), FiniteSet(0, 1, 2)) + assert (2, 1) in DisjointUnion(FiniteSet(0, 1, 2), FiniteSet(0, 1, 2), FiniteSet(0, 1, 2)) + assert (2, 2) in DisjointUnion(FiniteSet(0, 1, 2), FiniteSet(0, 1, 2), FiniteSet(0, 1, 2)) + assert (0, 1, 2) not in DisjointUnion(FiniteSet(0, 1, 2), FiniteSet(0, 1, 2), FiniteSet(0, 1, 2)) + assert (0, 0.5) not in DisjointUnion(FiniteSet(0.5)) + assert (0, 5) not in DisjointUnion(FiniteSet(0, 1, 2), FiniteSet(0, 1, 2), FiniteSet(0, 1, 2)) + assert (x, 0) in DisjointUnion(FiniteSet(x, y, z), S.EmptySet, FiniteSet(y)) + assert (y, 0) in DisjointUnion(FiniteSet(x, y, z), S.EmptySet, FiniteSet(y)) + assert (z, 0) in DisjointUnion(FiniteSet(x, y, z), S.EmptySet, FiniteSet(y)) + assert (y, 2) in DisjointUnion(FiniteSet(x, y, z), S.EmptySet, FiniteSet(y)) + assert (0.5, 0) in DisjointUnion(Interval(0, 1), Interval(0, 2)) + assert (0.5, 1) in DisjointUnion(Interval(0, 1), Interval(0, 2)) + assert (1.5, 0) not in DisjointUnion(Interval(0, 1), Interval(0, 2)) + assert (1.5, 1) in DisjointUnion(Interval(0, 1), Interval(0, 2)) + +def test_DisjointUnion_iter(): + D = DisjointUnion(FiniteSet(3, 5, 7, 9), FiniteSet(x, y, z)) + it = iter(D) + L1 = [(x, 1), (y, 1), (z, 1)] + L2 = [(3, 0), (5, 0), (7, 0), (9, 0)] + nxt = next(it) + assert nxt in L2 + L2.remove(nxt) + nxt = next(it) + assert nxt in L1 + L1.remove(nxt) + nxt = next(it) + assert nxt in L2 + L2.remove(nxt) + nxt = next(it) + assert nxt in L1 + L1.remove(nxt) + nxt = next(it) + assert nxt in L2 + L2.remove(nxt) + nxt = next(it) + assert nxt in L1 + L1.remove(nxt) + nxt = next(it) + assert nxt in L2 + L2.remove(nxt) + raises(StopIteration, lambda: next(it)) + + raises(ValueError, lambda: iter(DisjointUnion(Interval(0, 1), S.EmptySet))) + +def test_DisjointUnion_len(): + assert len(DisjointUnion(FiniteSet(3, 5, 7, 9), FiniteSet(x, y, z))) == 7 + assert len(DisjointUnion(S.EmptySet, S.EmptySet, FiniteSet(x, y, z), S.EmptySet)) == 3 + raises(ValueError, lambda: len(DisjointUnion(Interval(0, 1), S.EmptySet))) + +def test_SetKind_ProductSet(): + p = ProductSet(FiniteSet(Matrix([1, 2])), FiniteSet(Matrix([1, 2]))) + mk = MatrixKind(NumberKind) + k = SetKind(TupleKind(mk, mk)) + assert p.kind is k + assert ProductSet(Interval(1, 2), FiniteSet(Matrix([1, 2]))).kind is SetKind(TupleKind(NumberKind, mk)) + +def test_SetKind_Interval(): + assert Interval(1, 2).kind is SetKind(NumberKind) + +def test_SetKind_EmptySet_UniversalSet(): + assert S.UniversalSet.kind is SetKind(UndefinedKind) + assert EmptySet.kind is SetKind() + +def test_SetKind_FiniteSet(): + assert FiniteSet(1, Matrix([1, 2])).kind is SetKind(UndefinedKind) + assert FiniteSet(1, 2).kind is SetKind(NumberKind) + +def test_SetKind_Unions(): + assert Union(FiniteSet(Matrix([1, 2])), Interval(1, 2)).kind is SetKind(UndefinedKind) + assert Union(Interval(1, 2), Interval(1, 7)).kind is SetKind(NumberKind) + +def test_SetKind_DisjointUnion(): + A = FiniteSet(1, 2, 3) + B = Interval(0, 5) + assert DisjointUnion(A, B).kind is SetKind(NumberKind) + +def test_SetKind_evaluate_False(): + U = lambda *args: Union(*args, evaluate=False) + assert U({1}, EmptySet).kind is SetKind(NumberKind) + assert U(Interval(1, 2), EmptySet).kind is SetKind(NumberKind) + assert U({1}, S.UniversalSet).kind is SetKind(UndefinedKind) + assert U(Interval(1, 2), Interval(4, 5), + FiniteSet(1)).kind is SetKind(NumberKind) + I = lambda *args: Intersection(*args, evaluate=False) + assert I({1}, S.UniversalSet).kind is SetKind(NumberKind) + assert I({1}, EmptySet).kind is SetKind() + C = lambda *args: Complement(*args, evaluate=False) + assert C(S.UniversalSet, {1, 2, 4, 5}).kind is SetKind(UndefinedKind) + assert C({1, 2, 3, 4, 5}, EmptySet).kind is SetKind(NumberKind) + assert C(EmptySet, {1, 2, 3, 4, 5}).kind is SetKind() + +def test_SetKind_ImageSet_Special(): + f = ImageSet(Lambda(n, n ** 2), Interval(1, 4)) + assert (f - FiniteSet(3)).kind is SetKind(NumberKind) + assert (f + Interval(16, 17)).kind is SetKind(NumberKind) + assert (f + FiniteSet(17)).kind is SetKind(NumberKind) + +def test_issue_20089(): + B = FiniteSet(FiniteSet(1, 2), FiniteSet(1)) + assert 1 not in B + assert 1.0 not in B + assert not Eq(1, FiniteSet(1, 2)) + assert FiniteSet(1) in B + A = FiniteSet(1, 2) + assert A in B + assert B.issubset(B) + assert not A.issubset(B) + assert 1 in A + C = FiniteSet(FiniteSet(1, 2), FiniteSet(1), 1, 2) + assert A.issubset(C) + assert B.issubset(C) + +def test_issue_19378(): + a = FiniteSet(1, 2) + b = ProductSet(a, a) + c = FiniteSet((1, 1), (1, 2), (2, 1), (2, 2)) + assert b.is_subset(c) is True + d = FiniteSet(1) + assert b.is_subset(d) is False + assert Eq(c, b).simplify() is S.true + assert Eq(a, c).simplify() is S.false + assert Eq({1}, {x}).simplify() == Eq({1}, {x}) + +def test_intersection_symbolic(): + n = Symbol('n') + # These should not throw an error + assert isinstance(Intersection(Range(n), Range(100)), Intersection) + assert isinstance(Intersection(Range(n), Interval(1, 100)), Intersection) + assert isinstance(Intersection(Range(100), Interval(1, n)), Intersection) + + +@XFAIL +def test_intersection_symbolic_failing(): + n = Symbol('n', integer=True, positive=True) + assert Intersection(Range(10, n), Range(4, 500, 5)) == Intersection( + Range(14, n), Range(14, 500, 5)) + assert Intersection(Interval(10, n), Range(4, 500, 5)) == Intersection( + Interval(14, n), Range(14, 500, 5)) + + +def test_issue_20379(): + #https://github.com/sympy/sympy/issues/20379 + x = pi - 3.14159265358979 + assert FiniteSet(x).evalf(2) == FiniteSet(Float('3.23108914886517e-15', 2)) + +def test_finiteset_simplify(): + S = FiniteSet(1, cos(1)**2 + sin(1)**2) + assert S.simplify() == {1} + +def test_issue_14336(): + #https://github.com/sympy/sympy/issues/14336 + U = S.Complexes + x = Symbol("x") + U -= U.intersect(Ne(x, 1).as_set()) + U -= U.intersect(S.true.as_set()) + +def test_issue_9855(): + #https://github.com/sympy/sympy/issues/9855 + x, y, z = symbols('x, y, z', real=True) + s1 = Interval(1, x) & Interval(y, 2) + s2 = Interval(1, 2) + assert s1.is_subset(s2) == None diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0619d1c3ebbd6c6a7d663093c7ed2202114148af --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/__init__.py @@ -0,0 +1,60 @@ +"""The module helps converting SymPy expressions into shorter forms of them. + +for example: +the expression E**(pi*I) will be converted into -1 +the expression (x+x)**2 will be converted into 4*x**2 +""" +from .simplify import (simplify, hypersimp, hypersimilar, + logcombine, separatevars, posify, besselsimp, kroneckersimp, + signsimp, nsimplify) + +from .fu import FU, fu + +from .sqrtdenest import sqrtdenest + +from .cse_main import cse + +from .epathtools import epath, EPath + +from .hyperexpand import hyperexpand + +from .radsimp import collect, rcollect, radsimp, collect_const, fraction, numer, denom + +from .trigsimp import trigsimp, exptrigsimp + +from .powsimp import powsimp, powdenest + +from .combsimp import combsimp + +from .gammasimp import gammasimp + +from .ratsimp import ratsimp, ratsimpmodprime + +__all__ = [ + 'simplify', 'hypersimp', 'hypersimilar', 'logcombine', 'separatevars', + 'posify', 'besselsimp', 'kroneckersimp', 'signsimp', + 'nsimplify', + + 'FU', 'fu', + + 'sqrtdenest', + + 'cse', + + 'epath', 'EPath', + + 'hyperexpand', + + 'collect', 'rcollect', 'radsimp', 'collect_const', 'fraction', 'numer', + 'denom', + + 'trigsimp', 'exptrigsimp', + + 'powsimp', 'powdenest', + + 'combsimp', + + 'gammasimp', + + 'ratsimp', 'ratsimpmodprime', +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/_cse_diff.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/_cse_diff.py new file mode 100644 index 0000000000000000000000000000000000000000..3496ad3b31a4f45312cac002429be40aa9aa0868 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/_cse_diff.py @@ -0,0 +1,291 @@ +"""Module for differentiation using CSE.""" + +from sympy import cse, Matrix, Derivative, MatrixBase +from sympy.utilities.iterables import iterable + + +def _remove_cse_from_derivative(replacements, reduced_expressions): + """ + This function is designed to postprocess the output of a common subexpression + elimination (CSE) operation. Specifically, it removes any CSE replacement + symbols from the arguments of ``Derivative`` terms in the expression. This + is necessary to ensure that the forward Jacobian function correctly handles + derivative terms. + + Parameters + ========== + + replacements : list of (Symbol, expression) pairs + Replacement symbols and relative common subexpressions that have been + replaced during a CSE operation. + + reduced_expressions : list of SymPy expressions + The reduced expressions with all the replacements from the + replacements list above. + + Returns + ======= + + processed_replacements : list of (Symbol, expression) pairs + Processed replacement list, in the same format of the + ``replacements`` input list. + + processed_reduced : list of SymPy expressions + Processed reduced list, in the same format of the + ``reduced_expressions`` input list. + """ + + def traverse(node, repl_dict): + if isinstance(node, Derivative): + return replace_all(node, repl_dict) + if not node.args: + return node + new_args = [traverse(arg, repl_dict) for arg in node.args] + return node.func(*new_args) + + def replace_all(node, repl_dict): + result = node + while True: + free_symbols = result.free_symbols + symbols_dict = {k: repl_dict[k] for k in free_symbols if k in repl_dict} + if not symbols_dict: + break + result = result.xreplace(symbols_dict) + return result + + repl_dict = dict(replacements) + processed_replacements = [ + (rep_sym, traverse(sub_exp, repl_dict)) + for rep_sym, sub_exp in replacements + ] + processed_reduced = [ + red_exp.__class__([traverse(exp, repl_dict) for exp in red_exp]) + for red_exp in reduced_expressions + ] + + return processed_replacements, processed_reduced + + +def _forward_jacobian_cse(replacements, reduced_expr, wrt): + """ + Core function to compute the Jacobian of an input Matrix of expressions + through forward accumulation. Takes directly the output of a CSE operation + (replacements and reduced_expr), and an iterable of variables (wrt) with + respect to which to differentiate the reduced expression and returns the + reduced Jacobian matrix and the ``replacements`` list. + + The function also returns a list of precomputed free symbols for each + subexpression, which are useful in the substitution process. + + Parameters + ========== + + replacements : list of (Symbol, expression) pairs + Replacement symbols and relative common subexpressions that have been + replaced during a CSE operation. + + reduced_expr : list of SymPy expressions + The reduced expressions with all the replacements from the + replacements list above. + + wrt : iterable + Iterable of expressions with respect to which to compute the + Jacobian matrix. + + Returns + ======= + + replacements : list of (Symbol, expression) pairs + Replacement symbols and relative common subexpressions that have been + replaced during a CSE operation. Compared to the input replacement list, + the output one doesn't contain replacement symbols inside + ``Derivative``'s arguments. + + jacobian : list of SymPy expressions + The list only contains one element, which is the Jacobian matrix with + elements in reduced form (replacement symbols are present). + + precomputed_fs: list + List of sets, which store the free symbols present in each sub-expression. + Useful in the substitution process. + """ + + if not isinstance(reduced_expr[0], MatrixBase): + raise TypeError("``expr`` must be of matrix type") + + if not (reduced_expr[0].shape[0] == 1 or reduced_expr[0].shape[1] == 1): + raise TypeError("``expr`` must be a row or a column matrix") + + if not iterable(wrt): + raise TypeError("``wrt`` must be an iterable of variables") + + elif not isinstance(wrt, MatrixBase): + wrt = Matrix(wrt) + + if not (wrt.shape[0] == 1 or wrt.shape[1] == 1): + raise TypeError("``wrt`` must be a row or a column matrix") + + replacements, reduced_expr = _remove_cse_from_derivative(replacements, reduced_expr) + + if replacements: + rep_sym, sub_expr = map(Matrix, zip(*replacements)) + else: + rep_sym, sub_expr = Matrix([]), Matrix([]) + + l_sub, l_wrt, l_red = len(sub_expr), len(wrt), len(reduced_expr[0]) + + f1 = reduced_expr[0].__class__.from_dok(l_red, l_wrt, + { + (i, j): diff_value + for i, r in enumerate(reduced_expr[0]) + for j, w in enumerate(wrt) + if (diff_value := r.diff(w)) != 0 + }, + ) + + if not replacements: + return [], [f1], [] + + f2 = Matrix.from_dok(l_red, l_sub, + { + (i, j): diff_value + for i, (r, fs) in enumerate([(r, r.free_symbols) for r in reduced_expr[0]]) + for j, s in enumerate(rep_sym) + if s in fs and (diff_value := r.diff(s)) != 0 + }, + ) + + rep_sym_set = set(rep_sym) + precomputed_fs = [s.free_symbols & rep_sym_set for s in sub_expr ] + + c_matrix = Matrix.from_dok(1, l_wrt, + {(0, j): diff_value for j, w in enumerate(wrt) + if (diff_value := sub_expr[0].diff(w)) != 0}) + + for i in range(1, l_sub): + + bi_matrix = Matrix.from_dok(1, i, + {(0, j): diff_value for j in range(i + 1) + if rep_sym[j] in precomputed_fs[i] + and (diff_value := sub_expr[i].diff(rep_sym[j])) != 0}) + + ai_matrix = Matrix.from_dok(1, l_wrt, + {(0, j): diff_value for j, w in enumerate(wrt) + if (diff_value := sub_expr[i].diff(w)) != 0}) + + if bi_matrix._rep.nnz(): + ci_matrix = bi_matrix.multiply(c_matrix).add(ai_matrix) + c_matrix = Matrix.vstack(c_matrix, ci_matrix) + else: + c_matrix = Matrix.vstack(c_matrix, ai_matrix) + + jacobian = f2.multiply(c_matrix).add(f1) + jacobian = [reduced_expr[0].__class__(jacobian)] + + return replacements, jacobian, precomputed_fs + + +def _forward_jacobian_norm_in_cse_out(expr, wrt): + """ + Function to compute the Jacobian of an input Matrix of expressions through + forward accumulation. Takes a sympy Matrix of expressions (expr) as input + and an iterable of variables (wrt) with respect to which to compute the + Jacobian matrix. The matrix is returned in reduced form (containing + replacement symbols) along with the ``replacements`` list. + + The function also returns a list of precomputed free symbols for each + subexpression, which are useful in the substitution process. + + Parameters + ========== + + expr : Matrix + The vector to be differentiated. + + wrt : iterable + The vector with respect to which to perform the differentiation. + Can be a matrix or an iterable of variables. + + Returns + ======= + + replacements : list of (Symbol, expression) pairs + Replacement symbols and relative common subexpressions that have been + replaced during a CSE operation. The output replacement list doesn't + contain replacement symbols inside ``Derivative``'s arguments. + + jacobian : list of SymPy expressions + The list only contains one element, which is the Jacobian matrix with + elements in reduced form (replacement symbols are present). + + precomputed_fs: list + List of sets, which store the free symbols present in each + sub-expression. Useful in the substitution process. + """ + + replacements, reduced_expr = cse(expr) + replacements, jacobian, precomputed_fs = _forward_jacobian_cse(replacements, reduced_expr, wrt) + + return replacements, jacobian, precomputed_fs + + +def _forward_jacobian(expr, wrt): + """ + Function to compute the Jacobian of an input Matrix of expressions through + forward accumulation. Takes a sympy Matrix of expressions (expr) as input + and an iterable of variables (wrt) with respect to which to compute the + Jacobian matrix. + + Explanation + =========== + + Expressions often contain repeated subexpressions. Using a tree structure, + these subexpressions are duplicated and differentiated multiple times, + leading to inefficiency. + + Instead, if a data structure called a directed acyclic graph (DAG) is used + then each of these repeated subexpressions will only exist a single time. + This function uses a combination of representing the expression as a DAG and + a forward accumulation algorithm (repeated application of the chain rule + symbolically) to more efficiently calculate the Jacobian matrix of a target + expression ``expr`` with respect to an expression or set of expressions + ``wrt``. + + Note that this function is intended to improve performance when + differentiating large expressions that contain many common subexpressions. + For small and simple expressions it is likely less performant than using + SymPy's standard differentiation functions and methods. + + Parameters + ========== + + expr : Matrix + The vector to be differentiated. + + wrt : iterable + The vector with respect to which to do the differentiation. + Can be a matrix or an iterable of variables. + + See Also + ======== + + Direct Acyclic Graph : https://en.wikipedia.org/wiki/Directed_acyclic_graph + """ + + replacements, reduced_expr = cse(expr) + + if replacements: + rep_sym, _ = map(Matrix, zip(*replacements)) + else: + rep_sym = Matrix([]) + + replacements, jacobian, precomputed_fs = _forward_jacobian_cse(replacements, reduced_expr, wrt) + + if not replacements: return jacobian[0] + + sub_rep = dict(replacements) + for i, ik in enumerate(precomputed_fs): + sub_dict = {j: sub_rep[j] for j in ik} + sub_rep[rep_sym[i]] = sub_rep[rep_sym[i]].xreplace(sub_dict) + + return jacobian[0].xreplace(sub_rep) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/combsimp.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/combsimp.py new file mode 100644 index 0000000000000000000000000000000000000000..8b0b3cefcba11b4b7759b7d3ec3c2d4415cfd849 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/combsimp.py @@ -0,0 +1,114 @@ +from sympy.core import Mul +from sympy.core.function import count_ops +from sympy.core.traversal import preorder_traversal, bottom_up +from sympy.functions.combinatorial.factorials import binomial, factorial +from sympy.functions import gamma +from sympy.simplify.gammasimp import gammasimp, _gammasimp + +from sympy.utilities.timeutils import timethis + + +@timethis('combsimp') +def combsimp(expr): + r""" + Simplify combinatorial expressions. + + Explanation + =========== + + This function takes as input an expression containing factorials, + binomials, Pochhammer symbol and other "combinatorial" functions, + and tries to minimize the number of those functions and reduce + the size of their arguments. + + The algorithm works by rewriting all combinatorial functions as + gamma functions and applying gammasimp() except simplification + steps that may make an integer argument non-integer. See docstring + of gammasimp for more information. + + Then it rewrites expression in terms of factorials and binomials by + rewriting gammas as factorials and converting (a+b)!/a!b! into + binomials. + + If expression has gamma functions or combinatorial functions + with non-integer argument, it is automatically passed to gammasimp. + + Examples + ======== + + >>> from sympy.simplify import combsimp + >>> from sympy import factorial, binomial, symbols + >>> n, k = symbols('n k', integer = True) + + >>> combsimp(factorial(n)/factorial(n - 3)) + n*(n - 2)*(n - 1) + >>> combsimp(binomial(n+1, k+1)/binomial(n, k)) + (n + 1)/(k + 1) + + """ + + expr = expr.rewrite(gamma, piecewise=False) + if any(isinstance(node, gamma) and not node.args[0].is_integer + for node in preorder_traversal(expr)): + return gammasimp(expr) + + expr = _gammasimp(expr, as_comb = True) + expr = _gamma_as_comb(expr) + return expr + + +def _gamma_as_comb(expr): + """ + Helper function for combsimp. + + Rewrites expression in terms of factorials and binomials + """ + + expr = expr.rewrite(factorial) + + def f(rv): + if not rv.is_Mul: + return rv + rvd = rv.as_powers_dict() + nd_fact_args = [[], []] # numerator, denominator + + for k in rvd: + if isinstance(k, factorial) and rvd[k].is_Integer: + if rvd[k].is_positive: + nd_fact_args[0].extend([k.args[0]]*rvd[k]) + else: + nd_fact_args[1].extend([k.args[0]]*-rvd[k]) + rvd[k] = 0 + if not nd_fact_args[0] or not nd_fact_args[1]: + return rv + + hit = False + for m in range(2): + i = 0 + while i < len(nd_fact_args[m]): + ai = nd_fact_args[m][i] + for j in range(i + 1, len(nd_fact_args[m])): + aj = nd_fact_args[m][j] + + sum = ai + aj + if sum in nd_fact_args[1 - m]: + hit = True + + nd_fact_args[1 - m].remove(sum) + del nd_fact_args[m][j] + del nd_fact_args[m][i] + + rvd[binomial(sum, ai if count_ops(ai) < + count_ops(aj) else aj)] += ( + -1 if m == 0 else 1) + break + else: + i += 1 + + if hit: + return Mul(*([k**rvd[k] for k in rvd] + [factorial(k) + for k in nd_fact_args[0]]))/Mul(*[factorial(k) + for k in nd_fact_args[1]]) + return rv + + return bottom_up(expr, f) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/cse_main.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/cse_main.py new file mode 100644 index 0000000000000000000000000000000000000000..bcd1b2e50adae8c3d3400d6c489e63a44ae1e59b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/cse_main.py @@ -0,0 +1,945 @@ +""" Tools for doing common subexpression elimination. +""" +from collections import defaultdict + +from sympy.core import Basic, Mul, Add, Pow, sympify +from sympy.core.containers import Tuple, OrderedSet +from sympy.core.exprtools import factor_terms +from sympy.core.singleton import S +from sympy.core.sorting import ordered +from sympy.core.symbol import symbols, Symbol +from sympy.matrices import (MatrixBase, Matrix, ImmutableMatrix, + SparseMatrix, ImmutableSparseMatrix) +from sympy.matrices.expressions import (MatrixExpr, MatrixSymbol, MatMul, + MatAdd, MatPow, Inverse) +from sympy.matrices.expressions.matexpr import MatrixElement +from sympy.polys.rootoftools import RootOf +from sympy.utilities.iterables import numbered_symbols, sift, \ + topological_sort, iterable + +from . import cse_opts + +# (preprocessor, postprocessor) pairs which are commonly useful. They should +# each take a SymPy expression and return a possibly transformed expression. +# When used in the function ``cse()``, the target expressions will be transformed +# by each of the preprocessor functions in order. After the common +# subexpressions are eliminated, each resulting expression will have the +# postprocessor functions transform them in *reverse* order in order to undo the +# transformation if necessary. This allows the algorithm to operate on +# a representation of the expressions that allows for more optimization +# opportunities. +# ``None`` can be used to specify no transformation for either the preprocessor or +# postprocessor. + + +basic_optimizations = [(cse_opts.sub_pre, cse_opts.sub_post), + (factor_terms, None)] + +# sometimes we want the output in a different format; non-trivial +# transformations can be put here for users +# =============================================================== + + +def reps_toposort(r): + """Sort replacements ``r`` so (k1, v1) appears before (k2, v2) + if k2 is in v1's free symbols. This orders items in the + way that cse returns its results (hence, in order to use the + replacements in a substitution option it would make sense + to reverse the order). + + Examples + ======== + + >>> from sympy.simplify.cse_main import reps_toposort + >>> from sympy.abc import x, y + >>> from sympy import Eq + >>> for l, r in reps_toposort([(x, y + 1), (y, 2)]): + ... print(Eq(l, r)) + ... + Eq(y, 2) + Eq(x, y + 1) + + """ + r = sympify(r) + E = [] + for c1, (k1, v1) in enumerate(r): + for c2, (k2, v2) in enumerate(r): + if k1 in v2.free_symbols: + E.append((c1, c2)) + return [r[i] for i in topological_sort((range(len(r)), E))] + + +def cse_separate(r, e): + """Move expressions that are in the form (symbol, expr) out of the + expressions and sort them into the replacements using the reps_toposort. + + Examples + ======== + + >>> from sympy.simplify.cse_main import cse_separate + >>> from sympy.abc import x, y, z + >>> from sympy import cos, exp, cse, Eq, symbols + >>> x0, x1 = symbols('x:2') + >>> eq = (x + 1 + exp((x + 1)/(y + 1)) + cos(y + 1)) + >>> cse([eq, Eq(x, z + 1), z - 2], postprocess=cse_separate) in [ + ... [[(x0, y + 1), (x, z + 1), (x1, x + 1)], + ... [x1 + exp(x1/x0) + cos(x0), z - 2]], + ... [[(x1, y + 1), (x, z + 1), (x0, x + 1)], + ... [x0 + exp(x0/x1) + cos(x1), z - 2]]] + ... + True + """ + d = sift(e, lambda w: w.is_Equality and w.lhs.is_Symbol) + r = r + [w.args for w in d[True]] + e = d[False] + return [reps_toposort(r), e] + + +def cse_release_variables(r, e): + """ + Return tuples giving ``(a, b)`` where ``a`` is a symbol and ``b`` is + either an expression or None. The value of None is used when a + symbol is no longer needed for subsequent expressions. + + Use of such output can reduce the memory footprint of lambdified + expressions that contain large, repeated subexpressions. + + Examples + ======== + + >>> from sympy import cse + >>> from sympy.simplify.cse_main import cse_release_variables + >>> from sympy.abc import x, y + >>> eqs = [(x + y - 1)**2, x, x + y, (x + y)/(2*x + 1) + (x + y - 1)**2, (2*x + 1)**(x + y)] + >>> defs, rvs = cse_release_variables(*cse(eqs)) + >>> for i in defs: + ... print(i) + ... + (x0, x + y) + (x1, (x0 - 1)**2) + (x2, 2*x + 1) + (_3, x0/x2 + x1) + (_4, x2**x0) + (x2, None) + (_0, x1) + (x1, None) + (_2, x0) + (x0, None) + (_1, x) + >>> print(rvs) + (_0, _1, _2, _3, _4) + """ + if not r: + return r, e + + s, p = zip(*r) + esyms = symbols('_:%d' % len(e)) + syms = list(esyms) + s = list(s) + in_use = set(s) + p = list(p) + # sort e so those with most sub-expressions appear first + e = [(e[i], syms[i]) for i in range(len(e))] + e, syms = zip(*sorted(e, + key=lambda x: -sum(p[s.index(i)].count_ops() + for i in x[0].free_symbols & in_use))) + syms = list(syms) + p += e + rv = [] + i = len(p) - 1 + while i >= 0: + _p = p.pop() + c = in_use & _p.free_symbols + if c: # sorting for canonical results + rv.extend([(s, None) for s in sorted(c, key=str)]) + if i >= len(r): + rv.append((syms.pop(), _p)) + else: + rv.append((s[i], _p)) + in_use -= c + i -= 1 + rv.reverse() + return rv, esyms + + +# ====end of cse postprocess idioms=========================== + + +def preprocess_for_cse(expr, optimizations): + """ Preprocess an expression to optimize for common subexpression + elimination. + + Parameters + ========== + + expr : SymPy expression + The target expression to optimize. + optimizations : list of (callable, callable) pairs + The (preprocessor, postprocessor) pairs. + + Returns + ======= + + expr : SymPy expression + The transformed expression. + """ + for pre, post in optimizations: + if pre is not None: + expr = pre(expr) + return expr + + +def postprocess_for_cse(expr, optimizations): + """Postprocess an expression after common subexpression elimination to + return the expression to canonical SymPy form. + + Parameters + ========== + + expr : SymPy expression + The target expression to transform. + optimizations : list of (callable, callable) pairs, optional + The (preprocessor, postprocessor) pairs. The postprocessors will be + applied in reversed order to undo the effects of the preprocessors + correctly. + + Returns + ======= + + expr : SymPy expression + The transformed expression. + """ + for pre, post in reversed(optimizations): + if post is not None: + expr = post(expr) + return expr + + +class FuncArgTracker: + """ + A class which manages a mapping from functions to arguments and an inverse + mapping from arguments to functions. + """ + + def __init__(self, funcs): + # To minimize the number of symbolic comparisons, all function arguments + # get assigned a value number. + self.value_numbers = {} + self.value_number_to_value = [] + + # Both of these maps use integer indices for arguments / functions. + self.arg_to_funcset = [] + self.func_to_argset = [] + + for func_i, func in enumerate(funcs): + func_argset = OrderedSet() + + for func_arg in func.args: + arg_number = self.get_or_add_value_number(func_arg) + func_argset.add(arg_number) + self.arg_to_funcset[arg_number].add(func_i) + + self.func_to_argset.append(func_argset) + + def get_args_in_value_order(self, argset): + """ + Return the list of arguments in sorted order according to their value + numbers. + """ + return [self.value_number_to_value[argn] for argn in sorted(argset)] + + def get_or_add_value_number(self, value): + """ + Return the value number for the given argument. + """ + nvalues = len(self.value_numbers) + value_number = self.value_numbers.setdefault(value, nvalues) + if value_number == nvalues: + self.value_number_to_value.append(value) + self.arg_to_funcset.append(OrderedSet()) + return value_number + + def stop_arg_tracking(self, func_i): + """ + Remove the function func_i from the argument to function mapping. + """ + for arg in self.func_to_argset[func_i]: + self.arg_to_funcset[arg].remove(func_i) + + + def get_common_arg_candidates(self, argset, min_func_i=0): + """Return a dict whose keys are function numbers. The entries of the dict are + the number of arguments said function has in common with + ``argset``. Entries have at least 2 items in common. All keys have + value at least ``min_func_i``. + """ + count_map = defaultdict(lambda: 0) + if not argset: + return count_map + + funcsets = [self.arg_to_funcset[arg] for arg in argset] + # As an optimization below, we handle the largest funcset separately from + # the others. + largest_funcset = max(funcsets, key=len) + + for funcset in funcsets: + if largest_funcset is funcset: + continue + for func_i in funcset: + if func_i >= min_func_i: + count_map[func_i] += 1 + + # We pick the smaller of the two containers (count_map, largest_funcset) + # to iterate over to reduce the number of iterations needed. + (smaller_funcs_container, + larger_funcs_container) = sorted( + [largest_funcset, count_map], + key=len) + + for func_i in smaller_funcs_container: + # Not already in count_map? It can't possibly be in the output, so + # skip it. + if count_map[func_i] < 1: + continue + + if func_i in larger_funcs_container: + count_map[func_i] += 1 + + return {k: v for k, v in count_map.items() if v >= 2} + + def get_subset_candidates(self, argset, restrict_to_funcset=None): + """ + Return a set of functions each of which whose argument list contains + ``argset``, optionally filtered only to contain functions in + ``restrict_to_funcset``. + """ + iarg = iter(argset) + + indices = OrderedSet( + fi for fi in self.arg_to_funcset[next(iarg)]) + + if restrict_to_funcset is not None: + indices &= restrict_to_funcset + + for arg in iarg: + indices &= self.arg_to_funcset[arg] + + return indices + + def update_func_argset(self, func_i, new_argset): + """ + Update a function with a new set of arguments. + """ + new_args = OrderedSet(new_argset) + old_args = self.func_to_argset[func_i] + + for deleted_arg in old_args - new_args: + self.arg_to_funcset[deleted_arg].remove(func_i) + for added_arg in new_args - old_args: + self.arg_to_funcset[added_arg].add(func_i) + + self.func_to_argset[func_i].clear() + self.func_to_argset[func_i].update(new_args) + + +class Unevaluated: + + def __init__(self, func, args): + self.func = func + self.args = args + + def __str__(self): + return "Uneval<{}>({})".format( + self.func, ", ".join(str(a) for a in self.args)) + + def as_unevaluated_basic(self): + return self.func(*self.args, evaluate=False) + + @property + def free_symbols(self): + return set().union(*[a.free_symbols for a in self.args]) + + __repr__ = __str__ + + +def match_common_args(func_class, funcs, opt_subs): + """ + Recognize and extract common subexpressions of function arguments within a + set of function calls. For instance, for the following function calls:: + + x + z + y + sin(x + y) + + this will extract a common subexpression of `x + y`:: + + w = x + y + w + z + sin(w) + + The function we work with is assumed to be associative and commutative. + + Parameters + ========== + + func_class: class + The function class (e.g. Add, Mul) + funcs: list of functions + A list of function calls. + opt_subs: dict + A dictionary of substitutions which this function may update. + """ + + # Sort to ensure that whole-function subexpressions come before the items + # that use them. + funcs = sorted(funcs, key=lambda f: len(f.args)) + arg_tracker = FuncArgTracker(funcs) + + changed = OrderedSet() + + for i in range(len(funcs)): + common_arg_candidates_counts = arg_tracker.get_common_arg_candidates( + arg_tracker.func_to_argset[i], min_func_i=i + 1) + + # Sort the candidates in order of match size. + # This makes us try combining smaller matches first. + common_arg_candidates = OrderedSet(sorted( + common_arg_candidates_counts.keys(), + key=lambda k: (common_arg_candidates_counts[k], k))) + + while common_arg_candidates: + j = common_arg_candidates.pop(last=False) + + com_args = arg_tracker.func_to_argset[i].intersection( + arg_tracker.func_to_argset[j]) + + if len(com_args) <= 1: + # This may happen if a set of common arguments was already + # combined in a previous iteration. + continue + + # For all sets, replace the common symbols by the function + # over them, to allow recursive matches. + + diff_i = arg_tracker.func_to_argset[i].difference(com_args) + if diff_i: + # com_func needs to be unevaluated to allow for recursive matches. + com_func = Unevaluated( + func_class, arg_tracker.get_args_in_value_order(com_args)) + com_func_number = arg_tracker.get_or_add_value_number(com_func) + arg_tracker.update_func_argset(i, diff_i | OrderedSet([com_func_number])) + changed.add(i) + else: + # Treat the whole expression as a CSE. + # + # The reason this needs to be done is somewhat subtle. Within + # tree_cse(), to_eliminate only contains expressions that are + # seen more than once. The problem is unevaluated expressions + # do not compare equal to the evaluated equivalent. So + # tree_cse() won't mark funcs[i] as a CSE if we use an + # unevaluated version. + com_func_number = arg_tracker.get_or_add_value_number(funcs[i]) + + diff_j = arg_tracker.func_to_argset[j].difference(com_args) + arg_tracker.update_func_argset(j, diff_j | OrderedSet([com_func_number])) + changed.add(j) + + for k in arg_tracker.get_subset_candidates( + com_args, common_arg_candidates): + diff_k = arg_tracker.func_to_argset[k].difference(com_args) + arg_tracker.update_func_argset(k, diff_k | OrderedSet([com_func_number])) + changed.add(k) + + if i in changed: + opt_subs[funcs[i]] = Unevaluated(func_class, + arg_tracker.get_args_in_value_order(arg_tracker.func_to_argset[i])) + + arg_tracker.stop_arg_tracking(i) + + +def opt_cse(exprs, order='canonical'): + """Find optimization opportunities in Adds, Muls, Pows and negative + coefficient Muls. + + Parameters + ========== + + exprs : list of SymPy expressions + The expressions to optimize. + order : string, 'none' or 'canonical' + The order by which Mul and Add arguments are processed. For large + expressions where speed is a concern, use the setting order='none'. + + Returns + ======= + + opt_subs : dictionary of expression substitutions + The expression substitutions which can be useful to optimize CSE. + + Examples + ======== + + >>> from sympy.simplify.cse_main import opt_cse + >>> from sympy.abc import x + >>> opt_subs = opt_cse([x**-2]) + >>> k, v = list(opt_subs.keys())[0], list(opt_subs.values())[0] + >>> print((k, v.as_unevaluated_basic())) + (x**(-2), 1/(x**2)) + """ + opt_subs = {} + + adds = OrderedSet() + muls = OrderedSet() + + seen_subexp = set() + collapsible_subexp = set() + + def _find_opts(expr): + + if not isinstance(expr, (Basic, Unevaluated)): + return + + if expr.is_Atom or expr.is_Order: + return + + if iterable(expr): + list(map(_find_opts, expr)) + return + + if expr in seen_subexp: + return expr + seen_subexp.add(expr) + + list(map(_find_opts, expr.args)) + + if not isinstance(expr, MatrixExpr) and expr.could_extract_minus_sign(): + # XXX -expr does not always work rigorously for some expressions + # containing UnevaluatedExpr. + # https://github.com/sympy/sympy/issues/24818 + if isinstance(expr, Add): + neg_expr = Add(*(-i for i in expr.args)) + else: + neg_expr = -expr + + if not neg_expr.is_Atom: + opt_subs[expr] = Unevaluated(Mul, (S.NegativeOne, neg_expr)) + seen_subexp.add(neg_expr) + expr = neg_expr + + if isinstance(expr, (Mul, MatMul)): + if len(expr.args) == 1: + collapsible_subexp.add(expr) + else: + muls.add(expr) + + elif isinstance(expr, (Add, MatAdd)): + if len(expr.args) == 1: + collapsible_subexp.add(expr) + else: + adds.add(expr) + + elif isinstance(expr, Inverse): + # Do not want to treat `Inverse` as a `MatPow` + pass + + elif isinstance(expr, (Pow, MatPow)): + base, exp = expr.base, expr.exp + if exp.could_extract_minus_sign(): + opt_subs[expr] = Unevaluated(Pow, (Pow(base, -exp), -1)) + + for e in exprs: + if isinstance(e, (Basic, Unevaluated)): + _find_opts(e) + + # Handle collapsing of multinary operations with single arguments + edges = [(s, s.args[0]) for s in collapsible_subexp + if s.args[0] in collapsible_subexp] + for e in reversed(topological_sort((collapsible_subexp, edges))): + opt_subs[e] = opt_subs.get(e.args[0], e.args[0]) + + # split muls into commutative + commutative_muls = OrderedSet() + for m in muls: + c, nc = m.args_cnc(cset=False) + if c: + c_mul = m.func(*c) + if nc: + if c_mul == 1: + new_obj = m.func(*nc) + else: + if isinstance(m, MatMul): + new_obj = m.func(c_mul, *nc, evaluate=False) + else: + new_obj = m.func(c_mul, m.func(*nc), evaluate=False) + opt_subs[m] = new_obj + if len(c) > 1: + commutative_muls.add(c_mul) + + match_common_args(Add, adds, opt_subs) + match_common_args(Mul, commutative_muls, opt_subs) + + return opt_subs + + +def tree_cse(exprs, symbols, opt_subs=None, order='canonical', ignore=()): + """Perform raw CSE on expression tree, taking opt_subs into account. + + Parameters + ========== + + exprs : list of SymPy expressions + The expressions to reduce. + symbols : infinite iterator yielding unique Symbols + The symbols used to label the common subexpressions which are pulled + out. + opt_subs : dictionary of expression substitutions + The expressions to be substituted before any CSE action is performed. + order : string, 'none' or 'canonical' + The order by which Mul and Add arguments are processed. For large + expressions where speed is a concern, use the setting order='none'. + ignore : iterable of Symbols + Substitutions containing any Symbol from ``ignore`` will be ignored. + """ + if opt_subs is None: + opt_subs = {} + + ## Find repeated sub-expressions + + to_eliminate = set() + + seen_subexp = set() + excluded_symbols = set() + + def _find_repeated(expr): + if not isinstance(expr, (Basic, Unevaluated)): + return + + if isinstance(expr, RootOf): + return + + if isinstance(expr, Basic) and ( + expr.is_Atom or + expr.is_Order or + isinstance(expr, (MatrixSymbol, MatrixElement))): + if expr.is_Symbol: + excluded_symbols.add(expr.name) + return + + if iterable(expr): + args = expr + + else: + if expr in seen_subexp: + for ign in ignore: + if ign in expr.free_symbols: + break + else: + to_eliminate.add(expr) + return + + seen_subexp.add(expr) + + if expr in opt_subs: + expr = opt_subs[expr] + + args = expr.args + + list(map(_find_repeated, args)) + + for e in exprs: + if isinstance(e, Basic): + _find_repeated(e) + + ## Rebuild tree + + # Remove symbols from the generator that conflict with names in the expressions. + symbols = (_ for _ in symbols if _.name not in excluded_symbols) + + replacements = [] + + subs = {} + + def _rebuild(expr): + if not isinstance(expr, (Basic, Unevaluated)): + return expr + + if not expr.args: + return expr + + if iterable(expr): + new_args = [_rebuild(arg) for arg in expr.args] + return expr.func(*new_args) + + if expr in subs: + return subs[expr] + + orig_expr = expr + if expr in opt_subs: + expr = opt_subs[expr] + + # If enabled, parse Muls and Adds arguments by order to ensure + # replacement order independent from hashes + if order != 'none': + if isinstance(expr, (Mul, MatMul)): + c, nc = expr.args_cnc() + if c == [1]: + args = nc + else: + args = list(ordered(c)) + nc + elif isinstance(expr, (Add, MatAdd)): + args = list(ordered(expr.args)) + else: + args = expr.args + else: + args = expr.args + + new_args = list(map(_rebuild, args)) + if isinstance(expr, Unevaluated) or new_args != args: + new_expr = expr.func(*new_args) + else: + new_expr = expr + + if orig_expr in to_eliminate: + try: + sym = next(symbols) + except StopIteration: + raise ValueError("Symbols iterator ran out of symbols.") + + if isinstance(orig_expr, MatrixExpr): + sym = MatrixSymbol(sym.name, orig_expr.rows, + orig_expr.cols) + + subs[orig_expr] = sym + replacements.append((sym, new_expr)) + return sym + + else: + return new_expr + + reduced_exprs = [] + for e in exprs: + if isinstance(e, Basic): + reduced_e = _rebuild(e) + else: + reduced_e = e + reduced_exprs.append(reduced_e) + return replacements, reduced_exprs + + +def cse(exprs, symbols=None, optimizations=None, postprocess=None, + order='canonical', ignore=(), list=True): + """ Perform common subexpression elimination on an expression. + + Parameters + ========== + + exprs : list of SymPy expressions, or a single SymPy expression + The expressions to reduce. + symbols : infinite iterator yielding unique Symbols + The symbols used to label the common subexpressions which are pulled + out. The ``numbered_symbols`` generator is useful. The default is a + stream of symbols of the form "x0", "x1", etc. This must be an + infinite iterator. + optimizations : list of (callable, callable) pairs + The (preprocessor, postprocessor) pairs of external optimization + functions. Optionally 'basic' can be passed for a set of predefined + basic optimizations. Such 'basic' optimizations were used by default + in old implementation, however they can be really slow on larger + expressions. Now, no pre or post optimizations are made by default. + postprocess : a function which accepts the two return values of cse and + returns the desired form of output from cse, e.g. if you want the + replacements reversed the function might be the following lambda: + lambda r, e: return reversed(r), e + order : string, 'none' or 'canonical' + The order by which Mul and Add arguments are processed. If set to + 'canonical', arguments will be canonically ordered. If set to 'none', + ordering will be faster but dependent on expressions hashes, thus + machine dependent and variable. For large expressions where speed is a + concern, use the setting order='none'. + ignore : iterable of Symbols + Substitutions containing any Symbol from ``ignore`` will be ignored. + list : bool, (default True) + Returns expression in list or else with same type as input (when False). + + Returns + ======= + + replacements : list of (Symbol, expression) pairs + All of the common subexpressions that were replaced. Subexpressions + earlier in this list might show up in subexpressions later in this + list. + reduced_exprs : list of SymPy expressions + The reduced expressions with all of the replacements above. + + Examples + ======== + + >>> from sympy import cse, SparseMatrix + >>> from sympy.abc import x, y, z, w + >>> cse(((w + x + y + z)*(w + y + z))/(w + x)**3) + ([(x0, y + z), (x1, w + x)], [(w + x0)*(x0 + x1)/x1**3]) + + + List of expressions with recursive substitutions: + + >>> m = SparseMatrix([x + y, x + y + z]) + >>> cse([(x+y)**2, x + y + z, y + z, x + z + y, m]) + ([(x0, x + y), (x1, x0 + z)], [x0**2, x1, y + z, x1, Matrix([ + [x0], + [x1]])]) + + Note: the type and mutability of input matrices is retained. + + >>> isinstance(_[1][-1], SparseMatrix) + True + + The user may disallow substitutions containing certain symbols: + + >>> cse([y**2*(x + 1), 3*y**2*(x + 1)], ignore=(y,)) + ([(x0, x + 1)], [x0*y**2, 3*x0*y**2]) + + The default return value for the reduced expression(s) is a list, even if there is only + one expression. The `list` flag preserves the type of the input in the output: + + >>> cse(x) + ([], [x]) + >>> cse(x, list=False) + ([], x) + """ + if not list: + return _cse_homogeneous(exprs, + symbols=symbols, optimizations=optimizations, + postprocess=postprocess, order=order, ignore=ignore) + + if isinstance(exprs, (int, float)): + exprs = sympify(exprs) + + # Handle the case if just one expression was passed. + if isinstance(exprs, (Basic, MatrixBase)): + exprs = [exprs] + + copy = exprs + temp = [] + for e in exprs: + if isinstance(e, (Matrix, ImmutableMatrix)): + temp.append(Tuple(*e.flat())) + elif isinstance(e, (SparseMatrix, ImmutableSparseMatrix)): + temp.append(Tuple(*e.todok().items())) + else: + temp.append(e) + exprs = temp + del temp + + if optimizations is None: + optimizations = [] + elif optimizations == 'basic': + optimizations = basic_optimizations + + # Preprocess the expressions to give us better optimization opportunities. + reduced_exprs = [preprocess_for_cse(e, optimizations) for e in exprs] + + if symbols is None: + symbols = numbered_symbols(cls=Symbol) + else: + # In case we get passed an iterable with an __iter__ method instead of + # an actual iterator. + symbols = iter(symbols) + + # Find other optimization opportunities. + opt_subs = opt_cse(reduced_exprs, order) + + # Main CSE algorithm. + replacements, reduced_exprs = tree_cse(reduced_exprs, symbols, opt_subs, + order, ignore) + + # Postprocess the expressions to return the expressions to canonical form. + exprs = copy + replacements = [(sym, postprocess_for_cse(subtree, optimizations)) + for sym, subtree in replacements] + reduced_exprs = [postprocess_for_cse(e, optimizations) + for e in reduced_exprs] + + # Get the matrices back + for i, e in enumerate(exprs): + if isinstance(e, (Matrix, ImmutableMatrix)): + reduced_exprs[i] = Matrix(e.rows, e.cols, reduced_exprs[i]) + if isinstance(e, ImmutableMatrix): + reduced_exprs[i] = reduced_exprs[i].as_immutable() + elif isinstance(e, (SparseMatrix, ImmutableSparseMatrix)): + m = SparseMatrix(e.rows, e.cols, {}) + for k, v in reduced_exprs[i]: + m[k] = v + if isinstance(e, ImmutableSparseMatrix): + m = m.as_immutable() + reduced_exprs[i] = m + + if postprocess is None: + return replacements, reduced_exprs + + return postprocess(replacements, reduced_exprs) + + +def _cse_homogeneous(exprs, **kwargs): + """ + Same as ``cse`` but the ``reduced_exprs`` are returned + with the same type as ``exprs`` or a sympified version of the same. + + Parameters + ========== + + exprs : an Expr, iterable of Expr or dictionary with Expr values + the expressions in which repeated subexpressions will be identified + kwargs : additional arguments for the ``cse`` function + + Returns + ======= + + replacements : list of (Symbol, expression) pairs + All of the common subexpressions that were replaced. Subexpressions + earlier in this list might show up in subexpressions later in this + list. + reduced_exprs : list of SymPy expressions + The reduced expressions with all of the replacements above. + + Examples + ======== + + >>> from sympy.simplify.cse_main import cse + >>> from sympy import cos, Tuple, Matrix + >>> from sympy.abc import x + >>> output = lambda x: type(cse(x, list=False)[1]) + >>> output(1) + + >>> output('cos(x)') + + >>> output(cos(x)) + cos + >>> output(Tuple(1, x)) + + >>> output(Matrix([[1,0], [0,1]])) + + >>> output([1, x]) + + >>> output((1, x)) + + >>> output({1, x}) + + """ + if isinstance(exprs, str): + replacements, reduced_exprs = _cse_homogeneous( + sympify(exprs), **kwargs) + return replacements, repr(reduced_exprs) + if isinstance(exprs, (list, tuple, set)): + replacements, reduced_exprs = cse(exprs, **kwargs) + return replacements, type(exprs)(reduced_exprs) + if isinstance(exprs, dict): + keys = list(exprs.keys()) # In order to guarantee the order of the elements. + replacements, values = cse([exprs[k] for k in keys], **kwargs) + reduced_exprs = dict(zip(keys, values)) + return replacements, reduced_exprs + + try: + replacements, (reduced_exprs,) = cse(exprs, **kwargs) + except TypeError: # For example 'mpf' objects + return [], exprs + else: + return replacements, reduced_exprs diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/cse_opts.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/cse_opts.py new file mode 100644 index 0000000000000000000000000000000000000000..36a59857411de740ae47423442af88b118a3395d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/cse_opts.py @@ -0,0 +1,52 @@ +""" Optimizations of the expression tree representation for better CSE +opportunities. +""" +from sympy.core import Add, Basic, Mul +from sympy.core.singleton import S +from sympy.core.sorting import default_sort_key +from sympy.core.traversal import preorder_traversal + + +def sub_pre(e): + """ Replace y - x with -(x - y) if -1 can be extracted from y - x. + """ + # replacing Add, A, from which -1 can be extracted with -1*-A + adds = [a for a in e.atoms(Add) if a.could_extract_minus_sign()] + reps = {} + ignore = set() + for a in adds: + na = -a + if na.is_Mul: # e.g. MatExpr + ignore.add(a) + continue + reps[a] = Mul._from_args([S.NegativeOne, na]) + + e = e.xreplace(reps) + + # repeat again for persisting Adds but mark these with a leading 1, -1 + # e.g. y - x -> 1*-1*(x - y) + if isinstance(e, Basic): + negs = {} + for a in sorted(e.atoms(Add), key=default_sort_key): + if a in ignore: + continue + if a in reps: + negs[a] = reps[a] + elif a.could_extract_minus_sign(): + negs[a] = Mul._from_args([S.One, S.NegativeOne, -a]) + e = e.xreplace(negs) + return e + + +def sub_post(e): + """ Replace 1*-1*x with -x. + """ + replacements = [] + for node in preorder_traversal(e): + if isinstance(node, Mul) and \ + node.args[0] is S.One and node.args[1] is S.NegativeOne: + replacements.append((node, -Mul._from_args(node.args[2:]))) + for node, replacement in replacements: + e = e.xreplace({node: replacement}) + + return e diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/epathtools.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/epathtools.py new file mode 100644 index 0000000000000000000000000000000000000000..7be983ada63fd39d7d467acf9afd62b3a41a2d85 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/epathtools.py @@ -0,0 +1,352 @@ +"""Tools for manipulation of expressions using paths. """ + +from sympy.core import Basic + + +class EPath: + r""" + Manipulate expressions using paths. + + EPath grammar in EBNF notation:: + + literal ::= /[A-Za-z_][A-Za-z_0-9]*/ + number ::= /-?\d+/ + type ::= literal + attribute ::= literal "?" + all ::= "*" + slice ::= "[" number? (":" number? (":" number?)?)? "]" + range ::= all | slice + query ::= (type | attribute) ("|" (type | attribute))* + selector ::= range | query range? + path ::= "/" selector ("/" selector)* + + See the docstring of the epath() function. + + """ + + __slots__ = ("_path", "_epath") + + def __new__(cls, path): + """Construct new EPath. """ + if isinstance(path, EPath): + return path + + if not path: + raise ValueError("empty EPath") + + _path = path + + if path[0] == '/': + path = path[1:] + else: + raise NotImplementedError("non-root EPath") + + epath = [] + + for selector in path.split('/'): + selector = selector.strip() + + if not selector: + raise ValueError("empty selector") + + index = 0 + + for c in selector: + if c.isalnum() or c in ('_', '|', '?'): + index += 1 + else: + break + + attrs = [] + types = [] + + if index: + elements = selector[:index] + selector = selector[index:] + + for element in elements.split('|'): + element = element.strip() + + if not element: + raise ValueError("empty element") + + if element.endswith('?'): + attrs.append(element[:-1]) + else: + types.append(element) + + span = None + + if selector == '*': + pass + else: + if selector.startswith('['): + try: + i = selector.index(']') + except ValueError: + raise ValueError("expected ']', got EOL") + + _span, span = selector[1:i], [] + + if ':' not in _span: + span = int(_span) + else: + for elt in _span.split(':', 3): + if not elt: + span.append(None) + else: + span.append(int(elt)) + + span = slice(*span) + + selector = selector[i + 1:] + + if selector: + raise ValueError("trailing characters in selector") + + epath.append((attrs, types, span)) + + obj = object.__new__(cls) + + obj._path = _path + obj._epath = epath + + return obj + + def __repr__(self): + return "%s(%r)" % (self.__class__.__name__, self._path) + + def _get_ordered_args(self, expr): + """Sort ``expr.args`` using printing order. """ + if expr.is_Add: + return expr.as_ordered_terms() + elif expr.is_Mul: + return expr.as_ordered_factors() + else: + return expr.args + + def _hasattrs(self, expr, attrs) -> bool: + """Check if ``expr`` has any of ``attrs``. """ + return all(hasattr(expr, attr) for attr in attrs) + + def _hastypes(self, expr, types): + """Check if ``expr`` is any of ``types``. """ + _types = [ cls.__name__ for cls in expr.__class__.mro() ] + return bool(set(_types).intersection(types)) + + def _has(self, expr, attrs, types): + """Apply ``_hasattrs`` and ``_hastypes`` to ``expr``. """ + if not (attrs or types): + return True + + if attrs and self._hasattrs(expr, attrs): + return True + + if types and self._hastypes(expr, types): + return True + + return False + + def apply(self, expr, func, args=None, kwargs=None): + """ + Modify parts of an expression selected by a path. + + Examples + ======== + + >>> from sympy.simplify.epathtools import EPath + >>> from sympy import sin, cos, E + >>> from sympy.abc import x, y, z, t + + >>> path = EPath("/*/[0]/Symbol") + >>> expr = [((x, 1), 2), ((3, y), z)] + + >>> path.apply(expr, lambda expr: expr**2) + [((x**2, 1), 2), ((3, y**2), z)] + + >>> path = EPath("/*/*/Symbol") + >>> expr = t + sin(x + 1) + cos(x + y + E) + + >>> path.apply(expr, lambda expr: 2*expr) + t + sin(2*x + 1) + cos(2*x + 2*y + E) + + """ + def _apply(path, expr, func): + if not path: + return func(expr) + else: + selector, path = path[0], path[1:] + attrs, types, span = selector + + if isinstance(expr, Basic): + if not expr.is_Atom: + args, basic = self._get_ordered_args(expr), True + else: + return expr + elif hasattr(expr, '__iter__'): + args, basic = expr, False + else: + return expr + + args = list(args) + + if span is not None: + if isinstance(span, slice): + indices = range(*span.indices(len(args))) + else: + indices = [span] + else: + indices = range(len(args)) + + for i in indices: + try: + arg = args[i] + except IndexError: + continue + + if self._has(arg, attrs, types): + args[i] = _apply(path, arg, func) + + if basic: + return expr.func(*args) + else: + return expr.__class__(args) + + _args, _kwargs = args or (), kwargs or {} + _func = lambda expr: func(expr, *_args, **_kwargs) + + return _apply(self._epath, expr, _func) + + def select(self, expr): + """ + Retrieve parts of an expression selected by a path. + + Examples + ======== + + >>> from sympy.simplify.epathtools import EPath + >>> from sympy import sin, cos, E + >>> from sympy.abc import x, y, z, t + + >>> path = EPath("/*/[0]/Symbol") + >>> expr = [((x, 1), 2), ((3, y), z)] + + >>> path.select(expr) + [x, y] + + >>> path = EPath("/*/*/Symbol") + >>> expr = t + sin(x + 1) + cos(x + y + E) + + >>> path.select(expr) + [x, x, y] + + """ + result = [] + + def _select(path, expr): + if not path: + result.append(expr) + else: + selector, path = path[0], path[1:] + attrs, types, span = selector + + if isinstance(expr, Basic): + args = self._get_ordered_args(expr) + elif hasattr(expr, '__iter__'): + args = expr + else: + return + + if span is not None: + if isinstance(span, slice): + args = args[span] + else: + try: + args = [args[span]] + except IndexError: + return + + for arg in args: + if self._has(arg, attrs, types): + _select(path, arg) + + _select(self._epath, expr) + return result + + +def epath(path, expr=None, func=None, args=None, kwargs=None): + r""" + Manipulate parts of an expression selected by a path. + + Explanation + =========== + + This function allows to manipulate large nested expressions in single + line of code, utilizing techniques to those applied in XML processing + standards (e.g. XPath). + + If ``func`` is ``None``, :func:`epath` retrieves elements selected by + the ``path``. Otherwise it applies ``func`` to each matching element. + + Note that it is more efficient to create an EPath object and use the select + and apply methods of that object, since this will compile the path string + only once. This function should only be used as a convenient shortcut for + interactive use. + + This is the supported syntax: + + * select all: ``/*`` + Equivalent of ``for arg in args:``. + * select slice: ``/[0]`` or ``/[1:5]`` or ``/[1:5:2]`` + Supports standard Python's slice syntax. + * select by type: ``/list`` or ``/list|tuple`` + Emulates ``isinstance()``. + * select by attribute: ``/__iter__?`` + Emulates ``hasattr()``. + + Parameters + ========== + + path : str | EPath + A path as a string or a compiled EPath. + expr : Basic | iterable + An expression or a container of expressions. + func : callable (optional) + A callable that will be applied to matching parts. + args : tuple (optional) + Additional positional arguments to ``func``. + kwargs : dict (optional) + Additional keyword arguments to ``func``. + + Examples + ======== + + >>> from sympy.simplify.epathtools import epath + >>> from sympy import sin, cos, E + >>> from sympy.abc import x, y, z, t + + >>> path = "/*/[0]/Symbol" + >>> expr = [((x, 1), 2), ((3, y), z)] + + >>> epath(path, expr) + [x, y] + >>> epath(path, expr, lambda expr: expr**2) + [((x**2, 1), 2), ((3, y**2), z)] + + >>> path = "/*/*/Symbol" + >>> expr = t + sin(x + 1) + cos(x + y + E) + + >>> epath(path, expr) + [x, x, y] + >>> epath(path, expr, lambda expr: 2*expr) + t + sin(2*x + 1) + cos(2*x + 2*y + E) + + """ + _epath = EPath(path) + + if expr is None: + return _epath + if func is None: + return _epath.select(expr) + else: + return _epath.apply(expr, func, args, kwargs) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/fu.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/fu.py new file mode 100644 index 0000000000000000000000000000000000000000..a26706edca98385df0009a8ee41476a17d36420c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/fu.py @@ -0,0 +1,2112 @@ +from collections import defaultdict + +from sympy.core.add import Add +from sympy.core.cache import cacheit +from sympy.core.expr import Expr +from sympy.core.exprtools import Factors, gcd_terms, factor_terms +from sympy.core.function import expand_mul +from sympy.core.mul import Mul +from sympy.core.numbers import pi, I +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.sorting import ordered +from sympy.core.symbol import Dummy +from sympy.core.sympify import sympify +from sympy.core.traversal import bottom_up +from sympy.functions.combinatorial.factorials import binomial +from sympy.functions.elementary.hyperbolic import ( + cosh, sinh, tanh, coth, sech, csch, HyperbolicFunction) +from sympy.functions.elementary.trigonometric import ( + cos, sin, tan, cot, sec, csc, sqrt, TrigonometricFunction) +from sympy.ntheory.factor_ import perfect_power +from sympy.polys.polytools import factor +from sympy.strategies.tree import greedy +from sympy.strategies.core import identity, debug + +from sympy import SYMPY_DEBUG + + +# ================== Fu-like tools =========================== + + +def TR0(rv): + """Simplification of rational polynomials, trying to simplify + the expression, e.g. combine things like 3*x + 2*x, etc.... + """ + # although it would be nice to use cancel, it doesn't work + # with noncommutatives + return rv.normal().factor().expand() + + +def TR1(rv): + """Replace sec, csc with 1/cos, 1/sin + + Examples + ======== + + >>> from sympy.simplify.fu import TR1, sec, csc + >>> from sympy.abc import x + >>> TR1(2*csc(x) + sec(x)) + 1/cos(x) + 2/sin(x) + """ + + def f(rv): + if isinstance(rv, sec): + a = rv.args[0] + return S.One/cos(a) + elif isinstance(rv, csc): + a = rv.args[0] + return S.One/sin(a) + return rv + + return bottom_up(rv, f) + + +def TR2(rv): + """Replace tan and cot with sin/cos and cos/sin + + Examples + ======== + + >>> from sympy.simplify.fu import TR2 + >>> from sympy.abc import x + >>> from sympy import tan, cot, sin, cos + >>> TR2(tan(x)) + sin(x)/cos(x) + >>> TR2(cot(x)) + cos(x)/sin(x) + >>> TR2(tan(tan(x) - sin(x)/cos(x))) + 0 + + """ + + def f(rv): + if isinstance(rv, tan): + a = rv.args[0] + return sin(a)/cos(a) + elif isinstance(rv, cot): + a = rv.args[0] + return cos(a)/sin(a) + return rv + + return bottom_up(rv, f) + + +def TR2i(rv, half=False): + """Converts ratios involving sin and cos as follows:: + sin(x)/cos(x) -> tan(x) + sin(x)/(cos(x) + 1) -> tan(x/2) if half=True + + Examples + ======== + + >>> from sympy.simplify.fu import TR2i + >>> from sympy.abc import x, a + >>> from sympy import sin, cos + >>> TR2i(sin(x)/cos(x)) + tan(x) + + Powers of the numerator and denominator are also recognized + + >>> TR2i(sin(x)**2/(cos(x) + 1)**2, half=True) + tan(x/2)**2 + + The transformation does not take place unless assumptions allow + (i.e. the base must be positive or the exponent must be an integer + for both numerator and denominator) + + >>> TR2i(sin(x)**a/(cos(x) + 1)**a) + sin(x)**a/(cos(x) + 1)**a + + """ + + def f(rv): + if not rv.is_Mul: + return rv + + n, d = rv.as_numer_denom() + if n.is_Atom or d.is_Atom: + return rv + + def ok(k, e): + # initial filtering of factors + return ( + (e.is_integer or k.is_positive) and ( + k.func in (sin, cos) or (half and + k.is_Add and + len(k.args) >= 2 and + any(any(isinstance(ai, cos) or ai.is_Pow and ai.base is cos + for ai in Mul.make_args(a)) for a in k.args)))) + + n = n.as_powers_dict() + ndone = [(k, n.pop(k)) for k in list(n.keys()) if not ok(k, n[k])] + if not n: + return rv + + d = d.as_powers_dict() + ddone = [(k, d.pop(k)) for k in list(d.keys()) if not ok(k, d[k])] + if not d: + return rv + + # factoring if necessary + + def factorize(d, ddone): + newk = [] + for k in d: + if k.is_Add and len(k.args) > 1: + knew = factor(k) if half else factor_terms(k) + if knew != k: + newk.append((k, knew)) + if newk: + for i, (k, knew) in enumerate(newk): + del d[k] + newk[i] = knew + newk = Mul(*newk).as_powers_dict() + for k in newk: + v = d[k] + newk[k] + if ok(k, v): + d[k] = v + else: + ddone.append((k, v)) + del newk + factorize(n, ndone) + factorize(d, ddone) + + # joining + t = [] + for k in n: + if isinstance(k, sin): + a = cos(k.args[0], evaluate=False) + if a in d and d[a] == n[k]: + t.append(tan(k.args[0])**n[k]) + n[k] = d[a] = None + elif half: + a1 = 1 + a + if a1 in d and d[a1] == n[k]: + t.append((tan(k.args[0]/2))**n[k]) + n[k] = d[a1] = None + elif isinstance(k, cos): + a = sin(k.args[0], evaluate=False) + if a in d and d[a] == n[k]: + t.append(tan(k.args[0])**-n[k]) + n[k] = d[a] = None + elif half and k.is_Add and k.args[0] is S.One and \ + isinstance(k.args[1], cos): + a = sin(k.args[1].args[0], evaluate=False) + if a in d and d[a] == n[k] and (d[a].is_integer or \ + a.is_positive): + t.append(tan(a.args[0]/2)**-n[k]) + n[k] = d[a] = None + + if t: + rv = Mul(*(t + [b**e for b, e in n.items() if e]))/\ + Mul(*[b**e for b, e in d.items() if e]) + rv *= Mul(*[b**e for b, e in ndone])/Mul(*[b**e for b, e in ddone]) + + return rv + + return bottom_up(rv, f) + + +def TR3(rv): + """Induced formula: example sin(-a) = -sin(a) + + Examples + ======== + + >>> from sympy.simplify.fu import TR3 + >>> from sympy.abc import x, y + >>> from sympy import pi + >>> from sympy import cos + >>> TR3(cos(y - x*(y - x))) + cos(x*(x - y) + y) + >>> cos(pi/2 + x) + -sin(x) + >>> cos(30*pi/2 + x) + -cos(x) + + """ + from sympy.simplify.simplify import signsimp + + # Negative argument (already automatic for funcs like sin(-x) -> -sin(x) + # but more complicated expressions can use it, too). Also, trig angles + # between pi/4 and pi/2 are not reduced to an angle between 0 and pi/4. + # The following are automatically handled: + # Argument of type: pi/2 +/- angle + # Argument of type: pi +/- angle + # Argument of type : 2k*pi +/- angle + + def f(rv): + if not isinstance(rv, TrigonometricFunction): + return rv + rv = rv.func(signsimp(rv.args[0])) + if not isinstance(rv, TrigonometricFunction): + return rv + if (rv.args[0] - S.Pi/4).is_positive is (S.Pi/2 - rv.args[0]).is_positive is True: + fmap = {cos: sin, sin: cos, tan: cot, cot: tan, sec: csc, csc: sec} + rv = fmap[type(rv)](S.Pi/2 - rv.args[0]) + return rv + + # touch numbers iside of trig functions to let them automatically update + rv = rv.replace( + lambda x: isinstance(x, TrigonometricFunction), + lambda x: x.replace( + lambda n: n.is_number and n.is_Mul, + lambda n: n.func(*n.args))) + + return bottom_up(rv, f) + + +def TR4(rv): + """Identify values of special angles. + + a= 0 pi/6 pi/4 pi/3 pi/2 + ---------------------------------------------------- + sin(a) 0 1/2 sqrt(2)/2 sqrt(3)/2 1 + cos(a) 1 sqrt(3)/2 sqrt(2)/2 1/2 0 + tan(a) 0 sqt(3)/3 1 sqrt(3) -- + + Examples + ======== + + >>> from sympy import pi + >>> from sympy import cos, sin, tan, cot + >>> for s in (0, pi/6, pi/4, pi/3, pi/2): + ... print('%s %s %s %s' % (cos(s), sin(s), tan(s), cot(s))) + ... + 1 0 0 zoo + sqrt(3)/2 1/2 sqrt(3)/3 sqrt(3) + sqrt(2)/2 sqrt(2)/2 1 1 + 1/2 sqrt(3)/2 sqrt(3) sqrt(3)/3 + 0 1 zoo 0 + """ + # special values at 0, pi/6, pi/4, pi/3, pi/2 already handled + return rv.replace( + lambda x: + isinstance(x, TrigonometricFunction) and + (r:=x.args[0]/pi).is_Rational and r.q in (1, 2, 3, 4, 6), + lambda x: + x.func(x.args[0].func(*x.args[0].args))) + + +def _TR56(rv, f, g, h, max, pow): + """Helper for TR5 and TR6 to replace f**2 with h(g**2) + + Options + ======= + + max : controls size of exponent that can appear on f + e.g. if max=4 then f**4 will be changed to h(g**2)**2. + pow : controls whether the exponent must be a perfect power of 2 + e.g. if pow=True (and max >= 6) then f**6 will not be changed + but f**8 will be changed to h(g**2)**4 + + >>> from sympy.simplify.fu import _TR56 as T + >>> from sympy.abc import x + >>> from sympy import sin, cos + >>> h = lambda x: 1 - x + >>> T(sin(x)**3, sin, cos, h, 4, False) + (1 - cos(x)**2)*sin(x) + >>> T(sin(x)**6, sin, cos, h, 6, False) + (1 - cos(x)**2)**3 + >>> T(sin(x)**6, sin, cos, h, 6, True) + sin(x)**6 + >>> T(sin(x)**8, sin, cos, h, 10, True) + (1 - cos(x)**2)**4 + """ + + def _f(rv): + # I'm not sure if this transformation should target all even powers + # or only those expressible as powers of 2. Also, should it only + # make the changes in powers that appear in sums -- making an isolated + # change is not going to allow a simplification as far as I can tell. + if not (rv.is_Pow and rv.base.func == f): + return rv + if not rv.exp.is_real: + return rv + + if (rv.exp < 0) == True: + return rv + if (rv.exp > max) == True: + return rv + if rv.exp == 1: + return rv + if rv.exp == 2: + return h(g(rv.base.args[0])**2) + else: + if rv.exp % 2 == 1: + e = rv.exp//2 + return f(rv.base.args[0])*h(g(rv.base.args[0])**2)**e + elif rv.exp == 4: + e = 2 + elif not pow: + if rv.exp % 2: + return rv + e = rv.exp//2 + else: + p = perfect_power(rv.exp) + if not p: + return rv + e = rv.exp//2 + return h(g(rv.base.args[0])**2)**e + + return bottom_up(rv, _f) + + +def TR5(rv, max=4, pow=False): + """Replacement of sin**2 with 1 - cos(x)**2. + + See _TR56 docstring for advanced use of ``max`` and ``pow``. + + Examples + ======== + + >>> from sympy.simplify.fu import TR5 + >>> from sympy.abc import x + >>> from sympy import sin + >>> TR5(sin(x)**2) + 1 - cos(x)**2 + >>> TR5(sin(x)**-2) # unchanged + sin(x)**(-2) + >>> TR5(sin(x)**4) + (1 - cos(x)**2)**2 + """ + return _TR56(rv, sin, cos, lambda x: 1 - x, max=max, pow=pow) + + +def TR6(rv, max=4, pow=False): + """Replacement of cos**2 with 1 - sin(x)**2. + + See _TR56 docstring for advanced use of ``max`` and ``pow``. + + Examples + ======== + + >>> from sympy.simplify.fu import TR6 + >>> from sympy.abc import x + >>> from sympy import cos + >>> TR6(cos(x)**2) + 1 - sin(x)**2 + >>> TR6(cos(x)**-2) #unchanged + cos(x)**(-2) + >>> TR6(cos(x)**4) + (1 - sin(x)**2)**2 + """ + return _TR56(rv, cos, sin, lambda x: 1 - x, max=max, pow=pow) + + +def TR7(rv): + """Lowering the degree of cos(x)**2. + + Examples + ======== + + >>> from sympy.simplify.fu import TR7 + >>> from sympy.abc import x + >>> from sympy import cos + >>> TR7(cos(x)**2) + cos(2*x)/2 + 1/2 + >>> TR7(cos(x)**2 + 1) + cos(2*x)/2 + 3/2 + + """ + + def f(rv): + if not (rv.is_Pow and rv.base.func == cos and rv.exp == 2): + return rv + return (1 + cos(2*rv.base.args[0]))/2 + + return bottom_up(rv, f) + + +def TR8(rv, first=True): + """Converting products of ``cos`` and/or ``sin`` to a sum or + difference of ``cos`` and or ``sin`` terms. + + Examples + ======== + + >>> from sympy.simplify.fu import TR8 + >>> from sympy import cos, sin + >>> TR8(cos(2)*cos(3)) + cos(5)/2 + cos(1)/2 + >>> TR8(cos(2)*sin(3)) + sin(5)/2 + sin(1)/2 + >>> TR8(sin(2)*sin(3)) + -cos(5)/2 + cos(1)/2 + """ + + def f(rv): + if not ( + rv.is_Mul or + rv.is_Pow and + rv.base.func in (cos, sin) and + (rv.exp.is_integer or rv.base.is_positive)): + return rv + + if first: + n, d = [expand_mul(i) for i in rv.as_numer_denom()] + newn = TR8(n, first=False) + newd = TR8(d, first=False) + if newn != n or newd != d: + rv = gcd_terms(newn/newd) + if rv.is_Mul and rv.args[0].is_Rational and \ + len(rv.args) == 2 and rv.args[1].is_Add: + rv = Mul(*rv.as_coeff_Mul()) + return rv + + args = {cos: [], sin: [], None: []} + for a in Mul.make_args(rv): + if a.func in (cos, sin): + args[type(a)].append(a.args[0]) + elif (a.is_Pow and a.exp.is_Integer and a.exp > 0 and \ + a.base.func in (cos, sin)): + # XXX this is ok but pathological expression could be handled + # more efficiently as in TRmorrie + args[type(a.base)].extend([a.base.args[0]]*a.exp) + else: + args[None].append(a) + c = args[cos] + s = args[sin] + if not (c and s or len(c) > 1 or len(s) > 1): + return rv + + args = args[None] + n = min(len(c), len(s)) + for i in range(n): + a1 = s.pop() + a2 = c.pop() + args.append((sin(a1 + a2) + sin(a1 - a2))/2) + while len(c) > 1: + a1 = c.pop() + a2 = c.pop() + args.append((cos(a1 + a2) + cos(a1 - a2))/2) + if c: + args.append(cos(c.pop())) + while len(s) > 1: + a1 = s.pop() + a2 = s.pop() + args.append((-cos(a1 + a2) + cos(a1 - a2))/2) + if s: + args.append(sin(s.pop())) + return TR8(expand_mul(Mul(*args))) + + return bottom_up(rv, f) + + +def TR9(rv): + """Sum of ``cos`` or ``sin`` terms as a product of ``cos`` or ``sin``. + + Examples + ======== + + >>> from sympy.simplify.fu import TR9 + >>> from sympy import cos, sin + >>> TR9(cos(1) + cos(2)) + 2*cos(1/2)*cos(3/2) + >>> TR9(cos(1) + 2*sin(1) + 2*sin(2)) + cos(1) + 4*sin(3/2)*cos(1/2) + + If no change is made by TR9, no re-arrangement of the + expression will be made. For example, though factoring + of common term is attempted, if the factored expression + was not changed, the original expression will be returned: + + >>> TR9(cos(3) + cos(3)*cos(2)) + cos(3) + cos(2)*cos(3) + + """ + + def f(rv): + if not rv.is_Add: + return rv + + def do(rv, first=True): + # cos(a)+/-cos(b) can be combined into a product of cosines and + # sin(a)+/-sin(b) can be combined into a product of cosine and + # sine. + # + # If there are more than two args, the pairs which "work" will + # have a gcd extractable and the remaining two terms will have + # the above structure -- all pairs must be checked to find the + # ones that work. args that don't have a common set of symbols + # are skipped since this doesn't lead to a simpler formula and + # also has the arbitrariness of combining, for example, the x + # and y term instead of the y and z term in something like + # cos(x) + cos(y) + cos(z). + + if not rv.is_Add: + return rv + + args = list(ordered(rv.args)) + if len(args) != 2: + hit = False + for i in range(len(args)): + ai = args[i] + if ai is None: + continue + for j in range(i + 1, len(args)): + aj = args[j] + if aj is None: + continue + was = ai + aj + new = do(was) + if new != was: + args[i] = new # update in place + args[j] = None + hit = True + break # go to next i + if hit: + rv = Add(*[_f for _f in args if _f]) + if rv.is_Add: + rv = do(rv) + + return rv + + # two-arg Add + split = trig_split(*args) + if not split: + return rv + gcd, n1, n2, a, b, iscos = split + + # application of rule if possible + if iscos: + if n1 == n2: + return gcd*n1*2*cos((a + b)/2)*cos((a - b)/2) + if n1 < 0: + a, b = b, a + return -2*gcd*sin((a + b)/2)*sin((a - b)/2) + else: + if n1 == n2: + return gcd*n1*2*sin((a + b)/2)*cos((a - b)/2) + if n1 < 0: + a, b = b, a + return 2*gcd*cos((a + b)/2)*sin((a - b)/2) + + return process_common_addends(rv, do) # DON'T sift by free symbols + + return bottom_up(rv, f) + + +def TR10(rv, first=True): + """Separate sums in ``cos`` and ``sin``. + + Examples + ======== + + >>> from sympy.simplify.fu import TR10 + >>> from sympy.abc import a, b, c + >>> from sympy import cos, sin + >>> TR10(cos(a + b)) + -sin(a)*sin(b) + cos(a)*cos(b) + >>> TR10(sin(a + b)) + sin(a)*cos(b) + sin(b)*cos(a) + >>> TR10(sin(a + b + c)) + (-sin(a)*sin(b) + cos(a)*cos(b))*sin(c) + \ + (sin(a)*cos(b) + sin(b)*cos(a))*cos(c) + """ + + def f(rv): + if rv.func not in (cos, sin): + return rv + + f = rv.func + arg = rv.args[0] + if arg.is_Add: + if first: + args = list(ordered(arg.args)) + else: + args = list(arg.args) + a = args.pop() + b = Add._from_args(args) + if b.is_Add: + if f == sin: + return sin(a)*TR10(cos(b), first=False) + \ + cos(a)*TR10(sin(b), first=False) + else: + return cos(a)*TR10(cos(b), first=False) - \ + sin(a)*TR10(sin(b), first=False) + else: + if f == sin: + return sin(a)*cos(b) + cos(a)*sin(b) + else: + return cos(a)*cos(b) - sin(a)*sin(b) + return rv + + return bottom_up(rv, f) + + +def TR10i(rv): + """Sum of products to function of sum. + + Examples + ======== + + >>> from sympy.simplify.fu import TR10i + >>> from sympy import cos, sin, sqrt + >>> from sympy.abc import x + + >>> TR10i(cos(1)*cos(3) + sin(1)*sin(3)) + cos(2) + >>> TR10i(cos(1)*sin(3) + sin(1)*cos(3) + cos(3)) + cos(3) + sin(4) + >>> TR10i(sqrt(2)*cos(x)*x + sqrt(6)*sin(x)*x) + 2*sqrt(2)*x*sin(x + pi/6) + + """ + def f(rv): + if not rv.is_Add: + return rv + + def do(rv, first=True): + # args which can be expressed as A*(cos(a)*cos(b)+/-sin(a)*sin(b)) + # or B*(cos(a)*sin(b)+/-cos(b)*sin(a)) can be combined into + # A*f(a+/-b) where f is either sin or cos. + # + # If there are more than two args, the pairs which "work" will have + # a gcd extractable and the remaining two terms will have the above + # structure -- all pairs must be checked to find the ones that + # work. + + if not rv.is_Add: + return rv + + args = list(ordered(rv.args)) + if len(args) != 2: + hit = False + for i in range(len(args)): + ai = args[i] + if ai is None: + continue + for j in range(i + 1, len(args)): + aj = args[j] + if aj is None: + continue + was = ai + aj + new = do(was) + if new != was: + args[i] = new # update in place + args[j] = None + hit = True + break # go to next i + if hit: + rv = Add(*[_f for _f in args if _f]) + if rv.is_Add: + rv = do(rv) + + return rv + + # two-arg Add + split = trig_split(*args, two=True) + if not split: + return rv + gcd, n1, n2, a, b, same = split + + # identify and get c1 to be cos then apply rule if possible + if same: # coscos, sinsin + gcd = n1*gcd + if n1 == n2: + return gcd*cos(a - b) + return gcd*cos(a + b) + else: #cossin, cossin + gcd = n1*gcd + if n1 == n2: + return gcd*sin(a + b) + return gcd*sin(b - a) + + rv = process_common_addends( + rv, do, lambda x: tuple(ordered(x.free_symbols))) + + # need to check for inducible pairs in ratio of sqrt(3):1 that + # appeared in different lists when sorting by coefficient + while rv.is_Add: + byrad = defaultdict(list) + for a in rv.args: + hit = 0 + if a.is_Mul: + for ai in a.args: + if ai.is_Pow and ai.exp is S.Half and \ + ai.base.is_Integer: + byrad[ai].append(a) + hit = 1 + break + if not hit: + byrad[S.One].append(a) + + # no need to check all pairs -- just check for the onees + # that have the right ratio + args = [] + for a in byrad: + for b in [_ROOT3()*a, _invROOT3()]: + if b in byrad: + for i in range(len(byrad[a])): + if byrad[a][i] is None: + continue + for j in range(len(byrad[b])): + if byrad[b][j] is None: + continue + was = Add(byrad[a][i] + byrad[b][j]) + new = do(was) + if new != was: + args.append(new) + byrad[a][i] = None + byrad[b][j] = None + break + if args: + rv = Add(*(args + [Add(*[_f for _f in v if _f]) + for v in byrad.values()])) + else: + rv = do(rv) # final pass to resolve any new inducible pairs + break + + return rv + + return bottom_up(rv, f) + + +def TR11(rv, base=None): + """Function of double angle to product. The ``base`` argument can be used + to indicate what is the un-doubled argument, e.g. if 3*pi/7 is the base + then cosine and sine functions with argument 6*pi/7 will be replaced. + + Examples + ======== + + >>> from sympy.simplify.fu import TR11 + >>> from sympy import cos, sin, pi + >>> from sympy.abc import x + >>> TR11(sin(2*x)) + 2*sin(x)*cos(x) + >>> TR11(cos(2*x)) + -sin(x)**2 + cos(x)**2 + >>> TR11(sin(4*x)) + 4*(-sin(x)**2 + cos(x)**2)*sin(x)*cos(x) + >>> TR11(sin(4*x/3)) + 4*(-sin(x/3)**2 + cos(x/3)**2)*sin(x/3)*cos(x/3) + + If the arguments are simply integers, no change is made + unless a base is provided: + + >>> TR11(cos(2)) + cos(2) + >>> TR11(cos(4), 2) + -sin(2)**2 + cos(2)**2 + + There is a subtle issue here in that autosimplification will convert + some higher angles to lower angles + + >>> cos(6*pi/7) + cos(3*pi/7) + -cos(pi/7) + cos(3*pi/7) + + The 6*pi/7 angle is now pi/7 but can be targeted with TR11 by supplying + the 3*pi/7 base: + + >>> TR11(_, 3*pi/7) + -sin(3*pi/7)**2 + cos(3*pi/7)**2 + cos(3*pi/7) + + """ + + def f(rv): + if rv.func not in (cos, sin): + return rv + + if base: + f = rv.func + t = f(base*2) + co = S.One + if t.is_Mul: + co, t = t.as_coeff_Mul() + if t.func not in (cos, sin): + return rv + if rv.args[0] == t.args[0]: + c = cos(base) + s = sin(base) + if f is cos: + return (c**2 - s**2)/co + else: + return 2*c*s/co + return rv + + elif not rv.args[0].is_Number: + # make a change if the leading coefficient's numerator is + # divisible by 2 + c, m = rv.args[0].as_coeff_Mul(rational=True) + if c.p % 2 == 0: + arg = c.p//2*m/c.q + c = TR11(cos(arg)) + s = TR11(sin(arg)) + if rv.func == sin: + rv = 2*s*c + else: + rv = c**2 - s**2 + return rv + + return bottom_up(rv, f) + + +def _TR11(rv): + """ + Helper for TR11 to find half-arguments for sin in factors of + num/den that appear in cos or sin factors in the den/num. + + Examples + ======== + + >>> from sympy.simplify.fu import TR11, _TR11 + >>> from sympy import cos, sin + >>> from sympy.abc import x + >>> TR11(sin(x/3)/(cos(x/6))) + sin(x/3)/cos(x/6) + >>> _TR11(sin(x/3)/(cos(x/6))) + 2*sin(x/6) + >>> TR11(sin(x/6)/(sin(x/3))) + sin(x/6)/sin(x/3) + >>> _TR11(sin(x/6)/(sin(x/3))) + 1/(2*cos(x/6)) + + """ + def f(rv): + if not isinstance(rv, Expr): + return rv + + def sincos_args(flat): + # find arguments of sin and cos that + # appears as bases in args of flat + # and have Integer exponents + args = defaultdict(set) + for fi in Mul.make_args(flat): + b, e = fi.as_base_exp() + if e.is_Integer and e > 0: + if b.func in (cos, sin): + args[type(b)].add(b.args[0]) + return args + num_args, den_args = map(sincos_args, rv.as_numer_denom()) + def handle_match(rv, num_args, den_args): + # for arg in sin args of num_args, look for arg/2 + # in den_args and pass this half-angle to TR11 + # for handling in rv + for narg in num_args[sin]: + half = narg/2 + if half in den_args[cos]: + func = cos + elif half in den_args[sin]: + func = sin + else: + continue + rv = TR11(rv, half) + den_args[func].remove(half) + return rv + # sin in num, sin or cos in den + rv = handle_match(rv, num_args, den_args) + # sin in den, sin or cos in num + rv = handle_match(rv, den_args, num_args) + return rv + + return bottom_up(rv, f) + + +def TR12(rv, first=True): + """Separate sums in ``tan``. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy import tan + >>> from sympy.simplify.fu import TR12 + >>> TR12(tan(x + y)) + (tan(x) + tan(y))/(-tan(x)*tan(y) + 1) + """ + + def f(rv): + if not rv.func == tan: + return rv + + arg = rv.args[0] + if arg.is_Add: + if first: + args = list(ordered(arg.args)) + else: + args = list(arg.args) + a = args.pop() + b = Add._from_args(args) + if b.is_Add: + tb = TR12(tan(b), first=False) + else: + tb = tan(b) + return (tan(a) + tb)/(1 - tan(a)*tb) + return rv + + return bottom_up(rv, f) + + +def TR12i(rv): + """Combine tan arguments as + (tan(y) + tan(x))/(tan(x)*tan(y) - 1) -> -tan(x + y). + + Examples + ======== + + >>> from sympy.simplify.fu import TR12i + >>> from sympy import tan + >>> from sympy.abc import a, b, c + >>> ta, tb, tc = [tan(i) for i in (a, b, c)] + >>> TR12i((ta + tb)/(-ta*tb + 1)) + tan(a + b) + >>> TR12i((ta + tb)/(ta*tb - 1)) + -tan(a + b) + >>> TR12i((-ta - tb)/(ta*tb - 1)) + tan(a + b) + >>> eq = (ta + tb)/(-ta*tb + 1)**2*(-3*ta - 3*tc)/(2*(ta*tc - 1)) + >>> TR12i(eq.expand()) + -3*tan(a + b)*tan(a + c)/(2*(tan(a) + tan(b) - 1)) + """ + def f(rv): + if not (rv.is_Add or rv.is_Mul or rv.is_Pow): + return rv + + n, d = rv.as_numer_denom() + if not d.args or not n.args: + return rv + + dok = {} + + def ok(di): + m = as_f_sign_1(di) + if m: + g, f, s = m + if s is S.NegativeOne and f.is_Mul and len(f.args) == 2 and \ + all(isinstance(fi, tan) for fi in f.args): + return g, f + + d_args = list(Mul.make_args(d)) + for i, di in enumerate(d_args): + m = ok(di) + if m: + g, t = m + s = Add(*[_.args[0] for _ in t.args]) + dok[s] = S.One + d_args[i] = g + continue + if di.is_Add: + di = factor(di) + if di.is_Mul: + d_args.extend(di.args) + d_args[i] = S.One + elif di.is_Pow and (di.exp.is_integer or di.base.is_positive): + m = ok(di.base) + if m: + g, t = m + s = Add(*[_.args[0] for _ in t.args]) + dok[s] = di.exp + d_args[i] = g**di.exp + else: + di = factor(di) + if di.is_Mul: + d_args.extend(di.args) + d_args[i] = S.One + if not dok: + return rv + + def ok(ni): + if ni.is_Add and len(ni.args) == 2: + a, b = ni.args + if isinstance(a, tan) and isinstance(b, tan): + return a, b + n_args = list(Mul.make_args(factor_terms(n))) + hit = False + for i, ni in enumerate(n_args): + m = ok(ni) + if not m: + m = ok(-ni) + if m: + n_args[i] = S.NegativeOne + else: + if ni.is_Add: + ni = factor(ni) + if ni.is_Mul: + n_args.extend(ni.args) + n_args[i] = S.One + continue + elif ni.is_Pow and ( + ni.exp.is_integer or ni.base.is_positive): + m = ok(ni.base) + if m: + n_args[i] = S.One + else: + ni = factor(ni) + if ni.is_Mul: + n_args.extend(ni.args) + n_args[i] = S.One + continue + else: + continue + else: + n_args[i] = S.One + hit = True + s = Add(*[_.args[0] for _ in m]) + ed = dok[s] + newed = ed.extract_additively(S.One) + if newed is not None: + if newed: + dok[s] = newed + else: + dok.pop(s) + n_args[i] *= -tan(s) + + if hit: + rv = Mul(*n_args)/Mul(*d_args)/Mul(*[(Add(*[ + tan(a) for a in i.args]) - 1)**e for i, e in dok.items()]) + + return rv + + return bottom_up(rv, f) + + +def TR13(rv): + """Change products of ``tan`` or ``cot``. + + Examples + ======== + + >>> from sympy.simplify.fu import TR13 + >>> from sympy import tan, cot + >>> TR13(tan(3)*tan(2)) + -tan(2)/tan(5) - tan(3)/tan(5) + 1 + >>> TR13(cot(3)*cot(2)) + cot(2)*cot(5) + 1 + cot(3)*cot(5) + """ + + def f(rv): + if not rv.is_Mul: + return rv + + # XXX handle products of powers? or let power-reducing handle it? + args = {tan: [], cot: [], None: []} + for a in Mul.make_args(rv): + if a.func in (tan, cot): + args[type(a)].append(a.args[0]) + else: + args[None].append(a) + t = args[tan] + c = args[cot] + if len(t) < 2 and len(c) < 2: + return rv + args = args[None] + while len(t) > 1: + t1 = t.pop() + t2 = t.pop() + args.append(1 - (tan(t1)/tan(t1 + t2) + tan(t2)/tan(t1 + t2))) + if t: + args.append(tan(t.pop())) + while len(c) > 1: + t1 = c.pop() + t2 = c.pop() + args.append(1 + cot(t1)*cot(t1 + t2) + cot(t2)*cot(t1 + t2)) + if c: + args.append(cot(c.pop())) + return Mul(*args) + + return bottom_up(rv, f) + + +def TRmorrie(rv): + """Returns cos(x)*cos(2*x)*...*cos(2**(k-1)*x) -> sin(2**k*x)/(2**k*sin(x)) + + Examples + ======== + + >>> from sympy.simplify.fu import TRmorrie, TR8, TR3 + >>> from sympy.abc import x + >>> from sympy import Mul, cos, pi + >>> TRmorrie(cos(x)*cos(2*x)) + sin(4*x)/(4*sin(x)) + >>> TRmorrie(7*Mul(*[cos(x) for x in range(10)])) + 7*sin(12)*sin(16)*cos(5)*cos(7)*cos(9)/(64*sin(1)*sin(3)) + + Sometimes autosimplification will cause a power to be + not recognized. e.g. in the following, cos(4*pi/7) automatically + simplifies to -cos(3*pi/7) so only 2 of the 3 terms are + recognized: + + >>> TRmorrie(cos(pi/7)*cos(2*pi/7)*cos(4*pi/7)) + -sin(3*pi/7)*cos(3*pi/7)/(4*sin(pi/7)) + + A touch by TR8 resolves the expression to a Rational + + >>> TR8(_) + -1/8 + + In this case, if eq is unsimplified, the answer is obtained + directly: + + >>> eq = cos(pi/9)*cos(2*pi/9)*cos(3*pi/9)*cos(4*pi/9) + >>> TRmorrie(eq) + 1/16 + + But if angles are made canonical with TR3 then the answer + is not simplified without further work: + + >>> TR3(eq) + sin(pi/18)*cos(pi/9)*cos(2*pi/9)/2 + >>> TRmorrie(_) + sin(pi/18)*sin(4*pi/9)/(8*sin(pi/9)) + >>> TR8(_) + cos(7*pi/18)/(16*sin(pi/9)) + >>> TR3(_) + 1/16 + + The original expression would have resolve to 1/16 directly with TR8, + however: + + >>> TR8(eq) + 1/16 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Morrie%27s_law + + """ + + def f(rv, first=True): + if not rv.is_Mul: + return rv + if first: + n, d = rv.as_numer_denom() + return f(n, 0)/f(d, 0) + + args = defaultdict(list) + coss = {} + other = [] + for c in rv.args: + b, e = c.as_base_exp() + if e.is_Integer and isinstance(b, cos): + co, a = b.args[0].as_coeff_Mul() + args[a].append(co) + coss[b] = e + else: + other.append(c) + + new = [] + for a in args: + c = args[a] + c.sort() + while c: + k = 0 + cc = ci = c[0] + while cc in c: + k += 1 + cc *= 2 + if k > 1: + newarg = sin(2**k*ci*a)/2**k/sin(ci*a) + # see how many times this can be taken + take = None + ccs = [] + for i in range(k): + cc /= 2 + key = cos(a*cc, evaluate=False) + ccs.append(cc) + take = min(coss[key], take or coss[key]) + # update exponent counts + for i in range(k): + cc = ccs.pop() + key = cos(a*cc, evaluate=False) + coss[key] -= take + if not coss[key]: + c.remove(cc) + new.append(newarg**take) + else: + b = cos(c.pop(0)*a) + other.append(b**coss[b]) + + if new: + rv = Mul(*(new + other + [ + cos(k*a, evaluate=False) for a in args for k in args[a]])) + + return rv + + return bottom_up(rv, f) + + +def TR14(rv, first=True): + """Convert factored powers of sin and cos identities into simpler + expressions. + + Examples + ======== + + >>> from sympy.simplify.fu import TR14 + >>> from sympy.abc import x, y + >>> from sympy import cos, sin + >>> TR14((cos(x) - 1)*(cos(x) + 1)) + -sin(x)**2 + >>> TR14((sin(x) - 1)*(sin(x) + 1)) + -cos(x)**2 + >>> p1 = (cos(x) + 1)*(cos(x) - 1) + >>> p2 = (cos(y) - 1)*2*(cos(y) + 1) + >>> p3 = (3*(cos(y) - 1))*(3*(cos(y) + 1)) + >>> TR14(p1*p2*p3*(x - 1)) + -18*(x - 1)*sin(x)**2*sin(y)**4 + + """ + + def f(rv): + if not rv.is_Mul: + return rv + + if first: + # sort them by location in numerator and denominator + # so the code below can just deal with positive exponents + n, d = rv.as_numer_denom() + if d is not S.One: + newn = TR14(n, first=False) + newd = TR14(d, first=False) + if newn != n or newd != d: + rv = newn/newd + return rv + + other = [] + process = [] + for a in rv.args: + if a.is_Pow: + b, e = a.as_base_exp() + if not (e.is_integer or b.is_positive): + other.append(a) + continue + a = b + else: + e = S.One + m = as_f_sign_1(a) + if not m or m[1].func not in (cos, sin): + if e is S.One: + other.append(a) + else: + other.append(a**e) + continue + g, f, si = m + process.append((g, e.is_Number, e, f, si, a)) + + # sort them to get like terms next to each other + process = list(ordered(process)) + + # keep track of whether there was any change + nother = len(other) + + # access keys + keys = (g, t, e, f, si, a) = list(range(6)) + + while process: + A = process.pop(0) + if process: + B = process[0] + + if A[e].is_Number and B[e].is_Number: + # both exponents are numbers + if A[f] == B[f]: + if A[si] != B[si]: + B = process.pop(0) + take = min(A[e], B[e]) + + # reinsert any remainder + # the B will likely sort after A so check it first + if B[e] != take: + rem = [B[i] for i in keys] + rem[e] -= take + process.insert(0, rem) + elif A[e] != take: + rem = [A[i] for i in keys] + rem[e] -= take + process.insert(0, rem) + + if isinstance(A[f], cos): + t = sin + else: + t = cos + other.append((-A[g]*B[g]*t(A[f].args[0])**2)**take) + continue + + elif A[e] == B[e]: + # both exponents are equal symbols + if A[f] == B[f]: + if A[si] != B[si]: + B = process.pop(0) + take = A[e] + if isinstance(A[f], cos): + t = sin + else: + t = cos + other.append((-A[g]*B[g]*t(A[f].args[0])**2)**take) + continue + + # either we are done or neither condition above applied + other.append(A[a]**A[e]) + + if len(other) != nother: + rv = Mul(*other) + + return rv + + return bottom_up(rv, f) + + +def TR15(rv, max=4, pow=False): + """Convert sin(x)**-2 to 1 + cot(x)**2. + + See _TR56 docstring for advanced use of ``max`` and ``pow``. + + Examples + ======== + + >>> from sympy.simplify.fu import TR15 + >>> from sympy.abc import x + >>> from sympy import sin + >>> TR15(1 - 1/sin(x)**2) + -cot(x)**2 + + """ + + def f(rv): + if not (isinstance(rv, Pow) and isinstance(rv.base, sin)): + return rv + + e = rv.exp + if e % 2 == 1: + return TR15(rv.base**(e + 1))/rv.base + + ia = 1/rv + a = _TR56(ia, sin, cot, lambda x: 1 + x, max=max, pow=pow) + if a != ia: + rv = a + return rv + + return bottom_up(rv, f) + + +def TR16(rv, max=4, pow=False): + """Convert cos(x)**-2 to 1 + tan(x)**2. + + See _TR56 docstring for advanced use of ``max`` and ``pow``. + + Examples + ======== + + >>> from sympy.simplify.fu import TR16 + >>> from sympy.abc import x + >>> from sympy import cos + >>> TR16(1 - 1/cos(x)**2) + -tan(x)**2 + + """ + + def f(rv): + if not (isinstance(rv, Pow) and isinstance(rv.base, cos)): + return rv + + e = rv.exp + if e % 2 == 1: + return TR15(rv.base**(e + 1))/rv.base + + ia = 1/rv + a = _TR56(ia, cos, tan, lambda x: 1 + x, max=max, pow=pow) + if a != ia: + rv = a + return rv + + return bottom_up(rv, f) + + +def TR111(rv): + """Convert f(x)**-i to g(x)**i where either ``i`` is an integer + or the base is positive and f, g are: tan, cot; sin, csc; or cos, sec. + + Examples + ======== + + >>> from sympy.simplify.fu import TR111 + >>> from sympy.abc import x + >>> from sympy import tan + >>> TR111(1 - 1/tan(x)**2) + 1 - cot(x)**2 + + """ + + def f(rv): + if not ( + isinstance(rv, Pow) and + (rv.base.is_positive or rv.exp.is_integer and rv.exp.is_negative)): + return rv + + if isinstance(rv.base, tan): + return cot(rv.base.args[0])**-rv.exp + elif isinstance(rv.base, sin): + return csc(rv.base.args[0])**-rv.exp + elif isinstance(rv.base, cos): + return sec(rv.base.args[0])**-rv.exp + return rv + + return bottom_up(rv, f) + + +def TR22(rv, max=4, pow=False): + """Convert tan(x)**2 to sec(x)**2 - 1 and cot(x)**2 to csc(x)**2 - 1. + + See _TR56 docstring for advanced use of ``max`` and ``pow``. + + Examples + ======== + + >>> from sympy.simplify.fu import TR22 + >>> from sympy.abc import x + >>> from sympy import tan, cot + >>> TR22(1 + tan(x)**2) + sec(x)**2 + >>> TR22(1 + cot(x)**2) + csc(x)**2 + + """ + + def f(rv): + if not (isinstance(rv, Pow) and rv.base.func in (cot, tan)): + return rv + + rv = _TR56(rv, tan, sec, lambda x: x - 1, max=max, pow=pow) + rv = _TR56(rv, cot, csc, lambda x: x - 1, max=max, pow=pow) + return rv + + return bottom_up(rv, f) + + +def TRpower(rv): + """Convert sin(x)**n and cos(x)**n with positive n to sums. + + Examples + ======== + + >>> from sympy.simplify.fu import TRpower + >>> from sympy.abc import x + >>> from sympy import cos, sin + >>> TRpower(sin(x)**6) + -15*cos(2*x)/32 + 3*cos(4*x)/16 - cos(6*x)/32 + 5/16 + >>> TRpower(sin(x)**3*cos(2*x)**4) + (3*sin(x)/4 - sin(3*x)/4)*(cos(4*x)/2 + cos(8*x)/8 + 3/8) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Power-reduction_formulae + + """ + + def f(rv): + if not (isinstance(rv, Pow) and isinstance(rv.base, (sin, cos))): + return rv + b, n = rv.as_base_exp() + x = b.args[0] + if n.is_Integer and n.is_positive: + if n.is_odd and isinstance(b, cos): + rv = 2**(1-n)*Add(*[binomial(n, k)*cos((n - 2*k)*x) + for k in range((n + 1)/2)]) + elif n.is_odd and isinstance(b, sin): + rv = 2**(1-n)*S.NegativeOne**((n-1)/2)*Add(*[binomial(n, k)* + S.NegativeOne**k*sin((n - 2*k)*x) for k in range((n + 1)/2)]) + elif n.is_even and isinstance(b, cos): + rv = 2**(1-n)*Add(*[binomial(n, k)*cos((n - 2*k)*x) + for k in range(n/2)]) + elif n.is_even and isinstance(b, sin): + rv = 2**(1-n)*S.NegativeOne**(n/2)*Add(*[binomial(n, k)* + S.NegativeOne**k*cos((n - 2*k)*x) for k in range(n/2)]) + if n.is_even: + rv += 2**(-n)*binomial(n, n/2) + return rv + + return bottom_up(rv, f) + + +def L(rv): + """Return count of trigonometric functions in expression. + + Examples + ======== + + >>> from sympy.simplify.fu import L + >>> from sympy.abc import x + >>> from sympy import cos, sin + >>> L(cos(x)+sin(x)) + 2 + """ + return S(rv.count(TrigonometricFunction)) + + +# ============== end of basic Fu-like tools ===================== + +if SYMPY_DEBUG: + (TR0, TR1, TR2, TR3, TR4, TR5, TR6, TR7, TR8, TR9, TR10, TR11, TR12, TR13, + TR2i, TRmorrie, TR14, TR15, TR16, TR12i, TR111, TR22 + )= list(map(debug, + (TR0, TR1, TR2, TR3, TR4, TR5, TR6, TR7, TR8, TR9, TR10, TR11, TR12, TR13, + TR2i, TRmorrie, TR14, TR15, TR16, TR12i, TR111, TR22))) + + +# tuples are chains -- (f, g) -> lambda x: g(f(x)) +# lists are choices -- [f, g] -> lambda x: min(f(x), g(x), key=objective) + +CTR1 = [(TR5, TR0), (TR6, TR0), identity] + +CTR2 = (TR11, [(TR5, TR0), (TR6, TR0), TR0]) + +CTR3 = [(TRmorrie, TR8, TR0), (TRmorrie, TR8, TR10i, TR0), identity] + +CTR4 = [(TR4, TR10i), identity] + +RL1 = (TR4, TR3, TR4, TR12, TR4, TR13, TR4, TR0) + + +# XXX it's a little unclear how this one is to be implemented +# see Fu paper of reference, page 7. What is the Union symbol referring to? +# The diagram shows all these as one chain of transformations, but the +# text refers to them being applied independently. Also, a break +# if L starts to increase has not been implemented. +RL2 = [ + (TR4, TR3, TR10, TR4, TR3, TR11), + (TR5, TR7, TR11, TR4), + (CTR3, CTR1, TR9, CTR2, TR4, TR9, TR9, CTR4), + identity, + ] + + +def fu(rv, measure=lambda x: (L(x), x.count_ops())): + """Attempt to simplify expression by using transformation rules given + in the algorithm by Fu et al. + + :func:`fu` will try to minimize the objective function ``measure``. + By default this first minimizes the number of trig terms and then minimizes + the number of total operations. + + Examples + ======== + + >>> from sympy.simplify.fu import fu + >>> from sympy import cos, sin, tan, pi, S, sqrt + >>> from sympy.abc import x, y, a, b + + >>> fu(sin(50)**2 + cos(50)**2 + sin(pi/6)) + 3/2 + >>> fu(sqrt(6)*cos(x) + sqrt(2)*sin(x)) + 2*sqrt(2)*sin(x + pi/3) + + CTR1 example + + >>> eq = sin(x)**4 - cos(y)**2 + sin(y)**2 + 2*cos(x)**2 + >>> fu(eq) + cos(x)**4 - 2*cos(y)**2 + 2 + + CTR2 example + + >>> fu(S.Half - cos(2*x)/2) + sin(x)**2 + + CTR3 example + + >>> fu(sin(a)*(cos(b) - sin(b)) + cos(a)*(sin(b) + cos(b))) + sqrt(2)*sin(a + b + pi/4) + + CTR4 example + + >>> fu(sqrt(3)*cos(x)/2 + sin(x)/2) + sin(x + pi/3) + + Example 1 + + >>> fu(1-sin(2*x)**2/4-sin(y)**2-cos(x)**4) + -cos(x)**2 + cos(y)**2 + + Example 2 + + >>> fu(cos(4*pi/9)) + sin(pi/18) + >>> fu(cos(pi/9)*cos(2*pi/9)*cos(3*pi/9)*cos(4*pi/9)) + 1/16 + + Example 3 + + >>> fu(tan(7*pi/18)+tan(5*pi/18)-sqrt(3)*tan(5*pi/18)*tan(7*pi/18)) + -sqrt(3) + + Objective function example + + >>> fu(sin(x)/cos(x)) # default objective function + tan(x) + >>> fu(sin(x)/cos(x), measure=lambda x: -x.count_ops()) # maximize op count + sin(x)/cos(x) + + References + ========== + + .. [1] https://www.sciencedirect.com/science/article/pii/S0895717706001609 + """ + fRL1 = greedy(RL1, measure) + fRL2 = greedy(RL2, measure) + + was = rv + rv = sympify(rv) + if not isinstance(rv, Expr): + return rv.func(*[fu(a, measure=measure) for a in rv.args]) + rv = TR1(rv) + if rv.has(tan, cot): + rv1 = fRL1(rv) + if (measure(rv1) < measure(rv)): + rv = rv1 + if rv.has(tan, cot): + rv = TR2(rv) + if rv.has(sin, cos): + rv1 = fRL2(rv) + rv2 = TR8(TRmorrie(rv1)) + rv = min([was, rv, rv1, rv2], key=measure) + return min(TR2i(rv), rv, key=measure) + + +def process_common_addends(rv, do, key2=None, key1=True): + """Apply ``do`` to addends of ``rv`` that (if ``key1=True``) share at least + a common absolute value of their coefficient and the value of ``key2`` when + applied to the argument. If ``key1`` is False ``key2`` must be supplied and + will be the only key applied. + """ + + # collect by absolute value of coefficient and key2 + absc = defaultdict(list) + if key1: + for a in rv.args: + c, a = a.as_coeff_Mul() + if c < 0: + c = -c + a = -a # put the sign on `a` + absc[(c, key2(a) if key2 else 1)].append(a) + elif key2: + for a in rv.args: + absc[(S.One, key2(a))].append(a) + else: + raise ValueError('must have at least one key') + + args = [] + hit = False + for k in absc: + v = absc[k] + c, _ = k + if len(v) > 1: + e = Add(*v, evaluate=False) + new = do(e) + if new != e: + e = new + hit = True + args.append(c*e) + else: + args.append(c*v[0]) + if hit: + rv = Add(*args) + + return rv + + +fufuncs = ''' + TR0 TR1 TR2 TR3 TR4 TR5 TR6 TR7 TR8 TR9 TR10 TR10i TR11 + TR12 TR13 L TR2i TRmorrie TR12i + TR14 TR15 TR16 TR111 TR22'''.split() +FU = dict(list(zip(fufuncs, list(map(locals().get, fufuncs))))) + + +@cacheit +def _ROOT2(): + return sqrt(2) + + +@cacheit +def _ROOT3(): + return sqrt(3) + + +@cacheit +def _invROOT3(): + return 1/sqrt(3) + + +def trig_split(a, b, two=False): + """Return the gcd, s1, s2, a1, a2, bool where + + If two is False (default) then:: + a + b = gcd*(s1*f(a1) + s2*f(a2)) where f = cos if bool else sin + else: + if bool, a + b was +/- cos(a1)*cos(a2) +/- sin(a1)*sin(a2) and equals + n1*gcd*cos(a - b) if n1 == n2 else + n1*gcd*cos(a + b) + else a + b was +/- cos(a1)*sin(a2) +/- sin(a1)*cos(a2) and equals + n1*gcd*sin(a + b) if n1 = n2 else + n1*gcd*sin(b - a) + + Examples + ======== + + >>> from sympy.simplify.fu import trig_split + >>> from sympy.abc import x, y, z + >>> from sympy import cos, sin, sqrt + + >>> trig_split(cos(x), cos(y)) + (1, 1, 1, x, y, True) + >>> trig_split(2*cos(x), -2*cos(y)) + (2, 1, -1, x, y, True) + >>> trig_split(cos(x)*sin(y), cos(y)*sin(y)) + (sin(y), 1, 1, x, y, True) + + >>> trig_split(cos(x), -sqrt(3)*sin(x), two=True) + (2, 1, -1, x, pi/6, False) + >>> trig_split(cos(x), sin(x), two=True) + (sqrt(2), 1, 1, x, pi/4, False) + >>> trig_split(cos(x), -sin(x), two=True) + (sqrt(2), 1, -1, x, pi/4, False) + >>> trig_split(sqrt(2)*cos(x), -sqrt(6)*sin(x), two=True) + (2*sqrt(2), 1, -1, x, pi/6, False) + >>> trig_split(-sqrt(6)*cos(x), -sqrt(2)*sin(x), two=True) + (-2*sqrt(2), 1, 1, x, pi/3, False) + >>> trig_split(cos(x)/sqrt(6), sin(x)/sqrt(2), two=True) + (sqrt(6)/3, 1, 1, x, pi/6, False) + >>> trig_split(-sqrt(6)*cos(x)*sin(y), -sqrt(2)*sin(x)*sin(y), two=True) + (-2*sqrt(2)*sin(y), 1, 1, x, pi/3, False) + + >>> trig_split(cos(x), sin(x)) + >>> trig_split(cos(x), sin(z)) + >>> trig_split(2*cos(x), -sin(x)) + >>> trig_split(cos(x), -sqrt(3)*sin(x)) + >>> trig_split(cos(x)*cos(y), sin(x)*sin(z)) + >>> trig_split(cos(x)*cos(y), sin(x)*sin(y)) + >>> trig_split(-sqrt(6)*cos(x), sqrt(2)*sin(x)*sin(y), two=True) + """ + a, b = [Factors(i) for i in (a, b)] + ua, ub = a.normal(b) + gcd = a.gcd(b).as_expr() + n1 = n2 = 1 + if S.NegativeOne in ua.factors: + ua = ua.quo(S.NegativeOne) + n1 = -n1 + elif S.NegativeOne in ub.factors: + ub = ub.quo(S.NegativeOne) + n2 = -n2 + a, b = [i.as_expr() for i in (ua, ub)] + + def pow_cos_sin(a, two): + """Return ``a`` as a tuple (r, c, s) such that + ``a = (r or 1)*(c or 1)*(s or 1)``. + + Three arguments are returned (radical, c-factor, s-factor) as + long as the conditions set by ``two`` are met; otherwise None is + returned. If ``two`` is True there will be one or two non-None + values in the tuple: c and s or c and r or s and r or s or c with c + being a cosine function (if possible) else a sine, and s being a sine + function (if possible) else oosine. If ``two`` is False then there + will only be a c or s term in the tuple. + + ``two`` also require that either two cos and/or sin be present (with + the condition that if the functions are the same the arguments are + different or vice versa) or that a single cosine or a single sine + be present with an optional radical. + + If the above conditions dictated by ``two`` are not met then None + is returned. + """ + c = s = None + co = S.One + if a.is_Mul: + co, a = a.as_coeff_Mul() + if len(a.args) > 2 or not two: + return None + if a.is_Mul: + args = list(a.args) + else: + args = [a] + a = args.pop(0) + if isinstance(a, cos): + c = a + elif isinstance(a, sin): + s = a + elif a.is_Pow and a.exp is S.Half: # autoeval doesn't allow -1/2 + co *= a + else: + return None + if args: + b = args[0] + if isinstance(b, cos): + if c: + s = b + else: + c = b + elif isinstance(b, sin): + if s: + c = b + else: + s = b + elif b.is_Pow and b.exp is S.Half: + co *= b + else: + return None + return co if co is not S.One else None, c, s + elif isinstance(a, cos): + c = a + elif isinstance(a, sin): + s = a + if c is None and s is None: + return + co = co if co is not S.One else None + return co, c, s + + # get the parts + m = pow_cos_sin(a, two) + if m is None: + return + coa, ca, sa = m + m = pow_cos_sin(b, two) + if m is None: + return + cob, cb, sb = m + + # check them + if (not ca) and cb or ca and isinstance(ca, sin): + coa, ca, sa, cob, cb, sb = cob, cb, sb, coa, ca, sa + n1, n2 = n2, n1 + if not two: # need cos(x) and cos(y) or sin(x) and sin(y) + c = ca or sa + s = cb or sb + if not isinstance(c, s.func): + return None + return gcd, n1, n2, c.args[0], s.args[0], isinstance(c, cos) + else: + if not coa and not cob: + if (ca and cb and sa and sb): + if isinstance(ca, sa.func) is not isinstance(cb, sb.func): + return + args = {j.args for j in (ca, sa)} + if not all(i.args in args for i in (cb, sb)): + return + return gcd, n1, n2, ca.args[0], sa.args[0], isinstance(ca, sa.func) + if ca and sa or cb and sb or \ + two and (ca is None and sa is None or cb is None and sb is None): + return + c = ca or sa + s = cb or sb + if c.args != s.args: + return + if not coa: + coa = S.One + if not cob: + cob = S.One + if coa is cob: + gcd *= _ROOT2() + return gcd, n1, n2, c.args[0], pi/4, False + elif coa/cob == _ROOT3(): + gcd *= 2*cob + return gcd, n1, n2, c.args[0], pi/3, False + elif coa/cob == _invROOT3(): + gcd *= 2*coa + return gcd, n1, n2, c.args[0], pi/6, False + + +def as_f_sign_1(e): + """If ``e`` is a sum that can be written as ``g*(a + s)`` where + ``s`` is ``+/-1``, return ``g``, ``a``, and ``s`` where ``a`` does + not have a leading negative coefficient. + + Examples + ======== + + >>> from sympy.simplify.fu import as_f_sign_1 + >>> from sympy.abc import x + >>> as_f_sign_1(x + 1) + (1, x, 1) + >>> as_f_sign_1(x - 1) + (1, x, -1) + >>> as_f_sign_1(-x + 1) + (-1, x, -1) + >>> as_f_sign_1(-x - 1) + (-1, x, 1) + >>> as_f_sign_1(2*x + 2) + (2, x, 1) + """ + if not e.is_Add or len(e.args) != 2: + return + # exact match + a, b = e.args + if a in (S.NegativeOne, S.One): + g = S.One + if b.is_Mul and b.args[0].is_Number and b.args[0] < 0: + a, b = -a, -b + g = -g + return g, b, a + # gcd match + a, b = [Factors(i) for i in e.args] + ua, ub = a.normal(b) + gcd = a.gcd(b).as_expr() + if S.NegativeOne in ua.factors: + ua = ua.quo(S.NegativeOne) + n1 = -1 + n2 = 1 + elif S.NegativeOne in ub.factors: + ub = ub.quo(S.NegativeOne) + n1 = 1 + n2 = -1 + else: + n1 = n2 = 1 + a, b = [i.as_expr() for i in (ua, ub)] + if a is S.One: + a, b = b, a + n1, n2 = n2, n1 + if n1 == -1: + gcd = -gcd + n2 = -n2 + + if b is S.One: + return gcd, a, n2 + + +def _osborne(e, d): + """Replace all hyperbolic functions with trig functions using + the Osborne rule. + + Notes + ===== + + ``d`` is a dummy variable to prevent automatic evaluation + of trigonometric/hyperbolic functions. + + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Hyperbolic_function + """ + + def f(rv): + if not isinstance(rv, HyperbolicFunction): + return rv + a = rv.args[0] + a = a*d if not a.is_Add else Add._from_args([i*d for i in a.args]) + if isinstance(rv, sinh): + return I*sin(a) + elif isinstance(rv, cosh): + return cos(a) + elif isinstance(rv, tanh): + return I*tan(a) + elif isinstance(rv, coth): + return cot(a)/I + elif isinstance(rv, sech): + return sec(a) + elif isinstance(rv, csch): + return csc(a)/I + else: + raise NotImplementedError('unhandled %s' % rv.func) + + return bottom_up(e, f) + + +def _osbornei(e, d): + """Replace all trig functions with hyperbolic functions using + the Osborne rule. + + Notes + ===== + + ``d`` is a dummy variable to prevent automatic evaluation + of trigonometric/hyperbolic functions. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Hyperbolic_function + """ + + def f(rv): + if not isinstance(rv, TrigonometricFunction): + return rv + const, x = rv.args[0].as_independent(d, as_Add=True) + a = x.xreplace({d: S.One}) + const*I + if isinstance(rv, sin): + return sinh(a)/I + elif isinstance(rv, cos): + return cosh(a) + elif isinstance(rv, tan): + return tanh(a)/I + elif isinstance(rv, cot): + return coth(a)*I + elif isinstance(rv, sec): + return sech(a) + elif isinstance(rv, csc): + return csch(a)*I + else: + raise NotImplementedError('unhandled %s' % rv.func) + + return bottom_up(e, f) + + +def hyper_as_trig(rv): + """Return an expression containing hyperbolic functions in terms + of trigonometric functions. Any trigonometric functions initially + present are replaced with Dummy symbols and the function to undo + the masking and the conversion back to hyperbolics is also returned. It + should always be true that:: + + t, f = hyper_as_trig(expr) + expr == f(t) + + Examples + ======== + + >>> from sympy.simplify.fu import hyper_as_trig, fu + >>> from sympy.abc import x + >>> from sympy import cosh, sinh + >>> eq = sinh(x)**2 + cosh(x)**2 + >>> t, f = hyper_as_trig(eq) + >>> f(fu(t)) + cosh(2*x) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Hyperbolic_function + """ + from sympy.simplify.simplify import signsimp + from sympy.simplify.radsimp import collect + + # mask off trig functions + trigs = rv.atoms(TrigonometricFunction) + reps = [(t, Dummy()) for t in trigs] + masked = rv.xreplace(dict(reps)) + + # get inversion substitutions in place + reps = [(v, k) for k, v in reps] + + d = Dummy() + + return _osborne(masked, d), lambda x: collect(signsimp( + _osbornei(x, d).xreplace(dict(reps))), S.ImaginaryUnit) + + +def sincos_to_sum(expr): + """Convert products and powers of sin and cos to sums. + + Explanation + =========== + + Applied power reduction TRpower first, then expands products, and + converts products to sums with TR8. + + Examples + ======== + + >>> from sympy.simplify.fu import sincos_to_sum + >>> from sympy.abc import x + >>> from sympy import cos, sin + >>> sincos_to_sum(16*sin(x)**3*cos(2*x)**2) + 7*sin(x) - 5*sin(3*x) + 3*sin(5*x) - sin(7*x) + """ + + if not expr.has(cos, sin): + return expr + else: + return TR8(expand_mul(TRpower(expr))) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/gammasimp.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/gammasimp.py new file mode 100644 index 0000000000000000000000000000000000000000..aec20c56eb60efb8e1aadfb5bff3d1ba1ab51869 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/gammasimp.py @@ -0,0 +1,493 @@ +from sympy.core import Function, S, Mul, Pow, Add +from sympy.core.sorting import ordered, default_sort_key +from sympy.core.function import expand_func +from sympy.core.symbol import Dummy +from sympy.functions import gamma, sqrt, sin +from sympy.polys import factor, cancel +from sympy.utilities.iterables import sift, uniq + + +def gammasimp(expr): + r""" + Simplify expressions with gamma functions. + + Explanation + =========== + + This function takes as input an expression containing gamma + functions or functions that can be rewritten in terms of gamma + functions and tries to minimize the number of those functions and + reduce the size of their arguments. + + The algorithm works by rewriting all gamma functions as expressions + involving rising factorials (Pochhammer symbols) and applies + recurrence relations and other transformations applicable to rising + factorials, to reduce their arguments, possibly letting the resulting + rising factorial to cancel. Rising factorials with the second argument + being an integer are expanded into polynomial forms and finally all + other rising factorial are rewritten in terms of gamma functions. + + Then the following two steps are performed. + + 1. Reduce the number of gammas by applying the reflection theorem + gamma(x)*gamma(1-x) == pi/sin(pi*x). + 2. Reduce the number of gammas by applying the multiplication theorem + gamma(x)*gamma(x+1/n)*...*gamma(x+(n-1)/n) == C*gamma(n*x). + + It then reduces the number of prefactors by absorbing them into gammas + where possible and expands gammas with rational argument. + + All transformation rules can be found (or were derived from) here: + + .. [1] https://functions.wolfram.com/GammaBetaErf/Pochhammer/17/01/02/ + .. [2] https://functions.wolfram.com/GammaBetaErf/Pochhammer/27/01/0005/ + + Examples + ======== + + >>> from sympy.simplify import gammasimp + >>> from sympy import gamma, Symbol + >>> from sympy.abc import x + >>> n = Symbol('n', integer = True) + + >>> gammasimp(gamma(x)/gamma(x - 3)) + (x - 3)*(x - 2)*(x - 1) + >>> gammasimp(gamma(n + 3)) + gamma(n + 3) + + """ + + expr = expr.rewrite(gamma) + + # compute_ST will be looking for Functions and we don't want + # it looking for non-gamma functions: issue 22606 + # so we mask free, non-gamma functions + f = expr.atoms(Function) + # take out gammas + gammas = {i for i in f if isinstance(i, gamma)} + if not gammas: + return expr # avoid side effects like factoring + f -= gammas + # keep only those without bound symbols + f = f & expr.as_dummy().atoms(Function) + if f: + dum, fun, simp = zip(*[ + (Dummy(), fi, fi.func(*[ + _gammasimp(a, as_comb=False) for a in fi.args])) + for fi in ordered(f)]) + d = expr.xreplace(dict(zip(fun, dum))) + return _gammasimp(d, as_comb=False).xreplace(dict(zip(dum, simp))) + + return _gammasimp(expr, as_comb=False) + + +def _gammasimp(expr, as_comb): + """ + Helper function for gammasimp and combsimp. + + Explanation + =========== + + Simplifies expressions written in terms of gamma function. If + as_comb is True, it tries to preserve integer arguments. See + docstring of gammasimp for more information. This was part of + combsimp() in combsimp.py. + """ + expr = expr.replace(gamma, + lambda n: _rf(1, (n - 1).expand())) + + if as_comb: + expr = expr.replace(_rf, + lambda a, b: gamma(b + 1)) + else: + expr = expr.replace(_rf, + lambda a, b: gamma(a + b)/gamma(a)) + + def rule_gamma(expr, level=0): + """ Simplify products of gamma functions further. """ + + if expr.is_Atom: + return expr + + def gamma_rat(x): + # helper to simplify ratios of gammas + was = x.count(gamma) + xx = x.replace(gamma, lambda n: _rf(1, (n - 1).expand() + ).replace(_rf, lambda a, b: gamma(a + b)/gamma(a))) + if xx.count(gamma) < was: + x = xx + return x + + def gamma_factor(x): + # return True if there is a gamma factor in shallow args + if isinstance(x, gamma): + return True + if x.is_Add or x.is_Mul: + return any(gamma_factor(xi) for xi in x.args) + if x.is_Pow and (x.exp.is_integer or x.base.is_positive): + return gamma_factor(x.base) + return False + + # recursion step + if level == 0: + expr = expr.func(*[rule_gamma(x, level + 1) for x in expr.args]) + level += 1 + + if not expr.is_Mul: + return expr + + # non-commutative step + if level == 1: + args, nc = expr.args_cnc() + if not args: + return expr + if nc: + return rule_gamma(Mul._from_args(args), level + 1)*Mul._from_args(nc) + level += 1 + + # pure gamma handling, not factor absorption + if level == 2: + T, F = sift(expr.args, gamma_factor, binary=True) + gamma_ind = Mul(*F) + d = Mul(*T) + + nd, dd = d.as_numer_denom() + for ipass in range(2): + args = list(ordered(Mul.make_args(nd))) + for i, ni in enumerate(args): + if ni.is_Add: + ni, dd = Add(*[ + rule_gamma(gamma_rat(a/dd), level + 1) for a in ni.args] + ).as_numer_denom() + args[i] = ni + if not dd.has(gamma): + break + nd = Mul(*args) + if ipass == 0 and not gamma_factor(nd): + break + nd, dd = dd, nd # now process in reversed order + expr = gamma_ind*nd/dd + if not (expr.is_Mul and (gamma_factor(dd) or gamma_factor(nd))): + return expr + level += 1 + + # iteration until constant + if level == 3: + while True: + was = expr + expr = rule_gamma(expr, 4) + if expr == was: + return expr + + numer_gammas = [] + denom_gammas = [] + numer_others = [] + denom_others = [] + def explicate(p): + if p is S.One: + return None, [] + b, e = p.as_base_exp() + if e.is_Integer: + if isinstance(b, gamma): + return True, [b.args[0]]*e + else: + return False, [b]*e + else: + return False, [p] + + newargs = list(ordered(expr.args)) + while newargs: + n, d = newargs.pop().as_numer_denom() + isg, l = explicate(n) + if isg: + numer_gammas.extend(l) + elif isg is False: + numer_others.extend(l) + isg, l = explicate(d) + if isg: + denom_gammas.extend(l) + elif isg is False: + denom_others.extend(l) + + # =========== level 2 work: pure gamma manipulation ========= + + if not as_comb: + # Try to reduce the number of gamma factors by applying the + # reflection formula gamma(x)*gamma(1-x) = pi/sin(pi*x) + for gammas, numer, denom in [( + numer_gammas, numer_others, denom_others), + (denom_gammas, denom_others, numer_others)]: + new = [] + while gammas: + g1 = gammas.pop() + if g1.is_integer: + new.append(g1) + continue + for i, g2 in enumerate(gammas): + n = g1 + g2 - 1 + if not n.is_Integer: + continue + numer.append(S.Pi) + denom.append(sin(S.Pi*g1)) + gammas.pop(i) + if n > 0: + numer.extend(1 - g1 + k for k in range(n)) + elif n < 0: + denom.extend(-g1 - k for k in range(-n)) + break + else: + new.append(g1) + # /!\ updating IN PLACE + gammas[:] = new + + # Try to reduce the number of gammas by using the duplication + # theorem to cancel an upper and lower: gamma(2*s)/gamma(s) = + # 2**(2*s + 1)/(4*sqrt(pi))*gamma(s + 1/2). Although this could + # be done with higher argument ratios like gamma(3*x)/gamma(x), + # this would not reduce the number of gammas as in this case. + for ng, dg, no, do in [(numer_gammas, denom_gammas, numer_others, + denom_others), + (denom_gammas, numer_gammas, denom_others, + numer_others)]: + + while True: + for x in ng: + for y in dg: + n = x - 2*y + if n.is_Integer: + break + else: + continue + break + else: + break + ng.remove(x) + dg.remove(y) + if n > 0: + no.extend(2*y + k for k in range(n)) + elif n < 0: + do.extend(2*y - 1 - k for k in range(-n)) + ng.append(y + S.Half) + no.append(2**(2*y - 1)) + do.append(sqrt(S.Pi)) + + # Try to reduce the number of gamma factors by applying the + # multiplication theorem (used when n gammas with args differing + # by 1/n mod 1 are encountered). + # + # run of 2 with args differing by 1/2 + # + # >>> gammasimp(gamma(x)*gamma(x+S.Half)) + # 2*sqrt(2)*2**(-2*x - 1/2)*sqrt(pi)*gamma(2*x) + # + # run of 3 args differing by 1/3 (mod 1) + # + # >>> gammasimp(gamma(x)*gamma(x+S(1)/3)*gamma(x+S(2)/3)) + # 6*3**(-3*x - 1/2)*pi*gamma(3*x) + # >>> gammasimp(gamma(x)*gamma(x+S(1)/3)*gamma(x+S(5)/3)) + # 2*3**(-3*x - 1/2)*pi*(3*x + 2)*gamma(3*x) + # + def _run(coeffs): + # find runs in coeffs such that the difference in terms (mod 1) + # of t1, t2, ..., tn is 1/n + u = list(uniq(coeffs)) + for i in range(len(u)): + dj = ([((u[j] - u[i]) % 1, j) for j in range(i + 1, len(u))]) + for one, j in dj: + if one.p == 1 and one.q != 1: + n = one.q + got = [i] + get = list(range(1, n)) + for d, j in dj: + m = n*d + if m.is_Integer and m in get: + get.remove(m) + got.append(j) + if not get: + break + else: + continue + for i, j in enumerate(got): + c = u[j] + coeffs.remove(c) + got[i] = c + return one.q, got[0], got[1:] + + def _mult_thm(gammas, numer, denom): + # pull off and analyze the leading coefficient from each gamma arg + # looking for runs in those Rationals + + # expr -> coeff + resid -> rats[resid] = coeff + rats = {} + for g in gammas: + c, resid = g.as_coeff_Add() + rats.setdefault(resid, []).append(c) + + # look for runs in Rationals for each resid + keys = sorted(rats, key=default_sort_key) + for resid in keys: + coeffs = sorted(rats[resid]) + new = [] + while True: + run = _run(coeffs) + if run is None: + break + + # process the sequence that was found: + # 1) convert all the gamma functions to have the right + # argument (could be off by an integer) + # 2) append the factors corresponding to the theorem + # 3) append the new gamma function + + n, ui, other = run + + # (1) + for u in other: + con = resid + u - 1 + for k in range(int(u - ui)): + numer.append(con - k) + + con = n*(resid + ui) # for (2) and (3) + + # (2) + numer.append((2*S.Pi)**(S(n - 1)/2)* + n**(S.Half - con)) + # (3) + new.append(con) + + # restore resid to coeffs + rats[resid] = [resid + c for c in coeffs] + new + + # rebuild the gamma arguments + g = [] + for resid in keys: + g += rats[resid] + # /!\ updating IN PLACE + gammas[:] = g + + for l, numer, denom in [(numer_gammas, numer_others, denom_others), + (denom_gammas, denom_others, numer_others)]: + _mult_thm(l, numer, denom) + + # =========== level >= 2 work: factor absorption ========= + + if level >= 2: + # Try to absorb factors into the gammas: x*gamma(x) -> gamma(x + 1) + # and gamma(x)/(x - 1) -> gamma(x - 1) + # This code (in particular repeated calls to find_fuzzy) can be very + # slow. + def find_fuzzy(l, x): + if not l: + return + S1, T1 = compute_ST(x) + for y in l: + S2, T2 = inv[y] + if T1 != T2 or (not S1.intersection(S2) and + (S1 != set() or S2 != set())): + continue + # XXX we want some simplification (e.g. cancel or + # simplify) but no matter what it's slow. + a = len(cancel(x/y).free_symbols) + b = len(x.free_symbols) + c = len(y.free_symbols) + # TODO is there a better heuristic? + if a == 0 and (b > 0 or c > 0): + return y + + # We thus try to avoid expensive calls by building the following + # "invariants": For every factor or gamma function argument + # - the set of free symbols S + # - the set of functional components T + # We will only try to absorb if T1==T2 and (S1 intersect S2 != emptyset + # or S1 == S2 == emptyset) + inv = {} + + def compute_ST(expr): + if expr in inv: + return inv[expr] + return (expr.free_symbols, expr.atoms(Function).union( + {e.exp for e in expr.atoms(Pow)})) + + def update_ST(expr): + inv[expr] = compute_ST(expr) + for expr in numer_gammas + denom_gammas + numer_others + denom_others: + update_ST(expr) + + for gammas, numer, denom in [( + numer_gammas, numer_others, denom_others), + (denom_gammas, denom_others, numer_others)]: + new = [] + while gammas: + g = gammas.pop() + cont = True + while cont: + cont = False + y = find_fuzzy(numer, g) + if y is not None: + numer.remove(y) + if y != g: + numer.append(y/g) + update_ST(y/g) + g += 1 + cont = True + y = find_fuzzy(denom, g - 1) + if y is not None: + denom.remove(y) + if y != g - 1: + numer.append((g - 1)/y) + update_ST((g - 1)/y) + g -= 1 + cont = True + new.append(g) + # /!\ updating IN PLACE + gammas[:] = new + + # =========== rebuild expr ================================== + + return Mul(*[gamma(g) for g in numer_gammas]) \ + / Mul(*[gamma(g) for g in denom_gammas]) \ + * Mul(*numer_others) / Mul(*denom_others) + + was = factor(expr) + # (for some reason we cannot use Basic.replace in this case) + expr = rule_gamma(was) + if expr != was: + expr = factor(expr) + + expr = expr.replace(gamma, + lambda n: expand_func(gamma(n)) if n.is_Rational else gamma(n)) + + return expr + + +class _rf(Function): + @classmethod + def eval(cls, a, b): + if b.is_Integer: + if not b: + return S.One + + n = int(b) + + if n > 0: + return Mul(*[a + i for i in range(n)]) + elif n < 0: + return 1/Mul(*[a - i for i in range(1, -n + 1)]) + else: + if b.is_Add: + c, _b = b.as_coeff_Add() + + if c.is_Integer: + if c > 0: + return _rf(a, _b)*_rf(a + _b, c) + elif c < 0: + return _rf(a, _b)/_rf(a + _b + c, -c) + + if a.is_Add: + c, _a = a.as_coeff_Add() + + if c.is_Integer: + if c > 0: + return _rf(_a, b)*_rf(_a + b, c)/_rf(_a, c) + elif c < 0: + return _rf(_a, b)*_rf(_a + c, -c)/_rf(_a + b + c, -c) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/hyperexpand.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/hyperexpand.py new file mode 100644 index 0000000000000000000000000000000000000000..c070aa2e44b92794107b3e33df897813a54307b9 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/hyperexpand.py @@ -0,0 +1,2494 @@ +""" +Expand Hypergeometric (and Meijer G) functions into named +special functions. + +The algorithm for doing this uses a collection of lookup tables of +hypergeometric functions, and various of their properties, to expand +many hypergeometric functions in terms of special functions. + +It is based on the following paper: + Kelly B. Roach. Meijer G Function Representations. + In: Proceedings of the 1997 International Symposium on Symbolic and + Algebraic Computation, pages 205-211, New York, 1997. ACM. + +It is described in great(er) detail in the Sphinx documentation. +""" +# SUMMARY OF EXTENSIONS FOR MEIJER G FUNCTIONS +# +# o z**rho G(ap, bq; z) = G(ap + rho, bq + rho; z) +# +# o denote z*d/dz by D +# +# o It is helpful to keep in mind that ap and bq play essentially symmetric +# roles: G(1/z) has slightly altered parameters, with ap and bq interchanged. +# +# o There are four shift operators: +# A_J = b_J - D, J = 1, ..., n +# B_J = 1 - a_j + D, J = 1, ..., m +# C_J = -b_J + D, J = m+1, ..., q +# D_J = a_J - 1 - D, J = n+1, ..., p +# +# A_J, C_J increment b_J +# B_J, D_J decrement a_J +# +# o The corresponding four inverse-shift operators are defined if there +# is no cancellation. Thus e.g. an index a_J (upper or lower) can be +# incremented if a_J != b_i for i = 1, ..., q. +# +# o Order reduction: if b_j - a_i is a non-negative integer, where +# j <= m and i > n, the corresponding quotient of gamma functions reduces +# to a polynomial. Hence the G function can be expressed using a G-function +# of lower order. +# Similarly if j > m and i <= n. +# +# Secondly, there are paired index theorems [Adamchik, The evaluation of +# integrals of Bessel functions via G-function identities]. Suppose there +# are three parameters a, b, c, where a is an a_i, i <= n, b is a b_j, +# j <= m and c is a denominator parameter (i.e. a_i, i > n or b_j, j > m). +# Suppose further all three differ by integers. +# Then the order can be reduced. +# TODO work this out in detail. +# +# o An index quadruple is called suitable if its order cannot be reduced. +# If there exists a sequence of shift operators transforming one index +# quadruple into another, we say one is reachable from the other. +# +# o Deciding if one index quadruple is reachable from another is tricky. For +# this reason, we use hand-built routines to match and instantiate formulas. +# +from collections import defaultdict +from itertools import product +from functools import reduce +from math import prod + +from sympy import SYMPY_DEBUG +from sympy.core import (S, Dummy, symbols, sympify, Tuple, expand, I, pi, Mul, + EulerGamma, oo, zoo, expand_func, Add, nan, Expr, Rational) +from sympy.core.mod import Mod +from sympy.core.sorting import default_sort_key +from sympy.functions import (exp, sqrt, root, log, lowergamma, cos, + besseli, gamma, uppergamma, expint, erf, sin, besselj, Ei, Ci, Si, Shi, + sinh, cosh, Chi, fresnels, fresnelc, polar_lift, exp_polar, floor, ceiling, + rf, factorial, lerchphi, Piecewise, re, elliptic_k, elliptic_e) +from sympy.functions.elementary.complexes import polarify, unpolarify +from sympy.functions.special.hyper import (hyper, HyperRep_atanh, + HyperRep_power1, HyperRep_power2, HyperRep_log1, HyperRep_asin1, + HyperRep_asin2, HyperRep_sqrts1, HyperRep_sqrts2, HyperRep_log2, + HyperRep_cosasin, HyperRep_sinasin, meijerg) +from sympy.matrices import Matrix, eye, zeros +from sympy.polys import apart, poly, Poly +from sympy.series import residue +from sympy.simplify.powsimp import powdenest +from sympy.utilities.iterables import sift + +# function to define "buckets" +def _mod1(x): + # TODO see if this can work as Mod(x, 1); this will require + # different handling of the "buckets" since these need to + # be sorted and that fails when there is a mixture of + # integers and expressions with parameters. With the current + # Mod behavior, Mod(k, 1) == Mod(1, 1) == 0 if k is an integer. + # Although the sorting can be done with Basic.compare, this may + # still require different handling of the sorted buckets. + if x.is_Number: + return Mod(x, 1) + c, x = x.as_coeff_Add() + return Mod(c, 1) + x + + +# leave add formulae at the top for easy reference +def add_formulae(formulae): + """ Create our knowledge base. """ + a, b, c, z = symbols('a b c, z', cls=Dummy) + + def add(ap, bq, res): + func = Hyper_Function(ap, bq) + formulae.append(Formula(func, z, res, (a, b, c))) + + def addb(ap, bq, B, C, M): + func = Hyper_Function(ap, bq) + formulae.append(Formula(func, z, None, (a, b, c), B, C, M)) + + # Luke, Y. L. (1969), The Special Functions and Their Approximations, + # Volume 1, section 6.2 + + # 0F0 + add((), (), exp(z)) + + # 1F0 + add((a, ), (), HyperRep_power1(-a, z)) + + # 2F1 + addb((a, a - S.Half), (2*a, ), + Matrix([HyperRep_power2(a, z), + HyperRep_power2(a + S.Half, z)/2]), + Matrix([[1, 0]]), + Matrix([[(a - S.Half)*z/(1 - z), (S.Half - a)*z/(1 - z)], + [a/(1 - z), a*(z - 2)/(1 - z)]])) + addb((1, 1), (2, ), + Matrix([HyperRep_log1(z), 1]), Matrix([[-1/z, 0]]), + Matrix([[0, z/(z - 1)], [0, 0]])) + addb((S.Half, 1), (S('3/2'), ), + Matrix([HyperRep_atanh(z), 1]), + Matrix([[1, 0]]), + Matrix([[Rational(-1, 2), 1/(1 - z)/2], [0, 0]])) + addb((S.Half, S.Half), (S('3/2'), ), + Matrix([HyperRep_asin1(z), HyperRep_power1(Rational(-1, 2), z)]), + Matrix([[1, 0]]), + Matrix([[Rational(-1, 2), S.Half], [0, z/(1 - z)/2]])) + addb((a, S.Half + a), (S.Half, ), + Matrix([HyperRep_sqrts1(-a, z), -HyperRep_sqrts2(-a - S.Half, z)]), + Matrix([[1, 0]]), + Matrix([[0, -a], + [z*(-2*a - 1)/2/(1 - z), S.Half - z*(-2*a - 1)/(1 - z)]])) + + # A. P. Prudnikov, Yu. A. Brychkov and O. I. Marichev (1990). + # Integrals and Series: More Special Functions, Vol. 3,. + # Gordon and Breach Science Publisher + addb([a, -a], [S.Half], + Matrix([HyperRep_cosasin(a, z), HyperRep_sinasin(a, z)]), + Matrix([[1, 0]]), + Matrix([[0, -a], [a*z/(1 - z), 1/(1 - z)/2]])) + addb([1, 1], [3*S.Half], + Matrix([HyperRep_asin2(z), 1]), Matrix([[1, 0]]), + Matrix([[(z - S.Half)/(1 - z), 1/(1 - z)/2], [0, 0]])) + + # Complete elliptic integrals K(z) and E(z), both a 2F1 function + addb([S.Half, S.Half], [S.One], + Matrix([elliptic_k(z), elliptic_e(z)]), + Matrix([[2/pi, 0]]), + Matrix([[Rational(-1, 2), -1/(2*z-2)], + [Rational(-1, 2), S.Half]])) + addb([Rational(-1, 2), S.Half], [S.One], + Matrix([elliptic_k(z), elliptic_e(z)]), + Matrix([[0, 2/pi]]), + Matrix([[Rational(-1, 2), -1/(2*z-2)], + [Rational(-1, 2), S.Half]])) + + # 3F2 + addb([Rational(-1, 2), 1, 1], [S.Half, 2], + Matrix([z*HyperRep_atanh(z), HyperRep_log1(z), 1]), + Matrix([[Rational(-2, 3), -S.One/(3*z), Rational(2, 3)]]), + Matrix([[S.Half, 0, z/(1 - z)/2], + [0, 0, z/(z - 1)], + [0, 0, 0]])) + # actually the formula for 3/2 is much nicer ... + addb([Rational(-1, 2), 1, 1], [2, 2], + Matrix([HyperRep_power1(S.Half, z), HyperRep_log2(z), 1]), + Matrix([[Rational(4, 9) - 16/(9*z), 4/(3*z), 16/(9*z)]]), + Matrix([[z/2/(z - 1), 0, 0], [1/(2*(z - 1)), 0, S.Half], [0, 0, 0]])) + + # 1F1 + addb([1], [b], Matrix([z**(1 - b) * exp(z) * lowergamma(b - 1, z), 1]), + Matrix([[b - 1, 0]]), Matrix([[1 - b + z, 1], [0, 0]])) + addb([a], [2*a], + Matrix([z**(S.Half - a)*exp(z/2)*besseli(a - S.Half, z/2) + * gamma(a + S.Half)/4**(S.Half - a), + z**(S.Half - a)*exp(z/2)*besseli(a + S.Half, z/2) + * gamma(a + S.Half)/4**(S.Half - a)]), + Matrix([[1, 0]]), + Matrix([[z/2, z/2], [z/2, (z/2 - 2*a)]])) + mz = polar_lift(-1)*z + addb([a], [a + 1], + Matrix([mz**(-a)*a*lowergamma(a, mz), a*exp(z)]), + Matrix([[1, 0]]), + Matrix([[-a, 1], [0, z]])) + # This one is redundant. + add([Rational(-1, 2)], [S.Half], exp(z) - sqrt(pi*z)*(-I)*erf(I*sqrt(z))) + + # Added to get nice results for Laplace transform of Fresnel functions + # https://functions.wolfram.com/07.22.03.6437.01 + # Basic rule + #add([1], [Rational(3, 4), Rational(5, 4)], + # sqrt(pi) * (cos(2*sqrt(polar_lift(-1)*z))*fresnelc(2*root(polar_lift(-1)*z,4)/sqrt(pi)) + + # sin(2*sqrt(polar_lift(-1)*z))*fresnels(2*root(polar_lift(-1)*z,4)/sqrt(pi))) + # / (2*root(polar_lift(-1)*z,4))) + # Manually tuned rule + addb([1], [Rational(3, 4), Rational(5, 4)], + Matrix([ sqrt(pi)*(I*sinh(2*sqrt(z))*fresnels(2*root(z, 4)*exp(I*pi/4)/sqrt(pi)) + + cosh(2*sqrt(z))*fresnelc(2*root(z, 4)*exp(I*pi/4)/sqrt(pi))) + * exp(-I*pi/4)/(2*root(z, 4)), + sqrt(pi)*root(z, 4)*(sinh(2*sqrt(z))*fresnelc(2*root(z, 4)*exp(I*pi/4)/sqrt(pi)) + + I*cosh(2*sqrt(z))*fresnels(2*root(z, 4)*exp(I*pi/4)/sqrt(pi))) + *exp(-I*pi/4)/2, + 1 ]), + Matrix([[1, 0, 0]]), + Matrix([[Rational(-1, 4), 1, Rational(1, 4)], + [ z, Rational(1, 4), 0], + [ 0, 0, 0]])) + + # 2F2 + addb([S.Half, a], [Rational(3, 2), a + 1], + Matrix([a/(2*a - 1)*(-I)*sqrt(pi/z)*erf(I*sqrt(z)), + a/(2*a - 1)*(polar_lift(-1)*z)**(-a)* + lowergamma(a, polar_lift(-1)*z), + a/(2*a - 1)*exp(z)]), + Matrix([[1, -1, 0]]), + Matrix([[Rational(-1, 2), 0, 1], [0, -a, 1], [0, 0, z]])) + # We make a "basis" of four functions instead of three, and give EulerGamma + # an extra slot (it could just be a coefficient to 1). The advantage is + # that this way Polys will not see multivariate polynomials (it treats + # EulerGamma as an indeterminate), which is *way* faster. + addb([1, 1], [2, 2], + Matrix([Ei(z) - log(z), exp(z), 1, EulerGamma]), + Matrix([[1/z, 0, 0, -1/z]]), + Matrix([[0, 1, -1, 0], [0, z, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]])) + + # 0F1 + add((), (S.Half, ), cosh(2*sqrt(z))) + addb([], [b], + Matrix([gamma(b)*z**((1 - b)/2)*besseli(b - 1, 2*sqrt(z)), + gamma(b)*z**(1 - b/2)*besseli(b, 2*sqrt(z))]), + Matrix([[1, 0]]), Matrix([[0, 1], [z, (1 - b)]])) + + # 0F3 + x = 4*z**Rational(1, 4) + + def fp(a, z): + return besseli(a, x) + besselj(a, x) + + def fm(a, z): + return besseli(a, x) - besselj(a, x) + + # TODO branching + addb([], [S.Half, a, a + S.Half], + Matrix([fp(2*a - 1, z), fm(2*a, z)*z**Rational(1, 4), + fm(2*a - 1, z)*sqrt(z), fp(2*a, z)*z**Rational(3, 4)]) + * 2**(-2*a)*gamma(2*a)*z**((1 - 2*a)/4), + Matrix([[1, 0, 0, 0]]), + Matrix([[0, 1, 0, 0], + [0, S.Half - a, 1, 0], + [0, 0, S.Half, 1], + [z, 0, 0, 1 - a]])) + x = 2*(4*z)**Rational(1, 4)*exp_polar(I*pi/4) + addb([], [a, a + S.Half, 2*a], + (2*sqrt(polar_lift(-1)*z))**(1 - 2*a)*gamma(2*a)**2 * + Matrix([besselj(2*a - 1, x)*besseli(2*a - 1, x), + x*(besseli(2*a, x)*besselj(2*a - 1, x) + - besseli(2*a - 1, x)*besselj(2*a, x)), + x**2*besseli(2*a, x)*besselj(2*a, x), + x**3*(besseli(2*a, x)*besselj(2*a - 1, x) + + besseli(2*a - 1, x)*besselj(2*a, x))]), + Matrix([[1, 0, 0, 0]]), + Matrix([[0, Rational(1, 4), 0, 0], + [0, (1 - 2*a)/2, Rational(-1, 2), 0], + [0, 0, 1 - 2*a, Rational(1, 4)], + [-32*z, 0, 0, 1 - a]])) + + # 1F2 + addb([a], [a - S.Half, 2*a], + Matrix([z**(S.Half - a)*besseli(a - S.Half, sqrt(z))**2, + z**(1 - a)*besseli(a - S.Half, sqrt(z)) + *besseli(a - Rational(3, 2), sqrt(z)), + z**(Rational(3, 2) - a)*besseli(a - Rational(3, 2), sqrt(z))**2]), + Matrix([[-gamma(a + S.Half)**2/4**(S.Half - a), + 2*gamma(a - S.Half)*gamma(a + S.Half)/4**(1 - a), + 0]]), + Matrix([[1 - 2*a, 1, 0], [z/2, S.Half - a, S.Half], [0, z, 0]])) + addb([S.Half], [b, 2 - b], + pi*(1 - b)/sin(pi*b)* + Matrix([besseli(1 - b, sqrt(z))*besseli(b - 1, sqrt(z)), + sqrt(z)*(besseli(-b, sqrt(z))*besseli(b - 1, sqrt(z)) + + besseli(1 - b, sqrt(z))*besseli(b, sqrt(z))), + besseli(-b, sqrt(z))*besseli(b, sqrt(z))]), + Matrix([[1, 0, 0]]), + Matrix([[b - 1, S.Half, 0], + [z, 0, z], + [0, S.Half, -b]])) + addb([S.Half], [Rational(3, 2), Rational(3, 2)], + Matrix([Shi(2*sqrt(z))/2/sqrt(z), sinh(2*sqrt(z))/2/sqrt(z), + cosh(2*sqrt(z))]), + Matrix([[1, 0, 0]]), + Matrix([[Rational(-1, 2), S.Half, 0], [0, Rational(-1, 2), S.Half], [0, 2*z, 0]])) + + # FresnelS + # Basic rule + #add([Rational(3, 4)], [Rational(3, 2),Rational(7, 4)], 6*fresnels( exp(pi*I/4)*root(z,4)*2/sqrt(pi) ) / ( pi * (exp(pi*I/4)*root(z,4)*2/sqrt(pi))**3 ) ) + # Manually tuned rule + addb([Rational(3, 4)], [Rational(3, 2), Rational(7, 4)], + Matrix( + [ fresnels( + exp( + pi*I/4)*root( + z, 4)*2/sqrt( + pi) ) / ( + pi * (exp(pi*I/4)*root(z, 4)*2/sqrt(pi))**3 ), + sinh(2*sqrt(z))/sqrt(z), + cosh(2*sqrt(z)) ]), + Matrix([[6, 0, 0]]), + Matrix([[Rational(-3, 4), Rational(1, 16), 0], + [ 0, Rational(-1, 2), 1], + [ 0, z, 0]])) + + # FresnelC + # Basic rule + #add([Rational(1, 4)], [S.Half,Rational(5, 4)], fresnelc( exp(pi*I/4)*root(z,4)*2/sqrt(pi) ) / ( exp(pi*I/4)*root(z,4)*2/sqrt(pi) ) ) + # Manually tuned rule + addb([Rational(1, 4)], [S.Half, Rational(5, 4)], + Matrix( + [ sqrt( + pi)*exp( + -I*pi/4)*fresnelc( + 2*root(z, 4)*exp(I*pi/4)/sqrt(pi))/(2*root(z, 4)), + cosh(2*sqrt(z)), + sinh(2*sqrt(z))*sqrt(z) ]), + Matrix([[1, 0, 0]]), + Matrix([[Rational(-1, 4), Rational(1, 4), 0 ], + [ 0, 0, 1 ], + [ 0, z, S.Half]])) + + # 2F3 + # XXX with this five-parameter formula is pretty slow with the current + # Formula.find_instantiations (creates 2!*3!*3**(2+3) ~ 3000 + # instantiations ... But it's not too bad. + addb([a, a + S.Half], [2*a, b, 2*a - b + 1], + gamma(b)*gamma(2*a - b + 1) * (sqrt(z)/2)**(1 - 2*a) * + Matrix([besseli(b - 1, sqrt(z))*besseli(2*a - b, sqrt(z)), + sqrt(z)*besseli(b, sqrt(z))*besseli(2*a - b, sqrt(z)), + sqrt(z)*besseli(b - 1, sqrt(z))*besseli(2*a - b + 1, sqrt(z)), + besseli(b, sqrt(z))*besseli(2*a - b + 1, sqrt(z))]), + Matrix([[1, 0, 0, 0]]), + Matrix([[0, S.Half, S.Half, 0], + [z/2, 1 - b, 0, z/2], + [z/2, 0, b - 2*a, z/2], + [0, S.Half, S.Half, -2*a]])) + # (C/f above comment about eulergamma in the basis). + addb([1, 1], [2, 2, Rational(3, 2)], + Matrix([Chi(2*sqrt(z)) - log(2*sqrt(z)), + cosh(2*sqrt(z)), sqrt(z)*sinh(2*sqrt(z)), 1, EulerGamma]), + Matrix([[1/z, 0, 0, 0, -1/z]]), + Matrix([[0, S.Half, 0, Rational(-1, 2), 0], + [0, 0, 1, 0, 0], + [0, z, S.Half, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]])) + + # 3F3 + # This is rule: https://functions.wolfram.com/07.31.03.0134.01 + # Initial reason to add it was a nice solution for + # integrate(erf(a*z)/z**2, z) and same for erfc and erfi. + # Basic rule + # add([1, 1, a], [2, 2, a+1], (a/(z*(a-1)**2)) * + # (1 - (-z)**(1-a) * (gamma(a) - uppergamma(a,-z)) + # - (a-1) * (EulerGamma + uppergamma(0,-z) + log(-z)) + # - exp(z))) + # Manually tuned rule + addb([1, 1, a], [2, 2, a+1], + Matrix([a*(log(-z) + expint(1, -z) + EulerGamma)/(z*(a**2 - 2*a + 1)), + a*(-z)**(-a)*(gamma(a) - uppergamma(a, -z))/(a - 1)**2, + a*exp(z)/(a**2 - 2*a + 1), + a/(z*(a**2 - 2*a + 1))]), + Matrix([[1-a, 1, -1/z, 1]]), + Matrix([[-1,0,-1/z,1], + [0,-a,1,0], + [0,0,z,0], + [0,0,0,-1]])) + + +def add_meijerg_formulae(formulae): + a, b, c, z = list(map(Dummy, 'abcz')) + rho = Dummy('rho') + + def add(an, ap, bm, bq, B, C, M, matcher): + formulae.append(MeijerFormula(an, ap, bm, bq, z, [a, b, c, rho], + B, C, M, matcher)) + + def detect_uppergamma(func): + x = func.an[0] + y, z = func.bm + swapped = False + if not _mod1((x - y).simplify()): + swapped = True + (y, z) = (z, y) + if _mod1((x - z).simplify()) or x - z > 0: + return None + l = [y, x] + if swapped: + l = [x, y] + return {rho: y, a: x - y}, G_Function([x], [], l, []) + + add([a + rho], [], [rho, a + rho], [], + Matrix([gamma(1 - a)*z**rho*exp(z)*uppergamma(a, z), + gamma(1 - a)*z**(a + rho)]), + Matrix([[1, 0]]), + Matrix([[rho + z, -1], [0, a + rho]]), + detect_uppergamma) + + def detect_3113(func): + """https://functions.wolfram.com/07.34.03.0984.01""" + x = func.an[0] + u, v, w = func.bm + if _mod1((u - v).simplify()) == 0: + if _mod1((v - w).simplify()) == 0: + return + sig = (S.Half, S.Half, S.Zero) + x1, x2, y = u, v, w + else: + if _mod1((x - u).simplify()) == 0: + sig = (S.Half, S.Zero, S.Half) + x1, y, x2 = u, v, w + else: + sig = (S.Zero, S.Half, S.Half) + y, x1, x2 = u, v, w + + if (_mod1((x - x1).simplify()) != 0 or + _mod1((x - x2).simplify()) != 0 or + _mod1((x - y).simplify()) != S.Half or + x - x1 > 0 or x - x2 > 0): + return + + return {a: x}, G_Function([x], [], [x - S.Half + t for t in sig], []) + + s = sin(2*sqrt(z)) + c_ = cos(2*sqrt(z)) + S_ = Si(2*sqrt(z)) - pi/2 + C = Ci(2*sqrt(z)) + add([a], [], [a, a, a - S.Half], [], + Matrix([sqrt(pi)*z**(a - S.Half)*(c_*S_ - s*C), + sqrt(pi)*z**a*(s*S_ + c_*C), + sqrt(pi)*z**a]), + Matrix([[-2, 0, 0]]), + Matrix([[a - S.Half, -1, 0], [z, a, S.Half], [0, 0, a]]), + detect_3113) + + +def make_simp(z): + """ Create a function that simplifies rational functions in ``z``. """ + + def simp(expr): + """ Efficiently simplify the rational function ``expr``. """ + numer, denom = expr.as_numer_denom() + numer = numer.expand() + # denom = denom.expand() # is this needed? + c, numer, denom = poly(numer, z).cancel(poly(denom, z)) + return c * numer.as_expr() / denom.as_expr() + + return simp + + +def debug(*args): + if SYMPY_DEBUG: + for a in args: + print(a, end="") + print() + + +class Hyper_Function(Expr): + """ A generalized hypergeometric function. """ + + def __new__(cls, ap, bq): + obj = super().__new__(cls) + obj.ap = Tuple(*list(map(expand, ap))) + obj.bq = Tuple(*list(map(expand, bq))) + return obj + + @property + def args(self): + return (self.ap, self.bq) + + @property + def sizes(self): + return (len(self.ap), len(self.bq)) + + @property + def gamma(self): + """ + Number of upper parameters that are negative integers + + This is a transformation invariant. + """ + return sum(bool(x.is_integer and x.is_negative) for x in self.ap) + + def _hashable_content(self): + return super()._hashable_content() + (self.ap, + self.bq) + + def __call__(self, arg): + return hyper(self.ap, self.bq, arg) + + def build_invariants(self): + """ + Compute the invariant vector. + + Explanation + =========== + + The invariant vector is: + (gamma, ((s1, n1), ..., (sk, nk)), ((t1, m1), ..., (tr, mr))) + where gamma is the number of integer a < 0, + s1 < ... < sk + nl is the number of parameters a_i congruent to sl mod 1 + t1 < ... < tr + ml is the number of parameters b_i congruent to tl mod 1 + + If the index pair contains parameters, then this is not truly an + invariant, since the parameters cannot be sorted uniquely mod1. + + Examples + ======== + + >>> from sympy.simplify.hyperexpand import Hyper_Function + >>> from sympy import S + >>> ap = (S.Half, S.One/3, S(-1)/2, -2) + >>> bq = (1, 2) + + Here gamma = 1, + k = 3, s1 = 0, s2 = 1/3, s3 = 1/2 + n1 = 1, n2 = 1, n2 = 2 + r = 1, t1 = 0 + m1 = 2: + + >>> Hyper_Function(ap, bq).build_invariants() + (1, ((0, 1), (1/3, 1), (1/2, 2)), ((0, 2),)) + """ + abuckets, bbuckets = sift(self.ap, _mod1), sift(self.bq, _mod1) + + def tr(bucket): + bucket = list(bucket.items()) + if not any(isinstance(x[0], Mod) for x in bucket): + bucket.sort(key=lambda x: default_sort_key(x[0])) + bucket = tuple([(mod, len(values)) for mod, values in bucket if + values]) + return bucket + + return (self.gamma, tr(abuckets), tr(bbuckets)) + + def difficulty(self, func): + """ Estimate how many steps it takes to reach ``func`` from self. + Return -1 if impossible. """ + if self.gamma != func.gamma: + return -1 + oabuckets, obbuckets, abuckets, bbuckets = [sift(params, _mod1) for + params in (self.ap, self.bq, func.ap, func.bq)] + + diff = 0 + for bucket, obucket in [(abuckets, oabuckets), (bbuckets, obbuckets)]: + for mod in set(list(bucket.keys()) + list(obucket.keys())): + if (mod not in bucket) or (mod not in obucket) \ + or len(bucket[mod]) != len(obucket[mod]): + return -1 + l1 = list(bucket[mod]) + l2 = list(obucket[mod]) + l1.sort() + l2.sort() + for i, j in zip(l1, l2): + diff += abs(i - j) + + return diff + + def _is_suitable_origin(self): + """ + Decide if ``self`` is a suitable origin. + + Explanation + =========== + + A function is a suitable origin iff: + * none of the ai equals bj + n, with n a non-negative integer + * none of the ai is zero + * none of the bj is a non-positive integer + + Note that this gives meaningful results only when none of the indices + are symbolic. + + """ + for a in self.ap: + for b in self.bq: + if (a - b).is_integer and (a - b).is_negative is False: + return False + for a in self.ap: + if a == 0: + return False + for b in self.bq: + if b.is_integer and b.is_nonpositive: + return False + return True + + +class G_Function(Expr): + """ A Meijer G-function. """ + + def __new__(cls, an, ap, bm, bq): + obj = super().__new__(cls) + obj.an = Tuple(*list(map(expand, an))) + obj.ap = Tuple(*list(map(expand, ap))) + obj.bm = Tuple(*list(map(expand, bm))) + obj.bq = Tuple(*list(map(expand, bq))) + return obj + + @property + def args(self): + return (self.an, self.ap, self.bm, self.bq) + + def _hashable_content(self): + return super()._hashable_content() + self.args + + def __call__(self, z): + return meijerg(self.an, self.ap, self.bm, self.bq, z) + + def compute_buckets(self): + """ + Compute buckets for the fours sets of parameters. + + Explanation + =========== + + We guarantee that any two equal Mod objects returned are actually the + same, and that the buckets are sorted by real part (an and bq + descendending, bm and ap ascending). + + Examples + ======== + + >>> from sympy.simplify.hyperexpand import G_Function + >>> from sympy.abc import y + >>> from sympy import S + + >>> a, b = [1, 3, 2, S(3)/2], [1 + y, y, 2, y + 3] + >>> G_Function(a, b, [2], [y]).compute_buckets() + ({0: [3, 2, 1], 1/2: [3/2]}, + {0: [2], y: [y, y + 1, y + 3]}, {0: [2]}, {y: [y]}) + + """ + dicts = pan, pap, pbm, pbq = [defaultdict(list) for i in range(4)] + for dic, lis in zip(dicts, (self.an, self.ap, self.bm, self.bq)): + for x in lis: + dic[_mod1(x)].append(x) + + for dic, flip in zip(dicts, (True, False, False, True)): + for m, items in dic.items(): + x0 = items[0] + items.sort(key=lambda x: x - x0, reverse=flip) + dic[m] = items + + return tuple([dict(w) for w in dicts]) + + @property + def signature(self): + return (len(self.an), len(self.ap), len(self.bm), len(self.bq)) + + +# Dummy variable. +_x = Dummy('x') + +class Formula: + """ + This class represents hypergeometric formulae. + + Explanation + =========== + + Its data members are: + - z, the argument + - closed_form, the closed form expression + - symbols, the free symbols (parameters) in the formula + - func, the function + - B, C, M (see _compute_basis) + + Examples + ======== + + >>> from sympy.abc import a, b, z + >>> from sympy.simplify.hyperexpand import Formula, Hyper_Function + >>> func = Hyper_Function((a/2, a/3 + b, (1+a)/2), (a, b, (a+b)/7)) + >>> f = Formula(func, z, None, [a, b]) + + """ + + def _compute_basis(self, closed_form): + """ + Compute a set of functions B=(f1, ..., fn), a nxn matrix M + and a 1xn matrix C such that: + closed_form = C B + z d/dz B = M B. + """ + afactors = [_x + a for a in self.func.ap] + bfactors = [_x + b - 1 for b in self.func.bq] + expr = _x*Mul(*bfactors) - self.z*Mul(*afactors) + poly = Poly(expr, _x) + + n = poly.degree() - 1 + b = [closed_form] + for _ in range(n): + b.append(self.z*b[-1].diff(self.z)) + + self.B = Matrix(b) + self.C = Matrix([[1] + [0]*n]) + + m = eye(n) + m = m.col_insert(0, zeros(n, 1)) + l = poly.all_coeffs()[1:] + l.reverse() + self.M = m.row_insert(n, -Matrix([l])/poly.all_coeffs()[0]) + + def __init__(self, func, z, res, symbols, B=None, C=None, M=None): + z = sympify(z) + res = sympify(res) + symbols = [x for x in sympify(symbols) if func.has(x)] + + self.z = z + self.symbols = symbols + self.B = B + self.C = C + self.M = M + self.func = func + + # TODO with symbolic parameters, it could be advantageous + # (for prettier answers) to compute a basis only *after* + # instantiation + if res is not None: + self._compute_basis(res) + + @property + def closed_form(self): + return reduce(lambda s,m: s+m[0]*m[1], zip(self.C, self.B), S.Zero) + + def find_instantiations(self, func): + """ + Find substitutions of the free symbols that match ``func``. + + Return the substitution dictionaries as a list. Note that the returned + instantiations need not actually match, or be valid! + + """ + from sympy.solvers import solve + ap = func.ap + bq = func.bq + if len(ap) != len(self.func.ap) or len(bq) != len(self.func.bq): + raise TypeError('Cannot instantiate other number of parameters') + symbol_values = [] + for a in self.symbols: + if a in self.func.ap.args: + symbol_values.append(ap) + elif a in self.func.bq.args: + symbol_values.append(bq) + else: + raise ValueError("At least one of the parameters of the " + "formula must be equal to %s" % (a,)) + base_repl = [dict(list(zip(self.symbols, values))) + for values in product(*symbol_values)] + abuckets, bbuckets = [sift(params, _mod1) for params in [ap, bq]] + a_inv, b_inv = [{a: len(vals) for a, vals in bucket.items()} + for bucket in [abuckets, bbuckets]] + critical_values = [[0] for _ in self.symbols] + result = [] + _n = Dummy() + for repl in base_repl: + symb_a, symb_b = [sift(params, lambda x: _mod1(x.xreplace(repl))) + for params in [self.func.ap, self.func.bq]] + for bucket, obucket in [(abuckets, symb_a), (bbuckets, symb_b)]: + for mod in set(list(bucket.keys()) + list(obucket.keys())): + if (mod not in bucket) or (mod not in obucket) \ + or len(bucket[mod]) != len(obucket[mod]): + break + for a, vals in zip(self.symbols, critical_values): + if repl[a].free_symbols: + continue + exprs = [expr for expr in obucket[mod] if expr.has(a)] + repl0 = repl.copy() + repl0[a] += _n + for expr in exprs: + for target in bucket[mod]: + n0, = solve(expr.xreplace(repl0) - target, _n) + if n0.free_symbols: + raise ValueError("Value should not be true") + vals.append(n0) + else: + values = [] + for a, vals in zip(self.symbols, critical_values): + a0 = repl[a] + min_ = floor(min(vals)) + max_ = ceiling(max(vals)) + values.append([a0 + n for n in range(min_, max_ + 1)]) + result.extend(dict(list(zip(self.symbols, l))) for l in product(*values)) + return result + + + + +class FormulaCollection: + """ A collection of formulae to use as origins. """ + + def __init__(self): + """ Doing this globally at module init time is a pain ... """ + self.symbolic_formulae = {} + self.concrete_formulae = {} + self.formulae = [] + + add_formulae(self.formulae) + + # Now process the formulae into a helpful form. + # These dicts are indexed by (p, q). + + for f in self.formulae: + sizes = f.func.sizes + if len(f.symbols) > 0: + self.symbolic_formulae.setdefault(sizes, []).append(f) + else: + inv = f.func.build_invariants() + self.concrete_formulae.setdefault(sizes, {})[inv] = f + + def lookup_origin(self, func): + """ + Given the suitable target ``func``, try to find an origin in our + knowledge base. + + Examples + ======== + + >>> from sympy.simplify.hyperexpand import (FormulaCollection, + ... Hyper_Function) + >>> f = FormulaCollection() + >>> f.lookup_origin(Hyper_Function((), ())).closed_form + exp(_z) + >>> f.lookup_origin(Hyper_Function([1], ())).closed_form + HyperRep_power1(-1, _z) + + >>> from sympy import S + >>> i = Hyper_Function([S('1/4'), S('3/4 + 4')], [S.Half]) + >>> f.lookup_origin(i).closed_form + HyperRep_sqrts1(-1/4, _z) + """ + inv = func.build_invariants() + sizes = func.sizes + if sizes in self.concrete_formulae and \ + inv in self.concrete_formulae[sizes]: + return self.concrete_formulae[sizes][inv] + + # We don't have a concrete formula. Try to instantiate. + if sizes not in self.symbolic_formulae: + return None # Too bad... + + possible = [] + for f in self.symbolic_formulae[sizes]: + repls = f.find_instantiations(func) + for repl in repls: + func2 = f.func.xreplace(repl) + if not func2._is_suitable_origin(): + continue + diff = func2.difficulty(func) + if diff == -1: + continue + possible.append((diff, repl, f, func2)) + + # find the nearest origin + possible.sort(key=lambda x: x[0]) + for _, repl, f, func2 in possible: + f2 = Formula(func2, f.z, None, [], f.B.subs(repl), + f.C.subs(repl), f.M.subs(repl)) + if not any(e.has(S.NaN, oo, -oo, zoo) for e in [f2.B, f2.M, f2.C]): + return f2 + + return None + + +class MeijerFormula: + """ + This class represents a Meijer G-function formula. + + Its data members are: + - z, the argument + - symbols, the free symbols (parameters) in the formula + - func, the function + - B, C, M (c/f ordinary Formula) + """ + + def __init__(self, an, ap, bm, bq, z, symbols, B, C, M, matcher): + an, ap, bm, bq = [Tuple(*list(map(expand, w))) for w in [an, ap, bm, bq]] + self.func = G_Function(an, ap, bm, bq) + self.z = z + self.symbols = symbols + self._matcher = matcher + self.B = B + self.C = C + self.M = M + + @property + def closed_form(self): + return reduce(lambda s,m: s+m[0]*m[1], zip(self.C, self.B), S.Zero) + + def try_instantiate(self, func): + """ + Try to instantiate the current formula to (almost) match func. + This uses the _matcher passed on init. + """ + if func.signature != self.func.signature: + return None + res = self._matcher(func) + if res is not None: + subs, newfunc = res + return MeijerFormula(newfunc.an, newfunc.ap, newfunc.bm, newfunc.bq, + self.z, [], + self.B.subs(subs), self.C.subs(subs), + self.M.subs(subs), None) + + +class MeijerFormulaCollection: + """ + This class holds a collection of meijer g formulae. + """ + + def __init__(self): + formulae = [] + add_meijerg_formulae(formulae) + self.formulae = defaultdict(list) + for formula in formulae: + self.formulae[formula.func.signature].append(formula) + self.formulae = dict(self.formulae) + + def lookup_origin(self, func): + """ Try to find a formula that matches func. """ + if func.signature not in self.formulae: + return None + for formula in self.formulae[func.signature]: + res = formula.try_instantiate(func) + if res is not None: + return res + + +class Operator: + """ + Base class for operators to be applied to our functions. + + Explanation + =========== + + These operators are differential operators. They are by convention + expressed in the variable D = z*d/dz (although this base class does + not actually care). + Note that when the operator is applied to an object, we typically do + *not* blindly differentiate but instead use a different representation + of the z*d/dz operator (see make_derivative_operator). + + To subclass from this, define a __init__ method that initializes a + self._poly variable. This variable stores a polynomial. By convention + the generator is z*d/dz, and acts to the right of all coefficients. + + Thus this poly + x**2 + 2*z*x + 1 + represents the differential operator + (z*d/dz)**2 + 2*z**2*d/dz. + + This class is used only in the implementation of the hypergeometric + function expansion algorithm. + """ + + def apply(self, obj, op): + """ + Apply ``self`` to the object ``obj``, where the generator is ``op``. + + Examples + ======== + + >>> from sympy.simplify.hyperexpand import Operator + >>> from sympy.polys.polytools import Poly + >>> from sympy.abc import x, y, z + >>> op = Operator() + >>> op._poly = Poly(x**2 + z*x + y, x) + >>> op.apply(z**7, lambda f: f.diff(z)) + y*z**7 + 7*z**7 + 42*z**5 + """ + coeffs = self._poly.all_coeffs() + coeffs.reverse() + diffs = [obj] + for c in coeffs[1:]: + diffs.append(op(diffs[-1])) + r = coeffs[0]*diffs[0] + for c, d in zip(coeffs[1:], diffs[1:]): + r += c*d + return r + + +class MultOperator(Operator): + """ Simply multiply by a "constant" """ + + def __init__(self, p): + self._poly = Poly(p, _x) + + +class ShiftA(Operator): + """ Increment an upper index. """ + + def __init__(self, ai): + ai = sympify(ai) + if ai == 0: + raise ValueError('Cannot increment zero upper index.') + self._poly = Poly(_x/ai + 1, _x) + + def __str__(self): + return '' % (1/self._poly.all_coeffs()[0]) + + +class ShiftB(Operator): + """ Decrement a lower index. """ + + def __init__(self, bi): + bi = sympify(bi) + if bi == 1: + raise ValueError('Cannot decrement unit lower index.') + self._poly = Poly(_x/(bi - 1) + 1, _x) + + def __str__(self): + return '' % (1/self._poly.all_coeffs()[0] + 1) + + +class UnShiftA(Operator): + """ Decrement an upper index. """ + + def __init__(self, ap, bq, i, z): + """ Note: i counts from zero! """ + ap, bq, i = list(map(sympify, [ap, bq, i])) + + self._ap = ap + self._bq = bq + self._i = i + + ap = list(ap) + bq = list(bq) + ai = ap.pop(i) - 1 + + if ai == 0: + raise ValueError('Cannot decrement unit upper index.') + + m = Poly(z*ai, _x) + for a in ap: + m *= Poly(_x + a, _x) + + A = Dummy('A') + n = D = Poly(ai*A - ai, A) + for b in bq: + n *= D + (b - 1).as_poly(A) + + b0 = -n.nth(0) + if b0 == 0: + raise ValueError('Cannot decrement upper index: ' + 'cancels with lower') + + n = Poly(Poly(n.all_coeffs()[:-1], A).as_expr().subs(A, _x/ai + 1), _x) + + self._poly = Poly((n - m)/b0, _x) + + def __str__(self): + return '' % (self._i, + self._ap, self._bq) + + +class UnShiftB(Operator): + """ Increment a lower index. """ + + def __init__(self, ap, bq, i, z): + """ Note: i counts from zero! """ + ap, bq, i = list(map(sympify, [ap, bq, i])) + + self._ap = ap + self._bq = bq + self._i = i + + ap = list(ap) + bq = list(bq) + bi = bq.pop(i) + 1 + + if bi == 0: + raise ValueError('Cannot increment -1 lower index.') + + m = Poly(_x*(bi - 1), _x) + for b in bq: + m *= Poly(_x + b - 1, _x) + + B = Dummy('B') + D = Poly((bi - 1)*B - bi + 1, B) + n = Poly(z, B) + for a in ap: + n *= (D + a.as_poly(B)) + + b0 = n.nth(0) + if b0 == 0: + raise ValueError('Cannot increment index: cancels with upper') + + n = Poly(Poly(n.all_coeffs()[:-1], B).as_expr().subs( + B, _x/(bi - 1) + 1), _x) + + self._poly = Poly((m - n)/b0, _x) + + def __str__(self): + return '' % (self._i, + self._ap, self._bq) + + +class MeijerShiftA(Operator): + """ Increment an upper b index. """ + + def __init__(self, bi): + bi = sympify(bi) + self._poly = Poly(bi - _x, _x) + + def __str__(self): + return '' % (self._poly.all_coeffs()[1]) + + +class MeijerShiftB(Operator): + """ Decrement an upper a index. """ + + def __init__(self, bi): + bi = sympify(bi) + self._poly = Poly(1 - bi + _x, _x) + + def __str__(self): + return '' % (1 - self._poly.all_coeffs()[1]) + + +class MeijerShiftC(Operator): + """ Increment a lower b index. """ + + def __init__(self, bi): + bi = sympify(bi) + self._poly = Poly(-bi + _x, _x) + + def __str__(self): + return '' % (-self._poly.all_coeffs()[1]) + + +class MeijerShiftD(Operator): + """ Decrement a lower a index. """ + + def __init__(self, bi): + bi = sympify(bi) + self._poly = Poly(bi - 1 - _x, _x) + + def __str__(self): + return '' % (self._poly.all_coeffs()[1] + 1) + + +class MeijerUnShiftA(Operator): + """ Decrement an upper b index. """ + + def __init__(self, an, ap, bm, bq, i, z): + """ Note: i counts from zero! """ + an, ap, bm, bq, i = list(map(sympify, [an, ap, bm, bq, i])) + + self._an = an + self._ap = ap + self._bm = bm + self._bq = bq + self._i = i + + an = list(an) + ap = list(ap) + bm = list(bm) + bq = list(bq) + bi = bm.pop(i) - 1 + + m = Poly(1, _x) * prod(Poly(b - _x, _x) for b in bm) * prod(Poly(_x - b, _x) for b in bq) + + A = Dummy('A') + D = Poly(bi - A, A) + n = Poly(z, A) * prod((D + 1 - a) for a in an) * prod((-D + a - 1) for a in ap) + + b0 = n.nth(0) + if b0 == 0: + raise ValueError('Cannot decrement upper b index (cancels)') + + n = Poly(Poly(n.all_coeffs()[:-1], A).as_expr().subs(A, bi - _x), _x) + + self._poly = Poly((m - n)/b0, _x) + + def __str__(self): + return '' % (self._i, + self._an, self._ap, self._bm, self._bq) + + +class MeijerUnShiftB(Operator): + """ Increment an upper a index. """ + + def __init__(self, an, ap, bm, bq, i, z): + """ Note: i counts from zero! """ + an, ap, bm, bq, i = list(map(sympify, [an, ap, bm, bq, i])) + + self._an = an + self._ap = ap + self._bm = bm + self._bq = bq + self._i = i + + an = list(an) + ap = list(ap) + bm = list(bm) + bq = list(bq) + ai = an.pop(i) + 1 + + m = Poly(z, _x) + for a in an: + m *= Poly(1 - a + _x, _x) + for a in ap: + m *= Poly(a - 1 - _x, _x) + + B = Dummy('B') + D = Poly(B + ai - 1, B) + n = Poly(1, B) + for b in bm: + n *= (-D + b) + for b in bq: + n *= (D - b) + + b0 = n.nth(0) + if b0 == 0: + raise ValueError('Cannot increment upper a index (cancels)') + + n = Poly(Poly(n.all_coeffs()[:-1], B).as_expr().subs( + B, 1 - ai + _x), _x) + + self._poly = Poly((m - n)/b0, _x) + + def __str__(self): + return '' % (self._i, + self._an, self._ap, self._bm, self._bq) + + +class MeijerUnShiftC(Operator): + """ Decrement a lower b index. """ + # XXX this is "essentially" the same as MeijerUnShiftA. This "essentially" + # can be made rigorous using the functional equation G(1/z) = G'(z), + # where G' denotes a G function of slightly altered parameters. + # However, sorting out the details seems harder than just coding it + # again. + + def __init__(self, an, ap, bm, bq, i, z): + """ Note: i counts from zero! """ + an, ap, bm, bq, i = list(map(sympify, [an, ap, bm, bq, i])) + + self._an = an + self._ap = ap + self._bm = bm + self._bq = bq + self._i = i + + an = list(an) + ap = list(ap) + bm = list(bm) + bq = list(bq) + bi = bq.pop(i) - 1 + + m = Poly(1, _x) + for b in bm: + m *= Poly(b - _x, _x) + for b in bq: + m *= Poly(_x - b, _x) + + C = Dummy('C') + D = Poly(bi + C, C) + n = Poly(z, C) + for a in an: + n *= (D + 1 - a) + for a in ap: + n *= (-D + a - 1) + + b0 = n.nth(0) + if b0 == 0: + raise ValueError('Cannot decrement lower b index (cancels)') + + n = Poly(Poly(n.all_coeffs()[:-1], C).as_expr().subs(C, _x - bi), _x) + + self._poly = Poly((m - n)/b0, _x) + + def __str__(self): + return '' % (self._i, + self._an, self._ap, self._bm, self._bq) + + +class MeijerUnShiftD(Operator): + """ Increment a lower a index. """ + # XXX This is essentially the same as MeijerUnShiftA. + # See comment at MeijerUnShiftC. + + def __init__(self, an, ap, bm, bq, i, z): + """ Note: i counts from zero! """ + an, ap, bm, bq, i = list(map(sympify, [an, ap, bm, bq, i])) + + self._an = an + self._ap = ap + self._bm = bm + self._bq = bq + self._i = i + + an = list(an) + ap = list(ap) + bm = list(bm) + bq = list(bq) + ai = ap.pop(i) + 1 + + m = Poly(z, _x) + for a in an: + m *= Poly(1 - a + _x, _x) + for a in ap: + m *= Poly(a - 1 - _x, _x) + + B = Dummy('B') # - this is the shift operator `D_I` + D = Poly(ai - 1 - B, B) + n = Poly(1, B) + for b in bm: + n *= (-D + b) + for b in bq: + n *= (D - b) + + b0 = n.nth(0) + if b0 == 0: + raise ValueError('Cannot increment lower a index (cancels)') + + n = Poly(Poly(n.all_coeffs()[:-1], B).as_expr().subs( + B, ai - 1 - _x), _x) + + self._poly = Poly((m - n)/b0, _x) + + def __str__(self): + return '' % (self._i, + self._an, self._ap, self._bm, self._bq) + + +class ReduceOrder(Operator): + """ Reduce Order by cancelling an upper and a lower index. """ + + def __new__(cls, ai, bj): + """ For convenience if reduction is not possible, return None. """ + ai = sympify(ai) + bj = sympify(bj) + n = ai - bj + if not n.is_Integer or n < 0: + return None + if bj.is_integer and bj.is_nonpositive: + return None + + expr = Operator.__new__(cls) + + p = S.One + for k in range(n): + p *= (_x + bj + k)/(bj + k) + + expr._poly = Poly(p, _x) + expr._a = ai + expr._b = bj + + return expr + + @classmethod + def _meijer(cls, b, a, sign): + """ Cancel b + sign*s and a + sign*s + This is for meijer G functions. """ + b = sympify(b) + a = sympify(a) + n = b - a + if n.is_negative or not n.is_Integer: + return None + + expr = Operator.__new__(cls) + + p = S.One + for k in range(n): + p *= (sign*_x + a + k) + + expr._poly = Poly(p, _x) + if sign == -1: + expr._a = b + expr._b = a + else: + expr._b = Add(1, a - 1, evaluate=False) + expr._a = Add(1, b - 1, evaluate=False) + + return expr + + @classmethod + def meijer_minus(cls, b, a): + return cls._meijer(b, a, -1) + + @classmethod + def meijer_plus(cls, a, b): + return cls._meijer(1 - a, 1 - b, 1) + + def __str__(self): + return '' % \ + (self._a, self._b) + + +def _reduce_order(ap, bq, gen, key): + """ Order reduction algorithm used in Hypergeometric and Meijer G """ + ap = list(ap) + bq = list(bq) + + ap.sort(key=key) + bq.sort(key=key) + + nap = [] + # we will edit bq in place + operators = [] + for a in ap: + op = None + for i in range(len(bq)): + op = gen(a, bq[i]) + if op is not None: + bq.pop(i) + break + if op is None: + nap.append(a) + else: + operators.append(op) + + return nap, bq, operators + + +def reduce_order(func): + """ + Given the hypergeometric function ``func``, find a sequence of operators to + reduces order as much as possible. + + Explanation + =========== + + Return (newfunc, [operators]), where applying the operators to the + hypergeometric function newfunc yields func. + + Examples + ======== + + >>> from sympy.simplify.hyperexpand import reduce_order, Hyper_Function + >>> reduce_order(Hyper_Function((1, 2), (3, 4))) + (Hyper_Function((1, 2), (3, 4)), []) + >>> reduce_order(Hyper_Function((1,), (1,))) + (Hyper_Function((), ()), []) + >>> reduce_order(Hyper_Function((2, 4), (3, 3))) + (Hyper_Function((2,), (3,)), []) + """ + nap, nbq, operators = _reduce_order(func.ap, func.bq, ReduceOrder, default_sort_key) + + return Hyper_Function(Tuple(*nap), Tuple(*nbq)), operators + + +def reduce_order_meijer(func): + """ + Given the Meijer G function parameters, ``func``, find a sequence of + operators that reduces order as much as possible. + + Return newfunc, [operators]. + + Examples + ======== + + >>> from sympy.simplify.hyperexpand import (reduce_order_meijer, + ... G_Function) + >>> reduce_order_meijer(G_Function([3, 4], [5, 6], [3, 4], [1, 2]))[0] + G_Function((4, 3), (5, 6), (3, 4), (2, 1)) + >>> reduce_order_meijer(G_Function([3, 4], [5, 6], [3, 4], [1, 8]))[0] + G_Function((3,), (5, 6), (3, 4), (1,)) + >>> reduce_order_meijer(G_Function([3, 4], [5, 6], [7, 5], [1, 5]))[0] + G_Function((3,), (), (), (1,)) + >>> reduce_order_meijer(G_Function([3, 4], [5, 6], [7, 5], [5, 3]))[0] + G_Function((), (), (), ()) + """ + + nan, nbq, ops1 = _reduce_order(func.an, func.bq, ReduceOrder.meijer_plus, + lambda x: default_sort_key(-x)) + nbm, nap, ops2 = _reduce_order(func.bm, func.ap, ReduceOrder.meijer_minus, + default_sort_key) + + return G_Function(nan, nap, nbm, nbq), ops1 + ops2 + + +def make_derivative_operator(M, z): + """ Create a derivative operator, to be passed to Operator.apply. """ + def doit(C): + r = z*C.diff(z) + C*M + r = r.applyfunc(make_simp(z)) + return r + return doit + + +def apply_operators(obj, ops, op): + """ + Apply the list of operators ``ops`` to object ``obj``, substituting + ``op`` for the generator. + """ + res = obj + for o in reversed(ops): + res = o.apply(res, op) + return res + + +def devise_plan(target, origin, z): + """ + Devise a plan (consisting of shift and un-shift operators) to be applied + to the hypergeometric function ``target`` to yield ``origin``. + Returns a list of operators. + + Examples + ======== + + >>> from sympy.simplify.hyperexpand import devise_plan, Hyper_Function + >>> from sympy.abc import z + + Nothing to do: + + >>> devise_plan(Hyper_Function((1, 2), ()), Hyper_Function((1, 2), ()), z) + [] + >>> devise_plan(Hyper_Function((), (1, 2)), Hyper_Function((), (1, 2)), z) + [] + + Very simple plans: + + >>> devise_plan(Hyper_Function((2,), ()), Hyper_Function((1,), ()), z) + [] + >>> devise_plan(Hyper_Function((), (2,)), Hyper_Function((), (1,)), z) + [] + + Several buckets: + + >>> from sympy import S + >>> devise_plan(Hyper_Function((1, S.Half), ()), + ... Hyper_Function((2, S('3/2')), ()), z) #doctest: +NORMALIZE_WHITESPACE + [, + ] + + A slightly more complicated plan: + + >>> devise_plan(Hyper_Function((1, 3), ()), Hyper_Function((2, 2), ()), z) + [, ] + + Another more complicated plan: (note that the ap have to be shifted first!) + + >>> devise_plan(Hyper_Function((1, -1), (2,)), Hyper_Function((3, -2), (4,)), z) + [, , + , + , ] + """ + abuckets, bbuckets, nabuckets, nbbuckets = [sift(params, _mod1) for + params in (target.ap, target.bq, origin.ap, origin.bq)] + + if len(list(abuckets.keys())) != len(list(nabuckets.keys())) or \ + len(list(bbuckets.keys())) != len(list(nbbuckets.keys())): + raise ValueError('%s not reachable from %s' % (target, origin)) + + ops = [] + + def do_shifts(fro, to, inc, dec): + ops = [] + for i in range(len(fro)): + if to[i] - fro[i] > 0: + sh = inc + ch = 1 + else: + sh = dec + ch = -1 + + while to[i] != fro[i]: + ops += [sh(fro, i)] + fro[i] += ch + + return ops + + def do_shifts_a(nal, nbk, al, aother, bother): + """ Shift us from (nal, nbk) to (al, nbk). """ + return do_shifts(nal, al, lambda p, i: ShiftA(p[i]), + lambda p, i: UnShiftA(p + aother, nbk + bother, i, z)) + + def do_shifts_b(nal, nbk, bk, aother, bother): + """ Shift us from (nal, nbk) to (nal, bk). """ + return do_shifts(nbk, bk, + lambda p, i: UnShiftB(nal + aother, p + bother, i, z), + lambda p, i: ShiftB(p[i])) + + for r in sorted(list(abuckets.keys()) + list(bbuckets.keys()), key=default_sort_key): + al = () + nal = () + bk = () + nbk = () + if r in abuckets: + al = abuckets[r] + nal = nabuckets[r] + if r in bbuckets: + bk = bbuckets[r] + nbk = nbbuckets[r] + if len(al) != len(nal) or len(bk) != len(nbk): + raise ValueError('%s not reachable from %s' % (target, origin)) + + al, nal, bk, nbk = [sorted(w, key=default_sort_key) + for w in [al, nal, bk, nbk]] + + def others(dic, key): + l = [] + for k in dic: + if k != key: + l.extend(dic[k]) + return l + aother = others(nabuckets, r) + bother = others(nbbuckets, r) + + if len(al) == 0: + # there can be no complications, just shift the bs as we please + ops += do_shifts_b([], nbk, bk, aother, bother) + elif len(bk) == 0: + # there can be no complications, just shift the as as we please + ops += do_shifts_a(nal, [], al, aother, bother) + else: + namax = nal[-1] + amax = al[-1] + + if nbk[0] - namax <= 0 or bk[0] - amax <= 0: + raise ValueError('Non-suitable parameters.') + + if namax - amax > 0: + # we are going to shift down - first do the as, then the bs + ops += do_shifts_a(nal, nbk, al, aother, bother) + ops += do_shifts_b(al, nbk, bk, aother, bother) + else: + # we are going to shift up - first do the bs, then the as + ops += do_shifts_b(nal, nbk, bk, aother, bother) + ops += do_shifts_a(nal, bk, al, aother, bother) + + nabuckets[r] = al + nbbuckets[r] = bk + + ops.reverse() + return ops + + +def try_shifted_sum(func, z): + """ Try to recognise a hypergeometric sum that starts from k > 0. """ + abuckets, bbuckets = sift(func.ap, _mod1), sift(func.bq, _mod1) + if len(abuckets[S.Zero]) != 1: + return None + r = abuckets[S.Zero][0] + if r <= 0: + return None + if S.Zero not in bbuckets: + return None + l = list(bbuckets[S.Zero]) + l.sort() + k = l[0] + if k <= 0: + return None + + nap = list(func.ap) + nap.remove(r) + nbq = list(func.bq) + nbq.remove(k) + k -= 1 + nap = [x - k for x in nap] + nbq = [x - k for x in nbq] + + ops = [] + for n in range(r - 1): + ops.append(ShiftA(n + 1)) + ops.reverse() + + fac = factorial(k)/z**k + fac *= Mul(*[rf(b, k) for b in nbq]) + fac /= Mul(*[rf(a, k) for a in nap]) + + ops += [MultOperator(fac)] + + p = 0 + for n in range(k): + m = z**n/factorial(n) + m *= Mul(*[rf(a, n) for a in nap]) + m /= Mul(*[rf(b, n) for b in nbq]) + p += m + + return Hyper_Function(nap, nbq), ops, -p + + +def try_polynomial(func, z): + """ Recognise polynomial cases. Returns None if not such a case. + Requires order to be fully reduced. """ + abuckets, bbuckets = sift(func.ap, _mod1), sift(func.bq, _mod1) + a0 = abuckets[S.Zero] + b0 = bbuckets[S.Zero] + a0.sort() + b0.sort() + al0 = [x for x in a0 if x <= 0] + bl0 = [x for x in b0 if x <= 0] + + if bl0 and all(a < bl0[-1] for a in al0): + return oo + if not al0: + return None + + a = al0[-1] + fac = 1 + res = S.One + for n in Tuple(*list(range(-a))): + fac *= z + fac /= n + 1 + fac *= Mul(*[a + n for a in func.ap]) + fac /= Mul(*[b + n for b in func.bq]) + res += fac + return res + + +def try_lerchphi(func): + """ + Try to find an expression for Hyper_Function ``func`` in terms of Lerch + Transcendents. + + Return None if no such expression can be found. + """ + # This is actually quite simple, and is described in Roach's paper, + # section 18. + # We don't need to implement the reduction to polylog here, this + # is handled by expand_func. + + # First we need to figure out if the summation coefficient is a rational + # function of the summation index, and construct that rational function. + abuckets, bbuckets = sift(func.ap, _mod1), sift(func.bq, _mod1) + + paired = {} + for key, value in abuckets.items(): + if key != 0 and key not in bbuckets: + return None + bvalue = bbuckets[key] + paired[key] = (list(value), list(bvalue)) + bbuckets.pop(key, None) + if bbuckets != {}: + return None + if S.Zero not in abuckets: + return None + aints, bints = paired[S.Zero] + # Account for the additional n! in denominator + paired[S.Zero] = (aints, bints + [1]) + + t = Dummy('t') + numer = S.One + denom = S.One + for key, (avalue, bvalue) in paired.items(): + if len(avalue) != len(bvalue): + return None + # Note that since order has been reduced fully, all the b are + # bigger than all the a they differ from by an integer. In particular + # if there are any negative b left, this function is not well-defined. + for a, b in zip(avalue, bvalue): + if (a - b).is_positive: + k = a - b + numer *= rf(b + t, k) + denom *= rf(b, k) + else: + k = b - a + numer *= rf(a, k) + denom *= rf(a + t, k) + + # Now do a partial fraction decomposition. + # We assemble two structures: a list monomials of pairs (a, b) representing + # a*t**b (b a non-negative integer), and a dict terms, where + # terms[a] = [(b, c)] means that there is a term b/(t-a)**c. + part = apart(numer/denom, t) + args = Add.make_args(part) + monomials = [] + terms = {} + for arg in args: + numer, denom = arg.as_numer_denom() + if not denom.has(t): + p = Poly(numer, t) + if not p.is_monomial: + raise TypeError("p should be monomial") + ((b, ), a) = p.LT() + monomials += [(a/denom, b)] + continue + if numer.has(t): + raise NotImplementedError('Need partial fraction decomposition' + ' with linear denominators') + indep, [dep] = denom.as_coeff_mul(t) + n = 1 + if dep.is_Pow: + n = dep.exp + dep = dep.base + if dep == t: + a = 0 + elif dep.is_Add: + a, tmp = dep.as_independent(t) + b = 1 + if tmp != t: + b, _ = tmp.as_independent(t) + if dep != b*t + a: + raise NotImplementedError('unrecognised form %s' % dep) + a /= b + indep *= b**n + else: + raise NotImplementedError('unrecognised form of partial fraction') + terms.setdefault(a, []).append((numer/indep, n)) + + # Now that we have this information, assemble our formula. All the + # monomials yield rational functions and go into one basis element. + # The terms[a] are related by differentiation. If the largest exponent is + # n, we need lerchphi(z, k, a) for k = 1, 2, ..., n. + # deriv maps a basis to its derivative, expressed as a C(z)-linear + # combination of other basis elements. + deriv = {} + coeffs = {} + z = Dummy('z') + monomials.sort(key=lambda x: x[1]) + mon = {0: 1/(1 - z)} + if monomials: + for k in range(monomials[-1][1]): + mon[k + 1] = z*mon[k].diff(z) + for a, n in monomials: + coeffs.setdefault(S.One, []).append(a*mon[n]) + for a, l in terms.items(): + for c, k in l: + coeffs.setdefault(lerchphi(z, k, a), []).append(c) + l.sort(key=lambda x: x[1]) + for k in range(2, l[-1][1] + 1): + deriv[lerchphi(z, k, a)] = [(-a, lerchphi(z, k, a)), + (1, lerchphi(z, k - 1, a))] + deriv[lerchphi(z, 1, a)] = [(-a, lerchphi(z, 1, a)), + (1/(1 - z), S.One)] + trans = {} + for n, b in enumerate([S.One] + list(deriv.keys())): + trans[b] = n + basis = [expand_func(b) for (b, _) in sorted(trans.items(), + key=lambda x:x[1])] + B = Matrix(basis) + C = Matrix([[0]*len(B)]) + for b, c in coeffs.items(): + C[trans[b]] = Add(*c) + M = zeros(len(B)) + for b, l in deriv.items(): + for c, b2 in l: + M[trans[b], trans[b2]] = c + return Formula(func, z, None, [], B, C, M) + + +def build_hypergeometric_formula(func): + """ + Create a formula object representing the hypergeometric function ``func``. + + """ + # We know that no `ap` are negative integers, otherwise "detect poly" + # would have kicked in. However, `ap` could be empty. In this case we can + # use a different basis. + # I'm not aware of a basis that works in all cases. + z = Dummy('z') + if func.ap: + afactors = [_x + a for a in func.ap] + bfactors = [_x + b - 1 for b in func.bq] + expr = _x*Mul(*bfactors) - z*Mul(*afactors) + poly = Poly(expr, _x) + n = poly.degree() + basis = [] + M = zeros(n) + for k in range(n): + a = func.ap[0] + k + basis += [hyper([a] + list(func.ap[1:]), func.bq, z)] + if k < n - 1: + M[k, k] = -a + M[k, k + 1] = a + B = Matrix(basis) + C = Matrix([[1] + [0]*(n - 1)]) + derivs = [eye(n)] + for k in range(n): + derivs.append(M*derivs[k]) + l = poly.all_coeffs() + l.reverse() + res = [0]*n + for k, c in enumerate(l): + for r, d in enumerate(C*derivs[k]): + res[r] += c*d + for k, c in enumerate(res): + M[n - 1, k] = -c/derivs[n - 1][0, n - 1]/poly.all_coeffs()[0] + return Formula(func, z, None, [], B, C, M) + else: + # Since there are no `ap`, none of the `bq` can be non-positive + # integers. + basis = [] + bq = list(func.bq[:]) + for i in range(len(bq)): + basis += [hyper([], bq, z)] + bq[i] += 1 + basis += [hyper([], bq, z)] + B = Matrix(basis) + n = len(B) + C = Matrix([[1] + [0]*(n - 1)]) + M = zeros(n) + M[0, n - 1] = z/Mul(*func.bq) + for k in range(1, n): + M[k, k - 1] = func.bq[k - 1] + M[k, k] = -func.bq[k - 1] + return Formula(func, z, None, [], B, C, M) + + +def hyperexpand_special(ap, bq, z): + """ + Try to find a closed-form expression for hyper(ap, bq, z), where ``z`` + is supposed to be a "special" value, e.g. 1. + + This function tries various of the classical summation formulae + (Gauss, Saalschuetz, etc). + """ + # This code is very ad-hoc. There are many clever algorithms + # (notably Zeilberger's) related to this problem. + # For now we just want a few simple cases to work. + p, q = len(ap), len(bq) + z_ = z + z = unpolarify(z) + if z == 0: + return S.One + from sympy.simplify.simplify import simplify + if p == 2 and q == 1: + # 2F1 + a, b, c = ap + bq + if z == 1: + # Gauss + return gamma(c - a - b)*gamma(c)/gamma(c - a)/gamma(c - b) + if z == -1 and simplify(b - a + c) == 1: + b, a = a, b + if z == -1 and simplify(a - b + c) == 1: + # Kummer + if b.is_integer and b.is_negative: + return 2*cos(pi*b/2)*gamma(-b)*gamma(b - a + 1) \ + /gamma(-b/2)/gamma(b/2 - a + 1) + else: + return gamma(b/2 + 1)*gamma(b - a + 1) \ + /gamma(b + 1)/gamma(b/2 - a + 1) + # TODO tons of more formulae + # investigate what algorithms exist + return hyper(ap, bq, z_) + +_collection = None + + +def _hyperexpand(func, z, ops0=[], z0=Dummy('z0'), premult=1, prem=0, + rewrite='default'): + """ + Try to find an expression for the hypergeometric function ``func``. + + Explanation + =========== + + The result is expressed in terms of a dummy variable ``z0``. Then it + is multiplied by ``premult``. Then ``ops0`` is applied. + ``premult`` must be a*z**prem for some a independent of ``z``. + """ + + if z.is_zero: + return S.One + + from sympy.simplify.simplify import simplify + + z = polarify(z, subs=False) + if rewrite == 'default': + rewrite = 'nonrepsmall' + + def carryout_plan(f, ops): + C = apply_operators(f.C.subs(f.z, z0), ops, + make_derivative_operator(f.M.subs(f.z, z0), z0)) + C = apply_operators(C, ops0, + make_derivative_operator(f.M.subs(f.z, z0) + + prem*eye(f.M.shape[0]), z0)) + + if premult == 1: + C = C.applyfunc(make_simp(z0)) + r = reduce(lambda s,m: s+m[0]*m[1], zip(C, f.B.subs(f.z, z0)), S.Zero)*premult + res = r.subs(z0, z) + if rewrite: + res = res.rewrite(rewrite) + return res + + # TODO + # The following would be possible: + # *) PFD Duplication (see Kelly Roach's paper) + # *) In a similar spirit, try_lerchphi() can be generalised considerably. + + global _collection + if _collection is None: + _collection = FormulaCollection() + + debug('Trying to expand hypergeometric function ', func) + + # First reduce order as much as possible. + func, ops = reduce_order(func) + if ops: + debug(' Reduced order to ', func) + else: + debug(' Could not reduce order.') + + # Now try polynomial cases + res = try_polynomial(func, z0) + if res is not None: + debug(' Recognised polynomial.') + p = apply_operators(res, ops, lambda f: z0*f.diff(z0)) + p = apply_operators(p*premult, ops0, lambda f: z0*f.diff(z0)) + return unpolarify(simplify(p).subs(z0, z)) + + # Try to recognise a shifted sum. + p = S.Zero + res = try_shifted_sum(func, z0) + if res is not None: + func, nops, p = res + debug(' Recognised shifted sum, reduced order to ', func) + ops += nops + + # apply the plan for poly + p = apply_operators(p, ops, lambda f: z0*f.diff(z0)) + p = apply_operators(p*premult, ops0, lambda f: z0*f.diff(z0)) + p = simplify(p).subs(z0, z) + + # Try special expansions early. + if unpolarify(z) in [1, -1] and (len(func.ap), len(func.bq)) == (2, 1): + f = build_hypergeometric_formula(func) + r = carryout_plan(f, ops).replace(hyper, hyperexpand_special) + if not r.has(hyper): + return r + p + + # Try to find a formula in our collection + formula = _collection.lookup_origin(func) + + # Now try a lerch phi formula + if formula is None: + formula = try_lerchphi(func) + + if formula is None: + debug(' Could not find an origin. ', + 'Will return answer in terms of ' + 'simpler hypergeometric functions.') + formula = build_hypergeometric_formula(func) + + debug(' Found an origin: ', formula.closed_form, ' ', formula.func) + + # We need to find the operators that convert formula into func. + ops += devise_plan(func, formula.func, z0) + + # Now carry out the plan. + r = carryout_plan(formula, ops) + p + + return powdenest(r, polar=True).replace(hyper, hyperexpand_special) + + +def devise_plan_meijer(fro, to, z): + """ + Find operators to convert G-function ``fro`` into G-function ``to``. + + Explanation + =========== + + It is assumed that ``fro`` and ``to`` have the same signatures, and that in fact + any corresponding pair of parameters differs by integers, and a direct path + is possible. I.e. if there are parameters a1 b1 c1 and a2 b2 c2 it is + assumed that a1 can be shifted to a2, etc. The only thing this routine + determines is the order of shifts to apply, nothing clever will be tried. + It is also assumed that ``fro`` is suitable. + + Examples + ======== + + >>> from sympy.simplify.hyperexpand import (devise_plan_meijer, + ... G_Function) + >>> from sympy.abc import z + + Empty plan: + + >>> devise_plan_meijer(G_Function([1], [2], [3], [4]), + ... G_Function([1], [2], [3], [4]), z) + [] + + Very simple plans: + + >>> devise_plan_meijer(G_Function([0], [], [], []), + ... G_Function([1], [], [], []), z) + [] + >>> devise_plan_meijer(G_Function([0], [], [], []), + ... G_Function([-1], [], [], []), z) + [] + >>> devise_plan_meijer(G_Function([], [1], [], []), + ... G_Function([], [2], [], []), z) + [] + + Slightly more complicated plans: + + >>> devise_plan_meijer(G_Function([0], [], [], []), + ... G_Function([2], [], [], []), z) + [, + ] + >>> devise_plan_meijer(G_Function([0], [], [0], []), + ... G_Function([-1], [], [1], []), z) + [, ] + + Order matters: + + >>> devise_plan_meijer(G_Function([0], [], [0], []), + ... G_Function([1], [], [1], []), z) + [, ] + """ + # TODO for now, we use the following simple heuristic: inverse-shift + # when possible, shift otherwise. Give up if we cannot make progress. + + def try_shift(f, t, shifter, diff, counter): + """ Try to apply ``shifter`` in order to bring some element in ``f`` + nearer to its counterpart in ``to``. ``diff`` is +/- 1 and + determines the effect of ``shifter``. Counter is a list of elements + blocking the shift. + + Return an operator if change was possible, else None. + """ + for idx, (a, b) in enumerate(zip(f, t)): + if ( + (a - b).is_integer and (b - a)/diff > 0 and + all(a != x for x in counter)): + sh = shifter(idx) + f[idx] += diff + return sh + fan = list(fro.an) + fap = list(fro.ap) + fbm = list(fro.bm) + fbq = list(fro.bq) + ops = [] + change = True + while change: + change = False + op = try_shift(fan, to.an, + lambda i: MeijerUnShiftB(fan, fap, fbm, fbq, i, z), + 1, fbm + fbq) + if op is not None: + ops += [op] + change = True + continue + op = try_shift(fap, to.ap, + lambda i: MeijerUnShiftD(fan, fap, fbm, fbq, i, z), + 1, fbm + fbq) + if op is not None: + ops += [op] + change = True + continue + op = try_shift(fbm, to.bm, + lambda i: MeijerUnShiftA(fan, fap, fbm, fbq, i, z), + -1, fan + fap) + if op is not None: + ops += [op] + change = True + continue + op = try_shift(fbq, to.bq, + lambda i: MeijerUnShiftC(fan, fap, fbm, fbq, i, z), + -1, fan + fap) + if op is not None: + ops += [op] + change = True + continue + op = try_shift(fan, to.an, lambda i: MeijerShiftB(fan[i]), -1, []) + if op is not None: + ops += [op] + change = True + continue + op = try_shift(fap, to.ap, lambda i: MeijerShiftD(fap[i]), -1, []) + if op is not None: + ops += [op] + change = True + continue + op = try_shift(fbm, to.bm, lambda i: MeijerShiftA(fbm[i]), 1, []) + if op is not None: + ops += [op] + change = True + continue + op = try_shift(fbq, to.bq, lambda i: MeijerShiftC(fbq[i]), 1, []) + if op is not None: + ops += [op] + change = True + continue + if fan != list(to.an) or fap != list(to.ap) or fbm != list(to.bm) or \ + fbq != list(to.bq): + raise NotImplementedError('Could not devise plan.') + ops.reverse() + return ops + +_meijercollection = None + + +def _meijergexpand(func, z0, allow_hyper=False, rewrite='default', + place=None): + """ + Try to find an expression for the Meijer G function specified + by the G_Function ``func``. If ``allow_hyper`` is True, then returning + an expression in terms of hypergeometric functions is allowed. + + Currently this just does Slater's theorem. + If expansions exist both at zero and at infinity, ``place`` + can be set to ``0`` or ``zoo`` for the preferred choice. + """ + global _meijercollection + if _meijercollection is None: + _meijercollection = MeijerFormulaCollection() + if rewrite == 'default': + rewrite = None + + func0 = func + debug('Try to expand Meijer G function corresponding to ', func) + + # We will play games with analytic continuation - rather use a fresh symbol + z = Dummy('z') + + func, ops = reduce_order_meijer(func) + if ops: + debug(' Reduced order to ', func) + else: + debug(' Could not reduce order.') + + # Try to find a direct formula + f = _meijercollection.lookup_origin(func) + if f is not None: + debug(' Found a Meijer G formula: ', f.func) + ops += devise_plan_meijer(f.func, func, z) + + # Now carry out the plan. + C = apply_operators(f.C.subs(f.z, z), ops, + make_derivative_operator(f.M.subs(f.z, z), z)) + + C = C.applyfunc(make_simp(z)) + r = C*f.B.subs(f.z, z) + r = r[0].subs(z, z0) + return powdenest(r, polar=True) + + debug(" Could not find a direct formula. Trying Slater's theorem.") + + # TODO the following would be possible: + # *) Paired Index Theorems + # *) PFD Duplication + # (See Kelly Roach's paper for details on either.) + # + # TODO Also, we tend to create combinations of gamma functions that can be + # simplified. + + def can_do(pbm, pap): + """ Test if slater applies. """ + for i in pbm: + if len(pbm[i]) > 1: + l = 0 + if i in pap: + l = len(pap[i]) + if l + 1 < len(pbm[i]): + return False + return True + + def do_slater(an, bm, ap, bq, z, zfinal): + # zfinal is the value that will eventually be substituted for z. + # We pass it to _hyperexpand to improve performance. + func = G_Function(an, bm, ap, bq) + _, pbm, pap, _ = func.compute_buckets() + if not can_do(pbm, pap): + return S.Zero, False + + cond = len(an) + len(ap) < len(bm) + len(bq) + if len(an) + len(ap) == len(bm) + len(bq): + cond = abs(z) < 1 + if cond is False: + return S.Zero, False + + res = S.Zero + for m in pbm: + if len(pbm[m]) == 1: + bh = pbm[m][0] + fac = 1 + bo = list(bm) + bo.remove(bh) + for bj in bo: + fac *= gamma(bj - bh) + for aj in an: + fac *= gamma(1 + bh - aj) + for bj in bq: + fac /= gamma(1 + bh - bj) + for aj in ap: + fac /= gamma(aj - bh) + nap = [1 + bh - a for a in list(an) + list(ap)] + nbq = [1 + bh - b for b in list(bo) + list(bq)] + + k = polar_lift(S.NegativeOne**(len(ap) - len(bm))) + harg = k*zfinal + # NOTE even though k "is" +-1, this has to be t/k instead of + # t*k ... we are using polar numbers for consistency! + premult = (t/k)**bh + hyp = _hyperexpand(Hyper_Function(nap, nbq), harg, ops, + t, premult, bh, rewrite=None) + res += fac * hyp + else: + b_ = pbm[m][0] + ki = [bi - b_ for bi in pbm[m][1:]] + u = len(ki) + li = [ai - b_ for ai in pap[m][:u + 1]] + bo = list(bm) + for b in pbm[m]: + bo.remove(b) + ao = list(ap) + for a in pap[m][:u]: + ao.remove(a) + lu = li[-1] + di = [l - k for (l, k) in zip(li, ki)] + + # We first work out the integrand: + s = Dummy('s') + integrand = z**s + for b in bm: + if not Mod(b, 1) and b.is_Number: + b = int(round(b)) + integrand *= gamma(b - s) + for a in an: + integrand *= gamma(1 - a + s) + for b in bq: + integrand /= gamma(1 - b + s) + for a in ap: + integrand /= gamma(a - s) + + # Now sum the finitely many residues: + # XXX This speeds up some cases - is it a good idea? + integrand = expand_func(integrand) + for r in range(int(round(lu))): + resid = residue(integrand, s, b_ + r) + resid = apply_operators(resid, ops, lambda f: z*f.diff(z)) + res -= resid + + # Now the hypergeometric term. + au = b_ + lu + k = polar_lift(S.NegativeOne**(len(ao) + len(bo) + 1)) + harg = k*zfinal + premult = (t/k)**au + nap = [1 + au - a for a in list(an) + list(ap)] + [1] + nbq = [1 + au - b for b in list(bm) + list(bq)] + + hyp = _hyperexpand(Hyper_Function(nap, nbq), harg, ops, + t, premult, au, rewrite=None) + + C = S.NegativeOne**(lu)/factorial(lu) + for i in range(u): + C *= S.NegativeOne**di[i]/rf(lu - li[i] + 1, di[i]) + for a in an: + C *= gamma(1 - a + au) + for b in bo: + C *= gamma(b - au) + for a in ao: + C /= gamma(a - au) + for b in bq: + C /= gamma(1 - b + au) + + res += C*hyp + + return res, cond + + t = Dummy('t') + slater1, cond1 = do_slater(func.an, func.bm, func.ap, func.bq, z, z0) + + def tr(l): + return [1 - x for x in l] + + for op in ops: + op._poly = Poly(op._poly.subs({z: 1/t, _x: -_x}), _x) + slater2, cond2 = do_slater(tr(func.bm), tr(func.an), tr(func.bq), tr(func.ap), + t, 1/z0) + + slater1 = powdenest(slater1.subs(z, z0), polar=True) + slater2 = powdenest(slater2.subs(t, 1/z0), polar=True) + if not isinstance(cond2, bool): + cond2 = cond2.subs(t, 1/z) + + m = func(z) + if m.delta > 0 or \ + (m.delta == 0 and len(m.ap) == len(m.bq) and + (re(m.nu) < -1) is not False and polar_lift(z0) == polar_lift(1)): + # The condition delta > 0 means that the convergence region is + # connected. Any expression we find can be continued analytically + # to the entire convergence region. + # The conditions delta==0, p==q, re(nu) < -1 imply that G is continuous + # on the positive reals, so the values at z=1 agree. + if cond1 is not False: + cond1 = True + if cond2 is not False: + cond2 = True + + if cond1 is True: + slater1 = slater1.rewrite(rewrite or 'nonrep') + else: + slater1 = slater1.rewrite(rewrite or 'nonrepsmall') + if cond2 is True: + slater2 = slater2.rewrite(rewrite or 'nonrep') + else: + slater2 = slater2.rewrite(rewrite or 'nonrepsmall') + + if cond1 is not False and cond2 is not False: + # If one condition is False, there is no choice. + if place == 0: + cond2 = False + if place == zoo: + cond1 = False + + if not isinstance(cond1, bool): + cond1 = cond1.subs(z, z0) + if not isinstance(cond2, bool): + cond2 = cond2.subs(z, z0) + + def weight(expr, cond): + if cond is True: + c0 = 0 + elif cond is False: + c0 = 1 + else: + c0 = 2 + if expr.has(oo, zoo, -oo, nan): + # XXX this actually should not happen, but consider + # S('meijerg(((0, -1/2, 0, -1/2, 1/2), ()), ((0,), + # (-1/2, -1/2, -1/2, -1)), exp_polar(I*pi))/4') + c0 = 3 + return (c0, expr.count(hyper), expr.count_ops()) + + w1 = weight(slater1, cond1) + w2 = weight(slater2, cond2) + if min(w1, w2) <= (0, 1, oo): + if w1 < w2: + return slater1 + else: + return slater2 + if max(w1[0], w2[0]) <= 1 and max(w1[1], w2[1]) <= 1: + return Piecewise((slater1, cond1), (slater2, cond2), (func0(z0), True)) + + # We couldn't find an expression without hypergeometric functions. + # TODO it would be helpful to give conditions under which the integral + # is known to diverge. + r = Piecewise((slater1, cond1), (slater2, cond2), (func0(z0), True)) + if r.has(hyper) and not allow_hyper: + debug(' Could express using hypergeometric functions, ' + 'but not allowed.') + if not r.has(hyper) or allow_hyper: + return r + + return func0(z0) + + +def hyperexpand(f, allow_hyper=False, rewrite='default', place=None): + """ + Expand hypergeometric functions. If allow_hyper is True, allow partial + simplification (that is a result different from input, + but still containing hypergeometric functions). + + If a G-function has expansions both at zero and at infinity, + ``place`` can be set to ``0`` or ``zoo`` to indicate the + preferred choice. + + Examples + ======== + + >>> from sympy.simplify.hyperexpand import hyperexpand + >>> from sympy.functions import hyper + >>> from sympy.abc import z + >>> hyperexpand(hyper([], [], z)) + exp(z) + + Non-hyperegeometric parts of the expression and hypergeometric expressions + that are not recognised are left unchanged: + + >>> hyperexpand(1 + hyper([1, 1, 1], [], z)) + hyper((1, 1, 1), (), z) + 1 + """ + f = sympify(f) + + def do_replace(ap, bq, z): + r = _hyperexpand(Hyper_Function(ap, bq), z, rewrite=rewrite) + if r is None: + return hyper(ap, bq, z) + else: + return r + + def do_meijer(ap, bq, z): + r = _meijergexpand(G_Function(ap[0], ap[1], bq[0], bq[1]), z, + allow_hyper, rewrite=rewrite, place=place) + if not r.has(nan, zoo, oo, -oo): + return r + return f.replace(hyper, do_replace).replace(meijerg, do_meijer) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/hyperexpand_doc.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/hyperexpand_doc.py new file mode 100644 index 0000000000000000000000000000000000000000..a18377f3aede5214036fbf628825536611001584 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/hyperexpand_doc.py @@ -0,0 +1,18 @@ +""" This module cooks up a docstring when imported. Its only purpose is to + be displayed in the sphinx documentation. """ + +from sympy.core.relational import Eq +from sympy.functions.special.hyper import hyper +from sympy.printing.latex import latex +from sympy.simplify.hyperexpand import FormulaCollection + +c = FormulaCollection() + +doc = "" + +for f in c.formulae: + obj = Eq(hyper(f.func.ap, f.func.bq, f.z), + f.closed_form.rewrite('nonrepsmall')) + doc += ".. math::\n %s\n" % latex(obj) + +__doc__ = doc diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/powsimp.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/powsimp.py new file mode 100644 index 0000000000000000000000000000000000000000..f72dfeb072e0d0d4737ace310eda5c2a3a082c16 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/powsimp.py @@ -0,0 +1,718 @@ +from collections import defaultdict +from functools import reduce +from math import prod + +from sympy.core.function import expand_log, count_ops, _coeff_isneg +from sympy.core import sympify, Basic, Dummy, S, Add, Mul, Pow, expand_mul, factor_terms +from sympy.core.sorting import ordered, default_sort_key +from sympy.core.numbers import Integer, Rational, equal_valued +from sympy.core.mul import _keep_coeff +from sympy.core.rules import Transform +from sympy.functions import exp_polar, exp, log, root, polarify, unpolarify +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.polys import lcm, gcd +from sympy.ntheory.factor_ import multiplicity + + + +def powsimp(expr, deep=False, combine='all', force=False, measure=count_ops): + """ + Reduce expression by combining powers with similar bases and exponents. + + Explanation + =========== + + If ``deep`` is ``True`` then powsimp() will also simplify arguments of + functions. By default ``deep`` is set to ``False``. + + If ``force`` is ``True`` then bases will be combined without checking for + assumptions, e.g. sqrt(x)*sqrt(y) -> sqrt(x*y) which is not true + if x and y are both negative. + + You can make powsimp() only combine bases or only combine exponents by + changing combine='base' or combine='exp'. By default, combine='all', + which does both. combine='base' will only combine:: + + a a a 2x x + x * y => (x*y) as well as things like 2 => 4 + + and combine='exp' will only combine + :: + + a b (a + b) + x * x => x + + combine='exp' will strictly only combine exponents in the way that used + to be automatic. Also use deep=True if you need the old behavior. + + When combine='all', 'exp' is evaluated first. Consider the first + example below for when there could be an ambiguity relating to this. + This is done so things like the second example can be completely + combined. If you want 'base' combined first, do something like + powsimp(powsimp(expr, combine='base'), combine='exp'). + + Examples + ======== + + >>> from sympy import powsimp, exp, log, symbols + >>> from sympy.abc import x, y, z, n + >>> powsimp(x**y*x**z*y**z, combine='all') + x**(y + z)*y**z + >>> powsimp(x**y*x**z*y**z, combine='exp') + x**(y + z)*y**z + >>> powsimp(x**y*x**z*y**z, combine='base', force=True) + x**y*(x*y)**z + + >>> powsimp(x**z*x**y*n**z*n**y, combine='all', force=True) + (n*x)**(y + z) + >>> powsimp(x**z*x**y*n**z*n**y, combine='exp') + n**(y + z)*x**(y + z) + >>> powsimp(x**z*x**y*n**z*n**y, combine='base', force=True) + (n*x)**y*(n*x)**z + + >>> x, y = symbols('x y', positive=True) + >>> powsimp(log(exp(x)*exp(y))) + log(exp(x)*exp(y)) + >>> powsimp(log(exp(x)*exp(y)), deep=True) + x + y + + Radicals with Mul bases will be combined if combine='exp' + + >>> from sympy import sqrt + >>> x, y = symbols('x y') + + Two radicals are automatically joined through Mul: + + >>> a=sqrt(x*sqrt(y)) + >>> a*a**3 == a**4 + True + + But if an integer power of that radical has been + autoexpanded then Mul does not join the resulting factors: + + >>> a**4 # auto expands to a Mul, no longer a Pow + x**2*y + >>> _*a # so Mul doesn't combine them + x**2*y*sqrt(x*sqrt(y)) + >>> powsimp(_) # but powsimp will + (x*sqrt(y))**(5/2) + >>> powsimp(x*y*a) # but won't when doing so would violate assumptions + x*y*sqrt(x*sqrt(y)) + + """ + def recurse(arg, **kwargs): + _deep = kwargs.get('deep', deep) + _combine = kwargs.get('combine', combine) + _force = kwargs.get('force', force) + _measure = kwargs.get('measure', measure) + return powsimp(arg, _deep, _combine, _force, _measure) + + expr = sympify(expr) + + if (not isinstance(expr, Basic) or isinstance(expr, MatrixSymbol) or ( + expr.is_Atom or expr in (exp_polar(0), exp_polar(1)))): + return expr + + if deep or expr.is_Add or expr.is_Mul and _y not in expr.args: + expr = expr.func(*[recurse(w) for w in expr.args]) + + if expr.is_Pow: + return recurse(expr*_y, deep=False)/_y + + if not expr.is_Mul: + return expr + + # handle the Mul + if combine in ('exp', 'all'): + # Collect base/exp data, while maintaining order in the + # non-commutative parts of the product + c_powers = defaultdict(list) + nc_part = [] + newexpr = [] + coeff = S.One + for term in expr.args: + if term.is_Rational: + coeff *= term + continue + if term.is_Pow: + term = _denest_pow(term) + if term.is_commutative: + b, e = term.as_base_exp() + if deep: + b, e = [recurse(i) for i in [b, e]] + if b.is_Pow or isinstance(b, exp): + # don't let smthg like sqrt(x**a) split into x**a, 1/2 + # or else it will be joined as x**(a/2) later + b, e = b**e, S.One + c_powers[b].append(e) + else: + # This is the logic that combines exponents for equal, + # but non-commutative bases: A**x*A**y == A**(x+y). + if nc_part: + b1, e1 = nc_part[-1].as_base_exp() + b2, e2 = term.as_base_exp() + if (b1 == b2 and + e1.is_commutative and e2.is_commutative): + nc_part[-1] = Pow(b1, Add(e1, e2)) + continue + nc_part.append(term) + + # add up exponents of common bases + for b, e in ordered(iter(c_powers.items())): + # allow 2**x/4 -> 2**(x - 2); don't do this when b and e are + # Numbers since autoevaluation will undo it, e.g. + # 2**(1/3)/4 -> 2**(1/3 - 2) -> 2**(1/3)/4 + if (b and b.is_Rational and not all(ei.is_Number for ei in e) and \ + coeff is not S.One and + b not in (S.One, S.NegativeOne)): + m = multiplicity(abs(b), abs(coeff)) + if m: + e.append(m) + coeff /= b**m + c_powers[b] = Add(*e) + if coeff is not S.One: + if coeff in c_powers: + c_powers[coeff] += S.One + else: + c_powers[coeff] = S.One + + # convert to plain dictionary + c_powers = dict(c_powers) + + # check for base and inverted base pairs + be = list(c_powers.items()) + skip = set() # skip if we already saw them + for b, e in be: + if b in skip: + continue + bpos = b.is_positive or b.is_polar + if bpos: + binv = 1/b + #Special case for float 1 + if b.is_Float and equal_valued(b, 1): + c_powers[b] = S.One + continue + if b != binv and binv in c_powers: + if b.as_numer_denom()[0] is S.One: + c_powers.pop(b) + c_powers[binv] -= e + else: + skip.add(binv) + e = c_powers.pop(binv) + c_powers[b] -= e + + # check for base and negated base pairs + be = list(c_powers.items()) + _n = S.NegativeOne + for b, e in be: + if (b.is_Symbol or b.is_Add) and -b in c_powers and b in c_powers: + if (b.is_positive is not None or e.is_integer): + if e.is_integer or b.is_negative: + c_powers[-b] += c_powers.pop(b) + else: # (-b).is_positive so use its e + e = c_powers.pop(-b) + c_powers[b] += e + if _n in c_powers: + c_powers[_n] += e + else: + c_powers[_n] = e + + # filter c_powers and convert to a list + c_powers = [(b, e) for b, e in c_powers.items() if e] + + # ============================================================== + # check for Mul bases of Rational powers that can be combined with + # separated bases, e.g. x*sqrt(x*y)*sqrt(x*sqrt(x*y)) -> + # (x*sqrt(x*y))**(3/2) + # ---------------- helper functions + + def ratq(x): + '''Return Rational part of x's exponent as it appears in the bkey. + ''' + return bkey(x)[0][1] + + def bkey(b, e=None): + '''Return (b**s, c.q), c.p where e -> c*s. If e is not given then + it will be taken by using as_base_exp() on the input b. + e.g. + x**3/2 -> (x, 2), 3 + x**y -> (x**y, 1), 1 + x**(2*y/3) -> (x**y, 3), 2 + exp(x/2) -> (exp(a), 2), 1 + + ''' + if e is not None: # coming from c_powers or from below + if e.is_Integer: + return (b, S.One), e + elif e.is_Rational: + return (b, Integer(e.q)), Integer(e.p) + else: + c, m = e.as_coeff_Mul(rational=True) + if c is not S.One: + if m.is_integer: + return (b, Integer(c.q)), m*Integer(c.p) + return (b**m, Integer(c.q)), Integer(c.p) + else: + return (b**e, S.One), S.One + else: + return bkey(*b.as_base_exp()) + + def update(b): + '''Decide what to do with base, b. If its exponent is now an + integer multiple of the Rational denominator, then remove it + and put the factors of its base in the common_b dictionary or + update the existing bases if necessary. If it has been zeroed + out, simply remove the base. + ''' + newe, r = divmod(common_b[b], b[1]) + if not r: + common_b.pop(b) + if newe: + for m in Mul.make_args(b[0]**newe): + b, e = bkey(m) + if b not in common_b: + common_b[b] = 0 + common_b[b] += e + if b[1] != 1: + bases.append(b) + # ---------------- end of helper functions + + # assemble a dictionary of the factors having a Rational power + common_b = {} + done = [] + bases = [] + for b, e in c_powers: + b, e = bkey(b, e) + if b in common_b: + common_b[b] = common_b[b] + e + else: + common_b[b] = e + if b[1] != 1 and b[0].is_Mul: + bases.append(b) + bases.sort(key=default_sort_key) # this makes tie-breaking canonical + bases.sort(key=measure, reverse=True) # handle longest first + for base in bases: + if base not in common_b: # it may have been removed already + continue + b, exponent = base + last = False # True when no factor of base is a radical + qlcm = 1 # the lcm of the radical denominators + while True: + bstart = b + qstart = qlcm + + bb = [] # list of factors + ee = [] # (factor's expo. and it's current value in common_b) + for bi in Mul.make_args(b): + bib, bie = bkey(bi) + if bib not in common_b or common_b[bib] < bie: + ee = bb = [] # failed + break + ee.append([bie, common_b[bib]]) + bb.append(bib) + if ee: + # find the number of integral extractions possible + # e.g. [(1, 2), (2, 2)] -> min(2/1, 2/2) -> 1 + min1 = ee[0][1]//ee[0][0] + for i in range(1, len(ee)): + rat = ee[i][1]//ee[i][0] + if rat < 1: + break + min1 = min(min1, rat) + else: + # update base factor counts + # e.g. if ee = [(2, 5), (3, 6)] then min1 = 2 + # and the new base counts will be 5-2*2 and 6-2*3 + for i in range(len(bb)): + common_b[bb[i]] -= min1*ee[i][0] + update(bb[i]) + # update the count of the base + # e.g. x**2*y*sqrt(x*sqrt(y)) the count of x*sqrt(y) + # will increase by 4 to give bkey (x*sqrt(y), 2, 5) + common_b[base] += min1*qstart*exponent + if (last # no more radicals in base + or len(common_b) == 1 # nothing left to join with + or all(k[1] == 1 for k in common_b) # no rad's in common_b + ): + break + # see what we can exponentiate base by to remove any radicals + # so we know what to search for + # e.g. if base were x**(1/2)*y**(1/3) then we should + # exponentiate by 6 and look for powers of x and y in the ratio + # of 2 to 3 + qlcm = lcm([ratq(bi) for bi in Mul.make_args(bstart)]) + if qlcm == 1: + break # we are done + b = bstart**qlcm + qlcm *= qstart + if all(ratq(bi) == 1 for bi in Mul.make_args(b)): + last = True # we are going to be done after this next pass + # this base no longer can find anything to join with and + # since it was longer than any other we are done with it + b, q = base + done.append((b, common_b.pop(base)*Rational(1, q))) + + # update c_powers and get ready to continue with powsimp + c_powers = done + # there may be terms still in common_b that were bases that were + # identified as needing processing, so remove those, too + for (b, q), e in common_b.items(): + if (b.is_Pow or isinstance(b, exp)) and \ + q is not S.One and not b.exp.is_Rational: + b, be = b.as_base_exp() + b = b**(be/q) + else: + b = root(b, q) + c_powers.append((b, e)) + check = len(c_powers) + c_powers = dict(c_powers) + assert len(c_powers) == check # there should have been no duplicates + # ============================================================== + + # rebuild the expression + newexpr = expr.func(*(newexpr + [Pow(b, e) for b, e in c_powers.items()])) + if combine == 'exp': + return expr.func(newexpr, expr.func(*nc_part)) + else: + return recurse(expr.func(*nc_part), combine='base') * \ + recurse(newexpr, combine='base') + + elif combine == 'base': + + # Build c_powers and nc_part. These must both be lists not + # dicts because exp's are not combined. + c_powers = [] + nc_part = [] + for term in expr.args: + if term.is_commutative: + c_powers.append(list(term.as_base_exp())) + else: + nc_part.append(term) + + # Pull out numerical coefficients from exponent if assumptions allow + # e.g., 2**(2*x) => 4**x + for i in range(len(c_powers)): + b, e = c_powers[i] + if not (all(x.is_nonnegative for x in b.as_numer_denom()) or e.is_integer or force or b.is_polar): + continue + exp_c, exp_t = e.as_coeff_Mul(rational=True) + if exp_c is not S.One and exp_t is not S.One: + c_powers[i] = [Pow(b, exp_c), exp_t] + + # Combine bases whenever they have the same exponent and + # assumptions allow + # first gather the potential bases under the common exponent + c_exp = defaultdict(list) + for b, e in c_powers: + if deep: + e = recurse(e) + if e.is_Add and (b.is_positive or e.is_integer): + e = factor_terms(e) + if _coeff_isneg(e): + e = -e + b = 1/b + c_exp[e].append(b) + del c_powers + + # Merge back in the results of the above to form a new product + c_powers = defaultdict(list) + for e in c_exp: + bases = c_exp[e] + + # calculate the new base for e + + if len(bases) == 1: + new_base = bases[0] + elif e.is_integer or force: + new_base = expr.func(*bases) + else: + # see which ones can be joined + unk = [] + nonneg = [] + neg = [] + for bi in bases: + if bi.is_negative: + neg.append(bi) + elif bi.is_nonnegative: + nonneg.append(bi) + elif bi.is_polar: + nonneg.append( + bi) # polar can be treated like non-negative + else: + unk.append(bi) + if len(unk) == 1 and not neg or len(neg) == 1 and not unk: + # a single neg or a single unk can join the rest + nonneg.extend(unk + neg) + unk = neg = [] + elif neg: + # their negative signs cancel in groups of 2*q if we know + # that e = p/q else we have to treat them as unknown + israt = False + if e.is_Rational: + israt = True + else: + p, d = e.as_numer_denom() + if p.is_integer and d.is_integer: + israt = True + if israt: + neg = [-w for w in neg] + unk.extend([S.NegativeOne]*len(neg)) + else: + unk.extend(neg) + neg = [] + del israt + + # these shouldn't be joined + for b in unk: + c_powers[b].append(e) + # here is a new joined base + new_base = expr.func(*(nonneg + neg)) + # if there are positive parts they will just get separated + # again unless some change is made + + def _terms(e): + # return the number of terms of this expression + # when multiplied out -- assuming no joining of terms + if e.is_Add: + return sum(_terms(ai) for ai in e.args) + if e.is_Mul: + return prod([_terms(mi) for mi in e.args]) + return 1 + xnew_base = expand_mul(new_base, deep=False) + if len(Add.make_args(xnew_base)) < _terms(new_base): + new_base = factor_terms(xnew_base) + + c_powers[new_base].append(e) + + # break out the powers from c_powers now + c_part = [Pow(b, ei) for b, e in c_powers.items() for ei in e] + + # we're done + return expr.func(*(c_part + nc_part)) + + else: + raise ValueError("combine must be one of ('all', 'exp', 'base').") + + +def powdenest(eq, force=False, polar=False): + r""" + Collect exponents on powers as assumptions allow. + + Explanation + =========== + + Given ``(bb**be)**e``, this can be simplified as follows: + * if ``bb`` is positive, or + * ``e`` is an integer, or + * ``|be| < 1`` then this simplifies to ``bb**(be*e)`` + + Given a product of powers raised to a power, ``(bb1**be1 * + bb2**be2...)**e``, simplification can be done as follows: + + - if e is positive, the gcd of all bei can be joined with e; + - all non-negative bb can be separated from those that are negative + and their gcd can be joined with e; autosimplification already + handles this separation. + - integer factors from powers that have integers in the denominator + of the exponent can be removed from any term and the gcd of such + integers can be joined with e + + Setting ``force`` to ``True`` will make symbols that are not explicitly + negative behave as though they are positive, resulting in more + denesting. + + Setting ``polar`` to ``True`` will do simplifications on the Riemann surface of + the logarithm, also resulting in more denestings. + + When there are sums of logs in exp() then a product of powers may be + obtained e.g. ``exp(3*(log(a) + 2*log(b)))`` - > ``a**3*b**6``. + + Examples + ======== + + >>> from sympy.abc import a, b, x, y, z + >>> from sympy import Symbol, exp, log, sqrt, symbols, powdenest + + >>> powdenest((x**(2*a/3))**(3*x)) + (x**(2*a/3))**(3*x) + >>> powdenest(exp(3*x*log(2))) + 2**(3*x) + + Assumptions may prevent expansion: + + >>> powdenest(sqrt(x**2)) + sqrt(x**2) + + >>> p = symbols('p', positive=True) + >>> powdenest(sqrt(p**2)) + p + + No other expansion is done. + + >>> i, j = symbols('i,j', integer=True) + >>> powdenest((x**x)**(i + j)) # -X-> (x**x)**i*(x**x)**j + x**(x*(i + j)) + + But exp() will be denested by moving all non-log terms outside of + the function; this may result in the collapsing of the exp to a power + with a different base: + + >>> powdenest(exp(3*y*log(x))) + x**(3*y) + >>> powdenest(exp(y*(log(a) + log(b)))) + (a*b)**y + >>> powdenest(exp(3*(log(a) + log(b)))) + a**3*b**3 + + If assumptions allow, symbols can also be moved to the outermost exponent: + + >>> i = Symbol('i', integer=True) + >>> powdenest(((x**(2*i))**(3*y))**x) + ((x**(2*i))**(3*y))**x + >>> powdenest(((x**(2*i))**(3*y))**x, force=True) + x**(6*i*x*y) + + >>> powdenest(((x**(2*a/3))**(3*y/i))**x) + ((x**(2*a/3))**(3*y/i))**x + >>> powdenest((x**(2*i)*y**(4*i))**z, force=True) + (x*y**2)**(2*i*z) + + >>> n = Symbol('n', negative=True) + + >>> powdenest((x**i)**y, force=True) + x**(i*y) + >>> powdenest((n**i)**x, force=True) + (n**i)**x + + """ + from sympy.simplify.simplify import posify + + if force: + def _denest(b, e): + if not isinstance(b, (Pow, exp)): + return b.is_positive, Pow(b, e, evaluate=False) + return _denest(b.base, b.exp*e) + reps = [] + for p in eq.atoms(Pow, exp): + if isinstance(p.base, (Pow, exp)): + ok, dp = _denest(*p.args) + if ok is not False: + reps.append((p, dp)) + if reps: + eq = eq.subs(reps) + eq, reps = posify(eq) + return powdenest(eq, force=False, polar=polar).xreplace(reps) + + if polar: + eq, rep = polarify(eq) + return unpolarify(powdenest(unpolarify(eq, exponents_only=True)), rep) + + new = powsimp(eq) + return new.xreplace(Transform( + _denest_pow, filter=lambda m: m.is_Pow or isinstance(m, exp))) + +_y = Dummy('y') + + +def _denest_pow(eq): + """ + Denest powers. + + This is a helper function for powdenest that performs the actual + transformation. + """ + from sympy.simplify.simplify import logcombine + + b, e = eq.as_base_exp() + if b.is_Pow or isinstance(b, exp) and e != 1: + new = b._eval_power(e) + if new is not None: + eq = new + b, e = new.as_base_exp() + + # denest exp with log terms in exponent + if b is S.Exp1 and e.is_Mul: + logs = [] + other = [] + for ei in e.args: + if any(isinstance(ai, log) for ai in Add.make_args(ei)): + logs.append(ei) + else: + other.append(ei) + logs = logcombine(Mul(*logs)) + return Pow(exp(logs), Mul(*other)) + + _, be = b.as_base_exp() + if be is S.One and not (b.is_Mul or + b.is_Rational and b.q != 1 or + b.is_positive): + return eq + + # denest eq which is either pos**e or Pow**e or Mul**e or + # Mul(b1**e1, b2**e2) + + # handle polar numbers specially + polars, nonpolars = [], [] + for bb in Mul.make_args(b): + if bb.is_polar: + polars.append(bb.as_base_exp()) + else: + nonpolars.append(bb) + if len(polars) == 1 and not polars[0][0].is_Mul: + return Pow(polars[0][0], polars[0][1]*e)*powdenest(Mul(*nonpolars)**e) + elif polars: + return Mul(*[powdenest(bb**(ee*e)) for (bb, ee) in polars]) \ + *powdenest(Mul(*nonpolars)**e) + + if b.is_Integer: + # use log to see if there is a power here + logb = expand_log(log(b)) + if logb.is_Mul: + c, logb = logb.args + e *= c + base = logb.args[0] + return Pow(base, e) + + # if b is not a Mul or any factor is an atom then there is nothing to do + if not b.is_Mul or any(s.is_Atom for s in Mul.make_args(b)): + return eq + + # let log handle the case of the base of the argument being a Mul, e.g. + # sqrt(x**(2*i)*y**(6*i)) -> x**i*y**(3**i) if x and y are positive; we + # will take the log, expand it, and then factor out the common powers that + # now appear as coefficient. We do this manually since terms_gcd pulls out + # fractions, terms_gcd(x+x*y/2) -> x*(y + 2)/2 and we don't want the 1/2; + # gcd won't pull out numerators from a fraction: gcd(3*x, 9*x/2) -> x but + # we want 3*x. Neither work with noncommutatives. + + def nc_gcd(aa, bb): + a, b = [i.as_coeff_Mul() for i in [aa, bb]] + c = gcd(a[0], b[0]).as_numer_denom()[0] + g = Mul(*(a[1].args_cnc(cset=True)[0] & b[1].args_cnc(cset=True)[0])) + return _keep_coeff(c, g) + + glogb = expand_log(log(b)) + if glogb.is_Add: + args = glogb.args + g = reduce(nc_gcd, args) + if g != 1: + cg, rg = g.as_coeff_Mul() + glogb = _keep_coeff(cg, rg*Add(*[a/g for a in args])) + + # now put the log back together again + if isinstance(glogb, log) or not glogb.is_Mul: + if glogb.args[0].is_Pow or isinstance(glogb.args[0], exp): + glogb = _denest_pow(glogb.args[0]) + if (abs(glogb.exp) < 1) == True: + return Pow(glogb.base, glogb.exp*e) + return eq + + # the log(b) was a Mul so join any adds with logcombine + add = [] + other = [] + for a in glogb.args: + if a.is_Add: + add.append(a) + else: + other.append(a) + return Pow(exp(logcombine(Mul(*add))), e*Mul(*other)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/radsimp.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/radsimp.py new file mode 100644 index 0000000000000000000000000000000000000000..c878168ebfbc29fc632577d6325befc120c26f56 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/radsimp.py @@ -0,0 +1,1234 @@ +from collections import defaultdict + +from sympy.core import sympify, S, Mul, Derivative, Pow +from sympy.core.add import _unevaluated_Add, Add +from sympy.core.assumptions import assumptions +from sympy.core.exprtools import Factors, gcd_terms +from sympy.core.function import _mexpand, expand_mul, expand_power_base +from sympy.core.mul import _keep_coeff, _unevaluated_Mul, _mulsort +from sympy.core.numbers import Rational, zoo, nan +from sympy.core.parameters import global_parameters +from sympy.core.sorting import ordered, default_sort_key +from sympy.core.symbol import Dummy, Wild, symbols +from sympy.functions import exp, sqrt, log +from sympy.functions.elementary.complexes import Abs +from sympy.polys import gcd +from sympy.simplify.sqrtdenest import sqrtdenest +from sympy.utilities.iterables import iterable, sift + + + + +def collect(expr, syms, func=None, evaluate=None, exact=False, distribute_order_term=True): + """ + Collect additive terms of an expression. + + Explanation + =========== + + This function collects additive terms of an expression with respect + to a list of expression up to powers with rational exponents. By the + term symbol here are meant arbitrary expressions, which can contain + powers, products, sums etc. In other words symbol is a pattern which + will be searched for in the expression's terms. + + The input expression is not expanded by :func:`collect`, so user is + expected to provide an expression in an appropriate form. This makes + :func:`collect` more predictable as there is no magic happening behind the + scenes. However, it is important to note, that powers of products are + converted to products of powers using the :func:`~.expand_power_base` + function. + + There are two possible types of output. First, if ``evaluate`` flag is + set, this function will return an expression with collected terms or + else it will return a dictionary with expressions up to rational powers + as keys and collected coefficients as values. + + Examples + ======== + + >>> from sympy import S, collect, expand, factor, Wild + >>> from sympy.abc import a, b, c, x, y + + This function can collect symbolic coefficients in polynomials or + rational expressions. It will manage to find all integer or rational + powers of collection variable:: + + >>> collect(a*x**2 + b*x**2 + a*x - b*x + c, x) + c + x**2*(a + b) + x*(a - b) + + The same result can be achieved in dictionary form:: + + >>> d = collect(a*x**2 + b*x**2 + a*x - b*x + c, x, evaluate=False) + >>> d[x**2] + a + b + >>> d[x] + a - b + >>> d[S.One] + c + + You can also work with multivariate polynomials. However, remember that + this function is greedy so it will care only about a single symbol at time, + in specification order:: + + >>> collect(x**2 + y*x**2 + x*y + y + a*y, [x, y]) + x**2*(y + 1) + x*y + y*(a + 1) + + Also more complicated expressions can be used as patterns:: + + >>> from sympy import sin, log + >>> collect(a*sin(2*x) + b*sin(2*x), sin(2*x)) + (a + b)*sin(2*x) + + >>> collect(a*x*log(x) + b*(x*log(x)), x*log(x)) + x*(a + b)*log(x) + + You can use wildcards in the pattern:: + + >>> w = Wild('w1') + >>> collect(a*x**y - b*x**y, w**y) + x**y*(a - b) + + It is also possible to work with symbolic powers, although it has more + complicated behavior, because in this case power's base and symbolic part + of the exponent are treated as a single symbol:: + + >>> collect(a*x**c + b*x**c, x) + a*x**c + b*x**c + >>> collect(a*x**c + b*x**c, x**c) + x**c*(a + b) + + However if you incorporate rationals to the exponents, then you will get + well known behavior:: + + >>> collect(a*x**(2*c) + b*x**(2*c), x**c) + x**(2*c)*(a + b) + + Note also that all previously stated facts about :func:`collect` function + apply to the exponential function, so you can get:: + + >>> from sympy import exp + >>> collect(a*exp(2*x) + b*exp(2*x), exp(x)) + (a + b)*exp(2*x) + + If you are interested only in collecting specific powers of some symbols + then set ``exact`` flag to True:: + + >>> collect(a*x**7 + b*x**7, x, exact=True) + a*x**7 + b*x**7 + >>> collect(a*x**7 + b*x**7, x**7, exact=True) + x**7*(a + b) + + If you want to collect on any object containing symbols, set + ``exact`` to None: + + >>> collect(x*exp(x) + sin(x)*y + sin(x)*2 + 3*x, x, exact=None) + x*exp(x) + 3*x + (y + 2)*sin(x) + >>> collect(a*x*y + x*y + b*x + x, [x, y], exact=None) + x*y*(a + 1) + x*(b + 1) + + You can also apply this function to differential equations, where + derivatives of arbitrary order can be collected. Note that if you + collect with respect to a function or a derivative of a function, all + derivatives of that function will also be collected. Use + ``exact=True`` to prevent this from happening:: + + >>> from sympy import Derivative as D, collect, Function + >>> f = Function('f') (x) + + >>> collect(a*D(f,x) + b*D(f,x), D(f,x)) + (a + b)*Derivative(f(x), x) + + >>> collect(a*D(D(f,x),x) + b*D(D(f,x),x), f) + (a + b)*Derivative(f(x), (x, 2)) + + >>> collect(a*D(D(f,x),x) + b*D(D(f,x),x), D(f,x), exact=True) + a*Derivative(f(x), (x, 2)) + b*Derivative(f(x), (x, 2)) + + >>> collect(a*D(f,x) + b*D(f,x) + a*f + b*f, f) + (a + b)*f(x) + (a + b)*Derivative(f(x), x) + + Or you can even match both derivative order and exponent at the same time:: + + >>> collect(a*D(D(f,x),x)**2 + b*D(D(f,x),x)**2, D(f,x)) + (a + b)*Derivative(f(x), (x, 2))**2 + + Finally, you can apply a function to each of the collected coefficients. + For example you can factorize symbolic coefficients of polynomial:: + + >>> f = expand((x + a + 1)**3) + + >>> collect(f, x, factor) + x**3 + 3*x**2*(a + 1) + 3*x*(a + 1)**2 + (a + 1)**3 + + .. note:: Arguments are expected to be in expanded form, so you might have + to call :func:`~.expand` prior to calling this function. + + See Also + ======== + + collect_const, collect_sqrt, rcollect + """ + expr = sympify(expr) + syms = [sympify(i) for i in (syms if iterable(syms) else [syms])] + + # replace syms[i] if it is not x, -x or has Wild symbols + cond = lambda x: x.is_Symbol or (-x).is_Symbol or bool( + x.atoms(Wild)) + _, nonsyms = sift(syms, cond, binary=True) + if nonsyms: + reps = dict(zip(nonsyms, [Dummy(**assumptions(i)) for i in nonsyms])) + syms = [reps.get(s, s) for s in syms] + rv = collect(expr.subs(reps), syms, + func=func, evaluate=evaluate, exact=exact, + distribute_order_term=distribute_order_term) + urep = {v: k for k, v in reps.items()} + if not isinstance(rv, dict): + return rv.xreplace(urep) + else: + return {urep.get(k, k).xreplace(urep): v.xreplace(urep) + for k, v in rv.items()} + + # see if other expressions should be considered + if exact is None: + _syms = set() + for i in Add.make_args(expr): + if not i.has_free(*syms) or i in syms: + continue + if not i.is_Mul and i not in syms: + _syms.add(i) + else: + # identify compound generators + g = i._new_rawargs(*i.as_coeff_mul(*syms)[1]) + if g not in syms: + _syms.add(g) + simple = all(i.is_Pow and i.base in syms for i in _syms) + syms = syms + list(ordered(_syms)) + if not simple: + return collect(expr, syms, + func=func, evaluate=evaluate, exact=False, + distribute_order_term=distribute_order_term) + + if evaluate is None: + evaluate = global_parameters.evaluate + + def make_expression(terms): + product = [] + + for term, rat, sym, deriv in terms: + if deriv is not None: + var, order = deriv + for _ in range(order): + term = Derivative(term, var) + + if sym is None: + if rat is S.One: + product.append(term) + else: + product.append(Pow(term, rat)) + else: + product.append(Pow(term, rat*sym)) + + return Mul(*product) + + def parse_derivative(deriv): + # scan derivatives tower in the input expression and return + # underlying function and maximal differentiation order + expr, sym, order = deriv.expr, deriv.variables[0], 1 + + for s in deriv.variables[1:]: + if s == sym: + order += 1 + else: + raise NotImplementedError( + 'Improve MV Derivative support in collect') + + while isinstance(expr, Derivative): + s0 = expr.variables[0] + + if any(s != s0 for s in expr.variables): + raise NotImplementedError( + 'Improve MV Derivative support in collect') + + if s0 == sym: + expr, order = expr.expr, order + len(expr.variables) + else: + break + + return expr, (sym, Rational(order)) + + def parse_term(expr): + """Parses expression expr and outputs tuple (sexpr, rat_expo, + sym_expo, deriv) + where: + - sexpr is the base expression + - rat_expo is the rational exponent that sexpr is raised to + - sym_expo is the symbolic exponent that sexpr is raised to + - deriv contains the derivatives of the expression + + For example, the output of x would be (x, 1, None, None) + the output of 2**x would be (2, 1, x, None). + """ + rat_expo, sym_expo = S.One, None + sexpr, deriv = expr, None + + if expr.is_Pow: + if isinstance(expr.base, Derivative): + sexpr, deriv = parse_derivative(expr.base) + else: + sexpr = expr.base + + if expr.base == S.Exp1: + arg = expr.exp + if arg.is_Rational: + sexpr, rat_expo = S.Exp1, arg + elif arg.is_Mul: + coeff, tail = arg.as_coeff_Mul(rational=True) + sexpr, rat_expo = exp(tail), coeff + + elif expr.exp.is_Number: + rat_expo = expr.exp + else: + coeff, tail = expr.exp.as_coeff_Mul() + + if coeff.is_Number: + rat_expo, sym_expo = coeff, tail + else: + sym_expo = expr.exp + elif isinstance(expr, exp): + arg = expr.exp + if arg.is_Rational: + sexpr, rat_expo = S.Exp1, arg + elif arg.is_Mul: + coeff, tail = arg.as_coeff_Mul(rational=True) + sexpr, rat_expo = exp(tail), coeff + elif isinstance(expr, Derivative): + sexpr, deriv = parse_derivative(expr) + + return sexpr, rat_expo, sym_expo, deriv + + def parse_expression(terms, pattern): + """Parse terms searching for a pattern. + Terms is a list of tuples as returned by parse_terms; + Pattern is an expression treated as a product of factors. + """ + pattern = Mul.make_args(pattern) + + if len(terms) < len(pattern): + # pattern is longer than matched product + # so no chance for positive parsing result + return None + else: + pattern = [parse_term(elem) for elem in pattern] + + terms = terms[:] # need a copy + elems, common_expo, has_deriv = [], None, False + + for elem, e_rat, e_sym, e_ord in pattern: + + if elem.is_Number and e_rat == 1 and e_sym is None: + # a constant is a match for everything + continue + + for j in range(len(terms)): + if terms[j] is None: + continue + + term, t_rat, t_sym, t_ord = terms[j] + + # keeping track of whether one of the terms had + # a derivative or not as this will require rebuilding + # the expression later + if t_ord is not None: + has_deriv = True + + if (term.match(elem) is not None and + (t_sym == e_sym or t_sym is not None and + e_sym is not None and + t_sym.match(e_sym) is not None)): + if exact is False: + # we don't have to be exact so find common exponent + # for both expression's term and pattern's element + expo = t_rat / e_rat + + if common_expo is None: + # first time + common_expo = expo + else: + # common exponent was negotiated before so + # there is no chance for a pattern match unless + # common and current exponents are equal + if common_expo != expo: + common_expo = 1 + else: + # we ought to be exact so all fields of + # interest must match in every details + if e_rat != t_rat or e_ord != t_ord: + continue + + # found common term so remove it from the expression + # and try to match next element in the pattern + elems.append(terms[j]) + terms[j] = None + + break + + else: + # pattern element not found + return None + + return [_f for _f in terms if _f], elems, common_expo, has_deriv + + if evaluate: + if expr.is_Add: + o = expr.getO() or 0 + expr = expr.func(*[ + collect(a, syms, func, True, exact, distribute_order_term) + for a in expr.args if a != o]) + o + elif expr.is_Mul: + return expr.func(*[ + collect(term, syms, func, True, exact, distribute_order_term) + for term in expr.args]) + elif expr.is_Pow: + b = collect( + expr.base, syms, func, True, exact, distribute_order_term) + return Pow(b, expr.exp) + + syms = [expand_power_base(i, deep=False) for i in syms] + + order_term = None + + if distribute_order_term: + order_term = expr.getO() + + if order_term is not None: + if order_term.has(*syms): + order_term = None + else: + expr = expr.removeO() + + summa = [expand_power_base(i, deep=False) for i in Add.make_args(expr)] + + collected, disliked = defaultdict(list), S.Zero + for product in summa: + c, nc = product.args_cnc(split_1=False) + args = list(ordered(c)) + nc + terms = [parse_term(i) for i in args] + small_first = True + + for symbol in syms: + if isinstance(symbol, Derivative) and small_first: + terms = list(reversed(terms)) + small_first = not small_first + result = parse_expression(terms, symbol) + + if result is not None: + if not symbol.is_commutative: + raise AttributeError("Can not collect noncommutative symbol") + + terms, elems, common_expo, has_deriv = result + + # when there was derivative in current pattern we + # will need to rebuild its expression from scratch + if not has_deriv: + margs = [] + for elem in elems: + if elem[2] is None: + e = elem[1] + else: + e = elem[1]*elem[2] + margs.append(Pow(elem[0], e)) + index = Mul(*margs) + else: + index = make_expression(elems) + terms = expand_power_base(make_expression(terms), deep=False) + index = expand_power_base(index, deep=False) + collected[index].append(terms) + break + else: + # none of the patterns matched + disliked += product + # add terms now for each key + collected = {k: Add(*v) for k, v in collected.items()} + + if disliked is not S.Zero: + collected[S.One] = disliked + + if order_term is not None: + for key, val in collected.items(): + collected[key] = val + order_term + + if func is not None: + collected = { + key: func(val) for key, val in collected.items()} + + if evaluate: + return Add(*[key*val for key, val in collected.items()]) + else: + return collected + + +def rcollect(expr, *vars): + """ + Recursively collect sums in an expression. + + Examples + ======== + + >>> from sympy.simplify import rcollect + >>> from sympy.abc import x, y + + >>> expr = (x**2*y + x*y + x + y)/(x + y) + + >>> rcollect(expr, y) + (x + y*(x**2 + x + 1))/(x + y) + + See Also + ======== + + collect, collect_const, collect_sqrt + """ + if expr.is_Atom or not expr.has(*vars): + return expr + else: + expr = expr.__class__(*[rcollect(arg, *vars) for arg in expr.args]) + + if expr.is_Add: + return collect(expr, vars) + else: + return expr + + +def collect_sqrt(expr, evaluate=None): + """Return expr with terms having common square roots collected together. + If ``evaluate`` is False a count indicating the number of sqrt-containing + terms will be returned and, if non-zero, the terms of the Add will be + returned, else the expression itself will be returned as a single term. + If ``evaluate`` is True, the expression with any collected terms will be + returned. + + Note: since I = sqrt(-1), it is collected, too. + + Examples + ======== + + >>> from sympy import sqrt + >>> from sympy.simplify.radsimp import collect_sqrt + >>> from sympy.abc import a, b + + >>> r2, r3, r5 = [sqrt(i) for i in [2, 3, 5]] + >>> collect_sqrt(a*r2 + b*r2) + sqrt(2)*(a + b) + >>> collect_sqrt(a*r2 + b*r2 + a*r3 + b*r3) + sqrt(2)*(a + b) + sqrt(3)*(a + b) + >>> collect_sqrt(a*r2 + b*r2 + a*r3 + b*r5) + sqrt(3)*a + sqrt(5)*b + sqrt(2)*(a + b) + + If evaluate is False then the arguments will be sorted and + returned as a list and a count of the number of sqrt-containing + terms will be returned: + + >>> collect_sqrt(a*r2 + b*r2 + a*r3 + b*r5, evaluate=False) + ((sqrt(3)*a, sqrt(5)*b, sqrt(2)*(a + b)), 3) + >>> collect_sqrt(a*sqrt(2) + b, evaluate=False) + ((b, sqrt(2)*a), 1) + >>> collect_sqrt(a + b, evaluate=False) + ((a + b,), 0) + + See Also + ======== + + collect, collect_const, rcollect + """ + if evaluate is None: + evaluate = global_parameters.evaluate + # this step will help to standardize any complex arguments + # of sqrts + coeff, expr = expr.as_content_primitive() + vars = set() + for a in Add.make_args(expr): + for m in a.args_cnc()[0]: + if m.is_number and ( + m.is_Pow and m.exp.is_Rational and m.exp.q == 2 or + m is S.ImaginaryUnit): + vars.add(m) + + # we only want radicals, so exclude Number handling; in this case + # d will be evaluated + d = collect_const(expr, *vars, Numbers=False) + hit = expr != d + + if not evaluate: + nrad = 0 + # make the evaluated args canonical + args = list(ordered(Add.make_args(d))) + for i, m in enumerate(args): + c, nc = m.args_cnc() + for ci in c: + # XXX should this be restricted to ci.is_number as above? + if ci.is_Pow and ci.exp.is_Rational and ci.exp.q == 2 or \ + ci is S.ImaginaryUnit: + nrad += 1 + break + args[i] *= coeff + if not (hit or nrad): + args = [Add(*args)] + return tuple(args), nrad + + return coeff*d + + +def collect_abs(expr): + """Return ``expr`` with arguments of multiple Abs in a term collected + under a single instance. + + Examples + ======== + + >>> from sympy.simplify.radsimp import collect_abs + >>> from sympy.abc import x + >>> collect_abs(abs(x + 1)/abs(x**2 - 1)) + Abs((x + 1)/(x**2 - 1)) + >>> collect_abs(abs(1/x)) + Abs(1/x) + """ + def _abs(mul): + c, nc = mul.args_cnc() + a = [] + o = [] + for i in c: + if isinstance(i, Abs): + a.append(i.args[0]) + elif isinstance(i, Pow) and isinstance(i.base, Abs) and i.exp.is_real: + a.append(i.base.args[0]**i.exp) + else: + o.append(i) + if len(a) < 2 and not any(i.exp.is_negative for i in a if isinstance(i, Pow)): + return mul + absarg = Mul(*a) + A = Abs(absarg) + args = [A] + args.extend(o) + if not A.has(Abs): + args.extend(nc) + return Mul(*args) + if not isinstance(A, Abs): + # reevaluate and make it unevaluated + A = Abs(absarg, evaluate=False) + args[0] = A + _mulsort(args) + args.extend(nc) # nc always go last + return Mul._from_args(args, is_commutative=not nc) + + return expr.replace( + lambda x: isinstance(x, Mul), + lambda x: _abs(x)).replace( + lambda x: isinstance(x, Pow), + lambda x: _abs(x)) + + +def collect_const(expr, *vars, Numbers=True): + """A non-greedy collection of terms with similar number coefficients in + an Add expr. If ``vars`` is given then only those constants will be + targeted. Although any Number can also be targeted, if this is not + desired set ``Numbers=False`` and no Float or Rational will be collected. + + Parameters + ========== + + expr : SymPy expression + This parameter defines the expression the expression from which + terms with similar coefficients are to be collected. A non-Add + expression is returned as it is. + + vars : variable length collection of Numbers, optional + Specifies the constants to target for collection. Can be multiple in + number. + + Numbers : bool + Specifies to target all instance of + :class:`sympy.core.numbers.Number` class. If ``Numbers=False``, then + no Float or Rational will be collected. + + Returns + ======= + + expr : Expr + Returns an expression with similar coefficient terms collected. + + Examples + ======== + + >>> from sympy import sqrt + >>> from sympy.abc import s, x, y, z + >>> from sympy.simplify.radsimp import collect_const + >>> collect_const(sqrt(3) + sqrt(3)*(1 + sqrt(2))) + sqrt(3)*(sqrt(2) + 2) + >>> collect_const(sqrt(3)*s + sqrt(7)*s + sqrt(3) + sqrt(7)) + (sqrt(3) + sqrt(7))*(s + 1) + >>> s = sqrt(2) + 2 + >>> collect_const(sqrt(3)*s + sqrt(3) + sqrt(7)*s + sqrt(7)) + (sqrt(2) + 3)*(sqrt(3) + sqrt(7)) + >>> collect_const(sqrt(3)*s + sqrt(3) + sqrt(7)*s + sqrt(7), sqrt(3)) + sqrt(7) + sqrt(3)*(sqrt(2) + 3) + sqrt(7)*(sqrt(2) + 2) + + The collection is sign-sensitive, giving higher precedence to the + unsigned values: + + >>> collect_const(x - y - z) + x - (y + z) + >>> collect_const(-y - z) + -(y + z) + >>> collect_const(2*x - 2*y - 2*z, 2) + 2*(x - y - z) + >>> collect_const(2*x - 2*y - 2*z, -2) + 2*x - 2*(y + z) + + See Also + ======== + + collect, collect_sqrt, rcollect + """ + if not expr.is_Add: + return expr + + recurse = False + + if not vars: + recurse = True + vars = set() + for a in expr.args: + for m in Mul.make_args(a): + if m.is_number: + vars.add(m) + else: + vars = sympify(vars) + if not Numbers: + vars = [v for v in vars if not v.is_Number] + + vars = list(ordered(vars)) + for v in vars: + terms = defaultdict(list) + Fv = Factors(v) + for m in Add.make_args(expr): + f = Factors(m) + q, r = f.div(Fv) + if r.is_one: + # only accept this as a true factor if + # it didn't change an exponent from an Integer + # to a non-Integer, e.g. 2/sqrt(2) -> sqrt(2) + # -- we aren't looking for this sort of change + fwas = f.factors.copy() + fnow = q.factors + if not any(k in fwas and fwas[k].is_Integer and not + fnow[k].is_Integer for k in fnow): + terms[v].append(q.as_expr()) + continue + terms[S.One].append(m) + + args = [] + hit = False + uneval = False + for k in ordered(terms): + v = terms[k] + if k is S.One: + args.extend(v) + continue + + if len(v) > 1: + v = Add(*v) + hit = True + if recurse and v != expr: + vars.append(v) + else: + v = v[0] + + # be careful not to let uneval become True unless + # it must be because it's going to be more expensive + # to rebuild the expression as an unevaluated one + if Numbers and k.is_Number and v.is_Add: + args.append(_keep_coeff(k, v, sign=True)) + uneval = True + else: + args.append(k*v) + + if hit: + if uneval: + expr = _unevaluated_Add(*args) + else: + expr = Add(*args) + if not expr.is_Add: + break + + return expr + + +def radsimp(expr, symbolic=True, max_terms=4): + r""" + Rationalize the denominator by removing square roots. + + Explanation + =========== + + The expression returned from radsimp must be used with caution + since if the denominator contains symbols, it will be possible to make + substitutions that violate the assumptions of the simplification process: + that for a denominator matching a + b*sqrt(c), a != +/-b*sqrt(c). (If + there are no symbols, this assumptions is made valid by collecting terms + of sqrt(c) so the match variable ``a`` does not contain ``sqrt(c)``.) If + you do not want the simplification to occur for symbolic denominators, set + ``symbolic`` to False. + + If there are more than ``max_terms`` radical terms then the expression is + returned unchanged. + + Examples + ======== + + >>> from sympy import radsimp, sqrt, Symbol, pprint + >>> from sympy import factor_terms, fraction, signsimp + >>> from sympy.simplify.radsimp import collect_sqrt + >>> from sympy.abc import a, b, c + + >>> radsimp(1/(2 + sqrt(2))) + (2 - sqrt(2))/2 + >>> x,y = map(Symbol, 'xy') + >>> e = ((2 + 2*sqrt(2))*x + (2 + sqrt(8))*y)/(2 + sqrt(2)) + >>> radsimp(e) + sqrt(2)*(x + y) + + No simplification beyond removal of the gcd is done. One might + want to polish the result a little, however, by collecting + square root terms: + + >>> r2 = sqrt(2) + >>> r5 = sqrt(5) + >>> ans = radsimp(1/(y*r2 + x*r2 + a*r5 + b*r5)); pprint(ans) + ___ ___ ___ ___ + \/ 5 *a + \/ 5 *b - \/ 2 *x - \/ 2 *y + ------------------------------------------ + 2 2 2 2 + 5*a + 10*a*b + 5*b - 2*x - 4*x*y - 2*y + + >>> n, d = fraction(ans) + >>> pprint(factor_terms(signsimp(collect_sqrt(n))/d, radical=True)) + ___ ___ + \/ 5 *(a + b) - \/ 2 *(x + y) + ------------------------------------------ + 2 2 2 2 + 5*a + 10*a*b + 5*b - 2*x - 4*x*y - 2*y + + If radicals in the denominator cannot be removed or there is no denominator, + the original expression will be returned. + + >>> radsimp(sqrt(2)*x + sqrt(2)) + sqrt(2)*x + sqrt(2) + + Results with symbols will not always be valid for all substitutions: + + >>> eq = 1/(a + b*sqrt(c)) + >>> eq.subs(a, b*sqrt(c)) + 1/(2*b*sqrt(c)) + >>> radsimp(eq).subs(a, b*sqrt(c)) + nan + + If ``symbolic=False``, symbolic denominators will not be transformed (but + numeric denominators will still be processed): + + >>> radsimp(eq, symbolic=False) + 1/(a + b*sqrt(c)) + + """ + from sympy.core.expr import Expr + from sympy.simplify.simplify import signsimp + + syms = symbols("a:d A:D") + def _num(rterms): + # return the multiplier that will simplify the expression described + # by rterms [(sqrt arg, coeff), ... ] + a, b, c, d, A, B, C, D = syms + if len(rterms) == 2: + reps = dict(list(zip([A, a, B, b], [j for i in rterms for j in i]))) + return ( + sqrt(A)*a - sqrt(B)*b).xreplace(reps) + if len(rterms) == 3: + reps = dict(list(zip([A, a, B, b, C, c], [j for i in rterms for j in i]))) + return ( + (sqrt(A)*a + sqrt(B)*b - sqrt(C)*c)*(2*sqrt(A)*sqrt(B)*a*b - A*a**2 - + B*b**2 + C*c**2)).xreplace(reps) + elif len(rterms) == 4: + reps = dict(list(zip([A, a, B, b, C, c, D, d], [j for i in rterms for j in i]))) + return ((sqrt(A)*a + sqrt(B)*b - sqrt(C)*c - sqrt(D)*d)*(2*sqrt(A)*sqrt(B)*a*b + - A*a**2 - B*b**2 - 2*sqrt(C)*sqrt(D)*c*d + C*c**2 + + D*d**2)*(-8*sqrt(A)*sqrt(B)*sqrt(C)*sqrt(D)*a*b*c*d + A**2*a**4 - + 2*A*B*a**2*b**2 - 2*A*C*a**2*c**2 - 2*A*D*a**2*d**2 + B**2*b**4 - + 2*B*C*b**2*c**2 - 2*B*D*b**2*d**2 + C**2*c**4 - 2*C*D*c**2*d**2 + + D**2*d**4)).xreplace(reps) + elif len(rterms) == 1: + return sqrt(rterms[0][0]) + else: + raise NotImplementedError + + def ispow2(d, log2=False): + if not d.is_Pow: + return False + e = d.exp + if e.is_Rational and e.q == 2 or symbolic and denom(e) == 2: + return True + if log2: + q = 1 + if e.is_Rational: + q = e.q + elif symbolic: + d = denom(e) + if d.is_Integer: + q = d + if q != 1 and log(q, 2).is_Integer: + return True + return False + + def handle(expr): + # Handle first reduces to the case + # expr = 1/d, where d is an add, or d is base**p/2. + # We do this by recursively calling handle on each piece. + from sympy.simplify.simplify import nsimplify + + if expr.is_Atom: + return expr + elif not isinstance(expr, Expr): + return expr.func(*[handle(a) for a in expr.args]) + + n, d = fraction(expr) + + if d.is_Atom and n.is_Atom: + return expr + elif not n.is_Atom: + n = n.func(*[handle(a) for a in n.args]) + return _unevaluated_Mul(n, handle(1/d)) + elif n is not S.One: + return _unevaluated_Mul(n, handle(1/d)) + elif d.is_Mul: + return _unevaluated_Mul(*[handle(1/d) for d in d.args]) + + # By this step, expr is 1/d, and d is not a mul. + if not symbolic and d.free_symbols: + return expr + + if ispow2(d): + d2 = sqrtdenest(sqrt(d.base))**numer(d.exp) + if d2 != d: + return handle(1/d2) + elif d.is_Pow and (d.exp.is_integer or d.base.is_positive): + # (1/d**i) = (1/d)**i + return handle(1/d.base)**d.exp + + if not (d.is_Add or ispow2(d)): + return 1/d.func(*[handle(a) for a in d.args]) + + # handle 1/d treating d as an Add (though it may not be) + + keep = True # keep changes that are made + + # flatten it and collect radicals after checking for special + # conditions + d = _mexpand(d) + + # did it change? + if d.is_Atom: + return 1/d + + # is it a number that might be handled easily? + if d.is_number: + _d = nsimplify(d) + if _d.is_Number and _d.equals(d): + return 1/_d + + while True: + # collect similar terms + collected = defaultdict(list) + for m in Add.make_args(d): # d might have become non-Add + p2 = [] + other = [] + for i in Mul.make_args(m): + if ispow2(i, log2=True): + p2.append(i.base if i.exp is S.Half else i.base**(2*i.exp)) + elif i is S.ImaginaryUnit: + p2.append(S.NegativeOne) + else: + other.append(i) + collected[tuple(ordered(p2))].append(Mul(*other)) + rterms = list(ordered(list(collected.items()))) + rterms = [(Mul(*i), Add(*j)) for i, j in rterms] + nrad = len(rterms) - (1 if rterms[0][0] is S.One else 0) + if nrad < 1: + break + elif nrad > max_terms: + # there may have been invalid operations leading to this point + # so don't keep changes, e.g. this expression is troublesome + # in collecting terms so as not to raise the issue of 2834: + # r = sqrt(sqrt(5) + 5) + # eq = 1/(sqrt(5)*r + 2*sqrt(5)*sqrt(-sqrt(5) + 5) + 5*r) + keep = False + break + if len(rterms) > 4: + # in general, only 4 terms can be removed with repeated squaring + # but other considerations can guide selection of radical terms + # so that radicals are removed + if all(x.is_Integer and (y**2).is_Rational for x, y in rterms): + nd, d = rad_rationalize(S.One, Add._from_args( + [sqrt(x)*y for x, y in rterms])) + n *= nd + else: + # is there anything else that might be attempted? + keep = False + break + from sympy.simplify.powsimp import powsimp, powdenest + + num = powsimp(_num(rterms)) + n *= num + d *= num + d = powdenest(_mexpand(d), force=symbolic) + if d.has(S.Zero, nan, zoo): + return expr + if d.is_Atom: + break + + if not keep: + return expr + return _unevaluated_Mul(n, 1/d) + + if not isinstance(expr, Expr): + return expr.func(*[radsimp(a, symbolic=symbolic, max_terms=max_terms) for a in expr.args]) + + coeff, expr = expr.as_coeff_Add() + expr = expr.normal() + old = fraction(expr) + n, d = fraction(handle(expr)) + if old != (n, d): + if not d.is_Atom: + was = (n, d) + n = signsimp(n, evaluate=False) + d = signsimp(d, evaluate=False) + u = Factors(_unevaluated_Mul(n, 1/d)) + u = _unevaluated_Mul(*[k**v for k, v in u.factors.items()]) + n, d = fraction(u) + if old == (n, d): + n, d = was + n = expand_mul(n) + if d.is_Number or d.is_Add: + n2, d2 = fraction(gcd_terms(_unevaluated_Mul(n, 1/d))) + if d2.is_Number or (d2.count_ops() <= d.count_ops()): + n, d = [signsimp(i) for i in (n2, d2)] + if n.is_Mul and n.args[0].is_Number: + n = n.func(*n.args) + + return coeff + _unevaluated_Mul(n, 1/d) + + +def rad_rationalize(num, den): + """ + Rationalize ``num/den`` by removing square roots in the denominator; + num and den are sum of terms whose squares are positive rationals. + + Examples + ======== + + >>> from sympy import sqrt + >>> from sympy.simplify.radsimp import rad_rationalize + >>> rad_rationalize(sqrt(3), 1 + sqrt(2)/3) + (-sqrt(3) + sqrt(6)/3, -7/9) + """ + if not den.is_Add: + return num, den + g, a, b = split_surds(den) + a = a*sqrt(g) + num = _mexpand((a - b)*num) + den = _mexpand(a**2 - b**2) + return rad_rationalize(num, den) + + +def fraction(expr, exact=False): + """Returns a pair with expression's numerator and denominator. + If the given expression is not a fraction then this function + will return the tuple (expr, 1). + + This function will not make any attempt to simplify nested + fractions or to do any term rewriting at all. + + If only one of the numerator/denominator pair is needed then + use numer(expr) or denom(expr) functions respectively. + + >>> from sympy import fraction, Rational, Symbol + >>> from sympy.abc import x, y + + >>> fraction(x/y) + (x, y) + >>> fraction(x) + (x, 1) + + >>> fraction(1/y**2) + (1, y**2) + + >>> fraction(x*y/2) + (x*y, 2) + >>> fraction(Rational(1, 2)) + (1, 2) + + This function will also work fine with assumptions: + + >>> k = Symbol('k', negative=True) + >>> fraction(x * y**k) + (x, y**(-k)) + + If we know nothing about sign of some exponent and ``exact`` + flag is unset, then the exponent's structure will + be analyzed and pretty fraction will be returned: + + >>> from sympy import exp, Mul + >>> fraction(2*x**(-y)) + (2, x**y) + + >>> fraction(exp(-x)) + (1, exp(x)) + + >>> fraction(exp(-x), exact=True) + (exp(-x), 1) + + The ``exact`` flag will also keep any unevaluated Muls from + being evaluated: + + >>> u = Mul(2, x + 1, evaluate=False) + >>> fraction(u) + (2*x + 2, 1) + >>> fraction(u, exact=True) + (2*(x + 1), 1) + """ + expr = sympify(expr) + + numer, denom = [], [] + + for term in Mul.make_args(expr): + if term.is_commutative and (term.is_Pow or isinstance(term, exp)): + b, ex = term.as_base_exp() + if ex.is_negative: + if ex is S.NegativeOne: + denom.append(b) + elif exact: + if ex.is_constant(): + denom.append(Pow(b, -ex)) + else: + numer.append(term) + else: + denom.append(Pow(b, -ex)) + elif ex.is_positive: + numer.append(term) + elif not exact and ex.is_Mul: + n, d = term.as_numer_denom() # this will cause evaluation + if n != 1: + numer.append(n) + denom.append(d) + else: + numer.append(term) + elif term.is_Rational and not term.is_Integer: + if term.p != 1: + numer.append(term.p) + denom.append(term.q) + else: + numer.append(term) + return Mul(*numer, evaluate=not exact), Mul(*denom, evaluate=not exact) + + +def numer(expr, exact=False): # default matches fraction's default + return fraction(expr, exact=exact)[0] + + +def denom(expr, exact=False): # default matches fraction's default + return fraction(expr, exact=exact)[1] + + +def fraction_expand(expr, **hints): + return expr.expand(frac=True, **hints) + + +def numer_expand(expr, **hints): + # default matches fraction's default + a, b = fraction(expr, exact=hints.get('exact', False)) + return a.expand(numer=True, **hints) / b + + +def denom_expand(expr, **hints): + # default matches fraction's default + a, b = fraction(expr, exact=hints.get('exact', False)) + return a / b.expand(denom=True, **hints) + + +expand_numer = numer_expand +expand_denom = denom_expand +expand_fraction = fraction_expand + + +def split_surds(expr): + """ + Split an expression with terms whose squares are positive rationals + into a sum of terms whose surds squared have gcd equal to g + and a sum of terms with surds squared prime with g. + + Examples + ======== + + >>> from sympy import sqrt + >>> from sympy.simplify.radsimp import split_surds + >>> split_surds(3*sqrt(3) + sqrt(5)/7 + sqrt(6) + sqrt(10) + sqrt(15)) + (3, sqrt(2) + sqrt(5) + 3, sqrt(5)/7 + sqrt(10)) + """ + args = sorted(expr.args, key=default_sort_key) + coeff_muls = [x.as_coeff_Mul() for x in args] + surds = [x[1]**2 for x in coeff_muls if x[1].is_Pow] + surds.sort(key=default_sort_key) + g, b1, b2 = _split_gcd(*surds) + g2 = g + if not b2 and len(b1) >= 2: + b1n = [x/g for x in b1] + b1n = [x for x in b1n if x != 1] + # only a common factor has been factored; split again + g1, b1n, b2 = _split_gcd(*b1n) + g2 = g*g1 + a1v, a2v = [], [] + for c, s in coeff_muls: + if s.is_Pow and s.exp == S.Half: + s1 = s.base + if s1 in b1: + a1v.append(c*sqrt(s1/g2)) + else: + a2v.append(c*s) + else: + a2v.append(c*s) + a = Add(*a1v) + b = Add(*a2v) + return g2, a, b + + +def _split_gcd(*a): + """ + Split the list of integers ``a`` into a list of integers, ``a1`` having + ``g = gcd(a1)``, and a list ``a2`` whose elements are not divisible by + ``g``. Returns ``g, a1, a2``. + + Examples + ======== + + >>> from sympy.simplify.radsimp import _split_gcd + >>> _split_gcd(55, 35, 22, 14, 77, 10) + (5, [55, 35, 10], [22, 14, 77]) + """ + g = a[0] + b1 = [g] + b2 = [] + for x in a[1:]: + g1 = gcd(g, x) + if g1 == 1: + b2.append(x) + else: + g = g1 + b1.append(x) + return g, b1, b2 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/ratsimp.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/ratsimp.py new file mode 100644 index 0000000000000000000000000000000000000000..95751fab47f585d3ae2e1289f014fba0f2708224 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/ratsimp.py @@ -0,0 +1,222 @@ +from itertools import combinations_with_replacement +from sympy.core import symbols, Add, Dummy +from sympy.core.numbers import Rational +from sympy.polys import cancel, ComputationFailed, parallel_poly_from_expr, reduced, Poly +from sympy.polys.monomials import Monomial, monomial_div +from sympy.polys.polyerrors import DomainError, PolificationFailed +from sympy.utilities.misc import debug, debugf + +def ratsimp(expr): + """ + Put an expression over a common denominator, cancel and reduce. + + Examples + ======== + + >>> from sympy import ratsimp + >>> from sympy.abc import x, y + >>> ratsimp(1/x + 1/y) + (x + y)/(x*y) + """ + + f, g = cancel(expr).as_numer_denom() + try: + Q, r = reduced(f, [g], field=True, expand=False) + except ComputationFailed: + return f/g + + return Add(*Q) + cancel(r/g) + + +def ratsimpmodprime(expr, G, *gens, quick=True, polynomial=False, **args): + """ + Simplifies a rational expression ``expr`` modulo the prime ideal + generated by ``G``. ``G`` should be a Groebner basis of the + ideal. + + Examples + ======== + + >>> from sympy.simplify.ratsimp import ratsimpmodprime + >>> from sympy.abc import x, y + >>> eq = (x + y**5 + y)/(x - y) + >>> ratsimpmodprime(eq, [x*y**5 - x - y], x, y, order='lex') + (-x**2 - x*y - x - y)/(-x**2 + x*y) + + If ``polynomial`` is ``False``, the algorithm computes a rational + simplification which minimizes the sum of the total degrees of + the numerator and the denominator. + + If ``polynomial`` is ``True``, this function just brings numerator and + denominator into a canonical form. This is much faster, but has + potentially worse results. + + References + ========== + + .. [1] M. Monagan, R. Pearce, Rational Simplification Modulo a Polynomial + Ideal, https://dl.acm.org/doi/pdf/10.1145/1145768.1145809 + (specifically, the second algorithm) + """ + from sympy.solvers.solvers import solve + + debug('ratsimpmodprime', expr) + + # usual preparation of polynomials: + + num, denom = cancel(expr).as_numer_denom() + + try: + polys, opt = parallel_poly_from_expr([num, denom] + G, *gens, **args) + except PolificationFailed: + return expr + + domain = opt.domain + + if domain.has_assoc_Field: + opt.domain = domain.get_field() + else: + raise DomainError( + "Cannot compute rational simplification over %s" % domain) + + # compute only once + leading_monomials = [g.LM(opt.order) for g in polys[2:]] + tested = set() + + def staircase(n): + """ + Compute all monomials with degree less than ``n`` that are + not divisible by any element of ``leading_monomials``. + """ + if n == 0: + return [1] + S = [] + for mi in combinations_with_replacement(range(len(opt.gens)), n): + m = [0]*len(opt.gens) + for i in mi: + m[i] += 1 + if all(monomial_div(m, lmg) is None for lmg in + leading_monomials): + S.append(m) + + return [Monomial(s).as_expr(*opt.gens) for s in S] + staircase(n - 1) + + def _ratsimpmodprime(a, b, allsol, N=0, D=0): + r""" + Computes a rational simplification of ``a/b`` which minimizes + the sum of the total degrees of the numerator and the denominator. + + Explanation + =========== + + The algorithm proceeds by looking at ``a * d - b * c`` modulo + the ideal generated by ``G`` for some ``c`` and ``d`` with degree + less than ``a`` and ``b`` respectively. + The coefficients of ``c`` and ``d`` are indeterminates and thus + the coefficients of the normalform of ``a * d - b * c`` are + linear polynomials in these indeterminates. + If these linear polynomials, considered as system of + equations, have a nontrivial solution, then `\frac{a}{b} + \equiv \frac{c}{d}` modulo the ideal generated by ``G``. So, + by construction, the degree of ``c`` and ``d`` is less than + the degree of ``a`` and ``b``, so a simpler representation + has been found. + After a simpler representation has been found, the algorithm + tries to reduce the degree of the numerator and denominator + and returns the result afterwards. + + As an extension, if quick=False, we look at all possible degrees such + that the total degree is less than *or equal to* the best current + solution. We retain a list of all solutions of minimal degree, and try + to find the best one at the end. + """ + c, d = a, b + steps = 0 + + maxdeg = a.total_degree() + b.total_degree() + if quick: + bound = maxdeg - 1 + else: + bound = maxdeg + while N + D <= bound: + if (N, D) in tested: + break + tested.add((N, D)) + + M1 = staircase(N) + M2 = staircase(D) + debugf('%s / %s: %s, %s', (N, D, M1, M2)) + + Cs = symbols("c:%d" % len(M1), cls=Dummy) + Ds = symbols("d:%d" % len(M2), cls=Dummy) + ng = Cs + Ds + + c_hat = Poly( + sum(Cs[i] * M1[i] for i in range(len(M1))), opt.gens + ng) + d_hat = Poly( + sum(Ds[i] * M2[i] for i in range(len(M2))), opt.gens + ng) + + r = reduced(a * d_hat - b * c_hat, G, opt.gens + ng, + order=opt.order, polys=True)[1] + + S = Poly(r, gens=opt.gens).coeffs() + sol = solve(S, Cs + Ds, particular=True, quick=True) + + if sol and not all(s == 0 for s in sol.values()): + c = c_hat.subs(sol) + d = d_hat.subs(sol) + + # The "free" variables occurring before as parameters + # might still be in the substituted c, d, so set them + # to the value chosen before: + c = c.subs(dict(list(zip(Cs + Ds, [1] * (len(Cs) + len(Ds)))))) + d = d.subs(dict(list(zip(Cs + Ds, [1] * (len(Cs) + len(Ds)))))) + + c = Poly(c, opt.gens) + d = Poly(d, opt.gens) + if d == 0: + raise ValueError('Ideal not prime?') + + allsol.append((c_hat, d_hat, S, Cs + Ds)) + if N + D != maxdeg: + allsol = [allsol[-1]] + + break + + steps += 1 + N += 1 + D += 1 + + if steps > 0: + c, d, allsol = _ratsimpmodprime(c, d, allsol, N, D - steps) + c, d, allsol = _ratsimpmodprime(c, d, allsol, N - steps, D) + + return c, d, allsol + + # preprocessing. this improves performance a bit when deg(num) + # and deg(denom) are large: + num = reduced(num, G, opt.gens, order=opt.order)[1] + denom = reduced(denom, G, opt.gens, order=opt.order)[1] + + if polynomial: + return (num/denom).cancel() + + c, d, allsol = _ratsimpmodprime( + Poly(num, opt.gens, domain=opt.domain), Poly(denom, opt.gens, domain=opt.domain), []) + if not quick and allsol: + debugf('Looking for best minimal solution. Got: %s', len(allsol)) + newsol = [] + for c_hat, d_hat, S, ng in allsol: + sol = solve(S, ng, particular=True, quick=False) + # all values of sol should be numbers; if not, solve is broken + newsol.append((c_hat.subs(sol), d_hat.subs(sol))) + c, d = min(newsol, key=lambda x: len(x[0].terms()) + len(x[1].terms())) + + if not domain.is_Field: + cn, c = c.clear_denoms(convert=True) + dn, d = d.clear_denoms(convert=True) + r = Rational(cn, dn) + else: + r = Rational(1) + + return (c*r.q)/(d*r.p) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/simplify.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/simplify.py new file mode 100644 index 0000000000000000000000000000000000000000..8b315cc20c19fc10c37b903d16129a7f5579ecd3 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/simplify.py @@ -0,0 +1,2164 @@ +from __future__ import annotations + +from typing import overload + +from collections import defaultdict + +from sympy.concrete.products import Product +from sympy.concrete.summations import Sum +from sympy.core import (Basic, S, Add, Mul, Pow, Symbol, sympify, + expand_func, Function, Dummy, Expr, factor_terms, + expand_power_exp, Eq) +from sympy.core.exprtools import factor_nc +from sympy.core.parameters import global_parameters +from sympy.core.function import (expand_log, count_ops, _mexpand, + nfloat, expand_mul, expand) +from sympy.core.numbers import Float, I, pi, Rational, equal_valued +from sympy.core.relational import Relational +from sympy.core.rules import Transform +from sympy.core.sorting import ordered +from sympy.core.sympify import _sympify +from sympy.core.traversal import bottom_up as _bottom_up, walk as _walk +from sympy.functions import gamma, exp, sqrt, log, exp_polar, re +from sympy.functions.combinatorial.factorials import CombinatorialFunction +from sympy.functions.elementary.complexes import unpolarify, Abs, sign +from sympy.functions.elementary.exponential import ExpBase +from sympy.functions.elementary.hyperbolic import HyperbolicFunction +from sympy.functions.elementary.integers import ceiling +from sympy.functions.elementary.piecewise import (Piecewise, piecewise_fold, + piecewise_simplify) +from sympy.functions.elementary.trigonometric import TrigonometricFunction +from sympy.functions.special.bessel import (BesselBase, besselj, besseli, + besselk, bessely, jn) +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.integrals.integrals import Integral +from sympy.logic.boolalg import Boolean +from sympy.matrices.expressions import (MatrixExpr, MatAdd, MatMul, + MatPow, MatrixSymbol) +from sympy.polys import together, cancel, factor +from sympy.polys.numberfields.minpoly import _is_sum_surds, _minimal_polynomial_sq +from sympy.sets.sets import Set +from sympy.simplify.combsimp import combsimp +from sympy.simplify.cse_opts import sub_pre, sub_post +from sympy.simplify.hyperexpand import hyperexpand +from sympy.simplify.powsimp import powsimp +from sympy.simplify.radsimp import radsimp, fraction, collect_abs +from sympy.simplify.sqrtdenest import sqrtdenest +from sympy.simplify.trigsimp import trigsimp, exptrigsimp +from sympy.utilities.decorator import deprecated +from sympy.utilities.iterables import has_variety, sift, subsets, iterable +from sympy.utilities.misc import as_int + +import mpmath + + +def separatevars(expr, symbols=[], dict=False, force=False): + """ + Separates variables in an expression, if possible. By + default, it separates with respect to all symbols in an + expression and collects constant coefficients that are + independent of symbols. + + Explanation + =========== + + If ``dict=True`` then the separated terms will be returned + in a dictionary keyed to their corresponding symbols. + By default, all symbols in the expression will appear as + keys; if symbols are provided, then all those symbols will + be used as keys, and any terms in the expression containing + other symbols or non-symbols will be returned keyed to the + string 'coeff'. (Passing None for symbols will return the + expression in a dictionary keyed to 'coeff'.) + + If ``force=True``, then bases of powers will be separated regardless + of assumptions on the symbols involved. + + Notes + ===== + + The order of the factors is determined by Mul, so that the + separated expressions may not necessarily be grouped together. + + Although factoring is necessary to separate variables in some + expressions, it is not necessary in all cases, so one should not + count on the returned factors being factored. + + Examples + ======== + + >>> from sympy.abc import x, y, z, alpha + >>> from sympy import separatevars, sin + >>> separatevars((x*y)**y) + (x*y)**y + >>> separatevars((x*y)**y, force=True) + x**y*y**y + + >>> e = 2*x**2*z*sin(y)+2*z*x**2 + >>> separatevars(e) + 2*x**2*z*(sin(y) + 1) + >>> separatevars(e, symbols=(x, y), dict=True) + {'coeff': 2*z, x: x**2, y: sin(y) + 1} + >>> separatevars(e, [x, y, alpha], dict=True) + {'coeff': 2*z, alpha: 1, x: x**2, y: sin(y) + 1} + + If the expression is not really separable, or is only partially + separable, separatevars will do the best it can to separate it + by using factoring. + + >>> separatevars(x + x*y - 3*x**2) + -x*(3*x - y - 1) + + If the expression is not separable then expr is returned unchanged + or (if dict=True) then None is returned. + + >>> eq = 2*x + y*sin(x) + >>> separatevars(eq) == eq + True + >>> separatevars(2*x + y*sin(x), symbols=(x, y), dict=True) is None + True + + """ + expr = sympify(expr) + if dict: + return _separatevars_dict(_separatevars(expr, force), symbols) + else: + return _separatevars(expr, force) + + +def _separatevars(expr, force): + if isinstance(expr, Abs): + arg = expr.args[0] + if arg.is_Mul and not arg.is_number: + s = separatevars(arg, dict=True, force=force) + if s is not None: + return Mul(*map(expr.func, s.values())) + else: + return expr + + if len(expr.free_symbols) < 2: + return expr + + # don't destroy a Mul since much of the work may already be done + if expr.is_Mul: + args = list(expr.args) + changed = False + for i, a in enumerate(args): + args[i] = separatevars(a, force) + changed = changed or args[i] != a + if changed: + expr = expr.func(*args) + return expr + + # get a Pow ready for expansion + if expr.is_Pow and expr.base != S.Exp1: + expr = Pow(separatevars(expr.base, force=force), expr.exp) + + # First try other expansion methods + expr = expr.expand(mul=False, multinomial=False, force=force) + + _expr, reps = posify(expr) if force else (expr, {}) + expr = factor(_expr).subs(reps) + + if not expr.is_Add: + return expr + + # Find any common coefficients to pull out + args = list(expr.args) + commonc = args[0].args_cnc(cset=True, warn=False)[0] + for i in args[1:]: + commonc &= i.args_cnc(cset=True, warn=False)[0] + commonc = Mul(*commonc) + commonc = commonc.as_coeff_Mul()[1] # ignore constants + commonc_set = commonc.args_cnc(cset=True, warn=False)[0] + + # remove them + for i, a in enumerate(args): + c, nc = a.args_cnc(cset=True, warn=False) + c = c - commonc_set + args[i] = Mul(*c)*Mul(*nc) + nonsepar = Add(*args) + + if len(nonsepar.free_symbols) > 1: + _expr = nonsepar + _expr, reps = posify(_expr) if force else (_expr, {}) + _expr = (factor(_expr)).subs(reps) + + if not _expr.is_Add: + nonsepar = _expr + + return commonc*nonsepar + + +def _separatevars_dict(expr, symbols): + if symbols: + if not all(t.is_Atom for t in symbols): + raise ValueError("symbols must be Atoms.") + symbols = list(symbols) + elif symbols is None: + return {'coeff': expr} + else: + symbols = list(expr.free_symbols) + if not symbols: + return None + + ret = {i: [] for i in symbols + ['coeff']} + + for i in Mul.make_args(expr): + expsym = i.free_symbols + intersection = set(symbols).intersection(expsym) + if len(intersection) > 1: + return None + if len(intersection) == 0: + # There are no symbols, so it is part of the coefficient + ret['coeff'].append(i) + else: + ret[intersection.pop()].append(i) + + # rebuild + for k, v in ret.items(): + ret[k] = Mul(*v) + + return ret + + +def posify(eq): + """Return ``eq`` (with generic symbols made positive) and a + dictionary containing the mapping between the old and new + symbols. + + Explanation + =========== + + Any symbol that has positive=None will be replaced with a positive dummy + symbol having the same name. This replacement will allow more symbolic + processing of expressions, especially those involving powers and + logarithms. + + A dictionary that can be sent to subs to restore ``eq`` to its original + symbols is also returned. + + >>> from sympy import posify, Symbol, log, solve + >>> from sympy.abc import x + >>> posify(x + Symbol('p', positive=True) + Symbol('n', negative=True)) + (_x + n + p, {_x: x}) + + >>> eq = 1/x + >>> log(eq).expand() + log(1/x) + >>> log(posify(eq)[0]).expand() + -log(_x) + >>> p, rep = posify(eq) + >>> log(p).expand().subs(rep) + -log(x) + + It is possible to apply the same transformations to an iterable + of expressions: + + >>> eq = x**2 - 4 + >>> solve(eq, x) + [-2, 2] + >>> eq_x, reps = posify([eq, x]); eq_x + [_x**2 - 4, _x] + >>> solve(*eq_x) + [2] + """ + eq = sympify(eq) + if not isinstance(eq, Basic) and iterable(eq): + f = type(eq) + eq = list(eq) + syms = set() + for e in eq: + syms = syms.union(e.atoms(Symbol)) + reps = {} + for s in syms: + reps.update({v: k for k, v in posify(s)[1].items()}) + for i, e in enumerate(eq): + eq[i] = e.subs(reps) + return f(eq), {r: s for s, r in reps.items()} + + reps = {s: Dummy(s.name, positive=True, **s.assumptions0) + for s in eq.free_symbols if s.is_positive is None} + eq = eq.subs(reps) + return eq, {r: s for s, r in reps.items()} + + +def hypersimp(f, k): + """Given combinatorial term f(k) simplify its consecutive term ratio + i.e. f(k+1)/f(k). The input term can be composed of functions and + integer sequences which have equivalent representation in terms + of gamma special function. + + Explanation + =========== + + The algorithm performs three basic steps: + + 1. Rewrite all functions in terms of gamma, if possible. + + 2. Rewrite all occurrences of gamma in terms of products + of gamma and rising factorial with integer, absolute + constant exponent. + + 3. Perform simplification of nested fractions, powers + and if the resulting expression is a quotient of + polynomials, reduce their total degree. + + If f(k) is hypergeometric then as result we arrive with a + quotient of polynomials of minimal degree. Otherwise None + is returned. + + For more information on the implemented algorithm refer to: + + 1. W. Koepf, Algorithms for m-fold Hypergeometric Summation, + Journal of Symbolic Computation (1995) 20, 399-417 + """ + f = sympify(f) + + g = f.subs(k, k + 1) / f + + g = g.rewrite(gamma) + if g.has(Piecewise): + g = piecewise_fold(g) + g = g.args[-1][0] + g = expand_func(g) + g = powsimp(g, deep=True, combine='exp') + + if g.is_rational_function(k): + return simplify(g, ratio=S.Infinity) + else: + return None + + +def hypersimilar(f, g, k): + """ + Returns True if ``f`` and ``g`` are hyper-similar. + + Explanation + =========== + + Similarity in hypergeometric sense means that a quotient of + f(k) and g(k) is a rational function in ``k``. This procedure + is useful in solving recurrence relations. + + For more information see hypersimp(). + + """ + f, g = list(map(sympify, (f, g))) + + h = (f/g).rewrite(gamma) + h = h.expand(func=True, basic=False) + + return h.is_rational_function(k) + + +def signsimp(expr, evaluate=None): + """Make all Add sub-expressions canonical wrt sign. + + Explanation + =========== + + If an Add subexpression, ``a``, can have a sign extracted, + as determined by could_extract_minus_sign, it is replaced + with Mul(-1, a, evaluate=False). This allows signs to be + extracted from powers and products. + + Examples + ======== + + >>> from sympy import signsimp, exp, symbols + >>> from sympy.abc import x, y + >>> i = symbols('i', odd=True) + >>> n = -1 + 1/x + >>> n/x/(-n)**2 - 1/n/x + (-1 + 1/x)/(x*(1 - 1/x)**2) - 1/(x*(-1 + 1/x)) + >>> signsimp(_) + 0 + >>> x*n + x*-n + x*(-1 + 1/x) + x*(1 - 1/x) + >>> signsimp(_) + 0 + + Since powers automatically handle leading signs + + >>> (-2)**i + -2**i + + signsimp can be used to put the base of a power with an integer + exponent into canonical form: + + >>> n**i + (-1 + 1/x)**i + + By default, signsimp does not leave behind any hollow simplification: + if making an Add canonical wrt sign didn't change the expression, the + original Add is restored. If this is not desired then the keyword + ``evaluate`` can be set to False: + + >>> e = exp(y - x) + >>> signsimp(e) == e + True + >>> signsimp(e, evaluate=False) + exp(-(x - y)) + + """ + if evaluate is None: + evaluate = global_parameters.evaluate + expr = sympify(expr) + if not isinstance(expr, (Expr, Relational)) or expr.is_Atom: + return expr + # get rid of an pre-existing unevaluation regarding sign + e = expr.replace(lambda x: x.is_Mul and -(-x) != x, lambda x: -(-x)) + e = sub_post(sub_pre(e)) + if not isinstance(e, (Expr, Relational)) or e.is_Atom: + return e + if e.is_Add: + rv = e.func(*[signsimp(a) for a in e.args]) + if not evaluate and isinstance(rv, Add + ) and rv.could_extract_minus_sign(): + return Mul(S.NegativeOne, -rv, evaluate=False) + return rv + if evaluate: + e = e.replace(lambda x: x.is_Mul and -(-x) != x, lambda x: -(-x)) + return e + + +@overload +def simplify(expr: Expr, **kwargs) -> Expr: ... +@overload +def simplify(expr: Boolean, **kwargs) -> Boolean: ... +@overload +def simplify(expr: Set, **kwargs) -> Set: ... +@overload +def simplify(expr: Basic, **kwargs) -> Basic: ... + +def simplify(expr, ratio=1.7, measure=count_ops, rational=False, inverse=False, doit=True, **kwargs): + """Simplifies the given expression. + + Explanation + =========== + + Simplification is not a well defined term and the exact strategies + this function tries can change in the future versions of SymPy. If + your algorithm relies on "simplification" (whatever it is), try to + determine what you need exactly - is it powsimp()?, radsimp()?, + together()?, logcombine()?, or something else? And use this particular + function directly, because those are well defined and thus your algorithm + will be robust. + + Nonetheless, especially for interactive use, or when you do not know + anything about the structure of the expression, simplify() tries to apply + intelligent heuristics to make the input expression "simpler". For + example: + + >>> from sympy import simplify, cos, sin + >>> from sympy.abc import x, y + >>> a = (x + x**2)/(x*sin(y)**2 + x*cos(y)**2) + >>> a + (x**2 + x)/(x*sin(y)**2 + x*cos(y)**2) + >>> simplify(a) + x + 1 + + Note that we could have obtained the same result by using specific + simplification functions: + + >>> from sympy import trigsimp, cancel + >>> trigsimp(a) + (x**2 + x)/x + >>> cancel(_) + x + 1 + + In some cases, applying :func:`simplify` may actually result in some more + complicated expression. The default ``ratio=1.7`` prevents more extreme + cases: if (result length)/(input length) > ratio, then input is returned + unmodified. The ``measure`` parameter lets you specify the function used + to determine how complex an expression is. The function should take a + single argument as an expression and return a number such that if + expression ``a`` is more complex than expression ``b``, then + ``measure(a) > measure(b)``. The default measure function is + :func:`~.count_ops`, which returns the total number of operations in the + expression. + + For example, if ``ratio=1``, ``simplify`` output cannot be longer + than input. + + :: + + >>> from sympy import sqrt, simplify, count_ops, oo + >>> root = 1/(sqrt(2)+3) + + Since ``simplify(root)`` would result in a slightly longer expression, + root is returned unchanged instead:: + + >>> simplify(root, ratio=1) == root + True + + If ``ratio=oo``, simplify will be applied anyway:: + + >>> count_ops(simplify(root, ratio=oo)) > count_ops(root) + True + + Note that the shortest expression is not necessary the simplest, so + setting ``ratio`` to 1 may not be a good idea. + Heuristically, the default value ``ratio=1.7`` seems like a reasonable + choice. + + You can easily define your own measure function based on what you feel + should represent the "size" or "complexity" of the input expression. Note + that some choices, such as ``lambda expr: len(str(expr))`` may appear to be + good metrics, but have other problems (in this case, the measure function + may slow down simplify too much for very large expressions). If you do not + know what a good metric would be, the default, ``count_ops``, is a good + one. + + For example: + + >>> from sympy import symbols, log + >>> a, b = symbols('a b', positive=True) + >>> g = log(a) + log(b) + log(a)*log(1/b) + >>> h = simplify(g) + >>> h + log(a*b**(1 - log(a))) + >>> count_ops(g) + 8 + >>> count_ops(h) + 5 + + So you can see that ``h`` is simpler than ``g`` using the count_ops metric. + However, we may not like how ``simplify`` (in this case, using + ``logcombine``) has created the ``b**(log(1/a) + 1)`` term. A simple way + to reduce this would be to give more weight to powers as operations in + ``count_ops``. We can do this by using the ``visual=True`` option: + + >>> print(count_ops(g, visual=True)) + 2*ADD + DIV + 4*LOG + MUL + >>> print(count_ops(h, visual=True)) + 2*LOG + MUL + POW + SUB + + >>> from sympy import Symbol, S + >>> def my_measure(expr): + ... POW = Symbol('POW') + ... # Discourage powers by giving POW a weight of 10 + ... count = count_ops(expr, visual=True).subs(POW, 10) + ... # Every other operation gets a weight of 1 (the default) + ... count = count.replace(Symbol, type(S.One)) + ... return count + >>> my_measure(g) + 8 + >>> my_measure(h) + 14 + >>> 15./8 > 1.7 # 1.7 is the default ratio + True + >>> simplify(g, measure=my_measure) + -log(a)*log(b) + log(a) + log(b) + + Note that because ``simplify()`` internally tries many different + simplification strategies and then compares them using the measure + function, we get a completely different result that is still different + from the input expression by doing this. + + If ``rational=True``, Floats will be recast as Rationals before simplification. + If ``rational=None``, Floats will be recast as Rationals but the result will + be recast as Floats. If rational=False(default) then nothing will be done + to the Floats. + + If ``inverse=True``, it will be assumed that a composition of inverse + functions, such as sin and asin, can be cancelled in any order. + For example, ``asin(sin(x))`` will yield ``x`` without checking whether + x belongs to the set where this relation is true. The default is + False. + + Note that ``simplify()`` automatically calls ``doit()`` on the final + expression. You can avoid this behavior by passing ``doit=False`` as + an argument. + + Also, it should be noted that simplifying a boolean expression is not + well defined. If the expression prefers automatic evaluation (such as + :obj:`~.Eq()` or :obj:`~.Or()`), simplification will return ``True`` or + ``False`` if truth value can be determined. If the expression is not + evaluated by default (such as :obj:`~.Predicate()`), simplification will + not reduce it and you should use :func:`~.refine` or :func:`~.ask` + function. This inconsistency will be resolved in future version. + + See Also + ======== + + sympy.assumptions.refine.refine : Simplification using assumptions. + sympy.assumptions.ask.ask : Query for boolean expressions using assumptions. + """ + + def shorter(*choices): + """ + Return the choice that has the fewest ops. In case of a tie, + the expression listed first is selected. + """ + if not has_variety(choices): + return choices[0] + return min(choices, key=measure) + + def done(e): + rv = e.doit() if doit else e + return shorter(rv, collect_abs(rv)) + + expr = sympify(expr, rational=rational) + kwargs = { + "ratio": kwargs.get('ratio', ratio), + "measure": kwargs.get('measure', measure), + "rational": kwargs.get('rational', rational), + "inverse": kwargs.get('inverse', inverse), + "doit": kwargs.get('doit', doit)} + # no routine for Expr needs to check for is_zero + if isinstance(expr, Expr) and expr.is_zero: + return S.Zero if not expr.is_Number else expr + + _eval_simplify = getattr(expr, '_eval_simplify', None) + if _eval_simplify is not None: + return _eval_simplify(**kwargs) + + original_expr = expr = collect_abs(signsimp(expr)) + + if not isinstance(expr, Basic) or not expr.args: # XXX: temporary hack + return expr + + if inverse and expr.has(Function): + expr = inversecombine(expr) + if not expr.args: # simplified to atomic + return expr + + # do deep simplification + handled = Add, Mul, Pow, ExpBase + expr = expr.replace( + # here, checking for x.args is not enough because Basic has + # args but Basic does not always play well with replace, e.g. + # when simultaneous is True found expressions will be masked + # off with a Dummy but not all Basic objects in an expression + # can be replaced with a Dummy + lambda x: isinstance(x, Expr) and x.args and not isinstance( + x, handled), + lambda x: x.func(*[simplify(i, **kwargs) for i in x.args]), + simultaneous=False) + if not isinstance(expr, handled): + return done(expr) + + if not expr.is_commutative: + expr = nc_simplify(expr) + + # TODO: Apply different strategies, considering expression pattern: + # is it a purely rational function? Is there any trigonometric function?... + # See also https://github.com/sympy/sympy/pull/185. + + # rationalize Floats + floats = False + if rational is not False and expr.has(Float): + floats = True + expr = nsimplify(expr, rational=True) + + expr = _bottom_up(expr, lambda w: getattr(w, 'normal', lambda: w)()) + expr = Mul(*powsimp(expr).as_content_primitive()) + _e = cancel(expr) + expr1 = shorter(_e, _mexpand(_e).cancel()) # issue 6829 + expr2 = shorter(together(expr, deep=True), together(expr1, deep=True)) + + if ratio is S.Infinity: + expr = expr2 + else: + expr = shorter(expr2, expr1, expr) + if not isinstance(expr, Basic): # XXX: temporary hack + return expr + + expr = factor_terms(expr, sign=False) + + # must come before `Piecewise` since this introduces more `Piecewise` terms + if expr.has(sign): + expr = expr.rewrite(Abs) + + # Deal with Piecewise separately to avoid recursive growth of expressions + if expr.has(Piecewise): + # Fold into a single Piecewise + expr = piecewise_fold(expr) + # Apply doit, if doit=True + expr = done(expr) + # Still a Piecewise? + if expr.has(Piecewise): + # Fold into a single Piecewise, in case doit lead to some + # expressions being Piecewise + expr = piecewise_fold(expr) + # kroneckersimp also affects Piecewise + if expr.has(KroneckerDelta): + expr = kroneckersimp(expr) + # Still a Piecewise? + if expr.has(Piecewise): + # Do not apply doit on the segments as it has already + # been done above, but simplify + expr = piecewise_simplify(expr, deep=True, doit=False) + # Still a Piecewise? + if expr.has(Piecewise): + # Try factor common terms + expr = shorter(expr, factor_terms(expr)) + # As all expressions have been simplified above with the + # complete simplify, nothing more needs to be done here + return expr + + # hyperexpand automatically only works on hypergeometric terms + # Do this after the Piecewise part to avoid recursive expansion + expr = hyperexpand(expr) + + if expr.has(KroneckerDelta): + expr = kroneckersimp(expr) + + if expr.has(BesselBase): + expr = besselsimp(expr) + + if expr.has(TrigonometricFunction, HyperbolicFunction): + expr = trigsimp(expr, deep=True) + + if expr.has(log): + expr = shorter(expand_log(expr, deep=True), logcombine(expr)) + + if expr.has(CombinatorialFunction, gamma): + # expression with gamma functions or non-integer arguments is + # automatically passed to gammasimp + expr = combsimp(expr) + + if expr.has(Sum): + expr = sum_simplify(expr, **kwargs) + + if expr.has(Integral): + expr = expr.xreplace({ + i: factor_terms(i) for i in expr.atoms(Integral)}) + + if expr.has(Product): + expr = product_simplify(expr, **kwargs) + + from sympy.physics.units import Quantity + + if expr.has(Quantity): + from sympy.physics.units.util import quantity_simplify + expr = quantity_simplify(expr) + + short = shorter(powsimp(expr, combine='exp', deep=True), powsimp(expr), expr) + short = shorter(short, cancel(short)) + short = shorter(short, factor_terms(short), expand_power_exp(expand_mul(short))) + if short.has(TrigonometricFunction, HyperbolicFunction, ExpBase, exp): + short = exptrigsimp(short) + + # get rid of hollow 2-arg Mul factorization + hollow_mul = Transform( + lambda x: Mul(*x.args), + lambda x: + x.is_Mul and + len(x.args) == 2 and + x.args[0].is_Number and + x.args[1].is_Add and + x.is_commutative) + expr = short.xreplace(hollow_mul) + + numer, denom = expr.as_numer_denom() + if denom.is_Add: + n, d = fraction(radsimp(1/denom, symbolic=False, max_terms=1)) + if n is not S.One: + expr = (numer*n).expand()/d + + if expr.could_extract_minus_sign(): + n, d = fraction(expr) + if d != 0: + expr = signsimp(-n/(-d)) + + if measure(expr) > ratio*measure(original_expr): + expr = original_expr + + # restore floats + if floats and rational is None: + expr = nfloat(expr, exponent=False) + + return done(expr) + + +def sum_simplify(s, **kwargs): + """Main function for Sum simplification""" + if not isinstance(s, Add): + s = s.xreplace({a: sum_simplify(a, **kwargs) + for a in s.atoms(Add) if a.has(Sum)}) + s = expand(s) + if not isinstance(s, Add): + return s + + terms = s.args + s_t = [] # Sum Terms + o_t = [] # Other Terms + + for term in terms: + sum_terms, other = sift(Mul.make_args(term), + lambda i: isinstance(i, Sum), binary=True) + if not sum_terms: + o_t.append(term) + continue + other = [Mul(*other)] + s_t.append(Mul(*(other + [s._eval_simplify(**kwargs) for s in sum_terms]))) + + result = Add(sum_combine(s_t), *o_t) + + return result + + +def sum_combine(s_t): + """Helper function for Sum simplification + + Attempts to simplify a list of sums, by combining limits / sum function's + returns the simplified sum + """ + used = [False] * len(s_t) + + for method in range(2): + for i, s_term1 in enumerate(s_t): + if not used[i]: + for j, s_term2 in enumerate(s_t): + if not used[j] and i != j: + temp = sum_add(s_term1, s_term2, method) + if isinstance(temp, (Sum, Mul)): + s_t[i] = temp + s_term1 = s_t[i] + used[j] = True + + result = S.Zero + for i, s_term in enumerate(s_t): + if not used[i]: + result = Add(result, s_term) + + return result + +def factor_sum(self, limits=None, radical=False, clear=False, fraction=False, sign=True): + """Return Sum with constant factors extracted. + + If ``limits`` is specified then ``self`` is the summand; the other + keywords are passed to ``factor_terms``. + + Examples + ======== + + >>> from sympy import Sum + >>> from sympy.abc import x, y + >>> from sympy.simplify.simplify import factor_sum + >>> s = Sum(x*y, (x, 1, 3)) + >>> factor_sum(s) + y*Sum(x, (x, 1, 3)) + >>> factor_sum(s.function, s.limits) + y*Sum(x, (x, 1, 3)) + """ + + # XXX deprecate in favor of direct call to factor_terms + kwargs = {"radical": radical, "clear": clear, + "fraction": fraction, "sign": sign} + expr = Sum(self, *limits) if limits else self + return factor_terms(expr, **kwargs) + + +def sum_add(self, other, method=0): + """Helper function for Sum simplification""" + #we know this is something in terms of a constant * a sum + #so we temporarily put the constants inside for simplification + #then simplify the result + def __refactor(val): + args = Mul.make_args(val) + sumv = next(x for x in args if isinstance(x, Sum)) + constant = Mul(*[x for x in args if x != sumv]) + return Sum(constant * sumv.function, *sumv.limits) + + if isinstance(self, Mul): + rself = __refactor(self) + else: + rself = self + + if isinstance(other, Mul): + rother = __refactor(other) + else: + rother = other + + if type(rself) is type(rother): + if method == 0: + if rself.limits == rother.limits: + return factor_sum(Sum(rself.function + rother.function, *rself.limits)) + elif method == 1: + if simplify(rself.function - rother.function) == 0: + if len(rself.limits) == len(rother.limits) == 1: + i = rself.limits[0][0] + x1 = rself.limits[0][1] + y1 = rself.limits[0][2] + j = rother.limits[0][0] + x2 = rother.limits[0][1] + y2 = rother.limits[0][2] + + if i == j: + if x2 == y1 + 1: + return factor_sum(Sum(rself.function, (i, x1, y2))) + elif x1 == y2 + 1: + return factor_sum(Sum(rself.function, (i, x2, y1))) + + return Add(self, other) + + +def product_simplify(s, **kwargs): + """Main function for Product simplification""" + terms = Mul.make_args(s) + p_t = [] # Product Terms + o_t = [] # Other Terms + + deep = kwargs.get('deep', True) + for term in terms: + if isinstance(term, Product): + if deep: + p_t.append(Product(term.function.simplify(**kwargs), + *term.limits)) + else: + p_t.append(term) + else: + o_t.append(term) + + used = [False] * len(p_t) + + for method in range(2): + for i, p_term1 in enumerate(p_t): + if not used[i]: + for j, p_term2 in enumerate(p_t): + if not used[j] and i != j: + tmp_prod = product_mul(p_term1, p_term2, method) + if isinstance(tmp_prod, Product): + p_t[i] = tmp_prod + used[j] = True + + result = Mul(*o_t) + + for i, p_term in enumerate(p_t): + if not used[i]: + result = Mul(result, p_term) + + return result + + +def product_mul(self, other, method=0): + """Helper function for Product simplification""" + if type(self) is type(other): + if method == 0: + if self.limits == other.limits: + return Product(self.function * other.function, *self.limits) + elif method == 1: + if simplify(self.function - other.function) == 0: + if len(self.limits) == len(other.limits) == 1: + i = self.limits[0][0] + x1 = self.limits[0][1] + y1 = self.limits[0][2] + j = other.limits[0][0] + x2 = other.limits[0][1] + y2 = other.limits[0][2] + + if i == j: + if x2 == y1 + 1: + return Product(self.function, (i, x1, y2)) + elif x1 == y2 + 1: + return Product(self.function, (i, x2, y1)) + + return Mul(self, other) + + +def _nthroot_solve(p, n, prec): + """ + helper function for ``nthroot`` + It denests ``p**Rational(1, n)`` using its minimal polynomial + """ + from sympy.solvers import solve + while n % 2 == 0: + p = sqrtdenest(sqrt(p)) + n = n // 2 + if n == 1: + return p + pn = p**Rational(1, n) + x = Symbol('x') + f = _minimal_polynomial_sq(p, n, x) + if f is None: + return None + sols = solve(f, x) + for sol in sols: + if abs(sol - pn).n() < 1./10**prec: + sol = sqrtdenest(sol) + if _mexpand(sol**n) == p: + return sol + + +def logcombine(expr, force=False): + """ + Takes logarithms and combines them using the following rules: + + - log(x) + log(y) == log(x*y) if both are positive + - a*log(x) == log(x**a) if x is positive and a is real + + If ``force`` is ``True`` then the assumptions above will be assumed to hold if + there is no assumption already in place on a quantity. For example, if + ``a`` is imaginary or the argument negative, force will not perform a + combination but if ``a`` is a symbol with no assumptions the change will + take place. + + Examples + ======== + + >>> from sympy import Symbol, symbols, log, logcombine, I + >>> from sympy.abc import a, x, y, z + >>> logcombine(a*log(x) + log(y) - log(z)) + a*log(x) + log(y) - log(z) + >>> logcombine(a*log(x) + log(y) - log(z), force=True) + log(x**a*y/z) + >>> x,y,z = symbols('x,y,z', positive=True) + >>> a = Symbol('a', real=True) + >>> logcombine(a*log(x) + log(y) - log(z)) + log(x**a*y/z) + + The transformation is limited to factors and/or terms that + contain logs, so the result depends on the initial state of + expansion: + + >>> eq = (2 + 3*I)*log(x) + >>> logcombine(eq, force=True) == eq + True + >>> logcombine(eq.expand(), force=True) + log(x**2) + I*log(x**3) + + See Also + ======== + + posify: replace all symbols with symbols having positive assumptions + sympy.core.function.expand_log: expand the logarithms of products + and powers; the opposite of logcombine + + """ + + def f(rv): + if not (rv.is_Add or rv.is_Mul): + return rv + + def gooda(a): + # bool to tell whether the leading ``a`` in ``a*log(x)`` + # could appear as log(x**a) + return (a is not S.NegativeOne and # -1 *could* go, but we disallow + (a.is_extended_real or force and a.is_extended_real is not False)) + + def goodlog(l): + # bool to tell whether log ``l``'s argument can combine with others + a = l.args[0] + return a.is_positive or force and a.is_nonpositive is not False + + other = [] + logs = [] + log1 = defaultdict(list) + for a in Add.make_args(rv): + if isinstance(a, log) and goodlog(a): + log1[()].append(([], a)) + elif not a.is_Mul: + other.append(a) + else: + ot = [] + co = [] + lo = [] + for ai in a.args: + if ai.is_Rational and ai < 0: + ot.append(S.NegativeOne) + co.append(-ai) + elif isinstance(ai, log) and goodlog(ai): + lo.append(ai) + elif gooda(ai): + co.append(ai) + else: + ot.append(ai) + if len(lo) > 1: + logs.append((ot, co, lo)) + elif lo: + log1[tuple(ot)].append((co, lo[0])) + else: + other.append(a) + + # if there is only one log in other, put it with the + # good logs + if len(other) == 1 and isinstance(other[0], log): + log1[()].append(([], other.pop())) + # if there is only one log at each coefficient and none have + # an exponent to place inside the log then there is nothing to do + if not logs and all(len(log1[k]) == 1 and log1[k][0] == [] for k in log1): + return rv + + # collapse multi-logs as far as possible in a canonical way + # TODO: see if x*log(a)+x*log(a)*log(b) -> x*log(a)*(1+log(b))? + # -- in this case, it's unambiguous, but if it were were a log(c) in + # each term then it's arbitrary whether they are grouped by log(a) or + # by log(c). So for now, just leave this alone; it's probably better to + # let the user decide + for o, e, l in logs: + l = list(ordered(l)) + e = log(l.pop(0).args[0]**Mul(*e)) + while l: + li = l.pop(0) + e = log(li.args[0]**e) + c, l = Mul(*o), e + if isinstance(l, log): # it should be, but check to be sure + log1[(c,)].append(([], l)) + else: + other.append(c*l) + + # logs that have the same coefficient can multiply + for k in list(log1.keys()): + log1[Mul(*k)] = log(logcombine(Mul(*[ + l.args[0]**Mul(*c) for c, l in log1.pop(k)]), + force=force), evaluate=False) + + # logs that have oppositely signed coefficients can divide + for k in ordered(list(log1.keys())): + if k not in log1: # already popped as -k + continue + if -k in log1: + # figure out which has the minus sign; the one with + # more op counts should be the one + num, den = k, -k + if num.count_ops() > den.count_ops(): + num, den = den, num + other.append( + num*log(log1.pop(num).args[0]/log1.pop(den).args[0], + evaluate=False)) + else: + other.append(k*log1.pop(k)) + + return Add(*other) + + return _bottom_up(expr, f) + + +def inversecombine(expr): + """Simplify the composition of a function and its inverse. + + Explanation + =========== + + No attention is paid to whether the inverse is a left inverse or a + right inverse; thus, the result will in general not be equivalent + to the original expression. + + Examples + ======== + + >>> from sympy.simplify.simplify import inversecombine + >>> from sympy import asin, sin, log, exp + >>> from sympy.abc import x + >>> inversecombine(asin(sin(x))) + x + >>> inversecombine(2*log(exp(3*x))) + 6*x + """ + + def f(rv): + if isinstance(rv, log): + if isinstance(rv.args[0], exp) or (rv.args[0].is_Pow and rv.args[0].base == S.Exp1): + rv = rv.args[0].exp + elif rv.is_Function and hasattr(rv, "inverse"): + if (len(rv.args) == 1 and len(rv.args[0].args) == 1 and + isinstance(rv.args[0], rv.inverse(argindex=1))): + rv = rv.args[0].args[0] + if rv.is_Pow and rv.base == S.Exp1: + if isinstance(rv.exp, log): + rv = rv.exp.args[0] + return rv + + return _bottom_up(expr, f) + + +def kroneckersimp(expr): + """ + Simplify expressions with KroneckerDelta. + + The only simplification currently attempted is to identify multiplicative cancellation: + + Examples + ======== + + >>> from sympy import KroneckerDelta, kroneckersimp + >>> from sympy.abc import i + >>> kroneckersimp(1 + KroneckerDelta(0, i) * KroneckerDelta(1, i)) + 1 + """ + def args_cancel(args1, args2): + for i1 in range(2): + for i2 in range(2): + a1 = args1[i1] + a2 = args2[i2] + a3 = args1[(i1 + 1) % 2] + a4 = args2[(i2 + 1) % 2] + if Eq(a1, a2) is S.true and Eq(a3, a4) is S.false: + return True + return False + + def cancel_kronecker_mul(m): + args = m.args + deltas = [a for a in args if isinstance(a, KroneckerDelta)] + for delta1, delta2 in subsets(deltas, 2): + args1 = delta1.args + args2 = delta2.args + if args_cancel(args1, args2): + return S.Zero * m # In case of oo etc + return m + + if not expr.has(KroneckerDelta): + return expr + + if expr.has(Piecewise): + expr = expr.rewrite(KroneckerDelta) + + newexpr = expr + expr = None + + while newexpr != expr: + expr = newexpr + newexpr = expr.replace(lambda e: isinstance(e, Mul), cancel_kronecker_mul) + + return expr + + +def besselsimp(expr): + """ + Simplify bessel-type functions. + + Explanation + =========== + + This routine tries to simplify bessel-type functions. Currently it only + works on the Bessel J and I functions, however. It works by looking at all + such functions in turn, and eliminating factors of "I" and "-1" (actually + their polar equivalents) in front of the argument. Then, functions of + half-integer order are rewritten using trigonometric functions and + functions of integer order (> 1) are rewritten using functions + of low order. Finally, if the expression was changed, compute + factorization of the result with factor(). + + >>> from sympy import besselj, besseli, besselsimp, polar_lift, I, S + >>> from sympy.abc import z, nu + >>> besselsimp(besselj(nu, z*polar_lift(-1))) + exp(I*pi*nu)*besselj(nu, z) + >>> besselsimp(besseli(nu, z*polar_lift(-I))) + exp(-I*pi*nu/2)*besselj(nu, z) + >>> besselsimp(besseli(S(-1)/2, z)) + sqrt(2)*cosh(z)/(sqrt(pi)*sqrt(z)) + >>> besselsimp(z*besseli(0, z) + z*(besseli(2, z))/2 + besseli(1, z)) + 3*z*besseli(0, z)/2 + """ + # TODO + # - better algorithm? + # - simplify (cos(pi*b)*besselj(b,z) - besselj(-b,z))/sin(pi*b) ... + # - use contiguity relations? + + def replacer(fro, to, factors): + factors = set(factors) + + def repl(nu, z): + if factors.intersection(Mul.make_args(z)): + return to(nu, z) + return fro(nu, z) + return repl + + def torewrite(fro, to): + def tofunc(nu, z): + return fro(nu, z).rewrite(to) + return tofunc + + def tominus(fro): + def tofunc(nu, z): + return exp(I*pi*nu)*fro(nu, exp_polar(-I*pi)*z) + return tofunc + + orig_expr = expr + + ifactors = [I, exp_polar(I*pi/2), exp_polar(-I*pi/2)] + expr = expr.replace( + besselj, replacer(besselj, + torewrite(besselj, besseli), ifactors)) + expr = expr.replace( + besseli, replacer(besseli, + torewrite(besseli, besselj), ifactors)) + + minusfactors = [-1, exp_polar(I*pi)] + expr = expr.replace( + besselj, replacer(besselj, tominus(besselj), minusfactors)) + expr = expr.replace( + besseli, replacer(besseli, tominus(besseli), minusfactors)) + + z0 = Dummy('z') + + def expander(fro): + def repl(nu, z): + if (nu % 1) == S.Half: + return simplify(trigsimp(unpolarify( + fro(nu, z0).rewrite(besselj).rewrite(jn).expand( + func=True)).subs(z0, z))) + elif nu.is_Integer and nu > 1: + return fro(nu, z).expand(func=True) + return fro(nu, z) + return repl + + expr = expr.replace(besselj, expander(besselj)) + expr = expr.replace(bessely, expander(bessely)) + expr = expr.replace(besseli, expander(besseli)) + expr = expr.replace(besselk, expander(besselk)) + + def _bessel_simp_recursion(expr): + + def _use_recursion(bessel, expr): + while True: + bessels = expr.find(lambda x: isinstance(x, bessel)) + try: + for ba in sorted(bessels, key=lambda x: re(x.args[0])): + a, x = ba.args + bap1 = bessel(a+1, x) + bap2 = bessel(a+2, x) + if expr.has(bap1) and expr.has(bap2): + expr = expr.subs(ba, 2*(a+1)/x*bap1 - bap2) + break + else: + return expr + except (ValueError, TypeError): + return expr + if expr.has(besselj): + expr = _use_recursion(besselj, expr) + if expr.has(bessely): + expr = _use_recursion(bessely, expr) + return expr + + expr = _bessel_simp_recursion(expr) + if expr != orig_expr: + expr = expr.factor() + + return expr + + +def nthroot(expr, n, max_len=4, prec=15): + """ + Compute a real nth-root of a sum of surds. + + Parameters + ========== + + expr : sum of surds + n : integer + max_len : maximum number of surds passed as constants to ``nsimplify`` + + Algorithm + ========= + + First ``nsimplify`` is used to get a candidate root; if it is not a + root the minimal polynomial is computed; the answer is one of its + roots. + + Examples + ======== + + >>> from sympy.simplify.simplify import nthroot + >>> from sympy import sqrt + >>> nthroot(90 + 34*sqrt(7), 3) + sqrt(7) + 3 + + """ + expr = sympify(expr) + n = sympify(n) + p = expr**Rational(1, n) + if not n.is_integer: + return p + if not _is_sum_surds(expr): + return p + surds = [] + coeff_muls = [x.as_coeff_Mul() for x in expr.args] + for x, y in coeff_muls: + if not x.is_rational: + return p + if y is S.One: + continue + if not (y.is_Pow and y.exp == S.Half and y.base.is_integer): + return p + surds.append(y) + surds.sort() + surds = surds[:max_len] + if expr < 0 and n % 2 == 1: + p = (-expr)**Rational(1, n) + a = nsimplify(p, constants=surds) + res = a if _mexpand(a**n) == _mexpand(-expr) else p + return -res + a = nsimplify(p, constants=surds) + if _mexpand(a) is not _mexpand(p) and _mexpand(a**n) == _mexpand(expr): + return _mexpand(a) + expr = _nthroot_solve(expr, n, prec) + if expr is None: + return p + return expr + + +def nsimplify(expr, constants=(), tolerance=None, full=False, rational=None, + rational_conversion='base10'): + """ + Find a simple representation for a number or, if there are free symbols or + if ``rational=True``, then replace Floats with their Rational equivalents. If + no change is made and rational is not False then Floats will at least be + converted to Rationals. + + Explanation + =========== + + For numerical expressions, a simple formula that numerically matches the + given numerical expression is sought (and the input should be possible + to evalf to a precision of at least 30 digits). + + Optionally, a list of (rationally independent) constants to + include in the formula may be given. + + A lower tolerance may be set to find less exact matches. If no tolerance + is given then the least precise value will set the tolerance (e.g. Floats + default to 15 digits of precision, so would be tolerance=10**-15). + + With ``full=True``, a more extensive search is performed + (this is useful to find simpler numbers when the tolerance + is set low). + + When converting to rational, if rational_conversion='base10' (the default), then + convert floats to rationals using their base-10 (string) representation. + When rational_conversion='exact' it uses the exact, base-2 representation. + + Examples + ======== + + >>> from sympy import nsimplify, sqrt, GoldenRatio, exp, I, pi + >>> nsimplify(4/(1+sqrt(5)), [GoldenRatio]) + -2 + 2*GoldenRatio + >>> nsimplify((1/(exp(3*pi*I/5)+1))) + 1/2 - I*sqrt(sqrt(5)/10 + 1/4) + >>> nsimplify(I**I, [pi]) + exp(-pi/2) + >>> nsimplify(pi, tolerance=0.01) + 22/7 + + >>> nsimplify(0.333333333333333, rational=True, rational_conversion='exact') + 6004799503160655/18014398509481984 + >>> nsimplify(0.333333333333333, rational=True) + 1/3 + + See Also + ======== + + sympy.core.function.nfloat + + """ + try: + return sympify(as_int(expr)) + except (TypeError, ValueError): + pass + expr = sympify(expr).xreplace({ + Float('inf'): S.Infinity, + Float('-inf'): S.NegativeInfinity, + }) + if expr is S.Infinity or expr is S.NegativeInfinity: + return expr + if rational or expr.free_symbols: + return _real_to_rational(expr, tolerance, rational_conversion) + + # SymPy's default tolerance for Rationals is 15; other numbers may have + # lower tolerances set, so use them to pick the largest tolerance if None + # was given + if tolerance is None: + tolerance = 10**-min([15] + + [mpmath.libmp.libmpf.prec_to_dps(n._prec) + for n in expr.atoms(Float)]) + # XXX should prec be set independent of tolerance or should it be computed + # from tolerance? + prec = 30 + bprec = int(prec*3.33) + + constants_dict = {} + for constant in constants: + constant = sympify(constant) + v = constant.evalf(prec) + if not v.is_Float: + raise ValueError("constants must be real-valued") + constants_dict[str(constant)] = v._to_mpmath(bprec) + + exprval = expr.evalf(prec, chop=True) + re, im = exprval.as_real_imag() + + # safety check to make sure that this evaluated to a number + if not (re.is_Number and im.is_Number): + return expr + + def nsimplify_real(x): + orig = mpmath.mp.dps + xv = x._to_mpmath(bprec) + try: + # We'll be happy with low precision if a simple fraction + if not (tolerance or full): + mpmath.mp.dps = 15 + rat = mpmath.pslq([xv, 1]) + if rat is not None: + return Rational(-int(rat[1]), int(rat[0])) + mpmath.mp.dps = prec + newexpr = mpmath.identify(xv, constants=constants_dict, + tol=tolerance, full=full) + if not newexpr: + raise ValueError + if full: + newexpr = newexpr[0] + expr = sympify(newexpr) + if x and not expr: # don't let x become 0 + raise ValueError + if expr.is_finite is False and xv not in [mpmath.inf, mpmath.ninf]: + raise ValueError + return expr + finally: + # even though there are returns above, this is executed + # before leaving + mpmath.mp.dps = orig + try: + if re: + re = nsimplify_real(re) + if im: + im = nsimplify_real(im) + except ValueError: + if rational is None: + return _real_to_rational(expr, rational_conversion=rational_conversion) + return expr + + rv = re + im*S.ImaginaryUnit + # if there was a change or rational is explicitly not wanted + # return the value, else return the Rational representation + if rv != expr or rational is False: + return rv + return _real_to_rational(expr, rational_conversion=rational_conversion) + + +def _real_to_rational(expr, tolerance=None, rational_conversion='base10'): + """ + Replace all reals in expr with rationals. + + Examples + ======== + + >>> from sympy.simplify.simplify import _real_to_rational + >>> from sympy.abc import x + + >>> _real_to_rational(.76 + .1*x**.5) + sqrt(x)/10 + 19/25 + + If rational_conversion='base10', this uses the base-10 string. If + rational_conversion='exact', the exact, base-2 representation is used. + + >>> _real_to_rational(0.333333333333333, rational_conversion='exact') + 6004799503160655/18014398509481984 + >>> _real_to_rational(0.333333333333333) + 1/3 + + """ + expr = _sympify(expr) + inf = Float('inf') + p = expr + reps = {} + reduce_num = None + if tolerance is not None and tolerance < 1: + reduce_num = ceiling(1/tolerance) + for fl in p.atoms(Float): + key = fl + if reduce_num is not None: + r = Rational(fl).limit_denominator(reduce_num) + elif (tolerance is not None and tolerance >= 1 and + fl.is_Integer is False): + r = Rational(tolerance*round(fl/tolerance) + ).limit_denominator(int(tolerance)) + else: + if rational_conversion == 'exact': + r = Rational(fl) + reps[key] = r + continue + elif rational_conversion != 'base10': + raise ValueError("rational_conversion must be 'base10' or 'exact'") + + r = nsimplify(fl, rational=False) + # e.g. log(3).n() -> log(3) instead of a Rational + if fl and not r: + r = Rational(fl) + elif not r.is_Rational: + if fl in (inf, -inf): + r = S.ComplexInfinity + elif fl < 0: + fl = -fl + d = Pow(10, int(mpmath.log(fl)/mpmath.log(10))) + r = -Rational(str(fl/d))*d + elif fl > 0: + d = Pow(10, int(mpmath.log(fl)/mpmath.log(10))) + r = Rational(str(fl/d))*d + else: + r = S.Zero + reps[key] = r + return p.subs(reps, simultaneous=True) + + +def clear_coefficients(expr, rhs=S.Zero): + """Return `p, r` where `p` is the expression obtained when Rational + additive and multiplicative coefficients of `expr` have been stripped + away in a naive fashion (i.e. without simplification). The operations + needed to remove the coefficients will be applied to `rhs` and returned + as `r`. + + Examples + ======== + + >>> from sympy.simplify.simplify import clear_coefficients + >>> from sympy.abc import x, y + >>> from sympy import Dummy + >>> expr = 4*y*(6*x + 3) + >>> clear_coefficients(expr - 2) + (y*(2*x + 1), 1/6) + + When solving 2 or more expressions like `expr = a`, + `expr = b`, etc..., it is advantageous to provide a Dummy symbol + for `rhs` and simply replace it with `a`, `b`, etc... in `r`. + + >>> rhs = Dummy('rhs') + >>> clear_coefficients(expr, rhs) + (y*(2*x + 1), _rhs/12) + >>> _[1].subs(rhs, 2) + 1/6 + """ + was = None + free = expr.free_symbols + if expr.is_Rational: + return (S.Zero, rhs - expr) + while expr and was != expr: + was = expr + m, expr = ( + expr.as_content_primitive() + if free else + factor_terms(expr).as_coeff_Mul(rational=True)) + rhs /= m + c, expr = expr.as_coeff_Add(rational=True) + rhs -= c + expr = signsimp(expr, evaluate = False) + if expr.could_extract_minus_sign(): + expr = -expr + rhs = -rhs + return expr, rhs + +def nc_simplify(expr, deep=True): + ''' + Simplify a non-commutative expression composed of multiplication + and raising to a power by grouping repeated subterms into one power. + Priority is given to simplifications that give the fewest number + of arguments in the end (for example, in a*b*a*b*c*a*b*c simplifying + to (a*b)**2*c*a*b*c gives 5 arguments while a*b*(a*b*c)**2 has 3). + If ``expr`` is a sum of such terms, the sum of the simplified terms + is returned. + + Keyword argument ``deep`` controls whether or not subexpressions + nested deeper inside the main expression are simplified. See examples + below. Setting `deep` to `False` can save time on nested expressions + that do not need simplifying on all levels. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.simplify.simplify import nc_simplify + >>> a, b, c = symbols("a b c", commutative=False) + >>> nc_simplify(a*b*a*b*c*a*b*c) + a*b*(a*b*c)**2 + >>> expr = a**2*b*a**4*b*a**4 + >>> nc_simplify(expr) + a**2*(b*a**4)**2 + >>> nc_simplify(a*b*a*b*c**2*(a*b)**2*c**2) + ((a*b)**2*c**2)**2 + >>> nc_simplify(a*b*a*b + 2*a*c*a**2*c*a**2*c*a) + (a*b)**2 + 2*(a*c*a)**3 + >>> nc_simplify(b**-1*a**-1*(a*b)**2) + a*b + >>> nc_simplify(a**-1*b**-1*c*a) + (b*a)**(-1)*c*a + >>> expr = (a*b*a*b)**2*a*c*a*c + >>> nc_simplify(expr) + (a*b)**4*(a*c)**2 + >>> nc_simplify(expr, deep=False) + (a*b*a*b)**2*(a*c)**2 + + ''' + if isinstance(expr, MatrixExpr): + expr = expr.doit(inv_expand=False) + _Add, _Mul, _Pow, _Symbol = MatAdd, MatMul, MatPow, MatrixSymbol + else: + _Add, _Mul, _Pow, _Symbol = Add, Mul, Pow, Symbol + + # =========== Auxiliary functions ======================== + def _overlaps(args): + # Calculate a list of lists m such that m[i][j] contains the lengths + # of all possible overlaps between args[:i+1] and args[i+1+j:]. + # An overlap is a suffix of the prefix that matches a prefix + # of the suffix. + # For example, let expr=c*a*b*a*b*a*b*a*b. Then m[3][0] contains + # the lengths of overlaps of c*a*b*a*b with a*b*a*b. The overlaps + # are a*b*a*b, a*b and the empty word so that m[3][0]=[4,2,0]. + # All overlaps rather than only the longest one are recorded + # because this information helps calculate other overlap lengths. + m = [[([1, 0] if a == args[0] else [0]) for a in args[1:]]] + for i in range(1, len(args)): + overlaps = [] + j = 0 + for j in range(len(args) - i - 1): + overlap = [] + for v in m[i-1][j+1]: + if j + i + 1 + v < len(args) and args[i] == args[j+i+1+v]: + overlap.append(v + 1) + overlap += [0] + overlaps.append(overlap) + m.append(overlaps) + return m + + def _reduce_inverses(_args): + # replace consecutive negative powers by an inverse + # of a product of positive powers, e.g. a**-1*b**-1*c + # will simplify to (a*b)**-1*c; + # return that new args list and the number of negative + # powers in it (inv_tot) + inv_tot = 0 # total number of inverses + inverses = [] + args = [] + for arg in _args: + if isinstance(arg, _Pow) and arg.args[1].is_extended_negative: + inverses = [arg**-1] + inverses + inv_tot += 1 + else: + if len(inverses) == 1: + args.append(inverses[0]**-1) + elif len(inverses) > 1: + args.append(_Pow(_Mul(*inverses), -1)) + inv_tot -= len(inverses) - 1 + inverses = [] + args.append(arg) + if inverses: + args.append(_Pow(_Mul(*inverses), -1)) + inv_tot -= len(inverses) - 1 + return inv_tot, tuple(args) + + def get_score(s): + # compute the number of arguments of s + # (including in nested expressions) overall + # but ignore exponents + if isinstance(s, _Pow): + return get_score(s.args[0]) + elif isinstance(s, (_Add, _Mul)): + return sum(get_score(a) for a in s.args) + return 1 + + def compare(s, alt_s): + # compare two possible simplifications and return a + # "better" one + if s != alt_s and get_score(alt_s) < get_score(s): + return alt_s + return s + # ======================================================== + + if not isinstance(expr, (_Add, _Mul, _Pow)) or expr.is_commutative: + return expr + args = expr.args[:] + if isinstance(expr, _Pow): + if deep: + return _Pow(nc_simplify(args[0]), args[1]).doit() + else: + return expr + elif isinstance(expr, _Add): + return _Add(*[nc_simplify(a, deep=deep) for a in args]).doit() + else: + # get the non-commutative part + c_args, args = expr.args_cnc() + com_coeff = Mul(*c_args) + if not equal_valued(com_coeff, 1): + return com_coeff*nc_simplify(expr/com_coeff, deep=deep) + + inv_tot, args = _reduce_inverses(args) + # if most arguments are negative, work with the inverse + # of the expression, e.g. a**-1*b*a**-1*c**-1 will become + # (c*a*b**-1*a)**-1 at the end so can work with c*a*b**-1*a + invert = False + if inv_tot > len(args)/2: + invert = True + args = [a**-1 for a in args[::-1]] + + if deep: + args = tuple(nc_simplify(a) for a in args) + + m = _overlaps(args) + + # simps will be {subterm: end} where `end` is the ending + # index of a sequence of repetitions of subterm; + # this is for not wasting time with subterms that are part + # of longer, already considered sequences + simps = {} + + post = 1 + pre = 1 + + # the simplification coefficient is the number of + # arguments by which contracting a given sequence + # would reduce the word; e.g. in a*b*a*b*c*a*b*c, + # contracting a*b*a*b to (a*b)**2 removes 3 arguments + # while a*b*c*a*b*c to (a*b*c)**2 removes 6. It's + # better to contract the latter so simplification + # with a maximum simplification coefficient will be chosen + max_simp_coeff = 0 + simp = None # information about future simplification + + for i in range(1, len(args)): + simp_coeff = 0 + l = 0 # length of a subterm + p = 0 # the power of a subterm + if i < len(args) - 1: + rep = m[i][0] + start = i # starting index of the repeated sequence + end = i+1 # ending index of the repeated sequence + if i == len(args)-1 or rep == [0]: + # no subterm is repeated at this stage, at least as + # far as the arguments are concerned - there may be + # a repetition if powers are taken into account + if (isinstance(args[i], _Pow) and + not isinstance(args[i].args[0], _Symbol)): + subterm = args[i].args[0].args + l = len(subterm) + if args[i-l:i] == subterm: + # e.g. a*b in a*b*(a*b)**2 is not repeated + # in args (= [a, b, (a*b)**2]) but it + # can be matched here + p += 1 + start -= l + if args[i+1:i+1+l] == subterm: + # e.g. a*b in (a*b)**2*a*b + p += 1 + end += l + if p: + p += args[i].args[1] + else: + continue + else: + l = rep[0] # length of the longest repeated subterm at this point + start -= l - 1 + subterm = args[start:end] + p = 2 + end += l + + if subterm in simps and simps[subterm] >= start: + # the subterm is part of a sequence that + # has already been considered + continue + + # count how many times it's repeated + while end < len(args): + if l in m[end-1][0]: + p += 1 + end += l + elif isinstance(args[end], _Pow) and args[end].args[0].args == subterm: + # for cases like a*b*a*b*(a*b)**2*a*b + p += args[end].args[1] + end += 1 + else: + break + + # see if another match can be made, e.g. + # for b*a**2 in b*a**2*b*a**3 or a*b in + # a**2*b*a*b + + pre_exp = 0 + pre_arg = 1 + if start - l >= 0 and args[start-l+1:start] == subterm[1:]: + if isinstance(subterm[0], _Pow): + pre_arg = subterm[0].args[0] + exp = subterm[0].args[1] + else: + pre_arg = subterm[0] + exp = 1 + if isinstance(args[start-l], _Pow) and args[start-l].args[0] == pre_arg: + pre_exp = args[start-l].args[1] - exp + start -= l + p += 1 + elif args[start-l] == pre_arg: + pre_exp = 1 - exp + start -= l + p += 1 + + post_exp = 0 + post_arg = 1 + if end + l - 1 < len(args) and args[end:end+l-1] == subterm[:-1]: + if isinstance(subterm[-1], _Pow): + post_arg = subterm[-1].args[0] + exp = subterm[-1].args[1] + else: + post_arg = subterm[-1] + exp = 1 + if isinstance(args[end+l-1], _Pow) and args[end+l-1].args[0] == post_arg: + post_exp = args[end+l-1].args[1] - exp + end += l + p += 1 + elif args[end+l-1] == post_arg: + post_exp = 1 - exp + end += l + p += 1 + + # Consider a*b*a**2*b*a**2*b*a: + # b*a**2 is explicitly repeated, but note + # that in this case a*b*a is also repeated + # so there are two possible simplifications: + # a*(b*a**2)**3*a**-1 or (a*b*a)**3 + # The latter is obviously simpler. + # But in a*b*a**2*b**2*a**2 the simplifications are + # a*(b*a**2)**2 and (a*b*a)**3*a in which case + # it's better to stick with the shorter subterm + if post_exp and exp % 2 == 0 and start > 0: + exp = exp/2 + _pre_exp = 1 + _post_exp = 1 + if isinstance(args[start-1], _Pow) and args[start-1].args[0] == post_arg: + _post_exp = post_exp + exp + _pre_exp = args[start-1].args[1] - exp + elif args[start-1] == post_arg: + _post_exp = post_exp + exp + _pre_exp = 1 - exp + if _pre_exp == 0 or _post_exp == 0: + if not pre_exp: + start -= 1 + post_exp = _post_exp + pre_exp = _pre_exp + pre_arg = post_arg + subterm = (post_arg**exp,) + subterm[:-1] + (post_arg**exp,) + + simp_coeff += end-start + + if post_exp: + simp_coeff -= 1 + if pre_exp: + simp_coeff -= 1 + + simps[subterm] = end + + if simp_coeff > max_simp_coeff: + max_simp_coeff = simp_coeff + simp = (start, _Mul(*subterm), p, end, l) + pre = pre_arg**pre_exp + post = post_arg**post_exp + + if simp: + subterm = _Pow(nc_simplify(simp[1], deep=deep), simp[2]) + pre = nc_simplify(_Mul(*args[:simp[0]])*pre, deep=deep) + post = post*nc_simplify(_Mul(*args[simp[3]:]), deep=deep) + simp = pre*subterm*post + if pre != 1 or post != 1: + # new simplifications may be possible but no need + # to recurse over arguments + simp = nc_simplify(simp, deep=False) + else: + simp = _Mul(*args) + + if invert: + simp = _Pow(simp, -1) + + # see if factor_nc(expr) is simplified better + if not isinstance(expr, MatrixExpr): + f_expr = factor_nc(expr) + if f_expr != expr: + alt_simp = nc_simplify(f_expr, deep=deep) + simp = compare(simp, alt_simp) + else: + simp = simp.doit(inv_expand=False) + return simp + + +def dotprodsimp(expr, withsimp=False): + """Simplification for a sum of products targeted at the kind of blowup that + occurs during summation of products. Intended to reduce expression blowup + during matrix multiplication or other similar operations. Only works with + algebraic expressions and does not recurse into non. + + Parameters + ========== + + withsimp : bool, optional + Specifies whether a flag should be returned along with the expression + to indicate roughly whether simplification was successful. It is used + in ``MatrixArithmetic._eval_pow_by_recursion`` to avoid attempting to + simplify an expression repetitively which does not simplify. + """ + + def count_ops_alg(expr): + """Optimized count algebraic operations with no recursion into + non-algebraic args that ``core.function.count_ops`` does. Also returns + whether rational functions may be present according to negative + exponents of powers or non-number fractions. + + Returns + ======= + + ops, ratfunc : int, bool + ``ops`` is the number of algebraic operations starting at the top + level expression (not recursing into non-alg children). ``ratfunc`` + specifies whether the expression MAY contain rational functions + which ``cancel`` MIGHT optimize. + """ + + ops = 0 + args = [expr] + ratfunc = False + + while args: + a = args.pop() + + if not isinstance(a, Basic): + continue + + if a.is_Rational: + if a is not S.One: # -1/3 = NEG + DIV + ops += bool (a.p < 0) + bool (a.q != 1) + + elif a.is_Mul: + if a.could_extract_minus_sign(): + ops += 1 + if a.args[0] is S.NegativeOne: + a = a.as_two_terms()[1] + else: + a = -a + + n, d = fraction(a) + + if n.is_Integer: + ops += 1 + bool (n < 0) + args.append(d) # won't be -Mul but could be Add + + elif d is not S.One: + if not d.is_Integer: + args.append(d) + ratfunc=True + + ops += 1 + args.append(n) # could be -Mul + + else: + ops += len(a.args) - 1 + args.extend(a.args) + + elif a.is_Add: + laargs = len(a.args) + negs = 0 + + for ai in a.args: + if ai.could_extract_minus_sign(): + negs += 1 + ai = -ai + args.append(ai) + + ops += laargs - (negs != laargs) # -x - y = NEG + SUB + + elif a.is_Pow: + ops += 1 + args.append(a.base) + + if not ratfunc: + ratfunc = a.exp.is_negative is not False + + return ops, ratfunc + + def nonalg_subs_dummies(expr, dummies): + """Substitute dummy variables for non-algebraic expressions to avoid + evaluation of non-algebraic terms that ``polys.polytools.cancel`` does. + """ + + if not expr.args: + return expr + + if expr.is_Add or expr.is_Mul or expr.is_Pow: + args = None + + for i, a in enumerate(expr.args): + c = nonalg_subs_dummies(a, dummies) + + if c is a: + continue + + if args is None: + args = list(expr.args) + + args[i] = c + + if args is None: + return expr + + return expr.func(*args) + + return dummies.setdefault(expr, Dummy()) + + simplified = False # doesn't really mean simplified, rather "can simplify again" + + if isinstance(expr, Basic) and (expr.is_Add or expr.is_Mul or expr.is_Pow): + expr2 = expr.expand(deep=True, modulus=None, power_base=False, + power_exp=False, mul=True, log=False, multinomial=True, basic=False) + + if expr2 != expr: + expr = expr2 + simplified = True + + exprops, ratfunc = count_ops_alg(expr) + + if exprops >= 6: # empirically tested cutoff for expensive simplification + if ratfunc: + dummies = {} + expr2 = nonalg_subs_dummies(expr, dummies) + + if expr2 is expr or count_ops_alg(expr2)[0] >= 6: # check again after substitution + expr3 = cancel(expr2) + + if expr3 != expr2: + expr = expr3.subs([(d, e) for e, d in dummies.items()]) + simplified = True + + # very special case: x/(x-1) - 1/(x-1) -> 1 + elif (exprops == 5 and expr.is_Add and expr.args [0].is_Mul and + expr.args [1].is_Mul and expr.args [0].args [-1].is_Pow and + expr.args [1].args [-1].is_Pow and + expr.args [0].args [-1].exp is S.NegativeOne and + expr.args [1].args [-1].exp is S.NegativeOne): + + expr2 = together (expr) + expr2ops = count_ops_alg(expr2)[0] + + if expr2ops < exprops: + expr = expr2 + simplified = True + + else: + simplified = True + + return (expr, simplified) if withsimp else expr + + +bottom_up = deprecated( + """ + Using bottom_up from the sympy.simplify.simplify submodule is + deprecated. + + Instead, use bottom_up from the top-level sympy namespace, like + + sympy.bottom_up + """, + deprecated_since_version="1.10", + active_deprecations_target="deprecated-traversal-functions-moved", +)(_bottom_up) + + +# XXX: This function really should either be private API or exported in the +# top-level sympy/__init__.py +walk = deprecated( + """ + Using walk from the sympy.simplify.simplify submodule is + deprecated. + + Instead, use walk from sympy.core.traversal.walk + """, + deprecated_since_version="1.10", + active_deprecations_target="deprecated-traversal-functions-moved", +)(_walk) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/sqrtdenest.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/sqrtdenest.py new file mode 100644 index 0000000000000000000000000000000000000000..d266de7e62a4b7d37a2109f7091ff91e4df7c79d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/sqrtdenest.py @@ -0,0 +1,678 @@ +from sympy.core import Add, Expr, Mul, S, sympify +from sympy.core.function import _mexpand, count_ops, expand_mul +from sympy.core.sorting import default_sort_key +from sympy.core.symbol import Dummy +from sympy.functions import root, sign, sqrt +from sympy.polys import Poly, PolynomialError + + +def is_sqrt(expr): + """Return True if expr is a sqrt, otherwise False.""" + + return expr.is_Pow and expr.exp.is_Rational and abs(expr.exp) is S.Half + + +def sqrt_depth(p) -> int: + """Return the maximum depth of any square root argument of p. + + >>> from sympy.functions.elementary.miscellaneous import sqrt + >>> from sympy.simplify.sqrtdenest import sqrt_depth + + Neither of these square roots contains any other square roots + so the depth is 1: + + >>> sqrt_depth(1 + sqrt(2)*(1 + sqrt(3))) + 1 + + The sqrt(3) is contained within a square root so the depth is + 2: + + >>> sqrt_depth(1 + sqrt(2)*sqrt(1 + sqrt(3))) + 2 + """ + if p is S.ImaginaryUnit: + return 1 + if p.is_Atom: + return 0 + if p.is_Add or p.is_Mul: + return max(sqrt_depth(x) for x in p.args) + if is_sqrt(p): + return sqrt_depth(p.base) + 1 + return 0 + + +def is_algebraic(p): + """Return True if p is comprised of only Rationals or square roots + of Rationals and algebraic operations. + + Examples + ======== + + >>> from sympy.functions.elementary.miscellaneous import sqrt + >>> from sympy.simplify.sqrtdenest import is_algebraic + >>> from sympy import cos + >>> is_algebraic(sqrt(2)*(3/(sqrt(7) + sqrt(5)*sqrt(2)))) + True + >>> is_algebraic(sqrt(2)*(3/(sqrt(7) + sqrt(5)*cos(2)))) + False + """ + + if p.is_Rational: + return True + elif p.is_Atom: + return False + elif is_sqrt(p) or p.is_Pow and p.exp.is_Integer: + return is_algebraic(p.base) + elif p.is_Add or p.is_Mul: + return all(is_algebraic(x) for x in p.args) + else: + return False + + +def _subsets(n): + """ + Returns all possible subsets of the set (0, 1, ..., n-1) except the + empty set, listed in reversed lexicographical order according to binary + representation, so that the case of the fourth root is treated last. + + Examples + ======== + + >>> from sympy.simplify.sqrtdenest import _subsets + >>> _subsets(2) + [[1, 0], [0, 1], [1, 1]] + + """ + if n == 1: + a = [[1]] + elif n == 2: + a = [[1, 0], [0, 1], [1, 1]] + elif n == 3: + a = [[1, 0, 0], [0, 1, 0], [1, 1, 0], + [0, 0, 1], [1, 0, 1], [0, 1, 1], [1, 1, 1]] + else: + b = _subsets(n - 1) + a0 = [x + [0] for x in b] + a1 = [x + [1] for x in b] + a = a0 + [[0]*(n - 1) + [1]] + a1 + return a + + +def sqrtdenest(expr, max_iter=3): + """Denests sqrts in an expression that contain other square roots + if possible, otherwise returns the expr unchanged. This is based on the + algorithms of [1]. + + Examples + ======== + + >>> from sympy.simplify.sqrtdenest import sqrtdenest + >>> from sympy import sqrt + >>> sqrtdenest(sqrt(5 + 2 * sqrt(6))) + sqrt(2) + sqrt(3) + + See Also + ======== + + sympy.solvers.solvers.unrad + + References + ========== + + .. [1] https://web.archive.org/web/20210806201615/https://researcher.watson.ibm.com/researcher/files/us-fagin/symb85.pdf + + .. [2] D. J. Jeffrey and A. D. Rich, 'Symplifying Square Roots of Square Roots + by Denesting' (available at https://www.cybertester.com/data/denest.pdf) + + """ + expr = expand_mul(expr) + for i in range(max_iter): + z = _sqrtdenest0(expr) + if expr == z: + return expr + expr = z + return expr + + +def _sqrt_match(p): + """Return [a, b, r] for p.match(a + b*sqrt(r)) where, in addition to + matching, sqrt(r) also has then maximal sqrt_depth among addends of p. + + Examples + ======== + + >>> from sympy.functions.elementary.miscellaneous import sqrt + >>> from sympy.simplify.sqrtdenest import _sqrt_match + >>> _sqrt_match(1 + sqrt(2) + sqrt(2)*sqrt(3) + 2*sqrt(1+sqrt(5))) + [1 + sqrt(2) + sqrt(6), 2, 1 + sqrt(5)] + """ + from sympy.simplify.radsimp import split_surds + + p = _mexpand(p) + if p.is_Number: + res = (p, S.Zero, S.Zero) + elif p.is_Add: + pargs = sorted(p.args, key=default_sort_key) + sqargs = [x**2 for x in pargs] + if all(sq.is_Rational and sq.is_positive for sq in sqargs): + r, b, a = split_surds(p) + res = a, b, r + return list(res) + # to make the process canonical, the argument is included in the tuple + # so when the max is selected, it will be the largest arg having a + # given depth + v = [(sqrt_depth(x), x, i) for i, x in enumerate(pargs)] + nmax = max(v, key=default_sort_key) + if nmax[0] == 0: + res = [] + else: + # select r + depth, _, i = nmax + r = pargs.pop(i) + v.pop(i) + b = S.One + if r.is_Mul: + bv = [] + rv = [] + for x in r.args: + if sqrt_depth(x) < depth: + bv.append(x) + else: + rv.append(x) + b = Mul._from_args(bv) + r = Mul._from_args(rv) + # collect terms containing r + a1 = [] + b1 = [b] + for x in v: + if x[0] < depth: + a1.append(x[1]) + else: + x1 = x[1] + if x1 == r: + b1.append(1) + else: + if x1.is_Mul: + x1args = list(x1.args) + if r in x1args: + x1args.remove(r) + b1.append(Mul(*x1args)) + else: + a1.append(x[1]) + else: + a1.append(x[1]) + a = Add(*a1) + b = Add(*b1) + res = (a, b, r**2) + else: + b, r = p.as_coeff_Mul() + if is_sqrt(r): + res = (S.Zero, b, r**2) + else: + res = [] + return list(res) + + +class SqrtdenestStopIteration(StopIteration): + pass + + +def _sqrtdenest0(expr): + """Returns expr after denesting its arguments.""" + + if is_sqrt(expr): + n, d = expr.as_numer_denom() + if d is S.One: # n is a square root + if n.base.is_Add: + args = sorted(n.base.args, key=default_sort_key) + if len(args) > 2 and all((x**2).is_Integer for x in args): + try: + return _sqrtdenest_rec(n) + except SqrtdenestStopIteration: + pass + expr = sqrt(_mexpand(Add(*[_sqrtdenest0(x) for x in args]))) + return _sqrtdenest1(expr) + else: + n, d = [_sqrtdenest0(i) for i in (n, d)] + return n/d + + if isinstance(expr, Add): + cs = [] + args = [] + for arg in expr.args: + c, a = arg.as_coeff_Mul() + cs.append(c) + args.append(a) + + if all(c.is_Rational for c in cs) and all(is_sqrt(arg) for arg in args): + return _sqrt_ratcomb(cs, args) + + if isinstance(expr, Expr): + args = expr.args + if args: + return expr.func(*[_sqrtdenest0(a) for a in args]) + return expr + + +def _sqrtdenest_rec(expr): + """Helper that denests the square root of three or more surds. + + Explanation + =========== + + It returns the denested expression; if it cannot be denested it + throws SqrtdenestStopIteration + + Algorithm: expr.base is in the extension Q_m = Q(sqrt(r_1),..,sqrt(r_k)); + split expr.base = a + b*sqrt(r_k), where `a` and `b` are on + Q_(m-1) = Q(sqrt(r_1),..,sqrt(r_(k-1))); then a**2 - b**2*r_k is + on Q_(m-1); denest sqrt(a**2 - b**2*r_k) and so on. + See [1], section 6. + + Examples + ======== + + >>> from sympy import sqrt + >>> from sympy.simplify.sqrtdenest import _sqrtdenest_rec + >>> _sqrtdenest_rec(sqrt(-72*sqrt(2) + 158*sqrt(5) + 498)) + -sqrt(10) + sqrt(2) + 9 + 9*sqrt(5) + >>> w=-6*sqrt(55)-6*sqrt(35)-2*sqrt(22)-2*sqrt(14)+2*sqrt(77)+6*sqrt(10)+65 + >>> _sqrtdenest_rec(sqrt(w)) + -sqrt(11) - sqrt(7) + sqrt(2) + 3*sqrt(5) + """ + from sympy.simplify.radsimp import radsimp, rad_rationalize, split_surds + if not expr.is_Pow: + return sqrtdenest(expr) + if expr.base < 0: + return sqrt(-1)*_sqrtdenest_rec(sqrt(-expr.base)) + g, a, b = split_surds(expr.base) + a = a*sqrt(g) + if a < b: + a, b = b, a + c2 = _mexpand(a**2 - b**2) + if len(c2.args) > 2: + g, a1, b1 = split_surds(c2) + a1 = a1*sqrt(g) + if a1 < b1: + a1, b1 = b1, a1 + c2_1 = _mexpand(a1**2 - b1**2) + c_1 = _sqrtdenest_rec(sqrt(c2_1)) + d_1 = _sqrtdenest_rec(sqrt(a1 + c_1)) + num, den = rad_rationalize(b1, d_1) + c = _mexpand(d_1/sqrt(2) + num/(den*sqrt(2))) + else: + c = _sqrtdenest1(sqrt(c2)) + + if sqrt_depth(c) > 1: + raise SqrtdenestStopIteration + ac = a + c + if len(ac.args) >= len(expr.args): + if count_ops(ac) >= count_ops(expr.base): + raise SqrtdenestStopIteration + d = sqrtdenest(sqrt(ac)) + if sqrt_depth(d) > 1: + raise SqrtdenestStopIteration + num, den = rad_rationalize(b, d) + r = d/sqrt(2) + num/(den*sqrt(2)) + r = radsimp(r) + return _mexpand(r) + + +def _sqrtdenest1(expr, denester=True): + """Return denested expr after denesting with simpler methods or, that + failing, using the denester.""" + + from sympy.simplify.simplify import radsimp + + if not is_sqrt(expr): + return expr + + a = expr.base + if a.is_Atom: + return expr + val = _sqrt_match(a) + if not val: + return expr + + a, b, r = val + # try a quick numeric denesting + d2 = _mexpand(a**2 - b**2*r) + if d2.is_Rational: + if d2.is_positive: + z = _sqrt_numeric_denest(a, b, r, d2) + if z is not None: + return z + else: + # fourth root case + # sqrtdenest(sqrt(3 + 2*sqrt(3))) = + # sqrt(2)*3**(1/4)/2 + sqrt(2)*3**(3/4)/2 + dr2 = _mexpand(-d2*r) + dr = sqrt(dr2) + if dr.is_Rational: + z = _sqrt_numeric_denest(_mexpand(b*r), a, r, dr2) + if z is not None: + return z/root(r, 4) + + else: + z = _sqrt_symbolic_denest(a, b, r) + if z is not None: + return z + + if not denester or not is_algebraic(expr): + return expr + + res = sqrt_biquadratic_denest(expr, a, b, r, d2) + if res: + return res + + # now call to the denester + av0 = [a, b, r, d2] + z = _denester([radsimp(expr**2)], av0, 0, sqrt_depth(expr))[0] + if av0[1] is None: + return expr + if z is not None: + if sqrt_depth(z) == sqrt_depth(expr) and count_ops(z) > count_ops(expr): + return expr + return z + return expr + + +def _sqrt_symbolic_denest(a, b, r): + """Given an expression, sqrt(a + b*sqrt(b)), return the denested + expression or None. + + Explanation + =========== + + If r = ra + rb*sqrt(rr), try replacing sqrt(rr) in ``a`` with + (y**2 - ra)/rb, and if the result is a quadratic, ca*y**2 + cb*y + cc, and + (cb + b)**2 - 4*ca*cc is 0, then sqrt(a + b*sqrt(r)) can be rewritten as + sqrt(ca*(sqrt(r) + (cb + b)/(2*ca))**2). + + Examples + ======== + + >>> from sympy.simplify.sqrtdenest import _sqrt_symbolic_denest, sqrtdenest + >>> from sympy import sqrt, Symbol + >>> from sympy.abc import x + + >>> a, b, r = 16 - 2*sqrt(29), 2, -10*sqrt(29) + 55 + >>> _sqrt_symbolic_denest(a, b, r) + sqrt(11 - 2*sqrt(29)) + sqrt(5) + + If the expression is numeric, it will be simplified: + + >>> w = sqrt(sqrt(sqrt(3) + 1) + 1) + 1 + sqrt(2) + >>> sqrtdenest(sqrt((w**2).expand())) + 1 + sqrt(2) + sqrt(1 + sqrt(1 + sqrt(3))) + + Otherwise, it will only be simplified if assumptions allow: + + >>> w = w.subs(sqrt(3), sqrt(x + 3)) + >>> sqrtdenest(sqrt((w**2).expand())) + sqrt((sqrt(sqrt(sqrt(x + 3) + 1) + 1) + 1 + sqrt(2))**2) + + Notice that the argument of the sqrt is a square. If x is made positive + then the sqrt of the square is resolved: + + >>> _.subs(x, Symbol('x', positive=True)) + sqrt(sqrt(sqrt(x + 3) + 1) + 1) + 1 + sqrt(2) + """ + + a, b, r = map(sympify, (a, b, r)) + rval = _sqrt_match(r) + if not rval: + return None + ra, rb, rr = rval + if rb: + y = Dummy('y', positive=True) + try: + newa = Poly(a.subs(sqrt(rr), (y**2 - ra)/rb), y) + except PolynomialError: + return None + if newa.degree() == 2: + ca, cb, cc = newa.all_coeffs() + cb += b + if _mexpand(cb**2 - 4*ca*cc).equals(0): + z = sqrt(ca*(sqrt(r) + cb/(2*ca))**2) + if z.is_number: + z = _mexpand(Mul._from_args(z.as_content_primitive())) + return z + + +def _sqrt_numeric_denest(a, b, r, d2): + r"""Helper that denest + $\sqrt{a + b \sqrt{r}}, d^2 = a^2 - b^2 r > 0$ + + If it cannot be denested, it returns ``None``. + """ + d = sqrt(d2) + s = a + d + # sqrt_depth(res) <= sqrt_depth(s) + 1 + # sqrt_depth(expr) = sqrt_depth(r) + 2 + # there is denesting if sqrt_depth(s) + 1 < sqrt_depth(r) + 2 + # if s**2 is Number there is a fourth root + if sqrt_depth(s) < sqrt_depth(r) + 1 or (s**2).is_Rational: + s1, s2 = sign(s), sign(b) + if s1 == s2 == -1: + s1 = s2 = 1 + res = (s1 * sqrt(a + d) + s2 * sqrt(a - d)) * sqrt(2) / 2 + return res.expand() + + +def sqrt_biquadratic_denest(expr, a, b, r, d2): + """denest expr = sqrt(a + b*sqrt(r)) + where a, b, r are linear combinations of square roots of + positive rationals on the rationals (SQRR) and r > 0, b != 0, + d2 = a**2 - b**2*r > 0 + + If it cannot denest it returns None. + + Explanation + =========== + + Search for a solution A of type SQRR of the biquadratic equation + 4*A**4 - 4*a*A**2 + b**2*r = 0 (1) + sqd = sqrt(a**2 - b**2*r) + Choosing the sqrt to be positive, the possible solutions are + A = sqrt(a/2 +/- sqd/2) + Since a, b, r are SQRR, then a**2 - b**2*r is a SQRR, + so if sqd can be denested, it is done by + _sqrtdenest_rec, and the result is a SQRR. + Similarly for A. + Examples of solutions (in both cases a and sqd are positive): + + Example of expr with solution sqrt(a/2 + sqd/2) but not + solution sqrt(a/2 - sqd/2): + expr = sqrt(-sqrt(15) - sqrt(2)*sqrt(-sqrt(5) + 5) - sqrt(3) + 8) + a = -sqrt(15) - sqrt(3) + 8; sqd = -2*sqrt(5) - 2 + 4*sqrt(3) + + Example of expr with solution sqrt(a/2 - sqd/2) but not + solution sqrt(a/2 + sqd/2): + w = 2 + r2 + r3 + (1 + r3)*sqrt(2 + r2 + 5*r3) + expr = sqrt((w**2).expand()) + a = 4*sqrt(6) + 8*sqrt(2) + 47 + 28*sqrt(3) + sqd = 29 + 20*sqrt(3) + + Define B = b/2*A; eq.(1) implies a = A**2 + B**2*r; then + expr**2 = a + b*sqrt(r) = (A + B*sqrt(r))**2 + + Examples + ======== + + >>> from sympy import sqrt + >>> from sympy.simplify.sqrtdenest import _sqrt_match, sqrt_biquadratic_denest + >>> z = sqrt((2*sqrt(2) + 4)*sqrt(2 + sqrt(2)) + 5*sqrt(2) + 8) + >>> a, b, r = _sqrt_match(z**2) + >>> d2 = a**2 - b**2*r + >>> sqrt_biquadratic_denest(z, a, b, r, d2) + sqrt(2) + sqrt(sqrt(2) + 2) + 2 + """ + from sympy.simplify.radsimp import radsimp, rad_rationalize + if r <= 0 or d2 < 0 or not b or sqrt_depth(expr.base) < 2: + return None + for x in (a, b, r): + for y in x.args: + y2 = y**2 + if not y2.is_Integer or not y2.is_positive: + return None + sqd = _mexpand(sqrtdenest(sqrt(radsimp(d2)))) + if sqrt_depth(sqd) > 1: + return None + x1, x2 = [a/2 + sqd/2, a/2 - sqd/2] + # look for a solution A with depth 1 + for x in (x1, x2): + A = sqrtdenest(sqrt(x)) + if sqrt_depth(A) > 1: + continue + Bn, Bd = rad_rationalize(b, _mexpand(2*A)) + B = Bn/Bd + z = A + B*sqrt(r) + if z < 0: + z = -z + return _mexpand(z) + return None + + +def _denester(nested, av0, h, max_depth_level): + """Denests a list of expressions that contain nested square roots. + + Explanation + =========== + + Algorithm based on . + + It is assumed that all of the elements of 'nested' share the same + bottom-level radicand. (This is stated in the paper, on page 177, in + the paragraph immediately preceding the algorithm.) + + When evaluating all of the arguments in parallel, the bottom-level + radicand only needs to be denested once. This means that calling + _denester with x arguments results in a recursive invocation with x+1 + arguments; hence _denester has polynomial complexity. + + However, if the arguments were evaluated separately, each call would + result in two recursive invocations, and the algorithm would have + exponential complexity. + + This is discussed in the paper in the middle paragraph of page 179. + """ + from sympy.simplify.simplify import radsimp + if h > max_depth_level: + return None, None + if av0[1] is None: + return None, None + if (av0[0] is None and + all(n.is_Number for n in nested)): # no arguments are nested + for f in _subsets(len(nested)): # test subset 'f' of nested + p = _mexpand(Mul(*[nested[i] for i in range(len(f)) if f[i]])) + if f.count(1) > 1 and f[-1]: + p = -p + sqp = sqrt(p) + if sqp.is_Rational: + return sqp, f # got a perfect square so return its square root. + # Otherwise, return the radicand from the previous invocation. + return sqrt(nested[-1]), [0]*len(nested) + else: + R = None + if av0[0] is not None: + values = [av0[:2]] + R = av0[2] + nested2 = [av0[3], R] + av0[0] = None + else: + values = list(filter(None, [_sqrt_match(expr) for expr in nested])) + for v in values: + if v[2]: # Since if b=0, r is not defined + if R is not None: + if R != v[2]: + av0[1] = None + return None, None + else: + R = v[2] + if R is None: + # return the radicand from the previous invocation + return sqrt(nested[-1]), [0]*len(nested) + nested2 = [_mexpand(v[0]**2) - + _mexpand(R*v[1]**2) for v in values] + [R] + d, f = _denester(nested2, av0, h + 1, max_depth_level) + if not f: + return None, None + if not any(f[i] for i in range(len(nested))): + v = values[-1] + return sqrt(v[0] + _mexpand(v[1]*d)), f + else: + p = Mul(*[nested[i] for i in range(len(nested)) if f[i]]) + v = _sqrt_match(p) + if 1 in f and f.index(1) < len(nested) - 1 and f[len(nested) - 1]: + v[0] = -v[0] + v[1] = -v[1] + if not f[len(nested)]: # Solution denests with square roots + vad = _mexpand(v[0] + d) + if vad <= 0: + # return the radicand from the previous invocation. + return sqrt(nested[-1]), [0]*len(nested) + if not(sqrt_depth(vad) <= sqrt_depth(R) + 1 or + (vad**2).is_Number): + av0[1] = None + return None, None + + sqvad = _sqrtdenest1(sqrt(vad), denester=False) + if not (sqrt_depth(sqvad) <= sqrt_depth(R) + 1): + av0[1] = None + return None, None + sqvad1 = radsimp(1/sqvad) + res = _mexpand(sqvad/sqrt(2) + (v[1]*sqrt(R)*sqvad1/sqrt(2))) + return res, f + + # sign(v[1])*sqrt(_mexpand(v[1]**2*R*vad1/2))), f + else: # Solution requires a fourth root + s2 = _mexpand(v[1]*R) + d + if s2 <= 0: + return sqrt(nested[-1]), [0]*len(nested) + FR, s = root(_mexpand(R), 4), sqrt(s2) + return _mexpand(s/(sqrt(2)*FR) + v[0]*FR/(sqrt(2)*s)), f + + +def _sqrt_ratcomb(cs, args): + """Denest rational combinations of radicals. + + Based on section 5 of [1]. + + Examples + ======== + + >>> from sympy import sqrt + >>> from sympy.simplify.sqrtdenest import sqrtdenest + >>> z = sqrt(1+sqrt(3)) + sqrt(3+3*sqrt(3)) - sqrt(10+6*sqrt(3)) + >>> sqrtdenest(z) + 0 + """ + from sympy.simplify.radsimp import radsimp + + # check if there exists a pair of sqrt that can be denested + def find(a): + n = len(a) + for i in range(n - 1): + for j in range(i + 1, n): + s1 = a[i].base + s2 = a[j].base + p = _mexpand(s1 * s2) + s = sqrtdenest(sqrt(p)) + if s != sqrt(p): + return s, i, j + + indices = find(args) + if indices is None: + return Add(*[c * arg for c, arg in zip(cs, args)]) + + s, i1, i2 = indices + + c2 = cs.pop(i2) + args.pop(i2) + a1 = args[i1] + + # replace a2 by s/a1 + cs[i1] += radsimp(c2 * s / a1.base) + + return _sqrt_ratcomb(cs, args) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_combsimp.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_combsimp.py new file mode 100644 index 0000000000000000000000000000000000000000..e56758a005fbb013c2b6ea4121b16c3434a54b03 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_combsimp.py @@ -0,0 +1,75 @@ +from sympy.core.numbers import Rational +from sympy.core.symbol import symbols +from sympy.functions.combinatorial.factorials import (FallingFactorial, RisingFactorial, binomial, factorial) +from sympy.functions.special.gamma_functions import gamma +from sympy.simplify.combsimp import combsimp +from sympy.abc import x + + +def test_combsimp(): + k, m, n = symbols('k m n', integer = True) + + assert combsimp(factorial(n)) == factorial(n) + assert combsimp(binomial(n, k)) == binomial(n, k) + + assert combsimp(factorial(n)/factorial(n - 3)) == n*(-1 + n)*(-2 + n) + assert combsimp(binomial(n + 1, k + 1)/binomial(n, k)) == (1 + n)/(1 + k) + + assert combsimp(binomial(3*n + 4, n + 1)/binomial(3*n + 1, n)) == \ + Rational(3, 2)*((3*n + 2)*(3*n + 4)/((n + 1)*(2*n + 3))) + + assert combsimp(factorial(n)**2/factorial(n - 3)) == \ + factorial(n)*n*(-1 + n)*(-2 + n) + assert combsimp(factorial(n)*binomial(n + 1, k + 1)/binomial(n, k)) == \ + factorial(n + 1)/(1 + k) + + assert combsimp(gamma(n + 3)) == factorial(n + 2) + + assert combsimp(factorial(x)) == gamma(x + 1) + + # issue 9699 + assert combsimp((n + 1)*factorial(n)) == factorial(n + 1) + assert combsimp(factorial(n)/n) == factorial(n-1) + + # issue 6658 + assert combsimp(binomial(n, n - k)) == binomial(n, k) + + # issue 6341, 7135 + assert combsimp(factorial(n)/(factorial(k)*factorial(n - k))) == \ + binomial(n, k) + assert combsimp(factorial(k)*factorial(n - k)/factorial(n)) == \ + 1/binomial(n, k) + assert combsimp(factorial(2*n)/factorial(n)**2) == binomial(2*n, n) + assert combsimp(factorial(2*n)*factorial(k)*factorial(n - k)/ + factorial(n)**3) == binomial(2*n, n)/binomial(n, k) + + assert combsimp(factorial(n*(1 + n) - n**2 - n)) == 1 + + assert combsimp(6*FallingFactorial(-4, n)/factorial(n)) == \ + (-1)**n*(n + 1)*(n + 2)*(n + 3) + assert combsimp(6*FallingFactorial(-4, n - 1)/factorial(n - 1)) == \ + (-1)**(n - 1)*n*(n + 1)*(n + 2) + assert combsimp(6*FallingFactorial(-4, n - 3)/factorial(n - 3)) == \ + (-1)**(n - 3)*n*(n - 1)*(n - 2) + assert combsimp(6*FallingFactorial(-4, -n - 1)/factorial(-n - 1)) == \ + -(-1)**(-n - 1)*n*(n - 1)*(n - 2) + + assert combsimp(6*RisingFactorial(4, n)/factorial(n)) == \ + (n + 1)*(n + 2)*(n + 3) + assert combsimp(6*RisingFactorial(4, n - 1)/factorial(n - 1)) == \ + n*(n + 1)*(n + 2) + assert combsimp(6*RisingFactorial(4, n - 3)/factorial(n - 3)) == \ + n*(n - 1)*(n - 2) + assert combsimp(6*RisingFactorial(4, -n - 1)/factorial(-n - 1)) == \ + -n*(n - 1)*(n - 2) + + +def test_issue_6878(): + n = symbols('n', integer=True) + assert combsimp(RisingFactorial(-10, n)) == 3628800*(-1)**n/factorial(10 - n) + + +def test_issue_14528(): + p = symbols("p", integer=True, positive=True) + assert combsimp(binomial(1,p)) == 1/(factorial(p)*factorial(1-p)) + assert combsimp(factorial(2-p)) == factorial(2-p) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_cse.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_cse.py new file mode 100644 index 0000000000000000000000000000000000000000..c2a34dfb0e227547bd41bed2491284fd7150d0b6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_cse.py @@ -0,0 +1,761 @@ +from functools import reduce +import itertools +from operator import add + +from sympy.codegen.matrix_nodes import MatrixSolve +from sympy.core.add import Add +from sympy.core.containers import Tuple +from sympy.core.expr import UnevaluatedExpr +from sympy.core.function import Function +from sympy.core.mul import Mul +from sympy.core.power import Pow +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.core.sympify import sympify +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.matrices.dense import Matrix +from sympy.matrices.expressions import Inverse, MatAdd, MatMul, Transpose +from sympy.polys.rootoftools import CRootOf +from sympy.series.order import O +from sympy.simplify.cse_main import cse +from sympy.simplify.simplify import signsimp +from sympy.tensor.indexed import (Idx, IndexedBase) + +from sympy.core.function import count_ops +from sympy.simplify.cse_opts import sub_pre, sub_post +from sympy.functions.special.hyper import meijerg +from sympy.simplify import cse_main, cse_opts +from sympy.utilities.iterables import subsets +from sympy.testing.pytest import XFAIL, raises +from sympy.matrices import (MutableDenseMatrix, MutableSparseMatrix, + ImmutableDenseMatrix, ImmutableSparseMatrix) +from sympy.matrices.expressions import MatrixSymbol + + +w, x, y, z = symbols('w,x,y,z') +x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12 = symbols('x:13') + + +def test_numbered_symbols(): + ns = cse_main.numbered_symbols(prefix='y') + assert list(itertools.islice( + ns, 0, 10)) == [Symbol('y%s' % i) for i in range(0, 10)] + ns = cse_main.numbered_symbols(prefix='y') + assert list(itertools.islice( + ns, 10, 20)) == [Symbol('y%s' % i) for i in range(10, 20)] + ns = cse_main.numbered_symbols() + assert list(itertools.islice( + ns, 0, 10)) == [Symbol('x%s' % i) for i in range(0, 10)] + +# Dummy "optimization" functions for testing. + + +def opt1(expr): + return expr + y + + +def opt2(expr): + return expr*z + + +def test_preprocess_for_cse(): + assert cse_main.preprocess_for_cse(x, [(opt1, None)]) == x + y + assert cse_main.preprocess_for_cse(x, [(None, opt1)]) == x + assert cse_main.preprocess_for_cse(x, [(None, None)]) == x + assert cse_main.preprocess_for_cse(x, [(opt1, opt2)]) == x + y + assert cse_main.preprocess_for_cse( + x, [(opt1, None), (opt2, None)]) == (x + y)*z + + +def test_postprocess_for_cse(): + assert cse_main.postprocess_for_cse(x, [(opt1, None)]) == x + assert cse_main.postprocess_for_cse(x, [(None, opt1)]) == x + y + assert cse_main.postprocess_for_cse(x, [(None, None)]) == x + assert cse_main.postprocess_for_cse(x, [(opt1, opt2)]) == x*z + # Note the reverse order of application. + assert cse_main.postprocess_for_cse( + x, [(None, opt1), (None, opt2)]) == x*z + y + + +def test_cse_single(): + # Simple substitution. + e = Add(Pow(x + y, 2), sqrt(x + y)) + substs, reduced = cse([e]) + assert substs == [(x0, x + y)] + assert reduced == [sqrt(x0) + x0**2] + + subst42, (red42,) = cse([42]) # issue_15082 + assert len(subst42) == 0 and red42 == 42 + subst_half, (red_half,) = cse([0.5]) + assert len(subst_half) == 0 and red_half == 0.5 + + +def test_cse_single2(): + # Simple substitution, test for being able to pass the expression directly + e = Add(Pow(x + y, 2), sqrt(x + y)) + substs, reduced = cse(e) + assert substs == [(x0, x + y)] + assert reduced == [sqrt(x0) + x0**2] + substs, reduced = cse(Matrix([[1]])) + assert isinstance(reduced[0], Matrix) + + subst42, (red42,) = cse(42) # issue 15082 + assert len(subst42) == 0 and red42 == 42 + subst_half, (red_half,) = cse(0.5) # issue 15082 + assert len(subst_half) == 0 and red_half == 0.5 + + +def test_cse_not_possible(): + # No substitution possible. + e = Add(x, y) + substs, reduced = cse([e]) + assert substs == [] + assert reduced == [x + y] + # issue 6329 + eq = (meijerg((1, 2), (y, 4), (5,), [], x) + + meijerg((1, 3), (y, 4), (5,), [], x)) + assert cse(eq) == ([], [eq]) + + +def test_nested_substitution(): + # Substitution within a substitution. + e = Add(Pow(w*x + y, 2), sqrt(w*x + y)) + substs, reduced = cse([e]) + assert substs == [(x0, w*x + y)] + assert reduced == [sqrt(x0) + x0**2] + + +def test_subtraction_opt(): + # Make sure subtraction is optimized. + e = (x - y)*(z - y) + exp((x - y)*(z - y)) + substs, reduced = cse( + [e], optimizations=[(cse_opts.sub_pre, cse_opts.sub_post)]) + assert substs == [(x0, (x - y)*(y - z))] + assert reduced == [-x0 + exp(-x0)] + e = -(x - y)*(z - y) + exp(-(x - y)*(z - y)) + substs, reduced = cse( + [e], optimizations=[(cse_opts.sub_pre, cse_opts.sub_post)]) + assert substs == [(x0, (x - y)*(y - z))] + assert reduced == [x0 + exp(x0)] + # issue 4077 + n = -1 + 1/x + e = n/x/(-n)**2 - 1/n/x + assert cse(e, optimizations=[(cse_opts.sub_pre, cse_opts.sub_post)]) == \ + ([], [0]) + assert cse(((w + x + y + z)*(w - y - z))/(w + x)**3) == \ + ([(x0, w + x), (x1, y + z)], [(w - x1)*(x0 + x1)/x0**3]) + + +def test_multiple_expressions(): + e1 = (x + y)*z + e2 = (x + y)*w + substs, reduced = cse([e1, e2]) + assert substs == [(x0, x + y)] + assert reduced == [x0*z, x0*w] + l = [w*x*y + z, w*y] + substs, reduced = cse(l) + rsubsts, _ = cse(reversed(l)) + assert substs == rsubsts + assert reduced == [z + x*x0, x0] + l = [w*x*y, w*x*y + z, w*y] + substs, reduced = cse(l) + rsubsts, _ = cse(reversed(l)) + assert substs == rsubsts + assert reduced == [x1, x1 + z, x0] + l = [(x - z)*(y - z), x - z, y - z] + substs, reduced = cse(l) + rsubsts, _ = cse(reversed(l)) + assert substs == [(x0, -z), (x1, x + x0), (x2, x0 + y)] + assert rsubsts == [(x0, -z), (x1, x0 + y), (x2, x + x0)] + assert reduced == [x1*x2, x1, x2] + l = [w*y + w + x + y + z, w*x*y] + assert cse(l) == ([(x0, w*y)], [w + x + x0 + y + z, x*x0]) + assert cse([x + y, x + y + z]) == ([(x0, x + y)], [x0, z + x0]) + assert cse([x + y, x + z]) == ([], [x + y, x + z]) + assert cse([x*y, z + x*y, x*y*z + 3]) == \ + ([(x0, x*y)], [x0, z + x0, 3 + x0*z]) + + +@XFAIL # CSE of non-commutative Mul terms is disabled +def test_non_commutative_cse(): + A, B, C = symbols('A B C', commutative=False) + l = [A*B*C, A*C] + assert cse(l) == ([], l) + l = [A*B*C, A*B] + assert cse(l) == ([(x0, A*B)], [x0*C, x0]) + + +# Test if CSE of non-commutative Mul terms is disabled +def test_bypass_non_commutatives(): + A, B, C = symbols('A B C', commutative=False) + l = [A*B*C, A*C] + assert cse(l) == ([], l) + l = [A*B*C, A*B] + assert cse(l) == ([], l) + l = [B*C, A*B*C] + assert cse(l) == ([], l) + + +@XFAIL # CSE fails when replacing non-commutative sub-expressions +def test_non_commutative_order(): + A, B, C = symbols('A B C', commutative=False) + x0 = symbols('x0', commutative=False) + l = [B+C, A*(B+C)] + assert cse(l) == ([(x0, B+C)], [x0, A*x0]) + + +@XFAIL # Worked in gh-11232, but was reverted due to performance considerations +def test_issue_10228(): + assert cse([x*y**2 + x*y]) == ([(x0, x*y)], [x0*y + x0]) + assert cse([x + y, 2*x + y]) == ([(x0, x + y)], [x0, x + x0]) + assert cse((w + 2*x + y + z, w + x + 1)) == ( + [(x0, w + x)], [x0 + x + y + z, x0 + 1]) + assert cse(((w + x + y + z)*(w - x))/(w + x)) == ( + [(x0, w + x)], [(x0 + y + z)*(w - x)/x0]) + a, b, c, d, f, g, j, m = symbols('a, b, c, d, f, g, j, m') + exprs = (d*g**2*j*m, 4*a*f*g*m, a*b*c*f**2) + assert cse(exprs) == ( + [(x0, g*m), (x1, a*f)], [d*g*j*x0, 4*x0*x1, b*c*f*x1] +) + +@XFAIL +def test_powers(): + assert cse(x*y**2 + x*y) == ([(x0, x*y)], [x0*y + x0]) + + +def test_issue_4498(): + assert cse(w/(x - y) + z/(y - x), optimizations='basic') == \ + ([], [(w - z)/(x - y)]) + + +def test_issue_4020(): + assert cse(x**5 + x**4 + x**3 + x**2, optimizations='basic') \ + == ([(x0, x**2)], [x0*(x**3 + x + x0 + 1)]) + + +def test_issue_4203(): + assert cse(sin(x**x)/x**x) == ([(x0, x**x)], [sin(x0)/x0]) + + +def test_issue_6263(): + e = Eq(x*(-x + 1) + x*(x - 1), 0) + assert cse(e, optimizations='basic') == ([], [True]) + + +def test_issue_25043(): + c = symbols("c") + x = symbols("x0", real=True) + cse_expr = cse(c*x**2 + c*(x**4 - x**2))[-1][-1] + free = cse_expr.free_symbols + assert len(free) == len({i.name for i in free}) + + +def test_dont_cse_tuples(): + from sympy.core.function import Subs + f = Function("f") + g = Function("g") + + name_val, (expr,) = cse( + Subs(f(x, y), (x, y), (0, 1)) + + Subs(g(x, y), (x, y), (0, 1))) + + assert name_val == [] + assert expr == (Subs(f(x, y), (x, y), (0, 1)) + + Subs(g(x, y), (x, y), (0, 1))) + + name_val, (expr,) = cse( + Subs(f(x, y), (x, y), (0, x + y)) + + Subs(g(x, y), (x, y), (0, x + y))) + + assert name_val == [(x0, x + y)] + assert expr == Subs(f(x, y), (x, y), (0, x0)) + \ + Subs(g(x, y), (x, y), (0, x0)) + + +def test_pow_invpow(): + assert cse(1/x**2 + x**2) == \ + ([(x0, x**2)], [x0 + 1/x0]) + assert cse(x**2 + (1 + 1/x**2)/x**2) == \ + ([(x0, x**2), (x1, 1/x0)], [x0 + x1*(x1 + 1)]) + assert cse(1/x**2 + (1 + 1/x**2)*x**2) == \ + ([(x0, x**2), (x1, 1/x0)], [x0*(x1 + 1) + x1]) + assert cse(cos(1/x**2) + sin(1/x**2)) == \ + ([(x0, x**(-2))], [sin(x0) + cos(x0)]) + assert cse(cos(x**2) + sin(x**2)) == \ + ([(x0, x**2)], [sin(x0) + cos(x0)]) + assert cse(y/(2 + x**2) + z/x**2/y) == \ + ([(x0, x**2)], [y/(x0 + 2) + z/(x0*y)]) + assert cse(exp(x**2) + x**2*cos(1/x**2)) == \ + ([(x0, x**2)], [x0*cos(1/x0) + exp(x0)]) + assert cse((1 + 1/x**2)/x**2) == \ + ([(x0, x**(-2))], [x0*(x0 + 1)]) + assert cse(x**(2*y) + x**(-2*y)) == \ + ([(x0, x**(2*y))], [x0 + 1/x0]) + + +def test_postprocess(): + eq = (x + 1 + exp((x + 1)/(y + 1)) + cos(y + 1)) + assert cse([eq, Eq(x, z + 1), z - 2, (z + 1)*(x + 1)], + postprocess=cse_main.cse_separate) == \ + [[(x0, y + 1), (x2, z + 1), (x, x2), (x1, x + 1)], + [x1 + exp(x1/x0) + cos(x0), z - 2, x1*x2]] + + +def test_issue_4499(): + # previously, this gave 16 constants + from sympy.abc import a, b + B = Function('B') + G = Function('G') + t = Tuple(* + (a, a + S.Half, 2*a, b, 2*a - b + 1, (sqrt(z)/2)**(-2*a + 1)*B(2*a - + b, sqrt(z))*B(b - 1, sqrt(z))*G(b)*G(2*a - b + 1), + sqrt(z)*(sqrt(z)/2)**(-2*a + 1)*B(b, sqrt(z))*B(2*a - b, + sqrt(z))*G(b)*G(2*a - b + 1), sqrt(z)*(sqrt(z)/2)**(-2*a + 1)*B(b - 1, + sqrt(z))*B(2*a - b + 1, sqrt(z))*G(b)*G(2*a - b + 1), + (sqrt(z)/2)**(-2*a + 1)*B(b, sqrt(z))*B(2*a - b + 1, + sqrt(z))*G(b)*G(2*a - b + 1), 1, 0, S.Half, z/2, -b + 1, -2*a + b, + -2*a)) + c = cse(t) + ans = ( + [(x0, 2*a), (x1, -b + x0), (x2, x1 + 1), (x3, b - 1), (x4, sqrt(z)), + (x5, B(x3, x4)), (x6, (x4/2)**(1 - x0)*G(b)*G(x2)), (x7, x6*B(x1, x4)), + (x8, B(b, x4)), (x9, x6*B(x2, x4))], + [(a, a + S.Half, x0, b, x2, x5*x7, x4*x7*x8, x4*x5*x9, x8*x9, + 1, 0, S.Half, z/2, -x3, -x1, -x0)]) + assert ans == c + + +def test_issue_6169(): + r = CRootOf(x**6 - 4*x**5 - 2, 1) + assert cse(r) == ([], [r]) + # and a check that the right thing is done with the new + # mechanism + assert sub_post(sub_pre((-x - y)*z - x - y)) == -z*(x + y) - x - y + + +def test_cse_Indexed(): + len_y = 5 + y = IndexedBase('y', shape=(len_y,)) + x = IndexedBase('x', shape=(len_y,)) + i = Idx('i', len_y-1) + + expr1 = (y[i+1]-y[i])/(x[i+1]-x[i]) + expr2 = 1/(x[i+1]-x[i]) + replacements, reduced_exprs = cse([expr1, expr2]) + assert len(replacements) > 0 + + +def test_cse_MatrixSymbol(): + # MatrixSymbols have non-Basic args, so make sure that works + A = MatrixSymbol("A", 3, 3) + assert cse(A) == ([], [A]) + + n = symbols('n', integer=True) + B = MatrixSymbol("B", n, n) + assert cse(B) == ([], [B]) + + assert cse(A[0] * A[0]) == ([], [A[0]*A[0]]) + + assert cse(A[0,0]*A[0,1] + A[0,0]*A[0,1]*A[0,2]) == ([(x0, A[0, 0]*A[0, 1])], [x0*A[0, 2] + x0]) + +def test_cse_MatrixExpr(): + A = MatrixSymbol('A', 3, 3) + y = MatrixSymbol('y', 3, 1) + + expr1 = (A.T*A).I * A * y + expr2 = (A.T*A) * A * y + replacements, reduced_exprs = cse([expr1, expr2]) + assert len(replacements) > 0 + + replacements, reduced_exprs = cse([expr1 + expr2, expr1]) + assert replacements + + replacements, reduced_exprs = cse([A**2, A + A**2]) + assert replacements + + +def test_Piecewise(): + f = Piecewise((-z + x*y, Eq(y, 0)), (-z - x*y, True)) + ans = cse(f) + actual_ans = ([(x0, x*y)], + [Piecewise((x0 - z, Eq(y, 0)), (-z - x0, True))]) + assert ans == actual_ans + + +def test_ignore_order_terms(): + eq = exp(x).series(x,0,3) + sin(y+x**3) - 1 + assert cse(eq) == ([], [sin(x**3 + y) + x + x**2/2 + O(x**3)]) + + +def test_name_conflict(): + z1 = x0 + y + z2 = x2 + x3 + l = [cos(z1) + z1, cos(z2) + z2, x0 + x2] + substs, reduced = cse(l) + assert [e.subs(reversed(substs)) for e in reduced] == l + + +def test_name_conflict_cust_symbols(): + z1 = x0 + y + z2 = x2 + x3 + l = [cos(z1) + z1, cos(z2) + z2, x0 + x2] + substs, reduced = cse(l, symbols("x:10")) + assert [e.subs(reversed(substs)) for e in reduced] == l + + +def test_symbols_exhausted_error(): + l = cos(x+y)+x+y+cos(w+y)+sin(w+y) + sym = [x, y, z] + with raises(ValueError): + cse(l, symbols=sym) + + +def test_issue_7840(): + # daveknippers' example + C393 = sympify( \ + 'Piecewise((C391 - 1.65, C390 < 0.5), (Piecewise((C391 - 1.65, \ + C391 > 2.35), (C392, True)), True))' + ) + C391 = sympify( \ + 'Piecewise((2.05*C390**(-1.03), C390 < 0.5), (2.5*C390**(-0.625), True))' + ) + C393 = C393.subs('C391',C391) + # simple substitution + sub = {} + sub['C390'] = 0.703451854 + sub['C392'] = 1.01417794 + ss_answer = C393.subs(sub) + # cse + substitutions,new_eqn = cse(C393) + for pair in substitutions: + sub[pair[0].name] = pair[1].subs(sub) + cse_answer = new_eqn[0].subs(sub) + # both methods should be the same + assert ss_answer == cse_answer + + # GitRay's example + expr = sympify( + "Piecewise((Symbol('ON'), Equality(Symbol('mode'), Symbol('ON'))), \ + (Piecewise((Piecewise((Symbol('OFF'), StrictLessThan(Symbol('x'), \ + Symbol('threshold'))), (Symbol('ON'), true)), Equality(Symbol('mode'), \ + Symbol('AUTO'))), (Symbol('OFF'), true)), true))" + ) + substitutions, new_eqn = cse(expr) + # this Piecewise should be exactly the same + assert new_eqn[0] == expr + # there should not be any replacements + assert len(substitutions) < 1 + + +def test_issue_8891(): + for cls in (MutableDenseMatrix, MutableSparseMatrix, + ImmutableDenseMatrix, ImmutableSparseMatrix): + m = cls(2, 2, [x + y, 0, 0, 0]) + res = cse([x + y, m]) + ans = ([(x0, x + y)], [x0, cls([[x0, 0], [0, 0]])]) + assert res == ans + assert isinstance(res[1][-1], cls) + + +def test_issue_11230(): + # a specific test that always failed + a, b, f, k, l, i = symbols('a b f k l i') + p = [a*b*f*k*l, a*i*k**2*l, f*i*k**2*l] + R, C = cse(p) + assert not any(i.is_Mul for a in C for i in a.args) + + # random tests for the issue + from sympy.core.random import choice + from sympy.core.function import expand_mul + s = symbols('a:m') + # 35 Mul tests, none of which should ever fail + ex = [Mul(*[choice(s) for i in range(5)]) for i in range(7)] + for p in subsets(ex, 3): + p = list(p) + R, C = cse(p) + assert not any(i.is_Mul for a in C for i in a.args) + for ri in reversed(R): + for i in range(len(C)): + C[i] = C[i].subs(*ri) + assert p == C + # 35 Add tests, none of which should ever fail + ex = [Add(*[choice(s[:7]) for i in range(5)]) for i in range(7)] + for p in subsets(ex, 3): + p = list(p) + R, C = cse(p) + assert not any(i.is_Add for a in C for i in a.args) + for ri in reversed(R): + for i in range(len(C)): + C[i] = C[i].subs(*ri) + # use expand_mul to handle cases like this: + # p = [a + 2*b + 2*e, 2*b + c + 2*e, b + 2*c + 2*g] + # x0 = 2*(b + e) is identified giving a rebuilt p that + # is now `[a + 2*(b + e), c + 2*(b + e), b + 2*c + 2*g]` + assert p == [expand_mul(i) for i in C] + + +@XFAIL +def test_issue_11577(): + def check(eq): + r, c = cse(eq) + assert eq.count_ops() >= \ + len(r) + sum(i[1].count_ops() for i in r) + \ + count_ops(c) + + eq = x**5*y**2 + x**5*y + x**5 + assert cse(eq) == ( + [(x0, x**4), (x1, x*y)], [x**5 + x0*x1*y + x0*x1]) + # ([(x0, x**5*y)], [x0*y + x0 + x**5]) or + # ([(x0, x**5)], [x0*y**2 + x0*y + x0]) + check(eq) + + eq = x**2/(y + 1)**2 + x/(y + 1) + assert cse(eq) == ( + [(x0, y + 1)], [x**2/x0**2 + x/x0]) + # ([(x0, x/(y + 1))], [x0**2 + x0]) + check(eq) + + +def test_hollow_rejection(): + eq = [x + 3, x + 4] + assert cse(eq) == ([], eq) + + +def test_cse_ignore(): + exprs = [exp(y)*(3*y + 3*sqrt(x+1)), exp(y)*(5*y + 5*sqrt(x+1))] + subst1, red1 = cse(exprs) + assert any(y in sub.free_symbols for _, sub in subst1), "cse failed to identify any term with y" + + subst2, red2 = cse(exprs, ignore=(y,)) # y is not allowed in substitutions + assert not any(y in sub.free_symbols for _, sub in subst2), "Sub-expressions containing y must be ignored" + assert any(sub - sqrt(x + 1) == 0 for _, sub in subst2), "cse failed to identify sqrt(x + 1) as sub-expression" + + +def test_cse_ignore_issue_15002(): + l = [ + w*exp(x)*exp(-z), + exp(y)*exp(x)*exp(-z) + ] + substs, reduced = cse(l, ignore=(x,)) + rl = [e.subs(reversed(substs)) for e in reduced] + assert rl == l + + +def test_cse_unevaluated(): + xp1 = UnevaluatedExpr(x + 1) + # This used to cause RecursionError + [(x0, ue)], [red] = cse([(-1 - xp1) / (1 - xp1)]) + if ue == xp1: + assert red == (-1 - x0) / (1 - x0) + elif ue == -xp1: + assert red == (-1 + x0) / (1 + x0) + else: + msg = f'Expected common subexpression {xp1} or {-xp1}, instead got {ue}' + assert False, msg + + +def test_cse__performance(): + nexprs, nterms = 3, 20 + x = symbols('x:%d' % nterms) + exprs = [ + reduce(add, [x[j]*(-1)**(i+j) for j in range(nterms)]) + for i in range(nexprs) + ] + assert (exprs[0] + exprs[1]).simplify() == 0 + subst, red = cse(exprs) + assert len(subst) > 0, "exprs[0] == -exprs[2], i.e. a CSE" + for i, e in enumerate(red): + assert (e.subs(reversed(subst)) - exprs[i]).simplify() == 0 + + +def test_issue_12070(): + exprs = [x + y, 2 + x + y, x + y + z, 3 + x + y + z] + subst, red = cse(exprs) + assert 6 >= (len(subst) + sum(v.count_ops() for k, v in subst) + + count_ops(red)) + + +def test_issue_13000(): + eq = x/(-4*x**2 + y**2) + cse_eq = cse(eq)[1][0] + assert cse_eq == eq + + +def test_issue_18203(): + eq = CRootOf(x**5 + 11*x - 2, 0) + CRootOf(x**5 + 11*x - 2, 1) + assert cse(eq) == ([], [eq]) + + +def test_unevaluated_mul(): + eq = Mul(x + y, x + y, evaluate=False) + assert cse(eq) == ([(x0, x + y)], [x0**2]) + + +def test_cse_release_variables(): + from sympy.simplify.cse_main import cse_release_variables + _0, _1, _2, _3, _4 = symbols('_:5') + eqs = [(x + y - 1)**2, x, + x + y, (x + y)/(2*x + 1) + (x + y - 1)**2, + (2*x + 1)**(x + y)] + r, e = cse(eqs, postprocess=cse_release_variables) + # this can change in keeping with the intention of the function + assert r, e == ([ + (x0, x + y), (x1, (x0 - 1)**2), (x2, 2*x + 1), + (_3, x0/x2 + x1), (_4, x2**x0), (x2, None), (_0, x1), + (x1, None), (_2, x0), (x0, None), (_1, x)], (_0, _1, _2, _3, _4)) + r.reverse() + r = [(s, v) for s, v in r if v is not None] + assert eqs == [i.subs(r) for i in e] + + +def test_cse_list(): + _cse = lambda x: cse(x, list=False) + assert _cse(x) == ([], x) + assert _cse('x') == ([], 'x') + it = [x] + for c in (list, tuple, set): + assert _cse(c(it)) == ([], c(it)) + #Tuple works different from tuple: + assert _cse(Tuple(*it)) == ([], Tuple(*it)) + d = {x: 1} + assert _cse(d) == ([], d) + +def test_issue_18991(): + A = MatrixSymbol('A', 2, 2) + assert signsimp(-A * A - A) == -A * A - A + + +def test_unevaluated_Mul(): + m = [Mul(1, 2, evaluate=False)] + assert cse(m) == ([], m) + + +def test_cse_matrix_expression_inverse(): + A = ImmutableDenseMatrix(symbols('A:4')).reshape(2, 2) + x = Inverse(A) + cse_expr = cse(x) + assert cse_expr == ([], [Inverse(A)]) + + +def test_cse_matrix_expression_matmul_inverse(): + A = ImmutableDenseMatrix(symbols('A:4')).reshape(2, 2) + b = ImmutableDenseMatrix(symbols('b:2')) + x = MatMul(Inverse(A), b) + cse_expr = cse(x) + assert cse_expr == ([], [x]) + + +def test_cse_matrix_negate_matrix(): + A = ImmutableDenseMatrix(symbols('A:4')).reshape(2, 2) + x = MatMul(S.NegativeOne, A) + cse_expr = cse(x) + assert cse_expr == ([], [x]) + + +def test_cse_matrix_negate_matmul_not_extracted(): + A = ImmutableDenseMatrix(symbols('A:4')).reshape(2, 2) + B = ImmutableDenseMatrix(symbols('B:4')).reshape(2, 2) + x = MatMul(S.NegativeOne, A, B) + cse_expr = cse(x) + assert cse_expr == ([], [x]) + + +@XFAIL # No simplification rule for nested associative operations +def test_cse_matrix_nested_matmul_collapsed(): + A = ImmutableDenseMatrix(symbols('A:4')).reshape(2, 2) + B = ImmutableDenseMatrix(symbols('B:4')).reshape(2, 2) + x = MatMul(S.NegativeOne, MatMul(A, B)) + cse_expr = cse(x) + assert cse_expr == ([], [MatMul(S.NegativeOne, A, B)]) + + +def test_cse_matrix_optimize_out_single_argument_mul(): + A = ImmutableDenseMatrix(symbols('A:4')).reshape(2, 2) + x = MatMul(MatMul(MatMul(A))) + cse_expr = cse(x) + assert cse_expr == ([], [A]) + + +@XFAIL # Multiple simplification passed not supported in CSE +def test_cse_matrix_optimize_out_single_argument_mul_combined(): + A = ImmutableDenseMatrix(symbols('A:4')).reshape(2, 2) + x = MatAdd(MatMul(MatMul(MatMul(A))), MatMul(MatMul(A)), MatMul(A), A) + cse_expr = cse(x) + assert cse_expr == ([], [MatMul(4, A)]) + + +def test_cse_matrix_optimize_out_single_argument_add(): + A = ImmutableDenseMatrix(symbols('A:4')).reshape(2, 2) + x = MatAdd(MatAdd(MatAdd(MatAdd(A)))) + cse_expr = cse(x) + assert cse_expr == ([], [A]) + + +@XFAIL # Multiple simplification passed not supported in CSE +def test_cse_matrix_optimize_out_single_argument_add_combined(): + A = ImmutableDenseMatrix(symbols('A:4')).reshape(2, 2) + x = MatMul(MatAdd(MatAdd(MatAdd(A))), MatAdd(MatAdd(A)), MatAdd(A), A) + cse_expr = cse(x) + assert cse_expr == ([], [MatMul(4, A)]) + + +def test_cse_matrix_expression_matrix_solve(): + A = ImmutableDenseMatrix(symbols('A:4')).reshape(2, 2) + b = ImmutableDenseMatrix(symbols('b:2')) + x = MatrixSolve(A, b) + cse_expr = cse(x) + assert cse_expr == ([], [x]) + + +def test_cse_matrix_matrix_expression(): + X = ImmutableDenseMatrix(symbols('X:4')).reshape(2, 2) + y = ImmutableDenseMatrix(symbols('y:2')) + b = MatMul(Inverse(MatMul(Transpose(X), X)), Transpose(X), y) + cse_expr = cse(b) + x0 = MatrixSymbol('x0', 2, 2) + reduced_expr_expected = MatMul(Inverse(MatMul(x0, X)), x0, y) + assert cse_expr == ([(x0, Transpose(X))], [reduced_expr_expected]) + + +def test_cse_matrix_kalman_filter(): + """Kalman Filter example from Matthew Rocklin's SciPy 2013 talk. + + Talk titled: "Matrix Expressions and BLAS/LAPACK; SciPy 2013 Presentation" + + Video: https://pyvideo.org/scipy-2013/matrix-expressions-and-blaslapack-scipy-2013-pr.html + + Notes + ===== + + Equations are: + + new_mu = mu + Sigma*H.T * (R + H*Sigma*H.T).I * (H*mu - data) + = MatAdd(mu, MatMul(Sigma, Transpose(H), Inverse(MatAdd(R, MatMul(H, Sigma, Transpose(H)))), MatAdd(MatMul(H, mu), MatMul(S.NegativeOne, data)))) + new_Sigma = Sigma - Sigma*H.T * (R + H*Sigma*H.T).I * H * Sigma + = MatAdd(Sigma, MatMul(S.NegativeOne, Sigma, Transpose(H)), Inverse(MatAdd(R, MatMul(H*Sigma*Transpose(H)))), H, Sigma)) + + """ + N = 2 + mu = ImmutableDenseMatrix(symbols(f'mu:{N}')) + Sigma = ImmutableDenseMatrix(symbols(f'Sigma:{N * N}')).reshape(N, N) + H = ImmutableDenseMatrix(symbols(f'H:{N * N}')).reshape(N, N) + R = ImmutableDenseMatrix(symbols(f'R:{N * N}')).reshape(N, N) + data = ImmutableDenseMatrix(symbols(f'data:{N}')) + new_mu = MatAdd(mu, MatMul(Sigma, Transpose(H), Inverse(MatAdd(R, MatMul(H, Sigma, Transpose(H)))), MatAdd(MatMul(H, mu), MatMul(S.NegativeOne, data)))) + new_Sigma = MatAdd(Sigma, MatMul(S.NegativeOne, Sigma, Transpose(H), Inverse(MatAdd(R, MatMul(H, Sigma, Transpose(H)))), H, Sigma)) + cse_expr = cse([new_mu, new_Sigma]) + x0 = MatrixSymbol('x0', N, N) + x1 = MatrixSymbol('x1', N, N) + replacements_expected = [ + (x0, Transpose(H)), + (x1, Inverse(MatAdd(R, MatMul(H, Sigma, x0)))), + ] + reduced_exprs_expected = [ + MatAdd(mu, MatMul(Sigma, x0, x1, MatAdd(MatMul(H, mu), MatMul(S.NegativeOne, data)))), + MatAdd(Sigma, MatMul(S.NegativeOne, Sigma, x0, x1, H, Sigma)), + ] + assert cse_expr == (replacements_expected, reduced_exprs_expected) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_cse_diff.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_cse_diff.py new file mode 100644 index 0000000000000000000000000000000000000000..92b2d3d6bbaafb838a5e75f32a214511a1d39567 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_cse_diff.py @@ -0,0 +1,206 @@ +"""Tests for the ``sympy.simplify._cse_diff.py`` module.""" + +import pytest + +from sympy.core.symbol import (Symbol, symbols) +from sympy.core.numbers import Integer +from sympy.core.function import Function +from sympy.core import Derivative +from sympy.functions.elementary.exponential import exp +from sympy.matrices.immutable import ImmutableDenseMatrix +from sympy.physics.mechanics import dynamicsymbols +from sympy.simplify._cse_diff import (_forward_jacobian, + _remove_cse_from_derivative, + _forward_jacobian_cse, + _forward_jacobian_norm_in_cse_out) +from sympy.simplify.simplify import simplify +from sympy.matrices import Matrix, eye + +from sympy.testing.pytest import raises +from sympy.functions.elementary.trigonometric import (cos, sin, tan) +from sympy.simplify.trigsimp import trigsimp + +from sympy import cse + + +w = Symbol('w') +x = Symbol('x') +y = Symbol('y') +z = Symbol('z') + +q1, q2, q3 = dynamicsymbols('q1 q2 q3') + +# Define the custom functions +k = Function('k')(x, y) +f = Function('f')(k, z) + +zero = Integer(0) +one = Integer(1) +two = Integer(2) +neg_one = Integer(-1) + + +@pytest.mark.parametrize( + 'expr, wrt', + [ + ([zero], [x]), + ([one], [x]), + ([two], [x]), + ([neg_one], [x]), + ([x], [x]), + ([y], [x]), + ([x + y], [x]), + ([x*y], [x]), + ([x**2], [x]), + ([x**y], [x]), + ([exp(x)], [x]), + ([sin(x)], [x]), + ([tan(x)], [x]), + ([zero, one, x, y, x*y, x + y], [x, y]), + ([((x/y) + sin(x/y) - exp(y))*((x/y) - exp(y))], [x, y]), + ([w*tan(y*z)/(x - tan(y*z)), w*x*tan(y*z)/(x - tan(y*z))], [w, x, y, z]), + ([q1**2 + q2, q2**2 + q3, q3**2 + q1], [q1, q2, q3]), + ([f + Derivative(f, x) + k + 2*x], [x]) + ] +) + + +def test_forward_jacobian(expr, wrt): + expr = ImmutableDenseMatrix([expr]).T + wrt = ImmutableDenseMatrix([wrt]).T + jacobian = _forward_jacobian(expr, wrt) + zeros = ImmutableDenseMatrix.zeros(*jacobian.shape) + assert simplify(jacobian - expr.jacobian(wrt)) == zeros + + +def test_process_cse(): + x, y, z = symbols('x y z') + f = Function('f') + k = Function('k') + expr = Matrix([f(k(x,y), z) + Derivative(f(k(x,y), z), x) + k(x,y) + 2*x]) + repl, reduced = cse(expr) + p_repl, p_reduced = _remove_cse_from_derivative(repl, reduced) + + x0 = symbols('x0') + x1 = symbols('x1') + + expected_output = ( + [(x0, k(x, y)), (x1, f(x0, z))], + [Matrix([2 * x + x0 + x1 + Derivative(f(k(x, y), z), x)])] + ) + + assert p_repl == expected_output[0], f"Expected {expected_output[0]}, but got {p_repl}" + assert p_reduced == expected_output[1], f"Expected {expected_output[1]}, but got {p_reduced}" + + +def test_io_matrix_type(): + x, y, z = symbols('x y z') + expr = ImmutableDenseMatrix([ + x * y + y * z + x * y * z, + x ** 2 + y ** 2 + z ** 2, + x * y + x * z + y * z + ]) + wrt = ImmutableDenseMatrix([x, y, z]) + + replacements, reduced_expr = cse(expr) + + # Test _forward_jacobian_core + replacements_core, jacobian_core, precomputed_fs_core = _forward_jacobian_cse(replacements, reduced_expr, wrt) + assert isinstance(jacobian_core[0], type(reduced_expr[0])), "Jacobian should be a Matrix of the same type as the input" + + # Test _forward_jacobian_norm_in_dag_out + replacements_norm, jacobian_norm, precomputed_fs_norm = _forward_jacobian_norm_in_cse_out( + expr, wrt) + assert isinstance(jacobian_norm[0], type(reduced_expr[0])), "Jacobian should be a Matrix of the same type as the input" + + # Test _forward_jacobian + jacobian = _forward_jacobian(expr, wrt) + assert isinstance(jacobian, type(expr)), "Jacobian should be a Matrix of the same type as the input" + + +def test_forward_jacobian_input_output(): + x, y, z = symbols('x y z') + expr = Matrix([ + x * y + y * z + x * y * z, + x ** 2 + y ** 2 + z ** 2, + x * y + x * z + y * z + ]) + wrt = Matrix([x, y, z]) + + replacements, reduced_expr = cse(expr) + + # Test _forward_jacobian_core + replacements_core, jacobian_core, precomputed_fs_core = _forward_jacobian_cse(replacements, reduced_expr, wrt) + assert isinstance(replacements_core, type(replacements)), "Replacements should be a list" + assert isinstance(jacobian_core, type(reduced_expr)), "Jacobian should be a list" + assert isinstance(precomputed_fs_core, list), "Precomputed free symbols should be a list" + assert len(replacements_core) == len(replacements), "Length of replacements does not match" + assert len(jacobian_core) == 1, "Jacobian should have one element" + assert len(precomputed_fs_core) == len(replacements), "Length of precomputed free symbols does not match" + + # Test _forward_jacobian_norm_in_dag_out + replacements_norm, jacobian_norm, precomputed_fs_norm = _forward_jacobian_norm_in_cse_out(expr, wrt) + assert isinstance(replacements_norm, type(replacements)), "Replacements should be a list" + assert isinstance(jacobian_norm, type(reduced_expr)), "Jacobian should be a list" + assert isinstance(precomputed_fs_norm, list), "Precomputed free symbols should be a list" + assert len(replacements_norm) == len(replacements), "Length of replacements does not match" + assert len(jacobian_norm) == 1, "Jacobian should have one element" + assert len(precomputed_fs_norm) == len(replacements), "Length of precomputed free symbols does not match" + + +def test_jacobian_hessian(): + L = Matrix(1, 2, [x**2*y, 2*y**2 + x*y]) + syms = [x, y] + assert _forward_jacobian(L, syms) == Matrix([[2*x*y, x**2], [y, 4*y + x]]) + + L = Matrix(1, 2, [x, x**2*y**3]) + assert _forward_jacobian(L, syms) == Matrix([[1, 0], [2*x*y**3, x**2*3*y**2]]) + + +def test_jacobian_metrics(): + rho, phi = symbols("rho,phi") + X = Matrix([rho * cos(phi), rho * sin(phi)]) + Y = Matrix([rho, phi]) + J = _forward_jacobian(X, Y) + assert J == X.jacobian(Y.T) + assert J == (X.T).jacobian(Y) + assert J == (X.T).jacobian(Y.T) + g = J.T * eye(J.shape[0]) * J + g = g.applyfunc(trigsimp) + assert g == Matrix([[1, 0], [0, rho ** 2]]) + + +def test_jacobian2(): + rho, phi = symbols("rho,phi") + X = Matrix([rho * cos(phi), rho * sin(phi), rho ** 2]) + Y = Matrix([rho, phi]) + J = Matrix([ + [cos(phi), -rho * sin(phi)], + [sin(phi), rho * cos(phi)], + [2 * rho, 0], + ]) + assert _forward_jacobian(X, Y) == J + + +def test_issue_4564(): + X = Matrix([exp(x + y + z), exp(x + y + z), exp(x + y + z)]) + Y = Matrix([x, y, z]) + for i in range(1, 3): + for j in range(1, 3): + X_slice = X[:i, :] + Y_slice = Y[:j, :] + J = _forward_jacobian(X_slice, Y_slice) + assert J.rows == i + assert J.cols == j + for k in range(j): + assert J[:, k] == X_slice + + +def test_nonvectorJacobian(): + X = Matrix([[exp(x + y + z), exp(x + y + z)], + [exp(x + y + z), exp(x + y + z)]]) + raises(TypeError, lambda: _forward_jacobian(X, Matrix([x, y, z]))) + X = X[0, :] + Y = Matrix([[x, y], [x, z]]) + raises(TypeError, lambda: _forward_jacobian(X, Y)) + raises(TypeError, lambda: _forward_jacobian(X, Matrix([[x, y], [x, z]]))) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_epathtools.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_epathtools.py new file mode 100644 index 0000000000000000000000000000000000000000..a8bb47b2f2ff624077ab9905677b181c587ab5a7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_epathtools.py @@ -0,0 +1,90 @@ +"""Tests for tools for manipulation of expressions using paths. """ + +from sympy.simplify.epathtools import epath, EPath +from sympy.testing.pytest import raises + +from sympy.core.numbers import E +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.abc import x, y, z, t + + +def test_epath_select(): + expr = [((x, 1, t), 2), ((3, y, 4), z)] + + assert epath("/*", expr) == [((x, 1, t), 2), ((3, y, 4), z)] + assert epath("/*/*", expr) == [(x, 1, t), 2, (3, y, 4), z] + assert epath("/*/*/*", expr) == [x, 1, t, 3, y, 4] + assert epath("/*/*/*/*", expr) == [] + + assert epath("/[:]", expr) == [((x, 1, t), 2), ((3, y, 4), z)] + assert epath("/[:]/[:]", expr) == [(x, 1, t), 2, (3, y, 4), z] + assert epath("/[:]/[:]/[:]", expr) == [x, 1, t, 3, y, 4] + assert epath("/[:]/[:]/[:]/[:]", expr) == [] + + assert epath("/*/[:]", expr) == [(x, 1, t), 2, (3, y, 4), z] + + assert epath("/*/[0]", expr) == [(x, 1, t), (3, y, 4)] + assert epath("/*/[1]", expr) == [2, z] + assert epath("/*/[2]", expr) == [] + + assert epath("/*/int", expr) == [2] + assert epath("/*/Symbol", expr) == [z] + assert epath("/*/tuple", expr) == [(x, 1, t), (3, y, 4)] + assert epath("/*/__iter__?", expr) == [(x, 1, t), (3, y, 4)] + + assert epath("/*/int|tuple", expr) == [(x, 1, t), 2, (3, y, 4)] + assert epath("/*/Symbol|tuple", expr) == [(x, 1, t), (3, y, 4), z] + assert epath("/*/int|Symbol|tuple", expr) == [(x, 1, t), 2, (3, y, 4), z] + + assert epath("/*/int|__iter__?", expr) == [(x, 1, t), 2, (3, y, 4)] + assert epath("/*/Symbol|__iter__?", expr) == [(x, 1, t), (3, y, 4), z] + assert epath( + "/*/int|Symbol|__iter__?", expr) == [(x, 1, t), 2, (3, y, 4), z] + + assert epath("/*/[0]/int", expr) == [1, 3, 4] + assert epath("/*/[0]/Symbol", expr) == [x, t, y] + + assert epath("/*/[0]/int[1:]", expr) == [1, 4] + assert epath("/*/[0]/Symbol[1:]", expr) == [t, y] + + assert epath("/Symbol", x + y + z + 1) == [x, y, z] + assert epath("/*/*/Symbol", t + sin(x + 1) + cos(x + y + E)) == [x, x, y] + + +def test_epath_apply(): + expr = [((x, 1, t), 2), ((3, y, 4), z)] + func = lambda expr: expr**2 + + assert epath("/*", expr, list) == [[(x, 1, t), 2], [(3, y, 4), z]] + + assert epath("/*/[0]", expr, list) == [([x, 1, t], 2), ([3, y, 4], z)] + assert epath("/*/[1]", expr, func) == [((x, 1, t), 4), ((3, y, 4), z**2)] + assert epath("/*/[2]", expr, list) == expr + + assert epath("/*/[0]/int", expr, func) == [((x, 1, t), 2), ((9, y, 16), z)] + assert epath("/*/[0]/Symbol", expr, func) == [((x**2, 1, t**2), 2), + ((3, y**2, 4), z)] + assert epath( + "/*/[0]/int[1:]", expr, func) == [((x, 1, t), 2), ((3, y, 16), z)] + assert epath("/*/[0]/Symbol[1:]", expr, func) == [((x, 1, t**2), + 2), ((3, y**2, 4), z)] + + assert epath("/Symbol", x + y + z + 1, func) == x**2 + y**2 + z**2 + 1 + assert epath("/*/*/Symbol", t + sin(x + 1) + cos(x + y + E), func) == \ + t + sin(x**2 + 1) + cos(x**2 + y**2 + E) + + +def test_EPath(): + assert EPath("/*/[0]")._path == "/*/[0]" + assert EPath(EPath("/*/[0]"))._path == "/*/[0]" + assert isinstance(epath("/*/[0]"), EPath) is True + + assert repr(EPath("/*/[0]")) == "EPath('/*/[0]')" + + raises(ValueError, lambda: EPath("")) + raises(ValueError, lambda: EPath("/")) + raises(ValueError, lambda: EPath("/|x")) + raises(ValueError, lambda: EPath("/[")) + raises(ValueError, lambda: EPath("/[0]%")) + + raises(NotImplementedError, lambda: EPath("Symbol")) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_fu.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_fu.py new file mode 100644 index 0000000000000000000000000000000000000000..2de2126b7333195fceeffe72dc9cb642e7eba9a9 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_fu.py @@ -0,0 +1,492 @@ +from sympy.core.add import Add +from sympy.core.mul import Mul +from sympy.core.numbers import (I, Rational, pi) +from sympy.core.parameters import evaluate +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol, symbols) +from sympy.functions.elementary.hyperbolic import (cosh, coth, csch, sech, sinh, tanh) +from sympy.functions.elementary.miscellaneous import (root, sqrt) +from sympy.functions.elementary.trigonometric import (cos, cot, csc, sec, sin, tan) +from sympy.simplify.powsimp import powsimp +from sympy.simplify.fu import ( + L, TR1, TR10, TR10i, TR11, _TR11, TR12, TR12i, TR13, TR14, TR15, TR16, + TR111, TR2, TR2i, TR3, TR4, TR5, TR6, TR7, TR8, TR9, TRmorrie, _TR56 as T, + TRpower, hyper_as_trig, fu, process_common_addends, trig_split, + as_f_sign_1) +from sympy.core.random import verify_numerically +from sympy.abc import a, b, c, x, y, z + + +def test_TR1(): + assert TR1(2*csc(x) + sec(x)) == 1/cos(x) + 2/sin(x) + + +def test_TR2(): + assert TR2(tan(x)) == sin(x)/cos(x) + assert TR2(cot(x)) == cos(x)/sin(x) + assert TR2(tan(tan(x) - sin(x)/cos(x))) == 0 + + +def test_TR2i(): + # just a reminder that ratios of powers only simplify if both + # numerator and denominator satisfy the condition that each + # has a positive base or an integer exponent; e.g. the following, + # at y=-1, x=1/2 gives sqrt(2)*I != -sqrt(2)*I + assert powsimp(2**x/y**x) != (2/y)**x + + assert TR2i(sin(x)/cos(x)) == tan(x) + assert TR2i(sin(x)*sin(y)/cos(x)) == tan(x)*sin(y) + assert TR2i(1/(sin(x)/cos(x))) == 1/tan(x) + assert TR2i(1/(sin(x)*sin(y)/cos(x))) == 1/tan(x)/sin(y) + assert TR2i(sin(x)/2/(cos(x) + 1)) == sin(x)/(cos(x) + 1)/2 + + assert TR2i(sin(x)/2/(cos(x) + 1), half=True) == tan(x/2)/2 + assert TR2i(sin(1)/(cos(1) + 1), half=True) == tan(S.Half) + assert TR2i(sin(2)/(cos(2) + 1), half=True) == tan(1) + assert TR2i(sin(4)/(cos(4) + 1), half=True) == tan(2) + assert TR2i(sin(5)/(cos(5) + 1), half=True) == tan(5*S.Half) + assert TR2i((cos(1) + 1)/sin(1), half=True) == 1/tan(S.Half) + assert TR2i((cos(2) + 1)/sin(2), half=True) == 1/tan(1) + assert TR2i((cos(4) + 1)/sin(4), half=True) == 1/tan(2) + assert TR2i((cos(5) + 1)/sin(5), half=True) == 1/tan(5*S.Half) + assert TR2i((cos(1) + 1)**(-a)*sin(1)**a, half=True) == tan(S.Half)**a + assert TR2i((cos(2) + 1)**(-a)*sin(2)**a, half=True) == tan(1)**a + assert TR2i((cos(4) + 1)**(-a)*sin(4)**a, half=True) == (cos(4) + 1)**(-a)*sin(4)**a + assert TR2i((cos(5) + 1)**(-a)*sin(5)**a, half=True) == (cos(5) + 1)**(-a)*sin(5)**a + assert TR2i((cos(1) + 1)**a*sin(1)**(-a), half=True) == tan(S.Half)**(-a) + assert TR2i((cos(2) + 1)**a*sin(2)**(-a), half=True) == tan(1)**(-a) + assert TR2i((cos(4) + 1)**a*sin(4)**(-a), half=True) == (cos(4) + 1)**a*sin(4)**(-a) + assert TR2i((cos(5) + 1)**a*sin(5)**(-a), half=True) == (cos(5) + 1)**a*sin(5)**(-a) + + i = symbols('i', integer=True) + assert TR2i(((cos(5) + 1)**i*sin(5)**(-i)), half=True) == tan(5*S.Half)**(-i) + assert TR2i(1/((cos(5) + 1)**i*sin(5)**(-i)), half=True) == tan(5*S.Half)**i + + +def test_TR3(): + assert TR3(cos(y - x*(y - x))) == cos(x*(x - y) + y) + assert cos(pi/2 + x) == -sin(x) + assert cos(30*pi/2 + x) == -cos(x) + + for f in (cos, sin, tan, cot, csc, sec): + i = f(pi*Rational(3, 7)) + j = TR3(i) + assert verify_numerically(i, j) and i.func != j.func + + with evaluate(False): + eq = cos(9*pi/22) + assert eq.has(9*pi) and TR3(eq) == sin(pi/11) + + +def test_TR4(): + for i in [0, pi/6, pi/4, pi/3, pi/2]: + with evaluate(False): + eq = cos(i) + assert isinstance(eq, cos) and TR4(eq) == cos(i) + + +def test__TR56(): + h = lambda x: 1 - x + assert T(sin(x)**3, sin, cos, h, 4, False) == sin(x)*(-cos(x)**2 + 1) + assert T(sin(x)**10, sin, cos, h, 4, False) == sin(x)**10 + assert T(sin(x)**6, sin, cos, h, 6, False) == (-cos(x)**2 + 1)**3 + assert T(sin(x)**6, sin, cos, h, 6, True) == sin(x)**6 + assert T(sin(x)**8, sin, cos, h, 10, True) == (-cos(x)**2 + 1)**4 + + # issue 17137 + assert T(sin(x)**I, sin, cos, h, 4, True) == sin(x)**I + assert T(sin(x)**(2*I + 1), sin, cos, h, 4, True) == sin(x)**(2*I + 1) + + +def test_TR5(): + assert TR5(sin(x)**2) == -cos(x)**2 + 1 + assert TR5(sin(x)**-2) == sin(x)**(-2) + assert TR5(sin(x)**4) == (-cos(x)**2 + 1)**2 + + +def test_TR6(): + assert TR6(cos(x)**2) == -sin(x)**2 + 1 + assert TR6(cos(x)**-2) == cos(x)**(-2) + assert TR6(cos(x)**4) == (-sin(x)**2 + 1)**2 + + +def test_TR7(): + assert TR7(cos(x)**2) == cos(2*x)/2 + S.Half + assert TR7(cos(x)**2 + 1) == cos(2*x)/2 + Rational(3, 2) + + +def test_TR8(): + assert TR8(cos(2)*cos(3)) == cos(5)/2 + cos(1)/2 + assert TR8(cos(2)*sin(3)) == sin(5)/2 + sin(1)/2 + assert TR8(sin(2)*sin(3)) == -cos(5)/2 + cos(1)/2 + assert TR8(sin(1)*sin(2)*sin(3)) == sin(4)/4 - sin(6)/4 + sin(2)/4 + assert TR8(cos(2)*cos(3)*cos(4)*cos(5)) == \ + cos(4)/4 + cos(10)/8 + cos(2)/8 + cos(8)/8 + cos(14)/8 + \ + cos(6)/8 + Rational(1, 8) + assert TR8(cos(2)*cos(3)*cos(4)*cos(5)*cos(6)) == \ + cos(10)/8 + cos(4)/8 + 3*cos(2)/16 + cos(16)/16 + cos(8)/8 + \ + cos(14)/16 + cos(20)/16 + cos(12)/16 + Rational(1, 16) + cos(6)/8 + assert TR8(sin(pi*Rational(3, 7))**2*cos(pi*Rational(3, 7))**2/(16*sin(pi/7)**2)) == Rational(1, 64) + +def test_TR9(): + a = S.Half + b = 3*a + assert TR9(a) == a + assert TR9(cos(1) + cos(2)) == 2*cos(a)*cos(b) + assert TR9(cos(1) - cos(2)) == 2*sin(a)*sin(b) + assert TR9(sin(1) - sin(2)) == -2*sin(a)*cos(b) + assert TR9(sin(1) + sin(2)) == 2*sin(b)*cos(a) + assert TR9(cos(1) + 2*sin(1) + 2*sin(2)) == cos(1) + 4*sin(b)*cos(a) + assert TR9(cos(4) + cos(2) + 2*cos(1)*cos(3)) == 4*cos(1)*cos(3) + assert TR9((cos(4) + cos(2))/cos(3)/2 + cos(3)) == 2*cos(1)*cos(2) + assert TR9(cos(3) + cos(4) + cos(5) + cos(6)) == \ + 4*cos(S.Half)*cos(1)*cos(Rational(9, 2)) + assert TR9(cos(3) + cos(3)*cos(2)) == cos(3) + cos(2)*cos(3) + assert TR9(-cos(y) + cos(x*y)) == -2*sin(x*y/2 - y/2)*sin(x*y/2 + y/2) + assert TR9(-sin(y) + sin(x*y)) == 2*sin(x*y/2 - y/2)*cos(x*y/2 + y/2) + c = cos(x) + s = sin(x) + for si in ((1, 1), (1, -1), (-1, 1), (-1, -1)): + for a in ((c, s), (s, c), (cos(x), cos(x*y)), (sin(x), sin(x*y))): + args = zip(si, a) + ex = Add(*[Mul(*ai) for ai in args]) + t = TR9(ex) + assert not (a[0].func == a[1].func and ( + not verify_numerically(ex, t.expand(trig=True)) or t.is_Add) + or a[1].func != a[0].func and ex != t) + + +def test_TR10(): + assert TR10(cos(a + b)) == -sin(a)*sin(b) + cos(a)*cos(b) + assert TR10(sin(a + b)) == sin(a)*cos(b) + sin(b)*cos(a) + assert TR10(sin(a + b + c)) == \ + (-sin(a)*sin(b) + cos(a)*cos(b))*sin(c) + \ + (sin(a)*cos(b) + sin(b)*cos(a))*cos(c) + assert TR10(cos(a + b + c)) == \ + (-sin(a)*sin(b) + cos(a)*cos(b))*cos(c) - \ + (sin(a)*cos(b) + sin(b)*cos(a))*sin(c) + + +def test_TR10i(): + assert TR10i(cos(1)*cos(3) + sin(1)*sin(3)) == cos(2) + assert TR10i(cos(1)*cos(3) - sin(1)*sin(3)) == cos(4) + assert TR10i(cos(1)*sin(3) - sin(1)*cos(3)) == sin(2) + assert TR10i(cos(1)*sin(3) + sin(1)*cos(3)) == sin(4) + assert TR10i(cos(1)*sin(3) + sin(1)*cos(3) + 7) == sin(4) + 7 + assert TR10i(cos(1)*sin(3) + sin(1)*cos(3) + cos(3)) == cos(3) + sin(4) + assert TR10i(2*cos(1)*sin(3) + 2*sin(1)*cos(3) + cos(3)) == \ + 2*sin(4) + cos(3) + assert TR10i(cos(2)*cos(3) + sin(2)*(cos(1)*sin(2) + cos(2)*sin(1))) == \ + cos(1) + eq = (cos(2)*cos(3) + sin(2)*( + cos(1)*sin(2) + cos(2)*sin(1)))*cos(5) + sin(1)*sin(5) + assert TR10i(eq) == TR10i(eq.expand()) == cos(4) + assert TR10i(sqrt(2)*cos(x)*x + sqrt(6)*sin(x)*x) == \ + 2*sqrt(2)*x*sin(x + pi/6) + assert TR10i(cos(x)/sqrt(6) + sin(x)/sqrt(2) + + cos(x)/sqrt(6)/3 + sin(x)/sqrt(2)/3) == 4*sqrt(6)*sin(x + pi/6)/9 + assert TR10i(cos(x)/sqrt(6) + sin(x)/sqrt(2) + + cos(y)/sqrt(6)/3 + sin(y)/sqrt(2)/3) == \ + sqrt(6)*sin(x + pi/6)/3 + sqrt(6)*sin(y + pi/6)/9 + assert TR10i(cos(x) + sqrt(3)*sin(x) + 2*sqrt(3)*cos(x + pi/6)) == 4*cos(x) + assert TR10i(cos(x) + sqrt(3)*sin(x) + + 2*sqrt(3)*cos(x + pi/6) + 4*sin(x)) == 4*sqrt(2)*sin(x + pi/4) + assert TR10i(cos(2)*sin(3) + sin(2)*cos(4)) == \ + sin(2)*cos(4) + sin(3)*cos(2) + + A = Symbol('A', commutative=False) + assert TR10i(sqrt(2)*cos(x)*A + sqrt(6)*sin(x)*A) == \ + 2*sqrt(2)*sin(x + pi/6)*A + + + c = cos(x) + s = sin(x) + h = sin(y) + r = cos(y) + for si in ((1, 1), (1, -1), (-1, 1), (-1, -1)): + for argsi in ((c*r, s*h), (c*h, s*r)): # explicit 2-args + args = zip(si, argsi) + ex = Add(*[Mul(*ai) for ai in args]) + t = TR10i(ex) + assert not (ex - t.expand(trig=True) or t.is_Add) + + c = cos(x) + s = sin(x) + h = sin(pi/6) + r = cos(pi/6) + for si in ((1, 1), (1, -1), (-1, 1), (-1, -1)): + for argsi in ((c*r, s*h), (c*h, s*r)): # induced + args = zip(si, argsi) + ex = Add(*[Mul(*ai) for ai in args]) + t = TR10i(ex) + assert not (ex - t.expand(trig=True) or t.is_Add) + + +def test_TR11(): + + assert TR11(sin(2*x)) == 2*sin(x)*cos(x) + assert TR11(sin(4*x)) == 4*((-sin(x)**2 + cos(x)**2)*sin(x)*cos(x)) + assert TR11(sin(x*Rational(4, 3))) == \ + 4*((-sin(x/3)**2 + cos(x/3)**2)*sin(x/3)*cos(x/3)) + + assert TR11(cos(2*x)) == -sin(x)**2 + cos(x)**2 + assert TR11(cos(4*x)) == \ + (-sin(x)**2 + cos(x)**2)**2 - 4*sin(x)**2*cos(x)**2 + + assert TR11(cos(2)) == cos(2) + + assert TR11(cos(pi*Rational(3, 7)), pi*Rational(2, 7)) == -cos(pi*Rational(2, 7))**2 + sin(pi*Rational(2, 7))**2 + assert TR11(cos(4), 2) == -sin(2)**2 + cos(2)**2 + assert TR11(cos(6), 2) == cos(6) + assert TR11(sin(x)/cos(x/2), x/2) == 2*sin(x/2) + +def test__TR11(): + + assert _TR11(sin(x/3)*sin(2*x)*sin(x/4)/(cos(x/6)*cos(x/8))) == \ + 4*sin(x/8)*sin(x/6)*sin(2*x),_TR11(sin(x/3)*sin(2*x)*sin(x/4)/(cos(x/6)*cos(x/8))) + assert _TR11(sin(x/3)/cos(x/6)) == 2*sin(x/6) + + assert _TR11(cos(x/6)/sin(x/3)) == 1/(2*sin(x/6)) + assert _TR11(sin(2*x)*cos(x/8)/sin(x/4)) == sin(2*x)/(2*sin(x/8)), _TR11(sin(2*x)*cos(x/8)/sin(x/4)) + assert _TR11(sin(x)/sin(x/2)) == 2*cos(x/2) + + +def test_TR12(): + assert TR12(tan(x + y)) == (tan(x) + tan(y))/(-tan(x)*tan(y) + 1) + assert TR12(tan(x + y + z)) ==\ + (tan(z) + (tan(x) + tan(y))/(-tan(x)*tan(y) + 1))/( + 1 - (tan(x) + tan(y))*tan(z)/(-tan(x)*tan(y) + 1)) + assert TR12(tan(x*y)) == tan(x*y) + + +def test_TR13(): + assert TR13(tan(3)*tan(2)) == -tan(2)/tan(5) - tan(3)/tan(5) + 1 + assert TR13(cot(3)*cot(2)) == 1 + cot(3)*cot(5) + cot(2)*cot(5) + assert TR13(tan(1)*tan(2)*tan(3)) == \ + (-tan(2)/tan(5) - tan(3)/tan(5) + 1)*tan(1) + assert TR13(tan(1)*tan(2)*cot(3)) == \ + (-tan(2)/tan(3) + 1 - tan(1)/tan(3))*cot(3) + + +def test_L(): + assert L(cos(x) + sin(x)) == 2 + + +def test_fu(): + + assert fu(sin(50)**2 + cos(50)**2 + sin(pi/6)) == Rational(3, 2) + assert fu(sqrt(6)*cos(x) + sqrt(2)*sin(x)) == 2*sqrt(2)*sin(x + pi/3) + + + eq = sin(x)**4 - cos(y)**2 + sin(y)**2 + 2*cos(x)**2 + assert fu(eq) == cos(x)**4 - 2*cos(y)**2 + 2 + + assert fu(S.Half - cos(2*x)/2) == sin(x)**2 + + assert fu(sin(a)*(cos(b) - sin(b)) + cos(a)*(sin(b) + cos(b))) == \ + sqrt(2)*sin(a + b + pi/4) + + assert fu(sqrt(3)*cos(x)/2 + sin(x)/2) == sin(x + pi/3) + + assert fu(1 - sin(2*x)**2/4 - sin(y)**2 - cos(x)**4) == \ + -cos(x)**2 + cos(y)**2 + + assert fu(cos(pi*Rational(4, 9))) == sin(pi/18) + assert fu(cos(pi/9)*cos(pi*Rational(2, 9))*cos(pi*Rational(3, 9))*cos(pi*Rational(4, 9))) == Rational(1, 16) + + assert fu( + tan(pi*Rational(7, 18)) + tan(pi*Rational(5, 18)) - sqrt(3)*tan(pi*Rational(5, 18))*tan(pi*Rational(7, 18))) == \ + -sqrt(3) + + assert fu(tan(1)*tan(2)) == tan(1)*tan(2) + + expr = Mul(*[cos(2**i) for i in range(10)]) + assert fu(expr) == sin(1024)/(1024*sin(1)) + + # issue #18059: + assert fu(cos(x) + sqrt(sin(x)**2)) == cos(x) + sqrt(sin(x)**2) + + assert fu((-14*sin(x)**3 + 35*sin(x) + 6*sqrt(3)*cos(x)**3 + 9*sqrt(3)*cos(x))/((cos(2*x) + 4))) == \ + 7*sin(x) + 3*sqrt(3)*cos(x) + + +def test_objective(): + assert fu(sin(x)/cos(x), measure=lambda x: x.count_ops()) == \ + tan(x) + assert fu(sin(x)/cos(x), measure=lambda x: -x.count_ops()) == \ + sin(x)/cos(x) + + +def test_process_common_addends(): + # this tests that the args are not evaluated as they are given to do + # and that key2 works when key1 is False + do = lambda x: Add(*[i**(i%2) for i in x.args]) + assert process_common_addends(Add(*[1, 2, 3, 4], evaluate=False), do, + key2=lambda x: x%2, key1=False) == 1**1 + 3**1 + 2**0 + 4**0 + + +def test_trig_split(): + assert trig_split(cos(x), cos(y)) == (1, 1, 1, x, y, True) + assert trig_split(2*cos(x), -2*cos(y)) == (2, 1, -1, x, y, True) + assert trig_split(cos(x)*sin(y), cos(y)*sin(y)) == \ + (sin(y), 1, 1, x, y, True) + + assert trig_split(cos(x), -sqrt(3)*sin(x), two=True) == \ + (2, 1, -1, x, pi/6, False) + assert trig_split(cos(x), sin(x), two=True) == \ + (sqrt(2), 1, 1, x, pi/4, False) + assert trig_split(cos(x), -sin(x), two=True) == \ + (sqrt(2), 1, -1, x, pi/4, False) + assert trig_split(sqrt(2)*cos(x), -sqrt(6)*sin(x), two=True) == \ + (2*sqrt(2), 1, -1, x, pi/6, False) + assert trig_split(-sqrt(6)*cos(x), -sqrt(2)*sin(x), two=True) == \ + (-2*sqrt(2), 1, 1, x, pi/3, False) + assert trig_split(cos(x)/sqrt(6), sin(x)/sqrt(2), two=True) == \ + (sqrt(6)/3, 1, 1, x, pi/6, False) + assert trig_split(-sqrt(6)*cos(x)*sin(y), + -sqrt(2)*sin(x)*sin(y), two=True) == \ + (-2*sqrt(2)*sin(y), 1, 1, x, pi/3, False) + + assert trig_split(cos(x), sin(x)) is None + assert trig_split(cos(x), sin(z)) is None + assert trig_split(2*cos(x), -sin(x)) is None + assert trig_split(cos(x), -sqrt(3)*sin(x)) is None + assert trig_split(cos(x)*cos(y), sin(x)*sin(z)) is None + assert trig_split(cos(x)*cos(y), sin(x)*sin(y)) is None + assert trig_split(-sqrt(6)*cos(x), sqrt(2)*sin(x)*sin(y), two=True) is \ + None + + assert trig_split(sqrt(3)*sqrt(x), cos(3), two=True) is None + assert trig_split(sqrt(3)*root(x, 3), sin(3)*cos(2), two=True) is None + assert trig_split(cos(5)*cos(6), cos(7)*sin(5), two=True) is None + + +def test_TRmorrie(): + assert TRmorrie(7*Mul(*[cos(i) for i in range(10)])) == \ + 7*sin(12)*sin(16)*cos(5)*cos(7)*cos(9)/(64*sin(1)*sin(3)) + assert TRmorrie(x) == x + assert TRmorrie(2*x) == 2*x + e = cos(pi/7)*cos(pi*Rational(2, 7))*cos(pi*Rational(4, 7)) + assert TR8(TRmorrie(e)) == Rational(-1, 8) + e = Mul(*[cos(2**i*pi/17) for i in range(1, 17)]) + assert TR8(TR3(TRmorrie(e))) == Rational(1, 65536) + # issue 17063 + eq = cos(x)/cos(x/2) + assert TRmorrie(eq) == eq + # issue #20430 + eq = cos(x/2)*sin(x/2)*cos(x)**3 + assert TRmorrie(eq) == sin(2*x)*cos(x)**2/4 + + +def test_TRpower(): + assert TRpower(1/sin(x)**2) == 1/sin(x)**2 + assert TRpower(cos(x)**3*sin(x/2)**4) == \ + (3*cos(x)/4 + cos(3*x)/4)*(-cos(x)/2 + cos(2*x)/8 + Rational(3, 8)) + for k in range(2, 8): + assert verify_numerically(sin(x)**k, TRpower(sin(x)**k)) + assert verify_numerically(cos(x)**k, TRpower(cos(x)**k)) + + +def test_hyper_as_trig(): + from sympy.simplify.fu import _osborne, _osbornei + + eq = sinh(x)**2 + cosh(x)**2 + t, f = hyper_as_trig(eq) + assert f(fu(t)) == cosh(2*x) + e, f = hyper_as_trig(tanh(x + y)) + assert f(TR12(e)) == (tanh(x) + tanh(y))/(tanh(x)*tanh(y) + 1) + + d = Dummy() + assert _osborne(sinh(x), d) == I*sin(x*d) + assert _osborne(tanh(x), d) == I*tan(x*d) + assert _osborne(coth(x), d) == cot(x*d)/I + assert _osborne(cosh(x), d) == cos(x*d) + assert _osborne(sech(x), d) == sec(x*d) + assert _osborne(csch(x), d) == csc(x*d)/I + for func in (sinh, cosh, tanh, coth, sech, csch): + h = func(pi) + assert _osbornei(_osborne(h, d), d) == h + # /!\ the _osborne functions are not meant to work + # in the o(i(trig, d), d) direction so we just check + # that they work as they are supposed to work + assert _osbornei(cos(x*y + z), y) == cosh(x + z*I) + assert _osbornei(sin(x*y + z), y) == sinh(x + z*I)/I + assert _osbornei(tan(x*y + z), y) == tanh(x + z*I)/I + assert _osbornei(cot(x*y + z), y) == coth(x + z*I)*I + assert _osbornei(sec(x*y + z), y) == sech(x + z*I) + assert _osbornei(csc(x*y + z), y) == csch(x + z*I)*I + + +def test_TR12i(): + ta, tb, tc = [tan(i) for i in (a, b, c)] + assert TR12i((ta + tb)/(-ta*tb + 1)) == tan(a + b) + assert TR12i((ta + tb)/(ta*tb - 1)) == -tan(a + b) + assert TR12i((-ta - tb)/(ta*tb - 1)) == tan(a + b) + eq = (ta + tb)/(-ta*tb + 1)**2*(-3*ta - 3*tc)/(2*(ta*tc - 1)) + assert TR12i(eq.expand()) == \ + -3*tan(a + b)*tan(a + c)/(tan(a) + tan(b) - 1)/2 + assert TR12i(tan(x)/sin(x)) == tan(x)/sin(x) + eq = (ta + cos(2))/(-ta*tb + 1) + assert TR12i(eq) == eq + eq = (ta + tb + 2)**2/(-ta*tb + 1) + assert TR12i(eq) == eq + eq = ta/(-ta*tb + 1) + assert TR12i(eq) == eq + eq = (((ta + tb)*(a + 1)).expand())**2/(ta*tb - 1) + assert TR12i(eq) == -(a + 1)**2*tan(a + b) + + +def test_TR14(): + eq = (cos(x) - 1)*(cos(x) + 1) + ans = -sin(x)**2 + assert TR14(eq) == ans + assert TR14(1/eq) == 1/ans + assert TR14((cos(x) - 1)**2*(cos(x) + 1)**2) == ans**2 + assert TR14((cos(x) - 1)**2*(cos(x) + 1)**3) == ans**2*(cos(x) + 1) + assert TR14((cos(x) - 1)**3*(cos(x) + 1)**2) == ans**2*(cos(x) - 1) + eq = (cos(x) - 1)**y*(cos(x) + 1)**y + assert TR14(eq) == eq + eq = (cos(x) - 2)**y*(cos(x) + 1) + assert TR14(eq) == eq + eq = (tan(x) - 2)**2*(cos(x) + 1) + assert TR14(eq) == eq + i = symbols('i', integer=True) + assert TR14((cos(x) - 1)**i*(cos(x) + 1)**i) == ans**i + assert TR14((sin(x) - 1)**i*(sin(x) + 1)**i) == (-cos(x)**2)**i + # could use extraction in this case + eq = (cos(x) - 1)**(i + 1)*(cos(x) + 1)**i + assert TR14(eq) in [(cos(x) - 1)*ans**i, eq] + + assert TR14((sin(x) - 1)*(sin(x) + 1)) == -cos(x)**2 + p1 = (cos(x) + 1)*(cos(x) - 1) + p2 = (cos(y) - 1)*2*(cos(y) + 1) + p3 = (3*(cos(y) - 1))*(3*(cos(y) + 1)) + assert TR14(p1*p2*p3*(x - 1)) == -18*((x - 1)*sin(x)**2*sin(y)**4) + + +def test_TR15_16_17(): + assert TR15(1 - 1/sin(x)**2) == -cot(x)**2 + assert TR16(1 - 1/cos(x)**2) == -tan(x)**2 + assert TR111(1 - 1/tan(x)**2) == 1 - cot(x)**2 + + +def test_as_f_sign_1(): + assert as_f_sign_1(x + 1) == (1, x, 1) + assert as_f_sign_1(x - 1) == (1, x, -1) + assert as_f_sign_1(-x + 1) == (-1, x, -1) + assert as_f_sign_1(-x - 1) == (-1, x, 1) + assert as_f_sign_1(2*x + 2) == (2, x, 1) + assert as_f_sign_1(x*y - y) == (y, x, -1) + assert as_f_sign_1(-x*y + y) == (-y, x, -1) + + +def test_issue_25590(): + A = Symbol('A', commutative=False) + B = Symbol('B', commutative=False) + + assert TR8(2*cos(x)*sin(x)*B*A) == sin(2*x)*B*A + assert TR13(tan(2)*tan(3)*B*A) == (-tan(2)/tan(5) - tan(3)/tan(5) + 1)*B*A + + # XXX The result may not be optimal than + # sin(2*x)*B*A + cos(x)**2 and may change in the future + assert (2*cos(x)*sin(x)*B*A + cos(x)**2).simplify() == sin(2*x)*B*A + cos(2*x)/2 + S.One/2 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_function.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_function.py new file mode 100644 index 0000000000000000000000000000000000000000..441b9faf1bb3c5e7f2279b2a61066d050e45f773 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_function.py @@ -0,0 +1,54 @@ +""" Unit tests for Hyper_Function""" +from sympy.core import symbols, Dummy, Tuple, S, Rational +from sympy.functions import hyper + +from sympy.simplify.hyperexpand import Hyper_Function + +def test_attrs(): + a, b = symbols('a, b', cls=Dummy) + f = Hyper_Function([2, a], [b]) + assert f.ap == Tuple(2, a) + assert f.bq == Tuple(b) + assert f.args == (Tuple(2, a), Tuple(b)) + assert f.sizes == (2, 1) + +def test_call(): + a, b, x = symbols('a, b, x', cls=Dummy) + f = Hyper_Function([2, a], [b]) + assert f(x) == hyper([2, a], [b], x) + +def test_has(): + a, b, c = symbols('a, b, c', cls=Dummy) + f = Hyper_Function([2, -a], [b]) + assert f.has(a) + assert f.has(Tuple(b)) + assert not f.has(c) + +def test_eq(): + assert Hyper_Function([1], []) == Hyper_Function([1], []) + assert (Hyper_Function([1], []) != Hyper_Function([1], [])) is False + assert Hyper_Function([1], []) != Hyper_Function([2], []) + assert Hyper_Function([1], []) != Hyper_Function([1, 2], []) + assert Hyper_Function([1], []) != Hyper_Function([1], [2]) + +def test_gamma(): + assert Hyper_Function([2, 3], [-1]).gamma == 0 + assert Hyper_Function([-2, -3], [-1]).gamma == 2 + n = Dummy(integer=True) + assert Hyper_Function([-1, n, 1], []).gamma == 1 + assert Hyper_Function([-1, -n, 1], []).gamma == 1 + p = Dummy(integer=True, positive=True) + assert Hyper_Function([-1, p, 1], []).gamma == 1 + assert Hyper_Function([-1, -p, 1], []).gamma == 2 + +def test_suitable_origin(): + assert Hyper_Function((S.Half,), (Rational(3, 2),))._is_suitable_origin() is True + assert Hyper_Function((S.Half,), (S.Half,))._is_suitable_origin() is False + assert Hyper_Function((S.Half,), (Rational(-1, 2),))._is_suitable_origin() is False + assert Hyper_Function((S.Half,), (0,))._is_suitable_origin() is False + assert Hyper_Function((S.Half,), (-1, 1,))._is_suitable_origin() is False + assert Hyper_Function((S.Half, 0), (1,))._is_suitable_origin() is False + assert Hyper_Function((S.Half, 1), + (2, Rational(-2, 3)))._is_suitable_origin() is True + assert Hyper_Function((S.Half, 1), + (2, Rational(-2, 3), Rational(3, 2)))._is_suitable_origin() is True diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_gammasimp.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_gammasimp.py new file mode 100644 index 0000000000000000000000000000000000000000..e4c73093250b279510e3c2274db22818a9adffd8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_gammasimp.py @@ -0,0 +1,127 @@ +from sympy.core.function import Function +from sympy.core.numbers import (Rational, pi) +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.combinatorial.factorials import (rf, binomial, factorial) +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.functions.special.gamma_functions import gamma +from sympy.simplify.gammasimp import gammasimp +from sympy.simplify.powsimp import powsimp +from sympy.simplify.simplify import simplify + +from sympy.abc import x, y, n, k + + +def test_gammasimp(): + R = Rational + + # was part of test_combsimp_gamma() in test_combsimp.py + assert gammasimp(gamma(x)) == gamma(x) + assert gammasimp(gamma(x + 1)/x) == gamma(x) + assert gammasimp(gamma(x)/(x - 1)) == gamma(x - 1) + assert gammasimp(x*gamma(x)) == gamma(x + 1) + assert gammasimp((x + 1)*gamma(x + 1)) == gamma(x + 2) + assert gammasimp(gamma(x + y)*(x + y)) == gamma(x + y + 1) + assert gammasimp(x/gamma(x + 1)) == 1/gamma(x) + assert gammasimp((x + 1)**2/gamma(x + 2)) == (x + 1)/gamma(x + 1) + assert gammasimp(x*gamma(x) + gamma(x + 3)/(x + 2)) == \ + (x + 2)*gamma(x + 1) + + assert gammasimp(gamma(2*x)*x) == gamma(2*x + 1)/2 + assert gammasimp(gamma(2*x)/(x - S.Half)) == 2*gamma(2*x - 1) + + assert gammasimp(gamma(x)*gamma(1 - x)) == pi/sin(pi*x) + assert gammasimp(gamma(x)*gamma(-x)) == -pi/(x*sin(pi*x)) + assert gammasimp(1/gamma(x + 3)/gamma(1 - x)) == \ + sin(pi*x)/(pi*x*(x + 1)*(x + 2)) + + assert gammasimp(factorial(n + 2)) == gamma(n + 3) + assert gammasimp(binomial(n, k)) == \ + gamma(n + 1)/(gamma(k + 1)*gamma(-k + n + 1)) + + assert powsimp(gammasimp( + gamma(x)*gamma(x + S.Half)*gamma(y)/gamma(x + y))) == \ + 2**(-2*x + 1)*sqrt(pi)*gamma(2*x)*gamma(y)/gamma(x + y) + assert gammasimp(1/gamma(x)/gamma(x - Rational(1, 3))/gamma(x + Rational(1, 3))) == \ + 3**(3*x - Rational(3, 2))/(2*pi*gamma(3*x - 1)) + assert simplify( + gamma(S.Half + x/2)*gamma(1 + x/2)/gamma(1 + x)/sqrt(pi)*2**x) == 1 + assert gammasimp(gamma(Rational(-1, 4))*gamma(Rational(-3, 4))) == 16*sqrt(2)*pi/3 + + assert powsimp(gammasimp(gamma(2*x)/gamma(x))) == \ + 2**(2*x - 1)*gamma(x + S.Half)/sqrt(pi) + + # issue 6792 + e = (-gamma(k)*gamma(k + 2) + gamma(k + 1)**2)/gamma(k)**2 + assert gammasimp(e) == -k + assert gammasimp(1/e) == -1/k + e = (gamma(x) + gamma(x + 1))/gamma(x) + assert gammasimp(e) == x + 1 + assert gammasimp(1/e) == 1/(x + 1) + e = (gamma(x) + gamma(x + 2))*(gamma(x - 1) + gamma(x))/gamma(x) + assert gammasimp(e) == (x**2 + x + 1)*gamma(x + 1)/(x - 1) + e = (-gamma(k)*gamma(k + 2) + gamma(k + 1)**2)/gamma(k)**2 + assert gammasimp(e**2) == k**2 + assert gammasimp(e**2/gamma(k + 1)) == k/gamma(k) + a = R(1, 2) + R(1, 3) + b = a + R(1, 3) + assert gammasimp(gamma(2*k)/gamma(k)*gamma(k + a)*gamma(k + b) + ) == 3*2**(2*k + 1)*3**(-3*k - 2)*sqrt(pi)*gamma(3*k + R(3, 2))/2 + + # issue 9699 + assert gammasimp((x + 1)*factorial(x)/gamma(y)) == gamma(x + 2)/gamma(y) + assert gammasimp(rf(x + n, k)*binomial(n, k)).simplify() == Piecewise( + (gamma(n + 1)*gamma(k + n + x)/(gamma(k + 1)*gamma(n + x)*gamma(-k + n + 1)), n > -x), + ((-1)**k*gamma(n + 1)*gamma(-n - x + 1)/(gamma(k + 1)*gamma(-k + n + 1)*gamma(-k - n - x + 1)), True)) + + A, B = symbols('A B', commutative=False) + assert gammasimp(e*B*A) == gammasimp(e)*B*A + + # check iteration + assert gammasimp(gamma(2*k)/gamma(k)*gamma(-k - R(1, 2))) == ( + -2**(2*k + 1)*sqrt(pi)/(2*((2*k + 1)*cos(pi*k)))) + assert gammasimp( + gamma(k)*gamma(k + R(1, 3))*gamma(k + R(2, 3))/gamma(k*R(3, 2))) == ( + 3*2**(3*k + 1)*3**(-3*k - S.Half)*sqrt(pi)*gamma(k*R(3, 2) + S.Half)/2) + + # issue 6153 + assert gammasimp(gamma(Rational(1, 4))/gamma(Rational(5, 4))) == 4 + + # was part of test_combsimp() in test_combsimp.py + assert gammasimp(binomial(n + 2, k + S.Half)) == gamma(n + 3)/ \ + (gamma(k + R(3, 2))*gamma(-k + n + R(5, 2))) + assert gammasimp(binomial(n + 2, k + 2.0)) == \ + gamma(n + 3)/(gamma(k + 3.0)*gamma(-k + n + 1)) + + # issue 11548 + assert gammasimp(binomial(0, x)) == sin(pi*x)/(pi*x) + + e = gamma(n + Rational(1, 3))*gamma(n + R(2, 3)) + assert gammasimp(e) == e + assert gammasimp(gamma(4*n + S.Half)/gamma(2*n - R(3, 4))) == \ + 2**(4*n - R(5, 2))*(8*n - 3)*gamma(2*n + R(3, 4))/sqrt(pi) + + i, m = symbols('i m', integer = True) + e = gamma(exp(i)) + assert gammasimp(e) == e + e = gamma(m + 3) + assert gammasimp(e) == e + e = gamma(m + 1)/(gamma(i + 1)*gamma(-i + m + 1)) + assert gammasimp(e) == e + + p = symbols("p", integer=True, positive=True) + assert gammasimp(gamma(-p + 4)) == gamma(-p + 4) + + +def test_issue_22606(): + fx = Function('f')(x) + eq = x + gamma(y) + # seems like ans should be `eq`, not `(x*y + gamma(y + 1))/y` + ans = gammasimp(eq) + assert gammasimp(eq.subs(x, fx)).subs(fx, x) == ans + assert gammasimp(eq.subs(x, cos(x))).subs(cos(x), x) == ans + assert 1/gammasimp(1/eq) == ans + assert gammasimp(fx.subs(x, eq)).args[0] == ans diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_hyperexpand.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_hyperexpand.py new file mode 100644 index 0000000000000000000000000000000000000000..c703c228a13201de13cfd4c3413fc75a2cf5bdb6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_hyperexpand.py @@ -0,0 +1,1063 @@ +from sympy.core.random import randrange + +from sympy.simplify.hyperexpand import (ShiftA, ShiftB, UnShiftA, UnShiftB, + MeijerShiftA, MeijerShiftB, MeijerShiftC, MeijerShiftD, + MeijerUnShiftA, MeijerUnShiftB, MeijerUnShiftC, + MeijerUnShiftD, + ReduceOrder, reduce_order, apply_operators, + devise_plan, make_derivative_operator, Formula, + hyperexpand, Hyper_Function, G_Function, + reduce_order_meijer, + build_hypergeometric_formula) +from sympy.concrete.summations import Sum +from sympy.core.containers import Tuple +from sympy.core.expr import Expr +from sympy.core.numbers import I +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.combinatorial.factorials import binomial +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.special.hyper import (hyper, meijerg) +from sympy.abc import z, a, b, c +from sympy.testing.pytest import XFAIL, raises, slow, tooslow +from sympy.core.random import verify_numerically as tn + +from sympy.core.numbers import (Rational, pi) +from sympy.functions.elementary.exponential import (exp, exp_polar, log) +from sympy.functions.elementary.hyperbolic import atanh +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (asin, cos, sin) +from sympy.functions.special.bessel import besseli +from sympy.functions.special.error_functions import erf +from sympy.functions.special.gamma_functions import (gamma, lowergamma) + + +def test_branch_bug(): + assert hyperexpand(hyper((Rational(-1, 3), S.Half), (Rational(2, 3), Rational(3, 2)), -z)) == \ + -z**S('1/3')*lowergamma(exp_polar(I*pi)/3, z)/5 \ + + sqrt(pi)*erf(sqrt(z))/(5*sqrt(z)) + assert hyperexpand(meijerg([Rational(7, 6), 1], [], [Rational(2, 3)], [Rational(1, 6), 0], z)) == \ + 2*z**S('2/3')*(2*sqrt(pi)*erf(sqrt(z))/sqrt(z) - 2*lowergamma( + Rational(2, 3), z)/z**S('2/3'))*gamma(Rational(2, 3))/gamma(Rational(5, 3)) + + +def test_hyperexpand(): + # Luke, Y. L. (1969), The Special Functions and Their Approximations, + # Volume 1, section 6.2 + + assert hyperexpand(hyper([], [], z)) == exp(z) + assert hyperexpand(hyper([1, 1], [2], -z)*z) == log(1 + z) + assert hyperexpand(hyper([], [S.Half], -z**2/4)) == cos(z) + assert hyperexpand(z*hyper([], [S('3/2')], -z**2/4)) == sin(z) + assert hyperexpand(hyper([S('1/2'), S('1/2')], [S('3/2')], z**2)*z) \ + == asin(z) + assert isinstance(Sum(binomial(2, z)*z**2, (z, 0, a)).doit(), Expr) + + +def can_do(ap, bq, numerical=True, div=1, lowerplane=False): + r = hyperexpand(hyper(ap, bq, z)) + if r.has(hyper): + return False + if not numerical: + return True + repl = {} + randsyms = r.free_symbols - {z} + while randsyms: + # Only randomly generated parameters are checked. + for n, ai in enumerate(randsyms): + repl[ai] = randcplx(n)/div + if not any(b.is_Integer and b <= 0 for b in Tuple(*bq).subs(repl)): + break + [a, b, c, d] = [2, -1, 3, 1] + if lowerplane: + [a, b, c, d] = [2, -2, 3, -1] + return tn( + hyper(ap, bq, z).subs(repl), + r.replace(exp_polar, exp).subs(repl), + z, a=a, b=b, c=c, d=d) + + +def test_roach(): + # Kelly B. Roach. Meijer G Function Representations. + # Section "Gallery" + assert can_do([S.Half], [Rational(9, 2)]) + assert can_do([], [1, Rational(5, 2), 4]) + assert can_do([Rational(-1, 2), 1, 2], [3, 4]) + assert can_do([Rational(1, 3)], [Rational(-2, 3), Rational(-1, 2), S.Half, 1]) + assert can_do([Rational(-3, 2), Rational(-1, 2)], [Rational(-5, 2), 1]) + assert can_do([Rational(-3, 2), ], [Rational(-1, 2), S.Half]) # shine-integral + assert can_do([Rational(-3, 2), Rational(-1, 2)], [2]) # elliptic integrals + + +@XFAIL +def test_roach_fail(): + assert can_do([Rational(-1, 2), 1], [Rational(1, 4), S.Half, Rational(3, 4)]) # PFDD + assert can_do([Rational(3, 2)], [Rational(5, 2), 5]) # struve function + assert can_do([Rational(-1, 2), S.Half, 1], [Rational(3, 2), Rational(5, 2)]) # polylog, pfdd + assert can_do([1, 2, 3], [S.Half, 4]) # XXX ? + assert can_do([S.Half], [Rational(-1, 3), Rational(-1, 2), Rational(-2, 3)]) # PFDD ? + +# For the long table tests, see end of file + + +def test_polynomial(): + from sympy.core.numbers import oo + assert hyperexpand(hyper([], [-1], z)) is oo + assert hyperexpand(hyper([-2], [-1], z)) is oo + assert hyperexpand(hyper([0, 0], [-1], z)) == 1 + assert can_do([-5, -2, randcplx(), randcplx()], [-10, randcplx()]) + assert hyperexpand(hyper((-1, 1), (-2,), z)) == 1 + z/2 + + +def test_hyperexpand_bases(): + assert hyperexpand(hyper([2], [a], z)) == \ + a + z**(-a + 1)*(-a**2 + 3*a + z*(a - 1) - 2)*exp(z)* \ + lowergamma(a - 1, z) - 1 + # TODO [a+1, aRational(-1, 2)], [2*a] + assert hyperexpand(hyper([1, 2], [3], z)) == -2/z - 2*log(-z + 1)/z**2 + assert hyperexpand(hyper([S.Half, 2], [Rational(3, 2)], z)) == \ + -1/(2*z - 2) + atanh(sqrt(z))/sqrt(z)/2 + assert hyperexpand(hyper([S.Half, S.Half], [Rational(5, 2)], z)) == \ + (-3*z + 3)/4/(z*sqrt(-z + 1)) \ + + (6*z - 3)*asin(sqrt(z))/(4*z**Rational(3, 2)) + assert hyperexpand(hyper([1, 2], [Rational(3, 2)], z)) == -1/(2*z - 2) \ + - asin(sqrt(z))/(sqrt(z)*(2*z - 2)*sqrt(-z + 1)) + assert hyperexpand(hyper([Rational(-1, 2) - 1, 1, 2], [S.Half, 3], z)) == \ + sqrt(z)*(z*Rational(6, 7) - Rational(6, 5))*atanh(sqrt(z)) \ + + (-30*z**2 + 32*z - 6)/35/z - 6*log(-z + 1)/(35*z**2) + assert hyperexpand(hyper([1 + S.Half, 1, 1], [2, 2], z)) == \ + -4*log(sqrt(-z + 1)/2 + S.Half)/z + # TODO hyperexpand(hyper([a], [2*a + 1], z)) + # TODO [S.Half, a], [Rational(3, 2), a+1] + assert hyperexpand(hyper([2], [b, 1], z)) == \ + z**(-b/2 + S.Half)*besseli(b - 1, 2*sqrt(z))*gamma(b) \ + + z**(-b/2 + 1)*besseli(b, 2*sqrt(z))*gamma(b) + # TODO [a], [a - S.Half, 2*a] + + +def test_hyperexpand_parametric(): + assert hyperexpand(hyper([a, S.Half + a], [S.Half], z)) \ + == (1 + sqrt(z))**(-2*a)/2 + (1 - sqrt(z))**(-2*a)/2 + assert hyperexpand(hyper([a, Rational(-1, 2) + a], [2*a], z)) \ + == 2**(2*a - 1)*((-z + 1)**S.Half + 1)**(-2*a + 1) + + +def test_shifted_sum(): + from sympy.simplify.simplify import simplify + assert simplify(hyperexpand(z**4*hyper([2], [3, S('3/2')], -z**2))) \ + == z*sin(2*z) + (-z**2 + S.Half)*cos(2*z) - S.Half + + +def _randrat(): + """ Steer clear of integers. """ + return S(randrange(25) + 10)/50 + + +def randcplx(offset=-1): + """ Polys is not good with real coefficients. """ + return _randrat() + I*_randrat() + I*(1 + offset) + + +@slow +def test_formulae(): + from sympy.simplify.hyperexpand import FormulaCollection + formulae = FormulaCollection().formulae + for formula in formulae: + h = formula.func(formula.z) + rep = {} + for n, sym in enumerate(formula.symbols): + rep[sym] = randcplx(n) + + # NOTE hyperexpand returns truly branched functions. We know we are + # on the main sheet, but numerical evaluation can still go wrong + # (e.g. if exp_polar cannot be evalf'd). + # Just replace all exp_polar by exp, this usually works. + + # first test if the closed-form is actually correct + h = h.subs(rep) + closed_form = formula.closed_form.subs(rep).rewrite('nonrepsmall') + z = formula.z + assert tn(h, closed_form.replace(exp_polar, exp), z) + + # now test the computed matrix + cl = (formula.C * formula.B)[0].subs(rep).rewrite('nonrepsmall') + assert tn(closed_form.replace( + exp_polar, exp), cl.replace(exp_polar, exp), z) + deriv1 = z*formula.B.applyfunc(lambda t: t.rewrite( + 'nonrepsmall')).diff(z) + deriv2 = formula.M * formula.B + for d1, d2 in zip(deriv1, deriv2): + assert tn(d1.subs(rep).replace(exp_polar, exp), + d2.subs(rep).rewrite('nonrepsmall').replace(exp_polar, exp), z) + + +def test_meijerg_formulae(): + from sympy.simplify.hyperexpand import MeijerFormulaCollection + formulae = MeijerFormulaCollection().formulae + for sig in formulae: + for formula in formulae[sig]: + g = meijerg(formula.func.an, formula.func.ap, + formula.func.bm, formula.func.bq, + formula.z) + rep = {} + for sym in formula.symbols: + rep[sym] = randcplx() + + # first test if the closed-form is actually correct + g = g.subs(rep) + closed_form = formula.closed_form.subs(rep) + z = formula.z + assert tn(g, closed_form, z) + + # now test the computed matrix + cl = (formula.C * formula.B)[0].subs(rep) + assert tn(closed_form, cl, z) + deriv1 = z*formula.B.diff(z) + deriv2 = formula.M * formula.B + for d1, d2 in zip(deriv1, deriv2): + assert tn(d1.subs(rep), d2.subs(rep), z) + + +def op(f): + return z*f.diff(z) + + +def test_plan(): + assert devise_plan(Hyper_Function([0], ()), + Hyper_Function([0], ()), z) == [] + with raises(ValueError): + devise_plan(Hyper_Function([1], ()), Hyper_Function((), ()), z) + with raises(ValueError): + devise_plan(Hyper_Function([2], [1]), Hyper_Function([2], [2]), z) + with raises(ValueError): + devise_plan(Hyper_Function([2], []), Hyper_Function([S("1/2")], []), z) + + # We cannot use pi/(10000 + n) because polys is insanely slow. + a1, a2, b1 = (randcplx(n) for n in range(3)) + b1 += 2*I + h = hyper([a1, a2], [b1], z) + + h2 = hyper((a1 + 1, a2), [b1], z) + assert tn(apply_operators(h, + devise_plan(Hyper_Function((a1 + 1, a2), [b1]), + Hyper_Function((a1, a2), [b1]), z), op), + h2, z) + + h2 = hyper((a1 + 1, a2 - 1), [b1], z) + assert tn(apply_operators(h, + devise_plan(Hyper_Function((a1 + 1, a2 - 1), [b1]), + Hyper_Function((a1, a2), [b1]), z), op), + h2, z) + + +def test_plan_derivatives(): + a1, a2, a3 = 1, 2, S('1/2') + b1, b2 = 3, S('5/2') + h = Hyper_Function((a1, a2, a3), (b1, b2)) + h2 = Hyper_Function((a1 + 1, a2 + 1, a3 + 2), (b1 + 1, b2 + 1)) + ops = devise_plan(h2, h, z) + f = Formula(h, z, h(z), []) + deriv = make_derivative_operator(f.M, z) + assert tn((apply_operators(f.C, ops, deriv)*f.B)[0], h2(z), z) + + h2 = Hyper_Function((a1, a2 - 1, a3 - 2), (b1 - 1, b2 - 1)) + ops = devise_plan(h2, h, z) + assert tn((apply_operators(f.C, ops, deriv)*f.B)[0], h2(z), z) + + +def test_reduction_operators(): + a1, a2, b1 = (randcplx(n) for n in range(3)) + h = hyper([a1], [b1], z) + + assert ReduceOrder(2, 0) is None + assert ReduceOrder(2, -1) is None + assert ReduceOrder(1, S('1/2')) is None + + h2 = hyper((a1, a2), (b1, a2), z) + assert tn(ReduceOrder(a2, a2).apply(h, op), h2, z) + + h2 = hyper((a1, a2 + 1), (b1, a2), z) + assert tn(ReduceOrder(a2 + 1, a2).apply(h, op), h2, z) + + h2 = hyper((a2 + 4, a1), (b1, a2), z) + assert tn(ReduceOrder(a2 + 4, a2).apply(h, op), h2, z) + + # test several step order reduction + ap = (a2 + 4, a1, b1 + 1) + bq = (a2, b1, b1) + func, ops = reduce_order(Hyper_Function(ap, bq)) + assert func.ap == (a1,) + assert func.bq == (b1,) + assert tn(apply_operators(h, ops, op), hyper(ap, bq, z), z) + + +def test_shift_operators(): + a1, a2, b1, b2, b3 = (randcplx(n) for n in range(5)) + h = hyper((a1, a2), (b1, b2, b3), z) + + raises(ValueError, lambda: ShiftA(0)) + raises(ValueError, lambda: ShiftB(1)) + + assert tn(ShiftA(a1).apply(h, op), hyper((a1 + 1, a2), (b1, b2, b3), z), z) + assert tn(ShiftA(a2).apply(h, op), hyper((a1, a2 + 1), (b1, b2, b3), z), z) + assert tn(ShiftB(b1).apply(h, op), hyper((a1, a2), (b1 - 1, b2, b3), z), z) + assert tn(ShiftB(b2).apply(h, op), hyper((a1, a2), (b1, b2 - 1, b3), z), z) + assert tn(ShiftB(b3).apply(h, op), hyper((a1, a2), (b1, b2, b3 - 1), z), z) + + +def test_ushift_operators(): + a1, a2, b1, b2, b3 = (randcplx(n) for n in range(5)) + h = hyper((a1, a2), (b1, b2, b3), z) + + raises(ValueError, lambda: UnShiftA((1,), (), 0, z)) + raises(ValueError, lambda: UnShiftB((), (-1,), 0, z)) + raises(ValueError, lambda: UnShiftA((1,), (0, -1, 1), 0, z)) + raises(ValueError, lambda: UnShiftB((0, 1), (1,), 0, z)) + + s = UnShiftA((a1, a2), (b1, b2, b3), 0, z) + assert tn(s.apply(h, op), hyper((a1 - 1, a2), (b1, b2, b3), z), z) + s = UnShiftA((a1, a2), (b1, b2, b3), 1, z) + assert tn(s.apply(h, op), hyper((a1, a2 - 1), (b1, b2, b3), z), z) + + s = UnShiftB((a1, a2), (b1, b2, b3), 0, z) + assert tn(s.apply(h, op), hyper((a1, a2), (b1 + 1, b2, b3), z), z) + s = UnShiftB((a1, a2), (b1, b2, b3), 1, z) + assert tn(s.apply(h, op), hyper((a1, a2), (b1, b2 + 1, b3), z), z) + s = UnShiftB((a1, a2), (b1, b2, b3), 2, z) + assert tn(s.apply(h, op), hyper((a1, a2), (b1, b2, b3 + 1), z), z) + + +def can_do_meijer(a1, a2, b1, b2, numeric=True): + """ + This helper function tries to hyperexpand() the meijer g-function + corresponding to the parameters a1, a2, b1, b2. + It returns False if this expansion still contains g-functions. + If numeric is True, it also tests the so-obtained formula numerically + (at random values) and returns False if the test fails. + Else it returns True. + """ + from sympy.core.function import expand + from sympy.functions.elementary.complexes import unpolarify + r = hyperexpand(meijerg(a1, a2, b1, b2, z)) + if r.has(meijerg): + return False + # NOTE hyperexpand() returns a truly branched function, whereas numerical + # evaluation only works on the main branch. Since we are evaluating on + # the main branch, this should not be a problem, but expressions like + # exp_polar(I*pi/2*x)**a are evaluated incorrectly. We thus have to get + # rid of them. The expand heuristically does this... + r = unpolarify(expand(r, force=True, power_base=True, power_exp=False, + mul=False, log=False, multinomial=False, basic=False)) + + if not numeric: + return True + + repl = {} + for n, ai in enumerate(meijerg(a1, a2, b1, b2, z).free_symbols - {z}): + repl[ai] = randcplx(n) + return tn(meijerg(a1, a2, b1, b2, z).subs(repl), r.subs(repl), z) + + +@slow +def test_meijerg_expand(): + from sympy.simplify.gammasimp import gammasimp + from sympy.simplify.simplify import simplify + # from mpmath docs + assert hyperexpand(meijerg([[], []], [[0], []], -z)) == exp(z) + + assert hyperexpand(meijerg([[1, 1], []], [[1], [0]], z)) == \ + log(z + 1) + assert hyperexpand(meijerg([[1, 1], []], [[1], [1]], z)) == \ + z/(z + 1) + assert hyperexpand(meijerg([[], []], [[S.Half], [0]], (z/2)**2)) \ + == sin(z)/sqrt(pi) + assert hyperexpand(meijerg([[], []], [[0], [S.Half]], (z/2)**2)) \ + == cos(z)/sqrt(pi) + assert can_do_meijer([], [a], [a - 1, a - S.Half], []) + assert can_do_meijer([], [], [a/2], [-a/2], False) # branches... + assert can_do_meijer([a], [b], [a], [b, a - 1]) + + # wikipedia + assert hyperexpand(meijerg([1], [], [], [0], z)) == \ + Piecewise((0, abs(z) < 1), (1, abs(1/z) < 1), + (meijerg([1], [], [], [0], z), True)) + assert hyperexpand(meijerg([], [1], [0], [], z)) == \ + Piecewise((1, abs(z) < 1), (0, abs(1/z) < 1), + (meijerg([], [1], [0], [], z), True)) + + # The Special Functions and their Approximations + assert can_do_meijer([], [], [a + b/2], [a, a - b/2, a + S.Half]) + assert can_do_meijer( + [], [], [a], [b], False) # branches only agree for small z + assert can_do_meijer([], [S.Half], [a], [-a]) + assert can_do_meijer([], [], [a, b], []) + assert can_do_meijer([], [], [a, b], []) + assert can_do_meijer([], [], [a, a + S.Half], [b, b + S.Half]) + assert can_do_meijer([], [], [a, -a], [0, S.Half], False) # dito + assert can_do_meijer([], [], [a, a + S.Half, b, b + S.Half], []) + assert can_do_meijer([S.Half], [], [0], [a, -a]) + assert can_do_meijer([S.Half], [], [a], [0, -a], False) # dito + assert can_do_meijer([], [a - S.Half], [a, b], [a - S.Half], False) + assert can_do_meijer([], [a + S.Half], [a + b, a - b, a], [], False) + assert can_do_meijer([a + S.Half], [], [b, 2*a - b, a], [], False) + + # This for example is actually zero. + assert can_do_meijer([], [], [], [a, b]) + + # Testing a bug: + assert hyperexpand(meijerg([0, 2], [], [], [-1, 1], z)) == \ + Piecewise((0, abs(z) < 1), + (z*(1 - 1/z**2)/2, abs(1/z) < 1), + (meijerg([0, 2], [], [], [-1, 1], z), True)) + + # Test that the simplest possible answer is returned: + assert gammasimp(simplify(hyperexpand( + meijerg([1], [1 - a], [-a/2, -a/2 + S.Half], [], 1/z)))) == \ + -2*sqrt(pi)*(sqrt(z + 1) + 1)**a/a + + # Test that hyper is returned + assert hyperexpand(meijerg([1], [], [a], [0, 0], z)) == hyper( + (a,), (a + 1, a + 1), z*exp_polar(I*pi))*z**a*gamma(a)/gamma(a + 1)**2 + + # Test place option + f = meijerg(((0, 1), ()), ((S.Half,), (0,)), z**2) + assert hyperexpand(f) == sqrt(pi)/sqrt(1 + z**(-2)) + assert hyperexpand(f, place=0) == sqrt(pi)*z/sqrt(z**2 + 1) + + +def test_meijerg_lookup(): + from sympy.functions.special.error_functions import (Ci, Si) + from sympy.functions.special.gamma_functions import uppergamma + assert hyperexpand(meijerg([a], [], [b, a], [], z)) == \ + z**b*exp(z)*gamma(-a + b + 1)*uppergamma(a - b, z) + assert hyperexpand(meijerg([0], [], [0, 0], [], z)) == \ + exp(z)*uppergamma(0, z) + assert can_do_meijer([a], [], [b, a + 1], []) + assert can_do_meijer([a], [], [b + 2, a], []) + assert can_do_meijer([a], [], [b - 2, a], []) + + assert hyperexpand(meijerg([a], [], [a, a, a - S.Half], [], z)) == \ + -sqrt(pi)*z**(a - S.Half)*(2*cos(2*sqrt(z))*(Si(2*sqrt(z)) - pi/2) + - 2*sin(2*sqrt(z))*Ci(2*sqrt(z))) == \ + hyperexpand(meijerg([a], [], [a, a - S.Half, a], [], z)) == \ + hyperexpand(meijerg([a], [], [a - S.Half, a, a], [], z)) + assert can_do_meijer([a - 1], [], [a + 2, a - Rational(3, 2), a + 1], []) + + +@XFAIL +def test_meijerg_expand_fail(): + # These basically test hyper([], [1/2 - a, 1/2 + 1, 1/2], z), + # which is *very* messy. But since the meijer g actually yields a + # sum of bessel functions, things can sometimes be simplified a lot and + # are then put into tables... + assert can_do_meijer([], [], [a + S.Half], [a, a - b/2, a + b/2]) + assert can_do_meijer([], [], [0, S.Half], [a, -a]) + assert can_do_meijer([], [], [3*a - S.Half, a, -a - S.Half], [a - S.Half]) + assert can_do_meijer([], [], [0, a - S.Half, -a - S.Half], [S.Half]) + assert can_do_meijer([], [], [a, b + S.Half, b], [2*b - a]) + assert can_do_meijer([], [], [a, b + S.Half, b, 2*b - a]) + assert can_do_meijer([S.Half], [], [-a, a], [0]) + + +@slow +def test_meijerg(): + # carefully set up the parameters. + # NOTE: this used to fail sometimes. I believe it is fixed, but if you + # hit an inexplicable test failure here, please let me know the seed. + a1, a2 = (randcplx(n) - 5*I - n*I for n in range(2)) + b1, b2 = (randcplx(n) + 5*I + n*I for n in range(2)) + b3, b4, b5, a3, a4, a5 = (randcplx() for n in range(6)) + g = meijerg([a1], [a3, a4], [b1], [b3, b4], z) + + assert ReduceOrder.meijer_minus(3, 4) is None + assert ReduceOrder.meijer_plus(4, 3) is None + + g2 = meijerg([a1, a2], [a3, a4], [b1], [b3, b4, a2], z) + assert tn(ReduceOrder.meijer_plus(a2, a2).apply(g, op), g2, z) + + g2 = meijerg([a1, a2], [a3, a4], [b1], [b3, b4, a2 + 1], z) + assert tn(ReduceOrder.meijer_plus(a2, a2 + 1).apply(g, op), g2, z) + + g2 = meijerg([a1, a2 - 1], [a3, a4], [b1], [b3, b4, a2 + 2], z) + assert tn(ReduceOrder.meijer_plus(a2 - 1, a2 + 2).apply(g, op), g2, z) + + g2 = meijerg([a1], [a3, a4, b2 - 1], [b1, b2 + 2], [b3, b4], z) + assert tn(ReduceOrder.meijer_minus( + b2 + 2, b2 - 1).apply(g, op), g2, z, tol=1e-6) + + # test several-step reduction + an = [a1, a2] + bq = [b3, b4, a2 + 1] + ap = [a3, a4, b2 - 1] + bm = [b1, b2 + 1] + niq, ops = reduce_order_meijer(G_Function(an, ap, bm, bq)) + assert niq.an == (a1,) + assert set(niq.ap) == {a3, a4} + assert niq.bm == (b1,) + assert set(niq.bq) == {b3, b4} + assert tn(apply_operators(g, ops, op), meijerg(an, ap, bm, bq, z), z) + + +def test_meijerg_shift_operators(): + # carefully set up the parameters. XXX this still fails sometimes + a1, a2, a3, a4, a5, b1, b2, b3, b4, b5 = (randcplx(n) for n in range(10)) + g = meijerg([a1], [a3, a4], [b1], [b3, b4], z) + + assert tn(MeijerShiftA(b1).apply(g, op), + meijerg([a1], [a3, a4], [b1 + 1], [b3, b4], z), z) + assert tn(MeijerShiftB(a1).apply(g, op), + meijerg([a1 - 1], [a3, a4], [b1], [b3, b4], z), z) + assert tn(MeijerShiftC(b3).apply(g, op), + meijerg([a1], [a3, a4], [b1], [b3 + 1, b4], z), z) + assert tn(MeijerShiftD(a3).apply(g, op), + meijerg([a1], [a3 - 1, a4], [b1], [b3, b4], z), z) + + s = MeijerUnShiftA([a1], [a3, a4], [b1], [b3, b4], 0, z) + assert tn( + s.apply(g, op), meijerg([a1], [a3, a4], [b1 - 1], [b3, b4], z), z) + + s = MeijerUnShiftC([a1], [a3, a4], [b1], [b3, b4], 0, z) + assert tn( + s.apply(g, op), meijerg([a1], [a3, a4], [b1], [b3 - 1, b4], z), z) + + s = MeijerUnShiftB([a1], [a3, a4], [b1], [b3, b4], 0, z) + assert tn( + s.apply(g, op), meijerg([a1 + 1], [a3, a4], [b1], [b3, b4], z), z) + + s = MeijerUnShiftD([a1], [a3, a4], [b1], [b3, b4], 0, z) + assert tn( + s.apply(g, op), meijerg([a1], [a3 + 1, a4], [b1], [b3, b4], z), z) + + +@slow +def test_meijerg_confluence(): + def t(m, a, b): + from sympy.core.sympify import sympify + a, b = sympify([a, b]) + m_ = m + m = hyperexpand(m) + if not m == Piecewise((a, abs(z) < 1), (b, abs(1/z) < 1), (m_, True)): + return False + if not (m.args[0].args[0] == a and m.args[1].args[0] == b): + return False + z0 = randcplx()/10 + if abs(m.subs(z, z0).n() - a.subs(z, z0).n()).n() > 1e-10: + return False + if abs(m.subs(z, 1/z0).n() - b.subs(z, 1/z0).n()).n() > 1e-10: + return False + return True + + assert t(meijerg([], [1, 1], [0, 0], [], z), -log(z), 0) + assert t(meijerg( + [], [3, 1], [0, 0], [], z), -z**2/4 + z - log(z)/2 - Rational(3, 4), 0) + assert t(meijerg([], [3, 1], [-1, 0], [], z), + z**2/12 - z/2 + log(z)/2 + Rational(1, 4) + 1/(6*z), 0) + assert t(meijerg([], [1, 1, 1, 1], [0, 0, 0, 0], [], z), -log(z)**3/6, 0) + assert t(meijerg([1, 1], [], [], [0, 0], z), 0, -log(1/z)) + assert t(meijerg([1, 1], [2, 2], [1, 1], [0, 0], z), + -z*log(z) + 2*z, -log(1/z) + 2) + assert t(meijerg([S.Half], [1, 1], [0, 0], [Rational(3, 2)], z), log(z)/2 - 1, 0) + + def u(an, ap, bm, bq): + m = meijerg(an, ap, bm, bq, z) + m2 = hyperexpand(m, allow_hyper=True) + if m2.has(meijerg) and not (m2.is_Piecewise and len(m2.args) == 3): + return False + return tn(m, m2, z) + assert u([], [1], [0, 0], []) + assert u([1, 1], [], [], [0]) + assert u([1, 1], [2, 2, 5], [1, 1, 6], [0, 0]) + assert u([1, 1], [2, 2, 5], [1, 1, 6], [0]) + + +def test_meijerg_with_Floats(): + # see issue #10681 + from sympy.polys.domains.realfield import RR + f = meijerg(((3.0, 1), ()), ((Rational(3, 2),), (0,)), z) + a = -2.3632718012073 + g = a*z**Rational(3, 2)*hyper((-0.5, Rational(3, 2)), (Rational(5, 2),), z*exp_polar(I*pi)) + assert RR.almosteq((hyperexpand(f)/g).n(), 1.0, 1e-12) + + +def test_lerchphi(): + from sympy.functions.special.zeta_functions import (lerchphi, polylog) + from sympy.simplify.gammasimp import gammasimp + assert hyperexpand(hyper([1, a], [a + 1], z)/a) == lerchphi(z, 1, a) + assert hyperexpand( + hyper([1, a, a], [a + 1, a + 1], z)/a**2) == lerchphi(z, 2, a) + assert hyperexpand(hyper([1, a, a, a], [a + 1, a + 1, a + 1], z)/a**3) == \ + lerchphi(z, 3, a) + assert hyperexpand(hyper([1] + [a]*10, [a + 1]*10, z)/a**10) == \ + lerchphi(z, 10, a) + assert gammasimp(hyperexpand(meijerg([0, 1 - a], [], [0], + [-a], exp_polar(-I*pi)*z))) == lerchphi(z, 1, a) + assert gammasimp(hyperexpand(meijerg([0, 1 - a, 1 - a], [], [0], + [-a, -a], exp_polar(-I*pi)*z))) == lerchphi(z, 2, a) + assert gammasimp(hyperexpand(meijerg([0, 1 - a, 1 - a, 1 - a], [], [0], + [-a, -a, -a], exp_polar(-I*pi)*z))) == lerchphi(z, 3, a) + + assert hyperexpand(z*hyper([1, 1], [2], z)) == -log(1 + -z) + assert hyperexpand(z*hyper([1, 1, 1], [2, 2], z)) == polylog(2, z) + assert hyperexpand(z*hyper([1, 1, 1, 1], [2, 2, 2], z)) == polylog(3, z) + + assert hyperexpand(hyper([1, a, 1 + S.Half], [a + 1, S.Half], z)) == \ + -2*a/(z - 1) + (-2*a**2 + a)*lerchphi(z, 1, a) + + # Now numerical tests. These make sure reductions etc are carried out + # correctly + + # a rational function (polylog at negative integer order) + assert can_do([2, 2, 2], [1, 1]) + + # NOTE these contain log(1-x) etc ... better make sure we have |z| < 1 + # reduction of order for polylog + assert can_do([1, 1, 1, b + 5], [2, 2, b], div=10) + + # reduction of order for lerchphi + # XXX lerchphi in mpmath is flaky + assert can_do( + [1, a, a, a, b + 5], [a + 1, a + 1, a + 1, b], numerical=False) + + # test a bug + from sympy.functions.elementary.complexes import Abs + assert hyperexpand(hyper([S.Half, S.Half, S.Half, 1], + [Rational(3, 2), Rational(3, 2), Rational(3, 2)], Rational(1, 4))) == \ + Abs(-polylog(3, exp_polar(I*pi)/2) + polylog(3, S.Half)) + + +def test_partial_simp(): + # First test that hypergeometric function formulae work. + a, b, c, d, e = (randcplx() for _ in range(5)) + for func in [Hyper_Function([a, b, c], [d, e]), + Hyper_Function([], [a, b, c, d, e])]: + f = build_hypergeometric_formula(func) + z = f.z + assert f.closed_form == func(z) + deriv1 = f.B.diff(z)*z + deriv2 = f.M*f.B + for func1, func2 in zip(deriv1, deriv2): + assert tn(func1, func2, z) + + # Now test that formulae are partially simplified. + a, b, z = symbols('a b z') + assert hyperexpand(hyper([3, a], [1, b], z)) == \ + (-a*b/2 + a*z/2 + 2*a)*hyper([a + 1], [b], z) \ + + (a*b/2 - 2*a + 1)*hyper([a], [b], z) + assert tn( + hyperexpand(hyper([3, d], [1, e], z)), hyper([3, d], [1, e], z), z) + assert hyperexpand(hyper([3], [1, a, b], z)) == \ + hyper((), (a, b), z) \ + + z*hyper((), (a + 1, b), z)/(2*a) \ + - z*(b - 4)*hyper((), (a + 1, b + 1), z)/(2*a*b) + assert tn( + hyperexpand(hyper([3], [1, d, e], z)), hyper([3], [1, d, e], z), z) + + +def test_hyperexpand_special(): + assert hyperexpand(hyper([a, b], [c], 1)) == \ + gamma(c)*gamma(c - a - b)/gamma(c - a)/gamma(c - b) + assert hyperexpand(hyper([a, b], [1 + a - b], -1)) == \ + gamma(1 + a/2)*gamma(1 + a - b)/gamma(1 + a)/gamma(1 + a/2 - b) + assert hyperexpand(hyper([a, b], [1 + b - a], -1)) == \ + gamma(1 + b/2)*gamma(1 + b - a)/gamma(1 + b)/gamma(1 + b/2 - a) + assert hyperexpand(meijerg([1 - z - a/2], [1 - z + a/2], [b/2], [-b/2], 1)) == \ + gamma(1 - 2*z)*gamma(z + a/2 + b/2)/gamma(1 - z + a/2 - b/2) \ + /gamma(1 - z - a/2 + b/2)/gamma(1 - z + a/2 + b/2) + assert hyperexpand(hyper([a], [b], 0)) == 1 + assert hyper([a], [b], 0) != 0 + + +def test_Mod1_behavior(): + from sympy.core.symbol import Symbol + from sympy.simplify.simplify import simplify + n = Symbol('n', integer=True) + # Note: this should not hang. + assert simplify(hyperexpand(meijerg([1], [], [n + 1], [0], z))) == \ + lowergamma(n + 1, z) + + +@slow +def test_prudnikov_misc(): + assert can_do([1, (3 + I)/2, (3 - I)/2], [Rational(3, 2), 2]) + assert can_do([S.Half, a - 1], [Rational(3, 2), a + 1], lowerplane=True) + assert can_do([], [b + 1]) + assert can_do([a], [a - 1, b + 1]) + + assert can_do([a], [a - S.Half, 2*a]) + assert can_do([a], [a - S.Half, 2*a + 1]) + assert can_do([a], [a - S.Half, 2*a - 1]) + assert can_do([a], [a + S.Half, 2*a]) + assert can_do([a], [a + S.Half, 2*a + 1]) + assert can_do([a], [a + S.Half, 2*a - 1]) + assert can_do([S.Half], [b, 2 - b]) + assert can_do([S.Half], [b, 3 - b]) + assert can_do([1], [2, b]) + + assert can_do([a, a + S.Half], [2*a, b, 2*a - b + 1]) + assert can_do([a, a + S.Half], [S.Half, 2*a, 2*a + S.Half]) + assert can_do([a], [a + 1], lowerplane=True) # lowergamma + + +def test_prudnikov_1(): + # A. P. Prudnikov, Yu. A. Brychkov and O. I. Marichev (1990). + # Integrals and Series: More Special Functions, Vol. 3,. + # Gordon and Breach Science Publisher + + # 7.3.1 + assert can_do([a, -a], [S.Half]) + assert can_do([a, 1 - a], [S.Half]) + assert can_do([a, 1 - a], [Rational(3, 2)]) + assert can_do([a, 2 - a], [S.Half]) + assert can_do([a, 2 - a], [Rational(3, 2)]) + assert can_do([a, 2 - a], [Rational(3, 2)]) + assert can_do([a, a + S.Half], [2*a - 1]) + assert can_do([a, a + S.Half], [2*a]) + assert can_do([a, a + S.Half], [2*a + 1]) + assert can_do([a, a + S.Half], [S.Half]) + assert can_do([a, a + S.Half], [Rational(3, 2)]) + assert can_do([a, a/2 + 1], [a/2]) + assert can_do([1, b], [2]) + assert can_do([1, b], [b + 1], numerical=False) # Lerch Phi + # NOTE: branches are complicated for |z| > 1 + + assert can_do([a], [2*a]) + assert can_do([a], [2*a + 1]) + assert can_do([a], [2*a - 1]) + + +@slow +def test_prudnikov_2(): + h = S.Half + assert can_do([-h, -h], [h]) + assert can_do([-h, h], [3*h]) + assert can_do([-h, h], [5*h]) + assert can_do([-h, h], [7*h]) + assert can_do([-h, 1], [h]) + + for p in [-h, h]: + for n in [-h, h, 1, 3*h, 2, 5*h, 3, 7*h, 4]: + for m in [-h, h, 3*h, 5*h, 7*h]: + assert can_do([p, n], [m]) + for n in [1, 2, 3, 4]: + for m in [1, 2, 3, 4]: + assert can_do([p, n], [m]) + + +def test_prudnikov_3(): + h = S.Half + assert can_do([Rational(1, 4), Rational(3, 4)], [h]) + assert can_do([Rational(1, 4), Rational(3, 4)], [3*h]) + assert can_do([Rational(1, 3), Rational(2, 3)], [3*h]) + assert can_do([Rational(3, 4), Rational(5, 4)], [h]) + assert can_do([Rational(3, 4), Rational(5, 4)], [3*h]) + + +@tooslow +def test_prudnikov_3_slow(): + # XXX: This is marked as tooslow and hence skipped in CI. None of the + # individual cases below fails or hangs. Some cases are slow and the loops + # below generate 280 different cases. Is it really necessary to test all + # 280 cases here? + h = S.Half + for p in [1, 2, 3, 4]: + for n in [-h, h, 1, 3*h, 2, 5*h, 3, 7*h, 4, 9*h]: + for m in [1, 3*h, 2, 5*h, 3, 7*h, 4]: + assert can_do([p, m], [n]) + + +@slow +def test_prudnikov_4(): + h = S.Half + for p in [3*h, 5*h, 7*h]: + for n in [-h, h, 3*h, 5*h, 7*h]: + for m in [3*h, 2, 5*h, 3, 7*h, 4]: + assert can_do([p, m], [n]) + for n in [1, 2, 3, 4]: + for m in [2, 3, 4]: + assert can_do([p, m], [n]) + + +@slow +def test_prudnikov_5(): + h = S.Half + + for p in [1, 2, 3]: + for q in range(p, 4): + for r in [1, 2, 3]: + for s in range(r, 4): + assert can_do([-h, p, q], [r, s]) + + for p in [h, 1, 3*h, 2, 5*h, 3]: + for q in [h, 3*h, 5*h]: + for r in [h, 3*h, 5*h]: + for s in [h, 3*h, 5*h]: + if s <= q and s <= r: + assert can_do([-h, p, q], [r, s]) + + for p in [h, 1, 3*h, 2, 5*h, 3]: + for q in [1, 2, 3]: + for r in [h, 3*h, 5*h]: + for s in [1, 2, 3]: + assert can_do([-h, p, q], [r, s]) + + +@slow +def test_prudnikov_6(): + h = S.Half + + for m in [3*h, 5*h]: + for n in [1, 2, 3]: + for q in [h, 1, 2]: + for p in [1, 2, 3]: + assert can_do([h, q, p], [m, n]) + for q in [1, 2, 3]: + for p in [3*h, 5*h]: + assert can_do([h, q, p], [m, n]) + + for q in [1, 2]: + for p in [1, 2, 3]: + for m in [1, 2, 3]: + for n in [1, 2, 3]: + assert can_do([h, q, p], [m, n]) + + assert can_do([h, h, 5*h], [3*h, 3*h]) + assert can_do([h, 1, 5*h], [3*h, 3*h]) + assert can_do([h, 2, 2], [1, 3]) + + # pages 435 to 457 contain more PFDD and stuff like this + + +@slow +def test_prudnikov_7(): + assert can_do([3], [6]) + + h = S.Half + for n in [h, 3*h, 5*h, 7*h]: + assert can_do([-h], [n]) + for m in [-h, h, 1, 3*h, 2, 5*h, 3, 7*h, 4]: # HERE + for n in [-h, h, 3*h, 5*h, 7*h, 1, 2, 3, 4]: + assert can_do([m], [n]) + + +@slow +def test_prudnikov_8(): + h = S.Half + + # 7.12.2 + for ai in [1, 2, 3]: + for bi in [1, 2, 3]: + for ci in range(1, ai + 1): + for di in [h, 1, 3*h, 2, 5*h, 3]: + assert can_do([ai, bi], [ci, di]) + for bi in [3*h, 5*h]: + for ci in [h, 1, 3*h, 2, 5*h, 3]: + for di in [1, 2, 3]: + assert can_do([ai, bi], [ci, di]) + + for ai in [-h, h, 3*h, 5*h]: + for bi in [1, 2, 3]: + for ci in [h, 1, 3*h, 2, 5*h, 3]: + for di in [1, 2, 3]: + assert can_do([ai, bi], [ci, di]) + for bi in [h, 3*h, 5*h]: + for ci in [h, 3*h, 5*h, 3]: + for di in [h, 1, 3*h, 2, 5*h, 3]: + if ci <= bi: + assert can_do([ai, bi], [ci, di]) + + +def test_prudnikov_9(): + # 7.13.1 [we have a general formula ... so this is a bit pointless] + for i in range(9): + assert can_do([], [(S(i) + 1)/2]) + for i in range(5): + assert can_do([], [-(2*S(i) + 1)/2]) + + +@slow +def test_prudnikov_10(): + # 7.14.2 + h = S.Half + for p in [-h, h, 1, 3*h, 2, 5*h, 3, 7*h, 4]: + for m in [1, 2, 3, 4]: + for n in range(m, 5): + assert can_do([p], [m, n]) + + for p in [1, 2, 3, 4]: + for n in [h, 3*h, 5*h, 7*h]: + for m in [1, 2, 3, 4]: + assert can_do([p], [n, m]) + + for p in [3*h, 5*h, 7*h]: + for m in [h, 1, 2, 5*h, 3, 7*h, 4]: + assert can_do([p], [h, m]) + assert can_do([p], [3*h, m]) + + for m in [h, 1, 2, 5*h, 3, 7*h, 4]: + assert can_do([7*h], [5*h, m]) + + assert can_do([Rational(-1, 2)], [S.Half, S.Half]) # shine-integral shi + + +def test_prudnikov_11(): + # 7.15 + assert can_do([a, a + S.Half], [2*a, b, 2*a - b]) + assert can_do([a, a + S.Half], [Rational(3, 2), 2*a, 2*a - S.Half]) + + assert can_do([Rational(1, 4), Rational(3, 4)], [S.Half, S.Half, 1]) + assert can_do([Rational(5, 4), Rational(3, 4)], [Rational(3, 2), S.Half, 2]) + assert can_do([Rational(5, 4), Rational(3, 4)], [Rational(3, 2), Rational(3, 2), 1]) + assert can_do([Rational(5, 4), Rational(7, 4)], [Rational(3, 2), Rational(5, 2), 2]) + + assert can_do([1, 1], [Rational(3, 2), 2, 2]) # cosh-integral chi + + +def test_prudnikov_12(): + # 7.16 + assert can_do( + [], [a, a + S.Half, 2*a], False) # branches only agree for some z! + assert can_do([], [a, a + S.Half, 2*a + 1], False) # dito + assert can_do([], [S.Half, a, a + S.Half]) + assert can_do([], [Rational(3, 2), a, a + S.Half]) + + assert can_do([], [Rational(1, 4), S.Half, Rational(3, 4)]) + assert can_do([], [S.Half, S.Half, 1]) + assert can_do([], [S.Half, Rational(3, 2), 1]) + assert can_do([], [Rational(3, 4), Rational(3, 2), Rational(5, 4)]) + assert can_do([], [1, 1, Rational(3, 2)]) + assert can_do([], [1, 2, Rational(3, 2)]) + assert can_do([], [1, Rational(3, 2), Rational(3, 2)]) + assert can_do([], [Rational(5, 4), Rational(3, 2), Rational(7, 4)]) + assert can_do([], [2, Rational(3, 2), Rational(3, 2)]) + + +@slow +def test_prudnikov_2F1(): + h = S.Half + # Elliptic integrals + for p in [-h, h]: + for m in [h, 3*h, 5*h, 7*h]: + for n in [1, 2, 3, 4]: + assert can_do([p, m], [n]) + + +@XFAIL +def test_prudnikov_fail_2F1(): + assert can_do([a, b], [b + 1]) # incomplete beta function + assert can_do([-1, b], [c]) # Poly. also -2, -3 etc + + # TODO polys + + # Legendre functions: + assert can_do([a, b], [a + b + S.Half]) + assert can_do([a, b], [a + b - S.Half]) + assert can_do([a, b], [a + b + Rational(3, 2)]) + assert can_do([a, b], [(a + b + 1)/2]) + assert can_do([a, b], [(a + b)/2 + 1]) + assert can_do([a, b], [a - b + 1]) + assert can_do([a, b], [a - b + 2]) + assert can_do([a, b], [2*b]) + assert can_do([a, b], [S.Half]) + assert can_do([a, b], [Rational(3, 2)]) + assert can_do([a, 1 - a], [c]) + assert can_do([a, 2 - a], [c]) + assert can_do([a, 3 - a], [c]) + assert can_do([a, a + S.Half], [c]) + assert can_do([1, b], [c]) + assert can_do([1, b], [Rational(3, 2)]) + + assert can_do([Rational(1, 4), Rational(3, 4)], [1]) + + # PFDD + o = S.One + assert can_do([o/8, 1], [o/8*9]) + assert can_do([o/6, 1], [o/6*7]) + assert can_do([o/6, 1], [o/6*13]) + assert can_do([o/5, 1], [o/5*6]) + assert can_do([o/5, 1], [o/5*11]) + assert can_do([o/4, 1], [o/4*5]) + assert can_do([o/4, 1], [o/4*9]) + assert can_do([o/3, 1], [o/3*4]) + assert can_do([o/3, 1], [o/3*7]) + assert can_do([o/8*3, 1], [o/8*11]) + assert can_do([o/5*2, 1], [o/5*7]) + assert can_do([o/5*2, 1], [o/5*12]) + assert can_do([o/5*3, 1], [o/5*8]) + assert can_do([o/5*3, 1], [o/5*13]) + assert can_do([o/8*5, 1], [o/8*13]) + assert can_do([o/4*3, 1], [o/4*7]) + assert can_do([o/4*3, 1], [o/4*11]) + assert can_do([o/3*2, 1], [o/3*5]) + assert can_do([o/3*2, 1], [o/3*8]) + assert can_do([o/5*4, 1], [o/5*9]) + assert can_do([o/5*4, 1], [o/5*14]) + assert can_do([o/6*5, 1], [o/6*11]) + assert can_do([o/6*5, 1], [o/6*17]) + assert can_do([o/8*7, 1], [o/8*15]) + + +@XFAIL +def test_prudnikov_fail_3F2(): + assert can_do([a, a + Rational(1, 3), a + Rational(2, 3)], [Rational(1, 3), Rational(2, 3)]) + assert can_do([a, a + Rational(1, 3), a + Rational(2, 3)], [Rational(2, 3), Rational(4, 3)]) + assert can_do([a, a + Rational(1, 3), a + Rational(2, 3)], [Rational(4, 3), Rational(5, 3)]) + + # page 421 + assert can_do([a, a + Rational(1, 3), a + Rational(2, 3)], [a*Rational(3, 2), (3*a + 1)/2]) + + # pages 422 ... + assert can_do([Rational(-1, 2), S.Half, S.Half], [1, 1]) # elliptic integrals + assert can_do([Rational(-1, 2), S.Half, 1], [Rational(3, 2), Rational(3, 2)]) + # TODO LOTS more + + # PFDD + assert can_do([Rational(1, 8), Rational(3, 8), 1], [Rational(9, 8), Rational(11, 8)]) + assert can_do([Rational(1, 8), Rational(5, 8), 1], [Rational(9, 8), Rational(13, 8)]) + assert can_do([Rational(1, 8), Rational(7, 8), 1], [Rational(9, 8), Rational(15, 8)]) + assert can_do([Rational(1, 6), Rational(1, 3), 1], [Rational(7, 6), Rational(4, 3)]) + assert can_do([Rational(1, 6), Rational(2, 3), 1], [Rational(7, 6), Rational(5, 3)]) + assert can_do([Rational(1, 6), Rational(2, 3), 1], [Rational(5, 3), Rational(13, 6)]) + assert can_do([S.Half, 1, 1], [Rational(1, 4), Rational(3, 4)]) + # LOTS more + + +@XFAIL +def test_prudnikov_fail_other(): + # 7.11.2 + + # 7.12.1 + assert can_do([1, a], [b, 1 - 2*a + b]) # ??? + + # 7.14.2 + assert can_do([Rational(-1, 2)], [S.Half, 1]) # struve + assert can_do([1], [S.Half, S.Half]) # struve + assert can_do([Rational(1, 4)], [S.Half, Rational(5, 4)]) # PFDD + assert can_do([Rational(3, 4)], [Rational(3, 2), Rational(7, 4)]) # PFDD + assert can_do([1], [Rational(1, 4), Rational(3, 4)]) # PFDD + assert can_do([1], [Rational(3, 4), Rational(5, 4)]) # PFDD + assert can_do([1], [Rational(5, 4), Rational(7, 4)]) # PFDD + # TODO LOTS more + + # 7.15.2 + assert can_do([S.Half, 1], [Rational(3, 4), Rational(5, 4), Rational(3, 2)]) # PFDD + assert can_do([S.Half, 1], [Rational(7, 4), Rational(5, 4), Rational(3, 2)]) # PFDD + + # 7.16.1 + assert can_do([], [Rational(1, 3), S(2/3)]) # PFDD + assert can_do([], [Rational(2, 3), S(4/3)]) # PFDD + assert can_do([], [Rational(5, 3), S(4/3)]) # PFDD + + # XXX this does not *evaluate* right?? + assert can_do([], [a, a + S.Half, 2*a - 1]) + + +def test_bug(): + h = hyper([-1, 1], [z], -1) + assert hyperexpand(h) == (z + 1)/z + + +def test_omgissue_203(): + h = hyper((-5, -3, -4), (-6, -6), 1) + assert hyperexpand(h) == Rational(1, 30) + h = hyper((-6, -7, -5), (-6, -6), 1) + assert hyperexpand(h) == Rational(-1, 6) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_powsimp.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_powsimp.py new file mode 100644 index 0000000000000000000000000000000000000000..61bdc93d052baf4b1e80da8f5864cf22b8fa383e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_powsimp.py @@ -0,0 +1,368 @@ +from sympy.core.function import Function +from sympy.core.mul import Mul +from sympy.core.numbers import (E, I, Rational, oo, pi) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol, symbols) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import (root, sqrt) +from sympy.functions.elementary.trigonometric import sin +from sympy.functions.special.gamma_functions import gamma +from sympy.functions.special.hyper import hyper +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.simplify.powsimp import (powdenest, powsimp) +from sympy.simplify.simplify import (signsimp, simplify) +from sympy.core.symbol import Str + +from sympy.abc import x, y, z, a, b + + +def test_powsimp(): + x, y, z, n = symbols('x,y,z,n') + f = Function('f') + assert powsimp( 4**x * 2**(-x) * 2**(-x) ) == 1 + assert powsimp( (-4)**x * (-2)**(-x) * 2**(-x) ) == 1 + + assert powsimp( + f(4**x * 2**(-x) * 2**(-x)) ) == f(4**x * 2**(-x) * 2**(-x)) + assert powsimp( f(4**x * 2**(-x) * 2**(-x)), deep=True ) == f(1) + assert exp(x)*exp(y) == exp(x)*exp(y) + assert powsimp(exp(x)*exp(y)) == exp(x + y) + assert powsimp(exp(x)*exp(y)*2**x*2**y) == (2*E)**(x + y) + assert powsimp(exp(x)*exp(y)*2**x*2**y, combine='exp') == \ + exp(x + y)*2**(x + y) + assert powsimp(exp(x)*exp(y)*exp(2)*sin(x) + sin(y) + 2**x*2**y) == \ + exp(2 + x + y)*sin(x) + sin(y) + 2**(x + y) + assert powsimp(sin(exp(x)*exp(y))) == sin(exp(x)*exp(y)) + assert powsimp(sin(exp(x)*exp(y)), deep=True) == sin(exp(x + y)) + assert powsimp(x**2*x**y) == x**(2 + y) + # This should remain factored, because 'exp' with deep=True is supposed + # to act like old automatic exponent combining. + assert powsimp((1 + E*exp(E))*exp(-E), combine='exp', deep=True) == \ + (1 + exp(1 + E))*exp(-E) + assert powsimp((1 + E*exp(E))*exp(-E), deep=True) == \ + (1 + exp(1 + E))*exp(-E) + assert powsimp((1 + E*exp(E))*exp(-E)) == (1 + exp(1 + E))*exp(-E) + assert powsimp((1 + E*exp(E))*exp(-E), combine='exp') == \ + (1 + exp(1 + E))*exp(-E) + assert powsimp((1 + E*exp(E))*exp(-E), combine='base') == \ + (1 + E*exp(E))*exp(-E) + x, y = symbols('x,y', nonnegative=True) + n = Symbol('n', real=True) + assert powsimp(y**n * (y/x)**(-n)) == x**n + assert powsimp(x**(x**(x*y)*y**(x*y))*y**(x**(x*y)*y**(x*y)), deep=True) \ + == (x*y)**(x*y)**(x*y) + assert powsimp(2**(2**(2*x)*x), deep=False) == 2**(2**(2*x)*x) + assert powsimp(2**(2**(2*x)*x), deep=True) == 2**(x*4**x) + assert powsimp( + exp(-x + exp(-x)*exp(-x*log(x))), deep=False, combine='exp') == \ + exp(-x + exp(-x)*exp(-x*log(x))) + assert powsimp( + exp(-x + exp(-x)*exp(-x*log(x))), deep=False, combine='exp') == \ + exp(-x + exp(-x)*exp(-x*log(x))) + assert powsimp((x + y)/(3*z), deep=False, combine='exp') == (x + y)/(3*z) + assert powsimp((x/3 + y/3)/z, deep=True, combine='exp') == (x/3 + y/3)/z + assert powsimp(exp(x)/(1 + exp(x)*exp(y)), deep=True) == \ + exp(x)/(1 + exp(x + y)) + assert powsimp(x*y**(z**x*z**y), deep=True) == x*y**(z**(x + y)) + assert powsimp((z**x*z**y)**x, deep=True) == (z**(x + y))**x + assert powsimp(x*(z**x*z**y)**x, deep=True) == x*(z**(x + y))**x + p = symbols('p', positive=True) + assert powsimp((1/x)**log(2)/x) == (1/x)**(1 + log(2)) + assert powsimp((1/p)**log(2)/p) == p**(-1 - log(2)) + + # coefficient of exponent can only be simplified for positive bases + assert powsimp(2**(2*x)) == 4**x + assert powsimp((-1)**(2*x)) == (-1)**(2*x) + i = symbols('i', integer=True) + assert powsimp((-1)**(2*i)) == 1 + assert powsimp((-1)**(-x)) != (-1)**x # could be 1/((-1)**x), but is not + # force=True overrides assumptions + assert powsimp((-1)**(2*x), force=True) == 1 + + # rational exponents allow combining of negative terms + w, n, m = symbols('w n m', negative=True) + e = i/a # not a rational exponent if `a` is unknown + ex = w**e*n**e*m**e + assert powsimp(ex) == m**(i/a)*n**(i/a)*w**(i/a) + e = i/3 + ex = w**e*n**e*m**e + assert powsimp(ex) == (-1)**i*(-m*n*w)**(i/3) + e = (3 + i)/i + ex = w**e*n**e*m**e + assert powsimp(ex) == (-1)**(3*e)*(-m*n*w)**e + + eq = x**(a*Rational(2, 3)) + # eq != (x**a)**(2/3) (try x = -1 and a = 3 to see) + assert powsimp(eq).exp == eq.exp == a*Rational(2, 3) + # powdenest goes the other direction + assert powsimp(2**(2*x)) == 4**x + + assert powsimp(exp(p/2)) == exp(p/2) + + # issue 6368 + eq = Mul(*[sqrt(Dummy(imaginary=True)) for i in range(3)]) + assert powsimp(eq) == eq and eq.is_Mul + + assert all(powsimp(e) == e for e in (sqrt(x**a), sqrt(x**2))) + + # issue 8836 + assert str( powsimp(exp(I*pi/3)*root(-1,3)) ) == '(-1)**(2/3)' + + # issue 9183 + assert powsimp(-0.1**x) == -0.1**x + + # issue 10095 + assert powsimp((1/(2*E))**oo) == (exp(-1)/2)**oo + + # PR 13131 + eq = sin(2*x)**2*sin(2.0*x)**2 + assert powsimp(eq) == eq + + # issue 14615 + assert powsimp(x**2*y**3*(x*y**2)**Rational(3, 2) + ) == x*y*(x*y**2)**Rational(5, 2) + + #issue 27380 + assert powsimp(1.0**(x+1)/1.0**x) == 1.0 + +def test_powsimp_negated_base(): + assert powsimp((-x + y)/sqrt(x - y)) == -sqrt(x - y) + assert powsimp((-x + y)*(-z + y)/sqrt(x - y)/sqrt(z - y)) == sqrt(x - y)*sqrt(z - y) + p = symbols('p', positive=True) + reps = {p: 2, a: S.Half} + assert powsimp((-p)**a/p**a).subs(reps) == ((-1)**a).subs(reps) + assert powsimp((-p)**a*p**a).subs(reps) == ((-p**2)**a).subs(reps) + n = symbols('n', negative=True) + reps = {p: -2, a: S.Half} + assert powsimp((-n)**a/n**a).subs(reps) == (-1)**(-a).subs(a, S.Half) + assert powsimp((-n)**a*n**a).subs(reps) == ((-n**2)**a).subs(reps) + # if x is 0 then the lhs is 0**a*oo**a which is not (-1)**a + eq = (-x)**a/x**a + assert powsimp(eq) == eq + + +def test_powsimp_nc(): + x, y, z = symbols('x,y,z') + A, B, C = symbols('A B C', commutative=False) + + assert powsimp(A**x*A**y, combine='all') == A**(x + y) + assert powsimp(A**x*A**y, combine='base') == A**x*A**y + assert powsimp(A**x*A**y, combine='exp') == A**(x + y) + + assert powsimp(A**x*B**x, combine='all') == A**x*B**x + assert powsimp(A**x*B**x, combine='base') == A**x*B**x + assert powsimp(A**x*B**x, combine='exp') == A**x*B**x + + assert powsimp(B**x*A**x, combine='all') == B**x*A**x + assert powsimp(B**x*A**x, combine='base') == B**x*A**x + assert powsimp(B**x*A**x, combine='exp') == B**x*A**x + + assert powsimp(A**x*A**y*A**z, combine='all') == A**(x + y + z) + assert powsimp(A**x*A**y*A**z, combine='base') == A**x*A**y*A**z + assert powsimp(A**x*A**y*A**z, combine='exp') == A**(x + y + z) + + assert powsimp(A**x*B**x*C**x, combine='all') == A**x*B**x*C**x + assert powsimp(A**x*B**x*C**x, combine='base') == A**x*B**x*C**x + assert powsimp(A**x*B**x*C**x, combine='exp') == A**x*B**x*C**x + + assert powsimp(B**x*A**x*C**x, combine='all') == B**x*A**x*C**x + assert powsimp(B**x*A**x*C**x, combine='base') == B**x*A**x*C**x + assert powsimp(B**x*A**x*C**x, combine='exp') == B**x*A**x*C**x + + +def test_issue_6440(): + assert powsimp(16*2**a*8**b) == 2**(a + 3*b + 4) + + +def test_powdenest(): + x, y = symbols('x,y') + p, q = symbols('p q', positive=True) + i, j = symbols('i,j', integer=True) + + assert powdenest(x) == x + assert powdenest(x + 2*(x**(a*Rational(2, 3)))**(3*x)) == (x + 2*(x**(a*Rational(2, 3)))**(3*x)) + assert powdenest((exp(a*Rational(2, 3)))**(3*x)) # -X-> (exp(a/3))**(6*x) + assert powdenest((x**(a*Rational(2, 3)))**(3*x)) == ((x**(a*Rational(2, 3)))**(3*x)) + assert powdenest(exp(3*x*log(2))) == 2**(3*x) + assert powdenest(sqrt(p**2)) == p + eq = p**(2*i)*q**(4*i) + assert powdenest(eq) == (p*q**2)**(2*i) + # -X-> (x**x)**i*(x**x)**j == x**(x*(i + j)) + assert powdenest((x**x)**(i + j)) + assert powdenest(exp(3*y*log(x))) == x**(3*y) + assert powdenest(exp(y*(log(a) + log(b)))) == (a*b)**y + assert powdenest(exp(3*(log(a) + log(b)))) == a**3*b**3 + assert powdenest(((x**(2*i))**(3*y))**x) == ((x**(2*i))**(3*y))**x + assert powdenest(((x**(2*i))**(3*y))**x, force=True) == x**(6*i*x*y) + assert powdenest(((x**(a*Rational(2, 3)))**(3*y/i))**x) == \ + (((x**(a*Rational(2, 3)))**(3*y/i))**x) + assert powdenest((x**(2*i)*y**(4*i))**z, force=True) == (x*y**2)**(2*i*z) + assert powdenest((p**(2*i)*q**(4*i))**j) == (p*q**2)**(2*i*j) + e = ((p**(2*a))**(3*y))**x + assert powdenest(e) == e + e = ((x**2*y**4)**a)**(x*y) + assert powdenest(e) == e + e = (((x**2*y**4)**a)**(x*y))**3 + assert powdenest(e) == ((x**2*y**4)**a)**(3*x*y) + assert powdenest((((x**2*y**4)**a)**(x*y)), force=True) == \ + (x*y**2)**(2*a*x*y) + assert powdenest((((x**2*y**4)**a)**(x*y))**3, force=True) == \ + (x*y**2)**(6*a*x*y) + assert powdenest((x**2*y**6)**i) != (x*y**3)**(2*i) + x, y = symbols('x,y', positive=True) + assert powdenest((x**2*y**6)**i) == (x*y**3)**(2*i) + + assert powdenest((x**(i*Rational(2, 3))*y**(i/2))**(2*i)) == (x**Rational(4, 3)*y)**(i**2) + assert powdenest(sqrt(x**(2*i)*y**(6*i))) == (x*y**3)**i + + assert powdenest(4**x) == 2**(2*x) + assert powdenest((4**x)**y) == 2**(2*x*y) + assert powdenest(4**x*y) == 2**(2*x)*y + + +def test_powdenest_polar(): + x, y, z = symbols('x y z', polar=True) + a, b, c = symbols('a b c') + assert powdenest((x*y*z)**a) == x**a*y**a*z**a + assert powdenest((x**a*y**b)**c) == x**(a*c)*y**(b*c) + assert powdenest(((x**a)**b*y**c)**c) == x**(a*b*c)*y**(c**2) + + +def test_issue_5805(): + arg = ((gamma(x)*hyper((), (), x))*pi)**2 + assert powdenest(arg) == (pi*gamma(x)*hyper((), (), x))**2 + assert arg.is_positive is None + + +def test_issue_9324_powsimp_on_matrix_symbol(): + M = MatrixSymbol('M', 10, 10) + expr = powsimp(M, deep=True) + assert expr == M + assert expr.args[0] == Str('M') + + +def test_issue_6367(): + z = -5*sqrt(2)/(2*sqrt(2*sqrt(29) + 29)) + sqrt(-sqrt(29)/29 + S.Half) + assert Mul(*[powsimp(a) for a in Mul.make_args(z.normal())]) == 0 + assert powsimp(z.normal()) == 0 + assert simplify(z) == 0 + assert powsimp(sqrt(2 + sqrt(3))*sqrt(2 - sqrt(3)) + 1) == 2 + assert powsimp(z) != 0 + + +def test_powsimp_polar(): + from sympy.functions.elementary.complexes import polar_lift + from sympy.functions.elementary.exponential import exp_polar + x, y, z = symbols('x y z') + p, q, r = symbols('p q r', polar=True) + + assert (polar_lift(-1))**(2*x) == exp_polar(2*pi*I*x) + assert powsimp(p**x * q**x) == (p*q)**x + assert p**x * (1/p)**x == 1 + assert (1/p)**x == p**(-x) + + assert exp_polar(x)*exp_polar(y) == exp_polar(x)*exp_polar(y) + assert powsimp(exp_polar(x)*exp_polar(y)) == exp_polar(x + y) + assert powsimp(exp_polar(x)*exp_polar(y)*p**x*p**y) == \ + (p*exp_polar(1))**(x + y) + assert powsimp(exp_polar(x)*exp_polar(y)*p**x*p**y, combine='exp') == \ + exp_polar(x + y)*p**(x + y) + assert powsimp( + exp_polar(x)*exp_polar(y)*exp_polar(2)*sin(x) + sin(y) + p**x*p**y) \ + == p**(x + y) + sin(x)*exp_polar(2 + x + y) + sin(y) + assert powsimp(sin(exp_polar(x)*exp_polar(y))) == \ + sin(exp_polar(x)*exp_polar(y)) + assert powsimp(sin(exp_polar(x)*exp_polar(y)), deep=True) == \ + sin(exp_polar(x + y)) + + +def test_issue_5728(): + b = x*sqrt(y) + a = sqrt(b) + c = sqrt(sqrt(x)*y) + assert powsimp(a*b) == sqrt(b)**3 + assert powsimp(a*b**2*sqrt(y)) == sqrt(y)*a**5 + assert powsimp(a*x**2*c**3*y) == c**3*a**5 + assert powsimp(a*x*c**3*y**2) == c**7*a + assert powsimp(x*c**3*y**2) == c**7 + assert powsimp(x*c**3*y) == x*y*c**3 + assert powsimp(sqrt(x)*c**3*y) == c**5 + assert powsimp(sqrt(x)*a**3*sqrt(y)) == sqrt(x)*sqrt(y)*a**3 + assert powsimp(Mul(sqrt(x)*c**3*sqrt(y), y, evaluate=False)) == \ + sqrt(x)*sqrt(y)**3*c**3 + assert powsimp(a**2*a*x**2*y) == a**7 + + # symbolic powers work, too + b = x**y*y + a = b*sqrt(b) + assert a.is_Mul is True + assert powsimp(a) == sqrt(b)**3 + + # as does exp + a = x*exp(y*Rational(2, 3)) + assert powsimp(a*sqrt(a)) == sqrt(a)**3 + assert powsimp(a**2*sqrt(a)) == sqrt(a)**5 + assert powsimp(a**2*sqrt(sqrt(a))) == sqrt(sqrt(a))**9 + + +def test_issue_from_PR1599(): + n1, n2, n3, n4 = symbols('n1 n2 n3 n4', negative=True) + assert (powsimp(sqrt(n1)*sqrt(n2)*sqrt(n3)) == + -I*sqrt(-n1)*sqrt(-n2)*sqrt(-n3)) + assert (powsimp(root(n1, 3)*root(n2, 3)*root(n3, 3)*root(n4, 3)) == + -(-1)**Rational(1, 3)* + (-n1)**Rational(1, 3)*(-n2)**Rational(1, 3)*(-n3)**Rational(1, 3)*(-n4)**Rational(1, 3)) + + +def test_issue_10195(): + a = Symbol('a', integer=True) + l = Symbol('l', even=True, nonzero=True) + n = Symbol('n', odd=True) + e_x = (-1)**(n/2 - S.Half) - (-1)**(n*Rational(3, 2) - S.Half) + assert powsimp((-1)**(l/2)) == I**l + assert powsimp((-1)**(n/2)) == I**n + assert powsimp((-1)**(n*Rational(3, 2))) == -I**n + assert powsimp(e_x) == (-1)**(n/2 - S.Half) + (-1)**(n*Rational(3, 2) + + S.Half) + assert powsimp((-1)**(a*Rational(3, 2))) == (-I)**a + +def test_issue_15709(): + assert powsimp(3**x*Rational(2, 3)) == 2*3**(x-1) + assert powsimp(2*3**x/3) == 2*3**(x-1) + + +def test_issue_11981(): + x, y = symbols('x y', commutative=False) + assert powsimp((x*y)**2 * (y*x)**2) == (x*y)**2 * (y*x)**2 + + +def test_issue_17524(): + a = symbols("a", real=True) + e = (-1 - a**2)*sqrt(1 + a**2) + assert signsimp(powsimp(e)) == signsimp(e) == -(a**2 + 1)**(S(3)/2) + + +def test_issue_19627(): + # if you use force the user must verify + assert powdenest(sqrt(sin(x)**2), force=True) == sin(x) + assert powdenest((x**(S.Half/y))**(2*y), force=True) == x + from sympy.core.function import expand_power_base + e = 1 - a + expr = (exp(z/e)*x**(b/e)*y**((1 - b)/e))**e + assert powdenest(expand_power_base(expr, force=True), force=True + ) == x**b*y**(1 - b)*exp(z) + + +def test_issue_22546(): + p1, p2 = symbols('p1, p2', positive=True) + ref = powsimp(p1**z/p2**z) + e = z + 1 + ans = ref.subs(z, e) + assert ans.is_Pow + assert powsimp(p1**e/p2**e) == ans + i = symbols('i', integer=True) + ref = powsimp(x**i/y**i) + e = i + 1 + ans = ref.subs(i, e) + assert ans.is_Pow + assert powsimp(x**e/y**e) == ans diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_radsimp.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_radsimp.py new file mode 100644 index 0000000000000000000000000000000000000000..f8ff955e48a34536c1752c565c0864dedae6a214 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_radsimp.py @@ -0,0 +1,498 @@ +from sympy.core.add import Add +from sympy.core.function import (Derivative, Function, diff) +from sympy.core.mul import Mul +from sympy.core.numbers import (I, Rational) +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, Wild, symbols) +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import (root, sqrt) +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.polys.polytools import factor +from sympy.series.order import O +from sympy.simplify.radsimp import (collect, collect_const, fraction, radsimp, rcollect) + +from sympy.core.expr import unchanged +from sympy.core.mul import _unevaluated_Mul as umul +from sympy.simplify.radsimp import (_unevaluated_Add, + collect_sqrt, fraction_expand, collect_abs) +from sympy.testing.pytest import raises + +from sympy.abc import x, y, z, a, b, c, d + + +def test_radsimp(): + r2 = sqrt(2) + r3 = sqrt(3) + r5 = sqrt(5) + r7 = sqrt(7) + assert fraction(radsimp(1/r2)) == (sqrt(2), 2) + assert radsimp(1/(1 + r2)) == \ + -1 + sqrt(2) + assert radsimp(1/(r2 + r3)) == \ + -sqrt(2) + sqrt(3) + assert fraction(radsimp(1/(1 + r2 + r3))) == \ + (-sqrt(6) + sqrt(2) + 2, 4) + assert fraction(radsimp(1/(r2 + r3 + r5))) == \ + (-sqrt(30) + 2*sqrt(3) + 3*sqrt(2), 12) + assert fraction(radsimp(1/(1 + r2 + r3 + r5))) == ( + (-34*sqrt(10) - 26*sqrt(15) - 55*sqrt(3) - 61*sqrt(2) + 14*sqrt(30) + + 93 + 46*sqrt(6) + 53*sqrt(5), 71)) + assert fraction(radsimp(1/(r2 + r3 + r5 + r7))) == ( + (-50*sqrt(42) - 133*sqrt(5) - 34*sqrt(70) - 145*sqrt(3) + 22*sqrt(105) + + 185*sqrt(2) + 62*sqrt(30) + 135*sqrt(7), 215)) + z = radsimp(1/(1 + r2/3 + r3/5 + r5 + r7)) + assert len((3616791619821680643598*z).args) == 16 + assert radsimp(1/z) == 1/z + assert radsimp(1/z, max_terms=20).expand() == 1 + r2/3 + r3/5 + r5 + r7 + assert radsimp(1/(r2*3)) == \ + sqrt(2)/6 + assert radsimp(1/(r2*a + r3 + r5 + r7)) == ( + (8*sqrt(2)*a**7 - 8*sqrt(7)*a**6 - 8*sqrt(5)*a**6 - 8*sqrt(3)*a**6 - + 180*sqrt(2)*a**5 + 8*sqrt(30)*a**5 + 8*sqrt(42)*a**5 + 8*sqrt(70)*a**5 + - 24*sqrt(105)*a**4 + 84*sqrt(3)*a**4 + 100*sqrt(5)*a**4 + + 116*sqrt(7)*a**4 - 72*sqrt(70)*a**3 - 40*sqrt(42)*a**3 - + 8*sqrt(30)*a**3 + 782*sqrt(2)*a**3 - 462*sqrt(3)*a**2 - + 302*sqrt(7)*a**2 - 254*sqrt(5)*a**2 + 120*sqrt(105)*a**2 - + 795*sqrt(2)*a - 62*sqrt(30)*a + 82*sqrt(42)*a + 98*sqrt(70)*a - + 118*sqrt(105) + 59*sqrt(7) + 295*sqrt(5) + 531*sqrt(3))/(16*a**8 - + 480*a**6 + 3128*a**4 - 6360*a**2 + 3481)) + assert radsimp(1/(r2*a + r2*b + r3 + r7)) == ( + (sqrt(2)*a*(a + b)**2 - 5*sqrt(2)*a + sqrt(42)*a + sqrt(2)*b*(a + + b)**2 - 5*sqrt(2)*b + sqrt(42)*b - sqrt(7)*(a + b)**2 - sqrt(3)*(a + + b)**2 - 2*sqrt(3) + 2*sqrt(7))/(2*a**4 + 8*a**3*b + 12*a**2*b**2 - + 20*a**2 + 8*a*b**3 - 40*a*b + 2*b**4 - 20*b**2 + 8)) + assert radsimp(1/(r2*a + r2*b + r2*c + r2*d)) == \ + sqrt(2)/(2*a + 2*b + 2*c + 2*d) + assert radsimp(1/(1 + r2*a + r2*b + r2*c + r2*d)) == ( + (sqrt(2)*a + sqrt(2)*b + sqrt(2)*c + sqrt(2)*d - 1)/(2*a**2 + 4*a*b + + 4*a*c + 4*a*d + 2*b**2 + 4*b*c + 4*b*d + 2*c**2 + 4*c*d + 2*d**2 - 1)) + assert radsimp((y**2 - x)/(y - sqrt(x))) == \ + sqrt(x) + y + assert radsimp(-(y**2 - x)/(y - sqrt(x))) == \ + -(sqrt(x) + y) + assert radsimp(1/(1 - I + a*I)) == \ + (-I*a + 1 + I)/(a**2 - 2*a + 2) + assert radsimp(1/((-x + y)*(x - sqrt(y)))) == \ + (-x - sqrt(y))/((x - y)*(x**2 - y)) + e = (3 + 3*sqrt(2))*x*(3*x - 3*sqrt(y)) + assert radsimp(e) == x*(3 + 3*sqrt(2))*(3*x - 3*sqrt(y)) + assert radsimp(1/e) == ( + (-9*x + 9*sqrt(2)*x - 9*sqrt(y) + 9*sqrt(2)*sqrt(y))/(9*x*(9*x**2 - + 9*y))) + assert radsimp(1 + 1/(1 + sqrt(3))) == \ + Mul(S.Half, -1 + sqrt(3), evaluate=False) + 1 + A = symbols("A", commutative=False) + assert radsimp(x**2 + sqrt(2)*x**2 - sqrt(2)*x*A) == \ + x**2 + sqrt(2)*x**2 - sqrt(2)*x*A + assert radsimp(1/sqrt(5 + 2 * sqrt(6))) == -sqrt(2) + sqrt(3) + assert radsimp(1/sqrt(5 + 2 * sqrt(6))**3) == -(-sqrt(3) + sqrt(2))**3 + + # issue 6532 + assert fraction(radsimp(1/sqrt(x))) == (sqrt(x), x) + assert fraction(radsimp(1/sqrt(2*x + 3))) == (sqrt(2*x + 3), 2*x + 3) + assert fraction(radsimp(1/sqrt(2*(x + 3)))) == (sqrt(2*x + 6), 2*x + 6) + + # issue 5994 + e = S('-(2 + 2*sqrt(2) + 4*2**(1/4))/' + '(1 + 2**(3/4) + 3*2**(1/4) + 3*sqrt(2))') + assert radsimp(e).expand() == -2*2**Rational(3, 4) - 2*2**Rational(1, 4) + 2 + 2*sqrt(2) + + # issue 5986 (modifications to radimp didn't initially recognize this so + # the test is included here) + assert radsimp(1/(-sqrt(5)/2 - S.Half + (-sqrt(5)/2 - S.Half)**2)) == 1 + + # from issue 5934 + eq = ( + (-240*sqrt(2)*sqrt(sqrt(5) + 5)*sqrt(8*sqrt(5) + 40) - + 360*sqrt(2)*sqrt(-8*sqrt(5) + 40)*sqrt(-sqrt(5) + 5) - + 120*sqrt(10)*sqrt(-8*sqrt(5) + 40)*sqrt(-sqrt(5) + 5) + + 120*sqrt(2)*sqrt(-sqrt(5) + 5)*sqrt(8*sqrt(5) + 40) + + 120*sqrt(2)*sqrt(-8*sqrt(5) + 40)*sqrt(sqrt(5) + 5) + + 120*sqrt(10)*sqrt(-sqrt(5) + 5)*sqrt(8*sqrt(5) + 40) + + 120*sqrt(10)*sqrt(-8*sqrt(5) + 40)*sqrt(sqrt(5) + 5))/(-36000 - + 7200*sqrt(5) + (12*sqrt(10)*sqrt(sqrt(5) + 5) + + 24*sqrt(10)*sqrt(-sqrt(5) + 5))**2)) + assert radsimp(eq) is S.NaN # it's 0/0 + + # work with normal form + e = 1/sqrt(sqrt(7)/7 + 2*sqrt(2) + 3*sqrt(3) + 5*sqrt(5)) + 3 + assert radsimp(e) == ( + -sqrt(sqrt(7) + 14*sqrt(2) + 21*sqrt(3) + + 35*sqrt(5))*(-11654899*sqrt(35) - 1577436*sqrt(210) - 1278438*sqrt(15) + - 1346996*sqrt(10) + 1635060*sqrt(6) + 5709765 + 7539830*sqrt(14) + + 8291415*sqrt(21))/1300423175 + 3) + + # obey power rules + base = sqrt(3) - sqrt(2) + assert radsimp(1/base**3) == (sqrt(3) + sqrt(2))**3 + assert radsimp(1/(-base)**3) == -(sqrt(2) + sqrt(3))**3 + assert radsimp(1/(-base)**x) == (-base)**(-x) + assert radsimp(1/base**x) == (sqrt(2) + sqrt(3))**x + assert radsimp(root(1/(-1 - sqrt(2)), -x)) == (-1)**(-1/x)*(1 + sqrt(2))**(1/x) + + # recurse + e = cos(1/(1 + sqrt(2))) + assert radsimp(e) == cos(-sqrt(2) + 1) + assert radsimp(e/2) == cos(-sqrt(2) + 1)/2 + assert radsimp(1/e) == 1/cos(-sqrt(2) + 1) + assert radsimp(2/e) == 2/cos(-sqrt(2) + 1) + assert fraction(radsimp(e/sqrt(x))) == (sqrt(x)*cos(-sqrt(2)+1), x) + + # test that symbolic denominators are not processed + r = 1 + sqrt(2) + assert radsimp(x/r, symbolic=False) == -x*(-sqrt(2) + 1) + assert radsimp(x/(y + r), symbolic=False) == x/(y + 1 + sqrt(2)) + assert radsimp(x/(y + r)/r, symbolic=False) == \ + -x*(-sqrt(2) + 1)/(y + 1 + sqrt(2)) + + # issue 7408 + eq = sqrt(x)/sqrt(y) + assert radsimp(eq) == umul(sqrt(x), sqrt(y), 1/y) + assert radsimp(eq, symbolic=False) == eq + + # issue 7498 + assert radsimp(sqrt(x)/sqrt(y)**3) == umul(sqrt(x), sqrt(y**3), 1/y**3) + + # for coverage + eq = sqrt(x)/y**2 + assert radsimp(eq) == eq + + # handle non-Expr args + from sympy.integrals.integrals import Integral + eq = Integral(x/(sqrt(2) - 1), (x, 0, 1/(sqrt(2) + 1))) + assert radsimp(eq) == Integral((sqrt(2) + 1)*x , (x, 0, sqrt(2) - 1)) + + from sympy.sets import FiniteSet + eq = FiniteSet(x/(sqrt(2) - 1)) + assert radsimp(eq) == FiniteSet((sqrt(2) + 1)*x) + +def test_radsimp_issue_3214(): + c, p = symbols('c p', positive=True) + s = sqrt(c**2 - p**2) + b = (c + I*p - s)/(c + I*p + s) + assert radsimp(b) == -I*(c + I*p - sqrt(c**2 - p**2))**2/(2*c*p) + + +def test_collect_1(): + """Collect with respect to Symbol""" + x, y, z, n = symbols('x,y,z,n') + assert collect(1, x) == 1 + assert collect( x + y*x, x ) == x * (1 + y) + assert collect( x + x**2, x ) == x + x**2 + assert collect( x**2 + y*x**2, x ) == (x**2)*(1 + y) + assert collect( x**2 + y*x, x ) == x*y + x**2 + assert collect( 2*x**2 + y*x**2 + 3*x*y, [x] ) == x**2*(2 + y) + 3*x*y + assert collect( 2*x**2 + y*x**2 + 3*x*y, [y] ) == 2*x**2 + y*(x**2 + 3*x) + + assert collect( ((1 + y + x)**4).expand(), x) == ((1 + y)**4).expand() + \ + x*(4*(1 + y)**3).expand() + x**2*(6*(1 + y)**2).expand() + \ + x**3*(4*(1 + y)).expand() + x**4 + # symbols can be given as any iterable + expr = x + y + assert collect(expr, expr.free_symbols) == expr + assert collect(x*exp(x) + sin(x)*y + sin(x)*2 + 3*x, x, exact=None + ) == x*exp(x) + 3*x + (y + 2)*sin(x) + assert collect(x*exp(x) + sin(x)*y + sin(x)*2 + 3*x + y*x + + y*x*exp(x), x, exact=None + ) == x*exp(x)*(y + 1) + (3 + y)*x + (y + 2)*sin(x) + + +def test_collect_2(): + """Collect with respect to a sum""" + a, b, x = symbols('a,b,x') + assert collect(a*(cos(x) + sin(x)) + b*(cos(x) + sin(x)), + sin(x) + cos(x)) == (a + b)*(cos(x) + sin(x)) + + +def test_collect_3(): + """Collect with respect to a product""" + a, b, c = symbols('a,b,c') + f = Function('f') + x, y, z, n = symbols('x,y,z,n') + + assert collect(-x/8 + x*y, -x) == x*(y - Rational(1, 8)) + + assert collect( 1 + x*(y**2), x*y ) == 1 + x*(y**2) + assert collect( x*y + a*x*y, x*y) == x*y*(1 + a) + assert collect( 1 + x*y + a*x*y, x*y) == 1 + x*y*(1 + a) + assert collect(a*x*f(x) + b*(x*f(x)), x*f(x)) == x*(a + b)*f(x) + + assert collect(a*x*log(x) + b*(x*log(x)), x*log(x)) == x*(a + b)*log(x) + assert collect(a*x**2*log(x)**2 + b*(x*log(x))**2, x*log(x)) == \ + x**2*log(x)**2*(a + b) + + # with respect to a product of three symbols + assert collect(y*x*z + a*x*y*z, x*y*z) == (1 + a)*x*y*z + + +def test_collect_4(): + """Collect with respect to a power""" + a, b, c, x = symbols('a,b,c,x') + + assert collect(a*x**c + b*x**c, x**c) == x**c*(a + b) + # issue 6096: 2 stays with c (unless c is integer or x is positive0 + assert collect(a*x**(2*c) + b*x**(2*c), x**c) == x**(2*c)*(a + b) + + +def test_collect_5(): + """Collect with respect to a tuple""" + a, x, y, z, n = symbols('a,x,y,z,n') + assert collect(x**2*y**4 + z*(x*y**2)**2 + z + a*z, [x*y**2, z]) in [ + z*(1 + a + x**2*y**4) + x**2*y**4, + z*(1 + a) + x**2*y**4*(1 + z) ] + assert collect((1 + (x + y) + (x + y)**2).expand(), + [x, y]) == 1 + y + x*(1 + 2*y) + x**2 + y**2 + + +def test_collect_pr19431(): + """Unevaluated collect with respect to a product""" + a = symbols('a') + assert collect(a**2*(a**2 + 1), a**2, evaluate=False)[a**2] == (a**2 + 1) + + +def test_collect_D(): + D = Derivative + f = Function('f') + x, a, b = symbols('x,a,b') + fx = D(f(x), x) + fxx = D(f(x), x, x) + + assert collect(a*fx + b*fx, fx) == (a + b)*fx + assert collect(a*D(fx, x) + b*D(fx, x), fx) == (a + b)*D(fx, x) + assert collect(a*fxx + b*fxx, fx) == (a + b)*D(fx, x) + # issue 4784 + assert collect(5*f(x) + 3*fx, fx) == 5*f(x) + 3*fx + assert collect(f(x) + f(x)*diff(f(x), x) + x*diff(f(x), x)*f(x), f(x).diff(x)) == \ + (x*f(x) + f(x))*D(f(x), x) + f(x) + assert collect(f(x) + f(x)*diff(f(x), x) + x*diff(f(x), x)*f(x), f(x).diff(x), exact=True) == \ + (x*f(x) + f(x))*D(f(x), x) + f(x) + assert collect(1/f(x) + 1/f(x)*diff(f(x), x) + x*diff(f(x), x)/f(x), f(x).diff(x), exact=True) == \ + (1/f(x) + x/f(x))*D(f(x), x) + 1/f(x) + e = (1 + x*fx + fx)/f(x) + assert collect(e.expand(), fx) == fx*(x/f(x) + 1/f(x)) + 1/f(x) + + +def test_collect_func(): + f = ((x + a + 1)**3).expand() + + assert collect(f, x) == a**3 + 3*a**2 + 3*a + x**3 + x**2*(3*a + 3) + \ + x*(3*a**2 + 6*a + 3) + 1 + assert collect(f, x, factor) == x**3 + 3*x**2*(a + 1) + 3*x*(a + 1)**2 + \ + (a + 1)**3 + + assert collect(f, x, evaluate=False) == { + S.One: a**3 + 3*a**2 + 3*a + 1, + x: 3*a**2 + 6*a + 3, x**2: 3*a + 3, + x**3: 1 + } + + assert collect(f, x, factor, evaluate=False) == { + S.One: (a + 1)**3, x: 3*(a + 1)**2, + x**2: umul(S(3), a + 1), x**3: 1} + + +def test_collect_order(): + a, b, x, t = symbols('a,b,x,t') + + assert collect(t + t*x + t*x**2 + O(x**3), t) == t*(1 + x + x**2 + O(x**3)) + assert collect(t + t*x + x**2 + O(x**3), t) == \ + t*(1 + x + O(x**3)) + x**2 + O(x**3) + + f = a*x + b*x + c*x**2 + d*x**2 + O(x**3) + g = x*(a + b) + x**2*(c + d) + O(x**3) + + assert collect(f, x) == g + assert collect(f, x, distribute_order_term=False) == g + + f = sin(a + b).series(b, 0, 10) + + assert collect(f, [sin(a), cos(a)]) == \ + sin(a)*cos(b).series(b, 0, 10) + cos(a)*sin(b).series(b, 0, 10) + assert collect(f, [sin(a), cos(a)], distribute_order_term=False) == \ + sin(a)*cos(b).series(b, 0, 10).removeO() + \ + cos(a)*sin(b).series(b, 0, 10).removeO() + O(b**10) + + +def test_rcollect(): + assert rcollect((x**2*y + x*y + x + y)/(x + y), y) == \ + (x + y*(1 + x + x**2))/(x + y) + assert rcollect(sqrt(-((x + 1)*(y + 1))), z) == sqrt(-((x + 1)*(y + 1))) + + +def test_collect_D_0(): + D = Derivative + f = Function('f') + x, a, b = symbols('x,a,b') + fxx = D(f(x), x, x) + + assert collect(a*fxx + b*fxx, fxx) == (a + b)*fxx + + +def test_collect_Wild(): + """Collect with respect to functions with Wild argument""" + a, b, x, y = symbols('a b x y') + f = Function('f') + w1 = Wild('.1') + w2 = Wild('.2') + assert collect(f(x) + a*f(x), f(w1)) == (1 + a)*f(x) + assert collect(f(x, y) + a*f(x, y), f(w1)) == f(x, y) + a*f(x, y) + assert collect(f(x, y) + a*f(x, y), f(w1, w2)) == (1 + a)*f(x, y) + assert collect(f(x, y) + a*f(x, y), f(w1, w1)) == f(x, y) + a*f(x, y) + assert collect(f(x, x) + a*f(x, x), f(w1, w1)) == (1 + a)*f(x, x) + assert collect(a*(x + 1)**y + (x + 1)**y, w1**y) == (1 + a)*(x + 1)**y + assert collect(a*(x + 1)**y + (x + 1)**y, w1**b) == \ + a*(x + 1)**y + (x + 1)**y + assert collect(a*(x + 1)**y + (x + 1)**y, (x + 1)**w2) == \ + (1 + a)*(x + 1)**y + assert collect(a*(x + 1)**y + (x + 1)**y, w1**w2) == (1 + a)*(x + 1)**y + + +def test_collect_const(): + # coverage not provided by above tests + assert collect_const(2*sqrt(3) + 4*a*sqrt(5)) == \ + 2*(2*sqrt(5)*a + sqrt(3)) # let the primitive reabsorb + assert collect_const(2*sqrt(3) + 4*a*sqrt(5), sqrt(3)) == \ + 2*sqrt(3) + 4*a*sqrt(5) + assert collect_const(sqrt(2)*(1 + sqrt(2)) + sqrt(3) + x*sqrt(2)) == \ + sqrt(2)*(x + 1 + sqrt(2)) + sqrt(3) + + # issue 5290 + assert collect_const(2*x + 2*y + 1, 2) == \ + collect_const(2*x + 2*y + 1) == \ + Add(S.One, Mul(2, x + y, evaluate=False), evaluate=False) + assert collect_const(-y - z) == Mul(-1, y + z, evaluate=False) + assert collect_const(2*x - 2*y - 2*z, 2) == \ + Mul(2, x - y - z, evaluate=False) + assert collect_const(2*x - 2*y - 2*z, -2) == \ + _unevaluated_Add(2*x, Mul(-2, y + z, evaluate=False)) + + # this is why the content_primitive is used + eq = (sqrt(15 + 5*sqrt(2))*x + sqrt(3 + sqrt(2))*y)*2 + assert collect_sqrt(eq + 2) == \ + 2*sqrt(sqrt(2) + 3)*(sqrt(5)*x + y) + 2 + + # issue 16296 + assert collect_const(a + b + x/2 + y/2) == a + b + Mul(S.Half, x + y, evaluate=False) + + +def test_issue_13143(): + f = Function('f') + fx = f(x).diff(x) + e = f(x) + fx + f(x)*fx + # collect function before derivative + assert collect(e, Wild('w')) == f(x)*(fx + 1) + fx + e = f(x) + f(x)*fx + x*fx*f(x) + assert collect(e, fx) == (x*f(x) + f(x))*fx + f(x) + assert collect(e, f(x)) == (x*fx + fx + 1)*f(x) + e = f(x) + fx + f(x)*fx + assert collect(e, [f(x), fx]) == f(x)*(1 + fx) + fx + assert collect(e, [fx, f(x)]) == fx*(1 + f(x)) + f(x) + + +def test_issue_6097(): + assert collect(a*y**(2.0*x) + b*y**(2.0*x), y**x) == (a + b)*(y**x)**2.0 + assert collect(a*2**(2.0*x) + b*2**(2.0*x), 2**x) == (a + b)*(2**x)**2.0 + + +def test_fraction_expand(): + eq = (x + y)*y/x + assert eq.expand(frac=True) == fraction_expand(eq) == (x*y + y**2)/x + assert eq.expand() == y + y**2/x + + +def test_fraction(): + x, y, z = map(Symbol, 'xyz') + A = Symbol('A', commutative=False) + + assert fraction(S.Half) == (1, 2) + + assert fraction(x) == (x, 1) + assert fraction(1/x) == (1, x) + assert fraction(x/y) == (x, y) + assert fraction(x/2) == (x, 2) + + assert fraction(x*y/z) == (x*y, z) + assert fraction(x/(y*z)) == (x, y*z) + + assert fraction(1/y**2) == (1, y**2) + assert fraction(x/y**2) == (x, y**2) + + assert fraction((x**2 + 1)/y) == (x**2 + 1, y) + assert fraction(x*(y + 1)/y**7) == (x*(y + 1), y**7) + + assert fraction(exp(-x), exact=True) == (exp(-x), 1) + assert fraction((1/(x + y))/2, exact=True) == (1, Mul(2,(x + y), evaluate=False)) + + assert fraction(x*A/y) == (x*A, y) + assert fraction(x*A**-1/y) == (x*A**-1, y) + + n = symbols('n', negative=True) + assert fraction(exp(n)) == (1, exp(-n)) + assert fraction(exp(-n)) == (exp(-n), 1) + + p = symbols('p', positive=True) + assert fraction(exp(-p)*log(p), exact=True) == (exp(-p)*log(p), 1) + + m = Mul(1, 1, S.Half, evaluate=False) + assert fraction(m) == (1, 2) + assert fraction(m, exact=True) == (Mul(1, 1, evaluate=False), 2) + + m = Mul(1, 1, S.Half, S.Half, Pow(1, -1, evaluate=False), evaluate=False) + assert fraction(m) == (1, 4) + assert fraction(m, exact=True) == \ + (Mul(1, 1, evaluate=False), Mul(2, 2, 1, evaluate=False)) + + +def test_issue_5615(): + aA, Re, a, b, D = symbols('aA Re a b D') + e = ((D**3*a + b*aA**3)/Re).expand() + assert collect(e, [aA**3/Re, a]) == e + + +def test_issue_5933(): + from sympy.geometry.polygon import (Polygon, RegularPolygon) + from sympy.simplify.radsimp import denom + x = Polygon(*RegularPolygon((0, 0), 1, 5).vertices).centroid.x + assert abs(denom(x).n()) > 1e-12 + assert abs(denom(radsimp(x))) > 1e-12 # in case simplify didn't handle it + + +def test_issue_14608(): + a, b = symbols('a b', commutative=False) + x, y = symbols('x y') + raises(AttributeError, lambda: collect(a*b + b*a, a)) + assert collect(x*y + y*(x+1), a) == x*y + y*(x+1) + assert collect(x*y + y*(x+1) + a*b + b*a, y) == y*(2*x + 1) + a*b + b*a + + +def test_collect_abs(): + s = abs(x) + abs(y) + assert collect_abs(s) == s + assert unchanged(Mul, abs(x), abs(y)) + ans = Abs(x*y) + assert isinstance(ans, Abs) + assert collect_abs(abs(x)*abs(y)) == ans + assert collect_abs(1 + exp(abs(x)*abs(y))) == 1 + exp(ans) + + # See https://github.com/sympy/sympy/issues/12910 + p = Symbol('p', positive=True) + assert collect_abs(p/abs(1-p)).is_commutative is True + + +def test_issue_19149(): + eq = exp(3*x/4) + assert collect(eq, exp(x)) == eq + +def test_issue_19719(): + a, b = symbols('a, b') + expr = a**2 * (b + 1) + (7 + 1/b)/a + collected = collect(expr, (a**2, 1/a), evaluate=False) + # Would return {_Dummy_20**(-2): b + 1, 1/a: 7 + 1/b} without xreplace + assert collected == {a**2: b + 1, 1/a: 7 + 1/b} + + +def test_issue_21355(): + assert radsimp(1/(x + sqrt(x**2))) == 1/(x + sqrt(x**2)) + assert radsimp(1/(x - sqrt(x**2))) == 1/(x - sqrt(x**2)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_ratsimp.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_ratsimp.py new file mode 100644 index 0000000000000000000000000000000000000000..14e84fd2b227518baff1bda4e5b27ecc40a8bcdd --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_ratsimp.py @@ -0,0 +1,78 @@ +from sympy.core.numbers import (Rational, pi) +from sympy.functions.elementary.exponential import log +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.special.error_functions import erf +from sympy.polys.domains import GF +from sympy.simplify.ratsimp import (ratsimp, ratsimpmodprime) + +from sympy.abc import x, y, z, t, a, b, c, d, e + + +def test_ratsimp(): + f, g = 1/x + 1/y, (x + y)/(x*y) + + assert f != g and ratsimp(f) == g + + f, g = 1/(1 + 1/x), 1 - 1/(x + 1) + + assert f != g and ratsimp(f) == g + + f, g = x/(x + y) + y/(x + y), 1 + + assert f != g and ratsimp(f) == g + + f, g = -x - y - y**2/(x + y) + x**2/(x + y), -2*y + + assert f != g and ratsimp(f) == g + + f = (a*c*x*y + a*c*z - b*d*x*y - b*d*z - b*t*x*y - b*t*x - b*t*z + + e*x)/(x*y + z) + G = [a*c - b*d - b*t + (-b*t*x + e*x)/(x*y + z), + a*c - b*d - b*t - ( b*t*x - e*x)/(x*y + z)] + + assert f != g and ratsimp(f) in G + + A = sqrt(pi) + + B = log(erf(x) - 1) + C = log(erf(x) + 1) + + D = 8 - 8*erf(x) + + f = A*B/D - A*C/D + A*C*erf(x)/D - A*B*erf(x)/D + 2*A/D + + assert ratsimp(f) == A*B/8 - A*C/8 - A/(4*erf(x) - 4) + + +def test_ratsimpmodprime(): + a = y**5 + x + y + b = x - y + F = [x*y**5 - x - y] + assert ratsimpmodprime(a/b, F, x, y, order='lex') == \ + (-x**2 - x*y - x - y) / (-x**2 + x*y) + + a = x + y**2 - 2 + b = x + y**2 - y - 1 + F = [x*y - 1] + assert ratsimpmodprime(a/b, F, x, y, order='lex') == \ + (1 + y - x)/(y - x) + + a = 5*x**3 + 21*x**2 + 4*x*y + 23*x + 12*y + 15 + b = 7*x**3 - y*x**2 + 31*x**2 + 2*x*y + 15*y + 37*x + 21 + F = [x**2 + y**2 - 1] + assert ratsimpmodprime(a/b, F, x, y, order='lex') == \ + (1 + 5*y - 5*x)/(8*y - 6*x) + + a = x*y - x - 2*y + 4 + b = x + y**2 - 2*y + F = [x - 2, y - 3] + assert ratsimpmodprime(a/b, F, x, y, order='lex') == \ + Rational(2, 5) + + # Test a bug where denominators would be dropped + assert ratsimpmodprime(x, [y - 2*x], order='lex') == \ + y/2 + + a = (x**5 + 2*x**4 + 2*x**3 + 2*x**2 + x + 2/x + x**(-2)) + assert ratsimpmodprime(a, [x + 1], domain=GF(2)) == 1 + assert ratsimpmodprime(a, [x + 1], domain=GF(3)) == -1 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_rewrite.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_rewrite.py new file mode 100644 index 0000000000000000000000000000000000000000..56d2fb7a85bd959bd4accc2f36127429efbdbe70 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_rewrite.py @@ -0,0 +1,31 @@ +from sympy.core.numbers import I +from sympy.core.symbol import symbols +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.trigonometric import (cos, cot, sin) +from sympy.testing.pytest import _both_exp_pow + +x, y, z, n = symbols('x,y,z,n') + + +@_both_exp_pow +def test_has(): + assert cot(x).has(x) + assert cot(x).has(cot) + assert not cot(x).has(sin) + assert sin(x).has(x) + assert sin(x).has(sin) + assert not sin(x).has(cot) + assert exp(x).has(exp) + + +@_both_exp_pow +def test_sin_exp_rewrite(): + assert sin(x).rewrite(sin, exp) == -I/2*(exp(I*x) - exp(-I*x)) + assert sin(x).rewrite(sin, exp).rewrite(exp, sin) == sin(x) + assert cos(x).rewrite(cos, exp).rewrite(exp, cos) == cos(x) + assert (sin(5*y) - sin( + 2*x)).rewrite(sin, exp).rewrite(exp, sin) == sin(5*y) - sin(2*x) + assert sin(x + y).rewrite(sin, exp).rewrite(exp, sin) == sin(x + y) + assert cos(x + y).rewrite(cos, exp).rewrite(exp, cos) == cos(x + y) + # This next test currently passes... not clear whether it should or not? + assert cos(x).rewrite(cos, exp).rewrite(exp, sin) == cos(x) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_simplify.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_simplify.py new file mode 100644 index 0000000000000000000000000000000000000000..a5bf469f68adf5c5dfbdf7559414681e2fb28ba7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_simplify.py @@ -0,0 +1,1093 @@ +from sympy.concrete.summations import Sum +from sympy.core.add import Add +from sympy.core.basic import Basic +from sympy.core.expr import unchanged +from sympy.core.function import (count_ops, diff, expand, expand_multinomial, Function, Derivative) +from sympy.core.mul import Mul, _keep_coeff +from sympy.core import GoldenRatio +from sympy.core.numbers import (E, Float, I, oo, pi, Rational, zoo) +from sympy.core.relational import (Eq, Lt, Gt, Ge, Le) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import (binomial, factorial) +from sympy.functions.elementary.complexes import (Abs, sign) +from sympy.functions.elementary.exponential import (exp, exp_polar, log) +from sympy.functions.elementary.hyperbolic import (cosh, csch, sinh) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (acos, asin, atan, cos, sin, sinc, tan) +from sympy.functions.special.error_functions import erf +from sympy.functions.special.gamma_functions import gamma +from sympy.functions.special.hyper import hyper +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.geometry.polygon import rad +from sympy.integrals.integrals import (Integral, integrate) +from sympy.logic.boolalg import (And, Or) +from sympy.matrices.dense import (Matrix, eye) +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.polys.polytools import (factor, Poly) +from sympy.simplify.simplify import (besselsimp, hypersimp, inversecombine, logcombine, nsimplify, nthroot, posify, separatevars, signsimp, simplify) +from sympy.solvers.solvers import solve + +from sympy.testing.pytest import XFAIL, slow, _both_exp_pow +from sympy.abc import x, y, z, t, a, b, c, d, e, f, g, h, i, n + + +def test_issue_7263(): + assert abs((simplify(30.8**2 - 82.5**2 * sin(rad(11.6))**2)).evalf() - \ + 673.447451402970) < 1e-12 + + +def test_factorial_simplify(): + # There are more tests in test_factorials.py. + x = Symbol('x') + assert simplify(factorial(x)/x) == gamma(x) + assert simplify(factorial(factorial(x))) == factorial(factorial(x)) + + +def test_simplify_expr(): + x, y, z, k, n, m, w, s, A = symbols('x,y,z,k,n,m,w,s,A') + f = Function('f') + + assert all(simplify(tmp) == tmp for tmp in [I, E, oo, x, -x, -oo, -E, -I]) + + e = 1/x + 1/y + assert e != (x + y)/(x*y) + assert simplify(e) == (x + y)/(x*y) + + e = A**2*s**4/(4*pi*k*m**3) + assert simplify(e) == e + + e = (4 + 4*x - 2*(2 + 2*x))/(2 + 2*x) + assert simplify(e) == 0 + + e = (-4*x*y**2 - 2*y**3 - 2*x**2*y)/(x + y)**2 + assert simplify(e) == -2*y + + e = -x - y - (x + y)**(-1)*y**2 + (x + y)**(-1)*x**2 + assert simplify(e) == -2*y + + e = (x + x*y)/x + assert simplify(e) == 1 + y + + e = (f(x) + y*f(x))/f(x) + assert simplify(e) == 1 + y + + e = (2 * (1/n - cos(n * pi)/n))/pi + assert simplify(e) == (-cos(pi*n) + 1)/(pi*n)*2 + + e = integrate(1/(x**3 + 1), x).diff(x) + assert simplify(e) == 1/(x**3 + 1) + + e = integrate(x/(x**2 + 3*x + 1), x).diff(x) + assert simplify(e) == x/(x**2 + 3*x + 1) + + f = Symbol('f') + A = Matrix([[2*k - m*w**2, -k], [-k, k - m*w**2]]).inv() + assert simplify((A*Matrix([0, f]))[1] - + (-f*(2*k - m*w**2)/(k**2 - (k - m*w**2)*(2*k - m*w**2)))) == 0 + + f = -x + y/(z + t) + z*x/(z + t) + z*a/(z + t) + t*x/(z + t) + assert simplify(f) == (y + a*z)/(z + t) + + # issue 10347 + expr = -x*(y**2 - 1)*(2*y**2*(x**2 - 1)/(a*(x**2 - y**2)**2) + (x**2 - 1) + /(a*(x**2 - y**2)))/(a*(x**2 - y**2)) + x*(-2*x**2*sqrt(-x**2*y**2 + x**2 + + y**2 - 1)*sin(z)/(a*(x**2 - y**2)**2) - x**2*sqrt(-x**2*y**2 + x**2 + + y**2 - 1)*sin(z)/(a*(x**2 - 1)*(x**2 - y**2)) + (x**2*sqrt((-x**2 + 1)* + (y**2 - 1))*sqrt(-x**2*y**2 + x**2 + y**2 - 1)*sin(z)/(x**2 - 1) + sqrt( + (-x**2 + 1)*(y**2 - 1))*(x*(-x*y**2 + x)/sqrt(-x**2*y**2 + x**2 + y**2 - + 1) + sqrt(-x**2*y**2 + x**2 + y**2 - 1))*sin(z))/(a*sqrt((-x**2 + 1)*( + y**2 - 1))*(x**2 - y**2)))*sqrt(-x**2*y**2 + x**2 + y**2 - 1)*sin(z)/(a* + (x**2 - y**2)) + x*(-2*x**2*sqrt(-x**2*y**2 + x**2 + y**2 - 1)*cos(z)/(a* + (x**2 - y**2)**2) - x**2*sqrt(-x**2*y**2 + x**2 + y**2 - 1)*cos(z)/(a* + (x**2 - 1)*(x**2 - y**2)) + (x**2*sqrt((-x**2 + 1)*(y**2 - 1))*sqrt(-x**2 + *y**2 + x**2 + y**2 - 1)*cos(z)/(x**2 - 1) + x*sqrt((-x**2 + 1)*(y**2 - + 1))*(-x*y**2 + x)*cos(z)/sqrt(-x**2*y**2 + x**2 + y**2 - 1) + sqrt((-x**2 + + 1)*(y**2 - 1))*sqrt(-x**2*y**2 + x**2 + y**2 - 1)*cos(z))/(a*sqrt((-x**2 + + 1)*(y**2 - 1))*(x**2 - y**2)))*sqrt(-x**2*y**2 + x**2 + y**2 - 1)*cos( + z)/(a*(x**2 - y**2)) - y*sqrt((-x**2 + 1)*(y**2 - 1))*(-x*y*sqrt(-x**2* + y**2 + x**2 + y**2 - 1)*sin(z)/(a*(x**2 - y**2)*(y**2 - 1)) + 2*x*y*sqrt( + -x**2*y**2 + x**2 + y**2 - 1)*sin(z)/(a*(x**2 - y**2)**2) + (x*y*sqrt(( + -x**2 + 1)*(y**2 - 1))*sqrt(-x**2*y**2 + x**2 + y**2 - 1)*sin(z)/(y**2 - + 1) + x*sqrt((-x**2 + 1)*(y**2 - 1))*(-x**2*y + y)*sin(z)/sqrt(-x**2*y**2 + + x**2 + y**2 - 1))/(a*sqrt((-x**2 + 1)*(y**2 - 1))*(x**2 - y**2)))*sin( + z)/(a*(x**2 - y**2)) + y*(x**2 - 1)*(-2*x*y*(x**2 - 1)/(a*(x**2 - y**2) + **2) + 2*x*y/(a*(x**2 - y**2)))/(a*(x**2 - y**2)) + y*(x**2 - 1)*(y**2 - + 1)*(-x*y*sqrt(-x**2*y**2 + x**2 + y**2 - 1)*cos(z)/(a*(x**2 - y**2)*(y**2 + - 1)) + 2*x*y*sqrt(-x**2*y**2 + x**2 + y**2 - 1)*cos(z)/(a*(x**2 - y**2) + **2) + (x*y*sqrt((-x**2 + 1)*(y**2 - 1))*sqrt(-x**2*y**2 + x**2 + y**2 - + 1)*cos(z)/(y**2 - 1) + x*sqrt((-x**2 + 1)*(y**2 - 1))*(-x**2*y + y)*cos( + z)/sqrt(-x**2*y**2 + x**2 + y**2 - 1))/(a*sqrt((-x**2 + 1)*(y**2 - 1) + )*(x**2 - y**2)))*cos(z)/(a*sqrt((-x**2 + 1)*(y**2 - 1))*(x**2 - y**2) + ) - x*sqrt((-x**2 + 1)*(y**2 - 1))*sqrt(-x**2*y**2 + x**2 + y**2 - 1)*sin( + z)**2/(a**2*(x**2 - 1)*(x**2 - y**2)*(y**2 - 1)) - x*sqrt((-x**2 + 1)*( + y**2 - 1))*sqrt(-x**2*y**2 + x**2 + y**2 - 1)*cos(z)**2/(a**2*(x**2 - 1)*( + x**2 - y**2)*(y**2 - 1)) + assert simplify(expr) == 2*x/(a**2*(x**2 - y**2)) + + #issue 17631 + assert simplify('((-1/2)*Boole(True)*Boole(False)-1)*Boole(True)') == \ + Mul(sympify('(2 + Boole(True)*Boole(False))'), sympify('-Boole(True)/2')) + + A, B = symbols('A,B', commutative=False) + + assert simplify(A*B - B*A) == A*B - B*A + assert simplify(A/(1 + y/x)) == x*A/(x + y) + assert simplify(A*(1/x + 1/y)) == A/x + A/y #(x + y)*A/(x*y) + + assert simplify(log(2) + log(3)) == log(6) + assert simplify(log(2*x) - log(2)) == log(x) + + assert simplify(hyper([], [], x)) == exp(x) + + +def test_issue_3557(): + f_1 = x*a + y*b + z*c - 1 + f_2 = x*d + y*e + z*f - 1 + f_3 = x*g + y*h + z*i - 1 + + solutions = solve([f_1, f_2, f_3], x, y, z, simplify=False) + + assert simplify(solutions[y]) == \ + (a*i + c*d + f*g - a*f - c*g - d*i)/ \ + (a*e*i + b*f*g + c*d*h - a*f*h - b*d*i - c*e*g) + + +def test_simplify_other(): + assert simplify(sin(x)**2 + cos(x)**2) == 1 + assert simplify(gamma(x + 1)/gamma(x)) == x + assert simplify(sin(x)**2 + cos(x)**2 + factorial(x)/gamma(x)) == 1 + x + assert simplify( + Eq(sin(x)**2 + cos(x)**2, factorial(x)/gamma(x))) == Eq(x, 1) + nc = symbols('nc', commutative=False) + assert simplify(x + x*nc) == x*(1 + nc) + # issue 6123 + # f = exp(-I*(k*sqrt(t) + x/(2*sqrt(t)))**2) + # ans = integrate(f, (k, -oo, oo), conds='none') + ans = I*(-pi*x*exp(I*pi*Rational(-3, 4) + I*x**2/(4*t))*erf(x*exp(I*pi*Rational(-3, 4))/ + (2*sqrt(t)))/(2*sqrt(t)) + pi*x*exp(I*pi*Rational(-3, 4) + I*x**2/(4*t))/ + (2*sqrt(t)))*exp(-I*x**2/(4*t))/(sqrt(pi)*x) - I*sqrt(pi) * \ + (-erf(x*exp(I*pi/4)/(2*sqrt(t))) + 1)*exp(I*pi/4)/(2*sqrt(t)) + assert simplify(ans) == -(-1)**Rational(3, 4)*sqrt(pi)/sqrt(t) + # issue 6370 + assert simplify(2**(2 + x)/4) == 2**x + + +@_both_exp_pow +def test_simplify_complex(): + cosAsExp = cos(x)._eval_rewrite_as_exp(x) + tanAsExp = tan(x)._eval_rewrite_as_exp(x) + assert simplify(cosAsExp*tanAsExp) == sin(x) # issue 4341 + + # issue 10124 + assert simplify(exp(Matrix([[0, -1], [1, 0]]))) == Matrix([[cos(1), + -sin(1)], [sin(1), cos(1)]]) + + +def test_simplify_ratio(): + # roots of x**3-3*x+5 + roots = ['(1/2 - sqrt(3)*I/2)*(sqrt(21)/2 + 5/2)**(1/3) + 1/((1/2 - ' + 'sqrt(3)*I/2)*(sqrt(21)/2 + 5/2)**(1/3))', + '1/((1/2 + sqrt(3)*I/2)*(sqrt(21)/2 + 5/2)**(1/3)) + ' + '(1/2 + sqrt(3)*I/2)*(sqrt(21)/2 + 5/2)**(1/3)', + '-(sqrt(21)/2 + 5/2)**(1/3) - 1/(sqrt(21)/2 + 5/2)**(1/3)'] + + for r in roots: + r = S(r) + assert count_ops(simplify(r, ratio=1)) <= count_ops(r) + # If ratio=oo, simplify() is always applied: + assert simplify(r, ratio=oo) is not r + + +def test_simplify_measure(): + measure1 = lambda expr: len(str(expr)) + measure2 = lambda expr: -count_ops(expr) + # Return the most complicated result + expr = (x + 1)/(x + sin(x)**2 + cos(x)**2) + assert measure1(simplify(expr, measure=measure1)) <= measure1(expr) + assert measure2(simplify(expr, measure=measure2)) <= measure2(expr) + + expr2 = Eq(sin(x)**2 + cos(x)**2, 1) + assert measure1(simplify(expr2, measure=measure1)) <= measure1(expr2) + assert measure2(simplify(expr2, measure=measure2)) <= measure2(expr2) + + +def test_simplify_rational(): + expr = 2**x*2.**y + assert simplify(expr, rational = True) == 2**(x+y) + assert simplify(expr, rational = None) == 2.0**(x+y) + assert simplify(expr, rational = False) == expr + assert simplify('0.9 - 0.8 - 0.1', rational = True) == 0 + + +def test_simplify_issue_1308(): + assert simplify(exp(Rational(-1, 2)) + exp(Rational(-3, 2))) == \ + (1 + E)*exp(Rational(-3, 2)) + + +def test_issue_5652(): + assert simplify(E + exp(-E)) == exp(-E) + E + n = symbols('n', commutative=False) + assert simplify(n + n**(-n)) == n + n**(-n) + +def test_issue_27380(): + assert simplify(1.0**(x+1)/1.0**x) == 1.0 + +def test_simplify_fail1(): + x = Symbol('x') + y = Symbol('y') + e = (x + y)**2/(-4*x*y**2 - 2*y**3 - 2*x**2*y) + assert simplify(e) == 1 / (-2*y) + + +def test_nthroot(): + assert nthroot(90 + 34*sqrt(7), 3) == sqrt(7) + 3 + q = 1 + sqrt(2) - 2*sqrt(3) + sqrt(6) + sqrt(7) + assert nthroot(expand_multinomial(q**3), 3) == q + assert nthroot(41 + 29*sqrt(2), 5) == 1 + sqrt(2) + assert nthroot(-41 - 29*sqrt(2), 5) == -1 - sqrt(2) + expr = 1320*sqrt(10) + 4216 + 2576*sqrt(6) + 1640*sqrt(15) + assert nthroot(expr, 5) == 1 + sqrt(6) + sqrt(15) + q = 1 + sqrt(2) + sqrt(3) + sqrt(5) + assert expand_multinomial(nthroot(expand_multinomial(q**5), 5)) == q + q = 1 + sqrt(2) + 7*sqrt(6) + 2*sqrt(10) + assert nthroot(expand_multinomial(q**5), 5, 8) == q + q = 1 + sqrt(2) - 2*sqrt(3) + 1171*sqrt(6) + assert nthroot(expand_multinomial(q**3), 3) == q + assert nthroot(expand_multinomial(q**6), 6) == q + + +def test_nthroot1(): + q = 1 + sqrt(2) + sqrt(3) + S.One/10**20 + p = expand_multinomial(q**5) + assert nthroot(p, 5) == q + q = 1 + sqrt(2) + sqrt(3) + S.One/10**30 + p = expand_multinomial(q**5) + assert nthroot(p, 5) == q + + +@_both_exp_pow +def test_separatevars(): + x, y, z, n = symbols('x,y,z,n') + assert separatevars(2*n*x*z + 2*x*y*z) == 2*x*z*(n + y) + assert separatevars(x*z + x*y*z) == x*z*(1 + y) + assert separatevars(pi*x*z + pi*x*y*z) == pi*x*z*(1 + y) + assert separatevars(x*y**2*sin(x) + x*sin(x)*sin(y)) == \ + x*(sin(y) + y**2)*sin(x) + assert separatevars(x*exp(x + y) + x*exp(x)) == x*(1 + exp(y))*exp(x) + assert separatevars((x*(y + 1))**z).is_Pow # != x**z*(1 + y)**z + assert separatevars(1 + x + y + x*y) == (x + 1)*(y + 1) + assert separatevars(y/pi*exp(-(z - x)/cos(n))) == \ + y*exp(x/cos(n))*exp(-z/cos(n))/pi + assert separatevars((x + y)*(x - y) + y**2 + 2*x + 1) == (x + 1)**2 + # issue 4858 + p = Symbol('p', positive=True) + assert separatevars(sqrt(p**2 + x*p**2)) == p*sqrt(1 + x) + assert separatevars(sqrt(y*(p**2 + x*p**2))) == p*sqrt(y*(1 + x)) + assert separatevars(sqrt(y*(p**2 + x*p**2)), force=True) == \ + p*sqrt(y)*sqrt(1 + x) + # issue 4865 + assert separatevars(sqrt(x*y)).is_Pow + assert separatevars(sqrt(x*y), force=True) == sqrt(x)*sqrt(y) + # issue 4957 + # any type sequence for symbols is fine + assert separatevars(((2*x + 2)*y), dict=True, symbols=()) == \ + {'coeff': 1, x: 2*x + 2, y: y} + # separable + assert separatevars(((2*x + 2)*y), dict=True, symbols=[x]) == \ + {'coeff': y, x: 2*x + 2} + assert separatevars(((2*x + 2)*y), dict=True, symbols=[]) == \ + {'coeff': 1, x: 2*x + 2, y: y} + assert separatevars(((2*x + 2)*y), dict=True) == \ + {'coeff': 1, x: 2*x + 2, y: y} + assert separatevars(((2*x + 2)*y), dict=True, symbols=None) == \ + {'coeff': y*(2*x + 2)} + # not separable + assert separatevars(3, dict=True) is None + assert separatevars(2*x + y, dict=True, symbols=()) is None + assert separatevars(2*x + y, dict=True) is None + assert separatevars(2*x + y, dict=True, symbols=None) == {'coeff': 2*x + y} + # issue 4808 + n, m = symbols('n,m', commutative=False) + assert separatevars(m + n*m) == (1 + n)*m + assert separatevars(x + x*n) == x*(1 + n) + # issue 4910 + f = Function('f') + assert separatevars(f(x) + x*f(x)) == f(x) + x*f(x) + # a noncommutable object present + eq = x*(1 + hyper((), (), y*z)) + assert separatevars(eq) == eq + + s = separatevars(abs(x*y)) + assert s == abs(x)*abs(y) and s.is_Mul + z = cos(1)**2 + sin(1)**2 - 1 + a = abs(x*z) + s = separatevars(a) + assert not a.is_Mul and s.is_Mul and s == abs(x)*abs(z) + s = separatevars(abs(x*y*z)) + assert s == abs(x)*abs(y)*abs(z) + + # abs(x+y)/abs(z) would be better but we test this here to + # see that it doesn't raise + assert separatevars(abs((x+y)/z)) == abs((x+y)/z) + + +def test_separatevars_advanced_factor(): + x, y, z = symbols('x,y,z') + assert separatevars(1 + log(x)*log(y) + log(x) + log(y)) == \ + (log(x) + 1)*(log(y) + 1) + assert separatevars(1 + x - log(z) - x*log(z) - exp(y)*log(z) - + x*exp(y)*log(z) + x*exp(y) + exp(y)) == \ + -((x + 1)*(log(z) - 1)*(exp(y) + 1)) + x, y = symbols('x,y', positive=True) + assert separatevars(1 + log(x**log(y)) + log(x*y)) == \ + (log(x) + 1)*(log(y) + 1) + + +def test_hypersimp(): + n, k = symbols('n,k', integer=True) + + assert hypersimp(factorial(k), k) == k + 1 + assert hypersimp(factorial(k**2), k) is None + + assert hypersimp(1/factorial(k), k) == 1/(k + 1) + + assert hypersimp(2**k/factorial(k)**2, k) == 2/(k + 1)**2 + + assert hypersimp(binomial(n, k), k) == (n - k)/(k + 1) + assert hypersimp(binomial(n + 1, k), k) == (n - k + 1)/(k + 1) + + term = (4*k + 1)*factorial(k)/factorial(2*k + 1) + assert hypersimp(term, k) == S.Half*((4*k + 5)/(3 + 14*k + 8*k**2)) + + term = 1/((2*k - 1)*factorial(2*k + 1)) + assert hypersimp(term, k) == (k - S.Half)/((k + 1)*(2*k + 1)*(2*k + 3)) + + term = binomial(n, k)*(-1)**k/factorial(k) + assert hypersimp(term, k) == (k - n)/(k + 1)**2 + + +def test_nsimplify(): + x = Symbol("x") + assert nsimplify(0) == 0 + assert nsimplify(-1) == -1 + assert nsimplify(1) == 1 + assert nsimplify(1 + x) == 1 + x + assert nsimplify(2.7) == Rational(27, 10) + assert nsimplify(1 - GoldenRatio) == (1 - sqrt(5))/2 + assert nsimplify((1 + sqrt(5))/4, [GoldenRatio]) == GoldenRatio/2 + assert nsimplify(2/GoldenRatio, [GoldenRatio]) == 2*GoldenRatio - 2 + assert nsimplify(exp(pi*I*Rational(5, 3), evaluate=False)) == \ + sympify('1/2 - sqrt(3)*I/2') + assert nsimplify(sin(pi*Rational(3, 5), evaluate=False)) == \ + sympify('sqrt(sqrt(5)/8 + 5/8)') + assert nsimplify(sqrt(atan('1', evaluate=False))*(2 + I), [pi]) == \ + sqrt(pi) + sqrt(pi)/2*I + assert nsimplify(2 + exp(2*atan('1/4')*I)) == sympify('49/17 + 8*I/17') + assert nsimplify(pi, tolerance=0.01) == Rational(22, 7) + assert nsimplify(pi, tolerance=0.001) == Rational(355, 113) + assert nsimplify(0.33333, tolerance=1e-4) == Rational(1, 3) + assert nsimplify(2.0**(1/3.), tolerance=0.001) == Rational(635, 504) + assert nsimplify(2.0**(1/3.), tolerance=0.001, full=True) == \ + 2**Rational(1, 3) + assert nsimplify(x + .5, rational=True) == S.Half + x + assert nsimplify(1/.3 + x, rational=True) == Rational(10, 3) + x + assert nsimplify(log(3).n(), rational=True) == \ + sympify('109861228866811/100000000000000') + assert nsimplify(Float(0.272198261287950), [pi, log(2)]) == pi*log(2)/8 + assert nsimplify(Float(0.272198261287950).n(3), [pi, log(2)]) == \ + -pi/4 - log(2) + Rational(7, 4) + assert nsimplify(x/7.0) == x/7 + assert nsimplify(pi/1e2) == pi/100 + assert nsimplify(pi/1e2, rational=False) == pi/100.0 + assert nsimplify(pi/1e-7) == 10000000*pi + assert not nsimplify( + factor(-3.0*z**2*(z**2)**(-2.5) + 3*(z**2)**(-1.5))).atoms(Float) + e = x**0.0 + assert e.is_Pow and nsimplify(x**0.0) == 1 + assert nsimplify(3.333333, tolerance=0.1, rational=True) == Rational(10, 3) + assert nsimplify(3.333333, tolerance=0.01, rational=True) == Rational(10, 3) + assert nsimplify(3.666666, tolerance=0.1, rational=True) == Rational(11, 3) + assert nsimplify(3.666666, tolerance=0.01, rational=True) == Rational(11, 3) + assert nsimplify(33, tolerance=10, rational=True) == Rational(33) + assert nsimplify(33.33, tolerance=10, rational=True) == Rational(30) + assert nsimplify(37.76, tolerance=10, rational=True) == Rational(40) + assert nsimplify(-203.1) == Rational(-2031, 10) + assert nsimplify(.2, tolerance=0) == Rational(1, 5) + assert nsimplify(-.2, tolerance=0) == Rational(-1, 5) + assert nsimplify(.2222, tolerance=0) == Rational(1111, 5000) + assert nsimplify(-.2222, tolerance=0) == Rational(-1111, 5000) + # issue 7211, PR 4112 + assert nsimplify(S(2e-8)) == Rational(1, 50000000) + # issue 7322 direct test + assert nsimplify(1e-42, rational=True) != 0 + # issue 10336 + inf = Float('inf') + infs = (-oo, oo, inf, -inf) + for zi in infs: + ans = sign(zi)*oo + assert nsimplify(zi) == ans + assert nsimplify(zi + x) == x + ans + + assert nsimplify(0.33333333, rational=True, rational_conversion='exact') == Rational(0.33333333) + + # Make sure nsimplify on expressions uses full precision + assert nsimplify(pi.evalf(100)*x, rational_conversion='exact').evalf(100) == pi.evalf(100)*x + + +def test_issue_9448(): + tmp = sympify("1/(1 - (-1)**(2/3) - (-1)**(1/3)) + 1/(1 + (-1)**(2/3) + (-1)**(1/3))") + assert nsimplify(tmp) == S.Half + + +def test_extract_minus_sign(): + x = Symbol("x") + y = Symbol("y") + a = Symbol("a") + b = Symbol("b") + assert simplify(-x/-y) == x/y + assert simplify(-x/y) == -x/y + assert simplify(x/y) == x/y + assert simplify(x/-y) == -x/y + assert simplify(-x/0) == zoo*x + assert simplify(Rational(-5, 0)) is zoo + assert simplify(-a*x/(-y - b)) == a*x/(b + y) + + +def test_diff(): + x = Symbol("x") + y = Symbol("y") + f = Function("f") + g = Function("g") + assert simplify(g(x).diff(x)*f(x).diff(x) - f(x).diff(x)*g(x).diff(x)) == 0 + assert simplify(2*f(x)*f(x).diff(x) - diff(f(x)**2, x)) == 0 + assert simplify(diff(1/f(x), x) + f(x).diff(x)/f(x)**2) == 0 + assert simplify(f(x).diff(x, y) - f(x).diff(y, x)) == 0 + + +def test_logcombine_1(): + x, y = symbols("x,y") + a = Symbol("a") + z, w = symbols("z,w", positive=True) + b = Symbol("b", real=True) + assert logcombine(log(x) + 2*log(y)) == log(x) + 2*log(y) + assert logcombine(log(x) + 2*log(y), force=True) == log(x*y**2) + assert logcombine(a*log(w) + log(z)) == a*log(w) + log(z) + assert logcombine(b*log(z) + b*log(x)) == log(z**b) + b*log(x) + assert logcombine(b*log(z) - log(w)) == log(z**b/w) + assert logcombine(log(x)*log(z)) == log(x)*log(z) + assert logcombine(log(w)*log(x)) == log(w)*log(x) + assert logcombine(cos(-2*log(z) + b*log(w))) in [cos(log(w**b/z**2)), + cos(log(z**2/w**b))] + assert logcombine(log(log(x) - log(y)) - log(z), force=True) == \ + log(log(x/y)/z) + assert logcombine((2 + I)*log(x), force=True) == (2 + I)*log(x) + assert logcombine((x**2 + log(x) - log(y))/(x*y), force=True) == \ + (x**2 + log(x/y))/(x*y) + # the following could also give log(z*x**log(y**2)), what we + # are testing is that a canonical result is obtained + assert logcombine(log(x)*2*log(y) + log(z), force=True) == \ + log(z*y**log(x**2)) + assert logcombine((x*y + sqrt(x**4 + y**4) + log(x) - log(y))/(pi*x**Rational(2, 3)* + sqrt(y)**3), force=True) == ( + x*y + sqrt(x**4 + y**4) + log(x/y))/(pi*x**Rational(2, 3)*y**Rational(3, 2)) + assert logcombine(gamma(-log(x/y))*acos(-log(x/y)), force=True) == \ + acos(-log(x/y))*gamma(-log(x/y)) + + assert logcombine(2*log(z)*log(w)*log(x) + log(z) + log(w)) == \ + log(z**log(w**2))*log(x) + log(w*z) + assert logcombine(3*log(w) + 3*log(z)) == log(w**3*z**3) + assert logcombine(x*(y + 1) + log(2) + log(3)) == x*(y + 1) + log(6) + assert logcombine((x + y)*log(w) + (-x - y)*log(3)) == (x + y)*log(w/3) + # a single unknown can combine + assert logcombine(log(x) + log(2)) == log(2*x) + eq = log(abs(x)) + log(abs(y)) + assert logcombine(eq) == eq + reps = {x: 0, y: 0} + assert log(abs(x)*abs(y)).subs(reps) != eq.subs(reps) + + +def test_logcombine_complex_coeff(): + i = Integral((sin(x**2) + cos(x**3))/x, x) + assert logcombine(i, force=True) == i + assert logcombine(i + 2*log(x), force=True) == \ + i + log(x**2) + + +def test_issue_5950(): + x, y = symbols("x,y", positive=True) + assert logcombine(log(3) - log(2)) == log(Rational(3,2), evaluate=False) + assert logcombine(log(x) - log(y)) == log(x/y) + assert logcombine(log(Rational(3,2), evaluate=False) - log(2)) == \ + log(Rational(3,4), evaluate=False) + + +def test_posify(): + x = symbols('x') + + assert str(posify( + x + + Symbol('p', positive=True) + + Symbol('n', negative=True))) == '(_x + n + p, {_x: x})' + + eq, rep = posify(1/x) + assert log(eq).expand().subs(rep) == -log(x) + assert str(posify([x, 1 + x])) == '([_x, _x + 1], {_x: x})' + + p = symbols('p', positive=True) + n = symbols('n', negative=True) + orig = [x, n, p] + modified, reps = posify(orig) + assert str(modified) == '[_x, n, p]' + assert [w.subs(reps) for w in modified] == orig + + assert str(Integral(posify(1/x + y)[0], (y, 1, 3)).expand()) == \ + 'Integral(1/_x, (y, 1, 3)) + Integral(_y, (y, 1, 3))' + assert str(Sum(posify(1/x**n)[0], (n,1,3)).expand()) == \ + 'Sum(_x**(-n), (n, 1, 3))' + + A = Matrix([[1, 2, 3], [4, 5, 6 * Abs(x)]]) + Ap, rep = posify(A) + assert Ap == A.subs(*reversed(rep.popitem())) + + # issue 16438 + k = Symbol('k', finite=True) + eq, rep = posify(k) + assert eq.assumptions0 == {'positive': True, 'zero': False, 'imaginary': False, + 'nonpositive': False, 'commutative': True, 'hermitian': True, 'real': True, 'nonzero': True, + 'nonnegative': True, 'negative': False, 'complex': True, 'finite': True, + 'infinite': False, 'extended_real':True, 'extended_negative': False, + 'extended_nonnegative': True, 'extended_nonpositive': False, + 'extended_nonzero': True, 'extended_positive': True} + + +def test_issue_4194(): + # simplify should call cancel + f = Function('f') + assert simplify((4*x + 6*f(y))/(2*x + 3*f(y))) == 2 + + +@XFAIL +def test_simplify_float_vs_integer(): + # Test for issue 4473: + # https://github.com/sympy/sympy/issues/4473 + assert simplify(x**2.0 - x**2) == 0 + assert simplify(x**2 - x**2.0) == 0 + + +def test_as_content_primitive(): + assert (x/2 + y).as_content_primitive() == (S.Half, x + 2*y) + assert (x/2 + y).as_content_primitive(clear=False) == (S.One, x/2 + y) + assert (y*(x/2 + y)).as_content_primitive() == (S.Half, y*(x + 2*y)) + assert (y*(x/2 + y)).as_content_primitive(clear=False) == (S.One, y*(x/2 + y)) + + # although the _as_content_primitive methods do not alter the underlying structure, + # the as_content_primitive function will touch up the expression and join + # bases that would otherwise have not been joined. + assert (x*(2 + 2*x)*(3*x + 3)**2).as_content_primitive() == \ + (18, x*(x + 1)**3) + assert (2 + 2*x + 2*y*(3 + 3*y)).as_content_primitive() == \ + (2, x + 3*y*(y + 1) + 1) + assert ((2 + 6*x)**2).as_content_primitive() == \ + (4, (3*x + 1)**2) + assert ((2 + 6*x)**(2*y)).as_content_primitive() == \ + (1, (_keep_coeff(S(2), (3*x + 1)))**(2*y)) + assert (5 + 10*x + 2*y*(3 + 3*y)).as_content_primitive() == \ + (1, 10*x + 6*y*(y + 1) + 5) + assert (5*(x*(1 + y)) + 2*x*(3 + 3*y)).as_content_primitive() == \ + (11, x*(y + 1)) + assert ((5*(x*(1 + y)) + 2*x*(3 + 3*y))**2).as_content_primitive() == \ + (121, x**2*(y + 1)**2) + assert (y**2).as_content_primitive() == \ + (1, y**2) + assert (S.Infinity).as_content_primitive() == (1, oo) + eq = x**(2 + y) + assert (eq).as_content_primitive() == (1, eq) + assert (S.Half**(2 + x)).as_content_primitive() == (Rational(1, 4), 2**-x) + assert (Rational(-1, 2)**(2 + x)).as_content_primitive() == \ + (Rational(1, 4), (Rational(-1, 2))**x) + assert (Rational(-1, 2)**(2 + x)).as_content_primitive() == \ + (Rational(1, 4), Rational(-1, 2)**x) + assert (4**((1 + y)/2)).as_content_primitive() == (2, 4**(y/2)) + assert (3**((1 + y)/2)).as_content_primitive() == \ + (1, 3**(Mul(S.Half, 1 + y, evaluate=False))) + assert (5**Rational(3, 4)).as_content_primitive() == (1, 5**Rational(3, 4)) + assert (5**Rational(7, 4)).as_content_primitive() == (5, 5**Rational(3, 4)) + assert Add(z*Rational(5, 7), 0.5*x, y*Rational(3, 2), evaluate=False).as_content_primitive() == \ + (Rational(1, 14), 7.0*x + 21*y + 10*z) + assert (2**Rational(3, 4) + 2**Rational(1, 4)*sqrt(3)).as_content_primitive(radical=True) == \ + (1, 2**Rational(1, 4)*(sqrt(2) + sqrt(3))) + + +def test_signsimp(): + e = x*(-x + 1) + x*(x - 1) + assert signsimp(Eq(e, 0)) is S.true + assert Abs(x - 1) == Abs(1 - x) + assert signsimp(y - x) == y - x + assert signsimp(y - x, evaluate=False) == Mul(-1, x - y, evaluate=False) + + +def test_besselsimp(): + from sympy.functions.special.bessel import (besseli, besselj, bessely) + from sympy.integrals.transforms import cosine_transform + assert besselsimp(exp(-I*pi*y/2)*besseli(y, z*exp_polar(I*pi/2))) == \ + besselj(y, z) + assert besselsimp(exp(-I*pi*a/2)*besseli(a, 2*sqrt(x)*exp_polar(I*pi/2))) == \ + besselj(a, 2*sqrt(x)) + assert besselsimp(sqrt(2)*sqrt(pi)*x**Rational(1, 4)*exp(I*pi/4)*exp(-I*pi*a/2) * + besseli(Rational(-1, 2), sqrt(x)*exp_polar(I*pi/2)) * + besseli(a, sqrt(x)*exp_polar(I*pi/2))/2) == \ + besselj(a, sqrt(x)) * cos(sqrt(x)) + assert besselsimp(besseli(Rational(-1, 2), z)) == \ + sqrt(2)*cosh(z)/(sqrt(pi)*sqrt(z)) + assert besselsimp(besseli(a, z*exp_polar(-I*pi/2))) == \ + exp(-I*pi*a/2)*besselj(a, z) + assert cosine_transform(1/t*sin(a/t), t, y) == \ + sqrt(2)*sqrt(pi)*besselj(0, 2*sqrt(a)*sqrt(y))/2 + + assert besselsimp(x**2*(a*(-2*besselj(5*I, x) + besselj(-2 + 5*I, x) + + besselj(2 + 5*I, x)) + b*(-2*bessely(5*I, x) + bessely(-2 + 5*I, x) + + bessely(2 + 5*I, x)))/4 + x*(a*(besselj(-1 + 5*I, x)/2 - besselj(1 + 5*I, x)/2) + + b*(bessely(-1 + 5*I, x)/2 - bessely(1 + 5*I, x)/2)) + (x**2 + 25)*(a*besselj(5*I, x) + + b*bessely(5*I, x))) == 0 + + assert besselsimp(81*x**2*(a*(besselj(Rational(-5, 3), 9*x) - 2*besselj(Rational(1, 3), 9*x) + besselj(Rational(7, 3), 9*x)) + + b*(bessely(Rational(-5, 3), 9*x) - 2*bessely(Rational(1, 3), 9*x) + bessely(Rational(7, 3), 9*x)))/4 + x*(a*(9*besselj(Rational(-2, 3), 9*x)/2 + - 9*besselj(Rational(4, 3), 9*x)/2) + b*(9*bessely(Rational(-2, 3), 9*x)/2 - 9*bessely(Rational(4, 3), 9*x)/2)) + + (81*x**2 - Rational(1, 9))*(a*besselj(Rational(1, 3), 9*x) + b*bessely(Rational(1, 3), 9*x))) == 0 + + assert besselsimp(besselj(a-1,x) + besselj(a+1, x) - 2*a*besselj(a, x)/x) == 0 + + assert besselsimp(besselj(a-1,x) + besselj(a+1, x) + besselj(a, x)) == (2*a + x)*besselj(a, x)/x + + assert besselsimp(x**2* besselj(a,x) + x**3*besselj(a+1, x) + besselj(a+2, x)) == \ + 2*a*x*besselj(a + 1, x) + x**3*besselj(a + 1, x) - x**2*besselj(a + 2, x) + 2*x*besselj(a + 1, x) + besselj(a + 2, x) + +def test_Piecewise(): + e1 = x*(x + y) - y*(x + y) + e2 = sin(x)**2 + cos(x)**2 + e3 = expand((x + y)*y/x) + s1 = simplify(e1) + s2 = simplify(e2) + s3 = simplify(e3) + assert simplify(Piecewise((e1, x < e2), (e3, True))) == \ + Piecewise((s1, x < s2), (s3, True)) + + +def test_polymorphism(): + class A(Basic): + def _eval_simplify(x, **kwargs): + return S.One + + a = A(S(5), S(2)) + assert simplify(a) == 1 + + +def test_issue_from_PR1599(): + n1, n2, n3, n4 = symbols('n1 n2 n3 n4', negative=True) + assert simplify(I*sqrt(n1)) == -sqrt(-n1) + + +def test_issue_6811(): + eq = (x + 2*y)*(2*x + 2) + assert simplify(eq) == (x + 1)*(x + 2*y)*2 + # reject the 2-arg Mul -- these are a headache for test writing + assert simplify(eq.expand()) == \ + 2*x**2 + 4*x*y + 2*x + 4*y + + +def test_issue_6920(): + e = [cos(x) + I*sin(x), cos(x) - I*sin(x), + cosh(x) - sinh(x), cosh(x) + sinh(x)] + ok = [exp(I*x), exp(-I*x), exp(-x), exp(x)] + # wrap in f to show that the change happens wherever ei occurs + f = Function('f') + assert [simplify(f(ei)).args[0] for ei in e] == ok + + +def test_issue_7001(): + from sympy.abc import r, R + assert simplify(-(r*Piecewise((pi*Rational(4, 3), r <= R), + (-8*pi*R**3/(3*r**3), True)) + 2*Piecewise((pi*r*Rational(4, 3), r <= R), + (4*pi*R**3/(3*r**2), True)))/(4*pi*r)) == \ + Piecewise((-1, r <= R), (0, True)) + + +def test_inequality_no_auto_simplify(): + # no simplify on creation but can be simplified + lhs = cos(x)**2 + sin(x)**2 + rhs = 2 + e = Lt(lhs, rhs, evaluate=False) + assert e is not S.true + assert simplify(e) + + +def test_issue_9398(): + from sympy.core.numbers import Number + from sympy.polys.polytools import cancel + assert cancel(1e-14) != 0 + assert cancel(1e-14*I) != 0 + + assert simplify(1e-14) != 0 + assert simplify(1e-14*I) != 0 + + assert (I*Number(1.)*Number(10)**Number(-14)).simplify() != 0 + + assert cancel(1e-20) != 0 + assert cancel(1e-20*I) != 0 + + assert simplify(1e-20) != 0 + assert simplify(1e-20*I) != 0 + + assert cancel(1e-100) != 0 + assert cancel(1e-100*I) != 0 + + assert simplify(1e-100) != 0 + assert simplify(1e-100*I) != 0 + + f = Float("1e-1000") + assert cancel(f) != 0 + assert cancel(f*I) != 0 + + assert simplify(f) != 0 + assert simplify(f*I) != 0 + + +def test_issue_9324_simplify(): + M = MatrixSymbol('M', 10, 10) + e = M[0, 0] + M[5, 4] + 1304 + assert simplify(e) == e + + +def test_issue_9817_simplify(): + # simplify on trace of substituted explicit quadratic form of matrix + # expressions (a scalar) should return without errors (AttributeError) + # See issue #9817 and #9190 for the original bug more discussion on this + from sympy.matrices.expressions import Identity, trace + v = MatrixSymbol('v', 3, 1) + A = MatrixSymbol('A', 3, 3) + x = Matrix([i + 1 for i in range(3)]) + X = Identity(3) + quadratic = v.T * A * v + assert simplify((trace(quadratic.as_explicit())).xreplace({v:x, A:X})) == 14 + + +def test_issue_13474(): + x = Symbol('x') + assert simplify(x + csch(sinc(1))) == x + csch(sinc(1)) + + +@_both_exp_pow +def test_simplify_function_inverse(): + # "inverse" attribute does not guarantee that f(g(x)) is x + # so this simplification should not happen automatically. + # See issue #12140 + x, y = symbols('x, y') + g = Function('g') + + class f(Function): + def inverse(self, argindex=1): + return g + + assert simplify(f(g(x))) == f(g(x)) + assert inversecombine(f(g(x))) == x + assert simplify(f(g(x)), inverse=True) == x + assert simplify(f(g(sin(x)**2 + cos(x)**2)), inverse=True) == 1 + assert simplify(f(g(x, y)), inverse=True) == f(g(x, y)) + assert unchanged(asin, sin(x)) + assert simplify(asin(sin(x))) == asin(sin(x)) + assert simplify(2*asin(sin(3*x)), inverse=True) == 6*x + assert simplify(log(exp(x))) == log(exp(x)) + assert simplify(log(exp(x)), inverse=True) == x + assert simplify(exp(log(x)), inverse=True) == x + assert simplify(log(exp(x), 2), inverse=True) == x/log(2) + assert simplify(log(exp(x), 2, evaluate=False), inverse=True) == x/log(2) + + +def test_clear_coefficients(): + from sympy.simplify.simplify import clear_coefficients + assert clear_coefficients(4*y*(6*x + 3)) == (y*(2*x + 1), 0) + assert clear_coefficients(4*y*(6*x + 3) - 2) == (y*(2*x + 1), Rational(1, 6)) + assert clear_coefficients(4*y*(6*x + 3) - 2, x) == (y*(2*x + 1), x/12 + Rational(1, 6)) + assert clear_coefficients(sqrt(2) - 2) == (sqrt(2), 2) + assert clear_coefficients(4*sqrt(2) - 2) == (sqrt(2), S.Half) + assert clear_coefficients(S(3), x) == (0, x - 3) + assert clear_coefficients(S.Infinity, x) == (S.Infinity, x) + assert clear_coefficients(-S.Pi, x) == (S.Pi, -x) + assert clear_coefficients(2 - S.Pi/3, x) == (pi, -3*x + 6) + +def test_nc_simplify(): + from sympy.simplify.simplify import nc_simplify + from sympy.matrices.expressions import MatPow, Identity + from sympy.core import Pow + from functools import reduce + + a, b, c, d = symbols('a b c d', commutative = False) + x = Symbol('x') + A = MatrixSymbol("A", x, x) + B = MatrixSymbol("B", x, x) + C = MatrixSymbol("C", x, x) + D = MatrixSymbol("D", x, x) + subst = {a: A, b: B, c: C, d:D} + funcs = {Add: lambda x,y: x+y, Mul: lambda x,y: x*y } + + def _to_matrix(expr): + if expr in subst: + return subst[expr] + if isinstance(expr, Pow): + return MatPow(_to_matrix(expr.args[0]), expr.args[1]) + elif isinstance(expr, (Add, Mul)): + return reduce(funcs[expr.func],[_to_matrix(a) for a in expr.args]) + else: + return expr*Identity(x) + + def _check(expr, simplified, deep=True, matrix=True): + assert nc_simplify(expr, deep=deep) == simplified + assert expand(expr) == expand(simplified) + if matrix: + m_simp = _to_matrix(simplified).doit(inv_expand=False) + assert nc_simplify(_to_matrix(expr), deep=deep) == m_simp + + _check(a*b*a*b*a*b*c*(a*b)**3*c, ((a*b)**3*c)**2) + _check(a*b*(a*b)**-2*a*b, 1) + _check(a**2*b*a*b*a*b*(a*b)**-1, a*(a*b)**2, matrix=False) + _check(b*a*b**2*a*b**2*a*b**2, b*(a*b**2)**3) + _check(a*b*a**2*b*a**2*b*a**3, (a*b*a)**3*a**2) + _check(a**2*b*a**4*b*a**4*b*a**2, (a**2*b*a**2)**3) + _check(a**3*b*a**4*b*a**4*b*a, a**3*(b*a**4)**3*a**-3) + _check(a*b*a*b + a*b*c*x*a*b*c, (a*b)**2 + x*(a*b*c)**2) + _check(a*b*a*b*c*a*b*a*b*c, ((a*b)**2*c)**2) + _check(b**-1*a**-1*(a*b)**2, a*b) + _check(a**-1*b*c**-1, (c*b**-1*a)**-1) + expr = a**3*b*a**4*b*a**4*b*a**2*b*a**2*(b*a**2)**2*b*a**2*b*a**2 + for _ in range(10): + expr *= a*b + _check(expr, a**3*(b*a**4)**2*(b*a**2)**6*(a*b)**10) + _check((a*b*a*b)**2, (a*b*a*b)**2, deep=False) + _check(a*b*(c*d)**2, a*b*(c*d)**2) + expr = b**-1*(a**-1*b**-1 - a**-1*c*b**-1)**-1*a**-1 + assert nc_simplify(expr) == (1-c)**-1 + # commutative expressions should be returned without an error + assert nc_simplify(2*x**2) == 2*x**2 + +def test_issue_15965(): + A = Sum(z*x**y, (x, 1, a)) + anew = z*Sum(x**y, (x, 1, a)) + B = Integral(x*y, x) + bdo = x**2*y/2 + assert simplify(A + B) == anew + bdo + assert simplify(A) == anew + assert simplify(B) == bdo + assert simplify(B, doit=False) == y*Integral(x, x) + + +def test_issue_17137(): + assert simplify(cos(x)**I) == cos(x)**I + assert simplify(cos(x)**(2 + 3*I)) == cos(x)**(2 + 3*I) + + +def test_issue_21869(): + x = Symbol('x', real=True) + y = Symbol('y', real=True) + expr = And(Eq(x**2, 4), Le(x, y)) + assert expr.simplify() == expr + + expr = And(Eq(x**2, 4), Eq(x, 2)) + assert expr.simplify() == Eq(x, 2) + + expr = And(Eq(x**3, x**2), Eq(x, 1)) + assert expr.simplify() == Eq(x, 1) + + expr = And(Eq(sin(x), x**2), Eq(x, 0)) + assert expr.simplify() == Eq(x, 0) + + expr = And(Eq(x**3, x**2), Eq(x, 2)) + assert expr.simplify() == S.false + + expr = And(Eq(y, x**2), Eq(x, 1)) + assert expr.simplify() == And(Eq(y,1), Eq(x, 1)) + + expr = And(Eq(y**2, 1), Eq(y, x**2), Eq(x, 1)) + assert expr.simplify() == And(Eq(y,1), Eq(x, 1)) + + expr = And(Eq(y**2, 4), Eq(y, 2*x**2), Eq(x, 1)) + assert expr.simplify() == And(Eq(y,2), Eq(x, 1)) + + expr = And(Eq(y**2, 4), Eq(y, x**2), Eq(x, 1)) + assert expr.simplify() == S.false + + +def test_issue_7971_21740(): + z = Integral(x, (x, 1, 1)) + assert z != 0 + assert simplify(z) is S.Zero + assert simplify(S.Zero) is S.Zero + z = simplify(Float(0)) + assert z is not S.Zero and z == 0.0 + + +@slow +def test_issue_17141_slow(): + # Should not give RecursionError + assert simplify((2**acos(I+1)**2).rewrite('log')) == 2**((pi + 2*I*log(-1 + + sqrt(1 - 2*I) + I))**2/4) + + +def test_issue_17141(): + # Check that there is no RecursionError + assert simplify(x**(1 / acos(I))) == x**(2/(pi - 2*I*log(1 + sqrt(2)))) + assert simplify(acos(-I)**2*acos(I)**2) == \ + log(1 + sqrt(2))**4 + pi**2*log(1 + sqrt(2))**2/2 + pi**4/16 + assert simplify(2**acos(I)**2) == 2**((pi - 2*I*log(1 + sqrt(2)))**2/4) + p = 2**acos(I+1)**2 + assert simplify(p) == p + + +def test_simplify_kroneckerdelta(): + i, j = symbols("i j") + K = KroneckerDelta + + assert simplify(K(i, j)) == K(i, j) + assert simplify(K(0, j)) == K(0, j) + assert simplify(K(i, 0)) == K(i, 0) + + assert simplify(K(0, j).rewrite(Piecewise) * K(1, j)) == 0 + assert simplify(K(1, i) + Piecewise((1, Eq(j, 2)), (0, True))) == K(1, i) + K(2, j) + + # issue 17214 + assert simplify(K(0, j) * K(1, j)) == 0 + + n = Symbol('n', integer=True) + assert simplify(K(0, n) * K(1, n)) == 0 + + M = Matrix(4, 4, lambda i, j: K(j - i, n) if i <= j else 0) + assert simplify(M**2) == Matrix([[K(0, n), 0, K(1, n), 0], + [0, K(0, n), 0, K(1, n)], + [0, 0, K(0, n), 0], + [0, 0, 0, K(0, n)]]) + assert simplify(eye(1) * KroneckerDelta(0, n) * + KroneckerDelta(1, n)) == Matrix([[0]]) + + assert simplify(S.Infinity * KroneckerDelta(0, n) * + KroneckerDelta(1, n)) is S.NaN + + +def test_issue_17292(): + assert simplify(abs(x)/abs(x**2)) == 1/abs(x) + # this is bigger than the issue: check that deep processing works + assert simplify(5*abs((x**2 - 1)/(x - 1))) == 5*Abs(x + 1) + + +def test_issue_19822(): + expr = And(Gt(n-2, 1), Gt(n, 1)) + assert simplify(expr) == Gt(n, 3) + + +def test_issue_18645(): + expr = And(Ge(x, 3), Le(x, 3)) + assert simplify(expr) == Eq(x, 3) + expr = And(Eq(x, 3), Le(x, 3)) + assert simplify(expr) == Eq(x, 3) + + +@XFAIL +def test_issue_18642(): + i = Symbol("i", integer=True) + n = Symbol("n", integer=True) + expr = And(Eq(i, 2 * n), Le(i, 2*n -1)) + assert simplify(expr) == S.false + + +@XFAIL +def test_issue_18389(): + n = Symbol("n", integer=True) + expr = Eq(n, 0) | (n >= 1) + assert simplify(expr) == Ge(n, 0) + + +def test_issue_8373(): + x = Symbol('x', real=True) + assert simplify(Or(x < 1, x >= 1)) == S.true + + +def test_issue_7950(): + expr = And(Eq(x, 1), Eq(x, 2)) + assert simplify(expr) == S.false + + +def test_issue_22020(): + expr = I*pi/2 -oo + assert simplify(expr) == expr + # Used to throw an error + + +def test_issue_19484(): + assert simplify(sign(x) * Abs(x)) == x + + e = x + sign(x + x**3) + assert simplify(Abs(x + x**3)*e) == x**3 + x*Abs(x**3 + x) + x + + e = x**2 + sign(x**3 + 1) + assert simplify(Abs(x**3 + 1) * e) == x**3 + x**2*Abs(x**3 + 1) + 1 + + f = Function('f') + e = x + sign(x + f(x)**3) + assert simplify(Abs(x + f(x)**3) * e) == x*Abs(x + f(x)**3) + x + f(x)**3 + + +def test_issue_23543(): + # Used to give an error + x, y, z = symbols("x y z", commutative=False) + assert (x*(y + z/2)).simplify() == x*(2*y + z)/2 + + +def test_issue_11004(): + + def f(n): + return sqrt(2*pi*n) * (n/E)**n + + def m(n, k): + return f(n) / (f(n/k)**k) + + def p(n,k): + return m(n, k) / (k**n) + + N, k = symbols('N k') + half = Float('0.5', 4) + z = log(p(n, k) / p(n, k + 1)).expand(force=True) + r = simplify(z.subs(n, N).n(4)) + assert r == ( + half*k*log(k) + - half*k*log(k + 1) + + half*log(N) + - half*log(k + 1) + + Float(0.9189224, 4) + ) + + +def test_issue_19161(): + polynomial = Poly('x**2').simplify() + assert (polynomial-x**2).simplify() == 0 + + +def test_issue_22210(): + d = Symbol('d', integer=True) + expr = 2*Derivative(sin(x), (x, d)) + assert expr.simplify() == expr + + +def test_reduce_inverses_nc_pow(): + x, y = symbols("x y", commutative=True) + Z = symbols("Z", commutative=False) + assert simplify(2**Z * y**Z) == 2**Z * y**Z + assert simplify(x**Z * y**Z) == x**Z * y**Z + x, y = symbols("x y", positive=True) + assert expand((x*y)**Z) == x**Z * y**Z + assert simplify(x**Z * y**Z) == expand((x*y)**Z) + +def test_nc_recursion_coeff(): + X = symbols("X", commutative = False) + assert (2 * cos(pi/3) * X).simplify() == X + assert (2.0 * cos(pi/3) * X).simplify() == X diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_sqrtdenest.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_sqrtdenest.py new file mode 100644 index 0000000000000000000000000000000000000000..41c771bb2055a1199d349ae3649f33927d79313a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_sqrtdenest.py @@ -0,0 +1,204 @@ +from sympy.core.mul import Mul +from sympy.core.numbers import (I, Integer, Rational) +from sympy.core.symbol import Symbol +from sympy.functions.elementary.miscellaneous import (root, sqrt) +from sympy.functions.elementary.trigonometric import cos +from sympy.integrals.integrals import Integral +from sympy.simplify.sqrtdenest import sqrtdenest +from sympy.simplify.sqrtdenest import ( + _subsets as subsets, _sqrt_numeric_denest) + +r2, r3, r5, r6, r7, r10, r15, r29 = [sqrt(x) for x in (2, 3, 5, 6, 7, 10, + 15, 29)] + + +def test_sqrtdenest(): + d = {sqrt(5 + 2 * r6): r2 + r3, + sqrt(5. + 2 * r6): sqrt(5. + 2 * r6), + sqrt(5. + 4*sqrt(5 + 2 * r6)): sqrt(5.0 + 4*r2 + 4*r3), + sqrt(r2): sqrt(r2), + sqrt(5 + r7): sqrt(5 + r7), + sqrt(3 + sqrt(5 + 2*r7)): + 3*r2*(5 + 2*r7)**Rational(1, 4)/(2*sqrt(6 + 3*r7)) + + r2*sqrt(6 + 3*r7)/(2*(5 + 2*r7)**Rational(1, 4)), + sqrt(3 + 2*r3): 3**Rational(3, 4)*(r6/2 + 3*r2/2)/3} + for i in d: + assert sqrtdenest(i) == d[i], i + + +def test_sqrtdenest2(): + assert sqrtdenest(sqrt(16 - 2*r29 + 2*sqrt(55 - 10*r29))) == \ + r5 + sqrt(11 - 2*r29) + e = sqrt(-r5 + sqrt(-2*r29 + 2*sqrt(-10*r29 + 55) + 16)) + assert sqrtdenest(e) == root(-2*r29 + 11, 4) + r = sqrt(1 + r7) + assert sqrtdenest(sqrt(1 + r)) == sqrt(1 + r) + e = sqrt(((1 + sqrt(1 + 2*sqrt(3 + r2 + r5)))**2).expand()) + assert sqrtdenest(e) == 1 + sqrt(1 + 2*sqrt(r2 + r5 + 3)) + + assert sqrtdenest(sqrt(5*r3 + 6*r2)) == \ + sqrt(2)*root(3, 4) + root(3, 4)**3 + + assert sqrtdenest(sqrt(((1 + r5 + sqrt(1 + r3))**2).expand())) == \ + 1 + r5 + sqrt(1 + r3) + + assert sqrtdenest(sqrt(((1 + r5 + r7 + sqrt(1 + r3))**2).expand())) == \ + 1 + sqrt(1 + r3) + r5 + r7 + + e = sqrt(((1 + cos(2) + cos(3) + sqrt(1 + r3))**2).expand()) + assert sqrtdenest(e) == cos(3) + cos(2) + 1 + sqrt(1 + r3) + + e = sqrt(-2*r10 + 2*r2*sqrt(-2*r10 + 11) + 14) + assert sqrtdenest(e) == sqrt(-2*r10 - 2*r2 + 4*r5 + 14) + + # check that the result is not more complicated than the input + z = sqrt(-2*r29 + cos(2) + 2*sqrt(-10*r29 + 55) + 16) + assert sqrtdenest(z) == z + + assert sqrtdenest(sqrt(r6 + sqrt(15))) == sqrt(r6 + sqrt(15)) + + z = sqrt(15 - 2*sqrt(31) + 2*sqrt(55 - 10*r29)) + assert sqrtdenest(z) == z + + +def test_sqrtdenest_rec(): + assert sqrtdenest(sqrt(-4*sqrt(14) - 2*r6 + 4*sqrt(21) + 33)) == \ + -r2 + r3 + 2*r7 + assert sqrtdenest(sqrt(-28*r7 - 14*r5 + 4*sqrt(35) + 82)) == \ + -7 + r5 + 2*r7 + assert sqrtdenest(sqrt(6*r2/11 + 2*sqrt(22)/11 + 6*sqrt(11)/11 + 2)) == \ + sqrt(11)*(r2 + 3 + sqrt(11))/11 + assert sqrtdenest(sqrt(468*r3 + 3024*r2 + 2912*r6 + 19735)) == \ + 9*r3 + 26 + 56*r6 + z = sqrt(-490*r3 - 98*sqrt(115) - 98*sqrt(345) - 2107) + assert sqrtdenest(z) == sqrt(-1)*(7*r5 + 7*r15 + 7*sqrt(23)) + z = sqrt(-4*sqrt(14) - 2*r6 + 4*sqrt(21) + 34) + assert sqrtdenest(z) == z + assert sqrtdenest(sqrt(-8*r2 - 2*r5 + 18)) == -r10 + 1 + r2 + r5 + assert sqrtdenest(sqrt(8*r2 + 2*r5 - 18)) == \ + sqrt(-1)*(-r10 + 1 + r2 + r5) + assert sqrtdenest(sqrt(8*r2/3 + 14*r5/3 + Rational(154, 9))) == \ + -r10/3 + r2 + r5 + 3 + assert sqrtdenest(sqrt(sqrt(2*r6 + 5) + sqrt(2*r7 + 8))) == \ + sqrt(1 + r2 + r3 + r7) + assert sqrtdenest(sqrt(4*r15 + 8*r5 + 12*r3 + 24)) == 1 + r3 + r5 + r15 + + w = 1 + r2 + r3 + r5 + r7 + assert sqrtdenest(sqrt((w**2).expand())) == w + z = sqrt((w**2).expand() + 1) + assert sqrtdenest(z) == z + + z = sqrt(2*r10 + 6*r2 + 4*r5 + 12 + 10*r15 + 30*r3) + assert sqrtdenest(z) == z + + +def test_issue_6241(): + z = sqrt( -320 + 32*sqrt(5) + 64*r15) + assert sqrtdenest(z) == z + + +def test_sqrtdenest3(): + z = sqrt(13 - 2*r10 + 2*r2*sqrt(-2*r10 + 11)) + assert sqrtdenest(z) == -1 + r2 + r10 + assert sqrtdenest(z, max_iter=1) == -1 + sqrt(2) + sqrt(10) + z = sqrt(sqrt(r2 + 2) + 2) + assert sqrtdenest(z) == z + assert sqrtdenest(sqrt(-2*r10 + 4*r2*sqrt(-2*r10 + 11) + 20)) == \ + sqrt(-2*r10 - 4*r2 + 8*r5 + 20) + assert sqrtdenest(sqrt((112 + 70*r2) + (46 + 34*r2)*r5)) == \ + r10 + 5 + 4*r2 + 3*r5 + z = sqrt(5 + sqrt(2*r6 + 5)*sqrt(-2*r29 + 2*sqrt(-10*r29 + 55) + 16)) + r = sqrt(-2*r29 + 11) + assert sqrtdenest(z) == sqrt(r2*r + r3*r + r10 + r15 + 5) + + n = sqrt(2*r6/7 + 2*r7/7 + 2*sqrt(42)/7 + 2) + d = sqrt(16 - 2*r29 + 2*sqrt(55 - 10*r29)) + assert sqrtdenest(n/d) == r7*(1 + r6 + r7)/(Mul(7, (sqrt(-2*r29 + 11) + r5), + evaluate=False)) + + +def test_sqrtdenest4(): + # see Denest_en.pdf in https://github.com/sympy/sympy/issues/3192 + z = sqrt(8 - r2*sqrt(5 - r5) - sqrt(3)*(1 + r5)) + z1 = sqrtdenest(z) + c = sqrt(-r5 + 5) + z1 = ((-r15*c - r3*c + c + r5*c - r6 - r2 + r10 + sqrt(30))/4).expand() + assert sqrtdenest(z) == z1 + + z = sqrt(2*r2*sqrt(r2 + 2) + 5*r2 + 4*sqrt(r2 + 2) + 8) + assert sqrtdenest(z) == r2 + sqrt(r2 + 2) + 2 + + w = 2 + r2 + r3 + (1 + r3)*sqrt(2 + r2 + 5*r3) + z = sqrt((w**2).expand()) + assert sqrtdenest(z) == w.expand() + + +def test_sqrt_symbolic_denest(): + x = Symbol('x') + z = sqrt(((1 + sqrt(sqrt(2 + x) + 3))**2).expand()) + assert sqrtdenest(z) == sqrt((1 + sqrt(sqrt(2 + x) + 3))**2) + z = sqrt(((1 + sqrt(sqrt(2 + cos(1)) + 3))**2).expand()) + assert sqrtdenest(z) == 1 + sqrt(sqrt(2 + cos(1)) + 3) + z = ((1 + cos(2))**4 + 1).expand() + assert sqrtdenest(z) == z + z = sqrt(((1 + sqrt(sqrt(2 + cos(3*x)) + 3))**2 + 1).expand()) + assert sqrtdenest(z) == z + c = cos(3) + c2 = c**2 + assert sqrtdenest(sqrt(2*sqrt(1 + r3)*c + c2 + 1 + r3*c2)) == \ + -1 - sqrt(1 + r3)*c + ra = sqrt(1 + r3) + z = sqrt(20*ra*sqrt(3 + 3*r3) + 12*r3*ra*sqrt(3 + 3*r3) + 64*r3 + 112) + assert sqrtdenest(z) == z + + +def test_issue_5857(): + from sympy.abc import x, y + z = sqrt(1/(4*r3 + 7) + 1) + ans = (r2 + r6)/(r3 + 2) + assert sqrtdenest(z) == ans + assert sqrtdenest(1 + z) == 1 + ans + assert sqrtdenest(Integral(z + 1, (x, 1, 2))) == \ + Integral(1 + ans, (x, 1, 2)) + assert sqrtdenest(x + sqrt(y)) == x + sqrt(y) + ans = (r2 + r6)/(r3 + 2) + assert sqrtdenest(z) == ans + assert sqrtdenest(1 + z) == 1 + ans + assert sqrtdenest(Integral(z + 1, (x, 1, 2))) == \ + Integral(1 + ans, (x, 1, 2)) + assert sqrtdenest(x + sqrt(y)) == x + sqrt(y) + + +def test_subsets(): + assert subsets(1) == [[1]] + assert subsets(4) == [ + [1, 0, 0, 0], [0, 1, 0, 0], [1, 1, 0, 0], [0, 0, 1, 0], [1, 0, 1, 0], + [0, 1, 1, 0], [1, 1, 1, 0], [0, 0, 0, 1], [1, 0, 0, 1], [0, 1, 0, 1], + [1, 1, 0, 1], [0, 0, 1, 1], [1, 0, 1, 1], [0, 1, 1, 1], [1, 1, 1, 1]] + + +def test_issue_5653(): + assert sqrtdenest( + sqrt(2 + sqrt(2 + sqrt(2)))) == sqrt(2 + sqrt(2 + sqrt(2))) + +def test_issue_12420(): + assert sqrtdenest((3 - sqrt(2)*sqrt(4 + 3*I) + 3*I)/2) == I + e = 3 - sqrt(2)*sqrt(4 + I) + 3*I + assert sqrtdenest(e) == e + +def test_sqrt_ratcomb(): + assert sqrtdenest(sqrt(1 + r3) + sqrt(3 + 3*r3) - sqrt(10 + 6*r3)) == 0 + +def test_issue_18041(): + e = -sqrt(-2 + 2*sqrt(3)*I) + assert sqrtdenest(e) == -1 - sqrt(3)*I + +def test_issue_19914(): + a = Integer(-8) + b = Integer(-1) + r = Integer(63) + d2 = a*a - b*b*r + + assert _sqrt_numeric_denest(a, b, r, d2) == \ + sqrt(14)*I/2 + 3*sqrt(2)*I/2 + assert sqrtdenest(sqrt(-8-sqrt(63))) == sqrt(14)*I/2 + 3*sqrt(2)*I/2 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_trigsimp.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_trigsimp.py new file mode 100644 index 0000000000000000000000000000000000000000..ea091ec8a6c7d654405968e3d035c2bbe02ccdf7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/tests/test_trigsimp.py @@ -0,0 +1,520 @@ +from itertools import product +from sympy.core.function import (Subs, count_ops, diff, expand) +from sympy.core.numbers import (E, I, Rational, pi) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.hyperbolic import (cosh, coth, sinh, tanh) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (cos, cot, sin, tan) +from sympy.functions.elementary.trigonometric import (acos, asin, atan2) +from sympy.functions.elementary.trigonometric import (asec, acsc) +from sympy.functions.elementary.trigonometric import (acot, atan) +from sympy.integrals.integrals import integrate +from sympy.matrices.dense import Matrix +from sympy.simplify.simplify import simplify +from sympy.simplify.trigsimp import (exptrigsimp, trigsimp) + +from sympy.testing.pytest import XFAIL + +from sympy.abc import x, y + + + +def test_trigsimp1(): + x, y = symbols('x,y') + + assert trigsimp(1 - sin(x)**2) == cos(x)**2 + assert trigsimp(1 - cos(x)**2) == sin(x)**2 + assert trigsimp(sin(x)**2 + cos(x)**2) == 1 + assert trigsimp(1 + tan(x)**2) == 1/cos(x)**2 + assert trigsimp(1/cos(x)**2 - 1) == tan(x)**2 + assert trigsimp(1/cos(x)**2 - tan(x)**2) == 1 + assert trigsimp(1 + cot(x)**2) == 1/sin(x)**2 + assert trigsimp(1/sin(x)**2 - 1) == 1/tan(x)**2 + assert trigsimp(1/sin(x)**2 - cot(x)**2) == 1 + + assert trigsimp(5*cos(x)**2 + 5*sin(x)**2) == 5 + assert trigsimp(5*cos(x/2)**2 + 2*sin(x/2)**2) == 3*cos(x)/2 + Rational(7, 2) + + assert trigsimp(sin(x)/cos(x)) == tan(x) + assert trigsimp(2*tan(x)*cos(x)) == 2*sin(x) + assert trigsimp(cot(x)**3*sin(x)**3) == cos(x)**3 + assert trigsimp(y*tan(x)**2/sin(x)**2) == y/cos(x)**2 + assert trigsimp(cot(x)/cos(x)) == 1/sin(x) + + assert trigsimp(sin(x + y) + sin(x - y)) == 2*sin(x)*cos(y) + assert trigsimp(sin(x + y) - sin(x - y)) == 2*sin(y)*cos(x) + assert trigsimp(cos(x + y) + cos(x - y)) == 2*cos(x)*cos(y) + assert trigsimp(cos(x + y) - cos(x - y)) == -2*sin(x)*sin(y) + assert trigsimp(tan(x + y) - tan(x)/(1 - tan(x)*tan(y))) == \ + sin(y)/(-sin(y)*tan(x) + cos(y)) # -tan(y)/(tan(x)*tan(y) - 1) + + assert trigsimp(sinh(x + y) + sinh(x - y)) == 2*sinh(x)*cosh(y) + assert trigsimp(sinh(x + y) - sinh(x - y)) == 2*sinh(y)*cosh(x) + assert trigsimp(cosh(x + y) + cosh(x - y)) == 2*cosh(x)*cosh(y) + assert trigsimp(cosh(x + y) - cosh(x - y)) == 2*sinh(x)*sinh(y) + assert trigsimp(tanh(x + y) - tanh(x)/(1 + tanh(x)*tanh(y))) == \ + sinh(y)/(sinh(y)*tanh(x) + cosh(y)) + + assert trigsimp(cos(0.12345)**2 + sin(0.12345)**2) == 1.0 + e = 2*sin(x)**2 + 2*cos(x)**2 + assert trigsimp(log(e)) == log(2) + + +def test_trigsimp1a(): + assert trigsimp(sin(2)**2*cos(3)*exp(2)/cos(2)**2) == tan(2)**2*cos(3)*exp(2) + assert trigsimp(tan(2)**2*cos(3)*exp(2)*cos(2)**2) == sin(2)**2*cos(3)*exp(2) + assert trigsimp(cot(2)*cos(3)*exp(2)*sin(2)) == cos(3)*exp(2)*cos(2) + assert trigsimp(tan(2)*cos(3)*exp(2)/sin(2)) == cos(3)*exp(2)/cos(2) + assert trigsimp(cot(2)*cos(3)*exp(2)/cos(2)) == cos(3)*exp(2)/sin(2) + assert trigsimp(cot(2)*cos(3)*exp(2)*tan(2)) == cos(3)*exp(2) + assert trigsimp(sinh(2)*cos(3)*exp(2)/cosh(2)) == tanh(2)*cos(3)*exp(2) + assert trigsimp(tanh(2)*cos(3)*exp(2)*cosh(2)) == sinh(2)*cos(3)*exp(2) + assert trigsimp(coth(2)*cos(3)*exp(2)*sinh(2)) == cosh(2)*cos(3)*exp(2) + assert trigsimp(tanh(2)*cos(3)*exp(2)/sinh(2)) == cos(3)*exp(2)/cosh(2) + assert trigsimp(coth(2)*cos(3)*exp(2)/cosh(2)) == cos(3)*exp(2)/sinh(2) + assert trigsimp(coth(2)*cos(3)*exp(2)*tanh(2)) == cos(3)*exp(2) + + +def test_trigsimp2(): + x, y = symbols('x,y') + assert trigsimp(cos(x)**2*sin(y)**2 + cos(x)**2*cos(y)**2 + sin(x)**2, + recursive=True) == 1 + assert trigsimp(sin(x)**2*sin(y)**2 + sin(x)**2*cos(y)**2 + cos(x)**2, + recursive=True) == 1 + assert trigsimp( + Subs(x, x, sin(y)**2 + cos(y)**2)) == Subs(x, x, 1) + + +def test_issue_4373(): + x = Symbol("x") + assert abs(trigsimp(2.0*sin(x)**2 + 2.0*cos(x)**2) - 2.0) < 1e-10 + + +def test_trigsimp3(): + x, y = symbols('x,y') + assert trigsimp(sin(x)/cos(x)) == tan(x) + assert trigsimp(sin(x)**2/cos(x)**2) == tan(x)**2 + assert trigsimp(sin(x)**3/cos(x)**3) == tan(x)**3 + assert trigsimp(sin(x)**10/cos(x)**10) == tan(x)**10 + + assert trigsimp(cos(x)/sin(x)) == 1/tan(x) + assert trigsimp(cos(x)**2/sin(x)**2) == 1/tan(x)**2 + assert trigsimp(cos(x)**10/sin(x)**10) == 1/tan(x)**10 + + assert trigsimp(tan(x)) == trigsimp(sin(x)/cos(x)) + + +def test_issue_4661(): + a, x, y = symbols('a x y') + eq = -4*sin(x)**4 + 4*cos(x)**4 - 8*cos(x)**2 + assert trigsimp(eq) == -4 + n = sin(x)**6 + 4*sin(x)**4*cos(x)**2 + 5*sin(x)**2*cos(x)**4 + 2*cos(x)**6 + d = -sin(x)**2 - 2*cos(x)**2 + assert simplify(n/d) == -1 + assert trigsimp(-2*cos(x)**2 + cos(x)**4 - sin(x)**4) == -1 + eq = (- sin(x)**3/4)*cos(x) + (cos(x)**3/4)*sin(x) - sin(2*x)*cos(2*x)/8 + assert trigsimp(eq) == 0 + + +def test_issue_4494(): + a, b = symbols('a b') + eq = sin(a)**2*sin(b)**2 + cos(a)**2*cos(b)**2*tan(a)**2 + cos(a)**2 + assert trigsimp(eq) == 1 + + +def test_issue_5948(): + a, x, y = symbols('a x y') + assert trigsimp(diff(integrate(cos(x)/sin(x)**7, x), x)) == \ + cos(x)/sin(x)**7 + + +def test_issue_4775(): + a, x, y = symbols('a x y') + assert trigsimp(sin(x)*cos(y)+cos(x)*sin(y)) == sin(x + y) + assert trigsimp(sin(x)*cos(y)+cos(x)*sin(y)+3) == sin(x + y) + 3 + + +def test_issue_4280(): + a, x, y = symbols('a x y') + assert trigsimp(cos(x)**2 + cos(y)**2*sin(x)**2 + sin(y)**2*sin(x)**2) == 1 + assert trigsimp(a**2*sin(x)**2 + a**2*cos(y)**2*cos(x)**2 + a**2*cos(x)**2*sin(y)**2) == a**2 + assert trigsimp(a**2*cos(y)**2*sin(x)**2 + a**2*sin(y)**2*sin(x)**2) == a**2*sin(x)**2 + + +def test_issue_3210(): + eqs = (sin(2)*cos(3) + sin(3)*cos(2), + -sin(2)*sin(3) + cos(2)*cos(3), + sin(2)*cos(3) - sin(3)*cos(2), + sin(2)*sin(3) + cos(2)*cos(3), + sin(2)*sin(3) + cos(2)*cos(3) + cos(2), + sinh(2)*cosh(3) + sinh(3)*cosh(2), + sinh(2)*sinh(3) + cosh(2)*cosh(3), + ) + assert [trigsimp(e) for e in eqs] == [ + sin(5), + cos(5), + -sin(1), + cos(1), + cos(1) + cos(2), + sinh(5), + cosh(5), + ] + + +def test_trigsimp_issues(): + a, x, y = symbols('a x y') + + # issue 4625 - factor_terms works, too + assert trigsimp(sin(x)**3 + cos(x)**2*sin(x)) == sin(x) + + # issue 5948 + assert trigsimp(diff(integrate(cos(x)/sin(x)**3, x), x)) == \ + cos(x)/sin(x)**3 + assert trigsimp(diff(integrate(sin(x)/cos(x)**3, x), x)) == \ + sin(x)/cos(x)**3 + + # check integer exponents + e = sin(x)**y/cos(x)**y + assert trigsimp(e) == e + assert trigsimp(e.subs(y, 2)) == tan(x)**2 + assert trigsimp(e.subs(x, 1)) == tan(1)**y + + # check for multiple patterns + assert (cos(x)**2/sin(x)**2*cos(y)**2/sin(y)**2).trigsimp() == \ + 1/tan(x)**2/tan(y)**2 + assert trigsimp(cos(x)/sin(x)*cos(x+y)/sin(x+y)) == \ + 1/(tan(x)*tan(x + y)) + + eq = cos(2)*(cos(3) + 1)**2/(cos(3) - 1)**2 + assert trigsimp(eq) == eq.factor() # factor makes denom (-1 + cos(3))**2 + assert trigsimp(cos(2)*(cos(3) + 1)**2*(cos(3) - 1)**2) == \ + cos(2)*sin(3)**4 + + # issue 6789; this generates an expression that formerly caused + # trigsimp to hang + assert cot(x).equals(tan(x)) is False + + # nan or the unchanged expression is ok, but not sin(1) + z = cos(x)**2 + sin(x)**2 - 1 + z1 = tan(x)**2 - 1/cot(x)**2 + n = (1 + z1/z) + assert trigsimp(sin(n)) != sin(1) + eq = x*(n - 1) - x*n + assert trigsimp(eq) is S.NaN + assert trigsimp(eq, recursive=True) is S.NaN + assert trigsimp(1).is_Integer + + assert trigsimp(-sin(x)**4 - 2*sin(x)**2*cos(x)**2 - cos(x)**4) == -1 + + +def test_trigsimp_issue_2515(): + x = Symbol('x') + assert trigsimp(x*cos(x)*tan(x)) == x*sin(x) + assert trigsimp(-sin(x) + cos(x)*tan(x)) == 0 + + +def test_trigsimp_issue_3826(): + assert trigsimp(tan(2*x).expand(trig=True)) == tan(2*x) + + +def test_trigsimp_issue_4032(): + n = Symbol('n', integer=True, positive=True) + assert trigsimp(2**(n/2)*cos(pi*n/4)/2 + 2**(n - 1)/2) == \ + 2**(n/2)*cos(pi*n/4)/2 + 2**n/4 + + +def test_trigsimp_issue_7761(): + assert trigsimp(cosh(pi/4)) == cosh(pi/4) + + +def test_trigsimp_noncommutative(): + x, y = symbols('x,y') + A, B = symbols('A,B', commutative=False) + + assert trigsimp(A - A*sin(x)**2) == A*cos(x)**2 + assert trigsimp(A - A*cos(x)**2) == A*sin(x)**2 + assert trigsimp(A*sin(x)**2 + A*cos(x)**2) == A + assert trigsimp(A + A*tan(x)**2) == A/cos(x)**2 + assert trigsimp(A/cos(x)**2 - A) == A*tan(x)**2 + assert trigsimp(A/cos(x)**2 - A*tan(x)**2) == A + assert trigsimp(A + A*cot(x)**2) == A/sin(x)**2 + assert trigsimp(A/sin(x)**2 - A) == A/tan(x)**2 + assert trigsimp(A/sin(x)**2 - A*cot(x)**2) == A + + assert trigsimp(y*A*cos(x)**2 + y*A*sin(x)**2) == y*A + + assert trigsimp(A*sin(x)/cos(x)) == A*tan(x) + assert trigsimp(A*tan(x)*cos(x)) == A*sin(x) + assert trigsimp(A*cot(x)**3*sin(x)**3) == A*cos(x)**3 + assert trigsimp(y*A*tan(x)**2/sin(x)**2) == y*A/cos(x)**2 + assert trigsimp(A*cot(x)/cos(x)) == A/sin(x) + + assert trigsimp(A*sin(x + y) + A*sin(x - y)) == 2*A*sin(x)*cos(y) + assert trigsimp(A*sin(x + y) - A*sin(x - y)) == 2*A*sin(y)*cos(x) + assert trigsimp(A*cos(x + y) + A*cos(x - y)) == 2*A*cos(x)*cos(y) + assert trigsimp(A*cos(x + y) - A*cos(x - y)) == -2*A*sin(x)*sin(y) + + assert trigsimp(A*sinh(x + y) + A*sinh(x - y)) == 2*A*sinh(x)*cosh(y) + assert trigsimp(A*sinh(x + y) - A*sinh(x - y)) == 2*A*sinh(y)*cosh(x) + assert trigsimp(A*cosh(x + y) + A*cosh(x - y)) == 2*A*cosh(x)*cosh(y) + assert trigsimp(A*cosh(x + y) - A*cosh(x - y)) == 2*A*sinh(x)*sinh(y) + + assert trigsimp(A*cos(0.12345)**2 + A*sin(0.12345)**2) == 1.0*A + + +def test_hyperbolic_simp(): + x, y = symbols('x,y') + + assert trigsimp(sinh(x)**2 + 1) == cosh(x)**2 + assert trigsimp(cosh(x)**2 - 1) == sinh(x)**2 + assert trigsimp(cosh(x)**2 - sinh(x)**2) == 1 + assert trigsimp(1 - tanh(x)**2) == 1/cosh(x)**2 + assert trigsimp(1 - 1/cosh(x)**2) == tanh(x)**2 + assert trigsimp(tanh(x)**2 + 1/cosh(x)**2) == 1 + assert trigsimp(coth(x)**2 - 1) == 1/sinh(x)**2 + assert trigsimp(1/sinh(x)**2 + 1) == 1/tanh(x)**2 + assert trigsimp(coth(x)**2 - 1/sinh(x)**2) == 1 + + assert trigsimp(5*cosh(x)**2 - 5*sinh(x)**2) == 5 + assert trigsimp(5*cosh(x/2)**2 - 2*sinh(x/2)**2) == 3*cosh(x)/2 + Rational(7, 2) + + assert trigsimp(sinh(x)/cosh(x)) == tanh(x) + assert trigsimp(tanh(x)) == trigsimp(sinh(x)/cosh(x)) + assert trigsimp(cosh(x)/sinh(x)) == 1/tanh(x) + assert trigsimp(2*tanh(x)*cosh(x)) == 2*sinh(x) + assert trigsimp(coth(x)**3*sinh(x)**3) == cosh(x)**3 + assert trigsimp(y*tanh(x)**2/sinh(x)**2) == y/cosh(x)**2 + assert trigsimp(coth(x)/cosh(x)) == 1/sinh(x) + + for a in (pi/6*I, pi/4*I, pi/3*I): + assert trigsimp(sinh(a)*cosh(x) + cosh(a)*sinh(x)) == sinh(x + a) + assert trigsimp(-sinh(a)*cosh(x) + cosh(a)*sinh(x)) == sinh(x - a) + + e = 2*cosh(x)**2 - 2*sinh(x)**2 + assert trigsimp(log(e)) == log(2) + + # issue 19535: + assert trigsimp(sqrt(cosh(x)**2 - 1)) == sqrt(sinh(x)**2) + + assert trigsimp(cosh(x)**2*cosh(y)**2 - cosh(x)**2*sinh(y)**2 - sinh(x)**2, + recursive=True) == 1 + assert trigsimp(sinh(x)**2*sinh(y)**2 - sinh(x)**2*cosh(y)**2 + cosh(x)**2, + recursive=True) == 1 + + assert abs(trigsimp(2.0*cosh(x)**2 - 2.0*sinh(x)**2) - 2.0) < 1e-10 + + assert trigsimp(sinh(x)**2/cosh(x)**2) == tanh(x)**2 + assert trigsimp(sinh(x)**3/cosh(x)**3) == tanh(x)**3 + assert trigsimp(sinh(x)**10/cosh(x)**10) == tanh(x)**10 + assert trigsimp(cosh(x)**3/sinh(x)**3) == 1/tanh(x)**3 + + assert trigsimp(cosh(x)/sinh(x)) == 1/tanh(x) + assert trigsimp(cosh(x)**2/sinh(x)**2) == 1/tanh(x)**2 + assert trigsimp(cosh(x)**10/sinh(x)**10) == 1/tanh(x)**10 + + assert trigsimp(x*cosh(x)*tanh(x)) == x*sinh(x) + assert trigsimp(-sinh(x) + cosh(x)*tanh(x)) == 0 + + assert tan(x) != 1/cot(x) # cot doesn't auto-simplify + + assert trigsimp(tan(x) - 1/cot(x)) == 0 + assert trigsimp(3*tanh(x)**7 - 2/coth(x)**7) == tanh(x)**7 + + +def test_trigsimp_groebner(): + from sympy.simplify.trigsimp import trigsimp_groebner + + c = cos(x) + s = sin(x) + ex = (4*s*c + 12*s + 5*c**3 + 21*c**2 + 23*c + 15)/( + -s*c**2 + 2*s*c + 15*s + 7*c**3 + 31*c**2 + 37*c + 21) + resnum = (5*s - 5*c + 1) + resdenom = (8*s - 6*c) + results = [resnum/resdenom, (-resnum)/(-resdenom)] + assert trigsimp_groebner(ex) in results + assert trigsimp_groebner(s/c, hints=[tan]) == tan(x) + assert trigsimp_groebner(c*s) == c*s + assert trigsimp((-s + 1)/c + c/(-s + 1), + method='groebner') == 2/c + assert trigsimp((-s + 1)/c + c/(-s + 1), + method='groebner', polynomial=True) == 2/c + + # Test quick=False works + assert trigsimp_groebner(ex, hints=[2]) in results + assert trigsimp_groebner(ex, hints=[int(2)]) in results + + # test "I" + assert trigsimp_groebner(sin(I*x)/cos(I*x), hints=[tanh]) == I*tanh(x) + + # test hyperbolic / sums + assert trigsimp_groebner((tanh(x)+tanh(y))/(1+tanh(x)*tanh(y)), + hints=[(tanh, x, y)]) == tanh(x + y) + + +def test_issue_2827_trigsimp_methods(): + measure1 = lambda expr: len(str(expr)) + measure2 = lambda expr: -count_ops(expr) + # Return the most complicated result + expr = (x + 1)/(x + sin(x)**2 + cos(x)**2) + ans = Matrix([1]) + M = Matrix([expr]) + assert trigsimp(M, method='fu', measure=measure1) == ans + assert trigsimp(M, method='fu', measure=measure2) != ans + # all methods should work with Basic expressions even if they + # aren't Expr + M = Matrix.eye(1) + assert all(trigsimp(M, method=m) == M for m in + 'fu matching groebner old'.split()) + # watch for E in exptrigsimp, not only exp() + eq = 1/sqrt(E) + E + assert exptrigsimp(eq) == eq + +def test_issue_15129_trigsimp_methods(): + t1 = Matrix([sin(Rational(1, 50)), cos(Rational(1, 50)), 0]) + t2 = Matrix([sin(Rational(1, 25)), cos(Rational(1, 25)), 0]) + t3 = Matrix([cos(Rational(1, 25)), sin(Rational(1, 25)), 0]) + r1 = t1.dot(t2) + r2 = t1.dot(t3) + assert trigsimp(r1) == cos(Rational(1, 50)) + assert trigsimp(r2) == sin(Rational(3, 50)) + +def test_exptrigsimp(): + def valid(a, b): + from sympy.core.random import verify_numerically as tn + if not (tn(a, b) and a == b): + return False + return True + + assert exptrigsimp(exp(x) + exp(-x)) == 2*cosh(x) + assert exptrigsimp(exp(x) - exp(-x)) == 2*sinh(x) + assert exptrigsimp((2*exp(x)-2*exp(-x))/(exp(x)+exp(-x))) == 2*tanh(x) + assert exptrigsimp((2*exp(2*x)-2)/(exp(2*x)+1)) == 2*tanh(x) + e = [cos(x) + I*sin(x), cos(x) - I*sin(x), + cosh(x) - sinh(x), cosh(x) + sinh(x)] + ok = [exp(I*x), exp(-I*x), exp(-x), exp(x)] + assert all(valid(i, j) for i, j in zip( + [exptrigsimp(ei) for ei in e], ok)) + + ue = [cos(x) + sin(x), cos(x) - sin(x), + cosh(x) + I*sinh(x), cosh(x) - I*sinh(x)] + assert [exptrigsimp(ei) == ei for ei in ue] + + res = [] + ok = [y*tanh(1), 1/(y*tanh(1)), I*y*tan(1), -I/(y*tan(1)), + y*tanh(x), 1/(y*tanh(x)), I*y*tan(x), -I/(y*tan(x)), + y*tanh(1 + I), 1/(y*tanh(1 + I))] + for a in (1, I, x, I*x, 1 + I): + w = exp(a) + eq = y*(w - 1/w)/(w + 1/w) + res.append(simplify(eq)) + res.append(simplify(1/eq)) + assert all(valid(i, j) for i, j in zip(res, ok)) + + for a in range(1, 3): + w = exp(a) + e = w + 1/w + s = simplify(e) + assert s == exptrigsimp(e) + assert valid(s, 2*cosh(a)) + e = w - 1/w + s = simplify(e) + assert s == exptrigsimp(e) + assert valid(s, 2*sinh(a)) + +def test_exptrigsimp_noncommutative(): + a,b = symbols('a b', commutative=False) + x = Symbol('x', commutative=True) + assert exp(a + x) == exptrigsimp(exp(a)*exp(x)) + p = exp(a)*exp(b) - exp(b)*exp(a) + assert p == exptrigsimp(p) != 0 + +def test_powsimp_on_numbers(): + assert 2**(Rational(1, 3) - 2) == 2**Rational(1, 3)/4 + + +@XFAIL +def test_issue_6811_fail(): + # from doc/src/modules/physics/mechanics/examples.rst, the current `eq` + # at Line 576 (in different variables) was formerly the equivalent and + # shorter expression given below...it would be nice to get the short one + # back again + xp, y, x, z = symbols('xp, y, x, z') + eq = 4*(-19*sin(x)*y + 5*sin(3*x)*y + 15*cos(2*x)*z - 21*z)*xp/(9*cos(x) - 5*cos(3*x)) + assert trigsimp(eq) == -2*(2*cos(x)*tan(x)*y + 3*z)*xp/cos(x) + + +def test_Piecewise(): + e1 = x*(x + y) - y*(x + y) + e2 = sin(x)**2 + cos(x)**2 + e3 = expand((x + y)*y/x) + # s1 = simplify(e1) + s2 = simplify(e2) + # s3 = simplify(e3) + + # trigsimp tries not to touch non-trig containing args + assert trigsimp(Piecewise((e1, e3 < e2), (e3, True))) == \ + Piecewise((e1, e3 < s2), (e3, True)) + + +def test_issue_21594(): + assert simplify(exp(Rational(1,2)) + exp(Rational(-1,2))) == cosh(S.Half)*2 + + +def test_trigsimp_old(): + x, y = symbols('x,y') + + assert trigsimp(1 - sin(x)**2, old=True) == cos(x)**2 + assert trigsimp(1 - cos(x)**2, old=True) == sin(x)**2 + assert trigsimp(sin(x)**2 + cos(x)**2, old=True) == 1 + assert trigsimp(1 + tan(x)**2, old=True) == 1/cos(x)**2 + assert trigsimp(1/cos(x)**2 - 1, old=True) == tan(x)**2 + assert trigsimp(1/cos(x)**2 - tan(x)**2, old=True) == 1 + assert trigsimp(1 + cot(x)**2, old=True) == 1/sin(x)**2 + assert trigsimp(1/sin(x)**2 - cot(x)**2, old=True) == 1 + + assert trigsimp(5*cos(x)**2 + 5*sin(x)**2, old=True) == 5 + + assert trigsimp(sin(x)/cos(x), old=True) == tan(x) + assert trigsimp(2*tan(x)*cos(x), old=True) == 2*sin(x) + assert trigsimp(cot(x)**3*sin(x)**3, old=True) == cos(x)**3 + assert trigsimp(y*tan(x)**2/sin(x)**2, old=True) == y/cos(x)**2 + assert trigsimp(cot(x)/cos(x), old=True) == 1/sin(x) + + assert trigsimp(sin(x + y) + sin(x - y), old=True) == 2*sin(x)*cos(y) + assert trigsimp(sin(x + y) - sin(x - y), old=True) == 2*sin(y)*cos(x) + assert trigsimp(cos(x + y) + cos(x - y), old=True) == 2*cos(x)*cos(y) + assert trigsimp(cos(x + y) - cos(x - y), old=True) == -2*sin(x)*sin(y) + + assert trigsimp(sinh(x + y) + sinh(x - y), old=True) == 2*sinh(x)*cosh(y) + assert trigsimp(sinh(x + y) - sinh(x - y), old=True) == 2*sinh(y)*cosh(x) + assert trigsimp(cosh(x + y) + cosh(x - y), old=True) == 2*cosh(x)*cosh(y) + assert trigsimp(cosh(x + y) - cosh(x - y), old=True) == 2*sinh(x)*sinh(y) + + assert trigsimp(cos(0.12345)**2 + sin(0.12345)**2, old=True) == 1.0 + + assert trigsimp(sin(x)/cos(x), old=True, method='combined') == tan(x) + assert trigsimp(sin(x)/cos(x), old=True, method='groebner') == sin(x)/cos(x) + assert trigsimp(sin(x)/cos(x), old=True, method='groebner', hints=[tan]) == tan(x) + + assert trigsimp(1-sin(sin(x)**2+cos(x)**2)**2, old=True, deep=True) == cos(1)**2 + + +def test_trigsimp_inverse(): + alpha = symbols('alpha') + s, c = sin(alpha), cos(alpha) + + for finv in [asin, acos, asec, acsc, atan, acot]: + f = finv.inverse(None) + assert alpha == trigsimp(finv(f(alpha)), inverse=True) + + # test atan2(cos, sin), atan2(sin, cos), etc... + for a, b in [[c, s], [s, c]]: + for i, j in product([-1, 1], repeat=2): + angle = atan2(i*b, j*a) + angle_inverted = trigsimp(angle, inverse=True) + assert angle_inverted != angle # assures simplification happened + assert sin(angle_inverted) == trigsimp(sin(angle)) + assert cos(angle_inverted) == trigsimp(cos(angle)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/traversaltools.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/traversaltools.py new file mode 100644 index 0000000000000000000000000000000000000000..75b0bd0d8fd198cb12640ab8a0fe63a23c81ed8f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/traversaltools.py @@ -0,0 +1,15 @@ +from sympy.core.traversal import use as _use +from sympy.utilities.decorator import deprecated + +use = deprecated( + """ + Using use from the sympy.simplify.traversaltools submodule is + deprecated. + + Instead, use use from the top-level sympy namespace, like + + sympy.use + """, + deprecated_since_version="1.10", + active_deprecations_target="deprecated-traversal-functions-moved" +)(_use) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/trigsimp.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/trigsimp.py new file mode 100644 index 0000000000000000000000000000000000000000..fe5be1444a4625e4b63b339877e441d12cfbe8de --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/simplify/trigsimp.py @@ -0,0 +1,1252 @@ +from collections import defaultdict +from functools import reduce + +from sympy.core import (sympify, Basic, S, Expr, factor_terms, + Mul, Add, bottom_up) +from sympy.core.cache import cacheit +from sympy.core.function import (count_ops, _mexpand, FunctionClass, expand, + expand_mul, _coeff_isneg, Derivative) +from sympy.core.numbers import I, Integer +from sympy.core.intfunc import igcd +from sympy.core.sorting import _nodes +from sympy.core.symbol import Dummy, symbols, Wild +from sympy.external.gmpy import SYMPY_INTS +from sympy.functions import sin, cos, exp, cosh, tanh, sinh, tan, cot, coth +from sympy.functions import atan2 +from sympy.functions.elementary.hyperbolic import HyperbolicFunction +from sympy.functions.elementary.trigonometric import TrigonometricFunction +from sympy.polys import Poly, factor, cancel, parallel_poly_from_expr +from sympy.polys.domains import ZZ +from sympy.polys.polyerrors import PolificationFailed +from sympy.polys.polytools import groebner +from sympy.simplify.cse_main import cse +from sympy.strategies.core import identity +from sympy.strategies.tree import greedy +from sympy.utilities.iterables import iterable +from sympy.utilities.misc import debug + +def trigsimp_groebner(expr, hints=[], quick=False, order="grlex", + polynomial=False): + """ + Simplify trigonometric expressions using a groebner basis algorithm. + + Explanation + =========== + + This routine takes a fraction involving trigonometric or hyperbolic + expressions, and tries to simplify it. The primary metric is the + total degree. Some attempts are made to choose the simplest possible + expression of the minimal degree, but this is non-rigorous, and also + very slow (see the ``quick=True`` option). + + If ``polynomial`` is set to True, instead of simplifying numerator and + denominator together, this function just brings numerator and denominator + into a canonical form. This is much faster, but has potentially worse + results. However, if the input is a polynomial, then the result is + guaranteed to be an equivalent polynomial of minimal degree. + + The most important option is hints. Its entries can be any of the + following: + + - a natural number + - a function + - an iterable of the form (func, var1, var2, ...) + - anything else, interpreted as a generator + + A number is used to indicate that the search space should be increased. + A function is used to indicate that said function is likely to occur in a + simplified expression. + An iterable is used indicate that func(var1 + var2 + ...) is likely to + occur in a simplified . + An additional generator also indicates that it is likely to occur. + (See examples below). + + This routine carries out various computationally intensive algorithms. + The option ``quick=True`` can be used to suppress one particularly slow + step (at the expense of potentially more complicated results, but never at + the expense of increased total degree). + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy import sin, tan, cos, sinh, cosh, tanh + >>> from sympy.simplify.trigsimp import trigsimp_groebner + + Suppose you want to simplify ``sin(x)*cos(x)``. Naively, nothing happens: + + >>> ex = sin(x)*cos(x) + >>> trigsimp_groebner(ex) + sin(x)*cos(x) + + This is because ``trigsimp_groebner`` only looks for a simplification + involving just ``sin(x)`` and ``cos(x)``. You can tell it to also try + ``2*x`` by passing ``hints=[2]``: + + >>> trigsimp_groebner(ex, hints=[2]) + sin(2*x)/2 + >>> trigsimp_groebner(sin(x)**2 - cos(x)**2, hints=[2]) + -cos(2*x) + + Increasing the search space this way can quickly become expensive. A much + faster way is to give a specific expression that is likely to occur: + + >>> trigsimp_groebner(ex, hints=[sin(2*x)]) + sin(2*x)/2 + + Hyperbolic expressions are similarly supported: + + >>> trigsimp_groebner(sinh(2*x)/sinh(x)) + 2*cosh(x) + + Note how no hints had to be passed, since the expression already involved + ``2*x``. + + The tangent function is also supported. You can either pass ``tan`` in the + hints, to indicate that tan should be tried whenever cosine or sine are, + or you can pass a specific generator: + + >>> trigsimp_groebner(sin(x)/cos(x), hints=[tan]) + tan(x) + >>> trigsimp_groebner(sinh(x)/cosh(x), hints=[tanh(x)]) + tanh(x) + + Finally, you can use the iterable form to suggest that angle sum formulae + should be tried: + + >>> ex = (tan(x) + tan(y))/(1 - tan(x)*tan(y)) + >>> trigsimp_groebner(ex, hints=[(tan, x, y)]) + tan(x + y) + """ + # TODO + # - preprocess by replacing everything by funcs we can handle + # - optionally use cot instead of tan + # - more intelligent hinting. + # For example, if the ideal is small, and we have sin(x), sin(y), + # add sin(x + y) automatically... ? + # - algebraic numbers ... + # - expressions of lowest degree are not distinguished properly + # e.g. 1 - sin(x)**2 + # - we could try to order the generators intelligently, so as to influence + # which monomials appear in the quotient basis + + # THEORY + # ------ + # Ratsimpmodprime above can be used to "simplify" a rational function + # modulo a prime ideal. "Simplify" mainly means finding an equivalent + # expression of lower total degree. + # + # We intend to use this to simplify trigonometric functions. To do that, + # we need to decide (a) which ring to use, and (b) modulo which ideal to + # simplify. In practice, (a) means settling on a list of "generators" + # a, b, c, ..., such that the fraction we want to simplify is a rational + # function in a, b, c, ..., with coefficients in ZZ (integers). + # (2) means that we have to decide what relations to impose on the + # generators. There are two practical problems: + # (1) The ideal has to be *prime* (a technical term). + # (2) The relations have to be polynomials in the generators. + # + # We typically have two kinds of generators: + # - trigonometric expressions, like sin(x), cos(5*x), etc + # - "everything else", like gamma(x), pi, etc. + # + # Since this function is trigsimp, we will concentrate on what to do with + # trigonometric expressions. We can also simplify hyperbolic expressions, + # but the extensions should be clear. + # + # One crucial point is that all *other* generators really should behave + # like indeterminates. In particular if (say) "I" is one of them, then + # in fact I**2 + 1 = 0 and we may and will compute non-sensical + # expressions. However, we can work with a dummy and add the relation + # I**2 + 1 = 0 to our ideal, then substitute back in the end. + # + # Now regarding trigonometric generators. We split them into groups, + # according to the argument of the trigonometric functions. We want to + # organise this in such a way that most trigonometric identities apply in + # the same group. For example, given sin(x), cos(2*x) and cos(y), we would + # group as [sin(x), cos(2*x)] and [cos(y)]. + # + # Our prime ideal will be built in three steps: + # (1) For each group, compute a "geometrically prime" ideal of relations. + # Geometrically prime means that it generates a prime ideal in + # CC[gens], not just ZZ[gens]. + # (2) Take the union of all the generators of the ideals for all groups. + # By the geometric primality condition, this is still prime. + # (3) Add further inter-group relations which preserve primality. + # + # Step (1) works as follows. We will isolate common factors in the + # argument, so that all our generators are of the form sin(n*x), cos(n*x) + # or tan(n*x), with n an integer. Suppose first there are no tan terms. + # The ideal [sin(x)**2 + cos(x)**2 - 1] is geometrically prime, since + # X**2 + Y**2 - 1 is irreducible over CC. + # Now, if we have a generator sin(n*x), than we can, using trig identities, + # express sin(n*x) as a polynomial in sin(x) and cos(x). We can add this + # relation to the ideal, preserving geometric primality, since the quotient + # ring is unchanged. + # Thus we have treated all sin and cos terms. + # For tan(n*x), we add a relation tan(n*x)*cos(n*x) - sin(n*x) = 0. + # (This requires of course that we already have relations for cos(n*x) and + # sin(n*x).) It is not obvious, but it seems that this preserves geometric + # primality. + # XXX A real proof would be nice. HELP! + # Sketch that is a prime ideal of + # CC[S, C, T]: + # - it suffices to show that the projective closure in CP**3 is + # irreducible + # - using the half-angle substitutions, we can express sin(x), tan(x), + # cos(x) as rational functions in tan(x/2) + # - from this, we get a rational map from CP**1 to our curve + # - this is a morphism, hence the curve is prime + # + # Step (2) is trivial. + # + # Step (3) works by adding selected relations of the form + # sin(x + y) - sin(x)*cos(y) - sin(y)*cos(x), etc. Geometric primality is + # preserved by the same argument as before. + + def parse_hints(hints): + """Split hints into (n, funcs, iterables, gens).""" + n = 1 + funcs, iterables, gens = [], [], [] + for e in hints: + if isinstance(e, (SYMPY_INTS, Integer)): + n = e + elif isinstance(e, FunctionClass): + funcs.append(e) + elif iterable(e): + iterables.append((e[0], e[1:])) + # XXX sin(x+2y)? + # Note: we go through polys so e.g. + # sin(-x) -> -sin(x) -> sin(x) + gens.extend(parallel_poly_from_expr( + [e[0](x) for x in e[1:]] + [e[0](Add(*e[1:]))])[1].gens) + else: + gens.append(e) + return n, funcs, iterables, gens + + def build_ideal(x, terms): + """ + Build generators for our ideal. ``Terms`` is an iterable with elements of + the form (fn, coeff), indicating that we have a generator fn(coeff*x). + + If any of the terms is trigonometric, sin(x) and cos(x) are guaranteed + to appear in terms. Similarly for hyperbolic functions. For tan(n*x), + sin(n*x) and cos(n*x) are guaranteed. + """ + I = [] + y = Dummy('y') + for fn, coeff in terms: + for c, s, t, rel in ( + [cos, sin, tan, cos(x)**2 + sin(x)**2 - 1], + [cosh, sinh, tanh, cosh(x)**2 - sinh(x)**2 - 1]): + if coeff == 1 and fn in [c, s]: + I.append(rel) + elif fn == t: + I.append(t(coeff*x)*c(coeff*x) - s(coeff*x)) + elif fn in [c, s]: + cn = fn(coeff*y).expand(trig=True).subs(y, x) + I.append(fn(coeff*x) - cn) + return list(set(I)) + + def analyse_gens(gens, hints): + """ + Analyse the generators ``gens``, using the hints ``hints``. + + The meaning of ``hints`` is described in the main docstring. + Return a new list of generators, and also the ideal we should + work with. + """ + # First parse the hints + n, funcs, iterables, extragens = parse_hints(hints) + debug('n=%s funcs: %s iterables: %s extragens: %s', + (funcs, iterables, extragens)) + + # We just add the extragens to gens and analyse them as before + gens = list(gens) + gens.extend(extragens) + + # remove duplicates + funcs = list(set(funcs)) + iterables = list(set(iterables)) + gens = list(set(gens)) + + # all the functions we can do anything with + allfuncs = {sin, cos, tan, sinh, cosh, tanh} + # sin(3*x) -> ((3, x), sin) + trigterms = [(g.args[0].as_coeff_mul(), g.func) for g in gens + if g.func in allfuncs] + # Our list of new generators - start with anything that we cannot + # work with (i.e. is not a trigonometric term) + freegens = [g for g in gens if g.func not in allfuncs] + newgens = [] + trigdict = {} + for (coeff, var), fn in trigterms: + trigdict.setdefault(var, []).append((coeff, fn)) + res = [] # the ideal + + for key, val in trigdict.items(): + # We have now assembeled a dictionary. Its keys are common + # arguments in trigonometric expressions, and values are lists of + # pairs (fn, coeff). x0, (fn, coeff) in trigdict means that we + # need to deal with fn(coeff*x0). We take the rational gcd of the + # coeffs, call it ``gcd``. We then use x = x0/gcd as "base symbol", + # all other arguments are integral multiples thereof. + # We will build an ideal which works with sin(x), cos(x). + # If hint tan is provided, also work with tan(x). Moreover, if + # n > 1, also work with sin(k*x) for k <= n, and similarly for cos + # (and tan if the hint is provided). Finally, any generators which + # the ideal does not work with but we need to accommodate (either + # because it was in expr or because it was provided as a hint) + # we also build into the ideal. + # This selection process is expressed in the list ``terms``. + # build_ideal then generates the actual relations in our ideal, + # from this list. + fns = [x[1] for x in val] + val = [x[0] for x in val] + gcd = reduce(igcd, val) + terms = [(fn, v/gcd) for (fn, v) in zip(fns, val)] + fs = set(funcs + fns) + for c, s, t in ([cos, sin, tan], [cosh, sinh, tanh]): + if any(x in fs for x in (c, s, t)): + fs.add(c) + fs.add(s) + for fn in fs: + terms.extend((fn, k) for k in range(1, n + 1)) + extra = [] + for fn, v in terms: + if fn == tan: + extra.append((sin, v)) + extra.append((cos, v)) + if fn in [sin, cos] and tan in fs: + extra.append((tan, v)) + if fn == tanh: + extra.append((sinh, v)) + extra.append((cosh, v)) + if fn in [sinh, cosh] and tanh in fs: + extra.append((tanh, v)) + terms.extend(extra) + x = gcd*Mul(*key) + r = build_ideal(x, terms) + res.extend(r) + newgens.extend({fn(v*x) for fn, v in terms}) + + # Add generators for compound expressions from iterables + for fn, args in iterables: + if fn == tan: + # Tan expressions are recovered from sin and cos. + iterables.extend([(sin, args), (cos, args)]) + elif fn == tanh: + # Tanh expressions are recovered from sihn and cosh. + iterables.extend([(sinh, args), (cosh, args)]) + else: + dummys = symbols('d:%i' % len(args), cls=Dummy) + expr = fn( Add(*dummys)).expand(trig=True).subs(list(zip(dummys, args))) + res.append(fn(Add(*args)) - expr) + + if myI in gens: + res.append(myI**2 + 1) + freegens.remove(myI) + newgens.append(myI) + + return res, freegens, newgens + + myI = Dummy('I') + expr = expr.subs(S.ImaginaryUnit, myI) + subs = [(myI, S.ImaginaryUnit)] + + num, denom = cancel(expr).as_numer_denom() + try: + (pnum, pdenom), opt = parallel_poly_from_expr([num, denom]) + except PolificationFailed: + return expr + debug('initial gens:', opt.gens) + ideal, freegens, gens = analyse_gens(opt.gens, hints) + debug('ideal:', ideal) + debug('new gens:', gens, " -- len", len(gens)) + debug('free gens:', freegens, " -- len", len(gens)) + # NOTE we force the domain to be ZZ to stop polys from injecting generators + # (which is usually a sign of a bug in the way we build the ideal) + if not gens: + return expr + G = groebner(ideal, order=order, gens=gens, domain=ZZ) + debug('groebner basis:', list(G), " -- len", len(G)) + + # If our fraction is a polynomial in the free generators, simplify all + # coefficients separately: + + from sympy.simplify.ratsimp import ratsimpmodprime + + if freegens and pdenom.has_only_gens(*set(gens).intersection(pdenom.gens)): + num = Poly(num, gens=gens+freegens).eject(*gens) + res = [] + for monom, coeff in num.terms(): + ourgens = set(parallel_poly_from_expr([coeff, denom])[1].gens) + # We compute the transitive closure of all generators that can + # be reached from our generators through relations in the ideal. + changed = True + while changed: + changed = False + for p in ideal: + p = Poly(p) + if not ourgens.issuperset(p.gens) and \ + not p.has_only_gens(*set(p.gens).difference(ourgens)): + changed = True + ourgens.update(p.exclude().gens) + # NOTE preserve order! + realgens = [x for x in gens if x in ourgens] + # The generators of the ideal have now been (implicitly) split + # into two groups: those involving ourgens and those that don't. + # Since we took the transitive closure above, these two groups + # live in subgrings generated by a *disjoint* set of variables. + # Any sensible groebner basis algorithm will preserve this disjoint + # structure (i.e. the elements of the groebner basis can be split + # similarly), and and the two subsets of the groebner basis then + # form groebner bases by themselves. (For the smaller generating + # sets, of course.) + ourG = [g.as_expr() for g in G.polys if + g.has_only_gens(*ourgens.intersection(g.gens))] + res.append(Mul(*[a**b for a, b in zip(freegens, monom)]) * \ + ratsimpmodprime(coeff/denom, ourG, order=order, + gens=realgens, quick=quick, domain=ZZ, + polynomial=polynomial).subs(subs)) + return Add(*res) + # NOTE The following is simpler and has less assumptions on the + # groebner basis algorithm. If the above turns out to be broken, + # use this. + return Add(*[Mul(*[a**b for a, b in zip(freegens, monom)]) * \ + ratsimpmodprime(coeff/denom, list(G), order=order, + gens=gens, quick=quick, domain=ZZ) + for monom, coeff in num.terms()]) + else: + return ratsimpmodprime( + expr, list(G), order=order, gens=freegens+gens, + quick=quick, domain=ZZ, polynomial=polynomial).subs(subs) + + +_trigs = (TrigonometricFunction, HyperbolicFunction) + + +def _trigsimp_inverse(rv): + + def check_args(x, y): + try: + return x.args[0] == y.args[0] + except IndexError: + return False + + def f(rv): + # for simple functions + g = getattr(rv, 'inverse', None) + if (g is not None and isinstance(rv.args[0], g()) and + isinstance(g()(1), TrigonometricFunction)): + return rv.args[0].args[0] + + # for atan2 simplifications, harder because atan2 has 2 args + if isinstance(rv, atan2): + y, x = rv.args + if _coeff_isneg(y): + return -f(atan2(-y, x)) + elif _coeff_isneg(x): + return S.Pi - f(atan2(y, -x)) + + if check_args(x, y): + if isinstance(y, sin) and isinstance(x, cos): + return x.args[0] + if isinstance(y, cos) and isinstance(x, sin): + return S.Pi / 2 - x.args[0] + + return rv + + return bottom_up(rv, f) + + +def trigsimp(expr, inverse=False, **opts): + """Returns a reduced expression by using known trig identities. + + Parameters + ========== + + inverse : bool, optional + If ``inverse=True``, it will be assumed that a composition of inverse + functions, such as sin and asin, can be cancelled in any order. + For example, ``asin(sin(x))`` will yield ``x`` without checking whether + x belongs to the set where this relation is true. The default is False. + Default : True + + method : string, optional + Specifies the method to use. Valid choices are: + + - ``'matching'``, default + - ``'groebner'`` + - ``'combined'`` + - ``'fu'`` + - ``'old'`` + + If ``'matching'``, simplify the expression recursively by targeting + common patterns. If ``'groebner'``, apply an experimental groebner + basis algorithm. In this case further options are forwarded to + ``trigsimp_groebner``, please refer to + its docstring. If ``'combined'``, it first runs the groebner basis + algorithm with small default parameters, then runs the ``'matching'`` + algorithm. If ``'fu'``, run the collection of trigonometric + transformations described by Fu, et al. (see the + :py:func:`~sympy.simplify.fu.fu` docstring). If ``'old'``, the original + SymPy trig simplification function is run. + opts : + Optional keyword arguments passed to the method. See each method's + function docstring for details. + + Examples + ======== + + >>> from sympy import trigsimp, sin, cos, log + >>> from sympy.abc import x + >>> e = 2*sin(x)**2 + 2*cos(x)**2 + >>> trigsimp(e) + 2 + + Simplification occurs wherever trigonometric functions are located. + + >>> trigsimp(log(e)) + log(2) + + Using ``method='groebner'`` (or ``method='combined'``) might lead to + greater simplification. + + The old trigsimp routine can be accessed as with method ``method='old'``. + + >>> from sympy import coth, tanh + >>> t = 3*tanh(x)**7 - 2/coth(x)**7 + >>> trigsimp(t, method='old') == t + True + >>> trigsimp(t) + tanh(x)**7 + + """ + from sympy.simplify.fu import fu + + expr = sympify(expr) + + _eval_trigsimp = getattr(expr, '_eval_trigsimp', None) + if _eval_trigsimp is not None: + return _eval_trigsimp(**opts) + + old = opts.pop('old', False) + if not old: + opts.pop('deep', None) + opts.pop('recursive', None) + method = opts.pop('method', 'matching') + else: + method = 'old' + + def groebnersimp(ex, **opts): + def traverse(e): + if e.is_Atom: + return e + args = [traverse(x) for x in e.args] + if e.is_Function or e.is_Pow: + args = [trigsimp_groebner(x, **opts) for x in args] + return e.func(*args) + new = traverse(ex) + if not isinstance(new, Expr): + return new + return trigsimp_groebner(new, **opts) + + trigsimpfunc = { + 'fu': (lambda x: fu(x, **opts)), + 'matching': (lambda x: futrig(x)), + 'groebner': (lambda x: groebnersimp(x, **opts)), + 'combined': (lambda x: futrig(groebnersimp(x, + polynomial=True, hints=[2, tan]))), + 'old': lambda x: trigsimp_old(x, **opts), + }[method] + + expr_simplified = trigsimpfunc(expr) + if inverse: + expr_simplified = _trigsimp_inverse(expr_simplified) + + return expr_simplified + + +def exptrigsimp(expr): + """ + Simplifies exponential / trigonometric / hyperbolic functions. + + Examples + ======== + + >>> from sympy import exptrigsimp, exp, cosh, sinh + >>> from sympy.abc import z + + >>> exptrigsimp(exp(z) + exp(-z)) + 2*cosh(z) + >>> exptrigsimp(cosh(z) - sinh(z)) + exp(-z) + """ + from sympy.simplify.fu import hyper_as_trig, TR2i + + def exp_trig(e): + # select the better of e, and e rewritten in terms of exp or trig + # functions + choices = [e] + if e.has(*_trigs): + choices.append(e.rewrite(exp)) + choices.append(e.rewrite(cos)) + return min(*choices, key=count_ops) + newexpr = bottom_up(expr, exp_trig) + + def f(rv): + if not rv.is_Mul: + return rv + commutative_part, noncommutative_part = rv.args_cnc() + # Since as_powers_dict loses order information, + # if there is more than one noncommutative factor, + # it should only be used to simplify the commutative part. + if (len(noncommutative_part) > 1): + return f(Mul(*commutative_part))*Mul(*noncommutative_part) + rvd = rv.as_powers_dict() + newd = rvd.copy() + + def signlog(expr, sign=S.One): + if expr is S.Exp1: + return sign, S.One + elif isinstance(expr, exp) or (expr.is_Pow and expr.base == S.Exp1): + return sign, expr.exp + elif sign is S.One: + return signlog(-expr, sign=-S.One) + else: + return None, None + + ee = rvd[S.Exp1] + for k in rvd: + if k.is_Add and len(k.args) == 2: + # k == c*(1 + sign*E**x) + c = k.args[0] + sign, x = signlog(k.args[1]/c) + if not x: + continue + m = rvd[k] + newd[k] -= m + if ee == -x*m/2: + # sinh and cosh + newd[S.Exp1] -= ee + ee = 0 + if sign == 1: + newd[2*c*cosh(x/2)] += m + else: + newd[-2*c*sinh(x/2)] += m + elif newd[1 - sign*S.Exp1**x] == -m: + # tanh + del newd[1 - sign*S.Exp1**x] + if sign == 1: + newd[-c/tanh(x/2)] += m + else: + newd[-c*tanh(x/2)] += m + else: + newd[1 + sign*S.Exp1**x] += m + newd[c] += m + + return Mul(*[k**newd[k] for k in newd]) + newexpr = bottom_up(newexpr, f) + + # sin/cos and sinh/cosh ratios to tan and tanh, respectively + if newexpr.has(HyperbolicFunction): + e, f = hyper_as_trig(newexpr) + newexpr = f(TR2i(e)) + if newexpr.has(TrigonometricFunction): + newexpr = TR2i(newexpr) + + # can we ever generate an I where there was none previously? + if not (newexpr.has(I) and not expr.has(I)): + expr = newexpr + return expr + +#-------------------- the old trigsimp routines --------------------- + +def trigsimp_old(expr, *, first=True, **opts): + """ + Reduces expression by using known trig identities. + + Notes + ===== + + deep: + - Apply trigsimp inside all objects with arguments + + recursive: + - Use common subexpression elimination (cse()) and apply + trigsimp recursively (this is quite expensive if the + expression is large) + + method: + - Determine the method to use. Valid choices are 'matching' (default), + 'groebner', 'combined', 'fu' and 'futrig'. If 'matching', simplify the + expression recursively by pattern matching. If 'groebner', apply an + experimental groebner basis algorithm. In this case further options + are forwarded to ``trigsimp_groebner``, please refer to its docstring. + If 'combined', first run the groebner basis algorithm with small + default parameters, then run the 'matching' algorithm. 'fu' runs the + collection of trigonometric transformations described by Fu, et al. + (see the `fu` docstring) while `futrig` runs a subset of Fu-transforms + that mimic the behavior of `trigsimp`. + + compare: + - show input and output from `trigsimp` and `futrig` when different, + but returns the `trigsimp` value. + + Examples + ======== + + >>> from sympy import trigsimp, sin, cos, log, cot + >>> from sympy.abc import x + >>> e = 2*sin(x)**2 + 2*cos(x)**2 + >>> trigsimp(e, old=True) + 2 + >>> trigsimp(log(e), old=True) + log(2*sin(x)**2 + 2*cos(x)**2) + >>> trigsimp(log(e), deep=True, old=True) + log(2) + + Using `method="groebner"` (or `"combined"`) can sometimes lead to a lot + more simplification: + + >>> e = (-sin(x) + 1)/cos(x) + cos(x)/(-sin(x) + 1) + >>> trigsimp(e, old=True) + (1 - sin(x))/cos(x) + cos(x)/(1 - sin(x)) + >>> trigsimp(e, method="groebner", old=True) + 2/cos(x) + + >>> trigsimp(1/cot(x)**2, compare=True, old=True) + futrig: tan(x)**2 + cot(x)**(-2) + + """ + old = expr + if first: + if not expr.has(*_trigs): + return expr + + trigsyms = set().union(*[t.free_symbols for t in expr.atoms(*_trigs)]) + if len(trigsyms) > 1: + from sympy.simplify.simplify import separatevars + + d = separatevars(expr) + if d.is_Mul: + d = separatevars(d, dict=True) or d + if isinstance(d, dict): + expr = 1 + for v in d.values(): + # remove hollow factoring + was = v + v = expand_mul(v) + opts['first'] = False + vnew = trigsimp(v, **opts) + if vnew == v: + vnew = was + expr *= vnew + old = expr + else: + if d.is_Add: + for s in trigsyms: + r, e = expr.as_independent(s) + if r: + opts['first'] = False + expr = r + trigsimp(e, **opts) + if not expr.is_Add: + break + old = expr + + recursive = opts.pop('recursive', False) + deep = opts.pop('deep', False) + method = opts.pop('method', 'matching') + + def groebnersimp(ex, deep, **opts): + def traverse(e): + if e.is_Atom: + return e + args = [traverse(x) for x in e.args] + if e.is_Function or e.is_Pow: + args = [trigsimp_groebner(x, **opts) for x in args] + return e.func(*args) + if deep: + ex = traverse(ex) + return trigsimp_groebner(ex, **opts) + + trigsimpfunc = { + 'matching': (lambda x, d: _trigsimp(x, d)), + 'groebner': (lambda x, d: groebnersimp(x, d, **opts)), + 'combined': (lambda x, d: _trigsimp(groebnersimp(x, + d, polynomial=True, hints=[2, tan]), + d)) + }[method] + + if recursive: + w, g = cse(expr) + g = trigsimpfunc(g[0], deep) + + for sub in reversed(w): + g = g.subs(sub[0], sub[1]) + g = trigsimpfunc(g, deep) + result = g + else: + result = trigsimpfunc(expr, deep) + + if opts.get('compare', False): + f = futrig(old) + if f != result: + print('\tfutrig:', f) + + return result + + +def _dotrig(a, b): + """Helper to tell whether ``a`` and ``b`` have the same sorts + of symbols in them -- no need to test hyperbolic patterns against + expressions that have no hyperbolics in them.""" + return a.func == b.func and ( + a.has(TrigonometricFunction) and b.has(TrigonometricFunction) or + a.has(HyperbolicFunction) and b.has(HyperbolicFunction)) + + +_trigpat = None +def _trigpats(): + global _trigpat + a, b, c = symbols('a b c', cls=Wild) + d = Wild('d', commutative=False) + + # for the simplifications like sinh/cosh -> tanh: + # DO NOT REORDER THE FIRST 14 since these are assumed to be in this + # order in _match_div_rewrite. + matchers_division = ( + (a*sin(b)**c/cos(b)**c, a*tan(b)**c, sin(b), cos(b)), + (a*tan(b)**c*cos(b)**c, a*sin(b)**c, sin(b), cos(b)), + (a*cot(b)**c*sin(b)**c, a*cos(b)**c, sin(b), cos(b)), + (a*tan(b)**c/sin(b)**c, a/cos(b)**c, sin(b), cos(b)), + (a*cot(b)**c/cos(b)**c, a/sin(b)**c, sin(b), cos(b)), + (a*cot(b)**c*tan(b)**c, a, sin(b), cos(b)), + (a*(cos(b) + 1)**c*(cos(b) - 1)**c, + a*(-sin(b)**2)**c, cos(b) + 1, cos(b) - 1), + (a*(sin(b) + 1)**c*(sin(b) - 1)**c, + a*(-cos(b)**2)**c, sin(b) + 1, sin(b) - 1), + + (a*sinh(b)**c/cosh(b)**c, a*tanh(b)**c, S.One, S.One), + (a*tanh(b)**c*cosh(b)**c, a*sinh(b)**c, S.One, S.One), + (a*coth(b)**c*sinh(b)**c, a*cosh(b)**c, S.One, S.One), + (a*tanh(b)**c/sinh(b)**c, a/cosh(b)**c, S.One, S.One), + (a*coth(b)**c/cosh(b)**c, a/sinh(b)**c, S.One, S.One), + (a*coth(b)**c*tanh(b)**c, a, S.One, S.One), + + (c*(tanh(a) + tanh(b))/(1 + tanh(a)*tanh(b)), + tanh(a + b)*c, S.One, S.One), + ) + + matchers_add = ( + (c*sin(a)*cos(b) + c*cos(a)*sin(b) + d, sin(a + b)*c + d), + (c*cos(a)*cos(b) - c*sin(a)*sin(b) + d, cos(a + b)*c + d), + (c*sin(a)*cos(b) - c*cos(a)*sin(b) + d, sin(a - b)*c + d), + (c*cos(a)*cos(b) + c*sin(a)*sin(b) + d, cos(a - b)*c + d), + (c*sinh(a)*cosh(b) + c*sinh(b)*cosh(a) + d, sinh(a + b)*c + d), + (c*cosh(a)*cosh(b) + c*sinh(a)*sinh(b) + d, cosh(a + b)*c + d), + ) + + # for cos(x)**2 + sin(x)**2 -> 1 + matchers_identity = ( + (a*sin(b)**2, a - a*cos(b)**2), + (a*tan(b)**2, a*(1/cos(b))**2 - a), + (a*cot(b)**2, a*(1/sin(b))**2 - a), + (a*sin(b + c), a*(sin(b)*cos(c) + sin(c)*cos(b))), + (a*cos(b + c), a*(cos(b)*cos(c) - sin(b)*sin(c))), + (a*tan(b + c), a*((tan(b) + tan(c))/(1 - tan(b)*tan(c)))), + + (a*sinh(b)**2, a*cosh(b)**2 - a), + (a*tanh(b)**2, a - a*(1/cosh(b))**2), + (a*coth(b)**2, a + a*(1/sinh(b))**2), + (a*sinh(b + c), a*(sinh(b)*cosh(c) + sinh(c)*cosh(b))), + (a*cosh(b + c), a*(cosh(b)*cosh(c) + sinh(b)*sinh(c))), + (a*tanh(b + c), a*((tanh(b) + tanh(c))/(1 + tanh(b)*tanh(c)))), + + ) + + # Reduce any lingering artifacts, such as sin(x)**2 changing + # to 1-cos(x)**2 when sin(x)**2 was "simpler" + artifacts = ( + (a - a*cos(b)**2 + c, a*sin(b)**2 + c, cos), + (a - a*(1/cos(b))**2 + c, -a*tan(b)**2 + c, cos), + (a - a*(1/sin(b))**2 + c, -a*cot(b)**2 + c, sin), + + (a - a*cosh(b)**2 + c, -a*sinh(b)**2 + c, cosh), + (a - a*(1/cosh(b))**2 + c, a*tanh(b)**2 + c, cosh), + (a + a*(1/sinh(b))**2 + c, a*coth(b)**2 + c, sinh), + + # same as above but with noncommutative prefactor + (a*d - a*d*cos(b)**2 + c, a*d*sin(b)**2 + c, cos), + (a*d - a*d*(1/cos(b))**2 + c, -a*d*tan(b)**2 + c, cos), + (a*d - a*d*(1/sin(b))**2 + c, -a*d*cot(b)**2 + c, sin), + + (a*d - a*d*cosh(b)**2 + c, -a*d*sinh(b)**2 + c, cosh), + (a*d - a*d*(1/cosh(b))**2 + c, a*d*tanh(b)**2 + c, cosh), + (a*d + a*d*(1/sinh(b))**2 + c, a*d*coth(b)**2 + c, sinh), + ) + + _trigpat = (a, b, c, d, matchers_division, matchers_add, + matchers_identity, artifacts) + return _trigpat + + +def _replace_mul_fpowxgpow(expr, f, g, rexp, h, rexph): + """Helper for _match_div_rewrite. + + Replace f(b_)**c_*g(b_)**(rexp(c_)) with h(b)**rexph(c) if f(b_) + and g(b_) are both positive or if c_ is an integer. + """ + # assert expr.is_Mul and expr.is_commutative and f != g + fargs = defaultdict(int) + gargs = defaultdict(int) + args = [] + for x in expr.args: + if x.is_Pow or x.func in (f, g): + b, e = x.as_base_exp() + if b.is_positive or e.is_integer: + if b.func == f: + fargs[b.args[0]] += e + continue + elif b.func == g: + gargs[b.args[0]] += e + continue + args.append(x) + common = set(fargs) & set(gargs) + hit = False + while common: + key = common.pop() + fe = fargs.pop(key) + ge = gargs.pop(key) + if fe == rexp(ge): + args.append(h(key)**rexph(fe)) + hit = True + else: + fargs[key] = fe + gargs[key] = ge + if not hit: + return expr + while fargs: + key, e = fargs.popitem() + args.append(f(key)**e) + while gargs: + key, e = gargs.popitem() + args.append(g(key)**e) + return Mul(*args) + + +_idn = lambda x: x +_midn = lambda x: -x +_one = lambda x: S.One + +def _match_div_rewrite(expr, i): + """helper for __trigsimp""" + if i == 0: + expr = _replace_mul_fpowxgpow(expr, sin, cos, + _midn, tan, _idn) + elif i == 1: + expr = _replace_mul_fpowxgpow(expr, tan, cos, + _idn, sin, _idn) + elif i == 2: + expr = _replace_mul_fpowxgpow(expr, cot, sin, + _idn, cos, _idn) + elif i == 3: + expr = _replace_mul_fpowxgpow(expr, tan, sin, + _midn, cos, _midn) + elif i == 4: + expr = _replace_mul_fpowxgpow(expr, cot, cos, + _midn, sin, _midn) + elif i == 5: + expr = _replace_mul_fpowxgpow(expr, cot, tan, + _idn, _one, _idn) + # i in (6, 7) is skipped + elif i == 8: + expr = _replace_mul_fpowxgpow(expr, sinh, cosh, + _midn, tanh, _idn) + elif i == 9: + expr = _replace_mul_fpowxgpow(expr, tanh, cosh, + _idn, sinh, _idn) + elif i == 10: + expr = _replace_mul_fpowxgpow(expr, coth, sinh, + _idn, cosh, _idn) + elif i == 11: + expr = _replace_mul_fpowxgpow(expr, tanh, sinh, + _midn, cosh, _midn) + elif i == 12: + expr = _replace_mul_fpowxgpow(expr, coth, cosh, + _midn, sinh, _midn) + elif i == 13: + expr = _replace_mul_fpowxgpow(expr, coth, tanh, + _idn, _one, _idn) + else: + return None + return expr + + +def _trigsimp(expr, deep=False): + # protect the cache from non-trig patterns; we only allow + # trig patterns to enter the cache + if expr.has(*_trigs): + return __trigsimp(expr, deep) + return expr + + +@cacheit +def __trigsimp(expr, deep=False): + """recursive helper for trigsimp""" + from sympy.simplify.fu import TR10i + + if _trigpat is None: + _trigpats() + a, b, c, d, matchers_division, matchers_add, \ + matchers_identity, artifacts = _trigpat + + if expr.is_Mul: + # do some simplifications like sin/cos -> tan: + if not expr.is_commutative: + com, nc = expr.args_cnc() + expr = _trigsimp(Mul._from_args(com), deep)*Mul._from_args(nc) + else: + for i, (pattern, simp, ok1, ok2) in enumerate(matchers_division): + if not _dotrig(expr, pattern): + continue + + newexpr = _match_div_rewrite(expr, i) + if newexpr is not None: + if newexpr != expr: + expr = newexpr + break + else: + continue + + # use SymPy matching instead + res = expr.match(pattern) + if res and res.get(c, 0): + if not res[c].is_integer: + ok = ok1.subs(res) + if not ok.is_positive: + continue + ok = ok2.subs(res) + if not ok.is_positive: + continue + # if "a" contains any of trig or hyperbolic funcs with + # argument "b" then skip the simplification + if any(w.args[0] == res[b] for w in res[a].atoms( + TrigonometricFunction, HyperbolicFunction)): + continue + # simplify and finish: + expr = simp.subs(res) + break # process below + + if expr.is_Add: + args = [] + for term in expr.args: + if not term.is_commutative: + com, nc = term.args_cnc() + nc = Mul._from_args(nc) + term = Mul._from_args(com) + else: + nc = S.One + term = _trigsimp(term, deep) + for pattern, result in matchers_identity: + res = term.match(pattern) + if res is not None: + term = result.subs(res) + break + args.append(term*nc) + if args != expr.args: + expr = Add(*args) + expr = min(expr, expand(expr), key=count_ops) + if expr.is_Add: + for pattern, result in matchers_add: + if not _dotrig(expr, pattern): + continue + expr = TR10i(expr) + if expr.has(HyperbolicFunction): + res = expr.match(pattern) + # if "d" contains any trig or hyperbolic funcs with + # argument "a" or "b" then skip the simplification; + # this isn't perfect -- see tests + if res is None or not (a in res and b in res) or any( + w.args[0] in (res[a], res[b]) for w in res[d].atoms( + TrigonometricFunction, HyperbolicFunction)): + continue + expr = result.subs(res) + break + + # Reduce any lingering artifacts, such as sin(x)**2 changing + # to 1 - cos(x)**2 when sin(x)**2 was "simpler" + for pattern, result, ex in artifacts: + if not _dotrig(expr, pattern): + continue + # Substitute a new wild that excludes some function(s) + # to help influence a better match. This is because + # sometimes, for example, 'a' would match sec(x)**2 + a_t = Wild('a', exclude=[ex]) + pattern = pattern.subs(a, a_t) + result = result.subs(a, a_t) + + m = expr.match(pattern) + was = None + while m and was != expr: + was = expr + if m[a_t] == 0 or \ + -m[a_t] in m[c].args or m[a_t] + m[c] == 0: + break + if d in m and m[a_t]*m[d] + m[c] == 0: + break + expr = result.subs(m) + m = expr.match(pattern) + m.setdefault(c, S.Zero) + + elif expr.is_Mul or expr.is_Pow or deep and expr.args: + expr = expr.func(*[_trigsimp(a, deep) for a in expr.args]) + + try: + if not expr.has(*_trigs): + raise TypeError + e = expr.atoms(exp) + new = expr.rewrite(exp, deep=deep) + if new == e: + raise TypeError + fnew = factor(new) + if fnew != new: + new = min([new, factor(new)], key=count_ops) + # if all exp that were introduced disappeared then accept it + if not (new.atoms(exp) - e): + expr = new + except TypeError: + pass + + return expr +#------------------- end of old trigsimp routines -------------------- + + +def futrig(e, *, hyper=True, **kwargs): + """Return simplified ``e`` using Fu-like transformations. + This is not the "Fu" algorithm. This is called by default + from ``trigsimp``. By default, hyperbolics subexpressions + will be simplified, but this can be disabled by setting + ``hyper=False``. + + Examples + ======== + + >>> from sympy import trigsimp, tan, sinh, tanh + >>> from sympy.simplify.trigsimp import futrig + >>> from sympy.abc import x + >>> trigsimp(1/tan(x)**2) + tan(x)**(-2) + + >>> futrig(sinh(x)/tanh(x)) + cosh(x) + + """ + from sympy.simplify.fu import hyper_as_trig + + e = sympify(e) + + if not isinstance(e, Basic): + return e + + if not e.args: + return e + + old = e + e = bottom_up(e, _futrig) + + if hyper and e.has(HyperbolicFunction): + e, f = hyper_as_trig(e) + e = f(bottom_up(e, _futrig)) + + if e != old and e.is_Mul and e.args[0].is_Rational: + # redistribute leading coeff on 2-arg Add + e = Mul(*e.as_coeff_Mul()) + return e + + +def _futrig(e): + """Helper for futrig.""" + from sympy.simplify.fu import ( + TR1, TR2, TR3, TR2i, TR10, L, TR10i, + TR8, TR6, TR15, TR16, TR111, TR5, TRmorrie, TR11, _TR11, TR14, TR22, + TR12) + + if not e.has(TrigonometricFunction): + return e + + if e.is_Mul: + coeff, e = e.as_independent(TrigonometricFunction) + else: + coeff = None + + Lops = lambda x: (L(x), x.count_ops(), _nodes(x), len(x.args), x.is_Add) + trigs = lambda x: x.has(TrigonometricFunction) + + tree = [identity, + ( + TR3, # canonical angles + TR1, # sec-csc -> cos-sin + TR12, # expand tan of sum + lambda x: _eapply(factor, x, trigs), + TR2, # tan-cot -> sin-cos + [identity, lambda x: _eapply(_mexpand, x, trigs)], + TR2i, # sin-cos ratio -> tan + lambda x: _eapply(lambda i: factor(i.normal()), x, trigs), + TR14, # factored identities + TR5, # sin-pow -> cos_pow + TR10, # sin-cos of sums -> sin-cos prod + TR11, _TR11, TR6, # reduce double angles and rewrite cos pows + lambda x: _eapply(factor, x, trigs), + TR14, # factored powers of identities + [identity, lambda x: _eapply(_mexpand, x, trigs)], + TR10i, # sin-cos products > sin-cos of sums + TRmorrie, + [identity, TR8], # sin-cos products -> sin-cos of sums + [identity, lambda x: TR2i(TR2(x))], # tan -> sin-cos -> tan + [ + lambda x: _eapply(expand_mul, TR5(x), trigs), + lambda x: _eapply( + expand_mul, TR15(x), trigs)], # pos/neg powers of sin + [ + lambda x: _eapply(expand_mul, TR6(x), trigs), + lambda x: _eapply( + expand_mul, TR16(x), trigs)], # pos/neg powers of cos + TR111, # tan, sin, cos to neg power -> cot, csc, sec + [identity, TR2i], # sin-cos ratio to tan + [identity, lambda x: _eapply( + expand_mul, TR22(x), trigs)], # tan-cot to sec-csc + TR1, TR2, TR2i, + [identity, lambda x: _eapply( + factor_terms, TR12(x), trigs)], # expand tan of sum + )] + e = greedy(tree, objective=Lops)(e) + + if coeff is not None: + e = coeff * e + + return e + + +def _is_Expr(e): + """_eapply helper to tell whether ``e`` and all its args + are Exprs.""" + if isinstance(e, Derivative): + return _is_Expr(e.expr) + if not isinstance(e, Expr): + return False + return all(_is_Expr(i) for i in e.args) + + +def _eapply(func, e, cond=None): + """Apply ``func`` to ``e`` if all args are Exprs else only + apply it to those args that *are* Exprs.""" + if not isinstance(e, Expr): + return e + if _is_Expr(e) or not e.args: + return func(e) + return e.func(*[ + _eapply(func, ei) if (cond is None or cond(ei)) else ei + for ei in e.args]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..02cfb35a765c748e70ecc36dd78d8d4432118c64 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/__init__.py @@ -0,0 +1,75 @@ +"""A module for solving all kinds of equations. + + Examples + ======== + + >>> from sympy.solvers import solve + >>> from sympy.abc import x + >>> solve(((x + 1)**5).expand(), x) + [-1] +""" +from sympy.core.assumptions import check_assumptions, failing_assumptions + +from .solvers import solve, solve_linear_system, solve_linear_system_LU, \ + solve_undetermined_coeffs, nsolve, solve_linear, checksol, \ + det_quick, inv_quick + +from sympy.solvers.diophantine.diophantine import diophantine + +from .recurr import rsolve, rsolve_poly, rsolve_ratio, rsolve_hyper + +from .ode import checkodesol, classify_ode, dsolve, \ + homogeneous_order + +from .polysys import solve_poly_system, solve_triangulated, factor_system + +from .pde import pde_separate, pde_separate_add, pde_separate_mul, \ + pdsolve, classify_pde, checkpdesol + +from .deutils import ode_order + +from .inequalities import reduce_inequalities, reduce_abs_inequality, \ + reduce_abs_inequalities, solve_poly_inequality, solve_rational_inequalities, solve_univariate_inequality + +from .decompogen import decompogen + +from .solveset import solveset, linsolve, linear_eq_to_matrix, nonlinsolve, substitution + +from .simplex import lpmin, lpmax, linprog + +# This is here instead of sympy/sets/__init__.py to avoid circular import issues +from ..core.singleton import S +Complexes = S.Complexes + +__all__ = [ + 'solve', 'solve_linear_system', 'solve_linear_system_LU', + 'solve_undetermined_coeffs', 'nsolve', 'solve_linear', 'checksol', + 'det_quick', 'inv_quick', 'check_assumptions', 'failing_assumptions', + + 'diophantine', + + 'rsolve', 'rsolve_poly', 'rsolve_ratio', 'rsolve_hyper', + + 'checkodesol', 'classify_ode', 'dsolve', 'homogeneous_order', + + 'solve_poly_system', 'solve_triangulated', 'factor_system', + + 'pde_separate', 'pde_separate_add', 'pde_separate_mul', 'pdsolve', + 'classify_pde', 'checkpdesol', + + 'ode_order', + + 'reduce_inequalities', 'reduce_abs_inequality', 'reduce_abs_inequalities', + 'solve_poly_inequality', 'solve_rational_inequalities', + 'solve_univariate_inequality', + + 'decompogen', + + 'solveset', 'linsolve', 'linear_eq_to_matrix', 'nonlinsolve', + 'substitution', + + # This is here instead of sympy/sets/__init__.py to avoid circular import issues + 'Complexes', + + 'lpmin', 'lpmax', 'linprog' +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/benchmarks/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/benchmarks/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/benchmarks/bench_solvers.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/benchmarks/bench_solvers.py new file mode 100644 index 0000000000000000000000000000000000000000..d18102873f7efcde1d111e0e8eca12e208f94663 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/benchmarks/bench_solvers.py @@ -0,0 +1,12 @@ +from sympy.core.symbol import Symbol +from sympy.matrices.dense import (eye, zeros) +from sympy.solvers.solvers import solve_linear_system + +N = 8 +M = zeros(N, N + 1) +M[:, :N] = eye(N) +S = [Symbol('A%i' % i) for i in range(N)] + + +def timeit_linsolve_trivial(): + solve_linear_system(M, *S) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/bivariate.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/bivariate.py new file mode 100644 index 0000000000000000000000000000000000000000..72f8e266a16634fa65366e1058543dfe2171ba1c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/bivariate.py @@ -0,0 +1,509 @@ +from sympy.core.add import Add +from sympy.core.exprtools import factor_terms +from sympy.core.function import expand_log, _mexpand +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.sorting import ordered +from sympy.core.symbol import Dummy +from sympy.functions.elementary.exponential import (LambertW, exp, log) +from sympy.functions.elementary.miscellaneous import root +from sympy.polys.polyroots import roots +from sympy.polys.polytools import Poly, factor +from sympy.simplify.simplify import separatevars +from sympy.simplify.radsimp import collect +from sympy.simplify.simplify import powsimp +from sympy.solvers.solvers import solve, _invert +from sympy.utilities.iterables import uniq + + +def _filtered_gens(poly, symbol): + """process the generators of ``poly``, returning the set of generators that + have ``symbol``. If there are two generators that are inverses of each other, + prefer the one that has no denominator. + + Examples + ======== + + >>> from sympy.solvers.bivariate import _filtered_gens + >>> from sympy import Poly, exp + >>> from sympy.abc import x + >>> _filtered_gens(Poly(x + 1/x + exp(x)), x) + {x, exp(x)} + + """ + # TODO it would be good to pick the smallest divisible power + # instead of the base for something like x**4 + x**2 --> + # return x**2 not x + gens = {g for g in poly.gens if symbol in g.free_symbols} + for g in list(gens): + ag = 1/g + if g in gens and ag in gens: + if ag.as_numer_denom()[1] is not S.One: + g = ag + gens.remove(g) + return gens + + +def _mostfunc(lhs, func, X=None): + """Returns the term in lhs which contains the most of the + func-type things e.g. log(log(x)) wins over log(x) if both terms appear. + + ``func`` can be a function (exp, log, etc...) or any other SymPy object, + like Pow. + + If ``X`` is not ``None``, then the function returns the term composed with the + most ``func`` having the specified variable. + + Examples + ======== + + >>> from sympy.solvers.bivariate import _mostfunc + >>> from sympy import exp + >>> from sympy.abc import x, y + >>> _mostfunc(exp(x) + exp(exp(x) + 2), exp) + exp(exp(x) + 2) + >>> _mostfunc(exp(x) + exp(exp(y) + 2), exp) + exp(exp(y) + 2) + >>> _mostfunc(exp(x) + exp(exp(y) + 2), exp, x) + exp(x) + >>> _mostfunc(x, exp, x) is None + True + >>> _mostfunc(exp(x) + exp(x*y), exp, x) + exp(x) + """ + fterms = [tmp for tmp in lhs.atoms(func) if (not X or + X.is_Symbol and X in tmp.free_symbols or + not X.is_Symbol and tmp.has(X))] + if len(fterms) == 1: + return fterms[0] + elif fterms: + return max(list(ordered(fterms)), key=lambda x: x.count(func)) + return None + + +def _linab(arg, symbol): + """Return ``a, b, X`` assuming ``arg`` can be written as ``a*X + b`` + where ``X`` is a symbol-dependent factor and ``a`` and ``b`` are + independent of ``symbol``. + + Examples + ======== + + >>> from sympy.solvers.bivariate import _linab + >>> from sympy.abc import x, y + >>> from sympy import exp, S + >>> _linab(S(2), x) + (2, 0, 1) + >>> _linab(2*x, x) + (2, 0, x) + >>> _linab(y + y*x + 2*x, x) + (y + 2, y, x) + >>> _linab(3 + 2*exp(x), x) + (2, 3, exp(x)) + """ + arg = factor_terms(arg.expand()) + ind, dep = arg.as_independent(symbol) + if arg.is_Mul and dep.is_Add: + a, b, x = _linab(dep, symbol) + return ind*a, ind*b, x + if not arg.is_Add: + b = 0 + a, x = ind, dep + else: + b = ind + a, x = separatevars(dep).as_independent(symbol, as_Add=False) + if x.could_extract_minus_sign(): + a = -a + x = -x + return a, b, x + + +def _lambert(eq, x): + """ + Given an expression assumed to be in the form + ``F(X, a..f) = a*log(b*X + c) + d*X + f = 0`` + where X = g(x) and x = g^-1(X), return the Lambert solution, + ``x = g^-1(-c/b + (a/d)*W(d/(a*b)*exp(c*d/a/b)*exp(-f/a)))``. + """ + eq = _mexpand(expand_log(eq)) + mainlog = _mostfunc(eq, log, x) + if not mainlog: + return [] # violated assumptions + other = eq.subs(mainlog, 0) + if isinstance(-other, log): + eq = (eq - other).subs(mainlog, mainlog.args[0]) + mainlog = mainlog.args[0] + if not isinstance(mainlog, log): + return [] # violated assumptions + other = -(-other).args[0] + eq += other + if x not in other.free_symbols: + return [] # violated assumptions + d, f, X2 = _linab(other, x) + logterm = collect(eq - other, mainlog) + a = logterm.as_coefficient(mainlog) + if a is None or x in a.free_symbols: + return [] # violated assumptions + logarg = mainlog.args[0] + b, c, X1 = _linab(logarg, x) + if X1 != X2: + return [] # violated assumptions + + # invert the generator X1 so we have x(u) + u = Dummy('rhs') + xusolns = solve(X1 - u, x) + + # There are infinitely many branches for LambertW + # but only branches for k = -1 and 0 might be real. The k = 0 + # branch is real and the k = -1 branch is real if the LambertW argument + # in in range [-1/e, 0]. Since `solve` does not return infinite + # solutions we will only include the -1 branch if it tests as real. + # Otherwise, inclusion of any LambertW in the solution indicates to + # the user that there are imaginary solutions corresponding to + # different k values. + lambert_real_branches = [-1, 0] + sol = [] + + # solution of the given Lambert equation is like + # sol = -c/b + (a/d)*LambertW(arg, k), + # where arg = d/(a*b)*exp((c*d-b*f)/a/b) and k in lambert_real_branches. + # Instead of considering the single arg, `d/(a*b)*exp((c*d-b*f)/a/b)`, + # the individual `p` roots obtained when writing `exp((c*d-b*f)/a/b)` + # as `exp(A/p) = exp(A)**(1/p)`, where `p` is an Integer, are used. + + # calculating args for LambertW + num, den = ((c*d-b*f)/a/b).as_numer_denom() + p, den = den.as_coeff_Mul() + e = exp(num/den) + t = Dummy('t') + args = [d/(a*b)*t for t in roots(t**p - e, t).keys()] + + # calculating solutions from args + for arg in args: + for k in lambert_real_branches: + w = LambertW(arg, k) + if k and not w.is_real: + continue + rhs = -c/b + (a/d)*w + + sol.extend(xu.subs(u, rhs) for xu in xusolns) + return sol + + +def _solve_lambert(f, symbol, gens): + """Return solution to ``f`` if it is a Lambert-type expression + else raise NotImplementedError. + + For ``f(X, a..f) = a*log(b*X + c) + d*X - f = 0`` the solution + for ``X`` is ``X = -c/b + (a/d)*W(d/(a*b)*exp(c*d/a/b)*exp(f/a))``. + There are a variety of forms for `f(X, a..f)` as enumerated below: + + 1a1) + if B**B = R for R not in [0, 1] (since those cases would already + be solved before getting here) then log of both sides gives + log(B) + log(log(B)) = log(log(R)) and + X = log(B), a = 1, b = 1, c = 0, d = 1, f = log(log(R)) + 1a2) + if B*(b*log(B) + c)**a = R then log of both sides gives + log(B) + a*log(b*log(B) + c) = log(R) and + X = log(B), d=1, f=log(R) + 1b) + if a*log(b*B + c) + d*B = R and + X = B, f = R + 2a) + if (b*B + c)*exp(d*B + g) = R then log of both sides gives + log(b*B + c) + d*B + g = log(R) and + X = B, a = 1, f = log(R) - g + 2b) + if g*exp(d*B + h) - b*B = c then the log form is + log(g) + d*B + h - log(b*B + c) = 0 and + X = B, a = -1, f = -h - log(g) + 3) + if d*p**(a*B + g) - b*B = c then the log form is + log(d) + (a*B + g)*log(p) - log(b*B + c) = 0 and + X = B, a = -1, d = a*log(p), f = -log(d) - g*log(p) + """ + + def _solve_even_degree_expr(expr, t, symbol): + """Return the unique solutions of equations derived from + ``expr`` by replacing ``t`` with ``+/- symbol``. + + Parameters + ========== + + expr : Expr + The expression which includes a dummy variable t to be + replaced with +symbol and -symbol. + + symbol : Symbol + The symbol for which a solution is being sought. + + Returns + ======= + + List of unique solution of the two equations generated by + replacing ``t`` with positive and negative ``symbol``. + + Notes + ===== + + If ``expr = 2*log(t) + x/2` then solutions for + ``2*log(x) + x/2 = 0`` and ``2*log(-x) + x/2 = 0`` are + returned by this function. Though this may seem + counter-intuitive, one must note that the ``expr`` being + solved here has been derived from a different expression. For + an expression like ``eq = x**2*g(x) = 1``, if we take the + log of both sides we obtain ``log(x**2) + log(g(x)) = 0``. If + x is positive then this simplifies to + ``2*log(x) + log(g(x)) = 0``; the Lambert-solving routines will + return solutions for this, but we must also consider the + solutions for ``2*log(-x) + log(g(x))`` since those must also + be a solution of ``eq`` which has the same value when the ``x`` + in ``x**2`` is negated. If `g(x)` does not have even powers of + symbol then we do not want to replace the ``x`` there with + ``-x``. So the role of the ``t`` in the expression received by + this function is to mark where ``+/-x`` should be inserted + before obtaining the Lambert solutions. + + """ + nlhs, plhs = [ + expr.xreplace({t: sgn*symbol}) for sgn in (-1, 1)] + sols = _solve_lambert(nlhs, symbol, gens) + if plhs != nlhs: + sols.extend(_solve_lambert(plhs, symbol, gens)) + # uniq is needed for a case like + # 2*log(t) - log(-z**2) + log(z + log(x) + log(z)) + # where substituting t with +/-x gives all the same solution; + # uniq, rather than list(set()), is used to maintain canonical + # order + return list(uniq(sols)) + + nrhs, lhs = f.as_independent(symbol, as_Add=True) + rhs = -nrhs + + lamcheck = [tmp for tmp in gens + if (tmp.func in [exp, log] or + (tmp.is_Pow and symbol in tmp.exp.free_symbols))] + if not lamcheck: + raise NotImplementedError() + + if lhs.is_Add or lhs.is_Mul: + # replacing all even_degrees of symbol with dummy variable t + # since these will need special handling; non-Add/Mul do not + # need this handling + t = Dummy('t', **symbol.assumptions0) + lhs = lhs.replace( + lambda i: # find symbol**even + i.is_Pow and i.base == symbol and i.exp.is_even, + lambda i: # replace t**even + t**i.exp) + + if lhs.is_Add and lhs.has(t): + t_indep = lhs.subs(t, 0) + t_term = lhs - t_indep + _rhs = rhs - t_indep + if not t_term.is_Add and _rhs and not ( + t_term.has(S.ComplexInfinity, S.NaN)): + eq = expand_log(log(t_term) - log(_rhs)) + return _solve_even_degree_expr(eq, t, symbol) + elif lhs.is_Mul and rhs: + # this needs to happen whether t is present or not + lhs = expand_log(log(lhs), force=True) + rhs = log(rhs) + if lhs.has(t) and lhs.is_Add: + # it expanded from Mul to Add + eq = lhs - rhs + return _solve_even_degree_expr(eq, t, symbol) + + # restore symbol in lhs + lhs = lhs.xreplace({t: symbol}) + + lhs = powsimp(factor(lhs, deep=True)) + + # make sure we have inverted as completely as possible + r = Dummy() + i, lhs = _invert(lhs - r, symbol) + rhs = i.xreplace({r: rhs}) + + # For the first forms: + # + # 1a1) B**B = R will arrive here as B*log(B) = log(R) + # lhs is Mul so take log of both sides: + # log(B) + log(log(B)) = log(log(R)) + # 1a2) B*(b*log(B) + c)**a = R will arrive unchanged so + # lhs is Mul, so take log of both sides: + # log(B) + a*log(b*log(B) + c) = log(R) + # 1b) d*log(a*B + b) + c*B = R will arrive unchanged so + # lhs is Add, so isolate c*B and expand log of both sides: + # log(c) + log(B) = log(R - d*log(a*B + b)) + + soln = [] + if not soln: + mainlog = _mostfunc(lhs, log, symbol) + if mainlog: + if lhs.is_Mul and rhs != 0: + soln = _lambert(log(lhs) - log(rhs), symbol) + elif lhs.is_Add: + other = lhs.subs(mainlog, 0) + if other and not other.is_Add and [ + tmp for tmp in other.atoms(Pow) + if symbol in tmp.free_symbols]: + if not rhs: + diff = log(other) - log(other - lhs) + else: + diff = log(lhs - other) - log(rhs - other) + soln = _lambert(expand_log(diff), symbol) + else: + #it's ready to go + soln = _lambert(lhs - rhs, symbol) + + # For the next forms, + # + # collect on main exp + # 2a) (b*B + c)*exp(d*B + g) = R + # lhs is mul, so take log of both sides: + # log(b*B + c) + d*B = log(R) - g + # 2b) g*exp(d*B + h) - b*B = R + # lhs is add, so add b*B to both sides, + # take the log of both sides and rearrange to give + # log(R + b*B) - d*B = log(g) + h + + if not soln: + mainexp = _mostfunc(lhs, exp, symbol) + if mainexp: + lhs = collect(lhs, mainexp) + if lhs.is_Mul and rhs != 0: + soln = _lambert(expand_log(log(lhs) - log(rhs)), symbol) + elif lhs.is_Add: + # move all but mainexp-containing term to rhs + other = lhs.subs(mainexp, 0) + mainterm = lhs - other + rhs = rhs - other + if (mainterm.could_extract_minus_sign() and + rhs.could_extract_minus_sign()): + mainterm *= -1 + rhs *= -1 + diff = log(mainterm) - log(rhs) + soln = _lambert(expand_log(diff), symbol) + + # For the last form: + # + # 3) d*p**(a*B + g) - b*B = c + # collect on main pow, add b*B to both sides, + # take log of both sides and rearrange to give + # a*B*log(p) - log(b*B + c) = -log(d) - g*log(p) + if not soln: + mainpow = _mostfunc(lhs, Pow, symbol) + if mainpow and symbol in mainpow.exp.free_symbols: + lhs = collect(lhs, mainpow) + if lhs.is_Mul and rhs != 0: + # b*B = 0 + soln = _lambert(expand_log(log(lhs) - log(rhs)), symbol) + elif lhs.is_Add: + # move all but mainpow-containing term to rhs + other = lhs.subs(mainpow, 0) + mainterm = lhs - other + rhs = rhs - other + diff = log(mainterm) - log(rhs) + soln = _lambert(expand_log(diff), symbol) + + if not soln: + raise NotImplementedError('%s does not appear to have a solution in ' + 'terms of LambertW' % f) + + return list(ordered(soln)) + + +def bivariate_type(f, x, y, *, first=True): + """Given an expression, f, 3 tests will be done to see what type + of composite bivariate it might be, options for u(x, y) are:: + + x*y + x+y + x*y+x + x*y+y + + If it matches one of these types, ``u(x, y)``, ``P(u)`` and dummy + variable ``u`` will be returned. Solving ``P(u)`` for ``u`` and + equating the solutions to ``u(x, y)`` and then solving for ``x`` or + ``y`` is equivalent to solving the original expression for ``x`` or + ``y``. If ``x`` and ``y`` represent two functions in the same + variable, e.g. ``x = g(t)`` and ``y = h(t)``, then if ``u(x, y) - p`` + can be solved for ``t`` then these represent the solutions to + ``P(u) = 0`` when ``p`` are the solutions of ``P(u) = 0``. + + Only positive values of ``u`` are considered. + + Examples + ======== + + >>> from sympy import solve + >>> from sympy.solvers.bivariate import bivariate_type + >>> from sympy.abc import x, y + >>> eq = (x**2 - 3).subs(x, x + y) + >>> bivariate_type(eq, x, y) + (x + y, _u**2 - 3, _u) + >>> uxy, pu, u = _ + >>> usol = solve(pu, u); usol + [sqrt(3)] + >>> [solve(uxy - s) for s in solve(pu, u)] + [[{x: -y + sqrt(3)}]] + >>> all(eq.subs(s).equals(0) for sol in _ for s in sol) + True + + """ + + u = Dummy('u', positive=True) + + if first: + p = Poly(f, x, y) + f = p.as_expr() + _x = Dummy() + _y = Dummy() + rv = bivariate_type(Poly(f.subs({x: _x, y: _y}), _x, _y), _x, _y, first=False) + if rv: + reps = {_x: x, _y: y} + return rv[0].xreplace(reps), rv[1].xreplace(reps), rv[2] + return + + p = f + f = p.as_expr() + + # f(x*y) + args = Add.make_args(p.as_expr()) + new = [] + for a in args: + a = _mexpand(a.subs(x, u/y)) + free = a.free_symbols + if x in free or y in free: + break + new.append(a) + else: + return x*y, Add(*new), u + + def ok(f, v, c): + new = _mexpand(f.subs(v, c)) + free = new.free_symbols + return None if (x in free or y in free) else new + + # f(a*x + b*y) + new = [] + d = p.degree(x) + if p.degree(y) == d: + a = root(p.coeff_monomial(x**d), d) + b = root(p.coeff_monomial(y**d), d) + new = ok(f, x, (u - b*y)/a) + if new is not None: + return a*x + b*y, new, u + + # f(a*x*y + b*y) + new = [] + d = p.degree(x) + if p.degree(y) == d: + for itry in range(2): + a = root(p.coeff_monomial(x**d*y**d), d) + b = root(p.coeff_monomial(y**d), d) + new = ok(f, x, (u - b*y)/a/y) + if new is not None: + return a*x*y + b*y, new, u + x, y = y, x diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/decompogen.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/decompogen.py new file mode 100644 index 0000000000000000000000000000000000000000..ec1b3b683511a34e6f98b9839d112b87517390d8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/decompogen.py @@ -0,0 +1,126 @@ +from sympy.core import (Function, Pow, sympify, Expr) +from sympy.core.relational import Relational +from sympy.core.singleton import S +from sympy.polys import Poly, decompose +from sympy.utilities.misc import func_name +from sympy.functions.elementary.miscellaneous import Min, Max + + +def decompogen(f, symbol): + """ + Computes General functional decomposition of ``f``. + Given an expression ``f``, returns a list ``[f_1, f_2, ..., f_n]``, + where:: + f = f_1 o f_2 o ... f_n = f_1(f_2(... f_n)) + + Note: This is a General decomposition function. It also decomposes + Polynomials. For only Polynomial decomposition see ``decompose`` in polys. + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy import decompogen, sqrt, sin, cos + >>> decompogen(sin(cos(x)), x) + [sin(x), cos(x)] + >>> decompogen(sin(x)**2 + sin(x) + 1, x) + [x**2 + x + 1, sin(x)] + >>> decompogen(sqrt(6*x**2 - 5), x) + [sqrt(x), 6*x**2 - 5] + >>> decompogen(sin(sqrt(cos(x**2 + 1))), x) + [sin(x), sqrt(x), cos(x), x**2 + 1] + >>> decompogen(x**4 + 2*x**3 - x - 1, x) + [x**2 - x - 1, x**2 + x] + + """ + f = sympify(f) + if not isinstance(f, Expr) or isinstance(f, Relational): + raise TypeError('expecting Expr but got: `%s`' % func_name(f)) + if symbol not in f.free_symbols: + return [f] + + + # ===== Simple Functions ===== # + if isinstance(f, (Function, Pow)): + if f.is_Pow and f.base == S.Exp1: + arg = f.exp + else: + arg = f.args[0] + if arg == symbol: + return [f] + return [f.subs(arg, symbol)] + decompogen(arg, symbol) + + # ===== Min/Max Functions ===== # + if isinstance(f, (Min, Max)): + args = list(f.args) + d0 = None + for i, a in enumerate(args): + if not a.has_free(symbol): + continue + d = decompogen(a, symbol) + if len(d) == 1: + d = [symbol] + d + if d0 is None: + d0 = d[1:] + elif d[1:] != d0: + # decomposition is not the same for each arg: + # mark as having no decomposition + d = [symbol] + break + args[i] = d[0] + if d[0] == symbol: + return [f] + return [f.func(*args)] + d0 + + # ===== Convert to Polynomial ===== # + fp = Poly(f) + gens = list(filter(lambda x: symbol in x.free_symbols, fp.gens)) + + if len(gens) == 1 and gens[0] != symbol: + f1 = f.subs(gens[0], symbol) + f2 = gens[0] + return [f1] + decompogen(f2, symbol) + + # ===== Polynomial decompose() ====== # + try: + return decompose(f) + except ValueError: + return [f] + + +def compogen(g_s, symbol): + """ + Returns the composition of functions. + Given a list of functions ``g_s``, returns their composition ``f``, + where: + f = g_1 o g_2 o .. o g_n + + Note: This is a General composition function. It also composes Polynomials. + For only Polynomial composition see ``compose`` in polys. + + Examples + ======== + + >>> from sympy.solvers.decompogen import compogen + >>> from sympy.abc import x + >>> from sympy import sqrt, sin, cos + >>> compogen([sin(x), cos(x)], x) + sin(cos(x)) + >>> compogen([x**2 + x + 1, sin(x)], x) + sin(x)**2 + sin(x) + 1 + >>> compogen([sqrt(x), 6*x**2 - 5], x) + sqrt(6*x**2 - 5) + >>> compogen([sin(x), sqrt(x), cos(x), x**2 + 1], x) + sin(sqrt(cos(x**2 + 1))) + >>> compogen([x**2 - x - 1, x**2 + x], x) + -x**2 - x + (x**2 + x)**2 - 1 + """ + if len(g_s) == 1: + return g_s[0] + + foo = g_s[0].subs(symbol, g_s[1]) + + if len(g_s) == 2: + return foo + + return compogen([foo] + g_s[2:], symbol) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/deutils.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/deutils.py new file mode 100644 index 0000000000000000000000000000000000000000..b13f37b5004fe7bce2545ad2c788ec3feae56025 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/deutils.py @@ -0,0 +1,273 @@ +"""Utility functions for classifying and solving +ordinary and partial differential equations. + +Contains +======== +_preprocess +ode_order +_desolve + +""" +from sympy.core import Pow +from sympy.core.function import Derivative, AppliedUndef +from sympy.core.relational import Equality +from sympy.core.symbol import Wild + +def _preprocess(expr, func=None, hint='_Integral'): + """Prepare expr for solving by making sure that differentiation + is done so that only func remains in unevaluated derivatives and + (if hint does not end with _Integral) that doit is applied to all + other derivatives. If hint is None, do not do any differentiation. + (Currently this may cause some simple differential equations to + fail.) + + In case func is None, an attempt will be made to autodetect the + function to be solved for. + + >>> from sympy.solvers.deutils import _preprocess + >>> from sympy import Derivative, Function + >>> from sympy.abc import x, y, z + >>> f, g = map(Function, 'fg') + + If f(x)**p == 0 and p>0 then we can solve for f(x)=0 + >>> _preprocess((f(x).diff(x)-4)**5, f(x)) + (Derivative(f(x), x) - 4, f(x)) + + Apply doit to derivatives that contain more than the function + of interest: + + >>> _preprocess(Derivative(f(x) + x, x)) + (Derivative(f(x), x) + 1, f(x)) + + Do others if the differentiation variable(s) intersect with those + of the function of interest or contain the function of interest: + + >>> _preprocess(Derivative(g(x), y, z), f(y)) + (0, f(y)) + >>> _preprocess(Derivative(f(y), z), f(y)) + (0, f(y)) + + Do others if the hint does not end in '_Integral' (the default + assumes that it does): + + >>> _preprocess(Derivative(g(x), y), f(x)) + (Derivative(g(x), y), f(x)) + >>> _preprocess(Derivative(f(x), y), f(x), hint='') + (0, f(x)) + + Do not do any derivatives if hint is None: + + >>> eq = Derivative(f(x) + 1, x) + Derivative(f(x), y) + >>> _preprocess(eq, f(x), hint=None) + (Derivative(f(x) + 1, x) + Derivative(f(x), y), f(x)) + + If it's not clear what the function of interest is, it must be given: + + >>> eq = Derivative(f(x) + g(x), x) + >>> _preprocess(eq, g(x)) + (Derivative(f(x), x) + Derivative(g(x), x), g(x)) + >>> try: _preprocess(eq) + ... except ValueError: print("A ValueError was raised.") + A ValueError was raised. + + """ + if isinstance(expr, Pow): + # if f(x)**p=0 then f(x)=0 (p>0) + if (expr.exp).is_positive: + expr = expr.base + derivs = expr.atoms(Derivative) + if not func: + funcs = set().union(*[d.atoms(AppliedUndef) for d in derivs]) + if len(funcs) != 1: + raise ValueError('The function cannot be ' + 'automatically detected for %s.' % expr) + func = funcs.pop() + fvars = set(func.args) + if hint is None: + return expr, func + reps = [(d, d.doit()) for d in derivs if not hint.endswith('_Integral') or + d.has(func) or set(d.variables) & fvars] + eq = expr.subs(reps) + return eq, func + + +def ode_order(expr, func): + """ + Returns the order of a given differential + equation with respect to func. + + This function is implemented recursively. + + Examples + ======== + + >>> from sympy import Function + >>> from sympy.solvers.deutils import ode_order + >>> from sympy.abc import x + >>> f, g = map(Function, ['f', 'g']) + >>> ode_order(f(x).diff(x, 2) + f(x).diff(x)**2 + + ... f(x).diff(x), f(x)) + 2 + >>> ode_order(f(x).diff(x, 2) + g(x).diff(x, 3), f(x)) + 2 + >>> ode_order(f(x).diff(x, 2) + g(x).diff(x, 3), g(x)) + 3 + + """ + a = Wild('a', exclude=[func]) + if expr.match(a): + return 0 + + if isinstance(expr, Derivative): + if expr.args[0] == func: + return len(expr.variables) + else: + args = expr.args[0].args + rv = len(expr.variables) + if args: + rv += max(ode_order(_, func) for _ in args) + return rv + else: + return max(ode_order(_, func) for _ in expr.args) if expr.args else 0 + + +def _desolve(eq, func=None, hint="default", ics=None, simplify=True, *, prep=True, **kwargs): + """This is a helper function to dsolve and pdsolve in the ode + and pde modules. + + If the hint provided to the function is "default", then a dict with + the following keys are returned + + 'func' - It provides the function for which the differential equation + has to be solved. This is useful when the expression has + more than one function in it. + + 'default' - The default key as returned by classifier functions in ode + and pde.py + + 'hint' - The hint given by the user for which the differential equation + is to be solved. If the hint given by the user is 'default', + then the value of 'hint' and 'default' is the same. + + 'order' - The order of the function as returned by ode_order + + 'match' - It returns the match as given by the classifier functions, for + the default hint. + + If the hint provided to the function is not "default" and is not in + ('all', 'all_Integral', 'best'), then a dict with the above mentioned keys + is returned along with the keys which are returned when dict in + classify_ode or classify_pde is set True + + If the hint given is in ('all', 'all_Integral', 'best'), then this function + returns a nested dict, with the keys, being the set of classified hints + returned by classifier functions, and the values being the dict of form + as mentioned above. + + Key 'eq' is a common key to all the above mentioned hints which returns an + expression if eq given by user is an Equality. + + See Also + ======== + classify_ode(ode.py) + classify_pde(pde.py) + """ + if isinstance(eq, Equality): + eq = eq.lhs - eq.rhs + + # preprocess the equation and find func if not given + if prep or func is None: + eq, func = _preprocess(eq, func) + prep = False + + # type is an argument passed by the solve functions in ode and pde.py + # that identifies whether the function caller is an ordinary + # or partial differential equation. Accordingly corresponding + # changes are made in the function. + type = kwargs.get('type', None) + xi = kwargs.get('xi') + eta = kwargs.get('eta') + x0 = kwargs.get('x0', 0) + terms = kwargs.get('n') + + if type == 'ode': + from sympy.solvers.ode import classify_ode, allhints + classifier = classify_ode + string = 'ODE ' + dummy = '' + + elif type == 'pde': + from sympy.solvers.pde import classify_pde, allhints + classifier = classify_pde + string = 'PDE ' + dummy = 'p' + + # Magic that should only be used internally. Prevents classify_ode from + # being called more than it needs to be by passing its results through + # recursive calls. + if kwargs.get('classify', True): + hints = classifier(eq, func, dict=True, ics=ics, xi=xi, eta=eta, + n=terms, x0=x0, hint=hint, prep=prep) + + else: + # Here is what all this means: + # + # hint: The hint method given to _desolve() by the user. + # hints: The dictionary of hints that match the DE, along with other + # information (including the internal pass-through magic). + # default: The default hint to return, the first hint from allhints + # that matches the hint; obtained from classify_ode(). + # match: Dictionary containing the match dictionary for each hint + # (the parts of the DE for solving). When going through the + # hints in "all", this holds the match string for the current + # hint. + # order: The order of the DE, as determined by ode_order(). + hints = kwargs.get('hint', + {'default': hint, + hint: kwargs['match'], + 'order': kwargs['order']}) + if not hints['default']: + # classify_ode will set hints['default'] to None if no hints match. + if hint not in allhints and hint != 'default': + raise ValueError("Hint not recognized: " + hint) + elif hint not in hints['ordered_hints'] and hint != 'default': + raise ValueError(string + str(eq) + " does not match hint " + hint) + # If dsolve can't solve the purely algebraic equation then dsolve will raise + # ValueError + elif hints['order'] == 0: + raise ValueError( + str(eq) + " is not a solvable differential equation in " + str(func)) + else: + raise NotImplementedError(dummy + "solve" + ": Cannot solve " + str(eq)) + if hint == 'default': + return _desolve(eq, func, ics=ics, hint=hints['default'], simplify=simplify, + prep=prep, x0=x0, classify=False, order=hints['order'], + match=hints[hints['default']], xi=xi, eta=eta, n=terms, type=type) + elif hint in ('all', 'all_Integral', 'best'): + retdict = {} + gethints = set(hints) - {'order', 'default', 'ordered_hints'} + if hint == 'all_Integral': + for i in hints: + if i.endswith('_Integral'): + gethints.remove(i.removesuffix('_Integral')) + # special cases + for k in ["1st_homogeneous_coeff_best", "1st_power_series", + "lie_group", "2nd_power_series_ordinary", "2nd_power_series_regular"]: + if k in gethints: + gethints.remove(k) + for i in gethints: + sol = _desolve(eq, func, ics=ics, hint=i, x0=x0, simplify=simplify, prep=prep, + classify=False, n=terms, order=hints['order'], match=hints[i], type=type) + retdict[i] = sol + retdict['all'] = True + retdict['eq'] = eq + return retdict + elif hint not in allhints: # and hint not in ('default', 'ordered_hints'): + raise ValueError("Hint not recognized: " + hint) + elif hint not in hints: + raise ValueError(string + str(eq) + " does not match hint " + hint) + else: + # Key added to identify the hint needed to solve the equation + hints['hint'] = hint + hints.update({'func': func, 'eq': eq}) + return hints diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/diophantine/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/diophantine/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..23c21242208d6f520c130250ecdce43382b9d868 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/diophantine/__init__.py @@ -0,0 +1,5 @@ +from .diophantine import diophantine, classify_diop, diop_solve + +__all__ = [ + 'diophantine', 'classify_diop', 'diop_solve' +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/diophantine/diophantine.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/diophantine/diophantine.py new file mode 100644 index 0000000000000000000000000000000000000000..ffdef6344451c96ed48dff099cf8f02494f4b504 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/diophantine/diophantine.py @@ -0,0 +1,3980 @@ +from __future__ import annotations + +from sympy.core.add import Add +from sympy.core.assumptions import check_assumptions +from sympy.core.containers import Tuple +from sympy.core.exprtools import factor_terms +from sympy.core.function import _mexpand +from sympy.core.mul import Mul +from sympy.core.numbers import Rational, int_valued +from sympy.core.intfunc import igcdex, ilcm, igcd, integer_nthroot, isqrt +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.sorting import default_sort_key, ordered +from sympy.core.symbol import Symbol, symbols +from sympy.core.sympify import _sympify +from sympy.external.gmpy import jacobi, remove, invert, iroot +from sympy.functions.elementary.complexes import sign +from sympy.functions.elementary.integers import floor +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.matrices.dense import MutableDenseMatrix as Matrix +from sympy.ntheory.factor_ import divisors, factorint, perfect_power +from sympy.ntheory.generate import nextprime +from sympy.ntheory.primetest import is_square, isprime +from sympy.ntheory.modular import symmetric_residue +from sympy.ntheory.residue_ntheory import sqrt_mod, sqrt_mod_iter +from sympy.polys.polyerrors import GeneratorsNeeded +from sympy.polys.polytools import Poly, factor_list +from sympy.simplify.simplify import signsimp +from sympy.solvers.solveset import solveset_real +from sympy.utilities import numbered_symbols +from sympy.utilities.misc import as_int, filldedent +from sympy.utilities.iterables import (is_sequence, subsets, permute_signs, + signed_permutations, ordered_partitions) + + +# these are imported with 'from sympy.solvers.diophantine import * +__all__ = ['diophantine', 'classify_diop'] + + +class DiophantineSolutionSet(set): + """ + Container for a set of solutions to a particular diophantine equation. + + The base representation is a set of tuples representing each of the solutions. + + Parameters + ========== + + symbols : list + List of free symbols in the original equation. + parameters: list + List of parameters to be used in the solution. + + Examples + ======== + + Adding solutions: + + >>> from sympy.solvers.diophantine.diophantine import DiophantineSolutionSet + >>> from sympy.abc import x, y, t, u + >>> s1 = DiophantineSolutionSet([x, y], [t, u]) + >>> s1 + set() + >>> s1.add((2, 3)) + >>> s1.add((-1, u)) + >>> s1 + {(-1, u), (2, 3)} + >>> s2 = DiophantineSolutionSet([x, y], [t, u]) + >>> s2.add((3, 4)) + >>> s1.update(*s2) + >>> s1 + {(-1, u), (2, 3), (3, 4)} + + Conversion of solutions into dicts: + + >>> list(s1.dict_iterator()) + [{x: -1, y: u}, {x: 2, y: 3}, {x: 3, y: 4}] + + Substituting values: + + >>> s3 = DiophantineSolutionSet([x, y], [t, u]) + >>> s3.add((t**2, t + u)) + >>> s3 + {(t**2, t + u)} + >>> s3.subs({t: 2, u: 3}) + {(4, 5)} + >>> s3.subs(t, -1) + {(1, u - 1)} + >>> s3.subs(t, 3) + {(9, u + 3)} + + Evaluation at specific values. Positional arguments are given in the same order as the parameters: + + >>> s3(-2, 3) + {(4, 1)} + >>> s3(5) + {(25, u + 5)} + >>> s3(None, 2) + {(t**2, t + 2)} + """ + + def __init__(self, symbols_seq, parameters): + super().__init__() + + if not is_sequence(symbols_seq): + raise ValueError("Symbols must be given as a sequence.") + + if not is_sequence(parameters): + raise ValueError("Parameters must be given as a sequence.") + + self.symbols = tuple(symbols_seq) + self.parameters = tuple(parameters) + + def add(self, solution): + if len(solution) != len(self.symbols): + raise ValueError("Solution should have a length of %s, not %s" % (len(self.symbols), len(solution))) + # make solution canonical wrt sign (i.e. no -x unless x is also present as an arg) + args = set(solution) + for i in range(len(solution)): + x = solution[i] + if not type(x) is int and (-x).is_Symbol and -x not in args: + solution = [_.subs(-x, x) for _ in solution] + super().add(Tuple(*solution)) + + def update(self, *solutions): + for solution in solutions: + self.add(solution) + + def dict_iterator(self): + for solution in ordered(self): + yield dict(zip(self.symbols, solution)) + + def subs(self, *args, **kwargs): + result = DiophantineSolutionSet(self.symbols, self.parameters) + for solution in self: + result.add(solution.subs(*args, **kwargs)) + return result + + def __call__(self, *args): + if len(args) > len(self.parameters): + raise ValueError("Evaluation should have at most %s values, not %s" % (len(self.parameters), len(args))) + rep = {p: v for p, v in zip(self.parameters, args) if v is not None} + return self.subs(rep) + + +class DiophantineEquationType: + """ + Internal representation of a particular diophantine equation type. + + Parameters + ========== + + equation : + The diophantine equation that is being solved. + free_symbols : list (optional) + The symbols being solved for. + + Attributes + ========== + + total_degree : + The maximum of the degrees of all terms in the equation + homogeneous : + Does the equation contain a term of degree 0 + homogeneous_order : + Does the equation contain any coefficient that is in the symbols being solved for + dimension : + The number of symbols being solved for + """ + name: str + + def __init__(self, equation, free_symbols=None): + self.equation = _sympify(equation).expand(force=True) + + if free_symbols is not None: + self.free_symbols = free_symbols + else: + self.free_symbols = list(self.equation.free_symbols) + self.free_symbols.sort(key=default_sort_key) + + if not self.free_symbols: + raise ValueError('equation should have 1 or more free symbols') + + self.coeff = self.equation.as_coefficients_dict() + if not all(int_valued(c) for c in self.coeff.values()): + raise TypeError("Coefficients should be Integers") + + self.total_degree = Poly(self.equation).total_degree() + self.homogeneous = 1 not in self.coeff + self.homogeneous_order = not (set(self.coeff) & set(self.free_symbols)) + self.dimension = len(self.free_symbols) + self._parameters = None + + def matches(self): + """ + Determine whether the given equation can be matched to the particular equation type. + """ + return False + + @property + def n_parameters(self): + return self.dimension + + @property + def parameters(self): + if self._parameters is None: + self._parameters = symbols('t_:%i' % (self.n_parameters,), integer=True) + return self._parameters + + def solve(self, parameters=None, limit=None) -> DiophantineSolutionSet: + raise NotImplementedError('No solver has been written for %s.' % self.name) + + def pre_solve(self, parameters=None): + if not self.matches(): + raise ValueError("This equation does not match the %s equation type." % self.name) + + if parameters is not None: + if len(parameters) != self.n_parameters: + raise ValueError("Expected %s parameter(s) but got %s" % (self.n_parameters, len(parameters))) + + self._parameters = parameters + + +class Univariate(DiophantineEquationType): + """ + Representation of a univariate diophantine equation. + + A univariate diophantine equation is an equation of the form + `a_{0} + a_{1}x + a_{2}x^2 + .. + a_{n}x^n = 0` where `a_{1}, a_{2}, ..a_{n}` are + integer constants and `x` is an integer variable. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import Univariate + >>> from sympy.abc import x + >>> Univariate((x - 2)*(x - 3)**2).solve() # solves equation (x - 2)*(x - 3)**2 == 0 + {(2,), (3,)} + + """ + + name = 'univariate' + + def matches(self): + return self.dimension == 1 + + def solve(self, parameters=None, limit=None): + self.pre_solve(parameters) + + result = DiophantineSolutionSet(self.free_symbols, parameters=self.parameters) + for i in solveset_real(self.equation, self.free_symbols[0]).intersect(S.Integers): + result.add((i,)) + return result + + +class Linear(DiophantineEquationType): + """ + Representation of a linear diophantine equation. + + A linear diophantine equation is an equation of the form `a_{1}x_{1} + + a_{2}x_{2} + .. + a_{n}x_{n} = 0` where `a_{1}, a_{2}, ..a_{n}` are + integer constants and `x_{1}, x_{2}, ..x_{n}` are integer variables. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import Linear + >>> from sympy.abc import x, y, z + >>> l1 = Linear(2*x - 3*y - 5) + >>> l1.matches() # is this equation linear + True + >>> l1.solve() # solves equation 2*x - 3*y - 5 == 0 + {(3*t_0 - 5, 2*t_0 - 5)} + + Here x = -3*t_0 - 5 and y = -2*t_0 - 5 + + >>> Linear(2*x - 3*y - 4*z -3).solve() + {(t_0, 2*t_0 + 4*t_1 + 3, -t_0 - 3*t_1 - 3)} + + """ + + name = 'linear' + + def matches(self): + return self.total_degree == 1 + + def solve(self, parameters=None, limit=None): + self.pre_solve(parameters) + + coeff = self.coeff + var = self.free_symbols + + if 1 in coeff: + # negate coeff[] because input is of the form: ax + by + c == 0 + # but is used as: ax + by == -c + c = -coeff[1] + else: + c = 0 + + result = DiophantineSolutionSet(var, parameters=self.parameters) + params = result.parameters + + if len(var) == 1: + q, r = divmod(c, coeff[var[0]]) + if not r: + result.add((q,)) + return result + + ''' + base_solution_linear() can solve diophantine equations of the form: + + a*x + b*y == c + + We break down multivariate linear diophantine equations into a + series of bivariate linear diophantine equations which can then + be solved individually by base_solution_linear(). + + Consider the following: + + a_0*x_0 + a_1*x_1 + a_2*x_2 == c + + which can be re-written as: + + a_0*x_0 + g_0*y_0 == c + + where + + g_0 == gcd(a_1, a_2) + + and + + y == (a_1*x_1)/g_0 + (a_2*x_2)/g_0 + + This leaves us with two binary linear diophantine equations. + For the first equation: + + a == a_0 + b == g_0 + c == c + + For the second: + + a == a_1/g_0 + b == a_2/g_0 + c == the solution we find for y_0 in the first equation. + + The arrays A and B are the arrays of integers used for + 'a' and 'b' in each of the n-1 bivariate equations we solve. + ''' + + A = [coeff[v] for v in var] + B = [] + if len(var) > 2: + B.append(igcd(A[-2], A[-1])) + A[-2] = A[-2] // B[0] + A[-1] = A[-1] // B[0] + for i in range(len(A) - 3, 0, -1): + gcd = igcd(B[0], A[i]) + B[0] = B[0] // gcd + A[i] = A[i] // gcd + B.insert(0, gcd) + B.append(A[-1]) + + ''' + Consider the trivariate linear equation: + + 4*x_0 + 6*x_1 + 3*x_2 == 2 + + This can be re-written as: + + 4*x_0 + 3*y_0 == 2 + + where + + y_0 == 2*x_1 + x_2 + (Note that gcd(3, 6) == 3) + + The complete integral solution to this equation is: + + x_0 == 2 + 3*t_0 + y_0 == -2 - 4*t_0 + + where 't_0' is any integer. + + Now that we have a solution for 'x_0', find 'x_1' and 'x_2': + + 2*x_1 + x_2 == -2 - 4*t_0 + + We can then solve for '-2' and '-4' independently, + and combine the results: + + 2*x_1a + x_2a == -2 + x_1a == 0 + t_0 + x_2a == -2 - 2*t_0 + + 2*x_1b + x_2b == -4*t_0 + x_1b == 0*t_0 + t_1 + x_2b == -4*t_0 - 2*t_1 + + ==> + + x_1 == t_0 + t_1 + x_2 == -2 - 6*t_0 - 2*t_1 + + where 't_0' and 't_1' are any integers. + + Note that: + + 4*(2 + 3*t_0) + 6*(t_0 + t_1) + 3*(-2 - 6*t_0 - 2*t_1) == 2 + + for any integral values of 't_0', 't_1'; as required. + + This method is generalised for many variables, below. + + ''' + solutions = [] + for Ai, Bi in zip(A, B): + tot_x, tot_y = [], [] + + for arg in Add.make_args(c): + if arg.is_Integer: + # example: 5 -> k = 5 + k, p = arg, S.One + pnew = params[0] + else: # arg is a Mul or Symbol + # example: 3*t_1 -> k = 3 + # example: t_0 -> k = 1 + k, p = arg.as_coeff_Mul() + pnew = params[params.index(p) + 1] + + sol = sol_x, sol_y = base_solution_linear(k, Ai, Bi, pnew) + + if p is S.One: + if None in sol: + return result + else: + # convert a + b*pnew -> a*p + b*pnew + if isinstance(sol_x, Add): + sol_x = sol_x.args[0]*p + sol_x.args[1] + if isinstance(sol_y, Add): + sol_y = sol_y.args[0]*p + sol_y.args[1] + + tot_x.append(sol_x) + tot_y.append(sol_y) + + solutions.append(Add(*tot_x)) + c = Add(*tot_y) + + solutions.append(c) + result.add(solutions) + return result + + +class BinaryQuadratic(DiophantineEquationType): + """ + Representation of a binary quadratic diophantine equation. + + A binary quadratic diophantine equation is an equation of the + form `Ax^2 + Bxy + Cy^2 + Dx + Ey + F = 0`, where `A, B, C, D, E, + F` are integer constants and `x` and `y` are integer variables. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy.solvers.diophantine.diophantine import BinaryQuadratic + >>> b1 = BinaryQuadratic(x**3 + y**2 + 1) + >>> b1.matches() + False + >>> b2 = BinaryQuadratic(x**2 + y**2 + 2*x + 2*y + 2) + >>> b2.matches() + True + >>> b2.solve() + {(-1, -1)} + + References + ========== + + .. [1] Methods to solve Ax^2 + Bxy + Cy^2 + Dx + Ey + F = 0, [online], + Available: https://www.alpertron.com.ar/METHODS.HTM + .. [2] Solving the equation ax^2+ bxy + cy^2 + dx + ey + f= 0, [online], + Available: https://web.archive.org/web/20160323033111/http://www.jpr2718.org/ax2p.pdf + + """ + + name = 'binary_quadratic' + + def matches(self): + return self.total_degree == 2 and self.dimension == 2 + + def solve(self, parameters=None, limit=None) -> DiophantineSolutionSet: + self.pre_solve(parameters) + + var = self.free_symbols + coeff = self.coeff + + x, y = var + + A = coeff[x**2] + B = coeff[x*y] + C = coeff[y**2] + D = coeff[x] + E = coeff[y] + F = coeff[S.One] + + A, B, C, D, E, F = [as_int(i) for i in _remove_gcd(A, B, C, D, E, F)] + + # (1) Simple-Hyperbolic case: A = C = 0, B != 0 + # In this case equation can be converted to (Bx + E)(By + D) = DE - BF + # We consider two cases; DE - BF = 0 and DE - BF != 0 + # More details, https://www.alpertron.com.ar/METHODS.HTM#SHyperb + + result = DiophantineSolutionSet(var, self.parameters) + t, u = result.parameters + + discr = B**2 - 4*A*C + if A == 0 and C == 0 and B != 0: + + if D*E - B*F == 0: + q, r = divmod(E, B) + if not r: + result.add((-q, t)) + q, r = divmod(D, B) + if not r: + result.add((t, -q)) + else: + div = divisors(D*E - B*F) + div = div + [-term for term in div] + for d in div: + x0, r = divmod(d - E, B) + if not r: + q, r = divmod(D*E - B*F, d) + if not r: + y0, r = divmod(q - D, B) + if not r: + result.add((x0, y0)) + + # (2) Parabolic case: B**2 - 4*A*C = 0 + # There are two subcases to be considered in this case. + # sqrt(c)D - sqrt(a)E = 0 and sqrt(c)D - sqrt(a)E != 0 + # More Details, https://www.alpertron.com.ar/METHODS.HTM#Parabol + + elif discr == 0: + + if A == 0: + s = BinaryQuadratic(self.equation, free_symbols=[y, x]).solve(parameters=[t, u]) + for soln in s: + result.add((soln[1], soln[0])) + + else: + g = sign(A)*igcd(A, C) + a = A // g + c = C // g + e = sign(B / A) + + sqa = isqrt(a) + sqc = isqrt(c) + _c = e*sqc*D - sqa*E + if not _c: + z = Symbol("z", real=True) + eq = sqa*g*z**2 + D*z + sqa*F + roots = solveset_real(eq, z).intersect(S.Integers) + for root in roots: + ans = diop_solve(sqa*x + e*sqc*y - root) + result.add((ans[0], ans[1])) + + elif int_valued(c): + solve_x = lambda u: -e*sqc*g*_c*t**2 - (E + 2*e*sqc*g*u)*t \ + - (e*sqc*g*u**2 + E*u + e*sqc*F) // _c + + solve_y = lambda u: sqa*g*_c*t**2 + (D + 2*sqa*g*u)*t \ + + (sqa*g*u**2 + D*u + sqa*F) // _c + + for z0 in range(0, abs(_c)): + # Check if the coefficients of y and x obtained are integers or not + if (divisible(sqa*g*z0**2 + D*z0 + sqa*F, _c) and + divisible(e*sqc*g*z0**2 + E*z0 + e*sqc*F, _c)): + result.add((solve_x(z0), solve_y(z0))) + + # (3) Method used when B**2 - 4*A*C is a square, is described in p. 6 of the below paper + # by John P. Robertson. + # https://web.archive.org/web/20160323033111/http://www.jpr2718.org/ax2p.pdf + + elif is_square(discr): + if A != 0: + r = sqrt(discr) + u, v = symbols("u, v", integer=True) + eq = _mexpand( + 4*A*r*u*v + 4*A*D*(B*v + r*u + r*v - B*u) + + 2*A*4*A*E*(u - v) + 4*A*r*4*A*F) + + solution = diop_solve(eq, t) + + for s0, t0 in solution: + + num = B*t0 + r*s0 + r*t0 - B*s0 + x_0 = S(num) / (4*A*r) + y_0 = S(s0 - t0) / (2*r) + if isinstance(s0, Symbol) or isinstance(t0, Symbol): + if len(check_param(x_0, y_0, 4*A*r, parameters)) > 0: + ans = check_param(x_0, y_0, 4*A*r, parameters) + result.update(*ans) + elif x_0.is_Integer and y_0.is_Integer: + if is_solution_quad(var, coeff, x_0, y_0): + result.add((x_0, y_0)) + + else: + s = BinaryQuadratic(self.equation, free_symbols=var[::-1]).solve(parameters=[t, u]) # Interchange x and y + while s: + result.add(s.pop()[::-1]) # and solution <--------+ + + # (4) B**2 - 4*A*C > 0 and B**2 - 4*A*C not a square or B**2 - 4*A*C < 0 + + else: + + P, Q = _transformation_to_DN(var, coeff) + D, N = _find_DN(var, coeff) + solns_pell = diop_DN(D, N) + + if D < 0: + for x0, y0 in solns_pell: + for x in [-x0, x0]: + for y in [-y0, y0]: + s = P*Matrix([x, y]) + Q + try: + result.add([as_int(_) for _ in s]) + except ValueError: + pass + else: + # In this case equation can be transformed into a Pell equation + + solns_pell = set(solns_pell) + solns_pell.update((-X, -Y) for X, Y in list(solns_pell)) + + a = diop_DN(D, 1) + T = a[0][0] + U = a[0][1] + + if all(int_valued(_) for _ in P[:4] + Q[:2]): + for r, s in solns_pell: + _a = (r + s*sqrt(D))*(T + U*sqrt(D))**t + _b = (r - s*sqrt(D))*(T - U*sqrt(D))**t + x_n = _mexpand(S(_a + _b) / 2) + y_n = _mexpand(S(_a - _b) / (2*sqrt(D))) + s = P*Matrix([x_n, y_n]) + Q + result.add(s) + + else: + L = ilcm(*[_.q for _ in P[:4] + Q[:2]]) + + k = 1 + + T_k = T + U_k = U + + while (T_k - 1) % L != 0 or U_k % L != 0: + T_k, U_k = T_k*T + D*U_k*U, T_k*U + U_k*T + k += 1 + + for X, Y in solns_pell: + + for i in range(k): + if all(int_valued(_) for _ in P*Matrix([X, Y]) + Q): + _a = (X + sqrt(D)*Y)*(T_k + sqrt(D)*U_k)**t + _b = (X - sqrt(D)*Y)*(T_k - sqrt(D)*U_k)**t + Xt = S(_a + _b) / 2 + Yt = S(_a - _b) / (2*sqrt(D)) + s = P*Matrix([Xt, Yt]) + Q + result.add(s) + + X, Y = X*T + D*U*Y, X*U + Y*T + + return result + + +class InhomogeneousTernaryQuadratic(DiophantineEquationType): + """ + + Representation of an inhomogeneous ternary quadratic. + + No solver is currently implemented for this equation type. + + """ + + name = 'inhomogeneous_ternary_quadratic' + + def matches(self): + if not (self.total_degree == 2 and self.dimension == 3): + return False + if not self.homogeneous: + return False + return not self.homogeneous_order + + +class HomogeneousTernaryQuadraticNormal(DiophantineEquationType): + """ + Representation of a homogeneous ternary quadratic normal diophantine equation. + + Examples + ======== + + >>> from sympy.abc import x, y, z + >>> from sympy.solvers.diophantine.diophantine import HomogeneousTernaryQuadraticNormal + >>> HomogeneousTernaryQuadraticNormal(4*x**2 - 5*y**2 + z**2).solve() + {(1, 2, 4)} + + """ + + name = 'homogeneous_ternary_quadratic_normal' + + def matches(self): + if not (self.total_degree == 2 and self.dimension == 3): + return False + if not self.homogeneous: + return False + if not self.homogeneous_order: + return False + + nonzero = [k for k in self.coeff if self.coeff[k]] + return len(nonzero) == 3 and all(i**2 in nonzero for i in self.free_symbols) + + def solve(self, parameters=None, limit=None) -> DiophantineSolutionSet: + self.pre_solve(parameters) + + var = self.free_symbols + coeff = self.coeff + + x, y, z = var + + a = coeff[x**2] + b = coeff[y**2] + c = coeff[z**2] + + (sqf_of_a, sqf_of_b, sqf_of_c), (a_1, b_1, c_1), (a_2, b_2, c_2) = \ + sqf_normal(a, b, c, steps=True) + + A = -a_2*c_2 + B = -b_2*c_2 + + result = DiophantineSolutionSet(var, parameters=self.parameters) + + # If following two conditions are satisfied then there are no solutions + if A < 0 and B < 0: + return result + + if ( + sqrt_mod(-b_2*c_2, a_2) is None or + sqrt_mod(-c_2*a_2, b_2) is None or + sqrt_mod(-a_2*b_2, c_2) is None): + return result + + z_0, x_0, y_0 = descent(A, B) + + z_0, q = _rational_pq(z_0, abs(c_2)) + x_0 *= q + y_0 *= q + + x_0, y_0, z_0 = _remove_gcd(x_0, y_0, z_0) + + # Holzer reduction + if sign(a) == sign(b): + x_0, y_0, z_0 = holzer(x_0, y_0, z_0, abs(a_2), abs(b_2), abs(c_2)) + elif sign(a) == sign(c): + x_0, z_0, y_0 = holzer(x_0, z_0, y_0, abs(a_2), abs(c_2), abs(b_2)) + else: + y_0, z_0, x_0 = holzer(y_0, z_0, x_0, abs(b_2), abs(c_2), abs(a_2)) + + x_0 = reconstruct(b_1, c_1, x_0) + y_0 = reconstruct(a_1, c_1, y_0) + z_0 = reconstruct(a_1, b_1, z_0) + + sq_lcm = ilcm(sqf_of_a, sqf_of_b, sqf_of_c) + + x_0 = abs(x_0*sq_lcm // sqf_of_a) + y_0 = abs(y_0*sq_lcm // sqf_of_b) + z_0 = abs(z_0*sq_lcm // sqf_of_c) + + result.add(_remove_gcd(x_0, y_0, z_0)) + return result + + +class HomogeneousTernaryQuadratic(DiophantineEquationType): + """ + Representation of a homogeneous ternary quadratic diophantine equation. + + Examples + ======== + + >>> from sympy.abc import x, y, z + >>> from sympy.solvers.diophantine.diophantine import HomogeneousTernaryQuadratic + >>> HomogeneousTernaryQuadratic(x**2 + y**2 - 3*z**2 + x*y).solve() + {(-1, 2, 1)} + >>> HomogeneousTernaryQuadratic(3*x**2 + y**2 - 3*z**2 + 5*x*y + y*z).solve() + {(3, 12, 13)} + + """ + + name = 'homogeneous_ternary_quadratic' + + def matches(self): + if not (self.total_degree == 2 and self.dimension == 3): + return False + if not self.homogeneous: + return False + if not self.homogeneous_order: + return False + + nonzero = [k for k in self.coeff if self.coeff[k]] + return not (len(nonzero) == 3 and all(i**2 in nonzero for i in self.free_symbols)) + + def solve(self, parameters=None, limit=None): + self.pre_solve(parameters) + + _var = self.free_symbols + coeff = self.coeff + + x, y, z = _var + var = [x, y, z] + + # Equations of the form B*x*y + C*z*x + E*y*z = 0 and At least two of the + # coefficients A, B, C are non-zero. + # There are infinitely many solutions for the equation. + # Ex: (0, 0, t), (0, t, 0), (t, 0, 0) + # Equation can be re-written as y*(B*x + E*z) = -C*x*z and we can find rather + # unobvious solutions. Set y = -C and B*x + E*z = x*z. The latter can be solved by + # using methods for binary quadratic diophantine equations. Let's select the + # solution which minimizes |x| + |z| + + result = DiophantineSolutionSet(var, parameters=self.parameters) + + def unpack_sol(sol): + if len(sol) > 0: + return list(sol)[0] + return None, None, None + + if not any(coeff[i**2] for i in var): + if coeff[x*z]: + sols = diophantine(coeff[x*y]*x + coeff[y*z]*z - x*z) + s = min(sols, key=lambda r: abs(r[0]) + abs(r[1])) + result.add(_remove_gcd(s[0], -coeff[x*z], s[1])) + return result + + var[0], var[1] = _var[1], _var[0] + y_0, x_0, z_0 = unpack_sol(_diop_ternary_quadratic(var, coeff)) + if x_0 is not None: + result.add((x_0, y_0, z_0)) + return result + + if coeff[x**2] == 0: + # If the coefficient of x is zero change the variables + if coeff[y**2] == 0: + var[0], var[2] = _var[2], _var[0] + z_0, y_0, x_0 = unpack_sol(_diop_ternary_quadratic(var, coeff)) + + else: + var[0], var[1] = _var[1], _var[0] + y_0, x_0, z_0 = unpack_sol(_diop_ternary_quadratic(var, coeff)) + + else: + if coeff[x*y] or coeff[x*z]: + # Apply the transformation x --> X - (B*y + C*z)/(2*A) + A = coeff[x**2] + B = coeff[x*y] + C = coeff[x*z] + D = coeff[y**2] + E = coeff[y*z] + F = coeff[z**2] + + _coeff = {} + + _coeff[x**2] = 4*A**2 + _coeff[y**2] = 4*A*D - B**2 + _coeff[z**2] = 4*A*F - C**2 + _coeff[y*z] = 4*A*E - 2*B*C + _coeff[x*y] = 0 + _coeff[x*z] = 0 + + x_0, y_0, z_0 = unpack_sol(_diop_ternary_quadratic(var, _coeff)) + + if x_0 is None: + return result + + p, q = _rational_pq(B*y_0 + C*z_0, 2*A) + x_0, y_0, z_0 = x_0*q - p, y_0*q, z_0*q + + elif coeff[z*y] != 0: + if coeff[y**2] == 0: + if coeff[z**2] == 0: + # Equations of the form A*x**2 + E*yz = 0. + A = coeff[x**2] + E = coeff[y*z] + + b, a = _rational_pq(-E, A) + + x_0, y_0, z_0 = b, a, b + + else: + # Ax**2 + E*y*z + F*z**2 = 0 + var[0], var[2] = _var[2], _var[0] + z_0, y_0, x_0 = unpack_sol(_diop_ternary_quadratic(var, coeff)) + + else: + # A*x**2 + D*y**2 + E*y*z + F*z**2 = 0, C may be zero + var[0], var[1] = _var[1], _var[0] + y_0, x_0, z_0 = unpack_sol(_diop_ternary_quadratic(var, coeff)) + + else: + # Ax**2 + D*y**2 + F*z**2 = 0, C may be zero + x_0, y_0, z_0 = unpack_sol(_diop_ternary_quadratic_normal(var, coeff)) + + if x_0 is None: + return result + + result.add(_remove_gcd(x_0, y_0, z_0)) + return result + + +class InhomogeneousGeneralQuadratic(DiophantineEquationType): + """ + + Representation of an inhomogeneous general quadratic. + + No solver is currently implemented for this equation type. + + """ + + name = 'inhomogeneous_general_quadratic' + + def matches(self): + if not (self.total_degree == 2 and self.dimension >= 3): + return False + if not self.homogeneous_order: + return True + # there may be Pow keys like x**2 or Mul keys like x*y + return any(k.is_Mul for k in self.coeff) and not self.homogeneous + + +class HomogeneousGeneralQuadratic(DiophantineEquationType): + """ + + Representation of a homogeneous general quadratic. + + No solver is currently implemented for this equation type. + + """ + + name = 'homogeneous_general_quadratic' + + def matches(self): + if not (self.total_degree == 2 and self.dimension >= 3): + return False + if not self.homogeneous_order: + return False + # there may be Pow keys like x**2 or Mul keys like x*y + return any(k.is_Mul for k in self.coeff) and self.homogeneous + + +class GeneralSumOfSquares(DiophantineEquationType): + r""" + Representation of the diophantine equation + + `x_{1}^2 + x_{2}^2 + . . . + x_{n}^2 - k = 0`. + + Details + ======= + + When `n = 3` if `k = 4^a(8m + 7)` for some `a, m \in Z` then there will be + no solutions. Refer [1]_ for more details. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import GeneralSumOfSquares + >>> from sympy.abc import a, b, c, d, e + >>> GeneralSumOfSquares(a**2 + b**2 + c**2 + d**2 + e**2 - 2345).solve() + {(15, 22, 22, 24, 24)} + + By default only 1 solution is returned. Use the `limit` keyword for more: + + >>> sorted(GeneralSumOfSquares(a**2 + b**2 + c**2 + d**2 + e**2 - 2345).solve(limit=3)) + [(15, 22, 22, 24, 24), (16, 19, 24, 24, 24), (16, 20, 22, 23, 26)] + + References + ========== + + .. [1] Representing an integer as a sum of three squares, [online], + Available: + https://proofwiki.org/wiki/Integer_as_Sum_of_Three_Squares + """ + + name = 'general_sum_of_squares' + + def matches(self): + if not (self.total_degree == 2 and self.dimension >= 3): + return False + if not self.homogeneous_order: + return False + if any(k.is_Mul for k in self.coeff): + return False + return all(self.coeff[k] == 1 for k in self.coeff if k != 1) + + def solve(self, parameters=None, limit=1): + self.pre_solve(parameters) + + var = self.free_symbols + k = -int(self.coeff[1]) + n = self.dimension + + result = DiophantineSolutionSet(var, parameters=self.parameters) + + if k < 0 or limit < 1: + return result + + signs = [-1 if x.is_nonpositive else 1 for x in var] + negs = signs.count(-1) != 0 + + took = 0 + for t in sum_of_squares(k, n, zeros=True): + if negs: + result.add([signs[i]*j for i, j in enumerate(t)]) + else: + result.add(t) + took += 1 + if took == limit: + break + return result + + +class GeneralPythagorean(DiophantineEquationType): + """ + Representation of the general pythagorean equation, + `a_{1}^2x_{1}^2 + a_{2}^2x_{2}^2 + . . . + a_{n}^2x_{n}^2 - a_{n + 1}^2x_{n + 1}^2 = 0`. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import GeneralPythagorean + >>> from sympy.abc import a, b, c, d, e, x, y, z, t + >>> GeneralPythagorean(a**2 + b**2 + c**2 - d**2).solve() + {(t_0**2 + t_1**2 - t_2**2, 2*t_0*t_2, 2*t_1*t_2, t_0**2 + t_1**2 + t_2**2)} + >>> GeneralPythagorean(9*a**2 - 4*b**2 + 16*c**2 + 25*d**2 + e**2).solve(parameters=[x, y, z, t]) + {(-10*t**2 + 10*x**2 + 10*y**2 + 10*z**2, 15*t**2 + 15*x**2 + 15*y**2 + 15*z**2, 15*t*x, 12*t*y, 60*t*z)} + """ + + name = 'general_pythagorean' + + def matches(self): + if not (self.total_degree == 2 and self.dimension >= 3): + return False + if not self.homogeneous_order: + return False + if any(k.is_Mul for k in self.coeff): + return False + if all(self.coeff[k] == 1 for k in self.coeff if k != 1): + return False + if not all(is_square(abs(self.coeff[k])) for k in self.coeff): + return False + # all but one has the same sign + # e.g. 4*x**2 + y**2 - 4*z**2 + return abs(sum(sign(self.coeff[k]) for k in self.coeff)) == self.dimension - 2 + + @property + def n_parameters(self): + return self.dimension - 1 + + def solve(self, parameters=None, limit=1): + self.pre_solve(parameters) + + coeff = self.coeff + var = self.free_symbols + n = self.dimension + + if sign(coeff[var[0] ** 2]) + sign(coeff[var[1] ** 2]) + sign(coeff[var[2] ** 2]) < 0: + for key in coeff.keys(): + coeff[key] = -coeff[key] + + result = DiophantineSolutionSet(var, parameters=self.parameters) + + index = 0 + + for i, v in enumerate(var): + if sign(coeff[v ** 2]) == -1: + index = i + + m = result.parameters + + ith = sum(m_i ** 2 for m_i in m) + L = [ith - 2 * m[n - 2] ** 2] + L.extend([2 * m[i] * m[n - 2] for i in range(n - 2)]) + sol = L[:index] + [ith] + L[index:] + + lcm = 1 + for i, v in enumerate(var): + if i == index or (index > 0 and i == 0) or (index == 0 and i == 1): + lcm = ilcm(lcm, sqrt(abs(coeff[v ** 2]))) + else: + s = sqrt(coeff[v ** 2]) + lcm = ilcm(lcm, s if _odd(s) else s // 2) + + for i, v in enumerate(var): + sol[i] = (lcm * sol[i]) / sqrt(abs(coeff[v ** 2])) + + result.add(sol) + return result + + +class CubicThue(DiophantineEquationType): + """ + Representation of a cubic Thue diophantine equation. + + A cubic Thue diophantine equation is a polynomial of the form + `f(x, y) = r` of degree 3, where `x` and `y` are integers + and `r` is a rational number. + + No solver is currently implemented for this equation type. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy.solvers.diophantine.diophantine import CubicThue + >>> c1 = CubicThue(x**3 + y**2 + 1) + >>> c1.matches() + True + + """ + + name = 'cubic_thue' + + def matches(self): + return self.total_degree == 3 and self.dimension == 2 + + +class GeneralSumOfEvenPowers(DiophantineEquationType): + """ + Representation of the diophantine equation + + `x_{1}^e + x_{2}^e + . . . + x_{n}^e - k = 0` + + where `e` is an even, integer power. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import GeneralSumOfEvenPowers + >>> from sympy.abc import a, b + >>> GeneralSumOfEvenPowers(a**4 + b**4 - (2**4 + 3**4)).solve() + {(2, 3)} + + """ + + name = 'general_sum_of_even_powers' + + def matches(self): + if not self.total_degree > 3: + return False + if self.total_degree % 2 != 0: + return False + if not all(k.is_Pow and k.exp == self.total_degree for k in self.coeff if k != 1): + return False + return all(self.coeff[k] == 1 for k in self.coeff if k != 1) + + def solve(self, parameters=None, limit=1): + self.pre_solve(parameters) + + var = self.free_symbols + coeff = self.coeff + + p = None + for q in coeff.keys(): + if q.is_Pow and coeff[q]: + p = q.exp + + k = len(var) + n = -coeff[1] + + result = DiophantineSolutionSet(var, parameters=self.parameters) + + if n < 0 or limit < 1: + return result + + sign = [-1 if x.is_nonpositive else 1 for x in var] + negs = sign.count(-1) != 0 + + took = 0 + for t in power_representation(n, p, k): + if negs: + result.add([sign[i]*j for i, j in enumerate(t)]) + else: + result.add(t) + took += 1 + if took == limit: + break + return result + +# these types are known (but not necessarily handled) +# note that order is important here (in the current solver state) +all_diop_classes = [ + Linear, + Univariate, + BinaryQuadratic, + InhomogeneousTernaryQuadratic, + HomogeneousTernaryQuadraticNormal, + HomogeneousTernaryQuadratic, + InhomogeneousGeneralQuadratic, + HomogeneousGeneralQuadratic, + GeneralSumOfSquares, + GeneralPythagorean, + CubicThue, + GeneralSumOfEvenPowers, +] + +diop_known = {diop_class.name for diop_class in all_diop_classes} + + +def _remove_gcd(*x): + try: + g = igcd(*x) + except ValueError: + fx = list(filter(None, x)) + if len(fx) < 2: + return x + g = igcd(*[i.as_content_primitive()[0] for i in fx]) + except TypeError: + raise TypeError('_remove_gcd(a,b,c) or _remove_gcd(*container)') + if g == 1: + return x + return tuple([i//g for i in x]) + + +def _rational_pq(a, b): + # return `(numer, denom)` for a/b; sign in numer and gcd removed + return _remove_gcd(sign(b)*a, abs(b)) + + +def _nint_or_floor(p, q): + # return nearest int to p/q; in case of tie return floor(p/q) + w, r = divmod(p, q) + if abs(r) <= abs(q)//2: + return w + return w + 1 + + +def _odd(i): + return i % 2 != 0 + + +def _even(i): + return i % 2 == 0 + + +def diophantine(eq, param=symbols("t", integer=True), syms=None, + permute=False): + """ + Simplify the solution procedure of diophantine equation ``eq`` by + converting it into a product of terms which should equal zero. + + Explanation + =========== + + For example, when solving, `x^2 - y^2 = 0` this is treated as + `(x + y)(x - y) = 0` and `x + y = 0` and `x - y = 0` are solved + independently and combined. Each term is solved by calling + ``diop_solve()``. (Although it is possible to call ``diop_solve()`` + directly, one must be careful to pass an equation in the correct + form and to interpret the output correctly; ``diophantine()`` is + the public-facing function to use in general.) + + Output of ``diophantine()`` is a set of tuples. The elements of the + tuple are the solutions for each variable in the equation and + are arranged according to the alphabetic ordering of the variables. + e.g. For an equation with two variables, `a` and `b`, the first + element of the tuple is the solution for `a` and the second for `b`. + + Usage + ===== + + ``diophantine(eq, t, syms)``: Solve the diophantine + equation ``eq``. + ``t`` is the optional parameter to be used by ``diop_solve()``. + ``syms`` is an optional list of symbols which determines the + order of the elements in the returned tuple. + + By default, only the base solution is returned. If ``permute`` is set to + True then permutations of the base solution and/or permutations of the + signs of the values will be returned when applicable. + + Details + ======= + + ``eq`` should be an expression which is assumed to be zero. + ``t`` is the parameter to be used in the solution. + + Examples + ======== + + >>> from sympy import diophantine + >>> from sympy.abc import a, b + >>> eq = a**4 + b**4 - (2**4 + 3**4) + >>> diophantine(eq) + {(2, 3)} + >>> diophantine(eq, permute=True) + {(-3, -2), (-3, 2), (-2, -3), (-2, 3), (2, -3), (2, 3), (3, -2), (3, 2)} + + >>> from sympy.abc import x, y, z + >>> diophantine(x**2 - y**2) + {(t_0, -t_0), (t_0, t_0)} + + >>> diophantine(x*(2*x + 3*y - z)) + {(0, n1, n2), (t_0, t_1, 2*t_0 + 3*t_1)} + >>> diophantine(x**2 + 3*x*y + 4*x) + {(0, n1), (-3*t_0 - 4, t_0)} + + See Also + ======== + + diop_solve + sympy.utilities.iterables.permute_signs + sympy.utilities.iterables.signed_permutations + """ + + eq = _sympify(eq) + + if isinstance(eq, Eq): + eq = eq.lhs - eq.rhs + + try: + var = list(eq.expand(force=True).free_symbols) + var.sort(key=default_sort_key) + if syms: + if not is_sequence(syms): + raise TypeError( + 'syms should be given as a sequence, e.g. a list') + syms = [i for i in syms if i in var] + if syms != var: + dict_sym_index = dict(zip(syms, range(len(syms)))) + return {tuple([t[dict_sym_index[i]] for i in var]) + for t in diophantine(eq, param, permute=permute)} + n, d = eq.as_numer_denom() + if n.is_number: + return set() + if not d.is_number: + dsol = diophantine(d) + good = diophantine(n) - dsol + return {s for s in good if _mexpand(d.subs(zip(var, s)))} + eq = factor_terms(n) + assert not eq.is_number + eq = eq.as_independent(*var, as_Add=False)[1] + p = Poly(eq) + assert not any(g.is_number for g in p.gens) + eq = p.as_expr() + assert eq.is_polynomial() + except (GeneratorsNeeded, AssertionError): + raise TypeError(filldedent(''' + Equation should be a polynomial with Rational coefficients.''')) + + # permute only sign + do_permute_signs = False + # permute sign and values + do_permute_signs_var = False + # permute few signs + permute_few_signs = False + try: + # if we know that factoring should not be attempted, skip + # the factoring step + v, c, t = classify_diop(eq) + + # check for permute sign + if permute: + len_var = len(v) + permute_signs_for = [ + GeneralSumOfSquares.name, + GeneralSumOfEvenPowers.name] + permute_signs_check = [ + HomogeneousTernaryQuadratic.name, + HomogeneousTernaryQuadraticNormal.name, + BinaryQuadratic.name] + if t in permute_signs_for: + do_permute_signs_var = True + elif t in permute_signs_check: + # if all the variables in eq have even powers + # then do_permute_sign = True + if len_var == 3: + var_mul = list(subsets(v, 2)) + # here var_mul is like [(x, y), (x, z), (y, z)] + xy_coeff = True + x_coeff = True + var1_mul_var2 = (a[0]*a[1] for a in var_mul) + # if coeff(y*z), coeff(y*x), coeff(x*z) is not 0 then + # `xy_coeff` => True and do_permute_sign => False. + # Means no permuted solution. + for v1_mul_v2 in var1_mul_var2: + try: + coeff = c[v1_mul_v2] + except KeyError: + coeff = 0 + xy_coeff = bool(xy_coeff) and bool(coeff) + var_mul = list(subsets(v, 1)) + # here var_mul is like [(x,), (y, )] + for v1 in var_mul: + try: + coeff = c[v1[0]] + except KeyError: + coeff = 0 + x_coeff = bool(x_coeff) and bool(coeff) + if not any((xy_coeff, x_coeff)): + # means only x**2, y**2, z**2, const is present + do_permute_signs = True + elif not x_coeff: + permute_few_signs = True + elif len_var == 2: + var_mul = list(subsets(v, 2)) + # here var_mul is like [(x, y)] + xy_coeff = True + x_coeff = True + var1_mul_var2 = (x[0]*x[1] for x in var_mul) + for v1_mul_v2 in var1_mul_var2: + try: + coeff = c[v1_mul_v2] + except KeyError: + coeff = 0 + xy_coeff = bool(xy_coeff) and bool(coeff) + var_mul = list(subsets(v, 1)) + # here var_mul is like [(x,), (y, )] + for v1 in var_mul: + try: + coeff = c[v1[0]] + except KeyError: + coeff = 0 + x_coeff = bool(x_coeff) and bool(coeff) + if not any((xy_coeff, x_coeff)): + # means only x**2, y**2 and const is present + # so we can get more soln by permuting this soln. + do_permute_signs = True + elif not x_coeff: + # when coeff(x), coeff(y) is not present then signs of + # x, y can be permuted such that their sign are same + # as sign of x*y. + # e.g 1. (x_val,y_val)=> (x_val,y_val), (-x_val,-y_val) + # 2. (-x_vall, y_val)=> (-x_val,y_val), (x_val,-y_val) + permute_few_signs = True + if t == 'general_sum_of_squares': + # trying to factor such expressions will sometimes hang + terms = [(eq, 1)] + else: + raise TypeError + except (TypeError, NotImplementedError): + fl = factor_list(eq) + if fl[0].is_Rational and fl[0] != 1: + return diophantine(eq/fl[0], param=param, syms=syms, permute=permute) + terms = fl[1] + + sols = set() + + for term in terms: + + base, _ = term + var_t, _, eq_type = classify_diop(base, _dict=False) + _, base = signsimp(base, evaluate=False).as_coeff_Mul() + solution = diop_solve(base, param) + + if eq_type in [ + Linear.name, + HomogeneousTernaryQuadratic.name, + HomogeneousTernaryQuadraticNormal.name, + GeneralPythagorean.name]: + sols.add(merge_solution(var, var_t, solution)) + + elif eq_type in [ + BinaryQuadratic.name, + GeneralSumOfSquares.name, + GeneralSumOfEvenPowers.name, + Univariate.name]: + sols.update(merge_solution(var, var_t, sol) for sol in solution) + + else: + raise NotImplementedError('unhandled type: %s' % eq_type) + + sols.discard(()) + null = tuple([0]*len(var)) + # if there is no solution, return trivial solution + if not sols and eq.subs(zip(var, null)).is_zero: + if all(check_assumptions(val, **s.assumptions0) is not False for val, s in zip(null, var)): + sols.add(null) + + final_soln = set() + for sol in sols: + if all(int_valued(s) for s in sol): + if do_permute_signs: + permuted_sign = set(permute_signs(sol)) + final_soln.update(permuted_sign) + elif permute_few_signs: + lst = list(permute_signs(sol)) + lst = list(filter(lambda x: x[0]*x[1] == sol[1]*sol[0], lst)) + permuted_sign = set(lst) + final_soln.update(permuted_sign) + elif do_permute_signs_var: + permuted_sign_var = set(signed_permutations(sol)) + final_soln.update(permuted_sign_var) + else: + final_soln.add(sol) + else: + final_soln.add(sol) + return final_soln + + +def merge_solution(var, var_t, solution): + """ + This is used to construct the full solution from the solutions of sub + equations. + + Explanation + =========== + + For example when solving the equation `(x - y)(x^2 + y^2 - z^2) = 0`, + solutions for each of the equations `x - y = 0` and `x^2 + y^2 - z^2` are + found independently. Solutions for `x - y = 0` are `(x, y) = (t, t)`. But + we should introduce a value for z when we output the solution for the + original equation. This function converts `(t, t)` into `(t, t, n_{1})` + where `n_{1}` is an integer parameter. + """ + sol = [] + + if None in solution: + return () + + solution = iter(solution) + params = numbered_symbols("n", integer=True, start=1) + for v in var: + if v in var_t: + sol.append(next(solution)) + else: + sol.append(next(params)) + + for val, symb in zip(sol, var): + if check_assumptions(val, **symb.assumptions0) is False: + return () + + return tuple(sol) + + +def _diop_solve(eq, params=None): + for diop_type in all_diop_classes: + if diop_type(eq).matches(): + return diop_type(eq).solve(parameters=params) + + +def diop_solve(eq, param=symbols("t", integer=True)): + """ + Solves the diophantine equation ``eq``. + + Explanation + =========== + + Unlike ``diophantine()``, factoring of ``eq`` is not attempted. Uses + ``classify_diop()`` to determine the type of the equation and calls + the appropriate solver function. + + Use of ``diophantine()`` is recommended over other helper functions. + ``diop_solve()`` can return either a set or a tuple depending on the + nature of the equation. All non-trivial solutions are returned: assumptions + on symbols are ignored. + + Usage + ===== + + ``diop_solve(eq, t)``: Solve diophantine equation, ``eq`` using ``t`` + as a parameter if needed. + + Details + ======= + + ``eq`` should be an expression which is assumed to be zero. + ``t`` is a parameter to be used in the solution. + + Examples + ======== + + >>> from sympy.solvers.diophantine import diop_solve + >>> from sympy.abc import x, y, z, w + >>> diop_solve(2*x + 3*y - 5) + (3*t_0 - 5, 5 - 2*t_0) + >>> diop_solve(4*x + 3*y - 4*z + 5) + (t_0, 8*t_0 + 4*t_1 + 5, 7*t_0 + 3*t_1 + 5) + >>> diop_solve(x + 3*y - 4*z + w - 6) + (t_0, t_0 + t_1, 6*t_0 + 5*t_1 + 4*t_2 - 6, 5*t_0 + 4*t_1 + 3*t_2 - 6) + >>> diop_solve(x**2 + y**2 - 5) + {(-2, -1), (-2, 1), (-1, -2), (-1, 2), (1, -2), (1, 2), (2, -1), (2, 1)} + + + See Also + ======== + + diophantine() + """ + var, coeff, eq_type = classify_diop(eq, _dict=False) + + if eq_type == Linear.name: + return diop_linear(eq, param) + + elif eq_type == BinaryQuadratic.name: + return diop_quadratic(eq, param) + + elif eq_type == HomogeneousTernaryQuadratic.name: + return diop_ternary_quadratic(eq, parameterize=True) + + elif eq_type == HomogeneousTernaryQuadraticNormal.name: + return diop_ternary_quadratic_normal(eq, parameterize=True) + + elif eq_type == GeneralPythagorean.name: + return diop_general_pythagorean(eq, param) + + elif eq_type == Univariate.name: + return diop_univariate(eq) + + elif eq_type == GeneralSumOfSquares.name: + return diop_general_sum_of_squares(eq, limit=S.Infinity) + + elif eq_type == GeneralSumOfEvenPowers.name: + return diop_general_sum_of_even_powers(eq, limit=S.Infinity) + + if eq_type is not None and eq_type not in diop_known: + raise ValueError(filldedent(''' + Although this type of equation was identified, it is not yet + handled. It should, however, be listed in `diop_known` at the + top of this file. Developers should see comments at the end of + `classify_diop`. + ''')) # pragma: no cover + else: + raise NotImplementedError( + 'No solver has been written for %s.' % eq_type) + + +def classify_diop(eq, _dict=True): + # docstring supplied externally + + matched = False + diop_type = None + for diop_class in all_diop_classes: + diop_type = diop_class(eq) + if diop_type.matches(): + matched = True + break + + if matched: + return diop_type.free_symbols, dict(diop_type.coeff) if _dict else diop_type.coeff, diop_type.name + + # new diop type instructions + # -------------------------- + # if this error raises and the equation *can* be classified, + # * it should be identified in the if-block above + # * the type should be added to the diop_known + # if a solver can be written for it, + # * a dedicated handler should be written (e.g. diop_linear) + # * it should be passed to that handler in diop_solve + raise NotImplementedError(filldedent(''' + This equation is not yet recognized or else has not been + simplified sufficiently to put it in a form recognized by + diop_classify().''')) + + +classify_diop.func_doc = ( # type: ignore + ''' + Helper routine used by diop_solve() to find information about ``eq``. + + Explanation + =========== + + Returns a tuple containing the type of the diophantine equation + along with the variables (free symbols) and their coefficients. + Variables are returned as a list and coefficients are returned + as a dict with the key being the respective term and the constant + term is keyed to 1. The type is one of the following: + + * %s + + Usage + ===== + + ``classify_diop(eq)``: Return variables, coefficients and type of the + ``eq``. + + Details + ======= + + ``eq`` should be an expression which is assumed to be zero. + ``_dict`` is for internal use: when True (default) a dict is returned, + otherwise a defaultdict which supplies 0 for missing keys is returned. + + Examples + ======== + + >>> from sympy.solvers.diophantine import classify_diop + >>> from sympy.abc import x, y, z, w, t + >>> classify_diop(4*x + 6*y - 4) + ([x, y], {1: -4, x: 4, y: 6}, 'linear') + >>> classify_diop(x + 3*y -4*z + 5) + ([x, y, z], {1: 5, x: 1, y: 3, z: -4}, 'linear') + >>> classify_diop(x**2 + y**2 - x*y + x + 5) + ([x, y], {1: 5, x: 1, x**2: 1, y**2: 1, x*y: -1}, 'binary_quadratic') + ''' % ('\n * '.join(sorted(diop_known)))) + + +def diop_linear(eq, param=symbols("t", integer=True)): + """ + Solves linear diophantine equations. + + A linear diophantine equation is an equation of the form `a_{1}x_{1} + + a_{2}x_{2} + .. + a_{n}x_{n} = 0` where `a_{1}, a_{2}, ..a_{n}` are + integer constants and `x_{1}, x_{2}, ..x_{n}` are integer variables. + + Usage + ===== + + ``diop_linear(eq)``: Returns a tuple containing solutions to the + diophantine equation ``eq``. Values in the tuple is arranged in the same + order as the sorted variables. + + Details + ======= + + ``eq`` is a linear diophantine equation which is assumed to be zero. + ``param`` is the parameter to be used in the solution. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import diop_linear + >>> from sympy.abc import x, y, z + >>> diop_linear(2*x - 3*y - 5) # solves equation 2*x - 3*y - 5 == 0 + (3*t_0 - 5, 2*t_0 - 5) + + Here x = -3*t_0 - 5 and y = -2*t_0 - 5 + + >>> diop_linear(2*x - 3*y - 4*z -3) + (t_0, 2*t_0 + 4*t_1 + 3, -t_0 - 3*t_1 - 3) + + See Also + ======== + + diop_quadratic(), diop_ternary_quadratic(), diop_general_pythagorean(), + diop_general_sum_of_squares() + """ + var, coeff, diop_type = classify_diop(eq, _dict=False) + + if diop_type == Linear.name: + parameters = None + if param is not None: + parameters = symbols('%s_0:%i' % (param, len(var)), integer=True) + + result = Linear(eq).solve(parameters=parameters) + + if param is None: + result = result(*[0]*len(result.parameters)) + + if len(result) > 0: + return list(result)[0] + else: + return tuple([None]*len(result.parameters)) + + +def base_solution_linear(c, a, b, t=None): + """ + Return the base solution for the linear equation, `ax + by = c`. + + Explanation + =========== + + Used by ``diop_linear()`` to find the base solution of a linear + Diophantine equation. If ``t`` is given then the parametrized solution is + returned. + + Usage + ===== + + ``base_solution_linear(c, a, b, t)``: ``a``, ``b``, ``c`` are coefficients + in `ax + by = c` and ``t`` is the parameter to be used in the solution. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import base_solution_linear + >>> from sympy.abc import t + >>> base_solution_linear(5, 2, 3) # equation 2*x + 3*y = 5 + (-5, 5) + >>> base_solution_linear(0, 5, 7) # equation 5*x + 7*y = 0 + (0, 0) + >>> base_solution_linear(5, 2, 3, t) # equation 2*x + 3*y = 5 + (3*t - 5, 5 - 2*t) + >>> base_solution_linear(0, 5, 7, t) # equation 5*x + 7*y = 0 + (7*t, -5*t) + """ + a, b, c = _remove_gcd(a, b, c) + + if c == 0: + if t is None: + return (0, 0) + if b < 0: + t = -t + return (b*t, -a*t) + + x0, y0, d = igcdex(abs(a), abs(b)) + x0 *= sign(a) + y0 *= sign(b) + if c % d: + return (None, None) + if t is None: + return (c*x0, c*y0) + if b < 0: + t = -t + return (c*x0 + b*t, c*y0 - a*t) + + +def diop_univariate(eq): + """ + Solves a univariate diophantine equations. + + Explanation + =========== + + A univariate diophantine equation is an equation of the form + `a_{0} + a_{1}x + a_{2}x^2 + .. + a_{n}x^n = 0` where `a_{1}, a_{2}, ..a_{n}` are + integer constants and `x` is an integer variable. + + Usage + ===== + + ``diop_univariate(eq)``: Returns a set containing solutions to the + diophantine equation ``eq``. + + Details + ======= + + ``eq`` is a univariate diophantine equation which is assumed to be zero. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import diop_univariate + >>> from sympy.abc import x + >>> diop_univariate((x - 2)*(x - 3)**2) # solves equation (x - 2)*(x - 3)**2 == 0 + {(2,), (3,)} + + """ + var, coeff, diop_type = classify_diop(eq, _dict=False) + + if diop_type == Univariate.name: + return {(int(i),) for i in solveset_real( + eq, var[0]).intersect(S.Integers)} + + +def divisible(a, b): + """ + Returns `True` if ``a`` is divisible by ``b`` and `False` otherwise. + """ + return not a % b + + +def diop_quadratic(eq, param=symbols("t", integer=True)): + """ + Solves quadratic diophantine equations. + + i.e. equations of the form `Ax^2 + Bxy + Cy^2 + Dx + Ey + F = 0`. Returns a + set containing the tuples `(x, y)` which contains the solutions. If there + are no solutions then `(None, None)` is returned. + + Usage + ===== + + ``diop_quadratic(eq, param)``: ``eq`` is a quadratic binary diophantine + equation. ``param`` is used to indicate the parameter to be used in the + solution. + + Details + ======= + + ``eq`` should be an expression which is assumed to be zero. + ``param`` is a parameter to be used in the solution. + + Examples + ======== + + >>> from sympy.abc import x, y, t + >>> from sympy.solvers.diophantine.diophantine import diop_quadratic + >>> diop_quadratic(x**2 + y**2 + 2*x + 2*y + 2, t) + {(-1, -1)} + + References + ========== + + .. [1] Methods to solve Ax^2 + Bxy + Cy^2 + Dx + Ey + F = 0, [online], + Available: https://www.alpertron.com.ar/METHODS.HTM + .. [2] Solving the equation ax^2+ bxy + cy^2 + dx + ey + f= 0, [online], + Available: https://web.archive.org/web/20160323033111/http://www.jpr2718.org/ax2p.pdf + + See Also + ======== + + diop_linear(), diop_ternary_quadratic(), diop_general_sum_of_squares(), + diop_general_pythagorean() + """ + var, coeff, diop_type = classify_diop(eq, _dict=False) + + if diop_type == BinaryQuadratic.name: + if param is not None: + parameters = [param, Symbol("u", integer=True)] + else: + parameters = None + return set(BinaryQuadratic(eq).solve(parameters=parameters)) + + +def is_solution_quad(var, coeff, u, v): + """ + Check whether `(u, v)` is solution to the quadratic binary diophantine + equation with the variable list ``var`` and coefficient dictionary + ``coeff``. + + Not intended for use by normal users. + """ + reps = dict(zip(var, (u, v))) + eq = Add(*[j*i.xreplace(reps) for i, j in coeff.items()]) + return _mexpand(eq) == 0 + + +def diop_DN(D, N, t=symbols("t", integer=True)): + """ + Solves the equation `x^2 - Dy^2 = N`. + + Explanation + =========== + + Mainly concerned with the case `D > 0, D` is not a perfect square, + which is the same as the generalized Pell equation. The LMM + algorithm [1]_ is used to solve this equation. + + Returns one solution tuple, (`x, y)` for each class of the solutions. + Other solutions of the class can be constructed according to the + values of ``D`` and ``N``. + + Usage + ===== + + ``diop_DN(D, N, t)``: D and N are integers as in `x^2 - Dy^2 = N` and + ``t`` is the parameter to be used in the solutions. + + Details + ======= + + ``D`` and ``N`` correspond to D and N in the equation. + ``t`` is the parameter to be used in the solutions. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import diop_DN + >>> diop_DN(13, -4) # Solves equation x**2 - 13*y**2 = -4 + [(3, 1), (393, 109), (36, 10)] + + The output can be interpreted as follows: There are three fundamental + solutions to the equation `x^2 - 13y^2 = -4` given by (3, 1), (393, 109) + and (36, 10). Each tuple is in the form (x, y), i.e. solution (3, 1) means + that `x = 3` and `y = 1`. + + >>> diop_DN(986, 1) # Solves equation x**2 - 986*y**2 = 1 + [(49299, 1570)] + + See Also + ======== + + find_DN(), diop_bf_DN() + + References + ========== + + .. [1] Solving the generalized Pell equation x**2 - D*y**2 = N, John P. + Robertson, July 31, 2004, Pages 16 - 17. [online], Available: + https://web.archive.org/web/20160323033128/http://www.jpr2718.org/pell.pdf + """ + if D < 0: + if N == 0: + return [(0, 0)] + if N < 0: + return [] + # N > 0: + sol = [] + for d in divisors(square_factor(N), generator=True): + for x, y in cornacchia(1, int(-D), int(N // d**2)): + sol.append((d*x, d*y)) + if D == -1: + sol.append((d*y, d*x)) + return sol + + if D == 0: + if N < 0: + return [] + if N == 0: + return [(0, t)] + sN, _exact = integer_nthroot(N, 2) + if _exact: + return [(sN, t)] + return [] + + # D > 0 + sD, _exact = integer_nthroot(D, 2) + if _exact: + if N == 0: + return [(sD*t, t)] + + sol = [] + for y in range(floor(sign(N)*(N - 1)/(2*sD)) + 1): + try: + sq, _exact = integer_nthroot(D*y**2 + N, 2) + except ValueError: + _exact = False + if _exact: + sol.append((sq, y)) + return sol + + if 1 < N**2 < D: + # It is much faster to call `_special_diop_DN`. + return _special_diop_DN(D, N) + + if N == 0: + return [(0, 0)] + + sol = [] + if abs(N) == 1: + pqa = PQa(0, 1, D) + *_, prev_B, prev_G = next(pqa) + for j, (*_, a, _, _B, _G) in enumerate(pqa): + if a == 2*sD: + break + prev_B, prev_G = _B, _G + if j % 2: + if N == 1: + sol.append((prev_G, prev_B)) + return sol + if N == -1: + return [(prev_G, prev_B)] + for _ in range(j): + *_, _B, _G = next(pqa) + return [(_G, _B)] + + for f in divisors(square_factor(N), generator=True): + m = N // f**2 + am = abs(m) + for sqm in sqrt_mod(D, am, all_roots=True): + z = symmetric_residue(sqm, am) + pqa = PQa(z, am, D) + *_, prev_B, prev_G = next(pqa) + for _ in range(length(z, am, D) - 1): + _, q, *_, _B, _G = next(pqa) + if abs(q) == 1: + if prev_G**2 - D*prev_B**2 == m: + sol.append((f*prev_G, f*prev_B)) + elif a := diop_DN(D, -1): + sol.append((f*(prev_G*a[0][0] + prev_B*D*a[0][1]), + f*(prev_G*a[0][1] + prev_B*a[0][0]))) + break + prev_B, prev_G = _B, _G + return sol + + +def _special_diop_DN(D, N): + """ + Solves the equation `x^2 - Dy^2 = N` for the special case where + `1 < N**2 < D` and `D` is not a perfect square. + It is better to call `diop_DN` rather than this function, as + the former checks the condition `1 < N**2 < D`, and calls the latter only + if appropriate. + + Usage + ===== + + WARNING: Internal method. Do not call directly! + + ``_special_diop_DN(D, N)``: D and N are integers as in `x^2 - Dy^2 = N`. + + Details + ======= + + ``D`` and ``N`` correspond to D and N in the equation. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import _special_diop_DN + >>> _special_diop_DN(13, -3) # Solves equation x**2 - 13*y**2 = -3 + [(7, 2), (137, 38)] + + The output can be interpreted as follows: There are two fundamental + solutions to the equation `x^2 - 13y^2 = -3` given by (7, 2) and + (137, 38). Each tuple is in the form (x, y), i.e. solution (7, 2) means + that `x = 7` and `y = 2`. + + >>> _special_diop_DN(2445, -20) # Solves equation x**2 - 2445*y**2 = -20 + [(445, 9), (17625560, 356454), (698095554475, 14118073569)] + + See Also + ======== + + diop_DN() + + References + ========== + + .. [1] Section 4.4.4 of the following book: + Quadratic Diophantine Equations, T. Andreescu and D. Andrica, + Springer, 2015. + """ + + # The following assertion was removed for efficiency, with the understanding + # that this method is not called directly. The parent method, `diop_DN` + # is responsible for performing the appropriate checks. + # + # assert (1 < N**2 < D) and (not integer_nthroot(D, 2)[1]) + + sqrt_D = isqrt(D) + F = {N // f**2: f for f in divisors(square_factor(abs(N)), generator=True)} + P = 0 + Q = 1 + G0, G1 = 0, 1 + B0, B1 = 1, 0 + + solutions = [] + while True: + for _ in range(2): + a = (P + sqrt_D) // Q + P = a*Q - P + Q = (D - P**2) // Q + G0, G1 = G1, a*G1 + G0 + B0, B1 = B1, a*B1 + B0 + if (s := G1**2 - D*B1**2) in F: + f = F[s] + solutions.append((f*G1, f*B1)) + if Q == 1: + break + return solutions + + +def cornacchia(a:int, b:int, m:int) -> set[tuple[int, int]]: + r""" + Solves `ax^2 + by^2 = m` where `\gcd(a, b) = 1 = gcd(a, m)` and `a, b > 0`. + + Explanation + =========== + + Uses the algorithm due to Cornacchia. The method only finds primitive + solutions, i.e. ones with `\gcd(x, y) = 1`. So this method cannot be used to + find the solutions of `x^2 + y^2 = 20` since the only solution to former is + `(x, y) = (4, 2)` and it is not primitive. When `a = b`, only the + solutions with `x \leq y` are found. For more details, see the References. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import cornacchia + >>> cornacchia(2, 3, 35) # equation 2x**2 + 3y**2 = 35 + {(2, 3), (4, 1)} + >>> cornacchia(1, 1, 25) # equation x**2 + y**2 = 25 + {(4, 3)} + + References + =========== + + .. [1] A. Nitaj, "L'algorithme de Cornacchia" + .. [2] Solving the diophantine equation ax**2 + by**2 = m by Cornacchia's + method, [online], Available: + http://www.numbertheory.org/php/cornacchia.html + + See Also + ======== + + sympy.utilities.iterables.signed_permutations + """ + # Assume gcd(a, b) = gcd(a, m) = 1 and a, b > 0 but no error checking + sols = set() + + if a + b > m: + # xy = 0 must hold if there exists a solution + if a == 1: + # y = 0 + s, _exact = iroot(m // a, 2) + if _exact: + sols.add((int(s), 0)) + if a == b: + # only keep one solution + return sols + if m % b == 0: + # x = 0 + s, _exact = iroot(m // b, 2) + if _exact: + sols.add((0, int(s))) + return sols + + # the original cornacchia + for t in sqrt_mod_iter(-b*invert(a, m), m): + if t < m // 2: + continue + u, r = m, t + while (m1 := m - a*r**2) <= 0: + u, r = r, u % r + m1, _r = divmod(m1, b) + if _r: + continue + s, _exact = iroot(m1, 2) + if _exact: + if a == b and r < s: + r, s = s, r + sols.add((int(r), int(s))) + return sols + + +def PQa(P_0, Q_0, D): + r""" + Returns useful information needed to solve the Pell equation. + + Explanation + =========== + + There are six sequences of integers defined related to the continued + fraction representation of `\\frac{P + \sqrt{D}}{Q}`, namely {`P_{i}`}, + {`Q_{i}`}, {`a_{i}`},{`A_{i}`}, {`B_{i}`}, {`G_{i}`}. ``PQa()`` Returns + these values as a 6-tuple in the same order as mentioned above. Refer [1]_ + for more detailed information. + + Usage + ===== + + ``PQa(P_0, Q_0, D)``: ``P_0``, ``Q_0`` and ``D`` are integers corresponding + to `P_{0}`, `Q_{0}` and `D` in the continued fraction + `\\frac{P_{0} + \sqrt{D}}{Q_{0}}`. + Also it's assumed that `P_{0}^2 == D mod(|Q_{0}|)` and `D` is square free. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import PQa + >>> pqa = PQa(13, 4, 5) # (13 + sqrt(5))/4 + >>> next(pqa) # (P_0, Q_0, a_0, A_0, B_0, G_0) + (13, 4, 3, 3, 1, -1) + >>> next(pqa) # (P_1, Q_1, a_1, A_1, B_1, G_1) + (-1, 1, 1, 4, 1, 3) + + References + ========== + + .. [1] Solving the generalized Pell equation x^2 - Dy^2 = N, John P. + Robertson, July 31, 2004, Pages 4 - 8. https://web.archive.org/web/20160323033128/http://www.jpr2718.org/pell.pdf + """ + sqD = isqrt(D) + A2 = B1 = 0 + A1 = B2 = 1 + G1 = Q_0 + G2 = -P_0 + P_i = P_0 + Q_i = Q_0 + + while True: + a_i = (P_i + sqD) // Q_i + A1, A2 = a_i*A1 + A2, A1 + B1, B2 = a_i*B1 + B2, B1 + G1, G2 = a_i*G1 + G2, G1 + yield P_i, Q_i, a_i, A1, B1, G1 + + P_i = a_i*Q_i - P_i + Q_i = (D - P_i**2) // Q_i + + +def diop_bf_DN(D, N, t=symbols("t", integer=True)): + r""" + Uses brute force to solve the equation, `x^2 - Dy^2 = N`. + + Explanation + =========== + + Mainly concerned with the generalized Pell equation which is the case when + `D > 0, D` is not a perfect square. For more information on the case refer + [1]_. Let `(t, u)` be the minimal positive solution of the equation + `x^2 - Dy^2 = 1`. Then this method requires + `\sqrt{\\frac{\mid N \mid (t \pm 1)}{2D}}` to be small. + + Usage + ===== + + ``diop_bf_DN(D, N, t)``: ``D`` and ``N`` are coefficients in + `x^2 - Dy^2 = N` and ``t`` is the parameter to be used in the solutions. + + Details + ======= + + ``D`` and ``N`` correspond to D and N in the equation. + ``t`` is the parameter to be used in the solutions. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import diop_bf_DN + >>> diop_bf_DN(13, -4) + [(3, 1), (-3, 1), (36, 10)] + >>> diop_bf_DN(986, 1) + [(49299, 1570)] + + See Also + ======== + + diop_DN() + + References + ========== + + .. [1] Solving the generalized Pell equation x**2 - D*y**2 = N, John P. + Robertson, July 31, 2004, Page 15. https://web.archive.org/web/20160323033128/http://www.jpr2718.org/pell.pdf + """ + D = as_int(D) + N = as_int(N) + + sol = [] + a = diop_DN(D, 1) + u = a[0][0] + + if N == 0: + if D < 0: + return [(0, 0)] + if D == 0: + return [(0, t)] + sD, _exact = integer_nthroot(D, 2) + if _exact: + return [(sD*t, t), (-sD*t, t)] + return [(0, 0)] + + if abs(N) == 1: + return diop_DN(D, N) + + if N > 1: + L1 = 0 + L2 = integer_nthroot(int(N*(u - 1)/(2*D)), 2)[0] + 1 + else: # N < -1 + L1, _exact = integer_nthroot(-int(N/D), 2) + if not _exact: + L1 += 1 + L2 = integer_nthroot(-int(N*(u + 1)/(2*D)), 2)[0] + 1 + + for y in range(L1, L2): + try: + x, _exact = integer_nthroot(N + D*y**2, 2) + except ValueError: + _exact = False + if _exact: + sol.append((x, y)) + if not equivalent(x, y, -x, y, D, N): + sol.append((-x, y)) + + return sol + + +def equivalent(u, v, r, s, D, N): + """ + Returns True if two solutions `(u, v)` and `(r, s)` of `x^2 - Dy^2 = N` + belongs to the same equivalence class and False otherwise. + + Explanation + =========== + + Two solutions `(u, v)` and `(r, s)` to the above equation fall to the same + equivalence class iff both `(ur - Dvs)` and `(us - vr)` are divisible by + `N`. See reference [1]_. No test is performed to test whether `(u, v)` and + `(r, s)` are actually solutions to the equation. User should take care of + this. + + Usage + ===== + + ``equivalent(u, v, r, s, D, N)``: `(u, v)` and `(r, s)` are two solutions + of the equation `x^2 - Dy^2 = N` and all parameters involved are integers. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import equivalent + >>> equivalent(18, 5, -18, -5, 13, -1) + True + >>> equivalent(3, 1, -18, 393, 109, -4) + False + + References + ========== + + .. [1] Solving the generalized Pell equation x**2 - D*y**2 = N, John P. + Robertson, July 31, 2004, Page 12. https://web.archive.org/web/20160323033128/http://www.jpr2718.org/pell.pdf + + """ + return divisible(u*r - D*v*s, N) and divisible(u*s - v*r, N) + + +def length(P, Q, D): + r""" + Returns the (length of aperiodic part + length of periodic part) of + continued fraction representation of `\\frac{P + \sqrt{D}}{Q}`. + + It is important to remember that this does NOT return the length of the + periodic part but the sum of the lengths of the two parts as mentioned + above. + + Usage + ===== + + ``length(P, Q, D)``: ``P``, ``Q`` and ``D`` are integers corresponding to + the continued fraction `\\frac{P + \sqrt{D}}{Q}`. + + Details + ======= + + ``P``, ``D`` and ``Q`` corresponds to P, D and Q in the continued fraction, + `\\frac{P + \sqrt{D}}{Q}`. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import length + >>> length(-2, 4, 5) # (-2 + sqrt(5))/4 + 3 + >>> length(-5, 4, 17) # (-5 + sqrt(17))/4 + 4 + + See Also + ======== + sympy.ntheory.continued_fraction.continued_fraction_periodic + """ + from sympy.ntheory.continued_fraction import continued_fraction_periodic + v = continued_fraction_periodic(P, Q, D) + if isinstance(v[-1], list): + rpt = len(v[-1]) + nonrpt = len(v) - 1 + else: + rpt = 0 + nonrpt = len(v) + return rpt + nonrpt + + +def transformation_to_DN(eq): + """ + This function transforms general quadratic, + `ax^2 + bxy + cy^2 + dx + ey + f = 0` + to more easy to deal with `X^2 - DY^2 = N` form. + + Explanation + =========== + + This is used to solve the general quadratic equation by transforming it to + the latter form. Refer to [1]_ for more detailed information on the + transformation. This function returns a tuple (A, B) where A is a 2 X 2 + matrix and B is a 2 X 1 matrix such that, + + Transpose([x y]) = A * Transpose([X Y]) + B + + Usage + ===== + + ``transformation_to_DN(eq)``: where ``eq`` is the quadratic to be + transformed. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy.solvers.diophantine.diophantine import transformation_to_DN + >>> A, B = transformation_to_DN(x**2 - 3*x*y - y**2 - 2*y + 1) + >>> A + Matrix([ + [1/26, 3/26], + [ 0, 1/13]]) + >>> B + Matrix([ + [-6/13], + [-4/13]]) + + A, B returned are such that Transpose((x y)) = A * Transpose((X Y)) + B. + Substituting these values for `x` and `y` and a bit of simplifying work + will give an equation of the form `x^2 - Dy^2 = N`. + + >>> from sympy.abc import X, Y + >>> from sympy import Matrix, simplify + >>> u = (A*Matrix([X, Y]) + B)[0] # Transformation for x + >>> u + X/26 + 3*Y/26 - 6/13 + >>> v = (A*Matrix([X, Y]) + B)[1] # Transformation for y + >>> v + Y/13 - 4/13 + + Next we will substitute these formulas for `x` and `y` and do + ``simplify()``. + + >>> eq = simplify((x**2 - 3*x*y - y**2 - 2*y + 1).subs(zip((x, y), (u, v)))) + >>> eq + X**2/676 - Y**2/52 + 17/13 + + By multiplying the denominator appropriately, we can get a Pell equation + in the standard form. + + >>> eq * 676 + X**2 - 13*Y**2 + 884 + + If only the final equation is needed, ``find_DN()`` can be used. + + See Also + ======== + + find_DN() + + References + ========== + + .. [1] Solving the equation ax^2 + bxy + cy^2 + dx + ey + f = 0, + John P.Robertson, May 8, 2003, Page 7 - 11. + https://web.archive.org/web/20160323033111/http://www.jpr2718.org/ax2p.pdf + """ + + var, coeff, diop_type = classify_diop(eq, _dict=False) + if diop_type == BinaryQuadratic.name: + return _transformation_to_DN(var, coeff) + + +def _transformation_to_DN(var, coeff): + + x, y = var + + a = coeff[x**2] + b = coeff[x*y] + c = coeff[y**2] + d = coeff[x] + e = coeff[y] + f = coeff[1] + + a, b, c, d, e, f = [as_int(i) for i in _remove_gcd(a, b, c, d, e, f)] + + X, Y = symbols("X, Y", integer=True) + + if b: + B, C = _rational_pq(2*a, b) + A, T = _rational_pq(a, B**2) + + # eq_1 = A*B*X**2 + B*(c*T - A*C**2)*Y**2 + d*T*X + (B*e*T - d*T*C)*Y + f*T*B + coeff = {X**2: A*B, X*Y: 0, Y**2: B*(c*T - A*C**2), X: d*T, Y: B*e*T - d*T*C, 1: f*T*B} + A_0, B_0 = _transformation_to_DN([X, Y], coeff) + return Matrix(2, 2, [S.One/B, -S(C)/B, 0, 1])*A_0, Matrix(2, 2, [S.One/B, -S(C)/B, 0, 1])*B_0 + + if d: + B, C = _rational_pq(2*a, d) + A, T = _rational_pq(a, B**2) + + # eq_2 = A*X**2 + c*T*Y**2 + e*T*Y + f*T - A*C**2 + coeff = {X**2: A, X*Y: 0, Y**2: c*T, X: 0, Y: e*T, 1: f*T - A*C**2} + A_0, B_0 = _transformation_to_DN([X, Y], coeff) + return Matrix(2, 2, [S.One/B, 0, 0, 1])*A_0, Matrix(2, 2, [S.One/B, 0, 0, 1])*B_0 + Matrix([-S(C)/B, 0]) + + if e: + B, C = _rational_pq(2*c, e) + A, T = _rational_pq(c, B**2) + + # eq_3 = a*T*X**2 + A*Y**2 + f*T - A*C**2 + coeff = {X**2: a*T, X*Y: 0, Y**2: A, X: 0, Y: 0, 1: f*T - A*C**2} + A_0, B_0 = _transformation_to_DN([X, Y], coeff) + return Matrix(2, 2, [1, 0, 0, S.One/B])*A_0, Matrix(2, 2, [1, 0, 0, S.One/B])*B_0 + Matrix([0, -S(C)/B]) + + # TODO: pre-simplification: Not necessary but may simplify + # the equation. + return Matrix(2, 2, [S.One/a, 0, 0, 1]), Matrix([0, 0]) + + +def find_DN(eq): + """ + This function returns a tuple, `(D, N)` of the simplified form, + `x^2 - Dy^2 = N`, corresponding to the general quadratic, + `ax^2 + bxy + cy^2 + dx + ey + f = 0`. + + Solving the general quadratic is then equivalent to solving the equation + `X^2 - DY^2 = N` and transforming the solutions by using the transformation + matrices returned by ``transformation_to_DN()``. + + Usage + ===== + + ``find_DN(eq)``: where ``eq`` is the quadratic to be transformed. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy.solvers.diophantine.diophantine import find_DN + >>> find_DN(x**2 - 3*x*y - y**2 - 2*y + 1) + (13, -884) + + Interpretation of the output is that we get `X^2 -13Y^2 = -884` after + transforming `x^2 - 3xy - y^2 - 2y + 1` using the transformation returned + by ``transformation_to_DN()``. + + See Also + ======== + + transformation_to_DN() + + References + ========== + + .. [1] Solving the equation ax^2 + bxy + cy^2 + dx + ey + f = 0, + John P.Robertson, May 8, 2003, Page 7 - 11. + https://web.archive.org/web/20160323033111/http://www.jpr2718.org/ax2p.pdf + """ + var, coeff, diop_type = classify_diop(eq, _dict=False) + if diop_type == BinaryQuadratic.name: + return _find_DN(var, coeff) + + +def _find_DN(var, coeff): + + x, y = var + X, Y = symbols("X, Y", integer=True) + A, B = _transformation_to_DN(var, coeff) + + u = (A*Matrix([X, Y]) + B)[0] + v = (A*Matrix([X, Y]) + B)[1] + eq = x**2*coeff[x**2] + x*y*coeff[x*y] + y**2*coeff[y**2] + x*coeff[x] + y*coeff[y] + coeff[1] + + simplified = _mexpand(eq.subs(zip((x, y), (u, v)))) + + coeff = simplified.as_coefficients_dict() + + return -coeff[Y**2]/coeff[X**2], -coeff[1]/coeff[X**2] + + +def check_param(x, y, a, params): + """ + If there is a number modulo ``a`` such that ``x`` and ``y`` are both + integers, then return a parametric representation for ``x`` and ``y`` + else return (None, None). + + Here ``x`` and ``y`` are functions of ``t``. + """ + from sympy.simplify.simplify import clear_coefficients + + if x.is_number and not x.is_Integer: + return DiophantineSolutionSet([x, y], parameters=params) + + if y.is_number and not y.is_Integer: + return DiophantineSolutionSet([x, y], parameters=params) + + m, n = symbols("m, n", integer=True) + c, p = (m*x + n*y).as_content_primitive() + if a % c.q: + return DiophantineSolutionSet([x, y], parameters=params) + + # clear_coefficients(mx + b, R)[1] -> (R - b)/m + eq = clear_coefficients(x, m)[1] - clear_coefficients(y, n)[1] + junk, eq = eq.as_content_primitive() + + return _diop_solve(eq, params=params) + + +def diop_ternary_quadratic(eq, parameterize=False): + """ + Solves the general quadratic ternary form, + `ax^2 + by^2 + cz^2 + fxy + gyz + hxz = 0`. + + Returns a tuple `(x, y, z)` which is a base solution for the above + equation. If there are no solutions, `(None, None, None)` is returned. + + Usage + ===== + + ``diop_ternary_quadratic(eq)``: Return a tuple containing a basic solution + to ``eq``. + + Details + ======= + + ``eq`` should be an homogeneous expression of degree two in three variables + and it is assumed to be zero. + + Examples + ======== + + >>> from sympy.abc import x, y, z + >>> from sympy.solvers.diophantine.diophantine import diop_ternary_quadratic + >>> diop_ternary_quadratic(x**2 + 3*y**2 - z**2) + (1, 0, 1) + >>> diop_ternary_quadratic(4*x**2 + 5*y**2 - z**2) + (1, 0, 2) + >>> diop_ternary_quadratic(45*x**2 - 7*y**2 - 8*x*y - z**2) + (28, 45, 105) + >>> diop_ternary_quadratic(x**2 - 49*y**2 - z**2 + 13*z*y -8*x*y) + (9, 1, 5) + """ + var, coeff, diop_type = classify_diop(eq, _dict=False) + + if diop_type in ( + HomogeneousTernaryQuadratic.name, + HomogeneousTernaryQuadraticNormal.name): + sol = _diop_ternary_quadratic(var, coeff) + if len(sol) > 0: + x_0, y_0, z_0 = list(sol)[0] + else: + x_0, y_0, z_0 = None, None, None + + if parameterize: + return _parametrize_ternary_quadratic( + (x_0, y_0, z_0), var, coeff) + return x_0, y_0, z_0 + + +def _diop_ternary_quadratic(_var, coeff): + eq = sum(i*coeff[i] for i in coeff) + if HomogeneousTernaryQuadratic(eq).matches(): + return HomogeneousTernaryQuadratic(eq, free_symbols=_var).solve() + elif HomogeneousTernaryQuadraticNormal(eq).matches(): + return HomogeneousTernaryQuadraticNormal(eq, free_symbols=_var).solve() + + +def transformation_to_normal(eq): + """ + Returns the transformation Matrix that converts a general ternary + quadratic equation ``eq`` (`ax^2 + by^2 + cz^2 + dxy + eyz + fxz`) + to a form without cross terms: `ax^2 + by^2 + cz^2 = 0`. This is + not used in solving ternary quadratics; it is only implemented for + the sake of completeness. + """ + var, coeff, diop_type = classify_diop(eq, _dict=False) + + if diop_type in ( + "homogeneous_ternary_quadratic", + "homogeneous_ternary_quadratic_normal"): + return _transformation_to_normal(var, coeff) + + +def _transformation_to_normal(var, coeff): + + _var = list(var) # copy + x, y, z = var + + if not any(coeff[i**2] for i in var): + # https://math.stackexchange.com/questions/448051/transform-quadratic-ternary-form-to-normal-form/448065#448065 + a = coeff[x*y] + b = coeff[y*z] + c = coeff[x*z] + swap = False + if not a: # b can't be 0 or else there aren't 3 vars + swap = True + a, b = b, a + T = Matrix(((1, 1, -b/a), (1, -1, -c/a), (0, 0, 1))) + if swap: + T.row_swap(0, 1) + T.col_swap(0, 1) + return T + + if coeff[x**2] == 0: + # If the coefficient of x is zero change the variables + if coeff[y**2] == 0: + _var[0], _var[2] = var[2], var[0] + T = _transformation_to_normal(_var, coeff) + T.row_swap(0, 2) + T.col_swap(0, 2) + return T + + _var[0], _var[1] = var[1], var[0] + T = _transformation_to_normal(_var, coeff) + T.row_swap(0, 1) + T.col_swap(0, 1) + return T + + # Apply the transformation x --> X - (B*Y + C*Z)/(2*A) + if coeff[x*y] != 0 or coeff[x*z] != 0: + A = coeff[x**2] + B = coeff[x*y] + C = coeff[x*z] + D = coeff[y**2] + E = coeff[y*z] + F = coeff[z**2] + + _coeff = {} + + _coeff[x**2] = 4*A**2 + _coeff[y**2] = 4*A*D - B**2 + _coeff[z**2] = 4*A*F - C**2 + _coeff[y*z] = 4*A*E - 2*B*C + _coeff[x*y] = 0 + _coeff[x*z] = 0 + + T_0 = _transformation_to_normal(_var, _coeff) + return Matrix(3, 3, [1, S(-B)/(2*A), S(-C)/(2*A), 0, 1, 0, 0, 0, 1])*T_0 + + elif coeff[y*z] != 0: + if coeff[y**2] == 0: + if coeff[z**2] == 0: + # Equations of the form A*x**2 + E*yz = 0. + # Apply transformation y -> Y + Z ans z -> Y - Z + return Matrix(3, 3, [1, 0, 0, 0, 1, 1, 0, 1, -1]) + + # Ax**2 + E*y*z + F*z**2 = 0 + _var[0], _var[2] = var[2], var[0] + T = _transformation_to_normal(_var, coeff) + T.row_swap(0, 2) + T.col_swap(0, 2) + return T + + # A*x**2 + D*y**2 + E*y*z + F*z**2 = 0, F may be zero + _var[0], _var[1] = var[1], var[0] + T = _transformation_to_normal(_var, coeff) + T.row_swap(0, 1) + T.col_swap(0, 1) + return T + + return Matrix.eye(3) + + +def parametrize_ternary_quadratic(eq): + """ + Returns the parametrized general solution for the ternary quadratic + equation ``eq`` which has the form + `ax^2 + by^2 + cz^2 + fxy + gyz + hxz = 0`. + + Examples + ======== + + >>> from sympy import Tuple, ordered + >>> from sympy.abc import x, y, z + >>> from sympy.solvers.diophantine.diophantine import parametrize_ternary_quadratic + + The parametrized solution may be returned with three parameters: + + >>> parametrize_ternary_quadratic(2*x**2 + y**2 - 2*z**2) + (p**2 - 2*q**2, -2*p**2 + 4*p*q - 4*p*r - 4*q**2, p**2 - 4*p*q + 2*q**2 - 4*q*r) + + There might also be only two parameters: + + >>> parametrize_ternary_quadratic(4*x**2 + 2*y**2 - 3*z**2) + (2*p**2 - 3*q**2, -4*p**2 + 12*p*q - 6*q**2, 4*p**2 - 8*p*q + 6*q**2) + + Notes + ===== + + Consider ``p`` and ``q`` in the previous 2-parameter + solution and observe that more than one solution can be represented + by a given pair of parameters. If `p` and ``q`` are not coprime, this is + trivially true since the common factor will also be a common factor of the + solution values. But it may also be true even when ``p`` and + ``q`` are coprime: + + >>> sol = Tuple(*_) + >>> p, q = ordered(sol.free_symbols) + >>> sol.subs([(p, 3), (q, 2)]) + (6, 12, 12) + >>> sol.subs([(q, 1), (p, 1)]) + (-1, 2, 2) + >>> sol.subs([(q, 0), (p, 1)]) + (2, -4, 4) + >>> sol.subs([(q, 1), (p, 0)]) + (-3, -6, 6) + + Except for sign and a common factor, these are equivalent to + the solution of (1, 2, 2). + + References + ========== + + .. [1] The algorithmic resolution of Diophantine equations, Nigel P. Smart, + London Mathematical Society Student Texts 41, Cambridge University + Press, Cambridge, 1998. + + """ + var, coeff, diop_type = classify_diop(eq, _dict=False) + + if diop_type in ( + "homogeneous_ternary_quadratic", + "homogeneous_ternary_quadratic_normal"): + x_0, y_0, z_0 = list(_diop_ternary_quadratic(var, coeff))[0] + return _parametrize_ternary_quadratic( + (x_0, y_0, z_0), var, coeff) + + +def _parametrize_ternary_quadratic(solution, _var, coeff): + # called for a*x**2 + b*y**2 + c*z**2 + d*x*y + e*y*z + f*x*z = 0 + assert 1 not in coeff + + x_0, y_0, z_0 = solution + + v = list(_var) # copy + + if x_0 is None: + return (None, None, None) + + if solution.count(0) >= 2: + # if there are 2 zeros the equation reduces + # to k*X**2 == 0 where X is x, y, or z so X must + # be zero, too. So there is only the trivial + # solution. + return (None, None, None) + + if x_0 == 0: + v[0], v[1] = v[1], v[0] + y_p, x_p, z_p = _parametrize_ternary_quadratic( + (y_0, x_0, z_0), v, coeff) + return x_p, y_p, z_p + + x, y, z = v + r, p, q = symbols("r, p, q", integer=True) + + eq = sum(k*v for k, v in coeff.items()) + eq_1 = _mexpand(eq.subs(zip( + (x, y, z), (r*x_0, r*y_0 + p, r*z_0 + q)))) + A, B = eq_1.as_independent(r, as_Add=True) + + + x = A*x_0 + y = (A*y_0 - _mexpand(B/r*p)) + z = (A*z_0 - _mexpand(B/r*q)) + + return _remove_gcd(x, y, z) + + +def diop_ternary_quadratic_normal(eq, parameterize=False): + """ + Solves the quadratic ternary diophantine equation, + `ax^2 + by^2 + cz^2 = 0`. + + Explanation + =========== + + Here the coefficients `a`, `b`, and `c` should be non zero. Otherwise the + equation will be a quadratic binary or univariate equation. If solvable, + returns a tuple `(x, y, z)` that satisfies the given equation. If the + equation does not have integer solutions, `(None, None, None)` is returned. + + Usage + ===== + + ``diop_ternary_quadratic_normal(eq)``: where ``eq`` is an equation of the form + `ax^2 + by^2 + cz^2 = 0`. + + Examples + ======== + + >>> from sympy.abc import x, y, z + >>> from sympy.solvers.diophantine.diophantine import diop_ternary_quadratic_normal + >>> diop_ternary_quadratic_normal(x**2 + 3*y**2 - z**2) + (1, 0, 1) + >>> diop_ternary_quadratic_normal(4*x**2 + 5*y**2 - z**2) + (1, 0, 2) + >>> diop_ternary_quadratic_normal(34*x**2 - 3*y**2 - 301*z**2) + (4, 9, 1) + """ + var, coeff, diop_type = classify_diop(eq, _dict=False) + if diop_type == HomogeneousTernaryQuadraticNormal.name: + sol = _diop_ternary_quadratic_normal(var, coeff) + if len(sol) > 0: + x_0, y_0, z_0 = list(sol)[0] + else: + x_0, y_0, z_0 = None, None, None + if parameterize: + return _parametrize_ternary_quadratic( + (x_0, y_0, z_0), var, coeff) + return x_0, y_0, z_0 + + +def _diop_ternary_quadratic_normal(var, coeff): + eq = sum(i * coeff[i] for i in coeff) + return HomogeneousTernaryQuadraticNormal(eq, free_symbols=var).solve() + + +def sqf_normal(a, b, c, steps=False): + """ + Return `a', b', c'`, the coefficients of the square-free normal + form of `ax^2 + by^2 + cz^2 = 0`, where `a', b', c'` are pairwise + prime. If `steps` is True then also return three tuples: + `sq`, `sqf`, and `(a', b', c')` where `sq` contains the square + factors of `a`, `b` and `c` after removing the `gcd(a, b, c)`; + `sqf` contains the values of `a`, `b` and `c` after removing + both the `gcd(a, b, c)` and the square factors. + + The solutions for `ax^2 + by^2 + cz^2 = 0` can be + recovered from the solutions of `a'x^2 + b'y^2 + c'z^2 = 0`. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import sqf_normal + >>> sqf_normal(2 * 3**2 * 5, 2 * 5 * 11, 2 * 7**2 * 11) + (11, 1, 5) + >>> sqf_normal(2 * 3**2 * 5, 2 * 5 * 11, 2 * 7**2 * 11, True) + ((3, 1, 7), (5, 55, 11), (11, 1, 5)) + + References + ========== + + .. [1] Legendre's Theorem, Legrange's Descent, + https://public.csusm.edu/aitken_html/notes/legendre.pdf + + + See Also + ======== + + reconstruct() + """ + ABC = _remove_gcd(a, b, c) + sq = tuple(square_factor(i) for i in ABC) + sqf = A, B, C = tuple([i//j**2 for i,j in zip(ABC, sq)]) + pc = igcd(A, B) + A /= pc + B /= pc + pa = igcd(B, C) + B /= pa + C /= pa + pb = igcd(A, C) + A /= pb + B /= pb + + A *= pa + B *= pb + C *= pc + + if steps: + return (sq, sqf, (A, B, C)) + else: + return A, B, C + + +def square_factor(a): + r""" + Returns an integer `c` s.t. `a = c^2k, \ c,k \in Z`. Here `k` is square + free. `a` can be given as an integer or a dictionary of factors. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import square_factor + >>> square_factor(24) + 2 + >>> square_factor(-36*3) + 6 + >>> square_factor(1) + 1 + >>> square_factor({3: 2, 2: 1, -1: 1}) # -18 + 3 + + See Also + ======== + sympy.ntheory.factor_.core + """ + f = a if isinstance(a, dict) else factorint(a) + return Mul(*[p**(e//2) for p, e in f.items()]) + + +def reconstruct(A, B, z): + """ + Reconstruct the `z` value of an equivalent solution of `ax^2 + by^2 + cz^2` + from the `z` value of a solution of the square-free normal form of the + equation, `a'*x^2 + b'*y^2 + c'*z^2`, where `a'`, `b'` and `c'` are square + free and `gcd(a', b', c') == 1`. + """ + f = factorint(igcd(A, B)) + for p, e in f.items(): + if e != 1: + raise ValueError('a and b should be square-free') + z *= p + return z + + +def ldescent(A, B): + """ + Return a non-trivial solution to `w^2 = Ax^2 + By^2` using + Lagrange's method; return None if there is no such solution. + + Parameters + ========== + + A : Integer + B : Integer + non-zero integer + + Returns + ======= + + (int, int, int) | None : a tuple `(w_0, x_0, y_0)` which is a solution to the above equation. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import ldescent + >>> ldescent(1, 1) # w^2 = x^2 + y^2 + (1, 1, 0) + >>> ldescent(4, -7) # w^2 = 4x^2 - 7y^2 + (2, -1, 0) + + This means that `x = -1, y = 0` and `w = 2` is a solution to the equation + `w^2 = 4x^2 - 7y^2` + + >>> ldescent(5, -1) # w^2 = 5x^2 - y^2 + (2, 1, -1) + + References + ========== + + .. [1] The algorithmic resolution of Diophantine equations, Nigel P. Smart, + London Mathematical Society Student Texts 41, Cambridge University + Press, Cambridge, 1998. + .. [2] Cremona, J. E., Rusin, D. (2003). Efficient Solution of Rational Conics. + Mathematics of Computation, 72(243), 1417-1441. + https://doi.org/10.1090/S0025-5718-02-01480-1 + """ + if A == 0 or B == 0: + raise ValueError("A and B must be non-zero integers") + if abs(A) > abs(B): + w, y, x = ldescent(B, A) + return w, x, y + if A == 1: + return (1, 1, 0) + if B == 1: + return (1, 0, 1) + if B == -1: # and A == -1 + return + + r = sqrt_mod(A, B) + if r is None: + return + Q = (r**2 - A) // B + if Q == 0: + return r, -1, 0 + for i in divisors(Q): + d, _exact = integer_nthroot(abs(Q) // i, 2) + if _exact: + B_0 = sign(Q)*i + W, X, Y = ldescent(A, B_0) + return _remove_gcd(-A*X + r*W, r*X - W, Y*B_0*d) + + +def descent(A, B): + """ + Returns a non-trivial solution, (x, y, z), to `x^2 = Ay^2 + Bz^2` + using Lagrange's descent method with lattice-reduction. `A` and `B` + are assumed to be valid for such a solution to exist. + + This is faster than the normal Lagrange's descent algorithm because + the Gaussian reduction is used. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import descent + >>> descent(3, 1) # x**2 = 3*y**2 + z**2 + (1, 0, 1) + + `(x, y, z) = (1, 0, 1)` is a solution to the above equation. + + >>> descent(41, -113) + (-16, -3, 1) + + References + ========== + + .. [1] Cremona, J. E., Rusin, D. (2003). Efficient Solution of Rational Conics. + Mathematics of Computation, 72(243), 1417-1441. + https://doi.org/10.1090/S0025-5718-02-01480-1 + """ + if abs(A) > abs(B): + x, y, z = descent(B, A) + return x, z, y + + if B == 1: + return (1, 0, 1) + if A == 1: + return (1, 1, 0) + if B == -A: + return (0, 1, 1) + if B == A: + x, z, y = descent(-1, A) + return (A*y, z, x) + + w = sqrt_mod(A, B) + x_0, z_0 = gaussian_reduce(w, A, B) + + t = (x_0**2 - A*z_0**2) // B + t_2 = square_factor(t) + t_1 = t // t_2**2 + + x_1, z_1, y_1 = descent(A, t_1) + + return _remove_gcd(x_0*x_1 + A*z_0*z_1, z_0*x_1 + x_0*z_1, t_1*t_2*y_1) + + +def gaussian_reduce(w:int, a:int, b:int) -> tuple[int, int]: + r""" + Returns a reduced solution `(x, z)` to the congruence + `X^2 - aZ^2 \equiv 0 \pmod{b}` so that `x^2 + |a|z^2` is as small as possible. + Here ``w`` is a solution of the congruence `x^2 \equiv a \pmod{b}`. + + This function is intended to be used only for ``descent()``. + + Explanation + =========== + + The Gaussian reduction can find the shortest vector for any norm. + So we define the special norm for the vectors `u = (u_1, u_2)` and `v = (v_1, v_2)` as follows. + + .. math :: + u \cdot v := (wu_1 + bu_2)(wv_1 + bv_2) + |a|u_1v_1 + + Note that, given the mapping `f: (u_1, u_2) \to (wu_1 + bu_2, u_1)`, + `f((u_1,u_2))` is the solution to `X^2 - aZ^2 \equiv 0 \pmod{b}`. + In other words, finding the shortest vector in this norm will yield a solution with smaller `X^2 + |a|Z^2`. + The algorithm starts from basis vectors `(0, 1)` and `(1, 0)` + (corresponding to solutions `(b, 0)` and `(w, 1)`, respectively) and finds the shortest vector. + The shortest vector does not necessarily correspond to the smallest solution, + but since ``descent()`` only wants the smallest possible solution, it is sufficient. + + Parameters + ========== + + w : int + ``w`` s.t. `w^2 \equiv a \pmod{b}` + a : int + square-free nonzero integer + b : int + square-free nonzero integer + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import gaussian_reduce + >>> from sympy.ntheory.residue_ntheory import sqrt_mod + >>> a, b = 19, 101 + >>> gaussian_reduce(sqrt_mod(a, b), a, b) # 1**2 - 19*(-4)**2 = -303 + (1, -4) + >>> a, b = 11, 14 + >>> x, z = gaussian_reduce(sqrt_mod(a, b), a, b) + >>> (x**2 - a*z**2) % b == 0 + True + + It does not always return the smallest solution. + + >>> a, b = 6, 95 + >>> min_x, min_z = 1, 4 + >>> x, z = gaussian_reduce(sqrt_mod(a, b), a, b) + >>> (x**2 - a*z**2) % b == 0 and (min_x**2 - a*min_z**2) % b == 0 + True + >>> min_x**2 + abs(a)*min_z**2 < x**2 + abs(a)*z**2 + True + + References + ========== + + .. [1] Gaussian lattice Reduction [online]. Available: + https://web.archive.org/web/20201021115213/http://home.ie.cuhk.edu.hk/~wkshum/wordpress/?p=404 + .. [2] Cremona, J. E., Rusin, D. (2003). Efficient Solution of Rational Conics. + Mathematics of Computation, 72(243), 1417-1441. + https://doi.org/10.1090/S0025-5718-02-01480-1 + """ + a = abs(a) + def _dot(u, v): + return u[0]*v[0] + a*u[1]*v[1] + + u = (b, 0) + v = (w, 1) if b*w >= 0 else (-w, -1) + # i.e., _dot(u, v) >= 0 + + if b**2 < w**2 + a: + u, v = v, u + # i.e., norm(u) >= norm(v), where norm(u) := sqrt(_dot(u, u)) + + while _dot(u, u) > (dv := _dot(v, v)): + k = _dot(u, v) // dv + u, v = v, (u[0] - k*v[0], u[1] - k*v[1]) + c = (v[0] - u[0], v[1] - u[1]) + if _dot(c, c) <= _dot(u, u) <= 2*_dot(u, v): + return c + return u + + +def holzer(x, y, z, a, b, c): + r""" + Simplify the solution `(x, y, z)` of the equation + `ax^2 + by^2 = cz^2` with `a, b, c > 0` and `z^2 \geq \mid ab \mid` to + a new reduced solution `(x', y', z')` such that `z'^2 \leq \mid ab \mid`. + + The algorithm is an interpretation of Mordell's reduction as described + on page 8 of Cremona and Rusin's paper [1]_ and the work of Mordell in + reference [2]_. + + References + ========== + + .. [1] Cremona, J. E., Rusin, D. (2003). Efficient Solution of Rational Conics. + Mathematics of Computation, 72(243), 1417-1441. + https://doi.org/10.1090/S0025-5718-02-01480-1 + .. [2] Diophantine Equations, L. J. Mordell, page 48. + + """ + + if _odd(c): + k = 2*c + else: + k = c//2 + + small = a*b*c + step = 0 + while True: + t1, t2, t3 = a*x**2, b*y**2, c*z**2 + # check that it's a solution + if t1 + t2 != t3: + if step == 0: + raise ValueError('bad starting solution') + break + x_0, y_0, z_0 = x, y, z + if max(t1, t2, t3) <= small: + # Holzer condition + break + + uv = u, v = base_solution_linear(k, y_0, -x_0) + if None in uv: + break + + p, q = -(a*u*x_0 + b*v*y_0), c*z_0 + r = Rational(p, q) + if _even(c): + w = _nint_or_floor(p, q) + assert abs(w - r) <= S.Half + else: + w = p//q # floor + if _odd(a*u + b*v + c*w): + w += 1 + assert abs(w - r) <= S.One + + A = (a*u**2 + b*v**2 + c*w**2) + B = (a*u*x_0 + b*v*y_0 + c*w*z_0) + x = Rational(x_0*A - 2*u*B, k) + y = Rational(y_0*A - 2*v*B, k) + z = Rational(z_0*A - 2*w*B, k) + assert all(i.is_Integer for i in (x, y, z)) + step += 1 + + return tuple([int(i) for i in (x_0, y_0, z_0)]) + + +def diop_general_pythagorean(eq, param=symbols("m", integer=True)): + """ + Solves the general pythagorean equation, + `a_{1}^2x_{1}^2 + a_{2}^2x_{2}^2 + . . . + a_{n}^2x_{n}^2 - a_{n + 1}^2x_{n + 1}^2 = 0`. + + Returns a tuple which contains a parametrized solution to the equation, + sorted in the same order as the input variables. + + Usage + ===== + + ``diop_general_pythagorean(eq, param)``: where ``eq`` is a general + pythagorean equation which is assumed to be zero and ``param`` is the base + parameter used to construct other parameters by subscripting. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import diop_general_pythagorean + >>> from sympy.abc import a, b, c, d, e + >>> diop_general_pythagorean(a**2 + b**2 + c**2 - d**2) + (m1**2 + m2**2 - m3**2, 2*m1*m3, 2*m2*m3, m1**2 + m2**2 + m3**2) + >>> diop_general_pythagorean(9*a**2 - 4*b**2 + 16*c**2 + 25*d**2 + e**2) + (10*m1**2 + 10*m2**2 + 10*m3**2 - 10*m4**2, 15*m1**2 + 15*m2**2 + 15*m3**2 + 15*m4**2, 15*m1*m4, 12*m2*m4, 60*m3*m4) + """ + var, coeff, diop_type = classify_diop(eq, _dict=False) + + if diop_type == GeneralPythagorean.name: + if param is None: + params = None + else: + params = symbols('%s1:%i' % (param, len(var)), integer=True) + return list(GeneralPythagorean(eq).solve(parameters=params))[0] + + +def diop_general_sum_of_squares(eq, limit=1): + r""" + Solves the equation `x_{1}^2 + x_{2}^2 + . . . + x_{n}^2 - k = 0`. + + Returns at most ``limit`` number of solutions. + + Usage + ===== + + ``general_sum_of_squares(eq, limit)`` : Here ``eq`` is an expression which + is assumed to be zero. Also, ``eq`` should be in the form, + `x_{1}^2 + x_{2}^2 + . . . + x_{n}^2 - k = 0`. + + Details + ======= + + When `n = 3` if `k = 4^a(8m + 7)` for some `a, m \in Z` then there will be + no solutions. Refer to [1]_ for more details. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import diop_general_sum_of_squares + >>> from sympy.abc import a, b, c, d, e + >>> diop_general_sum_of_squares(a**2 + b**2 + c**2 + d**2 + e**2 - 2345) + {(15, 22, 22, 24, 24)} + + Reference + ========= + + .. [1] Representing an integer as a sum of three squares, [online], + Available: + https://proofwiki.org/wiki/Integer_as_Sum_of_Three_Squares + """ + var, coeff, diop_type = classify_diop(eq, _dict=False) + + if diop_type == GeneralSumOfSquares.name: + return set(GeneralSumOfSquares(eq).solve(limit=limit)) + + +def diop_general_sum_of_even_powers(eq, limit=1): + """ + Solves the equation `x_{1}^e + x_{2}^e + . . . + x_{n}^e - k = 0` + where `e` is an even, integer power. + + Returns at most ``limit`` number of solutions. + + Usage + ===== + + ``general_sum_of_even_powers(eq, limit)`` : Here ``eq`` is an expression which + is assumed to be zero. Also, ``eq`` should be in the form, + `x_{1}^e + x_{2}^e + . . . + x_{n}^e - k = 0`. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import diop_general_sum_of_even_powers + >>> from sympy.abc import a, b + >>> diop_general_sum_of_even_powers(a**4 + b**4 - (2**4 + 3**4)) + {(2, 3)} + + See Also + ======== + + power_representation + """ + var, coeff, diop_type = classify_diop(eq, _dict=False) + + if diop_type == GeneralSumOfEvenPowers.name: + return set(GeneralSumOfEvenPowers(eq).solve(limit=limit)) + + +## Functions below this comment can be more suitably grouped under +## an Additive number theory module rather than the Diophantine +## equation module. + + +def partition(n, k=None, zeros=False): + """ + Returns a generator that can be used to generate partitions of an integer + `n`. + + Explanation + =========== + + A partition of `n` is a set of positive integers which add up to `n`. For + example, partitions of 3 are 3, 1 + 2, 1 + 1 + 1. A partition is returned + as a tuple. If ``k`` equals None, then all possible partitions are returned + irrespective of their size, otherwise only the partitions of size ``k`` are + returned. If the ``zero`` parameter is set to True then a suitable + number of zeros are added at the end of every partition of size less than + ``k``. + + ``zero`` parameter is considered only if ``k`` is not None. When the + partitions are over, the last `next()` call throws the ``StopIteration`` + exception, so this function should always be used inside a try - except + block. + + Details + ======= + + ``partition(n, k)``: Here ``n`` is a positive integer and ``k`` is the size + of the partition which is also positive integer. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import partition + >>> f = partition(5) + >>> next(f) + (1, 1, 1, 1, 1) + >>> next(f) + (1, 1, 1, 2) + >>> g = partition(5, 3) + >>> next(g) + (1, 1, 3) + >>> next(g) + (1, 2, 2) + >>> g = partition(5, 3, zeros=True) + >>> next(g) + (0, 0, 5) + + """ + if not zeros or k is None: + for i in ordered_partitions(n, k): + yield tuple(i) + else: + for m in range(1, k + 1): + for i in ordered_partitions(n, m): + i = tuple(i) + yield (0,)*(k - len(i)) + i + + +def prime_as_sum_of_two_squares(p): + """ + Represent a prime `p` as a unique sum of two squares; this can + only be done if the prime is congruent to 1 mod 4. + + Parameters + ========== + + p : Integer + A prime that is congruent to 1 mod 4 + + Returns + ======= + + (int, int) | None : Pair of positive integers ``(x, y)`` satisfying ``x**2 + y**2 = p``. + None if ``p`` is not congruent to 1 mod 4. + + Raises + ====== + + ValueError + If ``p`` is not prime number + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import prime_as_sum_of_two_squares + >>> prime_as_sum_of_two_squares(7) # can't be done + >>> prime_as_sum_of_two_squares(5) + (1, 2) + + Reference + ========= + + .. [1] Representing a number as a sum of four squares, [online], + Available: https://schorn.ch/lagrange.html + + See Also + ======== + + sum_of_squares + + """ + p = as_int(p) + if p % 4 != 1: + return + if not isprime(p): + raise ValueError("p should be a prime number") + + if p % 8 == 5: + # Legendre symbol (2/p) == -1 if p % 8 in [3, 5] + b = 2 + elif p % 12 == 5: + # Legendre symbol (3/p) == -1 if p % 12 in [5, 7] + b = 3 + elif p % 5 in [2, 3]: + # Legendre symbol (5/p) == -1 if p % 5 in [2, 3] + b = 5 + else: + b = 7 + while jacobi(b, p) == 1: + b = nextprime(b) + + b = pow(b, p >> 2, p) + a = p + while b**2 > p: + a, b = b, a % b + return (int(a % b), int(b)) # convert from long + + +def sum_of_three_squares(n): + r""" + Returns a 3-tuple $(a, b, c)$ such that $a^2 + b^2 + c^2 = n$ and + $a, b, c \geq 0$. + + Returns None if $n = 4^a(8m + 7)$ for some `a, m \in \mathbb{Z}`. See + [1]_ for more details. + + Parameters + ========== + + n : Integer + non-negative integer + + Returns + ======= + + (int, int, int) | None : 3-tuple non-negative integers ``(a, b, c)`` satisfying ``a**2 + b**2 + c**2 = n``. + a,b,c are sorted in ascending order. ``None`` if no such ``(a,b,c)``. + + Raises + ====== + + ValueError + If ``n`` is a negative integer + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import sum_of_three_squares + >>> sum_of_three_squares(44542) + (18, 37, 207) + + References + ========== + + .. [1] Representing a number as a sum of three squares, [online], + Available: https://schorn.ch/lagrange.html + + See Also + ======== + + power_representation : + ``sum_of_three_squares(n)`` is one of the solutions output by ``power_representation(n, 2, 3, zeros=True)`` + + """ + # https://math.stackexchange.com/questions/483101/rabin-and-shallit-algorithm/651425#651425 + # discusses these numbers (except for 1, 2, 3) as the exceptions of H&L's conjecture that + # Every sufficiently large number n is either a square or the sum of a prime and a square. + special = {1: (0, 0, 1), 2: (0, 1, 1), 3: (1, 1, 1), 10: (0, 1, 3), 34: (3, 3, 4), + 58: (0, 3, 7), 85: (0, 6, 7), 130: (0, 3, 11), 214: (3, 6, 13), 226: (8, 9, 9), + 370: (8, 9, 15), 526: (6, 7, 21), 706: (15, 15, 16), 730: (0, 1, 27), + 1414: (6, 17, 33), 1906: (13, 21, 36), 2986: (21, 32, 39), 9634: (56, 57, 57)} + n = as_int(n) + if n < 0: + raise ValueError("n should be a non-negative integer") + if n == 0: + return (0, 0, 0) + n, v = remove(n, 4) + v = 1 << v + if n % 8 == 7: + return + if n in special: + return tuple([v*i for i in special[n]]) + + s, _exact = integer_nthroot(n, 2) + if _exact: + return (0, 0, v*s) + if n % 8 == 3: + if not s % 2: + s -= 1 + for x in range(s, -1, -2): + N = (n - x**2) // 2 + if isprime(N): + # n % 8 == 3 and x % 2 == 1 => N % 4 == 1 + y, z = prime_as_sum_of_two_squares(N) + return tuple(sorted([v*x, v*(y + z), v*abs(y - z)])) + # We will never reach this point because there must be a solution. + assert False + + # assert n % 4 in [1, 2] + if not((n % 2) ^ (s % 2)): + s -= 1 + for x in range(s, -1, -2): + N = n - x**2 + if isprime(N): + # assert N % 4 == 1 + y, z = prime_as_sum_of_two_squares(N) + return tuple(sorted([v*x, v*y, v*z])) + # We will never reach this point because there must be a solution. + assert False + + +def sum_of_four_squares(n): + r""" + Returns a 4-tuple `(a, b, c, d)` such that `a^2 + b^2 + c^2 + d^2 = n`. + Here `a, b, c, d \geq 0`. + + Parameters + ========== + + n : Integer + non-negative integer + + Returns + ======= + + (int, int, int, int) : 4-tuple non-negative integers ``(a, b, c, d)`` satisfying ``a**2 + b**2 + c**2 + d**2 = n``. + a,b,c,d are sorted in ascending order. + + Raises + ====== + + ValueError + If ``n`` is a negative integer + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import sum_of_four_squares + >>> sum_of_four_squares(3456) + (8, 8, 32, 48) + >>> sum_of_four_squares(1294585930293) + (0, 1234, 2161, 1137796) + + References + ========== + + .. [1] Representing a number as a sum of four squares, [online], + Available: https://schorn.ch/lagrange.html + + See Also + ======== + + power_representation : + ``sum_of_four_squares(n)`` is one of the solutions output by ``power_representation(n, 2, 4, zeros=True)`` + + """ + n = as_int(n) + if n < 0: + raise ValueError("n should be a non-negative integer") + if n == 0: + return (0, 0, 0, 0) + # remove factors of 4 since a solution in terms of 3 squares is + # going to be returned; this is also done in sum_of_three_squares, + # but it needs to be done here to select d + n, v = remove(n, 4) + v = 1 << v + if n % 8 == 7: + d = 2 + n = n - 4 + elif n % 8 in (2, 6): + d = 1 + n = n - 1 + else: + d = 0 + x, y, z = sum_of_three_squares(n) # sorted + return tuple(sorted([v*d, v*x, v*y, v*z])) + + +def power_representation(n, p, k, zeros=False): + r""" + Returns a generator for finding k-tuples of integers, + `(n_{1}, n_{2}, . . . n_{k})`, such that + `n = n_{1}^p + n_{2}^p + . . . n_{k}^p`. + + Usage + ===== + + ``power_representation(n, p, k, zeros)``: Represent non-negative number + ``n`` as a sum of ``k`` ``p``\ th powers. If ``zeros`` is true, then the + solutions is allowed to contain zeros. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import power_representation + + Represent 1729 as a sum of two cubes: + + >>> f = power_representation(1729, 3, 2) + >>> next(f) + (9, 10) + >>> next(f) + (1, 12) + + If the flag `zeros` is True, the solution may contain tuples with + zeros; any such solutions will be generated after the solutions + without zeros: + + >>> list(power_representation(125, 2, 3, zeros=True)) + [(5, 6, 8), (3, 4, 10), (0, 5, 10), (0, 2, 11)] + + For even `p` the `permute_sign` function can be used to get all + signed values: + + >>> from sympy.utilities.iterables import permute_signs + >>> list(permute_signs((1, 12))) + [(1, 12), (-1, 12), (1, -12), (-1, -12)] + + All possible signed permutations can also be obtained: + + >>> from sympy.utilities.iterables import signed_permutations + >>> list(signed_permutations((1, 12))) + [(1, 12), (-1, 12), (1, -12), (-1, -12), (12, 1), (-12, 1), (12, -1), (-12, -1)] + """ + n, p, k = [as_int(i) for i in (n, p, k)] + + if n < 0: + if p % 2: + for t in power_representation(-n, p, k, zeros): + yield tuple(-i for i in t) + return + + if p < 1 or k < 1: + raise ValueError(filldedent(''' + Expecting positive integers for `(p, k)`, but got `(%s, %s)`''' + % (p, k))) + + if n == 0: + if zeros: + yield (0,)*k + return + + if k == 1: + if p == 1: + yield (n,) + elif n == 1: + yield (1,) + else: + be = perfect_power(n) + if be: + b, e = be + d, r = divmod(e, p) + if not r: + yield (b**d,) + return + + if p == 1: + yield from partition(n, k, zeros=zeros) + return + + if p == 2: + if k == 3: + n, v = remove(n, 4) + if v: + v = 1 << v + for t in power_representation(n, p, k, zeros): + yield tuple(i*v for i in t) + return + feasible = _can_do_sum_of_squares(n, k) + if not feasible: + return + if not zeros: + if n > 33 and k >= 5 and k <= n and n - k in ( + 13, 10, 7, 5, 4, 2, 1): + '''Todd G. Will, "When Is n^2 a Sum of k Squares?", [online]. + Available: https://www.maa.org/sites/default/files/Will-MMz-201037918.pdf''' + return + # quick tests since feasibility includes the possibility of 0 + if k == 4 and (n in (1, 3, 5, 9, 11, 17, 29, 41) or remove(n, 4)[0] in (2, 6, 14)): + # A000534 + return + if k == 3 and n in (1, 2, 5, 10, 13, 25, 37, 58, 85, 130): # or n = some number >= 5*10**10 + # A051952 + return + if feasible is not True: # it's prime and k == 2 + yield prime_as_sum_of_two_squares(n) + return + + if k == 2 and p > 2: + be = perfect_power(n) + if be and be[1] % p == 0: + return # Fermat: a**n + b**n = c**n has no solution for n > 2 + + if n >= k: + a = integer_nthroot(n - (k - 1), p)[0] + for t in pow_rep_recursive(a, k, n, [], p): + yield tuple(reversed(t)) + + if zeros: + a = integer_nthroot(n, p)[0] + for i in range(1, k): + for t in pow_rep_recursive(a, i, n, [], p): + yield tuple(reversed(t + (0,)*(k - i))) + + +sum_of_powers = power_representation + + +def pow_rep_recursive(n_i, k, n_remaining, terms, p): + # Invalid arguments + if n_i <= 0 or k <= 0: + return + + # No solutions may exist + if n_remaining < k: + return + if k * pow(n_i, p) < n_remaining: + return + + if k == 0 and n_remaining == 0: + yield tuple(terms) + + elif k == 1: + # next_term^p must equal to n_remaining + next_term, exact = integer_nthroot(n_remaining, p) + if exact and next_term <= n_i: + yield tuple(terms + [next_term]) + return + + else: + # TODO: Fall back to diop_DN when k = 2 + if n_i >= 1 and k > 0: + for next_term in range(1, n_i + 1): + residual = n_remaining - pow(next_term, p) + if residual < 0: + break + yield from pow_rep_recursive(next_term, k - 1, residual, terms + [next_term], p) + + +def sum_of_squares(n, k, zeros=False): + """Return a generator that yields the k-tuples of nonnegative + values, the squares of which sum to n. If zeros is False (default) + then the solution will not contain zeros. The nonnegative + elements of a tuple are sorted. + + * If k == 1 and n is square, (n,) is returned. + + * If k == 2 then n can only be written as a sum of squares if + every prime in the factorization of n that has the form + 4*k + 3 has an even multiplicity. If n is prime then + it can only be written as a sum of two squares if it is + in the form 4*k + 1. + + * if k == 3 then n can be written as a sum of squares if it does + not have the form 4**m*(8*k + 7). + + * all integers can be written as the sum of 4 squares. + + * if k > 4 then n can be partitioned and each partition can + be written as a sum of 4 squares; if n is not evenly divisible + by 4 then n can be written as a sum of squares only if the + an additional partition can be written as sum of squares. + For example, if k = 6 then n is partitioned into two parts, + the first being written as a sum of 4 squares and the second + being written as a sum of 2 squares -- which can only be + done if the condition above for k = 2 can be met, so this will + automatically reject certain partitions of n. + + Examples + ======== + + >>> from sympy.solvers.diophantine.diophantine import sum_of_squares + >>> list(sum_of_squares(25, 2)) + [(3, 4)] + >>> list(sum_of_squares(25, 2, True)) + [(3, 4), (0, 5)] + >>> list(sum_of_squares(25, 4)) + [(1, 2, 2, 4)] + + See Also + ======== + + sympy.utilities.iterables.signed_permutations + """ + yield from power_representation(n, 2, k, zeros) + + +def _can_do_sum_of_squares(n, k): + """Return True if n can be written as the sum of k squares, + False if it cannot, or 1 if ``k == 2`` and ``n`` is prime (in which + case it *can* be written as a sum of two squares). A False + is returned only if it cannot be written as ``k``-squares, even + if 0s are allowed. + """ + if k < 1: + return False + if n < 0: + return False + if n == 0: + return True + if k == 1: + return is_square(n) + if k == 2: + if n in (1, 2): + return True + if isprime(n): + if n % 4 == 1: + return 1 # signal that it was prime + return False + # n is a composite number + # we can proceed iff no prime factor in the form 4*k + 3 + # has an odd multiplicity + return all(p % 4 !=3 or m % 2 == 0 for p, m in factorint(n).items()) + if k == 3: + return remove(n, 4)[0] % 8 != 7 + # every number can be written as a sum of 4 squares; for k > 4 partitions + # can be 0 + return True diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/diophantine/tests/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/diophantine/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/diophantine/tests/test_diophantine.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/diophantine/tests/test_diophantine.py new file mode 100644 index 0000000000000000000000000000000000000000..b8b031a1e63fa445e3dbb0b425f84cfe88888667 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/diophantine/tests/test_diophantine.py @@ -0,0 +1,1071 @@ +from sympy.core.add import Add +from sympy.core.mul import Mul +from sympy.core.numbers import (Rational, oo, pi) +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.matrices.dense import Matrix +from sympy.ntheory.factor_ import factorint +from sympy.simplify.powsimp import powsimp +from sympy.core.function import _mexpand +from sympy.core.sorting import default_sort_key, ordered +from sympy.functions.elementary.trigonometric import sin +from sympy.solvers.diophantine import diophantine +from sympy.solvers.diophantine.diophantine import (diop_DN, + diop_solve, diop_ternary_quadratic_normal, + diop_general_pythagorean, diop_ternary_quadratic, diop_linear, + diop_quadratic, diop_general_sum_of_squares, diop_general_sum_of_even_powers, + descent, diop_bf_DN, divisible, equivalent, find_DN, ldescent, length, + reconstruct, partition, power_representation, + prime_as_sum_of_two_squares, square_factor, sum_of_four_squares, + sum_of_three_squares, transformation_to_DN, transformation_to_normal, + classify_diop, base_solution_linear, cornacchia, sqf_normal, gaussian_reduce, holzer, + check_param, parametrize_ternary_quadratic, sum_of_powers, sum_of_squares, + _diop_ternary_quadratic_normal, _nint_or_floor, + _odd, _even, _remove_gcd, _can_do_sum_of_squares, DiophantineSolutionSet, GeneralPythagorean, + BinaryQuadratic) + +from sympy.testing.pytest import slow, raises, XFAIL +from sympy.utilities.iterables import ( + signed_permutations) + +a, b, c, d, p, q, x, y, z, w, t, u, v, X, Y, Z = symbols( + "a, b, c, d, p, q, x, y, z, w, t, u, v, X, Y, Z", integer=True) +t_0, t_1, t_2, t_3, t_4, t_5, t_6 = symbols("t_:7", integer=True) +m1, m2, m3 = symbols('m1:4', integer=True) +n1 = symbols('n1', integer=True) + + +def diop_simplify(eq): + return _mexpand(powsimp(_mexpand(eq))) + + +def test_input_format(): + raises(TypeError, lambda: diophantine(sin(x))) + raises(TypeError, lambda: diophantine(x/pi - 3)) + + +def test_nosols(): + # diophantine should sympify eq so that these are equivalent + assert diophantine(3) == set() + assert diophantine(S(3)) == set() + + +def test_univariate(): + assert diop_solve((x - 1)*(x - 2)**2) == {(1,), (2,)} + assert diop_solve((x - 1)*(x - 2)) == {(1,), (2,)} + + +def test_classify_diop(): + raises(TypeError, lambda: classify_diop(x**2/3 - 1)) + raises(ValueError, lambda: classify_diop(1)) + raises(NotImplementedError, lambda: classify_diop(w*x*y*z - 1)) + raises(NotImplementedError, lambda: classify_diop(x**3 + y**3 + z**4 - 90)) + assert classify_diop(14*x**2 + 15*x - 42) == ( + [x], {1: -42, x: 15, x**2: 14}, 'univariate') + assert classify_diop(x*y + z) == ( + [x, y, z], {x*y: 1, z: 1}, 'inhomogeneous_ternary_quadratic') + assert classify_diop(x*y + z + w + x**2) == ( + [w, x, y, z], {x*y: 1, w: 1, x**2: 1, z: 1}, 'inhomogeneous_general_quadratic') + assert classify_diop(x*y + x*z + x**2 + 1) == ( + [x, y, z], {x*y: 1, x*z: 1, x**2: 1, 1: 1}, 'inhomogeneous_general_quadratic') + assert classify_diop(x*y + z + w + 42) == ( + [w, x, y, z], {x*y: 1, w: 1, 1: 42, z: 1}, 'inhomogeneous_general_quadratic') + assert classify_diop(x*y + z*w) == ( + [w, x, y, z], {x*y: 1, w*z: 1}, 'homogeneous_general_quadratic') + assert classify_diop(x*y**2 + 1) == ( + [x, y], {x*y**2: 1, 1: 1}, 'cubic_thue') + assert classify_diop(x**4 + y**4 + z**4 - (1 + 16 + 81)) == ( + [x, y, z], {1: -98, x**4: 1, z**4: 1, y**4: 1}, 'general_sum_of_even_powers') + assert classify_diop(x**2 + y**2 + z**2) == ( + [x, y, z], {x**2: 1, y**2: 1, z**2: 1}, 'homogeneous_ternary_quadratic_normal') + + +def test_linear(): + assert diop_solve(x) == (0,) + assert diop_solve(1*x) == (0,) + assert diop_solve(3*x) == (0,) + assert diop_solve(x + 1) == (-1,) + assert diop_solve(2*x + 1) == (None,) + assert diop_solve(2*x + 4) == (-2,) + assert diop_solve(y + x) == (t_0, -t_0) + assert diop_solve(y + x + 0) == (t_0, -t_0) + assert diop_solve(y + x - 0) == (t_0, -t_0) + assert diop_solve(0*x - y - 5) == (-5,) + assert diop_solve(3*y + 2*x - 5) == (3*t_0 - 5, -2*t_0 + 5) + assert diop_solve(2*x - 3*y - 5) == (3*t_0 - 5, 2*t_0 - 5) + assert diop_solve(-2*x - 3*y - 5) == (3*t_0 + 5, -2*t_0 - 5) + assert diop_solve(7*x + 5*y) == (5*t_0, -7*t_0) + assert diop_solve(2*x + 4*y) == (-2*t_0, t_0) + assert diop_solve(4*x + 6*y - 4) == (3*t_0 - 2, -2*t_0 + 2) + assert diop_solve(4*x + 6*y - 3) == (None, None) + assert diop_solve(0*x + 3*y - 4*z + 5) == (4*t_0 + 5, 3*t_0 + 5) + assert diop_solve(4*x + 3*y - 4*z + 5) == (t_0, 8*t_0 + 4*t_1 + 5, 7*t_0 + 3*t_1 + 5) + assert diop_solve(4*x + 3*y - 4*z + 5, None) == (0, 5, 5) + assert diop_solve(4*x + 2*y + 8*z - 5) == (None, None, None) + assert diop_solve(5*x + 7*y - 2*z - 6) == (t_0, -3*t_0 + 2*t_1 + 6, -8*t_0 + 7*t_1 + 18) + assert diop_solve(3*x - 6*y + 12*z - 9) == (2*t_0 + 3, t_0 + 2*t_1, t_1) + assert diop_solve(6*w + 9*x + 20*y - z) == (t_0, t_1, t_1 + t_2, 6*t_0 + 29*t_1 + 20*t_2) + + # to ignore constant factors, use diophantine + raises(TypeError, lambda: diop_solve(x/2)) + + +def test_quadratic_simple_hyperbolic_case(): + # Simple Hyperbolic case: A = C = 0 and B != 0 + assert diop_solve(3*x*y + 34*x - 12*y + 1) == \ + {(-133, -11), (5, -57)} + assert diop_solve(6*x*y + 2*x + 3*y + 1) == set() + assert diop_solve(-13*x*y + 2*x - 4*y - 54) == {(27, 0)} + assert diop_solve(-27*x*y - 30*x - 12*y - 54) == {(-14, -1)} + assert diop_solve(2*x*y + 5*x + 56*y + 7) == {(-161, -3), (-47, -6), (-35, -12), + (-29, -69), (-27, 64), (-21, 7), + (-9, 1), (105, -2)} + assert diop_solve(6*x*y + 9*x + 2*y + 3) == set() + assert diop_solve(x*y + x + y + 1) == {(-1, t), (t, -1)} + assert diophantine(48*x*y) + + +def test_quadratic_elliptical_case(): + # Elliptical case: B**2 - 4AC < 0 + + assert diop_solve(42*x**2 + 8*x*y + 15*y**2 + 23*x + 17*y - 4915) == {(-11, -1)} + assert diop_solve(4*x**2 + 3*y**2 + 5*x - 11*y + 12) == set() + assert diop_solve(x**2 + y**2 + 2*x + 2*y + 2) == {(-1, -1)} + assert diop_solve(15*x**2 - 9*x*y + 14*y**2 - 23*x - 14*y - 4950) == {(-15, 6)} + assert diop_solve(10*x**2 + 12*x*y + 12*y**2 - 34) == \ + {(-1, -1), (-1, 2), (1, -2), (1, 1)} + + +def test_quadratic_parabolic_case(): + # Parabolic case: B**2 - 4AC = 0 + assert check_solutions(8*x**2 - 24*x*y + 18*y**2 + 5*x + 7*y + 16) + assert check_solutions(8*x**2 - 24*x*y + 18*y**2 + 6*x + 12*y - 6) + assert check_solutions(8*x**2 + 24*x*y + 18*y**2 + 4*x + 6*y - 7) + assert check_solutions(-4*x**2 + 4*x*y - y**2 + 2*x - 3) + assert check_solutions(x**2 + 2*x*y + y**2 + 2*x + 2*y + 1) + assert check_solutions(x**2 - 2*x*y + y**2 + 2*x + 2*y + 1) + assert check_solutions(y**2 - 41*x + 40) + + +def test_quadratic_perfect_square(): + # B**2 - 4*A*C > 0 + # B**2 - 4*A*C is a perfect square + assert check_solutions(48*x*y) + assert check_solutions(4*x**2 - 5*x*y + y**2 + 2) + assert check_solutions(-2*x**2 - 3*x*y + 2*y**2 -2*x - 17*y + 25) + assert check_solutions(12*x**2 + 13*x*y + 3*y**2 - 2*x + 3*y - 12) + assert check_solutions(8*x**2 + 10*x*y + 2*y**2 - 32*x - 13*y - 23) + assert check_solutions(4*x**2 - 4*x*y - 3*y- 8*x - 3) + assert check_solutions(- 4*x*y - 4*y**2 - 3*y- 5*x - 10) + assert check_solutions(x**2 - y**2 - 2*x - 2*y) + assert check_solutions(x**2 - 9*y**2 - 2*x - 6*y) + assert check_solutions(4*x**2 - 9*y**2 - 4*x - 12*y - 3) + + +def test_quadratic_non_perfect_square(): + # B**2 - 4*A*C is not a perfect square + # Used check_solutions() since the solutions are complex expressions involving + # square roots and exponents + assert check_solutions(x**2 - 2*x - 5*y**2) + assert check_solutions(3*x**2 - 2*y**2 - 2*x - 2*y) + assert check_solutions(x**2 - x*y - y**2 - 3*y) + assert check_solutions(x**2 - 9*y**2 - 2*x - 6*y) + assert BinaryQuadratic(x**2 + y**2 + 2*x + 2*y + 2).solve() == {(-1, -1)} + + +def test_issue_9106(): + eq = -48 - 2*x*(3*x - 1) + y*(3*y - 1) + v = (x, y) + for sol in diophantine(eq): + assert not diop_simplify(eq.xreplace(dict(zip(v, sol)))) + + +def test_issue_18138(): + eq = x**2 - x - y**2 + v = (x, y) + for sol in diophantine(eq): + assert not diop_simplify(eq.xreplace(dict(zip(v, sol)))) + + +@slow +def test_quadratic_non_perfect_slow(): + assert check_solutions(8*x**2 + 10*x*y - 2*y**2 - 32*x - 13*y - 23) + # This leads to very large numbers. + # assert check_solutions(5*x**2 - 13*x*y + y**2 - 4*x - 4*y - 15) + assert check_solutions(-3*x**2 - 2*x*y + 7*y**2 - 5*x - 7) + assert check_solutions(-4 - x + 4*x**2 - y - 3*x*y - 4*y**2) + assert check_solutions(1 + 2*x + 2*x**2 + 2*y + x*y - 2*y**2) + + +def test_DN(): + # Most of the test cases were adapted from, + # Solving the generalized Pell equation x**2 - D*y**2 = N, John P. Robertson, July 31, 2004. + # https://web.archive.org/web/20160323033128/http://www.jpr2718.org/pell.pdf + # others are verified using Wolfram Alpha. + + # Covers cases where D <= 0 or D > 0 and D is a square or N = 0 + # Solutions are straightforward in these cases. + assert diop_DN(3, 0) == [(0, 0)] + assert diop_DN(-17, -5) == [] + assert diop_DN(-19, 23) == [(2, 1)] + assert diop_DN(-13, 17) == [(2, 1)] + assert diop_DN(-15, 13) == [] + assert diop_DN(0, 5) == [] + assert diop_DN(0, 9) == [(3, t)] + assert diop_DN(9, 0) == [(3*t, t)] + assert diop_DN(16, 24) == [] + assert diop_DN(9, 180) == [(18, 4)] + assert diop_DN(9, -180) == [(12, 6)] + assert diop_DN(7, 0) == [(0, 0)] + + # When equation is x**2 + y**2 = N + # Solutions are interchangeable + assert diop_DN(-1, 5) == [(2, 1), (1, 2)] + assert diop_DN(-1, 169) == [(12, 5), (5, 12), (13, 0), (0, 13)] + + # D > 0 and D is not a square + + # N = 1 + assert diop_DN(13, 1) == [(649, 180)] + assert diop_DN(980, 1) == [(51841, 1656)] + assert diop_DN(981, 1) == [(158070671986249, 5046808151700)] + assert diop_DN(986, 1) == [(49299, 1570)] + assert diop_DN(991, 1) == [(379516400906811930638014896080, 12055735790331359447442538767)] + assert diop_DN(17, 1) == [(33, 8)] + assert diop_DN(19, 1) == [(170, 39)] + + # N = -1 + assert diop_DN(13, -1) == [(18, 5)] + assert diop_DN(991, -1) == [] + assert diop_DN(41, -1) == [(32, 5)] + assert diop_DN(290, -1) == [(17, 1)] + assert diop_DN(21257, -1) == [(13913102721304, 95427381109)] + assert diop_DN(32, -1) == [] + + # |N| > 1 + # Some tests were created using calculator at + # http://www.numbertheory.org/php/patz.html + + assert diop_DN(13, -4) == [(3, 1), (393, 109), (36, 10)] + # Source I referred returned (3, 1), (393, 109) and (-3, 1) as fundamental solutions + # So (-3, 1) and (393, 109) should be in the same equivalent class + assert equivalent(-3, 1, 393, 109, 13, -4) == True + + assert diop_DN(13, 27) == [(220, 61), (40, 11), (768, 213), (12, 3)] + assert set(diop_DN(157, 12)) == {(13, 1), (10663, 851), (579160, 46222), + (483790960, 38610722), (26277068347, 2097138361), + (21950079635497, 1751807067011)} + assert diop_DN(13, 25) == [(3245, 900)] + assert diop_DN(192, 18) == [] + assert diop_DN(23, 13) == [(-6, 1), (6, 1)] + assert diop_DN(167, 2) == [(13, 1)] + assert diop_DN(167, -2) == [] + + assert diop_DN(123, -2) == [(11, 1)] + # One calculator returned [(11, 1), (-11, 1)] but both of these are in + # the same equivalence class + assert equivalent(11, 1, -11, 1, 123, -2) + + assert diop_DN(123, -23) == [(-10, 1), (10, 1)] + + assert diop_DN(0, 0, t) == [(0, t)] + assert diop_DN(0, -1, t) == [] + + +def test_bf_pell(): + assert diop_bf_DN(13, -4) == [(3, 1), (-3, 1), (36, 10)] + assert diop_bf_DN(13, 27) == [(12, 3), (-12, 3), (40, 11), (-40, 11)] + assert diop_bf_DN(167, -2) == [] + assert diop_bf_DN(1729, 1) == [(44611924489705, 1072885712316)] + assert diop_bf_DN(89, -8) == [(9, 1), (-9, 1)] + assert diop_bf_DN(21257, -1) == [(13913102721304, 95427381109)] + assert diop_bf_DN(340, -4) == [(756, 41)] + assert diop_bf_DN(-1, 0, t) == [(0, 0)] + assert diop_bf_DN(0, 0, t) == [(0, t)] + assert diop_bf_DN(4, 0, t) == [(2*t, t), (-2*t, t)] + assert diop_bf_DN(3, 0, t) == [(0, 0)] + assert diop_bf_DN(1, -2, t) == [] + + +def test_length(): + assert length(2, 1, 0) == 1 + assert length(-2, 4, 5) == 3 + assert length(-5, 4, 17) == 4 + assert length(0, 4, 13) == 6 + assert length(7, 13, 11) == 23 + assert length(1, 6, 4) == 2 + + +def is_pell_transformation_ok(eq): + """ + Test whether X*Y, X, or Y terms are present in the equation + after transforming the equation using the transformation returned + by transformation_to_pell(). If they are not present we are good. + Moreover, coefficient of X**2 should be a divisor of coefficient of + Y**2 and the constant term. + """ + A, B = transformation_to_DN(eq) + u = (A*Matrix([X, Y]) + B)[0] + v = (A*Matrix([X, Y]) + B)[1] + simplified = diop_simplify(eq.subs(zip((x, y), (u, v)))) + + coeff = dict([reversed(t.as_independent(*[X, Y])) for t in simplified.args]) + + for term in [X*Y, X, Y]: + if term in coeff.keys(): + return False + + for term in [X**2, Y**2, 1]: + if term not in coeff.keys(): + coeff[term] = 0 + + if coeff[X**2] != 0: + return divisible(coeff[Y**2], coeff[X**2]) and \ + divisible(coeff[1], coeff[X**2]) + + return True + + +def test_transformation_to_pell(): + assert is_pell_transformation_ok(-13*x**2 - 7*x*y + y**2 + 2*x - 2*y - 14) + assert is_pell_transformation_ok(-17*x**2 + 19*x*y - 7*y**2 - 5*x - 13*y - 23) + assert is_pell_transformation_ok(x**2 - y**2 + 17) + assert is_pell_transformation_ok(-x**2 + 7*y**2 - 23) + assert is_pell_transformation_ok(25*x**2 - 45*x*y + 5*y**2 - 5*x - 10*y + 5) + assert is_pell_transformation_ok(190*x**2 + 30*x*y + y**2 - 3*y - 170*x - 130) + assert is_pell_transformation_ok(x**2 - 2*x*y -190*y**2 - 7*y - 23*x - 89) + assert is_pell_transformation_ok(15*x**2 - 9*x*y + 14*y**2 - 23*x - 14*y - 4950) + + +def test_find_DN(): + assert find_DN(x**2 - 2*x - y**2) == (1, 1) + assert find_DN(x**2 - 3*y**2 - 5) == (3, 5) + assert find_DN(x**2 - 2*x*y - 4*y**2 - 7) == (5, 7) + assert find_DN(4*x**2 - 8*x*y - y**2 - 9) == (20, 36) + assert find_DN(7*x**2 - 2*x*y - y**2 - 12) == (8, 84) + assert find_DN(-3*x**2 + 4*x*y -y**2) == (1, 0) + assert find_DN(-13*x**2 - 7*x*y + y**2 + 2*x - 2*y -14) == (101, -7825480) + + +def test_ldescent(): + # Equations which have solutions + u = ([(13, 23), (3, -11), (41, -113), (4, -7), (-7, 4), (91, -3), (1, 1), (1, -1), + (4, 32), (17, 13), (123689, 1), (19, -570)]) + for a, b in u: + w, x, y = ldescent(a, b) + assert a*x**2 + b*y**2 == w**2 + assert ldescent(-1, -1) is None + assert ldescent(2, 6) is None + + +def test_diop_ternary_quadratic_normal(): + assert check_solutions(234*x**2 - 65601*y**2 - z**2) + assert check_solutions(23*x**2 + 616*y**2 - z**2) + assert check_solutions(5*x**2 + 4*y**2 - z**2) + assert check_solutions(3*x**2 + 6*y**2 - 3*z**2) + assert check_solutions(x**2 + 3*y**2 - z**2) + assert check_solutions(4*x**2 + 5*y**2 - z**2) + assert check_solutions(x**2 + y**2 - z**2) + assert check_solutions(16*x**2 + y**2 - 25*z**2) + assert check_solutions(6*x**2 - y**2 + 10*z**2) + assert check_solutions(213*x**2 + 12*y**2 - 9*z**2) + assert check_solutions(34*x**2 - 3*y**2 - 301*z**2) + assert check_solutions(124*x**2 - 30*y**2 - 7729*z**2) + + +def is_normal_transformation_ok(eq): + A = transformation_to_normal(eq) + X, Y, Z = A*Matrix([x, y, z]) + simplified = diop_simplify(eq.subs(zip((x, y, z), (X, Y, Z)))) + + coeff = dict([reversed(t.as_independent(*[X, Y, Z])) for t in simplified.args]) + for term in [X*Y, Y*Z, X*Z]: + if term in coeff.keys(): + return False + + return True + + +def test_transformation_to_normal(): + assert is_normal_transformation_ok(x**2 + 3*y**2 + z**2 - 13*x*y - 16*y*z + 12*x*z) + assert is_normal_transformation_ok(x**2 + 3*y**2 - 100*z**2) + assert is_normal_transformation_ok(x**2 + 23*y*z) + assert is_normal_transformation_ok(3*y**2 - 100*z**2 - 12*x*y) + assert is_normal_transformation_ok(x**2 + 23*x*y - 34*y*z + 12*x*z) + assert is_normal_transformation_ok(z**2 + 34*x*y - 23*y*z + x*z) + assert is_normal_transformation_ok(x**2 + y**2 + z**2 - x*y - y*z - x*z) + assert is_normal_transformation_ok(x**2 + 2*y*z + 3*z**2) + assert is_normal_transformation_ok(x*y + 2*x*z + 3*y*z) + assert is_normal_transformation_ok(2*x*z + 3*y*z) + + +def test_diop_ternary_quadratic(): + assert check_solutions(2*x**2 + z**2 + y**2 - 4*x*y) + assert check_solutions(x**2 - y**2 - z**2 - x*y - y*z) + assert check_solutions(3*x**2 - x*y - y*z - x*z) + assert check_solutions(x**2 - y*z - x*z) + assert check_solutions(5*x**2 - 3*x*y - x*z) + assert check_solutions(4*x**2 - 5*y**2 - x*z) + assert check_solutions(3*x**2 + 2*y**2 - z**2 - 2*x*y + 5*y*z - 7*y*z) + assert check_solutions(8*x**2 - 12*y*z) + assert check_solutions(45*x**2 - 7*y**2 - 8*x*y - z**2) + assert check_solutions(x**2 - 49*y**2 - z**2 + 13*z*y -8*x*y) + assert check_solutions(90*x**2 + 3*y**2 + 5*x*y + 2*z*y + 5*x*z) + assert check_solutions(x**2 + 3*y**2 + z**2 - x*y - 17*y*z) + assert check_solutions(x**2 + 3*y**2 + z**2 - x*y - 16*y*z + 12*x*z) + assert check_solutions(x**2 + 3*y**2 + z**2 - 13*x*y - 16*y*z + 12*x*z) + assert check_solutions(x*y - 7*y*z + 13*x*z) + + assert diop_ternary_quadratic_normal(x**2 + y**2 + z**2) == (None, None, None) + assert diop_ternary_quadratic_normal(x**2 + y**2) is None + raises(ValueError, lambda: + _diop_ternary_quadratic_normal((x, y, z), + {x*y: 1, x**2: 2, y**2: 3, z**2: 0})) + eq = -2*x*y - 6*x*z + 7*y**2 - 3*y*z + 4*z**2 + assert diop_ternary_quadratic(eq) == (7, 2, 0) + assert diop_ternary_quadratic_normal(4*x**2 + 5*y**2 - z**2) == \ + (1, 0, 2) + assert diop_ternary_quadratic(x*y + 2*y*z) == \ + (-2, 0, n1) + eq = -5*x*y - 8*x*z - 3*y*z + 8*z**2 + assert parametrize_ternary_quadratic(eq) == \ + (8*p**2 - 3*p*q, -8*p*q + 8*q**2, 5*p*q) + # this cannot be tested with diophantine because it will + # factor into a product + assert diop_solve(x*y + 2*y*z) == (-2*p*q, -n1*p**2 + p**2, p*q) + + +def test_square_factor(): + assert square_factor(1) == square_factor(-1) == 1 + assert square_factor(0) == 1 + assert square_factor(5) == square_factor(-5) == 1 + assert square_factor(4) == square_factor(-4) == 2 + assert square_factor(12) == square_factor(-12) == 2 + assert square_factor(6) == 1 + assert square_factor(18) == 3 + assert square_factor(52) == 2 + assert square_factor(49) == 7 + assert square_factor(392) == 14 + assert square_factor(factorint(-12)) == 2 + + +def test_parametrize_ternary_quadratic(): + assert check_solutions(x**2 + y**2 - z**2) + assert check_solutions(x**2 + 2*x*y + z**2) + assert check_solutions(234*x**2 - 65601*y**2 - z**2) + assert check_solutions(3*x**2 + 2*y**2 - z**2 - 2*x*y + 5*y*z - 7*y*z) + assert check_solutions(x**2 - y**2 - z**2) + assert check_solutions(x**2 - 49*y**2 - z**2 + 13*z*y - 8*x*y) + assert check_solutions(8*x*y + z**2) + assert check_solutions(124*x**2 - 30*y**2 - 7729*z**2) + assert check_solutions(236*x**2 - 225*y**2 - 11*x*y - 13*y*z - 17*x*z) + assert check_solutions(90*x**2 + 3*y**2 + 5*x*y + 2*z*y + 5*x*z) + assert check_solutions(124*x**2 - 30*y**2 - 7729*z**2) + + +def test_no_square_ternary_quadratic(): + assert check_solutions(2*x*y + y*z - 3*x*z) + assert check_solutions(189*x*y - 345*y*z - 12*x*z) + assert check_solutions(23*x*y + 34*y*z) + assert check_solutions(x*y + y*z + z*x) + assert check_solutions(23*x*y + 23*y*z + 23*x*z) + + +def test_descent(): + + u = ([(13, 23), (3, -11), (41, -113), (91, -3), (1, 1), (1, -1), (17, 13), (123689, 1), (19, -570)]) + for a, b in u: + w, x, y = descent(a, b) + assert a*x**2 + b*y**2 == w**2 + # the docstring warns against bad input, so these are expected results + # - can't both be negative + raises(TypeError, lambda: descent(-1, -3)) + # A can't be zero unless B != 1 + raises(ZeroDivisionError, lambda: descent(0, 3)) + # supposed to be square-free + raises(TypeError, lambda: descent(4, 3)) + + +def test_diophantine(): + assert check_solutions((x - y)*(y - z)*(z - x)) + assert check_solutions((x - y)*(x**2 + y**2 - z**2)) + assert check_solutions((x - 3*y + 7*z)*(x**2 + y**2 - z**2)) + assert check_solutions(x**2 - 3*y**2 - 1) + assert check_solutions(y**2 + 7*x*y) + assert check_solutions(x**2 - 3*x*y + y**2) + assert check_solutions(z*(x**2 - y**2 - 15)) + assert check_solutions(x*(2*y - 2*z + 5)) + assert check_solutions((x**2 - 3*y**2 - 1)*(x**2 - y**2 - 15)) + assert check_solutions((x**2 - 3*y**2 - 1)*(y - 7*z)) + assert check_solutions((x**2 + y**2 - z**2)*(x - 7*y - 3*z + 4*w)) + # Following test case caused problems in parametric representation + # But this can be solved by factoring out y. + # No need to use methods for ternary quadratic equations. + assert check_solutions(y**2 - 7*x*y + 4*y*z) + assert check_solutions(x**2 - 2*x + 1) + + assert diophantine(x - y) == diophantine(Eq(x, y)) + # 18196 + eq = x**4 + y**4 - 97 + assert diophantine(eq, permute=True) == diophantine(-eq, permute=True) + assert diophantine(3*x*pi - 2*y*pi) == {(2*t_0, 3*t_0)} + eq = x**2 + y**2 + z**2 - 14 + base_sol = {(1, 2, 3)} + assert diophantine(eq) == base_sol + complete_soln = set(signed_permutations(base_sol.pop())) + assert diophantine(eq, permute=True) == complete_soln + + assert diophantine(x**2 + x*Rational(15, 14) - 3) == set() + # test issue 11049 + eq = 92*x**2 - 99*y**2 - z**2 + coeff = eq.as_coefficients_dict() + assert _diop_ternary_quadratic_normal((x, y, z), coeff) == \ + {(9, 7, 51)} + assert diophantine(eq) == {( + 891*p**2 + 9*q**2, -693*p**2 - 102*p*q + 7*q**2, + 5049*p**2 - 1386*p*q - 51*q**2)} + eq = 2*x**2 + 2*y**2 - z**2 + coeff = eq.as_coefficients_dict() + assert _diop_ternary_quadratic_normal((x, y, z), coeff) == \ + {(1, 1, 2)} + assert diophantine(eq) == {( + 2*p**2 - q**2, -2*p**2 + 4*p*q - q**2, + 4*p**2 - 4*p*q + 2*q**2)} + eq = 411*x**2+57*y**2-221*z**2 + coeff = eq.as_coefficients_dict() + assert _diop_ternary_quadratic_normal((x, y, z), coeff) == \ + {(2021, 2645, 3066)} + assert diophantine(eq) == \ + {(115197*p**2 - 446641*q**2, -150765*p**2 + 1355172*p*q - + 584545*q**2, 174762*p**2 - 301530*p*q + 677586*q**2)} + eq = 573*x**2+267*y**2-984*z**2 + coeff = eq.as_coefficients_dict() + assert _diop_ternary_quadratic_normal((x, y, z), coeff) == \ + {(49, 233, 127)} + assert diophantine(eq) == \ + {(4361*p**2 - 16072*q**2, -20737*p**2 + 83312*p*q - 76424*q**2, + 11303*p**2 - 41474*p*q + 41656*q**2)} + # this produces factors during reconstruction + eq = x**2 + 3*y**2 - 12*z**2 + coeff = eq.as_coefficients_dict() + assert _diop_ternary_quadratic_normal((x, y, z), coeff) == \ + {(0, 2, 1)} + assert diophantine(eq) == \ + {(24*p*q, 2*p**2 - 24*q**2, p**2 + 12*q**2)} + # solvers have not been written for every type + raises(NotImplementedError, lambda: diophantine(x*y**2 + 1)) + + # rational expressions + assert diophantine(1/x) == set() + assert diophantine(1/x + 1/y - S.Half) == {(6, 3), (-2, 1), (4, 4), (1, -2), (3, 6)} + assert diophantine(x**2 + y**2 +3*x- 5, permute=True) == \ + {(-1, 1), (-4, -1), (1, -1), (1, 1), (-4, 1), (-1, -1), (4, 1), (4, -1)} + + + #test issue 18186 + assert diophantine(y**4 + x**4 - 2**4 - 3**4, syms=(x, y), permute=True) == \ + {(-3, -2), (-3, 2), (-2, -3), (-2, 3), (2, -3), (2, 3), (3, -2), (3, 2)} + assert diophantine(y**4 + x**4 - 2**4 - 3**4, syms=(y, x), permute=True) == \ + {(-3, -2), (-3, 2), (-2, -3), (-2, 3), (2, -3), (2, 3), (3, -2), (3, 2)} + + # issue 18122 + assert check_solutions(x**2 - y) + assert check_solutions(y**2 - x) + assert diophantine((x**2 - y), t) == {(t, t**2)} + assert diophantine((y**2 - x), t) == {(t**2, t)} + + +def test_general_pythagorean(): + from sympy.abc import a, b, c, d, e + + assert check_solutions(a**2 + b**2 + c**2 - d**2) + assert check_solutions(a**2 + 4*b**2 + 4*c**2 - d**2) + assert check_solutions(9*a**2 + 4*b**2 + 4*c**2 - d**2) + assert check_solutions(9*a**2 + 4*b**2 - 25*d**2 + 4*c**2 ) + assert check_solutions(9*a**2 - 16*d**2 + 4*b**2 + 4*c**2) + assert check_solutions(-e**2 + 9*a**2 + 4*b**2 + 4*c**2 + 25*d**2) + assert check_solutions(16*a**2 - b**2 + 9*c**2 + d**2 + 25*e**2) + + assert GeneralPythagorean(a**2 + b**2 + c**2 - d**2).solve(parameters=[x, y, z]) == \ + {(x**2 + y**2 - z**2, 2*x*z, 2*y*z, x**2 + y**2 + z**2)} + + +def test_diop_general_sum_of_squares_quick(): + for i in range(3, 10): + assert check_solutions(sum(i**2 for i in symbols(':%i' % i)) - i) + + assert diop_general_sum_of_squares(x**2 + y**2 - 2) is None + assert diop_general_sum_of_squares(x**2 + y**2 + z**2 + 2) == set() + eq = x**2 + y**2 + z**2 - (1 + 4 + 9) + assert diop_general_sum_of_squares(eq) == \ + {(1, 2, 3)} + eq = u**2 + v**2 + x**2 + y**2 + z**2 - 1313 + assert len(diop_general_sum_of_squares(eq, 3)) == 3 + # issue 11016 + var = symbols(':5') + (symbols('6', negative=True),) + eq = Add(*[i**2 for i in var]) - 112 + + base_soln = {(0, 1, 1, 5, 6, -7), (1, 1, 1, 3, 6, -8), (2, 3, 3, 4, 5, -7), (0, 1, 1, 1, 3, -10), + (0, 0, 4, 4, 4, -8), (1, 2, 3, 3, 5, -8), (0, 1, 2, 3, 7, -7), (2, 2, 4, 4, 6, -6), + (1, 1, 3, 4, 6, -7), (0, 2, 3, 3, 3, -9), (0, 0, 2, 2, 2, -10), (1, 1, 2, 3, 4, -9), + (0, 1, 1, 2, 5, -9), (0, 0, 2, 6, 6, -6), (1, 3, 4, 5, 5, -6), (0, 2, 2, 2, 6, -8), + (0, 3, 3, 3, 6, -7), (0, 2, 3, 5, 5, -7), (0, 1, 5, 5, 5, -6)} + assert diophantine(eq) == base_soln + assert len(diophantine(eq, permute=True)) == 196800 + + # handle negated squares with signsimp + assert diophantine(12 - x**2 - y**2 - z**2) == {(2, 2, 2)} + # diophantine handles simplification, so classify_diop should + # not have to look for additional patterns that are removed + # by diophantine + eq = a**2 + b**2 + c**2 + d**2 - 4 + raises(NotImplementedError, lambda: classify_diop(-eq)) + + +def test_issue_23807(): + # fixes recursion error + eq = x**2 + y**2 + z**2 - 1000000 + base_soln = {(0, 0, 1000), (0, 352, 936), (480, 600, 640), (24, 640, 768), (192, 640, 744), + (192, 480, 856), (168, 224, 960), (0, 600, 800), (280, 576, 768), (152, 480, 864), + (0, 280, 960), (352, 360, 864), (424, 480, 768), (360, 480, 800), (224, 600, 768), + (96, 360, 928), (168, 576, 800), (96, 480, 872)} + + assert diophantine(eq) == base_soln + + +def test_diop_partition(): + for n in [8, 10]: + for k in range(1, 8): + for p in partition(n, k): + assert len(p) == k + assert list(partition(3, 5)) == [] + assert [list(p) for p in partition(3, 5, 1)] == [ + [0, 0, 0, 0, 3], [0, 0, 0, 1, 2], [0, 0, 1, 1, 1]] + assert list(partition(0)) == [()] + assert list(partition(1, 0)) == [()] + assert [list(i) for i in partition(3)] == [[1, 1, 1], [1, 2], [3]] + + +def test_prime_as_sum_of_two_squares(): + for i in [5, 13, 17, 29, 37, 41, 2341, 3557, 34841, 64601]: + a, b = prime_as_sum_of_two_squares(i) + assert a**2 + b**2 == i + assert prime_as_sum_of_two_squares(7) is None + ans = prime_as_sum_of_two_squares(800029) + assert ans == (450, 773) and type(ans[0]) is int + + +def test_sum_of_three_squares(): + for i in [0, 1, 2, 34, 123, 34304595905, 34304595905394941, 343045959052344, + 800, 801, 802, 803, 804, 805, 806]: + a, b, c = sum_of_three_squares(i) + assert a**2 + b**2 + c**2 == i + assert a >= 0 + + # error + raises(ValueError, lambda: sum_of_three_squares(-1)) + + assert sum_of_three_squares(7) is None + assert sum_of_three_squares((4**5)*15) is None + # if there are two zeros, there might be a solution + # with only one zero, e.g. 25 => (0, 3, 4) or + # with no zeros, e.g. 49 => (2, 3, 6) + assert sum_of_three_squares(25) == (0, 0, 5) + assert sum_of_three_squares(4) == (0, 0, 2) + + +def test_sum_of_four_squares(): + from sympy.core.random import randint + + # this should never fail + n = randint(1, 100000000000000) + assert sum(i**2 for i in sum_of_four_squares(n)) == n + + # error + raises(ValueError, lambda: sum_of_four_squares(-1)) + + for n in range(1000): + result = sum_of_four_squares(n) + assert len(result) == 4 + assert all(r >= 0 for r in result) + assert sum(r**2 for r in result) == n + assert list(result) == sorted(result) + + +def test_power_representation(): + tests = [(1729, 3, 2), (234, 2, 4), (2, 1, 2), (3, 1, 3), (5, 2, 2), (12352, 2, 4), + (32760, 2, 3)] + + for test in tests: + n, p, k = test + f = power_representation(n, p, k) + + while True: + try: + l = next(f) + assert len(l) == k + + chk_sum = 0 + for l_i in l: + chk_sum = chk_sum + l_i**p + assert chk_sum == n + + except StopIteration: + break + + assert list(power_representation(20, 2, 4, True)) == \ + [(1, 1, 3, 3), (0, 0, 2, 4)] + raises(ValueError, lambda: list(power_representation(1.2, 2, 2))) + raises(ValueError, lambda: list(power_representation(2, 0, 2))) + raises(ValueError, lambda: list(power_representation(2, 2, 0))) + assert list(power_representation(-1, 2, 2)) == [] + assert list(power_representation(1, 1, 1)) == [(1,)] + assert list(power_representation(3, 2, 1)) == [] + assert list(power_representation(4, 2, 1)) == [(2,)] + assert list(power_representation(3**4, 4, 6, zeros=True)) == \ + [(1, 2, 2, 2, 2, 2), (0, 0, 0, 0, 0, 3)] + assert list(power_representation(3**4, 4, 5, zeros=False)) == [] + assert list(power_representation(-2, 3, 2)) == [(-1, -1)] + assert list(power_representation(-2, 4, 2)) == [] + assert list(power_representation(0, 3, 2, True)) == [(0, 0)] + assert list(power_representation(0, 3, 2, False)) == [] + # when we are dealing with squares, do feasibility checks + assert len(list(power_representation(4**10*(8*10 + 7), 2, 3))) == 0 + # there will be a recursion error if these aren't recognized + big = 2**30 + for i in [13, 10, 7, 5, 4, 2, 1]: + assert list(sum_of_powers(big, 2, big - i)) == [] + + +def test_assumptions(): + """ + Test whether diophantine respects the assumptions. + """ + #Test case taken from the below so question regarding assumptions in diophantine module + #https://stackoverflow.com/questions/23301941/how-can-i-declare-natural-symbols-with-sympy + m, n = symbols('m n', integer=True, positive=True) + diof = diophantine(n**2 + m*n - 500) + assert diof == {(5, 20), (40, 10), (95, 5), (121, 4), (248, 2), (499, 1)} + + a, b = symbols('a b', integer=True, positive=False) + diof = diophantine(a*b + 2*a + 3*b - 6) + assert diof == {(-15, -3), (-9, -4), (-7, -5), (-6, -6), (-5, -8), (-4, -14)} + + x, y = symbols('x y', integer=True) + diof = diophantine(10*x**2 + 5*x*y - 3*y) + assert diof == {(1, -5), (-3, 5), (0, 0)} + + x, y = symbols('x y', integer=True, positive=True) + diof = diophantine(10*x**2 + 5*x*y - 3*y) + assert diof == set() + + x, y = symbols('x y', integer=True, negative=False) + diof = diophantine(10*x**2 + 5*x*y - 3*y) + assert diof == {(0, 0)} + + +def check_solutions(eq): + """ + Determines whether solutions returned by diophantine() satisfy the original + equation. Hope to generalize this so we can remove functions like check_ternay_quadratic, + check_solutions_normal, check_solutions() + """ + s = diophantine(eq) + + factors = Mul.make_args(eq) + + var = list(eq.free_symbols) + var.sort(key=default_sort_key) + + while s: + solution = s.pop() + for f in factors: + if diop_simplify(f.subs(zip(var, solution))) == 0: + break + else: + return False + return True + + +def test_diopcoverage(): + eq = (2*x + y + 1)**2 + assert diop_solve(eq) == {(t_0, -2*t_0 - 1)} + eq = 2*x**2 + 6*x*y + 12*x + 4*y**2 + 18*y + 18 + assert diop_solve(eq) == {(t, -t - 3), (-2*t - 3, t)} + assert diop_quadratic(x + y**2 - 3) == {(-t**2 + 3, t)} + + assert diop_linear(x + y - 3) == (t_0, 3 - t_0) + + assert base_solution_linear(0, 1, 2, t=None) == (0, 0) + ans = (3*t - 1, -2*t + 1) + assert base_solution_linear(4, 8, 12, t) == ans + assert base_solution_linear(4, 8, 12, t=None) == tuple(_.subs(t, 0) for _ in ans) + + assert cornacchia(1, 1, 20) == set() + assert cornacchia(1, 1, 5) == {(2, 1)} + assert cornacchia(1, 2, 17) == {(3, 2)} + + raises(ValueError, lambda: reconstruct(4, 20, 1)) + + assert gaussian_reduce(4, 1, 3) == (1, 1) + eq = -w**2 - x**2 - y**2 + z**2 + + assert diop_general_pythagorean(eq) == \ + diop_general_pythagorean(-eq) == \ + (m1**2 + m2**2 - m3**2, 2*m1*m3, + 2*m2*m3, m1**2 + m2**2 + m3**2) + + assert len(check_param(S(3) + x/3, S(4) + x/2, S(2), [x])) == 0 + assert len(check_param(Rational(3, 2), S(4) + x, S(2), [x])) == 0 + assert len(check_param(S(4) + x, Rational(3, 2), S(2), [x])) == 0 + + assert _nint_or_floor(16, 10) == 2 + assert _odd(1) == (not _even(1)) == True + assert _odd(0) == (not _even(0)) == False + assert _remove_gcd(2, 4, 6) == (1, 2, 3) + raises(TypeError, lambda: _remove_gcd((2, 4, 6))) + assert sqf_normal(2*3**2*5, 2*5*11, 2*7**2*11) == \ + (11, 1, 5) + + # it's ok if these pass some day when the solvers are implemented + raises(NotImplementedError, lambda: diophantine(x**2 + y**2 + x*y + 2*y*z - 12)) + raises(NotImplementedError, lambda: diophantine(x**3 + y**2)) + assert diop_quadratic(x**2 + y**2 - 1**2 - 3**4) == \ + {(-9, -1), (-9, 1), (-1, -9), (-1, 9), (1, -9), (1, 9), (9, -1), (9, 1)} + + +def test_holzer(): + # if the input is good, don't let it diverge in holzer() + # (but see test_fail_holzer below) + assert holzer(2, 7, 13, 4, 79, 23) == (2, 7, 13) + + # None in uv condition met; solution is not Holzer reduced + # so this will hopefully change but is here for coverage + assert holzer(2, 6, 2, 1, 1, 10) == (2, 6, 2) + + raises(ValueError, lambda: holzer(2, 7, 14, 4, 79, 23)) + + +@XFAIL +def test_fail_holzer(): + eq = lambda x, y, z: a*x**2 + b*y**2 - c*z**2 + a, b, c = 4, 79, 23 + x, y, z = xyz = 26, 1, 11 + X, Y, Z = ans = 2, 7, 13 + assert eq(*xyz) == 0 + assert eq(*ans) == 0 + assert max(a*x**2, b*y**2, c*z**2) <= a*b*c + assert max(a*X**2, b*Y**2, c*Z**2) <= a*b*c + h = holzer(x, y, z, a, b, c) + assert h == ans # it would be nice to get the smaller soln + + +def test_issue_9539(): + assert diophantine(6*w + 9*y + 20*x - z) == \ + {(t_0, t_1, t_1 + t_2, 6*t_0 + 29*t_1 + 9*t_2)} + + +def test_issue_8943(): + assert diophantine( + 3*(x**2 + y**2 + z**2) - 14*(x*y + y*z + z*x)) == \ + {(0, 0, 0)} + + +def test_diop_sum_of_even_powers(): + eq = x**4 + y**4 + z**4 - 2673 + assert diop_solve(eq) == {(3, 6, 6), (2, 4, 7)} + assert diop_general_sum_of_even_powers(eq, 2) == {(3, 6, 6), (2, 4, 7)} + raises(NotImplementedError, lambda: diop_general_sum_of_even_powers(-eq, 2)) + neg = symbols('neg', negative=True) + eq = x**4 + y**4 + neg**4 - 2673 + assert diop_general_sum_of_even_powers(eq) == {(-3, 6, 6)} + assert diophantine(x**4 + y**4 + 2) == set() + assert diop_general_sum_of_even_powers(x**4 + y**4 - 2, limit=0) == set() + + +def test_sum_of_squares_powers(): + tru = {(0, 0, 1, 1, 11), (0, 0, 5, 7, 7), (0, 1, 3, 7, 8), (0, 1, 4, 5, 9), (0, 3, 4, 7, 7), (0, 3, 5, 5, 8), + (1, 1, 2, 6, 9), (1, 1, 6, 6, 7), (1, 2, 3, 3, 10), (1, 3, 4, 4, 9), (1, 5, 5, 6, 6), (2, 2, 3, 5, 9), + (2, 3, 5, 6, 7), (3, 3, 4, 5, 8)} + eq = u**2 + v**2 + x**2 + y**2 + z**2 - 123 + ans = diop_general_sum_of_squares(eq, oo) # allow oo to be used + assert len(ans) == 14 + assert ans == tru + + raises(ValueError, lambda: list(sum_of_squares(10, -1))) + assert list(sum_of_squares(1, 1)) == [(1,)] + assert list(sum_of_squares(1, 2)) == [] + assert list(sum_of_squares(1, 2, True)) == [(0, 1)] + assert list(sum_of_squares(-10, 2)) == [] + assert list(sum_of_squares(2, 3)) == [] + assert list(sum_of_squares(0, 3, True)) == [(0, 0, 0)] + assert list(sum_of_squares(0, 3)) == [] + assert list(sum_of_squares(4, 1)) == [(2,)] + assert list(sum_of_squares(5, 1)) == [] + assert list(sum_of_squares(50, 2)) == [(5, 5), (1, 7)] + assert list(sum_of_squares(11, 5, True)) == [ + (1, 1, 1, 2, 2), (0, 0, 1, 1, 3)] + assert list(sum_of_squares(8, 8)) == [(1, 1, 1, 1, 1, 1, 1, 1)] + + assert [len(list(sum_of_squares(i, 5, True))) for i in range(30)] == [ + 1, 1, 1, 1, 2, + 2, 1, 1, 2, 2, + 2, 2, 2, 3, 2, + 1, 3, 3, 3, 3, + 4, 3, 3, 2, 2, + 4, 4, 4, 4, 5] + assert [len(list(sum_of_squares(i, 5))) for i in range(30)] == [ + 0, 0, 0, 0, 0, + 1, 0, 0, 1, 0, + 0, 1, 0, 1, 1, + 0, 1, 1, 0, 1, + 2, 1, 1, 1, 1, + 1, 1, 1, 1, 3] + for i in range(30): + s1 = set(sum_of_squares(i, 5, True)) + assert not s1 or all(sum(j**2 for j in t) == i for t in s1) + s2 = set(sum_of_squares(i, 5)) + assert all(sum(j**2 for j in t) == i for t in s2) + + raises(ValueError, lambda: list(sum_of_powers(2, -1, 1))) + raises(ValueError, lambda: list(sum_of_powers(2, 1, -1))) + assert list(sum_of_powers(-2, 3, 2)) == [(-1, -1)] + assert list(sum_of_powers(-2, 4, 2)) == [] + assert list(sum_of_powers(2, 1, 1)) == [(2,)] + assert list(sum_of_powers(2, 1, 3, True)) == [(0, 0, 2), (0, 1, 1)] + assert list(sum_of_powers(5, 1, 2, True)) == [(0, 5), (1, 4), (2, 3)] + assert list(sum_of_powers(6, 2, 2)) == [] + assert list(sum_of_powers(3**5, 3, 1)) == [] + assert list(sum_of_powers(3**6, 3, 1)) == [(9,)] and (9**3 == 3**6) + assert list(sum_of_powers(2**1000, 5, 2)) == [] + + +def test__can_do_sum_of_squares(): + assert _can_do_sum_of_squares(3, -1) is False + assert _can_do_sum_of_squares(-3, 1) is False + assert _can_do_sum_of_squares(0, 1) + assert _can_do_sum_of_squares(4, 1) + assert _can_do_sum_of_squares(1, 2) + assert _can_do_sum_of_squares(2, 2) + assert _can_do_sum_of_squares(3, 2) is False + + +def test_diophantine_permute_sign(): + from sympy.abc import a, b, c, d, e + eq = a**4 + b**4 - (2**4 + 3**4) + base_sol = {(2, 3)} + assert diophantine(eq) == base_sol + complete_soln = set(signed_permutations(base_sol.pop())) + assert diophantine(eq, permute=True) == complete_soln + + eq = a**2 + b**2 + c**2 + d**2 + e**2 - 234 + assert len(diophantine(eq)) == 35 + assert len(diophantine(eq, permute=True)) == 62000 + soln = {(-1, -1), (-1, 2), (1, -2), (1, 1)} + assert diophantine(10*x**2 + 12*x*y + 12*y**2 - 34, permute=True) == soln + + +@XFAIL +def test_not_implemented(): + eq = x**2 + y**4 - 1**2 - 3**4 + assert diophantine(eq, syms=[x, y]) == {(9, 1), (1, 3)} + + +def test_issue_9538(): + eq = x - 3*y + 2 + assert diophantine(eq, syms=[y,x]) == {(t_0, 3*t_0 - 2)} + raises(TypeError, lambda: diophantine(eq, syms={y, x})) + + +def test_ternary_quadratic(): + # solution with 3 parameters + s = diophantine(2*x**2 + y**2 - 2*z**2) + p, q, r = ordered(S(s).free_symbols) + assert s == {( + p**2 - 2*q**2, + -2*p**2 + 4*p*q - 4*p*r - 4*q**2, + p**2 - 4*p*q + 2*q**2 - 4*q*r)} + # solution with Mul in solution + s = diophantine(x**2 + 2*y**2 - 2*z**2) + assert s == {(4*p*q, p**2 - 2*q**2, p**2 + 2*q**2)} + # solution with no Mul in solution + s = diophantine(2*x**2 + 2*y**2 - z**2) + assert s == {(2*p**2 - q**2, -2*p**2 + 4*p*q - q**2, + 4*p**2 - 4*p*q + 2*q**2)} + # reduced form when parametrized + s = diophantine(3*x**2 + 72*y**2 - 27*z**2) + assert s == {(24*p**2 - 9*q**2, 6*p*q, 8*p**2 + 3*q**2)} + assert parametrize_ternary_quadratic( + 3*x**2 + 2*y**2 - z**2 - 2*x*y + 5*y*z - 7*y*z) == ( + 2*p**2 - 2*p*q - q**2, 2*p**2 + 2*p*q - q**2, 2*p**2 - + 2*p*q + 3*q**2) + assert parametrize_ternary_quadratic( + 124*x**2 - 30*y**2 - 7729*z**2) == ( + -1410*p**2 - 363263*q**2, 2700*p**2 + 30916*p*q - + 695610*q**2, -60*p**2 + 5400*p*q + 15458*q**2) + + +def test_diophantine_solution_set(): + s1 = DiophantineSolutionSet([], []) + assert set(s1) == set() + assert s1.symbols == () + assert s1.parameters == () + raises(ValueError, lambda: s1.add((x,))) + assert list(s1.dict_iterator()) == [] + + s2 = DiophantineSolutionSet([x, y], [t, u]) + assert s2.symbols == (x, y) + assert s2.parameters == (t, u) + raises(ValueError, lambda: s2.add((1,))) + s2.add((3, 4)) + assert set(s2) == {(3, 4)} + s2.update((3, 4), (-1, u)) + assert set(s2) == {(3, 4), (-1, u)} + raises(ValueError, lambda: s1.update(s2)) + assert list(s2.dict_iterator()) == [{x: -1, y: u}, {x: 3, y: 4}] + + s3 = DiophantineSolutionSet([x, y, z], [t, u]) + assert len(s3.parameters) == 2 + s3.add((t**2 + u, t - u, 1)) + assert set(s3) == {(t**2 + u, t - u, 1)} + assert s3.subs(t, 2) == {(u + 4, 2 - u, 1)} + assert s3(2) == {(u + 4, 2 - u, 1)} + assert s3.subs({t: 7, u: 8}) == {(57, -1, 1)} + assert s3(7, 8) == {(57, -1, 1)} + assert s3.subs({t: 5}) == {(u + 25, 5 - u, 1)} + assert s3(5) == {(u + 25, 5 - u, 1)} + assert s3.subs(u, -3) == {(t**2 - 3, t + 3, 1)} + assert s3(None, -3) == {(t**2 - 3, t + 3, 1)} + assert s3.subs({t: 2, u: 8}) == {(12, -6, 1)} + assert s3(2, 8) == {(12, -6, 1)} + assert s3.subs({t: 5, u: -3}) == {(22, 8, 1)} + assert s3(5, -3) == {(22, 8, 1)} + raises(TypeError, lambda: s3.subs(x=1)) + raises(TypeError, lambda: s3.subs(1, 2, 3)) + raises(ValueError, lambda: s3.add(())) + raises(ValueError, lambda: s3.add((1, 2, 3, 4))) + raises(ValueError, lambda: s3.add((1, 2))) + raises(ValueError, lambda: s3(1, 2, 3)) + raises(TypeError, lambda: s3(t=1)) + + s4 = DiophantineSolutionSet([x, y], [t, u]) + s4.add((t, 11*t)) + s4.add((-t, 22*t)) + assert s4(0, 0) == {(0, 0)} + + +def test_quadratic_parameter_passing(): + eq = -33*x*y + 3*y**2 + solution = BinaryQuadratic(eq).solve(parameters=[t, u]) + # test that parameters are passed all the way to the final solution + assert solution == {(t, 11*t), (t, -22*t)} + assert solution(0, 0) == {(0, 0)} + +def test_issue_18628(): + eq1 = x**2 - 15*x + y**2 - 8*y + sol = diophantine(eq1) + assert sol == {(15, 0), (15, 8), (-1, 4), (0, 0), (0, 8), (16, 4)} + eq2 = 2*x**2 - 9*x + 4*y**2 - 8*y + 14 + sol = diophantine(eq2) + assert sol == {(2, 1)} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/inequalities.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/inequalities.py new file mode 100644 index 0000000000000000000000000000000000000000..f50f7a7572ff25e8f48a4214d7b0c5ec6b5b35f3 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/inequalities.py @@ -0,0 +1,986 @@ +"""Tools for solving inequalities and systems of inequalities. """ +import itertools + +from sympy.calculus.util import (continuous_domain, periodicity, + function_range) +from sympy.core import sympify +from sympy.core.exprtools import factor_terms +from sympy.core.relational import Relational, Lt, Ge, Eq +from sympy.core.symbol import Symbol, Dummy +from sympy.sets.sets import Interval, FiniteSet, Union, Intersection +from sympy.core.singleton import S +from sympy.core.function import expand_mul +from sympy.functions.elementary.complexes import Abs +from sympy.logic import And +from sympy.polys import Poly, PolynomialError, parallel_poly_from_expr +from sympy.polys.polyutils import _nsort +from sympy.solvers.solveset import solvify, solveset +from sympy.utilities.iterables import sift, iterable +from sympy.utilities.misc import filldedent + + +def solve_poly_inequality(poly, rel): + """Solve a polynomial inequality with rational coefficients. + + Examples + ======== + + >>> from sympy import solve_poly_inequality, Poly + >>> from sympy.abc import x + + >>> solve_poly_inequality(Poly(x, x, domain='ZZ'), '==') + [{0}] + + >>> solve_poly_inequality(Poly(x**2 - 1, x, domain='ZZ'), '!=') + [Interval.open(-oo, -1), Interval.open(-1, 1), Interval.open(1, oo)] + + >>> solve_poly_inequality(Poly(x**2 - 1, x, domain='ZZ'), '==') + [{-1}, {1}] + + See Also + ======== + solve_poly_inequalities + """ + if not isinstance(poly, Poly): + raise ValueError( + 'For efficiency reasons, `poly` should be a Poly instance') + if poly.as_expr().is_number: + t = Relational(poly.as_expr(), 0, rel) + if t is S.true: + return [S.Reals] + elif t is S.false: + return [S.EmptySet] + else: + raise NotImplementedError( + "could not determine truth value of %s" % t) + + reals, intervals = poly.real_roots(multiple=False), [] + + if rel == '==': + for root, _ in reals: + interval = Interval(root, root) + intervals.append(interval) + elif rel == '!=': + left = S.NegativeInfinity + + for right, _ in reals + [(S.Infinity, 1)]: + interval = Interval(left, right, True, True) + intervals.append(interval) + left = right + else: + if poly.LC() > 0: + sign = +1 + else: + sign = -1 + + eq_sign, equal = None, False + + if rel == '>': + eq_sign = +1 + elif rel == '<': + eq_sign = -1 + elif rel == '>=': + eq_sign, equal = +1, True + elif rel == '<=': + eq_sign, equal = -1, True + else: + raise ValueError("'%s' is not a valid relation" % rel) + + right, right_open = S.Infinity, True + + for left, multiplicity in reversed(reals): + if multiplicity % 2: + if sign == eq_sign: + intervals.insert( + 0, Interval(left, right, not equal, right_open)) + + sign, right, right_open = -sign, left, not equal + else: + if sign == eq_sign and not equal: + intervals.insert( + 0, Interval(left, right, True, right_open)) + right, right_open = left, True + elif sign != eq_sign and equal: + intervals.insert(0, Interval(left, left)) + + if sign == eq_sign: + intervals.insert( + 0, Interval(S.NegativeInfinity, right, True, right_open)) + + return intervals + + +def solve_poly_inequalities(polys): + """Solve polynomial inequalities with rational coefficients. + + Examples + ======== + + >>> from sympy import Poly + >>> from sympy.solvers.inequalities import solve_poly_inequalities + >>> from sympy.abc import x + >>> solve_poly_inequalities((( + ... Poly(x**2 - 3), ">"), ( + ... Poly(-x**2 + 1), ">"))) + Union(Interval.open(-oo, -sqrt(3)), Interval.open(-1, 1), Interval.open(sqrt(3), oo)) + """ + return Union(*[s for p in polys for s in solve_poly_inequality(*p)]) + + +def solve_rational_inequalities(eqs): + """Solve a system of rational inequalities with rational coefficients. + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy import solve_rational_inequalities, Poly + + >>> solve_rational_inequalities([[ + ... ((Poly(-x + 1), Poly(1, x)), '>='), + ... ((Poly(-x + 1), Poly(1, x)), '<=')]]) + {1} + + >>> solve_rational_inequalities([[ + ... ((Poly(x), Poly(1, x)), '!='), + ... ((Poly(-x + 1), Poly(1, x)), '>=')]]) + Union(Interval.open(-oo, 0), Interval.Lopen(0, 1)) + + See Also + ======== + solve_poly_inequality + """ + result = S.EmptySet + + for _eqs in eqs: + if not _eqs: + continue + + global_intervals = [Interval(S.NegativeInfinity, S.Infinity)] + + for (numer, denom), rel in _eqs: + numer_intervals = solve_poly_inequality(numer*denom, rel) + denom_intervals = solve_poly_inequality(denom, '==') + + intervals = [] + + for numer_interval, global_interval in itertools.product( + numer_intervals, global_intervals): + interval = numer_interval.intersect(global_interval) + + if interval is not S.EmptySet: + intervals.append(interval) + + global_intervals = intervals + + intervals = [] + + for global_interval in global_intervals: + for denom_interval in denom_intervals: + global_interval -= denom_interval + + if global_interval is not S.EmptySet: + intervals.append(global_interval) + + global_intervals = intervals + + if not global_intervals: + break + + for interval in global_intervals: + result = result.union(interval) + + return result + + +def reduce_rational_inequalities(exprs, gen, relational=True): + """Reduce a system of rational inequalities with rational coefficients. + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.solvers.inequalities import reduce_rational_inequalities + + >>> x = Symbol('x', real=True) + + >>> reduce_rational_inequalities([[x**2 <= 0]], x) + Eq(x, 0) + + >>> reduce_rational_inequalities([[x + 2 > 0]], x) + -2 < x + >>> reduce_rational_inequalities([[(x + 2, ">")]], x) + -2 < x + >>> reduce_rational_inequalities([[x + 2]], x) + Eq(x, -2) + + This function find the non-infinite solution set so if the unknown symbol + is declared as extended real rather than real then the result may include + finiteness conditions: + + >>> y = Symbol('y', extended_real=True) + >>> reduce_rational_inequalities([[y + 2 > 0]], y) + (-2 < y) & (y < oo) + """ + exact = True + eqs = [] + solution = S.EmptySet # add pieces for each group + for _exprs in exprs: + if not _exprs: + continue + _eqs = [] + _sol = S.Reals + for expr in _exprs: + if isinstance(expr, tuple): + expr, rel = expr + else: + if expr.is_Relational: + expr, rel = expr.lhs - expr.rhs, expr.rel_op + else: + rel = '==' + + if expr is S.true: + numer, denom, rel = S.Zero, S.One, '==' + elif expr is S.false: + numer, denom, rel = S.One, S.One, '==' + else: + numer, denom = expr.together().as_numer_denom() + + try: + (numer, denom), opt = parallel_poly_from_expr( + (numer, denom), gen) + except PolynomialError: + raise PolynomialError(filldedent(''' + only polynomials and rational functions are + supported in this context. + ''')) + + if not opt.domain.is_Exact: + numer, denom, exact = numer.to_exact(), denom.to_exact(), False + + domain = opt.domain.get_exact() + + if not (domain.is_ZZ or domain.is_QQ): + expr = numer/denom + expr = Relational(expr, 0, rel) + _sol &= solve_univariate_inequality(expr, gen, relational=False) + else: + _eqs.append(((numer, denom), rel)) + + if _eqs: + _sol &= solve_rational_inequalities([_eqs]) + exclude = solve_rational_inequalities([[((d, d.one), '==') + for i in eqs for ((n, d), _) in i if d.has(gen)]]) + _sol -= exclude + + solution |= _sol + + if not exact and solution: + solution = solution.evalf() + + if relational: + solution = solution.as_relational(gen) + + return solution + + +def reduce_abs_inequality(expr, rel, gen): + """Reduce an inequality with nested absolute values. + + Examples + ======== + + >>> from sympy import reduce_abs_inequality, Abs, Symbol + >>> x = Symbol('x', real=True) + + >>> reduce_abs_inequality(Abs(x - 5) - 3, '<', x) + (2 < x) & (x < 8) + + >>> reduce_abs_inequality(Abs(x + 2)*3 - 13, '<', x) + (-19/3 < x) & (x < 7/3) + + See Also + ======== + + reduce_abs_inequalities + """ + if gen.is_extended_real is False: + raise TypeError(filldedent(''' + Cannot solve inequalities with absolute values containing + non-real variables. + ''')) + + def _bottom_up_scan(expr): + exprs = [] + + if expr.is_Add or expr.is_Mul: + op = expr.func + + for arg in expr.args: + _exprs = _bottom_up_scan(arg) + + if not exprs: + exprs = _exprs + else: + exprs = [(op(expr, _expr), conds + _conds) for (expr, conds), (_expr, _conds) in + itertools.product(exprs, _exprs)] + elif expr.is_Pow: + n = expr.exp + if not n.is_Integer: + raise ValueError("Only Integer Powers are allowed on Abs.") + + exprs.extend((expr**n, conds) for expr, conds in _bottom_up_scan(expr.base)) + elif isinstance(expr, Abs): + _exprs = _bottom_up_scan(expr.args[0]) + + for expr, conds in _exprs: + exprs.append(( expr, conds + [Ge(expr, 0)])) + exprs.append((-expr, conds + [Lt(expr, 0)])) + else: + exprs = [(expr, [])] + + return exprs + + mapping = {'<': '>', '<=': '>='} + inequalities = [] + + for expr, conds in _bottom_up_scan(expr): + if rel not in mapping.keys(): + expr = Relational( expr, 0, rel) + else: + expr = Relational(-expr, 0, mapping[rel]) + + inequalities.append([expr] + conds) + + return reduce_rational_inequalities(inequalities, gen) + + +def reduce_abs_inequalities(exprs, gen): + """Reduce a system of inequalities with nested absolute values. + + Examples + ======== + + >>> from sympy import reduce_abs_inequalities, Abs, Symbol + >>> x = Symbol('x', extended_real=True) + + >>> reduce_abs_inequalities([(Abs(3*x - 5) - 7, '<'), + ... (Abs(x + 25) - 13, '>')], x) + (-2/3 < x) & (x < 4) & (((-oo < x) & (x < -38)) | ((-12 < x) & (x < oo))) + + >>> reduce_abs_inequalities([(Abs(x - 4) + Abs(3*x - 5) - 7, '<')], x) + (1/2 < x) & (x < 4) + + See Also + ======== + + reduce_abs_inequality + """ + return And(*[ reduce_abs_inequality(expr, rel, gen) + for expr, rel in exprs ]) + + +def solve_univariate_inequality(expr, gen, relational=True, domain=S.Reals, continuous=False): + """Solves a real univariate inequality. + + Parameters + ========== + + expr : Relational + The target inequality + gen : Symbol + The variable for which the inequality is solved + relational : bool + A Relational type output is expected or not + domain : Set + The domain over which the equation is solved + continuous: bool + True if expr is known to be continuous over the given domain + (and so continuous_domain() does not need to be called on it) + + Raises + ====== + + NotImplementedError + The solution of the inequality cannot be determined due to limitation + in :func:`sympy.solvers.solveset.solvify`. + + Notes + ===== + + Currently, we cannot solve all the inequalities due to limitations in + :func:`sympy.solvers.solveset.solvify`. Also, the solution returned for trigonometric inequalities + are restricted in its periodic interval. + + See Also + ======== + + sympy.solvers.solveset.solvify: solver returning solveset solutions with solve's output API + + Examples + ======== + + >>> from sympy import solve_univariate_inequality, Symbol, sin, Interval, S + >>> x = Symbol('x') + + >>> solve_univariate_inequality(x**2 >= 4, x) + ((2 <= x) & (x < oo)) | ((-oo < x) & (x <= -2)) + + >>> solve_univariate_inequality(x**2 >= 4, x, relational=False) + Union(Interval(-oo, -2), Interval(2, oo)) + + >>> domain = Interval(0, S.Infinity) + >>> solve_univariate_inequality(x**2 >= 4, x, False, domain) + Interval(2, oo) + + >>> solve_univariate_inequality(sin(x) > 0, x, relational=False) + Interval.open(0, pi) + + """ + from sympy.solvers.solvers import denoms + + if domain.is_subset(S.Reals) is False: + raise NotImplementedError(filldedent(''' + Inequalities in the complex domain are + not supported. Try the real domain by + setting domain=S.Reals''')) + elif domain is not S.Reals: + rv = solve_univariate_inequality( + expr, gen, relational=False, continuous=continuous).intersection(domain) + if relational: + rv = rv.as_relational(gen) + return rv + else: + pass # continue with attempt to solve in Real domain + + # This keeps the function independent of the assumptions about `gen`. + # `solveset` makes sure this function is called only when the domain is + # real. + _gen = gen + _domain = domain + if gen.is_extended_real is False: + rv = S.EmptySet + return rv if not relational else rv.as_relational(_gen) + elif gen.is_extended_real is None: + gen = Dummy('gen', extended_real=True) + try: + expr = expr.xreplace({_gen: gen}) + except TypeError: + raise TypeError(filldedent(''' + When gen is real, the relational has a complex part + which leads to an invalid comparison like I < 0. + ''')) + + rv = None + + if expr is S.true: + rv = domain + + elif expr is S.false: + rv = S.EmptySet + + else: + e = expr.lhs - expr.rhs + period = periodicity(e, gen) + if period == S.Zero: + e = expand_mul(e) + const = expr.func(e, 0) + if const is S.true: + rv = domain + elif const is S.false: + rv = S.EmptySet + elif period is not None: + frange = function_range(e, gen, domain) + + rel = expr.rel_op + if rel in ('<', '<='): + if expr.func(frange.sup, 0): + rv = domain + elif not expr.func(frange.inf, 0): + rv = S.EmptySet + + elif rel in ('>', '>='): + if expr.func(frange.inf, 0): + rv = domain + elif not expr.func(frange.sup, 0): + rv = S.EmptySet + + inf, sup = domain.inf, domain.sup + if sup - inf is S.Infinity: + domain = Interval(0, period, False, True).intersect(_domain) + _domain = domain + + if rv is None: + n, d = e.as_numer_denom() + try: + if gen not in n.free_symbols and len(e.free_symbols) > 1: + raise ValueError + # this might raise ValueError on its own + # or it might give None... + solns = solvify(e, gen, domain) + if solns is None: + # in which case we raise ValueError + raise ValueError + except (ValueError, NotImplementedError): + # replace gen with generic x since it's + # univariate anyway + raise NotImplementedError(filldedent(''' + The inequality, %s, cannot be solved using + solve_univariate_inequality. + ''' % expr.subs(gen, Symbol('x')))) + + expanded_e = expand_mul(e) + def valid(x): + # this is used to see if gen=x satisfies the + # relational by substituting it into the + # expanded form and testing against 0, e.g. + # if expr = x*(x + 1) < 2 then e = x*(x + 1) - 2 + # and expanded_e = x**2 + x - 2; the test is + # whether a given value of x satisfies + # x**2 + x - 2 < 0 + # + # expanded_e, expr and gen used from enclosing scope + v = expanded_e.subs(gen, expand_mul(x)) + try: + r = expr.func(v, 0) + except TypeError: + r = S.false + if r in (S.true, S.false): + return r + if v.is_extended_real is False: + return S.false + else: + v = v.n(2) + if v.is_comparable: + return expr.func(v, 0) + # not comparable or couldn't be evaluated + raise NotImplementedError( + 'relationship did not evaluate: %s' % r) + + singularities = [] + for d in denoms(expr, gen): + singularities.extend(solvify(d, gen, domain)) + if not continuous: + domain = continuous_domain(expanded_e, gen, domain) + + include_x = '=' in expr.rel_op and expr.rel_op != '!=' + + try: + discontinuities = set(domain.boundary - + FiniteSet(domain.inf, domain.sup)) + # remove points that are not between inf and sup of domain + critical_points = FiniteSet(*(solns + singularities + list( + discontinuities))).intersection( + Interval(domain.inf, domain.sup, + domain.inf not in domain, domain.sup not in domain)) + if all(r.is_number for r in critical_points): + reals = _nsort(critical_points, separated=True)[0] + else: + sifted = sift(critical_points, lambda x: x.is_extended_real) + if sifted[None]: + # there were some roots that weren't known + # to be real + raise NotImplementedError + try: + reals = sifted[True] + if len(reals) > 1: + reals = sorted(reals) + except TypeError: + raise NotImplementedError + except NotImplementedError: + raise NotImplementedError('sorting of these roots is not supported') + + # If expr contains imaginary coefficients, only take real + # values of x for which the imaginary part is 0 + make_real = S.Reals + if (coeffI := expanded_e.coeff(S.ImaginaryUnit)) != S.Zero: + check = True + im_sol = FiniteSet() + try: + a = solveset(coeffI, gen, domain) + if not isinstance(a, Interval): + for z in a: + if z not in singularities and valid(z) and z.is_extended_real: + im_sol += FiniteSet(z) + else: + start, end = a.inf, a.sup + for z in _nsort(critical_points + FiniteSet(end)): + valid_start = valid(start) + if start != end: + valid_z = valid(z) + pt = _pt(start, z) + if pt not in singularities and pt.is_extended_real and valid(pt): + if valid_start and valid_z: + im_sol += Interval(start, z) + elif valid_start: + im_sol += Interval.Ropen(start, z) + elif valid_z: + im_sol += Interval.Lopen(start, z) + else: + im_sol += Interval.open(start, z) + start = z + for s in singularities: + im_sol -= FiniteSet(s) + except (TypeError): + im_sol = S.Reals + check = False + + if im_sol is S.EmptySet: + raise ValueError(filldedent(''' + %s contains imaginary parts which cannot be + made 0 for any value of %s satisfying the + inequality, leading to relations like I < 0. + ''' % (expr.subs(gen, _gen), _gen))) + + make_real = make_real.intersect(im_sol) + + sol_sets = [S.EmptySet] + + start = domain.inf + if start in domain and valid(start) and start.is_finite: + sol_sets.append(FiniteSet(start)) + + for x in reals: + end = x + + if valid(_pt(start, end)): + sol_sets.append(Interval(start, end, True, True)) + + if x in singularities: + singularities.remove(x) + else: + if x in discontinuities: + discontinuities.remove(x) + _valid = valid(x) + else: # it's a solution + _valid = include_x + if _valid: + sol_sets.append(FiniteSet(x)) + + start = end + + end = domain.sup + if end in domain and valid(end) and end.is_finite: + sol_sets.append(FiniteSet(end)) + + if valid(_pt(start, end)): + sol_sets.append(Interval.open(start, end)) + + if coeffI != S.Zero and check: + rv = (make_real).intersect(_domain) + else: + rv = Intersection( + (Union(*sol_sets)), make_real, _domain).subs(gen, _gen) + + return rv if not relational else rv.as_relational(_gen) + + +def _pt(start, end): + """Return a point between start and end""" + if not start.is_infinite and not end.is_infinite: + pt = (start + end)/2 + elif start.is_infinite and end.is_infinite: + pt = S.Zero + else: + if (start.is_infinite and start.is_extended_positive is None or + end.is_infinite and end.is_extended_positive is None): + raise ValueError('cannot proceed with unsigned infinite values') + if (end.is_infinite and end.is_extended_negative or + start.is_infinite and start.is_extended_positive): + start, end = end, start + # if possible, use a multiple of self which has + # better behavior when checking assumptions than + # an expression obtained by adding or subtracting 1 + if end.is_infinite: + if start.is_extended_positive: + pt = start*2 + elif start.is_extended_negative: + pt = start*S.Half + else: + pt = start + 1 + elif start.is_infinite: + if end.is_extended_positive: + pt = end*S.Half + elif end.is_extended_negative: + pt = end*2 + else: + pt = end - 1 + return pt + + +def _solve_inequality(ie, s, linear=False): + """Return the inequality with s isolated on the left, if possible. + If the relationship is non-linear, a solution involving And or Or + may be returned. False or True are returned if the relationship + is never True or always True, respectively. + + If `linear` is True (default is False) an `s`-dependent expression + will be isolated on the left, if possible + but it will not be solved for `s` unless the expression is linear + in `s`. Furthermore, only "safe" operations which do not change the + sense of the relationship are applied: no division by an unsigned + value is attempted unless the relationship involves Eq or Ne and + no division by a value not known to be nonzero is ever attempted. + + Examples + ======== + + >>> from sympy import Eq, Symbol + >>> from sympy.solvers.inequalities import _solve_inequality as f + >>> from sympy.abc import x, y + + For linear expressions, the symbol can be isolated: + + >>> f(x - 2 < 0, x) + x < 2 + >>> f(-x - 6 < x, x) + x > -3 + + Sometimes nonlinear relationships will be False + + >>> f(x**2 + 4 < 0, x) + False + + Or they may involve more than one region of values: + + >>> f(x**2 - 4 < 0, x) + (-2 < x) & (x < 2) + + To restrict the solution to a relational, set linear=True + and only the x-dependent portion will be isolated on the left: + + >>> f(x**2 - 4 < 0, x, linear=True) + x**2 < 4 + + Division of only nonzero quantities is allowed, so x cannot + be isolated by dividing by y: + + >>> y.is_nonzero is None # it is unknown whether it is 0 or not + True + >>> f(x*y < 1, x) + x*y < 1 + + And while an equality (or inequality) still holds after dividing by a + non-zero quantity + + >>> nz = Symbol('nz', nonzero=True) + >>> f(Eq(x*nz, 1), x) + Eq(x, 1/nz) + + the sign must be known for other inequalities involving > or <: + + >>> f(x*nz <= 1, x) + nz*x <= 1 + >>> p = Symbol('p', positive=True) + >>> f(x*p <= 1, x) + x <= 1/p + + When there are denominators in the original expression that + are removed by expansion, conditions for them will be returned + as part of the result: + + >>> f(x < x*(2/x - 1), x) + (x < 1) & Ne(x, 0) + """ + from sympy.solvers.solvers import denoms + if s not in ie.free_symbols: + return ie + if ie.rhs == s: + ie = ie.reversed + if ie.lhs == s and s not in ie.rhs.free_symbols: + return ie + + def classify(ie, s, i): + # return True or False if ie evaluates when substituting s with + # i else None (if unevaluated) or NaN (when there is an error + # in evaluating) + try: + v = ie.subs(s, i) + if v is S.NaN: + return v + elif v not in (True, False): + return + return v + except TypeError: + return S.NaN + + rv = None + oo = S.Infinity + expr = ie.lhs - ie.rhs + try: + p = Poly(expr, s) + if p.degree() == 0: + rv = ie.func(p.as_expr(), 0) + elif not linear and p.degree() > 1: + # handle in except clause + raise NotImplementedError + except (PolynomialError, NotImplementedError): + if not linear: + try: + rv = reduce_rational_inequalities([[ie]], s) + except PolynomialError: + rv = solve_univariate_inequality(ie, s) + # remove restrictions wrt +/-oo that may have been + # applied when using sets to simplify the relationship + okoo = classify(ie, s, oo) + if okoo is S.true and classify(rv, s, oo) is S.false: + rv = rv.subs(s < oo, True) + oknoo = classify(ie, s, -oo) + if (oknoo is S.true and + classify(rv, s, -oo) is S.false): + rv = rv.subs(-oo < s, True) + rv = rv.subs(s > -oo, True) + if rv is S.true: + rv = (s <= oo) if okoo is S.true else (s < oo) + if oknoo is not S.true: + rv = And(-oo < s, rv) + else: + p = Poly(expr) + + conds = [] + if rv is None: + e = p.as_expr() # this is in expanded form + # Do a safe inversion of e, moving non-s terms + # to the rhs and dividing by a nonzero factor if + # the relational is Eq/Ne; for other relationals + # the sign must also be positive or negative + rhs = 0 + b, ax = e.as_independent(s, as_Add=True) + e -= b + rhs -= b + ef = factor_terms(e) + a, e = ef.as_independent(s, as_Add=False) + if (a.is_zero != False or # don't divide by potential 0 + a.is_negative == + a.is_positive is None and # if sign is not known then + ie.rel_op not in ('!=', '==')): # reject if not Eq/Ne + e = ef + a = S.One + rhs /= a + if a.is_positive: + rv = ie.func(e, rhs) + else: + rv = ie.reversed.func(e, rhs) + + # return conditions under which the value is + # valid, too. + beginning_denoms = denoms(ie.lhs) | denoms(ie.rhs) + current_denoms = denoms(rv) + for d in beginning_denoms - current_denoms: + c = _solve_inequality(Eq(d, 0), s, linear=linear) + if isinstance(c, Eq) and c.lhs == s: + if classify(rv, s, c.rhs) is S.true: + # rv is permitting this value but it shouldn't + conds.append(~c) + for i in (-oo, oo): + if (classify(rv, s, i) is S.true and + classify(ie, s, i) is not S.true): + conds.append(s < i if i is oo else i < s) + + conds.append(rv) + return And(*conds) + + +def _reduce_inequalities(inequalities, symbols): + # helper for reduce_inequalities + + poly_part, abs_part = {}, {} + other = [] + + for inequality in inequalities: + + expr, rel = inequality.lhs, inequality.rel_op # rhs is 0 + + # check for gens using atoms which is more strict than free_symbols to + # guard against EX domain which won't be handled by + # reduce_rational_inequalities + gens = expr.atoms(Symbol) + + if len(gens) == 1: + gen = gens.pop() + else: + common = expr.free_symbols & symbols + if len(common) == 1: + gen = common.pop() + other.append(_solve_inequality(Relational(expr, 0, rel), gen)) + continue + else: + raise NotImplementedError(filldedent(''' + inequality has more than one symbol of interest. + ''')) + + if expr.is_polynomial(gen): + poly_part.setdefault(gen, []).append((expr, rel)) + else: + components = expr.find(lambda u: + u.has(gen) and ( + u.is_Function or u.is_Pow and not u.exp.is_Integer)) + if components and all(isinstance(i, Abs) for i in components): + abs_part.setdefault(gen, []).append((expr, rel)) + else: + other.append(_solve_inequality(Relational(expr, 0, rel), gen)) + + poly_reduced = [reduce_rational_inequalities([exprs], gen) for gen, exprs in poly_part.items()] + abs_reduced = [reduce_abs_inequalities(exprs, gen) for gen, exprs in abs_part.items()] + + return And(*(poly_reduced + abs_reduced + other)) + + +def reduce_inequalities(inequalities, symbols=[]): + """Reduce a system of inequalities with rational coefficients. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy import reduce_inequalities + + >>> reduce_inequalities(0 <= x + 3, []) + (-3 <= x) & (x < oo) + + >>> reduce_inequalities(0 <= x + y*2 - 1, [x]) + (x < oo) & (x >= 1 - 2*y) + """ + if not iterable(inequalities): + inequalities = [inequalities] + inequalities = [sympify(i) for i in inequalities] + + gens = set().union(*[i.free_symbols for i in inequalities]) + + if not iterable(symbols): + symbols = [symbols] + symbols = (set(symbols) or gens) & gens + if any(i.is_extended_real is False for i in symbols): + raise TypeError(filldedent(''' + inequalities cannot contain symbols that are not real. + ''')) + + # make vanilla symbol real + recast = {i: Dummy(i.name, extended_real=True) + for i in gens if i.is_extended_real is None} + inequalities = [i.xreplace(recast) for i in inequalities] + symbols = {i.xreplace(recast) for i in symbols} + + # prefilter + keep = [] + for i in inequalities: + if isinstance(i, Relational): + i = i.func(i.lhs.as_expr() - i.rhs.as_expr(), 0) + elif i not in (True, False): + i = Eq(i, 0) + if i == True: + continue + elif i == False: + return S.false + if i.lhs.is_number: + raise NotImplementedError( + "could not determine truth value of %s" % i) + keep.append(i) + inequalities = keep + del keep + + # solve system + rv = _reduce_inequalities(inequalities, symbols) + + # restore original symbols and return + return rv.xreplace({v: k for k, v in recast.items()}) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2b543425251dea6380a1860279cb6d636f3dd629 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/__init__.py @@ -0,0 +1,16 @@ +from .ode import (allhints, checkinfsol, classify_ode, + constantsimp, dsolve, homogeneous_order) + +from .lie_group import infinitesimals + +from .subscheck import checkodesol + +from .systems import (canonical_odes, linear_ode_to_matrix, + linodesolve) + + +__all__ = [ + 'allhints', 'checkinfsol', 'checkodesol', 'classify_ode', 'constantsimp', + 'dsolve', 'homogeneous_order', 'infinitesimals', 'canonical_odes', 'linear_ode_to_matrix', + 'linodesolve' +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/hypergeometric.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/hypergeometric.py new file mode 100644 index 0000000000000000000000000000000000000000..5699d2418058acaf48d1e4f87f6635a7b1f7284c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/hypergeometric.py @@ -0,0 +1,272 @@ +r''' +This module contains the implementation of the 2nd_hypergeometric hint for +dsolve. This is an incomplete implementation of the algorithm described in [1]. +The algorithm solves 2nd order linear ODEs of the form + +.. math:: y'' + A(x) y' + B(x) y = 0\text{,} + +where `A` and `B` are rational functions. The algorithm should find any +solution of the form + +.. math:: y = P(x) _pF_q(..; ..;\frac{\alpha x^k + \beta}{\gamma x^k + \delta})\text{,} + +where pFq is any of 2F1, 1F1 or 0F1 and `P` is an "arbitrary function". +Currently only the 2F1 case is implemented in SymPy but the other cases are +described in the paper and could be implemented in future (contributions +welcome!). + +References +========== + +.. [1] L. Chan, E.S. Cheb-Terrab, Non-Liouvillian solutions for second order + linear ODEs, (2004). + https://arxiv.org/abs/math-ph/0402063 +''' + +from sympy.core import S, Pow +from sympy.core.function import expand +from sympy.core.relational import Eq +from sympy.core.symbol import Symbol, Wild +from sympy.functions import exp, sqrt, hyper +from sympy.integrals import Integral +from sympy.polys import roots, gcd +from sympy.polys.polytools import cancel, factor +from sympy.simplify import collect, simplify, logcombine # type: ignore +from sympy.simplify.powsimp import powdenest +from sympy.solvers.ode.ode import get_numbered_constants + + +def match_2nd_hypergeometric(eq, func): + x = func.args[0] + df = func.diff(x) + a3 = Wild('a3', exclude=[func, func.diff(x), func.diff(x, 2)]) + b3 = Wild('b3', exclude=[func, func.diff(x), func.diff(x, 2)]) + c3 = Wild('c3', exclude=[func, func.diff(x), func.diff(x, 2)]) + deq = a3*(func.diff(x, 2)) + b3*df + c3*func + r = collect(eq, + [func.diff(x, 2), func.diff(x), func]).match(deq) + if r: + if not all(val.is_polynomial() for val in r.values()): + n, d = eq.as_numer_denom() + eq = expand(n) + r = collect(eq, [func.diff(x, 2), func.diff(x), func]).match(deq) + + if r and r[a3]!=0: + A = cancel(r[b3]/r[a3]) + B = cancel(r[c3]/r[a3]) + return [A, B] + else: + return [] + + +def equivalence_hypergeometric(A, B, func): + # This method for finding the equivalence is only for 2F1 type. + # We can extend it for 1F1 and 0F1 type also. + x = func.args[0] + + # making given equation in normal form + I1 = factor(cancel(A.diff(x)/2 + A**2/4 - B)) + + # computing shifted invariant(J1) of the equation + J1 = factor(cancel(x**2*I1 + S(1)/4)) + num, dem = J1.as_numer_denom() + num = powdenest(expand(num)) + dem = powdenest(expand(dem)) + # this function will compute the different powers of variable(x) in J1. + # then it will help in finding value of k. k is power of x such that we can express + # J1 = x**k * J0(x**k) then all the powers in J0 become integers. + def _power_counting(num): + _pow = {0} + for val in num: + if val.has(x): + if isinstance(val, Pow) and val.as_base_exp()[0] == x: + _pow.add(val.as_base_exp()[1]) + elif val == x: + _pow.add(val.as_base_exp()[1]) + else: + _pow.update(_power_counting(val.args)) + return _pow + + pow_num = _power_counting((num, )) + pow_dem = _power_counting((dem, )) + pow_dem.update(pow_num) + + _pow = pow_dem + k = gcd(_pow) + + # computing I0 of the given equation + I0 = powdenest(simplify(factor(((J1/k**2) - S(1)/4)/((x**k)**2))), force=True) + I0 = factor(cancel(powdenest(I0.subs(x, x**(S(1)/k)), force=True))) + + # Before this point I0, J1 might be functions of e.g. sqrt(x) but replacing + # x with x**(1/k) should result in I0 being a rational function of x or + # otherwise the hypergeometric solver cannot be used. Note that k can be a + # non-integer rational such as 2/7. + if not I0.is_rational_function(x): + return None + + num, dem = I0.as_numer_denom() + + max_num_pow = max(_power_counting((num, ))) + dem_args = dem.args + sing_point = [] + dem_pow = [] + # calculating singular point of I0. + for arg in dem_args: + if arg.has(x): + if isinstance(arg, Pow): + # (x-a)**n + dem_pow.append(arg.as_base_exp()[1]) + sing_point.append(list(roots(arg.as_base_exp()[0], x).keys())[0]) + else: + # (x-a) type + dem_pow.append(arg.as_base_exp()[1]) + sing_point.append(list(roots(arg, x).keys())[0]) + + dem_pow.sort() + # checking if equivalence is exists or not. + + if equivalence(max_num_pow, dem_pow) == "2F1": + return {'I0':I0, 'k':k, 'sing_point':sing_point, 'type':"2F1"} + else: + return None + + +def match_2nd_2F1_hypergeometric(I, k, sing_point, func): + x = func.args[0] + a = Wild("a") + b = Wild("b") + c = Wild("c") + t = Wild("t") + s = Wild("s") + r = Wild("r") + alpha = Wild("alpha") + beta = Wild("beta") + gamma = Wild("gamma") + delta = Wild("delta") + # I0 of the standard 2F1 equation. + I0 = ((a-b+1)*(a-b-1)*x**2 + 2*((1-a-b)*c + 2*a*b)*x + c*(c-2))/(4*x**2*(x-1)**2) + if sing_point != [0, 1]: + # If singular point is [0, 1] then we have standard equation. + eqs = [] + sing_eqs = [-beta/alpha, -delta/gamma, (delta-beta)/(alpha-gamma)] + # making equations for the finding the mobius transformation + for i in range(3): + if i>> from sympy import Function, Eq, pprint + >>> from sympy.abc import x, y + >>> xi, eta, h = map(Function, ['xi', 'eta', 'h']) + >>> h = h(x, y) # dy/dx = h + >>> eta = eta(x, y) + >>> xi = xi(x, y) + >>> genform = Eq(eta.diff(x) + (eta.diff(y) - xi.diff(x))*h + ... - (xi.diff(y))*h**2 - xi*(h.diff(x)) - eta*(h.diff(y)), 0) + >>> pprint(genform) + /d d \ d 2 d d d + |--(eta(x, y)) - --(xi(x, y))|*h(x, y) - eta(x, y)*--(h(x, y)) - h (x, y)*--(xi(x, y)) - xi(x, y)*--(h(x, y)) + --(eta(x, y)) = 0 + \dy dx / dy dy dx dx + + Solving the above mentioned PDE is not trivial, and can be solved only by + making intelligent assumptions for `\xi` and `\eta` (heuristics). Once an + infinitesimal is found, the attempt to find more heuristics stops. This is done to + optimise the speed of solving the differential equation. If a list of all the + infinitesimals is needed, ``hint`` should be flagged as ``all``, which gives + the complete list of infinitesimals. If the infinitesimals for a particular + heuristic needs to be found, it can be passed as a flag to ``hint``. + + Examples + ======== + + >>> from sympy import Function + >>> from sympy.solvers.ode.lie_group import infinitesimals + >>> from sympy.abc import x + >>> f = Function('f') + >>> eq = f(x).diff(x) - x**2*f(x) + >>> infinitesimals(eq) + [{eta(x, f(x)): exp(x**3/3), xi(x, f(x)): 0}] + + References + ========== + + - Solving differential equations by Symmetry Groups, + John Starrett, pp. 1 - pp. 14 + + """ + + if isinstance(eq, Equality): + eq = eq.lhs - eq.rhs + if not func: + eq, func = _preprocess(eq) + variables = func.args + if len(variables) != 1: + raise ValueError("ODE's have only one independent variable") + else: + x = variables[0] + if not order: + order = ode_order(eq, func) + if order != 1: + raise NotImplementedError("Infinitesimals for only " + "first order ODE's have been implemented") + else: + df = func.diff(x) + # Matching differential equation of the form a*df + b + a = Wild('a', exclude = [df]) + b = Wild('b', exclude = [df]) + if match: # Used by lie_group hint + h = match['h'] + y = match['y'] + else: + match = collect(expand(eq), df).match(a*df + b) + if match: + h = -simplify(match[b]/match[a]) + else: + try: + sol = solve(eq, df) + except NotImplementedError: + raise NotImplementedError("Infinitesimals for the " + "first order ODE could not be found") + else: + h = sol[0] # Find infinitesimals for one solution + y = Dummy("y") + h = h.subs(func, y) + + u = Dummy("u") + hx = h.diff(x) + hy = h.diff(y) + hinv = ((1/h).subs([(x, u), (y, x)])).subs(u, y) # Inverse ODE + match = {'h': h, 'func': func, 'hx': hx, 'hy': hy, 'y': y, 'hinv': hinv} + if hint == 'all': + xieta = [] + for heuristic in lie_heuristics: + function = globals()['lie_heuristic_' + heuristic] + inflist = function(match, comp=True) + if inflist: + xieta.extend([inf for inf in inflist if inf not in xieta]) + if xieta: + return xieta + else: + raise NotImplementedError("Infinitesimals could not be found for " + "the given ODE") + + elif hint == 'default': + for heuristic in lie_heuristics: + function = globals()['lie_heuristic_' + heuristic] + xieta = function(match, comp=False) + if xieta: + return xieta + + raise NotImplementedError("Infinitesimals could not be found for" + " the given ODE") + + elif hint not in lie_heuristics: + raise ValueError("Heuristic not recognized: " + hint) + + else: + function = globals()['lie_heuristic_' + hint] + xieta = function(match, comp=True) + if xieta: + return xieta + else: + raise ValueError("Infinitesimals could not be found using the" + " given heuristic") + + +def lie_heuristic_abaco1_simple(match, comp=False): + r""" + The first heuristic uses the following four sets of + assumptions on `\xi` and `\eta` + + .. math:: \xi = 0, \eta = f(x) + + .. math:: \xi = 0, \eta = f(y) + + .. math:: \xi = f(x), \eta = 0 + + .. math:: \xi = f(y), \eta = 0 + + The success of this heuristic is determined by algebraic factorisation. + For the first assumption `\xi = 0` and `\eta` to be a function of `x`, the PDE + + .. math:: \frac{\partial \eta}{\partial x} + (\frac{\partial \eta}{\partial y} + - \frac{\partial \xi}{\partial x})*h + - \frac{\partial \xi}{\partial y}*h^{2} + - \xi*\frac{\partial h}{\partial x} - \eta*\frac{\partial h}{\partial y} = 0 + + reduces to `f'(x) - f\frac{\partial h}{\partial y} = 0` + If `\frac{\partial h}{\partial y}` is a function of `x`, then this can usually + be integrated easily. A similar idea is applied to the other 3 assumptions as well. + + + References + ========== + + - E.S Cheb-Terrab, L.G.S Duarte and L.A,C.P da Mota, Computer Algebra + Solving of First Order ODEs Using Symmetry Methods, pp. 8 + + + """ + + xieta = [] + y = match['y'] + h = match['h'] + func = match['func'] + x = func.args[0] + hx = match['hx'] + hy = match['hy'] + xi = Function('xi')(x, func) + eta = Function('eta')(x, func) + + hysym = hy.free_symbols + if y not in hysym: + try: + fx = exp(integrate(hy, x)) + except NotImplementedError: + pass + else: + inf = {xi: S.Zero, eta: fx} + if not comp: + return [inf] + if comp and inf not in xieta: + xieta.append(inf) + + factor = hy/h + facsym = factor.free_symbols + if x not in facsym: + try: + fy = exp(integrate(factor, y)) + except NotImplementedError: + pass + else: + inf = {xi: S.Zero, eta: fy.subs(y, func)} + if not comp: + return [inf] + if comp and inf not in xieta: + xieta.append(inf) + + factor = -hx/h + facsym = factor.free_symbols + if y not in facsym: + try: + fx = exp(integrate(factor, x)) + except NotImplementedError: + pass + else: + inf = {xi: fx, eta: S.Zero} + if not comp: + return [inf] + if comp and inf not in xieta: + xieta.append(inf) + + factor = -hx/(h**2) + facsym = factor.free_symbols + if x not in facsym: + try: + fy = exp(integrate(factor, y)) + except NotImplementedError: + pass + else: + inf = {xi: fy.subs(y, func), eta: S.Zero} + if not comp: + return [inf] + if comp and inf not in xieta: + xieta.append(inf) + + if xieta: + return xieta + +def lie_heuristic_abaco1_product(match, comp=False): + r""" + The second heuristic uses the following two assumptions on `\xi` and `\eta` + + .. math:: \eta = 0, \xi = f(x)*g(y) + + .. math:: \eta = f(x)*g(y), \xi = 0 + + The first assumption of this heuristic holds good if + `\frac{1}{h^{2}}\frac{\partial^2}{\partial x \partial y}\log(h)` is + separable in `x` and `y`, then the separated factors containing `x` + is `f(x)`, and `g(y)` is obtained by + + .. math:: e^{\int f\frac{\partial}{\partial x}\left(\frac{1}{f*h}\right)\,dy} + + provided `f\frac{\partial}{\partial x}\left(\frac{1}{f*h}\right)` is a function + of `y` only. + + The second assumption holds good if `\frac{dy}{dx} = h(x, y)` is rewritten as + `\frac{dy}{dx} = \frac{1}{h(y, x)}` and the same properties of the first assumption + satisfies. After obtaining `f(x)` and `g(y)`, the coordinates are again + interchanged, to get `\eta` as `f(x)*g(y)` + + + References + ========== + - E.S. Cheb-Terrab, A.D. Roche, Symmetries and First Order + ODE Patterns, pp. 7 - pp. 8 + + """ + + xieta = [] + y = match['y'] + h = match['h'] + hinv = match['hinv'] + func = match['func'] + x = func.args[0] + xi = Function('xi')(x, func) + eta = Function('eta')(x, func) + + + inf = separatevars(((log(h).diff(y)).diff(x))/h**2, dict=True, symbols=[x, y]) + if inf and inf['coeff']: + fx = inf[x] + gy = simplify(fx*((1/(fx*h)).diff(x))) + gysyms = gy.free_symbols + if x not in gysyms: + gy = exp(integrate(gy, y)) + inf = {eta: S.Zero, xi: (fx*gy).subs(y, func)} + if not comp: + return [inf] + if comp and inf not in xieta: + xieta.append(inf) + + u1 = Dummy("u1") + inf = separatevars(((log(hinv).diff(y)).diff(x))/hinv**2, dict=True, symbols=[x, y]) + if inf and inf['coeff']: + fx = inf[x] + gy = simplify(fx*((1/(fx*hinv)).diff(x))) + gysyms = gy.free_symbols + if x not in gysyms: + gy = exp(integrate(gy, y)) + etaval = fx*gy + etaval = (etaval.subs([(x, u1), (y, x)])).subs(u1, y) + inf = {eta: etaval.subs(y, func), xi: S.Zero} + if not comp: + return [inf] + if comp and inf not in xieta: + xieta.append(inf) + + if xieta: + return xieta + +def lie_heuristic_bivariate(match, comp=False): + r""" + The third heuristic assumes the infinitesimals `\xi` and `\eta` + to be bi-variate polynomials in `x` and `y`. The assumption made here + for the logic below is that `h` is a rational function in `x` and `y` + though that may not be necessary for the infinitesimals to be + bivariate polynomials. The coefficients of the infinitesimals + are found out by substituting them in the PDE and grouping similar terms + that are polynomials and since they form a linear system, solve and check + for non trivial solutions. The degree of the assumed bivariates + are increased till a certain maximum value. + + References + ========== + - Lie Groups and Differential Equations + pp. 327 - pp. 329 + + """ + + h = match['h'] + hx = match['hx'] + hy = match['hy'] + func = match['func'] + x = func.args[0] + y = match['y'] + xi = Function('xi')(x, func) + eta = Function('eta')(x, func) + + if h.is_rational_function(): + # The maximum degree that the infinitesimals can take is + # calculated by this technique. + etax, etay, etad, xix, xiy, xid = symbols("etax etay etad xix xiy xid") + ipde = etax + (etay - xix)*h - xiy*h**2 - xid*hx - etad*hy + num, denom = cancel(ipde).as_numer_denom() + deg = Poly(num, x, y).total_degree() + deta = Function('deta')(x, y) + dxi = Function('dxi')(x, y) + ipde = (deta.diff(x) + (deta.diff(y) - dxi.diff(x))*h - (dxi.diff(y))*h**2 + - dxi*hx - deta*hy) + xieq = Symbol("xi0") + etaeq = Symbol("eta0") + + for i in range(deg + 1): + if i: + xieq += Add(*[ + Symbol("xi_" + str(power) + "_" + str(i - power))*x**power*y**(i - power) + for power in range(i + 1)]) + etaeq += Add(*[ + Symbol("eta_" + str(power) + "_" + str(i - power))*x**power*y**(i - power) + for power in range(i + 1)]) + pden, denom = (ipde.subs({dxi: xieq, deta: etaeq}).doit()).as_numer_denom() + pden = expand(pden) + + # If the individual terms are monomials, the coefficients + # are grouped + if pden.is_polynomial(x, y) and pden.is_Add: + polyy = Poly(pden, x, y).as_dict() + if polyy: + symset = xieq.free_symbols.union(etaeq.free_symbols) - {x, y} + soldict = solve(polyy.values(), *symset) + if isinstance(soldict, list): + soldict = soldict[0] + if any(soldict.values()): + xired = xieq.subs(soldict) + etared = etaeq.subs(soldict) + # Scaling is done by substituting one for the parameters + # This can be any number except zero. + dict_ = dict.fromkeys(symset, 1) + inf = {eta: etared.subs(dict_).subs(y, func), + xi: xired.subs(dict_).subs(y, func)} + return [inf] + +def lie_heuristic_chi(match, comp=False): + r""" + The aim of the fourth heuristic is to find the function `\chi(x, y)` + that satisfies the PDE `\frac{d\chi}{dx} + h\frac{d\chi}{dx} + - \frac{\partial h}{\partial y}\chi = 0`. + + This assumes `\chi` to be a bivariate polynomial in `x` and `y`. By intuition, + `h` should be a rational function in `x` and `y`. The method used here is + to substitute a general binomial for `\chi` up to a certain maximum degree + is reached. The coefficients of the polynomials, are calculated by by collecting + terms of the same order in `x` and `y`. + + After finding `\chi`, the next step is to use `\eta = \xi*h + \chi`, to + determine `\xi` and `\eta`. This can be done by dividing `\chi` by `h` + which would give `-\xi` as the quotient and `\eta` as the remainder. + + + References + ========== + - E.S Cheb-Terrab, L.G.S Duarte and L.A,C.P da Mota, Computer Algebra + Solving of First Order ODEs Using Symmetry Methods, pp. 8 + + """ + + h = match['h'] + hy = match['hy'] + func = match['func'] + x = func.args[0] + y = match['y'] + xi = Function('xi')(x, func) + eta = Function('eta')(x, func) + + if h.is_rational_function(): + schi, schix, schiy = symbols("schi, schix, schiy") + cpde = schix + h*schiy - hy*schi + num, denom = cancel(cpde).as_numer_denom() + deg = Poly(num, x, y).total_degree() + + chi = Function('chi')(x, y) + chix = chi.diff(x) + chiy = chi.diff(y) + cpde = chix + h*chiy - hy*chi + chieq = Symbol("chi") + for i in range(1, deg + 1): + chieq += Add(*[ + Symbol("chi_" + str(power) + "_" + str(i - power))*x**power*y**(i - power) + for power in range(i + 1)]) + cnum, cden = cancel(cpde.subs({chi : chieq}).doit()).as_numer_denom() + cnum = expand(cnum) + if cnum.is_polynomial(x, y) and cnum.is_Add: + cpoly = Poly(cnum, x, y).as_dict() + if cpoly: + solsyms = chieq.free_symbols - {x, y} + soldict = solve(cpoly.values(), *solsyms) + if isinstance(soldict, list): + soldict = soldict[0] + if any(soldict.values()): + chieq = chieq.subs(soldict) + dict_ = dict.fromkeys(solsyms, 1) + chieq = chieq.subs(dict_) + # After finding chi, the main aim is to find out + # eta, xi by the equation eta = xi*h + chi + # One method to set xi, would be rearranging it to + # (eta/h) - xi = (chi/h). This would mean dividing + # chi by h would give -xi as the quotient and eta + # as the remainder. Thanks to Sean Vig for suggesting + # this method. + xic, etac = div(chieq, h) + inf = {eta: etac.subs(y, func), xi: -xic.subs(y, func)} + return [inf] + +def lie_heuristic_function_sum(match, comp=False): + r""" + This heuristic uses the following two assumptions on `\xi` and `\eta` + + .. math:: \eta = 0, \xi = f(x) + g(y) + + .. math:: \eta = f(x) + g(y), \xi = 0 + + The first assumption of this heuristic holds good if + + .. math:: \frac{\partial}{\partial y}[(h\frac{\partial^{2}}{ + \partial x^{2}}(h^{-1}))^{-1}] + + is separable in `x` and `y`, + + 1. The separated factors containing `y` is `\frac{\partial g}{\partial y}`. + From this `g(y)` can be determined. + 2. The separated factors containing `x` is `f''(x)`. + 3. `h\frac{\partial^{2}}{\partial x^{2}}(h^{-1})` equals + `\frac{f''(x)}{f(x) + g(y)}`. From this `f(x)` can be determined. + + The second assumption holds good if `\frac{dy}{dx} = h(x, y)` is rewritten as + `\frac{dy}{dx} = \frac{1}{h(y, x)}` and the same properties of the first + assumption satisfies. After obtaining `f(x)` and `g(y)`, the coordinates + are again interchanged, to get `\eta` as `f(x) + g(y)`. + + For both assumptions, the constant factors are separated among `g(y)` + and `f''(x)`, such that `f''(x)` obtained from 3] is the same as that + obtained from 2]. If not possible, then this heuristic fails. + + + References + ========== + - E.S. Cheb-Terrab, A.D. Roche, Symmetries and First Order + ODE Patterns, pp. 7 - pp. 8 + + """ + + xieta = [] + h = match['h'] + func = match['func'] + hinv = match['hinv'] + x = func.args[0] + y = match['y'] + xi = Function('xi')(x, func) + eta = Function('eta')(x, func) + + for odefac in [h, hinv]: + factor = odefac*((1/odefac).diff(x, 2)) + sep = separatevars((1/factor).diff(y), dict=True, symbols=[x, y]) + if sep and sep['coeff'] and sep[x].has(x) and sep[y].has(y): + k = Dummy("k") + try: + gy = k*integrate(sep[y], y) + except NotImplementedError: + pass + else: + fdd = 1/(k*sep[x]*sep['coeff']) + fx = simplify(fdd/factor - gy) + check = simplify(fx.diff(x, 2) - fdd) + if fx: + if not check: + fx = fx.subs(k, 1) + gy = (gy/k) + else: + sol = solve(check, k) + if sol: + sol = sol[0] + fx = fx.subs(k, sol) + gy = (gy/k)*sol + else: + continue + if odefac == hinv: # Inverse ODE + fx = fx.subs(x, y) + gy = gy.subs(y, x) + etaval = factor_terms(fx + gy) + if etaval.is_Mul: + etaval = Mul(*[arg for arg in etaval.args if arg.has(x, y)]) + if odefac == hinv: # Inverse ODE + inf = {eta: etaval.subs(y, func), xi : S.Zero} + else: + inf = {xi: etaval.subs(y, func), eta : S.Zero} + if not comp: + return [inf] + else: + xieta.append(inf) + + if xieta: + return xieta + +def lie_heuristic_abaco2_similar(match, comp=False): + r""" + This heuristic uses the following two assumptions on `\xi` and `\eta` + + .. math:: \eta = g(x), \xi = f(x) + + .. math:: \eta = f(y), \xi = g(y) + + For the first assumption, + + 1. First `\frac{\frac{\partial h}{\partial y}}{\frac{\partial^{2} h}{ + \partial yy}}` is calculated. Let us say this value is A + + 2. If this is constant, then `h` is matched to the form `A(x) + B(x)e^{ + \frac{y}{C}}` then, `\frac{e^{\int \frac{A(x)}{C} \,dx}}{B(x)}` gives `f(x)` + and `A(x)*f(x)` gives `g(x)` + + 3. Otherwise `\frac{\frac{\partial A}{\partial X}}{\frac{\partial A}{ + \partial Y}} = \gamma` is calculated. If + + a] `\gamma` is a function of `x` alone + + b] `\frac{\gamma\frac{\partial h}{\partial y} - \gamma'(x) - \frac{ + \partial h}{\partial x}}{h + \gamma} = G` is a function of `x` alone. + then, `e^{\int G \,dx}` gives `f(x)` and `-\gamma*f(x)` gives `g(x)` + + The second assumption holds good if `\frac{dy}{dx} = h(x, y)` is rewritten as + `\frac{dy}{dx} = \frac{1}{h(y, x)}` and the same properties of the first assumption + satisfies. After obtaining `f(x)` and `g(x)`, the coordinates are again + interchanged, to get `\xi` as `f(x^*)` and `\eta` as `g(y^*)` + + References + ========== + - E.S. Cheb-Terrab, A.D. Roche, Symmetries and First Order + ODE Patterns, pp. 10 - pp. 12 + + """ + + h = match['h'] + hx = match['hx'] + hy = match['hy'] + func = match['func'] + hinv = match['hinv'] + x = func.args[0] + y = match['y'] + xi = Function('xi')(x, func) + eta = Function('eta')(x, func) + + factor = cancel(h.diff(y)/h.diff(y, 2)) + factorx = factor.diff(x) + factory = factor.diff(y) + if not factor.has(x) and not factor.has(y): + A = Wild('A', exclude=[y]) + B = Wild('B', exclude=[y]) + C = Wild('C', exclude=[x, y]) + match = h.match(A + B*exp(y/C)) + try: + tau = exp(-integrate(match[A]/match[C]), x)/match[B] + except NotImplementedError: + pass + else: + gx = match[A]*tau + return [{xi: tau, eta: gx}] + + else: + gamma = cancel(factorx/factory) + if not gamma.has(y): + tauint = cancel((gamma*hy - gamma.diff(x) - hx)/(h + gamma)) + if not tauint.has(y): + try: + tau = exp(integrate(tauint, x)) + except NotImplementedError: + pass + else: + gx = -tau*gamma + return [{xi: tau, eta: gx}] + + factor = cancel(hinv.diff(y)/hinv.diff(y, 2)) + factorx = factor.diff(x) + factory = factor.diff(y) + if not factor.has(x) and not factor.has(y): + A = Wild('A', exclude=[y]) + B = Wild('B', exclude=[y]) + C = Wild('C', exclude=[x, y]) + match = h.match(A + B*exp(y/C)) + try: + tau = exp(-integrate(match[A]/match[C]), x)/match[B] + except NotImplementedError: + pass + else: + gx = match[A]*tau + return [{eta: tau.subs(x, func), xi: gx.subs(x, func)}] + + else: + gamma = cancel(factorx/factory) + if not gamma.has(y): + tauint = cancel((gamma*hinv.diff(y) - gamma.diff(x) - hinv.diff(x))/( + hinv + gamma)) + if not tauint.has(y): + try: + tau = exp(integrate(tauint, x)) + except NotImplementedError: + pass + else: + gx = -tau*gamma + return [{eta: tau.subs(x, func), xi: gx.subs(x, func)}] + + +def lie_heuristic_abaco2_unique_unknown(match, comp=False): + r""" + This heuristic assumes the presence of unknown functions or known functions + with non-integer powers. + + 1. A list of all functions and non-integer powers containing x and y + 2. Loop over each element `f` in the list, find `\frac{\frac{\partial f}{\partial x}}{ + \frac{\partial f}{\partial x}} = R` + + If it is separable in `x` and `y`, let `X` be the factors containing `x`. Then + + a] Check if `\xi = X` and `\eta = -\frac{X}{R}` satisfy the PDE. If yes, then return + `\xi` and `\eta` + b] Check if `\xi = \frac{-R}{X}` and `\eta = -\frac{1}{X}` satisfy the PDE. + If yes, then return `\xi` and `\eta` + + If not, then check if + + a] :math:`\xi = -R,\eta = 1` + + b] :math:`\xi = 1, \eta = -\frac{1}{R}` + + are solutions. + + References + ========== + - E.S. Cheb-Terrab, A.D. Roche, Symmetries and First Order + ODE Patterns, pp. 10 - pp. 12 + + """ + + h = match['h'] + hx = match['hx'] + hy = match['hy'] + func = match['func'] + x = func.args[0] + y = match['y'] + xi = Function('xi')(x, func) + eta = Function('eta')(x, func) + + funclist = [] + for atom in h.atoms(Pow): + base, exp = atom.as_base_exp() + if base.has(x) and base.has(y): + if not exp.is_Integer: + funclist.append(atom) + + for function in h.atoms(AppliedUndef): + syms = function.free_symbols + if x in syms and y in syms: + funclist.append(function) + + for f in funclist: + frac = cancel(f.diff(y)/f.diff(x)) + sep = separatevars(frac, dict=True, symbols=[x, y]) + if sep and sep['coeff']: + xitry1 = sep[x] + etatry1 = -1/(sep[y]*sep['coeff']) + pde1 = etatry1.diff(y)*h - xitry1.diff(x)*h - xitry1*hx - etatry1*hy + if not simplify(pde1): + return [{xi: xitry1, eta: etatry1.subs(y, func)}] + xitry2 = 1/etatry1 + etatry2 = 1/xitry1 + pde2 = etatry2.diff(x) - (xitry2.diff(y))*h**2 - xitry2*hx - etatry2*hy + if not simplify(expand(pde2)): + return [{xi: xitry2.subs(y, func), eta: etatry2}] + + else: + etatry = -1/frac + pde = etatry.diff(x) + etatry.diff(y)*h - hx - etatry*hy + if not simplify(pde): + return [{xi: S.One, eta: etatry.subs(y, func)}] + xitry = -frac + pde = -xitry.diff(x)*h -xitry.diff(y)*h**2 - xitry*hx -hy + if not simplify(expand(pde)): + return [{xi: xitry.subs(y, func), eta: S.One}] + + +def lie_heuristic_abaco2_unique_general(match, comp=False): + r""" + This heuristic finds if infinitesimals of the form `\eta = f(x)`, `\xi = g(y)` + without making any assumptions on `h`. + + The complete sequence of steps is given in the paper mentioned below. + + References + ========== + - E.S. Cheb-Terrab, A.D. Roche, Symmetries and First Order + ODE Patterns, pp. 10 - pp. 12 + + """ + hx = match['hx'] + hy = match['hy'] + func = match['func'] + x = func.args[0] + y = match['y'] + xi = Function('xi')(x, func) + eta = Function('eta')(x, func) + + A = hx.diff(y) + B = hy.diff(y) + hy**2 + C = hx.diff(x) - hx**2 + + if not (A and B and C): + return + + Ax = A.diff(x) + Ay = A.diff(y) + Axy = Ax.diff(y) + Axx = Ax.diff(x) + Ayy = Ay.diff(y) + D = simplify(2*Axy + hx*Ay - Ax*hy + (hx*hy + 2*A)*A)*A - 3*Ax*Ay + if not D: + E1 = simplify(3*Ax**2 + ((hx**2 + 2*C)*A - 2*Axx)*A) + if E1: + E2 = simplify((2*Ayy + (2*B - hy**2)*A)*A - 3*Ay**2) + if not E2: + E3 = simplify( + E1*((28*Ax + 4*hx*A)*A**3 - E1*(hy*A + Ay)) - E1.diff(x)*8*A**4) + if not E3: + etaval = cancel((4*A**3*(Ax - hx*A) + E1*(hy*A - Ay))/(S(2)*A*E1)) + if x not in etaval: + try: + etaval = exp(integrate(etaval, y)) + except NotImplementedError: + pass + else: + xival = -4*A**3*etaval/E1 + if y not in xival: + return [{xi: xival, eta: etaval.subs(y, func)}] + + else: + E1 = simplify((2*Ayy + (2*B - hy**2)*A)*A - 3*Ay**2) + if E1: + E2 = simplify( + 4*A**3*D - D**2 + E1*((2*Axx - (hx**2 + 2*C)*A)*A - 3*Ax**2)) + if not E2: + E3 = simplify( + -(A*D)*E1.diff(y) + ((E1.diff(x) - hy*D)*A + 3*Ay*D + + (A*hx - 3*Ax)*E1)*E1) + if not E3: + etaval = cancel(((A*hx - Ax)*E1 - (Ay + A*hy)*D)/(S(2)*A*D)) + if x not in etaval: + try: + etaval = exp(integrate(etaval, y)) + except NotImplementedError: + pass + else: + xival = -E1*etaval/D + if y not in xival: + return [{xi: xival, eta: etaval.subs(y, func)}] + + +def lie_heuristic_linear(match, comp=False): + r""" + This heuristic assumes + + 1. `\xi = ax + by + c` and + 2. `\eta = fx + gy + h` + + After substituting the following assumptions in the determining PDE, it + reduces to + + .. math:: f + (g - a)h - bh^{2} - (ax + by + c)\frac{\partial h}{\partial x} + - (fx + gy + c)\frac{\partial h}{\partial y} + + Solving the reduced PDE obtained, using the method of characteristics, becomes + impractical. The method followed is grouping similar terms and solving the system + of linear equations obtained. The difference between the bivariate heuristic is that + `h` need not be a rational function in this case. + + References + ========== + - E.S. Cheb-Terrab, A.D. Roche, Symmetries and First Order + ODE Patterns, pp. 10 - pp. 12 + + """ + h = match['h'] + hx = match['hx'] + hy = match['hy'] + func = match['func'] + x = func.args[0] + y = match['y'] + xi = Function('xi')(x, func) + eta = Function('eta')(x, func) + + coeffdict = {} + symbols = numbered_symbols("c", cls=Dummy) + symlist = [next(symbols) for _ in islice(symbols, 6)] + C0, C1, C2, C3, C4, C5 = symlist + pde = C3 + (C4 - C0)*h - (C0*x + C1*y + C2)*hx - (C3*x + C4*y + C5)*hy - C1*h**2 + pde, denom = pde.as_numer_denom() + pde = powsimp(expand(pde)) + if pde.is_Add: + terms = pde.args + for term in terms: + if term.is_Mul: + rem = Mul(*[m for m in term.args if not m.has(x, y)]) + xypart = term/rem + if xypart not in coeffdict: + coeffdict[xypart] = rem + else: + coeffdict[xypart] += rem + else: + if term not in coeffdict: + coeffdict[term] = S.One + else: + coeffdict[term] += S.One + + sollist = coeffdict.values() + soldict = solve(sollist, symlist) + if soldict: + if isinstance(soldict, list): + soldict = soldict[0] + subval = soldict.values() + if any(t for t in subval): + onedict = dict(zip(symlist, [1]*6)) + xival = C0*x + C1*func + C2 + etaval = C3*x + C4*func + C5 + xival = xival.subs(soldict) + etaval = etaval.subs(soldict) + xival = xival.subs(onedict) + etaval = etaval.subs(onedict) + return [{xi: xival, eta: etaval}] + + +def _lie_group_remove(coords): + r""" + This function is strictly meant for internal use by the Lie group ODE solving + method. It replaces arbitrary functions returned by pdsolve as follows: + + 1] If coords is an arbitrary function, then its argument is returned. + 2] An arbitrary function in an Add object is replaced by zero. + 3] An arbitrary function in a Mul object is replaced by one. + 4] If there is no arbitrary function coords is returned unchanged. + + Examples + ======== + + >>> from sympy.solvers.ode.lie_group import _lie_group_remove + >>> from sympy import Function + >>> from sympy.abc import x, y + >>> F = Function("F") + >>> eq = x**2*y + >>> _lie_group_remove(eq) + x**2*y + >>> eq = F(x**2*y) + >>> _lie_group_remove(eq) + x**2*y + >>> eq = x*y**2 + F(x**3) + >>> _lie_group_remove(eq) + x*y**2 + >>> eq = (F(x**3) + y)*x**4 + >>> _lie_group_remove(eq) + x**4*y + + """ + if isinstance(coords, AppliedUndef): + return coords.args[0] + elif coords.is_Add: + subfunc = coords.atoms(AppliedUndef) + if subfunc: + for func in subfunc: + coords = coords.subs(func, 0) + return coords + elif coords.is_Pow: + base, expr = coords.as_base_exp() + base = _lie_group_remove(base) + expr = _lie_group_remove(expr) + return base**expr + elif coords.is_Mul: + mulargs = [] + coordargs = coords.args + for arg in coordargs: + if not isinstance(coords, AppliedUndef): + mulargs.append(_lie_group_remove(arg)) + return Mul(*mulargs) + return coords diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/nonhomogeneous.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/nonhomogeneous.py new file mode 100644 index 0000000000000000000000000000000000000000..ae39d55664e4850168ca7d68f65cf02171979957 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/nonhomogeneous.py @@ -0,0 +1,484 @@ +r""" +This File contains helper functions for nth_linear_constant_coeff_undetermined_coefficients, +nth_linear_euler_eq_nonhomogeneous_undetermined_coefficients, +nth_linear_constant_coeff_variation_of_parameters, +and nth_linear_euler_eq_nonhomogeneous_variation_of_parameters. + +All the functions in this file are used by more than one solvers so, instead of creating +instances in other classes for using them it is better to keep it here as separate helpers. + +""" +from collections import Counter +from sympy.core import Add, S +from sympy.core.function import diff, expand, _mexpand, expand_mul +from sympy.core.relational import Eq +from sympy.core.sorting import default_sort_key +from sympy.core.symbol import Dummy, Wild +from sympy.functions import exp, cos, cosh, im, log, re, sin, sinh, \ + atan2, conjugate +from sympy.integrals import Integral +from sympy.polys import (Poly, RootOf, rootof, roots) +from sympy.simplify import collect, simplify, separatevars, powsimp, trigsimp # type: ignore +from sympy.utilities import numbered_symbols +from sympy.solvers.solvers import solve +from sympy.matrices import wronskian +from .subscheck import sub_func_doit +from sympy.solvers.ode.ode import get_numbered_constants + + +def _test_term(coeff, func, order): + r""" + Linear Euler ODEs have the form K*x**order*diff(y(x), x, order) = F(x), + where K is independent of x and y(x), order>= 0. + So we need to check that for each term, coeff == K*x**order from + some K. We have a few cases, since coeff may have several + different types. + """ + x = func.args[0] + f = func.func + if order < 0: + raise ValueError("order should be greater than 0") + if coeff == 0: + return True + if order == 0: + if x in coeff.free_symbols: + return False + return True + if coeff.is_Mul: + if coeff.has(f(x)): + return False + return x**order in coeff.args + elif coeff.is_Pow: + return coeff.as_base_exp() == (x, order) + elif order == 1: + return x == coeff + return False + + +def _get_euler_characteristic_eq_sols(eq, func, match_obj): + r""" + Returns the solution of homogeneous part of the linear euler ODE and + the list of roots of characteristic equation. + + The parameter ``match_obj`` is a dict of order:coeff terms, where order is the order + of the derivative on each term, and coeff is the coefficient of that derivative. + + """ + x = func.args[0] + f = func.func + + # First, set up characteristic equation. + chareq, symbol = S.Zero, Dummy('x') + + for i in match_obj: + if i >= 0: + chareq += (match_obj[i]*diff(x**symbol, x, i)*x**-symbol).expand() + + chareq = Poly(chareq, symbol) + chareqroots = [rootof(chareq, k) for k in range(chareq.degree())] + collectterms = [] + + # A generator of constants + constants = list(get_numbered_constants(eq, num=chareq.degree()*2)) + constants.reverse() + + # Create a dict root: multiplicity or charroots + charroots = Counter(chareqroots) + gsol = S.Zero + ln = log + for root, multiplicity in charroots.items(): + for i in range(multiplicity): + if isinstance(root, RootOf): + gsol += (x**root) * constants.pop() + if multiplicity != 1: + raise ValueError("Value should be 1") + collectterms = [(0, root, 0)] + collectterms + elif root.is_real: + gsol += ln(x)**i*(x**root) * constants.pop() + collectterms = [(i, root, 0)] + collectterms + else: + reroot = re(root) + imroot = im(root) + gsol += ln(x)**i * (x**reroot) * ( + constants.pop() * sin(abs(imroot)*ln(x)) + + constants.pop() * cos(imroot*ln(x))) + collectterms = [(i, reroot, imroot)] + collectterms + + gsol = Eq(f(x), gsol) + + gensols = [] + # Keep track of when to use sin or cos for nonzero imroot + for i, reroot, imroot in collectterms: + if imroot == 0: + gensols.append(ln(x)**i*x**reroot) + else: + sin_form = ln(x)**i*x**reroot*sin(abs(imroot)*ln(x)) + if sin_form in gensols: + cos_form = ln(x)**i*x**reroot*cos(imroot*ln(x)) + gensols.append(cos_form) + else: + gensols.append(sin_form) + return gsol, gensols + + +def _solve_variation_of_parameters(eq, func, roots, homogen_sol, order, match_obj, simplify_flag=True): + r""" + Helper function for the method of variation of parameters and nonhomogeneous euler eq. + + See the + :py:meth:`~sympy.solvers.ode.single.NthLinearConstantCoeffVariationOfParameters` + docstring for more information on this method. + + The parameter are ``match_obj`` should be a dictionary that has the following + keys: + + ``list`` + A list of solutions to the homogeneous equation. + + ``sol`` + The general solution. + + """ + f = func.func + x = func.args[0] + r = match_obj + psol = 0 + wr = wronskian(roots, x) + + if simplify_flag: + wr = simplify(wr) # We need much better simplification for + # some ODEs. See issue 4662, for example. + # To reduce commonly occurring sin(x)**2 + cos(x)**2 to 1 + wr = trigsimp(wr, deep=True, recursive=True) + if not wr: + # The wronskian will be 0 iff the solutions are not linearly + # independent. + raise NotImplementedError("Cannot find " + str(order) + + " solutions to the homogeneous equation necessary to apply " + + "variation of parameters to " + str(eq) + " (Wronskian == 0)") + if len(roots) != order: + raise NotImplementedError("Cannot find " + str(order) + + " solutions to the homogeneous equation necessary to apply " + + "variation of parameters to " + + str(eq) + " (number of terms != order)") + negoneterm = S.NegativeOne**(order) + for i in roots: + psol += negoneterm*Integral(wronskian([sol for sol in roots if sol != i], x)*r[-1]/wr, x)*i/r[order] + negoneterm *= -1 + + if simplify_flag: + psol = simplify(psol) + psol = trigsimp(psol, deep=True) + return Eq(f(x), homogen_sol.rhs + psol) + + +def _get_const_characteristic_eq_sols(r, func, order): + r""" + Returns the roots of characteristic equation of constant coefficient + linear ODE and list of collectterms which is later on used by simplification + to use collect on solution. + + The parameter `r` is a dict of order:coeff terms, where order is the order of the + derivative on each term, and coeff is the coefficient of that derivative. + + """ + x = func.args[0] + # First, set up characteristic equation. + chareq, symbol = S.Zero, Dummy('x') + + for i in r.keys(): + if isinstance(i, str) or i < 0: + pass + else: + chareq += r[i]*symbol**i + + chareq = Poly(chareq, symbol) + # Can't just call roots because it doesn't return rootof for unsolveable + # polynomials. + chareqroots = roots(chareq, multiple=True) + if len(chareqroots) != order: + chareqroots = [rootof(chareq, k) for k in range(chareq.degree())] + + chareq_is_complex = not all(i.is_real for i in chareq.all_coeffs()) + + # Create a dict root: multiplicity or charroots + charroots = Counter(chareqroots) + # We need to keep track of terms so we can run collect() at the end. + # This is necessary for constantsimp to work properly. + collectterms = [] + gensols = [] + conjugate_roots = [] # used to prevent double-use of conjugate roots + # Loop over roots in theorder provided by roots/rootof... + for root in chareqroots: + # but don't repoeat multiple roots. + if root not in charroots: + continue + multiplicity = charroots.pop(root) + for i in range(multiplicity): + if chareq_is_complex: + gensols.append(x**i*exp(root*x)) + collectterms = [(i, root, 0)] + collectterms + continue + reroot = re(root) + imroot = im(root) + if imroot.has(atan2) and reroot.has(atan2): + # Remove this condition when re and im stop returning + # circular atan2 usages. + gensols.append(x**i*exp(root*x)) + collectterms = [(i, root, 0)] + collectterms + else: + if root in conjugate_roots: + collectterms = [(i, reroot, imroot)] + collectterms + continue + if imroot == 0: + gensols.append(x**i*exp(reroot*x)) + collectterms = [(i, reroot, 0)] + collectterms + continue + conjugate_roots.append(conjugate(root)) + gensols.append(x**i*exp(reroot*x) * sin(abs(imroot) * x)) + gensols.append(x**i*exp(reroot*x) * cos( imroot * x)) + + # This ordering is important + collectterms = [(i, reroot, imroot)] + collectterms + return gensols, collectterms + + +# Ideally these kind of simplification functions shouldn't be part of solvers. +# odesimp should be improved to handle these kind of specific simplifications. +def _get_simplified_sol(sol, func, collectterms): + r""" + Helper function which collects the solution on + collectterms. Ideally this should be handled by odesimp.It is used + only when the simplify is set to True in dsolve. + + The parameter ``collectterms`` is a list of tuple (i, reroot, imroot) where `i` is + the multiplicity of the root, reroot is real part and imroot being the imaginary part. + + """ + f = func.func + x = func.args[0] + collectterms.sort(key=default_sort_key) + collectterms.reverse() + assert len(sol) == 1 and sol[0].lhs == f(x) + sol = sol[0].rhs + sol = expand_mul(sol) + for i, reroot, imroot in collectterms: + sol = collect(sol, x**i*exp(reroot*x)*sin(abs(imroot)*x)) + sol = collect(sol, x**i*exp(reroot*x)*cos(imroot*x)) + for i, reroot, imroot in collectterms: + sol = collect(sol, x**i*exp(reroot*x)) + sol = powsimp(sol) + return Eq(f(x), sol) + + +def _undetermined_coefficients_match(expr, x, func=None, eq_homogeneous=S.Zero): + r""" + Returns a trial function match if undetermined coefficients can be applied + to ``expr``, and ``None`` otherwise. + + A trial expression can be found for an expression for use with the method + of undetermined coefficients if the expression is an + additive/multiplicative combination of constants, polynomials in `x` (the + independent variable of expr), `\sin(a x + b)`, `\cos(a x + b)`, and + `e^{a x}` terms (in other words, it has a finite number of linearly + independent derivatives). + + Note that you may still need to multiply each term returned here by + sufficient `x` to make it linearly independent with the solutions to the + homogeneous equation. + + This is intended for internal use by ``undetermined_coefficients`` hints. + + SymPy currently has no way to convert `\sin^n(x) \cos^m(y)` into a sum of + only `\sin(a x)` and `\cos(b x)` terms, so these are not implemented. So, + for example, you will need to manually convert `\sin^2(x)` into `[1 + + \cos(2 x)]/2` to properly apply the method of undetermined coefficients on + it. + + Examples + ======== + + >>> from sympy import log, exp + >>> from sympy.solvers.ode.nonhomogeneous import _undetermined_coefficients_match + >>> from sympy.abc import x + >>> _undetermined_coefficients_match(9*x*exp(x) + exp(-x), x) + {'test': True, 'trialset': {x*exp(x), exp(-x), exp(x)}} + >>> _undetermined_coefficients_match(log(x), x) + {'test': False} + + """ + a = Wild('a', exclude=[x]) + b = Wild('b', exclude=[x]) + expr = powsimp(expr, combine='exp') # exp(x)*exp(2*x + 1) => exp(3*x + 1) + retdict = {} + + def _test_term(expr, x) -> bool: + r""" + Test if ``expr`` fits the proper form for undetermined coefficients. + """ + if not expr.has(x): + return True + if expr.is_Add: + return all(_test_term(i, x) for i in expr.args) + if expr.is_Mul: + if expr.has(sin, cos): + foundtrig = False + # Make sure that there is only one trig function in the args. + # See the docstring. + for i in expr.args: + if i.has(sin, cos): + if foundtrig: + return False + else: + foundtrig = True + return all(_test_term(i, x) for i in expr.args) + if expr.is_Function: + return expr.func in (sin, cos, exp, sinh, cosh) and \ + bool(expr.args[0].match(a*x + b)) + if expr.is_Pow and expr.base.is_Symbol and expr.exp.is_Integer and \ + expr.exp >= 0: + return True + if expr.is_Pow and expr.base.is_number: + return bool(expr.exp.match(a*x + b)) + return expr.is_Symbol or bool(expr.is_number) + + def _get_trial_set(expr, x, exprs=set()): + r""" + Returns a set of trial terms for undetermined coefficients. + + The idea behind undetermined coefficients is that the terms expression + repeat themselves after a finite number of derivatives, except for the + coefficients (they are linearly dependent). So if we collect these, + we should have the terms of our trial function. + """ + def _remove_coefficient(expr, x): + r""" + Returns the expression without a coefficient. + + Similar to expr.as_independent(x)[1], except it only works + multiplicatively. + """ + term = S.One + if expr.is_Mul: + for i in expr.args: + if i.has(x): + term *= i + elif expr.has(x): + term = expr + return term + + expr = expand_mul(expr) + if expr.is_Add: + for term in expr.args: + if _remove_coefficient(term, x) in exprs: + pass + else: + exprs.add(_remove_coefficient(term, x)) + exprs = exprs.union(_get_trial_set(term, x, exprs)) + else: + term = _remove_coefficient(expr, x) + tmpset = exprs.union({term}) + oldset = set() + while tmpset != oldset: + # If you get stuck in this loop, then _test_term is probably + # broken + oldset = tmpset.copy() + expr = expr.diff(x) + term = _remove_coefficient(expr, x) + if term.is_Add: + tmpset = tmpset.union(_get_trial_set(term, x, tmpset)) + else: + tmpset.add(term) + exprs = tmpset + return exprs + + def is_homogeneous_solution(term): + r""" This function checks whether the given trialset contains any root + of homogeneous equation""" + return expand(sub_func_doit(eq_homogeneous, func, term)).is_zero + + retdict['test'] = _test_term(expr, x) + if retdict['test']: + # Try to generate a list of trial solutions that will have the + # undetermined coefficients. Note that if any of these are not linearly + # independent with any of the solutions to the homogeneous equation, + # then they will need to be multiplied by sufficient x to make them so. + # This function DOES NOT do that (it doesn't even look at the + # homogeneous equation). + temp_set = set() + for i in Add.make_args(expr): + act = _get_trial_set(i, x) + if eq_homogeneous is not S.Zero: + while any(is_homogeneous_solution(ts) for ts in act): + act = {x*ts for ts in act} + temp_set = temp_set.union(act) + + retdict['trialset'] = temp_set + return retdict + + +def _solve_undetermined_coefficients(eq, func, order, match, trialset): + r""" + Helper function for the method of undetermined coefficients. + + See the + :py:meth:`~sympy.solvers.ode.single.NthLinearConstantCoeffUndeterminedCoefficients` + docstring for more information on this method. + + The parameter ``trialset`` is the set of trial functions as returned by + ``_undetermined_coefficients_match()['trialset']``. + + The parameter ``match`` should be a dictionary that has the following + keys: + + ``list`` + A list of solutions to the homogeneous equation. + + ``sol`` + The general solution. + + """ + r = match + coeffs = numbered_symbols('a', cls=Dummy) + coefflist = [] + gensols = r['list'] + gsol = r['sol'] + f = func.func + x = func.args[0] + + if len(gensols) != order: + raise NotImplementedError("Cannot find " + str(order) + + " solutions to the homogeneous equation necessary to apply" + + " undetermined coefficients to " + str(eq) + + " (number of terms != order)") + + trialfunc = 0 + for i in trialset: + c = next(coeffs) + coefflist.append(c) + trialfunc += c*i + + eqs = sub_func_doit(eq, f(x), trialfunc) + + coeffsdict = dict(list(zip(trialset, [0]*(len(trialset) + 1)))) + + eqs = _mexpand(eqs) + + for i in Add.make_args(eqs): + s = separatevars(i, dict=True, symbols=[x]) + if coeffsdict.get(s[x]): + coeffsdict[s[x]] += s['coeff'] + else: + coeffsdict[s[x]] = s['coeff'] + + coeffvals = solve(list(coeffsdict.values()), coefflist) + + if not coeffvals: + raise NotImplementedError( + "Could not solve `%s` using the " + "method of undetermined coefficients " + "(unable to solve for coefficients)." % eq) + + psol = trialfunc.subs(coeffvals) + + return Eq(f(x), gsol.rhs + psol) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/ode.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/ode.py new file mode 100644 index 0000000000000000000000000000000000000000..6a28b2162b38a2dd8612c14e91fd588912f6756a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/ode.py @@ -0,0 +1,3572 @@ +r""" +This module contains :py:meth:`~sympy.solvers.ode.dsolve` and different helper +functions that it uses. + +:py:meth:`~sympy.solvers.ode.dsolve` solves ordinary differential equations. +See the docstring on the various functions for their uses. Note that partial +differential equations support is in ``pde.py``. Note that hint functions +have docstrings describing their various methods, but they are intended for +internal use. Use ``dsolve(ode, func, hint=hint)`` to solve an ODE using a +specific hint. See also the docstring on +:py:meth:`~sympy.solvers.ode.dsolve`. + +**Functions in this module** + + These are the user functions in this module: + + - :py:meth:`~sympy.solvers.ode.dsolve` - Solves ODEs. + - :py:meth:`~sympy.solvers.ode.classify_ode` - Classifies ODEs into + possible hints for :py:meth:`~sympy.solvers.ode.dsolve`. + - :py:meth:`~sympy.solvers.ode.checkodesol` - Checks if an equation is the + solution to an ODE. + - :py:meth:`~sympy.solvers.ode.homogeneous_order` - Returns the + homogeneous order of an expression. + - :py:meth:`~sympy.solvers.ode.infinitesimals` - Returns the infinitesimals + of the Lie group of point transformations of an ODE, such that it is + invariant. + - :py:meth:`~sympy.solvers.ode.checkinfsol` - Checks if the given infinitesimals + are the actual infinitesimals of a first order ODE. + + These are the non-solver helper functions that are for internal use. The + user should use the various options to + :py:meth:`~sympy.solvers.ode.dsolve` to obtain the functionality provided + by these functions: + + - :py:meth:`~sympy.solvers.ode.ode.odesimp` - Does all forms of ODE + simplification. + - :py:meth:`~sympy.solvers.ode.ode.ode_sol_simplicity` - A key function for + comparing solutions by simplicity. + - :py:meth:`~sympy.solvers.ode.constantsimp` - Simplifies arbitrary + constants. + - :py:meth:`~sympy.solvers.ode.ode.constant_renumber` - Renumber arbitrary + constants. + - :py:meth:`~sympy.solvers.ode.ode._handle_Integral` - Evaluate unevaluated + Integrals. + + See also the docstrings of these functions. + +**Currently implemented solver methods** + +The following methods are implemented for solving ordinary differential +equations. See the docstrings of the various hint functions for more +information on each (run ``help(ode)``): + + - 1st order separable differential equations. + - 1st order differential equations whose coefficients or `dx` and `dy` are + functions homogeneous of the same order. + - 1st order exact differential equations. + - 1st order linear differential equations. + - 1st order Bernoulli differential equations. + - Power series solutions for first order differential equations. + - Lie Group method of solving first order differential equations. + - 2nd order Liouville differential equations. + - Power series solutions for second order differential equations + at ordinary and regular singular points. + - `n`\th order differential equation that can be solved with algebraic + rearrangement and integration. + - `n`\th order linear homogeneous differential equation with constant + coefficients. + - `n`\th order linear inhomogeneous differential equation with constant + coefficients using the method of undetermined coefficients. + - `n`\th order linear inhomogeneous differential equation with constant + coefficients using the method of variation of parameters. + +**Philosophy behind this module** + +This module is designed to make it easy to add new ODE solving methods without +having to mess with the solving code for other methods. The idea is that +there is a :py:meth:`~sympy.solvers.ode.classify_ode` function, which takes in +an ODE and tells you what hints, if any, will solve the ODE. It does this +without attempting to solve the ODE, so it is fast. Each solving method is a +hint, and it has its own function, named ``ode_``. That function takes +in the ODE and any match expression gathered by +:py:meth:`~sympy.solvers.ode.classify_ode` and returns a solved result. If +this result has any integrals in it, the hint function will return an +unevaluated :py:class:`~sympy.integrals.integrals.Integral` class. +:py:meth:`~sympy.solvers.ode.dsolve`, which is the user wrapper function +around all of this, will then call :py:meth:`~sympy.solvers.ode.ode.odesimp` on +the result, which, among other things, will attempt to solve the equation for +the dependent variable (the function we are solving for), simplify the +arbitrary constants in the expression, and evaluate any integrals, if the hint +allows it. + +**How to add new solution methods** + +If you have an ODE that you want :py:meth:`~sympy.solvers.ode.dsolve` to be +able to solve, try to avoid adding special case code here. Instead, try +finding a general method that will solve your ODE, as well as others. This +way, the :py:mod:`~sympy.solvers.ode` module will become more robust, and +unhindered by special case hacks. WolphramAlpha and Maple's +DETools[odeadvisor] function are two resources you can use to classify a +specific ODE. It is also better for a method to work with an `n`\th order ODE +instead of only with specific orders, if possible. + +To add a new method, there are a few things that you need to do. First, you +need a hint name for your method. Try to name your hint so that it is +unambiguous with all other methods, including ones that may not be implemented +yet. If your method uses integrals, also include a ``hint_Integral`` hint. +If there is more than one way to solve ODEs with your method, include a hint +for each one, as well as a ``_best`` hint. Your ``ode__best()`` +function should choose the best using min with ``ode_sol_simplicity`` as the +key argument. See +:obj:`~sympy.solvers.ode.single.HomogeneousCoeffBest`, for example. +The function that uses your method will be called ``ode_()``, so the +hint must only use characters that are allowed in a Python function name +(alphanumeric characters and the underscore '``_``' character). Include a +function for every hint, except for ``_Integral`` hints +(:py:meth:`~sympy.solvers.ode.dsolve` takes care of those automatically). +Hint names should be all lowercase, unless a word is commonly capitalized +(such as Integral or Bernoulli). If you have a hint that you do not want to +run with ``all_Integral`` that does not have an ``_Integral`` counterpart (such +as a best hint that would defeat the purpose of ``all_Integral``), you will +need to remove it manually in the :py:meth:`~sympy.solvers.ode.dsolve` code. +See also the :py:meth:`~sympy.solvers.ode.classify_ode` docstring for +guidelines on writing a hint name. + +Determine *in general* how the solutions returned by your method compare with +other methods that can potentially solve the same ODEs. Then, put your hints +in the :py:data:`~sympy.solvers.ode.allhints` tuple in the order that they +should be called. The ordering of this tuple determines which hints are +default. Note that exceptions are ok, because it is easy for the user to +choose individual hints with :py:meth:`~sympy.solvers.ode.dsolve`. In +general, ``_Integral`` variants should go at the end of the list, and +``_best`` variants should go before the various hints they apply to. For +example, the ``undetermined_coefficients`` hint comes before the +``variation_of_parameters`` hint because, even though variation of parameters +is more general than undetermined coefficients, undetermined coefficients +generally returns cleaner results for the ODEs that it can solve than +variation of parameters does, and it does not require integration, so it is +much faster. + +Next, you need to have a match expression or a function that matches the type +of the ODE, which you should put in :py:meth:`~sympy.solvers.ode.classify_ode` +(if the match function is more than just a few lines. It should match the +ODE without solving for it as much as possible, so that +:py:meth:`~sympy.solvers.ode.classify_ode` remains fast and is not hindered by +bugs in solving code. Be sure to consider corner cases. For example, if your +solution method involves dividing by something, make sure you exclude the case +where that division will be 0. + +In most cases, the matching of the ODE will also give you the various parts +that you need to solve it. You should put that in a dictionary (``.match()`` +will do this for you), and add that as ``matching_hints['hint'] = matchdict`` +in the relevant part of :py:meth:`~sympy.solvers.ode.classify_ode`. +:py:meth:`~sympy.solvers.ode.classify_ode` will then send this to +:py:meth:`~sympy.solvers.ode.dsolve`, which will send it to your function as +the ``match`` argument. Your function should be named ``ode_(eq, func, +order, match)`. If you need to send more information, put it in the ``match`` +dictionary. For example, if you had to substitute in a dummy variable in +:py:meth:`~sympy.solvers.ode.classify_ode` to match the ODE, you will need to +pass it to your function using the `match` dict to access it. You can access +the independent variable using ``func.args[0]``, and the dependent variable +(the function you are trying to solve for) as ``func.func``. If, while trying +to solve the ODE, you find that you cannot, raise ``NotImplementedError``. +:py:meth:`~sympy.solvers.ode.dsolve` will catch this error with the ``all`` +meta-hint, rather than causing the whole routine to fail. + +Add a docstring to your function that describes the method employed. Like +with anything else in SymPy, you will need to add a doctest to the docstring, +in addition to real tests in ``test_ode.py``. Try to maintain consistency +with the other hint functions' docstrings. Add your method to the list at the +top of this docstring. Also, add your method to ``ode.rst`` in the +``docs/src`` directory, so that the Sphinx docs will pull its docstring into +the main SymPy documentation. Be sure to make the Sphinx documentation by +running ``make html`` from within the doc directory to verify that the +docstring formats correctly. + +If your solution method involves integrating, use :py:obj:`~.Integral` instead of +:py:meth:`~sympy.core.expr.Expr.integrate`. This allows the user to bypass +hard/slow integration by using the ``_Integral`` variant of your hint. In +most cases, calling :py:meth:`sympy.core.basic.Basic.doit` will integrate your +solution. If this is not the case, you will need to write special code in +:py:meth:`~sympy.solvers.ode.ode._handle_Integral`. Arbitrary constants should be +symbols named ``C1``, ``C2``, and so on. All solution methods should return +an equality instance. If you need an arbitrary number of arbitrary constants, +you can use ``constants = numbered_symbols(prefix='C', cls=Symbol, start=1)``. +If it is possible to solve for the dependent function in a general way, do so. +Otherwise, do as best as you can, but do not call solve in your +``ode_()`` function. :py:meth:`~sympy.solvers.ode.ode.odesimp` will attempt +to solve the solution for you, so you do not need to do that. Lastly, if your +ODE has a common simplification that can be applied to your solutions, you can +add a special case in :py:meth:`~sympy.solvers.ode.ode.odesimp` for it. For +example, solutions returned from the ``1st_homogeneous_coeff`` hints often +have many :obj:`~sympy.functions.elementary.exponential.log` terms, so +:py:meth:`~sympy.solvers.ode.ode.odesimp` calls +:py:meth:`~sympy.simplify.simplify.logcombine` on them (it also helps to write +the arbitrary constant as ``log(C1)`` instead of ``C1`` in this case). Also +consider common ways that you can rearrange your solution to have +:py:meth:`~sympy.solvers.ode.constantsimp` take better advantage of it. It is +better to put simplification in :py:meth:`~sympy.solvers.ode.ode.odesimp` than in +your method, because it can then be turned off with the simplify flag in +:py:meth:`~sympy.solvers.ode.dsolve`. If you have any extraneous +simplification in your function, be sure to only run it using ``if +match.get('simplify', True):``, especially if it can be slow or if it can +reduce the domain of the solution. + +Finally, as with every contribution to SymPy, your method will need to be +tested. Add a test for each method in ``test_ode.py``. Follow the +conventions there, i.e., test the solver using ``dsolve(eq, f(x), +hint=your_hint)``, and also test the solution using +:py:meth:`~sympy.solvers.ode.checkodesol` (you can put these in a separate +tests and skip/XFAIL if it runs too slow/does not work). Be sure to call your +hint specifically in :py:meth:`~sympy.solvers.ode.dsolve`, that way the test +will not be broken simply by the introduction of another matching hint. If your +method works for higher order (>1) ODEs, you will need to run ``sol = +constant_renumber(sol, 'C', 1, order)`` for each solution, where ``order`` is +the order of the ODE. This is because ``constant_renumber`` renumbers the +arbitrary constants by printing order, which is platform dependent. Try to +test every corner case of your solver, including a range of orders if it is a +`n`\th order solver, but if your solver is slow, such as if it involves hard +integration, try to keep the test run time down. + +Feel free to refactor existing hints to avoid duplicating code or creating +inconsistencies. If you can show that your method exactly duplicates an +existing method, including in the simplicity and speed of obtaining the +solutions, then you can remove the old, less general method. The existing +code is tested extensively in ``test_ode.py``, so if anything is broken, one +of those tests will surely fail. + +""" + +from sympy.core import Add, S, Mul, Pow, oo +from sympy.core.containers import Tuple +from sympy.core.expr import AtomicExpr, Expr +from sympy.core.function import (Function, Derivative, AppliedUndef, diff, + expand, expand_mul, Subs) +from sympy.core.multidimensional import vectorize +from sympy.core.numbers import nan, zoo, Number +from sympy.core.relational import Equality, Eq +from sympy.core.sorting import default_sort_key, ordered +from sympy.core.symbol import Symbol, Wild, Dummy, symbols +from sympy.core.sympify import sympify +from sympy.core.traversal import preorder_traversal + +from sympy.logic.boolalg import (BooleanAtom, BooleanTrue, + BooleanFalse) +from sympy.functions import exp, log, sqrt +from sympy.functions.combinatorial.factorials import factorial +from sympy.integrals.integrals import Integral +from sympy.polys import (Poly, terms_gcd, PolynomialError, lcm) +from sympy.polys.polytools import cancel +from sympy.series import Order +from sympy.series.series import series +from sympy.simplify import (collect, logcombine, powsimp, # type: ignore + separatevars, simplify, cse) +from sympy.simplify.radsimp import collect_const +from sympy.solvers import checksol, solve + +from sympy.utilities import numbered_symbols +from sympy.utilities.iterables import uniq, sift, iterable +from sympy.solvers.deutils import _preprocess, ode_order, _desolve + + +#: This is a list of hints in the order that they should be preferred by +#: :py:meth:`~sympy.solvers.ode.classify_ode`. In general, hints earlier in the +#: list should produce simpler solutions than those later in the list (for +#: ODEs that fit both). For now, the order of this list is based on empirical +#: observations by the developers of SymPy. +#: +#: The hint used by :py:meth:`~sympy.solvers.ode.dsolve` for a specific ODE +#: can be overridden (see the docstring). +#: +#: In general, ``_Integral`` hints are grouped at the end of the list, unless +#: there is a method that returns an unevaluable integral most of the time +#: (which go near the end of the list anyway). ``default``, ``all``, +#: ``best``, and ``all_Integral`` meta-hints should not be included in this +#: list, but ``_best`` and ``_Integral`` hints should be included. +allhints = ( + "factorable", + "nth_algebraic", + "separable", + "1st_exact", + "1st_linear", + "Bernoulli", + "1st_rational_riccati", + "Riccati_special_minus2", + "1st_homogeneous_coeff_best", + "1st_homogeneous_coeff_subs_indep_div_dep", + "1st_homogeneous_coeff_subs_dep_div_indep", + "almost_linear", + "linear_coefficients", + "separable_reduced", + "1st_power_series", + "lie_group", + "nth_linear_constant_coeff_homogeneous", + "nth_linear_euler_eq_homogeneous", + "nth_linear_constant_coeff_undetermined_coefficients", + "nth_linear_euler_eq_nonhomogeneous_undetermined_coefficients", + "nth_linear_constant_coeff_variation_of_parameters", + "nth_linear_euler_eq_nonhomogeneous_variation_of_parameters", + "Liouville", + "2nd_linear_airy", + "2nd_linear_bessel", + "2nd_hypergeometric", + "2nd_hypergeometric_Integral", + "nth_order_reducible", + "2nd_power_series_ordinary", + "2nd_power_series_regular", + "nth_algebraic_Integral", + "separable_Integral", + "1st_exact_Integral", + "1st_linear_Integral", + "Bernoulli_Integral", + "1st_homogeneous_coeff_subs_indep_div_dep_Integral", + "1st_homogeneous_coeff_subs_dep_div_indep_Integral", + "almost_linear_Integral", + "linear_coefficients_Integral", + "separable_reduced_Integral", + "nth_linear_constant_coeff_variation_of_parameters_Integral", + "nth_linear_euler_eq_nonhomogeneous_variation_of_parameters_Integral", + "Liouville_Integral", + "2nd_nonlinear_autonomous_conserved", + "2nd_nonlinear_autonomous_conserved_Integral", + ) + + + +def get_numbered_constants(eq, num=1, start=1, prefix='C'): + """ + Returns a list of constants that do not occur + in eq already. + """ + + ncs = iter_numbered_constants(eq, start, prefix) + Cs = [next(ncs) for i in range(num)] + return (Cs[0] if num == 1 else tuple(Cs)) + + +def iter_numbered_constants(eq, start=1, prefix='C'): + """ + Returns an iterator of constants that do not occur + in eq already. + """ + + if isinstance(eq, (Expr, Eq)): + eq = [eq] + elif not iterable(eq): + raise ValueError("Expected Expr or iterable but got %s" % eq) + + atom_set = set().union(*[i.free_symbols for i in eq]) + func_set = set().union(*[i.atoms(Function) for i in eq]) + if func_set: + atom_set |= {Symbol(str(f.func)) for f in func_set} + return numbered_symbols(start=start, prefix=prefix, exclude=atom_set) + + +def dsolve(eq, func=None, hint="default", simplify=True, + ics= None, xi=None, eta=None, x0=0, n=6, **kwargs): + r""" + Solves any (supported) kind of ordinary differential equation and + system of ordinary differential equations. + + For single ordinary differential equation + ========================================= + + It is classified under this when number of equation in ``eq`` is one. + **Usage** + + ``dsolve(eq, f(x), hint)`` -> Solve ordinary differential equation + ``eq`` for function ``f(x)``, using method ``hint``. + + **Details** + + ``eq`` can be any supported ordinary differential equation (see the + :py:mod:`~sympy.solvers.ode` docstring for supported methods). + This can either be an :py:class:`~sympy.core.relational.Equality`, + or an expression, which is assumed to be equal to ``0``. + + ``f(x)`` is a function of one variable whose derivatives in that + variable make up the ordinary differential equation ``eq``. In + many cases it is not necessary to provide this; it will be + autodetected (and an error raised if it could not be detected). + + ``hint`` is the solving method that you want dsolve to use. Use + ``classify_ode(eq, f(x))`` to get all of the possible hints for an + ODE. The default hint, ``default``, will use whatever hint is + returned first by :py:meth:`~sympy.solvers.ode.classify_ode`. See + Hints below for more options that you can use for hint. + + ``simplify`` enables simplification by + :py:meth:`~sympy.solvers.ode.ode.odesimp`. See its docstring for more + information. Turn this off, for example, to disable solving of + solutions for ``func`` or simplification of arbitrary constants. + It will still integrate with this hint. Note that the solution may + contain more arbitrary constants than the order of the ODE with + this option enabled. + + ``xi`` and ``eta`` are the infinitesimal functions of an ordinary + differential equation. They are the infinitesimals of the Lie group + of point transformations for which the differential equation is + invariant. The user can specify values for the infinitesimals. If + nothing is specified, ``xi`` and ``eta`` are calculated using + :py:meth:`~sympy.solvers.ode.infinitesimals` with the help of various + heuristics. + + ``ics`` is the set of initial/boundary conditions for the differential equation. + It should be given in the form of ``{f(x0): x1, f(x).diff(x).subs(x, x2): + x3}`` and so on. For power series solutions, if no initial + conditions are specified ``f(0)`` is assumed to be ``C0`` and the power + series solution is calculated about 0. + + ``x0`` is the point about which the power series solution of a differential + equation is to be evaluated. + + ``n`` gives the exponent of the dependent variable up to which the power series + solution of a differential equation is to be evaluated. + + **Hints** + + Aside from the various solving methods, there are also some meta-hints + that you can pass to :py:meth:`~sympy.solvers.ode.dsolve`: + + ``default``: + This uses whatever hint is returned first by + :py:meth:`~sympy.solvers.ode.classify_ode`. This is the + default argument to :py:meth:`~sympy.solvers.ode.dsolve`. + + ``all``: + To make :py:meth:`~sympy.solvers.ode.dsolve` apply all + relevant classification hints, use ``dsolve(ODE, func, + hint="all")``. This will return a dictionary of + ``hint:solution`` terms. If a hint causes dsolve to raise the + ``NotImplementedError``, value of that hint's key will be the + exception object raised. The dictionary will also include + some special keys: + + - ``order``: The order of the ODE. See also + :py:meth:`~sympy.solvers.deutils.ode_order` in + ``deutils.py``. + - ``best``: The simplest hint; what would be returned by + ``best`` below. + - ``best_hint``: The hint that would produce the solution + given by ``best``. If more than one hint produces the best + solution, the first one in the tuple returned by + :py:meth:`~sympy.solvers.ode.classify_ode` is chosen. + - ``default``: The solution that would be returned by default. + This is the one produced by the hint that appears first in + the tuple returned by + :py:meth:`~sympy.solvers.ode.classify_ode`. + + ``all_Integral``: + This is the same as ``all``, except if a hint also has a + corresponding ``_Integral`` hint, it only returns the + ``_Integral`` hint. This is useful if ``all`` causes + :py:meth:`~sympy.solvers.ode.dsolve` to hang because of a + difficult or impossible integral. This meta-hint will also be + much faster than ``all``, because + :py:meth:`~sympy.core.expr.Expr.integrate` is an expensive + routine. + + ``best``: + To have :py:meth:`~sympy.solvers.ode.dsolve` try all methods + and return the simplest one. This takes into account whether + the solution is solvable in the function, whether it contains + any Integral classes (i.e. unevaluatable integrals), and + which one is the shortest in size. + + See also the :py:meth:`~sympy.solvers.ode.classify_ode` docstring for + more info on hints, and the :py:mod:`~sympy.solvers.ode` docstring for + a list of all supported hints. + + **Tips** + + - You can declare the derivative of an unknown function this way: + + >>> from sympy import Function, Derivative + >>> from sympy.abc import x # x is the independent variable + >>> f = Function("f")(x) # f is a function of x + >>> # f_ will be the derivative of f with respect to x + >>> f_ = Derivative(f, x) + + - See ``test_ode.py`` for many tests, which serves also as a set of + examples for how to use :py:meth:`~sympy.solvers.ode.dsolve`. + - :py:meth:`~sympy.solvers.ode.dsolve` always returns an + :py:class:`~sympy.core.relational.Equality` class (except for the + case when the hint is ``all`` or ``all_Integral``). If possible, it + solves the solution explicitly for the function being solved for. + Otherwise, it returns an implicit solution. + - Arbitrary constants are symbols named ``C1``, ``C2``, and so on. + - Because all solutions should be mathematically equivalent, some + hints may return the exact same result for an ODE. Often, though, + two different hints will return the same solution formatted + differently. The two should be equivalent. Also note that sometimes + the values of the arbitrary constants in two different solutions may + not be the same, because one constant may have "absorbed" other + constants into it. + - Do ``help(ode.ode_)`` to get help more information on a + specific hint, where ```` is the name of a hint without + ``_Integral``. + + For system of ordinary differential equations + ============================================= + + **Usage** + ``dsolve(eq, func)`` -> Solve a system of ordinary differential + equations ``eq`` for ``func`` being list of functions including + `x(t)`, `y(t)`, `z(t)` where number of functions in the list depends + upon the number of equations provided in ``eq``. + + **Details** + + ``eq`` can be any supported system of ordinary differential equations + This can either be an :py:class:`~sympy.core.relational.Equality`, + or an expression, which is assumed to be equal to ``0``. + + ``func`` holds ``x(t)`` and ``y(t)`` being functions of one variable which + together with some of their derivatives make up the system of ordinary + differential equation ``eq``. It is not necessary to provide this; it + will be autodetected (and an error raised if it could not be detected). + + **Hints** + + The hints are formed by parameters returned by classify_sysode, combining + them give hints name used later for forming method name. + + Examples + ======== + + >>> from sympy import Function, dsolve, Eq, Derivative, sin, cos, symbols + >>> from sympy.abc import x + >>> f = Function('f') + >>> dsolve(Derivative(f(x), x, x) + 9*f(x), f(x)) + Eq(f(x), C1*sin(3*x) + C2*cos(3*x)) + + >>> eq = sin(x)*cos(f(x)) + cos(x)*sin(f(x))*f(x).diff(x) + >>> dsolve(eq, hint='1st_exact') + [Eq(f(x), -acos(C1/cos(x)) + 2*pi), Eq(f(x), acos(C1/cos(x)))] + >>> dsolve(eq, hint='almost_linear') + [Eq(f(x), -acos(C1/cos(x)) + 2*pi), Eq(f(x), acos(C1/cos(x)))] + >>> t = symbols('t') + >>> x, y = symbols('x, y', cls=Function) + >>> eq = (Eq(Derivative(x(t),t), 12*t*x(t) + 8*y(t)), Eq(Derivative(y(t),t), 21*x(t) + 7*t*y(t))) + >>> dsolve(eq) + [Eq(x(t), C1*x0(t) + C2*x0(t)*Integral(8*exp(Integral(7*t, t))*exp(Integral(12*t, t))/x0(t)**2, t)), + Eq(y(t), C1*y0(t) + C2*(y0(t)*Integral(8*exp(Integral(7*t, t))*exp(Integral(12*t, t))/x0(t)**2, t) + + exp(Integral(7*t, t))*exp(Integral(12*t, t))/x0(t)))] + >>> eq = (Eq(Derivative(x(t),t),x(t)*y(t)*sin(t)), Eq(Derivative(y(t),t),y(t)**2*sin(t))) + >>> dsolve(eq) + {Eq(x(t), -exp(C1)/(C2*exp(C1) - cos(t))), Eq(y(t), -1/(C1 - cos(t)))} + """ + if iterable(eq): + from sympy.solvers.ode.systems import dsolve_system + + # This may have to be changed in future + # when we have weakly and strongly + # connected components. This have to + # changed to show the systems that haven't + # been solved. + try: + sol = dsolve_system(eq, funcs=func, ics=ics, doit=True) + return sol[0] if len(sol) == 1 else sol + except NotImplementedError: + pass + + match = classify_sysode(eq, func) + + eq = match['eq'] + order = match['order'] + func = match['func'] + t = list(list(eq[0].atoms(Derivative))[0].atoms(Symbol))[0] + + # keep highest order term coefficient positive + for i in range(len(eq)): + for func_ in func: + if isinstance(func_, list): + pass + else: + if eq[i].coeff(diff(func[i],t,ode_order(eq[i], func[i]))).is_negative: + eq[i] = -eq[i] + match['eq'] = eq + if len(set(order.values()))!=1: + raise ValueError("It solves only those systems of equations whose orders are equal") + match['order'] = list(order.values())[0] + def recur_len(l): + return sum(recur_len(item) if isinstance(item,list) else 1 for item in l) + if recur_len(func) != len(eq): + raise ValueError("dsolve() and classify_sysode() work with " + "number of functions being equal to number of equations") + if match['type_of_equation'] is None: + raise NotImplementedError + else: + if match['is_linear'] == True: + solvefunc = globals()['sysode_linear_%(no_of_equation)seq_order%(order)s' % match] + else: + solvefunc = globals()['sysode_nonlinear_%(no_of_equation)seq_order%(order)s' % match] + sols = solvefunc(match) + if ics: + constants = Tuple(*sols).free_symbols - Tuple(*eq).free_symbols + solved_constants = solve_ics(sols, func, constants, ics) + return [sol.subs(solved_constants) for sol in sols] + return sols + else: + given_hint = hint # hint given by the user + + # See the docstring of _desolve for more details. + hints = _desolve(eq, func=func, + hint=hint, simplify=True, xi=xi, eta=eta, type='ode', ics=ics, + x0=x0, n=n, **kwargs) + eq = hints.pop('eq', eq) + all_ = hints.pop('all', False) + if all_: + retdict = {} + failed_hints = {} + gethints = classify_ode(eq, dict=True, hint='all') + orderedhints = gethints['ordered_hints'] + for hint in hints: + try: + rv = _helper_simplify(eq, hint, hints[hint], simplify) + except NotImplementedError as detail: + failed_hints[hint] = detail + else: + retdict[hint] = rv + func = hints[hint]['func'] + + retdict['best'] = min(list(retdict.values()), key=lambda x: + ode_sol_simplicity(x, func, trysolving=not simplify)) + if given_hint == 'best': + return retdict['best'] + for i in orderedhints: + if retdict['best'] == retdict.get(i, None): + retdict['best_hint'] = i + break + retdict['default'] = gethints['default'] + retdict['order'] = gethints['order'] + retdict.update(failed_hints) + return retdict + + else: + # The key 'hint' stores the hint needed to be solved for. + hint = hints['hint'] + return _helper_simplify(eq, hint, hints, simplify, ics=ics) + + +def _helper_simplify(eq, hint, match, simplify=True, ics=None, **kwargs): + r""" + Helper function of dsolve that calls the respective + :py:mod:`~sympy.solvers.ode` functions to solve for the ordinary + differential equations. This minimizes the computation in calling + :py:meth:`~sympy.solvers.deutils._desolve` multiple times. + """ + r = match + func = r['func'] + order = r['order'] + match = r[hint] + + if isinstance(match, SingleODESolver): + solvefunc = match + else: + solvefunc = globals()['ode_' + hint.removesuffix('_Integral')] + + free = eq.free_symbols + cons = lambda s: s.free_symbols.difference(free) + + if simplify: + # odesimp() will attempt to integrate, if necessary, apply constantsimp(), + # attempt to solve for func, and apply any other hint specific + # simplifications + if isinstance(solvefunc, SingleODESolver): + sols = solvefunc.get_general_solution() + else: + sols = solvefunc(eq, func, order, match) + if iterable(sols): + rv = [] + for s in sols: + simp = odesimp(eq, s, func, hint) + if iterable(simp): + rv.extend(simp) + else: + rv.append(simp) + else: + rv = odesimp(eq, sols, func, hint) + else: + # We still want to integrate (you can disable it separately with the hint) + if isinstance(solvefunc, SingleODESolver): + exprs = solvefunc.get_general_solution(simplify=False) + else: + match['simplify'] = False # Some hints can take advantage of this option + exprs = solvefunc(eq, func, order, match) + if isinstance(exprs, list): + rv = [_handle_Integral(expr, func, hint) for expr in exprs] + else: + rv = _handle_Integral(exprs, func, hint) + + if isinstance(rv, list): + assert all(isinstance(i, Eq) for i in rv), rv # if not => internal error + if simplify: + rv = _remove_redundant_solutions(eq, rv, order, func.args[0]) + if len(rv) == 1: + rv = rv[0] + if ics and 'power_series' not in hint: + if isinstance(rv, (Expr, Eq)): + solved_constants = solve_ics([rv], [r['func']], cons(rv), ics) + rv = rv.subs(solved_constants) + else: + rv1 = [] + for s in rv: + try: + solved_constants = solve_ics([s], [r['func']], cons(s), ics) + except ValueError: + continue + rv1.append(s.subs(solved_constants)) + if len(rv1) == 1: + return rv1[0] + rv = rv1 + return rv + + +def solve_ics(sols, funcs, constants, ics): + """ + Solve for the constants given initial conditions + + ``sols`` is a list of solutions. + + ``funcs`` is a list of functions. + + ``constants`` is a list of constants. + + ``ics`` is the set of initial/boundary conditions for the differential + equation. It should be given in the form of ``{f(x0): x1, + f(x).diff(x).subs(x, x2): x3}`` and so on. + + Returns a dictionary mapping constants to values. + ``solution.subs(constants)`` will replace the constants in ``solution``. + + Example + ======= + >>> # From dsolve(f(x).diff(x) - f(x), f(x)) + >>> from sympy import symbols, Eq, exp, Function + >>> from sympy.solvers.ode.ode import solve_ics + >>> f = Function('f') + >>> x, C1 = symbols('x C1') + >>> sols = [Eq(f(x), C1*exp(x))] + >>> funcs = [f(x)] + >>> constants = [C1] + >>> ics = {f(0): 2} + >>> solved_constants = solve_ics(sols, funcs, constants, ics) + >>> solved_constants + {C1: 2} + >>> sols[0].subs(solved_constants) + Eq(f(x), 2*exp(x)) + + """ + # Assume ics are of the form f(x0): value or Subs(diff(f(x), x, n), (x, + # x0)): value (currently checked by classify_ode). To solve, replace x + # with x0, f(x0) with value, then solve for constants. For f^(n)(x0), + # differentiate the solution n times, so that f^(n)(x) appears. + x = funcs[0].args[0] + diff_sols = [] + subs_sols = [] + diff_variables = set() + for funcarg, value in ics.items(): + if isinstance(funcarg, AppliedUndef): + x0 = funcarg.args[0] + matching_func = [f for f in funcs if f.func == funcarg.func][0] + S = sols + elif isinstance(funcarg, (Subs, Derivative)): + if isinstance(funcarg, Subs): + # Make sure it stays a subs. Otherwise subs below will produce + # a different looking term. + funcarg = funcarg.doit() + if isinstance(funcarg, Subs): + deriv = funcarg.expr + x0 = funcarg.point[0] + variables = funcarg.expr.variables + matching_func = deriv + elif isinstance(funcarg, Derivative): + deriv = funcarg + x0 = funcarg.variables[0] + variables = (x,)*len(funcarg.variables) + matching_func = deriv.subs(x0, x) + for sol in sols: + if sol.has(deriv.expr.func): + diff_sols.append(Eq(sol.lhs.diff(*variables), sol.rhs.diff(*variables))) + diff_variables.add(variables) + S = diff_sols + else: + raise NotImplementedError("Unrecognized initial condition") + + for sol in S: + if sol.has(matching_func): + sol2 = sol + sol2 = sol2.subs(x, x0) + sol2 = sol2.subs(funcarg, value) + # This check is necessary because of issue #15724 + if not isinstance(sol2, BooleanAtom) or not subs_sols: + subs_sols = [s for s in subs_sols if not isinstance(s, BooleanAtom)] + subs_sols.append(sol2) + + # TODO: Use solveset here + try: + solved_constants = solve(subs_sols, constants, dict=True) + except NotImplementedError: + solved_constants = [] + + # XXX: We can't differentiate between the solution not existing because of + # invalid initial conditions, and not existing because solve is not smart + # enough. If we could use solveset, this might be improvable, but for now, + # we use NotImplementedError in this case. + if not solved_constants: + raise ValueError("Couldn't solve for initial conditions") + + if solved_constants == True: + raise ValueError("Initial conditions did not produce any solutions for constants. Perhaps they are degenerate.") + + if len(solved_constants) > 1: + raise NotImplementedError("Initial conditions produced too many solutions for constants") + + return solved_constants[0] + +def classify_ode(eq, func=None, dict=False, ics=None, *, prep=True, xi=None, eta=None, n=None, **kwargs): + r""" + Returns a tuple of possible :py:meth:`~sympy.solvers.ode.dsolve` + classifications for an ODE. + + The tuple is ordered so that first item is the classification that + :py:meth:`~sympy.solvers.ode.dsolve` uses to solve the ODE by default. In + general, classifications at the near the beginning of the list will + produce better solutions faster than those near the end, thought there are + always exceptions. To make :py:meth:`~sympy.solvers.ode.dsolve` use a + different classification, use ``dsolve(ODE, func, + hint=)``. See also the + :py:meth:`~sympy.solvers.ode.dsolve` docstring for different meta-hints + you can use. + + If ``dict`` is true, :py:meth:`~sympy.solvers.ode.classify_ode` will + return a dictionary of ``hint:match`` expression terms. This is intended + for internal use by :py:meth:`~sympy.solvers.ode.dsolve`. Note that + because dictionaries are ordered arbitrarily, this will most likely not be + in the same order as the tuple. + + You can get help on different hints by executing + ``help(ode.ode_hintname)``, where ``hintname`` is the name of the hint + without ``_Integral``. + + See :py:data:`~sympy.solvers.ode.allhints` or the + :py:mod:`~sympy.solvers.ode` docstring for a list of all supported hints + that can be returned from :py:meth:`~sympy.solvers.ode.classify_ode`. + + Notes + ===== + + These are remarks on hint names. + + ``_Integral`` + + If a classification has ``_Integral`` at the end, it will return the + expression with an unevaluated :py:class:`~.Integral` + class in it. Note that a hint may do this anyway if + :py:meth:`~sympy.core.expr.Expr.integrate` cannot do the integral, + though just using an ``_Integral`` will do so much faster. Indeed, an + ``_Integral`` hint will always be faster than its corresponding hint + without ``_Integral`` because + :py:meth:`~sympy.core.expr.Expr.integrate` is an expensive routine. + If :py:meth:`~sympy.solvers.ode.dsolve` hangs, it is probably because + :py:meth:`~sympy.core.expr.Expr.integrate` is hanging on a tough or + impossible integral. Try using an ``_Integral`` hint or + ``all_Integral`` to get it return something. + + Note that some hints do not have ``_Integral`` counterparts. This is + because :py:func:`~sympy.integrals.integrals.integrate` is not used in + solving the ODE for those method. For example, `n`\th order linear + homogeneous ODEs with constant coefficients do not require integration + to solve, so there is no + ``nth_linear_homogeneous_constant_coeff_Integrate`` hint. You can + easily evaluate any unevaluated + :py:class:`~sympy.integrals.integrals.Integral`\s in an expression by + doing ``expr.doit()``. + + Ordinals + + Some hints contain an ordinal such as ``1st_linear``. This is to help + differentiate them from other hints, as well as from other methods + that may not be implemented yet. If a hint has ``nth`` in it, such as + the ``nth_linear`` hints, this means that the method used to applies + to ODEs of any order. + + ``indep`` and ``dep`` + + Some hints contain the words ``indep`` or ``dep``. These reference + the independent variable and the dependent function, respectively. For + example, if an ODE is in terms of `f(x)`, then ``indep`` will refer to + `x` and ``dep`` will refer to `f`. + + ``subs`` + + If a hints has the word ``subs`` in it, it means that the ODE is solved + by substituting the expression given after the word ``subs`` for a + single dummy variable. This is usually in terms of ``indep`` and + ``dep`` as above. The substituted expression will be written only in + characters allowed for names of Python objects, meaning operators will + be spelled out. For example, ``indep``/``dep`` will be written as + ``indep_div_dep``. + + ``coeff`` + + The word ``coeff`` in a hint refers to the coefficients of something + in the ODE, usually of the derivative terms. See the docstring for + the individual methods for more info (``help(ode)``). This is + contrast to ``coefficients``, as in ``undetermined_coefficients``, + which refers to the common name of a method. + + ``_best`` + + Methods that have more than one fundamental way to solve will have a + hint for each sub-method and a ``_best`` meta-classification. This + will evaluate all hints and return the best, using the same + considerations as the normal ``best`` meta-hint. + + + Examples + ======== + + >>> from sympy import Function, classify_ode, Eq + >>> from sympy.abc import x + >>> f = Function('f') + >>> classify_ode(Eq(f(x).diff(x), 0), f(x)) + ('nth_algebraic', + 'separable', + '1st_exact', + '1st_linear', + 'Bernoulli', + '1st_homogeneous_coeff_best', + '1st_homogeneous_coeff_subs_indep_div_dep', + '1st_homogeneous_coeff_subs_dep_div_indep', + '1st_power_series', 'lie_group', 'nth_linear_constant_coeff_homogeneous', + 'nth_linear_euler_eq_homogeneous', + 'nth_algebraic_Integral', 'separable_Integral', '1st_exact_Integral', + '1st_linear_Integral', 'Bernoulli_Integral', + '1st_homogeneous_coeff_subs_indep_div_dep_Integral', + '1st_homogeneous_coeff_subs_dep_div_indep_Integral') + >>> classify_ode(f(x).diff(x, 2) + 3*f(x).diff(x) + 2*f(x) - 4) + ('factorable', 'nth_linear_constant_coeff_undetermined_coefficients', + 'nth_linear_constant_coeff_variation_of_parameters', + 'nth_linear_constant_coeff_variation_of_parameters_Integral') + + """ + ics = sympify(ics) + + if func and len(func.args) != 1: + raise ValueError("dsolve() and classify_ode() only " + "work with functions of one variable, not %s" % func) + + if isinstance(eq, Equality): + eq = eq.lhs - eq.rhs + + # Some methods want the unprocessed equation + eq_orig = eq + + if prep or func is None: + eq, func_ = _preprocess(eq, func) + if func is None: + func = func_ + x = func.args[0] + f = func.func + y = Dummy('y') + terms = 5 if n is None else n + + order = ode_order(eq, f(x)) + # hint:matchdict or hint:(tuple of matchdicts) + # Also will contain "default": and "order":order items. + matching_hints = {"order": order} + + df = f(x).diff(x) + a = Wild('a', exclude=[f(x)]) + d = Wild('d', exclude=[df, f(x).diff(x, 2)]) + e = Wild('e', exclude=[df]) + n = Wild('n', exclude=[x, f(x), df]) + c1 = Wild('c1', exclude=[x]) + a3 = Wild('a3', exclude=[f(x), df, f(x).diff(x, 2)]) + b3 = Wild('b3', exclude=[f(x), df, f(x).diff(x, 2)]) + c3 = Wild('c3', exclude=[f(x), df, f(x).diff(x, 2)]) + boundary = {} # Used to extract initial conditions + C1 = Symbol("C1") + + # Preprocessing to get the initial conditions out + if ics is not None: + for funcarg in ics: + # Separating derivatives + if isinstance(funcarg, (Subs, Derivative)): + # f(x).diff(x).subs(x, 0) is a Subs, but f(x).diff(x).subs(x, + # y) is a Derivative + if isinstance(funcarg, Subs): + deriv = funcarg.expr + old = funcarg.variables[0] + new = funcarg.point[0] + elif isinstance(funcarg, Derivative): + deriv = funcarg + # No information on this. Just assume it was x + old = x + new = funcarg.variables[0] + + if (isinstance(deriv, Derivative) and isinstance(deriv.args[0], + AppliedUndef) and deriv.args[0].func == f and + len(deriv.args[0].args) == 1 and old == x and not + new.has(x) and all(i == deriv.variables[0] for i in + deriv.variables) and x not in ics[funcarg].free_symbols): + + dorder = ode_order(deriv, x) + temp = 'f' + str(dorder) + boundary.update({temp: new, temp + 'val': ics[funcarg]}) + else: + raise ValueError("Invalid boundary conditions for Derivatives") + + + # Separating functions + elif isinstance(funcarg, AppliedUndef): + if (funcarg.func == f and len(funcarg.args) == 1 and + not funcarg.args[0].has(x) and x not in ics[funcarg].free_symbols): + boundary.update({'f0': funcarg.args[0], 'f0val': ics[funcarg]}) + else: + raise ValueError("Invalid boundary conditions for Function") + + else: + raise ValueError("Enter boundary conditions of the form ics={f(point): value, f(x).diff(x, order).subs(x, point): value}") + + ode = SingleODEProblem(eq_orig, func, x, prep=prep, xi=xi, eta=eta) + user_hint = kwargs.get('hint', 'default') + # Used when dsolve is called without an explicit hint. + # We exit early to return the first valid match + early_exit = (user_hint=='default') + user_hint = user_hint.removesuffix('_Integral') + user_map = solver_map + # An explicit hint has been given to dsolve + # Skip matching code for other hints + if user_hint not in ['default', 'all', 'all_Integral', 'best'] and user_hint in solver_map: + user_map = {user_hint: solver_map[user_hint]} + + for hint in user_map: + solver = user_map[hint](ode) + if solver.matches(): + matching_hints[hint] = solver + if user_map[hint].has_integral: + matching_hints[hint + "_Integral"] = solver + if dict and early_exit: + matching_hints["default"] = hint + return matching_hints + + eq = expand(eq) + # Precondition to try remove f(x) from highest order derivative + reduced_eq = None + if eq.is_Add: + deriv_coef = eq.coeff(f(x).diff(x, order)) + if deriv_coef not in (1, 0): + r = deriv_coef.match(a*f(x)**c1) + if r and r[c1]: + den = f(x)**r[c1] + reduced_eq = Add(*[arg/den for arg in eq.args]) + if not reduced_eq: + reduced_eq = eq + + if order == 1: + + # NON-REDUCED FORM OF EQUATION matches + r = collect(eq, df, exact=True).match(d + e * df) + if r: + r['d'] = d + r['e'] = e + r['y'] = y + r[d] = r[d].subs(f(x), y) + r[e] = r[e].subs(f(x), y) + + # FIRST ORDER POWER SERIES WHICH NEEDS INITIAL CONDITIONS + # TODO: Hint first order series should match only if d/e is analytic. + # For now, only d/e and (d/e).diff(arg) is checked for existence at + # at a given point. + # This is currently done internally in ode_1st_power_series. + point = boundary.get('f0', 0) + value = boundary.get('f0val', C1) + check = cancel(r[d]/r[e]) + check1 = check.subs({x: point, y: value}) + if not check1.has(oo) and not check1.has(zoo) and \ + not check1.has(nan) and not check1.has(-oo): + check2 = (check1.diff(x)).subs({x: point, y: value}) + if not check2.has(oo) and not check2.has(zoo) and \ + not check2.has(nan) and not check2.has(-oo): + rseries = r.copy() + rseries.update({'terms': terms, 'f0': point, 'f0val': value}) + matching_hints["1st_power_series"] = rseries + + elif order == 2: + # Homogeneous second order differential equation of the form + # a3*f(x).diff(x, 2) + b3*f(x).diff(x) + c3 + # It has a definite power series solution at point x0 if, b3/a3 and c3/a3 + # are analytic at x0. + deq = a3*(f(x).diff(x, 2)) + b3*df + c3*f(x) + r = collect(reduced_eq, + [f(x).diff(x, 2), f(x).diff(x), f(x)]).match(deq) + ordinary = False + if r: + if not all(r[key].is_polynomial() for key in r): + n, d = reduced_eq.as_numer_denom() + reduced_eq = expand(n) + r = collect(reduced_eq, + [f(x).diff(x, 2), f(x).diff(x), f(x)]).match(deq) + if r and r[a3] != 0: + p = cancel(r[b3]/r[a3]) # Used below + q = cancel(r[c3]/r[a3]) # Used below + point = kwargs.get('x0', 0) + check = p.subs(x, point) + if not check.has(oo, nan, zoo, -oo): + check = q.subs(x, point) + if not check.has(oo, nan, zoo, -oo): + ordinary = True + r.update({'a3': a3, 'b3': b3, 'c3': c3, 'x0': point, 'terms': terms}) + matching_hints["2nd_power_series_ordinary"] = r + + # Checking if the differential equation has a regular singular point + # at x0. It has a regular singular point at x0, if (b3/a3)*(x - x0) + # and (c3/a3)*((x - x0)**2) are analytic at x0. + if not ordinary: + p = cancel((x - point)*p) + check = p.subs(x, point) + if not check.has(oo, nan, zoo, -oo): + q = cancel(((x - point)**2)*q) + check = q.subs(x, point) + if not check.has(oo, nan, zoo, -oo): + coeff_dict = {'p': p, 'q': q, 'x0': point, 'terms': terms} + matching_hints["2nd_power_series_regular"] = coeff_dict + + + # Order keys based on allhints. + retlist = [i for i in allhints if i in matching_hints] + if dict: + # Dictionaries are ordered arbitrarily, so make note of which + # hint would come first for dsolve(). Use an ordered dict in Py 3. + matching_hints["default"] = retlist[0] if retlist else None + matching_hints["ordered_hints"] = tuple(retlist) + return matching_hints + else: + return tuple(retlist) + + +def classify_sysode(eq, funcs=None, **kwargs): + r""" + Returns a dictionary of parameter names and values that define the system + of ordinary differential equations in ``eq``. + The parameters are further used in + :py:meth:`~sympy.solvers.ode.dsolve` for solving that system. + + Some parameter names and values are: + + 'is_linear' (boolean), which tells whether the given system is linear. + Note that "linear" here refers to the operator: terms such as ``x*diff(x,t)`` are + nonlinear, whereas terms like ``sin(t)*diff(x,t)`` are still linear operators. + + 'func' (list) contains the :py:class:`~sympy.core.function.Function`s that + appear with a derivative in the ODE, i.e. those that we are trying to solve + the ODE for. + + 'order' (dict) with the maximum derivative for each element of the 'func' + parameter. + + 'func_coeff' (dict or Matrix) with the coefficient for each triple ``(equation number, + function, order)```. The coefficients are those subexpressions that do not + appear in 'func', and hence can be considered constant for purposes of ODE + solving. The value of this parameter can also be a Matrix if the system of ODEs are + linear first order of the form X' = AX where X is the vector of dependent variables. + Here, this function returns the coefficient matrix A. + + 'eq' (list) with the equations from ``eq``, sympified and transformed into + expressions (we are solving for these expressions to be zero). + + 'no_of_equations' (int) is the number of equations (same as ``len(eq)``). + + 'type_of_equation' (string) is an internal classification of the type of + ODE. + + 'is_constant' (boolean), which tells if the system of ODEs is constant coefficient + or not. This key is temporary addition for now and is in the match dict only when + the system of ODEs is linear first order constant coefficient homogeneous. So, this + key's value is True for now if it is available else it does not exist. + + 'is_homogeneous' (boolean), which tells if the system of ODEs is homogeneous. Like the + key 'is_constant', this key is a temporary addition and it is True since this key value + is available only when the system is linear first order constant coefficient homogeneous. + + References + ========== + -https://eqworld.ipmnet.ru/en/solutions/sysode/sode-toc1.htm + -A. D. Polyanin and A. V. Manzhirov, Handbook of Mathematics for Engineers and Scientists + + Examples + ======== + + >>> from sympy import Function, Eq, symbols, diff + >>> from sympy.solvers.ode.ode import classify_sysode + >>> from sympy.abc import t + >>> f, x, y = symbols('f, x, y', cls=Function) + >>> k, l, m, n = symbols('k, l, m, n', Integer=True) + >>> x1 = diff(x(t), t) ; y1 = diff(y(t), t) + >>> x2 = diff(x(t), t, t) ; y2 = diff(y(t), t, t) + >>> eq = (Eq(x1, 12*x(t) - 6*y(t)), Eq(y1, 11*x(t) + 3*y(t))) + >>> classify_sysode(eq) + {'eq': [-12*x(t) + 6*y(t) + Derivative(x(t), t), -11*x(t) - 3*y(t) + Derivative(y(t), t)], 'func': [x(t), y(t)], + 'func_coeff': {(0, x(t), 0): -12, (0, x(t), 1): 1, (0, y(t), 0): 6, (0, y(t), 1): 0, (1, x(t), 0): -11, (1, x(t), 1): 0, (1, y(t), 0): -3, (1, y(t), 1): 1}, 'is_linear': True, 'no_of_equation': 2, 'order': {x(t): 1, y(t): 1}, 'type_of_equation': None} + >>> eq = (Eq(diff(x(t),t), 5*t*x(t) + t**2*y(t) + 2), Eq(diff(y(t),t), -t**2*x(t) + 5*t*y(t))) + >>> classify_sysode(eq) + {'eq': [-t**2*y(t) - 5*t*x(t) + Derivative(x(t), t) - 2, t**2*x(t) - 5*t*y(t) + Derivative(y(t), t)], + 'func': [x(t), y(t)], 'func_coeff': {(0, x(t), 0): -5*t, (0, x(t), 1): 1, (0, y(t), 0): -t**2, (0, y(t), 1): 0, + (1, x(t), 0): t**2, (1, x(t), 1): 0, (1, y(t), 0): -5*t, (1, y(t), 1): 1}, 'is_linear': True, 'no_of_equation': 2, + 'order': {x(t): 1, y(t): 1}, 'type_of_equation': None} + + """ + + # Sympify equations and convert iterables of equations into + # a list of equations + def _sympify(eq): + return list(map(sympify, eq if iterable(eq) else [eq])) + + eq, funcs = (_sympify(w) for w in [eq, funcs]) + for i, fi in enumerate(eq): + if isinstance(fi, Equality): + eq[i] = fi.lhs - fi.rhs + + t = list(list(eq[0].atoms(Derivative))[0].atoms(Symbol))[0] + matching_hints = {"no_of_equation":i+1} + matching_hints['eq'] = eq + if i==0: + raise ValueError("classify_sysode() works for systems of ODEs. " + "For scalar ODEs, classify_ode should be used") + + # find all the functions if not given + order = {} + if funcs==[None]: + funcs = _extract_funcs(eq) + + funcs = list(set(funcs)) + if len(funcs) != len(eq): + raise ValueError("Number of functions given is not equal to the number of equations %s" % funcs) + + # This logic of list of lists in funcs to + # be replaced later. + func_dict = {} + for func in funcs: + if not order.get(func, False): + max_order = 0 + for i, eqs_ in enumerate(eq): + order_ = ode_order(eqs_,func) + if max_order < order_: + max_order = order_ + eq_no = i + if eq_no in func_dict: + func_dict[eq_no] = [func_dict[eq_no], func] + else: + func_dict[eq_no] = func + order[func] = max_order + + funcs = [func_dict[i] for i in range(len(func_dict))] + matching_hints['func'] = funcs + for func in funcs: + if isinstance(func, list): + for func_elem in func: + if len(func_elem.args) != 1: + raise ValueError("dsolve() and classify_sysode() work with " + "functions of one variable only, not %s" % func) + else: + if func and len(func.args) != 1: + raise ValueError("dsolve() and classify_sysode() work with " + "functions of one variable only, not %s" % func) + + # find the order of all equation in system of odes + matching_hints["order"] = order + + # find coefficients of terms f(t), diff(f(t),t) and higher derivatives + # and similarly for other functions g(t), diff(g(t),t) in all equations. + # Here j denotes the equation number, funcs[l] denotes the function about + # which we are talking about and k denotes the order of function funcs[l] + # whose coefficient we are calculating. + def linearity_check(eqs, j, func, is_linear_): + for k in range(order[func] + 1): + func_coef[j, func, k] = collect(eqs.expand(), [diff(func, t, k)]).coeff(diff(func, t, k)) + if is_linear_ == True: + if func_coef[j, func, k] == 0: + if k == 0: + coef = eqs.as_independent(func, as_Add=True)[1] + for xr in range(1, ode_order(eqs,func) + 1): + coef -= eqs.as_independent(diff(func, t, xr), as_Add=True)[1] + if coef != 0: + is_linear_ = False + else: + if eqs.as_independent(diff(func, t, k), as_Add=True)[1]: + is_linear_ = False + else: + for func_ in funcs: + if isinstance(func_, list): + for elem_func_ in func_: + dep = func_coef[j, func, k].as_independent(elem_func_, as_Add=True)[1] + if dep != 0: + is_linear_ = False + else: + dep = func_coef[j, func, k].as_independent(func_, as_Add=True)[1] + if dep != 0: + is_linear_ = False + return is_linear_ + + func_coef = {} + is_linear = True + for j, eqs in enumerate(eq): + for func in funcs: + if isinstance(func, list): + for func_elem in func: + is_linear = linearity_check(eqs, j, func_elem, is_linear) + else: + is_linear = linearity_check(eqs, j, func, is_linear) + matching_hints['func_coeff'] = func_coef + matching_hints['is_linear'] = is_linear + + + if len(set(order.values())) == 1: + order_eq = list(matching_hints['order'].values())[0] + if matching_hints['is_linear'] == True: + if matching_hints['no_of_equation'] == 2: + if order_eq == 1: + type_of_equation = check_linear_2eq_order1(eq, funcs, func_coef) + else: + type_of_equation = None + # If the equation does not match up with any of the + # general case solvers in systems.py and the number + # of equations is greater than 2, then NotImplementedError + # should be raised. + else: + type_of_equation = None + + else: + if matching_hints['no_of_equation'] == 2: + if order_eq == 1: + type_of_equation = check_nonlinear_2eq_order1(eq, funcs, func_coef) + else: + type_of_equation = None + elif matching_hints['no_of_equation'] == 3: + if order_eq == 1: + type_of_equation = check_nonlinear_3eq_order1(eq, funcs, func_coef) + else: + type_of_equation = None + else: + type_of_equation = None + else: + type_of_equation = None + + matching_hints['type_of_equation'] = type_of_equation + + return matching_hints + + +def check_linear_2eq_order1(eq, func, func_coef): + x = func[0].func + y = func[1].func + fc = func_coef + t = list(list(eq[0].atoms(Derivative))[0].atoms(Symbol))[0] + r = {} + # for equations Eq(a1*diff(x(t),t), b1*x(t) + c1*y(t) + d1) + # and Eq(a2*diff(y(t),t), b2*x(t) + c2*y(t) + d2) + r['a1'] = fc[0,x(t),1] ; r['a2'] = fc[1,y(t),1] + r['b1'] = -fc[0,x(t),0]/fc[0,x(t),1] ; r['b2'] = -fc[1,x(t),0]/fc[1,y(t),1] + r['c1'] = -fc[0,y(t),0]/fc[0,x(t),1] ; r['c2'] = -fc[1,y(t),0]/fc[1,y(t),1] + forcing = [S.Zero,S.Zero] + for i in range(2): + for j in Add.make_args(eq[i]): + if not j.has(x(t), y(t)): + forcing[i] += j + if not (forcing[0].has(t) or forcing[1].has(t)): + # We can handle homogeneous case and simple constant forcings + r['d1'] = forcing[0] + r['d2'] = forcing[1] + else: + # Issue #9244: nonhomogeneous linear systems are not supported + return None + + # Conditions to check for type 6 whose equations are Eq(diff(x(t),t), f(t)*x(t) + g(t)*y(t)) and + # Eq(diff(y(t),t), a*[f(t) + a*h(t)]x(t) + a*[g(t) - h(t)]*y(t)) + p = 0 + q = 0 + p1 = cancel(r['b2']/(cancel(r['b2']/r['c2']).as_numer_denom()[0])) + p2 = cancel(r['b1']/(cancel(r['b1']/r['c1']).as_numer_denom()[0])) + for n, i in enumerate([p1, p2]): + for j in Mul.make_args(collect_const(i)): + if not j.has(t): + q = j + if q and n==0: + if ((r['b2']/j - r['b1'])/(r['c1'] - r['c2']/j)) == j: + p = 1 + elif q and n==1: + if ((r['b1']/j - r['b2'])/(r['c2'] - r['c1']/j)) == j: + p = 2 + # End of condition for type 6 + + if r['d1']!=0 or r['d2']!=0: + return None + else: + if not any(r[k].has(t) for k in 'a1 a2 b1 b2 c1 c2'.split()): + return None + else: + r['b1'] = r['b1']/r['a1'] ; r['b2'] = r['b2']/r['a2'] + r['c1'] = r['c1']/r['a1'] ; r['c2'] = r['c2']/r['a2'] + if p: + return "type6" + else: + # Equations for type 7 are Eq(diff(x(t),t), f(t)*x(t) + g(t)*y(t)) and Eq(diff(y(t),t), h(t)*x(t) + p(t)*y(t)) + return "type7" +def check_nonlinear_2eq_order1(eq, func, func_coef): + t = list(list(eq[0].atoms(Derivative))[0].atoms(Symbol))[0] + f = Wild('f') + g = Wild('g') + u, v = symbols('u, v', cls=Dummy) + def check_type(x, y): + r1 = eq[0].match(t*diff(x(t),t) - x(t) + f) + r2 = eq[1].match(t*diff(y(t),t) - y(t) + g) + if not (r1 and r2): + r1 = eq[0].match(diff(x(t),t) - x(t)/t + f/t) + r2 = eq[1].match(diff(y(t),t) - y(t)/t + g/t) + if not (r1 and r2): + r1 = (-eq[0]).match(t*diff(x(t),t) - x(t) + f) + r2 = (-eq[1]).match(t*diff(y(t),t) - y(t) + g) + if not (r1 and r2): + r1 = (-eq[0]).match(diff(x(t),t) - x(t)/t + f/t) + r2 = (-eq[1]).match(diff(y(t),t) - y(t)/t + g/t) + if r1 and r2 and not (r1[f].subs(diff(x(t),t),u).subs(diff(y(t),t),v).has(t) \ + or r2[g].subs(diff(x(t),t),u).subs(diff(y(t),t),v).has(t)): + return 'type5' + else: + return None + for func_ in func: + if isinstance(func_, list): + x = func[0][0].func + y = func[0][1].func + eq_type = check_type(x, y) + if not eq_type: + eq_type = check_type(y, x) + return eq_type + x = func[0].func + y = func[1].func + fc = func_coef + n = Wild('n', exclude=[x(t),y(t)]) + f1 = Wild('f1', exclude=[v,t]) + f2 = Wild('f2', exclude=[v,t]) + g1 = Wild('g1', exclude=[u,t]) + g2 = Wild('g2', exclude=[u,t]) + for i in range(2): + eqs = 0 + for terms in Add.make_args(eq[i]): + eqs += terms/fc[i,func[i],1] + eq[i] = eqs + r = eq[0].match(diff(x(t),t) - x(t)**n*f) + if r: + g = (diff(y(t),t) - eq[1])/r[f] + if r and not (g.has(x(t)) or g.subs(y(t),v).has(t) or r[f].subs(x(t),u).subs(y(t),v).has(t)): + return 'type1' + r = eq[0].match(diff(x(t),t) - exp(n*x(t))*f) + if r: + g = (diff(y(t),t) - eq[1])/r[f] + if r and not (g.has(x(t)) or g.subs(y(t),v).has(t) or r[f].subs(x(t),u).subs(y(t),v).has(t)): + return 'type2' + g = Wild('g') + r1 = eq[0].match(diff(x(t),t) - f) + r2 = eq[1].match(diff(y(t),t) - g) + if r1 and r2 and not (r1[f].subs(x(t),u).subs(y(t),v).has(t) or \ + r2[g].subs(x(t),u).subs(y(t),v).has(t)): + return 'type3' + r1 = eq[0].match(diff(x(t),t) - f) + r2 = eq[1].match(diff(y(t),t) - g) + num, den = ( + (r1[f].subs(x(t),u).subs(y(t),v))/ + (r2[g].subs(x(t),u).subs(y(t),v))).as_numer_denom() + R1 = num.match(f1*g1) + R2 = den.match(f2*g2) + # phi = (r1[f].subs(x(t),u).subs(y(t),v))/num + if R1 and R2: + return 'type4' + return None + + +def check_nonlinear_2eq_order2(eq, func, func_coef): + return None + +def check_nonlinear_3eq_order1(eq, func, func_coef): + x = func[0].func + y = func[1].func + z = func[2].func + fc = func_coef + t = list(list(eq[0].atoms(Derivative))[0].atoms(Symbol))[0] + u, v, w = symbols('u, v, w', cls=Dummy) + a = Wild('a', exclude=[x(t), y(t), z(t), t]) + b = Wild('b', exclude=[x(t), y(t), z(t), t]) + c = Wild('c', exclude=[x(t), y(t), z(t), t]) + f = Wild('f') + F1 = Wild('F1') + F2 = Wild('F2') + F3 = Wild('F3') + for i in range(3): + eqs = 0 + for terms in Add.make_args(eq[i]): + eqs += terms/fc[i,func[i],1] + eq[i] = eqs + r1 = eq[0].match(diff(x(t),t) - a*y(t)*z(t)) + r2 = eq[1].match(diff(y(t),t) - b*z(t)*x(t)) + r3 = eq[2].match(diff(z(t),t) - c*x(t)*y(t)) + if r1 and r2 and r3: + num1, den1 = r1[a].as_numer_denom() + num2, den2 = r2[b].as_numer_denom() + num3, den3 = r3[c].as_numer_denom() + if solve([num1*u-den1*(v-w), num2*v-den2*(w-u), num3*w-den3*(u-v)],[u, v]): + return 'type1' + r = eq[0].match(diff(x(t),t) - y(t)*z(t)*f) + if r: + r1 = collect_const(r[f]).match(a*f) + r2 = ((diff(y(t),t) - eq[1])/r1[f]).match(b*z(t)*x(t)) + r3 = ((diff(z(t),t) - eq[2])/r1[f]).match(c*x(t)*y(t)) + if r1 and r2 and r3: + num1, den1 = r1[a].as_numer_denom() + num2, den2 = r2[b].as_numer_denom() + num3, den3 = r3[c].as_numer_denom() + if solve([num1*u-den1*(v-w), num2*v-den2*(w-u), num3*w-den3*(u-v)],[u, v]): + return 'type2' + r = eq[0].match(diff(x(t),t) - (F2-F3)) + if r: + r1 = collect_const(r[F2]).match(c*F2) + r1.update(collect_const(r[F3]).match(b*F3)) + if r1: + if eq[1].has(r1[F2]) and not eq[1].has(r1[F3]): + r1[F2], r1[F3] = r1[F3], r1[F2] + r1[c], r1[b] = -r1[b], -r1[c] + r2 = eq[1].match(diff(y(t),t) - a*r1[F3] + r1[c]*F1) + if r2: + r3 = (eq[2] == diff(z(t),t) - r1[b]*r2[F1] + r2[a]*r1[F2]) + if r1 and r2 and r3: + return 'type3' + r = eq[0].match(diff(x(t),t) - z(t)*F2 + y(t)*F3) + if r: + r1 = collect_const(r[F2]).match(c*F2) + r1.update(collect_const(r[F3]).match(b*F3)) + if r1: + if eq[1].has(r1[F2]) and not eq[1].has(r1[F3]): + r1[F2], r1[F3] = r1[F3], r1[F2] + r1[c], r1[b] = -r1[b], -r1[c] + r2 = (diff(y(t),t) - eq[1]).match(a*x(t)*r1[F3] - r1[c]*z(t)*F1) + if r2: + r3 = (diff(z(t),t) - eq[2] == r1[b]*y(t)*r2[F1] - r2[a]*x(t)*r1[F2]) + if r1 and r2 and r3: + return 'type4' + r = (diff(x(t),t) - eq[0]).match(x(t)*(F2 - F3)) + if r: + r1 = collect_const(r[F2]).match(c*F2) + r1.update(collect_const(r[F3]).match(b*F3)) + if r1: + if eq[1].has(r1[F2]) and not eq[1].has(r1[F3]): + r1[F2], r1[F3] = r1[F3], r1[F2] + r1[c], r1[b] = -r1[b], -r1[c] + r2 = (diff(y(t),t) - eq[1]).match(y(t)*(a*r1[F3] - r1[c]*F1)) + if r2: + r3 = (diff(z(t),t) - eq[2] == z(t)*(r1[b]*r2[F1] - r2[a]*r1[F2])) + if r1 and r2 and r3: + return 'type5' + return None + + +def check_nonlinear_3eq_order2(eq, func, func_coef): + return None + + +@vectorize(0) +def odesimp(ode, eq, func, hint): + r""" + Simplifies solutions of ODEs, including trying to solve for ``func`` and + running :py:meth:`~sympy.solvers.ode.constantsimp`. + + It may use knowledge of the type of solution that the hint returns to + apply additional simplifications. + + It also attempts to integrate any :py:class:`~sympy.integrals.integrals.Integral`\s + in the expression, if the hint is not an ``_Integral`` hint. + + This function should have no effect on expressions returned by + :py:meth:`~sympy.solvers.ode.dsolve`, as + :py:meth:`~sympy.solvers.ode.dsolve` already calls + :py:meth:`~sympy.solvers.ode.ode.odesimp`, but the individual hint functions + do not call :py:meth:`~sympy.solvers.ode.ode.odesimp` (because the + :py:meth:`~sympy.solvers.ode.dsolve` wrapper does). Therefore, this + function is designed for mainly internal use. + + Examples + ======== + + >>> from sympy import sin, symbols, dsolve, pprint, Function + >>> from sympy.solvers.ode.ode import odesimp + >>> x, u2, C1= symbols('x,u2,C1') + >>> f = Function('f') + + >>> eq = dsolve(x*f(x).diff(x) - f(x) - x*sin(f(x)/x), f(x), + ... hint='1st_homogeneous_coeff_subs_indep_div_dep_Integral', + ... simplify=False) + >>> pprint(eq, wrap_line=False) + x + ---- + f(x) + / + | + | / 1 \ + | -|u1 + -------| + | | /1 \| + | | sin|--|| + | \ \u1// + log(f(x)) = log(C1) + | ---------------- d(u1) + | 2 + | u1 + | + / + + >>> pprint(odesimp(eq, f(x), 1, {C1}, + ... hint='1st_homogeneous_coeff_subs_indep_div_dep' + ... )) #doctest: +SKIP + x + --------- = C1 + /f(x)\ + tan|----| + \2*x / + + """ + x = func.args[0] + f = func.func + C1 = get_numbered_constants(eq, num=1) + constants = eq.free_symbols - ode.free_symbols + + # First, integrate if the hint allows it. + eq = _handle_Integral(eq, func, hint) + if hint.startswith("nth_linear_euler_eq_nonhomogeneous"): + eq = simplify(eq) + if not isinstance(eq, Equality): + raise TypeError("eq should be an instance of Equality") + + # allow simplifications under assumption that symbols are nonzero + eq = eq.xreplace((_:={i: Dummy(nonzero=True) for i in constants})).xreplace({_[i]: i for i in _}) + + # Second, clean up the arbitrary constants. + # Right now, nth linear hints can put as many as 2*order constants in an + # expression. If that number grows with another hint, the third argument + # here should be raised accordingly, or constantsimp() rewritten to handle + # an arbitrary number of constants. + eq = constantsimp(eq, constants) + + # Lastly, now that we have cleaned up the expression, try solving for func. + # When CRootOf is implemented in solve(), we will want to return a CRootOf + # every time instead of an Equality. + + # Get the f(x) on the left if possible. + if eq.rhs == func and not eq.lhs.has(func): + eq = [Eq(eq.rhs, eq.lhs)] + + # make sure we are working with lists of solutions in simplified form. + if eq.lhs == func and not eq.rhs.has(func): + # The solution is already solved + eq = [eq] + + else: + # The solution is not solved, so try to solve it + try: + floats = any(i.is_Float for i in eq.atoms(Number)) + eqsol = solve(eq, func, force=True, rational=False if floats else None) + if not eqsol: + raise NotImplementedError + except (NotImplementedError, PolynomialError): + eq = [eq] + else: + def _expand(expr): + numer, denom = expr.as_numer_denom() + + if denom.is_Add: + return expr + else: + return powsimp(expr.expand(), combine='exp', deep=True) + + # XXX: the rest of odesimp() expects each ``t`` to be in a + # specific normal form: rational expression with numerator + # expanded, but with combined exponential functions (at + # least in this setup all tests pass). + eq = [Eq(f(x), _expand(t)) for t in eqsol] + + # special simplification of the lhs. + if hint.startswith("1st_homogeneous_coeff"): + for j, eqi in enumerate(eq): + newi = logcombine(eqi, force=True) + if isinstance(newi.lhs, log) and newi.rhs == 0: + newi = Eq(newi.lhs.args[0]/C1, C1) + eq[j] = newi + + # We cleaned up the constants before solving to help the solve engine with + # a simpler expression, but the solved expression could have introduced + # things like -C1, so rerun constantsimp() one last time before returning. + for i, eqi in enumerate(eq): + eq[i] = constantsimp(eqi, constants) + eq[i] = constant_renumber(eq[i], ode.free_symbols) + + # If there is only 1 solution, return it; + # otherwise return the list of solutions. + if len(eq) == 1: + eq = eq[0] + return eq + + +def ode_sol_simplicity(sol, func, trysolving=True): + r""" + Returns an extended integer representing how simple a solution to an ODE + is. + + The following things are considered, in order from most simple to least: + + - ``sol`` is solved for ``func``. + - ``sol`` is not solved for ``func``, but can be if passed to solve (e.g., + a solution returned by ``dsolve(ode, func, simplify=False``). + - If ``sol`` is not solved for ``func``, then base the result on the + length of ``sol``, as computed by ``len(str(sol))``. + - If ``sol`` has any unevaluated :py:class:`~sympy.integrals.integrals.Integral`\s, + this will automatically be considered less simple than any of the above. + + This function returns an integer such that if solution A is simpler than + solution B by above metric, then ``ode_sol_simplicity(sola, func) < + ode_sol_simplicity(solb, func)``. + + Currently, the following are the numbers returned, but if the heuristic is + ever improved, this may change. Only the ordering is guaranteed. + + +----------------------------------------------+-------------------+ + | Simplicity | Return | + +==============================================+===================+ + | ``sol`` solved for ``func`` | ``-2`` | + +----------------------------------------------+-------------------+ + | ``sol`` not solved for ``func`` but can be | ``-1`` | + +----------------------------------------------+-------------------+ + | ``sol`` is not solved nor solvable for | ``len(str(sol))`` | + | ``func`` | | + +----------------------------------------------+-------------------+ + | ``sol`` contains an | ``oo`` | + | :obj:`~sympy.integrals.integrals.Integral` | | + +----------------------------------------------+-------------------+ + + ``oo`` here means the SymPy infinity, which should compare greater than + any integer. + + If you already know :py:meth:`~sympy.solvers.solvers.solve` cannot solve + ``sol``, you can use ``trysolving=False`` to skip that step, which is the + only potentially slow step. For example, + :py:meth:`~sympy.solvers.ode.dsolve` with the ``simplify=False`` flag + should do this. + + If ``sol`` is a list of solutions, if the worst solution in the list + returns ``oo`` it returns that, otherwise it returns ``len(str(sol))``, + that is, the length of the string representation of the whole list. + + Examples + ======== + + This function is designed to be passed to ``min`` as the key argument, + such as ``min(listofsolutions, key=lambda i: ode_sol_simplicity(i, + f(x)))``. + + >>> from sympy import symbols, Function, Eq, tan, Integral + >>> from sympy.solvers.ode.ode import ode_sol_simplicity + >>> x, C1, C2 = symbols('x, C1, C2') + >>> f = Function('f') + + >>> ode_sol_simplicity(Eq(f(x), C1*x**2), f(x)) + -2 + >>> ode_sol_simplicity(Eq(x**2 + f(x), C1), f(x)) + -1 + >>> ode_sol_simplicity(Eq(f(x), C1*Integral(2*x, x)), f(x)) + oo + >>> eq1 = Eq(f(x)/tan(f(x)/(2*x)), C1) + >>> eq2 = Eq(f(x)/tan(f(x)/(2*x) + f(x)), C2) + >>> [ode_sol_simplicity(eq, f(x)) for eq in [eq1, eq2]] + [28, 35] + >>> min([eq1, eq2], key=lambda i: ode_sol_simplicity(i, f(x))) + Eq(f(x)/tan(f(x)/(2*x)), C1) + + """ + # TODO: if two solutions are solved for f(x), we still want to be + # able to get the simpler of the two + + # See the docstring for the coercion rules. We check easier (faster) + # things here first, to save time. + + if iterable(sol): + # See if there are Integrals + for i in sol: + if ode_sol_simplicity(i, func, trysolving=trysolving) == oo: + return oo + + return len(str(sol)) + + if sol.has(Integral): + return oo + + # Next, try to solve for func. This code will change slightly when CRootOf + # is implemented in solve(). Probably a CRootOf solution should fall + # somewhere between a normal solution and an unsolvable expression. + + # First, see if they are already solved + if sol.lhs == func and not sol.rhs.has(func) or \ + sol.rhs == func and not sol.lhs.has(func): + return -2 + # We are not so lucky, try solving manually + if trysolving: + try: + sols = solve(sol, func) + if not sols: + raise NotImplementedError + except NotImplementedError: + pass + else: + return -1 + + # Finally, a naive computation based on the length of the string version + # of the expression. This may favor combined fractions because they + # will not have duplicate denominators, and may slightly favor expressions + # with fewer additions and subtractions, as those are separated by spaces + # by the printer. + + # Additional ideas for simplicity heuristics are welcome, like maybe + # checking if a equation has a larger domain, or if constantsimp has + # introduced arbitrary constants numbered higher than the order of a + # given ODE that sol is a solution of. + return len(str(sol)) + + +def _extract_funcs(eqs): + funcs = [] + for eq in eqs: + derivs = [node for node in preorder_traversal(eq) if isinstance(node, Derivative)] + func = [] + for d in derivs: + func += list(d.atoms(AppliedUndef)) + for func_ in func: + funcs.append(func_) + funcs = list(uniq(funcs)) + + return funcs + + +def _get_constant_subexpressions(expr, Cs): + Cs = set(Cs) + Ces = [] + def _recursive_walk(expr): + expr_syms = expr.free_symbols + if expr_syms and expr_syms.issubset(Cs): + Ces.append(expr) + else: + if expr.func == exp: + expr = expr.expand(mul=True) + if expr.func in (Add, Mul): + d = sift(expr.args, lambda i : i.free_symbols.issubset(Cs)) + if len(d[True]) > 1: + x = expr.func(*d[True]) + if not x.is_number: + Ces.append(x) + elif isinstance(expr, Integral): + if expr.free_symbols.issubset(Cs) and \ + all(len(x) == 3 for x in expr.limits): + Ces.append(expr) + for i in expr.args: + _recursive_walk(i) + return + _recursive_walk(expr) + return Ces + +def __remove_linear_redundancies(expr, Cs): + cnts = {i: expr.count(i) for i in Cs} + Cs = [i for i in Cs if cnts[i] > 0] + + def _linear(expr): + if isinstance(expr, Add): + xs = [i for i in Cs if expr.count(i)==cnts[i] \ + and 0 == expr.diff(i, 2)] + d = {} + for x in xs: + y = expr.diff(x) + if y not in d: + d[y]=[] + d[y].append(x) + for y in d: + if len(d[y]) > 1: + d[y].sort(key=str) + for x in d[y][1:]: + expr = expr.subs(x, 0) + return expr + + def _recursive_walk(expr): + if len(expr.args) != 0: + expr = expr.func(*[_recursive_walk(i) for i in expr.args]) + expr = _linear(expr) + return expr + + if isinstance(expr, Equality): + lhs, rhs = [_recursive_walk(i) for i in expr.args] + f = lambda i: isinstance(i, Number) or i in Cs + if isinstance(lhs, Symbol) and lhs in Cs: + rhs, lhs = lhs, rhs + if lhs.func in (Add, Symbol) and rhs.func in (Add, Symbol): + dlhs = sift([lhs] if isinstance(lhs, AtomicExpr) else lhs.args, f) + drhs = sift([rhs] if isinstance(rhs, AtomicExpr) else rhs.args, f) + for i in [True, False]: + for hs in [dlhs, drhs]: + if i not in hs: + hs[i] = [0] + # this calculation can be simplified + lhs = Add(*dlhs[False]) - Add(*drhs[False]) + rhs = Add(*drhs[True]) - Add(*dlhs[True]) + elif lhs.func in (Mul, Symbol) and rhs.func in (Mul, Symbol): + dlhs = sift([lhs] if isinstance(lhs, AtomicExpr) else lhs.args, f) + if True in dlhs: + if False not in dlhs: + dlhs[False] = [1] + lhs = Mul(*dlhs[False]) + rhs = rhs/Mul(*dlhs[True]) + return Eq(lhs, rhs) + else: + return _recursive_walk(expr) + +@vectorize(0) +def constantsimp(expr, constants): + r""" + Simplifies an expression with arbitrary constants in it. + + This function is written specifically to work with + :py:meth:`~sympy.solvers.ode.dsolve`, and is not intended for general use. + + Simplification is done by "absorbing" the arbitrary constants into other + arbitrary constants, numbers, and symbols that they are not independent + of. + + The symbols must all have the same name with numbers after it, for + example, ``C1``, ``C2``, ``C3``. The ``symbolname`` here would be + '``C``', the ``startnumber`` would be 1, and the ``endnumber`` would be 3. + If the arbitrary constants are independent of the variable ``x``, then the + independent symbol would be ``x``. There is no need to specify the + dependent function, such as ``f(x)``, because it already has the + independent symbol, ``x``, in it. + + Because terms are "absorbed" into arbitrary constants and because + constants are renumbered after simplifying, the arbitrary constants in + expr are not necessarily equal to the ones of the same name in the + returned result. + + If two or more arbitrary constants are added, multiplied, or raised to the + power of each other, they are first absorbed together into a single + arbitrary constant. Then the new constant is combined into other terms if + necessary. + + Absorption of constants is done with limited assistance: + + 1. terms of :py:class:`~sympy.core.add.Add`\s are collected to try join + constants so `e^x (C_1 \cos(x) + C_2 \cos(x))` will simplify to `e^x + C_1 \cos(x)`; + + 2. powers with exponents that are :py:class:`~sympy.core.add.Add`\s are + expanded so `e^{C_1 + x}` will be simplified to `C_1 e^x`. + + Use :py:meth:`~sympy.solvers.ode.ode.constant_renumber` to renumber constants + after simplification or else arbitrary numbers on constants may appear, + e.g. `C_1 + C_3 x`. + + In rare cases, a single constant can be "simplified" into two constants. + Every differential equation solution should have as many arbitrary + constants as the order of the differential equation. The result here will + be technically correct, but it may, for example, have `C_1` and `C_2` in + an expression, when `C_1` is actually equal to `C_2`. Use your discretion + in such situations, and also take advantage of the ability to use hints in + :py:meth:`~sympy.solvers.ode.dsolve`. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.solvers.ode.ode import constantsimp + >>> C1, C2, C3, x, y = symbols('C1, C2, C3, x, y') + >>> constantsimp(2*C1*x, {C1, C2, C3}) + C1*x + >>> constantsimp(C1 + 2 + x, {C1, C2, C3}) + C1 + x + >>> constantsimp(C1*C2 + 2 + C2 + C3*x, {C1, C2, C3}) + C1 + C3*x + + """ + # This function works recursively. The idea is that, for Mul, + # Add, Pow, and Function, if the class has a constant in it, then + # we can simplify it, which we do by recursing down and + # simplifying up. Otherwise, we can skip that part of the + # expression. + + Cs = constants + + orig_expr = expr + + constant_subexprs = _get_constant_subexpressions(expr, Cs) + for xe in constant_subexprs: + xes = list(xe.free_symbols) + if not xes: + continue + if all(expr.count(c) == xe.count(c) for c in xes): + xes.sort(key=str) + expr = expr.subs(xe, xes[0]) + + # try to perform common sub-expression elimination of constant terms + try: + commons, rexpr = cse(expr) + commons.reverse() + rexpr = rexpr[0] + for s in commons: + cs = list(s[1].atoms(Symbol)) + if len(cs) == 1 and cs[0] in Cs and \ + cs[0] not in rexpr.atoms(Symbol) and \ + not any(cs[0] in ex for ex in commons if ex != s): + rexpr = rexpr.subs(s[0], cs[0]) + else: + rexpr = rexpr.subs(*s) + expr = rexpr + except IndexError: + pass + expr = __remove_linear_redundancies(expr, Cs) + + def _conditional_term_factoring(expr): + new_expr = terms_gcd(expr, clear=False, deep=True, expand=False) + + # we do not want to factor exponentials, so handle this separately + if new_expr.is_Mul: + infac = False + asfac = False + for m in new_expr.args: + if isinstance(m, exp): + asfac = True + elif m.is_Add: + infac = any(isinstance(fi, exp) for t in m.args + for fi in Mul.make_args(t)) + if asfac and infac: + new_expr = expr + break + return new_expr + + expr = _conditional_term_factoring(expr) + + # call recursively if more simplification is possible + if orig_expr != expr: + return constantsimp(expr, Cs) + return expr + + +def constant_renumber(expr, variables=None, newconstants=None): + r""" + Renumber arbitrary constants in ``expr`` to use the symbol names as given + in ``newconstants``. In the process, this reorders expression terms in a + standard way. + + If ``newconstants`` is not provided then the new constant names will be + ``C1``, ``C2`` etc. Otherwise ``newconstants`` should be an iterable + giving the new symbols to use for the constants in order. + + The ``variables`` argument is a list of non-constant symbols. All other + free symbols found in ``expr`` are assumed to be constants and will be + renumbered. If ``variables`` is not given then any numbered symbol + beginning with ``C`` (e.g. ``C1``) is assumed to be a constant. + + Symbols are renumbered based on ``.sort_key()``, so they should be + numbered roughly in the order that they appear in the final, printed + expression. Note that this ordering is based in part on hashes, so it can + produce different results on different machines. + + The structure of this function is very similar to that of + :py:meth:`~sympy.solvers.ode.constantsimp`. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.solvers.ode.ode import constant_renumber + >>> x, C1, C2, C3 = symbols('x,C1:4') + >>> expr = C3 + C2*x + C1*x**2 + >>> expr + C1*x**2 + C2*x + C3 + >>> constant_renumber(expr) + C1 + C2*x + C3*x**2 + + The ``variables`` argument specifies which are constants so that the + other symbols will not be renumbered: + + >>> constant_renumber(expr, [C1, x]) + C1*x**2 + C2 + C3*x + + The ``newconstants`` argument is used to specify what symbols to use when + replacing the constants: + + >>> constant_renumber(expr, [x], newconstants=symbols('E1:4')) + E1 + E2*x + E3*x**2 + + """ + + # System of expressions + if isinstance(expr, (set, list, tuple)): + return type(expr)(constant_renumber(Tuple(*expr), + variables=variables, newconstants=newconstants)) + + # Symbols in solution but not ODE are constants + if variables is not None: + variables = set(variables) + free_symbols = expr.free_symbols + constantsymbols = list(free_symbols - variables) + # Any Cn is a constant... + else: + variables = set() + isconstant = lambda s: s.startswith('C') and s[1:].isdigit() + constantsymbols = [sym for sym in expr.free_symbols if isconstant(sym.name)] + + # Find new constants checking that they aren't already in the ODE + if newconstants is None: + iter_constants = numbered_symbols(start=1, prefix='C', exclude=variables) + else: + iter_constants = (sym for sym in newconstants if sym not in variables) + + constants_found = [] + + # make a mapping to send all constantsymbols to S.One and use + # that to make sure that term ordering is not dependent on + # the indexed value of C + C_1 = [(ci, S.One) for ci in constantsymbols] + sort_key=lambda arg: default_sort_key(arg.subs(C_1)) + + def _constant_renumber(expr): + r""" + We need to have an internal recursive function + """ + + # For system of expressions + if isinstance(expr, Tuple): + renumbered = [_constant_renumber(e) for e in expr] + return Tuple(*renumbered) + + if isinstance(expr, Equality): + return Eq( + _constant_renumber(expr.lhs), + _constant_renumber(expr.rhs)) + + if type(expr) not in (Mul, Add, Pow) and not expr.is_Function and \ + not expr.has(*constantsymbols): + # Base case, as above. Hope there aren't constants inside + # of some other class, because they won't be renumbered. + return expr + elif expr.is_Piecewise: + return expr + elif expr in constantsymbols: + if expr not in constants_found: + constants_found.append(expr) + return expr + elif expr.is_Function or expr.is_Pow: + return expr.func( + *[_constant_renumber(x) for x in expr.args]) + else: + sortedargs = list(expr.args) + sortedargs.sort(key=sort_key) + return expr.func(*[_constant_renumber(x) for x in sortedargs]) + expr = _constant_renumber(expr) + + # Don't renumber symbols present in the ODE. + constants_found = [c for c in constants_found if c not in variables] + + # Renumbering happens here + subs_dict = dict(zip(constants_found, iter_constants)) + expr = expr.subs(subs_dict, simultaneous=True) + + return expr + + +def _handle_Integral(expr, func, hint): + r""" + Converts a solution with Integrals in it into an actual solution. + + For most hints, this simply runs ``expr.doit()``. + + """ + if hint == "nth_linear_constant_coeff_homogeneous": + sol = expr + elif not hint.endswith("_Integral"): + sol = expr.doit() + else: + sol = expr + return sol + + +# XXX: Should this function maybe go somewhere else? + + +def homogeneous_order(eq, *symbols): + r""" + Returns the order `n` if `g` is homogeneous and ``None`` if it is not + homogeneous. + + Determines if a function is homogeneous and if so of what order. A + function `f(x, y, \cdots)` is homogeneous of order `n` if `f(t x, t y, + \cdots) = t^n f(x, y, \cdots)`. + + If the function is of two variables, `F(x, y)`, then `f` being homogeneous + of any order is equivalent to being able to rewrite `F(x, y)` as `G(x/y)` + or `H(y/x)`. This fact is used to solve 1st order ordinary differential + equations whose coefficients are homogeneous of the same order (see the + docstrings of + :obj:`~sympy.solvers.ode.single.HomogeneousCoeffSubsDepDivIndep` and + :obj:`~sympy.solvers.ode.single.HomogeneousCoeffSubsIndepDivDep`). + + Symbols can be functions, but every argument of the function must be a + symbol, and the arguments of the function that appear in the expression + must match those given in the list of symbols. If a declared function + appears with different arguments than given in the list of symbols, + ``None`` is returned. + + Examples + ======== + + >>> from sympy import Function, homogeneous_order, sqrt + >>> from sympy.abc import x, y + >>> f = Function('f') + >>> homogeneous_order(f(x), f(x)) is None + True + >>> homogeneous_order(f(x,y), f(y, x), x, y) is None + True + >>> homogeneous_order(f(x), f(x), x) + 1 + >>> homogeneous_order(x**2*f(x)/sqrt(x**2+f(x)**2), x, f(x)) + 2 + >>> homogeneous_order(x**2+f(x), x, f(x)) is None + True + + """ + + if not symbols: + raise ValueError("homogeneous_order: no symbols were given.") + symset = set(symbols) + eq = sympify(eq) + + # The following are not supported + if eq.has(Order, Derivative): + return None + + # These are all constants + if (eq.is_Number or + eq.is_NumberSymbol or + eq.is_number + ): + return S.Zero + + # Replace all functions with dummy variables + dum = numbered_symbols(prefix='d', cls=Dummy) + newsyms = set() + for i in [j for j in symset if getattr(j, 'is_Function')]: + iargs = set(i.args) + if iargs.difference(symset): + return None + else: + dummyvar = next(dum) + eq = eq.subs(i, dummyvar) + symset.remove(i) + newsyms.add(dummyvar) + symset.update(newsyms) + + if not eq.free_symbols & symset: + return None + + # assuming order of a nested function can only be equal to zero + if isinstance(eq, Function): + return None if homogeneous_order( + eq.args[0], *tuple(symset)) != 0 else S.Zero + + # make the replacement of x with x*t and see if t can be factored out + t = Dummy('t', positive=True) # It is sufficient that t > 0 + eqs = separatevars(eq.subs([(i, t*i) for i in symset]), [t], dict=True)[t] + if eqs is S.One: + return S.Zero # there was no term with only t + i, d = eqs.as_independent(t, as_Add=False) + b, e = d.as_base_exp() + if b == t: + return e + + +def ode_2nd_power_series_ordinary(eq, func, order, match): + r""" + Gives a power series solution to a second order homogeneous differential + equation with polynomial coefficients at an ordinary point. A homogeneous + differential equation is of the form + + .. math :: P(x)\frac{d^2y}{dx^2} + Q(x)\frac{dy}{dx} + R(x) y(x) = 0 + + For simplicity it is assumed that `P(x)`, `Q(x)` and `R(x)` are polynomials, + it is sufficient that `\frac{Q(x)}{P(x)}` and `\frac{R(x)}{P(x)}` exists at + `x_{0}`. A recurrence relation is obtained by substituting `y` as `\sum_{n=0}^\infty a_{n}x^{n}`, + in the differential equation, and equating the nth term. Using this relation + various terms can be generated. + + + Examples + ======== + + >>> from sympy import dsolve, Function, pprint + >>> from sympy.abc import x + >>> f = Function("f") + >>> eq = f(x).diff(x, 2) + f(x) + >>> pprint(dsolve(eq, hint='2nd_power_series_ordinary')) + / 4 2 \ / 2\ + |x x | | x | / 6\ + f(x) = C2*|-- - -- + 1| + C1*x*|1 - --| + O\x / + \24 2 / \ 6 / + + + References + ========== + - https://tutorial.math.lamar.edu/Classes/DE/SeriesSolutions.aspx + - George E. Simmons, "Differential Equations with Applications and + Historical Notes", p.p 176 - 184 + + """ + x = func.args[0] + f = func.func + C0, C1 = get_numbered_constants(eq, num=2) + n = Dummy("n", integer=True) + s = Wild("s") + k = Wild("k", exclude=[x]) + x0 = match['x0'] + terms = match['terms'] + p = match[match['a3']] + q = match[match['b3']] + r = match[match['c3']] + seriesdict = {} + recurr = Function("r") + + # Generating the recurrence relation which works this way: + # for the second order term the summation begins at n = 2. The coefficients + # p is multiplied with an*(n - 1)*(n - 2)*x**n-2 and a substitution is made such that + # the exponent of x becomes n. + # For example, if p is x, then the second degree recurrence term is + # an*(n - 1)*(n - 2)*x**n-1, substituting (n - 1) as n, it transforms to + # an+1*n*(n - 1)*x**n. + # A similar process is done with the first order and zeroth order term. + + coefflist = [(recurr(n), r), (n*recurr(n), q), (n*(n - 1)*recurr(n), p)] + for index, coeff in enumerate(coefflist): + if coeff[1]: + f2 = powsimp(expand((coeff[1]*(x - x0)**(n - index)).subs(x, x + x0))) + if f2.is_Add: + addargs = f2.args + else: + addargs = [f2] + for arg in addargs: + powm = arg.match(s*x**k) + term = coeff[0]*powm[s] + if not powm[k].is_Symbol: + term = term.subs(n, n - powm[k].as_independent(n)[0]) + startind = powm[k].subs(n, index) + # Seeing if the startterm can be reduced further. + # If it vanishes for n lesser than startind, it is + # equal to summation from n. + if startind: + for i in reversed(range(startind)): + if not term.subs(n, i): + seriesdict[term] = i + else: + seriesdict[term] = i + 1 + break + else: + seriesdict[term] = S.Zero + + # Stripping of terms so that the sum starts with the same number. + teq = S.Zero + suminit = seriesdict.values() + rkeys = seriesdict.keys() + req = Add(*rkeys) + if any(suminit): + maxval = max(suminit) + for term in seriesdict: + val = seriesdict[term] + if val != maxval: + for i in range(val, maxval): + teq += term.subs(n, val) + + finaldict = {} + if teq: + fargs = teq.atoms(AppliedUndef) + if len(fargs) == 1: + finaldict[fargs.pop()] = 0 + else: + maxf = max(fargs, key = lambda x: x.args[0]) + sol = solve(teq, maxf) + if isinstance(sol, list): + sol = sol[0] + finaldict[maxf] = sol + + # Finding the recurrence relation in terms of the largest term. + fargs = req.atoms(AppliedUndef) + maxf = max(fargs, key = lambda x: x.args[0]) + minf = min(fargs, key = lambda x: x.args[0]) + if minf.args[0].is_Symbol: + startiter = 0 + else: + startiter = -minf.args[0].as_independent(n)[0] + lhs = maxf + rhs = solve(req, maxf) + if isinstance(rhs, list): + rhs = rhs[0] + + # Checking how many values are already present + tcounter = len([t for t in finaldict.values() if t]) + + for _ in range(tcounter, terms - 3): # Assuming c0 and c1 to be arbitrary + check = rhs.subs(n, startiter) + nlhs = lhs.subs(n, startiter) + nrhs = check.subs(finaldict) + finaldict[nlhs] = nrhs + startiter += 1 + + # Post processing + series = C0 + C1*(x - x0) + for term in finaldict: + if finaldict[term]: + fact = term.args[0] + series += (finaldict[term].subs([(recurr(0), C0), (recurr(1), C1)])*( + x - x0)**fact) + series = collect(expand_mul(series), [C0, C1]) + Order(x**terms) + return Eq(f(x), series) + + +def ode_2nd_power_series_regular(eq, func, order, match): + r""" + Gives a power series solution to a second order homogeneous differential + equation with polynomial coefficients at a regular point. A second order + homogeneous differential equation is of the form + + .. math :: P(x)\frac{d^2y}{dx^2} + Q(x)\frac{dy}{dx} + R(x) y(x) = 0 + + A point is said to regular singular at `x0` if `x - x0\frac{Q(x)}{P(x)}` + and `(x - x0)^{2}\frac{R(x)}{P(x)}` are analytic at `x0`. For simplicity + `P(x)`, `Q(x)` and `R(x)` are assumed to be polynomials. The algorithm for + finding the power series solutions is: + + 1. Try expressing `(x - x0)P(x)` and `((x - x0)^{2})Q(x)` as power series + solutions about x0. Find `p0` and `q0` which are the constants of the + power series expansions. + 2. Solve the indicial equation `f(m) = m(m - 1) + m*p0 + q0`, to obtain the + roots `m1` and `m2` of the indicial equation. + 3. If `m1 - m2` is a non integer there exists two series solutions. If + `m1 = m2`, there exists only one solution. If `m1 - m2` is an integer, + then the existence of one solution is confirmed. The other solution may + or may not exist. + + The power series solution is of the form `x^{m}\sum_{n=0}^\infty a_{n}x^{n}`. The + coefficients are determined by the following recurrence relation. + `a_{n} = -\frac{\sum_{k=0}^{n-1} q_{n-k} + (m + k)p_{n-k}}{f(m + n)}`. For the case + in which `m1 - m2` is an integer, it can be seen from the recurrence relation + that for the lower root `m`, when `n` equals the difference of both the + roots, the denominator becomes zero. So if the numerator is not equal to zero, + a second series solution exists. + + + Examples + ======== + + >>> from sympy import dsolve, Function, pprint + >>> from sympy.abc import x + >>> f = Function("f") + >>> eq = x*(f(x).diff(x, 2)) + 2*(f(x).diff(x)) + x*f(x) + >>> pprint(dsolve(eq, hint='2nd_power_series_regular')) + / 6 4 2 \ + | x x x | + / 4 2 \ C1*|- --- + -- - -- + 1| + |x x | \ 720 24 2 / / 6\ + f(x) = C2*|--- - -- + 1| + ------------------------ + O\x / + \120 6 / x + + + References + ========== + - George E. Simmons, "Differential Equations with Applications and + Historical Notes", p.p 176 - 184 + + """ + x = func.args[0] + f = func.func + C0, C1 = get_numbered_constants(eq, num=2) + m = Dummy("m") # for solving the indicial equation + x0 = match['x0'] + terms = match['terms'] + p = match['p'] + q = match['q'] + + # Generating the indicial equation + indicial = [] + for term in [p, q]: + if not term.has(x): + indicial.append(term) + else: + term = series(term, x=x, n=1, x0=x0) + if isinstance(term, Order): + indicial.append(S.Zero) + else: + for arg in term.args: + if not arg.has(x): + indicial.append(arg) + break + + p0, q0 = indicial + sollist = solve(m*(m - 1) + m*p0 + q0, m) + if sollist and isinstance(sollist, list) and all( + sol.is_real for sol in sollist): + serdict1 = {} + serdict2 = {} + if len(sollist) == 1: + # Only one series solution exists in this case. + m1 = m2 = sollist.pop() + if terms-m1-1 <= 0: + return Eq(f(x), Order(terms)) + serdict1 = _frobenius(terms-m1-1, m1, p0, q0, p, q, x0, x, C0) + + else: + m1 = sollist[0] + m2 = sollist[1] + if m1 < m2: + m1, m2 = m2, m1 + # Irrespective of whether m1 - m2 is an integer or not, one + # Frobenius series solution exists. + serdict1 = _frobenius(terms-m1-1, m1, p0, q0, p, q, x0, x, C0) + if not (m1 - m2).is_integer: + # Second frobenius series solution exists. + serdict2 = _frobenius(terms-m2-1, m2, p0, q0, p, q, x0, x, C1) + else: + # Check if second frobenius series solution exists. + serdict2 = _frobenius(terms-m2-1, m2, p0, q0, p, q, x0, x, C1, check=m1) + + if serdict1: + finalseries1 = C0 + for key in serdict1: + power = int(key.name[1:]) + finalseries1 += serdict1[key]*(x - x0)**power + finalseries1 = (x - x0)**m1*finalseries1 + finalseries2 = S.Zero + if serdict2: + for key in serdict2: + power = int(key.name[1:]) + finalseries2 += serdict2[key]*(x - x0)**power + finalseries2 += C1 + finalseries2 = (x - x0)**m2*finalseries2 + return Eq(f(x), collect(finalseries1 + finalseries2, + [C0, C1]) + Order(x**terms)) + + +def _frobenius(n, m, p0, q0, p, q, x0, x, c, check=None): + r""" + Returns a dict with keys as coefficients and values as their values in terms of C0 + """ + n = int(n) + # In cases where m1 - m2 is not an integer + m2 = check + + d = Dummy("d") + numsyms = numbered_symbols("C", start=0) + numsyms = [next(numsyms) for i in range(n + 1)] + serlist = [] + for ser in [p, q]: + # Order term not present + if ser.is_polynomial(x) and Poly(ser, x).degree() <= n: + if x0: + ser = ser.subs(x, x + x0) + dict_ = Poly(ser, x).as_dict() + # Order term present + else: + tseries = series(ser, x=x0, n=n+1) + # Removing order + dict_ = Poly(list(ordered(tseries.args))[: -1], x).as_dict() + # Fill in with zeros, if coefficients are zero. + for i in range(n + 1): + if (i,) not in dict_: + dict_[(i,)] = S.Zero + serlist.append(dict_) + + pseries = serlist[0] + qseries = serlist[1] + indicial = d*(d - 1) + d*p0 + q0 + frobdict = {} + for i in range(1, n + 1): + num = c*(m*pseries[(i,)] + qseries[(i,)]) + for j in range(1, i): + sym = Symbol("C" + str(j)) + num += frobdict[sym]*((m + j)*pseries[(i - j,)] + qseries[(i - j,)]) + + # Checking for cases when m1 - m2 is an integer. If num equals zero + # then a second Frobenius series solution cannot be found. If num is not zero + # then set constant as zero and proceed. + if m2 is not None and i == m2 - m: + if num: + return False + else: + frobdict[numsyms[i]] = S.Zero + else: + frobdict[numsyms[i]] = -num/(indicial.subs(d, m+i)) + + return frobdict + +def _remove_redundant_solutions(eq, solns, order, var): + r""" + Remove redundant solutions from the set of solutions. + + This function is needed because otherwise dsolve can return + redundant solutions. As an example consider: + + eq = Eq((f(x).diff(x, 2))*f(x).diff(x), 0) + + There are two ways to find solutions to eq. The first is to solve f(x).diff(x, 2) = 0 + leading to solution f(x)=C1 + C2*x. The second is to solve the equation f(x).diff(x) = 0 + leading to the solution f(x) = C1. In this particular case we then see + that the second solution is a special case of the first and we do not + want to return it. + + This does not always happen. If we have + + eq = Eq((f(x)**2-4)*(f(x).diff(x)-4), 0) + + then we get the algebraic solution f(x) = [-2, 2] and the integral solution + f(x) = x + C1 and in this case the two solutions are not equivalent wrt + initial conditions so both should be returned. + """ + def is_special_case_of(soln1, soln2): + return _is_special_case_of(soln1, soln2, eq, order, var) + + unique_solns = [] + for soln1 in solns: + for soln2 in unique_solns.copy(): + if is_special_case_of(soln1, soln2): + break + elif is_special_case_of(soln2, soln1): + unique_solns.remove(soln2) + else: + unique_solns.append(soln1) + + return unique_solns + +def _is_special_case_of(soln1, soln2, eq, order, var): + r""" + True if soln1 is found to be a special case of soln2 wrt some value of the + constants that appear in soln2. False otherwise. + """ + # The solutions returned by dsolve may be given explicitly or implicitly. + # We will equate the sol1=(soln1.rhs - soln1.lhs), sol2=(soln2.rhs - soln2.lhs) + # of the two solutions. + # + # Since this is supposed to hold for all x it also holds for derivatives. + # For an order n ode we should be able to differentiate + # each solution n times to get n+1 equations. + # + # We then try to solve those n+1 equations for the integrations constants + # in sol2. If we can find a solution that does not depend on x then it + # means that some value of the constants in sol1 is a special case of + # sol2 corresponding to a particular choice of the integration constants. + + # In case the solution is in implicit form we subtract the sides + soln1 = soln1.rhs - soln1.lhs + soln2 = soln2.rhs - soln2.lhs + + # Work for the series solution + if soln1.has(Order) and soln2.has(Order): + if soln1.getO() == soln2.getO(): + soln1 = soln1.removeO() + soln2 = soln2.removeO() + else: + return False + elif soln1.has(Order) or soln2.has(Order): + return False + + constants1 = soln1.free_symbols.difference(eq.free_symbols) + constants2 = soln2.free_symbols.difference(eq.free_symbols) + + constants1_new = get_numbered_constants(Tuple(soln1, soln2), len(constants1)) + if len(constants1) == 1: + constants1_new = {constants1_new} + for c_old, c_new in zip(constants1, constants1_new): + soln1 = soln1.subs(c_old, c_new) + + # n equations for sol1 = sol2, sol1'=sol2', ... + lhs = soln1 + rhs = soln2 + eqns = [Eq(lhs, rhs)] + for n in range(1, order): + lhs = lhs.diff(var) + rhs = rhs.diff(var) + eq = Eq(lhs, rhs) + eqns.append(eq) + + # BooleanTrue/False awkwardly show up for trivial equations + if any(isinstance(eq, BooleanFalse) for eq in eqns): + return False + eqns = [eq for eq in eqns if not isinstance(eq, BooleanTrue)] + + try: + constant_solns = solve(eqns, constants2) + except NotImplementedError: + return False + + # Sometimes returns a dict and sometimes a list of dicts + if isinstance(constant_solns, dict): + constant_solns = [constant_solns] + + # after solving the issue 17418, maybe we don't need the following checksol code. + for constant_soln in constant_solns: + for eq in eqns: + eq=eq.rhs-eq.lhs + if checksol(eq, constant_soln) is not True: + return False + + # If any solution gives all constants as expressions that don't depend on + # x then there exists constants for soln2 that give soln1 + for constant_soln in constant_solns: + if not any(c.has(var) for c in constant_soln.values()): + return True + + return False + + +def ode_1st_power_series(eq, func, order, match): + r""" + The power series solution is a method which gives the Taylor series expansion + to the solution of a differential equation. + + For a first order differential equation `\frac{dy}{dx} = h(x, y)`, a power + series solution exists at a point `x = x_{0}` if `h(x, y)` is analytic at `x_{0}`. + The solution is given by + + .. math:: y(x) = y(x_{0}) + \sum_{n = 1}^{\infty} \frac{F_{n}(x_{0},b)(x - x_{0})^n}{n!}, + + where `y(x_{0}) = b` is the value of y at the initial value of `x_{0}`. + To compute the values of the `F_{n}(x_{0},b)` the following algorithm is + followed, until the required number of terms are generated. + + 1. `F_1 = h(x_{0}, b)` + 2. `F_{n+1} = \frac{\partial F_{n}}{\partial x} + \frac{\partial F_{n}}{\partial y}F_{1}` + + Examples + ======== + + >>> from sympy import Function, pprint, exp, dsolve + >>> from sympy.abc import x + >>> f = Function('f') + >>> eq = exp(x)*(f(x).diff(x)) - f(x) + >>> pprint(dsolve(eq, hint='1st_power_series')) + 3 4 5 + C1*x C1*x C1*x / 6\ + f(x) = C1 + C1*x - ----- + ----- + ----- + O\x / + 6 24 60 + + + References + ========== + + - Travis W. Walker, Analytic power series technique for solving first-order + differential equations, p.p 17, 18 + + """ + x = func.args[0] + y = match['y'] + f = func.func + h = -match[match['d']]/match[match['e']] + point = match['f0'] + value = match['f0val'] + terms = match['terms'] + + # First term + F = h + if not h: + return Eq(f(x), value) + + # Initialization + series = value + if terms > 1: + hc = h.subs({x: point, y: value}) + if hc.has(oo) or hc.has(nan) or hc.has(zoo): + # Derivative does not exist, not analytic + return Eq(f(x), oo) + elif hc: + series += hc*(x - point) + + for factcount in range(2, terms): + Fnew = F.diff(x) + F.diff(y)*h + Fnewc = Fnew.subs({x: point, y: value}) + # Same logic as above + if Fnewc.has(oo) or Fnewc.has(nan) or Fnewc.has(-oo) or Fnewc.has(zoo): + return Eq(f(x), oo) + series += Fnewc*((x - point)**factcount)/factorial(factcount) + F = Fnew + series += Order(x**terms) + return Eq(f(x), series) + + +def checkinfsol(eq, infinitesimals, func=None, order=None): + r""" + This function is used to check if the given infinitesimals are the + actual infinitesimals of the given first order differential equation. + This method is specific to the Lie Group Solver of ODEs. + + As of now, it simply checks, by substituting the infinitesimals in the + partial differential equation. + + + .. math:: \frac{\partial \eta}{\partial x} + \left(\frac{\partial \eta}{\partial y} + - \frac{\partial \xi}{\partial x}\right)*h + - \frac{\partial \xi}{\partial y}*h^{2} + - \xi\frac{\partial h}{\partial x} - \eta\frac{\partial h}{\partial y} = 0 + + + where `\eta`, and `\xi` are the infinitesimals and `h(x,y) = \frac{dy}{dx}` + + The infinitesimals should be given in the form of a list of dicts + ``[{xi(x, y): inf, eta(x, y): inf}]``, corresponding to the + output of the function infinitesimals. It returns a list + of values of the form ``[(True/False, sol)]`` where ``sol`` is the value + obtained after substituting the infinitesimals in the PDE. If it + is ``True``, then ``sol`` would be 0. + + """ + if isinstance(eq, Equality): + eq = eq.lhs - eq.rhs + if not func: + eq, func = _preprocess(eq) + variables = func.args + if len(variables) != 1: + raise ValueError("ODE's have only one independent variable") + else: + x = variables[0] + if not order: + order = ode_order(eq, func) + if order != 1: + raise NotImplementedError("Lie groups solver has been implemented " + "only for first order differential equations") + else: + df = func.diff(x) + a = Wild('a', exclude = [df]) + b = Wild('b', exclude = [df]) + match = collect(expand(eq), df).match(a*df + b) + + if match: + h = -simplify(match[b]/match[a]) + else: + try: + sol = solve(eq, df) + except NotImplementedError: + raise NotImplementedError("Infinitesimals for the " + "first order ODE could not be found") + else: + h = sol[0] # Find infinitesimals for one solution + + y = Dummy('y') + h = h.subs(func, y) + xi = Function('xi')(x, y) + eta = Function('eta')(x, y) + dxi = Function('xi')(x, func) + deta = Function('eta')(x, func) + pde = (eta.diff(x) + (eta.diff(y) - xi.diff(x))*h - + (xi.diff(y))*h**2 - xi*(h.diff(x)) - eta*(h.diff(y))) + soltup = [] + for sol in infinitesimals: + tsol = {xi: S(sol[dxi]).subs(func, y), + eta: S(sol[deta]).subs(func, y)} + sol = simplify(pde.subs(tsol).doit()) + if sol: + soltup.append((False, sol.subs(y, func))) + else: + soltup.append((True, 0)) + return soltup + + +def sysode_linear_2eq_order1(match_): + x = match_['func'][0].func + y = match_['func'][1].func + func = match_['func'] + fc = match_['func_coeff'] + eq = match_['eq'] + r = {} + t = list(list(eq[0].atoms(Derivative))[0].atoms(Symbol))[0] + for i in range(2): + eq[i] = Add(*[terms/fc[i,func[i],1] for terms in Add.make_args(eq[i])]) + + # for equations Eq(a1*diff(x(t),t), a*x(t) + b*y(t) + k1) + # and Eq(a2*diff(x(t),t), c*x(t) + d*y(t) + k2) + r['a'] = -fc[0,x(t),0]/fc[0,x(t),1] + r['c'] = -fc[1,x(t),0]/fc[1,y(t),1] + r['b'] = -fc[0,y(t),0]/fc[0,x(t),1] + r['d'] = -fc[1,y(t),0]/fc[1,y(t),1] + forcing = [S.Zero,S.Zero] + for i in range(2): + for j in Add.make_args(eq[i]): + if not j.has(x(t), y(t)): + forcing[i] += j + if not (forcing[0].has(t) or forcing[1].has(t)): + r['k1'] = forcing[0] + r['k2'] = forcing[1] + else: + raise NotImplementedError("Only homogeneous problems are supported" + + " (and constant inhomogeneity)") + + if match_['type_of_equation'] == 'type6': + sol = _linear_2eq_order1_type6(x, y, t, r, eq) + if match_['type_of_equation'] == 'type7': + sol = _linear_2eq_order1_type7(x, y, t, r, eq) + return sol + +def _linear_2eq_order1_type6(x, y, t, r, eq): + r""" + The equations of this type of ode are . + + .. math:: x' = f(t) x + g(t) y + + .. math:: y' = a [f(t) + a h(t)] x + a [g(t) - h(t)] y + + This is solved by first multiplying the first equation by `-a` and adding + it to the second equation to obtain + + .. math:: y' - a x' = -a h(t) (y - a x) + + Setting `U = y - ax` and integrating the equation we arrive at + + .. math:: y - ax = C_1 e^{-a \int h(t) \,dt} + + and on substituting the value of y in first equation give rise to first order ODEs. After solving for + `x`, we can obtain `y` by substituting the value of `x` in second equation. + + """ + C1, C2, C3, C4 = get_numbered_constants(eq, num=4) + p = 0 + q = 0 + p1 = cancel(r['c']/cancel(r['c']/r['d']).as_numer_denom()[0]) + p2 = cancel(r['a']/cancel(r['a']/r['b']).as_numer_denom()[0]) + for n, i in enumerate([p1, p2]): + for j in Mul.make_args(collect_const(i)): + if not j.has(t): + q = j + if q!=0 and n==0: + if ((r['c']/j - r['a'])/(r['b'] - r['d']/j)) == j: + p = 1 + s = j + break + if q!=0 and n==1: + if ((r['a']/j - r['c'])/(r['d'] - r['b']/j)) == j: + p = 2 + s = j + break + + if p == 1: + equ = diff(x(t),t) - r['a']*x(t) - r['b']*(s*x(t) + C1*exp(-s*Integral(r['b'] - r['d']/s, t))) + hint1 = classify_ode(equ)[1] + sol1 = dsolve(equ, hint=hint1+'_Integral').rhs + sol2 = s*sol1 + C1*exp(-s*Integral(r['b'] - r['d']/s, t)) + elif p ==2: + equ = diff(y(t),t) - r['c']*y(t) - r['d']*s*y(t) + C1*exp(-s*Integral(r['d'] - r['b']/s, t)) + hint1 = classify_ode(equ)[1] + sol2 = dsolve(equ, hint=hint1+'_Integral').rhs + sol1 = s*sol2 + C1*exp(-s*Integral(r['d'] - r['b']/s, t)) + return [Eq(x(t), sol1), Eq(y(t), sol2)] + +def _linear_2eq_order1_type7(x, y, t, r, eq): + r""" + The equations of this type of ode are . + + .. math:: x' = f(t) x + g(t) y + + .. math:: y' = h(t) x + p(t) y + + Differentiating the first equation and substituting the value of `y` + from second equation will give a second-order linear equation + + .. math:: g x'' - (fg + gp + g') x' + (fgp - g^{2} h + f g' - f' g) x = 0 + + This above equation can be easily integrated if following conditions are satisfied. + + 1. `fgp - g^{2} h + f g' - f' g = 0` + + 2. `fgp - g^{2} h + f g' - f' g = ag, fg + gp + g' = bg` + + If first condition is satisfied then it is solved by current dsolve solver and in second case it becomes + a constant coefficient differential equation which is also solved by current solver. + + Otherwise if the above condition fails then, + a particular solution is assumed as `x = x_0(t)` and `y = y_0(t)` + Then the general solution is expressed as + + .. math:: x = C_1 x_0(t) + C_2 x_0(t) \int \frac{g(t) F(t) P(t)}{x_0^{2}(t)} \,dt + + .. math:: y = C_1 y_0(t) + C_2 [\frac{F(t) P(t)}{x_0(t)} + y_0(t) \int \frac{g(t) F(t) P(t)}{x_0^{2}(t)} \,dt] + + where C1 and C2 are arbitrary constants and + + .. math:: F(t) = e^{\int f(t) \,dt}, P(t) = e^{\int p(t) \,dt} + + """ + C1, C2, C3, C4 = get_numbered_constants(eq, num=4) + e1 = r['a']*r['b']*r['c'] - r['b']**2*r['c'] + r['a']*diff(r['b'],t) - diff(r['a'],t)*r['b'] + e2 = r['a']*r['c']*r['d'] - r['b']*r['c']**2 + diff(r['c'],t)*r['d'] - r['c']*diff(r['d'],t) + m1 = r['a']*r['b'] + r['b']*r['d'] + diff(r['b'],t) + m2 = r['a']*r['c'] + r['c']*r['d'] + diff(r['c'],t) + if e1 == 0: + sol1 = dsolve(r['b']*diff(x(t),t,t) - m1*diff(x(t),t)).rhs + sol2 = dsolve(diff(y(t),t) - r['c']*sol1 - r['d']*y(t)).rhs + elif e2 == 0: + sol2 = dsolve(r['c']*diff(y(t),t,t) - m2*diff(y(t),t)).rhs + sol1 = dsolve(diff(x(t),t) - r['a']*x(t) - r['b']*sol2).rhs + elif not (e1/r['b']).has(t) and not (m1/r['b']).has(t): + sol1 = dsolve(diff(x(t),t,t) - (m1/r['b'])*diff(x(t),t) - (e1/r['b'])*x(t)).rhs + sol2 = dsolve(diff(y(t),t) - r['c']*sol1 - r['d']*y(t)).rhs + elif not (e2/r['c']).has(t) and not (m2/r['c']).has(t): + sol2 = dsolve(diff(y(t),t,t) - (m2/r['c'])*diff(y(t),t) - (e2/r['c'])*y(t)).rhs + sol1 = dsolve(diff(x(t),t) - r['a']*x(t) - r['b']*sol2).rhs + else: + x0 = Function('x0')(t) # x0 and y0 being particular solutions + y0 = Function('y0')(t) + F = exp(Integral(r['a'],t)) + P = exp(Integral(r['d'],t)) + sol1 = C1*x0 + C2*x0*Integral(r['b']*F*P/x0**2, t) + sol2 = C1*y0 + C2*(F*P/x0 + y0*Integral(r['b']*F*P/x0**2, t)) + return [Eq(x(t), sol1), Eq(y(t), sol2)] + + +def sysode_nonlinear_2eq_order1(match_): + func = match_['func'] + eq = match_['eq'] + fc = match_['func_coeff'] + t = list(list(eq[0].atoms(Derivative))[0].atoms(Symbol))[0] + if match_['type_of_equation'] == 'type5': + sol = _nonlinear_2eq_order1_type5(func, t, eq) + return sol + x = func[0].func + y = func[1].func + for i in range(2): + eqs = 0 + for terms in Add.make_args(eq[i]): + eqs += terms/fc[i,func[i],1] + eq[i] = eqs + if match_['type_of_equation'] == 'type1': + sol = _nonlinear_2eq_order1_type1(x, y, t, eq) + elif match_['type_of_equation'] == 'type2': + sol = _nonlinear_2eq_order1_type2(x, y, t, eq) + elif match_['type_of_equation'] == 'type3': + sol = _nonlinear_2eq_order1_type3(x, y, t, eq) + elif match_['type_of_equation'] == 'type4': + sol = _nonlinear_2eq_order1_type4(x, y, t, eq) + return sol + + +def _nonlinear_2eq_order1_type1(x, y, t, eq): + r""" + Equations: + + .. math:: x' = x^n F(x,y) + + .. math:: y' = g(y) F(x,y) + + Solution: + + .. math:: x = \varphi(y), \int \frac{1}{g(y) F(\varphi(y),y)} \,dy = t + C_2 + + where + + if `n \neq 1` + + .. math:: \varphi = [C_1 + (1-n) \int \frac{1}{g(y)} \,dy]^{\frac{1}{1-n}} + + if `n = 1` + + .. math:: \varphi = C_1 e^{\int \frac{1}{g(y)} \,dy} + + where `C_1` and `C_2` are arbitrary constants. + + """ + C1, C2 = get_numbered_constants(eq, num=2) + n = Wild('n', exclude=[x(t),y(t)]) + f = Wild('f') + u, v = symbols('u, v') + r = eq[0].match(diff(x(t),t) - x(t)**n*f) + g = ((diff(y(t),t) - eq[1])/r[f]).subs(y(t),v) + F = r[f].subs(x(t),u).subs(y(t),v) + n = r[n] + if n!=1: + phi = (C1 + (1-n)*Integral(1/g, v))**(1/(1-n)) + else: + phi = C1*exp(Integral(1/g, v)) + phi = phi.doit() + sol2 = solve(Integral(1/(g*F.subs(u,phi)), v).doit() - t - C2, v) + sol = [] + for sols in sol2: + sol.append(Eq(x(t),phi.subs(v, sols))) + sol.append(Eq(y(t), sols)) + return sol + +def _nonlinear_2eq_order1_type2(x, y, t, eq): + r""" + Equations: + + .. math:: x' = e^{\lambda x} F(x,y) + + .. math:: y' = g(y) F(x,y) + + Solution: + + .. math:: x = \varphi(y), \int \frac{1}{g(y) F(\varphi(y),y)} \,dy = t + C_2 + + where + + if `\lambda \neq 0` + + .. math:: \varphi = -\frac{1}{\lambda} log(C_1 - \lambda \int \frac{1}{g(y)} \,dy) + + if `\lambda = 0` + + .. math:: \varphi = C_1 + \int \frac{1}{g(y)} \,dy + + where `C_1` and `C_2` are arbitrary constants. + + """ + C1, C2 = get_numbered_constants(eq, num=2) + n = Wild('n', exclude=[x(t),y(t)]) + f = Wild('f') + u, v = symbols('u, v') + r = eq[0].match(diff(x(t),t) - exp(n*x(t))*f) + g = ((diff(y(t),t) - eq[1])/r[f]).subs(y(t),v) + F = r[f].subs(x(t),u).subs(y(t),v) + n = r[n] + if n: + phi = -1/n*log(C1 - n*Integral(1/g, v)) + else: + phi = C1 + Integral(1/g, v) + phi = phi.doit() + sol2 = solve(Integral(1/(g*F.subs(u,phi)), v).doit() - t - C2, v) + sol = [] + for sols in sol2: + sol.append(Eq(x(t),phi.subs(v, sols))) + sol.append(Eq(y(t), sols)) + return sol + +def _nonlinear_2eq_order1_type3(x, y, t, eq): + r""" + Autonomous system of general form + + .. math:: x' = F(x,y) + + .. math:: y' = G(x,y) + + Assuming `y = y(x, C_1)` where `C_1` is an arbitrary constant is the general + solution of the first-order equation + + .. math:: F(x,y) y'_x = G(x,y) + + Then the general solution of the original system of equations has the form + + .. math:: \int \frac{1}{F(x,y(x,C_1))} \,dx = t + C_1 + + """ + C1, C2, C3, C4 = get_numbered_constants(eq, num=4) + v = Function('v') + u = Symbol('u') + f = Wild('f') + g = Wild('g') + r1 = eq[0].match(diff(x(t),t) - f) + r2 = eq[1].match(diff(y(t),t) - g) + F = r1[f].subs(x(t), u).subs(y(t), v(u)) + G = r2[g].subs(x(t), u).subs(y(t), v(u)) + sol2r = dsolve(Eq(diff(v(u), u), G/F)) + if isinstance(sol2r, Equality): + sol2r = [sol2r] + for sol2s in sol2r: + sol1 = solve(Integral(1/F.subs(v(u), sol2s.rhs), u).doit() - t - C2, u) + sol = [] + for sols in sol1: + sol.append(Eq(x(t), sols)) + sol.append(Eq(y(t), (sol2s.rhs).subs(u, sols))) + return sol + +def _nonlinear_2eq_order1_type4(x, y, t, eq): + r""" + Equation: + + .. math:: x' = f_1(x) g_1(y) \phi(x,y,t) + + .. math:: y' = f_2(x) g_2(y) \phi(x,y,t) + + First integral: + + .. math:: \int \frac{f_2(x)}{f_1(x)} \,dx - \int \frac{g_1(y)}{g_2(y)} \,dy = C + + where `C` is an arbitrary constant. + + On solving the first integral for `x` (resp., `y` ) and on substituting the + resulting expression into either equation of the original solution, one + arrives at a first-order equation for determining `y` (resp., `x` ). + + """ + C1, C2 = get_numbered_constants(eq, num=2) + u, v = symbols('u, v') + U, V = symbols('U, V', cls=Function) + f = Wild('f') + g = Wild('g') + f1 = Wild('f1', exclude=[v,t]) + f2 = Wild('f2', exclude=[v,t]) + g1 = Wild('g1', exclude=[u,t]) + g2 = Wild('g2', exclude=[u,t]) + r1 = eq[0].match(diff(x(t),t) - f) + r2 = eq[1].match(diff(y(t),t) - g) + num, den = ( + (r1[f].subs(x(t),u).subs(y(t),v))/ + (r2[g].subs(x(t),u).subs(y(t),v))).as_numer_denom() + R1 = num.match(f1*g1) + R2 = den.match(f2*g2) + phi = (r1[f].subs(x(t),u).subs(y(t),v))/num + F1 = R1[f1]; F2 = R2[f2] + G1 = R1[g1]; G2 = R2[g2] + sol1r = solve(Integral(F2/F1, u).doit() - Integral(G1/G2,v).doit() - C1, u) + sol2r = solve(Integral(F2/F1, u).doit() - Integral(G1/G2,v).doit() - C1, v) + sol = [] + for sols in sol1r: + sol.append(Eq(y(t), dsolve(diff(V(t),t) - F2.subs(u,sols).subs(v,V(t))*G2.subs(v,V(t))*phi.subs(u,sols).subs(v,V(t))).rhs)) + for sols in sol2r: + sol.append(Eq(x(t), dsolve(diff(U(t),t) - F1.subs(u,U(t))*G1.subs(v,sols).subs(u,U(t))*phi.subs(v,sols).subs(u,U(t))).rhs)) + return set(sol) + +def _nonlinear_2eq_order1_type5(func, t, eq): + r""" + Clairaut system of ODEs + + .. math:: x = t x' + F(x',y') + + .. math:: y = t y' + G(x',y') + + The following are solutions of the system + + `(i)` straight lines: + + .. math:: x = C_1 t + F(C_1, C_2), y = C_2 t + G(C_1, C_2) + + where `C_1` and `C_2` are arbitrary constants; + + `(ii)` envelopes of the above lines; + + `(iii)` continuously differentiable lines made up from segments of the lines + `(i)` and `(ii)`. + + """ + C1, C2 = get_numbered_constants(eq, num=2) + f = Wild('f') + g = Wild('g') + def check_type(x, y): + r1 = eq[0].match(t*diff(x(t),t) - x(t) + f) + r2 = eq[1].match(t*diff(y(t),t) - y(t) + g) + if not (r1 and r2): + r1 = eq[0].match(diff(x(t),t) - x(t)/t + f/t) + r2 = eq[1].match(diff(y(t),t) - y(t)/t + g/t) + if not (r1 and r2): + r1 = (-eq[0]).match(t*diff(x(t),t) - x(t) + f) + r2 = (-eq[1]).match(t*diff(y(t),t) - y(t) + g) + if not (r1 and r2): + r1 = (-eq[0]).match(diff(x(t),t) - x(t)/t + f/t) + r2 = (-eq[1]).match(diff(y(t),t) - y(t)/t + g/t) + return [r1, r2] + for func_ in func: + if isinstance(func_, list): + x = func[0][0].func + y = func[0][1].func + [r1, r2] = check_type(x, y) + if not (r1 and r2): + [r1, r2] = check_type(y, x) + x, y = y, x + x1 = diff(x(t),t); y1 = diff(y(t),t) + return {Eq(x(t), C1*t + r1[f].subs(x1,C1).subs(y1,C2)), Eq(y(t), C2*t + r2[g].subs(x1,C1).subs(y1,C2))} + +def sysode_nonlinear_3eq_order1(match_): + x = match_['func'][0].func + y = match_['func'][1].func + z = match_['func'][2].func + eq = match_['eq'] + t = list(list(eq[0].atoms(Derivative))[0].atoms(Symbol))[0] + if match_['type_of_equation'] == 'type1': + sol = _nonlinear_3eq_order1_type1(x, y, z, t, eq) + if match_['type_of_equation'] == 'type2': + sol = _nonlinear_3eq_order1_type2(x, y, z, t, eq) + if match_['type_of_equation'] == 'type3': + sol = _nonlinear_3eq_order1_type3(x, y, z, t, eq) + if match_['type_of_equation'] == 'type4': + sol = _nonlinear_3eq_order1_type4(x, y, z, t, eq) + if match_['type_of_equation'] == 'type5': + sol = _nonlinear_3eq_order1_type5(x, y, z, t, eq) + return sol + +def _nonlinear_3eq_order1_type1(x, y, z, t, eq): + r""" + Equations: + + .. math:: a x' = (b - c) y z, \enspace b y' = (c - a) z x, \enspace c z' = (a - b) x y + + First Integrals: + + .. math:: a x^{2} + b y^{2} + c z^{2} = C_1 + + .. math:: a^{2} x^{2} + b^{2} y^{2} + c^{2} z^{2} = C_2 + + where `C_1` and `C_2` are arbitrary constants. On solving the integrals for `y` and + `z` and on substituting the resulting expressions into the first equation of the + system, we arrives at a separable first-order equation on `x`. Similarly doing that + for other two equations, we will arrive at first order equation on `y` and `z` too. + + References + ========== + -https://eqworld.ipmnet.ru/en/solutions/sysode/sode0401.pdf + + """ + C1, C2 = get_numbered_constants(eq, num=2) + u, v, w = symbols('u, v, w') + p = Wild('p', exclude=[x(t), y(t), z(t), t]) + q = Wild('q', exclude=[x(t), y(t), z(t), t]) + s = Wild('s', exclude=[x(t), y(t), z(t), t]) + r = (diff(x(t),t) - eq[0]).match(p*y(t)*z(t)) + r.update((diff(y(t),t) - eq[1]).match(q*z(t)*x(t))) + r.update((diff(z(t),t) - eq[2]).match(s*x(t)*y(t))) + n1, d1 = r[p].as_numer_denom() + n2, d2 = r[q].as_numer_denom() + n3, d3 = r[s].as_numer_denom() + val = solve([n1*u-d1*v+d1*w, d2*u+n2*v-d2*w, d3*u-d3*v-n3*w],[u,v]) + vals = [val[v], val[u]] + c = lcm(vals[0].as_numer_denom()[1], vals[1].as_numer_denom()[1]) + b = vals[0].subs(w, c) + a = vals[1].subs(w, c) + y_x = sqrt(((c*C1-C2) - a*(c-a)*x(t)**2)/(b*(c-b))) + z_x = sqrt(((b*C1-C2) - a*(b-a)*x(t)**2)/(c*(b-c))) + z_y = sqrt(((a*C1-C2) - b*(a-b)*y(t)**2)/(c*(a-c))) + x_y = sqrt(((c*C1-C2) - b*(c-b)*y(t)**2)/(a*(c-a))) + x_z = sqrt(((b*C1-C2) - c*(b-c)*z(t)**2)/(a*(b-a))) + y_z = sqrt(((a*C1-C2) - c*(a-c)*z(t)**2)/(b*(a-b))) + sol1 = dsolve(a*diff(x(t),t) - (b-c)*y_x*z_x) + sol2 = dsolve(b*diff(y(t),t) - (c-a)*z_y*x_y) + sol3 = dsolve(c*diff(z(t),t) - (a-b)*x_z*y_z) + return [sol1, sol2, sol3] + + +def _nonlinear_3eq_order1_type2(x, y, z, t, eq): + r""" + Equations: + + .. math:: a x' = (b - c) y z f(x, y, z, t) + + .. math:: b y' = (c - a) z x f(x, y, z, t) + + .. math:: c z' = (a - b) x y f(x, y, z, t) + + First Integrals: + + .. math:: a x^{2} + b y^{2} + c z^{2} = C_1 + + .. math:: a^{2} x^{2} + b^{2} y^{2} + c^{2} z^{2} = C_2 + + where `C_1` and `C_2` are arbitrary constants. On solving the integrals for `y` and + `z` and on substituting the resulting expressions into the first equation of the + system, we arrives at a first-order differential equations on `x`. Similarly doing + that for other two equations we will arrive at first order equation on `y` and `z`. + + References + ========== + -https://eqworld.ipmnet.ru/en/solutions/sysode/sode0402.pdf + + """ + C1, C2 = get_numbered_constants(eq, num=2) + u, v, w = symbols('u, v, w') + p = Wild('p', exclude=[x(t), y(t), z(t), t]) + q = Wild('q', exclude=[x(t), y(t), z(t), t]) + s = Wild('s', exclude=[x(t), y(t), z(t), t]) + f = Wild('f') + r1 = (diff(x(t),t) - eq[0]).match(y(t)*z(t)*f) + r = collect_const(r1[f]).match(p*f) + r.update(((diff(y(t),t) - eq[1])/r[f]).match(q*z(t)*x(t))) + r.update(((diff(z(t),t) - eq[2])/r[f]).match(s*x(t)*y(t))) + n1, d1 = r[p].as_numer_denom() + n2, d2 = r[q].as_numer_denom() + n3, d3 = r[s].as_numer_denom() + val = solve([n1*u-d1*v+d1*w, d2*u+n2*v-d2*w, -d3*u+d3*v+n3*w],[u,v]) + vals = [val[v], val[u]] + c = lcm(vals[0].as_numer_denom()[1], vals[1].as_numer_denom()[1]) + a = vals[0].subs(w, c) + b = vals[1].subs(w, c) + y_x = sqrt(((c*C1-C2) - a*(c-a)*x(t)**2)/(b*(c-b))) + z_x = sqrt(((b*C1-C2) - a*(b-a)*x(t)**2)/(c*(b-c))) + z_y = sqrt(((a*C1-C2) - b*(a-b)*y(t)**2)/(c*(a-c))) + x_y = sqrt(((c*C1-C2) - b*(c-b)*y(t)**2)/(a*(c-a))) + x_z = sqrt(((b*C1-C2) - c*(b-c)*z(t)**2)/(a*(b-a))) + y_z = sqrt(((a*C1-C2) - c*(a-c)*z(t)**2)/(b*(a-b))) + sol1 = dsolve(a*diff(x(t),t) - (b-c)*y_x*z_x*r[f]) + sol2 = dsolve(b*diff(y(t),t) - (c-a)*z_y*x_y*r[f]) + sol3 = dsolve(c*diff(z(t),t) - (a-b)*x_z*y_z*r[f]) + return [sol1, sol2, sol3] + +def _nonlinear_3eq_order1_type3(x, y, z, t, eq): + r""" + Equations: + + .. math:: x' = c F_2 - b F_3, \enspace y' = a F_3 - c F_1, \enspace z' = b F_1 - a F_2 + + where `F_n = F_n(x, y, z, t)`. + + 1. First Integral: + + .. math:: a x + b y + c z = C_1, + + where C is an arbitrary constant. + + 2. If we assume function `F_n` to be independent of `t`,i.e, `F_n` = `F_n (x, y, z)` + Then, on eliminating `t` and `z` from the first two equation of the system, one + arrives at the first-order equation + + .. math:: \frac{dy}{dx} = \frac{a F_3 (x, y, z) - c F_1 (x, y, z)}{c F_2 (x, y, z) - + b F_3 (x, y, z)} + + where `z = \frac{1}{c} (C_1 - a x - b y)` + + References + ========== + -https://eqworld.ipmnet.ru/en/solutions/sysode/sode0404.pdf + + """ + C1 = get_numbered_constants(eq, num=1) + u, v, w = symbols('u, v, w') + fu, fv, fw = symbols('u, v, w', cls=Function) + p = Wild('p', exclude=[x(t), y(t), z(t), t]) + q = Wild('q', exclude=[x(t), y(t), z(t), t]) + s = Wild('s', exclude=[x(t), y(t), z(t), t]) + F1, F2, F3 = symbols('F1, F2, F3', cls=Wild) + r1 = (diff(x(t), t) - eq[0]).match(F2-F3) + r = collect_const(r1[F2]).match(s*F2) + r.update(collect_const(r1[F3]).match(q*F3)) + if eq[1].has(r[F2]) and not eq[1].has(r[F3]): + r[F2], r[F3] = r[F3], r[F2] + r[s], r[q] = -r[q], -r[s] + r.update((diff(y(t), t) - eq[1]).match(p*r[F3] - r[s]*F1)) + a = r[p]; b = r[q]; c = r[s] + F1 = r[F1].subs(x(t), u).subs(y(t),v).subs(z(t), w) + F2 = r[F2].subs(x(t), u).subs(y(t),v).subs(z(t), w) + F3 = r[F3].subs(x(t), u).subs(y(t),v).subs(z(t), w) + z_xy = (C1-a*u-b*v)/c + y_zx = (C1-a*u-c*w)/b + x_yz = (C1-b*v-c*w)/a + y_x = dsolve(diff(fv(u),u) - ((a*F3-c*F1)/(c*F2-b*F3)).subs(w,z_xy).subs(v,fv(u))).rhs + z_x = dsolve(diff(fw(u),u) - ((b*F1-a*F2)/(c*F2-b*F3)).subs(v,y_zx).subs(w,fw(u))).rhs + z_y = dsolve(diff(fw(v),v) - ((b*F1-a*F2)/(a*F3-c*F1)).subs(u,x_yz).subs(w,fw(v))).rhs + x_y = dsolve(diff(fu(v),v) - ((c*F2-b*F3)/(a*F3-c*F1)).subs(w,z_xy).subs(u,fu(v))).rhs + y_z = dsolve(diff(fv(w),w) - ((a*F3-c*F1)/(b*F1-a*F2)).subs(u,x_yz).subs(v,fv(w))).rhs + x_z = dsolve(diff(fu(w),w) - ((c*F2-b*F3)/(b*F1-a*F2)).subs(v,y_zx).subs(u,fu(w))).rhs + sol1 = dsolve(diff(fu(t),t) - (c*F2 - b*F3).subs(v,y_x).subs(w,z_x).subs(u,fu(t))).rhs + sol2 = dsolve(diff(fv(t),t) - (a*F3 - c*F1).subs(u,x_y).subs(w,z_y).subs(v,fv(t))).rhs + sol3 = dsolve(diff(fw(t),t) - (b*F1 - a*F2).subs(u,x_z).subs(v,y_z).subs(w,fw(t))).rhs + return [sol1, sol2, sol3] + +def _nonlinear_3eq_order1_type4(x, y, z, t, eq): + r""" + Equations: + + .. math:: x' = c z F_2 - b y F_3, \enspace y' = a x F_3 - c z F_1, \enspace z' = b y F_1 - a x F_2 + + where `F_n = F_n (x, y, z, t)` + + 1. First integral: + + .. math:: a x^{2} + b y^{2} + c z^{2} = C_1 + + where `C` is an arbitrary constant. + + 2. Assuming the function `F_n` is independent of `t`: `F_n = F_n (x, y, z)`. Then on + eliminating `t` and `z` from the first two equations of the system, one arrives at + the first-order equation + + .. math:: \frac{dy}{dx} = \frac{a x F_3 (x, y, z) - c z F_1 (x, y, z)} + {c z F_2 (x, y, z) - b y F_3 (x, y, z)} + + where `z = \pm \sqrt{\frac{1}{c} (C_1 - a x^{2} - b y^{2})}` + + References + ========== + -https://eqworld.ipmnet.ru/en/solutions/sysode/sode0405.pdf + + """ + C1 = get_numbered_constants(eq, num=1) + u, v, w = symbols('u, v, w') + p = Wild('p', exclude=[x(t), y(t), z(t), t]) + q = Wild('q', exclude=[x(t), y(t), z(t), t]) + s = Wild('s', exclude=[x(t), y(t), z(t), t]) + F1, F2, F3 = symbols('F1, F2, F3', cls=Wild) + r1 = eq[0].match(diff(x(t),t) - z(t)*F2 + y(t)*F3) + r = collect_const(r1[F2]).match(s*F2) + r.update(collect_const(r1[F3]).match(q*F3)) + if eq[1].has(r[F2]) and not eq[1].has(r[F3]): + r[F2], r[F3] = r[F3], r[F2] + r[s], r[q] = -r[q], -r[s] + r.update((diff(y(t),t) - eq[1]).match(p*x(t)*r[F3] - r[s]*z(t)*F1)) + a = r[p]; b = r[q]; c = r[s] + F1 = r[F1].subs(x(t),u).subs(y(t),v).subs(z(t),w) + F2 = r[F2].subs(x(t),u).subs(y(t),v).subs(z(t),w) + F3 = r[F3].subs(x(t),u).subs(y(t),v).subs(z(t),w) + x_yz = sqrt((C1 - b*v**2 - c*w**2)/a) + y_zx = sqrt((C1 - c*w**2 - a*u**2)/b) + z_xy = sqrt((C1 - a*u**2 - b*v**2)/c) + y_x = dsolve(diff(v(u),u) - ((a*u*F3-c*w*F1)/(c*w*F2-b*v*F3)).subs(w,z_xy).subs(v,v(u))).rhs + z_x = dsolve(diff(w(u),u) - ((b*v*F1-a*u*F2)/(c*w*F2-b*v*F3)).subs(v,y_zx).subs(w,w(u))).rhs + z_y = dsolve(diff(w(v),v) - ((b*v*F1-a*u*F2)/(a*u*F3-c*w*F1)).subs(u,x_yz).subs(w,w(v))).rhs + x_y = dsolve(diff(u(v),v) - ((c*w*F2-b*v*F3)/(a*u*F3-c*w*F1)).subs(w,z_xy).subs(u,u(v))).rhs + y_z = dsolve(diff(v(w),w) - ((a*u*F3-c*w*F1)/(b*v*F1-a*u*F2)).subs(u,x_yz).subs(v,v(w))).rhs + x_z = dsolve(diff(u(w),w) - ((c*w*F2-b*v*F3)/(b*v*F1-a*u*F2)).subs(v,y_zx).subs(u,u(w))).rhs + sol1 = dsolve(diff(u(t),t) - (c*w*F2 - b*v*F3).subs(v,y_x).subs(w,z_x).subs(u,u(t))).rhs + sol2 = dsolve(diff(v(t),t) - (a*u*F3 - c*w*F1).subs(u,x_y).subs(w,z_y).subs(v,v(t))).rhs + sol3 = dsolve(diff(w(t),t) - (b*v*F1 - a*u*F2).subs(u,x_z).subs(v,y_z).subs(w,w(t))).rhs + return [sol1, sol2, sol3] + +def _nonlinear_3eq_order1_type5(x, y, z, t, eq): + r""" + .. math:: x' = x (c F_2 - b F_3), \enspace y' = y (a F_3 - c F_1), \enspace z' = z (b F_1 - a F_2) + + where `F_n = F_n (x, y, z, t)` and are arbitrary functions. + + First Integral: + + .. math:: \left|x\right|^{a} \left|y\right|^{b} \left|z\right|^{c} = C_1 + + where `C` is an arbitrary constant. If the function `F_n` is independent of `t`, + then, by eliminating `t` and `z` from the first two equations of the system, one + arrives at a first-order equation. + + References + ========== + -https://eqworld.ipmnet.ru/en/solutions/sysode/sode0406.pdf + + """ + C1 = get_numbered_constants(eq, num=1) + u, v, w = symbols('u, v, w') + fu, fv, fw = symbols('u, v, w', cls=Function) + p = Wild('p', exclude=[x(t), y(t), z(t), t]) + q = Wild('q', exclude=[x(t), y(t), z(t), t]) + s = Wild('s', exclude=[x(t), y(t), z(t), t]) + F1, F2, F3 = symbols('F1, F2, F3', cls=Wild) + r1 = eq[0].match(diff(x(t), t) - x(t)*F2 + x(t)*F3) + r = collect_const(r1[F2]).match(s*F2) + r.update(collect_const(r1[F3]).match(q*F3)) + if eq[1].has(r[F2]) and not eq[1].has(r[F3]): + r[F2], r[F3] = r[F3], r[F2] + r[s], r[q] = -r[q], -r[s] + r.update((diff(y(t), t) - eq[1]).match(y(t)*(p*r[F3] - r[s]*F1))) + a = r[p]; b = r[q]; c = r[s] + F1 = r[F1].subs(x(t), u).subs(y(t), v).subs(z(t), w) + F2 = r[F2].subs(x(t), u).subs(y(t), v).subs(z(t), w) + F3 = r[F3].subs(x(t), u).subs(y(t), v).subs(z(t), w) + x_yz = (C1*v**-b*w**-c)**-a + y_zx = (C1*w**-c*u**-a)**-b + z_xy = (C1*u**-a*v**-b)**-c + y_x = dsolve(diff(fv(u), u) - ((v*(a*F3 - c*F1))/(u*(c*F2 - b*F3))).subs(w, z_xy).subs(v, fv(u))).rhs + z_x = dsolve(diff(fw(u), u) - ((w*(b*F1 - a*F2))/(u*(c*F2 - b*F3))).subs(v, y_zx).subs(w, fw(u))).rhs + z_y = dsolve(diff(fw(v), v) - ((w*(b*F1 - a*F2))/(v*(a*F3 - c*F1))).subs(u, x_yz).subs(w, fw(v))).rhs + x_y = dsolve(diff(fu(v), v) - ((u*(c*F2 - b*F3))/(v*(a*F3 - c*F1))).subs(w, z_xy).subs(u, fu(v))).rhs + y_z = dsolve(diff(fv(w), w) - ((v*(a*F3 - c*F1))/(w*(b*F1 - a*F2))).subs(u, x_yz).subs(v, fv(w))).rhs + x_z = dsolve(diff(fu(w), w) - ((u*(c*F2 - b*F3))/(w*(b*F1 - a*F2))).subs(v, y_zx).subs(u, fu(w))).rhs + sol1 = dsolve(diff(fu(t), t) - (u*(c*F2 - b*F3)).subs(v, y_x).subs(w, z_x).subs(u, fu(t))).rhs + sol2 = dsolve(diff(fv(t), t) - (v*(a*F3 - c*F1)).subs(u, x_y).subs(w, z_y).subs(v, fv(t))).rhs + sol3 = dsolve(diff(fw(t), t) - (w*(b*F1 - a*F2)).subs(u, x_z).subs(v, y_z).subs(w, fw(t))).rhs + return [sol1, sol2, sol3] + + +#This import is written at the bottom to avoid circular imports. +from .single import SingleODEProblem, SingleODESolver, solver_map diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/riccati.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/riccati.py new file mode 100644 index 0000000000000000000000000000000000000000..2ef66ed0896d39bee8fba1b74a0c93734742fc1f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/riccati.py @@ -0,0 +1,893 @@ +r""" +This module contains :py:meth:`~sympy.solvers.ode.riccati.solve_riccati`, +a function which gives all rational particular solutions to first order +Riccati ODEs. A general first order Riccati ODE is given by - + +.. math:: y' = b_0(x) + b_1(x)w + b_2(x)w^2 + +where `b_0, b_1` and `b_2` can be arbitrary rational functions of `x` +with `b_2 \ne 0`. When `b_2 = 0`, the equation is not a Riccati ODE +anymore and becomes a Linear ODE. Similarly, when `b_0 = 0`, the equation +is a Bernoulli ODE. The algorithm presented below can find rational +solution(s) to all ODEs with `b_2 \ne 0` that have a rational solution, +or prove that no rational solution exists for the equation. + +Background +========== + +A Riccati equation can be transformed to its normal form + +.. math:: y' + y^2 = a(x) + +using the transformation + +.. math:: y = -b_2(x) - \frac{b'_2(x)}{2 b_2(x)} - \frac{b_1(x)}{2} + +where `a(x)` is given by + +.. math:: a(x) = \frac{1}{4}\left(\frac{b_2'}{b_2} + b_1\right)^2 - \frac{1}{2}\left(\frac{b_2'}{b_2} + b_1\right)' - b_0 b_2 + +Thus, we can develop an algorithm to solve for the Riccati equation +in its normal form, which would in turn give us the solution for +the original Riccati equation. + +Algorithm +========= + +The algorithm implemented here is presented in the Ph.D thesis +"Rational and Algebraic Solutions of First-Order Algebraic ODEs" +by N. Thieu Vo. The entire thesis can be found here - +https://www3.risc.jku.at/publications/download/risc_5387/PhDThesisThieu.pdf + +We have only implemented the Rational Riccati solver (Algorithm 11, +Pg 78-82 in Thesis). Before we proceed towards the implementation +of the algorithm, a few definitions to understand are - + +1. Valuation of a Rational Function at `\infty`: + The valuation of a rational function `p(x)` at `\infty` is equal + to the difference between the degree of the denominator and the + numerator of `p(x)`. + + NOTE: A general definition of valuation of a rational function + at any value of `x` can be found in Pg 63 of the thesis, but + is not of any interest for this algorithm. + +2. Zeros and Poles of a Rational Function: + Let `a(x) = \frac{S(x)}{T(x)}, T \ne 0` be a rational function + of `x`. Then - + + a. The Zeros of `a(x)` are the roots of `S(x)`. + b. The Poles of `a(x)` are the roots of `T(x)`. However, `\infty` + can also be a pole of a(x). We say that `a(x)` has a pole at + `\infty` if `a(\frac{1}{x})` has a pole at 0. + +Every pole is associated with an order that is equal to the multiplicity +of its appearance as a root of `T(x)`. A pole is called a simple pole if +it has an order 1. Similarly, a pole is called a multiple pole if it has +an order `\ge` 2. + +Necessary Conditions +==================== + +For a Riccati equation in its normal form, + +.. math:: y' + y^2 = a(x) + +we can define + +a. A pole is called a movable pole if it is a pole of `y(x)` and is not +a pole of `a(x)`. +b. Similarly, a pole is called a non-movable pole if it is a pole of both +`y(x)` and `a(x)`. + +Then, the algorithm states that a rational solution exists only if - + +a. Every pole of `a(x)` must be either a simple pole or a multiple pole +of even order. +b. The valuation of `a(x)` at `\infty` must be even or be `\ge` 2. + +This algorithm finds all possible rational solutions for the Riccati ODE. +If no rational solutions are found, it means that no rational solutions +exist. + +The algorithm works for Riccati ODEs where the coefficients are rational +functions in the independent variable `x` with rational number coefficients +i.e. in `Q(x)`. The coefficients in the rational function cannot be floats, +irrational numbers, symbols or any other kind of expression. The reasons +for this are - + +1. When using symbols, different symbols could take the same value and this +would affect the multiplicity of poles if symbols are present here. + +2. An integer degree bound is required to calculate a polynomial solution +to an auxiliary differential equation, which in turn gives the particular +solution for the original ODE. If symbols/floats/irrational numbers are +present, we cannot determine if the expression for the degree bound is an +integer or not. + +Solution +======== + +With these definitions, we can state a general form for the solution of +the equation. `y(x)` must have the form - + +.. math:: y(x) = \sum_{i=1}^{n} \sum_{j=1}^{r_i} \frac{c_{ij}}{(x - x_i)^j} + \sum_{i=1}^{m} \frac{1}{x - \chi_i} + \sum_{i=0}^{N} d_i x^i + +where `x_1, x_2, \dots, x_n` are non-movable poles of `a(x)`, +`\chi_1, \chi_2, \dots, \chi_m` are movable poles of `a(x)`, and the values +of `N, n, r_1, r_2, \dots, r_n` can be determined from `a(x)`. The +coefficient vectors `(d_0, d_1, \dots, d_N)` and `(c_{i1}, c_{i2}, \dots, c_{i r_i})` +can be determined from `a(x)`. We will have 2 choices each of these vectors +and part of the procedure is figuring out which of the 2 should be used +to get the solution correctly. + +Implementation +============== + +In this implementation, we use ``Poly`` to represent a rational function +rather than using ``Expr`` since ``Poly`` is much faster. Since we cannot +represent rational functions directly using ``Poly``, we instead represent +a rational function with 2 ``Poly`` objects - one for its numerator and +the other for its denominator. + +The code is written to match the steps given in the thesis (Pg 82) + +Step 0 : Match the equation - +Find `b_0, b_1` and `b_2`. If `b_2 = 0` or no such functions exist, raise +an error + +Step 1 : Transform the equation to its normal form as explained in the +theory section. + +Step 2 : Initialize an empty set of solutions, ``sol``. + +Step 3 : If `a(x) = 0`, append `\frac{1}/{(x - C1)}` to ``sol``. + +Step 4 : If `a(x)` is a rational non-zero number, append `\pm \sqrt{a}` +to ``sol``. + +Step 5 : Find the poles and their multiplicities of `a(x)`. Let +the number of poles be `n`. Also find the valuation of `a(x)` at +`\infty` using ``val_at_inf``. + +NOTE: Although the algorithm considers `\infty` as a pole, it is +not mentioned if it a part of the set of finite poles. `\infty` +is NOT a part of the set of finite poles. If a pole exists at +`\infty`, we use its multiplicity to find the laurent series of +`a(x)` about `\infty`. + +Step 6 : Find `n` c-vectors (one for each pole) and 1 d-vector using +``construct_c`` and ``construct_d``. Now, determine all the ``2**(n + 1)`` +combinations of choosing between 2 choices for each of the `n` c-vectors +and 1 d-vector. + +NOTE: The equation for `d_{-1}` in Case 4 (Pg 80) has a printinig +mistake. The term `- d_N` must be replaced with `-N d_N`. The same +has been explained in the code as well. + +For each of these above combinations, do + +Step 8 : Compute `m` in ``compute_m_ybar``. `m` is the degree bound of +the polynomial solution we must find for the auxiliary equation. + +Step 9 : In ``compute_m_ybar``, compute ybar as well where ``ybar`` is +one part of y(x) - + +.. math:: \overline{y}(x) = \sum_{i=1}^{n} \sum_{j=1}^{r_i} \frac{c_{ij}}{(x - x_i)^j} + \sum_{i=0}^{N} d_i x^i + +Step 10 : If `m` is a non-negative integer - + +Step 11: Find a polynomial solution of degree `m` for the auxiliary equation. + +There are 2 cases possible - + + a. `m` is a non-negative integer: We can solve for the coefficients + in `p(x)` using Undetermined Coefficients. + + b. `m` is not a non-negative integer: In this case, we cannot find + a polynomial solution to the auxiliary equation, and hence, we ignore + this value of `m`. + +Step 12 : For each `p(x)` that exists, append `ybar + \frac{p'(x)}{p(x)}` +to ``sol``. + +Step 13 : For each solution in ``sol``, apply an inverse transformation, +so that the solutions of the original equation are found using the +solutions of the equation in its normal form. +""" + + +from itertools import product +from sympy.core import S +from sympy.core.add import Add +from sympy.core.numbers import oo, Float +from sympy.core.function import count_ops +from sympy.core.relational import Eq +from sympy.core.symbol import symbols, Symbol, Dummy +from sympy.functions import sqrt, exp +from sympy.functions.elementary.complexes import sign +from sympy.integrals.integrals import Integral +from sympy.polys.domains import ZZ +from sympy.polys.polytools import Poly +from sympy.polys.polyroots import roots +from sympy.solvers.solveset import linsolve + + +def riccati_normal(w, x, b1, b2): + """ + Given a solution `w(x)` to the equation + + .. math:: w'(x) = b_0(x) + b_1(x)*w(x) + b_2(x)*w(x)^2 + + and rational function coefficients `b_1(x)` and + `b_2(x)`, this function transforms the solution to + give a solution `y(x)` for its corresponding normal + Riccati ODE + + .. math:: y'(x) + y(x)^2 = a(x) + + using the transformation + + .. math:: y(x) = -b_2(x)*w(x) - b'_2(x)/(2*b_2(x)) - b_1(x)/2 + """ + return -b2*w - b2.diff(x)/(2*b2) - b1/2 + + +def riccati_inverse_normal(y, x, b1, b2, bp=None): + """ + Inverse transforming the solution to the normal + Riccati ODE to get the solution to the Riccati ODE. + """ + # bp is the expression which is independent of the solution + # and hence, it need not be computed again + if bp is None: + bp = -b2.diff(x)/(2*b2**2) - b1/(2*b2) + # w(x) = -y(x)/b2(x) - b2'(x)/(2*b2(x)^2) - b1(x)/(2*b2(x)) + return -y/b2 + bp + + +def riccati_reduced(eq, f, x): + """ + Convert a Riccati ODE into its corresponding + normal Riccati ODE. + """ + match, funcs = match_riccati(eq, f, x) + # If equation is not a Riccati ODE, exit + if not match: + return False + # Using the rational functions, find the expression for a(x) + b0, b1, b2 = funcs + a = -b0*b2 + b1**2/4 - b1.diff(x)/2 + 3*b2.diff(x)**2/(4*b2**2) + b1*b2.diff(x)/(2*b2) - \ + b2.diff(x, 2)/(2*b2) + # Normal form of Riccati ODE is f'(x) + f(x)^2 = a(x) + return f(x).diff(x) + f(x)**2 - a + +def linsolve_dict(eq, syms): + """ + Get the output of linsolve as a dict + """ + # Convert tuple type return value of linsolve + # to a dictionary for ease of use + sol = linsolve(eq, syms) + if not sol: + return {} + return dict(zip(syms, list(sol)[0])) + + +def match_riccati(eq, f, x): + """ + A function that matches and returns the coefficients + if an equation is a Riccati ODE + + Parameters + ========== + + eq: Equation to be matched + f: Dependent variable + x: Independent variable + + Returns + ======= + + match: True if equation is a Riccati ODE, False otherwise + funcs: [b0, b1, b2] if match is True, [] otherwise. Here, + b0, b1 and b2 are rational functions which match the equation. + """ + # Group terms based on f(x) + if isinstance(eq, Eq): + eq = eq.lhs - eq.rhs + eq = eq.expand().collect(f(x)) + cf = eq.coeff(f(x).diff(x)) + + # There must be an f(x).diff(x) term. + # eq must be an Add object since we are using the expanded + # equation and it must have atleast 2 terms (b2 != 0) + if cf != 0 and isinstance(eq, Add): + + # Divide all coefficients by the coefficient of f(x).diff(x) + # and add the terms again to get the same equation + eq = Add(*((x/cf).cancel() for x in eq.args)).collect(f(x)) + + # Match the equation with the pattern + b1 = -eq.coeff(f(x)) + b2 = -eq.coeff(f(x)**2) + b0 = (f(x).diff(x) - b1*f(x) - b2*f(x)**2 - eq).expand() + funcs = [b0, b1, b2] + + # Check if coefficients are not symbols and floats + if any(len(x.atoms(Symbol)) > 1 or len(x.atoms(Float)) for x in funcs): + return False, [] + + # If b_0(x) contains f(x), it is not a Riccati ODE + if len(b0.atoms(f)) or not all((b2 != 0, b0.is_rational_function(x), + b1.is_rational_function(x), b2.is_rational_function(x))): + return False, [] + return True, funcs + return False, [] + + +def val_at_inf(num, den, x): + # Valuation of a rational function at oo = deg(denom) - deg(numer) + return den.degree(x) - num.degree(x) + + +def check_necessary_conds(val_inf, muls): + """ + The necessary conditions for a rational solution + to exist are as follows - + + i) Every pole of a(x) must be either a simple pole + or a multiple pole of even order. + + ii) The valuation of a(x) at infinity must be even + or be greater than or equal to 2. + + Here, a simple pole is a pole with multiplicity 1 + and a multiple pole is a pole with multiplicity + greater than 1. + """ + return (val_inf >= 2 or (val_inf <= 0 and val_inf%2 == 0)) and \ + all(mul == 1 or (mul%2 == 0 and mul >= 2) for mul in muls) + + +def inverse_transform_poly(num, den, x): + """ + A function to make the substitution + x -> 1/x in a rational function that + is represented using Poly objects for + numerator and denominator. + """ + # Declare for reuse + one = Poly(1, x) + xpoly = Poly(x, x) + + # Check if degree of numerator is same as denominator + pwr = val_at_inf(num, den, x) + if pwr >= 0: + # Denominator has greater degree. Substituting x with + # 1/x would make the extra power go to the numerator + if num.expr != 0: + num = num.transform(one, xpoly) * x**pwr + den = den.transform(one, xpoly) + else: + # Numerator has greater degree. Substituting x with + # 1/x would make the extra power go to the denominator + num = num.transform(one, xpoly) + den = den.transform(one, xpoly) * x**(-pwr) + return num.cancel(den, include=True) + + +def limit_at_inf(num, den, x): + """ + Find the limit of a rational function + at oo + """ + # pwr = degree(num) - degree(den) + pwr = -val_at_inf(num, den, x) + # Numerator has a greater degree than denominator + # Limit at infinity would depend on the sign of the + # leading coefficients of numerator and denominator + if pwr > 0: + return oo*sign(num.LC()/den.LC()) + # Degree of numerator is equal to that of denominator + # Limit at infinity is just the ratio of leading coeffs + elif pwr == 0: + return num.LC()/den.LC() + # Degree of numerator is less than that of denominator + # Limit at infinity is just 0 + else: + return 0 + + +def construct_c_case_1(num, den, x, pole): + # Find the coefficient of 1/(x - pole)**2 in the + # Laurent series expansion of a(x) about pole. + num1, den1 = (num*Poly((x - pole)**2, x, extension=True)).cancel(den, include=True) + r = (num1.subs(x, pole))/(den1.subs(x, pole)) + + # If multiplicity is 2, the coefficient to be added + # in the c-vector is c = (1 +- sqrt(1 + 4*r))/2 + if r != -S(1)/4: + return [[(1 + sqrt(1 + 4*r))/2], [(1 - sqrt(1 + 4*r))/2]] + return [[S.Half]] + + +def construct_c_case_2(num, den, x, pole, mul): + # Generate the coefficients using the recurrence + # relation mentioned in (5.14) in the thesis (Pg 80) + + # r_i = mul/2 + ri = mul//2 + + # Find the Laurent series coefficients about the pole + ser = rational_laurent_series(num, den, x, pole, mul, 6) + + # Start with an empty memo to store the coefficients + # This is for the plus case + cplus = [0 for i in range(ri)] + + # Base Case + cplus[ri-1] = sqrt(ser[2*ri]) + + # Iterate backwards to find all coefficients + s = ri - 1 + sm = 0 + for s in range(ri-1, 0, -1): + sm = 0 + for j in range(s+1, ri): + sm += cplus[j-1]*cplus[ri+s-j-1] + if s!= 1: + cplus[s-1] = (ser[ri+s] - sm)/(2*cplus[ri-1]) + + # Memo for the minus case + cminus = [-x for x in cplus] + + # Find the 0th coefficient in the recurrence + cplus[0] = (ser[ri+s] - sm - ri*cplus[ri-1])/(2*cplus[ri-1]) + cminus[0] = (ser[ri+s] - sm - ri*cminus[ri-1])/(2*cminus[ri-1]) + + # Add both the plus and minus cases' coefficients + if cplus != cminus: + return [cplus, cminus] + return cplus + + +def construct_c_case_3(): + # If multiplicity is 1, the coefficient to be added + # in the c-vector is 1 (no choice) + return [[1]] + + +def construct_c(num, den, x, poles, muls): + """ + Helper function to calculate the coefficients + in the c-vector for each pole. + """ + c = [] + for pole, mul in zip(poles, muls): + c.append([]) + + # Case 3 + if mul == 1: + # Add the coefficients from Case 3 + c[-1].extend(construct_c_case_3()) + + # Case 1 + elif mul == 2: + # Add the coefficients from Case 1 + c[-1].extend(construct_c_case_1(num, den, x, pole)) + + # Case 2 + else: + # Add the coefficients from Case 2 + c[-1].extend(construct_c_case_2(num, den, x, pole, mul)) + + return c + + +def construct_d_case_4(ser, N): + # Initialize an empty vector + dplus = [0 for i in range(N+2)] + # d_N = sqrt(a_{2*N}) + dplus[N] = sqrt(ser[2*N]) + + # Use the recurrence relations to find + # the value of d_s + for s in range(N-1, -2, -1): + sm = 0 + for j in range(s+1, N): + sm += dplus[j]*dplus[N+s-j] + if s != -1: + dplus[s] = (ser[N+s] - sm)/(2*dplus[N]) + + # Coefficients for the case of d_N = -sqrt(a_{2*N}) + dminus = [-x for x in dplus] + + # The third equation in Eq 5.15 of the thesis is WRONG! + # d_N must be replaced with N*d_N in that equation. + dplus[-1] = (ser[N+s] - N*dplus[N] - sm)/(2*dplus[N]) + dminus[-1] = (ser[N+s] - N*dminus[N] - sm)/(2*dminus[N]) + + if dplus != dminus: + return [dplus, dminus] + return dplus + + +def construct_d_case_5(ser): + # List to store coefficients for plus case + dplus = [0, 0] + + # d_0 = sqrt(a_0) + dplus[0] = sqrt(ser[0]) + + # d_(-1) = a_(-1)/(2*d_0) + dplus[-1] = ser[-1]/(2*dplus[0]) + + # Coefficients for the minus case are just the negative + # of the coefficients for the positive case. + dminus = [-x for x in dplus] + + if dplus != dminus: + return [dplus, dminus] + return dplus + + +def construct_d_case_6(num, den, x): + # s_oo = lim x->0 1/x**2 * a(1/x) which is equivalent to + # s_oo = lim x->oo x**2 * a(x) + s_inf = limit_at_inf(Poly(x**2, x)*num, den, x) + + # d_(-1) = (1 +- sqrt(1 + 4*s_oo))/2 + if s_inf != -S(1)/4: + return [[(1 + sqrt(1 + 4*s_inf))/2], [(1 - sqrt(1 + 4*s_inf))/2]] + return [[S.Half]] + + +def construct_d(num, den, x, val_inf): + """ + Helper function to calculate the coefficients + in the d-vector based on the valuation of the + function at oo. + """ + N = -val_inf//2 + # Multiplicity of oo as a pole + mul = -val_inf if val_inf < 0 else 0 + ser = rational_laurent_series(num, den, x, oo, mul, 1) + + # Case 4 + if val_inf < 0: + d = construct_d_case_4(ser, N) + + # Case 5 + elif val_inf == 0: + d = construct_d_case_5(ser) + + # Case 6 + else: + d = construct_d_case_6(num, den, x) + + return d + + +def rational_laurent_series(num, den, x, r, m, n): + r""" + The function computes the Laurent series coefficients + of a rational function. + + Parameters + ========== + + num: A Poly object that is the numerator of `f(x)`. + den: A Poly object that is the denominator of `f(x)`. + x: The variable of expansion of the series. + r: The point of expansion of the series. + m: Multiplicity of r if r is a pole of `f(x)`. Should + be zero otherwise. + n: Order of the term upto which the series is expanded. + + Returns + ======= + + series: A dictionary that has power of the term as key + and coefficient of that term as value. + + Below is a basic outline of how the Laurent series of a + rational function `f(x)` about `x_0` is being calculated - + + 1. Substitute `x + x_0` in place of `x`. If `x_0` + is a pole of `f(x)`, multiply the expression by `x^m` + where `m` is the multiplicity of `x_0`. Denote the + the resulting expression as g(x). We do this substitution + so that we can now find the Laurent series of g(x) about + `x = 0`. + + 2. We can then assume that the Laurent series of `g(x)` + takes the following form - + + .. math:: g(x) = \frac{num(x)}{den(x)} = \sum_{m = 0}^{\infty} a_m x^m + + where `a_m` denotes the Laurent series coefficients. + + 3. Multiply the denominator to the RHS of the equation + and form a recurrence relation for the coefficients `a_m`. + """ + one = Poly(1, x, extension=True) + + if r == oo: + # Series at x = oo is equal to first transforming + # the function from x -> 1/x and finding the + # series at x = 0 + num, den = inverse_transform_poly(num, den, x) + r = S(0) + + if r: + # For an expansion about a non-zero point, a + # transformation from x -> x + r must be made + num = num.transform(Poly(x + r, x, extension=True), one) + den = den.transform(Poly(x + r, x, extension=True), one) + + # Remove the pole from the denominator if the series + # expansion is about one of the poles + num, den = (num*x**m).cancel(den, include=True) + + # Equate coefficients for the first terms (base case) + maxdegree = 1 + max(num.degree(), den.degree()) + syms = symbols(f'a:{maxdegree}', cls=Dummy) + diff = num - den * Poly(syms[::-1], x) + coeff_diffs = diff.all_coeffs()[::-1][:maxdegree] + (coeffs, ) = linsolve(coeff_diffs, syms) + + # Use the recursion relation for the rest + recursion = den.all_coeffs()[::-1] + div, rec_rhs = recursion[0], recursion[1:] + series = list(coeffs) + while len(series) < n: + next_coeff = Add(*(c*series[-1-n] for n, c in enumerate(rec_rhs))) / div + series.append(-next_coeff) + series = {m - i: val for i, val in enumerate(series)} + return series + +def compute_m_ybar(x, poles, choice, N): + """ + Helper function to calculate - + + 1. m - The degree bound for the polynomial + solution that must be found for the auxiliary + differential equation. + + 2. ybar - Part of the solution which can be + computed using the poles, c and d vectors. + """ + ybar = 0 + m = Poly(choice[-1][-1], x, extension=True) + + # Calculate the first (nested) summation for ybar + # as given in Step 9 of the Thesis (Pg 82) + dybar = [] + for i, polei in enumerate(poles): + for j, cij in enumerate(choice[i]): + dybar.append(cij/(x - polei)**(j + 1)) + m -=Poly(choice[i][0], x, extension=True) # can't accumulate Poly and use with Add + ybar += Add(*dybar) + + # Calculate the second summation for ybar + for i in range(N+1): + ybar += choice[-1][i]*x**i + return (m.expr, ybar) + + +def solve_aux_eq(numa, dena, numy, deny, x, m): + """ + Helper function to find a polynomial solution + of degree m for the auxiliary differential + equation. + """ + # Assume that the solution is of the type + # p(x) = C_0 + C_1*x + ... + C_{m-1}*x**(m-1) + x**m + psyms = symbols(f'C0:{m}', cls=Dummy) + K = ZZ[psyms] + psol = Poly(K.gens, x, domain=K) + Poly(x**m, x, domain=K) + + # Eq (5.16) in Thesis - Pg 81 + auxeq = (dena*(numy.diff(x)*deny - numy*deny.diff(x) + numy**2) - numa*deny**2)*psol + if m >= 1: + px = psol.diff(x) + auxeq += px*(2*numy*deny*dena) + if m >= 2: + auxeq += px.diff(x)*(deny**2*dena) + if m != 0: + # m is a non-zero integer. Find the constant terms using undetermined coefficients + return psol, linsolve_dict(auxeq.all_coeffs(), psyms), True + else: + # m == 0 . Check if 1 (x**0) is a solution to the auxiliary equation + return S.One, auxeq, auxeq == 0 + + +def remove_redundant_sols(sol1, sol2, x): + """ + Helper function to remove redundant + solutions to the differential equation. + """ + # If y1 and y2 are redundant solutions, there is + # some value of the arbitrary constant for which + # they will be equal + + syms1 = sol1.atoms(Symbol, Dummy) + syms2 = sol2.atoms(Symbol, Dummy) + num1, den1 = [Poly(e, x, extension=True) for e in sol1.together().as_numer_denom()] + num2, den2 = [Poly(e, x, extension=True) for e in sol2.together().as_numer_denom()] + # Cross multiply + e = num1*den2 - den1*num2 + # Check if there are any constants + syms = list(e.atoms(Symbol, Dummy)) + if len(syms): + # Find values of constants for which solutions are equal + redn = linsolve(e.all_coeffs(), syms) + if len(redn): + # Return the general solution over a particular solution + if len(syms1) > len(syms2): + return sol2 + # If both have constants, return the lesser complex solution + elif len(syms1) == len(syms2): + return sol1 if count_ops(syms1) >= count_ops(syms2) else sol2 + else: + return sol1 + + +def get_gen_sol_from_part_sol(part_sols, a, x): + """" + Helper function which computes the general + solution for a Riccati ODE from its particular + solutions. + + There are 3 cases to find the general solution + from the particular solutions for a Riccati ODE + depending on the number of particular solution(s) + we have - 1, 2 or 3. + + For more information, see Section 6 of + "Methods of Solution of the Riccati Differential Equation" + by D. R. Haaheim and F. M. Stein + """ + + # If no particular solutions are found, a general + # solution cannot be found + if len(part_sols) == 0: + return [] + + # In case of a single particular solution, the general + # solution can be found by using the substitution + # y = y1 + 1/z and solving a Bernoulli ODE to find z. + elif len(part_sols) == 1: + y1 = part_sols[0] + i = exp(Integral(2*y1, x)) + z = i * Integral(a/i, x) + z = z.doit() + if a == 0 or z == 0: + return y1 + return y1 + 1/z + + # In case of 2 particular solutions, the general solution + # can be found by solving a separable equation. This is + # the most common case, i.e. most Riccati ODEs have 2 + # rational particular solutions. + elif len(part_sols) == 2: + y1, y2 = part_sols + # One of them already has a constant + if len(y1.atoms(Dummy)) + len(y2.atoms(Dummy)) > 0: + u = exp(Integral(y2 - y1, x)).doit() + # Introduce a constant + else: + C1 = Dummy('C1') + u = C1*exp(Integral(y2 - y1, x)).doit() + if u == 1: + return y2 + return (y2*u - y1)/(u - 1) + + # In case of 3 particular solutions, a closed form + # of the general solution can be obtained directly + else: + y1, y2, y3 = part_sols[:3] + C1 = Dummy('C1') + return (C1 + 1)*y2*(y1 - y3)/(C1*y1 + y2 - (C1 + 1)*y3) + + +def solve_riccati(fx, x, b0, b1, b2, gensol=False): + """ + The main function that gives particular/general + solutions to Riccati ODEs that have atleast 1 + rational particular solution. + """ + # Step 1 : Convert to Normal Form + a = -b0*b2 + b1**2/4 - b1.diff(x)/2 + 3*b2.diff(x)**2/(4*b2**2) + b1*b2.diff(x)/(2*b2) - \ + b2.diff(x, 2)/(2*b2) + a_t = a.together() + num, den = [Poly(e, x, extension=True) for e in a_t.as_numer_denom()] + num, den = num.cancel(den, include=True) + + # Step 2 + presol = [] + + # Step 3 : a(x) is 0 + if num == 0: + presol.append(1/(x + Dummy('C1'))) + + # Step 4 : a(x) is a non-zero constant + elif x not in num.free_symbols.union(den.free_symbols): + presol.extend([sqrt(a), -sqrt(a)]) + + # Step 5 : Find poles and valuation at infinity + poles = roots(den, x) + poles, muls = list(poles.keys()), list(poles.values()) + val_inf = val_at_inf(num, den, x) + + if len(poles): + # Check necessary conditions (outlined in the module docstring) + if not check_necessary_conds(val_inf, muls): + raise ValueError("Rational Solution doesn't exist") + + # Step 6 + # Construct c-vectors for each singular point + c = construct_c(num, den, x, poles, muls) + + # Construct d vectors for each singular point + d = construct_d(num, den, x, val_inf) + + # Step 7 : Iterate over all possible combinations and return solutions + # For each possible combination, generate an array of 0's and 1's + # where 0 means pick 1st choice and 1 means pick the second choice. + + # NOTE: We could exit from the loop if we find 3 particular solutions, + # but it is not implemented here as - + # a. Finding 3 particular solutions is very rare. Most of the time, + # only 2 particular solutions are found. + # b. In case we exit after finding 3 particular solutions, it might + # happen that 1 or 2 of them are redundant solutions. So, instead of + # spending some more time in computing the particular solutions, + # we will end up computing the general solution from a single + # particular solution which is usually slower than computing the + # general solution from 2 or 3 particular solutions. + c.append(d) + choices = product(*c) + for choice in choices: + m, ybar = compute_m_ybar(x, poles, choice, -val_inf//2) + numy, deny = [Poly(e, x, extension=True) for e in ybar.together().as_numer_denom()] + # Step 10 : Check if a valid solution exists. If yes, also check + # if m is a non-negative integer + if m.is_nonnegative == True and m.is_integer == True: + + # Step 11 : Find polynomial solutions of degree m for the auxiliary equation + psol, coeffs, exists = solve_aux_eq(num, den, numy, deny, x, m) + + # Step 12 : If valid polynomial solution exists, append solution. + if exists: + # m == 0 case + if psol == 1 and coeffs == 0: + # p(x) = 1, so p'(x)/p(x) term need not be added + presol.append(ybar) + # m is a positive integer and there are valid coefficients + elif len(coeffs): + # Substitute the valid coefficients to get p(x) + psol = psol.xreplace(coeffs) + # y(x) = ybar(x) + p'(x)/p(x) + presol.append(ybar + psol.diff(x)/psol) + + # Remove redundant solutions from the list of existing solutions + remove = set() + for i in range(len(presol)): + for j in range(i+1, len(presol)): + rem = remove_redundant_sols(presol[i], presol[j], x) + if rem is not None: + remove.add(rem) + sols = [x for x in presol if x not in remove] + + # Step 15 : Inverse transform the solutions of the equation in normal form + bp = -b2.diff(x)/(2*b2**2) - b1/(2*b2) + + # If general solution is required, compute it from the particular solutions + if gensol: + sols = [get_gen_sol_from_part_sol(sols, a, x)] + + # Inverse transform the particular solutions + presol = [Eq(fx, riccati_inverse_normal(y, x, b1, b2, bp).cancel(extension=True)) for y in sols] + return presol diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/single.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/single.py new file mode 100644 index 0000000000000000000000000000000000000000..c4829acf41293c2f6f20af3e9c45d37457802102 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/single.py @@ -0,0 +1,2977 @@ +# +# This is the module for ODE solver classes for single ODEs. +# + +from __future__ import annotations +from typing import ClassVar, Iterator + +from .riccati import match_riccati, solve_riccati +from sympy.core import Add, S, Pow, Rational +from sympy.core.cache import cached_property +from sympy.core.exprtools import factor_terms +from sympy.core.expr import Expr +from sympy.core.function import AppliedUndef, Derivative, diff, Function, expand, Subs, _mexpand +from sympy.core.numbers import zoo +from sympy.core.relational import Equality, Eq +from sympy.core.symbol import Symbol, Dummy, Wild +from sympy.core.mul import Mul +from sympy.functions import exp, tan, log, sqrt, besselj, bessely, cbrt, airyai, airybi +from sympy.integrals import Integral +from sympy.polys import Poly +from sympy.polys.polytools import cancel, factor, degree +from sympy.simplify import collect, simplify, separatevars, logcombine, posify # type: ignore +from sympy.simplify.radsimp import fraction +from sympy.utilities import numbered_symbols +from sympy.solvers.solvers import solve +from sympy.solvers.deutils import ode_order, _preprocess +from sympy.polys.matrices.linsolve import _lin_eq2dict +from sympy.polys.solvers import PolyNonlinearError +from .hypergeometric import equivalence_hypergeometric, match_2nd_2F1_hypergeometric, \ + get_sol_2F1_hypergeometric, match_2nd_hypergeometric +from .nonhomogeneous import _get_euler_characteristic_eq_sols, _get_const_characteristic_eq_sols, \ + _solve_undetermined_coefficients, _solve_variation_of_parameters, _test_term, _undetermined_coefficients_match, \ + _get_simplified_sol +from .lie_group import _ode_lie_group + + +class ODEMatchError(NotImplementedError): + """Raised if a SingleODESolver is asked to solve an ODE it does not match""" + pass + + +class SingleODEProblem: + """Represents an ordinary differential equation (ODE) + + This class is used internally in the by dsolve and related + functions/classes so that properties of an ODE can be computed + efficiently. + + Examples + ======== + + This class is used internally by dsolve. To instantiate an instance + directly first define an ODE problem: + + >>> from sympy import Function, Symbol + >>> x = Symbol('x') + >>> f = Function('f') + >>> eq = f(x).diff(x, 2) + + Now you can create a SingleODEProblem instance and query its properties: + + >>> from sympy.solvers.ode.single import SingleODEProblem + >>> problem = SingleODEProblem(f(x).diff(x), f(x), x) + >>> problem.eq + Derivative(f(x), x) + >>> problem.func + f(x) + >>> problem.sym + x + """ + + # Instance attributes: + eq: Expr + func: AppliedUndef + sym: Symbol + _order: int + _eq_expanded: Expr + _eq_preprocessed: Expr + _eq_high_order_free = None + + def __init__(self, eq, func, sym, prep=True, **kwargs): + assert isinstance(eq, Expr) + assert isinstance(func, AppliedUndef) + assert isinstance(sym, Symbol) + assert isinstance(prep, bool) + self.eq = eq + self.func = func + self.sym = sym + self.prep = prep + self.params = kwargs + + @cached_property + def order(self) -> int: + return ode_order(self.eq, self.func) + + @cached_property + def eq_preprocessed(self) -> Expr: + return self._get_eq_preprocessed() + + @cached_property + def eq_high_order_free(self) -> Expr: + a = Wild('a', exclude=[self.func]) + c1 = Wild('c1', exclude=[self.sym]) + # Precondition to try remove f(x) from highest order derivative + reduced_eq = None + if self.eq.is_Add: + deriv_coef = self.eq.coeff(self.func.diff(self.sym, self.order)) + if deriv_coef not in (1, 0): + r = deriv_coef.match(a*self.func**c1) + if r and r[c1]: + den = self.func**r[c1] + reduced_eq = Add(*[arg/den for arg in self.eq.args]) + if reduced_eq is None: + reduced_eq = expand(self.eq) + return reduced_eq + + @cached_property + def eq_expanded(self) -> Expr: + return expand(self.eq_preprocessed) + + def _get_eq_preprocessed(self) -> Expr: + if self.prep: + process_eq, process_func = _preprocess(self.eq, self.func) + if process_func != self.func: + raise ValueError + else: + process_eq = self.eq + return process_eq + + def get_numbered_constants(self, num=1, start=1, prefix='C') -> list[Symbol]: + """ + Returns a list of constants that do not occur + in eq already. + """ + ncs = self.iter_numbered_constants(start, prefix) + Cs = [next(ncs) for i in range(num)] + return Cs + + def iter_numbered_constants(self, start=1, prefix='C') -> Iterator[Symbol]: + """ + Returns an iterator of constants that do not occur + in eq already. + """ + atom_set = self.eq.free_symbols + func_set = self.eq.atoms(Function) + if func_set: + atom_set |= {Symbol(str(f.func)) for f in func_set} + return numbered_symbols(start=start, prefix=prefix, exclude=atom_set) + + @cached_property + def is_autonomous(self): + u = Dummy('u') + x = self.sym + syms = self.eq.subs(self.func, u).free_symbols + return x not in syms + + def get_linear_coefficients(self, eq, func, order): + r""" + Matches a differential equation to the linear form: + + .. math:: a_n(x) y^{(n)} + \cdots + a_1(x)y' + a_0(x) y + B(x) = 0 + + Returns a dict of order:coeff terms, where order is the order of the + derivative on each term, and coeff is the coefficient of that derivative. + The key ``-1`` holds the function `B(x)`. Returns ``None`` if the ODE is + not linear. This function assumes that ``func`` has already been checked + to be good. + + Examples + ======== + + >>> from sympy import Function, cos, sin + >>> from sympy.abc import x + >>> from sympy.solvers.ode.single import SingleODEProblem + >>> f = Function('f') + >>> eq = f(x).diff(x, 3) + 2*f(x).diff(x) + \ + ... x*f(x).diff(x, 2) + cos(x)*f(x).diff(x) + x - f(x) - \ + ... sin(x) + >>> obj = SingleODEProblem(eq, f(x), x) + >>> obj.get_linear_coefficients(eq, f(x), 3) + {-1: x - sin(x), 0: -1, 1: cos(x) + 2, 2: x, 3: 1} + >>> eq = f(x).diff(x, 3) + 2*f(x).diff(x) + \ + ... x*f(x).diff(x, 2) + cos(x)*f(x).diff(x) + x - f(x) - \ + ... sin(f(x)) + >>> obj = SingleODEProblem(eq, f(x), x) + >>> obj.get_linear_coefficients(eq, f(x), 3) == None + True + + """ + f = func.func + x = func.args[0] + symset = {Derivative(f(x), x, i) for i in range(order+1)} + try: + rhs, lhs_terms = _lin_eq2dict(eq, symset) + except PolyNonlinearError: + return None + + if rhs.has(func) or any(c.has(func) for c in lhs_terms.values()): + return None + terms = {i: lhs_terms.get(f(x).diff(x, i), S.Zero) for i in range(order+1)} + terms[-1] = rhs + return terms + + # TODO: Add methods that can be used by many ODE solvers: + # order + # is_linear() + # get_linear_coefficients() + # eq_prepared (the ODE in prepared form) + + +class SingleODESolver: + """ + Base class for Single ODE solvers. + + Subclasses should implement the _matches and _get_general_solution + methods. This class is not intended to be instantiated directly but its + subclasses are as part of dsolve. + + Examples + ======== + + You can use a subclass of SingleODEProblem to solve a particular type of + ODE. We first define a particular ODE problem: + + >>> from sympy import Function, Symbol + >>> x = Symbol('x') + >>> f = Function('f') + >>> eq = f(x).diff(x, 2) + + Now we solve this problem using the NthAlgebraic solver which is a + subclass of SingleODESolver: + + >>> from sympy.solvers.ode.single import NthAlgebraic, SingleODEProblem + >>> problem = SingleODEProblem(eq, f(x), x) + >>> solver = NthAlgebraic(problem) + >>> solver.get_general_solution() + [Eq(f(x), _C*x + _C)] + + The normal way to solve an ODE is to use dsolve (which would use + NthAlgebraic and other solvers internally). When using dsolve a number of + other things are done such as evaluating integrals, simplifying the + solution and renumbering the constants: + + >>> from sympy import dsolve + >>> dsolve(eq, hint='nth_algebraic') + Eq(f(x), C1 + C2*x) + """ + + # Subclasses should store the hint name (the argument to dsolve) in this + # attribute + hint: ClassVar[str] + + # Subclasses should define this to indicate if they support an _Integral + # hint. + has_integral: ClassVar[bool] + + # The ODE to be solved + ode_problem: SingleODEProblem + + # Cache whether or not the equation has matched the method + _matched: bool | None = None + + # Subclasses should store in this attribute the list of order(s) of ODE + # that subclass can solve or leave it to None if not specific to any order + order: list | None = None + + def __init__(self, ode_problem): + self.ode_problem = ode_problem + + def matches(self) -> bool: + if self.order is not None and self.ode_problem.order not in self.order: + self._matched = False + return self._matched + + if self._matched is None: + self._matched = self._matches() + return self._matched + + def get_general_solution(self, *, simplify: bool = True) -> list[Equality]: + if not self.matches(): + msg = "%s solver cannot solve:\n%s" + raise ODEMatchError(msg % (self.hint, self.ode_problem.eq)) + return self._get_general_solution(simplify_flag=simplify) + + def _matches(self) -> bool: + msg = "Subclasses of SingleODESolver should implement matches." + raise NotImplementedError(msg) + + def _get_general_solution(self, *, simplify_flag: bool = True) -> list[Equality]: + msg = "Subclasses of SingleODESolver should implement get_general_solution." + raise NotImplementedError(msg) + + +class SinglePatternODESolver(SingleODESolver): + '''Superclass for ODE solvers based on pattern matching''' + + def wilds(self): + prob = self.ode_problem + f = prob.func.func + x = prob.sym + order = prob.order + return self._wilds(f, x, order) + + def wilds_match(self): + match = self._wilds_match + return [match.get(w, S.Zero) for w in self.wilds()] + + def _matches(self): + eq = self.ode_problem.eq_expanded + f = self.ode_problem.func.func + x = self.ode_problem.sym + order = self.ode_problem.order + df = f(x).diff(x, order) + + if order not in [1, 2]: + return False + + pattern = self._equation(f(x), x, order) + + if not pattern.coeff(df).has(Wild): + eq = expand(eq / eq.coeff(df)) + eq = eq.collect([f(x).diff(x), f(x)], func = cancel) + + self._wilds_match = match = eq.match(pattern) + if match is not None: + return self._verify(f(x)) + return False + + def _verify(self, fx) -> bool: + return True + + def _wilds(self, f, x, order): + msg = "Subclasses of SingleODESolver should implement _wilds" + raise NotImplementedError(msg) + + def _equation(self, fx, x, order): + msg = "Subclasses of SingleODESolver should implement _equation" + raise NotImplementedError(msg) + + +class NthAlgebraic(SingleODESolver): + r""" + Solves an `n`\th order ordinary differential equation using algebra and + integrals. + + There is no general form for the kind of equation that this can solve. The + the equation is solved algebraically treating differentiation as an + invertible algebraic function. + + Examples + ======== + + >>> from sympy import Function, dsolve, Eq + >>> from sympy.abc import x + >>> f = Function('f') + >>> eq = Eq(f(x) * (f(x).diff(x)**2 - 1), 0) + >>> dsolve(eq, f(x), hint='nth_algebraic') + [Eq(f(x), 0), Eq(f(x), C1 - x), Eq(f(x), C1 + x)] + + Note that this solver can return algebraic solutions that do not have any + integration constants (f(x) = 0 in the above example). + """ + + hint = 'nth_algebraic' + has_integral = True # nth_algebraic_Integral hint + + def _matches(self): + r""" + Matches any differential equation that nth_algebraic can solve. Uses + `sympy.solve` but teaches it how to integrate derivatives. + + This involves calling `sympy.solve` and does most of the work of finding a + solution (apart from evaluating the integrals). + """ + eq = self.ode_problem.eq + func = self.ode_problem.func + var = self.ode_problem.sym + + # Derivative that solve can handle: + diffx = self._get_diffx(var) + + # Replace derivatives wrt the independent variable with diffx + def replace(eq, var): + def expand_diffx(*args): + differand, diffs = args[0], args[1:] + toreplace = differand + for v, n in diffs: + for _ in range(n): + if v == var: + toreplace = diffx(toreplace) + else: + toreplace = Derivative(toreplace, v) + return toreplace + return eq.replace(Derivative, expand_diffx) + + # Restore derivatives in solution afterwards + def unreplace(eq, var): + return eq.replace(diffx, lambda e: Derivative(e, var)) + + subs_eqn = replace(eq, var) + try: + # turn off simplification to protect Integrals that have + # _t instead of fx in them and would otherwise factor + # as t_*Integral(1, x) + solns = solve(subs_eqn, func, simplify=False) + except NotImplementedError: + solns = [] + + solns = [simplify(unreplace(soln, var)) for soln in solns] + solns = [Equality(func, soln) for soln in solns] + + self.solutions = solns + return len(solns) != 0 + + def _get_general_solution(self, *, simplify_flag: bool = True): + return self.solutions + + # This needs to produce an invertible function but the inverse depends + # which variable we are integrating with respect to. Since the class can + # be stored in cached results we need to ensure that we always get the + # same class back for each particular integration variable so we store these + # classes in a global dict: + _diffx_stored: dict[Symbol, type[Function]] = {} + + @staticmethod + def _get_diffx(var): + diffcls = NthAlgebraic._diffx_stored.get(var, None) + + if diffcls is None: + # A class that behaves like Derivative wrt var but is "invertible". + class diffx(Function): + def inverse(self): + # don't use integrate here because fx has been replaced by _t + # in the equation; integrals will not be correct while solve + # is at work. + return lambda expr: Integral(expr, var) + Dummy('C') + + diffcls = NthAlgebraic._diffx_stored.setdefault(var, diffx) + + return diffcls + + +class FirstExact(SinglePatternODESolver): + r""" + Solves 1st order exact ordinary differential equations. + + A 1st order differential equation is called exact if it is the total + differential of a function. That is, the differential equation + + .. math:: P(x, y) \,\partial{}x + Q(x, y) \,\partial{}y = 0 + + is exact if there is some function `F(x, y)` such that `P(x, y) = + \partial{}F/\partial{}x` and `Q(x, y) = \partial{}F/\partial{}y`. It can + be shown that a necessary and sufficient condition for a first order ODE + to be exact is that `\partial{}P/\partial{}y = \partial{}Q/\partial{}x`. + Then, the solution will be as given below:: + + >>> from sympy import Function, Eq, Integral, symbols, pprint + >>> x, y, t, x0, y0, C1= symbols('x,y,t,x0,y0,C1') + >>> P, Q, F= map(Function, ['P', 'Q', 'F']) + >>> pprint(Eq(Eq(F(x, y), Integral(P(t, y), (t, x0, x)) + + ... Integral(Q(x0, t), (t, y0, y))), C1)) + x y + / / + | | + F(x, y) = | P(t, y) dt + | Q(x0, t) dt = C1 + | | + / / + x0 y0 + + Where the first partials of `P` and `Q` exist and are continuous in a + simply connected region. + + A note: SymPy currently has no way to represent inert substitution on an + expression, so the hint ``1st_exact_Integral`` will return an integral + with `dy`. This is supposed to represent the function that you are + solving for. + + Examples + ======== + + >>> from sympy import Function, dsolve, cos, sin + >>> from sympy.abc import x + >>> f = Function('f') + >>> dsolve(cos(f(x)) - (x*sin(f(x)) - f(x)**2)*f(x).diff(x), + ... f(x), hint='1st_exact') + Eq(x*cos(f(x)) + f(x)**3/3, C1) + + References + ========== + + - https://en.wikipedia.org/wiki/Exact_differential_equation + - M. Tenenbaum & H. Pollard, "Ordinary Differential Equations", + Dover 1963, pp. 73 + + # indirect doctest + + """ + hint = "1st_exact" + has_integral = True + order = [1] + + def _wilds(self, f, x, order): + P = Wild('P', exclude=[f(x).diff(x)]) + Q = Wild('Q', exclude=[f(x).diff(x)]) + return P, Q + + def _equation(self, fx, x, order): + P, Q = self.wilds() + return P + Q*fx.diff(x) + + def _verify(self, fx) -> bool: + P, Q = self.wilds() + x = self.ode_problem.sym + y = Dummy('y') + + m, n = self.wilds_match() + + m = m.subs(fx, y) + n = n.subs(fx, y) + numerator = cancel(m.diff(y) - n.diff(x)) + + if numerator.is_zero: + # Is exact + return True + else: + # The following few conditions try to convert a non-exact + # differential equation into an exact one. + # References: + # 1. Differential equations with applications + # and historical notes - George E. Simmons + # 2. https://math.okstate.edu/people/binegar/2233-S99/2233-l12.pdf + + factor_n = cancel(numerator/n) + factor_m = cancel(-numerator/m) + if y not in factor_n.free_symbols: + # If (dP/dy - dQ/dx) / Q = f(x) + # then exp(integral(f(x))*equation becomes exact + factor = factor_n + integration_variable = x + elif x not in factor_m.free_symbols: + # If (dP/dy - dQ/dx) / -P = f(y) + # then exp(integral(f(y))*equation becomes exact + factor = factor_m + integration_variable = y + else: + # Couldn't convert to exact + return False + + factor = exp(Integral(factor, integration_variable)) + m *= factor + n *= factor + self._wilds_match[P] = m.subs(y, fx) + self._wilds_match[Q] = n.subs(y, fx) + return True + + def _get_general_solution(self, *, simplify_flag: bool = True): + m, n = self.wilds_match() + fx = self.ode_problem.func + x = self.ode_problem.sym + (C1,) = self.ode_problem.get_numbered_constants(num=1) + y = Dummy('y') + + m = m.subs(fx, y) + n = n.subs(fx, y) + + gen_sol = Eq(Subs(Integral(m, x) + + Integral(n - Integral(m, x).diff(y), y), y, fx), C1) + return [gen_sol] + + +class FirstLinear(SinglePatternODESolver): + r""" + Solves 1st order linear differential equations. + + These are differential equations of the form + + .. math:: dy/dx + P(x) y = Q(x)\text{.} + + These kinds of differential equations can be solved in a general way. The + integrating factor `e^{\int P(x) \,dx}` will turn the equation into a + separable equation. The general solution is:: + + >>> from sympy import Function, dsolve, Eq, pprint, diff, sin + >>> from sympy.abc import x + >>> f, P, Q = map(Function, ['f', 'P', 'Q']) + >>> genform = Eq(f(x).diff(x) + P(x)*f(x), Q(x)) + >>> pprint(genform) + d + P(x)*f(x) + --(f(x)) = Q(x) + dx + >>> pprint(dsolve(genform, f(x), hint='1st_linear_Integral')) + / / \ + | | | + | | / | / + | | | | | + | | | P(x) dx | - | P(x) dx + | | | | | + | | / | / + f(x) = |C1 + | Q(x)*e dx|*e + | | | + \ / / + + + Examples + ======== + + >>> f = Function('f') + >>> pprint(dsolve(Eq(x*diff(f(x), x) - f(x), x**2*sin(x)), + ... f(x), '1st_linear')) + f(x) = x*(C1 - cos(x)) + + References + ========== + + - https://en.wikipedia.org/wiki/Linear_differential_equation#First-order_equation_with_variable_coefficients + - M. Tenenbaum & H. Pollard, "Ordinary Differential Equations", + Dover 1963, pp. 92 + + # indirect doctest + + """ + hint = '1st_linear' + has_integral = True + order = [1] + + def _wilds(self, f, x, order): + P = Wild('P', exclude=[f(x)]) + Q = Wild('Q', exclude=[f(x), f(x).diff(x)]) + return P, Q + + def _equation(self, fx, x, order): + P, Q = self.wilds() + return fx.diff(x) + P*fx - Q + + def _get_general_solution(self, *, simplify_flag: bool = True): + P, Q = self.wilds_match() + fx = self.ode_problem.func + x = self.ode_problem.sym + (C1,) = self.ode_problem.get_numbered_constants(num=1) + gensol = Eq(fx, ((C1 + Integral(Q*exp(Integral(P, x)), x)) + * exp(-Integral(P, x)))) + return [gensol] + + +class AlmostLinear(SinglePatternODESolver): + r""" + Solves an almost-linear differential equation. + + The general form of an almost linear differential equation is + + .. math:: a(x) g'(f(x)) f'(x) + b(x) g(f(x)) + c(x) + + Here `f(x)` is the function to be solved for (the dependent variable). + The substitution `g(f(x)) = u(x)` leads to a linear differential equation + for `u(x)` of the form `a(x) u' + b(x) u + c(x) = 0`. This can be solved + for `u(x)` by the `first_linear` hint and then `f(x)` is found by solving + `g(f(x)) = u(x)`. + + See Also + ======== + :obj:`sympy.solvers.ode.single.FirstLinear` + + Examples + ======== + + >>> from sympy import dsolve, Function, pprint, sin, cos + >>> from sympy.abc import x + >>> f = Function('f') + >>> d = f(x).diff(x) + >>> eq = x*d + x*f(x) + 1 + >>> dsolve(eq, f(x), hint='almost_linear') + Eq(f(x), (C1 - Ei(x))*exp(-x)) + >>> pprint(dsolve(eq, f(x), hint='almost_linear')) + -x + f(x) = (C1 - Ei(x))*e + >>> example = cos(f(x))*f(x).diff(x) + sin(f(x)) + 1 + >>> pprint(example) + d + sin(f(x)) + cos(f(x))*--(f(x)) + 1 + dx + >>> pprint(dsolve(example, f(x), hint='almost_linear')) + / -x \ / -x \ + [f(x) = pi - asin\C1*e - 1/, f(x) = asin\C1*e - 1/] + + + References + ========== + + - Joel Moses, "Symbolic Integration - The Stormy Decade", Communications + of the ACM, Volume 14, Number 8, August 1971, pp. 558 + """ + hint = "almost_linear" + has_integral = True + order = [1] + + def _wilds(self, f, x, order): + P = Wild('P', exclude=[f(x).diff(x)]) + Q = Wild('Q', exclude=[f(x).diff(x)]) + return P, Q + + def _equation(self, fx, x, order): + P, Q = self.wilds() + return P*fx.diff(x) + Q + + def _verify(self, fx): + a, b = self.wilds_match() + c, b = b.as_independent(fx) if b.is_Add else (S.Zero, b) + # a, b and c are the function a(x), b(x) and c(x) respectively. + # c(x) is obtained by separating out b as terms with and without fx i.e, l(y) + # The following conditions checks if the given equation is an almost-linear differential equation using the fact that + # a(x)*(l(y))' / l(y)' is independent of l(y) + + if b.diff(fx) != 0 and not simplify(b.diff(fx)/a).has(fx): + self.ly = factor_terms(b).as_independent(fx, as_Add=False)[1] # Gives the term containing fx i.e., l(y) + self.ax = a / self.ly.diff(fx) + self.cx = -c # cx is taken as -c(x) to simplify expression in the solution integral + self.bx = factor_terms(b) / self.ly + return True + + return False + + def _get_general_solution(self, *, simplify_flag: bool = True): + x = self.ode_problem.sym + (C1,) = self.ode_problem.get_numbered_constants(num=1) + gensol = Eq(self.ly, ((C1 + Integral((self.cx/self.ax)*exp(Integral(self.bx/self.ax, x)), x)) + * exp(-Integral(self.bx/self.ax, x)))) + + return [gensol] + + +class Bernoulli(SinglePatternODESolver): + r""" + Solves Bernoulli differential equations. + + These are equations of the form + + .. math:: dy/dx + P(x) y = Q(x) y^n\text{, }n \ne 1`\text{.} + + The substitution `w = 1/y^{1-n}` will transform an equation of this form + into one that is linear (see the docstring of + :obj:`~sympy.solvers.ode.single.FirstLinear`). The general solution is:: + + >>> from sympy import Function, dsolve, Eq, pprint + >>> from sympy.abc import x, n + >>> f, P, Q = map(Function, ['f', 'P', 'Q']) + >>> genform = Eq(f(x).diff(x) + P(x)*f(x), Q(x)*f(x)**n) + >>> pprint(genform) + d n + P(x)*f(x) + --(f(x)) = Q(x)*f (x) + dx + >>> pprint(dsolve(genform, f(x), hint='Bernoulli_Integral'), num_columns=110) + -1 + ----- + n - 1 + // / / \ \ + || | | | | + || | / | / | / | + || | | | | | | | + || | -(n - 1)* | P(x) dx | -(n - 1)* | P(x) dx | (n - 1)* | P(x) dx| + || | | | | | | | + || | / | / | / | + f(x) = ||C1 - n* | Q(x)*e dx + | Q(x)*e dx|*e | + || | | | | + \\ / / / / + + + Note that the equation is separable when `n = 1` (see the docstring of + :obj:`~sympy.solvers.ode.single.Separable`). + + >>> pprint(dsolve(Eq(f(x).diff(x) + P(x)*f(x), Q(x)*f(x)), f(x), + ... hint='separable_Integral')) + f(x) + / + | / + | 1 | + | - dy = C1 + | (-P(x) + Q(x)) dx + | y | + | / + / + + + Examples + ======== + + >>> from sympy import Function, dsolve, Eq, pprint, log + >>> from sympy.abc import x + >>> f = Function('f') + + >>> pprint(dsolve(Eq(x*f(x).diff(x) + f(x), log(x)*f(x)**2), + ... f(x), hint='Bernoulli')) + 1 + f(x) = ----------------- + C1*x + log(x) + 1 + + References + ========== + + - https://en.wikipedia.org/wiki/Bernoulli_differential_equation + + - M. Tenenbaum & H. Pollard, "Ordinary Differential Equations", + Dover 1963, pp. 95 + + # indirect doctest + + """ + hint = "Bernoulli" + has_integral = True + order = [1] + + def _wilds(self, f, x, order): + P = Wild('P', exclude=[f(x)]) + Q = Wild('Q', exclude=[f(x)]) + n = Wild('n', exclude=[x, f(x), f(x).diff(x)]) + return P, Q, n + + def _equation(self, fx, x, order): + P, Q, n = self.wilds() + return fx.diff(x) + P*fx - Q*fx**n + + def _get_general_solution(self, *, simplify_flag: bool = True): + P, Q, n = self.wilds_match() + fx = self.ode_problem.func + x = self.ode_problem.sym + (C1,) = self.ode_problem.get_numbered_constants(num=1) + if n==1: + gensol = Eq(log(fx), ( + C1 + Integral((-P + Q), x) + )) + else: + gensol = Eq(fx**(1-n), ( + (C1 - (n - 1) * Integral(Q*exp(-n*Integral(P, x)) + * exp(Integral(P, x)), x) + ) * exp(-(1 - n)*Integral(P, x))) + ) + return [gensol] + + +class Factorable(SingleODESolver): + r""" + Solves equations having a solvable factor. + + This function is used to solve the equation having factors. Factors may be of type algebraic or ode. It + will try to solve each factor independently. Factors will be solved by calling dsolve. We will return the + list of solutions. + + Examples + ======== + + >>> from sympy import Function, dsolve, pprint + >>> from sympy.abc import x + >>> f = Function('f') + >>> eq = (f(x)**2-4)*(f(x).diff(x)+f(x)) + >>> pprint(dsolve(eq, f(x))) + -x + [f(x) = 2, f(x) = -2, f(x) = C1*e ] + + + """ + hint = "factorable" + has_integral = False + + def _matches(self): + eq_orig = self.ode_problem.eq + f = self.ode_problem.func.func + x = self.ode_problem.sym + df = f(x).diff(x) + self.eqs = [] + eq = eq_orig.collect(f(x), func = cancel) + eq = fraction(factor(eq))[0] + factors = Mul.make_args(factor(eq)) + roots = [fac.as_base_exp() for fac in factors if len(fac.args)!=0] + if len(roots)>1 or roots[0][1]>1: + for base, expo in roots: + if base.has(f(x)): + self.eqs.append(base) + if len(self.eqs)>0: + return True + roots = solve(eq, df) + if len(roots)>0: + self.eqs = [(df - root) for root in roots] + # Avoid infinite recursion + matches = self.eqs != [eq_orig] + return matches + for i in factors: + if i.has(f(x)): + self.eqs.append(i) + return len(self.eqs)>0 and len(factors)>1 + + def _get_general_solution(self, *, simplify_flag: bool = True): + func = self.ode_problem.func.func + x = self.ode_problem.sym + eqns = self.eqs + sols = [] + for eq in eqns: + try: + sol = dsolve(eq, func(x)) + except NotImplementedError: + continue + else: + if isinstance(sol, list): + sols.extend(sol) + else: + sols.append(sol) + + if sols == []: + raise NotImplementedError("The given ODE " + str(eq) + " cannot be solved by" + + " the factorable group method") + return sols + + +class RiccatiSpecial(SinglePatternODESolver): + r""" + The general Riccati equation has the form + + .. math:: dy/dx = f(x) y^2 + g(x) y + h(x)\text{.} + + While it does not have a general solution [1], the "special" form, `dy/dx + = a y^2 - b x^c`, does have solutions in many cases [2]. This routine + returns a solution for `a(dy/dx) = b y^2 + c y/x + d/x^2` that is obtained + by using a suitable change of variables to reduce it to the special form + and is valid when neither `a` nor `b` are zero and either `c` or `d` is + zero. + + >>> from sympy.abc import x, a, b, c, d + >>> from sympy import dsolve, checkodesol, pprint, Function + >>> f = Function('f') + >>> y = f(x) + >>> genform = a*y.diff(x) - (b*y**2 + c*y/x + d/x**2) + >>> sol = dsolve(genform, y, hint="Riccati_special_minus2") + >>> pprint(sol, wrap_line=False) + / / __________________ \\ + | __________________ | / 2 || + | / 2 | \/ 4*b*d - (a + c) *log(x)|| + -|a + c - \/ 4*b*d - (a + c) *tan|C1 + ----------------------------|| + \ \ 2*a // + f(x) = ------------------------------------------------------------------------ + 2*b*x + + >>> checkodesol(genform, sol, order=1)[0] + True + + References + ========== + + - https://www.maplesoft.com/support/help/Maple/view.aspx?path=odeadvisor/Riccati + - https://eqworld.ipmnet.ru/en/solutions/ode/ode0106.pdf - + https://eqworld.ipmnet.ru/en/solutions/ode/ode0123.pdf + """ + hint = "Riccati_special_minus2" + has_integral = False + order = [1] + + def _wilds(self, f, x, order): + a = Wild('a', exclude=[x, f(x), f(x).diff(x), 0]) + b = Wild('b', exclude=[x, f(x), f(x).diff(x), 0]) + c = Wild('c', exclude=[x, f(x), f(x).diff(x)]) + d = Wild('d', exclude=[x, f(x), f(x).diff(x)]) + return a, b, c, d + + def _equation(self, fx, x, order): + a, b, c, d = self.wilds() + return a*fx.diff(x) + b*fx**2 + c*fx/x + d/x**2 + + def _get_general_solution(self, *, simplify_flag: bool = True): + a, b, c, d = self.wilds_match() + fx = self.ode_problem.func + x = self.ode_problem.sym + (C1,) = self.ode_problem.get_numbered_constants(num=1) + mu = sqrt(4*d*b - (a - c)**2) + + gensol = Eq(fx, (a - c - mu*tan(mu/(2*a)*log(x) + C1))/(2*b*x)) + return [gensol] + + +class RationalRiccati(SinglePatternODESolver): + r""" + Gives general solutions to the first order Riccati differential + equations that have atleast one rational particular solution. + + .. math :: y' = b_0(x) + b_1(x) y + b_2(x) y^2 + + where `b_0`, `b_1` and `b_2` are rational functions of `x` + with `b_2 \ne 0` (`b_2 = 0` would make it a Bernoulli equation). + + Examples + ======== + + >>> from sympy import Symbol, Function, dsolve, checkodesol + >>> f = Function('f') + >>> x = Symbol('x') + + >>> eq = -x**4*f(x)**2 + x**3*f(x).diff(x) + x**2*f(x) + 20 + >>> sol = dsolve(eq, hint="1st_rational_riccati") + >>> sol + Eq(f(x), (4*C1 - 5*x**9 - 4)/(x**2*(C1 + x**9 - 1))) + >>> checkodesol(eq, sol) + (True, 0) + + References + ========== + + - Riccati ODE: https://en.wikipedia.org/wiki/Riccati_equation + - N. Thieu Vo - Rational and Algebraic Solutions of First-Order Algebraic ODEs: + Algorithm 11, pp. 78 - https://www3.risc.jku.at/publications/download/risc_5387/PhDThesisThieu.pdf + """ + has_integral = False + hint = "1st_rational_riccati" + order = [1] + + def _wilds(self, f, x, order): + b0 = Wild('b0', exclude=[f(x), f(x).diff(x)]) + b1 = Wild('b1', exclude=[f(x), f(x).diff(x)]) + b2 = Wild('b2', exclude=[f(x), f(x).diff(x)]) + return (b0, b1, b2) + + def _equation(self, fx, x, order): + b0, b1, b2 = self.wilds() + return fx.diff(x) - b0 - b1*fx - b2*fx**2 + + def _matches(self): + eq = self.ode_problem.eq_expanded + f = self.ode_problem.func.func + x = self.ode_problem.sym + order = self.ode_problem.order + + if order != 1: + return False + + match, funcs = match_riccati(eq, f, x) + if not match: + return False + _b0, _b1, _b2 = funcs + b0, b1, b2 = self.wilds() + self._wilds_match = match = {b0: _b0, b1: _b1, b2: _b2} + return True + + def _get_general_solution(self, *, simplify_flag: bool = True): + # Match the equation + b0, b1, b2 = self.wilds_match() + fx = self.ode_problem.func + x = self.ode_problem.sym + return solve_riccati(fx, x, b0, b1, b2, gensol=True) + + +class SecondNonlinearAutonomousConserved(SinglePatternODESolver): + r""" + Gives solution for the autonomous second order nonlinear + differential equation of the form + + .. math :: f''(x) = g(f(x)) + + The solution for this differential equation can be computed + by multiplying by `f'(x)` and integrating on both sides, + converting it into a first order differential equation. + + Examples + ======== + + >>> from sympy import Function, symbols, dsolve + >>> f, g = symbols('f g', cls=Function) + >>> x = symbols('x') + + >>> eq = f(x).diff(x, 2) - g(f(x)) + >>> dsolve(eq, simplify=False) + [Eq(Integral(1/sqrt(C1 + 2*Integral(g(_u), _u)), (_u, f(x))), C2 + x), + Eq(Integral(1/sqrt(C1 + 2*Integral(g(_u), _u)), (_u, f(x))), C2 - x)] + + >>> from sympy import exp, log + >>> eq = f(x).diff(x, 2) - exp(f(x)) + log(f(x)) + >>> dsolve(eq, simplify=False) + [Eq(Integral(1/sqrt(-2*_u*log(_u) + 2*_u + C1 + 2*exp(_u)), (_u, f(x))), C2 + x), + Eq(Integral(1/sqrt(-2*_u*log(_u) + 2*_u + C1 + 2*exp(_u)), (_u, f(x))), C2 - x)] + + References + ========== + + - https://eqworld.ipmnet.ru/en/solutions/ode/ode0301.pdf + """ + hint = "2nd_nonlinear_autonomous_conserved" + has_integral = True + order = [2] + + def _wilds(self, f, x, order): + fy = Wild('fy', exclude=[0, f(x).diff(x), f(x).diff(x, 2)]) + return (fy, ) + + def _equation(self, fx, x, order): + fy = self.wilds()[0] + return fx.diff(x, 2) + fy + + def _verify(self, fx): + return self.ode_problem.is_autonomous + + def _get_general_solution(self, *, simplify_flag: bool = True): + g = self.wilds_match()[0] + fx = self.ode_problem.func + x = self.ode_problem.sym + u = Dummy('u') + g = g.subs(fx, u) + C1, C2 = self.ode_problem.get_numbered_constants(num=2) + inside = -2*Integral(g, u) + C1 + lhs = Integral(1/sqrt(inside), (u, fx)) + return [Eq(lhs, C2 + x), Eq(lhs, C2 - x)] + + +class Liouville(SinglePatternODESolver): + r""" + Solves 2nd order Liouville differential equations. + + The general form of a Liouville ODE is + + .. math:: \frac{d^2 y}{dx^2} + g(y) \left(\! + \frac{dy}{dx}\!\right)^2 + h(x) + \frac{dy}{dx}\text{.} + + The general solution is: + + >>> from sympy import Function, dsolve, Eq, pprint, diff + >>> from sympy.abc import x + >>> f, g, h = map(Function, ['f', 'g', 'h']) + >>> genform = Eq(diff(f(x),x,x) + g(f(x))*diff(f(x),x)**2 + + ... h(x)*diff(f(x),x), 0) + >>> pprint(genform) + 2 2 + /d \ d d + g(f(x))*|--(f(x))| + h(x)*--(f(x)) + ---(f(x)) = 0 + \dx / dx 2 + dx + >>> pprint(dsolve(genform, f(x), hint='Liouville_Integral')) + f(x) + / / + | | + | / | / + | | | | + | - | h(x) dx | | g(y) dy + | | | | + | / | / + C1 + C2* | e dx + | e dy = 0 + | | + / / + + Examples + ======== + + >>> from sympy import Function, dsolve, Eq, pprint + >>> from sympy.abc import x + >>> f = Function('f') + >>> pprint(dsolve(diff(f(x), x, x) + diff(f(x), x)**2/f(x) + + ... diff(f(x), x)/x, f(x), hint='Liouville')) + ________________ ________________ + [f(x) = -\/ C1 + C2*log(x) , f(x) = \/ C1 + C2*log(x) ] + + References + ========== + + - Goldstein and Braun, "Advanced Methods for the Solution of Differential + Equations", pp. 98 + - https://www.maplesoft.com/support/help/Maple/view.aspx?path=odeadvisor/Liouville + + # indirect doctest + + """ + hint = "Liouville" + has_integral = True + order = [2] + + def _wilds(self, f, x, order): + d = Wild('d', exclude=[f(x).diff(x), f(x).diff(x, 2)]) + e = Wild('e', exclude=[f(x).diff(x)]) + k = Wild('k', exclude=[f(x).diff(x)]) + return d, e, k + + def _equation(self, fx, x, order): + # Liouville ODE in the form + # f(x).diff(x, 2) + g(f(x))*(f(x).diff(x))**2 + h(x)*f(x).diff(x) + # See Goldstein and Braun, "Advanced Methods for the Solution of + # Differential Equations", pg. 98 + d, e, k = self.wilds() + return d*fx.diff(x, 2) + e*fx.diff(x)**2 + k*fx.diff(x) + + def _verify(self, fx): + d, e, k = self.wilds_match() + self.y = Dummy('y') + x = self.ode_problem.sym + self.g = simplify(e/d).subs(fx, self.y) + self.h = simplify(k/d).subs(fx, self.y) + if self.y in self.h.free_symbols or x in self.g.free_symbols: + return False + return True + + def _get_general_solution(self, *, simplify_flag: bool = True): + d, e, k = self.wilds_match() + fx = self.ode_problem.func + x = self.ode_problem.sym + C1, C2 = self.ode_problem.get_numbered_constants(num=2) + int = Integral(exp(Integral(self.g, self.y)), (self.y, None, fx)) + gen_sol = Eq(int + C1*Integral(exp(-Integral(self.h, x)), x) + C2, 0) + + return [gen_sol] + + +class Separable(SinglePatternODESolver): + r""" + Solves separable 1st order differential equations. + + This is any differential equation that can be written as `P(y) + \tfrac{dy}{dx} = Q(x)`. The solution can then just be found by + rearranging terms and integrating: `\int P(y) \,dy = \int Q(x) \,dx`. + This hint uses :py:meth:`sympy.simplify.simplify.separatevars` as its back + end, so if a separable equation is not caught by this solver, it is most + likely the fault of that function. + :py:meth:`~sympy.simplify.simplify.separatevars` is + smart enough to do most expansion and factoring necessary to convert a + separable equation `F(x, y)` into the proper form `P(x)\cdot{}Q(y)`. The + general solution is:: + + >>> from sympy import Function, dsolve, Eq, pprint + >>> from sympy.abc import x + >>> a, b, c, d, f = map(Function, ['a', 'b', 'c', 'd', 'f']) + >>> genform = Eq(a(x)*b(f(x))*f(x).diff(x), c(x)*d(f(x))) + >>> pprint(genform) + d + a(x)*b(f(x))*--(f(x)) = c(x)*d(f(x)) + dx + >>> pprint(dsolve(genform, f(x), hint='separable_Integral')) + f(x) + / / + | | + | b(y) | c(x) + | ---- dy = C1 + | ---- dx + | d(y) | a(x) + | | + / / + + Examples + ======== + + >>> from sympy import Function, dsolve, Eq + >>> from sympy.abc import x + >>> f = Function('f') + >>> pprint(dsolve(Eq(f(x)*f(x).diff(x) + x, 3*x*f(x)**2), f(x), + ... hint='separable', simplify=False)) + / 2 \ 2 + log\3*f (x) - 1/ x + ---------------- = C1 + -- + 6 2 + + References + ========== + + - M. Tenenbaum & H. Pollard, "Ordinary Differential Equations", + Dover 1963, pp. 52 + + # indirect doctest + + """ + hint = "separable" + has_integral = True + order = [1] + + def _wilds(self, f, x, order): + d = Wild('d', exclude=[f(x).diff(x), f(x).diff(x, 2)]) + e = Wild('e', exclude=[f(x).diff(x)]) + return d, e + + def _equation(self, fx, x, order): + d, e = self.wilds() + return d + e*fx.diff(x) + + def _verify(self, fx): + d, e = self.wilds_match() + self.y = Dummy('y') + x = self.ode_problem.sym + d = separatevars(d.subs(fx, self.y)) + e = separatevars(e.subs(fx, self.y)) + # m1[coeff]*m1[x]*m1[y] + m2[coeff]*m2[x]*m2[y]*y' + self.m1 = separatevars(d, dict=True, symbols=(x, self.y)) + self.m2 = separatevars(e, dict=True, symbols=(x, self.y)) + return bool(self.m1 and self.m2) + + def _get_match_object(self): + fx = self.ode_problem.func + x = self.ode_problem.sym + return self.m1, self.m2, x, fx + + def _get_general_solution(self, *, simplify_flag: bool = True): + m1, m2, x, fx = self._get_match_object() + (C1,) = self.ode_problem.get_numbered_constants(num=1) + int = Integral(m2['coeff']*m2[self.y]/m1[self.y], + (self.y, None, fx)) + gen_sol = Eq(int, Integral(-m1['coeff']*m1[x]/ + m2[x], x) + C1) + return [gen_sol] + + +class SeparableReduced(Separable): + r""" + Solves a differential equation that can be reduced to the separable form. + + The general form of this equation is + + .. math:: y' + (y/x) H(x^n y) = 0\text{}. + + This can be solved by substituting `u(y) = x^n y`. The equation then + reduces to the separable form `\frac{u'}{u (\mathrm{power} - H(u))} - + \frac{1}{x} = 0`. + + The general solution is: + + >>> from sympy import Function, dsolve, pprint + >>> from sympy.abc import x, n + >>> f, g = map(Function, ['f', 'g']) + >>> genform = f(x).diff(x) + (f(x)/x)*g(x**n*f(x)) + >>> pprint(genform) + / n \ + d f(x)*g\x *f(x)/ + --(f(x)) + --------------- + dx x + >>> pprint(dsolve(genform, hint='separable_reduced')) + n + x *f(x) + / + | + | 1 + | ------------ dy = C1 + log(x) + | y*(n - g(y)) + | + / + + See Also + ======== + :obj:`sympy.solvers.ode.single.Separable` + + Examples + ======== + + >>> from sympy import dsolve, Function, pprint + >>> from sympy.abc import x + >>> f = Function('f') + >>> d = f(x).diff(x) + >>> eq = (x - x**2*f(x))*d - f(x) + >>> dsolve(eq, hint='separable_reduced') + [Eq(f(x), (1 - sqrt(C1*x**2 + 1))/x), Eq(f(x), (sqrt(C1*x**2 + 1) + 1)/x)] + >>> pprint(dsolve(eq, hint='separable_reduced')) + ___________ ___________ + / 2 / 2 + 1 - \/ C1*x + 1 \/ C1*x + 1 + 1 + [f(x) = ------------------, f(x) = ------------------] + x x + + References + ========== + + - Joel Moses, "Symbolic Integration - The Stormy Decade", Communications + of the ACM, Volume 14, Number 8, August 1971, pp. 558 + """ + hint = "separable_reduced" + has_integral = True + order = [1] + + def _degree(self, expr, x): + # Made this function to calculate the degree of + # x in an expression. If expr will be of form + # x**p*y, (wheare p can be variables/rationals) then it + # will return p. + for val in expr: + if val.has(x): + if isinstance(val, Pow) and val.as_base_exp()[0] == x: + return (val.as_base_exp()[1]) + elif val == x: + return (val.as_base_exp()[1]) + else: + return self._degree(val.args, x) + return 0 + + def _powers(self, expr): + # this function will return all the different relative power of x w.r.t f(x). + # expr = x**p * f(x)**q then it will return {p/q}. + pows = set() + fx = self.ode_problem.func + x = self.ode_problem.sym + self.y = Dummy('y') + if isinstance(expr, Add): + exprs = expr.atoms(Add) + elif isinstance(expr, Mul): + exprs = expr.atoms(Mul) + elif isinstance(expr, Pow): + exprs = expr.atoms(Pow) + else: + exprs = {expr} + + for arg in exprs: + if arg.has(x): + _, u = arg.as_independent(x, fx) + pow = self._degree((u.subs(fx, self.y), ), x)/self._degree((u.subs(fx, self.y), ), self.y) + pows.add(pow) + return pows + + def _verify(self, fx): + num, den = self.wilds_match() + x = self.ode_problem.sym + factor = simplify(x/fx*num/den) + # Try representing factor in terms of x^n*y + # where n is lowest power of x in factor; + # first remove terms like sqrt(2)*3 from factor.atoms(Mul) + num, dem = factor.as_numer_denom() + num = expand(num) + dem = expand(dem) + pows = self._powers(num) + pows.update(self._powers(dem)) + pows = list(pows) + if(len(pows)==1) and pows[0]!=zoo: + self.t = Dummy('t') + self.r2 = {'t': self.t} + num = num.subs(x**pows[0]*fx, self.t) + dem = dem.subs(x**pows[0]*fx, self.t) + test = num/dem + free = test.free_symbols + if len(free) == 1 and free.pop() == self.t: + self.r2.update({'power' : pows[0], 'u' : test}) + return True + return False + return False + + def _get_match_object(self): + fx = self.ode_problem.func + x = self.ode_problem.sym + u = self.r2['u'].subs(self.r2['t'], self.y) + ycoeff = 1/(self.y*(self.r2['power'] - u)) + m1 = {self.y: 1, x: -1/x, 'coeff': 1} + m2 = {self.y: ycoeff, x: 1, 'coeff': 1} + return m1, m2, x, x**self.r2['power']*fx + + +class HomogeneousCoeffSubsDepDivIndep(SinglePatternODESolver): + r""" + Solves a 1st order differential equation with homogeneous coefficients + using the substitution `u_1 = \frac{\text{}}{\text{}}`. + + This is a differential equation + + .. math:: P(x, y) + Q(x, y) dy/dx = 0 + + such that `P` and `Q` are homogeneous and of the same order. A function + `F(x, y)` is homogeneous of order `n` if `F(x t, y t) = t^n F(x, y)`. + Equivalently, `F(x, y)` can be rewritten as `G(y/x)` or `H(x/y)`. See + also the docstring of :py:meth:`~sympy.solvers.ode.homogeneous_order`. + + If the coefficients `P` and `Q` in the differential equation above are + homogeneous functions of the same order, then it can be shown that the + substitution `y = u_1 x` (i.e. `u_1 = y/x`) will turn the differential + equation into an equation separable in the variables `x` and `u`. If + `h(u_1)` is the function that results from making the substitution `u_1 = + f(x)/x` on `P(x, f(x))` and `g(u_2)` is the function that results from the + substitution on `Q(x, f(x))` in the differential equation `P(x, f(x)) + + Q(x, f(x)) f'(x) = 0`, then the general solution is:: + + >>> from sympy import Function, dsolve, pprint + >>> from sympy.abc import x + >>> f, g, h = map(Function, ['f', 'g', 'h']) + >>> genform = g(f(x)/x) + h(f(x)/x)*f(x).diff(x) + >>> pprint(genform) + /f(x)\ /f(x)\ d + g|----| + h|----|*--(f(x)) + \ x / \ x / dx + >>> pprint(dsolve(genform, f(x), + ... hint='1st_homogeneous_coeff_subs_dep_div_indep_Integral')) + f(x) + ---- + x + / + | + | -h(u1) + log(x) = C1 + | ---------------- d(u1) + | u1*h(u1) + g(u1) + | + / + + Where `u_1 h(u_1) + g(u_1) \ne 0` and `x \ne 0`. + + See also the docstrings of + :obj:`~sympy.solvers.ode.single.HomogeneousCoeffBest` and + :obj:`~sympy.solvers.ode.single.HomogeneousCoeffSubsIndepDivDep`. + + Examples + ======== + + >>> from sympy import Function, dsolve + >>> from sympy.abc import x + >>> f = Function('f') + >>> pprint(dsolve(2*x*f(x) + (x**2 + f(x)**2)*f(x).diff(x), f(x), + ... hint='1st_homogeneous_coeff_subs_dep_div_indep', simplify=False)) + / 3 \ + |3*f(x) f (x)| + log|------ + -----| + | x 3 | + \ x / + log(x) = log(C1) - ------------------- + 3 + + References + ========== + + - https://en.wikipedia.org/wiki/Homogeneous_differential_equation + - M. Tenenbaum & H. Pollard, "Ordinary Differential Equations", + Dover 1963, pp. 59 + + # indirect doctest + + """ + hint = "1st_homogeneous_coeff_subs_dep_div_indep" + has_integral = True + order = [1] + + def _wilds(self, f, x, order): + d = Wild('d', exclude=[f(x).diff(x), f(x).diff(x, 2)]) + e = Wild('e', exclude=[f(x).diff(x)]) + return d, e + + def _equation(self, fx, x, order): + d, e = self.wilds() + return d + e*fx.diff(x) + + def _verify(self, fx): + self.d, self.e = self.wilds_match() + self.y = Dummy('y') + x = self.ode_problem.sym + self.d = separatevars(self.d.subs(fx, self.y)) + self.e = separatevars(self.e.subs(fx, self.y)) + ordera = homogeneous_order(self.d, x, self.y) + orderb = homogeneous_order(self.e, x, self.y) + if ordera == orderb and ordera is not None: + self.u = Dummy('u') + if simplify((self.d + self.u*self.e).subs({x: 1, self.y: self.u})) != 0: + return True + return False + return False + + def _get_match_object(self): + fx = self.ode_problem.func + x = self.ode_problem.sym + self.u1 = Dummy('u1') + xarg = 0 + yarg = 0 + return [self.d, self.e, fx, x, self.u, self.u1, self.y, xarg, yarg] + + def _get_general_solution(self, *, simplify_flag: bool = True): + d, e, fx, x, u, u1, y, xarg, yarg = self._get_match_object() + (C1,) = self.ode_problem.get_numbered_constants(num=1) + int = Integral( + (-e/(d + u1*e)).subs({x: 1, y: u1}), + (u1, None, fx/x)) + sol = logcombine(Eq(log(x), int + log(C1)), force=True) + gen_sol = sol.subs(fx, u).subs(((u, u - yarg), (x, x - xarg), (u, fx))) + return [gen_sol] + + +class HomogeneousCoeffSubsIndepDivDep(SinglePatternODESolver): + r""" + Solves a 1st order differential equation with homogeneous coefficients + using the substitution `u_2 = \frac{\text{}}{\text{}}`. + + This is a differential equation + + .. math:: P(x, y) + Q(x, y) dy/dx = 0 + + such that `P` and `Q` are homogeneous and of the same order. A function + `F(x, y)` is homogeneous of order `n` if `F(x t, y t) = t^n F(x, y)`. + Equivalently, `F(x, y)` can be rewritten as `G(y/x)` or `H(x/y)`. See + also the docstring of :py:meth:`~sympy.solvers.ode.homogeneous_order`. + + If the coefficients `P` and `Q` in the differential equation above are + homogeneous functions of the same order, then it can be shown that the + substitution `x = u_2 y` (i.e. `u_2 = x/y`) will turn the differential + equation into an equation separable in the variables `y` and `u_2`. If + `h(u_2)` is the function that results from making the substitution `u_2 = + x/f(x)` on `P(x, f(x))` and `g(u_2)` is the function that results from the + substitution on `Q(x, f(x))` in the differential equation `P(x, f(x)) + + Q(x, f(x)) f'(x) = 0`, then the general solution is: + + >>> from sympy import Function, dsolve, pprint + >>> from sympy.abc import x + >>> f, g, h = map(Function, ['f', 'g', 'h']) + >>> genform = g(x/f(x)) + h(x/f(x))*f(x).diff(x) + >>> pprint(genform) + / x \ / x \ d + g|----| + h|----|*--(f(x)) + \f(x)/ \f(x)/ dx + >>> pprint(dsolve(genform, f(x), + ... hint='1st_homogeneous_coeff_subs_indep_div_dep_Integral')) + x + ---- + f(x) + / + | + | -g(u1) + | ---------------- d(u1) + | u1*g(u1) + h(u1) + | + / + + f(x) = C1*e + + Where `u_1 g(u_1) + h(u_1) \ne 0` and `f(x) \ne 0`. + + See also the docstrings of + :obj:`~sympy.solvers.ode.single.HomogeneousCoeffBest` and + :obj:`~sympy.solvers.ode.single.HomogeneousCoeffSubsDepDivIndep`. + + Examples + ======== + + >>> from sympy import Function, pprint, dsolve + >>> from sympy.abc import x + >>> f = Function('f') + >>> pprint(dsolve(2*x*f(x) + (x**2 + f(x)**2)*f(x).diff(x), f(x), + ... hint='1st_homogeneous_coeff_subs_indep_div_dep', + ... simplify=False)) + / 2 \ + |3*x | + log|----- + 1| + | 2 | + \f (x) / + log(f(x)) = log(C1) - -------------- + 3 + + References + ========== + + - https://en.wikipedia.org/wiki/Homogeneous_differential_equation + - M. Tenenbaum & H. Pollard, "Ordinary Differential Equations", + Dover 1963, pp. 59 + + # indirect doctest + + """ + hint = "1st_homogeneous_coeff_subs_indep_div_dep" + has_integral = True + order = [1] + + def _wilds(self, f, x, order): + d = Wild('d', exclude=[f(x).diff(x), f(x).diff(x, 2)]) + e = Wild('e', exclude=[f(x).diff(x)]) + return d, e + + def _equation(self, fx, x, order): + d, e = self.wilds() + return d + e*fx.diff(x) + + def _verify(self, fx): + self.d, self.e = self.wilds_match() + self.y = Dummy('y') + x = self.ode_problem.sym + self.d = separatevars(self.d.subs(fx, self.y)) + self.e = separatevars(self.e.subs(fx, self.y)) + ordera = homogeneous_order(self.d, x, self.y) + orderb = homogeneous_order(self.e, x, self.y) + if ordera == orderb and ordera is not None: + self.u = Dummy('u') + if simplify((self.e + self.u*self.d).subs({x: self.u, self.y: 1})) != 0: + return True + return False + return False + + def _get_match_object(self): + fx = self.ode_problem.func + x = self.ode_problem.sym + self.u1 = Dummy('u1') + xarg = 0 + yarg = 0 + return [self.d, self.e, fx, x, self.u, self.u1, self.y, xarg, yarg] + + def _get_general_solution(self, *, simplify_flag: bool = True): + d, e, fx, x, u, u1, y, xarg, yarg = self._get_match_object() + (C1,) = self.ode_problem.get_numbered_constants(num=1) + int = Integral(simplify((-d/(e + u1*d)).subs({x: u1, y: 1})), (u1, None, x/fx)) # type: ignore + sol = logcombine(Eq(log(fx), int + log(C1)), force=True) + gen_sol = sol.subs(fx, u).subs(((u, u - yarg), (x, x - xarg), (u, fx))) + return [gen_sol] + + +class HomogeneousCoeffBest(HomogeneousCoeffSubsIndepDivDep, HomogeneousCoeffSubsDepDivIndep): + r""" + Returns the best solution to an ODE from the two hints + ``1st_homogeneous_coeff_subs_dep_div_indep`` and + ``1st_homogeneous_coeff_subs_indep_div_dep``. + + This is as determined by :py:meth:`~sympy.solvers.ode.ode.ode_sol_simplicity`. + + See the + :obj:`~sympy.solvers.ode.single.HomogeneousCoeffSubsIndepDivDep` + and + :obj:`~sympy.solvers.ode.single.HomogeneousCoeffSubsDepDivIndep` + docstrings for more information on these hints. Note that there is no + ``ode_1st_homogeneous_coeff_best_Integral`` hint. + + Examples + ======== + + >>> from sympy import Function, dsolve, pprint + >>> from sympy.abc import x + >>> f = Function('f') + >>> pprint(dsolve(2*x*f(x) + (x**2 + f(x)**2)*f(x).diff(x), f(x), + ... hint='1st_homogeneous_coeff_best', simplify=False)) + / 2 \ + |3*x | + log|----- + 1| + | 2 | + \f (x) / + log(f(x)) = log(C1) - -------------- + 3 + + References + ========== + + - https://en.wikipedia.org/wiki/Homogeneous_differential_equation + - M. Tenenbaum & H. Pollard, "Ordinary Differential Equations", + Dover 1963, pp. 59 + + # indirect doctest + + """ + hint = "1st_homogeneous_coeff_best" + has_integral = False + order = [1] + + def _verify(self, fx): + return HomogeneousCoeffSubsIndepDivDep._verify(self, fx) and \ + HomogeneousCoeffSubsDepDivIndep._verify(self, fx) + + def _get_general_solution(self, *, simplify_flag: bool = True): + # There are two substitutions that solve the equation, u1=y/x and u2=x/y + # # They produce different integrals, so try them both and see which + # # one is easier + sol1 = HomogeneousCoeffSubsIndepDivDep._get_general_solution(self) + sol2 = HomogeneousCoeffSubsDepDivIndep._get_general_solution(self) + fx = self.ode_problem.func + if simplify_flag: + sol1 = odesimp(self.ode_problem.eq, *sol1, fx, "1st_homogeneous_coeff_subs_indep_div_dep") + sol2 = odesimp(self.ode_problem.eq, *sol2, fx, "1st_homogeneous_coeff_subs_dep_div_indep") + # XXX: not simplify should be not simplify_flag. mypy correctly complains + return min([sol1, sol2], key=lambda x: ode_sol_simplicity(x, fx, trysolving=not simplify)) # type: ignore + + +class LinearCoefficients(HomogeneousCoeffBest): + r""" + Solves a differential equation with linear coefficients. + + The general form of a differential equation with linear coefficients is + + .. math:: y' + F\left(\!\frac{a_1 x + b_1 y + c_1}{a_2 x + b_2 y + + c_2}\!\right) = 0\text{,} + + where `a_1`, `b_1`, `c_1`, `a_2`, `b_2`, `c_2` are constants and `a_1 b_2 + - a_2 b_1 \ne 0`. + + This can be solved by substituting: + + .. math:: x = x' + \frac{b_2 c_1 - b_1 c_2}{a_2 b_1 - a_1 b_2} + + y = y' + \frac{a_1 c_2 - a_2 c_1}{a_2 b_1 - a_1 + b_2}\text{.} + + This substitution reduces the equation to a homogeneous differential + equation. + + See Also + ======== + :obj:`sympy.solvers.ode.single.HomogeneousCoeffBest` + :obj:`sympy.solvers.ode.single.HomogeneousCoeffSubsIndepDivDep` + :obj:`sympy.solvers.ode.single.HomogeneousCoeffSubsDepDivIndep` + + Examples + ======== + + >>> from sympy import dsolve, Function, pprint + >>> from sympy.abc import x + >>> f = Function('f') + >>> df = f(x).diff(x) + >>> eq = (x + f(x) + 1)*df + (f(x) - 6*x + 1) + >>> dsolve(eq, hint='linear_coefficients') + [Eq(f(x), -x - sqrt(C1 + 7*x**2) - 1), Eq(f(x), -x + sqrt(C1 + 7*x**2) - 1)] + >>> pprint(dsolve(eq, hint='linear_coefficients')) + ___________ ___________ + / 2 / 2 + [f(x) = -x - \/ C1 + 7*x - 1, f(x) = -x + \/ C1 + 7*x - 1] + + + References + ========== + + - Joel Moses, "Symbolic Integration - The Stormy Decade", Communications + of the ACM, Volume 14, Number 8, August 1971, pp. 558 + """ + hint = "linear_coefficients" + has_integral = True + order = [1] + + def _wilds(self, f, x, order): + d = Wild('d', exclude=[f(x).diff(x), f(x).diff(x, 2)]) + e = Wild('e', exclude=[f(x).diff(x)]) + return d, e + + def _equation(self, fx, x, order): + d, e = self.wilds() + return d + e*fx.diff(x) + + def _verify(self, fx): + self.d, self.e = self.wilds_match() + a, b = self.wilds() + F = self.d/self.e + x = self.ode_problem.sym + params = self._linear_coeff_match(F, fx) + if params: + self.xarg, self.yarg = params + u = Dummy('u') + t = Dummy('t') + self.y = Dummy('y') + # Dummy substitution for df and f(x). + dummy_eq = self.ode_problem.eq.subs(((fx.diff(x), t), (fx, u))) + reps = ((x, x + self.xarg), (u, u + self.yarg), (t, fx.diff(x)), (u, fx)) + dummy_eq = simplify(dummy_eq.subs(reps)) + # get the re-cast values for e and d + r2 = collect(expand(dummy_eq), [fx.diff(x), fx]).match(a*fx.diff(x) + b) + if r2: + self.d, self.e = r2[b], r2[a] + orderd = homogeneous_order(self.d, x, fx) + ordere = homogeneous_order(self.e, x, fx) + if orderd == ordere and orderd is not None: + self.d = self.d.subs(fx, self.y) + self.e = self.e.subs(fx, self.y) + return True + return False + return False + + def _linear_coeff_match(self, expr, func): + r""" + Helper function to match hint ``linear_coefficients``. + + Matches the expression to the form `(a_1 x + b_1 f(x) + c_1)/(a_2 x + b_2 + f(x) + c_2)` where the following conditions hold: + + 1. `a_1`, `b_1`, `c_1`, `a_2`, `b_2`, `c_2` are Rationals; + 2. `c_1` or `c_2` are not equal to zero; + 3. `a_2 b_1 - a_1 b_2` is not equal to zero. + + Return ``xarg``, ``yarg`` where + + 1. ``xarg`` = `(b_2 c_1 - b_1 c_2)/(a_2 b_1 - a_1 b_2)` + 2. ``yarg`` = `(a_1 c_2 - a_2 c_1)/(a_2 b_1 - a_1 b_2)` + + + Examples + ======== + + >>> from sympy import Function, sin + >>> from sympy.abc import x + >>> from sympy.solvers.ode.single import LinearCoefficients + >>> f = Function('f') + >>> eq = (-25*f(x) - 8*x + 62)/(4*f(x) + 11*x - 11) + >>> obj = LinearCoefficients(eq) + >>> obj._linear_coeff_match(eq, f(x)) + (1/9, 22/9) + >>> eq = sin((-5*f(x) - 8*x + 6)/(4*f(x) + x - 1)) + >>> obj = LinearCoefficients(eq) + >>> obj._linear_coeff_match(eq, f(x)) + (19/27, 2/27) + >>> eq = sin(f(x)/x) + >>> obj = LinearCoefficients(eq) + >>> obj._linear_coeff_match(eq, f(x)) + + """ + f = func.func + x = func.args[0] + def abc(eq): + r''' + Internal function of _linear_coeff_match + that returns Rationals a, b, c + if eq is a*x + b*f(x) + c, else None. + ''' + eq = _mexpand(eq) + c = eq.as_independent(x, f(x), as_Add=True)[0] + if not c.is_Rational: + return + a = eq.coeff(x) + if not a.is_Rational: + return + b = eq.coeff(f(x)) + if not b.is_Rational: + return + if eq == a*x + b*f(x) + c: + return a, b, c + + def match(arg): + r''' + Internal function of _linear_coeff_match that returns Rationals a1, + b1, c1, a2, b2, c2 and a2*b1 - a1*b2 of the expression (a1*x + b1*f(x) + + c1)/(a2*x + b2*f(x) + c2) if one of c1 or c2 and a2*b1 - a1*b2 is + non-zero, else None. + ''' + n, d = arg.together().as_numer_denom() + m = abc(n) + if m is not None: + a1, b1, c1 = m + m = abc(d) + if m is not None: + a2, b2, c2 = m + d = a2*b1 - a1*b2 + if (c1 or c2) and d: + return a1, b1, c1, a2, b2, c2, d + + m = [fi.args[0] for fi in expr.atoms(Function) if fi.func != f and + len(fi.args) == 1 and not fi.args[0].is_Function] or {expr} + m1 = match(m.pop()) + if m1 and all(match(mi) == m1 for mi in m): + a1, b1, c1, a2, b2, c2, denom = m1 + return (b2*c1 - b1*c2)/denom, (a1*c2 - a2*c1)/denom + + def _get_match_object(self): + fx = self.ode_problem.func + x = self.ode_problem.sym + self.u1 = Dummy('u1') + u = Dummy('u') + return [self.d, self.e, fx, x, u, self.u1, self.y, self.xarg, self.yarg] + + +class NthOrderReducible(SingleODESolver): + r""" + Solves ODEs that only involve derivatives of the dependent variable using + a substitution of the form `f^n(x) = g(x)`. + + For example any second order ODE of the form `f''(x) = h(f'(x), x)` can be + transformed into a pair of 1st order ODEs `g'(x) = h(g(x), x)` and + `f'(x) = g(x)`. Usually the 1st order ODE for `g` is easier to solve. If + that gives an explicit solution for `g` then `f` is found simply by + integration. + + + Examples + ======== + + >>> from sympy import Function, dsolve, Eq + >>> from sympy.abc import x + >>> f = Function('f') + >>> eq = Eq(x*f(x).diff(x)**2 + f(x).diff(x, 2), 0) + >>> dsolve(eq, f(x), hint='nth_order_reducible') + ... # doctest: +NORMALIZE_WHITESPACE + Eq(f(x), C1 - sqrt(-1/C2)*log(-C2*sqrt(-1/C2) + x) + sqrt(-1/C2)*log(C2*sqrt(-1/C2) + x)) + + """ + hint = "nth_order_reducible" + has_integral = False + + def _matches(self): + # Any ODE that can be solved with a substitution and + # repeated integration e.g.: + # `d^2/dx^2(y) + x*d/dx(y) = constant + #f'(x) must be finite for this to work + eq = self.ode_problem.eq_preprocessed + func = self.ode_problem.func + x = self.ode_problem.sym + r""" + Matches any differential equation that can be rewritten with a smaller + order. Only derivatives of ``func`` alone, wrt a single variable, + are considered, and only in them should ``func`` appear. + """ + # ODE only handles functions of 1 variable so this affirms that state + assert len(func.args) == 1 + vc = [d.variable_count[0] for d in eq.atoms(Derivative) + if d.expr == func and len(d.variable_count) == 1] + ords = [c for v, c in vc if v == x] + if len(ords) < 2: + return False + self.smallest = min(ords) + # make sure func does not appear outside of derivatives + D = Dummy() + if eq.subs(func.diff(x, self.smallest), D).has(func): + return False + return True + + def _get_general_solution(self, *, simplify_flag: bool = True): + eq = self.ode_problem.eq + f = self.ode_problem.func.func + x = self.ode_problem.sym + n = self.smallest + # get a unique function name for g + names = [a.name for a in eq.atoms(AppliedUndef)] + while True: + name = Dummy().name + if name not in names: + g = Function(name) + break + w = f(x).diff(x, n) + geq = eq.subs(w, g(x)) + gsol = dsolve(geq, g(x)) + + if not isinstance(gsol, list): + gsol = [gsol] + + # Might be multiple solutions to the reduced ODE: + fsol = [] + for gsoli in gsol: + fsoli = dsolve(gsoli.subs(g(x), w), f(x)) # or do integration n times + fsol.append(fsoli) + + return fsol + + +class SecondHypergeometric(SingleODESolver): + r""" + Solves 2nd order linear differential equations. + + It computes special function solutions which can be expressed using the + 2F1, 1F1 or 0F1 hypergeometric functions. + + .. math:: y'' + A(x) y' + B(x) y = 0\text{,} + + where `A` and `B` are rational functions. + + These kinds of differential equations have solution of non-Liouvillian form. + + Given linear ODE can be obtained from 2F1 given by + + .. math:: (x^2 - x) y'' + ((a + b + 1) x - c) y' + b a y = 0\text{,} + + where {a, b, c} are arbitrary constants. + + Notes + ===== + + The algorithm should find any solution of the form + + .. math:: y = P(x) _pF_q(..; ..;\frac{\alpha x^k + \beta}{\gamma x^k + \delta})\text{,} + + where pFq is any of 2F1, 1F1 or 0F1 and `P` is an "arbitrary function". + Currently only the 2F1 case is implemented in SymPy but the other cases are + described in the paper and could be implemented in future (contributions + welcome!). + + + Examples + ======== + + >>> from sympy import Function, dsolve, pprint + >>> from sympy.abc import x + >>> f = Function('f') + >>> eq = (x*x - x)*f(x).diff(x,2) + (5*x - 1)*f(x).diff(x) + 4*f(x) + >>> pprint(dsolve(eq, f(x), '2nd_hypergeometric')) + _ + / / 4 \\ |_ /-1, -1 | \ + |C1 + C2*|log(x) + -----||* | | | x| + \ \ x + 1// 2 1 \ 1 | / + f(x) = -------------------------------------------- + 3 + (x - 1) + + + References + ========== + + - "Non-Liouvillian solutions for second order linear ODEs" by L. Chan, E.S. Cheb-Terrab + + """ + hint = "2nd_hypergeometric" + has_integral = True + + def _matches(self): + eq = self.ode_problem.eq_preprocessed + func = self.ode_problem.func + r = match_2nd_hypergeometric(eq, func) + self.match_object = None + if r: + A, B = r + d = equivalence_hypergeometric(A, B, func) + if d: + if d['type'] == "2F1": + self.match_object = match_2nd_2F1_hypergeometric(d['I0'], d['k'], d['sing_point'], func) + if self.match_object is not None: + self.match_object.update({'A':A, 'B':B}) + # We can extend it for 1F1 and 0F1 type also. + return self.match_object is not None + + def _get_general_solution(self, *, simplify_flag: bool = True): + eq = self.ode_problem.eq + func = self.ode_problem.func + if self.match_object['type'] == "2F1": + sol = get_sol_2F1_hypergeometric(eq, func, self.match_object) + if sol is None: + raise NotImplementedError("The given ODE " + str(eq) + " cannot be solved by" + + " the hypergeometric method") + + return [sol] + + +class NthLinearConstantCoeffHomogeneous(SingleODESolver): + r""" + Solves an `n`\th order linear homogeneous differential equation with + constant coefficients. + + This is an equation of the form + + .. math:: a_n f^{(n)}(x) + a_{n-1} f^{(n-1)}(x) + \cdots + a_1 f'(x) + + a_0 f(x) = 0\text{.} + + These equations can be solved in a general manner, by taking the roots of + the characteristic equation `a_n m^n + a_{n-1} m^{n-1} + \cdots + a_1 m + + a_0 = 0`. The solution will then be the sum of `C_n x^i e^{r x}` terms, + for each where `C_n` is an arbitrary constant, `r` is a root of the + characteristic equation and `i` is one of each from 0 to the multiplicity + of the root - 1 (for example, a root 3 of multiplicity 2 would create the + terms `C_1 e^{3 x} + C_2 x e^{3 x}`). The exponential is usually expanded + for complex roots using Euler's equation `e^{I x} = \cos(x) + I \sin(x)`. + Complex roots always come in conjugate pairs in polynomials with real + coefficients, so the two roots will be represented (after simplifying the + constants) as `e^{a x} \left(C_1 \cos(b x) + C_2 \sin(b x)\right)`. + + If SymPy cannot find exact roots to the characteristic equation, a + :py:class:`~sympy.polys.rootoftools.ComplexRootOf` instance will be return + instead. + + >>> from sympy import Function, dsolve + >>> from sympy.abc import x + >>> f = Function('f') + >>> dsolve(f(x).diff(x, 5) + 10*f(x).diff(x) - 2*f(x), f(x), + ... hint='nth_linear_constant_coeff_homogeneous') + ... # doctest: +NORMALIZE_WHITESPACE + Eq(f(x), C5*exp(x*CRootOf(_x**5 + 10*_x - 2, 0)) + + (C1*sin(x*im(CRootOf(_x**5 + 10*_x - 2, 1))) + + C2*cos(x*im(CRootOf(_x**5 + 10*_x - 2, 1))))*exp(x*re(CRootOf(_x**5 + 10*_x - 2, 1))) + + (C3*sin(x*im(CRootOf(_x**5 + 10*_x - 2, 3))) + + C4*cos(x*im(CRootOf(_x**5 + 10*_x - 2, 3))))*exp(x*re(CRootOf(_x**5 + 10*_x - 2, 3)))) + + Note that because this method does not involve integration, there is no + ``nth_linear_constant_coeff_homogeneous_Integral`` hint. + + Examples + ======== + + >>> from sympy import Function, dsolve, pprint + >>> from sympy.abc import x + >>> f = Function('f') + >>> pprint(dsolve(f(x).diff(x, 4) + 2*f(x).diff(x, 3) - + ... 2*f(x).diff(x, 2) - 6*f(x).diff(x) + 5*f(x), f(x), + ... hint='nth_linear_constant_coeff_homogeneous')) + x -2*x + f(x) = (C1 + C2*x)*e + (C3*sin(x) + C4*cos(x))*e + + References + ========== + + - https://en.wikipedia.org/wiki/Linear_differential_equation section: + Nonhomogeneous_equation_with_constant_coefficients + - M. Tenenbaum & H. Pollard, "Ordinary Differential Equations", + Dover 1963, pp. 211 + + # indirect doctest + + """ + hint = "nth_linear_constant_coeff_homogeneous" + has_integral = False + + def _matches(self): + eq = self.ode_problem.eq_high_order_free + func = self.ode_problem.func + order = self.ode_problem.order + x = self.ode_problem.sym + self.r = self.ode_problem.get_linear_coefficients(eq, func, order) + if order and self.r and not any(self.r[i].has(x) for i in self.r if i >= 0): + if not self.r[-1]: + return True + else: + return False + return False + + def _get_general_solution(self, *, simplify_flag: bool = True): + fx = self.ode_problem.func + order = self.ode_problem.order + roots, collectterms = _get_const_characteristic_eq_sols(self.r, fx, order) + # A generator of constants + constants = self.ode_problem.get_numbered_constants(num=len(roots)) + gsol_rhs = Add(*[i*j for (i, j) in zip(constants, roots)]) + gsol = Eq(fx, gsol_rhs) + if simplify_flag: + gsol = _get_simplified_sol([gsol], fx, collectterms) + + return [gsol] + + +class NthLinearConstantCoeffVariationOfParameters(SingleODESolver): + r""" + Solves an `n`\th order linear differential equation with constant + coefficients using the method of variation of parameters. + + This method works on any differential equations of the form + + .. math:: f^{(n)}(x) + a_{n-1} f^{(n-1)}(x) + \cdots + a_1 f'(x) + a_0 + f(x) = P(x)\text{.} + + This method works by assuming that the particular solution takes the form + + .. math:: \sum_{x=1}^{n} c_i(x) y_i(x)\text{,} + + where `y_i` is the `i`\th solution to the homogeneous equation. The + solution is then solved using Wronskian's and Cramer's Rule. The + particular solution is given by + + .. math:: \sum_{x=1}^n \left( \int \frac{W_i(x)}{W(x)} \,dx + \right) y_i(x) \text{,} + + where `W(x)` is the Wronskian of the fundamental system (the system of `n` + linearly independent solutions to the homogeneous equation), and `W_i(x)` + is the Wronskian of the fundamental system with the `i`\th column replaced + with `[0, 0, \cdots, 0, P(x)]`. + + This method is general enough to solve any `n`\th order inhomogeneous + linear differential equation with constant coefficients, but sometimes + SymPy cannot simplify the Wronskian well enough to integrate it. If this + method hangs, try using the + ``nth_linear_constant_coeff_variation_of_parameters_Integral`` hint and + simplifying the integrals manually. Also, prefer using + ``nth_linear_constant_coeff_undetermined_coefficients`` when it + applies, because it does not use integration, making it faster and more + reliable. + + Warning, using simplify=False with + 'nth_linear_constant_coeff_variation_of_parameters' in + :py:meth:`~sympy.solvers.ode.dsolve` may cause it to hang, because it will + not attempt to simplify the Wronskian before integrating. It is + recommended that you only use simplify=False with + 'nth_linear_constant_coeff_variation_of_parameters_Integral' for this + method, especially if the solution to the homogeneous equation has + trigonometric functions in it. + + Examples + ======== + + >>> from sympy import Function, dsolve, pprint, exp, log + >>> from sympy.abc import x + >>> f = Function('f') + >>> pprint(dsolve(f(x).diff(x, 3) - 3*f(x).diff(x, 2) + + ... 3*f(x).diff(x) - f(x) - exp(x)*log(x), f(x), + ... hint='nth_linear_constant_coeff_variation_of_parameters')) + / / / x*log(x) 11*x\\\ x + f(x) = |C1 + x*|C2 + x*|C3 + -------- - ----|||*e + \ \ \ 6 36 /// + + References + ========== + + - https://en.wikipedia.org/wiki/Variation_of_parameters + - https://planetmath.org/VariationOfParameters + - M. Tenenbaum & H. Pollard, "Ordinary Differential Equations", + Dover 1963, pp. 233 + + # indirect doctest + + """ + hint = "nth_linear_constant_coeff_variation_of_parameters" + has_integral = True + + def _matches(self): + eq = self.ode_problem.eq_high_order_free + func = self.ode_problem.func + order = self.ode_problem.order + x = self.ode_problem.sym + self.r = self.ode_problem.get_linear_coefficients(eq, func, order) + + if order and self.r and not any(self.r[i].has(x) for i in self.r if i >= 0): + if self.r[-1]: + return True + else: + return False + return False + + def _get_general_solution(self, *, simplify_flag: bool = True): + eq = self.ode_problem.eq_high_order_free + f = self.ode_problem.func.func + x = self.ode_problem.sym + order = self.ode_problem.order + roots, collectterms = _get_const_characteristic_eq_sols(self.r, f(x), order) + # A generator of constants + constants = self.ode_problem.get_numbered_constants(num=len(roots)) + homogen_sol_rhs = Add(*[i*j for (i, j) in zip(constants, roots)]) + homogen_sol = Eq(f(x), homogen_sol_rhs) + homogen_sol = _solve_variation_of_parameters(eq, f(x), roots, homogen_sol, order, self.r, simplify_flag) + if simplify_flag: + homogen_sol = _get_simplified_sol([homogen_sol], f(x), collectterms) + return [homogen_sol] + + +class NthLinearConstantCoeffUndeterminedCoefficients(SingleODESolver): + r""" + Solves an `n`\th order linear differential equation with constant + coefficients using the method of undetermined coefficients. + + This method works on differential equations of the form + + .. math:: a_n f^{(n)}(x) + a_{n-1} f^{(n-1)}(x) + \cdots + a_1 f'(x) + + a_0 f(x) = P(x)\text{,} + + where `P(x)` is a function that has a finite number of linearly + independent derivatives. + + Functions that fit this requirement are finite sums functions of the form + `a x^i e^{b x} \sin(c x + d)` or `a x^i e^{b x} \cos(c x + d)`, where `i` + is a non-negative integer and `a`, `b`, `c`, and `d` are constants. For + example any polynomial in `x`, functions like `x^2 e^{2 x}`, `x \sin(x)`, + and `e^x \cos(x)` can all be used. Products of `\sin`'s and `\cos`'s have + a finite number of derivatives, because they can be expanded into `\sin(a + x)` and `\cos(b x)` terms. However, SymPy currently cannot do that + expansion, so you will need to manually rewrite the expression in terms of + the above to use this method. So, for example, you will need to manually + convert `\sin^2(x)` into `(1 + \cos(2 x))/2` to properly apply the method + of undetermined coefficients on it. + + This method works by creating a trial function from the expression and all + of its linear independent derivatives and substituting them into the + original ODE. The coefficients for each term will be a system of linear + equations, which are be solved for and substituted, giving the solution. + If any of the trial functions are linearly dependent on the solution to + the homogeneous equation, they are multiplied by sufficient `x` to make + them linearly independent. + + Examples + ======== + + >>> from sympy import Function, dsolve, pprint, exp, cos + >>> from sympy.abc import x + >>> f = Function('f') + >>> pprint(dsolve(f(x).diff(x, 2) + 2*f(x).diff(x) + f(x) - + ... 4*exp(-x)*x**2 + cos(2*x), f(x), + ... hint='nth_linear_constant_coeff_undetermined_coefficients')) + / / 3\\ + | | x || -x 4*sin(2*x) 3*cos(2*x) + f(x) = |C1 + x*|C2 + --||*e - ---------- + ---------- + \ \ 3 // 25 25 + + References + ========== + + - https://en.wikipedia.org/wiki/Method_of_undetermined_coefficients + - M. Tenenbaum & H. Pollard, "Ordinary Differential Equations", + Dover 1963, pp. 221 + + # indirect doctest + + """ + hint = "nth_linear_constant_coeff_undetermined_coefficients" + has_integral = False + + def _matches(self): + eq = self.ode_problem.eq_high_order_free + func = self.ode_problem.func + order = self.ode_problem.order + x = self.ode_problem.sym + self.r = self.ode_problem.get_linear_coefficients(eq, func, order) + does_match = False + if order and self.r and not any(self.r[i].has(x) for i in self.r if i >= 0): + if self.r[-1]: + eq_homogeneous = Add(eq, -self.r[-1]) + undetcoeff = _undetermined_coefficients_match(self.r[-1], x, func, eq_homogeneous) + if undetcoeff['test']: + self.trialset = undetcoeff['trialset'] + does_match = True + return does_match + + def _get_general_solution(self, *, simplify_flag: bool = True): + eq = self.ode_problem.eq + f = self.ode_problem.func.func + x = self.ode_problem.sym + order = self.ode_problem.order + roots, collectterms = _get_const_characteristic_eq_sols(self.r, f(x), order) + # A generator of constants + constants = self.ode_problem.get_numbered_constants(num=len(roots)) + homogen_sol_rhs = Add(*[i*j for (i, j) in zip(constants, roots)]) + homogen_sol = Eq(f(x), homogen_sol_rhs) + self.r.update({'list': roots, 'sol': homogen_sol, 'simpliy_flag': simplify_flag}) + gsol = _solve_undetermined_coefficients(eq, f(x), order, self.r, self.trialset) + if simplify_flag: + gsol = _get_simplified_sol([gsol], f(x), collectterms) + return [gsol] + + +class NthLinearEulerEqHomogeneous(SingleODESolver): + r""" + Solves an `n`\th order linear homogeneous variable-coefficient + Cauchy-Euler equidimensional ordinary differential equation. + + This is an equation with form `0 = a_0 f(x) + a_1 x f'(x) + a_2 x^2 f''(x) + \cdots`. + + These equations can be solved in a general manner, by substituting + solutions of the form `f(x) = x^r`, and deriving a characteristic equation + for `r`. When there are repeated roots, we include extra terms of the + form `C_{r k} \ln^k(x) x^r`, where `C_{r k}` is an arbitrary integration + constant, `r` is a root of the characteristic equation, and `k` ranges + over the multiplicity of `r`. In the cases where the roots are complex, + solutions of the form `C_1 x^a \sin(b \log(x)) + C_2 x^a \cos(b \log(x))` + are returned, based on expansions with Euler's formula. The general + solution is the sum of the terms found. If SymPy cannot find exact roots + to the characteristic equation, a + :py:obj:`~.ComplexRootOf` instance will be returned + instead. + + >>> from sympy import Function, dsolve + >>> from sympy.abc import x + >>> f = Function('f') + >>> dsolve(4*x**2*f(x).diff(x, 2) + f(x), f(x), + ... hint='nth_linear_euler_eq_homogeneous') + ... # doctest: +NORMALIZE_WHITESPACE + Eq(f(x), sqrt(x)*(C1 + C2*log(x))) + + Note that because this method does not involve integration, there is no + ``nth_linear_euler_eq_homogeneous_Integral`` hint. + + The following is for internal use: + + - ``returns = 'sol'`` returns the solution to the ODE. + - ``returns = 'list'`` returns a list of linearly independent solutions, + corresponding to the fundamental solution set, for use with non + homogeneous solution methods like variation of parameters and + undetermined coefficients. Note that, though the solutions should be + linearly independent, this function does not explicitly check that. You + can do ``assert simplify(wronskian(sollist)) != 0`` to check for linear + independence. Also, ``assert len(sollist) == order`` will need to pass. + - ``returns = 'both'``, return a dictionary ``{'sol': , + 'list': }``. + + Examples + ======== + + >>> from sympy import Function, dsolve, pprint + >>> from sympy.abc import x + >>> f = Function('f') + >>> eq = f(x).diff(x, 2)*x**2 - 4*f(x).diff(x)*x + 6*f(x) + >>> pprint(dsolve(eq, f(x), + ... hint='nth_linear_euler_eq_homogeneous')) + 2 + f(x) = x *(C1 + C2*x) + + References + ========== + + - https://en.wikipedia.org/wiki/Cauchy%E2%80%93Euler_equation + - C. Bender & S. Orszag, "Advanced Mathematical Methods for Scientists and + Engineers", Springer 1999, pp. 12 + + # indirect doctest + + """ + hint = "nth_linear_euler_eq_homogeneous" + has_integral = False + + def _matches(self): + eq = self.ode_problem.eq_preprocessed + f = self.ode_problem.func.func + order = self.ode_problem.order + x = self.ode_problem.sym + match = self.ode_problem.get_linear_coefficients(eq, f(x), order) + self.r = None + does_match = False + + if order and match: + coeff = match[order] + factor = x**order / coeff + self.r = {i: factor*match[i] for i in match} + if self.r and all(_test_term(self.r[i], f(x), i) for i in + self.r if i >= 0): + if not self.r[-1]: + does_match = True + return does_match + + def _get_general_solution(self, *, simplify_flag: bool = True): + fx = self.ode_problem.func + eq = self.ode_problem.eq + homogen_sol = _get_euler_characteristic_eq_sols(eq, fx, self.r)[0] + return [homogen_sol] + + +class NthLinearEulerEqNonhomogeneousVariationOfParameters(SingleODESolver): + r""" + Solves an `n`\th order linear non homogeneous Cauchy-Euler equidimensional + ordinary differential equation using variation of parameters. + + This is an equation with form `g(x) = a_0 f(x) + a_1 x f'(x) + a_2 x^2 f''(x) + \cdots`. + + This method works by assuming that the particular solution takes the form + + .. math:: \sum_{x=1}^{n} c_i(x) y_i(x) {a_n} {x^n} \text{, } + + where `y_i` is the `i`\th solution to the homogeneous equation. The + solution is then solved using Wronskian's and Cramer's Rule. The + particular solution is given by multiplying eq given below with `a_n x^{n}` + + .. math:: \sum_{x=1}^n \left( \int \frac{W_i(x)}{W(x)} \, dx + \right) y_i(x) \text{, } + + where `W(x)` is the Wronskian of the fundamental system (the system of `n` + linearly independent solutions to the homogeneous equation), and `W_i(x)` + is the Wronskian of the fundamental system with the `i`\th column replaced + with `[0, 0, \cdots, 0, \frac{x^{- n}}{a_n} g{\left(x \right)}]`. + + This method is general enough to solve any `n`\th order inhomogeneous + linear differential equation, but sometimes SymPy cannot simplify the + Wronskian well enough to integrate it. If this method hangs, try using the + ``nth_linear_constant_coeff_variation_of_parameters_Integral`` hint and + simplifying the integrals manually. Also, prefer using + ``nth_linear_constant_coeff_undetermined_coefficients`` when it + applies, because it does not use integration, making it faster and more + reliable. + + Warning, using simplify=False with + 'nth_linear_constant_coeff_variation_of_parameters' in + :py:meth:`~sympy.solvers.ode.dsolve` may cause it to hang, because it will + not attempt to simplify the Wronskian before integrating. It is + recommended that you only use simplify=False with + 'nth_linear_constant_coeff_variation_of_parameters_Integral' for this + method, especially if the solution to the homogeneous equation has + trigonometric functions in it. + + Examples + ======== + + >>> from sympy import Function, dsolve, Derivative + >>> from sympy.abc import x + >>> f = Function('f') + >>> eq = x**2*Derivative(f(x), x, x) - 2*x*Derivative(f(x), x) + 2*f(x) - x**4 + >>> dsolve(eq, f(x), + ... hint='nth_linear_euler_eq_nonhomogeneous_variation_of_parameters').expand() + Eq(f(x), C1*x + C2*x**2 + x**4/6) + + """ + hint = "nth_linear_euler_eq_nonhomogeneous_variation_of_parameters" + has_integral = True + + def _matches(self): + eq = self.ode_problem.eq_preprocessed + f = self.ode_problem.func.func + order = self.ode_problem.order + x = self.ode_problem.sym + match = self.ode_problem.get_linear_coefficients(eq, f(x), order) + self.r = None + does_match = False + + if order and match: + coeff = match[order] + factor = x**order / coeff + self.r = {i: factor*match[i] for i in match} + if self.r and all(_test_term(self.r[i], f(x), i) for i in + self.r if i >= 0): + if self.r[-1]: + does_match = True + + return does_match + + def _get_general_solution(self, *, simplify_flag: bool = True): + eq = self.ode_problem.eq + f = self.ode_problem.func.func + x = self.ode_problem.sym + order = self.ode_problem.order + homogen_sol, roots = _get_euler_characteristic_eq_sols(eq, f(x), self.r) + self.r[-1] = self.r[-1]/self.r[order] + sol = _solve_variation_of_parameters(eq, f(x), roots, homogen_sol, order, self.r, simplify_flag) + + return [Eq(f(x), homogen_sol.rhs + (sol.rhs - homogen_sol.rhs)*self.r[order])] + + +class NthLinearEulerEqNonhomogeneousUndeterminedCoefficients(SingleODESolver): + r""" + Solves an `n`\th order linear non homogeneous Cauchy-Euler equidimensional + ordinary differential equation using undetermined coefficients. + + This is an equation with form `g(x) = a_0 f(x) + a_1 x f'(x) + a_2 x^2 f''(x) + \cdots`. + + These equations can be solved in a general manner, by substituting + solutions of the form `x = exp(t)`, and deriving a characteristic equation + of form `g(exp(t)) = b_0 f(t) + b_1 f'(t) + b_2 f''(t) \cdots` which can + be then solved by nth_linear_constant_coeff_undetermined_coefficients if + g(exp(t)) has finite number of linearly independent derivatives. + + Functions that fit this requirement are finite sums functions of the form + `a x^i e^{b x} \sin(c x + d)` or `a x^i e^{b x} \cos(c x + d)`, where `i` + is a non-negative integer and `a`, `b`, `c`, and `d` are constants. For + example any polynomial in `x`, functions like `x^2 e^{2 x}`, `x \sin(x)`, + and `e^x \cos(x)` can all be used. Products of `\sin`'s and `\cos`'s have + a finite number of derivatives, because they can be expanded into `\sin(a + x)` and `\cos(b x)` terms. However, SymPy currently cannot do that + expansion, so you will need to manually rewrite the expression in terms of + the above to use this method. So, for example, you will need to manually + convert `\sin^2(x)` into `(1 + \cos(2 x))/2` to properly apply the method + of undetermined coefficients on it. + + After replacement of x by exp(t), this method works by creating a trial function + from the expression and all of its linear independent derivatives and + substituting them into the original ODE. The coefficients for each term + will be a system of linear equations, which are be solved for and + substituted, giving the solution. If any of the trial functions are linearly + dependent on the solution to the homogeneous equation, they are multiplied + by sufficient `x` to make them linearly independent. + + Examples + ======== + + >>> from sympy import dsolve, Function, Derivative, log + >>> from sympy.abc import x + >>> f = Function('f') + >>> eq = x**2*Derivative(f(x), x, x) - 2*x*Derivative(f(x), x) + 2*f(x) - log(x) + >>> dsolve(eq, f(x), + ... hint='nth_linear_euler_eq_nonhomogeneous_undetermined_coefficients').expand() + Eq(f(x), C1*x + C2*x**2 + log(x)/2 + 3/4) + + """ + hint = "nth_linear_euler_eq_nonhomogeneous_undetermined_coefficients" + has_integral = False + + def _matches(self): + eq = self.ode_problem.eq_high_order_free + f = self.ode_problem.func.func + order = self.ode_problem.order + x = self.ode_problem.sym + match = self.ode_problem.get_linear_coefficients(eq, f(x), order) + self.r = None + does_match = False + + if order and match: + coeff = match[order] + factor = x**order / coeff + self.r = {i: factor*match[i] for i in match} + if self.r and all(_test_term(self.r[i], f(x), i) for i in + self.r if i >= 0): + if self.r[-1]: + e, re = posify(self.r[-1].subs(x, exp(x))) + undetcoeff = _undetermined_coefficients_match(e.subs(re), x) + if undetcoeff['test']: + does_match = True + return does_match + + def _get_general_solution(self, *, simplify_flag: bool = True): + f = self.ode_problem.func.func + x = self.ode_problem.sym + chareq, eq, symbol = S.Zero, S.Zero, Dummy('x') + for i in self.r.keys(): + if i >= 0: + chareq += (self.r[i]*diff(x**symbol, x, i)*x**-symbol).expand() + + for i in range(1, degree(Poly(chareq, symbol))+1): + eq += chareq.coeff(symbol**i)*diff(f(x), x, i) + + if chareq.as_coeff_add(symbol)[0]: + eq += chareq.as_coeff_add(symbol)[0]*f(x) + e, re = posify(self.r[-1].subs(x, exp(x))) + eq += e.subs(re) + + self.const_undet_instance = NthLinearConstantCoeffUndeterminedCoefficients(SingleODEProblem(eq, f(x), x)) + sol = self.const_undet_instance.get_general_solution(simplify = simplify_flag)[0] + sol = sol.subs(x, log(x)) # type: ignore + sol = sol.subs(f(log(x)), f(x)).expand() # type: ignore + + return [sol] + + +class SecondLinearBessel(SingleODESolver): + r""" + Gives solution of the Bessel differential equation + + .. math :: x^2 \frac{d^2y}{dx^2} + x \frac{dy}{dx} y(x) + (x^2-n^2) y(x) + + if `n` is integer then the solution is of the form ``Eq(f(x), C0 besselj(n,x) + + C1 bessely(n,x))`` as both the solutions are linearly independent else if + `n` is a fraction then the solution is of the form ``Eq(f(x), C0 besselj(n,x) + + C1 besselj(-n,x))`` which can also transform into ``Eq(f(x), C0 besselj(n,x) + + C1 bessely(n,x))``. + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy import Symbol + >>> v = Symbol('v', positive=True) + >>> from sympy import dsolve, Function + >>> f = Function('f') + >>> y = f(x) + >>> genform = x**2*y.diff(x, 2) + x*y.diff(x) + (x**2 - v**2)*y + >>> dsolve(genform) + Eq(f(x), C1*besselj(v, x) + C2*bessely(v, x)) + + References + ========== + + https://math24.net/bessel-differential-equation.html + + """ + hint = "2nd_linear_bessel" + has_integral = False + + def _matches(self): + eq = self.ode_problem.eq_high_order_free + f = self.ode_problem.func + order = self.ode_problem.order + x = self.ode_problem.sym + df = f.diff(x) + a = Wild('a', exclude=[f,df]) + b = Wild('b', exclude=[x, f,df]) + a4 = Wild('a4', exclude=[x,f,df]) + b4 = Wild('b4', exclude=[x,f,df]) + c4 = Wild('c4', exclude=[x,f,df]) + d4 = Wild('d4', exclude=[x,f,df]) + a3 = Wild('a3', exclude=[f, df, f.diff(x, 2)]) + b3 = Wild('b3', exclude=[f, df, f.diff(x, 2)]) + c3 = Wild('c3', exclude=[f, df, f.diff(x, 2)]) + deq = a3*(f.diff(x, 2)) + b3*df + c3*f + r = collect(eq, + [f.diff(x, 2), df, f]).match(deq) + if order == 2 and r: + if not all(r[key].is_polynomial() for key in r): + n, d = eq.as_numer_denom() + eq = expand(n) + r = collect(eq, + [f.diff(x, 2), df, f]).match(deq) + + if r and r[a3] != 0: + # leading coeff of f(x).diff(x, 2) + coeff = factor(r[a3]).match(a4*(x-b)**b4) + + if coeff: + # if coeff[b4] = 0 means constant coefficient + if coeff[b4] == 0: + return False + point = coeff[b] + else: + return False + + if point: + r[a3] = simplify(r[a3].subs(x, x+point)) + r[b3] = simplify(r[b3].subs(x, x+point)) + r[c3] = simplify(r[c3].subs(x, x+point)) + + # making a3 in the form of x**2 + r[a3] = cancel(r[a3]/(coeff[a4]*(x)**(-2+coeff[b4]))) + r[b3] = cancel(r[b3]/(coeff[a4]*(x)**(-2+coeff[b4]))) + r[c3] = cancel(r[c3]/(coeff[a4]*(x)**(-2+coeff[b4]))) + # checking if b3 is of form c*(x-b) + coeff1 = factor(r[b3]).match(a4*(x)) + if coeff1 is None: + return False + # c3 maybe of very complex form so I am simply checking (a - b) form + # if yes later I will match with the standard form of bessel in a and b + # a, b are wild variable defined above. + _coeff2 = expand(r[c3]).match(a - b) + if _coeff2 is None: + return False + # matching with standard form for c3 + coeff2 = factor(_coeff2[a]).match(c4**2*(x)**(2*a4)) + if coeff2 is None: + return False + + if _coeff2[b] == 0: + coeff2[d4] = 0 + else: + coeff2[d4] = factor(_coeff2[b]).match(d4**2)[d4] + + self.rn = {'n':coeff2[d4], 'a4':coeff2[c4], 'd4':coeff2[a4]} + self.rn['c4'] = coeff1[a4] + self.rn['b4'] = point + return True + return False + + def _get_general_solution(self, *, simplify_flag: bool = True): + f = self.ode_problem.func.func + x = self.ode_problem.sym + n = self.rn['n'] + a4 = self.rn['a4'] + c4 = self.rn['c4'] + d4 = self.rn['d4'] + b4 = self.rn['b4'] + n = sqrt(n**2 + Rational(1, 4)*(c4 - 1)**2) + (C1, C2) = self.ode_problem.get_numbered_constants(num=2) + return [Eq(f(x), ((x**(Rational(1-c4,2)))*(C1*besselj(n/d4,a4*x**d4/d4) + + C2*bessely(n/d4,a4*x**d4/d4))).subs(x, x-b4))] + + +class SecondLinearAiry(SingleODESolver): + r""" + Gives solution of the Airy differential equation + + .. math :: \frac{d^2y}{dx^2} + (a + b x) y(x) = 0 + + in terms of Airy special functions airyai and airybi. + + Examples + ======== + + >>> from sympy import dsolve, Function + >>> from sympy.abc import x + >>> f = Function("f") + >>> eq = f(x).diff(x, 2) - x*f(x) + >>> dsolve(eq) + Eq(f(x), C1*airyai(x) + C2*airybi(x)) + """ + hint = "2nd_linear_airy" + has_integral = False + + def _matches(self): + eq = self.ode_problem.eq_high_order_free + f = self.ode_problem.func + order = self.ode_problem.order + x = self.ode_problem.sym + df = f.diff(x) + a4 = Wild('a4', exclude=[x,f,df]) + b4 = Wild('b4', exclude=[x,f,df]) + match = self.ode_problem.get_linear_coefficients(eq, f, order) + does_match = False + if order == 2 and match and match[2] != 0: + if match[1].is_zero: + self.rn = cancel(match[0]/match[2]).match(a4+b4*x) + if self.rn and self.rn[b4] != 0: + self.rn = {'b':self.rn[a4],'m':self.rn[b4]} + does_match = True + return does_match + + def _get_general_solution(self, *, simplify_flag: bool = True): + f = self.ode_problem.func.func + x = self.ode_problem.sym + (C1, C2) = self.ode_problem.get_numbered_constants(num=2) + b = self.rn['b'] + m = self.rn['m'] + if m.is_positive: + arg = - b/cbrt(m)**2 - cbrt(m)*x + elif m.is_negative: + arg = - b/cbrt(-m)**2 + cbrt(-m)*x + else: + arg = - b/cbrt(-m)**2 + cbrt(-m)*x + + return [Eq(f(x), C1*airyai(arg) + C2*airybi(arg))] + + +class LieGroup(SingleODESolver): + r""" + This hint implements the Lie group method of solving first order differential + equations. The aim is to convert the given differential equation from the + given coordinate system into another coordinate system where it becomes + invariant under the one-parameter Lie group of translations. The converted + ODE can be easily solved by quadrature. It makes use of the + :py:meth:`sympy.solvers.ode.infinitesimals` function which returns the + infinitesimals of the transformation. + + The coordinates `r` and `s` can be found by solving the following Partial + Differential Equations. + + .. math :: \xi\frac{\partial r}{\partial x} + \eta\frac{\partial r}{\partial y} + = 0 + + .. math :: \xi\frac{\partial s}{\partial x} + \eta\frac{\partial s}{\partial y} + = 1 + + The differential equation becomes separable in the new coordinate system + + .. math :: \frac{ds}{dr} = \frac{\frac{\partial s}{\partial x} + + h(x, y)\frac{\partial s}{\partial y}}{ + \frac{\partial r}{\partial x} + h(x, y)\frac{\partial r}{\partial y}} + + After finding the solution by integration, it is then converted back to the original + coordinate system by substituting `r` and `s` in terms of `x` and `y` again. + + Examples + ======== + + >>> from sympy import Function, dsolve, exp, pprint + >>> from sympy.abc import x + >>> f = Function('f') + >>> pprint(dsolve(f(x).diff(x) + 2*x*f(x) - x*exp(-x**2), f(x), + ... hint='lie_group')) + / 2\ 2 + | x | -x + f(x) = |C1 + --|*e + \ 2 / + + + References + ========== + + - Solving differential equations by Symmetry Groups, + John Starrett, pp. 1 - pp. 14 + + """ + hint = "lie_group" + has_integral = False + + def _has_additional_params(self): + return 'xi' in self.ode_problem.params and 'eta' in self.ode_problem.params + + def _matches(self): + eq = self.ode_problem.eq + f = self.ode_problem.func.func + order = self.ode_problem.order + x = self.ode_problem.sym + df = f(x).diff(x) + y = Dummy('y') + d = Wild('d', exclude=[df, f(x).diff(x, 2)]) + e = Wild('e', exclude=[df]) + does_match = False + if self._has_additional_params() and order == 1: + xi = self.ode_problem.params['xi'] + eta = self.ode_problem.params['eta'] + self.r3 = {'xi': xi, 'eta': eta} + r = collect(eq, df, exact=True).match(d + e * df) + if r: + r['d'] = d + r['e'] = e + r['y'] = y + r[d] = r[d].subs(f(x), y) + r[e] = r[e].subs(f(x), y) + self.r3.update(r) + does_match = True + return does_match + + def _get_general_solution(self, *, simplify_flag: bool = True): + eq = self.ode_problem.eq + x = self.ode_problem.sym + func = self.ode_problem.func + order = self.ode_problem.order + df = func.diff(x) + + try: + eqsol = solve(eq, df) + except NotImplementedError: + eqsol = [] + + desols = [] + for s in eqsol: + sol = _ode_lie_group(s, func, order, match=self.r3) + if sol: + desols.extend(sol) + + if desols == []: + raise NotImplementedError("The given ODE " + str(eq) + " cannot be solved by" + + " the lie group method") + return desols + + +solver_map = { + 'factorable': Factorable, + 'nth_linear_constant_coeff_homogeneous': NthLinearConstantCoeffHomogeneous, + 'nth_linear_euler_eq_homogeneous': NthLinearEulerEqHomogeneous, + 'nth_linear_constant_coeff_undetermined_coefficients': NthLinearConstantCoeffUndeterminedCoefficients, + 'nth_linear_euler_eq_nonhomogeneous_undetermined_coefficients': NthLinearEulerEqNonhomogeneousUndeterminedCoefficients, + 'separable': Separable, + '1st_exact': FirstExact, + '1st_linear': FirstLinear, + 'Bernoulli': Bernoulli, + 'Riccati_special_minus2': RiccatiSpecial, + '1st_rational_riccati': RationalRiccati, + '1st_homogeneous_coeff_best': HomogeneousCoeffBest, + '1st_homogeneous_coeff_subs_indep_div_dep': HomogeneousCoeffSubsIndepDivDep, + '1st_homogeneous_coeff_subs_dep_div_indep': HomogeneousCoeffSubsDepDivIndep, + 'almost_linear': AlmostLinear, + 'linear_coefficients': LinearCoefficients, + 'separable_reduced': SeparableReduced, + 'nth_linear_constant_coeff_variation_of_parameters': NthLinearConstantCoeffVariationOfParameters, + 'nth_linear_euler_eq_nonhomogeneous_variation_of_parameters': NthLinearEulerEqNonhomogeneousVariationOfParameters, + 'Liouville': Liouville, + '2nd_linear_airy': SecondLinearAiry, + '2nd_linear_bessel': SecondLinearBessel, + '2nd_hypergeometric': SecondHypergeometric, + 'nth_order_reducible': NthOrderReducible, + '2nd_nonlinear_autonomous_conserved': SecondNonlinearAutonomousConserved, + 'nth_algebraic': NthAlgebraic, + 'lie_group': LieGroup, + } + +# Avoid circular import: +from .ode import dsolve, ode_sol_simplicity, odesimp, homogeneous_order diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/subscheck.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/subscheck.py new file mode 100644 index 0000000000000000000000000000000000000000..6ac7fba7d364bf599e928ccf591b5bef096576d0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/subscheck.py @@ -0,0 +1,392 @@ +from sympy.core import S, Pow +from sympy.core.function import (Derivative, AppliedUndef, diff) +from sympy.core.relational import Equality, Eq +from sympy.core.symbol import Dummy +from sympy.core.sympify import sympify + +from sympy.logic.boolalg import BooleanAtom +from sympy.functions import exp +from sympy.series import Order +from sympy.simplify.simplify import simplify, posify, besselsimp +from sympy.simplify.trigsimp import trigsimp +from sympy.simplify.sqrtdenest import sqrtdenest +from sympy.solvers import solve +from sympy.solvers.deutils import _preprocess, ode_order +from sympy.utilities.iterables import iterable, is_sequence + + +def sub_func_doit(eq, func, new): + r""" + When replacing the func with something else, we usually want the + derivative evaluated, so this function helps in making that happen. + + Examples + ======== + + >>> from sympy import Derivative, symbols, Function + >>> from sympy.solvers.ode.subscheck import sub_func_doit + >>> x, z = symbols('x, z') + >>> y = Function('y') + + >>> sub_func_doit(3*Derivative(y(x), x) - 1, y(x), x) + 2 + + >>> sub_func_doit(x*Derivative(y(x), x) - y(x)**2 + y(x), y(x), + ... 1/(x*(z + 1/x))) + x*(-1/(x**2*(z + 1/x)) + 1/(x**3*(z + 1/x)**2)) + 1/(x*(z + 1/x)) + ...- 1/(x**2*(z + 1/x)**2) + """ + reps= {func: new} + for d in eq.atoms(Derivative): + if d.expr == func: + reps[d] = new.diff(*d.variable_count) + else: + reps[d] = d.xreplace({func: new}).doit(deep=False) + return eq.xreplace(reps) + + +def checkodesol(ode, sol, func=None, order='auto', solve_for_func=True): + r""" + Substitutes ``sol`` into ``ode`` and checks that the result is ``0``. + + This works when ``func`` is one function, like `f(x)` or a list of + functions like `[f(x), g(x)]` when `ode` is a system of ODEs. ``sol`` can + be a single solution or a list of solutions. Each solution may be an + :py:class:`~sympy.core.relational.Equality` that the solution satisfies, + e.g. ``Eq(f(x), C1), Eq(f(x) + C1, 0)``; or simply an + :py:class:`~sympy.core.expr.Expr`, e.g. ``f(x) - C1``. In most cases it + will not be necessary to explicitly identify the function, but if the + function cannot be inferred from the original equation it can be supplied + through the ``func`` argument. + + If a sequence of solutions is passed, the same sort of container will be + used to return the result for each solution. + + It tries the following methods, in order, until it finds zero equivalence: + + 1. Substitute the solution for `f` in the original equation. This only + works if ``ode`` is solved for `f`. It will attempt to solve it first + unless ``solve_for_func == False``. + 2. Take `n` derivatives of the solution, where `n` is the order of + ``ode``, and check to see if that is equal to the solution. This only + works on exact ODEs. + 3. Take the 1st, 2nd, ..., `n`\th derivatives of the solution, each time + solving for the derivative of `f` of that order (this will always be + possible because `f` is a linear operator). Then back substitute each + derivative into ``ode`` in reverse order. + + This function returns a tuple. The first item in the tuple is ``True`` if + the substitution results in ``0``, and ``False`` otherwise. The second + item in the tuple is what the substitution results in. It should always + be ``0`` if the first item is ``True``. Sometimes this function will + return ``False`` even when an expression is identically equal to ``0``. + This happens when :py:meth:`~sympy.simplify.simplify.simplify` does not + reduce the expression to ``0``. If an expression returned by this + function vanishes identically, then ``sol`` really is a solution to + the ``ode``. + + If this function seems to hang, it is probably because of a hard + simplification. + + To use this function to test, test the first item of the tuple. + + Examples + ======== + + >>> from sympy import (Eq, Function, checkodesol, symbols, + ... Derivative, exp) + >>> x, C1, C2 = symbols('x,C1,C2') + >>> f, g = symbols('f g', cls=Function) + >>> checkodesol(f(x).diff(x), Eq(f(x), C1)) + (True, 0) + >>> assert checkodesol(f(x).diff(x), C1)[0] + >>> assert not checkodesol(f(x).diff(x), x)[0] + >>> checkodesol(f(x).diff(x, 2), x**2) + (False, 2) + + >>> eqs = [Eq(Derivative(f(x), x), f(x)), Eq(Derivative(g(x), x), g(x))] + >>> sol = [Eq(f(x), C1*exp(x)), Eq(g(x), C2*exp(x))] + >>> checkodesol(eqs, sol) + (True, [0, 0]) + + """ + if iterable(ode): + return checksysodesol(ode, sol, func=func) + + if not isinstance(ode, Equality): + ode = Eq(ode, 0) + if func is None: + try: + _, func = _preprocess(ode.lhs) + except ValueError: + funcs = [s.atoms(AppliedUndef) for s in ( + sol if is_sequence(sol, set) else [sol])] + funcs = set().union(*funcs) + if len(funcs) != 1: + raise ValueError( + 'must pass func arg to checkodesol for this case.') + func = funcs.pop() + if not isinstance(func, AppliedUndef) or len(func.args) != 1: + raise ValueError( + "func must be a function of one variable, not %s" % func) + if is_sequence(sol, set): + return type(sol)([checkodesol(ode, i, order=order, solve_for_func=solve_for_func) for i in sol]) + + if not isinstance(sol, Equality): + sol = Eq(func, sol) + elif sol.rhs == func: + sol = sol.reversed + + if order == 'auto': + order = ode_order(ode, func) + solved = sol.lhs == func and not sol.rhs.has(func) + if solve_for_func and not solved: + rhs = solve(sol, func) + if rhs: + eqs = [Eq(func, t) for t in rhs] + if len(rhs) == 1: + eqs = eqs[0] + return checkodesol(ode, eqs, order=order, + solve_for_func=False) + + x = func.args[0] + + # Handle series solutions here + if sol.has(Order): + assert sol.lhs == func + Oterm = sol.rhs.getO() + solrhs = sol.rhs.removeO() + + Oexpr = Oterm.expr + assert isinstance(Oexpr, Pow) + sorder = Oexpr.exp + assert Oterm == Order(x**sorder) + + odesubs = (ode.lhs-ode.rhs).subs(func, solrhs).doit().expand() + + neworder = Order(x**(sorder - order)) + odesubs = odesubs + neworder + assert odesubs.getO() == neworder + residual = odesubs.removeO() + + return (residual == 0, residual) + + s = True + testnum = 0 + while s: + if testnum == 0: + # First pass, try substituting a solved solution directly into the + # ODE. This has the highest chance of succeeding. + ode_diff = ode.lhs - ode.rhs + + if sol.lhs == func: + s = sub_func_doit(ode_diff, func, sol.rhs) + s = besselsimp(s) + else: + testnum += 1 + continue + ss = simplify(s.rewrite(exp)) + if ss: + # with the new numer_denom in power.py, if we do a simple + # expansion then testnum == 0 verifies all solutions. + s = ss.expand(force=True) + else: + s = 0 + testnum += 1 + elif testnum == 1: + # Second pass. If we cannot substitute f, try seeing if the nth + # derivative is equal, this will only work for odes that are exact, + # by definition. + s = simplify( + trigsimp(diff(sol.lhs, x, order) - diff(sol.rhs, x, order)) - + trigsimp(ode.lhs) + trigsimp(ode.rhs)) + # s2 = simplify( + # diff(sol.lhs, x, order) - diff(sol.rhs, x, order) - \ + # ode.lhs + ode.rhs) + testnum += 1 + elif testnum == 2: + # Third pass. Try solving for df/dx and substituting that into the + # ODE. Thanks to Chris Smith for suggesting this method. Many of + # the comments below are his, too. + # The method: + # - Take each of 1..n derivatives of the solution. + # - Solve each nth derivative for d^(n)f/dx^(n) + # (the differential of that order) + # - Back substitute into the ODE in decreasing order + # (i.e., n, n-1, ...) + # - Check the result for zero equivalence + if sol.lhs == func and not sol.rhs.has(func): + diffsols = {0: sol.rhs} + elif sol.rhs == func and not sol.lhs.has(func): + diffsols = {0: sol.lhs} + else: + diffsols = {} + sol = sol.lhs - sol.rhs + for i in range(1, order + 1): + # Differentiation is a linear operator, so there should always + # be 1 solution. Nonetheless, we test just to make sure. + # We only need to solve once. After that, we automatically + # have the solution to the differential in the order we want. + if i == 1: + ds = sol.diff(x) + try: + sdf = solve(ds, func.diff(x, i)) + if not sdf: + raise NotImplementedError + except NotImplementedError: + testnum += 1 + break + else: + diffsols[i] = sdf[0] + else: + # This is what the solution says df/dx should be. + diffsols[i] = diffsols[i - 1].diff(x) + + # Make sure the above didn't fail. + if testnum > 2: + continue + else: + # Substitute it into ODE to check for self consistency. + lhs, rhs = ode.lhs, ode.rhs + for i in range(order, -1, -1): + if i == 0 and 0 not in diffsols: + # We can only substitute f(x) if the solution was + # solved for f(x). + break + lhs = sub_func_doit(lhs, func.diff(x, i), diffsols[i]) + rhs = sub_func_doit(rhs, func.diff(x, i), diffsols[i]) + ode_or_bool = Eq(lhs, rhs) + ode_or_bool = simplify(ode_or_bool) + + if isinstance(ode_or_bool, (bool, BooleanAtom)): + if ode_or_bool: + lhs = rhs = S.Zero + else: + lhs = ode_or_bool.lhs + rhs = ode_or_bool.rhs + # No sense in overworking simplify -- just prove that the + # numerator goes to zero + num = trigsimp((lhs - rhs).as_numer_denom()[0]) + # since solutions are obtained using force=True we test + # using the same level of assumptions + ## replace function with dummy so assumptions will work + _func = Dummy('func') + num = num.subs(func, _func) + ## posify the expression + num, reps = posify(num) + s = simplify(num).xreplace(reps).xreplace({_func: func}) + testnum += 1 + else: + break + + if not s: + return (True, s) + elif s is True: # The code above never was able to change s + raise NotImplementedError("Unable to test if " + str(sol) + + " is a solution to " + str(ode) + ".") + else: + return (False, s) + + +def checksysodesol(eqs, sols, func=None): + r""" + Substitutes corresponding ``sols`` for each functions into each ``eqs`` and + checks that the result of substitutions for each equation is ``0``. The + equations and solutions passed can be any iterable. + + This only works when each ``sols`` have one function only, like `x(t)` or `y(t)`. + For each function, ``sols`` can have a single solution or a list of solutions. + In most cases it will not be necessary to explicitly identify the function, + but if the function cannot be inferred from the original equation it + can be supplied through the ``func`` argument. + + When a sequence of equations is passed, the same sequence is used to return + the result for each equation with each function substituted with corresponding + solutions. + + It tries the following method to find zero equivalence for each equation: + + Substitute the solutions for functions, like `x(t)` and `y(t)` into the + original equations containing those functions. + This function returns a tuple. The first item in the tuple is ``True`` if + the substitution results for each equation is ``0``, and ``False`` otherwise. + The second item in the tuple is what the substitution results in. Each element + of the ``list`` should always be ``0`` corresponding to each equation if the + first item is ``True``. Note that sometimes this function may return ``False``, + but with an expression that is identically equal to ``0``, instead of returning + ``True``. This is because :py:meth:`~sympy.simplify.simplify.simplify` cannot + reduce the expression to ``0``. If an expression returned by each function + vanishes identically, then ``sols`` really is a solution to ``eqs``. + + If this function seems to hang, it is probably because of a difficult simplification. + + Examples + ======== + + >>> from sympy import Eq, diff, symbols, sin, cos, exp, sqrt, S, Function + >>> from sympy.solvers.ode.subscheck import checksysodesol + >>> C1, C2 = symbols('C1:3') + >>> t = symbols('t') + >>> x, y = symbols('x, y', cls=Function) + >>> eq = (Eq(diff(x(t),t), x(t) + y(t) + 17), Eq(diff(y(t),t), -2*x(t) + y(t) + 12)) + >>> sol = [Eq(x(t), (C1*sin(sqrt(2)*t) + C2*cos(sqrt(2)*t))*exp(t) - S(5)/3), + ... Eq(y(t), (sqrt(2)*C1*cos(sqrt(2)*t) - sqrt(2)*C2*sin(sqrt(2)*t))*exp(t) - S(46)/3)] + >>> checksysodesol(eq, sol) + (True, [0, 0]) + >>> eq = (Eq(diff(x(t),t),x(t)*y(t)**4), Eq(diff(y(t),t),y(t)**3)) + >>> sol = [Eq(x(t), C1*exp(-1/(4*(C2 + t)))), Eq(y(t), -sqrt(2)*sqrt(-1/(C2 + t))/2), + ... Eq(x(t), C1*exp(-1/(4*(C2 + t)))), Eq(y(t), sqrt(2)*sqrt(-1/(C2 + t))/2)] + >>> checksysodesol(eq, sol) + (True, [0, 0]) + + """ + def _sympify(eq): + return list(map(sympify, eq if iterable(eq) else [eq])) + eqs = _sympify(eqs) + for i in range(len(eqs)): + if isinstance(eqs[i], Equality): + eqs[i] = eqs[i].lhs - eqs[i].rhs + if func is None: + funcs = [] + for eq in eqs: + derivs = eq.atoms(Derivative) + func = set().union(*[d.atoms(AppliedUndef) for d in derivs]) + funcs.extend(func) + funcs = list(set(funcs)) + if not all(isinstance(func, AppliedUndef) and len(func.args) == 1 for func in funcs)\ + and len({func.args for func in funcs})!=1: + raise ValueError("func must be a function of one variable, not %s" % func) + for sol in sols: + if len(sol.atoms(AppliedUndef)) != 1: + raise ValueError("solutions should have one function only") + if len(funcs) != len({sol.lhs for sol in sols}): + raise ValueError("number of solutions provided does not match the number of equations") + dictsol = {} + for sol in sols: + func = list(sol.atoms(AppliedUndef))[0] + if sol.rhs == func: + sol = sol.reversed + solved = sol.lhs == func and not sol.rhs.has(func) + if not solved: + rhs = solve(sol, func) + if not rhs: + raise NotImplementedError + else: + rhs = sol.rhs + dictsol[func] = rhs + checkeq = [] + for eq in eqs: + for func in funcs: + eq = sub_func_doit(eq, func, dictsol[func]) + ss = simplify(eq) + if ss != 0: + eq = ss.expand(force=True) + if eq != 0: + eq = sqrtdenest(eq).simplify() + else: + eq = 0 + checkeq.append(eq) + if len(set(checkeq)) == 1 and list(set(checkeq))[0] == 0: + return (True, checkeq) + else: + return (False, checkeq) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/systems.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/systems.py new file mode 100644 index 0000000000000000000000000000000000000000..2d2c9b57a969c7fb5c67c06ce952fa398e22a48d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/systems.py @@ -0,0 +1,2135 @@ +from sympy.core import Add, Mul, S +from sympy.core.containers import Tuple +from sympy.core.exprtools import factor_terms +from sympy.core.numbers import I +from sympy.core.relational import Eq, Equality +from sympy.core.sorting import default_sort_key, ordered +from sympy.core.symbol import Dummy, Symbol +from sympy.core.function import (expand_mul, expand, Derivative, + AppliedUndef, Function, Subs) +from sympy.functions import (exp, im, cos, sin, re, Piecewise, + piecewise_fold, sqrt, log) +from sympy.functions.combinatorial.factorials import factorial +from sympy.matrices import zeros, Matrix, NonSquareMatrixError, MatrixBase, eye +from sympy.polys import Poly, together +from sympy.simplify import collect, radsimp, signsimp # type: ignore +from sympy.simplify.powsimp import powdenest, powsimp +from sympy.simplify.ratsimp import ratsimp +from sympy.simplify.simplify import simplify +from sympy.sets.sets import FiniteSet +from sympy.solvers.deutils import ode_order +from sympy.solvers.solveset import NonlinearError, solveset +from sympy.utilities.iterables import (connected_components, iterable, + strongly_connected_components) +from sympy.utilities.misc import filldedent +from sympy.integrals.integrals import Integral, integrate + + +def _get_func_order(eqs, funcs): + return {func: max(ode_order(eq, func) for eq in eqs) for func in funcs} + + +class ODEOrderError(ValueError): + """Raised by linear_ode_to_matrix if the system has the wrong order""" + pass + + +class ODENonlinearError(NonlinearError): + """Raised by linear_ode_to_matrix if the system is nonlinear""" + pass + + +def _simpsol(soleq): + lhs = soleq.lhs + sol = soleq.rhs + sol = powsimp(sol) + gens = list(sol.atoms(exp)) + p = Poly(sol, *gens, expand=False) + gens = [factor_terms(g) for g in gens] + if not gens: + gens = p.gens + syms = [Symbol('C1'), Symbol('C2')] + terms = [] + for coeff, monom in zip(p.coeffs(), p.monoms()): + coeff = piecewise_fold(coeff) + if isinstance(coeff, Piecewise): + coeff = Piecewise(*((ratsimp(coef).collect(syms), cond) for coef, cond in coeff.args)) + else: + coeff = ratsimp(coeff).collect(syms) + monom = Mul(*(g ** i for g, i in zip(gens, monom))) + terms.append(coeff * monom) + return Eq(lhs, Add(*terms)) + + +def _solsimp(e, t): + no_t, has_t = powsimp(expand_mul(e)).as_independent(t) + + no_t = ratsimp(no_t) + has_t = has_t.replace(exp, lambda a: exp(factor_terms(a))) + + return no_t + has_t + + +def simpsol(sol, wrt1, wrt2, doit=True): + """Simplify solutions from dsolve_system.""" + + # The parameter sol is the solution as returned by dsolve (list of Eq). + # + # The parameters wrt1 and wrt2 are lists of symbols to be collected for + # with those in wrt1 being collected for first. This allows for collecting + # on any factors involving the independent variable before collecting on + # the integration constants or vice versa using e.g.: + # + # sol = simpsol(sol, [t], [C1, C2]) # t first, constants after + # sol = simpsol(sol, [C1, C2], [t]) # constants first, t after + # + # If doit=True (default) then simpsol will begin by evaluating any + # unevaluated integrals. Since many integrals will appear multiple times + # in the solutions this is done intelligently by computing each integral + # only once. + # + # The strategy is to first perform simple cancellation with factor_terms + # and then multiply out all brackets with expand_mul. This gives an Add + # with many terms. + # + # We split each term into two multiplicative factors dep and coeff where + # all factors that involve wrt1 are in dep and any constant factors are in + # coeff e.g. + # sqrt(2)*C1*exp(t) -> ( exp(t), sqrt(2)*C1 ) + # + # The dep factors are simplified using powsimp to combine expanded + # exponential factors e.g. + # exp(a*t)*exp(b*t) -> exp(t*(a+b)) + # + # We then collect coefficients for all terms having the same (simplified) + # dep. The coefficients are then simplified using together and ratsimp and + # lastly by recursively applying the same transformation to the + # coefficients to collect on wrt2. + # + # Finally the result is recombined into an Add and signsimp is used to + # normalise any minus signs. + + def simprhs(rhs, rep, wrt1, wrt2): + """Simplify the rhs of an ODE solution""" + if rep: + rhs = rhs.subs(rep) + rhs = factor_terms(rhs) + rhs = simp_coeff_dep(rhs, wrt1, wrt2) + rhs = signsimp(rhs) + return rhs + + def simp_coeff_dep(expr, wrt1, wrt2=None): + """Split rhs into terms, split terms into dep and coeff and collect on dep""" + add_dep_terms = lambda e: e.is_Add and e.has(*wrt1) + expandable = lambda e: e.is_Mul and any(map(add_dep_terms, e.args)) + expand_func = lambda e: expand_mul(e, deep=False) + expand_mul_mod = lambda e: e.replace(expandable, expand_func) + terms = Add.make_args(expand_mul_mod(expr)) + dc = {} + for term in terms: + coeff, dep = term.as_independent(*wrt1, as_Add=False) + # Collect together the coefficients for terms that have the same + # dependence on wrt1 (after dep is normalised using simpdep). + dep = simpdep(dep, wrt1) + + # See if the dependence on t cancels out... + if dep is not S.One: + dep2 = factor_terms(dep) + if not dep2.has(*wrt1): + coeff *= dep2 + dep = S.One + + if dep not in dc: + dc[dep] = coeff + else: + dc[dep] += coeff + # Apply the method recursively to the coefficients but this time + # collecting on wrt2 rather than wrt2. + termpairs = ((simpcoeff(c, wrt2), d) for d, c in dc.items()) + if wrt2 is not None: + termpairs = ((simp_coeff_dep(c, wrt2), d) for c, d in termpairs) + return Add(*(c * d for c, d in termpairs)) + + def simpdep(term, wrt1): + """Normalise factors involving t with powsimp and recombine exp""" + def canonicalise(a): + # Using factor_terms here isn't quite right because it leads to things + # like exp(t*(1+t)) that we don't want. We do want to cancel factors + # and pull out a common denominator but ideally the numerator would be + # expressed as a standard form polynomial in t so we expand_mul + # and collect afterwards. + a = factor_terms(a) + num, den = a.as_numer_denom() + num = expand_mul(num) + num = collect(num, wrt1) + return num / den + + term = powsimp(term) + rep = {e: exp(canonicalise(e.args[0])) for e in term.atoms(exp)} + term = term.subs(rep) + return term + + def simpcoeff(coeff, wrt2): + """Bring to a common fraction and cancel with ratsimp""" + coeff = together(coeff) + if coeff.is_polynomial(): + # Calling ratsimp can be expensive. The main reason is to simplify + # sums of terms with irrational denominators so we limit ourselves + # to the case where the expression is polynomial in any symbols. + # Maybe there's a better approach... + coeff = ratsimp(radsimp(coeff)) + # collect on secondary variables first and any remaining symbols after + if wrt2 is not None: + syms = list(wrt2) + list(ordered(coeff.free_symbols - set(wrt2))) + else: + syms = list(ordered(coeff.free_symbols)) + coeff = collect(coeff, syms) + coeff = together(coeff) + return coeff + + # There are often repeated integrals. Collect unique integrals and + # evaluate each once and then substitute into the final result to replace + # all occurrences in each of the solution equations. + if doit: + integrals = set().union(*(s.atoms(Integral) for s in sol)) + rep = {i: factor_terms(i).doit() for i in integrals} + else: + rep = {} + + sol = [Eq(s.lhs, simprhs(s.rhs, rep, wrt1, wrt2)) for s in sol] + return sol + + +def linodesolve_type(A, t, b=None): + r""" + Helper function that determines the type of the system of ODEs for solving with :obj:`sympy.solvers.ode.systems.linodesolve()` + + Explanation + =========== + + This function takes in the coefficient matrix and/or the non-homogeneous term + and returns the type of the equation that can be solved by :obj:`sympy.solvers.ode.systems.linodesolve()`. + + If the system is constant coefficient homogeneous, then "type1" is returned + + If the system is constant coefficient non-homogeneous, then "type2" is returned + + If the system is non-constant coefficient homogeneous, then "type3" is returned + + If the system is non-constant coefficient non-homogeneous, then "type4" is returned + + If the system has a non-constant coefficient matrix which can be factorized into constant + coefficient matrix, then "type5" or "type6" is returned for when the system is homogeneous or + non-homogeneous respectively. + + Note that, if the system of ODEs is of "type3" or "type4", then along with the type, + the commutative antiderivative of the coefficient matrix is also returned. + + If the system cannot be solved by :obj:`sympy.solvers.ode.systems.linodesolve()`, then + NotImplementedError is raised. + + Parameters + ========== + + A : Matrix + Coefficient matrix of the system of ODEs + b : Matrix or None + Non-homogeneous term of the system. The default value is None. + If this argument is None, then the system is assumed to be homogeneous. + + Examples + ======== + + >>> from sympy import symbols, Matrix + >>> from sympy.solvers.ode.systems import linodesolve_type + >>> t = symbols("t") + >>> A = Matrix([[1, 1], [2, 3]]) + >>> b = Matrix([t, 1]) + + >>> linodesolve_type(A, t) + {'antiderivative': None, 'type_of_equation': 'type1'} + + >>> linodesolve_type(A, t, b=b) + {'antiderivative': None, 'type_of_equation': 'type2'} + + >>> A_t = Matrix([[1, t], [-t, 1]]) + + >>> linodesolve_type(A_t, t) + {'antiderivative': Matrix([ + [ t, t**2/2], + [-t**2/2, t]]), 'type_of_equation': 'type3'} + + >>> linodesolve_type(A_t, t, b=b) + {'antiderivative': Matrix([ + [ t, t**2/2], + [-t**2/2, t]]), 'type_of_equation': 'type4'} + + >>> A_non_commutative = Matrix([[1, t], [t, -1]]) + >>> linodesolve_type(A_non_commutative, t) + Traceback (most recent call last): + ... + NotImplementedError: + The system does not have a commutative antiderivative, it cannot be + solved by linodesolve. + + Returns + ======= + + Dict + + Raises + ====== + + NotImplementedError + When the coefficient matrix does not have a commutative antiderivative + + See Also + ======== + + linodesolve: Function for which linodesolve_type gets the information + + """ + + match = {} + is_non_constant = not _matrix_is_constant(A, t) + is_non_homogeneous = not (b is None or b.is_zero_matrix) + type = "type{}".format(int("{}{}".format(int(is_non_constant), int(is_non_homogeneous)), 2) + 1) + + B = None + match.update({"type_of_equation": type, "antiderivative": B}) + + if is_non_constant: + B, is_commuting = _is_commutative_anti_derivative(A, t) + if not is_commuting: + raise NotImplementedError(filldedent(''' + The system does not have a commutative antiderivative, it cannot be solved + by linodesolve. + ''')) + + match['antiderivative'] = B + match.update(_first_order_type5_6_subs(A, t, b=b)) + + return match + + +def _first_order_type5_6_subs(A, t, b=None): + match = {} + + factor_terms = _factor_matrix(A, t) + is_homogeneous = b is None or b.is_zero_matrix + + if factor_terms is not None: + t_ = Symbol("{}_".format(t)) + F_t = integrate(factor_terms[0], t) + inverse = solveset(Eq(t_, F_t), t) + + # Note: A simple way to check if a function is invertible + # or not. + if isinstance(inverse, FiniteSet) and not inverse.has(Piecewise)\ + and len(inverse) == 1: + + A = factor_terms[1] + if not is_homogeneous: + b = b / factor_terms[0] + b = b.subs(t, list(inverse)[0]) + type = "type{}".format(5 + (not is_homogeneous)) + match.update({'func_coeff': A, 'tau': F_t, + 't_': t_, 'type_of_equation': type, 'rhs': b}) + + return match + + +def linear_ode_to_matrix(eqs, funcs, t, order): + r""" + Convert a linear system of ODEs to matrix form + + Explanation + =========== + + Express a system of linear ordinary differential equations as a single + matrix differential equation [1]. For example the system $x' = x + y + 1$ + and $y' = x - y$ can be represented as + + .. math:: A_1 X' = A_0 X + b + + where $A_1$ and $A_0$ are $2 \times 2$ matrices and $b$, $X$ and $X'$ are + $2 \times 1$ matrices with $X = [x, y]^T$. + + Higher-order systems are represented with additional matrices e.g. a + second-order system would look like + + .. math:: A_2 X'' = A_1 X' + A_0 X + b + + Examples + ======== + + >>> from sympy import Function, Symbol, Matrix, Eq + >>> from sympy.solvers.ode.systems import linear_ode_to_matrix + >>> t = Symbol('t') + >>> x = Function('x') + >>> y = Function('y') + + We can create a system of linear ODEs like + + >>> eqs = [ + ... Eq(x(t).diff(t), x(t) + y(t) + 1), + ... Eq(y(t).diff(t), x(t) - y(t)), + ... ] + >>> funcs = [x(t), y(t)] + >>> order = 1 # 1st order system + + Now ``linear_ode_to_matrix`` can represent this as a matrix + differential equation. + + >>> (A1, A0), b = linear_ode_to_matrix(eqs, funcs, t, order) + >>> A1 + Matrix([ + [1, 0], + [0, 1]]) + >>> A0 + Matrix([ + [1, 1], + [1, -1]]) + >>> b + Matrix([ + [1], + [0]]) + + The original equations can be recovered from these matrices: + + >>> eqs_mat = Matrix([eq.lhs - eq.rhs for eq in eqs]) + >>> X = Matrix(funcs) + >>> A1 * X.diff(t) - A0 * X - b == eqs_mat + True + + If the system of equations has a maximum order greater than the + order of the system specified, a ODEOrderError exception is raised. + + >>> eqs = [Eq(x(t).diff(t, 2), x(t).diff(t) + x(t)), Eq(y(t).diff(t), y(t) + x(t))] + >>> linear_ode_to_matrix(eqs, funcs, t, 1) + Traceback (most recent call last): + ... + ODEOrderError: Cannot represent system in 1-order form + + If the system of equations is nonlinear, then ODENonlinearError is + raised. + + >>> eqs = [Eq(x(t).diff(t), x(t) + y(t)), Eq(y(t).diff(t), y(t)**2 + x(t))] + >>> linear_ode_to_matrix(eqs, funcs, t, 1) + Traceback (most recent call last): + ... + ODENonlinearError: The system of ODEs is nonlinear. + + Parameters + ========== + + eqs : list of SymPy expressions or equalities + The equations as expressions (assumed equal to zero). + funcs : list of applied functions + The dependent variables of the system of ODEs. + t : symbol + The independent variable. + order : int + The order of the system of ODEs. + + Returns + ======= + + The tuple ``(As, b)`` where ``As`` is a tuple of matrices and ``b`` is the + the matrix representing the rhs of the matrix equation. + + Raises + ====== + + ODEOrderError + When the system of ODEs have an order greater than what was specified + ODENonlinearError + When the system of ODEs is nonlinear + + See Also + ======== + + linear_eq_to_matrix: for systems of linear algebraic equations. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Matrix_differential_equation + + """ + from sympy.solvers.solveset import linear_eq_to_matrix + + if any(ode_order(eq, func) > order for eq in eqs for func in funcs): + msg = "Cannot represent system in {}-order form" + raise ODEOrderError(msg.format(order)) + + As = [] + + for o in range(order, -1, -1): + # Work from the highest derivative down + syms = [func.diff(t, o) for func in funcs] + + # Ai is the matrix for X(t).diff(t, o) + # eqs is minus the remainder of the equations. + try: + Ai, b = linear_eq_to_matrix(eqs, syms) + except NonlinearError: + raise ODENonlinearError("The system of ODEs is nonlinear.") + + Ai = Ai.applyfunc(expand_mul) + + As.append(Ai if o == order else -Ai) + + if o: + eqs = [-eq for eq in b] + else: + rhs = b + + return As, rhs + + +def matrix_exp(A, t): + r""" + Matrix exponential $\exp(A*t)$ for the matrix ``A`` and scalar ``t``. + + Explanation + =========== + + This functions returns the $\exp(A*t)$ by doing a simple + matrix multiplication: + + .. math:: \exp(A*t) = P * expJ * P^{-1} + + where $expJ$ is $\exp(J*t)$. $J$ is the Jordan normal + form of $A$ and $P$ is matrix such that: + + .. math:: A = P * J * P^{-1} + + The matrix exponential $\exp(A*t)$ appears in the solution of linear + differential equations. For example if $x$ is a vector and $A$ is a matrix + then the initial value problem + + .. math:: \frac{dx(t)}{dt} = A \times x(t), x(0) = x0 + + has the unique solution + + .. math:: x(t) = \exp(A t) x0 + + Examples + ======== + + >>> from sympy import Symbol, Matrix, pprint + >>> from sympy.solvers.ode.systems import matrix_exp + >>> t = Symbol('t') + + We will consider a 2x2 matrix for comupting the exponential + + >>> A = Matrix([[2, -5], [2, -4]]) + >>> pprint(A) + [2 -5] + [ ] + [2 -4] + + Now, exp(A*t) is given as follows: + + >>> pprint(matrix_exp(A, t)) + [ -t -t -t ] + [3*e *sin(t) + e *cos(t) -5*e *sin(t) ] + [ ] + [ -t -t -t ] + [ 2*e *sin(t) - 3*e *sin(t) + e *cos(t)] + + Parameters + ========== + + A : Matrix + The matrix $A$ in the expression $\exp(A*t)$ + t : Symbol + The independent variable + + See Also + ======== + + matrix_exp_jordan_form: For exponential of Jordan normal form + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Jordan_normal_form + .. [2] https://en.wikipedia.org/wiki/Matrix_exponential + + """ + P, expJ = matrix_exp_jordan_form(A, t) + return P * expJ * P.inv() + + +def matrix_exp_jordan_form(A, t): + r""" + Matrix exponential $\exp(A*t)$ for the matrix *A* and scalar *t*. + + Explanation + =========== + + Returns the Jordan form of the $\exp(A*t)$ along with the matrix $P$ such that: + + .. math:: + \exp(A*t) = P * expJ * P^{-1} + + Examples + ======== + + >>> from sympy import Matrix, Symbol + >>> from sympy.solvers.ode.systems import matrix_exp, matrix_exp_jordan_form + >>> t = Symbol('t') + + We will consider a 2x2 defective matrix. This shows that our method + works even for defective matrices. + + >>> A = Matrix([[1, 1], [0, 1]]) + + It can be observed that this function gives us the Jordan normal form + and the required invertible matrix P. + + >>> P, expJ = matrix_exp_jordan_form(A, t) + + Here, it is shown that P and expJ returned by this function is correct + as they satisfy the formula: P * expJ * P_inverse = exp(A*t). + + >>> P * expJ * P.inv() == matrix_exp(A, t) + True + + Parameters + ========== + + A : Matrix + The matrix $A$ in the expression $\exp(A*t)$ + t : Symbol + The independent variable + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Defective_matrix + .. [2] https://en.wikipedia.org/wiki/Jordan_matrix + .. [3] https://en.wikipedia.org/wiki/Jordan_normal_form + + """ + + N, M = A.shape + if N != M: + raise ValueError('Needed square matrix but got shape (%s, %s)' % (N, M)) + elif A.has(t): + raise ValueError('Matrix A should not depend on t') + + def jordan_chains(A): + '''Chains from Jordan normal form analogous to M.eigenvects(). + Returns a dict with eignevalues as keys like: + {e1: [[v111,v112,...], [v121, v122,...]], e2:...} + where vijk is the kth vector in the jth chain for eigenvalue i. + ''' + P, blocks = A.jordan_cells() + basis = [P[:,i] for i in range(P.shape[1])] + n = 0 + chains = {} + for b in blocks: + eigval = b[0, 0] + size = b.shape[0] + if eigval not in chains: + chains[eigval] = [] + chains[eigval].append(basis[n:n+size]) + n += size + return chains + + eigenchains = jordan_chains(A) + + # Needed for consistency across Python versions + eigenchains_iter = sorted(eigenchains.items(), key=default_sort_key) + isreal = not A.has(I) + + blocks = [] + vectors = [] + seen_conjugate = set() + for e, chains in eigenchains_iter: + for chain in chains: + n = len(chain) + if isreal and e != e.conjugate() and e.conjugate() in eigenchains: + if e in seen_conjugate: + continue + seen_conjugate.add(e.conjugate()) + exprt = exp(re(e) * t) + imrt = im(e) * t + imblock = Matrix([[cos(imrt), sin(imrt)], + [-sin(imrt), cos(imrt)]]) + expJblock2 = Matrix(n, n, lambda i,j: + imblock * t**(j-i) / factorial(j-i) if j >= i + else zeros(2, 2)) + expJblock = Matrix(2*n, 2*n, lambda i,j: expJblock2[i//2,j//2][i%2,j%2]) + + blocks.append(exprt * expJblock) + for i in range(n): + vectors.append(re(chain[i])) + vectors.append(im(chain[i])) + else: + vectors.extend(chain) + fun = lambda i,j: t**(j-i)/factorial(j-i) if j >= i else 0 + expJblock = Matrix(n, n, fun) + blocks.append(exp(e * t) * expJblock) + + expJ = Matrix.diag(*blocks) + P = Matrix(N, N, lambda i,j: vectors[j][i]) + + return P, expJ + + +# Note: To add a docstring example with tau +def linodesolve(A, t, b=None, B=None, type="auto", doit=False, + tau=None): + r""" + System of n equations linear first-order differential equations + + Explanation + =========== + + This solver solves the system of ODEs of the following form: + + .. math:: + X'(t) = A(t) X(t) + b(t) + + Here, $A(t)$ is the coefficient matrix, $X(t)$ is the vector of n independent variables, + $b(t)$ is the non-homogeneous term and $X'(t)$ is the derivative of $X(t)$ + + Depending on the properties of $A(t)$ and $b(t)$, this solver evaluates the solution + differently. + + When $A(t)$ is constant coefficient matrix and $b(t)$ is zero vector i.e. system is homogeneous, + the system is "type1". The solution is: + + .. math:: + X(t) = \exp(A t) C + + Here, $C$ is a vector of constants and $A$ is the constant coefficient matrix. + + When $A(t)$ is constant coefficient matrix and $b(t)$ is non-zero i.e. system is non-homogeneous, + the system is "type2". The solution is: + + .. math:: + X(t) = e^{A t} ( \int e^{- A t} b \,dt + C) + + When $A(t)$ is coefficient matrix such that its commutative with its antiderivative $B(t)$ and + $b(t)$ is a zero vector i.e. system is homogeneous, the system is "type3". The solution is: + + .. math:: + X(t) = \exp(B(t)) C + + When $A(t)$ is commutative with its antiderivative $B(t)$ and $b(t)$ is non-zero i.e. system is + non-homogeneous, the system is "type4". The solution is: + + .. math:: + X(t) = e^{B(t)} ( \int e^{-B(t)} b(t) \,dt + C) + + When $A(t)$ is a coefficient matrix such that it can be factorized into a scalar and a constant + coefficient matrix: + + .. math:: + A(t) = f(t) * A + + Where $f(t)$ is a scalar expression in the independent variable $t$ and $A$ is a constant matrix, + then we can do the following substitutions: + + .. math:: + tau = \int f(t) dt, X(t) = Y(tau), b(t) = b(f^{-1}(tau)) + + Here, the substitution for the non-homogeneous term is done only when its non-zero. + Using these substitutions, our original system becomes: + + .. math:: + Y'(tau) = A * Y(tau) + b(tau)/f(tau) + + The above system can be easily solved using the solution for "type1" or "type2" depending + on the homogeneity of the system. After we get the solution for $Y(tau)$, we substitute the + solution for $tau$ as $t$ to get back $X(t)$ + + .. math:: + X(t) = Y(tau) + + Systems of "type5" and "type6" have a commutative antiderivative but we use this solution + because its faster to compute. + + The final solution is the general solution for all the four equations since a constant coefficient + matrix is always commutative with its antidervative. + + An additional feature of this function is, if someone wants to substitute for value of the independent + variable, they can pass the substitution `tau` and the solution will have the independent variable + substituted with the passed expression(`tau`). + + Parameters + ========== + + A : Matrix + Coefficient matrix of the system of linear first order ODEs. + t : Symbol + Independent variable in the system of ODEs. + b : Matrix or None + Non-homogeneous term in the system of ODEs. If None is passed, + a homogeneous system of ODEs is assumed. + B : Matrix or None + Antiderivative of the coefficient matrix. If the antiderivative + is not passed and the solution requires the term, then the solver + would compute it internally. + type : String + Type of the system of ODEs passed. Depending on the type, the + solution is evaluated. The type values allowed and the corresponding + system it solves are: "type1" for constant coefficient homogeneous + "type2" for constant coefficient non-homogeneous, "type3" for non-constant + coefficient homogeneous, "type4" for non-constant coefficient non-homogeneous, + "type5" and "type6" for non-constant coefficient homogeneous and non-homogeneous + systems respectively where the coefficient matrix can be factorized to a constant + coefficient matrix. + The default value is "auto" which will let the solver decide the correct type of + the system passed. + doit : Boolean + Evaluate the solution if True, default value is False + tau: Expression + Used to substitute for the value of `t` after we get the solution of the system. + + Examples + ======== + + To solve the system of ODEs using this function directly, several things must be + done in the right order. Wrong inputs to the function will lead to incorrect results. + + >>> from sympy import symbols, Function, Eq + >>> from sympy.solvers.ode.systems import canonical_odes, linear_ode_to_matrix, linodesolve, linodesolve_type + >>> from sympy.solvers.ode.subscheck import checkodesol + >>> f, g = symbols("f, g", cls=Function) + >>> x, a = symbols("x, a") + >>> funcs = [f(x), g(x)] + >>> eqs = [Eq(f(x).diff(x) - f(x), a*g(x) + 1), Eq(g(x).diff(x) + g(x), a*f(x))] + + Here, it is important to note that before we derive the coefficient matrix, it is + important to get the system of ODEs into the desired form. For that we will use + :obj:`sympy.solvers.ode.systems.canonical_odes()`. + + >>> eqs = canonical_odes(eqs, funcs, x) + >>> eqs + [[Eq(Derivative(f(x), x), a*g(x) + f(x) + 1), Eq(Derivative(g(x), x), a*f(x) - g(x))]] + + Now, we will use :obj:`sympy.solvers.ode.systems.linear_ode_to_matrix()` to get the coefficient matrix and the + non-homogeneous term if it is there. + + >>> eqs = eqs[0] + >>> (A1, A0), b = linear_ode_to_matrix(eqs, funcs, x, 1) + >>> A = A0 + + We have the coefficient matrices and the non-homogeneous term ready. Now, we can use + :obj:`sympy.solvers.ode.systems.linodesolve_type()` to get the information for the system of ODEs + to finally pass it to the solver. + + >>> system_info = linodesolve_type(A, x, b=b) + >>> sol_vector = linodesolve(A, x, b=b, B=system_info['antiderivative'], type=system_info['type_of_equation']) + + Now, we can prove if the solution is correct or not by using :obj:`sympy.solvers.ode.checkodesol()` + + >>> sol = [Eq(f, s) for f, s in zip(funcs, sol_vector)] + >>> checkodesol(eqs, sol) + (True, [0, 0]) + + We can also use the doit method to evaluate the solutions passed by the function. + + >>> sol_vector_evaluated = linodesolve(A, x, b=b, type="type2", doit=True) + + Now, we will look at a system of ODEs which is non-constant. + + >>> eqs = [Eq(f(x).diff(x), f(x) + x*g(x)), Eq(g(x).diff(x), -x*f(x) + g(x))] + + The system defined above is already in the desired form, so we do not have to convert it. + + >>> (A1, A0), b = linear_ode_to_matrix(eqs, funcs, x, 1) + >>> A = A0 + + A user can also pass the commutative antiderivative required for type3 and type4 system of ODEs. + Passing an incorrect one will lead to incorrect results. If the coefficient matrix is not commutative + with its antiderivative, then :obj:`sympy.solvers.ode.systems.linodesolve_type()` raises a NotImplementedError. + If it does have a commutative antiderivative, then the function just returns the information about the system. + + >>> system_info = linodesolve_type(A, x, b=b) + + Now, we can pass the antiderivative as an argument to get the solution. If the system information is not + passed, then the solver will compute the required arguments internally. + + >>> sol_vector = linodesolve(A, x, b=b) + + Once again, we can verify the solution obtained. + + >>> sol = [Eq(f, s) for f, s in zip(funcs, sol_vector)] + >>> checkodesol(eqs, sol) + (True, [0, 0]) + + Returns + ======= + + List + + Raises + ====== + + ValueError + This error is raised when the coefficient matrix, non-homogeneous term + or the antiderivative, if passed, are not a matrix or + do not have correct dimensions + NonSquareMatrixError + When the coefficient matrix or its antiderivative, if passed is not a + square matrix + NotImplementedError + If the coefficient matrix does not have a commutative antiderivative + + See Also + ======== + + linear_ode_to_matrix: Coefficient matrix computation function + canonical_odes: System of ODEs representation change + linodesolve_type: Getting information about systems of ODEs to pass in this solver + + """ + + if not isinstance(A, MatrixBase): + raise ValueError(filldedent('''\ + The coefficients of the system of ODEs should be of type Matrix + ''')) + + if not A.is_square: + raise NonSquareMatrixError(filldedent('''\ + The coefficient matrix must be a square + ''')) + + if b is not None: + if not isinstance(b, MatrixBase): + raise ValueError(filldedent('''\ + The non-homogeneous terms of the system of ODEs should be of type Matrix + ''')) + + if A.rows != b.rows: + raise ValueError(filldedent('''\ + The system of ODEs should have the same number of non-homogeneous terms and the number of + equations + ''')) + + if B is not None: + if not isinstance(B, MatrixBase): + raise ValueError(filldedent('''\ + The antiderivative of coefficients of the system of ODEs should be of type Matrix + ''')) + + if not B.is_square: + raise NonSquareMatrixError(filldedent('''\ + The antiderivative of the coefficient matrix must be a square + ''')) + + if A.rows != B.rows: + raise ValueError(filldedent('''\ + The coefficient matrix and its antiderivative should have same dimensions + ''')) + + if not any(type == "type{}".format(i) for i in range(1, 7)) and not type == "auto": + raise ValueError(filldedent('''\ + The input type should be a valid one + ''')) + + n = A.rows + + # constants = numbered_symbols(prefix='C', cls=Dummy, start=const_idx+1) + Cvect = Matrix([Dummy() for _ in range(n)]) + + if b is None and any(type == typ for typ in ["type2", "type4", "type6"]): + b = zeros(n, 1) + + is_transformed = tau is not None + passed_type = type + + if type == "auto": + system_info = linodesolve_type(A, t, b=b) + type = system_info["type_of_equation"] + B = system_info["antiderivative"] + + if type in ("type5", "type6"): + is_transformed = True + if passed_type != "auto": + if tau is None: + system_info = _first_order_type5_6_subs(A, t, b=b) + if not system_info: + raise ValueError(filldedent(''' + The system passed isn't {}. + '''.format(type))) + + tau = system_info['tau'] + t = system_info['t_'] + A = system_info['A'] + b = system_info['b'] + + intx_wrtt = lambda x: Integral(x, t) if x else 0 + if type in ("type1", "type2", "type5", "type6"): + P, J = matrix_exp_jordan_form(A, t) + P = simplify(P) + + if type in ("type1", "type5"): + sol_vector = P * (J * Cvect) + else: + Jinv = J.subs(t, -t) + sol_vector = P * J * ((Jinv * P.inv() * b).applyfunc(intx_wrtt) + Cvect) + else: + if B is None: + B, _ = _is_commutative_anti_derivative(A, t) + + if type == "type3": + sol_vector = B.exp() * Cvect + else: + sol_vector = B.exp() * (((-B).exp() * b).applyfunc(intx_wrtt) + Cvect) + + if is_transformed: + sol_vector = sol_vector.subs(t, tau) + + gens = sol_vector.atoms(exp) + + if type != "type1": + sol_vector = [expand_mul(s) for s in sol_vector] + + sol_vector = [collect(s, ordered(gens), exact=True) for s in sol_vector] + + if doit: + sol_vector = [s.doit() for s in sol_vector] + + return sol_vector + + +def _matrix_is_constant(M, t): + """Checks if the matrix M is independent of t or not.""" + return all(coef.as_independent(t, as_Add=True)[1] == 0 for coef in M) + + +def canonical_odes(eqs, funcs, t): + r""" + Function that solves for highest order derivatives in a system + + Explanation + =========== + + This function inputs a system of ODEs and based on the system, + the dependent variables and their highest order, returns the system + in the following form: + + .. math:: + X'(t) = A(t) X(t) + b(t) + + Here, $X(t)$ is the vector of dependent variables of lower order, $A(t)$ is + the coefficient matrix, $b(t)$ is the non-homogeneous term and $X'(t)$ is the + vector of dependent variables in their respective highest order. We use the term + canonical form to imply the system of ODEs which is of the above form. + + If the system passed has a non-linear term with multiple solutions, then a list of + systems is returned in its canonical form. + + Parameters + ========== + + eqs : List + List of the ODEs + funcs : List + List of dependent variables + t : Symbol + Independent variable + + Examples + ======== + + >>> from sympy import symbols, Function, Eq, Derivative + >>> from sympy.solvers.ode.systems import canonical_odes + >>> f, g = symbols("f g", cls=Function) + >>> x, y = symbols("x y") + >>> funcs = [f(x), g(x)] + >>> eqs = [Eq(f(x).diff(x) - 7*f(x), 12*g(x)), Eq(g(x).diff(x) + g(x), 20*f(x))] + + >>> canonical_eqs = canonical_odes(eqs, funcs, x) + >>> canonical_eqs + [[Eq(Derivative(f(x), x), 7*f(x) + 12*g(x)), Eq(Derivative(g(x), x), 20*f(x) - g(x))]] + + >>> system = [Eq(Derivative(f(x), x)**2 - 2*Derivative(f(x), x) + 1, 4), Eq(-y*f(x) + Derivative(g(x), x), 0)] + + >>> canonical_system = canonical_odes(system, funcs, x) + >>> canonical_system + [[Eq(Derivative(f(x), x), -1), Eq(Derivative(g(x), x), y*f(x))], [Eq(Derivative(f(x), x), 3), Eq(Derivative(g(x), x), y*f(x))]] + + Returns + ======= + + List + + """ + from sympy.solvers.solvers import solve + + order = _get_func_order(eqs, funcs) + + canon_eqs = solve(eqs, *[func.diff(t, order[func]) for func in funcs], dict=True) + + systems = [] + for eq in canon_eqs: + system = [Eq(func.diff(t, order[func]), eq[func.diff(t, order[func])]) for func in funcs] + systems.append(system) + + return systems + + +def _is_commutative_anti_derivative(A, t): + r""" + Helper function for determining if the Matrix passed is commutative with its antiderivative + + Explanation + =========== + + This function checks if the Matrix $A$ passed is commutative with its antiderivative with respect + to the independent variable $t$. + + .. math:: + B(t) = \int A(t) dt + + The function outputs two values, first one being the antiderivative $B(t)$, second one being a + boolean value, if True, then the matrix $A(t)$ passed is commutative with $B(t)$, else the matrix + passed isn't commutative with $B(t)$. + + Parameters + ========== + + A : Matrix + The matrix which has to be checked + t : Symbol + Independent variable + + Examples + ======== + + >>> from sympy import symbols, Matrix + >>> from sympy.solvers.ode.systems import _is_commutative_anti_derivative + >>> t = symbols("t") + >>> A = Matrix([[1, t], [-t, 1]]) + + >>> B, is_commuting = _is_commutative_anti_derivative(A, t) + >>> is_commuting + True + + Returns + ======= + + Matrix, Boolean + + """ + B = integrate(A, t) + is_commuting = (B*A - A*B).applyfunc(expand).applyfunc(factor_terms).is_zero_matrix + + is_commuting = False if is_commuting is None else is_commuting + + return B, is_commuting + + +def _factor_matrix(A, t): + term = None + for element in A: + temp_term = element.as_independent(t)[1] + if temp_term.has(t): + term = temp_term + break + + if term is not None: + A_factored = (A/term).applyfunc(ratsimp) + can_factor = _matrix_is_constant(A_factored, t) + term = (term, A_factored) if can_factor else None + + return term + + +def _is_second_order_type2(A, t): + term = _factor_matrix(A, t) + is_type2 = False + + if term is not None: + term = 1/term[0] + is_type2 = term.is_polynomial() + + if is_type2: + poly = Poly(term.expand(), t) + monoms = poly.monoms() + + if monoms[0][0] in (2, 4): + cs = _get_poly_coeffs(poly, 4) + a, b, c, d, e = cs + + a1 = powdenest(sqrt(a), force=True) + c1 = powdenest(sqrt(e), force=True) + b1 = powdenest(sqrt(c - 2*a1*c1), force=True) + + is_type2 = (b == 2*a1*b1) and (d == 2*b1*c1) + term = a1*t**2 + b1*t + c1 + + else: + is_type2 = False + + return is_type2, term + + +def _get_poly_coeffs(poly, order): + cs = [0 for _ in range(order+1)] + for c, m in zip(poly.coeffs(), poly.monoms()): + cs[-1-m[0]] = c + return cs + + +def _match_second_order_type(A1, A0, t, b=None): + r""" + Works only for second order system in its canonical form. + + Type 0: Constant coefficient matrix, can be simply solved by + introducing dummy variables. + Type 1: When the substitution: $U = t*X' - X$ works for reducing + the second order system to first order system. + Type 2: When the system is of the form: $poly * X'' = A*X$ where + $poly$ is square of a quadratic polynomial with respect to + *t* and $A$ is a constant coefficient matrix. + + """ + match = {"type_of_equation": "type0"} + n = A1.shape[0] + + if _matrix_is_constant(A1, t) and _matrix_is_constant(A0, t): + return match + + if (A1 + A0*t).applyfunc(expand_mul).is_zero_matrix: + match.update({"type_of_equation": "type1", "A1": A1}) + + elif A1.is_zero_matrix and (b is None or b.is_zero_matrix): + is_type2, term = _is_second_order_type2(A0, t) + if is_type2: + a, b, c = _get_poly_coeffs(Poly(term, t), 2) + A = (A0*(term**2).expand()).applyfunc(ratsimp) + (b**2/4 - a*c)*eye(n, n) + tau = integrate(1/term, t) + t_ = Symbol("{}_".format(t)) + match.update({"type_of_equation": "type2", "A0": A, + "g(t)": sqrt(term), "tau": tau, "is_transformed": True, + "t_": t_}) + + return match + + +def _second_order_subs_type1(A, b, funcs, t): + r""" + For a linear, second order system of ODEs, a particular substitution. + + A system of the below form can be reduced to a linear first order system of + ODEs: + .. math:: + X'' = A(t) * (t*X' - X) + b(t) + + By substituting: + .. math:: U = t*X' - X + + To get the system: + .. math:: U' = t*(A(t)*U + b(t)) + + Where $U$ is the vector of dependent variables, $X$ is the vector of dependent + variables in `funcs` and $X'$ is the first order derivative of $X$ with respect to + $t$. It may or may not reduce the system into linear first order system of ODEs. + + Then a check is made to determine if the system passed can be reduced or not, if + this substitution works, then the system is reduced and its solved for the new + substitution. After we get the solution for $U$: + + .. math:: U = a(t) + + We substitute and return the reduced system: + + .. math:: + a(t) = t*X' - X + + Parameters + ========== + + A: Matrix + Coefficient matrix($A(t)*t$) of the second order system of this form. + b: Matrix + Non-homogeneous term($b(t)$) of the system of ODEs. + funcs: List + List of dependent variables + t: Symbol + Independent variable of the system of ODEs. + + Returns + ======= + + List + + """ + + U = Matrix([t*func.diff(t) - func for func in funcs]) + + sol = linodesolve(A, t, t*b) + reduced_eqs = [Eq(u, s) for s, u in zip(sol, U)] + reduced_eqs = canonical_odes(reduced_eqs, funcs, t)[0] + + return reduced_eqs + + +def _second_order_subs_type2(A, funcs, t_): + r""" + Returns a second order system based on the coefficient matrix passed. + + Explanation + =========== + + This function returns a system of second order ODE of the following form: + + .. math:: + X'' = A * X + + Here, $X$ is the vector of dependent variables, but a bit modified, $A$ is the + coefficient matrix passed. + + Along with returning the second order system, this function also returns the new + dependent variables with the new independent variable `t_` passed. + + Parameters + ========== + + A: Matrix + Coefficient matrix of the system + funcs: List + List of old dependent variables + t_: Symbol + New independent variable + + Returns + ======= + + List, List + + """ + func_names = [func.func.__name__ for func in funcs] + new_funcs = [Function(Dummy("{}_".format(name)))(t_) for name in func_names] + rhss = A * Matrix(new_funcs) + new_eqs = [Eq(func.diff(t_, 2), rhs) for func, rhs in zip(new_funcs, rhss)] + + return new_eqs, new_funcs + + +def _is_euler_system(As, t): + return all(_matrix_is_constant((A*t**i).applyfunc(ratsimp), t) for i, A in enumerate(As)) + + +def _classify_linear_system(eqs, funcs, t, is_canon=False): + r""" + Returns a dictionary with details of the eqs if the system passed is linear + and can be classified by this function else returns None + + Explanation + =========== + + This function takes the eqs, converts it into a form Ax = b where x is a vector of terms + containing dependent variables and their derivatives till their maximum order. If it is + possible to convert eqs into Ax = b, then all the equations in eqs are linear otherwise + they are non-linear. + + To check if the equations are constant coefficient, we need to check if all the terms in + A obtained above are constant or not. + + To check if the equations are homogeneous or not, we need to check if b is a zero matrix + or not. + + Parameters + ========== + + eqs: List + List of ODEs + funcs: List + List of dependent variables + t: Symbol + Independent variable of the equations in eqs + is_canon: Boolean + If True, then this function will not try to get the + system in canonical form. Default value is False + + Returns + ======= + + match = { + 'no_of_equation': len(eqs), + 'eq': eqs, + 'func': funcs, + 'order': order, + 'is_linear': is_linear, + 'is_constant': is_constant, + 'is_homogeneous': is_homogeneous, + } + + Dict or list of Dicts or None + Dict with values for keys: + 1. no_of_equation: Number of equations + 2. eq: The set of equations + 3. func: List of dependent variables + 4. order: A dictionary that gives the order of the + dependent variable in eqs + 5. is_linear: Boolean value indicating if the set of + equations are linear or not. + 6. is_constant: Boolean value indicating if the set of + equations have constant coefficients or not. + 7. is_homogeneous: Boolean value indicating if the set of + equations are homogeneous or not. + 8. commutative_antiderivative: Antiderivative of the coefficient + matrix if the coefficient matrix is non-constant + and commutative with its antiderivative. This key + may or may not exist. + 9. is_general: Boolean value indicating if the system of ODEs is + solvable using one of the general case solvers or not. + 10. rhs: rhs of the non-homogeneous system of ODEs in Matrix form. This + key may or may not exist. + 11. is_higher_order: True if the system passed has an order greater than 1. + This key may or may not exist. + 12. is_second_order: True if the system passed is a second order ODE. This + key may or may not exist. + This Dict is the answer returned if the eqs are linear and constant + coefficient. Otherwise, None is returned. + + """ + + # Error for i == 0 can be added but isn't for now + + # Check for len(funcs) == len(eqs) + if len(funcs) != len(eqs): + raise ValueError("Number of functions given is not equal to the number of equations %s" % funcs) + + # ValueError when functions have more than one arguments + for func in funcs: + if len(func.args) != 1: + raise ValueError("dsolve() and classify_sysode() work with " + "functions of one variable only, not %s" % func) + + # Getting the func_dict and order using the helper + # function + order = _get_func_order(eqs, funcs) + system_order = max(order[func] for func in funcs) + is_higher_order = system_order > 1 + is_second_order = system_order == 2 and all(order[func] == 2 for func in funcs) + + # Not adding the check if the len(func.args) for + # every func in funcs is 1 + + # Linearity check + try: + + canon_eqs = canonical_odes(eqs, funcs, t) if not is_canon else [eqs] + if len(canon_eqs) == 1: + As, b = linear_ode_to_matrix(canon_eqs[0], funcs, t, system_order) + else: + + match = { + 'is_implicit': True, + 'canon_eqs': canon_eqs + } + + return match + + # When the system of ODEs is non-linear, an ODENonlinearError is raised. + # This function catches the error and None is returned. + except ODENonlinearError: + return None + + is_linear = True + + # Homogeneous check + is_homogeneous = True if b.is_zero_matrix else False + + # Is general key is used to identify if the system of ODEs can be solved by + # one of the general case solvers or not. + match = { + 'no_of_equation': len(eqs), + 'eq': eqs, + 'func': funcs, + 'order': order, + 'is_linear': is_linear, + 'is_homogeneous': is_homogeneous, + 'is_general': True + } + + if not is_homogeneous: + match['rhs'] = b + + is_constant = all(_matrix_is_constant(A_, t) for A_ in As) + + # The match['is_linear'] check will be added in the future when this + # function becomes ready to deal with non-linear systems of ODEs + + if not is_higher_order: + A = As[1] + match['func_coeff'] = A + + # Constant coefficient check + is_constant = _matrix_is_constant(A, t) + match['is_constant'] = is_constant + + try: + system_info = linodesolve_type(A, t, b=b) + except NotImplementedError: + return None + + match.update(system_info) + antiderivative = match.pop("antiderivative") + + if not is_constant: + match['commutative_antiderivative'] = antiderivative + + return match + else: + match['type_of_equation'] = "type0" + + if is_second_order: + A1, A0 = As[1:] + + match_second_order = _match_second_order_type(A1, A0, t) + match.update(match_second_order) + + match['is_second_order'] = True + + # If system is constant, then no need to check if its in euler + # form or not. It will be easier and faster to directly proceed + # to solve it. + if match['type_of_equation'] == "type0" and not is_constant: + is_euler = _is_euler_system(As, t) + if is_euler: + t_ = Symbol('{}_'.format(t)) + match.update({'is_transformed': True, 'type_of_equation': 'type1', + 't_': t_}) + else: + is_jordan = lambda M: M == Matrix.jordan_block(M.shape[0], M[0, 0]) + terms = _factor_matrix(As[-1], t) + if all(A.is_zero_matrix for A in As[1:-1]) and terms is not None and not is_jordan(terms[1]): + P, J = terms[1].jordan_form() + match.update({'type_of_equation': 'type2', 'J': J, + 'f(t)': terms[0], 'P': P, 'is_transformed': True}) + + if match['type_of_equation'] != 'type0' and is_second_order: + match.pop('is_second_order', None) + + match['is_higher_order'] = is_higher_order + + return match + +def _preprocess_eqs(eqs): + processed_eqs = [] + for eq in eqs: + processed_eqs.append(eq if isinstance(eq, Equality) else Eq(eq, 0)) + + return processed_eqs + + +def _eqs2dict(eqs, funcs): + eqsorig = {} + eqsmap = {} + funcset = set(funcs) + for eq in eqs: + f1, = eq.lhs.atoms(AppliedUndef) + f2s = (eq.rhs.atoms(AppliedUndef) - {f1}) & funcset + eqsmap[f1] = f2s + eqsorig[f1] = eq + return eqsmap, eqsorig + + +def _dict2graph(d): + nodes = list(d) + edges = [(f1, f2) for f1, f2s in d.items() for f2 in f2s] + G = (nodes, edges) + return G + + +def _is_type1(scc, t): + eqs, funcs = scc + + try: + (A1, A0), b = linear_ode_to_matrix(eqs, funcs, t, 1) + except (ODENonlinearError, ODEOrderError): + return False + + if _matrix_is_constant(A0, t) and b.is_zero_matrix: + return True + + return False + + +def _combine_type1_subsystems(subsystem, funcs, t): + indices = [i for i, sys in enumerate(zip(subsystem, funcs)) if _is_type1(sys, t)] + remove = set() + for ip, i in enumerate(indices): + for j in indices[ip+1:]: + if any(eq2.has(funcs[i]) for eq2 in subsystem[j]): + subsystem[j] = subsystem[i] + subsystem[j] + remove.add(i) + subsystem = [sys for i, sys in enumerate(subsystem) if i not in remove] + return subsystem + + +def _component_division(eqs, funcs, t): + + # Assuming that each eq in eqs is in canonical form, + # that is, [f(x).diff(x) = .., g(x).diff(x) = .., etc] + # and that the system passed is in its first order + eqsmap, eqsorig = _eqs2dict(eqs, funcs) + + subsystems = [] + for cc in connected_components(_dict2graph(eqsmap)): + eqsmap_c = {f: eqsmap[f] for f in cc} + sccs = strongly_connected_components(_dict2graph(eqsmap_c)) + subsystem = [[eqsorig[f] for f in scc] for scc in sccs] + subsystem = _combine_type1_subsystems(subsystem, sccs, t) + subsystems.append(subsystem) + + return subsystems + + +# Returns: List of equations +def _linear_ode_solver(match): + t = match['t'] + funcs = match['func'] + + rhs = match.get('rhs', None) + tau = match.get('tau', None) + t = match['t_'] if 't_' in match else t + A = match['func_coeff'] + + # Note: To make B None when the matrix has constant + # coefficient + B = match.get('commutative_antiderivative', None) + type = match['type_of_equation'] + + sol_vector = linodesolve(A, t, b=rhs, B=B, + type=type, tau=tau) + + sol = [Eq(f, s) for f, s in zip(funcs, sol_vector)] + + return sol + + +def _select_equations(eqs, funcs, key=lambda x: x): + eq_dict = {e.lhs: e.rhs for e in eqs} + return [Eq(f, eq_dict[key(f)]) for f in funcs] + + +def _higher_order_ode_solver(match): + eqs = match["eq"] + funcs = match["func"] + t = match["t"] + sysorder = match['order'] + type = match.get('type_of_equation', "type0") + + is_second_order = match.get('is_second_order', False) + is_transformed = match.get('is_transformed', False) + is_euler = is_transformed and type == "type1" + is_higher_order_type2 = is_transformed and type == "type2" and 'P' in match + + if is_second_order: + new_eqs, new_funcs = _second_order_to_first_order(eqs, funcs, t, + A1=match.get("A1", None), A0=match.get("A0", None), + b=match.get("rhs", None), type=type, + t_=match.get("t_", None)) + else: + new_eqs, new_funcs = _higher_order_to_first_order(eqs, sysorder, t, funcs=funcs, + type=type, J=match.get('J', None), + f_t=match.get('f(t)', None), + P=match.get('P', None), b=match.get('rhs', None)) + + if is_transformed: + t = match.get('t_', t) + + if not is_higher_order_type2: + new_eqs = _select_equations(new_eqs, [f.diff(t) for f in new_funcs]) + + sol = None + + # NotImplementedError may be raised when the system may be actually + # solvable if it can be just divided into sub-systems + try: + if not is_higher_order_type2: + sol = _strong_component_solver(new_eqs, new_funcs, t) + except NotImplementedError: + sol = None + + # Dividing the system only when it becomes essential + if sol is None: + try: + sol = _component_solver(new_eqs, new_funcs, t) + except NotImplementedError: + sol = None + + if sol is None: + return sol + + is_second_order_type2 = is_second_order and type == "type2" + + underscores = '__' if is_transformed else '_' + + sol = _select_equations(sol, funcs, + key=lambda x: Function(Dummy('{}{}0'.format(x.func.__name__, underscores)))(t)) + + if match.get("is_transformed", False): + if is_second_order_type2: + g_t = match["g(t)"] + tau = match["tau"] + sol = [Eq(s.lhs, s.rhs.subs(t, tau) * g_t) for s in sol] + elif is_euler: + t = match['t'] + tau = match['t_'] + sol = [s.subs(tau, log(t)) for s in sol] + elif is_higher_order_type2: + P = match['P'] + sol_vector = P * Matrix([s.rhs for s in sol]) + sol = [Eq(f, s) for f, s in zip(funcs, sol_vector)] + + return sol + + +# Returns: List of equations or None +# If None is returned by this solver, then the system +# of ODEs cannot be solved directly by dsolve_system. +def _strong_component_solver(eqs, funcs, t): + from sympy.solvers.ode.ode import dsolve, constant_renumber + + match = _classify_linear_system(eqs, funcs, t, is_canon=True) + sol = None + + # Assuming that we can't get an implicit system + # since we are already canonical equations from + # dsolve_system + if match: + match['t'] = t + + if match.get('is_higher_order', False): + sol = _higher_order_ode_solver(match) + + elif match.get('is_linear', False): + sol = _linear_ode_solver(match) + + # Note: For now, only linear systems are handled by this function + # hence, the match condition is added. This can be removed later. + if sol is None and len(eqs) == 1: + sol = dsolve(eqs[0], func=funcs[0]) + variables = Tuple(eqs[0]).free_symbols + new_constants = [Dummy() for _ in range(ode_order(eqs[0], funcs[0]))] + sol = constant_renumber(sol, variables=variables, newconstants=new_constants) + sol = [sol] + + # To add non-linear case here in future + + return sol + + +def _get_funcs_from_canon(eqs): + return [eq.lhs.args[0] for eq in eqs] + + +# Returns: List of Equations(a solution) +def _weak_component_solver(wcc, t): + + # We will divide the systems into sccs + # only when the wcc cannot be solved as + # a whole + eqs = [] + for scc in wcc: + eqs += scc + funcs = _get_funcs_from_canon(eqs) + + sol = _strong_component_solver(eqs, funcs, t) + if sol: + return sol + + sol = [] + + for scc in wcc: + eqs = scc + funcs = _get_funcs_from_canon(eqs) + + # Substituting solutions for the dependent + # variables solved in previous SCC, if any solved. + comp_eqs = [eq.subs({s.lhs: s.rhs for s in sol}) for eq in eqs] + scc_sol = _strong_component_solver(comp_eqs, funcs, t) + + if scc_sol is None: + raise NotImplementedError(filldedent(''' + The system of ODEs passed cannot be solved by dsolve_system. + ''')) + + # scc_sol: List of equations + # scc_sol is a solution + sol += scc_sol + + return sol + + +# Returns: List of Equations(a solution) +def _component_solver(eqs, funcs, t): + components = _component_division(eqs, funcs, t) + sol = [] + + for wcc in components: + + # wcc_sol: List of Equations + sol += _weak_component_solver(wcc, t) + + # sol: List of Equations + return sol + + +def _second_order_to_first_order(eqs, funcs, t, type="auto", A1=None, + A0=None, b=None, t_=None): + r""" + Expects the system to be in second order and in canonical form + + Explanation + =========== + + Reduces a second order system into a first order one depending on the type of second + order system. + 1. "type0": If this is passed, then the system will be reduced to first order by + introducing dummy variables. + 2. "type1": If this is passed, then a particular substitution will be used to reduce the + the system into first order. + 3. "type2": If this is passed, then the system will be transformed with new dependent + variables and independent variables. This transformation is a part of solving + the corresponding system of ODEs. + + `A1` and `A0` are the coefficient matrices from the system and it is assumed that the + second order system has the form given below: + + .. math:: + A2 * X'' = A1 * X' + A0 * X + b + + Here, $A2$ is the coefficient matrix for the vector $X''$ and $b$ is the non-homogeneous + term. + + Default value for `b` is None but if `A1` and `A0` are passed and `b` is not passed, then the + system will be assumed homogeneous. + + """ + is_a1 = A1 is None + is_a0 = A0 is None + + if (type == "type1" and is_a1) or (type == "type2" and is_a0)\ + or (type == "auto" and (is_a1 or is_a0)): + (A2, A1, A0), b = linear_ode_to_matrix(eqs, funcs, t, 2) + + if not A2.is_Identity: + raise ValueError(filldedent(''' + The system must be in its canonical form. + ''')) + + if type == "auto": + match = _match_second_order_type(A1, A0, t) + type = match["type_of_equation"] + A1 = match.get("A1", None) + A0 = match.get("A0", None) + + sys_order = dict.fromkeys(funcs, 2) + + if type == "type1": + if b is None: + b = zeros(len(eqs)) + eqs = _second_order_subs_type1(A1, b, funcs, t) + sys_order = dict.fromkeys(funcs, 1) + + if type == "type2": + if t_ is None: + t_ = Symbol("{}_".format(t)) + t = t_ + eqs, funcs = _second_order_subs_type2(A0, funcs, t_) + sys_order = dict.fromkeys(funcs, 2) + + return _higher_order_to_first_order(eqs, sys_order, t, funcs=funcs) + + +def _higher_order_type2_to_sub_systems(J, f_t, funcs, t, max_order, b=None, P=None): + + # Note: To add a test for this ValueError + if J is None or f_t is None or not _matrix_is_constant(J, t): + raise ValueError(filldedent(''' + Correctly input for args 'A' and 'f_t' for Linear, Higher Order, + Type 2 + ''')) + + if P is None and b is not None and not b.is_zero_matrix: + raise ValueError(filldedent(''' + Provide the keyword 'P' for matrix P in A = P * J * P-1. + ''')) + + new_funcs = Matrix([Function(Dummy('{}__0'.format(f.func.__name__)))(t) for f in funcs]) + new_eqs = new_funcs.diff(t, max_order) - f_t * J * new_funcs + + if b is not None and not b.is_zero_matrix: + new_eqs -= P.inv() * b + + new_eqs = canonical_odes(new_eqs, new_funcs, t)[0] + + return new_eqs, new_funcs + + +def _higher_order_to_first_order(eqs, sys_order, t, funcs=None, type="type0", **kwargs): + if funcs is None: + funcs = sys_order.keys() + + # Standard Cauchy Euler system + if type == "type1": + t_ = Symbol('{}_'.format(t)) + new_funcs = [Function(Dummy('{}_'.format(f.func.__name__)))(t_) for f in funcs] + max_order = max(sys_order[func] for func in funcs) + subs_dict = dict(zip(funcs, new_funcs)) + subs_dict[t] = exp(t_) + + free_function = Function(Dummy()) + + def _get_coeffs_from_subs_expression(expr): + if isinstance(expr, Subs): + free_symbol = expr.args[1][0] + term = expr.args[0] + return {ode_order(term, free_symbol): 1} + + if isinstance(expr, Mul): + coeff = expr.args[0] + order = list(_get_coeffs_from_subs_expression(expr.args[1]).keys())[0] + return {order: coeff} + + if isinstance(expr, Add): + coeffs = {} + for arg in expr.args: + + if isinstance(arg, Mul): + coeffs.update(_get_coeffs_from_subs_expression(arg)) + + else: + order = list(_get_coeffs_from_subs_expression(arg).keys())[0] + coeffs[order] = 1 + + return coeffs + + for o in range(1, max_order + 1): + expr = free_function(log(t_)).diff(t_, o)*t_**o + coeff_dict = _get_coeffs_from_subs_expression(expr) + coeffs = [coeff_dict[order] if order in coeff_dict else 0 for order in range(o + 1)] + expr_to_subs = sum(free_function(t_).diff(t_, i) * c for i, c in + enumerate(coeffs)) / t**o + subs_dict.update({f.diff(t, o): expr_to_subs.subs(free_function(t_), nf) + for f, nf in zip(funcs, new_funcs)}) + + new_eqs = [eq.subs(subs_dict) for eq in eqs] + new_sys_order = {nf: sys_order[f] for f, nf in zip(funcs, new_funcs)} + + new_eqs = canonical_odes(new_eqs, new_funcs, t_)[0] + + return _higher_order_to_first_order(new_eqs, new_sys_order, t_, funcs=new_funcs) + + # Systems of the form: X(n)(t) = f(t)*A*X + b + # where X(n)(t) is the nth derivative of the vector of dependent variables + # with respect to the independent variable and A is a constant matrix. + if type == "type2": + J = kwargs.get('J', None) + f_t = kwargs.get('f_t', None) + b = kwargs.get('b', None) + P = kwargs.get('P', None) + max_order = max(sys_order[func] for func in funcs) + + return _higher_order_type2_to_sub_systems(J, f_t, funcs, t, max_order, P=P, b=b) + + # Note: To be changed to this after doit option is disabled for default cases + # new_sysorder = _get_func_order(new_eqs, new_funcs) + # + # return _higher_order_to_first_order(new_eqs, new_sysorder, t, funcs=new_funcs) + + new_funcs = [] + + for prev_func in funcs: + func_name = prev_func.func.__name__ + func = Function(Dummy('{}_0'.format(func_name)))(t) + new_funcs.append(func) + subs_dict = {prev_func: func} + new_eqs = [] + + for i in range(1, sys_order[prev_func]): + new_func = Function(Dummy('{}_{}'.format(func_name, i)))(t) + subs_dict[prev_func.diff(t, i)] = new_func + new_funcs.append(new_func) + + prev_f = subs_dict[prev_func.diff(t, i-1)] + new_eq = Eq(prev_f.diff(t), new_func) + new_eqs.append(new_eq) + + eqs = [eq.subs(subs_dict) for eq in eqs] + new_eqs + + return eqs, new_funcs + + +def dsolve_system(eqs, funcs=None, t=None, ics=None, doit=False, simplify=True): + r""" + Solves any(supported) system of Ordinary Differential Equations + + Explanation + =========== + + This function takes a system of ODEs as an input, determines if the + it is solvable by this function, and returns the solution if found any. + + This function can handle: + 1. Linear, First Order, Constant coefficient homogeneous system of ODEs + 2. Linear, First Order, Constant coefficient non-homogeneous system of ODEs + 3. Linear, First Order, non-constant coefficient homogeneous system of ODEs + 4. Linear, First Order, non-constant coefficient non-homogeneous system of ODEs + 5. Any implicit system which can be divided into system of ODEs which is of the above 4 forms + 6. Any higher order linear system of ODEs that can be reduced to one of the 5 forms of systems described above. + + The types of systems described above are not limited by the number of equations, i.e. this + function can solve the above types irrespective of the number of equations in the system passed. + But, the bigger the system, the more time it will take to solve the system. + + This function returns a list of solutions. Each solution is a list of equations where LHS is + the dependent variable and RHS is an expression in terms of the independent variable. + + Among the non constant coefficient types, not all the systems are solvable by this function. Only + those which have either a coefficient matrix with a commutative antiderivative or those systems which + may be divided further so that the divided systems may have coefficient matrix with commutative antiderivative. + + Parameters + ========== + + eqs : List + system of ODEs to be solved + funcs : List or None + List of dependent variables that make up the system of ODEs + t : Symbol or None + Independent variable in the system of ODEs + ics : Dict or None + Set of initial boundary/conditions for the system of ODEs + doit : Boolean + Evaluate the solutions if True. Default value is True. Can be + set to false if the integral evaluation takes too much time and/or + is not required. + simplify: Boolean + Simplify the solutions for the systems. Default value is True. + Can be set to false if simplification takes too much time and/or + is not required. + + Examples + ======== + + >>> from sympy import symbols, Eq, Function + >>> from sympy.solvers.ode.systems import dsolve_system + >>> f, g = symbols("f g", cls=Function) + >>> x = symbols("x") + + >>> eqs = [Eq(f(x).diff(x), g(x)), Eq(g(x).diff(x), f(x))] + >>> dsolve_system(eqs) + [[Eq(f(x), -C1*exp(-x) + C2*exp(x)), Eq(g(x), C1*exp(-x) + C2*exp(x))]] + + You can also pass the initial conditions for the system of ODEs: + + >>> dsolve_system(eqs, ics={f(0): 1, g(0): 0}) + [[Eq(f(x), exp(x)/2 + exp(-x)/2), Eq(g(x), exp(x)/2 - exp(-x)/2)]] + + Optionally, you can pass the dependent variables and the independent + variable for which the system is to be solved: + + >>> funcs = [f(x), g(x)] + >>> dsolve_system(eqs, funcs=funcs, t=x) + [[Eq(f(x), -C1*exp(-x) + C2*exp(x)), Eq(g(x), C1*exp(-x) + C2*exp(x))]] + + Lets look at an implicit system of ODEs: + + >>> eqs = [Eq(f(x).diff(x)**2, g(x)**2), Eq(g(x).diff(x), g(x))] + >>> dsolve_system(eqs) + [[Eq(f(x), C1 - C2*exp(x)), Eq(g(x), C2*exp(x))], [Eq(f(x), C1 + C2*exp(x)), Eq(g(x), C2*exp(x))]] + + Returns + ======= + + List of List of Equations + + Raises + ====== + + NotImplementedError + When the system of ODEs is not solvable by this function. + ValueError + When the parameters passed are not in the required form. + + """ + from sympy.solvers.ode.ode import solve_ics, _extract_funcs, constant_renumber + + if not iterable(eqs): + raise ValueError(filldedent(''' + List of equations should be passed. The input is not valid. + ''')) + + eqs = _preprocess_eqs(eqs) + + if funcs is not None and not isinstance(funcs, list): + raise ValueError(filldedent(''' + Input to the funcs should be a list of functions. + ''')) + + if funcs is None: + funcs = _extract_funcs(eqs) + + if any(len(func.args) != 1 for func in funcs): + raise ValueError(filldedent(''' + dsolve_system can solve a system of ODEs with only one independent + variable. + ''')) + + if len(eqs) != len(funcs): + raise ValueError(filldedent(''' + Number of equations and number of functions do not match + ''')) + + if t is not None and not isinstance(t, Symbol): + raise ValueError(filldedent(''' + The independent variable must be of type Symbol + ''')) + + if t is None: + t = list(list(eqs[0].atoms(Derivative))[0].atoms(Symbol))[0] + + sols = [] + canon_eqs = canonical_odes(eqs, funcs, t) + + for canon_eq in canon_eqs: + try: + sol = _strong_component_solver(canon_eq, funcs, t) + except NotImplementedError: + sol = None + + if sol is None: + sol = _component_solver(canon_eq, funcs, t) + + sols.append(sol) + + if sols: + final_sols = [] + variables = Tuple(*eqs).free_symbols + + for sol in sols: + + sol = _select_equations(sol, funcs) + sol = constant_renumber(sol, variables=variables) + + if ics: + constants = Tuple(*sol).free_symbols - variables + solved_constants = solve_ics(sol, funcs, constants, ics) + sol = [s.subs(solved_constants) for s in sol] + + if simplify: + constants = Tuple(*sol).free_symbols - variables + sol = simpsol(sol, [t], constants, doit=doit) + + final_sols.append(sol) + + sols = final_sols + + return sols diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/tests/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/tests/test_lie_group.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/tests/test_lie_group.py new file mode 100644 index 0000000000000000000000000000000000000000..153d30ff563773819e49c989f447c1ec7962169b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/tests/test_lie_group.py @@ -0,0 +1,152 @@ +from sympy.core.function import Function +from sympy.core.numbers import Rational +from sympy.core.relational import Eq +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (atan, sin, tan) + +from sympy.solvers.ode import (classify_ode, checkinfsol, dsolve, infinitesimals) + +from sympy.solvers.ode.subscheck import checkodesol + +from sympy.testing.pytest import XFAIL + + +C1 = Symbol('C1') +x, y = symbols("x y") +f = Function('f') +xi = Function('xi') +eta = Function('eta') + + +def test_heuristic1(): + a, b, c, a4, a3, a2, a1, a0 = symbols("a b c a4 a3 a2 a1 a0") + df = f(x).diff(x) + eq = Eq(df, x**2*f(x)) + eq1 = f(x).diff(x) + a*f(x) - c*exp(b*x) + eq2 = f(x).diff(x) + 2*x*f(x) - x*exp(-x**2) + eq3 = (1 + 2*x)*df + 2 - 4*exp(-f(x)) + eq4 = f(x).diff(x) - (a4*x**4 + a3*x**3 + a2*x**2 + a1*x + a0)**Rational(-1, 2) + eq5 = x**2*df - f(x) + x**2*exp(x - (1/x)) + eqlist = [eq, eq1, eq2, eq3, eq4, eq5] + + i = infinitesimals(eq, hint='abaco1_simple') + assert i == [{eta(x, f(x)): exp(x**3/3), xi(x, f(x)): 0}, + {eta(x, f(x)): f(x), xi(x, f(x)): 0}, + {eta(x, f(x)): 0, xi(x, f(x)): x**(-2)}] + i1 = infinitesimals(eq1, hint='abaco1_simple') + assert i1 == [{eta(x, f(x)): exp(-a*x), xi(x, f(x)): 0}] + i2 = infinitesimals(eq2, hint='abaco1_simple') + assert i2 == [{eta(x, f(x)): exp(-x**2), xi(x, f(x)): 0}] + i3 = infinitesimals(eq3, hint='abaco1_simple') + assert i3 == [{eta(x, f(x)): 0, xi(x, f(x)): 2*x + 1}, + {eta(x, f(x)): 0, xi(x, f(x)): 1/(exp(f(x)) - 2)}] + i4 = infinitesimals(eq4, hint='abaco1_simple') + assert i4 == [{eta(x, f(x)): 1, xi(x, f(x)): 0}, + {eta(x, f(x)): 0, + xi(x, f(x)): sqrt(a0 + a1*x + a2*x**2 + a3*x**3 + a4*x**4)}] + i5 = infinitesimals(eq5, hint='abaco1_simple') + assert i5 == [{xi(x, f(x)): 0, eta(x, f(x)): exp(-1/x)}] + + ilist = [i, i1, i2, i3, i4, i5] + for eq, i in (zip(eqlist, ilist)): + check = checkinfsol(eq, i) + assert check[0] + + # This ODE can be solved by the Lie Group method, when there are + # better assumptions + eq6 = df - (f(x)/x)*(x*log(x**2/f(x)) + 2) + i = infinitesimals(eq6, hint='abaco1_product') + assert i == [{eta(x, f(x)): f(x)*exp(-x), xi(x, f(x)): 0}] + assert checkinfsol(eq6, i)[0] + + eq7 = x*(f(x).diff(x)) + 1 - f(x)**2 + i = infinitesimals(eq7, hint='chi') + assert checkinfsol(eq7, i)[0] + + +def test_heuristic3(): + a, b = symbols("a b") + df = f(x).diff(x) + + eq = x**2*df + x*f(x) + f(x)**2 + x**2 + i = infinitesimals(eq, hint='bivariate') + assert i == [{eta(x, f(x)): f(x), xi(x, f(x)): x}] + assert checkinfsol(eq, i)[0] + + eq = x**2*(-f(x)**2 + df)- a*x**2*f(x) + 2 - a*x + i = infinitesimals(eq, hint='bivariate') + assert checkinfsol(eq, i)[0] + + +def test_heuristic_function_sum(): + eq = f(x).diff(x) - (3*(1 + x**2/f(x)**2)*atan(f(x)/x) + (1 - 2*f(x))/x + + (1 - 3*f(x))*(x/f(x)**2)) + i = infinitesimals(eq, hint='function_sum') + assert i == [{eta(x, f(x)): f(x)**(-2) + x**(-2), xi(x, f(x)): 0}] + assert checkinfsol(eq, i)[0] + + +def test_heuristic_abaco2_similar(): + a, b = symbols("a b") + F = Function('F') + eq = f(x).diff(x) - F(a*x + b*f(x)) + i = infinitesimals(eq, hint='abaco2_similar') + assert i == [{eta(x, f(x)): -a/b, xi(x, f(x)): 1}] + assert checkinfsol(eq, i)[0] + + eq = f(x).diff(x) - (f(x)**2 / (sin(f(x) - x) - x**2 + 2*x*f(x))) + i = infinitesimals(eq, hint='abaco2_similar') + assert i == [{eta(x, f(x)): f(x)**2, xi(x, f(x)): f(x)**2}] + assert checkinfsol(eq, i)[0] + + +def test_heuristic_abaco2_unique_unknown(): + + a, b = symbols("a b") + F = Function('F') + eq = f(x).diff(x) - x**(a - 1)*(f(x)**(1 - b))*F(x**a/a + f(x)**b/b) + i = infinitesimals(eq, hint='abaco2_unique_unknown') + assert i == [{eta(x, f(x)): -f(x)*f(x)**(-b), xi(x, f(x)): x*x**(-a)}] + assert checkinfsol(eq, i)[0] + + eq = f(x).diff(x) + tan(F(x**2 + f(x)**2) + atan(x/f(x))) + i = infinitesimals(eq, hint='abaco2_unique_unknown') + assert i == [{eta(x, f(x)): x, xi(x, f(x)): -f(x)}] + assert checkinfsol(eq, i)[0] + + eq = (x*f(x).diff(x) + f(x) + 2*x)**2 -4*x*f(x) -4*x**2 -4*a + i = infinitesimals(eq, hint='abaco2_unique_unknown') + assert checkinfsol(eq, i)[0] + + +def test_heuristic_linear(): + a, b, m, n = symbols("a b m n") + + eq = x**(n*(m + 1) - m)*(f(x).diff(x)) - a*f(x)**n -b*x**(n*(m + 1)) + i = infinitesimals(eq, hint='linear') + assert checkinfsol(eq, i)[0] + + +@XFAIL +def test_kamke(): + a, b, alpha, c = symbols("a b alpha c") + eq = x**2*(a*f(x)**2+(f(x).diff(x))) + b*x**alpha + c + i = infinitesimals(eq, hint='sum_function') # XFAIL + assert checkinfsol(eq, i)[0] + + +def test_user_infinitesimals(): + x = Symbol("x") # assuming x is real generates an error + eq = x*(f(x).diff(x)) + 1 - f(x)**2 + sol = Eq(f(x), (C1 + x**2)/(C1 - x**2)) + infinitesimals = {'xi':sqrt(f(x) - 1)/sqrt(f(x) + 1), 'eta':0} + assert dsolve(eq, hint='lie_group', **infinitesimals) == sol + assert checkodesol(eq, sol) == (True, 0) + + +@XFAIL +def test_lie_group_issue15219(): + eqn = exp(f(x).diff(x)-f(x)) + assert 'lie_group' not in classify_ode(eqn, f(x)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/tests/test_ode.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/tests/test_ode.py new file mode 100644 index 0000000000000000000000000000000000000000..65e0fa62d52445a4669f3cdc5ef278dbf9c88ea4 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/tests/test_ode.py @@ -0,0 +1,1105 @@ +from sympy.core.function import (Derivative, Function, Subs, diff) +from sympy.core.numbers import (E, I, Rational, pi) +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.complexes import (im, re) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.hyperbolic import acosh +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (atan2, cos, sin, tan) +from sympy.integrals.integrals import Integral +from sympy.polys.polytools import Poly +from sympy.series.order import O +from sympy.simplify.radsimp import collect + +from sympy.solvers.ode import (classify_ode, + homogeneous_order, dsolve) + +from sympy.solvers.ode.subscheck import checkodesol +from sympy.solvers.ode.ode import (classify_sysode, + constant_renumber, constantsimp, get_numbered_constants, solve_ics) + +from sympy.solvers.ode.nonhomogeneous import _undetermined_coefficients_match +from sympy.solvers.ode.single import LinearCoefficients +from sympy.solvers.deutils import ode_order +from sympy.testing.pytest import XFAIL, raises, slow, SKIP +from sympy.utilities.misc import filldedent + + +C0, C1, C2, C3, C4, C5, C6, C7, C8, C9, C10 = symbols('C0:11') +u, x, y, z = symbols('u,x:z', real=True) +f = Function('f') +g = Function('g') +h = Function('h') + +# Note: Examples which were specifically testing Single ODE solver are moved to test_single.py +# and all the system of ode examples are moved to test_systems.py +# Note: the tests below may fail (but still be correct) if ODE solver, +# the integral engine, solve(), or even simplify() changes. Also, in +# differently formatted solutions, the arbitrary constants might not be +# equal. Using specific hints in tests can help to avoid this. + +# Tests of order higher than 1 should run the solutions through +# constant_renumber because it will normalize it (constant_renumber causes +# dsolve() to return different results on different machines) + + +def test_get_numbered_constants(): + with raises(ValueError): + get_numbered_constants(None) + + +def test_dsolve_all_hint(): + eq = f(x).diff(x) + output = dsolve(eq, hint='all') + + # Match the Dummy variables: + sol1 = output['separable_Integral'] + _y = sol1.lhs.args[1][0] + sol1 = output['1st_homogeneous_coeff_subs_dep_div_indep_Integral'] + _u1 = sol1.rhs.args[1].args[1][0] + + expected = {'Bernoulli_Integral': Eq(f(x), C1 + Integral(0, x)), + '1st_homogeneous_coeff_best': Eq(f(x), C1), + 'Bernoulli': Eq(f(x), C1), + 'nth_algebraic': Eq(f(x), C1), + 'nth_linear_euler_eq_homogeneous': Eq(f(x), C1), + 'nth_linear_constant_coeff_homogeneous': Eq(f(x), C1), + 'separable': Eq(f(x), C1), + '1st_homogeneous_coeff_subs_indep_div_dep': Eq(f(x), C1), + 'nth_algebraic_Integral': Eq(f(x), C1), + '1st_linear': Eq(f(x), C1), + '1st_linear_Integral': Eq(f(x), C1 + Integral(0, x)), + '1st_exact': Eq(f(x), C1), + '1st_exact_Integral': Eq(Subs(Integral(0, x) + Integral(1, _y), _y, f(x)), C1), + 'lie_group': Eq(f(x), C1), + '1st_homogeneous_coeff_subs_dep_div_indep': Eq(f(x), C1), + '1st_homogeneous_coeff_subs_dep_div_indep_Integral': Eq(log(x), C1 + Integral(-1/_u1, (_u1, f(x)/x))), + '1st_power_series': Eq(f(x), C1), + 'separable_Integral': Eq(Integral(1, (_y, f(x))), C1 + Integral(0, x)), + '1st_homogeneous_coeff_subs_indep_div_dep_Integral': Eq(f(x), C1), + 'best': Eq(f(x), C1), + 'best_hint': 'nth_algebraic', + 'default': 'nth_algebraic', + 'order': 1} + assert output == expected + + assert dsolve(eq, hint='best') == Eq(f(x), C1) + + +def test_dsolve_ics(): + # Maybe this should just use one of the solutions instead of raising... + with raises(NotImplementedError): + dsolve(f(x).diff(x) - sqrt(f(x)), ics={f(1):1}) + + +@slow +def test_dsolve_options(): + eq = x*f(x).diff(x) + f(x) + a = dsolve(eq, hint='all') + b = dsolve(eq, hint='all', simplify=False) + c = dsolve(eq, hint='all_Integral') + keys = ['1st_exact', '1st_exact_Integral', '1st_homogeneous_coeff_best', + '1st_homogeneous_coeff_subs_dep_div_indep', + '1st_homogeneous_coeff_subs_dep_div_indep_Integral', + '1st_homogeneous_coeff_subs_indep_div_dep', + '1st_homogeneous_coeff_subs_indep_div_dep_Integral', '1st_linear', + '1st_linear_Integral', 'Bernoulli', 'Bernoulli_Integral', + 'almost_linear', 'almost_linear_Integral', 'best', 'best_hint', + 'default', 'factorable', 'lie_group', + 'nth_linear_euler_eq_homogeneous', 'order', + 'separable', 'separable_Integral'] + Integral_keys = ['1st_exact_Integral', + '1st_homogeneous_coeff_subs_dep_div_indep_Integral', + '1st_homogeneous_coeff_subs_indep_div_dep_Integral', '1st_linear_Integral', + 'Bernoulli_Integral', 'almost_linear_Integral', 'best', 'best_hint', 'default', + 'factorable', 'nth_linear_euler_eq_homogeneous', + 'order', 'separable_Integral'] + assert sorted(a.keys()) == keys + assert a['order'] == ode_order(eq, f(x)) + assert a['best'] == Eq(f(x), C1/x) + assert dsolve(eq, hint='best') == Eq(f(x), C1/x) + assert a['default'] == 'factorable' + assert a['best_hint'] == 'factorable' + assert not a['1st_exact'].has(Integral) + assert not a['separable'].has(Integral) + assert not a['1st_homogeneous_coeff_best'].has(Integral) + assert not a['1st_homogeneous_coeff_subs_dep_div_indep'].has(Integral) + assert not a['1st_homogeneous_coeff_subs_indep_div_dep'].has(Integral) + assert not a['1st_linear'].has(Integral) + assert a['1st_linear_Integral'].has(Integral) + assert a['1st_exact_Integral'].has(Integral) + assert a['1st_homogeneous_coeff_subs_dep_div_indep_Integral'].has(Integral) + assert a['1st_homogeneous_coeff_subs_indep_div_dep_Integral'].has(Integral) + assert a['separable_Integral'].has(Integral) + assert sorted(b.keys()) == keys + assert b['order'] == ode_order(eq, f(x)) + assert b['best'] == Eq(f(x), C1/x) + assert dsolve(eq, hint='best', simplify=False) == Eq(f(x), C1/x) + assert b['default'] == 'factorable' + assert b['best_hint'] == 'factorable' + assert a['separable'] != b['separable'] + assert a['1st_homogeneous_coeff_subs_dep_div_indep'] != \ + b['1st_homogeneous_coeff_subs_dep_div_indep'] + assert a['1st_homogeneous_coeff_subs_indep_div_dep'] != \ + b['1st_homogeneous_coeff_subs_indep_div_dep'] + assert not b['1st_exact'].has(Integral) + assert not b['separable'].has(Integral) + assert not b['1st_homogeneous_coeff_best'].has(Integral) + assert not b['1st_homogeneous_coeff_subs_dep_div_indep'].has(Integral) + assert not b['1st_homogeneous_coeff_subs_indep_div_dep'].has(Integral) + assert not b['1st_linear'].has(Integral) + assert b['1st_linear_Integral'].has(Integral) + assert b['1st_exact_Integral'].has(Integral) + assert b['1st_homogeneous_coeff_subs_dep_div_indep_Integral'].has(Integral) + assert b['1st_homogeneous_coeff_subs_indep_div_dep_Integral'].has(Integral) + assert b['separable_Integral'].has(Integral) + assert sorted(c.keys()) == Integral_keys + raises(ValueError, lambda: dsolve(eq, hint='notarealhint')) + raises(ValueError, lambda: dsolve(eq, hint='Liouville')) + assert dsolve(f(x).diff(x) - 1/f(x)**2, hint='all')['best'] == \ + dsolve(f(x).diff(x) - 1/f(x)**2, hint='best') + assert dsolve(f(x) + f(x).diff(x) + sin(x).diff(x) + 1, f(x), + hint="1st_linear_Integral") == \ + Eq(f(x), (C1 + Integral((-sin(x).diff(x) - 1)* + exp(Integral(1, x)), x))*exp(-Integral(1, x))) + + +def test_classify_ode(): + assert classify_ode(f(x).diff(x, 2), f(x)) == \ + ( + 'nth_algebraic', + 'nth_linear_constant_coeff_homogeneous', + 'nth_linear_euler_eq_homogeneous', + 'Liouville', + '2nd_power_series_ordinary', + 'nth_algebraic_Integral', + 'Liouville_Integral', + ) + assert classify_ode(f(x), f(x)) == ('nth_algebraic', 'nth_algebraic_Integral') + assert classify_ode(Eq(f(x).diff(x), 0), f(x)) == ( + 'nth_algebraic', + 'separable', + '1st_exact', + '1st_linear', + 'Bernoulli', + '1st_homogeneous_coeff_best', + '1st_homogeneous_coeff_subs_indep_div_dep', + '1st_homogeneous_coeff_subs_dep_div_indep', + '1st_power_series', 'lie_group', + 'nth_linear_constant_coeff_homogeneous', + 'nth_linear_euler_eq_homogeneous', + 'nth_algebraic_Integral', + 'separable_Integral', + '1st_exact_Integral', + '1st_linear_Integral', + 'Bernoulli_Integral', + '1st_homogeneous_coeff_subs_indep_div_dep_Integral', + '1st_homogeneous_coeff_subs_dep_div_indep_Integral') + assert classify_ode(f(x).diff(x)**2, f(x)) == ('factorable', + 'nth_algebraic', + 'separable', + '1st_exact', + '1st_linear', + 'Bernoulli', + '1st_homogeneous_coeff_best', + '1st_homogeneous_coeff_subs_indep_div_dep', + '1st_homogeneous_coeff_subs_dep_div_indep', + '1st_power_series', + 'lie_group', + 'nth_linear_euler_eq_homogeneous', + 'nth_algebraic_Integral', + 'separable_Integral', + '1st_exact_Integral', + '1st_linear_Integral', + 'Bernoulli_Integral', + '1st_homogeneous_coeff_subs_indep_div_dep_Integral', + '1st_homogeneous_coeff_subs_dep_div_indep_Integral') + # issue 4749: f(x) should be cleared from highest derivative before classifying + a = classify_ode(Eq(f(x).diff(x) + f(x), x), f(x)) + b = classify_ode(f(x).diff(x)*f(x) + f(x)*f(x) - x*f(x), f(x)) + c = classify_ode(f(x).diff(x)/f(x) + f(x)/f(x) - x/f(x), f(x)) + assert a == ('1st_exact', + '1st_linear', + 'Bernoulli', + 'almost_linear', + '1st_power_series', "lie_group", + 'nth_linear_constant_coeff_undetermined_coefficients', + 'nth_linear_constant_coeff_variation_of_parameters', + '1st_exact_Integral', + '1st_linear_Integral', + 'Bernoulli_Integral', + 'almost_linear_Integral', + 'nth_linear_constant_coeff_variation_of_parameters_Integral') + assert b == ('factorable', + '1st_linear', + 'Bernoulli', + '1st_power_series', + 'lie_group', + 'nth_linear_constant_coeff_undetermined_coefficients', + 'nth_linear_constant_coeff_variation_of_parameters', + '1st_linear_Integral', + 'Bernoulli_Integral', + 'nth_linear_constant_coeff_variation_of_parameters_Integral') + assert c == ('factorable', + '1st_linear', + 'Bernoulli', + '1st_power_series', + 'lie_group', + 'nth_linear_constant_coeff_undetermined_coefficients', + 'nth_linear_constant_coeff_variation_of_parameters', + '1st_linear_Integral', + 'Bernoulli_Integral', + 'nth_linear_constant_coeff_variation_of_parameters_Integral') + + assert classify_ode( + 2*x*f(x)*f(x).diff(x) + (1 + x)*f(x)**2 - exp(x), f(x) + ) == ('factorable', '1st_exact', 'Bernoulli', 'almost_linear', 'lie_group', + '1st_exact_Integral', 'Bernoulli_Integral', 'almost_linear_Integral') + assert 'Riccati_special_minus2' in \ + classify_ode(2*f(x).diff(x) + f(x)**2 - f(x)/x + 3*x**(-2), f(x)) + raises(ValueError, lambda: classify_ode(x + f(x, y).diff(x).diff( + y), f(x, y))) + # issue 5176 + k = Symbol('k') + assert classify_ode(f(x).diff(x)/(k*f(x) + k*x*f(x)) + 2*f(x)/(k*f(x) + + k*x*f(x)) + x*f(x).diff(x)/(k*f(x) + k*x*f(x)) + z, f(x)) == \ + ('factorable', 'separable', '1st_exact', '1st_linear', 'Bernoulli', + '1st_power_series', 'lie_group', 'separable_Integral', '1st_exact_Integral', + '1st_linear_Integral', 'Bernoulli_Integral') + # preprocessing + ans = ('factorable', 'nth_algebraic', 'separable', '1st_exact', '1st_linear', 'Bernoulli', + '1st_homogeneous_coeff_best', + '1st_homogeneous_coeff_subs_indep_div_dep', + '1st_homogeneous_coeff_subs_dep_div_indep', + '1st_power_series', 'lie_group', + 'nth_linear_constant_coeff_undetermined_coefficients', + 'nth_linear_euler_eq_nonhomogeneous_undetermined_coefficients', + 'nth_linear_constant_coeff_variation_of_parameters', + 'nth_linear_euler_eq_nonhomogeneous_variation_of_parameters', + 'nth_algebraic_Integral', + 'separable_Integral', '1st_exact_Integral', + '1st_linear_Integral', + 'Bernoulli_Integral', + '1st_homogeneous_coeff_subs_indep_div_dep_Integral', + '1st_homogeneous_coeff_subs_dep_div_indep_Integral', + 'nth_linear_constant_coeff_variation_of_parameters_Integral', + 'nth_linear_euler_eq_nonhomogeneous_variation_of_parameters_Integral') + # w/o f(x) given + assert classify_ode(diff(f(x) + x, x) + diff(f(x), x)) == ans + # w/ f(x) and prep=True + assert classify_ode(diff(f(x) + x, x) + diff(f(x), x), f(x), + prep=True) == ans + + assert classify_ode(Eq(2*x**3*f(x).diff(x), 0), f(x)) == \ + ('factorable', 'nth_algebraic', 'separable', '1st_exact', + '1st_linear', 'Bernoulli', '1st_power_series', + 'lie_group', 'nth_linear_euler_eq_homogeneous', + 'nth_algebraic_Integral', 'separable_Integral', '1st_exact_Integral', + '1st_linear_Integral', 'Bernoulli_Integral') + + + assert classify_ode(Eq(2*f(x)**3*f(x).diff(x), 0), f(x)) == \ + ('factorable', 'nth_algebraic', 'separable', '1st_exact', '1st_linear', + 'Bernoulli', '1st_power_series', 'lie_group', 'nth_algebraic_Integral', + 'separable_Integral', '1st_exact_Integral', '1st_linear_Integral', + 'Bernoulli_Integral') + # test issue 13864 + assert classify_ode(Eq(diff(f(x), x) - f(x)**x, 0), f(x)) == \ + ('1st_power_series', 'lie_group') + assert isinstance(classify_ode(Eq(f(x), 5), f(x), dict=True), dict) + + #This is for new behavior of classify_ode when called internally with default, It should + # return the first hint which matches therefore, 'ordered_hints' key will not be there. + assert sorted(classify_ode(Eq(f(x).diff(x), 0), f(x), dict=True).keys()) == \ + ['default', 'nth_linear_constant_coeff_homogeneous', 'order'] + a = classify_ode(2*x*f(x)*f(x).diff(x) + (1 + x)*f(x)**2 - exp(x), f(x), dict=True, hint='Bernoulli') + assert sorted(a.keys()) == ['Bernoulli', 'Bernoulli_Integral', 'default', 'order', 'ordered_hints'] + + # test issue 22155 + a = classify_ode(f(x).diff(x) - exp(f(x) - x), f(x)) + assert a == ('separable', + '1st_exact', '1st_power_series', + 'lie_group', 'separable_Integral', + '1st_exact_Integral') + + +def test_classify_ode_ics(): + # Dummy + eq = f(x).diff(x, x) - f(x) + + # Not f(0) or f'(0) + ics = {x: 1} + raises(ValueError, lambda: classify_ode(eq, f(x), ics=ics)) + + + ############################ + # f(0) type (AppliedUndef) # + ############################ + + + # Wrong function + ics = {g(0): 1} + raises(ValueError, lambda: classify_ode(eq, f(x), ics=ics)) + + # Contains x + ics = {f(x): 1} + raises(ValueError, lambda: classify_ode(eq, f(x), ics=ics)) + + # Too many args + ics = {f(0, 0): 1} + raises(ValueError, lambda: classify_ode(eq, f(x), ics=ics)) + + # point contains x + ics = {f(0): f(x)} + raises(ValueError, lambda: classify_ode(eq, f(x), ics=ics)) + + # Does not raise + ics = {f(0): f(0)} + classify_ode(eq, f(x), ics=ics) + + # Does not raise + ics = {f(0): 1} + classify_ode(eq, f(x), ics=ics) + + + ##################### + # f'(0) type (Subs) # + ##################### + + # Wrong function + ics = {g(x).diff(x).subs(x, 0): 1} + raises(ValueError, lambda: classify_ode(eq, f(x), ics=ics)) + + # Contains x + ics = {f(y).diff(y).subs(y, x): 1} + raises(ValueError, lambda: classify_ode(eq, f(x), ics=ics)) + + # Wrong variable + ics = {f(y).diff(y).subs(y, 0): 1} + raises(ValueError, lambda: classify_ode(eq, f(x), ics=ics)) + + # Too many args + ics = {f(x, y).diff(x).subs(x, 0): 1} + raises(ValueError, lambda: classify_ode(eq, f(x), ics=ics)) + + # Derivative wrt wrong vars + ics = {Derivative(f(x), x, y).subs(x, 0): 1} + raises(ValueError, lambda: classify_ode(eq, f(x), ics=ics)) + + # point contains x + ics = {f(x).diff(x).subs(x, 0): f(x)} + raises(ValueError, lambda: classify_ode(eq, f(x), ics=ics)) + + # Does not raise + ics = {f(x).diff(x).subs(x, 0): f(x).diff(x).subs(x, 0)} + classify_ode(eq, f(x), ics=ics) + + # Does not raise + ics = {f(x).diff(x).subs(x, 0): 1} + classify_ode(eq, f(x), ics=ics) + + ########################### + # f'(y) type (Derivative) # + ########################### + + # Wrong function + ics = {g(x).diff(x).subs(x, y): 1} + raises(ValueError, lambda: classify_ode(eq, f(x), ics=ics)) + + # Contains x + ics = {f(y).diff(y).subs(y, x): 1} + raises(ValueError, lambda: classify_ode(eq, f(x), ics=ics)) + + # Too many args + ics = {f(x, y).diff(x).subs(x, y): 1} + raises(ValueError, lambda: classify_ode(eq, f(x), ics=ics)) + + # Derivative wrt wrong vars + ics = {Derivative(f(x), x, z).subs(x, y): 1} + raises(ValueError, lambda: classify_ode(eq, f(x), ics=ics)) + + # point contains x + ics = {f(x).diff(x).subs(x, y): f(x)} + raises(ValueError, lambda: classify_ode(eq, f(x), ics=ics)) + + # Does not raise + ics = {f(x).diff(x).subs(x, 0): f(0)} + classify_ode(eq, f(x), ics=ics) + + # Does not raise + ics = {f(x).diff(x).subs(x, y): 1} + classify_ode(eq, f(x), ics=ics) + +def test_classify_sysode(): + # Here x is assumed to be x(t) and y as y(t) for simplicity. + # Similarly diff(x,t) and diff(y,y) is assumed to be x1 and y1 respectively. + k, l, m, n = symbols('k, l, m, n', Integer=True) + k1, k2, k3, l1, l2, l3, m1, m2, m3 = symbols('k1, k2, k3, l1, l2, l3, m1, m2, m3', Integer=True) + P, Q, R, p, q, r = symbols('P, Q, R, p, q, r', cls=Function) + P1, P2, P3, Q1, Q2, R1, R2 = symbols('P1, P2, P3, Q1, Q2, R1, R2', cls=Function) + x, y, z = symbols('x, y, z', cls=Function) + t = symbols('t') + x1 = diff(x(t),t) + y1 = diff(y(t),t) + + eq6 = (Eq(x1, exp(k*x(t))*P(x(t),y(t))), Eq(y1,r(y(t))*P(x(t),y(t)))) + sol6 = {'no_of_equation': 2, 'func_coeff': {(0, x(t), 0): 0, (1, x(t), 1): 0, (0, x(t), 1): 1, (1, y(t), 0): 0, \ + (1, x(t), 0): 0, (0, y(t), 1): 0, (0, y(t), 0): 0, (1, y(t), 1): 1}, 'type_of_equation': 'type2', 'func': \ + [x(t), y(t)], 'is_linear': False, 'eq': [-P(x(t), y(t))*exp(k*x(t)) + Derivative(x(t), t), -P(x(t), \ + y(t))*r(y(t)) + Derivative(y(t), t)], 'order': {y(t): 1, x(t): 1}} + assert classify_sysode(eq6) == sol6 + + eq7 = (Eq(x1, x(t)**2+y(t)/x(t)), Eq(y1, x(t)/y(t))) + sol7 = {'no_of_equation': 2, 'func_coeff': {(0, x(t), 0): 0, (1, x(t), 1): 0, (0, x(t), 1): 1, (1, y(t), 0): 0, \ + (1, x(t), 0): -1/y(t), (0, y(t), 1): 0, (0, y(t), 0): -1/x(t), (1, y(t), 1): 1}, 'type_of_equation': 'type3', \ + 'func': [x(t), y(t)], 'is_linear': False, 'eq': [-x(t)**2 + Derivative(x(t), t) - y(t)/x(t), -x(t)/y(t) + \ + Derivative(y(t), t)], 'order': {y(t): 1, x(t): 1}} + assert classify_sysode(eq7) == sol7 + + eq8 = (Eq(x1, P1(x(t))*Q1(y(t))*R(x(t),y(t),t)), Eq(y1, P1(x(t))*Q1(y(t))*R(x(t),y(t),t))) + sol8 = {'func': [x(t), y(t)], 'is_linear': False, 'type_of_equation': 'type4', 'eq': \ + [-P1(x(t))*Q1(y(t))*R(x(t), y(t), t) + Derivative(x(t), t), -P1(x(t))*Q1(y(t))*R(x(t), y(t), t) + \ + Derivative(y(t), t)], 'func_coeff': {(0, y(t), 1): 0, (1, y(t), 1): 1, (1, x(t), 1): 0, (0, y(t), 0): 0, \ + (1, x(t), 0): 0, (0, x(t), 0): 0, (1, y(t), 0): 0, (0, x(t), 1): 1}, 'order': {y(t): 1, x(t): 1}, 'no_of_equation': 2} + assert classify_sysode(eq8) == sol8 + + eq11 = (Eq(x1,x(t)*y(t)**3), Eq(y1,y(t)**5)) + sol11 = {'no_of_equation': 2, 'func_coeff': {(0, x(t), 0): -y(t)**3, (1, x(t), 1): 0, (0, x(t), 1): 1, \ + (1, y(t), 0): 0, (1, x(t), 0): 0, (0, y(t), 1): 0, (0, y(t), 0): 0, (1, y(t), 1): 1}, 'type_of_equation': \ + 'type1', 'func': [x(t), y(t)], 'is_linear': False, 'eq': [-x(t)*y(t)**3 + Derivative(x(t), t), \ + -y(t)**5 + Derivative(y(t), t)], 'order': {y(t): 1, x(t): 1}} + assert classify_sysode(eq11) == sol11 + + eq13 = (Eq(x1,x(t)*y(t)*sin(t)**2), Eq(y1,y(t)**2*sin(t)**2)) + sol13 = {'no_of_equation': 2, 'func_coeff': {(0, x(t), 0): -y(t)*sin(t)**2, (1, x(t), 1): 0, (0, x(t), 1): 1, \ + (1, y(t), 0): 0, (1, x(t), 0): 0, (0, y(t), 1): 0, (0, y(t), 0): -x(t)*sin(t)**2, (1, y(t), 1): 1}, \ + 'type_of_equation': 'type4', 'func': [x(t), y(t)], 'is_linear': False, 'eq': [-x(t)*y(t)*sin(t)**2 + \ + Derivative(x(t), t), -y(t)**2*sin(t)**2 + Derivative(y(t), t)], 'order': {y(t): 1, x(t): 1}} + assert classify_sysode(eq13) == sol13 + + +def test_solve_ics(): + # Basic tests that things work from dsolve. + assert dsolve(f(x).diff(x) - 1/f(x), f(x), ics={f(1): 2}) == \ + Eq(f(x), sqrt(2 * x + 2)) + assert dsolve(f(x).diff(x) - f(x), f(x), ics={f(0): 1}) == Eq(f(x), exp(x)) + assert dsolve(f(x).diff(x) - f(x), f(x), ics={f(x).diff(x).subs(x, 0): 1}) == Eq(f(x), exp(x)) + assert dsolve(f(x).diff(x, x) + f(x), f(x), ics={f(0): 1, + f(x).diff(x).subs(x, 0): 1}) == Eq(f(x), sin(x) + cos(x)) + assert dsolve([f(x).diff(x) - f(x) + g(x), g(x).diff(x) - g(x) - f(x)], + [f(x), g(x)], ics={f(0): 1, g(0): 0}) == [Eq(f(x), exp(x)*cos(x)), Eq(g(x), exp(x)*sin(x))] + + # Test cases where dsolve returns two solutions. + eq = (x**2*f(x)**2 - x).diff(x) + assert dsolve(eq, f(x), ics={f(1): 0}) == [Eq(f(x), + -sqrt(x - 1)/x), Eq(f(x), sqrt(x - 1)/x)] + assert dsolve(eq, f(x), ics={f(x).diff(x).subs(x, 1): 0}) == [Eq(f(x), + -sqrt(x - S.Half)/x), Eq(f(x), sqrt(x - S.Half)/x)] + + eq = cos(f(x)) - (x*sin(f(x)) - f(x)**2)*f(x).diff(x) + assert dsolve(eq, f(x), + ics={f(0):1}, hint='1st_exact', simplify=False) == Eq(x*cos(f(x)) + f(x)**3/3, Rational(1, 3)) + assert dsolve(eq, f(x), + ics={f(0):1}, hint='1st_exact', simplify=True) == Eq(x*cos(f(x)) + f(x)**3/3, Rational(1, 3)) + + assert solve_ics([Eq(f(x), C1*exp(x))], [f(x)], [C1], {f(0): 1}) == {C1: 1} + assert solve_ics([Eq(f(x), C1*sin(x) + C2*cos(x))], [f(x)], [C1, C2], + {f(0): 1, f(pi/2): 1}) == {C1: 1, C2: 1} + + assert solve_ics([Eq(f(x), C1*sin(x) + C2*cos(x))], [f(x)], [C1, C2], + {f(0): 1, f(x).diff(x).subs(x, 0): 1}) == {C1: 1, C2: 1} + + assert solve_ics([Eq(f(x), C1*sin(x) + C2*cos(x))], [f(x)], [C1, C2], {f(0): 1}) == \ + {C2: 1} + + # Some more complicated tests Refer to PR #16098 + + assert set(dsolve(f(x).diff(x)*(f(x).diff(x, 2)-x), ics={f(0):0, f(x).diff(x).subs(x, 1):0})) == \ + {Eq(f(x), 0), Eq(f(x), x ** 3 / 6 - x / 2)} + assert set(dsolve(f(x).diff(x)*(f(x).diff(x, 2)-x), ics={f(0):0})) == \ + {Eq(f(x), 0), Eq(f(x), C2*x + x**3/6)} + + K, r, f0 = symbols('K r f0') + sol = Eq(f(x), K*f0*exp(r*x)/((-K + f0)*(f0*exp(r*x)/(-K + f0) - 1))) + assert (dsolve(Eq(f(x).diff(x), r * f(x) * (1 - f(x) / K)), f(x), ics={f(0): f0})) == sol + + + #Order dependent issues Refer to PR #16098 + assert set(dsolve(f(x).diff(x)*(f(x).diff(x, 2)-x), ics={f(x).diff(x).subs(x,0):0, f(0):0})) == \ + {Eq(f(x), 0), Eq(f(x), x ** 3 / 6)} + assert set(dsolve(f(x).diff(x)*(f(x).diff(x, 2)-x), ics={f(0):0, f(x).diff(x).subs(x,0):0})) == \ + {Eq(f(x), 0), Eq(f(x), x ** 3 / 6)} + + # XXX: Ought to be ValueError + raises(ValueError, lambda: solve_ics([Eq(f(x), C1*sin(x) + C2*cos(x))], [f(x)], [C1, C2], {f(0): 1, f(pi): 1})) + + # Degenerate case. f'(0) is identically 0. + raises(ValueError, lambda: solve_ics([Eq(f(x), sqrt(C1 - x**2))], [f(x)], [C1], {f(x).diff(x).subs(x, 0): 0})) + + EI, q, L = symbols('EI q L') + + # eq = Eq(EI*diff(f(x), x, 4), q) + sols = [Eq(f(x), C1 + C2*x + C3*x**2 + C4*x**3 + q*x**4/(24*EI))] + funcs = [f(x)] + constants = [C1, C2, C3, C4] + # Test both cases, Derivative (the default from f(x).diff(x).subs(x, L)), + # and Subs + ics1 = {f(0): 0, + f(x).diff(x).subs(x, 0): 0, + f(L).diff(L, 2): 0, + f(L).diff(L, 3): 0} + ics2 = {f(0): 0, + f(x).diff(x).subs(x, 0): 0, + Subs(f(x).diff(x, 2), x, L): 0, + Subs(f(x).diff(x, 3), x, L): 0} + + solved_constants1 = solve_ics(sols, funcs, constants, ics1) + solved_constants2 = solve_ics(sols, funcs, constants, ics2) + assert solved_constants1 == solved_constants2 == { + C1: 0, + C2: 0, + C3: L**2*q/(4*EI), + C4: -L*q/(6*EI)} + + # Allow the ics to refer to f + ics = {f(0): f(0)} + assert dsolve(f(x).diff(x) - f(x), f(x), ics=ics) == Eq(f(x), f(0)*exp(x)) + + ics = {f(x).diff(x).subs(x, 0): f(x).diff(x).subs(x, 0), f(0): f(0)} + assert dsolve(f(x).diff(x, x) + f(x), f(x), ics=ics) == \ + Eq(f(x), f(0)*cos(x) + f(x).diff(x).subs(x, 0)*sin(x)) + +def test_ode_order(): + f = Function('f') + g = Function('g') + x = Symbol('x') + assert ode_order(3*x*exp(f(x)), f(x)) == 0 + assert ode_order(x*diff(f(x), x) + 3*x*f(x) - sin(x)/x, f(x)) == 1 + assert ode_order(x**2*f(x).diff(x, x) + x*diff(f(x), x) - f(x), f(x)) == 2 + assert ode_order(diff(x*exp(f(x)), x, x), f(x)) == 2 + assert ode_order(diff(x*diff(x*exp(f(x)), x, x), x), f(x)) == 3 + assert ode_order(diff(f(x), x, x), g(x)) == 0 + assert ode_order(diff(f(x), x, x)*diff(g(x), x), f(x)) == 2 + assert ode_order(diff(f(x), x, x)*diff(g(x), x), g(x)) == 1 + assert ode_order(diff(x*diff(x*exp(f(x)), x, x), x), g(x)) == 0 + # issue 5835: ode_order has to also work for unevaluated derivatives + # (ie, without using doit()). + assert ode_order(Derivative(x*f(x), x), f(x)) == 1 + assert ode_order(x*sin(Derivative(x*f(x)**2, x, x)), f(x)) == 2 + assert ode_order(Derivative(x*Derivative(x*exp(f(x)), x, x), x), g(x)) == 0 + assert ode_order(Derivative(f(x), x, x), g(x)) == 0 + assert ode_order(Derivative(x*exp(f(x)), x, x), f(x)) == 2 + assert ode_order(Derivative(f(x), x, x)*Derivative(g(x), x), g(x)) == 1 + assert ode_order(Derivative(x*Derivative(f(x), x, x), x), f(x)) == 3 + assert ode_order( + x*sin(Derivative(x*Derivative(f(x), x)**2, x, x)), f(x)) == 3 + + +def test_homogeneous_order(): + assert homogeneous_order(exp(y/x) + tan(y/x), x, y) == 0 + assert homogeneous_order(x**2 + sin(x)*cos(y), x, y) is None + assert homogeneous_order(x - y - x*sin(y/x), x, y) == 1 + assert homogeneous_order((x*y + sqrt(x**4 + y**4) + x**2*(log(x) - log(y)))/ + (pi*x**Rational(2, 3)*sqrt(y)**3), x, y) == Rational(-1, 6) + assert homogeneous_order(y/x*cos(y/x) - x/y*sin(y/x) + cos(y/x), x, y) == 0 + assert homogeneous_order(f(x), x, f(x)) == 1 + assert homogeneous_order(f(x)**2, x, f(x)) == 2 + assert homogeneous_order(x*y*z, x, y) == 2 + assert homogeneous_order(x*y*z, x, y, z) == 3 + assert homogeneous_order(x**2*f(x)/sqrt(x**2 + f(x)**2), f(x)) is None + assert homogeneous_order(f(x, y)**2, x, f(x, y), y) == 2 + assert homogeneous_order(f(x, y)**2, x, f(x), y) is None + assert homogeneous_order(f(x, y)**2, x, f(x, y)) is None + assert homogeneous_order(f(y, x)**2, x, y, f(x, y)) is None + assert homogeneous_order(f(y), f(x), x) is None + assert homogeneous_order(-f(x)/x + 1/sin(f(x)/ x), f(x), x) == 0 + assert homogeneous_order(log(1/y) + log(x**2), x, y) is None + assert homogeneous_order(log(1/y) + log(x), x, y) == 0 + assert homogeneous_order(log(x/y), x, y) == 0 + assert homogeneous_order(2*log(1/y) + 2*log(x), x, y) == 0 + a = Symbol('a') + assert homogeneous_order(a*log(1/y) + a*log(x), x, y) == 0 + assert homogeneous_order(f(x).diff(x), x, y) is None + assert homogeneous_order(-f(x).diff(x) + x, x, y) is None + assert homogeneous_order(O(x), x, y) is None + assert homogeneous_order(x + O(x**2), x, y) is None + assert homogeneous_order(x**pi, x) == pi + assert homogeneous_order(x**x, x) is None + raises(ValueError, lambda: homogeneous_order(x*y)) + + +@XFAIL +def test_noncircularized_real_imaginary_parts(): + # If this passes, lines numbered 3878-3882 (at the time of this commit) + # of sympy/solvers/ode.py for nth_linear_constant_coeff_homogeneous + # should be removed. + y = sqrt(1+x) + i, r = im(y), re(y) + assert not (i.has(atan2) and r.has(atan2)) + + +def test_collect_respecting_exponentials(): + # If this test passes, lines 1306-1311 (at the time of this commit) + # of sympy/solvers/ode.py should be removed. + sol = 1 + exp(x/2) + assert sol == collect( sol, exp(x/3)) + + +def test_undetermined_coefficients_match(): + assert _undetermined_coefficients_match(g(x), x) == {'test': False} + assert _undetermined_coefficients_match(sin(2*x + sqrt(5)), x) == \ + {'test': True, 'trialset': + {cos(2*x + sqrt(5)), sin(2*x + sqrt(5))}} + assert _undetermined_coefficients_match(sin(x)*cos(x), x) == \ + {'test': False} + s = {cos(x), x*cos(x), x**2*cos(x), x**2*sin(x), x*sin(x), sin(x)} + assert _undetermined_coefficients_match(sin(x)*(x**2 + x + 1), x) == \ + {'test': True, 'trialset': s} + assert _undetermined_coefficients_match( + sin(x)*x**2 + sin(x)*x + sin(x), x) == {'test': True, 'trialset': s} + assert _undetermined_coefficients_match( + exp(2*x)*sin(x)*(x**2 + x + 1), x + ) == { + 'test': True, 'trialset': {exp(2*x)*sin(x), x**2*exp(2*x)*sin(x), + cos(x)*exp(2*x), x**2*cos(x)*exp(2*x), x*cos(x)*exp(2*x), + x*exp(2*x)*sin(x)}} + assert _undetermined_coefficients_match(1/sin(x), x) == {'test': False} + assert _undetermined_coefficients_match(log(x), x) == {'test': False} + assert _undetermined_coefficients_match(2**(x)*(x**2 + x + 1), x) == \ + {'test': True, 'trialset': {2**x, x*2**x, x**2*2**x}} + assert _undetermined_coefficients_match(x**y, x) == {'test': False} + assert _undetermined_coefficients_match(exp(x)*exp(2*x + 1), x) == \ + {'test': True, 'trialset': {exp(1 + 3*x)}} + assert _undetermined_coefficients_match(sin(x)*(x**2 + x + 1), x) == \ + {'test': True, 'trialset': {x*cos(x), x*sin(x), x**2*cos(x), + x**2*sin(x), cos(x), sin(x)}} + assert _undetermined_coefficients_match(sin(x)*(x + sin(x)), x) == \ + {'test': False} + assert _undetermined_coefficients_match(sin(x)*(x + sin(2*x)), x) == \ + {'test': False} + assert _undetermined_coefficients_match(sin(x)*tan(x), x) == \ + {'test': False} + assert _undetermined_coefficients_match( + x**2*sin(x)*exp(x) + x*sin(x) + x, x + ) == { + 'test': True, 'trialset': {x**2*cos(x)*exp(x), x, cos(x), S.One, + exp(x)*sin(x), sin(x), x*exp(x)*sin(x), x*cos(x), x*cos(x)*exp(x), + x*sin(x), cos(x)*exp(x), x**2*exp(x)*sin(x)}} + assert _undetermined_coefficients_match(4*x*sin(x - 2), x) == { + 'trialset': {x*cos(x - 2), x*sin(x - 2), cos(x - 2), sin(x - 2)}, + 'test': True, + } + assert _undetermined_coefficients_match(2**x*x, x) == \ + {'test': True, 'trialset': {2**x, x*2**x}} + assert _undetermined_coefficients_match(2**x*exp(2*x), x) == \ + {'test': True, 'trialset': {2**x*exp(2*x)}} + assert _undetermined_coefficients_match(exp(-x)/x, x) == \ + {'test': False} + # Below are from Ordinary Differential Equations, + # Tenenbaum and Pollard, pg. 231 + assert _undetermined_coefficients_match(S(4), x) == \ + {'test': True, 'trialset': {S.One}} + assert _undetermined_coefficients_match(12*exp(x), x) == \ + {'test': True, 'trialset': {exp(x)}} + assert _undetermined_coefficients_match(exp(I*x), x) == \ + {'test': True, 'trialset': {exp(I*x)}} + assert _undetermined_coefficients_match(sin(x), x) == \ + {'test': True, 'trialset': {cos(x), sin(x)}} + assert _undetermined_coefficients_match(cos(x), x) == \ + {'test': True, 'trialset': {cos(x), sin(x)}} + assert _undetermined_coefficients_match(8 + 6*exp(x) + 2*sin(x), x) == \ + {'test': True, 'trialset': {S.One, cos(x), sin(x), exp(x)}} + assert _undetermined_coefficients_match(x**2, x) == \ + {'test': True, 'trialset': {S.One, x, x**2}} + assert _undetermined_coefficients_match(9*x*exp(x) + exp(-x), x) == \ + {'test': True, 'trialset': {x*exp(x), exp(x), exp(-x)}} + assert _undetermined_coefficients_match(2*exp(2*x)*sin(x), x) == \ + {'test': True, 'trialset': {exp(2*x)*sin(x), cos(x)*exp(2*x)}} + assert _undetermined_coefficients_match(x - sin(x), x) == \ + {'test': True, 'trialset': {S.One, x, cos(x), sin(x)}} + assert _undetermined_coefficients_match(x**2 + 2*x, x) == \ + {'test': True, 'trialset': {S.One, x, x**2}} + assert _undetermined_coefficients_match(4*x*sin(x), x) == \ + {'test': True, 'trialset': {x*cos(x), x*sin(x), cos(x), sin(x)}} + assert _undetermined_coefficients_match(x*sin(2*x), x) == \ + {'test': True, 'trialset': + {x*cos(2*x), x*sin(2*x), cos(2*x), sin(2*x)}} + assert _undetermined_coefficients_match(x**2*exp(-x), x) == \ + {'test': True, 'trialset': {x*exp(-x), x**2*exp(-x), exp(-x)}} + assert _undetermined_coefficients_match(2*exp(-x) - x**2*exp(-x), x) == \ + {'test': True, 'trialset': {x*exp(-x), x**2*exp(-x), exp(-x)}} + assert _undetermined_coefficients_match(exp(-2*x) + x**2, x) == \ + {'test': True, 'trialset': {S.One, x, x**2, exp(-2*x)}} + assert _undetermined_coefficients_match(x*exp(-x), x) == \ + {'test': True, 'trialset': {x*exp(-x), exp(-x)}} + assert _undetermined_coefficients_match(x + exp(2*x), x) == \ + {'test': True, 'trialset': {S.One, x, exp(2*x)}} + assert _undetermined_coefficients_match(sin(x) + exp(-x), x) == \ + {'test': True, 'trialset': {cos(x), sin(x), exp(-x)}} + assert _undetermined_coefficients_match(exp(x), x) == \ + {'test': True, 'trialset': {exp(x)}} + # converted from sin(x)**2 + assert _undetermined_coefficients_match(S.Half - cos(2*x)/2, x) == \ + {'test': True, 'trialset': {S.One, cos(2*x), sin(2*x)}} + # converted from exp(2*x)*sin(x)**2 + assert _undetermined_coefficients_match( + exp(2*x)*(S.Half + cos(2*x)/2), x + ) == { + 'test': True, 'trialset': {exp(2*x)*sin(2*x), cos(2*x)*exp(2*x), + exp(2*x)}} + assert _undetermined_coefficients_match(2*x + sin(x) + cos(x), x) == \ + {'test': True, 'trialset': {S.One, x, cos(x), sin(x)}} + # converted from sin(2*x)*sin(x) + assert _undetermined_coefficients_match(cos(x)/2 - cos(3*x)/2, x) == \ + {'test': True, 'trialset': {cos(x), cos(3*x), sin(x), sin(3*x)}} + assert _undetermined_coefficients_match(cos(x**2), x) == {'test': False} + assert _undetermined_coefficients_match(2**(x**2), x) == {'test': False} + + +def test_issue_4785_22462(): + from sympy.abc import A + eq = x + A*(x + diff(f(x), x) + f(x)) + diff(f(x), x) + f(x) + 2 + assert classify_ode(eq, f(x)) == ('factorable', '1st_exact', '1st_linear', + 'Bernoulli', 'almost_linear', '1st_power_series', 'lie_group', + 'nth_linear_constant_coeff_undetermined_coefficients', + 'nth_linear_constant_coeff_variation_of_parameters', + '1st_exact_Integral', '1st_linear_Integral', 'Bernoulli_Integral', + 'almost_linear_Integral', + 'nth_linear_constant_coeff_variation_of_parameters_Integral') + # issue 4864 + eq = (x**2 + f(x)**2)*f(x).diff(x) - 2*x*f(x) + assert classify_ode(eq, f(x)) == ('factorable', '1st_exact', + '1st_homogeneous_coeff_best', + '1st_homogeneous_coeff_subs_indep_div_dep', + '1st_homogeneous_coeff_subs_dep_div_indep', + '1st_power_series', + 'lie_group', '1st_exact_Integral', + '1st_homogeneous_coeff_subs_indep_div_dep_Integral', + '1st_homogeneous_coeff_subs_dep_div_indep_Integral') + + +def test_issue_4825(): + raises(ValueError, lambda: dsolve(f(x, y).diff(x) - y*f(x, y), f(x))) + assert classify_ode(f(x, y).diff(x) - y*f(x, y), f(x), dict=True) == \ + {'order': 0, 'default': None, 'ordered_hints': ()} + # See also issue 3793, test Z13. + raises(ValueError, lambda: dsolve(f(x).diff(x), f(y))) + assert classify_ode(f(x).diff(x), f(y), dict=True) == \ + {'order': 0, 'default': None, 'ordered_hints': ()} + + +def test_constant_renumber_order_issue_5308(): + from sympy.utilities.iterables import variations + + assert constant_renumber(C1*x + C2*y) == \ + constant_renumber(C1*y + C2*x) == \ + C1*x + C2*y + e = C1*(C2 + x)*(C3 + y) + for a, b, c in variations([C1, C2, C3], 3): + assert constant_renumber(a*(b + x)*(c + y)) == e + + +def test_constant_renumber(): + e1, e2, x, y = symbols("e1:3 x y") + exprs = [e2*x, e1*x + e2*y] + + assert constant_renumber(exprs[0]) == e2*x + assert constant_renumber(exprs[0], variables=[x]) == C1*x + assert constant_renumber(exprs[0], variables=[x], newconstants=[C2]) == C2*x + assert constant_renumber(exprs, variables=[x, y]) == [C1*x, C1*y + C2*x] + assert constant_renumber(exprs, variables=[x, y], newconstants=symbols("C3:5")) == [C3*x, C3*y + C4*x] + + +def test_issue_5770(): + k = Symbol("k", real=True) + t = Symbol('t') + w = Function('w') + sol = dsolve(w(t).diff(t, 6) - k**6*w(t), w(t)) + assert len([s for s in sol.free_symbols if s.name.startswith('C')]) == 6 + assert constantsimp((C1*cos(x) + C2*cos(x))*exp(x), {C1, C2}) == \ + C1*cos(x)*exp(x) + assert constantsimp(C1*cos(x) + C2*cos(x) + C3*sin(x), {C1, C2, C3}) == \ + C1*cos(x) + C3*sin(x) + assert constantsimp(exp(C1 + x), {C1}) == C1*exp(x) + assert constantsimp(x + C1 + y, {C1, y}) == C1 + x + assert constantsimp(x + C1 + Integral(x, (x, 1, 2)), {C1}) == C1 + x + + +def test_issue_5112_5430(): + assert homogeneous_order(-log(x) + acosh(x), x) is None + assert homogeneous_order(y - log(x), x, y) is None + + +def test_issue_5095(): + f = Function('f') + raises(ValueError, lambda: dsolve(f(x).diff(x)**2, f(x), 'fdsjf')) + + +def test_homogeneous_function(): + f = Function('f') + eq1 = tan(x + f(x)) + eq2 = sin((3*x)/(4*f(x))) + eq3 = cos(x*f(x)*Rational(3, 4)) + eq4 = log((3*x + 4*f(x))/(5*f(x) + 7*x)) + eq5 = exp((2*x**2)/(3*f(x)**2)) + eq6 = log((3*x + 4*f(x))/(5*f(x) + 7*x) + exp((2*x**2)/(3*f(x)**2))) + eq7 = sin((3*x)/(5*f(x) + x**2)) + assert homogeneous_order(eq1, x, f(x)) == None + assert homogeneous_order(eq2, x, f(x)) == 0 + assert homogeneous_order(eq3, x, f(x)) == None + assert homogeneous_order(eq4, x, f(x)) == 0 + assert homogeneous_order(eq5, x, f(x)) == 0 + assert homogeneous_order(eq6, x, f(x)) == 0 + assert homogeneous_order(eq7, x, f(x)) == None + + +def test_linear_coeff_match(): + n, d = z*(2*x + 3*f(x) + 5), z*(7*x + 9*f(x) + 11) + rat = n/d + eq1 = sin(rat) + cos(rat.expand()) + obj1 = LinearCoefficients(eq1) + eq2 = rat + obj2 = LinearCoefficients(eq2) + eq3 = log(sin(rat)) + obj3 = LinearCoefficients(eq3) + ans = (4, Rational(-13, 3)) + assert obj1._linear_coeff_match(eq1, f(x)) == ans + assert obj2._linear_coeff_match(eq2, f(x)) == ans + assert obj3._linear_coeff_match(eq3, f(x)) == ans + + # no c + eq4 = (3*x)/f(x) + obj4 = LinearCoefficients(eq4) + # not x and f(x) + eq5 = (3*x + 2)/x + obj5 = LinearCoefficients(eq5) + # denom will be zero + eq6 = (3*x + 2*f(x) + 1)/(3*x + 2*f(x) + 5) + obj6 = LinearCoefficients(eq6) + # not rational coefficient + eq7 = (3*x + 2*f(x) + sqrt(2))/(3*x + 2*f(x) + 5) + obj7 = LinearCoefficients(eq7) + assert obj4._linear_coeff_match(eq4, f(x)) is None + assert obj5._linear_coeff_match(eq5, f(x)) is None + assert obj6._linear_coeff_match(eq6, f(x)) is None + assert obj7._linear_coeff_match(eq7, f(x)) is None + + +def test_constantsimp_take_problem(): + c = exp(C1) + 2 + assert len(Poly(constantsimp(exp(C1) + c + c*x, [C1])).gens) == 2 + + +def test_series(): + C1 = Symbol("C1") + eq = f(x).diff(x) - f(x) + sol = Eq(f(x), C1 + C1*x + C1*x**2/2 + C1*x**3/6 + C1*x**4/24 + + C1*x**5/120 + O(x**6)) + assert dsolve(eq, hint='1st_power_series') == sol + assert checkodesol(eq, sol, order=1)[0] + + eq = f(x).diff(x) - x*f(x) + sol = Eq(f(x), C1*x**4/8 + C1*x**2/2 + C1 + O(x**6)) + assert dsolve(eq, hint='1st_power_series') == sol + assert checkodesol(eq, sol, order=1)[0] + + eq = f(x).diff(x) - sin(x*f(x)) + sol = Eq(f(x), (x - 2)**2*(1+ sin(4))*cos(4) + (x - 2)*sin(4) + 2 + O(x**3)) + assert dsolve(eq, hint='1st_power_series', ics={f(2): 2}, n=3) == sol + # FIXME: The solution here should be O((x-2)**3) so is incorrect + #assert checkodesol(eq, sol, order=1)[0] + + +@slow +def test_2nd_power_series_ordinary(): + C1, C2 = symbols("C1 C2") + + eq = f(x).diff(x, 2) - x*f(x) + assert classify_ode(eq) == ('2nd_linear_airy', '2nd_power_series_ordinary') + sol = Eq(f(x), C2*(x**3/6 + 1) + C1*x*(x**3/12 + 1) + O(x**6)) + assert dsolve(eq, hint='2nd_power_series_ordinary') == sol + assert checkodesol(eq, sol) == (True, 0) + + sol = Eq(f(x), C2*((x + 2)**4/6 + (x + 2)**3/6 - (x + 2)**2 + 1) + + C1*(x + (x + 2)**4/12 - (x + 2)**3/3 + S(2)) + + O(x**6)) + assert dsolve(eq, hint='2nd_power_series_ordinary', x0=-2) == sol + # FIXME: Solution should be O((x+2)**6) + # assert checkodesol(eq, sol) == (True, 0) + + sol = Eq(f(x), C2*x + C1 + O(x**2)) + assert dsolve(eq, hint='2nd_power_series_ordinary', n=2) == sol + assert checkodesol(eq, sol) == (True, 0) + + eq = (1 + x**2)*(f(x).diff(x, 2)) + 2*x*(f(x).diff(x)) -2*f(x) + assert classify_ode(eq) == ('factorable', '2nd_hypergeometric', '2nd_hypergeometric_Integral', + '2nd_power_series_ordinary') + + sol = Eq(f(x), C2*(-x**4/3 + x**2 + 1) + C1*x + O(x**6)) + assert dsolve(eq, hint='2nd_power_series_ordinary') == sol + assert checkodesol(eq, sol) == (True, 0) + + eq = f(x).diff(x, 2) + x*(f(x).diff(x)) + f(x) + assert classify_ode(eq) == ('factorable', '2nd_power_series_ordinary',) + sol = Eq(f(x), C2*(x**4/8 - x**2/2 + 1) + C1*x*(-x**2/3 + 1) + O(x**6)) + assert dsolve(eq) == sol + # FIXME: checkodesol fails for this solution... + # assert checkodesol(eq, sol) == (True, 0) + + eq = f(x).diff(x, 2) + f(x).diff(x) - x*f(x) + assert classify_ode(eq) == ('2nd_power_series_ordinary',) + sol = Eq(f(x), C2*(-x**4/24 + x**3/6 + 1) + + C1*x*(x**3/24 + x**2/6 - x/2 + 1) + O(x**6)) + assert dsolve(eq) == sol + # FIXME: checkodesol fails for this solution... + # assert checkodesol(eq, sol) == (True, 0) + + eq = f(x).diff(x, 2) + x*f(x) + assert classify_ode(eq) == ('2nd_linear_airy', '2nd_power_series_ordinary') + sol = Eq(f(x), C2*(x**6/180 - x**3/6 + 1) + C1*x*(-x**3/12 + 1) + O(x**7)) + assert dsolve(eq, hint='2nd_power_series_ordinary', n=7) == sol + assert checkodesol(eq, sol) == (True, 0) + + +def test_2nd_power_series_regular(): + C1, C2, a = symbols("C1 C2 a") + eq = x**2*(f(x).diff(x, 2)) - 3*x*(f(x).diff(x)) + (4*x + 4)*f(x) + sol = Eq(f(x), C1*x**2*(-16*x**3/9 + 4*x**2 - 4*x + 1) + O(x**6)) + assert dsolve(eq, hint='2nd_power_series_regular') == sol + assert checkodesol(eq, sol) == (True, 0) + + eq = 4*x**2*(f(x).diff(x, 2)) -8*x**2*(f(x).diff(x)) + (4*x**2 + + 1)*f(x) + sol = Eq(f(x), C1*sqrt(x)*(x**4/24 + x**3/6 + x**2/2 + x + 1) + O(x**6)) + assert dsolve(eq, hint='2nd_power_series_regular') == sol + assert checkodesol(eq, sol) == (True, 0) + + eq = x**2*(f(x).diff(x, 2)) - x**2*(f(x).diff(x)) + ( + x**2 - 2)*f(x) + sol = Eq(f(x), C1*(-x**6/720 - 3*x**5/80 - x**4/8 + x**2/2 + x/2 + 1)/x + + C2*x**2*(-x**3/60 + x**2/20 + x/2 + 1) + O(x**6)) + assert dsolve(eq) == sol + assert checkodesol(eq, sol) == (True, 0) + + eq = x**2*(f(x).diff(x, 2)) + x*(f(x).diff(x)) + (x**2 - Rational(1, 4))*f(x) + sol = Eq(f(x), C1*(x**4/24 - x**2/2 + 1)/sqrt(x) + + C2*sqrt(x)*(x**4/120 - x**2/6 + 1) + O(x**6)) + assert dsolve(eq, hint='2nd_power_series_regular') == sol + assert checkodesol(eq, sol) == (True, 0) + + eq = x*f(x).diff(x, 2) + f(x).diff(x) - a*x*f(x) + sol = Eq(f(x), C1*(a**2*x**4/64 + a*x**2/4 + 1) + O(x**6)) + assert dsolve(eq, f(x), hint="2nd_power_series_regular") == sol + assert checkodesol(eq, sol) == (True, 0) + + eq = f(x).diff(x, 2) + ((1 - x)/x)*f(x).diff(x) + (a/x)*f(x) + sol = Eq(f(x), C1*(-a*x**5*(a - 4)*(a - 3)*(a - 2)*(a - 1)/14400 + \ + a*x**4*(a - 3)*(a - 2)*(a - 1)/576 - a*x**3*(a - 2)*(a - 1)/36 + \ + a*x**2*(a - 1)/4 - a*x + 1) + O(x**6)) + assert dsolve(eq, f(x), hint="2nd_power_series_regular") == sol + assert checkodesol(eq, sol) == (True, 0) + + +def test_issue_15056(): + t = Symbol('t') + C3 = Symbol('C3') + assert get_numbered_constants(Symbol('C1') * Function('C2')(t)) == C3 + + +def test_issue_15913(): + eq = -C1/x - 2*x*f(x) - f(x) + Derivative(f(x), x) + sol = C2*exp(x**2 + x) + exp(x**2 + x)*Integral(C1*exp(-x**2 - x)/x, x) + assert checkodesol(eq, sol) == (True, 0) + sol = C1 + C2*exp(-x*y) + eq = Derivative(y*f(x), x) + f(x).diff(x, 2) + assert checkodesol(eq, sol, f(x)) == (True, 0) + + +def test_issue_16146(): + raises(ValueError, lambda: dsolve([f(x).diff(x), g(x).diff(x)], [f(x), g(x), h(x)])) + raises(ValueError, lambda: dsolve([f(x).diff(x), g(x).diff(x)], [f(x)])) + + +def test_dsolve_remove_redundant_solutions(): + + eq = (f(x)-2)*f(x).diff(x) + sol = Eq(f(x), C1) + assert dsolve(eq) == sol + + eq = (f(x)-sin(x))*(f(x).diff(x, 2)) + sol = {Eq(f(x), C1 + C2*x), Eq(f(x), sin(x))} + assert set(dsolve(eq)) == sol + + eq = (f(x)**2-2*f(x)+1)*f(x).diff(x, 3) + sol = Eq(f(x), C1 + C2*x + C3*x**2) + assert dsolve(eq) == sol + + +def test_issue_13060(): + A, B = symbols("A B", cls=Function) + t = Symbol("t") + eq = [Eq(Derivative(A(t), t), A(t)*B(t)), Eq(Derivative(B(t), t), A(t)*B(t))] + sol = dsolve(eq) + assert checkodesol(eq, sol) == (True, [0, 0]) + + +def test_issue_22523(): + N, s = symbols('N s') + rho = Function('rho') + # intentionally use 4.0 to confirm issue with nfloat + # works here + eqn = 4.0*N*sqrt(N - 1)*rho(s) + (4*s**2*(N - 1) + (N - 2*s*(N - 1))**2 + )*Derivative(rho(s), (s, 2)) + match = classify_ode(eqn, dict=True, hint='all') + assert match['2nd_power_series_ordinary']['terms'] == 5 + C1, C2 = symbols('C1,C2') + sol = dsolve(eqn, hint='2nd_power_series_ordinary') + # there is no r(2.0) in this result + assert filldedent(sol) == filldedent(str(''' + Eq(rho(s), C2*(1 - 4.0*s**4*sqrt(N - 1.0)/N + 0.666666666666667*s**4/N + - 2.66666666666667*s**3*sqrt(N - 1.0)/N - 2.0*s**2*sqrt(N - 1.0)/N + + 9.33333333333333*s**4*sqrt(N - 1.0)/N**2 - 0.666666666666667*s**4/N**2 + + 2.66666666666667*s**3*sqrt(N - 1.0)/N**2 - + 5.33333333333333*s**4*sqrt(N - 1.0)/N**3) + C1*s*(1.0 - + 1.33333333333333*s**3*sqrt(N - 1.0)/N - 0.666666666666667*s**2*sqrt(N + - 1.0)/N + 1.33333333333333*s**3*sqrt(N - 1.0)/N**2) + O(s**6))''')) + + +def test_issue_22604(): + x1, x2 = symbols('x1, x2', cls = Function) + t, k1, k2, m1, m2 = symbols('t k1 k2 m1 m2', real = True) + k1, k2, m1, m2 = 1, 1, 1, 1 + eq1 = Eq(m1*diff(x1(t), t, 2) + k1*x1(t) - k2*(x2(t) - x1(t)), 0) + eq2 = Eq(m2*diff(x2(t), t, 2) + k2*(x2(t) - x1(t)), 0) + eqs = [eq1, eq2] + [x1sol, x2sol] = dsolve(eqs, [x1(t), x2(t)], ics = {x1(0):0, x1(t).diff().subs(t,0):0, \ + x2(0):1, x2(t).diff().subs(t,0):0}) + assert x1sol == Eq(x1(t), sqrt(3 - sqrt(5))*(sqrt(10) + 5*sqrt(2))*cos(sqrt(2)*t*sqrt(3 - sqrt(5))/2)/20 + \ + (-5*sqrt(2) + sqrt(10))*sqrt(sqrt(5) + 3)*cos(sqrt(2)*t*sqrt(sqrt(5) + 3)/2)/20) + assert x2sol == Eq(x2(t), (sqrt(5) + 5)*cos(sqrt(2)*t*sqrt(3 - sqrt(5))/2)/10 + (5 - sqrt(5))*cos(sqrt(2)*t*sqrt(sqrt(5) + 3)/2)/10) + + +def test_issue_22462(): + for de in [ + Eq(f(x).diff(x), -20*f(x)**2 - 500*f(x)/7200), + Eq(f(x).diff(x), -2*f(x)**2 - 5*f(x)/7)]: + assert 'Bernoulli' in classify_ode(de, f(x)) + + +def test_issue_23425(): + x = symbols('x') + y = Function('y') + eq = Eq(-E**x*y(x).diff().diff() + y(x).diff(), 0) + assert classify_ode(eq) == \ + ('Liouville', 'nth_order_reducible', \ + '2nd_power_series_ordinary', 'Liouville_Integral') + + +@SKIP("too slow for @slow") +def test_issue_25820(): + x = Symbol('x') + y = Function('y') + eq = y(x)**3*y(x).diff(x, 2) + 49 + assert dsolve(eq, y(x)) is not None # doesn't raise diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/tests/test_riccati.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/tests/test_riccati.py new file mode 100644 index 0000000000000000000000000000000000000000..548a1ee5b5e82d88f1b0aa319af09b8b9d1d9bfe --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/tests/test_riccati.py @@ -0,0 +1,877 @@ +from sympy.core.random import randint +from sympy.core.function import Function +from sympy.core.mul import Mul +from sympy.core.numbers import (I, Rational, oo) +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, symbols) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.hyperbolic import tanh +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import sin +from sympy.polys.polytools import Poly +from sympy.simplify.ratsimp import ratsimp +from sympy.solvers.ode.subscheck import checkodesol +from sympy.testing.pytest import slow +from sympy.solvers.ode.riccati import (riccati_normal, riccati_inverse_normal, + riccati_reduced, match_riccati, inverse_transform_poly, limit_at_inf, + check_necessary_conds, val_at_inf, construct_c_case_1, + construct_c_case_2, construct_c_case_3, construct_d_case_4, + construct_d_case_5, construct_d_case_6, rational_laurent_series, + solve_riccati) + +f = Function('f') +x = symbols('x') + +# These are the functions used to generate the tests +# SHOULD NOT BE USED DIRECTLY IN TESTS + +def rand_rational(maxint): + return Rational(randint(-maxint, maxint), randint(1, maxint)) + + +def rand_poly(x, degree, maxint): + return Poly([rand_rational(maxint) for _ in range(degree+1)], x) + + +def rand_rational_function(x, degree, maxint): + degnum = randint(1, degree) + degden = randint(1, degree) + num = rand_poly(x, degnum, maxint) + den = rand_poly(x, degden, maxint) + while den == Poly(0, x): + den = rand_poly(x, degden, maxint) + return num / den + + +def find_riccati_ode(ratfunc, x, yf): + y = ratfunc + yp = y.diff(x) + q1 = rand_rational_function(x, 1, 3) + q2 = rand_rational_function(x, 1, 3) + while q2 == 0: + q2 = rand_rational_function(x, 1, 3) + q0 = ratsimp(yp - q1*y - q2*y**2) + eq = Eq(yf.diff(), q0 + q1*yf + q2*yf**2) + sol = Eq(yf, y) + assert checkodesol(eq, sol) == (True, 0) + return eq, q0, q1, q2 + + +# Testing functions start + +def test_riccati_transformation(): + """ + This function tests the transformation of the + solution of a Riccati ODE to the solution of + its corresponding normal Riccati ODE. + + Each test case 4 values - + + 1. w - The solution to be transformed + 2. b1 - The coefficient of f(x) in the ODE. + 3. b2 - The coefficient of f(x)**2 in the ODE. + 4. y - The solution to the normal Riccati ODE. + """ + tests = [ + ( + x/(x - 1), + (x**2 + 7)/3*x, + x, + -x**2/(x - 1) - x*(x**2/3 + S(7)/3)/2 - 1/(2*x) + ), + ( + (2*x + 3)/(2*x + 2), + (3 - 3*x)/(x + 1), + 5*x, + -5*x*(2*x + 3)/(2*x + 2) - (3 - 3*x)/(Mul(2, x + 1, evaluate=False)) - 1/(2*x) + ), + ( + -1/(2*x**2 - 1), + 0, + (2 - x)/(4*x - 2), + (2 - x)/((4*x - 2)*(2*x**2 - 1)) - (4*x - 2)*(Mul(-4, 2 - x, evaluate=False)/(4*x - \ + 2)**2 - 1/(4*x - 2))/(Mul(2, 2 - x, evaluate=False)) + ), + ( + x, + (8*x - 12)/(12*x + 9), + x**3/(6*x - 9), + -x**4/(6*x - 9) - (8*x - 12)/(Mul(2, 12*x + 9, evaluate=False)) - (6*x - 9)*(-6*x**3/(6*x \ + - 9)**2 + 3*x**2/(6*x - 9))/(2*x**3) + )] + for w, b1, b2, y in tests: + assert y == riccati_normal(w, x, b1, b2) + assert w == riccati_inverse_normal(y, x, b1, b2).cancel() + + # Test bp parameter in riccati_inverse_normal + tests = [ + ( + (-2*x - 1)/(2*x**2 + 2*x - 2), + -2/x, + (-x - 1)/(4*x), + 8*x**2*(1/(4*x) + (-x - 1)/(4*x**2))/(-x - 1)**2 + 4/(-x - 1), + -2*x*(-1/(4*x) - (-x - 1)/(4*x**2))/(-x - 1) - (-2*x - 1)*(-x - 1)/(4*x*(2*x**2 + 2*x \ + - 2)) + 1/x + ), + ( + 3/(2*x**2), + -2/x, + (-x - 1)/(4*x), + 8*x**2*(1/(4*x) + (-x - 1)/(4*x**2))/(-x - 1)**2 + 4/(-x - 1), + -2*x*(-1/(4*x) - (-x - 1)/(4*x**2))/(-x - 1) + 1/x - Mul(3, -x - 1, evaluate=False)/(8*x**3) + )] + for w, b1, b2, bp, y in tests: + assert y == riccati_normal(w, x, b1, b2) + assert w == riccati_inverse_normal(y, x, b1, b2, bp).cancel() + + +def test_riccati_reduced(): + """ + This function tests the transformation of a + Riccati ODE to its normal Riccati ODE. + + Each test case 2 values - + + 1. eq - A Riccati ODE. + 2. normal_eq - The normal Riccati ODE of eq. + """ + tests = [ + ( + f(x).diff(x) - x**2 - x*f(x) - x*f(x)**2, + + f(x).diff(x) + f(x)**2 + x**3 - x**2/4 - 3/(4*x**2) + ), + ( + 6*x/(2*x + 9) + f(x).diff(x) - (x + 1)*f(x)**2/x, + + -3*x**2*(1/x + (-x - 1)/x**2)**2/(4*(-x - 1)**2) + Mul(6, \ + -x - 1, evaluate=False)/(2*x + 9) + f(x)**2 + f(x).diff(x) \ + - (-1 + (x + 1)/x)/(x*(-x - 1)) + ), + ( + f(x)**2 + f(x).diff(x) - (x - 1)*f(x)/(-x - S(1)/2), + + -(2*x - 2)**2/(4*(2*x + 1)**2) + (2*x - 2)/(2*x + 1)**2 + \ + f(x)**2 + f(x).diff(x) - 1/(2*x + 1) + ), + ( + f(x).diff(x) - f(x)**2/x, + + f(x)**2 + f(x).diff(x) + 1/(4*x**2) + ), + ( + -3*(-x**2 - x + 1)/(x**2 + 6*x + 1) + f(x).diff(x) + f(x)**2/x, + + f(x)**2 + f(x).diff(x) + (3*x**2/(x**2 + 6*x + 1) + 3*x/(x**2 \ + + 6*x + 1) - 3/(x**2 + 6*x + 1))/x + 1/(4*x**2) + ), + ( + 6*x/(2*x + 9) + f(x).diff(x) - (x + 1)*f(x)/x, + + False + ), + ( + f(x)*f(x).diff(x) - 1/x + f(x)/3 + f(x)**2/(x**2 - 2), + + False + )] + for eq, normal_eq in tests: + assert normal_eq == riccati_reduced(eq, f, x) + + +def test_match_riccati(): + """ + This function tests if an ODE is Riccati or not. + + Each test case has 5 values - + + 1. eq - The Riccati ODE. + 2. match - Boolean indicating if eq is a Riccati ODE. + 3. b0 - + 4. b1 - Coefficient of f(x) in eq. + 5. b2 - Coefficient of f(x)**2 in eq. + """ + tests = [ + # Test Rational Riccati ODEs + ( + f(x).diff(x) - (405*x**3 - 882*x**2 - 78*x + 92)/(243*x**4 \ + - 945*x**3 + 846*x**2 + 180*x - 72) - 2 - f(x)**2/(3*x + 1) \ + - (S(1)/3 - x)*f(x)/(S(1)/3 - 3*x/2), + + True, + + 45*x**3/(27*x**4 - 105*x**3 + 94*x**2 + 20*x - 8) - 98*x**2/ \ + (27*x**4 - 105*x**3 + 94*x**2 + 20*x - 8) - 26*x/(81*x**4 - \ + 315*x**3 + 282*x**2 + 60*x - 24) + 2 + 92/(243*x**4 - 945*x**3 \ + + 846*x**2 + 180*x - 72), + + Mul(-1, 2 - 6*x, evaluate=False)/(9*x - 2), + + 1/(3*x + 1) + ), + ( + f(x).diff(x) + 4*x/27 - (x/3 - 1)*f(x)**2 - (2*x/3 + \ + 1)*f(x)/(3*x + 2) - S(10)/27 - (265*x**2 + 423*x + 162) \ + /(324*x**3 + 216*x**2), + + True, + + -4*x/27 + S(10)/27 + 3/(6*x**3 + 4*x**2) + 47/(36*x**2 \ + + 24*x) + 265/(324*x + 216), + + Mul(-1, -2*x - 3, evaluate=False)/(9*x + 6), + + x/3 - 1 + ), + ( + f(x).diff(x) - (304*x**5 - 745*x**4 + 631*x**3 - 876*x**2 \ + + 198*x - 108)/(36*x**6 - 216*x**5 + 477*x**4 - 567*x**3 + \ + 360*x**2 - 108*x) - S(17)/9 - (x - S(3)/2)*f(x)/(x/2 - \ + S(3)/2) - (x/3 - 3)*f(x)**2/(3*x), + + True, + + 304*x**4/(36*x**5 - 216*x**4 + 477*x**3 - 567*x**2 + 360*x - \ + 108) - 745*x**3/(36*x**5 - 216*x**4 + 477*x**3 - 567*x**2 + \ + 360*x - 108) + 631*x**2/(36*x**5 - 216*x**4 + 477*x**3 - 567* \ + x**2 + 360*x - 108) - 292*x/(12*x**5 - 72*x**4 + 159*x**3 - \ + 189*x**2 + 120*x - 36) + S(17)/9 - 12/(4*x**6 - 24*x**5 + \ + 53*x**4 - 63*x**3 + 40*x**2 - 12*x) + 22/(4*x**5 - 24*x**4 \ + + 53*x**3 - 63*x**2 + 40*x - 12), + + Mul(-1, 3 - 2*x, evaluate=False)/(x - 3), + + Mul(-1, 9 - x, evaluate=False)/(9*x) + ), + # Test Non-Rational Riccati ODEs + ( + f(x).diff(x) - x**(S(3)/2)/(x**(S(1)/2) - 2) + x**2*f(x) + \ + x*f(x)**2/(x**(S(3)/4)), + False, 0, 0, 0 + ), + ( + f(x).diff(x) - sin(x**2) + exp(x)*f(x) + log(x)*f(x)**2, + False, 0, 0, 0 + ), + ( + f(x).diff(x) - tanh(x + sqrt(x)) + f(x) + x**4*f(x)**2, + False, 0, 0, 0 + ), + # Test Non-Riccati ODEs + ( + (1 - x**2)*f(x).diff(x, 2) - 2*x*f(x).diff(x) + 20*f(x), + False, 0, 0, 0 + ), + ( + f(x).diff(x) - x**2 + x**3*f(x) + (x**2/(x + 1))*f(x)**3, + False, 0, 0, 0 + ), + ( + f(x).diff(x)*f(x)**2 + (x**2 - 1)/(x**3 + 1)*f(x) + 1/(2*x \ + + 3) + f(x)**2, + False, 0, 0, 0 + )] + for eq, res, b0, b1, b2 in tests: + match, funcs = match_riccati(eq, f, x) + assert match == res + if res: + assert [b0, b1, b2] == funcs + + +def test_val_at_inf(): + """ + This function tests the valuation of rational + function at oo. + + Each test case has 3 values - + + 1. num - Numerator of rational function. + 2. den - Denominator of rational function. + 3. val_inf - Valuation of rational function at oo + """ + tests = [ + # degree(denom) > degree(numer) + ( + Poly(10*x**3 + 8*x**2 - 13*x + 6, x), + Poly(-13*x**10 - x**9 + 5*x**8 + 7*x**7 + 10*x**6 + 6*x**5 - 7*x**4 + 11*x**3 - 8*x**2 + 5*x + 13, x), + 7 + ), + ( + Poly(1, x), + Poly(-9*x**4 + 3*x**3 + 15*x**2 - 6*x - 14, x), + 4 + ), + # degree(denom) == degree(numer) + ( + Poly(-6*x**3 - 8*x**2 + 8*x - 6, x), + Poly(-5*x**3 + 12*x**2 - 6*x - 9, x), + 0 + ), + # degree(denom) < degree(numer) + ( + Poly(12*x**8 - 12*x**7 - 11*x**6 + 8*x**5 + 3*x**4 - x**3 + x**2 - 11*x, x), + Poly(-14*x**2 + x, x), + -6 + ), + ( + Poly(5*x**6 + 9*x**5 - 11*x**4 - 9*x**3 + x**2 - 4*x + 4, x), + Poly(15*x**4 + 3*x**3 - 8*x**2 + 15*x + 12, x), + -2 + )] + for num, den, val in tests: + assert val_at_inf(num, den, x) == val + + +def test_necessary_conds(): + """ + This function tests the necessary conditions for + a Riccati ODE to have a rational particular solution. + """ + # Valuation at Infinity is an odd negative integer + assert check_necessary_conds(-3, [1, 2, 4]) == False + # Valuation at Infinity is a positive integer lesser than 2 + assert check_necessary_conds(1, [1, 2, 4]) == False + # Multiplicity of a pole is an odd integer greater than 1 + assert check_necessary_conds(2, [3, 1, 6]) == False + # All values are correct + assert check_necessary_conds(-10, [1, 2, 8, 12]) == True + + +def test_inverse_transform_poly(): + """ + This function tests the substitution x -> 1/x + in rational functions represented using Poly. + """ + fns = [ + (15*x**3 - 8*x**2 - 2*x - 6)/(18*x + 6), + + (180*x**5 + 40*x**4 + 80*x**3 + 30*x**2 - 60*x - 80)/(180*x**3 - 150*x**2 + 75*x + 12), + + (-15*x**5 - 36*x**4 + 75*x**3 - 60*x**2 - 80*x - 60)/(80*x**4 + 60*x**3 + 60*x**2 + 60*x - 80), + + (60*x**7 + 24*x**6 - 15*x**5 - 20*x**4 + 30*x**2 + 100*x - 60)/(240*x**2 - 20*x - 30), + + (30*x**6 - 12*x**5 + 15*x**4 - 15*x**2 + 10*x + 60)/(3*x**10 - 45*x**9 + 15*x**5 + 15*x**4 - 5*x**3 \ + + 15*x**2 + 45*x - 15) + ] + for f in fns: + num, den = [Poly(e, x) for e in f.as_numer_denom()] + num, den = inverse_transform_poly(num, den, x) + assert f.subs(x, 1/x).cancel() == num/den + + +def test_limit_at_inf(): + """ + This function tests the limit at oo of a + rational function. + + Each test case has 3 values - + + 1. num - Numerator of rational function. + 2. den - Denominator of rational function. + 3. limit_at_inf - Limit of rational function at oo + """ + tests = [ + # deg(denom) > deg(numer) + ( + Poly(-12*x**2 + 20*x + 32, x), + Poly(32*x**3 + 72*x**2 + 3*x - 32, x), + 0 + ), + # deg(denom) < deg(numer) + ( + Poly(1260*x**4 - 1260*x**3 - 700*x**2 - 1260*x + 1400, x), + Poly(6300*x**3 - 1575*x**2 + 756*x - 540, x), + oo + ), + # deg(denom) < deg(numer), one of the leading coefficients is negative + ( + Poly(-735*x**8 - 1400*x**7 + 1680*x**6 - 315*x**5 - 600*x**4 + 840*x**3 - 525*x**2 \ + + 630*x + 3780, x), + Poly(1008*x**7 - 2940*x**6 - 84*x**5 + 2940*x**4 - 420*x**3 + 1512*x**2 + 105*x + 168, x), + -oo + ), + # deg(denom) == deg(numer) + ( + Poly(105*x**7 - 960*x**6 + 60*x**5 + 60*x**4 - 80*x**3 + 45*x**2 + 120*x + 15, x), + Poly(735*x**7 + 525*x**6 + 720*x**5 + 720*x**4 - 8400*x**3 - 2520*x**2 + 2800*x + 280, x), + S(1)/7 + ), + ( + Poly(288*x**4 - 450*x**3 + 280*x**2 - 900*x - 90, x), + Poly(607*x**4 + 840*x**3 - 1050*x**2 + 420*x + 420, x), + S(288)/607 + )] + for num, den, lim in tests: + assert limit_at_inf(num, den, x) == lim + + +def test_construct_c_case_1(): + """ + This function tests the Case 1 in the step + to calculate coefficients of c-vectors. + + Each test case has 4 values - + + 1. num - Numerator of the rational function a(x). + 2. den - Denominator of the rational function a(x). + 3. pole - Pole of a(x) for which c-vector is being + calculated. + 4. c - The c-vector for the pole. + """ + tests = [ + ( + Poly(-3*x**3 + 3*x**2 + 4*x - 5, x, extension=True), + Poly(4*x**8 + 16*x**7 + 9*x**5 + 12*x**4 + 6*x**3 + 12*x**2, x, extension=True), + S(0), + [[S(1)/2 + sqrt(6)*I/6], [S(1)/2 - sqrt(6)*I/6]] + ), + ( + Poly(1200*x**3 + 1440*x**2 + 816*x + 560, x, extension=True), + Poly(128*x**5 - 656*x**4 + 1264*x**3 - 1125*x**2 + 385*x + 49, x, extension=True), + S(7)/4, + [[S(1)/2 + sqrt(16367978)/634], [S(1)/2 - sqrt(16367978)/634]] + ), + ( + Poly(4*x + 2, x, extension=True), + Poly(18*x**4 + (2 - 18*sqrt(3))*x**3 + (14 - 11*sqrt(3))*x**2 + (4 - 6*sqrt(3))*x \ + + 8*sqrt(3) + 16, x, domain='QQ'), + (S(1) + sqrt(3))/2, + [[S(1)/2 + sqrt(Mul(4, 2*sqrt(3) + 4, evaluate=False)/(19*sqrt(3) + 44) + 1)/2], \ + [S(1)/2 - sqrt(Mul(4, 2*sqrt(3) + 4, evaluate=False)/(19*sqrt(3) + 44) + 1)/2]] + )] + for num, den, pole, c in tests: + assert construct_c_case_1(num, den, x, pole) == c + + +def test_construct_c_case_2(): + """ + This function tests the Case 2 in the step + to calculate coefficients of c-vectors. + + Each test case has 5 values - + + 1. num - Numerator of the rational function a(x). + 2. den - Denominator of the rational function a(x). + 3. pole - Pole of a(x) for which c-vector is being + calculated. + 4. mul - The multiplicity of the pole. + 5. c - The c-vector for the pole. + """ + tests = [ + # Testing poles with multiplicity 2 + ( + Poly(1, x, extension=True), + Poly((x - 1)**2*(x - 2), x, extension=True), + 1, 2, + [[-I*(-1 - I)/2], [I*(-1 + I)/2]] + ), + ( + Poly(3*x**5 - 12*x**4 - 7*x**3 + 1, x, extension=True), + Poly((3*x - 1)**2*(x + 2)**2, x, extension=True), + S(1)/3, 2, + [[-S(89)/98], [-S(9)/98]] + ), + # Testing poles with multiplicity 4 + ( + Poly(x**3 - x**2 + 4*x, x, extension=True), + Poly((x - 2)**4*(x + 5)**2, x, extension=True), + 2, 4, + [[7*sqrt(3)*(S(60)/343 - 4*sqrt(3)/7)/12, 2*sqrt(3)/7], \ + [-7*sqrt(3)*(S(60)/343 + 4*sqrt(3)/7)/12, -2*sqrt(3)/7]] + ), + ( + Poly(3*x**5 + x**4 + 3, x, extension=True), + Poly((4*x + 1)**4*(x + 2), x, extension=True), + -S(1)/4, 4, + [[128*sqrt(439)*(-sqrt(439)/128 - S(55)/14336)/439, sqrt(439)/256], \ + [-128*sqrt(439)*(sqrt(439)/128 - S(55)/14336)/439, -sqrt(439)/256]] + ), + # Testing poles with multiplicity 6 + ( + Poly(x**3 + 2, x, extension=True), + Poly((3*x - 1)**6*(x**2 + 1), x, extension=True), + S(1)/3, 6, + [[27*sqrt(66)*(-sqrt(66)/54 - S(131)/267300)/22, -2*sqrt(66)/1485, sqrt(66)/162], \ + [-27*sqrt(66)*(sqrt(66)/54 - S(131)/267300)/22, 2*sqrt(66)/1485, -sqrt(66)/162]] + ), + ( + Poly(x**2 + 12, x, extension=True), + Poly((x - sqrt(2))**6, x, extension=True), + sqrt(2), 6, + [[sqrt(14)*(S(6)/7 - 3*sqrt(14))/28, sqrt(7)/7, sqrt(14)], \ + [-sqrt(14)*(S(6)/7 + 3*sqrt(14))/28, -sqrt(7)/7, -sqrt(14)]] + )] + for num, den, pole, mul, c in tests: + assert construct_c_case_2(num, den, x, pole, mul) == c + + +def test_construct_c_case_3(): + """ + This function tests the Case 3 in the step + to calculate coefficients of c-vectors. + """ + assert construct_c_case_3() == [[1]] + + +def test_construct_d_case_4(): + """ + This function tests the Case 4 in the step + to calculate coefficients of the d-vector. + + Each test case has 4 values - + + 1. num - Numerator of the rational function a(x). + 2. den - Denominator of the rational function a(x). + 3. mul - Multiplicity of oo as a pole. + 4. d - The d-vector. + """ + tests = [ + # Tests with multiplicity at oo = 2 + ( + Poly(-x**5 - 2*x**4 + 4*x**3 + 2*x + 5, x, extension=True), + Poly(9*x**3 - 2*x**2 + 10*x - 2, x, extension=True), + 2, + [[10*I/27, I/3, -3*I*(S(158)/243 - I/3)/2], \ + [-10*I/27, -I/3, 3*I*(S(158)/243 + I/3)/2]] + ), + ( + Poly(-x**6 + 9*x**5 + 5*x**4 + 6*x**3 + 5*x**2 + 6*x + 7, x, extension=True), + Poly(x**4 + 3*x**3 + 12*x**2 - x + 7, x, extension=True), + 2, + [[-6*I, I, -I*(17 - I)/2], [6*I, -I, I*(17 + I)/2]] + ), + # Tests with multiplicity at oo = 4 + ( + Poly(-2*x**6 - x**5 - x**4 - 2*x**3 - x**2 - 3*x - 3, x, extension=True), + Poly(3*x**2 + 10*x + 7, x, extension=True), + 4, + [[269*sqrt(6)*I/288, -17*sqrt(6)*I/36, sqrt(6)*I/3, -sqrt(6)*I*(S(16969)/2592 \ + - 2*sqrt(6)*I/3)/4], [-269*sqrt(6)*I/288, 17*sqrt(6)*I/36, -sqrt(6)*I/3, \ + sqrt(6)*I*(S(16969)/2592 + 2*sqrt(6)*I/3)/4]] + ), + ( + Poly(-3*x**5 - 3*x**4 - 3*x**3 - x**2 - 1, x, extension=True), + Poly(12*x - 2, x, extension=True), + 4, + [[41*I/192, 7*I/24, I/2, -I*(-S(59)/6912 - I)], \ + [-41*I/192, -7*I/24, -I/2, I*(-S(59)/6912 + I)]] + ), + # Tests with multiplicity at oo = 4 + ( + Poly(-x**7 - x**5 - x**4 - x**2 - x, x, extension=True), + Poly(x + 2, x, extension=True), + 6, + [[-5*I/2, 2*I, -I, I, -I*(-9 - 3*I)/2], [5*I/2, -2*I, I, -I, I*(-9 + 3*I)/2]] + ), + ( + Poly(-x**7 - x**6 - 2*x**5 - 2*x**4 - x**3 - x**2 + 2*x - 2, x, extension=True), + Poly(2*x - 2, x, extension=True), + 6, + [[3*sqrt(2)*I/4, 3*sqrt(2)*I/4, sqrt(2)*I/2, sqrt(2)*I/2, -sqrt(2)*I*(-S(7)/8 - \ + 3*sqrt(2)*I/2)/2], [-3*sqrt(2)*I/4, -3*sqrt(2)*I/4, -sqrt(2)*I/2, -sqrt(2)*I/2, \ + sqrt(2)*I*(-S(7)/8 + 3*sqrt(2)*I/2)/2]] + )] + for num, den, mul, d in tests: + ser = rational_laurent_series(num, den, x, oo, mul, 1) + assert construct_d_case_4(ser, mul//2) == d + + +def test_construct_d_case_5(): + """ + This function tests the Case 5 in the step + to calculate coefficients of the d-vector. + + Each test case has 3 values - + + 1. num - Numerator of the rational function a(x). + 2. den - Denominator of the rational function a(x). + 3. d - The d-vector. + """ + tests = [ + ( + Poly(2*x**3 + x**2 + x - 2, x, extension=True), + Poly(9*x**3 + 5*x**2 + 2*x - 1, x, extension=True), + [[sqrt(2)/3, -sqrt(2)/108], [-sqrt(2)/3, sqrt(2)/108]] + ), + ( + Poly(3*x**5 + x**4 - x**3 + x**2 - 2*x - 2, x, domain='ZZ'), + Poly(9*x**5 + 7*x**4 + 3*x**3 + 2*x**2 + 5*x + 7, x, domain='ZZ'), + [[sqrt(3)/3, -2*sqrt(3)/27], [-sqrt(3)/3, 2*sqrt(3)/27]] + ), + ( + Poly(x**2 - x + 1, x, domain='ZZ'), + Poly(3*x**2 + 7*x + 3, x, domain='ZZ'), + [[sqrt(3)/3, -5*sqrt(3)/9], [-sqrt(3)/3, 5*sqrt(3)/9]] + )] + for num, den, d in tests: + # Multiplicity of oo is 0 + ser = rational_laurent_series(num, den, x, oo, 0, 1) + assert construct_d_case_5(ser) == d + + +def test_construct_d_case_6(): + """ + This function tests the Case 6 in the step + to calculate coefficients of the d-vector. + + Each test case has 3 values - + + 1. num - Numerator of the rational function a(x). + 2. den - Denominator of the rational function a(x). + 3. d - The d-vector. + """ + tests = [ + ( + Poly(-2*x**2 - 5, x, domain='ZZ'), + Poly(4*x**4 + 2*x**2 + 10*x + 2, x, domain='ZZ'), + [[S(1)/2 + I/2], [S(1)/2 - I/2]] + ), + ( + Poly(-2*x**3 - 4*x**2 - 2*x - 5, x, domain='ZZ'), + Poly(x**6 - x**5 + 2*x**4 - 4*x**3 - 5*x**2 - 5*x + 9, x, domain='ZZ'), + [[1], [0]] + ), + ( + Poly(-5*x**3 + x**2 + 11*x + 12, x, domain='ZZ'), + Poly(6*x**8 - 26*x**7 - 27*x**6 - 10*x**5 - 44*x**4 - 46*x**3 - 34*x**2 \ + - 27*x - 42, x, domain='ZZ'), + [[1], [0]] + )] + for num, den, d in tests: + assert construct_d_case_6(num, den, x) == d + + +def test_rational_laurent_series(): + """ + This function tests the computation of coefficients + of Laurent series of a rational function. + + Each test case has 5 values - + + 1. num - Numerator of the rational function. + 2. den - Denominator of the rational function. + 3. x0 - Point about which Laurent series is to + be calculated. + 4. mul - Multiplicity of x0 if x0 is a pole of + the rational function (0 otherwise). + 5. n - Number of terms upto which the series + is to be calculated. + """ + tests = [ + # Laurent series about simple pole (Multiplicity = 1) + ( + Poly(x**2 - 3*x + 9, x, extension=True), + Poly(x**2 - x, x, extension=True), + S(1), 1, 6, + {1: 7, 0: -8, -1: 9, -2: -9, -3: 9, -4: -9} + ), + # Laurent series about multiple pole (Multiplicity > 1) + ( + Poly(64*x**3 - 1728*x + 1216, x, extension=True), + Poly(64*x**4 - 80*x**3 - 831*x**2 + 1809*x - 972, x, extension=True), + S(9)/8, 2, 3, + {0: S(32177152)/46521675, 2: S(1019)/984, -1: S(11947565056)/28610830125, \ + 1: S(209149)/75645} + ), + ( + Poly(1, x, extension=True), + Poly(x**5 + (-4*sqrt(2) - 1)*x**4 + (4*sqrt(2) + 12)*x**3 + (-12 - 8*sqrt(2))*x**2 \ + + (4 + 8*sqrt(2))*x - 4, x, extension=True), + sqrt(2), 4, 6, + {4: 1 + sqrt(2), 3: -3 - 2*sqrt(2), 2: Mul(-1, -3 - 2*sqrt(2), evaluate=False)/(-1 \ + + sqrt(2)), 1: (-3 - 2*sqrt(2))/(-1 + sqrt(2))**2, 0: Mul(-1, -3 - 2*sqrt(2), evaluate=False \ + )/(-1 + sqrt(2))**3, -1: (-3 - 2*sqrt(2))/(-1 + sqrt(2))**4} + ), + # Laurent series about oo + ( + Poly(x**5 - 4*x**3 + 6*x**2 + 10*x - 13, x, extension=True), + Poly(x**2 - 5, x, extension=True), + oo, 3, 6, + {3: 1, 2: 0, 1: 1, 0: 6, -1: 15, -2: 17} + ), + # Laurent series at x0 where x0 is not a pole of the function + # Using multiplicity as 0 (as x0 will not be a pole) + ( + Poly(3*x**3 + 6*x**2 - 2*x + 5, x, extension=True), + Poly(9*x**4 - x**3 - 3*x**2 + 4*x + 4, x, extension=True), + S(2)/5, 0, 1, + {0: S(3345)/3304, -1: S(399325)/2729104, -2: S(3926413375)/4508479808, \ + -3: S(-5000852751875)/1862002160704, -4: S(-6683640101653125)/6152055138966016} + ), + ( + Poly(-7*x**2 + 2*x - 4, x, extension=True), + Poly(7*x**5 + 9*x**4 + 8*x**3 + 3*x**2 + 6*x + 9, x, extension=True), + oo, 0, 6, + {0: 0, -2: 0, -5: -S(71)/49, -1: 0, -3: -1, -4: S(11)/7} + )] + for num, den, x0, mul, n, ser in tests: + assert ser == rational_laurent_series(num, den, x, x0, mul, n) + + +def check_dummy_sol(eq, solse, dummy_sym): + """ + Helper function to check if actual solution + matches expected solution if actual solution + contains dummy symbols. + """ + if isinstance(eq, Eq): + eq = eq.lhs - eq.rhs + _, funcs = match_riccati(eq, f, x) + + sols = solve_riccati(f(x), x, *funcs) + C1 = Dummy('C1') + sols = [sol.subs(C1, dummy_sym) for sol in sols] + + assert all(x[0] for x in checkodesol(eq, sols)) + assert all(s1.dummy_eq(s2, dummy_sym) for s1, s2 in zip(sols, solse)) + + +def test_solve_riccati(): + """ + This function tests the computation of rational + particular solutions for a Riccati ODE. + + Each test case has 2 values - + + 1. eq - Riccati ODE to be solved. + 2. sol - Expected solution to the equation. + + Some examples have been taken from the paper - "Statistical Investigation of + First-Order Algebraic ODEs and their Rational General Solutions" by + Georg Grasegger, N. Thieu Vo, Franz Winkler + + https://www3.risc.jku.at/publications/download/risc_5197/RISCReport15-19.pdf + """ + C0 = Dummy('C0') + # Type: 1st Order Rational Riccati, dy/dx = a + b*y + c*y**2, + # a, b, c are rational functions of x + + tests = [ + # a(x) is a constant + ( + Eq(f(x).diff(x) + f(x)**2 - 2, 0), + [Eq(f(x), sqrt(2)), Eq(f(x), -sqrt(2))] + ), + # a(x) is a constant + ( + f(x)**2 + f(x).diff(x) + 4*f(x)/x + 2/x**2, + [Eq(f(x), (-2*C0 - x)/(C0*x + x**2))] + ), + # a(x) is a constant + ( + 2*x**2*f(x).diff(x) - x*(4*f(x) + f(x).diff(x) - 4) + (f(x) - 1)*f(x), + [Eq(f(x), (C0 + 2*x**2)/(C0 + x))] + ), + # Pole with multiplicity 1 + ( + Eq(f(x).diff(x), -f(x)**2 - 2/(x**3 - x**2)), + [Eq(f(x), 1/(x**2 - x))] + ), + # One pole of multiplicity 2 + ( + x**2 - (2*x + 1/x)*f(x) + f(x)**2 + f(x).diff(x), + [Eq(f(x), (C0*x + x**3 + 2*x)/(C0 + x**2)), Eq(f(x), x)] + ), + ( + x**4*f(x).diff(x) + x**2 - x*(2*f(x)**2 + f(x).diff(x)) + f(x), + [Eq(f(x), (C0*x**2 + x)/(C0 + x**2)), Eq(f(x), x**2)] + ), + # Multiple poles of multiplicity 2 + ( + -f(x)**2 + f(x).diff(x) + (15*x**2 - 20*x + 7)/((x - 1)**2*(2*x \ + - 1)**2), + [Eq(f(x), (9*C0*x - 6*C0 - 15*x**5 + 60*x**4 - 94*x**3 + 72*x**2 \ + - 30*x + 6)/(6*C0*x**2 - 9*C0*x + 3*C0 + 6*x**6 - 29*x**5 + \ + 57*x**4 - 58*x**3 + 30*x**2 - 6*x)), Eq(f(x), (3*x - 2)/(2*x**2 \ + - 3*x + 1))] + ), + # Regression: Poles with even multiplicity > 2 fixed + ( + f(x)**2 + f(x).diff(x) - (4*x**6 - 8*x**5 + 12*x**4 + 4*x**3 + \ + 7*x**2 - 20*x + 4)/(4*x**4), + [Eq(f(x), (2*x**5 - 2*x**4 - x**3 + 4*x**2 + 3*x - 2)/(2*x**4 \ + - 2*x**2))] + ), + # Regression: Poles with even multiplicity > 2 fixed + ( + Eq(f(x).diff(x), (-x**6 + 15*x**4 - 40*x**3 + 45*x**2 - 24*x + 4)/\ + (x**12 - 12*x**11 + 66*x**10 - 220*x**9 + 495*x**8 - 792*x**7 + 924*x**6 - \ + 792*x**5 + 495*x**4 - 220*x**3 + 66*x**2 - 12*x + 1) + f(x)**2 + f(x)), + [Eq(f(x), 1/(x**6 - 6*x**5 + 15*x**4 - 20*x**3 + 15*x**2 - 6*x + 1))] + ), + # More than 2 poles with multiplicity 2 + # Regression: Fixed mistake in necessary conditions + ( + Eq(f(x).diff(x), x*f(x) + 2*x + (3*x - 2)*f(x)**2/(4*x + 2) + \ + (8*x**2 - 7*x + 26)/(16*x**3 - 24*x**2 + 8) - S(3)/2), + [Eq(f(x), (1 - 4*x)/(2*x - 2))] + ), + # Regression: Fixed mistake in necessary conditions + ( + Eq(f(x).diff(x), (-12*x**2 - 48*x - 15)/(24*x**3 - 40*x**2 + 8*x + 8) \ + + 3*f(x)**2/(6*x + 2)), + [Eq(f(x), (2*x + 1)/(2*x - 2))] + ), + # Imaginary poles + ( + f(x).diff(x) + (3*x**2 + 1)*f(x)**2/x + (6*x**2 - x + 3)*f(x)/(x*(x \ + - 1)) + (3*x**2 - 2*x + 2)/(x*(x - 1)**2), + [Eq(f(x), (-C0 - x**3 + x**2 - 2*x)/(C0*x - C0 + x**4 - x**3 + x**2 \ + - x)), Eq(f(x), -1/(x - 1))], + ), + # Imaginary coefficients in equation + ( + f(x).diff(x) - 2*I*(f(x)**2 + 1)/x, + [Eq(f(x), (-I*C0 + I*x**4)/(C0 + x**4)), Eq(f(x), -I)] + ), + # Regression: linsolve returning empty solution + # Large value of m (> 10) + ( + Eq(f(x).diff(x), x*f(x)/(S(3)/2 - 2*x) + (x/2 - S(1)/3)*f(x)**2/\ + (2*x/3 - S(1)/2) - S(5)/4 + (281*x**2 - 1260*x + 756)/(16*x**3 - 12*x**2)), + [Eq(f(x), (9 - x)/x), Eq(f(x), (40*x**14 + 28*x**13 + 420*x**12 + 2940*x**11 + \ + 18480*x**10 + 103950*x**9 + 519750*x**8 + 2286900*x**7 + 8731800*x**6 + 28378350*\ + x**5 + 76403250*x**4 + 163721250*x**3 + 261954000*x**2 + 278326125*x + 147349125)/\ + ((24*x**14 + 140*x**13 + 840*x**12 + 4620*x**11 + 23100*x**10 + 103950*x**9 + \ + 415800*x**8 + 1455300*x**7 + 4365900*x**6 + 10914750*x**5 + 21829500*x**4 + 32744250\ + *x**3 + 32744250*x**2 + 16372125*x)))] + ), + # Regression: Fixed bug due to a typo in paper + ( + Eq(f(x).diff(x), 18*x**3 + 18*x**2 + (-x/2 - S(1)/2)*f(x)**2 + 6), + [Eq(f(x), 6*x)] + ), + # Regression: Fixed bug due to a typo in paper + ( + Eq(f(x).diff(x), -3*x**3/4 + 15*x/2 + (x/3 - S(4)/3)*f(x)**2 \ + + 9 + (1 - x)*f(x)/x + 3/x), + [Eq(f(x), -3*x/2 - 3)] + )] + for eq, sol in tests: + check_dummy_sol(eq, sol, C0) + + +@slow +def test_solve_riccati_slow(): + """ + This function tests the computation of rational + particular solutions for a Riccati ODE. + + Each test case has 2 values - + + 1. eq - Riccati ODE to be solved. + 2. sol - Expected solution to the equation. + """ + C0 = Dummy('C0') + tests = [ + # Very large values of m (989 and 991) + ( + Eq(f(x).diff(x), (1 - x)*f(x)/(x - 3) + (2 - 12*x)*f(x)**2/(2*x - 9) + \ + (54924*x**3 - 405264*x**2 + 1084347*x - 1087533)/(8*x**4 - 132*x**3 + 810*x**2 - \ + 2187*x + 2187) + 495), + [Eq(f(x), (18*x + 6)/(2*x - 9))] + )] + for eq, sol in tests: + check_dummy_sol(eq, sol, C0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/tests/test_single.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/tests/test_single.py new file mode 100644 index 0000000000000000000000000000000000000000..45b38029e97b9e74236c45f2f3efb6aa87c26e5c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/tests/test_single.py @@ -0,0 +1,2902 @@ +# +# The main tests for the code in single.py are currently located in +# sympy/solvers/tests/test_ode.py +# +r""" +This File contains test functions for the individual hints used for solving ODEs. + +Examples of each solver will be returned by _get_examples_ode_sol_name_of_solver. + +Examples should have a key 'XFAIL' which stores the list of hints if they are +expected to fail for that hint. + +Functions that are for internal use: + +1) _ode_solver_test(ode_examples) - It takes a dictionary of examples returned by + _get_examples method and tests them with their respective hints. + +2) _test_particular_example(our_hint, example_name) - It tests the ODE example corresponding + to the hint provided. + +3) _test_all_hints(runxfail=False) - It is used to test all the examples with all the hints + currently implemented. It calls _test_all_examples_for_one_hint() which outputs whether the + given hint functions properly if it classifies the ODE example. + If runxfail flag is set to True then it will only test the examples which are expected to fail. + + Everytime the ODE of a particular solver is added, _test_all_hints() is to be executed to find + the possible failures of different solver hints. + +4) _test_all_examples_for_one_hint(our_hint, all_examples) - It takes hint as argument and checks + this hint against all the ODE examples and gives output as the number of ODEs matched, number + of ODEs which were solved correctly, list of ODEs which gives incorrect solution and list of + ODEs which raises exception. + +""" +from sympy.core.function import (Derivative, diff) +from sympy.core.mul import Mul +from sympy.core.numbers import (E, I, Rational, pi) +from sympy.core.relational import (Eq, Ne) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, symbols) +from sympy.functions.elementary.complexes import (im, re) +from sympy.functions.elementary.exponential import (LambertW, exp, log) +from sympy.functions.elementary.hyperbolic import (asinh, cosh, sinh, tanh) +from sympy.functions.elementary.miscellaneous import (cbrt, sqrt) +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (acos, asin, atan, cos, sec, sin, tan) +from sympy.functions.special.error_functions import (Ei, erfi) +from sympy.functions.special.hyper import hyper +from sympy.integrals.integrals import (Integral, integrate) +from sympy.polys.rootoftools import rootof + +from sympy.core import Function, Symbol +from sympy.functions import airyai, airybi, besselj, bessely, lowergamma +from sympy.integrals.risch import NonElementaryIntegral +from sympy.solvers.ode import classify_ode, dsolve +from sympy.solvers.ode.ode import allhints, _remove_redundant_solutions +from sympy.solvers.ode.single import (FirstLinear, ODEMatchError, + SingleODEProblem, SingleODESolver, NthOrderReducible) + +from sympy.solvers.ode.subscheck import checkodesol + +from sympy.testing.pytest import raises, slow +import traceback + + +x = Symbol('x') +u = Symbol('u') +_u = Dummy('u') +y = Symbol('y') +f = Function('f') +g = Function('g') +C1, C2, C3, C4, C5, C6, C7, C8, C9, C10 = symbols('C1:11') +a, b, c = symbols('a b c') + + +hint_message = """\ +Hint did not match the example {example}. + +The ODE is: +{eq}. + +The expected hint was +{our_hint}\ +""" + +expected_sol_message = """\ +Different solution found from dsolve for example {example}. + +The ODE is: +{eq} + +The expected solution was +{sol} + +What dsolve returned is: +{dsolve_sol}\ +""" + +checkodesol_msg = """\ +solution found is not correct for example {example}. + +The ODE is: +{eq}\ +""" + +dsol_incorrect_msg = """\ +solution returned by dsolve is incorrect when using {hint}. + +The ODE is: +{eq} + +The expected solution was +{sol} + +what dsolve returned is: +{dsolve_sol} + +You can test this with: + +eq = {eq} +sol = dsolve(eq, hint='{hint}') +print(sol) +print(checkodesol(eq, sol)) + +""" + +exception_msg = """\ +dsolve raised exception : {e} + +when using {hint} for the example {example} + +You can test this with: + +from sympy.solvers.ode.tests.test_single import _test_an_example + +_test_an_example('{hint}', example_name = '{example}') + +The ODE is: +{eq} + +\ +""" + +check_hint_msg = """\ +Tested hint was : {hint} + +Total of {matched} examples matched with this hint. + +Out of which {solve} gave correct results. + +Examples which gave incorrect results are {unsolve}. + +Examples which raised exceptions are {exceptions} +\ +""" + + +def _add_example_keys(func): + def inner(): + solver=func() + examples=[] + for example in solver['examples']: + temp={ + 'eq': solver['examples'][example]['eq'], + 'sol': solver['examples'][example]['sol'], + 'XFAIL': solver['examples'][example].get('XFAIL', []), + 'func': solver['examples'][example].get('func',solver['func']), + 'example_name': example, + 'slow': solver['examples'][example].get('slow', False), + 'simplify_flag':solver['examples'][example].get('simplify_flag',True), + 'checkodesol_XFAIL': solver['examples'][example].get('checkodesol_XFAIL', False), + 'dsolve_too_slow':solver['examples'][example].get('dsolve_too_slow',False), + 'checkodesol_too_slow':solver['examples'][example].get('checkodesol_too_slow',False), + 'hint': solver['hint'] + } + examples.append(temp) + return examples + return inner() + + +def _ode_solver_test(ode_examples, run_slow_test=False): + for example in ode_examples: + if ((not run_slow_test) and example['slow']) or (run_slow_test and (not example['slow'])): + continue + + result = _test_particular_example(example['hint'], example, solver_flag=True) + if result['xpass_msg'] != "": + print(result['xpass_msg']) + + +def _test_all_hints(runxfail=False): + all_hints = list(allhints)+["default"] + all_examples = _get_all_examples() + + for our_hint in all_hints: + if our_hint.endswith('_Integral') or 'series' in our_hint: + continue + _test_all_examples_for_one_hint(our_hint, all_examples, runxfail) + + +def _test_dummy_sol(expected_sol,dsolve_sol): + if type(dsolve_sol)==list: + return any(expected_sol.dummy_eq(sub_dsol) for sub_dsol in dsolve_sol) + else: + return expected_sol.dummy_eq(dsolve_sol) + + +def _test_an_example(our_hint, example_name): + all_examples = _get_all_examples() + for example in all_examples: + if example['example_name'] == example_name: + _test_particular_example(our_hint, example) + + +def _test_particular_example(our_hint, ode_example, solver_flag=False): + eq = ode_example['eq'] + expected_sol = ode_example['sol'] + example = ode_example['example_name'] + xfail = our_hint in ode_example['XFAIL'] + func = ode_example['func'] + result = {'msg': '', 'xpass_msg': ''} + simplify_flag=ode_example['simplify_flag'] + checkodesol_XFAIL = ode_example['checkodesol_XFAIL'] + dsolve_too_slow = ode_example['dsolve_too_slow'] + checkodesol_too_slow = ode_example['checkodesol_too_slow'] + xpass = True + if solver_flag: + if our_hint not in classify_ode(eq, func): + message = hint_message.format(example=example, eq=eq, our_hint=our_hint) + raise AssertionError(message) + + if our_hint in classify_ode(eq, func): + result['match_list'] = example + try: + if not (dsolve_too_slow): + dsolve_sol = dsolve(eq, func, simplify=simplify_flag,hint=our_hint) + else: + if len(expected_sol)==1: + dsolve_sol = expected_sol[0] + else: + dsolve_sol = expected_sol + + except Exception as e: + dsolve_sol = [] + result['exception_list'] = example + if not solver_flag: + traceback.print_exc() + result['msg'] = exception_msg.format(e=str(e), hint=our_hint, example=example, eq=eq) + if solver_flag and not xfail: + print(result['msg']) + raise + xpass = False + + if solver_flag and dsolve_sol!=[]: + expect_sol_check = False + if type(dsolve_sol)==list: + for sub_sol in expected_sol: + if sub_sol.has(Dummy): + expect_sol_check = not _test_dummy_sol(sub_sol, dsolve_sol) + else: + expect_sol_check = sub_sol not in dsolve_sol + if expect_sol_check: + break + else: + expect_sol_check = dsolve_sol not in expected_sol + for sub_sol in expected_sol: + if sub_sol.has(Dummy): + expect_sol_check = not _test_dummy_sol(sub_sol, dsolve_sol) + + if expect_sol_check: + message = expected_sol_message.format(example=example, eq=eq, sol=expected_sol, dsolve_sol=dsolve_sol) + raise AssertionError(message) + + expected_checkodesol = [(True, 0) for i in range(len(expected_sol))] + if len(expected_sol) == 1: + expected_checkodesol = (True, 0) + + if not checkodesol_too_slow: + if not checkodesol_XFAIL: + if checkodesol(eq, dsolve_sol, func, solve_for_func=False) != expected_checkodesol: + result['unsolve_list'] = example + xpass = False + message = dsol_incorrect_msg.format(hint=our_hint, eq=eq, sol=expected_sol,dsolve_sol=dsolve_sol) + if solver_flag: + message = checkodesol_msg.format(example=example, eq=eq) + raise AssertionError(message) + else: + result['msg'] = 'AssertionError: ' + message + + if xpass and xfail: + result['xpass_msg'] = example + "is now passing for the hint" + our_hint + return result + + +def _test_all_examples_for_one_hint(our_hint, all_examples=[], runxfail=None): + if all_examples == []: + all_examples = _get_all_examples() + match_list, unsolve_list, exception_list = [], [], [] + for ode_example in all_examples: + xfail = our_hint in ode_example['XFAIL'] + if runxfail and not xfail: + continue + if xfail: + continue + result = _test_particular_example(our_hint, ode_example) + match_list += result.get('match_list',[]) + unsolve_list += result.get('unsolve_list',[]) + exception_list += result.get('exception_list',[]) + if runxfail is not None: + msg = result['msg'] + if msg!='': + print(result['msg']) + # print(result.get('xpass_msg','')) + if runxfail is None: + match_count = len(match_list) + solved = len(match_list)-len(unsolve_list)-len(exception_list) + msg = check_hint_msg.format(hint=our_hint, matched=match_count, solve=solved, unsolve=unsolve_list, exceptions=exception_list) + print(msg) + + +def test_SingleODESolver(): + # Test that not implemented methods give NotImplementedError + # Subclasses should override these methods. + problem = SingleODEProblem(f(x).diff(x), f(x), x) + solver = SingleODESolver(problem) + raises(NotImplementedError, lambda: solver.matches()) + raises(NotImplementedError, lambda: solver.get_general_solution()) + raises(NotImplementedError, lambda: solver._matches()) + raises(NotImplementedError, lambda: solver._get_general_solution()) + + # This ODE can not be solved by the FirstLinear solver. Here we test that + # it does not match and the asking for a general solution gives + # ODEMatchError + + problem = SingleODEProblem(f(x).diff(x) + f(x)*f(x), f(x), x) + + solver = FirstLinear(problem) + raises(ODEMatchError, lambda: solver.get_general_solution()) + + solver = FirstLinear(problem) + assert solver.matches() is False + + #These are just test for order of ODE + + problem = SingleODEProblem(f(x).diff(x) + f(x), f(x), x) + assert problem.order == 1 + + problem = SingleODEProblem(f(x).diff(x,4) + f(x).diff(x,2) - f(x).diff(x,3), f(x), x) + assert problem.order == 4 + + problem = SingleODEProblem(f(x).diff(x, 3) + f(x).diff(x, 2) - f(x)**2, f(x), x) + assert problem.is_autonomous == True + + problem = SingleODEProblem(f(x).diff(x, 3) + x*f(x).diff(x, 2) - f(x)**2, f(x), x) + assert problem.is_autonomous == False + + +def test_linear_coefficients(): + _ode_solver_test(_get_examples_ode_sol_linear_coefficients) + + +@slow +def test_1st_homogeneous_coeff_ode(): + #These were marked as test_1st_homogeneous_coeff_corner_case + eq1 = f(x).diff(x) - f(x)/x + c1 = classify_ode(eq1, f(x)) + eq2 = x*f(x).diff(x) - f(x) + c2 = classify_ode(eq2, f(x)) + sdi = "1st_homogeneous_coeff_subs_dep_div_indep" + sid = "1st_homogeneous_coeff_subs_indep_div_dep" + assert sid not in c1 and sdi not in c1 + assert sid not in c2 and sdi not in c2 + _ode_solver_test(_get_examples_ode_sol_1st_homogeneous_coeff_subs_dep_div_indep) + _ode_solver_test(_get_examples_ode_sol_1st_homogeneous_coeff_best) + + +@slow +def test_slow_examples_1st_homogeneous_coeff_ode(): + _ode_solver_test(_get_examples_ode_sol_1st_homogeneous_coeff_subs_dep_div_indep, run_slow_test=True) + _ode_solver_test(_get_examples_ode_sol_1st_homogeneous_coeff_best, run_slow_test=True) + + +@slow +def test_nth_linear_constant_coeff_homogeneous(): + _ode_solver_test(_get_examples_ode_sol_nth_linear_constant_coeff_homogeneous) + + +@slow +def test_slow_examples_nth_linear_constant_coeff_homogeneous(): + _ode_solver_test(_get_examples_ode_sol_nth_linear_constant_coeff_homogeneous, run_slow_test=True) + + +def test_Airy_equation(): + _ode_solver_test(_get_examples_ode_sol_2nd_linear_airy) + + +@slow +def test_lie_group(): + _ode_solver_test(_get_examples_ode_sol_lie_group) + + +@slow +def test_separable_reduced(): + df = f(x).diff(x) + eq = (x / f(x))*df + tan(x**2*f(x) / (x**2*f(x) - 1)) + assert classify_ode(eq) == ('factorable', 'separable_reduced', 'lie_group', + 'separable_reduced_Integral') + _ode_solver_test(_get_examples_ode_sol_separable_reduced) + + +@slow +def test_slow_examples_separable_reduced(): + _ode_solver_test(_get_examples_ode_sol_separable_reduced, run_slow_test=True) + + +@slow +def test_2nd_2F1_hypergeometric(): + _ode_solver_test(_get_examples_ode_sol_2nd_2F1_hypergeometric) + + +def test_2nd_2F1_hypergeometric_integral(): + eq = x*(x-1)*f(x).diff(x, 2) + (-1+ S(7)/2*x)*f(x).diff(x) + f(x) + sol = Eq(f(x), (C1 + C2*Integral(exp(Integral((1 - x/2)/(x*(x - 1)), x))/(1 - + x/2)**2, x))*exp(Integral(1/(x - 1), x)/4)*exp(-Integral(7/(x - + 1), x)/4)*hyper((S(1)/2, -1), (1,), x)) + assert sol == dsolve(eq, hint='2nd_hypergeometric_Integral') + assert checkodesol(eq, sol) == (True, 0) + + +@slow +def test_2nd_nonlinear_autonomous_conserved(): + _ode_solver_test(_get_examples_ode_sol_2nd_nonlinear_autonomous_conserved) + + +def test_2nd_nonlinear_autonomous_conserved_integral(): + eq = f(x).diff(x, 2) + asin(f(x)) + actual = [Eq(Integral(1/sqrt(C1 - 2*Integral(asin(_u), _u)), (_u, f(x))), C2 + x), + Eq(Integral(1/sqrt(C1 - 2*Integral(asin(_u), _u)), (_u, f(x))), C2 - x)] + solved = dsolve(eq, hint='2nd_nonlinear_autonomous_conserved_Integral', simplify=False) + for a,s in zip(actual, solved): + assert a.dummy_eq(s) + # checkodesol unable to simplify solutions with f(x) in an integral equation + assert checkodesol(eq, [s.doit() for s in solved]) == [(True, 0), (True, 0)] + + +@slow +def test_2nd_linear_bessel_equation(): + _ode_solver_test(_get_examples_ode_sol_2nd_linear_bessel) + + +@slow +def test_nth_algebraic(): + eqn = f(x) + f(x)*f(x).diff(x) + solns = [Eq(f(x), exp(x)), + Eq(f(x), C1*exp(C2*x))] + solns_final = _remove_redundant_solutions(eqn, solns, 2, x) + assert solns_final == [Eq(f(x), C1*exp(C2*x))] + + _ode_solver_test(_get_examples_ode_sol_nth_algebraic) + + +@slow +def test_slow_examples_nth_linear_constant_coeff_var_of_parameters(): + _ode_solver_test(_get_examples_ode_sol_nth_linear_var_of_parameters, run_slow_test=True) + + +def test_nth_linear_constant_coeff_var_of_parameters(): + _ode_solver_test(_get_examples_ode_sol_nth_linear_var_of_parameters) + + +@slow +def test_nth_linear_constant_coeff_variation_of_parameters__integral(): + # solve_variation_of_parameters shouldn't attempt to simplify the + # Wronskian if simplify=False. If wronskian() ever gets good enough + # to simplify the result itself, this test might fail. + our_hint = 'nth_linear_constant_coeff_variation_of_parameters_Integral' + eq = f(x).diff(x, 5) + 2*f(x).diff(x, 3) + f(x).diff(x) - 2*x - exp(I*x) + sol_simp = dsolve(eq, f(x), hint=our_hint, simplify=True) + sol_nsimp = dsolve(eq, f(x), hint=our_hint, simplify=False) + assert sol_simp != sol_nsimp + assert checkodesol(eq, sol_simp, order=5, solve_for_func=False) == (True, 0) + assert checkodesol(eq, sol_simp, order=5, solve_for_func=False) == (True, 0) + + +@slow +def test_slow_examples_1st_exact(): + _ode_solver_test(_get_examples_ode_sol_1st_exact, run_slow_test=True) + + +@slow +def test_1st_exact(): + _ode_solver_test(_get_examples_ode_sol_1st_exact) + + +def test_1st_exact_integral(): + eq = cos(f(x)) - (x*sin(f(x)) - f(x)**2)*f(x).diff(x) + sol_1 = dsolve(eq, f(x), simplify=False, hint='1st_exact_Integral') + assert checkodesol(eq, sol_1, order=1, solve_for_func=False) + + +@slow +def test_slow_examples_nth_order_reducible(): + _ode_solver_test(_get_examples_ode_sol_nth_order_reducible, run_slow_test=True) + + +@slow +def test_slow_examples_nth_linear_constant_coeff_undetermined_coefficients(): + _ode_solver_test(_get_examples_ode_sol_nth_linear_undetermined_coefficients, run_slow_test=True) + + +@slow +def test_slow_examples_separable(): + _ode_solver_test(_get_examples_ode_sol_separable, run_slow_test=True) + + +@slow +def test_nth_linear_constant_coeff_undetermined_coefficients(): + #issue-https://github.com/sympy/sympy/issues/5787 + # This test case is to show the classification of imaginary constants under + # nth_linear_constant_coeff_undetermined_coefficients + eq = Eq(diff(f(x), x), I*f(x) + S.Half - I) + our_hint = 'nth_linear_constant_coeff_undetermined_coefficients' + assert our_hint in classify_ode(eq) + _ode_solver_test(_get_examples_ode_sol_nth_linear_undetermined_coefficients) + + +def test_nth_order_reducible(): + F = lambda eq: NthOrderReducible(SingleODEProblem(eq, f(x), x))._matches() + D = Derivative + assert F(D(y*f(x), x, y) + D(f(x), x)) == False + assert F(D(y*f(y), y, y) + D(f(y), y)) == False + assert F(f(x)*D(f(x), x) + D(f(x), x, 2))== False + assert F(D(x*f(y), y, 2) + D(u*y*f(x), x, 3)) == False # no simplification by design + assert F(D(f(y), y, 2) + D(f(y), y, 3) + D(f(x), x, 4)) == False + assert F(D(f(x), x, 2) + D(f(x), x, 3)) == True + _ode_solver_test(_get_examples_ode_sol_nth_order_reducible) + + +@slow +def test_separable(): + _ode_solver_test(_get_examples_ode_sol_separable) + + +@slow +def test_factorable(): + assert integrate(-asin(f(2*x)+pi), x) == -Integral(asin(pi + f(2*x)), x) + _ode_solver_test(_get_examples_ode_sol_factorable) + + +@slow +def test_slow_examples_factorable(): + _ode_solver_test(_get_examples_ode_sol_factorable, run_slow_test=True) + + +def test_Riccati_special_minus2(): + _ode_solver_test(_get_examples_ode_sol_riccati) + + +@slow +def test_1st_rational_riccati(): + _ode_solver_test(_get_examples_ode_sol_1st_rational_riccati) + + +def test_Bernoulli(): + _ode_solver_test(_get_examples_ode_sol_bernoulli) + + +def test_1st_linear(): + _ode_solver_test(_get_examples_ode_sol_1st_linear) + + +def test_almost_linear(): + _ode_solver_test(_get_examples_ode_sol_almost_linear) + + +@slow +def test_Liouville_ODE(): + hint = 'Liouville' + not_Liouville1 = classify_ode(diff(f(x), x)/x + f(x)*diff(f(x), x, x)/2 - + diff(f(x), x)**2/2, f(x)) + not_Liouville2 = classify_ode(diff(f(x), x)/x + diff(f(x), x, x)/2 - + x*diff(f(x), x)**2/2, f(x)) + assert hint not in not_Liouville1 + assert hint not in not_Liouville2 + assert hint + '_Integral' not in not_Liouville1 + assert hint + '_Integral' not in not_Liouville2 + + _ode_solver_test(_get_examples_ode_sol_liouville) + + +def test_nth_order_linear_euler_eq_homogeneous(): + x, t, a, b, c = symbols('x t a b c') + y = Function('y') + our_hint = "nth_linear_euler_eq_homogeneous" + + eq = diff(f(t), t, 4)*t**4 - 13*diff(f(t), t, 2)*t**2 + 36*f(t) + assert our_hint in classify_ode(eq) + + eq = a*y(t) + b*t*diff(y(t), t) + c*t**2*diff(y(t), t, 2) + assert our_hint in classify_ode(eq) + + _ode_solver_test(_get_examples_ode_sol_euler_homogeneous) + + +def test_nth_order_linear_euler_eq_nonhomogeneous_undetermined_coefficients(): + x, t = symbols('x t') + a, b, c, d = symbols('a b c d', integer=True) + our_hint = "nth_linear_euler_eq_nonhomogeneous_undetermined_coefficients" + + eq = x**4*diff(f(x), x, 4) - 13*x**2*diff(f(x), x, 2) + 36*f(x) + x + assert our_hint in classify_ode(eq, f(x)) + + eq = a*x**2*diff(f(x), x, 2) + b*x*diff(f(x), x) + c*f(x) + d*log(x) + assert our_hint in classify_ode(eq, f(x)) + + _ode_solver_test(_get_examples_ode_sol_euler_undetermined_coeff) + + +@slow +def test_nth_order_linear_euler_eq_nonhomogeneous_variation_of_parameters(): + x, t = symbols('x, t') + a, b, c, d = symbols('a, b, c, d', integer=True) + our_hint = "nth_linear_euler_eq_nonhomogeneous_variation_of_parameters" + + eq = Eq(x**2*diff(f(x),x,2) - 8*x*diff(f(x),x) + 12*f(x), x**2) + assert our_hint in classify_ode(eq, f(x)) + + eq = Eq(a*x**3*diff(f(x),x,3) + b*x**2*diff(f(x),x,2) + c*x*diff(f(x),x) + d*f(x), x*log(x)) + assert our_hint in classify_ode(eq, f(x)) + + _ode_solver_test(_get_examples_ode_sol_euler_var_para) + + +@_add_example_keys +def _get_examples_ode_sol_euler_homogeneous(): + r1, r2, r3, r4, r5 = [rootof(x**5 - 14*x**4 + 71*x**3 - 154*x**2 + 120*x - 1, n) for n in range(5)] + return { + 'hint': "nth_linear_euler_eq_homogeneous", + 'func': f(x), + 'examples':{ + 'euler_hom_01': { + 'eq': Eq(-3*diff(f(x), x)*x + 2*x**2*diff(f(x), x, x), 0), + 'sol': [Eq(f(x), C1 + C2*x**Rational(5, 2))], + }, + + 'euler_hom_02': { + 'eq': Eq(3*f(x) - 5*diff(f(x), x)*x + 2*x**2*diff(f(x), x, x), 0), + 'sol': [Eq(f(x), C1*sqrt(x) + C2*x**3)] + }, + + 'euler_hom_03': { + 'eq': Eq(4*f(x) + 5*diff(f(x), x)*x + x**2*diff(f(x), x, x), 0), + 'sol': [Eq(f(x), (C1 + C2*log(x))/x**2)] + }, + + 'euler_hom_04': { + 'eq': Eq(6*f(x) - 6*diff(f(x), x)*x + 1*x**2*diff(f(x), x, x) + x**3*diff(f(x), x, x, x), 0), + 'sol': [Eq(f(x), C1/x**2 + C2*x + C3*x**3)] + }, + + 'euler_hom_05': { + 'eq': Eq(-125*f(x) + 61*diff(f(x), x)*x - 12*x**2*diff(f(x), x, x) + x**3*diff(f(x), x, x, x), 0), + 'sol': [Eq(f(x), x**5*(C1 + C2*log(x) + C3*log(x)**2))] + }, + + 'euler_hom_06': { + 'eq': x**2*diff(f(x), x, 2) + x*diff(f(x), x) - 9*f(x), + 'sol': [Eq(f(x), C1*x**-3 + C2*x**3)] + }, + + 'euler_hom_07': { + 'eq': sin(x)*x**2*f(x).diff(x, 2) + sin(x)*x*f(x).diff(x) + sin(x)*f(x), + 'sol': [Eq(f(x), C1*sin(log(x)) + C2*cos(log(x)))], + 'XFAIL': ['2nd_power_series_regular','nth_linear_euler_eq_nonhomogeneous_undetermined_coefficients'] + }, + + 'euler_hom_08': { + 'eq': x**6 * f(x).diff(x, 6) - x*f(x).diff(x) + f(x), + 'sol': [Eq(f(x), C1*x + C2*x**r1 + C3*x**r2 + C4*x**r3 + C5*x**r4 + C6*x**r5)], + 'checkodesol_XFAIL':True + }, + + #This example is from issue: https://github.com/sympy/sympy/issues/15237 #This example is from issue: + # https://github.com/sympy/sympy/issues/15237 + 'euler_hom_09': { + 'eq': Derivative(x*f(x), x, x, x), + 'sol': [Eq(f(x), C1 + C2/x + C3*x)], + }, + } + } + + +@_add_example_keys +def _get_examples_ode_sol_euler_undetermined_coeff(): + return { + 'hint': "nth_linear_euler_eq_nonhomogeneous_undetermined_coefficients", + 'func': f(x), + 'examples':{ + 'euler_undet_01': { + 'eq': Eq(x**2*diff(f(x), x, x) + x*diff(f(x), x), 1), + 'sol': [Eq(f(x), C1 + C2*log(x) + log(x)**2/2)] + }, + + 'euler_undet_02': { + 'eq': Eq(x**2*diff(f(x), x, x) - 2*x*diff(f(x), x) + 2*f(x), x**3), + 'sol': [Eq(f(x), x*(C1 + C2*x + Rational(1, 2)*x**2))] + }, + + 'euler_undet_03': { + 'eq': Eq(x**2*diff(f(x), x, x) - x*diff(f(x), x) - 3*f(x), log(x)/x), + 'sol': [Eq(f(x), (C1 + C2*x**4 - log(x)**2/8 - log(x)/16)/x)] + }, + + 'euler_undet_04': { + 'eq': Eq(x**2*diff(f(x), x, x) + 3*x*diff(f(x), x) - 8*f(x), log(x)**3 - log(x)), + 'sol': [Eq(f(x), C1/x**4 + C2*x**2 - Rational(1,8)*log(x)**3 - Rational(3,32)*log(x)**2 - Rational(1,64)*log(x) - Rational(7, 256))] + }, + + 'euler_undet_05': { + 'eq': Eq(x**3*diff(f(x), x, x, x) - 3*x**2*diff(f(x), x, x) + 6*x*diff(f(x), x) - 6*f(x), log(x)), + 'sol': [Eq(f(x), C1*x + C2*x**2 + C3*x**3 - Rational(1, 6)*log(x) - Rational(11, 36))] + }, + + #Below examples were added for the issue: https://github.com/sympy/sympy/issues/5096 + 'euler_undet_06': { + 'eq': 2*x**2*f(x).diff(x, 2) + f(x) + sqrt(2*x)*sin(log(2*x)/2), + 'sol': [Eq(f(x), sqrt(x)*(C1*sin(log(x)/2) + C2*cos(log(x)/2) + sqrt(2)*log(x)*cos(log(2*x)/2)/2))] + }, + + 'euler_undet_07': { + 'eq': 2*x**2*f(x).diff(x, 2) + f(x) + sin(log(2*x)/2), + 'sol': [Eq(f(x), C1*sqrt(x)*sin(log(x)/2) + C2*sqrt(x)*cos(log(x)/2) - 2*sin(log(2*x)/2)/5 - 4*cos(log(2*x)/2)/5)] + }, + } + } + + +@_add_example_keys +def _get_examples_ode_sol_euler_var_para(): + return { + 'hint': "nth_linear_euler_eq_nonhomogeneous_variation_of_parameters", + 'func': f(x), + 'examples':{ + 'euler_var_01': { + 'eq': Eq(x**2*Derivative(f(x), x, x) - 2*x*Derivative(f(x), x) + 2*f(x), x**4), + 'sol': [Eq(f(x), x*(C1 + C2*x + x**3/6))] + }, + + 'euler_var_02': { + 'eq': Eq(3*x**2*diff(f(x), x, x) + 6*x*diff(f(x), x) - 6*f(x), x**3*exp(x)), + 'sol': [Eq(f(x), C1/x**2 + C2*x + x*exp(x)/3 - 4*exp(x)/3 + 8*exp(x)/(3*x) - 8*exp(x)/(3*x**2))] + }, + + 'euler_var_03': { + 'eq': Eq(x**2*Derivative(f(x), x, x) - 2*x*Derivative(f(x), x) + 2*f(x), x**4*exp(x)), + 'sol': [Eq(f(x), x*(C1 + C2*x + x*exp(x) - 2*exp(x)))] + }, + + 'euler_var_04': { + 'eq': x**2*Derivative(f(x), x, x) - 2*x*Derivative(f(x), x) + 2*f(x) - log(x), + 'sol': [Eq(f(x), C1*x + C2*x**2 + log(x)/2 + Rational(3, 4))] + }, + + 'euler_var_05': { + 'eq': -exp(x) + (x*Derivative(f(x), (x, 2)) + Derivative(f(x), x))/x, + 'sol': [Eq(f(x), C1 + C2*log(x) + exp(x) - Ei(x))] + }, + + 'euler_var_06': { + 'eq': x**2 * f(x).diff(x, 2) + x * f(x).diff(x) + 4 * f(x) - 1/x, + 'sol': [Eq(f(x), C1*sin(2*log(x)) + C2*cos(2*log(x)) + 1/(5*x))] + }, + } + } + + +@_add_example_keys +def _get_examples_ode_sol_bernoulli(): + # Type: Bernoulli, f'(x) + p(x)*f(x) == q(x)*f(x)**n + return { + 'hint': "Bernoulli", + 'func': f(x), + 'examples':{ + 'bernoulli_01': { + 'eq': Eq(x*f(x).diff(x) + f(x) - f(x)**2, 0), + 'sol': [Eq(f(x), 1/(C1*x + 1))], + 'XFAIL': ['separable_reduced'] + }, + + 'bernoulli_02': { + 'eq': f(x).diff(x) - y*f(x), + 'sol': [Eq(f(x), C1*exp(x*y))] + }, + + 'bernoulli_03': { + 'eq': f(x)*f(x).diff(x) - 1, + 'sol': [Eq(f(x), -sqrt(C1 + 2*x)), Eq(f(x), sqrt(C1 + 2*x))] + }, + } + } + + +@_add_example_keys +def _get_examples_ode_sol_riccati(): + # Type: Riccati special alpha = -2, a*dy/dx + b*y**2 + c*y/x +d/x**2 + return { + 'hint': "Riccati_special_minus2", + 'func': f(x), + 'examples':{ + 'riccati_01': { + 'eq': 2*f(x).diff(x) + f(x)**2 - f(x)/x + 3*x**(-2), + 'sol': [Eq(f(x), (-sqrt(3)*tan(C1 + sqrt(3)*log(x)/4) + 3)/(2*x))], + }, + }, + } + + +@_add_example_keys +def _get_examples_ode_sol_1st_rational_riccati(): + # Type: 1st Order Rational Riccati, dy/dx = a + b*y + c*y**2, + # a, b, c are rational functions of x + return { + 'hint': "1st_rational_riccati", + 'func': f(x), + 'examples':{ + # a(x) is a constant + "rational_riccati_01": { + "eq": Eq(f(x).diff(x) + f(x)**2 - 2, 0), + "sol": [Eq(f(x), sqrt(2)*(-C1 - exp(2*sqrt(2)*x))/(C1 - exp(2*sqrt(2)*x)))] + }, + # a(x) is a constant + "rational_riccati_02": { + "eq": f(x)**2 + Derivative(f(x), x) + 4*f(x)/x + 2/x**2, + "sol": [Eq(f(x), (-2*C1 - x)/(x*(C1 + x)))] + }, + # a(x) is a constant + "rational_riccati_03": { + "eq": 2*x**2*Derivative(f(x), x) - x*(4*f(x) + Derivative(f(x), x) - 4) + (f(x) - 1)*f(x), + "sol": [Eq(f(x), (C1 + 2*x**2)/(C1 + x))] + }, + # Constant coefficients + "rational_riccati_04": { + "eq": f(x).diff(x) - 6 - 5*f(x) - f(x)**2, + "sol": [Eq(f(x), (-2*C1 + 3*exp(x))/(C1 - exp(x)))] + }, + # One pole of multiplicity 2 + "rational_riccati_05": { + "eq": x**2 - (2*x + 1/x)*f(x) + f(x)**2 + Derivative(f(x), x), + "sol": [Eq(f(x), x*(C1 + x**2 + 1)/(C1 + x**2 - 1))] + }, + # One pole of multiplicity 2 + "rational_riccati_06": { + "eq": x**4*Derivative(f(x), x) + x**2 - x*(2*f(x)**2 + Derivative(f(x), x)) + f(x), + "sol": [Eq(f(x), x*(C1*x - x + 1)/(C1 + x**2 - 1))] + }, + # Multiple poles of multiplicity 2 + "rational_riccati_07": { + "eq": -f(x)**2 + Derivative(f(x), x) + (15*x**2 - 20*x + 7)/((x - 1)**2*(2*x \ + - 1)**2), + "sol": [Eq(f(x), (9*C1*x - 6*C1 - 15*x**5 + 60*x**4 - 94*x**3 + 72*x**2 - \ + 33*x + 8)/(6*C1*x**2 - 9*C1*x + 3*C1 + 6*x**6 - 29*x**5 + 57*x**4 - \ + 58*x**3 + 28*x**2 - 3*x - 1))] + }, + # Imaginary poles + "rational_riccati_08": { + "eq": Derivative(f(x), x) + (3*x**2 + 1)*f(x)**2/x + (6*x**2 - x + 3)*f(x)/(x*(x \ + - 1)) + (3*x**2 - 2*x + 2)/(x*(x - 1)**2), + "sol": [Eq(f(x), (-C1 - x**3 + x**2 - 2*x + 1)/(C1*x - C1 + x**4 - x**3 + x**2 - \ + 2*x + 1))], + }, + # Imaginary coefficients in equation + "rational_riccati_09": { + "eq": Derivative(f(x), x) - 2*I*(f(x)**2 + 1)/x, + "sol": [Eq(f(x), (-I*C1 + I*x**4 + I)/(C1 + x**4 - 1))] + }, + # Regression: linsolve returning empty solution + # Large value of m (> 10) + "rational_riccati_10": { + "eq": Eq(Derivative(f(x), x), x*f(x)/(S(3)/2 - 2*x) + (x/2 - S(1)/3)*f(x)**2/\ + (2*x/3 - S(1)/2) - S(5)/4 + (281*x**2 - 1260*x + 756)/(16*x**3 - 12*x**2)), + "sol": [Eq(f(x), (40*C1*x**14 + 28*C1*x**13 + 420*C1*x**12 + 2940*C1*x**11 + \ + 18480*C1*x**10 + 103950*C1*x**9 + 519750*C1*x**8 + 2286900*C1*x**7 + \ + 8731800*C1*x**6 + 28378350*C1*x**5 + 76403250*C1*x**4 + 163721250*C1*x**3 \ + + 261954000*C1*x**2 + 278326125*C1*x + 147349125*C1 + x*exp(2*x) - 9*exp(2*x) \ + )/(x*(24*C1*x**13 + 140*C1*x**12 + 840*C1*x**11 + 4620*C1*x**10 + 23100*C1*x**9 \ + + 103950*C1*x**8 + 415800*C1*x**7 + 1455300*C1*x**6 + 4365900*C1*x**5 + \ + 10914750*C1*x**4 + 21829500*C1*x**3 + 32744250*C1*x**2 + 32744250*C1*x + \ + 16372125*C1 - exp(2*x))))] + } + } + } + + + +@_add_example_keys +def _get_examples_ode_sol_1st_linear(): + # Type: first order linear form f'(x)+p(x)f(x)=q(x) + return { + 'hint': "1st_linear", + 'func': f(x), + 'examples':{ + 'linear_01': { + 'eq': Eq(f(x).diff(x) + x*f(x), x**2), + 'sol': [Eq(f(x), (C1 + x*exp(x**2/2)- sqrt(2)*sqrt(pi)*erfi(sqrt(2)*x/2)/2)*exp(-x**2/2))], + }, + }, + } + + +@_add_example_keys +def _get_examples_ode_sol_factorable(): + """ some hints are marked as xfail for examples because they missed additional algebraic solution + which could be found by Factorable hint. Fact_01 raise exception for + nth_linear_constant_coeff_undetermined_coefficients""" + + y = Dummy('y') + a0,a1,a2,a3,a4 = symbols('a0, a1, a2, a3, a4') + return { + 'hint': "factorable", + 'func': f(x), + 'examples':{ + 'fact_01': { + 'eq': f(x) + f(x)*f(x).diff(x), + 'sol': [Eq(f(x), 0), Eq(f(x), C1 - x)], + 'XFAIL': ['separable', '1st_exact', '1st_linear', 'Bernoulli', '1st_homogeneous_coeff_best', + '1st_homogeneous_coeff_subs_indep_div_dep', '1st_homogeneous_coeff_subs_dep_div_indep', + 'lie_group', 'nth_linear_euler_eq_nonhomogeneous_undetermined_coefficients', + 'nth_linear_constant_coeff_variation_of_parameters', + 'nth_linear_euler_eq_nonhomogeneous_variation_of_parameters', + 'nth_linear_constant_coeff_undetermined_coefficients'] + }, + + 'fact_02': { + 'eq': f(x)*(f(x).diff(x)+f(x)*x+2), + 'sol': [Eq(f(x), (C1 - sqrt(2)*sqrt(pi)*erfi(sqrt(2)*x/2))*exp(-x**2/2)), Eq(f(x), 0)], + 'XFAIL': ['Bernoulli', '1st_linear', 'lie_group'] + }, + + 'fact_03': { + 'eq': (f(x).diff(x)+f(x)*x**2)*(f(x).diff(x, 2) + x*f(x)), + 'sol': [Eq(f(x), C1*airyai(-x) + C2*airybi(-x)),Eq(f(x), C1*exp(-x**3/3))] + }, + + 'fact_04': { + 'eq': (f(x).diff(x)+f(x)*x**2)*(f(x).diff(x, 2) + f(x)), + 'sol': [Eq(f(x), C1*exp(-x**3/3)), Eq(f(x), C1*sin(x) + C2*cos(x))] + }, + + 'fact_05': { + 'eq': (f(x).diff(x)**2-1)*(f(x).diff(x)**2-4), + 'sol': [Eq(f(x), C1 - x), Eq(f(x), C1 + x), Eq(f(x), C1 + 2*x), Eq(f(x), C1 - 2*x)] + }, + + 'fact_06': { + 'eq': (f(x).diff(x, 2)-exp(f(x)))*f(x).diff(x), + 'sol': [ + Eq(f(x), log(-C1/(cos(sqrt(-C1)*(C2 + x)) + 1))), + Eq(f(x), log(-C1/(cos(sqrt(-C1)*(C2 - x)) + 1))), + Eq(f(x), C1) + ], + 'slow': True, + }, + + 'fact_07': { + 'eq': (f(x).diff(x)**2-1)*(f(x)*f(x).diff(x)-1), + 'sol': [Eq(f(x), C1 - x), Eq(f(x), -sqrt(C1 + 2*x)),Eq(f(x), sqrt(C1 + 2*x)), Eq(f(x), C1 + x)] + }, + + 'fact_08': { + 'eq': Derivative(f(x), x)**4 - 2*Derivative(f(x), x)**2 + 1, + 'sol': [Eq(f(x), C1 - x), Eq(f(x), C1 + x)] + }, + + 'fact_09': { + 'eq': f(x)**2*Derivative(f(x), x)**6 - 2*f(x)**2*Derivative(f(x), + x)**4 + f(x)**2*Derivative(f(x), x)**2 - 2*f(x)*Derivative(f(x), + x)**5 + 4*f(x)*Derivative(f(x), x)**3 - 2*f(x)*Derivative(f(x), + x) + Derivative(f(x), x)**4 - 2*Derivative(f(x), x)**2 + 1, + 'sol': [ + Eq(f(x), C1 - x), Eq(f(x), -sqrt(C1 + 2*x)), + Eq(f(x), sqrt(C1 + 2*x)), Eq(f(x), C1 + x) + ] + }, + + 'fact_10': { + 'eq': x**4*f(x)**2 + 2*x**4*f(x)*Derivative(f(x), (x, 2)) + x**4*Derivative(f(x), + (x, 2))**2 + 2*x**3*f(x)*Derivative(f(x), x) + 2*x**3*Derivative(f(x), + x)*Derivative(f(x), (x, 2)) - 7*x**2*f(x)**2 - 7*x**2*f(x)*Derivative(f(x), + (x, 2)) + x**2*Derivative(f(x), x)**2 - 7*x*f(x)*Derivative(f(x), x) + 12*f(x)**2, + 'sol': [ + Eq(f(x), C1*besselj(2, x) + C2*bessely(2, x)), + Eq(f(x), C1*besselj(sqrt(3), x) + C2*bessely(sqrt(3), x)) + ], + 'slow': True, + }, + + 'fact_11': { + 'eq': (f(x).diff(x, 2)-exp(f(x)))*(f(x).diff(x, 2)+exp(f(x))), + 'sol': [ + Eq(f(x), log(C1/(cos(C1*sqrt(-1/C1)*(C2 + x)) - 1))), + Eq(f(x), log(C1/(cos(C1*sqrt(-1/C1)*(C2 - x)) - 1))), + Eq(f(x), log(C1/(1 - cos(C1*sqrt(-1/C1)*(C2 + x))))), + Eq(f(x), log(C1/(1 - cos(C1*sqrt(-1/C1)*(C2 - x))))) + ], + 'dsolve_too_slow': True, + }, + + #Below examples were added for the issue: https://github.com/sympy/sympy/issues/15889 + 'fact_12': { + 'eq': exp(f(x).diff(x))-f(x)**2, + 'sol': [Eq(NonElementaryIntegral(1/log(y**2), (y, f(x))), C1 + x)], + 'XFAIL': ['lie_group'] #It shows not implemented error for lie_group. + }, + + 'fact_13': { + 'eq': f(x).diff(x)**2 - f(x)**3, + 'sol': [Eq(f(x), 4/(C1**2 - 2*C1*x + x**2))], + 'XFAIL': ['lie_group'] #It shows not implemented error for lie_group. + }, + + 'fact_14': { + 'eq': f(x).diff(x)**2 - f(x), + 'sol': [Eq(f(x), C1**2/4 - C1*x/2 + x**2/4)] + }, + + 'fact_15': { + 'eq': f(x).diff(x)**2 - f(x)**2, + 'sol': [Eq(f(x), C1*exp(x)), Eq(f(x), C1*exp(-x))] + }, + + 'fact_16': { + 'eq': f(x).diff(x)**2 - f(x)**3, + 'sol': [Eq(f(x), 4/(C1**2 - 2*C1*x + x**2))], + }, + + # kamke ode 1.1 + 'fact_17': { + 'eq': f(x).diff(x)-(a4*x**4 + a3*x**3 + a2*x**2 + a1*x + a0)**(-1/2), + 'sol': [Eq(f(x), C1 + Integral(1/sqrt(a0 + a1*x + a2*x**2 + a3*x**3 + a4*x**4), x))], + 'slow': True + }, + + # This is from issue: https://github.com/sympy/sympy/issues/9446 + 'fact_18':{ + 'eq': Eq(f(2 * x), sin(Derivative(f(x)))), + 'sol': [Eq(f(x), C1 + Integral(pi - asin(f(2*x)), x)), Eq(f(x), C1 + Integral(asin(f(2*x)), x))], + 'checkodesol_XFAIL':True + }, + + # This is from issue: https://github.com/sympy/sympy/issues/7093 + 'fact_19': { + 'eq': Derivative(f(x), x)**2 - x**3, + 'sol': [Eq(f(x), C1 - 2*x**Rational(5,2)/5), Eq(f(x), C1 + 2*x**Rational(5,2)/5)], + }, + + 'fact_20': { + 'eq': x*f(x).diff(x, 2) - x*f(x), + 'sol': [Eq(f(x), C1*exp(-x) + C2*exp(x))], + }, + } + } + + + +@_add_example_keys +def _get_examples_ode_sol_almost_linear(): + from sympy.functions.special.error_functions import Ei + A = Symbol('A', positive=True) + f = Function('f') + d = f(x).diff(x) + + return { + 'hint': "almost_linear", + 'func': f(x), + 'examples':{ + 'almost_lin_01': { + 'eq': x**2*f(x)**2*d + f(x)**3 + 1, + 'sol': [Eq(f(x), (C1*exp(3/x) - 1)**Rational(1, 3)), + Eq(f(x), (-1 - sqrt(3)*I)*(C1*exp(3/x) - 1)**Rational(1, 3)/2), + Eq(f(x), (-1 + sqrt(3)*I)*(C1*exp(3/x) - 1)**Rational(1, 3)/2)], + + }, + + 'almost_lin_02': { + 'eq': x*f(x)*d + 2*x*f(x)**2 + 1, + 'sol': [Eq(f(x), -sqrt((C1 - 2*Ei(4*x))*exp(-4*x))), Eq(f(x), sqrt((C1 - 2*Ei(4*x))*exp(-4*x)))] + }, + + 'almost_lin_03': { + 'eq': x*d + x*f(x) + 1, + 'sol': [Eq(f(x), (C1 - Ei(x))*exp(-x))] + }, + + 'almost_lin_04': { + 'eq': x*exp(f(x))*d + exp(f(x)) + 3*x, + 'sol': [Eq(f(x), log(C1/x - x*Rational(3, 2)))], + }, + + 'almost_lin_05': { + 'eq': x + A*(x + diff(f(x), x) + f(x)) + diff(f(x), x) + f(x) + 2, + 'sol': [Eq(f(x), (C1 + Piecewise( + (x, Eq(A + 1, 0)), ((-A*x + A - x - 1)*exp(x)/(A + 1), True)))*exp(-x))], + }, + } + } + + +@_add_example_keys +def _get_examples_ode_sol_liouville(): + n = Symbol('n') + _y = Dummy('y') + return { + 'hint': "Liouville", + 'func': f(x), + 'examples':{ + 'liouville_01': { + 'eq': diff(f(x), x)/x + diff(f(x), x, x)/2 - diff(f(x), x)**2/2, + 'sol': [Eq(f(x), log(x/(C1 + C2*x)))], + + }, + + 'liouville_02': { + 'eq': diff(x*exp(-f(x)), x, x), + 'sol': [Eq(f(x), log(x/(C1 + C2*x)))] + }, + + 'liouville_03': { + 'eq': ((diff(f(x), x)/x + diff(f(x), x, x)/2 - diff(f(x), x)**2/2)*exp(-f(x))/exp(f(x))).expand(), + 'sol': [Eq(f(x), log(x/(C1 + C2*x)))] + }, + + 'liouville_04': { + 'eq': diff(f(x), x, x) + 1/f(x)*(diff(f(x), x))**2 + 1/x*diff(f(x), x), + 'sol': [Eq(f(x), -sqrt(C1 + C2*log(x))), Eq(f(x), sqrt(C1 + C2*log(x)))], + }, + + 'liouville_05': { + 'eq': x*diff(f(x), x, x) + x/f(x)*diff(f(x), x)**2 + x*diff(f(x), x), + 'sol': [Eq(f(x), -sqrt(C1 + C2*exp(-x))), Eq(f(x), sqrt(C1 + C2*exp(-x)))], + }, + + 'liouville_06': { + 'eq': Eq((x*exp(f(x))).diff(x, x), 0), + 'sol': [Eq(f(x), log(C1 + C2/x))], + }, + + 'liouville_07': { + 'eq': (diff(f(x), x)/x + diff(f(x), x, x)/2 - diff(f(x), x)**2/2)*exp(-f(x))/exp(f(x)), + 'sol': [Eq(f(x), log(x/(C1 + C2*x)))], + }, + + 'liouville_08': { + 'eq': x**2*diff(f(x),x) + (n*f(x) + f(x)**2)*diff(f(x),x)**2 + diff(f(x), (x, 2)), + 'sol': [Eq(C1 + C2*lowergamma(Rational(1,3), x**3/3) + NonElementaryIntegral(exp(_y**3/3)*exp(_y**2*n/2), (_y, f(x))), 0)], + }, + } + } + + +@_add_example_keys +def _get_examples_ode_sol_nth_algebraic(): + M, m, r, t = symbols('M m r t') + phi = Function('phi') + k = Symbol('k') + # This one needs a substitution f' = g. + # 'algeb_12': { + # 'eq': -exp(x) + (x*Derivative(f(x), (x, 2)) + Derivative(f(x), x))/x, + # 'sol': [Eq(f(x), C1 + C2*log(x) + exp(x) - Ei(x))], + # }, + return { + 'hint': "nth_algebraic", + 'func': f(x), + 'examples':{ + 'algeb_01': { + 'eq': f(x) * f(x).diff(x) * f(x).diff(x, x) * (f(x) - 1) * (f(x).diff(x) - x), + 'sol': [Eq(f(x), C1 + x**2/2), Eq(f(x), C1 + C2*x)] + }, + + 'algeb_02': { + 'eq': f(x) * f(x).diff(x) * f(x).diff(x, x) * (f(x) - 1), + 'sol': [Eq(f(x), C1 + C2*x)] + }, + + 'algeb_03': { + 'eq': f(x) * f(x).diff(x) * f(x).diff(x, x), + 'sol': [Eq(f(x), C1 + C2*x)] + }, + + 'algeb_04': { + 'eq': Eq(-M * phi(t).diff(t), + Rational(3, 2) * m * r**2 * phi(t).diff(t) * phi(t).diff(t,t)), + 'sol': [Eq(phi(t), C1), Eq(phi(t), C1 + C2*t - M*t**2/(3*m*r**2))], + 'func': phi(t) + }, + + 'algeb_05': { + 'eq': (1 - sin(f(x))) * f(x).diff(x), + 'sol': [Eq(f(x), C1)], + 'XFAIL': ['separable'] #It raised exception. + }, + + 'algeb_06': { + 'eq': (diff(f(x)) - x)*(diff(f(x)) + x), + 'sol': [Eq(f(x), C1 - x**2/2), Eq(f(x), C1 + x**2/2)] + }, + + 'algeb_07': { + 'eq': Eq(Derivative(f(x), x), Derivative(g(x), x)), + 'sol': [Eq(f(x), C1 + g(x))], + }, + + 'algeb_08': { + 'eq': f(x).diff(x) - C1, #this example is from issue 15999 + 'sol': [Eq(f(x), C1*x + C2)], + }, + + 'algeb_09': { + 'eq': f(x)*f(x).diff(x), + 'sol': [Eq(f(x), C1)], + }, + + 'algeb_10': { + 'eq': (diff(f(x)) - x)*(diff(f(x)) + x), + 'sol': [Eq(f(x), C1 - x**2/2), Eq(f(x), C1 + x**2/2)], + }, + + 'algeb_11': { + 'eq': f(x) + f(x)*f(x).diff(x), + 'sol': [Eq(f(x), 0), Eq(f(x), C1 - x)], + 'XFAIL': ['separable', '1st_exact', '1st_linear', 'Bernoulli', '1st_homogeneous_coeff_best', + '1st_homogeneous_coeff_subs_indep_div_dep', '1st_homogeneous_coeff_subs_dep_div_indep', + 'lie_group', 'nth_linear_constant_coeff_undetermined_coefficients', + 'nth_linear_euler_eq_nonhomogeneous_undetermined_coefficients', + 'nth_linear_constant_coeff_variation_of_parameters', + 'nth_linear_euler_eq_nonhomogeneous_variation_of_parameters'] + #nth_linear_constant_coeff_undetermined_coefficients raises exception rest all of them misses a solution. + }, + + 'algeb_12': { + 'eq': Derivative(x*f(x), x, x, x), + 'sol': [Eq(f(x), (C1 + C2*x + C3*x**2) / x)], + 'XFAIL': ['nth_algebraic'] # It passes only when prep=False is set in dsolve. + }, + + 'algeb_13': { + 'eq': Eq(Derivative(x*Derivative(f(x), x), x)/x, exp(x)), + 'sol': [Eq(f(x), C1 + C2*log(x) + exp(x) - Ei(x))], + 'XFAIL': ['nth_algebraic'] # It passes only when prep=False is set in dsolve. + }, + + # These are simple tests from the old ode module example 14-18 + 'algeb_14': { + 'eq': Eq(f(x).diff(x), 0), + 'sol': [Eq(f(x), C1)], + }, + + 'algeb_15': { + 'eq': Eq(3*f(x).diff(x) - 5, 0), + 'sol': [Eq(f(x), C1 + x*Rational(5, 3))], + }, + + 'algeb_16': { + 'eq': Eq(3*f(x).diff(x), 5), + 'sol': [Eq(f(x), C1 + x*Rational(5, 3))], + }, + + # Type: 2nd order, constant coefficients (two complex roots) + 'algeb_17': { + 'eq': Eq(3*f(x).diff(x) - 1, 0), + 'sol': [Eq(f(x), C1 + x/3)], + }, + + 'algeb_18': { + 'eq': Eq(x*f(x).diff(x) - 1, 0), + 'sol': [Eq(f(x), C1 + log(x))], + }, + + # https://github.com/sympy/sympy/issues/6989 + 'algeb_19': { + 'eq': f(x).diff(x) - x*exp(-k*x), + 'sol': [Eq(f(x), C1 + Piecewise(((-k*x - 1)*exp(-k*x)/k**2, Ne(k**2, 0)),(x**2/2, True)))], + }, + + 'algeb_20': { + 'eq': -f(x).diff(x) + x*exp(-k*x), + 'sol': [Eq(f(x), C1 + Piecewise(((-k*x - 1)*exp(-k*x)/k**2, Ne(k**2, 0)),(x**2/2, True)))], + }, + + # https://github.com/sympy/sympy/issues/10867 + 'algeb_21': { + 'eq': Eq(g(x).diff(x).diff(x), (x-2)**2 + (x-3)**3), + 'sol': [Eq(g(x), C1 + C2*x + x**5/20 - 2*x**4/3 + 23*x**3/6 - 23*x**2/2)], + 'func': g(x), + }, + + # https://github.com/sympy/sympy/issues/13691 + 'algeb_22': { + 'eq': f(x).diff(x) - C1*g(x).diff(x), + 'sol': [Eq(f(x), C2 + C1*g(x))], + 'func': f(x), + }, + + # https://github.com/sympy/sympy/issues/4838 + 'algeb_23': { + 'eq': f(x).diff(x) - 3*C1 - 3*x**2, + 'sol': [Eq(f(x), C2 + 3*C1*x + x**3)], + }, + } + } + + +@_add_example_keys +def _get_examples_ode_sol_nth_order_reducible(): + return { + 'hint': "nth_order_reducible", + 'func': f(x), + 'examples':{ + 'reducible_01': { + 'eq': Eq(x*Derivative(f(x), x)**2 + Derivative(f(x), x, 2), 0), + 'sol': [Eq(f(x),C1 - sqrt(-1/C2)*log(-C2*sqrt(-1/C2) + x) + + sqrt(-1/C2)*log(C2*sqrt(-1/C2) + x))], + 'slow': True, + }, + + 'reducible_02': { + 'eq': -exp(x) + (x*Derivative(f(x), (x, 2)) + Derivative(f(x), x))/x, + 'sol': [Eq(f(x), C1 + C2*log(x) + exp(x) - Ei(x))], + 'slow': True, + }, + + 'reducible_03': { + 'eq': Eq(sqrt(2) * f(x).diff(x,x,x) + f(x).diff(x), 0), + 'sol': [Eq(f(x), C1 + C2*sin(2**Rational(3, 4)*x/2) + C3*cos(2**Rational(3, 4)*x/2))], + 'slow': True, + }, + + 'reducible_04': { + 'eq': f(x).diff(x, 2) + 2*f(x).diff(x), + 'sol': [Eq(f(x), C1 + C2*exp(-2*x))], + }, + + 'reducible_05': { + 'eq': f(x).diff(x, 3) + f(x).diff(x, 2) - 6*f(x).diff(x), + 'sol': [Eq(f(x), C1 + C2*exp(-3*x) + C3*exp(2*x))], + 'slow': True, + }, + + 'reducible_06': { + 'eq': f(x).diff(x, 4) - f(x).diff(x, 3) - 4*f(x).diff(x, 2) + \ + 4*f(x).diff(x), + 'sol': [Eq(f(x), C1 + C2*exp(-2*x) + C3*exp(x) + C4*exp(2*x))], + 'slow': True, + }, + + 'reducible_07': { + 'eq': f(x).diff(x, 4) + 3*f(x).diff(x, 3), + 'sol': [Eq(f(x), C1 + C2*x + C3*x**2 + C4*exp(-3*x))], + 'slow': True, + }, + + 'reducible_08': { + 'eq': f(x).diff(x, 4) - 2*f(x).diff(x, 2), + 'sol': [Eq(f(x), C1 + C2*x + C3*exp(-sqrt(2)*x) + C4*exp(sqrt(2)*x))], + 'slow': True, + }, + + 'reducible_09': { + 'eq': f(x).diff(x, 4) + 4*f(x).diff(x, 2), + 'sol': [Eq(f(x), C1 + C2*x + C3*sin(2*x) + C4*cos(2*x))], + 'slow': True, + }, + + 'reducible_10': { + 'eq': f(x).diff(x, 5) + 2*f(x).diff(x, 3) + f(x).diff(x), + 'sol': [Eq(f(x), C1 + C2*x*sin(x) + C2*cos(x) - C3*x*cos(x) + C3*sin(x) + C4*sin(x) + C5*cos(x))], + 'slow': True, + }, + + 'reducible_11': { + 'eq': f(x).diff(x, 2) - f(x).diff(x)**3, + 'sol': [Eq(f(x), C1 - sqrt(2)*sqrt(-1/(C2 + x))*(C2 + x)), + Eq(f(x), C1 + sqrt(2)*sqrt(-1/(C2 + x))*(C2 + x))], + 'slow': True, + }, + + # Needs to be a way to know how to combine derivatives in the expression + 'reducible_12': { + 'eq': Derivative(x*f(x), x, x, x) + Derivative(f(x), x, x, x), + 'sol': [Eq(f(x), C1 + C3/Mul(2, (x**2 + 2*x + 1), evaluate=False) + + x*(C2 + C3/Mul(2, (x**2 + 2*x + 1), evaluate=False)))], # 2-arg Mul! + 'slow': True, + }, + } + } + + + +@_add_example_keys +def _get_examples_ode_sol_nth_linear_undetermined_coefficients(): + # examples 3-27 below are from Ordinary Differential Equations, + # Tenenbaum and Pollard, pg. 231 + g = exp(-x) + f2 = f(x).diff(x, 2) + c = 3*f(x).diff(x, 3) + 5*f2 + f(x).diff(x) - f(x) - x + t = symbols("t") + u = symbols("u",cls=Function) + R, L, C, E_0, alpha = symbols("R L C E_0 alpha",positive=True) + omega = Symbol('omega') + return { + 'hint': "nth_linear_constant_coeff_undetermined_coefficients", + 'func': f(x), + 'examples':{ + 'undet_01': { + 'eq': c - x*g, + 'sol': [Eq(f(x), C3*exp(x/3) - x + (C1 + x*(C2 - x**2/24 - 3*x/32))*exp(-x) - 1)], + 'slow': True, + }, + + 'undet_02': { + 'eq': c - g, + 'sol': [Eq(f(x), C3*exp(x/3) - x + (C1 + x*(C2 - x/8))*exp(-x) - 1)], + 'slow': True, + }, + + 'undet_03': { + 'eq': f2 + 3*f(x).diff(x) + 2*f(x) - 4, + 'sol': [Eq(f(x), C1*exp(-2*x) + C2*exp(-x) + 2)], + 'slow': True, + }, + + 'undet_04': { + 'eq': f2 + 3*f(x).diff(x) + 2*f(x) - 12*exp(x), + 'sol': [Eq(f(x), C1*exp(-2*x) + C2*exp(-x) + 2*exp(x))], + 'slow': True, + }, + + 'undet_05': { + 'eq': f2 + 3*f(x).diff(x) + 2*f(x) - exp(I*x), + 'sol': [Eq(f(x), (S(3)/10 + I/10)*(C1*exp(-2*x) + C2*exp(-x) - I*exp(I*x)))], + 'slow': True, + }, + + 'undet_06': { + 'eq': f2 + 3*f(x).diff(x) + 2*f(x) - sin(x), + 'sol': [Eq(f(x), C1*exp(-2*x) + C2*exp(-x) + sin(x)/10 - 3*cos(x)/10)], + 'slow': True, + }, + + 'undet_07': { + 'eq': f2 + 3*f(x).diff(x) + 2*f(x) - cos(x), + 'sol': [Eq(f(x), C1*exp(-2*x) + C2*exp(-x) + 3*sin(x)/10 + cos(x)/10)], + 'slow': True, + }, + + 'undet_08': { + 'eq': f2 + 3*f(x).diff(x) + 2*f(x) - (8 + 6*exp(x) + 2*sin(x)), + 'sol': [Eq(f(x), C1*exp(-2*x) + C2*exp(-x) + exp(x) + sin(x)/5 - 3*cos(x)/5 + 4)], + 'slow': True, + }, + + 'undet_09': { + 'eq': f2 + f(x).diff(x) + f(x) - x**2, + 'sol': [Eq(f(x), -2*x + x**2 + (C1*sin(x*sqrt(3)/2) + C2*cos(x*sqrt(3)/2))*exp(-x/2))], + 'slow': True, + }, + + 'undet_10': { + 'eq': f2 - 2*f(x).diff(x) - 8*f(x) - 9*x*exp(x) - 10*exp(-x), + 'sol': [Eq(f(x), -x*exp(x) - 2*exp(-x) + C1*exp(-2*x) + C2*exp(4*x))], + 'slow': True, + }, + + 'undet_11': { + 'eq': f2 - 3*f(x).diff(x) - 2*exp(2*x)*sin(x), + 'sol': [Eq(f(x), C1 + C2*exp(3*x) - 3*exp(2*x)*sin(x)/5 - exp(2*x)*cos(x)/5)], + 'slow': True, + }, + + 'undet_12': { + 'eq': f(x).diff(x, 4) - 2*f2 + f(x) - x + sin(x), + 'sol': [Eq(f(x), x - sin(x)/4 + (C1 + C2*x)*exp(-x) + (C3 + C4*x)*exp(x))], + 'slow': True, + }, + + 'undet_13': { + 'eq': f2 + f(x).diff(x) - x**2 - 2*x, + 'sol': [Eq(f(x), C1 + x**3/3 + C2*exp(-x))], + 'slow': True, + }, + + 'undet_14': { + 'eq': f2 + f(x).diff(x) - x - sin(2*x), + 'sol': [Eq(f(x), C1 - x - sin(2*x)/5 - cos(2*x)/10 + x**2/2 + C2*exp(-x))], + 'slow': True, + }, + + 'undet_15': { + 'eq': f2 + f(x) - 4*x*sin(x), + 'sol': [Eq(f(x), (C1 - x**2)*cos(x) + (C2 + x)*sin(x))], + 'slow': True, + }, + + 'undet_16': { + 'eq': f2 + 4*f(x) - x*sin(2*x), + 'sol': [Eq(f(x), (C1 - x**2/8)*cos(2*x) + (C2 + x/16)*sin(2*x))], + 'slow': True, + }, + + 'undet_17': { + 'eq': f2 + 2*f(x).diff(x) + f(x) - x**2*exp(-x), + 'sol': [Eq(f(x), (C1 + x*(C2 + x**3/12))*exp(-x))], + 'slow': True, + }, + + 'undet_18': { + 'eq': f(x).diff(x, 3) + 3*f2 + 3*f(x).diff(x) + f(x) - 2*exp(-x) + \ + x**2*exp(-x), + 'sol': [Eq(f(x), (C1 + x*(C2 + x*(C3 - x**3/60 + x/3)))*exp(-x))], + 'slow': True, + }, + + 'undet_19': { + 'eq': f2 + 3*f(x).diff(x) + 2*f(x) - exp(-2*x) - x**2, + 'sol': [Eq(f(x), C2*exp(-x) + x**2/2 - x*Rational(3,2) + (C1 - x)*exp(-2*x) + Rational(7,4))], + 'slow': True, + }, + + 'undet_20': { + 'eq': f2 - 3*f(x).diff(x) + 2*f(x) - x*exp(-x), + 'sol': [Eq(f(x), C1*exp(x) + C2*exp(2*x) + (6*x + 5)*exp(-x)/36)], + 'slow': True, + }, + + 'undet_21': { + 'eq': f2 + f(x).diff(x) - 6*f(x) - x - exp(2*x), + 'sol': [Eq(f(x), Rational(-1, 36) - x/6 + C2*exp(-3*x) + (C1 + x/5)*exp(2*x))], + 'slow': True, + }, + + 'undet_22': { + 'eq': f2 + f(x) - sin(x) - exp(-x), + 'sol': [Eq(f(x), C2*sin(x) + (C1 - x/2)*cos(x) + exp(-x)/2)], + 'slow': True, + }, + + 'undet_23': { + 'eq': f(x).diff(x, 3) - 3*f2 + 3*f(x).diff(x) - f(x) - exp(x), + 'sol': [Eq(f(x), (C1 + x*(C2 + x*(C3 + x/6)))*exp(x))], + 'slow': True, + }, + + 'undet_24': { + 'eq': f2 + f(x) - S.Half - cos(2*x)/2, + 'sol': [Eq(f(x), S.Half - cos(2*x)/6 + C1*sin(x) + C2*cos(x))], + 'slow': True, + }, + + 'undet_25': { + 'eq': f(x).diff(x, 3) - f(x).diff(x) - exp(2*x)*(S.Half - cos(2*x)/2), + 'sol': [Eq(f(x), C1 + C2*exp(-x) + C3*exp(x) + (-21*sin(2*x) + 27*cos(2*x) + 130)*exp(2*x)/1560)], + 'slow': True, + }, + + #Note: 'undet_26' is referred in 'undet_37' + 'undet_26': { + 'eq': (f(x).diff(x, 5) + 2*f(x).diff(x, 3) + f(x).diff(x) - 2*x - + sin(x) - cos(x)), + 'sol': [Eq(f(x), C1 + x**2 + (C2 + x*(C3 - x/8))*sin(x) + (C4 + x*(C5 + x/8))*cos(x))], + 'slow': True, + }, + + 'undet_27': { + 'eq': f2 + f(x) - cos(x)/2 + cos(3*x)/2, + 'sol': [Eq(f(x), cos(3*x)/16 + C2*cos(x) + (C1 + x/4)*sin(x))], + 'slow': True, + }, + + 'undet_28': { + 'eq': f(x).diff(x) - 1, + 'sol': [Eq(f(x), C1 + x)], + 'slow': True, + }, + + # https://github.com/sympy/sympy/issues/19358 + 'undet_29': { + 'eq': f2 + f(x).diff(x) + exp(x-C1), + 'sol': [Eq(f(x), C2 + C3*exp(-x) - exp(-C1 + x)/2)], + 'slow': True, + }, + + # https://github.com/sympy/sympy/issues/18408 + 'undet_30': { + 'eq': f(x).diff(x, 3) - f(x).diff(x) - sinh(x), + 'sol': [Eq(f(x), C1 + C2*exp(-x) + C3*exp(x) + x*sinh(x)/2)], + }, + + 'undet_31': { + 'eq': f(x).diff(x, 2) - 49*f(x) - sinh(3*x), + 'sol': [Eq(f(x), C1*exp(-7*x) + C2*exp(7*x) - sinh(3*x)/40)], + }, + + 'undet_32': { + 'eq': f(x).diff(x, 3) - f(x).diff(x) - sinh(x) - exp(x), + 'sol': [Eq(f(x), C1 + C3*exp(-x) + x*sinh(x)/2 + (C2 + x/2)*exp(x))], + }, + + # https://github.com/sympy/sympy/issues/5096 + 'undet_33': { + 'eq': f(x).diff(x, x) + f(x) - x*sin(x - 2), + 'sol': [Eq(f(x), C1*sin(x) + C2*cos(x) - x**2*cos(x - 2)/4 + x*sin(x - 2)/4)], + }, + + 'undet_34': { + 'eq': f(x).diff(x, 2) + f(x) - x**4*sin(x-1), + 'sol': [ Eq(f(x), C1*sin(x) + C2*cos(x) - x**5*cos(x - 1)/10 + x**4*sin(x - 1)/4 + x**3*cos(x - 1)/2 - 3*x**2*sin(x - 1)/4 - 3*x*cos(x - 1)/4)], + }, + + 'undet_35': { + 'eq': f(x).diff(x, 2) - f(x) - exp(x - 1), + 'sol': [Eq(f(x), C2*exp(-x) + (C1 + x*exp(-1)/2)*exp(x))], + }, + + 'undet_36': { + 'eq': f(x).diff(x, 2)+f(x)-(sin(x-2)+1), + 'sol': [Eq(f(x), C1*sin(x) + C2*cos(x) - x*cos(x - 2)/2 + 1)], + }, + + # Equivalent to example_name 'undet_26'. + # This previously failed because the algorithm for undetermined coefficients + # didn't know to multiply exp(I*x) by sufficient x because it is linearly + # dependent on sin(x) and cos(x). + 'undet_37': { + 'eq': f(x).diff(x, 5) + 2*f(x).diff(x, 3) + f(x).diff(x) - 2*x - exp(I*x), + 'sol': [Eq(f(x), C1 + x**2*(I*exp(I*x)/8 + 1) + (C2 + C3*x)*sin(x) + (C4 + C5*x)*cos(x))], + }, + + # https://github.com/sympy/sympy/issues/12623 + 'undet_38': { + 'eq': Eq( u(t).diff(t,t) + R /L*u(t).diff(t) + 1/(L*C)*u(t), alpha), + 'sol': [Eq(u(t), C*L*alpha + C2*exp(-t*(R + sqrt(C*R**2 - 4*L)/sqrt(C))/(2*L)) + + C1*exp(t*(-R + sqrt(C*R**2 - 4*L)/sqrt(C))/(2*L)))], + 'func': u(t) + }, + + 'undet_39': { + 'eq': Eq( L*C*u(t).diff(t,t) + R*C*u(t).diff(t) + u(t), E_0*exp(I*omega*t) ), + 'sol': [Eq(u(t), C2*exp(-t*(R + sqrt(C*R**2 - 4*L)/sqrt(C))/(2*L)) + + C1*exp(t*(-R + sqrt(C*R**2 - 4*L)/sqrt(C))/(2*L)) + - E_0*exp(I*omega*t)/(C*L*omega**2 - I*C*R*omega - 1))], + 'func': u(t), + }, + + # https://github.com/sympy/sympy/issues/6879 + 'undet_40': { + 'eq': Eq(Derivative(f(x), x, 2) - 2*Derivative(f(x), x) + f(x), sin(x)), + 'sol': [Eq(f(x), (C1 + C2*x)*exp(x) + cos(x)/2)], + }, + } + } + + +@_add_example_keys +def _get_examples_ode_sol_separable(): + # test_separable1-5 are from Ordinary Differential Equations, Tenenbaum and + # Pollard, pg. 55 + t,a = symbols('a,t') + m = 96 + g = 9.8 + k = .2 + f1 = g * m + v = Function('v') + return { + 'hint': "separable", + 'func': f(x), + 'examples':{ + 'separable_01': { + 'eq': f(x).diff(x) - f(x), + 'sol': [Eq(f(x), C1*exp(x))], + }, + + 'separable_02': { + 'eq': x*f(x).diff(x) - f(x), + 'sol': [Eq(f(x), C1*x)], + }, + + 'separable_03': { + 'eq': f(x).diff(x) + sin(x), + 'sol': [Eq(f(x), C1 + cos(x))], + }, + + 'separable_04': { + 'eq': f(x)**2 + 1 - (x**2 + 1)*f(x).diff(x), + 'sol': [Eq(f(x), tan(C1 + atan(x)))], + }, + + 'separable_05': { + 'eq': f(x).diff(x)/tan(x) - f(x) - 2, + 'sol': [Eq(f(x), C1/cos(x) - 2)], + }, + + 'separable_06': { + 'eq': f(x).diff(x) * (1 - sin(f(x))) - 1, + 'sol': [Eq(-x + f(x) + cos(f(x)), C1)], + }, + + 'separable_07': { + 'eq': f(x)*x**2*f(x).diff(x) - f(x)**3 - 2*x**2*f(x).diff(x), + 'sol': [Eq(f(x), (-x - sqrt(x*(4*C1*x + x - 4)))/(C1*x - 1)/2), + Eq(f(x), (-x + sqrt(x*(4*C1*x + x - 4)))/(C1*x - 1)/2)], + 'slow': True, + }, + + 'separable_08': { + 'eq': f(x)**2 - 1 - (2*f(x) + x*f(x))*f(x).diff(x), + 'sol': [Eq(f(x), -sqrt(C1*x**2 + 4*C1*x + 4*C1 + 1)), + Eq(f(x), sqrt(C1*x**2 + 4*C1*x + 4*C1 + 1))], + 'slow': True, + }, + + 'separable_09': { + 'eq': x*log(x)*f(x).diff(x) + sqrt(1 + f(x)**2), + 'sol': [Eq(f(x), sinh(C1 - log(log(x))))], #One more solution is f(x)=I + 'slow': True, + 'checkodesol_XFAIL': True, + }, + + 'separable_10': { + 'eq': exp(x + 1)*tan(f(x)) + cos(f(x))*f(x).diff(x), + 'sol': [Eq(E*exp(x) + log(cos(f(x)) - 1)/2 - log(cos(f(x)) + 1)/2 + cos(f(x)), C1)], + 'slow': True, + }, + + 'separable_11': { + 'eq': (x*cos(f(x)) + x**2*sin(f(x))*f(x).diff(x) - a**2*sin(f(x))*f(x).diff(x)), + 'sol': [ + Eq(f(x), -acos(C1*sqrt(-a**2 + x**2)) + 2*pi), + Eq(f(x), acos(C1*sqrt(-a**2 + x**2))) + ], + 'slow': True, + }, + + 'separable_12': { + 'eq': f(x).diff(x) - f(x)*tan(x), + 'sol': [Eq(f(x), C1/cos(x))], + }, + + 'separable_13': { + 'eq': (x - 1)*cos(f(x))*f(x).diff(x) - 2*x*sin(f(x)), + 'sol': [ + Eq(f(x), pi - asin(C1*(x**2 - 2*x + 1)*exp(2*x))), + Eq(f(x), asin(C1*(x**2 - 2*x + 1)*exp(2*x))) + ], + }, + + 'separable_14': { + 'eq': f(x).diff(x) - f(x)*log(f(x))/tan(x), + 'sol': [Eq(f(x), exp(C1*sin(x)))], + }, + + 'separable_15': { + 'eq': x*f(x).diff(x) + (1 + f(x)**2)*atan(f(x)), + 'sol': [Eq(f(x), tan(C1/x))], #Two more solutions are f(x)=0 and f(x)=I + 'slow': True, + 'checkodesol_XFAIL': True, + }, + + 'separable_16': { + 'eq': f(x).diff(x) + x*(f(x) + 1), + 'sol': [Eq(f(x), -1 + C1*exp(-x**2/2))], + }, + + 'separable_17': { + 'eq': exp(f(x)**2)*(x**2 + 2*x + 1) + (x*f(x) + f(x))*f(x).diff(x), + 'sol': [ + Eq(f(x), -sqrt(log(1/(C1 + x**2 + 2*x)))), + Eq(f(x), sqrt(log(1/(C1 + x**2 + 2*x)))) + ], + }, + + 'separable_18': { + 'eq': f(x).diff(x) + f(x), + 'sol': [Eq(f(x), C1*exp(-x))], + }, + + 'separable_19': { + 'eq': sin(x)*cos(2*f(x)) + cos(x)*sin(2*f(x))*f(x).diff(x), + 'sol': [Eq(f(x), pi - acos(C1/cos(x)**2)/2), Eq(f(x), acos(C1/cos(x)**2)/2)], + }, + + 'separable_20': { + 'eq': (1 - x)*f(x).diff(x) - x*(f(x) + 1), + 'sol': [Eq(f(x), (C1*exp(-x) - x + 1)/(x - 1))], + }, + + 'separable_21': { + 'eq': f(x)*diff(f(x), x) + x - 3*x*f(x)**2, + 'sol': [Eq(f(x), -sqrt(3)*sqrt(C1*exp(3*x**2) + 1)/3), + Eq(f(x), sqrt(3)*sqrt(C1*exp(3*x**2) + 1)/3)], + }, + + 'separable_22': { + 'eq': f(x).diff(x) - exp(x + f(x)), + 'sol': [Eq(f(x), log(-1/(C1 + exp(x))))], + 'XFAIL': ['lie_group'] #It shows 'NoneType' object is not subscriptable for lie_group. + }, + + # https://github.com/sympy/sympy/issues/7081 + 'separable_23': { + 'eq': x*(f(x).diff(x)) + 1 - f(x)**2, + 'sol': [Eq(f(x), (-C1 - x**2)/(-C1 + x**2))], + }, + + # https://github.com/sympy/sympy/issues/10379 + 'separable_24': { + 'eq': f(t).diff(t)-(1-51.05*y*f(t)), + 'sol': [Eq(f(t), (0.019588638589618023*exp(y*(C1 - 51.049999999999997*t)) + 0.019588638589618023)/y)], + 'func': f(t), + }, + + # https://github.com/sympy/sympy/issues/15999 + 'separable_25': { + 'eq': f(x).diff(x) - C1*f(x), + 'sol': [Eq(f(x), C2*exp(C1*x))], + }, + + 'separable_26': { + 'eq': f1 - k * (v(t) ** 2) - m * Derivative(v(t)), + 'sol': [Eq(v(t), -68.585712797928991/tanh(C1 - 0.14288690166235204*t))], + 'func': v(t), + 'checkodesol_XFAIL': True, + }, + + #https://github.com/sympy/sympy/issues/22155 + 'separable_27': { + 'eq': f(x).diff(x) - exp(f(x) - x), + 'sol': [Eq(f(x), log(-exp(x)/(C1*exp(x) - 1)))], + } + } + } + + +@_add_example_keys +def _get_examples_ode_sol_1st_exact(): + # Type: Exact differential equation, p(x,f) + q(x,f)*f' == 0, + # where dp/df == dq/dx + ''' + Example 7 is an exact equation that fails under the exact engine. It is caught + by first order homogeneous albeit with a much contorted solution. The + exact engine fails because of a poorly simplified integral of q(0,y)dy, + where q is the function multiplying f'. The solutions should be + Eq(sqrt(x**2+f(x)**2)**3+y**3, C1). The equation below is + equivalent, but it is so complex that checkodesol fails, and takes a long + time to do so. + ''' + return { + 'hint': "1st_exact", + 'func': f(x), + 'examples':{ + '1st_exact_01': { + 'eq': sin(x)*cos(f(x)) + cos(x)*sin(f(x))*f(x).diff(x), + 'sol': [Eq(f(x), -acos(C1/cos(x)) + 2*pi), Eq(f(x), acos(C1/cos(x)))], + 'slow': True, + }, + + '1st_exact_02': { + 'eq': (2*x*f(x) + 1)/f(x) + (f(x) - x)/f(x)**2*f(x).diff(x), + 'sol': [Eq(f(x), exp(C1 - x**2 + LambertW(-x*exp(-C1 + x**2))))], + 'XFAIL': ['lie_group'], #It shows dsolve raises an exception: List index out of range for lie_group + 'slow': True, + 'checkodesol_XFAIL':True + }, + + '1st_exact_03': { + 'eq': 2*x + f(x)*cos(x) + (2*f(x) + sin(x) - sin(f(x)))*f(x).diff(x), + 'sol': [Eq(f(x)*sin(x) + cos(f(x)) + x**2 + f(x)**2, C1)], + 'XFAIL': ['lie_group'], #It goes into infinite loop for lie_group. + 'slow': True, + }, + + '1st_exact_04': { + 'eq': cos(f(x)) - (x*sin(f(x)) - f(x)**2)*f(x).diff(x), + 'sol': [Eq(x*cos(f(x)) + f(x)**3/3, C1)], + 'slow': True, + }, + + '1st_exact_05': { + 'eq': 2*x*f(x) + (x**2 + f(x)**2)*f(x).diff(x), + 'sol': [Eq(x**2*f(x) + f(x)**3/3, C1)], + 'slow': True, + 'simplify_flag':False + }, + + # This was from issue: https://github.com/sympy/sympy/issues/11290 + '1st_exact_06': { + 'eq': cos(f(x)) - (x*sin(f(x)) - f(x)**2)*f(x).diff(x), + 'sol': [Eq(x*cos(f(x)) + f(x)**3/3, C1)], + 'simplify_flag':False + }, + + '1st_exact_07': { + 'eq': x*sqrt(x**2 + f(x)**2) - (x**2*f(x)/(f(x) - sqrt(x**2 + f(x)**2)))*f(x).diff(x), + 'sol': [Eq(log(x), + C1 - 9*sqrt(1 + f(x)**2/x**2)*asinh(f(x)/x)/(-27*f(x)/x + + 27*sqrt(1 + f(x)**2/x**2)) - 9*sqrt(1 + f(x)**2/x**2)* + log(1 - sqrt(1 + f(x)**2/x**2)*f(x)/x + 2*f(x)**2/x**2)/ + (-27*f(x)/x + 27*sqrt(1 + f(x)**2/x**2)) + + 9*asinh(f(x)/x)*f(x)/(x*(-27*f(x)/x + 27*sqrt(1 + f(x)**2/x**2))) + + 9*f(x)*log(1 - sqrt(1 + f(x)**2/x**2)*f(x)/x + 2*f(x)**2/x**2)/ + (x*(-27*f(x)/x + 27*sqrt(1 + f(x)**2/x**2))))], + 'slow': True, + 'dsolve_too_slow':True + }, + + # Type: a(x)f'(x)+b(x)*f(x)+c(x)=0 + '1st_exact_08': { + 'eq': Eq(x**2*f(x).diff(x) + 3*x*f(x) - sin(x)/x, 0), + 'sol': [Eq(f(x), (C1 - cos(x))/x**3)], + }, + + # these examples are from test_exact_enhancement + '1st_exact_09': { + 'eq': f(x)/x**2 + ((f(x)*x - 1)/x)*f(x).diff(x), + 'sol': [Eq(f(x), (i*sqrt(C1*x**2 + 1) + 1)/x) for i in (-1, 1)], + }, + + '1st_exact_10': { + 'eq': (x*f(x) - 1) + f(x).diff(x)*(x**2 - x*f(x)), + 'sol': [Eq(f(x), x - sqrt(C1 + x**2 - 2*log(x))), Eq(f(x), x + sqrt(C1 + x**2 - 2*log(x)))], + }, + + '1st_exact_11': { + 'eq': (x + 2)*sin(f(x)) + f(x).diff(x)*x*cos(f(x)), + 'sol': [Eq(f(x), -asin(C1*exp(-x)/x**2) + pi), Eq(f(x), asin(C1*exp(-x)/x**2))], + }, + } + } + + +@_add_example_keys +def _get_examples_ode_sol_nth_linear_var_of_parameters(): + g = exp(-x) + f2 = f(x).diff(x, 2) + c = 3*f(x).diff(x, 3) + 5*f2 + f(x).diff(x) - f(x) - x + return { + 'hint': "nth_linear_constant_coeff_variation_of_parameters", + 'func': f(x), + 'examples':{ + 'var_of_parameters_01': { + 'eq': c - x*g, + 'sol': [Eq(f(x), C3*exp(x/3) - x + (C1 + x*(C2 - x**2/24 - 3*x/32))*exp(-x) - 1)], + 'slow': True, + }, + + 'var_of_parameters_02': { + 'eq': c - g, + 'sol': [Eq(f(x), C3*exp(x/3) - x + (C1 + x*(C2 - x/8))*exp(-x) - 1)], + 'slow': True, + }, + + 'var_of_parameters_03': { + 'eq': f(x).diff(x) - 1, + 'sol': [Eq(f(x), C1 + x)], + 'slow': True, + }, + + 'var_of_parameters_04': { + 'eq': f2 + 3*f(x).diff(x) + 2*f(x) - 4, + 'sol': [Eq(f(x), C1*exp(-2*x) + C2*exp(-x) + 2)], + 'slow': True, + }, + + 'var_of_parameters_05': { + 'eq': f2 + 3*f(x).diff(x) + 2*f(x) - 12*exp(x), + 'sol': [Eq(f(x), C1*exp(-2*x) + C2*exp(-x) + 2*exp(x))], + 'slow': True, + }, + + 'var_of_parameters_06': { + 'eq': f2 - 2*f(x).diff(x) - 8*f(x) - 9*x*exp(x) - 10*exp(-x), + 'sol': [Eq(f(x), -x*exp(x) - 2*exp(-x) + C1*exp(-2*x) + C2*exp(4*x))], + 'slow': True, + }, + + 'var_of_parameters_07': { + 'eq': f2 + 2*f(x).diff(x) + f(x) - x**2*exp(-x), + 'sol': [Eq(f(x), (C1 + x*(C2 + x**3/12))*exp(-x))], + 'slow': True, + }, + + 'var_of_parameters_08': { + 'eq': f2 - 3*f(x).diff(x) + 2*f(x) - x*exp(-x), + 'sol': [Eq(f(x), C1*exp(x) + C2*exp(2*x) + (6*x + 5)*exp(-x)/36)], + 'slow': True, + }, + + 'var_of_parameters_09': { + 'eq': f(x).diff(x, 3) - 3*f2 + 3*f(x).diff(x) - f(x) - exp(x), + 'sol': [Eq(f(x), (C1 + x*(C2 + x*(C3 + x/6)))*exp(x))], + 'slow': True, + }, + + 'var_of_parameters_10': { + 'eq': f2 + 2*f(x).diff(x) + f(x) - exp(-x)/x, + 'sol': [Eq(f(x), (C1 + x*(C2 + log(x)))*exp(-x))], + 'slow': True, + }, + + 'var_of_parameters_11': { + 'eq': f2 + f(x) - 1/sin(x)*1/cos(x), + 'sol': [Eq(f(x), (C1 + log(sin(x) - 1)/2 - log(sin(x) + 1)/2 + )*cos(x) + (C2 + log(cos(x) - 1)/2 - log(cos(x) + 1)/2)*sin(x))], + 'slow': True, + }, + + 'var_of_parameters_12': { + 'eq': f(x).diff(x, 4) - 1/x, + 'sol': [Eq(f(x), C1 + C2*x + C3*x**2 + x**3*(C4 + log(x)/6))], + 'slow': True, + }, + + # These were from issue: https://github.com/sympy/sympy/issues/15996 + 'var_of_parameters_13': { + 'eq': f(x).diff(x, 5) + 2*f(x).diff(x, 3) + f(x).diff(x) - 2*x - exp(I*x), + 'sol': [Eq(f(x), C1 + x**2 + (C2 + x*(C3 - x/8 + 3*exp(I*x)/2 + 3*exp(-I*x)/2) + 5*exp(2*I*x)/16 + 2*I*exp(I*x) - 2*I*exp(-I*x))*sin(x) + (C4 + x*(C5 + I*x/8 + 3*I*exp(I*x)/2 - 3*I*exp(-I*x)/2) + + 5*I*exp(2*I*x)/16 - 2*exp(I*x) - 2*exp(-I*x))*cos(x) - I*exp(I*x))], + }, + + 'var_of_parameters_14': { + 'eq': f(x).diff(x, 5) + 2*f(x).diff(x, 3) + f(x).diff(x) - exp(I*x), + 'sol': [Eq(f(x), C1 + (C2 + x*(C3 - x/8) + 5*exp(2*I*x)/16)*sin(x) + (C4 + x*(C5 + I*x/8) + 5*I*exp(2*I*x)/16)*cos(x) - I*exp(I*x))], + }, + + # https://github.com/sympy/sympy/issues/14395 + 'var_of_parameters_15': { + 'eq': Derivative(f(x), x, x) + 9*f(x) - sec(x), + 'sol': [Eq(f(x), (C1 - x/3 + sin(2*x)/3)*sin(3*x) + (C2 + log(cos(x)) + - 2*log(cos(x)**2)/3 + 2*cos(x)**2/3)*cos(3*x))], + 'slow': True, + }, + } + } + + +@_add_example_keys +def _get_examples_ode_sol_2nd_linear_bessel(): + return { + 'hint': "2nd_linear_bessel", + 'func': f(x), + 'examples':{ + '2nd_lin_bessel_01': { + 'eq': x**2*(f(x).diff(x, 2)) + x*(f(x).diff(x)) + (x**2 - 4)*f(x), + 'sol': [Eq(f(x), C1*besselj(2, x) + C2*bessely(2, x))], + }, + + '2nd_lin_bessel_02': { + 'eq': x**2*(f(x).diff(x, 2)) + x*(f(x).diff(x)) + (x**2 +25)*f(x), + 'sol': [Eq(f(x), C1*besselj(5*I, x) + C2*bessely(5*I, x))], + }, + + '2nd_lin_bessel_03': { + 'eq': x**2*(f(x).diff(x, 2)) + x*(f(x).diff(x)) + (x**2)*f(x), + 'sol': [Eq(f(x), C1*besselj(0, x) + C2*bessely(0, x))], + }, + + '2nd_lin_bessel_04': { + 'eq': x**2*(f(x).diff(x, 2)) + x*(f(x).diff(x)) + (81*x**2 -S(1)/9)*f(x), + 'sol': [Eq(f(x), C1*besselj(S(1)/3, 9*x) + C2*bessely(S(1)/3, 9*x))], + }, + + '2nd_lin_bessel_05': { + 'eq': x**2*(f(x).diff(x, 2)) + x*(f(x).diff(x)) + (x**4 - 4)*f(x), + 'sol': [Eq(f(x), C1*besselj(1, x**2/2) + C2*bessely(1, x**2/2))], + }, + + '2nd_lin_bessel_06': { + 'eq': x**2*(f(x).diff(x, 2)) + 2*x*(f(x).diff(x)) + (x**4 - 4)*f(x), + 'sol': [Eq(f(x), (C1*besselj(sqrt(17)/4, x**2/2) + C2*bessely(sqrt(17)/4, x**2/2))/sqrt(x))], + }, + + '2nd_lin_bessel_07': { + 'eq': x**2*(f(x).diff(x, 2)) + x*(f(x).diff(x)) + (x**2 - S(1)/4)*f(x), + 'sol': [Eq(f(x), C1*besselj(S(1)/2, x) + C2*bessely(S(1)/2, x))], + }, + + '2nd_lin_bessel_08': { + 'eq': x**2*(f(x).diff(x, 2)) - 3*x*(f(x).diff(x)) + (4*x + 4)*f(x), + 'sol': [Eq(f(x), x**2*(C1*besselj(0, 4*sqrt(x)) + C2*bessely(0, 4*sqrt(x))))], + }, + + '2nd_lin_bessel_09': { + 'eq': x*(f(x).diff(x, 2)) - f(x).diff(x) + 4*x**3*f(x), + 'sol': [Eq(f(x), x*(C1*besselj(S(1)/2, x**2) + C2*bessely(S(1)/2, x**2)))], + }, + + '2nd_lin_bessel_10': { + 'eq': (x-2)**2*(f(x).diff(x, 2)) - (x-2)*f(x).diff(x) + 4*(x-2)**2*f(x), + 'sol': [Eq(f(x), (x - 2)*(C1*besselj(1, 2*x - 4) + C2*bessely(1, 2*x - 4)))], + }, + + # https://github.com/sympy/sympy/issues/4414 + '2nd_lin_bessel_11': { + 'eq': f(x).diff(x, x) + 2/x*f(x).diff(x) + f(x), + 'sol': [Eq(f(x), (C1*besselj(S(1)/2, x) + C2*bessely(S(1)/2, x))/sqrt(x))], + }, + '2nd_lin_bessel_12': { + 'eq': x**2*f(x).diff(x, 2) + x*f(x).diff(x) + (a**2*x**2/c**2 - b**2)*f(x), + 'sol': [Eq(f(x), C1*besselj(sqrt(b**2), x*sqrt(a**2/c**2)) + C2*bessely(sqrt(b**2), x*sqrt(a**2/c**2)))], + }, + } + } + + +@_add_example_keys +def _get_examples_ode_sol_2nd_2F1_hypergeometric(): + return { + 'hint': "2nd_hypergeometric", + 'func': f(x), + 'examples':{ + '2nd_2F1_hyper_01': { + 'eq': x*(x-1)*f(x).diff(x, 2) + (S(3)/2 -2*x)*f(x).diff(x) + 2*f(x), + 'sol': [Eq(f(x), C1*x**(S(5)/2)*hyper((S(3)/2, S(1)/2), (S(7)/2,), x) + C2*hyper((-1, -2), (-S(3)/2,), x))], + }, + + '2nd_2F1_hyper_02': { + 'eq': x*(x-1)*f(x).diff(x, 2) + (S(7)/2*x)*f(x).diff(x) + f(x), + 'sol': [Eq(f(x), (C1*(1 - x)**(S(5)/2)*hyper((S(1)/2, 2), (S(7)/2,), 1 - x) + + C2*hyper((-S(1)/2, -2), (-S(3)/2,), 1 - x))/(x - 1)**(S(5)/2))], + }, + + '2nd_2F1_hyper_03': { + 'eq': x*(x-1)*f(x).diff(x, 2) + (S(3)+ S(7)/2*x)*f(x).diff(x) + f(x), + 'sol': [Eq(f(x), (C1*(1 - x)**(S(11)/2)*hyper((S(1)/2, 2), (S(13)/2,), 1 - x) + + C2*hyper((-S(7)/2, -5), (-S(9)/2,), 1 - x))/(x - 1)**(S(11)/2))], + }, + + '2nd_2F1_hyper_04': { + 'eq': -x**(S(5)/7)*(-416*x**(S(9)/7)/9 - 2385*x**(S(5)/7)/49 + S(298)*x/3)*f(x)/(196*(-x**(S(6)/7) + + x)**2*(x**(S(6)/7) + x)**2) + Derivative(f(x), (x, 2)), + 'sol': [Eq(f(x), x**(S(45)/98)*(C1*x**(S(4)/49)*hyper((S(1)/3, -S(1)/2), (S(9)/7,), x**(S(2)/7)) + + C2*hyper((S(1)/21, -S(11)/14), (S(5)/7,), x**(S(2)/7)))/(x**(S(2)/7) - 1)**(S(19)/84))], + 'checkodesol_XFAIL':True, + }, + } + } + +@_add_example_keys +def _get_examples_ode_sol_2nd_nonlinear_autonomous_conserved(): + return { + 'hint': "2nd_nonlinear_autonomous_conserved", + 'func': f(x), + 'examples': { + '2nd_nonlinear_autonomous_conserved_01': { + 'eq': f(x).diff(x, 2) + exp(f(x)) + log(f(x)), + 'sol': [ + Eq(Integral(1/sqrt(C1 - 2*_u*log(_u) + 2*_u - 2*exp(_u)), (_u, f(x))), C2 + x), + Eq(Integral(1/sqrt(C1 - 2*_u*log(_u) + 2*_u - 2*exp(_u)), (_u, f(x))), C2 - x) + ], + 'simplify_flag': False, + }, + '2nd_nonlinear_autonomous_conserved_02': { + 'eq': f(x).diff(x, 2) + cbrt(f(x)) + 1/f(x), + 'sol': [ + Eq(sqrt(2)*Integral(1/sqrt(2*C1 - 3*_u**Rational(4, 3) - 4*log(_u)), (_u, f(x))), C2 + x), + Eq(sqrt(2)*Integral(1/sqrt(2*C1 - 3*_u**Rational(4, 3) - 4*log(_u)), (_u, f(x))), C2 - x) + ], + 'simplify_flag': False, + }, + '2nd_nonlinear_autonomous_conserved_03': { + 'eq': f(x).diff(x, 2) + sin(f(x)), + 'sol': [ + Eq(Integral(1/sqrt(C1 + 2*cos(_u)), (_u, f(x))), C2 + x), + Eq(Integral(1/sqrt(C1 + 2*cos(_u)), (_u, f(x))), C2 - x) + ], + 'simplify_flag': False, + }, + '2nd_nonlinear_autonomous_conserved_04': { + 'eq': f(x).diff(x, 2) + cosh(f(x)), + 'sol': [ + Eq(Integral(1/sqrt(C1 - 2*sinh(_u)), (_u, f(x))), C2 + x), + Eq(Integral(1/sqrt(C1 - 2*sinh(_u)), (_u, f(x))), C2 - x) + ], + 'simplify_flag': False, + }, + '2nd_nonlinear_autonomous_conserved_05': { + 'eq': f(x).diff(x, 2) + asin(f(x)), + 'sol': [ + Eq(Integral(1/sqrt(C1 - 2*_u*asin(_u) - 2*sqrt(1 - _u**2)), (_u, f(x))), C2 + x), + Eq(Integral(1/sqrt(C1 - 2*_u*asin(_u) - 2*sqrt(1 - _u**2)), (_u, f(x))), C2 - x) + ], + 'simplify_flag': False, + 'XFAIL': ['2nd_nonlinear_autonomous_conserved_Integral'] + } + } + } + + +@_add_example_keys +def _get_examples_ode_sol_separable_reduced(): + df = f(x).diff(x) + return { + 'hint': "separable_reduced", + 'func': f(x), + 'examples':{ + 'separable_reduced_01': { + 'eq': x* df + f(x)* (1 / (x**2*f(x) - 1)), + 'sol': [Eq(log(x**2*f(x))/3 + log(x**2*f(x) - Rational(3, 2))/6, C1 + log(x))], + 'simplify_flag': False, + 'XFAIL': ['lie_group'], #It hangs. + }, + + #Note: 'separable_reduced_02' is referred in 'separable_reduced_11' + 'separable_reduced_02': { + 'eq': f(x).diff(x) + (f(x) / (x**4*f(x) - x)), + 'sol': [Eq(log(x**3*f(x))/4 + log(x**3*f(x) - Rational(4,3))/12, C1 + log(x))], + 'simplify_flag': False, + 'checkodesol_XFAIL':True, #It hangs for this. + }, + + 'separable_reduced_03': { + 'eq': x*df + f(x)*(x**2*f(x)), + 'sol': [Eq(log(x**2*f(x))/2 - log(x**2*f(x) - 2)/2, C1 + log(x))], + 'simplify_flag': False, + }, + + 'separable_reduced_04': { + 'eq': Eq(f(x).diff(x) + f(x)/x * (1 + (x**(S(2)/3)*f(x))**2), 0), + 'sol': [Eq(-3*log(x**(S(2)/3)*f(x)) + 3*log(3*x**(S(4)/3)*f(x)**2 + 1)/2, C1 + log(x))], + 'simplify_flag': False, + }, + + 'separable_reduced_05': { + 'eq': Eq(f(x).diff(x) + f(x)/x * (1 + (x*f(x))**2), 0), + 'sol': [Eq(f(x), -sqrt(2)*sqrt(1/(C1 + log(x)))/(2*x)),\ + Eq(f(x), sqrt(2)*sqrt(1/(C1 + log(x)))/(2*x))], + }, + + 'separable_reduced_06': { + 'eq': Eq(f(x).diff(x) + (x**4*f(x)**2 + x**2*f(x))*f(x)/(x*(x**6*f(x)**3 + x**4*f(x)**2)), 0), + 'sol': [Eq(f(x), C1 + 1/(2*x**2))], + }, + + 'separable_reduced_07': { + 'eq': Eq(f(x).diff(x) + (f(x)**2)*f(x)/(x), 0), + 'sol': [ + Eq(f(x), -sqrt(2)*sqrt(1/(C1 + log(x)))/2), + Eq(f(x), sqrt(2)*sqrt(1/(C1 + log(x)))/2) + ], + }, + + 'separable_reduced_08': { + 'eq': Eq(f(x).diff(x) + (f(x)+3)*f(x)/(x*(f(x)+2)), 0), + 'sol': [Eq(-log(f(x) + 3)/3 - 2*log(f(x))/3, C1 + log(x))], + 'simplify_flag': False, + 'XFAIL': ['lie_group'], #It hangs. + }, + + 'separable_reduced_09': { + 'eq': Eq(f(x).diff(x) + (f(x)+3)*f(x)/x, 0), + 'sol': [Eq(f(x), 3/(C1*x**3 - 1))], + }, + + 'separable_reduced_10': { + 'eq': Eq(f(x).diff(x) + (f(x)**2+f(x))*f(x)/(x), 0), + 'sol': [Eq(- log(x) - log(f(x) + 1) + log(f(x)) + 1/f(x), C1)], + 'XFAIL': ['lie_group'],#No algorithms are implemented to solve equation -C1 + x*(_y + 1)*exp(-1/_y)/_y + + }, + + # Equivalent to example_name 'separable_reduced_02'. Only difference is testing with simplify=True + 'separable_reduced_11': { + 'eq': f(x).diff(x) + (f(x) / (x**4*f(x) - x)), + 'sol': [Eq(f(x), -sqrt(2)*sqrt(3*3**Rational(1,3)*(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) +- 3*3**Rational(2,3)*exp(12*C1)/(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) + 2/x**6)/6 +- sqrt(2)*sqrt(-3*3**Rational(1,3)*(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) ++ 3*3**Rational(2,3)*exp(12*C1)/(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) + 4/x**6 +- 4*sqrt(2)/(x**9*sqrt(3*3**Rational(1,3)*(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) +- 3*3**Rational(2,3)*exp(12*C1)/(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) + 2/x**6)))/6 + 1/(3*x**3)), +Eq(f(x), -sqrt(2)*sqrt(3*3**Rational(1,3)*(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) +- 3*3**Rational(2,3)*exp(12*C1)/(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) + 2/x**6)/6 ++ sqrt(2)*sqrt(-3*3**Rational(1,3)*(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) ++ 3*3**Rational(2,3)*exp(12*C1)/(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) + 4/x**6 +- 4*sqrt(2)/(x**9*sqrt(3*3**Rational(1,3)*(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) +- 3*3**Rational(2,3)*exp(12*C1)/(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) + 2/x**6)))/6 + 1/(3*x**3)), +Eq(f(x), sqrt(2)*sqrt(3*3**Rational(1,3)*(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) +- 3*3**Rational(2,3)*exp(12*C1)/(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) + 2/x**6)/6 +- sqrt(2)*sqrt(-3*3**Rational(1,3)*(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) ++ 3*3**Rational(2,3)*exp(12*C1)/(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) ++ 4/x**6 + 4*sqrt(2)/(x**9*sqrt(3*3**Rational(1,3)*(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) +- 3*3**Rational(2,3)*exp(12*C1)/(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) + 2/x**6)))/6 + 1/(3*x**3)), +Eq(f(x), sqrt(2)*sqrt(3*3**Rational(1,3)*(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) +- 3*3**Rational(2,3)*exp(12*C1)/(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) + 2/x**6)/6 ++ sqrt(2)*sqrt(-3*3**Rational(1,3)*(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) + 3*3**Rational(2,3)*exp(12*C1)/(sqrt((3*exp(12*C1) ++ x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) + 4/x**6 + 4*sqrt(2)/(x**9*sqrt(3*3**Rational(1,3)*(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) +- exp(12*C1)/x**6)**Rational(1,3) - 3*3**Rational(2,3)*exp(12*C1)/(sqrt((3*exp(12*C1) + x**(-12))*exp(24*C1)) - exp(12*C1)/x**6)**Rational(1,3) + 2/x**6)))/6 + 1/(3*x**3))], + 'checkodesol_XFAIL':True, #It hangs for this. + 'slow': True, + }, + + #These were from issue: https://github.com/sympy/sympy/issues/6247 + 'separable_reduced_12': { + 'eq': x**2*f(x)**2 + x*Derivative(f(x), x), + 'sol': [Eq(f(x), 2*C1/(C1*x**2 - 1))], + }, + } + } + + +@_add_example_keys +def _get_examples_ode_sol_lie_group(): + a, b, c = symbols("a b c") + return { + 'hint': "lie_group", + 'func': f(x), + 'examples':{ + #Example 1-4 and 19-20 were from issue: https://github.com/sympy/sympy/issues/17322 + 'lie_group_01': { + 'eq': x*f(x).diff(x)*(f(x)+4) + (f(x)**2) -2*f(x)-2*x, + 'sol': [], + 'dsolve_too_slow': True, + 'checkodesol_too_slow': True, + }, + + 'lie_group_02': { + 'eq': x*f(x).diff(x)*(f(x)+4) + (f(x)**2) -2*f(x)-2*x, + 'sol': [], + 'dsolve_too_slow': True, + }, + + 'lie_group_03': { + 'eq': Eq(x**7*Derivative(f(x), x) + 5*x**3*f(x)**2 - (2*x**2 + 2)*f(x)**3, 0), + 'sol': [], + 'dsolve_too_slow': True, + }, + + 'lie_group_04': { + 'eq': f(x).diff(x) - (f(x) - x*log(x))**2/x**2 + log(x), + 'sol': [], + 'XFAIL': ['lie_group'], + }, + + 'lie_group_05': { + 'eq': f(x).diff(x)**2, + 'sol': [Eq(f(x), C1)], + 'XFAIL': ['factorable'], #It raises Not Implemented error + }, + + 'lie_group_06': { + 'eq': Eq(f(x).diff(x), x**2*f(x)), + 'sol': [Eq(f(x), C1*exp(x**3)**Rational(1, 3))], + }, + + 'lie_group_07': { + 'eq': f(x).diff(x) + a*f(x) - c*exp(b*x), + 'sol': [Eq(f(x), Piecewise(((-C1*(a + b) + c*exp(x*(a + b)))*exp(-a*x)/(a + b),\ + Ne(a, -b)), ((-C1 + c*x)*exp(-a*x), True)))], + }, + + 'lie_group_08': { + 'eq': f(x).diff(x) + 2*x*f(x) - x*exp(-x**2), + 'sol': [Eq(f(x), (C1 + x**2/2)*exp(-x**2))], + }, + + 'lie_group_09': { + 'eq': (1 + 2*x)*(f(x).diff(x)) + 2 - 4*exp(-f(x)), + 'sol': [Eq(f(x), log(C1/(2*x + 1) + 2))], + }, + + 'lie_group_10': { + 'eq': x**2*(f(x).diff(x)) - f(x) + x**2*exp(x - (1/x)), + 'sol': [Eq(f(x), (C1 - exp(x))*exp(-1/x))], + 'XFAIL': ['factorable'], #It raises Recursion Error (maixmum depth exceeded) + }, + + 'lie_group_11': { + 'eq': x**2*f(x)**2 + x*Derivative(f(x), x), + 'sol': [Eq(f(x), 2/(C1 + x**2))], + }, + + 'lie_group_12': { + 'eq': diff(f(x),x) + 2*x*f(x) - x*exp(-x**2), + 'sol': [Eq(f(x), exp(-x**2)*(C1 + x**2/2))], + }, + + 'lie_group_13': { + 'eq': diff(f(x),x) + f(x)*cos(x) - exp(2*x), + 'sol': [Eq(f(x), exp(-sin(x))*(C1 + Integral(exp(2*x)*exp(sin(x)), x)))], + }, + + 'lie_group_14': { + 'eq': diff(f(x),x) + f(x)*cos(x) - sin(2*x)/2, + 'sol': [Eq(f(x), C1*exp(-sin(x)) + sin(x) - 1)], + }, + + 'lie_group_15': { + 'eq': x*diff(f(x),x) + f(x) - x*sin(x), + 'sol': [Eq(f(x), (C1 - x*cos(x) + sin(x))/x)], + }, + + 'lie_group_16': { + 'eq': x*diff(f(x),x) - f(x) - x/log(x), + 'sol': [Eq(f(x), x*(C1 + log(log(x))))], + }, + + 'lie_group_17': { + 'eq': (f(x).diff(x)-f(x)) * (f(x).diff(x)+f(x)), + 'sol': [Eq(f(x), C1*exp(x)), Eq(f(x), C1*exp(-x))], + }, + + 'lie_group_18': { + 'eq': f(x).diff(x) * (f(x).diff(x) - f(x)), + 'sol': [Eq(f(x), C1*exp(x)), Eq(f(x), C1)], + }, + + 'lie_group_19': { + 'eq': (f(x).diff(x)-f(x)) * (f(x).diff(x)+f(x)), + 'sol': [Eq(f(x), C1*exp(-x)), Eq(f(x), C1*exp(x))], + }, + + 'lie_group_20': { + 'eq': f(x).diff(x)*(f(x).diff(x)+f(x)), + 'sol': [Eq(f(x), C1), Eq(f(x), C1*exp(-x))], + }, + } + } + + +@_add_example_keys +def _get_examples_ode_sol_2nd_linear_airy(): + return { + 'hint': "2nd_linear_airy", + 'func': f(x), + 'examples':{ + '2nd_lin_airy_01': { + 'eq': f(x).diff(x, 2) - x*f(x), + 'sol': [Eq(f(x), C1*airyai(x) + C2*airybi(x))], + }, + + '2nd_lin_airy_02': { + 'eq': f(x).diff(x, 2) + 2*x*f(x), + 'sol': [Eq(f(x), C1*airyai(-2**(S(1)/3)*x) + C2*airybi(-2**(S(1)/3)*x))], + }, + } + } + + +@_add_example_keys +def _get_examples_ode_sol_nth_linear_constant_coeff_homogeneous(): + # From Exercise 20, in Ordinary Differential Equations, + # Tenenbaum and Pollard, pg. 220 + a = Symbol('a', positive=True) + k = Symbol('k', real=True) + r1, r2, r3, r4, r5 = [rootof(x**5 + 11*x - 2, n) for n in range(5)] + r6, r7, r8, r9, r10 = [rootof(x**5 - 3*x + 1, n) for n in range(5)] + r11, r12, r13, r14, r15 = [rootof(x**5 - 100*x**3 + 1000*x + 1, n) for n in range(5)] + r16, r17, r18, r19, r20 = [rootof(x**5 - x**4 + 10, n) for n in range(5)] + r21, r22, r23, r24, r25 = [rootof(x**5 - x + 1, n) for n in range(5)] + E = exp(1) + return { + 'hint': "nth_linear_constant_coeff_homogeneous", + 'func': f(x), + 'examples':{ + 'lin_const_coeff_hom_01': { + 'eq': f(x).diff(x, 2) + 2*f(x).diff(x), + 'sol': [Eq(f(x), C1 + C2*exp(-2*x))], + }, + + 'lin_const_coeff_hom_02': { + 'eq': f(x).diff(x, 2) - 3*f(x).diff(x) + 2*f(x), + 'sol': [Eq(f(x), (C1 + C2*exp(x))*exp(x))], + }, + + 'lin_const_coeff_hom_03': { + 'eq': f(x).diff(x, 2) - f(x), + 'sol': [Eq(f(x), C1*exp(-x) + C2*exp(x))], + }, + + 'lin_const_coeff_hom_04': { + 'eq': f(x).diff(x, 3) + f(x).diff(x, 2) - 6*f(x).diff(x), + 'sol': [Eq(f(x), C1 + C2*exp(-3*x) + C3*exp(2*x))], + 'slow': True, + }, + + 'lin_const_coeff_hom_05': { + 'eq': 6*f(x).diff(x, 2) - 11*f(x).diff(x) + 4*f(x), + 'sol': [Eq(f(x), C1*exp(x/2) + C2*exp(x*Rational(4, 3)))], + 'slow': True, + }, + + 'lin_const_coeff_hom_06': { + 'eq': Eq(f(x).diff(x, 2) + 2*f(x).diff(x) - f(x), 0), + 'sol': [Eq(f(x), C1*exp(x*(-1 + sqrt(2))) + C2*exp(-x*(sqrt(2) + 1)))], + 'slow': True, + }, + + 'lin_const_coeff_hom_07': { + 'eq': diff(f(x), x, 3) + diff(f(x), x, 2) - 10*diff(f(x), x) - 6*f(x), + 'sol': [Eq(f(x), C1*exp(3*x) + C3*exp(-x*(2 + sqrt(2))) + C2*exp(x*(-2 + sqrt(2))))], + 'slow': True, + }, + + 'lin_const_coeff_hom_08': { + 'eq': f(x).diff(x, 4) - f(x).diff(x, 3) - 4*f(x).diff(x, 2) + \ + 4*f(x).diff(x), + 'sol': [Eq(f(x), C1 + C2*exp(-2*x) + C3*exp(x) + C4*exp(2*x))], + 'slow': True, + }, + + 'lin_const_coeff_hom_09': { + 'eq': f(x).diff(x, 4) + 4*f(x).diff(x, 3) + f(x).diff(x, 2) - \ + 4*f(x).diff(x) - 2*f(x), + 'sol': [Eq(f(x), C3*exp(-x) + C4*exp(x) + (C1*exp(-sqrt(2)*x) + C2*exp(sqrt(2)*x))*exp(-2*x))], + 'slow': True, + }, + + 'lin_const_coeff_hom_10': { + 'eq': f(x).diff(x, 4) - a**2*f(x), + 'sol': [Eq(f(x), C1*exp(-sqrt(a)*x) + C2*exp(sqrt(a)*x) + C3*sin(sqrt(a)*x) + C4*cos(sqrt(a)*x))], + 'slow': True, + }, + + 'lin_const_coeff_hom_11': { + 'eq': f(x).diff(x, 2) - 2*k*f(x).diff(x) - 2*f(x), + 'sol': [Eq(f(x), C1*exp(x*(k - sqrt(k**2 + 2))) + C2*exp(x*(k + sqrt(k**2 + 2))))], + 'slow': True, + }, + + 'lin_const_coeff_hom_12': { + 'eq': f(x).diff(x, 2) + 4*k*f(x).diff(x) - 12*k**2*f(x), + 'sol': [Eq(f(x), C1*exp(-6*k*x) + C2*exp(2*k*x))], + 'slow': True, + }, + + 'lin_const_coeff_hom_13': { + 'eq': f(x).diff(x, 4), + 'sol': [Eq(f(x), C1 + C2*x + C3*x**2 + C4*x**3)], + 'slow': True, + }, + + 'lin_const_coeff_hom_14': { + 'eq': f(x).diff(x, 2) + 4*f(x).diff(x) + 4*f(x), + 'sol': [Eq(f(x), (C1 + C2*x)*exp(-2*x))], + 'slow': True, + }, + + 'lin_const_coeff_hom_15': { + 'eq': 3*f(x).diff(x, 3) + 5*f(x).diff(x, 2) + f(x).diff(x) - f(x), + 'sol': [Eq(f(x), (C1 + C2*x)*exp(-x) + C3*exp(x/3))], + 'slow': True, + }, + + 'lin_const_coeff_hom_16': { + 'eq': f(x).diff(x, 3) - 6*f(x).diff(x, 2) + 12*f(x).diff(x) - 8*f(x), + 'sol': [Eq(f(x), (C1 + x*(C2 + C3*x))*exp(2*x))], + 'slow': True, + }, + + 'lin_const_coeff_hom_17': { + 'eq': f(x).diff(x, 2) - 2*a*f(x).diff(x) + a**2*f(x), + 'sol': [Eq(f(x), (C1 + C2*x)*exp(a*x))], + 'slow': True, + }, + + 'lin_const_coeff_hom_18': { + 'eq': f(x).diff(x, 4) + 3*f(x).diff(x, 3), + 'sol': [Eq(f(x), C1 + C2*x + C3*x**2 + C4*exp(-3*x))], + 'slow': True, + }, + + 'lin_const_coeff_hom_19': { + 'eq': f(x).diff(x, 4) - 2*f(x).diff(x, 2), + 'sol': [Eq(f(x), C1 + C2*x + C3*exp(-sqrt(2)*x) + C4*exp(sqrt(2)*x))], + 'slow': True, + }, + + 'lin_const_coeff_hom_20': { + 'eq': f(x).diff(x, 4) + 2*f(x).diff(x, 3) - 11*f(x).diff(x, 2) - \ + 12*f(x).diff(x) + 36*f(x), + 'sol': [Eq(f(x), (C1 + C2*x)*exp(-3*x) + (C3 + C4*x)*exp(2*x))], + 'slow': True, + }, + + 'lin_const_coeff_hom_21': { + 'eq': 36*f(x).diff(x, 4) - 37*f(x).diff(x, 2) + 4*f(x).diff(x) + 5*f(x), + 'sol': [Eq(f(x), C1*exp(-x) + C2*exp(-x/3) + C3*exp(x/2) + C4*exp(x*Rational(5, 6)))], + 'slow': True, + }, + + 'lin_const_coeff_hom_22': { + 'eq': f(x).diff(x, 4) - 8*f(x).diff(x, 2) + 16*f(x), + 'sol': [Eq(f(x), (C1 + C2*x)*exp(-2*x) + (C3 + C4*x)*exp(2*x))], + 'slow': True, + }, + + 'lin_const_coeff_hom_23': { + 'eq': f(x).diff(x, 2) - 2*f(x).diff(x) + 5*f(x), + 'sol': [Eq(f(x), (C1*sin(2*x) + C2*cos(2*x))*exp(x))], + 'slow': True, + }, + + 'lin_const_coeff_hom_24': { + 'eq': f(x).diff(x, 2) - f(x).diff(x) + f(x), + 'sol': [Eq(f(x), (C1*sin(x*sqrt(3)/2) + C2*cos(x*sqrt(3)/2))*exp(x/2))], + 'slow': True, + }, + + 'lin_const_coeff_hom_25': { + 'eq': f(x).diff(x, 4) + 5*f(x).diff(x, 2) + 6*f(x), + 'sol': [Eq(f(x), + C1*sin(sqrt(2)*x) + C2*sin(sqrt(3)*x) + C3*cos(sqrt(2)*x) + C4*cos(sqrt(3)*x))], + 'slow': True, + }, + + 'lin_const_coeff_hom_26': { + 'eq': f(x).diff(x, 2) - 4*f(x).diff(x) + 20*f(x), + 'sol': [Eq(f(x), (C1*sin(4*x) + C2*cos(4*x))*exp(2*x))], + 'slow': True, + }, + + 'lin_const_coeff_hom_27': { + 'eq': f(x).diff(x, 4) + 4*f(x).diff(x, 2) + 4*f(x), + 'sol': [Eq(f(x), (C1 + C2*x)*sin(x*sqrt(2)) + (C3 + C4*x)*cos(x*sqrt(2)))], + 'slow': True, + }, + + 'lin_const_coeff_hom_28': { + 'eq': f(x).diff(x, 3) + 8*f(x), + 'sol': [Eq(f(x), (C1*sin(x*sqrt(3)) + C2*cos(x*sqrt(3)))*exp(x) + C3*exp(-2*x))], + 'slow': True, + }, + + 'lin_const_coeff_hom_29': { + 'eq': f(x).diff(x, 4) + 4*f(x).diff(x, 2), + 'sol': [Eq(f(x), C1 + C2*x + C3*sin(2*x) + C4*cos(2*x))], + 'slow': True, + }, + + 'lin_const_coeff_hom_30': { + 'eq': f(x).diff(x, 5) + 2*f(x).diff(x, 3) + f(x).diff(x), + 'sol': [Eq(f(x), C1 + (C2 + C3*x)*sin(x) + (C4 + C5*x)*cos(x))], + 'slow': True, + }, + + 'lin_const_coeff_hom_31': { + 'eq': f(x).diff(x, 4) + f(x).diff(x, 2) + f(x), + 'sol': [Eq(f(x), (C1*sin(sqrt(3)*x/2) + C2*cos(sqrt(3)*x/2))*exp(-x/2) + + (C3*sin(sqrt(3)*x/2) + C4*cos(sqrt(3)*x/2))*exp(x/2))], + 'slow': True, + }, + + 'lin_const_coeff_hom_32': { + 'eq': f(x).diff(x, 4) + 4*f(x).diff(x, 2) + f(x), + 'sol': [Eq(f(x), C1*sin(x*sqrt(-sqrt(3) + 2)) + C2*sin(x*sqrt(sqrt(3) + 2)) + + C3*cos(x*sqrt(-sqrt(3) + 2)) + C4*cos(x*sqrt(sqrt(3) + 2)))], + 'slow': True, + }, + + # One real root, two complex conjugate pairs + 'lin_const_coeff_hom_33': { + 'eq': f(x).diff(x, 5) + 11*f(x).diff(x) - 2*f(x), + 'sol': [Eq(f(x), + C5*exp(r1*x) + exp(re(r2)*x) * (C1*sin(im(r2)*x) + C2*cos(im(r2)*x)) + + exp(re(r4)*x) * (C3*sin(im(r4)*x) + C4*cos(im(r4)*x)))], + 'checkodesol_XFAIL':True, #It Hangs + }, + + # Three real roots, one complex conjugate pair + 'lin_const_coeff_hom_34': { + 'eq': f(x).diff(x,5) - 3*f(x).diff(x) + f(x), + 'sol': [Eq(f(x), + C3*exp(r6*x) + C4*exp(r7*x) + C5*exp(r8*x) + + exp(re(r9)*x) * (C1*sin(im(r9)*x) + C2*cos(im(r9)*x)))], + 'checkodesol_XFAIL':True, #It Hangs + }, + + # Five distinct real roots + 'lin_const_coeff_hom_35': { + 'eq': f(x).diff(x,5) - 100*f(x).diff(x,3) + 1000*f(x).diff(x) + f(x), + 'sol': [Eq(f(x), C1*exp(r11*x) + C2*exp(r12*x) + C3*exp(r13*x) + C4*exp(r14*x) + C5*exp(r15*x))], + 'checkodesol_XFAIL':True, #It Hangs + }, + + # Rational root and unsolvable quintic + 'lin_const_coeff_hom_36': { + 'eq': f(x).diff(x, 6) - 6*f(x).diff(x, 5) + 5*f(x).diff(x, 4) + 10*f(x).diff(x) - 50 * f(x), + 'sol': [Eq(f(x), + C5*exp(5*x) + + C6*exp(x*r16) + + exp(re(r17)*x) * (C1*sin(im(r17)*x) + C2*cos(im(r17)*x)) + + exp(re(r19)*x) * (C3*sin(im(r19)*x) + C4*cos(im(r19)*x)))], + 'checkodesol_XFAIL':True, #It Hangs + }, + + # Five double roots (this is (x**5 - x + 1)**2) + 'lin_const_coeff_hom_37': { + 'eq': f(x).diff(x, 10) - 2*f(x).diff(x, 6) + 2*f(x).diff(x, 5) + + f(x).diff(x, 2) - 2*f(x).diff(x, 1) + f(x), + 'sol': [Eq(f(x), (C1 + C2*x)*exp(x*r21) + (-((C3 + C4*x)*sin(x*im(r22))) + + (C5 + C6*x)*cos(x*im(r22)))*exp(x*re(r22)) + (-((C7 + C8*x)*sin(x*im(r24))) + + (C10*x + C9)*cos(x*im(r24)))*exp(x*re(r24)))], + 'checkodesol_XFAIL':True, #It Hangs + }, + + 'lin_const_coeff_hom_38': { + 'eq': Eq(sqrt(2) * f(x).diff(x,x,x) + f(x).diff(x), 0), + 'sol': [Eq(f(x), C1 + C2*sin(2**Rational(3, 4)*x/2) + C3*cos(2**Rational(3, 4)*x/2))], + }, + + 'lin_const_coeff_hom_39': { + 'eq': Eq(E * f(x).diff(x,x,x) + f(x).diff(x), 0), + 'sol': [Eq(f(x), C1 + C2*sin(x/sqrt(E)) + C3*cos(x/sqrt(E)))], + }, + + 'lin_const_coeff_hom_40': { + 'eq': Eq(pi * f(x).diff(x,x,x) + f(x).diff(x), 0), + 'sol': [Eq(f(x), C1 + C2*sin(x/sqrt(pi)) + C3*cos(x/sqrt(pi)))], + }, + + 'lin_const_coeff_hom_41': { + 'eq': Eq(I * f(x).diff(x,x,x) + f(x).diff(x), 0), + 'sol': [Eq(f(x), C1 + C2*exp(-sqrt(I)*x) + C3*exp(sqrt(I)*x))], + }, + + 'lin_const_coeff_hom_42': { + 'eq': f(x).diff(x, x) + y*f(x), + 'sol': [Eq(f(x), C1*exp(-x*sqrt(-y)) + C2*exp(x*sqrt(-y)))], + }, + + 'lin_const_coeff_hom_43': { + 'eq': Eq(9*f(x).diff(x, x) + f(x), 0), + 'sol': [Eq(f(x), C1*sin(x/3) + C2*cos(x/3))], + }, + + 'lin_const_coeff_hom_44': { + 'eq': Eq(9*f(x).diff(x, x), f(x)), + 'sol': [Eq(f(x), C1*exp(-x/3) + C2*exp(x/3))], + }, + + 'lin_const_coeff_hom_45': { + 'eq': Eq(f(x).diff(x, x) - 3*diff(f(x), x) + 2*f(x), 0), + 'sol': [Eq(f(x), (C1 + C2*exp(x))*exp(x))], + }, + + 'lin_const_coeff_hom_46': { + 'eq': Eq(f(x).diff(x, x) - 4*diff(f(x), x) + 4*f(x), 0), + 'sol': [Eq(f(x), (C1 + C2*x)*exp(2*x))], + }, + + # Type: 2nd order, constant coefficients (two real equal roots) + 'lin_const_coeff_hom_47': { + 'eq': Eq(f(x).diff(x, x) + 2*diff(f(x), x) + 3*f(x), 0), + 'sol': [Eq(f(x), (C1*sin(x*sqrt(2)) + C2*cos(x*sqrt(2)))*exp(-x))], + }, + + #These were from issue: https://github.com/sympy/sympy/issues/6247 + 'lin_const_coeff_hom_48': { + 'eq': f(x).diff(x, x) + 4*f(x), + 'sol': [Eq(f(x), C1*sin(2*x) + C2*cos(2*x))], + }, + } + } + + +@_add_example_keys +def _get_examples_ode_sol_1st_homogeneous_coeff_subs_dep_div_indep(): + return { + 'hint': "1st_homogeneous_coeff_subs_dep_div_indep", + 'func': f(x), + 'examples':{ + 'dep_div_indep_01': { + 'eq': f(x)/x*cos(f(x)/x) - (x/f(x)*sin(f(x)/x) + cos(f(x)/x))*f(x).diff(x), + 'sol': [Eq(log(x), C1 - log(f(x)*sin(f(x)/x)/x))], + 'slow': True + }, + + #indep_div_dep actually has a simpler solution for example 2 but it runs too slow. + 'dep_div_indep_02': { + 'eq': x*f(x).diff(x) - f(x) - x*sin(f(x)/x), + 'sol': [Eq(log(x), log(C1) + log(cos(f(x)/x) - 1)/2 - log(cos(f(x)/x) + 1)/2)], + 'simplify_flag':False, + }, + + 'dep_div_indep_03': { + 'eq': x*exp(f(x)/x) - f(x)*sin(f(x)/x) + x*sin(f(x)/x)*f(x).diff(x), + 'sol': [Eq(log(x), C1 + exp(-f(x)/x)*sin(f(x)/x)/2 + exp(-f(x)/x)*cos(f(x)/x)/2)], + 'slow': True + }, + + 'dep_div_indep_04': { + 'eq': f(x).diff(x) - f(x)/x + 1/sin(f(x)/x), + 'sol': [Eq(f(x), x*(-acos(C1 + log(x)) + 2*pi)), Eq(f(x), x*acos(C1 + log(x)))], + 'slow': True + }, + + # previous code was testing with these other solution: + # example5_solb = Eq(f(x), log(log(C1/x)**(-x))) + 'dep_div_indep_05': { + 'eq': x*exp(f(x)/x) + f(x) - x*f(x).diff(x), + 'sol': [Eq(f(x), log((1/(C1 - log(x)))**x))], + 'checkodesol_XFAIL':True, #(because of **x?) + }, + } + } + +@_add_example_keys +def _get_examples_ode_sol_linear_coefficients(): + return { + 'hint': "linear_coefficients", + 'func': f(x), + 'examples':{ + 'linear_coeff_01': { + 'eq': f(x).diff(x) + (3 + 2*f(x))/(x + 3), + 'sol': [Eq(f(x), C1/(x**2 + 6*x + 9) - Rational(3, 2))], + }, + } + } + +@_add_example_keys +def _get_examples_ode_sol_1st_homogeneous_coeff_best(): + return { + 'hint': "1st_homogeneous_coeff_best", + 'func': f(x), + 'examples':{ + # previous code was testing this with other solution: + # example1_solb = Eq(-f(x)/(1 + log(x/f(x))), C1) + '1st_homogeneous_coeff_best_01': { + 'eq': f(x) + (x*log(f(x)/x) - 2*x)*diff(f(x), x), + 'sol': [Eq(f(x), -exp(C1)*LambertW(-x*exp(-C1 + 1)))], + 'checkodesol_XFAIL':True, #(because of LambertW?) + }, + + '1st_homogeneous_coeff_best_02': { + 'eq': 2*f(x)*exp(x/f(x)) + f(x)*f(x).diff(x) - 2*x*exp(x/f(x))*f(x).diff(x), + 'sol': [Eq(log(f(x)), C1 - 2*exp(x/f(x)))], + }, + + # previous code was testing this with other solution: + # example3_solb = Eq(log(C1*x*sqrt(1/x)*sqrt(f(x))) + x**2/(2*f(x)**2), 0) + '1st_homogeneous_coeff_best_03': { + 'eq': 2*x**2*f(x) + f(x)**3 + (x*f(x)**2 - 2*x**3)*f(x).diff(x), + 'sol': [Eq(f(x), exp(2*C1 + LambertW(-2*x**4*exp(-4*C1))/2)/x)], + 'checkodesol_XFAIL':True, #(because of LambertW?) + }, + + '1st_homogeneous_coeff_best_04': { + 'eq': (x + sqrt(f(x)**2 - x*f(x)))*f(x).diff(x) - f(x), + 'sol': [Eq(log(f(x)), C1 - 2*sqrt(-x/f(x) + 1))], + 'slow': True, + }, + + '1st_homogeneous_coeff_best_05': { + 'eq': x + f(x) - (x - f(x))*f(x).diff(x), + 'sol': [Eq(log(x), C1 - log(sqrt(1 + f(x)**2/x**2)) + atan(f(x)/x))], + }, + + '1st_homogeneous_coeff_best_06': { + 'eq': x*f(x).diff(x) - f(x) - x*sin(f(x)/x), + 'sol': [Eq(f(x), 2*x*atan(C1*x))], + }, + + '1st_homogeneous_coeff_best_07': { + 'eq': x**2 + f(x)**2 - 2*x*f(x)*f(x).diff(x), + 'sol': [Eq(f(x), -sqrt(x*(C1 + x))), Eq(f(x), sqrt(x*(C1 + x)))], + }, + + '1st_homogeneous_coeff_best_08': { + 'eq': f(x)**2 + (x*sqrt(f(x)**2 - x**2) - x*f(x))*f(x).diff(x), + 'sol': [Eq(f(x), -C1*sqrt(-x/(x - 2*C1))), Eq(f(x), C1*sqrt(-x/(x - 2*C1)))], + 'checkodesol_XFAIL': True # solutions are valid in a range + }, + } + } + + +def _get_all_examples(): + all_examples = _get_examples_ode_sol_euler_homogeneous + \ + _get_examples_ode_sol_euler_undetermined_coeff + \ + _get_examples_ode_sol_euler_var_para + \ + _get_examples_ode_sol_factorable + \ + _get_examples_ode_sol_bernoulli + \ + _get_examples_ode_sol_nth_algebraic + \ + _get_examples_ode_sol_riccati + \ + _get_examples_ode_sol_1st_linear + \ + _get_examples_ode_sol_1st_exact + \ + _get_examples_ode_sol_almost_linear + \ + _get_examples_ode_sol_nth_order_reducible + \ + _get_examples_ode_sol_nth_linear_undetermined_coefficients + \ + _get_examples_ode_sol_liouville + \ + _get_examples_ode_sol_separable + \ + _get_examples_ode_sol_1st_rational_riccati + \ + _get_examples_ode_sol_nth_linear_var_of_parameters + \ + _get_examples_ode_sol_2nd_linear_bessel + \ + _get_examples_ode_sol_2nd_2F1_hypergeometric + \ + _get_examples_ode_sol_2nd_nonlinear_autonomous_conserved + \ + _get_examples_ode_sol_separable_reduced + \ + _get_examples_ode_sol_lie_group + \ + _get_examples_ode_sol_2nd_linear_airy + \ + _get_examples_ode_sol_nth_linear_constant_coeff_homogeneous +\ + _get_examples_ode_sol_1st_homogeneous_coeff_best +\ + _get_examples_ode_sol_1st_homogeneous_coeff_subs_dep_div_indep +\ + _get_examples_ode_sol_linear_coefficients + + return all_examples diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/tests/test_subscheck.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/tests/test_subscheck.py new file mode 100644 index 0000000000000000000000000000000000000000..799c2854e878208721b600767de350cda08cd7e5 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/tests/test_subscheck.py @@ -0,0 +1,203 @@ +from sympy.core.function import (Derivative, Function, diff) +from sympy.core.numbers import (I, Rational, pi) +from sympy.core.relational import Eq +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.functions.special.error_functions import (Ei, erf, erfi) +from sympy.integrals.integrals import Integral + +from sympy.solvers.ode.subscheck import checkodesol, checksysodesol + +from sympy.functions import besselj, bessely + +from sympy.testing.pytest import raises, slow + + +C0, C1, C2, C3, C4 = symbols('C0:5') +u, x, y, z = symbols('u,x:z', real=True) +f = Function('f') +g = Function('g') +h = Function('h') + + +@slow +def test_checkodesol(): + # For the most part, checkodesol is well tested in the tests below. + # These tests only handle cases not checked below. + raises(ValueError, lambda: checkodesol(f(x, y).diff(x), Eq(f(x, y), x))) + raises(ValueError, lambda: checkodesol(f(x).diff(x), Eq(f(x, y), + x), f(x, y))) + assert checkodesol(f(x).diff(x), Eq(f(x, y), x)) == \ + (False, -f(x).diff(x) + f(x, y).diff(x) - 1) + assert checkodesol(f(x).diff(x), Eq(f(x), x)) is not True + assert checkodesol(f(x).diff(x), Eq(f(x), x)) == (False, 1) + sol1 = Eq(f(x)**5 + 11*f(x) - 2*f(x) + x, 0) + assert checkodesol(diff(sol1.lhs, x), sol1) == (True, 0) + assert checkodesol(diff(sol1.lhs, x)*exp(f(x)), sol1) == (True, 0) + assert checkodesol(diff(sol1.lhs, x, 2), sol1) == (True, 0) + assert checkodesol(diff(sol1.lhs, x, 2)*exp(f(x)), sol1) == (True, 0) + assert checkodesol(diff(sol1.lhs, x, 3), sol1) == (True, 0) + assert checkodesol(diff(sol1.lhs, x, 3)*exp(f(x)), sol1) == (True, 0) + assert checkodesol(diff(sol1.lhs, x, 3), Eq(f(x), x*log(x))) == \ + (False, 60*x**4*((log(x) + 1)**2 + log(x))*( + log(x) + 1)*log(x)**2 - 5*x**4*log(x)**4 - 9) + assert checkodesol(diff(exp(f(x)) + x, x)*x, Eq(exp(f(x)) + x, 0)) == \ + (True, 0) + assert checkodesol(diff(exp(f(x)) + x, x)*x, Eq(exp(f(x)) + x, 0), + solve_for_func=False) == (True, 0) + assert checkodesol(f(x).diff(x, 2), [Eq(f(x), C1 + C2*x), + Eq(f(x), C2 + C1*x), Eq(f(x), C1*x + C2*x**2)]) == \ + [(True, 0), (True, 0), (False, C2)] + assert checkodesol(f(x).diff(x, 2), {Eq(f(x), C1 + C2*x), + Eq(f(x), C2 + C1*x), Eq(f(x), C1*x + C2*x**2)}) == \ + {(True, 0), (True, 0), (False, C2)} + assert checkodesol(f(x).diff(x) - 1/f(x)/2, Eq(f(x)**2, x)) == \ + [(True, 0), (True, 0)] + assert checkodesol(f(x).diff(x) - f(x), Eq(C1*exp(x), f(x))) == (True, 0) + # Based on test_1st_homogeneous_coeff_ode2_eq3sol. Make sure that + # checkodesol tries back substituting f(x) when it can. + eq3 = x*exp(f(x)/x) + f(x) - x*f(x).diff(x) + sol3 = Eq(f(x), log(log(C1/x)**(-x))) + assert not checkodesol(eq3, sol3)[1].has(f(x)) + # This case was failing intermittently depending on hash-seed: + eqn = Eq(Derivative(x*Derivative(f(x), x), x)/x, exp(x)) + sol = Eq(f(x), C1 + C2*log(x) + exp(x) - Ei(x)) + assert checkodesol(eqn, sol, order=2, solve_for_func=False)[0] + eq = x**2*(f(x).diff(x, 2)) + x*(f(x).diff(x)) + (2*x**2 +25)*f(x) + sol = Eq(f(x), C1*besselj(5*I, sqrt(2)*x) + C2*bessely(5*I, sqrt(2)*x)) + assert checkodesol(eq, sol) == (True, 0) + + eqs = [Eq(f(x).diff(x), f(x) + g(x)), Eq(g(x).diff(x), f(x) + g(x))] + sol = [Eq(f(x), -C1 + C2*exp(2*x)), Eq(g(x), C1 + C2*exp(2*x))] + assert checkodesol(eqs, sol) == (True, [0, 0]) + + +def test_checksysodesol(): + x, y, z = symbols('x, y, z', cls=Function) + t = Symbol('t') + eq = (Eq(diff(x(t),t), 9*y(t)), Eq(diff(y(t),t), 12*x(t))) + sol = [Eq(x(t), 9*C1*exp(-6*sqrt(3)*t) + 9*C2*exp(6*sqrt(3)*t)), \ + Eq(y(t), -6*sqrt(3)*C1*exp(-6*sqrt(3)*t) + 6*sqrt(3)*C2*exp(6*sqrt(3)*t))] + assert checksysodesol(eq, sol) == (True, [0, 0]) + + eq = (Eq(diff(x(t),t), 2*x(t) + 4*y(t)), Eq(diff(y(t),t), 12*x(t) + 41*y(t))) + sol = [Eq(x(t), 4*C1*exp(t*(-sqrt(1713)/2 + Rational(43, 2))) + 4*C2*exp(t*(sqrt(1713)/2 + \ + Rational(43, 2)))), Eq(y(t), C1*(-sqrt(1713)/2 + Rational(39, 2))*exp(t*(-sqrt(1713)/2 + \ + Rational(43, 2))) + C2*(Rational(39, 2) + sqrt(1713)/2)*exp(t*(sqrt(1713)/2 + Rational(43, 2))))] + assert checksysodesol(eq, sol) == (True, [0, 0]) + + eq = (Eq(diff(x(t),t), x(t) + y(t)), Eq(diff(y(t),t), -2*x(t) + 2*y(t))) + sol = [Eq(x(t), (C1*sin(sqrt(7)*t/2) + C2*cos(sqrt(7)*t/2))*exp(t*Rational(3, 2))), \ + Eq(y(t), ((C1/2 - sqrt(7)*C2/2)*sin(sqrt(7)*t/2) + (sqrt(7)*C1/2 + \ + C2/2)*cos(sqrt(7)*t/2))*exp(t*Rational(3, 2)))] + assert checksysodesol(eq, sol) == (True, [0, 0]) + + eq = (Eq(diff(x(t),t), x(t) + y(t) + 9), Eq(diff(y(t),t), 2*x(t) + 5*y(t) + 23)) + sol = [Eq(x(t), C1*exp(t*(-sqrt(6) + 3)) + C2*exp(t*(sqrt(6) + 3)) - \ + Rational(22, 3)), Eq(y(t), C1*(-sqrt(6) + 2)*exp(t*(-sqrt(6) + 3)) + C2*(2 + \ + sqrt(6))*exp(t*(sqrt(6) + 3)) - Rational(5, 3))] + assert checksysodesol(eq, sol) == (True, [0, 0]) + + eq = (Eq(diff(x(t),t), x(t) + y(t) + 81), Eq(diff(y(t),t), -2*x(t) + y(t) + 23)) + sol = [Eq(x(t), (C1*sin(sqrt(2)*t) + C2*cos(sqrt(2)*t))*exp(t) - Rational(58, 3)), \ + Eq(y(t), (sqrt(2)*C1*cos(sqrt(2)*t) - sqrt(2)*C2*sin(sqrt(2)*t))*exp(t) - Rational(185, 3))] + assert checksysodesol(eq, sol) == (True, [0, 0]) + + eq = (Eq(diff(x(t),t), 5*t*x(t) + 2*y(t)), Eq(diff(y(t),t), 2*x(t) + 5*t*y(t))) + sol = [Eq(x(t), (C1*exp(Integral(2, t).doit()) + C2*exp(-(Integral(2, t)).doit()))*\ + exp((Integral(5*t, t)).doit())), Eq(y(t), (C1*exp((Integral(2, t)).doit()) - \ + C2*exp(-(Integral(2, t)).doit()))*exp((Integral(5*t, t)).doit()))] + assert checksysodesol(eq, sol) == (True, [0, 0]) + + eq = (Eq(diff(x(t),t), 5*t*x(t) + t**2*y(t)), Eq(diff(y(t),t), -t**2*x(t) + 5*t*y(t))) + sol = [Eq(x(t), (C1*cos((Integral(t**2, t)).doit()) + C2*sin((Integral(t**2, t)).doit()))*\ + exp((Integral(5*t, t)).doit())), Eq(y(t), (-C1*sin((Integral(t**2, t)).doit()) + \ + C2*cos((Integral(t**2, t)).doit()))*exp((Integral(5*t, t)).doit()))] + assert checksysodesol(eq, sol) == (True, [0, 0]) + + eq = (Eq(diff(x(t),t), 5*t*x(t) + t**2*y(t)), Eq(diff(y(t),t), -t**2*x(t) + (5*t+9*t**2)*y(t))) + sol = [Eq(x(t), (C1*exp((-sqrt(77)/2 + Rational(9, 2))*(Integral(t**2, t)).doit()) + \ + C2*exp((sqrt(77)/2 + Rational(9, 2))*(Integral(t**2, t)).doit()))*exp((Integral(5*t, t)).doit())), \ + Eq(y(t), (C1*(-sqrt(77)/2 + Rational(9, 2))*exp((-sqrt(77)/2 + Rational(9, 2))*(Integral(t**2, t)).doit()) + \ + C2*(sqrt(77)/2 + Rational(9, 2))*exp((sqrt(77)/2 + Rational(9, 2))*(Integral(t**2, t)).doit()))*exp((Integral(5*t, t)).doit()))] + assert checksysodesol(eq, sol) == (True, [0, 0]) + + eq = (Eq(diff(x(t),t,t), 5*x(t) + 43*y(t)), Eq(diff(y(t),t,t), x(t) + 9*y(t))) + root0 = -sqrt(-sqrt(47) + 7) + root1 = sqrt(-sqrt(47) + 7) + root2 = -sqrt(sqrt(47) + 7) + root3 = sqrt(sqrt(47) + 7) + sol = [Eq(x(t), 43*C1*exp(t*root0) + 43*C2*exp(t*root1) + 43*C3*exp(t*root2) + 43*C4*exp(t*root3)), \ + Eq(y(t), C1*(root0**2 - 5)*exp(t*root0) + C2*(root1**2 - 5)*exp(t*root1) + \ + C3*(root2**2 - 5)*exp(t*root2) + C4*(root3**2 - 5)*exp(t*root3))] + assert checksysodesol(eq, sol) == (True, [0, 0]) + + eq = (Eq(diff(x(t),t,t), 8*x(t)+3*y(t)+31), Eq(diff(y(t),t,t), 9*x(t)+7*y(t)+12)) + root0 = -sqrt(-sqrt(109)/2 + Rational(15, 2)) + root1 = sqrt(-sqrt(109)/2 + Rational(15, 2)) + root2 = -sqrt(sqrt(109)/2 + Rational(15, 2)) + root3 = sqrt(sqrt(109)/2 + Rational(15, 2)) + sol = [Eq(x(t), 3*C1*exp(t*root0) + 3*C2*exp(t*root1) + 3*C3*exp(t*root2) + 3*C4*exp(t*root3) - Rational(181, 29)), \ + Eq(y(t), C1*(root0**2 - 8)*exp(t*root0) + C2*(root1**2 - 8)*exp(t*root1) + \ + C3*(root2**2 - 8)*exp(t*root2) + C4*(root3**2 - 8)*exp(t*root3) + Rational(183, 29))] + assert checksysodesol(eq, sol) == (True, [0, 0]) + + eq = (Eq(diff(x(t),t,t) - 9*diff(y(t),t) + 7*x(t),0), Eq(diff(y(t),t,t) + 9*diff(x(t),t) + 7*y(t),0)) + sol = [Eq(x(t), C1*cos(t*(Rational(9, 2) + sqrt(109)/2)) + C2*sin(t*(Rational(9, 2) + sqrt(109)/2)) + \ + C3*cos(t*(-sqrt(109)/2 + Rational(9, 2))) + C4*sin(t*(-sqrt(109)/2 + Rational(9, 2)))), Eq(y(t), -C1*sin(t*(Rational(9, 2) + sqrt(109)/2)) \ + + C2*cos(t*(Rational(9, 2) + sqrt(109)/2)) - C3*sin(t*(-sqrt(109)/2 + Rational(9, 2))) + C4*cos(t*(-sqrt(109)/2 + Rational(9, 2))))] + assert checksysodesol(eq, sol) == (True, [0, 0]) + + eq = (Eq(diff(x(t),t,t), 9*t*diff(y(t),t)-9*y(t)), Eq(diff(y(t),t,t),7*t*diff(x(t),t)-7*x(t))) + I1 = sqrt(6)*7**Rational(1, 4)*sqrt(pi)*erfi(sqrt(6)*7**Rational(1, 4)*t/2)/2 - exp(3*sqrt(7)*t**2/2)/t + I2 = -sqrt(6)*7**Rational(1, 4)*sqrt(pi)*erf(sqrt(6)*7**Rational(1, 4)*t/2)/2 - exp(-3*sqrt(7)*t**2/2)/t + sol = [Eq(x(t), C3*t + t*(9*C1*I1 + 9*C2*I2)), Eq(y(t), C4*t + t*(3*sqrt(7)*C1*I1 - 3*sqrt(7)*C2*I2))] + assert checksysodesol(eq, sol) == (True, [0, 0]) + + eq = (Eq(diff(x(t),t), 21*x(t)), Eq(diff(y(t),t), 17*x(t)+3*y(t)), Eq(diff(z(t),t), 5*x(t)+7*y(t)+9*z(t))) + sol = [Eq(x(t), C1*exp(21*t)), Eq(y(t), 17*C1*exp(21*t)/18 + C2*exp(3*t)), \ + Eq(z(t), 209*C1*exp(21*t)/216 - 7*C2*exp(3*t)/6 + C3*exp(9*t))] + assert checksysodesol(eq, sol) == (True, [0, 0, 0]) + + eq = (Eq(diff(x(t),t),3*y(t)-11*z(t)),Eq(diff(y(t),t),7*z(t)-3*x(t)),Eq(diff(z(t),t),11*x(t)-7*y(t))) + sol = [Eq(x(t), 7*C0 + sqrt(179)*C1*cos(sqrt(179)*t) + (77*C1/3 + 130*C2/3)*sin(sqrt(179)*t)), \ + Eq(y(t), 11*C0 + sqrt(179)*C2*cos(sqrt(179)*t) + (-58*C1/3 - 77*C2/3)*sin(sqrt(179)*t)), \ + Eq(z(t), 3*C0 + sqrt(179)*(-7*C1/3 - 11*C2/3)*cos(sqrt(179)*t) + (11*C1 - 7*C2)*sin(sqrt(179)*t))] + assert checksysodesol(eq, sol) == (True, [0, 0, 0]) + + eq = (Eq(3*diff(x(t),t),4*5*(y(t)-z(t))),Eq(4*diff(y(t),t),3*5*(z(t)-x(t))),Eq(5*diff(z(t),t),3*4*(x(t)-y(t)))) + sol = [Eq(x(t), C0 + 5*sqrt(2)*C1*cos(5*sqrt(2)*t) + (12*C1/5 + 164*C2/15)*sin(5*sqrt(2)*t)), \ + Eq(y(t), C0 + 5*sqrt(2)*C2*cos(5*sqrt(2)*t) + (-51*C1/10 - 12*C2/5)*sin(5*sqrt(2)*t)), \ + Eq(z(t), C0 + 5*sqrt(2)*(-9*C1/25 - 16*C2/25)*cos(5*sqrt(2)*t) + (12*C1/5 - 12*C2/5)*sin(5*sqrt(2)*t))] + assert checksysodesol(eq, sol) == (True, [0, 0, 0]) + + eq = (Eq(diff(x(t),t),4*x(t) - z(t)),Eq(diff(y(t),t),2*x(t)+2*y(t)-z(t)),Eq(diff(z(t),t),3*x(t)+y(t))) + sol = [Eq(x(t), C1*exp(2*t) + C2*t*exp(2*t) + C2*exp(2*t) + C3*t**2*exp(2*t)/2 + C3*t*exp(2*t) + C3*exp(2*t)), \ + Eq(y(t), C1*exp(2*t) + C2*t*exp(2*t) + C2*exp(2*t) + C3*t**2*exp(2*t)/2 + C3*t*exp(2*t)), \ + Eq(z(t), 2*C1*exp(2*t) + 2*C2*t*exp(2*t) + C2*exp(2*t) + C3*t**2*exp(2*t) + C3*t*exp(2*t) + C3*exp(2*t))] + assert checksysodesol(eq, sol) == (True, [0, 0, 0]) + + eq = (Eq(diff(x(t),t),4*x(t) - y(t) - 2*z(t)),Eq(diff(y(t),t),2*x(t) + y(t)- 2*z(t)),Eq(diff(z(t),t),5*x(t)-3*z(t))) + sol = [Eq(x(t), C1*exp(2*t) + C2*(-sin(t) + 3*cos(t)) + C3*(3*sin(t) + cos(t))), \ + Eq(y(t), C2*(-sin(t) + 3*cos(t)) + C3*(3*sin(t) + cos(t))), Eq(z(t), C1*exp(2*t) + 5*C2*cos(t) + 5*C3*sin(t))] + assert checksysodesol(eq, sol) == (True, [0, 0, 0]) + + eq = (Eq(diff(x(t),t),x(t)*y(t)**3), Eq(diff(y(t),t),y(t)**5)) + sol = [Eq(x(t), C1*exp((-1/(4*C2 + 4*t))**(Rational(-1, 4)))), Eq(y(t), -(-1/(4*C2 + 4*t))**Rational(1, 4)), \ + Eq(x(t), C1*exp(-1/(-1/(4*C2 + 4*t))**Rational(1, 4))), Eq(y(t), (-1/(4*C2 + 4*t))**Rational(1, 4)), \ + Eq(x(t), C1*exp(-I/(-1/(4*C2 + 4*t))**Rational(1, 4))), Eq(y(t), -I*(-1/(4*C2 + 4*t))**Rational(1, 4)), \ + Eq(x(t), C1*exp(I/(-1/(4*C2 + 4*t))**Rational(1, 4))), Eq(y(t), I*(-1/(4*C2 + 4*t))**Rational(1, 4))] + assert checksysodesol(eq, sol) == (True, [0, 0]) + + eq = (Eq(diff(x(t),t), exp(3*x(t))*y(t)**3),Eq(diff(y(t),t), y(t)**5)) + sol = [Eq(x(t), -log(C1 - 3/(-1/(4*C2 + 4*t))**Rational(1, 4))/3), Eq(y(t), -(-1/(4*C2 + 4*t))**Rational(1, 4)), \ + Eq(x(t), -log(C1 + 3/(-1/(4*C2 + 4*t))**Rational(1, 4))/3), Eq(y(t), (-1/(4*C2 + 4*t))**Rational(1, 4)), \ + Eq(x(t), -log(C1 + 3*I/(-1/(4*C2 + 4*t))**Rational(1, 4))/3), Eq(y(t), -I*(-1/(4*C2 + 4*t))**Rational(1, 4)), \ + Eq(x(t), -log(C1 - 3*I/(-1/(4*C2 + 4*t))**Rational(1, 4))/3), Eq(y(t), I*(-1/(4*C2 + 4*t))**Rational(1, 4))] + assert checksysodesol(eq, sol) == (True, [0, 0]) + + eq = (Eq(x(t),t*diff(x(t),t)+diff(x(t),t)*diff(y(t),t)), Eq(y(t),t*diff(y(t),t)+diff(y(t),t)**2)) + sol = {Eq(x(t), C1*C2 + C1*t), Eq(y(t), C2**2 + C2*t)} + assert checksysodesol(eq, sol) == (True, [0, 0]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/tests/test_systems.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/tests/test_systems.py new file mode 100644 index 0000000000000000000000000000000000000000..9d206129dfcf38c7b8c2e0ab42bd875003253f35 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/ode/tests/test_systems.py @@ -0,0 +1,2544 @@ +from sympy.core.function import (Derivative, Function, diff) +from sympy.core.mul import Mul +from sympy.core.numbers import (I, Rational, pi) +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.hyperbolic import sinh +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.matrices.dense import Matrix +from sympy.core.containers import Tuple +from sympy.functions import exp, cos, sin, log, Ci, Si, erf, erfi +from sympy.matrices import dotprodsimp, NonSquareMatrixError +from sympy.solvers.ode import dsolve +from sympy.solvers.ode.ode import constant_renumber +from sympy.solvers.ode.subscheck import checksysodesol +from sympy.solvers.ode.systems import (_classify_linear_system, linear_ode_to_matrix, + ODEOrderError, ODENonlinearError, _simpsol, + _is_commutative_anti_derivative, linodesolve, + canonical_odes, dsolve_system, _component_division, + _eqs2dict, _dict2graph) +from sympy.functions import airyai, airybi +from sympy.integrals.integrals import Integral +from sympy.simplify.ratsimp import ratsimp +from sympy.testing.pytest import raises, slow, tooslow, XFAIL + + +C0, C1, C2, C3, C4, C5, C6, C7, C8, C9, C10 = symbols('C0:11') +x = symbols('x') +f = Function('f') +g = Function('g') +h = Function('h') + + +def test_linear_ode_to_matrix(): + f, g, h = symbols("f, g, h", cls=Function) + t = Symbol("t") + funcs = [f(t), g(t), h(t)] + f1 = f(t).diff(t) + g1 = g(t).diff(t) + h1 = h(t).diff(t) + f2 = f(t).diff(t, 2) + g2 = g(t).diff(t, 2) + h2 = h(t).diff(t, 2) + + eqs_1 = [Eq(f1, g(t)), Eq(g1, f(t))] + sol_1 = ([Matrix([[1, 0], [0, 1]]), Matrix([[ 0, 1], [1, 0]])], Matrix([[0],[0]])) + assert linear_ode_to_matrix(eqs_1, funcs[:-1], t, 1) == sol_1 + + eqs_2 = [Eq(f1, f(t) + 2*g(t)), Eq(g1, h(t)), Eq(h1, g(t) + h(t) + f(t))] + sol_2 = ([Matrix([[1, 0, 0], [0, 1, 0], [0, 0, 1]]), Matrix([[1, 2, 0], [ 0, 0, 1], [1, 1, 1]])], + Matrix([[0], [0], [0]])) + assert linear_ode_to_matrix(eqs_2, funcs, t, 1) == sol_2 + + eqs_3 = [Eq(2*f1 + 3*h1, f(t) + g(t)), Eq(4*h1 + 5*g1, f(t) + h(t)), Eq(5*f1 + 4*g1, g(t) + h(t))] + sol_3 = ([Matrix([[2, 0, 3], [0, 5, 4], [5, 4, 0]]), Matrix([[1, 1, 0], [1, 0, 1], [0, 1, 1]])], + Matrix([[0], [0], [0]])) + assert linear_ode_to_matrix(eqs_3, funcs, t, 1) == sol_3 + + eqs_4 = [Eq(f2 + h(t), f1 + g(t)), Eq(2*h2 + g2 + g1 + g(t), 0), Eq(3*h1, 4)] + sol_4 = ([Matrix([[1, 0, 0], [0, 1, 2], [0, 0, 0]]), Matrix([[1, 0, 0], [0, -1, 0], [0, 0, -3]]), + Matrix([[0, 1, -1], [0, -1, 0], [0, 0, 0]])], Matrix([[0], [0], [4]])) + assert linear_ode_to_matrix(eqs_4, funcs, t, 2) == sol_4 + + eqs_5 = [Eq(f2, g(t)), Eq(f1 + g1, f(t))] + raises(ODEOrderError, lambda: linear_ode_to_matrix(eqs_5, funcs[:-1], t, 1)) + + eqs_6 = [Eq(f1, f(t)**2), Eq(g1, f(t) + g(t))] + raises(ODENonlinearError, lambda: linear_ode_to_matrix(eqs_6, funcs[:-1], t, 1)) + + +def test__classify_linear_system(): + x, y, z, w = symbols('x, y, z, w', cls=Function) + t, k, l = symbols('t k l') + x1 = diff(x(t), t) + y1 = diff(y(t), t) + z1 = diff(z(t), t) + w1 = diff(w(t), t) + x2 = diff(x(t), t, t) + y2 = diff(y(t), t, t) + funcs = [x(t), y(t)] + funcs_2 = funcs + [z(t), w(t)] + + eqs_1 = (5 * x1 + 12 * x(t) - 6 * (y(t)), (2 * y1 - 11 * t * x(t) + 3 * y(t) + t)) + assert _classify_linear_system(eqs_1, funcs, t) is None + + eqs_2 = (5 * (x1**2) + 12 * x(t) - 6 * (y(t)), (2 * y1 - 11 * t * x(t) + 3 * y(t) + t)) + sol2 = {'is_implicit': True, + 'canon_eqs': [[Eq(Derivative(x(t), t), -sqrt(-12*x(t)/5 + 6*y(t)/5)), + Eq(Derivative(y(t), t), 11*t*x(t)/2 - t/2 - 3*y(t)/2)], + [Eq(Derivative(x(t), t), sqrt(-12*x(t)/5 + 6*y(t)/5)), + Eq(Derivative(y(t), t), 11*t*x(t)/2 - t/2 - 3*y(t)/2)]]} + assert _classify_linear_system(eqs_2, funcs, t) == sol2 + + eqs_2_1 = [Eq(Derivative(x(t), t), -sqrt(-12*x(t)/5 + 6*y(t)/5)), + Eq(Derivative(y(t), t), 11*t*x(t)/2 - t/2 - 3*y(t)/2)] + assert _classify_linear_system(eqs_2_1, funcs, t) is None + + eqs_2_2 = [Eq(Derivative(x(t), t), sqrt(-12*x(t)/5 + 6*y(t)/5)), + Eq(Derivative(y(t), t), 11*t*x(t)/2 - t/2 - 3*y(t)/2)] + assert _classify_linear_system(eqs_2_2, funcs, t) is None + + eqs_3 = (5 * x1 + 12 * x(t) - 6 * (y(t)), (2 * y1 - 11 * x(t) + 3 * y(t)), (5 * w1 + z(t)), (z1 + w(t))) + answer_3 = {'no_of_equation': 4, + 'eq': (12*x(t) - 6*y(t) + 5*Derivative(x(t), t), + -11*x(t) + 3*y(t) + 2*Derivative(y(t), t), + z(t) + 5*Derivative(w(t), t), + w(t) + Derivative(z(t), t)), + 'func': [x(t), y(t), z(t), w(t)], + 'order': {x(t): 1, y(t): 1, z(t): 1, w(t): 1}, + 'is_linear': True, + 'is_constant': True, + 'is_homogeneous': True, + 'func_coeff': -Matrix([ + [Rational(12, 5), Rational(-6, 5), 0, 0], + [Rational(-11, 2), Rational(3, 2), 0, 0], + [0, 0, 0, 1], + [0, 0, Rational(1, 5), 0]]), + 'type_of_equation': 'type1', + 'is_general': True} + assert _classify_linear_system(eqs_3, funcs_2, t) == answer_3 + + eqs_4 = (5 * x1 + 12 * x(t) - 6 * (y(t)), (2 * y1 - 11 * x(t) + 3 * y(t)), (z1 - w(t)), (w1 - z(t))) + answer_4 = {'no_of_equation': 4, + 'eq': (12 * x(t) - 6 * y(t) + 5 * Derivative(x(t), t), + -11 * x(t) + 3 * y(t) + 2 * Derivative(y(t), t), + -w(t) + Derivative(z(t), t), + -z(t) + Derivative(w(t), t)), + 'func': [x(t), y(t), z(t), w(t)], + 'order': {x(t): 1, y(t): 1, z(t): 1, w(t): 1}, + 'is_linear': True, + 'is_constant': True, + 'is_homogeneous': True, + 'func_coeff': -Matrix([ + [Rational(12, 5), Rational(-6, 5), 0, 0], + [Rational(-11, 2), Rational(3, 2), 0, 0], + [0, 0, 0, -1], + [0, 0, -1, 0]]), + 'type_of_equation': 'type1', + 'is_general': True} + assert _classify_linear_system(eqs_4, funcs_2, t) == answer_4 + + eqs_5 = (5*x1 + 12*x(t) - 6*(y(t)) + x2, (2*y1 - 11*x(t) + 3*y(t)), (z1 - w(t)), (w1 - z(t))) + answer_5 = {'no_of_equation': 4, 'eq': (12*x(t) - 6*y(t) + 5*Derivative(x(t), t) + Derivative(x(t), (t, 2)), + -11*x(t) + 3*y(t) + 2*Derivative(y(t), t), -w(t) + Derivative(z(t), t), -z(t) + Derivative(w(t), + t)), 'func': [x(t), y(t), z(t), w(t)], 'order': {x(t): 2, y(t): 1, z(t): 1, w(t): 1}, 'is_linear': + True, 'is_homogeneous': True, 'is_general': True, 'type_of_equation': 'type0', 'is_higher_order': True} + assert _classify_linear_system(eqs_5, funcs_2, t) == answer_5 + + eqs_6 = (Eq(x1, 3*y(t) - 11*z(t)), Eq(y1, 7*z(t) - 3*x(t)), Eq(z1, 11*x(t) - 7*y(t))) + answer_6 = {'no_of_equation': 3, 'eq': (Eq(Derivative(x(t), t), 3*y(t) - 11*z(t)), Eq(Derivative(y(t), t), -3*x(t) + 7*z(t)), + Eq(Derivative(z(t), t), 11*x(t) - 7*y(t))), 'func': [x(t), y(t), z(t)], 'order': {x(t): 1, y(t): 1, z(t): 1}, + 'is_linear': True, 'is_constant': True, 'is_homogeneous': True, + 'func_coeff': -Matrix([ + [ 0, -3, 11], + [ 3, 0, -7], + [-11, 7, 0]]), + 'type_of_equation': 'type1', 'is_general': True} + + assert _classify_linear_system(eqs_6, funcs_2[:-1], t) == answer_6 + + eqs_7 = (Eq(x1, y(t)), Eq(y1, x(t))) + answer_7 = {'no_of_equation': 2, 'eq': (Eq(Derivative(x(t), t), y(t)), Eq(Derivative(y(t), t), x(t))), + 'func': [x(t), y(t)], 'order': {x(t): 1, y(t): 1}, 'is_linear': True, 'is_constant': True, + 'is_homogeneous': True, 'func_coeff': -Matrix([ + [ 0, -1], + [-1, 0]]), + 'type_of_equation': 'type1', 'is_general': True} + assert _classify_linear_system(eqs_7, funcs, t) == answer_7 + + eqs_8 = (Eq(x1, 21*x(t)), Eq(y1, 17*x(t) + 3*y(t)), Eq(z1, 5*x(t) + 7*y(t) + 9*z(t))) + answer_8 = {'no_of_equation': 3, 'eq': (Eq(Derivative(x(t), t), 21*x(t)), Eq(Derivative(y(t), t), 17*x(t) + 3*y(t)), + Eq(Derivative(z(t), t), 5*x(t) + 7*y(t) + 9*z(t))), 'func': [x(t), y(t), z(t)], 'order': {x(t): 1, y(t): 1, z(t): 1}, + 'is_linear': True, 'is_constant': True, 'is_homogeneous': True, + 'func_coeff': -Matrix([ + [-21, 0, 0], + [-17, -3, 0], + [ -5, -7, -9]]), + 'type_of_equation': 'type1', 'is_general': True} + + assert _classify_linear_system(eqs_8, funcs_2[:-1], t) == answer_8 + + eqs_9 = (Eq(x1, 4*x(t) + 5*y(t) + 2*z(t)), Eq(y1, x(t) + 13*y(t) + 9*z(t)), Eq(z1, 32*x(t) + 41*y(t) + 11*z(t))) + answer_9 = {'no_of_equation': 3, 'eq': (Eq(Derivative(x(t), t), 4*x(t) + 5*y(t) + 2*z(t)), + Eq(Derivative(y(t), t), x(t) + 13*y(t) + 9*z(t)), Eq(Derivative(z(t), t), 32*x(t) + 41*y(t) + 11*z(t))), + 'func': [x(t), y(t), z(t)], 'order': {x(t): 1, y(t): 1, z(t): 1}, 'is_linear': True, + 'is_constant': True, 'is_homogeneous': True, + 'func_coeff': -Matrix([ + [ -4, -5, -2], + [ -1, -13, -9], + [-32, -41, -11]]), + 'type_of_equation': 'type1', 'is_general': True} + assert _classify_linear_system(eqs_9, funcs_2[:-1], t) == answer_9 + + eqs_10 = (Eq(3*x1, 4*5*(y(t) - z(t))), Eq(4*y1, 3*5*(z(t) - x(t))), Eq(5*z1, 3*4*(x(t) - y(t)))) + answer_10 = {'no_of_equation': 3, 'eq': (Eq(3*Derivative(x(t), t), 20*y(t) - 20*z(t)), + Eq(4*Derivative(y(t), t), -15*x(t) + 15*z(t)), Eq(5*Derivative(z(t), t), 12*x(t) - 12*y(t))), + 'func': [x(t), y(t), z(t)], 'order': {x(t): 1, y(t): 1, z(t): 1}, 'is_linear': True, + 'is_constant': True, 'is_homogeneous': True, + 'func_coeff': -Matrix([ + [ 0, Rational(-20, 3), Rational(20, 3)], + [Rational(15, 4), 0, Rational(-15, 4)], + [Rational(-12, 5), Rational(12, 5), 0]]), + 'type_of_equation': 'type1', 'is_general': True} + assert _classify_linear_system(eqs_10, funcs_2[:-1], t) == answer_10 + + eq11 = (Eq(x1, 3*y(t) - 11*z(t)), Eq(y1, 7*z(t) - 3*x(t)), Eq(z1, 11*x(t) - 7*y(t))) + sol11 = {'no_of_equation': 3, 'eq': (Eq(Derivative(x(t), t), 3*y(t) - 11*z(t)), Eq(Derivative(y(t), t), -3*x(t) + 7*z(t)), + Eq(Derivative(z(t), t), 11*x(t) - 7*y(t))), 'func': [x(t), y(t), z(t)], 'order': {x(t): 1, y(t): 1, z(t): 1}, + 'is_linear': True, 'is_constant': True, 'is_homogeneous': True, 'func_coeff': -Matrix([ + [ 0, -3, 11], [ 3, 0, -7], [-11, 7, 0]]), 'type_of_equation': 'type1', 'is_general': True} + assert _classify_linear_system(eq11, funcs_2[:-1], t) == sol11 + + eq12 = (Eq(Derivative(x(t), t), y(t)), Eq(Derivative(y(t), t), x(t))) + sol12 = {'no_of_equation': 2, 'eq': (Eq(Derivative(x(t), t), y(t)), Eq(Derivative(y(t), t), x(t))), + 'func': [x(t), y(t)], 'order': {x(t): 1, y(t): 1}, 'is_linear': True, 'is_constant': True, + 'is_homogeneous': True, 'func_coeff': -Matrix([ + [0, -1], + [-1, 0]]), 'type_of_equation': 'type1', 'is_general': True} + assert _classify_linear_system(eq12, [x(t), y(t)], t) == sol12 + + eq13 = (Eq(Derivative(x(t), t), 21*x(t)), Eq(Derivative(y(t), t), 17*x(t) + 3*y(t)), + Eq(Derivative(z(t), t), 5*x(t) + 7*y(t) + 9*z(t))) + sol13 = {'no_of_equation': 3, 'eq': ( + Eq(Derivative(x(t), t), 21 * x(t)), Eq(Derivative(y(t), t), 17 * x(t) + 3 * y(t)), + Eq(Derivative(z(t), t), 5 * x(t) + 7 * y(t) + 9 * z(t))), 'func': [x(t), y(t), z(t)], + 'order': {x(t): 1, y(t): 1, z(t): 1}, 'is_linear': True, 'is_constant': True, 'is_homogeneous': True, + 'func_coeff': -Matrix([ + [-21, 0, 0], + [-17, -3, 0], + [-5, -7, -9]]), 'type_of_equation': 'type1', 'is_general': True} + assert _classify_linear_system(eq13, [x(t), y(t), z(t)], t) == sol13 + + eq14 = ( + Eq(Derivative(x(t), t), 4*x(t) + 5*y(t) + 2*z(t)), Eq(Derivative(y(t), t), x(t) + 13*y(t) + 9*z(t)), + Eq(Derivative(z(t), t), 32*x(t) + 41*y(t) + 11*z(t))) + sol14 = {'no_of_equation': 3, 'eq': ( + Eq(Derivative(x(t), t), 4 * x(t) + 5 * y(t) + 2 * z(t)), Eq(Derivative(y(t), t), x(t) + 13 * y(t) + 9 * z(t)), + Eq(Derivative(z(t), t), 32 * x(t) + 41 * y(t) + 11 * z(t))), 'func': [x(t), y(t), z(t)], + 'order': {x(t): 1, y(t): 1, z(t): 1}, 'is_linear': True, 'is_constant': True, 'is_homogeneous': True, + 'func_coeff': -Matrix([ + [-4, -5, -2], + [-1, -13, -9], + [-32, -41, -11]]), 'type_of_equation': 'type1', 'is_general': True} + assert _classify_linear_system(eq14, [x(t), y(t), z(t)], t) == sol14 + + eq15 = (Eq(3*Derivative(x(t), t), 20*y(t) - 20*z(t)), Eq(4*Derivative(y(t), t), -15*x(t) + 15*z(t)), + Eq(5*Derivative(z(t), t), 12*x(t) - 12*y(t))) + sol15 = {'no_of_equation': 3, 'eq': ( + Eq(3 * Derivative(x(t), t), 20 * y(t) - 20 * z(t)), Eq(4 * Derivative(y(t), t), -15 * x(t) + 15 * z(t)), + Eq(5 * Derivative(z(t), t), 12 * x(t) - 12 * y(t))), 'func': [x(t), y(t), z(t)], + 'order': {x(t): 1, y(t): 1, z(t): 1}, 'is_linear': True, 'is_constant': True, 'is_homogeneous': True, + 'func_coeff': -Matrix([ + [0, Rational(-20, 3), Rational(20, 3)], + [Rational(15, 4), 0, Rational(-15, 4)], + [Rational(-12, 5), Rational(12, 5), 0]]), 'type_of_equation': 'type1', 'is_general': True} + assert _classify_linear_system(eq15, [x(t), y(t), z(t)], t) == sol15 + + # Constant coefficient homogeneous ODEs + eq1 = (Eq(diff(x(t), t), x(t) + y(t) + 9), Eq(diff(y(t), t), 2*x(t) + 5*y(t) + 23)) + sol1 = {'no_of_equation': 2, 'eq': (Eq(Derivative(x(t), t), x(t) + y(t) + 9), + Eq(Derivative(y(t), t), 2*x(t) + 5*y(t) + 23)), 'func': [x(t), y(t)], + 'order': {x(t): 1, y(t): 1}, 'is_linear': True, 'is_constant': True, 'is_homogeneous': False, 'is_general': True, + 'func_coeff': -Matrix([[-1, -1], [-2, -5]]), 'rhs': Matrix([[ 9], [23]]), 'type_of_equation': 'type2'} + assert _classify_linear_system(eq1, funcs, t) == sol1 + + # Non constant coefficient homogeneous ODEs + eq1 = (Eq(diff(x(t), t), 5*t*x(t) + 2*y(t)), Eq(diff(y(t), t), 2*x(t) + 5*t*y(t))) + sol1 = {'no_of_equation': 2, 'eq': (Eq(Derivative(x(t), t), 5*t*x(t) + 2*y(t)), Eq(Derivative(y(t), t), 5*t*y(t) + 2*x(t))), + 'func': [x(t), y(t)], 'order': {x(t): 1, y(t): 1}, 'is_linear': True, 'is_constant': False, + 'is_homogeneous': True, 'func_coeff': -Matrix([ [-5*t, -2], [ -2, -5*t]]), 'commutative_antiderivative': Matrix([ + [5*t**2/2, 2*t], [ 2*t, 5*t**2/2]]), 'type_of_equation': 'type3', 'is_general': True} + assert _classify_linear_system(eq1, funcs, t) == sol1 + + # Non constant coefficient non-homogeneous ODEs + eq1 = [Eq(x1, x(t) + t*y(t) + t), Eq(y1, t*x(t) + y(t))] + sol1 = {'no_of_equation': 2, 'eq': [Eq(Derivative(x(t), t), t*y(t) + t + x(t)), Eq(Derivative(y(t), t), + t*x(t) + y(t))], 'func': [x(t), y(t)], 'order': {x(t): 1, y(t): 1}, 'is_linear': True, + 'is_constant': False, 'is_homogeneous': False, 'is_general': True, 'func_coeff': -Matrix([ [-1, -t], + [-t, -1]]), 'commutative_antiderivative': Matrix([ [ t, t**2/2], [t**2/2, t]]), 'rhs': + Matrix([ [t], [0]]), 'type_of_equation': 'type4'} + assert _classify_linear_system(eq1, funcs, t) == sol1 + + eq2 = [Eq(x1, t*x(t) + t*y(t) + t), Eq(y1, t*x(t) + t*y(t) + cos(t))] + sol2 = {'no_of_equation': 2, 'eq': [Eq(Derivative(x(t), t), t*x(t) + t*y(t) + t), Eq(Derivative(y(t), t), + t*x(t) + t*y(t) + cos(t))], 'func': [x(t), y(t)], 'order': {x(t): 1, y(t): 1}, 'is_linear': True, + 'is_homogeneous': False, 'is_general': True, 'rhs': Matrix([ [ t], [cos(t)]]), 'func_coeff': + Matrix([ [t, t], [t, t]]), 'is_constant': False, 'type_of_equation': 'type4', + 'commutative_antiderivative': Matrix([ [t**2/2, t**2/2], [t**2/2, t**2/2]])} + assert _classify_linear_system(eq2, funcs, t) == sol2 + + eq3 = [Eq(x1, t*(x(t) + y(t) + z(t) + 1)), Eq(y1, t*(x(t) + y(t) + z(t))), Eq(z1, t*(x(t) + y(t) + z(t)))] + sol3 = {'no_of_equation': 3, 'eq': [Eq(Derivative(x(t), t), t*(x(t) + y(t) + z(t) + 1)), + Eq(Derivative(y(t), t), t*(x(t) + y(t) + z(t))), Eq(Derivative(z(t), t), t*(x(t) + y(t) + z(t)))], + 'func': [x(t), y(t), z(t)], 'order': {x(t): 1, y(t): 1, z(t): 1}, 'is_linear': True, 'is_constant': + False, 'is_homogeneous': False, 'is_general': True, 'func_coeff': -Matrix([ [-t, -t, -t], [-t, -t, + -t], [-t, -t, -t]]), 'commutative_antiderivative': Matrix([ [t**2/2, t**2/2, t**2/2], [t**2/2, + t**2/2, t**2/2], [t**2/2, t**2/2, t**2/2]]), 'rhs': Matrix([ [t], [0], [0]]), 'type_of_equation': + 'type4'} + assert _classify_linear_system(eq3, funcs_2[:-1], t) == sol3 + + eq4 = [Eq(x1, x(t) + y(t) + t*z(t) + 1), Eq(y1, x(t) + t*y(t) + z(t) + 10), Eq(z1, t*x(t) + y(t) + z(t) + t)] + sol4 = {'no_of_equation': 3, 'eq': [Eq(Derivative(x(t), t), t*z(t) + x(t) + y(t) + 1), Eq(Derivative(y(t), + t), t*y(t) + x(t) + z(t) + 10), Eq(Derivative(z(t), t), t*x(t) + t + y(t) + z(t))], 'func': [x(t), + y(t), z(t)], 'order': {x(t): 1, y(t): 1, z(t): 1}, 'is_linear': True, 'is_constant': False, + 'is_homogeneous': False, 'is_general': True, 'func_coeff': -Matrix([ [-1, -1, -t], [-1, -t, -1], [-t, + -1, -1]]), 'commutative_antiderivative': Matrix([ [ t, t, t**2/2], [ t, t**2/2, + t], [t**2/2, t, t]]), 'rhs': Matrix([ [ 1], [10], [ t]]), 'type_of_equation': 'type4'} + assert _classify_linear_system(eq4, funcs_2[:-1], t) == sol4 + + sum_terms = t*(x(t) + y(t) + z(t) + w(t)) + eq5 = [Eq(x1, sum_terms), Eq(y1, sum_terms), Eq(z1, sum_terms + 1), Eq(w1, sum_terms)] + sol5 = {'no_of_equation': 4, 'eq': [Eq(Derivative(x(t), t), t*(w(t) + x(t) + y(t) + z(t))), + Eq(Derivative(y(t), t), t*(w(t) + x(t) + y(t) + z(t))), Eq(Derivative(z(t), t), t*(w(t) + x(t) + + y(t) + z(t)) + 1), Eq(Derivative(w(t), t), t*(w(t) + x(t) + y(t) + z(t)))], 'func': [x(t), y(t), + z(t), w(t)], 'order': {x(t): 1, y(t): 1, z(t): 1, w(t): 1}, 'is_linear': True, 'is_constant': False, + 'is_homogeneous': False, 'is_general': True, 'func_coeff': -Matrix([ [-t, -t, -t, -t], [-t, -t, -t, + -t], [-t, -t, -t, -t], [-t, -t, -t, -t]]), 'commutative_antiderivative': Matrix([ [t**2/2, t**2/2, + t**2/2, t**2/2], [t**2/2, t**2/2, t**2/2, t**2/2], [t**2/2, t**2/2, t**2/2, t**2/2], [t**2/2, + t**2/2, t**2/2, t**2/2]]), 'rhs': Matrix([ [0], [0], [1], [0]]), 'type_of_equation': 'type4'} + assert _classify_linear_system(eq5, funcs_2, t) == sol5 + + # Second Order + t_ = symbols("t_") + + eq1 = (Eq(9*x(t) + 7*y(t) + 4*Derivative(x(t), t) + Derivative(x(t), (t, 2)) + 3*Derivative(y(t), t), 11*exp(I*t)), + Eq(3*x(t) + 12*y(t) + 5*Derivative(x(t), t) + 8*Derivative(y(t), t) + Derivative(y(t), (t, 2)), 2*exp(I*t))) + sol1 = {'no_of_equation': 2, 'eq': (Eq(9*x(t) + 7*y(t) + 4*Derivative(x(t), t) + Derivative(x(t), (t, 2)) + + 3*Derivative(y(t), t), 11*exp(I*t)), Eq(3*x(t) + 12*y(t) + 5*Derivative(x(t), t) + + 8*Derivative(y(t), t) + Derivative(y(t), (t, 2)), 2*exp(I*t))), 'func': [x(t), y(t)], 'order': + {x(t): 2, y(t): 2}, 'is_linear': True, 'is_homogeneous': False, 'is_general': True, 'rhs': Matrix([ + [11*exp(I*t)], [ 2*exp(I*t)]]), 'type_of_equation': 'type0', 'is_second_order': True, + 'is_higher_order': True} + assert _classify_linear_system(eq1, funcs, t) == sol1 + + eq2 = (Eq((4*t**2 + 7*t + 1)**2*Derivative(x(t), (t, 2)), 5*x(t) + 35*y(t)), + Eq((4*t**2 + 7*t + 1)**2*Derivative(y(t), (t, 2)), x(t) + 9*y(t))) + sol2 = {'no_of_equation': 2, 'eq': (Eq((4*t**2 + 7*t + 1)**2*Derivative(x(t), (t, 2)), 5*x(t) + 35*y(t)), + Eq((4*t**2 + 7*t + 1)**2*Derivative(y(t), (t, 2)), x(t) + 9*y(t))), 'func': [x(t), y(t)], 'order': + {x(t): 2, y(t): 2}, 'is_linear': True, 'is_homogeneous': True, 'is_general': True, + 'type_of_equation': 'type2', 'A0': Matrix([ [Rational(53, 4), 35], [ 1, Rational(69, 4)]]), 'g(t)': sqrt(4*t**2 + 7*t + + 1), 'tau': sqrt(33)*log(t - sqrt(33)/8 + Rational(7, 8))/33 - sqrt(33)*log(t + sqrt(33)/8 + Rational(7, 8))/33, + 'is_transformed': True, 't_': t_, 'is_second_order': True, 'is_higher_order': True} + assert _classify_linear_system(eq2, funcs, t) == sol2 + + eq3 = ((t*Derivative(x(t), t) - x(t))*log(t) + (t*Derivative(y(t), t) - y(t))*exp(t) + Derivative(x(t), (t, 2)), + t**2*(t*Derivative(x(t), t) - x(t)) + t*(t*Derivative(y(t), t) - y(t)) + Derivative(y(t), (t, 2))) + sol3 = {'no_of_equation': 2, 'eq': ((t*Derivative(x(t), t) - x(t))*log(t) + (t*Derivative(y(t), t) - + y(t))*exp(t) + Derivative(x(t), (t, 2)), t**2*(t*Derivative(x(t), t) - x(t)) + t*(t*Derivative(y(t), + t) - y(t)) + Derivative(y(t), (t, 2))), 'func': [x(t), y(t)], 'order': {x(t): 2, y(t): 2}, + 'is_linear': True, 'is_homogeneous': True, 'is_general': True, 'type_of_equation': 'type1', 'A1': + Matrix([ [-t*log(t), -t*exp(t)], [ -t**3, -t**2]]), 'is_second_order': True, + 'is_higher_order': True} + assert _classify_linear_system(eq3, funcs, t) == sol3 + + eq4 = (Eq(x2, k*x(t) - l*y1), Eq(y2, l*x1 + k*y(t))) + sol4 = {'no_of_equation': 2, 'eq': (Eq(Derivative(x(t), (t, 2)), k*x(t) - l*Derivative(y(t), t)), + Eq(Derivative(y(t), (t, 2)), k*y(t) + l*Derivative(x(t), t))), 'func': [x(t), y(t)], 'order': {x(t): + 2, y(t): 2}, 'is_linear': True, 'is_homogeneous': True, 'is_general': True, 'type_of_equation': + 'type0', 'is_second_order': True, 'is_higher_order': True} + assert _classify_linear_system(eq4, funcs, t) == sol4 + + + # Multiple matches + + f, g = symbols("f g", cls=Function) + y, t_ = symbols("y t_") + funcs = [f(t), g(t)] + + eq1 = [Eq(Derivative(f(t), t)**2 - 2*Derivative(f(t), t) + 1, 4), + Eq(-y*f(t) + Derivative(g(t), t), 0)] + sol1 = {'is_implicit': True, + 'canon_eqs': [[Eq(Derivative(f(t), t), -1), Eq(Derivative(g(t), t), y*f(t))], + [Eq(Derivative(f(t), t), 3), Eq(Derivative(g(t), t), y*f(t))]]} + assert _classify_linear_system(eq1, funcs, t) == sol1 + + raises(ValueError, lambda: _classify_linear_system(eq1, funcs[:1], t)) + + eq2 = [Eq(Derivative(f(t), t), (2*f(t) + g(t) + 1)/t), Eq(Derivative(g(t), t), (f(t) + 2*g(t))/t)] + sol2 = {'no_of_equation': 2, 'eq': [Eq(Derivative(f(t), t), (2*f(t) + g(t) + 1)/t), Eq(Derivative(g(t), t), + (f(t) + 2*g(t))/t)], 'func': [f(t), g(t)], 'order': {f(t): 1, g(t): 1}, 'is_linear': True, + 'is_homogeneous': False, 'is_general': True, 'rhs': Matrix([ [1], [0]]), 'func_coeff': Matrix([ [2, + 1], [1, 2]]), 'is_constant': False, 'type_of_equation': 'type6', 't_': t_, 'tau': log(t), + 'commutative_antiderivative': Matrix([ [2*log(t), log(t)], [ log(t), 2*log(t)]])} + assert _classify_linear_system(eq2, funcs, t) == sol2 + + eq3 = [Eq(Derivative(f(t), t), (2*f(t) + g(t))/t), Eq(Derivative(g(t), t), (f(t) + 2*g(t))/t)] + sol3 = {'no_of_equation': 2, 'eq': [Eq(Derivative(f(t), t), (2*f(t) + g(t))/t), Eq(Derivative(g(t), t), + (f(t) + 2*g(t))/t)], 'func': [f(t), g(t)], 'order': {f(t): 1, g(t): 1}, 'is_linear': True, + 'is_homogeneous': True, 'is_general': True, 'func_coeff': Matrix([ [2, 1], [1, 2]]), 'is_constant': + False, 'type_of_equation': 'type5', 't_': t_, 'rhs': Matrix([ [0], [0]]), 'tau': log(t), + 'commutative_antiderivative': Matrix([ [2*log(t), log(t)], [ log(t), 2*log(t)]])} + assert _classify_linear_system(eq3, funcs, t) == sol3 + + +def test_matrix_exp(): + from sympy.matrices.dense import Matrix, eye, zeros + from sympy.solvers.ode.systems import matrix_exp + t = Symbol('t') + + for n in range(1, 6+1): + assert matrix_exp(zeros(n), t) == eye(n) + + for n in range(1, 6+1): + A = eye(n) + expAt = exp(t) * eye(n) + assert matrix_exp(A, t) == expAt + + for n in range(1, 6+1): + A = Matrix(n, n, lambda i,j: i+1 if i==j else 0) + expAt = Matrix(n, n, lambda i,j: exp((i+1)*t) if i==j else 0) + assert matrix_exp(A, t) == expAt + + A = Matrix([[0, 1], [-1, 0]]) + expAt = Matrix([[cos(t), sin(t)], [-sin(t), cos(t)]]) + assert matrix_exp(A, t) == expAt + + A = Matrix([[2, -5], [2, -4]]) + expAt = Matrix([ + [3*exp(-t)*sin(t) + exp(-t)*cos(t), -5*exp(-t)*sin(t)], + [2*exp(-t)*sin(t), -3*exp(-t)*sin(t) + exp(-t)*cos(t)] + ]) + assert matrix_exp(A, t) == expAt + + A = Matrix([[21, 17, 6], [-5, -1, -6], [4, 4, 16]]) + # TO update this. + # expAt = Matrix([ + # [(8*t*exp(12*t) + 5*exp(12*t) - 1)*exp(4*t)/4, + # (8*t*exp(12*t) + 5*exp(12*t) - 5)*exp(4*t)/4, + # (exp(12*t) - 1)*exp(4*t)/2], + # [(-8*t*exp(12*t) - exp(12*t) + 1)*exp(4*t)/4, + # (-8*t*exp(12*t) - exp(12*t) + 5)*exp(4*t)/4, + # (-exp(12*t) + 1)*exp(4*t)/2], + # [4*t*exp(16*t), 4*t*exp(16*t), exp(16*t)]]) + expAt = Matrix([ + [2*t*exp(16*t) + 5*exp(16*t)/4 - exp(4*t)/4, 2*t*exp(16*t) + 5*exp(16*t)/4 - 5*exp(4*t)/4, exp(16*t)/2 - exp(4*t)/2], + [ -2*t*exp(16*t) - exp(16*t)/4 + exp(4*t)/4, -2*t*exp(16*t) - exp(16*t)/4 + 5*exp(4*t)/4, -exp(16*t)/2 + exp(4*t)/2], + [ 4*t*exp(16*t), 4*t*exp(16*t), exp(16*t)] + ]) + assert matrix_exp(A, t) == expAt + + A = Matrix([[1, 1, 0, 0], + [0, 1, 1, 0], + [0, 0, 1, -S(1)/8], + [0, 0, S(1)/2, S(1)/2]]) + expAt = Matrix([ + [exp(t), t*exp(t), 4*t*exp(3*t/4) + 8*t*exp(t) + 48*exp(3*t/4) - 48*exp(t), + -2*t*exp(3*t/4) - 2*t*exp(t) - 16*exp(3*t/4) + 16*exp(t)], + [0, exp(t), -t*exp(3*t/4) - 8*exp(3*t/4) + 8*exp(t), t*exp(3*t/4)/2 + 2*exp(3*t/4) - 2*exp(t)], + [0, 0, t*exp(3*t/4)/4 + exp(3*t/4), -t*exp(3*t/4)/8], + [0, 0, t*exp(3*t/4)/2, -t*exp(3*t/4)/4 + exp(3*t/4)] + ]) + assert matrix_exp(A, t) == expAt + + A = Matrix([ + [ 0, 1, 0, 0], + [-1, 0, 0, 0], + [ 0, 0, 0, 1], + [ 0, 0, -1, 0]]) + + expAt = Matrix([ + [ cos(t), sin(t), 0, 0], + [-sin(t), cos(t), 0, 0], + [ 0, 0, cos(t), sin(t)], + [ 0, 0, -sin(t), cos(t)]]) + assert matrix_exp(A, t) == expAt + + A = Matrix([ + [ 0, 1, 1, 0], + [-1, 0, 0, 1], + [ 0, 0, 0, 1], + [ 0, 0, -1, 0]]) + + expAt = Matrix([ + [ cos(t), sin(t), t*cos(t), t*sin(t)], + [-sin(t), cos(t), -t*sin(t), t*cos(t)], + [ 0, 0, cos(t), sin(t)], + [ 0, 0, -sin(t), cos(t)]]) + assert matrix_exp(A, t) == expAt + + # This case is unacceptably slow right now but should be solvable... + #a, b, c, d, e, f = symbols('a b c d e f') + #A = Matrix([ + #[-a, b, c, d], + #[ a, -b, e, 0], + #[ 0, 0, -c - e - f, 0], + #[ 0, 0, f, -d]]) + + A = Matrix([[0, I], [I, 0]]) + expAt = Matrix([ + [exp(I*t)/2 + exp(-I*t)/2, exp(I*t)/2 - exp(-I*t)/2], + [exp(I*t)/2 - exp(-I*t)/2, exp(I*t)/2 + exp(-I*t)/2]]) + assert matrix_exp(A, t) == expAt + + # Testing Errors + M = Matrix([[1, 2, 3], [4, 5, 6], [7, 7, 7]]) + M1 = Matrix([[t, 1], [1, 1]]) + + raises(ValueError, lambda: matrix_exp(M[:, :2], t)) + raises(ValueError, lambda: matrix_exp(M[:2, :], t)) + raises(ValueError, lambda: matrix_exp(M1, t)) + raises(ValueError, lambda: matrix_exp(M1[:1, :1], t)) + + +def test_canonical_odes(): + f, g, h = symbols('f g h', cls=Function) + x = symbols('x') + funcs = [f(x), g(x), h(x)] + + eqs1 = [Eq(f(x).diff(x, x), f(x) + 2*g(x)), Eq(g(x) + 1, g(x).diff(x) + f(x))] + sol1 = [[Eq(Derivative(f(x), (x, 2)), f(x) + 2*g(x)), Eq(Derivative(g(x), x), -f(x) + g(x) + 1)]] + assert canonical_odes(eqs1, funcs[:2], x) == sol1 + + eqs2 = [Eq(f(x).diff(x), h(x).diff(x) + f(x)), Eq(g(x).diff(x)**2, f(x) + h(x)), Eq(h(x).diff(x), f(x))] + sol2 = [[Eq(Derivative(f(x), x), 2*f(x)), Eq(Derivative(g(x), x), -sqrt(f(x) + h(x))), Eq(Derivative(h(x), x), f(x))], + [Eq(Derivative(f(x), x), 2*f(x)), Eq(Derivative(g(x), x), sqrt(f(x) + h(x))), Eq(Derivative(h(x), x), f(x))]] + assert canonical_odes(eqs2, funcs, x) == sol2 + + +def test_sysode_linear_neq_order1_type1(): + + f, g, x, y, h = symbols('f g x y h', cls=Function) + a, b, c, t = symbols('a b c t') + + eqs1 = [Eq(Derivative(x(t), t), x(t)), + Eq(Derivative(y(t), t), y(t))] + sol1 = [Eq(x(t), C1*exp(t)), + Eq(y(t), C2*exp(t))] + assert dsolve(eqs1) == sol1 + assert checksysodesol(eqs1, sol1) == (True, [0, 0]) + + eqs2 = [Eq(Derivative(x(t), t), 2*x(t)), + Eq(Derivative(y(t), t), 3*y(t))] + sol2 = [Eq(x(t), C1*exp(2*t)), + Eq(y(t), C2*exp(3*t))] + assert dsolve(eqs2) == sol2 + assert checksysodesol(eqs2, sol2) == (True, [0, 0]) + + eqs3 = [Eq(Derivative(x(t), t), a*x(t)), + Eq(Derivative(y(t), t), a*y(t))] + sol3 = [Eq(x(t), C1*exp(a*t)), + Eq(y(t), C2*exp(a*t))] + assert dsolve(eqs3) == sol3 + assert checksysodesol(eqs3, sol3) == (True, [0, 0]) + + # Regression test case for issue #15474 + # https://github.com/sympy/sympy/issues/15474 + eqs4 = [Eq(Derivative(x(t), t), a*x(t)), + Eq(Derivative(y(t), t), b*y(t))] + sol4 = [Eq(x(t), C1*exp(a*t)), + Eq(y(t), C2*exp(b*t))] + assert dsolve(eqs4) == sol4 + assert checksysodesol(eqs4, sol4) == (True, [0, 0]) + + eqs5 = [Eq(Derivative(x(t), t), -y(t)), + Eq(Derivative(y(t), t), x(t))] + sol5 = [Eq(x(t), -C1*sin(t) - C2*cos(t)), + Eq(y(t), C1*cos(t) - C2*sin(t))] + assert dsolve(eqs5) == sol5 + assert checksysodesol(eqs5, sol5) == (True, [0, 0]) + + eqs6 = [Eq(Derivative(x(t), t), -2*y(t)), + Eq(Derivative(y(t), t), 2*x(t))] + sol6 = [Eq(x(t), -C1*sin(2*t) - C2*cos(2*t)), + Eq(y(t), C1*cos(2*t) - C2*sin(2*t))] + assert dsolve(eqs6) == sol6 + assert checksysodesol(eqs6, sol6) == (True, [0, 0]) + + eqs7 = [Eq(Derivative(x(t), t), I*y(t)), + Eq(Derivative(y(t), t), I*x(t))] + sol7 = [Eq(x(t), -C1*exp(-I*t) + C2*exp(I*t)), + Eq(y(t), C1*exp(-I*t) + C2*exp(I*t))] + assert dsolve(eqs7) == sol7 + assert checksysodesol(eqs7, sol7) == (True, [0, 0]) + + eqs8 = [Eq(Derivative(x(t), t), -a*y(t)), + Eq(Derivative(y(t), t), a*x(t))] + sol8 = [Eq(x(t), -I*C1*exp(-I*a*t) + I*C2*exp(I*a*t)), + Eq(y(t), C1*exp(-I*a*t) + C2*exp(I*a*t))] + assert dsolve(eqs8) == sol8 + assert checksysodesol(eqs8, sol8) == (True, [0, 0]) + + eqs9 = [Eq(Derivative(x(t), t), x(t) + y(t)), + Eq(Derivative(y(t), t), x(t) - y(t))] + sol9 = [Eq(x(t), C1*(1 - sqrt(2))*exp(-sqrt(2)*t) + C2*(1 + sqrt(2))*exp(sqrt(2)*t)), + Eq(y(t), C1*exp(-sqrt(2)*t) + C2*exp(sqrt(2)*t))] + assert dsolve(eqs9) == sol9 + assert checksysodesol(eqs9, sol9) == (True, [0, 0]) + + eqs10 = [Eq(Derivative(x(t), t), x(t) + y(t)), + Eq(Derivative(y(t), t), x(t) + y(t))] + sol10 = [Eq(x(t), -C1 + C2*exp(2*t)), + Eq(y(t), C1 + C2*exp(2*t))] + assert dsolve(eqs10) == sol10 + assert checksysodesol(eqs10, sol10) == (True, [0, 0]) + + eqs11 = [Eq(Derivative(x(t), t), 2*x(t) + y(t)), + Eq(Derivative(y(t), t), -x(t) + 2*y(t))] + sol11 = [Eq(x(t), C1*exp(2*t)*sin(t) + C2*exp(2*t)*cos(t)), + Eq(y(t), C1*exp(2*t)*cos(t) - C2*exp(2*t)*sin(t))] + assert dsolve(eqs11) == sol11 + assert checksysodesol(eqs11, sol11) == (True, [0, 0]) + + eqs12 = [Eq(Derivative(x(t), t), x(t) + 2*y(t)), + Eq(Derivative(y(t), t), 2*x(t) + y(t))] + sol12 = [Eq(x(t), -C1*exp(-t) + C2*exp(3*t)), + Eq(y(t), C1*exp(-t) + C2*exp(3*t))] + assert dsolve(eqs12) == sol12 + assert checksysodesol(eqs12, sol12) == (True, [0, 0]) + + eqs13 = [Eq(Derivative(x(t), t), 4*x(t) + y(t)), + Eq(Derivative(y(t), t), -x(t) + 2*y(t))] + sol13 = [Eq(x(t), C2*t*exp(3*t) + (C1 + C2)*exp(3*t)), + Eq(y(t), -C1*exp(3*t) - C2*t*exp(3*t))] + assert dsolve(eqs13) == sol13 + assert checksysodesol(eqs13, sol13) == (True, [0, 0]) + + eqs14 = [Eq(Derivative(x(t), t), a*y(t)), + Eq(Derivative(y(t), t), a*x(t))] + sol14 = [Eq(x(t), -C1*exp(-a*t) + C2*exp(a*t)), + Eq(y(t), C1*exp(-a*t) + C2*exp(a*t))] + assert dsolve(eqs14) == sol14 + assert checksysodesol(eqs14, sol14) == (True, [0, 0]) + + eqs15 = [Eq(Derivative(x(t), t), a*y(t)), + Eq(Derivative(y(t), t), b*x(t))] + sol15 = [Eq(x(t), -C1*a*exp(-t*sqrt(a*b))/sqrt(a*b) + C2*a*exp(t*sqrt(a*b))/sqrt(a*b)), + Eq(y(t), C1*exp(-t*sqrt(a*b)) + C2*exp(t*sqrt(a*b)))] + assert dsolve(eqs15) == sol15 + assert checksysodesol(eqs15, sol15) == (True, [0, 0]) + + eqs16 = [Eq(Derivative(x(t), t), a*x(t) + b*y(t)), + Eq(Derivative(y(t), t), c*x(t))] + sol16 = [Eq(x(t), -2*C1*b*exp(t*(a + sqrt(a**2 + 4*b*c))/2)/(a - sqrt(a**2 + 4*b*c)) - 2*C2*b*exp(t*(a - + sqrt(a**2 + 4*b*c))/2)/(a + sqrt(a**2 + 4*b*c))), + Eq(y(t), C1*exp(t*(a + sqrt(a**2 + 4*b*c))/2) + C2*exp(t*(a - sqrt(a**2 + 4*b*c))/2))] + assert dsolve(eqs16) == sol16 + assert checksysodesol(eqs16, sol16) == (True, [0, 0]) + + # Regression test case for issue #18562 + # https://github.com/sympy/sympy/issues/18562 + eqs17 = [Eq(Derivative(x(t), t), a*y(t) + x(t)), + Eq(Derivative(y(t), t), a*x(t) - y(t))] + sol17 = [Eq(x(t), C1*a*exp(t*sqrt(a**2 + 1))/(sqrt(a**2 + 1) - 1) - C2*a*exp(-t*sqrt(a**2 + 1))/(sqrt(a**2 + + 1) + 1)), + Eq(y(t), C1*exp(t*sqrt(a**2 + 1)) + C2*exp(-t*sqrt(a**2 + 1)))] + assert dsolve(eqs17) == sol17 + assert checksysodesol(eqs17, sol17) == (True, [0, 0]) + + eqs18 = [Eq(Derivative(x(t), t), 0), + Eq(Derivative(y(t), t), 0)] + sol18 = [Eq(x(t), C1), + Eq(y(t), C2)] + assert dsolve(eqs18) == sol18 + assert checksysodesol(eqs18, sol18) == (True, [0, 0]) + + eqs19 = [Eq(Derivative(x(t), t), 2*x(t) - y(t)), + Eq(Derivative(y(t), t), x(t))] + sol19 = [Eq(x(t), C2*t*exp(t) + (C1 + C2)*exp(t)), + Eq(y(t), C1*exp(t) + C2*t*exp(t))] + assert dsolve(eqs19) == sol19 + assert checksysodesol(eqs19, sol19) == (True, [0, 0]) + + eqs20 = [Eq(Derivative(x(t), t), x(t)), + Eq(Derivative(y(t), t), x(t) + y(t))] + sol20 = [Eq(x(t), C1*exp(t)), + Eq(y(t), C1*t*exp(t) + C2*exp(t))] + assert dsolve(eqs20) == sol20 + assert checksysodesol(eqs20, sol20) == (True, [0, 0]) + + eqs21 = [Eq(Derivative(x(t), t), 3*x(t)), + Eq(Derivative(y(t), t), x(t) + y(t))] + sol21 = [Eq(x(t), 2*C1*exp(3*t)), + Eq(y(t), C1*exp(3*t) + C2*exp(t))] + assert dsolve(eqs21) == sol21 + assert checksysodesol(eqs21, sol21) == (True, [0, 0]) + + eqs22 = [Eq(Derivative(x(t), t), 3*x(t)), + Eq(Derivative(y(t), t), y(t))] + sol22 = [Eq(x(t), C1*exp(3*t)), + Eq(y(t), C2*exp(t))] + assert dsolve(eqs22) == sol22 + assert checksysodesol(eqs22, sol22) == (True, [0, 0]) + + +@slow +def test_sysode_linear_neq_order1_type1_slow(): + + t = Symbol('t') + Z0 = Function('Z0') + Z1 = Function('Z1') + Z2 = Function('Z2') + Z3 = Function('Z3') + + k01, k10, k20, k21, k23, k30 = symbols('k01 k10 k20 k21 k23 k30') + + eqs1 = [Eq(Derivative(Z0(t), t), -k01*Z0(t) + k10*Z1(t) + k20*Z2(t) + k30*Z3(t)), + Eq(Derivative(Z1(t), t), k01*Z0(t) - k10*Z1(t) + k21*Z2(t)), + Eq(Derivative(Z2(t), t), (-k20 - k21 - k23)*Z2(t)), + Eq(Derivative(Z3(t), t), k23*Z2(t) - k30*Z3(t))] + sol1 = [Eq(Z0(t), C1*k10/k01 - C2*(k10 - k30)*exp(-k30*t)/(k01 + k10 - k30) - C3*(k10*(k20 + k21 - k30) - + k20**2 - k20*(k21 + k23 - k30) + k23*k30)*exp(-t*(k20 + k21 + k23))/(k23*(-k01 - k10 + k20 + k21 + + k23)) - C4*exp(-t*(k01 + k10))), + Eq(Z1(t), C1 - C2*k01*exp(-k30*t)/(k01 + k10 - k30) + C3*(-k01*(k20 + k21 - k30) + k20*k21 + k21**2 + + k21*(k23 - k30))*exp(-t*(k20 + k21 + k23))/(k23*(-k01 - k10 + k20 + k21 + k23)) + C4*exp(-t*(k01 + + k10))), + Eq(Z2(t), -C3*(k20 + k21 + k23 - k30)*exp(-t*(k20 + k21 + k23))/k23), + Eq(Z3(t), C2*exp(-k30*t) + C3*exp(-t*(k20 + k21 + k23)))] + assert dsolve(eqs1) == sol1 + assert checksysodesol(eqs1, sol1) == (True, [0, 0, 0, 0]) + + x, y, z, u, v, w = symbols('x y z u v w', cls=Function) + k2, k3 = symbols('k2 k3') + a_b, a_c = symbols('a_b a_c', real=True) + + eqs2 = [Eq(Derivative(z(t), t), k2*y(t)), + Eq(Derivative(x(t), t), k3*y(t)), + Eq(Derivative(y(t), t), (-k2 - k3)*y(t))] + sol2 = [Eq(z(t), C1 - C2*k2*exp(-t*(k2 + k3))/(k2 + k3)), + Eq(x(t), -C2*k3*exp(-t*(k2 + k3))/(k2 + k3) + C3), + Eq(y(t), C2*exp(-t*(k2 + k3)))] + assert dsolve(eqs2) == sol2 + assert checksysodesol(eqs2, sol2) == (True, [0, 0, 0]) + + eqs3 = [4*u(t) - v(t) - 2*w(t) + Derivative(u(t), t), + 2*u(t) + v(t) - 2*w(t) + Derivative(v(t), t), + 5*u(t) + v(t) - 3*w(t) + Derivative(w(t), t)] + sol3 = [Eq(u(t), C3*exp(-2*t) + (C1/2 + sqrt(3)*C2/6)*cos(sqrt(3)*t) + sin(sqrt(3)*t)*(sqrt(3)*C1/6 + + C2*Rational(-1, 2))), + Eq(v(t), (C1/2 + sqrt(3)*C2/6)*cos(sqrt(3)*t) + sin(sqrt(3)*t)*(sqrt(3)*C1/6 + C2*Rational(-1, 2))), + Eq(w(t), C1*cos(sqrt(3)*t) - C2*sin(sqrt(3)*t) + C3*exp(-2*t))] + assert dsolve(eqs3) == sol3 + assert checksysodesol(eqs3, sol3) == (True, [0, 0, 0]) + + eqs4 = [Eq(Derivative(x(t), t), w(t)*Rational(-2, 9) + 2*x(t) + y(t) + z(t)*Rational(-8, 9)), + Eq(Derivative(y(t), t), w(t)*Rational(4, 9) + 2*y(t) + z(t)*Rational(16, 9)), + Eq(Derivative(z(t), t), w(t)*Rational(-2, 9) + z(t)*Rational(37, 9)), + Eq(Derivative(w(t), t), w(t)*Rational(44, 9) + z(t)*Rational(-4, 9))] + sol4 = [Eq(x(t), C1*exp(2*t) + C2*t*exp(2*t)), + Eq(y(t), C2*exp(2*t) + 2*C3*exp(4*t)), + Eq(z(t), 2*C3*exp(4*t) + C4*exp(5*t)*Rational(-1, 4)), + Eq(w(t), C3*exp(4*t) + C4*exp(5*t))] + assert dsolve(eqs4) == sol4 + assert checksysodesol(eqs4, sol4) == (True, [0, 0, 0, 0]) + + # Regression test case for issue #15574 + # https://github.com/sympy/sympy/issues/15574 + eq5 = [Eq(x(t).diff(t), x(t)), Eq(y(t).diff(t), y(t)), Eq(z(t).diff(t), z(t)), Eq(w(t).diff(t), w(t))] + sol5 = [Eq(x(t), C1*exp(t)), Eq(y(t), C2*exp(t)), Eq(z(t), C3*exp(t)), Eq(w(t), C4*exp(t))] + assert dsolve(eq5) == sol5 + assert checksysodesol(eq5, sol5) == (True, [0, 0, 0, 0]) + + eqs6 = [Eq(Derivative(x(t), t), x(t) + y(t)), + Eq(Derivative(y(t), t), y(t) + z(t)), + Eq(Derivative(z(t), t), w(t)*Rational(-1, 8) + z(t)), + Eq(Derivative(w(t), t), w(t)/2 + z(t)/2)] + sol6 = [Eq(x(t), C1*exp(t) + C2*t*exp(t) + 4*C4*t*exp(t*Rational(3, 4)) + (4*C3 + 48*C4)*exp(t*Rational(3, + 4))), + Eq(y(t), C2*exp(t) - C4*t*exp(t*Rational(3, 4)) - (C3 + 8*C4)*exp(t*Rational(3, 4))), + Eq(z(t), C4*t*exp(t*Rational(3, 4))/4 + (C3/4 + C4)*exp(t*Rational(3, 4))), + Eq(w(t), C3*exp(t*Rational(3, 4))/2 + C4*t*exp(t*Rational(3, 4))/2)] + assert dsolve(eqs6) == sol6 + assert checksysodesol(eqs6, sol6) == (True, [0, 0, 0, 0]) + + # Regression test case for issue #15574 + # https://github.com/sympy/sympy/issues/15574 + eq7 = [Eq(Derivative(x(t), t), x(t)), Eq(Derivative(y(t), t), y(t)), Eq(Derivative(z(t), t), z(t)), + Eq(Derivative(w(t), t), w(t)), Eq(Derivative(u(t), t), u(t))] + sol7 = [Eq(x(t), C1*exp(t)), Eq(y(t), C2*exp(t)), Eq(z(t), C3*exp(t)), Eq(w(t), C4*exp(t)), + Eq(u(t), C5*exp(t))] + assert dsolve(eq7) == sol7 + assert checksysodesol(eq7, sol7) == (True, [0, 0, 0, 0, 0]) + + eqs8 = [Eq(Derivative(x(t), t), 2*x(t) + y(t)), + Eq(Derivative(y(t), t), 2*y(t)), + Eq(Derivative(z(t), t), 4*z(t)), + Eq(Derivative(w(t), t), u(t) + 5*w(t)), + Eq(Derivative(u(t), t), 5*u(t))] + sol8 = [Eq(x(t), C1*exp(2*t) + C2*t*exp(2*t)), + Eq(y(t), C2*exp(2*t)), + Eq(z(t), C3*exp(4*t)), + Eq(w(t), C4*exp(5*t) + C5*t*exp(5*t)), + Eq(u(t), C5*exp(5*t))] + assert dsolve(eqs8) == sol8 + assert checksysodesol(eqs8, sol8) == (True, [0, 0, 0, 0, 0]) + + # Regression test case for issue #15574 + # https://github.com/sympy/sympy/issues/15574 + eq9 = [Eq(Derivative(x(t), t), x(t)), Eq(Derivative(y(t), t), y(t)), Eq(Derivative(z(t), t), z(t))] + sol9 = [Eq(x(t), C1*exp(t)), Eq(y(t), C2*exp(t)), Eq(z(t), C3*exp(t))] + assert dsolve(eq9) == sol9 + assert checksysodesol(eq9, sol9) == (True, [0, 0, 0]) + + # Regression test case for issue #15407 + # https://github.com/sympy/sympy/issues/15407 + eqs10 = [Eq(Derivative(x(t), t), (-a_b - a_c)*x(t)), + Eq(Derivative(y(t), t), a_b*y(t)), + Eq(Derivative(z(t), t), a_c*x(t))] + sol10 = [Eq(x(t), -C1*(a_b + a_c)*exp(-t*(a_b + a_c))/a_c), + Eq(y(t), C2*exp(a_b*t)), + Eq(z(t), C1*exp(-t*(a_b + a_c)) + C3)] + assert dsolve(eqs10) == sol10 + assert checksysodesol(eqs10, sol10) == (True, [0, 0, 0]) + + # Regression test case for issue #14312 + # https://github.com/sympy/sympy/issues/14312 + eqs11 = [Eq(Derivative(x(t), t), k3*y(t)), + Eq(Derivative(y(t), t), (-k2 - k3)*y(t)), + Eq(Derivative(z(t), t), k2*y(t))] + sol11 = [Eq(x(t), C1 + C2*k3*exp(-t*(k2 + k3))/k2), + Eq(y(t), -C2*(k2 + k3)*exp(-t*(k2 + k3))/k2), + Eq(z(t), C2*exp(-t*(k2 + k3)) + C3)] + assert dsolve(eqs11) == sol11 + assert checksysodesol(eqs11, sol11) == (True, [0, 0, 0]) + + # Regression test case for issue #14312 + # https://github.com/sympy/sympy/issues/14312 + eqs12 = [Eq(Derivative(z(t), t), k2*y(t)), + Eq(Derivative(x(t), t), k3*y(t)), + Eq(Derivative(y(t), t), (-k2 - k3)*y(t))] + sol12 = [Eq(z(t), C1 - C2*k2*exp(-t*(k2 + k3))/(k2 + k3)), + Eq(x(t), -C2*k3*exp(-t*(k2 + k3))/(k2 + k3) + C3), + Eq(y(t), C2*exp(-t*(k2 + k3)))] + assert dsolve(eqs12) == sol12 + assert checksysodesol(eqs12, sol12) == (True, [0, 0, 0]) + + f, g, h = symbols('f, g, h', cls=Function) + a, b, c = symbols('a, b, c') + + # Regression test case for issue #15474 + # https://github.com/sympy/sympy/issues/15474 + eqs13 = [Eq(Derivative(f(t), t), 2*f(t) + g(t)), + Eq(Derivative(g(t), t), a*f(t))] + sol13 = [Eq(f(t), C1*exp(t*(sqrt(a + 1) + 1))/(sqrt(a + 1) - 1) - C2*exp(-t*(sqrt(a + 1) - 1))/(sqrt(a + 1) + + 1)), + Eq(g(t), C1*exp(t*(sqrt(a + 1) + 1)) + C2*exp(-t*(sqrt(a + 1) - 1)))] + assert dsolve(eqs13) == sol13 + assert checksysodesol(eqs13, sol13) == (True, [0, 0]) + + eqs14 = [Eq(Derivative(f(t), t), 2*g(t) - 3*h(t)), + Eq(Derivative(g(t), t), -2*f(t) + 4*h(t)), + Eq(Derivative(h(t), t), 3*f(t) - 4*g(t))] + sol14 = [Eq(f(t), 2*C1 - sin(sqrt(29)*t)*(sqrt(29)*C2*Rational(3, 25) + C3*Rational(-8, 25)) - + cos(sqrt(29)*t)*(C2*Rational(8, 25) + sqrt(29)*C3*Rational(3, 25))), + Eq(g(t), C1*Rational(3, 2) + sin(sqrt(29)*t)*(sqrt(29)*C2*Rational(4, 25) + C3*Rational(6, 25)) - + cos(sqrt(29)*t)*(C2*Rational(6, 25) + sqrt(29)*C3*Rational(-4, 25))), + Eq(h(t), C1 + C2*cos(sqrt(29)*t) - C3*sin(sqrt(29)*t))] + assert dsolve(eqs14) == sol14 + assert checksysodesol(eqs14, sol14) == (True, [0, 0, 0]) + + eqs15 = [Eq(2*Derivative(f(t), t), 12*g(t) - 12*h(t)), + Eq(3*Derivative(g(t), t), -8*f(t) + 8*h(t)), + Eq(4*Derivative(h(t), t), 6*f(t) - 6*g(t))] + sol15 = [Eq(f(t), C1 - sin(sqrt(29)*t)*(sqrt(29)*C2*Rational(6, 13) + C3*Rational(-16, 13)) - + cos(sqrt(29)*t)*(C2*Rational(16, 13) + sqrt(29)*C3*Rational(6, 13))), + Eq(g(t), C1 + sin(sqrt(29)*t)*(sqrt(29)*C2*Rational(8, 39) + C3*Rational(16, 13)) - + cos(sqrt(29)*t)*(C2*Rational(16, 13) + sqrt(29)*C3*Rational(-8, 39))), + Eq(h(t), C1 + C2*cos(sqrt(29)*t) - C3*sin(sqrt(29)*t))] + assert dsolve(eqs15) == sol15 + assert checksysodesol(eqs15, sol15) == (True, [0, 0, 0]) + + eq16 = (Eq(diff(x(t), t), 21*x(t)), Eq(diff(y(t), t), 17*x(t) + 3*y(t)), + Eq(diff(z(t), t), 5*x(t) + 7*y(t) + 9*z(t))) + sol16 = [Eq(x(t), 216*C1*exp(21*t)/209), + Eq(y(t), 204*C1*exp(21*t)/209 - 6*C2*exp(3*t)/7), + Eq(z(t), C1*exp(21*t) + C2*exp(3*t) + C3*exp(9*t))] + assert dsolve(eq16) == sol16 + assert checksysodesol(eq16, sol16) == (True, [0, 0, 0]) + + eqs17 = [Eq(Derivative(x(t), t), 3*y(t) - 11*z(t)), + Eq(Derivative(y(t), t), -3*x(t) + 7*z(t)), + Eq(Derivative(z(t), t), 11*x(t) - 7*y(t))] + sol17 = [Eq(x(t), C1*Rational(7, 3) - sin(sqrt(179)*t)*(sqrt(179)*C2*Rational(11, 170) + C3*Rational(-21, + 170)) - cos(sqrt(179)*t)*(C2*Rational(21, 170) + sqrt(179)*C3*Rational(11, 170))), + Eq(y(t), C1*Rational(11, 3) + sin(sqrt(179)*t)*(sqrt(179)*C2*Rational(7, 170) + C3*Rational(33, + 170)) - cos(sqrt(179)*t)*(C2*Rational(33, 170) + sqrt(179)*C3*Rational(-7, 170))), + Eq(z(t), C1 + C2*cos(sqrt(179)*t) - C3*sin(sqrt(179)*t))] + assert dsolve(eqs17) == sol17 + assert checksysodesol(eqs17, sol17) == (True, [0, 0, 0]) + + eqs18 = [Eq(3*Derivative(x(t), t), 20*y(t) - 20*z(t)), + Eq(4*Derivative(y(t), t), -15*x(t) + 15*z(t)), + Eq(5*Derivative(z(t), t), 12*x(t) - 12*y(t))] + sol18 = [Eq(x(t), C1 - sin(5*sqrt(2)*t)*(sqrt(2)*C2*Rational(4, 3) - C3) - cos(5*sqrt(2)*t)*(C2 + + sqrt(2)*C3*Rational(4, 3))), + Eq(y(t), C1 + sin(5*sqrt(2)*t)*(sqrt(2)*C2*Rational(3, 4) + C3) - cos(5*sqrt(2)*t)*(C2 + + sqrt(2)*C3*Rational(-3, 4))), + Eq(z(t), C1 + C2*cos(5*sqrt(2)*t) - C3*sin(5*sqrt(2)*t))] + assert dsolve(eqs18) == sol18 + assert checksysodesol(eqs18, sol18) == (True, [0, 0, 0]) + + eqs19 = [Eq(Derivative(x(t), t), 4*x(t) - z(t)), + Eq(Derivative(y(t), t), 2*x(t) + 2*y(t) - z(t)), + Eq(Derivative(z(t), t), 3*x(t) + y(t))] + sol19 = [Eq(x(t), C2*t**2*exp(2*t)/2 + t*(2*C2 + C3)*exp(2*t) + (C1 + C2 + 2*C3)*exp(2*t)), + Eq(y(t), C2*t**2*exp(2*t)/2 + t*(2*C2 + C3)*exp(2*t) + (C1 + 2*C3)*exp(2*t)), + Eq(z(t), C2*t**2*exp(2*t) + t*(3*C2 + 2*C3)*exp(2*t) + (2*C1 + 3*C3)*exp(2*t))] + assert dsolve(eqs19) == sol19 + assert checksysodesol(eqs19, sol19) == (True, [0, 0, 0]) + + eqs20 = [Eq(Derivative(x(t), t), 4*x(t) - y(t) - 2*z(t)), + Eq(Derivative(y(t), t), 2*x(t) + y(t) - 2*z(t)), + Eq(Derivative(z(t), t), 5*x(t) - 3*z(t))] + sol20 = [Eq(x(t), C1*exp(2*t) - sin(t)*(C2*Rational(3, 5) + C3/5) - cos(t)*(C2/5 + C3*Rational(-3, 5))), + Eq(y(t), -sin(t)*(C2*Rational(3, 5) + C3/5) - cos(t)*(C2/5 + C3*Rational(-3, 5))), + Eq(z(t), C1*exp(2*t) - C2*sin(t) + C3*cos(t))] + assert dsolve(eqs20) == sol20 + assert checksysodesol(eqs20, sol20) == (True, [0, 0, 0]) + + eq21 = (Eq(diff(x(t), t), 9*y(t)), Eq(diff(y(t), t), 12*x(t))) + sol21 = [Eq(x(t), -sqrt(3)*C1*exp(-6*sqrt(3)*t)/2 + sqrt(3)*C2*exp(6*sqrt(3)*t)/2), + Eq(y(t), C1*exp(-6*sqrt(3)*t) + C2*exp(6*sqrt(3)*t))] + + assert dsolve(eq21) == sol21 + assert checksysodesol(eq21, sol21) == (True, [0, 0]) + + eqs22 = [Eq(Derivative(x(t), t), 2*x(t) + 4*y(t)), + Eq(Derivative(y(t), t), 12*x(t) + 41*y(t))] + sol22 = [Eq(x(t), C1*(39 - sqrt(1713))*exp(t*(sqrt(1713) + 43)/2)*Rational(-1, 24) + C2*(39 + + sqrt(1713))*exp(t*(43 - sqrt(1713))/2)*Rational(-1, 24)), + Eq(y(t), C1*exp(t*(sqrt(1713) + 43)/2) + C2*exp(t*(43 - sqrt(1713))/2))] + assert dsolve(eqs22) == sol22 + assert checksysodesol(eqs22, sol22) == (True, [0, 0]) + + eqs23 = [Eq(Derivative(x(t), t), x(t) + y(t)), + Eq(Derivative(y(t), t), -2*x(t) + 2*y(t))] + sol23 = [Eq(x(t), (C1/4 + sqrt(7)*C2/4)*cos(sqrt(7)*t/2)*exp(t*Rational(3, 2)) + + sin(sqrt(7)*t/2)*(sqrt(7)*C1/4 + C2*Rational(-1, 4))*exp(t*Rational(3, 2))), + Eq(y(t), C1*cos(sqrt(7)*t/2)*exp(t*Rational(3, 2)) - C2*sin(sqrt(7)*t/2)*exp(t*Rational(3, 2)))] + assert dsolve(eqs23) == sol23 + assert checksysodesol(eqs23, sol23) == (True, [0, 0]) + + # Regression test case for issue #15474 + # https://github.com/sympy/sympy/issues/15474 + a = Symbol("a", real=True) + eq24 = [x(t).diff(t) - a*y(t), y(t).diff(t) + a*x(t)] + sol24 = [Eq(x(t), C1*sin(a*t) + C2*cos(a*t)), Eq(y(t), C1*cos(a*t) - C2*sin(a*t))] + assert dsolve(eq24) == sol24 + assert checksysodesol(eq24, sol24) == (True, [0, 0]) + + # Regression test case for issue #19150 + # https://github.com/sympy/sympy/issues/19150 + eqs25 = [Eq(Derivative(f(t), t), 0), + Eq(Derivative(g(t), t), (f(t) - 2*g(t) + x(t))/(b*c)), + Eq(Derivative(x(t), t), (g(t) - 2*x(t) + y(t))/(b*c)), + Eq(Derivative(y(t), t), (h(t) + x(t) - 2*y(t))/(b*c)), + Eq(Derivative(h(t), t), 0)] + sol25 = [Eq(f(t), -3*C1 + 4*C2), + Eq(g(t), -2*C1 + 3*C2 - C3*exp(-2*t/(b*c)) + C4*exp(-t*(sqrt(2) + 2)/(b*c)) + C5*exp(-t*(2 - + sqrt(2))/(b*c))), + Eq(x(t), -C1 + 2*C2 - sqrt(2)*C4*exp(-t*(sqrt(2) + 2)/(b*c)) + sqrt(2)*C5*exp(-t*(2 - + sqrt(2))/(b*c))), + Eq(y(t), C2 + C3*exp(-2*t/(b*c)) + C4*exp(-t*(sqrt(2) + 2)/(b*c)) + C5*exp(-t*(2 - sqrt(2))/(b*c))), + Eq(h(t), C1)] + assert dsolve(eqs25) == sol25 + assert checksysodesol(eqs25, sol25) == (True, [0, 0, 0, 0, 0]) + + eq26 = [Eq(Derivative(f(t), t), 2*f(t)), Eq(Derivative(g(t), t), 3*f(t) + 7*g(t))] + sol26 = [Eq(f(t), -5*C1*exp(2*t)/3), Eq(g(t), C1*exp(2*t) + C2*exp(7*t))] + assert dsolve(eq26) == sol26 + assert checksysodesol(eq26, sol26) == (True, [0, 0]) + + eq27 = [Eq(Derivative(f(t), t), -9*I*f(t) - 4*g(t)), Eq(Derivative(g(t), t), -4*I*g(t))] + sol27 = [Eq(f(t), 4*I*C1*exp(-4*I*t)/5 + C2*exp(-9*I*t)), Eq(g(t), C1*exp(-4*I*t))] + assert dsolve(eq27) == sol27 + assert checksysodesol(eq27, sol27) == (True, [0, 0]) + + eq28 = [Eq(Derivative(f(t), t), -9*I*f(t)), Eq(Derivative(g(t), t), -4*I*g(t))] + sol28 = [Eq(f(t), C1*exp(-9*I*t)), Eq(g(t), C2*exp(-4*I*t))] + assert dsolve(eq28) == sol28 + assert checksysodesol(eq28, sol28) == (True, [0, 0]) + + eq29 = [Eq(Derivative(f(t), t), 0), Eq(Derivative(g(t), t), 0)] + sol29 = [Eq(f(t), C1), Eq(g(t), C2)] + assert dsolve(eq29) == sol29 + assert checksysodesol(eq29, sol29) == (True, [0, 0]) + + eq30 = [Eq(Derivative(f(t), t), f(t)), Eq(Derivative(g(t), t), 0)] + sol30 = [Eq(f(t), C1*exp(t)), Eq(g(t), C2)] + assert dsolve(eq30) == sol30 + assert checksysodesol(eq30, sol30) == (True, [0, 0]) + + eq31 = [Eq(Derivative(f(t), t), g(t)), Eq(Derivative(g(t), t), 0)] + sol31 = [Eq(f(t), C1 + C2*t), Eq(g(t), C2)] + assert dsolve(eq31) == sol31 + assert checksysodesol(eq31, sol31) == (True, [0, 0]) + + eq32 = [Eq(Derivative(f(t), t), 0), Eq(Derivative(g(t), t), f(t))] + sol32 = [Eq(f(t), C1), Eq(g(t), C1*t + C2)] + assert dsolve(eq32) == sol32 + assert checksysodesol(eq32, sol32) == (True, [0, 0]) + + eq33 = [Eq(Derivative(f(t), t), 0), Eq(Derivative(g(t), t), g(t))] + sol33 = [Eq(f(t), C1), Eq(g(t), C2*exp(t))] + assert dsolve(eq33) == sol33 + assert checksysodesol(eq33, sol33) == (True, [0, 0]) + + eq34 = [Eq(Derivative(f(t), t), f(t)), Eq(Derivative(g(t), t), I*g(t))] + sol34 = [Eq(f(t), C1*exp(t)), Eq(g(t), C2*exp(I*t))] + assert dsolve(eq34) == sol34 + assert checksysodesol(eq34, sol34) == (True, [0, 0]) + + eq35 = [Eq(Derivative(f(t), t), I*f(t)), Eq(Derivative(g(t), t), -I*g(t))] + sol35 = [Eq(f(t), C1*exp(I*t)), Eq(g(t), C2*exp(-I*t))] + assert dsolve(eq35) == sol35 + assert checksysodesol(eq35, sol35) == (True, [0, 0]) + + eq36 = [Eq(Derivative(f(t), t), I*g(t)), Eq(Derivative(g(t), t), 0)] + sol36 = [Eq(f(t), I*C1 + I*C2*t), Eq(g(t), C2)] + assert dsolve(eq36) == sol36 + assert checksysodesol(eq36, sol36) == (True, [0, 0]) + + eq37 = [Eq(Derivative(f(t), t), I*g(t)), Eq(Derivative(g(t), t), I*f(t))] + sol37 = [Eq(f(t), -C1*exp(-I*t) + C2*exp(I*t)), Eq(g(t), C1*exp(-I*t) + C2*exp(I*t))] + assert dsolve(eq37) == sol37 + assert checksysodesol(eq37, sol37) == (True, [0, 0]) + + # Multiple systems + eq1 = [Eq(Derivative(f(t), t)**2, g(t)**2), Eq(-f(t) + Derivative(g(t), t), 0)] + sol1 = [[Eq(f(t), -C1*sin(t) - C2*cos(t)), + Eq(g(t), C1*cos(t) - C2*sin(t))], + [Eq(f(t), -C1*exp(-t) + C2*exp(t)), + Eq(g(t), C1*exp(-t) + C2*exp(t))]] + assert dsolve(eq1) == sol1 + for sol in sol1: + assert checksysodesol(eq1, sol) == (True, [0, 0]) + + +def test_sysode_linear_neq_order1_type2(): + + f, g, h, k = symbols('f g h k', cls=Function) + x, t, a, b, c, d, y = symbols('x t a b c d y') + k1, k2 = symbols('k1 k2') + + + eqs1 = [Eq(Derivative(f(x), x), f(x) + g(x) + 5), + Eq(Derivative(g(x), x), -f(x) - g(x) + 7)] + sol1 = [Eq(f(x), C1 + C2 + 6*x**2 + x*(C2 + 5)), + Eq(g(x), -C1 - 6*x**2 - x*(C2 - 7))] + assert dsolve(eqs1) == sol1 + assert checksysodesol(eqs1, sol1) == (True, [0, 0]) + + eqs2 = [Eq(Derivative(f(x), x), f(x) + g(x) + 5), + Eq(Derivative(g(x), x), f(x) + g(x) + 7)] + sol2 = [Eq(f(x), -C1 + C2*exp(2*x) - x - 3), + Eq(g(x), C1 + C2*exp(2*x) + x - 3)] + assert dsolve(eqs2) == sol2 + assert checksysodesol(eqs2, sol2) == (True, [0, 0]) + + eqs3 = [Eq(Derivative(f(x), x), f(x) + 5), + Eq(Derivative(g(x), x), f(x) + 7)] + sol3 = [Eq(f(x), C1*exp(x) - 5), + Eq(g(x), C1*exp(x) + C2 + 2*x - 5)] + assert dsolve(eqs3) == sol3 + assert checksysodesol(eqs3, sol3) == (True, [0, 0]) + + eqs4 = [Eq(Derivative(f(x), x), f(x) + exp(x)), + Eq(Derivative(g(x), x), x*exp(x) + f(x) + g(x))] + sol4 = [Eq(f(x), C1*exp(x) + x*exp(x)), + Eq(g(x), C1*x*exp(x) + C2*exp(x) + x**2*exp(x))] + assert dsolve(eqs4) == sol4 + assert checksysodesol(eqs4, sol4) == (True, [0, 0]) + + eqs5 = [Eq(Derivative(f(x), x), 5*x + f(x) + g(x)), + Eq(Derivative(g(x), x), f(x) - g(x))] + sol5 = [Eq(f(x), C1*(1 + sqrt(2))*exp(sqrt(2)*x) + C2*(1 - sqrt(2))*exp(-sqrt(2)*x) + x*Rational(-5, 2) + + Rational(-5, 2)), + Eq(g(x), C1*exp(sqrt(2)*x) + C2*exp(-sqrt(2)*x) + x*Rational(-5, 2))] + assert dsolve(eqs5) == sol5 + assert checksysodesol(eqs5, sol5) == (True, [0, 0]) + + eqs6 = [Eq(Derivative(f(x), x), -9*f(x) - 4*g(x)), + Eq(Derivative(g(x), x), -4*g(x)), + Eq(Derivative(h(x), x), h(x) + exp(x))] + sol6 = [Eq(f(x), C2*exp(-4*x)*Rational(-4, 5) + C1*exp(-9*x)), + Eq(g(x), C2*exp(-4*x)), + Eq(h(x), C3*exp(x) + x*exp(x))] + assert dsolve(eqs6) == sol6 + assert checksysodesol(eqs6, sol6) == (True, [0, 0, 0]) + + # Regression test case for issue #8859 + # https://github.com/sympy/sympy/issues/8859 + eqs7 = [Eq(Derivative(f(t), t), 3*t + f(t)), + Eq(Derivative(g(t), t), g(t))] + sol7 = [Eq(f(t), C1*exp(t) - 3*t - 3), + Eq(g(t), C2*exp(t))] + assert dsolve(eqs7) == sol7 + assert checksysodesol(eqs7, sol7) == (True, [0, 0]) + + # Regression test case for issue #8567 + # https://github.com/sympy/sympy/issues/8567 + eqs8 = [Eq(Derivative(f(t), t), f(t) + 2*g(t)), + Eq(Derivative(g(t), t), -2*f(t) + g(t) + 2*exp(t))] + sol8 = [Eq(f(t), C1*exp(t)*sin(2*t) + C2*exp(t)*cos(2*t) + + exp(t)*sin(2*t)**2 + exp(t)*cos(2*t)**2), + Eq(g(t), C1*exp(t)*cos(2*t) - C2*exp(t)*sin(2*t))] + assert dsolve(eqs8) == sol8 + assert checksysodesol(eqs8, sol8) == (True, [0, 0]) + + # Regression test case for issue #19150 + # https://github.com/sympy/sympy/issues/19150 + eqs9 = [Eq(Derivative(f(t), t), (c - 2*f(t) + g(t))/(a*b)), + Eq(Derivative(g(t), t), (f(t) - 2*g(t) + h(t))/(a*b)), + Eq(Derivative(h(t), t), (d + g(t) - 2*h(t))/(a*b))] + sol9 = [Eq(f(t), -C1*exp(-2*t/(a*b)) + C2*exp(-t*(sqrt(2) + 2)/(a*b)) + C3*exp(-t*(2 - sqrt(2))/(a*b)) + + Mul(Rational(1, 4), 3*c + d, evaluate=False)), + Eq(g(t), -sqrt(2)*C2*exp(-t*(sqrt(2) + 2)/(a*b)) + sqrt(2)*C3*exp(-t*(2 - sqrt(2))/(a*b)) + + Mul(Rational(1, 2), c + d, evaluate=False)), + Eq(h(t), C1*exp(-2*t/(a*b)) + C2*exp(-t*(sqrt(2) + 2)/(a*b)) + C3*exp(-t*(2 - sqrt(2))/(a*b)) + + Mul(Rational(1, 4), c + 3*d, evaluate=False))] + assert dsolve(eqs9) == sol9 + assert checksysodesol(eqs9, sol9) == (True, [0, 0, 0]) + + # Regression test case for issue #16635 + # https://github.com/sympy/sympy/issues/16635 + eqs10 = [Eq(Derivative(f(t), t), 15*t + f(t) - g(t) - 10), + Eq(Derivative(g(t), t), -15*t + f(t) - g(t) - 5)] + sol10 = [Eq(f(t), C1 + C2 + 5*t**3 + 5*t**2 + t*(C2 - 10)), + Eq(g(t), C1 + 5*t**3 - 10*t**2 + t*(C2 - 5))] + assert dsolve(eqs10) == sol10 + assert checksysodesol(eqs10, sol10) == (True, [0, 0]) + + # Multiple solutions + eqs11 = [Eq(Derivative(f(t), t)**2 - 2*Derivative(f(t), t) + 1, 4), + Eq(-y*f(t) + Derivative(g(t), t), 0)] + sol11 = [[Eq(f(t), C1 - t), Eq(g(t), C1*t*y + C2*y + t**2*y*Rational(-1, 2))], + [Eq(f(t), C1 + 3*t), Eq(g(t), C1*t*y + C2*y + t**2*y*Rational(3, 2))]] + assert dsolve(eqs11) == sol11 + for s11 in sol11: + assert checksysodesol(eqs11, s11) == (True, [0, 0]) + + # test case for issue #19831 + # https://github.com/sympy/sympy/issues/19831 + n = symbols('n', positive=True) + x0 = symbols('x_0') + t0 = symbols('t_0') + x_0 = symbols('x_0') + t_0 = symbols('t_0') + t = symbols('t') + x = Function('x') + y = Function('y') + T = symbols('T') + + eqs12 = [Eq(Derivative(y(t), t), x(t)), + Eq(Derivative(x(t), t), n*(y(t) + 1))] + sol12 = [Eq(y(t), C1*exp(sqrt(n)*t)*n**Rational(-1, 2) - C2*exp(-sqrt(n)*t)*n**Rational(-1, 2) - 1), + Eq(x(t), C1*exp(sqrt(n)*t) + C2*exp(-sqrt(n)*t))] + assert dsolve(eqs12) == sol12 + assert checksysodesol(eqs12, sol12) == (True, [0, 0]) + + sol12b = [ + Eq(y(t), (T*exp(-sqrt(n)*t_0)/2 + exp(-sqrt(n)*t_0)/2 + + x_0*exp(-sqrt(n)*t_0)/(2*sqrt(n)))*exp(sqrt(n)*t) + + (T*exp(sqrt(n)*t_0)/2 + exp(sqrt(n)*t_0)/2 - + x_0*exp(sqrt(n)*t_0)/(2*sqrt(n)))*exp(-sqrt(n)*t) - 1), + Eq(x(t), (T*sqrt(n)*exp(-sqrt(n)*t_0)/2 + sqrt(n)*exp(-sqrt(n)*t_0)/2 + + x_0*exp(-sqrt(n)*t_0)/2)*exp(sqrt(n)*t) + - (T*sqrt(n)*exp(sqrt(n)*t_0)/2 + sqrt(n)*exp(sqrt(n)*t_0)/2 - + x_0*exp(sqrt(n)*t_0)/2)*exp(-sqrt(n)*t)) + ] + assert dsolve(eqs12, ics={y(t0): T, x(t0): x0}) == sol12b + assert checksysodesol(eqs12, sol12b) == (True, [0, 0]) + + #Test cases added for the issue 19763 + #https://github.com/sympy/sympy/issues/19763 + + eq13 = [Eq(Derivative(f(t), t), f(t) + g(t) + 9), + Eq(Derivative(g(t), t), 2*f(t) + 5*g(t) + 23)] + sol13 = [Eq(f(t), -C1*(2 + sqrt(6))*exp(t*(3 - sqrt(6)))/2 - C2*(2 - sqrt(6))*exp(t*(sqrt(6) + 3))/2 - + Rational(22,3)), + Eq(g(t), C1*exp(t*(3 - sqrt(6))) + C2*exp(t*(sqrt(6) + 3)) - Rational(5,3))] + assert dsolve(eq13) == sol13 + assert checksysodesol(eq13, sol13) == (True, [0, 0]) + + eq14 = [Eq(Derivative(f(t), t), f(t) + g(t) + 81), + Eq(Derivative(g(t), t), -2*f(t) + g(t) + 23)] + sol14 = [Eq(f(t), sqrt(2)*C1*exp(t)*sin(sqrt(2)*t)/2 + + sqrt(2)*C2*exp(t)*cos(sqrt(2)*t)/2 + - 58*sin(sqrt(2)*t)**2/3 - 58*cos(sqrt(2)*t)**2/3), + Eq(g(t), C1*exp(t)*cos(sqrt(2)*t) - C2*exp(t)*sin(sqrt(2)*t) + - 185*sin(sqrt(2)*t)**2/3 - 185*cos(sqrt(2)*t)**2/3)] + assert dsolve(eq14) == sol14 + assert checksysodesol(eq14, sol14) == (True, [0,0]) + + eq15 = [Eq(Derivative(f(t), t), f(t) + 2*g(t) + k1), + Eq(Derivative(g(t), t), 3*f(t) + 4*g(t) + k2)] + sol15 = [Eq(f(t), -C1*(3 - sqrt(33))*exp(t*(5 + sqrt(33))/2)/6 - + C2*(3 + sqrt(33))*exp(t*(5 - sqrt(33))/2)/6 + 2*k1 - k2), + Eq(g(t), C1*exp(t*(5 + sqrt(33))/2) + C2*exp(t*(5 - sqrt(33))/2) - + Mul(Rational(1,2), 3*k1 - k2, evaluate = False))] + assert dsolve(eq15) == sol15 + assert checksysodesol(eq15, sol15) == (True, [0,0]) + + eq16 = [Eq(Derivative(f(t), t), k1), + Eq(Derivative(g(t), t), k2)] + sol16 = [Eq(f(t), C1 + k1*t), + Eq(g(t), C2 + k2*t)] + assert dsolve(eq16) == sol16 + assert checksysodesol(eq16, sol16) == (True, [0,0]) + + eq17 = [Eq(Derivative(f(t), t), 0), + Eq(Derivative(g(t), t), c*f(t) + k2)] + sol17 = [Eq(f(t), C1), + Eq(g(t), C2*c + t*(C1*c + k2))] + assert dsolve(eq17) == sol17 + assert checksysodesol(eq17 , sol17) == (True , [0,0]) + + eq18 = [Eq(Derivative(f(t), t), k1), + Eq(Derivative(g(t), t), f(t) + k2)] + sol18 = [Eq(f(t), C1 + k1*t), + Eq(g(t), C2 + k1*t**2/2 + t*(C1 + k2))] + assert dsolve(eq18) == sol18 + assert checksysodesol(eq18 , sol18) == (True , [0,0]) + + eq19 = [Eq(Derivative(f(t), t), k1), + Eq(Derivative(g(t), t), f(t) + 2*g(t) + k2)] + sol19 = [Eq(f(t), -2*C1 + k1*t), + Eq(g(t), C1 + C2*exp(2*t) - k1*t/2 - Mul(Rational(1,4), k1 + 2*k2 , evaluate = False))] + assert dsolve(eq19) == sol19 + assert checksysodesol(eq19 , sol19) == (True , [0,0]) + + eq20 = [Eq(diff(f(t), t), f(t) + k1), + Eq(diff(g(t), t), k2)] + sol20 = [Eq(f(t), C1*exp(t) - k1), + Eq(g(t), C2 + k2*t)] + assert dsolve(eq20) == sol20 + assert checksysodesol(eq20 , sol20) == (True , [0,0]) + + eq21 = [Eq(diff(f(t), t), g(t) + k1), + Eq(diff(g(t), t), 0)] + sol21 = [Eq(f(t), C1 + t*(C2 + k1)), + Eq(g(t), C2)] + assert dsolve(eq21) == sol21 + assert checksysodesol(eq21 , sol21) == (True , [0,0]) + + eq22 = [Eq(Derivative(f(t), t), f(t) + 2*g(t) + k1), + Eq(Derivative(g(t), t), k2)] + sol22 = [Eq(f(t), -2*C1 + C2*exp(t) - k1 - 2*k2*t - 2*k2), + Eq(g(t), C1 + k2*t)] + assert dsolve(eq22) == sol22 + assert checksysodesol(eq22 , sol22) == (True , [0,0]) + + eq23 = [Eq(Derivative(f(t), t), g(t) + k1), + Eq(Derivative(g(t), t), 2*g(t) + k2)] + sol23 = [Eq(f(t), C1 + C2*exp(2*t)/2 - k2/4 + t*(2*k1 - k2)/2), + Eq(g(t), C2*exp(2*t) - k2/2)] + assert dsolve(eq23) == sol23 + assert checksysodesol(eq23 , sol23) == (True , [0,0]) + + eq24 = [Eq(Derivative(f(t), t), f(t) + k1), + Eq(Derivative(g(t), t), 2*f(t) + k2)] + sol24 = [Eq(f(t), C1*exp(t)/2 - k1), + Eq(g(t), C1*exp(t) + C2 - 2*k1 - t*(2*k1 - k2))] + assert dsolve(eq24) == sol24 + assert checksysodesol(eq24 , sol24) == (True , [0,0]) + + eq25 = [Eq(Derivative(f(t), t), f(t) + 2*g(t) + k1), + Eq(Derivative(g(t), t), 3*f(t) + 6*g(t) + k2)] + sol25 = [Eq(f(t), -2*C1 + C2*exp(7*t)/3 + 2*t*(3*k1 - k2)/7 - + Mul(Rational(1,49), k1 + 2*k2 , evaluate = False)), + Eq(g(t), C1 + C2*exp(7*t) - t*(3*k1 - k2)/7 - + Mul(Rational(3,49), k1 + 2*k2 , evaluate = False))] + assert dsolve(eq25) == sol25 + assert checksysodesol(eq25 , sol25) == (True , [0,0]) + + eq26 = [Eq(Derivative(f(t), t), 2*f(t) - g(t) + k1), + Eq(Derivative(g(t), t), 4*f(t) - 2*g(t) + 2*k1)] + sol26 = [Eq(f(t), C1 + 2*C2 + t*(2*C1 + k1)), + Eq(g(t), 4*C2 + t*(4*C1 + 2*k1))] + assert dsolve(eq26) == sol26 + assert checksysodesol(eq26 , sol26) == (True , [0,0]) + + # Test Case added for issue #22715 + # https://github.com/sympy/sympy/issues/22715 + + eq27 = [Eq(diff(x(t),t),-1*y(t)+10), Eq(diff(y(t),t),5*x(t)-2*y(t)+3)] + sol27 = [Eq(x(t), (C1/5 - 2*C2/5)*exp(-t)*cos(2*t) + - (2*C1/5 + C2/5)*exp(-t)*sin(2*t) + + 17*sin(2*t)**2/5 + 17*cos(2*t)**2/5), + Eq(y(t), C1*exp(-t)*cos(2*t) - C2*exp(-t)*sin(2*t) + + 10*sin(2*t)**2 + 10*cos(2*t)**2)] + assert dsolve(eq27) == sol27 + assert checksysodesol(eq27 , sol27) == (True , [0,0]) + + +def test_sysode_linear_neq_order1_type3(): + + f, g, h, k, x0 , y0 = symbols('f g h k x0 y0', cls=Function) + x, t, a = symbols('x t a') + r = symbols('r', real=True) + + eqs1 = [Eq(Derivative(f(r), r), r*g(r) + f(r)), + Eq(Derivative(g(r), r), -r*f(r) + g(r))] + sol1 = [Eq(f(r), C1*exp(r)*sin(r**2/2) + C2*exp(r)*cos(r**2/2)), + Eq(g(r), C1*exp(r)*cos(r**2/2) - C2*exp(r)*sin(r**2/2))] + assert dsolve(eqs1) == sol1 + assert checksysodesol(eqs1, sol1) == (True, [0, 0]) + + eqs2 = [Eq(Derivative(f(x), x), x**2*g(x) + x*f(x)), + Eq(Derivative(g(x), x), 2*x**2*f(x) + (3*x**2 + x)*g(x))] + sol2 = [Eq(f(x), (sqrt(17)*C1/17 + C2*(17 - 3*sqrt(17))/34)*exp(x**3*(3 + sqrt(17))/6 + x**2/2) - + exp(x**3*(3 - sqrt(17))/6 + x**2/2)*(sqrt(17)*C1/17 + C2*(3*sqrt(17) + 17)*Rational(-1, 34))), + Eq(g(x), exp(x**3*(3 - sqrt(17))/6 + x**2/2)*(C1*(17 - 3*sqrt(17))/34 + sqrt(17)*C2*Rational(-2, + 17)) + exp(x**3*(3 + sqrt(17))/6 + x**2/2)*(C1*(3*sqrt(17) + 17)/34 + sqrt(17)*C2*Rational(2, 17)))] + assert dsolve(eqs2) == sol2 + assert checksysodesol(eqs2, sol2) == (True, [0, 0]) + + eqs3 = [Eq(f(x).diff(x), x*f(x) + g(x)), + Eq(g(x).diff(x), -f(x) + x*g(x))] + sol3 = [Eq(f(x), (C1/2 + I*C2/2)*exp(x**2/2 - I*x) + exp(x**2/2 + I*x)*(C1/2 + I*C2*Rational(-1, 2))), + Eq(g(x), (I*C1/2 + C2/2)*exp(x**2/2 + I*x) - exp(x**2/2 - I*x)*(I*C1/2 + C2*Rational(-1, 2)))] + assert dsolve(eqs3) == sol3 + assert checksysodesol(eqs3, sol3) == (True, [0, 0]) + + eqs4 = [Eq(f(x).diff(x), x*(f(x) + g(x) + h(x))), Eq(g(x).diff(x), x*(f(x) + g(x) + h(x))), + Eq(h(x).diff(x), x*(f(x) + g(x) + h(x)))] + sol4 = [Eq(f(x), -C1/3 - C2/3 + 2*C3/3 + (C1/3 + C2/3 + C3/3)*exp(3*x**2/2)), + Eq(g(x), 2*C1/3 - C2/3 - C3/3 + (C1/3 + C2/3 + C3/3)*exp(3*x**2/2)), + Eq(h(x), -C1/3 + 2*C2/3 - C3/3 + (C1/3 + C2/3 + C3/3)*exp(3*x**2/2))] + assert dsolve(eqs4) == sol4 + assert checksysodesol(eqs4, sol4) == (True, [0, 0, 0]) + + eqs5 = [Eq(f(x).diff(x), x**2*(f(x) + g(x) + h(x))), Eq(g(x).diff(x), x**2*(f(x) + g(x) + h(x))), + Eq(h(x).diff(x), x**2*(f(x) + g(x) + h(x)))] + sol5 = [Eq(f(x), -C1/3 - C2/3 + 2*C3/3 + (C1/3 + C2/3 + C3/3)*exp(x**3)), + Eq(g(x), 2*C1/3 - C2/3 - C3/3 + (C1/3 + C2/3 + C3/3)*exp(x**3)), + Eq(h(x), -C1/3 + 2*C2/3 - C3/3 + (C1/3 + C2/3 + C3/3)*exp(x**3))] + assert dsolve(eqs5) == sol5 + assert checksysodesol(eqs5, sol5) == (True, [0, 0, 0]) + + eqs6 = [Eq(Derivative(f(x), x), x*(f(x) + g(x) + h(x) + k(x))), + Eq(Derivative(g(x), x), x*(f(x) + g(x) + h(x) + k(x))), + Eq(Derivative(h(x), x), x*(f(x) + g(x) + h(x) + k(x))), + Eq(Derivative(k(x), x), x*(f(x) + g(x) + h(x) + k(x)))] + sol6 = [Eq(f(x), -C1/4 - C2/4 - C3/4 + 3*C4/4 + (C1/4 + C2/4 + C3/4 + C4/4)*exp(2*x**2)), + Eq(g(x), 3*C1/4 - C2/4 - C3/4 - C4/4 + (C1/4 + C2/4 + C3/4 + C4/4)*exp(2*x**2)), + Eq(h(x), -C1/4 + 3*C2/4 - C3/4 - C4/4 + (C1/4 + C2/4 + C3/4 + C4/4)*exp(2*x**2)), + Eq(k(x), -C1/4 - C2/4 + 3*C3/4 - C4/4 + (C1/4 + C2/4 + C3/4 + C4/4)*exp(2*x**2))] + assert dsolve(eqs6) == sol6 + assert checksysodesol(eqs6, sol6) == (True, [0, 0, 0, 0]) + + y = symbols("y", real=True) + + eqs7 = [Eq(Derivative(f(y), y), y*f(y) + g(y)), + Eq(Derivative(g(y), y), y*g(y) - f(y))] + sol7 = [Eq(f(y), C1*exp(y**2/2)*sin(y) + C2*exp(y**2/2)*cos(y)), + Eq(g(y), C1*exp(y**2/2)*cos(y) - C2*exp(y**2/2)*sin(y))] + assert dsolve(eqs7) == sol7 + assert checksysodesol(eqs7, sol7) == (True, [0, 0]) + + #Test cases added for the issue 19763 + #https://github.com/sympy/sympy/issues/19763 + + eqs8 = [Eq(Derivative(f(t), t), 5*t*f(t) + 2*h(t)), + Eq(Derivative(h(t), t), 2*f(t) + 5*t*h(t))] + sol8 = [Eq(f(t), Mul(-1, (C1/2 - C2/2), evaluate = False)*exp(5*t**2/2 - 2*t) + (C1/2 + C2/2)*exp(5*t**2/2 + 2*t)), + Eq(h(t), (C1/2 - C2/2)*exp(5*t**2/2 - 2*t) + (C1/2 + C2/2)*exp(5*t**2/2 + 2*t))] + assert dsolve(eqs8) == sol8 + assert checksysodesol(eqs8, sol8) == (True, [0, 0]) + + eqs9 = [Eq(diff(f(t), t), 5*t*f(t) + t**2*g(t)), + Eq(diff(g(t), t), -t**2*f(t) + 5*t*g(t))] + sol9 = [Eq(f(t), (C1/2 - I*C2/2)*exp(I*t**3/3 + 5*t**2/2) + (C1/2 + I*C2/2)*exp(-I*t**3/3 + 5*t**2/2)), + Eq(g(t), Mul(-1, (I*C1/2 - C2/2) , evaluate = False)*exp(-I*t**3/3 + 5*t**2/2) + (I*C1/2 + C2/2)*exp(I*t**3/3 + 5*t**2/2))] + assert dsolve(eqs9) == sol9 + assert checksysodesol(eqs9 , sol9) == (True , [0,0]) + + eqs10 = [Eq(diff(f(t), t), t**2*g(t) + 5*t*f(t)), + Eq(diff(g(t), t), -t**2*f(t) + (9*t**2 + 5*t)*g(t))] + sol10 = [Eq(f(t), (C1*(77 - 9*sqrt(77))/154 + sqrt(77)*C2/77)*exp(t**3*(sqrt(77) + 9)/6 + 5*t**2/2) + (C1*(77 + 9*sqrt(77))/154 - sqrt(77)*C2/77)*exp(t**3*(9 - sqrt(77))/6 + 5*t**2/2)), + Eq(g(t), (sqrt(77)*C1/77 + C2*(77 - 9*sqrt(77))/154)*exp(t**3*(9 - sqrt(77))/6 + 5*t**2/2) - (sqrt(77)*C1/77 - C2*(77 + 9*sqrt(77))/154)*exp(t**3*(sqrt(77) + 9)/6 + 5*t**2/2))] + assert dsolve(eqs10) == sol10 + assert checksysodesol(eqs10 , sol10) == (True , [0,0]) + + eqs11 = [Eq(diff(f(t), t), 5*t*f(t) + t**2*g(t)), + Eq(diff(g(t), t), (1-t**2)*f(t) + (5*t + 9*t**2)*g(t))] + sol11 = [Eq(f(t), C1*x0(t) + C2*x0(t)*Integral(t**2*exp(Integral(5*t, t))*exp(Integral(9*t**2 + 5*t, t))/x0(t)**2, t)), + Eq(g(t), C1*y0(t) + C2*(y0(t)*Integral(t**2*exp(Integral(5*t, t))*exp(Integral(9*t**2 + 5*t, t))/x0(t)**2, t) + exp(Integral(5*t, t))*exp(Integral(9*t**2 + 5*t, t))/x0(t)))] + assert dsolve(eqs11) == sol11 + +@slow +def test_sysode_linear_neq_order1_type4(): + + f, g, h, k = symbols('f g h k', cls=Function) + x, t, a = symbols('x t a') + r = symbols('r', real=True) + + eqs1 = [Eq(diff(f(r), r), f(r) + r*g(r) + r**2), Eq(diff(g(r), r), -r*f(r) + g(r) + r)] + sol1 = [Eq(f(r), C1*exp(r)*sin(r**2/2) + C2*exp(r)*cos(r**2/2) + exp(r)*sin(r**2/2)*Integral(r**2*exp(-r)*sin(r**2/2) + + r*exp(-r)*cos(r**2/2), r) + exp(r)*cos(r**2/2)*Integral(r**2*exp(-r)*cos(r**2/2) - r*exp(-r)*sin(r**2/2), r)), + Eq(g(r), C1*exp(r)*cos(r**2/2) - C2*exp(r)*sin(r**2/2) - exp(r)*sin(r**2/2)*Integral(r**2*exp(-r)*cos(r**2/2) - + r*exp(-r)*sin(r**2/2), r) + exp(r)*cos(r**2/2)*Integral(r**2*exp(-r)*sin(r**2/2) + r*exp(-r)*cos(r**2/2), r))] + assert dsolve(eqs1) == sol1 + assert checksysodesol(eqs1, sol1) == (True, [0, 0]) + + eqs2 = [Eq(diff(f(r), r), f(r) + r*g(r) + r), Eq(diff(g(r), r), -r*f(r) + g(r) + log(r))] + sol2 = [Eq(f(r), C1*exp(r)*sin(r**2/2) + C2*exp(r)*cos(r**2/2) + exp(r)*sin(r**2/2)*Integral(r*exp(-r)*sin(r**2/2) + + exp(-r)*log(r)*cos(r**2/2), r) + exp(r)*cos(r**2/2)*Integral(r*exp(-r)*cos(r**2/2) - exp(-r)*log(r)*sin( + r**2/2), r)), + Eq(g(r), C1*exp(r)*cos(r**2/2) - C2*exp(r)*sin(r**2/2) - exp(r)*sin(r**2/2)*Integral(r*exp(-r)*cos(r**2/2) - + exp(-r)*log(r)*sin(r**2/2), r) + exp(r)*cos(r**2/2)*Integral(r*exp(-r)*sin(r**2/2) + exp(-r)*log(r)*cos( + r**2/2), r))] + # XXX: dsolve hangs for this in integration + assert dsolve_system(eqs2, simplify=False, doit=False) == [sol2] + assert checksysodesol(eqs2, sol2) == (True, [0, 0]) + + eqs3 = [Eq(Derivative(f(x), x), x*(f(x) + g(x) + h(x)) + x), + Eq(Derivative(g(x), x), x*(f(x) + g(x) + h(x)) + x), + Eq(Derivative(h(x), x), x*(f(x) + g(x) + h(x)) + 1)] + sol3 = [Eq(f(x), C1*Rational(-1, 3) + C2*Rational(-1, 3) + C3*Rational(2, 3) + x**2/6 + x*Rational(-1, 3) + + (C1/3 + C2/3 + C3/3)*exp(x**2*Rational(3, 2)) + + sqrt(6)*sqrt(pi)*erf(sqrt(6)*x/2)*exp(x**2*Rational(3, 2))/18 + Rational(-2, 9)), + Eq(g(x), C1*Rational(2, 3) + C2*Rational(-1, 3) + C3*Rational(-1, 3) + x**2/6 + x*Rational(-1, 3) + + (C1/3 + C2/3 + C3/3)*exp(x**2*Rational(3, 2)) + + sqrt(6)*sqrt(pi)*erf(sqrt(6)*x/2)*exp(x**2*Rational(3, 2))/18 + Rational(-2, 9)), + Eq(h(x), C1*Rational(-1, 3) + C2*Rational(2, 3) + C3*Rational(-1, 3) + x**2*Rational(-1, 3) + + x*Rational(2, 3) + (C1/3 + C2/3 + C3/3)*exp(x**2*Rational(3, 2)) + + sqrt(6)*sqrt(pi)*erf(sqrt(6)*x/2)*exp(x**2*Rational(3, 2))/18 + Rational(-2, 9))] + assert dsolve(eqs3) == sol3 + assert checksysodesol(eqs3, sol3) == (True, [0, 0, 0]) + + eqs4 = [Eq(Derivative(f(x), x), x*(f(x) + g(x) + h(x)) + sin(x)), + Eq(Derivative(g(x), x), x*(f(x) + g(x) + h(x)) + sin(x)), + Eq(Derivative(h(x), x), x*(f(x) + g(x) + h(x)) + sin(x))] + sol4 = [Eq(f(x), C1*Rational(-1, 3) + C2*Rational(-1, 3) + C3*Rational(2, 3) + (C1/3 + C2/3 + + C3/3)*exp(x**2*Rational(3, 2)) + Integral(sin(x)*exp(x**2*Rational(-3, 2)), x)*exp(x**2*Rational(3, + 2))), + Eq(g(x), C1*Rational(2, 3) + C2*Rational(-1, 3) + C3*Rational(-1, 3) + (C1/3 + C2/3 + + C3/3)*exp(x**2*Rational(3, 2)) + Integral(sin(x)*exp(x**2*Rational(-3, 2)), x)*exp(x**2*Rational(3, + 2))), + Eq(h(x), C1*Rational(-1, 3) + C2*Rational(2, 3) + C3*Rational(-1, 3) + (C1/3 + C2/3 + + C3/3)*exp(x**2*Rational(3, 2)) + Integral(sin(x)*exp(x**2*Rational(-3, 2)), x)*exp(x**2*Rational(3, + 2)))] + assert dsolve(eqs4) == sol4 + assert checksysodesol(eqs4, sol4) == (True, [0, 0, 0]) + + eqs5 = [Eq(Derivative(f(x), x), x*(f(x) + g(x) + h(x) + k(x) + 1)), + Eq(Derivative(g(x), x), x*(f(x) + g(x) + h(x) + k(x) + 1)), + Eq(Derivative(h(x), x), x*(f(x) + g(x) + h(x) + k(x) + 1)), + Eq(Derivative(k(x), x), x*(f(x) + g(x) + h(x) + k(x) + 1))] + sol5 = [Eq(f(x), C1*Rational(-1, 4) + C2*Rational(-1, 4) + C3*Rational(-1, 4) + C4*Rational(3, 4) + (C1/4 + + C2/4 + C3/4 + C4/4)*exp(2*x**2) + Rational(-1, 4)), + Eq(g(x), C1*Rational(3, 4) + C2*Rational(-1, 4) + C3*Rational(-1, 4) + C4*Rational(-1, 4) + (C1/4 + + C2/4 + C3/4 + C4/4)*exp(2*x**2) + Rational(-1, 4)), + Eq(h(x), C1*Rational(-1, 4) + C2*Rational(3, 4) + C3*Rational(-1, 4) + C4*Rational(-1, 4) + (C1/4 + + C2/4 + C3/4 + C4/4)*exp(2*x**2) + Rational(-1, 4)), + Eq(k(x), C1*Rational(-1, 4) + C2*Rational(-1, 4) + C3*Rational(3, 4) + C4*Rational(-1, 4) + (C1/4 + + C2/4 + C3/4 + C4/4)*exp(2*x**2) + Rational(-1, 4))] + assert dsolve(eqs5) == sol5 + assert checksysodesol(eqs5, sol5) == (True, [0, 0, 0, 0]) + + eqs6 = [Eq(Derivative(f(x), x), x**2*(f(x) + g(x) + h(x) + k(x) + 1)), + Eq(Derivative(g(x), x), x**2*(f(x) + g(x) + h(x) + k(x) + 1)), + Eq(Derivative(h(x), x), x**2*(f(x) + g(x) + h(x) + k(x) + 1)), + Eq(Derivative(k(x), x), x**2*(f(x) + g(x) + h(x) + k(x) + 1))] + sol6 = [Eq(f(x), C1*Rational(-1, 4) + C2*Rational(-1, 4) + C3*Rational(-1, 4) + C4*Rational(3, 4) + (C1/4 + + C2/4 + C3/4 + C4/4)*exp(x**3*Rational(4, 3)) + Rational(-1, 4)), + Eq(g(x), C1*Rational(3, 4) + C2*Rational(-1, 4) + C3*Rational(-1, 4) + C4*Rational(-1, 4) + (C1/4 + + C2/4 + C3/4 + C4/4)*exp(x**3*Rational(4, 3)) + Rational(-1, 4)), + Eq(h(x), C1*Rational(-1, 4) + C2*Rational(3, 4) + C3*Rational(-1, 4) + C4*Rational(-1, 4) + (C1/4 + + C2/4 + C3/4 + C4/4)*exp(x**3*Rational(4, 3)) + Rational(-1, 4)), + Eq(k(x), C1*Rational(-1, 4) + C2*Rational(-1, 4) + C3*Rational(3, 4) + C4*Rational(-1, 4) + (C1/4 + + C2/4 + C3/4 + C4/4)*exp(x**3*Rational(4, 3)) + Rational(-1, 4))] + assert dsolve(eqs6) == sol6 + assert checksysodesol(eqs6, sol6) == (True, [0, 0, 0, 0]) + + eqs7 = [Eq(Derivative(f(x), x), (f(x) + g(x) + h(x))*log(x) + sin(x)), Eq(Derivative(g(x), x), (f(x) + g(x) + + h(x))*log(x) + sin(x)), Eq(Derivative(h(x), x), (f(x) + g(x) + h(x))*log(x) + sin(x))] + sol7 = [Eq(f(x), -C1/3 - C2/3 + 2*C3/3 + (C1/3 + C2/3 + + C3/3)*exp(x*(3*log(x) - 3)) + exp(x*(3*log(x) - + 3))*Integral(exp(3*x)*exp(-3*x*log(x))*sin(x), x)), + Eq(g(x), 2*C1/3 - C2/3 - C3/3 + (C1/3 + C2/3 + + C3/3)*exp(x*(3*log(x) - 3)) + exp(x*(3*log(x) - + 3))*Integral(exp(3*x)*exp(-3*x*log(x))*sin(x), x)), + Eq(h(x), -C1/3 + 2*C2/3 - C3/3 + (C1/3 + C2/3 + + C3/3)*exp(x*(3*log(x) - 3)) + exp(x*(3*log(x) - + 3))*Integral(exp(3*x)*exp(-3*x*log(x))*sin(x), x))] + with dotprodsimp(True): + assert dsolve(eqs7, simplify=False, doit=False) == sol7 + assert checksysodesol(eqs7, sol7) == (True, [0, 0, 0]) + + eqs8 = [Eq(Derivative(f(x), x), (f(x) + g(x) + h(x) + k(x))*log(x) + sin(x)), Eq(Derivative(g(x), x), (f(x) + + g(x) + h(x) + k(x))*log(x) + sin(x)), Eq(Derivative(h(x), x), (f(x) + g(x) + h(x) + k(x))*log(x) + + sin(x)), Eq(Derivative(k(x), x), (f(x) + g(x) + h(x) + k(x))*log(x) + sin(x))] + sol8 = [Eq(f(x), -C1/4 - C2/4 - C3/4 + 3*C4/4 + (C1/4 + C2/4 + C3/4 + + C4/4)*exp(x*(4*log(x) - 4)) + exp(x*(4*log(x) - + 4))*Integral(exp(4*x)*exp(-4*x*log(x))*sin(x), x)), + Eq(g(x), 3*C1/4 - C2/4 - C3/4 - C4/4 + (C1/4 + C2/4 + C3/4 + + C4/4)*exp(x*(4*log(x) - 4)) + exp(x*(4*log(x) - + 4))*Integral(exp(4*x)*exp(-4*x*log(x))*sin(x), x)), + Eq(h(x), -C1/4 + 3*C2/4 - C3/4 - C4/4 + (C1/4 + C2/4 + C3/4 + + C4/4)*exp(x*(4*log(x) - 4)) + exp(x*(4*log(x) - + 4))*Integral(exp(4*x)*exp(-4*x*log(x))*sin(x), x)), + Eq(k(x), -C1/4 - C2/4 + 3*C3/4 - C4/4 + (C1/4 + C2/4 + C3/4 + + C4/4)*exp(x*(4*log(x) - 4)) + exp(x*(4*log(x) - + 4))*Integral(exp(4*x)*exp(-4*x*log(x))*sin(x), x))] + with dotprodsimp(True): + assert dsolve(eqs8) == sol8 + assert checksysodesol(eqs8, sol8) == (True, [0, 0, 0, 0]) + + +def test_sysode_linear_neq_order1_type5_type6(): + f, g = symbols("f g", cls=Function) + x, x_ = symbols("x x_") + + # Type 5 + eqs1 = [Eq(Derivative(f(x), x), (2*f(x) + g(x))/x), Eq(Derivative(g(x), x), (f(x) + 2*g(x))/x)] + sol1 = [Eq(f(x), -C1*x + C2*x**3), Eq(g(x), C1*x + C2*x**3)] + assert dsolve(eqs1) == sol1 + assert checksysodesol(eqs1, sol1) == (True, [0, 0]) + + # Type 6 + eqs2 = [Eq(Derivative(f(x), x), (2*f(x) + g(x) + 1)/x), + Eq(Derivative(g(x), x), (x + f(x) + 2*g(x))/x)] + sol2 = [Eq(f(x), C2*x**3 - x*(C1 + Rational(1, 4)) + x*log(x)*Rational(-1, 2) + Rational(-2, 3)), + Eq(g(x), C2*x**3 + x*log(x)/2 + x*(C1 + Rational(-1, 4)) + Rational(1, 3))] + assert dsolve(eqs2) == sol2 + assert checksysodesol(eqs2, sol2) == (True, [0, 0]) + + +def test_higher_order_to_first_order(): + f, g = symbols('f g', cls=Function) + x = symbols('x') + + eqs1 = [Eq(Derivative(f(x), (x, 2)), 2*f(x) + g(x)), + Eq(Derivative(g(x), (x, 2)), -f(x))] + sol1 = [Eq(f(x), -C2*x*exp(-x) + C3*x*exp(x) - (C1 - C2)*exp(-x) + (C3 + C4)*exp(x)), + Eq(g(x), C2*x*exp(-x) - C3*x*exp(x) + (C1 + C2)*exp(-x) + (C3 - C4)*exp(x))] + assert dsolve(eqs1) == sol1 + assert checksysodesol(eqs1, sol1) == (True, [0, 0]) + + eqs2 = [Eq(f(x).diff(x, 2), 0), Eq(g(x).diff(x, 2), f(x))] + sol2 = [Eq(f(x), C1 + C2*x), Eq(g(x), C1*x**2/2 + C2*x**3/6 + C3 + C4*x)] + assert dsolve(eqs2) == sol2 + assert checksysodesol(eqs2, sol2) == (True, [0, 0]) + + eqs3 = [Eq(Derivative(f(x), (x, 2)), 2*f(x)), + Eq(Derivative(g(x), (x, 2)), -f(x) + 2*g(x))] + sol3 = [Eq(f(x), 4*C1*exp(-sqrt(2)*x) + 4*C2*exp(sqrt(2)*x)), + Eq(g(x), sqrt(2)*C1*x*exp(-sqrt(2)*x) - sqrt(2)*C2*x*exp(sqrt(2)*x) + (C1 + + sqrt(2)*C4)*exp(-sqrt(2)*x) + (C2 - sqrt(2)*C3)*exp(sqrt(2)*x))] + assert dsolve(eqs3) == sol3 + assert checksysodesol(eqs3, sol3) == (True, [0, 0]) + + eqs4 = [Eq(Derivative(f(x), (x, 2)), 2*f(x) + g(x)), + Eq(Derivative(g(x), (x, 2)), 2*g(x))] + sol4 = [Eq(f(x), C1*x*exp(sqrt(2)*x)/4 + C3*x*exp(-sqrt(2)*x)/4 + (C2/4 + sqrt(2)*C3/8)*exp(-sqrt(2)*x) - + exp(sqrt(2)*x)*(sqrt(2)*C1/8 + C4*Rational(-1, 4))), + Eq(g(x), sqrt(2)*C1*exp(sqrt(2)*x)/2 + sqrt(2)*C3*exp(-sqrt(2)*x)*Rational(-1, 2))] + assert dsolve(eqs4) == sol4 + assert checksysodesol(eqs4, sol4) == (True, [0, 0]) + + eqs5 = [Eq(f(x).diff(x, 2), f(x)), Eq(g(x).diff(x, 2), f(x))] + sol5 = [Eq(f(x), -C1*exp(-x) + C2*exp(x)), Eq(g(x), -C1*exp(-x) + C2*exp(x) + C3 + C4*x)] + assert dsolve(eqs5) == sol5 + assert checksysodesol(eqs5, sol5) == (True, [0, 0]) + + eqs6 = [Eq(Derivative(f(x), (x, 2)), f(x) + g(x)), + Eq(Derivative(g(x), (x, 2)), -f(x) - g(x))] + sol6 = [Eq(f(x), C1 + C2*x**2/2 + C2 + C4*x**3/6 + x*(C3 + C4)), + Eq(g(x), -C1 + C2*x**2*Rational(-1, 2) - C3*x + C4*x**3*Rational(-1, 6))] + assert dsolve(eqs6) == sol6 + assert checksysodesol(eqs6, sol6) == (True, [0, 0]) + + eqs7 = [Eq(Derivative(f(x), (x, 2)), f(x) + g(x) + 1), + Eq(Derivative(g(x), (x, 2)), f(x) + g(x) + 1)] + sol7 = [Eq(f(x), -C1 - C2*x + sqrt(2)*C3*exp(sqrt(2)*x)/2 + sqrt(2)*C4*exp(-sqrt(2)*x)*Rational(-1, 2) + + Rational(-1, 2)), + Eq(g(x), C1 + C2*x + sqrt(2)*C3*exp(sqrt(2)*x)/2 + sqrt(2)*C4*exp(-sqrt(2)*x)*Rational(-1, 2) + + Rational(-1, 2))] + assert dsolve(eqs7) == sol7 + assert checksysodesol(eqs7, sol7) == (True, [0, 0]) + + eqs8 = [Eq(Derivative(f(x), (x, 2)), f(x) + g(x) + 1), + Eq(Derivative(g(x), (x, 2)), -f(x) - g(x) + 1)] + sol8 = [Eq(f(x), C1 + C2 + C4*x**3/6 + x**4/12 + x**2*(C2/2 + Rational(1, 2)) + x*(C3 + C4)), + Eq(g(x), -C1 - C3*x + C4*x**3*Rational(-1, 6) + x**4*Rational(-1, 12) - x**2*(C2/2 + Rational(-1, + 2)))] + assert dsolve(eqs8) == sol8 + assert checksysodesol(eqs8, sol8) == (True, [0, 0]) + + x, y = symbols('x, y', cls=Function) + t, l = symbols('t, l') + + eqs10 = [Eq(Derivative(x(t), (t, 2)), 5*x(t) + 43*y(t)), + Eq(Derivative(y(t), (t, 2)), x(t) + 9*y(t))] + sol10 = [Eq(x(t), C1*(61 - 9*sqrt(47))*sqrt(sqrt(47) + 7)*exp(-t*sqrt(sqrt(47) + 7))/2 + C2*sqrt(7 - + sqrt(47))*(61 + 9*sqrt(47))*exp(-t*sqrt(7 - sqrt(47)))/2 + C3*(61 - 9*sqrt(47))*sqrt(sqrt(47) + + 7)*exp(t*sqrt(sqrt(47) + 7))*Rational(-1, 2) + C4*sqrt(7 - sqrt(47))*(61 + 9*sqrt(47))*exp(t*sqrt(7 + - sqrt(47)))*Rational(-1, 2)), + Eq(y(t), C1*(7 - sqrt(47))*sqrt(sqrt(47) + 7)*exp(-t*sqrt(sqrt(47) + 7))*Rational(-1, 2) + C2*sqrt(7 + - sqrt(47))*(sqrt(47) + 7)*exp(-t*sqrt(7 - sqrt(47)))*Rational(-1, 2) + C3*(7 - + sqrt(47))*sqrt(sqrt(47) + 7)*exp(t*sqrt(sqrt(47) + 7))/2 + C4*sqrt(7 - sqrt(47))*(sqrt(47) + + 7)*exp(t*sqrt(7 - sqrt(47)))/2)] + assert dsolve(eqs10) == sol10 + assert checksysodesol(eqs10, sol10) == (True, [0, 0]) + + eqs11 = [Eq(7*x(t) + Derivative(x(t), (t, 2)) - 9*Derivative(y(t), t), 0), + Eq(7*y(t) + 9*Derivative(x(t), t) + Derivative(y(t), (t, 2)), 0)] + sol11 = [Eq(y(t), C1*(9 - sqrt(109))*sin(sqrt(2)*t*sqrt(9*sqrt(109) + 95)/2)/14 + C2*(9 - + sqrt(109))*cos(sqrt(2)*t*sqrt(9*sqrt(109) + 95)/2)*Rational(-1, 14) + C3*(9 + + sqrt(109))*sin(sqrt(2)*t*sqrt(95 - 9*sqrt(109))/2)/14 + C4*(9 + sqrt(109))*cos(sqrt(2)*t*sqrt(95 - + 9*sqrt(109))/2)*Rational(-1, 14)), + Eq(x(t), C1*(9 - sqrt(109))*cos(sqrt(2)*t*sqrt(9*sqrt(109) + 95)/2)*Rational(-1, 14) + C2*(9 - + sqrt(109))*sin(sqrt(2)*t*sqrt(9*sqrt(109) + 95)/2)*Rational(-1, 14) + C3*(9 + + sqrt(109))*cos(sqrt(2)*t*sqrt(95 - 9*sqrt(109))/2)/14 + C4*(9 + sqrt(109))*sin(sqrt(2)*t*sqrt(95 - + 9*sqrt(109))/2)/14)] + assert dsolve(eqs11) == sol11 + assert checksysodesol(eqs11, sol11) == (True, [0, 0]) + + # Euler Systems + # Note: To add examples of euler systems solver with non-homogeneous term. + eqs13 = [Eq(Derivative(f(t), (t, 2)), Derivative(f(t), t)/t + f(t)/t**2 + g(t)/t**2), + Eq(Derivative(g(t), (t, 2)), g(t)/t**2)] + sol13 = [Eq(f(t), C1*(sqrt(5) + 3)*Rational(-1, 2)*t**(Rational(1, 2) + + sqrt(5)*Rational(-1, 2)) + C2*t**(Rational(1, 2) + + sqrt(5)/2)*(3 - sqrt(5))*Rational(-1, 2) - C3*t**(1 - + sqrt(2))*(1 + sqrt(2)) - C4*t**(1 + sqrt(2))*(1 - sqrt(2))), + Eq(g(t), C1*(1 + sqrt(5))*Rational(-1, 2)*t**(Rational(1, 2) + + sqrt(5)*Rational(-1, 2)) + C2*t**(Rational(1, 2) + + sqrt(5)/2)*(1 - sqrt(5))*Rational(-1, 2))] + assert dsolve(eqs13) == sol13 + assert checksysodesol(eqs13, sol13) == (True, [0, 0]) + + # Solving systems using dsolve separately + eqs14 = [Eq(Derivative(f(t), (t, 2)), t*f(t)), + Eq(Derivative(g(t), (t, 2)), t*g(t))] + sol14 = [Eq(f(t), C1*airyai(t) + C2*airybi(t)), + Eq(g(t), C3*airyai(t) + C4*airybi(t))] + assert dsolve(eqs14) == sol14 + assert checksysodesol(eqs14, sol14) == (True, [0, 0]) + + + eqs15 = [Eq(Derivative(x(t), (t, 2)), t*(4*Derivative(x(t), t) + 8*Derivative(y(t), t))), + Eq(Derivative(y(t), (t, 2)), t*(12*Derivative(x(t), t) - 6*Derivative(y(t), t)))] + sol15 = [Eq(x(t), C1 - erf(sqrt(6)*t)*(sqrt(6)*sqrt(pi)*C2/33 + sqrt(6)*sqrt(pi)*C3*Rational(-1, 44)) + + erfi(sqrt(5)*t)*(sqrt(5)*sqrt(pi)*C2*Rational(2, 55) + sqrt(5)*sqrt(pi)*C3*Rational(4, 55))), + Eq(y(t), C4 + erf(sqrt(6)*t)*(sqrt(6)*sqrt(pi)*C2*Rational(2, 33) + sqrt(6)*sqrt(pi)*C3*Rational(-1, + 22)) + erfi(sqrt(5)*t)*(sqrt(5)*sqrt(pi)*C2*Rational(3, 110) + sqrt(5)*sqrt(pi)*C3*Rational(3, 55)))] + assert dsolve(eqs15) == sol15 + assert checksysodesol(eqs15, sol15) == (True, [0, 0]) + + +@slow +def test_higher_order_to_first_order_9(): + f, g = symbols('f g', cls=Function) + x = symbols('x') + + eqs9 = [f(x) + g(x) - 2*exp(I*x) + 2*Derivative(f(x), x) + Derivative(f(x), (x, 2)), + f(x) + g(x) - 2*exp(I*x) + 2*Derivative(g(x), x) + Derivative(g(x), (x, 2))] + sol9 = [Eq(f(x), -C1 + C4*exp(-2*x)/2 - (C2/2 - C3/2)*exp(-x)*cos(x) + + (C2/2 + C3/2)*exp(-x)*sin(x) + 2*((1 - 2*I)*exp(I*x)*sin(x)**2/5) + + 2*((1 - 2*I)*exp(I*x)*cos(x)**2/5)), + Eq(g(x), C1 - C4*exp(-2*x)/2 - (C2/2 - C3/2)*exp(-x)*cos(x) + + (C2/2 + C3/2)*exp(-x)*sin(x) + 2*((1 - 2*I)*exp(I*x)*sin(x)**2/5) + + 2*((1 - 2*I)*exp(I*x)*cos(x)**2/5))] + assert dsolve(eqs9) == sol9 + assert checksysodesol(eqs9, sol9) == (True, [0, 0]) + + +def test_higher_order_to_first_order_12(): + f, g = symbols('f g', cls=Function) + x = symbols('x') + + x, y = symbols('x, y', cls=Function) + t, l = symbols('t, l') + + eqs12 = [Eq(4*x(t) + Derivative(x(t), (t, 2)) + 8*Derivative(y(t), t), 0), + Eq(4*y(t) - 8*Derivative(x(t), t) + Derivative(y(t), (t, 2)), 0)] + sol12 = [Eq(y(t), C1*(2 - sqrt(5))*sin(2*t*sqrt(4*sqrt(5) + 9))*Rational(-1, 2) + C2*(2 - + sqrt(5))*cos(2*t*sqrt(4*sqrt(5) + 9))/2 + C3*(2 + sqrt(5))*sin(2*t*sqrt(9 - 4*sqrt(5)))*Rational(-1, + 2) + C4*(2 + sqrt(5))*cos(2*t*sqrt(9 - 4*sqrt(5)))/2), + Eq(x(t), C1*(2 - sqrt(5))*cos(2*t*sqrt(4*sqrt(5) + 9))*Rational(-1, 2) + C2*(2 - + sqrt(5))*sin(2*t*sqrt(4*sqrt(5) + 9))*Rational(-1, 2) + C3*(2 + sqrt(5))*cos(2*t*sqrt(9 - + 4*sqrt(5)))/2 + C4*(2 + sqrt(5))*sin(2*t*sqrt(9 - 4*sqrt(5)))/2)] + assert dsolve(eqs12) == sol12 + assert checksysodesol(eqs12, sol12) == (True, [0, 0]) + + +def test_second_order_to_first_order_2(): + f, g = symbols("f g", cls=Function) + x, t, x_, t_, d, a, m = symbols("x t x_ t_ d a m") + + eqs2 = [Eq(f(x).diff(x, 2), 2*(x*g(x).diff(x) - g(x))), + Eq(g(x).diff(x, 2),-2*(x*f(x).diff(x) - f(x)))] + sol2 = [Eq(f(x), C1*x + x*Integral(C2*exp(-x_)*exp(I*exp(2*x_))/2 + C2*exp(-x_)*exp(-I*exp(2*x_))/2 - + I*C3*exp(-x_)*exp(I*exp(2*x_))/2 + I*C3*exp(-x_)*exp(-I*exp(2*x_))/2, (x_, log(x)))), + Eq(g(x), C4*x + x*Integral(I*C2*exp(-x_)*exp(I*exp(2*x_))/2 - I*C2*exp(-x_)*exp(-I*exp(2*x_))/2 + + C3*exp(-x_)*exp(I*exp(2*x_))/2 + C3*exp(-x_)*exp(-I*exp(2*x_))/2, (x_, log(x))))] + # XXX: dsolve hangs for this in integration + assert dsolve_system(eqs2, simplify=False, doit=False) == [sol2] + assert checksysodesol(eqs2, sol2) == (True, [0, 0]) + + eqs3 = (Eq(diff(f(t),t,t), 9*t*diff(g(t),t)-9*g(t)), Eq(diff(g(t),t,t),7*t*diff(f(t),t)-7*f(t))) + sol3 = [Eq(f(t), C1*t + t*Integral(C2*exp(-t_)*exp(3*sqrt(7)*exp(2*t_)/2)/2 + C2*exp(-t_)* + exp(-3*sqrt(7)*exp(2*t_)/2)/2 + 3*sqrt(7)*C3*exp(-t_)*exp(3*sqrt(7)*exp(2*t_)/2)/14 - + 3*sqrt(7)*C3*exp(-t_)*exp(-3*sqrt(7)*exp(2*t_)/2)/14, (t_, log(t)))), + Eq(g(t), C4*t + t*Integral(sqrt(7)*C2*exp(-t_)*exp(3*sqrt(7)*exp(2*t_)/2)/6 - sqrt(7)*C2*exp(-t_)* + exp(-3*sqrt(7)*exp(2*t_)/2)/6 + C3*exp(-t_)*exp(3*sqrt(7)*exp(2*t_)/2)/2 + C3*exp(-t_)*exp(-3*sqrt(7)* + exp(2*t_)/2)/2, (t_, log(t))))] + # XXX: dsolve hangs for this in integration + assert dsolve_system(eqs3, simplify=False, doit=False) == [sol3] + assert checksysodesol(eqs3, sol3) == (True, [0, 0]) + + # Regression Test case for sympy#19238 + # https://github.com/sympy/sympy/issues/19238 + # Note: When the doit method is removed, these particular types of systems + # can be divided first so that we have lesser number of big matrices. + eqs5 = [Eq(Derivative(g(t), (t, 2)), a*m), + Eq(Derivative(f(t), (t, 2)), 0)] + sol5 = [Eq(g(t), C1 + C2*t + a*m*t**2/2), + Eq(f(t), C3 + C4*t)] + assert dsolve(eqs5) == sol5 + assert checksysodesol(eqs5, sol5) == (True, [0, 0]) + + # Type 2 + eqs6 = [Eq(Derivative(f(t), (t, 2)), f(t)/t**4), + Eq(Derivative(g(t), (t, 2)), d*g(t)/t**4)] + sol6 = [Eq(f(t), C1*sqrt(t**2)*exp(-1/t) - C2*sqrt(t**2)*exp(1/t)), + Eq(g(t), C3*sqrt(t**2)*exp(-sqrt(d)/t)*d**Rational(-1, 2) - + C4*sqrt(t**2)*exp(sqrt(d)/t)*d**Rational(-1, 2))] + assert dsolve(eqs6) == sol6 + assert checksysodesol(eqs6, sol6) == (True, [0, 0]) + + +@slow +def test_second_order_to_first_order_slow1(): + f, g = symbols("f g", cls=Function) + x, t, x_, t_, d, a, m = symbols("x t x_ t_ d a m") + + # Type 1 + + eqs1 = [Eq(f(x).diff(x, 2), 2/x *(x*g(x).diff(x) - g(x))), + Eq(g(x).diff(x, 2),-2/x *(x*f(x).diff(x) - f(x)))] + sol1 = [Eq(f(x), C1*x + 2*C2*x*Ci(2*x) - C2*sin(2*x) - 2*C3*x*Si(2*x) - C3*cos(2*x)), + Eq(g(x), -2*C2*x*Si(2*x) - C2*cos(2*x) - 2*C3*x*Ci(2*x) + C3*sin(2*x) + C4*x)] + assert dsolve(eqs1) == sol1 + assert checksysodesol(eqs1, sol1) == (True, [0, 0]) + + +def test_second_order_to_first_order_slow4(): + f, g = symbols("f g", cls=Function) + x, t, x_, t_, d, a, m = symbols("x t x_ t_ d a m") + + eqs4 = [Eq(Derivative(f(t), (t, 2)), t*sin(t)*Derivative(g(t), t) - g(t)*sin(t)), + Eq(Derivative(g(t), (t, 2)), t*sin(t)*Derivative(f(t), t) - f(t)*sin(t))] + sol4 = [Eq(f(t), C1*t + t*Integral(C2*exp(-t_)*exp(exp(t_)*cos(exp(t_)))*exp(-sin(exp(t_)))/2 + + C2*exp(-t_)*exp(-exp(t_)*cos(exp(t_)))*exp(sin(exp(t_)))/2 - C3*exp(-t_)*exp(exp(t_)*cos(exp(t_)))* + exp(-sin(exp(t_)))/2 + + C3*exp(-t_)*exp(-exp(t_)*cos(exp(t_)))*exp(sin(exp(t_)))/2, (t_, log(t)))), + Eq(g(t), C4*t + t*Integral(-C2*exp(-t_)*exp(exp(t_)*cos(exp(t_)))*exp(-sin(exp(t_)))/2 + + C2*exp(-t_)*exp(-exp(t_)*cos(exp(t_)))*exp(sin(exp(t_)))/2 + C3*exp(-t_)*exp(exp(t_)*cos(exp(t_)))* + exp(-sin(exp(t_)))/2 + C3*exp(-t_)*exp(-exp(t_)*cos(exp(t_)))*exp(sin(exp(t_)))/2, (t_, log(t))))] + # XXX: dsolve hangs for this in integration + assert dsolve_system(eqs4, simplify=False, doit=False) == [sol4] + assert checksysodesol(eqs4, sol4) == (True, [0, 0]) + + +def test_component_division(): + f, g, h, k = symbols('f g h k', cls=Function) + x = symbols("x") + funcs = [f(x), g(x), h(x), k(x)] + + eqs1 = [Eq(Derivative(f(x), x), 2*f(x)), + Eq(Derivative(g(x), x), f(x)), + Eq(Derivative(h(x), x), h(x)), + Eq(Derivative(k(x), x), h(x)**4 + k(x))] + sol1 = [Eq(f(x), 2*C1*exp(2*x)), + Eq(g(x), C1*exp(2*x) + C2), + Eq(h(x), C3*exp(x)), + Eq(k(x), C3**4*exp(4*x)/3 + C4*exp(x))] + assert dsolve(eqs1) == sol1 + assert checksysodesol(eqs1, sol1) == (True, [0, 0, 0, 0]) + + components1 = {((Eq(Derivative(f(x), x), 2*f(x)),), (Eq(Derivative(g(x), x), f(x)),)), + ((Eq(Derivative(h(x), x), h(x)),), (Eq(Derivative(k(x), x), h(x)**4 + k(x)),))} + eqsdict1 = ({f(x): set(), g(x): {f(x)}, h(x): set(), k(x): {h(x)}}, + {f(x): Eq(Derivative(f(x), x), 2*f(x)), + g(x): Eq(Derivative(g(x), x), f(x)), + h(x): Eq(Derivative(h(x), x), h(x)), + k(x): Eq(Derivative(k(x), x), h(x)**4 + k(x))}) + graph1 = [{f(x), g(x), h(x), k(x)}, {(g(x), f(x)), (k(x), h(x))}] + assert {tuple(tuple(scc) for scc in wcc) for wcc in _component_division(eqs1, funcs, x)} == components1 + assert _eqs2dict(eqs1, funcs) == eqsdict1 + assert [set(element) for element in _dict2graph(eqsdict1[0])] == graph1 + + eqs2 = [Eq(Derivative(f(x), x), 2*f(x)), + Eq(Derivative(g(x), x), f(x)), + Eq(Derivative(h(x), x), h(x)), + Eq(Derivative(k(x), x), f(x)**4 + k(x))] + sol2 = [Eq(f(x), C1*exp(2*x)), + Eq(g(x), C1*exp(2*x)/2 + C2), + Eq(h(x), C3*exp(x)), + Eq(k(x), C1**4*exp(8*x)/7 + C4*exp(x))] + assert dsolve(eqs2) == sol2 + assert checksysodesol(eqs2, sol2) == (True, [0, 0, 0, 0]) + + components2 = {frozenset([(Eq(Derivative(f(x), x), 2*f(x)),), + (Eq(Derivative(g(x), x), f(x)),), + (Eq(Derivative(k(x), x), f(x)**4 + k(x)),)]), + frozenset([(Eq(Derivative(h(x), x), h(x)),)])} + eqsdict2 = ({f(x): set(), g(x): {f(x)}, h(x): set(), k(x): {f(x)}}, + {f(x): Eq(Derivative(f(x), x), 2*f(x)), + g(x): Eq(Derivative(g(x), x), f(x)), + h(x): Eq(Derivative(h(x), x), h(x)), + k(x): Eq(Derivative(k(x), x), f(x)**4 + k(x))}) + graph2 = [{f(x), g(x), h(x), k(x)}, {(g(x), f(x)), (k(x), f(x))}] + assert {frozenset(tuple(scc) for scc in wcc) for wcc in _component_division(eqs2, funcs, x)} == components2 + assert _eqs2dict(eqs2, funcs) == eqsdict2 + assert [set(element) for element in _dict2graph(eqsdict2[0])] == graph2 + + eqs3 = [Eq(Derivative(f(x), x), 2*f(x)), + Eq(Derivative(g(x), x), x + f(x)), + Eq(Derivative(h(x), x), h(x)), + Eq(Derivative(k(x), x), f(x)**4 + k(x))] + sol3 = [Eq(f(x), C1*exp(2*x)), + Eq(g(x), C1*exp(2*x)/2 + C2 + x**2/2), + Eq(h(x), C3*exp(x)), + Eq(k(x), C1**4*exp(8*x)/7 + C4*exp(x))] + assert dsolve(eqs3) == sol3 + assert checksysodesol(eqs3, sol3) == (True, [0, 0, 0, 0]) + + components3 = {frozenset([(Eq(Derivative(f(x), x), 2*f(x)),), + (Eq(Derivative(g(x), x), x + f(x)),), + (Eq(Derivative(k(x), x), f(x)**4 + k(x)),)]), + frozenset([(Eq(Derivative(h(x), x), h(x)),),])} + eqsdict3 = ({f(x): set(), g(x): {f(x)}, h(x): set(), k(x): {f(x)}}, + {f(x): Eq(Derivative(f(x), x), 2*f(x)), + g(x): Eq(Derivative(g(x), x), x + f(x)), + h(x): Eq(Derivative(h(x), x), h(x)), + k(x): Eq(Derivative(k(x), x), f(x)**4 + k(x))}) + graph3 = [{f(x), g(x), h(x), k(x)}, {(g(x), f(x)), (k(x), f(x))}] + assert {frozenset(tuple(scc) for scc in wcc) for wcc in _component_division(eqs3, funcs, x)} == components3 + assert _eqs2dict(eqs3, funcs) == eqsdict3 + assert [set(l) for l in _dict2graph(eqsdict3[0])] == graph3 + + # Note: To be uncommented when the default option to call dsolve first for + # single ODE system can be rearranged. This can be done after the doit + # option in dsolve is made False by default. + + eqs4 = [Eq(Derivative(f(x), x), x*f(x) + 2*g(x)), + Eq(Derivative(g(x), x), f(x) + x*g(x) + x), + Eq(Derivative(h(x), x), h(x)), + Eq(Derivative(k(x), x), f(x)**4 + k(x))] + sol4 = [Eq(f(x), (C1/2 - sqrt(2)*C2/2 - sqrt(2)*Integral(x*exp(-x**2/2 - sqrt(2)*x)/2 + x*exp(-x**2/2 +\ + sqrt(2)*x)/2, x)/2 + Integral(sqrt(2)*x*exp(-x**2/2 - sqrt(2)*x)/2 - sqrt(2)*x*exp(-x**2/2 +\ + sqrt(2)*x)/2, x)/2)*exp(x**2/2 - sqrt(2)*x) + (C1/2 + sqrt(2)*C2/2 + sqrt(2)*Integral(x*exp(-x**2/2 + - sqrt(2)*x)/2 + x*exp(-x**2/2 + sqrt(2)*x)/2, x)/2 + Integral(sqrt(2)*x*exp(-x**2/2 - sqrt(2)*x)/2 + - sqrt(2)*x*exp(-x**2/2 + sqrt(2)*x)/2, x)/2)*exp(x**2/2 + sqrt(2)*x)), + Eq(g(x), (-sqrt(2)*C1/4 + C2/2 + Integral(x*exp(-x**2/2 - sqrt(2)*x)/2 + x*exp(-x**2/2 + sqrt(2)*x)/2, x)/2 -\ + sqrt(2)*Integral(sqrt(2)*x*exp(-x**2/2 - sqrt(2)*x)/2 - sqrt(2)*x*exp(-x**2/2 + sqrt(2)*x)/2, + x)/4)*exp(x**2/2 - sqrt(2)*x) + (sqrt(2)*C1/4 + C2/2 + Integral(x*exp(-x**2/2 - sqrt(2)*x)/2 + + x*exp(-x**2/2 + sqrt(2)*x)/2, x)/2 + sqrt(2)*Integral(sqrt(2)*x*exp(-x**2/2 - sqrt(2)*x)/2 - + sqrt(2)*x*exp(-x**2/2 + sqrt(2)*x)/2, x)/4)*exp(x**2/2 + sqrt(2)*x)), + Eq(h(x), C3*exp(x)), + Eq(k(x), C4*exp(x) + exp(x)*Integral((C1*exp(x**2/2 - sqrt(2)*x)/2 + C1*exp(x**2/2 + sqrt(2)*x)/2 - + sqrt(2)*C2*exp(x**2/2 - sqrt(2)*x)/2 + sqrt(2)*C2*exp(x**2/2 + sqrt(2)*x)/2 - sqrt(2)*exp(x**2/2 - + sqrt(2)*x)*Integral(x*exp(-x**2/2 - sqrt(2)*x)/2 + x*exp(-x**2/2 + sqrt(2)*x)/2, x)/2 + exp(x**2/2 - + sqrt(2)*x)*Integral(sqrt(2)*x*exp(-x**2/2 - sqrt(2)*x)/2 - sqrt(2)*x*exp(-x**2/2 + sqrt(2)*x)/2, + x)/2 + sqrt(2)*exp(x**2/2 + sqrt(2)*x)*Integral(x*exp(-x**2/2 - sqrt(2)*x)/2 + x*exp(-x**2/2 + + sqrt(2)*x)/2, x)/2 + exp(x**2/2 + sqrt(2)*x)*Integral(sqrt(2)*x*exp(-x**2/2 - sqrt(2)*x)/2 - + sqrt(2)*x*exp(-x**2/2 + sqrt(2)*x)/2, x)/2)**4*exp(-x), x))] + components4 = {(frozenset([Eq(Derivative(f(x), x), x*f(x) + 2*g(x)), + Eq(Derivative(g(x), x), x*g(x) + x + f(x))]), + frozenset([Eq(Derivative(k(x), x), f(x)**4 + k(x)),])), + (frozenset([Eq(Derivative(h(x), x), h(x)),]),)} + eqsdict4 = ({f(x): {g(x)}, g(x): {f(x)}, h(x): set(), k(x): {f(x)}}, + {f(x): Eq(Derivative(f(x), x), x*f(x) + 2*g(x)), + g(x): Eq(Derivative(g(x), x), x*g(x) + x + f(x)), + h(x): Eq(Derivative(h(x), x), h(x)), + k(x): Eq(Derivative(k(x), x), f(x)**4 + k(x))}) + graph4 = [{f(x), g(x), h(x), k(x)}, {(f(x), g(x)), (g(x), f(x)), (k(x), f(x))}] + assert {tuple(frozenset(scc) for scc in wcc) for wcc in _component_division(eqs4, funcs, x)} == components4 + assert _eqs2dict(eqs4, funcs) == eqsdict4 + assert [set(element) for element in _dict2graph(eqsdict4[0])] == graph4 + # XXX: dsolve hangs in integration here: + assert dsolve_system(eqs4, simplify=False, doit=False) == [sol4] + assert checksysodesol(eqs4, sol4) == (True, [0, 0, 0, 0]) + + eqs5 = [Eq(Derivative(f(x), x), x*f(x) + 2*g(x)), + Eq(Derivative(g(x), x), x*g(x) + f(x)), + Eq(Derivative(h(x), x), h(x)), + Eq(Derivative(k(x), x), f(x)**4 + k(x))] + sol5 = [Eq(f(x), (C1/2 - sqrt(2)*C2/2)*exp(x**2/2 - sqrt(2)*x) + (C1/2 + sqrt(2)*C2/2)*exp(x**2/2 + sqrt(2)*x)), + Eq(g(x), (-sqrt(2)*C1/4 + C2/2)*exp(x**2/2 - sqrt(2)*x) + (sqrt(2)*C1/4 + C2/2)*exp(x**2/2 + sqrt(2)*x)), + Eq(h(x), C3*exp(x)), + Eq(k(x), C4*exp(x) + exp(x)*Integral((C1*exp(x**2/2 - sqrt(2)*x)/2 + C1*exp(x**2/2 + sqrt(2)*x)/2 - + sqrt(2)*C2*exp(x**2/2 - sqrt(2)*x)/2 + sqrt(2)*C2*exp(x**2/2 + sqrt(2)*x)/2)**4*exp(-x), x))] + components5 = {(frozenset([Eq(Derivative(f(x), x), x*f(x) + 2*g(x)), + Eq(Derivative(g(x), x), x*g(x) + f(x))]), + frozenset([Eq(Derivative(k(x), x), f(x)**4 + k(x)),])), + (frozenset([Eq(Derivative(h(x), x), h(x)),]),)} + eqsdict5 = ({f(x): {g(x)}, g(x): {f(x)}, h(x): set(), k(x): {f(x)}}, + {f(x): Eq(Derivative(f(x), x), x*f(x) + 2*g(x)), + g(x): Eq(Derivative(g(x), x), x*g(x) + f(x)), + h(x): Eq(Derivative(h(x), x), h(x)), + k(x): Eq(Derivative(k(x), x), f(x)**4 + k(x))}) + graph5 = [{f(x), g(x), h(x), k(x)}, {(f(x), g(x)), (g(x), f(x)), (k(x), f(x))}] + assert {tuple(frozenset(scc) for scc in wcc) for wcc in _component_division(eqs5, funcs, x)} == components5 + assert _eqs2dict(eqs5, funcs) == eqsdict5 + assert [set(element) for element in _dict2graph(eqsdict5[0])] == graph5 + # XXX: dsolve hangs in integration here: + assert dsolve_system(eqs5, simplify=False, doit=False) == [sol5] + assert checksysodesol(eqs5, sol5) == (True, [0, 0, 0, 0]) + + +def test_linodesolve(): + t, x, a = symbols("t x a") + f, g, h = symbols("f g h", cls=Function) + + # Testing the Errors + raises(ValueError, lambda: linodesolve(1, t)) + raises(ValueError, lambda: linodesolve(a, t)) + + A1 = Matrix([[1, 2], [2, 4], [4, 6]]) + raises(NonSquareMatrixError, lambda: linodesolve(A1, t)) + + A2 = Matrix([[1, 2, 1], [3, 1, 2]]) + raises(NonSquareMatrixError, lambda: linodesolve(A2, t)) + + # Testing auto functionality + func = [f(t), g(t)] + eq = [Eq(f(t).diff(t) + g(t).diff(t), g(t)), Eq(g(t).diff(t), f(t))] + ceq = canonical_odes(eq, func, t) + (A1, A0), b = linear_ode_to_matrix(ceq[0], func, t, 1) + A = A0 + sol = [C1*(-Rational(1, 2) + sqrt(5)/2)*exp(t*(-Rational(1, 2) + sqrt(5)/2)) + C2*(-sqrt(5)/2 - Rational(1, 2))* + exp(t*(-sqrt(5)/2 - Rational(1, 2))), + C1*exp(t*(-Rational(1, 2) + sqrt(5)/2)) + C2*exp(t*(-sqrt(5)/2 - Rational(1, 2)))] + assert constant_renumber(linodesolve(A, t), variables=Tuple(*eq).free_symbols) == sol + + # Testing the Errors + raises(ValueError, lambda: linodesolve(1, t, b=Matrix([t+1]))) + raises(ValueError, lambda: linodesolve(a, t, b=Matrix([log(t) + sin(t)]))) + + raises(ValueError, lambda: linodesolve(Matrix([7]), t, b=t**2)) + raises(ValueError, lambda: linodesolve(Matrix([a+10]), t, b=log(t)*cos(t))) + + raises(ValueError, lambda: linodesolve(7, t, b=t**2)) + raises(ValueError, lambda: linodesolve(a, t, b=log(t) + sin(t))) + + A1 = Matrix([[1, 2], [2, 4], [4, 6]]) + b1 = Matrix([t, 1, t**2]) + raises(NonSquareMatrixError, lambda: linodesolve(A1, t, b=b1)) + + A2 = Matrix([[1, 2, 1], [3, 1, 2]]) + b2 = Matrix([t, t**2]) + raises(NonSquareMatrixError, lambda: linodesolve(A2, t, b=b2)) + + raises(ValueError, lambda: linodesolve(A1[:2, :], t, b=b1)) + raises(ValueError, lambda: linodesolve(A1[:2, :], t, b=b1[:1])) + + # DOIT check + A1 = Matrix([[1, -1], [1, -1]]) + b1 = Matrix([15*t - 10, -15*t - 5]) + sol1 = [C1 + C2*t + C2 - 10*t**3 + 10*t**2 + t*(15*t**2 - 5*t) - 10*t, + C1 + C2*t - 10*t**3 - 5*t**2 + t*(15*t**2 - 5*t) - 5*t] + assert constant_renumber(linodesolve(A1, t, b=b1, type="type2", doit=True), + variables=[t]) == sol1 + + # Testing auto functionality + func = [f(t), g(t)] + eq = [Eq(f(t).diff(t) + g(t).diff(t), g(t) + t), Eq(g(t).diff(t), f(t))] + ceq = canonical_odes(eq, func, t) + (A1, A0), b = linear_ode_to_matrix(ceq[0], func, t, 1) + A = A0 + sol = [-C1*exp(-t/2 + sqrt(5)*t/2)/2 + sqrt(5)*C1*exp(-t/2 + sqrt(5)*t/2)/2 - sqrt(5)*C2*exp(-sqrt(5)*t/2 - + t/2)/2 - C2*exp(-sqrt(5)*t/2 - t/2)/2 - exp(-t/2 + sqrt(5)*t/2)*Integral(t*exp(-sqrt(5)*t/2 + + t/2)/(-5 + sqrt(5)) - sqrt(5)*t*exp(-sqrt(5)*t/2 + t/2)/(-5 + sqrt(5)), t)/2 + sqrt(5)*exp(-t/2 + + sqrt(5)*t/2)*Integral(t*exp(-sqrt(5)*t/2 + t/2)/(-5 + sqrt(5)) - sqrt(5)*t*exp(-sqrt(5)*t/2 + + t/2)/(-5 + sqrt(5)), t)/2 - sqrt(5)*exp(-sqrt(5)*t/2 - t/2)*Integral(-sqrt(5)*t*exp(t/2 + + sqrt(5)*t/2)/5, t)/2 - exp(-sqrt(5)*t/2 - t/2)*Integral(-sqrt(5)*t*exp(t/2 + sqrt(5)*t/2)/5, t)/2, + C1*exp(-t/2 + sqrt(5)*t/2) + C2*exp(-sqrt(5)*t/2 - t/2) + exp(-t/2 + + sqrt(5)*t/2)*Integral(t*exp(-sqrt(5)*t/2 + t/2)/(-5 + sqrt(5)) - sqrt(5)*t*exp(-sqrt(5)*t/2 + + t/2)/(-5 + sqrt(5)), t) + exp(-sqrt(5)*t/2 - + t/2)*Integral(-sqrt(5)*t*exp(t/2 + sqrt(5)*t/2)/5, t)] + assert constant_renumber(linodesolve(A, t, b=b), variables=[t]) == sol + + # non-homogeneous term assumed to be 0 + sol1 = [-C1*exp(-t/2 + sqrt(5)*t/2)/2 + sqrt(5)*C1*exp(-t/2 + sqrt(5)*t/2)/2 - sqrt(5)*C2*exp(-sqrt(5)*t/2 + - t/2)/2 - C2*exp(-sqrt(5)*t/2 - t/2)/2, + C1*exp(-t/2 + sqrt(5)*t/2) + C2*exp(-sqrt(5)*t/2 - t/2)] + assert constant_renumber(linodesolve(A, t, type="type2"), variables=[t]) == sol1 + + # Testing the Errors + raises(ValueError, lambda: linodesolve(t+10, t)) + raises(ValueError, lambda: linodesolve(a*t, t)) + + A1 = Matrix([[1, t], [-t, 1]]) + B1, _ = _is_commutative_anti_derivative(A1, t) + raises(NonSquareMatrixError, lambda: linodesolve(A1[:, :1], t, B=B1)) + raises(ValueError, lambda: linodesolve(A1, t, B=1)) + + A2 = Matrix([[t, t, t], [t, t, t], [t, t, t]]) + B2, _ = _is_commutative_anti_derivative(A2, t) + raises(NonSquareMatrixError, lambda: linodesolve(A2, t, B=B2[:2, :])) + raises(ValueError, lambda: linodesolve(A2, t, B=2)) + raises(ValueError, lambda: linodesolve(A2, t, B=B2, type="type31")) + + raises(ValueError, lambda: linodesolve(A1, t, B=B2)) + raises(ValueError, lambda: linodesolve(A2, t, B=B1)) + + # Testing auto functionality + func = [f(t), g(t)] + eq = [Eq(f(t).diff(t), f(t) + t*g(t)), Eq(g(t).diff(t), -t*f(t) + g(t))] + ceq = canonical_odes(eq, func, t) + (A1, A0), b = linear_ode_to_matrix(ceq[0], func, t, 1) + A = A0 + sol = [(C1/2 - I*C2/2)*exp(I*t**2/2 + t) + (C1/2 + I*C2/2)*exp(-I*t**2/2 + t), + (-I*C1/2 + C2/2)*exp(-I*t**2/2 + t) + (I*C1/2 + C2/2)*exp(I*t**2/2 + t)] + assert constant_renumber(linodesolve(A, t), variables=Tuple(*eq).free_symbols) == sol + assert constant_renumber(linodesolve(A, t, type="type3"), variables=Tuple(*eq).free_symbols) == sol + + A1 = Matrix([[t, 1], [t, -1]]) + raises(NotImplementedError, lambda: linodesolve(A1, t)) + + # Testing the Errors + raises(ValueError, lambda: linodesolve(t+10, t, b=Matrix([t+1]))) + raises(ValueError, lambda: linodesolve(a*t, t, b=Matrix([log(t) + sin(t)]))) + + raises(ValueError, lambda: linodesolve(Matrix([7*t]), t, b=t**2)) + raises(ValueError, lambda: linodesolve(Matrix([a + 10*log(t)]), t, b=log(t)*cos(t))) + + raises(ValueError, lambda: linodesolve(7*t, t, b=t**2)) + raises(ValueError, lambda: linodesolve(a*t**2, t, b=log(t) + sin(t))) + + A1 = Matrix([[1, t], [-t, 1]]) + b1 = Matrix([t, t ** 2]) + B1, _ = _is_commutative_anti_derivative(A1, t) + raises(NonSquareMatrixError, lambda: linodesolve(A1[:, :1], t, b=b1)) + + A2 = Matrix([[t, t, t], [t, t, t], [t, t, t]]) + b2 = Matrix([t, 1, t**2]) + B2, _ = _is_commutative_anti_derivative(A2, t) + raises(NonSquareMatrixError, lambda: linodesolve(A2[:2, :], t, b=b2)) + + raises(ValueError, lambda: linodesolve(A1, t, b=b2)) + raises(ValueError, lambda: linodesolve(A2, t, b=b1)) + + raises(ValueError, lambda: linodesolve(A1, t, b=b1, B=B2)) + raises(ValueError, lambda: linodesolve(A2, t, b=b2, B=B1)) + + # Testing auto functionality + func = [f(x), g(x), h(x)] + eq = [Eq(f(x).diff(x), x*(f(x) + g(x) + h(x)) + x), + Eq(g(x).diff(x), x*(f(x) + g(x) + h(x)) + x), + Eq(h(x).diff(x), x*(f(x) + g(x) + h(x)) + 1)] + ceq = canonical_odes(eq, func, x) + (A1, A0), b = linear_ode_to_matrix(ceq[0], func, x, 1) + A = A0 + _x1 = exp(-3*x**2/2) + _x2 = exp(3*x**2/2) + _x3 = Integral(2*_x1*x/3 + _x1/3 + x/3 - Rational(1, 3), x) + _x4 = 2*_x2*_x3/3 + _x5 = Integral(2*_x1*x/3 + _x1/3 - 2*x/3 + Rational(2, 3), x) + sol = [ + C1*_x2/3 - C1/3 + C2*_x2/3 - C2/3 + C3*_x2/3 + 2*C3/3 + _x2*_x5/3 + _x3/3 + _x4 - _x5/3, + C1*_x2/3 + 2*C1/3 + C2*_x2/3 - C2/3 + C3*_x2/3 - C3/3 + _x2*_x5/3 + _x3/3 + _x4 - _x5/3, + C1*_x2/3 - C1/3 + C2*_x2/3 + 2*C2/3 + C3*_x2/3 - C3/3 + _x2*_x5/3 - 2*_x3/3 + _x4 + 2*_x5/3, + ] + assert constant_renumber(linodesolve(A, x, b=b), variables=Tuple(*eq).free_symbols) == sol + assert constant_renumber(linodesolve(A, x, b=b, type="type4"), + variables=Tuple(*eq).free_symbols) == sol + + A1 = Matrix([[t, 1], [t, -1]]) + raises(NotImplementedError, lambda: linodesolve(A1, t, b=b1)) + + # non-homogeneous term not passed + sol1 = [-C1/3 - C2/3 + 2*C3/3 + (C1/3 + C2/3 + C3/3)*exp(3*x**2/2), 2*C1/3 - C2/3 - C3/3 + (C1/3 + C2/3 + C3/3)*exp(3*x**2/2), + -C1/3 + 2*C2/3 - C3/3 + (C1/3 + C2/3 + C3/3)*exp(3*x**2/2)] + assert constant_renumber(linodesolve(A, x, type="type4", doit=True), variables=Tuple(*eq).free_symbols) == sol1 + + +@slow +def test_linear_3eq_order1_type4_slow(): + x, y, z = symbols('x, y, z', cls=Function) + t = Symbol('t') + + f = t ** 3 + log(t) + g = t ** 2 + sin(t) + eq1 = (Eq(diff(x(t), t), (4 * f + g) * x(t) - f * y(t) - 2 * f * z(t)), + Eq(diff(y(t), t), 2 * f * x(t) + (f + g) * y(t) - 2 * f * z(t)), Eq(diff(z(t), t), 5 * f * x(t) + f * y( + t) + (-3 * f + g) * z(t))) + with dotprodsimp(True): + dsolve(eq1) + + +@slow +def test_linear_neq_order1_type2_slow1(): + i, r1, c1, r2, c2, t = symbols('i, r1, c1, r2, c2, t') + x1 = Function('x1') + x2 = Function('x2') + + eq1 = r1*c1*Derivative(x1(t), t) + x1(t) - x2(t) - r1*i + eq2 = r2*c1*Derivative(x1(t), t) + r2*c2*Derivative(x2(t), t) + x2(t) - r2*i + eq = [eq1, eq2] + + # XXX: Solution is too complicated + [sol] = dsolve_system(eq, simplify=False, doit=False) + assert checksysodesol(eq, sol) == (True, [0, 0]) + + +# Regression test case for issue #9204 +# https://github.com/sympy/sympy/issues/9204 +@tooslow +def test_linear_new_order1_type2_de_lorentz_slow_check(): + m = Symbol("m", real=True) + q = Symbol("q", real=True) + t = Symbol("t", real=True) + + e1, e2, e3 = symbols("e1:4", real=True) + b1, b2, b3 = symbols("b1:4", real=True) + v1, v2, v3 = symbols("v1:4", cls=Function, real=True) + + eqs = [ + -e1*q + m*Derivative(v1(t), t) - q*(-b2*v3(t) + b3*v2(t)), + -e2*q + m*Derivative(v2(t), t) - q*(b1*v3(t) - b3*v1(t)), + -e3*q + m*Derivative(v3(t), t) - q*(-b1*v2(t) + b2*v1(t)) + ] + sol = dsolve(eqs) + assert checksysodesol(eqs, sol) == (True, [0, 0, 0]) + + +# Regression test case for issue #14001 +# https://github.com/sympy/sympy/issues/14001 +@slow +def test_linear_neq_order1_type2_slow_check(): + RC, t, C, Vs, L, R1, V0, I0 = symbols("RC t C Vs L R1 V0 I0") + V = Function("V") + I = Function("I") + system = [Eq(V(t).diff(t), -1/RC*V(t) + I(t)/C), Eq(I(t).diff(t), -R1/L*I(t) - 1/L*V(t) + Vs/L)] + [sol] = dsolve_system(system, simplify=False, doit=False) + + assert checksysodesol(system, sol) == (True, [0, 0]) + + +def _linear_3eq_order1_type4_long(): + x, y, z = symbols('x, y, z', cls=Function) + t = Symbol('t') + + f = t ** 3 + log(t) + g = t ** 2 + sin(t) + + eq1 = (Eq(diff(x(t), t), (4*f + g)*x(t) - f*y(t) - 2*f*z(t)), + Eq(diff(y(t), t), 2*f*x(t) + (f + g)*y(t) - 2*f*z(t)), Eq(diff(z(t), t), 5*f*x(t) + f*y( + t) + (-3*f + g)*z(t))) + + dsolve_sol = dsolve(eq1) + dsolve_sol1 = [_simpsol(sol) for sol in dsolve_sol] + + x_1 = sqrt(-t**6 - 8*t**3*log(t) + 8*t**3 - 16*log(t)**2 + 32*log(t) - 16) + x_2 = sqrt(3) + x_3 = 8324372644*C1*x_1*x_2 + 4162186322*C2*x_1*x_2 - 8324372644*C3*x_1*x_2 + x_4 = 1 / (1903457163*t**3 + 3825881643*x_1*x_2 + 7613828652*log(t) - 7613828652) + x_5 = exp(t**3/3 + t*x_1*x_2/4 - cos(t)) + x_6 = exp(t**3/3 - t*x_1*x_2/4 - cos(t)) + x_7 = exp(t**4/2 + t**3/3 + 2*t*log(t) - 2*t - cos(t)) + x_8 = 91238*C1*x_1*x_2 + 91238*C2*x_1*x_2 - 91238*C3*x_1*x_2 + x_9 = 1 / (66049*t**3 - 50629*x_1*x_2 + 264196*log(t) - 264196) + x_10 = 50629 * C1 / 25189 + 37909*C2/25189 - 50629*C3/25189 - x_3*x_4 + x_11 = -50629*C1/25189 - 12720*C2/25189 + 50629*C3/25189 + x_3*x_4 + sol = [Eq(x(t), x_10*x_5 + x_11*x_6 + x_7*(C1 - C2)), Eq(y(t), x_10*x_5 + x_11*x_6), Eq(z(t), x_5*( + -424*C1/257 - 167*C2/257 + 424*C3/257 - x_8*x_9) + x_6*(167*C1/257 + 424*C2/257 - + 167*C3/257 + x_8*x_9) + x_7*(C1 - C2))] + + assert dsolve_sol1 == sol + assert checksysodesol(eq1, dsolve_sol1) == (True, [0, 0, 0]) + + +@slow +def test_neq_order1_type4_slow_check1(): + f, g = symbols("f g", cls=Function) + x = symbols("x") + + eqs = [Eq(diff(f(x), x), x*f(x) + x**2*g(x) + x), + Eq(diff(g(x), x), 2*x**2*f(x) + (x + 3*x**2)*g(x) + 1)] + sol = dsolve(eqs) + assert checksysodesol(eqs, sol) == (True, [0, 0]) + + +@slow +def test_neq_order1_type4_slow_check2(): + f, g, h = symbols("f, g, h", cls=Function) + x = Symbol("x") + + eqs = [ + Eq(Derivative(f(x), x), x*h(x) + f(x) + g(x) + 1), + Eq(Derivative(g(x), x), x*g(x) + f(x) + h(x) + 10), + Eq(Derivative(h(x), x), x*f(x) + x + g(x) + h(x)) + ] + with dotprodsimp(True): + sol = dsolve(eqs) + assert checksysodesol(eqs, sol) == (True, [0, 0, 0]) + + +def _neq_order1_type4_slow3(): + f, g = symbols("f g", cls=Function) + x = symbols("x") + + eqs = [ + Eq(Derivative(f(x), x), x*f(x) + g(x) + sin(x)), + Eq(Derivative(g(x), x), x**2 + x*g(x) - f(x)) + ] + sol = [ + Eq(f(x), (C1/2 - I*C2/2 - I*Integral(x**2*exp(-x**2/2 - I*x)/2 + + x**2*exp(-x**2/2 + I*x)/2 + I*exp(-x**2/2 - I*x)*sin(x)/2 - + I*exp(-x**2/2 + I*x)*sin(x)/2, x)/2 + Integral(-I*x**2*exp(-x**2/2 + - I*x)/2 + I*x**2*exp(-x**2/2 + I*x)/2 + exp(-x**2/2 - + I*x)*sin(x)/2 + exp(-x**2/2 + I*x)*sin(x)/2, x)/2)*exp(x**2/2 + + I*x) + (C1/2 + I*C2/2 + I*Integral(x**2*exp(-x**2/2 - I*x)/2 + + x**2*exp(-x**2/2 + I*x)/2 + I*exp(-x**2/2 - I*x)*sin(x)/2 - + I*exp(-x**2/2 + I*x)*sin(x)/2, x)/2 + Integral(-I*x**2*exp(-x**2/2 + - I*x)/2 + I*x**2*exp(-x**2/2 + I*x)/2 + exp(-x**2/2 - + I*x)*sin(x)/2 + exp(-x**2/2 + I*x)*sin(x)/2, x)/2)*exp(x**2/2 - + I*x)), + Eq(g(x), (-I*C1/2 + C2/2 + Integral(x**2*exp(-x**2/2 - I*x)/2 + + x**2*exp(-x**2/2 + I*x)/2 + I*exp(-x**2/2 - I*x)*sin(x)/2 - + I*exp(-x**2/2 + I*x)*sin(x)/2, x)/2 - + I*Integral(-I*x**2*exp(-x**2/2 - I*x)/2 + I*x**2*exp(-x**2/2 + + I*x)/2 + exp(-x**2/2 - I*x)*sin(x)/2 + exp(-x**2/2 + + I*x)*sin(x)/2, x)/2)*exp(x**2/2 - I*x) + (I*C1/2 + C2/2 + + Integral(x**2*exp(-x**2/2 - I*x)/2 + x**2*exp(-x**2/2 + I*x)/2 + + I*exp(-x**2/2 - I*x)*sin(x)/2 - I*exp(-x**2/2 + I*x)*sin(x)/2, + x)/2 + I*Integral(-I*x**2*exp(-x**2/2 - I*x)/2 + + I*x**2*exp(-x**2/2 + I*x)/2 + exp(-x**2/2 - I*x)*sin(x)/2 + + exp(-x**2/2 + I*x)*sin(x)/2, x)/2)*exp(x**2/2 + I*x)) + ] + + return eqs, sol + + +def test_neq_order1_type4_slow3(): + eqs, sol = _neq_order1_type4_slow3() + assert dsolve_system(eqs, simplify=False, doit=False) == [sol] + # XXX: dsolve gives an error in integration: + # assert dsolve(eqs) == sol + # https://github.com/sympy/sympy/issues/20155 + + +@slow +def test_neq_order1_type4_slow_check3(): + eqs, sol = _neq_order1_type4_slow3() + assert checksysodesol(eqs, sol) == (True, [0, 0]) + + +@tooslow +@XFAIL +def test_linear_3eq_order1_type4_long_dsolve_slow_xfail(): + eq, sol = _linear_3eq_order1_type4_long() + + dsolve_sol = dsolve(eq) + dsolve_sol1 = [_simpsol(sol) for sol in dsolve_sol] + + assert dsolve_sol1 == sol + + +@tooslow +def test_linear_3eq_order1_type4_long_dsolve_dotprodsimp(): + eq, sol = _linear_3eq_order1_type4_long() + + # XXX: Only works with dotprodsimp see + # test_linear_3eq_order1_type4_long_dsolve_slow_xfail which is too slow + with dotprodsimp(True): + dsolve_sol = dsolve(eq) + + dsolve_sol1 = [_simpsol(sol) for sol in dsolve_sol] + assert dsolve_sol1 == sol + + +@tooslow +def test_linear_3eq_order1_type4_long_check(): + eq, sol = _linear_3eq_order1_type4_long() + assert checksysodesol(eq, sol) == (True, [0, 0, 0]) + + +def test_dsolve_system(): + f, g = symbols("f g", cls=Function) + x = symbols("x") + eqs = [Eq(f(x).diff(x), f(x) + g(x)), Eq(g(x).diff(x), f(x) + g(x))] + funcs = [f(x), g(x)] + + sol = [[Eq(f(x), -C1 + C2*exp(2*x)), Eq(g(x), C1 + C2*exp(2*x))]] + assert dsolve_system(eqs, funcs=funcs, t=x, doit=True) == sol + + raises(ValueError, lambda: dsolve_system(1)) + raises(ValueError, lambda: dsolve_system(eqs, 1)) + raises(ValueError, lambda: dsolve_system(eqs, funcs, 1)) + raises(ValueError, lambda: dsolve_system(eqs, funcs[:1], x)) + + eq = (Eq(f(x).diff(x), 12 * f(x) - 6 * g(x)), Eq(g(x).diff(x) ** 2, 11 * f(x) + 3 * g(x))) + raises(NotImplementedError, lambda: dsolve_system(eq) == ([], [])) + + raises(NotImplementedError, lambda: dsolve_system(eq, funcs=[f(x), g(x)]) == ([], [])) + raises(NotImplementedError, lambda: dsolve_system(eq, funcs=[f(x), g(x)], t=x) == ([], [])) + raises(NotImplementedError, lambda: dsolve_system(eq, funcs=[f(x), g(x)], t=x, ics={f(0): 1, g(0): 1}) == ([], [])) + raises(NotImplementedError, lambda: dsolve_system(eq, t=x, ics={f(0): 1, g(0): 1}) == ([], [])) + raises(NotImplementedError, lambda: dsolve_system(eq, ics={f(0): 1, g(0): 1}) == ([], [])) + raises(NotImplementedError, lambda: dsolve_system(eq, funcs=[f(x), g(x)], ics={f(0): 1, g(0): 1}) == ([], [])) + +def test_dsolve(): + + f, g = symbols('f g', cls=Function) + x, y = symbols('x y') + + eqs = [f(x).diff(x) - x, f(x).diff(x) + x] + with raises(ValueError): + dsolve(eqs) + + eqs = [f(x, y).diff(x)] + with raises(ValueError): + dsolve(eqs) + + eqs = [f(x, y).diff(x)+g(x).diff(x), g(x).diff(x)] + with raises(ValueError): + dsolve(eqs) + + +@slow +def test_higher_order1_slow1(): + x, y = symbols("x y", cls=Function) + t = symbols("t") + + eq = [ + Eq(diff(x(t),t,t), (log(t)+t**2)*diff(x(t),t)+(log(t)+t**2)*3*diff(y(t),t)), + Eq(diff(y(t),t,t), (log(t)+t**2)*2*diff(x(t),t)+(log(t)+t**2)*9*diff(y(t),t)) + ] + sol, = dsolve_system(eq, simplify=False, doit=False) + # The solution is too long to write out explicitly and checkodesol is too + # slow so we test for particular values of t: + for e in eq: + res = (e.lhs - e.rhs).subs({sol[0].lhs:sol[0].rhs, sol[1].lhs:sol[1].rhs}) + res = res.subs({d: d.doit(deep=False) for d in res.atoms(Derivative)}) + assert ratsimp(res.subs(t, 1)) == 0 + + +def test_second_order_type2_slow1(): + x, y, z = symbols('x, y, z', cls=Function) + t, l = symbols('t, l') + + eqs1 = [Eq(Derivative(x(t), (t, 2)), t*(2*x(t) + y(t))), + Eq(Derivative(y(t), (t, 2)), t*(-x(t) + 2*y(t)))] + sol1 = [Eq(x(t), I*C1*airyai(t*(2 - I)**(S(1)/3)) + I*C2*airybi(t*(2 - I)**(S(1)/3)) - I*C3*airyai(t*(2 + + I)**(S(1)/3)) - I*C4*airybi(t*(2 + I)**(S(1)/3))), + Eq(y(t), C1*airyai(t*(2 - I)**(S(1)/3)) + C2*airybi(t*(2 - I)**(S(1)/3)) + C3*airyai(t*(2 + I)**(S(1)/3)) + + C4*airybi(t*(2 + I)**(S(1)/3)))] + assert dsolve(eqs1) == sol1 + assert checksysodesol(eqs1, sol1) == (True, [0, 0]) + + +@tooslow +@XFAIL +def test_nonlinear_3eq_order1_type1(): + a, b, c = symbols('a b c') + + eqs = [ + a * f(x).diff(x) - (b - c) * g(x) * h(x), + b * g(x).diff(x) - (c - a) * h(x) * f(x), + c * h(x).diff(x) - (a - b) * f(x) * g(x), + ] + + assert dsolve(eqs) # NotImplementedError + + +@XFAIL +def test_nonlinear_3eq_order1_type4(): + eqs = [ + Eq(f(x).diff(x), (2*h(x)*g(x) - 3*g(x)*h(x))), + Eq(g(x).diff(x), (4*f(x)*h(x) - 2*h(x)*f(x))), + Eq(h(x).diff(x), (3*g(x)*f(x) - 4*f(x)*g(x))), + ] + dsolve(eqs) # KeyError when matching + # sol = ? + # assert dsolve_sol == sol + # assert checksysodesol(eqs, dsolve_sol) == (True, [0, 0, 0]) + + +@tooslow +@XFAIL +def test_nonlinear_3eq_order1_type3(): + eqs = [ + Eq(f(x).diff(x), (2*f(x)**2 - 3 )), + Eq(g(x).diff(x), (4 - 2*h(x) )), + Eq(h(x).diff(x), (3*h(x) - 4*f(x)**2)), + ] + dsolve(eqs) # Not sure if this finishes... + # sol = ? + # assert dsolve_sol == sol + # assert checksysodesol(eqs, dsolve_sol) == (True, [0, 0, 0]) + + +@XFAIL +def test_nonlinear_3eq_order1_type5(): + eqs = [ + Eq(f(x).diff(x), f(x)*(2*f(x) - 3*g(x))), + Eq(g(x).diff(x), g(x)*(4*g(x) - 2*h(x))), + Eq(h(x).diff(x), h(x)*(3*h(x) - 4*f(x))), + ] + dsolve(eqs) # KeyError + # sol = ? + # assert dsolve_sol == sol + # assert checksysodesol(eqs, dsolve_sol) == (True, [0, 0, 0]) + + +def test_linear_2eq_order1(): + x, y, z = symbols('x, y, z', cls=Function) + k, l, m, n = symbols('k, l, m, n', Integer=True) + t = Symbol('t') + x0, y0 = symbols('x0, y0', cls=Function) + + eq1 = (Eq(diff(x(t),t), x(t) + y(t) + 9), Eq(diff(y(t),t), 2*x(t) + 5*y(t) + 23)) + sol1 = [Eq(x(t), C1*exp(t*(sqrt(6) + 3)) + C2*exp(t*(-sqrt(6) + 3)) - Rational(22, 3)), \ + Eq(y(t), C1*(2 + sqrt(6))*exp(t*(sqrt(6) + 3)) + C2*(-sqrt(6) + 2)*exp(t*(-sqrt(6) + 3)) - Rational(5, 3))] + assert checksysodesol(eq1, sol1) == (True, [0, 0]) + + eq2 = (Eq(diff(x(t),t), x(t) + y(t) + 81), Eq(diff(y(t),t), -2*x(t) + y(t) + 23)) + sol2 = [Eq(x(t), (C1*cos(sqrt(2)*t) + C2*sin(sqrt(2)*t))*exp(t) - Rational(58, 3)), \ + Eq(y(t), (-sqrt(2)*C1*sin(sqrt(2)*t) + sqrt(2)*C2*cos(sqrt(2)*t))*exp(t) - Rational(185, 3))] + assert checksysodesol(eq2, sol2) == (True, [0, 0]) + + eq3 = (Eq(diff(x(t),t), 5*t*x(t) + 2*y(t)), Eq(diff(y(t),t), 2*x(t) + 5*t*y(t))) + sol3 = [Eq(x(t), (C1*exp(2*t) + C2*exp(-2*t))*exp(Rational(5, 2)*t**2)), \ + Eq(y(t), (C1*exp(2*t) - C2*exp(-2*t))*exp(Rational(5, 2)*t**2))] + assert checksysodesol(eq3, sol3) == (True, [0, 0]) + + eq4 = (Eq(diff(x(t),t), 5*t*x(t) + t**2*y(t)), Eq(diff(y(t),t), -t**2*x(t) + 5*t*y(t))) + sol4 = [Eq(x(t), (C1*cos((t**3)/3) + C2*sin((t**3)/3))*exp(Rational(5, 2)*t**2)), \ + Eq(y(t), (-C1*sin((t**3)/3) + C2*cos((t**3)/3))*exp(Rational(5, 2)*t**2))] + assert checksysodesol(eq4, sol4) == (True, [0, 0]) + + eq5 = (Eq(diff(x(t),t), 5*t*x(t) + t**2*y(t)), Eq(diff(y(t),t), -t**2*x(t) + (5*t+9*t**2)*y(t))) + sol5 = [Eq(x(t), (C1*exp((sqrt(77)/2 + Rational(9, 2))*(t**3)/3) + \ + C2*exp((-sqrt(77)/2 + Rational(9, 2))*(t**3)/3))*exp(Rational(5, 2)*t**2)), \ + Eq(y(t), (C1*(sqrt(77)/2 + Rational(9, 2))*exp((sqrt(77)/2 + Rational(9, 2))*(t**3)/3) + \ + C2*(-sqrt(77)/2 + Rational(9, 2))*exp((-sqrt(77)/2 + Rational(9, 2))*(t**3)/3))*exp(Rational(5, 2)*t**2))] + assert checksysodesol(eq5, sol5) == (True, [0, 0]) + + eq6 = (Eq(diff(x(t),t), 5*t*x(t) + t**2*y(t)), Eq(diff(y(t),t), (1-t**2)*x(t) + (5*t+9*t**2)*y(t))) + sol6 = [Eq(x(t), C1*x0(t) + C2*x0(t)*Integral(t**2*exp(Integral(5*t, t))*exp(Integral(9*t**2 + 5*t, t))/x0(t)**2, t)), \ + Eq(y(t), C1*y0(t) + C2*(y0(t)*Integral(t**2*exp(Integral(5*t, t))*exp(Integral(9*t**2 + 5*t, t))/x0(t)**2, t) + \ + exp(Integral(5*t, t))*exp(Integral(9*t**2 + 5*t, t))/x0(t)))] + s = dsolve(eq6) + assert s == sol6 # too complicated to test with subs and simplify + # assert checksysodesol(eq10, sol10) == (True, [0, 0]) # this one fails + + +def test_nonlinear_2eq_order1(): + x, y, z = symbols('x, y, z', cls=Function) + t = Symbol('t') + eq1 = (Eq(diff(x(t),t),x(t)*y(t)**3), Eq(diff(y(t),t),y(t)**5)) + sol1 = [ + Eq(x(t), C1*exp((-1/(4*C2 + 4*t))**(Rational(-1, 4)))), + Eq(y(t), -(-1/(4*C2 + 4*t))**Rational(1, 4)), + Eq(x(t), C1*exp(-1/(-1/(4*C2 + 4*t))**Rational(1, 4))), + Eq(y(t), (-1/(4*C2 + 4*t))**Rational(1, 4)), + Eq(x(t), C1*exp(-I/(-1/(4*C2 + 4*t))**Rational(1, 4))), + Eq(y(t), -I*(-1/(4*C2 + 4*t))**Rational(1, 4)), + Eq(x(t), C1*exp(I/(-1/(4*C2 + 4*t))**Rational(1, 4))), + Eq(y(t), I*(-1/(4*C2 + 4*t))**Rational(1, 4))] + assert dsolve(eq1) == sol1 + assert checksysodesol(eq1, sol1) == (True, [0, 0]) + + eq2 = (Eq(diff(x(t),t), exp(3*x(t))*y(t)**3),Eq(diff(y(t),t), y(t)**5)) + sol2 = [ + Eq(x(t), -log(C1 - 3/(-1/(4*C2 + 4*t))**Rational(1, 4))/3), + Eq(y(t), -(-1/(4*C2 + 4*t))**Rational(1, 4)), + Eq(x(t), -log(C1 + 3/(-1/(4*C2 + 4*t))**Rational(1, 4))/3), + Eq(y(t), (-1/(4*C2 + 4*t))**Rational(1, 4)), + Eq(x(t), -log(C1 + 3*I/(-1/(4*C2 + 4*t))**Rational(1, 4))/3), + Eq(y(t), -I*(-1/(4*C2 + 4*t))**Rational(1, 4)), + Eq(x(t), -log(C1 - 3*I/(-1/(4*C2 + 4*t))**Rational(1, 4))/3), + Eq(y(t), I*(-1/(4*C2 + 4*t))**Rational(1, 4))] + assert dsolve(eq2) == sol2 + assert checksysodesol(eq2, sol2) == (True, [0, 0]) + + eq3 = (Eq(diff(x(t),t), y(t)*x(t)), Eq(diff(y(t),t), x(t)**3)) + tt = Rational(2, 3) + sol3 = [ + Eq(x(t), 6**tt/(6*(-sinh(sqrt(C1)*(C2 + t)/2)/sqrt(C1))**tt)), + Eq(y(t), sqrt(C1 + C1/sinh(sqrt(C1)*(C2 + t)/2)**2)/3)] + assert dsolve(eq3) == sol3 + # FIXME: assert checksysodesol(eq3, sol3) == (True, [0, 0]) + + eq4 = (Eq(diff(x(t),t),x(t)*y(t)*sin(t)**2), Eq(diff(y(t),t),y(t)**2*sin(t)**2)) + sol4 = {Eq(x(t), -2*exp(C1)/(C2*exp(C1) + t - sin(2*t)/2)), Eq(y(t), -2/(C1 + t - sin(2*t)/2))} + assert dsolve(eq4) == sol4 + # FIXME: assert checksysodesol(eq4, sol4) == (True, [0, 0]) + + eq5 = (Eq(x(t),t*diff(x(t),t)+diff(x(t),t)*diff(y(t),t)), Eq(y(t),t*diff(y(t),t)+diff(y(t),t)**2)) + sol5 = {Eq(x(t), C1*C2 + C1*t), Eq(y(t), C2**2 + C2*t)} + assert dsolve(eq5) == sol5 + assert checksysodesol(eq5, sol5) == (True, [0, 0]) + + eq6 = (Eq(diff(x(t),t),x(t)**2*y(t)**3), Eq(diff(y(t),t),y(t)**5)) + sol6 = [ + Eq(x(t), 1/(C1 - 1/(-1/(4*C2 + 4*t))**Rational(1, 4))), + Eq(y(t), -(-1/(4*C2 + 4*t))**Rational(1, 4)), + Eq(x(t), 1/(C1 + (-1/(4*C2 + 4*t))**(Rational(-1, 4)))), + Eq(y(t), (-1/(4*C2 + 4*t))**Rational(1, 4)), + Eq(x(t), 1/(C1 + I/(-1/(4*C2 + 4*t))**Rational(1, 4))), + Eq(y(t), -I*(-1/(4*C2 + 4*t))**Rational(1, 4)), + Eq(x(t), 1/(C1 - I/(-1/(4*C2 + 4*t))**Rational(1, 4))), + Eq(y(t), I*(-1/(4*C2 + 4*t))**Rational(1, 4))] + assert dsolve(eq6) == sol6 + assert checksysodesol(eq6, sol6) == (True, [0, 0]) + + +@slow +def test_nonlinear_3eq_order1(): + x, y, z = symbols('x, y, z', cls=Function) + t, u = symbols('t u') + eq1 = (4*diff(x(t),t) + 2*y(t)*z(t), 3*diff(y(t),t) - z(t)*x(t), 5*diff(z(t),t) - x(t)*y(t)) + sol1 = [Eq(4*Integral(1/(sqrt(-4*u**2 - 3*C1 + C2)*sqrt(-4*u**2 + 5*C1 - C2)), (u, x(t))), + C3 - sqrt(15)*t/15), Eq(3*Integral(1/(sqrt(-6*u**2 - C1 + 5*C2)*sqrt(3*u**2 + C1 - 4*C2)), + (u, y(t))), C3 + sqrt(5)*t/10), Eq(5*Integral(1/(sqrt(-10*u**2 - 3*C1 + C2)* + sqrt(5*u**2 + 4*C1 - C2)), (u, z(t))), C3 + sqrt(3)*t/6)] + assert [i.dummy_eq(j) for i, j in zip(dsolve(eq1), sol1)] + # FIXME: assert checksysodesol(eq1, sol1) == (True, [0, 0, 0]) + + eq2 = (4*diff(x(t),t) + 2*y(t)*z(t)*sin(t), 3*diff(y(t),t) - z(t)*x(t)*sin(t), 5*diff(z(t),t) - x(t)*y(t)*sin(t)) + sol2 = [Eq(3*Integral(1/(sqrt(-6*u**2 - C1 + 5*C2)*sqrt(3*u**2 + C1 - 4*C2)), (u, x(t))), C3 + + sqrt(5)*cos(t)/10), Eq(4*Integral(1/(sqrt(-4*u**2 - 3*C1 + C2)*sqrt(-4*u**2 + 5*C1 - C2)), + (u, y(t))), C3 - sqrt(15)*cos(t)/15), Eq(5*Integral(1/(sqrt(-10*u**2 - 3*C1 + C2)* + sqrt(5*u**2 + 4*C1 - C2)), (u, z(t))), C3 + sqrt(3)*cos(t)/6)] + assert [i.dummy_eq(j) for i, j in zip(dsolve(eq2), sol2)] + # FIXME: assert checksysodesol(eq2, sol2) == (True, [0, 0, 0]) + + +def test_C1_function_9239(): + t = Symbol('t') + C1 = Function('C1') + C2 = Function('C2') + C3 = Symbol('C3') + C4 = Symbol('C4') + eq = (Eq(diff(C1(t), t), 9*C2(t)), Eq(diff(C2(t), t), 12*C1(t))) + sol = [Eq(C1(t), 9*C3*exp(6*sqrt(3)*t) + 9*C4*exp(-6*sqrt(3)*t)), + Eq(C2(t), 6*sqrt(3)*C3*exp(6*sqrt(3)*t) - 6*sqrt(3)*C4*exp(-6*sqrt(3)*t))] + assert checksysodesol(eq, sol) == (True, [0, 0]) + + +def test_dsolve_linsystem_symbol(): + eps = Symbol('epsilon', positive=True) + eq1 = (Eq(diff(f(x), x), -eps*g(x)), Eq(diff(g(x), x), eps*f(x))) + sol1 = [Eq(f(x), -C1*eps*cos(eps*x) - C2*eps*sin(eps*x)), + Eq(g(x), -C1*eps*sin(eps*x) + C2*eps*cos(eps*x))] + assert checksysodesol(eq1, sol1) == (True, [0, 0]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/pde.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/pde.py new file mode 100644 index 0000000000000000000000000000000000000000..791ac67ae681ea952ea6e1dabacb7220d1843ebc --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/pde.py @@ -0,0 +1,966 @@ +""" +This module contains pdsolve() and different helper functions that it +uses. It is heavily inspired by the ode module and hence the basic +infrastructure remains the same. + +**Functions in this module** + + These are the user functions in this module: + + - pdsolve() - Solves PDE's + - classify_pde() - Classifies PDEs into possible hints for dsolve(). + - pde_separate() - Separate variables in partial differential equation either by + additive or multiplicative separation approach. + + These are the helper functions in this module: + + - pde_separate_add() - Helper function for searching additive separable solutions. + - pde_separate_mul() - Helper function for searching multiplicative + separable solutions. + +**Currently implemented solver methods** + +The following methods are implemented for solving partial differential +equations. See the docstrings of the various pde_hint() functions for +more information on each (run help(pde)): + + - 1st order linear homogeneous partial differential equations + with constant coefficients. + - 1st order linear general partial differential equations + with constant coefficients. + - 1st order linear partial differential equations with + variable coefficients. + +""" +from functools import reduce + +from itertools import combinations_with_replacement +from sympy.simplify import simplify # type: ignore +from sympy.core import Add, S +from sympy.core.function import Function, expand, AppliedUndef, Subs +from sympy.core.relational import Equality, Eq +from sympy.core.symbol import Symbol, Wild, symbols +from sympy.functions import exp +from sympy.integrals.integrals import Integral, integrate +from sympy.utilities.iterables import has_dups, is_sequence +from sympy.utilities.misc import filldedent + +from sympy.solvers.deutils import _preprocess, ode_order, _desolve +from sympy.solvers.solvers import solve +from sympy.simplify.radsimp import collect + +import operator + + +allhints = ( + "1st_linear_constant_coeff_homogeneous", + "1st_linear_constant_coeff", + "1st_linear_constant_coeff_Integral", + "1st_linear_variable_coeff" + ) + + +def pdsolve(eq, func=None, hint='default', dict=False, solvefun=None, **kwargs): + """ + Solves any (supported) kind of partial differential equation. + + **Usage** + + pdsolve(eq, f(x,y), hint) -> Solve partial differential equation + eq for function f(x,y), using method hint. + + **Details** + + ``eq`` can be any supported partial differential equation (see + the pde docstring for supported methods). This can either + be an Equality, or an expression, which is assumed to be + equal to 0. + + ``f(x,y)`` is a function of two variables whose derivatives in that + variable make up the partial differential equation. In many + cases it is not necessary to provide this; it will be autodetected + (and an error raised if it could not be detected). + + ``hint`` is the solving method that you want pdsolve to use. Use + classify_pde(eq, f(x,y)) to get all of the possible hints for + a PDE. The default hint, 'default', will use whatever hint + is returned first by classify_pde(). See Hints below for + more options that you can use for hint. + + ``solvefun`` is the convention used for arbitrary functions returned + by the PDE solver. If not set by the user, it is set by default + to be F. + + **Hints** + + Aside from the various solving methods, there are also some + meta-hints that you can pass to pdsolve(): + + "default": + This uses whatever hint is returned first by + classify_pde(). This is the default argument to + pdsolve(). + + "all": + To make pdsolve apply all relevant classification hints, + use pdsolve(PDE, func, hint="all"). This will return a + dictionary of hint:solution terms. If a hint causes + pdsolve to raise the NotImplementedError, value of that + hint's key will be the exception object raised. The + dictionary will also include some special keys: + + - order: The order of the PDE. See also ode_order() in + deutils.py + - default: The solution that would be returned by + default. This is the one produced by the hint that + appears first in the tuple returned by classify_pde(). + + "all_Integral": + This is the same as "all", except if a hint also has a + corresponding "_Integral" hint, it only returns the + "_Integral" hint. This is useful if "all" causes + pdsolve() to hang because of a difficult or impossible + integral. This meta-hint will also be much faster than + "all", because integrate() is an expensive routine. + + See also the classify_pde() docstring for more info on hints, + and the pde docstring for a list of all supported hints. + + **Tips** + - You can declare the derivative of an unknown function this way: + + >>> from sympy import Function, Derivative + >>> from sympy.abc import x, y # x and y are the independent variables + >>> f = Function("f")(x, y) # f is a function of x and y + >>> # fx will be the partial derivative of f with respect to x + >>> fx = Derivative(f, x) + >>> # fy will be the partial derivative of f with respect to y + >>> fy = Derivative(f, y) + + - See test_pde.py for many tests, which serves also as a set of + examples for how to use pdsolve(). + - pdsolve always returns an Equality class (except for the case + when the hint is "all" or "all_Integral"). Note that it is not possible + to get an explicit solution for f(x, y) as in the case of ODE's + - Do help(pde.pde_hintname) to get help more information on a + specific hint + + + Examples + ======== + + >>> from sympy.solvers.pde import pdsolve + >>> from sympy import Function, Eq + >>> from sympy.abc import x, y + >>> f = Function('f') + >>> u = f(x, y) + >>> ux = u.diff(x) + >>> uy = u.diff(y) + >>> eq = Eq(1 + (2*(ux/u)) + (3*(uy/u)), 0) + >>> pdsolve(eq) + Eq(f(x, y), F(3*x - 2*y)*exp(-2*x/13 - 3*y/13)) + + """ + + if not solvefun: + solvefun = Function('F') + + # See the docstring of _desolve for more details. + hints = _desolve(eq, func=func, hint=hint, simplify=True, + type='pde', **kwargs) + eq = hints.pop('eq', False) + all_ = hints.pop('all', False) + + if all_: + # TODO : 'best' hint should be implemented when adequate + # number of hints are added. + pdedict = {} + failed_hints = {} + gethints = classify_pde(eq, dict=True) + pdedict.update({'order': gethints['order'], + 'default': gethints['default']}) + for hint in hints: + try: + rv = _helper_simplify(eq, hint, hints[hint]['func'], + hints[hint]['order'], hints[hint][hint], solvefun) + except NotImplementedError as detail: + failed_hints[hint] = detail + else: + pdedict[hint] = rv + pdedict.update(failed_hints) + return pdedict + + else: + return _helper_simplify(eq, hints['hint'], hints['func'], + hints['order'], hints[hints['hint']], solvefun) + + +def _helper_simplify(eq, hint, func, order, match, solvefun): + """Helper function of pdsolve that calls the respective + pde functions to solve for the partial differential + equations. This minimizes the computation in + calling _desolve multiple times. + """ + solvefunc = globals()["pde_" + hint.removesuffix("_Integral")] + return _handle_Integral(solvefunc(eq, func, order, + match, solvefun), func, order, hint) + + +def _handle_Integral(expr, func, order, hint): + r""" + Converts a solution with integrals in it into an actual solution. + + Simplifies the integral mainly using doit() + """ + if hint.endswith("_Integral"): + return expr + + elif hint == "1st_linear_constant_coeff": + return simplify(expr.doit()) + + else: + return expr + + +def classify_pde(eq, func=None, dict=False, *, prep=True, **kwargs): + """ + Returns a tuple of possible pdsolve() classifications for a PDE. + + The tuple is ordered so that first item is the classification that + pdsolve() uses to solve the PDE by default. In general, + classifications near the beginning of the list will produce + better solutions faster than those near the end, though there are + always exceptions. To make pdsolve use a different classification, + use pdsolve(PDE, func, hint=). See also the pdsolve() + docstring for different meta-hints you can use. + + If ``dict`` is true, classify_pde() will return a dictionary of + hint:match expression terms. This is intended for internal use by + pdsolve(). Note that because dictionaries are ordered arbitrarily, + this will most likely not be in the same order as the tuple. + + You can get help on different hints by doing help(pde.pde_hintname), + where hintname is the name of the hint without "_Integral". + + See sympy.pde.allhints or the sympy.pde docstring for a list of all + supported hints that can be returned from classify_pde. + + + Examples + ======== + + >>> from sympy.solvers.pde import classify_pde + >>> from sympy import Function, Eq + >>> from sympy.abc import x, y + >>> f = Function('f') + >>> u = f(x, y) + >>> ux = u.diff(x) + >>> uy = u.diff(y) + >>> eq = Eq(1 + (2*(ux/u)) + (3*(uy/u)), 0) + >>> classify_pde(eq) + ('1st_linear_constant_coeff_homogeneous',) + """ + + if func and len(func.args) != 2: + raise NotImplementedError("Right now only partial " + "differential equations of two variables are supported") + + if prep or func is None: + prep, func_ = _preprocess(eq, func) + if func is None: + func = func_ + + if isinstance(eq, Equality): + if eq.rhs != 0: + return classify_pde(eq.lhs - eq.rhs, func) + eq = eq.lhs + + f = func.func + x = func.args[0] + y = func.args[1] + fx = f(x,y).diff(x) + fy = f(x,y).diff(y) + + # TODO : For now pde.py uses support offered by the ode_order function + # to find the order with respect to a multi-variable function. An + # improvement could be to classify the order of the PDE on the basis of + # individual variables. + order = ode_order(eq, f(x,y)) + + # hint:matchdict or hint:(tuple of matchdicts) + # Also will contain "default": and "order":order items. + matching_hints = {'order': order} + + if not order: + if dict: + matching_hints["default"] = None + return matching_hints + return () + + eq = expand(eq) + + a = Wild('a', exclude = [f(x,y)]) + b = Wild('b', exclude = [f(x,y), fx, fy, x, y]) + c = Wild('c', exclude = [f(x,y), fx, fy, x, y]) + d = Wild('d', exclude = [f(x,y), fx, fy, x, y]) + e = Wild('e', exclude = [f(x,y), fx, fy]) + n = Wild('n', exclude = [x, y]) + # Try removing the smallest power of f(x,y) + # from the highest partial derivatives of f(x,y) + reduced_eq = eq + if eq.is_Add: + power = None + for i in set(combinations_with_replacement((x,y), order)): + coeff = eq.coeff(f(x,y).diff(*i)) + if coeff == 1: + continue + match = coeff.match(a*f(x,y)**n) + if match and match[a]: + if power is None or match[n] < power: + power = match[n] + if power: + den = f(x,y)**power + reduced_eq = Add(*[arg/den for arg in eq.args]) + + if order == 1: + reduced_eq = collect(reduced_eq, f(x, y)) + r = reduced_eq.match(b*fx + c*fy + d*f(x,y) + e) + if r: + if not r[e]: + ## Linear first-order homogeneous partial-differential + ## equation with constant coefficients + r.update({'b': b, 'c': c, 'd': d}) + matching_hints["1st_linear_constant_coeff_homogeneous"] = r + elif r[b]**2 + r[c]**2 != 0: + ## Linear first-order general partial-differential + ## equation with constant coefficients + r.update({'b': b, 'c': c, 'd': d, 'e': e}) + matching_hints["1st_linear_constant_coeff"] = r + matching_hints["1st_linear_constant_coeff_Integral"] = r + + else: + b = Wild('b', exclude=[f(x, y), fx, fy]) + c = Wild('c', exclude=[f(x, y), fx, fy]) + d = Wild('d', exclude=[f(x, y), fx, fy]) + r = reduced_eq.match(b*fx + c*fy + d*f(x,y) + e) + if r: + r.update({'b': b, 'c': c, 'd': d, 'e': e}) + matching_hints["1st_linear_variable_coeff"] = r + + # Order keys based on allhints. + rettuple = tuple(i for i in allhints if i in matching_hints) + + if dict: + # Dictionaries are ordered arbitrarily, so make note of which + # hint would come first for pdsolve(). Use an ordered dict in Py 3. + matching_hints["default"] = None + matching_hints["ordered_hints"] = rettuple + for i in allhints: + if i in matching_hints: + matching_hints["default"] = i + break + return matching_hints + return rettuple + + +def checkpdesol(pde, sol, func=None, solve_for_func=True): + """ + Checks if the given solution satisfies the partial differential + equation. + + pde is the partial differential equation which can be given in the + form of an equation or an expression. sol is the solution for which + the pde is to be checked. This can also be given in an equation or + an expression form. If the function is not provided, the helper + function _preprocess from deutils is used to identify the function. + + If a sequence of solutions is passed, the same sort of container will be + used to return the result for each solution. + + The following methods are currently being implemented to check if the + solution satisfies the PDE: + + 1. Directly substitute the solution in the PDE and check. If the + solution has not been solved for f, then it will solve for f + provided solve_for_func has not been set to False. + + If the solution satisfies the PDE, then a tuple (True, 0) is returned. + Otherwise a tuple (False, expr) where expr is the value obtained + after substituting the solution in the PDE. However if a known solution + returns False, it may be due to the inability of doit() to simplify it to zero. + + Examples + ======== + + >>> from sympy import Function, symbols + >>> from sympy.solvers.pde import checkpdesol, pdsolve + >>> x, y = symbols('x y') + >>> f = Function('f') + >>> eq = 2*f(x,y) + 3*f(x,y).diff(x) + 4*f(x,y).diff(y) + >>> sol = pdsolve(eq) + >>> assert checkpdesol(eq, sol)[0] + >>> eq = x*f(x,y) + f(x,y).diff(x) + >>> checkpdesol(eq, sol) + (False, (x*F(4*x - 3*y) - 6*F(4*x - 3*y)/25 + 4*Subs(Derivative(F(_xi_1), _xi_1), _xi_1, 4*x - 3*y))*exp(-6*x/25 - 8*y/25)) + """ + + # Converting the pde into an equation + if not isinstance(pde, Equality): + pde = Eq(pde, 0) + + # If no function is given, try finding the function present. + if func is None: + try: + _, func = _preprocess(pde.lhs) + except ValueError: + funcs = [s.atoms(AppliedUndef) for s in ( + sol if is_sequence(sol, set) else [sol])] + funcs = set().union(funcs) + if len(funcs) != 1: + raise ValueError( + 'must pass func arg to checkpdesol for this case.') + func = funcs.pop() + + # If the given solution is in the form of a list or a set + # then return a list or set of tuples. + if is_sequence(sol, set): + return type(sol)([checkpdesol( + pde, i, func=func, + solve_for_func=solve_for_func) for i in sol]) + + # Convert solution into an equation + if not isinstance(sol, Equality): + sol = Eq(func, sol) + elif sol.rhs == func: + sol = sol.reversed + + # Try solving for the function + solved = sol.lhs == func and not sol.rhs.has(func) + if solve_for_func and not solved: + solved = solve(sol, func) + if solved: + if len(solved) == 1: + return checkpdesol(pde, Eq(func, solved[0]), + func=func, solve_for_func=False) + else: + return checkpdesol(pde, [Eq(func, t) for t in solved], + func=func, solve_for_func=False) + + # try direct substitution of the solution into the PDE and simplify + if sol.lhs == func: + pde = pde.lhs - pde.rhs + s = simplify(pde.subs(func, sol.rhs).doit()) + return s is S.Zero, s + + raise NotImplementedError(filldedent(''' + Unable to test if %s is a solution to %s.''' % (sol, pde))) + + + +def pde_1st_linear_constant_coeff_homogeneous(eq, func, order, match, solvefun): + r""" + Solves a first order linear homogeneous + partial differential equation with constant coefficients. + + The general form of this partial differential equation is + + .. math:: a \frac{\partial f(x,y)}{\partial x} + + b \frac{\partial f(x,y)}{\partial y} + c f(x,y) = 0 + + where `a`, `b` and `c` are constants. + + The general solution is of the form: + + .. math:: + f(x, y) = F(- a y + b x ) e^{- \frac{c (a x + b y)}{a^2 + b^2}} + + and can be found in SymPy with ``pdsolve``:: + + >>> from sympy.solvers import pdsolve + >>> from sympy.abc import x, y, a, b, c + >>> from sympy import Function, pprint + >>> f = Function('f') + >>> u = f(x,y) + >>> ux = u.diff(x) + >>> uy = u.diff(y) + >>> genform = a*ux + b*uy + c*u + >>> pprint(genform) + d d + a*--(f(x, y)) + b*--(f(x, y)) + c*f(x, y) + dx dy + + >>> pprint(pdsolve(genform)) + -c*(a*x + b*y) + --------------- + 2 2 + a + b + f(x, y) = F(-a*y + b*x)*e + + Examples + ======== + + >>> from sympy import pdsolve + >>> from sympy import Function, pprint + >>> from sympy.abc import x,y + >>> f = Function('f') + >>> pdsolve(f(x,y) + f(x,y).diff(x) + f(x,y).diff(y)) + Eq(f(x, y), F(x - y)*exp(-x/2 - y/2)) + >>> pprint(pdsolve(f(x,y) + f(x,y).diff(x) + f(x,y).diff(y))) + x y + - - - - + 2 2 + f(x, y) = F(x - y)*e + + References + ========== + + - Viktor Grigoryan, "Partial Differential Equations" + Math 124A - Fall 2010, pp.7 + + """ + # TODO : For now homogeneous first order linear PDE's having + # two variables are implemented. Once there is support for + # solving systems of ODE's, this can be extended to n variables. + + f = func.func + x = func.args[0] + y = func.args[1] + b = match[match['b']] + c = match[match['c']] + d = match[match['d']] + return Eq(f(x,y), exp(-S(d)/(b**2 + c**2)*(b*x + c*y))*solvefun(c*x - b*y)) + + +def pde_1st_linear_constant_coeff(eq, func, order, match, solvefun): + r""" + Solves a first order linear partial differential equation + with constant coefficients. + + The general form of this partial differential equation is + + .. math:: a \frac{\partial f(x,y)}{\partial x} + + b \frac{\partial f(x,y)}{\partial y} + + c f(x,y) = G(x,y) + + where `a`, `b` and `c` are constants and `G(x, y)` can be an arbitrary + function in `x` and `y`. + + The general solution of the PDE is: + + .. math:: + f(x, y) = \left. \left[F(\eta) + \frac{1}{a^2 + b^2} + \int\limits^{a x + b y} G\left(\frac{a \xi + b \eta}{a^2 + b^2}, + \frac{- a \eta + b \xi}{a^2 + b^2} \right) + e^{\frac{c \xi}{a^2 + b^2}}\, d\xi\right] + e^{- \frac{c \xi}{a^2 + b^2}} + \right|_{\substack{\eta=- a y + b x\\ \xi=a x + b y }}\, , + + where `F(\eta)` is an arbitrary single-valued function. The solution + can be found in SymPy with ``pdsolve``:: + + >>> from sympy.solvers import pdsolve + >>> from sympy.abc import x, y, a, b, c + >>> from sympy import Function, pprint + >>> f = Function('f') + >>> G = Function('G') + >>> u = f(x, y) + >>> ux = u.diff(x) + >>> uy = u.diff(y) + >>> genform = a*ux + b*uy + c*u - G(x,y) + >>> pprint(genform) + d d + a*--(f(x, y)) + b*--(f(x, y)) + c*f(x, y) - G(x, y) + dx dy + >>> pprint(pdsolve(genform, hint='1st_linear_constant_coeff_Integral')) + // a*x + b*y \ \| + || / | || + || | | || + || | c*xi | || + || | ------- | || + || | 2 2 | || + || | /a*xi + b*eta -a*eta + b*xi\ a + b | || + || | G|------------, -------------|*e d(xi)| || + || | | 2 2 2 2 | | || + || | \ a + b a + b / | -c*xi || + || | | -------|| + || / | 2 2|| + || | a + b || + f(x, y) = ||F(eta) + -------------------------------------------------------|*e || + || 2 2 | || + \\ a + b / /|eta=-a*y + b*x, xi=a*x + b*y + + Examples + ======== + + >>> from sympy.solvers.pde import pdsolve + >>> from sympy import Function, pprint, exp + >>> from sympy.abc import x,y + >>> f = Function('f') + >>> eq = -2*f(x,y).diff(x) + 4*f(x,y).diff(y) + 5*f(x,y) - exp(x + 3*y) + >>> pdsolve(eq) + Eq(f(x, y), (F(4*x + 2*y)*exp(x/2) + exp(x + 4*y)/15)*exp(-y)) + + References + ========== + + - Viktor Grigoryan, "Partial Differential Equations" + Math 124A - Fall 2010, pp.7 + + """ + + # TODO : For now homogeneous first order linear PDE's having + # two variables are implemented. Once there is support for + # solving systems of ODE's, this can be extended to n variables. + xi, eta = symbols("xi eta") + f = func.func + x = func.args[0] + y = func.args[1] + b = match[match['b']] + c = match[match['c']] + d = match[match['d']] + e = -match[match['e']] + expterm = exp(-S(d)/(b**2 + c**2)*xi) + functerm = solvefun(eta) + solvedict = solve((b*x + c*y - xi, c*x - b*y - eta), x, y) + # Integral should remain as it is in terms of xi, + # doit() should be done in _handle_Integral. + genterm = (1/S(b**2 + c**2))*Integral( + (1/expterm*e).subs(solvedict), (xi, b*x + c*y)) + return Eq(f(x,y), Subs(expterm*(functerm + genterm), + (eta, xi), (c*x - b*y, b*x + c*y))) + + +def pde_1st_linear_variable_coeff(eq, func, order, match, solvefun): + r""" + Solves a first order linear partial differential equation + with variable coefficients. The general form of this partial + differential equation is + + .. math:: a(x, y) \frac{\partial f(x, y)}{\partial x} + + b(x, y) \frac{\partial f(x, y)}{\partial y} + + c(x, y) f(x, y) = G(x, y) + + where `a(x, y)`, `b(x, y)`, `c(x, y)` and `G(x, y)` are arbitrary + functions in `x` and `y`. This PDE is converted into an ODE by + making the following transformation: + + 1. `\xi` as `x` + + 2. `\eta` as the constant in the solution to the differential + equation `\frac{dy}{dx} = -\frac{b}{a}` + + Making the previous substitutions reduces it to the linear ODE + + .. math:: a(\xi, \eta)\frac{du}{d\xi} + c(\xi, \eta)u - G(\xi, \eta) = 0 + + which can be solved using ``dsolve``. + + >>> from sympy.abc import x, y + >>> from sympy import Function, pprint + >>> a, b, c, G, f= [Function(i) for i in ['a', 'b', 'c', 'G', 'f']] + >>> u = f(x,y) + >>> ux = u.diff(x) + >>> uy = u.diff(y) + >>> genform = a(x, y)*u + b(x, y)*ux + c(x, y)*uy - G(x,y) + >>> pprint(genform) + d d + -G(x, y) + a(x, y)*f(x, y) + b(x, y)*--(f(x, y)) + c(x, y)*--(f(x, y)) + dx dy + + + Examples + ======== + + >>> from sympy.solvers.pde import pdsolve + >>> from sympy import Function, pprint + >>> from sympy.abc import x,y + >>> f = Function('f') + >>> eq = x*(u.diff(x)) - y*(u.diff(y)) + y**2*u - y**2 + >>> pdsolve(eq) + Eq(f(x, y), F(x*y)*exp(y**2/2) + 1) + + References + ========== + + - Viktor Grigoryan, "Partial Differential Equations" + Math 124A - Fall 2010, pp.7 + + """ + from sympy.solvers.ode import dsolve + + eta = symbols("eta") + f = func.func + x = func.args[0] + y = func.args[1] + b = match[match['b']] + c = match[match['c']] + d = match[match['d']] + e = -match[match['e']] + + + if not d: + # To deal with cases like b*ux = e or c*uy = e + if not (b and c): + if c: + try: + tsol = integrate(e/c, y) + except NotImplementedError: + raise NotImplementedError("Unable to find a solution" + " due to inability of integrate") + else: + return Eq(f(x,y), solvefun(x) + tsol) + if b: + try: + tsol = integrate(e/b, x) + except NotImplementedError: + raise NotImplementedError("Unable to find a solution" + " due to inability of integrate") + else: + return Eq(f(x,y), solvefun(y) + tsol) + + if not c: + # To deal with cases when c is 0, a simpler method is used. + # The PDE reduces to b*(u.diff(x)) + d*u = e, which is a linear ODE in x + plode = f(x).diff(x)*b + d*f(x) - e + sol = dsolve(plode, f(x)) + syms = sol.free_symbols - plode.free_symbols - {x, y} + rhs = _simplify_variable_coeff(sol.rhs, syms, solvefun, y) + return Eq(f(x, y), rhs) + + if not b: + # To deal with cases when b is 0, a simpler method is used. + # The PDE reduces to c*(u.diff(y)) + d*u = e, which is a linear ODE in y + plode = f(y).diff(y)*c + d*f(y) - e + sol = dsolve(plode, f(y)) + syms = sol.free_symbols - plode.free_symbols - {x, y} + rhs = _simplify_variable_coeff(sol.rhs, syms, solvefun, x) + return Eq(f(x, y), rhs) + + dummy = Function('d') + h = (c/b).subs(y, dummy(x)) + sol = dsolve(dummy(x).diff(x) - h, dummy(x)) + if isinstance(sol, list): + sol = sol[0] + solsym = sol.free_symbols - h.free_symbols - {x, y} + if len(solsym) == 1: + solsym = solsym.pop() + etat = (solve(sol, solsym)[0]).subs(dummy(x), y) + ysub = solve(eta - etat, y)[0] + deq = (b*(f(x).diff(x)) + d*f(x) - e).subs(y, ysub) + final = (dsolve(deq, f(x), hint='1st_linear')).rhs + if isinstance(final, list): + final = final[0] + finsyms = final.free_symbols - deq.free_symbols - {x, y} + rhs = _simplify_variable_coeff(final, finsyms, solvefun, etat) + return Eq(f(x, y), rhs) + + else: + raise NotImplementedError("Cannot solve the partial differential equation due" + " to inability of constantsimp") + + +def _simplify_variable_coeff(sol, syms, func, funcarg): + r""" + Helper function to replace constants by functions in 1st_linear_variable_coeff + """ + eta = Symbol("eta") + if len(syms) == 1: + sym = syms.pop() + final = sol.subs(sym, func(funcarg)) + + else: + for sym in syms: + final = sol.subs(sym, func(funcarg)) + + return simplify(final.subs(eta, funcarg)) + + +def pde_separate(eq, fun, sep, strategy='mul'): + """Separate variables in partial differential equation either by additive + or multiplicative separation approach. It tries to rewrite an equation so + that one of the specified variables occurs on a different side of the + equation than the others. + + :param eq: Partial differential equation + + :param fun: Original function F(x, y, z) + + :param sep: List of separated functions [X(x), u(y, z)] + + :param strategy: Separation strategy. You can choose between additive + separation ('add') and multiplicative separation ('mul') which is + default. + + Examples + ======== + + >>> from sympy import E, Eq, Function, pde_separate, Derivative as D + >>> from sympy.abc import x, t + >>> u, X, T = map(Function, 'uXT') + + >>> eq = Eq(D(u(x, t), x), E**(u(x, t))*D(u(x, t), t)) + >>> pde_separate(eq, u(x, t), [X(x), T(t)], strategy='add') + [exp(-X(x))*Derivative(X(x), x), exp(T(t))*Derivative(T(t), t)] + + >>> eq = Eq(D(u(x, t), x, 2), D(u(x, t), t, 2)) + >>> pde_separate(eq, u(x, t), [X(x), T(t)], strategy='mul') + [Derivative(X(x), (x, 2))/X(x), Derivative(T(t), (t, 2))/T(t)] + + See Also + ======== + pde_separate_add, pde_separate_mul + """ + + do_add = False + if strategy == 'add': + do_add = True + elif strategy == 'mul': + do_add = False + else: + raise ValueError('Unknown strategy: %s' % strategy) + + if isinstance(eq, Equality): + if eq.rhs != 0: + return pde_separate(Eq(eq.lhs - eq.rhs, 0), fun, sep, strategy) + else: + return pde_separate(Eq(eq, 0), fun, sep, strategy) + + if eq.rhs != 0: + raise ValueError("Value should be 0") + + # Handle arguments + orig_args = list(fun.args) + subs_args = [arg for s in sep for arg in s.args] + + if do_add: + functions = reduce(operator.add, sep) + else: + functions = reduce(operator.mul, sep) + + # Check whether variables match + if len(subs_args) != len(orig_args): + raise ValueError("Variable counts do not match") + # Check for duplicate arguments like [X(x), u(x, y)] + if has_dups(subs_args): + raise ValueError("Duplicate substitution arguments detected") + # Check whether the variables match + if set(orig_args) != set(subs_args): + raise ValueError("Arguments do not match") + + # Substitute original function with separated... + result = eq.lhs.subs(fun, functions).doit() + + # Divide by terms when doing multiplicative separation + if not do_add: + eq = 0 + for i in result.args: + eq += i/functions + result = eq + + svar = subs_args[0] + dvar = subs_args[1:] + return _separate(result, svar, dvar) + + +def pde_separate_add(eq, fun, sep): + """ + Helper function for searching additive separable solutions. + + Consider an equation of two independent variables x, y and a dependent + variable w, we look for the product of two functions depending on different + arguments: + + `w(x, y, z) = X(x) + y(y, z)` + + Examples + ======== + + >>> from sympy import E, Eq, Function, pde_separate_add, Derivative as D + >>> from sympy.abc import x, t + >>> u, X, T = map(Function, 'uXT') + + >>> eq = Eq(D(u(x, t), x), E**(u(x, t))*D(u(x, t), t)) + >>> pde_separate_add(eq, u(x, t), [X(x), T(t)]) + [exp(-X(x))*Derivative(X(x), x), exp(T(t))*Derivative(T(t), t)] + + """ + return pde_separate(eq, fun, sep, strategy='add') + + +def pde_separate_mul(eq, fun, sep): + """ + Helper function for searching multiplicative separable solutions. + + Consider an equation of two independent variables x, y and a dependent + variable w, we look for the product of two functions depending on different + arguments: + + `w(x, y, z) = X(x)*u(y, z)` + + Examples + ======== + + >>> from sympy import Function, Eq, pde_separate_mul, Derivative as D + >>> from sympy.abc import x, y + >>> u, X, Y = map(Function, 'uXY') + + >>> eq = Eq(D(u(x, y), x, 2), D(u(x, y), y, 2)) + >>> pde_separate_mul(eq, u(x, y), [X(x), Y(y)]) + [Derivative(X(x), (x, 2))/X(x), Derivative(Y(y), (y, 2))/Y(y)] + + """ + return pde_separate(eq, fun, sep, strategy='mul') + + +def _separate(eq, dep, others): + """Separate expression into two parts based on dependencies of variables.""" + + # FIRST PASS + # Extract derivatives depending our separable variable... + terms = set() + for term in eq.args: + if term.is_Mul: + for i in term.args: + if i.is_Derivative and not i.has(*others): + terms.add(term) + continue + elif term.is_Derivative and not term.has(*others): + terms.add(term) + # Find the factor that we need to divide by + div = set() + for term in terms: + ext, sep = term.expand().as_independent(dep) + # Failed? + if sep.has(*others): + return None + div.add(ext) + # FIXME: Find lcm() of all the divisors and divide with it, instead of + # current hack :( + # https://github.com/sympy/sympy/issues/4597 + if len(div) > 0: + # double sum required or some tests will fail + eq = Add(*[simplify(Add(*[term/i for i in div])) for term in eq.args]) + # SECOND PASS - separate the derivatives + div = set() + lhs = rhs = 0 + for term in eq.args: + # Check, whether we have already term with independent variable... + if not term.has(*others): + lhs += term + continue + # ...otherwise, try to separate + temp, sep = term.expand().as_independent(dep) + # Failed? + if sep.has(*others): + return None + # Extract the divisors + div.add(sep) + rhs -= term.expand() + # Do the division + fulldiv = reduce(operator.add, div) + lhs = simplify(lhs/fulldiv).expand() + rhs = simplify(rhs/fulldiv).expand() + # ...and check whether we were successful :) + if lhs.has(*others) or rhs.has(dep): + return None + return [lhs, rhs] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/polysys.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/polysys.py new file mode 100644 index 0000000000000000000000000000000000000000..2edc70b36c25b986c975c33fcc57535ef0b31df2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/polysys.py @@ -0,0 +1,872 @@ +"""Solvers of systems of polynomial equations. """ + +from __future__ import annotations + +from typing import Any +from collections.abc import Sequence, Iterable + +import itertools + +from sympy import Dummy +from sympy.core import S +from sympy.core.expr import Expr +from sympy.core.exprtools import factor_terms +from sympy.core.sorting import default_sort_key +from sympy.logic.boolalg import Boolean +from sympy.polys import Poly, groebner, roots +from sympy.polys.domains import ZZ +from sympy.polys.polyoptions import build_options +from sympy.polys.polytools import parallel_poly_from_expr, sqf_part +from sympy.polys.polyerrors import ( + ComputationFailed, + PolificationFailed, + CoercionFailed, + GeneratorsNeeded, + DomainError +) +from sympy.simplify import rcollect +from sympy.utilities import postfixes +from sympy.utilities.iterables import cartes +from sympy.utilities.misc import filldedent +from sympy.logic.boolalg import Or, And +from sympy.core.relational import Eq + + +class SolveFailed(Exception): + """Raised when solver's conditions were not met. """ + + +def solve_poly_system(seq, *gens, strict=False, **args): + """ + Return a list of solutions for the system of polynomial equations + or else None. + + Parameters + ========== + + seq: a list/tuple/set + Listing all the equations that are needed to be solved + gens: generators + generators of the equations in seq for which we want the + solutions + strict: a boolean (default is False) + if strict is True, NotImplementedError will be raised if + the solution is known to be incomplete (which can occur if + not all solutions are expressible in radicals) + args: Keyword arguments + Special options for solving the equations. + + + Returns + ======= + + List[Tuple] + a list of tuples with elements being solutions for the + symbols in the order they were passed as gens + None + None is returned when the computed basis contains only the ground. + + Examples + ======== + + >>> from sympy import solve_poly_system + >>> from sympy.abc import x, y + + >>> solve_poly_system([x*y - 2*y, 2*y**2 - x**2], x, y) + [(0, 0), (2, -sqrt(2)), (2, sqrt(2))] + + >>> solve_poly_system([x**5 - x + y**3, y**2 - 1], x, y, strict=True) + Traceback (most recent call last): + ... + UnsolvableFactorError + + """ + try: + polys, opt = parallel_poly_from_expr(seq, *gens, **args) + except PolificationFailed as exc: + raise ComputationFailed('solve_poly_system', len(seq), exc) + + if len(polys) == len(opt.gens) == 2: + f, g = polys + + if all(i <= 2 for i in f.degree_list() + g.degree_list()): + try: + return solve_biquadratic(f, g, opt) + except SolveFailed: + pass + + return solve_generic(polys, opt, strict=strict) + + +def solve_biquadratic(f, g, opt): + """Solve a system of two bivariate quadratic polynomial equations. + + Parameters + ========== + + f: a single Expr or Poly + First equation + g: a single Expr or Poly + Second Equation + opt: an Options object + For specifying keyword arguments and generators + + Returns + ======= + + List[Tuple] + a list of tuples with elements being solutions for the + symbols in the order they were passed as gens + None + None is returned when the computed basis contains only the ground. + + Examples + ======== + + >>> from sympy import Options, Poly + >>> from sympy.abc import x, y + >>> from sympy.solvers.polysys import solve_biquadratic + >>> NewOption = Options((x, y), {'domain': 'ZZ'}) + + >>> a = Poly(y**2 - 4 + x, y, x, domain='ZZ') + >>> b = Poly(y*2 + 3*x - 7, y, x, domain='ZZ') + >>> solve_biquadratic(a, b, NewOption) + [(1/3, 3), (41/27, 11/9)] + + >>> a = Poly(y + x**2 - 3, y, x, domain='ZZ') + >>> b = Poly(-y + x - 4, y, x, domain='ZZ') + >>> solve_biquadratic(a, b, NewOption) + [(7/2 - sqrt(29)/2, -sqrt(29)/2 - 1/2), (sqrt(29)/2 + 7/2, -1/2 + \ + sqrt(29)/2)] + """ + G = groebner([f, g]) + + if len(G) == 1 and G[0].is_ground: + return None + + if len(G) != 2: + raise SolveFailed + + x, y = opt.gens + p, q = G + if not p.gcd(q).is_ground: + # not 0-dimensional + raise SolveFailed + + p = Poly(p, x, expand=False) + p_roots = [rcollect(expr, y) for expr in roots(p).keys()] + + q = q.ltrim(-1) + q_roots = list(roots(q).keys()) + + solutions = [(p_root.subs(y, q_root), q_root) for q_root, p_root in + itertools.product(q_roots, p_roots)] + + return sorted(solutions, key=default_sort_key) + + +def solve_generic(polys, opt, strict=False): + """ + Solve a generic system of polynomial equations. + + Returns all possible solutions over C[x_1, x_2, ..., x_m] of a + set F = { f_1, f_2, ..., f_n } of polynomial equations, using + Groebner basis approach. For now only zero-dimensional systems + are supported, which means F can have at most a finite number + of solutions. If the basis contains only the ground, None is + returned. + + The algorithm works by the fact that, supposing G is the basis + of F with respect to an elimination order (here lexicographic + order is used), G and F generate the same ideal, they have the + same set of solutions. By the elimination property, if G is a + reduced, zero-dimensional Groebner basis, then there exists an + univariate polynomial in G (in its last variable). This can be + solved by computing its roots. Substituting all computed roots + for the last (eliminated) variable in other elements of G, new + polynomial system is generated. Applying the above procedure + recursively, a finite number of solutions can be found. + + The ability of finding all solutions by this procedure depends + on the root finding algorithms. If no solutions were found, it + means only that roots() failed, but the system is solvable. To + overcome this difficulty use numerical algorithms instead. + + Parameters + ========== + + polys: a list/tuple/set + Listing all the polynomial equations that are needed to be solved + opt: an Options object + For specifying keyword arguments and generators + strict: a boolean + If strict is True, NotImplementedError will be raised if the solution + is known to be incomplete + + Returns + ======= + + List[Tuple] + a list of tuples with elements being solutions for the + symbols in the order they were passed as gens + None + None is returned when the computed basis contains only the ground. + + References + ========== + + .. [Buchberger01] B. Buchberger, Groebner Bases: A Short + Introduction for Systems Theorists, In: R. Moreno-Diaz, + B. Buchberger, J.L. Freire, Proceedings of EUROCAST'01, + February, 2001 + + .. [Cox97] D. Cox, J. Little, D. O'Shea, Ideals, Varieties + and Algorithms, Springer, Second Edition, 1997, pp. 112 + + Raises + ======== + + NotImplementedError + If the system is not zero-dimensional (does not have a finite + number of solutions) + + UnsolvableFactorError + If ``strict`` is True and not all solution components are + expressible in radicals + + Examples + ======== + + >>> from sympy import Poly, Options + >>> from sympy.solvers.polysys import solve_generic + >>> from sympy.abc import x, y + >>> NewOption = Options((x, y), {'domain': 'ZZ'}) + + >>> a = Poly(x - y + 5, x, y, domain='ZZ') + >>> b = Poly(x + y - 3, x, y, domain='ZZ') + >>> solve_generic([a, b], NewOption) + [(-1, 4)] + + >>> a = Poly(x - 2*y + 5, x, y, domain='ZZ') + >>> b = Poly(2*x - y - 3, x, y, domain='ZZ') + >>> solve_generic([a, b], NewOption) + [(11/3, 13/3)] + + >>> a = Poly(x**2 + y, x, y, domain='ZZ') + >>> b = Poly(x + y*4, x, y, domain='ZZ') + >>> solve_generic([a, b], NewOption) + [(0, 0), (1/4, -1/16)] + + >>> a = Poly(x**5 - x + y**3, x, y, domain='ZZ') + >>> b = Poly(y**2 - 1, x, y, domain='ZZ') + >>> solve_generic([a, b], NewOption, strict=True) + Traceback (most recent call last): + ... + UnsolvableFactorError + + """ + def _is_univariate(f): + """Returns True if 'f' is univariate in its last variable. """ + for monom in f.monoms(): + if any(monom[:-1]): + return False + + return True + + def _subs_root(f, gen, zero): + """Replace generator with a root so that the result is nice. """ + p = f.as_expr({gen: zero}) + + if f.degree(gen) >= 2: + p = p.expand(deep=False) + + return p + + def _solve_reduced_system(system, gens, entry=False): + """Recursively solves reduced polynomial systems. """ + if len(system) == len(gens) == 1: + # the below line will produce UnsolvableFactorError if + # strict=True and the solution from `roots` is incomplete + zeros = list(roots(system[0], gens[-1], strict=strict).keys()) + return [(zero,) for zero in zeros] + + basis = groebner(system, gens, polys=True) + + if len(basis) == 1 and basis[0].is_ground: + if not entry: + return [] + else: + return None + + univariate = list(filter(_is_univariate, basis)) + + if len(basis) < len(gens): + raise NotImplementedError(filldedent(''' + only zero-dimensional systems supported + (finite number of solutions) + ''')) + + if len(univariate) == 1: + f = univariate.pop() + else: + raise NotImplementedError(filldedent(''' + only zero-dimensional systems supported + (finite number of solutions) + ''')) + + gens = f.gens + gen = gens[-1] + + # the below line will produce UnsolvableFactorError if + # strict=True and the solution from `roots` is incomplete + zeros = list(roots(f.ltrim(gen), strict=strict).keys()) + + if not zeros: + return [] + + if len(basis) == 1: + return [(zero,) for zero in zeros] + + solutions = [] + + for zero in zeros: + new_system = [] + new_gens = gens[:-1] + + for b in basis[:-1]: + eq = _subs_root(b, gen, zero) + + if eq is not S.Zero: + new_system.append(eq) + + for solution in _solve_reduced_system(new_system, new_gens): + solutions.append(solution + (zero,)) + + if solutions and len(solutions[0]) != len(gens): + raise NotImplementedError(filldedent(''' + only zero-dimensional systems supported + (finite number of solutions) + ''')) + return solutions + + try: + result = _solve_reduced_system(polys, opt.gens, entry=True) + except CoercionFailed: + raise NotImplementedError + + if result is not None: + return sorted(result, key=default_sort_key) + + +def solve_triangulated(polys, *gens, **args): + """ + Solve a polynomial system using Gianni-Kalkbrenner algorithm. + + The algorithm proceeds by computing one Groebner basis in the ground + domain and then by iteratively computing polynomial factorizations in + appropriately constructed algebraic extensions of the ground domain. + + Parameters + ========== + + polys: a list/tuple/set + Listing all the equations that are needed to be solved + gens: generators + generators of the equations in polys for which we want the + solutions + args: Keyword arguments + Special options for solving the equations + + Returns + ======= + + List[Tuple] + A List of tuples. Solutions for symbols that satisfy the + equations listed in polys + + Examples + ======== + + >>> from sympy import solve_triangulated + >>> from sympy.abc import x, y, z + + >>> F = [x**2 + y + z - 1, x + y**2 + z - 1, x + y + z**2 - 1] + + >>> solve_triangulated(F, x, y, z) + [(0, 0, 1), (0, 1, 0), (1, 0, 0)] + + Using extension for algebraic solutions. + + >>> solve_triangulated(F, x, y, z, extension=True) #doctest: +NORMALIZE_WHITESPACE + [(0, 0, 1), (0, 1, 0), (1, 0, 0), + (CRootOf(x**2 + 2*x - 1, 0), CRootOf(x**2 + 2*x - 1, 0), CRootOf(x**2 + 2*x - 1, 0)), + (CRootOf(x**2 + 2*x - 1, 1), CRootOf(x**2 + 2*x - 1, 1), CRootOf(x**2 + 2*x - 1, 1))] + + References + ========== + + 1. Patrizia Gianni, Teo Mora, Algebraic Solution of System of + Polynomial Equations using Groebner Bases, AAECC-5 on Applied Algebra, + Algebraic Algorithms and Error-Correcting Codes, LNCS 356 247--257, 1989 + + """ + opt = build_options(gens, args) + + G = groebner(polys, gens, polys=True) + G = list(reversed(G)) + + extension = opt.get('extension', False) + if extension: + def _solve_univariate(f): + return [r for r, _ in f.all_roots(multiple=False, radicals=False)] + else: + domain = opt.get('domain') + + if domain is not None: + for i, g in enumerate(G): + G[i] = g.set_domain(domain) + + def _solve_univariate(f): + return list(f.ground_roots().keys()) + + f, G = G[0].ltrim(-1), G[1:] + dom = f.get_domain() + + zeros = _solve_univariate(f) + + if extension: + solutions = {((zero,), dom.algebraic_field(zero)) for zero in zeros} + else: + solutions = {((zero,), dom) for zero in zeros} + + var_seq = reversed(gens[:-1]) + vars_seq = postfixes(gens[1:]) + + for var, vars in zip(var_seq, vars_seq): + _solutions = set() + + for values, dom in solutions: + H, mapping = [], list(zip(vars, values)) + + for g in G: + _vars = (var,) + vars + + if g.has_only_gens(*_vars) and g.degree(var) != 0: + if extension: + g = g.set_domain(g.domain.unify(dom)) + h = g.ltrim(var).eval(dict(mapping)) + + if g.degree(var) == h.degree(): + H.append(h) + + p = min(H, key=lambda h: h.degree()) + zeros = _solve_univariate(p) + + for zero in zeros: + if not (zero in dom): + dom_zero = dom.algebraic_field(zero) + else: + dom_zero = dom + + _solutions.add(((zero,) + values, dom_zero)) + + solutions = _solutions + return sorted((s for s, _ in solutions), key=default_sort_key) + + +def factor_system(eqs: Sequence[Expr | complex], gens: Sequence[Expr] = (), **kwargs: Any) -> list[list[Expr]]: + """ + Factorizes a system of polynomial equations into + irreducible subsystems. + + Parameters + ========== + + eqs : list + List of expressions to be factored. + Each expression is assumed to be equal to zero. + + gens : list, optional + Generator(s) of the polynomial ring. + If not provided, all free symbols will be used. + + **kwargs : dict, optional + Same optional arguments taken by ``factor`` + + Returns + ======= + + list[list[Expr]] + A list of lists of expressions, where each sublist represents + an irreducible subsystem. When solved, each subsystem gives + one component of the solution. Only generic solutions are + returned (cases not requiring parameters to be zero). + + Examples + ======== + + >>> from sympy.solvers.polysys import factor_system, factor_system_cond + >>> from sympy.abc import x, y, a, b, c + + A simple system with multiple solutions: + + >>> factor_system([x**2 - 1, y - 1]) + [[x + 1, y - 1], [x - 1, y - 1]] + + A system with no solution: + + >>> factor_system([x, 1]) + [] + + A system where any value of the symbol(s) is a solution: + + >>> factor_system([x - x, (x + 1)**2 - (x**2 + 2*x + 1)]) + [[]] + + A system with no generic solution: + + >>> factor_system([a*x*(x-1), b*y, c], [x, y]) + [] + + If c is added to the unknowns then the system has a generic solution: + + >>> factor_system([a*x*(x-1), b*y, c], [x, y, c]) + [[x - 1, y, c], [x, y, c]] + + Alternatively :func:`factor_system_cond` can be used to get degenerate + cases as well: + + >>> factor_system_cond([a*x*(x-1), b*y, c], [x, y]) + [[x - 1, y, c], [x, y, c], [x - 1, b, c], [x, b, c], [y, a, c], [a, b, c]] + + Each of the above cases is only satisfiable in the degenerate case `c = 0`. + + The solution set of the original system represented + by eqs is the union of the solution sets of the + factorized systems. + + An empty list [] means no generic solution exists. + A list containing an empty list [[]] means any value of + the symbol(s) is a solution. + + See Also + ======== + + factor_system_cond : Returns both generic and degenerate solutions + factor_system_bool : Returns a Boolean combination representing all solutions + sympy.polys.polytools.factor : Factors a polynomial into irreducible factors + over the rational numbers + """ + + systems = _factor_system_poly_from_expr(eqs, gens, **kwargs) + systems_generic = [sys for sys in systems if not _is_degenerate(sys)] + systems_expr = [[p.as_expr() for p in system] for system in systems_generic] + return systems_expr + + +def _is_degenerate(system: list[Poly]) -> bool: + """Helper function to check if a system is degenerate""" + return any(p.is_ground for p in system) + + +def factor_system_bool(eqs: Sequence[Expr | complex], gens: Sequence[Expr] = (), **kwargs: Any) -> Boolean: + """ + Factorizes a system of polynomial equations into irreducible DNF. + + The system of expressions(eqs) is taken and a Boolean combination + of equations is returned that represents the same solution set. + The result is in disjunctive normal form (OR of ANDs). + + Parameters + ========== + + eqs : list + List of expressions to be factored. + Each expression is assumed to be equal to zero. + + gens : list, optional + Generator(s) of the polynomial ring. + If not provided, all free symbols will be used. + + **kwargs : dict, optional + Optional keyword arguments + + + Returns + ======= + + Boolean: + A Boolean combination of equations. The result is typically in + the form of a conjunction (AND) of a disjunctive normal form + with additional conditions. + + Examples + ======== + + >>> from sympy.solvers.polysys import factor_system_bool + >>> from sympy.abc import x, y, a, b, c + >>> factor_system_bool([x**2 - 1]) + Eq(x - 1, 0) | Eq(x + 1, 0) + + >>> factor_system_bool([x**2 - 1, y - 1]) + (Eq(x - 1, 0) & Eq(y - 1, 0)) | (Eq(x + 1, 0) & Eq(y - 1, 0)) + + >>> eqs = [a * (x - 1), b] + >>> factor_system_bool([a*(x - 1), b]) + (Eq(a, 0) & Eq(b, 0)) | (Eq(b, 0) & Eq(x - 1, 0)) + + >>> factor_system_bool([a*x**2 - a, b*(x + 1), c], [x]) + (Eq(c, 0) & Eq(x + 1, 0)) | (Eq(a, 0) & Eq(b, 0) & Eq(c, 0)) | (Eq(b, 0) & Eq(c, 0) & Eq(x - 1, 0)) + + >>> factor_system_bool([x**2 + 2*x + 1 - (x + 1)**2]) + True + + The result is logically equivalent to the system of equations + i.e. eqs. The function returns ``True`` when all values of + the symbol(s) is a solution and ``False`` when the system + cannot be solved. + + See Also + ======== + + factor_system : Returns factors and solvability condition separately + factor_system_cond : Returns both factors and conditions + + """ + + systems = factor_system_cond(eqs, gens, **kwargs) + return Or(*[And(*[Eq(eq, 0) for eq in sys]) for sys in systems]) + + +def factor_system_cond(eqs: Sequence[Expr | complex], gens: Sequence[Expr] = (), **kwargs: Any) -> list[list[Expr]]: + """ + Factorizes a polynomial system into irreducible components and returns + both generic and degenerate solutions. + + Parameters + ========== + + eqs : list + List of expressions to be factored. + Each expression is assumed to be equal to zero. + + gens : list, optional + Generator(s) of the polynomial ring. + If not provided, all free symbols will be used. + + **kwargs : dict, optional + Optional keyword arguments. + + Returns + ======= + + list[list[Expr]] + A list of lists of expressions, where each sublist represents + an irreducible subsystem. Includes both generic solutions and + degenerate cases requiring equality conditions on parameters. + + Examples + ======== + + >>> from sympy.solvers.polysys import factor_system_cond + >>> from sympy.abc import x, y, a, b, c + + >>> factor_system_cond([x**2 - 4, a*y, b], [x, y]) + [[x + 2, y, b], [x - 2, y, b], [x + 2, a, b], [x - 2, a, b]] + + >>> factor_system_cond([a*x*(x-1), b*y, c], [x, y]) + [[x - 1, y, c], [x, y, c], [x - 1, b, c], [x, b, c], [y, a, c], [a, b, c]] + + An empty list [] means no solution exists. + A list containing an empty list [[]] means any value of + the symbol(s) is a solution. + + See Also + ======== + + factor_system : Returns only generic solutions + factor_system_bool : Returns a Boolean combination representing all solutions + sympy.polys.polytools.factor : Factors a polynomial into irreducible factors + over the rational numbers + """ + systems_poly = _factor_system_poly_from_expr(eqs, gens, **kwargs) + systems = [[p.as_expr() for p in system] for system in systems_poly] + return systems + + +def _factor_system_poly_from_expr( + eqs: Sequence[Expr | complex], gens: Sequence[Expr], **kwargs: Any +) -> list[list[Poly]]: + """ + Convert expressions to polynomials and factor the system. + + Takes a sequence of expressions, converts them to + polynomials, and factors the resulting system. Handles both regular + polynomial systems and purely numerical cases. + """ + try: + polys, opts = parallel_poly_from_expr(eqs, *gens, **kwargs) + only_numbers = False + except (GeneratorsNeeded, PolificationFailed): + _u = Dummy('u') + polys, opts = parallel_poly_from_expr(eqs, [_u], **kwargs) + assert opts['domain'].is_Numerical + only_numbers = True + + if only_numbers: + return [[]] if all(p == 0 for p in polys) else [] + + return factor_system_poly(polys) + + +def factor_system_poly(polys: list[Poly]) -> list[list[Poly]]: + """ + Factors a system of polynomial equations into irreducible subsystems + + Core implementation that works directly with Poly instances. + + Parameters + ========== + + polys : list[Poly] + A list of Poly instances to be factored. + + Returns + ======= + + list[list[Poly]] + A list of lists of polynomials, where each sublist represents + an irreducible component of the solution. Includes both + generic and degenerate cases. + + Examples + ======== + + >>> from sympy import symbols, Poly, ZZ + >>> from sympy.solvers.polysys import factor_system_poly + >>> a, b, c, x = symbols('a b c x') + >>> p1 = Poly((a - 1)*(x - 2), x, domain=ZZ[a,b,c]) + >>> p2 = Poly((b - 3)*(x - 2), x, domain=ZZ[a,b,c]) + >>> p3 = Poly(c, x, domain=ZZ[a,b,c]) + + The equation to be solved for x is ``x - 2 = 0`` provided either + of the two conditions on the parameters ``a`` and ``b`` is nonzero + and the constant parameter ``c`` should be zero. + + >>> sys1, sys2 = factor_system_poly([p1, p2, p3]) + >>> sys1 + [Poly(x - 2, x, domain='ZZ[a,b,c]'), + Poly(c, x, domain='ZZ[a,b,c]')] + >>> sys2 + [Poly(a - 1, x, domain='ZZ[a,b,c]'), + Poly(b - 3, x, domain='ZZ[a,b,c]'), + Poly(c, x, domain='ZZ[a,b,c]')] + + An empty list [] when returned means no solution exists. + Whereas a list containing an empty list [[]] means any value is a solution. + + See Also + ======== + + factor_system : Returns only generic solutions + factor_system_bool : Returns a Boolean combination representing the solutions + factor_system_cond : Returns both generic and degenerate solutions + sympy.polys.polytools.factor : Factors a polynomial into irreducible factors + over the rational numbers + """ + if not all(isinstance(poly, Poly) for poly in polys): + raise TypeError("polys should be a list of Poly instances") + if not polys: + return [[]] + + base_domain = polys[0].domain + base_gens = polys[0].gens + if not all(poly.domain == base_domain and poly.gens == base_gens for poly in polys[1:]): + raise DomainError("All polynomials must have the same domain and generators") + + factor_sets = [] + for poly in polys: + constant, factors_mult = poly.factor_list() + + if constant.is_zero is True: + continue + elif constant.is_zero is False: + if not factors_mult: + return [] + factor_sets.append([f for f, _ in factors_mult]) + else: + constant = sqf_part(factor_terms(constant).as_coeff_Mul()[1]) + constp = Poly(constant, base_gens, domain=base_domain) + factors = [f for f, _ in factors_mult] + factors.append(constp) + factor_sets.append(factors) + + if not factor_sets: + return [[]] + + result = _factor_sets(factor_sets) + return _sort_systems(result) + + +def _factor_sets_slow(eqs: list[list]) -> set[frozenset]: + """ + Helper to find the minimal set of factorised subsystems that is + equivalent to the original system. + + The result is in DNF. + """ + if not eqs: + return {frozenset()} + systems_set = {frozenset(sys) for sys in cartes(*eqs)} + return {s1 for s1 in systems_set if not any(s1 > s2 for s2 in systems_set)} + + +def _factor_sets(eqs: list[list]) -> set[frozenset]: + """ + Helper that builds factor combinations. + """ + if not eqs: + return {frozenset()} + + current_set = min(eqs, key=len) + other_sets = [s for s in eqs if s is not current_set] + + stack = [(factor, [s for s in other_sets if factor not in s], {factor}) + for factor in current_set] + + result = set() + + while stack: + factor, remaining_sets, current_solution = stack.pop() + + if not remaining_sets: + result.add(frozenset(current_solution)) + continue + + next_set = min(remaining_sets, key=len) + next_remaining = [s for s in remaining_sets if s is not next_set] + + for next_factor in next_set: + valid_remaining = [s for s in next_remaining if next_factor not in s] + new_solution = current_solution | {next_factor} + stack.append((next_factor, valid_remaining, new_solution)) + + return {s1 for s1 in result if not any(s1 > s2 for s2 in result)} + + +def _sort_systems(systems: Iterable[Iterable[Poly]]) -> list[list[Poly]]: + """Sorts a list of lists of polynomials""" + systems_list = [sorted(s, key=_poly_sort_key, reverse=True) for s in systems] + return sorted(systems_list, key=_sys_sort_key, reverse=True) + + +def _poly_sort_key(poly): + """Sort key for polynomials""" + if poly.domain.is_FF: + poly = poly.set_domain(ZZ) + return poly.degree_list(), poly.rep.to_list() + + +def _sys_sort_key(sys): + """Sort key for lists of polynomials""" + return list(zip(*map(_poly_sort_key, sys))) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/recurr.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/recurr.py new file mode 100644 index 0000000000000000000000000000000000000000..ba627bbd4cb0844f11a8743634f5f10328aadca8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/recurr.py @@ -0,0 +1,843 @@ +r""" +This module is intended for solving recurrences or, in other words, +difference equations. Currently supported are linear, inhomogeneous +equations with polynomial or rational coefficients. + +The solutions are obtained among polynomials, rational functions, +hypergeometric terms, or combinations of hypergeometric term which +are pairwise dissimilar. + +``rsolve_X`` functions were meant as a low level interface +for ``rsolve`` which would use Mathematica's syntax. + +Given a recurrence relation: + + .. math:: a_{k}(n) y(n+k) + a_{k-1}(n) y(n+k-1) + + ... + a_{0}(n) y(n) = f(n) + +where `k > 0` and `a_{i}(n)` are polynomials in `n`. To use +``rsolve_X`` we need to put all coefficients in to a list ``L`` of +`k+1` elements the following way: + + ``L = [a_{0}(n), ..., a_{k-1}(n), a_{k}(n)]`` + +where ``L[i]``, for `i=0, \ldots, k`, maps to +`a_{i}(n) y(n+i)` (`y(n+i)` is implicit). + +For example if we would like to compute `m`-th Bernoulli polynomial +up to a constant (example was taken from rsolve_poly docstring), +then we would use `b(n+1) - b(n) = m n^{m-1}` recurrence, which +has solution `b(n) = B_m + C`. + +Then ``L = [-1, 1]`` and `f(n) = m n^(m-1)` and finally for `m=4`: + +>>> from sympy import Symbol, bernoulli, rsolve_poly +>>> n = Symbol('n', integer=True) + +>>> rsolve_poly([-1, 1], 4*n**3, n) +C0 + n**4 - 2*n**3 + n**2 + +>>> bernoulli(4, n) +n**4 - 2*n**3 + n**2 - 1/30 + +For the sake of completeness, `f(n)` can be: + + [1] a polynomial -> rsolve_poly + [2] a rational function -> rsolve_ratio + [3] a hypergeometric function -> rsolve_hyper +""" +from collections import defaultdict + +from sympy.concrete import product +from sympy.core.singleton import S +from sympy.core.numbers import Rational, I +from sympy.core.symbol import Symbol, Wild, Dummy +from sympy.core.relational import Equality +from sympy.core.add import Add +from sympy.core.mul import Mul +from sympy.core.sorting import default_sort_key +from sympy.core.sympify import sympify + +from sympy.simplify import simplify, hypersimp, hypersimilar # type: ignore +from sympy.solvers import solve, solve_undetermined_coeffs +from sympy.polys import Poly, quo, gcd, lcm, roots, resultant +from sympy.functions import binomial, factorial, FallingFactorial, RisingFactorial +from sympy.matrices import Matrix, casoratian +from sympy.utilities.iterables import numbered_symbols + + +def rsolve_poly(coeffs, f, n, shift=0, **hints): + r""" + Given linear recurrence operator `\operatorname{L}` of order + `k` with polynomial coefficients and inhomogeneous equation + `\operatorname{L} y = f`, where `f` is a polynomial, we seek for + all polynomial solutions over field `K` of characteristic zero. + + The algorithm performs two basic steps: + + (1) Compute degree `N` of the general polynomial solution. + (2) Find all polynomials of degree `N` or less + of `\operatorname{L} y = f`. + + There are two methods for computing the polynomial solutions. + If the degree bound is relatively small, i.e. it's smaller than + or equal to the order of the recurrence, then naive method of + undetermined coefficients is being used. This gives a system + of algebraic equations with `N+1` unknowns. + + In the other case, the algorithm performs transformation of the + initial equation to an equivalent one for which the system of + algebraic equations has only `r` indeterminates. This method is + quite sophisticated (in comparison with the naive one) and was + invented together by Abramov, Bronstein and Petkovsek. + + It is possible to generalize the algorithm implemented here to + the case of linear q-difference and differential equations. + + Lets say that we would like to compute `m`-th Bernoulli polynomial + up to a constant. For this we can use `b(n+1) - b(n) = m n^{m-1}` + recurrence, which has solution `b(n) = B_m + C`. For example: + + >>> from sympy import Symbol, rsolve_poly + >>> n = Symbol('n', integer=True) + + >>> rsolve_poly([-1, 1], 4*n**3, n) + C0 + n**4 - 2*n**3 + n**2 + + References + ========== + + .. [1] S. A. Abramov, M. Bronstein and M. Petkovsek, On polynomial + solutions of linear operator equations, in: T. Levelt, ed., + Proc. ISSAC '95, ACM Press, New York, 1995, 290-296. + + .. [2] M. Petkovsek, Hypergeometric solutions of linear recurrences + with polynomial coefficients, J. Symbolic Computation, + 14 (1992), 243-264. + + .. [3] M. Petkovsek, H. S. Wilf, D. Zeilberger, A = B, 1996. + + """ + f = sympify(f) + + if not f.is_polynomial(n): + return None + + homogeneous = f.is_zero + + r = len(coeffs) - 1 + + coeffs = [Poly(coeff, n) for coeff in coeffs] + + polys = [Poly(0, n)]*(r + 1) + terms = [(S.Zero, S.NegativeInfinity)]*(r + 1) + + for i in range(r + 1): + for j in range(i, r + 1): + polys[i] += coeffs[j]*(binomial(j, i).as_poly(n)) + + if not polys[i].is_zero: + (exp,), coeff = polys[i].LT() + terms[i] = (coeff, exp) + + d = b = terms[0][1] + + for i in range(1, r + 1): + if terms[i][1] > d: + d = terms[i][1] + + if terms[i][1] - i > b: + b = terms[i][1] - i + + d, b = int(d), int(b) + + x = Dummy('x') + + degree_poly = S.Zero + + for i in range(r + 1): + if terms[i][1] - i == b: + degree_poly += terms[i][0]*FallingFactorial(x, i) + + nni_roots = list(roots(degree_poly, x, filter='Z', + predicate=lambda r: r >= 0).keys()) + + if nni_roots: + N = [max(nni_roots)] + else: + N = [] + + if homogeneous: + N += [-b - 1] + else: + N += [f.as_poly(n).degree() - b, -b - 1] + + N = int(max(N)) + + if N < 0: + if homogeneous: + if hints.get('symbols', False): + return (S.Zero, []) + else: + return S.Zero + else: + return None + + if N <= r: + C = [] + y = E = S.Zero + + for i in range(N + 1): + C.append(Symbol('C' + str(i + shift))) + y += C[i] * n**i + + for i in range(r + 1): + E += coeffs[i].as_expr()*y.subs(n, n + i) + + solutions = solve_undetermined_coeffs(E - f, C, n) + + if solutions is not None: + _C = C + C = [c for c in C if (c not in solutions)] + result = y.subs(solutions) + else: + return None # TBD + else: + A = r + U = N + A + b + 1 + + nni_roots = list(roots(polys[r], filter='Z', + predicate=lambda r: r >= 0).keys()) + + if nni_roots != []: + a = max(nni_roots) + 1 + else: + a = S.Zero + + def _zero_vector(k): + return [S.Zero] * k + + def _one_vector(k): + return [S.One] * k + + def _delta(p, k): + B = S.One + D = p.subs(n, a + k) + + for i in range(1, k + 1): + B *= Rational(i - k - 1, i) + D += B * p.subs(n, a + k - i) + + return D + + alpha = {} + + for i in range(-A, d + 1): + I = _one_vector(d + 1) + + for k in range(1, d + 1): + I[k] = I[k - 1] * (x + i - k + 1)/k + + alpha[i] = S.Zero + + for j in range(A + 1): + for k in range(d + 1): + B = binomial(k, i + j) + D = _delta(polys[j].as_expr(), k) + + alpha[i] += I[k]*B*D + + V = Matrix(U, A, lambda i, j: int(i == j)) + + if homogeneous: + for i in range(A, U): + v = _zero_vector(A) + + for k in range(1, A + b + 1): + if i - k < 0: + break + + B = alpha[k - A].subs(x, i - k) + + for j in range(A): + v[j] += B * V[i - k, j] + + denom = alpha[-A].subs(x, i) + + for j in range(A): + V[i, j] = -v[j] / denom + else: + G = _zero_vector(U) + + for i in range(A, U): + v = _zero_vector(A) + g = S.Zero + + for k in range(1, A + b + 1): + if i - k < 0: + break + + B = alpha[k - A].subs(x, i - k) + + for j in range(A): + v[j] += B * V[i - k, j] + + g += B * G[i - k] + + denom = alpha[-A].subs(x, i) + + for j in range(A): + V[i, j] = -v[j] / denom + + G[i] = (_delta(f, i - A) - g) / denom + + P, Q = _one_vector(U), _zero_vector(A) + + for i in range(1, U): + P[i] = (P[i - 1] * (n - a - i + 1)/i).expand() + + for i in range(A): + Q[i] = Add(*[(v*p).expand() for v, p in zip(V[:, i], P)]) + + if not homogeneous: + h = Add(*[(g*p).expand() for g, p in zip(G, P)]) + + C = [Symbol('C' + str(i + shift)) for i in range(A)] + + g = lambda i: Add(*[c*_delta(q, i) for c, q in zip(C, Q)]) + + if homogeneous: + E = [g(i) for i in range(N + 1, U)] + else: + E = [g(i) + _delta(h, i) for i in range(N + 1, U)] + + if E != []: + solutions = solve(E, *C) + + if not solutions: + if homogeneous: + if hints.get('symbols', False): + return (S.Zero, []) + else: + return S.Zero + else: + return None + else: + solutions = {} + + if homogeneous: + result = S.Zero + else: + result = h + + _C = C[:] + for c, q in list(zip(C, Q)): + if c in solutions: + s = solutions[c]*q + C.remove(c) + else: + s = c*q + + result += s.expand() + + if C != _C: + # renumber so they are contiguous + result = result.xreplace(dict(zip(C, _C))) + C = _C[:len(C)] + + if hints.get('symbols', False): + return (result, C) + else: + return result + + +def rsolve_ratio(coeffs, f, n, **hints): + r""" + Given linear recurrence operator `\operatorname{L}` of order `k` + with polynomial coefficients and inhomogeneous equation + `\operatorname{L} y = f`, where `f` is a polynomial, we seek + for all rational solutions over field `K` of characteristic zero. + + This procedure accepts only polynomials, however if you are + interested in solving recurrence with rational coefficients + then use ``rsolve`` which will pre-process the given equation + and run this procedure with polynomial arguments. + + The algorithm performs two basic steps: + + (1) Compute polynomial `v(n)` which can be used as universal + denominator of any rational solution of equation + `\operatorname{L} y = f`. + + (2) Construct new linear difference equation by substitution + `y(n) = u(n)/v(n)` and solve it for `u(n)` finding all its + polynomial solutions. Return ``None`` if none were found. + + The algorithm implemented here is a revised version of the original + Abramov's algorithm, developed in 1989. The new approach is much + simpler to implement and has better overall efficiency. This + method can be easily adapted to the q-difference equations case. + + Besides finding rational solutions alone, this functions is + an important part of Hyper algorithm where it is used to find + a particular solution for the inhomogeneous part of a recurrence. + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy.solvers.recurr import rsolve_ratio + >>> rsolve_ratio([-2*x**3 + x**2 + 2*x - 1, 2*x**3 + x**2 - 6*x, + ... - 2*x**3 - 11*x**2 - 18*x - 9, 2*x**3 + 13*x**2 + 22*x + 8], 0, x) + C0*(2*x - 3)/(2*(x**2 - 1)) + + References + ========== + + .. [1] S. A. Abramov, Rational solutions of linear difference + and q-difference equations with polynomial coefficients, + in: T. Levelt, ed., Proc. ISSAC '95, ACM Press, New York, + 1995, 285-289 + + See Also + ======== + + rsolve_hyper + """ + f = sympify(f) + + if not f.is_polynomial(n): + return None + + coeffs = list(map(sympify, coeffs)) + + r = len(coeffs) - 1 + + A, B = coeffs[r], coeffs[0] + A = A.subs(n, n - r).expand() + + h = Dummy('h') + + res = resultant(A, B.subs(n, n + h), n) + + if not res.is_polynomial(h): + p, q = res.as_numer_denom() + res = quo(p, q, h) + + nni_roots = list(roots(res, h, filter='Z', + predicate=lambda r: r >= 0).keys()) + + if not nni_roots: + return rsolve_poly(coeffs, f, n, **hints) + else: + C, numers = S.One, [S.Zero]*(r + 1) + + for i in range(int(max(nni_roots)), -1, -1): + d = gcd(A, B.subs(n, n + i), n) + + A = quo(A, d, n) + B = quo(B, d.subs(n, n - i), n) + + C *= Mul(*[d.subs(n, n - j) for j in range(i + 1)]) + + denoms = [C.subs(n, n + i) for i in range(r + 1)] + + for i in range(r + 1): + g = gcd(coeffs[i], denoms[i], n) + + numers[i] = quo(coeffs[i], g, n) + denoms[i] = quo(denoms[i], g, n) + + for i in range(r + 1): + numers[i] *= Mul(*(denoms[:i] + denoms[i + 1:])) + + result = rsolve_poly(numers, f * Mul(*denoms), n, **hints) + + if result is not None: + if hints.get('symbols', False): + return (simplify(result[0] / C), result[1]) + else: + return simplify(result / C) + else: + return None + + +def rsolve_hyper(coeffs, f, n, **hints): + r""" + Given linear recurrence operator `\operatorname{L}` of order `k` + with polynomial coefficients and inhomogeneous equation + `\operatorname{L} y = f` we seek for all hypergeometric solutions + over field `K` of characteristic zero. + + The inhomogeneous part can be either hypergeometric or a sum + of a fixed number of pairwise dissimilar hypergeometric terms. + + The algorithm performs three basic steps: + + (1) Group together similar hypergeometric terms in the + inhomogeneous part of `\operatorname{L} y = f`, and find + particular solution using Abramov's algorithm. + + (2) Compute generating set of `\operatorname{L}` and find basis + in it, so that all solutions are linearly independent. + + (3) Form final solution with the number of arbitrary + constants equal to dimension of basis of `\operatorname{L}`. + + Term `a(n)` is hypergeometric if it is annihilated by first order + linear difference equations with polynomial coefficients or, in + simpler words, if consecutive term ratio is a rational function. + + The output of this procedure is a linear combination of fixed + number of hypergeometric terms. However the underlying method + can generate larger class of solutions - D'Alembertian terms. + + Note also that this method not only computes the kernel of the + inhomogeneous equation, but also reduces in to a basis so that + solutions generated by this procedure are linearly independent + + Examples + ======== + + >>> from sympy.solvers import rsolve_hyper + >>> from sympy.abc import x + + >>> rsolve_hyper([-1, -1, 1], 0, x) + C0*(1/2 - sqrt(5)/2)**x + C1*(1/2 + sqrt(5)/2)**x + + >>> rsolve_hyper([-1, 1], 1 + x, x) + C0 + x*(x + 1)/2 + + References + ========== + + .. [1] M. Petkovsek, Hypergeometric solutions of linear recurrences + with polynomial coefficients, J. Symbolic Computation, + 14 (1992), 243-264. + + .. [2] M. Petkovsek, H. S. Wilf, D. Zeilberger, A = B, 1996. + """ + coeffs = list(map(sympify, coeffs)) + + f = sympify(f) + + r, kernel, symbols = len(coeffs) - 1, [], set() + + if not f.is_zero: + if f.is_Add: + similar = {} + + for g in f.expand().args: + if not g.is_hypergeometric(n): + return None + + for h in similar.keys(): + if hypersimilar(g, h, n): + similar[h] += g + break + else: + similar[g] = S.Zero + + inhomogeneous = [g + h for g, h in similar.items()] + elif f.is_hypergeometric(n): + inhomogeneous = [f] + else: + return None + + for i, g in enumerate(inhomogeneous): + coeff, polys = S.One, coeffs[:] + denoms = [S.One]*(r + 1) + + s = hypersimp(g, n) + + for j in range(1, r + 1): + coeff *= s.subs(n, n + j - 1) + + p, q = coeff.as_numer_denom() + + polys[j] *= p + denoms[j] = q + + for j in range(r + 1): + polys[j] *= Mul(*(denoms[:j] + denoms[j + 1:])) + + # FIXME: The call to rsolve_ratio below should suffice (rsolve_poly + # call can be removed) but the XFAIL test_rsolve_ratio_missed must + # be fixed first. + R = rsolve_ratio(polys, Mul(*denoms), n, symbols=True) + if R is not None: + R, syms = R + if syms: + R = R.subs(zip(syms, [0]*len(syms))) + else: + R = rsolve_poly(polys, Mul(*denoms), n) + + if R: + inhomogeneous[i] *= R + else: + return None + + result = Add(*inhomogeneous) + result = simplify(result) + else: + result = S.Zero + + Z = Dummy('Z') + + p, q = coeffs[0], coeffs[r].subs(n, n - r + 1) + + p_factors = list(roots(p, n).keys()) + q_factors = list(roots(q, n).keys()) + + factors = [(S.One, S.One)] + + for p in p_factors: + for q in q_factors: + if p.is_integer and q.is_integer and p <= q: + continue + else: + factors += [(n - p, n - q)] + + p = [(n - p, S.One) for p in p_factors] + q = [(S.One, n - q) for q in q_factors] + + factors = p + factors + q + + for A, B in factors: + polys, degrees = [], [] + D = A*B.subs(n, n + r - 1) + + for i in range(r + 1): + a = Mul(*[A.subs(n, n + j) for j in range(i)]) + b = Mul(*[B.subs(n, n + j) for j in range(i, r)]) + + poly = quo(coeffs[i]*a*b, D, n) + polys.append(poly.as_poly(n)) + + if not poly.is_zero: + degrees.append(polys[i].degree()) + + if degrees: + d, poly = max(degrees), S.Zero + else: + return None + + for i in range(r + 1): + coeff = polys[i].nth(d) + + if coeff is not S.Zero: + poly += coeff * Z**i + + for z in roots(poly, Z).keys(): + if z.is_zero: + continue + + recurr_coeffs = [polys[i].as_expr()*z**i for i in range(r + 1)] + if d == 0 and 0 != Add(*[recurr_coeffs[j]*j for j in range(1, r + 1)]): + # faster inline check (than calling rsolve_poly) for a + # constant solution to a constant coefficient recurrence. + sol = [Symbol("C" + str(len(symbols)))] + else: + sol, syms = rsolve_poly(recurr_coeffs, 0, n, len(symbols), symbols=True) + sol = sol.collect(syms) + sol = [sol.coeff(s) for s in syms] + + for C in sol: + ratio = z * A * C.subs(n, n + 1) / B / C + ratio = simplify(ratio) + # If there is a nonnegative root in the denominator of the ratio, + # this indicates that the term y(n_root) is zero, and one should + # start the product with the term y(n_root + 1). + n0 = 0 + for n_root in roots(ratio.as_numer_denom()[1], n).keys(): + if n_root.has(I): + return None + elif (n0 < (n_root + 1)) == True: + n0 = n_root + 1 + K = product(ratio, (n, n0, n - 1)) + if K.has(factorial, FallingFactorial, RisingFactorial): + K = simplify(K) + + if casoratian(kernel + [K], n, zero=False) != 0: + kernel.append(K) + + kernel.sort(key=default_sort_key) + sk = list(zip(numbered_symbols('C'), kernel)) + + for C, ker in sk: + result += C * ker + + if hints.get('symbols', False): + # XXX: This returns the symbols in a non-deterministic order + symbols |= {s for s, k in sk} + return (result, list(symbols)) + else: + return result + + +def rsolve(f, y, init=None): + r""" + Solve univariate recurrence with rational coefficients. + + Given `k`-th order linear recurrence `\operatorname{L} y = f`, + or equivalently: + + .. math:: a_{k}(n) y(n+k) + a_{k-1}(n) y(n+k-1) + + \cdots + a_{0}(n) y(n) = f(n) + + where `a_{i}(n)`, for `i=0, \ldots, k`, are polynomials or rational + functions in `n`, and `f` is a hypergeometric function or a sum + of a fixed number of pairwise dissimilar hypergeometric terms in + `n`, finds all solutions or returns ``None``, if none were found. + + Initial conditions can be given as a dictionary in two forms: + + (1) ``{ n_0 : v_0, n_1 : v_1, ..., n_m : v_m}`` + (2) ``{y(n_0) : v_0, y(n_1) : v_1, ..., y(n_m) : v_m}`` + + or as a list ``L`` of values: + + ``L = [v_0, v_1, ..., v_m]`` + + where ``L[i] = v_i``, for `i=0, \ldots, m`, maps to `y(n_i)`. + + Examples + ======== + + Lets consider the following recurrence: + + .. math:: (n - 1) y(n + 2) - (n^2 + 3 n - 2) y(n + 1) + + 2 n (n + 1) y(n) = 0 + + >>> from sympy import Function, rsolve + >>> from sympy.abc import n + >>> y = Function('y') + + >>> f = (n - 1)*y(n + 2) - (n**2 + 3*n - 2)*y(n + 1) + 2*n*(n + 1)*y(n) + + >>> rsolve(f, y(n)) + 2**n*C0 + C1*factorial(n) + + >>> rsolve(f, y(n), {y(0):0, y(1):3}) + 3*2**n - 3*factorial(n) + + See Also + ======== + + rsolve_poly, rsolve_ratio, rsolve_hyper + + """ + if isinstance(f, Equality): + f = f.lhs - f.rhs + + n = y.args[0] + k = Wild('k', exclude=(n,)) + + # Preprocess user input to allow things like + # y(n) + a*(y(n + 1) + y(n - 1))/2 + f = f.expand().collect(y.func(Wild('m', integer=True))) + + h_part = defaultdict(list) + i_part = [] + for g in Add.make_args(f): + coeff, dep = g.as_coeff_mul(y.func) + if not dep: + i_part.append(coeff) + continue + for h in dep: + if h.is_Function and h.func == y.func: + result = h.args[0].match(n + k) + if result is not None: + h_part[int(result[k])].append(coeff) + continue + raise ValueError( + "'%s(%s + k)' expected, got '%s'" % (y.func, n, h)) + for k in h_part: + h_part[k] = Add(*h_part[k]) + h_part.default_factory = lambda: 0 + i_part = Add(*i_part) + + for k, coeff in h_part.items(): + h_part[k] = simplify(coeff) + + common = S.One + + if not i_part.is_zero and not i_part.is_hypergeometric(n) and \ + not (i_part.is_Add and all((x.is_hypergeometric(n) for x in i_part.expand().args))): + raise ValueError("The independent term should be a sum of hypergeometric functions, got '%s'" % i_part) + + for coeff in h_part.values(): + if coeff.is_rational_function(n): + if not coeff.is_polynomial(n): + common = lcm(common, coeff.as_numer_denom()[1], n) + else: + raise ValueError( + "Polynomial or rational function expected, got '%s'" % coeff) + + i_numer, i_denom = i_part.as_numer_denom() + + if i_denom.is_polynomial(n): + common = lcm(common, i_denom, n) + + if common is not S.One: + for k, coeff in h_part.items(): + numer, denom = coeff.as_numer_denom() + h_part[k] = numer*quo(common, denom, n) + + i_part = i_numer*quo(common, i_denom, n) + + K_min = min(h_part.keys()) + + if K_min < 0: + K = abs(K_min) + + H_part = defaultdict(lambda: S.Zero) + i_part = i_part.subs(n, n + K).expand() + common = common.subs(n, n + K).expand() + + for k, coeff in h_part.items(): + H_part[k + K] = coeff.subs(n, n + K).expand() + else: + H_part = h_part + + K_max = max(H_part.keys()) + coeffs = [H_part[i] for i in range(K_max + 1)] + + result = rsolve_hyper(coeffs, -i_part, n, symbols=True) + + if result is None: + return None + + solution, symbols = result + + if init in ({}, []): + init = None + + if symbols and init is not None: + if isinstance(init, list): + init = {i: init[i] for i in range(len(init))} + + equations = [] + + for k, v in init.items(): + try: + i = int(k) + except TypeError: + if k.is_Function and k.func == y.func: + i = int(k.args[0]) + else: + raise ValueError("Integer or term expected, got '%s'" % k) + + eq = solution.subs(n, i) - v + if eq.has(S.NaN): + eq = solution.limit(n, i) - v + equations.append(eq) + + result = solve(equations, *symbols) + + if not result: + return None + else: + solution = solution.subs(result) + + return solution diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/simplex.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/simplex.py new file mode 100644 index 0000000000000000000000000000000000000000..c8e652cb626507d7829f9bc1c78fc6f49809865f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/simplex.py @@ -0,0 +1,1104 @@ +"""Tools for optimizing a linear function for a given simplex. + +For the linear objective function ``f`` with linear constraints +expressed using `Le`, `Ge` or `Eq` can be found with ``lpmin`` or +``lpmax``. The symbols are **unbounded** unless specifically +constrained. + +As an alternative, the matrices describing the objective and the +constraints, and an optional list of bounds can be passed to +``linprog`` which will solve for the minimization of ``C*x`` +under constraints ``A*x <= b`` and/or ``Aeq*x = beq``, and +individual bounds for variables given as ``(lo, hi)``. The values +returned are **nonnegative** unless bounds are provided that +indicate otherwise. + +Errors that might be raised are UnboundedLPError when there is no +finite solution for the system or InfeasibleLPError when the +constraints represent impossible conditions (i.e. a non-existent + simplex). + +Here is a simple 1-D system: minimize `x` given that ``x >= 1``. + + >>> from sympy.solvers.simplex import lpmin, linprog + >>> from sympy.abc import x + + The function and a list with the constraint is passed directly + to `lpmin`: + + >>> lpmin(x, [x >= 1]) + (1, {x: 1}) + + For `linprog` the matrix for the objective is `[1]` and the + uivariate constraint can be passed as a bound with None acting + as infinity: + + >>> linprog([1], bounds=(1, None)) + (1, [1]) + + Or the matrices, corresponding to ``x >= 1`` expressed as + ``-x <= -1`` as required by the routine, can be passed: + + >>> linprog([1], [-1], [-1]) + (1, [1]) + + If there is no limit for the objective, an error is raised. + In this case there is a valid region of interest (simplex) + but no limit to how small ``x`` can be: + + >>> lpmin(x, []) + Traceback (most recent call last): + ... + sympy.solvers.simplex.UnboundedLPError: + Objective function can assume arbitrarily large values! + + An error is raised if there is no possible solution: + + >>> lpmin(x,[x<=1,x>=2]) + Traceback (most recent call last): + ... + sympy.solvers.simplex.InfeasibleLPError: + Inconsistent/False constraint +""" + +from sympy.core import sympify +from sympy.core.exprtools import factor_terms +from sympy.core.relational import Le, Ge, Eq +from sympy.core.singleton import S +from sympy.core.symbol import Dummy +from sympy.core.sorting import ordered +from sympy.functions.elementary.complexes import sign +from sympy.matrices.dense import Matrix, zeros +from sympy.solvers.solveset import linear_eq_to_matrix +from sympy.utilities.iterables import numbered_symbols +from sympy.utilities.misc import filldedent + + +class UnboundedLPError(Exception): + """ + A linear programming problem is said to be unbounded if its objective + function can assume arbitrarily large values. + + Example + ======= + + Suppose you want to maximize + 2x + subject to + x >= 0 + + There's no upper limit that 2x can take. + """ + + pass + + +class InfeasibleLPError(Exception): + """ + A linear programming problem is considered infeasible if its + constraint set is empty. That is, if the set of all vectors + satisfying the constraints is empty, then the problem is infeasible. + + Example + ======= + + Suppose you want to maximize + x + subject to + x >= 10 + x <= 9 + + No x can satisfy those constraints. + """ + + pass + + +def _pivot(M, i, j): + """ + The pivot element `M[i, j]` is inverted and the rest of the matrix + modified and returned as a new matrix; original is left unmodified. + + Example + ======= + + >>> from sympy.matrices.dense import Matrix + >>> from sympy.solvers.simplex import _pivot + >>> from sympy import var + >>> Matrix(3, 3, var('a:i')) + Matrix([ + [a, b, c], + [d, e, f], + [g, h, i]]) + >>> _pivot(_, 1, 0) + Matrix([ + [-a/d, -a*e/d + b, -a*f/d + c], + [ 1/d, e/d, f/d], + [-g/d, h - e*g/d, i - f*g/d]]) + """ + Mi, Mj, Mij = M[i, :], M[:, j], M[i, j] + if Mij == 0: + raise ZeroDivisionError( + "Tried to pivot about zero-valued entry.") + A = M - Mj * (Mi / Mij) + A[i, :] = Mi / Mij + A[:, j] = -Mj / Mij + A[i, j] = 1 / Mij + return A + + +def _choose_pivot_row(A, B, candidate_rows, pivot_col, Y): + # Choose row with smallest ratio + # If there are ties, pick using Bland's rule + return min(candidate_rows, key=lambda i: (B[i] / A[i, pivot_col], Y[i])) + + +def _simplex(A, B, C, D=None, dual=False): + """Return ``(o, x, y)`` obtained from the two-phase simplex method + using Bland's rule: ``o`` is the minimum value of primal, + ``Cx - D``, under constraints ``Ax <= B`` (with ``x >= 0``) and + the maximum of the dual, ``y^{T}B - D``, under constraints + ``A^{T}*y >= C^{T}`` (with ``y >= 0``). To compute the dual of + the system, pass `dual=True` and ``(o, y, x)`` will be returned. + + Note: the nonnegative constraints for ``x`` and ``y`` supercede + any values of ``A`` and ``B`` that are inconsistent with that + assumption, so if a constraint of ``x >= -1`` is represented + in ``A`` and ``B``, no value will be obtained that is negative; if + a constraint of ``x <= -1`` is represented, an error will be + raised since no solution is possible. + + This routine relies on the ability of determining whether an + expression is 0 or not. This is guaranteed if the input contains + only Float or Rational entries. It will raise a TypeError if + a relationship does not evaluate to True or False. + + Examples + ======== + + >>> from sympy.solvers.simplex import _simplex + >>> from sympy import Matrix + + Consider the simple minimization of ``f = x + y + 1`` under the + constraint that ``y + 2*x >= 4``. This is the "standard form" of + a minimization. + + In the nonnegative quadrant, this inequality describes a area above + a triangle with vertices at (0, 4), (0, 0) and (2, 0). The minimum + of ``f`` occurs at (2, 0). Define A, B, C, D for the standard + minimization: + + >>> A = Matrix([[2, 1]]) + >>> B = Matrix([4]) + >>> C = Matrix([[1, 1]]) + >>> D = Matrix([-1]) + + Confirm that this is the system of interest: + + >>> from sympy.abc import x, y + >>> X = Matrix([x, y]) + >>> (C*X - D)[0] + x + y + 1 + >>> [i >= j for i, j in zip(A*X, B)] + [2*x + y >= 4] + + Since `_simplex` will do a minimization for constraints given as + ``A*x <= B``, the signs of ``A`` and ``B`` must be negated since + the currently correspond to a greater-than inequality: + + >>> _simplex(-A, -B, C, D) + (3, [2, 0], [1/2]) + + The dual of minimizing ``f`` is maximizing ``F = c*y - d`` for + ``a*y <= b`` where ``a``, ``b``, ``c``, ``d`` are derived from the + transpose of the matrix representation of the standard minimization: + + >>> tr = lambda a, b, c, d: [i.T for i in (a, c, b, d)] + >>> a, b, c, d = tr(A, B, C, D) + + This time ``a*x <= b`` is the expected inequality for the `_simplex` + method, but to maximize ``F``, the sign of ``c`` and ``d`` must be + changed (so that minimizing the negative will give the negative of + the maximum of ``F``): + + >>> _simplex(a, b, -c, -d) + (-3, [1/2], [2, 0]) + + The negative of ``F`` and the min of ``f`` are the same. The dual + point `[1/2]` is the value of ``y`` that minimized ``F = c*y - d`` + under constraints a*x <= b``: + + >>> y = Matrix(['y']) + >>> (c*y - d)[0] + 4*y + 1 + >>> [i <= j for i, j in zip(a*y,b)] + [2*y <= 1, y <= 1] + + In this 1-dimensional dual system, the more restrictive constraint is + the first which limits ``y`` between 0 and 1/2 and the maximum of + ``F`` is attained at the nonzero value, hence is ``4*(1/2) + 1 = 3``. + + In this case the values for ``x`` and ``y`` were the same when the + dual representation was solved. This is not always the case (though + the value of the function will be the same). + + >>> l = [[1, 1], [-1, 1], [0, 1], [-1, 0]], [5, 1, 2, -1], [[1, 1]], [-1] + >>> A, B, C, D = [Matrix(i) for i in l] + >>> _simplex(A, B, -C, -D) + (-6, [3, 2], [1, 0, 0, 0]) + >>> _simplex(A, B, -C, -D, dual=True) # [5, 0] != [3, 2] + (-6, [1, 0, 0, 0], [5, 0]) + + In both cases the function has the same value: + + >>> Matrix(C)*Matrix([3, 2]) == Matrix(C)*Matrix([5, 0]) + True + + See Also + ======== + _lp - poses min/max problem in form compatible with _simplex + lpmin - minimization which calls _lp + lpmax - maximimzation which calls _lp + + References + ========== + + .. [1] Thomas S. Ferguson, LINEAR PROGRAMMING: A Concise Introduction + web.tecnico.ulisboa.pt/mcasquilho/acad/or/ftp/FergusonUCLA_lp.pdf + + """ + A, B, C, D = [Matrix(i) for i in (A, B, C, D or [0])] + if dual: + _o, d, p = _simplex(-A.T, C.T, B.T, -D) + return -_o, d, p + + if A and B: + M = Matrix([[A, B], [C, D]]) + else: + if A or B: + raise ValueError("must give A and B") + # no constraints given + M = Matrix([[C, D]]) + n = M.cols - 1 + m = M.rows - 1 + + if not all(i.is_Float or i.is_Rational for i in M): + # with literal Float and Rational we are guaranteed the + # ability of determining whether an expression is 0 or not + raise TypeError(filldedent(""" + Only rationals and floats are allowed. + """ + ) + ) + + # x variables have priority over y variables during Bland's rule + # since False < True + X = [(False, j) for j in range(n)] + Y = [(True, i) for i in range(m)] + + # Phase 1: find a feasible solution or determine none exist + + ## keep track of last pivot row and column + last = None + + while True: + B = M[:-1, -1] + A = M[:-1, :-1] + if all(B[i] >= 0 for i in range(B.rows)): + # We have found a feasible solution + break + + # Find k: first row with a negative rightmost entry + for k in range(B.rows): + if B[k] < 0: + break # use current value of k below + else: + pass # error will raise below + + # Choose pivot column, c + piv_cols = [_ for _ in range(A.cols) if A[k, _] < 0] + if not piv_cols: + raise InfeasibleLPError(filldedent(""" + The constraint set is empty!""")) + _, c = min((X[i], i) for i in piv_cols) # Bland's rule + + # Choose pivot row, r + piv_rows = [_ for _ in range(A.rows) if A[_, c] > 0 and B[_] > 0] + piv_rows.append(k) + r = _choose_pivot_row(A, B, piv_rows, c, Y) + + # check for oscillation + if (r, c) == last: + # Not sure what to do here; it looks like there will be + # oscillations; see o1 test added at this commit to + # see a system with no solution and the o2 for one + # with a solution. In the case of o2, the solution + # from linprog is the same as the one from lpmin, but + # the matrices created in the lpmin case are different + # than those created without replacements in linprog and + # the matrices in the linprog case lead to oscillations. + # If the matrices could be re-written in linprog like + # lpmin does, this behavior could be avoided and then + # perhaps the oscillating case would only occur when + # there is no solution. For now, the output is checked + # before exit if oscillations were detected and an + # error is raised there if the solution was invalid. + # + # cf section 6 of Ferguson for a non-cycling modification + last = True + break + last = r, c + + M = _pivot(M, r, c) + X[c], Y[r] = Y[r], X[c] + + # Phase 2: from a feasible solution, pivot to optimal + while True: + B = M[:-1, -1] + A = M[:-1, :-1] + C = M[-1, :-1] + + # Choose a pivot column, c + piv_cols = [_ for _ in range(n) if C[_] < 0] + if not piv_cols: + break + _, c = min((X[i], i) for i in piv_cols) # Bland's rule + + # Choose a pivot row, r + piv_rows = [_ for _ in range(m) if A[_, c] > 0] + if not piv_rows: + raise UnboundedLPError(filldedent(""" + Objective function can assume + arbitrarily large values!""")) + r = _choose_pivot_row(A, B, piv_rows, c, Y) + + M = _pivot(M, r, c) + X[c], Y[r] = Y[r], X[c] + + argmax = [None] * n + argmin_dual = [None] * m + + for i, (v, n) in enumerate(X): + if v == False: + argmax[n] = 0 + else: + argmin_dual[n] = M[-1, i] + + for i, (v, n) in enumerate(Y): + if v == True: + argmin_dual[n] = 0 + else: + argmax[n] = M[i, -1] + + if last and not all(i >= 0 for i in argmax + argmin_dual): + raise InfeasibleLPError(filldedent(""" + Oscillating system led to invalid solution. + If you believe there was a valid solution, please + report this as a bug.""")) + return -M[-1, -1], argmax, argmin_dual + + +## routines that use _simplex or support those that do + + +def _abcd(M, list=False): + """return parts of M as matrices or lists + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.solvers.simplex import _abcd + + >>> m = Matrix(3, 3, range(9)); m + Matrix([ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8]]) + >>> a, b, c, d = _abcd(m) + >>> a + Matrix([ + [0, 1], + [3, 4]]) + >>> b + Matrix([ + [2], + [5]]) + >>> c + Matrix([[6, 7]]) + >>> d + Matrix([[8]]) + + The matrices can be returned as compact lists, too: + + >>> L = a, b, c, d = _abcd(m, list=True); L + ([[0, 1], [3, 4]], [2, 5], [[6, 7]], [8]) + """ + + def aslist(i): + l = i.tolist() + if len(l[0]) == 1: # col vector + return [i[0] for i in l] + return l + + m = M[:-1, :-1], M[:-1, -1], M[-1, :-1], M[-1:, -1:] + if not list: + return m + return tuple([aslist(i) for i in m]) + + +def _m(a, b, c, d=None): + """return Matrix([[a, b], [c, d]]) from matrices + in Matrix or list form. + + Examples + ======== + + >>> from sympy import Matrix + >>> from sympy.solvers.simplex import _abcd, _m + >>> m = Matrix(3, 3, range(9)) + >>> L = _abcd(m, list=True); L + ([[0, 1], [3, 4]], [2, 5], [[6, 7]], [8]) + >>> _abcd(m) + (Matrix([ + [0, 1], + [3, 4]]), Matrix([ + [2], + [5]]), Matrix([[6, 7]]), Matrix([[8]])) + >>> assert m == _m(*L) == _m(*_) + """ + a, b, c, d = [Matrix(i) for i in (a, b, c, d or [0])] + return Matrix([[a, b], [c, d]]) + + +def _primal_dual(M, factor=True): + """return primal and dual function and constraints + assuming that ``M = Matrix([[A, b], [c, d]])`` and the + function ``c*x - d`` is being minimized with ``Ax >= b`` + for nonnegative values of ``x``. The dual and its + constraints will be for maximizing `b.T*y - d` subject + to ``A.T*y <= c.T``. + + Examples + ======== + + >>> from sympy.solvers.simplex import _primal_dual, lpmin, lpmax + >>> from sympy import Matrix + + The following matrix represents the primal task of + minimizing x + y + 7 for y >= x + 1 and y >= -2*x + 3. + The dual task seeks to maximize x + 3*y + 7 with + 2*y - x <= 1 and and x + y <= 1: + + >>> M = Matrix([ + ... [-1, 1, 1], + ... [ 2, 1, 3], + ... [ 1, 1, -7]]) + >>> p, d = _primal_dual(M) + + The minimum of the primal and maximum of the dual are the same + (though they occur at different points): + + >>> lpmin(*p) + (28/3, {x1: 2/3, x2: 5/3}) + >>> lpmax(*d) + (28/3, {y1: 1/3, y2: 2/3}) + + If the equivalent (but canonical) inequalities are + desired, leave `factor=True`, otherwise the unmodified + inequalities for M will be returned. + + >>> m = Matrix([ + ... [-3, -2, 4, -2], + ... [ 2, 0, 0, -2], + ... [ 0, 1, -3, 0]]) + + >>> _primal_dual(m, False) # last condition is 2*x1 >= -2 + ((x2 - 3*x3, + [-3*x1 - 2*x2 + 4*x3 >= -2, 2*x1 >= -2]), + (-2*y1 - 2*y2, + [-3*y1 + 2*y2 <= 0, -2*y1 <= 1, 4*y1 <= -3])) + + >>> _primal_dual(m) # condition now x1 >= -1 + ((x2 - 3*x3, + [-3*x1 - 2*x2 + 4*x3 >= -2, x1 >= -1]), + (-2*y1 - 2*y2, + [-3*y1 + 2*y2 <= 0, -2*y1 <= 1, 4*y1 <= -3])) + + If you pass the transpose of the matrix, the primal will be + identified as the standard minimization problem and the + dual as the standard maximization: + + >>> _primal_dual(m.T) + ((-2*x1 - 2*x2, + [-3*x1 + 2*x2 >= 0, -2*x1 >= 1, 4*x1 >= -3]), + (y2 - 3*y3, + [-3*y1 - 2*y2 + 4*y3 <= -2, y1 <= -1])) + + A matrix must have some size or else None will be returned for + the functions: + + >>> _primal_dual(Matrix([[1, 2]])) + ((x1 - 2, []), (-2, [])) + + >>> _primal_dual(Matrix([])) + ((None, []), (None, [])) + + References + ========== + + .. [1] David Galvin, Relations between Primal and Dual + www3.nd.edu/~dgalvin1/30210/30210_F07/presentations/dual_opt.pdf + """ + if not M: + return (None, []), (None, []) + if not hasattr(M, "shape"): + if len(M) not in (3, 4): + raise ValueError("expecting Matrix or 3 or 4 lists") + M = _m(*M) + m, n = [i - 1 for i in M.shape] + A, b, c, d = _abcd(M) + d = d[0] + _ = lambda x: numbered_symbols(x, start=1) + x = Matrix([i for i, j in zip(_("x"), range(n))]) + yT = Matrix([i for i, j in zip(_("y"), range(m))]).T + + def ineq(L, r, op): + rv = [] + for r in (op(i, j) for i, j in zip(L, r)): + if r == True: + continue + elif r == False: + return [False] + if factor: + f = factor_terms(r) + if f.lhs.is_Mul and f.rhs % f.lhs.args[0] == 0: + assert len(f.lhs.args) == 2, f.lhs + k = f.lhs.args[0] + r = r.func(sign(k) * f.lhs.args[1], f.rhs // abs(k)) + rv.append(r) + return rv + + eq = lambda x, d: x[0] - d if x else -d + F = eq(c * x, d) + f = eq(yT * b, d) + return (F, ineq(A * x, b, Ge)), (f, ineq(yT * A, c, Le)) + + +def _rel_as_nonpos(constr, syms): + """return `(np, d, aux)` where `np` is a list of nonpositive + expressions that represent the given constraints (possibly + rewritten in terms of auxilliary variables) expressible with + nonnegative symbols, and `d` is a dictionary mapping a given + symbols to an expression with an auxilliary variable. In some + cases a symbol will be used as part of the change of variables, + e.g. x: x - z1 instead of x: z1 - z2. + + If any constraint is False/empty, return None. All variables in + ``constr`` are assumed to be unbounded unless explicitly indicated + otherwise with a univariate constraint, e.g. ``x >= 0`` will + restrict ``x`` to nonnegative values. + + The ``syms`` must be included so all symbols can be given an + unbounded assumption if they are not otherwise bound with + univariate conditions like ``x <= 3``. + + Examples + ======== + + >>> from sympy.solvers.simplex import _rel_as_nonpos + >>> from sympy.abc import x, y + >>> _rel_as_nonpos([x >= y, x >= 0, y >= 0], (x, y)) + ([-x + y], {}, []) + >>> _rel_as_nonpos([x >= 3, x <= 5], [x]) + ([_z1 - 2], {x: _z1 + 3}, [_z1]) + >>> _rel_as_nonpos([x <= 5], [x]) + ([], {x: 5 - _z1}, [_z1]) + >>> _rel_as_nonpos([x >= 1], [x]) + ([], {x: _z1 + 1}, [_z1]) + """ + r = {} # replacements to handle change of variables + np = [] # nonpositive expressions + aux = [] # auxilliary symbols added + ui = numbered_symbols("z", start=1, cls=Dummy) # auxilliary symbols + univariate = {} # {x: interval} for univariate constraints + unbound = [] # symbols designated as unbound + syms = set(syms) # the expected syms of the system + + # separate out univariates + for i in constr: + if i == True: + continue # ignore + if i == False: + return # no solution + if i.has(S.Infinity, S.NegativeInfinity): + raise ValueError("only finite bounds are permitted") + if isinstance(i, (Le, Ge)): + i = i.lts - i.gts + freei = i.free_symbols + if freei - syms: + raise ValueError( + "unexpected symbol(s) in constraint: %s" % (freei - syms) + ) + if len(freei) > 1: + np.append(i) + elif freei: + x = freei.pop() + if x in unbound: + continue # will handle later + ivl = Le(i, 0, evaluate=False).as_set() + if x not in univariate: + univariate[x] = ivl + else: + univariate[x] &= ivl + elif i: + return False + else: + raise TypeError(filldedent(""" + only equalities like Eq(x, y) or non-strict + inequalities like x >= y are allowed in lp, not %s""" % i)) + + # introduce auxilliary variables as needed for univariate + # inequalities + for x in syms: + i = univariate.get(x, True) + if not i: + return None # no solution possible + if i == True: + unbound.append(x) + continue + a, b = i.inf, i.sup + if a.is_infinite: + u = next(ui) + r[x] = b - u + aux.append(u) + elif b.is_infinite: + if a: + u = next(ui) + r[x] = a + u + aux.append(u) + else: + # standard nonnegative relationship + pass + else: + u = next(ui) + aux.append(u) + # shift so u = x - a => x = u + a + r[x] = u + a + # add constraint for u <= b - a + # since when u = b-a then x = u + a = b - a + a = b: + # the upper limit for x + np.append(u - (b - a)) + + # make change of variables for unbound variables + for x in unbound: + u = next(ui) + r[x] = u - x # reusing x + aux.append(u) + + return np, r, aux + + +def _lp_matrices(objective, constraints): + """return A, B, C, D, r, x+X, X for maximizing + objective = Cx - D with constraints Ax <= B, introducing + introducing auxilliary variables, X, as necessary to make + replacements of symbols as given in r, {xi: expression with Xj}, + so all variables in x+X will take on nonnegative values. + + Every univariate condition creates a semi-infinite + condition, e.g. a single ``x <= 3`` creates the + interval ``[-oo, 3]`` while ``x <= 3`` and ``x >= 2`` + create an interval ``[2, 3]``. Variables not in a univariate + expression will take on nonnegative values. + """ + + # sympify input and collect free symbols + F = sympify(objective) + np = [sympify(i) for i in constraints] + syms = set.union(*[i.free_symbols for i in [F] + np], set()) + + # change Eq(x, y) to x - y <= 0 and y - x <= 0 + for i in range(len(np)): + if isinstance(np[i], Eq): + np[i] = np[i].lhs - np[i].rhs <= 0 + np.append(-np[i].lhs <= 0) + + # convert constraints to nonpositive expressions + _ = _rel_as_nonpos(np, syms) + if _ is None: + raise InfeasibleLPError(filldedent(""" + Inconsistent/False constraint""")) + np, r, aux = _ + + # do change of variables + F = F.xreplace(r) + np = [i.xreplace(r) for i in np] + + # convert to matrices + xx = list(ordered(syms)) + aux + A, B = linear_eq_to_matrix(np, xx) + C, D = linear_eq_to_matrix([F], xx) + return A, B, C, D, r, xx, aux + + +def _lp(min_max, f, constr): + """Return the optimization (min or max) of ``f`` with the given + constraints. All variables are unbounded unless constrained. + + If `min_max` is 'max' then the results corresponding to the + maximization of ``f`` will be returned, else the minimization. + The constraints can be given as Le, Ge or Eq expressions. + + Examples + ======== + + >>> from sympy.solvers.simplex import _lp as lp + >>> from sympy import Eq + >>> from sympy.abc import x, y, z + >>> f = x + y - 2*z + >>> c = [7*x + 4*y - 7*z <= 3, 3*x - y + 10*z <= 6] + >>> c += [i >= 0 for i in (x, y, z)] + >>> lp(min, f, c) + (-6/5, {x: 0, y: 0, z: 3/5}) + + By passing max, the maximum value for f under the constraints + is returned (if possible): + + >>> lp(max, f, c) + (3/4, {x: 0, y: 3/4, z: 0}) + + Constraints that are equalities will require that the solution + also satisfy them: + + >>> lp(max, f, c + [Eq(y - 9*x, 1)]) + (5/7, {x: 0, y: 1, z: 1/7}) + + All symbols are reported, even if they are not in the objective + function: + + >>> lp(min, x, [y + x >= 3, x >= 0]) + (0, {x: 0, y: 3}) + """ + # get the matrix components for the system expressed + # in terms of only nonnegative variables + A, B, C, D, r, xx, aux = _lp_matrices(f, constr) + + how = str(min_max).lower() + if "max" in how: + # _simplex minimizes for Ax <= B so we + # have to change the sign of the function + # and negate the optimal value returned + _o, p, d = _simplex(A, B, -C, -D) + o = -_o + elif "min" in how: + o, p, d = _simplex(A, B, C, D) + else: + raise ValueError("expecting min or max") + + # restore original variables and remove aux from p + p = dict(zip(xx, p)) + if r: # p has original symbols and auxilliary symbols + # if r has x: x - z1 use values from p to update + r = {k: v.xreplace(p) for k, v in r.items()} + # then use the actual value of x (= x - z1) in p + p.update(r) + # don't show aux + p = {k: p[k] for k in ordered(p) if k not in aux} + + # not returning dual since there may be extra constraints + # when a variable has finite bounds + return o, p + + +def lpmin(f, constr): + """return minimum of linear equation ``f`` under + linear constraints expressed using Ge, Le or Eq. + + All variables are unbounded unless constrained. + + Examples + ======== + + >>> from sympy.solvers.simplex import lpmin + >>> from sympy import Eq + >>> from sympy.abc import x, y + >>> lpmin(x, [2*x - 3*y >= -1, Eq(x + 3*y, 2), x <= 2*y]) + (1/3, {x: 1/3, y: 5/9}) + + Negative values for variables are permitted unless explicitly + excluding, so minimizing ``x`` for ``x <= 3`` is an + unbounded problem while the following has a bounded solution: + + >>> lpmin(x, [x >= 0, x <= 3]) + (0, {x: 0}) + + Without indicating that ``x`` is nonnegative, there + is no minimum for this objective: + + >>> lpmin(x, [x <= 3]) + Traceback (most recent call last): + ... + sympy.solvers.simplex.UnboundedLPError: + Objective function can assume arbitrarily large values! + + See Also + ======== + linprog, lpmax + """ + return _lp(min, f, constr) + + +def lpmax(f, constr): + """return maximum of linear equation ``f`` under + linear constraints expressed using Ge, Le or Eq. + + All variables are unbounded unless constrained. + + Examples + ======== + + >>> from sympy.solvers.simplex import lpmax + >>> from sympy import Eq + >>> from sympy.abc import x, y + >>> lpmax(x, [2*x - 3*y >= -1, Eq(x+ 3*y,2), x <= 2*y]) + (4/5, {x: 4/5, y: 2/5}) + + Negative values for variables are permitted unless explicitly + excluding: + + >>> lpmax(x, [x <= -1]) + (-1, {x: -1}) + + If a non-negative constraint is added for x, there is no + possible solution: + + >>> lpmax(x, [x <= -1, x >= 0]) + Traceback (most recent call last): + ... + sympy.solvers.simplex.InfeasibleLPError: inconsistent/False constraint + + See Also + ======== + linprog, lpmin + """ + return _lp(max, f, constr) + + +def _handle_bounds(bounds): + # introduce auxiliary variables as needed for univariate + # inequalities + + def _make_list(length: int, index_value_pairs): + li = [0] * length + for idx, val in index_value_pairs: + li[idx] = val + return li + + unbound = [] + row = [] + row2 = [] + b_len = len(bounds) + for x, (a, b) in enumerate(bounds): + if a is None and b is None: + unbound.append(x) + elif a is None: + # r[x] = b - u + b_len += 1 + row.append(_make_list(b_len, [(x, 1), (-1, 1)])) + row.append(_make_list(b_len, [(x, -1), (-1, -1)])) + row2.extend([[b], [-b]]) + elif b is None: + if a: + # r[x] = a + u + b_len += 1 + row.append(_make_list(b_len, [(x, 1), (-1, -1)])) + row.append(_make_list(b_len, [(x, -1), (-1, 1)])) + row2.extend([[a], [-a]]) + else: + # standard nonnegative relationship + pass + else: + # r[x] = u + a + b_len += 1 + row.append(_make_list(b_len, [(x, 1), (-1, -1)])) + row.append(_make_list(b_len, [(x, -1), (-1, 1)])) + # u <= b - a + row.append(_make_list(b_len, [(-1, 1)])) + row2.extend([[a], [-a], [b - a]]) + + # make change of variables for unbound variables + for x in unbound: + # r[x] = u - v + b_len += 2 + row.append(_make_list(b_len, [(x, 1), (-1, 1), (-2, -1)])) + row.append(_make_list(b_len, [(x, -1), (-1, -1), (-2, 1)])) + row2.extend([[0], [0]]) + + return Matrix([r + [0]*(b_len - len(r)) for r in row]), Matrix(row2) + + +def linprog(c, A=None, b=None, A_eq=None, b_eq=None, bounds=None): + """Return the minimization of ``c*x`` with the given + constraints ``A*x <= b`` and ``A_eq*x = b_eq``. Unless bounds + are given, variables will have nonnegative values in the solution. + + If ``A`` is not given, then the dimension of the system will + be determined by the length of ``C``. + + By default, all variables will be nonnegative. If ``bounds`` + is given as a single tuple, ``(lo, hi)``, then all variables + will be constrained to be between ``lo`` and ``hi``. Use + None for a ``lo`` or ``hi`` if it is unconstrained in the + negative or positive direction, respectively, e.g. + ``(None, 0)`` indicates nonpositive values. To set + individual ranges, pass a list with length equal to the + number of columns in ``A``, each element being a tuple; if + only a few variables take on non-default values they can be + passed as a dictionary with keys giving the corresponding + column to which the variable is assigned, e.g. ``bounds={2: + (1, 4)}`` would limit the 3rd variable to have a value in + range ``[1, 4]``. + + Examples + ======== + + >>> from sympy.solvers.simplex import linprog + >>> from sympy import symbols, Eq, linear_eq_to_matrix as M, Matrix + >>> x = x1, x2, x3, x4 = symbols('x1:5') + >>> X = Matrix(x) + >>> c, d = M(5*x2 + x3 + 4*x4 - x1, x) + >>> a, b = M([5*x2 + 2*x3 + 5*x4 - (x1 + 5)], x) + >>> aeq, beq = M([Eq(3*x2 + x4, 2), Eq(-x1 + x3 + 2*x4, 1)], x) + >>> constr = [i <= j for i,j in zip(a*X, b)] + >>> constr += [Eq(i, j) for i,j in zip(aeq*X, beq)] + >>> linprog(c, a, b, aeq, beq) + (9/2, [0, 1/2, 0, 1/2]) + >>> assert all(i.subs(dict(zip(x, _[1]))) for i in constr) + + See Also + ======== + lpmin, lpmax + """ + + ## the objective + C = Matrix(c) + if C.rows != 1 and C.cols == 1: + C = C.T + if C.rows != 1: + raise ValueError("C must be a single row.") + + ## the inequalities + if not A: + if b: + raise ValueError("A and b must both be given") + # the governing equations will be simple constraints + # on variables + A, b = zeros(0, C.cols), zeros(C.cols, 1) + else: + A, b = [Matrix(i) for i in (A, b)] + + if A.cols != C.cols: + raise ValueError("number of columns in A and C must match") + + ## the equalities + if A_eq is None: + if not b_eq is None: + raise ValueError("A_eq and b_eq must both be given") + else: + A_eq, b_eq = [Matrix(i) for i in (A_eq, b_eq)] + # if x == y then x <= y and x >= y (-x <= -y) + A = A.col_join(A_eq) + A = A.col_join(-A_eq) + b = b.col_join(b_eq) + b = b.col_join(-b_eq) + + if not (bounds is None or bounds == {} or bounds == (0, None)): + ## the bounds are interpreted + if type(bounds) is tuple and len(bounds) == 2: + bounds = [bounds] * A.cols + elif len(bounds) == A.cols and all( + type(i) is tuple and len(i) == 2 for i in bounds): + pass # individual bounds + elif type(bounds) is dict and all( + type(i) is tuple and len(i) == 2 + for i in bounds.values()): + # sparse bounds + db = bounds + bounds = [(0, None)] * A.cols + while db: + i, j = db.popitem() + bounds[i] = j # IndexError if out-of-bounds indices + else: + raise ValueError("unexpected bounds %s" % bounds) + A_, b_ = _handle_bounds(bounds) + aux = A_.cols - A.cols + if A: + A = Matrix([[A, zeros(A.rows, aux)], [A_]]) + b = b.col_join(b_) + else: + A = A_ + b = b_ + C = C.row_join(zeros(1, aux)) + else: + aux = -A.cols # set so -aux will give all cols below + + o, p, d = _simplex(A, b, C) + return o, p[:-aux] # don't include aux values + +def show_linprog(c, A=None, b=None, A_eq=None, b_eq=None, bounds=None): + from sympy import symbols + ## the objective + C = Matrix(c) + if C.rows != 1 and C.cols == 1: + C = C.T + if C.rows != 1: + raise ValueError("C must be a single row.") + + ## the inequalities + if not A: + if b: + raise ValueError("A and b must both be given") + # the governing equations will be simple constraints + # on variables + A, b = zeros(0, C.cols), zeros(C.cols, 1) + else: + A, b = [Matrix(i) for i in (A, b)] + + if A.cols != C.cols: + raise ValueError("number of columns in A and C must match") + + ## the equalities + if A_eq is None: + if not b_eq is None: + raise ValueError("A_eq and b_eq must both be given") + else: + A_eq, b_eq = [Matrix(i) for i in (A_eq, b_eq)] + + if not (bounds is None or bounds == {} or bounds == (0, None)): + ## the bounds are interpreted + if type(bounds) is tuple and len(bounds) == 2: + bounds = [bounds] * A.cols + elif len(bounds) == A.cols and all( + type(i) is tuple and len(i) == 2 for i in bounds): + pass # individual bounds + elif type(bounds) is dict and all( + type(i) is tuple and len(i) == 2 + for i in bounds.values()): + # sparse bounds + db = bounds + bounds = [(0, None)] * A.cols + while db: + i, j = db.popitem() + bounds[i] = j # IndexError if out-of-bounds indices + else: + raise ValueError("unexpected bounds %s" % bounds) + + x = Matrix(symbols('x1:%s' % (A.cols+1))) + f,c = (C*x)[0], [i<=j for i,j in zip(A*x, b)] + [Eq(i,j) for i,j in zip(A_eq*x,b_eq)] + for i, (lo, hi) in enumerate(bounds): + if lo is not None: + c.append(x[i]>=lo) + if hi is not None: + c.append(x[i]<=hi) + return f,c diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/solvers.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/solvers.py new file mode 100644 index 0000000000000000000000000000000000000000..ef621a84e34bde43a3181d2fd90e26fa7b05e968 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/solvers.py @@ -0,0 +1,3674 @@ +""" +This module contain solvers for all kinds of equations: + + - algebraic or transcendental, use solve() + + - recurrence, use rsolve() + + - differential, use dsolve() + + - nonlinear (numerically), use nsolve() + (you will need a good starting point) + +""" +from __future__ import annotations + +from sympy.core import (S, Add, Symbol, Dummy, Expr, Mul) +from sympy.core.assumptions import check_assumptions +from sympy.core.exprtools import factor_terms +from sympy.core.function import (expand_mul, expand_log, Derivative, + AppliedUndef, UndefinedFunction, nfloat, + Function, expand_power_exp, _mexpand, expand, + expand_func) +from sympy.core.logic import fuzzy_not, fuzzy_and +from sympy.core.numbers import Float, Rational, _illegal +from sympy.core.intfunc import integer_log, ilcm +from sympy.core.power import Pow +from sympy.core.relational import Eq, Ne +from sympy.core.sorting import ordered, default_sort_key +from sympy.core.sympify import sympify, _sympify +from sympy.core.traversal import preorder_traversal +from sympy.logic.boolalg import And, BooleanAtom + +from sympy.functions import (log, exp, LambertW, cos, sin, tan, acos, asin, atan, + Abs, re, im, arg, sqrt, atan2) +from sympy.functions.combinatorial.factorials import binomial +from sympy.functions.elementary.hyperbolic import HyperbolicFunction +from sympy.functions.elementary.piecewise import piecewise_fold, Piecewise +from sympy.functions.elementary.trigonometric import TrigonometricFunction +from sympy.integrals.integrals import Integral +from sympy.ntheory.factor_ import divisors +from sympy.simplify import (simplify, collect, powsimp, posify, # type: ignore + powdenest, nsimplify, denom, logcombine, sqrtdenest, fraction, + separatevars) +from sympy.simplify.sqrtdenest import sqrt_depth +from sympy.simplify.fu import TR1, TR2i, TR10, TR11 +from sympy.strategies.rl import rebuild +from sympy.matrices.exceptions import NonInvertibleMatrixError +from sympy.matrices import Matrix, zeros +from sympy.polys import roots, cancel, factor, Poly +from sympy.polys.solvers import sympy_eqs_to_ring, solve_lin_sys +from sympy.polys.polyerrors import GeneratorsNeeded, PolynomialError +from sympy.polys.polytools import gcd +from sympy.utilities.lambdify import lambdify +from sympy.utilities.misc import filldedent, debugf +from sympy.utilities.iterables import (connected_components, + generate_bell, uniq, iterable, is_sequence, subsets, flatten, sift) +from sympy.utilities.decorator import conserve_mpmath_dps + +from mpmath import findroot + +from sympy.solvers.polysys import solve_poly_system + +from types import GeneratorType +from collections import defaultdict +from itertools import combinations, product + +import warnings + + +def recast_to_symbols(eqs, symbols): + """ + Return (e, s, d) where e and s are versions of *eqs* and + *symbols* in which any non-Symbol objects in *symbols* have + been replaced with generic Dummy symbols and d is a dictionary + that can be used to restore the original expressions. + + Examples + ======== + + >>> from sympy.solvers.solvers import recast_to_symbols + >>> from sympy import symbols, Function + >>> x, y = symbols('x y') + >>> fx = Function('f')(x) + >>> eqs, syms = [fx + 1, x, y], [fx, y] + >>> e, s, d = recast_to_symbols(eqs, syms); (e, s, d) + ([_X0 + 1, x, y], [_X0, y], {_X0: f(x)}) + + The original equations and symbols can be restored using d: + + >>> assert [i.xreplace(d) for i in eqs] == eqs + >>> assert [d.get(i, i) for i in s] == syms + + """ + if not iterable(eqs) and iterable(symbols): + raise ValueError('Both eqs and symbols must be iterable') + orig = list(symbols) + symbols = list(ordered(symbols)) + swap_sym = {} + i = 0 + for s in symbols: + if not isinstance(s, Symbol) and s not in swap_sym: + swap_sym[s] = Dummy('X%d' % i) + i += 1 + new_f = [] + for i in eqs: + isubs = getattr(i, 'subs', None) + if isubs is not None: + new_f.append(isubs(swap_sym)) + else: + new_f.append(i) + restore = {v: k for k, v in swap_sym.items()} + return new_f, [swap_sym.get(i, i) for i in orig], restore + + +def _ispow(e): + """Return True if e is a Pow or is exp.""" + return isinstance(e, Expr) and (e.is_Pow or isinstance(e, exp)) + + +def _simple_dens(f, symbols): + # when checking if a denominator is zero, we can just check the + # base of powers with nonzero exponents since if the base is zero + # the power will be zero, too. To keep it simple and fast, we + # limit simplification to exponents that are Numbers + dens = set() + for d in denoms(f, symbols): + if d.is_Pow and d.exp.is_Number: + if d.exp.is_zero: + continue # foo**0 is never 0 + d = d.base + dens.add(d) + return dens + + +def denoms(eq, *symbols): + """ + Return (recursively) set of all denominators that appear in *eq* + that contain any symbol in *symbols*; if *symbols* are not + provided then all denominators will be returned. + + Examples + ======== + + >>> from sympy.solvers.solvers import denoms + >>> from sympy.abc import x, y, z + + >>> denoms(x/y) + {y} + + >>> denoms(x/(y*z)) + {y, z} + + >>> denoms(3/x + y/z) + {x, z} + + >>> denoms(x/2 + y/z) + {2, z} + + If *symbols* are provided then only denominators containing + those symbols will be returned: + + >>> denoms(1/x + 1/y + 1/z, y, z) + {y, z} + + """ + + pot = preorder_traversal(eq) + dens = set() + for p in pot: + # Here p might be Tuple or Relational + # Expr subtrees (e.g. lhs and rhs) will be traversed after by pot + if not isinstance(p, Expr): + continue + den = denom(p) + if den is S.One: + continue + dens.update(Mul.make_args(den)) + if not symbols: + return dens + elif len(symbols) == 1: + if iterable(symbols[0]): + symbols = symbols[0] + return {d for d in dens if any(s in d.free_symbols for s in symbols)} + + +def checksol(f, symbol, sol=None, **flags): + """ + Checks whether sol is a solution of equation f == 0. + + Explanation + =========== + + Input can be either a single symbol and corresponding value + or a dictionary of symbols and values. When given as a dictionary + and flag ``simplify=True``, the values in the dictionary will be + simplified. *f* can be a single equation or an iterable of equations. + A solution must satisfy all equations in *f* to be considered valid; + if a solution does not satisfy any equation, False is returned; if one or + more checks are inconclusive (and none are False) then None is returned. + + Examples + ======== + + >>> from sympy import checksol, symbols + >>> x, y = symbols('x,y') + >>> checksol(x**4 - 1, x, 1) + True + >>> checksol(x**4 - 1, x, 0) + False + >>> checksol(x**2 + y**2 - 5**2, {x: 3, y: 4}) + True + + To check if an expression is zero using ``checksol()``, pass it + as *f* and send an empty dictionary for *symbol*: + + >>> checksol(x**2 + x - x*(x + 1), {}) + True + + None is returned if ``checksol()`` could not conclude. + + flags: + 'numerical=True (default)' + do a fast numerical check if ``f`` has only one symbol. + 'minimal=True (default is False)' + a very fast, minimal testing. + 'warn=True (default is False)' + show a warning if checksol() could not conclude. + 'simplify=True (default)' + simplify solution before substituting into function and + simplify the function before trying specific simplifications + 'force=True (default is False)' + make positive all symbols without assumptions regarding sign. + + """ + from sympy.physics.units import Unit + + minimal = flags.get('minimal', False) + + if sol is not None: + sol = {symbol: sol} + elif isinstance(symbol, dict): + sol = symbol + else: + msg = 'Expecting (sym, val) or ({sym: val}, None) but got (%s, %s)' + raise ValueError(msg % (symbol, sol)) + + if iterable(f): + if not f: + raise ValueError('no functions to check') + return fuzzy_and(checksol(fi, sol, **flags) for fi in f) + + f = _sympify(f) + + if f.is_number: + return f.is_zero + + if isinstance(f, Poly): + f = f.as_expr() + elif isinstance(f, (Eq, Ne)): + if f.rhs in (S.true, S.false): + f = f.reversed + B, E = f.args + if isinstance(B, BooleanAtom): + f = f.subs(sol) + if not f.is_Boolean: + return + elif isinstance(f, Eq): + f = Add(f.lhs, -f.rhs, evaluate=False) + + if isinstance(f, BooleanAtom): + return bool(f) + elif not f.is_Relational and not f: + return True + + illegal = set(_illegal) + if any(sympify(v).atoms() & illegal for k, v in sol.items()): + return False + + attempt = -1 + numerical = flags.get('numerical', True) + while 1: + attempt += 1 + if attempt == 0: + val = f.subs(sol) + if isinstance(val, Mul): + val = val.as_independent(Unit)[0] + if val.atoms() & illegal: + return False + elif attempt == 1: + if not val.is_number: + if not val.is_constant(*list(sol.keys()), simplify=not minimal): + return False + # there are free symbols -- simple expansion might work + _, val = val.as_content_primitive() + val = _mexpand(val.as_numer_denom()[0], recursive=True) + elif attempt == 2: + if minimal: + return + if flags.get('simplify', True): + for k in sol: + sol[k] = simplify(sol[k]) + # start over without the failed expanded form, possibly + # with a simplified solution + val = simplify(f.subs(sol)) + if flags.get('force', True): + val, reps = posify(val) + # expansion may work now, so try again and check + exval = _mexpand(val, recursive=True) + if exval.is_number: + # we can decide now + val = exval + else: + # if there are no radicals and no functions then this can't be + # zero anymore -- can it? + pot = preorder_traversal(expand_mul(val)) + seen = set() + saw_pow_func = False + for p in pot: + if p in seen: + continue + seen.add(p) + if p.is_Pow and not p.exp.is_Integer: + saw_pow_func = True + elif p.is_Function: + saw_pow_func = True + elif isinstance(p, UndefinedFunction): + saw_pow_func = True + if saw_pow_func: + break + if saw_pow_func is False: + return False + if flags.get('force', True): + # don't do a zero check with the positive assumptions in place + val = val.subs(reps) + nz = fuzzy_not(val.is_zero) + if nz is not None: + # issue 5673: nz may be True even when False + # so these are just hacks to keep a false positive + # from being returned + + # HACK 1: LambertW (issue 5673) + if val.is_number and val.has(LambertW): + # don't eval this to verify solution since if we got here, + # numerical must be False + return None + + # add other HACKs here if necessary, otherwise we assume + # the nz value is correct + return not nz + break + if val.is_Rational: + return val == 0 + if numerical and val.is_number: + return (abs(val.n(18).n(12, chop=True)) < 1e-9) is S.true + + if flags.get('warn', False): + warnings.warn("\n\tWarning: could not verify solution %s." % sol) + # returns None if it can't conclude + # TODO: improve solution testing + + +def solve(f, *symbols, **flags): + r""" + Algebraically solves equations and systems of equations. + + Explanation + =========== + + Currently supported: + - polynomial + - transcendental + - piecewise combinations of the above + - systems of linear and polynomial equations + - systems containing relational expressions + - systems implied by undetermined coefficients + + Examples + ======== + + The default output varies according to the input and might + be a list (possibly empty), a dictionary, a list of + dictionaries or tuples, or an expression involving relationals. + For specifics regarding different forms of output that may appear, see :ref:`solve_output`. + Let it suffice here to say that to obtain a uniform output from + `solve` use ``dict=True`` or ``set=True`` (see below). + + >>> from sympy import solve, Poly, Eq, Matrix, Symbol + >>> from sympy.abc import x, y, z, a, b + + The expressions that are passed can be Expr, Equality, or Poly + classes (or lists of the same); a Matrix is considered to be a + list of all the elements of the matrix: + + >>> solve(x - 3, x) + [3] + >>> solve(Eq(x, 3), x) + [3] + >>> solve(Poly(x - 3), x) + [3] + >>> solve(Matrix([[x, x + y]]), x, y) == solve([x, x + y], x, y) + True + + If no symbols are indicated to be of interest and the equation is + univariate, a list of values is returned; otherwise, the keys in + a dictionary will indicate which (of all the variables used in + the expression(s)) variables and solutions were found: + + >>> solve(x**2 - 4) + [-2, 2] + >>> solve((x - a)*(y - b)) + [{a: x}, {b: y}] + >>> solve([x - 3, y - 1]) + {x: 3, y: 1} + >>> solve([x - 3, y**2 - 1]) + [{x: 3, y: -1}, {x: 3, y: 1}] + + If you pass symbols for which solutions are sought, the output will vary + depending on the number of symbols you passed, whether you are passing + a list of expressions or not, and whether a linear system was solved. + Uniform output is attained by using ``dict=True`` or ``set=True``. + + >>> #### *** feel free to skip to the stars below *** #### + >>> from sympy import TableForm + >>> h = [None, ';|;'.join(['e', 's', 'solve(e, s)', 'solve(e, s, dict=True)', + ... 'solve(e, s, set=True)']).split(';')] + >>> t = [] + >>> for e, s in [ + ... (x - y, y), + ... (x - y, [x, y]), + ... (x**2 - y, [x, y]), + ... ([x - 3, y -1], [x, y]), + ... ]: + ... how = [{}, dict(dict=True), dict(set=True)] + ... res = [solve(e, s, **f) for f in how] + ... t.append([e, '|', s, '|'] + [res[0], '|', res[1], '|', res[2]]) + ... + >>> # ******************************************************* # + >>> TableForm(t, headings=h, alignments="<") + e | s | solve(e, s) | solve(e, s, dict=True) | solve(e, s, set=True) + --------------------------------------------------------------------------------------- + x - y | y | [x] | [{y: x}] | ([y], {(x,)}) + x - y | [x, y] | [(y, y)] | [{x: y}] | ([x, y], {(y, y)}) + x**2 - y | [x, y] | [(x, x**2)] | [{y: x**2}] | ([x, y], {(x, x**2)}) + [x - 3, y - 1] | [x, y] | {x: 3, y: 1} | [{x: 3, y: 1}] | ([x, y], {(3, 1)}) + + * If any equation does not depend on the symbol(s) given, it will be + eliminated from the equation set and an answer may be given + implicitly in terms of variables that were not of interest: + + >>> solve([x - y, y - 3], x) + {x: y} + + When you pass all but one of the free symbols, an attempt + is made to find a single solution based on the method of + undetermined coefficients. If it succeeds, a dictionary of values + is returned. If you want an algebraic solutions for one + or more of the symbols, pass the expression to be solved in a list: + + >>> e = a*x + b - 2*x - 3 + >>> solve(e, [a, b]) + {a: 2, b: 3} + >>> solve([e], [a, b]) + {a: -b/x + (2*x + 3)/x} + + When there is no solution for any given symbol which will make all + expressions zero, the empty list is returned (or an empty set in + the tuple when ``set=True``): + + >>> from sympy import sqrt + >>> solve(3, x) + [] + >>> solve(x - 3, y) + [] + >>> solve(sqrt(x) + 1, x, set=True) + ([x], set()) + + When an object other than a Symbol is given as a symbol, it is + isolated algebraically and an implicit solution may be obtained. + This is mostly provided as a convenience to save you from replacing + the object with a Symbol and solving for that Symbol. It will only + work if the specified object can be replaced with a Symbol using the + subs method: + + >>> from sympy import exp, Function + >>> f = Function('f') + + >>> solve(f(x) - x, f(x)) + [x] + >>> solve(f(x).diff(x) - f(x) - x, f(x).diff(x)) + [x + f(x)] + >>> solve(f(x).diff(x) - f(x) - x, f(x)) + [-x + Derivative(f(x), x)] + >>> solve(x + exp(x)**2, exp(x), set=True) + ([exp(x)], {(-sqrt(-x),), (sqrt(-x),)}) + + >>> from sympy import Indexed, IndexedBase, Tuple + >>> A = IndexedBase('A') + >>> eqs = Tuple(A[1] + A[2] - 3, A[1] - A[2] + 1) + >>> solve(eqs, eqs.atoms(Indexed)) + {A[1]: 1, A[2]: 2} + + * To solve for a function within a derivative, use :func:`~.dsolve`. + + To solve for a symbol implicitly, use implicit=True: + + >>> solve(x + exp(x), x) + [-LambertW(1)] + >>> solve(x + exp(x), x, implicit=True) + [-exp(x)] + + It is possible to solve for anything in an expression that can be + replaced with a symbol using :obj:`~sympy.core.basic.Basic.subs`: + + >>> solve(x + 2 + sqrt(3), x + 2) + [-sqrt(3)] + >>> solve((x + 2 + sqrt(3), x + 4 + y), y, x + 2) + {y: -2 + sqrt(3), x + 2: -sqrt(3)} + + * Nothing heroic is done in this implicit solving so you may end up + with a symbol still in the solution: + + >>> eqs = (x*y + 3*y + sqrt(3), x + 4 + y) + >>> solve(eqs, y, x + 2) + {y: -sqrt(3)/(x + 3), x + 2: -2*x/(x + 3) - 6/(x + 3) + sqrt(3)/(x + 3)} + >>> solve(eqs, y*x, x) + {x: -y - 4, x*y: -3*y - sqrt(3)} + + * If you attempt to solve for a number, remember that the number + you have obtained does not necessarily mean that the value is + equivalent to the expression obtained: + + >>> solve(sqrt(2) - 1, 1) + [sqrt(2)] + >>> solve(x - y + 1, 1) # /!\ -1 is targeted, too + [x/(y - 1)] + >>> [_.subs(z, -1) for _ in solve((x - y + 1).subs(-1, z), 1)] + [-x + y] + + **Additional Examples** + + ``solve()`` with check=True (default) will run through the symbol tags to + eliminate unwanted solutions. If no assumptions are included, all possible + solutions will be returned: + + >>> x = Symbol("x") + >>> solve(x**2 - 1) + [-1, 1] + + By setting the ``positive`` flag, only one solution will be returned: + + >>> pos = Symbol("pos", positive=True) + >>> solve(pos**2 - 1) + [1] + + When the solutions are checked, those that make any denominator zero + are automatically excluded. If you do not want to exclude such solutions, + then use the check=False option: + + >>> from sympy import sin, limit + >>> solve(sin(x)/x) # 0 is excluded + [pi] + + If ``check=False``, then a solution to the numerator being zero is found + but the value of $x = 0$ is a spurious solution since $\sin(x)/x$ has the well + known limit (without discontinuity) of 1 at $x = 0$: + + >>> solve(sin(x)/x, check=False) + [0, pi] + + In the following case, however, the limit exists and is equal to the + value of $x = 0$ that is excluded when check=True: + + >>> eq = x**2*(1/x - z**2/x) + >>> solve(eq, x) + [] + >>> solve(eq, x, check=False) + [0] + >>> limit(eq, x, 0, '-') + 0 + >>> limit(eq, x, 0, '+') + 0 + + **Solving Relationships** + + When one or more expressions passed to ``solve`` is a relational, + a relational result is returned (and the ``dict`` and ``set`` flags + are ignored): + + >>> solve(x < 3) + (-oo < x) & (x < 3) + >>> solve([x < 3, x**2 > 4], x) + ((-oo < x) & (x < -2)) | ((2 < x) & (x < 3)) + >>> solve([x + y - 3, x > 3], x) + (3 < x) & (x < oo) & Eq(x, 3 - y) + + Although checking of assumptions on symbols in relationals + is not done, setting assumptions will affect how certain + relationals might automatically simplify: + + >>> solve(x**2 > 4) + ((-oo < x) & (x < -2)) | ((2 < x) & (x < oo)) + + >>> r = Symbol('r', real=True) + >>> solve(r**2 > 4) + (2 < r) | (r < -2) + + There is currently no algorithm in SymPy that allows you to use + relationships to resolve more than one variable. So the following + does not determine that ``q < 0`` (and trying to solve for ``r`` + and ``q`` will raise an error): + + >>> from sympy import symbols + >>> r, q = symbols('r, q', real=True) + >>> solve([r + q - 3, r > 3], r) + (3 < r) & Eq(r, 3 - q) + + You can directly call the routine that ``solve`` calls + when it encounters a relational: :func:`~.reduce_inequalities`. + It treats Expr like Equality. + + >>> from sympy import reduce_inequalities + >>> reduce_inequalities([x**2 - 4]) + Eq(x, -2) | Eq(x, 2) + + If each relationship contains only one symbol of interest, + the expressions can be processed for multiple symbols: + + >>> reduce_inequalities([0 <= x - 1, y < 3], [x, y]) + (-oo < y) & (1 <= x) & (x < oo) & (y < 3) + + But an error is raised if any relationship has more than one + symbol of interest: + + >>> reduce_inequalities([0 <= x*y - 1, y < 3], [x, y]) + Traceback (most recent call last): + ... + NotImplementedError: + inequality has more than one symbol of interest. + + **Disabling High-Order Explicit Solutions** + + When solving polynomial expressions, you might not want explicit solutions + (which can be quite long). If the expression is univariate, ``CRootOf`` + instances will be returned instead: + + >>> solve(x**3 - x + 1) + [-1/((-1/2 - sqrt(3)*I/2)*(3*sqrt(69)/2 + 27/2)**(1/3)) - + (-1/2 - sqrt(3)*I/2)*(3*sqrt(69)/2 + 27/2)**(1/3)/3, + -(-1/2 + sqrt(3)*I/2)*(3*sqrt(69)/2 + 27/2)**(1/3)/3 - + 1/((-1/2 + sqrt(3)*I/2)*(3*sqrt(69)/2 + 27/2)**(1/3)), + -(3*sqrt(69)/2 + 27/2)**(1/3)/3 - + 1/(3*sqrt(69)/2 + 27/2)**(1/3)] + >>> solve(x**3 - x + 1, cubics=False) + [CRootOf(x**3 - x + 1, 0), + CRootOf(x**3 - x + 1, 1), + CRootOf(x**3 - x + 1, 2)] + + If the expression is multivariate, no solution might be returned: + + >>> solve(x**3 - x + a, x, cubics=False) + [] + + Sometimes solutions will be obtained even when a flag is False because the + expression could be factored. In the following example, the equation can + be factored as the product of a linear and a quadratic factor so explicit + solutions (which did not require solving a cubic expression) are obtained: + + >>> eq = x**3 + 3*x**2 + x - 1 + >>> solve(eq, cubics=False) + [-1, -1 + sqrt(2), -sqrt(2) - 1] + + **Solving Equations Involving Radicals** + + Because of SymPy's use of the principle root, some solutions + to radical equations will be missed unless check=False: + + >>> from sympy import root + >>> eq = root(x**3 - 3*x**2, 3) + 1 - x + >>> solve(eq) + [] + >>> solve(eq, check=False) + [1/3] + + In the above example, there is only a single solution to the + equation. Other expressions will yield spurious roots which + must be checked manually; roots which give a negative argument + to odd-powered radicals will also need special checking: + + >>> from sympy import real_root, S + >>> eq = root(x, 3) - root(x, 5) + S(1)/7 + >>> solve(eq) # this gives 2 solutions but misses a 3rd + [CRootOf(7*x**5 - 7*x**3 + 1, 1)**15, + CRootOf(7*x**5 - 7*x**3 + 1, 2)**15] + >>> sol = solve(eq, check=False) + >>> [abs(eq.subs(x,i).n(2)) for i in sol] + [0.48, 0.e-110, 0.e-110, 0.052, 0.052] + + The first solution is negative so ``real_root`` must be used to see that it + satisfies the expression: + + >>> abs(real_root(eq.subs(x, sol[0])).n(2)) + 0.e-110 + + If the roots of the equation are not real then more care will be + necessary to find the roots, especially for higher order equations. + Consider the following expression: + + >>> expr = root(x, 3) - root(x, 5) + + We will construct a known value for this expression at x = 3 by selecting + the 1-th root for each radical: + + >>> expr1 = root(x, 3, 1) - root(x, 5, 1) + >>> v = expr1.subs(x, -3) + + The ``solve`` function is unable to find any exact roots to this equation: + + >>> eq = Eq(expr, v); eq1 = Eq(expr1, v) + >>> solve(eq, check=False), solve(eq1, check=False) + ([], []) + + The function ``unrad``, however, can be used to get a form of the equation + for which numerical roots can be found: + + >>> from sympy.solvers.solvers import unrad + >>> from sympy import nroots + >>> e, (p, cov) = unrad(eq) + >>> pvals = nroots(e) + >>> inversion = solve(cov, x)[0] + >>> xvals = [inversion.subs(p, i) for i in pvals] + + Although ``eq`` or ``eq1`` could have been used to find ``xvals``, the + solution can only be verified with ``expr1``: + + >>> z = expr - v + >>> [xi.n(chop=1e-9) for xi in xvals if abs(z.subs(x, xi).n()) < 1e-9] + [] + >>> z1 = expr1 - v + >>> [xi.n(chop=1e-9) for xi in xvals if abs(z1.subs(x, xi).n()) < 1e-9] + [-3.0] + + Parameters + ========== + + f : + - a single Expr or Poly that must be zero + - an Equality + - a Relational expression + - a Boolean + - iterable of one or more of the above + + symbols : (object(s) to solve for) specified as + - none given (other non-numeric objects will be used) + - single symbol + - denested list of symbols + (e.g., ``solve(f, x, y)``) + - ordered iterable of symbols + (e.g., ``solve(f, [x, y])``) + + flags : + dict=True (default is False) + Return list (perhaps empty) of solution mappings. + set=True (default is False) + Return list of symbols and set of tuple(s) of solution(s). + exclude=[] (default) + Do not try to solve for any of the free symbols in exclude; + if expressions are given, the free symbols in them will + be extracted automatically. + check=True (default) + If False, do not do any testing of solutions. This can be + useful if you want to include solutions that make any + denominator zero. + numerical=True (default) + Do a fast numerical check if *f* has only one symbol. + minimal=True (default is False) + A very fast, minimal testing. + warn=True (default is False) + Show a warning if ``checksol()`` could not conclude. + simplify=True (default) + Simplify all but polynomials of order 3 or greater before + returning them and (if check is not False) use the + general simplify function on the solutions and the + expression obtained when they are substituted into the + function which should be zero. + force=True (default is False) + Make positive all symbols without assumptions regarding sign. + rational=True (default) + Recast Floats as Rational; if this option is not used, the + system containing Floats may fail to solve because of issues + with polys. If rational=None, Floats will be recast as + rationals but the answer will be recast as Floats. If the + flag is False then nothing will be done to the Floats. + manual=True (default is False) + Do not use the polys/matrix method to solve a system of + equations, solve them one at a time as you might "manually." + implicit=True (default is False) + Allows ``solve`` to return a solution for a pattern in terms of + other functions that contain that pattern; this is only + needed if the pattern is inside of some invertible function + like cos, exp, etc. + particular=True (default is False) + Instructs ``solve`` to try to find a particular solution to + a linear system with as many zeros as possible; this is very + expensive. + quick=True (default is False; ``particular`` must be True) + Selects a fast heuristic to find a solution with many zeros + whereas a value of False uses the very slow method guaranteed + to find the largest number of zeros possible. + cubics=True (default) + Return explicit solutions when cubic expressions are encountered. + When False, quartics and quintics are disabled, too. + quartics=True (default) + Return explicit solutions when quartic expressions are encountered. + When False, quintics are disabled, too. + quintics=True (default) + Return explicit solutions (if possible) when quintic expressions + are encountered. + + See Also + ======== + + rsolve: For solving recurrence relationships + sympy.solvers.ode.dsolve: For solving differential equations + + """ + from .inequalities import reduce_inequalities + + # checking/recording flags + ########################################################################### + + # set solver types explicitly; as soon as one is False + # all the rest will be False + hints = ('cubics', 'quartics', 'quintics') + default = True + for k in hints: + default = flags.setdefault(k, bool(flags.get(k, default))) + + # allow solution to contain symbol if True: + implicit = flags.get('implicit', False) + + # record desire to see warnings + warn = flags.get('warn', False) + + # this flag will be needed for quick exits below, so record + # now -- but don't record `dict` yet since it might change + as_set = flags.get('set', False) + + # keeping track of how f was passed + bare_f = not iterable(f) + + # check flag usage for particular/quick which should only be used + # with systems of equations + if flags.get('quick', None) is not None: + if not flags.get('particular', None): + raise ValueError('when using `quick`, `particular` should be True') + if flags.get('particular', False) and bare_f: + raise ValueError(filldedent(""" + The 'particular/quick' flag is usually used with systems of + equations. Either pass your equation in a list or + consider using a solver like `diophantine` if you are + looking for a solution in integers.""")) + + # sympify everything, creating list of expressions and list of symbols + ########################################################################### + + def _sympified_list(w): + return list(map(sympify, w if iterable(w) else [w])) + f, symbols = (_sympified_list(w) for w in [f, symbols]) + + # preprocess symbol(s) + ########################################################################### + + ordered_symbols = None # were the symbols in a well defined order? + if not symbols: + # get symbols from equations + symbols = set().union(*[fi.free_symbols for fi in f]) + if len(symbols) < len(f): + for fi in f: + pot = preorder_traversal(fi) + for p in pot: + if isinstance(p, AppliedUndef): + if not as_set: + flags['dict'] = True # better show symbols + symbols.add(p) + pot.skip() # don't go any deeper + ordered_symbols = False + symbols = list(ordered(symbols)) # to make it canonical + else: + if len(symbols) == 1 and iterable(symbols[0]): + symbols = symbols[0] + ordered_symbols = symbols and is_sequence(symbols, + include=GeneratorType) + _symbols = list(uniq(symbols)) + if len(_symbols) != len(symbols): + ordered_symbols = False + symbols = list(ordered(symbols)) + else: + symbols = _symbols + + # check for duplicates + if len(symbols) != len(set(symbols)): + raise ValueError('duplicate symbols given') + # remove those not of interest + exclude = flags.pop('exclude', set()) + if exclude: + if isinstance(exclude, Expr): + exclude = [exclude] + exclude = set().union(*[e.free_symbols for e in sympify(exclude)]) + symbols = [s for s in symbols if s not in exclude] + + # preprocess equation(s) + ########################################################################### + + # automatically ignore True values + if isinstance(f, list): + f = [s for s in f if s is not S.true] + + # handle canonicalization of equation types + for i, fi in enumerate(f): + if isinstance(fi, (Eq, Ne)): + if 'ImmutableDenseMatrix' in [type(a).__name__ for a in fi.args]: + fi = fi.lhs - fi.rhs + else: + L, R = fi.args + if isinstance(R, BooleanAtom): + L, R = R, L + if isinstance(L, BooleanAtom): + if isinstance(fi, Ne): + L = ~L + if R.is_Relational: + fi = ~R if L is S.false else R + elif R.is_Symbol: + return L + elif R.is_Boolean and (~R).is_Symbol: + return ~L + else: + raise NotImplementedError(filldedent(''' + Unanticipated argument of Eq when other arg + is True or False. + ''')) + elif isinstance(fi, Eq): + fi = Add(fi.lhs, -fi.rhs, evaluate=False) + f[i] = fi + + # *** dispatch and handle as a system of relationals + # ************************************************** + if fi.is_Relational: + if len(symbols) != 1: + raise ValueError("can only solve for one symbol at a time") + if warn and symbols[0].assumptions0: + warnings.warn(filldedent(""" + \tWarning: assumptions about variable '%s' are + not handled currently.""" % symbols[0])) + return reduce_inequalities(f, symbols=symbols) + + # convert Poly to expression + if isinstance(fi, Poly): + f[i] = fi.as_expr() + + # rewrite hyperbolics in terms of exp if they have symbols of + # interest + f[i] = f[i].replace(lambda w: isinstance(w, HyperbolicFunction) and \ + w.has_free(*symbols), lambda w: w.rewrite(exp)) + + # if we have a Matrix, we need to iterate over its elements again + if f[i].is_Matrix: + try: + f[i] = f[i].as_explicit() + except ValueError: + raise ValueError( + "solve cannot handle matrices with symbolic shape." + ) + bare_f = False + f.extend(list(f[i])) + f[i] = S.Zero + + # if we can split it into real and imaginary parts then do so + freei = f[i].free_symbols + if freei and all(s.is_extended_real or s.is_imaginary for s in freei): + fr, fi = f[i].as_real_imag() + # accept as long as new re, im, arg or atan2 are not introduced + had = f[i].atoms(re, im, arg, atan2) + if fr and fi and fr != fi and not any( + i.atoms(re, im, arg, atan2) - had for i in (fr, fi)): + if bare_f: + bare_f = False + f[i: i + 1] = [fr, fi] + + # real/imag handling ----------------------------- + if any(isinstance(fi, (bool, BooleanAtom)) for fi in f): + if as_set: + return [], set() + return [] + + for i, fi in enumerate(f): + # Abs + while True: + was = fi + fi = fi.replace(Abs, lambda arg: + separatevars(Abs(arg)).rewrite(Piecewise) if arg.has(*symbols) + else Abs(arg)) + if was == fi: + break + + for e in fi.find(Abs): + if e.has(*symbols): + raise NotImplementedError('solving %s when the argument ' + 'is not real or imaginary.' % e) + + # arg + fi = fi.replace(arg, lambda a: arg(a).rewrite(atan2).rewrite(atan)) + + # save changes + f[i] = fi + + # see if re(s) or im(s) appear + freim = [fi for fi in f if fi.has(re, im)] + if freim: + irf = [] + for s in symbols: + if s.is_real or s.is_imaginary: + continue # neither re(x) nor im(x) will appear + # if re(s) or im(s) appear, the auxiliary equation must be present + if any(fi.has(re(s), im(s)) for fi in freim): + irf.append((s, re(s) + S.ImaginaryUnit*im(s))) + if irf: + for s, rhs in irf: + f = [fi.xreplace({s: rhs}) for fi in f] + [s - rhs] + symbols.extend([re(s), im(s)]) + if bare_f: + bare_f = False + flags['dict'] = True + # end of real/imag handling ----------------------------- + + # we can solve for non-symbol entities by replacing them with Dummy symbols + f, symbols, swap_sym = recast_to_symbols(f, symbols) + # this set of symbols (perhaps recast) is needed below + symset = set(symbols) + + # get rid of equations that have no symbols of interest; we don't + # try to solve them because the user didn't ask and they might be + # hard to solve; this means that solutions may be given in terms + # of the eliminated equations e.g. solve((x-y, y-3), x) -> {x: y} + newf = [] + for fi in f: + # let the solver handle equations that.. + # - have no symbols but are expressions + # - have symbols of interest + # - have no symbols of interest but are constant + # but when an expression is not constant and has no symbols of + # interest, it can't change what we obtain for a solution from + # the remaining equations so we don't include it; and if it's + # zero it can be removed and if it's not zero, there is no + # solution for the equation set as a whole + # + # The reason for doing this filtering is to allow an answer + # to be obtained to queries like solve((x - y, y), x); without + # this mod the return value is [] + ok = False + if fi.free_symbols & symset: + ok = True + else: + if fi.is_number: + if fi.is_Number: + if fi.is_zero: + continue + return [] + ok = True + else: + if fi.is_constant(): + ok = True + if ok: + newf.append(fi) + if not newf: + if as_set: + return symbols, set() + return [] + f = newf + del newf + + # mask off any Object that we aren't going to invert: Derivative, + # Integral, etc... so that solving for anything that they contain will + # give an implicit solution + seen = set() + non_inverts = set() + for fi in f: + pot = preorder_traversal(fi) + for p in pot: + if not isinstance(p, Expr) or isinstance(p, Piecewise): + pass + elif (isinstance(p, bool) or + not p.args or + p in symset or + p.is_Add or p.is_Mul or + p.is_Pow and not implicit or + p.is_Function and not implicit) and p.func not in (re, im): + continue + elif p not in seen: + seen.add(p) + if p.free_symbols & symset: + non_inverts.add(p) + else: + continue + pot.skip() + del seen + non_inverts = dict(list(zip(non_inverts, [Dummy() for _ in non_inverts]))) + f = [fi.subs(non_inverts) for fi in f] + + # Both xreplace and subs are needed below: xreplace to force substitution + # inside Derivative, subs to handle non-straightforward substitutions + non_inverts = [(v, k.xreplace(swap_sym).subs(swap_sym)) for k, v in non_inverts.items()] + + # rationalize Floats + floats = False + if flags.get('rational', True) is not False: + for i, fi in enumerate(f): + if fi.has(Float): + floats = True + f[i] = nsimplify(fi, rational=True) + + # capture any denominators before rewriting since + # they may disappear after the rewrite, e.g. issue 14779 + flags['_denominators'] = _simple_dens(f[0], symbols) + + # Any embedded piecewise functions need to be brought out to the + # top level so that the appropriate strategy gets selected. + # However, this is necessary only if one of the piecewise + # functions depends on one of the symbols we are solving for. + def _has_piecewise(e): + if e.is_Piecewise: + return e.has(*symbols) + return any(_has_piecewise(a) for a in e.args) + for i, fi in enumerate(f): + if _has_piecewise(fi): + f[i] = piecewise_fold(fi) + + # expand angles of sums; in general, expand_trig will allow + # more roots to be found but this is not a great solultion + # to not returning a parametric solution, otherwise + # many values can be returned that have a simple + # relationship between values + targs = {t for fi in f for t in fi.atoms(TrigonometricFunction)} + if len(targs) > 1: + add, other = sift(targs, lambda x: x.args[0].is_Add, binary=True) + add, other = [[i for i in l if i.has_free(*symbols)] for l in (add, other)] + trep = {} + for t in add: + a = t.args[0] + ind, dep = a.as_independent(*symbols) + if dep in symbols or -dep in symbols: + # don't let expansion expand wrt anything in ind + n = Dummy() if not ind.is_Number else ind + trep[t] = TR10(t.func(dep + n)).xreplace({n: ind}) + if other and len(other) <= 2: + base = gcd(*[i.args[0] for i in other]) if len(other) > 1 else other[0].args[0] + for i in other: + trep[i] = TR11(i, base) + f = [fi.xreplace(trep) for fi in f] + + # + # try to get a solution + ########################################################################### + if bare_f: + solution = None + if len(symbols) != 1: + solution = _solve_undetermined(f[0], symbols, flags) + if not solution: + solution = _solve(f[0], *symbols, **flags) + else: + linear, solution = _solve_system(f, symbols, **flags) + assert type(solution) is list + assert not solution or type(solution[0]) is dict, solution + # + # postprocessing + ########################################################################### + # capture as_dict flag now (as_set already captured) + as_dict = flags.get('dict', False) + + # define how solution will get unpacked + tuple_format = lambda s: [tuple([i.get(x, x) for x in symbols]) for i in s] + if as_dict or as_set: + unpack = None + elif bare_f: + if len(symbols) == 1: + unpack = lambda s: [i[symbols[0]] for i in s] + elif len(solution) == 1 and len(solution[0]) == len(symbols): + # undetermined linear coeffs solution + unpack = lambda s: s[0] + elif ordered_symbols: + unpack = tuple_format + else: + unpack = lambda s: s + else: + if solution: + if linear and len(solution) == 1: + # if you want the tuple solution for the linear + # case, use `set=True` + unpack = lambda s: s[0] + elif ordered_symbols: + unpack = tuple_format + else: + unpack = lambda s: s + else: + unpack = None + + # Restore masked-off objects + if non_inverts and type(solution) is list: + solution = [{k: v.subs(non_inverts) for k, v in s.items()} + for s in solution] + + # Restore original "symbols" if a dictionary is returned. + # This is not necessary for + # - the single univariate equation case + # since the symbol will have been removed from the solution; + # - the nonlinear poly_system since that only supports zero-dimensional + # systems and those results come back as a list + # + # ** unless there were Derivatives with the symbols, but those were handled + # above. + if swap_sym: + symbols = [swap_sym.get(k, k) for k in symbols] + for i, sol in enumerate(solution): + solution[i] = {swap_sym.get(k, k): v.subs(swap_sym) + for k, v in sol.items()} + + # Get assumptions about symbols, to filter solutions. + # Note that if assumptions about a solution can't be verified, it is still + # returned. + check = flags.get('check', True) + + # restore floats + if floats and solution and flags.get('rational', None) is None: + solution = nfloat(solution, exponent=False) + # nfloat might reveal more duplicates + solution = _remove_duplicate_solutions(solution) + + if check and solution: # assumption checking + warn = flags.get('warn', False) + got_None = [] # solutions for which one or more symbols gave None + no_False = [] # solutions for which no symbols gave False + for sol in solution: + v = fuzzy_and(check_assumptions(val, **symb.assumptions0) + for symb, val in sol.items()) + if v is False: + continue + no_False.append(sol) + if v is None: + got_None.append(sol) + + solution = no_False + if warn and got_None: + warnings.warn(filldedent(""" + \tWarning: assumptions concerning following solution(s) + cannot be checked:""" + '\n\t' + + ', '.join(str(s) for s in got_None))) + + # + # done + ########################################################################### + + if not solution: + if as_set: + return symbols, set() + return [] + + # make orderings canonical for list of dictionaries + if not as_set: # for set, no point in ordering + solution = [{k: s[k] for k in ordered(s)} for s in solution] + solution.sort(key=default_sort_key) + + if not (as_set or as_dict): + return unpack(solution) + + if as_dict: + return solution + + # set output: (symbols, {t1, t2, ...}) from list of dictionaries; + # include all symbols for those that like a verbose solution + # and to resolve any differences in dictionary keys. + # + # The set results can easily be used to make a verbose dict as + # k, v = solve(eqs, syms, set=True) + # sol = [dict(zip(k,i)) for i in v] + # + if ordered_symbols: + k = symbols # keep preferred order + else: + # just unify the symbols for which solutions were found + k = list(ordered(set(flatten(tuple(i.keys()) for i in solution)))) + return k, {tuple([s.get(ki, ki) for ki in k]) for s in solution} + + +def _solve_undetermined(g, symbols, flags): + """solve helper to return a list with one dict (solution) else None + + A direct call to solve_undetermined_coeffs is more flexible and + can return both multiple solutions and handle more than one independent + variable. Here, we have to be more cautious to keep from solving + something that does not look like an undetermined coeffs system -- + to minimize the surprise factor since singularities that cancel are not + prohibited in solve_undetermined_coeffs. + """ + if g.free_symbols - set(symbols): + sol = solve_undetermined_coeffs(g, symbols, **dict(flags, dict=True, set=None)) + if len(sol) == 1: + return sol + + +def _solve(f, *symbols, **flags): + """Return a checked solution for *f* in terms of one or more of the + symbols in the form of a list of dictionaries. + + If no method is implemented to solve the equation, a NotImplementedError + will be raised. In the case that conversion of an expression to a Poly + gives None a ValueError will be raised. + """ + + not_impl_msg = "No algorithms are implemented to solve equation %s" + + if len(symbols) != 1: + # look for solutions for desired symbols that are independent + # of symbols already solved for, e.g. if we solve for x = y + # then no symbol having x in its solution will be returned. + + # First solve for linear symbols (since that is easier and limits + # solution size) and then proceed with symbols appearing + # in a non-linear fashion. Ideally, if one is solving a single + # expression for several symbols, they would have to be + # appear in factors of an expression, but we do not here + # attempt factorization. XXX perhaps handling a Mul + # should come first in this routine whether there is + # one or several symbols. + nonlin_s = [] + got_s = set() + rhs_s = set() + result = [] + for s in symbols: + xi, v = solve_linear(f, symbols=[s]) + if xi == s: + # no need to check but we should simplify if desired + if flags.get('simplify', True): + v = simplify(v) + vfree = v.free_symbols + if vfree & got_s: + # was linear, but has redundant relationship + # e.g. x - y = 0 has y == x is redundant for x == y + # so ignore + continue + rhs_s |= vfree + got_s.add(xi) + result.append({xi: v}) + elif xi: # there might be a non-linear solution if xi is not 0 + nonlin_s.append(s) + if not nonlin_s: + return result + for s in nonlin_s: + try: + soln = _solve(f, s, **flags) + for sol in soln: + if sol[s].free_symbols & got_s: + # depends on previously solved symbols: ignore + continue + got_s.add(s) + result.append(sol) + except NotImplementedError: + continue + if got_s: + return result + else: + raise NotImplementedError(not_impl_msg % f) + + # solve f for a single variable + + symbol = symbols[0] + + # expand binomials only if it has the unknown symbol + f = f.replace(lambda e: isinstance(e, binomial) and e.has(symbol), + lambda e: expand_func(e)) + + # checking will be done unless it is turned off before making a + # recursive call; the variables `checkdens` and `check` are + # captured here (for reference below) in case flag value changes + flags['check'] = checkdens = check = flags.pop('check', True) + + # build up solutions if f is a Mul + if f.is_Mul: + result = set() + for m in f.args: + if m in {S.NegativeInfinity, S.ComplexInfinity, S.Infinity}: + result = set() + break + soln = _vsolve(m, symbol, **flags) + result.update(set(soln)) + result = [{symbol: v} for v in result] + if check: + # all solutions have been checked but now we must + # check that the solutions do not set denominators + # in any factor to zero + dens = flags.get('_denominators', _simple_dens(f, symbols)) + result = [s for s in result if + not any(checksol(den, s, **flags) for den in + dens)] + # set flags for quick exit at end; solutions for each + # factor were already checked and simplified + check = False + flags['simplify'] = False + + elif f.is_Piecewise: + result = set() + if any(e.is_zero for e, c in f.args): + f = f.simplify() # failure imminent w/o help + + cond = neg = True + for expr, cnd in f.args: + # the explicit condition for this expr is the current cond + # and none of the previous conditions + cond = And(neg, cnd) + neg = And(neg, ~cond) + + if expr.is_zero and cond.simplify() != False: + raise NotImplementedError(filldedent(''' + An expression is already zero when %s. + This means that in this *region* the solution + is zero but solve can only represent discrete, + not interval, solutions. If this is a spurious + interval it might be resolved with simplification + of the Piecewise conditions.''' % cond)) + candidates = _vsolve(expr, symbol, **flags) + + for candidate in candidates: + if candidate in result: + # an unconditional value was already there + continue + try: + v = cond.subs(symbol, candidate) + _eval_simplify = getattr(v, '_eval_simplify', None) + if _eval_simplify is not None: + # unconditionally take the simplification of v + v = _eval_simplify(ratio=2, measure=lambda x: 1) + except TypeError: + # incompatible type with condition(s) + continue + if v == False: + continue + if v == True: + result.add(candidate) + else: + result.add(Piecewise( + (candidate, v), + (S.NaN, True))) + # solutions already checked and simplified + # **************************************** + return [{symbol: r} for r in result] + else: + # first see if it really depends on symbol and whether there + # is only a linear solution + f_num, sol = solve_linear(f, symbols=symbols) + if f_num.is_zero or sol is S.NaN: + return [] + elif f_num.is_Symbol: + # no need to check but simplify if desired + if flags.get('simplify', True): + sol = simplify(sol) + return [{f_num: sol}] + + poly = None + # check for a single Add generator + if not f_num.is_Add: + add_args = [i for i in f_num.atoms(Add) + if symbol in i.free_symbols] + if len(add_args) == 1: + gen = add_args[0] + spart = gen.as_independent(symbol)[1].as_base_exp()[0] + if spart == symbol: + try: + poly = Poly(f_num, spart) + except PolynomialError: + pass + + result = False # no solution was obtained + msg = '' # there is no failure message + + # Poly is generally robust enough to convert anything to + # a polynomial and tell us the different generators that it + # contains, so we will inspect the generators identified by + # polys to figure out what to do. + + # try to identify a single generator that will allow us to solve this + # as a polynomial, followed (perhaps) by a change of variables if the + # generator is not a symbol + + try: + if poly is None: + poly = Poly(f_num) + if poly is None: + raise ValueError('could not convert %s to Poly' % f_num) + except GeneratorsNeeded: + simplified_f = simplify(f_num) + if simplified_f != f_num: + return _solve(simplified_f, symbol, **flags) + raise ValueError('expression appears to be a constant') + + gens = [g for g in poly.gens if g.has(symbol)] + + def _as_base_q(x): + """Return (b**e, q) for x = b**(p*e/q) where p/q is the leading + Rational of the exponent of x, e.g. exp(-2*x/3) -> (exp(x), 3) + """ + b, e = x.as_base_exp() + if e.is_Rational: + return b, e.q + if not e.is_Mul: + return x, 1 + c, ee = e.as_coeff_Mul() + if c.is_Rational and c is not S.One: # c could be a Float + return b**ee, c.q + return x, 1 + + if len(gens) > 1: + # If there is more than one generator, it could be that the + # generators have the same base but different powers, e.g. + # >>> Poly(exp(x) + 1/exp(x)) + # Poly(exp(-x) + exp(x), exp(-x), exp(x), domain='ZZ') + # + # If unrad was not disabled then there should be no rational + # exponents appearing as in + # >>> Poly(sqrt(x) + sqrt(sqrt(x))) + # Poly(sqrt(x) + x**(1/4), sqrt(x), x**(1/4), domain='ZZ') + + bases, qs = list(zip(*[_as_base_q(g) for g in gens])) + bases = set(bases) + + if len(bases) > 1 or not all(q == 1 for q in qs): + funcs = {b for b in bases if b.is_Function} + + trig = {_ for _ in funcs if + isinstance(_, TrigonometricFunction)} + other = funcs - trig + if not other and len(funcs.intersection(trig)) > 1: + newf = None + if f_num.is_Add and len(f_num.args) == 2: + # check for sin(x)**p = cos(x)**p + _args = f_num.args + t = a, b = [i.atoms(Function).intersection( + trig) for i in _args] + if all(len(i) == 1 for i in t): + a, b = [i.pop() for i in t] + if isinstance(a, cos): + a, b = b, a + _args = _args[::-1] + if isinstance(a, sin) and isinstance(b, cos + ) and a.args[0] == b.args[0]: + # sin(x) + cos(x) = 0 -> tan(x) + 1 = 0 + newf, _d = (TR2i(_args[0]/_args[1]) + 1 + ).as_numer_denom() + if not _d.is_Number: + newf = None + if newf is None: + newf = TR1(f_num).rewrite(tan) + if newf != f_num: + # don't check the rewritten form --check + # solutions in the un-rewritten form below + flags['check'] = False + result = _solve(newf, symbol, **flags) + flags['check'] = check + + # just a simple case - see if replacement of single function + # clears all symbol-dependent functions, e.g. + # log(x) - log(log(x) - 1) - 3 can be solved even though it has + # two generators. + + if result is False and funcs: + funcs = list(ordered(funcs)) # put shallowest function first + f1 = funcs[0] + t = Dummy('t') + # perform the substitution + ftry = f_num.subs(f1, t) + + # if no Functions left, we can proceed with usual solve + if not ftry.has(symbol): + cv_sols = _solve(ftry, t, **flags) + cv_inv = list(ordered(_vsolve(t - f1, symbol, **flags)))[0] + result = [{symbol: cv_inv.subs(sol)} for sol in cv_sols] + + if result is False: + msg = 'multiple generators %s' % gens + + else: + # e.g. case where gens are exp(x), exp(-x) + u = bases.pop() + t = Dummy('t') + inv = _vsolve(u - t, symbol, **flags) + if isinstance(u, (Pow, exp)): + # this will be resolved by factor in _tsolve but we might + # as well try a simple expansion here to get things in + # order so something like the following will work now without + # having to factor: + # + # >>> eq = (exp(I*(-x-2))+exp(I*(x+2))) + # >>> eq.subs(exp(x),y) # fails + # exp(I*(-x - 2)) + exp(I*(x + 2)) + # >>> eq.expand().subs(exp(x),y) # works + # y**I*exp(2*I) + y**(-I)*exp(-2*I) + def _expand(p): + b, e = p.as_base_exp() + e = expand_mul(e) + return expand_power_exp(b**e) + ftry = f_num.replace( + lambda w: w.is_Pow or isinstance(w, exp), + _expand).subs(u, t) + if not ftry.has(symbol): + soln = _solve(ftry, t, **flags) + result = [{symbol: i.subs(s)} for i in inv for s in soln] + + elif len(gens) == 1: + + # There is only one generator that we are interested in, but + # there may have been more than one generator identified by + # polys (e.g. for symbols other than the one we are interested + # in) so recast the poly in terms of our generator of interest. + # Also use composite=True with f_num since Poly won't update + # poly as documented in issue 8810. + + poly = Poly(f_num, gens[0], composite=True) + + # if we aren't on the tsolve-pass, use roots + if not flags.pop('tsolve', False): + soln = None + deg = poly.degree() + flags['tsolve'] = True + hints = ('cubics', 'quartics', 'quintics') + solvers = {h: flags.get(h) for h in hints} + soln = roots(poly, **solvers) + if sum(soln.values()) < deg: + # e.g. roots(32*x**5 + 400*x**4 + 2032*x**3 + + # 5000*x**2 + 6250*x + 3189) -> {} + # so all_roots is used and RootOf instances are + # returned *unless* the system is multivariate + # or high-order EX domain. + try: + soln = poly.all_roots() + except NotImplementedError: + if not flags.get('incomplete', True): + raise NotImplementedError( + filldedent(''' + Neither high-order multivariate polynomials + nor sorting of EX-domain polynomials is supported. + If you want to see any results, pass keyword incomplete=True to + solve; to see numerical values of roots + for univariate expressions, use nroots. + ''')) + else: + pass + else: + soln = list(soln.keys()) + + if soln is not None: + u = poly.gen + if u != symbol: + try: + t = Dummy('t') + inv = _vsolve(u - t, symbol, **flags) + soln = {i.subs(t, s) for i in inv for s in soln} + except NotImplementedError: + # perhaps _tsolve can handle f_num + soln = None + else: + check = False # only dens need to be checked + if soln is not None: + if len(soln) > 2: + # if the flag wasn't set then unset it since high-order + # results are quite long. Perhaps one could base this + # decision on a certain critical length of the + # roots. In addition, wester test M2 has an expression + # whose roots can be shown to be real with the + # unsimplified form of the solution whereas only one of + # the simplified forms appears to be real. + flags['simplify'] = flags.get('simplify', False) + if soln is not None: + result = [{symbol: v} for v in soln] + + # fallback if above fails + # ----------------------- + if result is False: + # try unrad + if flags.pop('_unrad', True): + try: + u = unrad(f_num, symbol) + except (ValueError, NotImplementedError): + u = False + if u: + eq, cov = u + if cov: + isym, ieq = cov + inv = _vsolve(ieq, symbol, **flags)[0] + rv = {inv.subs(xi) for xi in _solve(eq, isym, **flags)} + else: + try: + rv = set(_vsolve(eq, symbol, **flags)) + except NotImplementedError: + rv = None + if rv is not None: + result = [{symbol: v} for v in rv] + # if the flag wasn't set then unset it since unrad results + # can be quite long or of very high order + flags['simplify'] = flags.get('simplify', False) + else: + pass # for coverage + + # try _tsolve + if result is False: + flags.pop('tsolve', None) # allow tsolve to be used on next pass + try: + soln = _tsolve(f_num, symbol, **flags) + if soln is not None: + result = [{symbol: v} for v in soln] + except PolynomialError: + pass + # ----------- end of fallback ---------------------------- + + if result is False: + raise NotImplementedError('\n'.join([msg, not_impl_msg % f])) + + result = _remove_duplicate_solutions(result) + + if flags.get('simplify', True): + result = [{k: d[k].simplify() for k in d} for d in result] + # Simplification might reveal more duplicates + result = _remove_duplicate_solutions(result) + # we just simplified the solution so we now set the flag to + # False so the simplification doesn't happen again in checksol() + flags['simplify'] = False + + if checkdens: + # reject any result that makes any denom. affirmatively 0; + # if in doubt, keep it + dens = _simple_dens(f, symbols) + result = [r for r in result if + not any(checksol(d, r, **flags) + for d in dens)] + if check: + # keep only results if the check is not False + result = [r for r in result if + checksol(f_num, r, **flags) is not False] + return result + + +def _remove_duplicate_solutions(solutions: list[dict[Expr, Expr]] + ) -> list[dict[Expr, Expr]]: + """Remove duplicates from a list of dicts""" + solutions_set = set() + solutions_new = [] + + for sol in solutions: + solset = frozenset(sol.items()) + if solset not in solutions_set: + solutions_new.append(sol) + solutions_set.add(solset) + + return solutions_new + + +def _solve_system(exprs, symbols, **flags): + """return ``(linear, solution)`` where ``linear`` is True + if the system was linear, else False; ``solution`` + is a list of dictionaries giving solutions for the symbols + """ + if not exprs: + return False, [] + + if flags.pop('_split', True): + # Split the system into connected components + V = exprs + symsset = set(symbols) + exprsyms = {e: e.free_symbols & symsset for e in exprs} + E = [] + sym_indices = {sym: i for i, sym in enumerate(symbols)} + for n, e1 in enumerate(exprs): + for e2 in exprs[:n]: + # Equations are connected if they share a symbol + if exprsyms[e1] & exprsyms[e2]: + E.append((e1, e2)) + G = V, E + subexprs = connected_components(G) + if len(subexprs) > 1: + subsols = [] + linear = True + for subexpr in subexprs: + subsyms = set() + for e in subexpr: + subsyms |= exprsyms[e] + subsyms = sorted(subsyms, key = lambda x: sym_indices[x]) + flags['_split'] = False # skip split step + _linear, subsol = _solve_system(subexpr, subsyms, **flags) + if linear: + linear = linear and _linear + if not isinstance(subsol, list): + subsol = [subsol] + subsols.append(subsol) + # Full solution is cartesian product of subsystems + sols = [] + for soldicts in product(*subsols): + sols.append(dict(item for sd in soldicts + for item in sd.items())) + return linear, sols + + polys = [] + dens = set() + failed = [] + result = [] + solved_syms = [] + linear = True + manual = flags.get('manual', False) + checkdens = check = flags.get('check', True) + + for j, g in enumerate(exprs): + dens.update(_simple_dens(g, symbols)) + i, d = _invert(g, *symbols) + if d in symbols: + if linear: + linear = solve_linear(g, 0, [d])[0] == d + g = d - i + g = g.as_numer_denom()[0] + if manual: + failed.append(g) + continue + + poly = g.as_poly(*symbols, extension=True) + + if poly is not None: + polys.append(poly) + else: + failed.append(g) + + if polys: + if all(p.is_linear for p in polys): + n, m = len(polys), len(symbols) + matrix = zeros(n, m + 1) + + for i, poly in enumerate(polys): + for monom, coeff in poly.terms(): + try: + j = monom.index(1) + matrix[i, j] = coeff + except ValueError: + matrix[i, m] = -coeff + + # returns a dictionary ({symbols: values}) or None + if flags.pop('particular', False): + result = minsolve_linear_system(matrix, *symbols, **flags) + else: + result = solve_linear_system(matrix, *symbols, **flags) + result = [result] if result else [] + if failed: + if result: + solved_syms = list(result[0].keys()) # there is only one result dict + else: + solved_syms = [] + # linear doesn't change + else: + linear = False + if len(symbols) > len(polys): + + free = set().union(*[p.free_symbols for p in polys]) + free = list(ordered(free.intersection(symbols))) + got_s = set() + result = [] + for syms in subsets(free, min(len(free), len(polys))): + try: + # returns [], None or list of tuples + res = solve_poly_system(polys, *syms) + if res: + for r in set(res): + skip = False + for r1 in r: + if got_s and any(ss in r1.free_symbols + for ss in got_s): + # sol depends on previously + # solved symbols: discard it + skip = True + if not skip: + got_s.update(syms) + result.append(dict(list(zip(syms, r)))) + except NotImplementedError: + pass + if got_s: + solved_syms = list(got_s) + else: + failed.extend([g.as_expr() for g in polys]) + else: + try: + result = solve_poly_system(polys, *symbols) + if result: + solved_syms = symbols + result = [dict(list(zip(solved_syms, r))) for r in set(result)] + except NotImplementedError: + failed.extend([g.as_expr() for g in polys]) + solved_syms = [] + + # convert None or [] to [{}] + result = result or [{}] + + if failed: + linear = False + # For each failed equation, see if we can solve for one of the + # remaining symbols from that equation. If so, we update the + # solution set and continue with the next failed equation, + # repeating until we are done or we get an equation that can't + # be solved. + def _ok_syms(e, sort=False): + rv = e.free_symbols & legal + + # Solve first for symbols that have lower degree in the equation. + # Ideally we want to solve firstly for symbols that appear linearly + # with rational coefficients e.g. if e = x*y + z then we should + # solve for z first. + def key(sym): + ep = e.as_poly(sym) + if ep is None: + complexity = (S.Infinity, S.Infinity, S.Infinity) + else: + coeff_syms = ep.LC().free_symbols + complexity = (ep.degree(), len(coeff_syms & rv), len(coeff_syms)) + return complexity + (default_sort_key(sym),) + + if sort: + rv = sorted(rv, key=key) + return rv + + legal = set(symbols) # what we are interested in + # sort so equation with the fewest potential symbols is first + u = Dummy() # used in solution checking + for eq in ordered(failed, lambda _: len(_ok_syms(_))): + newresult = [] + bad_results = [] + hit = False + for r in result: + got_s = set() + # update eq with everything that is known so far + eq2 = eq.subs(r) + # if check is True then we see if it satisfies this + # equation, otherwise we just accept it + if check and r: + b = checksol(u, u, eq2, minimal=True) + if b is not None: + # this solution is sufficient to know whether + # it is valid or not so we either accept or + # reject it, then continue + if b: + newresult.append(r) + else: + bad_results.append(r) + continue + # search for a symbol amongst those available that + # can be solved for + ok_syms = _ok_syms(eq2, sort=True) + if not ok_syms: + if r: + newresult.append(r) + break # skip as it's independent of desired symbols + for s in ok_syms: + try: + soln = _vsolve(eq2, s, **flags) + except NotImplementedError: + continue + # put each solution in r and append the now-expanded + # result in the new result list; use copy since the + # solution for s is being added in-place + for sol in soln: + if got_s and any(ss in sol.free_symbols for ss in got_s): + # sol depends on previously solved symbols: discard it + continue + rnew = r.copy() + for k, v in r.items(): + rnew[k] = v.subs(s, sol) + # and add this new solution + rnew[s] = sol + # check that it is independent of previous solutions + iset = set(rnew.items()) + for i in newresult: + if len(i) < len(iset): + # update i with what is known + i_items_updated = {(k, v.xreplace(rnew)) for k, v in i.items()} + if not i_items_updated - iset: + # this is a superset of a known solution that + # is smaller + break + else: + # keep it + newresult.append(rnew) + hit = True + got_s.add(s) + if not hit: + raise NotImplementedError('could not solve %s' % eq2) + else: + result = newresult + for b in bad_results: + if b in result: + result.remove(b) + + if not result: + return False, [] + + # rely on linear/polynomial system solvers to simplify + # XXX the following tests show that the expressions + # returned are not the same as they would be if simplify + # were applied to this: + # sympy/solvers/ode/tests/test_systems/test__classify_linear_system + # sympy/solvers/tests/test_solvers/test_issue_4886 + # so the docs should be updated to reflect that or else + # the following should be `bool(failed) or not linear` + default_simplify = bool(failed) + if flags.get('simplify', default_simplify): + for r in result: + for k in r: + r[k] = simplify(r[k]) + flags['simplify'] = False # don't need to do so in checksol now + + if checkdens: + result = [r for r in result + if not any(checksol(d, r, **flags) for d in dens)] + + if check and not linear: + result = [r for r in result + if not any(checksol(e, r, **flags) is False for e in exprs)] + + result = [r for r in result if r] + return linear, result + + +def solve_linear(lhs, rhs=0, symbols=[], exclude=[]): + r""" + Return a tuple derived from ``f = lhs - rhs`` that is one of + the following: ``(0, 1)``, ``(0, 0)``, ``(symbol, solution)``, ``(n, d)``. + + Explanation + =========== + + ``(0, 1)`` meaning that ``f`` is independent of the symbols in *symbols* + that are not in *exclude*. + + ``(0, 0)`` meaning that there is no solution to the equation amongst the + symbols given. If the first element of the tuple is not zero, then the + function is guaranteed to be dependent on a symbol in *symbols*. + + ``(symbol, solution)`` where symbol appears linearly in the numerator of + ``f``, is in *symbols* (if given), and is not in *exclude* (if given). No + simplification is done to ``f`` other than a ``mul=True`` expansion, so the + solution will correspond strictly to a unique solution. + + ``(n, d)`` where ``n`` and ``d`` are the numerator and denominator of ``f`` + when the numerator was not linear in any symbol of interest; ``n`` will + never be a symbol unless a solution for that symbol was found (in which case + the second element is the solution, not the denominator). + + Examples + ======== + + >>> from sympy import cancel, Pow + + ``f`` is independent of the symbols in *symbols* that are not in + *exclude*: + + >>> from sympy import cos, sin, solve_linear + >>> from sympy.abc import x, y, z + >>> eq = y*cos(x)**2 + y*sin(x)**2 - y # = y*(1 - 1) = 0 + >>> solve_linear(eq) + (0, 1) + >>> eq = cos(x)**2 + sin(x)**2 # = 1 + >>> solve_linear(eq) + (0, 1) + >>> solve_linear(x, exclude=[x]) + (0, 1) + + The variable ``x`` appears as a linear variable in each of the + following: + + >>> solve_linear(x + y**2) + (x, -y**2) + >>> solve_linear(1/x - y**2) + (x, y**(-2)) + + When not linear in ``x`` or ``y`` then the numerator and denominator are + returned: + + >>> solve_linear(x**2/y**2 - 3) + (x**2 - 3*y**2, y**2) + + If the numerator of the expression is a symbol, then ``(0, 0)`` is + returned if the solution for that symbol would have set any + denominator to 0: + + >>> eq = 1/(1/x - 2) + >>> eq.as_numer_denom() + (x, 1 - 2*x) + >>> solve_linear(eq) + (0, 0) + + But automatic rewriting may cause a symbol in the denominator to + appear in the numerator so a solution will be returned: + + >>> (1/x)**-1 + x + >>> solve_linear((1/x)**-1) + (x, 0) + + Use an unevaluated expression to avoid this: + + >>> solve_linear(Pow(1/x, -1, evaluate=False)) + (0, 0) + + If ``x`` is allowed to cancel in the following expression, then it + appears to be linear in ``x``, but this sort of cancellation is not + done by ``solve_linear`` so the solution will always satisfy the + original expression without causing a division by zero error. + + >>> eq = x**2*(1/x - z**2/x) + >>> solve_linear(cancel(eq)) + (x, 0) + >>> solve_linear(eq) + (x**2*(1 - z**2), x) + + A list of symbols for which a solution is desired may be given: + + >>> solve_linear(x + y + z, symbols=[y]) + (y, -x - z) + + A list of symbols to ignore may also be given: + + >>> solve_linear(x + y + z, exclude=[x]) + (y, -x - z) + + (A solution for ``y`` is obtained because it is the first variable + from the canonically sorted list of symbols that had a linear + solution.) + + """ + if isinstance(lhs, Eq): + if rhs: + raise ValueError(filldedent(''' + If lhs is an Equality, rhs must be 0 but was %s''' % rhs)) + rhs = lhs.rhs + lhs = lhs.lhs + dens = None + eq = lhs - rhs + n, d = eq.as_numer_denom() + if not n: + return S.Zero, S.One + + free = n.free_symbols + if not symbols: + symbols = free + else: + bad = [s for s in symbols if not s.is_Symbol] + if bad: + if len(bad) == 1: + bad = bad[0] + if len(symbols) == 1: + eg = 'solve(%s, %s)' % (eq, symbols[0]) + else: + eg = 'solve(%s, *%s)' % (eq, list(symbols)) + raise ValueError(filldedent(''' + solve_linear only handles symbols, not %s. To isolate + non-symbols use solve, e.g. >>> %s <<<. + ''' % (bad, eg))) + symbols = free.intersection(symbols) + symbols = symbols.difference(exclude) + if not symbols: + return S.Zero, S.One + + # derivatives are easy to do but tricky to analyze to see if they + # are going to disallow a linear solution, so for simplicity we + # just evaluate the ones that have the symbols of interest + derivs = defaultdict(list) + for der in n.atoms(Derivative): + csym = der.free_symbols & symbols + for c in csym: + derivs[c].append(der) + + all_zero = True + for xi in sorted(symbols, key=default_sort_key): # canonical order + # if there are derivatives in this var, calculate them now + if isinstance(derivs[xi], list): + derivs[xi] = {der: der.doit() for der in derivs[xi]} + newn = n.subs(derivs[xi]) + dnewn_dxi = newn.diff(xi) + # dnewn_dxi can be nonzero if it survives differentation by any + # of its free symbols + free = dnewn_dxi.free_symbols + if dnewn_dxi and (not free or any(dnewn_dxi.diff(s) for s in free) or free == symbols): + all_zero = False + if dnewn_dxi is S.NaN: + break + if xi not in dnewn_dxi.free_symbols: + vi = -1/dnewn_dxi*(newn.subs(xi, 0)) + if dens is None: + dens = _simple_dens(eq, symbols) + if not any(checksol(di, {xi: vi}, minimal=True) is True + for di in dens): + # simplify any trivial integral + irep = [(i, i.doit()) for i in vi.atoms(Integral) if + i.function.is_number] + # do a slight bit of simplification + vi = expand_mul(vi.subs(irep)) + return xi, vi + if all_zero: + return S.Zero, S.One + if n.is_Symbol: # no solution for this symbol was found + return S.Zero, S.Zero + return n, d + + +def minsolve_linear_system(system, *symbols, **flags): + r""" + Find a particular solution to a linear system. + + Explanation + =========== + + In particular, try to find a solution with the minimal possible number + of non-zero variables using a naive algorithm with exponential complexity. + If ``quick=True``, a heuristic is used. + + """ + quick = flags.get('quick', False) + # Check if there are any non-zero solutions at all + s0 = solve_linear_system(system, *symbols, **flags) + if not s0 or all(v == 0 for v in s0.values()): + return s0 + if quick: + # We just solve the system and try to heuristically find a nice + # solution. + s = solve_linear_system(system, *symbols) + def update(determined, solution): + delete = [] + for k, v in solution.items(): + solution[k] = v.subs(determined) + if not solution[k].free_symbols: + delete.append(k) + determined[k] = solution[k] + for k in delete: + del solution[k] + determined = {} + update(determined, s) + while s: + # NOTE sort by default_sort_key to get deterministic result + k = max((k for k in s.values()), + key=lambda x: (len(x.free_symbols), default_sort_key(x))) + kfree = k.free_symbols + x = next(reversed(list(ordered(kfree)))) + if len(kfree) != 1: + determined[x] = S.Zero + else: + val = _vsolve(k, x, check=False)[0] + if not val and not any(v.subs(x, val) for v in s.values()): + determined[x] = S.One + else: + determined[x] = val + update(determined, s) + return determined + else: + # We try to select n variables which we want to be non-zero. + # All others will be assumed zero. We try to solve the modified system. + # If there is a non-trivial solution, just set the free variables to + # one. If we do this for increasing n, trying all combinations of + # variables, we will find an optimal solution. + # We speed up slightly by starting at one less than the number of + # variables the quick method manages. + N = len(symbols) + bestsol = minsolve_linear_system(system, *symbols, quick=True) + n0 = len([x for x in bestsol.values() if x != 0]) + for n in range(n0 - 1, 1, -1): + debugf('minsolve: %s', n) + thissol = None + for nonzeros in combinations(range(N), n): + subm = Matrix([system.col(i).T for i in nonzeros] + [system.col(-1).T]).T + s = solve_linear_system(subm, *[symbols[i] for i in nonzeros]) + if s and not all(v == 0 for v in s.values()): + subs = [(symbols[v], S.One) for v in nonzeros] + for k, v in s.items(): + s[k] = v.subs(subs) + for sym in symbols: + if sym not in s: + if symbols.index(sym) in nonzeros: + s[sym] = S.One + else: + s[sym] = S.Zero + thissol = s + break + if thissol is None: + break + bestsol = thissol + return bestsol + + +def solve_linear_system(system, *symbols, **flags): + r""" + Solve system of $N$ linear equations with $M$ variables, which means + both under- and overdetermined systems are supported. + + Explanation + =========== + + The possible number of solutions is zero, one, or infinite. Respectively, + this procedure will return None or a dictionary with solutions. In the + case of underdetermined systems, all arbitrary parameters are skipped. + This may cause a situation in which an empty dictionary is returned. + In that case, all symbols can be assigned arbitrary values. + + Input to this function is a $N\times M + 1$ matrix, which means it has + to be in augmented form. If you prefer to enter $N$ equations and $M$ + unknowns then use ``solve(Neqs, *Msymbols)`` instead. Note: a local + copy of the matrix is made by this routine so the matrix that is + passed will not be modified. + + The algorithm used here is fraction-free Gaussian elimination, + which results, after elimination, in an upper-triangular matrix. + Then solutions are found using back-substitution. This approach + is more efficient and compact than the Gauss-Jordan method. + + Examples + ======== + + >>> from sympy import Matrix, solve_linear_system + >>> from sympy.abc import x, y + + Solve the following system:: + + x + 4 y == 2 + -2 x + y == 14 + + >>> system = Matrix(( (1, 4, 2), (-2, 1, 14))) + >>> solve_linear_system(system, x, y) + {x: -6, y: 2} + + A degenerate system returns an empty dictionary: + + >>> system = Matrix(( (0,0,0), (0,0,0) )) + >>> solve_linear_system(system, x, y) + {} + + """ + assert system.shape[1] == len(symbols) + 1 + + # This is just a wrapper for solve_lin_sys + eqs = list(system * Matrix(symbols + (-1,))) + eqs, ring = sympy_eqs_to_ring(eqs, symbols) + sol = solve_lin_sys(eqs, ring, _raw=False) + if sol is not None: + sol = {sym:val for sym, val in sol.items() if sym != val} + return sol + + +def solve_undetermined_coeffs(equ, coeffs, *syms, **flags): + r""" + Solve a system of equations in $k$ parameters that is formed by + matching coefficients in variables ``coeffs`` that are on + factors dependent on the remaining variables (or those given + explicitly by ``syms``. + + Explanation + =========== + + The result of this function is a dictionary with symbolic values of those + parameters with respect to coefficients in $q$ -- empty if there + is no solution or coefficients do not appear in the equation -- else + None (if the system was not recognized). If there is more than one + solution, the solutions are passed as a list. The output can be modified using + the same semantics as for `solve` since the flags that are passed are sent + directly to `solve` so, for example the flag ``dict=True`` will always return a list + of solutions as dictionaries. + + This function accepts both Equality and Expr class instances. + The solving process is most efficient when symbols are specified + in addition to parameters to be determined, but an attempt to + determine them (if absent) will be made. If an expected solution is not + obtained (and symbols were not specified) try specifying them. + + Examples + ======== + + >>> from sympy import Eq, solve_undetermined_coeffs + >>> from sympy.abc import a, b, c, h, p, k, x, y + + >>> solve_undetermined_coeffs(Eq(a*x + a + b, x/2), [a, b], x) + {a: 1/2, b: -1/2} + >>> solve_undetermined_coeffs(a - 2, [a]) + {a: 2} + + The equation can be nonlinear in the symbols: + + >>> X, Y, Z = y, x**y, y*x**y + >>> eq = a*X + b*Y + c*Z - X - 2*Y - 3*Z + >>> coeffs = a, b, c + >>> syms = x, y + >>> solve_undetermined_coeffs(eq, coeffs, syms) + {a: 1, b: 2, c: 3} + + And the system can be nonlinear in coefficients, too, but if + there is only a single solution, it will be returned as a + dictionary: + + >>> eq = a*x**2 + b*x + c - ((x - h)**2 + 4*p*k)/4/p + >>> solve_undetermined_coeffs(eq, (h, p, k), x) + {h: -b/(2*a), k: (4*a*c - b**2)/(4*a), p: 1/(4*a)} + + Multiple solutions are always returned in a list: + + >>> solve_undetermined_coeffs(a**2*x + b - x, [a, b], x) + [{a: -1, b: 0}, {a: 1, b: 0}] + + Using flag ``dict=True`` (in keeping with semantics in :func:`~.solve`) + will force the result to always be a list with any solutions + as elements in that list. + + >>> solve_undetermined_coeffs(a*x - 2*x, [a], dict=True) + [{a: 2}] + """ + if not (coeffs and all(i.is_Symbol for i in coeffs)): + raise ValueError('must provide symbols for coeffs') + + if isinstance(equ, Eq): + eq = equ.lhs - equ.rhs + else: + eq = equ + + ceq = cancel(eq) + xeq = _mexpand(ceq.as_numer_denom()[0], recursive=True) + + free = xeq.free_symbols + coeffs = free & set(coeffs) + if not coeffs: + return ([], {}) if flags.get('set', None) else [] # solve(0, x) -> [] + + if not syms: + # e.g. A*exp(x) + B - (exp(x) + y) separated into parts that + # don't/do depend on coeffs gives + # -(exp(x) + y), A*exp(x) + B + # then see what symbols are common to both + # {x} = {x, A, B} - {x, y} + ind, dep = xeq.as_independent(*coeffs, as_Add=True) + dfree = dep.free_symbols + syms = dfree & ind.free_symbols + if not syms: + # but if the system looks like (a + b)*x + b - c + # then {} = {a, b, x} - c + # so calculate {x} = {a, b, x} - {a, b} + syms = dfree - set(coeffs) + if not syms: + syms = [Dummy()] + else: + if len(syms) == 1 and iterable(syms[0]): + syms = syms[0] + e, s, _ = recast_to_symbols([xeq], syms) + xeq = e[0] + syms = s + + # find the functional forms in which symbols appear + + gens = set(xeq.as_coefficients_dict(*syms).keys()) - {1} + cset = set(coeffs) + if any(g.has_xfree(cset) for g in gens): + return # a generator contained a coefficient symbol + + # make sure we are working with symbols for generators + + e, gens, _ = recast_to_symbols([xeq], list(gens)) + xeq = e[0] + + # collect coefficients in front of generators + + system = list(collect(xeq, gens, evaluate=False).values()) + + # get a solution + + soln = solve(system, coeffs, **flags) + + # unpack unless told otherwise if length is 1 + + settings = flags.get('dict', None) or flags.get('set', None) + if type(soln) is dict or settings or len(soln) != 1: + return soln + return soln[0] + + +def solve_linear_system_LU(matrix, syms): + """ + Solves the augmented matrix system using ``LUsolve`` and returns a + dictionary in which solutions are keyed to the symbols of *syms* as ordered. + + Explanation + =========== + + The matrix must be invertible. + + Examples + ======== + + >>> from sympy import Matrix, solve_linear_system_LU + >>> from sympy.abc import x, y, z + + >>> solve_linear_system_LU(Matrix([ + ... [1, 2, 0, 1], + ... [3, 2, 2, 1], + ... [2, 0, 0, 1]]), [x, y, z]) + {x: 1/2, y: 1/4, z: -1/2} + + See Also + ======== + + LUsolve + + """ + if matrix.rows != matrix.cols - 1: + raise ValueError("Rows should be equal to columns - 1") + A = matrix[:matrix.rows, :matrix.rows] + b = matrix[:, matrix.cols - 1:] + soln = A.LUsolve(b) + solutions = {} + for i in range(soln.rows): + solutions[syms[i]] = soln[i, 0] + return solutions + + +def det_perm(M): + """ + Return the determinant of *M* by using permutations to select factors. + + Explanation + =========== + + For sizes larger than 8 the number of permutations becomes prohibitively + large, or if there are no symbols in the matrix, it is better to use the + standard determinant routines (e.g., ``M.det()``.) + + See Also + ======== + + det_minor + det_quick + + """ + args = [] + s = True + n = M.rows + list_ = M.flat() + for perm in generate_bell(n): + fac = [] + idx = 0 + for j in perm: + fac.append(list_[idx + j]) + idx += n + term = Mul(*fac) # disaster with unevaluated Mul -- takes forever for n=7 + args.append(term if s else -term) + s = not s + return Add(*args) + + +def det_minor(M): + """ + Return the ``det(M)`` computed from minors without + introducing new nesting in products. + + See Also + ======== + + det_perm + det_quick + + """ + n = M.rows + if n == 2: + return M[0, 0]*M[1, 1] - M[1, 0]*M[0, 1] + else: + return sum((1, -1)[i % 2]*Add(*[M[0, i]*d for d in + Add.make_args(det_minor(M.minor_submatrix(0, i)))]) + if M[0, i] else S.Zero for i in range(n)) + + +def det_quick(M, method=None): + """ + Return ``det(M)`` assuming that either + there are lots of zeros or the size of the matrix + is small. If this assumption is not met, then the normal + Matrix.det function will be used with method = ``method``. + + See Also + ======== + + det_minor + det_perm + + """ + if any(i.has(Symbol) for i in M): + if M.rows < 8 and all(i.has(Symbol) for i in M): + return det_perm(M) + return det_minor(M) + else: + return M.det(method=method) if method else M.det() + + +def inv_quick(M): + """Return the inverse of ``M``, assuming that either + there are lots of zeros or the size of the matrix + is small. + """ + if not all(i.is_Number for i in M): + if not any(i.is_Number for i in M): + det = lambda _: det_perm(_) + else: + det = lambda _: det_minor(_) + else: + return M.inv() + n = M.rows + d = det(M) + if d == S.Zero: + raise NonInvertibleMatrixError("Matrix det == 0; not invertible") + ret = zeros(n) + s1 = -1 + for i in range(n): + s = s1 = -s1 + for j in range(n): + di = det(M.minor_submatrix(i, j)) + ret[j, i] = s*di/d + s = -s + return ret + + +# these are functions that have multiple inverse values per period +multi_inverses = { + sin: lambda x: (asin(x), S.Pi - asin(x)), + cos: lambda x: (acos(x), 2*S.Pi - acos(x)), +} + + +def _vsolve(e, s, **flags): + """return list of scalar values for the solution of e for symbol s""" + return [i[s] for i in _solve(e, s, **flags)] + + +def _tsolve(eq, sym, **flags): + """ + Helper for ``_solve`` that solves a transcendental equation with respect + to the given symbol. Various equations containing powers and logarithms, + can be solved. + + There is currently no guarantee that all solutions will be returned or + that a real solution will be favored over a complex one. + + Either a list of potential solutions will be returned or None will be + returned (in the case that no method was known to get a solution + for the equation). All other errors (like the inability to cast an + expression as a Poly) are unhandled. + + Examples + ======== + + >>> from sympy import log, ordered + >>> from sympy.solvers.solvers import _tsolve as tsolve + >>> from sympy.abc import x + + >>> list(ordered(tsolve(3**(2*x + 5) - 4, x))) + [-5/2 + log(2)/log(3), (-5*log(3)/2 + log(2) + I*pi)/log(3)] + + >>> tsolve(log(x) + 2*x, x) + [LambertW(2)/2] + + """ + if 'tsolve_saw' not in flags: + flags['tsolve_saw'] = [] + if eq in flags['tsolve_saw']: + return None + else: + flags['tsolve_saw'].append(eq) + + rhs, lhs = _invert(eq, sym) + + if lhs == sym: + return [rhs] + try: + if lhs.is_Add: + # it's time to try factoring; powdenest is used + # to try get powers in standard form for better factoring + f = factor(powdenest(lhs - rhs)) + if f.is_Mul: + return _vsolve(f, sym, **flags) + if rhs: + f = logcombine(lhs, force=flags.get('force', True)) + if f.count(log) != lhs.count(log): + if isinstance(f, log): + return _vsolve(f.args[0] - exp(rhs), sym, **flags) + return _tsolve(f - rhs, sym, **flags) + + elif lhs.is_Pow: + if lhs.exp.is_Integer: + if lhs - rhs != eq: + return _vsolve(lhs - rhs, sym, **flags) + + if sym not in lhs.exp.free_symbols: + return _vsolve(lhs.base - rhs**(1/lhs.exp), sym, **flags) + + # _tsolve calls this with Dummy before passing the actual number in. + if any(t.is_Dummy for t in rhs.free_symbols): + raise NotImplementedError # _tsolve will call here again... + + # a ** g(x) == 0 + if not rhs: + # f(x)**g(x) only has solutions where f(x) == 0 and g(x) != 0 at + # the same place + sol_base = _vsolve(lhs.base, sym, **flags) + return [s for s in sol_base if lhs.exp.subs(sym, s) != 0] # XXX use checksol here? + + # a ** g(x) == b + if not lhs.base.has(sym): + if lhs.base == 0: + return _vsolve(lhs.exp, sym, **flags) if rhs != 0 else [] + + # Gets most solutions... + if lhs.base == rhs.as_base_exp()[0]: + # handles case when bases are equal + sol = _vsolve(lhs.exp - rhs.as_base_exp()[1], sym, **flags) + else: + # handles cases when bases are not equal and exp + # may or may not be equal + f = exp(log(lhs.base)*lhs.exp) - exp(log(rhs)) + sol = _vsolve(f, sym, **flags) + + # Check for duplicate solutions + def equal(expr1, expr2): + _ = Dummy() + eq = checksol(expr1 - _, _, expr2) + if eq is None: + if nsimplify(expr1) != nsimplify(expr2): + return False + # they might be coincidentally the same + # so check more rigorously + eq = expr1.equals(expr2) # XXX expensive but necessary? + return eq + + # Guess a rational exponent + e_rat = nsimplify(log(abs(rhs))/log(abs(lhs.base))) + e_rat = simplify(posify(e_rat)[0]) + n, d = fraction(e_rat) + if expand(lhs.base**n - rhs**d) == 0: + sol = [s for s in sol if not equal(lhs.exp.subs(sym, s), e_rat)] + sol.extend(_vsolve(lhs.exp - e_rat, sym, **flags)) + + return list(set(sol)) + + # f(x) ** g(x) == c + else: + sol = [] + logform = lhs.exp*log(lhs.base) - log(rhs) + if logform != lhs - rhs: + try: + sol.extend(_vsolve(logform, sym, **flags)) + except NotImplementedError: + pass + + # Collect possible solutions and check with substitution later. + check = [] + if rhs == 1: + # f(x) ** g(x) = 1 -- g(x)=0 or f(x)=+-1 + check.extend(_vsolve(lhs.exp, sym, **flags)) + check.extend(_vsolve(lhs.base - 1, sym, **flags)) + check.extend(_vsolve(lhs.base + 1, sym, **flags)) + elif rhs.is_Rational: + for d in (i for i in divisors(abs(rhs.p)) if i != 1): + e, t = integer_log(rhs.p, d) + if not t: + continue # rhs.p != d**b + for s in divisors(abs(rhs.q)): + if s**e== rhs.q: + r = Rational(d, s) + check.extend(_vsolve(lhs.base - r, sym, **flags)) + check.extend(_vsolve(lhs.base + r, sym, **flags)) + check.extend(_vsolve(lhs.exp - e, sym, **flags)) + elif rhs.is_irrational: + b_l, e_l = lhs.base.as_base_exp() + n, d = (e_l*lhs.exp).as_numer_denom() + b, e = sqrtdenest(rhs).as_base_exp() + check = [sqrtdenest(i) for i in (_vsolve(lhs.base - b, sym, **flags))] + check.extend([sqrtdenest(i) for i in (_vsolve(lhs.exp - e, sym, **flags))]) + if e_l*d != 1: + check.extend(_vsolve(b_l**n - rhs**(e_l*d), sym, **flags)) + for s in check: + ok = checksol(eq, sym, s) + if ok is None: + ok = eq.subs(sym, s).equals(0) + if ok: + sol.append(s) + return list(set(sol)) + + elif lhs.is_Function and len(lhs.args) == 1: + if lhs.func in multi_inverses: + # sin(x) = 1/3 -> x - asin(1/3) & x - (pi - asin(1/3)) + soln = [] + for i in multi_inverses[type(lhs)](rhs): + soln.extend(_vsolve(lhs.args[0] - i, sym, **flags)) + return list(set(soln)) + elif lhs.func == LambertW: + return _vsolve(lhs.args[0] - rhs*exp(rhs), sym, **flags) + + rewrite = lhs.rewrite(exp) + rewrite = rebuild(rewrite) # avoid rewrites involving evaluate=False + if rewrite != lhs: + return _vsolve(rewrite - rhs, sym, **flags) + except NotImplementedError: + pass + + # maybe it is a lambert pattern + if flags.pop('bivariate', True): + # lambert forms may need some help being recognized, e.g. changing + # 2**(3*x) + x**3*log(2)**3 + 3*x**2*log(2)**2 + 3*x*log(2) + 1 + # to 2**(3*x) + (x*log(2) + 1)**3 + + # make generator in log have exponent of 1 + logs = eq.atoms(log) + spow = min( + {i.exp for j in logs for i in j.atoms(Pow) + if i.base == sym} or {1}) + if spow != 1: + p = sym**spow + u = Dummy('bivariate-cov') + ueq = eq.subs(p, u) + if not ueq.has_free(sym): + sol = _vsolve(ueq, u, **flags) + inv = _vsolve(p - u, sym) + return [i.subs(u, s) for i in inv for s in sol] + + g = _filtered_gens(eq.as_poly(), sym) + up_or_log = set() + for gi in g: + if isinstance(gi, (exp, log)) or (gi.is_Pow and gi.base == S.Exp1): + up_or_log.add(gi) + elif gi.is_Pow: + gisimp = powdenest(expand_power_exp(gi)) + if gisimp.is_Pow and sym in gisimp.exp.free_symbols: + up_or_log.add(gi) + eq_down = expand_log(expand_power_exp(eq)).subs( + dict(list(zip(up_or_log, [0]*len(up_or_log))))) + eq = expand_power_exp(factor(eq_down, deep=True) + (eq - eq_down)) + rhs, lhs = _invert(eq, sym) + if lhs.has(sym): + try: + poly = lhs.as_poly() + g = _filtered_gens(poly, sym) + _eq = lhs - rhs + sols = _solve_lambert(_eq, sym, g) + # use a simplified form if it satisfies eq + # and has fewer operations + for n, s in enumerate(sols): + ns = nsimplify(s) + if ns != s and ns.count_ops() <= s.count_ops(): + ok = checksol(_eq, sym, ns) + if ok is None: + ok = _eq.subs(sym, ns).equals(0) + if ok: + sols[n] = ns + return sols + except NotImplementedError: + # maybe it's a convoluted function + if len(g) == 2: + try: + gpu = bivariate_type(lhs - rhs, *g) + if gpu is None: + raise NotImplementedError + g, p, u = gpu + flags['bivariate'] = False + inversion = _tsolve(g - u, sym, **flags) + if inversion: + sol = _vsolve(p, u, **flags) + return list({i.subs(u, s) + for i in inversion for s in sol}) + except NotImplementedError: + pass + else: + pass + + if flags.pop('force', True): + flags['force'] = False + pos, reps = posify(lhs - rhs) + if rhs == S.ComplexInfinity: + return [] + for u, s in reps.items(): + if s == sym: + break + else: + u = sym + if pos.has(u): + try: + soln = _vsolve(pos, u, **flags) + return [s.subs(reps) for s in soln] + except NotImplementedError: + pass + else: + pass # here for coverage + + return # here for coverage + + +# TODO: option for calculating J numerically + +@conserve_mpmath_dps +def nsolve(*args, dict=False, **kwargs): + r""" + Solve a nonlinear equation system numerically: ``nsolve(f, [args,] x0, + modules=['mpmath'], **kwargs)``. + + Explanation + =========== + + ``f`` is a vector function of symbolic expressions representing the system. + *args* are the variables. If there is only one variable, this argument can + be omitted. ``x0`` is a starting vector close to a solution. + + Use the modules keyword to specify which modules should be used to + evaluate the function and the Jacobian matrix. Make sure to use a module + that supports matrices. For more information on the syntax, please see the + docstring of ``lambdify``. + + If the keyword arguments contain ``dict=True`` (default is False) ``nsolve`` + will return a list (perhaps empty) of solution mappings. This might be + especially useful if you want to use ``nsolve`` as a fallback to solve since + using the dict argument for both methods produces return values of + consistent type structure. Please note: to keep this consistent with + ``solve``, the solution will be returned in a list even though ``nsolve`` + (currently at least) only finds one solution at a time. + + Overdetermined systems are supported. + + Examples + ======== + + >>> from sympy import Symbol, nsolve + >>> import mpmath + >>> mpmath.mp.dps = 15 + >>> x1 = Symbol('x1') + >>> x2 = Symbol('x2') + >>> f1 = 3 * x1**2 - 2 * x2**2 - 1 + >>> f2 = x1**2 - 2 * x1 + x2**2 + 2 * x2 - 8 + >>> print(nsolve((f1, f2), (x1, x2), (-1, 1))) + Matrix([[-1.19287309935246], [1.27844411169911]]) + + For one-dimensional functions the syntax is simplified: + + >>> from sympy import sin, nsolve + >>> from sympy.abc import x + >>> nsolve(sin(x), x, 2) + 3.14159265358979 + >>> nsolve(sin(x), 2) + 3.14159265358979 + + To solve with higher precision than the default, use the prec argument: + + >>> from sympy import cos + >>> nsolve(cos(x) - x, 1) + 0.739085133215161 + >>> nsolve(cos(x) - x, 1, prec=50) + 0.73908513321516064165531208767387340401341175890076 + >>> cos(_) + 0.73908513321516064165531208767387340401341175890076 + + To solve for complex roots of real functions, a nonreal initial point + must be specified: + + >>> from sympy import I + >>> nsolve(x**2 + 2, I) + 1.4142135623731*I + + ``mpmath.findroot`` is used and you can find their more extensive + documentation, especially concerning keyword parameters and + available solvers. Note, however, that functions which are very + steep near the root, the verification of the solution may fail. In + this case you should use the flag ``verify=False`` and + independently verify the solution. + + >>> from sympy import cos, cosh + >>> f = cos(x)*cosh(x) - 1 + >>> nsolve(f, 3.14*100) + Traceback (most recent call last): + ... + ValueError: Could not find root within given tolerance. (1.39267e+230 > 2.1684e-19) + >>> ans = nsolve(f, 3.14*100, verify=False); ans + 312.588469032184 + >>> f.subs(x, ans).n(2) + 2.1e+121 + >>> (f/f.diff(x)).subs(x, ans).n(2) + 7.4e-15 + + One might safely skip the verification if bounds of the root are known + and a bisection method is used: + + >>> bounds = lambda i: (3.14*i, 3.14*(i + 1)) + >>> nsolve(f, bounds(100), solver='bisect', verify=False) + 315.730061685774 + + Alternatively, a function may be better behaved when the + denominator is ignored. Since this is not always the case, however, + the decision of what function to use is left to the discretion of + the user. + + >>> eq = x**2/(1 - x)/(1 - 2*x)**2 - 100 + >>> nsolve(eq, 0.46) + Traceback (most recent call last): + ... + ValueError: Could not find root within given tolerance. (10000 > 2.1684e-19) + Try another starting point or tweak arguments. + >>> nsolve(eq.as_numer_denom()[0], 0.46) + 0.46792545969349058 + + """ + # there are several other SymPy functions that use method= so + # guard against that here + if 'method' in kwargs: + raise ValueError(filldedent(''' + Keyword "method" should not be used in this context. When using + some mpmath solvers directly, the keyword "method" is + used, but when using nsolve (and findroot) the keyword to use is + "solver".''')) + + if 'prec' in kwargs: + import mpmath + mpmath.mp.dps = kwargs.pop('prec') + + # keyword argument to return result as a dictionary + as_dict = dict + from builtins import dict # to unhide the builtin + + # interpret arguments + if len(args) == 3: + f = args[0] + fargs = args[1] + x0 = args[2] + if iterable(fargs) and iterable(x0): + if len(x0) != len(fargs): + raise TypeError('nsolve expected exactly %i guess vectors, got %i' + % (len(fargs), len(x0))) + elif len(args) == 2: + f = args[0] + fargs = None + x0 = args[1] + if iterable(f): + raise TypeError('nsolve expected 3 arguments, got 2') + elif len(args) < 2: + raise TypeError('nsolve expected at least 2 arguments, got %i' + % len(args)) + else: + raise TypeError('nsolve expected at most 3 arguments, got %i' + % len(args)) + modules = kwargs.get('modules', ['mpmath']) + if iterable(f): + f = list(f) + for i, fi in enumerate(f): + if isinstance(fi, Eq): + f[i] = fi.lhs - fi.rhs + f = Matrix(f).T + if iterable(x0): + x0 = list(x0) + if not isinstance(f, Matrix): + # assume it's a SymPy expression + if isinstance(f, Eq): + f = f.lhs - f.rhs + elif f.is_Relational: + raise TypeError('nsolve cannot accept inequalities') + syms = f.free_symbols + if fargs is None: + fargs = syms.copy().pop() + if not (len(syms) == 1 and (fargs in syms or fargs[0] in syms)): + raise ValueError(filldedent(''' + expected a one-dimensional and numerical function''')) + + # the function is much better behaved if there is no denominator + # but sending the numerator is left to the user since sometimes + # the function is better behaved when the denominator is present + # e.g., issue 11768 + + f = lambdify(fargs, f, modules) + x = sympify(findroot(f, x0, **kwargs)) + if as_dict: + return [{fargs: x}] + return x + + if len(fargs) > f.cols: + raise NotImplementedError(filldedent(''' + need at least as many equations as variables''')) + verbose = kwargs.get('verbose', False) + if verbose: + print('f(x):') + print(f) + # derive Jacobian + J = f.jacobian(fargs) + if verbose: + print('J(x):') + print(J) + # create functions + f = lambdify(fargs, f.T, modules) + J = lambdify(fargs, J, modules) + # solve the system numerically + x = findroot(f, x0, J=J, **kwargs) + if as_dict: + return [dict(zip(fargs, [sympify(xi) for xi in x]))] + return Matrix(x) + + +def _invert(eq, *symbols, **kwargs): + """ + Return tuple (i, d) where ``i`` is independent of *symbols* and ``d`` + contains symbols. + + Explanation + =========== + + ``i`` and ``d`` are obtained after recursively using algebraic inversion + until an uninvertible ``d`` remains. If there are no free symbols then + ``d`` will be zero. Some (but not necessarily all) solutions to the + expression ``i - d`` will be related to the solutions of the original + expression. + + Examples + ======== + + >>> from sympy.solvers.solvers import _invert as invert + >>> from sympy import sqrt, cos + >>> from sympy.abc import x, y + >>> invert(x - 3) + (3, x) + >>> invert(3) + (3, 0) + >>> invert(2*cos(x) - 1) + (1/2, cos(x)) + >>> invert(sqrt(x) - 3) + (3, sqrt(x)) + >>> invert(sqrt(x) + y, x) + (-y, sqrt(x)) + >>> invert(sqrt(x) + y, y) + (-sqrt(x), y) + >>> invert(sqrt(x) + y, x, y) + (0, sqrt(x) + y) + + If there is more than one symbol in a power's base and the exponent + is not an Integer, then the principal root will be used for the + inversion: + + >>> invert(sqrt(x + y) - 2) + (4, x + y) + >>> invert(sqrt(x + y) + 2) # note +2 instead of -2 + (4, x + y) + + If the exponent is an Integer, setting ``integer_power`` to True + will force the principal root to be selected: + + >>> invert(x**2 - 4, integer_power=True) + (2, x) + + """ + eq = sympify(eq) + if eq.args: + # make sure we are working with flat eq + eq = eq.func(*eq.args) + free = eq.free_symbols + if not symbols: + symbols = free + if not free & set(symbols): + return eq, S.Zero + + dointpow = bool(kwargs.get('integer_power', False)) + + lhs = eq + rhs = S.Zero + while True: + was = lhs + while True: + indep, dep = lhs.as_independent(*symbols) + + # dep + indep == rhs + if lhs.is_Add: + # this indicates we have done it all + if indep.is_zero: + break + + lhs = dep + rhs -= indep + + # dep * indep == rhs + else: + # this indicates we have done it all + if indep is S.One: + break + + lhs = dep + rhs /= indep + + # collect like-terms in symbols + if lhs.is_Add: + terms = {} + for a in lhs.args: + i, d = a.as_independent(*symbols) + terms.setdefault(d, []).append(i) + if any(len(v) > 1 for v in terms.values()): + args = [] + for d, i in terms.items(): + if len(i) > 1: + args.append(Add(*i)*d) + else: + args.append(i[0]*d) + lhs = Add(*args) + + # if it's a two-term Add with rhs = 0 and two powers we can get the + # dependent terms together, e.g. 3*f(x) + 2*g(x) -> f(x)/g(x) = -2/3 + if lhs.is_Add and not rhs and len(lhs.args) == 2 and \ + not lhs.is_polynomial(*symbols): + a, b = ordered(lhs.args) + ai, ad = a.as_independent(*symbols) + bi, bd = b.as_independent(*symbols) + if any(_ispow(i) for i in (ad, bd)): + a_base, a_exp = ad.as_base_exp() + b_base, b_exp = bd.as_base_exp() + if a_base == b_base and a_exp.extract_additively(b_exp) is None: + # a = -b and exponents do not have canceling terms/factors + # e.g. if exponents were 3*x and x then the ratio would have + # an exponent of 2*x: one of the roots would be lost + rat = powsimp(powdenest(ad/bd)) + lhs = rat + rhs = -bi/ai + else: + rat = ad/bd + _lhs = powsimp(ad/bd) + if _lhs != rat: + lhs = _lhs + rhs = -bi/ai + elif ai == -bi: + if isinstance(ad, Function) and ad.func == bd.func: + if len(ad.args) == len(bd.args) == 1: + lhs = ad.args[0] - bd.args[0] + elif len(ad.args) == len(bd.args): + # should be able to solve + # f(x, y) - f(2 - x, 0) == 0 -> x == 1 + raise NotImplementedError( + 'equal function with more than 1 argument') + else: + raise ValueError( + 'function with different numbers of args') + + elif lhs.is_Mul and any(_ispow(a) for a in lhs.args): + lhs = powsimp(powdenest(lhs)) + + if lhs.is_Function: + if hasattr(lhs, 'inverse') and lhs.inverse() is not None and len(lhs.args) == 1: + # -1 + # f(x) = g -> x = f (g) + # + # /!\ inverse should not be defined if there are multiple values + # for the function -- these are handled in _tsolve + # + rhs = lhs.inverse()(rhs) + lhs = lhs.args[0] + elif isinstance(lhs, atan2): + y, x = lhs.args + lhs = 2*atan(y/(sqrt(x**2 + y**2) + x)) + elif lhs.func == rhs.func: + if len(lhs.args) == len(rhs.args) == 1: + lhs = lhs.args[0] + rhs = rhs.args[0] + elif len(lhs.args) == len(rhs.args): + # should be able to solve + # f(x, y) == f(2, 3) -> x == 2 + # f(x, x + y) == f(2, 3) -> x == 2 + raise NotImplementedError( + 'equal function with more than 1 argument') + else: + raise ValueError( + 'function with different numbers of args') + + + if rhs and lhs.is_Pow and lhs.exp.is_Integer and lhs.exp < 0: + lhs = 1/lhs + rhs = 1/rhs + + # base**a = b -> base = b**(1/a) if + # a is an Integer and dointpow=True (this gives real branch of root) + # a is not an Integer and the equation is multivariate and the + # base has more than 1 symbol in it + # The rationale for this is that right now the multi-system solvers + # doesn't try to resolve generators to see, for example, if the whole + # system is written in terms of sqrt(x + y) so it will just fail, so we + # do that step here. + if lhs.is_Pow and ( + lhs.exp.is_Integer and dointpow or not lhs.exp.is_Integer and + len(symbols) > 1 and len(lhs.base.free_symbols & set(symbols)) > 1): + rhs = rhs**(1/lhs.exp) + lhs = lhs.base + + if lhs == was: + break + return rhs, lhs + + +def unrad(eq, *syms, **flags): + """ + Remove radicals with symbolic arguments and return (eq, cov), + None, or raise an error. + + Explanation + =========== + + None is returned if there are no radicals to remove. + + NotImplementedError is raised if there are radicals and they cannot be + removed or if the relationship between the original symbols and the + change of variable needed to rewrite the system as a polynomial cannot + be solved. + + Otherwise the tuple, ``(eq, cov)``, is returned where: + + *eq*, ``cov`` + *eq* is an equation without radicals (in the symbol(s) of + interest) whose solutions are a superset of the solutions to the + original expression. *eq* might be rewritten in terms of a new + variable; the relationship to the original variables is given by + ``cov`` which is a list containing ``v`` and ``v**p - b`` where + ``p`` is the power needed to clear the radical and ``b`` is the + radical now expressed as a polynomial in the symbols of interest. + For example, for sqrt(2 - x) the tuple would be + ``(c, c**2 - 2 + x)``. The solutions of *eq* will contain + solutions to the original equation (if there are any). + + *syms* + An iterable of symbols which, if provided, will limit the focus of + radical removal: only radicals with one or more of the symbols of + interest will be cleared. All free symbols are used if *syms* is not + set. + + *flags* are used internally for communication during recursive calls. + Two options are also recognized: + + ``take``, when defined, is interpreted as a single-argument function + that returns True if a given Pow should be handled. + + Radicals can be removed from an expression if: + + * All bases of the radicals are the same; a change of variables is + done in this case. + * If all radicals appear in one term of the expression. + * There are only four terms with sqrt() factors or there are less than + four terms having sqrt() factors. + * There are only two terms with radicals. + + Examples + ======== + + >>> from sympy.solvers.solvers import unrad + >>> from sympy.abc import x + >>> from sympy import sqrt, Rational, root + + >>> unrad(sqrt(x)*x**Rational(1, 3) + 2) + (x**5 - 64, []) + >>> unrad(sqrt(x) + root(x + 1, 3)) + (-x**3 + x**2 + 2*x + 1, []) + >>> eq = sqrt(x) + root(x, 3) - 2 + >>> unrad(eq) + (_p**3 + _p**2 - 2, [_p, _p**6 - x]) + + """ + + uflags = {"check": False, "simplify": False} + + def _cov(p, e): + if cov: + # XXX - uncovered + oldp, olde = cov + if Poly(e, p).degree(p) in (1, 2): + cov[:] = [p, olde.subs(oldp, _vsolve(e, p, **uflags)[0])] + else: + raise NotImplementedError + else: + cov[:] = [p, e] + + def _canonical(eq, cov): + if cov: + # change symbol to vanilla so no solutions are eliminated + p, e = cov + rep = {p: Dummy(p.name)} + eq = eq.xreplace(rep) + cov = [p.xreplace(rep), e.xreplace(rep)] + + # remove constants and powers of factors since these don't change + # the location of the root; XXX should factor or factor_terms be used? + eq = factor_terms(_mexpand(eq.as_numer_denom()[0], recursive=True), clear=True) + if eq.is_Mul: + args = [] + for f in eq.args: + if f.is_number: + continue + if f.is_Pow: + args.append(f.base) + else: + args.append(f) + eq = Mul(*args) # leave as Mul for more efficient solving + + # make the sign canonical + margs = list(Mul.make_args(eq)) + changed = False + for i, m in enumerate(margs): + if m.could_extract_minus_sign(): + margs[i] = -m + changed = True + if changed: + eq = Mul(*margs, evaluate=False) + + return eq, cov + + def _Q(pow): + # return leading Rational of denominator of Pow's exponent + c = pow.as_base_exp()[1].as_coeff_Mul()[0] + if not c.is_Rational: + return S.One + return c.q + + # define the _take method that will determine whether a term is of interest + def _take(d): + # return True if coefficient of any factor's exponent's den is not 1 + for pow in Mul.make_args(d): + if not pow.is_Pow: + continue + if _Q(pow) == 1: + continue + if pow.free_symbols & syms: + return True + return False + _take = flags.setdefault('_take', _take) + + if isinstance(eq, Eq): + eq = eq.lhs - eq.rhs # XXX legacy Eq as Eqn support + elif not isinstance(eq, Expr): + return + + cov, nwas, rpt = [flags.setdefault(k, v) for k, v in + sorted({"cov": [], "n": None, "rpt": 0}.items())] + + # preconditioning + eq = powdenest(factor_terms(eq, radical=True, clear=True)) + eq = eq.as_numer_denom()[0] + eq = _mexpand(eq, recursive=True) + if eq.is_number: + return + + # see if there are radicals in symbols of interest + syms = set(syms) or eq.free_symbols # _take uses this + poly = eq.as_poly() + gens = [g for g in poly.gens if _take(g)] + if not gens: + return + + # recast poly in terms of eigen-gens + poly = eq.as_poly(*gens) + + # not a polynomial e.g. 1 + sqrt(x)*exp(sqrt(x)) with gen sqrt(x) + if poly is None: + return + + # - an exponent has a symbol of interest (don't handle) + if any(g.exp.has(*syms) for g in gens): + return + + def _rads_bases_lcm(poly): + # if all the bases are the same or all the radicals are in one + # term, `lcm` will be the lcm of the denominators of the + # exponents of the radicals + lcm = 1 + rads = set() + bases = set() + for g in poly.gens: + q = _Q(g) + if q != 1: + rads.add(g) + lcm = ilcm(lcm, q) + bases.add(g.base) + return rads, bases, lcm + rads, bases, lcm = _rads_bases_lcm(poly) + + covsym = Dummy('p', nonnegative=True) + + # only keep in syms symbols that actually appear in radicals; + # and update gens + newsyms = set() + for r in rads: + newsyms.update(syms & r.free_symbols) + if newsyms != syms: + syms = newsyms + # get terms together that have common generators + drad = dict(zip(rads, range(len(rads)))) + rterms = {(): []} + args = Add.make_args(poly.as_expr()) + for t in args: + if _take(t): + common = set(t.as_poly().gens).intersection(rads) + key = tuple(sorted([drad[i] for i in common])) + else: + key = () + rterms.setdefault(key, []).append(t) + others = Add(*rterms.pop(())) + rterms = [Add(*rterms[k]) for k in rterms.keys()] + + # the output will depend on the order terms are processed, so + # make it canonical quickly + rterms = list(reversed(list(ordered(rterms)))) + + ok = False # we don't have a solution yet + depth = sqrt_depth(eq) + + if len(rterms) == 1 and not (rterms[0].is_Add and lcm > 2): + eq = rterms[0]**lcm - ((-others)**lcm) + ok = True + else: + if len(rterms) == 1 and rterms[0].is_Add: + rterms = list(rterms[0].args) + if len(bases) == 1: + b = bases.pop() + if len(syms) > 1: + x = b.free_symbols + else: + x = syms + x = list(ordered(x))[0] + try: + inv = _vsolve(covsym**lcm - b, x, **uflags) + if not inv: + raise NotImplementedError + eq = poly.as_expr().subs(b, covsym**lcm).subs(x, inv[0]) + _cov(covsym, covsym**lcm - b) + return _canonical(eq, cov) + except NotImplementedError: + pass + + if len(rterms) == 2: + if not others: + eq = rterms[0]**lcm - (-rterms[1])**lcm + ok = True + elif not log(lcm, 2).is_Integer: + # the lcm-is-power-of-two case is handled below + r0, r1 = rterms + if flags.get('_reverse', False): + r1, r0 = r0, r1 + i0 = _rads0, _bases0, lcm0 = _rads_bases_lcm(r0.as_poly()) + i1 = _rads1, _bases1, lcm1 = _rads_bases_lcm(r1.as_poly()) + for reverse in range(2): + if reverse: + i0, i1 = i1, i0 + r0, r1 = r1, r0 + _rads1, _, lcm1 = i1 + _rads1 = Mul(*_rads1) + t1 = _rads1**lcm1 + c = covsym**lcm1 - t1 + for x in syms: + try: + sol = _vsolve(c, x, **uflags) + if not sol: + raise NotImplementedError + neweq = r0.subs(x, sol[0]) + covsym*r1/_rads1 + \ + others + tmp = unrad(neweq, covsym) + if tmp: + eq, newcov = tmp + if newcov: + newp, newc = newcov + _cov(newp, c.subs(covsym, + _vsolve(newc, covsym, **uflags)[0])) + else: + _cov(covsym, c) + else: + eq = neweq + _cov(covsym, c) + ok = True + break + except NotImplementedError: + if reverse: + raise NotImplementedError( + 'no successful change of variable found') + else: + pass + if ok: + break + elif len(rterms) == 3: + # two cube roots and another with order less than 5 + # (so an analytical solution can be found) or a base + # that matches one of the cube root bases + info = [_rads_bases_lcm(i.as_poly()) for i in rterms] + RAD = 0 + BASES = 1 + LCM = 2 + if info[0][LCM] != 3: + info.append(info.pop(0)) + rterms.append(rterms.pop(0)) + elif info[1][LCM] != 3: + info.append(info.pop(1)) + rterms.append(rterms.pop(1)) + if info[0][LCM] == info[1][LCM] == 3: + if info[1][BASES] != info[2][BASES]: + info[0], info[1] = info[1], info[0] + rterms[0], rterms[1] = rterms[1], rterms[0] + if info[1][BASES] == info[2][BASES]: + eq = rterms[0]**3 + (rterms[1] + rterms[2] + others)**3 + ok = True + elif info[2][LCM] < 5: + # a*root(A, 3) + b*root(B, 3) + others = c + a, b, c, d, A, B = [Dummy(i) for i in 'abcdAB'] + # zz represents the unraded expression into which the + # specifics for this case are substituted + zz = (c - d)*(A**3*a**9 + 3*A**2*B*a**6*b**3 - + 3*A**2*a**6*c**3 + 9*A**2*a**6*c**2*d - 9*A**2*a**6*c*d**2 + + 3*A**2*a**6*d**3 + 3*A*B**2*a**3*b**6 + 21*A*B*a**3*b**3*c**3 - + 63*A*B*a**3*b**3*c**2*d + 63*A*B*a**3*b**3*c*d**2 - + 21*A*B*a**3*b**3*d**3 + 3*A*a**3*c**6 - 18*A*a**3*c**5*d + + 45*A*a**3*c**4*d**2 - 60*A*a**3*c**3*d**3 + 45*A*a**3*c**2*d**4 - + 18*A*a**3*c*d**5 + 3*A*a**3*d**6 + B**3*b**9 - 3*B**2*b**6*c**3 + + 9*B**2*b**6*c**2*d - 9*B**2*b**6*c*d**2 + 3*B**2*b**6*d**3 + + 3*B*b**3*c**6 - 18*B*b**3*c**5*d + 45*B*b**3*c**4*d**2 - + 60*B*b**3*c**3*d**3 + 45*B*b**3*c**2*d**4 - 18*B*b**3*c*d**5 + + 3*B*b**3*d**6 - c**9 + 9*c**8*d - 36*c**7*d**2 + 84*c**6*d**3 - + 126*c**5*d**4 + 126*c**4*d**5 - 84*c**3*d**6 + 36*c**2*d**7 - + 9*c*d**8 + d**9) + def _t(i): + b = Mul(*info[i][RAD]) + return cancel(rterms[i]/b), Mul(*info[i][BASES]) + aa, AA = _t(0) + bb, BB = _t(1) + cc = -rterms[2] + dd = others + eq = zz.xreplace(dict(zip( + (a, A, b, B, c, d), + (aa, AA, bb, BB, cc, dd)))) + ok = True + # handle power-of-2 cases + if not ok: + if log(lcm, 2).is_Integer and (not others and + len(rterms) == 4 or len(rterms) < 4): + def _norm2(a, b): + return a**2 + b**2 + 2*a*b + + if len(rterms) == 4: + # (r0+r1)**2 - (r2+r3)**2 + r0, r1, r2, r3 = rterms + eq = _norm2(r0, r1) - _norm2(r2, r3) + ok = True + elif len(rterms) == 3: + # (r1+r2)**2 - (r0+others)**2 + r0, r1, r2 = rterms + eq = _norm2(r1, r2) - _norm2(r0, others) + ok = True + elif len(rterms) == 2: + # r0**2 - (r1+others)**2 + r0, r1 = rterms + eq = r0**2 - _norm2(r1, others) + ok = True + + new_depth = sqrt_depth(eq) if ok else depth + rpt += 1 # XXX how many repeats with others unchanging is enough? + if not ok or ( + nwas is not None and len(rterms) == nwas and + new_depth is not None and new_depth == depth and + rpt > 3): + raise NotImplementedError('Cannot remove all radicals') + + flags.update({"cov": cov, "n": len(rterms), "rpt": rpt}) + neq = unrad(eq, *syms, **flags) + if neq: + eq, cov = neq + eq, cov = _canonical(eq, cov) + return eq, cov + + +# delayed imports +from sympy.solvers.bivariate import ( + bivariate_type, _solve_lambert, _filtered_gens) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/solveset.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/solveset.py new file mode 100644 index 0000000000000000000000000000000000000000..0ae242d9c8c4c0d1c1c46cd968a0c5e547ff0f66 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/solveset.py @@ -0,0 +1,4131 @@ +""" +This module contains functions to: + + - solve a single equation for a single variable, in any domain either real or complex. + + - solve a single transcendental equation for a single variable in any domain either real or complex. + (currently supports solving in real domain only) + + - solve a system of linear equations with N variables and M equations. + + - solve a system of Non Linear Equations with N variables and M equations +""" +from sympy.core.sympify import sympify +from sympy.core import (S, Pow, Dummy, pi, Expr, Wild, Mul, + Add, Basic) +from sympy.core.containers import Tuple +from sympy.core.function import (Lambda, expand_complex, AppliedUndef, + expand_log, _mexpand, expand_trig, nfloat) +from sympy.core.mod import Mod +from sympy.core.numbers import I, Number, Rational, oo +from sympy.core.intfunc import integer_log +from sympy.core.relational import Eq, Ne, Relational +from sympy.core.sorting import default_sort_key, ordered +from sympy.core.symbol import Symbol, _uniquely_named_symbol +from sympy.core.sympify import _sympify +from sympy.core.traversal import preorder_traversal +from sympy.external.gmpy import gcd as number_gcd, lcm as number_lcm +from sympy.polys.matrices.linsolve import _linear_eq_to_dict +from sympy.polys.polyroots import UnsolvableFactorError +from sympy.simplify.simplify import simplify, fraction, trigsimp, nsimplify +from sympy.simplify import powdenest, logcombine +from sympy.functions import (log, tan, cot, sin, cos, sec, csc, exp, + acos, asin, atan, acot, acsc, asec, + piecewise_fold, Piecewise) +from sympy.functions.combinatorial.numbers import totient +from sympy.functions.elementary.complexes import Abs, arg, re, im +from sympy.functions.elementary.hyperbolic import (HyperbolicFunction, + sinh, cosh, tanh, coth, sech, csch, + asinh, acosh, atanh, acoth, asech, acsch) +from sympy.functions.elementary.miscellaneous import real_root +from sympy.functions.elementary.trigonometric import TrigonometricFunction +from sympy.logic.boolalg import And, BooleanTrue +from sympy.sets import (FiniteSet, imageset, Interval, Intersection, + Union, ConditionSet, ImageSet, Complement, Contains) +from sympy.sets.sets import Set, ProductSet +from sympy.matrices import zeros, Matrix, MatrixBase +from sympy.ntheory.factor_ import divisors +from sympy.ntheory.residue_ntheory import discrete_log, nthroot_mod +from sympy.polys import (roots, Poly, degree, together, PolynomialError, + RootOf, factor, lcm, gcd) +from sympy.polys.polyerrors import CoercionFailed +from sympy.polys.polytools import invert, groebner, poly +from sympy.polys.solvers import (sympy_eqs_to_ring, solve_lin_sys, + PolyNonlinearError) +from sympy.polys.matrices.linsolve import _linsolve +from sympy.solvers.solvers import (checksol, denoms, unrad, + _simple_dens, recast_to_symbols) +from sympy.solvers.polysys import solve_poly_system +from sympy.utilities import filldedent +from sympy.utilities.iterables import (numbered_symbols, has_dups, + is_sequence, iterable) +from sympy.calculus.util import periodicity, continuous_domain, function_range + + +from types import GeneratorType + + +class NonlinearError(ValueError): + """Raised when unexpectedly encountering nonlinear equations""" + pass + + +def _masked(f, *atoms): + """Return ``f``, with all objects given by ``atoms`` replaced with + Dummy symbols, ``d``, and the list of replacements, ``(d, e)``, + where ``e`` is an object of type given by ``atoms`` in which + any other instances of atoms have been recursively replaced with + Dummy symbols, too. The tuples are ordered so that if they are + applied in sequence, the origin ``f`` will be restored. + + Examples + ======== + + >>> from sympy import cos + >>> from sympy.abc import x + >>> from sympy.solvers.solveset import _masked + + >>> f = cos(cos(x) + 1) + >>> f, reps = _masked(cos(1 + cos(x)), cos) + >>> f + _a1 + >>> reps + [(_a1, cos(_a0 + 1)), (_a0, cos(x))] + >>> for d, e in reps: + ... f = f.xreplace({d: e}) + >>> f + cos(cos(x) + 1) + """ + sym = numbered_symbols('a', cls=Dummy, real=True) + mask = [] + for a in ordered(f.atoms(*atoms)): + for i in mask: + a = a.replace(*i) + mask.append((a, next(sym))) + for i, (o, n) in enumerate(mask): + f = f.replace(o, n) + mask[i] = (n, o) + mask = list(reversed(mask)) + return f, mask + + +def _invert(f_x, y, x, domain=S.Complexes): + r""" + Reduce the complex valued equation $f(x) = y$ to a set of equations + + $$\left\{g(x) = h_1(y),\ g(x) = h_2(y),\ \dots,\ g(x) = h_n(y) \right\}$$ + + where $g(x)$ is a simpler function than $f(x)$. The return value is a tuple + $(g(x), \mathrm{set}_h)$, where $g(x)$ is a function of $x$ and $\mathrm{set}_h$ is + the set of function $\left\{h_1(y), h_2(y), \dots, h_n(y)\right\}$. + Here, $y$ is not necessarily a symbol. + + $\mathrm{set}_h$ contains the functions, along with the information + about the domain in which they are valid, through set + operations. For instance, if :math:`y = |x| - n` is inverted + in the real domain, then $\mathrm{set}_h$ is not simply + $\{-n, n\}$ as the nature of `n` is unknown; rather, it is: + + $$ \left(\left[0, \infty\right) \cap \left\{n\right\}\right) \cup + \left(\left(-\infty, 0\right] \cap \left\{- n\right\}\right)$$ + + By default, the complex domain is used which means that inverting even + seemingly simple functions like $\exp(x)$ will give very different + results from those obtained in the real domain. + (In the case of $\exp(x)$, the inversion via $\log$ is multi-valued + in the complex domain, having infinitely many branches.) + + If you are working with real values only (or you are not sure which + function to use) you should probably set the domain to + ``S.Reals`` (or use ``invert_real`` which does that automatically). + + + Examples + ======== + + >>> from sympy.solvers.solveset import invert_complex, invert_real + >>> from sympy.abc import x, y + >>> from sympy import exp + + When does exp(x) == y? + + >>> invert_complex(exp(x), y, x) + (x, ImageSet(Lambda(_n, I*(2*_n*pi + arg(y)) + log(Abs(y))), Integers)) + >>> invert_real(exp(x), y, x) + (x, Intersection({log(y)}, Reals)) + + When does exp(x) == 1? + + >>> invert_complex(exp(x), 1, x) + (x, ImageSet(Lambda(_n, 2*_n*I*pi), Integers)) + >>> invert_real(exp(x), 1, x) + (x, {0}) + + See Also + ======== + invert_real, invert_complex + """ + x = sympify(x) + if not x.is_Symbol: + raise ValueError("x must be a symbol") + f_x = sympify(f_x) + if x not in f_x.free_symbols: + raise ValueError("Inverse of constant function doesn't exist") + y = sympify(y) + if x in y.free_symbols: + raise ValueError("y should be independent of x ") + + if domain.is_subset(S.Reals): + x1, s = _invert_real(f_x, FiniteSet(y), x) + else: + x1, s = _invert_complex(f_x, FiniteSet(y), x) + + # f couldn't be inverted completely; return unmodified. + if x1 != x: + return x1, s + + # Avoid adding gratuitous intersections with S.Complexes. Actual + # conditions should be handled by the respective inverters. + if domain is S.Complexes: + return x1, s + + if isinstance(s, FiniteSet): + return x1, s.intersect(domain) + + # "Fancier" solution sets like those obtained by inversion of trigonometric + # functions already include general validity conditions (i.e. conditions on + # the domain of the respective inverse functions), so we should avoid adding + # blanket intersections with S.Reals. But subsets of R (or C) must still be + # accounted for. + if domain is S.Reals: + return x1, s + else: + return x1, s.intersect(domain) + + +invert_complex = _invert + + +def invert_real(f_x, y, x): + """ + Inverts a real-valued function. Same as :func:`invert_complex`, but sets + the domain to ``S.Reals`` before inverting. + """ + return _invert(f_x, y, x, S.Reals) + + +def _invert_real(f, g_ys, symbol): + """Helper function for _invert.""" + + if f == symbol or g_ys is S.EmptySet: + return (symbol, g_ys) + + n = Dummy('n', real=True) + + if isinstance(f, exp) or (f.is_Pow and f.base == S.Exp1): + return _invert_real(f.exp, + imageset(Lambda(n, log(n)), g_ys), + symbol) + + if hasattr(f, 'inverse') and f.inverse() is not None and not isinstance(f, ( + TrigonometricFunction, + HyperbolicFunction, + )): + if len(f.args) > 1: + raise ValueError("Only functions with one argument are supported.") + return _invert_real(f.args[0], + imageset(Lambda(n, f.inverse()(n)), g_ys), + symbol) + + if isinstance(f, Abs): + return _invert_abs(f.args[0], g_ys, symbol) + + if f.is_Add: + # f = g + h + g, h = f.as_independent(symbol) + if g is not S.Zero: + return _invert_real(h, imageset(Lambda(n, n - g), g_ys), symbol) + + if f.is_Mul: + # f = g*h + g, h = f.as_independent(symbol) + + if g is not S.One: + return _invert_real(h, imageset(Lambda(n, n/g), g_ys), symbol) + + if f.is_Pow: + base, expo = f.args + base_has_sym = base.has(symbol) + expo_has_sym = expo.has(symbol) + + if not expo_has_sym: + + if expo.is_rational: + num, den = expo.as_numer_denom() + + if den % 2 == 0 and num % 2 == 1 and den.is_zero is False: + # Here we have f(x)**(num/den) = y + # where den is nonzero and even and y is an element + # of the set g_ys. + # den is even, so we are only interested in the cases + # where both f(x) and y are positive. + # Restricting y to be positive (using the set g_ys_pos) + # means that y**(den/num) is always positive. + # Therefore it isn't necessary to also constrain f(x) + # to be positive because we are only going to + # find solutions of f(x) = y**(d/n) + # where the rhs is already required to be positive. + root = Lambda(n, real_root(n, expo)) + g_ys_pos = g_ys & Interval(0, oo) + res = imageset(root, g_ys_pos) + _inv, _set = _invert_real(base, res, symbol) + return (_inv, _set) + + if den % 2 == 1: + root = Lambda(n, real_root(n, expo)) + res = imageset(root, g_ys) + if num % 2 == 0: + neg_res = imageset(Lambda(n, -n), res) + return _invert_real(base, res + neg_res, symbol) + if num % 2 == 1: + return _invert_real(base, res, symbol) + + elif expo.is_irrational: + root = Lambda(n, real_root(n, expo)) + g_ys_pos = g_ys & Interval(0, oo) + res = imageset(root, g_ys_pos) + return _invert_real(base, res, symbol) + + else: + # indeterminate exponent, e.g. Float or parity of + # num, den of rational could not be determined + pass # use default return + + if not base_has_sym: + rhs = g_ys.args[0] + if base.is_positive: + return _invert_real(expo, + imageset(Lambda(n, log(n, base, evaluate=False)), g_ys), symbol) + elif base.is_negative: + s, b = integer_log(rhs, base) + if b: + return _invert_real(expo, FiniteSet(s), symbol) + else: + return (expo, S.EmptySet) + elif base.is_zero: + one = Eq(rhs, 1) + if one == S.true: + # special case: 0**x - 1 + return _invert_real(expo, FiniteSet(0), symbol) + elif one == S.false: + return (expo, S.EmptySet) + + if isinstance(f, (TrigonometricFunction, HyperbolicFunction)): + return _invert_trig_hyp_real(f, g_ys, symbol) + + return (f, g_ys) + + +# Dictionaries of inverses will be cached after first use. +_trig_inverses = None +_hyp_inverses = None + +def _invert_trig_hyp_real(f, g_ys, symbol): + """Helper function for inverting trigonometric and hyperbolic functions. + + This helper only handles inversion over the reals. + + For trigonometric functions only finite `g_ys` sets are implemented. + + For hyperbolic functions the set `g_ys` is checked against the domain of the + respective inverse functions. Infinite `g_ys` sets are also supported. + """ + + if isinstance(f, HyperbolicFunction): + n = Dummy('n', real=True) + + if isinstance(f, sinh): + # asinh is defined over R. + return _invert_real(f.args[0], imageset(n, asinh(n), g_ys), symbol) + + if isinstance(f, cosh): + g_ys_dom = g_ys.intersect(Interval(1, oo)) + if isinstance(g_ys_dom, Intersection): + # could not properly resolve domain check + if isinstance(g_ys, FiniteSet): + # If g_ys is a `FiniteSet`` it should be sufficient to just + # let the calling `_invert_real()` add an intersection with + # `S.Reals` (or a subset `domain`) to ensure that only valid + # (real) solutions are returned. + # This avoids adding "too many" Intersections or + # ConditionSets in the returned set. + g_ys_dom = g_ys + else: + return (f, g_ys) + return _invert_real(f.args[0], Union( + imageset(n, acosh(n), g_ys_dom), + imageset(n, -acosh(n), g_ys_dom)), symbol) + + if isinstance(f, sech): + g_ys_dom = g_ys.intersect(Interval.Lopen(0, 1)) + if isinstance(g_ys_dom, Intersection): + if isinstance(g_ys, FiniteSet): + g_ys_dom = g_ys + else: + return (f, g_ys) + return _invert_real(f.args[0], Union( + imageset(n, asech(n), g_ys_dom), + imageset(n, -asech(n), g_ys_dom)), symbol) + + if isinstance(f, tanh): + g_ys_dom = g_ys.intersect(Interval.open(-1, 1)) + if isinstance(g_ys_dom, Intersection): + if isinstance(g_ys, FiniteSet): + g_ys_dom = g_ys + else: + return (f, g_ys) + return _invert_real(f.args[0], + imageset(n, atanh(n), g_ys_dom), symbol) + + if isinstance(f, coth): + g_ys_dom = g_ys - Interval(-1, 1) + if isinstance(g_ys_dom, Complement): + if isinstance(g_ys, FiniteSet): + g_ys_dom = g_ys + else: + return (f, g_ys) + return _invert_real(f.args[0], + imageset(n, acoth(n), g_ys_dom), symbol) + + if isinstance(f, csch): + g_ys_dom = g_ys - FiniteSet(0) + if isinstance(g_ys_dom, Complement): + if isinstance(g_ys, FiniteSet): + g_ys_dom = g_ys + else: + return (f, g_ys) + return _invert_real(f.args[0], + imageset(n, acsch(n), g_ys_dom), symbol) + + elif isinstance(f, TrigonometricFunction) and isinstance(g_ys, FiniteSet): + def _get_trig_inverses(func): + global _trig_inverses + if _trig_inverses is None: + _trig_inverses = { + sin : ((asin, lambda y: pi-asin(y)), 2*pi, Interval(-1, 1)), + cos : ((acos, lambda y: -acos(y)), 2*pi, Interval(-1, 1)), + tan : ((atan,), pi, S.Reals), + cot : ((acot,), pi, S.Reals), + sec : ((asec, lambda y: -asec(y)), 2*pi, + Union(Interval(-oo, -1), Interval(1, oo))), + csc : ((acsc, lambda y: pi-acsc(y)), 2*pi, + Union(Interval(-oo, -1), Interval(1, oo)))} + return _trig_inverses[func] + + invs, period, rng = _get_trig_inverses(f.func) + n = Dummy('n', integer=True) + def create_return_set(g): + # returns ConditionSet that will be part of the final (x, set) tuple + invsimg = Union(*[ + imageset(n, period*n + inv(g), S.Integers) for inv in invs]) + inv_f, inv_g_ys = _invert_real(f.args[0], invsimg, symbol) + if inv_f == symbol: # inversion successful + conds = rng.contains(g) + return ConditionSet(symbol, conds, inv_g_ys) + else: + return ConditionSet(symbol, Eq(f, g), S.Reals) + + retset = Union(*[create_return_set(g) for g in g_ys]) + return (symbol, retset) + + else: + return (f, g_ys) + + +def _invert_trig_hyp_complex(f, g_ys, symbol): + """Helper function for inverting trigonometric and hyperbolic functions. + + This helper only handles inversion over the complex numbers. + Only finite `g_ys` sets are implemented. + + Handling of singularities is only implemented for hyperbolic equations. + In case of a symbolic element g in g_ys a ConditionSet may be returned. + """ + + if isinstance(f, TrigonometricFunction) and isinstance(g_ys, FiniteSet): + def inv(trig): + if isinstance(trig, (sin, csc)): + F = asin if isinstance(trig, sin) else acsc + return ( + lambda a: 2*n*pi + F(a), + lambda a: 2*n*pi + pi - F(a)) + if isinstance(trig, (cos, sec)): + F = acos if isinstance(trig, cos) else asec + return ( + lambda a: 2*n*pi + F(a), + lambda a: 2*n*pi - F(a)) + if isinstance(trig, (tan, cot)): + return (lambda a: n*pi + trig.inverse()(a),) + + n = Dummy('n', integer=True) + invs = S.EmptySet + for L in inv(f): + invs += Union(*[imageset(Lambda(n, L(g)), S.Integers) for g in g_ys]) + return _invert_complex(f.args[0], invs, symbol) + + elif isinstance(f, HyperbolicFunction) and isinstance(g_ys, FiniteSet): + # There are two main options regarding singularities / domain checking + # for symbolic elements in g_ys: + # 1. Add a "catch-all" intersection with S.Complexes. + # 2. ConditionSets. + # At present ConditionSets seem to work better and have the additional + # benefit of representing the precise conditions that must be satisfied. + # The conditions are also rather straightforward. (At most two isolated + # points.) + def _get_hyp_inverses(func): + global _hyp_inverses + if _hyp_inverses is None: + _hyp_inverses = { + sinh : ((asinh, lambda y: I*pi-asinh(y)), 2*I*pi, ()), + cosh : ((acosh, lambda y: -acosh(y)), 2*I*pi, ()), + tanh : ((atanh,), I*pi, (-1, 1)), + coth : ((acoth,), I*pi, (-1, 1)), + sech : ((asech, lambda y: -asech(y)), 2*I*pi, (0, )), + csch : ((acsch, lambda y: I*pi-acsch(y)), 2*I*pi, (0, ))} + return _hyp_inverses[func] + + # invs: iterable of main inverses, e.g. (acosh, -acosh). + # excl: iterable of singularities to be checked for. + invs, period, excl = _get_hyp_inverses(f.func) + n = Dummy('n', integer=True) + def create_return_set(g): + # returns ConditionSet that will be part of the final (x, set) tuple + invsimg = Union(*[ + imageset(n, period*n + inv(g), S.Integers) for inv in invs]) + inv_f, inv_g_ys = _invert_complex(f.args[0], invsimg, symbol) + if inv_f == symbol: # inversion successful + conds = And(*[Ne(g, e) for e in excl]) + return ConditionSet(symbol, conds, inv_g_ys) + else: + return ConditionSet(symbol, Eq(f, g), S.Complexes) + + retset = Union(*[create_return_set(g) for g in g_ys]) + return (symbol, retset) + + else: + return (f, g_ys) + + +def _invert_complex(f, g_ys, symbol): + """Helper function for _invert.""" + + if f == symbol or g_ys is S.EmptySet: + return (symbol, g_ys) + + n = Dummy('n') + + if f.is_Add: + # f = g + h + g, h = f.as_independent(symbol) + if g is not S.Zero: + return _invert_complex(h, imageset(Lambda(n, n - g), g_ys), symbol) + + if f.is_Mul: + # f = g*h + g, h = f.as_independent(symbol) + + if g is not S.One: + if g in {S.NegativeInfinity, S.ComplexInfinity, S.Infinity}: + return (h, S.EmptySet) + return _invert_complex(h, imageset(Lambda(n, n/g), g_ys), symbol) + + if f.is_Pow: + base, expo = f.args + # special case: g**r = 0 + # Could be improved like `_invert_real` to handle more general cases. + if expo.is_Rational and g_ys == FiniteSet(0): + if expo.is_positive: + return _invert_complex(base, g_ys, symbol) + + if hasattr(f, 'inverse') and f.inverse() is not None and \ + not isinstance(f, TrigonometricFunction) and \ + not isinstance(f, HyperbolicFunction) and \ + not isinstance(f, exp): + if len(f.args) > 1: + raise ValueError("Only functions with one argument are supported.") + return _invert_complex(f.args[0], + imageset(Lambda(n, f.inverse()(n)), g_ys), symbol) + + if isinstance(f, exp) or (f.is_Pow and f.base == S.Exp1): + if isinstance(g_ys, ImageSet): + # can solve up to `(d*exp(exp(...(exp(a*x + b))...) + c)` format. + # Further can be improved to `(d*exp(exp(...(exp(a*x**n + b*x**(n-1) + ... + f))...) + c)`. + g_ys_expr = g_ys.lamda.expr + g_ys_vars = g_ys.lamda.variables + k = Dummy('k{}'.format(len(g_ys_vars))) + g_ys_vars_1 = (k,) + g_ys_vars + exp_invs = Union(*[imageset(Lambda((g_ys_vars_1,), (I*(2*k*pi + arg(g_ys_expr)) + + log(Abs(g_ys_expr)))), S.Integers**(len(g_ys_vars_1)))]) + return _invert_complex(f.exp, exp_invs, symbol) + + elif isinstance(g_ys, FiniteSet): + exp_invs = Union(*[imageset(Lambda(n, I*(2*n*pi + arg(g_y)) + + log(Abs(g_y))), S.Integers) + for g_y in g_ys if g_y != 0]) + return _invert_complex(f.exp, exp_invs, symbol) + + if isinstance(f, (TrigonometricFunction, HyperbolicFunction)): + return _invert_trig_hyp_complex(f, g_ys, symbol) + + return (f, g_ys) + + +def _invert_abs(f, g_ys, symbol): + """Helper function for inverting absolute value functions. + + Returns the complete result of inverting an absolute value + function along with the conditions which must also be satisfied. + + If it is certain that all these conditions are met, a :class:`~.FiniteSet` + of all possible solutions is returned. If any condition cannot be + satisfied, an :class:`~.EmptySet` is returned. Otherwise, a + :class:`~.ConditionSet` of the solutions, with all the required conditions + specified, is returned. + + """ + if not g_ys.is_FiniteSet: + # this could be used for FiniteSet, but the + # results are more compact if they aren't, e.g. + # ConditionSet(x, Contains(n, Interval(0, oo)), {-n, n}) vs + # Union(Intersection(Interval(0, oo), {n}), Intersection(Interval(-oo, 0), {-n})) + # for the solution of abs(x) - n + pos = Intersection(g_ys, Interval(0, S.Infinity)) + parg = _invert_real(f, pos, symbol) + narg = _invert_real(-f, pos, symbol) + if parg[0] != narg[0]: + raise NotImplementedError + return parg[0], Union(narg[1], parg[1]) + + # check conditions: all these must be true. If any are unknown + # then return them as conditions which must be satisfied + unknown = [] + for a in g_ys.args: + ok = a.is_nonnegative if a.is_Number else a.is_positive + if ok is None: + unknown.append(a) + elif not ok: + return symbol, S.EmptySet + if unknown: + conditions = And(*[Contains(i, Interval(0, oo)) + for i in unknown]) + else: + conditions = True + n = Dummy('n', real=True) + # this is slightly different than above: instead of solving + # +/-f on positive values, here we solve for f on +/- g_ys + g_x, values = _invert_real(f, Union( + imageset(Lambda(n, n), g_ys), + imageset(Lambda(n, -n), g_ys)), symbol) + return g_x, ConditionSet(g_x, conditions, values) + + +def domain_check(f, symbol, p): + """Returns False if point p is infinite or any subexpression of f + is infinite or becomes so after replacing symbol with p. If none of + these conditions is met then True will be returned. + + Examples + ======== + + >>> from sympy import Mul, oo + >>> from sympy.abc import x + >>> from sympy.solvers.solveset import domain_check + >>> g = 1/(1 + (1/(x + 1))**2) + >>> domain_check(g, x, -1) + False + >>> domain_check(x**2, x, 0) + True + >>> domain_check(1/x, x, oo) + False + + * The function relies on the assumption that the original form + of the equation has not been changed by automatic simplification. + + >>> domain_check(x/x, x, 0) # x/x is automatically simplified to 1 + True + + * To deal with automatic evaluations use evaluate=False: + + >>> domain_check(Mul(x, 1/x, evaluate=False), x, 0) + False + """ + f, p = sympify(f), sympify(p) + if p.is_infinite: + return False + return _domain_check(f, symbol, p) + + +def _domain_check(f, symbol, p): + # helper for domain check + if f.is_Atom and f.is_finite: + return True + elif f.subs(symbol, p).is_infinite: + return False + elif isinstance(f, Piecewise): + # Check the cases of the Piecewise in turn. There might be invalid + # expressions in later cases that don't apply e.g. + # solveset(Piecewise((0, Eq(x, 0)), (1/x, True)), x) + for expr, cond in f.args: + condsubs = cond.subs(symbol, p) + if condsubs is S.false: + continue + elif condsubs is S.true: + return _domain_check(expr, symbol, p) + else: + # We don't know which case of the Piecewise holds. On this + # basis we cannot decide whether any solution is in or out of + # the domain. Ideally this function would allow returning a + # symbolic condition for the validity of the solution that + # could be handled in the calling code. In the mean time we'll + # give this particular solution the benefit of the doubt and + # let it pass. + return True + else: + # TODO : We should not blindly recurse through all args of arbitrary expressions like this + return all(_domain_check(g, symbol, p) + for g in f.args) + + +def _is_finite_with_finite_vars(f, domain=S.Complexes): + """ + Return True if the given expression is finite. For symbols that + do not assign a value for `complex` and/or `real`, the domain will + be used to assign a value; symbols that do not assign a value + for `finite` will be made finite. All other assumptions are + left unmodified. + """ + def assumptions(s): + A = s.assumptions0 + A.setdefault('finite', A.get('finite', True)) + if domain.is_subset(S.Reals): + # if this gets set it will make complex=True, too + A.setdefault('real', True) + else: + # don't change 'real' because being complex implies + # nothing about being real + A.setdefault('complex', True) + return A + + reps = {s: Dummy(**assumptions(s)) for s in f.free_symbols} + return f.xreplace(reps).is_finite + + +def _is_function_class_equation(func_class, f, symbol): + """ Tests whether the equation is an equation of the given function class. + + The given equation belongs to the given function class if it is + comprised of functions of the function class which are multiplied by + or added to expressions independent of the symbol. In addition, the + arguments of all such functions must be linear in the symbol as well. + + Examples + ======== + + >>> from sympy.solvers.solveset import _is_function_class_equation + >>> from sympy import tan, sin, tanh, sinh, exp + >>> from sympy.abc import x + >>> from sympy.functions.elementary.trigonometric import TrigonometricFunction + >>> from sympy.functions.elementary.hyperbolic import HyperbolicFunction + >>> _is_function_class_equation(TrigonometricFunction, exp(x) + tan(x), x) + False + >>> _is_function_class_equation(TrigonometricFunction, tan(x) + sin(x), x) + True + >>> _is_function_class_equation(TrigonometricFunction, tan(x**2), x) + False + >>> _is_function_class_equation(TrigonometricFunction, tan(x + 2), x) + True + >>> _is_function_class_equation(HyperbolicFunction, tanh(x) + sinh(x), x) + True + """ + if f.is_Mul or f.is_Add: + return all(_is_function_class_equation(func_class, arg, symbol) + for arg in f.args) + + if f.is_Pow: + if not f.exp.has(symbol): + return _is_function_class_equation(func_class, f.base, symbol) + else: + return False + + if not f.has(symbol): + return True + + if isinstance(f, func_class): + try: + g = Poly(f.args[0], symbol) + return g.degree() <= 1 + except PolynomialError: + return False + else: + return False + + +def _solve_as_rational(f, symbol, domain): + """ solve rational functions""" + f = together(_mexpand(f, recursive=True), deep=True) + g, h = fraction(f) + if not h.has(symbol): + try: + return _solve_as_poly(g, symbol, domain) + except NotImplementedError: + # The polynomial formed from g could end up having + # coefficients in a ring over which finding roots + # isn't implemented yet, e.g. ZZ[a] for some symbol a + return ConditionSet(symbol, Eq(f, 0), domain) + except CoercionFailed: + # contained oo, zoo or nan + return S.EmptySet + else: + valid_solns = _solveset(g, symbol, domain) + invalid_solns = _solveset(h, symbol, domain) + return valid_solns - invalid_solns + + +class _SolveTrig1Error(Exception): + """Raised when _solve_trig1 heuristics do not apply""" + +def _solve_trig(f, symbol, domain): + """Function to call other helpers to solve trigonometric equations """ + # If f is composed of a single trig function (potentially appearing multiple + # times) we should solve by either inverting directly or inverting after a + # suitable change of variable. + # + # _solve_trig is currently only called by _solveset for trig/hyperbolic + # functions of an argument linear in x. Inverting a symbolic argument should + # include a guard against division by zero in order to have a result that is + # consistent with similar processing done by _solve_trig1. + # (Ideally _invert should add these conditions by itself.) + trig_expr, count = None, 0 + for expr in preorder_traversal(f): + if isinstance(expr, (TrigonometricFunction, + HyperbolicFunction)) and expr.has(symbol): + if not trig_expr: + trig_expr, count = expr, 1 + elif expr == trig_expr: + count += 1 + else: + trig_expr, count = False, 0 + break + if count == 1: + # direct inversion + x, sol = _invert(f, 0, symbol, domain) + if x == symbol: + cond = True + if trig_expr.free_symbols - {symbol}: + a, h = trig_expr.args[0].as_independent(symbol, as_Add=True) + m, h = h.as_independent(symbol, as_Add=False) + num, den = m.as_numer_denom() + cond = Ne(num, 0) & Ne(den, 0) + return ConditionSet(symbol, cond, sol) + else: + return ConditionSet(symbol, Eq(f, 0), domain) + elif count: + # solve by change of variable + y = Dummy('y') + f_cov = f.subs(trig_expr, y) + sol_cov = solveset(f_cov, y, domain) + if isinstance(sol_cov, FiniteSet): + return Union( + *[_solve_trig(trig_expr-s, symbol, domain) for s in sol_cov]) + + sol = None + try: + # multiple trig/hyp functions; solve by rewriting to exp + sol = _solve_trig1(f, symbol, domain) + except _SolveTrig1Error: + try: + # multiple trig/hyp functions; solve by rewriting to tan(x/2) + sol = _solve_trig2(f, symbol, domain) + except ValueError: + raise NotImplementedError(filldedent(''' + Solution to this kind of trigonometric equations + is yet to be implemented''')) + return sol + + +def _solve_trig1(f, symbol, domain): + """Primary solver for trigonometric and hyperbolic equations + + Returns either the solution set as a ConditionSet (auto-evaluated to a + union of ImageSets if no variables besides 'symbol' are involved) or + raises _SolveTrig1Error if f == 0 cannot be solved. + + Notes + ===== + Algorithm: + 1. Do a change of variable x -> mu*x in arguments to trigonometric and + hyperbolic functions, in order to reduce them to small integers. (This + step is crucial to keep the degrees of the polynomials of step 4 low.) + 2. Rewrite trigonometric/hyperbolic functions as exponentials. + 3. Proceed to a 2nd change of variable, replacing exp(I*x) or exp(x) by y. + 4. Solve the resulting rational equation. + 5. Use invert_complex or invert_real to return to the original variable. + 6. If the coefficients of 'symbol' were symbolic in nature, add the + necessary consistency conditions in a ConditionSet. + + """ + # Prepare change of variable + x = Dummy('x') + if _is_function_class_equation(HyperbolicFunction, f, symbol): + cov = exp(x) + inverter = invert_real if domain.is_subset(S.Reals) else invert_complex + else: + cov = exp(I*x) + inverter = invert_complex + + f = trigsimp(f) + f_original = f + trig_functions = f.atoms(TrigonometricFunction, HyperbolicFunction) + trig_arguments = [e.args[0] for e in trig_functions] + # trigsimp may have reduced the equation to an expression + # that is independent of 'symbol' (e.g. cos**2+sin**2) + if not any(a.has(symbol) for a in trig_arguments): + return solveset(f_original, symbol, domain) + + denominators = [] + numerators = [] + for ar in trig_arguments: + try: + poly_ar = Poly(ar, symbol) + except PolynomialError: + raise _SolveTrig1Error("trig argument is not a polynomial") + if poly_ar.degree() > 1: # degree >1 still bad + raise _SolveTrig1Error("degree of variable must not exceed one") + if poly_ar.degree() == 0: # degree 0, don't care + continue + c = poly_ar.all_coeffs()[0] # got the coefficient of 'symbol' + numerators.append(fraction(c)[0]) + denominators.append(fraction(c)[1]) + + mu = lcm(denominators)/gcd(numerators) + f = f.subs(symbol, mu*x) + f = f.rewrite(exp) + f = together(f) + g, h = fraction(f) + y = Dummy('y') + g, h = g.expand(), h.expand() + g, h = g.subs(cov, y), h.subs(cov, y) + if g.has(x) or h.has(x): + raise _SolveTrig1Error("change of variable not possible") + + solns = solveset_complex(g, y) - solveset_complex(h, y) + if isinstance(solns, ConditionSet): + raise _SolveTrig1Error("polynomial has ConditionSet solution") + + if isinstance(solns, FiniteSet): + if any(isinstance(s, RootOf) for s in solns): + raise _SolveTrig1Error("polynomial results in RootOf object") + # revert the change of variable + cov = cov.subs(x, symbol/mu) + result = Union(*[inverter(cov, s, symbol)[1] for s in solns]) + # In case of symbolic coefficients, the solution set is only valid + # if numerator and denominator of mu are non-zero. + if mu.has(Symbol): + syms = (mu).atoms(Symbol) + munum, muden = fraction(mu) + condnum = munum.as_independent(*syms, as_Add=False)[1] + condden = muden.as_independent(*syms, as_Add=False)[1] + cond = And(Ne(condnum, 0), Ne(condden, 0)) + else: + cond = True + # Actual conditions are returned as part of the ConditionSet. Adding an + # intersection with C would only complicate some solution sets due to + # current limitations of intersection code. (e.g. #19154) + if domain is S.Complexes: + # This is a slight abuse of ConditionSet. Ideally this should + # be some kind of "PiecewiseSet". (See #19507 discussion) + return ConditionSet(symbol, cond, result) + else: + return ConditionSet(symbol, cond, Intersection(result, domain)) + elif solns is S.EmptySet: + return S.EmptySet + else: + raise _SolveTrig1Error("polynomial solutions must form FiniteSet") + + +def _solve_trig2(f, symbol, domain): + """Secondary helper to solve trigonometric equations, + called when first helper fails """ + f = trigsimp(f) + f_original = f + trig_functions = f.atoms(sin, cos, tan, sec, cot, csc) + trig_arguments = [e.args[0] for e in trig_functions] + denominators = [] + numerators = [] + + # todo: This solver can be extended to hyperbolics if the + # analogous change of variable to tanh (instead of tan) + # is used. + if not trig_functions: + return ConditionSet(symbol, Eq(f_original, 0), domain) + + # todo: The pre-processing below (extraction of numerators, denominators, + # gcd, lcm, mu, etc.) should be updated to the enhanced version in + # _solve_trig1. (See #19507) + for ar in trig_arguments: + try: + poly_ar = Poly(ar, symbol) + except PolynomialError: + raise ValueError("give up, we cannot solve if this is not a polynomial in x") + if poly_ar.degree() > 1: # degree >1 still bad + raise ValueError("degree of variable inside polynomial should not exceed one") + if poly_ar.degree() == 0: # degree 0, don't care + continue + c = poly_ar.all_coeffs()[0] # got the coefficient of 'symbol' + try: + numerators.append(Rational(c).p) + denominators.append(Rational(c).q) + except TypeError: + return ConditionSet(symbol, Eq(f_original, 0), domain) + + x = Dummy('x') + + mu = Rational(2)*number_lcm(*denominators)/number_gcd(*numerators) + f = f.subs(symbol, mu*x) + f = f.rewrite(tan) + f = expand_trig(f) + f = together(f) + + g, h = fraction(f) + y = Dummy('y') + g, h = g.expand(), h.expand() + g, h = g.subs(tan(x), y), h.subs(tan(x), y) + + if g.has(x) or h.has(x): + return ConditionSet(symbol, Eq(f_original, 0), domain) + solns = solveset(g, y, S.Reals) - solveset(h, y, S.Reals) + + if isinstance(solns, FiniteSet): + result = Union(*[invert_real(tan(symbol/mu), s, symbol)[1] + for s in solns]) + dsol = invert_real(tan(symbol/mu), oo, symbol)[1] + if degree(h) > degree(g): # If degree(denom)>degree(num) then there + result = Union(result, dsol) # would be another sol at Lim(denom-->oo) + return Intersection(result, domain) + elif solns is S.EmptySet: + return S.EmptySet + else: + return ConditionSet(symbol, Eq(f_original, 0), S.Reals) + + +def _solve_as_poly(f, symbol, domain=S.Complexes): + """ + Solve the equation using polynomial techniques if it already is a + polynomial equation or, with a change of variables, can be made so. + """ + result = None + if f.is_polynomial(symbol): + solns = roots(f, symbol, cubics=True, quartics=True, + quintics=True, domain='EX') + num_roots = sum(solns.values()) + if degree(f, symbol) <= num_roots: + result = FiniteSet(*solns.keys()) + else: + poly = Poly(f, symbol) + solns = poly.all_roots() + if poly.degree() <= len(solns): + result = FiniteSet(*solns) + else: + result = ConditionSet(symbol, Eq(f, 0), domain) + else: + poly = Poly(f) + if poly is None: + result = ConditionSet(symbol, Eq(f, 0), domain) + gens = [g for g in poly.gens if g.has(symbol)] + + if len(gens) == 1: + poly = Poly(poly, gens[0]) + gen = poly.gen + deg = poly.degree() + poly = Poly(poly.as_expr(), poly.gen, composite=True) + poly_solns = FiniteSet(*roots(poly, cubics=True, quartics=True, + quintics=True).keys()) + + if len(poly_solns) < deg: + result = ConditionSet(symbol, Eq(f, 0), domain) + + if gen != symbol: + y = Dummy('y') + inverter = invert_real if domain.is_subset(S.Reals) else invert_complex + lhs, rhs_s = inverter(gen, y, symbol) + if lhs == symbol: + result = Union(*[rhs_s.subs(y, s) for s in poly_solns]) + if isinstance(result, FiniteSet) and isinstance(gen, Pow + ) and gen.base.is_Rational: + result = FiniteSet(*[expand_log(i) for i in result]) + else: + result = ConditionSet(symbol, Eq(f, 0), domain) + else: + result = ConditionSet(symbol, Eq(f, 0), domain) + + if result is not None: + if isinstance(result, FiniteSet): + # this is to simplify solutions like -sqrt(-I) to sqrt(2)/2 + # - sqrt(2)*I/2. We are not expanding for solution with symbols + # or undefined functions because that makes the solution more complicated. + # For example, expand_complex(a) returns re(a) + I*im(a) + if all(s.atoms(Symbol, AppliedUndef) == set() and not isinstance(s, RootOf) + for s in result): + s = Dummy('s') + result = imageset(Lambda(s, expand_complex(s)), result) + if isinstance(result, FiniteSet) and domain != S.Complexes: + # Avoid adding gratuitous intersections with S.Complexes. Actual + # conditions should be handled elsewhere. + result = result.intersection(domain) + return result + else: + return ConditionSet(symbol, Eq(f, 0), domain) + + +def _solve_radical(f, unradf, symbol, solveset_solver): + """ Helper function to solve equations with radicals """ + res = unradf + eq, cov = res if res else (f, []) + if not cov: + result = solveset_solver(eq, symbol) - \ + Union(*[solveset_solver(g, symbol) for g in denoms(f, symbol)]) + else: + y, yeq = cov + if not solveset_solver(y - I, y): + yreal = Dummy('yreal', real=True) + yeq = yeq.xreplace({y: yreal}) + eq = eq.xreplace({y: yreal}) + y = yreal + g_y_s = solveset_solver(yeq, symbol) + f_y_sols = solveset_solver(eq, y) + result = Union(*[imageset(Lambda(y, g_y), f_y_sols) + for g_y in g_y_s]) + + def check_finiteset(solutions): + f_set = [] # solutions for FiniteSet + c_set = [] # solutions for ConditionSet + for s in solutions: + if checksol(f, symbol, s): + f_set.append(s) + else: + c_set.append(s) + return FiniteSet(*f_set) + ConditionSet(symbol, Eq(f, 0), FiniteSet(*c_set)) + + def check_set(solutions): + if solutions is S.EmptySet: + return solutions + elif isinstance(solutions, ConditionSet): + # XXX: Maybe the base set should be checked? + return solutions + elif isinstance(solutions, FiniteSet): + return check_finiteset(solutions) + elif isinstance(solutions, Complement): + A, B = solutions.args + return Complement(check_set(A), B) + elif isinstance(solutions, Union): + return Union(*[check_set(s) for s in solutions.args]) + else: + # XXX: There should be more cases checked here. The cases above + # are all those that come up in the test suite for now. + return solutions + + solution_set = check_set(result) + + return solution_set + + +def _solve_abs(f, symbol, domain): + """ Helper function to solve equation involving absolute value function """ + if not domain.is_subset(S.Reals): + raise ValueError(filldedent(''' + Absolute values cannot be inverted in the + complex domain.''')) + p, q, r = Wild('p'), Wild('q'), Wild('r') + pattern_match = f.match(p*Abs(q) + r) or {} + f_p, f_q, f_r = [pattern_match.get(i, S.Zero) for i in (p, q, r)] + + if not (f_p.is_zero or f_q.is_zero): + domain = continuous_domain(f_q, symbol, domain) + from .inequalities import solve_univariate_inequality + q_pos_cond = solve_univariate_inequality(f_q >= 0, symbol, + relational=False, domain=domain, continuous=True) + q_neg_cond = q_pos_cond.complement(domain) + + sols_q_pos = solveset_real(f_p*f_q + f_r, + symbol).intersect(q_pos_cond) + sols_q_neg = solveset_real(f_p*(-f_q) + f_r, + symbol).intersect(q_neg_cond) + return Union(sols_q_pos, sols_q_neg) + else: + return ConditionSet(symbol, Eq(f, 0), domain) + + +def solve_decomposition(f, symbol, domain): + """ + Function to solve equations via the principle of "Decomposition + and Rewriting". + + Examples + ======== + >>> from sympy import exp, sin, Symbol, pprint, S + >>> from sympy.solvers.solveset import solve_decomposition as sd + >>> x = Symbol('x') + >>> f1 = exp(2*x) - 3*exp(x) + 2 + >>> sd(f1, x, S.Reals) + {0, log(2)} + >>> f2 = sin(x)**2 + 2*sin(x) + 1 + >>> pprint(sd(f2, x, S.Reals), use_unicode=False) + 3*pi + {2*n*pi + ---- | n in Integers} + 2 + >>> f3 = sin(x + 2) + >>> pprint(sd(f3, x, S.Reals), use_unicode=False) + {2*n*pi - 2 | n in Integers} U {2*n*pi - 2 + pi | n in Integers} + + """ + from sympy.solvers.decompogen import decompogen + # decompose the given function + g_s = decompogen(f, symbol) + # `y_s` represents the set of values for which the function `g` is to be + # solved. + # `solutions` represent the solutions of the equations `g = y_s` or + # `g = 0` depending on the type of `y_s`. + # As we are interested in solving the equation: f = 0 + y_s = FiniteSet(0) + for g in g_s: + frange = function_range(g, symbol, domain) + y_s = Intersection(frange, y_s) + result = S.EmptySet + if isinstance(y_s, FiniteSet): + for y in y_s: + solutions = solveset(Eq(g, y), symbol, domain) + if not isinstance(solutions, ConditionSet): + result += solutions + + else: + if isinstance(y_s, ImageSet): + iter_iset = (y_s,) + + elif isinstance(y_s, Union): + iter_iset = y_s.args + + elif y_s is S.EmptySet: + # y_s is not in the range of g in g_s, so no solution exists + #in the given domain + return S.EmptySet + + for iset in iter_iset: + new_solutions = solveset(Eq(iset.lamda.expr, g), symbol, domain) + dummy_var = tuple(iset.lamda.expr.free_symbols)[0] + (base_set,) = iset.base_sets + if isinstance(new_solutions, FiniteSet): + new_exprs = new_solutions + + elif isinstance(new_solutions, Intersection): + if isinstance(new_solutions.args[1], FiniteSet): + new_exprs = new_solutions.args[1] + + for new_expr in new_exprs: + result += ImageSet(Lambda(dummy_var, new_expr), base_set) + + if result is S.EmptySet: + return ConditionSet(symbol, Eq(f, 0), domain) + + y_s = result + + return y_s + + +def _solveset(f, symbol, domain, _check=False): + """Helper for solveset to return a result from an expression + that has already been sympify'ed and is known to contain the + given symbol.""" + # _check controls whether the answer is checked or not + from sympy.simplify.simplify import signsimp + + if isinstance(f, BooleanTrue): + return domain + + orig_f = f + if f.is_Mul: + coeff, f = f.as_independent(symbol, as_Add=False) + if coeff in {S.ComplexInfinity, S.NegativeInfinity, S.Infinity}: + f = together(orig_f) + elif f.is_Add: + a, h = f.as_independent(symbol) + m, h = h.as_independent(symbol, as_Add=False) + if m not in {S.ComplexInfinity, S.Zero, S.Infinity, + S.NegativeInfinity}: + f = a/m + h # XXX condition `m != 0` should be added to soln + + # assign the solvers to use + solver = lambda f, x, domain=domain: _solveset(f, x, domain) + inverter = lambda f, rhs, symbol: _invert(f, rhs, symbol, domain) + + result = S.EmptySet + + if f.expand().is_zero: + return domain + elif not f.has(symbol): + return S.EmptySet + elif f.is_Mul and all(_is_finite_with_finite_vars(m, domain) + for m in f.args): + # if f(x) and g(x) are both finite we can say that the solution of + # f(x)*g(x) == 0 is same as Union(f(x) == 0, g(x) == 0) is not true in + # general. g(x) can grow to infinitely large for the values where + # f(x) == 0. To be sure that we are not silently allowing any + # wrong solutions we are using this technique only if both f and g are + # finite for a finite input. + result = Union(*[solver(m, symbol) for m in f.args]) + elif (_is_function_class_equation(TrigonometricFunction, f, symbol) or \ + _is_function_class_equation(HyperbolicFunction, f, symbol)): + result = _solve_trig(f, symbol, domain) + elif isinstance(f, arg): + a = f.args[0] + result = Intersection(_solveset(re(a) > 0, symbol, domain), + _solveset(im(a), symbol, domain)) + elif f.is_Piecewise: + expr_set_pairs = f.as_expr_set_pairs(domain) + for (expr, in_set) in expr_set_pairs: + if in_set.is_Relational: + in_set = in_set.as_set() + solns = solver(expr, symbol, in_set) + result += solns + elif isinstance(f, Eq): + result = solver(Add(f.lhs, -f.rhs, evaluate=False), symbol, domain) + + elif f.is_Relational: + from .inequalities import solve_univariate_inequality + try: + result = solve_univariate_inequality( + f, symbol, domain=domain, relational=False) + except NotImplementedError: + result = ConditionSet(symbol, f, domain) + return result + elif _is_modular(f, symbol): + result = _solve_modular(f, symbol, domain) + else: + lhs, rhs_s = inverter(f, 0, symbol) + if lhs == symbol: + # do some very minimal simplification since + # repeated inversion may have left the result + # in a state that other solvers (e.g. poly) + # would have simplified; this is done here + # rather than in the inverter since here it + # is only done once whereas there it would + # be repeated for each step of the inversion + if isinstance(rhs_s, FiniteSet): + rhs_s = FiniteSet(*[Mul(* + signsimp(i).as_content_primitive()) + for i in rhs_s]) + result = rhs_s + + elif isinstance(rhs_s, FiniteSet): + for equation in [lhs - rhs for rhs in rhs_s]: + if equation == f: + u = unrad(f, symbol) + if u: + result += _solve_radical(equation, u, + symbol, + solver) + elif equation.has(Abs): + result += _solve_abs(f, symbol, domain) + else: + result_rational = _solve_as_rational(equation, symbol, domain) + if not isinstance(result_rational, ConditionSet): + result += result_rational + else: + # may be a transcendental type equation + t_result = _transolve(equation, symbol, domain) + if isinstance(t_result, ConditionSet): + # might need factoring; this is expensive so we + # have delayed until now. To avoid recursion + # errors look for a non-trivial factoring into + # a product of symbol dependent terms; I think + # that something that factors as a Pow would + # have already been recognized by now. + factored = equation.factor() + if factored.is_Mul and equation != factored: + _, dep = factored.as_independent(symbol) + if not dep.is_Add: + # non-trivial factoring of equation + # but use form with constants + # in case they need special handling + t_results = [] + for fac in Mul.make_args(factored): + if fac.has(symbol): + t_results.append(solver(fac, symbol)) + t_result = Union(*t_results) + result += t_result + else: + result += solver(equation, symbol) + + elif rhs_s is not S.EmptySet: + result = ConditionSet(symbol, Eq(f, 0), domain) + + if isinstance(result, ConditionSet): + if isinstance(f, Expr): + num, den = f.as_numer_denom() + if den.has(symbol): + _result = _solveset(num, symbol, domain) + if not isinstance(_result, ConditionSet): + singularities = _solveset(den, symbol, domain) + result = _result - singularities + + if _check: + if isinstance(result, ConditionSet): + # it wasn't solved or has enumerated all conditions + # -- leave it alone + return result + + # whittle away all but the symbol-containing core + # to use this for testing + if isinstance(orig_f, Expr): + fx = orig_f.as_independent(symbol, as_Add=True)[1] + fx = fx.as_independent(symbol, as_Add=False)[1] + else: + fx = orig_f + + if isinstance(result, FiniteSet): + # check the result for invalid solutions + result = FiniteSet(*[s for s in result + if isinstance(s, RootOf) + or domain_check(fx, symbol, s)]) + + return result + + +def _is_modular(f, symbol): + """ + Helper function to check below mentioned types of modular equations. + ``A - Mod(B, C) = 0`` + + A -> This can or cannot be a function of symbol. + B -> This is surely a function of symbol. + C -> It is an integer. + + Parameters + ========== + + f : Expr + The equation to be checked. + + symbol : Symbol + The concerned variable for which the equation is to be checked. + + Examples + ======== + + >>> from sympy import symbols, exp, Mod + >>> from sympy.solvers.solveset import _is_modular as check + >>> x, y = symbols('x y') + >>> check(Mod(x, 3) - 1, x) + True + >>> check(Mod(x, 3) - 1, y) + False + >>> check(Mod(x, 3)**2 - 5, x) + False + >>> check(Mod(x, 3)**2 - y, x) + False + >>> check(exp(Mod(x, 3)) - 1, x) + False + >>> check(Mod(3, y) - 1, y) + False + """ + + if not f.has(Mod): + return False + + # extract modterms from f. + modterms = list(f.atoms(Mod)) + + return (len(modterms) == 1 and # only one Mod should be present + modterms[0].args[0].has(symbol) and # B-> function of symbol + modterms[0].args[1].is_integer and # C-> to be an integer. + any(isinstance(term, Mod) + for term in list(_term_factors(f))) # free from other funcs + ) + + +def _invert_modular(modterm, rhs, n, symbol): + """ + Helper function to invert modular equation. + ``Mod(a, m) - rhs = 0`` + + Generally it is inverted as (a, ImageSet(Lambda(n, m*n + rhs), S.Integers)). + More simplified form will be returned if possible. + + If it is not invertible then (modterm, rhs) is returned. + + The following cases arise while inverting equation ``Mod(a, m) - rhs = 0``: + + 1. If a is symbol then m*n + rhs is the required solution. + + 2. If a is an instance of ``Add`` then we try to find two symbol independent + parts of a and the symbol independent part gets transferred to the other + side and again the ``_invert_modular`` is called on the symbol + dependent part. + + 3. If a is an instance of ``Mul`` then same as we done in ``Add`` we separate + out the symbol dependent and symbol independent parts and transfer the + symbol independent part to the rhs with the help of invert and again the + ``_invert_modular`` is called on the symbol dependent part. + + 4. If a is an instance of ``Pow`` then two cases arise as following: + + - If a is of type (symbol_indep)**(symbol_dep) then the remainder is + evaluated with the help of discrete_log function and then the least + period is being found out with the help of totient function. + period*n + remainder is the required solution in this case. + For reference: (https://en.wikipedia.org/wiki/Euler's_theorem) + + - If a is of type (symbol_dep)**(symbol_indep) then we try to find all + primitive solutions list with the help of nthroot_mod function. + m*n + rem is the general solution where rem belongs to solutions list + from nthroot_mod function. + + Parameters + ========== + + modterm, rhs : Expr + The modular equation to be inverted, ``modterm - rhs = 0`` + + symbol : Symbol + The variable in the equation to be inverted. + + n : Dummy + Dummy variable for output g_n. + + Returns + ======= + + A tuple (f_x, g_n) is being returned where f_x is modular independent function + of symbol and g_n being set of values f_x can have. + + Examples + ======== + + >>> from sympy import symbols, exp, Mod, Dummy, S + >>> from sympy.solvers.solveset import _invert_modular as invert_modular + >>> x, y = symbols('x y') + >>> n = Dummy('n') + >>> invert_modular(Mod(exp(x), 7), S(5), n, x) + (Mod(exp(x), 7), 5) + >>> invert_modular(Mod(x, 7), S(5), n, x) + (x, ImageSet(Lambda(_n, 7*_n + 5), Integers)) + >>> invert_modular(Mod(3*x + 8, 7), S(5), n, x) + (x, ImageSet(Lambda(_n, 7*_n + 6), Integers)) + >>> invert_modular(Mod(x**4, 7), S(5), n, x) + (x, EmptySet) + >>> invert_modular(Mod(2**(x**2 + x + 1), 7), S(2), n, x) + (x**2 + x + 1, ImageSet(Lambda(_n, 3*_n + 1), Naturals0)) + + """ + a, m = modterm.args + + if rhs.is_integer is False: + return symbol, S.EmptySet + + if rhs.is_real is False or any(term.is_real is False + for term in list(_term_factors(a))): + # Check for complex arguments + return modterm, rhs + + if abs(rhs) >= abs(m): + # if rhs has value greater than value of m. + return symbol, S.EmptySet + + if a == symbol: + return symbol, ImageSet(Lambda(n, m*n + rhs), S.Integers) + + if a.is_Add: + # g + h = a + g, h = a.as_independent(symbol) + if g is not S.Zero: + x_indep_term = rhs - Mod(g, m) + return _invert_modular(Mod(h, m), Mod(x_indep_term, m), n, symbol) + + if a.is_Mul: + # g*h = a + g, h = a.as_independent(symbol) + if g is not S.One: + x_indep_term = rhs*invert(g, m) + return _invert_modular(Mod(h, m), Mod(x_indep_term, m), n, symbol) + + if a.is_Pow: + # base**expo = a + base, expo = a.args + if expo.has(symbol) and not base.has(symbol): + # remainder -> solution independent of n of equation. + # m, rhs are made coprime by dividing number_gcd(m, rhs) + if not m.is_Integer and rhs.is_Integer and a.base.is_Integer: + return modterm, rhs + + mdiv = m.p // number_gcd(m.p, rhs.p) + try: + remainder = discrete_log(mdiv, rhs.p, a.base.p) + except ValueError: # log does not exist + return modterm, rhs + # period -> coefficient of n in the solution and also referred as + # the least period of expo in which it is repeats itself. + # (a**(totient(m)) - 1) divides m. Here is link of theorem: + # (https://en.wikipedia.org/wiki/Euler's_theorem) + period = totient(m) + for p in divisors(period): + # there might a lesser period exist than totient(m). + if pow(a.base, p, m / number_gcd(m.p, a.base.p)) == 1: + period = p + break + # recursion is not applied here since _invert_modular is currently + # not smart enough to handle infinite rhs as here expo has infinite + # rhs = ImageSet(Lambda(n, period*n + remainder), S.Naturals0). + return expo, ImageSet(Lambda(n, period*n + remainder), S.Naturals0) + elif base.has(symbol) and not expo.has(symbol): + try: + remainder_list = nthroot_mod(rhs, expo, m, all_roots=True) + if remainder_list == []: + return symbol, S.EmptySet + except (ValueError, NotImplementedError): + return modterm, rhs + g_n = S.EmptySet + for rem in remainder_list: + g_n += ImageSet(Lambda(n, m*n + rem), S.Integers) + return base, g_n + + return modterm, rhs + + +def _solve_modular(f, symbol, domain): + r""" + Helper function for solving modular equations of type ``A - Mod(B, C) = 0``, + where A can or cannot be a function of symbol, B is surely a function of + symbol and C is an integer. + + Currently ``_solve_modular`` is only able to solve cases + where A is not a function of symbol. + + Parameters + ========== + + f : Expr + The modular equation to be solved, ``f = 0`` + + symbol : Symbol + The variable in the equation to be solved. + + domain : Set + A set over which the equation is solved. It has to be a subset of + Integers. + + Returns + ======= + + A set of integer solutions satisfying the given modular equation. + A ``ConditionSet`` if the equation is unsolvable. + + Examples + ======== + + >>> from sympy.solvers.solveset import _solve_modular as solve_modulo + >>> from sympy import S, Symbol, sin, Intersection, Interval, Mod + >>> x = Symbol('x') + >>> solve_modulo(Mod(5*x - 8, 7) - 3, x, S.Integers) + ImageSet(Lambda(_n, 7*_n + 5), Integers) + >>> solve_modulo(Mod(5*x - 8, 7) - 3, x, S.Reals) # domain should be subset of integers. + ConditionSet(x, Eq(Mod(5*x + 6, 7) - 3, 0), Reals) + >>> solve_modulo(-7 + Mod(x, 5), x, S.Integers) + EmptySet + >>> solve_modulo(Mod(12**x, 21) - 18, x, S.Integers) + ImageSet(Lambda(_n, 6*_n + 2), Naturals0) + >>> solve_modulo(Mod(sin(x), 7) - 3, x, S.Integers) # not solvable + ConditionSet(x, Eq(Mod(sin(x), 7) - 3, 0), Integers) + >>> solve_modulo(3 - Mod(x, 5), x, Intersection(S.Integers, Interval(0, 100))) + Intersection(ImageSet(Lambda(_n, 5*_n + 3), Integers), Range(0, 101, 1)) + """ + # extract modterm and g_y from f + unsolved_result = ConditionSet(symbol, Eq(f, 0), domain) + modterm = list(f.atoms(Mod))[0] + rhs = -S.One*(f.subs(modterm, S.Zero)) + if f.as_coefficients_dict()[modterm].is_negative: + # checks if coefficient of modterm is negative in main equation. + rhs *= -S.One + + if not domain.is_subset(S.Integers): + return unsolved_result + + if rhs.has(symbol): + # TODO Case: A-> function of symbol, can be extended here + # in future. + return unsolved_result + + n = Dummy('n', integer=True) + f_x, g_n = _invert_modular(modterm, rhs, n, symbol) + + if f_x == modterm and g_n == rhs: + return unsolved_result + + if f_x == symbol: + if domain is not S.Integers: + return domain.intersect(g_n) + return g_n + + if isinstance(g_n, ImageSet): + lamda_expr = g_n.lamda.expr + lamda_vars = g_n.lamda.variables + base_sets = g_n.base_sets + sol_set = _solveset(f_x - lamda_expr, symbol, S.Integers) + if isinstance(sol_set, FiniteSet): + tmp_sol = S.EmptySet + for sol in sol_set: + tmp_sol += ImageSet(Lambda(lamda_vars, sol), *base_sets) + sol_set = tmp_sol + else: + sol_set = ImageSet(Lambda(lamda_vars, sol_set), *base_sets) + return domain.intersect(sol_set) + + return unsolved_result + + +def _term_factors(f): + """ + Iterator to get the factors of all terms present + in the given equation. + + Parameters + ========== + f : Expr + Equation that needs to be addressed + + Returns + ======= + Factors of all terms present in the equation. + + Examples + ======== + + >>> from sympy import symbols + >>> from sympy.solvers.solveset import _term_factors + >>> x = symbols('x') + >>> list(_term_factors(-2 - x**2 + x*(x + 1))) + [-2, -1, x**2, x, x + 1] + """ + for add_arg in Add.make_args(f): + yield from Mul.make_args(add_arg) + + +def _solve_exponential(lhs, rhs, symbol, domain): + r""" + Helper function for solving (supported) exponential equations. + + Exponential equations are the sum of (currently) at most + two terms with one or both of them having a power with a + symbol-dependent exponent. + + For example + + .. math:: 5^{2x + 3} - 5^{3x - 1} + + .. math:: 4^{5 - 9x} - e^{2 - x} + + Parameters + ========== + + lhs, rhs : Expr + The exponential equation to be solved, `lhs = rhs` + + symbol : Symbol + The variable in which the equation is solved + + domain : Set + A set over which the equation is solved. + + Returns + ======= + + A set of solutions satisfying the given equation. + A ``ConditionSet`` if the equation is unsolvable or + if the assumptions are not properly defined, in that case + a different style of ``ConditionSet`` is returned having the + solution(s) of the equation with the desired assumptions. + + Examples + ======== + + >>> from sympy.solvers.solveset import _solve_exponential as solve_expo + >>> from sympy import symbols, S + >>> x = symbols('x', real=True) + >>> a, b = symbols('a b') + >>> solve_expo(2**x + 3**x - 5**x, 0, x, S.Reals) # not solvable + ConditionSet(x, Eq(2**x + 3**x - 5**x, 0), Reals) + >>> solve_expo(a**x - b**x, 0, x, S.Reals) # solvable but incorrect assumptions + ConditionSet(x, (a > 0) & (b > 0), {0}) + >>> solve_expo(3**(2*x) - 2**(x + 3), 0, x, S.Reals) + {-3*log(2)/(-2*log(3) + log(2))} + >>> solve_expo(2**x - 4**x, 0, x, S.Reals) + {0} + + * Proof of correctness of the method + + The logarithm function is the inverse of the exponential function. + The defining relation between exponentiation and logarithm is: + + .. math:: {\log_b x} = y \enspace if \enspace b^y = x + + Therefore if we are given an equation with exponent terms, we can + convert every term to its corresponding logarithmic form. This is + achieved by taking logarithms and expanding the equation using + logarithmic identities so that it can easily be handled by ``solveset``. + + For example: + + .. math:: 3^{2x} = 2^{x + 3} + + Taking log both sides will reduce the equation to + + .. math:: (2x)\log(3) = (x + 3)\log(2) + + This form can be easily handed by ``solveset``. + """ + unsolved_result = ConditionSet(symbol, Eq(lhs - rhs, 0), domain) + newlhs = powdenest(lhs) + if lhs != newlhs: + # it may also be advantageous to factor the new expr + neweq = factor(newlhs - rhs) + if neweq != (lhs - rhs): + return _solveset(neweq, symbol, domain) # try again with _solveset + + if not (isinstance(lhs, Add) and len(lhs.args) == 2): + # solving for the sum of more than two powers is possible + # but not yet implemented + return unsolved_result + + if rhs != 0: + return unsolved_result + + a, b = list(ordered(lhs.args)) + a_term = a.as_independent(symbol)[1] + b_term = b.as_independent(symbol)[1] + + a_base, a_exp = a_term.as_base_exp() + b_base, b_exp = b_term.as_base_exp() + + if domain.is_subset(S.Reals): + conditions = And( + a_base > 0, + b_base > 0, + Eq(im(a_exp), 0), + Eq(im(b_exp), 0)) + else: + conditions = And( + Ne(a_base, 0), + Ne(b_base, 0)) + + L, R = (expand_log(log(i), force=True) for i in (a, -b)) + solutions = _solveset(L - R, symbol, domain) + + return ConditionSet(symbol, conditions, solutions) + + +def _is_exponential(f, symbol): + r""" + Return ``True`` if one or more terms contain ``symbol`` only in + exponents, else ``False``. + + Parameters + ========== + + f : Expr + The equation to be checked + + symbol : Symbol + The variable in which the equation is checked + + Examples + ======== + + >>> from sympy import symbols, cos, exp + >>> from sympy.solvers.solveset import _is_exponential as check + >>> x, y = symbols('x y') + >>> check(y, y) + False + >>> check(x**y - 1, y) + True + >>> check(x**y*2**y - 1, y) + True + >>> check(exp(x + 3) + 3**x, x) + True + >>> check(cos(2**x), x) + False + + * Philosophy behind the helper + + The function extracts each term of the equation and checks if it is + of exponential form w.r.t ``symbol``. + """ + rv = False + for expr_arg in _term_factors(f): + if symbol not in expr_arg.free_symbols: + continue + if (isinstance(expr_arg, Pow) and + symbol not in expr_arg.base.free_symbols or + isinstance(expr_arg, exp)): + rv = True # symbol in exponent + else: + return False # dependent on symbol in non-exponential way + return rv + + +def _solve_logarithm(lhs, rhs, symbol, domain): + r""" + Helper to solve logarithmic equations which are reducible + to a single instance of `\log`. + + Logarithmic equations are (currently) the equations that contains + `\log` terms which can be reduced to a single `\log` term or + a constant using various logarithmic identities. + + For example: + + .. math:: \log(x) + \log(x - 4) + + can be reduced to: + + .. math:: \log(x(x - 4)) + + Parameters + ========== + + lhs, rhs : Expr + The logarithmic equation to be solved, `lhs = rhs` + + symbol : Symbol + The variable in which the equation is solved + + domain : Set + A set over which the equation is solved. + + Returns + ======= + + A set of solutions satisfying the given equation. + A ``ConditionSet`` if the equation is unsolvable. + + Examples + ======== + + >>> from sympy import symbols, log, S + >>> from sympy.solvers.solveset import _solve_logarithm as solve_log + >>> x = symbols('x') + >>> f = log(x - 3) + log(x + 3) + >>> solve_log(f, 0, x, S.Reals) + {-sqrt(10), sqrt(10)} + + * Proof of correctness + + A logarithm is another way to write exponent and is defined by + + .. math:: {\log_b x} = y \enspace if \enspace b^y = x + + When one side of the equation contains a single logarithm, the + equation can be solved by rewriting the equation as an equivalent + exponential equation as defined above. But if one side contains + more than one logarithm, we need to use the properties of logarithm + to condense it into a single logarithm. + + Take for example + + .. math:: \log(2x) - 15 = 0 + + contains single logarithm, therefore we can directly rewrite it to + exponential form as + + .. math:: x = \frac{e^{15}}{2} + + But if the equation has more than one logarithm as + + .. math:: \log(x - 3) + \log(x + 3) = 0 + + we use logarithmic identities to convert it into a reduced form + + Using, + + .. math:: \log(a) + \log(b) = \log(ab) + + the equation becomes, + + .. math:: \log((x - 3)(x + 3)) + + This equation contains one logarithm and can be solved by rewriting + to exponents. + """ + new_lhs = logcombine(lhs, force=True) + new_f = new_lhs - rhs + + return _solveset(new_f, symbol, domain) + + +def _is_logarithmic(f, symbol): + r""" + Return ``True`` if the equation is in the form + `a\log(f(x)) + b\log(g(x)) + ... + c` else ``False``. + + Parameters + ========== + + f : Expr + The equation to be checked + + symbol : Symbol + The variable in which the equation is checked + + Returns + ======= + + ``True`` if the equation is logarithmic otherwise ``False``. + + Examples + ======== + + >>> from sympy import symbols, tan, log + >>> from sympy.solvers.solveset import _is_logarithmic as check + >>> x, y = symbols('x y') + >>> check(log(x + 2) - log(x + 3), x) + True + >>> check(tan(log(2*x)), x) + False + >>> check(x*log(x), x) + False + >>> check(x + log(x), x) + False + >>> check(y + log(x), x) + True + + * Philosophy behind the helper + + The function extracts each term and checks whether it is + logarithmic w.r.t ``symbol``. + """ + rv = False + for term in Add.make_args(f): + saw_log = False + for term_arg in Mul.make_args(term): + if symbol not in term_arg.free_symbols: + continue + if isinstance(term_arg, log): + if saw_log: + return False # more than one log in term + saw_log = True + else: + return False # dependent on symbol in non-log way + if saw_log: + rv = True + return rv + + +def _is_lambert(f, symbol): + r""" + If this returns ``False`` then the Lambert solver (``_solve_lambert``) will not be called. + + Explanation + =========== + + Quick check for cases that the Lambert solver might be able to handle. + + 1. Equations containing more than two operands and `symbol`s involving any of + `Pow`, `exp`, `HyperbolicFunction`,`TrigonometricFunction`, `log` terms. + + 2. In `Pow`, `exp` the exponent should have `symbol` whereas for + `HyperbolicFunction`,`TrigonometricFunction`, `log` should contain `symbol`. + + 3. For `HyperbolicFunction`,`TrigonometricFunction` the number of trigonometric functions in + equation should be less than number of symbols. (since `A*cos(x) + B*sin(x) - c` + is not the Lambert type). + + Some forms of lambert equations are: + 1. X**X = C + 2. X*(B*log(X) + D)**A = C + 3. A*log(B*X + A) + d*X = C + 4. (B*X + A)*exp(d*X + g) = C + 5. g*exp(B*X + h) - B*X = C + 6. A*D**(E*X + g) - B*X = C + 7. A*cos(X) + B*sin(X) - D*X = C + 8. A*cosh(X) + B*sinh(X) - D*X = C + + Where X is any variable, + A, B, C, D, E are any constants, + g, h are linear functions or log terms. + + Parameters + ========== + + f : Expr + The equation to be checked + + symbol : Symbol + The variable in which the equation is checked + + Returns + ======= + + If this returns ``False`` then the Lambert solver (``_solve_lambert``) will not be called. + + Examples + ======== + + >>> from sympy.solvers.solveset import _is_lambert + >>> from sympy import symbols, cosh, sinh, log + >>> x = symbols('x') + + >>> _is_lambert(3*log(x) - x*log(3), x) + True + >>> _is_lambert(log(log(x - 3)) + log(x-3), x) + True + >>> _is_lambert(cosh(x) - sinh(x), x) + False + >>> _is_lambert((x**2 - 2*x + 1).subs(x, (log(x) + 3*x)**2 - 1), x) + True + + See Also + ======== + + _solve_lambert + + """ + term_factors = list(_term_factors(f.expand())) + + # total number of symbols in equation + no_of_symbols = len([arg for arg in term_factors if arg.has(symbol)]) + # total number of trigonometric terms in equation + no_of_trig = len([arg for arg in term_factors \ + if arg.has(HyperbolicFunction, TrigonometricFunction)]) + + if f.is_Add and no_of_symbols >= 2: + # `log`, `HyperbolicFunction`, `TrigonometricFunction` should have symbols + # and no_of_trig < no_of_symbols + lambert_funcs = (log, HyperbolicFunction, TrigonometricFunction) + if any(isinstance(arg, lambert_funcs)\ + for arg in term_factors if arg.has(symbol)): + if no_of_trig < no_of_symbols: + return True + # here, `Pow`, `exp` exponent should have symbols + elif any(isinstance(arg, (Pow, exp)) \ + for arg in term_factors if (arg.as_base_exp()[1]).has(symbol)): + return True + return False + + +def _transolve(f, symbol, domain): + r""" + Function to solve transcendental equations. It is a helper to + ``solveset`` and should be used internally. ``_transolve`` + currently supports the following class of equations: + + - Exponential equations + - Logarithmic equations + + Parameters + ========== + + f : Any transcendental equation that needs to be solved. + This needs to be an expression, which is assumed + to be equal to ``0``. + + symbol : The variable for which the equation is solved. + This needs to be of class ``Symbol``. + + domain : A set over which the equation is solved. + This needs to be of class ``Set``. + + Returns + ======= + + Set + A set of values for ``symbol`` for which ``f`` is equal to + zero. An ``EmptySet`` is returned if ``f`` does not have solutions + in respective domain. A ``ConditionSet`` is returned as unsolved + object if algorithms to evaluate complete solution are not + yet implemented. + + How to use ``_transolve`` + ========================= + + ``_transolve`` should not be used as an independent function, because + it assumes that the equation (``f``) and the ``symbol`` comes from + ``solveset`` and might have undergone a few modification(s). + To use ``_transolve`` as an independent function the equation (``f``) + and the ``symbol`` should be passed as they would have been by + ``solveset``. + + Examples + ======== + + >>> from sympy.solvers.solveset import _transolve as transolve + >>> from sympy.solvers.solvers import _tsolve as tsolve + >>> from sympy import symbols, S, pprint + >>> x = symbols('x', real=True) # assumption added + >>> transolve(5**(x - 3) - 3**(2*x + 1), x, S.Reals) + {-(log(3) + 3*log(5))/(-log(5) + 2*log(3))} + + How ``_transolve`` works + ======================== + + ``_transolve`` uses two types of helper functions to solve equations + of a particular class: + + Identifying helpers: To determine whether a given equation + belongs to a certain class of equation or not. Returns either + ``True`` or ``False``. + + Solving helpers: Once an equation is identified, a corresponding + helper either solves the equation or returns a form of the equation + that ``solveset`` might better be able to handle. + + * Philosophy behind the module + + The purpose of ``_transolve`` is to take equations which are not + already polynomial in their generator(s) and to either recast them + as such through a valid transformation or to solve them outright. + A pair of helper functions for each class of supported + transcendental functions are employed for this purpose. One + identifies the transcendental form of an equation and the other + either solves it or recasts it into a tractable form that can be + solved by ``solveset``. + For example, an equation in the form `ab^{f(x)} - cd^{g(x)} = 0` + can be transformed to + `\log(a) + f(x)\log(b) - \log(c) - g(x)\log(d) = 0` + (under certain assumptions) and this can be solved with ``solveset`` + if `f(x)` and `g(x)` are in polynomial form. + + How ``_transolve`` is better than ``_tsolve`` + ============================================= + + 1) Better output + + ``_transolve`` provides expressions in a more simplified form. + + Consider a simple exponential equation + + >>> f = 3**(2*x) - 2**(x + 3) + >>> pprint(transolve(f, x, S.Reals), use_unicode=False) + -3*log(2) + {------------------} + -2*log(3) + log(2) + >>> pprint(tsolve(f, x), use_unicode=False) + / 3 \ + | --------| + | log(2/9)| + [-log\2 /] + + 2) Extensible + + The API of ``_transolve`` is designed such that it is easily + extensible, i.e. the code that solves a given class of + equations is encapsulated in a helper and not mixed in with + the code of ``_transolve`` itself. + + 3) Modular + + ``_transolve`` is designed to be modular i.e, for every class of + equation a separate helper for identification and solving is + implemented. This makes it easy to change or modify any of the + method implemented directly in the helpers without interfering + with the actual structure of the API. + + 4) Faster Computation + + Solving equation via ``_transolve`` is much faster as compared to + ``_tsolve``. In ``solve``, attempts are made computing every possibility + to get the solutions. This series of attempts makes solving a bit + slow. In ``_transolve``, computation begins only after a particular + type of equation is identified. + + How to add new class of equations + ================================= + + Adding a new class of equation solver is a three-step procedure: + + - Identify the type of the equations + + Determine the type of the class of equations to which they belong: + it could be of ``Add``, ``Pow``, etc. types. Separate internal functions + are used for each type. Write identification and solving helpers + and use them from within the routine for the given type of equation + (after adding it, if necessary). Something like: + + .. code-block:: python + + def add_type(lhs, rhs, x): + .... + if _is_exponential(lhs, x): + new_eq = _solve_exponential(lhs, rhs, x) + .... + rhs, lhs = eq.as_independent(x) + if lhs.is_Add: + result = add_type(lhs, rhs, x) + + - Define the identification helper. + + - Define the solving helper. + + Apart from this, a few other things needs to be taken care while + adding an equation solver: + + - Naming conventions: + Name of the identification helper should be as + ``_is_class`` where class will be the name or abbreviation + of the class of equation. The solving helper will be named as + ``_solve_class``. + For example: for exponential equations it becomes + ``_is_exponential`` and ``_solve_expo``. + - The identifying helpers should take two input parameters, + the equation to be checked and the variable for which a solution + is being sought, while solving helpers would require an additional + domain parameter. + - Be sure to consider corner cases. + - Add tests for each helper. + - Add a docstring to your helper that describes the method + implemented. + The documentation of the helpers should identify: + + - the purpose of the helper, + - the method used to identify and solve the equation, + - a proof of correctness + - the return values of the helpers + """ + + def add_type(lhs, rhs, symbol, domain): + """ + Helper for ``_transolve`` to handle equations of + ``Add`` type, i.e. equations taking the form as + ``a*f(x) + b*g(x) + .... = c``. + For example: 4**x + 8**x = 0 + """ + result = ConditionSet(symbol, Eq(lhs - rhs, 0), domain) + + # check if it is exponential type equation + if _is_exponential(lhs, symbol): + result = _solve_exponential(lhs, rhs, symbol, domain) + # check if it is logarithmic type equation + elif _is_logarithmic(lhs, symbol): + result = _solve_logarithm(lhs, rhs, symbol, domain) + + return result + + result = ConditionSet(symbol, Eq(f, 0), domain) + + # invert_complex handles the call to the desired inverter based + # on the domain specified. + lhs, rhs_s = invert_complex(f, 0, symbol, domain) + + if isinstance(rhs_s, FiniteSet): + assert (len(rhs_s.args)) == 1 + rhs = rhs_s.args[0] + + if lhs.is_Add: + result = add_type(lhs, rhs, symbol, domain) + else: + result = rhs_s + + return result + + +def solveset(f, symbol=None, domain=S.Complexes): + r"""Solves a given inequality or equation with set as output + + Parameters + ========== + + f : Expr or a relational. + The target equation or inequality + symbol : Symbol + The variable for which the equation is solved + domain : Set + The domain over which the equation is solved + + Returns + ======= + + Set + A set of values for `symbol` for which `f` is True or is equal to + zero. An :class:`~.EmptySet` is returned if `f` is False or nonzero. + A :class:`~.ConditionSet` is returned as unsolved object if algorithms + to evaluate complete solution are not yet implemented. + + ``solveset`` claims to be complete in the solution set that it returns. + + Raises + ====== + + NotImplementedError + The algorithms to solve inequalities in complex domain are + not yet implemented. + ValueError + The input is not valid. + RuntimeError + It is a bug, please report to the github issue tracker. + + + Notes + ===== + + Python interprets 0 and 1 as False and True, respectively, but + in this function they refer to solutions of an expression. So 0 and 1 + return the domain and EmptySet, respectively, while True and False + return the opposite (as they are assumed to be solutions of relational + expressions). + + + See Also + ======== + + solveset_real: solver for real domain + solveset_complex: solver for complex domain + + Examples + ======== + + >>> from sympy import exp, sin, Symbol, pprint, S, Eq + >>> from sympy.solvers.solveset import solveset, solveset_real + + * The default domain is complex. Not specifying a domain will lead + to the solving of the equation in the complex domain (and this + is not affected by the assumptions on the symbol): + + >>> x = Symbol('x') + >>> pprint(solveset(exp(x) - 1, x), use_unicode=False) + {2*n*I*pi | n in Integers} + + >>> x = Symbol('x', real=True) + >>> pprint(solveset(exp(x) - 1, x), use_unicode=False) + {2*n*I*pi | n in Integers} + + * If you want to use ``solveset`` to solve the equation in the + real domain, provide a real domain. (Using ``solveset_real`` + does this automatically.) + + >>> R = S.Reals + >>> x = Symbol('x') + >>> solveset(exp(x) - 1, x, R) + {0} + >>> solveset_real(exp(x) - 1, x) + {0} + + The solution is unaffected by assumptions on the symbol: + + >>> p = Symbol('p', positive=True) + >>> pprint(solveset(p**2 - 4)) + {-2, 2} + + When a :class:`~.ConditionSet` is returned, symbols with assumptions that + would alter the set are replaced with more generic symbols: + + >>> i = Symbol('i', imaginary=True) + >>> solveset(Eq(i**2 + i*sin(i), 1), i, domain=S.Reals) + ConditionSet(_R, Eq(_R**2 + _R*sin(_R) - 1, 0), Reals) + + * Inequalities can be solved over the real domain only. Use of a complex + domain leads to a NotImplementedError. + + >>> solveset(exp(x) > 1, x, R) + Interval.open(0, oo) + + """ + f = sympify(f) + symbol = sympify(symbol) + + if f is S.true: + return domain + + if f is S.false: + return S.EmptySet + + if not isinstance(f, (Expr, Relational, Number)): + raise ValueError("%s is not a valid SymPy expression" % f) + + if not isinstance(symbol, (Expr, Relational)) and symbol is not None: + raise ValueError("%s is not a valid SymPy symbol" % (symbol,)) + + if not isinstance(domain, Set): + raise ValueError("%s is not a valid domain" %(domain)) + + free_symbols = f.free_symbols + + if f.has(Piecewise): + f = piecewise_fold(f) + + if symbol is None and not free_symbols: + b = Eq(f, 0) + if b is S.true: + return domain + elif b is S.false: + return S.EmptySet + else: + raise NotImplementedError(filldedent(''' + relationship between value and 0 is unknown: %s''' % b)) + + if symbol is None: + if len(free_symbols) == 1: + symbol = free_symbols.pop() + elif free_symbols: + raise ValueError(filldedent(''' + The independent variable must be specified for a + multivariate equation.''')) + elif not isinstance(symbol, Symbol): + f, s, swap = recast_to_symbols([f], [symbol]) + # the xreplace will be needed if a ConditionSet is returned + return solveset(f[0], s[0], domain).xreplace(swap) + + # solveset should ignore assumptions on symbols + newsym = None + if domain.is_subset(S.Reals): + if symbol._assumptions_orig != {'real': True}: + newsym = Dummy('R', real=True) + elif domain.is_subset(S.Complexes): + if symbol._assumptions_orig != {'complex': True}: + newsym = Dummy('C', complex=True) + + if newsym is not None: + rv = solveset(f.xreplace({symbol: newsym}), newsym, domain) + # try to use the original symbol if possible + try: + _rv = rv.xreplace({newsym: symbol}) + except TypeError: + _rv = rv + if rv.dummy_eq(_rv): + rv = _rv + return rv + + # Abs has its own handling method which avoids the + # rewriting property that the first piece of abs(x) + # is for x >= 0 and the 2nd piece for x < 0 -- solutions + # can look better if the 2nd condition is x <= 0. Since + # the solution is a set, duplication of results is not + # an issue, e.g. {y, -y} when y is 0 will be {0} + f, mask = _masked(f, Abs) + f = f.rewrite(Piecewise) # everything that's not an Abs + for d, e in mask: + # everything *in* an Abs + e = e.func(e.args[0].rewrite(Piecewise)) + f = f.xreplace({d: e}) + f = piecewise_fold(f) + + return _solveset(f, symbol, domain, _check=True) + + +def solveset_real(f, symbol): + return solveset(f, symbol, S.Reals) + + +def solveset_complex(f, symbol): + return solveset(f, symbol, S.Complexes) + + +def _solveset_multi(eqs, syms, domains): + '''Basic implementation of a multivariate solveset. + + For internal use (not ready for public consumption)''' + + rep = {} + for sym, dom in zip(syms, domains): + if dom is S.Reals: + rep[sym] = Symbol(sym.name, real=True) + eqs = [eq.subs(rep) for eq in eqs] + syms = [sym.subs(rep) for sym in syms] + + syms = tuple(syms) + + if len(eqs) == 0: + return ProductSet(*domains) + + if len(syms) == 1: + sym = syms[0] + domain = domains[0] + solsets = [solveset(eq, sym, domain) for eq in eqs] + solset = Intersection(*solsets) + return ImageSet(Lambda((sym,), (sym,)), solset).doit() + + eqs = sorted(eqs, key=lambda eq: len(eq.free_symbols & set(syms))) + + for n, eq in enumerate(eqs): + sols = [] + all_handled = True + for sym in syms: + if sym not in eq.free_symbols: + continue + sol = solveset(eq, sym, domains[syms.index(sym)]) + + if isinstance(sol, FiniteSet): + i = syms.index(sym) + symsp = syms[:i] + syms[i+1:] + domainsp = domains[:i] + domains[i+1:] + eqsp = eqs[:n] + eqs[n+1:] + for s in sol: + eqsp_sub = [eq.subs(sym, s) for eq in eqsp] + sol_others = _solveset_multi(eqsp_sub, symsp, domainsp) + fun = Lambda((symsp,), symsp[:i] + (s,) + symsp[i:]) + sols.append(ImageSet(fun, sol_others).doit()) + else: + all_handled = False + if all_handled: + return Union(*sols) + + +def solvify(f, symbol, domain): + """Solves an equation using solveset and returns the solution in accordance + with the `solve` output API. + + Returns + ======= + + We classify the output based on the type of solution returned by `solveset`. + + Solution | Output + ---------------------------------------- + FiniteSet | list + + ImageSet, | list (if `f` is periodic) + Union | + + Union | list (with FiniteSet) + + EmptySet | empty list + + Others | None + + + Raises + ====== + + NotImplementedError + A ConditionSet is the input. + + Examples + ======== + + >>> from sympy.solvers.solveset import solvify + >>> from sympy.abc import x + >>> from sympy import S, tan, sin, exp + >>> solvify(x**2 - 9, x, S.Reals) + [-3, 3] + >>> solvify(sin(x) - 1, x, S.Reals) + [pi/2] + >>> solvify(tan(x), x, S.Reals) + [0] + >>> solvify(exp(x) - 1, x, S.Complexes) + + >>> solvify(exp(x) - 1, x, S.Reals) + [0] + + """ + solution_set = solveset(f, symbol, domain) + result = None + if solution_set is S.EmptySet: + result = [] + + elif isinstance(solution_set, ConditionSet): + raise NotImplementedError('solveset is unable to solve this equation.') + + elif isinstance(solution_set, FiniteSet): + result = list(solution_set) + + else: + period = periodicity(f, symbol) + if period is not None: + solutions = S.EmptySet + iter_solutions = () + if isinstance(solution_set, ImageSet): + iter_solutions = (solution_set,) + elif isinstance(solution_set, Union): + if all(isinstance(i, ImageSet) for i in solution_set.args): + iter_solutions = solution_set.args + + for solution in iter_solutions: + solutions += solution.intersect(Interval(0, period, False, True)) + + if isinstance(solutions, FiniteSet): + result = list(solutions) + + else: + solution = solution_set.intersect(domain) + if isinstance(solution, Union): + # concerned about only FiniteSet with Union but not about ImageSet + # if required could be extend + if any(isinstance(i, FiniteSet) for i in solution.args): + result = [sol for soln in solution.args \ + for sol in soln.args if isinstance(soln,FiniteSet)] + else: + return None + + elif isinstance(solution, FiniteSet): + result += solution + + return result + + +############################################################################### +################################ LINSOLVE ##################################### +############################################################################### + + +def linear_coeffs(eq, *syms, dict=False): + """Return a list whose elements are the coefficients of the + corresponding symbols in the sum of terms in ``eq``. + The additive constant is returned as the last element of the + list. + + Raises + ====== + + NonlinearError + The equation contains a nonlinear term + ValueError + duplicate or unordered symbols are passed + + Parameters + ========== + + dict - (default False) when True, return coefficients as a + dictionary with coefficients keyed to syms that were present; + key 1 gives the constant term + + Examples + ======== + + >>> from sympy.solvers.solveset import linear_coeffs + >>> from sympy.abc import x, y, z + >>> linear_coeffs(3*x + 2*y - 1, x, y) + [3, 2, -1] + + It is not necessary to expand the expression: + + >>> linear_coeffs(x + y*(z*(x*3 + 2) + 3), x) + [3*y*z + 1, y*(2*z + 3)] + + When nonlinear is detected, an error will be raised: + + * even if they would cancel after expansion (so the + situation does not pass silently past the caller's + attention) + + >>> eq = 1/x*(x - 1) + 1/x + >>> linear_coeffs(eq.expand(), x) + [0, 1] + >>> linear_coeffs(eq, x) + Traceback (most recent call last): + ... + NonlinearError: + nonlinear in given generators + + * when there are cross terms + + >>> linear_coeffs(x*(y + 1), x, y) + Traceback (most recent call last): + ... + NonlinearError: + symbol-dependent cross-terms encountered + + * when there are terms that contain an expression + dependent on the symbols that is not linear + + >>> linear_coeffs(x**2, x) + Traceback (most recent call last): + ... + NonlinearError: + nonlinear in given generators + """ + eq = _sympify(eq) + if len(syms) == 1 and iterable(syms[0]) and not isinstance(syms[0], Basic): + raise ValueError('expecting unpacked symbols, *syms') + symset = set(syms) + if len(symset) != len(syms): + raise ValueError('duplicate symbols given') + try: + d, c = _linear_eq_to_dict([eq], symset) + d = d[0] + c = c[0] + except PolyNonlinearError as err: + raise NonlinearError(str(err)) + if dict: + if c: + d[S.One] = c + return d + rv = [S.Zero]*(len(syms) + 1) + rv[-1] = c + for i, k in enumerate(syms): + if k not in d: + continue + rv[i] = d[k] + return rv + + +def linear_eq_to_matrix(equations, *symbols): + r""" + Converts a given System of Equations into Matrix form. Here ``equations`` + must be a linear system of equations in ``symbols``. Element ``M[i, j]`` + corresponds to the coefficient of the jth symbol in the ith equation. + + The Matrix form corresponds to the augmented matrix form. For example: + + .. math:: + + 4x + 2y + 3z & = 1 \\ + 3x + y + z & = -6 \\ + 2x + 4y + 9z & = 2 + + This system will return :math:`A` and :math:`b` as: + + .. math:: + + A = \left[\begin{array}{ccc} + 4 & 2 & 3 \\ + 3 & 1 & 1 \\ + 2 & 4 & 9 + \end{array}\right] \\ + + .. math:: + + b = \left[\begin{array}{c} + 1 \\ -6 \\ 2 + \end{array}\right] + + The only simplification performed is to convert + ``Eq(a, b)`` :math:`\Rightarrow a - b`. + + Raises + ====== + + NonlinearError + The equations contain a nonlinear term. + ValueError + The symbols are not given or are not unique. + + Examples + ======== + + >>> from sympy import linear_eq_to_matrix, symbols + >>> c, x, y, z = symbols('c, x, y, z') + + The coefficients (numerical or symbolic) of the symbols will + be returned as matrices: + + >>> eqns = [c*x + z - 1 - c, y + z, x - y] + >>> A, b = linear_eq_to_matrix(eqns, [x, y, z]) + >>> A + Matrix([ + [c, 0, 1], + [0, 1, 1], + [1, -1, 0]]) + >>> b + Matrix([ + [c + 1], + [ 0], + [ 0]]) + + This routine does not simplify expressions and will raise an error + if nonlinearity is encountered: + + >>> eqns = [ + ... (x**2 - 3*x)/(x - 3) - 3, + ... y**2 - 3*y - y*(y - 4) + x - 4] + >>> linear_eq_to_matrix(eqns, [x, y]) + Traceback (most recent call last): + ... + NonlinearError: + symbol-dependent term can be ignored using `strict=False` + + Simplifying these equations will discard the removable singularity in the + first and reveal the linear structure of the second: + + >>> [e.simplify() for e in eqns] + [x - 3, x + y - 4] + + Any such simplification needed to eliminate nonlinear terms must be done + *before* calling this routine. + + """ + if not symbols: + raise ValueError(filldedent(''' + Symbols must be given, for which coefficients + are to be found. + ''')) + + # Check if 'symbols' is a set and raise an error if it is + if isinstance(symbols[0], set): + raise TypeError( + "Unordered 'set' type is not supported as input for symbols.") + + if hasattr(symbols[0], '__iter__'): + symbols = symbols[0] + + if has_dups(symbols): + raise ValueError('Symbols must be unique') + + equations = sympify(equations) + if isinstance(equations, MatrixBase): + equations = list(equations) + elif isinstance(equations, (Expr, Eq)): + equations = [equations] + elif not is_sequence(equations): + raise ValueError(filldedent(''' + Equation(s) must be given as a sequence, Expr, + Eq or Matrix. + ''')) + + # construct the dictionaries + try: + eq, c = _linear_eq_to_dict(equations, symbols) + except PolyNonlinearError as err: + raise NonlinearError(str(err)) + # prepare output matrices + n, m = shape = len(eq), len(symbols) + ix = dict(zip(symbols, range(m))) + A = zeros(*shape) + for row, d in enumerate(eq): + for k in d: + col = ix[k] + A[row, col] = d[k] + b = Matrix(n, 1, [-i for i in c]) + return A, b + + +def linsolve(system, *symbols): + r""" + Solve system of $N$ linear equations with $M$ variables; both + underdetermined and overdetermined systems are supported. + The possible number of solutions is zero, one or infinite. + Zero solutions throws a ValueError, whereas infinite + solutions are represented parametrically in terms of the given + symbols. For unique solution a :class:`~.FiniteSet` of ordered tuples + is returned. + + All standard input formats are supported: + For the given set of equations, the respective input types + are given below: + + .. math:: 3x + 2y - z = 1 + .. math:: 2x - 2y + 4z = -2 + .. math:: 2x - y + 2z = 0 + + * Augmented matrix form, ``system`` given below: + + $$ \text{system} = \left[{array}{cccc} + 3 & 2 & -1 & 1\\ + 2 & -2 & 4 & -2\\ + 2 & -1 & 2 & 0 + \end{array}\right] $$ + + :: + + system = Matrix([[3, 2, -1, 1], [2, -2, 4, -2], [2, -1, 2, 0]]) + + * List of equations form + + :: + + system = [3x + 2y - z - 1, 2x - 2y + 4z + 2, 2x - y + 2z] + + * Input $A$ and $b$ in matrix form (from $Ax = b$) are given as: + + $$ A = \left[\begin{array}{ccc} + 3 & 2 & -1 \\ + 2 & -2 & 4 \\ + 2 & -1 & 2 + \end{array}\right] \ \ b = \left[\begin{array}{c} + 1 \\ -2 \\ 0 + \end{array}\right] $$ + + :: + + A = Matrix([[3, 2, -1], [2, -2, 4], [2, -1, 2]]) + b = Matrix([[1], [-2], [0]]) + system = (A, b) + + Symbols can always be passed but are actually only needed + when 1) a system of equations is being passed and 2) the + system is passed as an underdetermined matrix and one wants + to control the name of the free variables in the result. + An error is raised if no symbols are used for case 1, but if + no symbols are provided for case 2, internally generated symbols + will be provided. When providing symbols for case 2, there should + be at least as many symbols are there are columns in matrix A. + + The algorithm used here is Gauss-Jordan elimination, which + results, after elimination, in a row echelon form matrix. + + Returns + ======= + + A FiniteSet containing an ordered tuple of values for the + unknowns for which the `system` has a solution. (Wrapping + the tuple in FiniteSet is used to maintain a consistent + output format throughout solveset.) + + Returns EmptySet, if the linear system is inconsistent. + + Raises + ====== + + ValueError + The input is not valid. + The symbols are not given. + + Examples + ======== + + >>> from sympy import Matrix, linsolve, symbols + >>> x, y, z = symbols("x, y, z") + >>> A = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 10]]) + >>> b = Matrix([3, 6, 9]) + >>> A + Matrix([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 10]]) + >>> b + Matrix([ + [3], + [6], + [9]]) + >>> linsolve((A, b), [x, y, z]) + {(-1, 2, 0)} + + * Parametric Solution: In case the system is underdetermined, the + function will return a parametric solution in terms of the given + symbols. Those that are free will be returned unchanged. e.g. in + the system below, `z` is returned as the solution for variable z; + it can take on any value. + + >>> A = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + >>> b = Matrix([3, 6, 9]) + >>> linsolve((A, b), x, y, z) + {(z - 1, 2 - 2*z, z)} + + If no symbols are given, internally generated symbols will be used. + The ``tau0`` in the third position indicates (as before) that the third + variable -- whatever it is named -- can take on any value: + + >>> linsolve((A, b)) + {(tau0 - 1, 2 - 2*tau0, tau0)} + + * List of equations as input + + >>> Eqns = [3*x + 2*y - z - 1, 2*x - 2*y + 4*z + 2, - x + y/2 - z] + >>> linsolve(Eqns, x, y, z) + {(1, -2, -2)} + + * Augmented matrix as input + + >>> aug = Matrix([[2, 1, 3, 1], [2, 6, 8, 3], [6, 8, 18, 5]]) + >>> aug + Matrix([ + [2, 1, 3, 1], + [2, 6, 8, 3], + [6, 8, 18, 5]]) + >>> linsolve(aug, x, y, z) + {(3/10, 2/5, 0)} + + * Solve for symbolic coefficients + + >>> a, b, c, d, e, f = symbols('a, b, c, d, e, f') + >>> eqns = [a*x + b*y - c, d*x + e*y - f] + >>> linsolve(eqns, x, y) + {((-b*f + c*e)/(a*e - b*d), (a*f - c*d)/(a*e - b*d))} + + * A degenerate system returns solution as set of given + symbols. + + >>> system = Matrix(([0, 0, 0], [0, 0, 0], [0, 0, 0])) + >>> linsolve(system, x, y) + {(x, y)} + + * For an empty system linsolve returns empty set + + >>> linsolve([], x) + EmptySet + + * An error is raised if any nonlinearity is detected, even + if it could be removed with expansion + + >>> linsolve([x*(1/x - 1)], x) + Traceback (most recent call last): + ... + NonlinearError: nonlinear term: 1/x + + >>> linsolve([x*(y + 1)], x, y) + Traceback (most recent call last): + ... + NonlinearError: nonlinear cross-term: x*(y + 1) + + >>> linsolve([x**2 - 1], x) + Traceback (most recent call last): + ... + NonlinearError: nonlinear term: x**2 + """ + if not system: + return S.EmptySet + + # If second argument is an iterable + if symbols and hasattr(symbols[0], '__iter__'): + symbols = symbols[0] + sym_gen = isinstance(symbols, GeneratorType) + dup_msg = 'duplicate symbols given' + + + b = None # if we don't get b the input was bad + # unpack system + + if hasattr(system, '__iter__'): + + # 1). (A, b) + if len(system) == 2 and isinstance(system[0], MatrixBase): + A, b = system + + # 2). (eq1, eq2, ...) + if not isinstance(system[0], MatrixBase): + if sym_gen or not symbols: + raise ValueError(filldedent(''' + When passing a system of equations, the explicit + symbols for which a solution is being sought must + be given as a sequence, too. + ''')) + if len(set(symbols)) != len(symbols): + raise ValueError(dup_msg) + + # + # Pass to the sparse solver implemented in polys. It is important + # that we do not attempt to convert the equations to a matrix + # because that would be very inefficient for large sparse systems + # of equations. + # + eqs = system + eqs = [sympify(eq) for eq in eqs] + try: + sol = _linsolve(eqs, symbols) + except PolyNonlinearError as exc: + # e.g. cos(x) contains an element of the set of generators + raise NonlinearError(str(exc)) + + if sol is None: + return S.EmptySet + + sol = FiniteSet(Tuple(*(sol.get(sym, sym) for sym in symbols))) + return sol + + elif isinstance(system, MatrixBase) and not ( + symbols and not isinstance(symbols, GeneratorType) and + isinstance(symbols[0], MatrixBase)): + # 3). A augmented with b + A, b = system[:, :-1], system[:, -1:] + + if b is None: + raise ValueError("Invalid arguments") + if sym_gen: + symbols = [next(symbols) for i in range(A.cols)] + symset = set(symbols) + if any(symset & (A.free_symbols | b.free_symbols)): + raise ValueError(filldedent(''' + At least one of the symbols provided + already appears in the system to be solved. + One way to avoid this is to use Dummy symbols in + the generator, e.g. numbered_symbols('%s', cls=Dummy) + ''' % symbols[0].name.rstrip('1234567890'))) + elif len(symset) != len(symbols): + raise ValueError(dup_msg) + + if not symbols: + symbols = [Dummy() for _ in range(A.cols)] + name = _uniquely_named_symbol('tau', (A, b), + compare=lambda i: str(i).rstrip('1234567890')).name + gen = numbered_symbols(name) + else: + gen = None + + # This is just a wrapper for solve_lin_sys + eqs = [] + rows = A.tolist() + for rowi, bi in zip(rows, b): + terms = [elem * sym for elem, sym in zip(rowi, symbols) if elem] + terms.append(-bi) + eqs.append(Add(*terms)) + + eqs, ring = sympy_eqs_to_ring(eqs, symbols) + sol = solve_lin_sys(eqs, ring, _raw=False) + if sol is None: + return S.EmptySet + #sol = {sym:val for sym, val in sol.items() if sym != val} + sol = FiniteSet(Tuple(*(sol.get(sym, sym) for sym in symbols))) + + if gen is not None: + solsym = sol.free_symbols + rep = {sym: next(gen) for sym in symbols if sym in solsym} + sol = sol.subs(rep) + + return sol + + +############################################################################## +# ------------------------------nonlinsolve ---------------------------------# +############################################################################## + + +def _return_conditionset(eqs, symbols): + # return conditionset + eqs = (Eq(lhs, 0) for lhs in eqs) + condition_set = ConditionSet( + Tuple(*symbols), And(*eqs), S.Complexes**len(symbols)) + return condition_set + + +def substitution(system, symbols, result=[{}], known_symbols=[], + exclude=[], all_symbols=None): + r""" + Solves the `system` using substitution method. It is used in + :func:`~.nonlinsolve`. This will be called from :func:`~.nonlinsolve` when any + equation(s) is non polynomial equation. + + Parameters + ========== + + system : list of equations + The target system of equations + symbols : list of symbols to be solved. + The variable(s) for which the system is solved + known_symbols : list of solved symbols + Values are known for these variable(s) + result : An empty list or list of dict + If No symbol values is known then empty list otherwise + symbol as keys and corresponding value in dict. + exclude : Set of expression. + Mostly denominator expression(s) of the equations of the system. + Final solution should not satisfy these expressions. + all_symbols : known_symbols + symbols(unsolved). + + Returns + ======= + + A FiniteSet of ordered tuple of values of `all_symbols` for which the + `system` has solution. Order of values in the tuple is same as symbols + present in the parameter `all_symbols`. If parameter `all_symbols` is None + then same as symbols present in the parameter `symbols`. + + Please note that general FiniteSet is unordered, the solution returned + here is not simply a FiniteSet of solutions, rather it is a FiniteSet of + ordered tuple, i.e. the first & only argument to FiniteSet is a tuple of + solutions, which is ordered, & hence the returned solution is ordered. + + Also note that solution could also have been returned as an ordered tuple, + FiniteSet is just a wrapper `{}` around the tuple. It has no other + significance except for the fact it is just used to maintain a consistent + output format throughout the solveset. + + Raises + ====== + + ValueError + The input is not valid. + The symbols are not given. + AttributeError + The input symbols are not :class:`~.Symbol` type. + + Examples + ======== + + >>> from sympy import symbols, substitution + >>> x, y = symbols('x, y', real=True) + >>> substitution([x + y], [x], [{y: 1}], [y], set([]), [x, y]) + {(-1, 1)} + + * When you want a soln not satisfying $x + 1 = 0$ + + >>> substitution([x + y], [x], [{y: 1}], [y], set([x + 1]), [y, x]) + EmptySet + >>> substitution([x + y], [x], [{y: 1}], [y], set([x - 1]), [y, x]) + {(1, -1)} + >>> substitution([x + y - 1, y - x**2 + 5], [x, y]) + {(-3, 4), (2, -1)} + + * Returns both real and complex solution + + >>> x, y, z = symbols('x, y, z') + >>> from sympy import exp, sin + >>> substitution([exp(x) - sin(y), y**2 - 4], [x, y]) + {(ImageSet(Lambda(_n, I*(2*_n*pi + pi) + log(sin(2))), Integers), -2), + (ImageSet(Lambda(_n, 2*_n*I*pi + log(sin(2))), Integers), 2)} + + >>> eqs = [z**2 + exp(2*x) - sin(y), -3 + exp(-y)] + >>> substitution(eqs, [y, z]) + {(-log(3), -sqrt(-exp(2*x) - sin(log(3)))), + (-log(3), sqrt(-exp(2*x) - sin(log(3)))), + (ImageSet(Lambda(_n, 2*_n*I*pi - log(3)), Integers), + ImageSet(Lambda(_n, -sqrt(-exp(2*x) + sin(2*_n*I*pi - log(3)))), Integers)), + (ImageSet(Lambda(_n, 2*_n*I*pi - log(3)), Integers), + ImageSet(Lambda(_n, sqrt(-exp(2*x) + sin(2*_n*I*pi - log(3)))), Integers))} + + """ + + if not system: + return S.EmptySet + + for i, e in enumerate(system): + if isinstance(e, Eq): + system[i] = e.lhs - e.rhs + + if not symbols: + msg = ('Symbols must be given, for which solution of the ' + 'system is to be found.') + raise ValueError(filldedent(msg)) + + if not is_sequence(symbols): + msg = ('symbols should be given as a sequence, e.g. a list.' + 'Not type %s: %s') + raise TypeError(filldedent(msg % (type(symbols), symbols))) + + if not getattr(symbols[0], 'is_Symbol', False): + msg = ('Iterable of symbols must be given as ' + 'second argument, not type %s: %s') + raise ValueError(filldedent(msg % (type(symbols[0]), symbols[0]))) + + # By default `all_symbols` will be same as `symbols` + if all_symbols is None: + all_symbols = symbols + + old_result = result + # storing complements and intersection for particular symbol + complements = {} + intersections = {} + + # when total_solveset_call equals total_conditionset + # it means that solveset failed to solve all eqs. + total_conditionset = -1 + total_solveset_call = -1 + + def _unsolved_syms(eq, sort=False): + """Returns the unsolved symbol present + in the equation `eq`. + """ + free = eq.free_symbols + unsolved = (free - set(known_symbols)) & set(all_symbols) + if sort: + unsolved = list(unsolved) + unsolved.sort(key=default_sort_key) + return unsolved + + # sort such that equation with the fewest potential symbols is first. + # means eq with less number of variable first in the list. + eqs_in_better_order = list( + ordered(system, lambda _: len(_unsolved_syms(_)))) + + def add_intersection_complement(result, intersection_dict, complement_dict): + # If solveset has returned some intersection/complement + # for any symbol, it will be added in the final solution. + final_result = [] + for res in result: + res_copy = res + for key_res, value_res in res.items(): + intersect_set, complement_set = None, None + for key_sym, value_sym in intersection_dict.items(): + if key_sym == key_res: + intersect_set = value_sym + for key_sym, value_sym in complement_dict.items(): + if key_sym == key_res: + complement_set = value_sym + if intersect_set or complement_set: + new_value = FiniteSet(value_res) + if intersect_set and intersect_set != S.Complexes: + new_value = Intersection(new_value, intersect_set) + if complement_set: + new_value = Complement(new_value, complement_set) + if new_value is S.EmptySet: + res_copy = None + break + elif new_value.is_FiniteSet and len(new_value) == 1: + res_copy[key_res] = set(new_value).pop() + else: + res_copy[key_res] = new_value + + if res_copy is not None: + final_result.append(res_copy) + return final_result + + def _extract_main_soln(sym, sol, soln_imageset): + """Separate the Complements, Intersections, ImageSet lambda expr and + its base_set. This function returns the unmasked sol from different classes + of sets and also returns the appended ImageSet elements in a + soln_imageset dict: `{unmasked element: ImageSet}`. + """ + # if there is union, then need to check + # Complement, Intersection, Imageset. + # Order should not be changed. + if isinstance(sol, ConditionSet): + # extracts any solution in ConditionSet + sol = sol.base_set + + if isinstance(sol, Complement): + # extract solution and complement + complements[sym] = sol.args[1] + sol = sol.args[0] + # complement will be added at the end + # using `add_intersection_complement` method + + # if there is union of Imageset or other in soln. + # no testcase is written for this if block + if isinstance(sol, Union): + sol_args = sol.args + sol = S.EmptySet + # We need in sequence so append finteset elements + # and then imageset or other. + for sol_arg2 in sol_args: + if isinstance(sol_arg2, FiniteSet): + sol += sol_arg2 + else: + # ImageSet, Intersection, complement then + # append them directly + sol += FiniteSet(sol_arg2) + + if isinstance(sol, Intersection): + # Interval/Set will be at 0th index always + if sol.args[0] not in (S.Reals, S.Complexes): + # Sometimes solveset returns soln with intersection + # S.Reals or S.Complexes. We don't consider that + # intersection. + intersections[sym] = sol.args[0] + sol = sol.args[1] + # after intersection and complement Imageset should + # be checked. + if isinstance(sol, ImageSet): + soln_imagest = sol + expr2 = sol.lamda.expr + sol = FiniteSet(expr2) + soln_imageset[expr2] = soln_imagest + + if not isinstance(sol, FiniteSet): + sol = FiniteSet(sol) + return sol, soln_imageset + + def _check_exclude(rnew, imgset_yes): + rnew_ = rnew + if imgset_yes: + # replace all dummy variables (Imageset lambda variables) + # with zero before `checksol`. Considering fundamental soln + # for `checksol`. + rnew_copy = rnew.copy() + dummy_n = imgset_yes[0] + for key_res, value_res in rnew_copy.items(): + rnew_copy[key_res] = value_res.subs(dummy_n, 0) + rnew_ = rnew_copy + # satisfy_exclude == true if it satisfies the expr of `exclude` list. + try: + # something like : `Mod(-log(3), 2*I*pi)` can't be + # simplified right now, so `checksol` returns `TypeError`. + # when this issue is fixed this try block should be + # removed. Mod(-log(3), 2*I*pi) == -log(3) + satisfy_exclude = any( + checksol(d, rnew_) for d in exclude) + except TypeError: + satisfy_exclude = None + return satisfy_exclude + + def _restore_imgset(rnew, original_imageset, newresult): + restore_sym = set(rnew.keys()) & \ + set(original_imageset.keys()) + for key_sym in restore_sym: + img = original_imageset[key_sym] + rnew[key_sym] = img + if rnew not in newresult: + newresult.append(rnew) + + def _append_eq(eq, result, res, delete_soln, n=None): + u = Dummy('u') + if n: + eq = eq.subs(n, 0) + satisfy = eq if eq in (True, False) else checksol(u, u, eq, minimal=True) + if satisfy is False: + delete_soln = True + res = {} + else: + result.append(res) + return result, res, delete_soln + + def _append_new_soln(rnew, sym, sol, imgset_yes, soln_imageset, + original_imageset, newresult, eq=None): + """If `rnew` (A dict ) contains valid soln + append it to `newresult` list. + `imgset_yes` is (base, dummy_var) if there was imageset in previously + calculated result(otherwise empty tuple). `original_imageset` is dict + of imageset expr and imageset from this result. + `soln_imageset` dict of imageset expr and imageset of new soln. + """ + satisfy_exclude = _check_exclude(rnew, imgset_yes) + delete_soln = False + # soln should not satisfy expr present in `exclude` list. + if not satisfy_exclude: + local_n = None + # if it is imageset + if imgset_yes: + local_n = imgset_yes[0] + base = imgset_yes[1] + if sym and sol: + # when `sym` and `sol` is `None` means no new + # soln. In that case we will append rnew directly after + # substituting original imagesets in rnew values if present + # (second last line of this function using _restore_imgset) + dummy_list = list(sol.atoms(Dummy)) + # use one dummy `n` which is in + # previous imageset + local_n_list = [ + local_n for i in range( + 0, len(dummy_list))] + + dummy_zip = zip(dummy_list, local_n_list) + lam = Lambda(local_n, sol.subs(dummy_zip)) + rnew[sym] = ImageSet(lam, base) + if eq is not None: + newresult, rnew, delete_soln = _append_eq( + eq, newresult, rnew, delete_soln, local_n) + elif eq is not None: + newresult, rnew, delete_soln = _append_eq( + eq, newresult, rnew, delete_soln) + elif sol in soln_imageset.keys(): + rnew[sym] = soln_imageset[sol] + # restore original imageset + _restore_imgset(rnew, original_imageset, newresult) + else: + newresult.append(rnew) + elif satisfy_exclude: + delete_soln = True + rnew = {} + _restore_imgset(rnew, original_imageset, newresult) + return newresult, delete_soln + + def _new_order_result(result, eq): + # separate first, second priority. `res` that makes `eq` value equals + # to zero, should be used first then other result(second priority). + # If it is not done then we may miss some soln. + first_priority = [] + second_priority = [] + for res in result: + if not any(isinstance(val, ImageSet) for val in res.values()): + if eq.subs(res) == 0: + first_priority.append(res) + else: + second_priority.append(res) + if first_priority or second_priority: + return first_priority + second_priority + return result + + def _solve_using_known_values(result, solver): + """Solves the system using already known solution + (result contains the dict ). + solver is :func:`~.solveset_complex` or :func:`~.solveset_real`. + """ + # stores imageset . + soln_imageset = {} + total_solvest_call = 0 + total_conditionst = 0 + + # sort equations so the one with the fewest potential + # symbols appears first + for index, eq in enumerate(eqs_in_better_order): + newresult = [] + # if imageset, expr is used to solve for other symbol + imgset_yes = False + for res in result: + original_imageset = {} + got_symbol = set() # symbols solved in one iteration + # find the imageset and use its expr. + for k, v in res.items(): + if isinstance(v, ImageSet): + res[k] = v.lamda.expr + original_imageset[k] = v + dummy_n = v.lamda.expr.atoms(Dummy).pop() + (base,) = v.base_sets + imgset_yes = (dummy_n, base) + assert not isinstance(v, FiniteSet) # if so, internal error + # update eq with everything that is known so far + eq2 = eq.subs(res).expand() + if imgset_yes and not eq2.has(imgset_yes[0]): + # The substituted equation simplified in such a way that + # it's no longer necessary to encapsulate a potential new + # solution in an ImageSet. (E.g. at the previous step some + # {n*2*pi} was found as partial solution for one of the + # unknowns, but its main solution expression n*2*pi has now + # been substituted in a trigonometric function.) + imgset_yes = False + + unsolved_syms = _unsolved_syms(eq2, sort=True) + if not unsolved_syms: + if res: + newresult, delete_res = _append_new_soln( + res, None, None, imgset_yes, soln_imageset, + original_imageset, newresult, eq2) + if delete_res: + # `delete_res` is true, means substituting `res` in + # eq2 doesn't return `zero` or deleting the `res` + # (a soln) since it satisfies expr of `exclude` + # list. + result.remove(res) + continue # skip as it's independent of desired symbols + depen1, depen2 = eq2.as_independent(*unsolved_syms) + if (depen1.has(Abs) or depen2.has(Abs)) and solver == solveset_complex: + # Absolute values cannot be inverted in the + # complex domain + continue + soln_imageset = {} + for sym in unsolved_syms: + not_solvable = False + try: + soln = solver(eq2, sym) + total_solvest_call += 1 + soln_new = S.EmptySet + if isinstance(soln, Complement): + # separate solution and complement + complements[sym] = soln.args[1] + soln = soln.args[0] + # complement will be added at the end + if isinstance(soln, Intersection): + # Interval will be at 0th index always + if soln.args[0] != Interval(-oo, oo): + # sometimes solveset returns soln + # with intersection S.Reals, to confirm that + # soln is in domain=S.Reals + intersections[sym] = soln.args[0] + soln_new += soln.args[1] + soln = soln_new if soln_new else soln + if index > 0 and solver == solveset_real: + # one symbol's real soln, another symbol may have + # corresponding complex soln. + if not isinstance(soln, (ImageSet, ConditionSet)): + soln += solveset_complex(eq2, sym) # might give ValueError with Abs + except (NotImplementedError, ValueError): + # If solveset is not able to solve equation `eq2`. Next + # time we may get soln using next equation `eq2` + continue + if isinstance(soln, ConditionSet): + if soln.base_set in (S.Reals, S.Complexes): + soln = S.EmptySet + # don't do `continue` we may get soln + # in terms of other symbol(s) + not_solvable = True + total_conditionst += 1 + else: + soln = soln.base_set + + if soln is not S.EmptySet: + soln, soln_imageset = _extract_main_soln( + sym, soln, soln_imageset) + + for sol in soln: + # sol is not a `Union` since we checked it + # before this loop + sol, soln_imageset = _extract_main_soln( + sym, sol, soln_imageset) + sol = set(sol).pop() # XXX what if there are more solutions? + free = sol.free_symbols + if got_symbol and any( + ss in free for ss in got_symbol + ): + # sol depends on previously solved symbols + # then continue + continue + rnew = res.copy() + # put each solution in res and append the new result + # in the new result list (solution for symbol `s`) + # along with old results. + for k, v in res.items(): + if isinstance(v, Expr) and isinstance(sol, Expr): + # if any unsolved symbol is present + # Then subs known value + rnew[k] = v.subs(sym, sol) + # and add this new solution + if sol in soln_imageset.keys(): + # replace all lambda variables with 0. + imgst = soln_imageset[sol] + rnew[sym] = imgst.lamda( + *[0 for i in range(0, len( + imgst.lamda.variables))]) + else: + rnew[sym] = sol + newresult, delete_res = _append_new_soln( + rnew, sym, sol, imgset_yes, soln_imageset, + original_imageset, newresult) + if delete_res: + # deleting the `res` (a soln) since it satisfies + # eq of `exclude` list + result.remove(res) + # solution got for sym + if not not_solvable: + got_symbol.add(sym) + # next time use this new soln + if newresult: + result = newresult + return result, total_solvest_call, total_conditionst + + new_result_real, solve_call1, cnd_call1 = _solve_using_known_values( + old_result, solveset_real) + new_result_complex, solve_call2, cnd_call2 = _solve_using_known_values( + old_result, solveset_complex) + + # If total_solveset_call is equal to total_conditionset + # then solveset failed to solve all of the equations. + # In this case we return a ConditionSet here. + total_conditionset += (cnd_call1 + cnd_call2) + total_solveset_call += (solve_call1 + solve_call2) + + if total_conditionset == total_solveset_call and total_solveset_call != -1: + return _return_conditionset(eqs_in_better_order, all_symbols) + + # don't keep duplicate solutions + filtered_complex = [] + for i in list(new_result_complex): + for j in list(new_result_real): + if i.keys() != j.keys(): + continue + if all(a.dummy_eq(b) for a, b in zip(i.values(), j.values()) \ + if not (isinstance(a, int) and isinstance(b, int))): + break + else: + filtered_complex.append(i) + # overall result + result = new_result_real + filtered_complex + + result_all_variables = [] + result_infinite = [] + for res in result: + if not res: + # means {None : None} + continue + # If length < len(all_symbols) means infinite soln. + # Some or all the soln is dependent on 1 symbol. + # eg. {x: y+2} then final soln {x: y+2, y: y} + if len(res) < len(all_symbols): + solved_symbols = res.keys() + unsolved = list(filter( + lambda x: x not in solved_symbols, all_symbols)) + for unsolved_sym in unsolved: + res[unsolved_sym] = unsolved_sym + result_infinite.append(res) + if res not in result_all_variables: + result_all_variables.append(res) + + if result_infinite: + # we have general soln + # eg : [{x: -1, y : 1}, {x : -y, y: y}] then + # return [{x : -y, y : y}] + result_all_variables = result_infinite + if intersections or complements: + result_all_variables = add_intersection_complement( + result_all_variables, intersections, complements) + + # convert to ordered tuple + result = S.EmptySet + for r in result_all_variables: + temp = [r[symb] for symb in all_symbols] + result += FiniteSet(tuple(temp)) + return result + + +def _solveset_work(system, symbols): + soln = solveset(system[0], symbols[0]) + if isinstance(soln, FiniteSet): + _soln = FiniteSet(*[(s,) for s in soln]) + return _soln + else: + return FiniteSet(tuple(FiniteSet(soln))) + + +def _handle_positive_dimensional(polys, symbols, denominators): + from sympy.polys.polytools import groebner + # substitution method where new system is groebner basis of the system + _symbols = list(symbols) + _symbols.sort(key=default_sort_key) + basis = groebner(polys, _symbols, polys=True) + new_system = [] + for poly_eq in basis: + new_system.append(poly_eq.as_expr()) + result = [{}] + result = substitution( + new_system, symbols, result, [], + denominators) + return result + + +def _handle_zero_dimensional(polys, symbols, system): + # solve 0 dimensional poly system using `solve_poly_system` + result = solve_poly_system(polys, *symbols) + # May be some extra soln is added because + # we used `unrad` in `_separate_poly_nonpoly`, so + # need to check and remove if it is not a soln. + result_update = S.EmptySet + for res in result: + dict_sym_value = dict(list(zip(symbols, res))) + if all(checksol(eq, dict_sym_value) for eq in system): + result_update += FiniteSet(res) + return result_update + + +def _separate_poly_nonpoly(system, symbols): + polys = [] + polys_expr = [] + nonpolys = [] + # unrad_changed stores a list of expressions containing + # radicals that were processed using unrad + # this is useful if solutions need to be checked later. + unrad_changed = [] + denominators = set() + poly = None + for eq in system: + # Store denom expressions that contain symbols + denominators.update(_simple_dens(eq, symbols)) + # Convert equality to expression + if isinstance(eq, Eq): + eq = eq.lhs - eq.rhs + # try to remove sqrt and rational power + without_radicals = unrad(simplify(eq), *symbols) + if without_radicals: + unrad_changed.append(eq) + eq_unrad, cov = without_radicals + if not cov: + eq = eq_unrad + if isinstance(eq, Expr): + eq = eq.as_numer_denom()[0] + poly = eq.as_poly(*symbols, extension=True) + elif simplify(eq).is_number: + continue + if poly is not None: + polys.append(poly) + polys_expr.append(poly.as_expr()) + else: + nonpolys.append(eq) + return polys, polys_expr, nonpolys, denominators, unrad_changed + + +def _handle_poly(polys, symbols): + # _handle_poly(polys, symbols) -> (poly_sol, poly_eqs) + # + # We will return possible solution information to nonlinsolve as well as a + # new system of polynomial equations to be solved if we cannot solve + # everything directly here. The new system of polynomial equations will be + # a lex-order Groebner basis for the original system. The lex basis + # hopefully separate some of the variables and equations and give something + # easier for substitution to work with. + + # The format for representing solution sets in nonlinsolve and substitution + # is a list of dicts. These are the special cases: + no_information = [{}] # No equations solved yet + no_solutions = [] # The system is inconsistent and has no solutions. + + # If there is no need to attempt further solution of these equations then + # we return no equations: + no_equations = [] + + inexact = any(not p.domain.is_Exact for p in polys) + if inexact: + # The use of Groebner over RR is likely to result incorrectly in an + # inconsistent Groebner basis. So, convert any float coefficients to + # Rational before computing the Groebner basis. + polys = [poly(nsimplify(p, rational=True)) for p in polys] + + # Compute a Groebner basis in grevlex order wrt the ordering given. We will + # try to convert this to lex order later. Usually it seems to be more + # efficient to compute a lex order basis by computing a grevlex basis and + # converting to lex with fglm. + basis = groebner(polys, symbols, order='grevlex', polys=False) + + # + # No solutions (inconsistent equations)? + # + if 1 in basis: + + # No solutions: + poly_sol = no_solutions + poly_eqs = no_equations + + # + # Finite number of solutions (zero-dimensional case) + # + elif basis.is_zero_dimensional: + + # Convert Groebner basis to lex ordering + basis = basis.fglm('lex') + + # Convert polynomial coefficients back to float before calling + # solve_poly_system + if inexact: + basis = [nfloat(p) for p in basis] + + # Solve the zero-dimensional case using solve_poly_system if possible. + # If some polynomials have factors that cannot be solved in radicals + # then this will fail. Using solve_poly_system(..., strict=True) + # ensures that we either get a complete solution set in radicals or + # UnsolvableFactorError will be raised. + try: + result = solve_poly_system(basis, *symbols, strict=True) + except UnsolvableFactorError: + # Failure... not fully solvable in radicals. Return the lex-order + # basis for substitution to handle. + poly_sol = no_information + poly_eqs = list(basis) + else: + # Success! We have a finite solution set and solve_poly_system has + # succeeded in finding all solutions. Return the solutions and also + # an empty list of remaining equations to be solved. + poly_sol = [dict(zip(symbols, res)) for res in result] + poly_eqs = no_equations + + # + # Infinite families of solutions (positive-dimensional case) + # + else: + # In this case the grevlex basis cannot be converted to lex using the + # fglm method and also solve_poly_system cannot solve the equations. We + # would like to return a lex basis but since we can't use fglm we + # compute the lex basis directly here. The time required to recompute + # the basis is generally significantly less than the time required by + # substitution to solve the new system. + poly_sol = no_information + poly_eqs = list(groebner(polys, symbols, order='lex', polys=False)) + + if inexact: + poly_eqs = [nfloat(p) for p in poly_eqs] + + return poly_sol, poly_eqs + + +def nonlinsolve(system, *symbols): + r""" + Solve system of $N$ nonlinear equations with $M$ variables, which means both + under and overdetermined systems are supported. Positive dimensional + system is also supported (A system with infinitely many solutions is said + to be positive-dimensional). In a positive dimensional system the solution will + be dependent on at least one symbol. Returns both real solution + and complex solution (if they exist). + + Parameters + ========== + + system : list of equations + The target system of equations + symbols : list of Symbols + symbols should be given as a sequence eg. list + + Returns + ======= + + A :class:`~.FiniteSet` of ordered tuple of values of `symbols` for which the `system` + has solution. Order of values in the tuple is same as symbols present in + the parameter `symbols`. + + Please note that general :class:`~.FiniteSet` is unordered, the solution + returned here is not simply a :class:`~.FiniteSet` of solutions, rather it + is a :class:`~.FiniteSet` of ordered tuple, i.e. the first and only + argument to :class:`~.FiniteSet` is a tuple of solutions, which is + ordered, and, hence ,the returned solution is ordered. + + Also note that solution could also have been returned as an ordered tuple, + FiniteSet is just a wrapper ``{}`` around the tuple. It has no other + significance except for the fact it is just used to maintain a consistent + output format throughout the solveset. + + For the given set of equations, the respective input types + are given below: + + .. math:: xy - 1 = 0 + .. math:: 4x^2 + y^2 - 5 = 0 + + :: + + system = [x*y - 1, 4*x**2 + y**2 - 5] + symbols = [x, y] + + Raises + ====== + + ValueError + The input is not valid. + The symbols are not given. + AttributeError + The input symbols are not `Symbol` type. + + Examples + ======== + + >>> from sympy import symbols, nonlinsolve + >>> x, y, z = symbols('x, y, z', real=True) + >>> nonlinsolve([x*y - 1, 4*x**2 + y**2 - 5], [x, y]) + {(-1, -1), (-1/2, -2), (1/2, 2), (1, 1)} + + 1. Positive dimensional system and complements: + + >>> from sympy import pprint + >>> from sympy.polys.polytools import is_zero_dimensional + >>> a, b, c, d = symbols('a, b, c, d', extended_real=True) + >>> eq1 = a + b + c + d + >>> eq2 = a*b + b*c + c*d + d*a + >>> eq3 = a*b*c + b*c*d + c*d*a + d*a*b + >>> eq4 = a*b*c*d - 1 + >>> system = [eq1, eq2, eq3, eq4] + >>> is_zero_dimensional(system) + False + >>> pprint(nonlinsolve(system, [a, b, c, d]), use_unicode=False) + -1 1 1 -1 + {(---, -d, -, {d} \ {0}), (-, -d, ---, {d} \ {0})} + d d d d + >>> nonlinsolve([(x+y)**2 - 4, x + y - 2], [x, y]) + {(2 - y, y)} + + 2. If some of the equations are non-polynomial then `nonlinsolve` + will call the ``substitution`` function and return real and complex solutions, + if present. + + >>> from sympy import exp, sin + >>> nonlinsolve([exp(x) - sin(y), y**2 - 4], [x, y]) + {(ImageSet(Lambda(_n, I*(2*_n*pi + pi) + log(sin(2))), Integers), -2), + (ImageSet(Lambda(_n, 2*_n*I*pi + log(sin(2))), Integers), 2)} + + 3. If system is non-linear polynomial and zero-dimensional then it + returns both solution (real and complex solutions, if present) using + :func:`~.solve_poly_system`: + + >>> from sympy import sqrt + >>> nonlinsolve([x**2 - 2*y**2 -2, x*y - 2], [x, y]) + {(-2, -1), (2, 1), (-sqrt(2)*I, sqrt(2)*I), (sqrt(2)*I, -sqrt(2)*I)} + + 4. ``nonlinsolve`` can solve some linear (zero or positive dimensional) + system (because it uses the :func:`sympy.polys.polytools.groebner` function to get the + groebner basis and then uses the ``substitution`` function basis as the + new `system`). But it is not recommended to solve linear system using + ``nonlinsolve``, because :func:`~.linsolve` is better for general linear systems. + + >>> nonlinsolve([x + 2*y -z - 3, x - y - 4*z + 9, y + z - 4], [x, y, z]) + {(3*z - 5, 4 - z, z)} + + 5. System having polynomial equations and only real solution is + solved using :func:`~.solve_poly_system`: + + >>> e1 = sqrt(x**2 + y**2) - 10 + >>> e2 = sqrt(y**2 + (-x + 10)**2) - 3 + >>> nonlinsolve((e1, e2), (x, y)) + {(191/20, -3*sqrt(391)/20), (191/20, 3*sqrt(391)/20)} + >>> nonlinsolve([x**2 + 2/y - 2, x + y - 3], [x, y]) + {(1, 2), (1 - sqrt(5), 2 + sqrt(5)), (1 + sqrt(5), 2 - sqrt(5))} + >>> nonlinsolve([x**2 + 2/y - 2, x + y - 3], [y, x]) + {(2, 1), (2 - sqrt(5), 1 + sqrt(5)), (2 + sqrt(5), 1 - sqrt(5))} + + 6. It is better to use symbols instead of trigonometric functions or + :class:`~.Function`. For example, replace $\sin(x)$ with a symbol, replace + $f(x)$ with a symbol and so on. Get a solution from ``nonlinsolve`` and then + use :func:`~.solveset` to get the value of $x$. + + How nonlinsolve is better than old solver ``_solve_system`` : + ============================================================= + + 1. A positive dimensional system solver: nonlinsolve can return + solution for positive dimensional system. It finds the + Groebner Basis of the positive dimensional system(calling it as + basis) then we can start solving equation(having least number of + variable first in the basis) using solveset and substituting that + solved solutions into other equation(of basis) to get solution in + terms of minimum variables. Here the important thing is how we + are substituting the known values and in which equations. + + 2. Real and complex solutions: nonlinsolve returns both real + and complex solution. If all the equations in the system are polynomial + then using :func:`~.solve_poly_system` both real and complex solution is returned. + If all the equations in the system are not polynomial equation then goes to + ``substitution`` method with this polynomial and non polynomial equation(s), + to solve for unsolved variables. Here to solve for particular variable + solveset_real and solveset_complex is used. For both real and complex + solution ``_solve_using_known_values`` is used inside ``substitution`` + (``substitution`` will be called when any non-polynomial equation is present). + If a solution is valid its general solution is added to the final result. + + 3. :class:`~.Complement` and :class:`~.Intersection` will be added: + nonlinsolve maintains dict for complements and intersections. If solveset + find complements or/and intersections with any interval or set during the + execution of ``substitution`` function, then complement or/and + intersection for that variable is added before returning final solution. + + """ + if not system: + return S.EmptySet + + if not symbols: + msg = ('Symbols must be given, for which solution of the ' + 'system is to be found.') + raise ValueError(filldedent(msg)) + + if hasattr(symbols[0], '__iter__'): + symbols = symbols[0] + + if not is_sequence(symbols) or not symbols: + msg = ('Symbols must be given, for which solution of the ' + 'system is to be found.') + raise IndexError(filldedent(msg)) + + symbols = list(map(_sympify, symbols)) + system, symbols, swap = recast_to_symbols(system, symbols) + if swap: + soln = nonlinsolve(system, symbols) + return FiniteSet(*[tuple(i.xreplace(swap) for i in s) for s in soln]) + + if len(system) == 1 and len(symbols) == 1: + return _solveset_work(system, symbols) + + # main code of def nonlinsolve() starts from here + + polys, polys_expr, nonpolys, denominators, unrad_changed = \ + _separate_poly_nonpoly(system, symbols) + + poly_eqs = [] + poly_sol = [{}] + + if polys: + poly_sol, poly_eqs = _handle_poly(polys, symbols) + if poly_sol and poly_sol[0]: + poly_syms = set().union(*(eq.free_symbols for eq in polys)) + unrad_syms = set().union(*(eq.free_symbols for eq in unrad_changed)) + if unrad_syms == poly_syms and unrad_changed: + # if all the symbols have been solved by _handle_poly + # and unrad has been used then check solutions + poly_sol = [sol for sol in poly_sol if checksol(unrad_changed, sol)] + + # Collect together the unsolved polynomials with the non-polynomial + # equations. + remaining = poly_eqs + nonpolys + + # to_tuple converts a solution dictionary to a tuple containing the + # value for each symbol + to_tuple = lambda sol: tuple(sol[s] for s in symbols) + + if not remaining: + # If there is nothing left to solve then return the solution from + # solve_poly_system directly. + return FiniteSet(*map(to_tuple, poly_sol)) + else: + # Here we handle: + # + # 1. The Groebner basis if solve_poly_system failed. + # 2. The Groebner basis in the positive-dimensional case. + # 3. Any non-polynomial equations + # + # If solve_poly_system did succeed then we pass those solutions in as + # preliminary results. + subs_res = substitution(remaining, symbols, result=poly_sol, exclude=denominators) + + if not isinstance(subs_res, FiniteSet): + return subs_res + + # check solutions produced by substitution. Currently, checking is done for + # only those solutions which have non-Set variable values. + if unrad_changed: + result = [dict(zip(symbols, sol)) for sol in subs_res.args] + correct_sols = [sol for sol in result if any(isinstance(v, Set) for v in sol) + or checksol(unrad_changed, sol) != False] + return FiniteSet(*map(to_tuple, correct_sols)) + else: + return subs_res diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_constantsimp.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_constantsimp.py new file mode 100644 index 0000000000000000000000000000000000000000..efb966a4c8c2f93558d05e7c330f06530e69180c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_constantsimp.py @@ -0,0 +1,179 @@ +""" +If the arbitrary constant class from issue 4435 is ever implemented, this +should serve as a set of test cases. +""" + +from sympy.core.function import Function +from sympy.core.numbers import I +from sympy.core.power import Pow +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.hyperbolic import (cosh, sinh) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (acos, cos, sin) +from sympy.integrals.integrals import Integral +from sympy.solvers.ode.ode import constantsimp, constant_renumber +from sympy.testing.pytest import XFAIL + + +x = Symbol('x') +y = Symbol('y') +z = Symbol('z') +u2 = Symbol('u2') +_a = Symbol('_a') +C1 = Symbol('C1') +C2 = Symbol('C2') +C3 = Symbol('C3') +f = Function('f') + + +def test_constant_mul(): + # We want C1 (Constant) below to absorb the y's, but not the x's + assert constant_renumber(constantsimp(y*C1, [C1])) == C1*y + assert constant_renumber(constantsimp(C1*y, [C1])) == C1*y + assert constant_renumber(constantsimp(x*C1, [C1])) == x*C1 + assert constant_renumber(constantsimp(C1*x, [C1])) == x*C1 + assert constant_renumber(constantsimp(2*C1, [C1])) == C1 + assert constant_renumber(constantsimp(C1*2, [C1])) == C1 + assert constant_renumber(constantsimp(y*C1*x, [C1, y])) == C1*x + assert constant_renumber(constantsimp(x*y*C1, [C1, y])) == x*C1 + assert constant_renumber(constantsimp(y*x*C1, [C1, y])) == x*C1 + assert constant_renumber(constantsimp(C1*x*y, [C1, y])) == C1*x + assert constant_renumber(constantsimp(x*C1*y, [C1, y])) == x*C1 + assert constant_renumber(constantsimp(C1*y*(y + 1), [C1])) == C1*y*(y+1) + assert constant_renumber(constantsimp(y*C1*(y + 1), [C1])) == C1*y*(y+1) + assert constant_renumber(constantsimp(x*(y*C1), [C1])) == x*y*C1 + assert constant_renumber(constantsimp(x*(C1*y), [C1])) == x*y*C1 + assert constant_renumber(constantsimp(C1*(x*y), [C1, y])) == C1*x + assert constant_renumber(constantsimp((x*y)*C1, [C1, y])) == x*C1 + assert constant_renumber(constantsimp((y*x)*C1, [C1, y])) == x*C1 + assert constant_renumber(constantsimp(y*(y + 1)*C1, [C1, y])) == C1 + assert constant_renumber(constantsimp((C1*x)*y, [C1, y])) == C1*x + assert constant_renumber(constantsimp(y*(x*C1), [C1, y])) == x*C1 + assert constant_renumber(constantsimp((x*C1)*y, [C1, y])) == x*C1 + assert constant_renumber(constantsimp(C1*x*y*x*y*2, [C1, y])) == C1*x**2 + assert constant_renumber(constantsimp(C1*x*y*z, [C1, y, z])) == C1*x + assert constant_renumber(constantsimp(C1*x*y**2*sin(z), [C1, y, z])) == C1*x + assert constant_renumber(constantsimp(C1*C1, [C1])) == C1 + assert constant_renumber(constantsimp(C1*C2, [C1, C2])) == C1 + assert constant_renumber(constantsimp(C2*C2, [C1, C2])) == C1 + assert constant_renumber(constantsimp(C1*C1*C2, [C1, C2])) == C1 + assert constant_renumber(constantsimp(C1*x*2**x, [C1])) == C1*x*2**x + +def test_constant_add(): + assert constant_renumber(constantsimp(C1 + C1, [C1])) == C1 + assert constant_renumber(constantsimp(C1 + 2, [C1])) == C1 + assert constant_renumber(constantsimp(2 + C1, [C1])) == C1 + assert constant_renumber(constantsimp(C1 + y, [C1, y])) == C1 + assert constant_renumber(constantsimp(C1 + x, [C1])) == C1 + x + assert constant_renumber(constantsimp(C1 + C1, [C1])) == C1 + assert constant_renumber(constantsimp(C1 + C2, [C1, C2])) == C1 + assert constant_renumber(constantsimp(C2 + C1, [C1, C2])) == C1 + assert constant_renumber(constantsimp(C1 + C2 + C1, [C1, C2])) == C1 + + +def test_constant_power_as_base(): + assert constant_renumber(constantsimp(C1**C1, [C1])) == C1 + assert constant_renumber(constantsimp(Pow(C1, C1), [C1])) == C1 + assert constant_renumber(constantsimp(C1**C1, [C1])) == C1 + assert constant_renumber(constantsimp(C1**C2, [C1, C2])) == C1 + assert constant_renumber(constantsimp(C2**C1, [C1, C2])) == C1 + assert constant_renumber(constantsimp(C2**C2, [C1, C2])) == C1 + assert constant_renumber(constantsimp(C1**y, [C1, y])) == C1 + assert constant_renumber(constantsimp(C1**x, [C1])) == C1**x + assert constant_renumber(constantsimp(C1**2, [C1])) == C1 + assert constant_renumber( + constantsimp(C1**(x*y), [C1])) == C1**(x*y) + + +def test_constant_power_as_exp(): + assert constant_renumber(constantsimp(x**C1, [C1])) == x**C1 + assert constant_renumber(constantsimp(y**C1, [C1, y])) == C1 + assert constant_renumber(constantsimp(x**y**C1, [C1, y])) == x**C1 + assert constant_renumber( + constantsimp((x**y)**C1, [C1])) == (x**y)**C1 + assert constant_renumber( + constantsimp(x**(y**C1), [C1, y])) == x**C1 + assert constant_renumber(constantsimp(x**C1**y, [C1, y])) == x**C1 + assert constant_renumber( + constantsimp(x**(C1**y), [C1, y])) == x**C1 + assert constant_renumber( + constantsimp((x**C1)**y, [C1])) == (x**C1)**y + assert constant_renumber(constantsimp(2**C1, [C1])) == C1 + assert constant_renumber(constantsimp(S(2)**C1, [C1])) == C1 + assert constant_renumber(constantsimp(exp(C1), [C1])) == C1 + assert constant_renumber( + constantsimp(exp(C1 + x), [C1])) == C1*exp(x) + assert constant_renumber(constantsimp(Pow(2, C1), [C1])) == C1 + + +def test_constant_function(): + assert constant_renumber(constantsimp(sin(C1), [C1])) == C1 + assert constant_renumber(constantsimp(f(C1), [C1])) == C1 + assert constant_renumber(constantsimp(f(C1, C1), [C1])) == C1 + assert constant_renumber(constantsimp(f(C1, C2), [C1, C2])) == C1 + assert constant_renumber(constantsimp(f(C2, C1), [C1, C2])) == C1 + assert constant_renumber(constantsimp(f(C2, C2), [C1, C2])) == C1 + assert constant_renumber( + constantsimp(f(C1, x), [C1])) == f(C1, x) + assert constant_renumber(constantsimp(f(C1, y), [C1, y])) == C1 + assert constant_renumber(constantsimp(f(y, C1), [C1, y])) == C1 + assert constant_renumber(constantsimp(f(C1, y, C2), [C1, C2, y])) == C1 + + +def test_constant_function_multiple(): + # The rules to not renumber in this case would be too complicated, and + # dsolve is not likely to ever encounter anything remotely like this. + assert constant_renumber( + constantsimp(f(C1, C1, x), [C1])) == f(C1, C1, x) + + +def test_constant_multiple(): + assert constant_renumber(constantsimp(C1*2 + 2, [C1])) == C1 + assert constant_renumber(constantsimp(x*2/C1, [C1])) == C1*x + assert constant_renumber(constantsimp(C1**2*2 + 2, [C1])) == C1 + assert constant_renumber( + constantsimp(sin(2*C1) + x + sqrt(2), [C1])) == C1 + x + assert constant_renumber(constantsimp(2*C1 + C2, [C1, C2])) == C1 + +def test_constant_repeated(): + assert C1 + C1*x == constant_renumber( C1 + C1*x) + +def test_ode_solutions(): + # only a few examples here, the rest will be tested in the actual dsolve tests + assert constant_renumber(constantsimp(C1*exp(2*x) + exp(x)*(C2 + C3), [C1, C2, C3])) == \ + constant_renumber(C1*exp(x) + C2*exp(2*x)) + assert constant_renumber( + constantsimp(Eq(f(x), I*C1*sinh(x/3) + C2*cosh(x/3)), [C1, C2]) + ) == constant_renumber(Eq(f(x), C1*sinh(x/3) + C2*cosh(x/3))) + assert constant_renumber(constantsimp(Eq(f(x), acos((-C1)/cos(x))), [C1])) == \ + Eq(f(x), acos(C1/cos(x))) + assert constant_renumber( + constantsimp(Eq(log(f(x)/C1) + 2*exp(x/f(x)), 0), [C1]) + ) == Eq(log(C1*f(x)) + 2*exp(x/f(x)), 0) + assert constant_renumber(constantsimp(Eq(log(x*sqrt(2)*sqrt(1/x)*sqrt(f(x)) + /C1) + x**2/(2*f(x)**2), 0), [C1])) == \ + Eq(log(C1*sqrt(x)*sqrt(f(x))) + x**2/(2*f(x)**2), 0) + assert constant_renumber(constantsimp(Eq(-exp(-f(x)/x)*sin(f(x)/x)/2 + log(x/C1) - + cos(f(x)/x)*exp(-f(x)/x)/2, 0), [C1])) == \ + Eq(-exp(-f(x)/x)*sin(f(x)/x)/2 + log(C1*x) - cos(f(x)/x)* + exp(-f(x)/x)/2, 0) + assert constant_renumber(constantsimp(Eq(-Integral(-1/(sqrt(1 - u2**2)*u2), + (u2, _a, x/f(x))) + log(f(x)/C1), 0), [C1])) == \ + Eq(-Integral(-1/(u2*sqrt(1 - u2**2)), (u2, _a, x/f(x))) + + log(C1*f(x)), 0) + assert [constantsimp(i, [C1]) for i in [Eq(f(x), sqrt(-C1*x + x**2)), Eq(f(x), -sqrt(-C1*x + x**2))]] == \ + [Eq(f(x), sqrt(x*(C1 + x))), Eq(f(x), -sqrt(x*(C1 + x)))] + + +@XFAIL +def test_nonlocal_simplification(): + assert constantsimp(C1 + C2+x*C2, [C1, C2]) == C1 + C2*x + + +def test_constant_Eq(): + # C1 on the rhs is well-tested, but the lhs is only tested here + assert constantsimp(Eq(C1, 3 + f(x)*x), [C1]) == Eq(x*f(x), C1) + assert constantsimp(Eq(C1, 3 * f(x)*x), [C1]) == Eq(f(x)*x, C1) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_decompogen.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_decompogen.py new file mode 100644 index 0000000000000000000000000000000000000000..1ba03f4b42558231b626b6ed169f8b0a81a72bf9 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_decompogen.py @@ -0,0 +1,59 @@ +from sympy.solvers.decompogen import decompogen, compogen +from sympy.core.symbol import symbols +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt, Max +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.testing.pytest import XFAIL, raises + +x, y = symbols('x y') + + +def test_decompogen(): + assert decompogen(sin(cos(x)), x) == [sin(x), cos(x)] + assert decompogen(sin(x)**2 + sin(x) + 1, x) == [x**2 + x + 1, sin(x)] + assert decompogen(sqrt(6*x**2 - 5), x) == [sqrt(x), 6*x**2 - 5] + assert decompogen(sin(sqrt(cos(x**2 + 1))), x) == [sin(x), sqrt(x), cos(x), x**2 + 1] + assert decompogen(Abs(cos(x)**2 + 3*cos(x) - 4), x) == [Abs(x), x**2 + 3*x - 4, cos(x)] + assert decompogen(sin(x)**2 + sin(x) - sqrt(3)/2, x) == [x**2 + x - sqrt(3)/2, sin(x)] + assert decompogen(Abs(cos(y)**2 + 3*cos(x) - 4), x) == [Abs(x), 3*x + cos(y)**2 - 4, cos(x)] + assert decompogen(x, y) == [x] + assert decompogen(1, x) == [1] + assert decompogen(Max(3, x), x) == [Max(3, x)] + raises(TypeError, lambda: decompogen(x < 5, x)) + u = 2*x + 3 + assert decompogen(Max(sqrt(u),(u)**2), x) == [Max(sqrt(x), x**2), u] + assert decompogen(Max(u, u**2, y), x) == [Max(x, x**2, y), u] + assert decompogen(Max(sin(x), u), x) == [Max(2*x + 3, sin(x))] + + +def test_decompogen_poly(): + assert decompogen(x**4 + 2*x**2 + 1, x) == [x**2 + 2*x + 1, x**2] + assert decompogen(x**4 + 2*x**3 - x - 1, x) == [x**2 - x - 1, x**2 + x] + + +@XFAIL +def test_decompogen_fails(): + A = lambda x: x**2 + 2*x + 3 + B = lambda x: 4*x**2 + 5*x + 6 + assert decompogen(A(x*exp(x)), x) == [x**2 + 2*x + 3, x*exp(x)] + assert decompogen(A(B(x)), x) == [x**2 + 2*x + 3, 4*x**2 + 5*x + 6] + assert decompogen(A(1/x + 1/x**2), x) == [x**2 + 2*x + 3, 1/x + 1/x**2] + assert decompogen(A(1/x + 2/(x + 1)), x) == [x**2 + 2*x + 3, 1/x + 2/(x + 1)] + + +def test_compogen(): + assert compogen([sin(x), cos(x)], x) == sin(cos(x)) + assert compogen([x**2 + x + 1, sin(x)], x) == sin(x)**2 + sin(x) + 1 + assert compogen([sqrt(x), 6*x**2 - 5], x) == sqrt(6*x**2 - 5) + assert compogen([sin(x), sqrt(x), cos(x), x**2 + 1], x) == sin(sqrt( + cos(x**2 + 1))) + assert compogen([Abs(x), x**2 + 3*x - 4, cos(x)], x) == Abs(cos(x)**2 + + 3*cos(x) - 4) + assert compogen([x**2 + x - sqrt(3)/2, sin(x)], x) == (sin(x)**2 + sin(x) - + sqrt(3)/2) + assert compogen([Abs(x), 3*x + cos(y)**2 - 4, cos(x)], x) == \ + Abs(3*cos(x) + cos(y)**2 - 4) + assert compogen([x**2 + 2*x + 1, x**2], x) == x**4 + 2*x**2 + 1 + # the result is in unsimplified form + assert compogen([x**2 - x - 1, x**2 + x], x) == -x**2 - x + (x**2 + x)**2 - 1 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_inequalities.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_inequalities.py new file mode 100644 index 0000000000000000000000000000000000000000..6ce6f4520b52d8714102c95457c90d44543c685c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_inequalities.py @@ -0,0 +1,500 @@ +"""Tests for tools for solving inequalities and systems of inequalities. """ + +from sympy.concrete.summations import Sum +from sympy.core.function import Function +from sympy.core.numbers import I, Rational, oo, pi +from sympy.core.relational import Eq, Ge, Gt, Le, Lt, Ne +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol) +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.exponential import exp, log +from sympy.functions.elementary.miscellaneous import root, sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import cos, sin, tan +from sympy.integrals.integrals import Integral +from sympy.logic.boolalg import And, Or +from sympy.polys.polytools import Poly, PurePoly +from sympy.sets.sets import FiniteSet, Interval, Union +from sympy.solvers.inequalities import (reduce_inequalities, + solve_poly_inequality as psolve, + reduce_rational_inequalities, + solve_univariate_inequality as isolve, + reduce_abs_inequality, + _solve_inequality) +from sympy.polys.rootoftools import rootof +from sympy.solvers.solvers import solve +from sympy.solvers.solveset import solveset +from sympy.core.mod import Mod +from sympy.abc import x, y + +from sympy.testing.pytest import raises, XFAIL + + +inf = oo.evalf() + + +def test_solve_poly_inequality(): + assert psolve(Poly(0, x), '==') == [S.Reals] + assert psolve(Poly(1, x), '==') == [S.EmptySet] + assert psolve(PurePoly(x + 1, x), ">") == [Interval(-1, oo, True, False)] + + +def test_reduce_poly_inequalities_real_interval(): + assert reduce_rational_inequalities( + [[Eq(x**2, 0)]], x, relational=False) == FiniteSet(0) + assert reduce_rational_inequalities( + [[Le(x**2, 0)]], x, relational=False) == FiniteSet(0) + assert reduce_rational_inequalities( + [[Lt(x**2, 0)]], x, relational=False) == S.EmptySet + assert reduce_rational_inequalities( + [[Ge(x**2, 0)]], x, relational=False) == \ + S.Reals if x.is_real else Interval(-oo, oo) + assert reduce_rational_inequalities( + [[Gt(x**2, 0)]], x, relational=False) == \ + FiniteSet(0).complement(S.Reals) + assert reduce_rational_inequalities( + [[Ne(x**2, 0)]], x, relational=False) == \ + FiniteSet(0).complement(S.Reals) + + assert reduce_rational_inequalities( + [[Eq(x**2, 1)]], x, relational=False) == FiniteSet(-1, 1) + assert reduce_rational_inequalities( + [[Le(x**2, 1)]], x, relational=False) == Interval(-1, 1) + assert reduce_rational_inequalities( + [[Lt(x**2, 1)]], x, relational=False) == Interval(-1, 1, True, True) + assert reduce_rational_inequalities( + [[Ge(x**2, 1)]], x, relational=False) == \ + Union(Interval(-oo, -1), Interval(1, oo)) + assert reduce_rational_inequalities( + [[Gt(x**2, 1)]], x, relational=False) == \ + Interval(-1, 1).complement(S.Reals) + assert reduce_rational_inequalities( + [[Ne(x**2, 1)]], x, relational=False) == \ + FiniteSet(-1, 1).complement(S.Reals) + assert reduce_rational_inequalities([[Eq( + x**2, 1.0)]], x, relational=False) == FiniteSet(-1.0, 1.0).evalf() + assert reduce_rational_inequalities( + [[Le(x**2, 1.0)]], x, relational=False) == Interval(-1.0, 1.0) + assert reduce_rational_inequalities([[Lt( + x**2, 1.0)]], x, relational=False) == Interval(-1.0, 1.0, True, True) + assert reduce_rational_inequalities( + [[Ge(x**2, 1.0)]], x, relational=False) == \ + Union(Interval(-inf, -1.0), Interval(1.0, inf)) + assert reduce_rational_inequalities( + [[Gt(x**2, 1.0)]], x, relational=False) == \ + Union(Interval(-inf, -1.0, right_open=True), + Interval(1.0, inf, left_open=True)) + assert reduce_rational_inequalities([[Ne( + x**2, 1.0)]], x, relational=False) == \ + FiniteSet(-1.0, 1.0).complement(S.Reals) + + s = sqrt(2) + + assert reduce_rational_inequalities([[Lt( + x**2 - 1, 0), Gt(x**2 - 1, 0)]], x, relational=False) == S.EmptySet + assert reduce_rational_inequalities([[Le(x**2 - 1, 0), Ge( + x**2 - 1, 0)]], x, relational=False) == FiniteSet(-1, 1) + assert reduce_rational_inequalities( + [[Le(x**2 - 2, 0), Ge(x**2 - 1, 0)]], x, relational=False + ) == Union(Interval(-s, -1, False, False), Interval(1, s, False, False)) + assert reduce_rational_inequalities( + [[Le(x**2 - 2, 0), Gt(x**2 - 1, 0)]], x, relational=False + ) == Union(Interval(-s, -1, False, True), Interval(1, s, True, False)) + assert reduce_rational_inequalities( + [[Lt(x**2 - 2, 0), Ge(x**2 - 1, 0)]], x, relational=False + ) == Union(Interval(-s, -1, True, False), Interval(1, s, False, True)) + assert reduce_rational_inequalities( + [[Lt(x**2 - 2, 0), Gt(x**2 - 1, 0)]], x, relational=False + ) == Union(Interval(-s, -1, True, True), Interval(1, s, True, True)) + assert reduce_rational_inequalities( + [[Lt(x**2 - 2, 0), Ne(x**2 - 1, 0)]], x, relational=False + ) == Union(Interval(-s, -1, True, True), Interval(-1, 1, True, True), + Interval(1, s, True, True)) + + assert reduce_rational_inequalities([[Lt(x**2, -1.)]], x) is S.false + + +def test_reduce_poly_inequalities_complex_relational(): + assert reduce_rational_inequalities( + [[Eq(x**2, 0)]], x, relational=True) == Eq(x, 0) + assert reduce_rational_inequalities( + [[Le(x**2, 0)]], x, relational=True) == Eq(x, 0) + assert reduce_rational_inequalities( + [[Lt(x**2, 0)]], x, relational=True) == False + assert reduce_rational_inequalities( + [[Ge(x**2, 0)]], x, relational=True) == And(Lt(-oo, x), Lt(x, oo)) + assert reduce_rational_inequalities( + [[Gt(x**2, 0)]], x, relational=True) == \ + And(Gt(x, -oo), Lt(x, oo), Ne(x, 0)) + assert reduce_rational_inequalities( + [[Ne(x**2, 0)]], x, relational=True) == \ + And(Gt(x, -oo), Lt(x, oo), Ne(x, 0)) + + for one in (S.One, S(1.0)): + inf = one*oo + assert reduce_rational_inequalities( + [[Eq(x**2, one)]], x, relational=True) == \ + Or(Eq(x, -one), Eq(x, one)) + assert reduce_rational_inequalities( + [[Le(x**2, one)]], x, relational=True) == \ + And(And(Le(-one, x), Le(x, one))) + assert reduce_rational_inequalities( + [[Lt(x**2, one)]], x, relational=True) == \ + And(And(Lt(-one, x), Lt(x, one))) + assert reduce_rational_inequalities( + [[Ge(x**2, one)]], x, relational=True) == \ + And(Or(And(Le(one, x), Lt(x, inf)), And(Le(x, -one), Lt(-inf, x)))) + assert reduce_rational_inequalities( + [[Gt(x**2, one)]], x, relational=True) == \ + And(Or(And(Lt(-inf, x), Lt(x, -one)), And(Lt(one, x), Lt(x, inf)))) + assert reduce_rational_inequalities( + [[Ne(x**2, one)]], x, relational=True) == \ + Or(And(Lt(-inf, x), Lt(x, -one)), + And(Lt(-one, x), Lt(x, one)), + And(Lt(one, x), Lt(x, inf))) + + +def test_reduce_rational_inequalities_real_relational(): + assert reduce_rational_inequalities([], x) == False + assert reduce_rational_inequalities( + [[(x**2 + 3*x + 2)/(x**2 - 16) >= 0]], x, relational=False) == \ + Union(Interval.open(-oo, -4), Interval(-2, -1), Interval.open(4, oo)) + + assert reduce_rational_inequalities( + [[((-2*x - 10)*(3 - x))/((x**2 + 5)*(x - 2)**2) < 0]], x, + relational=False) == \ + Union(Interval.open(-5, 2), Interval.open(2, 3)) + + assert reduce_rational_inequalities([[(x + 1)/(x - 5) <= 0]], x, + relational=False) == \ + Interval.Ropen(-1, 5) + + assert reduce_rational_inequalities([[(x**2 + 4*x + 3)/(x - 1) > 0]], x, + relational=False) == \ + Union(Interval.open(-3, -1), Interval.open(1, oo)) + + assert reduce_rational_inequalities([[(x**2 - 16)/(x - 1)**2 < 0]], x, + relational=False) == \ + Union(Interval.open(-4, 1), Interval.open(1, 4)) + + assert reduce_rational_inequalities([[(3*x + 1)/(x + 4) >= 1]], x, + relational=False) == \ + Union(Interval.open(-oo, -4), Interval.Ropen(Rational(3, 2), oo)) + + assert reduce_rational_inequalities([[(x - 8)/x <= 3 - x]], x, + relational=False) == \ + Union(Interval.Lopen(-oo, -2), Interval.Lopen(0, 4)) + + # issue sympy/sympy#10237 + assert reduce_rational_inequalities( + [[x < oo, x >= 0, -oo < x]], x, relational=False) == Interval(0, oo) + + +def test_reduce_abs_inequalities(): + e = abs(x - 5) < 3 + ans = And(Lt(2, x), Lt(x, 8)) + assert reduce_inequalities(e) == ans + assert reduce_inequalities(e, x) == ans + assert reduce_inequalities(abs(x - 5)) == Eq(x, 5) + assert reduce_inequalities( + abs(2*x + 3) >= 8) == Or(And(Le(Rational(5, 2), x), Lt(x, oo)), + And(Le(x, Rational(-11, 2)), Lt(-oo, x))) + assert reduce_inequalities(abs(x - 4) + abs( + 3*x - 5) < 7) == And(Lt(S.Half, x), Lt(x, 4)) + assert reduce_inequalities(abs(x - 4) + abs(3*abs(x) - 5) < 7) == \ + Or(And(S(-2) < x, x < -1), And(S.Half < x, x < 4)) + + nr = Symbol('nr', extended_real=False) + raises(TypeError, lambda: reduce_inequalities(abs(nr - 5) < 3)) + assert reduce_inequalities(x < 3, symbols=[x, nr]) == And(-oo < x, x < 3) + + +def test_reduce_inequalities_general(): + assert reduce_inequalities(Ge(sqrt(2)*x, 1)) == And(sqrt(2)/2 <= x, x < oo) + assert reduce_inequalities(x + 1 > 0) == And(S.NegativeOne < x, x < oo) + + +def test_reduce_inequalities_boolean(): + assert reduce_inequalities( + [Eq(x**2, 0), True]) == Eq(x, 0) + assert reduce_inequalities([Eq(x**2, 0), False]) == False + assert reduce_inequalities(x**2 >= 0) is S.true # issue 10196 + + +def test_reduce_inequalities_multivariate(): + assert reduce_inequalities([Ge(x**2, 1), Ge(y**2, 1)]) == And( + Or(And(Le(S.One, x), Lt(x, oo)), And(Le(x, -1), Lt(-oo, x))), + Or(And(Le(S.One, y), Lt(y, oo)), And(Le(y, -1), Lt(-oo, y)))) + + +def test_reduce_inequalities_errors(): + raises(NotImplementedError, lambda: reduce_inequalities(Ge(sin(x) + x, 1))) + raises(NotImplementedError, lambda: reduce_inequalities(Ge(x**2*y + y, 1))) + + +def test__solve_inequalities(): + assert reduce_inequalities(x + y < 1, symbols=[x]) == (x < 1 - y) + assert reduce_inequalities(x + y >= 1, symbols=[x]) == (x < oo) & (x >= -y + 1) + assert reduce_inequalities(Eq(0, x - y), symbols=[x]) == Eq(x, y) + assert reduce_inequalities(Ne(0, x - y), symbols=[x]) == Ne(x, y) + + +def test_issue_6343(): + eq = -3*x**2/2 - x*Rational(45, 4) + Rational(33, 2) > 0 + assert reduce_inequalities(eq) == \ + And(x < Rational(-15, 4) + sqrt(401)/4, -sqrt(401)/4 - Rational(15, 4) < x) + + +def test_issue_8235(): + assert reduce_inequalities(x**2 - 1 < 0) == \ + And(S.NegativeOne < x, x < 1) + assert reduce_inequalities(x**2 - 1 <= 0) == \ + And(S.NegativeOne <= x, x <= 1) + assert reduce_inequalities(x**2 - 1 > 0) == \ + Or(And(-oo < x, x < -1), And(x < oo, S.One < x)) + assert reduce_inequalities(x**2 - 1 >= 0) == \ + Or(And(-oo < x, x <= -1), And(S.One <= x, x < oo)) + + eq = x**8 + x - 9 # we want CRootOf solns here + sol = solve(eq >= 0) + tru = Or(And(rootof(eq, 1) <= x, x < oo), And(-oo < x, x <= rootof(eq, 0))) + assert sol == tru + + # recast vanilla as real + assert solve(sqrt((-x + 1)**2) < 1) == And(S.Zero < x, x < 2) + + +def test_issue_5526(): + assert reduce_inequalities(0 <= + x + Integral(y**2, (y, 1, 3)) - 1, [x]) == \ + (x >= -Integral(y**2, (y, 1, 3)) + 1) + f = Function('f') + e = Sum(f(x), (x, 1, 3)) + assert reduce_inequalities(0 <= x + e + y**2, [x]) == \ + (x >= -y**2 - Sum(f(x), (x, 1, 3))) + + +def test_solve_univariate_inequality(): + assert isolve(x**2 >= 4, x, relational=False) == Union(Interval(-oo, -2), + Interval(2, oo)) + assert isolve(x**2 >= 4, x) == Or(And(Le(2, x), Lt(x, oo)), And(Le(x, -2), + Lt(-oo, x))) + assert isolve((x - 1)*(x - 2)*(x - 3) >= 0, x, relational=False) == \ + Union(Interval(1, 2), Interval(3, oo)) + assert isolve((x - 1)*(x - 2)*(x - 3) >= 0, x) == \ + Or(And(Le(1, x), Le(x, 2)), And(Le(3, x), Lt(x, oo))) + assert isolve((x - 1)*(x - 2)*(x - 4) < 0, x, domain = FiniteSet(0, 3)) == \ + Or(Eq(x, 0), Eq(x, 3)) + # issue 2785: + assert isolve(x**3 - 2*x - 1 > 0, x, relational=False) == \ + Union(Interval(-1, -sqrt(5)/2 + S.Half, True, True), + Interval(S.Half + sqrt(5)/2, oo, True, True)) + # issue 2794: + assert isolve(x**3 - x**2 + x - 1 > 0, x, relational=False) == \ + Interval(1, oo, True) + #issue 13105 + assert isolve((x + I)*(x + 2*I) < 0, x) == Eq(x, 0) + assert isolve(((x - 1)*(x - 2) + I)*((x - 1)*(x - 2) + 2*I) < 0, x) == Or(Eq(x, 1), Eq(x, 2)) + assert isolve((((x - 1)*(x - 2) + I)*((x - 1)*(x - 2) + 2*I))/(x - 2) > 0, x) == Eq(x, 1) + raises (ValueError, lambda: isolve((x**2 - 3*x*I + 2)/x < 0, x)) + + # numerical testing in valid() is needed + assert isolve(x**7 - x - 2 > 0, x) == \ + And(rootof(x**7 - x - 2, 0) < x, x < oo) + + # handle numerator and denominator; although these would be handled as + # rational inequalities, these test confirm that the right thing is done + # when the domain is EX (e.g. when 2 is replaced with sqrt(2)) + assert isolve(1/(x - 2) > 0, x) == And(S(2) < x, x < oo) + den = ((x - 1)*(x - 2)).expand() + assert isolve((x - 1)/den <= 0, x) == \ + (x > -oo) & (x < 2) & Ne(x, 1) + + n = Dummy('n') + raises(NotImplementedError, lambda: isolve(Abs(x) <= n, x, relational=False)) + c1 = Dummy("c1", positive=True) + raises(NotImplementedError, lambda: isolve(n/c1 < 0, c1)) + n = Dummy('n', negative=True) + assert isolve(n/c1 > -2, c1) == (-n/2 < c1) + assert isolve(n/c1 < 0, c1) == True + assert isolve(n/c1 > 0, c1) == False + + zero = cos(1)**2 + sin(1)**2 - 1 + raises(NotImplementedError, lambda: isolve(x**2 < zero, x)) + raises(NotImplementedError, lambda: isolve( + x**2 < zero*I, x)) + raises(NotImplementedError, lambda: isolve(1/(x - y) < 2, x)) + raises(NotImplementedError, lambda: isolve(1/(x - y) < 0, x)) + raises(TypeError, lambda: isolve(x - I < 0, x)) + + zero = x**2 + x - x*(x + 1) + assert isolve(zero < 0, x, relational=False) is S.EmptySet + assert isolve(zero <= 0, x, relational=False) is S.Reals + + # make sure iter_solutions gets a default value + raises(NotImplementedError, lambda: isolve( + Eq(cos(x)**2 + sin(x)**2, 1), x)) + + +def test_trig_inequalities(): + # all the inequalities are solved in a periodic interval. + assert isolve(sin(x) < S.Half, x, relational=False) == \ + Union(Interval(0, pi/6, False, True), Interval.open(pi*Rational(5, 6), 2*pi)) + assert isolve(sin(x) > S.Half, x, relational=False) == \ + Interval(pi/6, pi*Rational(5, 6), True, True) + assert isolve(cos(x) < S.Zero, x, relational=False) == \ + Interval(pi/2, pi*Rational(3, 2), True, True) + assert isolve(cos(x) >= S.Zero, x, relational=False) == \ + Union(Interval(0, pi/2), Interval.Ropen(pi*Rational(3, 2), 2*pi)) + + assert isolve(tan(x) < S.One, x, relational=False) == \ + Union(Interval.Ropen(0, pi/4), Interval.open(pi/2, pi)) + + assert isolve(sin(x) <= S.Zero, x, relational=False) == \ + Union(FiniteSet(S.Zero), Interval.Ropen(pi, 2*pi)) + + assert isolve(sin(x) <= S.One, x, relational=False) == S.Reals + assert isolve(cos(x) < S(-2), x, relational=False) == S.EmptySet + assert isolve(sin(x) >= S.NegativeOne, x, relational=False) == S.Reals + assert isolve(cos(x) > S.One, x, relational=False) == S.EmptySet + + +def test_issue_9954(): + assert isolve(x**2 >= 0, x, relational=False) == S.Reals + assert isolve(x**2 >= 0, x, relational=True) == S.Reals.as_relational(x) + assert isolve(x**2 < 0, x, relational=False) == S.EmptySet + assert isolve(x**2 < 0, x, relational=True) == S.EmptySet.as_relational(x) + + +@XFAIL +def test_slow_general_univariate(): + r = rootof(x**5 - x**2 + 1, 0) + assert solve(sqrt(x) + 1/root(x, 3) > 1) == \ + Or(And(0 < x, x < r**6), And(r**6 < x, x < oo)) + + +def test_issue_8545(): + eq = 1 - x - abs(1 - x) + ans = And(Lt(1, x), Lt(x, oo)) + assert reduce_abs_inequality(eq, '<', x) == ans + eq = 1 - x - sqrt((1 - x)**2) + assert reduce_inequalities(eq < 0) == ans + + +def test_issue_8974(): + assert isolve(-oo < x, x) == And(-oo < x, x < oo) + assert isolve(oo > x, x) == And(-oo < x, x < oo) + + +def test_issue_10198(): + assert reduce_inequalities( + -1 + 1/abs(1/x - 1) < 0) == (x > -oo) & (x < S(1)/2) & Ne(x, 0) + + assert reduce_inequalities(abs(1/sqrt(x)) - 1, x) == Eq(x, 1) + assert reduce_abs_inequality(-3 + 1/abs(1 - 1/x), '<', x) == \ + Or(And(-oo < x, x < 0), + And(S.Zero < x, x < Rational(3, 4)), And(Rational(3, 2) < x, x < oo)) + raises(ValueError,lambda: reduce_abs_inequality(-3 + 1/abs( + 1 - 1/sqrt(x)), '<', x)) + + +def test_issue_10047(): + # issue 10047: this must remain an inequality, not True, since if x + # is not real the inequality is invalid + # assert solve(sin(x) < 2) == (x <= oo) + + # with PR 16956, (x <= oo) autoevaluates when x is extended_real + # which is assumed in the current implementation of inequality solvers + assert solve(sin(x) < 2) == True + assert solveset(sin(x) < 2, domain=S.Reals) == S.Reals + + +def test_issue_10268(): + assert solve(log(x) < 1000) == And(S.Zero < x, x < exp(1000)) + + +@XFAIL +def test_isolve_Sets(): + n = Dummy('n') + assert isolve(Abs(x) <= n, x, relational=False) == \ + Piecewise((S.EmptySet, n < 0), (Interval(-n, n), True)) + + +def test_integer_domain_relational_isolve(): + + dom = FiniteSet(0, 3) + x = Symbol('x',zero=False) + assert isolve((x - 1)*(x - 2)*(x - 4) < 0, x, domain=dom) == Eq(x, 3) + + x = Symbol('x') + assert isolve(x + 2 < 0, x, domain=S.Integers) == \ + (x <= -3) & (x > -oo) & Eq(Mod(x, 1), 0) + assert isolve(2 * x + 3 > 0, x, domain=S.Integers) == \ + (x >= -1) & (x < oo) & Eq(Mod(x, 1), 0) + assert isolve((x ** 2 + 3 * x - 2) < 0, x, domain=S.Integers) == \ + (x >= -3) & (x <= 0) & Eq(Mod(x, 1), 0) + assert isolve((x ** 2 + 3 * x - 2) > 0, x, domain=S.Integers) == \ + ((x >= 1) & (x < oo) & Eq(Mod(x, 1), 0)) | ( + (x <= -4) & (x > -oo) & Eq(Mod(x, 1), 0)) + + +def test_issue_10671_12466(): + assert solveset(sin(y), y, Interval(0, pi)) == FiniteSet(0, pi) + i = Interval(1, 10) + assert solveset((1/x).diff(x) < 0, x, i) == i + assert solveset((log(x - 6)/x) <= 0, x, S.Reals) == \ + Interval.Lopen(6, 7) + + +def test__solve_inequality(): + for op in (Gt, Lt, Le, Ge, Eq, Ne): + assert _solve_inequality(op(x, 1), x).lhs == x + assert _solve_inequality(op(S.One, x), x).lhs == x + # don't get tricked by symbol on right: solve it + assert _solve_inequality(Eq(2*x - 1, x), x) == Eq(x, 1) + ie = Eq(S.One, y) + assert _solve_inequality(ie, x) == ie + for fx in (x**2, exp(x), sin(x) + cos(x), x*(1 + x)): + for c in (0, 1): + e = 2*fx - c > 0 + assert _solve_inequality(e, x, linear=True) == ( + fx > c/S(2)) + assert _solve_inequality(2*x**2 + 2*x - 1 < 0, x, linear=True) == ( + x*(x + 1) < S.Half) + assert _solve_inequality(Eq(x*y, 1), x) == Eq(x*y, 1) + nz = Symbol('nz', nonzero=True) + assert _solve_inequality(Eq(x*nz, 1), x) == Eq(x, 1/nz) + assert _solve_inequality(x*nz < 1, x) == (x*nz < 1) + a = Symbol('a', positive=True) + assert _solve_inequality(a/x > 1, x) == (S.Zero < x) & (x < a) + assert _solve_inequality(a/x > 1, x, linear=True) == (1/x > 1/a) + # make sure to include conditions under which solution is valid + e = Eq(1 - x, x*(1/x - 1)) + assert _solve_inequality(e, x) == Ne(x, 0) + assert _solve_inequality(x < x*(1/x - 1), x) == (x < S.Half) & Ne(x, 0) + + +def test__pt(): + from sympy.solvers.inequalities import _pt + assert _pt(-oo, oo) == 0 + assert _pt(S.One, S(3)) == 2 + assert _pt(S.One, oo) == _pt(oo, S.One) == 2 + assert _pt(S.One, -oo) == _pt(-oo, S.One) == S.Half + assert _pt(S.NegativeOne, oo) == _pt(oo, S.NegativeOne) == Rational(-1, 2) + assert _pt(S.NegativeOne, -oo) == _pt(-oo, S.NegativeOne) == -2 + assert _pt(x, oo) == _pt(oo, x) == x + 1 + assert _pt(x, -oo) == _pt(-oo, x) == x - 1 + raises(ValueError, lambda: _pt(Dummy('i', infinite=True), S.One)) + + +def test_issue_25697(): + assert _solve_inequality(log(x, 3) <= 2, x) == (x <= 9) & (S.Zero < x) + + +def test_issue_25738(): + assert reduce_inequalities(3 < abs(x) + ) == reduce_inequalities(pi < abs(x)).subs(pi, 3) + + +def test_issue_25983(): + assert(reduce_inequalities(pi/Abs(x) <= 1) == ((pi <= x) & (x < oo)) | ((-oo < x) & (x <= -pi))) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_numeric.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_numeric.py new file mode 100644 index 0000000000000000000000000000000000000000..12abd38c80f07279ed41aefc7952762da0f9f430 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_numeric.py @@ -0,0 +1,139 @@ +from sympy.core.function import nfloat +from sympy.core.numbers import (Float, I, Rational, pi) +from sympy.core.relational import Eq +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import sin +from sympy.integrals.integrals import Integral +from sympy.matrices.dense import Matrix +from mpmath import mnorm, mpf +from sympy.solvers import nsolve +from sympy.utilities.lambdify import lambdify +from sympy.testing.pytest import raises, XFAIL +from sympy.utilities.decorator import conserve_mpmath_dps + +@XFAIL +def test_nsolve_fail(): + x = symbols('x') + # Sometimes it is better to use the numerator (issue 4829) + # but sometimes it is not (issue 11768) so leave this to + # the discretion of the user + ans = nsolve(x**2/(1 - x)/(1 - 2*x)**2 - 100, x, 0) + assert ans > 0.46 and ans < 0.47 + + +def test_nsolve_denominator(): + x = symbols('x') + # Test that nsolve uses the full expression (numerator and denominator). + ans = nsolve((x**2 + 3*x + 2)/(x + 2), -2.1) + # The root -2 was divided out, so make sure we don't find it. + assert ans == -1.0 + +def test_nsolve(): + # onedimensional + x = Symbol('x') + assert nsolve(sin(x), 2) - pi.evalf() < 1e-15 + assert nsolve(Eq(2*x, 2), x, -10) == nsolve(2*x - 2, -10) + # Testing checks on number of inputs + raises(TypeError, lambda: nsolve(Eq(2*x, 2))) + raises(TypeError, lambda: nsolve(Eq(2*x, 2), x, 1, 2)) + # multidimensional + x1 = Symbol('x1') + x2 = Symbol('x2') + f1 = 3 * x1**2 - 2 * x2**2 - 1 + f2 = x1**2 - 2 * x1 + x2**2 + 2 * x2 - 8 + f = Matrix((f1, f2)).T + F = lambdify((x1, x2), f.T, modules='mpmath') + for x0 in [(-1, 1), (1, -2), (4, 4), (-4, -4)]: + x = nsolve(f, (x1, x2), x0, tol=1.e-8) + assert mnorm(F(*x), 1) <= 1.e-10 + # The Chinese mathematician Zhu Shijie was the very first to solve this + # nonlinear system 700 years ago (z was added to make it 3-dimensional) + x = Symbol('x') + y = Symbol('y') + z = Symbol('z') + f1 = -x + 2*y + f2 = (x**2 + x*(y**2 - 2) - 4*y) / (x + 4) + f3 = sqrt(x**2 + y**2)*z + f = Matrix((f1, f2, f3)).T + F = lambdify((x, y, z), f.T, modules='mpmath') + + def getroot(x0): + root = nsolve(f, (x, y, z), x0) + assert mnorm(F(*root), 1) <= 1.e-8 + return root + assert list(map(round, getroot((1, 1, 1)))) == [2, 1, 0] + assert nsolve([Eq( + f1, 0), Eq(f2, 0), Eq(f3, 0)], [x, y, z], (1, 1, 1)) # just see that it works + a = Symbol('a') + assert abs(nsolve(1/(0.001 + a)**3 - 6/(0.9 - a)**3, a, 0.3) - + mpf('0.31883011387318591')) < 1e-15 + + +def test_issue_6408(): + x = Symbol('x') + assert nsolve(Piecewise((x, x < 1), (x**2, True)), x, 2) == 0 + + +def test_issue_6408_integral(): + x, y = symbols('x y') + assert nsolve(Integral(x*y, (x, 0, 5)), y, 2) == 0 + + +@conserve_mpmath_dps +def test_increased_dps(): + # Issue 8564 + import mpmath + mpmath.mp.dps = 128 + x = Symbol('x') + e1 = x**2 - pi + q = nsolve(e1, x, 3.0) + + assert abs(sqrt(pi).evalf(128) - q) < 1e-128 + +def test_nsolve_precision(): + x, y = symbols('x y') + sol = nsolve(x**2 - pi, x, 3, prec=128) + assert abs(sqrt(pi).evalf(128) - sol) < 1e-128 + assert isinstance(sol, Float) + + sols = nsolve((y**2 - x, x**2 - pi), (x, y), (3, 3), prec=128) + assert isinstance(sols, Matrix) + assert sols.shape == (2, 1) + assert abs(sqrt(pi).evalf(128) - sols[0]) < 1e-128 + assert abs(sqrt(sqrt(pi)).evalf(128) - sols[1]) < 1e-128 + assert all(isinstance(i, Float) for i in sols) + +def test_nsolve_complex(): + x, y = symbols('x y') + + assert nsolve(x**2 + 2, 1j) == sqrt(2.)*I + assert nsolve(x**2 + 2, I) == sqrt(2.)*I + + assert nsolve([x**2 + 2, y**2 + 2], [x, y], [I, I]) == Matrix([sqrt(2.)*I, sqrt(2.)*I]) + assert nsolve([x**2 + 2, y**2 + 2], [x, y], [I, I]) == Matrix([sqrt(2.)*I, sqrt(2.)*I]) + +def test_nsolve_dict_kwarg(): + x, y = symbols('x y') + # one variable + assert nsolve(x**2 - 2, 1, dict = True) == \ + [{x: sqrt(2.)}] + # one variable with complex solution + assert nsolve(x**2 + 2, I, dict = True) == \ + [{x: sqrt(2.)*I}] + # two variables + assert nsolve([x**2 + y**2 - 5, x**2 - y**2 + 1], [x, y], [1, 1], dict = True) == \ + [{x: sqrt(2.), y: sqrt(3.)}] + +def test_nsolve_rational(): + x = symbols('x') + assert nsolve(x - Rational(1, 3), 0, prec=100) == Rational(1, 3).evalf(100) + + +def test_issue_14950(): + x = Matrix(symbols('t s')) + x0 = Matrix([17, 23]) + eqn = x + x0 + assert nsolve(eqn, x, x0) == nfloat(-x0) + assert nsolve(eqn.T, x.T, x0.T) == nfloat(-x0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_pde.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_pde.py new file mode 100644 index 0000000000000000000000000000000000000000..948d90c7be21a9e0e03753e723ef04f1fb08a5d6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_pde.py @@ -0,0 +1,239 @@ +from sympy.core.function import (Derivative as D, Function) +from sympy.core.relational import Eq +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.core import S +from sympy.solvers.pde import (pde_separate, pde_separate_add, pde_separate_mul, + pdsolve, classify_pde, checkpdesol) +from sympy.testing.pytest import raises + + +a, b, c, x, y = symbols('a b c x y') + +def test_pde_separate_add(): + x, y, z, t = symbols("x,y,z,t") + F, T, X, Y, Z, u = map(Function, 'FTXYZu') + + eq = Eq(D(u(x, t), x), D(u(x, t), t)*exp(u(x, t))) + res = pde_separate_add(eq, u(x, t), [X(x), T(t)]) + assert res == [D(X(x), x)*exp(-X(x)), D(T(t), t)*exp(T(t))] + + +def test_pde_separate(): + x, y, z, t = symbols("x,y,z,t") + F, T, X, Y, Z, u = map(Function, 'FTXYZu') + + eq = Eq(D(u(x, t), x), D(u(x, t), t)*exp(u(x, t))) + raises(ValueError, lambda: pde_separate(eq, u(x, t), [X(x), T(t)], 'div')) + + +def test_pde_separate_mul(): + x, y, z, t = symbols("x,y,z,t") + c = Symbol("C", real=True) + Phi = Function('Phi') + F, R, T, X, Y, Z, u = map(Function, 'FRTXYZu') + r, theta, z = symbols('r,theta,z') + + # Something simple :) + eq = Eq(D(F(x, y, z), x) + D(F(x, y, z), y) + D(F(x, y, z), z), 0) + + # Duplicate arguments in functions + raises( + ValueError, lambda: pde_separate_mul(eq, F(x, y, z), [X(x), u(z, z)])) + # Wrong number of arguments + raises(ValueError, lambda: pde_separate_mul(eq, F(x, y, z), [X(x), Y(y)])) + # Wrong variables: [x, y] -> [x, z] + raises( + ValueError, lambda: pde_separate_mul(eq, F(x, y, z), [X(t), Y(x, y)])) + + assert pde_separate_mul(eq, F(x, y, z), [Y(y), u(x, z)]) == \ + [D(Y(y), y)/Y(y), -D(u(x, z), x)/u(x, z) - D(u(x, z), z)/u(x, z)] + assert pde_separate_mul(eq, F(x, y, z), [X(x), Y(y), Z(z)]) == \ + [D(X(x), x)/X(x), -D(Z(z), z)/Z(z) - D(Y(y), y)/Y(y)] + + # wave equation + wave = Eq(D(u(x, t), t, t), c**2*D(u(x, t), x, x)) + res = pde_separate_mul(wave, u(x, t), [X(x), T(t)]) + assert res == [D(X(x), x, x)/X(x), D(T(t), t, t)/(c**2*T(t))] + + # Laplace equation in cylindrical coords + eq = Eq(1/r * D(Phi(r, theta, z), r) + D(Phi(r, theta, z), r, 2) + + 1/r**2 * D(Phi(r, theta, z), theta, 2) + D(Phi(r, theta, z), z, 2), 0) + # Separate z + res = pde_separate_mul(eq, Phi(r, theta, z), [Z(z), u(theta, r)]) + assert res == [D(Z(z), z, z)/Z(z), + -D(u(theta, r), r, r)/u(theta, r) - + D(u(theta, r), r)/(r*u(theta, r)) - + D(u(theta, r), theta, theta)/(r**2*u(theta, r))] + # Lets use the result to create a new equation... + eq = Eq(res[1], c) + # ...and separate theta... + res = pde_separate_mul(eq, u(theta, r), [T(theta), R(r)]) + assert res == [D(T(theta), theta, theta)/T(theta), + -r*D(R(r), r)/R(r) - r**2*D(R(r), r, r)/R(r) - c*r**2] + # ...or r... + res = pde_separate_mul(eq, u(theta, r), [R(r), T(theta)]) + assert res == [r*D(R(r), r)/R(r) + r**2*D(R(r), r, r)/R(r) + c*r**2, + -D(T(theta), theta, theta)/T(theta)] + + +def test_issue_11726(): + x, t = symbols("x t") + f = symbols("f", cls=Function) + X, T = symbols("X T", cls=Function) + + u = f(x, t) + eq = u.diff(x, 2) - u.diff(t, 2) + res = pde_separate(eq, u, [T(x), X(t)]) + assert res == [D(T(x), x, x)/T(x),D(X(t), t, t)/X(t)] + + +def test_pde_classify(): + # When more number of hints are added, add tests for classifying here. + f = Function('f') + eq1 = a*f(x,y) + b*f(x,y).diff(x) + c*f(x,y).diff(y) + eq2 = 3*f(x,y) + 2*f(x,y).diff(x) + f(x,y).diff(y) + eq3 = a*f(x,y) + b*f(x,y).diff(x) + 2*f(x,y).diff(y) + eq4 = x*f(x,y) + f(x,y).diff(x) + 3*f(x,y).diff(y) + eq5 = x**2*f(x,y) + x*f(x,y).diff(x) + x*y*f(x,y).diff(y) + eq6 = y*x**2*f(x,y) + y*f(x,y).diff(x) + f(x,y).diff(y) + for eq in [eq1, eq2, eq3]: + assert classify_pde(eq) == ('1st_linear_constant_coeff_homogeneous',) + for eq in [eq4, eq5, eq6]: + assert classify_pde(eq) == ('1st_linear_variable_coeff',) + + +def test_checkpdesol(): + f, F = map(Function, ['f', 'F']) + eq1 = a*f(x,y) + b*f(x,y).diff(x) + c*f(x,y).diff(y) + eq2 = 3*f(x,y) + 2*f(x,y).diff(x) + f(x,y).diff(y) + eq3 = a*f(x,y) + b*f(x,y).diff(x) + 2*f(x,y).diff(y) + for eq in [eq1, eq2, eq3]: + assert checkpdesol(eq, pdsolve(eq))[0] + eq4 = x*f(x,y) + f(x,y).diff(x) + 3*f(x,y).diff(y) + eq5 = 2*f(x,y) + 1*f(x,y).diff(x) + 3*f(x,y).diff(y) + eq6 = f(x,y) + 1*f(x,y).diff(x) + 3*f(x,y).diff(y) + assert checkpdesol(eq4, [pdsolve(eq5), pdsolve(eq6)]) == [ + (False, (x - 2)*F(3*x - y)*exp(-x/S(5) - 3*y/S(5))), + (False, (x - 1)*F(3*x - y)*exp(-x/S(10) - 3*y/S(10)))] + for eq in [eq4, eq5, eq6]: + assert checkpdesol(eq, pdsolve(eq))[0] + sol = pdsolve(eq4) + sol4 = Eq(sol.lhs - sol.rhs, 0) + raises(NotImplementedError, lambda: + checkpdesol(eq4, sol4, solve_for_func=False)) + + +def test_solvefun(): + f, F, G, H = map(Function, ['f', 'F', 'G', 'H']) + eq1 = f(x,y) + f(x,y).diff(x) + f(x,y).diff(y) + assert pdsolve(eq1) == Eq(f(x, y), F(x - y)*exp(-x/2 - y/2)) + assert pdsolve(eq1, solvefun=G) == Eq(f(x, y), G(x - y)*exp(-x/2 - y/2)) + assert pdsolve(eq1, solvefun=H) == Eq(f(x, y), H(x - y)*exp(-x/2 - y/2)) + + +def test_pde_1st_linear_constant_coeff_homogeneous(): + f, F = map(Function, ['f', 'F']) + u = f(x, y) + eq = 2*u + u.diff(x) + u.diff(y) + assert classify_pde(eq) == ('1st_linear_constant_coeff_homogeneous',) + sol = pdsolve(eq) + assert sol == Eq(u, F(x - y)*exp(-x - y)) + assert checkpdesol(eq, sol)[0] + + eq = 4 + (3*u.diff(x)/u) + (2*u.diff(y)/u) + assert classify_pde(eq) == ('1st_linear_constant_coeff_homogeneous',) + sol = pdsolve(eq) + assert sol == Eq(u, F(2*x - 3*y)*exp(-S(12)*x/13 - S(8)*y/13)) + assert checkpdesol(eq, sol)[0] + + eq = u + (6*u.diff(x)) + (7*u.diff(y)) + assert classify_pde(eq) == ('1st_linear_constant_coeff_homogeneous',) + sol = pdsolve(eq) + assert sol == Eq(u, F(7*x - 6*y)*exp(-6*x/S(85) - 7*y/S(85))) + assert checkpdesol(eq, sol)[0] + + eq = a*u + b*u.diff(x) + c*u.diff(y) + sol = pdsolve(eq) + assert checkpdesol(eq, sol)[0] + + +def test_pde_1st_linear_constant_coeff(): + f, F = map(Function, ['f', 'F']) + u = f(x,y) + eq = -2*u.diff(x) + 4*u.diff(y) + 5*u - exp(x + 3*y) + sol = pdsolve(eq) + assert sol == Eq(f(x,y), + (F(4*x + 2*y)*exp(x/2) + exp(x + 4*y)/15)*exp(-y)) + assert classify_pde(eq) == ('1st_linear_constant_coeff', + '1st_linear_constant_coeff_Integral') + assert checkpdesol(eq, sol)[0] + + eq = (u.diff(x)/u) + (u.diff(y)/u) + 1 - (exp(x + y)/u) + sol = pdsolve(eq) + assert sol == Eq(f(x, y), F(x - y)*exp(-x/2 - y/2) + exp(x + y)/3) + assert classify_pde(eq) == ('1st_linear_constant_coeff', + '1st_linear_constant_coeff_Integral') + assert checkpdesol(eq, sol)[0] + + eq = 2*u + -u.diff(x) + 3*u.diff(y) + sin(x) + sol = pdsolve(eq) + assert sol == Eq(f(x, y), + F(3*x + y)*exp(x/5 - 3*y/5) - 2*sin(x)/5 - cos(x)/5) + assert classify_pde(eq) == ('1st_linear_constant_coeff', + '1st_linear_constant_coeff_Integral') + assert checkpdesol(eq, sol)[0] + + eq = u + u.diff(x) + u.diff(y) + x*y + sol = pdsolve(eq) + assert sol.expand() == Eq(f(x, y), + x + y + (x - y)**2/4 - (x + y)**2/4 + F(x - y)*exp(-x/2 - y/2) - 2).expand() + assert classify_pde(eq) == ('1st_linear_constant_coeff', + '1st_linear_constant_coeff_Integral') + assert checkpdesol(eq, sol)[0] + eq = u + u.diff(x) + u.diff(y) + log(x) + assert classify_pde(eq) == ('1st_linear_constant_coeff', + '1st_linear_constant_coeff_Integral') + + +def test_pdsolve_all(): + f, F = map(Function, ['f', 'F']) + u = f(x,y) + eq = u + u.diff(x) + u.diff(y) + x**2*y + sol = pdsolve(eq, hint = 'all') + keys = ['1st_linear_constant_coeff', + '1st_linear_constant_coeff_Integral', 'default', 'order'] + assert sorted(sol.keys()) == keys + assert sol['order'] == 1 + assert sol['default'] == '1st_linear_constant_coeff' + assert sol['1st_linear_constant_coeff'].expand() == Eq(f(x, y), + -x**2*y + x**2 + 2*x*y - 4*x - 2*y + F(x - y)*exp(-x/2 - y/2) + 6).expand() + + +def test_pdsolve_variable_coeff(): + f, F = map(Function, ['f', 'F']) + u = f(x, y) + eq = x*(u.diff(x)) - y*(u.diff(y)) + y**2*u - y**2 + sol = pdsolve(eq, hint="1st_linear_variable_coeff") + assert sol == Eq(u, F(x*y)*exp(y**2/2) + 1) + assert checkpdesol(eq, sol)[0] + + eq = x**2*u + x*u.diff(x) + x*y*u.diff(y) + sol = pdsolve(eq, hint='1st_linear_variable_coeff') + assert sol == Eq(u, F(y*exp(-x))*exp(-x**2/2)) + assert checkpdesol(eq, sol)[0] + + eq = y*x**2*u + y*u.diff(x) + u.diff(y) + sol = pdsolve(eq, hint='1st_linear_variable_coeff') + assert sol == Eq(u, F(-2*x + y**2)*exp(-x**3/3)) + assert checkpdesol(eq, sol)[0] + + eq = exp(x)**2*(u.diff(x)) + y + sol = pdsolve(eq, hint='1st_linear_variable_coeff') + assert sol == Eq(u, y*exp(-2*x)/2 + F(y)) + assert checkpdesol(eq, sol)[0] + + eq = exp(2*x)*(u.diff(y)) + y*u - u + sol = pdsolve(eq, hint='1st_linear_variable_coeff') + assert sol == Eq(u, F(x)*exp(-y*(y - 2)*exp(-2*x)/2)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_polysys.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_polysys.py new file mode 100644 index 0000000000000000000000000000000000000000..a119591a0354ba377a18767eae6e8b7812810a0d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_polysys.py @@ -0,0 +1,462 @@ +"""Tests for solvers of systems of polynomial equations. """ +from sympy.polys.domains import ZZ, QQ_I +from sympy.core.numbers import (I, Integer, Rational) +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.polys.domains.rationalfield import QQ +from sympy.polys.polyerrors import UnsolvableFactorError +from sympy.polys.polyoptions import Options +from sympy.polys.polytools import Poly +from sympy.polys.rootoftools import CRootOf +from sympy.solvers.solvers import solve +from sympy.utilities.iterables import flatten +from sympy.abc import a, b, c, x, y, z +from sympy.polys import PolynomialError +from sympy.solvers.polysys import (solve_poly_system, + solve_triangulated, + solve_biquadratic, SolveFailed, + solve_generic, factor_system_bool, + factor_system_cond, factor_system_poly, + factor_system, _factor_sets, _factor_sets_slow) +from sympy.polys.polytools import parallel_poly_from_expr +from sympy.testing.pytest import raises +from sympy.core.relational import Eq +from sympy.functions.elementary.trigonometric import sin, cos + +from sympy.functions.elementary.exponential import exp + + +def test_solve_poly_system(): + assert solve_poly_system([x - 1], x) == [(S.One,)] + + assert solve_poly_system([y - x, y - x - 1], x, y) is None + + assert solve_poly_system([y - x**2, y + x**2], x, y) == [(S.Zero, S.Zero)] + + assert solve_poly_system([2*x - 3, y*Rational(3, 2) - 2*x, z - 5*y], x, y, z) == \ + [(Rational(3, 2), Integer(2), Integer(10))] + + assert solve_poly_system([x*y - 2*y, 2*y**2 - x**2], x, y) == \ + [(0, 0), (2, -sqrt(2)), (2, sqrt(2))] + + assert solve_poly_system([y - x**2, y + x**2 + 1], x, y) == \ + [(-I*sqrt(S.Half), Rational(-1, 2)), (I*sqrt(S.Half), Rational(-1, 2))] + + f_1 = x**2 + y + z - 1 + f_2 = x + y**2 + z - 1 + f_3 = x + y + z**2 - 1 + + a, b = sqrt(2) - 1, -sqrt(2) - 1 + + assert solve_poly_system([f_1, f_2, f_3], x, y, z) == \ + [(0, 0, 1), (0, 1, 0), (1, 0, 0), (a, a, a), (b, b, b)] + + solution = [(1, -1), (1, 1)] + + assert solve_poly_system([Poly(x**2 - y**2), Poly(x - 1)]) == solution + assert solve_poly_system([x**2 - y**2, x - 1], x, y) == solution + assert solve_poly_system([x**2 - y**2, x - 1]) == solution + + assert solve_poly_system( + [x + x*y - 3, y + x*y - 4], x, y) == [(-3, -2), (1, 2)] + + raises(NotImplementedError, lambda: solve_poly_system([x**3 - y**3], x, y)) + raises(NotImplementedError, lambda: solve_poly_system( + [z, -2*x*y**2 + x + y**2*z, y**2*(-z - 4) + 2])) + raises(PolynomialError, lambda: solve_poly_system([1/x], x)) + + raises(NotImplementedError, lambda: solve_poly_system( + [x-1,], (x, y))) + raises(NotImplementedError, lambda: solve_poly_system( + [y-1,], (x, y))) + + # solve_poly_system should ideally construct solutions using + # CRootOf for the following four tests + assert solve_poly_system([x**5 - x + 1], [x], strict=False) == [] + raises(UnsolvableFactorError, lambda: solve_poly_system( + [x**5 - x + 1], [x], strict=True)) + + assert solve_poly_system([(x - 1)*(x**5 - x + 1), y**2 - 1], [x, y], + strict=False) == [(1, -1), (1, 1)] + raises(UnsolvableFactorError, + lambda: solve_poly_system([(x - 1)*(x**5 - x + 1), y**2-1], + [x, y], strict=True)) + + +def test_solve_generic(): + NewOption = Options((x, y), {'domain': 'ZZ'}) + assert solve_generic([x**2 - 2*y**2, y**2 - y + 1], NewOption) == \ + [(-sqrt(-1 - sqrt(3)*I), Rational(1, 2) - sqrt(3)*I/2), + (sqrt(-1 - sqrt(3)*I), Rational(1, 2) - sqrt(3)*I/2), + (-sqrt(-1 + sqrt(3)*I), Rational(1, 2) + sqrt(3)*I/2), + (sqrt(-1 + sqrt(3)*I), Rational(1, 2) + sqrt(3)*I/2)] + + # solve_generic should ideally construct solutions using + # CRootOf for the following two tests + assert solve_generic( + [2*x - y, (y - 1)*(y**5 - y + 1)], NewOption, strict=False) == \ + [(Rational(1, 2), 1)] + raises(UnsolvableFactorError, lambda: solve_generic( + [2*x - y, (y - 1)*(y**5 - y + 1)], NewOption, strict=True)) + + +def test_solve_biquadratic(): + x0, y0, x1, y1, r = symbols('x0 y0 x1 y1 r') + + f_1 = (x - 1)**2 + (y - 1)**2 - r**2 + f_2 = (x - 2)**2 + (y - 2)**2 - r**2 + s = sqrt(2*r**2 - 1) + a = (3 - s)/2 + b = (3 + s)/2 + assert solve_poly_system([f_1, f_2], x, y) == [(a, b), (b, a)] + + f_1 = (x - 1)**2 + (y - 2)**2 - r**2 + f_2 = (x - 1)**2 + (y - 1)**2 - r**2 + + assert solve_poly_system([f_1, f_2], x, y) == \ + [(1 - sqrt((2*r - 1)*(2*r + 1))/2, Rational(3, 2)), + (1 + sqrt((2*r - 1)*(2*r + 1))/2, Rational(3, 2))] + + query = lambda expr: expr.is_Pow and expr.exp is S.Half + + f_1 = (x - 1 )**2 + (y - 2)**2 - r**2 + f_2 = (x - x1)**2 + (y - 1)**2 - r**2 + + result = solve_poly_system([f_1, f_2], x, y) + + assert len(result) == 2 and all(len(r) == 2 for r in result) + assert all(r.count(query) == 1 for r in flatten(result)) + + f_1 = (x - x0)**2 + (y - y0)**2 - r**2 + f_2 = (x - x1)**2 + (y - y1)**2 - r**2 + + result = solve_poly_system([f_1, f_2], x, y) + + assert len(result) == 2 and all(len(r) == 2 for r in result) + assert all(len(r.find(query)) == 1 for r in flatten(result)) + + s1 = (x*y - y, x**2 - x) + assert solve(s1) == [{x: 1}, {x: 0, y: 0}] + s2 = (x*y - x, y**2 - y) + assert solve(s2) == [{y: 1}, {x: 0, y: 0}] + gens = (x, y) + for seq in (s1, s2): + (f, g), opt = parallel_poly_from_expr(seq, *gens) + raises(SolveFailed, lambda: solve_biquadratic(f, g, opt)) + seq = (x**2 + y**2 - 2, y**2 - 1) + (f, g), opt = parallel_poly_from_expr(seq, *gens) + assert solve_biquadratic(f, g, opt) == [ + (-1, -1), (-1, 1), (1, -1), (1, 1)] + ans = [(0, -1), (0, 1)] + seq = (x**2 + y**2 - 1, y**2 - 1) + (f, g), opt = parallel_poly_from_expr(seq, *gens) + assert solve_biquadratic(f, g, opt) == ans + seq = (x**2 + y**2 - 1, x**2 - x + y**2 - 1) + (f, g), opt = parallel_poly_from_expr(seq, *gens) + assert solve_biquadratic(f, g, opt) == ans + + +def test_solve_triangulated(): + f_1 = x**2 + y + z - 1 + f_2 = x + y**2 + z - 1 + f_3 = x + y + z**2 - 1 + + a, b = sqrt(2) - 1, -sqrt(2) - 1 + + assert solve_triangulated([f_1, f_2, f_3], x, y, z) == \ + [(0, 0, 1), (0, 1, 0), (1, 0, 0)] + + dom = QQ.algebraic_field(sqrt(2)) + + assert solve_triangulated([f_1, f_2, f_3], x, y, z, domain=dom) == \ + [(0, 0, 1), (0, 1, 0), (1, 0, 0), (a, a, a), (b, b, b)] + + a, b = CRootOf(z**2 + 2*z - 1, 0), CRootOf(z**2 + 2*z - 1, 1) + assert solve_triangulated([f_1, f_2, f_3], x, y, z, extension=True) == \ + [(0, 0, 1), (0, 1, 0), (1, 0, 0), (a, a, a), (b, b, b)] + + +def test_solve_issue_3686(): + roots = solve_poly_system([((x - 5)**2/250000 + (y - Rational(5, 10))**2/250000) - 1, x], x, y) + assert roots == [(0, S.Half - 15*sqrt(1111)), (0, S.Half + 15*sqrt(1111))] + + roots = solve_poly_system([((x - 5)**2/250000 + (y - 5.0/10)**2/250000) - 1, x], x, y) + # TODO: does this really have to be so complicated?! + assert len(roots) == 2 + assert roots[0][0] == 0 + assert roots[0][1].epsilon_eq(-499.474999374969, 1e12) + assert roots[1][0] == 0 + assert roots[1][1].epsilon_eq(500.474999374969, 1e12) + + +def test_factor_system(): + + assert factor_system([x**2 + 2*x + 1]) == [[x + 1]] + assert factor_system([x**2 + 2*x + 1, y**2 + 2*y + 1]) == [[x + 1, y + 1]] + assert factor_system([x**2 + 1]) == [[x**2 + 1]] + assert factor_system([]) == [[]] + + assert factor_system([x**2 + y**2 + 2*x*y, x**2 - 2], extension=sqrt(2)) == [ + [x + y, x + sqrt(2)], + [x + y, x - sqrt(2)], + ] + + assert factor_system([x**2 + 1, y**2 + 1], gaussian=True) == [ + [x + I, y + I], + [x + I, y - I], + [x - I, y + I], + [x - I, y - I], + ] + + assert factor_system([x**2 + 1, y**2 + 1], domain=QQ_I) == [ + [x + I, y + I], + [x + I, y - I], + [x - I, y + I], + [x - I, y - I], + ] + + assert factor_system([0]) == [[]] + assert factor_system([1]) == [] + assert factor_system([0 , x]) == [[x]] + assert factor_system([1, 0, x]) == [] + + assert factor_system([x**4 - 1, y**6 - 1]) == [ + [x**2 + 1, y**2 + y + 1], + [x**2 + 1, y**2 - y + 1], + [x**2 + 1, y + 1], + [x**2 + 1, y - 1], + [x + 1, y**2 + y + 1], + [x + 1, y**2 - y + 1], + [x - 1, y**2 + y + 1], + [x - 1, y**2 - y + 1], + [x + 1, y + 1], + [x + 1, y - 1], + [x - 1, y + 1], + [x - 1, y - 1], + ] + + assert factor_system([(x - 1)*(y - 2), (y - 2)*(z - 3)]) == [ + [x - 1, z - 3], + [y - 2] + ] + + assert factor_system([sin(x)**2 + cos(x)**2 - 1, x]) == [ + [x, sin(x)**2 + cos(x)**2 - 1], + ] + + assert factor_system([sin(x)**2 + cos(x)**2 - 1]) == [ + [sin(x)**2 + cos(x)**2 - 1] + ] + + assert factor_system([sin(x)**2 + cos(x)**2]) == [ + [sin(x)**2 + cos(x)**2] + ] + + assert factor_system([a*x, y, a]) == [[y, a]] + + assert factor_system([a*x, y, a], [x, y]) == [] + + assert factor_system([a ** 2 * x, y], [x, y]) == [[x, y]] + + assert factor_system([a*x*(x - 1), b*y, c], [x, y]) == [] + + assert factor_system([a*x*(x - 1), b*y, c], [x, y, c]) == [ + [x - 1, y, c], + [x, y, c], + ] + + assert factor_system([a*x*(x - 1), b*y, c]) == [ + [x - 1, y, c], + [x, y, c], + [x - 1, b, c], + [x, b, c], + [y, a, c], + [a, b, c], + ] + + assert factor_system([x**2 - 2], [y]) == [] + + assert factor_system([x**2 - 2], [x]) == [[x**2 - 2]] + + assert factor_system([cos(x)**2 - sin(x)**2, cos(x)**2 + sin(x)**2 - 1]) == [ + [sin(x)**2 + cos(x)**2 - 1, sin(x) + cos(x)], + [sin(x)**2 + cos(x)**2 - 1, -sin(x) + cos(x)], + ] + + assert factor_system([(cos(x) + sin(x))**2 - 1, cos(x)**2 - sin(x)**2 - cos(2*x)]) == [ + [sin(x)**2 - cos(x)**2 + cos(2*x), sin(x) + cos(x) + 1], + [sin(x)**2 - cos(x)**2 + cos(2*x), sin(x) + cos(x) - 1], + ] + + assert factor_system([(cos(x) + sin(x))*exp(y) - 1, (cos(x) - sin(x))*exp(y) - 1]) == [ + [exp(y)*sin(x) + exp(y)*cos(x) - 1, -exp(y)*sin(x) + exp(y)*cos(x) - 1] + ] + + +def test_factor_system_poly(): + + px = lambda e: Poly(e, x) + pxab = lambda e: Poly(e, x, domain=ZZ[a, b]) + pxI = lambda e: Poly(e, x, domain=QQ_I) + pxyz = lambda e: Poly(e, (x, y, z)) + + assert factor_system_poly([px(x**2 - 1), px(x**2 - 4)]) == [ + [px(x + 2), px(x + 1)], + [px(x + 2), px(x - 1)], + [px(x + 1), px(x - 2)], + [px(x - 1), px(x - 2)], + ] + + assert factor_system_poly([px(x**2 - 1)]) == [[px(x + 1)], [px(x - 1)]] + + assert factor_system_poly([pxyz(x**2*y - y), pxyz(x**2*z - z)]) == [ + [pxyz(x + 1)], + [pxyz(x - 1)], + [pxyz(y), pxyz(z)], + ] + + assert factor_system_poly([px(x**2*(x - 1)**2), px(x*(x - 1))]) == [ + [px(x)], + [px(x - 1)], + ] + + assert factor_system_poly([pxyz(x**2 + y*x), pxyz(x**2 + z*x)]) == [ + [pxyz(x + y), pxyz(x + z)], + [pxyz(x)], + ] + + assert factor_system_poly([pxab((a - 1)*(x - 2)), pxab((b - 3)*(x - 2))]) == [ + [pxab(x - 2)], + [pxab(a - 1), pxab(b - 3)], + ] + + assert factor_system_poly([pxI(x**2 + 1)]) == [[pxI(x + I)], [pxI(x - I)]] + + assert factor_system_poly([]) == [[]] + + assert factor_system_poly([px(1)]) == [] + assert factor_system_poly([px(0), px(x)]) == [[px(x)]] + + +def test_factor_system_cond(): + + assert factor_system_cond([x ** 2 - 1, x ** 2 - 4]) == [ + [x + 2, x + 1], + [x + 2, x - 1], + [x + 1, x - 2], + [x - 1, x - 2], + ] + + assert factor_system_cond([1]) == [] + assert factor_system_cond([0]) == [[]] + assert factor_system_cond([1, x]) == [] + assert factor_system_cond([0, x]) == [[x]] + assert factor_system_cond([]) == [[]] + + assert factor_system_cond([x**2 + y*x]) == [[x + y], [x]] + + assert factor_system_cond([(a - 1)*(x - 2), (b - 3)*(x - 2)], [x]) == [ + [x - 2], + [a - 1, b - 3], + ] + + assert factor_system_cond([a * (x - 1), b], [x]) == [[x - 1, b], [a, b]] + + assert factor_system_cond([a*x*(x-1), b*y, c], [x, y]) == [ + [x - 1, y, c], + [x, y, c], + [x - 1, b, c], + [x, b, c], + [y, a, c], + [a, b, c], + ] + + assert factor_system_cond([x*(x-1), y], [x, y]) == [[x - 1, y], [x, y]] + + assert factor_system_cond([a*x, y, a], [x, y]) == [[y, a]] + + assert factor_system_cond([a*x, b*x], [x, y]) == [[x], [a, b]] + + assert factor_system_cond([a*b*x, y], [x, y]) == [[x, y], [y, a*b]] + + assert factor_system_cond([a*b*x, y]) == [[x, y], [y, a], [y, b]] + + assert factor_system_cond([a**2*x, y], [x, y]) == [[x, y], [y, a]] + +def test_factor_system_bool(): + + eqs = [a*(x - 1)*(y - 1), b*(x - 2)*(y - 1)*(y - 2)] + assert factor_system_bool(eqs, [x, y]) == ( + Eq(y - 1, 0) + | (Eq(a, 0) & Eq(b, 0)) + | (Eq(a, 0) & Eq(x - 2, 0)) + | (Eq(a, 0) & Eq(y - 2, 0)) + | (Eq(b, 0) & Eq(x - 1, 0)) + | (Eq(x - 2, 0) & Eq(x - 1, 0)) + | (Eq(x - 1, 0) & Eq(y - 2, 0)) + ) + + assert factor_system_bool([x - 1], [x]) == Eq(x - 1, 0) + + assert factor_system_bool([(x - 1)*(x - 2)], [x]) == Eq(x - 2, 0) | Eq(x - 1, 0) + + assert factor_system_bool([], [x]) == True + assert factor_system_bool([0], [x]) == True + assert factor_system_bool([1], [x]) == False + assert factor_system_bool([a], [x]) == Eq(a, 0) + + assert factor_system_bool([a * x, y, a], [x, y]) == Eq(a, 0) & Eq(y, 0) + + assert (factor_system_bool([a*x, b*y*x, a], [x, y]) == ( + Eq(a, 0) & Eq(b, 0)) + | (Eq(a, 0) & Eq(x, 0)) + | (Eq(a, 0) & Eq(y, 0))) + + assert (factor_system_bool([a*x, b*x], [x, y]) == Eq(x, 0) | + (Eq(a, 0) & Eq(b, 0))) + + assert (factor_system_bool([a*b*x, y], [x, y]) == ( + Eq(x, 0) & Eq(y, 0)) | + (Eq(y, 0) & Eq(a*b, 0))) + + assert (factor_system_bool([a**2*x, y], [x, y]) == ( + Eq(a, 0) & Eq(y, 0)) | + (Eq(x, 0) & Eq(y, 0))) + + assert factor_system_bool([a*x*y, b*y*z], [x, y, z]) == ( + Eq(y, 0) + | (Eq(a, 0) & Eq(b, 0)) + | (Eq(a, 0) & Eq(z, 0)) + | (Eq(b, 0) & Eq(x, 0)) + | (Eq(x, 0) & Eq(z, 0)) + ) + + assert factor_system_bool([a*(x - 1), b], [x]) == ( + (Eq(a, 0) & Eq(b, 0)) + | (Eq(x - 1, 0) & Eq(b, 0)) + ) + + +def test_factor_sets(): + # + from random import randint + + def generate_random_system(n_eqs=3, n_factors=2, max_val=10): + return [ + [randint(0, max_val) for _ in range(randint(1, n_factors))] + for _ in range(n_eqs) + ] + + test_cases = [ + [[1, 2], [1, 3]], + [[1, 2], [3, 4]], + [[1], [1, 2], [2]], + ] + + for case in test_cases: + assert _factor_sets(case) == _factor_sets_slow(case) + + for _ in range(100): + system = generate_random_system() + assert _factor_sets(system) == _factor_sets_slow(system) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_recurr.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_recurr.py new file mode 100644 index 0000000000000000000000000000000000000000..5a6306b51a5cf33ccd9fae131430a24690d540a7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_recurr.py @@ -0,0 +1,295 @@ +from sympy.core.function import (Function, Lambda, expand) +from sympy.core.numbers import (I, Rational) +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.combinatorial.factorials import (rf, binomial, factorial) +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.polys.polytools import factor +from sympy.solvers.recurr import rsolve, rsolve_hyper, rsolve_poly, rsolve_ratio +from sympy.testing.pytest import raises, slow, XFAIL +from sympy.abc import a, b + +y = Function('y') +n, k = symbols('n,k', integer=True) +C0, C1, C2 = symbols('C0,C1,C2') + + +def test_rsolve_poly(): + assert rsolve_poly([-1, -1, 1], 0, n) == 0 + assert rsolve_poly([-1, -1, 1], 1, n) == -1 + + assert rsolve_poly([-1, n + 1], n, n) == 1 + assert rsolve_poly([-1, 1], n, n) == C0 + (n**2 - n)/2 + assert rsolve_poly([-n - 1, n], 1, n) == C0*n - 1 + assert rsolve_poly([-4*n - 2, 1], 4*n + 1, n) == -1 + + assert rsolve_poly([-1, 1], n**5 + n**3, n) == \ + C0 - n**3 / 2 - n**5 / 2 + n**2 / 6 + n**6 / 6 + 2*n**4 / 3 + + +def test_rsolve_ratio(): + solution = rsolve_ratio([-2*n**3 + n**2 + 2*n - 1, 2*n**3 + n**2 - 6*n, + -2*n**3 - 11*n**2 - 18*n - 9, 2*n**3 + 13*n**2 + 22*n + 8], 0, n) + assert solution == C0*(2*n - 3)/(n**2 - 1)/2 + + +def test_rsolve_hyper(): + assert rsolve_hyper([-1, -1, 1], 0, n) in [ + C0*(S.Half - S.Half*sqrt(5))**n + C1*(S.Half + S.Half*sqrt(5))**n, + C1*(S.Half - S.Half*sqrt(5))**n + C0*(S.Half + S.Half*sqrt(5))**n, + ] + + assert rsolve_hyper([n**2 - 2, -2*n - 1, 1], 0, n) in [ + C0*rf(sqrt(2), n) + C1*rf(-sqrt(2), n), + C1*rf(sqrt(2), n) + C0*rf(-sqrt(2), n), + ] + + assert rsolve_hyper([n**2 - k, -2*n - 1, 1], 0, n) in [ + C0*rf(sqrt(k), n) + C1*rf(-sqrt(k), n), + C1*rf(sqrt(k), n) + C0*rf(-sqrt(k), n), + ] + + assert rsolve_hyper( + [2*n*(n + 1), -n**2 - 3*n + 2, n - 1], 0, n) == C1*factorial(n) + C0*2**n + + assert rsolve_hyper( + [n + 2, -(2*n + 3)*(17*n**2 + 51*n + 39), n + 1], 0, n) == 0 + + assert rsolve_hyper([-n - 1, -1, 1], 0, n) == 0 + + assert rsolve_hyper([-1, 1], n, n).expand() == C0 + n**2/2 - n/2 + + assert rsolve_hyper([-1, 1], 1 + n, n).expand() == C0 + n**2/2 + n/2 + + assert rsolve_hyper([-1, 1], 3*(n + n**2), n).expand() == C0 + n**3 - n + + assert rsolve_hyper([-a, 1],0,n).expand() == C0*a**n + + assert rsolve_hyper([-a, 0, 1], 0, n).expand() == (-1)**n*C1*a**(n/2) + C0*a**(n/2) + + assert rsolve_hyper([1, 1, 1], 0, n).expand() == \ + C0*(Rational(-1, 2) - sqrt(3)*I/2)**n + C1*(Rational(-1, 2) + sqrt(3)*I/2)**n + + assert rsolve_hyper([1, -2*n/a - 2/a, 1], 0, n) == 0 + + +@XFAIL +def test_rsolve_ratio_missed(): + # this arises during computation + # assert rsolve_hyper([-1, 1], 3*(n + n**2), n).expand() == C0 + n**3 - n + assert rsolve_ratio([-n, n + 2], n, n) is not None + + +def recurrence_term(c, f): + """Compute RHS of recurrence in f(n) with coefficients in c.""" + return sum(c[i]*f.subs(n, n + i) for i in range(len(c))) + + +def test_rsolve_bulk(): + """Some bulk-generated tests.""" + funcs = [ n, n + 1, n**2, n**3, n**4, n + n**2, 27*n + 52*n**2 - 3* + n**3 + 12*n**4 - 52*n**5 ] + coeffs = [ [-2, 1], [-2, -1, 1], [-1, 1, 1, -1, 1], [-n, 1], [n**2 - + n + 12, 1] ] + for p in funcs: + # compute difference + for c in coeffs: + q = recurrence_term(c, p) + if p.is_polynomial(n): + assert rsolve_poly(c, q, n) == p + # See issue 3956: + if p.is_hypergeometric(n) and len(c) <= 3: + assert rsolve_hyper(c, q, n).subs(zip(symbols('C:3'), [0, 0, 0])).expand() == p + + +def test_rsolve_0_sol_homogeneous(): + # fixed by cherry-pick from + # https://github.com/diofant/diofant/commit/e1d2e52125199eb3df59f12e8944f8a5f24b00a5 + assert rsolve_hyper([n**2 - n + 12, 1], n*(n**2 - n + 12) + n + 1, n) == n + + +def test_rsolve(): + f = y(n + 2) - y(n + 1) - y(n) + h = sqrt(5)*(S.Half + S.Half*sqrt(5))**n \ + - sqrt(5)*(S.Half - S.Half*sqrt(5))**n + + assert rsolve(f, y(n)) in [ + C0*(S.Half - S.Half*sqrt(5))**n + C1*(S.Half + S.Half*sqrt(5))**n, + C1*(S.Half - S.Half*sqrt(5))**n + C0*(S.Half + S.Half*sqrt(5))**n, + ] + + assert rsolve(f, y(n), [0, 5]) == h + assert rsolve(f, y(n), {0: 0, 1: 5}) == h + assert rsolve(f, y(n), {y(0): 0, y(1): 5}) == h + assert rsolve(y(n) - y(n - 1) - y(n - 2), y(n), [0, 5]) == h + assert rsolve(Eq(y(n), y(n - 1) + y(n - 2)), y(n), [0, 5]) == h + + assert f.subs(y, Lambda(k, rsolve(f, y(n)).subs(n, k))).simplify() == 0 + + f = (n - 1)*y(n + 2) - (n**2 + 3*n - 2)*y(n + 1) + 2*n*(n + 1)*y(n) + g = C1*factorial(n) + C0*2**n + h = -3*factorial(n) + 3*2**n + + assert rsolve(f, y(n)) == g + assert rsolve(f, y(n), []) == g + assert rsolve(f, y(n), {}) == g + + assert rsolve(f, y(n), [0, 3]) == h + assert rsolve(f, y(n), {0: 0, 1: 3}) == h + assert rsolve(f, y(n), {y(0): 0, y(1): 3}) == h + + assert f.subs(y, Lambda(k, rsolve(f, y(n)).subs(n, k))).simplify() == 0 + + f = y(n) - y(n - 1) - 2 + + assert rsolve(f, y(n), {y(0): 0}) == 2*n + assert rsolve(f, y(n), {y(0): 1}) == 2*n + 1 + assert rsolve(f, y(n), {y(0): 0, y(1): 1}) is None + + assert f.subs(y, Lambda(k, rsolve(f, y(n)).subs(n, k))).simplify() == 0 + + f = 3*y(n - 1) - y(n) - 1 + + assert rsolve(f, y(n), {y(0): 0}) == -3**n/2 + S.Half + assert rsolve(f, y(n), {y(0): 1}) == 3**n/2 + S.Half + assert rsolve(f, y(n), {y(0): 2}) == 3*3**n/2 + S.Half + + assert f.subs(y, Lambda(k, rsolve(f, y(n)).subs(n, k))).simplify() == 0 + + f = y(n) - 1/n*y(n - 1) + assert rsolve(f, y(n)) == C0/factorial(n) + assert f.subs(y, Lambda(k, rsolve(f, y(n)).subs(n, k))).simplify() == 0 + + f = y(n) - 1/n*y(n - 1) - 1 + assert rsolve(f, y(n)) is None + + f = 2*y(n - 1) + (1 - n)*y(n)/n + + assert rsolve(f, y(n), {y(1): 1}) == 2**(n - 1)*n + assert rsolve(f, y(n), {y(1): 2}) == 2**(n - 1)*n*2 + assert rsolve(f, y(n), {y(1): 3}) == 2**(n - 1)*n*3 + + assert f.subs(y, Lambda(k, rsolve(f, y(n)).subs(n, k))).simplify() == 0 + + f = (n - 1)*(n - 2)*y(n + 2) - (n + 1)*(n + 2)*y(n) + + assert rsolve(f, y(n), {y(3): 6, y(4): 24}) == n*(n - 1)*(n - 2) + assert rsolve( + f, y(n), {y(3): 6, y(4): -24}) == -n*(n - 1)*(n - 2)*(-1)**(n) + + assert f.subs(y, Lambda(k, rsolve(f, y(n)).subs(n, k))).simplify() == 0 + + assert rsolve(Eq(y(n + 1), a*y(n)), y(n), {y(1): a}).simplify() == a**n + + assert rsolve(y(n) - a*y(n-2),y(n), \ + {y(1): sqrt(a)*(a + b), y(2): a*(a - b)}).simplify() == \ + a**(n/2 + 1) - b*(-sqrt(a))**n + + f = (-16*n**2 + 32*n - 12)*y(n - 1) + (4*n**2 - 12*n + 9)*y(n) + + yn = rsolve(f, y(n), {y(1): binomial(2*n + 1, 3)}) + sol = 2**(2*n)*n*(2*n - 1)**2*(2*n + 1)/12 + assert factor(expand(yn, func=True)) == sol + + sol = rsolve(y(n) + a*(y(n + 1) + y(n - 1))/2, y(n)) + assert str(sol) == 'C0*((-sqrt(1 - a**2) - 1)/a)**n + C1*((sqrt(1 - a**2) - 1)/a)**n' + + assert rsolve((k + 1)*y(k), y(k)) is None + assert (rsolve((k + 1)*y(k) + (k + 3)*y(k + 1) + (k + 5)*y(k + 2), y(k)) + is None) + + assert rsolve(y(n) + y(n + 1) + 2**n + 3**n, y(n)) == (-1)**n*C0 - 2**n/3 - 3**n/4 + + +def test_rsolve_raises(): + x = Function('x') + raises(ValueError, lambda: rsolve(y(n) - y(k + 1), y(n))) + raises(ValueError, lambda: rsolve(y(n) - y(n + 1), x(n))) + raises(ValueError, lambda: rsolve(y(n) - x(n + 1), y(n))) + raises(ValueError, lambda: rsolve(y(n) - sqrt(n)*y(n + 1), y(n))) + raises(ValueError, lambda: rsolve(y(n) - y(n + 1), y(n), {x(0): 0})) + raises(ValueError, lambda: rsolve(y(n) + y(n + 1) + 2**n + cos(n), y(n))) + + +def test_issue_6844(): + f = y(n + 2) - y(n + 1) + y(n)/4 + assert rsolve(f, y(n)) == 2**(-n + 1)*C1*n + 2**(-n)*C0 + assert rsolve(f, y(n), {y(0): 0, y(1): 1}) == 2**(1 - n)*n + + +def test_issue_18751(): + r = Symbol('r', positive=True) + theta = Symbol('theta', real=True) + f = y(n) - 2 * r * cos(theta) * y(n - 1) + r**2 * y(n - 2) + assert rsolve(f, y(n)) == \ + C0*(r*(cos(theta) - I*Abs(sin(theta))))**n + C1*(r*(cos(theta) + I*Abs(sin(theta))))**n + + +def test_constant_naming(): + #issue 8697 + assert rsolve(y(n+3) - y(n+2) - y(n+1) + y(n), y(n)) == (-1)**n*C1 + C0 + C2*n + assert rsolve(y(n+3)+3*y(n+2)+3*y(n+1)+y(n), y(n)).expand() == (-1)**n*C0 - (-1)**n*C1*n - (-1)**n*C2*n**2 + assert rsolve(y(n) - 2*y(n - 3) + 5*y(n - 2) - 4*y(n - 1),y(n),[1,3,8]) == 3*2**n - n - 2 + + #issue 19630 + assert rsolve(y(n+3) - 3*y(n+1) + 2*y(n), y(n), {y(1):0, y(2):8, y(3):-2}) == (-2)**n + 2*n + + +@slow +def test_issue_15751(): + f = y(n) + 21*y(n + 1) - 273*y(n + 2) - 1092*y(n + 3) + 1820*y(n + 4) + 1092*y(n + 5) - 273*y(n + 6) - 21*y(n + 7) + y(n + 8) + assert rsolve(f, y(n)) is not None + + +def test_issue_17990(): + f = -10*y(n) + 4*y(n + 1) + 6*y(n + 2) + 46*y(n + 3) + sol = rsolve(f, y(n)) + expected = C0*((86*18**(S(1)/3)/69 + (-12 + (-1 + sqrt(3)*I)*(290412 + + 3036*sqrt(9165))**(S(1)/3))*(1 - sqrt(3)*I)*(24201 + 253*sqrt(9165))** + (S(1)/3)/276)/((1 - sqrt(3)*I)*(24201 + 253*sqrt(9165))**(S(1)/3)) + )**n + C1*((86*18**(S(1)/3)/69 + (-12 + (-1 - sqrt(3)*I)*(290412 + 3036 + *sqrt(9165))**(S(1)/3))*(1 + sqrt(3)*I)*(24201 + 253*sqrt(9165))** + (S(1)/3)/276)/((1 + sqrt(3)*I)*(24201 + 253*sqrt(9165))**(S(1)/3)) + )**n + C2*(-43*18**(S(1)/3)/(69*(24201 + 253*sqrt(9165))**(S(1)/3)) - + S(1)/23 + (290412 + 3036*sqrt(9165))**(S(1)/3)/138)**n + assert sol == expected + e = sol.subs({C0: 1, C1: 1, C2: 1, n: 1}).evalf() + assert abs(e + 0.130434782608696) < 1e-13 + + +def test_issue_8697(): + a = Function('a') + eq = a(n + 3) - a(n + 2) - a(n + 1) + a(n) + assert rsolve(eq, a(n)) == (-1)**n*C1 + C0 + C2*n + eq2 = a(n + 3) + 3*a(n + 2) + 3*a(n + 1) + a(n) + assert (rsolve(eq2, a(n)) == + (-1)**n*C0 + (-1)**(n + 1)*C1*n + (-1)**(n + 1)*C2*n**2) + + assert rsolve(a(n) - 2*a(n - 3) + 5*a(n - 2) - 4*a(n - 1), + a(n), {a(0): 1, a(1): 3, a(2): 8}) == 3*2**n - n - 2 + + # From issue thread (but fixed by https://github.com/diofant/diofant/commit/da9789c6cd7d0c2ceeea19fbf59645987125b289): + assert rsolve(a(n) - 2*a(n - 1) - n, a(n), {a(0): 1}) == 3*2**n - n - 2 + + +def test_diofantissue_294(): + f = y(n) - y(n - 1) - 2*y(n - 2) - 2*n + assert rsolve(f, y(n)) == (-1)**n*C0 + 2**n*C1 - n - Rational(5, 2) + # issue sympy/sympy#11261 + assert rsolve(f, y(n), {y(0): -1, y(1): 1}) == (-(-1)**n/2 + 2*2**n - + n - Rational(5, 2)) + # issue sympy/sympy#7055 + assert rsolve(-2*y(n) + y(n + 1) + n - 1, y(n)) == 2**n*C0 + n + + +def test_issue_15553(): + f = Function("f") + assert rsolve(Eq(f(n), 2*f(n - 1) + n), f(n)) == 2**n*C0 - n - 2 + assert rsolve(Eq(f(n + 1), 2*f(n) + n**2 + 1), f(n)) == 2**n*C0 - n**2 - 2*n - 4 + assert rsolve(Eq(f(n + 1), 2*f(n) + n**2 + 1), f(n), {f(1): 0}) == 7*2**n/2 - n**2 - 2*n - 4 + assert rsolve(Eq(f(n), 2*f(n - 1) + 3*n**2), f(n)) == 2**n*C0 - 3*n**2 - 12*n - 18 + assert rsolve(Eq(f(n), 2*f(n - 1) + n**2), f(n)) == 2**n*C0 - n**2 - 4*n - 6 + assert rsolve(Eq(f(n), 2*f(n - 1) + n), f(n), {f(0): 1}) == 3*2**n - n - 2 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_simplex.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_simplex.py new file mode 100644 index 0000000000000000000000000000000000000000..611205f5df009a6d0de6e687501695b63bb932c9 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_simplex.py @@ -0,0 +1,254 @@ +from sympy.core.numbers import Rational +from sympy.core.relational import Eq, Ne +from sympy.core.symbol import symbols +from sympy.core.sympify import sympify +from sympy.core.singleton import S +from sympy.core.random import random, choice +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.ntheory.generate import randprime +from sympy.matrices.dense import Matrix +from sympy.solvers.solveset import linear_eq_to_matrix +from sympy.solvers.simplex import (_lp as lp, _primal_dual, + UnboundedLPError, InfeasibleLPError, lpmin, lpmax, + _m, _abcd, _simplex, linprog) + +from sympy.external.importtools import import_module + +from sympy.testing.pytest import raises + +from sympy.abc import x, y, z + + +np = import_module("numpy") +scipy = import_module("scipy") + + +def test_lp(): + r1 = y + 2*z <= 3 + r2 = -x - 3*z <= -2 + r3 = 2*x + y + 7*z <= 5 + constraints = [r1, r2, r3, x >= 0, y >= 0, z >= 0] + objective = -x - y - 5 * z + ans = optimum, argmax = lp(max, objective, constraints) + assert ans == lpmax(objective, constraints) + assert objective.subs(argmax) == optimum + for constr in constraints: + assert constr.subs(argmax) == True + + r1 = x - y + 2*z <= 3 + r2 = -x + 2*y - 3*z <= -2 + r3 = 2*x + y - 7*z <= -5 + constraints = [r1, r2, r3, x >= 0, y >= 0, z >= 0] + objective = -x - y - 5*z + ans = optimum, argmax = lp(max, objective, constraints) + assert ans == lpmax(objective, constraints) + assert objective.subs(argmax) == optimum + for constr in constraints: + assert constr.subs(argmax) == True + + r1 = x - y + 2*z <= -4 + r2 = -x + 2*y - 3*z <= 8 + r3 = 2*x + y - 7*z <= 10 + constraints = [r1, r2, r3, x >= 0, y >= 0, z >= 0] + const = 2 + objective = -x-y-5*z+const # has constant term + ans = optimum, argmax = lp(max, objective, constraints) + assert ans == lpmax(objective, constraints) + assert objective.subs(argmax) == optimum + for constr in constraints: + assert constr.subs(argmax) == True + + # Section 4 Problem 1 from + # http://web.tecnico.ulisboa.pt/mcasquilho/acad/or/ftp/FergusonUCLA_LP.pdf + # answer on page 55 + v = x1, x2, x3, x4 = symbols('x1 x2 x3 x4') + r1 = x1 - x2 - 2*x3 - x4 <= 4 + r2 = 2*x1 + x3 -4*x4 <= 2 + r3 = -2*x1 + x2 + x4 <= 1 + objective, constraints = x1 - 2*x2 - 3*x3 - x4, [r1, r2, r3] + [ + i >= 0 for i in v] + ans = optimum, argmax = lp(max, objective, constraints) + assert ans == lpmax(objective, constraints) + assert ans == (4, {x1: 7, x2: 0, x3: 0, x4: 3}) + + # input contains Floats + r1 = x - y + 2.0*z <= -4 + r2 = -x + 2*y - 3.0*z <= 8 + r3 = 2*x + y - 7*z <= 10 + constraints = [r1, r2, r3] + [i >= 0 for i in (x, y, z)] + objective = -x-y-5*z + optimum, argmax = lp(max, objective, constraints) + assert objective.subs(argmax) == optimum + for constr in constraints: + assert constr.subs(argmax) == True + + # input contains non-float or non-Rational + r1 = x - y + sqrt(2) * z <= -4 + r2 = -x + 2*y - 3*z <= 8 + r3 = 2*x + y - 7*z <= 10 + raises(TypeError, lambda: lp(max, -x-y-5*z, [r1, r2, r3])) + + r1 = x >= 0 + raises(UnboundedLPError, lambda: lp(max, x, [r1])) + r2 = x <= -1 + raises(InfeasibleLPError, lambda: lp(max, x, [r1, r2])) + + # strict inequalities are not allowed + r1 = x > 0 + raises(TypeError, lambda: lp(max, x, [r1])) + + # not equals not allowed + r1 = Ne(x, 0) + raises(TypeError, lambda: lp(max, x, [r1])) + + def make_random_problem(nvar=2, num_constraints=2, sparsity=.1): + def rand(): + if random() < sparsity: + return sympify(0) + int1, int2 = [randprime(0, 200) for _ in range(2)] + return Rational(int1, int2)*choice([-1, 1]) + variables = symbols('x1:%s' % (nvar + 1)) + constraints = [(sum(rand()*x for x in variables) <= rand()) + for _ in range(num_constraints)] + objective = sum(rand() * x for x in variables) + return objective, constraints, variables + + # equality + r1 = Eq(x, y) + r2 = Eq(y, z) + r3 = z <= 3 + constraints = [r1, r2, r3] + objective = x + ans = optimum, argmax = lp(max, objective, constraints) + assert ans == lpmax(objective, constraints) + assert objective.subs(argmax) == optimum + for constr in constraints: + assert constr.subs(argmax) == True + + +def test_simplex(): + L = [ + [[1, 1], [-1, 1], [0, 1], [-1, 0]], + [5, 1, 2, -1], + [[1, 1]], + [-1]] + A, B, C, D = _abcd(_m(*L), list=False) + assert _simplex(A, B, -C, -D) == (-6, [3, 2], [1, 0, 0, 0]) + assert _simplex(A, B, -C, -D, dual=True) == (-6, + [1, 0, 0, 0], [5, 0]) + + assert _simplex([[]],[],[[1]],[0]) == (0, [0], []) + + # handling of Eq (or Eq-like x<=y, x>=y conditions) + assert lpmax(x - y, [x <= y + 2, x >= y + 2, x >= 0, y >= 0] + ) == (2, {x: 2, y: 0}) + assert lpmax(x - y, [x <= y + 2, Eq(x, y + 2), x >= 0, y >= 0] + ) == (2, {x: 2, y: 0}) + assert lpmax(x - y, [x <= y + 2, Eq(x, 2)]) == (2, {x: 2, y: 0}) + assert lpmax(y, [Eq(y, 2)]) == (2, {y: 2}) + + # the conditions are equivalent to Eq(x, y + 2) + assert lpmin(y, [x <= y + 2, x >= y + 2, y >= 0] + ) == (0, {x: 2, y: 0}) + # equivalent to Eq(y, -2) + assert lpmax(y, [0 <= y + 2, 0 >= y + 2]) == (-2, {y: -2}) + assert lpmax(y, [0 <= y + 2, 0 >= y + 2, y <= 0] + ) == (-2, {y: -2}) + + # extra symbols symbols + assert lpmin(x, [y >= 1, x >= y]) == (1, {x: 1, y: 1}) + assert lpmin(x, [y >= 1, x >= y + z, x >= 0, z >= 0] + ) == (1, {x: 1, y: 1, z: 0}) + + # detect oscillation + # o1 + v = x1, x2, x3, x4 = symbols('x1 x2 x3 x4') + raises(InfeasibleLPError, lambda: lpmin( + 9*x2 - 8*x3 + 3*x4 + 6, + [5*x2 - 2*x3 <= 0, + -x1 - 8*x2 + 9*x3 <= -3, + 10*x1 - x2+ 9*x4 <= -4] + [i >= 0 for i in v])) + # o2 - equations fed to lpmin are changed into a matrix + # system that doesn't oscillate and has the same solution + # as below + M = linear_eq_to_matrix + f = 5*x2 + x3 + 4*x4 - x1 + L = 5*x2 + 2*x3 + 5*x4 - (x1 + 5) + cond = [L <= 0] + [Eq(3*x2 + x4, 2), Eq(-x1 + x3 + 2*x4, 1)] + c, d = M(f, v) + a, b = M(L, v) + aeq, beq = M(cond[1:], v) + ans = (S(9)/2, [0, S(1)/2, 0, S(1)/2]) + assert linprog(c, a, b, aeq, beq, bounds=(0, 1)) == ans + lpans = lpmin(f, cond + [x1 >= 0, x1 <= 1, + x2 >= 0, x2 <= 1, x3 >= 0, x3 <= 1, x4 >= 0, x4 <= 1]) + assert (lpans[0], list(lpans[1].values())) == ans + + +def test_lpmin_lpmax(): + v = x1, x2, y1, y2 = symbols('x1 x2 y1 y2') + L = [[1, -1]], [1], [[1, 1]], [2] + a, b, c, d = [Matrix(i) for i in L] + m = Matrix([[a, b], [c, d]]) + f, constr = _primal_dual(m)[0] + ans = lpmin(f, constr + [i >= 0 for i in v[:2]]) + assert ans == (-1, {x1: 1, x2: 0}),ans + + L = [[1, -1], [1, 1]], [1, 1], [[1, 1]], [2] + a, b, c, d = [Matrix(i) for i in L] + m = Matrix([[a, b], [c, d]]) + f, constr = _primal_dual(m)[1] + ans = lpmax(f, constr + [i >= 0 for i in v[-2:]]) + assert ans == (-1, {y1: 1, y2: 0}) + + +def test_linprog(): + for do in range(2): + if not do: + M = lambda a, b: linear_eq_to_matrix(a, b) + else: + # check matrices as list + M = lambda a, b: tuple([ + i.tolist() for i in linear_eq_to_matrix(a, b)]) + + v = x, y, z = symbols('x1:4') + f = x + y - 2*z + c = M(f, v)[0] + ineq = [7*x + 4*y - 7*z <= 3, + 3*x - y + 10*z <= 6, + x >= 0, y >= 0, z >= 0] + ab = M([i.lts - i.gts for i in ineq], v) + ans = (-S(6)/5, [0, 0, S(3)/5]) + assert lpmin(f, ineq) == (ans[0], dict(zip(v, ans[1]))) + assert linprog(c, *ab) == ans + + f += 1 + c = M(f, v)[0] + eq = [Eq(y - 9*x, 1)] + abeq = M([i.lhs - i.rhs for i in eq], v) + ans = (1 - S(2)/5, [0, 1, S(7)/10]) + assert lpmin(f, ineq + eq) == (ans[0], dict(zip(v, ans[1]))) + assert linprog(c, *ab, *abeq) == (ans[0] - 1, ans[1]) + + eq = [z - y <= S.Half] + abeq = M([i.lhs - i.rhs for i in eq], v) + ans = (1 - S(10)/9, [0, S(1)/9, S(11)/18]) + assert lpmin(f, ineq + eq) == (ans[0], dict(zip(v, ans[1]))) + assert linprog(c, *ab, *abeq) == (ans[0] - 1, ans[1]) + + bounds = [(0, None), (0, None), (None, S.Half)] + ans = (0, [0, 0, S.Half]) + assert lpmin(f, ineq + [z <= S.Half]) == ( + ans[0], dict(zip(v, ans[1]))) + assert linprog(c, *ab, bounds=bounds) == (ans[0] - 1, ans[1]) + assert linprog(c, *ab, bounds={v.index(z): bounds[-1]} + ) == (ans[0] - 1, ans[1]) + eq = [z - y <= S.Half] + + assert linprog([[1]], [], [], bounds=(2, 3)) == (2, [2]) + assert linprog([1], [], [], bounds=(2, 3)) == (2, [2]) + assert linprog([1], bounds=(2, 3)) == (2, [2]) + assert linprog([1, -1], [[1, 1]], [2], bounds={1:(None, None)} + ) == (-2, [0, 2]) + assert linprog([1, -1], [[1, 1]], [5], bounds={1:(3, None)} + ) == (-5, [0, 5]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_solvers.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_solvers.py new file mode 100644 index 0000000000000000000000000000000000000000..ac9550ad404c2ec7592caf6afd2910f425138987 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_solvers.py @@ -0,0 +1,2725 @@ +from sympy.assumptions.ask import (Q, ask) +from sympy.core.add import Add +from sympy.core.containers import Tuple +from sympy.core.function import (Derivative, Function, diff) +from sympy.core.mod import Mod +from sympy.core.mul import Mul +from sympy.core import (GoldenRatio, TribonacciConstant) +from sympy.core.numbers import (E, Float, I, Rational, oo, pi) +from sympy.core.relational import (Eq, Gt, Lt, Ne) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol, Wild, symbols) +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import binomial +from sympy.functions.elementary.complexes import (Abs, arg, conjugate, im, re) +from sympy.functions.elementary.exponential import (LambertW, exp, log) +from sympy.functions.elementary.hyperbolic import (atanh, cosh, sinh, tanh) +from sympy.functions.elementary.integers import floor +from sympy.functions.elementary.miscellaneous import (cbrt, root, sqrt) +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (acos, asin, atan, atan2, cos, sec, sin, tan) +from sympy.functions.special.error_functions import (erf, erfc, erfcinv, erfinv) +from sympy.integrals.integrals import Integral +from sympy.logic.boolalg import (And, Or) +from sympy.matrices.dense import Matrix +from sympy.matrices import MatrixSymbol, SparseMatrix +from sympy.polys.polytools import Poly, groebner +from sympy.printing.str import sstr +from sympy.simplify.radsimp import denom +from sympy.solvers.solvers import (nsolve, solve, solve_linear) + +from sympy.core.function import nfloat +from sympy.solvers import solve_linear_system, solve_linear_system_LU, \ + solve_undetermined_coeffs +from sympy.solvers.bivariate import _filtered_gens, _solve_lambert, _lambert +from sympy.solvers.solvers import _invert, unrad, checksol, posify, _ispow, \ + det_quick, det_perm, det_minor, _simple_dens, denoms + +from sympy.physics.units import cm +from sympy.polys.rootoftools import CRootOf + +from sympy.testing.pytest import slow, XFAIL, SKIP, raises +from sympy.core.random import verify_numerically as tn + +from sympy.abc import a, b, c, d, e, k, h, p, x, y, z, t, q, m, R + + +def NS(e, n=15, **options): + return sstr(sympify(e).evalf(n, **options), full_prec=True) + + +def test_swap_back(): + f, g = map(Function, 'fg') + fx, gx = f(x), g(x) + assert solve([fx + y - 2, fx - gx - 5], fx, y, gx) == \ + {fx: gx + 5, y: -gx - 3} + assert solve(fx + gx*x - 2, [fx, gx], dict=True) == [{fx: 2, gx: 0}] + assert solve(fx + gx**2*x - y, [fx, gx], dict=True) == [{fx: y, gx: 0}] + assert solve([f(1) - 2, x + 2], dict=True) == [{x: -2, f(1): 2}] + + +def guess_solve_strategy(eq, symbol): + try: + solve(eq, symbol) + return True + except (TypeError, NotImplementedError): + return False + + +def test_guess_poly(): + # polynomial equations + assert guess_solve_strategy( S(4), x ) # == GS_POLY + assert guess_solve_strategy( x, x ) # == GS_POLY + assert guess_solve_strategy( x + a, x ) # == GS_POLY + assert guess_solve_strategy( 2*x, x ) # == GS_POLY + assert guess_solve_strategy( x + sqrt(2), x) # == GS_POLY + assert guess_solve_strategy( x + 2**Rational(1, 4), x) # == GS_POLY + assert guess_solve_strategy( x**2 + 1, x ) # == GS_POLY + assert guess_solve_strategy( x**2 - 1, x ) # == GS_POLY + assert guess_solve_strategy( x*y + y, x ) # == GS_POLY + assert guess_solve_strategy( x*exp(y) + y, x) # == GS_POLY + assert guess_solve_strategy( + (x - y**3)/(y**2*sqrt(1 - y**2)), x) # == GS_POLY + + +def test_guess_poly_cv(): + # polynomial equations via a change of variable + assert guess_solve_strategy( sqrt(x) + 1, x ) # == GS_POLY_CV_1 + assert guess_solve_strategy( + x**Rational(1, 3) + sqrt(x) + 1, x ) # == GS_POLY_CV_1 + assert guess_solve_strategy( 4*x*(1 - sqrt(x)), x ) # == GS_POLY_CV_1 + + # polynomial equation multiplying both sides by x**n + assert guess_solve_strategy( x + 1/x + y, x ) # == GS_POLY_CV_2 + + +def test_guess_rational_cv(): + # rational functions + assert guess_solve_strategy( (x + 1)/(x**2 + 2), x) # == GS_RATIONAL + assert guess_solve_strategy( + (x - y**3)/(y**2*sqrt(1 - y**2)), y) # == GS_RATIONAL_CV_1 + + # rational functions via the change of variable y -> x**n + assert guess_solve_strategy( (sqrt(x) + 1)/(x**Rational(1, 3) + sqrt(x) + 1), x ) \ + #== GS_RATIONAL_CV_1 + + +def test_guess_transcendental(): + #transcendental functions + assert guess_solve_strategy( exp(x) + 1, x ) # == GS_TRANSCENDENTAL + assert guess_solve_strategy( 2*cos(x) - y, x ) # == GS_TRANSCENDENTAL + assert guess_solve_strategy( + exp(x) + exp(-x) - y, x ) # == GS_TRANSCENDENTAL + assert guess_solve_strategy(3**x - 10, x) # == GS_TRANSCENDENTAL + assert guess_solve_strategy(-3**x + 10, x) # == GS_TRANSCENDENTAL + + assert guess_solve_strategy(a*x**b - y, x) # == GS_TRANSCENDENTAL + + +def test_solve_args(): + # equation container, issue 5113 + ans = {x: -3, y: 1} + eqs = (x + 5*y - 2, -3*x + 6*y - 15) + assert all(solve(container(eqs), x, y) == ans for container in + (tuple, list, set, frozenset)) + assert solve(Tuple(*eqs), x, y) == ans + # implicit symbol to solve for + assert set(solve(x**2 - 4)) == {S(2), -S(2)} + assert solve([x + y - 3, x - y - 5]) == {x: 4, y: -1} + assert solve(x - exp(x), x, implicit=True) == [exp(x)] + # no symbol to solve for + assert solve(42) == solve(42, x) == [] + assert solve([1, 2]) == [] + assert solve([sqrt(2)],[x]) == [] + # duplicate symbols raises + raises(ValueError, lambda: solve((x - 3, y + 2), x, y, x)) + raises(ValueError, lambda: solve(x, x, x)) + # no error in exclude + assert solve(x, x, exclude=[y, y]) == [0] + # duplicate symbols raises + raises(ValueError, lambda: solve((x - 3, y + 2), x, y, x)) + raises(ValueError, lambda: solve(x, x, x)) + # no error in exclude + assert solve(x, x, exclude=[y, y]) == [0] + # unordered symbols + # only 1 + assert solve(y - 3, {y}) == [3] + # more than 1 + assert solve(y - 3, {x, y}) == [{y: 3}] + # multiple symbols: take the first linear solution+ + # - return as tuple with values for all requested symbols + assert solve(x + y - 3, [x, y]) == [(3 - y, y)] + # - unless dict is True + assert solve(x + y - 3, [x, y], dict=True) == [{x: 3 - y}] + # - or no symbols are given + assert solve(x + y - 3) == [{x: 3 - y}] + # multiple symbols might represent an undetermined coefficients system + assert solve(a + b*x - 2, [a, b]) == {a: 2, b: 0} + assert solve((a + b)*x + b - c, [a, b]) == {a: -c, b: c} + eq = a*x**2 + b*x + c - ((x - h)**2 + 4*p*k)/4/p + # - check that flags are obeyed + sol = solve(eq, [h, p, k], exclude=[a, b, c]) + assert sol == {h: -b/(2*a), k: (4*a*c - b**2)/(4*a), p: 1/(4*a)} + assert solve(eq, [h, p, k], dict=True) == [sol] + assert solve(eq, [h, p, k], set=True) == \ + ([h, p, k], {(-b/(2*a), 1/(4*a), (4*a*c - b**2)/(4*a))}) + # issue 23889 - polysys not simplified + assert solve(eq, [h, p, k], exclude=[a, b, c], simplify=False) == \ + {h: -b/(2*a), k: (4*a*c - b**2)/(4*a), p: 1/(4*a)} + # but this only happens when system has a single solution + args = (a + b)*x - b**2 + 2, a, b + assert solve(*args) == [((b**2 - b*x - 2)/x, b)] + # and if the system has a solution; the following doesn't so + # an algebraic solution is returned + assert solve(a*x + b**2/(x + 4) - 3*x - 4/x, a, b, dict=True) == \ + [{a: (-b**2*x + 3*x**3 + 12*x**2 + 4*x + 16)/(x**2*(x + 4))}] + # failed single equation + assert solve(1/(1/x - y + exp(y))) == [] + raises( + NotImplementedError, lambda: solve(exp(x) + sin(x) + exp(y) + sin(y))) + # failed system + # -- when no symbols given, 1 fails + assert solve([y, exp(x) + x]) == [{x: -LambertW(1), y: 0}] + # both fail + assert solve( + (exp(x) - x, exp(y) - y)) == [{x: -LambertW(-1), y: -LambertW(-1)}] + # -- when symbols given + assert solve([y, exp(x) + x], x, y) == [(-LambertW(1), 0)] + # symbol is a number + assert solve(x**2 - pi, pi) == [x**2] + # no equations + assert solve([], [x]) == [] + # nonlinear system + assert solve((x**2 - 4, y - 2), x, y) == [(-2, 2), (2, 2)] + assert solve((x**2 - 4, y - 2), y, x) == [(2, -2), (2, 2)] + assert solve((x**2 - 4 + z, y - 2 - z), a, z, y, x, set=True + ) == ([a, z, y, x], { + (a, z, z + 2, -sqrt(4 - z)), + (a, z, z + 2, sqrt(4 - z))}) + # overdetermined system + # - nonlinear + assert solve([(x + y)**2 - 4, x + y - 2]) == [{x: -y + 2}] + # - linear + assert solve((x + y - 2, 2*x + 2*y - 4)) == {x: -y + 2} + # When one or more args are Boolean + assert solve(Eq(x**2, 0.0)) == [0.0] # issue 19048 + assert solve([True, Eq(x, 0)], [x], dict=True) == [{x: 0}] + assert solve([Eq(x, x), Eq(x, 0), Eq(x, x+1)], [x], dict=True) == [] + assert not solve([Eq(x, x+1), x < 2], x) + assert solve([Eq(x, 0), x+1<2]) == Eq(x, 0) + assert solve([Eq(x, x), Eq(x, x+1)], x) == [] + assert solve(True, x) == [] + assert solve([x - 1, False], [x], set=True) == ([], set()) + assert solve([-y*(x + y - 1)/2, (y - 1)/x/y + 1/y], + set=True, check=False) == ([x, y], {(1 - y, y), (x, 0)}) + # ordering should be canonical, fastest to order by keys instead + # of by size + assert list(solve((y - 1, x - sqrt(3)*z)).keys()) == [x, y] + # as set always returns as symbols, set even if no solution + assert solve([x - 1, x], (y, x), set=True) == ([y, x], set()) + assert solve([x - 1, x], {y, x}, set=True) == ([x, y], set()) + + +def test_solve_polynomial1(): + assert solve(3*x - 2, x) == [Rational(2, 3)] + assert solve(Eq(3*x, 2), x) == [Rational(2, 3)] + + assert set(solve(x**2 - 1, x)) == {-S.One, S.One} + assert set(solve(Eq(x**2, 1), x)) == {-S.One, S.One} + + assert solve(x - y**3, x) == [y**3] + rx = root(x, 3) + assert solve(x - y**3, y) == [ + rx, -rx/2 - sqrt(3)*I*rx/2, -rx/2 + sqrt(3)*I*rx/2] + a11, a12, a21, a22, b1, b2 = symbols('a11,a12,a21,a22,b1,b2') + + assert solve([a11*x + a12*y - b1, a21*x + a22*y - b2], x, y) == \ + { + x: (a22*b1 - a12*b2)/(a11*a22 - a12*a21), + y: (a11*b2 - a21*b1)/(a11*a22 - a12*a21), + } + + solution = {x: S.Zero, y: S.Zero} + + assert solve((x - y, x + y), x, y ) == solution + assert solve((x - y, x + y), (x, y)) == solution + assert solve((x - y, x + y), [x, y]) == solution + + assert set(solve(x**3 - 15*x - 4, x)) == { + -2 + 3**S.Half, + S(4), + -2 - 3**S.Half + } + + assert set(solve((x**2 - 1)**2 - a, x)) == \ + {sqrt(1 + sqrt(a)), -sqrt(1 + sqrt(a)), + sqrt(1 - sqrt(a)), -sqrt(1 - sqrt(a))} + + +def test_solve_polynomial2(): + assert solve(4, x) == [] + + +def test_solve_polynomial_cv_1a(): + """ + Test for solving on equations that can be converted to a polynomial equation + using the change of variable y -> x**Rational(p, q) + """ + assert solve( sqrt(x) - 1, x) == [1] + assert solve( sqrt(x) - 2, x) == [4] + assert solve( x**Rational(1, 4) - 2, x) == [16] + assert solve( x**Rational(1, 3) - 3, x) == [27] + assert solve(sqrt(x) + x**Rational(1, 3) + x**Rational(1, 4), x) == [0] + + +def test_solve_polynomial_cv_1b(): + assert set(solve(4*x*(1 - a*sqrt(x)), x)) == {S.Zero, 1/a**2} + assert set(solve(x*(root(x, 3) - 3), x)) == {S.Zero, S(27)} + + +def test_solve_polynomial_cv_2(): + """ + Test for solving on equations that can be converted to a polynomial equation + multiplying both sides of the equation by x**m + """ + assert solve(x + 1/x - 1, x) in \ + [[ S.Half + I*sqrt(3)/2, S.Half - I*sqrt(3)/2], + [ S.Half - I*sqrt(3)/2, S.Half + I*sqrt(3)/2]] + + +def test_quintics_1(): + f = x**5 - 110*x**3 - 55*x**2 + 2310*x + 979 + s = solve(f, check=False) + for r in s: + res = f.subs(x, r.n()).n() + assert tn(res, 0) + + f = x**5 - 15*x**3 - 5*x**2 + 10*x + 20 + s = solve(f) + for r in s: + assert r.func == CRootOf + + # if one uses solve to get the roots of a polynomial that has a CRootOf + # solution, make sure that the use of nfloat during the solve process + # doesn't fail. Note: if you want numerical solutions to a polynomial + # it is *much* faster to use nroots to get them than to solve the + # equation only to get RootOf solutions which are then numerically + # evaluated. So for eq = x**5 + 3*x + 7 do Poly(eq).nroots() rather + # than [i.n() for i in solve(eq)] to get the numerical roots of eq. + assert nfloat(solve(x**5 + 3*x**3 + 7)[0], exponent=False) == \ + CRootOf(x**5 + 3*x**3 + 7, 0).n() + + +def test_quintics_2(): + f = x**5 + 15*x + 12 + s = solve(f, check=False) + for r in s: + res = f.subs(x, r.n()).n() + assert tn(res, 0) + + f = x**5 - 15*x**3 - 5*x**2 + 10*x + 20 + s = solve(f) + for r in s: + assert r.func == CRootOf + + assert solve(x**5 - 6*x**3 - 6*x**2 + x - 6) == [ + CRootOf(x**5 - 6*x**3 - 6*x**2 + x - 6, 0), + CRootOf(x**5 - 6*x**3 - 6*x**2 + x - 6, 1), + CRootOf(x**5 - 6*x**3 - 6*x**2 + x - 6, 2), + CRootOf(x**5 - 6*x**3 - 6*x**2 + x - 6, 3), + CRootOf(x**5 - 6*x**3 - 6*x**2 + x - 6, 4)] + +def test_quintics_3(): + y = x**5 + x**3 - 2**Rational(1, 3) + assert solve(y) == solve(-y) == [] + + +def test_highorder_poly(): + # just testing that the uniq generator is unpacked + sol = solve(x**6 - 2*x + 2) + assert all(isinstance(i, CRootOf) for i in sol) and len(sol) == 6 + + +def test_solve_rational(): + """Test solve for rational functions""" + assert solve( ( x - y**3 )/( (y**2)*sqrt(1 - y**2) ), x) == [y**3] + + +def test_solve_conjugate(): + """Test solve for simple conjugate functions""" + assert solve(conjugate(x) -3 + I) == [3 + I] + + +def test_solve_nonlinear(): + assert solve(x**2 - y**2, x, y, dict=True) == [{x: -y}, {x: y}] + assert solve(x**2 - y**2/exp(x), y, x, dict=True) == [{y: -x*sqrt(exp(x))}, + {y: x*sqrt(exp(x))}] + + +def test_issue_8666(): + x = symbols('x') + assert solve(Eq(x**2 - 1/(x**2 - 4), 4 - 1/(x**2 - 4)), x) == [] + assert solve(Eq(x + 1/x, 1/x), x) == [] + + +def test_issue_7228(): + assert solve(4**(2*(x**2) + 2*x) - 8, x) == [Rational(-3, 2), S.Half] + + +def test_issue_7190(): + assert solve(log(x-3) + log(x+3), x) == [sqrt(10)] + + +def test_issue_21004(): + x = symbols('x') + f = x/sqrt(x**2+1) + f_diff = f.diff(x) + assert solve(f_diff, x) == [] + + +def test_issue_24650(): + x = symbols('x') + r = solve(Eq(Piecewise((x, Eq(x, 0) | (x > 1))), 0)) + assert r == [0] + r = checksol(Eq(Piecewise((x, Eq(x, 0) | (x > 1))), 0), x, sol=0) + assert r is True + + +def test_linear_system(): + x, y, z, t, n = symbols('x, y, z, t, n') + + assert solve([x - 1, x - y, x - 2*y, y - 1], [x, y]) == [] + + assert solve([x - 1, x - y, x - 2*y, x - 1], [x, y]) == [] + assert solve([x - 1, x - 1, x - y, x - 2*y], [x, y]) == [] + + assert solve([x + 5*y - 2, -3*x + 6*y - 15], x, y) == {x: -3, y: 1} + + M = Matrix([[0, 0, n*(n + 1), (n + 1)**2, 0], + [n + 1, n + 1, -2*n - 1, -(n + 1), 0], + [-1, 0, 1, 0, 0]]) + + assert solve_linear_system(M, x, y, z, t) == \ + {x: t*(-n-1)/n, y: 0, z: t*(-n-1)/n} + + assert solve([x + y + z + t, -z - t], x, y, z, t) == {x: -y, z: -t} + + +@XFAIL +def test_linear_system_xfail(): + # https://github.com/sympy/sympy/issues/6420 + M = Matrix([[0, 15.0, 10.0, 700.0], + [1, 1, 1, 100.0], + [0, 10.0, 5.0, 200.0], + [-5.0, 0, 0, 0 ]]) + + assert solve_linear_system(M, x, y, z) == {x: 0, y: -60.0, z: 160.0} + + +def test_linear_system_function(): + a = Function('a') + assert solve([a(0, 0) + a(0, 1) + a(1, 0) + a(1, 1), -a(1, 0) - a(1, 1)], + a(0, 0), a(0, 1), a(1, 0), a(1, 1)) == {a(1, 0): -a(1, 1), a(0, 0): -a(0, 1)} + + +def test_linear_system_symbols_doesnt_hang_1(): + + def _mk_eqs(wy): + # Equations for fitting a wy*2 - 1 degree polynomial between two points, + # at end points derivatives are known up to order: wy - 1 + order = 2*wy - 1 + x, x0, x1 = symbols('x, x0, x1', real=True) + y0s = symbols('y0_:{}'.format(wy), real=True) + y1s = symbols('y1_:{}'.format(wy), real=True) + c = symbols('c_:{}'.format(order+1), real=True) + + expr = sum(coeff*x**o for o, coeff in enumerate(c)) + eqs = [] + for i in range(wy): + eqs.append(expr.diff(x, i).subs({x: x0}) - y0s[i]) + eqs.append(expr.diff(x, i).subs({x: x1}) - y1s[i]) + return eqs, c + + # + # The purpose of this test is just to see that these calls don't hang. The + # expressions returned are complicated so are not included here. Testing + # their correctness takes longer than solving the system. + # + + for n in range(1, 7+1): + eqs, c = _mk_eqs(n) + solve(eqs, c) + + +def test_linear_system_symbols_doesnt_hang_2(): + + M = Matrix([ + [66, 24, 39, 50, 88, 40, 37, 96, 16, 65, 31, 11, 37, 72, 16, 19, 55, 37, 28, 76], + [10, 93, 34, 98, 59, 44, 67, 74, 74, 94, 71, 61, 60, 23, 6, 2, 57, 8, 29, 78], + [19, 91, 57, 13, 64, 65, 24, 53, 77, 34, 85, 58, 87, 39, 39, 7, 36, 67, 91, 3], + [74, 70, 15, 53, 68, 43, 86, 83, 81, 72, 25, 46, 67, 17, 59, 25, 78, 39, 63, 6], + [69, 40, 67, 21, 67, 40, 17, 13, 93, 44, 46, 89, 62, 31, 30, 38, 18, 20, 12, 81], + [50, 22, 74, 76, 34, 45, 19, 76, 28, 28, 11, 99, 97, 82, 8, 46, 99, 57, 68, 35], + [58, 18, 45, 88, 10, 64, 9, 34, 90, 82, 17, 41, 43, 81, 45, 83, 22, 88, 24, 39], + [42, 21, 70, 68, 6, 33, 64, 81, 83, 15, 86, 75, 86, 17, 77, 34, 62, 72, 20, 24], + [ 7, 8, 2, 72, 71, 52, 96, 5, 32, 51, 31, 36, 79, 88, 25, 77, 29, 26, 33, 13], + [19, 31, 30, 85, 81, 39, 63, 28, 19, 12, 16, 49, 37, 66, 38, 13, 3, 71, 61, 51], + [29, 82, 80, 49, 26, 85, 1, 37, 2, 74, 54, 82, 26, 47, 54, 9, 35, 0, 99, 40], + [15, 49, 82, 91, 93, 57, 45, 25, 45, 97, 15, 98, 48, 52, 66, 24, 62, 54, 97, 37], + [62, 23, 73, 53, 52, 86, 28, 38, 0, 74, 92, 38, 97, 70, 71, 29, 26, 90, 67, 45], + [ 2, 32, 23, 24, 71, 37, 25, 71, 5, 41, 97, 65, 93, 13, 65, 45, 25, 88, 69, 50], + [40, 56, 1, 29, 79, 98, 79, 62, 37, 28, 45, 47, 3, 1, 32, 74, 98, 35, 84, 32], + [33, 15, 87, 79, 65, 9, 14, 63, 24, 19, 46, 28, 74, 20, 29, 96, 84, 91, 93, 1], + [97, 18, 12, 52, 1, 2, 50, 14, 52, 76, 19, 82, 41, 73, 51, 79, 13, 3, 82, 96], + [40, 28, 52, 10, 10, 71, 56, 78, 82, 5, 29, 48, 1, 26, 16, 18, 50, 76, 86, 52], + [38, 89, 83, 43, 29, 52, 90, 77, 57, 0, 67, 20, 81, 88, 48, 96, 88, 58, 14, 3]]) + + syms = x0,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18 = symbols('x:19') + + sol = { + x0: -S(1967374186044955317099186851240896179)/3166636564687820453598895768302256588, + x1: -S(84268280268757263347292368432053826)/791659141171955113399723942075564147, + x2: -S(229962957341664730974463872411844965)/1583318282343910226799447884151128294, + x3: S(990156781744251750886760432229180537)/6333273129375640907197791536604513176, + x4: -S(2169830351210066092046760299593096265)/18999819388126922721593374609813539528, + x5: S(4680868883477577389628494526618745355)/9499909694063461360796687304906769764, + x6: -S(1590820774344371990683178396480879213)/3166636564687820453598895768302256588, + x7: -S(54104723404825537735226491634383072)/339282489073695048599881689460956063, + x8: S(3182076494196560075964847771774733847)/6333273129375640907197791536604513176, + x9: -S(10870817431029210431989147852497539675)/18999819388126922721593374609813539528, + x10: -S(13118019242576506476316318268573312603)/18999819388126922721593374609813539528, + x11: -S(5173852969886775824855781403820641259)/4749954847031730680398343652453384882, + x12: S(4261112042731942783763341580651820563)/4749954847031730680398343652453384882, + x13: -S(821833082694661608993818117038209051)/6333273129375640907197791536604513176, + x14: S(906881575107250690508618713632090559)/904753304196520129599684505229216168, + x15: -S(732162528717458388995329317371283987)/6333273129375640907197791536604513176, + x16: S(4524215476705983545537087360959896817)/9499909694063461360796687304906769764, + x17: -S(3898571347562055611881270844646055217)/6333273129375640907197791536604513176, + x18: S(7513502486176995632751685137907442269)/18999819388126922721593374609813539528 + } + + eqs = list(M * Matrix(syms + (1,))) + assert solve(eqs, syms) == sol + + y = Symbol('y') + eqs = list(y * M * Matrix(syms + (1,))) + assert solve(eqs, syms) == sol + + +def test_linear_systemLU(): + n = Symbol('n') + + M = Matrix([[1, 2, 0, 1], [1, 3, 2*n, 1], [4, -1, n**2, 1]]) + + assert solve_linear_system_LU(M, [x, y, z]) == {z: -3/(n**2 + 18*n), + x: 1 - 12*n/(n**2 + 18*n), + y: 6*n/(n**2 + 18*n)} + +# Note: multiple solutions exist for some of these equations, so the tests +# should be expected to break if the implementation of the solver changes +# in such a way that a different branch is chosen + +@slow +def test_solve_transcendental(): + from sympy.abc import a, b + + assert solve(exp(x) - 3, x) == [log(3)] + assert set(solve((a*x + b)*(exp(x) - 3), x)) == {-b/a, log(3)} + assert solve(cos(x) - y, x) == [-acos(y) + 2*pi, acos(y)] + assert solve(2*cos(x) - y, x) == [-acos(y/2) + 2*pi, acos(y/2)] + assert solve(Eq(cos(x), sin(x)), x) == [pi/4] + + assert set(solve(exp(x) + exp(-x) - y, x)) in [{ + log(y/2 - sqrt(y**2 - 4)/2), + log(y/2 + sqrt(y**2 - 4)/2), + }, { + log(y - sqrt(y**2 - 4)) - log(2), + log(y + sqrt(y**2 - 4)) - log(2)}, + { + log(y/2 - sqrt((y - 2)*(y + 2))/2), + log(y/2 + sqrt((y - 2)*(y + 2))/2)}] + assert solve(exp(x) - 3, x) == [log(3)] + assert solve(Eq(exp(x), 3), x) == [log(3)] + assert solve(log(x) - 3, x) == [exp(3)] + assert solve(sqrt(3*x) - 4, x) == [Rational(16, 3)] + assert solve(3**(x + 2), x) == [] + assert solve(3**(2 - x), x) == [] + assert solve(x + 2**x, x) == [-LambertW(log(2))/log(2)] + assert solve(2*x + 5 + log(3*x - 2), x) == \ + [Rational(2, 3) + LambertW(2*exp(Rational(-19, 3))/3)/2] + assert solve(3*x + log(4*x), x) == [LambertW(Rational(3, 4))/3] + assert set(solve((2*x + 8)*(8 + exp(x)), x)) == {S(-4), log(8) + pi*I} + eq = 2*exp(3*x + 4) - 3 + ans = solve(eq, x) # this generated a failure in flatten + assert len(ans) == 3 and all(eq.subs(x, a).n(chop=True) == 0 for a in ans) + assert solve(2*log(3*x + 4) - 3, x) == [(exp(Rational(3, 2)) - 4)/3] + assert solve(exp(x) + 1, x) == [pi*I] + + eq = 2*(3*x + 4)**5 - 6*7**(3*x + 9) + result = solve(eq, x) + x0 = -log(2401) + x1 = 3**Rational(1, 5) + x2 = log(7**(7*x1/20)) + x3 = sqrt(2) + x4 = sqrt(5) + x5 = x3*sqrt(x4 - 5) + x6 = x4 + 1 + x7 = 1/(3*log(7)) + x8 = -x4 + x9 = x3*sqrt(x8 - 5) + x10 = x8 + 1 + ans = [x7*(x0 - 5*LambertW(x2*(-x5 + x6))), + x7*(x0 - 5*LambertW(x2*(x5 + x6))), + x7*(x0 - 5*LambertW(x2*(x10 - x9))), + x7*(x0 - 5*LambertW(x2*(x10 + x9))), + x7*(x0 - 5*LambertW(-log(7**(7*x1/5))))] + assert result == ans, result + # it works if expanded, too + assert solve(eq.expand(), x) == result + + assert solve(z*cos(x) - y, x) == [-acos(y/z) + 2*pi, acos(y/z)] + assert solve(z*cos(2*x) - y, x) == [-acos(y/z)/2 + pi, acos(y/z)/2] + assert solve(z*cos(sin(x)) - y, x) == [ + pi - asin(acos(y/z)), asin(acos(y/z) - 2*pi) + pi, + -asin(acos(y/z) - 2*pi), asin(acos(y/z))] + + assert solve(z*cos(x), x) == [pi/2, pi*Rational(3, 2)] + + # issue 4508 + assert solve(y - b*x/(a + x), x) in [[-a*y/(y - b)], [a*y/(b - y)]] + assert solve(y - b*exp(a/x), x) == [a/log(y/b)] + # issue 4507 + assert solve(y - b/(1 + a*x), x) in [[(b - y)/(a*y)], [-((y - b)/(a*y))]] + # issue 4506 + assert solve(y - a*x**b, x) == [(y/a)**(1/b)] + # issue 4505 + assert solve(z**x - y, x) == [log(y)/log(z)] + # issue 4504 + assert solve(2**x - 10, x) == [1 + log(5)/log(2)] + # issue 6744 + assert solve(x*y) == [{x: 0}, {y: 0}] + assert solve([x*y]) == [{x: 0}, {y: 0}] + assert solve(x**y - 1) == [{x: 1}, {y: 0}] + assert solve([x**y - 1]) == [{x: 1}, {y: 0}] + assert solve(x*y*(x**2 - y**2)) == [{x: 0}, {x: -y}, {x: y}, {y: 0}] + assert solve([x*y*(x**2 - y**2)]) == [{x: 0}, {x: -y}, {x: y}, {y: 0}] + # issue 4739 + assert solve(exp(log(5)*x) - 2**x, x) == [0] + # issue 14791 + assert solve(exp(log(5)*x) - exp(log(2)*x), x) == [0] + f = Function('f') + assert solve(y*f(log(5)*x) - y*f(log(2)*x), x) == [0] + assert solve(f(x) - f(0), x) == [0] + assert solve(f(x) - f(2 - x), x) == [1] + raises(NotImplementedError, lambda: solve(f(x, y) - f(1, 2), x)) + raises(NotImplementedError, lambda: solve(f(x, y) - f(2 - x, 2), x)) + raises(ValueError, lambda: solve(f(x, y) - f(1 - x), x)) + raises(ValueError, lambda: solve(f(x, y) - f(1), x)) + + # misc + # make sure that the right variables is picked up in tsolve + # shouldn't generate a GeneratorsNeeded error in _tsolve when the NaN is generated + # for eq_down. Actual answers, as determined numerically are approx. +/- 0.83 + raises(NotImplementedError, lambda: + solve(sinh(x)*sinh(sinh(x)) + cosh(x)*cosh(sinh(x)) - 3)) + + # watch out for recursive loop in tsolve + raises(NotImplementedError, lambda: solve((x + 2)**y*x - 3, x)) + + # issue 7245 + assert solve(sin(sqrt(x))) == [0, pi**2] + + # issue 7602 + a, b = symbols('a, b', real=True, negative=False) + assert str(solve(Eq(a, 0.5 - cos(pi*b)/2), b)) == \ + '[2.0 - 0.318309886183791*acos(1.0 - 2.0*a), 0.318309886183791*acos(1.0 - 2.0*a)]' + + # issue 15325 + assert solve(y**(1/x) - z, x) == [log(y)/log(z)] + + # issue 25685 (basic trig identities should give simple solutions) + for yi in [cos(2*x),sin(2*x),cos(x - pi/3)]: + sol = solve([cos(x) - S(3)/5, yi - y]) + assert (sol[0][y] + sol[1][y]).is_Rational, (yi,sol) + # don't allow massive expansion + assert solve(cos(1000*x) - S.Half) == [pi/3000, pi/600] + assert solve(cos(x - 1000*y) - 1, x) == [1000*y, 1000*y + 2*pi] + assert solve(cos(x + y + z) - 1, x) == [-y - z, -y - z + 2*pi] + + # issue 26008 + assert solve(sin(x + pi/6)) == [-pi/6, 5*pi/6] + + +def test_solve_for_functions_derivatives(): + t = Symbol('t') + x = Function('x')(t) + y = Function('y')(t) + a11, a12, a21, a22, b1, b2 = symbols('a11,a12,a21,a22,b1,b2') + + soln = solve([a11*x + a12*y - b1, a21*x + a22*y - b2], x, y) + assert soln == { + x: (a22*b1 - a12*b2)/(a11*a22 - a12*a21), + y: (a11*b2 - a21*b1)/(a11*a22 - a12*a21), + } + + assert solve(x - 1, x) == [1] + assert solve(3*x - 2, x) == [Rational(2, 3)] + + soln = solve([a11*x.diff(t) + a12*y.diff(t) - b1, a21*x.diff(t) + + a22*y.diff(t) - b2], x.diff(t), y.diff(t)) + assert soln == { y.diff(t): (a11*b2 - a21*b1)/(a11*a22 - a12*a21), + x.diff(t): (a22*b1 - a12*b2)/(a11*a22 - a12*a21) } + + assert solve(x.diff(t) - 1, x.diff(t)) == [1] + assert solve(3*x.diff(t) - 2, x.diff(t)) == [Rational(2, 3)] + + eqns = {3*x - 1, 2*y - 4} + assert solve(eqns, {x, y}) == { x: Rational(1, 3), y: 2 } + x = Symbol('x') + f = Function('f') + F = x**2 + f(x)**2 - 4*x - 1 + assert solve(F.diff(x), diff(f(x), x)) == [(-x + 2)/f(x)] + + # Mixed cased with a Symbol and a Function + x = Symbol('x') + y = Function('y')(t) + + soln = solve([a11*x + a12*y.diff(t) - b1, a21*x + + a22*y.diff(t) - b2], x, y.diff(t)) + assert soln == { y.diff(t): (a11*b2 - a21*b1)/(a11*a22 - a12*a21), + x: (a22*b1 - a12*b2)/(a11*a22 - a12*a21) } + + # issue 13263 + x = Symbol('x') + f = Function('f') + soln = solve([f(x).diff(x) + f(x).diff(x, 2) - 1, f(x).diff(x) - f(x).diff(x, 2)], + f(x).diff(x), f(x).diff(x, 2)) + assert soln == { f(x).diff(x, 2): S(1)/2, f(x).diff(x): S(1)/2 } + + soln = solve([f(x).diff(x, 2) + f(x).diff(x, 3) - 1, 1 - f(x).diff(x, 2) - + f(x).diff(x, 3), 1 - f(x).diff(x,3)], f(x).diff(x, 2), f(x).diff(x, 3)) + assert soln == { f(x).diff(x, 2): 0, f(x).diff(x, 3): 1 } + + +def test_issue_3725(): + f = Function('f') + F = x**2 + f(x)**2 - 4*x - 1 + e = F.diff(x) + assert solve(e, f(x).diff(x)) in [[(2 - x)/f(x)], [-((x - 2)/f(x))]] + + +def test_solve_Matrix(): + # https://github.com/sympy/sympy/issues/3870 + a, b, c, d = symbols('a b c d') + A = Matrix(2, 2, [a, b, c, d]) + B = Matrix(2, 2, [0, 2, -3, 0]) + C = Matrix(2, 2, [1, 2, 3, 4]) + + assert solve(A*B - C, [a, b, c, d]) == {a: 1, b: Rational(-1, 3), c: 2, d: -1} + assert solve([A*B - C], [a, b, c, d]) == {a: 1, b: Rational(-1, 3), c: 2, d: -1} + assert solve(Eq(A*B, C), [a, b, c, d]) == {a: 1, b: Rational(-1, 3), c: 2, d: -1} + + assert solve([A*B - B*A], [a, b, c, d]) == {a: d, b: Rational(-2, 3)*c} + assert solve([A*C - C*A], [a, b, c, d]) == {a: d - c, b: Rational(2, 3)*c} + assert solve([A*B - B*A, A*C - C*A], [a, b, c, d]) == {a: d, b: 0, c: 0} + + assert solve([Eq(A*B, B*A)], [a, b, c, d]) == {a: d, b: Rational(-2, 3)*c} + assert solve([Eq(A*C, C*A)], [a, b, c, d]) == {a: d - c, b: Rational(2, 3)*c} + assert solve([Eq(A*B, B*A), Eq(A*C, C*A)], [a, b, c, d]) == {a: d, b: 0, c: 0} + + # https://github.com/sympy/sympy/issues/27854 + m, n = symbols("m n") + A = MatrixSymbol("A", m, n) + x = MatrixSymbol("x", n, 1) + b = MatrixSymbol('b', m, 1) + r = A * x - b + f = r.T * r + grad_f = f.diff(x) + raises(ValueError, lambda: solve(grad_f, x)) + + +def test_solve_linear(): + w = Wild('w') + assert solve_linear(x, x) == (0, 1) + assert solve_linear(x, exclude=[x]) == (0, 1) + assert solve_linear(x, symbols=[w]) == (0, 1) + assert solve_linear(x, y - 2*x) in [(x, y/3), (y, 3*x)] + assert solve_linear(x, y - 2*x, exclude=[x]) == (y, 3*x) + assert solve_linear(3*x - y, 0) in [(x, y/3), (y, 3*x)] + assert solve_linear(3*x - y, 0, [x]) == (x, y/3) + assert solve_linear(3*x - y, 0, [y]) == (y, 3*x) + assert solve_linear(x**2/y, 1) == (y, x**2) + assert solve_linear(w, x) in [(w, x), (x, w)] + assert solve_linear(cos(x)**2 + sin(x)**2 + 2 + y) == \ + (y, -2 - cos(x)**2 - sin(x)**2) + assert solve_linear(cos(x)**2 + sin(x)**2 + 2 + y, symbols=[x]) == (0, 1) + assert solve_linear(Eq(x, 3)) == (x, 3) + assert solve_linear(1/(1/x - 2)) == (0, 0) + assert solve_linear((x + 1)*exp(-x), symbols=[x]) == (x, -1) + assert solve_linear((x + 1)*exp(x), symbols=[x]) == ((x + 1)*exp(x), 1) + assert solve_linear(x*exp(-x**2), symbols=[x]) == (x, 0) + assert solve_linear(0**x - 1) == (0**x - 1, 1) + assert solve_linear(1 + 1/(x - 1)) == (x, 0) + eq = y*cos(x)**2 + y*sin(x)**2 - y # = y*(1 - 1) = 0 + assert solve_linear(eq) == (0, 1) + eq = cos(x)**2 + sin(x)**2 # = 1 + assert solve_linear(eq) == (0, 1) + raises(ValueError, lambda: solve_linear(Eq(x, 3), 3)) + + +def test_solve_undetermined_coeffs(): + assert solve_undetermined_coeffs( + a*x**2 + b*x**2 + b*x + 2*c*x + c + 1, [a, b, c], x + ) == {a: -2, b: 2, c: -1} + # Test that rational functions work + assert solve_undetermined_coeffs(a/x + b/(x + 1) + - (2*x + 1)/(x**2 + x), [a, b], x) == {a: 1, b: 1} + # Test cancellation in rational functions + assert solve_undetermined_coeffs( + ((c + 1)*a*x**2 + (c + 1)*b*x**2 + + (c + 1)*b*x + (c + 1)*2*c*x + (c + 1)**2)/(c + 1), + [a, b, c], x) == \ + {a: -2, b: 2, c: -1} + # multivariate + X, Y, Z = y, x**y, y*x**y + eq = a*X + b*Y + c*Z - X - 2*Y - 3*Z + coeffs = a, b, c + syms = x, y + assert solve_undetermined_coeffs(eq, coeffs) == { + a: 1, b: 2, c: 3} + assert solve_undetermined_coeffs(eq, coeffs, syms) == { + a: 1, b: 2, c: 3} + assert solve_undetermined_coeffs(eq, coeffs, *syms) == { + a: 1, b: 2, c: 3} + # check output format + assert solve_undetermined_coeffs(a*x + a - 2, [a]) == [] + assert solve_undetermined_coeffs(a**2*x - 4*x, [a]) == [ + {a: -2}, {a: 2}] + assert solve_undetermined_coeffs(0, [a]) == [] + assert solve_undetermined_coeffs(0, [a], dict=True) == [] + assert solve_undetermined_coeffs(0, [a], set=True) == ([], {}) + assert solve_undetermined_coeffs(1, [a]) == [] + abeq = a*x - 2*x + b - 3 + s = {b, a} + assert solve_undetermined_coeffs(abeq, s, x) == {a: 2, b: 3} + assert solve_undetermined_coeffs(abeq, s, x, set=True) == ([a, b], {(2, 3)}) + assert solve_undetermined_coeffs(sin(a*x) - sin(2*x), (a,)) is None + assert solve_undetermined_coeffs(a*x + b*x - 2*x, (a, b)) == {a: 2 - b} + + +def test_solve_inequalities(): + x = Symbol('x') + sol = And(S.Zero < x, x < oo) + assert solve(x + 1 > 1) == sol + assert solve([x + 1 > 1]) == sol + assert solve([x + 1 > 1], x) == sol + assert solve([x + 1 > 1], [x]) == sol + + system = [Lt(x**2 - 2, 0), Gt(x**2 - 1, 0)] + assert solve(system) == \ + And(Or(And(Lt(-sqrt(2), x), Lt(x, -1)), + And(Lt(1, x), Lt(x, sqrt(2)))), Eq(0, 0)) + + x = Symbol('x', real=True) + system = [Lt(x**2 - 2, 0), Gt(x**2 - 1, 0)] + assert solve(system) == \ + Or(And(Lt(-sqrt(2), x), Lt(x, -1)), And(Lt(1, x), Lt(x, sqrt(2)))) + + # issues 6627, 3448 + assert solve((x - 3)/(x - 2) < 0, x) == And(Lt(2, x), Lt(x, 3)) + assert solve(x/(x + 1) > 1, x) == And(Lt(-oo, x), Lt(x, -1)) + + assert solve(sin(x) > S.Half) == And(pi/6 < x, x < pi*Rational(5, 6)) + + assert solve(Eq(False, x < 1)) == (S.One <= x) & (x < oo) + assert solve(Eq(True, x < 1)) == (-oo < x) & (x < 1) + assert solve(Eq(x < 1, False)) == (S.One <= x) & (x < oo) + assert solve(Eq(x < 1, True)) == (-oo < x) & (x < 1) + + assert solve(Eq(False, x)) == False + assert solve(Eq(0, x)) == [0] + assert solve(Eq(True, x)) == True + assert solve(Eq(1, x)) == [1] + assert solve(Eq(False, ~x)) == True + assert solve(Eq(True, ~x)) == False + assert solve(Ne(True, x)) == False + assert solve(Ne(1, x)) == (x > -oo) & (x < oo) & Ne(x, 1) + + +def test_issue_4793(): + assert solve(1/x) == [] + assert solve(x*(1 - 5/x)) == [5] + assert solve(x + sqrt(x) - 2) == [1] + assert solve(-(1 + x)/(2 + x)**2 + 1/(2 + x)) == [] + assert solve(-x**2 - 2*x + (x + 1)**2 - 1) == [] + assert solve((x/(x + 1) + 3)**(-2)) == [] + assert solve(x/sqrt(x**2 + 1), x) == [0] + assert solve(exp(x) - y, x) == [log(y)] + assert solve(exp(x)) == [] + assert solve(x**2 + x + sin(y)**2 + cos(y)**2 - 1, x) in [[0, -1], [-1, 0]] + eq = 4*3**(5*x + 2) - 7 + ans = solve(eq, x) + assert len(ans) == 5 and all(eq.subs(x, a).n(chop=True) == 0 for a in ans) + assert solve(log(x**2) - y**2/exp(x), x, y, set=True) == ( + [x, y], + {(x, sqrt(exp(x) * log(x ** 2))), (x, -sqrt(exp(x) * log(x ** 2)))}) + assert solve(x**2*z**2 - z**2*y**2) == [{x: -y}, {x: y}, {z: 0}] + assert solve((x - 1)/(1 + 1/(x - 1))) == [] + assert solve(x**(y*z) - x, x) == [1] + raises(NotImplementedError, lambda: solve(log(x) - exp(x), x)) + raises(NotImplementedError, lambda: solve(2**x - exp(x) - 3)) + + +def test_PR1964(): + # issue 5171 + assert solve(sqrt(x)) == solve(sqrt(x**3)) == [0] + assert solve(sqrt(x - 1)) == [1] + # issue 4462 + a = Symbol('a') + assert solve(-3*a/sqrt(x), x) == [] + # issue 4486 + assert solve(2*x/(x + 2) - 1, x) == [2] + # issue 4496 + assert set(solve((x**2/(7 - x)).diff(x))) == {S.Zero, S(14)} + # issue 4695 + f = Function('f') + assert solve((3 - 5*x/f(x))*f(x), f(x)) == [x*Rational(5, 3)] + # issue 4497 + assert solve(1/root(5 + x, 5) - 9, x) == [Rational(-295244, 59049)] + + assert solve(sqrt(x) + sqrt(sqrt(x)) - 4) == [(Rational(-1, 2) + sqrt(17)/2)**4] + assert set(solve(Poly(sqrt(exp(x)) + sqrt(exp(-x)) - 4))) in \ + [ + {log((-sqrt(3) + 2)**2), log((sqrt(3) + 2)**2)}, + {2*log(-sqrt(3) + 2), 2*log(sqrt(3) + 2)}, + {log(-4*sqrt(3) + 7), log(4*sqrt(3) + 7)}, + ] + assert set(solve(Poly(exp(x) + exp(-x) - 4))) == \ + {log(-sqrt(3) + 2), log(sqrt(3) + 2)} + assert set(solve(x**y + x**(2*y) - 1, x)) == \ + {(Rational(-1, 2) + sqrt(5)/2)**(1/y), (Rational(-1, 2) - sqrt(5)/2)**(1/y)} + + assert solve(exp(x/y)*exp(-z/y) - 2, y) == [(x - z)/log(2)] + assert solve( + x**z*y**z - 2, z) in [[log(2)/(log(x) + log(y))], [log(2)/(log(x*y))]] + # if you do inversion too soon then multiple roots (as for the following) + # will be missed, e.g. if exp(3*x) = exp(3) -> 3*x = 3 + E = S.Exp1 + assert solve(exp(3*x) - exp(3), x) in [ + [1, log(E*(Rational(-1, 2) - sqrt(3)*I/2)), log(E*(Rational(-1, 2) + sqrt(3)*I/2))], + [1, log(-E/2 - sqrt(3)*E*I/2), log(-E/2 + sqrt(3)*E*I/2)], + ] + + # coverage test + p = Symbol('p', positive=True) + assert solve((1/p + 1)**(p + 1)) == [] + + +def test_issue_5197(): + x = Symbol('x', real=True) + assert solve(x**2 + 1, x) == [] + n = Symbol('n', integer=True, positive=True) + assert solve((n - 1)*(n + 2)*(2*n - 1), n) == [1] + x = Symbol('x', positive=True) + y = Symbol('y') + assert solve([x + 5*y - 2, -3*x + 6*y - 15], x, y) == [] + # not {x: -3, y: 1} b/c x is positive + # The solution following should not contain (-sqrt(2), sqrt(2)) + assert solve([(x + y), 2 - y**2], x, y) == [(sqrt(2), -sqrt(2))] + y = Symbol('y', positive=True) + # The solution following should not contain {y: -x*exp(x/2)} + assert solve(x**2 - y**2/exp(x), y, x, dict=True) == [{y: x*exp(x/2)}] + x, y, z = symbols('x y z', positive=True) + assert solve(z**2*x**2 - z**2*y**2/exp(x), y, x, z, dict=True) == [{y: x*exp(x/2)}] + + +def test_checking(): + assert set( + solve(x*(x - y/x), x, check=False)) == {sqrt(y), S.Zero, -sqrt(y)} + assert set(solve(x*(x - y/x), x, check=True)) == {sqrt(y), -sqrt(y)} + # {x: 0, y: 4} sets denominator to 0 in the following so system should return None + assert solve((1/(1/x + 2), 1/(y - 3) - 1)) == [] + # 0 sets denominator of 1/x to zero so None is returned + assert solve(1/(1/x + 2)) == [] + + +def test_issue_4671_4463_4467(): + assert solve(sqrt(x**2 - 1) - 2) in ([sqrt(5), -sqrt(5)], + [-sqrt(5), sqrt(5)]) + assert solve((2**exp(y**2/x) + 2)/(x**2 + 15), y) == [ + -sqrt(x*log(1 + I*pi/log(2))), sqrt(x*log(1 + I*pi/log(2)))] + + C1, C2 = symbols('C1 C2') + f = Function('f') + assert solve(C1 + C2/x**2 - exp(-f(x)), f(x)) == [log(x**2/(C1*x**2 + C2))] + a = Symbol('a') + E = S.Exp1 + assert solve(1 - log(a + 4*x**2), x) in ( + [-sqrt(-a + E)/2, sqrt(-a + E)/2], + [sqrt(-a + E)/2, -sqrt(-a + E)/2] + ) + assert solve(log(a**(-3) - x**2)/a, x) in ( + [-sqrt(-1 + a**(-3)), sqrt(-1 + a**(-3))], + [sqrt(-1 + a**(-3)), -sqrt(-1 + a**(-3))],) + assert solve(1 - log(a + 4*x**2), x) in ( + [-sqrt(-a + E)/2, sqrt(-a + E)/2], + [sqrt(-a + E)/2, -sqrt(-a + E)/2],) + assert solve((a**2 + 1)*(sin(a*x) + cos(a*x)), x) == [-pi/(4*a)] + assert solve(3 - (sinh(a*x) + cosh(a*x)), x) == [log(3)/a] + assert set(solve(3 - (sinh(a*x) + cosh(a*x)**2), x)) == \ + {log(-2 + sqrt(5))/a, log(-sqrt(2) + 1)/a, + log(-sqrt(5) - 2)/a, log(1 + sqrt(2))/a} + assert solve(atan(x) - 1) == [tan(1)] + + +def test_issue_5132(): + r, t = symbols('r,t') + assert set(solve([r - x**2 - y**2, tan(t) - y/x], [x, y])) == \ + {( + -sqrt(r*cos(t)**2), -1*sqrt(r*cos(t)**2)*tan(t)), + (sqrt(r*cos(t)**2), sqrt(r*cos(t)**2)*tan(t))} + assert solve([exp(x) - sin(y), 1/y - 3], [x, y]) == \ + [(log(sin(Rational(1, 3))), Rational(1, 3))] + assert solve([exp(x) - sin(y), 1/exp(y) - 3], [x, y]) == \ + [(log(-sin(log(3))), -log(3))] + assert set(solve([exp(x) - sin(y), y**2 - 4], [x, y])) == \ + {(log(-sin(2)), -S(2)), (log(sin(2)), S(2))} + eqs = [exp(x)**2 - sin(y) + z**2, 1/exp(y) - 3] + assert solve(eqs, set=True) == \ + ([y, z], { + (-log(3), sqrt(-exp(2*x) - sin(log(3)))), + (-log(3), -sqrt(-exp(2*x) - sin(log(3))))}) + assert solve(eqs, x, z, set=True) == ( + [x, z], + {(x, sqrt(-exp(2*x) + sin(y))), (x, -sqrt(-exp(2*x) + sin(y)))}) + assert set(solve(eqs, x, y)) == \ + { + (log(-sqrt(-z**2 - sin(log(3)))), -log(3)), + (log(-z**2 - sin(log(3)))/2, -log(3))} + assert set(solve(eqs, y, z)) == \ + { + (-log(3), -sqrt(-exp(2*x) - sin(log(3)))), + (-log(3), sqrt(-exp(2*x) - sin(log(3))))} + eqs = [exp(x)**2 - sin(y) + z, 1/exp(y) - 3] + assert solve(eqs, set=True) == ([y, z], { + (-log(3), -exp(2*x) - sin(log(3)))}) + assert solve(eqs, x, z, set=True) == ( + [x, z], {(x, -exp(2*x) + sin(y))}) + assert set(solve(eqs, x, y)) == { + (log(-sqrt(-z - sin(log(3)))), -log(3)), + (log(-z - sin(log(3)))/2, -log(3))} + assert solve(eqs, z, y) == \ + [(-exp(2*x) - sin(log(3)), -log(3))] + assert solve((sqrt(x**2 + y**2) - sqrt(10), x + y - 4), set=True) == ( + [x, y], {(S.One, S(3)), (S(3), S.One)}) + assert set(solve((sqrt(x**2 + y**2) - sqrt(10), x + y - 4), x, y)) == \ + {(S.One, S(3)), (S(3), S.One)} + + +def test_issue_5335(): + lam, a0, conc = symbols('lam a0 conc') + a = 0.005 + b = 0.743436700916726 + eqs = [lam + 2*y - a0*(1 - x/2)*x - a*x/2*x, + a0*(1 - x/2)*x - 1*y - b*y, + x + y - conc] + sym = [x, y, a0] + # there are 4 solutions obtained manually but only two are valid + assert len(solve(eqs, sym, manual=True, minimal=True)) == 2 + assert len(solve(eqs, sym)) == 2 # cf below with rational=False + + +@SKIP("Hangs") +def _test_issue_5335_float(): + # gives ZeroDivisionError: polynomial division + lam, a0, conc = symbols('lam a0 conc') + a = 0.005 + b = 0.743436700916726 + eqs = [lam + 2*y - a0*(1 - x/2)*x - a*x/2*x, + a0*(1 - x/2)*x - 1*y - b*y, + x + y - conc] + sym = [x, y, a0] + assert len(solve(eqs, sym, rational=False)) == 2 + + +def test_issue_5767(): + assert set(solve([x**2 + y + 4], [x])) == \ + {(-sqrt(-y - 4),), (sqrt(-y - 4),)} + + +def _make_example_24609(): + D, R, H, B_g, V, D_c = symbols("D, R, H, B_g, V, D_c", real=True, positive=True) + Sigma_f, Sigma_a, nu = symbols("Sigma_f, Sigma_a, nu", real=True, positive=True) + x = symbols("x", real=True, positive=True) + eq = ( + 2**(S(2)/3)*pi**(S(2)/3)*D_c*(S(231361)/10000 + pi**2/x**2) + /(6*V**(S(2)/3)*x**(S(1)/3)) + - 2**(S(2)/3)*pi**(S(8)/3)*D_c/(2*V**(S(2)/3)*x**(S(7)/3)) + ) + expected = 100*sqrt(2)*pi/481 + return eq, expected, x + + +def test_issue_24609(): + # https://github.com/sympy/sympy/issues/24609 + eq, expected, x = _make_example_24609() + assert solve(eq, x, simplify=True) == [expected] + [solapprox] = solve(eq.n(), x) + assert abs(solapprox - expected.n()) < 1e-14 + + +@XFAIL +def test_issue_24609_xfail(): + # + # This returns 5 solutions when it should be 1 (with x positive). + # Simplification reveals all solutions to be equivalent. It is expected + # that solve without simplify=True returns duplicate solutions in some + # cases but the core of this equation is a simple quadratic that can easily + # be solved without introducing any redundant solutions: + # + # >>> print(factor_terms(eq.as_numer_denom()[0])) + # 2**(2/3)*pi**(2/3)*D_c*V**(2/3)*x**(7/3)*(231361*x**2 - 20000*pi**2) + # + eq, expected, x = _make_example_24609() + assert len(solve(eq, x)) == [expected] + # + # We do not want to pass this test just by using simplify so if the above + # passes then uncomment the additional test below: + # + # assert len(solve(eq, x, simplify=False)) == 1 + + +def test_polysys(): + assert set(solve([x**2 + 2/y - 2, x + y - 3], [x, y])) == \ + {(S.One, S(2)), (1 + sqrt(5), 2 - sqrt(5)), + (1 - sqrt(5), 2 + sqrt(5))} + assert solve([x**2 + y - 2, x**2 + y]) == [] + # the ordering should be whatever the user requested + assert solve([x**2 + y - 3, x - y - 4], (x, y)) != solve([x**2 + + y - 3, x - y - 4], (y, x)) + + +@slow +def test_unrad1(): + raises(NotImplementedError, lambda: + unrad(sqrt(x) + sqrt(x + 1) + sqrt(1 - sqrt(x)) + 3)) + raises(NotImplementedError, lambda: + unrad(sqrt(x) + (x + 1)**Rational(1, 3) + 2*sqrt(y))) + + s = symbols('s', cls=Dummy) + + # checkers to deal with possibility of answer coming + # back with a sign change (cf issue 5203) + def check(rv, ans): + assert bool(rv[1]) == bool(ans[1]) + if ans[1]: + return s_check(rv, ans) + e = rv[0].expand() + a = ans[0].expand() + return e in [a, -a] and rv[1] == ans[1] + + def s_check(rv, ans): + # get the dummy + rv = list(rv) + d = rv[0].atoms(Dummy) + reps = list(zip(d, [s]*len(d))) + # replace s with this dummy + rv = (rv[0].subs(reps).expand(), [rv[1][0].subs(reps), rv[1][1].subs(reps)]) + ans = (ans[0].subs(reps).expand(), [ans[1][0].subs(reps), ans[1][1].subs(reps)]) + return str(rv[0]) in [str(ans[0]), str(-ans[0])] and \ + str(rv[1]) == str(ans[1]) + + assert unrad(1) is None + assert check(unrad(sqrt(x)), + (x, [])) + assert check(unrad(sqrt(x) + 1), + (x - 1, [])) + assert check(unrad(sqrt(x) + root(x, 3) + 2), + (s**3 + s**2 + 2, [s, s**6 - x])) + assert check(unrad(sqrt(x)*root(x, 3) + 2), + (x**5 - 64, [])) + assert check(unrad(sqrt(x) + (x + 1)**Rational(1, 3)), + (x**3 - (x + 1)**2, [])) + assert check(unrad(sqrt(x) + sqrt(x + 1) + sqrt(2*x)), + (-2*sqrt(2)*x - 2*x + 1, [])) + assert check(unrad(sqrt(x) + sqrt(x + 1) + 2), + (16*x - 9, [])) + assert check(unrad(sqrt(x) + sqrt(x + 1) + sqrt(1 - x)), + (5*x**2 - 4*x, [])) + assert check(unrad(a*sqrt(x) + b*sqrt(x) + c*sqrt(y) + d*sqrt(y)), + ((a*sqrt(x) + b*sqrt(x))**2 - (c*sqrt(y) + d*sqrt(y))**2, [])) + assert check(unrad(sqrt(x) + sqrt(1 - x)), + (2*x - 1, [])) + assert check(unrad(sqrt(x) + sqrt(1 - x) - 3), + (x**2 - x + 16, [])) + assert check(unrad(sqrt(x) + sqrt(1 - x) + sqrt(2 + x)), + (5*x**2 - 2*x + 1, [])) + assert unrad(sqrt(x) + sqrt(1 - x) + sqrt(2 + x) - 3) in [ + (25*x**4 + 376*x**3 + 1256*x**2 - 2272*x + 784, []), + (25*x**8 - 476*x**6 + 2534*x**4 - 1468*x**2 + 169, [])] + assert unrad(sqrt(x) + sqrt(1 - x) + sqrt(2 + x) - sqrt(1 - 2*x)) == \ + (41*x**4 + 40*x**3 + 232*x**2 - 160*x + 16, []) # orig root at 0.487 + assert check(unrad(sqrt(x) + sqrt(x + 1)), (S.One, [])) + + eq = sqrt(x) + sqrt(x + 1) + sqrt(1 - sqrt(x)) + assert check(unrad(eq), + (16*x**2 - 9*x, [])) + assert set(solve(eq, check=False)) == {S.Zero, Rational(9, 16)} + assert solve(eq) == [] + # but this one really does have those solutions + assert set(solve(sqrt(x) - sqrt(x + 1) + sqrt(1 - sqrt(x)))) == \ + {S.Zero, Rational(9, 16)} + + assert check(unrad(sqrt(x) + root(x + 1, 3) + 2*sqrt(y), y), + (S('2*sqrt(x)*(x + 1)**(1/3) + x - 4*y + (x + 1)**(2/3)'), [])) + assert check(unrad(sqrt(x/(1 - x)) + (x + 1)**Rational(1, 3)), + (x**5 - x**4 - x**3 + 2*x**2 + x - 1, [])) + assert check(unrad(sqrt(x/(1 - x)) + 2*sqrt(y), y), + (4*x*y + x - 4*y, [])) + assert check(unrad(sqrt(x)*sqrt(1 - x) + 2, x), + (x**2 - x + 4, [])) + + # http://tutorial.math.lamar.edu/ + # Classes/Alg/SolveRadicalEqns.aspx#Solve_Rad_Ex2_a + assert solve(Eq(x, sqrt(x + 6))) == [3] + assert solve(Eq(x + sqrt(x - 4), 4)) == [4] + assert solve(Eq(1, x + sqrt(2*x - 3))) == [] + assert set(solve(Eq(sqrt(5*x + 6) - 2, x))) == {-S.One, S(2)} + assert set(solve(Eq(sqrt(2*x - 1) - sqrt(x - 4), 2))) == {S(5), S(13)} + assert solve(Eq(sqrt(x + 7) + 2, sqrt(3 - x))) == [-6] + # http://www.purplemath.com/modules/solverad.htm + assert solve((2*x - 5)**Rational(1, 3) - 3) == [16] + assert set(solve(x + 1 - root(x**4 + 4*x**3 - x, 4))) == \ + {Rational(-1, 2), Rational(-1, 3)} + assert set(solve(sqrt(2*x**2 - 7) - (3 - x))) == {-S(8), S(2)} + assert solve(sqrt(2*x + 9) - sqrt(x + 1) - sqrt(x + 4)) == [0] + assert solve(sqrt(x + 4) + sqrt(2*x - 1) - 3*sqrt(x - 1)) == [5] + assert solve(sqrt(x)*sqrt(x - 7) - 12) == [16] + assert solve(sqrt(x - 3) + sqrt(x) - 3) == [4] + assert solve(sqrt(9*x**2 + 4) - (3*x + 2)) == [0] + assert solve(sqrt(x) - 2 - 5) == [49] + assert solve(sqrt(x - 3) - sqrt(x) - 3) == [] + assert solve(sqrt(x - 1) - x + 7) == [10] + assert solve(sqrt(x - 2) - 5) == [27] + assert solve(sqrt(17*x - sqrt(x**2 - 5)) - 7) == [3] + assert solve(sqrt(x) - sqrt(x - 1) + sqrt(sqrt(x))) == [] + + # don't posify the expression in unrad and do use _mexpand + z = sqrt(2*x + 1)/sqrt(x) - sqrt(2 + 1/x) + p = posify(z)[0] + assert solve(p) == [] + assert solve(z) == [] + assert solve(z + 6*I) == [Rational(-1, 11)] + assert solve(p + 6*I) == [] + # issue 8622 + assert unrad(root(x + 1, 5) - root(x, 3)) == ( + -(x**5 - x**3 - 3*x**2 - 3*x - 1), []) + # issue #8679 + assert check(unrad(x + root(x, 3) + root(x, 3)**2 + sqrt(y), x), + (s**3 + s**2 + s + sqrt(y), [s, s**3 - x])) + + # for coverage + assert check(unrad(sqrt(x) + root(x, 3) + y), + (s**3 + s**2 + y, [s, s**6 - x])) + assert solve(sqrt(x) + root(x, 3) - 2) == [1] + raises(NotImplementedError, lambda: + solve(sqrt(x) + root(x, 3) + root(x + 1, 5) - 2)) + # fails through a different code path + raises(NotImplementedError, lambda: solve(-sqrt(2) + cosh(x)/x)) + # unrad some + assert solve(sqrt(x + root(x, 3))+root(x - y, 5), y) == [ + x + (x**Rational(1, 3) + x)**Rational(5, 2)] + assert check(unrad(sqrt(x) - root(x + 1, 3)*sqrt(x + 2) + 2), + (s**10 + 8*s**8 + 24*s**6 - 12*s**5 - 22*s**4 - 160*s**3 - 212*s**2 - + 192*s - 56, [s, s**2 - x])) + e = root(x + 1, 3) + root(x, 3) + assert unrad(e) == (2*x + 1, []) + eq = (sqrt(x) + sqrt(x + 1) + sqrt(1 - x) - 6*sqrt(5)/5) + assert check(unrad(eq), + (15625*x**4 + 173000*x**3 + 355600*x**2 - 817920*x + 331776, [])) + assert check(unrad(root(x, 4) + root(x, 4)**3 - 1), + (s**3 + s - 1, [s, s**4 - x])) + assert check(unrad(root(x, 2) + root(x, 2)**3 - 1), + (x**3 + 2*x**2 + x - 1, [])) + assert unrad(x**0.5) is None + assert check(unrad(t + root(x + y, 5) + root(x + y, 5)**3), + (s**3 + s + t, [s, s**5 - x - y])) + assert check(unrad(x + root(x + y, 5) + root(x + y, 5)**3, y), + (s**3 + s + x, [s, s**5 - x - y])) + assert check(unrad(x + root(x + y, 5) + root(x + y, 5)**3, x), + (s**5 + s**3 + s - y, [s, s**5 - x - y])) + assert check(unrad(root(x - 1, 3) + root(x + 1, 5) + root(2, 5)), + (s**5 + 5*2**Rational(1, 5)*s**4 + s**3 + 10*2**Rational(2, 5)*s**3 + + 10*2**Rational(3, 5)*s**2 + 5*2**Rational(4, 5)*s + 4, [s, s**3 - x + 1])) + raises(NotImplementedError, lambda: + unrad((root(x, 2) + root(x, 3) + root(x, 4)).subs(x, x**5 - x + 1))) + + # the simplify flag should be reset to False for unrad results; + # if it's not then this next test will take a long time + assert solve(root(x, 3) + root(x, 5) - 2) == [1] + eq = (sqrt(x) + sqrt(x + 1) + sqrt(1 - x) - 6*sqrt(5)/5) + assert check(unrad(eq), + ((5*x - 4)*(3125*x**3 + 37100*x**2 + 100800*x - 82944), [])) + ans = S(''' + [4/5, -1484/375 + 172564/(140625*(114*sqrt(12657)/78125 + + 12459439/52734375)**(1/3)) + + 4*(114*sqrt(12657)/78125 + 12459439/52734375)**(1/3)]''') + assert solve(eq) == ans + # duplicate radical handling + assert check(unrad(sqrt(x + root(x + 1, 3)) - root(x + 1, 3) - 2), + (s**3 - s**2 - 3*s - 5, [s, s**3 - x - 1])) + # cov post-processing + e = root(x**2 + 1, 3) - root(x**2 - 1, 5) - 2 + assert check(unrad(e), + (s**5 - 10*s**4 + 39*s**3 - 80*s**2 + 80*s - 30, + [s, s**3 - x**2 - 1])) + + e = sqrt(x + root(x + 1, 2)) - root(x + 1, 3) - 2 + assert check(unrad(e), + (s**6 - 2*s**5 - 7*s**4 - 3*s**3 + 26*s**2 + 40*s + 25, + [s, s**3 - x - 1])) + assert check(unrad(e, _reverse=True), + (s**6 - 14*s**5 + 73*s**4 - 187*s**3 + 276*s**2 - 228*s + 89, + [s, s**2 - x - sqrt(x + 1)])) + # this one needs r0, r1 reversal to work + assert check(unrad(sqrt(x + sqrt(root(x, 3) - 1)) - root(x, 6) - 2), + (s**12 - 2*s**8 - 8*s**7 - 8*s**6 + s**4 + 8*s**3 + 23*s**2 + + 32*s + 17, [s, s**6 - x])) + + # why does this pass + assert unrad(root(cosh(x), 3)/x*root(x + 1, 5) - 1) == ( + -(x**15 - x**3*cosh(x)**5 - 3*x**2*cosh(x)**5 - 3*x*cosh(x)**5 + - cosh(x)**5), []) + # and this fail? + #assert unrad(sqrt(cosh(x)/x) + root(x + 1, 3)*sqrt(x) - 1) == ( + # -s**6 + 6*s**5 - 15*s**4 + 20*s**3 - 15*s**2 + 6*s + x**5 + + # 2*x**4 + x**3 - 1, [s, s**2 - cosh(x)/x]) + + # watch for symbols in exponents + assert unrad(S('(x+y)**(2*y/3) + (x+y)**(1/3) + 1')) is None + assert check(unrad(S('(x+y)**(2*y/3) + (x+y)**(1/3) + 1'), x), + (s**(2*y) + s + 1, [s, s**3 - x - y])) + # should _Q be so lenient? + assert unrad(x**(S.Half/y) + y, x) == (x**(1/y) - y**2, []) + + # This tests two things: that if full unrad is attempted and fails + # the solution should still be found; also it tests that the use of + # composite + assert len(solve(sqrt(y)*x + x**3 - 1, x)) == 3 + assert len(solve(-512*y**3 + 1344*(x + 2)**Rational(1, 3)*y**2 - + 1176*(x + 2)**Rational(2, 3)*y - 169*x + 686, y, _unrad=False)) == 3 + + # watch out for when the cov doesn't involve the symbol of interest + eq = S('-x + (7*y/8 - (27*x/2 + 27*sqrt(x**2)/2)**(1/3)/3)**3 - 1') + assert solve(eq, y) == [ + 2**(S(2)/3)*(27*x + 27*sqrt(x**2))**(S(1)/3)*S(4)/21 + (512*x/343 + + S(512)/343)**(S(1)/3)*(-S(1)/2 - sqrt(3)*I/2), 2**(S(2)/3)*(27*x + + 27*sqrt(x**2))**(S(1)/3)*S(4)/21 + (512*x/343 + + S(512)/343)**(S(1)/3)*(-S(1)/2 + sqrt(3)*I/2), 2**(S(2)/3)*(27*x + + 27*sqrt(x**2))**(S(1)/3)*S(4)/21 + (512*x/343 + S(512)/343)**(S(1)/3)] + + eq = root(x + 1, 3) - (root(x, 3) + root(x, 5)) + assert check(unrad(eq), + (3*s**13 + 3*s**11 + s**9 - 1, [s, s**15 - x])) + assert check(unrad(eq - 2), + (3*s**13 + 3*s**11 + 6*s**10 + s**9 + 12*s**8 + 6*s**6 + 12*s**5 + + 12*s**3 + 7, [s, s**15 - x])) + assert check(unrad(root(x, 3) - root(x + 1, 4)/2 + root(x + 2, 3)), + (s*(4096*s**9 + 960*s**8 + 48*s**7 - s**6 - 1728), + [s, s**4 - x - 1])) # orig expr has two real roots: -1, -.389 + assert check(unrad(root(x, 3) + root(x + 1, 4) - root(x + 2, 3)/2), + (343*s**13 + 2904*s**12 + 1344*s**11 + 512*s**10 - 1323*s**9 - + 3024*s**8 - 1728*s**7 + 1701*s**5 + 216*s**4 - 729*s, [s, s**4 - x - + 1])) # orig expr has one real root: -0.048 + assert check(unrad(root(x, 3)/2 - root(x + 1, 4) + root(x + 2, 3)), + (729*s**13 - 216*s**12 + 1728*s**11 - 512*s**10 + 1701*s**9 - + 3024*s**8 + 1344*s**7 + 1323*s**5 - 2904*s**4 + 343*s, [s, s**4 - x - + 1])) # orig expr has 2 real roots: -0.91, -0.15 + assert check(unrad(root(x, 3)/2 - root(x + 1, 4) + root(x + 2, 3) - 2), + (729*s**13 + 1242*s**12 + 18496*s**10 + 129701*s**9 + 388602*s**8 + + 453312*s**7 - 612864*s**6 - 3337173*s**5 - 6332418*s**4 - 7134912*s**3 + - 5064768*s**2 - 2111913*s - 398034, [s, s**4 - x - 1])) + # orig expr has 1 real root: 19.53 + + ans = solve(sqrt(x) + sqrt(x + 1) - + sqrt(1 - x) - sqrt(2 + x)) + assert len(ans) == 1 and NS(ans[0])[:4] == '0.73' + # the fence optimization problem + # https://github.com/sympy/sympy/issues/4793#issuecomment-36994519 + F = Symbol('F') + eq = F - (2*x + 2*y + sqrt(x**2 + y**2)) + ans = F*Rational(2, 7) - sqrt(2)*F/14 + X = solve(eq, x, check=False) + for xi in reversed(X): # reverse since currently, ans is the 2nd one + Y = solve((x*y).subs(x, xi).diff(y), y, simplify=False, check=False) + if any((a - ans).expand().is_zero for a in Y): + break + else: + assert None # no answer was found + assert solve(sqrt(x + 1) + root(x, 3) - 2) == S(''' + [(-11/(9*(47/54 + sqrt(93)/6)**(1/3)) + 1/3 + (47/54 + + sqrt(93)/6)**(1/3))**3]''') + assert solve(sqrt(sqrt(x + 1)) + x**Rational(1, 3) - 2) == S(''' + [(-sqrt(-2*(-1/16 + sqrt(6913)/16)**(1/3) + 6/(-1/16 + + sqrt(6913)/16)**(1/3) + 17/2 + 121/(4*sqrt(-6/(-1/16 + + sqrt(6913)/16)**(1/3) + 2*(-1/16 + sqrt(6913)/16)**(1/3) + 17/4)))/2 + + sqrt(-6/(-1/16 + sqrt(6913)/16)**(1/3) + 2*(-1/16 + + sqrt(6913)/16)**(1/3) + 17/4)/2 + 9/4)**3]''') + assert solve(sqrt(x) + root(sqrt(x) + 1, 3) - 2) == S(''' + [(-(81/2 + 3*sqrt(741)/2)**(1/3)/3 + (81/2 + 3*sqrt(741)/2)**(-1/3) + + 2)**2]''') + eq = S(''' + -x + (1/2 - sqrt(3)*I/2)*(3*x**3/2 - x*(3*x**2 - 34)/2 + sqrt((-3*x**3 + + x*(3*x**2 - 34) + 90)**2/4 - 39304/27) - 45)**(1/3) + 34/(3*(1/2 - + sqrt(3)*I/2)*(3*x**3/2 - x*(3*x**2 - 34)/2 + sqrt((-3*x**3 + x*(3*x**2 + - 34) + 90)**2/4 - 39304/27) - 45)**(1/3))''') + assert check(unrad(eq), + (s*-(-s**6 + sqrt(3)*s**6*I - 153*2**Rational(2, 3)*3**Rational(1, 3)*s**4 + + 51*12**Rational(1, 3)*s**4 - 102*2**Rational(2, 3)*3**Rational(5, 6)*s**4*I - 1620*s**3 + + 1620*sqrt(3)*s**3*I + 13872*18**Rational(1, 3)*s**2 - 471648 + + 471648*sqrt(3)*I), [s, s**3 - 306*x - sqrt(3)*sqrt(31212*x**2 - + 165240*x + 61484) + 810])) + + assert solve(eq) == [] # not other code errors + eq = root(x, 3) - root(y, 3) + root(x, 5) + assert check(unrad(eq), + (s**15 + 3*s**13 + 3*s**11 + s**9 - y, [s, s**15 - x])) + eq = root(x, 3) + root(y, 3) + root(x*y, 4) + assert check(unrad(eq), + (s*y*(-s**12 - 3*s**11*y - 3*s**10*y**2 - s**9*y**3 - + 3*s**8*y**2 + 21*s**7*y**3 - 3*s**6*y**4 - 3*s**4*y**4 - + 3*s**3*y**5 - y**6), [s, s**4 - x*y])) + raises(NotImplementedError, + lambda: unrad(root(x, 3) + root(y, 3) + root(x*y, 5))) + + # Test unrad with an Equality + eq = Eq(-x**(S(1)/5) + x**(S(1)/3), -3**(S(1)/3) - (-1)**(S(3)/5)*3**(S(1)/5)) + assert check(unrad(eq), + (-s**5 + s**3 - 3**(S(1)/3) - (-1)**(S(3)/5)*3**(S(1)/5), [s, s**15 - x])) + + # make sure buried radicals are exposed + s = sqrt(x) - 1 + assert unrad(s**2 - s**3) == (x**3 - 6*x**2 + 9*x - 4, []) + # make sure numerators which are already polynomial are rejected + assert unrad((x/(x + 1) + 3)**(-2), x) is None + + # https://github.com/sympy/sympy/issues/23707 + eq = sqrt(x - y)*exp(t*sqrt(x - y)) - exp(t*sqrt(x - y)) + assert solve(eq, y) == [x - 1] + assert unrad(eq) is None + + +@slow +def test_unrad_slow(): + # this has roots with multiplicity > 1; there should be no + # repeats in roots obtained, however + eq = (sqrt(1 + sqrt(1 - 4*x**2)) - x*(1 + sqrt(1 + 2*sqrt(1 - 4*x**2)))) + assert solve(eq) == [S.Half] + + +@XFAIL +def test_unrad_fail(): + # this only works if we check real_root(eq.subs(x, Rational(1, 3))) + # but checksol doesn't work like that + assert solve(root(x**3 - 3*x**2, 3) + 1 - x) == [Rational(1, 3)] + assert solve(root(x + 1, 3) + root(x**2 - 2, 5) + 1) == [ + -1, -1 + CRootOf(x**5 + x**4 + 5*x**3 + 8*x**2 + 10*x + 5, 0)**3] + + +def test_checksol(): + x, y, r, t = symbols('x, y, r, t') + eq = r - x**2 - y**2 + dict_var_soln = {y: - sqrt(r) / sqrt(tan(t)**2 + 1), + x: -sqrt(r)*tan(t)/sqrt(tan(t)**2 + 1)} + assert checksol(eq, dict_var_soln) == True + assert checksol(Eq(x, False), {x: False}) is True + assert checksol(Ne(x, False), {x: False}) is False + assert checksol(Eq(x < 1, True), {x: 0}) is True + assert checksol(Eq(x < 1, True), {x: 1}) is False + assert checksol(Eq(x < 1, False), {x: 1}) is True + assert checksol(Eq(x < 1, False), {x: 0}) is False + assert checksol(Eq(x + 1, x**2 + 1), {x: 1}) is True + assert checksol([x - 1, x**2 - 1], x, 1) is True + assert checksol([x - 1, x**2 - 2], x, 1) is False + assert checksol(Poly(x**2 - 1), x, 1) is True + assert checksol(0, {}) is True + assert checksol([1e-10, x - 2], x, 2) is False + assert checksol([0.5, 0, x], x, 0) is False + assert checksol(y, x, 2) is False + assert checksol(x+1e-10, x, 0, numerical=True) is True + assert checksol(x+1e-10, x, 0, numerical=False) is False + assert checksol(exp(92*x), {x: log(sqrt(2)/2)}) is False + assert checksol(exp(92*x), {x: log(sqrt(2)/2) + I*pi}) is False + assert checksol(1/x**5, x, 1000) is False + raises(ValueError, lambda: checksol(x, 1)) + raises(ValueError, lambda: checksol([], x, 1)) + + +def test__invert(): + assert _invert(x - 2) == (2, x) + assert _invert(2) == (2, 0) + assert _invert(exp(1/x) - 3, x) == (1/log(3), x) + assert _invert(exp(1/x + a/x) - 3, x) == ((a + 1)/log(3), x) + assert _invert(a, x) == (a, 0) + + +def test_issue_4463(): + assert solve(-a*x + 2*x*log(x), x) == [exp(a/2)] + assert solve(x**x) == [] + assert solve(x**x - 2) == [exp(LambertW(log(2)))] + assert solve(((x - 3)*(x - 2))**((x - 3)*(x - 4))) == [2] + +@slow +def test_issue_5114_solvers(): + a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r = symbols('a:r') + + # there is no 'a' in the equation set but this is how the + # problem was originally posed + syms = a, b, c, f, h, k, n + eqs = [b + r/d - c/d, + c*(1/d + 1/e + 1/g) - f/g - r/d, + f*(1/g + 1/i + 1/j) - c/g - h/i, + h*(1/i + 1/l + 1/m) - f/i - k/m, + k*(1/m + 1/o + 1/p) - h/m - n/p, + n*(1/p + 1/q) - k/p] + assert len(solve(eqs, syms, manual=True, check=False, simplify=False)) == 1 + + +def test_issue_5849(): + # + # XXX: This system does not have a solution for most values of the + # parameters. Generally solve returns the empty set for systems that are + # generically inconsistent. + # + I1, I2, I3, I4, I5, I6 = symbols('I1:7') + dI1, dI4, dQ2, dQ4, Q2, Q4 = symbols('dI1,dI4,dQ2,dQ4,Q2,Q4') + + e = ( + I1 - I2 - I3, + I3 - I4 - I5, + I4 + I5 - I6, + -I1 + I2 + I6, + -2*I1 - 2*I3 - 2*I5 - 3*I6 - dI1/2 + 12, + -I4 + dQ4, + -I2 + dQ2, + 2*I3 + 2*I5 + 3*I6 - Q2, + I4 - 2*I5 + 2*Q4 + dI4 + ) + + ans = [{ + I1: I2 + I3, + dI1: -4*I2 - 8*I3 - 4*I5 - 6*I6 + 24, + I4: I3 - I5, + dQ4: I3 - I5, + Q4: -I3/2 + 3*I5/2 - dI4/2, + dQ2: I2, + Q2: 2*I3 + 2*I5 + 3*I6}] + + v = I1, I4, Q2, Q4, dI1, dI4, dQ2, dQ4 + assert solve(e, *v, manual=True, check=False, dict=True) == ans + assert solve(e, *v, manual=True, check=False) == [ + tuple([a.get(i, i) for i in v]) for a in ans] + assert solve(e, *v, manual=True) == [] + assert solve(e, *v) == [] + + # the matrix solver (tested below) doesn't like this because it produces + # a zero row in the matrix. Is this related to issue 4551? + assert [ei.subs( + ans[0]) for ei in e] == [0, 0, I3 - I6, -I3 + I6, 0, 0, 0, 0, 0] + + +def test_issue_5849_matrix(): + '''Same as test_issue_5849 but solved with the matrix solver. + + A solution only exists if I3 == I6 which is not generically true, + but `solve` does not return conditions under which the solution is + valid, only a solution that is canonical and consistent with the input. + ''' + # a simple example with the same issue + # assert solve([x+y+z, x+y], [x, y]) == {x: y} + # the longer example + I1, I2, I3, I4, I5, I6 = symbols('I1:7') + dI1, dI4, dQ2, dQ4, Q2, Q4 = symbols('dI1,dI4,dQ2,dQ4,Q2,Q4') + + e = ( + I1 - I2 - I3, + I3 - I4 - I5, + I4 + I5 - I6, + -I1 + I2 + I6, + -2*I1 - 2*I3 - 2*I5 - 3*I6 - dI1/2 + 12, + -I4 + dQ4, + -I2 + dQ2, + 2*I3 + 2*I5 + 3*I6 - Q2, + I4 - 2*I5 + 2*Q4 + dI4 + ) + assert solve(e, I1, I4, Q2, Q4, dI1, dI4, dQ2, dQ4) == [] + + +def test_issue_21882(): + + a, b, c, d, f, g, k = unknowns = symbols('a, b, c, d, f, g, k') + + equations = [ + -k*a + b + 5*f/6 + 2*c/9 + 5*d/6 + 4*a/3, + -k*f + 4*f/3 + d/2, + -k*d + f/6 + d, + 13*b/18 + 13*c/18 + 13*a/18, + -k*c + b/2 + 20*c/9 + a, + -k*b + b + c/18 + a/6, + 5*b/3 + c/3 + a, + 2*b/3 + 2*c + 4*a/3, + -g, + ] + + answer = [ + {a: 0, f: 0, b: 0, d: 0, c: 0, g: 0}, + {a: 0, f: -d, b: 0, k: S(5)/6, c: 0, g: 0}, + {a: -2*c, f: 0, b: c, d: 0, k: S(13)/18, g: 0}] + # but not {a: 0, f: 0, b: 0, k: S(3)/2, c: 0, d: 0, g: 0} + # since this is already covered by the first solution + got = solve(equations, unknowns, dict=True) + assert got == answer, (got,answer) + + +def test_issue_5901(): + f, g, h = map(Function, 'fgh') + a = Symbol('a') + D = Derivative(f(x), x) + G = Derivative(g(a), a) + assert solve(f(x) + f(x).diff(x), f(x)) == \ + [-D] + assert solve(f(x) - 3, f(x)) == \ + [3] + assert solve(f(x) - 3*f(x).diff(x), f(x)) == \ + [3*D] + assert solve([f(x) - 3*f(x).diff(x)], f(x)) == \ + {f(x): 3*D} + assert solve([f(x) - 3*f(x).diff(x), f(x)**2 - y + 4], f(x), y) == \ + [(3*D, 9*D**2 + 4)] + assert solve(-f(a)**2*g(a)**2 + f(a)**2*h(a)**2 + g(a).diff(a), + h(a), g(a), set=True) == \ + ([h(a), g(a)], { + (-sqrt(f(a)**2*g(a)**2 - G)/f(a), g(a)), + (sqrt(f(a)**2*g(a)**2 - G)/f(a), g(a))}), solve(-f(a)**2*g(a)**2 + f(a)**2*h(a)**2 + g(a).diff(a), + h(a), g(a), set=True) + args = [[f(x).diff(x, 2)*(f(x) + g(x)), 2 - g(x)**2], f(x), g(x)] + assert solve(*args, set=True)[1] == \ + {(-sqrt(2), sqrt(2)), (sqrt(2), -sqrt(2))} + eqs = [f(x)**2 + g(x) - 2*f(x).diff(x), g(x)**2 - 4] + assert solve(eqs, f(x), g(x), set=True) == \ + ([f(x), g(x)], { + (-sqrt(2*D - 2), S(2)), + (sqrt(2*D - 2), S(2)), + (-sqrt(2*D + 2), -S(2)), + (sqrt(2*D + 2), -S(2))}) + + # the underlying problem was in solve_linear that was not masking off + # anything but a Mul or Add; it now raises an error if it gets anything + # but a symbol and solve handles the substitutions necessary so solve_linear + # won't make this error + raises( + ValueError, lambda: solve_linear(f(x) + f(x).diff(x), symbols=[f(x)])) + assert solve_linear(f(x) + f(x).diff(x), symbols=[x]) == \ + (f(x) + Derivative(f(x), x), 1) + assert solve_linear(f(x) + Integral(x, (x, y)), symbols=[x]) == \ + (f(x) + Integral(x, (x, y)), 1) + assert solve_linear(f(x) + Integral(x, (x, y)) + x, symbols=[x]) == \ + (x + f(x) + Integral(x, (x, y)), 1) + assert solve_linear(f(y) + Integral(x, (x, y)) + x, symbols=[x]) == \ + (x, -f(y) - Integral(x, (x, y))) + assert solve_linear(x - f(x)/a + (f(x) - 1)/a, symbols=[x]) == \ + (x, 1/a) + assert solve_linear(x + Derivative(2*x, x)) == \ + (x, -2) + assert solve_linear(x + Integral(x, y), symbols=[x]) == \ + (x, 0) + assert solve_linear(x + Integral(x, y) - 2, symbols=[x]) == \ + (x, 2/(y + 1)) + + assert set(solve(x + exp(x)**2, exp(x))) == \ + {-sqrt(-x), sqrt(-x)} + assert solve(x + exp(x), x, implicit=True) == \ + [-exp(x)] + assert solve(cos(x) - sin(x), x, implicit=True) == [] + assert solve(x - sin(x), x, implicit=True) == \ + [sin(x)] + assert solve(x**2 + x - 3, x, implicit=True) == \ + [-x**2 + 3] + assert solve(x**2 + x - 3, x**2, implicit=True) == \ + [-x + 3] + + +def test_issue_5912(): + assert set(solve(x**2 - x - 0.1, rational=True)) == \ + {S.Half + sqrt(35)/10, -sqrt(35)/10 + S.Half} + ans = solve(x**2 - x - 0.1, rational=False) + assert len(ans) == 2 and all(a.is_Number for a in ans) + ans = solve(x**2 - x - 0.1) + assert len(ans) == 2 and all(a.is_Number for a in ans) + + +def test_float_handling(): + def test(e1, e2): + return len(e1.atoms(Float)) == len(e2.atoms(Float)) + assert solve(x - 0.5, rational=True)[0].is_Rational + assert solve(x - 0.5, rational=False)[0].is_Float + assert solve(x - S.Half, rational=False)[0].is_Rational + assert solve(x - 0.5, rational=None)[0].is_Float + assert solve(x - S.Half, rational=None)[0].is_Rational + assert test(nfloat(1 + 2*x), 1.0 + 2.0*x) + for contain in [list, tuple, set]: + ans = nfloat(contain([1 + 2*x])) + assert type(ans) is contain and test(list(ans)[0], 1.0 + 2.0*x) + k, v = list(nfloat({2*x: [1 + 2*x]}).items())[0] + assert test(k, 2*x) and test(v[0], 1.0 + 2.0*x) + assert test(nfloat(cos(2*x)), cos(2.0*x)) + assert test(nfloat(3*x**2), 3.0*x**2) + assert test(nfloat(3*x**2, exponent=True), 3.0*x**2.0) + assert test(nfloat(exp(2*x)), exp(2.0*x)) + assert test(nfloat(x/3), x/3.0) + assert test(nfloat(x**4 + 2*x + cos(Rational(1, 3)) + 1), + x**4 + 2.0*x + 1.94495694631474) + # don't call nfloat if there is no solution + tot = 100 + c + z + t + assert solve(((.7 + c)/tot - .6, (.2 + z)/tot - .3, t/tot - .1)) == [] + + +def test_check_assumptions(): + x = symbols('x', positive=True) + assert solve(x**2 - 1) == [1] + + +def test_issue_6056(): + assert solve(tanh(x + 3)*tanh(x - 3) - 1) == [] + assert solve(tanh(x - 1)*tanh(x + 1) + 1) == \ + [I*pi*Rational(-3, 4), -I*pi/4, I*pi/4, I*pi*Rational(3, 4)] + assert solve((tanh(x + 3)*tanh(x - 3) + 1)**2) == \ + [I*pi*Rational(-3, 4), -I*pi/4, I*pi/4, I*pi*Rational(3, 4)] + + +def test_issue_5673(): + eq = -x + exp(exp(LambertW(log(x)))*LambertW(log(x))) + assert checksol(eq, x, 2) is True + assert checksol(eq, x, 2, numerical=False) is None + + +def test_exclude(): + R, C, Ri, Vout, V1, Vminus, Vplus, s = \ + symbols('R, C, Ri, Vout, V1, Vminus, Vplus, s') + Rf = symbols('Rf', positive=True) # to eliminate Rf = 0 soln + eqs = [C*V1*s + Vplus*(-2*C*s - 1/R), + Vminus*(-1/Ri - 1/Rf) + Vout/Rf, + C*Vplus*s + V1*(-C*s - 1/R) + Vout/R, + -Vminus + Vplus] + assert solve(eqs, exclude=s*C*R) == [ + { + Rf: Ri*(C*R*s + 1)**2/(C*R*s), + Vminus: Vplus, + V1: 2*Vplus + Vplus/(C*R*s), + Vout: C*R*Vplus*s + 3*Vplus + Vplus/(C*R*s)}, + { + Vplus: 0, + Vminus: 0, + V1: 0, + Vout: 0}, + ] + + # TODO: Investigate why currently solution [0] is preferred over [1]. + assert solve(eqs, exclude=[Vplus, s, C]) in [[{ + Vminus: Vplus, + V1: Vout/2 + Vplus/2 + sqrt((Vout - 5*Vplus)*(Vout - Vplus))/2, + R: (Vout - 3*Vplus - sqrt(Vout**2 - 6*Vout*Vplus + 5*Vplus**2))/(2*C*Vplus*s), + Rf: Ri*(Vout - Vplus)/Vplus, + }, { + Vminus: Vplus, + V1: Vout/2 + Vplus/2 - sqrt((Vout - 5*Vplus)*(Vout - Vplus))/2, + R: (Vout - 3*Vplus + sqrt(Vout**2 - 6*Vout*Vplus + 5*Vplus**2))/(2*C*Vplus*s), + Rf: Ri*(Vout - Vplus)/Vplus, + }], [{ + Vminus: Vplus, + Vout: (V1**2 - V1*Vplus - Vplus**2)/(V1 - 2*Vplus), + Rf: Ri*(V1 - Vplus)**2/(Vplus*(V1 - 2*Vplus)), + R: Vplus/(C*s*(V1 - 2*Vplus)), + }]] + + +def test_high_order_roots(): + s = x**5 + 4*x**3 + 3*x**2 + Rational(7, 4) + assert set(solve(s)) == set(Poly(s*4, domain='ZZ').all_roots()) + + +def test_minsolve_linear_system(): + pqt = {"quick": True, "particular": True} + pqf = {"quick": False, "particular": True} + assert solve([x + y - 5, 2*x - y - 1], **pqt) == {x: 2, y: 3} + assert solve([x + y - 5, 2*x - y - 1], **pqf) == {x: 2, y: 3} + def count(dic): + return len([x for x in dic.values() if x == 0]) + assert count(solve([x + y + z, y + z + a + t], **pqt)) == 3 + assert count(solve([x + y + z, y + z + a + t], **pqf)) == 3 + assert count(solve([x + y + z, y + z + a], **pqt)) == 1 + assert count(solve([x + y + z, y + z + a], **pqf)) == 2 + # issue 22718 + A = Matrix([ + [ 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0], + [ 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, -1, -1, 0, 0], + [-1, -1, 0, 0, -1, 0, 0, 0, 0, 0, 1, 1, 0, 1], + [ 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, -1, 0, -1, 0], + [-1, 0, -1, 0, 0, -1, 0, 0, 0, 0, 1, 0, 1, 1], + [-1, 0, 0, -1, 0, 0, -1, 0, 0, 0, -1, 0, 0, -1], + [ 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, -1, -1, 0], + [ 0, -1, -1, 0, 0, 0, 0, -1, 0, 0, 0, 1, 1, 1], + [ 0, -1, 0, -1, 0, 0, 0, 0, -1, 0, 0, -1, 0, -1], + [ 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, -1, -1], + [ 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [ 0, 0, 0, 0, -1, -1, 0, -1, 0, 0, 0, 0, 0, 0]]) + v = Matrix(symbols("v:14", integer=True)) + B = Matrix([[2], [-2], [0], [0], [0], [0], [0], [0], [0], + [0], [0], [0]]) + eqs = A@v-B + assert solve(eqs) == [] + assert solve(eqs, particular=True) == [] # assumption violated + assert all(v for v in solve([x + y + z, y + z + a]).values()) + for _q in (True, False): + assert not all(v for v in solve( + [x + y + z, y + z + a], quick=_q, + particular=True).values()) + # raise error if quick used w/o particular=True + raises(ValueError, lambda: solve([x + 1], quick=_q)) + raises(ValueError, lambda: solve([x + 1], quick=_q, particular=False)) + # and give a good error message if someone tries to use + # particular with a single equation + raises(ValueError, lambda: solve(x + 1, particular=True)) + + +def test_real_roots(): + # cf. issue 6650 + x = Symbol('x', real=True) + assert len(solve(x**5 + x**3 + 1)) == 1 + + +def test_issue_6528(): + eqs = [ + 327600995*x**2 - 37869137*x + 1809975124*y**2 - 9998905626, + 895613949*x**2 - 273830224*x*y + 530506983*y**2 - 10000000000] + # two expressions encountered are > 1400 ops long so if this hangs + # it is likely because simplification is being done + assert len(solve(eqs, y, x, check=False)) == 4 + + +def test_overdetermined(): + x = symbols('x', real=True) + eqs = [Abs(4*x - 7) - 5, Abs(3 - 8*x) - 1] + assert solve(eqs, x) == [(S.Half,)] + assert solve(eqs, x, manual=True) == [(S.Half,)] + assert solve(eqs, x, manual=True, check=False) == [(S.Half,), (S(3),)] + + +def test_issue_6605(): + x = symbols('x') + assert solve(4**(x/2) - 2**(x/3)) == [0, 3*I*pi/log(2)] + # while the first one passed, this one failed + x = symbols('x', real=True) + assert solve(5**(x/2) - 2**(x/3)) == [0] + b = sqrt(6)*sqrt(log(2))/sqrt(log(5)) + assert solve(5**(x/2) - 2**(3/x)) == [-b, b] + + +def test__ispow(): + assert _ispow(x**2) + assert not _ispow(x) + assert not _ispow(True) + + +def test_issue_6644(): + eq = -sqrt((m - q)**2 + (-m/(2*q) + S.Half)**2) + sqrt((-m**2/2 - sqrt( + 4*m**4 - 4*m**2 + 8*m + 1)/4 - Rational(1, 4))**2 + (m**2/2 - m - sqrt( + 4*m**4 - 4*m**2 + 8*m + 1)/4 - Rational(1, 4))**2) + sol = solve(eq, q, simplify=False, check=False) + assert len(sol) == 5 + + +def test_issue_6752(): + assert solve([a**2 + a, a - b], [a, b]) == [(-1, -1), (0, 0)] + assert solve([a**2 + a*c, a - b], [a, b]) == [(0, 0), (-c, -c)] + + +def test_issue_6792(): + assert solve(x*(x - 1)**2*(x + 1)*(x**6 - x + 1)) == [ + -1, 0, 1, CRootOf(x**6 - x + 1, 0), CRootOf(x**6 - x + 1, 1), + CRootOf(x**6 - x + 1, 2), CRootOf(x**6 - x + 1, 3), + CRootOf(x**6 - x + 1, 4), CRootOf(x**6 - x + 1, 5)] + + +def test_issues_6819_6820_6821_6248_8692_25777_25779(): + # issue 6821 + x, y = symbols('x y', real=True) + assert solve(abs(x + 3) - 2*abs(x - 3)) == [1, 9] + assert solve([abs(x) - 2, arg(x) - pi], x) == [(-2,)] + assert set(solve(abs(x - 7) - 8)) == {-S.One, S(15)} + + # issue 8692 + assert solve(Eq(Abs(x + 1) + Abs(x**2 - 7), 9), x) == [ + Rational(-1, 2) + sqrt(61)/2, -sqrt(69)/2 + S.Half] + + # issue 7145 + assert solve(2*abs(x) - abs(x - 1)) == [-1, Rational(1, 3)] + + # 25777 + assert solve(abs(x**3 + x + 2)/(x + 1)) == [] + + # 25779 + assert solve(abs(x)) == [0] + assert solve(Eq(abs(x**2 - 2*x), 4), x) == [ + 1 - sqrt(5), 1 + sqrt(5)] + nn = symbols('nn', nonnegative=True) + assert solve(abs(sqrt(nn))) == [0] + nz = symbols('nz', nonzero=True) + assert solve(Eq(Abs(4 + 1 / (4*nz)), 0)) == [-Rational(1, 16)] + + x = symbols('x') + assert solve([re(x) - 1, im(x) - 2], x) == [ + {x: 1 + 2*I, re(x): 1, im(x): 2}] + + # check for 'dict' handling of solution + eq = sqrt(re(x)**2 + im(x)**2) - 3 + assert solve(eq) == solve(eq, x) + + i = symbols('i', imaginary=True) + assert solve(abs(i) - 3) == [-3*I, 3*I] + raises(NotImplementedError, lambda: solve(abs(x) - 3)) + + w = symbols('w', integer=True) + assert solve(2*x**w - 4*y**w, w) == solve((x/y)**w - 2, w) + + x, y = symbols('x y', real=True) + assert solve(x + y*I + 3) == {y: 0, x: -3} + # issue 2642 + assert solve(x*(1 + I)) == [0] + + x, y = symbols('x y', imaginary=True) + assert solve(x + y*I + 3 + 2*I) == {x: -2*I, y: 3*I} + + x = symbols('x', real=True) + assert solve(x + y + 3 + 2*I) == {x: -3, y: -2*I} + + # issue 6248 + f = Function('f') + assert solve(f(x + 1) - f(2*x - 1)) == [2] + assert solve(log(x + 1) - log(2*x - 1)) == [2] + + x = symbols('x') + assert solve(2**x + 4**x) == [I*pi/log(2)] + +def test_issue_17638(): + + assert solve(((2-exp(2*x))*exp(x))/(exp(2*x)+2)**2 > 0, x) == (-oo < x) & (x < log(2)/2) + assert solve(((2-exp(2*x)+2)*exp(x+2))/(exp(x)+2)**2 > 0, x) == (-oo < x) & (x < log(4)/2) + assert solve((exp(x)+2+x**2)*exp(2*x+2)/(exp(x)+2)**2 > 0, x) == (-oo < x) & (x < oo) + + + +def test_issue_14607(): + # issue 14607 + s, tau_c, tau_1, tau_2, phi, K = symbols( + 's, tau_c, tau_1, tau_2, phi, K') + + target = (s**2*tau_1*tau_2 + s*tau_1 + s*tau_2 + 1)/(K*s*(-phi + tau_c)) + + K_C, tau_I, tau_D = symbols('K_C, tau_I, tau_D', + positive=True, nonzero=True) + PID = K_C*(1 + 1/(tau_I*s) + tau_D*s) + + eq = (target - PID).together() + eq *= denom(eq).simplify() + eq = Poly(eq, s) + c = eq.coeffs() + + vars = [K_C, tau_I, tau_D] + s = solve(c, vars, dict=True) + + assert len(s) == 1 + + knownsolution = {K_C: -(tau_1 + tau_2)/(K*(phi - tau_c)), + tau_I: tau_1 + tau_2, + tau_D: tau_1*tau_2/(tau_1 + tau_2)} + + for var in vars: + assert s[0][var].simplify() == knownsolution[var].simplify() + + +def test_lambert_multivariate(): + from sympy.abc import x, y + assert _filtered_gens(Poly(x + 1/x + exp(x) + y), x) == {x, exp(x)} + assert _lambert(x, x) == [] + assert solve((x**2 - 2*x + 1).subs(x, log(x) + 3*x)) == [LambertW(3*S.Exp1)/3] + assert solve((x**2 - 2*x + 1).subs(x, (log(x) + 3*x)**2 - 1)) == \ + [LambertW(3*exp(-sqrt(2)))/3, LambertW(3*exp(sqrt(2)))/3] + assert solve((x**2 - 2*x - 2).subs(x, log(x) + 3*x)) == \ + [LambertW(3*exp(1 - sqrt(3)))/3, LambertW(3*exp(1 + sqrt(3)))/3] + eq = (x*exp(x) - 3).subs(x, x*exp(x)) + assert solve(eq) == [LambertW(3*exp(-LambertW(3)))] + # coverage test + raises(NotImplementedError, lambda: solve(x - sin(x)*log(y - x), x)) + ans = [3, -3*LambertW(-log(3)/3)/log(3)] # 3 and 2.478... + assert solve(x**3 - 3**x, x) == ans + assert set(solve(3*log(x) - x*log(3))) == set(ans) + assert solve(LambertW(2*x) - y, x) == [y*exp(y)/2] + + +@XFAIL +def test_other_lambert(): + assert solve(3*sin(x) - x*sin(3), x) == [3] + assert set(solve(x**a - a**x), x) == { + a, -a*LambertW(-log(a)/a)/log(a)} + + +@slow +def test_lambert_bivariate(): + # tests passing current implementation + assert solve((x**2 + x)*exp(x**2 + x) - 1) == [ + Rational(-1, 2) + sqrt(1 + 4*LambertW(1))/2, + Rational(-1, 2) - sqrt(1 + 4*LambertW(1))/2] + assert solve((x**2 + x)*exp((x**2 + x)*2) - 1) == [ + Rational(-1, 2) + sqrt(1 + 2*LambertW(2))/2, + Rational(-1, 2) - sqrt(1 + 2*LambertW(2))/2] + assert solve(a/x + exp(x/2), x) == [2*LambertW(-a/2)] + assert solve((a/x + exp(x/2)).diff(x), x) == \ + [4*LambertW(-sqrt(2)*sqrt(a)/4), 4*LambertW(sqrt(2)*sqrt(a)/4)] + assert solve((1/x + exp(x/2)).diff(x), x) == \ + [4*LambertW(-sqrt(2)/4), + 4*LambertW(sqrt(2)/4), # nsimplifies as 2*2**(141/299)*3**(206/299)*5**(205/299)*7**(37/299)/21 + 4*LambertW(-sqrt(2)/4, -1)] + assert solve(x*log(x) + 3*x + 1, x) == \ + [exp(-3 + LambertW(-exp(3)))] + assert solve(-x**2 + 2**x, x) == [2, 4, -2*LambertW(log(2)/2)/log(2)] + assert solve(x**2 - 2**x, x) == [2, 4, -2*LambertW(log(2)/2)/log(2)] + ans = solve(3*x + 5 + 2**(-5*x + 3), x) + assert len(ans) == 1 and ans[0].expand() == \ + Rational(-5, 3) + LambertW(-10240*root(2, 3)*log(2)/3)/(5*log(2)) + assert solve(5*x - 1 + 3*exp(2 - 7*x), x) == \ + [Rational(1, 5) + LambertW(-21*exp(Rational(3, 5))/5)/7] + assert solve((log(x) + x).subs(x, x**2 + 1)) == [ + -I*sqrt(-LambertW(1) + 1), sqrt(-1 + LambertW(1))] + # check collection + ax = a**(3*x + 5) + ans = solve(3*log(ax) + b*log(ax) + ax, x) + x0 = 1/log(a) + x1 = sqrt(3)*I + x2 = b + 3 + x3 = x2*LambertW(1/x2)/a**5 + x4 = x3**Rational(1, 3)/2 + assert ans == [ + x0*log(x4*(-x1 - 1)), + x0*log(x4*(x1 - 1)), + x0*log(x3)/3] + x1 = LambertW(Rational(1, 3)) + x2 = a**(-5) + x3 = -3**Rational(1, 3) + x4 = 3**Rational(5, 6)*I + x5 = x1**Rational(1, 3)*x2**Rational(1, 3)/2 + ans = solve(3*log(ax) + ax, x) + assert ans == [ + x0*log(3*x1*x2)/3, + x0*log(x5*(x3 - x4)), + x0*log(x5*(x3 + x4))] + # coverage + p = symbols('p', positive=True) + eq = 4*2**(2*p + 3) - 2*p - 3 + assert _solve_lambert(eq, p, _filtered_gens(Poly(eq), p)) == [ + Rational(-3, 2) - LambertW(-4*log(2))/(2*log(2))] + assert set(solve(3**cos(x) - cos(x)**3)) == { + acos(3), acos(-3*LambertW(-log(3)/3)/log(3))} + # should give only one solution after using `uniq` + assert solve(2*log(x) - 2*log(z) + log(z + log(x) + log(z)), x) == [ + exp(-z + LambertW(2*z**4*exp(2*z))/2)/z] + # cases when p != S.One + # issue 4271 + ans = solve((a/x + exp(x/2)).diff(x, 2), x) + x0 = (-a)**Rational(1, 3) + x1 = sqrt(3)*I + x2 = x0/6 + assert ans == [ + 6*LambertW(x0/3), + 6*LambertW(x2*(-x1 - 1)), + 6*LambertW(x2*(x1 - 1))] + assert solve((1/x + exp(x/2)).diff(x, 2), x) == \ + [6*LambertW(Rational(-1, 3)), 6*LambertW(Rational(1, 6) - sqrt(3)*I/6), \ + 6*LambertW(Rational(1, 6) + sqrt(3)*I/6), 6*LambertW(Rational(-1, 3), -1)] + assert solve(x**2 - y**2/exp(x), x, y, dict=True) == \ + [{x: 2*LambertW(-y/2)}, {x: 2*LambertW(y/2)}] + # this is slow but not exceedingly slow + assert solve((x**3)**(x/2) + pi/2, x) == [ + exp(LambertW(-2*log(2)/3 + 2*log(pi)/3 + I*pi*Rational(2, 3)))] + + # issue 23253 + assert solve((1/log(sqrt(x) + 2)**2 - 1/x)) == [ + (LambertW(-exp(-2), -1) + 2)**2] + assert solve((1/log(1/sqrt(x) + 2)**2 - x)) == [ + (LambertW(-exp(-2), -1) + 2)**-2] + assert solve((1/log(x**2 + 2)**2 - x**-4)) == [ + -I*sqrt(2 - LambertW(exp(2))), + -I*sqrt(LambertW(-exp(-2)) + 2), + sqrt(-2 - LambertW(-exp(-2))), + sqrt(-2 + LambertW(exp(2))), + -sqrt(-2 - LambertW(-exp(-2), -1)), + sqrt(-2 - LambertW(-exp(-2), -1))] + + +def test_rewrite_trig(): + assert solve(sin(x) + tan(x)) == [0, -pi, pi, 2*pi] + assert solve(sin(x) + sec(x)) == [ + -2*atan(Rational(-1, 2) + sqrt(2)*sqrt(1 - sqrt(3)*I)/2 + sqrt(3)*I/2), + 2*atan(S.Half - sqrt(2)*sqrt(1 + sqrt(3)*I)/2 + sqrt(3)*I/2), 2*atan(S.Half + + sqrt(2)*sqrt(1 + sqrt(3)*I)/2 + sqrt(3)*I/2), 2*atan(S.Half - + sqrt(3)*I/2 + sqrt(2)*sqrt(1 - sqrt(3)*I)/2)] + assert solve(sinh(x) + tanh(x)) == [0, I*pi] + + # issue 6157 + assert solve(2*sin(x) - cos(x), x) == [atan(S.Half)] + + +@XFAIL +def test_rewrite_trigh(): + # if this import passes then the test below should also pass + from sympy.functions.elementary.hyperbolic import sech + assert solve(sinh(x) + sech(x)) == [ + 2*atanh(Rational(-1, 2) + sqrt(5)/2 - sqrt(-2*sqrt(5) + 2)/2), + 2*atanh(Rational(-1, 2) + sqrt(5)/2 + sqrt(-2*sqrt(5) + 2)/2), + 2*atanh(-sqrt(5)/2 - S.Half + sqrt(2 + 2*sqrt(5))/2), + 2*atanh(-sqrt(2 + 2*sqrt(5))/2 - sqrt(5)/2 - S.Half)] + + +def test_uselogcombine(): + eq = z - log(x) + log(y/(x*(-1 + y**2/x**2))) + assert solve(eq, x, force=True) == [-sqrt(y*(y - exp(z))), sqrt(y*(y - exp(z)))] + assert solve(log(x + 3) + log(1 + 3/x) - 3) in [ + [-3 + sqrt(-12 + exp(3))*exp(Rational(3, 2))/2 + exp(3)/2, + -sqrt(-12 + exp(3))*exp(Rational(3, 2))/2 - 3 + exp(3)/2], + [-3 + sqrt(-36 + (-exp(3) + 6)**2)/2 + exp(3)/2, + -3 - sqrt(-36 + (-exp(3) + 6)**2)/2 + exp(3)/2], + ] + assert solve(log(exp(2*x) + 1) + log(-tanh(x) + 1) - log(2)) == [] + + +def test_atan2(): + assert solve(atan2(x, 2) - pi/3, x) == [2*sqrt(3)] + + +def test_errorinverses(): + assert solve(erf(x) - y, x) == [erfinv(y)] + assert solve(erfinv(x) - y, x) == [erf(y)] + assert solve(erfc(x) - y, x) == [erfcinv(y)] + assert solve(erfcinv(x) - y, x) == [erfc(y)] + + +def test_issue_2725(): + R = Symbol('R') + eq = sqrt(2)*R*sqrt(1/(R + 1)) + (R + 1)*(sqrt(2)*sqrt(1/(R + 1)) - 1) + sol = solve(eq, R, set=True)[1] + assert sol == {(Rational(5, 3) + (Rational(-1, 2) - sqrt(3)*I/2)*(Rational(251, 27) + + sqrt(111)*I/9)**Rational(1, 3) + 40/(9*((Rational(-1, 2) - sqrt(3)*I/2)*(Rational(251, 27) + + sqrt(111)*I/9)**Rational(1, 3))),), (Rational(5, 3) + 40/(9*(Rational(251, 27) + + sqrt(111)*I/9)**Rational(1, 3)) + (Rational(251, 27) + sqrt(111)*I/9)**Rational(1, 3),)} + + +def test_issue_5114_6611(): + # See that it doesn't hang; this solves in about 2 seconds. + # Also check that the solution is relatively small. + # Note: the system in issue 6611 solves in about 5 seconds and has + # an op-count of 138336 (with simplify=False). + b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r = symbols('b:r') + eqs = Matrix([ + [b - c/d + r/d], [c*(1/g + 1/e + 1/d) - f/g - r/d], + [-c/g + f*(1/j + 1/i + 1/g) - h/i], [-f/i + h*(1/m + 1/l + 1/i) - k/m], + [-h/m + k*(1/p + 1/o + 1/m) - n/p], [-k/p + n*(1/q + 1/p)]]) + v = Matrix([f, h, k, n, b, c]) + ans = solve(list(eqs), list(v), simplify=False) + # If time is taken to simplify then then 2617 below becomes + # 1168 and the time is about 50 seconds instead of 2. + assert sum(s.count_ops() for s in ans.values()) <= 3270 + + +def test_det_quick(): + m = Matrix(3, 3, symbols('a:9')) + assert m.det() == det_quick(m) # calls det_perm + m[0, 0] = 1 + assert m.det() == det_quick(m) # calls det_minor + m = Matrix(3, 3, list(range(9))) + assert m.det() == det_quick(m) # defaults to .det() + # make sure they work with Sparse + s = SparseMatrix(2, 2, (1, 2, 1, 4)) + assert det_perm(s) == det_minor(s) == s.det() + + +def test_real_imag_splitting(): + a, b = symbols('a b', real=True) + assert solve(sqrt(a**2 + b**2) - 3, a) == \ + [-sqrt(-b**2 + 9), sqrt(-b**2 + 9)] + a, b = symbols('a b', imaginary=True) + assert solve(sqrt(a**2 + b**2) - 3, a) == [] + + +def test_issue_7110(): + y = -2*x**3 + 4*x**2 - 2*x + 5 + assert any(ask(Q.real(i)) for i in solve(y)) + + +def test_units(): + assert solve(1/x - 1/(2*cm)) == [2*cm] + + +def test_issue_7547(): + A, B, V = symbols('A,B,V') + eq1 = Eq(630.26*(V - 39.0)*V*(V + 39) - A + B, 0) + eq2 = Eq(B, 1.36*10**8*(V - 39)) + eq3 = Eq(A, 5.75*10**5*V*(V + 39.0)) + sol = Matrix(nsolve(Tuple(eq1, eq2, eq3), [A, B, V], (0, 0, 0))) + assert str(sol) == str(Matrix( + [['4442890172.68209'], + ['4289299466.1432'], + ['70.5389666628177']])) + + +def test_issue_7895(): + r = symbols('r', real=True) + assert solve(sqrt(r) - 2) == [4] + + +def test_issue_2777(): + # the equations represent two circles + x, y = symbols('x y', real=True) + e1, e2 = sqrt(x**2 + y**2) - 10, sqrt(y**2 + (-x + 10)**2) - 3 + a, b = Rational(191, 20), 3*sqrt(391)/20 + ans = [(a, -b), (a, b)] + assert solve((e1, e2), (x, y)) == ans + assert solve((e1, e2/(x - a)), (x, y)) == [] + # make the 2nd circle's radius be -3 + e2 += 6 + assert solve((e1, e2), (x, y)) == [] + assert solve((e1, e2), (x, y), check=False) == ans + + +def test_issue_7322(): + number = 5.62527e-35 + assert solve(x - number, x)[0] == number + + +def test_nsolve(): + raises(ValueError, lambda: nsolve(x, (-1, 1), method='bisect')) + raises(TypeError, lambda: nsolve((x - y + 3,x + y,z - y),(x,y,z),(-50,50))) + raises(TypeError, lambda: nsolve((x + y, x - y), (0, 1))) + raises(TypeError, lambda: nsolve(x < 0.5, x, 1)) + + +@slow +def test_high_order_multivariate(): + assert len(solve(a*x**3 - x + 1, x)) == 3 + assert len(solve(a*x**4 - x + 1, x)) == 4 + assert solve(a*x**5 - x + 1, x) == [] # incomplete solution allowed + raises(NotImplementedError, lambda: + solve(a*x**5 - x + 1, x, incomplete=False)) + + # result checking must always consider the denominator and CRootOf + # must be checked, too + d = x**5 - x + 1 + assert solve(d*(1 + 1/d)) == [CRootOf(d + 1, i) for i in range(5)] + d = x - 1 + assert solve(d*(2 + 1/d)) == [S.Half] + + +def test_base_0_exp_0(): + assert solve(0**x - 1) == [0] + assert solve(0**(x - 2) - 1) == [2] + assert solve(S('x*(1/x**0 - x)', evaluate=False)) == \ + [0, 1] + + +def test__simple_dens(): + assert _simple_dens(1/x**0, [x]) == set() + assert _simple_dens(1/x**y, [x]) == {x**y} + assert _simple_dens(1/root(x, 3), [x]) == {x} + + +def test_issue_8755(): + # This tests two things: that if full unrad is attempted and fails + # the solution should still be found; also it tests the use of + # keyword `composite`. + assert len(solve(sqrt(y)*x + x**3 - 1, x)) == 3 + assert len(solve(-512*y**3 + 1344*(x + 2)**Rational(1, 3)*y**2 - + 1176*(x + 2)**Rational(2, 3)*y - 169*x + 686, y, _unrad=False)) == 3 + + +@slow +def test_issue_8828(): + x1 = 0 + y1 = -620 + r1 = 920 + x2 = 126 + y2 = 276 + x3 = 51 + y3 = 205 + r3 = 104 + v = x, y, z + + f1 = (x - x1)**2 + (y - y1)**2 - (r1 - z)**2 + f2 = (x - x2)**2 + (y - y2)**2 - z**2 + f3 = (x - x3)**2 + (y - y3)**2 - (r3 - z)**2 + F = f1,f2,f3 + + g1 = sqrt((x - x1)**2 + (y - y1)**2) + z - r1 + g2 = f2 + g3 = sqrt((x - x3)**2 + (y - y3)**2) + z - r3 + G = g1,g2,g3 + + A = solve(F, v) + B = solve(G, v) + C = solve(G, v, manual=True) + + p, q, r = [{tuple(i.evalf(2) for i in j) for j in R} for R in [A, B, C]] + assert p == q == r + + +def test_issue_2840_8155(): + # with parameter-free solutions (i.e. no `n`), we want to avoid + # excessive periodic solutions + assert solve(sin(3*x) + sin(6*x)) == [0, -2*pi/9, 2*pi/9] + assert solve(sin(300*x) + sin(600*x)) == [0, -pi/450, pi/450] + assert solve(2*sin(x) - 2*sin(2*x)) == [0, -pi/3, pi/3] + + +def test_issue_9567(): + assert solve(1 + 1/(x - 1)) == [0] + + +def test_issue_11538(): + assert solve(x + E) == [-E] + assert solve(x**2 + E) == [-I*sqrt(E), I*sqrt(E)] + assert solve(x**3 + 2*E) == [ + -cbrt(2 * E), + cbrt(2)*cbrt(E)/2 - cbrt(2)*sqrt(3)*I*cbrt(E)/2, + cbrt(2)*cbrt(E)/2 + cbrt(2)*sqrt(3)*I*cbrt(E)/2] + assert solve([x + 4, y + E], x, y) == {x: -4, y: -E} + assert solve([x**2 + 4, y + E], x, y) == [ + (-2*I, -E), (2*I, -E)] + + e1 = x - y**3 + 4 + e2 = x + y + 4 + 4 * E + assert len(solve([e1, e2], x, y)) == 3 + + +@slow +def test_issue_12114(): + a, b, c, d, e, f, g = symbols('a,b,c,d,e,f,g') + terms = [1 + a*b + d*e, 1 + a*c + d*f, 1 + b*c + e*f, + g - a**2 - d**2, g - b**2 - e**2, g - c**2 - f**2] + sol = solve(terms, [a, b, c, d, e, f, g], dict=True) + s = sqrt(-f**2 - 1) + s2 = sqrt(2 - f**2) + s3 = sqrt(6 - 3*f**2) + s4 = sqrt(3)*f + s5 = sqrt(3)*s2 + assert sol == [ + {a: -s, b: -s, c: -s, d: f, e: f, g: -1}, + {a: s, b: s, c: s, d: f, e: f, g: -1}, + {a: -s4/2 - s2/2, b: s4/2 - s2/2, c: s2, + d: -f/2 + s3/2, e: -f/2 - s5/2, g: 2}, + {a: -s4/2 + s2/2, b: s4/2 + s2/2, c: -s2, + d: -f/2 - s3/2, e: -f/2 + s5/2, g: 2}, + {a: s4/2 - s2/2, b: -s4/2 - s2/2, c: s2, + d: -f/2 - s3/2, e: -f/2 + s5/2, g: 2}, + {a: s4/2 + s2/2, b: -s4/2 + s2/2, c: -s2, + d: -f/2 + s3/2, e: -f/2 - s5/2, g: 2}] + + +def test_inf(): + assert solve(1 - oo*x) == [] + assert solve(oo*x, x) == [] + assert solve(oo*x - oo, x) == [] + + +def test_issue_12448(): + f = Function('f') + fun = [f(i) for i in range(15)] + sym = symbols('x:15') + reps = dict(zip(fun, sym)) + + (x, y, z), c = sym[:3], sym[3:] + ssym = solve([c[4*i]*x + c[4*i + 1]*y + c[4*i + 2]*z + c[4*i + 3] + for i in range(3)], (x, y, z)) + + (x, y, z), c = fun[:3], fun[3:] + sfun = solve([c[4*i]*x + c[4*i + 1]*y + c[4*i + 2]*z + c[4*i + 3] + for i in range(3)], (x, y, z)) + + assert sfun[fun[0]].xreplace(reps).count_ops() == \ + ssym[sym[0]].count_ops() + + +def test_denoms(): + assert denoms(x/2 + 1/y) == {2, y} + assert denoms(x/2 + 1/y, y) == {y} + assert denoms(x/2 + 1/y, [y]) == {y} + assert denoms(1/x + 1/y + 1/z, [x, y]) == {x, y} + assert denoms(1/x + 1/y + 1/z, x, y) == {x, y} + assert denoms(1/x + 1/y + 1/z, {x, y}) == {x, y} + + +def test_issue_12476(): + x0, x1, x2, x3, x4, x5 = symbols('x0 x1 x2 x3 x4 x5') + eqns = [x0**2 - x0, x0*x1 - x1, x0*x2 - x2, x0*x3 - x3, x0*x4 - x4, x0*x5 - x5, + x0*x1 - x1, -x0/3 + x1**2 - 2*x2/3, x1*x2 - x1/3 - x2/3 - x3/3, + x1*x3 - x2/3 - x3/3 - x4/3, x1*x4 - 2*x3/3 - x5/3, x1*x5 - x4, x0*x2 - x2, + x1*x2 - x1/3 - x2/3 - x3/3, -x0/6 - x1/6 + x2**2 - x2/6 - x3/3 - x4/6, + -x1/6 + x2*x3 - x2/3 - x3/6 - x4/6 - x5/6, x2*x4 - x2/3 - x3/3 - x4/3, + x2*x5 - x3, x0*x3 - x3, x1*x3 - x2/3 - x3/3 - x4/3, + -x1/6 + x2*x3 - x2/3 - x3/6 - x4/6 - x5/6, + -x0/6 - x1/6 - x2/6 + x3**2 - x3/3 - x4/6, -x1/3 - x2/3 + x3*x4 - x3/3, + -x2 + x3*x5, x0*x4 - x4, x1*x4 - 2*x3/3 - x5/3, x2*x4 - x2/3 - x3/3 - x4/3, + -x1/3 - x2/3 + x3*x4 - x3/3, -x0/3 - 2*x2/3 + x4**2, -x1 + x4*x5, x0*x5 - x5, + x1*x5 - x4, x2*x5 - x3, -x2 + x3*x5, -x1 + x4*x5, -x0 + x5**2, x0 - 1] + sols = [{x0: 1, x3: Rational(1, 6), x2: Rational(1, 6), x4: Rational(-2, 3), x1: Rational(-2, 3), x5: 1}, + {x0: 1, x3: S.Half, x2: Rational(-1, 2), x4: 0, x1: 0, x5: -1}, + {x0: 1, x3: Rational(-1, 3), x2: Rational(-1, 3), x4: Rational(1, 3), x1: Rational(1, 3), x5: 1}, + {x0: 1, x3: 1, x2: 1, x4: 1, x1: 1, x5: 1}, + {x0: 1, x3: Rational(-1, 3), x2: Rational(1, 3), x4: sqrt(5)/3, x1: -sqrt(5)/3, x5: -1}, + {x0: 1, x3: Rational(-1, 3), x2: Rational(1, 3), x4: -sqrt(5)/3, x1: sqrt(5)/3, x5: -1}] + + assert solve(eqns) == sols + + +def test_issue_13849(): + t = symbols('t') + assert solve((t*(sqrt(5) + sqrt(2)) - sqrt(2), t), t) == [] + + +def test_issue_14860(): + from sympy.physics.units import newton, kilo + assert solve(8*kilo*newton + x + y, x) == [-8000*newton - y] + + +def test_issue_14721(): + k, h, a, b = symbols(':4') + assert solve([ + -1 + (-k + 1)**2/b**2 + (-h - 1)**2/a**2, + -1 + (-k + 1)**2/b**2 + (-h + 1)**2/a**2, + h, k + 2], h, k, a, b) == [ + (0, -2, -b*sqrt(1/(b**2 - 9)), b), + (0, -2, b*sqrt(1/(b**2 - 9)), b)] + assert solve([ + h, h/a + 1/b**2 - 2, -h/2 + 1/b**2 - 2], a, h, b) == [ + (a, 0, -sqrt(2)/2), (a, 0, sqrt(2)/2)] + assert solve((a + b**2 - 1, a + b**2 - 2)) == [] + + +def test_issue_14779(): + x = symbols('x', real=True) + assert solve(sqrt(x**4 - 130*x**2 + 1089) + sqrt(x**4 - 130*x**2 + + 3969) - 96*Abs(x)/x,x) == [sqrt(130)] + + +def test_issue_15307(): + assert solve((y - 2, Mul(x + 3,x - 2, evaluate=False))) == \ + [{x: -3, y: 2}, {x: 2, y: 2}] + assert solve((y - 2, Mul(3, x - 2, evaluate=False))) == \ + {x: 2, y: 2} + assert solve((y - 2, Add(x + 4, x - 2, evaluate=False))) == \ + {x: -1, y: 2} + eq1 = Eq(12513*x + 2*y - 219093, -5726*x - y) + eq2 = Eq(-2*x + 8, 2*x - 40) + assert solve([eq1, eq2]) == {x:12, y:75} + + +def test_issue_15415(): + assert solve(x - 3, x) == [3] + assert solve([x - 3], x) == {x:3} + assert solve(Eq(y + 3*x**2/2, y + 3*x), y) == [] + assert solve([Eq(y + 3*x**2/2, y + 3*x)], y) == [] + assert solve([Eq(y + 3*x**2/2, y + 3*x), Eq(x, 1)], y) == [] + + +@slow +def test_issue_15731(): + # f(x)**g(x)=c + assert solve(Eq((x**2 - 7*x + 11)**(x**2 - 13*x + 42), 1)) == [2, 3, 4, 5, 6, 7] + assert solve((x)**(x + 4) - 4) == [-2] + assert solve((-x)**(-x + 4) - 4) == [2] + assert solve((x**2 - 6)**(x**2 - 2) - 4) == [-2, 2] + assert solve((x**2 - 2*x - 1)**(x**2 - 3) - 1/(1 - 2*sqrt(2))) == [sqrt(2)] + assert solve(x**(x + S.Half) - 4*sqrt(2)) == [S(2)] + assert solve((x**2 + 1)**x - 25) == [2] + assert solve(x**(2/x) - 2) == [2, 4] + assert solve((x/2)**(2/x) - sqrt(2)) == [4, 8] + assert solve(x**(x + S.Half) - Rational(9, 4)) == [Rational(3, 2)] + # a**g(x)=c + assert solve((-sqrt(sqrt(2)))**x - 2) == [4, log(2)/(log(2**Rational(1, 4)) + I*pi)] + assert solve((sqrt(2))**x - sqrt(sqrt(2))) == [S.Half] + assert solve((-sqrt(2))**x + 2*(sqrt(2))) == [3, + (3*log(2)**2 + 4*pi**2 - 4*I*pi*log(2))/(log(2)**2 + 4*pi**2)] + assert solve((sqrt(2))**x - 2*(sqrt(2))) == [3] + assert solve(I**x + 1) == [2] + assert solve((1 + I)**x - 2*I) == [2] + assert solve((sqrt(2) + sqrt(3))**x - (2*sqrt(6) + 5)**Rational(1, 3)) == [Rational(2, 3)] + # bases of both sides are equal + b = Symbol('b') + assert solve(b**x - b**2, x) == [2] + assert solve(b**x - 1/b, x) == [-1] + assert solve(b**x - b, x) == [1] + b = Symbol('b', positive=True) + assert solve(b**x - b**2, x) == [2] + assert solve(b**x - 1/b, x) == [-1] + + +def test_issue_10933(): + assert solve(x**4 + y*(x + 0.1), x) # doesn't fail + assert solve(I*x**4 + x**3 + x**2 + 1.) # doesn't fail + + +def test_Abs_handling(): + x = symbols('x', real=True) + assert solve(abs(x/y), x) == [0] + + +def test_issue_7982(): + x = Symbol('x') + # Test that no exception happens + assert solve([2*x**2 + 5*x + 20 <= 0, x >= 1.5], x) is S.false + # From #8040 + assert solve([x**3 - 8.08*x**2 - 56.48*x/5 - 106 >= 0, x - 1 <= 0], [x]) is S.false + + +def test_issue_14645(): + x, y = symbols('x y') + assert solve([x*y - x - y, x*y - x - y], [x, y]) == [(y/(y - 1), y)] + + +def test_issue_12024(): + x, y = symbols('x y') + assert solve(Piecewise((0.0, x < 0.1), (x, x >= 0.1)) - y) == \ + [{y: Piecewise((0.0, x < 0.1), (x, True))}] + + +def test_issue_17452(): + assert solve((7**x)**x + pi, x) == [-sqrt(log(pi) + I*pi)/sqrt(log(7)), + sqrt(log(pi) + I*pi)/sqrt(log(7))] + assert solve(x**(x/11) + pi/11, x) == [exp(LambertW(-11*log(11) + 11*log(pi) + 11*I*pi))] + + +def test_issue_17799(): + assert solve(-erf(x**(S(1)/3))**pi + I, x) == [] + + +def test_issue_17650(): + x = Symbol('x', real=True) + assert solve(abs(abs(x**2 - 1) - x) - x) == [1, -1 + sqrt(2), 1 + sqrt(2)] + + +def test_issue_17882(): + eq = -8*x**2/(9*(x**2 - 1)**(S(4)/3)) + 4/(3*(x**2 - 1)**(S(1)/3)) + assert unrad(eq) is None + + +def test_issue_17949(): + assert solve(exp(+x+x**2), x) == [] + assert solve(exp(-x+x**2), x) == [] + assert solve(exp(+x-x**2), x) == [] + assert solve(exp(-x-x**2), x) == [] + + +def test_issue_10993(): + assert solve(Eq(binomial(x, 2), 3)) == [-2, 3] + assert solve(Eq(pow(x, 2) + binomial(x, 3), x)) == [-4, 0, 1] + assert solve(Eq(binomial(x, 2), 0)) == [0, 1] + assert solve(a+binomial(x, 3), a) == [-binomial(x, 3)] + assert solve(x-binomial(a, 3) + binomial(y, 2) + sin(a), x) == [-sin(a) + binomial(a, 3) - binomial(y, 2)] + assert solve((x+1)-binomial(x+1, 3), x) == [-2, -1, 3] + + +def test_issue_11553(): + eq1 = x + y + 1 + eq2 = x + GoldenRatio + assert solve([eq1, eq2], x, y) == {x: -GoldenRatio, y: -1 + GoldenRatio} + eq3 = x + 2 + TribonacciConstant + assert solve([eq1, eq3], x, y) == {x: -2 - TribonacciConstant, y: 1 + TribonacciConstant} + + +def test_issue_19113_19102(): + t = S(1)/3 + solve(cos(x)**5-sin(x)**5) + assert solve(4*cos(x)**3 - 2*sin(x)**3) == [ + atan(2**(t)), -atan(2**(t)*(1 - sqrt(3)*I)/2), + -atan(2**(t)*(1 + sqrt(3)*I)/2)] + h = S.Half + assert solve(cos(x)**2 + sin(x)) == [ + 2*atan(-h + sqrt(5)/2 + sqrt(2)*sqrt(1 - sqrt(5))/2), + -2*atan(h + sqrt(5)/2 + sqrt(2)*sqrt(1 + sqrt(5))/2), + -2*atan(-sqrt(5)/2 + h + sqrt(2)*sqrt(1 - sqrt(5))/2), + -2*atan(-sqrt(2)*sqrt(1 + sqrt(5))/2 + h + sqrt(5)/2)] + assert solve(3*cos(x) - sin(x)) == [atan(3)] + + +def test_issue_19509(): + a = S(3)/4 + b = S(5)/8 + c = sqrt(5)/8 + d = sqrt(5)/4 + assert solve(1/(x -1)**5 - 1) == [2, + -d + a - sqrt(-b + c), + -d + a + sqrt(-b + c), + d + a - sqrt(-b - c), + d + a + sqrt(-b - c)] + +def test_issue_20747(): + THT, HT, DBH, dib, c0, c1, c2, c3, c4 = symbols('THT HT DBH dib c0 c1 c2 c3 c4') + f = DBH*c3 + THT*c4 + c2 + rhs = 1 - ((HT - 1)/(THT - 1))**c1*(1 - exp(c0/f)) + eq = dib - DBH*(c0 - f*log(rhs)) + term = ((1 - exp((DBH*c0 - dib)/(DBH*(DBH*c3 + THT*c4 + c2)))) + / (1 - exp(c0/(DBH*c3 + THT*c4 + c2)))) + sol = [THT*term**(1/c1) - term**(1/c1) + 1] + assert solve(eq, HT) == sol + + +def test_issue_27001(): + assert solve((x, x**2), (x, y, z), dict=True) == [{x: 0}] + s = a1, a2, a3, a4, a5 = symbols('a1:6') + eqs = [8*a1**4*a2 + 4*a1**2*a2**3 - 8*a1**2*a2*a4 + a2**5/2 - 2*a2**3*a4 + + 8*a2*a3**2 + 2*a2*a4**2 + 8*a2*a5, 12*a1**4 + 6*a1**2*a2**2 - + 8*a1**2*a4 + 3*a2**4/4 - 2*a2**2*a4 + 4*a3**2 + a4**2 + 4*a5, 16*a1**3 + + 4*a1*a2**2 - 8*a1*a4, -8*a1**2*a2 - 2*a2**3 + 4*a2*a4] + sol = [{a4: 2*a1**2 + a2**2/2, a5: -a3**2}, {a1: 0, a2: 0, a5: -a3**2 - a4**2/4}] + assert solve(eqs, s, dict=True) == sol + assert (g:=solve(groebner(eqs, s), dict=True)) == sol, g + + +def test_issue_20902(): + f = (t / ((1 + t) ** 2)) + assert solve(f.subs({t: 3 * x + 2}).diff(x) > 0, x) == (S(-1) < x) & (x < S(-1)/3) + assert solve(f.subs({t: 3 * x + 3}).diff(x) > 0, x) == (S(-4)/3 < x) & (x < S(-2)/3) + assert solve(f.subs({t: 3 * x + 4}).diff(x) > 0, x) == (S(-5)/3 < x) & (x < S(-1)) + assert solve(f.subs({t: 3 * x + 2}).diff(x) > 0, x) == (S(-1) < x) & (x < S(-1)/3) + + +def test_issue_21034(): + a = symbols('a', real=True) + system = [x - cosh(cos(4)), y - sinh(cos(a)), z - tanh(x)] + # constants inside hyperbolic functions should not be rewritten in terms of exp + assert solve(system, x, y, z) == [(cosh(cos(4)), sinh(cos(a)), tanh(cosh(cos(4))))] + # but if the variable of interest is present in a hyperbolic function, + # then it should be rewritten in terms of exp and solved further + newsystem = [(exp(x) - exp(-x)) - tanh(x)*(exp(x) + exp(-x)) + x - 5] + assert solve(newsystem, x) == {x: 5} + + +def test_issue_4886(): + z = a*sqrt(R**2*a**2 + R**2*b**2 - c**2)/(a**2 + b**2) + t = b*c/(a**2 + b**2) + sol = [((b*(t - z) - c)/(-a), t - z), ((b*(t + z) - c)/(-a), t + z)] + assert solve([x**2 + y**2 - R**2, a*x + b*y - c], x, y) == sol + + +def test_issue_6819(): + a, b, c, d = symbols('a b c d', positive=True) + assert solve(a*b**x - c*d**x, x) == [log(c/a)/log(b/d)] + + +def test_issue_17454(): + x = Symbol('x') + assert solve((1 - x - I)**4, x) == [1 - I] + + +def test_issue_21852(): + solution = [21 - 21*sqrt(2)/2] + assert solve(2*x + sqrt(2*x**2) - 21) == solution + + +def test_issue_21942(): + eq = -d + (a*c**(1 - e) + b**(1 - e)*(1 - a))**(1/(1 - e)) + sol = solve(eq, c, simplify=False, check=False) + assert sol == [((a*b**(1 - e) - b**(1 - e) + + d**(1 - e))/a)**(1/(1 - e))] + + +def test_solver_flags(): + root = solve(x**5 + x**2 - x - 1, cubics=False) + rad = solve(x**5 + x**2 - x - 1, cubics=True) + assert root != rad + + +def test_issue_22768(): + eq = 2*x**3 - 16*(y - 1)**6*z**3 + assert solve(eq.expand(), x, simplify=False + ) == [2*z*(y - 1)**2, z*(-1 + sqrt(3)*I)*(y - 1)**2, + -z*(1 + sqrt(3)*I)*(y - 1)**2] + + +def test_issue_22717(): + assert solve((-y**2 + log(y**2/x) + 2, -2*x*y + 2*x/y)) == [ + {y: -1, x: E}, {y: 1, x: E}] + + +def test_issue_25176(): + eq = (x - 5)**-8 - 3 + sol = solve(eq) + assert not any(eq.subs(x, i) for i in sol) + + +def test_issue_10169(): + eq = S(-8*a - x**5*(a + b + c + e) - x**4*(4*a - 2**Rational(3,4)*c + 4*c + + d + 2**Rational(3,4)*e + 4*e + k) - x**3*(-4*2**Rational(3,4)*c + sqrt(2)*c - + 2**Rational(3,4)*d + 4*d + sqrt(2)*e + 4*2**Rational(3,4)*e + 2**Rational(3,4)*k + 4*k) - + x**2*(4*sqrt(2)*c - 4*2**Rational(3,4)*d + sqrt(2)*d + 4*sqrt(2)*e + + sqrt(2)*k + 4*2**Rational(3,4)*k) - x*(2*a + 2*b + 4*sqrt(2)*d + + 4*sqrt(2)*k) + 5) + assert solve_undetermined_coeffs(eq, [a, b, c, d, e, k], x) == { + a: Rational(5,8), + b: Rational(-5,1032), + c: Rational(-40,129) - 5*2**Rational(3,4)/129 + 5*2**Rational(1,4)/1032, + d: -20*2**Rational(3,4)/129 - 10*sqrt(2)/129 - 5*2**Rational(1,4)/258, + e: Rational(-40,129) - 5*2**Rational(1,4)/1032 + 5*2**Rational(3,4)/129, + k: -10*sqrt(2)/129 + 5*2**Rational(1,4)/258 + 20*2**Rational(3,4)/129 + } + + +def test_solve_undetermined_coeffs_issue_23927(): + A, B, r, phi = symbols('A, B, r, phi') + e = Eq(A*sin(t) + B*cos(t), r*sin(t - phi)) + eq = (e.lhs - e.rhs).expand(trig=True) + soln = solve_undetermined_coeffs(eq, (r, phi), t) + assert soln == [{ + phi: 2*atan((A - sqrt(A**2 + B**2))/B), + r: (-A**2 + A*sqrt(A**2 + B**2) - B**2)/(A - sqrt(A**2 + B**2)) + }, { + phi: 2*atan((A + sqrt(A**2 + B**2))/B), + r: (A**2 + A*sqrt(A**2 + B**2) + B**2)/(A + sqrt(A**2 + B**2))/-1 + }] + +def test_issue_24368(): + # Ideally these would produce a solution, but for now just check that they + # don't fail with a RuntimeError + raises(NotImplementedError, lambda: solve(Mod(x**2, 49), x)) + s2 = Symbol('s2', integer=True, positive=True) + f = floor(s2/2 - S(1)/2) + raises(NotImplementedError, lambda: solve((Mod(f**2/(f + 1) + 2*f/(f + 1) + 1/(f + 1), 1))*f + Mod(f**2/(f + 1) + 2*f/(f + 1) + 1/(f + 1), 1), s2)) + + +def test_solve_Piecewise(): + assert [S(10)/3] == solve(3*Piecewise( + (S.NaN, x <= 0), + (20*x - 3*(x - 6)**2/2 - 176, (x >= 0) & (x >= 2) & (x>= 4) & (x >= 6) & (x < 10)), + (100 - 26*x, (x >= 0) & (x >= 2) & (x >= 4) & (x < 10)), + (16*x - 3*(x - 6)**2/2 - 176, (x >= 2) & (x >= 4) & (x >= 6) & (x < 10)), + (100 - 30*x, (x >= 2) & (x >= 4) & (x < 10)), + (30*x - 3*(x - 6)**2/2 - 196, (x>= 0) & (x >= 4) & (x >= 6) & (x < 10)), + (80 - 16*x, (x >= 0) & (x >= 4) & (x < 10)), + (26*x - 3*(x - 6)**2/2 - 196, (x >= 4) & (x >= 6) & (x < 10)), + (80 - 20*x, (x >= 4) & (x < 10)), + (40*x - 3*(x - 6)**2/2 - 256, (x >= 0) & (x >= 2) & (x >= 6) & (x < 10)), + (20 - 6*x, (x >= 0) & (x >= 2) & (x < 10)), + (36*x - 3*(x - 6)**2/2 - 256, (x >= 2) & (x >= 6) & (x < 10)), + (20 - 10*x, (x >= 2) & (x < 10)), + (50*x - 3*(x - 6)**2/2 - 276, (x >= 0) & (x >= 6) & (x < 10)), + (4*x, (x >= 0) & (x < 10)), + (46*x - 3*(x - 6)**2/2 - 276, (x >= 6) & (x < 10)), + (0, x < 10), # this will simplify away + (S.NaN,True))) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_solveset.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_solveset.py new file mode 100644 index 0000000000000000000000000000000000000000..a1ba7a11e68ed518c4d83c050947b78756ade181 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/solvers/tests/test_solveset.py @@ -0,0 +1,3548 @@ +from math import isclose + +from sympy.calculus.util import stationary_points +from sympy.core.containers import Tuple +from sympy.core.function import (Function, Lambda, nfloat, diff) +from sympy.core.mod import Mod +from sympy.core.numbers import (E, I, Rational, oo, pi, Integer, all_close) +from sympy.core.relational import (Eq, Gt, Ne, Ge) +from sympy.core.singleton import S +from sympy.core.sorting import ordered +from sympy.core.symbol import (Dummy, Symbol, symbols) +from sympy.core.sympify import sympify +from sympy.functions.elementary.complexes import (Abs, arg, im, re, sign, conjugate) +from sympy.functions.elementary.exponential import (LambertW, exp, log) +from sympy.functions.elementary.hyperbolic import (HyperbolicFunction, + sinh, cosh, tanh, coth, sech, csch, asinh, acosh, atanh, acoth, asech, acsch) +from sympy.functions.elementary.miscellaneous import sqrt, Min, Max +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import ( + TrigonometricFunction, acos, acot, acsc, asec, asin, atan, atan2, + cos, cot, csc, sec, sin, tan) +from sympy.functions.special.error_functions import (erf, erfc, + erfcinv, erfinv) +from sympy.logic.boolalg import And +from sympy.matrices.dense import MutableDenseMatrix as Matrix +from sympy.matrices.immutable import ImmutableDenseMatrix +from sympy.polys.polytools import Poly +from sympy.polys.rootoftools import CRootOf +from sympy.sets.contains import Contains +from sympy.sets.conditionset import ConditionSet +from sympy.sets.fancysets import ImageSet, Range +from sympy.sets.sets import (Complement, FiniteSet, + Intersection, Interval, Union, imageset, ProductSet) +from sympy.simplify import simplify +from sympy.tensor.indexed import Indexed +from sympy.utilities.iterables import numbered_symbols + +from sympy.testing.pytest import (XFAIL, raises, skip, slow, SKIP, _both_exp_pow) +from sympy.core.random import verify_numerically as tn +from sympy.physics.units import cm + +from sympy.solvers import solve +from sympy.solvers.solveset import ( + solveset_real, domain_check, solveset_complex, linear_eq_to_matrix, + linsolve, _is_function_class_equation, invert_real, invert_complex, + _invert_trig_hyp_real, solveset, solve_decomposition, substitution, + nonlinsolve, solvify, + _is_finite_with_finite_vars, _transolve, _is_exponential, + _solve_exponential, _is_logarithmic, _is_lambert, + _solve_logarithm, _term_factors, _is_modular, NonlinearError) + +from sympy.abc import (a, b, c, d, e, f, g, h, i, j, k, l, m, n, q, r, + t, w, x, y, z) + + +def dumeq(i, j): + if type(i) in (list, tuple): + return all(dumeq(i, j) for i, j in zip(i, j)) + return i == j or i.dummy_eq(j) + + +def assert_close_ss(sol1, sol2): + """Test solutions with floats from solveset are close""" + sol1 = sympify(sol1) + sol2 = sympify(sol2) + assert isinstance(sol1, FiniteSet) + assert isinstance(sol2, FiniteSet) + assert len(sol1) == len(sol2) + assert all(isclose(v1, v2) for v1, v2 in zip(sol1, sol2)) + + +def assert_close_nl(sol1, sol2): + """Test solutions with floats from nonlinsolve are close""" + sol1 = sympify(sol1) + sol2 = sympify(sol2) + assert isinstance(sol1, FiniteSet) + assert isinstance(sol2, FiniteSet) + assert len(sol1) == len(sol2) + for s1, s2 in zip(sol1, sol2): + assert len(s1) == len(s2) + assert all(isclose(v1, v2) for v1, v2 in zip(s1, s2)) + + +@_both_exp_pow +def test_invert_real(): + x = Symbol('x', real=True) + + def ireal(x, s=S.Reals): + return Intersection(s, x) + + assert invert_real(exp(x), z, x) == (x, ireal(FiniteSet(log(z)))) + + y = Symbol('y', positive=True) + n = Symbol('n', real=True) + assert invert_real(x + 3, y, x) == (x, FiniteSet(y - 3)) + assert invert_real(x*3, y, x) == (x, FiniteSet(y / 3)) + + assert invert_real(exp(x), y, x) == (x, FiniteSet(log(y))) + assert invert_real(exp(3*x), y, x) == (x, FiniteSet(log(y) / 3)) + assert invert_real(exp(x + 3), y, x) == (x, FiniteSet(log(y) - 3)) + + assert invert_real(exp(x) + 3, y, x) == (x, ireal(FiniteSet(log(y - 3)))) + assert invert_real(exp(x)*3, y, x) == (x, FiniteSet(log(y / 3))) + + assert invert_real(log(x), y, x) == (x, FiniteSet(exp(y))) + assert invert_real(log(3*x), y, x) == (x, FiniteSet(exp(y) / 3)) + assert invert_real(log(x + 3), y, x) == (x, FiniteSet(exp(y) - 3)) + + assert invert_real(Abs(x), y, x) == (x, FiniteSet(y, -y)) + + assert invert_real(2**x, y, x) == (x, FiniteSet(log(y)/log(2))) + assert invert_real(2**exp(x), y, x) == (x, ireal(FiniteSet(log(log(y)/log(2))))) + + assert invert_real(x**2, y, x) == (x, FiniteSet(sqrt(y), -sqrt(y))) + assert invert_real(x**S.Half, y, x) == (x, FiniteSet(y**2)) + + raises(ValueError, lambda: invert_real(x, x, x)) + + # issue 21236 + assert invert_real(x**pi, y, x) == (x, FiniteSet(y**(1/pi))) + assert invert_real(x**pi, -E, x) == (x, S.EmptySet) + assert invert_real(x**Rational(3/2), 1000, x) == (x, FiniteSet(100)) + assert invert_real(x**1.0, 1, x) == (x**1.0, FiniteSet(1)) + + raises(ValueError, lambda: invert_real(S.One, y, x)) + + assert invert_real(x**31 + x, y, x) == (x**31 + x, FiniteSet(y)) + + lhs = x**31 + x + base_values = FiniteSet(y - 1, -y - 1) + assert invert_real(Abs(x**31 + x + 1), y, x) == (lhs, base_values) + + assert dumeq(invert_real(sin(x), y, x), (x, + ConditionSet(x, (S(-1) <= y) & (y <= S(1)), Union( + ImageSet(Lambda(n, 2*n*pi + asin(y)), S.Integers), + ImageSet(Lambda(n, pi*2*n + pi - asin(y)), S.Integers))))) + + assert dumeq(invert_real(sin(exp(x)), y, x), (x, + ConditionSet(x, (S(-1) <= y) & (y <= S(1)), Union( + ImageSet(Lambda(n, log(2*n*pi + asin(y))), S.Integers), + ImageSet(Lambda(n, log(pi*2*n + pi - asin(y))), S.Integers))))) + + assert dumeq(invert_real(csc(x), y, x), (x, + ConditionSet(x, ((S(1) <= y) & (y < oo)) | ((-oo < y) & (y <= S(-1))), + Union(ImageSet(Lambda(n, 2*n*pi + acsc(y)), S.Integers), + ImageSet(Lambda(n, 2*n*pi - acsc(y) + pi), S.Integers))))) + + assert dumeq(invert_real(csc(exp(x)), y, x), (x, + ConditionSet(x, ((S(1) <= y) & (y < oo)) | ((-oo < y) & (y <= S(-1))), + Union(ImageSet(Lambda(n, log(2*n*pi + acsc(y))), S.Integers), + ImageSet(Lambda(n, log(2*n*pi - acsc(y) + pi)), S.Integers))))) + + assert dumeq(invert_real(cos(x), y, x), (x, + ConditionSet(x, (S(-1) <= y) & (y <= S(1)), Union( + ImageSet(Lambda(n, 2*n*pi + acos(y)), S.Integers), + ImageSet(Lambda(n, 2*n*pi - acos(y)), S.Integers))))) + + assert dumeq(invert_real(cos(exp(x)), y, x), (x, + ConditionSet(x, (S(-1) <= y) & (y <= S(1)), Union( + ImageSet(Lambda(n, log(2*n*pi + acos(y))), S.Integers), + ImageSet(Lambda(n, log(2*n*pi - acos(y))), S.Integers))))) + + assert dumeq(invert_real(sec(x), y, x), (x, + ConditionSet(x, ((S(1) <= y) & (y < oo)) | ((-oo < y) & (y <= S(-1))), + Union(ImageSet(Lambda(n, 2*n*pi + asec(y)), S.Integers), \ + ImageSet(Lambda(n, 2*n*pi - asec(y)), S.Integers))))) + + assert dumeq(invert_real(sec(exp(x)), y, x), (x, + ConditionSet(x, ((S(1) <= y) & (y < oo)) | ((-oo < y) & (y <= S(-1))), + Union(ImageSet(Lambda(n, log(2*n*pi - asec(y))), S.Integers), + ImageSet(Lambda(n, log(2*n*pi + asec(y))), S.Integers))))) + + assert dumeq(invert_real(tan(x), y, x), (x, + ConditionSet(x, (-oo < y) & (y < oo), + ImageSet(Lambda(n, n*pi + atan(y)), S.Integers)))) + + assert dumeq(invert_real(tan(exp(x)), y, x), (x, + ConditionSet(x, (-oo < y) & (y < oo), + ImageSet(Lambda(n, log(n*pi + atan(y))), S.Integers)))) + + assert dumeq(invert_real(cot(x), y, x), (x, + ConditionSet(x, (-oo < y) & (y < oo), + ImageSet(Lambda(n, n*pi + acot(y)), S.Integers)))) + + assert dumeq(invert_real(cot(exp(x)), y, x), (x, + ConditionSet(x, (-oo < y) & (y < oo), + ImageSet(Lambda(n, log(n*pi + acot(y))), S.Integers)))) + + assert dumeq(invert_real(tan(tan(x)), y, x), + (x, ConditionSet(x, Eq(tan(tan(x)), y), S.Reals))) + # slight regression compared to previous result: + # (tan(x), imageset(Lambda(n, n*pi + atan(y)), S.Integers))) + + x = Symbol('x', positive=True) + assert invert_real(x**pi, y, x) == (x, FiniteSet(y**(1/pi))) + + r = Symbol('r', real=True) + p = Symbol('p', positive=True) + assert invert_real(sinh(x), r, x) == (x, FiniteSet(asinh(r))) + assert invert_real(sinh(log(x)), p, x) == (x, FiniteSet(exp(asinh(p)))) + + assert invert_real(cosh(x), r, x) == (x, Intersection( + FiniteSet(-acosh(r), acosh(r)), S.Reals)) + assert invert_real(cosh(x), p + 1, x) == (x, + FiniteSet(-acosh(p + 1), acosh(p + 1))) + + assert invert_real(tanh(x), r, x) == (x, Intersection(FiniteSet(atanh(r)), S.Reals)) + assert invert_real(coth(x), p+1, x) == (x, FiniteSet(acoth(p+1))) + assert invert_real(sech(x), r, x) == (x, Intersection( + FiniteSet(-asech(r), asech(r)), S.Reals)) + assert invert_real(csch(x), p, x) == (x, FiniteSet(acsch(p))) + + assert dumeq(invert_real(tanh(sin(x)), r, x), (x, + ConditionSet(x, (S(-1) <= atanh(r)) & (atanh(r) <= S(1)), Union( + ImageSet(Lambda(n, 2*n*pi + asin(atanh(r))), S.Integers), + ImageSet(Lambda(n, 2*n*pi - asin(atanh(r)) + pi), S.Integers))))) + + +def test_invert_trig_hyp_real(): + # check some codepaths that are not as easily reached otherwise + n = Dummy('n') + assert _invert_trig_hyp_real(cosh(x), Range(-5, 10, 1), x)[1].dummy_eq(Union( + ImageSet(Lambda(n, -acosh(n)), Range(1, 10, 1)), + ImageSet(Lambda(n, acosh(n)), Range(1, 10, 1)))) + assert _invert_trig_hyp_real(coth(x), Interval(-3, 2), x) == (x, Union( + Interval(-oo, -acoth(3)), Interval(acoth(2), oo))) + assert _invert_trig_hyp_real(tanh(x), Interval(-S.Half, 1), x) == (x, + Interval(-atanh(S.Half), oo)) + assert _invert_trig_hyp_real(sech(x), imageset(n, S.Half + n/3, S.Naturals0), x) == \ + (x, FiniteSet(-asech(S(1)/2), asech(S(1)/2), -asech(S(5)/6), asech(S(5)/6))) + assert _invert_trig_hyp_real(csch(x), S.Reals, x) == (x, + Union(Interval.open(-oo, 0), Interval.open(0, oo))) + + +def test_invert_complex(): + assert invert_complex(x + 3, y, x) == (x, FiniteSet(y - 3)) + assert invert_complex(x*3, y, x) == (x, FiniteSet(y / 3)) + assert invert_complex((x - 1)**3, 0, x) == (x, FiniteSet(1)) + + assert dumeq(invert_complex(exp(x), y, x), + (x, imageset(Lambda(n, I*(2*pi*n + arg(y)) + log(Abs(y))), S.Integers))) + + assert invert_complex(log(x), y, x) == (x, FiniteSet(exp(y))) + + raises(ValueError, lambda: invert_real(1, y, x)) + raises(ValueError, lambda: invert_complex(x, x, x)) + raises(ValueError, lambda: invert_complex(x, x, 1)) + + assert dumeq(invert_complex(sin(x), I, x), (x, Union( + ImageSet(Lambda(n, 2*n*pi + I*log(1 + sqrt(2))), S.Integers), + ImageSet(Lambda(n, 2*n*pi + pi - I*log(1 + sqrt(2))), S.Integers)))) + assert dumeq(invert_complex(cos(x), 1+I, x), (x, Union( + ImageSet(Lambda(n, 2*n*pi - acos(1 + I)), S.Integers), + ImageSet(Lambda(n, 2*n*pi + acos(1 + I)), S.Integers)))) + assert dumeq(invert_complex(tan(2*x), 1, x), (x, + ImageSet(Lambda(n, n*pi/2 + pi/8), S.Integers))) + assert dumeq(invert_complex(cot(x), 2*I, x), (x, + ImageSet(Lambda(n, n*pi - I*acoth(2)), S.Integers))) + + assert dumeq(invert_complex(sinh(x), 0, x), (x, Union( + ImageSet(Lambda(n, 2*n*I*pi), S.Integers), + ImageSet(Lambda(n, 2*n*I*pi + I*pi), S.Integers)))) + assert dumeq(invert_complex(cosh(x), 0, x), (x, Union( + ImageSet(Lambda(n, 2*n*I*pi + I*pi/2), S.Integers), + ImageSet(Lambda(n, 2*n*I*pi + 3*I*pi/2), S.Integers)))) + assert invert_complex(tanh(x), 1, x) == (x, S.EmptySet) + assert dumeq(invert_complex(tanh(x), a, x), (x, + ConditionSet(x, Ne(a, -1) & Ne(a, 1), + ImageSet(Lambda(n, n*I*pi + atanh(a)), S.Integers)))) + assert invert_complex(coth(x), 1, x) == (x, S.EmptySet) + assert dumeq(invert_complex(coth(x), a, x), (x, + ConditionSet(x, Ne(a, -1) & Ne(a, 1), + ImageSet(Lambda(n, n*I*pi + acoth(a)), S.Integers)))) + assert dumeq(invert_complex(sech(x), 2, x), (x, Union( + ImageSet(Lambda(n, 2*n*I*pi + I*pi/3), S.Integers), + ImageSet(Lambda(n, 2*n*I*pi + 5*I*pi/3), S.Integers)))) + + +def test_domain_check(): + assert domain_check(1/(1 + (1/(x+1))**2), x, -1) is False + assert domain_check(x**2, x, 0) is True + assert domain_check(x, x, oo) is False + assert domain_check(0, x, oo) is False + + +def test_issue_11536(): + assert solveset(0**x - 100, x, S.Reals) == S.EmptySet + assert solveset(0**x - 1, x, S.Reals) == FiniteSet(0) + + +def test_issue_17479(): + f = (x**2 + y**2)**2 + (x**2 + z**2)**2 - 2*(2*x**2 + y**2 + z**2) + fx = f.diff(x) + fy = f.diff(y) + fz = f.diff(z) + sol = nonlinsolve([fx, fy, fz], [x, y, z]) + assert len(sol) >= 4 and len(sol) <= 20 + # nonlinsolve has been giving a varying number of solutions + # (originally 18, then 20, now 19) due to various internal changes. + # Unfortunately not all the solutions are actually valid and some are + # redundant. Since the original issue was that an exception was raised, + # this first test only checks that nonlinsolve returns a "plausible" + # solution set. The next test checks the result for correctness. + + +@XFAIL +def test_issue_18449(): + x, y, z = symbols("x, y, z") + f = (x**2 + y**2)**2 + (x**2 + z**2)**2 - 2*(2*x**2 + y**2 + z**2) + fx = diff(f, x) + fy = diff(f, y) + fz = diff(f, z) + sol = nonlinsolve([fx, fy, fz], [x, y, z]) + for (xs, ys, zs) in sol: + d = {x: xs, y: ys, z: zs} + assert tuple(_.subs(d).simplify() for _ in (fx, fy, fz)) == (0, 0, 0) + # After simplification and removal of duplicate elements, there should + # only be 4 parametric solutions left: + # simplifiedsolutions = FiniteSet((sqrt(1 - z**2), z, z), + # (-sqrt(1 - z**2), z, z), + # (sqrt(1 - z**2), -z, z), + # (-sqrt(1 - z**2), -z, z)) + # TODO: Is the above solution set definitely complete? + + +def test_issue_21047(): + f = (2 - x)**2 + (sqrt(x - 1) - 1)**6 + assert solveset(f, x, S.Reals) == FiniteSet(2) + + f = (sqrt(x)-1)**2 + (sqrt(x)+1)**2 -2*x**2 + sqrt(2) + assert solveset(f, x, S.Reals) == FiniteSet( + S.Half - sqrt(2*sqrt(2) + 5)/2, S.Half + sqrt(2*sqrt(2) + 5)/2) + + +def test_is_function_class_equation(): + assert _is_function_class_equation(TrigonometricFunction, + tan(x), x) is True + assert _is_function_class_equation(TrigonometricFunction, + tan(x) - 1, x) is True + assert _is_function_class_equation(TrigonometricFunction, + tan(x) + sin(x), x) is True + assert _is_function_class_equation(TrigonometricFunction, + tan(x) + sin(x) - a, x) is True + assert _is_function_class_equation(TrigonometricFunction, + sin(x)*tan(x) + sin(x), x) is True + assert _is_function_class_equation(TrigonometricFunction, + sin(x)*tan(x + a) + sin(x), x) is True + assert _is_function_class_equation(TrigonometricFunction, + sin(x)*tan(x*a) + sin(x), x) is True + assert _is_function_class_equation(TrigonometricFunction, + a*tan(x) - 1, x) is True + assert _is_function_class_equation(TrigonometricFunction, + tan(x)**2 + sin(x) - 1, x) is True + assert _is_function_class_equation(TrigonometricFunction, + tan(x) + x, x) is False + assert _is_function_class_equation(TrigonometricFunction, + tan(x**2), x) is False + assert _is_function_class_equation(TrigonometricFunction, + tan(x**2) + sin(x), x) is False + assert _is_function_class_equation(TrigonometricFunction, + tan(x)**sin(x), x) is False + assert _is_function_class_equation(TrigonometricFunction, + tan(sin(x)) + sin(x), x) is False + assert _is_function_class_equation(HyperbolicFunction, + tanh(x), x) is True + assert _is_function_class_equation(HyperbolicFunction, + tanh(x) - 1, x) is True + assert _is_function_class_equation(HyperbolicFunction, + tanh(x) + sinh(x), x) is True + assert _is_function_class_equation(HyperbolicFunction, + tanh(x) + sinh(x) - a, x) is True + assert _is_function_class_equation(HyperbolicFunction, + sinh(x)*tanh(x) + sinh(x), x) is True + assert _is_function_class_equation(HyperbolicFunction, + sinh(x)*tanh(x + a) + sinh(x), x) is True + assert _is_function_class_equation(HyperbolicFunction, + sinh(x)*tanh(x*a) + sinh(x), x) is True + assert _is_function_class_equation(HyperbolicFunction, + a*tanh(x) - 1, x) is True + assert _is_function_class_equation(HyperbolicFunction, + tanh(x)**2 + sinh(x) - 1, x) is True + assert _is_function_class_equation(HyperbolicFunction, + tanh(x) + x, x) is False + assert _is_function_class_equation(HyperbolicFunction, + tanh(x**2), x) is False + assert _is_function_class_equation(HyperbolicFunction, + tanh(x**2) + sinh(x), x) is False + assert _is_function_class_equation(HyperbolicFunction, + tanh(x)**sinh(x), x) is False + assert _is_function_class_equation(HyperbolicFunction, + tanh(sinh(x)) + sinh(x), x) is False + + +def test_garbage_input(): + raises(ValueError, lambda: solveset_real([y], y)) + x = Symbol('x', real=True) + assert solveset_real(x, 1) == S.EmptySet + assert solveset_real(x - 1, 1) == FiniteSet(x) + assert solveset_real(x, pi) == S.EmptySet + assert solveset_real(x, x**2) == S.EmptySet + + raises(ValueError, lambda: solveset_complex([x], x)) + assert solveset_complex(x, pi) == S.EmptySet + + raises(ValueError, lambda: solveset((x, y), x)) + raises(ValueError, lambda: solveset(x + 1, S.Reals)) + raises(ValueError, lambda: solveset(x + 1, x, 2)) + + +def test_solve_mul(): + assert solveset_real((a*x + b)*(exp(x) - 3), x) == \ + Union({log(3)}, Intersection({-b/a}, S.Reals)) + anz = Symbol('anz', nonzero=True) + bb = Symbol('bb', real=True) + assert solveset_real((anz*x + bb)*(exp(x) - 3), x) == \ + FiniteSet(-bb/anz, log(3)) + assert solveset_real((2*x + 8)*(8 + exp(x)), x) == FiniteSet(S(-4)) + assert solveset_real(x/log(x), x) is S.EmptySet + + +def test_solve_invert(): + assert solveset_real(exp(x) - 3, x) == FiniteSet(log(3)) + assert solveset_real(log(x) - 3, x) == FiniteSet(exp(3)) + + assert solveset_real(3**(x + 2), x) == FiniteSet() + assert solveset_real(3**(2 - x), x) == FiniteSet() + + assert solveset_real(y - b*exp(a/x), x) == Intersection( + S.Reals, FiniteSet(a/log(y/b))) + + # issue 4504 + assert solveset_real(2**x - 10, x) == FiniteSet(1 + log(5)/log(2)) + + +def test_issue_25768(): + assert dumeq(solveset_real(sin(x) - S.Half, x), Union( + ImageSet(Lambda(n, pi*2*n + pi/6), S.Integers), + ImageSet(Lambda(n, pi*2*n + pi*5/6), S.Integers))) + n1 = solveset_real(sin(x) - 0.5, x).n(5) + n2 = solveset_real(sin(x) - S.Half, x).n(5) + # help pass despite fp differences + eq = [i.replace( + lambda x:x.is_Float, + lambda x:Rational(x).limit_denominator(1000)) for i in (n1, n2)] + assert dumeq(*eq),(n1,n2) + + +def test_errorinverses(): + assert solveset_real(erf(x) - S.Half, x) == \ + FiniteSet(erfinv(S.Half)) + assert solveset_real(erfinv(x) - 2, x) == \ + FiniteSet(erf(2)) + assert solveset_real(erfc(x) - S.One, x) == \ + FiniteSet(erfcinv(S.One)) + assert solveset_real(erfcinv(x) - 2, x) == FiniteSet(erfc(2)) + + +def test_solve_polynomial(): + x = Symbol('x', real=True) + y = Symbol('y', real=True) + assert solveset_real(3*x - 2, x) == FiniteSet(Rational(2, 3)) + + assert solveset_real(x**2 - 1, x) == FiniteSet(-S.One, S.One) + assert solveset_real(x - y**3, x) == FiniteSet(y ** 3) + + assert solveset_real(x**3 - 15*x - 4, x) == FiniteSet( + -2 + 3 ** S.Half, + S(4), + -2 - 3 ** S.Half) + + assert solveset_real(sqrt(x) - 1, x) == FiniteSet(1) + assert solveset_real(sqrt(x) - 2, x) == FiniteSet(4) + assert solveset_real(x**Rational(1, 4) - 2, x) == FiniteSet(16) + assert solveset_real(x**Rational(1, 3) - 3, x) == FiniteSet(27) + assert len(solveset_real(x**5 + x**3 + 1, x)) == 1 + assert len(solveset_real(-2*x**3 + 4*x**2 - 2*x + 6, x)) > 0 + assert solveset_real(x**6 + x**4 + I, x) is S.EmptySet + + +def test_return_root_of(): + f = x**5 - 15*x**3 - 5*x**2 + 10*x + 20 + s = list(solveset_complex(f, x)) + for root in s: + assert root.func == CRootOf + + # if one uses solve to get the roots of a polynomial that has a CRootOf + # solution, make sure that the use of nfloat during the solve process + # doesn't fail. Note: if you want numerical solutions to a polynomial + # it is *much* faster to use nroots to get them than to solve the + # equation only to get CRootOf solutions which are then numerically + # evaluated. So for eq = x**5 + 3*x + 7 do Poly(eq).nroots() rather + # than [i.n() for i in solve(eq)] to get the numerical roots of eq. + assert nfloat(list(solveset_complex(x**5 + 3*x**3 + 7, x))[0], + exponent=False) == CRootOf(x**5 + 3*x**3 + 7, 0).n() + + sol = list(solveset_complex(x**6 - 2*x + 2, x)) + assert all(isinstance(i, CRootOf) for i in sol) and len(sol) == 6 + + f = x**5 - 15*x**3 - 5*x**2 + 10*x + 20 + s = list(solveset_complex(f, x)) + for root in s: + assert root.func == CRootOf + + s = x**5 + 4*x**3 + 3*x**2 + Rational(7, 4) + assert solveset_complex(s, x) == \ + FiniteSet(*Poly(s*4, domain='ZZ').all_roots()) + + # Refer issue #7876 + eq = x*(x - 1)**2*(x + 1)*(x**6 - x + 1) + assert solveset_complex(eq, x) == \ + FiniteSet(-1, 0, 1, CRootOf(x**6 - x + 1, 0), + CRootOf(x**6 - x + 1, 1), + CRootOf(x**6 - x + 1, 2), + CRootOf(x**6 - x + 1, 3), + CRootOf(x**6 - x + 1, 4), + CRootOf(x**6 - x + 1, 5)) + + +def test_solveset_sqrt_1(): + assert solveset_real(sqrt(5*x + 6) - 2 - x, x) == \ + FiniteSet(-S.One, S(2)) + assert solveset_real(sqrt(x - 1) - x + 7, x) == FiniteSet(10) + assert solveset_real(sqrt(x - 2) - 5, x) == FiniteSet(27) + assert solveset_real(sqrt(x) - 2 - 5, x) == FiniteSet(49) + assert solveset_real(sqrt(x**3), x) == FiniteSet(0) + assert solveset_real(sqrt(x - 1), x) == FiniteSet(1) + assert solveset_real(sqrt((x-3)/x), x) == FiniteSet(3) + assert solveset_real(sqrt((x-3)/x)-Rational(1, 2), x) == \ + FiniteSet(4) + +def test_solveset_sqrt_2(): + x = Symbol('x', real=True) + y = Symbol('y', real=True) + # http://tutorial.math.lamar.edu/Classes/Alg/SolveRadicalEqns.aspx#Solve_Rad_Ex2_a + assert solveset_real(sqrt(2*x - 1) - sqrt(x - 4) - 2, x) == \ + FiniteSet(S(5), S(13)) + assert solveset_real(sqrt(x + 7) + 2 - sqrt(3 - x), x) == \ + FiniteSet(-6) + + # http://www.purplemath.com/modules/solverad.htm + assert solveset_real(sqrt(17*x - sqrt(x**2 - 5)) - 7, x) == \ + FiniteSet(3) + + eq = x + 1 - (x**4 + 4*x**3 - x)**Rational(1, 4) + assert solveset_real(eq, x) == FiniteSet(Rational(-1, 2), Rational(-1, 3)) + + eq = sqrt(2*x + 9) - sqrt(x + 1) - sqrt(x + 4) + assert solveset_real(eq, x) == FiniteSet(0) + + eq = sqrt(x + 4) + sqrt(2*x - 1) - 3*sqrt(x - 1) + assert solveset_real(eq, x) == FiniteSet(5) + + eq = sqrt(x)*sqrt(x - 7) - 12 + assert solveset_real(eq, x) == FiniteSet(16) + + eq = sqrt(x - 3) + sqrt(x) - 3 + assert solveset_real(eq, x) == FiniteSet(4) + + eq = sqrt(2*x**2 - 7) - (3 - x) + assert solveset_real(eq, x) == FiniteSet(-S(8), S(2)) + + # others + eq = sqrt(9*x**2 + 4) - (3*x + 2) + assert solveset_real(eq, x) == FiniteSet(0) + + assert solveset_real(sqrt(x - 3) - sqrt(x) - 3, x) == FiniteSet() + + eq = (2*x - 5)**Rational(1, 3) - 3 + assert solveset_real(eq, x) == FiniteSet(16) + + assert solveset_real(sqrt(x) + sqrt(sqrt(x)) - 4, x) == \ + FiniteSet((Rational(-1, 2) + sqrt(17)/2)**4) + + eq = sqrt(x) - sqrt(x - 1) + sqrt(sqrt(x)) + assert solveset_real(eq, x) == FiniteSet() + + eq = (x - 4)**2 + (sqrt(x) - 2)**4 + assert solveset_real(eq, x) == FiniteSet(-4, 4) + + eq = (sqrt(x) + sqrt(x + 1) + sqrt(1 - x) - 6*sqrt(5)/5) + ans = solveset_real(eq, x) + ra = S('''-1484/375 - 4*(-S(1)/2 + sqrt(3)*I/2)*(-12459439/52734375 + + 114*sqrt(12657)/78125)**(S(1)/3) - 172564/(140625*(-S(1)/2 + + sqrt(3)*I/2)*(-12459439/52734375 + 114*sqrt(12657)/78125)**(S(1)/3))''') + rb = Rational(4, 5) + assert all(abs(eq.subs(x, i).n()) < 1e-10 for i in (ra, rb)) and \ + len(ans) == 2 and \ + {i.n(chop=True) for i in ans} == \ + {i.n(chop=True) for i in (ra, rb)} + + assert solveset_real(sqrt(x) + x**Rational(1, 3) + + x**Rational(1, 4), x) == FiniteSet(0) + + assert solveset_real(x/sqrt(x**2 + 1), x) == FiniteSet(0) + + eq = (x - y**3)/((y**2)*sqrt(1 - y**2)) + assert solveset_real(eq, x) == FiniteSet(y**3) + + # issue 4497 + assert solveset_real(1/(5 + x)**Rational(1, 5) - 9, x) == \ + FiniteSet(Rational(-295244, 59049)) + + +@XFAIL +def test_solve_sqrt_fail(): + # this only works if we check real_root(eq.subs(x, Rational(1, 3))) + # but checksol doesn't work like that + eq = (x**3 - 3*x**2)**Rational(1, 3) + 1 - x + assert solveset_real(eq, x) == FiniteSet(Rational(1, 3)) + + +@slow +def test_solve_sqrt_3(): + R = Symbol('R') + eq = sqrt(2)*R*sqrt(1/(R + 1)) + (R + 1)*(sqrt(2)*sqrt(1/(R + 1)) - 1) + sol = solveset_complex(eq, R) + fset = [Rational(5, 3) + 4*sqrt(10)*cos(atan(3*sqrt(111)/251)/3)/3, + -sqrt(10)*cos(atan(3*sqrt(111)/251)/3)/3 + + 40*re(1/((Rational(-1, 2) - sqrt(3)*I/2)*(Rational(251, 27) + sqrt(111)*I/9)**Rational(1, 3)))/9 + + sqrt(30)*sin(atan(3*sqrt(111)/251)/3)/3 + Rational(5, 3) + + I*(-sqrt(30)*cos(atan(3*sqrt(111)/251)/3)/3 - + sqrt(10)*sin(atan(3*sqrt(111)/251)/3)/3 + + 40*im(1/((Rational(-1, 2) - sqrt(3)*I/2)*(Rational(251, 27) + sqrt(111)*I/9)**Rational(1, 3)))/9)] + cset = [40*re(1/((Rational(-1, 2) + sqrt(3)*I/2)*(Rational(251, 27) + sqrt(111)*I/9)**Rational(1, 3)))/9 - + sqrt(10)*cos(atan(3*sqrt(111)/251)/3)/3 - sqrt(30)*sin(atan(3*sqrt(111)/251)/3)/3 + + Rational(5, 3) + + I*(40*im(1/((Rational(-1, 2) + sqrt(3)*I/2)*(Rational(251, 27) + sqrt(111)*I/9)**Rational(1, 3)))/9 - + sqrt(10)*sin(atan(3*sqrt(111)/251)/3)/3 + + sqrt(30)*cos(atan(3*sqrt(111)/251)/3)/3)] + + fs = FiniteSet(*fset) + cs = ConditionSet(R, Eq(eq, 0), FiniteSet(*cset)) + assert sol == (fs - {-1}) | (cs - {-1}) + + # the number of real roots will depend on the value of m: for m=1 there are 4 + # and for m=-1 there are none. + eq = -sqrt((m - q)**2 + (-m/(2*q) + S.Half)**2) + sqrt((-m**2/2 - sqrt( + 4*m**4 - 4*m**2 + 8*m + 1)/4 - Rational(1, 4))**2 + (m**2/2 - m - sqrt( + 4*m**4 - 4*m**2 + 8*m + 1)/4 - Rational(1, 4))**2) + unsolved_object = ConditionSet(q, Eq(sqrt((m - q)**2 + (-m/(2*q) + S.Half)**2) - + sqrt((-m**2/2 - sqrt(4*m**4 - 4*m**2 + 8*m + 1)/4 - Rational(1, 4))**2 + (m**2/2 - m - + sqrt(4*m**4 - 4*m**2 + 8*m + 1)/4 - Rational(1, 4))**2), 0), S.Reals) + assert solveset_real(eq, q) == unsolved_object + + +def test_solve_polynomial_symbolic_param(): + assert solveset_complex((x**2 - 1)**2 - a, x) == \ + FiniteSet(sqrt(1 + sqrt(a)), -sqrt(1 + sqrt(a)), + sqrt(1 - sqrt(a)), -sqrt(1 - sqrt(a))) + + # issue 4507 + assert solveset_complex(y - b/(1 + a*x), x) == \ + FiniteSet((b/y - 1)/a) - FiniteSet(-1/a) + + # issue 4508 + assert solveset_complex(y - b*x/(a + x), x) == \ + FiniteSet(-a*y/(y - b)) - FiniteSet(-a) + + +def test_solve_rational(): + assert solveset_real(1/x + 1, x) == FiniteSet(-S.One) + assert solveset_real(1/exp(x) - 1, x) == FiniteSet(0) + assert solveset_real(x*(1 - 5/x), x) == FiniteSet(5) + assert solveset_real(2*x/(x + 2) - 1, x) == FiniteSet(2) + assert solveset_real((x**2/(7 - x)).diff(x), x) == \ + FiniteSet(S.Zero, S(14)) + + +def test_solveset_real_gen_is_pow(): + assert solveset_real(sqrt(1) + 1, x) is S.EmptySet + + +def test_no_sol(): + assert solveset(1 - oo*x) is S.EmptySet + assert solveset(oo*x, x) is S.EmptySet + assert solveset(oo*x - oo, x) is S.EmptySet + assert solveset_real(4, x) is S.EmptySet + assert solveset_real(exp(x), x) is S.EmptySet + assert solveset_real(x**2 + 1, x) is S.EmptySet + assert solveset_real(-3*a/sqrt(x), x) is S.EmptySet + assert solveset_real(1/x, x) is S.EmptySet + assert solveset_real(-(1 + x)/(2 + x)**2 + 1/(2 + x), x + ) is S.EmptySet + + +def test_sol_zero_real(): + assert solveset_real(0, x) == S.Reals + assert solveset(0, x, Interval(1, 2)) == Interval(1, 2) + assert solveset_real(-x**2 - 2*x + (x + 1)**2 - 1, x) == S.Reals + + +def test_no_sol_rational_extragenous(): + assert solveset_real((x/(x + 1) + 3)**(-2), x) is S.EmptySet + assert solveset_real((x - 1)/(1 + 1/(x - 1)), x) is S.EmptySet + + +def test_solve_polynomial_cv_1a(): + """ + Test for solving on equations that can be converted to + a polynomial equation using the change of variable y -> x**Rational(p, q) + """ + assert solveset_real(sqrt(x) - 1, x) == FiniteSet(1) + assert solveset_real(sqrt(x) - 2, x) == FiniteSet(4) + assert solveset_real(x**Rational(1, 4) - 2, x) == FiniteSet(16) + assert solveset_real(x**Rational(1, 3) - 3, x) == FiniteSet(27) + assert solveset_real(x*(x**(S.One / 3) - 3), x) == \ + FiniteSet(S.Zero, S(27)) + + +def test_solveset_real_rational(): + """Test solveset_real for rational functions""" + x = Symbol('x', real=True) + y = Symbol('y', real=True) + assert solveset_real((x - y**3) / ((y**2)*sqrt(1 - y**2)), x) \ + == FiniteSet(y**3) + # issue 4486 + assert solveset_real(2*x/(x + 2) - 1, x) == FiniteSet(2) + + +def test_solveset_real_log(): + assert solveset_real(log((x-1)*(x+1)), x) == \ + FiniteSet(sqrt(2), -sqrt(2)) + + +def test_poly_gens(): + assert solveset_real(4**(2*(x**2) + 2*x) - 8, x) == \ + FiniteSet(Rational(-3, 2), S.Half) + + +def test_solve_abs(): + n = Dummy('n') + raises(ValueError, lambda: solveset(Abs(x) - 1, x)) + assert solveset(Abs(x) - n, x, S.Reals).dummy_eq( + ConditionSet(x, Contains(n, Interval(0, oo)), {-n, n})) + assert solveset_real(Abs(x) - 2, x) == FiniteSet(-2, 2) + assert solveset_real(Abs(x) + 2, x) is S.EmptySet + assert solveset_real(Abs(x + 3) - 2*Abs(x - 3), x) == \ + FiniteSet(1, 9) + assert solveset_real(2*Abs(x) - Abs(x - 1), x) == \ + FiniteSet(-1, Rational(1, 3)) + + sol = ConditionSet( + x, + And( + Contains(b, Interval(0, oo)), + Contains(a + b, Interval(0, oo)), + Contains(a - b, Interval(0, oo))), + FiniteSet(-a - b - 3, -a + b - 3, a - b - 3, a + b - 3)) + eq = Abs(Abs(x + 3) - a) - b + assert invert_real(eq, 0, x)[1] == sol + reps = {a: 3, b: 1} + eqab = eq.subs(reps) + for si in sol.subs(reps): + assert not eqab.subs(x, si) + assert dumeq(solveset(Eq(sin(Abs(x)), 1), x, domain=S.Reals), Union( + Intersection(Interval(0, oo), Union( + Intersection(ImageSet(Lambda(n, 2*n*pi + 3*pi/2), S.Integers), + Interval(-oo, 0)), + Intersection(ImageSet(Lambda(n, 2*n*pi + pi/2), S.Integers), + Interval(0, oo)))))) + + +def test_issue_9824(): + assert dumeq(solveset(sin(x)**2 - 2*sin(x) + 1, x), ImageSet(Lambda(n, 2*n*pi + pi/2), S.Integers)) + assert dumeq(solveset(cos(x)**2 - 2*cos(x) + 1, x), ImageSet(Lambda(n, 2*n*pi), S.Integers)) + + +def test_issue_9565(): + assert solveset_real(Abs((x - 1)/(x - 5)) <= Rational(1, 3), x) == Interval(-1, 2) + + +def test_issue_10069(): + eq = abs(1/(x - 1)) - 1 > 0 + assert solveset_real(eq, x) == Union( + Interval.open(0, 1), Interval.open(1, 2)) + + +def test_real_imag_splitting(): + a, b = symbols('a b', real=True) + assert solveset_real(sqrt(a**2 - b**2) - 3, a) == \ + FiniteSet(-sqrt(b**2 + 9), sqrt(b**2 + 9)) + assert solveset_real(sqrt(a**2 + b**2) - 3, a) != \ + S.EmptySet + + +def test_units(): + assert solveset_real(1/x - 1/(2*cm), x) == FiniteSet(2*cm) + + +def test_solve_only_exp_1(): + y = Symbol('y', positive=True) + assert solveset_real(exp(x) - y, x) == FiniteSet(log(y)) + assert solveset_real(exp(x) + exp(-x) - 4, x) == \ + FiniteSet(log(-sqrt(3) + 2), log(sqrt(3) + 2)) + assert solveset_real(exp(x) + exp(-x) - y, x) != S.EmptySet + + +def test_atan2(): + # The .inverse() method on atan2 works only if x.is_real is True and the + # second argument is a real constant + assert solveset_real(atan2(x, 2) - pi/3, x) == FiniteSet(2*sqrt(3)) + + +def test_piecewise_solveset(): + eq = Piecewise((x - 2, Gt(x, 2)), (2 - x, True)) - 3 + assert set(solveset_real(eq, x)) == set(FiniteSet(-1, 5)) + + absxm3 = Piecewise( + (x - 3, 0 <= x - 3), + (3 - x, 0 > x - 3)) + y = Symbol('y', positive=True) + assert solveset_real(absxm3 - y, x) == FiniteSet(-y + 3, y + 3) + + f = Piecewise(((x - 2)**2, x >= 0), (0, True)) + assert solveset(f, x, domain=S.Reals) == Union(FiniteSet(2), Interval(-oo, 0, True, True)) + + assert solveset( + Piecewise((x + 1, x > 0), (I, True)) - I, x, S.Reals + ) == Interval(-oo, 0) + + assert solveset(Piecewise((x - 1, Ne(x, I)), (x, True)), x) == FiniteSet(1) + + # issue 19718 + g = Piecewise((1, x > 10), (0, True)) + assert solveset(g > 0, x, S.Reals) == Interval.open(10, oo) + + from sympy.logic.boolalg import BooleanTrue + f = BooleanTrue() + assert solveset(f, x, domain=Interval(-3, 10)) == Interval(-3, 10) + + # issue 20552 + f = Piecewise((0, Eq(x, 0)), (x**2/Abs(x), True)) + g = Piecewise((0, Eq(x, pi)), ((x - pi)/sin(x), True)) + assert solveset(f, x, domain=S.Reals) == FiniteSet(0) + assert solveset(g) == FiniteSet(pi) + + +def test_solveset_complex_polynomial(): + assert solveset_complex(a*x**2 + b*x + c, x) == \ + FiniteSet(-b/(2*a) - sqrt(-4*a*c + b**2)/(2*a), + -b/(2*a) + sqrt(-4*a*c + b**2)/(2*a)) + + assert solveset_complex(x - y**3, y) == FiniteSet( + (-x**Rational(1, 3))/2 + I*sqrt(3)*x**Rational(1, 3)/2, + x**Rational(1, 3), + (-x**Rational(1, 3))/2 - I*sqrt(3)*x**Rational(1, 3)/2) + + assert solveset_complex(x + 1/x - 1, x) == \ + FiniteSet(S.Half + I*sqrt(3)/2, S.Half - I*sqrt(3)/2) + + +def test_sol_zero_complex(): + assert solveset_complex(0, x) is S.Complexes + + +def test_solveset_complex_rational(): + assert solveset_complex((x - 1)*(x - I)/(x - 3), x) == \ + FiniteSet(1, I) + + assert solveset_complex((x - y**3)/((y**2)*sqrt(1 - y**2)), x) == \ + FiniteSet(y**3) + assert solveset_complex(-x**2 - I, x) == \ + FiniteSet(-sqrt(2)/2 + sqrt(2)*I/2, sqrt(2)/2 - sqrt(2)*I/2) + + +def test_solve_quintics(): + skip("This test is too slow") + f = x**5 - 110*x**3 - 55*x**2 + 2310*x + 979 + s = solveset_complex(f, x) + for root in s: + res = f.subs(x, root.n()).n() + assert tn(res, 0) + + f = x**5 + 15*x + 12 + s = solveset_complex(f, x) + for root in s: + res = f.subs(x, root.n()).n() + assert tn(res, 0) + + +def test_solveset_complex_exp(): + assert dumeq(solveset_complex(exp(x) - 1, x), + imageset(Lambda(n, I*2*n*pi), S.Integers)) + assert dumeq(solveset_complex(exp(x) - I, x), + imageset(Lambda(n, I*(2*n*pi + pi/2)), S.Integers)) + assert solveset_complex(1/exp(x), x) == S.EmptySet + assert dumeq(solveset_complex(sinh(x).rewrite(exp), x), + imageset(Lambda(n, n*pi*I), S.Integers)) + + +def test_solveset_real_exp(): + assert solveset(Eq((-2)**x, 4), x, S.Reals) == FiniteSet(2) + assert solveset(Eq(-2**x, 4), x, S.Reals) == S.EmptySet + assert solveset(Eq((-3)**x, 27), x, S.Reals) == S.EmptySet + assert solveset(Eq((-5)**(x+1), 625), x, S.Reals) == FiniteSet(3) + assert solveset(Eq(2**(x-3), -16), x, S.Reals) == S.EmptySet + assert solveset(Eq((-3)**(x - 3), -3**39), x, S.Reals) == FiniteSet(42) + assert solveset(Eq(2**x, y), x, S.Reals) == Intersection(S.Reals, FiniteSet(log(y)/log(2))) + + assert invert_real((-2)**(2*x) - 16, 0, x) == (x, FiniteSet(2)) + + +def test_solve_complex_log(): + assert solveset_complex(log(x), x) == FiniteSet(1) + assert solveset_complex(1 - log(a + 4*x**2), x) == \ + FiniteSet(-sqrt(-a + E)/2, sqrt(-a + E)/2) + + +def test_solve_complex_sqrt(): + assert solveset_complex(sqrt(5*x + 6) - 2 - x, x) == \ + FiniteSet(-S.One, S(2)) + assert solveset_complex(sqrt(5*x + 6) - (2 + 2*I) - x, x) == \ + FiniteSet(-S(2), 3 - 4*I) + assert solveset_complex(4*x*(1 - a * sqrt(x)), x) == \ + FiniteSet(S.Zero, 1 / a ** 2) + + +def test_solveset_complex_tan(): + s = solveset_complex(tan(x).rewrite(exp), x) + assert dumeq(s, imageset(Lambda(n, pi*n), S.Integers) - \ + imageset(Lambda(n, pi*n + pi/2), S.Integers)) + + +@_both_exp_pow +def test_solve_trig(): + assert dumeq(solveset_real(sin(x), x), + Union(imageset(Lambda(n, 2*pi*n), S.Integers), + imageset(Lambda(n, 2*pi*n + pi), S.Integers))) + + assert dumeq(solveset_real(sin(x) - 1, x), + imageset(Lambda(n, 2*pi*n + pi/2), S.Integers)) + + assert dumeq(solveset_real(cos(x), x), + Union(imageset(Lambda(n, 2*pi*n + pi/2), S.Integers), + imageset(Lambda(n, 2*pi*n + pi*Rational(3, 2)), S.Integers))) + + assert dumeq(solveset_real(sin(x) + cos(x), x), + Union(imageset(Lambda(n, 2*n*pi + pi*Rational(3, 4)), S.Integers), + imageset(Lambda(n, 2*n*pi + pi*Rational(7, 4)), S.Integers))) + + assert solveset_real(sin(x)**2 + cos(x)**2, x) == S.EmptySet + + assert dumeq(solveset_complex(cos(x) - S.Half, x), + Union(imageset(Lambda(n, 2*n*pi + pi*Rational(5, 3)), S.Integers), + imageset(Lambda(n, 2*n*pi + pi/3), S.Integers))) + + assert dumeq(solveset(sin(y + a) - sin(y), a, domain=S.Reals), + ConditionSet(a, (S(-1) <= sin(y)) & (sin(y) <= S(1)), Union( + ImageSet(Lambda(n, 2*n*pi - y + asin(sin(y))), S.Integers), + ImageSet(Lambda(n, 2*n*pi - y - asin(sin(y)) + pi), S.Integers)))) + + assert dumeq(solveset_real(sin(2*x)*cos(x) + cos(2*x)*sin(x)-1, x), + ImageSet(Lambda(n, n*pi*Rational(2, 3) + pi/6), S.Integers)) + + assert dumeq(solveset_real(2*tan(x)*sin(x) + 1, x), Union( + ImageSet(Lambda(n, 2*n*pi + atan(sqrt(2)*sqrt(-1 + sqrt(17))/ + (1 - sqrt(17))) + pi), S.Integers), + ImageSet(Lambda(n, 2*n*pi - atan(sqrt(2)*sqrt(-1 + sqrt(17))/ + (1 - sqrt(17))) + pi), S.Integers))) + + assert dumeq(solveset_real(cos(2*x)*cos(4*x) - 1, x), + ImageSet(Lambda(n, n*pi), S.Integers)) + + assert dumeq(solveset(sin(x/10) + Rational(3, 4)), Union( + ImageSet(Lambda(n, 20*n*pi - 10*asin(S(3)/4) + 20*pi), S.Integers), + ImageSet(Lambda(n, 20*n*pi + 10*asin(S(3)/4) + 10*pi), S.Integers))) + + assert dumeq(solveset(cos(x/15) + cos(x/5)), Union( + ImageSet(Lambda(n, 30*n*pi + 15*pi/2), S.Integers), + ImageSet(Lambda(n, 30*n*pi + 45*pi/2), S.Integers), + ImageSet(Lambda(n, 30*n*pi + 75*pi/4), S.Integers), + ImageSet(Lambda(n, 30*n*pi + 45*pi/4), S.Integers), + ImageSet(Lambda(n, 30*n*pi + 105*pi/4), S.Integers), + ImageSet(Lambda(n, 30*n*pi + 15*pi/4), S.Integers))) + + assert dumeq(solveset(sec(sqrt(2)*x/3) + 5), Union( + ImageSet(Lambda(n, 3*sqrt(2)*(2*n*pi - asec(-5))/2), S.Integers), + ImageSet(Lambda(n, 3*sqrt(2)*(2*n*pi + asec(-5))/2), S.Integers))) + + assert dumeq(simplify(solveset(tan(pi*x) - cot(pi/2*x))), Union( + ImageSet(Lambda(n, 4*n + 1), S.Integers), + ImageSet(Lambda(n, 4*n + 3), S.Integers), + ImageSet(Lambda(n, 4*n + Rational(7, 3)), S.Integers), + ImageSet(Lambda(n, 4*n + Rational(5, 3)), S.Integers), + ImageSet(Lambda(n, 4*n + Rational(11, 3)), S.Integers), + ImageSet(Lambda(n, 4*n + Rational(1, 3)), S.Integers))) + + assert dumeq(solveset(cos(9*x)), Union( + ImageSet(Lambda(n, 2*n*pi/9 + pi/18), S.Integers), + ImageSet(Lambda(n, 2*n*pi/9 + pi/6), S.Integers))) + + assert dumeq(solveset(sin(8*x) + cot(12*x), x, S.Reals), Union( + ImageSet(Lambda(n, n*pi/2 + pi/8), S.Integers), + ImageSet(Lambda(n, n*pi/2 + 3*pi/8), S.Integers), + ImageSet(Lambda(n, n*pi/2 + 5*pi/16), S.Integers), + ImageSet(Lambda(n, n*pi/2 + 3*pi/16), S.Integers), + ImageSet(Lambda(n, n*pi/2 + 7*pi/16), S.Integers), + ImageSet(Lambda(n, n*pi/2 + pi/16), S.Integers))) + + # This is the only remaining solveset test that actually ends up being solved + # by _solve_trig2(). All others are handled by the improved _solve_trig1. + assert dumeq(solveset_real(2*cos(x)*cos(2*x) - 1, x), + Union(ImageSet(Lambda(n, 2*n*pi + 2*atan(sqrt(-2*2**Rational(1, 3)*(67 + + 9*sqrt(57))**Rational(2, 3) + 8*2**Rational(2, 3) + 11*(67 + + 9*sqrt(57))**Rational(1, 3))/(3*(67 + 9*sqrt(57))**Rational(1, 6)))), S.Integers), + ImageSet(Lambda(n, 2*n*pi - 2*atan(sqrt(-2*2**Rational(1, 3)*(67 + + 9*sqrt(57))**Rational(2, 3) + 8*2**Rational(2, 3) + 11*(67 + + 9*sqrt(57))**Rational(1, 3))/(3*(67 + 9*sqrt(57))**Rational(1, 6))) + + 2*pi), S.Integers))) + + # issue #16870 + assert dumeq(simplify(solveset(sin(x/180*pi) - S.Half, x, S.Reals)), Union( + ImageSet(Lambda(n, 360*n + 150), S.Integers), + ImageSet(Lambda(n, 360*n + 30), S.Integers))) + + +def test_solve_trig_hyp_by_inversion(): + n = Dummy('n') + assert solveset_real(sin(2*x + 3) - S(1)/2, x).dummy_eq(Union( + ImageSet(Lambda(n, n*pi - S(3)/2 + 13*pi/12), S.Integers), + ImageSet(Lambda(n, n*pi - S(3)/2 + 17*pi/12), S.Integers))) + assert solveset_complex(sin(2*x + 3) - S(1)/2, x).dummy_eq(Union( + ImageSet(Lambda(n, n*pi - S(3)/2 + 13*pi/12), S.Integers), + ImageSet(Lambda(n, n*pi - S(3)/2 + 17*pi/12), S.Integers))) + assert solveset_real(tan(x) - tan(pi/10), x).dummy_eq( + ImageSet(Lambda(n, n*pi + pi/10), S.Integers)) + assert solveset_complex(tan(x) - tan(pi/10), x).dummy_eq( + ImageSet(Lambda(n, n*pi + pi/10), S.Integers)) + + assert solveset_real(3*cosh(2*x) - 5, x) == FiniteSet( + -acosh(S(5)/3)/2, acosh(S(5)/3)/2) + assert solveset_complex(3*cosh(2*x) - 5, x).dummy_eq(Union( + ImageSet(Lambda(n, n*I*pi - acosh(S(5)/3)/2), S.Integers), + ImageSet(Lambda(n, n*I*pi + acosh(S(5)/3)/2), S.Integers))) + assert solveset_real(sinh(x - 3) - 2, x) == FiniteSet( + asinh(2) + 3) + assert solveset_complex(sinh(x - 3) - 2, x).dummy_eq(Union( + ImageSet(Lambda(n, 2*n*I*pi + asinh(2) + 3), S.Integers), + ImageSet(Lambda(n, 2*n*I*pi - asinh(2) + 3 + I*pi), S.Integers))) + + assert solveset_real(cos(sinh(x))-cos(pi/12), x).dummy_eq(Union( + ImageSet(Lambda(n, asinh(2*n*pi + pi/12)), S.Integers), + ImageSet(Lambda(n, asinh(2*n*pi + 23*pi/12)), S.Integers))) + assert solveset(cos(sinh(x))-cos(pi/12), x, Interval(2,3)) == \ + FiniteSet(asinh(23*pi/12), asinh(25*pi/12)) + assert solveset_real(cosh(x**2-1)-2, x) == FiniteSet( + -sqrt(1 + acosh(2)), sqrt(1 + acosh(2))) + + assert solveset_real(sin(x) - 2, x) == S.EmptySet # issue #17334 + assert solveset_real(cos(x) + 2, x) == S.EmptySet + assert solveset_real(sec(x), x) == S.EmptySet + assert solveset_real(csc(x), x) == S.EmptySet + assert solveset_real(cosh(x) + 1, x) == S.EmptySet + assert solveset_real(coth(x), x) == S.EmptySet + assert solveset_real(sech(x) - 2, x) == S.EmptySet + assert solveset_real(sech(x), x) == S.EmptySet + assert solveset_real(tanh(x) + 1, x) == S.EmptySet + assert solveset_complex(tanh(x), 1) == S.EmptySet + assert solveset_complex(coth(x), -1) == S.EmptySet + assert solveset_complex(sech(x), 0) == S.EmptySet + assert solveset_complex(csch(x), 0) == S.EmptySet + + assert solveset_real(abs(csch(x)) - 3, x) == FiniteSet(-acsch(3), acsch(3)) + + assert solveset_real(tanh(x**2 - 1) - exp(-9), x) == FiniteSet( + -sqrt(atanh(exp(-9)) + 1), sqrt(atanh(exp(-9)) + 1)) + + assert solveset_real(coth(log(x)) + 2, x) == FiniteSet(exp(-acoth(2))) + assert solveset_real(coth(exp(x)) + 2, x) == S.EmptySet + + assert solveset_complex(sinh(x) - I/2, x).dummy_eq(Union( + ImageSet(Lambda(n, 2*I*pi*n + 5*I*pi/6), S.Integers), + ImageSet(Lambda(n, 2*I*pi*n + I*pi/6), S.Integers))) + assert solveset_complex(sinh(x/10) + Rational(3, 4), x).dummy_eq(Union( + ImageSet(Lambda(n, 20*n*I*pi - 10*asinh(S(3)/4)), S.Integers), + ImageSet(Lambda(n, 20*n*I*pi + 10*asinh(S(3)/4) + 10*I*pi), S.Integers))) + assert solveset_complex(sech(sqrt(2)*x/3) + 5, x).dummy_eq(Union( + ImageSet(Lambda(n, 3*sqrt(2)*(2*n*I*pi - asech(-5))/2), S.Integers), + ImageSet(Lambda(n, 3*sqrt(2)*(2*n*I*pi + asech(-5))/2), S.Integers))) + assert solveset_complex(cosh(9*x), x).dummy_eq(Union( + ImageSet(Lambda(n, 2*n*I*pi/9 + I*pi/18), S.Integers), + ImageSet(Lambda(n, 2*n*I*pi/9 + I*pi/6), S.Integers))) + + eq = (x**5 -4*x + 1).subs(x, coth(z)) + assert solveset(eq, z, S.Complexes).dummy_eq(Union( + ImageSet(Lambda(n, n*I*pi + acoth(CRootOf(x**5 -4*x + 1, 0))), S.Integers), + ImageSet(Lambda(n, n*I*pi + acoth(CRootOf(x**5 -4*x + 1, 1))), S.Integers), + ImageSet(Lambda(n, n*I*pi + acoth(CRootOf(x**5 -4*x + 1, 2))), S.Integers), + ImageSet(Lambda(n, n*I*pi + acoth(CRootOf(x**5 -4*x + 1, 3))), S.Integers), + ImageSet(Lambda(n, n*I*pi + acoth(CRootOf(x**5 -4*x + 1, 4))), S.Integers))) + assert solveset(eq, z, S.Reals) == FiniteSet( + acoth(CRootOf(x**5 - 4*x + 1, 0)), acoth(CRootOf(x**5 - 4*x + 1, 2))) + + eq = ((x-sqrt(3)/2)*(x+2)).expand().subs(x, cos(x)) + assert solveset(eq, x, S.Complexes).dummy_eq(Union( + ImageSet(Lambda(n, 2*n*pi - acos(-2)), S.Integers), + ImageSet(Lambda(n, 2*n*pi + acos(-2)), S.Integers), + ImageSet(Lambda(n, 2*n*pi + pi/6), S.Integers), + ImageSet(Lambda(n, 2*n*pi + 11*pi/6), S.Integers))) + assert solveset(eq, x, S.Reals).dummy_eq(Union( + ImageSet(Lambda(n, 2*n*pi + pi/6), S.Integers), + ImageSet(Lambda(n, 2*n*pi + 11*pi/6), S.Integers))) + + assert solveset((1+sec(sqrt(3)*x+4)**2)/(1-sec(sqrt(3)*x+4))).dummy_eq(Union( + ImageSet(Lambda(n, sqrt(3)*(2*n*pi - 4 - asec(I))/3), S.Integers), + ImageSet(Lambda(n, sqrt(3)*(2*n*pi - 4 + asec(I))/3), S.Integers), + ImageSet(Lambda(n, sqrt(3)*(2*n*pi - 4 - asec(-I))/3), S.Integers), + ImageSet(Lambda(n, sqrt(3)*(2*n*pi - 4 + asec(-I))/3), S.Integers))) + + assert all_close(solveset(tan(3.14*x)**(S(3)/2)-5.678, x, Interval(0, 3)), + FiniteSet(0.403301114561067, 0.403301114561067 + 0.318471337579618*pi, + 0.403301114561067 + 0.636942675159236*pi)) + + +def test_old_trig_issues(): + # issues #9606 / #9531: + assert solveset(sinh(x), x, S.Reals) == FiniteSet(0) + assert solveset(sinh(x), x, S.Complexes).dummy_eq(Union( + ImageSet(Lambda(n, 2*n*I*pi), S.Integers), + ImageSet(Lambda(n, 2*n*I*pi + I*pi), S.Integers))) + + # issues #11218 / #18427 + assert solveset(sin(pi*x), x, S.Reals).dummy_eq(Union( + ImageSet(Lambda(n, (2*n*pi + pi)/pi), S.Integers), + ImageSet(Lambda(n, 2*n), S.Integers))) + assert solveset(sin(pi*x), x).dummy_eq(Union( + ImageSet(Lambda(n, (2*n*pi + pi)/pi), S.Integers), + ImageSet(Lambda(n, 2*n), S.Integers))) + + # issue #17543 + assert solveset(I*cot(8*x - 8*E), x).dummy_eq( + ImageSet(Lambda(n, pi*n/8 - 13*pi/16 + E), S.Integers)) + + # issue #20798 + assert all_close(solveset(cos(2*x) - 0.5, x, Interval(0, 2*pi)), FiniteSet( + 0.523598775598299, -0.523598775598299 + pi, + -0.523598775598299 + 2*pi, 0.523598775598299 + pi)) + sol = Union(ImageSet(Lambda(n, n*pi - 0.523598775598299), S.Integers), + ImageSet(Lambda(n, n*pi + 0.523598775598299), S.Integers)) + ret = solveset(cos(2*x) - 0.5, x, S.Reals) + # replace Dummy n by the regular Symbol n to allow all_close comparison. + ret = ret.subs(ret.atoms(Dummy).pop(), n) + assert all_close(ret, sol) + ret = solveset(cos(2*x) - 0.5, x, S.Complexes) + ret = ret.subs(ret.atoms(Dummy).pop(), n) + assert all_close(ret, sol) + + # issue #21296 / #17667 + assert solveset(tan(x)-sqrt(2), x, Interval(0, pi/2)) == FiniteSet(atan(sqrt(2))) + assert solveset(tan(x)-pi, x, Interval(0, pi/2)) == FiniteSet(atan(pi)) + + # issue #17667 + # not yet working properly: + # solveset(cos(x)-y, x, Interval(0, pi)) + assert solveset(cos(x)-y, x, S.Reals).dummy_eq( + ConditionSet(x,(S(-1) <= y) & (y <= S(1)), Union( + ImageSet(Lambda(n, 2*n*pi - acos(y)), S.Integers), + ImageSet(Lambda(n, 2*n*pi + acos(y)), S.Integers)))) + + # issue #17579 + # Valid result, but the intersection could potentially be simplified. + assert solveset(sin(log(x)), x, Interval(0,1, True, False)).dummy_eq( + Union(Intersection(ImageSet(Lambda(n, exp(2*n*pi)), S.Integers), Interval.Lopen(0, 1)), + Intersection(ImageSet(Lambda(n, exp(2*n*pi + pi)), S.Integers), Interval.Lopen(0, 1)))) + + # issue #17334 + assert solveset(sin(x) - sin(1), x, S.Reals).dummy_eq(Union( + ImageSet(Lambda(n, 2*n*pi + 1), S.Integers), + ImageSet(Lambda(n, 2*n*pi - 1 + pi), S.Integers))) + assert solveset(sin(x) - sqrt(5)/3, x, S.Reals).dummy_eq(Union( + ImageSet(Lambda(n, 2*n*pi + asin(sqrt(5)/3)), S.Integers), + ImageSet(Lambda(n, 2*n*pi - asin(sqrt(5)/3) + pi), S.Integers))) + assert solveset(sinh(x)-cosh(2), x, S.Reals) == FiniteSet(asinh(cosh(2))) + + # issue 9825 + assert solveset(Eq(tan(x), y), x, domain=S.Reals).dummy_eq( + ConditionSet(x, (-oo < y) & (y < oo), + ImageSet(Lambda(n, n*pi + atan(y)), S.Integers))) + r = Symbol('r', real=True) + assert solveset(Eq(tan(x), r), x, domain=S.Reals).dummy_eq( + ImageSet(Lambda(n, n*pi + atan(r)), S.Integers)) + + +def test_solve_hyperbolic(): + # actual solver: _solve_trig1 + n = Dummy('n') + assert solveset(sinh(x) + cosh(x), x) == S.EmptySet + assert solveset(sinh(x) + cos(x), x) == ConditionSet(x, + Eq(cos(x) + sinh(x), 0), S.Complexes) + assert solveset_real(sinh(x) + sech(x), x) == FiniteSet( + log(sqrt(sqrt(5) - 2))) + assert solveset_real(cosh(2*x) + 2*sinh(x) - 5, x) == FiniteSet( + log(-2 + sqrt(5)), log(1 + sqrt(2))) + assert solveset_real((coth(x) + sinh(2*x))/cosh(x) - 3, x) == FiniteSet( + log(S.Half + sqrt(5)/2), log(1 + sqrt(2))) + assert solveset_real(cosh(x)*sinh(x) - 2, x) == FiniteSet( + log(4 + sqrt(17))/2) + assert solveset_real(sinh(x) + tanh(x) - 1, x) == FiniteSet( + log(sqrt(2)/2 + sqrt(-S(1)/2 + sqrt(2)))) + + assert dumeq(solveset_complex(sinh(x) + sech(x), x), Union( + ImageSet(Lambda(n, 2*n*I*pi + log(sqrt(-2 + sqrt(5)))), S.Integers), + ImageSet(Lambda(n, I*(2*n*pi + pi/2) + log(sqrt(2 + sqrt(5)))), S.Integers), + ImageSet(Lambda(n, I*(2*n*pi + pi) + log(sqrt(-2 + sqrt(5)))), S.Integers), + ImageSet(Lambda(n, I*(2*n*pi - pi/2) + log(sqrt(2 + sqrt(5)))), S.Integers))) + + assert dumeq(solveset(cosh(x/15) + cosh(x/5)), Union( + ImageSet(Lambda(n, 15*I*(2*n*pi + pi/2)), S.Integers), + ImageSet(Lambda(n, 15*I*(2*n*pi - pi/2)), S.Integers), + ImageSet(Lambda(n, 15*I*(2*n*pi - 3*pi/4)), S.Integers), + ImageSet(Lambda(n, 15*I*(2*n*pi + 3*pi/4)), S.Integers), + ImageSet(Lambda(n, 15*I*(2*n*pi - pi/4)), S.Integers), + ImageSet(Lambda(n, 15*I*(2*n*pi + pi/4)), S.Integers))) + + assert dumeq(solveset(tanh(pi*x) - coth(pi/2*x)), Union( + ImageSet(Lambda(n, 2*I*(2*n*pi + pi/2)/pi), S.Integers), + ImageSet(Lambda(n, 2*I*(2*n*pi - pi/2)/pi), S.Integers))) + + # issues #18490 / #19489 + assert solveset(cosh(x) + cosh(3*x) - cosh(5*x), x, S.Reals + ).dummy_eq(ConditionSet(x, + Eq(cosh(x) + cosh(3*x) - cosh(5*x), 0), S.Reals)) + assert solveset(sinh(8*x) + coth(12*x)).dummy_eq( + ConditionSet(x, Eq(sinh(8*x) + coth(12*x), 0), S.Complexes)) + + +def test_solve_trig_hyp_symbolic(): + # actual solver: invert_trig_hyp + assert dumeq(solveset(sin(a*x), x), ConditionSet(x, Ne(a, 0), Union( + ImageSet(Lambda(n, (2*n*pi + pi)/a), S.Integers), + ImageSet(Lambda(n, 2*n*pi/a), S.Integers)))) + + assert dumeq(solveset(cosh(x/a), x), ConditionSet(x, Ne(a, 0), Union( + ImageSet(Lambda(n, a*(2*n*I*pi + I*pi/2)), S.Integers), + ImageSet(Lambda(n, a*(2*n*I*pi + 3*I*pi/2)), S.Integers)))) + + assert dumeq(solveset(sin(2*sqrt(3)/3*a**2/(b*pi)*x) + + cos(4*sqrt(3)/3*a**2/(b*pi)*x), x), + ConditionSet(x, Ne(b, 0) & Ne(a**2, 0), Union( + ImageSet(Lambda(n, sqrt(3)*pi*b*(2*n*pi + pi/2)/(2*a**2)), S.Integers), + ImageSet(Lambda(n, sqrt(3)*pi*b*(2*n*pi - 5*pi/6)/(2*a**2)), S.Integers), + ImageSet(Lambda(n, sqrt(3)*pi*b*(2*n*pi - pi/6)/(2*a**2)), S.Integers)))) + + assert dumeq(solveset(cosh((a**2 + 1)*x) - 3, x), ConditionSet( + x, Ne(a**2 + 1, 0), Union( + ImageSet(Lambda(n, (2*n*I*pi - acosh(3))/(a**2 + 1)), S.Integers), + ImageSet(Lambda(n, (2*n*I*pi + acosh(3))/(a**2 + 1)), S.Integers)))) + + ar = Symbol('ar', real=True) + assert solveset(cosh((ar**2 + 1)*x) - 2, x, S.Reals) == FiniteSet( + -acosh(2)/(ar**2 + 1), acosh(2)/(ar**2 + 1)) + + # actual solver: _solve_trig1 + assert dumeq(simplify(solveset(cot((1 + I)*x) - cot((3 + 3*I)*x), x)), Union( + ImageSet(Lambda(n, pi*(1 - I)*(4*n + 1)/4), S.Integers), + ImageSet(Lambda(n, pi*(1 - I)*(4*n - 1)/4), S.Integers))) + + +def test_issue_9616(): + assert dumeq(solveset(sinh(x) + tanh(x) - 1, x), Union( + ImageSet(Lambda(n, 2*n*I*pi + log(sqrt(2)/2 + sqrt(-S.Half + sqrt(2)))), S.Integers), + ImageSet(Lambda(n, I*(2*n*pi - atan(sqrt(2)*sqrt(S.Half + sqrt(2))) + pi) + + log(sqrt(1 + sqrt(2)))), S.Integers), + ImageSet(Lambda(n, I*(2*n*pi + pi) + log(-sqrt(2)/2 + sqrt(-S.Half + sqrt(2)))), S.Integers), + ImageSet(Lambda(n, I*(2*n*pi - pi + atan(sqrt(2)*sqrt(S.Half + sqrt(2)))) + + log(sqrt(1 + sqrt(2)))), S.Integers))) + f1 = (sinh(x)).rewrite(exp) + f2 = (tanh(x)).rewrite(exp) + assert dumeq(solveset(f1 + f2 - 1, x), Union( + Complement(ImageSet( + Lambda(n, I*(2*n*pi + pi) + log(-sqrt(2)/2 + sqrt(-S.Half + sqrt(2)))), S.Integers), + ImageSet(Lambda(n, I*(2*n*pi + pi)/2), S.Integers)), + Complement(ImageSet(Lambda(n, I*(2*n*pi - pi + atan(sqrt(2)*sqrt(S.Half + sqrt(2)))) + + log(sqrt(1 + sqrt(2)))), S.Integers), + ImageSet(Lambda(n, I*(2*n*pi + pi)/2), S.Integers)), + Complement(ImageSet(Lambda(n, I*(2*n*pi - atan(sqrt(2)*sqrt(S.Half + sqrt(2))) + pi) + + log(sqrt(1 + sqrt(2)))), S.Integers), + ImageSet(Lambda(n, I*(2*n*pi + pi)/2), S.Integers)), + Complement( + ImageSet(Lambda(n, 2*n*I*pi + log(sqrt(2)/2 + sqrt(-S.Half + sqrt(2)))), S.Integers), + ImageSet(Lambda(n, I*(2*n*pi + pi)/2), S.Integers)))) + + +def test_solve_invalid_sol(): + assert 0 not in solveset_real(sin(x)/x, x) + assert 0 not in solveset_complex((exp(x) - 1)/x, x) + + +@XFAIL +def test_solve_trig_simplified(): + n = Dummy('n') + assert dumeq(solveset_real(sin(x), x), + imageset(Lambda(n, n*pi), S.Integers)) + + assert dumeq(solveset_real(cos(x), x), + imageset(Lambda(n, n*pi + pi/2), S.Integers)) + + assert dumeq(solveset_real(cos(x) + sin(x), x), + imageset(Lambda(n, n*pi - pi/4), S.Integers)) + + +@XFAIL +def test_solve_lambert(): + assert solveset_real(x*exp(x) - 1, x) == FiniteSet(LambertW(1)) + assert solveset_real(exp(x) + x, x) == FiniteSet(-LambertW(1)) + assert solveset_real(x + 2**x, x) == \ + FiniteSet(-LambertW(log(2))/log(2)) + + # issue 4739 + ans = solveset_real(3*x + 5 + 2**(-5*x + 3), x) + assert ans == FiniteSet(Rational(-5, 3) + + LambertW(-10240*2**Rational(1, 3)*log(2)/3)/(5*log(2))) + + eq = 2*(3*x + 4)**5 - 6*7**(3*x + 9) + result = solveset_real(eq, x) + ans = FiniteSet((log(2401) + + 5*LambertW(-log(7**(7*3**Rational(1, 5)/5))))/(3*log(7))/-1) + assert result == ans + assert solveset_real(eq.expand(), x) == result + + assert solveset_real(5*x - 1 + 3*exp(2 - 7*x), x) == \ + FiniteSet(Rational(1, 5) + LambertW(-21*exp(Rational(3, 5))/5)/7) + + assert solveset_real(2*x + 5 + log(3*x - 2), x) == \ + FiniteSet(Rational(2, 3) + LambertW(2*exp(Rational(-19, 3))/3)/2) + + assert solveset_real(3*x + log(4*x), x) == \ + FiniteSet(LambertW(Rational(3, 4))/3) + + assert solveset_real(x**x - 2) == FiniteSet(exp(LambertW(log(2)))) + + a = Symbol('a') + assert solveset_real(-a*x + 2*x*log(x), x) == FiniteSet(exp(a/2)) + a = Symbol('a', real=True) + assert solveset_real(a/x + exp(x/2), x) == \ + FiniteSet(2*LambertW(-a/2)) + assert solveset_real((a/x + exp(x/2)).diff(x), x) == \ + FiniteSet(4*LambertW(sqrt(2)*sqrt(a)/4)) + + # coverage test + assert solveset_real(tanh(x + 3)*tanh(x - 3) - 1, x) is S.EmptySet + + assert solveset_real((x**2 - 2*x + 1).subs(x, log(x) + 3*x), x) == \ + FiniteSet(LambertW(3*S.Exp1)/3) + assert solveset_real((x**2 - 2*x + 1).subs(x, (log(x) + 3*x)**2 - 1), x) == \ + FiniteSet(LambertW(3*exp(-sqrt(2)))/3, LambertW(3*exp(sqrt(2)))/3) + assert solveset_real((x**2 - 2*x - 2).subs(x, log(x) + 3*x), x) == \ + FiniteSet(LambertW(3*exp(1 + sqrt(3)))/3, LambertW(3*exp(-sqrt(3) + 1))/3) + assert solveset_real(x*log(x) + 3*x + 1, x) == \ + FiniteSet(exp(-3 + LambertW(-exp(3)))) + eq = (x*exp(x) - 3).subs(x, x*exp(x)) + assert solveset_real(eq, x) == \ + FiniteSet(LambertW(3*exp(-LambertW(3)))) + + assert solveset_real(3*log(a**(3*x + 5)) + a**(3*x + 5), x) == \ + FiniteSet(-((log(a**5) + LambertW(Rational(1, 3)))/(3*log(a)))) + p = symbols('p', positive=True) + assert solveset_real(3*log(p**(3*x + 5)) + p**(3*x + 5), x) == \ + FiniteSet( + log((-3**Rational(1, 3) - 3**Rational(5, 6)*I)*LambertW(Rational(1, 3))**Rational(1, 3)/(2*p**Rational(5, 3)))/log(p), + log((-3**Rational(1, 3) + 3**Rational(5, 6)*I)*LambertW(Rational(1, 3))**Rational(1, 3)/(2*p**Rational(5, 3)))/log(p), + log((3*LambertW(Rational(1, 3))/p**5)**(1/(3*log(p)))),) # checked numerically + # check collection + b = Symbol('b') + eq = 3*log(a**(3*x + 5)) + b*log(a**(3*x + 5)) + a**(3*x + 5) + assert solveset_real(eq, x) == FiniteSet( + -((log(a**5) + LambertW(1/(b + 3)))/(3*log(a)))) + + # issue 4271 + assert solveset_real((a/x + exp(x/2)).diff(x, 2), x) == FiniteSet( + 6*LambertW((-1)**Rational(1, 3)*a**Rational(1, 3)/3)) + + assert solveset_real(x**3 - 3**x, x) == \ + FiniteSet(-3/log(3)*LambertW(-log(3)/3)) + assert solveset_real(3**cos(x) - cos(x)**3) == FiniteSet( + acos(-3*LambertW(-log(3)/3)/log(3))) + + assert solveset_real(x**2 - 2**x, x) == \ + solveset_real(-x**2 + 2**x, x) + + assert solveset_real(3*log(x) - x*log(3)) == FiniteSet( + -3*LambertW(-log(3)/3)/log(3), + -3*LambertW(-log(3)/3, -1)/log(3)) + + assert solveset_real(LambertW(2*x) - y) == FiniteSet( + y*exp(y)/2) + + +@XFAIL +def test_other_lambert(): + a = Rational(6, 5) + assert solveset_real(x**a - a**x, x) == FiniteSet( + a, -a*LambertW(-log(a)/a)/log(a)) + + +@_both_exp_pow +def test_solveset(): + f = Function('f') + raises(ValueError, lambda: solveset(x + y)) + assert solveset(x, 1) == S.EmptySet + assert solveset(f(1)**2 + y + 1, f(1) + ) == FiniteSet(-sqrt(-y - 1), sqrt(-y - 1)) + assert solveset(f(1)**2 - 1, f(1), S.Reals) == FiniteSet(-1, 1) + assert solveset(f(1)**2 + 1, f(1)) == FiniteSet(-I, I) + assert solveset(x - 1, 1) == FiniteSet(x) + assert solveset(sin(x) - cos(x), sin(x)) == FiniteSet(cos(x)) + + assert solveset(0, domain=S.Reals) == S.Reals + assert solveset(1) == S.EmptySet + assert solveset(True, domain=S.Reals) == S.Reals # issue 10197 + assert solveset(False, domain=S.Reals) == S.EmptySet + + assert solveset(exp(x) - 1, domain=S.Reals) == FiniteSet(0) + assert solveset(exp(x) - 1, x, S.Reals) == FiniteSet(0) + assert solveset(Eq(exp(x), 1), x, S.Reals) == FiniteSet(0) + assert solveset(exp(x) - 1, exp(x), S.Reals) == FiniteSet(1) + A = Indexed('A', x) + assert solveset(A - 1, A, S.Reals) == FiniteSet(1) + + assert solveset(x - 1 >= 0, x, S.Reals) == Interval(1, oo) + assert solveset(exp(x) - 1 >= 0, x, S.Reals) == Interval(0, oo) + + assert dumeq(solveset(exp(x) - 1, x), imageset(Lambda(n, 2*I*pi*n), S.Integers)) + assert dumeq(solveset(Eq(exp(x), 1), x), imageset(Lambda(n, 2*I*pi*n), + S.Integers)) + # issue 13825 + assert solveset(x**2 + f(0) + 1, x) == {-sqrt(-f(0) - 1), sqrt(-f(0) - 1)} + + # issue 19977 + assert solveset(atan(log(x)) > 0, x, domain=Interval.open(0, oo)) == Interval.open(1, oo) + + +@_both_exp_pow +def test_multi_exp(): + k1, k2, k3 = symbols('k1, k2, k3') + assert dumeq(solveset(exp(exp(x)) - 5, x),\ + imageset(Lambda(((k1, n),), I*(2*k1*pi + arg(2*n*I*pi + log(5))) + log(Abs(2*n*I*pi + log(5)))),\ + ProductSet(S.Integers, S.Integers))) + assert dumeq(solveset((d*exp(exp(a*x + b)) + c), x),\ + imageset(Lambda(x, (-b + x)/a), ImageSet(Lambda(((k1, n),), \ + I*(2*k1*pi + arg(I*(2*n*pi + arg(-c/d)) + log(Abs(c/d)))) + log(Abs(I*(2*n*pi + arg(-c/d)) + log(Abs(c/d))))), \ + ProductSet(S.Integers, S.Integers)))) + + assert dumeq(solveset((d*exp(exp(exp(a*x + b))) + c), x),\ + imageset(Lambda(x, (-b + x)/a), ImageSet(Lambda(((k2, k1, n),), \ + I*(2*k2*pi + arg(I*(2*k1*pi + arg(I*(2*n*pi + arg(-c/d)) + log(Abs(c/d)))) + \ + log(Abs(I*(2*n*pi + arg(-c/d)) + log(Abs(c/d)))))) + log(Abs(I*(2*k1*pi + arg(I*(2*n*pi + arg(-c/d)) + \ + log(Abs(c/d)))) + log(Abs(I*(2*n*pi + arg(-c/d)) + log(Abs(c/d))))))), \ + ProductSet(S.Integers, S.Integers, S.Integers)))) + + assert dumeq(solveset((d*exp(exp(exp(exp(a*x + b)))) + c), x),\ + ImageSet(Lambda(x, (-b + x)/a), ImageSet(Lambda(((k3, k2, k1, n),), \ + I*(2*k3*pi + arg(I*(2*k2*pi + arg(I*(2*k1*pi + arg(I*(2*n*pi + arg(-c/d)) + log(Abs(c/d)))) + \ + log(Abs(I*(2*n*pi + arg(-c/d)) + log(Abs(c/d)))))) + log(Abs(I*(2*k1*pi + arg(I*(2*n*pi + arg(-c/d)) + \ + log(Abs(c/d)))) + log(Abs(I*(2*n*pi + arg(-c/d)) + log(Abs(c/d)))))))) + log(Abs(I*(2*k2*pi + \ + arg(I*(2*k1*pi + arg(I*(2*n*pi + arg(-c/d)) + log(Abs(c/d)))) + log(Abs(I*(2*n*pi + arg(-c/d)) + log(Abs(c/d)))))) + \ + log(Abs(I*(2*k1*pi + arg(I*(2*n*pi + arg(-c/d)) + log(Abs(c/d)))) + log(Abs(I*(2*n*pi + arg(-c/d)) + log(Abs(c/d))))))))), \ + ProductSet(S.Integers, S.Integers, S.Integers, S.Integers)))) + + +def test__solveset_multi(): + from sympy.solvers.solveset import _solveset_multi + from sympy.sets import Reals + + # Basic univariate case: + assert _solveset_multi([x**2-1], [x], [S.Reals]) == FiniteSet((1,), (-1,)) + + # Linear systems of two equations + assert _solveset_multi([x+y, x+1], [x, y], [Reals, Reals]) == FiniteSet((-1, 1)) + assert _solveset_multi([x+y, x+1], [y, x], [Reals, Reals]) == FiniteSet((1, -1)) + assert _solveset_multi([x+y, x-y-1], [x, y], [Reals, Reals]) == FiniteSet((S(1)/2, -S(1)/2)) + assert _solveset_multi([x-1, y-2], [x, y], [Reals, Reals]) == FiniteSet((1, 2)) + # assert dumeq(_solveset_multi([x+y], [x, y], [Reals, Reals]), ImageSet(Lambda(x, (x, -x)), Reals)) + assert dumeq(_solveset_multi([x+y], [x, y], [Reals, Reals]), Union( + ImageSet(Lambda(((x,),), (x, -x)), ProductSet(Reals)), + ImageSet(Lambda(((y,),), (-y, y)), ProductSet(Reals)))) + assert _solveset_multi([x+y, x+y+1], [x, y], [Reals, Reals]) == S.EmptySet + assert _solveset_multi([x+y, x-y, x-1], [x, y], [Reals, Reals]) == S.EmptySet + assert _solveset_multi([x+y, x-y, x-1], [y, x], [Reals, Reals]) == S.EmptySet + + # Systems of three equations: + assert _solveset_multi([x+y+z-1, x+y-z-2, x-y-z-3], [x, y, z], [Reals, + Reals, Reals]) == FiniteSet((2, -S.Half, -S.Half)) + + # Nonlinear systems: + from sympy.abc import theta + assert _solveset_multi([x**2+y**2-2, x+y], [x, y], [Reals, Reals]) == FiniteSet((-1, 1), (1, -1)) + assert _solveset_multi([x**2-1, y], [x, y], [Reals, Reals]) == FiniteSet((1, 0), (-1, 0)) + #assert _solveset_multi([x**2-y**2], [x, y], [Reals, Reals]) == Union( + # ImageSet(Lambda(x, (x, -x)), Reals), ImageSet(Lambda(x, (x, x)), Reals)) + assert dumeq(_solveset_multi([x**2-y**2], [x, y], [Reals, Reals]), Union( + ImageSet(Lambda(((x,),), (x, -Abs(x))), ProductSet(Reals)), + ImageSet(Lambda(((x,),), (x, Abs(x))), ProductSet(Reals)), + ImageSet(Lambda(((y,),), (-Abs(y), y)), ProductSet(Reals)), + ImageSet(Lambda(((y,),), (Abs(y), y)), ProductSet(Reals)))) + assert _solveset_multi([r*cos(theta)-1, r*sin(theta)], [theta, r], + [Interval(0, pi), Interval(-1, 1)]) == FiniteSet((0, 1), (pi, -1)) + assert _solveset_multi([r*cos(theta)-1, r*sin(theta)], [r, theta], + [Interval(0, 1), Interval(0, pi)]) == FiniteSet((1, 0)) + assert _solveset_multi([r*cos(theta)-r, r*sin(theta)], [r, theta], + [Interval(0, 1), Interval(0, pi)]) == Union( + ImageSet(Lambda(((r,),), (r, 0)), + ImageSet(Lambda(r, (r,)), Interval(0, 1))), + ImageSet(Lambda(((theta,),), (0, theta)), + ImageSet(Lambda(theta, (theta,)), Interval(0, pi)))) + + +def test_conditionset(): + assert solveset(Eq(sin(x)**2 + cos(x)**2, 1), x, domain=S.Reals + ) is S.Reals + + assert solveset(Eq(x**2 + x*sin(x), 1), x, domain=S.Reals + ).dummy_eq(ConditionSet(x, Eq(x**2 + x*sin(x) - 1, 0), S.Reals)) + + assert dumeq(solveset(Eq(-I*(exp(I*x) - exp(-I*x))/2, 1), x + ), imageset(Lambda(n, 2*n*pi + pi/2), S.Integers)) + + assert solveset(x + sin(x) > 1, x, domain=S.Reals + ).dummy_eq(ConditionSet(x, x + sin(x) > 1, S.Reals)) + + assert solveset(Eq(sin(Abs(x)), x), x, domain=S.Reals + ).dummy_eq(ConditionSet(x, Eq(-x + sin(Abs(x)), 0), S.Reals)) + + assert solveset(y**x-z, x, S.Reals + ).dummy_eq(ConditionSet(x, Eq(y**x - z, 0), S.Reals)) + + +@XFAIL +def test_conditionset_equality(): + ''' Checking equality of different representations of ConditionSet''' + assert solveset(Eq(tan(x), y), x) == ConditionSet(x, Eq(tan(x), y), S.Complexes) + + +def test_solveset_domain(): + assert solveset(x**2 - x - 6, x, Interval(0, oo)) == FiniteSet(3) + assert solveset(x**2 - 1, x, Interval(0, oo)) == FiniteSet(1) + assert solveset(x**4 - 16, x, Interval(0, 10)) == FiniteSet(2) + + +def test_improve_coverage(): + solution = solveset(exp(x) + sin(x), x, S.Reals) + unsolved_object = ConditionSet(x, Eq(exp(x) + sin(x), 0), S.Reals) + assert solution.dummy_eq(unsolved_object) + + +def test_issue_9522(): + expr1 = Eq(1/(x**2 - 4) + x, 1/(x**2 - 4) + 2) + expr2 = Eq(1/x + x, 1/x) + + assert solveset(expr1, x, S.Reals) is S.EmptySet + assert solveset(expr2, x, S.Reals) is S.EmptySet + + +def test_solvify(): + assert solvify(x**2 + 10, x, S.Reals) == [] + assert solvify(x**3 + 1, x, S.Complexes) == [-1, S.Half - sqrt(3)*I/2, + S.Half + sqrt(3)*I/2] + assert solvify(log(x), x, S.Reals) == [1] + assert solvify(cos(x), x, S.Reals) == [pi/2, pi*Rational(3, 2)] + assert solvify(sin(x) + 1, x, S.Reals) == [pi*Rational(3, 2)] + raises(NotImplementedError, lambda: solvify(sin(exp(x)), x, S.Complexes)) + + +def test_solvify_piecewise(): + p1 = Piecewise((0, x < -1), (x**2, x <= 1), (log(x), True)) + p2 = Piecewise((0, x < -10), (x**2 + 5*x - 6, x >= -9)) + p3 = Piecewise((0, Eq(x, 0)), (x**2/Abs(x), True)) + p4 = Piecewise((0, Eq(x, pi)), ((x - pi)/sin(x), True)) + + # issue 21079 + assert solvify(p1, x, S.Reals) == [0] + assert solvify(p2, x, S.Reals) == [-6, 1] + assert solvify(p3, x, S.Reals) == [0] + assert solvify(p4, x, S.Reals) == [pi] + + +def test_abs_invert_solvify(): + + x = Symbol('x',positive=True) + assert solvify(sin(Abs(x)), x, S.Reals) == [0, pi] + x = Symbol('x') + assert solvify(sin(Abs(x)), x, S.Reals) is None + + +def test_linear_eq_to_matrix(): + assert linear_eq_to_matrix(0, x) == (Matrix([[0]]), Matrix([[0]])) + assert linear_eq_to_matrix(1, x) == (Matrix([[0]]), Matrix([[-1]])) + + # integer coefficients + eqns1 = [2*x + y - 2*z - 3, x - y - z, x + y + 3*z - 12] + eqns2 = [Eq(3*x + 2*y - z, 1), Eq(2*x - 2*y + 4*z, -2), -2*x + y - 2*z] + + A, B = linear_eq_to_matrix(eqns1, x, y, z) + assert A == Matrix([[2, 1, -2], [1, -1, -1], [1, 1, 3]]) + assert B == Matrix([[3], [0], [12]]) + + A, B = linear_eq_to_matrix(eqns2, x, y, z) + assert A == Matrix([[3, 2, -1], [2, -2, 4], [-2, 1, -2]]) + assert B == Matrix([[1], [-2], [0]]) + + # Pure symbolic coefficients + eqns3 = [a*b*x + b*y + c*z - d, e*x + d*x + f*y + g*z - h, i*x + j*y + k*z - l] + A, B = linear_eq_to_matrix(eqns3, x, y, z) + assert A == Matrix([[a*b, b, c], [d + e, f, g], [i, j, k]]) + assert B == Matrix([[d], [h], [l]]) + + # raise Errors if + # 1) no symbols are given + raises(ValueError, lambda: linear_eq_to_matrix(eqns3)) + # 2) there are duplicates + raises(ValueError, lambda: linear_eq_to_matrix(eqns3, [x, x, y])) + # 3) a nonlinear term is detected in the original expression + raises(NonlinearError, lambda: linear_eq_to_matrix(Eq(1/x + x, 1/x), [x])) + raises(NonlinearError, lambda: linear_eq_to_matrix([x**2], [x])) + raises(NonlinearError, lambda: linear_eq_to_matrix([x*y], [x, y])) + # 4) Eq being used to represent equations autoevaluates + # (use unevaluated Eq instead) + raises(ValueError, lambda: linear_eq_to_matrix(Eq(x, x), x)) + raises(ValueError, lambda: linear_eq_to_matrix(Eq(x, x + 1), x)) + + + # if non-symbols are passed, the user is responsible for interpreting + assert linear_eq_to_matrix([x], [1/x]) == (Matrix([[0]]), Matrix([[-x]])) + + # issue 15195 + assert linear_eq_to_matrix(x + y*(z*(3*x + 2) + 3), x) == ( + Matrix([[3*y*z + 1]]), Matrix([[-y*(2*z + 3)]])) + assert linear_eq_to_matrix(Matrix( + [[a*x + b*y - 7], [5*x + 6*y - c]]), x, y) == ( + Matrix([[a, b], [5, 6]]), Matrix([[7], [c]])) + + # issue 15312 + assert linear_eq_to_matrix(Eq(x + 2, 1), x) == ( + Matrix([[1]]), Matrix([[-1]])) + + # issue 25423 + raises(TypeError, lambda: linear_eq_to_matrix([], {x, y})) + raises(TypeError, lambda: linear_eq_to_matrix([x + y], {x, y})) + raises(ValueError, lambda: linear_eq_to_matrix({x + y}, (x, y))) + + +def test_issue_16577(): + assert linear_eq_to_matrix(Eq(a*(2*x + 3*y) + 4*y, 5), x, y) == ( + Matrix([[2*a, 3*a + 4]]), Matrix([[5]])) + + +def test_issue_10085(): + assert invert_real(exp(x),0,x) == (x, S.EmptySet) + + +def test_linsolve(): + x1, x2, x3, x4 = symbols('x1, x2, x3, x4') + + # Test for different input forms + + M = Matrix([[1, 2, 1, 1, 7], [1, 2, 2, -1, 12], [2, 4, 0, 6, 4]]) + system1 = A, B = M[:, :-1], M[:, -1] + Eqns = [x1 + 2*x2 + x3 + x4 - 7, x1 + 2*x2 + 2*x3 - x4 - 12, + 2*x1 + 4*x2 + 6*x4 - 4] + + sol = FiniteSet((-2*x2 - 3*x4 + 2, x2, 2*x4 + 5, x4)) + assert linsolve(Eqns, (x1, x2, x3, x4)) == sol + assert linsolve(Eqns, *(x1, x2, x3, x4)) == sol + assert linsolve(system1, (x1, x2, x3, x4)) == sol + assert linsolve(system1, *(x1, x2, x3, x4)) == sol + # issue 9667 - symbols can be Dummy symbols + x1, x2, x3, x4 = symbols('x:4', cls=Dummy) + assert linsolve(system1, x1, x2, x3, x4) == FiniteSet( + (-2*x2 - 3*x4 + 2, x2, 2*x4 + 5, x4)) + + # raise ValueError for garbage value + raises(ValueError, lambda: linsolve(Eqns)) + raises(ValueError, lambda: linsolve(x1)) + raises(ValueError, lambda: linsolve(x1, x2)) + raises(ValueError, lambda: linsolve((A,), x1, x2)) + raises(ValueError, lambda: linsolve(A, B, x1, x2)) + raises(ValueError, lambda: linsolve([x1], x1, x1)) + raises(ValueError, lambda: linsolve([x1], (i for i in (x1, x1)))) + + #raise ValueError if equations are non-linear in given variables + raises(NonlinearError, lambda: linsolve([x + y - 1, x ** 2 + y - 3], [x, y])) + raises(NonlinearError, lambda: linsolve([cos(x) + y, x + y], [x, y])) + assert linsolve([x + z - 1, x ** 2 + y - 3], [z, y]) == {(-x + 1, -x**2 + 3)} + + # Fully symbolic test + A = Matrix([[a, b], [c, d]]) + B = Matrix([[e], [g]]) + system2 = (A, B) + sol = FiniteSet(((-b*g + d*e)/(a*d - b*c), (a*g - c*e)/(a*d - b*c))) + assert linsolve(system2, [x, y]) == sol + + # No solution + A = Matrix([[1, 2, 3], [2, 4, 6], [3, 6, 9]]) + B = Matrix([0, 0, 1]) + assert linsolve((A, B), (x, y, z)) is S.EmptySet + + # Issue #10056 + A, B, J1, J2 = symbols('A B J1 J2') + Augmatrix = Matrix([ + [2*I*J1, 2*I*J2, -2/J1], + [-2*I*J2, -2*I*J1, 2/J2], + [0, 2, 2*I/(J1*J2)], + [2, 0, 0], + ]) + + assert linsolve(Augmatrix, A, B) == FiniteSet((0, I/(J1*J2))) + + # Issue #10121 - Assignment of free variables + Augmatrix = Matrix([[0, 1, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0]]) + assert linsolve(Augmatrix, a, b, c, d, e) == FiniteSet((a, 0, c, 0, e)) + #raises(IndexError, lambda: linsolve(Augmatrix, a, b, c)) + + x0, x1, x2, _x0 = symbols('tau0 tau1 tau2 _tau0') + assert linsolve(Matrix([[0, 1, 0, 0, 0, 0], [0, 0, 0, 1, 0, _x0]]) + ) == FiniteSet((x0, 0, x1, _x0, x2)) + x0, x1, x2, _x0 = symbols('tau00 tau01 tau02 tau0') + assert linsolve(Matrix([[0, 1, 0, 0, 0, 0], [0, 0, 0, 1, 0, _x0]]) + ) == FiniteSet((x0, 0, x1, _x0, x2)) + x0, x1, x2, _x0 = symbols('tau00 tau01 tau02 tau1') + assert linsolve(Matrix([[0, 1, 0, 0, 0, 0], [0, 0, 0, 1, 0, _x0]]) + ) == FiniteSet((x0, 0, x1, _x0, x2)) + # symbols can be given as generators + x0, x2, x4 = symbols('x0, x2, x4') + assert linsolve(Augmatrix, numbered_symbols('x') + ) == FiniteSet((x0, 0, x2, 0, x4)) + Augmatrix[-1, -1] = x0 + # use Dummy to avoid clash; the names may clash but the symbols + # will not + Augmatrix[-1, -1] = symbols('_x0') + assert len(linsolve( + Augmatrix, numbered_symbols('x', cls=Dummy)).free_symbols) == 4 + + # Issue #12604 + f = Function('f') + assert linsolve([f(x) - 5], f(x)) == FiniteSet((5,)) + + # Issue #14860 + from sympy.physics.units import meter, newton, kilo + kN = kilo*newton + Eqns = [8*kN + x + y, 28*kN*meter + 3*x*meter] + assert linsolve(Eqns, x, y) == { + (kilo*newton*Rational(-28, 3), kN*Rational(4, 3))} + + # linsolve does not allow expansion (real or implemented) + # to remove singularities, but it will cancel linear terms + assert linsolve([Eq(x, x + y)], [x, y]) == {(x, 0)} + assert linsolve([Eq(x + x*y, 1 + y)], [x]) == {(1,)} + assert linsolve([Eq(1 + y, x + x*y)], [x]) == {(1,)} + raises(NonlinearError, lambda: + linsolve([Eq(x**2, x**2 + y)], [x, y])) + + # corner cases + # + # XXX: The case below should give the same as for [0] + # assert linsolve([], [x]) == {(x,)} + assert linsolve([], [x]) is S.EmptySet + assert linsolve([0], [x]) == {(x,)} + assert linsolve([x], [x, y]) == {(0, y)} + assert linsolve([x, 0], [x, y]) == {(0, y)} + + +def test_linsolve_large_sparse(): + # + # This is mainly a performance test + # + + def _mk_eqs_sol(n): + xs = symbols('x:{}'.format(n)) + ys = symbols('y:{}'.format(n)) + syms = xs + ys + eqs = [] + sol = (-S.Half,) * n + (S.Half,) * n + for xi, yi in zip(xs, ys): + eqs.extend([xi + yi, xi - yi + 1]) + return eqs, syms, FiniteSet(sol) + + n = 500 + eqs, syms, sol = _mk_eqs_sol(n) + assert linsolve(eqs, syms) == sol + + +def test_linsolve_immutable(): + A = ImmutableDenseMatrix([[1, 1, 2], [0, 1, 2], [0, 0, 1]]) + B = ImmutableDenseMatrix([2, 1, -1]) + assert linsolve([A, B], (x, y, z)) == FiniteSet((1, 3, -1)) + + A = ImmutableDenseMatrix([[1, 1, 7], [1, -1, 3]]) + assert linsolve(A) == FiniteSet((5, 2)) + + +def test_solve_decomposition(): + n = Dummy('n') + + f1 = exp(3*x) - 6*exp(2*x) + 11*exp(x) - 6 + f2 = sin(x)**2 - 2*sin(x) + 1 + f3 = sin(x)**2 - sin(x) + f4 = sin(x + 1) + f5 = exp(x + 2) - 1 + f6 = 1/log(x) + f7 = 1/x + + s1 = ImageSet(Lambda(n, 2*n*pi), S.Integers) + s2 = ImageSet(Lambda(n, 2*n*pi + pi), S.Integers) + s3 = ImageSet(Lambda(n, 2*n*pi + pi/2), S.Integers) + s4 = ImageSet(Lambda(n, 2*n*pi - 1), S.Integers) + s5 = ImageSet(Lambda(n, 2*n*pi - 1 + pi), S.Integers) + + assert solve_decomposition(f1, x, S.Reals) == FiniteSet(0, log(2), log(3)) + assert dumeq(solve_decomposition(f2, x, S.Reals), s3) + assert dumeq(solve_decomposition(f3, x, S.Reals), Union(s1, s2, s3)) + assert dumeq(solve_decomposition(f4, x, S.Reals), Union(s4, s5)) + assert solve_decomposition(f5, x, S.Reals) == FiniteSet(-2) + assert solve_decomposition(f6, x, S.Reals) == S.EmptySet + assert solve_decomposition(f7, x, S.Reals) == S.EmptySet + assert solve_decomposition(x, x, Interval(1, 2)) == S.EmptySet + + +# nonlinsolve testcases +def test_nonlinsolve_basic(): + assert nonlinsolve([],[]) == S.EmptySet + assert nonlinsolve([],[x, y]) == S.EmptySet + + system = [x, y - x - 5] + assert nonlinsolve([x],[x, y]) == FiniteSet((0, y)) + assert nonlinsolve(system, [y]) == S.EmptySet + soln = (ImageSet(Lambda(n, 2*n*pi + pi/2), S.Integers),) + assert dumeq(nonlinsolve([sin(x) - 1], [x]), FiniteSet(tuple(soln))) + soln = ((ImageSet(Lambda(n, 2*n*pi + pi), S.Integers), 1), + (ImageSet(Lambda(n, 2*n*pi), S.Integers), 1)) + assert dumeq(nonlinsolve([sin(x), y - 1], [x, y]), FiniteSet(*soln)) + assert nonlinsolve([x**2 - 1], [x]) == FiniteSet((-1,), (1,)) + + soln = FiniteSet((y, y)) + assert nonlinsolve([x - y, 0], x, y) == soln + assert nonlinsolve([0, x - y], x, y) == soln + assert nonlinsolve([x - y, x - y], x, y) == soln + assert nonlinsolve([x, 0], x, y) == FiniteSet((0, y)) + f = Function('f') + assert nonlinsolve([f(x), 0], f(x), y) == FiniteSet((0, y)) + assert nonlinsolve([f(x), 0], f(x), f(y)) == FiniteSet((0, f(y))) + A = Indexed('A', x) + assert nonlinsolve([A, 0], A, y) == FiniteSet((0, y)) + assert nonlinsolve([x**2 -1], [sin(x)]) == FiniteSet((S.EmptySet,)) + assert nonlinsolve([x**2 -1], sin(x)) == FiniteSet((S.EmptySet,)) + assert nonlinsolve([x**2 -1], 1) == FiniteSet((x**2,)) + assert nonlinsolve([x**2 -1], x + y) == FiniteSet((S.EmptySet,)) + assert nonlinsolve([Eq(1, x + y), Eq(1, -x + y - 1), Eq(1, -x + y - 1)], x, y) == FiniteSet( + (-S.Half, 3*S.Half)) + + +def test_nonlinsolve_abs(): + soln = FiniteSet((y, y), (-y, y)) + assert nonlinsolve([Abs(x) - y], x, y) == soln + + +def test_raise_exception_nonlinsolve(): + raises(IndexError, lambda: nonlinsolve([x**2 -1], [])) + raises(ValueError, lambda: nonlinsolve([x**2 -1])) + + +def test_trig_system(): + # TODO: add more simple testcases when solveset returns + # simplified soln for Trig eq + assert nonlinsolve([sin(x) - 1, cos(x) -1 ], x) == S.EmptySet + soln1 = (ImageSet(Lambda(n, 2*n*pi + pi/2), S.Integers),) + soln = FiniteSet(soln1) + assert dumeq(nonlinsolve([sin(x) - 1, cos(x)], x), soln) + + +@XFAIL +def test_trig_system_fail(): + # fails because solveset trig solver is not much smart. + sys = [x + y - pi/2, sin(x) + sin(y) - 1] + # solveset returns conditionset for sin(x) + sin(y) - 1 + soln_1 = (ImageSet(Lambda(n, n*pi + pi/2), S.Integers), + ImageSet(Lambda(n, n*pi), S.Integers)) + soln_1 = FiniteSet(soln_1) + soln_2 = (ImageSet(Lambda(n, n*pi), S.Integers), + ImageSet(Lambda(n, n*pi+ pi/2), S.Integers)) + soln_2 = FiniteSet(soln_2) + soln = soln_1 + soln_2 + assert dumeq(nonlinsolve(sys, [x, y]), soln) + + # Add more cases from here + # http://www.vitutor.com/geometry/trigonometry/equations_systems.html#uno + sys = [sin(x) + sin(y) - (sqrt(3)+1)/2, sin(x) - sin(y) - (sqrt(3) - 1)/2] + soln_x = Union(ImageSet(Lambda(n, 2*n*pi + pi/3), S.Integers), + ImageSet(Lambda(n, 2*n*pi + pi*Rational(2, 3)), S.Integers)) + soln_y = Union(ImageSet(Lambda(n, 2*n*pi + pi/6), S.Integers), + ImageSet(Lambda(n, 2*n*pi + pi*Rational(5, 6)), S.Integers)) + assert dumeq(nonlinsolve(sys, [x, y]), FiniteSet((soln_x, soln_y))) + + +def test_nonlinsolve_positive_dimensional(): + x, y, a, b, c, d = symbols('x, y, a, b, c, d', extended_real=True) + assert nonlinsolve([x*y, x*y - x], [x, y]) == FiniteSet((0, y)) + + system = [a**2 + a*c, a - b] + assert nonlinsolve(system, [a, b]) == FiniteSet((0, 0), (-c, -c)) + # here (a= 0, b = 0) is independent soln so both is printed. + # if symbols = [a, b, c] then only {a : -c ,b : -c} + + eq1 = a + b + c + d + eq2 = a*b + b*c + c*d + d*a + eq3 = a*b*c + b*c*d + c*d*a + d*a*b + eq4 = a*b*c*d - 1 + system = [eq1, eq2, eq3, eq4] + sol1 = (-1/d, -d, 1/d, FiniteSet(d) - FiniteSet(0)) + sol2 = (1/d, -d, -1/d, FiniteSet(d) - FiniteSet(0)) + soln = FiniteSet(sol1, sol2) + assert nonlinsolve(system, [a, b, c, d]) == soln + + assert nonlinsolve([x**4 - 3*x**2 + y*x, x*z**2, y*z - 1], [x, y, z]) == \ + {(0, 1/z, z)} + + +def test_nonlinsolve_polysys(): + x, y, z = symbols('x, y, z', real=True) + assert nonlinsolve([x**2 + y - 2, x**2 + y], [x, y]) == S.EmptySet + + s = (-y + 2, y) + assert nonlinsolve([(x + y)**2 - 4, x + y - 2], [x, y]) == FiniteSet(s) + + system = [x**2 - y**2] + soln_real = FiniteSet((-y, y), (y, y)) + soln_complex = FiniteSet((-Abs(y), y), (Abs(y), y)) + soln =soln_real + soln_complex + assert nonlinsolve(system, [x, y]) == soln + + system = [x**2 - y**2] + soln_real= FiniteSet((y, -y), (y, y)) + soln_complex = FiniteSet((y, -Abs(y)), (y, Abs(y))) + soln = soln_real + soln_complex + assert nonlinsolve(system, [y, x]) == soln + + system = [x**2 + y - 3, x - y - 4] + assert nonlinsolve(system, (x, y)) != nonlinsolve(system, (y, x)) + + assert nonlinsolve([-x**2 - y**2 + z, -2*x, -2*y, S.One], [x, y, z]) == S.EmptySet + assert nonlinsolve([x + y + z, S.One, S.One, S.One], [x, y, z]) == S.EmptySet + + system = [-x**2*z**2 + x*y*z + y**4, -2*x*z**2 + y*z, x*z + 4*y**3, -2*x**2*z + x*y] + assert nonlinsolve(system, [x, y, z]) == FiniteSet((0, 0, z), (x, 0, 0)) + + +def test_nonlinsolve_using_substitution(): + x, y, z, n = symbols('x, y, z, n', real = True) + system = [(x + y)*n - y**2 + 2] + s_x = (n*y - y**2 + 2)/n + soln = (-s_x, y) + assert nonlinsolve(system, [x, y]) == FiniteSet(soln) + + system = [z**2*x**2 - z**2*y**2/exp(x)] + soln_real_1 = (y, x, 0) + soln_real_2 = (-exp(x/2)*Abs(x), x, z) + soln_real_3 = (exp(x/2)*Abs(x), x, z) + soln_complex_1 = (-x*exp(x/2), x, z) + soln_complex_2 = (x*exp(x/2), x, z) + syms = [y, x, z] + soln = FiniteSet(soln_real_1, soln_complex_1, soln_complex_2,\ + soln_real_2, soln_real_3) + assert nonlinsolve(system,syms) == soln + + +def test_nonlinsolve_complex(): + n = Dummy('n') + assert dumeq(nonlinsolve([exp(x) - sin(y), 1/y - 3], [x, y]), { + (ImageSet(Lambda(n, 2*n*I*pi + log(sin(Rational(1, 3)))), S.Integers), Rational(1, 3))}) + + system = [exp(x) - sin(y), 1/exp(y) - 3] + assert dumeq(nonlinsolve(system, [x, y]), { + (ImageSet(Lambda(n, I*(2*n*pi + pi) + + log(sin(log(3)))), S.Integers), -log(3)), + (ImageSet(Lambda(n, I*(2*n*pi + arg(sin(2*n*I*pi - log(3)))) + + log(Abs(sin(2*n*I*pi - log(3))))), S.Integers), + ImageSet(Lambda(n, 2*n*I*pi - log(3)), S.Integers))}) + + system = [exp(x) - sin(y), y**2 - 4] + assert dumeq(nonlinsolve(system, [x, y]), { + (ImageSet(Lambda(n, I*(2*n*pi + pi) + log(sin(2))), S.Integers), -2), + (ImageSet(Lambda(n, 2*n*I*pi + log(sin(2))), S.Integers), 2)}) + + system = [exp(x) - 2, y ** 2 - 2] + assert dumeq(nonlinsolve(system, [x, y]), { + (log(2), -sqrt(2)), (log(2), sqrt(2)), + (ImageSet(Lambda(n, 2*n*I*pi + log(2)), S.Integers), -sqrt(2)), + (ImageSet(Lambda(n, 2 * n * I * pi + log(2)), S.Integers), sqrt(2))}) + + +def test_nonlinsolve_radical(): + assert nonlinsolve([sqrt(y) - x - z, y - 1], [x, y, z]) == {(1 - z, 1, z)} + + +def test_nonlinsolve_inexact(): + sol = [(-1.625, -1.375), (1.625, 1.375)] + res = nonlinsolve([(x + y)**2 - 9, x**2 - y**2 - 0.75], [x, y]) + assert all(abs(res.args[i][j]-sol[i][j]) < 1e-9 + for i in range(2) for j in range(2)) + + assert nonlinsolve([(x + y)**2 - 9, (x + y)**2 - 0.75], [x, y]) == S.EmptySet + + assert nonlinsolve([y**2 + (x - 0.5)**2 - 0.0625, 2*x - 1.0, 2*y], [x, y]) == \ + S.EmptySet + + res = nonlinsolve([x**2 + y - 0.5, (x + y)**2, log(z)], [x, y, z]) + sol = [(-0.366025403784439, 0.366025403784439, 1), + (-0.366025403784439, 0.366025403784439, 1), + (1.36602540378444, -1.36602540378444, 1)] + assert all(abs(res.args[i][j]-sol[i][j]) < 1e-9 + for i in range(3) for j in range(3)) + + res = nonlinsolve([y - x**2, x**5 - x + 1.0], [x, y]) + sol = [(-1.16730397826142, 1.36259857766493), + (-0.181232444469876 - 1.08395410131771*I, + -1.14211129483496 + 0.392895302949911*I), + (-0.181232444469876 + 1.08395410131771*I, + -1.14211129483496 - 0.392895302949911*I), + (0.764884433600585 - 0.352471546031726*I, + 0.460812006002492 - 0.539199997693599*I), + (0.764884433600585 + 0.352471546031726*I, + 0.460812006002492 + 0.539199997693599*I)] + assert all(abs(res.args[i][j] - sol[i][j]) < 1e-9 + for i in range(5) for j in range(2)) + +@XFAIL +def test_solve_nonlinear_trans(): + # After the transcendental equation solver these will work + x, y = symbols('x, y', real=True) + soln1 = FiniteSet((2*LambertW(y/2), y)) + soln2 = FiniteSet((-x*sqrt(exp(x)), y), (x*sqrt(exp(x)), y)) + soln3 = FiniteSet((x*exp(x/2), x)) + soln4 = FiniteSet(2*LambertW(y/2), y) + assert nonlinsolve([x**2 - y**2/exp(x)], [x, y]) == soln1 + assert nonlinsolve([x**2 - y**2/exp(x)], [y, x]) == soln2 + assert nonlinsolve([x**2 - y**2/exp(x)], [y, x]) == soln3 + assert nonlinsolve([x**2 - y**2/exp(x)], [x, y]) == soln4 + + +def test_nonlinsolve_issue_25182(): + a1, b1, c1, ca, cb, cg = symbols('a1, b1, c1, ca, cb, cg') + eq1 = a1*a1 + b1*b1 - 2.*a1*b1*cg - c1*c1 + eq2 = a1*a1 + c1*c1 - 2.*a1*c1*cb - b1*b1 + eq3 = b1*b1 + c1*c1 - 2.*b1*c1*ca - a1*a1 + assert nonlinsolve([eq1, eq2, eq3], [c1, cb, cg]) == FiniteSet( + (1.0*b1*ca - 1.0*sqrt(a1**2 + b1**2*ca**2 - b1**2), + -1.0*sqrt(a1**2 + b1**2*ca**2 - b1**2)/a1, + -1.0*b1*(ca - 1)*(ca + 1)/a1 + 1.0*ca*sqrt(a1**2 + b1**2*ca**2 - b1**2)/a1), + (1.0*b1*ca + 1.0*sqrt(a1**2 + b1**2*ca**2 - b1**2), + 1.0*sqrt(a1**2 + b1**2*ca**2 - b1**2)/a1, + -1.0*b1*(ca - 1)*(ca + 1)/a1 - 1.0*ca*sqrt(a1**2 + b1**2*ca**2 - b1**2)/a1)) + + +def test_issue_14642(): + x = Symbol('x') + n1 = 0.5*x**3+x**2+0.5+I #add I in the Polynomials + solution = solveset(n1, x) + assert abs(solution.args[0] - (-2.28267560928153 - 0.312325580497716*I)) <= 1e-9 + assert abs(solution.args[1] - (-0.297354141679308 + 1.01904778618762*I)) <= 1e-9 + assert abs(solution.args[2] - (0.580029750960839 - 0.706722205689907*I)) <= 1e-9 + + # Symbolic + n1 = S.Half*x**3+x**2+S.Half+I + res = FiniteSet(-((3*sqrt(3)*31985**(S(1)/4)*sin(atan(S(172)/49)/2)/2 + + S(43)/2)**2 + (27 + 3*sqrt(3)*31985**(S(1)/4)*cos(atan(S(172)/49) + /2)/2)**2)**(S(1)/6)*cos(atan((27 + 3*sqrt(3)*31985**(S(1)/4)* + cos(atan(S(172)/49)/2)/2)/(3*sqrt(3)*31985**(S(1)/4)*sin(atan( + S(172)/49)/2)/2 + S(43)/2))/3)/3 - S(2)/3 - 4*cos(atan((27 + + 3*sqrt(3)*31985**(S(1)/4)*cos(atan(S(172)/49)/2)/2)/(3*sqrt(3)* + 31985**(S(1)/4)*sin(atan(S(172)/49)/2)/2 + S(43)/2))/3)/(3*((3* + sqrt(3)*31985**(S(1)/4)*sin(atan(S(172)/49)/2)/2 + S(43)/2)**2 + + (27 + 3*sqrt(3)*31985**(S(1)/4)*cos(atan(S(172)/49)/2)/2)**2)**(S(1)/ + 6)) + I*(-((3*sqrt(3)*31985**(S(1)/4)*sin(atan(S(172)/49)/2)/2 + + S(43)/2)**2 + (27 + 3*sqrt(3)*31985**(S(1)/4)*cos(atan(S(172)/49)/ + 2)/2)**2)**(S(1)/6)*sin(atan((27 + 3*sqrt(3)*31985**(S(1)/4)*cos( + atan(S(172)/49)/2)/2)/(3*sqrt(3)*31985**(S(1)/4)*sin(atan(S(172)/49) + /2)/2 + S(43)/2))/3)/3 + 4*sin(atan((27 + 3*sqrt(3)*31985**(S(1)/4)* + cos(atan(S(172)/49)/2)/2)/(3*sqrt(3)*31985**(S(1)/4)*sin(atan(S(172) + /49)/2)/2 + S(43)/2))/3)/(3*((3*sqrt(3)*31985**(S(1)/4)*sin(atan( + S(172)/49)/2)/2 + S(43)/2)**2 + (27 + 3*sqrt(3)*31985**(S(1)/4)* + cos(atan(S(172)/49)/2)/2)**2)**(S(1)/6))), -S(2)/3 - sqrt(3)*((3* + sqrt(3)*31985**(S(1)/4)*sin(atan(S(172)/49)/2)/2 + S(43)/2)**2 + + (27 + 3*sqrt(3)*31985**(S(1)/4)*cos(atan(S(172)/49)/2)/2)**2)**(S(1) + /6)*sin(atan((27 + 3*sqrt(3)*31985**(S(1)/4)*cos(atan(S(172)/49)/2) + /2)/(3*sqrt(3)*31985**(S(1)/4)*sin(atan(S(172)/49)/2)/2 + S(43)/2)) + /3)/6 - 4*re(1/((-S(1)/2 - sqrt(3)*I/2)*(S(43)/2 + 27*I + sqrt(-256 + + (43 + 54*I)**2)/2)**(S(1)/3)))/3 + ((3*sqrt(3)*31985**(S(1)/4)*sin( + atan(S(172)/49)/2)/2 + S(43)/2)**2 + (27 + 3*sqrt(3)*31985**(S(1)/4)* + cos(atan(S(172)/49)/2)/2)**2)**(S(1)/6)*cos(atan((27 + 3*sqrt(3)* + 31985**(S(1)/4)*cos(atan(S(172)/49)/2)/2)/(3*sqrt(3)*31985**(S(1)/4)* + sin(atan(S(172)/49)/2)/2 + S(43)/2))/3)/6 + I*(-4*im(1/((-S(1)/2 - + sqrt(3)*I/2)*(S(43)/2 + 27*I + sqrt(-256 + (43 + 54*I)**2)/2)**(S(1)/ + 3)))/3 + ((3*sqrt(3)*31985**(S(1)/4)*sin(atan(S(172)/49)/2)/2 + + S(43)/2)**2 + (27 + 3*sqrt(3)*31985**(S(1)/4)*cos(atan(S(172)/49)/2) + /2)**2)**(S(1)/6)*sin(atan((27 + 3*sqrt(3)*31985**(S(1)/4)*cos(atan( + S(172)/49)/2)/2)/(3*sqrt(3)*31985**(S(1)/4)*sin(atan(S(172)/49)/2)/2 + + S(43)/2))/3)/6 + sqrt(3)*((3*sqrt(3)*31985**(S(1)/4)*sin(atan(S(172)/ + 49)/2)/2 + S(43)/2)**2 + (27 + 3*sqrt(3)*31985**(S(1)/4)*cos(atan( + S(172)/49)/2)/2)**2)**(S(1)/6)*cos(atan((27 + 3*sqrt(3)*31985**(S(1)/ + 4)*cos(atan(S(172)/49)/2)/2)/(3*sqrt(3)*31985**(S(1)/4)*sin(atan( + S(172)/49)/2)/2 + S(43)/2))/3)/6), -S(2)/3 - 4*re(1/((-S(1)/2 + + sqrt(3)*I/2)*(S(43)/2 + 27*I + sqrt(-256 + (43 + 54*I)**2)/2)**(S(1) + /3)))/3 + sqrt(3)*((3*sqrt(3)*31985**(S(1)/4)*sin(atan(S(172)/49)/2)/2 + + S(43)/2)**2 + (27 + 3*sqrt(3)*31985**(S(1)/4)*cos(atan(S(172)/49)/2) + /2)**2)**(S(1)/6)*sin(atan((27 + 3*sqrt(3)*31985**(S(1)/4)*cos(atan( + S(172)/49)/2)/2)/(3*sqrt(3)*31985**(S(1)/4)*sin(atan(S(172)/49)/2)/2 + + S(43)/2))/3)/6 + ((3*sqrt(3)*31985**(S(1)/4)*sin(atan(S(172)/49)/2)/2 + + S(43)/2)**2 + (27 + 3*sqrt(3)*31985**(S(1)/4)*cos(atan(S(172)/49)/2) + /2)**2)**(S(1)/6)*cos(atan((27 + 3*sqrt(3)*31985**(S(1)/4)*cos(atan( + S(172)/49)/2)/2)/(3*sqrt(3)*31985**(S(1)/4)*sin(atan(S(172)/49)/2)/2 + + S(43)/2))/3)/6 + I*(-sqrt(3)*((3*sqrt(3)*31985**(S(1)/4)*sin(atan( + S(172)/49)/2)/2 + S(43)/2)**2 + (27 + 3*sqrt(3)*31985**(S(1)/4)*cos( + atan(S(172)/49)/2)/2)**2)**(S(1)/6)*cos(atan((27 + 3*sqrt(3)*31985**( + S(1)/4)*cos(atan(S(172)/49)/2)/2)/(3*sqrt(3)*31985**(S(1)/4)*sin( + atan(S(172)/49)/2)/2 + S(43)/2))/3)/6 + ((3*sqrt(3)*31985**(S(1)/4)* + sin(atan(S(172)/49)/2)/2 + S(43)/2)**2 + (27 + 3*sqrt(3)*31985**(S(1)/4)* + cos(atan(S(172)/49)/2)/2)**2)**(S(1)/6)*sin(atan((27 + 3*sqrt(3)*31985**( + S(1)/4)*cos(atan(S(172)/49)/2)/2)/(3*sqrt(3)*31985**(S(1)/4)*sin( + atan(S(172)/49)/2)/2 + S(43)/2))/3)/6 - 4*im(1/((-S(1)/2 + sqrt(3)*I/2)* + (S(43)/2 + 27*I + sqrt(-256 + (43 + 54*I)**2)/2)**(S(1)/3)))/3)) + + assert solveset(n1, x) == res + + +def test_issue_13961(): + V = (ax, bx, cx, gx, jx, lx, mx, nx, q) = symbols('ax bx cx gx jx lx mx nx q') + S = (ax*q - lx*q - mx, ax - gx*q - lx, bx*q**2 + cx*q - jx*q - nx, q*(-ax*q + lx*q + mx), q*(-ax + gx*q + lx)) + + sol = FiniteSet((lx + mx/q, (-cx*q + jx*q + nx)/q**2, cx, mx/q**2, jx, lx, mx, nx, Complement({q}, {0})), + (lx + mx/q, (cx*q - jx*q - nx)/q**2*-1, cx, mx/q**2, jx, lx, mx, nx, Complement({q}, {0}))) + assert nonlinsolve(S, *V) == sol + # The two solutions are in fact identical, so even better if only one is returned + + +def test_issue_14541(): + solutions = solveset(sqrt(-x**2 - 2.0), x) + assert abs(solutions.args[0]+1.4142135623731*I) <= 1e-9 + assert abs(solutions.args[1]-1.4142135623731*I) <= 1e-9 + + +def test_issue_13396(): + expr = -2*y*exp(-x**2 - y**2)*Abs(x) + sol = FiniteSet(0) + + assert solveset(expr, y, domain=S.Reals) == sol + + # Related type of equation also solved here + assert solveset(atan(x**2 - y**2)-pi/2, y, S.Reals) is S.EmptySet + + +def test_issue_12032(): + sol = FiniteSet(-sqrt(-2/(3*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3))) + + 2*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3)))/2 + + sqrt(Abs(-2*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3)) + + 2/(3*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3))) + + 2/sqrt(-2/(3*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3))) + + 2*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3)))))/2, + -sqrt(Abs(-2*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3)) + + 2/(3*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3))) + + 2/sqrt(-2/(3*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3))) + + 2*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3)))))/2 - + sqrt(-2/(3*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3))) + + 2*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3)))/2, + sqrt(-2/(3*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3))) + + 2*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3)))/2 - + I*sqrt(Abs(-2/sqrt(-2/(3*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3))) + + 2*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3))) - + 2*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3)) + + 2/(3*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3)))))/2, + sqrt(-2/(3*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3))) + + 2*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3)))/2 + + I*sqrt(Abs(-2/sqrt(-2/(3*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3))) + + 2*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3))) - + 2*(Rational(1, 16) + sqrt(849)/144)**(Rational(1, 3)) + + 2/(3*(Rational(1, 16) + sqrt(849)/144)**(Rational(1,3)))))/2) + assert solveset(x**4 + x - 1, x) == sol + + +def test_issue_10876(): + assert solveset(1/sqrt(x), x) == S.EmptySet + + +def test_issue_19050(): + # test_issue_19050 --> TypeError removed + assert dumeq(nonlinsolve([x + y, sin(y)], [x, y]), + FiniteSet((ImageSet(Lambda(n, -2*n*pi), S.Integers), ImageSet(Lambda(n, 2*n*pi), S.Integers)),\ + (ImageSet(Lambda(n, -2*n*pi - pi), S.Integers), ImageSet(Lambda(n, 2*n*pi + pi), S.Integers)))) + assert dumeq(nonlinsolve([x + y, sin(y) + cos(y)], [x, y]), + FiniteSet((ImageSet(Lambda(n, -2*n*pi - 3*pi/4), S.Integers), ImageSet(Lambda(n, 2*n*pi + 3*pi/4), S.Integers)), \ + (ImageSet(Lambda(n, -2*n*pi - 7*pi/4), S.Integers), ImageSet(Lambda(n, 2*n*pi + 7*pi/4), S.Integers)))) + + +def test_issue_16618(): + eqn = [sin(x)*sin(y), cos(x)*cos(y) - 1] + # nonlinsolve's answer is still suspicious since it contains only three + # distinct Dummys instead of 4. (Both 'x' ImageSets share the same Dummy.) + ans = FiniteSet((ImageSet(Lambda(n, 2*n*pi), S.Integers), ImageSet(Lambda(n, 2*n*pi), S.Integers)), + (ImageSet(Lambda(n, 2*n*pi + pi), S.Integers), ImageSet(Lambda(n, 2*n*pi + pi), S.Integers))) + sol = nonlinsolve(eqn, [x, y]) + + for i0, j0 in zip(ordered(sol), ordered(ans)): + assert len(i0) == len(j0) == 2 + assert all(a.dummy_eq(b) for a, b in zip(i0, j0)) + assert len(sol) == len(ans) + + +def test_issue_17566(): + assert nonlinsolve([32*(2**x)/2**(-y) - 4**y, 27*(3**x) - S(1)/3**y], x, y) ==\ + FiniteSet((-log(81)/log(3), 1)) + + +def test_issue_16643(): + n = Dummy('n') + assert solveset(x**2*sin(x), x).dummy_eq(Union(ImageSet(Lambda(n, 2*n*pi + pi), S.Integers), + ImageSet(Lambda(n, 2*n*pi), S.Integers))) + + +def test_issue_19587(): + n,m = symbols('n m') + assert nonlinsolve([32*2**m*2**n - 4**n, 27*3**m - 3**(-n)], m, n) ==\ + FiniteSet((-log(81)/log(3), 1)) + + +def test_issue_5132_1(): + system = [sqrt(x**2 + y**2) - sqrt(10), x + y - 4] + assert nonlinsolve(system, [x, y]) == FiniteSet((1, 3), (3, 1)) + + n = Dummy('n') + eqs = [exp(x)**2 - sin(y) + z**2, 1/exp(y) - 3] + s_real_y = -log(3) + s_real_z = sqrt(-exp(2*x) - sin(log(3))) + soln_real = FiniteSet((s_real_y, s_real_z), (s_real_y, -s_real_z)) + lam = Lambda(n, 2*n*I*pi + -log(3)) + s_complex_y = ImageSet(lam, S.Integers) + lam = Lambda(n, sqrt(-exp(2*x) + sin(2*n*I*pi + -log(3)))) + s_complex_z_1 = ImageSet(lam, S.Integers) + lam = Lambda(n, -sqrt(-exp(2*x) + sin(2*n*I*pi + -log(3)))) + s_complex_z_2 = ImageSet(lam, S.Integers) + soln_complex = FiniteSet( + (s_complex_y, s_complex_z_1), + (s_complex_y, s_complex_z_2) + ) + soln = soln_real + soln_complex + assert dumeq(nonlinsolve(eqs, [y, z]), soln) + + +def test_issue_5132_2(): + x, y = symbols('x, y', real=True) + eqs = [exp(x)**2 - sin(y) + z**2] + n = Dummy('n') + soln_real = (log(-z**2 + sin(y))/2, z) + lam = Lambda( n, I*(2*n*pi + arg(-z**2 + sin(y)))/2 + log(Abs(z**2 - sin(y)))/2) + img = ImageSet(lam, S.Integers) + # not sure about the complex soln. But it looks correct. + soln_complex = (img, z) + soln = FiniteSet(soln_real, soln_complex) + assert dumeq(nonlinsolve(eqs, [x, z]), soln) + + system = [r - x**2 - y**2, tan(t) - y/x] + s_x = sqrt(r/(tan(t)**2 + 1)) + s_y = sqrt(r/(tan(t)**2 + 1))*tan(t) + soln = FiniteSet((s_x, s_y), (-s_x, -s_y)) + assert nonlinsolve(system, [x, y]) == soln + + +def test_issue_6752(): + a, b = symbols('a, b', real=True) + assert nonlinsolve([a**2 + a, a - b], [a, b]) == {(-1, -1), (0, 0)} + + +@SKIP("slow") +def test_issue_5114_solveset(): + # slow testcase + from sympy.abc import o, p + + # there is no 'a' in the equation set but this is how the + # problem was originally posed + syms = [a, b, c, f, h, k, n] + eqs = [b + r/d - c/d, + c*(1/d + 1/e + 1/g) - f/g - r/d, + f*(1/g + 1/i + 1/j) - c/g - h/i, + h*(1/i + 1/l + 1/m) - f/i - k/m, + k*(1/m + 1/o + 1/p) - h/m - n/p, + n*(1/p + 1/q) - k/p] + assert len(nonlinsolve(eqs, syms)) == 1 + + +@SKIP("Hangs") +def _test_issue_5335(): + # Not able to check zero dimensional system. + # is_zero_dimensional Hangs + lam, a0, conc = symbols('lam a0 conc') + eqs = [lam + 2*y - a0*(1 - x/2)*x - 0.005*x/2*x, + a0*(1 - x/2)*x - 1*y - 0.743436700916726*y, + x + y - conc] + sym = [x, y, a0] + # there are 4 solutions but only two are valid + assert len(nonlinsolve(eqs, sym)) == 2 + # float + eqs = [lam + 2*y - a0*(1 - x/2)*x - 0.005*x/2*x, + a0*(1 - x/2)*x - 1*y - 0.743436700916726*y, + x + y - conc] + sym = [x, y, a0] + assert len(nonlinsolve(eqs, sym)) == 2 + + +def test_issue_2777(): + # the equations represent two circles + x, y = symbols('x y', real=True) + e1, e2 = sqrt(x**2 + y**2) - 10, sqrt(y**2 + (-x + 10)**2) - 3 + a, b = Rational(191, 20), 3*sqrt(391)/20 + ans = {(a, -b), (a, b)} + assert nonlinsolve((e1, e2), (x, y)) == ans + assert nonlinsolve((e1, e2/(x - a)), (x, y)) == S.EmptySet + # make the 2nd circle's radius be -3 + e2 += 6 + assert nonlinsolve((e1, e2), (x, y)) == S.EmptySet + + +def test_issue_8828(): + x1 = 0 + y1 = -620 + r1 = 920 + x2 = 126 + y2 = 276 + x3 = 51 + y3 = 205 + r3 = 104 + v = [x, y, z] + + f1 = (x - x1)**2 + (y - y1)**2 - (r1 - z)**2 + f2 = (x2 - x)**2 + (y2 - y)**2 - z**2 + f3 = (x - x3)**2 + (y - y3)**2 - (r3 - z)**2 + F = [f1, f2, f3] + + g1 = sqrt((x - x1)**2 + (y - y1)**2) + z - r1 + g2 = f2 + g3 = sqrt((x - x3)**2 + (y - y3)**2) + z - r3 + G = [g1, g2, g3] + + # both soln same + A = nonlinsolve(F, v) + B = nonlinsolve(G, v) + assert A == B + + +def test_nonlinsolve_conditionset(): + # when solveset failed to solve all the eq + # return conditionset + f = Function('f') + f1 = f(x) - pi/2 + f2 = f(y) - pi*Rational(3, 2) + intermediate_system = Eq(2*f(x) - pi, 0) & Eq(2*f(y) - 3*pi, 0) + syms = Tuple(x, y) + soln = ConditionSet( + syms, + intermediate_system, + S.Complexes**2) + assert nonlinsolve([f1, f2], [x, y]) == soln + + +def test_substitution_basic(): + assert substitution([], [x, y]) == S.EmptySet + assert substitution([], []) == S.EmptySet + system = [2*x**2 + 3*y**2 - 30, 3*x**2 - 2*y**2 - 19] + soln = FiniteSet((-3, -2), (-3, 2), (3, -2), (3, 2)) + assert substitution(system, [x, y]) == soln + + soln = FiniteSet((-1, 1)) + assert substitution([x + y], [x], [{y: 1}], [y], set(), [x, y]) == soln + assert substitution( + [x + y], [x], [{y: 1}], [y], + {x + 1}, [y, x]) == S.EmptySet + + +def test_substitution_incorrect(): + # the solutions in the following two tests are incorrect. The + # correct result is EmptySet in both cases. + assert substitution([h - 1, k - 1, f - 2, f - 4, -2 * k], + [h, k, f]) == {(1, 1, f)} + assert substitution([x + y + z, S.One, S.One, S.One], [x, y, z]) == \ + {(-y - z, y, z)} + + # the correct result in the test below is {(-I, I, I, -I), + # (I, -I, -I, I)} + assert substitution([a - d, b + d, c + d, d**2 + 1], [a, b, c, d]) == \ + {(d, -d, -d, d)} + + # the result in the test below is incomplete. The complete result + # is {(0, b), (log(2), 2)} + assert substitution([a*(a - log(b)), a*(b - 2)], [a, b]) == \ + {(0, b)} + + # The system in the test below is zero-dimensional, so the result + # should have no free symbols + assert substitution([-k*y + 6*x - 4*y, -81*k + 49*y**2 - 270, + -3*k*z + k + z**3, k**2 - 2*k + 4], + [x, y, z, k]).free_symbols == {z} + + +def test_substitution_redundant(): + # the third and fourth solutions are redundant in the test below + assert substitution([x**2 - y**2, z - 1], [x, z]) == \ + {(-y, 1), (y, 1), (-sqrt(y**2), 1), (sqrt(y**2), 1)} + + # the system below has three solutions. Two of the solutions + # returned by substitution are redundant. + res = substitution([x - y, y**3 - 3*y**2 + 1], [x, y]) + assert len(res) == 5 + + +def test_issue_5132_substitution(): + x, y, z, r, t = symbols('x, y, z, r, t', real=True) + system = [r - x**2 - y**2, tan(t) - y/x] + s_x_1 = Complement(FiniteSet(-sqrt(r/(tan(t)**2 + 1))), FiniteSet(0)) + s_x_2 = Complement(FiniteSet(sqrt(r/(tan(t)**2 + 1))), FiniteSet(0)) + s_y = sqrt(r/(tan(t)**2 + 1))*tan(t) + soln = FiniteSet((s_x_2, s_y)) + FiniteSet((s_x_1, -s_y)) + assert substitution(system, [x, y]) == soln + + n = Dummy('n') + eqs = [exp(x)**2 - sin(y) + z**2, 1/exp(y) - 3] + s_real_y = -log(3) + s_real_z = sqrt(-exp(2*x) - sin(log(3))) + soln_real = FiniteSet((s_real_y, s_real_z), (s_real_y, -s_real_z)) + lam = Lambda(n, 2*n*I*pi + -log(3)) + s_complex_y = ImageSet(lam, S.Integers) + lam = Lambda(n, sqrt(-exp(2*x) + sin(2*n*I*pi + -log(3)))) + s_complex_z_1 = ImageSet(lam, S.Integers) + lam = Lambda(n, -sqrt(-exp(2*x) + sin(2*n*I*pi + -log(3)))) + s_complex_z_2 = ImageSet(lam, S.Integers) + soln_complex = FiniteSet( + (s_complex_y, s_complex_z_1), + (s_complex_y, s_complex_z_2)) + soln = soln_real + soln_complex + assert dumeq(substitution(eqs, [y, z]), soln) + + +def test_raises_substitution(): + raises(ValueError, lambda: substitution([x**2 -1], [])) + raises(TypeError, lambda: substitution([x**2 -1])) + raises(ValueError, lambda: substitution([x**2 -1], [sin(x)])) + raises(TypeError, lambda: substitution([x**2 -1], x)) + raises(TypeError, lambda: substitution([x**2 -1], 1)) + + +def test_issue_21022(): + from sympy.core.sympify import sympify + + eqs = [ + 'k-16', + 'p-8', + 'y*y+z*z-x*x', + 'd - x + p', + 'd*d+k*k-y*y', + 'z*z-p*p-k*k', + 'abc-efg', + ] + efg = Symbol('efg') + eqs = [sympify(x) for x in eqs] + + syb = list(ordered(set.union(*[x.free_symbols for x in eqs]))) + res = nonlinsolve(eqs, syb) + + ans = FiniteSet( + (efg, 32, efg, 16, 8, 40, -16*sqrt(5), -8*sqrt(5)), + (efg, 32, efg, 16, 8, 40, -16*sqrt(5), 8*sqrt(5)), + (efg, 32, efg, 16, 8, 40, 16*sqrt(5), -8*sqrt(5)), + (efg, 32, efg, 16, 8, 40, 16*sqrt(5), 8*sqrt(5)), + ) + assert len(res) == len(ans) == 4 + assert res == ans + for result in res.args: + assert len(result) == 8 + + +def test_issue_17940(): + n = Dummy('n') + k1 = Dummy('k1') + sol = ImageSet(Lambda(((k1, n),), I*(2*k1*pi + arg(2*n*I*pi + log(5))) + + log(Abs(2*n*I*pi + log(5)))), + ProductSet(S.Integers, S.Integers)) + assert solveset(exp(exp(x)) - 5, x).dummy_eq(sol) + + +def test_issue_17906(): + assert solveset(7**(x**2 - 80) - 49**x, x) == FiniteSet(-8, 10) + + +@XFAIL +def test_issue_17933(): + eq1 = x*sin(45) - y*cos(q) + eq2 = x*cos(45) - y*sin(q) + eq3 = 9*x*sin(45)/10 + y*cos(q) + eq4 = 9*x*cos(45)/10 + y*sin(z) - z + assert nonlinsolve([eq1, eq2, eq3, eq4], x, y, z, q) ==\ + FiniteSet((0, 0, 0, q)) + +def test_issue_17933_bis(): + # nonlinsolve's result depends on the 'default_sort_key' ordering of + # the unknowns. + eq1 = x*sin(45) - y*cos(q) + eq2 = x*cos(45) - y*sin(q) + eq3 = 9*x*sin(45)/10 + y*cos(q) + eq4 = 9*x*cos(45)/10 + y*sin(z) - z + zz = Symbol('zz') + eqs = [e.subs(q, zz) for e in (eq1, eq2, eq3, eq4)] + assert nonlinsolve(eqs, x, y, z, zz) == FiniteSet((0, 0, 0, zz)) + + +def test_issue_14565(): + # removed redundancy + assert dumeq(nonlinsolve([k + m, k + m*exp(-2*pi*k)], [k, m]) , + FiniteSet((-n*I, ImageSet(Lambda(n, n*I), S.Integers)))) + + +# end of tests for nonlinsolve + + +def test_issue_9556(): + b = Symbol('b', positive=True) + + assert solveset(Abs(x) + 1, x, S.Reals) is S.EmptySet + assert solveset(Abs(x) + b, x, S.Reals) is S.EmptySet + assert solveset(Eq(b, -1), b, S.Reals) is S.EmptySet + + +def test_issue_9611(): + assert solveset(Eq(x - x + a, a), x, S.Reals) == S.Reals + assert solveset(Eq(y - y + a, a), y) == S.Complexes + + +def test_issue_9557(): + assert solveset(x**2 + a, x, S.Reals) == Intersection(S.Reals, + FiniteSet(-sqrt(-a), sqrt(-a))) + + +def test_issue_9778(): + x = Symbol('x', real=True) + y = Symbol('y', real=True) + assert solveset(x**3 + 1, x, S.Reals) == FiniteSet(-1) + assert solveset(x**Rational(3, 5) + 1, x, S.Reals) == S.EmptySet + assert solveset(x**3 + y, x, S.Reals) == \ + FiniteSet(-Abs(y)**Rational(1, 3)*sign(y)) + + +def test_issue_10214(): + assert solveset(x**Rational(3, 2) + 4, x, S.Reals) == S.EmptySet + assert solveset(x**(Rational(-3, 2)) + 4, x, S.Reals) == S.EmptySet + + ans = FiniteSet(-2**Rational(2, 3)) + assert solveset(x**(S(3)) + 4, x, S.Reals) == ans + assert (x**(S(3)) + 4).subs(x,list(ans)[0]) == 0 # substituting ans and verifying the result. + assert (x**(S(3)) + 4).subs(x,-(-2)**Rational(2, 3)) == 0 + + +def test_issue_9849(): + assert solveset(Abs(sin(x)) + 1, x, S.Reals) == S.EmptySet + + +def test_issue_9953(): + assert linsolve([ ], x) == S.EmptySet + + +def test_issue_9913(): + assert solveset(2*x + 1/(x - 10)**2, x, S.Reals) == \ + FiniteSet(-(3*sqrt(24081)/4 + Rational(4027, 4))**Rational(1, 3)/3 - 100/ + (3*(3*sqrt(24081)/4 + Rational(4027, 4))**Rational(1, 3)) + Rational(20, 3)) + + +def test_issue_10397(): + assert solveset(sqrt(x), x, S.Complexes) == FiniteSet(0) + + +def test_issue_14987(): + raises(ValueError, lambda: linear_eq_to_matrix( + [x**2], x)) + raises(ValueError, lambda: linear_eq_to_matrix( + [x*(-3/x + 1) + 2*y - a], [x, y])) + raises(ValueError, lambda: linear_eq_to_matrix( + [(x**2 - 3*x)/(x - 3) - 3], x)) + raises(ValueError, lambda: linear_eq_to_matrix( + [(x + 1)**3 - x**3 - 3*x**2 + 7], x)) + raises(ValueError, lambda: linear_eq_to_matrix( + [x*(1/x + 1) + y], [x, y])) + raises(ValueError, lambda: linear_eq_to_matrix( + [(x + 1)*y], [x, y])) + raises(ValueError, lambda: linear_eq_to_matrix( + [Eq(1/x, 1/x + y)], [x, y])) + raises(ValueError, lambda: linear_eq_to_matrix( + [Eq(y/x, y/x + y)], [x, y])) + raises(ValueError, lambda: linear_eq_to_matrix( + [Eq(x*(x + 1), x**2 + y)], [x, y])) + + +def test_simplification(): + eq = x + (a - b)/(-2*a + 2*b) + assert solveset(eq, x) == FiniteSet(S.Half) + assert solveset(eq, x, S.Reals) == Intersection({-((a - b)/(-2*a + 2*b))}, S.Reals) + # So that ap - bn is not zero: + ap = Symbol('ap', positive=True) + bn = Symbol('bn', negative=True) + eq = x + (ap - bn)/(-2*ap + 2*bn) + assert solveset(eq, x) == FiniteSet(S.Half) + assert solveset(eq, x, S.Reals) == FiniteSet(S.Half) + + +def test_integer_domain_relational(): + eq1 = 2*x + 3 > 0 + eq2 = x**2 + 3*x - 2 >= 0 + eq3 = x + 1/x > -2 + 1/x + eq4 = x + sqrt(x**2 - 5) > 0 + eq = x + 1/x > -2 + 1/x + eq5 = eq.subs(x,log(x)) + eq6 = log(x)/x <= 0 + eq7 = log(x)/x < 0 + eq8 = x/(x-3) < 3 + eq9 = x/(x**2-3) < 3 + + assert solveset(eq1, x, S.Integers) == Range(-1, oo, 1) + assert solveset(eq2, x, S.Integers) == Union(Range(-oo, -3, 1), Range(1, oo, 1)) + assert solveset(eq3, x, S.Integers) == Union(Range(-1, 0, 1), Range(1, oo, 1)) + assert solveset(eq4, x, S.Integers) == Range(3, oo, 1) + assert solveset(eq5, x, S.Integers) == Range(2, oo, 1) + assert solveset(eq6, x, S.Integers) == Range(1, 2, 1) + assert solveset(eq7, x, S.Integers) == S.EmptySet + assert solveset(eq8, x, domain=Range(0,5)) == Range(0, 3, 1) + assert solveset(eq9, x, domain=Range(0,5)) == Union(Range(0, 2, 1), Range(2, 5, 1)) + + # test_issue_19794 + assert solveset(x + 2 < 0, x, S.Integers) == Range(-oo, -2, 1) + + +def test_issue_10555(): + f = Function('f') + g = Function('g') + assert solveset(f(x) - pi/2, x, S.Reals).dummy_eq( + ConditionSet(x, Eq(f(x) - pi/2, 0), S.Reals)) + assert solveset(f(g(x)) - pi/2, g(x), S.Reals).dummy_eq( + ConditionSet(g(x), Eq(f(g(x)) - pi/2, 0), S.Reals)) + + +def test_issue_8715(): + eq = x + 1/x > -2 + 1/x + assert solveset(eq, x, S.Reals) == \ + (Interval.open(-2, oo) - FiniteSet(0)) + assert solveset(eq.subs(x,log(x)), x, S.Reals) == \ + Interval.open(exp(-2), oo) - FiniteSet(1) + + +def test_issue_11174(): + eq = z**2 + exp(2*x) - sin(y) + soln = Intersection(S.Reals, FiniteSet(log(-z**2 + sin(y))/2)) + assert solveset(eq, x, S.Reals) == soln + + eq = sqrt(r)*Abs(tan(t))/sqrt(tan(t)**2 + 1) + x*tan(t) + s = -sqrt(r)*Abs(tan(t))/(sqrt(tan(t)**2 + 1)*tan(t)) + soln = Intersection(S.Reals, FiniteSet(s)) + assert solveset(eq, x, S.Reals) == soln + + +def test_issue_11534(): + # eq1 and eq2 should not have the same solutions because squaring both + # sides of the radical equation introduces a spurious solution branch. + # The equations have a symbolic parameter y and it is easy to see that for + # y != 0 the solution s1 will not be valid for eq1. + x = Symbol('x', real=True) + y = Symbol('y', real=True) + eq1 = -y + x/sqrt(-x**2 + 1) + eq2 = -y**2 + x**2/(-x**2 + 1) + + # We get a ConditionSet here because s1 works in eq1 if y is equal to zero + # although not for any other value of y. That case is redundant though + # because if y=0 then s1=s2 so the solution for eq1 could just be returned + # as s2 - {-1, 1}. In fact we have + # |y/sqrt(y**2 + 1)| < 1 + # So the complements are not needed either. The ideal output here would be + # sol1 = s2 + # sol2 = s1 | s2. + s1, s2 = FiniteSet(-y/sqrt(y**2 + 1)), FiniteSet(y/sqrt(y**2 + 1)) + cset = ConditionSet(x, Eq(eq1, 0), s1) + sol1 = (s2 - {-1, 1}) | (cset - {-1, 1}) + sol2 = (s1 | s2) - {-1, 1} + + assert solveset(eq1, x, S.Reals) == sol1 + assert solveset(eq2, x, S.Reals) == sol2 + + +def test_issue_10477(): + assert solveset((x**2 + 4*x - 3)/x < 2, x, S.Reals) == \ + Union(Interval.open(-oo, -3), Interval.open(0, 1)) + + +def test_issue_10671(): + assert solveset(sin(y), y, Interval(0, pi)) == FiniteSet(0, pi) + i = Interval(1, 10) + assert solveset((1/x).diff(x) < 0, x, i) == i + + +def test_issue_11064(): + eq = x + sqrt(x**2 - 5) + assert solveset(eq > 0, x, S.Reals) == \ + Interval(sqrt(5), oo) + assert solveset(eq < 0, x, S.Reals) == \ + Interval(-oo, -sqrt(5)) + assert solveset(eq > sqrt(5), x, S.Reals) == \ + Interval.Lopen(sqrt(5), oo) + + +def test_issue_12478(): + eq = sqrt(x - 2) + 2 + soln = solveset_real(eq, x) + assert soln is S.EmptySet + assert solveset(eq < 0, x, S.Reals) is S.EmptySet + assert solveset(eq > 0, x, S.Reals) == Interval(2, oo) + + +def test_issue_12429(): + eq = solveset(log(x)/x <= 0, x, S.Reals) + sol = Interval.Lopen(0, 1) + assert eq == sol + + +def test_issue_19506(): + eq = arg(x + I) + C = Dummy('C') + assert solveset(eq).dummy_eq(Intersection(ConditionSet(C, Eq(im(C) + 1, 0), S.Complexes), + ConditionSet(C, re(C) > 0, S.Complexes))) + + +def test_solveset_arg(): + assert solveset(arg(x), x, S.Reals) == Interval.open(0, oo) + assert solveset(arg(4*x -3), x, S.Reals) == Interval.open(Rational(3, 4), oo) + + +def test__is_finite_with_finite_vars(): + f = _is_finite_with_finite_vars + # issue 12482 + assert all(f(1/x) is None for x in ( + Dummy(), Dummy(real=True), Dummy(complex=True))) + assert f(1/Dummy(real=False)) is True # b/c it's finite but not 0 + + +def test_issue_13550(): + assert solveset(x**2 - 2*x - 15, symbol = x, domain = Interval(-oo, 0)) == FiniteSet(-3) + + +def test_issue_13849(): + assert nonlinsolve((t*(sqrt(5) + sqrt(2)) - sqrt(2), t), t) is S.EmptySet + + +def test_issue_14223(): + assert solveset((Abs(x + Min(x, 2)) - 2).rewrite(Piecewise), x, + S.Reals) == FiniteSet(-1, 1) + assert solveset((Abs(x + Min(x, 2)) - 2).rewrite(Piecewise), x, + Interval(0, 2)) == FiniteSet(1) + assert solveset(x, x, FiniteSet(1, 2)) is S.EmptySet + + +def test_issue_10158(): + dom = S.Reals + assert solveset(x*Max(x, 15) - 10, x, dom) == FiniteSet(Rational(2, 3)) + assert solveset(x*Min(x, 15) - 10, x, dom) == FiniteSet(-sqrt(10), sqrt(10)) + assert solveset(Max(Abs(x - 3) - 1, x + 2) - 3, x, dom) == FiniteSet(-1, 1) + assert solveset(Abs(x - 1) - Abs(y), x, dom) == FiniteSet(-Abs(y) + 1, Abs(y) + 1) + assert solveset(Abs(x + 4*Abs(x + 1)), x, dom) == FiniteSet(Rational(-4, 3), Rational(-4, 5)) + assert solveset(2*Abs(x + Abs(x + Max(3, x))) - 2, x, S.Reals) == FiniteSet(-1, -2) + dom = S.Complexes + raises(ValueError, lambda: solveset(x*Max(x, 15) - 10, x, dom)) + raises(ValueError, lambda: solveset(x*Min(x, 15) - 10, x, dom)) + raises(ValueError, lambda: solveset(Max(Abs(x - 3) - 1, x + 2) - 3, x, dom)) + raises(ValueError, lambda: solveset(Abs(x - 1) - Abs(y), x, dom)) + raises(ValueError, lambda: solveset(Abs(x + 4*Abs(x + 1)), x, dom)) + + +def test_issue_14300(): + f = 1 - exp(-18000000*x) - y + a1 = FiniteSet(-log(-y + 1)/18000000) + + assert solveset(f, x, S.Reals) == \ + Intersection(S.Reals, a1) + assert dumeq(solveset(f, x), + ImageSet(Lambda(n, -I*(2*n*pi + arg(-y + 1))/18000000 - + log(Abs(y - 1))/18000000), S.Integers)) + + +def test_issue_14454(): + number = CRootOf(x**4 + x - 1, 2) + raises(ValueError, lambda: invert_real(number, 0, x)) + assert invert_real(x**2, number, x) # no error + + +def test_issue_17882(): + assert solveset(-8*x**2/(9*(x**2 - 1)**(S(4)/3)) + 4/(3*(x**2 - 1)**(S(1)/3)), x, S.Complexes) == \ + FiniteSet(sqrt(3), -sqrt(3)) + + +def test_term_factors(): + assert list(_term_factors(3**x - 2)) == [-2, 3**x] + expr = 4**(x + 1) + 4**(x + 2) + 4**(x - 1) - 3**(x + 2) - 3**(x + 3) + assert set(_term_factors(expr)) == { + 3**(x + 2), 4**(x + 2), 3**(x + 3), 4**(x - 1), -1, 4**(x + 1)} + + +#################### tests for transolve and its helpers ############### + +def test_transolve(): + + assert _transolve(3**x, x, S.Reals) == S.EmptySet + assert _transolve(3**x - 9**(x + 5), x, S.Reals) == FiniteSet(-10) + + +def test_issue_21276(): + eq = (2*x*(y - z) - y*erf(y - z) - y + z*erf(y - z) + z)**2 + assert solveset(eq.expand(), y) == FiniteSet(z, z + erfinv(2*x - 1)) + + +# exponential tests +def test_exponential_real(): + from sympy.abc import y + + e1 = 3**(2*x) - 2**(x + 3) + e2 = 4**(5 - 9*x) - 8**(2 - x) + e3 = 2**x + 4**x + e4 = exp(log(5)*x) - 2**x + e5 = exp(x/y)*exp(-z/y) - 2 + e6 = 5**(x/2) - 2**(x/3) + e7 = 4**(x + 1) + 4**(x + 2) + 4**(x - 1) - 3**(x + 2) - 3**(x + 3) + e8 = -9*exp(-2*x + 5) + 4*exp(3*x + 1) + e9 = 2**x + 4**x + 8**x - 84 + e10 = 29*2**(x + 1)*615**(x) - 123*2726**(x) + + assert solveset(e1, x, S.Reals) == FiniteSet( + -3*log(2)/(-2*log(3) + log(2))) + assert solveset(e2, x, S.Reals) == FiniteSet(Rational(4, 15)) + assert solveset(e3, x, S.Reals) == S.EmptySet + assert solveset(e4, x, S.Reals) == FiniteSet(0) + assert solveset(e5, x, S.Reals) == Intersection( + S.Reals, FiniteSet(y*log(2*exp(z/y)))) + assert solveset(e6, x, S.Reals) == FiniteSet(0) + assert solveset(e7, x, S.Reals) == FiniteSet(2) + assert solveset(e8, x, S.Reals) == FiniteSet(-2*log(2)/5 + 2*log(3)/5 + Rational(4, 5)) + assert solveset(e9, x, S.Reals) == FiniteSet(2) + assert solveset(e10,x, S.Reals) == FiniteSet((-log(29) - log(2) + log(123))/(-log(2726) + log(2) + log(615))) + + assert solveset_real(-9*exp(-2*x + 5) + 2**(x + 1), x) == FiniteSet( + -((-5 - 2*log(3) + log(2))/(log(2) + 2))) + assert solveset_real(4**(x/2) - 2**(x/3), x) == FiniteSet(0) + b = sqrt(6)*sqrt(log(2))/sqrt(log(5)) + assert solveset_real(5**(x/2) - 2**(3/x), x) == FiniteSet(-b, b) + + # coverage test + C1, C2 = symbols('C1 C2') + f = Function('f') + assert solveset_real(C1 + C2/x**2 - exp(-f(x)), f(x)) == Intersection( + S.Reals, FiniteSet(-log(C1 + C2/x**2))) + y = symbols('y', positive=True) + assert solveset_real(x**2 - y**2/exp(x), y) == Intersection( + S.Reals, FiniteSet(-sqrt(x**2*exp(x)), sqrt(x**2*exp(x)))) + p = Symbol('p', positive=True) + assert solveset_real((1/p + 1)**(p + 1), p).dummy_eq( + ConditionSet(x, Eq((1 + 1/x)**(x + 1), 0), S.Reals)) + assert solveset(2**x - 4**x + 12, x, S.Reals) == {2} + assert solveset(2**x - 2**(2*x) + 12, x, S.Reals) == {2} + + +@XFAIL +def test_exponential_complex(): + n = Dummy('n') + + assert dumeq(solveset_complex(2**x + 4**x, x),imageset( + Lambda(n, I*(2*n*pi + pi)/log(2)), S.Integers)) + assert solveset_complex(x**z*y**z - 2, z) == FiniteSet( + log(2)/(log(x) + log(y))) + assert dumeq(solveset_complex(4**(x/2) - 2**(x/3), x), imageset( + Lambda(n, 3*n*I*pi/log(2)), S.Integers)) + assert dumeq(solveset(2**x + 32, x), imageset( + Lambda(n, (I*(2*n*pi + pi) + 5*log(2))/log(2)), S.Integers)) + + eq = (2**exp(y**2/x) + 2)/(x**2 + 15) + a = sqrt(x)*sqrt(-log(log(2)) + log(log(2) + 2*n*I*pi)) + assert solveset_complex(eq, y) == FiniteSet(-a, a) + + union1 = imageset(Lambda(n, I*(2*n*pi - pi*Rational(2, 3))/log(2)), S.Integers) + union2 = imageset(Lambda(n, I*(2*n*pi + pi*Rational(2, 3))/log(2)), S.Integers) + assert dumeq(solveset(2**x + 4**x + 8**x, x), Union(union1, union2)) + + eq = 4**(x + 1) + 4**(x + 2) + 4**(x - 1) - 3**(x + 2) - 3**(x + 3) + res = solveset(eq, x) + num = 2*n*I*pi - 4*log(2) + 2*log(3) + den = -2*log(2) + log(3) + ans = imageset(Lambda(n, num/den), S.Integers) + assert dumeq(res, ans) + + +def test_expo_conditionset(): + + f1 = (exp(x) + 1)**x - 2 + f2 = (x + 2)**y*x - 3 + f3 = 2**x - exp(x) - 3 + f4 = log(x) - exp(x) + f5 = 2**x + 3**x - 5**x + + assert solveset(f1, x, S.Reals).dummy_eq(ConditionSet( + x, Eq((exp(x) + 1)**x - 2, 0), S.Reals)) + assert solveset(f2, x, S.Reals).dummy_eq(ConditionSet( + x, Eq(x*(x + 2)**y - 3, 0), S.Reals)) + assert solveset(f3, x, S.Reals).dummy_eq(ConditionSet( + x, Eq(2**x - exp(x) - 3, 0), S.Reals)) + assert solveset(f4, x, S.Reals).dummy_eq(ConditionSet( + x, Eq(-exp(x) + log(x), 0), S.Reals)) + assert solveset(f5, x, S.Reals).dummy_eq(ConditionSet( + x, Eq(2**x + 3**x - 5**x, 0), S.Reals)) + + +def test_exponential_symbols(): + x, y, z = symbols('x y z', positive=True) + xr, zr = symbols('xr, zr', real=True) + + assert solveset(z**x - y, x, S.Reals) == Intersection( + S.Reals, FiniteSet(log(y)/log(z))) + + f1 = 2*x**w - 4*y**w + f2 = (x/y)**w - 2 + sol1 = Intersection({log(2)/(log(x) - log(y))}, S.Reals) + sol2 = Intersection({log(2)/log(x/y)}, S.Reals) + assert solveset(f1, w, S.Reals) == sol1, solveset(f1, w, S.Reals) + assert solveset(f2, w, S.Reals) == sol2, solveset(f2, w, S.Reals) + + assert solveset(x**x, x, Interval.Lopen(0,oo)).dummy_eq( + ConditionSet(w, Eq(w**w, 0), Interval.open(0, oo))) + assert solveset(x**y - 1, y, S.Reals) == FiniteSet(0) + assert solveset(exp(x/y)*exp(-z/y) - 2, y, S.Reals) == \ + Complement(ConditionSet(y, Eq(im(x)/y, 0) & Eq(im(z)/y, 0), \ + Complement(Intersection(FiniteSet((x - z)/log(2)), S.Reals), FiniteSet(0))), FiniteSet(0)) + assert solveset(exp(xr/y)*exp(-zr/y) - 2, y, S.Reals) == \ + Complement(FiniteSet((xr - zr)/log(2)), FiniteSet(0)) + + assert solveset(a**x - b**x, x).dummy_eq(ConditionSet( + w, Ne(a, 0) & Ne(b, 0), FiniteSet(0))) + + +def test_ignore_assumptions(): + # make sure assumptions are ignored + xpos = symbols('x', positive=True) + x = symbols('x') + assert solveset_complex(xpos**2 - 4, xpos + ) == solveset_complex(x**2 - 4, x) + + +@XFAIL +def test_issue_10864(): + assert solveset(x**(y*z) - x, x, S.Reals) == FiniteSet(1) + + +@XFAIL +def test_solve_only_exp_2(): + assert solveset_real(sqrt(exp(x)) + sqrt(exp(-x)) - 4, x) == \ + FiniteSet(2*log(-sqrt(3) + 2), 2*log(sqrt(3) + 2)) + + +def test_is_exponential(): + assert _is_exponential(y, x) is False + assert _is_exponential(3**x - 2, x) is True + assert _is_exponential(5**x - 7**(2 - x), x) is True + assert _is_exponential(sin(2**x) - 4*x, x) is False + assert _is_exponential(x**y - z, y) is True + assert _is_exponential(x**y - z, x) is False + assert _is_exponential(2**x + 4**x - 1, x) is True + assert _is_exponential(x**(y*z) - x, x) is False + assert _is_exponential(x**(2*x) - 3**x, x) is False + assert _is_exponential(x**y - y*z, y) is False + assert _is_exponential(x**y - x*z, y) is True + + +def test_solve_exponential(): + assert _solve_exponential(3**(2*x) - 2**(x + 3), 0, x, S.Reals) == \ + FiniteSet(-3*log(2)/(-2*log(3) + log(2))) + assert _solve_exponential(2**y + 4**y, 1, y, S.Reals) == \ + FiniteSet(log(Rational(-1, 2) + sqrt(5)/2)/log(2)) + assert _solve_exponential(2**y + 4**y, 0, y, S.Reals) == \ + S.EmptySet + assert _solve_exponential(2**x + 3**x - 5**x, 0, x, S.Reals) == \ + ConditionSet(x, Eq(2**x + 3**x - 5**x, 0), S.Reals) + +# end of exponential tests + + +# logarithmic tests +def test_logarithmic(): + assert solveset_real(log(x - 3) + log(x + 3), x) == FiniteSet( + -sqrt(10), sqrt(10)) + assert solveset_real(log(x + 1) - log(2*x - 1), x) == FiniteSet(2) + assert solveset_real(log(x + 3) + log(1 + 3/x) - 3, x) == FiniteSet( + -3 + sqrt(-12 + exp(3))*exp(Rational(3, 2))/2 + exp(3)/2, + -sqrt(-12 + exp(3))*exp(Rational(3, 2))/2 - 3 + exp(3)/2) + + eq = z - log(x) + log(y/(x*(-1 + y**2/x**2))) + assert solveset_real(eq, x) == \ + Intersection(S.Reals, FiniteSet(-sqrt(y**2 - y*exp(z)), + sqrt(y**2 - y*exp(z)))) - \ + Intersection(S.Reals, FiniteSet(-sqrt(y**2), sqrt(y**2))) + assert solveset_real( + log(3*x) - log(-x + 1) - log(4*x + 1), x) == FiniteSet(Rational(-1, 2), S.Half) + assert solveset(log(x**y) - y*log(x), x, S.Reals) == S.Reals + +@XFAIL +def test_uselogcombine_2(): + eq = log(exp(2*x) + 1) + log(-tanh(x) + 1) - log(2) + assert solveset_real(eq, x) is S.EmptySet + eq = log(8*x) - log(sqrt(x) + 1) - 2 + assert solveset_real(eq, x) is S.EmptySet + + +def test_is_logarithmic(): + assert _is_logarithmic(y, x) is False + assert _is_logarithmic(log(x), x) is True + assert _is_logarithmic(log(x) - 3, x) is True + assert _is_logarithmic(log(x)*log(y), x) is True + assert _is_logarithmic(log(x)**2, x) is False + assert _is_logarithmic(log(x - 3) + log(x + 3), x) is True + assert _is_logarithmic(log(x**y) - y*log(x), x) is True + assert _is_logarithmic(sin(log(x)), x) is False + assert _is_logarithmic(x + y, x) is False + assert _is_logarithmic(log(3*x) - log(1 - x) + 4, x) is True + assert _is_logarithmic(log(x) + log(y) + x, x) is False + assert _is_logarithmic(log(log(x - 3)) + log(x - 3), x) is True + assert _is_logarithmic(log(log(3) + x) + log(x), x) is True + assert _is_logarithmic(log(x)*(y + 3) + log(x), y) is False + + +def test_solve_logarithm(): + y = Symbol('y') + assert _solve_logarithm(log(x**y) - y*log(x), 0, x, S.Reals) == S.Reals + y = Symbol('y', positive=True) + assert _solve_logarithm(log(x)*log(y), 0, x, S.Reals) == FiniteSet(1) + +# end of logarithmic tests + + +# lambert tests +def test_is_lambert(): + a, b, c = symbols('a,b,c') + assert _is_lambert(x**2, x) is False + assert _is_lambert(a**x**2+b*x+c, x) is True + assert _is_lambert(E**2, x) is False + assert _is_lambert(x*E**2, x) is False + assert _is_lambert(3*log(x) - x*log(3), x) is True + assert _is_lambert(log(log(x - 3)) + log(x-3), x) is True + assert _is_lambert(5*x - 1 + 3*exp(2 - 7*x), x) is True + assert _is_lambert((a/x + exp(x/2)).diff(x, 2), x) is True + assert _is_lambert((x**2 - 2*x + 1).subs(x, (log(x) + 3*x)**2 - 1), x) is True + assert _is_lambert(x*sinh(x) - 1, x) is True + assert _is_lambert(x*cos(x) - 5, x) is True + assert _is_lambert(tanh(x) - 5*x, x) is True + assert _is_lambert(cosh(x) - sinh(x), x) is False + +# end of lambert tests + + +def test_linear_coeffs(): + from sympy.solvers.solveset import linear_coeffs + assert linear_coeffs(0, x) == [0, 0] + assert all(i is S.Zero for i in linear_coeffs(0, x)) + assert linear_coeffs(x + 2*y + 3, x, y) == [1, 2, 3] + assert linear_coeffs(x + 2*y + 3, y, x) == [2, 1, 3] + assert linear_coeffs(x + 2*x**2 + 3, x, x**2) == [1, 2, 3] + raises(ValueError, lambda: + linear_coeffs(x + 2*x**2 + x**3, x, x**2)) + raises(ValueError, lambda: + linear_coeffs(1/x*(x - 1) + 1/x, x)) + raises(ValueError, lambda: + linear_coeffs(x, x, x)) + assert linear_coeffs(a*(x + y), x, y) == [a, a, 0] + assert linear_coeffs(1.0, x, y) == [0, 0, 1.0] + # don't include coefficients of 0 + assert linear_coeffs(Eq(x, x + y), x, y, dict=True) == {y: -1} + assert linear_coeffs(0, x, y, dict=True) == {} + + +def test_is_modular(): + assert _is_modular(y, x) is False + assert _is_modular(Mod(x, 3) - 1, x) is True + assert _is_modular(Mod(x**3 - 3*x**2 - x + 1, 3) - 1, x) is True + assert _is_modular(Mod(exp(x + y), 3) - 2, x) is True + assert _is_modular(Mod(exp(x + y), 3) - log(x), x) is True + assert _is_modular(Mod(x, 3) - 1, y) is False + assert _is_modular(Mod(x, 3)**2 - 5, x) is False + assert _is_modular(Mod(x, 3)**2 - y, x) is False + assert _is_modular(exp(Mod(x, 3)) - 1, x) is False + assert _is_modular(Mod(3, y) - 1, y) is False + + +def test_invert_modular(): + n = Dummy('n', integer=True) + from sympy.solvers.solveset import _invert_modular as invert_modular + + # no solutions + assert invert_modular(Mod(x, 12), S(1)/2, n, x) == (x, S.EmptySet) + # non invertible cases + assert invert_modular(Mod(sin(x), 7), S(5), n, x) == (Mod(sin(x), 7), 5) + assert invert_modular(Mod(exp(x), 7), S(5), n, x) == (Mod(exp(x), 7), 5) + assert invert_modular(Mod(log(x), 7), S(5), n, x) == (Mod(log(x), 7), 5) + # a is symbol + assert dumeq(invert_modular(Mod(x, 7), S(5), n, x), + (x, ImageSet(Lambda(n, 7*n + 5), S.Integers))) + # a.is_Add + assert dumeq(invert_modular(Mod(x + 8, 7), S(5), n, x), + (x, ImageSet(Lambda(n, 7*n + 4), S.Integers))) + assert invert_modular(Mod(x**2 + x, 7), S(5), n, x) == \ + (Mod(x**2 + x, 7), 5) + # a.is_Mul + assert dumeq(invert_modular(Mod(3*x, 7), S(5), n, x), + (x, ImageSet(Lambda(n, 7*n + 4), S.Integers))) + assert invert_modular(Mod((x + 1)*(x + 2), 7), S(5), n, x) == \ + (Mod((x + 1)*(x + 2), 7), 5) + # a.is_Pow + assert invert_modular(Mod(x**4, 7), S(5), n, x) == \ + (x, S.EmptySet) + assert dumeq(invert_modular(Mod(3**x, 4), S(3), n, x), + (x, ImageSet(Lambda(n, 2*n + 1), S.Naturals0))) + assert dumeq(invert_modular(Mod(2**(x**2 + x + 1), 7), S(2), n, x), + (x**2 + x + 1, ImageSet(Lambda(n, 3*n + 1), S.Naturals0))) + assert invert_modular(Mod(sin(x)**4, 7), S(5), n, x) == (x, S.EmptySet) + + +def test_solve_modular(): + n = Dummy('n', integer=True) + # if rhs has symbol (need to be implemented in future). + assert solveset(Mod(x, 4) - x, x, S.Integers + ).dummy_eq( + ConditionSet(x, Eq(-x + Mod(x, 4), 0), + S.Integers)) + # when _invert_modular fails to invert + assert solveset(3 - Mod(sin(x), 7), x, S.Integers + ).dummy_eq( + ConditionSet(x, Eq(Mod(sin(x), 7) - 3, 0), S.Integers)) + assert solveset(3 - Mod(log(x), 7), x, S.Integers + ).dummy_eq( + ConditionSet(x, Eq(Mod(log(x), 7) - 3, 0), S.Integers)) + assert solveset(3 - Mod(exp(x), 7), x, S.Integers + ).dummy_eq(ConditionSet(x, Eq(Mod(exp(x), 7) - 3, 0), + S.Integers)) + # EmptySet solution definitely + assert solveset(7 - Mod(x, 5), x, S.Integers) is S.EmptySet + assert solveset(5 - Mod(x, 5), x, S.Integers) is S.EmptySet + # Negative m + assert dumeq(solveset(2 + Mod(x, -3), x, S.Integers), + ImageSet(Lambda(n, -3*n - 2), S.Integers)) + assert solveset(4 + Mod(x, -3), x, S.Integers) is S.EmptySet + # linear expression in Mod + assert dumeq(solveset(3 - Mod(x, 5), x, S.Integers), + ImageSet(Lambda(n, 5*n + 3), S.Integers)) + assert dumeq(solveset(3 - Mod(5*x - 8, 7), x, S.Integers), + ImageSet(Lambda(n, 7*n + 5), S.Integers)) + assert dumeq(solveset(3 - Mod(5*x, 7), x, S.Integers), + ImageSet(Lambda(n, 7*n + 2), S.Integers)) + # higher degree expression in Mod + assert dumeq(solveset(Mod(x**2, 160) - 9, x, S.Integers), + Union(ImageSet(Lambda(n, 160*n + 3), S.Integers), + ImageSet(Lambda(n, 160*n + 13), S.Integers), + ImageSet(Lambda(n, 160*n + 67), S.Integers), + ImageSet(Lambda(n, 160*n + 77), S.Integers), + ImageSet(Lambda(n, 160*n + 83), S.Integers), + ImageSet(Lambda(n, 160*n + 93), S.Integers), + ImageSet(Lambda(n, 160*n + 147), S.Integers), + ImageSet(Lambda(n, 160*n + 157), S.Integers))) + assert solveset(3 - Mod(x**4, 7), x, S.Integers) is S.EmptySet + assert dumeq(solveset(Mod(x**4, 17) - 13, x, S.Integers), + Union(ImageSet(Lambda(n, 17*n + 3), S.Integers), + ImageSet(Lambda(n, 17*n + 5), S.Integers), + ImageSet(Lambda(n, 17*n + 12), S.Integers), + ImageSet(Lambda(n, 17*n + 14), S.Integers))) + # a.is_Pow tests + assert dumeq(solveset(Mod(7**x, 41) - 15, x, S.Integers), + ImageSet(Lambda(n, 40*n + 3), S.Naturals0)) + assert dumeq(solveset(Mod(12**x, 21) - 18, x, S.Integers), + ImageSet(Lambda(n, 6*n + 2), S.Naturals0)) + assert dumeq(solveset(Mod(3**x, 4) - 3, x, S.Integers), + ImageSet(Lambda(n, 2*n + 1), S.Naturals0)) + assert dumeq(solveset(Mod(2**x, 7) - 2 , x, S.Integers), + ImageSet(Lambda(n, 3*n + 1), S.Naturals0)) + assert dumeq(solveset(Mod(3**(3**x), 4) - 3, x, S.Integers), + Intersection(ImageSet(Lambda(n, Intersection({log(2*n + 1)/log(3)}, + S.Integers)), S.Naturals0), S.Integers)) + # Implemented for m without primitive root + assert solveset(Mod(x**3, 7) - 2, x, S.Integers) is S.EmptySet + assert dumeq(solveset(Mod(x**3, 8) - 1, x, S.Integers), + ImageSet(Lambda(n, 8*n + 1), S.Integers)) + assert dumeq(solveset(Mod(x**4, 9) - 4, x, S.Integers), + Union(ImageSet(Lambda(n, 9*n + 4), S.Integers), + ImageSet(Lambda(n, 9*n + 5), S.Integers))) + # domain intersection + assert dumeq(solveset(3 - Mod(5*x - 8, 7), x, S.Naturals0), + Intersection(ImageSet(Lambda(n, 7*n + 5), S.Integers), S.Naturals0)) + # Complex args + assert solveset(Mod(x, 3) - I, x, S.Integers) == \ + S.EmptySet + assert solveset(Mod(I*x, 3) - 2, x, S.Integers + ).dummy_eq( + ConditionSet(x, Eq(Mod(I*x, 3) - 2, 0), S.Integers)) + assert solveset(Mod(I + x, 3) - 2, x, S.Integers + ).dummy_eq( + ConditionSet(x, Eq(Mod(x + I, 3) - 2, 0), S.Integers)) + + # issue 17373 (https://github.com/sympy/sympy/issues/17373) + assert dumeq(solveset(Mod(x**4, 14) - 11, x, S.Integers), + Union(ImageSet(Lambda(n, 14*n + 3), S.Integers), + ImageSet(Lambda(n, 14*n + 11), S.Integers))) + assert dumeq(solveset(Mod(x**31, 74) - 43, x, S.Integers), + ImageSet(Lambda(n, 74*n + 31), S.Integers)) + + # issue 13178 + n = symbols('n', integer=True) + a = 742938285 + b = 1898888478 + m = 2**31 - 1 + c = 20170816 + assert dumeq(solveset(c - Mod(a**n*b, m), n, S.Integers), + ImageSet(Lambda(n, 2147483646*n + 100), S.Naturals0)) + assert dumeq(solveset(c - Mod(a**n*b, m), n, S.Naturals0), + Intersection(ImageSet(Lambda(n, 2147483646*n + 100), S.Naturals0), + S.Naturals0)) + assert dumeq(solveset(c - Mod(a**(2*n)*b, m), n, S.Integers), + Intersection(ImageSet(Lambda(n, 1073741823*n + 50), S.Naturals0), + S.Integers)) + assert solveset(c - Mod(a**(2*n + 7)*b, m), n, S.Integers) is S.EmptySet + assert dumeq(solveset(c - Mod(a**(n - 4)*b, m), n, S.Integers), + Intersection(ImageSet(Lambda(n, 2147483646*n + 104), S.Naturals0), + S.Integers)) + +# end of modular tests + +def test_issue_17276(): + assert nonlinsolve([Eq(x, 5**(S(1)/5)), Eq(x*y, 25*sqrt(5))], x, y) == \ + FiniteSet((5**(S(1)/5), 25*5**(S(3)/10))) + + +def test_issue_10426(): + x = Dummy('x') + a = Symbol('a') + n = Dummy('n') + assert (solveset(sin(x + a) - sin(x), a)).dummy_eq(Dummy('x')) == (Union( + ImageSet(Lambda(n, 2*n*pi), S.Integers), + Intersection(S.Complexes, ImageSet(Lambda(n, -I*(I*(2*n*pi + arg(-exp(-2*I*x))) + 2*im(x))), + S.Integers)))).dummy_eq(Dummy('x,n')) + + +def test_solveset_conjugate(): + """Test solveset for simple conjugate functions""" + assert solveset(conjugate(x) -3 + I) == FiniteSet(3 + I) + + +def test_issue_18208(): + variables = symbols('x0:16') + symbols('y0:12') + x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15,\ + y0, y1, y2, y3, y4, y5, y6, y7, y8, y9, y10, y11 = variables + + eqs = [x0 + x1 + x2 + x3 - 51, + x0 + x1 + x4 + x5 - 46, + x2 + x3 + x6 + x7 - 39, + x0 + x3 + x4 + x7 - 50, + x1 + x2 + x5 + x6 - 35, + x4 + x5 + x6 + x7 - 34, + x4 + x5 + x8 + x9 - 46, + x10 + x11 + x6 + x7 - 23, + x11 + x4 + x7 + x8 - 25, + x10 + x5 + x6 + x9 - 44, + x10 + x11 + x8 + x9 - 35, + x12 + x13 + x8 + x9 - 35, + x10 + x11 + x14 + x15 - 29, + x11 + x12 + x15 + x8 - 35, + x10 + x13 + x14 + x9 - 29, + x12 + x13 + x14 + x15 - 29, + y0 + y1 + y2 + y3 - 55, + y0 + y1 + y4 + y5 - 53, + y2 + y3 + y6 + y7 - 56, + y0 + y3 + y4 + y7 - 57, + y1 + y2 + y5 + y6 - 52, + y4 + y5 + y6 + y7 - 54, + y4 + y5 + y8 + y9 - 48, + y10 + y11 + y6 + y7 - 60, + y11 + y4 + y7 + y8 - 51, + y10 + y5 + y6 + y9 - 57, + y10 + y11 + y8 + y9 - 54, + x10 - 2, + x11 - 5, + x12 - 1, + x13 - 6, + x14 - 1, + x15 - 21, + y0 - 12, + y1 - 20] + + expected = [38 - x3, x3 - 10, 23 - x3, x3, 12 - x7, x7 + 6, 16 - x7, x7, + 8, 20, 2, 5, 1, 6, 1, 21, 12, 20, -y11 + y9 + 2, y11 - y9 + 21, + -y11 - y7 + y9 + 24, y11 + y7 - y9 - 3, 33 - y7, y7, 27 - y9, y9, + 27 - y11, y11] + + A, b = linear_eq_to_matrix(eqs, variables) + + # solve + solve_expected = {v:eq for v, eq in zip(variables, expected) if v != eq} + + assert solve(eqs, variables) == solve_expected + + # linsolve + linsolve_expected = FiniteSet(Tuple(*expected)) + + assert linsolve(eqs, variables) == linsolve_expected + assert linsolve((A, b), variables) == linsolve_expected + + # gauss_jordan_solve + gj_solve, new_vars = A.gauss_jordan_solve(b) + gj_solve = list(gj_solve) + + gj_expected = linsolve_expected.subs(zip([x3, x7, y7, y9, y11], new_vars)) + + assert FiniteSet(Tuple(*gj_solve)) == gj_expected + + # nonlinsolve + # The solution set of nonlinsolve is currently equivalent to linsolve and is + # also correct. However, we would prefer to use the same symbols as parameters + # for the solution to the underdetermined system in all cases if possible. + # We want a solution that is not just equivalent but also given in the same form. + # This test may be changed should nonlinsolve be modified in this way. + + nonlinsolve_expected = FiniteSet((38 - x3, x3 - 10, 23 - x3, x3, 12 - x7, x7 + 6, + 16 - x7, x7, 8, 20, 2, 5, 1, 6, 1, 21, 12, 20, + -y5 + y7 - 1, y5 - y7 + 24, 21 - y5, y5, 33 - y7, + y7, 27 - y9, y9, -y5 + y7 - y9 + 24, y5 - y7 + y9 + 3)) + + assert nonlinsolve(eqs, variables) == nonlinsolve_expected + + +def test_substitution_with_infeasible_solution(): + a00, a01, a10, a11, l0, l1, l2, l3, m0, m1, m2, m3, m4, m5, m6, m7, c00, c01, c10, c11, p00, p01, p10, p11 = symbols( + 'a00, a01, a10, a11, l0, l1, l2, l3, m0, m1, m2, m3, m4, m5, m6, m7, c00, c01, c10, c11, p00, p01, p10, p11' + ) + solvefor = [p00, p01, p10, p11, c00, c01, c10, c11, m0, m1, m3, l0, l1, l2, l3] + system = [ + -l0 * c00 - l1 * c01 + m0 + c00 + c01, + -l0 * c10 - l1 * c11 + m1, + -l2 * c00 - l3 * c01 + c00 + c01, + -l2 * c10 - l3 * c11 + m3, + -l0 * p00 - l2 * p10 + p00 + p10, + -l1 * p00 - l3 * p10 + p00 + p10, + -l0 * p01 - l2 * p11, + -l1 * p01 - l3 * p11, + -a00 + c00 * p00 + c10 * p01, + -a01 + c01 * p00 + c11 * p01, + -a10 + c00 * p10 + c10 * p11, + -a11 + c01 * p10 + c11 * p11, + -m0 * p00, + -m1 * p01, + -m2 * p10, + -m3 * p11, + -m4 * c00, + -m5 * c01, + -m6 * c10, + -m7 * c11, + m2, + m4, + m5, + m6, + m7 + ] + sol = FiniteSet( + (0, Complement(FiniteSet(p01), FiniteSet(0)), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, l2, l3), + (p00, Complement(FiniteSet(p01), FiniteSet(0)), 0, p11, 0, 0, 0, 0, 0, 0, 0, 1, 1, -p01/p11, -p01/p11), + (0, Complement(FiniteSet(p01), FiniteSet(0)), 0, p11, 0, 0, 0, 0, 0, 0, 0, 1, -l3*p11/p01, -p01/p11, l3), + (0, Complement(FiniteSet(p01), FiniteSet(0)), 0, p11, 0, 0, 0, 0, 0, 0, 0, -l2*p11/p01, -l3*p11/p01, l2, l3), + ) + assert sol != nonlinsolve(system, solvefor) + + +def test_issue_20097(): + assert solveset(1/sqrt(x)) is S.EmptySet + + +def test_issue_15350(): + assert solveset(diff(sqrt(1/x+x))) == FiniteSet(-1, 1) + + +def test_issue_18359(): + c1 = Piecewise((0, x < 0), (Min(1, x)/2 - Min(2, x)/2 + Min(3, x)/2, True)) + c2 = Piecewise((Piecewise((0, x < 0), (Min(1, x)/2 - Min(2, x)/2 + Min(3, x)/2, True)), x >= 0), (0, True)) + correct_result = Interval(1, 2) + result1 = solveset(c1 - Rational(1, 2), x, Interval(0, 3)) + result2 = solveset(c2 - Rational(1, 2), x, Interval(0, 3)) + assert result1 == correct_result + assert result2 == correct_result + + +def test_issue_17604(): + lhs = -2**(3*x/11)*exp(x/11) + pi**(x/11) + assert _is_exponential(lhs, x) + assert _solve_exponential(lhs, 0, x, S.Complexes) == FiniteSet(0) + + +def test_issue_17580(): + assert solveset(1/(1 - x**3)**2, x, S.Reals) is S.EmptySet + + +def test_issue_17566_actual(): + sys = [2**x + 2**y - 3, 4**x + 9**y - 5] + # Not clear this is the correct result, but at least no recursion error + assert nonlinsolve(sys, x, y) == FiniteSet((log(3 - 2**y)/log(2), y)) + + +def test_issue_17565(): + eq = Ge(2*(x - 2)**2/(3*(x + 1)**(Integer(1)/3)) + 2*(x - 2)*(x + 1)**(Integer(2)/3), 0) + res = Union(Interval.Lopen(-1, -Rational(1, 4)), Interval(2, oo)) + assert solveset(eq, x, S.Reals) == res + + +def test_issue_15024(): + function = (x + 5)/sqrt(-x**2 - 10*x) + assert solveset(function, x, S.Reals) == FiniteSet(Integer(-5)) + + +def test_issue_16877(): + assert dumeq(nonlinsolve([x - 1, sin(y)], x, y), + FiniteSet((1, ImageSet(Lambda(n, 2*n*pi), S.Integers)), + (1, ImageSet(Lambda(n, 2*n*pi + pi), S.Integers)))) + # Even better if (1, ImageSet(Lambda(n, n*pi), S.Integers)) is obtained + + +def test_issue_16876(): + assert dumeq(nonlinsolve([sin(x), 2*x - 4*y], x, y), + FiniteSet((ImageSet(Lambda(n, 2*n*pi), S.Integers), + ImageSet(Lambda(n, n*pi), S.Integers)), + (ImageSet(Lambda(n, 2*n*pi + pi), S.Integers), + ImageSet(Lambda(n, n*pi + pi/2), S.Integers)))) + # Even better if (ImageSet(Lambda(n, n*pi), S.Integers), + # ImageSet(Lambda(n, n*pi/2), S.Integers)) is obtained + +def test_issue_21236(): + x, z = symbols("x z") + y = symbols('y', rational=True) + assert solveset(x**y - z, x, S.Reals) == ConditionSet(x, Eq(x**y - z, 0), S.Reals) + e1, e2 = symbols('e1 e2', even=True) + y = e1/e2 # don't know if num or den will be odd and the other even + assert solveset(x**y - z, x, S.Reals) == ConditionSet(x, Eq(x**y - z, 0), S.Reals) + + +def test_issue_21908(): + assert nonlinsolve([(x**2 + 2*x - y**2)*exp(x), -2*y*exp(x)], x, y + ) == {(-2, 0), (0, 0)} + + +def test_issue_19144(): + # test case 1 + expr1 = [x + y - 1, y**2 + 1] + eq1 = [Eq(i, 0) for i in expr1] + soln1 = {(1 - I, I), (1 + I, -I)} + soln_expr1 = nonlinsolve(expr1, [x, y]) + soln_eq1 = nonlinsolve(eq1, [x, y]) + assert soln_eq1 == soln_expr1 == soln1 + # test case 2 - with denoms + expr2 = [x/y - 1, y**2 + 1] + eq2 = [Eq(i, 0) for i in expr2] + soln2 = {(-I, -I), (I, I)} + soln_expr2 = nonlinsolve(expr2, [x, y]) + soln_eq2 = nonlinsolve(eq2, [x, y]) + assert soln_eq2 == soln_expr2 == soln2 + # denominators that cancel in expression + assert nonlinsolve([Eq(x + 1/x, 1/x)], [x]) == FiniteSet((S.EmptySet,)) + + +def test_issue_22413(): + res = nonlinsolve((4*y*(2*x + 2*exp(y) + 1)*exp(2*x), + 4*x*exp(2*x) + 4*y*exp(2*x + y) + 4*exp(2*x + y) + 1), + x, y) + # First solution is not correct, but the issue was an exception + sols = FiniteSet((x, S.Zero), (-exp(y) - S.Half, y)) + assert res == sols + + +def test_issue_23318(): + eqs_eq = [ + Eq(53.5780461486929, x * log(y / (5.0 - y) + 1) / y), + Eq(x, 0.0015 * z), + Eq(0.0015, 7845.32 * y / z), + ] + eqs_expr = [eq.lhs - eq.rhs for eq in eqs_eq] + + sol = {(266.97755814852, 0.0340301680681629, 177985.03876568)} + + assert_close_nl(nonlinsolve(eqs_eq, [x, y, z]), sol) + assert_close_nl(nonlinsolve(eqs_expr, [x, y, z]), sol) + + logterm = log(1.91196789933362e-7*z/(5.0 - 1.91196789933362e-7*z) + 1) + eq = -0.0015*z*logterm + 1.02439504345316e-5*z + assert_close_ss(solveset(eq, z), {0, 177985.038765679}) + + +def test_issue_19814(): + assert nonlinsolve([ 2**m - 2**(2*n), 4*2**m - 2**(4*n)], m, n + ) == FiniteSet((log(2**(2*n))/log(2), S.Complexes)) + + +def test_issue_22058(): + sol = solveset(-sqrt(t)*x**2 + 2*x + sqrt(t), x, S.Reals) + # doesn't fail (and following numerical check) + assert sol.xreplace({t: 1}) == {1 - sqrt(2), 1 + sqrt(2)}, sol.xreplace({t: 1}) + + +def test_issue_11184(): + assert solveset(20*sqrt(y**2 + (sqrt(-(y - 10)*(y + 10)) + 10)**2) - 60, y, S.Reals) is S.EmptySet + + +def test_issue_21890(): + e = S(2)/3 + assert nonlinsolve([4*x**3*y**4 - 2*y, 4*x**4*y**3 - 2*x], x, y) == { + (2**e/(2*y), y), ((-2**e/4 - 2**e*sqrt(3)*I/4)/y, y), + ((-2**e/4 + 2**e*sqrt(3)*I/4)/y, y)} + assert nonlinsolve([(1 - 4*x**2)*exp(-2*x**2 - 2*y**2), + -4*x*y*exp(-2*x**2)*exp(-2*y**2)], x, y) == {(-S(1)/2, 0), (S(1)/2, 0)} + rx, ry = symbols('x y', real=True) + sol = nonlinsolve([4*rx**3*ry**4 - 2*ry, 4*rx**4*ry**3 - 2*rx], rx, ry) + ans = {(2**(S(2)/3)/(2*ry), ry), + ((-2**(S(2)/3)/4 - 2**(S(2)/3)*sqrt(3)*I/4)/ry, ry), + ((-2**(S(2)/3)/4 + 2**(S(2)/3)*sqrt(3)*I/4)/ry, ry)} + assert sol == ans + + +def test_issue_22628(): + assert nonlinsolve([h - 1, k - 1, f - 2, f - 4, -2*k], h, k, f) == S.EmptySet + assert nonlinsolve([x**3 - 1, x + y, x**2 - 4], [x, y]) == S.EmptySet + + +def test_issue_25781(): + assert solve(sqrt(x/2) - x) == [0, S.Half] + + +def test_issue_26077(): + _n = Symbol('_n') + function = x*cot(5*x) + critical_points = stationary_points(function, x, S.Reals) + excluded_points = Union( + ImageSet(Lambda(_n, 2*_n*pi/5), S.Integers), + ImageSet(Lambda(_n, 2*_n*pi/5 + pi/5), S.Integers) + ) + solution = ConditionSet(x, + Eq(x*(-5*cot(5*x)**2 - 5) + cot(5*x), 0), + Complement(S.Reals, excluded_points) + ) + assert solution.as_dummy() == critical_points.as_dummy() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..adb79261954924305c1837555d7d47cd53b8430b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/__init__.py @@ -0,0 +1,202 @@ +""" +SymPy statistics module + +Introduces a random variable type into the SymPy language. + +Random variables may be declared using prebuilt functions such as +Normal, Exponential, Coin, Die, etc... or built with functions like FiniteRV. + +Queries on random expressions can be made using the functions + +========================= ============================= + Expression Meaning +------------------------- ----------------------------- + ``P(condition)`` Probability + ``E(expression)`` Expected value + ``H(expression)`` Entropy + ``variance(expression)`` Variance + ``density(expression)`` Probability Density Function + ``sample(expression)`` Produce a realization + ``where(condition)`` Where the condition is true +========================= ============================= + +Examples +======== + +>>> from sympy.stats import P, E, variance, Die, Normal +>>> from sympy import simplify +>>> X, Y = Die('X', 6), Die('Y', 6) # Define two six sided dice +>>> Z = Normal('Z', 0, 1) # Declare a Normal random variable with mean 0, std 1 +>>> P(X>3) # Probability X is greater than 3 +1/2 +>>> E(X+Y) # Expectation of the sum of two dice +7 +>>> variance(X+Y) # Variance of the sum of two dice +35/6 +>>> simplify(P(Z>1)) # Probability of Z being greater than 1 +1/2 - erf(sqrt(2)/2)/2 + + +One could also create custom distribution and define custom random variables +as follows: + +1. If you want to create a Continuous Random Variable: + +>>> from sympy.stats import ContinuousRV, P, E +>>> from sympy import exp, Symbol, Interval, oo +>>> x = Symbol('x') +>>> pdf = exp(-x) # pdf of the Continuous Distribution +>>> Z = ContinuousRV(x, pdf, set=Interval(0, oo)) +>>> E(Z) +1 +>>> P(Z > 5) +exp(-5) + +1.1 To create an instance of Continuous Distribution: + +>>> from sympy.stats import ContinuousDistributionHandmade +>>> from sympy import Lambda +>>> dist = ContinuousDistributionHandmade(Lambda(x, pdf), set=Interval(0, oo)) +>>> dist.pdf(x) +exp(-x) + +2. If you want to create a Discrete Random Variable: + +>>> from sympy.stats import DiscreteRV, P, E +>>> from sympy import Symbol, S +>>> p = S(1)/2 +>>> x = Symbol('x', integer=True, positive=True) +>>> pdf = p*(1 - p)**(x - 1) +>>> D = DiscreteRV(x, pdf, set=S.Naturals) +>>> E(D) +2 +>>> P(D > 3) +1/8 + +2.1 To create an instance of Discrete Distribution: + +>>> from sympy.stats import DiscreteDistributionHandmade +>>> from sympy import Lambda +>>> dist = DiscreteDistributionHandmade(Lambda(x, pdf), set=S.Naturals) +>>> dist.pdf(x) +2**(1 - x)/2 + +3. If you want to create a Finite Random Variable: + +>>> from sympy.stats import FiniteRV, P, E +>>> from sympy import Rational, Eq +>>> pmf = {1: Rational(1, 3), 2: Rational(1, 6), 3: Rational(1, 4), 4: Rational(1, 4)} +>>> X = FiniteRV('X', pmf) +>>> E(X) +29/12 +>>> P(X > 3) +1/4 + +3.1 To create an instance of Finite Distribution: + +>>> from sympy.stats import FiniteDistributionHandmade +>>> dist = FiniteDistributionHandmade(pmf) +>>> dist.pmf(x) +Lambda(x, Piecewise((1/3, Eq(x, 1)), (1/6, Eq(x, 2)), (1/4, Eq(x, 3) | Eq(x, 4)), (0, True))) +""" + +__all__ = [ + 'P', 'E', 'H', 'density', 'where', 'given', 'sample', 'cdf','median', + 'characteristic_function', 'pspace', 'sample_iter', 'variance', 'std', + 'skewness', 'kurtosis', 'covariance', 'dependent', 'entropy', 'independent', + 'random_symbols', 'correlation', 'factorial_moment', 'moment', 'cmoment', + 'sampling_density', 'moment_generating_function', 'smoment', 'quantile', + 'coskewness', 'sample_stochastic_process', + + 'FiniteRV', 'DiscreteUniform', 'Die', 'Bernoulli', 'Coin', 'Binomial', + 'BetaBinomial', 'Hypergeometric', 'Rademacher', 'IdealSoliton', 'RobustSoliton', + 'FiniteDistributionHandmade', + + 'ContinuousRV', 'Arcsin', 'Benini', 'Beta', 'BetaNoncentral', 'BetaPrime', + 'BoundedPareto', 'Cauchy', 'Chi', 'ChiNoncentral', 'ChiSquared', 'Dagum', 'Davis', 'Erlang', + 'ExGaussian', 'Exponential', 'ExponentialPower', 'FDistribution', + 'FisherZ', 'Frechet', 'Gamma', 'GammaInverse', 'Gompertz', 'Gumbel', + 'Kumaraswamy', 'Laplace', 'Levy', 'Logistic','LogCauchy', 'LogLogistic', 'LogitNormal', 'LogNormal', 'Lomax', + 'Moyal', 'Maxwell', 'Nakagami', 'Normal', 'GaussianInverse', 'Pareto', 'PowerFunction', + 'QuadraticU', 'RaisedCosine', 'Rayleigh','Reciprocal', 'StudentT', 'ShiftedGompertz', + 'Trapezoidal', 'Triangular', 'Uniform', 'UniformSum', 'VonMises', 'Wald', + 'Weibull', 'WignerSemicircle', 'ContinuousDistributionHandmade', + + 'FlorySchulz', 'Geometric','Hermite', 'Logarithmic', 'NegativeBinomial', 'Poisson', 'Skellam', + 'YuleSimon', 'Zeta', 'DiscreteRV', 'DiscreteDistributionHandmade', + + 'JointRV', 'Dirichlet', 'GeneralizedMultivariateLogGamma', + 'GeneralizedMultivariateLogGammaOmega', 'Multinomial', 'MultivariateBeta', + 'MultivariateEwens', 'MultivariateT', 'NegativeMultinomial', + 'NormalGamma', 'MultivariateNormal', 'MultivariateLaplace', 'marginal_distribution', + + 'StochasticProcess', 'DiscreteTimeStochasticProcess', + 'DiscreteMarkovChain', 'TransitionMatrixOf', 'StochasticStateSpaceOf', + 'GeneratorMatrixOf', 'ContinuousMarkovChain', 'BernoulliProcess', + 'PoissonProcess', 'WienerProcess', 'GammaProcess', + + 'CircularEnsemble', 'CircularUnitaryEnsemble', + 'CircularOrthogonalEnsemble', 'CircularSymplecticEnsemble', + 'GaussianEnsemble', 'GaussianUnitaryEnsemble', + 'GaussianOrthogonalEnsemble', 'GaussianSymplecticEnsemble', + 'joint_eigen_distribution', 'JointEigenDistribution', + 'level_spacing_distribution', + + 'MatrixGamma', 'Wishart', 'MatrixNormal', 'MatrixStudentT', + + 'Probability', 'Expectation', 'Variance', 'Covariance', 'Moment', + 'CentralMoment', + + 'ExpectationMatrix', 'VarianceMatrix', 'CrossCovarianceMatrix' + +] +from .rv_interface import (P, E, H, density, where, given, sample, cdf, median, + characteristic_function, pspace, sample_iter, variance, std, skewness, + kurtosis, covariance, dependent, entropy, independent, random_symbols, + correlation, factorial_moment, moment, cmoment, sampling_density, + moment_generating_function, smoment, quantile, coskewness, + sample_stochastic_process) + +from .frv_types import (FiniteRV, DiscreteUniform, Die, Bernoulli, Coin, + Binomial, BetaBinomial, Hypergeometric, Rademacher, + FiniteDistributionHandmade, IdealSoliton, RobustSoliton) + +from .crv_types import (ContinuousRV, Arcsin, Benini, Beta, BetaNoncentral, + BetaPrime, BoundedPareto, Cauchy, Chi, ChiNoncentral, ChiSquared, + Dagum, Davis, Erlang, ExGaussian, Exponential, ExponentialPower, + FDistribution, FisherZ, Frechet, Gamma, GammaInverse, GaussianInverse, + Gompertz, Gumbel, Kumaraswamy, Laplace, Levy, Logistic, LogCauchy, + LogLogistic, LogitNormal, LogNormal, Lomax, Maxwell, Moyal, Nakagami, + Normal, Pareto, QuadraticU, RaisedCosine, Rayleigh, Reciprocal, + StudentT, PowerFunction, ShiftedGompertz, Trapezoidal, Triangular, + Uniform, UniformSum, VonMises, Wald, Weibull, WignerSemicircle, + ContinuousDistributionHandmade) + +from .drv_types import (FlorySchulz, Geometric, Hermite, Logarithmic, NegativeBinomial, Poisson, + Skellam, YuleSimon, Zeta, DiscreteRV, DiscreteDistributionHandmade) + +from .joint_rv_types import (JointRV, Dirichlet, + GeneralizedMultivariateLogGamma, GeneralizedMultivariateLogGammaOmega, + Multinomial, MultivariateBeta, MultivariateEwens, MultivariateT, + NegativeMultinomial, NormalGamma, MultivariateNormal, MultivariateLaplace, + marginal_distribution) + +from .stochastic_process_types import (StochasticProcess, + DiscreteTimeStochasticProcess, DiscreteMarkovChain, + TransitionMatrixOf, StochasticStateSpaceOf, GeneratorMatrixOf, + ContinuousMarkovChain, BernoulliProcess, PoissonProcess, WienerProcess, + GammaProcess) + +from .random_matrix_models import (CircularEnsemble, CircularUnitaryEnsemble, + CircularOrthogonalEnsemble, CircularSymplecticEnsemble, + GaussianEnsemble, GaussianUnitaryEnsemble, GaussianOrthogonalEnsemble, + GaussianSymplecticEnsemble, joint_eigen_distribution, + JointEigenDistribution, level_spacing_distribution) + +from .matrix_distributions import MatrixGamma, Wishart, MatrixNormal, MatrixStudentT + +from .symbolic_probability import (Probability, Expectation, Variance, + Covariance, Moment, CentralMoment) + +from .symbolic_multivariate_probability import (ExpectationMatrix, VarianceMatrix, + CrossCovarianceMatrix) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/compound_rv.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/compound_rv.py new file mode 100644 index 0000000000000000000000000000000000000000..27555f4233fe691bac303800a87736205acbdee6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/compound_rv.py @@ -0,0 +1,223 @@ +from sympy.concrete.summations import Sum +from sympy.core.basic import Basic +from sympy.core.function import Lambda +from sympy.core.symbol import Dummy +from sympy.integrals.integrals import Integral +from sympy.stats.rv import (NamedArgsMixin, random_symbols, _symbol_converter, + PSpace, RandomSymbol, is_random, Distribution) +from sympy.stats.crv import ContinuousDistribution, SingleContinuousPSpace +from sympy.stats.drv import DiscreteDistribution, SingleDiscretePSpace +from sympy.stats.frv import SingleFiniteDistribution, SingleFinitePSpace +from sympy.stats.crv_types import ContinuousDistributionHandmade +from sympy.stats.drv_types import DiscreteDistributionHandmade +from sympy.stats.frv_types import FiniteDistributionHandmade + + +class CompoundPSpace(PSpace): + """ + A temporary Probability Space for the Compound Distribution. After + Marginalization, this returns the corresponding Probability Space of the + parent distribution. + """ + + def __new__(cls, s, distribution): + s = _symbol_converter(s) + if isinstance(distribution, ContinuousDistribution): + return SingleContinuousPSpace(s, distribution) + if isinstance(distribution, DiscreteDistribution): + return SingleDiscretePSpace(s, distribution) + if isinstance(distribution, SingleFiniteDistribution): + return SingleFinitePSpace(s, distribution) + if not isinstance(distribution, CompoundDistribution): + raise ValueError("%s should be an isinstance of " + "CompoundDistribution"%(distribution)) + return Basic.__new__(cls, s, distribution) + + @property + def value(self): + return RandomSymbol(self.symbol, self) + + @property + def symbol(self): + return self.args[0] + + @property + def is_Continuous(self): + return self.distribution.is_Continuous + + @property + def is_Finite(self): + return self.distribution.is_Finite + + @property + def is_Discrete(self): + return self.distribution.is_Discrete + + @property + def distribution(self): + return self.args[1] + + @property + def pdf(self): + return self.distribution.pdf(self.symbol) + + @property + def set(self): + return self.distribution.set + + @property + def domain(self): + return self._get_newpspace().domain + + def _get_newpspace(self, evaluate=False): + x = Dummy('x') + parent_dist = self.distribution.args[0] + func = Lambda(x, self.distribution.pdf(x, evaluate)) + new_pspace = self._transform_pspace(self.symbol, parent_dist, func) + if new_pspace is not None: + return new_pspace + message = ("Compound Distribution for %s is not implemented yet" % str(parent_dist)) + raise NotImplementedError(message) + + def _transform_pspace(self, sym, dist, pdf): + """ + This function returns the new pspace of the distribution using handmade + Distributions and their corresponding pspace. + """ + pdf = Lambda(sym, pdf(sym)) + _set = dist.set + if isinstance(dist, ContinuousDistribution): + return SingleContinuousPSpace(sym, ContinuousDistributionHandmade(pdf, _set)) + elif isinstance(dist, DiscreteDistribution): + return SingleDiscretePSpace(sym, DiscreteDistributionHandmade(pdf, _set)) + elif isinstance(dist, SingleFiniteDistribution): + dens = {k: pdf(k) for k in _set} + return SingleFinitePSpace(sym, FiniteDistributionHandmade(dens)) + + def compute_density(self, expr, *, compound_evaluate=True, **kwargs): + new_pspace = self._get_newpspace(compound_evaluate) + expr = expr.subs({self.value: new_pspace.value}) + return new_pspace.compute_density(expr, **kwargs) + + def compute_cdf(self, expr, *, compound_evaluate=True, **kwargs): + new_pspace = self._get_newpspace(compound_evaluate) + expr = expr.subs({self.value: new_pspace.value}) + return new_pspace.compute_cdf(expr, **kwargs) + + def compute_expectation(self, expr, rvs=None, evaluate=False, **kwargs): + new_pspace = self._get_newpspace(evaluate) + expr = expr.subs({self.value: new_pspace.value}) + if rvs: + rvs = rvs.subs({self.value: new_pspace.value}) + if isinstance(new_pspace, SingleFinitePSpace): + return new_pspace.compute_expectation(expr, rvs, **kwargs) + return new_pspace.compute_expectation(expr, rvs, evaluate, **kwargs) + + def probability(self, condition, *, compound_evaluate=True, **kwargs): + new_pspace = self._get_newpspace(compound_evaluate) + condition = condition.subs({self.value: new_pspace.value}) + return new_pspace.probability(condition) + + def conditional_space(self, condition, *, compound_evaluate=True, **kwargs): + new_pspace = self._get_newpspace(compound_evaluate) + condition = condition.subs({self.value: new_pspace.value}) + return new_pspace.conditional_space(condition) + + +class CompoundDistribution(Distribution, NamedArgsMixin): + """ + Class for Compound Distributions. + + Parameters + ========== + + dist : Distribution + Distribution must contain a random parameter + + Examples + ======== + + >>> from sympy.stats.compound_rv import CompoundDistribution + >>> from sympy.stats.crv_types import NormalDistribution + >>> from sympy.stats import Normal + >>> from sympy.abc import x + >>> X = Normal('X', 2, 4) + >>> N = NormalDistribution(X, 4) + >>> C = CompoundDistribution(N) + >>> C.set + Interval(-oo, oo) + >>> C.pdf(x, evaluate=True).simplify() + exp(-x**2/64 + x/16 - 1/16)/(8*sqrt(pi)) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Compound_probability_distribution + + """ + + def __new__(cls, dist): + if not isinstance(dist, (ContinuousDistribution, + SingleFiniteDistribution, DiscreteDistribution)): + message = "Compound Distribution for %s is not implemented yet" % str(dist) + raise NotImplementedError(message) + if not cls._compound_check(dist): + return dist + return Basic.__new__(cls, dist) + + @property + def set(self): + return self.args[0].set + + @property + def is_Continuous(self): + return isinstance(self.args[0], ContinuousDistribution) + + @property + def is_Finite(self): + return isinstance(self.args[0], SingleFiniteDistribution) + + @property + def is_Discrete(self): + return isinstance(self.args[0], DiscreteDistribution) + + def pdf(self, x, evaluate=False): + dist = self.args[0] + randoms = [rv for rv in dist.args if is_random(rv)] + if isinstance(dist, SingleFiniteDistribution): + y = Dummy('y', integer=True, negative=False) + expr = dist.pmf(y) + else: + y = Dummy('y') + expr = dist.pdf(y) + for rv in randoms: + expr = self._marginalise(expr, rv, evaluate) + return Lambda(y, expr)(x) + + def _marginalise(self, expr, rv, evaluate): + if isinstance(rv.pspace.distribution, SingleFiniteDistribution): + rv_dens = rv.pspace.distribution.pmf(rv) + else: + rv_dens = rv.pspace.distribution.pdf(rv) + rv_dom = rv.pspace.domain.set + if rv.pspace.is_Discrete or rv.pspace.is_Finite: + expr = Sum(expr*rv_dens, (rv, rv_dom._inf, + rv_dom._sup)) + else: + expr = Integral(expr*rv_dens, (rv, rv_dom._inf, + rv_dom._sup)) + if evaluate: + return expr.doit() + return expr + + @classmethod + def _compound_check(self, dist): + """ + Checks if the given distribution contains random parameters. + """ + randoms = [] + for arg in dist.args: + randoms.extend(random_symbols(arg)) + if len(randoms) == 0: + return False + return True diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/crv.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/crv.py new file mode 100644 index 0000000000000000000000000000000000000000..0a5184029679f663c83d81aa6c1b6ca4d948c70f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/crv.py @@ -0,0 +1,570 @@ +""" +Continuous Random Variables Module + +See Also +======== +sympy.stats.crv_types +sympy.stats.rv +sympy.stats.frv +""" + + +from sympy.core.basic import Basic +from sympy.core.cache import cacheit +from sympy.core.function import Lambda, PoleError +from sympy.core.numbers import (I, nan, oo) +from sympy.core.relational import (Eq, Ne) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, symbols) +from sympy.core.sympify import _sympify, sympify +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.special.delta_functions import DiracDelta +from sympy.integrals.integrals import (Integral, integrate) +from sympy.logic.boolalg import (And, Or) +from sympy.polys.polyerrors import PolynomialError +from sympy.polys.polytools import poly +from sympy.series.series import series +from sympy.sets.sets import (FiniteSet, Intersection, Interval, Union) +from sympy.solvers.solveset import solveset +from sympy.solvers.inequalities import reduce_rational_inequalities +from sympy.stats.rv import (RandomDomain, SingleDomain, ConditionalDomain, is_random, + ProductDomain, PSpace, SinglePSpace, random_symbols, NamedArgsMixin, Distribution) + + +class ContinuousDomain(RandomDomain): + """ + A domain with continuous support + + Represented using symbols and Intervals. + """ + is_Continuous = True + + def as_boolean(self): + raise NotImplementedError("Not Implemented for generic Domains") + + +class SingleContinuousDomain(ContinuousDomain, SingleDomain): + """ + A univariate domain with continuous support + + Represented using a single symbol and interval. + """ + def compute_expectation(self, expr, variables=None, **kwargs): + if variables is None: + variables = self.symbols + if not variables: + return expr + if frozenset(variables) != frozenset(self.symbols): + raise ValueError("Values should be equal") + # assumes only intervals + return Integral(expr, (self.symbol, self.set), **kwargs) + + def as_boolean(self): + return self.set.as_relational(self.symbol) + + +class ProductContinuousDomain(ProductDomain, ContinuousDomain): + """ + A collection of independent domains with continuous support + """ + + def compute_expectation(self, expr, variables=None, **kwargs): + if variables is None: + variables = self.symbols + for domain in self.domains: + domain_vars = frozenset(variables) & frozenset(domain.symbols) + if domain_vars: + expr = domain.compute_expectation(expr, domain_vars, **kwargs) + return expr + + def as_boolean(self): + return And(*[domain.as_boolean() for domain in self.domains]) + + +class ConditionalContinuousDomain(ContinuousDomain, ConditionalDomain): + """ + A domain with continuous support that has been further restricted by a + condition such as $x > 3$. + """ + + def compute_expectation(self, expr, variables=None, **kwargs): + if variables is None: + variables = self.symbols + if not variables: + return expr + # Extract the full integral + fullintgrl = self.fulldomain.compute_expectation(expr, variables) + # separate into integrand and limits + integrand, limits = fullintgrl.function, list(fullintgrl.limits) + + conditions = [self.condition] + while conditions: + cond = conditions.pop() + if cond.is_Boolean: + if isinstance(cond, And): + conditions.extend(cond.args) + elif isinstance(cond, Or): + raise NotImplementedError("Or not implemented here") + elif cond.is_Relational: + if cond.is_Equality: + # Add the appropriate Delta to the integrand + integrand *= DiracDelta(cond.lhs - cond.rhs) + else: + symbols = cond.free_symbols & set(self.symbols) + if len(symbols) != 1: # Can't handle x > y + raise NotImplementedError( + "Multivariate Inequalities not yet implemented") + # Can handle x > 0 + symbol = symbols.pop() + # Find the limit with x, such as (x, -oo, oo) + for i, limit in enumerate(limits): + if limit[0] == symbol: + # Make condition into an Interval like [0, oo] + cintvl = reduce_rational_inequalities_wrap( + cond, symbol) + # Make limit into an Interval like [-oo, oo] + lintvl = Interval(limit[1], limit[2]) + # Intersect them to get [0, oo] + intvl = cintvl.intersect(lintvl) + # Put back into limits list + limits[i] = (symbol, intvl.left, intvl.right) + else: + raise TypeError( + "Condition %s is not a relational or Boolean" % cond) + + return Integral(integrand, *limits, **kwargs) + + def as_boolean(self): + return And(self.fulldomain.as_boolean(), self.condition) + + @property + def set(self): + if len(self.symbols) == 1: + return (self.fulldomain.set & reduce_rational_inequalities_wrap( + self.condition, tuple(self.symbols)[0])) + else: + raise NotImplementedError( + "Set of Conditional Domain not Implemented") + + +class ContinuousDistribution(Distribution): + def __call__(self, *args): + return self.pdf(*args) + + +class SingleContinuousDistribution(ContinuousDistribution, NamedArgsMixin): + """ Continuous distribution of a single variable. + + Explanation + =========== + + Serves as superclass for Normal/Exponential/UniformDistribution etc.... + + Represented by parameters for each of the specific classes. E.g + NormalDistribution is represented by a mean and standard deviation. + + Provides methods for pdf, cdf, and sampling. + + See Also + ======== + + sympy.stats.crv_types.* + """ + + set = Interval(-oo, oo) + + def __new__(cls, *args): + args = list(map(sympify, args)) + return Basic.__new__(cls, *args) + + @staticmethod + def check(*args): + pass + + @cacheit + def compute_cdf(self, **kwargs): + """ Compute the CDF from the PDF. + + Returns a Lambda. + """ + x, z = symbols('x, z', real=True, cls=Dummy) + left_bound = self.set.start + + # CDF is integral of PDF from left bound to z + pdf = self.pdf(x) + cdf = integrate(pdf.doit(), (x, left_bound, z), **kwargs) + # CDF Ensure that CDF left of left_bound is zero + cdf = Piecewise((cdf, z >= left_bound), (0, True)) + return Lambda(z, cdf) + + def _cdf(self, x): + return None + + def cdf(self, x, **kwargs): + """ Cumulative density function """ + if len(kwargs) == 0: + cdf = self._cdf(x) + if cdf is not None: + return cdf + return self.compute_cdf(**kwargs)(x) + + @cacheit + def compute_characteristic_function(self, **kwargs): + """ Compute the characteristic function from the PDF. + + Returns a Lambda. + """ + x, t = symbols('x, t', real=True, cls=Dummy) + pdf = self.pdf(x) + cf = integrate(exp(I*t*x)*pdf, (x, self.set)) + return Lambda(t, cf) + + def _characteristic_function(self, t): + return None + + def characteristic_function(self, t, **kwargs): + """ Characteristic function """ + if len(kwargs) == 0: + cf = self._characteristic_function(t) + if cf is not None: + return cf + return self.compute_characteristic_function(**kwargs)(t) + + @cacheit + def compute_moment_generating_function(self, **kwargs): + """ Compute the moment generating function from the PDF. + + Returns a Lambda. + """ + x, t = symbols('x, t', real=True, cls=Dummy) + pdf = self.pdf(x) + mgf = integrate(exp(t * x) * pdf, (x, self.set)) + return Lambda(t, mgf) + + def _moment_generating_function(self, t): + return None + + def moment_generating_function(self, t, **kwargs): + """ Moment generating function """ + if not kwargs: + mgf = self._moment_generating_function(t) + if mgf is not None: + return mgf + return self.compute_moment_generating_function(**kwargs)(t) + + def expectation(self, expr, var, evaluate=True, **kwargs): + """ Expectation of expression over distribution """ + if evaluate: + try: + p = poly(expr, var) + if p.is_zero: + return S.Zero + t = Dummy('t', real=True) + mgf = self._moment_generating_function(t) + if mgf is None: + return integrate(expr * self.pdf(var), (var, self.set), **kwargs) + deg = p.degree() + taylor = poly(series(mgf, t, 0, deg + 1).removeO(), t) + result = 0 + for k in range(deg+1): + result += p.coeff_monomial(var ** k) * taylor.coeff_monomial(t ** k) * factorial(k) + return result + except PolynomialError: + return integrate(expr * self.pdf(var), (var, self.set), **kwargs) + else: + return Integral(expr * self.pdf(var), (var, self.set), **kwargs) + + @cacheit + def compute_quantile(self, **kwargs): + """ Compute the Quantile from the PDF. + + Returns a Lambda. + """ + x, p = symbols('x, p', real=True, cls=Dummy) + left_bound = self.set.start + + pdf = self.pdf(x) + cdf = integrate(pdf, (x, left_bound, x), **kwargs) + quantile = solveset(cdf - p, x, self.set) + return Lambda(p, Piecewise((quantile, (p >= 0) & (p <= 1) ), (nan, True))) + + def _quantile(self, x): + return None + + def quantile(self, x, **kwargs): + """ Cumulative density function """ + if len(kwargs) == 0: + quantile = self._quantile(x) + if quantile is not None: + return quantile + return self.compute_quantile(**kwargs)(x) + + +class ContinuousPSpace(PSpace): + """ Continuous Probability Space + + Represents the likelihood of an event space defined over a continuum. + + Represented with a ContinuousDomain and a PDF (Lambda-Like) + """ + + is_Continuous = True + is_real = True + + @property + def pdf(self): + return self.density(*self.domain.symbols) + + def compute_expectation(self, expr, rvs=None, evaluate=False, **kwargs): + if rvs is None: + rvs = self.values + else: + rvs = frozenset(rvs) + + expr = expr.xreplace({rv: rv.symbol for rv in rvs}) + + domain_symbols = frozenset(rv.symbol for rv in rvs) + + return self.domain.compute_expectation(self.pdf * expr, + domain_symbols, **kwargs) + + def compute_density(self, expr, **kwargs): + # Common case Density(X) where X in self.values + if expr in self.values: + # Marginalize all other random symbols out of the density + randomsymbols = tuple(set(self.values) - frozenset([expr])) + symbols = tuple(rs.symbol for rs in randomsymbols) + pdf = self.domain.compute_expectation(self.pdf, symbols, **kwargs) + return Lambda(expr.symbol, pdf) + + z = Dummy('z', real=True) + return Lambda(z, self.compute_expectation(DiracDelta(expr - z), **kwargs)) + + @cacheit + def compute_cdf(self, expr, **kwargs): + if not self.domain.set.is_Interval: + raise ValueError( + "CDF not well defined on multivariate expressions") + + d = self.compute_density(expr, **kwargs) + x, z = symbols('x, z', real=True, cls=Dummy) + left_bound = self.domain.set.start + + # CDF is integral of PDF from left bound to z + cdf = integrate(d(x), (x, left_bound, z), **kwargs) + # CDF Ensure that CDF left of left_bound is zero + cdf = Piecewise((cdf, z >= left_bound), (0, True)) + return Lambda(z, cdf) + + @cacheit + def compute_characteristic_function(self, expr, **kwargs): + if not self.domain.set.is_Interval: + raise NotImplementedError("Characteristic function of multivariate expressions not implemented") + + d = self.compute_density(expr, **kwargs) + x, t = symbols('x, t', real=True, cls=Dummy) + cf = integrate(exp(I*t*x)*d(x), (x, -oo, oo), **kwargs) + return Lambda(t, cf) + + @cacheit + def compute_moment_generating_function(self, expr, **kwargs): + if not self.domain.set.is_Interval: + raise NotImplementedError("Moment generating function of multivariate expressions not implemented") + + d = self.compute_density(expr, **kwargs) + x, t = symbols('x, t', real=True, cls=Dummy) + mgf = integrate(exp(t * x) * d(x), (x, -oo, oo), **kwargs) + return Lambda(t, mgf) + + @cacheit + def compute_quantile(self, expr, **kwargs): + if not self.domain.set.is_Interval: + raise ValueError( + "Quantile not well defined on multivariate expressions") + + d = self.compute_cdf(expr, **kwargs) + x = Dummy('x', real=True) + p = Dummy('p', positive=True) + + quantile = solveset(d(x) - p, x, self.set) + + return Lambda(p, quantile) + + def probability(self, condition, **kwargs): + z = Dummy('z', real=True) + cond_inv = False + if isinstance(condition, Ne): + condition = Eq(condition.args[0], condition.args[1]) + cond_inv = True + # Univariate case can be handled by where + try: + domain = self.where(condition) + rv = [rv for rv in self.values if rv.symbol == domain.symbol][0] + # Integrate out all other random variables + pdf = self.compute_density(rv, **kwargs) + # return S.Zero if `domain` is empty set + if domain.set is S.EmptySet or isinstance(domain.set, FiniteSet): + return S.Zero if not cond_inv else S.One + if isinstance(domain.set, Union): + return sum( + Integral(pdf(z), (z, subset), **kwargs) for subset in + domain.set.args if isinstance(subset, Interval)) + # Integrate out the last variable over the special domain + return Integral(pdf(z), (z, domain.set), **kwargs) + + # Other cases can be turned into univariate case + # by computing a density handled by density computation + except NotImplementedError: + from sympy.stats.rv import density + expr = condition.lhs - condition.rhs + if not is_random(expr): + dens = self.density + comp = condition.rhs + else: + dens = density(expr, **kwargs) + comp = 0 + if not isinstance(dens, ContinuousDistribution): + from sympy.stats.crv_types import ContinuousDistributionHandmade + dens = ContinuousDistributionHandmade(dens, set=self.domain.set) + # Turn problem into univariate case + space = SingleContinuousPSpace(z, dens) + result = space.probability(condition.__class__(space.value, comp)) + return result if not cond_inv else S.One - result + + def where(self, condition): + rvs = frozenset(random_symbols(condition)) + if not (len(rvs) == 1 and rvs.issubset(self.values)): + raise NotImplementedError( + "Multiple continuous random variables not supported") + rv = tuple(rvs)[0] + interval = reduce_rational_inequalities_wrap(condition, rv) + interval = interval.intersect(self.domain.set) + return SingleContinuousDomain(rv.symbol, interval) + + def conditional_space(self, condition, normalize=True, **kwargs): + condition = condition.xreplace({rv: rv.symbol for rv in self.values}) + domain = ConditionalContinuousDomain(self.domain, condition) + if normalize: + # create a clone of the variable to + # make sure that variables in nested integrals are different + # from the variables outside the integral + # this makes sure that they are evaluated separately + # and in the correct order + replacement = {rv: Dummy(str(rv)) for rv in self.symbols} + norm = domain.compute_expectation(self.pdf, **kwargs) + pdf = self.pdf / norm.xreplace(replacement) + # XXX: Converting set to tuple. The order matters to Lambda though + # so we shouldn't be starting with a set here... + density = Lambda(tuple(domain.symbols), pdf) + + return ContinuousPSpace(domain, density) + + +class SingleContinuousPSpace(ContinuousPSpace, SinglePSpace): + """ + A continuous probability space over a single univariate variable. + + These consist of a Symbol and a SingleContinuousDistribution + + This class is normally accessed through the various random variable + functions, Normal, Exponential, Uniform, etc.... + """ + + @property + def set(self): + return self.distribution.set + + @property + def domain(self): + return SingleContinuousDomain(sympify(self.symbol), self.set) + + def sample(self, size=(), library='scipy', seed=None): + """ + Internal sample method. + + Returns dictionary mapping RandomSymbol to realization value. + """ + return {self.value: self.distribution.sample(size, library=library, seed=seed)} + + def compute_expectation(self, expr, rvs=None, evaluate=False, **kwargs): + rvs = rvs or (self.value,) + if self.value not in rvs: + return expr + + expr = _sympify(expr) + expr = expr.xreplace({rv: rv.symbol for rv in rvs}) + + x = self.value.symbol + try: + return self.distribution.expectation(expr, x, evaluate=evaluate, **kwargs) + except PoleError: + return Integral(expr * self.pdf, (x, self.set), **kwargs) + + def compute_cdf(self, expr, **kwargs): + if expr == self.value: + z = Dummy("z", real=True) + return Lambda(z, self.distribution.cdf(z, **kwargs)) + else: + return ContinuousPSpace.compute_cdf(self, expr, **kwargs) + + def compute_characteristic_function(self, expr, **kwargs): + if expr == self.value: + t = Dummy("t", real=True) + return Lambda(t, self.distribution.characteristic_function(t, **kwargs)) + else: + return ContinuousPSpace.compute_characteristic_function(self, expr, **kwargs) + + def compute_moment_generating_function(self, expr, **kwargs): + if expr == self.value: + t = Dummy("t", real=True) + return Lambda(t, self.distribution.moment_generating_function(t, **kwargs)) + else: + return ContinuousPSpace.compute_moment_generating_function(self, expr, **kwargs) + + def compute_density(self, expr, **kwargs): + # https://en.wikipedia.org/wiki/Random_variable#Functions_of_random_variables + if expr == self.value: + return self.density + y = Dummy('y', real=True) + + gs = solveset(expr - y, self.value, S.Reals) + + if isinstance(gs, Intersection): + if len(gs.args) == 2 and gs.args[0] is S.Reals: + gs = gs.args[1] + if not gs.is_FiniteSet: + raise ValueError("Can not solve %s for %s" % (expr, self.value)) + fx = self.compute_density(self.value) + fy = sum(fx(g) * abs(g.diff(y)) for g in gs) + return Lambda(y, fy) + + def compute_quantile(self, expr, **kwargs): + + if expr == self.value: + p = Dummy("p", real=True) + return Lambda(p, self.distribution.quantile(p, **kwargs)) + else: + return ContinuousPSpace.compute_quantile(self, expr, **kwargs) + +def _reduce_inequalities(conditions, var, **kwargs): + try: + return reduce_rational_inequalities(conditions, var, **kwargs) + except PolynomialError: + raise ValueError("Reduction of condition failed %s\n" % conditions[0]) + + +def reduce_rational_inequalities_wrap(condition, var): + if condition.is_Relational: + return _reduce_inequalities([[condition]], var, relational=False) + if isinstance(condition, Or): + return Union(*[_reduce_inequalities([[arg]], var, relational=False) + for arg in condition.args]) + if isinstance(condition, And): + intervals = [_reduce_inequalities([[arg]], var, relational=False) + for arg in condition.args] + I = intervals[0] + for i in intervals: + I = I.intersect(i) + return I diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/crv_types.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/crv_types.py new file mode 100644 index 0000000000000000000000000000000000000000..073e7350fdf80aac39ecd1dd607488a8b76187e3 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/crv_types.py @@ -0,0 +1,4732 @@ +""" +Continuous Random Variables - Prebuilt variables + +Contains +======== +Arcsin +Benini +Beta +BetaNoncentral +BetaPrime +BoundedPareto +Cauchy +Chi +ChiNoncentral +ChiSquared +Dagum +Davis +Erlang +ExGaussian +Exponential +ExponentialPower +FDistribution +FisherZ +Frechet +Gamma +GammaInverse +Gumbel +Gompertz +Kumaraswamy +Laplace +Levy +LogCauchy +Logistic +LogLogistic +LogitNormal +LogNormal +Lomax +Maxwell +Moyal +Nakagami +Normal +Pareto +PowerFunction +QuadraticU +RaisedCosine +Rayleigh +Reciprocal +ShiftedGompertz +StudentT +Trapezoidal +Triangular +Uniform +UniformSum +VonMises +Wald +Weibull +WignerSemicircle +""" + + + +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.trigonometric import (atan, cos, sin, tan) +from sympy.functions.special.bessel import (besseli, besselj, besselk) +from sympy.functions.special.beta_functions import beta as beta_fn +from sympy.concrete.summations import Sum +from sympy.core.basic import Basic +from sympy.core.function import Lambda +from sympy.core.numbers import (I, Rational, pi) +from sympy.core.relational import (Eq, Ne) +from sympy.core.singleton import S +from sympy.core.symbol import Dummy +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import (binomial, factorial) +from sympy.functions.elementary.complexes import (Abs, sign) +from sympy.functions.elementary.exponential import log +from sympy.functions.elementary.hyperbolic import sinh +from sympy.functions.elementary.integers import floor +from sympy.functions.elementary.miscellaneous import sqrt, Max, Min +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import asin +from sympy.functions.special.error_functions import (erf, erfc, erfi, erfinv, expint) +from sympy.functions.special.gamma_functions import (gamma, lowergamma, uppergamma) +from sympy.functions.special.zeta_functions import zeta +from sympy.functions.special.hyper import hyper +from sympy.integrals.integrals import integrate +from sympy.logic.boolalg import And +from sympy.sets.sets import Interval +from sympy.matrices import MatrixBase +from sympy.stats.crv import SingleContinuousPSpace, SingleContinuousDistribution +from sympy.stats.rv import _value_check, is_random + +oo = S.Infinity + +__all__ = ['ContinuousRV', +'Arcsin', +'Benini', +'Beta', +'BetaNoncentral', +'BetaPrime', +'BoundedPareto', +'Cauchy', +'Chi', +'ChiNoncentral', +'ChiSquared', +'Dagum', +'Davis', +'Erlang', +'ExGaussian', +'Exponential', +'ExponentialPower', +'FDistribution', +'FisherZ', +'Frechet', +'Gamma', +'GammaInverse', +'Gompertz', +'Gumbel', +'Kumaraswamy', +'Laplace', +'Levy', +'LogCauchy', +'Logistic', +'LogLogistic', +'LogitNormal', +'LogNormal', +'Lomax', +'Maxwell', +'Moyal', +'Nakagami', +'Normal', +'GaussianInverse', +'Pareto', +'PowerFunction', +'QuadraticU', +'RaisedCosine', +'Rayleigh', +'Reciprocal', +'StudentT', +'ShiftedGompertz', +'Trapezoidal', +'Triangular', +'Uniform', +'UniformSum', +'VonMises', +'Wald', +'Weibull', +'WignerSemicircle', +] + + +@is_random.register(MatrixBase) +def _(x): + return any(is_random(i) for i in x) + +def rv(symbol, cls, args, **kwargs): + args = list(map(sympify, args)) + dist = cls(*args) + if kwargs.pop('check', True): + dist.check(*args) + pspace = SingleContinuousPSpace(symbol, dist) + if any(is_random(arg) for arg in args): + from sympy.stats.compound_rv import CompoundPSpace, CompoundDistribution + pspace = CompoundPSpace(symbol, CompoundDistribution(dist)) + return pspace.value + + +class ContinuousDistributionHandmade(SingleContinuousDistribution): + _argnames = ('pdf',) + + def __new__(cls, pdf, set=Interval(-oo, oo)): + return Basic.__new__(cls, pdf, set) + + @property + def set(self): + return self.args[1] + + @staticmethod + def check(pdf, set): + x = Dummy('x') + val = integrate(pdf(x), (x, set)) + _value_check(Eq(val, 1) != S.false, "The pdf on the given set is incorrect.") + + +def ContinuousRV(symbol, density, set=Interval(-oo, oo), **kwargs): + """ + Create a Continuous Random Variable given the following: + + Parameters + ========== + + symbol : Symbol + Represents name of the random variable. + density : Expression containing symbol + Represents probability density function. + set : set/Interval + Represents the region where the pdf is valid, by default is real line. + check : bool + If True, it will check whether the given density + integrates to 1 over the given set. If False, it + will not perform this check. Default is False. + + + Returns + ======= + + RandomSymbol + + Many common continuous random variable types are already implemented. + This function should be necessary only very rarely. + + + Examples + ======== + + >>> from sympy import Symbol, sqrt, exp, pi + >>> from sympy.stats import ContinuousRV, P, E + + >>> x = Symbol("x") + + >>> pdf = sqrt(2)*exp(-x**2/2)/(2*sqrt(pi)) # Normal distribution + >>> X = ContinuousRV(x, pdf) + + >>> E(X) + 0 + >>> P(X>0) + 1/2 + """ + pdf = Piecewise((density, set.as_relational(symbol)), (0, True)) + pdf = Lambda(symbol, pdf) + # have a default of False while `rv` should have a default of True + kwargs['check'] = kwargs.pop('check', False) + return rv(symbol.name, ContinuousDistributionHandmade, (pdf, set), **kwargs) + +######################################## +# Continuous Probability Distributions # +######################################## + +#------------------------------------------------------------------------------- +# Arcsin distribution ---------------------------------------------------------- + + +class ArcsinDistribution(SingleContinuousDistribution): + _argnames = ('a', 'b') + + @property + def set(self): + return Interval(self.a, self.b) + + def pdf(self, x): + a, b = self.a, self.b + return 1/(pi*sqrt((x - a)*(b - x))) + + def _cdf(self, x): + a, b = self.a, self.b + return Piecewise( + (S.Zero, x < a), + (2*asin(sqrt((x - a)/(b - a)))/pi, x <= b), + (S.One, True)) + + +def Arcsin(name, a=0, b=1): + r""" + Create a Continuous Random Variable with an arcsin distribution. + + The density of the arcsin distribution is given by + + .. math:: + f(x) := \frac{1}{\pi\sqrt{(x-a)(b-x)}} + + with :math:`x \in (a,b)`. It must hold that :math:`-\infty < a < b < \infty`. + + Parameters + ========== + + a : Real number, the left interval boundary + b : Real number, the right interval boundary + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Arcsin, density, cdf + >>> from sympy import Symbol + + >>> a = Symbol("a", real=True) + >>> b = Symbol("b", real=True) + >>> z = Symbol("z") + + >>> X = Arcsin("x", a, b) + + >>> density(X)(z) + 1/(pi*sqrt((-a + z)*(b - z))) + + >>> cdf(X)(z) + Piecewise((0, a > z), + (2*asin(sqrt((-a + z)/(-a + b)))/pi, b >= z), + (1, True)) + + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Arcsine_distribution + + """ + + return rv(name, ArcsinDistribution, (a, b)) + +#------------------------------------------------------------------------------- +# Benini distribution ---------------------------------------------------------- + + +class BeniniDistribution(SingleContinuousDistribution): + _argnames = ('alpha', 'beta', 'sigma') + + @staticmethod + def check(alpha, beta, sigma): + _value_check(alpha > 0, "Shape parameter Alpha must be positive.") + _value_check(beta > 0, "Shape parameter Beta must be positive.") + _value_check(sigma > 0, "Scale parameter Sigma must be positive.") + + @property + def set(self): + return Interval(self.sigma, oo) + + def pdf(self, x): + alpha, beta, sigma = self.alpha, self.beta, self.sigma + return (exp(-alpha*log(x/sigma) - beta*log(x/sigma)**2) + *(alpha/x + 2*beta*log(x/sigma)/x)) + + def _moment_generating_function(self, t): + raise NotImplementedError('The moment generating function of the ' + 'Benini distribution does not exist.') + +def Benini(name, alpha, beta, sigma): + r""" + Create a Continuous Random Variable with a Benini distribution. + + The density of the Benini distribution is given by + + .. math:: + f(x) := e^{-\alpha\log{\frac{x}{\sigma}} + -\beta\log^2\left[{\frac{x}{\sigma}}\right]} + \left(\frac{\alpha}{x}+\frac{2\beta\log{\frac{x}{\sigma}}}{x}\right) + + This is a heavy-tailed distribution and is also known as the log-Rayleigh + distribution. + + Parameters + ========== + + alpha : Real number, `\alpha > 0`, a shape + beta : Real number, `\beta > 0`, a shape + sigma : Real number, `\sigma > 0`, a scale + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Benini, density, cdf + >>> from sympy import Symbol, pprint + + >>> alpha = Symbol("alpha", positive=True) + >>> beta = Symbol("beta", positive=True) + >>> sigma = Symbol("sigma", positive=True) + >>> z = Symbol("z") + + >>> X = Benini("x", alpha, beta, sigma) + + >>> D = density(X)(z) + >>> pprint(D, use_unicode=False) + / / z \\ / z \ 2/ z \ + | 2*beta*log|-----|| - alpha*log|-----| - beta*log |-----| + |alpha \sigma/| \sigma/ \sigma/ + |----- + -----------------|*e + \ z z / + + >>> cdf(X)(z) + Piecewise((1 - exp(-alpha*log(z/sigma) - beta*log(z/sigma)**2), sigma <= z), + (0, True)) + + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Benini_distribution + .. [2] https://reference.wolfram.com/legacy/v8/ref/BeniniDistribution.html + + """ + + return rv(name, BeniniDistribution, (alpha, beta, sigma)) + +#------------------------------------------------------------------------------- +# Beta distribution ------------------------------------------------------------ + + +class BetaDistribution(SingleContinuousDistribution): + _argnames = ('alpha', 'beta') + + set = Interval(0, 1) + + @staticmethod + def check(alpha, beta): + _value_check(alpha > 0, "Shape parameter Alpha must be positive.") + _value_check(beta > 0, "Shape parameter Beta must be positive.") + + def pdf(self, x): + alpha, beta = self.alpha, self.beta + return x**(alpha - 1) * (1 - x)**(beta - 1) / beta_fn(alpha, beta) + + def _characteristic_function(self, t): + return hyper((self.alpha,), (self.alpha + self.beta,), I*t) + + def _moment_generating_function(self, t): + return hyper((self.alpha,), (self.alpha + self.beta,), t) + + +def Beta(name, alpha, beta): + r""" + Create a Continuous Random Variable with a Beta distribution. + + The density of the Beta distribution is given by + + .. math:: + f(x) := \frac{x^{\alpha-1}(1-x)^{\beta-1}} {\mathrm{B}(\alpha,\beta)} + + with :math:`x \in [0,1]`. + + Parameters + ========== + + alpha : Real number, `\alpha > 0`, a shape + beta : Real number, `\beta > 0`, a shape + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Beta, density, E, variance + >>> from sympy import Symbol, simplify, pprint, factor + + >>> alpha = Symbol("alpha", positive=True) + >>> beta = Symbol("beta", positive=True) + >>> z = Symbol("z") + + >>> X = Beta("x", alpha, beta) + + >>> D = density(X)(z) + >>> pprint(D, use_unicode=False) + alpha - 1 beta - 1 + z *(1 - z) + -------------------------- + B(alpha, beta) + + >>> simplify(E(X)) + alpha/(alpha + beta) + + >>> factor(simplify(variance(X))) + alpha*beta/((alpha + beta)**2*(alpha + beta + 1)) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Beta_distribution + .. [2] https://mathworld.wolfram.com/BetaDistribution.html + + """ + + return rv(name, BetaDistribution, (alpha, beta)) + +#------------------------------------------------------------------------------- +# Noncentral Beta distribution ------------------------------------------------------------ + + +class BetaNoncentralDistribution(SingleContinuousDistribution): + _argnames = ('alpha', 'beta', 'lamda') + + set = Interval(0, 1) + + @staticmethod + def check(alpha, beta, lamda): + _value_check(alpha > 0, "Shape parameter Alpha must be positive.") + _value_check(beta > 0, "Shape parameter Beta must be positive.") + _value_check(lamda >= 0, "Noncentrality parameter Lambda must be positive") + + def pdf(self, x): + alpha, beta, lamda = self.alpha, self.beta, self.lamda + k = Dummy("k") + return Sum(exp(-lamda / 2) * (lamda / 2)**k * x**(alpha + k - 1) *( + 1 - x)**(beta - 1) / (factorial(k) * beta_fn(alpha + k, beta)), (k, 0, oo)) + +def BetaNoncentral(name, alpha, beta, lamda): + r""" + Create a Continuous Random Variable with a Type I Noncentral Beta distribution. + + The density of the Noncentral Beta distribution is given by + + .. math:: + f(x) := \sum_{k=0}^\infty e^{-\lambda/2}\frac{(\lambda/2)^k}{k!} + \frac{x^{\alpha+k-1}(1-x)^{\beta-1}}{\mathrm{B}(\alpha+k,\beta)} + + with :math:`x \in [0,1]`. + + Parameters + ========== + + alpha : Real number, `\alpha > 0`, a shape + beta : Real number, `\beta > 0`, a shape + lamda : Real number, `\lambda \geq 0`, noncentrality parameter + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import BetaNoncentral, density, cdf + >>> from sympy import Symbol, pprint + + >>> alpha = Symbol("alpha", positive=True) + >>> beta = Symbol("beta", positive=True) + >>> lamda = Symbol("lamda", nonnegative=True) + >>> z = Symbol("z") + + >>> X = BetaNoncentral("x", alpha, beta, lamda) + + >>> D = density(X)(z) + >>> pprint(D, use_unicode=False) + oo + _____ + \ ` + \ -lamda + \ k ------- + \ k + alpha - 1 /lamda\ beta - 1 2 + ) z *|-----| *(1 - z) *e + / \ 2 / + / ------------------------------------------------ + / B(k + alpha, beta)*k! + /____, + k = 0 + + Compute cdf with specific 'x', 'alpha', 'beta' and 'lamda' values as follows: + + >>> cdf(BetaNoncentral("x", 1, 1, 1), evaluate=False)(2).doit() + 2*exp(1/2) + + The argument evaluate=False prevents an attempt at evaluation + of the sum for general x, before the argument 2 is passed. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Noncentral_beta_distribution + .. [2] https://reference.wolfram.com/language/ref/NoncentralBetaDistribution.html + + """ + + return rv(name, BetaNoncentralDistribution, (alpha, beta, lamda)) + + +#------------------------------------------------------------------------------- +# Beta prime distribution ------------------------------------------------------ + + +class BetaPrimeDistribution(SingleContinuousDistribution): + _argnames = ('alpha', 'beta') + + @staticmethod + def check(alpha, beta): + _value_check(alpha > 0, "Shape parameter Alpha must be positive.") + _value_check(beta > 0, "Shape parameter Beta must be positive.") + + set = Interval(0, oo) + + def pdf(self, x): + alpha, beta = self.alpha, self.beta + return x**(alpha - 1)*(1 + x)**(-alpha - beta)/beta_fn(alpha, beta) + +def BetaPrime(name, alpha, beta): + r""" + Create a continuous random variable with a Beta prime distribution. + + The density of the Beta prime distribution is given by + + .. math:: + f(x) := \frac{x^{\alpha-1} (1+x)^{-\alpha -\beta}}{B(\alpha,\beta)} + + with :math:`x > 0`. + + Parameters + ========== + + alpha : Real number, `\alpha > 0`, a shape + beta : Real number, `\beta > 0`, a shape + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import BetaPrime, density + >>> from sympy import Symbol, pprint + + >>> alpha = Symbol("alpha", positive=True) + >>> beta = Symbol("beta", positive=True) + >>> z = Symbol("z") + + >>> X = BetaPrime("x", alpha, beta) + + >>> D = density(X)(z) + >>> pprint(D, use_unicode=False) + alpha - 1 -alpha - beta + z *(z + 1) + ------------------------------- + B(alpha, beta) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Beta_prime_distribution + .. [2] https://mathworld.wolfram.com/BetaPrimeDistribution.html + + """ + + return rv(name, BetaPrimeDistribution, (alpha, beta)) + +#------------------------------------------------------------------------------- +# Bounded Pareto Distribution -------------------------------------------------- +class BoundedParetoDistribution(SingleContinuousDistribution): + _argnames = ('alpha', 'left', 'right') + + @property + def set(self): + return Interval(self.left, self.right) + + @staticmethod + def check(alpha, left, right): + _value_check (alpha.is_positive, "Shape must be positive.") + _value_check (left.is_positive, "Left value should be positive.") + _value_check (right > left, "Right should be greater than left.") + + def pdf(self, x): + alpha, left, right = self.alpha, self.left, self.right + num = alpha * (left**alpha) * x**(- alpha -1) + den = 1 - (left/right)**alpha + return num/den + +def BoundedPareto(name, alpha, left, right): + r""" + Create a continuous random variable with a Bounded Pareto distribution. + + The density of the Bounded Pareto distribution is given by + + .. math:: + f(x) := \frac{\alpha L^{\alpha}x^{-\alpha-1}}{1-(\frac{L}{H})^{\alpha}} + + Parameters + ========== + + alpha : Real Number, `\alpha > 0` + Shape parameter + left : Real Number, `left > 0` + Location parameter + right : Real Number, `right > left` + Location parameter + + Examples + ======== + + >>> from sympy.stats import BoundedPareto, density, cdf, E + >>> from sympy import symbols + >>> L, H = symbols('L, H', positive=True) + >>> X = BoundedPareto('X', 2, L, H) + >>> x = symbols('x') + >>> density(X)(x) + 2*L**2/(x**3*(1 - L**2/H**2)) + >>> cdf(X)(x) + Piecewise((-H**2*L**2/(x**2*(H**2 - L**2)) + H**2/(H**2 - L**2), L <= x), (0, True)) + >>> E(X).simplify() + 2*H*L/(H + L) + + Returns + ======= + + RandomSymbol + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Pareto_distribution#Bounded_Pareto_distribution + + """ + return rv (name, BoundedParetoDistribution, (alpha, left, right)) + +# ------------------------------------------------------------------------------ +# Cauchy distribution ---------------------------------------------------------- + + +class CauchyDistribution(SingleContinuousDistribution): + _argnames = ('x0', 'gamma') + + @staticmethod + def check(x0, gamma): + _value_check(gamma > 0, "Scale parameter Gamma must be positive.") + _value_check(x0.is_real, "Location parameter must be real.") + + def pdf(self, x): + return 1/(pi*self.gamma*(1 + ((x - self.x0)/self.gamma)**2)) + + def _cdf(self, x): + x0, gamma = self.x0, self.gamma + return (1/pi)*atan((x - x0)/gamma) + S.Half + + def _characteristic_function(self, t): + return exp(self.x0 * I * t - self.gamma * Abs(t)) + + def _moment_generating_function(self, t): + raise NotImplementedError("The moment generating function for the " + "Cauchy distribution does not exist.") + + def _quantile(self, p): + return self.x0 + self.gamma*tan(pi*(p - S.Half)) + + +def Cauchy(name, x0, gamma): + r""" + Create a continuous random variable with a Cauchy distribution. + + The density of the Cauchy distribution is given by + + .. math:: + f(x) := \frac{1}{\pi \gamma [1 + {(\frac{x-x_0}{\gamma})}^2]} + + Parameters + ========== + + x0 : Real number, the location + gamma : Real number, `\gamma > 0`, a scale + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Cauchy, density + >>> from sympy import Symbol + + >>> x0 = Symbol("x0") + >>> gamma = Symbol("gamma", positive=True) + >>> z = Symbol("z") + + >>> X = Cauchy("x", x0, gamma) + + >>> density(X)(z) + 1/(pi*gamma*(1 + (-x0 + z)**2/gamma**2)) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Cauchy_distribution + .. [2] https://mathworld.wolfram.com/CauchyDistribution.html + + """ + + return rv(name, CauchyDistribution, (x0, gamma)) + +#------------------------------------------------------------------------------- +# Chi distribution ------------------------------------------------------------- + + +class ChiDistribution(SingleContinuousDistribution): + _argnames = ('k',) + + @staticmethod + def check(k): + _value_check(k > 0, "Number of degrees of freedom (k) must be positive.") + _value_check(k.is_integer, "Number of degrees of freedom (k) must be an integer.") + + set = Interval(0, oo) + + def pdf(self, x): + return 2**(1 - self.k/2)*x**(self.k - 1)*exp(-x**2/2)/gamma(self.k/2) + + def _characteristic_function(self, t): + k = self.k + + part_1 = hyper((k/2,), (S.Half,), -t**2/2) + part_2 = I*t*sqrt(2)*gamma((k+1)/2)/gamma(k/2) + part_3 = hyper(((k+1)/2,), (Rational(3, 2),), -t**2/2) + return part_1 + part_2*part_3 + + def _moment_generating_function(self, t): + k = self.k + + part_1 = hyper((k / 2,), (S.Half,), t ** 2 / 2) + part_2 = t * sqrt(2) * gamma((k + 1) / 2) / gamma(k / 2) + part_3 = hyper(((k + 1) / 2,), (S(3) / 2,), t ** 2 / 2) + return part_1 + part_2 * part_3 + +def Chi(name, k): + r""" + Create a continuous random variable with a Chi distribution. + + The density of the Chi distribution is given by + + .. math:: + f(x) := \frac{2^{1-k/2}x^{k-1}e^{-x^2/2}}{\Gamma(k/2)} + + with :math:`x \geq 0`. + + Parameters + ========== + + k : Positive integer, The number of degrees of freedom + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Chi, density, E + >>> from sympy import Symbol, simplify + + >>> k = Symbol("k", integer=True) + >>> z = Symbol("z") + + >>> X = Chi("x", k) + + >>> density(X)(z) + 2**(1 - k/2)*z**(k - 1)*exp(-z**2/2)/gamma(k/2) + + >>> simplify(E(X)) + sqrt(2)*gamma(k/2 + 1/2)/gamma(k/2) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Chi_distribution + .. [2] https://mathworld.wolfram.com/ChiDistribution.html + + """ + + return rv(name, ChiDistribution, (k,)) + +#------------------------------------------------------------------------------- +# Non-central Chi distribution ------------------------------------------------- + + +class ChiNoncentralDistribution(SingleContinuousDistribution): + _argnames = ('k', 'l') + + @staticmethod + def check(k, l): + _value_check(k > 0, "Number of degrees of freedom (k) must be positive.") + _value_check(k.is_integer, "Number of degrees of freedom (k) must be an integer.") + _value_check(l > 0, "Shift parameter Lambda must be positive.") + + set = Interval(0, oo) + + def pdf(self, x): + k, l = self.k, self.l + return exp(-(x**2+l**2)/2)*x**k*l / (l*x)**(k/2) * besseli(k/2-1, l*x) + +def ChiNoncentral(name, k, l): + r""" + Create a continuous random variable with a non-central Chi distribution. + + Explanation + =========== + + The density of the non-central Chi distribution is given by + + .. math:: + f(x) := \frac{e^{-(x^2+\lambda^2)/2} x^k\lambda} + {(\lambda x)^{k/2}} I_{k/2-1}(\lambda x) + + with `x \geq 0`. Here, `I_\nu (x)` is the + :ref:`modified Bessel function of the first kind `. + + Parameters + ========== + + k : A positive Integer, $k > 0$ + The number of degrees of freedom. + lambda : Real number, `\lambda > 0` + Shift parameter. + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import ChiNoncentral, density + >>> from sympy import Symbol + + >>> k = Symbol("k", integer=True) + >>> l = Symbol("l") + >>> z = Symbol("z") + + >>> X = ChiNoncentral("x", k, l) + + >>> density(X)(z) + l*z**k*exp(-l**2/2 - z**2/2)*besseli(k/2 - 1, l*z)/(l*z)**(k/2) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Noncentral_chi_distribution + """ + + return rv(name, ChiNoncentralDistribution, (k, l)) + +#------------------------------------------------------------------------------- +# Chi squared distribution ----------------------------------------------------- + + +class ChiSquaredDistribution(SingleContinuousDistribution): + _argnames = ('k',) + + @staticmethod + def check(k): + _value_check(k > 0, "Number of degrees of freedom (k) must be positive.") + _value_check(k.is_integer, "Number of degrees of freedom (k) must be an integer.") + + set = Interval(0, oo) + + def pdf(self, x): + k = self.k + return 1/(2**(k/2)*gamma(k/2))*x**(k/2 - 1)*exp(-x/2) + + def _cdf(self, x): + k = self.k + return Piecewise( + (S.One/gamma(k/2)*lowergamma(k/2, x/2), x >= 0), + (0, True) + ) + + def _characteristic_function(self, t): + return (1 - 2*I*t)**(-self.k/2) + + def _moment_generating_function(self, t): + return (1 - 2*t)**(-self.k/2) + + +def ChiSquared(name, k): + r""" + Create a continuous random variable with a Chi-squared distribution. + + Explanation + =========== + + The density of the Chi-squared distribution is given by + + .. math:: + f(x) := \frac{1}{2^{\frac{k}{2}}\Gamma\left(\frac{k}{2}\right)} + x^{\frac{k}{2}-1} e^{-\frac{x}{2}} + + with :math:`x \geq 0`. + + Parameters + ========== + + k : Positive integer + The number of degrees of freedom. + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import ChiSquared, density, E, variance, moment + >>> from sympy import Symbol + + >>> k = Symbol("k", integer=True, positive=True) + >>> z = Symbol("z") + + >>> X = ChiSquared("x", k) + + >>> density(X)(z) + z**(k/2 - 1)*exp(-z/2)/(2**(k/2)*gamma(k/2)) + + >>> E(X) + k + + >>> variance(X) + 2*k + + >>> moment(X, 3) + k**3 + 6*k**2 + 8*k + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Chi_squared_distribution + .. [2] https://mathworld.wolfram.com/Chi-SquaredDistribution.html + """ + + return rv(name, ChiSquaredDistribution, (k, )) + +#------------------------------------------------------------------------------- +# Dagum distribution ----------------------------------------------------------- + + +class DagumDistribution(SingleContinuousDistribution): + _argnames = ('p', 'a', 'b') + + set = Interval(0, oo) + + @staticmethod + def check(p, a, b): + _value_check(p > 0, "Shape parameter p must be positive.") + _value_check(a > 0, "Shape parameter a must be positive.") + _value_check(b > 0, "Scale parameter b must be positive.") + + def pdf(self, x): + p, a, b = self.p, self.a, self.b + return a*p/x*((x/b)**(a*p)/(((x/b)**a + 1)**(p + 1))) + + def _cdf(self, x): + p, a, b = self.p, self.a, self.b + return Piecewise(((S.One + (S(x)/b)**-a)**-p, x>=0), + (S.Zero, True)) + +def Dagum(name, p, a, b): + r""" + Create a continuous random variable with a Dagum distribution. + + Explanation + =========== + + The density of the Dagum distribution is given by + + .. math:: + f(x) := \frac{a p}{x} \left( \frac{\left(\tfrac{x}{b}\right)^{a p}} + {\left(\left(\tfrac{x}{b}\right)^a + 1 \right)^{p+1}} \right) + + with :math:`x > 0`. + + Parameters + ========== + + p : Real number + `p > 0`, a shape. + a : Real number + `a > 0`, a shape. + b : Real number + `b > 0`, a scale. + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Dagum, density, cdf + >>> from sympy import Symbol + + >>> p = Symbol("p", positive=True) + >>> a = Symbol("a", positive=True) + >>> b = Symbol("b", positive=True) + >>> z = Symbol("z") + + >>> X = Dagum("x", p, a, b) + + >>> density(X)(z) + a*p*(z/b)**(a*p)*((z/b)**a + 1)**(-p - 1)/z + + >>> cdf(X)(z) + Piecewise(((1 + (z/b)**(-a))**(-p), z >= 0), (0, True)) + + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Dagum_distribution + + """ + + return rv(name, DagumDistribution, (p, a, b)) + +#------------------------------------------------------------------------------- +# Davis distribution ----------------------------------------------------------- + +class DavisDistribution(SingleContinuousDistribution): + _argnames = ('b', 'n', 'mu') + + set = Interval(0, oo) + + @staticmethod + def check(b, n, mu): + _value_check(b > 0, "Scale parameter b must be positive.") + _value_check(n > 1, "Shape parameter n must be above 1.") + _value_check(mu > 0, "Location parameter mu must be positive.") + + def pdf(self, x): + b, n, mu = self.b, self.n, self.mu + dividend = b**n*(x - mu)**(-1-n) + divisor = (exp(b/(x-mu))-1)*(gamma(n)*zeta(n)) + return dividend/divisor + + +def Davis(name, b, n, mu): + r""" Create a continuous random variable with Davis distribution. + + Explanation + =========== + + The density of Davis distribution is given by + + .. math:: + f(x; \mu; b, n) := \frac{b^{n}(x - \mu)^{1-n}}{ \left( e^{\frac{b}{x-\mu}} - 1 \right) \Gamma(n)\zeta(n)} + + with :math:`x \in [0,\infty]`. + + Davis distribution is a generalization of the Planck's law of radiation from statistical physics. It is used for modeling income distribution. + + Parameters + ========== + b : Real number + `p > 0`, a scale. + n : Real number + `n > 1`, a shape. + mu : Real number + `mu > 0`, a location. + + Returns + ======= + + RandomSymbol + + Examples + ======== + >>> from sympy.stats import Davis, density + >>> from sympy import Symbol + >>> b = Symbol("b", positive=True) + >>> n = Symbol("n", positive=True) + >>> mu = Symbol("mu", positive=True) + >>> z = Symbol("z") + >>> X = Davis("x", b, n, mu) + >>> density(X)(z) + b**n*(-mu + z)**(-n - 1)/((exp(b/(-mu + z)) - 1)*gamma(n)*zeta(n)) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Davis_distribution + .. [2] https://reference.wolfram.com/language/ref/DavisDistribution.html + + """ + return rv(name, DavisDistribution, (b, n, mu)) + + +#------------------------------------------------------------------------------- +# Erlang distribution ---------------------------------------------------------- + + +def Erlang(name, k, l): + r""" + Create a continuous random variable with an Erlang distribution. + + Explanation + =========== + + The density of the Erlang distribution is given by + + .. math:: + f(x) := \frac{\lambda^k x^{k-1} e^{-\lambda x}}{(k-1)!} + + with :math:`x \in [0,\infty]`. + + Parameters + ========== + + k : Positive integer + l : Real number, `\lambda > 0`, the rate + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Erlang, density, cdf, E, variance + >>> from sympy import Symbol, simplify, pprint + + >>> k = Symbol("k", integer=True, positive=True) + >>> l = Symbol("l", positive=True) + >>> z = Symbol("z") + + >>> X = Erlang("x", k, l) + + >>> D = density(X)(z) + >>> pprint(D, use_unicode=False) + k k - 1 -l*z + l *z *e + --------------- + Gamma(k) + + >>> C = cdf(X)(z) + >>> pprint(C, use_unicode=False) + /lowergamma(k, l*z) + |------------------ for z > 0 + < Gamma(k) + | + \ 0 otherwise + + + >>> E(X) + k/l + + >>> simplify(variance(X)) + k/l**2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Erlang_distribution + .. [2] https://mathworld.wolfram.com/ErlangDistribution.html + + """ + + return rv(name, GammaDistribution, (k, S.One/l)) + +# ------------------------------------------------------------------------------- +# ExGaussian distribution ----------------------------------------------------- + + +class ExGaussianDistribution(SingleContinuousDistribution): + _argnames = ('mean', 'std', 'rate') + + set = Interval(-oo, oo) + + @staticmethod + def check(mean, std, rate): + _value_check( + std > 0, "Standard deviation of ExGaussian must be positive.") + _value_check(rate > 0, "Rate of ExGaussian must be positive.") + + def pdf(self, x): + mean, std, rate = self.mean, self.std, self.rate + term1 = rate/2 + term2 = exp(rate * (2 * mean + rate * std**2 - 2*x)/2) + term3 = erfc((mean + rate*std**2 - x)/(sqrt(2)*std)) + return term1*term2*term3 + + def _cdf(self, x): + from sympy.stats import cdf + mean, std, rate = self.mean, self.std, self.rate + u = rate*(x - mean) + v = rate*std + GaussianCDF1 = cdf(Normal('x', 0, v))(u) + GaussianCDF2 = cdf(Normal('x', v**2, v))(u) + + return GaussianCDF1 - exp(-u + (v**2/2) + log(GaussianCDF2)) + + def _characteristic_function(self, t): + mean, std, rate = self.mean, self.std, self.rate + term1 = (1 - I*t/rate)**(-1) + term2 = exp(I*mean*t - std**2*t**2/2) + return term1 * term2 + + def _moment_generating_function(self, t): + mean, std, rate = self.mean, self.std, self.rate + term1 = (1 - t/rate)**(-1) + term2 = exp(mean*t + std**2*t**2/2) + return term1*term2 + + +def ExGaussian(name, mean, std, rate): + r""" + Create a continuous random variable with an Exponentially modified + Gaussian (EMG) distribution. + + Explanation + =========== + + The density of the exponentially modified Gaussian distribution is given by + + .. math:: + f(x) := \frac{\lambda}{2}e^{\frac{\lambda}{2}(2\mu+\lambda\sigma^2-2x)} + \text{erfc}(\frac{\mu + \lambda\sigma^2 - x}{\sqrt{2}\sigma}) + + with $x > 0$. Note that the expected value is `1/\lambda`. + + Parameters + ========== + + name : A string giving a name for this distribution + mean : A Real number, the mean of Gaussian component + std : A positive Real number, + :math: `\sigma^2 > 0` the variance of Gaussian component + rate : A positive Real number, + :math: `\lambda > 0` the rate of Exponential component + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import ExGaussian, density, cdf, E + >>> from sympy.stats import variance, skewness + >>> from sympy import Symbol, pprint, simplify + + >>> mean = Symbol("mu") + >>> std = Symbol("sigma", positive=True) + >>> rate = Symbol("lamda", positive=True) + >>> z = Symbol("z") + >>> X = ExGaussian("x", mean, std, rate) + + >>> pprint(density(X)(z), use_unicode=False) + / 2 \ + lamda*\lamda*sigma + 2*mu - 2*z/ + --------------------------------- / ___ / 2 \\ + 2 |\/ 2 *\lamda*sigma + mu - z/| + lamda*e *erfc|-----------------------------| + \ 2*sigma / + ---------------------------------------------------------------------------- + 2 + + >>> cdf(X)(z) + -(erf(sqrt(2)*(-lamda**2*sigma**2 + lamda*(-mu + z))/(2*lamda*sigma))/2 + 1/2)*exp(lamda**2*sigma**2/2 - lamda*(-mu + z)) + erf(sqrt(2)*(-mu + z)/(2*sigma))/2 + 1/2 + + >>> E(X) + (lamda*mu + 1)/lamda + + >>> simplify(variance(X)) + sigma**2 + lamda**(-2) + + >>> simplify(skewness(X)) + 2/(lamda**2*sigma**2 + 1)**(3/2) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Exponentially_modified_Gaussian_distribution + """ + return rv(name, ExGaussianDistribution, (mean, std, rate)) + +#------------------------------------------------------------------------------- +# Exponential distribution ----------------------------------------------------- + + +class ExponentialDistribution(SingleContinuousDistribution): + _argnames = ('rate',) + + set = Interval(0, oo) + + @staticmethod + def check(rate): + _value_check(rate > 0, "Rate must be positive.") + + def pdf(self, x): + return self.rate * exp(-self.rate*x) + + def _cdf(self, x): + return Piecewise( + (S.One - exp(-self.rate*x), x >= 0), + (0, True), + ) + + def _characteristic_function(self, t): + rate = self.rate + return rate / (rate - I*t) + + def _moment_generating_function(self, t): + rate = self.rate + return rate / (rate - t) + + def _quantile(self, p): + return -log(1-p)/self.rate + + +def Exponential(name, rate): + r""" + Create a continuous random variable with an Exponential distribution. + + Explanation + =========== + + The density of the exponential distribution is given by + + .. math:: + f(x) := \lambda \exp(-\lambda x) + + with $x > 0$. Note that the expected value is `1/\lambda`. + + Parameters + ========== + + rate : A positive Real number, `\lambda > 0`, the rate (or inverse scale/inverse mean) + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Exponential, density, cdf, E + >>> from sympy.stats import variance, std, skewness, quantile + >>> from sympy import Symbol + + >>> l = Symbol("lambda", positive=True) + >>> z = Symbol("z") + >>> p = Symbol("p") + >>> X = Exponential("x", l) + + >>> density(X)(z) + lambda*exp(-lambda*z) + + >>> cdf(X)(z) + Piecewise((1 - exp(-lambda*z), z >= 0), (0, True)) + + >>> quantile(X)(p) + -log(1 - p)/lambda + + >>> E(X) + 1/lambda + + >>> variance(X) + lambda**(-2) + + >>> skewness(X) + 2 + + >>> X = Exponential('x', 10) + + >>> density(X)(z) + 10*exp(-10*z) + + >>> E(X) + 1/10 + + >>> std(X) + 1/10 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Exponential_distribution + .. [2] https://mathworld.wolfram.com/ExponentialDistribution.html + + """ + + return rv(name, ExponentialDistribution, (rate, )) + + +# ------------------------------------------------------------------------------- +# Exponential Power distribution ----------------------------------------------------- + +class ExponentialPowerDistribution(SingleContinuousDistribution): + _argnames = ('mu', 'alpha', 'beta') + + set = Interval(-oo, oo) + + @staticmethod + def check(mu, alpha, beta): + _value_check(alpha > 0, "Scale parameter alpha must be positive.") + _value_check(beta > 0, "Shape parameter beta must be positive.") + + def pdf(self, x): + mu, alpha, beta = self.mu, self.alpha, self.beta + num = beta*exp(-(Abs(x - mu)/alpha)**beta) + den = 2*alpha*gamma(1/beta) + return num/den + + def _cdf(self, x): + mu, alpha, beta = self.mu, self.alpha, self.beta + num = lowergamma(1/beta, (Abs(x - mu) / alpha)**beta) + den = 2*gamma(1/beta) + return sign(x - mu)*num/den + S.Half + + +def ExponentialPower(name, mu, alpha, beta): + r""" + Create a Continuous Random Variable with Exponential Power distribution. + This distribution is known also as Generalized Normal + distribution version 1. + + Explanation + =========== + + The density of the Exponential Power distribution is given by + + .. math:: + f(x) := \frac{\beta}{2\alpha\Gamma(\frac{1}{\beta})} + e^{{-(\frac{|x - \mu|}{\alpha})^{\beta}}} + + with :math:`x \in [ - \infty, \infty ]`. + + Parameters + ========== + + mu : Real number + A location. + alpha : Real number,`\alpha > 0` + A scale. + beta : Real number, `\beta > 0` + A shape. + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import ExponentialPower, density, cdf + >>> from sympy import Symbol, pprint + >>> z = Symbol("z") + >>> mu = Symbol("mu") + >>> alpha = Symbol("alpha", positive=True) + >>> beta = Symbol("beta", positive=True) + >>> X = ExponentialPower("x", mu, alpha, beta) + >>> pprint(density(X)(z), use_unicode=False) + beta + /|mu - z|\ + -|--------| + \ alpha / + beta*e + --------------------- + / 1 \ + 2*alpha*Gamma|----| + \beta/ + >>> cdf(X)(z) + 1/2 + lowergamma(1/beta, (Abs(mu - z)/alpha)**beta)*sign(-mu + z)/(2*gamma(1/beta)) + + References + ========== + + .. [1] https://reference.wolfram.com/language/ref/ExponentialPowerDistribution.html + .. [2] https://en.wikipedia.org/wiki/Generalized_normal_distribution#Version_1 + + """ + return rv(name, ExponentialPowerDistribution, (mu, alpha, beta)) + + +#------------------------------------------------------------------------------- +# F distribution --------------------------------------------------------------- + + +class FDistributionDistribution(SingleContinuousDistribution): + _argnames = ('d1', 'd2') + + set = Interval(0, oo) + + @staticmethod + def check(d1, d2): + _value_check((d1 > 0, d1.is_integer), + "Degrees of freedom d1 must be positive integer.") + _value_check((d2 > 0, d2.is_integer), + "Degrees of freedom d2 must be positive integer.") + + def pdf(self, x): + d1, d2 = self.d1, self.d2 + return (sqrt((d1*x)**d1*d2**d2 / (d1*x+d2)**(d1+d2)) + / (x * beta_fn(d1/2, d2/2))) + + def _moment_generating_function(self, t): + raise NotImplementedError('The moment generating function for the ' + 'F-distribution does not exist.') + +def FDistribution(name, d1, d2): + r""" + Create a continuous random variable with a F distribution. + + Explanation + =========== + + The density of the F distribution is given by + + .. math:: + f(x) := \frac{\sqrt{\frac{(d_1 x)^{d_1} d_2^{d_2}} + {(d_1 x + d_2)^{d_1 + d_2}}}} + {x \mathrm{B} \left(\frac{d_1}{2}, \frac{d_2}{2}\right)} + + with :math:`x > 0`. + + Parameters + ========== + + d1 : `d_1 > 0`, where `d_1` is the degrees of freedom (`n_1 - 1`) + d2 : `d_2 > 0`, where `d_2` is the degrees of freedom (`n_2 - 1`) + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import FDistribution, density + >>> from sympy import Symbol, pprint + + >>> d1 = Symbol("d1", positive=True) + >>> d2 = Symbol("d2", positive=True) + >>> z = Symbol("z") + + >>> X = FDistribution("x", d1, d2) + + >>> D = density(X)(z) + >>> pprint(D, use_unicode=False) + d2 + -- ______________________________ + 2 / d1 -d1 - d2 + d2 *\/ (d1*z) *(d1*z + d2) + -------------------------------------- + /d1 d2\ + z*B|--, --| + \2 2 / + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/F-distribution + .. [2] https://mathworld.wolfram.com/F-Distribution.html + + """ + + return rv(name, FDistributionDistribution, (d1, d2)) + +#------------------------------------------------------------------------------- +# Fisher Z distribution -------------------------------------------------------- + +class FisherZDistribution(SingleContinuousDistribution): + _argnames = ('d1', 'd2') + + set = Interval(-oo, oo) + + @staticmethod + def check(d1, d2): + _value_check(d1 > 0, "Degree of freedom d1 must be positive.") + _value_check(d2 > 0, "Degree of freedom d2 must be positive.") + + def pdf(self, x): + d1, d2 = self.d1, self.d2 + return (2*d1**(d1/2)*d2**(d2/2) / beta_fn(d1/2, d2/2) * + exp(d1*x) / (d1*exp(2*x)+d2)**((d1+d2)/2)) + +def FisherZ(name, d1, d2): + r""" + Create a Continuous Random Variable with an Fisher's Z distribution. + + Explanation + =========== + + The density of the Fisher's Z distribution is given by + + .. math:: + f(x) := \frac{2d_1^{d_1/2} d_2^{d_2/2}} {\mathrm{B}(d_1/2, d_2/2)} + \frac{e^{d_1z}}{\left(d_1e^{2z}+d_2\right)^{\left(d_1+d_2\right)/2}} + + + .. TODO - What is the difference between these degrees of freedom? + + Parameters + ========== + + d1 : `d_1 > 0` + Degree of freedom. + d2 : `d_2 > 0` + Degree of freedom. + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import FisherZ, density + >>> from sympy import Symbol, pprint + + >>> d1 = Symbol("d1", positive=True) + >>> d2 = Symbol("d2", positive=True) + >>> z = Symbol("z") + + >>> X = FisherZ("x", d1, d2) + + >>> D = density(X)(z) + >>> pprint(D, use_unicode=False) + d1 d2 + d1 d2 - -- - -- + -- -- 2 2 + 2 2 / 2*z \ d1*z + 2*d1 *d2 *\d1*e + d2/ *e + ----------------------------------------- + /d1 d2\ + B|--, --| + \2 2 / + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Fisher%27s_z-distribution + .. [2] https://mathworld.wolfram.com/Fishersz-Distribution.html + + """ + + return rv(name, FisherZDistribution, (d1, d2)) + +#------------------------------------------------------------------------------- +# Frechet distribution --------------------------------------------------------- + +class FrechetDistribution(SingleContinuousDistribution): + _argnames = ('a', 's', 'm') + + set = Interval(0, oo) + + @staticmethod + def check(a, s, m): + _value_check(a > 0, "Shape parameter alpha must be positive.") + _value_check(s > 0, "Scale parameter s must be positive.") + + def __new__(cls, a, s=1, m=0): + a, s, m = list(map(sympify, (a, s, m))) + return Basic.__new__(cls, a, s, m) + + def pdf(self, x): + a, s, m = self.a, self.s, self.m + return a/s * ((x-m)/s)**(-1-a) * exp(-((x-m)/s)**(-a)) + + def _cdf(self, x): + a, s, m = self.a, self.s, self.m + return Piecewise((exp(-((x-m)/s)**(-a)), x >= m), + (S.Zero, True)) + +def Frechet(name, a, s=1, m=0): + r""" + Create a continuous random variable with a Frechet distribution. + + Explanation + =========== + + The density of the Frechet distribution is given by + + .. math:: + f(x) := \frac{\alpha}{s} \left(\frac{x-m}{s}\right)^{-1-\alpha} + e^{-(\frac{x-m}{s})^{-\alpha}} + + with :math:`x \geq m`. + + Parameters + ========== + + a : Real number, :math:`a \in \left(0, \infty\right)` the shape + s : Real number, :math:`s \in \left(0, \infty\right)` the scale + m : Real number, :math:`m \in \left(-\infty, \infty\right)` the minimum + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Frechet, density, cdf + >>> from sympy import Symbol + + >>> a = Symbol("a", positive=True) + >>> s = Symbol("s", positive=True) + >>> m = Symbol("m", real=True) + >>> z = Symbol("z") + + >>> X = Frechet("x", a, s, m) + + >>> density(X)(z) + a*((-m + z)/s)**(-a - 1)*exp(-1/((-m + z)/s)**a)/s + + >>> cdf(X)(z) + Piecewise((exp(-1/((-m + z)/s)**a), m <= z), (0, True)) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Fr%C3%A9chet_distribution + + """ + + return rv(name, FrechetDistribution, (a, s, m)) + +#------------------------------------------------------------------------------- +# Gamma distribution ----------------------------------------------------------- + + +class GammaDistribution(SingleContinuousDistribution): + _argnames = ('k', 'theta') + + set = Interval(0, oo) + + @staticmethod + def check(k, theta): + _value_check(k > 0, "k must be positive") + _value_check(theta > 0, "Theta must be positive") + + def pdf(self, x): + k, theta = self.k, self.theta + return x**(k - 1) * exp(-x/theta) / (gamma(k)*theta**k) + + def _cdf(self, x): + k, theta = self.k, self.theta + return Piecewise( + (lowergamma(k, S(x)/theta)/gamma(k), x > 0), + (S.Zero, True)) + + def _characteristic_function(self, t): + return (1 - self.theta*I*t)**(-self.k) + + def _moment_generating_function(self, t): + return (1- self.theta*t)**(-self.k) + + +def Gamma(name, k, theta): + r""" + Create a continuous random variable with a Gamma distribution. + + Explanation + =========== + + The density of the Gamma distribution is given by + + .. math:: + f(x) := \frac{1}{\Gamma(k) \theta^k} x^{k - 1} e^{-\frac{x}{\theta}} + + with :math:`x \in [0,1]`. + + Parameters + ========== + + k : Real number, `k > 0`, a shape + theta : Real number, `\theta > 0`, a scale + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Gamma, density, cdf, E, variance + >>> from sympy import Symbol, pprint, simplify + + >>> k = Symbol("k", positive=True) + >>> theta = Symbol("theta", positive=True) + >>> z = Symbol("z") + + >>> X = Gamma("x", k, theta) + + >>> D = density(X)(z) + >>> pprint(D, use_unicode=False) + -z + ----- + -k k - 1 theta + theta *z *e + --------------------- + Gamma(k) + + >>> C = cdf(X, meijerg=True)(z) + >>> pprint(C, use_unicode=False) + / / z \ + |k*lowergamma|k, -----| + | \ theta/ + <---------------------- for z >= 0 + | Gamma(k + 1) + | + \ 0 otherwise + + >>> E(X) + k*theta + + >>> V = simplify(variance(X)) + >>> pprint(V, use_unicode=False) + 2 + k*theta + + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Gamma_distribution + .. [2] https://mathworld.wolfram.com/GammaDistribution.html + + """ + + return rv(name, GammaDistribution, (k, theta)) + +#------------------------------------------------------------------------------- +# Inverse Gamma distribution --------------------------------------------------- + + +class GammaInverseDistribution(SingleContinuousDistribution): + _argnames = ('a', 'b') + + set = Interval(0, oo) + + @staticmethod + def check(a, b): + _value_check(a > 0, "alpha must be positive") + _value_check(b > 0, "beta must be positive") + + def pdf(self, x): + a, b = self.a, self.b + return b**a/gamma(a) * x**(-a-1) * exp(-b/x) + + def _cdf(self, x): + a, b = self.a, self.b + return Piecewise((uppergamma(a,b/x)/gamma(a), x > 0), + (S.Zero, True)) + + def _characteristic_function(self, t): + a, b = self.a, self.b + return 2 * (-I*b*t)**(a/2) * besselk(a, sqrt(-4*I*b*t)) / gamma(a) + + def _moment_generating_function(self, t): + raise NotImplementedError('The moment generating function for the ' + 'gamma inverse distribution does not exist.') + +def GammaInverse(name, a, b): + r""" + Create a continuous random variable with an inverse Gamma distribution. + + Explanation + =========== + + The density of the inverse Gamma distribution is given by + + .. math:: + f(x) := \frac{\beta^\alpha}{\Gamma(\alpha)} x^{-\alpha - 1} + \exp\left(\frac{-\beta}{x}\right) + + with :math:`x > 0`. + + Parameters + ========== + + a : Real number, `a > 0`, a shape + b : Real number, `b > 0`, a scale + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import GammaInverse, density, cdf + >>> from sympy import Symbol, pprint + + >>> a = Symbol("a", positive=True) + >>> b = Symbol("b", positive=True) + >>> z = Symbol("z") + + >>> X = GammaInverse("x", a, b) + + >>> D = density(X)(z) + >>> pprint(D, use_unicode=False) + -b + --- + a -a - 1 z + b *z *e + --------------- + Gamma(a) + + >>> cdf(X)(z) + Piecewise((uppergamma(a, b/z)/gamma(a), z > 0), (0, True)) + + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Inverse-gamma_distribution + + """ + + return rv(name, GammaInverseDistribution, (a, b)) + + +#------------------------------------------------------------------------------- +# Gumbel distribution (Maximum and Minimum) -------------------------------------------------------- + + +class GumbelDistribution(SingleContinuousDistribution): + _argnames = ('beta', 'mu', 'minimum') + + set = Interval(-oo, oo) + + @staticmethod + def check(beta, mu, minimum): + _value_check(beta > 0, "Scale parameter beta must be positive.") + + def pdf(self, x): + beta, mu = self.beta, self.mu + z = (x - mu)/beta + f_max = (1/beta)*exp(-z - exp(-z)) + f_min = (1/beta)*exp(z - exp(z)) + return Piecewise((f_min, self.minimum), (f_max, not self.minimum)) + + def _cdf(self, x): + beta, mu = self.beta, self.mu + z = (x - mu)/beta + F_max = exp(-exp(-z)) + F_min = 1 - exp(-exp(z)) + return Piecewise((F_min, self.minimum), (F_max, not self.minimum)) + + def _characteristic_function(self, t): + cf_max = gamma(1 - I*self.beta*t) * exp(I*self.mu*t) + cf_min = gamma(1 + I*self.beta*t) * exp(I*self.mu*t) + return Piecewise((cf_min, self.minimum), (cf_max, not self.minimum)) + + def _moment_generating_function(self, t): + mgf_max = gamma(1 - self.beta*t) * exp(self.mu*t) + mgf_min = gamma(1 + self.beta*t) * exp(self.mu*t) + return Piecewise((mgf_min, self.minimum), (mgf_max, not self.minimum)) + +def Gumbel(name, beta, mu, minimum=False): + r""" + Create a Continuous Random Variable with Gumbel distribution. + + Explanation + =========== + + The density of the Gumbel distribution is given by + + For Maximum + + .. math:: + f(x) := \dfrac{1}{\beta} \exp \left( -\dfrac{x-\mu}{\beta} + - \exp \left( -\dfrac{x - \mu}{\beta} \right) \right) + + with :math:`x \in [ - \infty, \infty ]`. + + For Minimum + + .. math:: + f(x) := \frac{e^{- e^{\frac{- \mu + x}{\beta}} + \frac{- \mu + x}{\beta}}}{\beta} + + with :math:`x \in [ - \infty, \infty ]`. + + Parameters + ========== + + mu : Real number, `\mu`, a location + beta : Real number, `\beta > 0`, a scale + minimum : Boolean, by default ``False``, set to ``True`` for enabling minimum distribution + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Gumbel, density, cdf + >>> from sympy import Symbol + >>> x = Symbol("x") + >>> mu = Symbol("mu") + >>> beta = Symbol("beta", positive=True) + >>> X = Gumbel("x", beta, mu) + >>> density(X)(x) + exp(-exp(-(-mu + x)/beta) - (-mu + x)/beta)/beta + >>> cdf(X)(x) + exp(-exp(-(-mu + x)/beta)) + + References + ========== + + .. [1] https://mathworld.wolfram.com/GumbelDistribution.html + .. [2] https://en.wikipedia.org/wiki/Gumbel_distribution + .. [3] https://web.archive.org/web/20200628222206/http://www.mathwave.com/help/easyfit/html/analyses/distributions/gumbel_max.html + .. [4] https://web.archive.org/web/20200628222212/http://www.mathwave.com/help/easyfit/html/analyses/distributions/gumbel_min.html + + """ + return rv(name, GumbelDistribution, (beta, mu, minimum)) + +#------------------------------------------------------------------------------- +# Gompertz distribution -------------------------------------------------------- + +class GompertzDistribution(SingleContinuousDistribution): + _argnames = ('b', 'eta') + + set = Interval(0, oo) + + @staticmethod + def check(b, eta): + _value_check(b > 0, "b must be positive") + _value_check(eta > 0, "eta must be positive") + + def pdf(self, x): + eta, b = self.eta, self.b + return b*eta*exp(b*x)*exp(eta)*exp(-eta*exp(b*x)) + + def _cdf(self, x): + eta, b = self.eta, self.b + return 1 - exp(eta)*exp(-eta*exp(b*x)) + + def _moment_generating_function(self, t): + eta, b = self.eta, self.b + return eta * exp(eta) * expint(t/b, eta) + +def Gompertz(name, b, eta): + r""" + Create a Continuous Random Variable with Gompertz distribution. + + Explanation + =========== + + The density of the Gompertz distribution is given by + + .. math:: + f(x) := b \eta e^{b x} e^{\eta} \exp \left(-\eta e^{bx} \right) + + with :math:`x \in [0, \infty)`. + + Parameters + ========== + + b : Real number, `b > 0`, a scale + eta : Real number, `\eta > 0`, a shape + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Gompertz, density + >>> from sympy import Symbol + + >>> b = Symbol("b", positive=True) + >>> eta = Symbol("eta", positive=True) + >>> z = Symbol("z") + + >>> X = Gompertz("x", b, eta) + + >>> density(X)(z) + b*eta*exp(eta)*exp(b*z)*exp(-eta*exp(b*z)) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Gompertz_distribution + + """ + return rv(name, GompertzDistribution, (b, eta)) + +#------------------------------------------------------------------------------- +# Kumaraswamy distribution ----------------------------------------------------- + + +class KumaraswamyDistribution(SingleContinuousDistribution): + _argnames = ('a', 'b') + + set = Interval(0, oo) + + @staticmethod + def check(a, b): + _value_check(a > 0, "a must be positive") + _value_check(b > 0, "b must be positive") + + def pdf(self, x): + a, b = self.a, self.b + return a * b * x**(a-1) * (1-x**a)**(b-1) + + def _cdf(self, x): + a, b = self.a, self.b + return Piecewise( + (S.Zero, x < S.Zero), + (1 - (1 - x**a)**b, x <= S.One), + (S.One, True)) + +def Kumaraswamy(name, a, b): + r""" + Create a Continuous Random Variable with a Kumaraswamy distribution. + + Explanation + =========== + + The density of the Kumaraswamy distribution is given by + + .. math:: + f(x) := a b x^{a-1} (1-x^a)^{b-1} + + with :math:`x \in [0,1]`. + + Parameters + ========== + + a : Real number, `a > 0`, a shape + b : Real number, `b > 0`, a shape + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Kumaraswamy, density, cdf + >>> from sympy import Symbol, pprint + + >>> a = Symbol("a", positive=True) + >>> b = Symbol("b", positive=True) + >>> z = Symbol("z") + + >>> X = Kumaraswamy("x", a, b) + + >>> D = density(X)(z) + >>> pprint(D, use_unicode=False) + b - 1 + a - 1 / a\ + a*b*z *\1 - z / + + >>> cdf(X)(z) + Piecewise((0, z < 0), (1 - (1 - z**a)**b, z <= 1), (1, True)) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Kumaraswamy_distribution + + """ + + return rv(name, KumaraswamyDistribution, (a, b)) + +#------------------------------------------------------------------------------- +# Laplace distribution --------------------------------------------------------- + + +class LaplaceDistribution(SingleContinuousDistribution): + _argnames = ('mu', 'b') + + set = Interval(-oo, oo) + + @staticmethod + def check(mu, b): + _value_check(b > 0, "Scale parameter b must be positive.") + _value_check(mu.is_real, "Location parameter mu should be real") + + def pdf(self, x): + mu, b = self.mu, self.b + return 1/(2*b)*exp(-Abs(x - mu)/b) + + def _cdf(self, x): + mu, b = self.mu, self.b + return Piecewise( + (S.Half*exp((x - mu)/b), x < mu), + (S.One - S.Half*exp(-(x - mu)/b), x >= mu) + ) + + def _characteristic_function(self, t): + return exp(self.mu*I*t) / (1 + self.b**2*t**2) + + def _moment_generating_function(self, t): + return exp(self.mu*t) / (1 - self.b**2*t**2) + +def Laplace(name, mu, b): + r""" + Create a continuous random variable with a Laplace distribution. + + Explanation + =========== + + The density of the Laplace distribution is given by + + .. math:: + f(x) := \frac{1}{2 b} \exp \left(-\frac{|x-\mu|}b \right) + + Parameters + ========== + + mu : Real number or a list/matrix, the location (mean) or the + location vector + b : Real number or a positive definite matrix, representing a scale + or the covariance matrix. + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Laplace, density, cdf + >>> from sympy import Symbol, pprint + + >>> mu = Symbol("mu") + >>> b = Symbol("b", positive=True) + >>> z = Symbol("z") + + >>> X = Laplace("x", mu, b) + + >>> density(X)(z) + exp(-Abs(mu - z)/b)/(2*b) + + >>> cdf(X)(z) + Piecewise((exp((-mu + z)/b)/2, mu > z), (1 - exp((mu - z)/b)/2, True)) + + >>> L = Laplace('L', [1, 2], [[1, 0], [0, 1]]) + >>> pprint(density(L)(1, 2), use_unicode=False) + 5 / ____\ + e *besselk\0, \/ 35 / + --------------------- + pi + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Laplace_distribution + .. [2] https://mathworld.wolfram.com/LaplaceDistribution.html + + """ + + if isinstance(mu, (list, MatrixBase)) and\ + isinstance(b, (list, MatrixBase)): + from sympy.stats.joint_rv_types import MultivariateLaplace + return MultivariateLaplace(name, mu, b) + + return rv(name, LaplaceDistribution, (mu, b)) + +#------------------------------------------------------------------------------- +# Levy distribution --------------------------------------------------------- + + +class LevyDistribution(SingleContinuousDistribution): + _argnames = ('mu', 'c') + + @property + def set(self): + return Interval(self.mu, oo) + + @staticmethod + def check(mu, c): + _value_check(c > 0, "c (scale parameter) must be positive") + _value_check(mu.is_real, "mu (location parameter) must be real") + + def pdf(self, x): + mu, c = self.mu, self.c + return sqrt(c/(2*pi))*exp(-c/(2*(x - mu)))/((x - mu)**(S.One + S.Half)) + + def _cdf(self, x): + mu, c = self.mu, self.c + return erfc(sqrt(c/(2*(x - mu)))) + + def _characteristic_function(self, t): + mu, c = self.mu, self.c + return exp(I * mu * t - sqrt(-2 * I * c * t)) + + def _moment_generating_function(self, t): + raise NotImplementedError('The moment generating function of Levy distribution does not exist.') + +def Levy(name, mu, c): + r""" + Create a continuous random variable with a Levy distribution. + + The density of the Levy distribution is given by + + .. math:: + f(x) := \sqrt(\frac{c}{2 \pi}) \frac{\exp -\frac{c}{2 (x - \mu)}}{(x - \mu)^{3/2}} + + Parameters + ========== + + mu : Real number + The location parameter. + c : Real number, `c > 0` + A scale parameter. + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Levy, density, cdf + >>> from sympy import Symbol + + >>> mu = Symbol("mu", real=True) + >>> c = Symbol("c", positive=True) + >>> z = Symbol("z") + + >>> X = Levy("x", mu, c) + + >>> density(X)(z) + sqrt(2)*sqrt(c)*exp(-c/(-2*mu + 2*z))/(2*sqrt(pi)*(-mu + z)**(3/2)) + + >>> cdf(X)(z) + erfc(sqrt(c)*sqrt(1/(-2*mu + 2*z))) + + References + ========== + .. [1] https://en.wikipedia.org/wiki/L%C3%A9vy_distribution + .. [2] https://mathworld.wolfram.com/LevyDistribution.html + """ + + return rv(name, LevyDistribution, (mu, c)) + +#------------------------------------------------------------------------------- +# Log-Cauchy distribution -------------------------------------------------------- + + +class LogCauchyDistribution(SingleContinuousDistribution): + _argnames = ('mu', 'sigma') + + set = Interval.open(0, oo) + + @staticmethod + def check(mu, sigma): + _value_check((sigma > 0) != False, "Scale parameter Gamma must be positive.") + _value_check(mu.is_real != False, "Location parameter must be real.") + + def pdf(self, x): + mu, sigma = self.mu, self.sigma + return 1/(x*pi)*(sigma/((log(x) - mu)**2 + sigma**2)) + + def _cdf(self, x): + mu, sigma = self.mu, self.sigma + return (1/pi)*atan((log(x) - mu)/sigma) + S.Half + + def _characteristic_function(self, t): + raise NotImplementedError("The characteristic function for the " + "Log-Cauchy distribution does not exist.") + + def _moment_generating_function(self, t): + raise NotImplementedError("The moment generating function for the " + "Log-Cauchy distribution does not exist.") + +def LogCauchy(name, mu, sigma): + r""" + Create a continuous random variable with a Log-Cauchy distribution. + The density of the Log-Cauchy distribution is given by + + .. math:: + f(x) := \frac{1}{\pi x} \frac{\sigma}{(log(x)-\mu^2) + \sigma^2} + + Parameters + ========== + + mu : Real number, the location + + sigma : Real number, `\sigma > 0`, a scale + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import LogCauchy, density, cdf + >>> from sympy import Symbol, S + + >>> mu = 2 + >>> sigma = S.One / 5 + >>> z = Symbol("z") + + >>> X = LogCauchy("x", mu, sigma) + + >>> density(X)(z) + 1/(5*pi*z*((log(z) - 2)**2 + 1/25)) + + >>> cdf(X)(z) + atan(5*log(z) - 10)/pi + 1/2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Log-Cauchy_distribution + """ + + return rv(name, LogCauchyDistribution, (mu, sigma)) + + +#------------------------------------------------------------------------------- +# Logistic distribution -------------------------------------------------------- + + +class LogisticDistribution(SingleContinuousDistribution): + _argnames = ('mu', 's') + + set = Interval(-oo, oo) + + @staticmethod + def check(mu, s): + _value_check(s > 0, "Scale parameter s must be positive.") + + def pdf(self, x): + mu, s = self.mu, self.s + return exp(-(x - mu)/s)/(s*(1 + exp(-(x - mu)/s))**2) + + def _cdf(self, x): + mu, s = self.mu, self.s + return S.One/(1 + exp(-(x - mu)/s)) + + def _characteristic_function(self, t): + return Piecewise((exp(I*t*self.mu) * pi*self.s*t / sinh(pi*self.s*t), Ne(t, 0)), (S.One, True)) + + def _moment_generating_function(self, t): + return exp(self.mu*t) * beta_fn(1 - self.s*t, 1 + self.s*t) + + def _quantile(self, p): + return self.mu - self.s*log(-S.One + S.One/p) + +def Logistic(name, mu, s): + r""" + Create a continuous random variable with a logistic distribution. + + Explanation + =========== + + The density of the logistic distribution is given by + + .. math:: + f(x) := \frac{e^{-(x-\mu)/s}} {s\left(1+e^{-(x-\mu)/s}\right)^2} + + Parameters + ========== + + mu : Real number, the location (mean) + s : Real number, `s > 0`, a scale + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Logistic, density, cdf + >>> from sympy import Symbol + + >>> mu = Symbol("mu", real=True) + >>> s = Symbol("s", positive=True) + >>> z = Symbol("z") + + >>> X = Logistic("x", mu, s) + + >>> density(X)(z) + exp((mu - z)/s)/(s*(exp((mu - z)/s) + 1)**2) + + >>> cdf(X)(z) + 1/(exp((mu - z)/s) + 1) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Logistic_distribution + .. [2] https://mathworld.wolfram.com/LogisticDistribution.html + + """ + + return rv(name, LogisticDistribution, (mu, s)) + +#------------------------------------------------------------------------------- +# Log-logistic distribution -------------------------------------------------------- + + +class LogLogisticDistribution(SingleContinuousDistribution): + _argnames = ('alpha', 'beta') + + set = Interval(0, oo) + + @staticmethod + def check(alpha, beta): + _value_check(alpha > 0, "Scale parameter Alpha must be positive.") + _value_check(beta > 0, "Shape parameter Beta must be positive.") + + def pdf(self, x): + a, b = self.alpha, self.beta + return ((b/a)*(x/a)**(b - 1))/(1 + (x/a)**b)**2 + + def _cdf(self, x): + a, b = self.alpha, self.beta + return 1/(1 + (x/a)**(-b)) + + def _quantile(self, p): + a, b = self.alpha, self.beta + return a*((p/(1 - p))**(1/b)) + + def expectation(self, expr, var, **kwargs): + a, b = self.args + return Piecewise((S.NaN, b <= 1), (pi*a/(b*sin(pi/b)), True)) + +def LogLogistic(name, alpha, beta): + r""" + Create a continuous random variable with a log-logistic distribution. + The distribution is unimodal when ``beta > 1``. + + Explanation + =========== + + The density of the log-logistic distribution is given by + + .. math:: + f(x) := \frac{(\frac{\beta}{\alpha})(\frac{x}{\alpha})^{\beta - 1}} + {(1 + (\frac{x}{\alpha})^{\beta})^2} + + Parameters + ========== + + alpha : Real number, `\alpha > 0`, scale parameter and median of distribution + beta : Real number, `\beta > 0`, a shape parameter + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import LogLogistic, density, cdf, quantile + >>> from sympy import Symbol, pprint + + >>> alpha = Symbol("alpha", positive=True) + >>> beta = Symbol("beta", positive=True) + >>> p = Symbol("p") + >>> z = Symbol("z", positive=True) + + >>> X = LogLogistic("x", alpha, beta) + + >>> D = density(X)(z) + >>> pprint(D, use_unicode=False) + beta - 1 + / z \ + beta*|-----| + \alpha/ + ------------------------ + 2 + / beta \ + |/ z \ | + alpha*||-----| + 1| + \\alpha/ / + + >>> cdf(X)(z) + 1/(1 + (z/alpha)**(-beta)) + + >>> quantile(X)(p) + alpha*(p/(1 - p))**(1/beta) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Log-logistic_distribution + + """ + + return rv(name, LogLogisticDistribution, (alpha, beta)) + +#------------------------------------------------------------------------------- +#Logit-Normal distribution------------------------------------------------------ + +class LogitNormalDistribution(SingleContinuousDistribution): + _argnames = ('mu', 's') + set = Interval.open(0, 1) + + @staticmethod + def check(mu, s): + _value_check((s ** 2).is_real is not False and s ** 2 > 0, "Squared scale parameter s must be positive.") + _value_check(mu.is_real is not False, "Location parameter must be real") + + def _logit(self, x): + return log(x / (1 - x)) + + def pdf(self, x): + mu, s = self.mu, self.s + return exp(-(self._logit(x) - mu)**2/(2*s**2))*(S.One/sqrt(2*pi*(s**2)))*(1/(x*(1 - x))) + + def _cdf(self, x): + mu, s = self.mu, self.s + return (S.One/2)*(1 + erf((self._logit(x) - mu)/(sqrt(2*s**2)))) + + +def LogitNormal(name, mu, s): + r""" + Create a continuous random variable with a Logit-Normal distribution. + + The density of the logistic distribution is given by + + .. math:: + f(x) := \frac{1}{s \sqrt{2 \pi}} \frac{1}{x(1 - x)} e^{- \frac{(logit(x) - \mu)^2}{s^2}} + where logit(x) = \log(\frac{x}{1 - x}) + Parameters + ========== + + mu : Real number, the location (mean) + s : Real number, `s > 0`, a scale + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import LogitNormal, density, cdf + >>> from sympy import Symbol,pprint + + >>> mu = Symbol("mu", real=True) + >>> s = Symbol("s", positive=True) + >>> z = Symbol("z") + >>> X = LogitNormal("x",mu,s) + + >>> D = density(X)(z) + >>> pprint(D, use_unicode=False) + 2 + / / z \\ + -|-mu + log|-----|| + \ \1 - z// + --------------------- + 2 + ___ 2*s + \/ 2 *e + ---------------------------- + ____ + 2*\/ pi *s*z*(1 - z) + + >>> density(X)(z) + sqrt(2)*exp(-(-mu + log(z/(1 - z)))**2/(2*s**2))/(2*sqrt(pi)*s*z*(1 - z)) + + >>> cdf(X)(z) + erf(sqrt(2)*(-mu + log(z/(1 - z)))/(2*s))/2 + 1/2 + + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Logit-normal_distribution + + """ + + return rv(name, LogitNormalDistribution, (mu, s)) + +#------------------------------------------------------------------------------- +# Log Normal distribution ------------------------------------------------------ + + +class LogNormalDistribution(SingleContinuousDistribution): + _argnames = ('mean', 'std') + + set = Interval(0, oo) + + @staticmethod + def check(mean, std): + _value_check(std > 0, "Parameter std must be positive.") + + def pdf(self, x): + mean, std = self.mean, self.std + return exp(-(log(x) - mean)**2 / (2*std**2)) / (x*sqrt(2*pi)*std) + + def _cdf(self, x): + mean, std = self.mean, self.std + return Piecewise( + (S.Half + S.Half*erf((log(x) - mean)/sqrt(2)/std), x > 0), + (S.Zero, True) + ) + + def _moment_generating_function(self, t): + raise NotImplementedError('Moment generating function of the log-normal distribution is not defined.') + + +def LogNormal(name, mean, std): + r""" + Create a continuous random variable with a log-normal distribution. + + Explanation + =========== + + The density of the log-normal distribution is given by + + .. math:: + f(x) := \frac{1}{x\sqrt{2\pi\sigma^2}} + e^{-\frac{\left(\ln x-\mu\right)^2}{2\sigma^2}} + + with :math:`x \geq 0`. + + Parameters + ========== + + mu : Real number + The log-scale. + sigma : Real number + A shape. ($\sigma^2 > 0$) + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import LogNormal, density + >>> from sympy import Symbol, pprint + + >>> mu = Symbol("mu", real=True) + >>> sigma = Symbol("sigma", positive=True) + >>> z = Symbol("z") + + >>> X = LogNormal("x", mu, sigma) + + >>> D = density(X)(z) + >>> pprint(D, use_unicode=False) + 2 + -(-mu + log(z)) + ----------------- + 2 + ___ 2*sigma + \/ 2 *e + ------------------------ + ____ + 2*\/ pi *sigma*z + + + >>> X = LogNormal('x', 0, 1) # Mean 0, standard deviation 1 + + >>> density(X)(z) + sqrt(2)*exp(-log(z)**2/2)/(2*sqrt(pi)*z) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Lognormal + .. [2] https://mathworld.wolfram.com/LogNormalDistribution.html + + """ + + return rv(name, LogNormalDistribution, (mean, std)) + +#------------------------------------------------------------------------------- +# Lomax Distribution ----------------------------------------------------------- + +class LomaxDistribution(SingleContinuousDistribution): + _argnames = ('alpha', 'lamda',) + set = Interval(0, oo) + + @staticmethod + def check(alpha, lamda): + _value_check(alpha.is_real, "Shape parameter should be real.") + _value_check(lamda.is_real, "Scale parameter should be real.") + _value_check(alpha.is_positive, "Shape parameter should be positive.") + _value_check(lamda.is_positive, "Scale parameter should be positive.") + + def pdf(self, x): + lamba, alpha = self.lamda, self.alpha + return (alpha/lamba) * (S.One + x/lamba)**(-alpha-1) + +def Lomax(name, alpha, lamda): + r""" + Create a continuous random variable with a Lomax distribution. + + Explanation + =========== + + The density of the Lomax distribution is given by + + .. math:: + f(x) := \frac{\alpha}{\lambda}\left[1+\frac{x}{\lambda}\right]^{-(\alpha+1)} + + Parameters + ========== + + alpha : Real Number, `\alpha > 0` + Shape parameter + lamda : Real Number, `\lambda > 0` + Scale parameter + + Examples + ======== + + >>> from sympy.stats import Lomax, density, cdf, E + >>> from sympy import symbols + >>> a, l = symbols('a, l', positive=True) + >>> X = Lomax('X', a, l) + >>> x = symbols('x') + >>> density(X)(x) + a*(1 + x/l)**(-a - 1)/l + >>> cdf(X)(x) + Piecewise((1 - 1/(1 + x/l)**a, x >= 0), (0, True)) + >>> a = 2 + >>> X = Lomax('X', a, l) + >>> E(X) + l + + Returns + ======= + + RandomSymbol + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Lomax_distribution + + """ + return rv(name, LomaxDistribution, (alpha, lamda)) + +#------------------------------------------------------------------------------- +# Maxwell distribution --------------------------------------------------------- + + +class MaxwellDistribution(SingleContinuousDistribution): + _argnames = ('a',) + + set = Interval(0, oo) + + @staticmethod + def check(a): + _value_check(a > 0, "Parameter a must be positive.") + + def pdf(self, x): + a = self.a + return sqrt(2/pi)*x**2*exp(-x**2/(2*a**2))/a**3 + + def _cdf(self, x): + a = self.a + return erf(sqrt(2)*x/(2*a)) - sqrt(2)*x*exp(-x**2/(2*a**2))/(sqrt(pi)*a) + +def Maxwell(name, a): + r""" + Create a continuous random variable with a Maxwell distribution. + + Explanation + =========== + + The density of the Maxwell distribution is given by + + .. math:: + f(x) := \sqrt{\frac{2}{\pi}} \frac{x^2 e^{-x^2/(2a^2)}}{a^3} + + with :math:`x \geq 0`. + + .. TODO - what does the parameter mean? + + Parameters + ========== + + a : Real number, `a > 0` + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Maxwell, density, E, variance + >>> from sympy import Symbol, simplify + + >>> a = Symbol("a", positive=True) + >>> z = Symbol("z") + + >>> X = Maxwell("x", a) + + >>> density(X)(z) + sqrt(2)*z**2*exp(-z**2/(2*a**2))/(sqrt(pi)*a**3) + + >>> E(X) + 2*sqrt(2)*a/sqrt(pi) + + >>> simplify(variance(X)) + a**2*(-8 + 3*pi)/pi + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Maxwell_distribution + .. [2] https://mathworld.wolfram.com/MaxwellDistribution.html + + """ + + return rv(name, MaxwellDistribution, (a, )) + +#------------------------------------------------------------------------------- +# Moyal Distribution ----------------------------------------------------------- +class MoyalDistribution(SingleContinuousDistribution): + _argnames = ('mu', 'sigma') + + @staticmethod + def check(mu, sigma): + _value_check(mu.is_real, "Location parameter must be real.") + _value_check(sigma.is_real and sigma > 0, "Scale parameter must be real\ + and positive.") + + def pdf(self, x): + mu, sigma = self.mu, self.sigma + num = exp(-(exp(-(x - mu)/sigma) + (x - mu)/(sigma))/2) + den = (sqrt(2*pi) * sigma) + return num/den + + def _characteristic_function(self, t): + mu, sigma = self.mu, self.sigma + term1 = exp(I*t*mu) + term2 = (2**(-I*sigma*t) * gamma(Rational(1, 2) - I*t*sigma)) + return (term1 * term2)/sqrt(pi) + + def _moment_generating_function(self, t): + mu, sigma = self.mu, self.sigma + term1 = exp(t*mu) + term2 = (2**(-1*sigma*t) * gamma(Rational(1, 2) - t*sigma)) + return (term1 * term2)/sqrt(pi) + +def Moyal(name, mu, sigma): + r""" + Create a continuous random variable with a Moyal distribution. + + Explanation + =========== + + The density of the Moyal distribution is given by + + .. math:: + f(x) := \frac{\exp-\frac{1}{2}\exp-\frac{x-\mu}{\sigma}-\frac{x-\mu}{2\sigma}}{\sqrt{2\pi}\sigma} + + with :math:`x \in \mathbb{R}`. + + Parameters + ========== + + mu : Real number + Location parameter + sigma : Real positive number + Scale parameter + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Moyal, density, cdf + >>> from sympy import Symbol, simplify + >>> mu = Symbol("mu", real=True) + >>> sigma = Symbol("sigma", positive=True, real=True) + >>> z = Symbol("z") + >>> X = Moyal("x", mu, sigma) + >>> density(X)(z) + sqrt(2)*exp(-exp((mu - z)/sigma)/2 - (-mu + z)/(2*sigma))/(2*sqrt(pi)*sigma) + >>> simplify(cdf(X)(z)) + 1 - erf(sqrt(2)*exp((mu - z)/(2*sigma))/2) + + References + ========== + + .. [1] https://reference.wolfram.com/language/ref/MoyalDistribution.html + .. [2] https://www.stat.rice.edu/~dobelman/textfiles/DistributionsHandbook.pdf + + """ + + return rv(name, MoyalDistribution, (mu, sigma)) + +#------------------------------------------------------------------------------- +# Nakagami distribution -------------------------------------------------------- + + +class NakagamiDistribution(SingleContinuousDistribution): + _argnames = ('mu', 'omega') + + set = Interval(0, oo) + + @staticmethod + def check(mu, omega): + _value_check(mu >= S.Half, "Shape parameter mu must be greater than equal to 1/2.") + _value_check(omega > 0, "Spread parameter omega must be positive.") + + def pdf(self, x): + mu, omega = self.mu, self.omega + return 2*mu**mu/(gamma(mu)*omega**mu)*x**(2*mu - 1)*exp(-mu/omega*x**2) + + def _cdf(self, x): + mu, omega = self.mu, self.omega + return Piecewise( + (lowergamma(mu, (mu/omega)*x**2)/gamma(mu), x > 0), + (S.Zero, True)) + +def Nakagami(name, mu, omega): + r""" + Create a continuous random variable with a Nakagami distribution. + + Explanation + =========== + + The density of the Nakagami distribution is given by + + .. math:: + f(x) := \frac{2\mu^\mu}{\Gamma(\mu)\omega^\mu} x^{2\mu-1} + \exp\left(-\frac{\mu}{\omega}x^2 \right) + + with :math:`x > 0`. + + Parameters + ========== + + mu : Real number, `\mu \geq \frac{1}{2}`, a shape + omega : Real number, `\omega > 0`, the spread + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Nakagami, density, E, variance, cdf + >>> from sympy import Symbol, simplify, pprint + + >>> mu = Symbol("mu", positive=True) + >>> omega = Symbol("omega", positive=True) + >>> z = Symbol("z") + + >>> X = Nakagami("x", mu, omega) + + >>> D = density(X)(z) + >>> pprint(D, use_unicode=False) + 2 + -mu*z + ------- + mu -mu 2*mu - 1 omega + 2*mu *omega *z *e + ---------------------------------- + Gamma(mu) + + >>> simplify(E(X)) + sqrt(mu)*sqrt(omega)*gamma(mu + 1/2)/gamma(mu + 1) + + >>> V = simplify(variance(X)) + >>> pprint(V, use_unicode=False) + 2 + omega*Gamma (mu + 1/2) + omega - ----------------------- + Gamma(mu)*Gamma(mu + 1) + + >>> cdf(X)(z) + Piecewise((lowergamma(mu, mu*z**2/omega)/gamma(mu), z > 0), + (0, True)) + + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Nakagami_distribution + + """ + + return rv(name, NakagamiDistribution, (mu, omega)) + +#------------------------------------------------------------------------------- +# Normal distribution ---------------------------------------------------------- + + +class NormalDistribution(SingleContinuousDistribution): + _argnames = ('mean', 'std') + + @staticmethod + def check(mean, std): + _value_check(std > 0, "Standard deviation must be positive") + + def pdf(self, x): + return exp(-(x - self.mean)**2 / (2*self.std**2)) / (sqrt(2*pi)*self.std) + + def _cdf(self, x): + mean, std = self.mean, self.std + return erf(sqrt(2)*(-mean + x)/(2*std))/2 + S.Half + + def _characteristic_function(self, t): + mean, std = self.mean, self.std + return exp(I*mean*t - std**2*t**2/2) + + def _moment_generating_function(self, t): + mean, std = self.mean, self.std + return exp(mean*t + std**2*t**2/2) + + def _quantile(self, p): + mean, std = self.mean, self.std + return mean + std*sqrt(2)*erfinv(2*p - 1) + + +def Normal(name, mean, std): + r""" + Create a continuous random variable with a Normal distribution. + + Explanation + =========== + + The density of the Normal distribution is given by + + .. math:: + f(x) := \frac{1}{\sigma\sqrt{2\pi}} e^{ -\frac{(x-\mu)^2}{2\sigma^2} } + + Parameters + ========== + + mu : Real number or a list representing the mean or the mean vector + sigma : Real number or a positive definite square matrix, + :math:`\sigma^2 > 0`, the variance + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Normal, density, E, std, cdf, skewness, quantile, marginal_distribution + >>> from sympy import Symbol, simplify, pprint + + >>> mu = Symbol("mu") + >>> sigma = Symbol("sigma", positive=True) + >>> z = Symbol("z") + >>> y = Symbol("y") + >>> p = Symbol("p") + >>> X = Normal("x", mu, sigma) + + >>> density(X)(z) + sqrt(2)*exp(-(-mu + z)**2/(2*sigma**2))/(2*sqrt(pi)*sigma) + + >>> C = simplify(cdf(X))(z) # it needs a little more help... + >>> pprint(C, use_unicode=False) + / ___ \ + |\/ 2 *(-mu + z)| + erf|---------------| + \ 2*sigma / 1 + -------------------- + - + 2 2 + + >>> quantile(X)(p) + mu + sqrt(2)*sigma*erfinv(2*p - 1) + + >>> simplify(skewness(X)) + 0 + + >>> X = Normal("x", 0, 1) # Mean 0, standard deviation 1 + >>> density(X)(z) + sqrt(2)*exp(-z**2/2)/(2*sqrt(pi)) + + >>> E(2*X + 1) + 1 + + >>> simplify(std(2*X + 1)) + 2 + + >>> m = Normal('X', [1, 2], [[2, 1], [1, 2]]) + >>> pprint(density(m)(y, z), use_unicode=False) + 2 2 + y y*z z + - -- + --- - -- + z - 1 + ___ 3 3 3 + \/ 3 *e + ------------------------------ + 6*pi + + >>> marginal_distribution(m, m[0])(1) + 1/(2*sqrt(pi)) + + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Normal_distribution + .. [2] https://mathworld.wolfram.com/NormalDistributionFunction.html + + """ + + if isinstance(mean, list) or getattr(mean, 'is_Matrix', False) and\ + isinstance(std, list) or getattr(std, 'is_Matrix', False): + from sympy.stats.joint_rv_types import MultivariateNormal + return MultivariateNormal(name, mean, std) + return rv(name, NormalDistribution, (mean, std)) + + +#------------------------------------------------------------------------------- +# Inverse Gaussian distribution ---------------------------------------------------------- + + +class GaussianInverseDistribution(SingleContinuousDistribution): + _argnames = ('mean', 'shape') + + @property + def set(self): + return Interval(0, oo) + + @staticmethod + def check(mean, shape): + _value_check(shape > 0, "Shape parameter must be positive") + _value_check(mean > 0, "Mean must be positive") + + def pdf(self, x): + mu, s = self.mean, self.shape + return exp(-s*(x - mu)**2 / (2*x*mu**2)) * sqrt(s/(2*pi*x**3)) + + def _cdf(self, x): + from sympy.stats import cdf + mu, s = self.mean, self.shape + stdNormalcdf = cdf(Normal('x', 0, 1)) + + first_term = stdNormalcdf(sqrt(s/x) * ((x/mu) - S.One)) + second_term = exp(2*s/mu) * stdNormalcdf(-sqrt(s/x)*(x/mu + S.One)) + + return first_term + second_term + + def _characteristic_function(self, t): + mu, s = self.mean, self.shape + return exp((s/mu)*(1 - sqrt(1 - (2*mu**2*I*t)/s))) + + def _moment_generating_function(self, t): + mu, s = self.mean, self.shape + return exp((s/mu)*(1 - sqrt(1 - (2*mu**2*t)/s))) + + +def GaussianInverse(name, mean, shape): + r""" + Create a continuous random variable with an Inverse Gaussian distribution. + Inverse Gaussian distribution is also known as Wald distribution. + + Explanation + =========== + + The density of the Inverse Gaussian distribution is given by + + .. math:: + f(x) := \sqrt{\frac{\lambda}{2\pi x^3}} e^{-\frac{\lambda(x-\mu)^2}{2x\mu^2}} + + Parameters + ========== + + mu : + Positive number representing the mean. + lambda : + Positive number representing the shape parameter. + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import GaussianInverse, density, E, std, skewness + >>> from sympy import Symbol, pprint + + >>> mu = Symbol("mu", positive=True) + >>> lamda = Symbol("lambda", positive=True) + >>> z = Symbol("z", positive=True) + >>> X = GaussianInverse("x", mu, lamda) + + >>> D = density(X)(z) + >>> pprint(D, use_unicode=False) + 2 + -lambda*(-mu + z) + ------------------- + 2 + ___ ________ 2*mu *z + \/ 2 *\/ lambda *e + ------------------------------------- + ____ 3/2 + 2*\/ pi *z + + >>> E(X) + mu + + >>> std(X).expand() + mu**(3/2)/sqrt(lambda) + + >>> skewness(X).expand() + 3*sqrt(mu)/sqrt(lambda) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution + .. [2] https://mathworld.wolfram.com/InverseGaussianDistribution.html + + """ + + return rv(name, GaussianInverseDistribution, (mean, shape)) + +Wald = GaussianInverse + +#------------------------------------------------------------------------------- +# Pareto distribution ---------------------------------------------------------- + + +class ParetoDistribution(SingleContinuousDistribution): + _argnames = ('xm', 'alpha') + + @property + def set(self): + return Interval(self.xm, oo) + + @staticmethod + def check(xm, alpha): + _value_check(xm > 0, "Xm must be positive") + _value_check(alpha > 0, "Alpha must be positive") + + def pdf(self, x): + xm, alpha = self.xm, self.alpha + return alpha * xm**alpha / x**(alpha + 1) + + def _cdf(self, x): + xm, alpha = self.xm, self.alpha + return Piecewise( + (S.One - xm**alpha/x**alpha, x>=xm), + (0, True), + ) + + def _moment_generating_function(self, t): + xm, alpha = self.xm, self.alpha + return alpha * (-xm*t)**alpha * uppergamma(-alpha, -xm*t) + + def _characteristic_function(self, t): + xm, alpha = self.xm, self.alpha + return alpha * (-I * xm * t) ** alpha * uppergamma(-alpha, -I * xm * t) + + +def Pareto(name, xm, alpha): + r""" + Create a continuous random variable with the Pareto distribution. + + Explanation + =========== + + The density of the Pareto distribution is given by + + .. math:: + f(x) := \frac{\alpha\,x_m^\alpha}{x^{\alpha+1}} + + with :math:`x \in [x_m,\infty]`. + + Parameters + ========== + + xm : Real number, `x_m > 0`, a scale + alpha : Real number, `\alpha > 0`, a shape + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Pareto, density + >>> from sympy import Symbol + + >>> xm = Symbol("xm", positive=True) + >>> beta = Symbol("beta", positive=True) + >>> z = Symbol("z") + + >>> X = Pareto("x", xm, beta) + + >>> density(X)(z) + beta*xm**beta*z**(-beta - 1) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Pareto_distribution + .. [2] https://mathworld.wolfram.com/ParetoDistribution.html + + """ + + return rv(name, ParetoDistribution, (xm, alpha)) + +#------------------------------------------------------------------------------- +# PowerFunction distribution --------------------------------------------------- + + +class PowerFunctionDistribution(SingleContinuousDistribution): + _argnames=('alpha','a','b') + + @property + def set(self): + return Interval(self.a, self.b) + + @staticmethod + def check(alpha, a, b): + _value_check(a.is_real, "Continuous Boundary parameter should be real.") + _value_check(b.is_real, "Continuous Boundary parameter should be real.") + _value_check(a < b, " 'a' the left Boundary must be smaller than 'b' the right Boundary." ) + _value_check(alpha.is_positive, "Continuous Shape parameter should be positive.") + + def pdf(self, x): + alpha, a, b = self.alpha, self.a, self.b + num = alpha*(x - a)**(alpha - 1) + den = (b - a)**alpha + return num/den + +def PowerFunction(name, alpha, a, b): + r""" + Creates a continuous random variable with a Power Function Distribution. + + Explanation + =========== + + The density of PowerFunction distribution is given by + + .. math:: + f(x) := \frac{{\alpha}(x - a)^{\alpha - 1}}{(b - a)^{\alpha}} + + with :math:`x \in [a,b]`. + + Parameters + ========== + + alpha : Positive number, `0 < \alpha`, the shape parameter + a : Real number, :math:`-\infty < a`, the left boundary + b : Real number, :math:`a < b < \infty`, the right boundary + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import PowerFunction, density, cdf, E, variance + >>> from sympy import Symbol + >>> alpha = Symbol("alpha", positive=True) + >>> a = Symbol("a", real=True) + >>> b = Symbol("b", real=True) + >>> z = Symbol("z") + + >>> X = PowerFunction("X", 2, a, b) + + >>> density(X)(z) + (-2*a + 2*z)/(-a + b)**2 + + >>> cdf(X)(z) + Piecewise((a**2/(a**2 - 2*a*b + b**2) - 2*a*z/(a**2 - 2*a*b + b**2) + + z**2/(a**2 - 2*a*b + b**2), a <= z), (0, True)) + + >>> alpha = 2 + >>> a = 0 + >>> b = 1 + >>> Y = PowerFunction("Y", alpha, a, b) + + >>> E(Y) + 2/3 + + >>> variance(Y) + 1/18 + + References + ========== + + .. [1] https://web.archive.org/web/20200204081320/http://www.mathwave.com/help/easyfit/html/analyses/distributions/power_func.html + + """ + return rv(name, PowerFunctionDistribution, (alpha, a, b)) + +#------------------------------------------------------------------------------- +# QuadraticU distribution ------------------------------------------------------ + + +class QuadraticUDistribution(SingleContinuousDistribution): + _argnames = ('a', 'b') + + @property + def set(self): + return Interval(self.a, self.b) + + @staticmethod + def check(a, b): + _value_check(b > a, "Parameter b must be in range (%s, oo)."%(a)) + + def pdf(self, x): + a, b = self.a, self.b + alpha = 12 / (b-a)**3 + beta = (a+b) / 2 + return Piecewise( + (alpha * (x-beta)**2, And(a<=x, x<=b)), + (S.Zero, True)) + + def _moment_generating_function(self, t): + a, b = self.a, self.b + return -3 * (exp(a*t) * (4 + (a**2 + 2*a*(-2 + b) + b**2) * t) \ + - exp(b*t) * (4 + (-4*b + (a + b)**2) * t)) / ((a-b)**3 * t**2) + + def _characteristic_function(self, t): + a, b = self.a, self.b + return -3*I*(exp(I*a*t*exp(I*b*t)) * (4*I - (-4*b + (a+b)**2)*t)) \ + / ((a-b)**3 * t**2) + + +def QuadraticU(name, a, b): + r""" + Create a Continuous Random Variable with a U-quadratic distribution. + + Explanation + =========== + + The density of the U-quadratic distribution is given by + + .. math:: + f(x) := \alpha (x-\beta)^2 + + with :math:`x \in [a,b]`. + + Parameters + ========== + + a : Real number + b : Real number, :math:`a < b` + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import QuadraticU, density + >>> from sympy import Symbol, pprint + + >>> a = Symbol("a", real=True) + >>> b = Symbol("b", real=True) + >>> z = Symbol("z") + + >>> X = QuadraticU("x", a, b) + + >>> D = density(X)(z) + >>> pprint(D, use_unicode=False) + / 2 + | / a b \ + |12*|- - - - + z| + | \ 2 2 / + <----------------- for And(b >= z, a <= z) + | 3 + | (-a + b) + | + \ 0 otherwise + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/U-quadratic_distribution + + """ + + return rv(name, QuadraticUDistribution, (a, b)) + +#------------------------------------------------------------------------------- +# RaisedCosine distribution ---------------------------------------------------- + + +class RaisedCosineDistribution(SingleContinuousDistribution): + _argnames = ('mu', 's') + + @property + def set(self): + return Interval(self.mu - self.s, self.mu + self.s) + + @staticmethod + def check(mu, s): + _value_check(s > 0, "s must be positive") + + def pdf(self, x): + mu, s = self.mu, self.s + return Piecewise( + ((1+cos(pi*(x-mu)/s)) / (2*s), And(mu-s<=x, x<=mu+s)), + (S.Zero, True)) + + def _characteristic_function(self, t): + mu, s = self.mu, self.s + return Piecewise((exp(-I*pi*mu/s)/2, Eq(t, -pi/s)), + (exp(I*pi*mu/s)/2, Eq(t, pi/s)), + (pi**2*sin(s*t)*exp(I*mu*t) / (s*t*(pi**2 - s**2*t**2)), True)) + + def _moment_generating_function(self, t): + mu, s = self.mu, self.s + return pi**2 * sinh(s*t) * exp(mu*t) / (s*t*(pi**2 + s**2*t**2)) + +def RaisedCosine(name, mu, s): + r""" + Create a Continuous Random Variable with a raised cosine distribution. + + Explanation + =========== + + The density of the raised cosine distribution is given by + + .. math:: + f(x) := \frac{1}{2s}\left(1+\cos\left(\frac{x-\mu}{s}\pi\right)\right) + + with :math:`x \in [\mu-s,\mu+s]`. + + Parameters + ========== + + mu : Real number + s : Real number, `s > 0` + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import RaisedCosine, density + >>> from sympy import Symbol, pprint + + >>> mu = Symbol("mu", real=True) + >>> s = Symbol("s", positive=True) + >>> z = Symbol("z") + + >>> X = RaisedCosine("x", mu, s) + + >>> D = density(X)(z) + >>> pprint(D, use_unicode=False) + / /pi*(-mu + z)\ + |cos|------------| + 1 + | \ s / + <--------------------- for And(z >= mu - s, z <= mu + s) + | 2*s + | + \ 0 otherwise + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Raised_cosine_distribution + + """ + + return rv(name, RaisedCosineDistribution, (mu, s)) + +#------------------------------------------------------------------------------- +# Rayleigh distribution -------------------------------------------------------- + + +class RayleighDistribution(SingleContinuousDistribution): + _argnames = ('sigma',) + + set = Interval(0, oo) + + @staticmethod + def check(sigma): + _value_check(sigma > 0, "Scale parameter sigma must be positive.") + + def pdf(self, x): + sigma = self.sigma + return x/sigma**2*exp(-x**2/(2*sigma**2)) + + def _cdf(self, x): + sigma = self.sigma + return 1 - exp(-(x**2/(2*sigma**2))) + + def _characteristic_function(self, t): + sigma = self.sigma + return 1 - sigma*t*exp(-sigma**2*t**2/2) * sqrt(pi/2) * (erfi(sigma*t/sqrt(2)) - I) + + def _moment_generating_function(self, t): + sigma = self.sigma + return 1 + sigma*t*exp(sigma**2*t**2/2) * sqrt(pi/2) * (erf(sigma*t/sqrt(2)) + 1) + + +def Rayleigh(name, sigma): + r""" + Create a continuous random variable with a Rayleigh distribution. + + Explanation + =========== + + The density of the Rayleigh distribution is given by + + .. math :: + f(x) := \frac{x}{\sigma^2} e^{-x^2/2\sigma^2} + + with :math:`x > 0`. + + Parameters + ========== + + sigma : Real number, `\sigma > 0` + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Rayleigh, density, E, variance + >>> from sympy import Symbol + + >>> sigma = Symbol("sigma", positive=True) + >>> z = Symbol("z") + + >>> X = Rayleigh("x", sigma) + + >>> density(X)(z) + z*exp(-z**2/(2*sigma**2))/sigma**2 + + >>> E(X) + sqrt(2)*sqrt(pi)*sigma/2 + + >>> variance(X) + -pi*sigma**2/2 + 2*sigma**2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Rayleigh_distribution + .. [2] https://mathworld.wolfram.com/RayleighDistribution.html + + """ + + return rv(name, RayleighDistribution, (sigma, )) + +#------------------------------------------------------------------------------- +# Reciprocal distribution -------------------------------------------------------- + +class ReciprocalDistribution(SingleContinuousDistribution): + _argnames = ('a', 'b') + + @property + def set(self): + return Interval(self.a, self.b) + + @staticmethod + def check(a, b): + _value_check(a > 0, "Parameter > 0. a = %s"%a) + _value_check((a < b), + "Parameter b must be in range (%s, +oo]. b = %s"%(a, b)) + + def pdf(self, x): + a, b = self.a, self.b + return 1/(x*(log(b) - log(a))) + + +def Reciprocal(name, a, b): + r"""Creates a continuous random variable with a reciprocal distribution. + + + Parameters + ========== + + a : Real number, :math:`0 < a` + b : Real number, :math:`a < b` + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Reciprocal, density, cdf + >>> from sympy import symbols + >>> a, b, x = symbols('a, b, x', positive=True) + >>> R = Reciprocal('R', a, b) + + >>> density(R)(x) + 1/(x*(-log(a) + log(b))) + >>> cdf(R)(x) + Piecewise((log(a)/(log(a) - log(b)) - log(x)/(log(a) - log(b)), a <= x), (0, True)) + + Reference + ========= + + .. [1] https://en.wikipedia.org/wiki/Reciprocal_distribution + + """ + return rv(name, ReciprocalDistribution, (a, b)) + + +#------------------------------------------------------------------------------- +# Shifted Gompertz distribution ------------------------------------------------ + + +class ShiftedGompertzDistribution(SingleContinuousDistribution): + _argnames = ('b', 'eta') + + set = Interval(0, oo) + + @staticmethod + def check(b, eta): + _value_check(b > 0, "b must be positive") + _value_check(eta > 0, "eta must be positive") + + def pdf(self, x): + b, eta = self.b, self.eta + return b*exp(-b*x)*exp(-eta*exp(-b*x))*(1+eta*(1-exp(-b*x))) + +def ShiftedGompertz(name, b, eta): + r""" + Create a continuous random variable with a Shifted Gompertz distribution. + + Explanation + =========== + + The density of the Shifted Gompertz distribution is given by + + .. math:: + f(x) := b e^{-b x} e^{-\eta \exp(-b x)} \left[1 + \eta(1 - e^(-bx)) \right] + + with :math:`x \in [0, \infty)`. + + Parameters + ========== + + b : Real number, `b > 0`, a scale + eta : Real number, `\eta > 0`, a shape + + Returns + ======= + + RandomSymbol + + Examples + ======== + >>> from sympy.stats import ShiftedGompertz, density + >>> from sympy import Symbol + + >>> b = Symbol("b", positive=True) + >>> eta = Symbol("eta", positive=True) + >>> x = Symbol("x") + + >>> X = ShiftedGompertz("x", b, eta) + + >>> density(X)(x) + b*(eta*(1 - exp(-b*x)) + 1)*exp(-b*x)*exp(-eta*exp(-b*x)) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Shifted_Gompertz_distribution + + """ + return rv(name, ShiftedGompertzDistribution, (b, eta)) + +#------------------------------------------------------------------------------- +# StudentT distribution -------------------------------------------------------- + + +class StudentTDistribution(SingleContinuousDistribution): + _argnames = ('nu',) + + set = Interval(-oo, oo) + + @staticmethod + def check(nu): + _value_check(nu > 0, "Degrees of freedom nu must be positive.") + + def pdf(self, x): + nu = self.nu + return 1/(sqrt(nu)*beta_fn(S.Half, nu/2))*(1 + x**2/nu)**(-(nu + 1)/2) + + def _cdf(self, x): + nu = self.nu + return S.Half + x*gamma((nu+1)/2)*hyper((S.Half, (nu+1)/2), + (Rational(3, 2),), -x**2/nu)/(sqrt(pi*nu)*gamma(nu/2)) + + def _moment_generating_function(self, t): + raise NotImplementedError('The moment generating function for the Student-T distribution is undefined.') + + +def StudentT(name, nu): + r""" + Create a continuous random variable with a student's t distribution. + + Explanation + =========== + + The density of the student's t distribution is given by + + .. math:: + f(x) := \frac{\Gamma \left(\frac{\nu+1}{2} \right)} + {\sqrt{\nu\pi}\Gamma \left(\frac{\nu}{2} \right)} + \left(1+\frac{x^2}{\nu} \right)^{-\frac{\nu+1}{2}} + + Parameters + ========== + + nu : Real number, `\nu > 0`, the degrees of freedom + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import StudentT, density, cdf + >>> from sympy import Symbol, pprint + + >>> nu = Symbol("nu", positive=True) + >>> z = Symbol("z") + + >>> X = StudentT("x", nu) + + >>> D = density(X)(z) + >>> pprint(D, use_unicode=False) + nu 1 + - -- - - + 2 2 + / 2\ + | z | + |1 + --| + \ nu/ + ----------------- + ____ / nu\ + \/ nu *B|1/2, --| + \ 2 / + + >>> cdf(X)(z) + 1/2 + z*gamma(nu/2 + 1/2)*hyper((1/2, nu/2 + 1/2), (3/2,), + -z**2/nu)/(sqrt(pi)*sqrt(nu)*gamma(nu/2)) + + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Student_t-distribution + .. [2] https://mathworld.wolfram.com/Studentst-Distribution.html + + """ + + return rv(name, StudentTDistribution, (nu, )) + +#------------------------------------------------------------------------------- +# Trapezoidal distribution ------------------------------------------------------ + + +class TrapezoidalDistribution(SingleContinuousDistribution): + _argnames = ('a', 'b', 'c', 'd') + + @property + def set(self): + return Interval(self.a, self.d) + + @staticmethod + def check(a, b, c, d): + _value_check(a < d, "Lower bound parameter a < %s. a = %s"%(d, a)) + _value_check((a <= b, b < c), + "Level start parameter b must be in range [%s, %s). b = %s"%(a, c, b)) + _value_check((b < c, c <= d), + "Level end parameter c must be in range (%s, %s]. c = %s"%(b, d, c)) + _value_check(d >= c, "Upper bound parameter d > %s. d = %s"%(c, d)) + + def pdf(self, x): + a, b, c, d = self.a, self.b, self.c, self.d + return Piecewise( + (2*(x-a) / ((b-a)*(d+c-a-b)), And(a <= x, x < b)), + (2 / (d+c-a-b), And(b <= x, x < c)), + (2*(d-x) / ((d-c)*(d+c-a-b)), And(c <= x, x <= d)), + (S.Zero, True)) + +def Trapezoidal(name, a, b, c, d): + r""" + Create a continuous random variable with a trapezoidal distribution. + + Explanation + =========== + + The density of the trapezoidal distribution is given by + + .. math:: + f(x) := \begin{cases} + 0 & \mathrm{for\ } x < a, \\ + \frac{2(x-a)}{(b-a)(d+c-a-b)} & \mathrm{for\ } a \le x < b, \\ + \frac{2}{d+c-a-b} & \mathrm{for\ } b \le x < c, \\ + \frac{2(d-x)}{(d-c)(d+c-a-b)} & \mathrm{for\ } c \le x < d, \\ + 0 & \mathrm{for\ } d < x. + \end{cases} + + Parameters + ========== + + a : Real number, :math:`a < d` + b : Real number, :math:`a \le b < c` + c : Real number, :math:`b < c \le d` + d : Real number + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Trapezoidal, density + >>> from sympy import Symbol, pprint + + >>> a = Symbol("a") + >>> b = Symbol("b") + >>> c = Symbol("c") + >>> d = Symbol("d") + >>> z = Symbol("z") + + >>> X = Trapezoidal("x", a,b,c,d) + + >>> pprint(density(X)(z), use_unicode=False) + / -2*a + 2*z + |------------------------- for And(a <= z, b > z) + |(-a + b)*(-a - b + c + d) + | + | 2 + | -------------- for And(b <= z, c > z) + < -a - b + c + d + | + | 2*d - 2*z + |------------------------- for And(d >= z, c <= z) + |(-c + d)*(-a - b + c + d) + | + \ 0 otherwise + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Trapezoidal_distribution + + """ + return rv(name, TrapezoidalDistribution, (a, b, c, d)) + +#------------------------------------------------------------------------------- +# Triangular distribution ------------------------------------------------------ + + +class TriangularDistribution(SingleContinuousDistribution): + _argnames = ('a', 'b', 'c') + + @property + def set(self): + return Interval(self.a, self.b) + + @staticmethod + def check(a, b, c): + _value_check(b > a, "Parameter b > %s. b = %s"%(a, b)) + _value_check((a <= c, c <= b), + "Parameter c must be in range [%s, %s]. c = %s"%(a, b, c)) + + def pdf(self, x): + a, b, c = self.a, self.b, self.c + return Piecewise( + (2*(x - a)/((b - a)*(c - a)), And(a <= x, x < c)), + (2/(b - a), Eq(x, c)), + (2*(b - x)/((b - a)*(b - c)), And(c < x, x <= b)), + (S.Zero, True)) + + def _characteristic_function(self, t): + a, b, c = self.a, self.b, self.c + return -2 *((b-c) * exp(I*a*t) - (b-a) * exp(I*c*t) + (c-a) * exp(I*b*t)) / ((b-a)*(c-a)*(b-c)*t**2) + + def _moment_generating_function(self, t): + a, b, c = self.a, self.b, self.c + return 2 * ((b - c) * exp(a * t) - (b - a) * exp(c * t) + (c - a) * exp(b * t)) / ( + (b - a) * (c - a) * (b - c) * t ** 2) + + +def Triangular(name, a, b, c): + r""" + Create a continuous random variable with a triangular distribution. + + Explanation + =========== + + The density of the triangular distribution is given by + + .. math:: + f(x) := \begin{cases} + 0 & \mathrm{for\ } x < a, \\ + \frac{2(x-a)}{(b-a)(c-a)} & \mathrm{for\ } a \le x < c, \\ + \frac{2}{b-a} & \mathrm{for\ } x = c, \\ + \frac{2(b-x)}{(b-a)(b-c)} & \mathrm{for\ } c < x \le b, \\ + 0 & \mathrm{for\ } b < x. + \end{cases} + + Parameters + ========== + + a : Real number, :math:`a \in \left(-\infty, \infty\right)` + b : Real number, :math:`a < b` + c : Real number, :math:`a \leq c \leq b` + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Triangular, density + >>> from sympy import Symbol, pprint + + >>> a = Symbol("a") + >>> b = Symbol("b") + >>> c = Symbol("c") + >>> z = Symbol("z") + + >>> X = Triangular("x", a,b,c) + + >>> pprint(density(X)(z), use_unicode=False) + / -2*a + 2*z + |----------------- for And(a <= z, c > z) + |(-a + b)*(-a + c) + | + | 2 + | ------ for c = z + < -a + b + | + | 2*b - 2*z + |---------------- for And(b >= z, c < z) + |(-a + b)*(b - c) + | + \ 0 otherwise + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Triangular_distribution + .. [2] https://mathworld.wolfram.com/TriangularDistribution.html + + """ + + return rv(name, TriangularDistribution, (a, b, c)) + +#------------------------------------------------------------------------------- +# Uniform distribution --------------------------------------------------------- + + +class UniformDistribution(SingleContinuousDistribution): + _argnames = ('left', 'right') + + @property + def set(self): + return Interval(self.left, self.right) + + @staticmethod + def check(left, right): + _value_check(left < right, "Lower limit should be less than Upper limit.") + + def pdf(self, x): + left, right = self.left, self.right + return Piecewise( + (S.One/(right - left), And(left <= x, x <= right)), + (S.Zero, True) + ) + + def _cdf(self, x): + left, right = self.left, self.right + return Piecewise( + (S.Zero, x < left), + ((x - left)/(right - left), x <= right), + (S.One, True) + ) + + def _characteristic_function(self, t): + left, right = self.left, self.right + return Piecewise(((exp(I*t*right) - exp(I*t*left)) / (I*t*(right - left)), Ne(t, 0)), + (S.One, True)) + + def _moment_generating_function(self, t): + left, right = self.left, self.right + return Piecewise(((exp(t*right) - exp(t*left)) / (t * (right - left)), Ne(t, 0)), + (S.One, True)) + + def expectation(self, expr, var, **kwargs): + kwargs['evaluate'] = True + result = SingleContinuousDistribution.expectation(self, expr, var, **kwargs) + result = result.subs({Max(self.left, self.right): self.right, + Min(self.left, self.right): self.left}) + return result + + +def Uniform(name, left, right): + r""" + Create a continuous random variable with a uniform distribution. + + Explanation + =========== + + The density of the uniform distribution is given by + + .. math:: + f(x) := \begin{cases} + \frac{1}{b - a} & \text{for } x \in [a,b] \\ + 0 & \text{otherwise} + \end{cases} + + with :math:`x \in [a,b]`. + + Parameters + ========== + + a : Real number, :math:`-\infty < a`, the left boundary + b : Real number, :math:`a < b < \infty`, the right boundary + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Uniform, density, cdf, E, variance + >>> from sympy import Symbol, simplify + + >>> a = Symbol("a", negative=True) + >>> b = Symbol("b", positive=True) + >>> z = Symbol("z") + + >>> X = Uniform("x", a, b) + + >>> density(X)(z) + Piecewise((1/(-a + b), (b >= z) & (a <= z)), (0, True)) + + >>> cdf(X)(z) + Piecewise((0, a > z), ((-a + z)/(-a + b), b >= z), (1, True)) + + >>> E(X) + a/2 + b/2 + + >>> simplify(variance(X)) + a**2/12 - a*b/6 + b**2/12 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Uniform_distribution_%28continuous%29 + .. [2] https://mathworld.wolfram.com/UniformDistribution.html + + """ + + return rv(name, UniformDistribution, (left, right)) + +#------------------------------------------------------------------------------- +# UniformSum distribution ------------------------------------------------------ + + +class UniformSumDistribution(SingleContinuousDistribution): + _argnames = ('n',) + + @property + def set(self): + return Interval(0, self.n) + + @staticmethod + def check(n): + _value_check((n > 0, n.is_integer), + "Parameter n must be positive integer.") + + def pdf(self, x): + n = self.n + k = Dummy("k") + return 1/factorial( + n - 1)*Sum((-1)**k*binomial(n, k)*(x - k)**(n - 1), (k, 0, floor(x))) + + def _cdf(self, x): + n = self.n + k = Dummy("k") + return Piecewise((S.Zero, x < 0), + (1/factorial(n)*Sum((-1)**k*binomial(n, k)*(x - k)**(n), + (k, 0, floor(x))), x <= n), + (S.One, True)) + + def _characteristic_function(self, t): + return ((exp(I*t) - 1) / (I*t))**self.n + + def _moment_generating_function(self, t): + return ((exp(t) - 1) / t)**self.n + +def UniformSum(name, n): + r""" + Create a continuous random variable with an Irwin-Hall distribution. + + Explanation + =========== + + The probability distribution function depends on a single parameter + $n$ which is an integer. + + The density of the Irwin-Hall distribution is given by + + .. math :: + f(x) := \frac{1}{(n-1)!}\sum_{k=0}^{\left\lfloor x\right\rfloor}(-1)^k + \binom{n}{k}(x-k)^{n-1} + + Parameters + ========== + + n : A positive integer, `n > 0` + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import UniformSum, density, cdf + >>> from sympy import Symbol, pprint + + >>> n = Symbol("n", integer=True) + >>> z = Symbol("z") + + >>> X = UniformSum("x", n) + + >>> D = density(X)(z) + >>> pprint(D, use_unicode=False) + floor(z) + ___ + \ ` + \ k n - 1 /n\ + ) (-1) *(-k + z) *| | + / \k/ + /__, + k = 0 + -------------------------------- + (n - 1)! + + >>> cdf(X)(z) + Piecewise((0, z < 0), (Sum((-1)**_k*(-_k + z)**n*binomial(n, _k), + (_k, 0, floor(z)))/factorial(n), n >= z), (1, True)) + + + Compute cdf with specific 'x' and 'n' values as follows : + >>> cdf(UniformSum("x", 5), evaluate=False)(2).doit() + 9/40 + + The argument evaluate=False prevents an attempt at evaluation + of the sum for general n, before the argument 2 is passed. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Uniform_sum_distribution + .. [2] https://mathworld.wolfram.com/UniformSumDistribution.html + + """ + + return rv(name, UniformSumDistribution, (n, )) + +#------------------------------------------------------------------------------- +# VonMises distribution -------------------------------------------------------- + + +class VonMisesDistribution(SingleContinuousDistribution): + _argnames = ('mu', 'k') + + set = Interval(0, 2*pi) + + @staticmethod + def check(mu, k): + _value_check(k > 0, "k must be positive") + + def pdf(self, x): + mu, k = self.mu, self.k + return exp(k*cos(x-mu)) / (2*pi*besseli(0, k)) + +def VonMises(name, mu, k): + r""" + Create a Continuous Random Variable with a von Mises distribution. + + Explanation + =========== + + The density of the von Mises distribution is given by + + .. math:: + f(x) := \frac{e^{\kappa\cos(x-\mu)}}{2\pi I_0(\kappa)} + + with :math:`x \in [0,2\pi]`. + + Parameters + ========== + + mu : Real number + Measure of location. + k : Real number + Measure of concentration. + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import VonMises, density + >>> from sympy import Symbol, pprint + + >>> mu = Symbol("mu") + >>> k = Symbol("k", positive=True) + >>> z = Symbol("z") + + >>> X = VonMises("x", mu, k) + + >>> D = density(X)(z) + >>> pprint(D, use_unicode=False) + k*cos(mu - z) + e + ------------------ + 2*pi*besseli(0, k) + + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Von_Mises_distribution + .. [2] https://mathworld.wolfram.com/vonMisesDistribution.html + + """ + + return rv(name, VonMisesDistribution, (mu, k)) + +#------------------------------------------------------------------------------- +# Weibull distribution --------------------------------------------------------- + + +class WeibullDistribution(SingleContinuousDistribution): + _argnames = ('alpha', 'beta') + + set = Interval(0, oo) + + @staticmethod + def check(alpha, beta): + _value_check(alpha > 0, "Alpha must be positive") + _value_check(beta > 0, "Beta must be positive") + + def pdf(self, x): + alpha, beta = self.alpha, self.beta + return beta * (x/alpha)**(beta - 1) * exp(-(x/alpha)**beta) / alpha + + +def Weibull(name, alpha, beta): + r""" + Create a continuous random variable with a Weibull distribution. + + Explanation + =========== + + The density of the Weibull distribution is given by + + .. math:: + f(x) := \begin{cases} + \frac{k}{\lambda}\left(\frac{x}{\lambda}\right)^{k-1} + e^{-(x/\lambda)^{k}} & x\geq0\\ + 0 & x<0 + \end{cases} + + Parameters + ========== + + lambda : Real number, $\lambda > 0$, a scale + k : Real number, $k > 0$, a shape + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Weibull, density, E, variance + >>> from sympy import Symbol, simplify + + >>> l = Symbol("lambda", positive=True) + >>> k = Symbol("k", positive=True) + >>> z = Symbol("z") + + >>> X = Weibull("x", l, k) + + >>> density(X)(z) + k*(z/lambda)**(k - 1)*exp(-(z/lambda)**k)/lambda + + >>> simplify(E(X)) + lambda*gamma(1 + 1/k) + + >>> simplify(variance(X)) + lambda**2*(-gamma(1 + 1/k)**2 + gamma(1 + 2/k)) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Weibull_distribution + .. [2] https://mathworld.wolfram.com/WeibullDistribution.html + + """ + + return rv(name, WeibullDistribution, (alpha, beta)) + +#------------------------------------------------------------------------------- +# Wigner semicircle distribution ----------------------------------------------- + + +class WignerSemicircleDistribution(SingleContinuousDistribution): + _argnames = ('R',) + + @property + def set(self): + return Interval(-self.R, self.R) + + @staticmethod + def check(R): + _value_check(R > 0, "Radius R must be positive.") + + def pdf(self, x): + R = self.R + return 2/(pi*R**2)*sqrt(R**2 - x**2) + + def _characteristic_function(self, t): + return Piecewise((2 * besselj(1, self.R*t) / (self.R*t), Ne(t, 0)), + (S.One, True)) + + def _moment_generating_function(self, t): + return Piecewise((2 * besseli(1, self.R*t) / (self.R*t), Ne(t, 0)), + (S.One, True)) + +def WignerSemicircle(name, R): + r""" + Create a continuous random variable with a Wigner semicircle distribution. + + Explanation + =========== + + The density of the Wigner semicircle distribution is given by + + .. math:: + f(x) := \frac2{\pi R^2}\,\sqrt{R^2-x^2} + + with :math:`x \in [-R,R]`. + + Parameters + ========== + + R : Real number, `R > 0`, the radius + + Returns + ======= + + A RandomSymbol. + + Examples + ======== + + >>> from sympy.stats import WignerSemicircle, density, E + >>> from sympy import Symbol + + >>> R = Symbol("R", positive=True) + >>> z = Symbol("z") + + >>> X = WignerSemicircle("x", R) + + >>> density(X)(z) + 2*sqrt(R**2 - z**2)/(pi*R**2) + + >>> E(X) + 0 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Wigner_semicircle_distribution + .. [2] https://mathworld.wolfram.com/WignersSemicircleLaw.html + + """ + + return rv(name, WignerSemicircleDistribution, (R,)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/drv.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/drv.py new file mode 100644 index 0000000000000000000000000000000000000000..dea14f2dfd1078c223c61bb5cd1373105e72ea28 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/drv.py @@ -0,0 +1,350 @@ +from sympy.concrete.summations import (Sum, summation) +from sympy.core.basic import Basic +from sympy.core.cache import cacheit +from sympy.core.function import Lambda +from sympy.core.numbers import I +from sympy.core.relational import (Eq, Ne) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, symbols) +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.integers import floor +from sympy.functions.elementary.piecewise import Piecewise +from sympy.logic.boolalg import And +from sympy.polys.polytools import poly +from sympy.series.series import series + +from sympy.polys.polyerrors import PolynomialError +from sympy.stats.crv import reduce_rational_inequalities_wrap +from sympy.stats.rv import (NamedArgsMixin, SinglePSpace, SingleDomain, + random_symbols, PSpace, ConditionalDomain, RandomDomain, + ProductDomain, Distribution) +from sympy.stats.symbolic_probability import Probability +from sympy.sets.fancysets import Range, FiniteSet +from sympy.sets.sets import Union +from sympy.sets.contains import Contains +from sympy.utilities import filldedent +from sympy.core.sympify import _sympify + + +class DiscreteDistribution(Distribution): + def __call__(self, *args): + return self.pdf(*args) + + +class SingleDiscreteDistribution(DiscreteDistribution, NamedArgsMixin): + """ Discrete distribution of a single variable. + + Serves as superclass for PoissonDistribution etc.... + + Provides methods for pdf, cdf, and sampling + + See Also: + sympy.stats.crv_types.* + """ + + set = S.Integers + + def __new__(cls, *args): + args = list(map(sympify, args)) + return Basic.__new__(cls, *args) + + @staticmethod + def check(*args): + pass + + @cacheit + def compute_cdf(self, **kwargs): + """ Compute the CDF from the PDF. + + Returns a Lambda. + """ + x = symbols('x', integer=True, cls=Dummy) + z = symbols('z', real=True, cls=Dummy) + left_bound = self.set.inf + + # CDF is integral of PDF from left bound to z + pdf = self.pdf(x) + cdf = summation(pdf, (x, left_bound, floor(z)), **kwargs) + # CDF Ensure that CDF left of left_bound is zero + cdf = Piecewise((cdf, z >= left_bound), (0, True)) + return Lambda(z, cdf) + + def _cdf(self, x): + return None + + def cdf(self, x, **kwargs): + """ Cumulative density function """ + if not kwargs: + cdf = self._cdf(x) + if cdf is not None: + return cdf + return self.compute_cdf(**kwargs)(x) + + @cacheit + def compute_characteristic_function(self, **kwargs): + """ Compute the characteristic function from the PDF. + + Returns a Lambda. + """ + x, t = symbols('x, t', real=True, cls=Dummy) + pdf = self.pdf(x) + cf = summation(exp(I*t*x)*pdf, (x, self.set.inf, self.set.sup)) + return Lambda(t, cf) + + def _characteristic_function(self, t): + return None + + def characteristic_function(self, t, **kwargs): + """ Characteristic function """ + if not kwargs: + cf = self._characteristic_function(t) + if cf is not None: + return cf + return self.compute_characteristic_function(**kwargs)(t) + + @cacheit + def compute_moment_generating_function(self, **kwargs): + t = Dummy('t', real=True) + x = Dummy('x', integer=True) + pdf = self.pdf(x) + mgf = summation(exp(t*x)*pdf, (x, self.set.inf, self.set.sup)) + return Lambda(t, mgf) + + def _moment_generating_function(self, t): + return None + + def moment_generating_function(self, t, **kwargs): + if not kwargs: + mgf = self._moment_generating_function(t) + if mgf is not None: + return mgf + return self.compute_moment_generating_function(**kwargs)(t) + + @cacheit + def compute_quantile(self, **kwargs): + """ Compute the Quantile from the PDF. + + Returns a Lambda. + """ + x = Dummy('x', integer=True) + p = Dummy('p', real=True) + left_bound = self.set.inf + pdf = self.pdf(x) + cdf = summation(pdf, (x, left_bound, x), **kwargs) + set = ((x, p <= cdf), ) + return Lambda(p, Piecewise(*set)) + + def _quantile(self, x): + return None + + def quantile(self, x, **kwargs): + """ Cumulative density function """ + if not kwargs: + quantile = self._quantile(x) + if quantile is not None: + return quantile + return self.compute_quantile(**kwargs)(x) + + def expectation(self, expr, var, evaluate=True, **kwargs): + """ Expectation of expression over distribution """ + # TODO: support discrete sets with non integer stepsizes + + if evaluate: + try: + p = poly(expr, var) + + t = Dummy('t', real=True) + + mgf = self.moment_generating_function(t) + deg = p.degree() + taylor = poly(series(mgf, t, 0, deg + 1).removeO(), t) + result = 0 + for k in range(deg+1): + result += p.coeff_monomial(var ** k) * taylor.coeff_monomial(t ** k) * factorial(k) + + return result + + except PolynomialError: + return summation(expr * self.pdf(var), + (var, self.set.inf, self.set.sup), **kwargs) + + else: + return Sum(expr * self.pdf(var), + (var, self.set.inf, self.set.sup), **kwargs) + + def __call__(self, *args): + return self.pdf(*args) + + +class DiscreteDomain(RandomDomain): + """ + A domain with discrete support with step size one. + Represented using symbols and Range. + """ + is_Discrete = True + +class SingleDiscreteDomain(DiscreteDomain, SingleDomain): + def as_boolean(self): + return Contains(self.symbol, self.set) + + +class ConditionalDiscreteDomain(DiscreteDomain, ConditionalDomain): + """ + Domain with discrete support of step size one, that is restricted by + some condition. + """ + @property + def set(self): + rv = self.symbols + if len(self.symbols) > 1: + raise NotImplementedError(filldedent(''' + Multivariate conditional domains are not yet implemented.''')) + rv = list(rv)[0] + return reduce_rational_inequalities_wrap(self.condition, + rv).intersect(self.fulldomain.set) + + +class DiscretePSpace(PSpace): + is_real = True + is_Discrete = True + + @property + def pdf(self): + return self.density(*self.symbols) + + def where(self, condition): + rvs = random_symbols(condition) + assert all(r.symbol in self.symbols for r in rvs) + if len(rvs) > 1: + raise NotImplementedError(filldedent('''Multivariate discrete + random variables are not yet supported.''')) + conditional_domain = reduce_rational_inequalities_wrap(condition, + rvs[0]) + conditional_domain = conditional_domain.intersect(self.domain.set) + return SingleDiscreteDomain(rvs[0].symbol, conditional_domain) + + def probability(self, condition): + complement = isinstance(condition, Ne) + if complement: + condition = Eq(condition.args[0], condition.args[1]) + try: + _domain = self.where(condition).set + if condition == False or _domain is S.EmptySet: + return S.Zero + if condition == True or _domain == self.domain.set: + return S.One + prob = self.eval_prob(_domain) + except NotImplementedError: + from sympy.stats.rv import density + expr = condition.lhs - condition.rhs + dens = density(expr) + if not isinstance(dens, DiscreteDistribution): + from sympy.stats.drv_types import DiscreteDistributionHandmade + dens = DiscreteDistributionHandmade(dens) + z = Dummy('z', real=True) + space = SingleDiscretePSpace(z, dens) + prob = space.probability(condition.__class__(space.value, 0)) + if prob is None: + prob = Probability(condition) + return prob if not complement else S.One - prob + + def eval_prob(self, _domain): + sym = list(self.symbols)[0] + if isinstance(_domain, Range): + n = symbols('n', integer=True) + inf, sup, step = (r for r in _domain.args) + summand = ((self.pdf).replace( + sym, n*step)) + rv = summation(summand, + (n, inf/step, (sup)/step - 1)).doit() + return rv + elif isinstance(_domain, FiniteSet): + pdf = Lambda(sym, self.pdf) + rv = sum(pdf(x) for x in _domain) + return rv + elif isinstance(_domain, Union): + rv = sum(self.eval_prob(x) for x in _domain.args) + return rv + + def conditional_space(self, condition): + # XXX: Converting from set to tuple. The order matters to Lambda + # though so we should be starting with a set... + density = Lambda(tuple(self.symbols), self.pdf/self.probability(condition)) + condition = condition.xreplace({rv: rv.symbol for rv in self.values}) + domain = ConditionalDiscreteDomain(self.domain, condition) + return DiscretePSpace(domain, density) + +class ProductDiscreteDomain(ProductDomain, DiscreteDomain): + def as_boolean(self): + return And(*[domain.as_boolean for domain in self.domains]) + +class SingleDiscretePSpace(DiscretePSpace, SinglePSpace): + """ Discrete probability space over a single univariate variable """ + is_real = True + + @property + def set(self): + return self.distribution.set + + @property + def domain(self): + return SingleDiscreteDomain(self.symbol, self.set) + + def sample(self, size=(), library='scipy', seed=None): + """ + Internal sample method. + + Returns dictionary mapping RandomSymbol to realization value. + """ + return {self.value: self.distribution.sample(size, library=library, seed=seed)} + + def compute_expectation(self, expr, rvs=None, evaluate=True, **kwargs): + rvs = rvs or (self.value,) + if self.value not in rvs: + return expr + + expr = _sympify(expr) + expr = expr.xreplace({rv: rv.symbol for rv in rvs}) + + x = self.value.symbol + try: + return self.distribution.expectation(expr, x, evaluate=evaluate, + **kwargs) + except NotImplementedError: + return Sum(expr * self.pdf, (x, self.set.inf, self.set.sup), + **kwargs) + + def compute_cdf(self, expr, **kwargs): + if expr == self.value: + x = Dummy("x", real=True) + return Lambda(x, self.distribution.cdf(x, **kwargs)) + else: + raise NotImplementedError() + + def compute_density(self, expr, **kwargs): + if expr == self.value: + return self.distribution + raise NotImplementedError() + + def compute_characteristic_function(self, expr, **kwargs): + if expr == self.value: + t = Dummy("t", real=True) + return Lambda(t, self.distribution.characteristic_function(t, **kwargs)) + else: + raise NotImplementedError() + + def compute_moment_generating_function(self, expr, **kwargs): + if expr == self.value: + t = Dummy("t", real=True) + return Lambda(t, self.distribution.moment_generating_function(t, **kwargs)) + else: + raise NotImplementedError() + + def compute_quantile(self, expr, **kwargs): + if expr == self.value: + p = Dummy("p", real=True) + return Lambda(p, self.distribution.quantile(p, **kwargs)) + else: + raise NotImplementedError() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/drv_types.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/drv_types.py new file mode 100644 index 0000000000000000000000000000000000000000..84920d31c0083828efc2cd3f752d2c48f5430102 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/drv_types.py @@ -0,0 +1,849 @@ +""" + +Contains +======== +FlorySchulz +Geometric +Hermite +Logarithmic +NegativeBinomial +Poisson +Skellam +YuleSimon +Zeta +""" + + + +from sympy.concrete.summations import Sum +from sympy.core.basic import Basic +from sympy.core.function import Lambda +from sympy.core.numbers import I +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import Dummy +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import (binomial, factorial, FallingFactorial) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.integers import floor +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.special.bessel import besseli +from sympy.functions.special.beta_functions import beta +from sympy.functions.special.hyper import hyper +from sympy.functions.special.zeta_functions import (polylog, zeta) +from sympy.stats.drv import SingleDiscreteDistribution, SingleDiscretePSpace +from sympy.stats.rv import _value_check, is_random + + +__all__ = ['FlorySchulz', +'Geometric', +'Hermite', +'Logarithmic', +'NegativeBinomial', +'Poisson', +'Skellam', +'YuleSimon', +'Zeta' +] + + +def rv(symbol, cls, *args, **kwargs): + args = list(map(sympify, args)) + dist = cls(*args) + if kwargs.pop('check', True): + dist.check(*args) + pspace = SingleDiscretePSpace(symbol, dist) + if any(is_random(arg) for arg in args): + from sympy.stats.compound_rv import CompoundPSpace, CompoundDistribution + pspace = CompoundPSpace(symbol, CompoundDistribution(dist)) + return pspace.value + + +class DiscreteDistributionHandmade(SingleDiscreteDistribution): + _argnames = ('pdf',) + + def __new__(cls, pdf, set=S.Integers): + return Basic.__new__(cls, pdf, set) + + @property + def set(self): + return self.args[1] + + @staticmethod + def check(pdf, set): + x = Dummy('x') + val = Sum(pdf(x), (x, set._inf, set._sup)).doit() + _value_check(Eq(val, 1) != S.false, "The pdf is incorrect on the given set.") + + + +def DiscreteRV(symbol, density, set=S.Integers, **kwargs): + """ + Create a Discrete Random Variable given the following: + + Parameters + ========== + + symbol : Symbol + Represents name of the random variable. + density : Expression containing symbol + Represents probability density function. + set : set + Represents the region where the pdf is valid, by default is real line. + check : bool + If True, it will check whether the given density + integrates to 1 over the given set. If False, it + will not perform this check. Default is False. + + Examples + ======== + + >>> from sympy.stats import DiscreteRV, P, E + >>> from sympy import Rational, Symbol + >>> x = Symbol('x') + >>> n = 10 + >>> density = Rational(1, 10) + >>> X = DiscreteRV(x, density, set=set(range(n))) + >>> E(X) + 9/2 + >>> P(X>3) + 3/5 + + Returns + ======= + + RandomSymbol + + """ + set = sympify(set) + pdf = Piecewise((density, set.as_relational(symbol)), (0, True)) + pdf = Lambda(symbol, pdf) + # have a default of False while `rv` should have a default of True + kwargs['check'] = kwargs.pop('check', False) + return rv(symbol.name, DiscreteDistributionHandmade, pdf, set, **kwargs) + + +#------------------------------------------------------------------------------- +# Flory-Schulz distribution ------------------------------------------------------------ + +class FlorySchulzDistribution(SingleDiscreteDistribution): + _argnames = ('a',) + set = S.Naturals + + @staticmethod + def check(a): + _value_check((0 < a, a < 1), "a must be between 0 and 1") + + def pdf(self, k): + a = self.a + return (a**2 * k * (1 - a)**(k - 1)) + + def _characteristic_function(self, t): + a = self.a + return a**2*exp(I*t)/((1 + (a - 1)*exp(I*t))**2) + + def _moment_generating_function(self, t): + a = self.a + return a**2*exp(t)/((1 + (a - 1)*exp(t))**2) + + +def FlorySchulz(name, a): + r""" + Create a discrete random variable with a FlorySchulz distribution. + + The density of the FlorySchulz distribution is given by + + .. math:: + f(k) := (a^2) k (1 - a)^{k-1} + + Parameters + ========== + + a : A real number between 0 and 1 + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import density, E, variance, FlorySchulz + >>> from sympy import Symbol, S + + >>> a = S.One / 5 + >>> z = Symbol("z") + + >>> X = FlorySchulz("x", a) + + >>> density(X)(z) + (4/5)**(z - 1)*z/25 + + >>> E(X) + 9 + + >>> variance(X) + 40 + + References + ========== + + https://en.wikipedia.org/wiki/Flory%E2%80%93Schulz_distribution + """ + return rv(name, FlorySchulzDistribution, a) + + +#------------------------------------------------------------------------------- +# Geometric distribution ------------------------------------------------------------ + +class GeometricDistribution(SingleDiscreteDistribution): + _argnames = ('p',) + set = S.Naturals + + @staticmethod + def check(p): + _value_check((0 < p, p <= 1), "p must be between 0 and 1") + + def pdf(self, k): + return (1 - self.p)**(k - 1) * self.p + + def _characteristic_function(self, t): + p = self.p + return p * exp(I*t) / (1 - (1 - p)*exp(I*t)) + + def _moment_generating_function(self, t): + p = self.p + return p * exp(t) / (1 - (1 - p) * exp(t)) + + +def Geometric(name, p): + r""" + Create a discrete random variable with a Geometric distribution. + + Explanation + =========== + + The density of the Geometric distribution is given by + + .. math:: + f(k) := p (1 - p)^{k - 1} + + Parameters + ========== + + p : A probability between 0 and 1 + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Geometric, density, E, variance + >>> from sympy import Symbol, S + + >>> p = S.One / 5 + >>> z = Symbol("z") + + >>> X = Geometric("x", p) + + >>> density(X)(z) + (4/5)**(z - 1)/5 + + >>> E(X) + 5 + + >>> variance(X) + 20 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Geometric_distribution + .. [2] https://mathworld.wolfram.com/GeometricDistribution.html + + """ + return rv(name, GeometricDistribution, p) + + +#------------------------------------------------------------------------------- +# Hermite distribution --------------------------------------------------------- + + +class HermiteDistribution(SingleDiscreteDistribution): + _argnames = ('a1', 'a2') + set = S.Naturals0 + + @staticmethod + def check(a1, a2): + _value_check(a1.is_nonnegative, 'Parameter a1 must be >= 0.') + _value_check(a2.is_nonnegative, 'Parameter a2 must be >= 0.') + + def pdf(self, k): + a1, a2 = self.a1, self.a2 + term1 = exp(-(a1 + a2)) + j = Dummy("j", integer=True) + num = a1**(k - 2*j) * a2**j + den = factorial(k - 2*j) * factorial(j) + return term1 * Sum(num/den, (j, 0, k//2)).doit() + + def _moment_generating_function(self, t): + a1, a2 = self.a1, self.a2 + term1 = a1 * (exp(t) - 1) + term2 = a2 * (exp(2*t) - 1) + return exp(term1 + term2) + + def _characteristic_function(self, t): + a1, a2 = self.a1, self.a2 + term1 = a1 * (exp(I*t) - 1) + term2 = a2 * (exp(2*I*t) - 1) + return exp(term1 + term2) + +def Hermite(name, a1, a2): + r""" + Create a discrete random variable with a Hermite distribution. + + Explanation + =========== + + The density of the Hermite distribution is given by + + .. math:: + f(x):= e^{-a_1 -a_2}\sum_{j=0}^{\left \lfloor x/2 \right \rfloor} + \frac{a_{1}^{x-2j}a_{2}^{j}}{(x-2j)!j!} + + Parameters + ========== + + a1 : A Positive number greater than equal to 0. + a2 : A Positive number greater than equal to 0. + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Hermite, density, E, variance + >>> from sympy import Symbol + + >>> a1 = Symbol("a1", positive=True) + >>> a2 = Symbol("a2", positive=True) + >>> x = Symbol("x") + + >>> H = Hermite("H", a1=5, a2=4) + + >>> density(H)(2) + 33*exp(-9)/2 + + >>> E(H) + 13 + + >>> variance(H) + 21 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Hermite_distribution + + """ + + return rv(name, HermiteDistribution, a1, a2) + + +#------------------------------------------------------------------------------- +# Logarithmic distribution ------------------------------------------------------------ + +class LogarithmicDistribution(SingleDiscreteDistribution): + _argnames = ('p',) + + set = S.Naturals + + @staticmethod + def check(p): + _value_check((p > 0, p < 1), "p should be between 0 and 1") + + def pdf(self, k): + p = self.p + return (-1) * p**k / (k * log(1 - p)) + + def _characteristic_function(self, t): + p = self.p + return log(1 - p * exp(I*t)) / log(1 - p) + + def _moment_generating_function(self, t): + p = self.p + return log(1 - p * exp(t)) / log(1 - p) + + +def Logarithmic(name, p): + r""" + Create a discrete random variable with a Logarithmic distribution. + + Explanation + =========== + + The density of the Logarithmic distribution is given by + + .. math:: + f(k) := \frac{-p^k}{k \ln{(1 - p)}} + + Parameters + ========== + + p : A value between 0 and 1 + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Logarithmic, density, E, variance + >>> from sympy import Symbol, S + + >>> p = S.One / 5 + >>> z = Symbol("z") + + >>> X = Logarithmic("x", p) + + >>> density(X)(z) + -1/(5**z*z*log(4/5)) + + >>> E(X) + -1/(-4*log(5) + 8*log(2)) + + >>> variance(X) + -1/((-4*log(5) + 8*log(2))*(-2*log(5) + 4*log(2))) + 1/(-64*log(2)*log(5) + 64*log(2)**2 + 16*log(5)**2) - 10/(-32*log(5) + 64*log(2)) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Logarithmic_distribution + .. [2] https://mathworld.wolfram.com/LogarithmicDistribution.html + + """ + return rv(name, LogarithmicDistribution, p) + + +#------------------------------------------------------------------------------- +# Negative binomial distribution ------------------------------------------------------------ + +class NegativeBinomialDistribution(SingleDiscreteDistribution): + _argnames = ('r', 'p') + set = S.Naturals0 + + @staticmethod + def check(r, p): + _value_check(r > 0, 'r should be positive') + _value_check((p > 0, p < 1), 'p should be between 0 and 1') + + def pdf(self, k): + r = self.r + p = self.p + + return binomial(k + r - 1, k) * (1 - p)**k * p**r + + def _characteristic_function(self, t): + r = self.r + p = self.p + + return (p / (1 - (1 - p) * exp(I*t)))**r + + def _moment_generating_function(self, t): + r = self.r + p = self.p + + return (p / (1 - (1 - p) * exp(t)))**r + +def NegativeBinomial(name, r, p): + r""" + Create a discrete random variable with a Negative Binomial distribution. + + Explanation + =========== + + The density of the Negative Binomial distribution is given by + + .. math:: + f(k) := \binom{k + r - 1}{k} (1 - p)^k p^r + + Parameters + ========== + + r : A positive value + Number of successes until the experiment is stopped. + p : A value between 0 and 1 + Probability of success. + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import NegativeBinomial, density, E, variance + >>> from sympy import Symbol, S + + >>> r = 5 + >>> p = S.One / 3 + >>> z = Symbol("z") + + >>> X = NegativeBinomial("x", r, p) + + >>> density(X)(z) + (2/3)**z*binomial(z + 4, z)/243 + + >>> E(X) + 10 + + >>> variance(X) + 30 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Negative_binomial_distribution + .. [2] https://mathworld.wolfram.com/NegativeBinomialDistribution.html + + """ + return rv(name, NegativeBinomialDistribution, r, p) + + +#------------------------------------------------------------------------------- +# Poisson distribution ------------------------------------------------------------ + +class PoissonDistribution(SingleDiscreteDistribution): + _argnames = ('lamda',) + + set = S.Naturals0 + + @staticmethod + def check(lamda): + _value_check(lamda > 0, "Lambda must be positive") + + def pdf(self, k): + return self.lamda**k / factorial(k) * exp(-self.lamda) + + def _characteristic_function(self, t): + return exp(self.lamda * (exp(I*t) - 1)) + + def _moment_generating_function(self, t): + return exp(self.lamda * (exp(t) - 1)) + + def expectation(self, expr, var, evaluate=True, **kwargs): + if evaluate: + if expr == var: + return self.lamda + if ( + isinstance(expr, FallingFactorial) + and expr.args[1].is_integer + and expr.args[1].is_positive + and expr.args[0] == var + ): + return self.lamda ** expr.args[1] + return super().expectation(expr, var, evaluate, **kwargs) + +def Poisson(name, lamda): + r""" + Create a discrete random variable with a Poisson distribution. + + Explanation + =========== + + The density of the Poisson distribution is given by + + .. math:: + f(k) := \frac{\lambda^{k} e^{- \lambda}}{k!} + + Parameters + ========== + + lamda : Positive number, a rate + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Poisson, density, E, variance + >>> from sympy import Symbol, simplify + + >>> rate = Symbol("lambda", positive=True) + >>> z = Symbol("z") + + >>> X = Poisson("x", rate) + + >>> density(X)(z) + lambda**z*exp(-lambda)/factorial(z) + + >>> E(X) + lambda + + >>> simplify(variance(X)) + lambda + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Poisson_distribution + .. [2] https://mathworld.wolfram.com/PoissonDistribution.html + + """ + return rv(name, PoissonDistribution, lamda) + + +# ----------------------------------------------------------------------------- +# Skellam distribution -------------------------------------------------------- + + +class SkellamDistribution(SingleDiscreteDistribution): + _argnames = ('mu1', 'mu2') + set = S.Integers + + @staticmethod + def check(mu1, mu2): + _value_check(mu1 >= 0, 'Parameter mu1 must be >= 0') + _value_check(mu2 >= 0, 'Parameter mu2 must be >= 0') + + def pdf(self, k): + (mu1, mu2) = (self.mu1, self.mu2) + term1 = exp(-(mu1 + mu2)) * (mu1 / mu2) ** (k / 2) + term2 = besseli(k, 2 * sqrt(mu1 * mu2)) + return term1 * term2 + + def _cdf(self, x): + raise NotImplementedError( + "Skellam doesn't have closed form for the CDF.") + + def _characteristic_function(self, t): + (mu1, mu2) = (self.mu1, self.mu2) + return exp(-(mu1 + mu2) + mu1 * exp(I * t) + mu2 * exp(-I * t)) + + def _moment_generating_function(self, t): + (mu1, mu2) = (self.mu1, self.mu2) + return exp(-(mu1 + mu2) + mu1 * exp(t) + mu2 * exp(-t)) + + +def Skellam(name, mu1, mu2): + r""" + Create a discrete random variable with a Skellam distribution. + + Explanation + =========== + + The Skellam is the distribution of the difference N1 - N2 + of two statistically independent random variables N1 and N2 + each Poisson-distributed with respective expected values mu1 and mu2. + + The density of the Skellam distribution is given by + + .. math:: + f(k) := e^{-(\mu_1+\mu_2)}(\frac{\mu_1}{\mu_2})^{k/2}I_k(2\sqrt{\mu_1\mu_2}) + + Parameters + ========== + + mu1 : A non-negative value + mu2 : A non-negative value + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Skellam, density, E, variance + >>> from sympy import Symbol, pprint + + >>> z = Symbol("z", integer=True) + >>> mu1 = Symbol("mu1", positive=True) + >>> mu2 = Symbol("mu2", positive=True) + >>> X = Skellam("x", mu1, mu2) + + >>> pprint(density(X)(z), use_unicode=False) + z + - + 2 + /mu1\ -mu1 - mu2 / _____ _____\ + |---| *e *besseli\z, 2*\/ mu1 *\/ mu2 / + \mu2/ + >>> E(X) + mu1 - mu2 + >>> variance(X).expand() + mu1 + mu2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Skellam_distribution + + """ + return rv(name, SkellamDistribution, mu1, mu2) + + +#------------------------------------------------------------------------------- +# Yule-Simon distribution ------------------------------------------------------------ + +class YuleSimonDistribution(SingleDiscreteDistribution): + _argnames = ('rho',) + set = S.Naturals + + @staticmethod + def check(rho): + _value_check(rho > 0, 'rho should be positive') + + def pdf(self, k): + rho = self.rho + return rho * beta(k, rho + 1) + + def _cdf(self, x): + return Piecewise((1 - floor(x) * beta(floor(x), self.rho + 1), x >= 1), (0, True)) + + def _characteristic_function(self, t): + rho = self.rho + return rho * hyper((1, 1), (rho + 2,), exp(I*t)) * exp(I*t) / (rho + 1) + + def _moment_generating_function(self, t): + rho = self.rho + return rho * hyper((1, 1), (rho + 2,), exp(t)) * exp(t) / (rho + 1) + + +def YuleSimon(name, rho): + r""" + Create a discrete random variable with a Yule-Simon distribution. + + Explanation + =========== + + The density of the Yule-Simon distribution is given by + + .. math:: + f(k) := \rho B(k, \rho + 1) + + Parameters + ========== + + rho : A positive value + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import YuleSimon, density, E, variance + >>> from sympy import Symbol, simplify + + >>> p = 5 + >>> z = Symbol("z") + + >>> X = YuleSimon("x", p) + + >>> density(X)(z) + 5*beta(z, 6) + + >>> simplify(E(X)) + 5/4 + + >>> simplify(variance(X)) + 25/48 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Yule%E2%80%93Simon_distribution + + """ + return rv(name, YuleSimonDistribution, rho) + + +#------------------------------------------------------------------------------- +# Zeta distribution ------------------------------------------------------------ + +class ZetaDistribution(SingleDiscreteDistribution): + _argnames = ('s',) + set = S.Naturals + + @staticmethod + def check(s): + _value_check(s > 1, 's should be greater than 1') + + def pdf(self, k): + s = self.s + return 1 / (k**s * zeta(s)) + + def _characteristic_function(self, t): + return polylog(self.s, exp(I*t)) / zeta(self.s) + + def _moment_generating_function(self, t): + return polylog(self.s, exp(t)) / zeta(self.s) + + +def Zeta(name, s): + r""" + Create a discrete random variable with a Zeta distribution. + + Explanation + =========== + + The density of the Zeta distribution is given by + + .. math:: + f(k) := \frac{1}{k^s \zeta{(s)}} + + Parameters + ========== + + s : A value greater than 1 + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import Zeta, density, E, variance + >>> from sympy import Symbol + + >>> s = 5 + >>> z = Symbol("z") + + >>> X = Zeta("x", s) + + >>> density(X)(z) + 1/(z**5*zeta(5)) + + >>> E(X) + pi**4/(90*zeta(5)) + + >>> variance(X) + -pi**8/(8100*zeta(5)**2) + zeta(3)/zeta(5) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Zeta_distribution + + """ + return rv(name, ZetaDistribution, s) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/error_prop.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/error_prop.py new file mode 100644 index 0000000000000000000000000000000000000000..e6cacb894307fe60cbf096c7760e6ed57f385a91 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/error_prop.py @@ -0,0 +1,100 @@ +"""Tools for arithmetic error propagation.""" + +from itertools import repeat, combinations + +from sympy.core.add import Add +from sympy.core.mul import Mul +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions.elementary.exponential import exp +from sympy.simplify.simplify import simplify +from sympy.stats.symbolic_probability import RandomSymbol, Variance, Covariance +from sympy.stats.rv import is_random + +_arg0_or_var = lambda var: var.args[0] if len(var.args) > 0 else var + + +def variance_prop(expr, consts=(), include_covar=False): + r"""Symbolically propagates variance (`\sigma^2`) for expressions. + This is computed as as seen in [1]_. + + Parameters + ========== + + expr : Expr + A SymPy expression to compute the variance for. + consts : sequence of Symbols, optional + Represents symbols that are known constants in the expr, + and thus have zero variance. All symbols not in consts are + assumed to be variant. + include_covar : bool, optional + Flag for whether or not to include covariances, default=False. + + Returns + ======= + + var_expr : Expr + An expression for the total variance of the expr. + The variance for the original symbols (e.g. x) are represented + via instance of the Variance symbol (e.g. Variance(x)). + + Examples + ======== + + >>> from sympy import symbols, exp + >>> from sympy.stats.error_prop import variance_prop + >>> x, y = symbols('x y') + + >>> variance_prop(x + y) + Variance(x) + Variance(y) + + >>> variance_prop(x * y) + x**2*Variance(y) + y**2*Variance(x) + + >>> variance_prop(exp(2*x)) + 4*exp(4*x)*Variance(x) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Propagation_of_uncertainty + + """ + args = expr.args + if len(args) == 0: + if expr in consts: + return S.Zero + elif is_random(expr): + return Variance(expr).doit() + elif isinstance(expr, Symbol): + return Variance(RandomSymbol(expr)).doit() + else: + return S.Zero + nargs = len(args) + var_args = list(map(variance_prop, args, repeat(consts, nargs), + repeat(include_covar, nargs))) + if isinstance(expr, Add): + var_expr = Add(*var_args) + if include_covar: + terms = [2 * Covariance(_arg0_or_var(x), _arg0_or_var(y)).expand() \ + for x, y in combinations(var_args, 2)] + var_expr += Add(*terms) + elif isinstance(expr, Mul): + terms = [v/a**2 for a, v in zip(args, var_args)] + var_expr = simplify(expr**2 * Add(*terms)) + if include_covar: + terms = [2*Covariance(_arg0_or_var(x), _arg0_or_var(y)).expand()/(a*b) \ + for (a, b), (x, y) in zip(combinations(args, 2), + combinations(var_args, 2))] + var_expr += Add(*terms) + elif isinstance(expr, Pow): + b = args[1] + v = var_args[0] * (expr * b / args[0])**2 + var_expr = simplify(v) + elif isinstance(expr, exp): + var_expr = simplify(var_args[0] * expr**2) + else: + # unknown how to proceed, return variance of whole expr. + var_expr = Variance(expr) + return var_expr diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/frv.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/frv.py new file mode 100644 index 0000000000000000000000000000000000000000..498d7e4006b2b8db306a0905ed67578021e220a8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/frv.py @@ -0,0 +1,512 @@ +""" +Finite Discrete Random Variables Module + +See Also +======== +sympy.stats.frv_types +sympy.stats.rv +sympy.stats.crv +""" +from itertools import product + +from sympy.concrete.summations import Sum +from sympy.core.basic import Basic +from sympy.core.cache import cacheit +from sympy.core.function import Lambda +from sympy.core.mul import Mul +from sympy.core.numbers import (I, nan) +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol) +from sympy.core.sympify import sympify +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.piecewise import Piecewise +from sympy.logic.boolalg import (And, Or) +from sympy.sets.sets import Intersection +from sympy.core.containers import Dict +from sympy.core.logic import Logic +from sympy.core.relational import Relational +from sympy.core.sympify import _sympify +from sympy.sets.sets import FiniteSet +from sympy.stats.rv import (RandomDomain, ProductDomain, ConditionalDomain, + PSpace, IndependentProductPSpace, SinglePSpace, random_symbols, + sumsets, rv_subs, NamedArgsMixin, Density, Distribution) + + +class FiniteDensity(dict): + """ + A domain with Finite Density. + """ + def __call__(self, item): + """ + Make instance of a class callable. + + If item belongs to current instance of a class, return it. + + Otherwise, return 0. + """ + item = sympify(item) + if item in self: + return self[item] + else: + return 0 + + @property + def dict(self): + """ + Return item as dictionary. + """ + return dict(self) + +class FiniteDomain(RandomDomain): + """ + A domain with discrete finite support + + Represented using a FiniteSet. + """ + is_Finite = True + + @property + def symbols(self): + return FiniteSet(sym for sym, val in self.elements) + + @property + def elements(self): + return self.args[0] + + @property + def dict(self): + return FiniteSet(*[Dict(dict(el)) for el in self.elements]) + + def __contains__(self, other): + return other in self.elements + + def __iter__(self): + return self.elements.__iter__() + + def as_boolean(self): + return Or(*[And(*[Eq(sym, val) for sym, val in item]) for item in self]) + + +class SingleFiniteDomain(FiniteDomain): + """ + A FiniteDomain over a single symbol/set + + Example: The possibilities of a *single* die roll. + """ + + def __new__(cls, symbol, set): + if not isinstance(set, FiniteSet) and \ + not isinstance(set, Intersection): + set = FiniteSet(*set) + return Basic.__new__(cls, symbol, set) + + @property + def symbol(self): + return self.args[0] + + @property + def symbols(self): + return FiniteSet(self.symbol) + + @property + def set(self): + return self.args[1] + + @property + def elements(self): + return FiniteSet(*[frozenset(((self.symbol, elem), )) for elem in self.set]) + + def __iter__(self): + return (frozenset(((self.symbol, elem),)) for elem in self.set) + + def __contains__(self, other): + sym, val = tuple(other)[0] + return sym == self.symbol and val in self.set + + +class ProductFiniteDomain(ProductDomain, FiniteDomain): + """ + A Finite domain consisting of several other FiniteDomains + + Example: The possibilities of the rolls of three independent dice + """ + + def __iter__(self): + proditer = product(*self.domains) + return (sumsets(items) for items in proditer) + + @property + def elements(self): + return FiniteSet(*self) + + +class ConditionalFiniteDomain(ConditionalDomain, ProductFiniteDomain): + """ + A FiniteDomain that has been restricted by a condition + + Example: The possibilities of a die roll under the condition that the + roll is even. + """ + + def __new__(cls, domain, condition): + """ + Create a new instance of ConditionalFiniteDomain class + """ + if condition is True: + return domain + cond = rv_subs(condition) + return Basic.__new__(cls, domain, cond) + + def _test(self, elem): + """ + Test the value. If value is boolean, return it. If value is equality + relational (two objects are equal), return it with left-hand side + being equal to right-hand side. Otherwise, raise ValueError exception. + """ + val = self.condition.xreplace(dict(elem)) + if val in [True, False]: + return val + elif val.is_Equality: + return val.lhs == val.rhs + raise ValueError("Undecidable if %s" % str(val)) + + def __contains__(self, other): + return other in self.fulldomain and self._test(other) + + def __iter__(self): + return (elem for elem in self.fulldomain if self._test(elem)) + + @property + def set(self): + if isinstance(self.fulldomain, SingleFiniteDomain): + return FiniteSet(*[elem for elem in self.fulldomain.set + if frozenset(((self.fulldomain.symbol, elem),)) in self]) + else: + raise NotImplementedError( + "Not implemented on multi-dimensional conditional domain") + + def as_boolean(self): + return FiniteDomain.as_boolean(self) + + +class SingleFiniteDistribution(Distribution, NamedArgsMixin): + def __new__(cls, *args): + args = list(map(sympify, args)) + return Basic.__new__(cls, *args) + + @staticmethod + def check(*args): + pass + + @property # type: ignore + @cacheit + def dict(self): + if self.is_symbolic: + return Density(self) + return {k: self.pmf(k) for k in self.set} + + def pmf(self, *args): # to be overridden by specific distribution + raise NotImplementedError() + + @property + def set(self): # to be overridden by specific distribution + raise NotImplementedError() + + values = property(lambda self: self.dict.values) + items = property(lambda self: self.dict.items) + is_symbolic = property(lambda self: False) + __iter__ = property(lambda self: self.dict.__iter__) + __getitem__ = property(lambda self: self.dict.__getitem__) + + def __call__(self, *args): + return self.pmf(*args) + + def __contains__(self, other): + return other in self.set + + +#============================================= +#========= Probability Space =============== +#============================================= + + +class FinitePSpace(PSpace): + """ + A Finite Probability Space + + Represents the probabilities of a finite number of events. + """ + is_Finite = True + + def __new__(cls, domain, density): + density = {sympify(key): sympify(val) + for key, val in density.items()} + public_density = Dict(density) + + obj = PSpace.__new__(cls, domain, public_density) + obj._density = density + return obj + + def prob_of(self, elem): + elem = sympify(elem) + density = self._density + if isinstance(list(density.keys())[0], FiniteSet): + return density.get(elem, S.Zero) + return density.get(tuple(elem)[0][1], S.Zero) + + def where(self, condition): + assert all(r.symbol in self.symbols for r in random_symbols(condition)) + return ConditionalFiniteDomain(self.domain, condition) + + def compute_density(self, expr): + expr = rv_subs(expr, self.values) + d = FiniteDensity() + for elem in self.domain: + val = expr.xreplace(dict(elem)) + prob = self.prob_of(elem) + d[val] = d.get(val, S.Zero) + prob + return d + + @cacheit + def compute_cdf(self, expr): + d = self.compute_density(expr) + cum_prob = S.Zero + cdf = [] + for key in sorted(d): + prob = d[key] + cum_prob += prob + cdf.append((key, cum_prob)) + + return dict(cdf) + + @cacheit + def sorted_cdf(self, expr, python_float=False): + cdf = self.compute_cdf(expr) + items = list(cdf.items()) + sorted_items = sorted(items, key=lambda val_cumprob: val_cumprob[1]) + if python_float: + sorted_items = [(v, float(cum_prob)) + for v, cum_prob in sorted_items] + return sorted_items + + @cacheit + def compute_characteristic_function(self, expr): + d = self.compute_density(expr) + t = Dummy('t', real=True) + + return Lambda(t, sum(exp(I*k*t)*v for k,v in d.items())) + + @cacheit + def compute_moment_generating_function(self, expr): + d = self.compute_density(expr) + t = Dummy('t', real=True) + + return Lambda(t, sum(exp(k*t)*v for k,v in d.items())) + + def compute_expectation(self, expr, rvs=None, **kwargs): + rvs = rvs or self.values + expr = rv_subs(expr, rvs) + probs = [self.prob_of(elem) for elem in self.domain] + if isinstance(expr, (Logic, Relational)): + parse_domain = [tuple(elem)[0][1] for elem in self.domain] + bools = [expr.xreplace(dict(elem)) for elem in self.domain] + else: + parse_domain = [expr.xreplace(dict(elem)) for elem in self.domain] + bools = [True for elem in self.domain] + return sum(Piecewise((prob * elem, blv), (S.Zero, True)) + for prob, elem, blv in zip(probs, parse_domain, bools)) + + def compute_quantile(self, expr): + cdf = self.compute_cdf(expr) + p = Dummy('p', real=True) + set = ((nan, (p < 0) | (p > 1)),) + for key, value in cdf.items(): + set = set + ((key, p <= value), ) + return Lambda(p, Piecewise(*set)) + + def probability(self, condition): + cond_symbols = frozenset(rs.symbol for rs in random_symbols(condition)) + cond = rv_subs(condition) + if not cond_symbols.issubset(self.symbols): + raise ValueError("Cannot compare foreign random symbols, %s" + %(str(cond_symbols - self.symbols))) + if isinstance(condition, Relational) and \ + (not cond.free_symbols.issubset(self.domain.free_symbols)): + rv = condition.lhs if isinstance(condition.rhs, Symbol) else condition.rhs + return sum(Piecewise( + (self.prob_of(elem), condition.subs(rv, list(elem)[0][1])), + (S.Zero, True)) for elem in self.domain) + return sympify(sum(self.prob_of(elem) for elem in self.where(condition))) + + def conditional_space(self, condition): + domain = self.where(condition) + prob = self.probability(condition) + density = {key: val / prob + for key, val in self._density.items() if domain._test(key)} + return FinitePSpace(domain, density) + + def sample(self, size=(), library='scipy', seed=None): + """ + Internal sample method + + Returns dictionary mapping RandomSymbol to realization value. + """ + return {self.value: self.distribution.sample(size, library, seed)} + + +class SingleFinitePSpace(SinglePSpace, FinitePSpace): + """ + A single finite probability space + + Represents the probabilities of a set of random events that can be + attributed to a single variable/symbol. + + This class is implemented by many of the standard FiniteRV types such as + Die, Bernoulli, Coin, etc.... + """ + @property + def domain(self): + return SingleFiniteDomain(self.symbol, self.distribution.set) + + @property + def _is_symbolic(self): + """ + Helper property to check if the distribution + of the random variable is having symbolic + dimension. + """ + return self.distribution.is_symbolic + + @property + def distribution(self): + return self.args[1] + + def pmf(self, expr): + return self.distribution.pmf(expr) + + @property # type: ignore + @cacheit + def _density(self): + return {FiniteSet((self.symbol, val)): prob + for val, prob in self.distribution.dict.items()} + + @cacheit + def compute_characteristic_function(self, expr): + if self._is_symbolic: + d = self.compute_density(expr) + t = Dummy('t', real=True) + ki = Dummy('ki') + return Lambda(t, Sum(d(ki)*exp(I*ki*t), (ki, self.args[1].low, self.args[1].high))) + expr = rv_subs(expr, self.values) + return FinitePSpace(self.domain, self.distribution).compute_characteristic_function(expr) + + @cacheit + def compute_moment_generating_function(self, expr): + if self._is_symbolic: + d = self.compute_density(expr) + t = Dummy('t', real=True) + ki = Dummy('ki') + return Lambda(t, Sum(d(ki)*exp(ki*t), (ki, self.args[1].low, self.args[1].high))) + expr = rv_subs(expr, self.values) + return FinitePSpace(self.domain, self.distribution).compute_moment_generating_function(expr) + + def compute_quantile(self, expr): + if self._is_symbolic: + raise NotImplementedError("Computing quantile for random variables " + "with symbolic dimension because the bounds of searching the required " + "value is undetermined.") + expr = rv_subs(expr, self.values) + return FinitePSpace(self.domain, self.distribution).compute_quantile(expr) + + def compute_density(self, expr): + if self._is_symbolic: + rv = list(random_symbols(expr))[0] + k = Dummy('k', integer=True) + cond = True if not isinstance(expr, (Relational, Logic)) \ + else expr.subs(rv, k) + return Lambda(k, + Piecewise((self.pmf(k), And(k >= self.args[1].low, + k <= self.args[1].high, cond)), (S.Zero, True))) + expr = rv_subs(expr, self.values) + return FinitePSpace(self.domain, self.distribution).compute_density(expr) + + def compute_cdf(self, expr): + if self._is_symbolic: + d = self.compute_density(expr) + k = Dummy('k') + ki = Dummy('ki') + return Lambda(k, Sum(d(ki), (ki, self.args[1].low, k))) + expr = rv_subs(expr, self.values) + return FinitePSpace(self.domain, self.distribution).compute_cdf(expr) + + def compute_expectation(self, expr, rvs=None, **kwargs): + if self._is_symbolic: + rv = random_symbols(expr)[0] + k = Dummy('k', integer=True) + expr = expr.subs(rv, k) + cond = True if not isinstance(expr, (Relational, Logic)) \ + else expr + func = self.pmf(k) * k if cond != True else self.pmf(k) * expr + return Sum(Piecewise((func, cond), (S.Zero, True)), + (k, self.distribution.low, self.distribution.high)).doit() + + expr = _sympify(expr) + expr = rv_subs(expr, rvs) + return FinitePSpace(self.domain, self.distribution).compute_expectation(expr, rvs, **kwargs) + + def probability(self, condition): + if self._is_symbolic: + #TODO: Implement the mechanism for handling queries for symbolic sized distributions. + raise NotImplementedError("Currently, probability queries are not " + "supported for random variables with symbolic sized distributions.") + condition = rv_subs(condition) + return FinitePSpace(self.domain, self.distribution).probability(condition) + + def conditional_space(self, condition): + """ + This method is used for transferring the + computation to probability method because + conditional space of random variables with + symbolic dimensions is currently not possible. + """ + if self._is_symbolic: + self + domain = self.where(condition) + prob = self.probability(condition) + density = {key: val / prob + for key, val in self._density.items() if domain._test(key)} + return FinitePSpace(domain, density) + + +class ProductFinitePSpace(IndependentProductPSpace, FinitePSpace): + """ + A collection of several independent finite probability spaces + """ + @property + def domain(self): + return ProductFiniteDomain(*[space.domain for space in self.spaces]) + + @property # type: ignore + @cacheit + def _density(self): + proditer = product(*[iter(space._density.items()) + for space in self.spaces]) + d = {} + for items in proditer: + elems, probs = list(zip(*items)) + elem = sumsets(elems) + prob = Mul(*probs) + d[elem] = d.get(elem, S.Zero) + prob + return Dict(d) + + @property # type: ignore + @cacheit + def density(self): + return Dict(self._density) + + def probability(self, condition): + return FinitePSpace.probability(self, condition) + + def compute_density(self, expr): + return FinitePSpace.compute_density(self, expr) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/frv_types.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/frv_types.py new file mode 100644 index 0000000000000000000000000000000000000000..bde656c219791c287ff445d5d215e3759271e923 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/frv_types.py @@ -0,0 +1,873 @@ +""" +Finite Discrete Random Variables - Prebuilt variable types + +Contains +======== +FiniteRV +DiscreteUniform +Die +Bernoulli +Coin +Binomial +BetaBinomial +Hypergeometric +Rademacher +IdealSoliton +RobustSoliton +""" + + +from sympy.core.cache import cacheit +from sympy.core.function import Lambda +from sympy.core.numbers import (Integer, Rational) +from sympy.core.relational import (Eq, Ge, Gt, Le, Lt) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol) +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import binomial +from sympy.functions.elementary.exponential import log +from sympy.functions.elementary.piecewise import Piecewise +from sympy.logic.boolalg import Or +from sympy.sets.contains import Contains +from sympy.sets.fancysets import Range +from sympy.sets.sets import (Intersection, Interval) +from sympy.functions.special.beta_functions import beta as beta_fn +from sympy.stats.frv import (SingleFiniteDistribution, + SingleFinitePSpace) +from sympy.stats.rv import _value_check, Density, is_random +from sympy.utilities.iterables import multiset +from sympy.utilities.misc import filldedent + + +__all__ = ['FiniteRV', +'DiscreteUniform', +'Die', +'Bernoulli', +'Coin', +'Binomial', +'BetaBinomial', +'Hypergeometric', +'Rademacher', +'IdealSoliton', +'RobustSoliton', +] + +def rv(name, cls, *args, **kwargs): + args = list(map(sympify, args)) + dist = cls(*args) + if kwargs.pop('check', True): + dist.check(*args) + pspace = SingleFinitePSpace(name, dist) + if any(is_random(arg) for arg in args): + from sympy.stats.compound_rv import CompoundPSpace, CompoundDistribution + pspace = CompoundPSpace(name, CompoundDistribution(dist)) + return pspace.value + +class FiniteDistributionHandmade(SingleFiniteDistribution): + + @property + def dict(self): + return self.args[0] + + def pmf(self, x): + x = Symbol('x') + return Lambda(x, Piecewise(*( + [(v, Eq(k, x)) for k, v in self.dict.items()] + [(S.Zero, True)]))) + + @property + def set(self): + return set(self.dict.keys()) + + @staticmethod + def check(density): + for p in density.values(): + _value_check((p >= 0, p <= 1), + "Probability at a point must be between 0 and 1.") + val = sum(density.values()) + _value_check(Eq(val, 1) != S.false, "Total Probability must be 1.") + +def FiniteRV(name, density, **kwargs): + r""" + Create a Finite Random Variable given a dict representing the density. + + Parameters + ========== + + name : Symbol + Represents name of the random variable. + density : dict + Dictionary containing the pdf of finite distribution + check : bool + If True, it will check whether the given density + integrates to 1 over the given set. If False, it + will not perform this check. Default is False. + + Examples + ======== + + >>> from sympy.stats import FiniteRV, P, E + + >>> density = {0: .1, 1: .2, 2: .3, 3: .4} + >>> X = FiniteRV('X', density) + + >>> E(X) + 2.00000000000000 + >>> P(X >= 2) + 0.700000000000000 + + Returns + ======= + + RandomSymbol + + """ + # have a default of False while `rv` should have a default of True + kwargs['check'] = kwargs.pop('check', False) + return rv(name, FiniteDistributionHandmade, density, **kwargs) + +class DiscreteUniformDistribution(SingleFiniteDistribution): + + @staticmethod + def check(*args): + # not using _value_check since there is a + # suggestion for the user + if len(set(args)) != len(args): + weights = multiset(args) + n = Integer(len(args)) + for k in weights: + weights[k] /= n + raise ValueError(filldedent(""" + Repeated args detected but set expected. For a + distribution having different weights for each + item use the following:""") + ( + '\nS("FiniteRV(%s, %s)")' % ("'X'", weights))) + + @property + def p(self): + return Rational(1, len(self.args)) + + @property # type: ignore + @cacheit + def dict(self): + return dict.fromkeys(self.set, self.p) + + @property + def set(self): + return set(self.args) + + def pmf(self, x): + if x in self.args: + return self.p + else: + return S.Zero + + +def DiscreteUniform(name, items): + r""" + Create a Finite Random Variable representing a uniform distribution over + the input set. + + Parameters + ========== + + items : list/tuple + Items over which Uniform distribution is to be made + + Examples + ======== + + >>> from sympy.stats import DiscreteUniform, density + >>> from sympy import symbols + + >>> X = DiscreteUniform('X', symbols('a b c')) # equally likely over a, b, c + >>> density(X).dict + {a: 1/3, b: 1/3, c: 1/3} + + >>> Y = DiscreteUniform('Y', list(range(5))) # distribution over a range + >>> density(Y).dict + {0: 1/5, 1: 1/5, 2: 1/5, 3: 1/5, 4: 1/5} + + Returns + ======= + + RandomSymbol + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Discrete_uniform_distribution + .. [2] https://mathworld.wolfram.com/DiscreteUniformDistribution.html + + """ + return rv(name, DiscreteUniformDistribution, *items) + + +class DieDistribution(SingleFiniteDistribution): + _argnames = ('sides',) + + @staticmethod + def check(sides): + _value_check((sides.is_positive, sides.is_integer), + "number of sides must be a positive integer.") + + @property + def is_symbolic(self): + return not self.sides.is_number + + @property + def high(self): + return self.sides + + @property + def low(self): + return S.One + + @property + def set(self): + if self.is_symbolic: + return Intersection(S.Naturals0, Interval(0, self.sides)) + return set(map(Integer, range(1, self.sides + 1))) + + def pmf(self, x): + x = sympify(x) + if not (x.is_number or x.is_Symbol or is_random(x)): + raise ValueError("'x' expected as an argument of type 'number', 'Symbol', or " + "'RandomSymbol' not %s" % (type(x))) + cond = Ge(x, 1) & Le(x, self.sides) & Contains(x, S.Integers) + return Piecewise((S.One/self.sides, cond), (S.Zero, True)) + +def Die(name, sides=6): + r""" + Create a Finite Random Variable representing a fair die. + + Parameters + ========== + + sides : Integer + Represents the number of sides of the Die, by default is 6 + + Examples + ======== + + >>> from sympy.stats import Die, density + >>> from sympy import Symbol + + >>> D6 = Die('D6', 6) # Six sided Die + >>> density(D6).dict + {1: 1/6, 2: 1/6, 3: 1/6, 4: 1/6, 5: 1/6, 6: 1/6} + + >>> D4 = Die('D4', 4) # Four sided Die + >>> density(D4).dict + {1: 1/4, 2: 1/4, 3: 1/4, 4: 1/4} + + >>> n = Symbol('n', positive=True, integer=True) + >>> Dn = Die('Dn', n) # n sided Die + >>> density(Dn).dict + Density(DieDistribution(n)) + >>> density(Dn).dict.subs(n, 4).doit() + {1: 1/4, 2: 1/4, 3: 1/4, 4: 1/4} + + Returns + ======= + + RandomSymbol + """ + + return rv(name, DieDistribution, sides) + + +class BernoulliDistribution(SingleFiniteDistribution): + _argnames = ('p', 'succ', 'fail') + + @staticmethod + def check(p, succ, fail): + _value_check((p >= 0, p <= 1), + "p should be in range [0, 1].") + + @property + def set(self): + return {self.succ, self.fail} + + def pmf(self, x): + if isinstance(self.succ, Symbol) and isinstance(self.fail, Symbol): + return Piecewise((self.p, x == self.succ), + (1 - self.p, x == self.fail), + (S.Zero, True)) + return Piecewise((self.p, Eq(x, self.succ)), + (1 - self.p, Eq(x, self.fail)), + (S.Zero, True)) + + +def Bernoulli(name, p, succ=1, fail=0): + r""" + Create a Finite Random Variable representing a Bernoulli process. + + Parameters + ========== + + p : Rational number between 0 and 1 + Represents probability of success + succ : Integer/symbol/string + Represents event of success + fail : Integer/symbol/string + Represents event of failure + + Examples + ======== + + >>> from sympy.stats import Bernoulli, density + >>> from sympy import S + + >>> X = Bernoulli('X', S(3)/4) # 1-0 Bernoulli variable, probability = 3/4 + >>> density(X).dict + {0: 1/4, 1: 3/4} + + >>> X = Bernoulli('X', S.Half, 'Heads', 'Tails') # A fair coin toss + >>> density(X).dict + {Heads: 1/2, Tails: 1/2} + + Returns + ======= + + RandomSymbol + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Bernoulli_distribution + .. [2] https://mathworld.wolfram.com/BernoulliDistribution.html + + """ + + return rv(name, BernoulliDistribution, p, succ, fail) + + +def Coin(name, p=S.Half): + r""" + Create a Finite Random Variable representing a Coin toss. + + This is an equivalent of a Bernoulli random variable with + "H" and "T" as success and failure events respectively. + + Parameters + ========== + + p : Rational Number between 0 and 1 + Represents probability of getting "Heads", by default is Half + + Examples + ======== + + >>> from sympy.stats import Coin, density + >>> from sympy import Rational + + >>> C = Coin('C') # A fair coin toss + >>> density(C).dict + {H: 1/2, T: 1/2} + + >>> C2 = Coin('C2', Rational(3, 5)) # An unfair coin + >>> density(C2).dict + {H: 3/5, T: 2/5} + + Returns + ======= + + RandomSymbol + + See Also + ======== + + sympy.stats.Binomial + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Coin_flipping + + """ + return rv(name, BernoulliDistribution, p, 'H', 'T') + + +class BinomialDistribution(SingleFiniteDistribution): + _argnames = ('n', 'p', 'succ', 'fail') + + @staticmethod + def check(n, p, succ, fail): + _value_check((n.is_integer, n.is_nonnegative), + "'n' must be nonnegative integer.") + _value_check((p <= 1, p >= 0), + "p should be in range [0, 1].") + + @property + def high(self): + return self.n + + @property + def low(self): + return S.Zero + + @property + def is_symbolic(self): + return not self.n.is_number + + @property + def set(self): + if self.is_symbolic: + return Intersection(S.Naturals0, Interval(0, self.n)) + return set(self.dict.keys()) + + def pmf(self, x): + n, p = self.n, self.p + x = sympify(x) + if not (x.is_number or x.is_Symbol or is_random(x)): + raise ValueError("'x' expected as an argument of type 'number', 'Symbol', or " + "'RandomSymbol' not %s" % (type(x))) + cond = Ge(x, 0) & Le(x, n) & Contains(x, S.Integers) + return Piecewise((binomial(n, x) * p**x * (1 - p)**(n - x), cond), (S.Zero, True)) + + @property # type: ignore + @cacheit + def dict(self): + if self.is_symbolic: + return Density(self) + return {k*self.succ + (self.n-k)*self.fail: self.pmf(k) + for k in range(0, self.n + 1)} + + +def Binomial(name, n, p, succ=1, fail=0): + r""" + Create a Finite Random Variable representing a binomial distribution. + + Parameters + ========== + + n : Positive Integer + Represents number of trials + p : Rational Number between 0 and 1 + Represents probability of success + succ : Integer/symbol/string + Represents event of success, by default is 1 + fail : Integer/symbol/string + Represents event of failure, by default is 0 + + Examples + ======== + + >>> from sympy.stats import Binomial, density + >>> from sympy import S, Symbol + + >>> X = Binomial('X', 4, S.Half) # Four "coin flips" + >>> density(X).dict + {0: 1/16, 1: 1/4, 2: 3/8, 3: 1/4, 4: 1/16} + + >>> n = Symbol('n', positive=True, integer=True) + >>> p = Symbol('p', positive=True) + >>> X = Binomial('X', n, S.Half) # n "coin flips" + >>> density(X).dict + Density(BinomialDistribution(n, 1/2, 1, 0)) + >>> density(X).dict.subs(n, 4).doit() + {0: 1/16, 1: 1/4, 2: 3/8, 3: 1/4, 4: 1/16} + + Returns + ======= + + RandomSymbol + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Binomial_distribution + .. [2] https://mathworld.wolfram.com/BinomialDistribution.html + + """ + + return rv(name, BinomialDistribution, n, p, succ, fail) + +#------------------------------------------------------------------------------- +# Beta-binomial distribution ---------------------------------------------------------- + +class BetaBinomialDistribution(SingleFiniteDistribution): + _argnames = ('n', 'alpha', 'beta') + + @staticmethod + def check(n, alpha, beta): + _value_check((n.is_integer, n.is_nonnegative), + "'n' must be nonnegative integer. n = %s." % str(n)) + _value_check((alpha > 0), + "'alpha' must be: alpha > 0 . alpha = %s" % str(alpha)) + _value_check((beta > 0), + "'beta' must be: beta > 0 . beta = %s" % str(beta)) + + @property + def high(self): + return self.n + + @property + def low(self): + return S.Zero + + @property + def is_symbolic(self): + return not self.n.is_number + + @property + def set(self): + if self.is_symbolic: + return Intersection(S.Naturals0, Interval(0, self.n)) + return set(map(Integer, range(self.n + 1))) + + def pmf(self, k): + n, a, b = self.n, self.alpha, self.beta + return binomial(n, k) * beta_fn(k + a, n - k + b) / beta_fn(a, b) + + +def BetaBinomial(name, n, alpha, beta): + r""" + Create a Finite Random Variable representing a Beta-binomial distribution. + + Parameters + ========== + + n : Positive Integer + Represents number of trials + alpha : Real positive number + beta : Real positive number + + Examples + ======== + + >>> from sympy.stats import BetaBinomial, density + + >>> X = BetaBinomial('X', 2, 1, 1) + >>> density(X).dict + {0: 1/3, 1: 2*beta(2, 2), 2: 1/3} + + Returns + ======= + + RandomSymbol + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Beta-binomial_distribution + .. [2] https://mathworld.wolfram.com/BetaBinomialDistribution.html + + """ + + return rv(name, BetaBinomialDistribution, n, alpha, beta) + + +class HypergeometricDistribution(SingleFiniteDistribution): + _argnames = ('N', 'm', 'n') + + @staticmethod + def check(n, N, m): + _value_check((N.is_integer, N.is_nonnegative), + "'N' must be nonnegative integer. N = %s." % str(N)) + _value_check((n.is_integer, n.is_nonnegative), + "'n' must be nonnegative integer. n = %s." % str(n)) + _value_check((m.is_integer, m.is_nonnegative), + "'m' must be nonnegative integer. m = %s." % str(m)) + + @property + def is_symbolic(self): + return not all(x.is_number for x in (self.N, self.m, self.n)) + + @property + def high(self): + return Piecewise((self.n, Lt(self.n, self.m) != False), (self.m, True)) + + @property + def low(self): + return Piecewise((0, Gt(0, self.n + self.m - self.N) != False), (self.n + self.m - self.N, True)) + + @property + def set(self): + N, m, n = self.N, self.m, self.n + if self.is_symbolic: + return Intersection(S.Naturals0, Interval(self.low, self.high)) + return set(range(max(0, n + m - N), min(n, m) + 1)) + + def pmf(self, k): + N, m, n = self.N, self.m, self.n + return S(binomial(m, k) * binomial(N - m, n - k))/binomial(N, n) + + +def Hypergeometric(name, N, m, n): + r""" + Create a Finite Random Variable representing a hypergeometric distribution. + + Parameters + ========== + + N : Positive Integer + Represents finite population of size N. + m : Positive Integer + Represents number of trials with required feature. + n : Positive Integer + Represents numbers of draws. + + + Examples + ======== + + >>> from sympy.stats import Hypergeometric, density + + >>> X = Hypergeometric('X', 10, 5, 3) # 10 marbles, 5 white (success), 3 draws + >>> density(X).dict + {0: 1/12, 1: 5/12, 2: 5/12, 3: 1/12} + + Returns + ======= + + RandomSymbol + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Hypergeometric_distribution + .. [2] https://mathworld.wolfram.com/HypergeometricDistribution.html + + """ + return rv(name, HypergeometricDistribution, N, m, n) + + +class RademacherDistribution(SingleFiniteDistribution): + + @property + def set(self): + return {-1, 1} + + @property + def pmf(self): + k = Dummy('k') + return Lambda(k, Piecewise((S.Half, Or(Eq(k, -1), Eq(k, 1))), (S.Zero, True))) + +def Rademacher(name): + r""" + Create a Finite Random Variable representing a Rademacher distribution. + + Examples + ======== + + >>> from sympy.stats import Rademacher, density + + >>> X = Rademacher('X') + >>> density(X).dict + {-1: 1/2, 1: 1/2} + + Returns + ======= + + RandomSymbol + + See Also + ======== + + sympy.stats.Bernoulli + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Rademacher_distribution + + """ + return rv(name, RademacherDistribution) + +class IdealSolitonDistribution(SingleFiniteDistribution): + _argnames = ('k',) + + @staticmethod + def check(k): + _value_check(k.is_integer and k.is_positive, + "'k' must be a positive integer.") + + @property + def low(self): + return S.One + + @property + def high(self): + return self.k + + @property + def set(self): + return set(map(Integer, range(1, self.k + 1))) + + @property # type: ignore + @cacheit + def dict(self): + if self.k.is_Symbol: + return Density(self) + d = {1: Rational(1, self.k)} + d.update({i: Rational(1, i*(i - 1)) for i in range(2, self.k + 1)}) + return d + + def pmf(self, x): + x = sympify(x) + if not (x.is_number or x.is_Symbol or is_random(x)): + raise ValueError("'x' expected as an argument of type 'number', 'Symbol', or " + "'RandomSymbol' not %s" % (type(x))) + cond1 = Eq(x, 1) & x.is_integer + cond2 = Ge(x, 1) & Le(x, self.k) & x.is_integer + return Piecewise((1/self.k, cond1), (1/(x*(x - 1)), cond2), (S.Zero, True)) + +def IdealSoliton(name, k): + r""" + Create a Finite Random Variable of Ideal Soliton Distribution + + Parameters + ========== + + k : Positive Integer + Represents the number of input symbols in an LT (Luby Transform) code. + + Examples + ======== + + >>> from sympy.stats import IdealSoliton, density, P, E + >>> sol = IdealSoliton('sol', 5) + >>> density(sol).dict + {1: 1/5, 2: 1/2, 3: 1/6, 4: 1/12, 5: 1/20} + >>> density(sol).set + {1, 2, 3, 4, 5} + + >>> from sympy import Symbol + >>> k = Symbol('k', positive=True, integer=True) + >>> sol = IdealSoliton('sol', k) + >>> density(sol).dict + Density(IdealSolitonDistribution(k)) + >>> density(sol).dict.subs(k, 10).doit() + {1: 1/10, 2: 1/2, 3: 1/6, 4: 1/12, 5: 1/20, 6: 1/30, 7: 1/42, 8: 1/56, 9: 1/72, 10: 1/90} + + >>> E(sol.subs(k, 10)) + 7381/2520 + + >>> P(sol.subs(k, 4) > 2) + 1/4 + + Returns + ======= + + RandomSymbol + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Soliton_distribution#Ideal_distribution + .. [2] https://pages.cs.wisc.edu/~suman/courses/740/papers/luby02lt.pdf + + """ + return rv(name, IdealSolitonDistribution, k) + +class RobustSolitonDistribution(SingleFiniteDistribution): + _argnames= ('k', 'delta', 'c') + + @staticmethod + def check(k, delta, c): + _value_check(k.is_integer and k.is_positive, + "'k' must be a positive integer") + _value_check(Gt(delta, 0) and Le(delta, 1), + "'delta' must be a real number in the interval (0,1)") + _value_check(c.is_positive, + "'c' must be a positive real number.") + + @property + def R(self): + return self.c * log(self.k/self.delta) * self.k**0.5 + + @property + def Z(self): + z = 0 + for i in Range(1, round(self.k/self.R)): + z += (1/i) + z += log(self.R/self.delta) + return 1 + z * self.R/self.k + + @property + def low(self): + return S.One + + @property + def high(self): + return self.k + + @property + def set(self): + return set(map(Integer, range(1, self.k + 1))) + + @property + def is_symbolic(self): + return not (self.k.is_number and self.c.is_number and self.delta.is_number) + + def pmf(self, x): + x = sympify(x) + if not (x.is_number or x.is_Symbol or is_random(x)): + raise ValueError("'x' expected as an argument of type 'number', 'Symbol', or " + "'RandomSymbol' not %s" % (type(x))) + + cond1 = Eq(x, 1) & x.is_integer + cond2 = Ge(x, 1) & Le(x, self.k) & x.is_integer + rho = Piecewise((Rational(1, self.k), cond1), (Rational(1, x*(x-1)), cond2), (S.Zero, True)) + + cond1 = Ge(x, 1) & Le(x, round(self.k/self.R)-1) + cond2 = Eq(x, round(self.k/self.R)) + tau = Piecewise((self.R/(self.k * x), cond1), (self.R * log(self.R/self.delta)/self.k, cond2), (S.Zero, True)) + + return (rho + tau)/self.Z + +def RobustSoliton(name, k, delta, c): + r''' + Create a Finite Random Variable of Robust Soliton Distribution + + Parameters + ========== + + k : Positive Integer + Represents the number of input symbols in an LT (Luby Transform) code. + delta : Positive Rational Number + Represents the failure probability. Must be in the interval (0,1). + c : Positive Rational Number + Constant of proportionality. Values close to 1 are recommended + + Examples + ======== + + >>> from sympy.stats import RobustSoliton, density, P, E + >>> robSol = RobustSoliton('robSol', 5, 0.5, 0.01) + >>> density(robSol).dict + {1: 0.204253668152708, 2: 0.490631107897393, 3: 0.165210624506162, 4: 0.0834387731899302, 5: 0.0505633404760675} + >>> density(robSol).set + {1, 2, 3, 4, 5} + + >>> from sympy import Symbol + >>> k = Symbol('k', positive=True, integer=True) + >>> c = Symbol('c', positive=True) + >>> robSol = RobustSoliton('robSol', k, 0.5, c) + >>> density(robSol).dict + Density(RobustSolitonDistribution(k, 0.5, c)) + >>> density(robSol).dict.subs(k, 10).subs(c, 0.03).doit() + {1: 0.116641095387194, 2: 0.467045731687165, 3: 0.159984123349381, 4: 0.0821431680681869, 5: 0.0505765646770100, + 6: 0.0345781523420719, 7: 0.0253132820710503, 8: 0.0194459129233227, 9: 0.0154831166726115, 10: 0.0126733075238887} + + >>> E(robSol.subs(k, 10).subs(c, 0.05)) + 2.91358846104106 + + >>> P(robSol.subs(k, 4).subs(c, 0.1) > 2) + 0.243650614389834 + + Returns + ======= + + RandomSymbol + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Soliton_distribution#Robust_distribution + .. [2] https://www.inference.org.uk/mackay/itprnn/ps/588.596.pdf + .. [3] https://pages.cs.wisc.edu/~suman/courses/740/papers/luby02lt.pdf + + ''' + return rv(name, RobustSolitonDistribution, k, delta, c) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/joint_rv.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/joint_rv.py new file mode 100644 index 0000000000000000000000000000000000000000..d147942f08b998e167b246628360fa27fc8ef348 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/joint_rv.py @@ -0,0 +1,426 @@ +""" +Joint Random Variables Module + +See Also +======== +sympy.stats.rv +sympy.stats.frv +sympy.stats.crv +sympy.stats.drv +""" +from math import prod + +from sympy.core.basic import Basic +from sympy.core.function import Lambda +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol) +from sympy.core.sympify import sympify +from sympy.sets.sets import ProductSet +from sympy.tensor.indexed import Indexed +from sympy.concrete.products import Product +from sympy.concrete.summations import Sum, summation +from sympy.core.containers import Tuple +from sympy.integrals.integrals import Integral, integrate +from sympy.matrices import ImmutableMatrix, matrix2numpy, list2numpy +from sympy.stats.crv import SingleContinuousDistribution, SingleContinuousPSpace +from sympy.stats.drv import SingleDiscreteDistribution, SingleDiscretePSpace +from sympy.stats.rv import (ProductPSpace, NamedArgsMixin, Distribution, + ProductDomain, RandomSymbol, random_symbols, + SingleDomain, _symbol_converter) +from sympy.utilities.iterables import iterable +from sympy.utilities.misc import filldedent +from sympy.external import import_module + +# __all__ = ['marginal_distribution'] + +class JointPSpace(ProductPSpace): + """ + Represents a joint probability space. Represented using symbols for + each component and a distribution. + """ + def __new__(cls, sym, dist): + if isinstance(dist, SingleContinuousDistribution): + return SingleContinuousPSpace(sym, dist) + if isinstance(dist, SingleDiscreteDistribution): + return SingleDiscretePSpace(sym, dist) + sym = _symbol_converter(sym) + return Basic.__new__(cls, sym, dist) + + @property + def set(self): + return self.domain.set + + @property + def symbol(self): + return self.args[0] + + @property + def distribution(self): + return self.args[1] + + @property + def value(self): + return JointRandomSymbol(self.symbol, self) + + @property + def component_count(self): + _set = self.distribution.set + if isinstance(_set, ProductSet): + return S(len(_set.args)) + elif isinstance(_set, Product): + return _set.limits[0][-1] + return S.One + + @property + def pdf(self): + sym = [Indexed(self.symbol, i) for i in range(self.component_count)] + return self.distribution(*sym) + + @property + def domain(self): + rvs = random_symbols(self.distribution) + if not rvs: + return SingleDomain(self.symbol, self.distribution.set) + return ProductDomain(*[rv.pspace.domain for rv in rvs]) + + def component_domain(self, index): + return self.set.args[index] + + def marginal_distribution(self, *indices): + count = self.component_count + if count.atoms(Symbol): + raise ValueError("Marginal distributions cannot be computed " + "for symbolic dimensions. It is a work under progress.") + orig = [Indexed(self.symbol, i) for i in range(count)] + all_syms = [Symbol(str(i)) for i in orig] + replace_dict = dict(zip(all_syms, orig)) + sym = tuple(Symbol(str(Indexed(self.symbol, i))) for i in indices) + limits = [[i,] for i in all_syms if i not in sym] + index = 0 + for i in range(count): + if i not in indices: + limits[index].append(self.distribution.set.args[i]) + limits[index] = tuple(limits[index]) + index += 1 + if self.distribution.is_Continuous: + f = Lambda(sym, integrate(self.distribution(*all_syms), *limits)) + elif self.distribution.is_Discrete: + f = Lambda(sym, summation(self.distribution(*all_syms), *limits)) + return f.xreplace(replace_dict) + + def compute_expectation(self, expr, rvs=None, evaluate=False, **kwargs): + syms = tuple(self.value[i] for i in range(self.component_count)) + rvs = rvs or syms + if not any(i in rvs for i in syms): + return expr + expr = expr*self.pdf + for rv in rvs: + if isinstance(rv, Indexed): + expr = expr.xreplace({rv: Indexed(str(rv.base), rv.args[1])}) + elif isinstance(rv, RandomSymbol): + expr = expr.xreplace({rv: rv.symbol}) + if self.value in random_symbols(expr): + raise NotImplementedError(filldedent(''' + Expectations of expression with unindexed joint random symbols + cannot be calculated yet.''')) + limits = tuple((Indexed(str(rv.base),rv.args[1]), + self.distribution.set.args[rv.args[1]]) for rv in syms) + return Integral(expr, *limits) + + def where(self, condition): + raise NotImplementedError() + + def compute_density(self, expr): + raise NotImplementedError() + + def sample(self, size=(), library='scipy', seed=None): + """ + Internal sample method + + Returns dictionary mapping RandomSymbol to realization value. + """ + return {RandomSymbol(self.symbol, self): self.distribution.sample(size, + library=library, seed=seed)} + + def probability(self, condition): + raise NotImplementedError() + + +class SampleJointScipy: + """Returns the sample from scipy of the given distribution""" + def __new__(cls, dist, size, seed=None): + return cls._sample_scipy(dist, size, seed) + + @classmethod + def _sample_scipy(cls, dist, size, seed): + """Sample from SciPy.""" + + import numpy + if seed is None or isinstance(seed, int): + rand_state = numpy.random.default_rng(seed=seed) + else: + rand_state = seed + from scipy import stats as scipy_stats + scipy_rv_map = { + 'MultivariateNormalDistribution': lambda dist, size: scipy_stats.multivariate_normal.rvs( + mean=matrix2numpy(dist.mu).flatten(), + cov=matrix2numpy(dist.sigma), size=size, random_state=rand_state), + 'MultivariateBetaDistribution': lambda dist, size: scipy_stats.dirichlet.rvs( + alpha=list2numpy(dist.alpha, float).flatten(), size=size, random_state=rand_state), + 'MultinomialDistribution': lambda dist, size: scipy_stats.multinomial.rvs( + n=int(dist.n), p=list2numpy(dist.p, float).flatten(), size=size, random_state=rand_state) + } + + sample_shape = { + 'MultivariateNormalDistribution': lambda dist: matrix2numpy(dist.mu).flatten().shape, + 'MultivariateBetaDistribution': lambda dist: list2numpy(dist.alpha).flatten().shape, + 'MultinomialDistribution': lambda dist: list2numpy(dist.p).flatten().shape + } + + dist_list = scipy_rv_map.keys() + + if dist.__class__.__name__ not in dist_list: + return None + + samples = scipy_rv_map[dist.__class__.__name__](dist, size) + return samples.reshape(size + sample_shape[dist.__class__.__name__](dist)) + +class SampleJointNumpy: + """Returns the sample from numpy of the given distribution""" + + def __new__(cls, dist, size, seed=None): + return cls._sample_numpy(dist, size, seed) + + @classmethod + def _sample_numpy(cls, dist, size, seed): + """Sample from NumPy.""" + + import numpy + if seed is None or isinstance(seed, int): + rand_state = numpy.random.default_rng(seed=seed) + else: + rand_state = seed + numpy_rv_map = { + 'MultivariateNormalDistribution': lambda dist, size: rand_state.multivariate_normal( + mean=matrix2numpy(dist.mu, float).flatten(), + cov=matrix2numpy(dist.sigma, float), size=size), + 'MultivariateBetaDistribution': lambda dist, size: rand_state.dirichlet( + alpha=list2numpy(dist.alpha, float).flatten(), size=size), + 'MultinomialDistribution': lambda dist, size: rand_state.multinomial( + n=int(dist.n), pvals=list2numpy(dist.p, float).flatten(), size=size) + } + + sample_shape = { + 'MultivariateNormalDistribution': lambda dist: matrix2numpy(dist.mu).flatten().shape, + 'MultivariateBetaDistribution': lambda dist: list2numpy(dist.alpha).flatten().shape, + 'MultinomialDistribution': lambda dist: list2numpy(dist.p).flatten().shape + } + + dist_list = numpy_rv_map.keys() + + if dist.__class__.__name__ not in dist_list: + return None + + samples = numpy_rv_map[dist.__class__.__name__](dist, prod(size)) + return samples.reshape(size + sample_shape[dist.__class__.__name__](dist)) + +class SampleJointPymc: + """Returns the sample from pymc of the given distribution""" + + def __new__(cls, dist, size, seed=None): + return cls._sample_pymc(dist, size, seed) + + @classmethod + def _sample_pymc(cls, dist, size, seed): + """Sample from PyMC.""" + + try: + import pymc + except ImportError: + import pymc3 as pymc + pymc_rv_map = { + 'MultivariateNormalDistribution': lambda dist: + pymc.MvNormal('X', mu=matrix2numpy(dist.mu, float).flatten(), + cov=matrix2numpy(dist.sigma, float), shape=(1, dist.mu.shape[0])), + 'MultivariateBetaDistribution': lambda dist: + pymc.Dirichlet('X', a=list2numpy(dist.alpha, float).flatten()), + 'MultinomialDistribution': lambda dist: + pymc.Multinomial('X', n=int(dist.n), + p=list2numpy(dist.p, float).flatten(), shape=(1, len(dist.p))) + } + + sample_shape = { + 'MultivariateNormalDistribution': lambda dist: matrix2numpy(dist.mu).flatten().shape, + 'MultivariateBetaDistribution': lambda dist: list2numpy(dist.alpha).flatten().shape, + 'MultinomialDistribution': lambda dist: list2numpy(dist.p).flatten().shape + } + + dist_list = pymc_rv_map.keys() + + if dist.__class__.__name__ not in dist_list: + return None + + import logging + logging.getLogger("pymc3").setLevel(logging.ERROR) + with pymc.Model(): + pymc_rv_map[dist.__class__.__name__](dist) + samples = pymc.sample(draws=prod(size), chains=1, progressbar=False, random_seed=seed, return_inferencedata=False, compute_convergence_checks=False)[:]['X'] + return samples.reshape(size + sample_shape[dist.__class__.__name__](dist)) + + +_get_sample_class_jrv = { + 'scipy': SampleJointScipy, + 'pymc3': SampleJointPymc, + 'pymc': SampleJointPymc, + 'numpy': SampleJointNumpy +} + +class JointDistribution(Distribution, NamedArgsMixin): + """ + Represented by the random variables part of the joint distribution. + Contains methods for PDF, CDF, sampling, marginal densities, etc. + """ + + _argnames = ('pdf', ) + + def __new__(cls, *args): + args = list(map(sympify, args)) + for i in range(len(args)): + if isinstance(args[i], list): + args[i] = ImmutableMatrix(args[i]) + return Basic.__new__(cls, *args) + + @property + def domain(self): + return ProductDomain(self.symbols) + + @property + def pdf(self): + return self.density.args[1] + + def cdf(self, other): + if not isinstance(other, dict): + raise ValueError("%s should be of type dict, got %s"%(other, type(other))) + rvs = other.keys() + _set = self.domain.set.sets + expr = self.pdf(tuple(i.args[0] for i in self.symbols)) + for i in range(len(other)): + if rvs[i].is_Continuous: + density = Integral(expr, (rvs[i], _set[i].inf, + other[rvs[i]])) + elif rvs[i].is_Discrete: + density = Sum(expr, (rvs[i], _set[i].inf, + other[rvs[i]])) + return density + + def sample(self, size=(), library='scipy', seed=None): + """ A random realization from the distribution """ + + libraries = ('scipy', 'numpy', 'pymc3', 'pymc') + if library not in libraries: + raise NotImplementedError("Sampling from %s is not supported yet." + % str(library)) + if not import_module(library): + raise ValueError("Failed to import %s" % library) + + samps = _get_sample_class_jrv[library](self, size, seed=seed) + + if samps is not None: + return samps + raise NotImplementedError( + "Sampling for %s is not currently implemented from %s" + % (self.__class__.__name__, library) + ) + + def __call__(self, *args): + return self.pdf(*args) + +class JointRandomSymbol(RandomSymbol): + """ + Representation of random symbols with joint probability distributions + to allow indexing." + """ + def __getitem__(self, key): + if isinstance(self.pspace, JointPSpace): + if (self.pspace.component_count <= key) == True: + raise ValueError("Index keys for %s can only up to %s." % + (self.name, self.pspace.component_count - 1)) + return Indexed(self, key) + + + +class MarginalDistribution(Distribution): + """ + Represents the marginal distribution of a joint probability space. + + Initialised using a probability distribution and random variables(or + their indexed components) which should be a part of the resultant + distribution. + """ + + def __new__(cls, dist, *rvs): + if len(rvs) == 1 and iterable(rvs[0]): + rvs = tuple(rvs[0]) + if not all(isinstance(rv, (Indexed, RandomSymbol)) for rv in rvs): + raise ValueError(filldedent('''Marginal distribution can be + intitialised only in terms of random variables or indexed random + variables''')) + rvs = Tuple.fromiter(rv for rv in rvs) + if not isinstance(dist, JointDistribution) and len(random_symbols(dist)) == 0: + return dist + return Basic.__new__(cls, dist, rvs) + + def check(self): + pass + + @property + def set(self): + rvs = [i for i in self.args[1] if isinstance(i, RandomSymbol)] + return ProductSet(*[rv.pspace.set for rv in rvs]) + + @property + def symbols(self): + rvs = self.args[1] + return {rv.pspace.symbol for rv in rvs} + + def pdf(self, *x): + expr, rvs = self.args[0], self.args[1] + marginalise_out = [i for i in random_symbols(expr) if i not in rvs] + if isinstance(expr, JointDistribution): + count = len(expr.domain.args) + x = Dummy('x', real=True) + syms = tuple(Indexed(x, i) for i in count) + expr = expr.pdf(syms) + else: + syms = tuple(rv.pspace.symbol if isinstance(rv, RandomSymbol) else rv.args[0] for rv in rvs) + return Lambda(syms, self.compute_pdf(expr, marginalise_out))(*x) + + def compute_pdf(self, expr, rvs): + for rv in rvs: + lpdf = 1 + if isinstance(rv, RandomSymbol): + lpdf = rv.pspace.pdf + expr = self.marginalise_out(expr*lpdf, rv) + return expr + + def marginalise_out(self, expr, rv): + from sympy.concrete.summations import Sum + if isinstance(rv, RandomSymbol): + dom = rv.pspace.set + elif isinstance(rv, Indexed): + dom = rv.base.component_domain( + rv.pspace.component_domain(rv.args[1])) + expr = expr.xreplace({rv: rv.pspace.symbol}) + if rv.pspace.is_Continuous: + #TODO: Modify to support integration + #for all kinds of sets. + expr = Integral(expr, (rv.pspace.symbol, dom)) + elif rv.pspace.is_Discrete: + #incorporate this into `Sum`/`summation` + if dom in (S.Integers, S.Naturals, S.Naturals0): + dom = (dom.inf, dom.sup) + expr = Sum(expr, (rv.pspace.symbol, dom)) + return expr + + def __call__(self, *args): + return self.pdf(*args) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/joint_rv_types.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/joint_rv_types.py new file mode 100644 index 0000000000000000000000000000000000000000..6cee9f9aa30897593ffb7c7b930a55a38f0c518a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/joint_rv_types.py @@ -0,0 +1,945 @@ +from sympy.concrete.products import Product +from sympy.concrete.summations import Sum +from sympy.core.add import Add +from sympy.core.function import Lambda +from sympy.core.mul import Mul +from sympy.core.numbers import (Integer, Rational, pi) +from sympy.core.power import Pow +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import (rf, factorial) +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.special.bessel import besselk +from sympy.functions.special.gamma_functions import gamma +from sympy.matrices.dense import (Matrix, ones) +from sympy.sets.fancysets import Range +from sympy.sets.sets import (Intersection, Interval) +from sympy.tensor.indexed import (Indexed, IndexedBase) +from sympy.matrices import ImmutableMatrix, MatrixSymbol +from sympy.matrices.expressions.determinant import det +from sympy.matrices.expressions.matexpr import MatrixElement +from sympy.stats.joint_rv import JointDistribution, JointPSpace, MarginalDistribution +from sympy.stats.rv import _value_check, random_symbols + +__all__ = ['JointRV', +'MultivariateNormal', +'MultivariateLaplace', +'Dirichlet', +'GeneralizedMultivariateLogGamma', +'GeneralizedMultivariateLogGammaOmega', +'Multinomial', +'MultivariateBeta', +'MultivariateEwens', +'MultivariateT', +'NegativeMultinomial', +'NormalGamma' +] + +def multivariate_rv(cls, sym, *args): + args = list(map(sympify, args)) + dist = cls(*args) + args = dist.args + dist.check(*args) + return JointPSpace(sym, dist).value + + +def marginal_distribution(rv, *indices): + """ + Marginal distribution function of a joint random variable. + + Parameters + ========== + + rv : A random variable with a joint probability distribution. + indices : Component indices or the indexed random symbol + for which the joint distribution is to be calculated + + Returns + ======= + + A Lambda expression in `sym`. + + Examples + ======== + + >>> from sympy.stats import MultivariateNormal, marginal_distribution + >>> m = MultivariateNormal('X', [1, 2], [[2, 1], [1, 2]]) + >>> marginal_distribution(m, m[0])(1) + 1/(2*sqrt(pi)) + + """ + indices = list(indices) + for i in range(len(indices)): + if isinstance(indices[i], Indexed): + indices[i] = indices[i].args[1] + prob_space = rv.pspace + if not indices: + raise ValueError( + "At least one component for marginal density is needed.") + if hasattr(prob_space.distribution, '_marginal_distribution'): + return prob_space.distribution._marginal_distribution(indices, rv.symbol) + return prob_space.marginal_distribution(*indices) + + +class JointDistributionHandmade(JointDistribution): + + _argnames = ('pdf',) + is_Continuous = True + + @property + def set(self): + return self.args[1] + + +def JointRV(symbol, pdf, _set=None): + """ + Create a Joint Random Variable where each of its component is continuous, + given the following: + + Parameters + ========== + + symbol : Symbol + Represents name of the random variable. + pdf : A PDF in terms of indexed symbols of the symbol given + as the first argument + + NOTE + ==== + + As of now, the set for each component for a ``JointRV`` is + equal to the set of all integers, which cannot be changed. + + Examples + ======== + + >>> from sympy import exp, pi, Indexed, S + >>> from sympy.stats import density, JointRV + >>> x1, x2 = (Indexed('x', i) for i in (1, 2)) + >>> pdf = exp(-x1**2/2 + x1 - x2**2/2 - S(1)/2)/(2*pi) + >>> N1 = JointRV('x', pdf) #Multivariate Normal distribution + >>> density(N1)(1, 2) + exp(-2)/(2*pi) + + Returns + ======= + + RandomSymbol + + """ + #TODO: Add support for sets provided by the user + symbol = sympify(symbol) + syms = [i for i in pdf.free_symbols if isinstance(i, Indexed) + and i.base == IndexedBase(symbol)] + syms = tuple(sorted(syms, key = lambda index: index.args[1])) + _set = S.Reals**len(syms) + pdf = Lambda(syms, pdf) + dist = JointDistributionHandmade(pdf, _set) + jrv = JointPSpace(symbol, dist).value + rvs = random_symbols(pdf) + if len(rvs) != 0: + dist = MarginalDistribution(dist, (jrv,)) + return JointPSpace(symbol, dist).value + return jrv + +#------------------------------------------------------------------------------- +# Multivariate Normal distribution --------------------------------------------- + +class MultivariateNormalDistribution(JointDistribution): + _argnames = ('mu', 'sigma') + + is_Continuous=True + + @property + def set(self): + k = self.mu.shape[0] + return S.Reals**k + + @staticmethod + def check(mu, sigma): + _value_check(mu.shape[0] == sigma.shape[0], + "Size of the mean vector and covariance matrix are incorrect.") + #check if covariance matrix is positive semi definite or not. + if not isinstance(sigma, MatrixSymbol): + _value_check(sigma.is_positive_semidefinite, + "The covariance matrix must be positive semi definite. ") + + def pdf(self, *args): + mu, sigma = self.mu, self.sigma + k = mu.shape[0] + if len(args) == 1 and args[0].is_Matrix: + args = args[0] + else: + args = ImmutableMatrix(args) + x = args - mu + density = S.One/sqrt((2*pi)**(k)*det(sigma))*exp( + Rational(-1, 2)*x.transpose()*(sigma.inv()*x)) + return MatrixElement(density, 0, 0) + + def _marginal_distribution(self, indices, sym): + sym = ImmutableMatrix([Indexed(sym, i) for i in indices]) + _mu, _sigma = self.mu, self.sigma + k = self.mu.shape[0] + for i in range(k): + if i not in indices: + _mu = _mu.row_del(i) + _sigma = _sigma.col_del(i) + _sigma = _sigma.row_del(i) + return Lambda(tuple(sym), S.One/sqrt((2*pi)**(len(_mu))*det(_sigma))*exp( + Rational(-1, 2)*(_mu - sym).transpose()*(_sigma.inv()*\ + (_mu - sym)))[0]) + +def MultivariateNormal(name, mu, sigma): + r""" + Creates a continuous random variable with Multivariate Normal + Distribution. + + The density of the multivariate normal distribution can be found at [1]. + + Parameters + ========== + + mu : List representing the mean or the mean vector + sigma : Positive semidefinite square matrix + Represents covariance Matrix. + If `\sigma` is noninvertible then only sampling is supported currently + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import MultivariateNormal, density, marginal_distribution + >>> from sympy import symbols, MatrixSymbol + >>> X = MultivariateNormal('X', [3, 4], [[2, 1], [1, 2]]) + >>> y, z = symbols('y z') + >>> density(X)(y, z) + sqrt(3)*exp(-y**2/3 + y*z/3 + 2*y/3 - z**2/3 + 5*z/3 - 13/3)/(6*pi) + >>> density(X)(1, 2) + sqrt(3)*exp(-4/3)/(6*pi) + >>> marginal_distribution(X, X[1])(y) + exp(-(y - 4)**2/4)/(2*sqrt(pi)) + >>> marginal_distribution(X, X[0])(y) + exp(-(y - 3)**2/4)/(2*sqrt(pi)) + + The example below shows that it is also possible to use + symbolic parameters to define the MultivariateNormal class. + + >>> n = symbols('n', integer=True, positive=True) + >>> Sg = MatrixSymbol('Sg', n, n) + >>> mu = MatrixSymbol('mu', n, 1) + >>> obs = MatrixSymbol('obs', n, 1) + >>> X = MultivariateNormal('X', mu, Sg) + + The density of a multivariate normal can be + calculated using a matrix argument, as shown below. + + >>> density(X)(obs) + (exp(((1/2)*mu.T - (1/2)*obs.T)*Sg**(-1)*(-mu + obs))/sqrt((2*pi)**n*Determinant(Sg)))[0, 0] + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Multivariate_normal_distribution + + """ + return multivariate_rv(MultivariateNormalDistribution, name, mu, sigma) + +#------------------------------------------------------------------------------- +# Multivariate Laplace distribution -------------------------------------------- + +class MultivariateLaplaceDistribution(JointDistribution): + _argnames = ('mu', 'sigma') + is_Continuous=True + + @property + def set(self): + k = self.mu.shape[0] + return S.Reals**k + + @staticmethod + def check(mu, sigma): + _value_check(mu.shape[0] == sigma.shape[0], + "Size of the mean vector and covariance matrix are incorrect.") + # check if covariance matrix is positive definite or not. + if not isinstance(sigma, MatrixSymbol): + _value_check(sigma.is_positive_definite, + "The covariance matrix must be positive definite. ") + + def pdf(self, *args): + mu, sigma = self.mu, self.sigma + mu_T = mu.transpose() + k = S(mu.shape[0]) + sigma_inv = sigma.inv() + args = ImmutableMatrix(args) + args_T = args.transpose() + x = (mu_T*sigma_inv*mu)[0] + y = (args_T*sigma_inv*args)[0] + v = 1 - k/2 + return (2 * (y/(2 + x))**(v/2) * besselk(v, sqrt((2 + x)*y)) * + exp((args_T * sigma_inv * mu)[0]) / + ((2 * pi)**(k/2) * sqrt(det(sigma)))) + + +def MultivariateLaplace(name, mu, sigma): + """ + Creates a continuous random variable with Multivariate Laplace + Distribution. + + The density of the multivariate Laplace distribution can be found at [1]. + + Parameters + ========== + + mu : List representing the mean or the mean vector + sigma : Positive definite square matrix + Represents covariance Matrix + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import MultivariateLaplace, density + >>> from sympy import symbols + >>> y, z = symbols('y z') + >>> X = MultivariateLaplace('X', [2, 4], [[3, 1], [1, 3]]) + >>> density(X)(y, z) + sqrt(2)*exp(y/4 + 5*z/4)*besselk(0, sqrt(15*y*(3*y/8 - z/8)/2 + 15*z*(-y/8 + 3*z/8)/2))/(4*pi) + >>> density(X)(1, 2) + sqrt(2)*exp(11/4)*besselk(0, sqrt(165)/4)/(4*pi) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Multivariate_Laplace_distribution + + """ + return multivariate_rv(MultivariateLaplaceDistribution, name, mu, sigma) + +#------------------------------------------------------------------------------- +# Multivariate StudentT distribution ------------------------------------------- + +class MultivariateTDistribution(JointDistribution): + _argnames = ('mu', 'shape_mat', 'dof') + is_Continuous=True + + @property + def set(self): + k = self.mu.shape[0] + return S.Reals**k + + @staticmethod + def check(mu, sigma, v): + _value_check(mu.shape[0] == sigma.shape[0], + "Size of the location vector and shape matrix are incorrect.") + # check if covariance matrix is positive definite or not. + if not isinstance(sigma, MatrixSymbol): + _value_check(sigma.is_positive_definite, + "The shape matrix must be positive definite. ") + + def pdf(self, *args): + mu, sigma = self.mu, self.shape_mat + v = S(self.dof) + k = S(mu.shape[0]) + sigma_inv = sigma.inv() + args = ImmutableMatrix(args) + x = args - mu + return gamma((k + v)/2)/(gamma(v/2)*(v*pi)**(k/2)*sqrt(det(sigma)))\ + *(1 + 1/v*(x.transpose()*sigma_inv*x)[0])**((-v - k)/2) + +def MultivariateT(syms, mu, sigma, v): + """ + Creates a joint random variable with multivariate T-distribution. + + Parameters + ========== + + syms : A symbol/str + For identifying the random variable. + mu : A list/matrix + Representing the location vector + sigma : The shape matrix for the distribution + + Examples + ======== + + >>> from sympy.stats import density, MultivariateT + >>> from sympy import Symbol + + >>> x = Symbol("x") + >>> X = MultivariateT("x", [1, 1], [[1, 0], [0, 1]], 2) + + >>> density(X)(1, 2) + 2/(9*pi) + + Returns + ======= + + RandomSymbol + + """ + return multivariate_rv(MultivariateTDistribution, syms, mu, sigma, v) + + +#------------------------------------------------------------------------------- +# Multivariate Normal Gamma distribution --------------------------------------- + +class NormalGammaDistribution(JointDistribution): + + _argnames = ('mu', 'lamda', 'alpha', 'beta') + is_Continuous=True + + @staticmethod + def check(mu, lamda, alpha, beta): + _value_check(mu.is_real, "Location must be real.") + _value_check(lamda > 0, "Lambda must be positive") + _value_check(alpha > 0, "alpha must be positive") + _value_check(beta > 0, "beta must be positive") + + @property + def set(self): + return S.Reals*Interval(0, S.Infinity) + + def pdf(self, x, tau): + beta, alpha, lamda = self.beta, self.alpha, self.lamda + mu = self.mu + + return beta**alpha*sqrt(lamda)/(gamma(alpha)*sqrt(2*pi))*\ + tau**(alpha - S.Half)*exp(-1*beta*tau)*\ + exp(-1*(lamda*tau*(x - mu)**2)/S(2)) + + def _marginal_distribution(self, indices, *sym): + if len(indices) == 2: + return self.pdf(*sym) + if indices[0] == 0: + #For marginal over `x`, return non-standardized Student-T's + #distribution + x = sym[0] + v, mu, sigma = self.alpha - S.Half, self.mu, \ + S(self.beta)/(self.lamda * self.alpha) + return Lambda(sym, gamma((v + 1)/2)/(gamma(v/2)*sqrt(pi*v)*sigma)*\ + (1 + 1/v*((x - mu)/sigma)**2)**((-v -1)/2)) + #For marginal over `tau`, return Gamma distribution as per construction + from sympy.stats.crv_types import GammaDistribution + return Lambda(sym, GammaDistribution(self.alpha, self.beta)(sym[0])) + +def NormalGamma(sym, mu, lamda, alpha, beta): + """ + Creates a bivariate joint random variable with multivariate Normal gamma + distribution. + + Parameters + ========== + + sym : A symbol/str + For identifying the random variable. + mu : A real number + The mean of the normal distribution + lamda : A positive integer + Parameter of joint distribution + alpha : A positive integer + Parameter of joint distribution + beta : A positive integer + Parameter of joint distribution + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import density, NormalGamma + >>> from sympy import symbols + + >>> X = NormalGamma('x', 0, 1, 2, 3) + >>> y, z = symbols('y z') + + >>> density(X)(y, z) + 9*sqrt(2)*z**(3/2)*exp(-3*z)*exp(-y**2*z/2)/(2*sqrt(pi)) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Normal-gamma_distribution + + """ + return multivariate_rv(NormalGammaDistribution, sym, mu, lamda, alpha, beta) + +#------------------------------------------------------------------------------- +# Multivariate Beta/Dirichlet distribution ------------------------------------- + +class MultivariateBetaDistribution(JointDistribution): + + _argnames = ('alpha',) + is_Continuous = True + + @staticmethod + def check(alpha): + _value_check(len(alpha) >= 2, "At least two categories should be passed.") + for a_k in alpha: + _value_check((a_k > 0) != False, "Each concentration parameter" + " should be positive.") + + @property + def set(self): + k = len(self.alpha) + return Interval(0, 1)**k + + def pdf(self, *syms): + alpha = self.alpha + B = Mul.fromiter(map(gamma, alpha))/gamma(Add(*alpha)) + return Mul.fromiter(sym**(a_k - 1) for a_k, sym in zip(alpha, syms))/B + +def MultivariateBeta(syms, *alpha): + """ + Creates a continuous random variable with Dirichlet/Multivariate Beta + Distribution. + + The density of the Dirichlet distribution can be found at [1]. + + Parameters + ========== + + alpha : Positive real numbers + Signifies concentration numbers. + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import density, MultivariateBeta, marginal_distribution + >>> from sympy import Symbol + >>> a1 = Symbol('a1', positive=True) + >>> a2 = Symbol('a2', positive=True) + >>> B = MultivariateBeta('B', [a1, a2]) + >>> C = MultivariateBeta('C', a1, a2) + >>> x = Symbol('x') + >>> y = Symbol('y') + >>> density(B)(x, y) + x**(a1 - 1)*y**(a2 - 1)*gamma(a1 + a2)/(gamma(a1)*gamma(a2)) + >>> marginal_distribution(C, C[0])(x) + x**(a1 - 1)*gamma(a1 + a2)/(a2*gamma(a1)*gamma(a2)) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Dirichlet_distribution + .. [2] https://mathworld.wolfram.com/DirichletDistribution.html + + """ + if not isinstance(alpha[0], list): + alpha = (list(alpha),) + return multivariate_rv(MultivariateBetaDistribution, syms, alpha[0]) + +Dirichlet = MultivariateBeta + +#------------------------------------------------------------------------------- +# Multivariate Ewens distribution ---------------------------------------------- + +class MultivariateEwensDistribution(JointDistribution): + + _argnames = ('n', 'theta') + is_Discrete = True + is_Continuous = False + + @staticmethod + def check(n, theta): + _value_check((n > 0), + "sample size should be positive integer.") + _value_check(theta.is_positive, "mutation rate should be positive.") + + @property + def set(self): + if not isinstance(self.n, Integer): + i = Symbol('i', integer=True, positive=True) + return Product(Intersection(S.Naturals0, Interval(0, self.n//i)), + (i, 1, self.n)) + prod_set = Range(0, self.n + 1) + for i in range(2, self.n + 1): + prod_set *= Range(0, self.n//i + 1) + return prod_set.flatten() + + def pdf(self, *syms): + n, theta = self.n, self.theta + condi = isinstance(self.n, Integer) + if not (isinstance(syms[0], IndexedBase) or condi): + raise ValueError("Please use IndexedBase object for syms as " + "the dimension is symbolic") + term_1 = factorial(n)/rf(theta, n) + if condi: + term_2 = Mul.fromiter(theta**syms[j]/((j+1)**syms[j]*factorial(syms[j])) + for j in range(n)) + cond = Eq(sum((k + 1)*syms[k] for k in range(n)), n) + return Piecewise((term_1 * term_2, cond), (0, True)) + syms = syms[0] + j, k = symbols('j, k', positive=True, integer=True) + term_2 = Product(theta**syms[j]/((j+1)**syms[j]*factorial(syms[j])), + (j, 0, n - 1)) + cond = Eq(Sum((k + 1)*syms[k], (k, 0, n - 1)), n) + return Piecewise((term_1 * term_2, cond), (0, True)) + + +def MultivariateEwens(syms, n, theta): + """ + Creates a discrete random variable with Multivariate Ewens + Distribution. + + The density of the said distribution can be found at [1]. + + Parameters + ========== + + n : Positive integer + Size of the sample or the integer whose partitions are considered + theta : Positive real number + Denotes Mutation rate + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import density, marginal_distribution, MultivariateEwens + >>> from sympy import Symbol + >>> a1 = Symbol('a1', positive=True) + >>> a2 = Symbol('a2', positive=True) + >>> ed = MultivariateEwens('E', 2, 1) + >>> density(ed)(a1, a2) + Piecewise((1/(2**a2*factorial(a1)*factorial(a2)), Eq(a1 + 2*a2, 2)), (0, True)) + >>> marginal_distribution(ed, ed[0])(a1) + Piecewise((1/factorial(a1), Eq(a1, 2)), (0, True)) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Ewens%27s_sampling_formula + .. [2] https://www.jstor.org/stable/24780825 + """ + return multivariate_rv(MultivariateEwensDistribution, syms, n, theta) + +#------------------------------------------------------------------------------- +# Generalized Multivariate Log Gamma distribution ------------------------------ + +class GeneralizedMultivariateLogGammaDistribution(JointDistribution): + + _argnames = ('delta', 'v', 'lamda', 'mu') + is_Continuous=True + + def check(self, delta, v, l, mu): + _value_check((delta >= 0, delta <= 1), "delta must be in range [0, 1].") + _value_check((v > 0), "v must be positive") + for lk in l: + _value_check((lk > 0), "lamda must be a positive vector.") + for muk in mu: + _value_check((muk > 0), "mu must be a positive vector.") + _value_check(len(l) > 1,"the distribution should have at least" + " two random variables.") + + @property + def set(self): + return S.Reals**len(self.lamda) + + def pdf(self, *y): + d, v, l, mu = self.delta, self.v, self.lamda, self.mu + n = Symbol('n', negative=False, integer=True) + k = len(l) + sterm1 = Pow((1 - d), n)/\ + ((gamma(v + n)**(k - 1))*gamma(v)*gamma(n + 1)) + sterm2 = Mul.fromiter(mui*li**(-v - n) for mui, li in zip(mu, l)) + term1 = sterm1 * sterm2 + sterm3 = (v + n) * sum(mui * yi for mui, yi in zip(mu, y)) + sterm4 = sum(exp(mui * yi)/li for (mui, yi, li) in zip(mu, y, l)) + term2 = exp(sterm3 - sterm4) + return Pow(d, v) * Sum(term1 * term2, (n, 0, S.Infinity)) + +def GeneralizedMultivariateLogGamma(syms, delta, v, lamda, mu): + """ + Creates a joint random variable with generalized multivariate log gamma + distribution. + + The joint pdf can be found at [1]. + + Parameters + ========== + + syms : list/tuple/set of symbols for identifying each component + delta : A constant in range $[0, 1]$ + v : Positive real number + lamda : List of positive real numbers + mu : List of positive real numbers + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import density + >>> from sympy.stats.joint_rv_types import GeneralizedMultivariateLogGamma + >>> from sympy import symbols, S + >>> v = 1 + >>> l, mu = [1, 1, 1], [1, 1, 1] + >>> d = S.Half + >>> y = symbols('y_1:4', positive=True) + >>> Gd = GeneralizedMultivariateLogGamma('G', d, v, l, mu) + >>> density(Gd)(y[0], y[1], y[2]) + Sum(exp((n + 1)*(y_1 + y_2 + y_3) - exp(y_1) - exp(y_2) - + exp(y_3))/(2**n*gamma(n + 1)**3), (n, 0, oo))/2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Generalized_multivariate_log-gamma_distribution + .. [2] https://www.researchgate.net/publication/234137346_On_a_multivariate_log-gamma_distribution_and_the_use_of_the_distribution_in_the_Bayesian_analysis + + Note + ==== + + If the GeneralizedMultivariateLogGamma is too long to type use, + + >>> from sympy.stats.joint_rv_types import GeneralizedMultivariateLogGamma as GMVLG + >>> Gd = GMVLG('G', d, v, l, mu) + + If you want to pass the matrix omega instead of the constant delta, then use + ``GeneralizedMultivariateLogGammaOmega``. + + """ + return multivariate_rv(GeneralizedMultivariateLogGammaDistribution, + syms, delta, v, lamda, mu) + +def GeneralizedMultivariateLogGammaOmega(syms, omega, v, lamda, mu): + """ + Extends GeneralizedMultivariateLogGamma. + + Parameters + ========== + + syms : list/tuple/set of symbols + For identifying each component + omega : A square matrix + Every element of square matrix must be absolute value of + square root of correlation coefficient + v : Positive real number + lamda : List of positive real numbers + mu : List of positive real numbers + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import density + >>> from sympy.stats.joint_rv_types import GeneralizedMultivariateLogGammaOmega + >>> from sympy import Matrix, symbols, S + >>> omega = Matrix([[1, S.Half, S.Half], [S.Half, 1, S.Half], [S.Half, S.Half, 1]]) + >>> v = 1 + >>> l, mu = [1, 1, 1], [1, 1, 1] + >>> G = GeneralizedMultivariateLogGammaOmega('G', omega, v, l, mu) + >>> y = symbols('y_1:4', positive=True) + >>> density(G)(y[0], y[1], y[2]) + sqrt(2)*Sum((1 - sqrt(2)/2)**n*exp((n + 1)*(y_1 + y_2 + y_3) - exp(y_1) - + exp(y_2) - exp(y_3))/gamma(n + 1)**3, (n, 0, oo))/2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Generalized_multivariate_log-gamma_distribution + .. [2] https://www.researchgate.net/publication/234137346_On_a_multivariate_log-gamma_distribution_and_the_use_of_the_distribution_in_the_Bayesian_analysis + + Notes + ===== + + If the GeneralizedMultivariateLogGammaOmega is too long to type use, + + >>> from sympy.stats.joint_rv_types import GeneralizedMultivariateLogGammaOmega as GMVLGO + >>> G = GMVLGO('G', omega, v, l, mu) + + """ + _value_check((omega.is_square, isinstance(omega, Matrix)), "omega must be a" + " square matrix") + for val in omega.values(): + _value_check((val >= 0, val <= 1), + "all values in matrix must be between 0 and 1(both inclusive).") + _value_check(omega.diagonal().equals(ones(1, omega.shape[0])), + "all the elements of diagonal should be 1.") + _value_check((omega.shape[0] == len(lamda), len(lamda) == len(mu)), + "lamda, mu should be of same length and omega should " + " be of shape (length of lamda, length of mu)") + _value_check(len(lamda) > 1,"the distribution should have at least" + " two random variables.") + delta = Pow(Rational(omega.det()), Rational(1, len(lamda) - 1)) + return GeneralizedMultivariateLogGamma(syms, delta, v, lamda, mu) + + +#------------------------------------------------------------------------------- +# Multinomial distribution ----------------------------------------------------- + +class MultinomialDistribution(JointDistribution): + + _argnames = ('n', 'p') + is_Continuous=False + is_Discrete = True + + @staticmethod + def check(n, p): + _value_check(n > 0, + "number of trials must be a positive integer") + for p_k in p: + _value_check((p_k >= 0, p_k <= 1), + "probability must be in range [0, 1]") + _value_check(Eq(sum(p), 1), + "probabilities must sum to 1") + + @property + def set(self): + return Intersection(S.Naturals0, Interval(0, self.n))**len(self.p) + + def pdf(self, *x): + n, p = self.n, self.p + term_1 = factorial(n)/Mul.fromiter(factorial(x_k) for x_k in x) + term_2 = Mul.fromiter(p_k**x_k for p_k, x_k in zip(p, x)) + return Piecewise((term_1 * term_2, Eq(sum(x), n)), (0, True)) + +def Multinomial(syms, n, *p): + """ + Creates a discrete random variable with Multinomial Distribution. + + The density of the said distribution can be found at [1]. + + Parameters + ========== + + n : Positive integer + Represents number of trials + p : List of event probabilities + Must be in the range of $[0, 1]$. + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import density, Multinomial, marginal_distribution + >>> from sympy import symbols + >>> x1, x2, x3 = symbols('x1, x2, x3', nonnegative=True, integer=True) + >>> p1, p2, p3 = symbols('p1, p2, p3', positive=True) + >>> M = Multinomial('M', 3, p1, p2, p3) + >>> density(M)(x1, x2, x3) + Piecewise((6*p1**x1*p2**x2*p3**x3/(factorial(x1)*factorial(x2)*factorial(x3)), + Eq(x1 + x2 + x3, 3)), (0, True)) + >>> marginal_distribution(M, M[0])(x1).subs(x1, 1) + 3*p1*p2**2 + 6*p1*p2*p3 + 3*p1*p3**2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Multinomial_distribution + .. [2] https://mathworld.wolfram.com/MultinomialDistribution.html + + """ + if not isinstance(p[0], list): + p = (list(p), ) + return multivariate_rv(MultinomialDistribution, syms, n, p[0]) + +#------------------------------------------------------------------------------- +# Negative Multinomial Distribution -------------------------------------------- + +class NegativeMultinomialDistribution(JointDistribution): + + _argnames = ('k0', 'p') + is_Continuous=False + is_Discrete = True + + @staticmethod + def check(k0, p): + _value_check(k0 > 0, + "number of failures must be a positive integer") + for p_k in p: + _value_check((p_k >= 0, p_k <= 1), + "probability must be in range [0, 1].") + _value_check(sum(p) <= 1, + "success probabilities must not be greater than 1.") + + @property + def set(self): + return Range(0, S.Infinity)**len(self.p) + + def pdf(self, *k): + k0, p = self.k0, self.p + term_1 = (gamma(k0 + sum(k))*(1 - sum(p))**k0)/gamma(k0) + term_2 = Mul.fromiter(pi**ki/factorial(ki) for pi, ki in zip(p, k)) + return term_1 * term_2 + +def NegativeMultinomial(syms, k0, *p): + """ + Creates a discrete random variable with Negative Multinomial Distribution. + + The density of the said distribution can be found at [1]. + + Parameters + ========== + + k0 : positive integer + Represents number of failures before the experiment is stopped + p : List of event probabilities + Must be in the range of $[0, 1]$ + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import density, NegativeMultinomial, marginal_distribution + >>> from sympy import symbols + >>> x1, x2, x3 = symbols('x1, x2, x3', nonnegative=True, integer=True) + >>> p1, p2, p3 = symbols('p1, p2, p3', positive=True) + >>> N = NegativeMultinomial('M', 3, p1, p2, p3) + >>> N_c = NegativeMultinomial('M', 3, 0.1, 0.1, 0.1) + >>> density(N)(x1, x2, x3) + p1**x1*p2**x2*p3**x3*(-p1 - p2 - p3 + 1)**3*gamma(x1 + x2 + + x3 + 3)/(2*factorial(x1)*factorial(x2)*factorial(x3)) + >>> marginal_distribution(N_c, N_c[0])(1).evalf().round(2) + 0.25 + + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Negative_multinomial_distribution + .. [2] https://mathworld.wolfram.com/NegativeBinomialDistribution.html + + """ + if not isinstance(p[0], list): + p = (list(p), ) + return multivariate_rv(NegativeMultinomialDistribution, syms, k0, p[0]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/matrix_distributions.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/matrix_distributions.py new file mode 100644 index 0000000000000000000000000000000000000000..9a43c0226bc25702211a910ebbe30e280ad0cf50 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/matrix_distributions.py @@ -0,0 +1,610 @@ +from math import prod + +from sympy.core.basic import Basic +from sympy.core.numbers import pi +from sympy.core.singleton import S +from sympy.functions.elementary.exponential import exp +from sympy.functions.special.gamma_functions import multigamma +from sympy.core.sympify import sympify, _sympify +from sympy.matrices import (ImmutableMatrix, Inverse, Trace, Determinant, + MatrixSymbol, MatrixBase, Transpose, MatrixSet, + matrix2numpy) +from sympy.stats.rv import (_value_check, RandomMatrixSymbol, NamedArgsMixin, PSpace, + _symbol_converter, MatrixDomain, Distribution) +from sympy.external import import_module + + +################################################################################ +#------------------------Matrix Probability Space------------------------------# +################################################################################ +class MatrixPSpace(PSpace): + """ + Represents probability space for + Matrix Distributions. + """ + def __new__(cls, sym, distribution, dim_n, dim_m): + sym = _symbol_converter(sym) + dim_n, dim_m = _sympify(dim_n), _sympify(dim_m) + if not (dim_n.is_integer and dim_m.is_integer): + raise ValueError("Dimensions should be integers") + return Basic.__new__(cls, sym, distribution, dim_n, dim_m) + + distribution = property(lambda self: self.args[1]) + symbol = property(lambda self: self.args[0]) + + @property + def domain(self): + return MatrixDomain(self.symbol, self.distribution.set) + + @property + def value(self): + return RandomMatrixSymbol(self.symbol, self.args[2], self.args[3], self) + + @property + def values(self): + return {self.value} + + def compute_density(self, expr, *args): + rms = expr.atoms(RandomMatrixSymbol) + if len(rms) > 1 or (not isinstance(expr, RandomMatrixSymbol)): + raise NotImplementedError("Currently, no algorithm has been " + "implemented to handle general expressions containing " + "multiple matrix distributions.") + return self.distribution.pdf(expr) + + def sample(self, size=(), library='scipy', seed=None): + """ + Internal sample method + + Returns dictionary mapping RandomMatrixSymbol to realization value. + """ + return {self.value: self.distribution.sample(size, library=library, seed=seed)} + + +def rv(symbol, cls, args): + args = list(map(sympify, args)) + dist = cls(*args) + dist.check(*args) + dim = dist.dimension + pspace = MatrixPSpace(symbol, dist, dim[0], dim[1]) + return pspace.value + + +class SampleMatrixScipy: + """Returns the sample from scipy of the given distribution""" + def __new__(cls, dist, size, seed=None): + return cls._sample_scipy(dist, size, seed) + + @classmethod + def _sample_scipy(cls, dist, size, seed): + """Sample from SciPy.""" + + from scipy import stats as scipy_stats + import numpy + scipy_rv_map = { + 'WishartDistribution': lambda dist, size, rand_state: scipy_stats.wishart.rvs( + df=int(dist.n), scale=matrix2numpy(dist.scale_matrix, float), size=size), + 'MatrixNormalDistribution': lambda dist, size, rand_state: scipy_stats.matrix_normal.rvs( + mean=matrix2numpy(dist.location_matrix, float), + rowcov=matrix2numpy(dist.scale_matrix_1, float), + colcov=matrix2numpy(dist.scale_matrix_2, float), size=size, random_state=rand_state) + } + + sample_shape = { + 'WishartDistribution': lambda dist: dist.scale_matrix.shape, + 'MatrixNormalDistribution' : lambda dist: dist.location_matrix.shape + } + + dist_list = scipy_rv_map.keys() + + if dist.__class__.__name__ not in dist_list: + return None + + if seed is None or isinstance(seed, int): + rand_state = numpy.random.default_rng(seed=seed) + else: + rand_state = seed + samp = scipy_rv_map[dist.__class__.__name__](dist, prod(size), rand_state) + return samp.reshape(size + sample_shape[dist.__class__.__name__](dist)) + + +class SampleMatrixNumpy: + """Returns the sample from numpy of the given distribution""" + + ### TODO: Add tests after adding matrix distributions in numpy_rv_map + def __new__(cls, dist, size, seed=None): + return cls._sample_numpy(dist, size, seed) + + @classmethod + def _sample_numpy(cls, dist, size, seed): + """Sample from NumPy.""" + + numpy_rv_map = { + } + + sample_shape = { + } + + dist_list = numpy_rv_map.keys() + + if dist.__class__.__name__ not in dist_list: + return None + + import numpy + if seed is None or isinstance(seed, int): + rand_state = numpy.random.default_rng(seed=seed) + else: + rand_state = seed + samp = numpy_rv_map[dist.__class__.__name__](dist, prod(size), rand_state) + return samp.reshape(size + sample_shape[dist.__class__.__name__](dist)) + + +class SampleMatrixPymc: + """Returns the sample from pymc of the given distribution""" + + def __new__(cls, dist, size, seed=None): + return cls._sample_pymc(dist, size, seed) + + @classmethod + def _sample_pymc(cls, dist, size, seed): + """Sample from PyMC.""" + + try: + import pymc + except ImportError: + import pymc3 as pymc + pymc_rv_map = { + 'MatrixNormalDistribution': lambda dist: pymc.MatrixNormal('X', + mu=matrix2numpy(dist.location_matrix, float), + rowcov=matrix2numpy(dist.scale_matrix_1, float), + colcov=matrix2numpy(dist.scale_matrix_2, float), + shape=dist.location_matrix.shape), + 'WishartDistribution': lambda dist: pymc.WishartBartlett('X', + nu=int(dist.n), S=matrix2numpy(dist.scale_matrix, float)) + } + + sample_shape = { + 'WishartDistribution': lambda dist: dist.scale_matrix.shape, + 'MatrixNormalDistribution' : lambda dist: dist.location_matrix.shape + } + + dist_list = pymc_rv_map.keys() + + if dist.__class__.__name__ not in dist_list: + return None + import logging + logging.getLogger("pymc").setLevel(logging.ERROR) + with pymc.Model(): + pymc_rv_map[dist.__class__.__name__](dist) + samps = pymc.sample(draws=prod(size), chains=1, progressbar=False, random_seed=seed, return_inferencedata=False, compute_convergence_checks=False)['X'] + return samps.reshape(size + sample_shape[dist.__class__.__name__](dist)) + +_get_sample_class_matrixrv = { + 'scipy': SampleMatrixScipy, + 'pymc3': SampleMatrixPymc, + 'pymc': SampleMatrixPymc, + 'numpy': SampleMatrixNumpy +} + +################################################################################ +#-------------------------Matrix Distribution----------------------------------# +################################################################################ + +class MatrixDistribution(Distribution, NamedArgsMixin): + """ + Abstract class for Matrix Distribution. + """ + def __new__(cls, *args): + args = [ImmutableMatrix(arg) if isinstance(arg, list) + else _sympify(arg) for arg in args] + return Basic.__new__(cls, *args) + + @staticmethod + def check(*args): + pass + + def __call__(self, expr): + if isinstance(expr, list): + expr = ImmutableMatrix(expr) + return self.pdf(expr) + + def sample(self, size=(), library='scipy', seed=None): + """ + Internal sample method + + Returns dictionary mapping RandomSymbol to realization value. + """ + + libraries = ['scipy', 'numpy', 'pymc3', 'pymc'] + if library not in libraries: + raise NotImplementedError("Sampling from %s is not supported yet." + % str(library)) + if not import_module(library): + raise ValueError("Failed to import %s" % library) + + samps = _get_sample_class_matrixrv[library](self, size, seed) + + if samps is not None: + return samps + raise NotImplementedError( + "Sampling for %s is not currently implemented from %s" + % (self.__class__.__name__, library) + ) + +################################################################################ +#------------------------Matrix Distribution Types-----------------------------# +################################################################################ + +#------------------------------------------------------------------------------- +# Matrix Gamma distribution ---------------------------------------------------- + +class MatrixGammaDistribution(MatrixDistribution): + + _argnames = ('alpha', 'beta', 'scale_matrix') + + @staticmethod + def check(alpha, beta, scale_matrix): + if not isinstance(scale_matrix, MatrixSymbol): + _value_check(scale_matrix.is_positive_definite, "The shape " + "matrix must be positive definite.") + _value_check(scale_matrix.is_square, "Should " + "be square matrix") + _value_check(alpha.is_positive, "Shape parameter should be positive.") + _value_check(beta.is_positive, "Scale parameter should be positive.") + + @property + def set(self): + k = self.scale_matrix.shape[0] + return MatrixSet(k, k, S.Reals) + + @property + def dimension(self): + return self.scale_matrix.shape + + def pdf(self, x): + alpha, beta, scale_matrix = self.alpha, self.beta, self.scale_matrix + p = scale_matrix.shape[0] + if isinstance(x, list): + x = ImmutableMatrix(x) + if not isinstance(x, (MatrixBase, MatrixSymbol)): + raise ValueError("%s should be an isinstance of Matrix " + "or MatrixSymbol" % str(x)) + sigma_inv_x = - Inverse(scale_matrix)*x / beta + term1 = exp(Trace(sigma_inv_x))/((beta**(p*alpha)) * multigamma(alpha, p)) + term2 = (Determinant(scale_matrix))**(-alpha) + term3 = (Determinant(x))**(alpha - S(p + 1)/2) + return term1 * term2 * term3 + +def MatrixGamma(symbol, alpha, beta, scale_matrix): + """ + Creates a random variable with Matrix Gamma Distribution. + + The density of the said distribution can be found at [1]. + + Parameters + ========== + + alpha: Positive Real number + Shape Parameter + beta: Positive Real number + Scale Parameter + scale_matrix: Positive definite real square matrix + Scale Matrix + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import density, MatrixGamma + >>> from sympy import MatrixSymbol, symbols + >>> a, b = symbols('a b', positive=True) + >>> M = MatrixGamma('M', a, b, [[2, 1], [1, 2]]) + >>> X = MatrixSymbol('X', 2, 2) + >>> density(M)(X).doit() + exp(Trace(Matrix([ + [-2/3, 1/3], + [ 1/3, -2/3]])*X)/b)*Determinant(X)**(a - 3/2)/(3**a*sqrt(pi)*b**(2*a)*gamma(a)*gamma(a - 1/2)) + >>> density(M)([[1, 0], [0, 1]]).doit() + exp(-4/(3*b))/(3**a*sqrt(pi)*b**(2*a)*gamma(a)*gamma(a - 1/2)) + + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Matrix_gamma_distribution + + """ + if isinstance(scale_matrix, list): + scale_matrix = ImmutableMatrix(scale_matrix) + return rv(symbol, MatrixGammaDistribution, (alpha, beta, scale_matrix)) + +#------------------------------------------------------------------------------- +# Wishart Distribution --------------------------------------------------------- + +class WishartDistribution(MatrixDistribution): + + _argnames = ('n', 'scale_matrix') + + @staticmethod + def check(n, scale_matrix): + if not isinstance(scale_matrix, MatrixSymbol): + _value_check(scale_matrix.is_positive_definite, "The shape " + "matrix must be positive definite.") + _value_check(scale_matrix.is_square, "Should " + "be square matrix") + _value_check(n.is_positive, "Shape parameter should be positive.") + + @property + def set(self): + k = self.scale_matrix.shape[0] + return MatrixSet(k, k, S.Reals) + + @property + def dimension(self): + return self.scale_matrix.shape + + def pdf(self, x): + n, scale_matrix = self.n, self.scale_matrix + p = scale_matrix.shape[0] + if isinstance(x, list): + x = ImmutableMatrix(x) + if not isinstance(x, (MatrixBase, MatrixSymbol)): + raise ValueError("%s should be an isinstance of Matrix " + "or MatrixSymbol" % str(x)) + sigma_inv_x = - Inverse(scale_matrix)*x / S(2) + term1 = exp(Trace(sigma_inv_x))/((2**(p*n/S(2))) * multigamma(n/S(2), p)) + term2 = (Determinant(scale_matrix))**(-n/S(2)) + term3 = (Determinant(x))**(S(n - p - 1)/2) + return term1 * term2 * term3 + +def Wishart(symbol, n, scale_matrix): + """ + Creates a random variable with Wishart Distribution. + + The density of the said distribution can be found at [1]. + + Parameters + ========== + + n: Positive Real number + Represents degrees of freedom + scale_matrix: Positive definite real square matrix + Scale Matrix + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy.stats import density, Wishart + >>> from sympy import MatrixSymbol, symbols + >>> n = symbols('n', positive=True) + >>> W = Wishart('W', n, [[2, 1], [1, 2]]) + >>> X = MatrixSymbol('X', 2, 2) + >>> density(W)(X).doit() + exp(Trace(Matrix([ + [-1/3, 1/6], + [ 1/6, -1/3]])*X))*Determinant(X)**(n/2 - 3/2)/(2**n*3**(n/2)*sqrt(pi)*gamma(n/2)*gamma(n/2 - 1/2)) + >>> density(W)([[1, 0], [0, 1]]).doit() + exp(-2/3)/(2**n*3**(n/2)*sqrt(pi)*gamma(n/2)*gamma(n/2 - 1/2)) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Wishart_distribution + + """ + if isinstance(scale_matrix, list): + scale_matrix = ImmutableMatrix(scale_matrix) + return rv(symbol, WishartDistribution, (n, scale_matrix)) + +#------------------------------------------------------------------------------- +# Matrix Normal distribution --------------------------------------------------- + +class MatrixNormalDistribution(MatrixDistribution): + + _argnames = ('location_matrix', 'scale_matrix_1', 'scale_matrix_2') + + @staticmethod + def check(location_matrix, scale_matrix_1, scale_matrix_2): + if not isinstance(scale_matrix_1, MatrixSymbol): + _value_check(scale_matrix_1.is_positive_definite, "The shape " + "matrix must be positive definite.") + if not isinstance(scale_matrix_2, MatrixSymbol): + _value_check(scale_matrix_2.is_positive_definite, "The shape " + "matrix must be positive definite.") + _value_check(scale_matrix_1.is_square, "Scale matrix 1 should be " + "be square matrix") + _value_check(scale_matrix_2.is_square, "Scale matrix 2 should be " + "be square matrix") + n = location_matrix.shape[0] + p = location_matrix.shape[1] + _value_check(scale_matrix_1.shape[0] == n, "Scale matrix 1 should be" + " of shape %s x %s"% (str(n), str(n))) + _value_check(scale_matrix_2.shape[0] == p, "Scale matrix 2 should be" + " of shape %s x %s"% (str(p), str(p))) + + @property + def set(self): + n, p = self.location_matrix.shape + return MatrixSet(n, p, S.Reals) + + @property + def dimension(self): + return self.location_matrix.shape + + def pdf(self, x): + M, U, V = self.location_matrix, self.scale_matrix_1, self.scale_matrix_2 + n, p = M.shape + if isinstance(x, list): + x = ImmutableMatrix(x) + if not isinstance(x, (MatrixBase, MatrixSymbol)): + raise ValueError("%s should be an isinstance of Matrix " + "or MatrixSymbol" % str(x)) + term1 = Inverse(V)*Transpose(x - M)*Inverse(U)*(x - M) + num = exp(-Trace(term1)/S(2)) + den = (2*pi)**(S(n*p)/2) * Determinant(U)**(S(p)/2) * Determinant(V)**(S(n)/2) + return num/den + +def MatrixNormal(symbol, location_matrix, scale_matrix_1, scale_matrix_2): + """ + Creates a random variable with Matrix Normal Distribution. + + The density of the said distribution can be found at [1]. + + Parameters + ========== + + location_matrix: Real ``n x p`` matrix + Represents degrees of freedom + scale_matrix_1: Positive definite matrix + Scale Matrix of shape ``n x n`` + scale_matrix_2: Positive definite matrix + Scale Matrix of shape ``p x p`` + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy import MatrixSymbol + >>> from sympy.stats import density, MatrixNormal + >>> M = MatrixNormal('M', [[1, 2]], [1], [[1, 0], [0, 1]]) + >>> X = MatrixSymbol('X', 1, 2) + >>> density(M)(X).doit() + exp(-Trace((Matrix([ + [-1], + [-2]]) + X.T)*(Matrix([[-1, -2]]) + X))/2)/(2*pi) + >>> density(M)([[3, 4]]).doit() + exp(-4)/(2*pi) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Matrix_normal_distribution + + """ + if isinstance(location_matrix, list): + location_matrix = ImmutableMatrix(location_matrix) + if isinstance(scale_matrix_1, list): + scale_matrix_1 = ImmutableMatrix(scale_matrix_1) + if isinstance(scale_matrix_2, list): + scale_matrix_2 = ImmutableMatrix(scale_matrix_2) + args = (location_matrix, scale_matrix_1, scale_matrix_2) + return rv(symbol, MatrixNormalDistribution, args) + +#------------------------------------------------------------------------------- +# Matrix Student's T distribution --------------------------------------------------- + +class MatrixStudentTDistribution(MatrixDistribution): + + _argnames = ('nu', 'location_matrix', 'scale_matrix_1', 'scale_matrix_2') + + @staticmethod + def check(nu, location_matrix, scale_matrix_1, scale_matrix_2): + if not isinstance(scale_matrix_1, MatrixSymbol): + _value_check(scale_matrix_1.is_positive_definite != False, "The shape " + "matrix must be positive definite.") + if not isinstance(scale_matrix_2, MatrixSymbol): + _value_check(scale_matrix_2.is_positive_definite != False, "The shape " + "matrix must be positive definite.") + _value_check(scale_matrix_1.is_square != False, "Scale matrix 1 should be " + "be square matrix") + _value_check(scale_matrix_2.is_square != False, "Scale matrix 2 should be " + "be square matrix") + n = location_matrix.shape[0] + p = location_matrix.shape[1] + _value_check(scale_matrix_1.shape[0] == p, "Scale matrix 1 should be" + " of shape %s x %s" % (str(p), str(p))) + _value_check(scale_matrix_2.shape[0] == n, "Scale matrix 2 should be" + " of shape %s x %s" % (str(n), str(n))) + _value_check(nu.is_positive != False, "Degrees of freedom must be positive") + + @property + def set(self): + n, p = self.location_matrix.shape + return MatrixSet(n, p, S.Reals) + + @property + def dimension(self): + return self.location_matrix.shape + + def pdf(self, x): + from sympy.matrices.dense import eye + if isinstance(x, list): + x = ImmutableMatrix(x) + if not isinstance(x, (MatrixBase, MatrixSymbol)): + raise ValueError("%s should be an isinstance of Matrix " + "or MatrixSymbol" % str(x)) + nu, M, Omega, Sigma = self.nu, self.location_matrix, self.scale_matrix_1, self.scale_matrix_2 + n, p = M.shape + + K = multigamma((nu + n + p - 1)/2, p) * Determinant(Omega)**(-n/2) * Determinant(Sigma)**(-p/2) \ + / ((pi)**(n*p/2) * multigamma((nu + p - 1)/2, p)) + return K * (Determinant(eye(n) + Inverse(Sigma)*(x - M)*Inverse(Omega)*Transpose(x - M))) \ + **(-(nu + n + p -1)/2) + + + +def MatrixStudentT(symbol, nu, location_matrix, scale_matrix_1, scale_matrix_2): + """ + Creates a random variable with Matrix Gamma Distribution. + + The density of the said distribution can be found at [1]. + + Parameters + ========== + + nu: Positive Real number + degrees of freedom + location_matrix: Positive definite real square matrix + Location Matrix of shape ``n x p`` + scale_matrix_1: Positive definite real square matrix + Scale Matrix of shape ``p x p`` + scale_matrix_2: Positive definite real square matrix + Scale Matrix of shape ``n x n`` + + Returns + ======= + + RandomSymbol + + Examples + ======== + + >>> from sympy import MatrixSymbol,symbols + >>> from sympy.stats import density, MatrixStudentT + >>> v = symbols('v',positive=True) + >>> M = MatrixStudentT('M', v, [[1, 2]], [[1, 0], [0, 1]], [1]) + >>> X = MatrixSymbol('X', 1, 2) + >>> density(M)(X) + gamma(v/2 + 1)*Determinant((Matrix([[-1, -2]]) + X)*(Matrix([ + [-1], + [-2]]) + X.T) + Matrix([[1]]))**(-v/2 - 1)/(pi**1.0*gamma(v/2)*Determinant(Matrix([[1]]))**1.0*Determinant(Matrix([ + [1, 0], + [0, 1]]))**0.5) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Matrix_t-distribution + + """ + if isinstance(location_matrix, list): + location_matrix = ImmutableMatrix(location_matrix) + if isinstance(scale_matrix_1, list): + scale_matrix_1 = ImmutableMatrix(scale_matrix_1) + if isinstance(scale_matrix_2, list): + scale_matrix_2 = ImmutableMatrix(scale_matrix_2) + args = (nu, location_matrix, scale_matrix_1, scale_matrix_2) + return rv(symbol, MatrixStudentTDistribution, args) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/random_matrix.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/random_matrix.py new file mode 100644 index 0000000000000000000000000000000000000000..fdd25cb9ad23fed9d3a85982b24bef33d04928f0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/random_matrix.py @@ -0,0 +1,30 @@ +from sympy.core.basic import Basic +from sympy.stats.rv import PSpace, _symbol_converter, RandomMatrixSymbol + +class RandomMatrixPSpace(PSpace): + """ + Represents probability space for + random matrices. It contains the mechanics + for handling the API calls for random matrices. + """ + def __new__(cls, sym, model=None): + sym = _symbol_converter(sym) + if model: + return Basic.__new__(cls, sym, model) + else: + return Basic.__new__(cls, sym) + + @property + def model(self): + try: + return self.args[1] + except IndexError: + return None + + def compute_density(self, expr, *args): + rms = expr.atoms(RandomMatrixSymbol) + if len(rms) > 2 or (not isinstance(expr, RandomMatrixSymbol)): + raise NotImplementedError("Currently, no algorithm has been " + "implemented to handle general expressions containing " + "multiple random matrices.") + return self.model.density(expr) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/random_matrix_models.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/random_matrix_models.py new file mode 100644 index 0000000000000000000000000000000000000000..6327a248ea5919c0bbb0ffc2c984105e04fe20e9 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/random_matrix_models.py @@ -0,0 +1,457 @@ +from sympy.concrete.products import Product +from sympy.concrete.summations import Sum +from sympy.core.basic import Basic +from sympy.core.function import Lambda +from sympy.core.numbers import (I, pi) +from sympy.core.singleton import S +from sympy.core.symbol import Dummy +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.exponential import exp +from sympy.functions.special.gamma_functions import gamma +from sympy.integrals.integrals import Integral +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.matrices.expressions.trace import Trace +from sympy.tensor.indexed import IndexedBase +from sympy.core.sympify import _sympify +from sympy.stats.rv import _symbol_converter, Density, RandomMatrixSymbol, is_random +from sympy.stats.joint_rv_types import JointDistributionHandmade +from sympy.stats.random_matrix import RandomMatrixPSpace +from sympy.tensor.array import ArrayComprehension + +__all__ = [ + 'CircularEnsemble', + 'CircularUnitaryEnsemble', + 'CircularOrthogonalEnsemble', + 'CircularSymplecticEnsemble', + 'GaussianEnsemble', + 'GaussianUnitaryEnsemble', + 'GaussianOrthogonalEnsemble', + 'GaussianSymplecticEnsemble', + 'joint_eigen_distribution', + 'JointEigenDistribution', + 'level_spacing_distribution' +] + +@is_random.register(RandomMatrixSymbol) +def _(x): + return True + + +class RandomMatrixEnsembleModel(Basic): + """ + Base class for random matrix ensembles. + It acts as an umbrella and contains + the methods common to all the ensembles + defined in sympy.stats.random_matrix_models. + """ + def __new__(cls, sym, dim=None): + sym, dim = _symbol_converter(sym), _sympify(dim) + if dim.is_integer == False: + raise ValueError("Dimension of the random matrices must be " + "integers, received %s instead."%(dim)) + return Basic.__new__(cls, sym, dim) + + symbol = property(lambda self: self.args[0]) + dimension = property(lambda self: self.args[1]) + + def density(self, expr): + return Density(expr) + + def __call__(self, expr): + return self.density(expr) + +class GaussianEnsembleModel(RandomMatrixEnsembleModel): + """ + Abstract class for Gaussian ensembles. + Contains the properties common to all the + gaussian ensembles. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Random_matrix#Gaussian_ensembles + .. [2] https://arxiv.org/pdf/1712.07903.pdf + """ + def _compute_normalization_constant(self, beta, n): + """ + Helper function for computing normalization + constant for joint probability density of eigen + values of Gaussian ensembles. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Selberg_integral#Mehta's_integral + """ + n = S(n) + prod_term = lambda j: gamma(1 + beta*S(j)/2)/gamma(S.One + beta/S(2)) + j = Dummy('j', integer=True, positive=True) + term1 = Product(prod_term(j), (j, 1, n)).doit() + term2 = (2/(beta*n))**(beta*n*(n - 1)/4 + n/2) + term3 = (2*pi)**(n/2) + return term1 * term2 * term3 + + def _compute_joint_eigen_distribution(self, beta): + """ + Helper function for computing the joint + probability distribution of eigen values + of the random matrix. + """ + n = self.dimension + Zbn = self._compute_normalization_constant(beta, n) + l = IndexedBase('l') + i = Dummy('i', integer=True, positive=True) + j = Dummy('j', integer=True, positive=True) + k = Dummy('k', integer=True, positive=True) + term1 = exp((-S(n)/2) * Sum(l[k]**2, (k, 1, n)).doit()) + sub_term = Lambda(i, Product(Abs(l[j] - l[i])**beta, (j, i + 1, n))) + term2 = Product(sub_term(i).doit(), (i, 1, n - 1)).doit() + syms = ArrayComprehension(l[k], (k, 1, n)).doit() + return Lambda(tuple(syms), (term1 * term2)/Zbn) + +class GaussianUnitaryEnsembleModel(GaussianEnsembleModel): + @property + def normalization_constant(self): + n = self.dimension + return 2**(S(n)/2) * pi**(S(n**2)/2) + + def density(self, expr): + n, ZGUE = self.dimension, self.normalization_constant + h_pspace = RandomMatrixPSpace('P', model=self) + H = RandomMatrixSymbol('H', n, n, pspace=h_pspace) + return Lambda(H, exp(-S(n)/2 * Trace(H**2))/ZGUE)(expr) + + def joint_eigen_distribution(self): + return self._compute_joint_eigen_distribution(S(2)) + + def level_spacing_distribution(self): + s = Dummy('s') + f = (32/pi**2)*(s**2)*exp((-4/pi)*s**2) + return Lambda(s, f) + +class GaussianOrthogonalEnsembleModel(GaussianEnsembleModel): + @property + def normalization_constant(self): + n = self.dimension + _H = MatrixSymbol('_H', n, n) + return Integral(exp(-S(n)/4 * Trace(_H**2))) + + def density(self, expr): + n, ZGOE = self.dimension, self.normalization_constant + h_pspace = RandomMatrixPSpace('P', model=self) + H = RandomMatrixSymbol('H', n, n, pspace=h_pspace) + return Lambda(H, exp(-S(n)/4 * Trace(H**2))/ZGOE)(expr) + + def joint_eigen_distribution(self): + return self._compute_joint_eigen_distribution(S.One) + + def level_spacing_distribution(self): + s = Dummy('s') + f = (pi/2)*s*exp((-pi/4)*s**2) + return Lambda(s, f) + +class GaussianSymplecticEnsembleModel(GaussianEnsembleModel): + @property + def normalization_constant(self): + n = self.dimension + _H = MatrixSymbol('_H', n, n) + return Integral(exp(-S(n) * Trace(_H**2))) + + def density(self, expr): + n, ZGSE = self.dimension, self.normalization_constant + h_pspace = RandomMatrixPSpace('P', model=self) + H = RandomMatrixSymbol('H', n, n, pspace=h_pspace) + return Lambda(H, exp(-S(n) * Trace(H**2))/ZGSE)(expr) + + def joint_eigen_distribution(self): + return self._compute_joint_eigen_distribution(S(4)) + + def level_spacing_distribution(self): + s = Dummy('s') + f = ((S(2)**18)/((S(3)**6)*(pi**3)))*(s**4)*exp((-64/(9*pi))*s**2) + return Lambda(s, f) + +def GaussianEnsemble(sym, dim): + sym, dim = _symbol_converter(sym), _sympify(dim) + model = GaussianEnsembleModel(sym, dim) + rmp = RandomMatrixPSpace(sym, model=model) + return RandomMatrixSymbol(sym, dim, dim, pspace=rmp) + +def GaussianUnitaryEnsemble(sym, dim): + """ + Represents Gaussian Unitary Ensembles. + + Examples + ======== + + >>> from sympy.stats import GaussianUnitaryEnsemble as GUE, density + >>> from sympy import MatrixSymbol + >>> G = GUE('U', 2) + >>> X = MatrixSymbol('X', 2, 2) + >>> density(G)(X) + exp(-Trace(X**2))/(2*pi**2) + """ + sym, dim = _symbol_converter(sym), _sympify(dim) + model = GaussianUnitaryEnsembleModel(sym, dim) + rmp = RandomMatrixPSpace(sym, model=model) + return RandomMatrixSymbol(sym, dim, dim, pspace=rmp) + +def GaussianOrthogonalEnsemble(sym, dim): + """ + Represents Gaussian Orthogonal Ensembles. + + Examples + ======== + + >>> from sympy.stats import GaussianOrthogonalEnsemble as GOE, density + >>> from sympy import MatrixSymbol + >>> G = GOE('U', 2) + >>> X = MatrixSymbol('X', 2, 2) + >>> density(G)(X) + exp(-Trace(X**2)/2)/Integral(exp(-Trace(_H**2)/2), _H) + """ + sym, dim = _symbol_converter(sym), _sympify(dim) + model = GaussianOrthogonalEnsembleModel(sym, dim) + rmp = RandomMatrixPSpace(sym, model=model) + return RandomMatrixSymbol(sym, dim, dim, pspace=rmp) + +def GaussianSymplecticEnsemble(sym, dim): + """ + Represents Gaussian Symplectic Ensembles. + + Examples + ======== + + >>> from sympy.stats import GaussianSymplecticEnsemble as GSE, density + >>> from sympy import MatrixSymbol + >>> G = GSE('U', 2) + >>> X = MatrixSymbol('X', 2, 2) + >>> density(G)(X) + exp(-2*Trace(X**2))/Integral(exp(-2*Trace(_H**2)), _H) + """ + sym, dim = _symbol_converter(sym), _sympify(dim) + model = GaussianSymplecticEnsembleModel(sym, dim) + rmp = RandomMatrixPSpace(sym, model=model) + return RandomMatrixSymbol(sym, dim, dim, pspace=rmp) + +class CircularEnsembleModel(RandomMatrixEnsembleModel): + """ + Abstract class for Circular ensembles. + Contains the properties and methods + common to all the circular ensembles. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Circular_ensemble + """ + def density(self, expr): + # TODO : Add support for Lie groups(as extensions of sympy.diffgeom) + # and define measures on them + raise NotImplementedError("Support for Haar measure hasn't been " + "implemented yet, therefore the density of " + "%s cannot be computed."%(self)) + + def _compute_joint_eigen_distribution(self, beta): + """ + Helper function to compute the joint distribution of phases + of the complex eigen values of matrices belonging to any + circular ensembles. + """ + n = self.dimension + Zbn = ((2*pi)**n)*(gamma(beta*n/2 + 1)/S(gamma(beta/2 + 1))**n) + t = IndexedBase('t') + i, j, k = (Dummy('i', integer=True), Dummy('j', integer=True), + Dummy('k', integer=True)) + syms = ArrayComprehension(t[i], (i, 1, n)).doit() + f = Product(Product(Abs(exp(I*t[k]) - exp(I*t[j]))**beta, (j, k + 1, n)).doit(), + (k, 1, n - 1)).doit() + return Lambda(tuple(syms), f/Zbn) + +class CircularUnitaryEnsembleModel(CircularEnsembleModel): + def joint_eigen_distribution(self): + return self._compute_joint_eigen_distribution(S(2)) + +class CircularOrthogonalEnsembleModel(CircularEnsembleModel): + def joint_eigen_distribution(self): + return self._compute_joint_eigen_distribution(S.One) + +class CircularSymplecticEnsembleModel(CircularEnsembleModel): + def joint_eigen_distribution(self): + return self._compute_joint_eigen_distribution(S(4)) + +def CircularEnsemble(sym, dim): + sym, dim = _symbol_converter(sym), _sympify(dim) + model = CircularEnsembleModel(sym, dim) + rmp = RandomMatrixPSpace(sym, model=model) + return RandomMatrixSymbol(sym, dim, dim, pspace=rmp) + +def CircularUnitaryEnsemble(sym, dim): + """ + Represents Circular Unitary Ensembles. + + Examples + ======== + + >>> from sympy.stats import CircularUnitaryEnsemble as CUE + >>> from sympy.stats import joint_eigen_distribution + >>> C = CUE('U', 1) + >>> joint_eigen_distribution(C) + Lambda(t[1], Product(Abs(exp(I*t[_j]) - exp(I*t[_k]))**2, (_j, _k + 1, 1), (_k, 1, 0))/(2*pi)) + + Note + ==== + + As can be seen above in the example, density of CiruclarUnitaryEnsemble + is not evaluated because the exact definition is based on haar measure of + unitary group which is not unique. + """ + sym, dim = _symbol_converter(sym), _sympify(dim) + model = CircularUnitaryEnsembleModel(sym, dim) + rmp = RandomMatrixPSpace(sym, model=model) + return RandomMatrixSymbol(sym, dim, dim, pspace=rmp) + +def CircularOrthogonalEnsemble(sym, dim): + """ + Represents Circular Orthogonal Ensembles. + + Examples + ======== + + >>> from sympy.stats import CircularOrthogonalEnsemble as COE + >>> from sympy.stats import joint_eigen_distribution + >>> C = COE('O', 1) + >>> joint_eigen_distribution(C) + Lambda(t[1], Product(Abs(exp(I*t[_j]) - exp(I*t[_k])), (_j, _k + 1, 1), (_k, 1, 0))/(2*pi)) + + Note + ==== + + As can be seen above in the example, density of CiruclarOrthogonalEnsemble + is not evaluated because the exact definition is based on haar measure of + unitary group which is not unique. + """ + sym, dim = _symbol_converter(sym), _sympify(dim) + model = CircularOrthogonalEnsembleModel(sym, dim) + rmp = RandomMatrixPSpace(sym, model=model) + return RandomMatrixSymbol(sym, dim, dim, pspace=rmp) + +def CircularSymplecticEnsemble(sym, dim): + """ + Represents Circular Symplectic Ensembles. + + Examples + ======== + + >>> from sympy.stats import CircularSymplecticEnsemble as CSE + >>> from sympy.stats import joint_eigen_distribution + >>> C = CSE('S', 1) + >>> joint_eigen_distribution(C) + Lambda(t[1], Product(Abs(exp(I*t[_j]) - exp(I*t[_k]))**4, (_j, _k + 1, 1), (_k, 1, 0))/(2*pi)) + + Note + ==== + + As can be seen above in the example, density of CiruclarSymplecticEnsemble + is not evaluated because the exact definition is based on haar measure of + unitary group which is not unique. + """ + sym, dim = _symbol_converter(sym), _sympify(dim) + model = CircularSymplecticEnsembleModel(sym, dim) + rmp = RandomMatrixPSpace(sym, model=model) + return RandomMatrixSymbol(sym, dim, dim, pspace=rmp) + +def joint_eigen_distribution(mat): + """ + For obtaining joint probability distribution + of eigen values of random matrix. + + Parameters + ========== + + mat: RandomMatrixSymbol + The matrix symbol whose eigen values are to be considered. + + Returns + ======= + + Lambda + + Examples + ======== + + >>> from sympy.stats import GaussianUnitaryEnsemble as GUE + >>> from sympy.stats import joint_eigen_distribution + >>> U = GUE('U', 2) + >>> joint_eigen_distribution(U) + Lambda((l[1], l[2]), exp(-l[1]**2 - l[2]**2)*Product(Abs(l[_i] - l[_j])**2, (_j, _i + 1, 2), (_i, 1, 1))/pi) + """ + if not isinstance(mat, RandomMatrixSymbol): + raise ValueError("%s is not of type, RandomMatrixSymbol."%(mat)) + return mat.pspace.model.joint_eigen_distribution() + +def JointEigenDistribution(mat): + """ + Creates joint distribution of eigen values of matrices with random + expressions. + + Parameters + ========== + + mat: Matrix + The matrix under consideration. + + Returns + ======= + + JointDistributionHandmade + + Examples + ======== + + >>> from sympy.stats import Normal, JointEigenDistribution + >>> from sympy import Matrix + >>> A = [[Normal('A00', 0, 1), Normal('A01', 0, 1)], + ... [Normal('A10', 0, 1), Normal('A11', 0, 1)]] + >>> JointEigenDistribution(Matrix(A)) + JointDistributionHandmade(-sqrt(A00**2 - 2*A00*A11 + 4*A01*A10 + A11**2)/2 + + A00/2 + A11/2, sqrt(A00**2 - 2*A00*A11 + 4*A01*A10 + A11**2)/2 + A00/2 + A11/2) + + """ + eigenvals = mat.eigenvals(multiple=True) + if not all(is_random(eigenval) for eigenval in set(eigenvals)): + raise ValueError("Eigen values do not have any random expression, " + "joint distribution cannot be generated.") + return JointDistributionHandmade(*eigenvals) + +def level_spacing_distribution(mat): + """ + For obtaining distribution of level spacings. + + Parameters + ========== + + mat: RandomMatrixSymbol + The random matrix symbol whose eigen values are + to be considered for finding the level spacings. + + Returns + ======= + + Lambda + + Examples + ======== + + >>> from sympy.stats import GaussianUnitaryEnsemble as GUE + >>> from sympy.stats import level_spacing_distribution + >>> U = GUE('U', 2) + >>> level_spacing_distribution(U) + Lambda(_s, 32*_s**2*exp(-4*_s**2/pi)/pi**2) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Random_matrix#Distribution_of_level_spacings + """ + return mat.pspace.model.level_spacing_distribution() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/rv.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/rv.py new file mode 100644 index 0000000000000000000000000000000000000000..75ab54deb551b7ff3d4d06f37482a1f16a789ba6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/rv.py @@ -0,0 +1,1798 @@ +""" +Main Random Variables Module + +Defines abstract random variable type. +Contains interfaces for probability space object (PSpace) as well as standard +operators, P, E, sample, density, where, quantile + +See Also +======== + +sympy.stats.crv +sympy.stats.frv +sympy.stats.rv_interface +""" + +from __future__ import annotations +from functools import singledispatch +from math import prod + +from sympy.core.add import Add +from sympy.core.basic import Basic +from sympy.core.containers import Tuple +from sympy.core.expr import Expr +from sympy.core.function import (Function, Lambda) +from sympy.core.logic import fuzzy_and +from sympy.core.mul import Mul +from sympy.core.relational import (Eq, Ne) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol) +from sympy.core.sympify import sympify +from sympy.functions.special.delta_functions import DiracDelta +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.logic.boolalg import (And, Or) +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.tensor.indexed import Indexed +from sympy.utilities.lambdify import lambdify +from sympy.core.relational import Relational +from sympy.core.sympify import _sympify +from sympy.sets.sets import FiniteSet, ProductSet, Intersection +from sympy.solvers.solveset import solveset +from sympy.external import import_module +from sympy.utilities.decorator import doctest_depends_on +from sympy.utilities.exceptions import sympy_deprecation_warning +from sympy.utilities.iterables import iterable + + +__doctest_requires__ = {('sample',): ['scipy']} + + +x = Symbol('x') + + +@singledispatch +def is_random(x): + return False + + +@is_random.register(Basic) +def _(x): + atoms = x.free_symbols + return any(is_random(i) for i in atoms) + + +class RandomDomain(Basic): + """ + Represents a set of variables and the values which they can take. + + See Also + ======== + + sympy.stats.crv.ContinuousDomain + sympy.stats.frv.FiniteDomain + """ + + is_ProductDomain = False + is_Finite = False + is_Continuous = False + is_Discrete = False + + def __new__(cls, symbols, *args): + symbols = FiniteSet(*symbols) + return Basic.__new__(cls, symbols, *args) + + @property + def symbols(self): + return self.args[0] + + @property + def set(self): + return self.args[1] + + def __contains__(self, other): + raise NotImplementedError() + + def compute_expectation(self, expr): + raise NotImplementedError() + + +class SingleDomain(RandomDomain): + """ + A single variable and its domain. + + See Also + ======== + + sympy.stats.crv.SingleContinuousDomain + sympy.stats.frv.SingleFiniteDomain + """ + def __new__(cls, symbol, set): + assert symbol.is_Symbol + return Basic.__new__(cls, symbol, set) + + @property + def symbol(self): + return self.args[0] + + @property + def symbols(self): + return FiniteSet(self.symbol) + + def __contains__(self, other): + if len(other) != 1: + return False + sym, val = tuple(other)[0] + return self.symbol == sym and val in self.set + + +class MatrixDomain(RandomDomain): + """ + A Random Matrix variable and its domain. + + """ + def __new__(cls, symbol, set): + symbol, set = _symbol_converter(symbol), _sympify(set) + return Basic.__new__(cls, symbol, set) + + @property + def symbol(self): + return self.args[0] + + @property + def symbols(self): + return FiniteSet(self.symbol) + + +class ConditionalDomain(RandomDomain): + """ + A RandomDomain with an attached condition. + + See Also + ======== + + sympy.stats.crv.ConditionalContinuousDomain + sympy.stats.frv.ConditionalFiniteDomain + """ + def __new__(cls, fulldomain, condition): + condition = condition.xreplace({rs: rs.symbol + for rs in random_symbols(condition)}) + return Basic.__new__(cls, fulldomain, condition) + + @property + def symbols(self): + return self.fulldomain.symbols + + @property + def fulldomain(self): + return self.args[0] + + @property + def condition(self): + return self.args[1] + + @property + def set(self): + raise NotImplementedError("Set of Conditional Domain not Implemented") + + def as_boolean(self): + return And(self.fulldomain.as_boolean(), self.condition) + + +class PSpace(Basic): + """ + A Probability Space. + + Explanation + =========== + + Probability Spaces encode processes that equal different values + probabilistically. These underly Random Symbols which occur in SymPy + expressions and contain the mechanics to evaluate statistical statements. + + See Also + ======== + + sympy.stats.crv.ContinuousPSpace + sympy.stats.frv.FinitePSpace + """ + + is_Finite: bool | None = None # Fails test if not set to None + is_Continuous: bool | None = None # Fails test if not set to None + is_Discrete: bool | None = None # Fails test if not set to None + is_real: bool | None + + @property + def domain(self): + return self.args[0] + + @property + def density(self): + return self.args[1] + + @property + def values(self): + return frozenset(RandomSymbol(sym, self) for sym in self.symbols) + + @property + def symbols(self): + return self.domain.symbols + + def where(self, condition): + raise NotImplementedError() + + def compute_density(self, expr): + raise NotImplementedError() + + def sample(self, size=(), library='scipy', seed=None): + raise NotImplementedError() + + def probability(self, condition): + raise NotImplementedError() + + def compute_expectation(self, expr): + raise NotImplementedError() + + +class SinglePSpace(PSpace): + """ + Represents the probabilities of a set of random events that can be + attributed to a single variable/symbol. + """ + def __new__(cls, s, distribution): + s = _symbol_converter(s) + return Basic.__new__(cls, s, distribution) + + @property + def value(self): + return RandomSymbol(self.symbol, self) + + @property + def symbol(self): + return self.args[0] + + @property + def distribution(self): + return self.args[1] + + @property + def pdf(self): + return self.distribution.pdf(self.symbol) + + +class RandomSymbol(Expr): + """ + Random Symbols represent ProbabilitySpaces in SymPy Expressions. + In principle they can take on any value that their symbol can take on + within the associated PSpace with probability determined by the PSpace + Density. + + Explanation + =========== + + Random Symbols contain pspace and symbol properties. + The pspace property points to the represented Probability Space + The symbol is a standard SymPy Symbol that is used in that probability space + for example in defining a density. + + You can form normal SymPy expressions using RandomSymbols and operate on + those expressions with the Functions + + E - Expectation of a random expression + P - Probability of a condition + density - Probability Density of an expression + given - A new random expression (with new random symbols) given a condition + + An object of the RandomSymbol type should almost never be created by the + user. They tend to be created instead by the PSpace class's value method. + Traditionally a user does not even do this but instead calls one of the + convenience functions Normal, Exponential, Coin, Die, FiniteRV, etc.... + """ + + def __new__(cls, symbol, pspace=None): + from sympy.stats.joint_rv import JointRandomSymbol + if pspace is None: + # Allow single arg, representing pspace == PSpace() + pspace = PSpace() + symbol = _symbol_converter(symbol) + if not isinstance(pspace, PSpace): + raise TypeError("pspace variable should be of type PSpace") + if cls == JointRandomSymbol and isinstance(pspace, SinglePSpace): + cls = RandomSymbol + return Basic.__new__(cls, symbol, pspace) + + is_finite = True + is_symbol = True + is_Atom = True + + _diff_wrt = True + + pspace = property(lambda self: self.args[1]) + symbol = property(lambda self: self.args[0]) + name = property(lambda self: self.symbol.name) + + def _eval_is_positive(self): + return self.symbol.is_positive + + def _eval_is_integer(self): + return self.symbol.is_integer + + def _eval_is_real(self): + return self.symbol.is_real or self.pspace.is_real + + @property + def is_commutative(self): + return self.symbol.is_commutative + + @property + def free_symbols(self): + return {self} + +class RandomIndexedSymbol(RandomSymbol): + + def __new__(cls, idx_obj, pspace=None): + if pspace is None: + # Allow single arg, representing pspace == PSpace() + pspace = PSpace() + if not isinstance(idx_obj, (Indexed, Function)): + raise TypeError("An Function or Indexed object is expected not %s"%(idx_obj)) + return Basic.__new__(cls, idx_obj, pspace) + + symbol = property(lambda self: self.args[0]) + name = property(lambda self: str(self.args[0])) + + @property + def key(self): + if isinstance(self.symbol, Indexed): + return self.symbol.args[1] + elif isinstance(self.symbol, Function): + return self.symbol.args[0] + + @property + def free_symbols(self): + if self.key.free_symbols: + free_syms = self.key.free_symbols + free_syms.add(self) + return free_syms + return {self} + + @property + def pspace(self): + return self.args[1] + +class RandomMatrixSymbol(RandomSymbol, MatrixSymbol): # type: ignore + def __new__(cls, symbol, n, m, pspace=None): + n, m = _sympify(n), _sympify(m) + symbol = _symbol_converter(symbol) + if pspace is None: + # Allow single arg, representing pspace == PSpace() + pspace = PSpace() + return Basic.__new__(cls, symbol, n, m, pspace) + + symbol = property(lambda self: self.args[0]) + pspace = property(lambda self: self.args[3]) + + +class ProductPSpace(PSpace): + """ + Abstract class for representing probability spaces with multiple random + variables. + + See Also + ======== + + sympy.stats.rv.IndependentProductPSpace + sympy.stats.joint_rv.JointPSpace + """ + pass + +class IndependentProductPSpace(ProductPSpace): + """ + A probability space resulting from the merger of two independent probability + spaces. + + Often created using the function, pspace. + """ + + def __new__(cls, *spaces): + rs_space_dict = {} + for space in spaces: + for value in space.values: + rs_space_dict[value] = space + + symbols = FiniteSet(*[val.symbol for val in rs_space_dict.keys()]) + + # Overlapping symbols + from sympy.stats.joint_rv import MarginalDistribution + from sympy.stats.compound_rv import CompoundDistribution + if len(symbols) < sum(len(space.symbols) for space in spaces if not + isinstance(space.distribution, ( + CompoundDistribution, MarginalDistribution))): + raise ValueError("Overlapping Random Variables") + + if all(space.is_Finite for space in spaces): + from sympy.stats.frv import ProductFinitePSpace + cls = ProductFinitePSpace + + obj = Basic.__new__(cls, *FiniteSet(*spaces)) + + return obj + + @property + def pdf(self): + p = Mul(*[space.pdf for space in self.spaces]) + return p.subs({rv: rv.symbol for rv in self.values}) + + @property + def rs_space_dict(self): + d = {} + for space in self.spaces: + for value in space.values: + d[value] = space + return d + + @property + def symbols(self): + return FiniteSet(*[val.symbol for val in self.rs_space_dict.keys()]) + + @property + def spaces(self): + return FiniteSet(*self.args) + + @property + def values(self): + return sumsets(space.values for space in self.spaces) + + def compute_expectation(self, expr, rvs=None, evaluate=False, **kwargs): + rvs = rvs or self.values + rvs = frozenset(rvs) + for space in self.spaces: + expr = space.compute_expectation(expr, rvs & space.values, evaluate=False, **kwargs) + if evaluate and hasattr(expr, 'doit'): + return expr.doit(**kwargs) + return expr + + @property + def domain(self): + return ProductDomain(*[space.domain for space in self.spaces]) + + @property + def density(self): + raise NotImplementedError("Density not available for ProductSpaces") + + def sample(self, size=(), library='scipy', seed=None): + return {k: v for space in self.spaces + for k, v in space.sample(size=size, library=library, seed=seed).items()} + + + def probability(self, condition, **kwargs): + cond_inv = False + if isinstance(condition, Ne): + condition = Eq(condition.args[0], condition.args[1]) + cond_inv = True + elif isinstance(condition, And): # they are independent + return Mul(*[self.probability(arg) for arg in condition.args]) + elif isinstance(condition, Or): # they are independent + return Add(*[self.probability(arg) for arg in condition.args]) + expr = condition.lhs - condition.rhs + rvs = random_symbols(expr) + dens = self.compute_density(expr) + if any(pspace(rv).is_Continuous for rv in rvs): + from sympy.stats.crv import SingleContinuousPSpace + from sympy.stats.crv_types import ContinuousDistributionHandmade + if expr in self.values: + # Marginalize all other random symbols out of the density + randomsymbols = tuple(set(self.values) - frozenset([expr])) + symbols = tuple(rs.symbol for rs in randomsymbols) + pdf = self.domain.integrate(self.pdf, symbols, **kwargs) + return Lambda(expr.symbol, pdf) + dens = ContinuousDistributionHandmade(dens) + z = Dummy('z', real=True) + space = SingleContinuousPSpace(z, dens) + result = space.probability(condition.__class__(space.value, 0)) + else: + from sympy.stats.drv import SingleDiscretePSpace + from sympy.stats.drv_types import DiscreteDistributionHandmade + dens = DiscreteDistributionHandmade(dens) + z = Dummy('z', integer=True) + space = SingleDiscretePSpace(z, dens) + result = space.probability(condition.__class__(space.value, 0)) + return result if not cond_inv else S.One - result + + def compute_density(self, expr, **kwargs): + rvs = random_symbols(expr) + if any(pspace(rv).is_Continuous for rv in rvs): + z = Dummy('z', real=True) + expr = self.compute_expectation(DiracDelta(expr - z), + **kwargs) + else: + z = Dummy('z', integer=True) + expr = self.compute_expectation(KroneckerDelta(expr, z), + **kwargs) + return Lambda(z, expr) + + def compute_cdf(self, expr, **kwargs): + raise ValueError("CDF not well defined on multivariate expressions") + + def conditional_space(self, condition, normalize=True, **kwargs): + rvs = random_symbols(condition) + condition = condition.xreplace({rv: rv.symbol for rv in self.values}) + pspaces = [pspace(rv) for rv in rvs] + if any(ps.is_Continuous for ps in pspaces): + from sympy.stats.crv import (ConditionalContinuousDomain, + ContinuousPSpace) + space = ContinuousPSpace + domain = ConditionalContinuousDomain(self.domain, condition) + elif any(ps.is_Discrete for ps in pspaces): + from sympy.stats.drv import (ConditionalDiscreteDomain, + DiscretePSpace) + space = DiscretePSpace + domain = ConditionalDiscreteDomain(self.domain, condition) + elif all(ps.is_Finite for ps in pspaces): + from sympy.stats.frv import FinitePSpace + return FinitePSpace.conditional_space(self, condition) + if normalize: + replacement = {rv: Dummy(str(rv)) for rv in self.symbols} + norm = domain.compute_expectation(self.pdf, **kwargs) + pdf = self.pdf / norm.xreplace(replacement) + # XXX: Converting symbols from set to tuple. The order matters to + # Lambda though so we shouldn't be starting with a set here... + density = Lambda(tuple(domain.symbols), pdf) + + return space(domain, density) + +class ProductDomain(RandomDomain): + """ + A domain resulting from the merger of two independent domains. + + See Also + ======== + sympy.stats.crv.ProductContinuousDomain + sympy.stats.frv.ProductFiniteDomain + """ + is_ProductDomain = True + + def __new__(cls, *domains): + # Flatten any product of products + domains2 = [] + for domain in domains: + if not domain.is_ProductDomain: + domains2.append(domain) + else: + domains2.extend(domain.domains) + domains2 = FiniteSet(*domains2) + + if all(domain.is_Finite for domain in domains2): + from sympy.stats.frv import ProductFiniteDomain + cls = ProductFiniteDomain + if all(domain.is_Continuous for domain in domains2): + from sympy.stats.crv import ProductContinuousDomain + cls = ProductContinuousDomain + if all(domain.is_Discrete for domain in domains2): + from sympy.stats.drv import ProductDiscreteDomain + cls = ProductDiscreteDomain + + return Basic.__new__(cls, *domains2) + + @property + def sym_domain_dict(self): + return {symbol: domain for domain in self.domains + for symbol in domain.symbols} + + @property + def symbols(self): + return FiniteSet(*[sym for domain in self.domains + for sym in domain.symbols]) + + @property + def domains(self): + return self.args + + @property + def set(self): + return ProductSet(*(domain.set for domain in self.domains)) + + def __contains__(self, other): + # Split event into each subdomain + for domain in self.domains: + # Collect the parts of this event which associate to this domain + elem = frozenset([item for item in other + if sympify(domain.symbols.contains(item[0])) + is S.true]) + # Test this sub-event + if elem not in domain: + return False + # All subevents passed + return True + + def as_boolean(self): + return And(*[domain.as_boolean() for domain in self.domains]) + + +def random_symbols(expr): + """ + Returns all RandomSymbols within a SymPy Expression. + """ + atoms = getattr(expr, 'atoms', None) + if atoms is not None: + comp = lambda rv: rv.symbol.name + l = list(atoms(RandomSymbol)) + return sorted(l, key=comp) + else: + return [] + + +def pspace(expr): + """ + Returns the underlying Probability Space of a random expression. + + For internal use. + + Examples + ======== + + >>> from sympy.stats import pspace, Normal + >>> X = Normal('X', 0, 1) + >>> pspace(2*X + 1) == X.pspace + True + """ + expr = sympify(expr) + if isinstance(expr, RandomSymbol) and expr.pspace is not None: + return expr.pspace + if expr.has(RandomMatrixSymbol): + rm = list(expr.atoms(RandomMatrixSymbol))[0] + return rm.pspace + + rvs = random_symbols(expr) + if not rvs: + raise ValueError("Expression containing Random Variable expected, not %s" % (expr)) + # If only one space present + if all(rv.pspace == rvs[0].pspace for rv in rvs): + return rvs[0].pspace + from sympy.stats.compound_rv import CompoundPSpace + from sympy.stats.stochastic_process import StochasticPSpace + for rv in rvs: + if isinstance(rv.pspace, (CompoundPSpace, StochasticPSpace)): + return rv.pspace + # Otherwise make a product space + return IndependentProductPSpace(*[rv.pspace for rv in rvs]) + + +def sumsets(sets): + """ + Union of sets + """ + return frozenset().union(*sets) + + +def rs_swap(a, b): + """ + Build a dictionary to swap RandomSymbols based on their underlying symbol. + + i.e. + if ``X = ('x', pspace1)`` + and ``Y = ('x', pspace2)`` + then ``X`` and ``Y`` match and the key, value pair + ``{X:Y}`` will appear in the result + + Inputs: collections a and b of random variables which share common symbols + Output: dict mapping RVs in a to RVs in b + """ + d = {} + for rsa in a: + d[rsa] = [rsb for rsb in b if rsa.symbol == rsb.symbol][0] + return d + + +def given(expr, condition=None, **kwargs): + r""" Conditional Random Expression. + + Explanation + =========== + + From a random expression and a condition on that expression creates a new + probability space from the condition and returns the same expression on that + conditional probability space. + + Examples + ======== + + >>> from sympy.stats import given, density, Die + >>> X = Die('X', 6) + >>> Y = given(X, X > 3) + >>> density(Y).dict + {4: 1/3, 5: 1/3, 6: 1/3} + + Following convention, if the condition is a random symbol then that symbol + is considered fixed. + + >>> from sympy.stats import Normal + >>> from sympy import pprint + >>> from sympy.abc import z + + >>> X = Normal('X', 0, 1) + >>> Y = Normal('Y', 0, 1) + >>> pprint(density(X + Y, Y)(z), use_unicode=False) + 2 + -(-Y + z) + ----------- + ___ 2 + \/ 2 *e + ------------------ + ____ + 2*\/ pi + """ + + if not is_random(condition) or pspace_independent(expr, condition): + return expr + + if isinstance(condition, RandomSymbol): + condition = Eq(condition, condition.symbol) + + condsymbols = random_symbols(condition) + if (isinstance(condition, Eq) and len(condsymbols) == 1 and + not isinstance(pspace(expr).domain, ConditionalDomain)): + rv = tuple(condsymbols)[0] + + results = solveset(condition, rv) + if isinstance(results, Intersection) and S.Reals in results.args: + results = list(results.args[1]) + + sums = 0 + for res in results: + temp = expr.subs(rv, res) + if temp == True: + return True + if temp != False: + # XXX: This seems nonsensical but preserves existing behaviour + # after the change that Relational is no longer a subclass of + # Expr. Here expr is sometimes Relational and sometimes Expr + # but we are trying to add them with +=. This needs to be + # fixed somehow. + if sums == 0 and isinstance(expr, Relational): + sums = expr.subs(rv, res) + else: + sums += expr.subs(rv, res) + if sums == 0: + return False + return sums + + # Get full probability space of both the expression and the condition + fullspace = pspace(Tuple(expr, condition)) + # Build new space given the condition + space = fullspace.conditional_space(condition, **kwargs) + # Dictionary to swap out RandomSymbols in expr with new RandomSymbols + # That point to the new conditional space + swapdict = rs_swap(fullspace.values, space.values) + # Swap random variables in the expression + expr = expr.xreplace(swapdict) + return expr + + +def expectation(expr, condition=None, numsamples=None, evaluate=True, **kwargs): + """ + Returns the expected value of a random expression. + + Parameters + ========== + + expr : Expr containing RandomSymbols + The expression of which you want to compute the expectation value + given : Expr containing RandomSymbols + A conditional expression. E(X, X>0) is expectation of X given X > 0 + numsamples : int + Enables sampling and approximates the expectation with this many samples + evalf : Bool (defaults to True) + If sampling return a number rather than a complex expression + evaluate : Bool (defaults to True) + In case of continuous systems return unevaluated integral + + Examples + ======== + + >>> from sympy.stats import E, Die + >>> X = Die('X', 6) + >>> E(X) + 7/2 + >>> E(2*X + 1) + 8 + + >>> E(X, X > 3) # Expectation of X given that it is above 3 + 5 + """ + + if not is_random(expr): # expr isn't random? + return expr + kwargs['numsamples'] = numsamples + from sympy.stats.symbolic_probability import Expectation + if evaluate: + return Expectation(expr, condition).doit(**kwargs) + return Expectation(expr, condition) + + +def probability(condition, given_condition=None, numsamples=None, + evaluate=True, **kwargs): + """ + Probability that a condition is true, optionally given a second condition. + + Parameters + ========== + + condition : Combination of Relationals containing RandomSymbols + The condition of which you want to compute the probability + given_condition : Combination of Relationals containing RandomSymbols + A conditional expression. P(X > 1, X > 0) is expectation of X > 1 + given X > 0 + numsamples : int + Enables sampling and approximates the probability with this many samples + evaluate : Bool (defaults to True) + In case of continuous systems return unevaluated integral + + Examples + ======== + + >>> from sympy.stats import P, Die + >>> from sympy import Eq + >>> X, Y = Die('X', 6), Die('Y', 6) + >>> P(X > 3) + 1/2 + >>> P(Eq(X, 5), X > 2) # Probability that X == 5 given that X > 2 + 1/4 + >>> P(X > Y) + 5/12 + """ + + kwargs['numsamples'] = numsamples + from sympy.stats.symbolic_probability import Probability + if evaluate: + return Probability(condition, given_condition).doit(**kwargs) + return Probability(condition, given_condition) + + +class Density(Basic): + expr = property(lambda self: self.args[0]) + + def __new__(cls, expr, condition = None): + expr = _sympify(expr) + if condition is None: + obj = Basic.__new__(cls, expr) + else: + condition = _sympify(condition) + obj = Basic.__new__(cls, expr, condition) + return obj + + @property + def condition(self): + if len(self.args) > 1: + return self.args[1] + else: + return None + + def doit(self, evaluate=True, **kwargs): + from sympy.stats.random_matrix import RandomMatrixPSpace + from sympy.stats.joint_rv import JointPSpace + from sympy.stats.matrix_distributions import MatrixPSpace + from sympy.stats.compound_rv import CompoundPSpace + from sympy.stats.frv import SingleFiniteDistribution + expr, condition = self.expr, self.condition + + if isinstance(expr, SingleFiniteDistribution): + return expr.dict + if condition is not None: + # Recompute on new conditional expr + expr = given(expr, condition, **kwargs) + if not random_symbols(expr): + return Lambda(x, DiracDelta(x - expr)) + if isinstance(expr, RandomSymbol): + if isinstance(expr.pspace, (SinglePSpace, JointPSpace, MatrixPSpace)) and \ + hasattr(expr.pspace, 'distribution'): + return expr.pspace.distribution + elif isinstance(expr.pspace, RandomMatrixPSpace): + return expr.pspace.model + if isinstance(pspace(expr), CompoundPSpace): + kwargs['compound_evaluate'] = evaluate + result = pspace(expr).compute_density(expr, **kwargs) + + if evaluate and hasattr(result, 'doit'): + return result.doit() + else: + return result + + +def density(expr, condition=None, evaluate=True, numsamples=None, **kwargs): + """ + Probability density of a random expression, optionally given a second + condition. + + Explanation + =========== + + This density will take on different forms for different types of + probability spaces. Discrete variables produce Dicts. Continuous + variables produce Lambdas. + + Parameters + ========== + + expr : Expr containing RandomSymbols + The expression of which you want to compute the density value + condition : Relational containing RandomSymbols + A conditional expression. density(X > 1, X > 0) is density of X > 1 + given X > 0 + numsamples : int + Enables sampling and approximates the density with this many samples + + Examples + ======== + + >>> from sympy.stats import density, Die, Normal + >>> from sympy import Symbol + + >>> x = Symbol('x') + >>> D = Die('D', 6) + >>> X = Normal(x, 0, 1) + + >>> density(D).dict + {1: 1/6, 2: 1/6, 3: 1/6, 4: 1/6, 5: 1/6, 6: 1/6} + >>> density(2*D).dict + {2: 1/6, 4: 1/6, 6: 1/6, 8: 1/6, 10: 1/6, 12: 1/6} + >>> density(X)(x) + sqrt(2)*exp(-x**2/2)/(2*sqrt(pi)) + """ + + if numsamples: + return sampling_density(expr, condition, numsamples=numsamples, + **kwargs) + + return Density(expr, condition).doit(evaluate=evaluate, **kwargs) + + +def cdf(expr, condition=None, evaluate=True, **kwargs): + """ + Cumulative Distribution Function of a random expression. + + optionally given a second condition. + + Explanation + =========== + + This density will take on different forms for different types of + probability spaces. + Discrete variables produce Dicts. + Continuous variables produce Lambdas. + + Examples + ======== + + >>> from sympy.stats import density, Die, Normal, cdf + + >>> D = Die('D', 6) + >>> X = Normal('X', 0, 1) + + >>> density(D).dict + {1: 1/6, 2: 1/6, 3: 1/6, 4: 1/6, 5: 1/6, 6: 1/6} + >>> cdf(D) + {1: 1/6, 2: 1/3, 3: 1/2, 4: 2/3, 5: 5/6, 6: 1} + >>> cdf(3*D, D > 2) + {9: 1/4, 12: 1/2, 15: 3/4, 18: 1} + + >>> cdf(X) + Lambda(_z, erf(sqrt(2)*_z/2)/2 + 1/2) + """ + if condition is not None: # If there is a condition + # Recompute on new conditional expr + return cdf(given(expr, condition, **kwargs), **kwargs) + + # Otherwise pass work off to the ProbabilitySpace + result = pspace(expr).compute_cdf(expr, **kwargs) + + if evaluate and hasattr(result, 'doit'): + return result.doit() + else: + return result + + +def characteristic_function(expr, condition=None, evaluate=True, **kwargs): + """ + Characteristic function of a random expression, optionally given a second condition. + + Returns a Lambda. + + Examples + ======== + + >>> from sympy.stats import Normal, DiscreteUniform, Poisson, characteristic_function + + >>> X = Normal('X', 0, 1) + >>> characteristic_function(X) + Lambda(_t, exp(-_t**2/2)) + + >>> Y = DiscreteUniform('Y', [1, 2, 7]) + >>> characteristic_function(Y) + Lambda(_t, exp(7*_t*I)/3 + exp(2*_t*I)/3 + exp(_t*I)/3) + + >>> Z = Poisson('Z', 2) + >>> characteristic_function(Z) + Lambda(_t, exp(2*exp(_t*I) - 2)) + """ + if condition is not None: + return characteristic_function(given(expr, condition, **kwargs), **kwargs) + + result = pspace(expr).compute_characteristic_function(expr, **kwargs) + + if evaluate and hasattr(result, 'doit'): + return result.doit() + else: + return result + +def moment_generating_function(expr, condition=None, evaluate=True, **kwargs): + if condition is not None: + return moment_generating_function(given(expr, condition, **kwargs), **kwargs) + + result = pspace(expr).compute_moment_generating_function(expr, **kwargs) + + if evaluate and hasattr(result, 'doit'): + return result.doit() + else: + return result + +def where(condition, given_condition=None, **kwargs): + """ + Returns the domain where a condition is True. + + Examples + ======== + + >>> from sympy.stats import where, Die, Normal + >>> from sympy import And + + >>> D1, D2 = Die('a', 6), Die('b', 6) + >>> a, b = D1.symbol, D2.symbol + >>> X = Normal('x', 0, 1) + + >>> where(X**2<1) + Domain: (-1 < x) & (x < 1) + + >>> where(X**2<1).set + Interval.open(-1, 1) + + >>> where(And(D1<=D2, D2<3)) + Domain: (Eq(a, 1) & Eq(b, 1)) | (Eq(a, 1) & Eq(b, 2)) | (Eq(a, 2) & Eq(b, 2)) + """ + if given_condition is not None: # If there is a condition + # Recompute on new conditional expr + return where(given(condition, given_condition, **kwargs), **kwargs) + + # Otherwise pass work off to the ProbabilitySpace + return pspace(condition).where(condition, **kwargs) + + +@doctest_depends_on(modules=('scipy',)) +def sample(expr, condition=None, size=(), library='scipy', + numsamples=1, seed=None, **kwargs): + """ + A realization of the random expression. + + Parameters + ========== + + expr : Expression of random variables + Expression from which sample is extracted + condition : Expr containing RandomSymbols + A conditional expression + size : int, tuple + Represents size of each sample in numsamples + library : str + - 'scipy' : Sample using scipy + - 'numpy' : Sample using numpy + - 'pymc' : Sample using PyMC + + Choose any of the available options to sample from as string, + by default is 'scipy' + numsamples : int + Number of samples, each with size as ``size``. + + .. deprecated:: 1.9 + + The ``numsamples`` parameter is deprecated and is only provided for + compatibility with v1.8. Use a list comprehension or an additional + dimension in ``size`` instead. See + :ref:`deprecated-sympy-stats-numsamples` for details. + + seed : + An object to be used as seed by the given external library for sampling `expr`. + Following is the list of possible types of object for the supported libraries, + + - 'scipy': int, numpy.random.RandomState, numpy.random.Generator + - 'numpy': int, numpy.random.RandomState, numpy.random.Generator + - 'pymc': int + + Optional, by default None, in which case seed settings + related to the given library will be used. + No modifications to environment's global seed settings + are done by this argument. + + Returns + ======= + + sample: float/list/numpy.ndarray + one sample or a collection of samples of the random expression. + + - sample(X) returns float/numpy.float64/numpy.int64 object. + - sample(X, size=int/tuple) returns numpy.ndarray object. + + Examples + ======== + + >>> from sympy.stats import Die, sample, Normal, Geometric + >>> X, Y, Z = Die('X', 6), Die('Y', 6), Die('Z', 6) # Finite Random Variable + >>> die_roll = sample(X + Y + Z) + >>> die_roll # doctest: +SKIP + 3 + >>> N = Normal('N', 3, 4) # Continuous Random Variable + >>> samp = sample(N) + >>> samp in N.pspace.domain.set + True + >>> samp = sample(N, N>0) + >>> samp > 0 + True + >>> samp_list = sample(N, size=4) + >>> [sam in N.pspace.domain.set for sam in samp_list] + [True, True, True, True] + >>> sample(N, size = (2,3)) # doctest: +SKIP + array([[5.42519758, 6.40207856, 4.94991743], + [1.85819627, 6.83403519, 1.9412172 ]]) + >>> G = Geometric('G', 0.5) # Discrete Random Variable + >>> samp_list = sample(G, size=3) + >>> samp_list # doctest: +SKIP + [1, 3, 2] + >>> [sam in G.pspace.domain.set for sam in samp_list] + [True, True, True] + >>> MN = Normal("MN", [3, 4], [[2, 1], [1, 2]]) # Joint Random Variable + >>> samp_list = sample(MN, size=4) + >>> samp_list # doctest: +SKIP + [array([2.85768055, 3.38954165]), + array([4.11163337, 4.3176591 ]), + array([0.79115232, 1.63232916]), + array([4.01747268, 3.96716083])] + >>> [tuple(sam) in MN.pspace.domain.set for sam in samp_list] + [True, True, True, True] + + .. versionchanged:: 1.7.0 + sample used to return an iterator containing the samples instead of value. + + .. versionchanged:: 1.9.0 + sample returns values or array of values instead of an iterator and numsamples is deprecated. + + """ + + iterator = sample_iter(expr, condition, size=size, library=library, + numsamples=numsamples, seed=seed) + + if numsamples != 1: + sympy_deprecation_warning( + f""" + The numsamples parameter to sympy.stats.sample() is deprecated. + Either use a list comprehension, like + + [sample(...) for i in range({numsamples})] + + or add a dimension to size, like + + sample(..., size={(numsamples,) + size}) + """, + deprecated_since_version="1.9", + active_deprecations_target="deprecated-sympy-stats-numsamples", + ) + return [next(iterator) for i in range(numsamples)] + + return next(iterator) + + +def quantile(expr, evaluate=True, **kwargs): + r""" + Return the :math:`p^{th}` order quantile of a probability distribution. + + Explanation + =========== + + Quantile is defined as the value at which the probability of the random + variable is less than or equal to the given probability. + + .. math:: + Q(p) = \inf\{x \in (-\infty, \infty) : p \le F(x)\} + + Examples + ======== + + >>> from sympy.stats import quantile, Die, Exponential + >>> from sympy import Symbol, pprint + >>> p = Symbol("p") + + >>> l = Symbol("lambda", positive=True) + >>> X = Exponential("x", l) + >>> quantile(X)(p) + -log(1 - p)/lambda + + >>> D = Die("d", 6) + >>> pprint(quantile(D)(p), use_unicode=False) + /nan for Or(p > 1, p < 0) + | + | 1 for p <= 1/6 + | + | 2 for p <= 1/3 + | + < 3 for p <= 1/2 + | + | 4 for p <= 2/3 + | + | 5 for p <= 5/6 + | + \ 6 for p <= 1 + + """ + result = pspace(expr).compute_quantile(expr, **kwargs) + + if evaluate and hasattr(result, 'doit'): + return result.doit() + else: + return result + +def sample_iter(expr, condition=None, size=(), library='scipy', + numsamples=S.Infinity, seed=None, **kwargs): + + """ + Returns an iterator of realizations from the expression given a condition. + + Parameters + ========== + + expr: Expr + Random expression to be realized + condition: Expr, optional + A conditional expression + size : int, tuple + Represents size of each sample in numsamples + numsamples: integer, optional + Length of the iterator (defaults to infinity) + seed : + An object to be used as seed by the given external library for sampling `expr`. + Following is the list of possible types of object for the supported libraries, + + - 'scipy': int, numpy.random.RandomState, numpy.random.Generator + - 'numpy': int, numpy.random.RandomState, numpy.random.Generator + - 'pymc': int + + Optional, by default None, in which case seed settings + related to the given library will be used. + No modifications to environment's global seed settings + are done by this argument. + + Examples + ======== + + >>> from sympy.stats import Normal, sample_iter + >>> X = Normal('X', 0, 1) + >>> expr = X*X + 3 + >>> iterator = sample_iter(expr, numsamples=3) # doctest: +SKIP + >>> list(iterator) # doctest: +SKIP + [12, 4, 7] + + Returns + ======= + + sample_iter: iterator object + iterator object containing the sample/samples of given expr + + See Also + ======== + + sample + sampling_P + sampling_E + + """ + from sympy.stats.joint_rv import JointRandomSymbol + if not import_module(library): + raise ValueError("Failed to import %s" % library) + + if condition is not None: + ps = pspace(Tuple(expr, condition)) + else: + ps = pspace(expr) + + rvs = list(ps.values) + if isinstance(expr, JointRandomSymbol): + expr = expr.subs({expr: RandomSymbol(expr.symbol, expr.pspace)}) + else: + sub = {} + for arg in expr.args: + if isinstance(arg, JointRandomSymbol): + sub[arg] = RandomSymbol(arg.symbol, arg.pspace) + expr = expr.subs(sub) + + def fn_subs(*args): + return expr.subs(dict(zip(rvs, args))) + + def given_fn_subs(*args): + if condition is not None: + return condition.subs(dict(zip(rvs, args))) + return False + + if library in ('pymc', 'pymc3'): + # Currently unable to lambdify in pymc + # TODO : Remove when lambdify accepts 'pymc' as module + fn = lambdify(rvs, expr, **kwargs) + else: + fn = lambdify(rvs, expr, modules=library, **kwargs) + + + if condition is not None: + given_fn = lambdify(rvs, condition, **kwargs) + + def return_generator_infinite(): + count = 0 + _size = (1,)+((size,) if isinstance(size, int) else size) + while count < numsamples: + d = ps.sample(size=_size, library=library, seed=seed) # a dictionary that maps RVs to values + args = [d[rv][0] for rv in rvs] + + if condition is not None: # Check that these values satisfy the condition + # TODO: Replace the try-except block with only given_fn(*args) + # once lambdify works with unevaluated SymPy objects. + try: + gd = given_fn(*args) + except (NameError, TypeError): + gd = given_fn_subs(*args) + if gd != True and gd != False: + raise ValueError( + "Conditions must not contain free symbols") + if not gd: # If the values don't satisfy then try again + continue + + yield fn(*args) + count += 1 + + def return_generator_finite(): + faulty = True + while faulty: + d = ps.sample(size=(numsamples,) + ((size,) if isinstance(size, int) else size), + library=library, seed=seed) # a dictionary that maps RVs to values + + faulty = False + count = 0 + while count < numsamples and not faulty: + args = [d[rv][count] for rv in rvs] + if condition is not None: # Check that these values satisfy the condition + # TODO: Replace the try-except block with only given_fn(*args) + # once lambdify works with unevaluated SymPy objects. + try: + gd = given_fn(*args) + except (NameError, TypeError): + gd = given_fn_subs(*args) + if gd != True and gd != False: + raise ValueError( + "Conditions must not contain free symbols") + if not gd: # If the values don't satisfy then try again + faulty = True + + count += 1 + + count = 0 + while count < numsamples: + args = [d[rv][count] for rv in rvs] + # TODO: Replace the try-except block with only fn(*args) + # once lambdify works with unevaluated SymPy objects. + try: + yield fn(*args) + except (NameError, TypeError): + yield fn_subs(*args) + count += 1 + + if numsamples is S.Infinity: + return return_generator_infinite() + + return return_generator_finite() + +def sample_iter_lambdify(expr, condition=None, size=(), + numsamples=S.Infinity, seed=None, **kwargs): + + return sample_iter(expr, condition=condition, size=size, + numsamples=numsamples, seed=seed, **kwargs) + +def sample_iter_subs(expr, condition=None, size=(), + numsamples=S.Infinity, seed=None, **kwargs): + + return sample_iter(expr, condition=condition, size=size, + numsamples=numsamples, seed=seed, **kwargs) + + +def sampling_P(condition, given_condition=None, library='scipy', numsamples=1, + evalf=True, seed=None, **kwargs): + """ + Sampling version of P. + + See Also + ======== + + P + sampling_E + sampling_density + + """ + + count_true = 0 + count_false = 0 + samples = sample_iter(condition, given_condition, library=library, + numsamples=numsamples, seed=seed, **kwargs) + + for sample in samples: + if sample: + count_true += 1 + else: + count_false += 1 + + result = S(count_true) / numsamples + if evalf: + return result.evalf() + else: + return result + + +def sampling_E(expr, given_condition=None, library='scipy', numsamples=1, + evalf=True, seed=None, **kwargs): + """ + Sampling version of E. + + See Also + ======== + + P + sampling_P + sampling_density + """ + samples = list(sample_iter(expr, given_condition, library=library, + numsamples=numsamples, seed=seed, **kwargs)) + result = Add(*samples) / numsamples + + if evalf: + return result.evalf() + else: + return result + +def sampling_density(expr, given_condition=None, library='scipy', + numsamples=1, seed=None, **kwargs): + """ + Sampling version of density. + + See Also + ======== + density + sampling_P + sampling_E + """ + + results = {} + for result in sample_iter(expr, given_condition, library=library, + numsamples=numsamples, seed=seed, **kwargs): + results[result] = results.get(result, 0) + 1 + + return results + + +def dependent(a, b): + """ + Dependence of two random expressions. + + Two expressions are independent if knowledge of one does not change + computations on the other. + + Examples + ======== + + >>> from sympy.stats import Normal, dependent, given + >>> from sympy import Tuple, Eq + + >>> X, Y = Normal('X', 0, 1), Normal('Y', 0, 1) + >>> dependent(X, Y) + False + >>> dependent(2*X + Y, -Y) + True + >>> X, Y = given(Tuple(X, Y), Eq(X + Y, 3)) + >>> dependent(X, Y) + True + + See Also + ======== + + independent + """ + if pspace_independent(a, b): + return False + + z = Symbol('z', real=True) + # Dependent if density is unchanged when one is given information about + # the other + return (density(a, Eq(b, z)) != density(a) or + density(b, Eq(a, z)) != density(b)) + + +def independent(a, b): + """ + Independence of two random expressions. + + Two expressions are independent if knowledge of one does not change + computations on the other. + + Examples + ======== + + >>> from sympy.stats import Normal, independent, given + >>> from sympy import Tuple, Eq + + >>> X, Y = Normal('X', 0, 1), Normal('Y', 0, 1) + >>> independent(X, Y) + True + >>> independent(2*X + Y, -Y) + False + >>> X, Y = given(Tuple(X, Y), Eq(X + Y, 3)) + >>> independent(X, Y) + False + + See Also + ======== + + dependent + """ + return not dependent(a, b) + + +def pspace_independent(a, b): + """ + Tests for independence between a and b by checking if their PSpaces have + overlapping symbols. This is a sufficient but not necessary condition for + independence and is intended to be used internally. + + Notes + ===== + + pspace_independent(a, b) implies independent(a, b) + independent(a, b) does not imply pspace_independent(a, b) + """ + a_symbols = set(pspace(b).symbols) + b_symbols = set(pspace(a).symbols) + + if len(set(random_symbols(a)).intersection(random_symbols(b))) != 0: + return False + + if len(a_symbols.intersection(b_symbols)) == 0: + return True + return None + + +def rv_subs(expr, symbols=None): + """ + Given a random expression replace all random variables with their symbols. + + If symbols keyword is given restrict the swap to only the symbols listed. + """ + if symbols is None: + symbols = random_symbols(expr) + if not symbols: + return expr + swapdict = {rv: rv.symbol for rv in symbols} + return expr.subs(swapdict) + + +class NamedArgsMixin: + _argnames: tuple[str, ...] = () + + def __getattr__(self, attr): + try: + return self.args[self._argnames.index(attr)] + except ValueError: + raise AttributeError("'%s' object has no attribute '%s'" % ( + type(self).__name__, attr)) + + +class Distribution(Basic): + + def sample(self, size=(), library='scipy', seed=None): + """ A random realization from the distribution """ + + module = import_module(library) + if library in {'scipy', 'numpy', 'pymc3', 'pymc'} and module is None: + raise ValueError("Failed to import %s" % library) + + if library == 'scipy': + # scipy does not require map as it can handle using custom distributions. + # However, we will still use a map where we can. + + # TODO: do this for drv.py and frv.py if necessary. + # TODO: add more distributions here if there are more + # See links below referring to sections beginning with "A common parametrization..." + # I will remove all these comments if everything is ok. + + from sympy.stats.sampling.sample_scipy import do_sample_scipy + import numpy + if seed is None or isinstance(seed, int): + rand_state = numpy.random.default_rng(seed=seed) + else: + rand_state = seed + samps = do_sample_scipy(self, size, rand_state) + + elif library == 'numpy': + from sympy.stats.sampling.sample_numpy import do_sample_numpy + import numpy + if seed is None or isinstance(seed, int): + rand_state = numpy.random.default_rng(seed=seed) + else: + rand_state = seed + _size = None if size == () else size + samps = do_sample_numpy(self, _size, rand_state) + elif library in ('pymc', 'pymc3'): + from sympy.stats.sampling.sample_pymc import do_sample_pymc + import logging + logging.getLogger("pymc").setLevel(logging.ERROR) + try: + import pymc + except ImportError: + import pymc3 as pymc + + with pymc.Model(): + if do_sample_pymc(self) is not None: + samps = pymc.sample(draws=prod(size), chains=1, compute_convergence_checks=False, + progressbar=False, random_seed=seed, return_inferencedata=False)[:]['X'] + samps = samps.reshape(size) + else: + samps = None + + else: + raise NotImplementedError("Sampling from %s is not supported yet." + % str(library)) + + if samps is not None: + return samps + raise NotImplementedError( + "Sampling for %s is not currently implemented from %s" + % (self, library)) + + +def _value_check(condition, message): + """ + Raise a ValueError with message if condition is False, else + return True if all conditions were True, else False. + + Examples + ======== + + >>> from sympy.stats.rv import _value_check + >>> from sympy.abc import a, b, c + >>> from sympy import And, Dummy + + >>> _value_check(2 < 3, '') + True + + Here, the condition is not False, but it does not evaluate to True + so False is returned (but no error is raised). So checking if the + return value is True or False will tell you if all conditions were + evaluated. + + >>> _value_check(a < b, '') + False + + In this case the condition is False so an error is raised: + + >>> r = Dummy(real=True) + >>> _value_check(r < r - 1, 'condition is not true') + Traceback (most recent call last): + ... + ValueError: condition is not true + + If no condition of many conditions must be False, they can be + checked by passing them as an iterable: + + >>> _value_check((a < 0, b < 0, c < 0), '') + False + + The iterable can be a generator, too: + + >>> _value_check((i < 0 for i in (a, b, c)), '') + False + + The following are equivalent to the above but do not pass + an iterable: + + >>> all(_value_check(i < 0, '') for i in (a, b, c)) + False + >>> _value_check(And(a < 0, b < 0, c < 0), '') + False + """ + if not iterable(condition): + condition = [condition] + truth = fuzzy_and(condition) + if truth == False: + raise ValueError(message) + return truth == True + +def _symbol_converter(sym): + """ + Casts the parameter to Symbol if it is 'str' + otherwise no operation is performed on it. + + Parameters + ========== + + sym + The parameter to be converted. + + Returns + ======= + + Symbol + the parameter converted to Symbol. + + Raises + ====== + + TypeError + If the parameter is not an instance of both str and + Symbol. + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.stats.rv import _symbol_converter + >>> s = _symbol_converter('s') + >>> isinstance(s, Symbol) + True + >>> _symbol_converter(1) + Traceback (most recent call last): + ... + TypeError: 1 is neither a Symbol nor a string + >>> r = Symbol('r') + >>> isinstance(r, Symbol) + True + """ + if isinstance(sym, str): + sym = Symbol(sym) + if not isinstance(sym, Symbol): + raise TypeError("%s is neither a Symbol nor a string"%(sym)) + return sym + +def sample_stochastic_process(process): + """ + This function is used to sample from stochastic process. + + Parameters + ========== + + process: StochasticProcess + Process used to extract the samples. It must be an instance of + StochasticProcess + + Examples + ======== + + >>> from sympy.stats import sample_stochastic_process, DiscreteMarkovChain + >>> from sympy import Matrix + >>> T = Matrix([[0.5, 0.2, 0.3],[0.2, 0.5, 0.3],[0.2, 0.3, 0.5]]) + >>> Y = DiscreteMarkovChain("Y", [0, 1, 2], T) + >>> next(sample_stochastic_process(Y)) in Y.state_space + True + >>> next(sample_stochastic_process(Y)) # doctest: +SKIP + 0 + >>> next(sample_stochastic_process(Y)) # doctest: +SKIP + 2 + + Returns + ======= + + sample: iterator object + iterator object containing the sample of given process + + """ + from sympy.stats.stochastic_process_types import StochasticProcess + if not isinstance(process, StochasticProcess): + raise ValueError("Process must be an instance of Stochastic Process") + return process.sample() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/rv_interface.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/rv_interface.py new file mode 100644 index 0000000000000000000000000000000000000000..16d65b83634cdb04ef7e5046175848cdf380434b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/rv_interface.py @@ -0,0 +1,519 @@ +from sympy.sets import FiniteSet +from sympy.core.numbers import Rational +from sympy.core.relational import Eq +from sympy.core.symbol import Dummy +from sympy.functions.combinatorial.factorials import FallingFactorial +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import piecewise_fold +from sympy.integrals.integrals import Integral +from sympy.solvers.solveset import solveset +from .rv import (probability, expectation, density, where, given, pspace, cdf, PSpace, + characteristic_function, sample, sample_iter, random_symbols, independent, dependent, + sampling_density, moment_generating_function, quantile, is_random, + sample_stochastic_process) + + +__all__ = ['P', 'E', 'H', 'density', 'where', 'given', 'sample', 'cdf', + 'characteristic_function', 'pspace', 'sample_iter', 'variance', 'std', + 'skewness', 'kurtosis', 'covariance', 'dependent', 'entropy', 'median', + 'independent', 'random_symbols', 'correlation', 'factorial_moment', + 'moment', 'cmoment', 'sampling_density', 'moment_generating_function', + 'smoment', 'quantile', 'sample_stochastic_process'] + + + +def moment(X, n, c=0, condition=None, *, evaluate=True, **kwargs): + """ + Return the nth moment of a random expression about c. + + .. math:: + moment(X, c, n) = E((X-c)^{n}) + + Default value of c is 0. + + Examples + ======== + + >>> from sympy.stats import Die, moment, E + >>> X = Die('X', 6) + >>> moment(X, 1, 6) + -5/2 + >>> moment(X, 2) + 91/6 + >>> moment(X, 1) == E(X) + True + """ + from sympy.stats.symbolic_probability import Moment + if evaluate: + return Moment(X, n, c, condition).doit() + return Moment(X, n, c, condition).rewrite(Integral) + + +def variance(X, condition=None, **kwargs): + """ + Variance of a random expression. + + .. math:: + variance(X) = E((X-E(X))^{2}) + + Examples + ======== + + >>> from sympy.stats import Die, Bernoulli, variance + >>> from sympy import simplify, Symbol + + >>> X = Die('X', 6) + >>> p = Symbol('p') + >>> B = Bernoulli('B', p, 1, 0) + + >>> variance(2*X) + 35/3 + + >>> simplify(variance(B)) + p*(1 - p) + """ + if is_random(X) and pspace(X) == PSpace(): + from sympy.stats.symbolic_probability import Variance + return Variance(X, condition) + + return cmoment(X, 2, condition, **kwargs) + + +def standard_deviation(X, condition=None, **kwargs): + r""" + Standard Deviation of a random expression + + .. math:: + std(X) = \sqrt(E((X-E(X))^{2})) + + Examples + ======== + + >>> from sympy.stats import Bernoulli, std + >>> from sympy import Symbol, simplify + + >>> p = Symbol('p') + >>> B = Bernoulli('B', p, 1, 0) + + >>> simplify(std(B)) + sqrt(p*(1 - p)) + """ + return sqrt(variance(X, condition, **kwargs)) +std = standard_deviation + +def entropy(expr, condition=None, **kwargs): + """ + Calculates entropy of a probability distribution. + + Parameters + ========== + + expression : the random expression whose entropy is to be calculated + condition : optional, to specify conditions on random expression + b: base of the logarithm, optional + By default, it is taken as Euler's number + + Returns + ======= + + result : Entropy of the expression, a constant + + Examples + ======== + + >>> from sympy.stats import Normal, Die, entropy + >>> X = Normal('X', 0, 1) + >>> entropy(X) + log(2)/2 + 1/2 + log(pi)/2 + + >>> D = Die('D', 4) + >>> entropy(D) + log(4) + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Entropy_%28information_theory%29 + .. [2] https://www.crmarsh.com/static/pdf/Charles_Marsh_Continuous_Entropy.pdf + .. [3] https://kconrad.math.uconn.edu/blurbs/analysis/entropypost.pdf + """ + pdf = density(expr, condition, **kwargs) + base = kwargs.get('b', exp(1)) + if isinstance(pdf, dict): + return sum(-prob*log(prob, base) for prob in pdf.values()) + return expectation(-log(pdf(expr), base)) + +def covariance(X, Y, condition=None, **kwargs): + """ + Covariance of two random expressions. + + Explanation + =========== + + The expectation that the two variables will rise and fall together + + .. math:: + covariance(X,Y) = E((X-E(X)) (Y-E(Y))) + + Examples + ======== + + >>> from sympy.stats import Exponential, covariance + >>> from sympy import Symbol + + >>> rate = Symbol('lambda', positive=True, real=True) + >>> X = Exponential('X', rate) + >>> Y = Exponential('Y', rate) + + >>> covariance(X, X) + lambda**(-2) + >>> covariance(X, Y) + 0 + >>> covariance(X, Y + rate*X) + 1/lambda + """ + if (is_random(X) and pspace(X) == PSpace()) or (is_random(Y) and pspace(Y) == PSpace()): + from sympy.stats.symbolic_probability import Covariance + return Covariance(X, Y, condition) + + return expectation( + (X - expectation(X, condition, **kwargs)) * + (Y - expectation(Y, condition, **kwargs)), + condition, **kwargs) + + +def correlation(X, Y, condition=None, **kwargs): + r""" + Correlation of two random expressions, also known as correlation + coefficient or Pearson's correlation. + + Explanation + =========== + + The normalized expectation that the two variables will rise + and fall together + + .. math:: + correlation(X,Y) = E((X-E(X))(Y-E(Y)) / (\sigma_x \sigma_y)) + + Examples + ======== + + >>> from sympy.stats import Exponential, correlation + >>> from sympy import Symbol + + >>> rate = Symbol('lambda', positive=True, real=True) + >>> X = Exponential('X', rate) + >>> Y = Exponential('Y', rate) + + >>> correlation(X, X) + 1 + >>> correlation(X, Y) + 0 + >>> correlation(X, Y + rate*X) + 1/sqrt(1 + lambda**(-2)) + """ + return covariance(X, Y, condition, **kwargs)/(std(X, condition, **kwargs) + * std(Y, condition, **kwargs)) + + +def cmoment(X, n, condition=None, *, evaluate=True, **kwargs): + """ + Return the nth central moment of a random expression about its mean. + + .. math:: + cmoment(X, n) = E((X - E(X))^{n}) + + Examples + ======== + + >>> from sympy.stats import Die, cmoment, variance + >>> X = Die('X', 6) + >>> cmoment(X, 3) + 0 + >>> cmoment(X, 2) + 35/12 + >>> cmoment(X, 2) == variance(X) + True + """ + from sympy.stats.symbolic_probability import CentralMoment + if evaluate: + return CentralMoment(X, n, condition).doit() + return CentralMoment(X, n, condition).rewrite(Integral) + + +def smoment(X, n, condition=None, **kwargs): + r""" + Return the nth Standardized moment of a random expression. + + .. math:: + smoment(X, n) = E(((X - \mu)/\sigma_X)^{n}) + + Examples + ======== + + >>> from sympy.stats import skewness, Exponential, smoment + >>> from sympy import Symbol + >>> rate = Symbol('lambda', positive=True, real=True) + >>> Y = Exponential('Y', rate) + >>> smoment(Y, 4) + 9 + >>> smoment(Y, 4) == smoment(3*Y, 4) + True + >>> smoment(Y, 3) == skewness(Y) + True + """ + sigma = std(X, condition, **kwargs) + return (1/sigma)**n*cmoment(X, n, condition, **kwargs) + +def skewness(X, condition=None, **kwargs): + r""" + Measure of the asymmetry of the probability distribution. + + Explanation + =========== + + Positive skew indicates that most of the values lie to the right of + the mean. + + .. math:: + skewness(X) = E(((X - E(X))/\sigma_X)^{3}) + + Parameters + ========== + + condition : Expr containing RandomSymbols + A conditional expression. skewness(X, X>0) is skewness of X given X > 0 + + Examples + ======== + + >>> from sympy.stats import skewness, Exponential, Normal + >>> from sympy import Symbol + >>> X = Normal('X', 0, 1) + >>> skewness(X) + 0 + >>> skewness(X, X > 0) # find skewness given X > 0 + (-sqrt(2)/sqrt(pi) + 4*sqrt(2)/pi**(3/2))/(1 - 2/pi)**(3/2) + + >>> rate = Symbol('lambda', positive=True, real=True) + >>> Y = Exponential('Y', rate) + >>> skewness(Y) + 2 + """ + return smoment(X, 3, condition=condition, **kwargs) + +def kurtosis(X, condition=None, **kwargs): + r""" + Characterizes the tails/outliers of a probability distribution. + + Explanation + =========== + + Kurtosis of any univariate normal distribution is 3. Kurtosis less than + 3 means that the distribution produces fewer and less extreme outliers + than the normal distribution. + + .. math:: + kurtosis(X) = E(((X - E(X))/\sigma_X)^{4}) + + Parameters + ========== + + condition : Expr containing RandomSymbols + A conditional expression. kurtosis(X, X>0) is kurtosis of X given X > 0 + + Examples + ======== + + >>> from sympy.stats import kurtosis, Exponential, Normal + >>> from sympy import Symbol + >>> X = Normal('X', 0, 1) + >>> kurtosis(X) + 3 + >>> kurtosis(X, X > 0) # find kurtosis given X > 0 + (-4/pi - 12/pi**2 + 3)/(1 - 2/pi)**2 + + >>> rate = Symbol('lamda', positive=True, real=True) + >>> Y = Exponential('Y', rate) + >>> kurtosis(Y) + 9 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Kurtosis + .. [2] https://mathworld.wolfram.com/Kurtosis.html + """ + return smoment(X, 4, condition=condition, **kwargs) + + +def factorial_moment(X, n, condition=None, **kwargs): + """ + The factorial moment is a mathematical quantity defined as the expectation + or average of the falling factorial of a random variable. + + .. math:: + factorial-moment(X, n) = E(X(X - 1)(X - 2)...(X - n + 1)) + + Parameters + ========== + + n: A natural number, n-th factorial moment. + + condition : Expr containing RandomSymbols + A conditional expression. + + Examples + ======== + + >>> from sympy.stats import factorial_moment, Poisson, Binomial + >>> from sympy import Symbol, S + >>> lamda = Symbol('lamda') + >>> X = Poisson('X', lamda) + >>> factorial_moment(X, 2) + lamda**2 + >>> Y = Binomial('Y', 2, S.Half) + >>> factorial_moment(Y, 2) + 1/2 + >>> factorial_moment(Y, 2, Y > 1) # find factorial moment for Y > 1 + 2 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Factorial_moment + .. [2] https://mathworld.wolfram.com/FactorialMoment.html + """ + return expectation(FallingFactorial(X, n), condition=condition, **kwargs) + +def median(X, evaluate=True, **kwargs): + r""" + Calculates the median of the probability distribution. + + Explanation + =========== + + Mathematically, median of Probability distribution is defined as all those + values of `m` for which the following condition is satisfied + + .. math:: + P(X\leq m) \geq \frac{1}{2} \text{ and} \text{ } P(X\geq m)\geq \frac{1}{2} + + Parameters + ========== + + X: The random expression whose median is to be calculated. + + Returns + ======= + + The FiniteSet or an Interval which contains the median of the + random expression. + + Examples + ======== + + >>> from sympy.stats import Normal, Die, median + >>> N = Normal('N', 3, 1) + >>> median(N) + {3} + >>> D = Die('D') + >>> median(D) + {3, 4} + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Median#Probability_distributions + + """ + if not is_random(X): + return X + + from sympy.stats.crv import ContinuousPSpace + from sympy.stats.drv import DiscretePSpace + from sympy.stats.frv import FinitePSpace + + if isinstance(pspace(X), FinitePSpace): + cdf = pspace(X).compute_cdf(X) + result = [] + for key, value in cdf.items(): + if value>= Rational(1, 2) and (1 - value) + \ + pspace(X).probability(Eq(X, key)) >= Rational(1, 2): + result.append(key) + return FiniteSet(*result) + if isinstance(pspace(X), (ContinuousPSpace, DiscretePSpace)): + cdf = pspace(X).compute_cdf(X) + x = Dummy('x') + result = solveset(piecewise_fold(cdf(x) - Rational(1, 2)), x, pspace(X).set) + return result + raise NotImplementedError("The median of %s is not implemented."%str(pspace(X))) + + +def coskewness(X, Y, Z, condition=None, **kwargs): + r""" + Calculates the co-skewness of three random variables. + + Explanation + =========== + + Mathematically Coskewness is defined as + + .. math:: + coskewness(X,Y,Z)=\frac{E[(X-E[X]) * (Y-E[Y]) * (Z-E[Z])]} {\sigma_{X}\sigma_{Y}\sigma_{Z}} + + Parameters + ========== + + X : RandomSymbol + Random Variable used to calculate coskewness + Y : RandomSymbol + Random Variable used to calculate coskewness + Z : RandomSymbol + Random Variable used to calculate coskewness + condition : Expr containing RandomSymbols + A conditional expression + + Examples + ======== + + >>> from sympy.stats import coskewness, Exponential, skewness + >>> from sympy import symbols + >>> p = symbols('p', positive=True) + >>> X = Exponential('X', p) + >>> Y = Exponential('Y', 2*p) + >>> coskewness(X, Y, Y) + 0 + >>> coskewness(X, Y + X, Y + 2*X) + 16*sqrt(85)/85 + >>> coskewness(X + 2*Y, Y + X, Y + 2*X, X > 3) + 9*sqrt(170)/85 + >>> coskewness(Y, Y, Y) == skewness(Y) + True + >>> coskewness(X, Y + p*X, Y + 2*p*X) + 4/(sqrt(1 + 1/(4*p**2))*sqrt(4 + 1/(4*p**2))) + + Returns + ======= + + coskewness : The coskewness of the three random variables + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Coskewness + + """ + num = expectation((X - expectation(X, condition, **kwargs)) \ + * (Y - expectation(Y, condition, **kwargs)) \ + * (Z - expectation(Z, condition, **kwargs)), condition, **kwargs) + den = std(X, condition, **kwargs) * std(Y, condition, **kwargs) \ + * std(Z, condition, **kwargs) + return num/den + + +P = probability +E = expectation +H = entropy diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/sample_numpy.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/sample_numpy.py new file mode 100644 index 0000000000000000000000000000000000000000..d65417945449ed8b62d2547215d8908f84820a9b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/sample_numpy.py @@ -0,0 +1,105 @@ +from functools import singledispatch + +from sympy.external import import_module +from sympy.stats.crv_types import BetaDistribution, ChiSquaredDistribution, ExponentialDistribution, GammaDistribution, \ + LogNormalDistribution, NormalDistribution, ParetoDistribution, UniformDistribution, FDistributionDistribution, GumbelDistribution, LaplaceDistribution, \ + LogisticDistribution, RayleighDistribution, TriangularDistribution +from sympy.stats.drv_types import GeometricDistribution, PoissonDistribution, ZetaDistribution +from sympy.stats.frv_types import BinomialDistribution, HypergeometricDistribution + + +numpy = import_module('numpy') + + +@singledispatch +def do_sample_numpy(dist, size, rand_state): + return None + + +# CRV: + +@do_sample_numpy.register(BetaDistribution) +def _(dist: BetaDistribution, size, rand_state): + return rand_state.beta(a=float(dist.alpha), b=float(dist.beta), size=size) + + +@do_sample_numpy.register(ChiSquaredDistribution) +def _(dist: ChiSquaredDistribution, size, rand_state): + return rand_state.chisquare(df=float(dist.k), size=size) + + +@do_sample_numpy.register(ExponentialDistribution) +def _(dist: ExponentialDistribution, size, rand_state): + return rand_state.exponential(1 / float(dist.rate), size=size) + +@do_sample_numpy.register(FDistributionDistribution) +def _(dist: FDistributionDistribution, size, rand_state): + return rand_state.f(dfnum = float(dist.d1), dfden = float(dist.d2), size=size) + +@do_sample_numpy.register(GammaDistribution) +def _(dist: GammaDistribution, size, rand_state): + return rand_state.gamma(shape = float(dist.k), scale = float(dist.theta), size=size) + +@do_sample_numpy.register(GumbelDistribution) +def _(dist: GumbelDistribution, size, rand_state): + return rand_state.gumbel(loc = float(dist.mu), scale = float(dist.beta), size=size) + +@do_sample_numpy.register(LaplaceDistribution) +def _(dist: LaplaceDistribution, size, rand_state): + return rand_state.laplace(loc = float(dist.mu), scale = float(dist.b), size=size) + +@do_sample_numpy.register(LogisticDistribution) +def _(dist: LogisticDistribution, size, rand_state): + return rand_state.logistic(loc = float(dist.mu), scale = float(dist.s), size=size) + +@do_sample_numpy.register(LogNormalDistribution) +def _(dist: LogNormalDistribution, size, rand_state): + return rand_state.lognormal(mean = float(dist.mean), sigma = float(dist.std), size=size) + +@do_sample_numpy.register(NormalDistribution) +def _(dist: NormalDistribution, size, rand_state): + return rand_state.normal(loc = float(dist.mean), scale = float(dist.std), size=size) + +@do_sample_numpy.register(RayleighDistribution) +def _(dist: RayleighDistribution, size, rand_state): + return rand_state.rayleigh(scale = float(dist.sigma), size=size) + +@do_sample_numpy.register(ParetoDistribution) +def _(dist: ParetoDistribution, size, rand_state): + return (numpy.random.pareto(a=float(dist.alpha), size=size) + 1) * float(dist.xm) + +@do_sample_numpy.register(TriangularDistribution) +def _(dist: TriangularDistribution, size, rand_state): + return rand_state.triangular(left = float(dist.a), mode = float(dist.b), right = float(dist.c), size=size) + +@do_sample_numpy.register(UniformDistribution) +def _(dist: UniformDistribution, size, rand_state): + return rand_state.uniform(low=float(dist.left), high=float(dist.right), size=size) + + +# DRV: + +@do_sample_numpy.register(GeometricDistribution) +def _(dist: GeometricDistribution, size, rand_state): + return rand_state.geometric(p=float(dist.p), size=size) + + +@do_sample_numpy.register(PoissonDistribution) +def _(dist: PoissonDistribution, size, rand_state): + return rand_state.poisson(lam=float(dist.lamda), size=size) + + +@do_sample_numpy.register(ZetaDistribution) +def _(dist: ZetaDistribution, size, rand_state): + return rand_state.zipf(a=float(dist.s), size=size) + + +# FRV: + +@do_sample_numpy.register(BinomialDistribution) +def _(dist: BinomialDistribution, size, rand_state): + return rand_state.binomial(n=int(dist.n), p=float(dist.p), size=size) + +@do_sample_numpy.register(HypergeometricDistribution) +def _(dist: HypergeometricDistribution, size, rand_state): + return rand_state.hypergeometric(ngood = int(dist.N), nbad = int(dist.m), nsample = int(dist.n), size=size) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/sample_pymc.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/sample_pymc.py new file mode 100644 index 0000000000000000000000000000000000000000..546f02a3092815af2e54b4a164463f62ece7a024 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/sample_pymc.py @@ -0,0 +1,99 @@ +from functools import singledispatch +from sympy.external import import_module +from sympy.stats.crv_types import BetaDistribution, CauchyDistribution, ChiSquaredDistribution, ExponentialDistribution, \ + GammaDistribution, LogNormalDistribution, NormalDistribution, ParetoDistribution, UniformDistribution, \ + GaussianInverseDistribution +from sympy.stats.drv_types import PoissonDistribution, GeometricDistribution, NegativeBinomialDistribution +from sympy.stats.frv_types import BinomialDistribution, BernoulliDistribution + + +try: + import pymc +except ImportError: + pymc = import_module('pymc3') + +@singledispatch +def do_sample_pymc(dist): + return None + + +# CRV: + +@do_sample_pymc.register(BetaDistribution) +def _(dist: BetaDistribution): + return pymc.Beta('X', alpha=float(dist.alpha), beta=float(dist.beta)) + + +@do_sample_pymc.register(CauchyDistribution) +def _(dist: CauchyDistribution): + return pymc.Cauchy('X', alpha=float(dist.x0), beta=float(dist.gamma)) + + +@do_sample_pymc.register(ChiSquaredDistribution) +def _(dist: ChiSquaredDistribution): + return pymc.ChiSquared('X', nu=float(dist.k)) + + +@do_sample_pymc.register(ExponentialDistribution) +def _(dist: ExponentialDistribution): + return pymc.Exponential('X', lam=float(dist.rate)) + + +@do_sample_pymc.register(GammaDistribution) +def _(dist: GammaDistribution): + return pymc.Gamma('X', alpha=float(dist.k), beta=1 / float(dist.theta)) + + +@do_sample_pymc.register(LogNormalDistribution) +def _(dist: LogNormalDistribution): + return pymc.Lognormal('X', mu=float(dist.mean), sigma=float(dist.std)) + + +@do_sample_pymc.register(NormalDistribution) +def _(dist: NormalDistribution): + return pymc.Normal('X', float(dist.mean), float(dist.std)) + + +@do_sample_pymc.register(GaussianInverseDistribution) +def _(dist: GaussianInverseDistribution): + return pymc.Wald('X', mu=float(dist.mean), lam=float(dist.shape)) + + +@do_sample_pymc.register(ParetoDistribution) +def _(dist: ParetoDistribution): + return pymc.Pareto('X', alpha=float(dist.alpha), m=float(dist.xm)) + + +@do_sample_pymc.register(UniformDistribution) +def _(dist: UniformDistribution): + return pymc.Uniform('X', lower=float(dist.left), upper=float(dist.right)) + + +# DRV: + +@do_sample_pymc.register(GeometricDistribution) +def _(dist: GeometricDistribution): + return pymc.Geometric('X', p=float(dist.p)) + + +@do_sample_pymc.register(NegativeBinomialDistribution) +def _(dist: NegativeBinomialDistribution): + return pymc.NegativeBinomial('X', mu=float((dist.p * dist.r) / (1 - dist.p)), + alpha=float(dist.r)) + + +@do_sample_pymc.register(PoissonDistribution) +def _(dist: PoissonDistribution): + return pymc.Poisson('X', mu=float(dist.lamda)) + + +# FRV: + +@do_sample_pymc.register(BernoulliDistribution) +def _(dist: BernoulliDistribution): + return pymc.Bernoulli('X', p=float(dist.p)) + + +@do_sample_pymc.register(BinomialDistribution) +def _(dist: BinomialDistribution): + return pymc.Binomial('X', n=int(dist.n), p=float(dist.p)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/sample_scipy.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/sample_scipy.py new file mode 100644 index 0000000000000000000000000000000000000000..f12508f68844488e9a14b1476005164eb422796e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/sample_scipy.py @@ -0,0 +1,167 @@ +from functools import singledispatch + +from sympy.core.symbol import Dummy +from sympy.functions.elementary.exponential import exp +from sympy.utilities.lambdify import lambdify +from sympy.external import import_module +from sympy.stats import DiscreteDistributionHandmade +from sympy.stats.crv import SingleContinuousDistribution +from sympy.stats.crv_types import ChiSquaredDistribution, ExponentialDistribution, GammaDistribution, \ + LogNormalDistribution, NormalDistribution, ParetoDistribution, UniformDistribution, BetaDistribution, \ + StudentTDistribution, CauchyDistribution +from sympy.stats.drv_types import GeometricDistribution, LogarithmicDistribution, NegativeBinomialDistribution, \ + PoissonDistribution, SkellamDistribution, YuleSimonDistribution, ZetaDistribution +from sympy.stats.frv import SingleFiniteDistribution + + +scipy = import_module("scipy", import_kwargs={'fromlist':['stats']}) + + +@singledispatch +def do_sample_scipy(dist, size, seed): + return None + + +# CRV + +@do_sample_scipy.register(SingleContinuousDistribution) +def _(dist: SingleContinuousDistribution, size, seed): + # if we don't need to make a handmade pdf, we won't + import scipy.stats + + z = Dummy('z') + handmade_pdf = lambdify(z, dist.pdf(z), ['numpy', 'scipy']) + + class scipy_pdf(scipy.stats.rv_continuous): + def _pdf(dist, x): + return handmade_pdf(x) + + scipy_rv = scipy_pdf(a=float(dist.set._inf), + b=float(dist.set._sup), name='scipy_pdf') + return scipy_rv.rvs(size=size, random_state=seed) + + +@do_sample_scipy.register(ChiSquaredDistribution) +def _(dist: ChiSquaredDistribution, size, seed): + # same parametrisation + return scipy.stats.chi2.rvs(df=float(dist.k), size=size, random_state=seed) + + +@do_sample_scipy.register(ExponentialDistribution) +def _(dist: ExponentialDistribution, size, seed): + # https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.expon.html#scipy.stats.expon + return scipy.stats.expon.rvs(scale=1 / float(dist.rate), size=size, random_state=seed) + + +@do_sample_scipy.register(GammaDistribution) +def _(dist: GammaDistribution, size, seed): + # https://stackoverflow.com/questions/42150965/how-to-plot-gamma-distribution-with-alpha-and-beta-parameters-in-python + return scipy.stats.gamma.rvs(a=float(dist.k), scale=float(dist.theta), size=size, random_state=seed) + + +@do_sample_scipy.register(LogNormalDistribution) +def _(dist: LogNormalDistribution, size, seed): + # https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.lognorm.html + return scipy.stats.lognorm.rvs(scale=float(exp(dist.mean)), s=float(dist.std), size=size, random_state=seed) + + +@do_sample_scipy.register(NormalDistribution) +def _(dist: NormalDistribution, size, seed): + return scipy.stats.norm.rvs(loc=float(dist.mean), scale=float(dist.std), size=size, random_state=seed) + + +@do_sample_scipy.register(ParetoDistribution) +def _(dist: ParetoDistribution, size, seed): + # https://stackoverflow.com/questions/42260519/defining-pareto-distribution-in-python-scipy + return scipy.stats.pareto.rvs(b=float(dist.alpha), scale=float(dist.xm), size=size, random_state=seed) + + +@do_sample_scipy.register(StudentTDistribution) +def _(dist: StudentTDistribution, size, seed): + return scipy.stats.t.rvs(df=float(dist.nu), size=size, random_state=seed) + + +@do_sample_scipy.register(UniformDistribution) +def _(dist: UniformDistribution, size, seed): + # https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.uniform.html + return scipy.stats.uniform.rvs(loc=float(dist.left), scale=float(dist.right - dist.left), size=size, random_state=seed) + + +@do_sample_scipy.register(BetaDistribution) +def _(dist: BetaDistribution, size, seed): + # same parametrisation + return scipy.stats.beta.rvs(a=float(dist.alpha), b=float(dist.beta), size=size, random_state=seed) + + +@do_sample_scipy.register(CauchyDistribution) +def _(dist: CauchyDistribution, size, seed): + return scipy.stats.cauchy.rvs(loc=float(dist.x0), scale=float(dist.gamma), size=size, random_state=seed) + + +# DRV: + +@do_sample_scipy.register(DiscreteDistributionHandmade) +def _(dist: DiscreteDistributionHandmade, size, seed): + from scipy.stats import rv_discrete + + z = Dummy('z') + handmade_pmf = lambdify(z, dist.pdf(z), ['numpy', 'scipy']) + + class scipy_pmf(rv_discrete): + def _pmf(dist, x): + return handmade_pmf(x) + + scipy_rv = scipy_pmf(a=float(dist.set._inf), b=float(dist.set._sup), + name='scipy_pmf') + return scipy_rv.rvs(size=size, random_state=seed) + + +@do_sample_scipy.register(GeometricDistribution) +def _(dist: GeometricDistribution, size, seed): + return scipy.stats.geom.rvs(p=float(dist.p), size=size, random_state=seed) + + +@do_sample_scipy.register(LogarithmicDistribution) +def _(dist: LogarithmicDistribution, size, seed): + return scipy.stats.logser.rvs(p=float(dist.p), size=size, random_state=seed) + + +@do_sample_scipy.register(NegativeBinomialDistribution) +def _(dist: NegativeBinomialDistribution, size, seed): + return scipy.stats.nbinom.rvs(n=float(dist.r), p=float(dist.p), size=size, random_state=seed) + + +@do_sample_scipy.register(PoissonDistribution) +def _(dist: PoissonDistribution, size, seed): + return scipy.stats.poisson.rvs(mu=float(dist.lamda), size=size, random_state=seed) + + +@do_sample_scipy.register(SkellamDistribution) +def _(dist: SkellamDistribution, size, seed): + return scipy.stats.skellam.rvs(mu1=float(dist.mu1), mu2=float(dist.mu2), size=size, random_state=seed) + + +@do_sample_scipy.register(YuleSimonDistribution) +def _(dist: YuleSimonDistribution, size, seed): + return scipy.stats.yulesimon.rvs(alpha=float(dist.rho), size=size, random_state=seed) + + +@do_sample_scipy.register(ZetaDistribution) +def _(dist: ZetaDistribution, size, seed): + return scipy.stats.zipf.rvs(a=float(dist.s), size=size, random_state=seed) + + +# FRV: + +@do_sample_scipy.register(SingleFiniteDistribution) +def _(dist: SingleFiniteDistribution, size, seed): + # scipy can handle with custom distributions + + from scipy.stats import rv_discrete + density_ = dist.dict + x, y = [], [] + for k, v in density_.items(): + x.append(int(k)) + y.append(float(v)) + scipy_rv = rv_discrete(name='scipy_rv', values=(x, y)) + return scipy_rv.rvs(size=size, random_state=seed) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/tests/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/tests/test_sample_continuous_rv.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/tests/test_sample_continuous_rv.py new file mode 100644 index 0000000000000000000000000000000000000000..953bb602df5e63da2882ee118de9dbf24b6f7804 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/tests/test_sample_continuous_rv.py @@ -0,0 +1,181 @@ +from sympy.core.numbers import oo +from sympy.core.symbol import Symbol +from sympy.functions.elementary.exponential import exp +from sympy.sets.sets import Interval +from sympy.external import import_module +from sympy.stats import Beta, Chi, Normal, Gamma, Exponential, LogNormal, Pareto, ChiSquared, Uniform, sample, \ + BetaPrime, Cauchy, GammaInverse, GaussianInverse, StudentT, Weibull, density, ContinuousRV, FDistribution, \ + Gumbel, Laplace, Logistic, Rayleigh, Triangular +from sympy.testing.pytest import skip, raises + + +def test_sample_numpy(): + distribs_numpy = [ + Beta("B", 1, 1), + Normal("N", 0, 1), + Gamma("G", 2, 7), + Exponential("E", 2), + LogNormal("LN", 0, 1), + Pareto("P", 1, 1), + ChiSquared("CS", 2), + Uniform("U", 0, 1), + FDistribution("FD", 1, 2), + Gumbel("GB", 1, 2), + Laplace("L", 1, 2), + Logistic("LO", 1, 2), + Rayleigh("R", 1), + Triangular("T", 1, 2, 2), + ] + size = 3 + numpy = import_module('numpy') + if not numpy: + skip('Numpy is not installed. Abort tests for _sample_numpy.') + else: + for X in distribs_numpy: + samps = sample(X, size=size, library='numpy') + for sam in samps: + assert sam in X.pspace.domain.set + raises(NotImplementedError, + lambda: sample(Chi("C", 1), library='numpy')) + raises(NotImplementedError, + lambda: Chi("C", 1).pspace.distribution.sample(library='tensorflow')) + + +def test_sample_scipy(): + distribs_scipy = [ + Beta("B", 1, 1), + BetaPrime("BP", 1, 1), + Cauchy("C", 1, 1), + Chi("C", 1), + Normal("N", 0, 1), + Gamma("G", 2, 7), + GammaInverse("GI", 1, 1), + GaussianInverse("GUI", 1, 1), + Exponential("E", 2), + LogNormal("LN", 0, 1), + Pareto("P", 1, 1), + StudentT("S", 2), + ChiSquared("CS", 2), + Uniform("U", 0, 1) + ] + size = 3 + scipy = import_module('scipy') + if not scipy: + skip('Scipy is not installed. Abort tests for _sample_scipy.') + else: + for X in distribs_scipy: + samps = sample(X, size=size, library='scipy') + samps2 = sample(X, size=(2, 2), library='scipy') + for sam in samps: + assert sam in X.pspace.domain.set + for i in range(2): + for j in range(2): + assert samps2[i][j] in X.pspace.domain.set + + +def test_sample_pymc(): + distribs_pymc = [ + Beta("B", 1, 1), + Cauchy("C", 1, 1), + Normal("N", 0, 1), + Gamma("G", 2, 7), + GaussianInverse("GI", 1, 1), + Exponential("E", 2), + LogNormal("LN", 0, 1), + Pareto("P", 1, 1), + ChiSquared("CS", 2), + Uniform("U", 0, 1) + ] + size = 3 + pymc = import_module('pymc') + if not pymc: + skip('PyMC is not installed. Abort tests for _sample_pymc.') + else: + for X in distribs_pymc: + samps = sample(X, size=size, library='pymc') + for sam in samps: + assert sam in X.pspace.domain.set + raises(NotImplementedError, + lambda: sample(Chi("C", 1), library='pymc')) + + +def test_sampling_gamma_inverse(): + scipy = import_module('scipy') + if not scipy: + skip('Scipy not installed. Abort tests for sampling of gamma inverse.') + X = GammaInverse("x", 1, 1) + assert sample(X) in X.pspace.domain.set + + +def test_lognormal_sampling(): + # Right now, only density function and sampling works + scipy = import_module('scipy') + if not scipy: + skip('Scipy is not installed. Abort tests') + for i in range(3): + X = LogNormal('x', i, 1) + assert sample(X) in X.pspace.domain.set + + size = 5 + samps = sample(X, size=size) + for samp in samps: + assert samp in X.pspace.domain.set + + +def test_sampling_gaussian_inverse(): + scipy = import_module('scipy') + if not scipy: + skip('Scipy not installed. Abort tests for sampling of Gaussian inverse.') + X = GaussianInverse("x", 1, 1) + assert sample(X, library='scipy') in X.pspace.domain.set + + +def test_prefab_sampling(): + scipy = import_module('scipy') + if not scipy: + skip('Scipy is not installed. Abort tests') + N = Normal('X', 0, 1) + L = LogNormal('L', 0, 1) + E = Exponential('Ex', 1) + P = Pareto('P', 1, 3) + W = Weibull('W', 1, 1) + U = Uniform('U', 0, 1) + B = Beta('B', 2, 5) + G = Gamma('G', 1, 3) + + variables = [N, L, E, P, W, U, B, G] + niter = 10 + size = 5 + for var in variables: + for _ in range(niter): + assert sample(var) in var.pspace.domain.set + samps = sample(var, size=size) + for samp in samps: + assert samp in var.pspace.domain.set + + +def test_sample_continuous(): + z = Symbol('z') + Z = ContinuousRV(z, exp(-z), set=Interval(0, oo)) + assert density(Z)(-1) == 0 + + scipy = import_module('scipy') + if not scipy: + skip('Scipy is not installed. Abort tests') + assert sample(Z) in Z.pspace.domain.set + sym, val = list(Z.pspace.sample().items())[0] + assert sym == Z and val in Interval(0, oo) + + libraries = ['scipy', 'numpy', 'pymc'] + for lib in libraries: + try: + imported_lib = import_module(lib) + if imported_lib: + s0, s1, s2 = [], [], [] + s0 = sample(Z, size=10, library=lib, seed=0) + s1 = sample(Z, size=10, library=lib, seed=0) + s2 = sample(Z, size=10, library=lib, seed=1) + assert all(s0 == s1) + assert all(s1 != s2) + except NotImplementedError: + continue diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/tests/test_sample_discrete_rv.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/tests/test_sample_discrete_rv.py new file mode 100644 index 0000000000000000000000000000000000000000..90d385cd599222fd7da7c1559b619bafbeb01831 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/tests/test_sample_discrete_rv.py @@ -0,0 +1,109 @@ +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.external import import_module +from sympy.stats import ( + Geometric, + Poisson, + Zeta, + sample, + Skellam, + Logarithmic, + NegativeBinomial, + YuleSimon, + DiscreteRV, +) +from sympy.testing.pytest import skip, raises, slow + + +def test_sample_numpy(): + distribs_numpy = [ + Geometric('G', 0.5), + Poisson('P', 1), + Zeta('Z', 2) + ] + size = 3 + numpy = import_module('numpy') + if not numpy: + skip('Numpy is not installed. Abort tests for _sample_numpy.') + else: + for X in distribs_numpy: + samps = sample(X, size=size, library='numpy') + for sam in samps: + assert sam in X.pspace.domain.set + raises(NotImplementedError, + lambda: sample(Skellam('S', 1, 1), library='numpy')) + raises(NotImplementedError, + lambda: Skellam('S', 1, 1).pspace.distribution.sample(library='tensorflow')) + + +def test_sample_scipy(): + p = S(2)/3 + x = Symbol('x', integer=True, positive=True) + pdf = p*(1 - p)**(x - 1) # pdf of Geometric Distribution + distribs_scipy = [ + DiscreteRV(x, pdf, set=S.Naturals), + Geometric('G', 0.5), + Logarithmic('L', 0.5), + NegativeBinomial('N', 5, 0.4), + Poisson('P', 1), + Skellam('S', 1, 1), + YuleSimon('Y', 1), + Zeta('Z', 2) + ] + size = 3 + scipy = import_module('scipy') + if not scipy: + skip('Scipy is not installed. Abort tests for _sample_scipy.') + else: + for X in distribs_scipy: + samps = sample(X, size=size, library='scipy') + samps2 = sample(X, size=(2, 2), library='scipy') + for sam in samps: + assert sam in X.pspace.domain.set + for i in range(2): + for j in range(2): + assert samps2[i][j] in X.pspace.domain.set + + +def test_sample_pymc(): + distribs_pymc = [ + Geometric('G', 0.5), + Poisson('P', 1), + NegativeBinomial('N', 5, 0.4) + ] + size = 3 + pymc = import_module('pymc') + if not pymc: + skip('PyMC is not installed. Abort tests for _sample_pymc.') + else: + for X in distribs_pymc: + samps = sample(X, size=size, library='pymc') + for sam in samps: + assert sam in X.pspace.domain.set + raises(NotImplementedError, + lambda: sample(Skellam('S', 1, 1), library='pymc')) + +@slow +def test_sample_discrete(): + X = Geometric('X', S.Half) + scipy = import_module('scipy') + if not scipy: + skip('Scipy not installed. Abort tests') + assert sample(X) in X.pspace.domain.set + samps = sample(X, size=2) # This takes long time if ran without scipy + for samp in samps: + assert samp in X.pspace.domain.set + + libraries = ['scipy', 'numpy', 'pymc'] + for lib in libraries: + try: + imported_lib = import_module(lib) + if imported_lib: + s0, s1, s2 = [], [], [] + s0 = sample(X, size=10, library=lib, seed=0) + s1 = sample(X, size=10, library=lib, seed=0) + s2 = sample(X, size=10, library=lib, seed=1) + assert all(s0 == s1) + assert not all(s1 == s2) + except NotImplementedError: + continue diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/tests/test_sample_finite_rv.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/tests/test_sample_finite_rv.py new file mode 100644 index 0000000000000000000000000000000000000000..96cabe0ff4aaa5977e16600217fbbdeb08b962ae --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/sampling/tests/test_sample_finite_rv.py @@ -0,0 +1,94 @@ +from sympy.core.numbers import Rational +from sympy.core.singleton import S +from sympy.external import import_module +from sympy.stats import Binomial, sample, Die, FiniteRV, DiscreteUniform, Bernoulli, BetaBinomial, Hypergeometric, \ + Rademacher +from sympy.testing.pytest import skip, raises + +def test_given_sample(): + X = Die('X', 6) + scipy = import_module('scipy') + if not scipy: + skip('Scipy is not installed. Abort tests') + assert sample(X, X > 5) == 6 + +def test_sample_numpy(): + distribs_numpy = [ + Binomial("B", 5, 0.4), + Hypergeometric("H", 2, 1, 1) + ] + size = 3 + numpy = import_module('numpy') + if not numpy: + skip('Numpy is not installed. Abort tests for _sample_numpy.') + else: + for X in distribs_numpy: + samps = sample(X, size=size, library='numpy') + for sam in samps: + assert sam in X.pspace.domain.set + raises(NotImplementedError, + lambda: sample(Die("D"), library='numpy')) + raises(NotImplementedError, + lambda: Die("D").pspace.sample(library='tensorflow')) + + +def test_sample_scipy(): + distribs_scipy = [ + FiniteRV('F', {1: S.Half, 2: Rational(1, 4), 3: Rational(1, 4)}), + DiscreteUniform("Y", list(range(5))), + Die("D"), + Bernoulli("Be", 0.3), + Binomial("Bi", 5, 0.4), + BetaBinomial("Bb", 2, 1, 1), + Hypergeometric("H", 1, 1, 1), + Rademacher("R") + ] + + size = 3 + scipy = import_module('scipy') + if not scipy: + skip('Scipy not installed. Abort tests for _sample_scipy.') + else: + for X in distribs_scipy: + samps = sample(X, size=size) + samps2 = sample(X, size=(2, 2)) + for sam in samps: + assert sam in X.pspace.domain.set + for i in range(2): + for j in range(2): + assert samps2[i][j] in X.pspace.domain.set + + +def test_sample_pymc(): + distribs_pymc = [ + Bernoulli('B', 0.2), + Binomial('N', 5, 0.4) + ] + size = 3 + pymc = import_module('pymc') + if not pymc: + skip('PyMC is not installed. Abort tests for _sample_pymc.') + else: + for X in distribs_pymc: + samps = sample(X, size=size, library='pymc') + for sam in samps: + assert sam in X.pspace.domain.set + raises(NotImplementedError, + lambda: (sample(Die("D"), library='pymc'))) + + +def test_sample_seed(): + F = FiniteRV('F', {1: S.Half, 2: Rational(1, 4), 3: Rational(1, 4)}) + size = 10 + libraries = ['scipy', 'numpy', 'pymc'] + for lib in libraries: + try: + imported_lib = import_module(lib) + if imported_lib: + s0 = sample(F, size=size, library=lib, seed=0) + s1 = sample(F, size=size, library=lib, seed=0) + s2 = sample(F, size=size, library=lib, seed=1) + assert all(s0 == s1) + assert not all(s1 == s2) + except NotImplementedError: + continue diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/stochastic_process.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/stochastic_process.py new file mode 100644 index 0000000000000000000000000000000000000000..bfb0e759c66be892ae38ddda004dfe928f683fee --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/stochastic_process.py @@ -0,0 +1,66 @@ +from sympy.core.basic import Basic +from sympy.stats.joint_rv import ProductPSpace +from sympy.stats.rv import ProductDomain, _symbol_converter, Distribution + + +class StochasticPSpace(ProductPSpace): + """ + Represents probability space of stochastic processes + and their random variables. Contains mechanics to do + computations for queries of stochastic processes. + + Explanation + =========== + + Initialized by symbol, the specific process and + distribution(optional) if the random indexed symbols + of the process follows any specific distribution, like, + in Bernoulli Process, each random indexed symbol follows + Bernoulli distribution. For processes with memory, this + parameter should not be passed. + """ + + def __new__(cls, sym, process, distribution=None): + sym = _symbol_converter(sym) + from sympy.stats.stochastic_process_types import StochasticProcess + if not isinstance(process, StochasticProcess): + raise TypeError("`process` must be an instance of StochasticProcess.") + if distribution is None: + distribution = Distribution() + return Basic.__new__(cls, sym, process, distribution) + + @property + def process(self): + """ + The associated stochastic process. + """ + return self.args[1] + + @property + def domain(self): + return ProductDomain(self.process.index_set, + self.process.state_space) + + @property + def symbol(self): + return self.args[0] + + @property + def distribution(self): + return self.args[2] + + def probability(self, condition, given_condition=None, evaluate=True, **kwargs): + """ + Transfers the task of handling queries to the specific stochastic + process because every process has their own logic of handling such + queries. + """ + return self.process.probability(condition, given_condition, evaluate, **kwargs) + + def compute_expectation(self, expr, condition=None, evaluate=True, **kwargs): + """ + Transfers the task of handling queries to the specific stochastic + process because every process has their own logic of handling such + queries. + """ + return self.process.expectation(expr, condition, evaluate, **kwargs) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/stochastic_process_types.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/stochastic_process_types.py new file mode 100644 index 0000000000000000000000000000000000000000..7387cd3dbcf6defb3b7e475f542d04ecef6fecf6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/stochastic_process_types.py @@ -0,0 +1,2383 @@ +from __future__ import annotations +import random +import itertools +from typing import Sequence as tSequence +from sympy.concrete.summations import Sum +from sympy.core.add import Add +from sympy.core.basic import Basic +from sympy.core.cache import cacheit +from sympy.core.containers import Tuple +from sympy.core.expr import Expr +from sympy.core.function import (Function, Lambda) +from sympy.core.mul import Mul +from sympy.core.intfunc import igcd +from sympy.core.numbers import (Integer, Rational, oo, pi) +from sympy.core.relational import (Eq, Ge, Gt, Le, Lt, Ne) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol) +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.integers import ceiling +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.special.gamma_functions import gamma +from sympy.logic.boolalg import (And, Not, Or) +from sympy.matrices.exceptions import NonSquareMatrixError +from sympy.matrices.dense import (Matrix, eye, ones, zeros) +from sympy.matrices.expressions.blockmatrix import BlockMatrix +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.matrices.expressions.special import Identity +from sympy.matrices.immutable import ImmutableMatrix +from sympy.sets.conditionset import ConditionSet +from sympy.sets.contains import Contains +from sympy.sets.fancysets import Range +from sympy.sets.sets import (FiniteSet, Intersection, Interval, Set, Union) +from sympy.solvers.solveset import linsolve +from sympy.tensor.indexed import (Indexed, IndexedBase) +from sympy.core.relational import Relational +from sympy.logic.boolalg import Boolean +from sympy.utilities.exceptions import sympy_deprecation_warning +from sympy.utilities.iterables import strongly_connected_components +from sympy.stats.joint_rv import JointDistribution +from sympy.stats.joint_rv_types import JointDistributionHandmade +from sympy.stats.rv import (RandomIndexedSymbol, random_symbols, RandomSymbol, + _symbol_converter, _value_check, pspace, given, + dependent, is_random, sample_iter, Distribution, + Density) +from sympy.stats.stochastic_process import StochasticPSpace +from sympy.stats.symbolic_probability import Probability, Expectation +from sympy.stats.frv_types import Bernoulli, BernoulliDistribution, FiniteRV +from sympy.stats.drv_types import Poisson, PoissonDistribution +from sympy.stats.crv_types import Normal, NormalDistribution, Gamma, GammaDistribution +from sympy.core.sympify import _sympify, sympify + +EmptySet = S.EmptySet + +__all__ = [ + 'StochasticProcess', + 'DiscreteTimeStochasticProcess', + 'DiscreteMarkovChain', + 'TransitionMatrixOf', + 'StochasticStateSpaceOf', + 'GeneratorMatrixOf', + 'ContinuousMarkovChain', + 'BernoulliProcess', + 'PoissonProcess', + 'WienerProcess', + 'GammaProcess' +] + + +@is_random.register(Indexed) +def _(x): + return is_random(x.base) + +@is_random.register(RandomIndexedSymbol) # type: ignore +def _(x): + return True + +def _set_converter(itr): + """ + Helper function for converting list/tuple/set to Set. + If parameter is not an instance of list/tuple/set then + no operation is performed. + + Returns + ======= + + Set + The argument converted to Set. + + + Raises + ====== + + TypeError + If the argument is not an instance of list/tuple/set. + """ + if isinstance(itr, (list, tuple, set)): + itr = FiniteSet(*itr) + if not isinstance(itr, Set): + raise TypeError("%s is not an instance of list/tuple/set."%(itr)) + return itr + +def _state_converter(itr: tSequence) -> Tuple | Range: + """ + Helper function for converting list/tuple/set/Range/Tuple/FiniteSet + to tuple/Range. + """ + itr_ret: Tuple | Range + + if isinstance(itr, (Tuple, set, FiniteSet)): + itr_ret = Tuple(*(sympify(i) if isinstance(i, str) else i for i in itr)) + + elif isinstance(itr, (list, tuple)): + # check if states are unique + if len(set(itr)) != len(itr): + raise ValueError('The state space must have unique elements.') + itr_ret = Tuple(*(sympify(i) if isinstance(i, str) else i for i in itr)) + + elif isinstance(itr, Range): + # the only ordered set in SymPy I know of + # try to convert to tuple + try: + itr_ret = Tuple(*(sympify(i) if isinstance(i, str) else i for i in itr)) + except (TypeError, ValueError): + itr_ret = itr + + else: + raise TypeError("%s is not an instance of list/tuple/set/Range/Tuple/FiniteSet." % (itr)) + return itr_ret + +def _sym_sympify(arg): + """ + Converts an arbitrary expression to a type that can be used inside SymPy. + As generally strings are unwise to use in the expressions, + it returns the Symbol of argument if the string type argument is passed. + + Parameters + ========= + + arg: The parameter to be converted to be used in SymPy. + + Returns + ======= + + The converted parameter. + + """ + if isinstance(arg, str): + return Symbol(arg) + else: + return _sympify(arg) + +def _matrix_checks(matrix): + if not isinstance(matrix, (Matrix, MatrixSymbol, ImmutableMatrix)): + raise TypeError("Transition probabilities either should " + "be a Matrix or a MatrixSymbol.") + if matrix.shape[0] != matrix.shape[1]: + raise NonSquareMatrixError("%s is not a square matrix"%(matrix)) + if isinstance(matrix, Matrix): + matrix = ImmutableMatrix(matrix.tolist()) + return matrix + +class StochasticProcess(Basic): + """ + Base class for all the stochastic processes whether + discrete or continuous. + + Parameters + ========== + + sym: Symbol or str + state_space: Set + The state space of the stochastic process, by default S.Reals. + For discrete sets it is zero indexed. + + See Also + ======== + + DiscreteTimeStochasticProcess + """ + + index_set = S.Reals + + def __new__(cls, sym, state_space=S.Reals, **kwargs): + sym = _symbol_converter(sym) + state_space = _set_converter(state_space) + return Basic.__new__(cls, sym, state_space) + + @property + def symbol(self): + return self.args[0] + + @property + def state_space(self) -> FiniteSet | Range: + if not isinstance(self.args[1], (FiniteSet, Range)): + assert isinstance(self.args[1], Tuple) + return FiniteSet(*self.args[1]) + return self.args[1] + + def _deprecation_warn_distribution(self): + sympy_deprecation_warning( + """ + Calling the distribution method with a RandomIndexedSymbol + argument, like X.distribution(X(t)) is deprecated. Instead, call + distribution() with the given timestamp, like + + X.distribution(t) + """, + deprecated_since_version="1.7.1", + active_deprecations_target="deprecated-distribution-randomindexedsymbol", + stacklevel=4, + ) + + def distribution(self, key=None): + if key is None: + self._deprecation_warn_distribution() + return Distribution() + + def density(self, x): + return Density() + + def __call__(self, time): + """ + Overridden in ContinuousTimeStochasticProcess. + """ + raise NotImplementedError("Use [] for indexing discrete time stochastic process.") + + def __getitem__(self, time): + """ + Overridden in DiscreteTimeStochasticProcess. + """ + raise NotImplementedError("Use () for indexing continuous time stochastic process.") + + def probability(self, condition): + raise NotImplementedError() + + def joint_distribution(self, *args): + """ + Computes the joint distribution of the random indexed variables. + + Parameters + ========== + + args: iterable + The finite list of random indexed variables/the key of a stochastic + process whose joint distribution has to be computed. + + Returns + ======= + + JointDistribution + The joint distribution of the list of random indexed variables. + An unevaluated object is returned if it is not possible to + compute the joint distribution. + + Raises + ====== + + ValueError: When the arguments passed are not of type RandomIndexSymbol + or Number. + """ + args = list(args) + for i, arg in enumerate(args): + if S(arg).is_Number: + if self.index_set.is_subset(S.Integers): + args[i] = self.__getitem__(arg) + else: + args[i] = self.__call__(arg) + elif not isinstance(arg, RandomIndexedSymbol): + raise ValueError("Expected a RandomIndexedSymbol or " + "key not %s"%(type(arg))) + + if args[0].pspace.distribution == Distribution(): + return JointDistribution(*args) + density = Lambda(tuple(args), + expr=Mul.fromiter(arg.pspace.process.density(arg) for arg in args)) + return JointDistributionHandmade(density) + + def expectation(self, condition, given_condition): + raise NotImplementedError("Abstract method for expectation queries.") + + def sample(self): + raise NotImplementedError("Abstract method for sampling queries.") + +class DiscreteTimeStochasticProcess(StochasticProcess): + """ + Base class for all discrete stochastic processes. + """ + def __getitem__(self, time): + """ + For indexing discrete time stochastic processes. + + Returns + ======= + + RandomIndexedSymbol + """ + time = sympify(time) + if not time.is_symbol and time not in self.index_set: + raise IndexError("%s is not in the index set of %s"%(time, self.symbol)) + idx_obj = Indexed(self.symbol, time) + pspace_obj = StochasticPSpace(self.symbol, self, self.distribution(time)) + return RandomIndexedSymbol(idx_obj, pspace_obj) + +class ContinuousTimeStochasticProcess(StochasticProcess): + """ + Base class for all continuous time stochastic process. + """ + def __call__(self, time): + """ + For indexing continuous time stochastic processes. + + Returns + ======= + + RandomIndexedSymbol + """ + time = sympify(time) + if not time.is_symbol and time not in self.index_set: + raise IndexError("%s is not in the index set of %s"%(time, self.symbol)) + func_obj = Function(self.symbol)(time) + pspace_obj = StochasticPSpace(self.symbol, self, self.distribution(time)) + return RandomIndexedSymbol(func_obj, pspace_obj) + +class TransitionMatrixOf(Boolean): + """ + Assumes that the matrix is the transition matrix + of the process. + """ + + def __new__(cls, process, matrix): + if not isinstance(process, DiscreteMarkovChain): + raise ValueError("Currently only DiscreteMarkovChain " + "support TransitionMatrixOf.") + matrix = _matrix_checks(matrix) + return Basic.__new__(cls, process, matrix) + + process = property(lambda self: self.args[0]) + matrix = property(lambda self: self.args[1]) + +class GeneratorMatrixOf(TransitionMatrixOf): + """ + Assumes that the matrix is the generator matrix + of the process. + """ + + def __new__(cls, process, matrix): + if not isinstance(process, ContinuousMarkovChain): + raise ValueError("Currently only ContinuousMarkovChain " + "support GeneratorMatrixOf.") + matrix = _matrix_checks(matrix) + return Basic.__new__(cls, process, matrix) + +class StochasticStateSpaceOf(Boolean): + + def __new__(cls, process, state_space): + if not isinstance(process, (DiscreteMarkovChain, ContinuousMarkovChain)): + raise ValueError("Currently only DiscreteMarkovChain and ContinuousMarkovChain " + "support StochasticStateSpaceOf.") + state_space = _state_converter(state_space) + if isinstance(state_space, Range): + ss_size = ceiling((state_space.stop - state_space.start) / state_space.step) + else: + ss_size = len(state_space) + state_index = Range(ss_size) + return Basic.__new__(cls, process, state_index) + + process = property(lambda self: self.args[0]) + state_index = property(lambda self: self.args[1]) + +class MarkovProcess(StochasticProcess): + """ + Contains methods that handle queries + common to Markov processes. + """ + + @property + def number_of_states(self) -> Integer | Symbol: + """ + The number of states in the Markov Chain. + """ + return _sympify(self.args[2].shape[0]) # type: ignore + + @property + def _state_index(self): + """ + Returns state index as Range. + """ + return self.args[1] + + @classmethod + def _sanity_checks(cls, state_space, trans_probs): + # Try to never have None as state_space or trans_probs. + # This helps a lot if we get it done at the start. + if (state_space is None) and (trans_probs is None): + _n = Dummy('n', integer=True, nonnegative=True) + state_space = _state_converter(Range(_n)) + trans_probs = _matrix_checks(MatrixSymbol('_T', _n, _n)) + + elif state_space is None: + trans_probs = _matrix_checks(trans_probs) + state_space = _state_converter(Range(trans_probs.shape[0])) + + elif trans_probs is None: + state_space = _state_converter(state_space) + if isinstance(state_space, Range): + _n = ceiling((state_space.stop - state_space.start) / state_space.step) + else: + _n = len(state_space) + trans_probs = MatrixSymbol('_T', _n, _n) + + else: + state_space = _state_converter(state_space) + trans_probs = _matrix_checks(trans_probs) + # Range object doesn't want to give a symbolic size + # so we do it ourselves. + if isinstance(state_space, Range): + ss_size = ceiling((state_space.stop - state_space.start) / state_space.step) + else: + ss_size = len(state_space) + if ss_size != trans_probs.shape[0]: + raise ValueError('The size of the state space and the number of ' + 'rows of the transition matrix must be the same.') + + return state_space, trans_probs + + def _extract_information(self, given_condition): + """ + Helper function to extract information, like, + transition matrix/generator matrix, state space, etc. + """ + if isinstance(self, DiscreteMarkovChain): + trans_probs = self.transition_probabilities + state_index = self._state_index + elif isinstance(self, ContinuousMarkovChain): + trans_probs = self.generator_matrix + state_index = self._state_index + if isinstance(given_condition, And): + gcs = given_condition.args + given_condition = S.true + for gc in gcs: + if isinstance(gc, TransitionMatrixOf): + trans_probs = gc.matrix + if isinstance(gc, StochasticStateSpaceOf): + state_index = gc.state_index + if isinstance(gc, Relational): + given_condition = given_condition & gc + if isinstance(given_condition, TransitionMatrixOf): + trans_probs = given_condition.matrix + given_condition = S.true + if isinstance(given_condition, StochasticStateSpaceOf): + state_index = given_condition.state_index + given_condition = S.true + return trans_probs, state_index, given_condition + + def _check_trans_probs(self, trans_probs, row_sum=1): + """ + Helper function for checking the validity of transition + probabilities. + """ + if not isinstance(trans_probs, MatrixSymbol): + rows = trans_probs.tolist() + for row in rows: + if (sum(row) - row_sum) != 0: + raise ValueError("Values in a row must sum to %s. " + "If you are using Float or floats then please use Rational."%(row_sum)) + + def _work_out_state_index(self, state_index, given_condition, trans_probs): + """ + Helper function to extract state space if there + is a random symbol in the given condition. + """ + # if given condition is None, then there is no need to work out + # state_space from random variables + if given_condition != None: + rand_var = list(given_condition.atoms(RandomSymbol) - + given_condition.atoms(RandomIndexedSymbol)) + if len(rand_var) == 1: + state_index = rand_var[0].pspace.set + + # `not None` is `True`. So the old test fails for symbolic sizes. + # Need to build the statement differently. + sym_cond = not self.number_of_states.is_Integer + cond1 = not sym_cond and len(state_index) != trans_probs.shape[0] + if cond1: + raise ValueError("state space is not compatible with the transition probabilities.") + if not isinstance(trans_probs.shape[0], Symbol): + state_index = FiniteSet(*range(trans_probs.shape[0])) + return state_index + + @cacheit + def _preprocess(self, given_condition, evaluate): + """ + Helper function for pre-processing the information. + """ + is_insufficient = False + + if not evaluate: # avoid pre-processing if the result is not to be evaluated + return (True, None, None, None) + + # extracting transition matrix and state space + trans_probs, state_index, given_condition = self._extract_information(given_condition) + + # given_condition does not have sufficient information + # for computations + if trans_probs is None or \ + given_condition is None: + is_insufficient = True + else: + # checking transition probabilities + if isinstance(self, DiscreteMarkovChain): + self._check_trans_probs(trans_probs, row_sum=1) + elif isinstance(self, ContinuousMarkovChain): + self._check_trans_probs(trans_probs, row_sum=0) + + # working out state space + state_index = self._work_out_state_index(state_index, given_condition, trans_probs) + + return is_insufficient, trans_probs, state_index, given_condition + + def replace_with_index(self, condition): + if isinstance(condition, Relational): + lhs, rhs = condition.lhs, condition.rhs + if not isinstance(lhs, RandomIndexedSymbol): + lhs, rhs = rhs, lhs + condition = type(condition)(self.index_of.get(lhs, lhs), + self.index_of.get(rhs, rhs)) + return condition + + def probability(self, condition, given_condition=None, evaluate=True, **kwargs): + """ + Handles probability queries for Markov process. + + Parameters + ========== + + condition: Relational + given_condition: Relational/And + + Returns + ======= + Probability + If the information is not sufficient. + Expr + In all other cases. + + Note + ==== + Any information passed at the time of query overrides + any information passed at the time of object creation like + transition probabilities, state space. + Pass the transition matrix using TransitionMatrixOf, + generator matrix using GeneratorMatrixOf and state space + using StochasticStateSpaceOf in given_condition using & or And. + """ + check, mat, state_index, new_given_condition = \ + self._preprocess(given_condition, evaluate) + + rv = list(condition.atoms(RandomIndexedSymbol)) + symbolic = False + for sym in rv: + if sym.key.is_symbol: + symbolic = True + break + + if check: + return Probability(condition, new_given_condition) + + if isinstance(self, ContinuousMarkovChain): + trans_probs = self.transition_probabilities(mat) + elif isinstance(self, DiscreteMarkovChain): + trans_probs = mat + condition = self.replace_with_index(condition) + given_condition = self.replace_with_index(given_condition) + new_given_condition = self.replace_with_index(new_given_condition) + + if isinstance(condition, Relational): + if isinstance(new_given_condition, And): + gcs = new_given_condition.args + else: + gcs = (new_given_condition, ) + min_key_rv = list(new_given_condition.atoms(RandomIndexedSymbol)) + + if len(min_key_rv): + min_key_rv = min_key_rv[0] + for r in rv: + if min_key_rv.key.is_symbol or r.key.is_symbol: + continue + if min_key_rv.key > r.key: + return Probability(condition) + else: + min_key_rv = None + return Probability(condition) + + if symbolic: + return self._symbolic_probability(condition, new_given_condition, rv, min_key_rv) + + if len(rv) > 1: + rv[0] = condition.lhs + rv[1] = condition.rhs + if rv[0].key < rv[1].key: + rv[0], rv[1] = rv[1], rv[0] + if isinstance(condition, Gt): + condition = Lt(condition.lhs, condition.rhs) + elif isinstance(condition, Lt): + condition = Gt(condition.lhs, condition.rhs) + elif isinstance(condition, Ge): + condition = Le(condition.lhs, condition.rhs) + elif isinstance(condition, Le): + condition = Ge(condition.lhs, condition.rhs) + s = Rational(0, 1) + n = len(self.state_space) + + if isinstance(condition, (Eq, Ne)): + for i in range(0, n): + s += self.probability(Eq(rv[0], i), Eq(rv[1], i)) * self.probability(Eq(rv[1], i), new_given_condition) + return s if isinstance(condition, Eq) else 1 - s + else: + upper = 0 + greater = False + if isinstance(condition, (Ge, Lt)): + upper = 1 + if isinstance(condition, (Ge, Gt)): + greater = True + + for i in range(0, n): + if i <= n//2: + for j in range(0, i + upper): + s += self.probability(Eq(rv[0], i), Eq(rv[1], j)) * self.probability(Eq(rv[1], j), new_given_condition) + else: + s += self.probability(Eq(rv[0], i), new_given_condition) + for j in range(i + upper, n): + s -= self.probability(Eq(rv[0], i), Eq(rv[1], j)) * self.probability(Eq(rv[1], j), new_given_condition) + return s if greater else 1 - s + + rv = rv[0] + states = condition.as_set() + prob, gstate = {}, None + for gc in gcs: + if gc.has(min_key_rv): + if gc.has(Probability): + p, gp = (gc.rhs, gc.lhs) if isinstance(gc.lhs, Probability) \ + else (gc.lhs, gc.rhs) + gr = gp.args[0] + gset = Intersection(gr.as_set(), state_index) + gstate = list(gset)[0] + prob[gset] = p + else: + _, gstate = (gc.lhs.key, gc.rhs) if isinstance(gc.lhs, RandomIndexedSymbol) \ + else (gc.rhs.key, gc.lhs) + + if not all(k in self.index_set for k in (rv.key, min_key_rv.key)): + raise IndexError("The timestamps of the process are not in it's index set.") + states = Intersection(states, state_index) if not isinstance(self.number_of_states, Symbol) else states + for state in Union(states, FiniteSet(gstate)): + if not state.is_Integer or Ge(state, mat.shape[0]) is True: + raise IndexError("No information is available for (%s, %s) in " + "transition probabilities of shape, (%s, %s). " + "State space is zero indexed." + %(gstate, state, mat.shape[0], mat.shape[1])) + if prob: + gstates = Union(*prob.keys()) + if len(gstates) == 1: + gstate = list(gstates)[0] + gprob = list(prob.values())[0] + prob[gstates] = gprob + elif len(gstates) == len(state_index) - 1: + gstate = list(state_index - gstates)[0] + gprob = S.One - sum(prob.values()) + prob[state_index - gstates] = gprob + else: + raise ValueError("Conflicting information.") + else: + gprob = S.One + + if min_key_rv == rv: + return sum(prob[FiniteSet(state)] for state in states) + if isinstance(self, ContinuousMarkovChain): + return gprob * sum(trans_probs(rv.key - min_key_rv.key).__getitem__((gstate, state)) + for state in states) + if isinstance(self, DiscreteMarkovChain): + return gprob * sum((trans_probs**(rv.key - min_key_rv.key)).__getitem__((gstate, state)) + for state in states) + + if isinstance(condition, Not): + expr = condition.args[0] + return S.One - self.probability(expr, given_condition, evaluate, **kwargs) + + if isinstance(condition, And): + compute_later, state2cond, conds = [], {}, condition.args + for expr in conds: + if isinstance(expr, Relational): + ris = list(expr.atoms(RandomIndexedSymbol))[0] + if state2cond.get(ris, None) is None: + state2cond[ris] = S.true + state2cond[ris] &= expr + else: + compute_later.append(expr) + ris = [] + for ri in state2cond: + ris.append(ri) + cset = Intersection(state2cond[ri].as_set(), state_index) + if len(cset) == 0: + return S.Zero + state2cond[ri] = cset.as_relational(ri) + sorted_ris = sorted(ris, key=lambda ri: ri.key) + prod = self.probability(state2cond[sorted_ris[0]], given_condition, evaluate, **kwargs) + for i in range(1, len(sorted_ris)): + ri, prev_ri = sorted_ris[i], sorted_ris[i-1] + if not isinstance(state2cond[ri], Eq): + raise ValueError("The process is in multiple states at %s, unable to determine the probability."%(ri)) + mat_of = TransitionMatrixOf(self, mat) if isinstance(self, DiscreteMarkovChain) else GeneratorMatrixOf(self, mat) + prod *= self.probability(state2cond[ri], state2cond[prev_ri] + & mat_of + & StochasticStateSpaceOf(self, state_index), + evaluate, **kwargs) + for expr in compute_later: + prod *= self.probability(expr, given_condition, evaluate, **kwargs) + return prod + + if isinstance(condition, Or): + return sum(self.probability(expr, given_condition, evaluate, **kwargs) + for expr in condition.args) + + raise NotImplementedError("Mechanism for handling (%s, %s) queries hasn't been " + "implemented yet."%(condition, given_condition)) + + def _symbolic_probability(self, condition, new_given_condition, rv, min_key_rv): + #Function to calculate probability for queries with symbols + if isinstance(condition, Relational): + curr_state = new_given_condition.rhs if isinstance(new_given_condition.lhs, RandomIndexedSymbol) \ + else new_given_condition.lhs + next_state = condition.rhs if isinstance(condition.lhs, RandomIndexedSymbol) \ + else condition.lhs + + if isinstance(condition, (Eq, Ne)): + if isinstance(self, DiscreteMarkovChain): + P = self.transition_probabilities**(rv[0].key - min_key_rv.key) + else: + P = exp(self.generator_matrix*(rv[0].key - min_key_rv.key)) + prob = P[curr_state, next_state] if isinstance(condition, Eq) else 1 - P[curr_state, next_state] + return Piecewise((prob, rv[0].key > min_key_rv.key), (Probability(condition), True)) + else: + upper = 1 + greater = False + if isinstance(condition, (Ge, Lt)): + upper = 0 + if isinstance(condition, (Ge, Gt)): + greater = True + k = Dummy('k') + condition = Eq(condition.lhs, k) if isinstance(condition.lhs, RandomIndexedSymbol)\ + else Eq(condition.rhs, k) + total = Sum(self.probability(condition, new_given_condition), (k, next_state + upper, self.state_space._sup)) + return Piecewise((total, rv[0].key > min_key_rv.key), (Probability(condition), True)) if greater\ + else Piecewise((1 - total, rv[0].key > min_key_rv.key), (Probability(condition), True)) + else: + return Probability(condition, new_given_condition) + + def expectation(self, expr, condition=None, evaluate=True, **kwargs): + """ + Handles expectation queries for markov process. + + Parameters + ========== + + expr: RandomIndexedSymbol, Relational, Logic + Condition for which expectation has to be computed. Must + contain a RandomIndexedSymbol of the process. + condition: Relational, Logic + The given conditions under which computations should be done. + + Returns + ======= + + Expectation + Unevaluated object if computations cannot be done due to + insufficient information. + Expr + In all other cases when the computations are successful. + + Note + ==== + + Any information passed at the time of query overrides + any information passed at the time of object creation like + transition probabilities, state space. + + Pass the transition matrix using TransitionMatrixOf, + generator matrix using GeneratorMatrixOf and state space + using StochasticStateSpaceOf in given_condition using & or And. + """ + + check, mat, state_index, condition = \ + self._preprocess(condition, evaluate) + + if check: + return Expectation(expr, condition) + + rvs = random_symbols(expr) + if isinstance(expr, Expr) and isinstance(condition, Eq) \ + and len(rvs) == 1: + # handle queries similar to E(f(X[i]), Eq(X[i-m], )) + condition=self.replace_with_index(condition) + state_index=self.replace_with_index(state_index) + rv = list(rvs)[0] + lhsg, rhsg = condition.lhs, condition.rhs + if not isinstance(lhsg, RandomIndexedSymbol): + lhsg, rhsg = (rhsg, lhsg) + if rhsg not in state_index: + raise ValueError("%s state is not in the state space."%(rhsg)) + if rv.key < lhsg.key: + raise ValueError("Incorrect given condition is given, expectation " + "time %s < time %s"%(rv.key, rv.key)) + mat_of = TransitionMatrixOf(self, mat) if isinstance(self, DiscreteMarkovChain) else GeneratorMatrixOf(self, mat) + cond = condition & mat_of & \ + StochasticStateSpaceOf(self, state_index) + func = lambda s: self.probability(Eq(rv, s), cond) * expr.subs(rv, self._state_index[s]) + return sum(func(s) for s in state_index) + + raise NotImplementedError("Mechanism for handling (%s, %s) queries hasn't been " + "implemented yet."%(expr, condition)) + +class DiscreteMarkovChain(DiscreteTimeStochasticProcess, MarkovProcess): + """ + Represents a finite discrete time-homogeneous Markov chain. + + This type of Markov Chain can be uniquely characterised by + its (ordered) state space and its one-step transition probability + matrix. + + Parameters + ========== + + sym: + The name given to the Markov Chain + state_space: + Optional, by default, Range(n) + trans_probs: + Optional, by default, MatrixSymbol('_T', n, n) + + Examples + ======== + + >>> from sympy.stats import DiscreteMarkovChain, TransitionMatrixOf, P, E + >>> from sympy import Matrix, MatrixSymbol, Eq, symbols + >>> T = Matrix([[0.5, 0.2, 0.3],[0.2, 0.5, 0.3],[0.2, 0.3, 0.5]]) + >>> Y = DiscreteMarkovChain("Y", [0, 1, 2], T) + >>> YS = DiscreteMarkovChain("Y") + + >>> Y.state_space + {0, 1, 2} + >>> Y.transition_probabilities + Matrix([ + [0.5, 0.2, 0.3], + [0.2, 0.5, 0.3], + [0.2, 0.3, 0.5]]) + >>> TS = MatrixSymbol('T', 3, 3) + >>> P(Eq(YS[3], 2), Eq(YS[1], 1) & TransitionMatrixOf(YS, TS)) + T[0, 2]*T[1, 0] + T[1, 1]*T[1, 2] + T[1, 2]*T[2, 2] + >>> P(Eq(Y[3], 2), Eq(Y[1], 1)).round(2) + 0.36 + + Probabilities will be calculated based on indexes rather + than state names. For example, with the Sunny-Cloudy-Rainy + model with string state names: + + >>> from sympy.core.symbol import Str + >>> Y = DiscreteMarkovChain("Y", [Str('Sunny'), Str('Cloudy'), Str('Rainy')], T) + >>> P(Eq(Y[3], 2), Eq(Y[1], 1)).round(2) + 0.36 + + This gives the same answer as the ``[0, 1, 2]`` state space. + Currently, there is no support for state names within probability + and expectation statements. Here is a work-around using ``Str``: + + >>> P(Eq(Str('Rainy'), Y[3]), Eq(Y[1], Str('Cloudy'))).round(2) + 0.36 + + Symbol state names can also be used: + + >>> sunny, cloudy, rainy = symbols('Sunny, Cloudy, Rainy') + >>> Y = DiscreteMarkovChain("Y", [sunny, cloudy, rainy], T) + >>> P(Eq(Y[3], rainy), Eq(Y[1], cloudy)).round(2) + 0.36 + + Expectations will be calculated as follows: + + >>> E(Y[3], Eq(Y[1], cloudy)) + 0.38*Cloudy + 0.36*Rainy + 0.26*Sunny + + Probability of expressions with multiple RandomIndexedSymbols + can also be calculated provided there is only 1 RandomIndexedSymbol + in the given condition. It is always better to use Rational instead + of floating point numbers for the probabilities in the + transition matrix to avoid errors. + + >>> from sympy import Gt, Le, Rational + >>> T = Matrix([[Rational(5, 10), Rational(3, 10), Rational(2, 10)], [Rational(2, 10), Rational(7, 10), Rational(1, 10)], [Rational(3, 10), Rational(3, 10), Rational(4, 10)]]) + >>> Y = DiscreteMarkovChain("Y", [0, 1, 2], T) + >>> P(Eq(Y[3], Y[1]), Eq(Y[0], 0)).round(3) + 0.409 + >>> P(Gt(Y[3], Y[1]), Eq(Y[0], 0)).round(2) + 0.36 + >>> P(Le(Y[15], Y[10]), Eq(Y[8], 2)).round(7) + 0.6963328 + + Symbolic probability queries are also supported + + >>> a, b, c, d = symbols('a b c d') + >>> T = Matrix([[Rational(1, 10), Rational(4, 10), Rational(5, 10)], [Rational(3, 10), Rational(4, 10), Rational(3, 10)], [Rational(7, 10), Rational(2, 10), Rational(1, 10)]]) + >>> Y = DiscreteMarkovChain("Y", [0, 1, 2], T) + >>> query = P(Eq(Y[a], b), Eq(Y[c], d)) + >>> query.subs({a:10, b:2, c:5, d:1}).round(4) + 0.3096 + >>> P(Eq(Y[10], 2), Eq(Y[5], 1)).evalf().round(4) + 0.3096 + >>> query_gt = P(Gt(Y[a], b), Eq(Y[c], d)) + >>> query_gt.subs({a:21, b:0, c:5, d:0}).evalf().round(5) + 0.64705 + >>> P(Gt(Y[21], 0), Eq(Y[5], 0)).round(5) + 0.64705 + + There is limited support for arbitrarily sized states: + + >>> n = symbols('n', nonnegative=True, integer=True) + >>> T = MatrixSymbol('T', n, n) + >>> Y = DiscreteMarkovChain("Y", trans_probs=T) + >>> Y.state_space + Range(0, n, 1) + >>> query = P(Eq(Y[a], b), Eq(Y[c], d)) + >>> query.subs({a:10, b:2, c:5, d:1}) + (T**5)[1, 2] + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Markov_chain#Discrete-time_Markov_chain + .. [2] https://web.archive.org/web/20201230182007/https://www.dartmouth.edu/~chance/teaching_aids/books_articles/probability_book/Chapter11.pdf + """ + index_set = S.Naturals0 + + def __new__(cls, sym, state_space=None, trans_probs=None): + sym = _symbol_converter(sym) + + state_space, trans_probs = MarkovProcess._sanity_checks(state_space, trans_probs) + + obj = Basic.__new__(cls, sym, state_space, trans_probs) # type: ignore + indices = {} + if isinstance(obj.number_of_states, Integer): + for index, state in enumerate(obj._state_index): + indices[state] = index + obj.index_of = indices + return obj + + @property + def transition_probabilities(self): + """ + Transition probabilities of discrete Markov chain, + either an instance of Matrix or MatrixSymbol. + """ + return self.args[2] + + def communication_classes(self) -> list[tuple[list[Basic], Boolean, Integer]]: + """ + Returns the list of communication classes that partition + the states of the markov chain. + + A communication class is defined to be a set of states + such that every state in that set is reachable from + every other state in that set. Due to its properties + this forms a class in the mathematical sense. + Communication classes are also known as recurrence + classes. + + Returns + ======= + + classes + The ``classes`` are a list of tuples. Each + tuple represents a single communication class + with its properties. The first element in the + tuple is the list of states in the class, the + second element is whether the class is recurrent + and the third element is the period of the + communication class. + + Examples + ======== + + >>> from sympy.stats import DiscreteMarkovChain + >>> from sympy import Matrix + >>> T = Matrix([[0, 1, 0], + ... [1, 0, 0], + ... [1, 0, 0]]) + >>> X = DiscreteMarkovChain('X', [1, 2, 3], T) + >>> classes = X.communication_classes() + >>> for states, is_recurrent, period in classes: + ... states, is_recurrent, period + ([1, 2], True, 2) + ([3], False, 1) + + From this we can see that states ``1`` and ``2`` + communicate, are recurrent and have a period + of 2. We can also see state ``3`` is transient + with a period of 1. + + Notes + ===== + + The algorithm used is of order ``O(n**2)`` where + ``n`` is the number of states in the markov chain. + It uses Tarjan's algorithm to find the classes + themselves and then it uses a breadth-first search + algorithm to find each class's periodicity. + Most of the algorithm's components approach ``O(n)`` + as the matrix becomes more and more sparse. + + References + ========== + + .. [1] https://web.archive.org/web/20220207032113/https://www.columbia.edu/~ww2040/4701Sum07/4701-06-Notes-MCII.pdf + .. [2] https://cecas.clemson.edu/~shierd/Shier/markov.pdf + .. [3] https://www.proquest.com/openview/4adc6a51d8371be5b0e4c7dff287fc70/1?pq-origsite=gscholar&cbl=2026366&diss=y + .. [4] https://www.mathworks.com/help/econ/dtmc.classify.html + """ + n = self.number_of_states + T = self.transition_probabilities + + if isinstance(T, MatrixSymbol): + raise NotImplementedError("Cannot perform the operation with a symbolic matrix.") + + # begin Tarjan's algorithm + V = Range(n) + # don't use state names. Rather use state + # indexes since we use them for matrix + # indexing here and later onward + E = [(i, j) for i in V for j in V if T[i, j] != 0] + classes = strongly_connected_components((V, E)) + # end Tarjan's algorithm + + recurrence = [] + periods = [] + for class_ in classes: + # begin recurrent check (similar to self._check_trans_probs()) + submatrix = T[class_, class_] # get the submatrix with those states + is_recurrent = S.true + rows = submatrix.tolist() + for row in rows: + if (sum(row) - 1) != 0: + is_recurrent = S.false + break + recurrence.append(is_recurrent) + # end recurrent check + + # begin breadth-first search + non_tree_edge_values: set[int] = set() + visited = {class_[0]} + newly_visited = {class_[0]} + level = {class_[0]: 0} + current_level = 0 + done = False # imitate a do-while loop + while not done: # runs at most len(class_) times + done = len(visited) == len(class_) + current_level += 1 + + # this loop and the while loop above run a combined len(class_) number of times. + # so this triple nested loop runs through each of the n states once. + for i in newly_visited: + + # the loop below runs len(class_) number of times + # complexity is around about O(n * avg(len(class_))) + newly_visited = {j for j in class_ if T[i, j] != 0} + + new_tree_edges = newly_visited.difference(visited) + for j in new_tree_edges: + level[j] = current_level + + new_non_tree_edges = newly_visited.intersection(visited) + new_non_tree_edge_values = {level[i]-level[j]+1 for j in new_non_tree_edges} + + non_tree_edge_values = non_tree_edge_values.union(new_non_tree_edge_values) + visited = visited.union(new_tree_edges) + + # igcd needs at least 2 arguments + positive_ntev = {val_e for val_e in non_tree_edge_values if val_e > 0} + if len(positive_ntev) == 0: + periods.append(len(class_)) + elif len(positive_ntev) == 1: + periods.append(positive_ntev.pop()) + else: + periods.append(igcd(*positive_ntev)) + # end breadth-first search + + # convert back to the user's state names + classes = [[_sympify(self._state_index[i]) for i in class_] for class_ in classes] + return list(zip(classes, recurrence, map(Integer,periods))) + + def fundamental_matrix(self): + """ + Each entry fundamental matrix can be interpreted as + the expected number of times the chains is in state j + if it started in state i. + + References + ========== + + .. [1] https://lips.cs.princeton.edu/the-fundamental-matrix-of-a-finite-markov-chain/ + + """ + _, _, _, Q = self.decompose() + + if Q.shape[0] > 0: # if non-ergodic + I = eye(Q.shape[0]) + if (I - Q).det() == 0: + raise ValueError("The fundamental matrix doesn't exist.") + return (I - Q).inv().as_immutable() + else: # if ergodic + P = self.transition_probabilities + I = eye(P.shape[0]) + w = self.fixed_row_vector() + W = Matrix([list(w) for i in range(0, P.shape[0])]) + if (I - P + W).det() == 0: + raise ValueError("The fundamental matrix doesn't exist.") + return (I - P + W).inv().as_immutable() + + def absorbing_probabilities(self): + """ + Computes the absorbing probabilities, i.e. + the ij-th entry of the matrix denotes the + probability of Markov chain being absorbed + in state j starting from state i. + """ + _, _, R, _ = self.decompose() + N = self.fundamental_matrix() + if R is None or N is None: + return None + return N*R + + def absorbing_probabilites(self): + sympy_deprecation_warning( + """ + DiscreteMarkovChain.absorbing_probabilites() is deprecated. Use + absorbing_probabilities() instead (note the spelling difference). + """, + deprecated_since_version="1.7", + active_deprecations_target="deprecated-absorbing_probabilites", + ) + return self.absorbing_probabilities() + + def is_regular(self): + tuples = self.communication_classes() + if len(tuples) == 0: + return S.false # not defined for a 0x0 matrix + classes, _, periods = list(zip(*tuples)) + return And(len(classes) == 1, periods[0] == 1) + + def is_ergodic(self): + tuples = self.communication_classes() + if len(tuples) == 0: + return S.false # not defined for a 0x0 matrix + classes, _, _ = list(zip(*tuples)) + return S(len(classes) == 1) + + def is_absorbing_state(self, state): + trans_probs = self.transition_probabilities + if isinstance(trans_probs, ImmutableMatrix) and \ + state < trans_probs.shape[0]: + return S(trans_probs[state, state]) is S.One + + def is_absorbing_chain(self): + states, A, B, C = self.decompose() + r = A.shape[0] + return And(r > 0, A == Identity(r).as_explicit()) + + def stationary_distribution(self, condition_set=False) -> ImmutableMatrix | ConditionSet | Lambda: + r""" + The stationary distribution is any row vector, p, that solves p = pP, + is row stochastic and each element in p must be nonnegative. + That means in matrix form: :math:`(P-I)^T p^T = 0` and + :math:`(1, \dots, 1) p = 1` + where ``P`` is the one-step transition matrix. + + All time-homogeneous Markov Chains with a finite state space + have at least one stationary distribution. In addition, if + a finite time-homogeneous Markov Chain is irreducible, the + stationary distribution is unique. + + Parameters + ========== + + condition_set : bool + If the chain has a symbolic size or transition matrix, + it will return a ``Lambda`` if ``False`` and return a + ``ConditionSet`` if ``True``. + + Examples + ======== + + >>> from sympy.stats import DiscreteMarkovChain + >>> from sympy import Matrix, S + + An irreducible Markov Chain + + >>> T = Matrix([[S(1)/2, S(1)/2, 0], + ... [S(4)/5, S(1)/5, 0], + ... [1, 0, 0]]) + >>> X = DiscreteMarkovChain('X', trans_probs=T) + >>> X.stationary_distribution() + Matrix([[8/13, 5/13, 0]]) + + A reducible Markov Chain + + >>> T = Matrix([[S(1)/2, S(1)/2, 0], + ... [S(4)/5, S(1)/5, 0], + ... [0, 0, 1]]) + >>> X = DiscreteMarkovChain('X', trans_probs=T) + >>> X.stationary_distribution() + Matrix([[8/13 - 8*tau0/13, 5/13 - 5*tau0/13, tau0]]) + + >>> Y = DiscreteMarkovChain('Y') + >>> Y.stationary_distribution() + Lambda((wm, _T), Eq(wm*_T, wm)) + + >>> Y.stationary_distribution(condition_set=True) + ConditionSet(wm, Eq(wm*_T, wm)) + + References + ========== + + .. [1] https://www.probabilitycourse.com/chapter11/11_2_6_stationary_and_limiting_distributions.php + .. [2] https://web.archive.org/web/20210508104430/https://galton.uchicago.edu/~yibi/teaching/stat317/2014/Lectures/Lecture4_6up.pdf + + See Also + ======== + + sympy.stats.DiscreteMarkovChain.limiting_distribution + """ + trans_probs = self.transition_probabilities + n = self.number_of_states + + if n == 0: + return ImmutableMatrix(Matrix([[]])) + + # symbolic matrix version + if isinstance(trans_probs, MatrixSymbol): + wm = MatrixSymbol('wm', 1, n) + if condition_set: + return ConditionSet(wm, Eq(wm * trans_probs, wm)) + else: + return Lambda((wm, trans_probs), Eq(wm * trans_probs, wm)) + + # numeric matrix version + a = Matrix(trans_probs - Identity(n)).T + a[0, 0:n] = ones(1, n) # type: ignore + b = zeros(n, 1) + b[0, 0] = 1 + + soln = list(linsolve((a, b)))[0] + return ImmutableMatrix([soln]) + + def fixed_row_vector(self): + """ + A wrapper for ``stationary_distribution()``. + """ + return self.stationary_distribution() + + @property + def limiting_distribution(self): + """ + The fixed row vector is the limiting + distribution of a discrete Markov chain. + """ + return self.fixed_row_vector() + + def decompose(self) -> tuple[list[Basic], ImmutableMatrix, ImmutableMatrix, ImmutableMatrix]: + """ + Decomposes the transition matrix into submatrices with + special properties. + + The transition matrix can be decomposed into 4 submatrices: + - A - the submatrix from recurrent states to recurrent states. + - B - the submatrix from transient to recurrent states. + - C - the submatrix from transient to transient states. + - O - the submatrix of zeros for recurrent to transient states. + + Returns + ======= + + states, A, B, C + ``states`` - a list of state names with the first being + the recurrent states and the last being + the transient states in the order + of the row names of A and then the row names of C. + ``A`` - the submatrix from recurrent states to recurrent states. + ``B`` - the submatrix from transient to recurrent states. + ``C`` - the submatrix from transient to transient states. + + Examples + ======== + + >>> from sympy.stats import DiscreteMarkovChain + >>> from sympy import Matrix, S + + One can decompose this chain for example: + + >>> T = Matrix([[S(1)/2, S(1)/2, 0, 0, 0], + ... [S(2)/5, S(1)/5, S(2)/5, 0, 0], + ... [0, 0, 1, 0, 0], + ... [0, 0, S(1)/2, S(1)/2, 0], + ... [S(1)/2, 0, 0, 0, S(1)/2]]) + >>> X = DiscreteMarkovChain('X', trans_probs=T) + >>> states, A, B, C = X.decompose() + >>> states + [2, 0, 1, 3, 4] + + >>> A # recurrent to recurrent + Matrix([[1]]) + + >>> B # transient to recurrent + Matrix([ + [ 0], + [2/5], + [1/2], + [ 0]]) + + >>> C # transient to transient + Matrix([ + [1/2, 1/2, 0, 0], + [2/5, 1/5, 0, 0], + [ 0, 0, 1/2, 0], + [1/2, 0, 0, 1/2]]) + + This means that state 2 is the only absorbing state + (since A is a 1x1 matrix). B is a 4x1 matrix since + the 4 remaining transient states all merge into recurrent + state 2. And C is the 4x4 matrix that shows how the + transient states 0, 1, 3, 4 all interact. + + See Also + ======== + + sympy.stats.DiscreteMarkovChain.communication_classes + sympy.stats.DiscreteMarkovChain.canonical_form + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Absorbing_Markov_chain + .. [2] https://people.brandeis.edu/~igusa/Math56aS08/Math56a_S08_notes015.pdf + """ + trans_probs = self.transition_probabilities + + classes = self.communication_classes() + r_states = [] + t_states = [] + + for states, recurrent, period in classes: + if recurrent: + r_states += states + else: + t_states += states + + states = r_states + t_states + indexes = [self.index_of[state] for state in states] # type: ignore + + A = Matrix(len(r_states), len(r_states), + lambda i, j: trans_probs[indexes[i], indexes[j]]) + + B = Matrix(len(t_states), len(r_states), + lambda i, j: trans_probs[indexes[len(r_states) + i], indexes[j]]) + + C = Matrix(len(t_states), len(t_states), + lambda i, j: trans_probs[indexes[len(r_states) + i], indexes[len(r_states) + j]]) + + return states, A.as_immutable(), B.as_immutable(), C.as_immutable() + + def canonical_form(self) -> tuple[list[Basic], ImmutableMatrix]: + """ + Reorders the one-step transition matrix + so that recurrent states appear first and transient + states appear last. Other representations include inserting + transient states first and recurrent states last. + + Returns + ======= + + states, P_new + ``states`` is the list that describes the order of the + new states in the matrix + so that the ith element in ``states`` is the state of the + ith row of A. + ``P_new`` is the new transition matrix in canonical form. + + Examples + ======== + + >>> from sympy.stats import DiscreteMarkovChain + >>> from sympy import Matrix, S + + You can convert your chain into canonical form: + + >>> T = Matrix([[S(1)/2, S(1)/2, 0, 0, 0], + ... [S(2)/5, S(1)/5, S(2)/5, 0, 0], + ... [0, 0, 1, 0, 0], + ... [0, 0, S(1)/2, S(1)/2, 0], + ... [S(1)/2, 0, 0, 0, S(1)/2]]) + >>> X = DiscreteMarkovChain('X', list(range(1, 6)), trans_probs=T) + >>> states, new_matrix = X.canonical_form() + >>> states + [3, 1, 2, 4, 5] + + >>> new_matrix + Matrix([ + [ 1, 0, 0, 0, 0], + [ 0, 1/2, 1/2, 0, 0], + [2/5, 2/5, 1/5, 0, 0], + [1/2, 0, 0, 1/2, 0], + [ 0, 1/2, 0, 0, 1/2]]) + + The new states are [3, 1, 2, 4, 5] and you can + create a new chain with this and its canonical + form will remain the same (since it is already + in canonical form). + + >>> X = DiscreteMarkovChain('X', states, new_matrix) + >>> states, new_matrix = X.canonical_form() + >>> states + [3, 1, 2, 4, 5] + + >>> new_matrix + Matrix([ + [ 1, 0, 0, 0, 0], + [ 0, 1/2, 1/2, 0, 0], + [2/5, 2/5, 1/5, 0, 0], + [1/2, 0, 0, 1/2, 0], + [ 0, 1/2, 0, 0, 1/2]]) + + This is not limited to absorbing chains: + + >>> T = Matrix([[0, 5, 5, 0, 0], + ... [0, 0, 0, 10, 0], + ... [5, 0, 5, 0, 0], + ... [0, 10, 0, 0, 0], + ... [0, 3, 0, 3, 4]])/10 + >>> X = DiscreteMarkovChain('X', trans_probs=T) + >>> states, new_matrix = X.canonical_form() + >>> states + [1, 3, 0, 2, 4] + + >>> new_matrix + Matrix([ + [ 0, 1, 0, 0, 0], + [ 1, 0, 0, 0, 0], + [ 1/2, 0, 0, 1/2, 0], + [ 0, 0, 1/2, 1/2, 0], + [3/10, 3/10, 0, 0, 2/5]]) + + See Also + ======== + + sympy.stats.DiscreteMarkovChain.communication_classes + sympy.stats.DiscreteMarkovChain.decompose + + References + ========== + + .. [1] https://onlinelibrary.wiley.com/doi/pdf/10.1002/9780470316887.app1 + .. [2] http://www.columbia.edu/~ww2040/6711F12/lect1023big.pdf + """ + states, A, B, C = self.decompose() + O = zeros(A.shape[0], C.shape[1]) + return states, BlockMatrix([[A, O], [B, C]]).as_explicit() + + def sample(self): + """ + Returns + ======= + + sample: iterator object + iterator object containing the sample + + """ + if not isinstance(self.transition_probabilities, (Matrix, ImmutableMatrix)): + raise ValueError("Transition Matrix must be provided for sampling") + Tlist = self.transition_probabilities.tolist() + samps = [random.choice(list(self.state_space))] + yield samps[0] + time = 1 + densities = {} + for state in self.state_space: + states = list(self.state_space) + densities[state] = {states[i]: Tlist[state][i] + for i in range(len(states))} + while time < S.Infinity: + samps.append((next(sample_iter(FiniteRV("_", densities[samps[time - 1]]))))) + yield samps[time] + time += 1 + +class ContinuousMarkovChain(ContinuousTimeStochasticProcess, MarkovProcess): + """ + Represents continuous time Markov chain. + + Parameters + ========== + + sym : Symbol/str + state_space : Set + Optional, by default, S.Reals + gen_mat : Matrix/ImmutableMatrix/MatrixSymbol + Optional, by default, None + + Examples + ======== + + >>> from sympy.stats import ContinuousMarkovChain, P + >>> from sympy import Matrix, S, Eq, Gt + >>> G = Matrix([[-S(1), S(1)], [S(1), -S(1)]]) + >>> C = ContinuousMarkovChain('C', state_space=[0, 1], gen_mat=G) + >>> C.limiting_distribution() + Matrix([[1/2, 1/2]]) + >>> C.state_space + {0, 1} + >>> C.generator_matrix + Matrix([ + [-1, 1], + [ 1, -1]]) + + Probability queries are supported + + >>> P(Eq(C(1.96), 0), Eq(C(0.78), 1)).round(5) + 0.45279 + >>> P(Gt(C(1.7), 0), Eq(C(0.82), 1)).round(5) + 0.58602 + + Probability of expressions with multiple RandomIndexedSymbols + can also be calculated provided there is only 1 RandomIndexedSymbol + in the given condition. It is always better to use Rational instead + of floating point numbers for the probabilities in the + generator matrix to avoid errors. + + >>> from sympy import Gt, Le, Rational + >>> G = Matrix([[-S(1), Rational(1, 10), Rational(9, 10)], [Rational(2, 5), -S(1), Rational(3, 5)], [Rational(1, 2), Rational(1, 2), -S(1)]]) + >>> C = ContinuousMarkovChain('C', state_space=[0, 1, 2], gen_mat=G) + >>> P(Eq(C(3.92), C(1.75)), Eq(C(0.46), 0)).round(5) + 0.37933 + >>> P(Gt(C(3.92), C(1.75)), Eq(C(0.46), 0)).round(5) + 0.34211 + >>> P(Le(C(1.57), C(3.14)), Eq(C(1.22), 1)).round(4) + 0.7143 + + Symbolic probability queries are also supported + + >>> from sympy import symbols + >>> a,b,c,d = symbols('a b c d') + >>> G = Matrix([[-S(1), Rational(1, 10), Rational(9, 10)], [Rational(2, 5), -S(1), Rational(3, 5)], [Rational(1, 2), Rational(1, 2), -S(1)]]) + >>> C = ContinuousMarkovChain('C', state_space=[0, 1, 2], gen_mat=G) + >>> query = P(Eq(C(a), b), Eq(C(c), d)) + >>> query.subs({a:3.65, b:2, c:1.78, d:1}).evalf().round(10) + 0.4002723175 + >>> P(Eq(C(3.65), 2), Eq(C(1.78), 1)).round(10) + 0.4002723175 + >>> query_gt = P(Gt(C(a), b), Eq(C(c), d)) + >>> query_gt.subs({a:43.2, b:0, c:3.29, d:2}).evalf().round(10) + 0.6832579186 + >>> P(Gt(C(43.2), 0), Eq(C(3.29), 2)).round(10) + 0.6832579186 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Markov_chain#Continuous-time_Markov_chain + .. [2] https://u.math.biu.ac.il/~amirgi/CTMCnotes.pdf + """ + index_set = S.Reals + + def __new__(cls, sym, state_space=None, gen_mat=None): + sym = _symbol_converter(sym) + state_space, gen_mat = MarkovProcess._sanity_checks(state_space, gen_mat) + obj = Basic.__new__(cls, sym, state_space, gen_mat) + indices = {} + if isinstance(obj.number_of_states, Integer): + for index, state in enumerate(obj.state_space): + indices[state] = index + obj.index_of = indices + return obj + + @property + def generator_matrix(self): + return self.args[2] + + @cacheit + def transition_probabilities(self, gen_mat=None): + t = Dummy('t') + if isinstance(gen_mat, (Matrix, ImmutableMatrix)) and \ + gen_mat.is_diagonalizable(): + # for faster computation use diagonalized generator matrix + Q, D = gen_mat.diagonalize() + return Lambda(t, Q*exp(t*D)*Q.inv()) + if gen_mat != None: + return Lambda(t, exp(t*gen_mat)) + + def limiting_distribution(self): + gen_mat = self.generator_matrix + if gen_mat is None: + return None + if isinstance(gen_mat, MatrixSymbol): + wm = MatrixSymbol('wm', 1, gen_mat.shape[0]) + return Lambda((wm, gen_mat), Eq(wm*gen_mat, wm)) + w = IndexedBase('w') + wi = [w[i] for i in range(gen_mat.shape[0])] + wm = Matrix([wi]) + eqs = (wm*gen_mat).tolist()[0] + eqs.append(sum(wi) - 1) + soln = list(linsolve(eqs, wi))[0] + return ImmutableMatrix([soln]) + + +class BernoulliProcess(DiscreteTimeStochasticProcess): + """ + The Bernoulli process consists of repeated + independent Bernoulli process trials with the same parameter `p`. + It's assumed that the probability `p` applies to every + trial and that the outcomes of each trial + are independent of all the rest. Therefore Bernoulli Process + is Discrete State and Discrete Time Stochastic Process. + + Parameters + ========== + + sym : Symbol/str + success : Integer/str + The event which is considered to be success. Default: 1. + failure: Integer/str + The event which is considered to be failure. Default: 0. + p : Real Number between 0 and 1 + Represents the probability of getting success. + + Examples + ======== + + >>> from sympy.stats import BernoulliProcess, P, E + >>> from sympy import Eq, Gt + >>> B = BernoulliProcess("B", p=0.7, success=1, failure=0) + >>> B.state_space + {0, 1} + >>> B.p.round(2) + 0.70 + >>> B.success + 1 + >>> B.failure + 0 + >>> X = B[1] + B[2] + B[3] + >>> P(Eq(X, 0)).round(2) + 0.03 + >>> P(Eq(X, 2)).round(2) + 0.44 + >>> P(Eq(X, 4)).round(2) + 0 + >>> P(Gt(X, 1)).round(2) + 0.78 + >>> P(Eq(B[1], 0) & Eq(B[2], 1) & Eq(B[3], 0) & Eq(B[4], 1)).round(2) + 0.04 + >>> B.joint_distribution(B[1], B[2]) + JointDistributionHandmade(Lambda((B[1], B[2]), Piecewise((0.7, Eq(B[1], 1)), + (0.3, Eq(B[1], 0)), (0, True))*Piecewise((0.7, Eq(B[2], 1)), (0.3, Eq(B[2], 0)), + (0, True)))) + >>> E(2*B[1] + B[2]).round(2) + 2.10 + >>> P(B[1] < 1).round(2) + 0.30 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Bernoulli_process + .. [2] https://mathcs.clarku.edu/~djoyce/ma217/bernoulli.pdf + + """ + + index_set = S.Naturals0 + + def __new__(cls, sym, p, success=1, failure=0): + _value_check(p >= 0 and p <= 1, 'Value of p must be between 0 and 1.') + sym = _symbol_converter(sym) + p = _sympify(p) + success = _sym_sympify(success) + failure = _sym_sympify(failure) + return Basic.__new__(cls, sym, p, success, failure) + + @property + def symbol(self): + return self.args[0] + + @property + def p(self): + return self.args[1] + + @property + def success(self): + return self.args[2] + + @property + def failure(self): + return self.args[3] + + @property + def state_space(self): + return _set_converter([self.success, self.failure]) + + def distribution(self, key=None): + if key is None: + self._deprecation_warn_distribution() + return BernoulliDistribution(self.p) + return BernoulliDistribution(self.p, self.success, self.failure) + + def simple_rv(self, rv): + return Bernoulli(rv.name, p=self.p, + succ=self.success, fail=self.failure) + + def expectation(self, expr, condition=None, evaluate=True, **kwargs): + """ + Computes expectation. + + Parameters + ========== + + expr : RandomIndexedSymbol, Relational, Logic + Condition for which expectation has to be computed. Must + contain a RandomIndexedSymbol of the process. + condition : Relational, Logic + The given conditions under which computations should be done. + + Returns + ======= + + Expectation of the RandomIndexedSymbol. + + """ + + return _SubstituteRV._expectation(expr, condition, evaluate, **kwargs) + + def probability(self, condition, given_condition=None, evaluate=True, **kwargs): + """ + Computes probability. + + Parameters + ========== + + condition : Relational + Condition for which probability has to be computed. Must + contain a RandomIndexedSymbol of the process. + given_condition : Relational, Logic + The given conditions under which computations should be done. + + Returns + ======= + + Probability of the condition. + + """ + + return _SubstituteRV._probability(condition, given_condition, evaluate, **kwargs) + + def density(self, x): + return Piecewise((self.p, Eq(x, self.success)), + (1 - self.p, Eq(x, self.failure)), + (S.Zero, True)) + +class _SubstituteRV: + """ + Internal class to handle the queries of expectation and probability + by substitution. + """ + + @staticmethod + def _rvindexed_subs(expr, condition=None): + """ + Substitutes the RandomIndexedSymbol with the RandomSymbol with + same name, distribution and probability as RandomIndexedSymbol. + + Parameters + ========== + + expr: RandomIndexedSymbol, Relational, Logic + Condition for which expectation has to be computed. Must + contain a RandomIndexedSymbol of the process. + condition: Relational, Logic + The given conditions under which computations should be done. + + """ + + rvs_expr = random_symbols(expr) + if len(rvs_expr) != 0: + swapdict_expr = {} + for rv in rvs_expr: + if isinstance(rv, RandomIndexedSymbol): + newrv = rv.pspace.process.simple_rv(rv) # substitute with equivalent simple rv + swapdict_expr[rv] = newrv + expr = expr.subs(swapdict_expr) + rvs_cond = random_symbols(condition) + if len(rvs_cond)!=0: + swapdict_cond = {} + for rv in rvs_cond: + if isinstance(rv, RandomIndexedSymbol): + newrv = rv.pspace.process.simple_rv(rv) + swapdict_cond[rv] = newrv + condition = condition.subs(swapdict_cond) + return expr, condition + + @classmethod + def _expectation(self, expr, condition=None, evaluate=True, **kwargs): + """ + Internal method for computing expectation of indexed RV. + + Parameters + ========== + + expr: RandomIndexedSymbol, Relational, Logic + Condition for which expectation has to be computed. Must + contain a RandomIndexedSymbol of the process. + condition: Relational, Logic + The given conditions under which computations should be done. + + Returns + ======= + + Expectation of the RandomIndexedSymbol. + + """ + new_expr, new_condition = self._rvindexed_subs(expr, condition) + + if not is_random(new_expr): + return new_expr + new_pspace = pspace(new_expr) + if new_condition is not None: + new_expr = given(new_expr, new_condition) + if new_expr.is_Add: # As E is Linear + return Add(*[new_pspace.compute_expectation( + expr=arg, evaluate=evaluate, **kwargs) + for arg in new_expr.args]) + return new_pspace.compute_expectation( + new_expr, evaluate=evaluate, **kwargs) + + @classmethod + def _probability(self, condition, given_condition=None, evaluate=True, **kwargs): + """ + Internal method for computing probability of indexed RV + + Parameters + ========== + + condition: Relational + Condition for which probability has to be computed. Must + contain a RandomIndexedSymbol of the process. + given_condition: Relational/And + The given conditions under which computations should be done. + + Returns + ======= + + Probability of the condition. + + """ + new_condition, new_givencondition = self._rvindexed_subs(condition, given_condition) + + if isinstance(new_givencondition, RandomSymbol): + condrv = random_symbols(new_condition) + if len(condrv) == 1 and condrv[0] == new_givencondition: + return BernoulliDistribution(self._probability(new_condition), 0, 1) + + if any(dependent(rv, new_givencondition) for rv in condrv): + return Probability(new_condition, new_givencondition) + else: + return self._probability(new_condition) + + if new_givencondition is not None and \ + not isinstance(new_givencondition, (Relational, Boolean)): + raise ValueError("%s is not a relational or combination of relationals" + % (new_givencondition)) + if new_givencondition == False or new_condition == False: + return S.Zero + if new_condition == True: + return S.One + if not isinstance(new_condition, (Relational, Boolean)): + raise ValueError("%s is not a relational or combination of relationals" + % (new_condition)) + + if new_givencondition is not None: # If there is a condition + # Recompute on new conditional expr + return self._probability(given(new_condition, new_givencondition, **kwargs), **kwargs) + result = pspace(new_condition).probability(new_condition, **kwargs) + if evaluate and hasattr(result, 'doit'): + return result.doit() + else: + return result + +def get_timerv_swaps(expr, condition): + """ + Finds the appropriate interval for each time stamp in expr by parsing + the given condition and returns intervals for each timestamp and + dictionary that maps variable time-stamped Random Indexed Symbol to its + corresponding Random Indexed variable with fixed time stamp. + + Parameters + ========== + + expr: SymPy Expression + Expression containing Random Indexed Symbols with variable time stamps + condition: Relational/Boolean Expression + Expression containing time bounds of variable time stamps in expr + + Examples + ======== + + >>> from sympy.stats.stochastic_process_types import get_timerv_swaps, PoissonProcess + >>> from sympy import symbols, Contains, Interval + >>> x, t, d = symbols('x t d', positive=True) + >>> X = PoissonProcess("X", 3) + >>> get_timerv_swaps(x*X(t), Contains(t, Interval.Lopen(0, 1))) + ([Interval.Lopen(0, 1)], {X(t): X(1)}) + >>> get_timerv_swaps((X(t)**2 + X(d)**2), Contains(t, Interval.Lopen(0, 1)) + ... & Contains(d, Interval.Ropen(1, 4))) # doctest: +SKIP + ([Interval.Ropen(1, 4), Interval.Lopen(0, 1)], {X(d): X(3), X(t): X(1)}) + + Returns + ======= + + intervals: list + List of Intervals/FiniteSet on which each time stamp is defined + rv_swap: dict + Dictionary mapping variable time Random Indexed Symbol to constant time + Random Indexed Variable + + """ + + if not isinstance(condition, (Relational, Boolean)): + raise ValueError("%s is not a relational or combination of relationals" + % (condition)) + expr_syms = list(expr.atoms(RandomIndexedSymbol)) + if isinstance(condition, (And, Or)): + given_cond_args = condition.args + else: # single condition + given_cond_args = (condition, ) + rv_swap = {} + intervals = [] + for expr_sym in expr_syms: + for arg in given_cond_args: + if arg.has(expr_sym.key) and isinstance(expr_sym.key, Symbol): + intv = _set_converter(arg.args[1]) + diff_key = intv._sup - intv._inf + if diff_key == oo: + raise ValueError("%s should have finite bounds" % str(expr_sym.name)) + elif diff_key == S.Zero: # has singleton set + diff_key = intv._sup + rv_swap[expr_sym] = expr_sym.subs({expr_sym.key: diff_key}) + intervals.append(intv) + return intervals, rv_swap + + +class CountingProcess(ContinuousTimeStochasticProcess): + """ + This class handles the common methods of the Counting Processes + such as Poisson, Wiener and Gamma Processes + """ + index_set = _set_converter(Interval(0, oo)) + + @property + def symbol(self): + return self.args[0] + + def expectation(self, expr, condition=None, evaluate=True, **kwargs): + """ + Computes expectation + + Parameters + ========== + + expr: RandomIndexedSymbol, Relational, Logic + Condition for which expectation has to be computed. Must + contain a RandomIndexedSymbol of the process. + condition: Relational, Boolean + The given conditions under which computations should be done, i.e, + the intervals on which each variable time stamp in expr is defined + + Returns + ======= + + Expectation of the given expr + + """ + if condition is not None: + intervals, rv_swap = get_timerv_swaps(expr, condition) + # they are independent when they have non-overlapping intervals + if len(intervals) == 1 or all(Intersection(*intv_comb) == EmptySet + for intv_comb in itertools.combinations(intervals, 2)): + if expr.is_Add: + return Add.fromiter(self.expectation(arg, condition) + for arg in expr.args) + expr = expr.subs(rv_swap) + else: + return Expectation(expr, condition) + + return _SubstituteRV._expectation(expr, evaluate=evaluate, **kwargs) + + def _solve_argwith_tworvs(self, arg): + if arg.args[0].key >= arg.args[1].key or isinstance(arg, Eq): + diff_key = abs(arg.args[0].key - arg.args[1].key) + rv = arg.args[0] + arg = arg.__class__(rv.pspace.process(diff_key), 0) + else: + diff_key = arg.args[1].key - arg.args[0].key + rv = arg.args[1] + arg = arg.__class__(rv.pspace.process(diff_key), 0) + return arg + + def _solve_numerical(self, condition, given_condition=None): + if isinstance(condition, And): + args_list = list(condition.args) + else: + args_list = [condition] + if given_condition is not None: + if isinstance(given_condition, And): + args_list.extend(list(given_condition.args)) + else: + args_list.extend([given_condition]) + # sort the args based on timestamp to get the independent increments in + # each segment using all the condition args as well as given_condition args + args_list = sorted(args_list, key=lambda x: x.args[0].key) + result = [] + cond_args = list(condition.args) if isinstance(condition, And) else [condition] + if args_list[0] in cond_args and not (is_random(args_list[0].args[0]) + and is_random(args_list[0].args[1])): + result.append(_SubstituteRV._probability(args_list[0])) + + if is_random(args_list[0].args[0]) and is_random(args_list[0].args[1]): + arg = self._solve_argwith_tworvs(args_list[0]) + result.append(_SubstituteRV._probability(arg)) + + for i in range(len(args_list) - 1): + curr, nex = args_list[i], args_list[i + 1] + diff_key = nex.args[0].key - curr.args[0].key + working_set = curr.args[0].pspace.process.state_space + if curr.args[1] > nex.args[1]: #impossible condition so return 0 + result.append(0) + break + if isinstance(curr, Eq): + working_set = Intersection(working_set, Interval.Lopen(curr.args[1], oo)) + else: + working_set = Intersection(working_set, curr.as_set()) + if isinstance(nex, Eq): + working_set = Intersection(working_set, Interval(-oo, nex.args[1])) + else: + working_set = Intersection(working_set, nex.as_set()) + if working_set == EmptySet: + rv = Eq(curr.args[0].pspace.process(diff_key), 0) + result.append(_SubstituteRV._probability(rv)) + else: + if working_set.is_finite_set: + if isinstance(curr, Eq) and isinstance(nex, Eq): + rv = Eq(curr.args[0].pspace.process(diff_key), len(working_set)) + result.append(_SubstituteRV._probability(rv)) + elif isinstance(curr, Eq) ^ isinstance(nex, Eq): + result.append(Add.fromiter(_SubstituteRV._probability(Eq( + curr.args[0].pspace.process(diff_key), x)) + for x in range(len(working_set)))) + else: + n = len(working_set) + result.append(Add.fromiter((n - x)*_SubstituteRV._probability(Eq( + curr.args[0].pspace.process(diff_key), x)) for x in range(n))) + else: + result.append(_SubstituteRV._probability( + curr.args[0].pspace.process(diff_key) <= working_set._sup - working_set._inf)) + return Mul.fromiter(result) + + + def probability(self, condition, given_condition=None, evaluate=True, **kwargs): + """ + Computes probability. + + Parameters + ========== + + condition: Relational + Condition for which probability has to be computed. Must + contain a RandomIndexedSymbol of the process. + given_condition: Relational, Boolean + The given conditions under which computations should be done, i.e, + the intervals on which each variable time stamp in expr is defined + + Returns + ======= + + Probability of the condition + + """ + check_numeric = True + if isinstance(condition, (And, Or)): + cond_args = condition.args + else: + cond_args = (condition, ) + # check that condition args are numeric or not + if not all(arg.args[0].key.is_number for arg in cond_args): + check_numeric = False + if given_condition is not None: + check_given_numeric = True + if isinstance(given_condition, (And, Or)): + given_cond_args = given_condition.args + else: + given_cond_args = (given_condition, ) + # check that given condition args are numeric or not + if given_condition.has(Contains): + check_given_numeric = False + # Handle numerical queries + if check_numeric and check_given_numeric: + res = [] + if isinstance(condition, Or): + res.append(Add.fromiter(self._solve_numerical(arg, given_condition) + for arg in condition.args)) + if isinstance(given_condition, Or): + res.append(Add.fromiter(self._solve_numerical(condition, arg) + for arg in given_condition.args)) + if res: + return Add.fromiter(res) + return self._solve_numerical(condition, given_condition) + + # No numeric queries, go by Contains?... then check that all the + # given condition are in form of `Contains` + if not all(arg.has(Contains) for arg in given_cond_args): + raise ValueError("If given condition is passed with `Contains`, then " + "please pass the evaluated condition with its corresponding information " + "in terms of intervals of each time stamp to be passed in given condition.") + + intervals, rv_swap = get_timerv_swaps(condition, given_condition) + # they are independent when they have non-overlapping intervals + if len(intervals) == 1 or all(Intersection(*intv_comb) == EmptySet + for intv_comb in itertools.combinations(intervals, 2)): + if isinstance(condition, And): + return Mul.fromiter(self.probability(arg, given_condition) + for arg in condition.args) + elif isinstance(condition, Or): + return Add.fromiter(self.probability(arg, given_condition) + for arg in condition.args) + condition = condition.subs(rv_swap) + else: + return Probability(condition, given_condition) + if check_numeric: + return self._solve_numerical(condition) + return _SubstituteRV._probability(condition, evaluate=evaluate, **kwargs) + +class PoissonProcess(CountingProcess): + """ + The Poisson process is a counting process. It is usually used in scenarios + where we are counting the occurrences of certain events that appear + to happen at a certain rate, but completely at random. + + Parameters + ========== + + sym : Symbol/str + lamda : Positive number + Rate of the process, ``lambda > 0`` + + Examples + ======== + + >>> from sympy.stats import PoissonProcess, P, E + >>> from sympy import symbols, Eq, Ne, Contains, Interval + >>> X = PoissonProcess("X", lamda=3) + >>> X.state_space + Naturals0 + >>> X.lamda + 3 + >>> t1, t2 = symbols('t1 t2', positive=True) + >>> P(X(t1) < 4) + (9*t1**3/2 + 9*t1**2/2 + 3*t1 + 1)*exp(-3*t1) + >>> P(Eq(X(t1), 2) | Ne(X(t1), 4), Contains(t1, Interval.Ropen(2, 4))) + 1 - 36*exp(-6) + >>> P(Eq(X(t1), 2) & Eq(X(t2), 3), Contains(t1, Interval.Lopen(0, 2)) + ... & Contains(t2, Interval.Lopen(2, 4))) + 648*exp(-12) + >>> E(X(t1)) + 3*t1 + >>> E(X(t1)**2 + 2*X(t2), Contains(t1, Interval.Lopen(0, 1)) + ... & Contains(t2, Interval.Lopen(1, 2))) + 18 + >>> P(X(3) < 1, Eq(X(1), 0)) + exp(-6) + >>> P(Eq(X(4), 3), Eq(X(2), 3)) + exp(-6) + >>> P(X(2) <= 3, X(1) > 1) + 5*exp(-3) + + Merging two Poisson Processes + + >>> Y = PoissonProcess("Y", lamda=4) + >>> Z = X + Y + >>> Z.lamda + 7 + + Splitting a Poisson Process into two independent Poisson Processes + + >>> N, M = Z.split(l1=2, l2=5) + >>> N.lamda, M.lamda + (2, 5) + + References + ========== + + .. [1] https://www.probabilitycourse.com/chapter11/11_0_0_intro.php + .. [2] https://en.wikipedia.org/wiki/Poisson_point_process + + """ + + def __new__(cls, sym, lamda): + _value_check(lamda > 0, 'lamda should be a positive number.') + sym = _symbol_converter(sym) + lamda = _sympify(lamda) + return Basic.__new__(cls, sym, lamda) + + @property + def lamda(self): + return self.args[1] + + @property + def state_space(self): + return S.Naturals0 + + def distribution(self, key): + if isinstance(key, RandomIndexedSymbol): + self._deprecation_warn_distribution() + return PoissonDistribution(self.lamda*key.key) + return PoissonDistribution(self.lamda*key) + + def density(self, x): + return (self.lamda*x.key)**x / factorial(x) * exp(-(self.lamda*x.key)) + + def simple_rv(self, rv): + return Poisson(rv.name, lamda=self.lamda*rv.key) + + def __add__(self, other): + if not isinstance(other, PoissonProcess): + raise ValueError("Only instances of Poisson Process can be merged") + return PoissonProcess(Dummy(self.symbol.name + other.symbol.name), + self.lamda + other.lamda) + + def split(self, l1, l2): + if _sympify(l1 + l2) != self.lamda: + raise ValueError("Sum of l1 and l2 should be %s" % str(self.lamda)) + return PoissonProcess(Dummy("l1"), l1), PoissonProcess(Dummy("l2"), l2) + +class WienerProcess(CountingProcess): + """ + The Wiener process is a real valued continuous-time stochastic process. + In physics it is used to study Brownian motion and it is often also called + Brownian motion due to its historical connection with physical process of the + same name originally observed by Scottish botanist Robert Brown. + + Parameters + ========== + + sym : Symbol/str + + Examples + ======== + + >>> from sympy.stats import WienerProcess, P, E + >>> from sympy import symbols, Contains, Interval + >>> X = WienerProcess("X") + >>> X.state_space + Reals + >>> t1, t2 = symbols('t1 t2', positive=True) + >>> P(X(t1) < 7).simplify() + erf(7*sqrt(2)/(2*sqrt(t1)))/2 + 1/2 + >>> P((X(t1) > 2) | (X(t1) < 4), Contains(t1, Interval.Ropen(2, 4))).simplify() + -erf(1)/2 + erf(2)/2 + 1 + >>> E(X(t1)) + 0 + >>> E(X(t1) + 2*X(t2), Contains(t1, Interval.Lopen(0, 1)) + ... & Contains(t2, Interval.Lopen(1, 2))) + 0 + + References + ========== + + .. [1] https://www.probabilitycourse.com/chapter11/11_4_0_brownian_motion_wiener_process.php + .. [2] https://en.wikipedia.org/wiki/Wiener_process + + """ + def __new__(cls, sym): + sym = _symbol_converter(sym) + return Basic.__new__(cls, sym) + + @property + def state_space(self): + return S.Reals + + def distribution(self, key): + if isinstance(key, RandomIndexedSymbol): + self._deprecation_warn_distribution() + return NormalDistribution(0, sqrt(key.key)) + return NormalDistribution(0, sqrt(key)) + + def density(self, x): + return exp(-x**2/(2*x.key)) / (sqrt(2*pi)*sqrt(x.key)) + + def simple_rv(self, rv): + return Normal(rv.name, 0, sqrt(rv.key)) + + +class GammaProcess(CountingProcess): + r""" + A Gamma process is a random process with independent gamma distributed + increments. It is a pure-jump increasing Levy process. + + Parameters + ========== + + sym : Symbol/str + lamda : Positive number + Jump size of the process, ``lamda > 0`` + gamma : Positive number + Rate of jump arrivals, `\gamma > 0` + + Examples + ======== + + >>> from sympy.stats import GammaProcess, E, P, variance + >>> from sympy import symbols, Contains, Interval, Not + >>> t, d, x, l, g = symbols('t d x l g', positive=True) + >>> X = GammaProcess("X", l, g) + >>> E(X(t)) + g*t/l + >>> variance(X(t)).simplify() + g*t/l**2 + >>> X = GammaProcess('X', 1, 2) + >>> P(X(t) < 1).simplify() + lowergamma(2*t, 1)/gamma(2*t) + >>> P(Not((X(t) < 5) & (X(d) > 3)), Contains(t, Interval.Ropen(2, 4)) & + ... Contains(d, Interval.Lopen(7, 8))).simplify() + -4*exp(-3) + 472*exp(-8)/3 + 1 + >>> E(X(2) + x*E(X(5))) + 10*x + 4 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Gamma_process + + """ + def __new__(cls, sym, lamda, gamma): + _value_check(lamda > 0, 'lamda should be a positive number') + _value_check(gamma > 0, 'gamma should be a positive number') + sym = _symbol_converter(sym) + gamma = _sympify(gamma) + lamda = _sympify(lamda) + return Basic.__new__(cls, sym, lamda, gamma) + + @property + def lamda(self): + return self.args[1] + + @property + def gamma(self): + return self.args[2] + + @property + def state_space(self): + return _set_converter(Interval(0, oo)) + + def distribution(self, key): + if isinstance(key, RandomIndexedSymbol): + self._deprecation_warn_distribution() + return GammaDistribution(self.gamma*key.key, 1/self.lamda) + return GammaDistribution(self.gamma*key, 1/self.lamda) + + def density(self, x): + k = self.gamma*x.key + theta = 1/self.lamda + return x**(k - 1) * exp(-x/theta) / (gamma(k)*theta**k) + + def simple_rv(self, rv): + return Gamma(rv.name, self.gamma*rv.key, 1/self.lamda) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/symbolic_multivariate_probability.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/symbolic_multivariate_probability.py new file mode 100644 index 0000000000000000000000000000000000000000..bbe8776e58e82489e29734cea48c9138bc512f34 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/symbolic_multivariate_probability.py @@ -0,0 +1,308 @@ +import itertools + +from sympy.core.add import Add +from sympy.core.expr import Expr +from sympy.core.function import expand as _expand +from sympy.core.mul import Mul +from sympy.core.singleton import S +from sympy.matrices.exceptions import ShapeError +from sympy.matrices.expressions.matexpr import MatrixExpr +from sympy.matrices.expressions.matmul import MatMul +from sympy.matrices.expressions.special import ZeroMatrix +from sympy.stats.rv import RandomSymbol, is_random +from sympy.core.sympify import _sympify +from sympy.stats.symbolic_probability import Variance, Covariance, Expectation + + +class ExpectationMatrix(Expectation, MatrixExpr): + """ + Expectation of a random matrix expression. + + Examples + ======== + + >>> from sympy.stats import ExpectationMatrix, Normal + >>> from sympy.stats.rv import RandomMatrixSymbol + >>> from sympy import symbols, MatrixSymbol, Matrix + >>> k = symbols("k") + >>> A, B = MatrixSymbol("A", k, k), MatrixSymbol("B", k, k) + >>> X, Y = RandomMatrixSymbol("X", k, 1), RandomMatrixSymbol("Y", k, 1) + >>> ExpectationMatrix(X) + ExpectationMatrix(X) + >>> ExpectationMatrix(A*X).shape + (k, 1) + + To expand the expectation in its expression, use ``expand()``: + + >>> ExpectationMatrix(A*X + B*Y).expand() + A*ExpectationMatrix(X) + B*ExpectationMatrix(Y) + >>> ExpectationMatrix((X + Y)*(X - Y).T).expand() + ExpectationMatrix(X*X.T) - ExpectationMatrix(X*Y.T) + ExpectationMatrix(Y*X.T) - ExpectationMatrix(Y*Y.T) + + To evaluate the ``ExpectationMatrix``, use ``doit()``: + + >>> N11, N12 = Normal('N11', 11, 1), Normal('N12', 12, 1) + >>> N21, N22 = Normal('N21', 21, 1), Normal('N22', 22, 1) + >>> M11, M12 = Normal('M11', 1, 1), Normal('M12', 2, 1) + >>> M21, M22 = Normal('M21', 3, 1), Normal('M22', 4, 1) + >>> x1 = Matrix([[N11, N12], [N21, N22]]) + >>> x2 = Matrix([[M11, M12], [M21, M22]]) + >>> ExpectationMatrix(x1 + x2).doit() + Matrix([ + [12, 14], + [24, 26]]) + + """ + def __new__(cls, expr, condition=None): + expr = _sympify(expr) + if condition is None: + if not is_random(expr): + return expr + obj = Expr.__new__(cls, expr) + else: + condition = _sympify(condition) + obj = Expr.__new__(cls, expr, condition) + + obj._shape = expr.shape + obj._condition = condition + return obj + + @property + def shape(self): + return self._shape + + def expand(self, **hints): + expr = self.args[0] + condition = self._condition + if not is_random(expr): + return expr + + if isinstance(expr, Add): + return Add.fromiter(Expectation(a, condition=condition).expand() + for a in expr.args) + + expand_expr = _expand(expr) + if isinstance(expand_expr, Add): + return Add.fromiter(Expectation(a, condition=condition).expand() + for a in expand_expr.args) + + elif isinstance(expr, (Mul, MatMul)): + rv = [] + nonrv = [] + postnon = [] + + for a in expr.args: + if is_random(a): + if rv: + rv.extend(postnon) + else: + nonrv.extend(postnon) + postnon = [] + rv.append(a) + elif a.is_Matrix: + postnon.append(a) + else: + nonrv.append(a) + + # In order to avoid infinite-looping (MatMul may call .doit() again), + # do not rebuild + if len(nonrv) == 0: + return self + return Mul.fromiter(nonrv)*Expectation(Mul.fromiter(rv), + condition=condition)*Mul.fromiter(postnon) + + return self + +class VarianceMatrix(Variance, MatrixExpr): + """ + Variance of a random matrix probability expression. Also known as + Covariance matrix, auto-covariance matrix, dispersion matrix, + or variance-covariance matrix. + + Examples + ======== + + >>> from sympy.stats import VarianceMatrix + >>> from sympy.stats.rv import RandomMatrixSymbol + >>> from sympy import symbols, MatrixSymbol + >>> k = symbols("k") + >>> A, B = MatrixSymbol("A", k, k), MatrixSymbol("B", k, k) + >>> X, Y = RandomMatrixSymbol("X", k, 1), RandomMatrixSymbol("Y", k, 1) + >>> VarianceMatrix(X) + VarianceMatrix(X) + >>> VarianceMatrix(X).shape + (k, k) + + To expand the variance in its expression, use ``expand()``: + + >>> VarianceMatrix(A*X).expand() + A*VarianceMatrix(X)*A.T + >>> VarianceMatrix(A*X + B*Y).expand() + 2*A*CrossCovarianceMatrix(X, Y)*B.T + A*VarianceMatrix(X)*A.T + B*VarianceMatrix(Y)*B.T + """ + def __new__(cls, arg, condition=None): + arg = _sympify(arg) + + if 1 not in arg.shape: + raise ShapeError("Expression is not a vector") + + shape = (arg.shape[0], arg.shape[0]) if arg.shape[1] == 1 else (arg.shape[1], arg.shape[1]) + + if condition: + obj = Expr.__new__(cls, arg, condition) + else: + obj = Expr.__new__(cls, arg) + + obj._shape = shape + obj._condition = condition + return obj + + @property + def shape(self): + return self._shape + + def expand(self, **hints): + arg = self.args[0] + condition = self._condition + + if not is_random(arg): + return ZeroMatrix(*self.shape) + + if isinstance(arg, RandomSymbol): + return self + elif isinstance(arg, Add): + rv = [] + for a in arg.args: + if is_random(a): + rv.append(a) + variances = Add(*(Variance(xv, condition).expand() for xv in rv)) + map_to_covar = lambda x: 2*Covariance(*x, condition=condition).expand() + covariances = Add(*map(map_to_covar, itertools.combinations(rv, 2))) + return variances + covariances + elif isinstance(arg, (Mul, MatMul)): + nonrv = [] + rv = [] + for a in arg.args: + if is_random(a): + rv.append(a) + else: + nonrv.append(a) + if len(rv) == 0: + return ZeroMatrix(*self.shape) + # Avoid possible infinite loops with MatMul: + if len(nonrv) == 0: + return self + # Variance of many multiple matrix products is not implemented: + if len(rv) > 1: + return self + return Mul.fromiter(nonrv)*Variance(Mul.fromiter(rv), + condition)*(Mul.fromiter(nonrv)).transpose() + + # this expression contains a RandomSymbol somehow: + return self + +class CrossCovarianceMatrix(Covariance, MatrixExpr): + """ + Covariance of a random matrix probability expression. + + Examples + ======== + + >>> from sympy.stats import CrossCovarianceMatrix + >>> from sympy.stats.rv import RandomMatrixSymbol + >>> from sympy import symbols, MatrixSymbol + >>> k = symbols("k") + >>> A, B = MatrixSymbol("A", k, k), MatrixSymbol("B", k, k) + >>> C, D = MatrixSymbol("C", k, k), MatrixSymbol("D", k, k) + >>> X, Y = RandomMatrixSymbol("X", k, 1), RandomMatrixSymbol("Y", k, 1) + >>> Z, W = RandomMatrixSymbol("Z", k, 1), RandomMatrixSymbol("W", k, 1) + >>> CrossCovarianceMatrix(X, Y) + CrossCovarianceMatrix(X, Y) + >>> CrossCovarianceMatrix(X, Y).shape + (k, k) + + To expand the covariance in its expression, use ``expand()``: + + >>> CrossCovarianceMatrix(X + Y, Z).expand() + CrossCovarianceMatrix(X, Z) + CrossCovarianceMatrix(Y, Z) + >>> CrossCovarianceMatrix(A*X, Y).expand() + A*CrossCovarianceMatrix(X, Y) + >>> CrossCovarianceMatrix(A*X, B.T*Y).expand() + A*CrossCovarianceMatrix(X, Y)*B + >>> CrossCovarianceMatrix(A*X + B*Y, C.T*Z + D.T*W).expand() + A*CrossCovarianceMatrix(X, W)*D + A*CrossCovarianceMatrix(X, Z)*C + B*CrossCovarianceMatrix(Y, W)*D + B*CrossCovarianceMatrix(Y, Z)*C + + """ + def __new__(cls, arg1, arg2, condition=None): + arg1 = _sympify(arg1) + arg2 = _sympify(arg2) + + if (1 not in arg1.shape) or (1 not in arg2.shape) or (arg1.shape[1] != arg2.shape[1]): + raise ShapeError("Expression is not a vector") + + shape = (arg1.shape[0], arg2.shape[0]) if arg1.shape[1] == 1 and arg2.shape[1] == 1 \ + else (1, 1) + + if condition: + obj = Expr.__new__(cls, arg1, arg2, condition) + else: + obj = Expr.__new__(cls, arg1, arg2) + + obj._shape = shape + obj._condition = condition + return obj + + @property + def shape(self): + return self._shape + + def expand(self, **hints): + arg1 = self.args[0] + arg2 = self.args[1] + condition = self._condition + + if arg1 == arg2: + return VarianceMatrix(arg1, condition).expand() + + if not is_random(arg1) or not is_random(arg2): + return ZeroMatrix(*self.shape) + + if isinstance(arg1, RandomSymbol) and isinstance(arg2, RandomSymbol): + return CrossCovarianceMatrix(arg1, arg2, condition) + + coeff_rv_list1 = self._expand_single_argument(arg1.expand()) + coeff_rv_list2 = self._expand_single_argument(arg2.expand()) + + addends = [a*CrossCovarianceMatrix(r1, r2, condition=condition)*b.transpose() + for (a, r1) in coeff_rv_list1 for (b, r2) in coeff_rv_list2] + return Add.fromiter(addends) + + @classmethod + def _expand_single_argument(cls, expr): + # return (coefficient, random_symbol) pairs: + if isinstance(expr, RandomSymbol): + return [(S.One, expr)] + elif isinstance(expr, Add): + outval = [] + for a in expr.args: + if isinstance(a, (Mul, MatMul)): + outval.append(cls._get_mul_nonrv_rv_tuple(a)) + elif is_random(a): + outval.append((S.One, a)) + + return outval + elif isinstance(expr, (Mul, MatMul)): + return [cls._get_mul_nonrv_rv_tuple(expr)] + elif is_random(expr): + return [(S.One, expr)] + + @classmethod + def _get_mul_nonrv_rv_tuple(cls, m): + rv = [] + nonrv = [] + for a in m.args: + if is_random(a): + rv.append(a) + else: + nonrv.append(a) + return (Mul.fromiter(nonrv), Mul.fromiter(rv)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/symbolic_probability.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/symbolic_probability.py new file mode 100644 index 0000000000000000000000000000000000000000..5d0b971a8691f82de15258d4c460129059eaf436 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/symbolic_probability.py @@ -0,0 +1,698 @@ +import itertools +from sympy.concrete.summations import Sum +from sympy.core.add import Add +from sympy.core.expr import Expr +from sympy.core.function import expand as _expand +from sympy.core.mul import Mul +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.integrals.integrals import Integral +from sympy.logic.boolalg import Not +from sympy.core.parameters import global_parameters +from sympy.core.sorting import default_sort_key +from sympy.core.sympify import _sympify +from sympy.core.relational import Relational +from sympy.logic.boolalg import Boolean +from sympy.stats import variance, covariance +from sympy.stats.rv import (RandomSymbol, pspace, dependent, + given, sampling_E, RandomIndexedSymbol, is_random, + PSpace, sampling_P, random_symbols) + +__all__ = ['Probability', 'Expectation', 'Variance', 'Covariance'] + + +@is_random.register(Expr) +def _(x): + atoms = x.free_symbols + if len(atoms) == 1 and next(iter(atoms)) == x: + return False + return any(is_random(i) for i in atoms) + +@is_random.register(RandomSymbol) # type: ignore +def _(x): + return True + + +class Probability(Expr): + """ + Symbolic expression for the probability. + + Examples + ======== + + >>> from sympy.stats import Probability, Normal + >>> from sympy import Integral + >>> X = Normal("X", 0, 1) + >>> prob = Probability(X > 1) + >>> prob + Probability(X > 1) + + Integral representation: + + >>> prob.rewrite(Integral) + Integral(sqrt(2)*exp(-_z**2/2)/(2*sqrt(pi)), (_z, 1, oo)) + + Evaluation of the integral: + + >>> prob.evaluate_integral() + sqrt(2)*(-sqrt(2)*sqrt(pi)*erf(sqrt(2)/2) + sqrt(2)*sqrt(pi))/(4*sqrt(pi)) + """ + + is_commutative = True + + def __new__(cls, prob, condition=None, **kwargs): + prob = _sympify(prob) + if condition is None: + obj = Expr.__new__(cls, prob) + else: + condition = _sympify(condition) + obj = Expr.__new__(cls, prob, condition) + obj._condition = condition + return obj + + def doit(self, **hints): + condition = self.args[0] + given_condition = self._condition + numsamples = hints.get('numsamples', False) + evaluate = hints.get('evaluate', True) + + if isinstance(condition, Not): + return S.One - self.func(condition.args[0], given_condition, + evaluate=evaluate).doit(**hints) + + if condition.has(RandomIndexedSymbol): + return pspace(condition).probability(condition, given_condition, + evaluate=evaluate) + + if isinstance(given_condition, RandomSymbol): + condrv = random_symbols(condition) + if len(condrv) == 1 and condrv[0] == given_condition: + from sympy.stats.frv_types import BernoulliDistribution + return BernoulliDistribution(self.func(condition).doit(**hints), 0, 1) + if any(dependent(rv, given_condition) for rv in condrv): + return Probability(condition, given_condition) + else: + return Probability(condition).doit() + + if given_condition is not None and \ + not isinstance(given_condition, (Relational, Boolean)): + raise ValueError("%s is not a relational or combination of relationals" + % (given_condition)) + + if given_condition == False or condition is S.false: + return S.Zero + if not isinstance(condition, (Relational, Boolean)): + raise ValueError("%s is not a relational or combination of relationals" + % (condition)) + if condition is S.true: + return S.One + + if numsamples: + return sampling_P(condition, given_condition, numsamples=numsamples) + if given_condition is not None: # If there is a condition + # Recompute on new conditional expr + return Probability(given(condition, given_condition)).doit() + + # Otherwise pass work off to the ProbabilitySpace + if pspace(condition) == PSpace(): + return Probability(condition, given_condition) + + result = pspace(condition).probability(condition) + if hasattr(result, 'doit') and evaluate: + return result.doit() + else: + return result + + def _eval_rewrite_as_Integral(self, arg, condition=None, **kwargs): + return self.func(arg, condition=condition).doit(evaluate=False) + + _eval_rewrite_as_Sum = _eval_rewrite_as_Integral + + def evaluate_integral(self): + return self.rewrite(Integral).doit() + + +class Expectation(Expr): + """ + Symbolic expression for the expectation. + + Examples + ======== + + >>> from sympy.stats import Expectation, Normal, Probability, Poisson + >>> from sympy import symbols, Integral, Sum + >>> mu = symbols("mu") + >>> sigma = symbols("sigma", positive=True) + >>> X = Normal("X", mu, sigma) + >>> Expectation(X) + Expectation(X) + >>> Expectation(X).evaluate_integral().simplify() + mu + + To get the integral expression of the expectation: + + >>> Expectation(X).rewrite(Integral) + Integral(sqrt(2)*X*exp(-(X - mu)**2/(2*sigma**2))/(2*sqrt(pi)*sigma), (X, -oo, oo)) + + The same integral expression, in more abstract terms: + + >>> Expectation(X).rewrite(Probability) + Integral(x*Probability(Eq(X, x)), (x, -oo, oo)) + + To get the Summation expression of the expectation for discrete random variables: + + >>> lamda = symbols('lamda', positive=True) + >>> Z = Poisson('Z', lamda) + >>> Expectation(Z).rewrite(Sum) + Sum(Z*lamda**Z*exp(-lamda)/factorial(Z), (Z, 0, oo)) + + This class is aware of some properties of the expectation: + + >>> from sympy.abc import a + >>> Expectation(a*X) + Expectation(a*X) + >>> Y = Normal("Y", 1, 2) + >>> Expectation(X + Y) + Expectation(X + Y) + + To expand the ``Expectation`` into its expression, use ``expand()``: + + >>> Expectation(X + Y).expand() + Expectation(X) + Expectation(Y) + >>> Expectation(a*X + Y).expand() + a*Expectation(X) + Expectation(Y) + >>> Expectation(a*X + Y) + Expectation(a*X + Y) + >>> Expectation((X + Y)*(X - Y)).expand() + Expectation(X**2) - Expectation(Y**2) + + To evaluate the ``Expectation``, use ``doit()``: + + >>> Expectation(X + Y).doit() + mu + 1 + >>> Expectation(X + Expectation(Y + Expectation(2*X))).doit() + 3*mu + 1 + + To prevent evaluating nested ``Expectation``, use ``doit(deep=False)`` + + >>> Expectation(X + Expectation(Y)).doit(deep=False) + mu + Expectation(Expectation(Y)) + >>> Expectation(X + Expectation(Y + Expectation(2*X))).doit(deep=False) + mu + Expectation(Expectation(Expectation(2*X) + Y)) + + """ + + def __new__(cls, expr, condition=None, **kwargs): + expr = _sympify(expr) + if expr.is_Matrix: + from sympy.stats.symbolic_multivariate_probability import ExpectationMatrix + return ExpectationMatrix(expr, condition) + if condition is None: + if not is_random(expr): + return expr + obj = Expr.__new__(cls, expr) + else: + condition = _sympify(condition) + obj = Expr.__new__(cls, expr, condition) + obj._condition = condition + return obj + + def _eval_is_commutative(self): + return(self.args[0].is_commutative) + + def expand(self, **hints): + expr = self.args[0] + condition = self._condition + + if not is_random(expr): + return expr + + if isinstance(expr, Add): + return Add.fromiter(Expectation(a, condition=condition).expand() + for a in expr.args) + + expand_expr = _expand(expr) + if isinstance(expand_expr, Add): + return Add.fromiter(Expectation(a, condition=condition).expand() + for a in expand_expr.args) + + elif isinstance(expr, Mul): + rv = [] + nonrv = [] + for a in expr.args: + if is_random(a): + rv.append(a) + else: + nonrv.append(a) + return Mul.fromiter(nonrv)*Expectation(Mul.fromiter(rv), condition=condition) + + return self + + def doit(self, **hints): + deep = hints.get('deep', True) + condition = self._condition + expr = self.args[0] + numsamples = hints.get('numsamples', False) + evaluate = hints.get('evaluate', True) + + if deep: + expr = expr.doit(**hints) + + if not is_random(expr) or isinstance(expr, Expectation): # expr isn't random? + return expr + if numsamples: # Computing by monte carlo sampling? + evalf = hints.get('evalf', True) + return sampling_E(expr, condition, numsamples=numsamples, evalf=evalf) + + if expr.has(RandomIndexedSymbol): + return pspace(expr).compute_expectation(expr, condition) + + # Create new expr and recompute E + if condition is not None: # If there is a condition + return self.func(given(expr, condition)).doit(**hints) + + # A few known statements for efficiency + + if expr.is_Add: # We know that E is Linear + return Add(*[self.func(arg, condition).doit(**hints) + if not isinstance(arg, Expectation) else self.func(arg, condition) + for arg in expr.args]) + if expr.is_Mul: + if expr.atoms(Expectation): + return expr + + if pspace(expr) == PSpace(): + return self.func(expr) + # Otherwise case is simple, pass work off to the ProbabilitySpace + result = pspace(expr).compute_expectation(expr, evaluate=evaluate) + if hasattr(result, 'doit') and evaluate: + return result.doit(**hints) + else: + return result + + + def _eval_rewrite_as_Probability(self, arg, condition=None, **kwargs): + rvs = arg.atoms(RandomSymbol) + if len(rvs) > 1: + raise NotImplementedError() + if len(rvs) == 0: + return arg + + rv = rvs.pop() + if rv.pspace is None: + raise ValueError("Probability space not known") + + symbol = rv.symbol + if symbol.name[0].isupper(): + symbol = Symbol(symbol.name.lower()) + else : + symbol = Symbol(symbol.name + "_1") + + if rv.pspace.is_Continuous: + return Integral(arg.replace(rv, symbol)*Probability(Eq(rv, symbol), condition), (symbol, rv.pspace.domain.set.inf, rv.pspace.domain.set.sup)) + else: + if rv.pspace.is_Finite: + raise NotImplementedError + else: + return Sum(arg.replace(rv, symbol)*Probability(Eq(rv, symbol), condition), (symbol, rv.pspace.domain.set.inf, rv.pspace.set.sup)) + + def _eval_rewrite_as_Integral(self, arg, condition=None, evaluate=False, **kwargs): + return self.func(arg, condition=condition).doit(deep=False, evaluate=evaluate) + + _eval_rewrite_as_Sum = _eval_rewrite_as_Integral # For discrete this will be Sum + + def evaluate_integral(self): + return self.rewrite(Integral).doit() + + evaluate_sum = evaluate_integral + +class Variance(Expr): + """ + Symbolic expression for the variance. + + Examples + ======== + + >>> from sympy import symbols, Integral + >>> from sympy.stats import Normal, Expectation, Variance, Probability + >>> mu = symbols("mu", positive=True) + >>> sigma = symbols("sigma", positive=True) + >>> X = Normal("X", mu, sigma) + >>> Variance(X) + Variance(X) + >>> Variance(X).evaluate_integral() + sigma**2 + + Integral representation of the underlying calculations: + + >>> Variance(X).rewrite(Integral) + Integral(sqrt(2)*(X - Integral(sqrt(2)*X*exp(-(X - mu)**2/(2*sigma**2))/(2*sqrt(pi)*sigma), (X, -oo, oo)))**2*exp(-(X - mu)**2/(2*sigma**2))/(2*sqrt(pi)*sigma), (X, -oo, oo)) + + Integral representation, without expanding the PDF: + + >>> Variance(X).rewrite(Probability) + -Integral(x*Probability(Eq(X, x)), (x, -oo, oo))**2 + Integral(x**2*Probability(Eq(X, x)), (x, -oo, oo)) + + Rewrite the variance in terms of the expectation + + >>> Variance(X).rewrite(Expectation) + -Expectation(X)**2 + Expectation(X**2) + + Some transformations based on the properties of the variance may happen: + + >>> from sympy.abc import a + >>> Y = Normal("Y", 0, 1) + >>> Variance(a*X) + Variance(a*X) + + To expand the variance in its expression, use ``expand()``: + + >>> Variance(a*X).expand() + a**2*Variance(X) + >>> Variance(X + Y) + Variance(X + Y) + >>> Variance(X + Y).expand() + 2*Covariance(X, Y) + Variance(X) + Variance(Y) + + """ + def __new__(cls, arg, condition=None, **kwargs): + arg = _sympify(arg) + + if arg.is_Matrix: + from sympy.stats.symbolic_multivariate_probability import VarianceMatrix + return VarianceMatrix(arg, condition) + if condition is None: + obj = Expr.__new__(cls, arg) + else: + condition = _sympify(condition) + obj = Expr.__new__(cls, arg, condition) + obj._condition = condition + return obj + + def _eval_is_commutative(self): + return self.args[0].is_commutative + + def expand(self, **hints): + arg = self.args[0] + condition = self._condition + + if not is_random(arg): + return S.Zero + + if isinstance(arg, RandomSymbol): + return self + elif isinstance(arg, Add): + rv = [] + for a in arg.args: + if is_random(a): + rv.append(a) + variances = Add(*(Variance(xv, condition).expand() for xv in rv)) + map_to_covar = lambda x: 2*Covariance(*x, condition=condition).expand() + covariances = Add(*map(map_to_covar, itertools.combinations(rv, 2))) + return variances + covariances + elif isinstance(arg, Mul): + nonrv = [] + rv = [] + for a in arg.args: + if is_random(a): + rv.append(a) + else: + nonrv.append(a**2) + if len(rv) == 0: + return S.Zero + return Mul.fromiter(nonrv)*Variance(Mul.fromiter(rv), condition) + + # this expression contains a RandomSymbol somehow: + return self + + def _eval_rewrite_as_Expectation(self, arg, condition=None, **kwargs): + e1 = Expectation(arg**2, condition) + e2 = Expectation(arg, condition)**2 + return e1 - e2 + + def _eval_rewrite_as_Probability(self, arg, condition=None, **kwargs): + return self.rewrite(Expectation).rewrite(Probability) + + def _eval_rewrite_as_Integral(self, arg, condition=None, **kwargs): + return variance(self.args[0], self._condition, evaluate=False) + + _eval_rewrite_as_Sum = _eval_rewrite_as_Integral + + def evaluate_integral(self): + return self.rewrite(Integral).doit() + + +class Covariance(Expr): + """ + Symbolic expression for the covariance. + + Examples + ======== + + >>> from sympy.stats import Covariance + >>> from sympy.stats import Normal + >>> X = Normal("X", 3, 2) + >>> Y = Normal("Y", 0, 1) + >>> Z = Normal("Z", 0, 1) + >>> W = Normal("W", 0, 1) + >>> cexpr = Covariance(X, Y) + >>> cexpr + Covariance(X, Y) + + Evaluate the covariance, `X` and `Y` are independent, + therefore zero is the result: + + >>> cexpr.evaluate_integral() + 0 + + Rewrite the covariance expression in terms of expectations: + + >>> from sympy.stats import Expectation + >>> cexpr.rewrite(Expectation) + Expectation(X*Y) - Expectation(X)*Expectation(Y) + + In order to expand the argument, use ``expand()``: + + >>> from sympy.abc import a, b, c, d + >>> Covariance(a*X + b*Y, c*Z + d*W) + Covariance(a*X + b*Y, c*Z + d*W) + >>> Covariance(a*X + b*Y, c*Z + d*W).expand() + a*c*Covariance(X, Z) + a*d*Covariance(W, X) + b*c*Covariance(Y, Z) + b*d*Covariance(W, Y) + + This class is aware of some properties of the covariance: + + >>> Covariance(X, X).expand() + Variance(X) + >>> Covariance(a*X, b*Y).expand() + a*b*Covariance(X, Y) + """ + + def __new__(cls, arg1, arg2, condition=None, **kwargs): + arg1 = _sympify(arg1) + arg2 = _sympify(arg2) + + if arg1.is_Matrix or arg2.is_Matrix: + from sympy.stats.symbolic_multivariate_probability import CrossCovarianceMatrix + return CrossCovarianceMatrix(arg1, arg2, condition) + + if kwargs.pop('evaluate', global_parameters.evaluate): + arg1, arg2 = sorted([arg1, arg2], key=default_sort_key) + + if condition is None: + obj = Expr.__new__(cls, arg1, arg2) + else: + condition = _sympify(condition) + obj = Expr.__new__(cls, arg1, arg2, condition) + obj._condition = condition + return obj + + def _eval_is_commutative(self): + return self.args[0].is_commutative + + def expand(self, **hints): + arg1 = self.args[0] + arg2 = self.args[1] + condition = self._condition + + if arg1 == arg2: + return Variance(arg1, condition).expand() + + if not is_random(arg1): + return S.Zero + if not is_random(arg2): + return S.Zero + + arg1, arg2 = sorted([arg1, arg2], key=default_sort_key) + + if isinstance(arg1, RandomSymbol) and isinstance(arg2, RandomSymbol): + return Covariance(arg1, arg2, condition) + + coeff_rv_list1 = self._expand_single_argument(arg1.expand()) + coeff_rv_list2 = self._expand_single_argument(arg2.expand()) + + addends = [a*b*Covariance(*sorted([r1, r2], key=default_sort_key), condition=condition) + for (a, r1) in coeff_rv_list1 for (b, r2) in coeff_rv_list2] + return Add.fromiter(addends) + + @classmethod + def _expand_single_argument(cls, expr): + # return (coefficient, random_symbol) pairs: + if isinstance(expr, RandomSymbol): + return [(S.One, expr)] + elif isinstance(expr, Add): + outval = [] + for a in expr.args: + if isinstance(a, Mul): + outval.append(cls._get_mul_nonrv_rv_tuple(a)) + elif is_random(a): + outval.append((S.One, a)) + + return outval + elif isinstance(expr, Mul): + return [cls._get_mul_nonrv_rv_tuple(expr)] + elif is_random(expr): + return [(S.One, expr)] + + @classmethod + def _get_mul_nonrv_rv_tuple(cls, m): + rv = [] + nonrv = [] + for a in m.args: + if is_random(a): + rv.append(a) + else: + nonrv.append(a) + return (Mul.fromiter(nonrv), Mul.fromiter(rv)) + + def _eval_rewrite_as_Expectation(self, arg1, arg2, condition=None, **kwargs): + e1 = Expectation(arg1*arg2, condition) + e2 = Expectation(arg1, condition)*Expectation(arg2, condition) + return e1 - e2 + + def _eval_rewrite_as_Probability(self, arg1, arg2, condition=None, **kwargs): + return self.rewrite(Expectation).rewrite(Probability) + + def _eval_rewrite_as_Integral(self, arg1, arg2, condition=None, **kwargs): + return covariance(self.args[0], self.args[1], self._condition, evaluate=False) + + _eval_rewrite_as_Sum = _eval_rewrite_as_Integral + + def evaluate_integral(self): + return self.rewrite(Integral).doit() + + +class Moment(Expr): + """ + Symbolic class for Moment + + Examples + ======== + + >>> from sympy import Symbol, Integral + >>> from sympy.stats import Normal, Expectation, Probability, Moment + >>> mu = Symbol('mu', real=True) + >>> sigma = Symbol('sigma', positive=True) + >>> X = Normal('X', mu, sigma) + >>> M = Moment(X, 3, 1) + + To evaluate the result of Moment use `doit`: + + >>> M.doit() + mu**3 - 3*mu**2 + 3*mu*sigma**2 + 3*mu - 3*sigma**2 - 1 + + Rewrite the Moment expression in terms of Expectation: + + >>> M.rewrite(Expectation) + Expectation((X - 1)**3) + + Rewrite the Moment expression in terms of Probability: + + >>> M.rewrite(Probability) + Integral((x - 1)**3*Probability(Eq(X, x)), (x, -oo, oo)) + + Rewrite the Moment expression in terms of Integral: + + >>> M.rewrite(Integral) + Integral(sqrt(2)*(X - 1)**3*exp(-(X - mu)**2/(2*sigma**2))/(2*sqrt(pi)*sigma), (X, -oo, oo)) + + """ + def __new__(cls, X, n, c=0, condition=None, **kwargs): + X = _sympify(X) + n = _sympify(n) + c = _sympify(c) + if condition is not None: + condition = _sympify(condition) + return super().__new__(cls, X, n, c, condition) + else: + return super().__new__(cls, X, n, c) + + def doit(self, **hints): + return self.rewrite(Expectation).doit(**hints) + + def _eval_rewrite_as_Expectation(self, X, n, c=0, condition=None, **kwargs): + return Expectation((X - c)**n, condition) + + def _eval_rewrite_as_Probability(self, X, n, c=0, condition=None, **kwargs): + return self.rewrite(Expectation).rewrite(Probability) + + def _eval_rewrite_as_Integral(self, X, n, c=0, condition=None, **kwargs): + return self.rewrite(Expectation).rewrite(Integral) + + +class CentralMoment(Expr): + """ + Symbolic class Central Moment + + Examples + ======== + + >>> from sympy import Symbol, Integral + >>> from sympy.stats import Normal, Expectation, Probability, CentralMoment + >>> mu = Symbol('mu', real=True) + >>> sigma = Symbol('sigma', positive=True) + >>> X = Normal('X', mu, sigma) + >>> CM = CentralMoment(X, 4) + + To evaluate the result of CentralMoment use `doit`: + + >>> CM.doit().simplify() + 3*sigma**4 + + Rewrite the CentralMoment expression in terms of Expectation: + + >>> CM.rewrite(Expectation) + Expectation((-Expectation(X) + X)**4) + + Rewrite the CentralMoment expression in terms of Probability: + + >>> CM.rewrite(Probability) + Integral((x - Integral(x*Probability(True), (x, -oo, oo)))**4*Probability(Eq(X, x)), (x, -oo, oo)) + + Rewrite the CentralMoment expression in terms of Integral: + + >>> CM.rewrite(Integral) + Integral(sqrt(2)*(X - Integral(sqrt(2)*X*exp(-(X - mu)**2/(2*sigma**2))/(2*sqrt(pi)*sigma), (X, -oo, oo)))**4*exp(-(X - mu)**2/(2*sigma**2))/(2*sqrt(pi)*sigma), (X, -oo, oo)) + + """ + def __new__(cls, X, n, condition=None, **kwargs): + X = _sympify(X) + n = _sympify(n) + if condition is not None: + condition = _sympify(condition) + return super().__new__(cls, X, n, condition) + else: + return super().__new__(cls, X, n) + + def doit(self, **hints): + return self.rewrite(Expectation).doit(**hints) + + def _eval_rewrite_as_Expectation(self, X, n, condition=None, **kwargs): + mu = Expectation(X, condition, **kwargs) + return Moment(X, n, mu, condition, **kwargs).rewrite(Expectation) + + def _eval_rewrite_as_Probability(self, X, n, condition=None, **kwargs): + return self.rewrite(Expectation).rewrite(Probability) + + def _eval_rewrite_as_Integral(self, X, n, condition=None, **kwargs): + return self.rewrite(Expectation).rewrite(Integral) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_compound_rv.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_compound_rv.py new file mode 100644 index 0000000000000000000000000000000000000000..573ba364b686738e56bb1c4615acd2a9bc8bf3ae --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_compound_rv.py @@ -0,0 +1,159 @@ +from sympy.concrete.summations import Sum +from sympy.core.numbers import (oo, pi) +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.special.beta_functions import beta +from sympy.functions.special.error_functions import erf +from sympy.functions.special.gamma_functions import gamma +from sympy.integrals.integrals import Integral +from sympy.sets.sets import Interval +from sympy.stats import (Normal, P, E, density, Gamma, Poisson, Rayleigh, + variance, Bernoulli, Beta, Uniform, cdf) +from sympy.stats.compound_rv import CompoundDistribution, CompoundPSpace +from sympy.stats.crv_types import NormalDistribution +from sympy.stats.drv_types import PoissonDistribution +from sympy.stats.frv_types import BernoulliDistribution +from sympy.testing.pytest import raises, ignore_warnings +from sympy.stats.joint_rv_types import MultivariateNormalDistribution + +from sympy.abc import x + + +# helpers for testing troublesome unevaluated expressions +flat = lambda s: ''.join(str(s).split()) +streq = lambda *a: len(set(map(flat, a))) == 1 +assert streq(x, x) +assert streq(x, 'x') +assert not streq(x, x + 1) + + +def test_normal_CompoundDist(): + X = Normal('X', 1, 2) + Y = Normal('X', X, 4) + assert density(Y)(x).simplify() == sqrt(10)*exp(-x**2/40 + x/20 - S(1)/40)/(20*sqrt(pi)) + assert E(Y) == 1 # it is always equal to mean of X + assert P(Y > 1) == S(1)/2 # as 1 is the mean + assert P(Y > 5).simplify() == S(1)/2 - erf(sqrt(10)/5)/2 + assert variance(Y) == variance(X) + 4**2 # 2**2 + 4**2 + # https://math.stackexchange.com/questions/1484451/ + # (Contains proof of E and variance computation) + + +def test_poisson_CompoundDist(): + k, t, y = symbols('k t y', positive=True, real=True) + G = Gamma('G', k, t) + D = Poisson('P', G) + assert density(D)(y).simplify() == t**y*(t + 1)**(-k - y)*gamma(k + y)/(gamma(k)*gamma(y + 1)) + # https://en.wikipedia.org/wiki/Negative_binomial_distribution#Gamma%E2%80%93Poisson_mixture + assert E(D).simplify() == k*t # mean of NegativeBinomialDistribution + + +def test_bernoulli_CompoundDist(): + X = Beta('X', 1, 2) + Y = Bernoulli('Y', X) + assert density(Y).dict == {0: S(2)/3, 1: S(1)/3} + assert E(Y) == P(Eq(Y, 1)) == S(1)/3 + assert variance(Y) == S(2)/9 + assert cdf(Y) == {0: S(2)/3, 1: 1} + + # test issue 8128 + a = Bernoulli('a', S(1)/2) + b = Bernoulli('b', a) + assert density(b).dict == {0: S(1)/2, 1: S(1)/2} + assert P(b > 0.5) == S(1)/2 + + X = Uniform('X', 0, 1) + Y = Bernoulli('Y', X) + assert E(Y) == S(1)/2 + assert P(Eq(Y, 1)) == E(Y) + + +def test_unevaluated_CompoundDist(): + # these tests need to be removed once they work with evaluation as they are currently not + # evaluated completely in sympy. + R = Rayleigh('R', 4) + X = Normal('X', 3, R) + ans = ''' + Piecewise(((-sqrt(pi)*sinh(x/4 - 3/4) + sqrt(pi)*cosh(x/4 - 3/4))/( + 8*sqrt(pi)), Abs(arg(x - 3)) <= pi/4), (Integral(sqrt(2)*exp(-(x - 3) + **2/(2*R**2))*exp(-R**2/32)/(32*sqrt(pi)), (R, 0, oo)), True))''' + assert streq(density(X)(x), ans) + + expre = ''' + Integral(X*Integral(sqrt(2)*exp(-(X-3)**2/(2*R**2))*exp(-R**2/32)/(32* + sqrt(pi)),(R,0,oo)),(X,-oo,oo))''' + with ignore_warnings(UserWarning): ### TODO: Restore tests once warnings are removed + assert streq(E(X, evaluate=False).rewrite(Integral), expre) + + X = Poisson('X', 1) + Y = Poisson('Y', X) + Z = Poisson('Z', Y) + exprd = Sum(exp(-Y)*Y**x*Sum(exp(-1)*exp(-X)*X**Y/(factorial(X)*factorial(Y) + ), (X, 0, oo))/factorial(x), (Y, 0, oo)) + assert density(Z)(x) == exprd + + N = Normal('N', 1, 2) + M = Normal('M', 3, 4) + D = Normal('D', M, N) + exprd = ''' + Integral(sqrt(2)*exp(-(N-1)**2/8)*Integral(exp(-(x-M)**2/(2*N**2))*exp + (-(M-3)**2/32)/(8*pi*N),(M,-oo,oo))/(4*sqrt(pi)),(N,-oo,oo))''' + assert streq(density(D, evaluate=False)(x), exprd) + + +def test_Compound_Distribution(): + X = Normal('X', 2, 4) + N = NormalDistribution(X, 4) + C = CompoundDistribution(N) + assert C.is_Continuous + assert C.set == Interval(-oo, oo) + assert C.pdf(x, evaluate=True).simplify() == exp(-x**2/64 + x/16 - S(1)/16)/(8*sqrt(pi)) + + assert not isinstance(CompoundDistribution(NormalDistribution(2, 3)), + CompoundDistribution) + M = MultivariateNormalDistribution([1, 2], [[2, 1], [1, 2]]) + raises(NotImplementedError, lambda: CompoundDistribution(M)) + + X = Beta('X', 2, 4) + B = BernoulliDistribution(X, 1, 0) + C = CompoundDistribution(B) + assert C.is_Finite + assert C.set == {0, 1} + y = symbols('y', negative=False, integer=True) + assert C.pdf(y, evaluate=True) == Piecewise((S(1)/(30*beta(2, 4)), Eq(y, 0)), + (S(1)/(60*beta(2, 4)), Eq(y, 1)), (0, True)) + + k, t, z = symbols('k t z', positive=True, real=True) + G = Gamma('G', k, t) + X = PoissonDistribution(G) + C = CompoundDistribution(X) + assert C.is_Discrete + assert C.set == S.Naturals0 + assert C.pdf(z, evaluate=True).simplify() == t**z*(t + 1)**(-k - z)*gamma(k \ + + z)/(gamma(k)*gamma(z + 1)) + + +def test_compound_pspace(): + X = Normal('X', 2, 4) + Y = Normal('Y', 3, 6) + assert not isinstance(Y.pspace, CompoundPSpace) + N = NormalDistribution(1, 2) + D = PoissonDistribution(3) + B = BernoulliDistribution(0.2, 1, 0) + pspace1 = CompoundPSpace('N', N) + pspace2 = CompoundPSpace('D', D) + pspace3 = CompoundPSpace('B', B) + assert not isinstance(pspace1, CompoundPSpace) + assert not isinstance(pspace2, CompoundPSpace) + assert not isinstance(pspace3, CompoundPSpace) + M = MultivariateNormalDistribution([1, 2], [[2, 1], [1, 2]]) + raises(ValueError, lambda: CompoundPSpace('M', M)) + Y = Normal('Y', X, 6) + assert isinstance(Y.pspace, CompoundPSpace) + assert Y.pspace.distribution == CompoundDistribution(NormalDistribution(X, 6)) + assert Y.pspace.domain.set == Interval(-oo, oo) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_continuous_rv.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_continuous_rv.py new file mode 100644 index 0000000000000000000000000000000000000000..b2c4206b5c29ffd3194d1ae05e57c51c9c1b6d78 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_continuous_rv.py @@ -0,0 +1,1583 @@ +from sympy.concrete.summations import Sum +from sympy.core.function import (Lambda, diff, expand_func) +from sympy.core.mul import Mul +from sympy.core import EulerGamma +from sympy.core.numbers import (E as e, I, Rational, pi) +from sympy.core.relational import (Eq, Ne) +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol, symbols) +from sympy.functions.combinatorial.factorials import (binomial, factorial) +from sympy.functions.elementary.complexes import (Abs, im, re, sign) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.hyperbolic import (cosh, sinh) +from sympy.functions.elementary.integers import floor +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (asin, atan, cos, sin, tan) +from sympy.functions.special.bessel import (besseli, besselj, besselk) +from sympy.functions.special.beta_functions import beta +from sympy.functions.special.error_functions import (erf, erfc, erfi, expint) +from sympy.functions.special.gamma_functions import (gamma, lowergamma, uppergamma) +from sympy.functions.special.zeta_functions import zeta +from sympy.functions.special.hyper import hyper +from sympy.integrals.integrals import Integral +from sympy.logic.boolalg import (And, Or) +from sympy.sets.sets import Interval +from sympy.simplify.simplify import simplify +from sympy.utilities.lambdify import lambdify +from sympy.functions.special.error_functions import erfinv +from sympy.functions.special.hyper import meijerg +from sympy.sets.sets import FiniteSet, Complement, Intersection +from sympy.stats import (P, E, where, density, variance, covariance, skewness, kurtosis, median, + given, pspace, cdf, characteristic_function, moment_generating_function, + ContinuousRV, Arcsin, Benini, Beta, BetaNoncentral, BetaPrime, + Cauchy, Chi, ChiSquared, ChiNoncentral, Dagum, Davis, Erlang, ExGaussian, + Exponential, ExponentialPower, FDistribution, FisherZ, Frechet, Gamma, + GammaInverse, Gompertz, Gumbel, Kumaraswamy, Laplace, Levy, Logistic, LogCauchy, + LogLogistic, LogitNormal, LogNormal, Maxwell, Moyal, Nakagami, Normal, GaussianInverse, + Pareto, PowerFunction, QuadraticU, RaisedCosine, Rayleigh, Reciprocal, ShiftedGompertz, StudentT, + Trapezoidal, Triangular, Uniform, UniformSum, VonMises, Weibull, coskewness, + WignerSemicircle, Wald, correlation, moment, cmoment, smoment, quantile, + Lomax, BoundedPareto) + +from sympy.stats.crv_types import NormalDistribution, ExponentialDistribution, ContinuousDistributionHandmade +from sympy.stats.joint_rv_types import MultivariateLaplaceDistribution, MultivariateNormalDistribution +from sympy.stats.crv import SingleContinuousPSpace, SingleContinuousDomain +from sympy.stats.compound_rv import CompoundPSpace +from sympy.stats.symbolic_probability import Probability +from sympy.testing.pytest import raises, XFAIL, slow, ignore_warnings +from sympy.core.random import verify_numerically as tn + +oo = S.Infinity + +x, y, z = map(Symbol, 'xyz') + +def test_single_normal(): + mu = Symbol('mu', real=True) + sigma = Symbol('sigma', positive=True) + X = Normal('x', 0, 1) + Y = X*sigma + mu + + assert E(Y) == mu + assert variance(Y) == sigma**2 + pdf = density(Y) + x = Symbol('x', real=True) + assert (pdf(x) == + 2**S.Half*exp(-(x - mu)**2/(2*sigma**2))/(2*pi**S.Half*sigma)) + + assert P(X**2 < 1) == erf(2**S.Half/2) + ans = quantile(Y)(x) + assert ans == Complement(Intersection(FiniteSet( + sqrt(2)*sigma*(sqrt(2)*mu/(2*sigma)+ erfinv(2*x - 1))), + Interval(-oo, oo)), FiniteSet(mu)) + assert E(X, Eq(X, mu)) == mu + + assert median(X) == FiniteSet(0) + # issue 8248 + assert X.pspace.compute_expectation(1).doit() == 1 + + +def test_conditional_1d(): + X = Normal('x', 0, 1) + Y = given(X, X >= 0) + z = Symbol('z') + + assert density(Y)(z) == 2 * density(X)(z) + + assert Y.pspace.domain.set == Interval(0, oo) + assert E(Y) == sqrt(2) / sqrt(pi) + + assert E(X**2) == E(Y**2) + + +def test_ContinuousDomain(): + X = Normal('x', 0, 1) + assert where(X**2 <= 1).set == Interval(-1, 1) + assert where(X**2 <= 1).symbol == X.symbol + assert where(And(X**2 <= 1, X >= 0)).set == Interval(0, 1) + raises(ValueError, lambda: where(sin(X) > 1)) + + Y = given(X, X >= 0) + + assert Y.pspace.domain.set == Interval(0, oo) + + +def test_multiple_normal(): + X, Y = Normal('x', 0, 1), Normal('y', 0, 1) + p = Symbol("p", positive=True) + + assert E(X + Y) == 0 + assert variance(X + Y) == 2 + assert variance(X + X) == 4 + assert covariance(X, Y) == 0 + assert covariance(2*X + Y, -X) == -2*variance(X) + assert skewness(X) == 0 + assert skewness(X + Y) == 0 + assert kurtosis(X) == 3 + assert kurtosis(X+Y) == 3 + assert correlation(X, Y) == 0 + assert correlation(X, X + Y) == correlation(X, X - Y) + assert moment(X, 2) == 1 + assert cmoment(X, 3) == 0 + assert moment(X + Y, 4) == 12 + assert cmoment(X, 2) == variance(X) + assert smoment(X*X, 2) == 1 + assert smoment(X + Y, 3) == skewness(X + Y) + assert smoment(X + Y, 4) == kurtosis(X + Y) + assert E(X, Eq(X + Y, 0)) == 0 + assert variance(X, Eq(X + Y, 0)) == S.Half + assert quantile(X)(p) == sqrt(2)*erfinv(2*p - S.One) + + +def test_symbolic(): + mu1, mu2 = symbols('mu1 mu2', real=True) + s1, s2 = symbols('sigma1 sigma2', positive=True) + rate = Symbol('lambda', positive=True) + X = Normal('x', mu1, s1) + Y = Normal('y', mu2, s2) + Z = Exponential('z', rate) + a, b, c = symbols('a b c', real=True) + + assert E(X) == mu1 + assert E(X + Y) == mu1 + mu2 + assert E(a*X + b) == a*E(X) + b + assert variance(X) == s1**2 + assert variance(X + a*Y + b) == variance(X) + a**2*variance(Y) + + assert E(Z) == 1/rate + assert E(a*Z + b) == a*E(Z) + b + assert E(X + a*Z + b) == mu1 + a/rate + b + assert median(X) == FiniteSet(mu1) + + +def test_cdf(): + X = Normal('x', 0, 1) + + d = cdf(X) + assert P(X < 1) == d(1).rewrite(erfc) + assert d(0) == S.Half + + d = cdf(X, X > 0) # given X>0 + assert d(0) == 0 + + Y = Exponential('y', 10) + d = cdf(Y) + assert d(-5) == 0 + assert P(Y > 3) == 1 - d(3) + + raises(ValueError, lambda: cdf(X + Y)) + + Z = Exponential('z', 1) + f = cdf(Z) + assert f(z) == Piecewise((1 - exp(-z), z >= 0), (0, True)) + + +def test_characteristic_function(): + X = Uniform('x', 0, 1) + + cf = characteristic_function(X) + assert cf(1) == -I*(-1 + exp(I)) + + Y = Normal('y', 1, 1) + cf = characteristic_function(Y) + assert cf(0) == 1 + assert cf(1) == exp(I - S.Half) + + Z = Exponential('z', 5) + cf = characteristic_function(Z) + assert cf(0) == 1 + assert cf(1).expand() == Rational(25, 26) + I*5/26 + + X = GaussianInverse('x', 1, 1) + cf = characteristic_function(X) + assert cf(0) == 1 + assert cf(1) == exp(1 - sqrt(1 - 2*I)) + + X = ExGaussian('x', 0, 1, 1) + cf = characteristic_function(X) + assert cf(0) == 1 + assert cf(1) == (1 + I)*exp(Rational(-1, 2))/2 + + L = Levy('x', 0, 1) + cf = characteristic_function(L) + assert cf(0) == 1 + assert cf(1) == exp(-sqrt(2)*sqrt(-I)) + + +def test_moment_generating_function(): + t = symbols('t', positive=True) + + # Symbolic tests + a, b, c = symbols('a b c') + + mgf = moment_generating_function(Beta('x', a, b))(t) + assert mgf == hyper((a,), (a + b,), t) + + mgf = moment_generating_function(Chi('x', a))(t) + assert mgf == sqrt(2)*t*gamma(a/2 + S.Half)*\ + hyper((a/2 + S.Half,), (Rational(3, 2),), t**2/2)/gamma(a/2) +\ + hyper((a/2,), (S.Half,), t**2/2) + + mgf = moment_generating_function(ChiSquared('x', a))(t) + assert mgf == (1 - 2*t)**(-a/2) + + mgf = moment_generating_function(Erlang('x', a, b))(t) + assert mgf == (1 - t/b)**(-a) + + mgf = moment_generating_function(ExGaussian("x", a, b, c))(t) + assert mgf == exp(a*t + b**2*t**2/2)/(1 - t/c) + + mgf = moment_generating_function(Exponential('x', a))(t) + assert mgf == a/(a - t) + + mgf = moment_generating_function(Gamma('x', a, b))(t) + assert mgf == (-b*t + 1)**(-a) + + mgf = moment_generating_function(Gumbel('x', a, b))(t) + assert mgf == exp(b*t)*gamma(-a*t + 1) + + mgf = moment_generating_function(Gompertz('x', a, b))(t) + assert mgf == b*exp(b)*expint(t/a, b) + + mgf = moment_generating_function(Laplace('x', a, b))(t) + assert mgf == exp(a*t)/(-b**2*t**2 + 1) + + mgf = moment_generating_function(Logistic('x', a, b))(t) + assert mgf == exp(a*t)*beta(-b*t + 1, b*t + 1) + + mgf = moment_generating_function(Normal('x', a, b))(t) + assert mgf == exp(a*t + b**2*t**2/2) + + mgf = moment_generating_function(Pareto('x', a, b))(t) + assert mgf == b*(-a*t)**b*uppergamma(-b, -a*t) + + mgf = moment_generating_function(QuadraticU('x', a, b))(t) + assert str(mgf) == ("(3*(t*(-4*b + (a + b)**2) + 4)*exp(b*t) - " + "3*(t*(a**2 + 2*a*(b - 2) + b**2) + 4)*exp(a*t))/(t**2*(a - b)**3)") + + mgf = moment_generating_function(RaisedCosine('x', a, b))(t) + assert mgf == pi**2*exp(a*t)*sinh(b*t)/(b*t*(b**2*t**2 + pi**2)) + + mgf = moment_generating_function(Rayleigh('x', a))(t) + assert mgf == sqrt(2)*sqrt(pi)*a*t*(erf(sqrt(2)*a*t/2) + 1)\ + *exp(a**2*t**2/2)/2 + 1 + + mgf = moment_generating_function(Triangular('x', a, b, c))(t) + assert str(mgf) == ("(-2*(-a + b)*exp(c*t) + 2*(-a + c)*exp(b*t) + " + "2*(b - c)*exp(a*t))/(t**2*(-a + b)*(-a + c)*(b - c))") + + mgf = moment_generating_function(Uniform('x', a, b))(t) + assert mgf == (-exp(a*t) + exp(b*t))/(t*(-a + b)) + + mgf = moment_generating_function(UniformSum('x', a))(t) + assert mgf == ((exp(t) - 1)/t)**a + + mgf = moment_generating_function(WignerSemicircle('x', a))(t) + assert mgf == 2*besseli(1, a*t)/(a*t) + + # Numeric tests + + mgf = moment_generating_function(Beta('x', 1, 1))(t) + assert mgf.diff(t).subs(t, 1) == hyper((2,), (3,), 1)/2 + + mgf = moment_generating_function(Chi('x', 1))(t) + assert mgf.diff(t).subs(t, 1) == sqrt(2)*hyper((1,), (Rational(3, 2),), S.Half + )/sqrt(pi) + hyper((Rational(3, 2),), (Rational(3, 2),), S.Half) + 2*sqrt(2)*hyper((2,), + (Rational(5, 2),), S.Half)/(3*sqrt(pi)) + + mgf = moment_generating_function(ChiSquared('x', 1))(t) + assert mgf.diff(t).subs(t, 1) == I + + mgf = moment_generating_function(Erlang('x', 1, 1))(t) + assert mgf.diff(t).subs(t, 0) == 1 + + mgf = moment_generating_function(ExGaussian("x", 0, 1, 1))(t) + assert mgf.diff(t).subs(t, 2) == -exp(2) + + mgf = moment_generating_function(Exponential('x', 1))(t) + assert mgf.diff(t).subs(t, 0) == 1 + + mgf = moment_generating_function(Gamma('x', 1, 1))(t) + assert mgf.diff(t).subs(t, 0) == 1 + + mgf = moment_generating_function(Gumbel('x', 1, 1))(t) + assert mgf.diff(t).subs(t, 0) == EulerGamma + 1 + + mgf = moment_generating_function(Gompertz('x', 1, 1))(t) + assert mgf.diff(t).subs(t, 1) == -e*meijerg(((), (1, 1)), + ((0, 0, 0), ()), 1) + + mgf = moment_generating_function(Laplace('x', 1, 1))(t) + assert mgf.diff(t).subs(t, 0) == 1 + + mgf = moment_generating_function(Logistic('x', 1, 1))(t) + assert mgf.diff(t).subs(t, 0) == beta(1, 1) + + mgf = moment_generating_function(Normal('x', 0, 1))(t) + assert mgf.diff(t).subs(t, 1) == exp(S.Half) + + mgf = moment_generating_function(Pareto('x', 1, 1))(t) + assert mgf.diff(t).subs(t, 0) == expint(1, 0) + + mgf = moment_generating_function(QuadraticU('x', 1, 2))(t) + assert mgf.diff(t).subs(t, 1) == -12*e - 3*exp(2) + + mgf = moment_generating_function(RaisedCosine('x', 1, 1))(t) + assert mgf.diff(t).subs(t, 1) == -2*e*pi**2*sinh(1)/\ + (1 + pi**2)**2 + e*pi**2*cosh(1)/(1 + pi**2) + + mgf = moment_generating_function(Rayleigh('x', 1))(t) + assert mgf.diff(t).subs(t, 0) == sqrt(2)*sqrt(pi)/2 + + mgf = moment_generating_function(Triangular('x', 1, 3, 2))(t) + assert mgf.diff(t).subs(t, 1) == -e + exp(3) + + mgf = moment_generating_function(Uniform('x', 0, 1))(t) + assert mgf.diff(t).subs(t, 1) == 1 + + mgf = moment_generating_function(UniformSum('x', 1))(t) + assert mgf.diff(t).subs(t, 1) == 1 + + mgf = moment_generating_function(WignerSemicircle('x', 1))(t) + assert mgf.diff(t).subs(t, 1) == -2*besseli(1, 1) + besseli(2, 1) +\ + besseli(0, 1) + + +def test_ContinuousRV(): + pdf = sqrt(2)*exp(-x**2/2)/(2*sqrt(pi)) # Normal distribution + # X and Y should be equivalent + X = ContinuousRV(x, pdf, check=True) + Y = Normal('y', 0, 1) + + assert variance(X) == variance(Y) + assert P(X > 0) == P(Y > 0) + Z = ContinuousRV(z, exp(-z), set=Interval(0, oo)) + assert Z.pspace.domain.set == Interval(0, oo) + assert E(Z) == 1 + assert P(Z > 5) == exp(-5) + raises(ValueError, lambda: ContinuousRV(z, exp(-z), set=Interval(0, 10), check=True)) + + # the correct pdf for Gamma(k, theta) but the integral in `check` + # integrates to something equivalent to 1 and not to 1 exactly + _x, k, theta = symbols("x k theta", positive=True) + pdf = 1/(gamma(k)*theta**k)*_x**(k-1)*exp(-_x/theta) + X = ContinuousRV(_x, pdf, set=Interval(0, oo)) + Y = Gamma('y', k, theta) + assert (E(X) - E(Y)).simplify() == 0 + assert (variance(X) - variance(Y)).simplify() == 0 + + +def test_arcsin(): + + a = Symbol("a", real=True) + b = Symbol("b", real=True) + + X = Arcsin('x', a, b) + assert density(X)(x) == 1/(pi*sqrt((-x + b)*(x - a))) + assert cdf(X)(x) == Piecewise((0, a > x), + (2*asin(sqrt((-a + x)/(-a + b)))/pi, b >= x), + (1, True)) + assert pspace(X).domain.set == Interval(a, b) + +def test_benini(): + alpha = Symbol("alpha", positive=True) + beta = Symbol("beta", positive=True) + sigma = Symbol("sigma", positive=True) + X = Benini('x', alpha, beta, sigma) + + assert density(X)(x) == ((alpha/x + 2*beta*log(x/sigma)/x) + *exp(-alpha*log(x/sigma) - beta*log(x/sigma)**2)) + + assert pspace(X).domain.set == Interval(sigma, oo) + raises(NotImplementedError, lambda: moment_generating_function(X)) + alpha = Symbol("alpha", nonpositive=True) + raises(ValueError, lambda: Benini('x', alpha, beta, sigma)) + + beta = Symbol("beta", nonpositive=True) + raises(ValueError, lambda: Benini('x', alpha, beta, sigma)) + + alpha = Symbol("alpha", positive=True) + raises(ValueError, lambda: Benini('x', alpha, beta, sigma)) + + beta = Symbol("beta", positive=True) + sigma = Symbol("sigma", nonpositive=True) + raises(ValueError, lambda: Benini('x', alpha, beta, sigma)) + +def test_beta(): + a, b = symbols('alpha beta', positive=True) + B = Beta('x', a, b) + + assert pspace(B).domain.set == Interval(0, 1) + assert characteristic_function(B)(x) == hyper((a,), (a + b,), I*x) + assert density(B)(x) == x**(a - 1)*(1 - x)**(b - 1)/beta(a, b) + + assert simplify(E(B)) == a / (a + b) + assert simplify(variance(B)) == a*b / (a**3 + 3*a**2*b + a**2 + 3*a*b**2 + 2*a*b + b**3 + b**2) + + # Full symbolic solution is too much, test with numeric version + a, b = 1, 2 + B = Beta('x', a, b) + assert expand_func(E(B)) == a / S(a + b) + assert expand_func(variance(B)) == (a*b) / S((a + b)**2 * (a + b + 1)) + assert median(B) == FiniteSet(1 - 1/sqrt(2)) + +def test_beta_noncentral(): + a, b = symbols('a b', positive=True) + c = Symbol('c', nonnegative=True) + _k = Dummy('k') + + X = BetaNoncentral('x', a, b, c) + + assert pspace(X).domain.set == Interval(0, 1) + + dens = density(X) + z = Symbol('z') + + res = Sum( z**(_k + a - 1)*(c/2)**_k*(1 - z)**(b - 1)*exp(-c/2)/ + (beta(_k + a, b)*factorial(_k)), (_k, 0, oo)) + assert dens(z).dummy_eq(res) + + # BetaCentral should not raise if the assumptions + # on the symbols can not be determined + a, b, c = symbols('a b c') + assert BetaNoncentral('x', a, b, c) + + a = Symbol('a', positive=False, real=True) + raises(ValueError, lambda: BetaNoncentral('x', a, b, c)) + + a = Symbol('a', positive=True) + b = Symbol('b', positive=False, real=True) + raises(ValueError, lambda: BetaNoncentral('x', a, b, c)) + + a = Symbol('a', positive=True) + b = Symbol('b', positive=True) + c = Symbol('c', nonnegative=False, real=True) + raises(ValueError, lambda: BetaNoncentral('x', a, b, c)) + +def test_betaprime(): + alpha = Symbol("alpha", positive=True) + + betap = Symbol("beta", positive=True) + + X = BetaPrime('x', alpha, betap) + assert density(X)(x) == x**(alpha - 1)*(x + 1)**(-alpha - betap)/beta(alpha, betap) + + alpha = Symbol("alpha", nonpositive=True) + raises(ValueError, lambda: BetaPrime('x', alpha, betap)) + + alpha = Symbol("alpha", positive=True) + betap = Symbol("beta", nonpositive=True) + raises(ValueError, lambda: BetaPrime('x', alpha, betap)) + X = BetaPrime('x', 1, 1) + assert median(X) == FiniteSet(1) + + +def test_BoundedPareto(): + L, H = symbols('L, H', negative=True) + raises(ValueError, lambda: BoundedPareto('X', 1, L, H)) + L, H = symbols('L, H', real=False) + raises(ValueError, lambda: BoundedPareto('X', 1, L, H)) + L, H = symbols('L, H', positive=True) + raises(ValueError, lambda: BoundedPareto('X', -1, L, H)) + + X = BoundedPareto('X', 2, L, H) + assert X.pspace.domain.set == Interval(L, H) + assert density(X)(x) == 2*L**2/(x**3*(1 - L**2/H**2)) + assert cdf(X)(x) == Piecewise((-H**2*L**2/(x**2*(H**2 - L**2)) \ + + H**2/(H**2 - L**2), L <= x), (0, True)) + assert E(X).simplify() == 2*H*L/(H + L) + X = BoundedPareto('X', 1, 2, 4) + assert E(X).simplify() == log(16) + assert median(X) == FiniteSet(Rational(8, 3)) + assert variance(X).simplify() == 8 - 16*log(2)**2 + + +def test_cauchy(): + x0 = Symbol("x0", real=True) + gamma = Symbol("gamma", positive=True) + p = Symbol("p", positive=True) + + X = Cauchy('x', x0, gamma) + # Tests the characteristic function + assert characteristic_function(X)(x) == exp(-gamma*Abs(x) + I*x*x0) + raises(NotImplementedError, lambda: moment_generating_function(X)) + assert density(X)(x) == 1/(pi*gamma*(1 + (x - x0)**2/gamma**2)) + assert diff(cdf(X)(x), x) == density(X)(x) + assert quantile(X)(p) == gamma*tan(pi*(p - S.Half)) + x0 + + x1 = Symbol("x1", real=False) + raises(ValueError, lambda: Cauchy('x', x1, gamma)) + gamma = Symbol("gamma", nonpositive=True) + raises(ValueError, lambda: Cauchy('x', x0, gamma)) + assert median(X) == FiniteSet(x0) + +def test_chi(): + from sympy.core.numbers import I + k = Symbol("k", integer=True) + + X = Chi('x', k) + assert density(X)(x) == 2**(-k/2 + 1)*x**(k - 1)*exp(-x**2/2)/gamma(k/2) + + # Tests the characteristic function + assert characteristic_function(X)(x) == sqrt(2)*I*x*gamma(k/2 + S(1)/2)*hyper((k/2 + S(1)/2,), + (S(3)/2,), -x**2/2)/gamma(k/2) + hyper((k/2,), (S(1)/2,), -x**2/2) + + # Tests the moment generating function + assert moment_generating_function(X)(x) == sqrt(2)*x*gamma(k/2 + S(1)/2)*hyper((k/2 + S(1)/2,), + (S(3)/2,), x**2/2)/gamma(k/2) + hyper((k/2,), (S(1)/2,), x**2/2) + + k = Symbol("k", integer=True, positive=False) + raises(ValueError, lambda: Chi('x', k)) + + k = Symbol("k", integer=False, positive=True) + raises(ValueError, lambda: Chi('x', k)) + +def test_chi_noncentral(): + k = Symbol("k", integer=True) + l = Symbol("l") + + X = ChiNoncentral("x", k, l) + assert density(X)(x) == (x**k*l*(x*l)**(-k/2)* + exp(-x**2/2 - l**2/2)*besseli(k/2 - 1, x*l)) + + k = Symbol("k", integer=True, positive=False) + raises(ValueError, lambda: ChiNoncentral('x', k, l)) + + k = Symbol("k", integer=True, positive=True) + l = Symbol("l", nonpositive=True) + raises(ValueError, lambda: ChiNoncentral('x', k, l)) + + k = Symbol("k", integer=False) + l = Symbol("l", positive=True) + raises(ValueError, lambda: ChiNoncentral('x', k, l)) + + +def test_chi_squared(): + k = Symbol("k", integer=True) + X = ChiSquared('x', k) + + # Tests the characteristic function + assert characteristic_function(X)(x) == ((-2*I*x + 1)**(-k/2)) + + assert density(X)(x) == 2**(-k/2)*x**(k/2 - 1)*exp(-x/2)/gamma(k/2) + assert cdf(X)(x) == Piecewise((lowergamma(k/2, x/2)/gamma(k/2), x >= 0), (0, True)) + assert E(X) == k + assert variance(X) == 2*k + + X = ChiSquared('x', 15) + assert cdf(X)(3) == -14873*sqrt(6)*exp(Rational(-3, 2))/(5005*sqrt(pi)) + erf(sqrt(6)/2) + + k = Symbol("k", integer=True, positive=False) + raises(ValueError, lambda: ChiSquared('x', k)) + + k = Symbol("k", integer=False, positive=True) + raises(ValueError, lambda: ChiSquared('x', k)) + + +def test_dagum(): + p = Symbol("p", positive=True) + b = Symbol("b", positive=True) + a = Symbol("a", positive=True) + + X = Dagum('x', p, a, b) + assert density(X)(x) == a*p*(x/b)**(a*p)*((x/b)**a + 1)**(-p - 1)/x + assert cdf(X)(x) == Piecewise(((1 + (x/b)**(-a))**(-p), x >= 0), + (0, True)) + + p = Symbol("p", nonpositive=True) + raises(ValueError, lambda: Dagum('x', p, a, b)) + + p = Symbol("p", positive=True) + b = Symbol("b", nonpositive=True) + raises(ValueError, lambda: Dagum('x', p, a, b)) + + b = Symbol("b", positive=True) + a = Symbol("a", nonpositive=True) + raises(ValueError, lambda: Dagum('x', p, a, b)) + X = Dagum('x', 1, 1, 1) + assert median(X) == FiniteSet(1) + +def test_davis(): + b = Symbol("b", positive=True) + n = Symbol("n", positive=True) + mu = Symbol("mu", positive=True) + + X = Davis('x', b, n, mu) + dividend = b**n*(x - mu)**(-1-n) + divisor = (exp(b/(x-mu))-1)*(gamma(n)*zeta(n)) + assert density(X)(x) == dividend/divisor + + +def test_erlang(): + k = Symbol("k", integer=True, positive=True) + l = Symbol("l", positive=True) + + X = Erlang("x", k, l) + assert density(X)(x) == x**(k - 1)*l**k*exp(-x*l)/gamma(k) + assert cdf(X)(x) == Piecewise((lowergamma(k, l*x)/gamma(k), x > 0), + (0, True)) + + +def test_exgaussian(): + m, z = symbols("m, z") + s, l = symbols("s, l", positive=True) + X = ExGaussian("x", m, s, l) + + assert density(X)(z) == l*exp(l*(l*s**2 + 2*m - 2*z)/2) *\ + erfc(sqrt(2)*(l*s**2 + m - z)/(2*s))/2 + + # Note: actual_output simplifies to expected_output. + # Ideally cdf(X)(z) would return expected_output + # expected_output = (erf(sqrt(2)*(l*s**2 + m - z)/(2*s)) - 1)*exp(l*(l*s**2 + 2*m - 2*z)/2)/2 - erf(sqrt(2)*(m - z)/(2*s))/2 + S.Half + u = l*(z - m) + v = l*s + GaussianCDF1 = cdf(Normal('x', 0, v))(u) + GaussianCDF2 = cdf(Normal('x', v**2, v))(u) + actual_output = GaussianCDF1 - exp(-u + (v**2/2) + log(GaussianCDF2)) + assert cdf(X)(z) == actual_output + # assert simplify(actual_output) == expected_output + + assert variance(X).expand() == s**2 + l**(-2) + + assert skewness(X).expand() == 2/(l**3*s**2*sqrt(s**2 + l**(-2)) + l * + sqrt(s**2 + l**(-2))) + + +@slow +def test_exponential(): + rate = Symbol('lambda', positive=True) + X = Exponential('x', rate) + p = Symbol("p", positive=True, real=True) + + assert E(X) == 1/rate + assert variance(X) == 1/rate**2 + assert skewness(X) == 2 + assert skewness(X) == smoment(X, 3) + assert kurtosis(X) == 9 + assert kurtosis(X) == smoment(X, 4) + assert smoment(2*X, 4) == smoment(X, 4) + assert moment(X, 3) == 3*2*1/rate**3 + assert P(X > 0) is S.One + assert P(X > 1) == exp(-rate) + assert P(X > 10) == exp(-10*rate) + assert quantile(X)(p) == -log(1-p)/rate + + assert where(X <= 1).set == Interval(0, 1) + Y = Exponential('y', 1) + assert median(Y) == FiniteSet(log(2)) + #Test issue 9970 + z = Dummy('z') + assert P(X > z) == exp(-z*rate) + assert P(X < z) == 0 + #Test issue 10076 (Distribution with interval(0,oo)) + x = Symbol('x') + _z = Dummy('_z') + b = SingleContinuousPSpace(x, ExponentialDistribution(2)) + + with ignore_warnings(UserWarning): ### TODO: Restore tests once warnings are removed + expected1 = Integral(2*exp(-2*_z), (_z, 3, oo)) + assert b.probability(x > 3, evaluate=False).rewrite(Integral).dummy_eq(expected1) + + expected2 = Integral(2*exp(-2*_z), (_z, 0, 4)) + assert b.probability(x < 4, evaluate=False).rewrite(Integral).dummy_eq(expected2) + Y = Exponential('y', 2*rate) + assert coskewness(X, X, X) == skewness(X) + assert coskewness(X, Y + rate*X, Y + 2*rate*X) == \ + 4/(sqrt(1 + 1/(4*rate**2))*sqrt(4 + 1/(4*rate**2))) + assert coskewness(X + 2*Y, Y + X, Y + 2*X, X > 3) == \ + sqrt(170)*Rational(9, 85) + +def test_exponential_power(): + mu = Symbol('mu') + z = Symbol('z') + alpha = Symbol('alpha', positive=True) + beta = Symbol('beta', positive=True) + + X = ExponentialPower('x', mu, alpha, beta) + + assert density(X)(z) == beta*exp(-(Abs(mu - z)/alpha) + ** beta)/(2*alpha*gamma(1/beta)) + assert cdf(X)(z) == S.Half + lowergamma(1/beta, + (Abs(mu - z)/alpha)**beta)*sign(-mu + z)/\ + (2*gamma(1/beta)) + + +def test_f_distribution(): + d1 = Symbol("d1", positive=True) + d2 = Symbol("d2", positive=True) + + X = FDistribution("x", d1, d2) + + assert density(X)(x) == (d2**(d2/2)*sqrt((d1*x)**d1*(d1*x + d2)**(-d1 - d2)) + /(x*beta(d1/2, d2/2))) + + raises(NotImplementedError, lambda: moment_generating_function(X)) + d1 = Symbol("d1", nonpositive=True) + raises(ValueError, lambda: FDistribution('x', d1, d1)) + + d1 = Symbol("d1", positive=True, integer=False) + raises(ValueError, lambda: FDistribution('x', d1, d1)) + + d1 = Symbol("d1", positive=True) + d2 = Symbol("d2", nonpositive=True) + raises(ValueError, lambda: FDistribution('x', d1, d2)) + + d2 = Symbol("d2", positive=True, integer=False) + raises(ValueError, lambda: FDistribution('x', d1, d2)) + + +def test_fisher_z(): + d1 = Symbol("d1", positive=True) + d2 = Symbol("d2", positive=True) + + X = FisherZ("x", d1, d2) + assert density(X)(x) == (2*d1**(d1/2)*d2**(d2/2)*(d1*exp(2*x) + d2) + **(-d1/2 - d2/2)*exp(d1*x)/beta(d1/2, d2/2)) + +def test_frechet(): + a = Symbol("a", positive=True) + s = Symbol("s", positive=True) + m = Symbol("m", real=True) + + X = Frechet("x", a, s=s, m=m) + assert density(X)(x) == a*((x - m)/s)**(-a - 1)*exp(-((x - m)/s)**(-a))/s + assert cdf(X)(x) == Piecewise((exp(-((-m + x)/s)**(-a)), m <= x), (0, True)) + +@slow +def test_gamma(): + k = Symbol("k", positive=True) + theta = Symbol("theta", positive=True) + + X = Gamma('x', k, theta) + + # Tests characteristic function + assert characteristic_function(X)(x) == ((-I*theta*x + 1)**(-k)) + + assert density(X)(x) == x**(k - 1)*theta**(-k)*exp(-x/theta)/gamma(k) + assert cdf(X, meijerg=True)(z) == Piecewise( + (-k*lowergamma(k, 0)/gamma(k + 1) + + k*lowergamma(k, z/theta)/gamma(k + 1), z >= 0), + (0, True)) + + # assert simplify(variance(X)) == k*theta**2 # handled numerically below + assert E(X) == moment(X, 1) + + k, theta = symbols('k theta', positive=True) + X = Gamma('x', k, theta) + assert E(X) == k*theta + assert variance(X) == k*theta**2 + assert skewness(X).expand() == 2/sqrt(k) + assert kurtosis(X).expand() == 3 + 6/k + + Y = Gamma('y', 2*k, 3*theta) + assert coskewness(X, theta*X + Y, k*X + Y).simplify() == \ + 2*531441**(-k)*sqrt(k)*theta*(3*3**(12*k) - 2*531441**k) \ + /(sqrt(k**2 + 18)*sqrt(theta**2 + 18)) + +def test_gamma_inverse(): + a = Symbol("a", positive=True) + b = Symbol("b", positive=True) + X = GammaInverse("x", a, b) + assert density(X)(x) == x**(-a - 1)*b**a*exp(-b/x)/gamma(a) + assert cdf(X)(x) == Piecewise((uppergamma(a, b/x)/gamma(a), x > 0), (0, True)) + assert characteristic_function(X)(x) == 2 * (-I*b*x)**(a/2) \ + * besselk(a, 2*sqrt(b)*sqrt(-I*x))/gamma(a) + raises(NotImplementedError, lambda: moment_generating_function(X)) + +def test_gompertz(): + b = Symbol("b", positive=True) + eta = Symbol("eta", positive=True) + + X = Gompertz("x", b, eta) + + assert density(X)(x) == b*eta*exp(eta)*exp(b*x)*exp(-eta*exp(b*x)) + assert cdf(X)(x) == 1 - exp(eta)*exp(-eta*exp(b*x)) + assert diff(cdf(X)(x), x) == density(X)(x) + + +def test_gumbel(): + beta = Symbol("beta", positive=True) + mu = Symbol("mu") + x = Symbol("x") + y = Symbol("y") + X = Gumbel("x", beta, mu) + Y = Gumbel("y", beta, mu, minimum=True) + assert density(X)(x).expand() == \ + exp(mu/beta)*exp(-x/beta)*exp(-exp(mu/beta)*exp(-x/beta))/beta + assert density(Y)(y).expand() == \ + exp(-mu/beta)*exp(y/beta)*exp(-exp(-mu/beta)*exp(y/beta))/beta + assert cdf(X)(x).expand() == \ + exp(-exp(mu/beta)*exp(-x/beta)) + assert characteristic_function(X)(x) == exp(I*mu*x)*gamma(-I*beta*x + 1) + +def test_kumaraswamy(): + a = Symbol("a", positive=True) + b = Symbol("b", positive=True) + + X = Kumaraswamy("x", a, b) + assert density(X)(x) == x**(a - 1)*a*b*(-x**a + 1)**(b - 1) + assert cdf(X)(x) == Piecewise((0, x < 0), + (-(-x**a + 1)**b + 1, x <= 1), + (1, True)) + + +def test_laplace(): + mu = Symbol("mu") + b = Symbol("b", positive=True) + + X = Laplace('x', mu, b) + + #Tests characteristic_function + assert characteristic_function(X)(x) == (exp(I*mu*x)/(b**2*x**2 + 1)) + + assert density(X)(x) == exp(-Abs(x - mu)/b)/(2*b) + assert cdf(X)(x) == Piecewise((exp((-mu + x)/b)/2, mu > x), + (-exp((mu - x)/b)/2 + 1, True)) + X = Laplace('x', [1, 2], [[1, 0], [0, 1]]) + assert isinstance(pspace(X).distribution, MultivariateLaplaceDistribution) + +def test_levy(): + mu = Symbol("mu", real=True) + c = Symbol("c", positive=True) + + X = Levy('x', mu, c) + assert X.pspace.domain.set == Interval(mu, oo) + assert density(X)(x) == sqrt(c/(2*pi))*exp(-c/(2*(x - mu)))/((x - mu)**(S.One + S.Half)) + assert cdf(X)(x) == erfc(sqrt(c/(2*(x - mu)))) + + raises(NotImplementedError, lambda: moment_generating_function(X)) + mu = Symbol("mu", real=False) + raises(ValueError, lambda: Levy('x',mu,c)) + + c = Symbol("c", nonpositive=True) + raises(ValueError, lambda: Levy('x',mu,c)) + + mu = Symbol("mu", real=True) + raises(ValueError, lambda: Levy('x',mu,c)) + +def test_logcauchy(): + mu = Symbol("mu", positive=True) + sigma = Symbol("sigma", positive=True) + + X = LogCauchy("x", mu, sigma) + + assert density(X)(x) == sigma/(x*pi*(sigma**2 + (-mu + log(x))**2)) + assert cdf(X)(x) == atan((log(x) - mu)/sigma)/pi + S.Half + + +def test_logistic(): + mu = Symbol("mu", real=True) + s = Symbol("s", positive=True) + p = Symbol("p", positive=True) + + X = Logistic('x', mu, s) + + #Tests characteristics_function + assert characteristic_function(X)(x) == \ + (Piecewise((pi*s*x*exp(I*mu*x)/sinh(pi*s*x), Ne(x, 0)), (1, True))) + + assert density(X)(x) == exp((-x + mu)/s)/(s*(exp((-x + mu)/s) + 1)**2) + assert cdf(X)(x) == 1/(exp((mu - x)/s) + 1) + assert quantile(X)(p) == mu - s*log(-S.One + 1/p) + +def test_loglogistic(): + a, b = symbols('a b') + assert LogLogistic('x', a, b) + + a = Symbol('a', negative=True) + b = Symbol('b', positive=True) + raises(ValueError, lambda: LogLogistic('x', a, b)) + + a = Symbol('a', positive=True) + b = Symbol('b', negative=True) + raises(ValueError, lambda: LogLogistic('x', a, b)) + + a, b, z, p = symbols('a b z p', positive=True) + X = LogLogistic('x', a, b) + assert density(X)(z) == b*(z/a)**(b - 1)/(a*((z/a)**b + 1)**2) + assert cdf(X)(z) == 1/(1 + (z/a)**(-b)) + assert quantile(X)(p) == a*(p/(1 - p))**(1/b) + + # Expectation + assert E(X) == Piecewise((S.NaN, b <= 1), (pi*a/(b*sin(pi/b)), True)) + b = symbols('b', prime=True) # b > 1 + X = LogLogistic('x', a, b) + assert E(X) == pi*a/(b*sin(pi/b)) + X = LogLogistic('x', 1, 2) + assert median(X) == FiniteSet(1) + +def test_logitnormal(): + mu = Symbol('mu', real=True) + s = Symbol('s', positive=True) + X = LogitNormal('x', mu, s) + x = Symbol('x') + + assert density(X)(x) == sqrt(2)*exp(-(-mu + log(x/(1 - x)))**2/(2*s**2))/(2*sqrt(pi)*s*x*(1 - x)) + assert cdf(X)(x) == erf(sqrt(2)*(-mu + log(x/(1 - x)))/(2*s))/2 + S(1)/2 + +def test_lognormal(): + mean = Symbol('mu', real=True) + std = Symbol('sigma', positive=True) + X = LogNormal('x', mean, std) + # The sympy integrator can't do this too well + #assert E(X) == exp(mean+std**2/2) + #assert variance(X) == (exp(std**2)-1) * exp(2*mean + std**2) + + # The sympy integrator can't do this too well + #assert E(X) == + raises(NotImplementedError, lambda: moment_generating_function(X)) + mu = Symbol("mu", real=True) + sigma = Symbol("sigma", positive=True) + + X = LogNormal('x', mu, sigma) + assert density(X)(x) == (sqrt(2)*exp(-(-mu + log(x))**2 + /(2*sigma**2))/(2*x*sqrt(pi)*sigma)) + # Tests cdf + assert cdf(X)(x) == Piecewise( + (erf(sqrt(2)*(-mu + log(x))/(2*sigma))/2 + + S(1)/2, x > 0), (0, True)) + + X = LogNormal('x', 0, 1) # Mean 0, standard deviation 1 + assert density(X)(x) == sqrt(2)*exp(-log(x)**2/2)/(2*x*sqrt(pi)) + + +def test_Lomax(): + a, l = symbols('a, l', negative=True) + raises(ValueError, lambda: Lomax('X', a, l)) + a, l = symbols('a, l', real=False) + raises(ValueError, lambda: Lomax('X', a, l)) + + a, l = symbols('a, l', positive=True) + X = Lomax('X', a, l) + assert X.pspace.domain.set == Interval(0, oo) + assert density(X)(x) == a*(1 + x/l)**(-a - 1)/l + assert cdf(X)(x) == Piecewise((1 - (1 + x/l)**(-a), x >= 0), (0, True)) + a = 3 + X = Lomax('X', a, l) + assert E(X) == l/2 + assert median(X) == FiniteSet(l*(-1 + 2**Rational(1, 3))) + assert variance(X) == 3*l**2/4 + + +def test_maxwell(): + a = Symbol("a", positive=True) + + X = Maxwell('x', a) + + assert density(X)(x) == (sqrt(2)*x**2*exp(-x**2/(2*a**2))/ + (sqrt(pi)*a**3)) + assert E(X) == 2*sqrt(2)*a/sqrt(pi) + assert variance(X) == -8*a**2/pi + 3*a**2 + assert cdf(X)(x) == erf(sqrt(2)*x/(2*a)) - sqrt(2)*x*exp(-x**2/(2*a**2))/(sqrt(pi)*a) + assert diff(cdf(X)(x), x) == density(X)(x) + + +@slow +def test_Moyal(): + mu = Symbol('mu',real=False) + sigma = Symbol('sigma', positive=True) + raises(ValueError, lambda: Moyal('M',mu, sigma)) + + mu = Symbol('mu', real=True) + sigma = Symbol('sigma', negative=True) + raises(ValueError, lambda: Moyal('M',mu, sigma)) + + sigma = Symbol('sigma', positive=True) + M = Moyal('M', mu, sigma) + assert density(M)(z) == sqrt(2)*exp(-exp((mu - z)/sigma)/2 + - (-mu + z)/(2*sigma))/(2*sqrt(pi)*sigma) + assert cdf(M)(z).simplify() == 1 - erf(sqrt(2)*exp((mu - z)/(2*sigma))/2) + assert characteristic_function(M)(z) == 2**(-I*sigma*z)*exp(I*mu*z) \ + *gamma(-I*sigma*z + Rational(1, 2))/sqrt(pi) + assert E(M) == mu + EulerGamma*sigma + sigma*log(2) + assert moment_generating_function(M)(z) == 2**(-sigma*z)*exp(mu*z) \ + *gamma(-sigma*z + Rational(1, 2))/sqrt(pi) + + +def test_nakagami(): + mu = Symbol("mu", positive=True) + omega = Symbol("omega", positive=True) + + X = Nakagami('x', mu, omega) + assert density(X)(x) == (2*x**(2*mu - 1)*mu**mu*omega**(-mu) + *exp(-x**2*mu/omega)/gamma(mu)) + assert simplify(E(X)) == (sqrt(mu)*sqrt(omega) + *gamma(mu + S.Half)/gamma(mu + 1)) + assert simplify(variance(X)) == ( + omega - omega*gamma(mu + S.Half)**2/(gamma(mu)*gamma(mu + 1))) + assert cdf(X)(x) == Piecewise( + (lowergamma(mu, mu*x**2/omega)/gamma(mu), x > 0), + (0, True)) + X = Nakagami('x', 1, 1) + assert median(X) == FiniteSet(sqrt(log(2))) + +def test_gaussian_inverse(): + # test for symbolic parameters + a, b = symbols('a b') + assert GaussianInverse('x', a, b) + + # Inverse Gaussian distribution is also known as Wald distribution + # `GaussianInverse` can also be referred by the name `Wald` + a, b, z = symbols('a b z') + X = Wald('x', a, b) + assert density(X)(z) == sqrt(2)*sqrt(b/z**3)*exp(-b*(-a + z)**2/(2*a**2*z))/(2*sqrt(pi)) + + a, b = symbols('a b', positive=True) + z = Symbol('z', positive=True) + + X = GaussianInverse('x', a, b) + assert density(X)(z) == sqrt(2)*sqrt(b)*sqrt(z**(-3))*exp(-b*(-a + z)**2/(2*a**2*z))/(2*sqrt(pi)) + assert E(X) == a + assert variance(X).expand() == a**3/b + assert cdf(X)(z) == (S.Half - erf(sqrt(2)*sqrt(b)*(1 + z/a)/(2*sqrt(z)))/2)*exp(2*b/a) +\ + erf(sqrt(2)*sqrt(b)*(-1 + z/a)/(2*sqrt(z)))/2 + S.Half + + a = symbols('a', nonpositive=True) + raises(ValueError, lambda: GaussianInverse('x', a, b)) + + a = symbols('a', positive=True) + b = symbols('b', nonpositive=True) + raises(ValueError, lambda: GaussianInverse('x', a, b)) + +def test_pareto(): + xm, beta = symbols('xm beta', positive=True) + alpha = beta + 5 + X = Pareto('x', xm, alpha) + + dens = density(X) + + #Tests cdf function + assert cdf(X)(x) == \ + Piecewise((-x**(-beta - 5)*xm**(beta + 5) + 1, x >= xm), (0, True)) + + #Tests characteristic_function + assert characteristic_function(X)(x) == \ + ((-I*x*xm)**(beta + 5)*(beta + 5)*uppergamma(-beta - 5, -I*x*xm)) + + assert dens(x) == x**(-(alpha + 1))*xm**(alpha)*(alpha) + + assert simplify(E(X)) == alpha*xm/(alpha-1) + + # computation of taylor series for MGF still too slow + #assert simplify(variance(X)) == xm**2*alpha / ((alpha-1)**2*(alpha-2)) + + +def test_pareto_numeric(): + xm, beta = 3, 2 + alpha = beta + 5 + X = Pareto('x', xm, alpha) + + assert E(X) == alpha*xm/S(alpha - 1) + assert variance(X) == xm**2*alpha / S((alpha - 1)**2*(alpha - 2)) + assert median(X) == FiniteSet(3*2**Rational(1, 7)) + # Skewness tests too slow. Try shortcutting function? + + +def test_PowerFunction(): + alpha = Symbol("alpha", nonpositive=True) + a, b = symbols('a, b', real=True) + raises (ValueError, lambda: PowerFunction('x', alpha, a, b)) + + a, b = symbols('a, b', real=False) + raises (ValueError, lambda: PowerFunction('x', alpha, a, b)) + + alpha = Symbol("alpha", positive=True) + a, b = symbols('a, b', real=True) + raises (ValueError, lambda: PowerFunction('x', alpha, 5, 2)) + + X = PowerFunction('X', 2, a, b) + assert density(X)(z) == (-2*a + 2*z)/(-a + b)**2 + assert cdf(X)(z) == Piecewise((a**2/(a**2 - 2*a*b + b**2) - + 2*a*z/(a**2 - 2*a*b + b**2) + z**2/(a**2 - 2*a*b + b**2), a <= z), (0, True)) + + X = PowerFunction('X', 2, 0, 1) + assert density(X)(z) == 2*z + assert cdf(X)(z) == Piecewise((z**2, z >= 0), (0,True)) + assert E(X) == Rational(2,3) + assert P(X < 0) == 0 + assert P(X < 1) == 1 + assert median(X) == FiniteSet(1/sqrt(2)) + +def test_raised_cosine(): + mu = Symbol("mu", real=True) + s = Symbol("s", positive=True) + + X = RaisedCosine("x", mu, s) + + assert pspace(X).domain.set == Interval(mu - s, mu + s) + #Tests characteristics_function + assert characteristic_function(X)(x) == \ + Piecewise((exp(-I*pi*mu/s)/2, Eq(x, -pi/s)), (exp(I*pi*mu/s)/2, Eq(x, pi/s)), (pi**2*exp(I*mu*x)*sin(s*x)/(s*x*(-s**2*x**2 + pi**2)), True)) + + assert density(X)(x) == (Piecewise(((cos(pi*(x - mu)/s) + 1)/(2*s), + And(x <= mu + s, mu - s <= x)), (0, True))) + + +def test_rayleigh(): + sigma = Symbol("sigma", positive=True) + + X = Rayleigh('x', sigma) + + #Tests characteristic_function + assert characteristic_function(X)(x) == (-sqrt(2)*sqrt(pi)*sigma*x*(erfi(sqrt(2)*sigma*x/2) - I)*exp(-sigma**2*x**2/2)/2 + 1) + + assert density(X)(x) == x*exp(-x**2/(2*sigma**2))/sigma**2 + assert E(X) == sqrt(2)*sqrt(pi)*sigma/2 + assert variance(X) == -pi*sigma**2/2 + 2*sigma**2 + assert cdf(X)(x) == 1 - exp(-x**2/(2*sigma**2)) + assert diff(cdf(X)(x), x) == density(X)(x) + +def test_reciprocal(): + a = Symbol("a", real=True) + b = Symbol("b", real=True) + + X = Reciprocal('x', a, b) + assert density(X)(x) == 1/(x*(-log(a) + log(b))) + assert cdf(X)(x) == Piecewise((log(a)/(log(a) - log(b)) - log(x)/(log(a) - log(b)), a <= x), (0, True)) + X = Reciprocal('x', 5, 30) + + assert E(X) == 25/(log(30) - log(5)) + assert P(X < 4) == S.Zero + assert P(X < 20) == log(20) / (log(30) - log(5)) - log(5) / (log(30) - log(5)) + assert cdf(X)(10) == log(10) / (log(30) - log(5)) - log(5) / (log(30) - log(5)) + + a = symbols('a', nonpositive=True) + raises(ValueError, lambda: Reciprocal('x', a, b)) + + a = symbols('a', positive=True) + b = symbols('b', positive=True) + raises(ValueError, lambda: Reciprocal('x', a + b, a)) + +def test_shiftedgompertz(): + b = Symbol("b", positive=True) + eta = Symbol("eta", positive=True) + X = ShiftedGompertz("x", b, eta) + assert density(X)(x) == b*(eta*(1 - exp(-b*x)) + 1)*exp(-b*x)*exp(-eta*exp(-b*x)) + + +def test_studentt(): + nu = Symbol("nu", positive=True) + + X = StudentT('x', nu) + assert density(X)(x) == (1 + x**2/nu)**(-nu/2 - S.Half)/(sqrt(nu)*beta(S.Half, nu/2)) + assert cdf(X)(x) == S.Half + x*gamma(nu/2 + S.Half)*hyper((S.Half, nu/2 + S.Half), + (Rational(3, 2),), -x**2/nu)/(sqrt(pi)*sqrt(nu)*gamma(nu/2)) + raises(NotImplementedError, lambda: moment_generating_function(X)) + +def test_trapezoidal(): + a = Symbol("a", real=True) + b = Symbol("b", real=True) + c = Symbol("c", real=True) + d = Symbol("d", real=True) + + X = Trapezoidal('x', a, b, c, d) + assert density(X)(x) == Piecewise(((-2*a + 2*x)/((-a + b)*(-a - b + c + d)), (a <= x) & (x < b)), + (2/(-a - b + c + d), (b <= x) & (x < c)), + ((2*d - 2*x)/((-c + d)*(-a - b + c + d)), (c <= x) & (x <= d)), + (0, True)) + + X = Trapezoidal('x', 0, 1, 2, 3) + assert E(X) == Rational(3, 2) + assert variance(X) == Rational(5, 12) + assert P(X < 2) == Rational(3, 4) + assert median(X) == FiniteSet(Rational(3, 2)) + +def test_triangular(): + a = Symbol("a") + b = Symbol("b") + c = Symbol("c") + + X = Triangular('x', a, b, c) + assert pspace(X).domain.set == Interval(a, b) + assert str(density(X)(x)) == ("Piecewise(((-2*a + 2*x)/((-a + b)*(-a + c)), (a <= x) & (c > x)), " + "(2/(-a + b), Eq(c, x)), ((2*b - 2*x)/((-a + b)*(b - c)), (b >= x) & (c < x)), (0, True))") + + #Tests moment_generating_function + assert moment_generating_function(X)(x).expand() == \ + ((-2*(-a + b)*exp(c*x) + 2*(-a + c)*exp(b*x) + 2*(b - c)*exp(a*x))/(x**2*(-a + b)*(-a + c)*(b - c))).expand() + assert str(characteristic_function(X)(x)) == \ + '(2*(-a + b)*exp(I*c*x) - 2*(-a + c)*exp(I*b*x) - 2*(b - c)*exp(I*a*x))/(x**2*(-a + b)*(-a + c)*(b - c))' + +def test_quadratic_u(): + a = Symbol("a", real=True) + b = Symbol("b", real=True) + + X = QuadraticU("x", a, b) + Y = QuadraticU("x", 1, 2) + + assert pspace(X).domain.set == Interval(a, b) + # Tests _moment_generating_function + assert moment_generating_function(Y)(1) == -15*exp(2) + 27*exp(1) + assert moment_generating_function(Y)(2) == -9*exp(4)/2 + 21*exp(2)/2 + + assert characteristic_function(Y)(1) == 3*I*(-1 + 4*I)*exp(I*exp(2*I)) + assert density(X)(x) == (Piecewise((12*(x - a/2 - b/2)**2/(-a + b)**3, + And(x <= b, a <= x)), (0, True))) + + +def test_uniform(): + l = Symbol('l', real=True) + w = Symbol('w', positive=True) + X = Uniform('x', l, l + w) + + assert E(X) == l + w/2 + assert variance(X).expand() == w**2/12 + + # With numbers all is well + X = Uniform('x', 3, 5) + assert P(X < 3) == 0 and P(X > 5) == 0 + assert P(X < 4) == P(X > 4) == S.Half + assert median(X) == FiniteSet(4) + + z = Symbol('z') + p = density(X)(z) + assert p.subs(z, 3.7) == S.Half + assert p.subs(z, -1) == 0 + assert p.subs(z, 6) == 0 + + c = cdf(X) + assert c(2) == 0 and c(3) == 0 + assert c(Rational(7, 2)) == Rational(1, 4) + assert c(5) == 1 and c(6) == 1 + + +@XFAIL +@slow +def test_uniform_P(): + """ This stopped working because SingleContinuousPSpace.compute_density no + longer calls integrate on a DiracDelta but rather just solves directly. + integrate used to call UniformDistribution.expectation which special-cased + subsed out the Min and Max terms that Uniform produces + + I decided to regress on this class for general cleanliness (and I suspect + speed) of the algorithm. + """ + l = Symbol('l', real=True) + w = Symbol('w', positive=True) + X = Uniform('x', l, l + w) + assert P(X < l) == 0 and P(X > l + w) == 0 + + +def test_uniformsum(): + n = Symbol("n", integer=True) + _k = Dummy("k") + x = Symbol("x") + + X = UniformSum('x', n) + res = Sum((-1)**_k*(-_k + x)**(n - 1)*binomial(n, _k), (_k, 0, floor(x)))/factorial(n - 1) + assert density(X)(x).dummy_eq(res) + + #Tests set functions + assert X.pspace.domain.set == Interval(0, n) + + #Tests the characteristic_function + assert characteristic_function(X)(x) == (-I*(exp(I*x) - 1)/x)**n + + #Tests the moment_generating_function + assert moment_generating_function(X)(x) == ((exp(x) - 1)/x)**n + + +def test_von_mises(): + mu = Symbol("mu") + k = Symbol("k", positive=True) + + X = VonMises("x", mu, k) + assert density(X)(x) == exp(k*cos(x - mu))/(2*pi*besseli(0, k)) + + +def test_weibull(): + a, b = symbols('a b', positive=True) + # FIXME: simplify(E(X)) seems to hang without extended_positive=True + # On a Linux machine this had a rapid memory leak... + # a, b = symbols('a b', positive=True) + X = Weibull('x', a, b) + + assert E(X).expand() == a * gamma(1 + 1/b) + assert variance(X).expand() == (a**2 * gamma(1 + 2/b) - E(X)**2).expand() + assert simplify(skewness(X)) == (2*gamma(1 + 1/b)**3 - 3*gamma(1 + 1/b)*gamma(1 + 2/b) + gamma(1 + 3/b))/(-gamma(1 + 1/b)**2 + gamma(1 + 2/b))**Rational(3, 2) + assert simplify(kurtosis(X)) == (-3*gamma(1 + 1/b)**4 +\ + 6*gamma(1 + 1/b)**2*gamma(1 + 2/b) - 4*gamma(1 + 1/b)*gamma(1 + 3/b) + gamma(1 + 4/b))/(gamma(1 + 1/b)**2 - gamma(1 + 2/b))**2 + +def test_weibull_numeric(): + # Test for integers and rationals + a = 1 + bvals = [S.Half, 1, Rational(3, 2), 5] + for b in bvals: + X = Weibull('x', a, b) + assert simplify(E(X)) == expand_func(a * gamma(1 + 1/S(b))) + assert simplify(variance(X)) == simplify( + a**2 * gamma(1 + 2/S(b)) - E(X)**2) + # Not testing Skew... it's slow with int/frac values > 3/2 + + +def test_wignersemicircle(): + R = Symbol("R", positive=True) + + X = WignerSemicircle('x', R) + assert pspace(X).domain.set == Interval(-R, R) + assert density(X)(x) == 2*sqrt(-x**2 + R**2)/(pi*R**2) + assert E(X) == 0 + + + #Tests ChiNoncentralDistribution + assert characteristic_function(X)(x) == \ + Piecewise((2*besselj(1, R*x)/(R*x), Ne(x, 0)), (1, True)) + + +def test_input_value_assertions(): + a, b = symbols('a b') + p, q = symbols('p q', positive=True) + m, n = symbols('m n', positive=False, real=True) + + raises(ValueError, lambda: Normal('x', 3, 0)) + raises(ValueError, lambda: Normal('x', m, n)) + Normal('X', a, p) # No error raised + raises(ValueError, lambda: Exponential('x', m)) + Exponential('Ex', p) # No error raised + for fn in [Pareto, Weibull, Beta, Gamma]: + raises(ValueError, lambda: fn('x', m, p)) + raises(ValueError, lambda: fn('x', p, n)) + fn('x', p, q) # No error raised + + +def test_unevaluated(): + X = Normal('x', 0, 1) + k = Dummy('k') + expr1 = Integral(sqrt(2)*k*exp(-k**2/2)/(2*sqrt(pi)), (k, -oo, oo)) + expr2 = Integral(sqrt(2)*exp(-k**2/2)/(2*sqrt(pi)), (k, 0, oo)) + with ignore_warnings(UserWarning): ### TODO: Restore tests once warnings are removed + assert E(X, evaluate=False).rewrite(Integral).dummy_eq(expr1) + assert E(X + 1, evaluate=False).rewrite(Integral).dummy_eq(expr1 + 1) + assert P(X > 0, evaluate=False).rewrite(Integral).dummy_eq(expr2) + + assert P(X > 0, X**2 < 1) == S.Half + + +def test_probability_unevaluated(): + T = Normal('T', 30, 3) + with ignore_warnings(UserWarning): ### TODO: Restore tests once warnings are removed + assert type(P(T > 33, evaluate=False)) == Probability + + +def test_density_unevaluated(): + X = Normal('X', 0, 1) + Y = Normal('Y', 0, 2) + assert isinstance(density(X+Y, evaluate=False)(z), Integral) + + +def test_NormalDistribution(): + nd = NormalDistribution(0, 1) + x = Symbol('x') + assert nd.cdf(x) == erf(sqrt(2)*x/2)/2 + S.Half + assert nd.expectation(1, x) == 1 + assert nd.expectation(x, x) == 0 + assert nd.expectation(x**2, x) == 1 + #Test issue 10076 + a = SingleContinuousPSpace(x, NormalDistribution(2, 4)) + _z = Dummy('_z') + + expected1 = Integral(sqrt(2)*exp(-(_z - 2)**2/32)/(8*sqrt(pi)),(_z, -oo, 1)) + assert a.probability(x < 1, evaluate=False).dummy_eq(expected1) is True + + expected2 = Integral(sqrt(2)*exp(-(_z - 2)**2/32)/(8*sqrt(pi)),(_z, 1, oo)) + assert a.probability(x > 1, evaluate=False).dummy_eq(expected2) is True + + b = SingleContinuousPSpace(x, NormalDistribution(1, 9)) + + expected3 = Integral(sqrt(2)*exp(-(_z - 1)**2/162)/(18*sqrt(pi)),(_z, 6, oo)) + assert b.probability(x > 6, evaluate=False).dummy_eq(expected3) is True + + expected4 = Integral(sqrt(2)*exp(-(_z - 1)**2/162)/(18*sqrt(pi)),(_z, -oo, 6)) + assert b.probability(x < 6, evaluate=False).dummy_eq(expected4) is True + + +def test_random_parameters(): + mu = Normal('mu', 2, 3) + meas = Normal('T', mu, 1) + assert density(meas, evaluate=False)(z) + assert isinstance(pspace(meas), CompoundPSpace) + X = Normal('x', [1, 2], [[1, 0], [0, 1]]) + assert isinstance(pspace(X).distribution, MultivariateNormalDistribution) + assert density(meas)(z).simplify() == sqrt(5)*exp(-z**2/20 + z/5 - S(1)/5)/(10*sqrt(pi)) + + +def test_random_parameters_given(): + mu = Normal('mu', 2, 3) + meas = Normal('T', mu, 1) + assert given(meas, Eq(mu, 5)) == Normal('T', 5, 1) + + +def test_conjugate_priors(): + mu = Normal('mu', 2, 3) + x = Normal('x', mu, 1) + assert isinstance(simplify(density(mu, Eq(x, y), evaluate=False)(z)), + Mul) + + +def test_difficult_univariate(): + """ Since using solve in place of deltaintegrate we're able to perform + substantially more complex density computations on single continuous random + variables """ + x = Normal('x', 0, 1) + assert density(x**3) + assert density(exp(x**2)) + assert density(log(x)) + + +def test_issue_10003(): + X = Exponential('x', 3) + G = Gamma('g', 1, 2) + assert P(X < -1) is S.Zero + assert P(G < -1) is S.Zero + + +def test_precomputed_cdf(): + x = symbols("x", real=True) + mu = symbols("mu", real=True) + sigma, xm, alpha = symbols("sigma xm alpha", positive=True) + n = symbols("n", integer=True, positive=True) + distribs = [ + Normal("X", mu, sigma), + Pareto("P", xm, alpha), + ChiSquared("C", n), + Exponential("E", sigma), + # LogNormal("L", mu, sigma), + ] + for X in distribs: + compdiff = cdf(X)(x) - simplify(X.pspace.density.compute_cdf()(x)) + compdiff = simplify(compdiff.rewrite(erfc)) + assert compdiff == 0 + + +@slow +def test_precomputed_characteristic_functions(): + import mpmath + + def test_cf(dist, support_lower_limit, support_upper_limit): + pdf = density(dist) + t = Symbol('t') + + # first function is the hardcoded CF of the distribution + cf1 = lambdify([t], characteristic_function(dist)(t), 'mpmath') + + # second function is the Fourier transform of the density function + f = lambdify([x, t], pdf(x)*exp(I*x*t), 'mpmath') + cf2 = lambda t: mpmath.quad(lambda x: f(x, t), [support_lower_limit, support_upper_limit], maxdegree=10) + + # compare the two functions at various points + for test_point in [2, 5, 8, 11]: + n1 = cf1(test_point) + n2 = cf2(test_point) + + assert abs(re(n1) - re(n2)) < 1e-12 + assert abs(im(n1) - im(n2)) < 1e-12 + + test_cf(Beta('b', 1, 2), 0, 1) + test_cf(Chi('c', 3), 0, mpmath.inf) + test_cf(ChiSquared('c', 2), 0, mpmath.inf) + test_cf(Exponential('e', 6), 0, mpmath.inf) + test_cf(Logistic('l', 1, 2), -mpmath.inf, mpmath.inf) + test_cf(Normal('n', -1, 5), -mpmath.inf, mpmath.inf) + test_cf(RaisedCosine('r', 3, 1), 2, 4) + test_cf(Rayleigh('r', 0.5), 0, mpmath.inf) + test_cf(Uniform('u', -1, 1), -1, 1) + test_cf(WignerSemicircle('w', 3), -3, 3) + + +def test_long_precomputed_cdf(): + x = symbols("x", real=True) + distribs = [ + Arcsin("A", -5, 9), + Dagum("D", 4, 10, 3), + Erlang("E", 14, 5), + Frechet("F", 2, 6, -3), + Gamma("G", 2, 7), + GammaInverse("GI", 3, 5), + Kumaraswamy("K", 6, 8), + Laplace("LA", -5, 4), + Logistic("L", -6, 7), + Nakagami("N", 2, 7), + StudentT("S", 4) + ] + for distr in distribs: + for _ in range(5): + assert tn(diff(cdf(distr)(x), x), density(distr)(x), x, a=0, b=0, c=1, d=0) + + US = UniformSum("US", 5) + pdf01 = density(US)(x).subs(floor(x), 0).doit() # pdf on (0, 1) + cdf01 = cdf(US, evaluate=False)(x).subs(floor(x), 0).doit() # cdf on (0, 1) + assert tn(diff(cdf01, x), pdf01, x, a=0, b=0, c=1, d=0) + + +def test_issue_13324(): + X = Uniform('X', 0, 1) + assert E(X, X > S.Half) == Rational(3, 4) + assert E(X, X > 0) == S.Half + +def test_issue_20756(): + X = Uniform('X', -1, +1) + Y = Uniform('Y', -1, +1) + assert E(X * Y) == S.Zero + assert E(X * ((Y + 1) - 1)) == S.Zero + assert E(Y * (X*(X + 1) - X*X)) == S.Zero + +def test_FiniteSet_prob(): + E = Exponential('E', 3) + N = Normal('N', 5, 7) + assert P(Eq(E, 1)) is S.Zero + assert P(Eq(N, 2)) is S.Zero + assert P(Eq(N, x)) is S.Zero + +def test_prob_neq(): + E = Exponential('E', 4) + X = ChiSquared('X', 4) + assert P(Ne(E, 2)) == 1 + assert P(Ne(X, 4)) == 1 + assert P(Ne(X, 4)) == 1 + assert P(Ne(X, 5)) == 1 + assert P(Ne(E, x)) == 1 + +def test_union(): + N = Normal('N', 3, 2) + assert simplify(P(N**2 - N > 2)) == \ + -erf(sqrt(2))/2 - erfc(sqrt(2)/4)/2 + Rational(3, 2) + assert simplify(P(N**2 - 4 > 0)) == \ + -erf(5*sqrt(2)/4)/2 - erfc(sqrt(2)/4)/2 + Rational(3, 2) + +def test_Or(): + N = Normal('N', 0, 1) + assert simplify(P(Or(N > 2, N < 1))) == \ + -erf(sqrt(2))/2 - erfc(sqrt(2)/2)/2 + Rational(3, 2) + assert P(Or(N < 0, N < 1)) == P(N < 1) + assert P(Or(N > 0, N < 0)) == 1 + + +def test_conditional_eq(): + E = Exponential('E', 1) + assert P(Eq(E, 1), Eq(E, 1)) == 1 + assert P(Eq(E, 1), Eq(E, 2)) == 0 + assert P(E > 1, Eq(E, 2)) == 1 + assert P(E < 1, Eq(E, 2)) == 0 + +def test_ContinuousDistributionHandmade(): + x = Symbol('x') + z = Dummy('z') + dens = Lambda(x, Piecewise((S.Half, (0<=x)&(x<1)), (0, (x>=1)&(x<2)), + (S.Half, (x>=2)&(x<3)), (0, True))) + dens = ContinuousDistributionHandmade(dens, set=Interval(0, 3)) + space = SingleContinuousPSpace(z, dens) + assert dens.pdf == Lambda(x, Piecewise((S(1)/2, (x >= 0) & (x < 1)), + (0, (x >= 1) & (x < 2)), (S(1)/2, (x >= 2) & (x < 3)), (0, True))) + assert median(space.value) == Interval(1, 2) + assert E(space.value) == Rational(3, 2) + assert variance(space.value) == Rational(13, 12) + + +def test_issue_16318(): + # test compute_expectation function of the SingleContinuousDomain + N = SingleContinuousDomain(x, Interval(0, 1)) + raises(ValueError, lambda: SingleContinuousDomain.compute_expectation(N, x+1, {x, y})) + +def test_compute_density(): + X = Normal('X', 0, Symbol("sigma")**2) + raises(ValueError, lambda: density(X**5 + X)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_discrete_rv.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_discrete_rv.py new file mode 100644 index 0000000000000000000000000000000000000000..39650a1a08e42840aaf8b06c1eb6e60c92a57f23 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_discrete_rv.py @@ -0,0 +1,312 @@ +from sympy.concrete.summations import Sum +from sympy.core.numbers import (I, Rational, oo, pi) +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions.elementary.complexes import (im, re) +from sympy.functions.elementary.exponential import log +from sympy.functions.elementary.integers import floor +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.special.bessel import besseli +from sympy.functions.special.beta_functions import beta +from sympy.functions.special.zeta_functions import zeta +from sympy.sets.sets import FiniteSet +from sympy.simplify.simplify import simplify +from sympy.utilities.lambdify import lambdify +from sympy.core.relational import Eq, Ne +from sympy.functions.elementary.exponential import exp +from sympy.logic.boolalg import Or +from sympy.sets.fancysets import Range +from sympy.stats import (P, E, variance, density, characteristic_function, + where, moment_generating_function, skewness, cdf, + kurtosis, coskewness) +from sympy.stats.drv_types import (PoissonDistribution, GeometricDistribution, + FlorySchulz, Poisson, Geometric, Hermite, Logarithmic, + NegativeBinomial, Skellam, YuleSimon, Zeta, + DiscreteRV) +from sympy.testing.pytest import slow, nocache_fail, raises, skip +from sympy.stats.symbolic_probability import Expectation +from sympy.functions.combinatorial.factorials import FallingFactorial + +x = Symbol('x') + + +def test_PoissonDistribution(): + l = 3 + p = PoissonDistribution(l) + assert abs(p.cdf(10).evalf() - 1) < .001 + assert abs(p.cdf(10.4).evalf() - 1) < .001 + assert p.expectation(x, x) == l + assert p.expectation(x**2, x) - p.expectation(x, x)**2 == l + + +def test_Poisson(): + l = 3 + x = Poisson('x', l) + assert E(x) == l + assert E(2*x) == 2*l + assert variance(x) == l + assert density(x) == PoissonDistribution(l) + assert isinstance(E(x, evaluate=False), Expectation) + assert isinstance(E(2*x, evaluate=False), Expectation) + # issue 8248 + assert x.pspace.compute_expectation(1) == 1 + # issue 27344 + try: + import numpy as np + except ImportError: + skip("numpy not installed") + y = Poisson('y', np.float64(4.72544290380919e-11)) + assert E(y) == 4.72544290380919e-11 + y = Poisson('y', np.float64(4.725442903809197e-11)) + assert E(y) == 4.725442903809197e-11 + l2 = 5 + z = Poisson('z', l2) + assert E(z) == l2 + assert E(FallingFactorial(z, 3)) == l2**3 + assert E(z**2) == l2 + l2**2 + + +def test_FlorySchulz(): + a = Symbol("a") + z = Symbol("z") + x = FlorySchulz('x', a) + assert E(x) == (2 - a)/a + assert (variance(x) - 2*(1 - a)/a**2).simplify() == S(0) + assert density(x)(z) == a**2*z*(1 - a)**(z - 1) + + +@slow +def test_GeometricDistribution(): + p = S.One / 5 + d = GeometricDistribution(p) + assert d.expectation(x, x) == 1/p + assert d.expectation(x**2, x) - d.expectation(x, x)**2 == (1-p)/p**2 + assert abs(d.cdf(20000).evalf() - 1) < .001 + assert abs(d.cdf(20000.8).evalf() - 1) < .001 + G = Geometric('G', p=S(1)/4) + assert cdf(G)(S(7)/2) == P(G <= S(7)/2) + + X = Geometric('X', Rational(1, 5)) + Y = Geometric('Y', Rational(3, 10)) + assert coskewness(X, X + Y, X + 2*Y).simplify() == sqrt(230)*Rational(81, 1150) + + +def test_Hermite(): + a1 = Symbol("a1", positive=True) + a2 = Symbol("a2", negative=True) + raises(ValueError, lambda: Hermite("H", a1, a2)) + + a1 = Symbol("a1", negative=True) + a2 = Symbol("a2", positive=True) + raises(ValueError, lambda: Hermite("H", a1, a2)) + + a1 = Symbol("a1", positive=True) + x = Symbol("x") + H = Hermite("H", a1, a2) + assert moment_generating_function(H)(x) == exp(a1*(exp(x) - 1) + + a2*(exp(2*x) - 1)) + assert characteristic_function(H)(x) == exp(a1*(exp(I*x) - 1) + + a2*(exp(2*I*x) - 1)) + assert E(H) == a1 + 2*a2 + + H = Hermite("H", a1=5, a2=4) + assert density(H)(2) == 33*exp(-9)/2 + assert E(H) == 13 + assert variance(H) == 21 + assert kurtosis(H) == Rational(464,147) + assert skewness(H) == 37*sqrt(21)/441 + +def test_Logarithmic(): + p = S.Half + x = Logarithmic('x', p) + assert E(x) == -p / ((1 - p) * log(1 - p)) + assert variance(x) == -1/log(2)**2 + 2/log(2) + assert E(2*x**2 + 3*x + 4) == 4 + 7 / log(2) + assert isinstance(E(x, evaluate=False), Expectation) + + +@nocache_fail +def test_negative_binomial(): + r = 5 + p = S.One / 3 + x = NegativeBinomial('x', r, p) + assert E(x) == r * (1 - p) / p + # This hangs when run with the cache disabled: + assert variance(x) == r * (1 - p) / p**2 + assert E(x**5 + 2*x + 3) == E(x**5) + 2*E(x) + 3 == Rational(796473, 1) + assert isinstance(E(x, evaluate=False), Expectation) + + +def test_skellam(): + mu1 = Symbol('mu1') + mu2 = Symbol('mu2') + z = Symbol('z') + X = Skellam('x', mu1, mu2) + + assert density(X)(z) == (mu1/mu2)**(z/2) * \ + exp(-mu1 - mu2)*besseli(z, 2*sqrt(mu1*mu2)) + assert skewness(X).expand() == mu1/(mu1*sqrt(mu1 + mu2) + mu2 * + sqrt(mu1 + mu2)) - mu2/(mu1*sqrt(mu1 + mu2) + mu2*sqrt(mu1 + mu2)) + assert variance(X).expand() == mu1 + mu2 + assert E(X) == mu1 - mu2 + assert characteristic_function(X)(z) == exp( + mu1*exp(I*z) - mu1 - mu2 + mu2*exp(-I*z)) + assert moment_generating_function(X)(z) == exp( + mu1*exp(z) - mu1 - mu2 + mu2*exp(-z)) + + +def test_yule_simon(): + from sympy.core.singleton import S + rho = S(3) + x = YuleSimon('x', rho) + assert simplify(E(x)) == rho / (rho - 1) + assert simplify(variance(x)) == rho**2 / ((rho - 1)**2 * (rho - 2)) + assert isinstance(E(x, evaluate=False), Expectation) + # To test the cdf function + assert cdf(x)(x) == Piecewise((-beta(floor(x), 4)*floor(x) + 1, x >= 1), (0, True)) + + +def test_zeta(): + s = S(5) + x = Zeta('x', s) + assert E(x) == zeta(s-1) / zeta(s) + assert simplify(variance(x)) == ( + zeta(s) * zeta(s-2) - zeta(s-1)**2) / zeta(s)**2 + + +def test_discrete_probability(): + X = Geometric('X', Rational(1, 5)) + Y = Poisson('Y', 4) + G = Geometric('e', x) + assert P(Eq(X, 3)) == Rational(16, 125) + assert P(X < 3) == Rational(9, 25) + assert P(X > 3) == Rational(64, 125) + assert P(X >= 3) == Rational(16, 25) + assert P(X <= 3) == Rational(61, 125) + assert P(Ne(X, 3)) == Rational(109, 125) + assert P(Eq(Y, 3)) == 32*exp(-4)/3 + assert P(Y < 3) == 13*exp(-4) + assert P(Y > 3).equals(32*(Rational(-71, 32) + 3*exp(4)/32)*exp(-4)/3) + assert P(Y >= 3).equals(32*(Rational(-39, 32) + 3*exp(4)/32)*exp(-4)/3) + assert P(Y <= 3) == 71*exp(-4)/3 + assert P(Ne(Y, 3)).equals( + 13*exp(-4) + 32*(Rational(-71, 32) + 3*exp(4)/32)*exp(-4)/3) + assert P(X < S.Infinity) is S.One + assert P(X > S.Infinity) is S.Zero + assert P(G < 3) == x*(2-x) + assert P(Eq(G, 3)) == x*(-x + 1)**2 + + +def test_DiscreteRV(): + p = S(1)/2 + x = Symbol('x', integer=True, positive=True) + pdf = p*(1 - p)**(x - 1) # pdf of Geometric Distribution + D = DiscreteRV(x, pdf, set=S.Naturals, check=True) + assert E(D) == E(Geometric('G', S(1)/2)) == 2 + assert P(D > 3) == S(1)/8 + assert D.pspace.domain.set == S.Naturals + raises(ValueError, lambda: DiscreteRV(x, x, FiniteSet(*range(4)), check=True)) + + # purposeful invalid pmf but it should not raise since check=False + # see test_drv_types.test_ContinuousRV for explanation + X = DiscreteRV(x, 1/x, S.Naturals) + assert P(X < 2) == 1 + assert E(X) == oo + +def test_precomputed_characteristic_functions(): + import mpmath + + def test_cf(dist, support_lower_limit, support_upper_limit): + pdf = density(dist) + t = S('t') + x = S('x') + + # first function is the hardcoded CF of the distribution + cf1 = lambdify([t], characteristic_function(dist)(t), 'mpmath') + + # second function is the Fourier transform of the density function + f = lambdify([x, t], pdf(x)*exp(I*x*t), 'mpmath') + cf2 = lambda t: mpmath.nsum(lambda x: f(x, t), [ + support_lower_limit, support_upper_limit], maxdegree=10) + + # compare the two functions at various points + for test_point in [2, 5, 8, 11]: + n1 = cf1(test_point) + n2 = cf2(test_point) + + assert abs(re(n1) - re(n2)) < 1e-12 + assert abs(im(n1) - im(n2)) < 1e-12 + + test_cf(Geometric('g', Rational(1, 3)), 1, mpmath.inf) + test_cf(Logarithmic('l', Rational(1, 5)), 1, mpmath.inf) + test_cf(NegativeBinomial('n', 5, Rational(1, 7)), 0, mpmath.inf) + test_cf(Poisson('p', 5), 0, mpmath.inf) + test_cf(YuleSimon('y', 5), 1, mpmath.inf) + test_cf(Zeta('z', 5), 1, mpmath.inf) + + +def test_moment_generating_functions(): + t = S('t') + + geometric_mgf = moment_generating_function(Geometric('g', S.Half))(t) + assert geometric_mgf.diff(t).subs(t, 0) == 2 + + logarithmic_mgf = moment_generating_function(Logarithmic('l', S.Half))(t) + assert logarithmic_mgf.diff(t).subs(t, 0) == 1/log(2) + + negative_binomial_mgf = moment_generating_function( + NegativeBinomial('n', 5, Rational(1, 3)))(t) + assert negative_binomial_mgf.diff(t).subs(t, 0) == Rational(10, 1) + + poisson_mgf = moment_generating_function(Poisson('p', 5))(t) + assert poisson_mgf.diff(t).subs(t, 0) == 5 + + skellam_mgf = moment_generating_function(Skellam('s', 1, 1))(t) + assert skellam_mgf.diff(t).subs( + t, 2) == (-exp(-2) + exp(2))*exp(-2 + exp(-2) + exp(2)) + + yule_simon_mgf = moment_generating_function(YuleSimon('y', 3))(t) + assert simplify(yule_simon_mgf.diff(t).subs(t, 0)) == Rational(3, 2) + + zeta_mgf = moment_generating_function(Zeta('z', 5))(t) + assert zeta_mgf.diff(t).subs(t, 0) == pi**4/(90*zeta(5)) + + +def test_Or(): + X = Geometric('X', S.Half) + assert P(Or(X < 3, X > 4)) == Rational(13, 16) + assert P(Or(X > 2, X > 1)) == P(X > 1) + assert P(Or(X >= 3, X < 3)) == 1 + + +def test_where(): + X = Geometric('X', Rational(1, 5)) + Y = Poisson('Y', 4) + assert where(X**2 > 4).set == Range(3, S.Infinity, 1) + assert where(X**2 >= 4).set == Range(2, S.Infinity, 1) + assert where(Y**2 < 9).set == Range(0, 3, 1) + assert where(Y**2 <= 9).set == Range(0, 4, 1) + + +def test_conditional(): + X = Geometric('X', Rational(2, 3)) + Y = Poisson('Y', 3) + assert P(X > 2, X > 3) == 1 + assert P(X > 3, X > 2) == Rational(1, 3) + assert P(Y > 2, Y < 2) == 0 + assert P(Eq(Y, 3), Y >= 0) == 9*exp(-3)/2 + assert P(Eq(Y, 3), Eq(Y, 2)) == 0 + assert P(X < 2, Eq(X, 2)) == 0 + assert P(X > 2, Eq(X, 3)) == 1 + + +def test_product_spaces(): + X1 = Geometric('X1', S.Half) + X2 = Geometric('X2', Rational(1, 3)) + assert str(P(X1 + X2 < 3).rewrite(Sum)) == ( + "Sum(Piecewise((1/(4*2**n), n >= -1), (0, True)), (n, -oo, -1))/3") + assert str(P(X1 + X2 > 3).rewrite(Sum)) == ( + 'Sum(Piecewise((2**(X2 - n - 2)*(2/3)**(X2 - 1)/6, ' + 'X2 - n <= 2), (0, True)), (X2, 1, oo), (n, 1, oo))') + assert P(Eq(X1 + X2, 3)) == Rational(1, 12) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_error_prop.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_error_prop.py new file mode 100644 index 0000000000000000000000000000000000000000..483fb4c36e202d744faeb355606ff9803a516873 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_error_prop.py @@ -0,0 +1,60 @@ +from sympy.core.function import Function +from sympy.core.symbol import symbols +from sympy.functions.elementary.exponential import exp +from sympy.stats.error_prop import variance_prop +from sympy.stats.symbolic_probability import (RandomSymbol, Variance, + Covariance) + + +def test_variance_prop(): + x, y, z = symbols('x y z') + phi, t = consts = symbols('phi t') + a = RandomSymbol(x) + var_x = Variance(a) + var_y = Variance(RandomSymbol(y)) + var_z = Variance(RandomSymbol(z)) + f = Function('f')(x) + cases = { + x + y: var_x + var_y, + a + y: var_x + var_y, + x + y + z: var_x + var_y + var_z, + 2*x: 4*var_x, + x*y: var_x*y**2 + var_y*x**2, + 1/x: var_x/x**4, + x/y: (var_x*y**2 + var_y*x**2)/y**4, + exp(x): var_x*exp(2*x), + exp(2*x): 4*var_x*exp(4*x), + exp(-x*t): t**2*var_x*exp(-2*t*x), + f: Variance(f), + } + for inp, out in cases.items(): + obs = variance_prop(inp, consts=consts) + assert out == obs + +def test_variance_prop_with_covar(): + x, y, z = symbols('x y z') + phi, t = consts = symbols('phi t') + a = RandomSymbol(x) + var_x = Variance(a) + b = RandomSymbol(y) + var_y = Variance(b) + c = RandomSymbol(z) + var_z = Variance(c) + covar_x_y = Covariance(a, b) + covar_x_z = Covariance(a, c) + covar_y_z = Covariance(b, c) + cases = { + x + y: var_x + var_y + 2*covar_x_y, + a + y: var_x + var_y + 2*covar_x_y, + x + y + z: var_x + var_y + var_z + \ + 2*covar_x_y + 2*covar_x_z + 2*covar_y_z, + 2*x: 4*var_x, + x*y: var_x*y**2 + var_y*x**2 + 2*covar_x_y/(x*y), + 1/x: var_x/x**4, + exp(x): var_x*exp(2*x), + exp(2*x): 4*var_x*exp(4*x), + exp(-x*t): t**2*var_x*exp(-2*t*x), + } + for inp, out in cases.items(): + obs = variance_prop(inp, consts=consts, include_covar=True) + assert out == obs diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_finite_rv.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_finite_rv.py new file mode 100644 index 0000000000000000000000000000000000000000..93bf0211a26ecc32d7f18c7e2d8236859857e445 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_finite_rv.py @@ -0,0 +1,509 @@ +from sympy.concrete.summations import Sum +from sympy.core.containers import (Dict, Tuple) +from sympy.core.function import Function +from sympy.core.numbers import (I, Rational, nan) +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, Symbol, symbols) +from sympy.core.sympify import sympify +from sympy.functions.combinatorial.factorials import binomial +from sympy.functions.combinatorial.numbers import harmonic +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import cos +from sympy.functions.special.beta_functions import beta +from sympy.logic.boolalg import (And, Or) +from sympy.polys.polytools import cancel +from sympy.sets.sets import FiniteSet +from sympy.simplify.simplify import simplify +from sympy.matrices import Matrix +from sympy.stats import (DiscreteUniform, Die, Bernoulli, Coin, Binomial, BetaBinomial, + Hypergeometric, Rademacher, IdealSoliton, RobustSoliton, P, E, variance, + covariance, skewness, density, where, FiniteRV, pspace, cdf, + correlation, moment, cmoment, smoment, characteristic_function, + moment_generating_function, quantile, kurtosis, median, coskewness) +from sympy.stats.frv_types import DieDistribution, BinomialDistribution, \ + HypergeometricDistribution +from sympy.stats.rv import Density +from sympy.testing.pytest import raises + + +def BayesTest(A, B): + assert P(A, B) == P(And(A, B)) / P(B) + assert P(A, B) == P(B, A) * P(A) / P(B) + + +def test_discreteuniform(): + # Symbolic + a, b, c, t = symbols('a b c t') + X = DiscreteUniform('X', [a, b, c]) + + assert E(X) == (a + b + c)/3 + assert simplify(variance(X) + - ((a**2 + b**2 + c**2)/3 - (a/3 + b/3 + c/3)**2)) == 0 + assert P(Eq(X, a)) == P(Eq(X, b)) == P(Eq(X, c)) == S('1/3') + + Y = DiscreteUniform('Y', range(-5, 5)) + + # Numeric + assert E(Y) == S('-1/2') + assert variance(Y) == S('33/4') + assert median(Y) == FiniteSet(-1, 0) + + for x in range(-5, 5): + assert P(Eq(Y, x)) == S('1/10') + assert P(Y <= x) == S(x + 6)/10 + assert P(Y >= x) == S(5 - x)/10 + + assert dict(density(Die('D', 6)).items()) == \ + dict(density(DiscreteUniform('U', range(1, 7))).items()) + + assert characteristic_function(X)(t) == exp(I*a*t)/3 + exp(I*b*t)/3 + exp(I*c*t)/3 + assert moment_generating_function(X)(t) == exp(a*t)/3 + exp(b*t)/3 + exp(c*t)/3 + # issue 18611 + raises(ValueError, lambda: DiscreteUniform('Z', [a, a, a, b, b, c])) + +def test_dice(): + # TODO: Make iid method! + X, Y, Z = Die('X', 6), Die('Y', 6), Die('Z', 6) + a, b, t, p = symbols('a b t p') + + assert E(X) == 3 + S.Half + assert variance(X) == Rational(35, 12) + assert E(X + Y) == 7 + assert E(X + X) == 7 + assert E(a*X + b) == a*E(X) + b + assert variance(X + Y) == variance(X) + variance(Y) == cmoment(X + Y, 2) + assert variance(X + X) == 4 * variance(X) == cmoment(X + X, 2) + assert cmoment(X, 0) == 1 + assert cmoment(4*X, 3) == 64*cmoment(X, 3) + assert covariance(X, Y) is S.Zero + assert covariance(X, X + Y) == variance(X) + assert density(Eq(cos(X*S.Pi), 1))[True] == S.Half + assert correlation(X, Y) == 0 + assert correlation(X, Y) == correlation(Y, X) + assert smoment(X + Y, 3) == skewness(X + Y) + assert smoment(X + Y, 4) == kurtosis(X + Y) + assert smoment(X, 0) == 1 + assert P(X > 3) == S.Half + assert P(2*X > 6) == S.Half + assert P(X > Y) == Rational(5, 12) + assert P(Eq(X, Y)) == P(Eq(X, 1)) + + assert E(X, X > 3) == 5 == moment(X, 1, 0, X > 3) + assert E(X, Y > 3) == E(X) == moment(X, 1, 0, Y > 3) + assert E(X + Y, Eq(X, Y)) == E(2*X) + assert moment(X, 0) == 1 + assert moment(5*X, 2) == 25*moment(X, 2) + assert quantile(X)(p) == Piecewise((nan, (p > 1) | (p < 0)),\ + (S.One, p <= Rational(1, 6)), (S(2), p <= Rational(1, 3)), (S(3), p <= S.Half),\ + (S(4), p <= Rational(2, 3)), (S(5), p <= Rational(5, 6)), (S(6), p <= 1)) + + assert P(X > 3, X > 3) is S.One + assert P(X > Y, Eq(Y, 6)) is S.Zero + assert P(Eq(X + Y, 12)) == Rational(1, 36) + assert P(Eq(X + Y, 12), Eq(X, 6)) == Rational(1, 6) + + assert density(X + Y) == density(Y + Z) != density(X + X) + d = density(2*X + Y**Z) + assert d[S(22)] == Rational(1, 108) and d[S(4100)] == Rational(1, 216) and S(3130) not in d + + assert pspace(X).domain.as_boolean() == Or( + *[Eq(X.symbol, i) for i in [1, 2, 3, 4, 5, 6]]) + + assert where(X > 3).set == FiniteSet(4, 5, 6) + + assert characteristic_function(X)(t) == exp(6*I*t)/6 + exp(5*I*t)/6 + exp(4*I*t)/6 + exp(3*I*t)/6 + exp(2*I*t)/6 + exp(I*t)/6 + assert moment_generating_function(X)(t) == exp(6*t)/6 + exp(5*t)/6 + exp(4*t)/6 + exp(3*t)/6 + exp(2*t)/6 + exp(t)/6 + assert median(X) == FiniteSet(3, 4) + D = Die('D', 7) + assert median(D) == FiniteSet(4) + # Bayes test for die + BayesTest(X > 3, X + Y < 5) + BayesTest(Eq(X - Y, Z), Z > Y) + BayesTest(X > 3, X > 2) + + # arg test for die + raises(ValueError, lambda: Die('X', -1)) # issue 8105: negative sides. + raises(ValueError, lambda: Die('X', 0)) + raises(ValueError, lambda: Die('X', 1.5)) # issue 8103: non integer sides. + + # symbolic test for die + n, k = symbols('n, k', positive=True) + D = Die('D', n) + dens = density(D).dict + assert dens == Density(DieDistribution(n)) + assert set(dens.subs(n, 4).doit().keys()) == {1, 2, 3, 4} + assert set(dens.subs(n, 4).doit().values()) == {Rational(1, 4)} + k = Dummy('k', integer=True) + assert E(D).dummy_eq( + Sum(Piecewise((k/n, k <= n), (0, True)), (k, 1, n))) + assert variance(D).subs(n, 6).doit() == Rational(35, 12) + + ki = Dummy('ki') + cumuf = cdf(D)(k) + assert cumuf.dummy_eq( + Sum(Piecewise((1/n, (ki >= 1) & (ki <= n)), (0, True)), (ki, 1, k))) + assert cumuf.subs({n: 6, k: 2}).doit() == Rational(1, 3) + + t = Dummy('t') + cf = characteristic_function(D)(t) + assert cf.dummy_eq( + Sum(Piecewise((exp(ki*I*t)/n, (ki >= 1) & (ki <= n)), (0, True)), (ki, 1, n))) + assert cf.subs(n, 3).doit() == exp(3*I*t)/3 + exp(2*I*t)/3 + exp(I*t)/3 + mgf = moment_generating_function(D)(t) + assert mgf.dummy_eq( + Sum(Piecewise((exp(ki*t)/n, (ki >= 1) & (ki <= n)), (0, True)), (ki, 1, n))) + assert mgf.subs(n, 3).doit() == exp(3*t)/3 + exp(2*t)/3 + exp(t)/3 + +def test_given(): + X = Die('X', 6) + assert density(X, X > 5) == {S(6): S.One} + assert where(X > 2, X > 5).as_boolean() == Eq(X.symbol, 6) + + +def test_domains(): + X, Y = Die('x', 6), Die('y', 6) + x, y = X.symbol, Y.symbol + # Domains + d = where(X > Y) + assert d.condition == (x > y) + d = where(And(X > Y, Y > 3)) + assert d.as_boolean() == Or(And(Eq(x, 5), Eq(y, 4)), And(Eq(x, 6), + Eq(y, 5)), And(Eq(x, 6), Eq(y, 4))) + assert len(d.elements) == 3 + + assert len(pspace(X + Y).domain.elements) == 36 + + Z = Die('x', 4) + + raises(ValueError, lambda: P(X > Z)) # Two domains with same internal symbol + + assert pspace(X + Y).domain.set == FiniteSet(1, 2, 3, 4, 5, 6)**2 + + assert where(X > 3).set == FiniteSet(4, 5, 6) + assert X.pspace.domain.dict == FiniteSet( + *[Dict({X.symbol: i}) for i in range(1, 7)]) + + assert where(X > Y).dict == FiniteSet(*[Dict({X.symbol: i, Y.symbol: j}) + for i in range(1, 7) for j in range(1, 7) if i > j]) + +def test_bernoulli(): + p, a, b, t = symbols('p a b t') + X = Bernoulli('B', p, a, b) + + assert E(X) == a*p + b*(-p + 1) + assert density(X)[a] == p + assert density(X)[b] == 1 - p + assert characteristic_function(X)(t) == p * exp(I * a * t) + (-p + 1) * exp(I * b * t) + assert moment_generating_function(X)(t) == p * exp(a * t) + (-p + 1) * exp(b * t) + + X = Bernoulli('B', p, 1, 0) + z = Symbol("z") + + assert E(X) == p + assert simplify(variance(X)) == p*(1 - p) + assert E(a*X + b) == a*E(X) + b + assert simplify(variance(a*X + b)) == simplify(a**2 * variance(X)) + assert quantile(X)(z) == Piecewise((nan, (z > 1) | (z < 0)), (0, z <= 1 - p), (1, z <= 1)) + Y = Bernoulli('Y', Rational(1, 2)) + assert median(Y) == FiniteSet(0, 1) + Z = Bernoulli('Z', Rational(2, 3)) + assert median(Z) == FiniteSet(1) + raises(ValueError, lambda: Bernoulli('B', 1.5)) + raises(ValueError, lambda: Bernoulli('B', -0.5)) + + #issue 8248 + assert X.pspace.compute_expectation(1) == 1 + + p = Rational(1, 5) + X = Binomial('X', 5, p) + Y = Binomial('Y', 7, 2*p) + Z = Binomial('Z', 9, 3*p) + assert coskewness(Y + Z, X + Y, X + Z).simplify() == 0 + assert coskewness(Y + 2*X + Z, X + 2*Y + Z, X + 2*Z + Y).simplify() == \ + sqrt(1529)*Rational(12, 16819) + assert coskewness(Y + 2*X + Z, X + 2*Y + Z, X + 2*Z + Y, X < 2).simplify() \ + == -sqrt(357451121)*Rational(2812, 4646864573) + +def test_cdf(): + D = Die('D', 6) + o = S.One + + assert cdf( + D) == sympify({1: o/6, 2: o/3, 3: o/2, 4: 2*o/3, 5: 5*o/6, 6: o}) + + +def test_coins(): + C, D = Coin('C'), Coin('D') + H, T = symbols('H, T') + assert P(Eq(C, D)) == S.Half + assert density(Tuple(C, D)) == {(H, H): Rational(1, 4), (H, T): Rational(1, 4), + (T, H): Rational(1, 4), (T, T): Rational(1, 4)} + assert dict(density(C).items()) == {H: S.Half, T: S.Half} + + F = Coin('F', Rational(1, 10)) + assert P(Eq(F, H)) == Rational(1, 10) + + d = pspace(C).domain + + assert d.as_boolean() == Or(Eq(C.symbol, H), Eq(C.symbol, T)) + + raises(ValueError, lambda: P(C > D)) # Can't intelligently compare H to T + +def test_binomial_verify_parameters(): + raises(ValueError, lambda: Binomial('b', .2, .5)) + raises(ValueError, lambda: Binomial('b', 3, 1.5)) + +def test_binomial_numeric(): + nvals = range(5) + pvals = [0, Rational(1, 4), S.Half, Rational(3, 4), 1] + + for n in nvals: + for p in pvals: + X = Binomial('X', n, p) + assert E(X) == n*p + assert variance(X) == n*p*(1 - p) + if n > 0 and 0 < p < 1: + assert skewness(X) == (1 - 2*p)/sqrt(n*p*(1 - p)) + assert kurtosis(X) == 3 + (1 - 6*p*(1 - p))/(n*p*(1 - p)) + for k in range(n + 1): + assert P(Eq(X, k)) == binomial(n, k)*p**k*(1 - p)**(n - k) + +def test_binomial_quantile(): + X = Binomial('X', 50, S.Half) + assert quantile(X)(0.95) == S(31) + assert median(X) == FiniteSet(25) + + X = Binomial('X', 5, S.Half) + p = Symbol("p", positive=True) + assert quantile(X)(p) == Piecewise((nan, p > S.One), (S.Zero, p <= Rational(1, 32)),\ + (S.One, p <= Rational(3, 16)), (S(2), p <= S.Half), (S(3), p <= Rational(13, 16)),\ + (S(4), p <= Rational(31, 32)), (S(5), p <= S.One)) + assert median(X) == FiniteSet(2, 3) + + +def test_binomial_symbolic(): + n = 2 + p = symbols('p', positive=True) + X = Binomial('X', n, p) + t = Symbol('t') + + assert simplify(E(X)) == n*p == simplify(moment(X, 1)) + assert simplify(variance(X)) == n*p*(1 - p) == simplify(cmoment(X, 2)) + assert cancel(skewness(X) - (1 - 2*p)/sqrt(n*p*(1 - p))) == 0 + assert cancel((kurtosis(X)) - (3 + (1 - 6*p*(1 - p))/(n*p*(1 - p)))) == 0 + assert characteristic_function(X)(t) == p ** 2 * exp(2 * I * t) + 2 * p * (-p + 1) * exp(I * t) + (-p + 1) ** 2 + assert moment_generating_function(X)(t) == p ** 2 * exp(2 * t) + 2 * p * (-p + 1) * exp(t) + (-p + 1) ** 2 + + # Test ability to change success/failure winnings + H, T = symbols('H T') + Y = Binomial('Y', n, p, succ=H, fail=T) + assert simplify(E(Y) - (n*(H*p + T*(1 - p)))) == 0 + + # test symbolic dimensions + n = symbols('n') + B = Binomial('B', n, p) + raises(NotImplementedError, lambda: P(B > 2)) + assert density(B).dict == Density(BinomialDistribution(n, p, 1, 0)) + assert set(density(B).dict.subs(n, 4).doit().keys()) == \ + {S.Zero, S.One, S(2), S(3), S(4)} + assert set(density(B).dict.subs(n, 4).doit().values()) == \ + {(1 - p)**4, 4*p*(1 - p)**3, 6*p**2*(1 - p)**2, 4*p**3*(1 - p), p**4} + k = Dummy('k', integer=True) + assert E(B > 2).dummy_eq( + Sum(Piecewise((k*p**k*(1 - p)**(-k + n)*binomial(n, k), (k >= 0) + & (k <= n) & (k > 2)), (0, True)), (k, 0, n))) + +def test_beta_binomial(): + # verify parameters + raises(ValueError, lambda: BetaBinomial('b', .2, 1, 2)) + raises(ValueError, lambda: BetaBinomial('b', 2, -1, 2)) + raises(ValueError, lambda: BetaBinomial('b', 2, 1, -2)) + assert BetaBinomial('b', 2, 1, 1) + + # test numeric values + nvals = range(1,5) + alphavals = [Rational(1, 4), S.Half, Rational(3, 4), 1, 10] + betavals = [Rational(1, 4), S.Half, Rational(3, 4), 1, 10] + + for n in nvals: + for a in alphavals: + for b in betavals: + X = BetaBinomial('X', n, a, b) + assert E(X) == moment(X, 1) + assert variance(X) == cmoment(X, 2) + + # test symbolic + n, a, b = symbols('a b n') + assert BetaBinomial('x', n, a, b) + n = 2 # Because we're using for loops, can't do symbolic n + a, b = symbols('a b', positive=True) + X = BetaBinomial('X', n, a, b) + t = Symbol('t') + + assert E(X).expand() == moment(X, 1).expand() + assert variance(X).expand() == cmoment(X, 2).expand() + assert skewness(X) == smoment(X, 3) + assert characteristic_function(X)(t) == exp(2*I*t)*beta(a + 2, b)/beta(a, b) +\ + 2*exp(I*t)*beta(a + 1, b + 1)/beta(a, b) + beta(a, b + 2)/beta(a, b) + assert moment_generating_function(X)(t) == exp(2*t)*beta(a + 2, b)/beta(a, b) +\ + 2*exp(t)*beta(a + 1, b + 1)/beta(a, b) + beta(a, b + 2)/beta(a, b) + +def test_hypergeometric_numeric(): + for N in range(1, 5): + for m in range(0, N + 1): + for n in range(1, N + 1): + X = Hypergeometric('X', N, m, n) + N, m, n = map(sympify, (N, m, n)) + assert sum(density(X).values()) == 1 + assert E(X) == n * m / N + if N > 1: + assert variance(X) == n*(m/N)*(N - m)/N*(N - n)/(N - 1) + # Only test for skewness when defined + if N > 2 and 0 < m < N and n < N: + assert skewness(X) == simplify((N - 2*m)*sqrt(N - 1)*(N - 2*n) + / (sqrt(n*m*(N - m)*(N - n))*(N - 2))) + +def test_hypergeometric_symbolic(): + N, m, n = symbols('N, m, n') + H = Hypergeometric('H', N, m, n) + dens = density(H).dict + expec = E(H > 2) + assert dens == Density(HypergeometricDistribution(N, m, n)) + assert dens.subs(N, 5).doit() == Density(HypergeometricDistribution(5, m, n)) + assert set(dens.subs({N: 3, m: 2, n: 1}).doit().keys()) == {S.Zero, S.One} + assert set(dens.subs({N: 3, m: 2, n: 1}).doit().values()) == {Rational(1, 3), Rational(2, 3)} + k = Dummy('k', integer=True) + assert expec.dummy_eq( + Sum(Piecewise((k*binomial(m, k)*binomial(N - m, -k + n) + /binomial(N, n), k > 2), (0, True)), (k, 0, n))) + +def test_rademacher(): + X = Rademacher('X') + t = Symbol('t') + + assert E(X) == 0 + assert variance(X) == 1 + assert density(X)[-1] == S.Half + assert density(X)[1] == S.Half + assert characteristic_function(X)(t) == exp(I*t)/2 + exp(-I*t)/2 + assert moment_generating_function(X)(t) == exp(t) / 2 + exp(-t) / 2 + +def test_ideal_soliton(): + raises(ValueError, lambda : IdealSoliton('sol', -12)) + raises(ValueError, lambda : IdealSoliton('sol', 13.2)) + raises(ValueError, lambda : IdealSoliton('sol', 0)) + f = Function('f') + raises(ValueError, lambda : density(IdealSoliton('sol', 10)).pmf(f)) + + k = Symbol('k', integer=True, positive=True) + x = Symbol('x', integer=True, positive=True) + t = Symbol('t') + sol = IdealSoliton('sol', k) + assert density(sol).low == S.One + assert density(sol).high == k + assert density(sol).dict == Density(density(sol)) + assert density(sol).pmf(x) == Piecewise((1/k, Eq(x, 1)), (1/(x*(x - 1)), k >= x), (0, True)) + + k_vals = [5, 20, 50, 100, 1000] + for i in k_vals: + assert E(sol.subs(k, i)) == harmonic(i) == moment(sol.subs(k, i), 1) + assert variance(sol.subs(k, i)) == (i - 1) + harmonic(i) - harmonic(i)**2 == cmoment(sol.subs(k, i),2) + assert skewness(sol.subs(k, i)) == smoment(sol.subs(k, i), 3) + assert kurtosis(sol.subs(k, i)) == smoment(sol.subs(k, i), 4) + + assert exp(I*t)/10 + Sum(exp(I*t*x)/(x*x - x), (x, 2, k)).subs(k, 10).doit() == characteristic_function(sol.subs(k, 10))(t) + assert exp(t)/10 + Sum(exp(t*x)/(x*x - x), (x, 2, k)).subs(k, 10).doit() == moment_generating_function(sol.subs(k, 10))(t) + +def test_robust_soliton(): + raises(ValueError, lambda : RobustSoliton('robSol', -12, 0.1, 0.02)) + raises(ValueError, lambda : RobustSoliton('robSol', 13, 1.89, 0.1)) + raises(ValueError, lambda : RobustSoliton('robSol', 15, 0.6, -2.31)) + f = Function('f') + raises(ValueError, lambda : density(RobustSoliton('robSol', 15, 0.6, 0.1)).pmf(f)) + + k = Symbol('k', integer=True, positive=True) + delta = Symbol('delta', positive=True) + c = Symbol('c', positive=True) + robSol = RobustSoliton('robSol', k, delta, c) + assert density(robSol).low == 1 + assert density(robSol).high == k + + k_vals = [10, 20, 50] + delta_vals = [0.2, 0.4, 0.6] + c_vals = [0.01, 0.03, 0.05] + for x in k_vals: + for y in delta_vals: + for z in c_vals: + assert E(robSol.subs({k: x, delta: y, c: z})) == moment(robSol.subs({k: x, delta: y, c: z}), 1) + assert variance(robSol.subs({k: x, delta: y, c: z})) == cmoment(robSol.subs({k: x, delta: y, c: z}), 2) + assert skewness(robSol.subs({k: x, delta: y, c: z})) == smoment(robSol.subs({k: x, delta: y, c: z}), 3) + assert kurtosis(robSol.subs({k: x, delta: y, c: z})) == smoment(robSol.subs({k: x, delta: y, c: z}), 4) + +def test_FiniteRV(): + F = FiniteRV('F', {1: S.Half, 2: Rational(1, 4), 3: Rational(1, 4)}, check=True) + p = Symbol("p", positive=True) + + assert dict(density(F).items()) == {S.One: S.Half, S(2): Rational(1, 4), S(3): Rational(1, 4)} + assert P(F >= 2) == S.Half + assert quantile(F)(p) == Piecewise((nan, p > S.One), (S.One, p <= S.Half),\ + (S(2), p <= Rational(3, 4)),(S(3), True)) + + assert pspace(F).domain.as_boolean() == Or( + *[Eq(F.symbol, i) for i in [1, 2, 3]]) + + assert F.pspace.domain.set == FiniteSet(1, 2, 3) + raises(ValueError, lambda: FiniteRV('F', {1: S.Half, 2: S.Half, 3: S.Half}, check=True)) + raises(ValueError, lambda: FiniteRV('F', {1: S.Half, 2: Rational(-1, 2), 3: S.One}, check=True)) + raises(ValueError, lambda: FiniteRV('F', {1: S.One, 2: Rational(3, 2), 3: S.Zero,\ + 4: Rational(-1, 2), 5: Rational(-3, 4), 6: Rational(-1, 4)}, check=True)) + + # purposeful invalid pmf but it should not raise since check=False + # see test_drv_types.test_ContinuousRV for explanation + X = FiniteRV('X', {1: 1, 2: 2}) + assert E(X) == 5 + assert P(X <= 2) + P(X > 2) != 1 + +def test_density_call(): + from sympy.abc import p + x = Bernoulli('x', p) + d = density(x) + assert d(0) == 1 - p + assert d(S.Zero) == 1 - p + assert d(5) == 0 + + assert 0 in d + assert 5 not in d + assert d(S.Zero) == d[S.Zero] + + +def test_DieDistribution(): + from sympy.abc import x + X = DieDistribution(6) + assert X.pmf(S.Half) is S.Zero + assert X.pmf(x).subs({x: 1}).doit() == Rational(1, 6) + assert X.pmf(x).subs({x: 7}).doit() == 0 + assert X.pmf(x).subs({x: -1}).doit() == 0 + assert X.pmf(x).subs({x: Rational(1, 3)}).doit() == 0 + raises(ValueError, lambda: X.pmf(Matrix([0, 0]))) + raises(ValueError, lambda: X.pmf(x**2 - 1)) + +def test_FinitePSpace(): + X = Die('X', 6) + space = pspace(X) + assert space.density == DieDistribution(6) + +def test_symbolic_conditions(): + B = Bernoulli('B', Rational(1, 4)) + D = Die('D', 4) + b, n = symbols('b, n') + Y = P(Eq(B, b)) + Z = E(D > n) + assert Y == \ + Piecewise((Rational(1, 4), Eq(b, 1)), (0, True)) + \ + Piecewise((Rational(3, 4), Eq(b, 0)), (0, True)) + assert Z == \ + Piecewise((Rational(1, 4), n < 1), (0, True)) + Piecewise((S.Half, n < 2), (0, True)) + \ + Piecewise((Rational(3, 4), n < 3), (0, True)) + Piecewise((S.One, n < 4), (0, True)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_joint_rv.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_joint_rv.py new file mode 100644 index 0000000000000000000000000000000000000000..057fc313dfbb31826b07fd1315205d22b86a7f96 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_joint_rv.py @@ -0,0 +1,436 @@ +from sympy.concrete.products import Product +from sympy.concrete.summations import Sum +from sympy.core.numbers import (Rational, oo, pi) +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.combinatorial.factorials import (RisingFactorial, factorial) +from sympy.functions.elementary.complexes import polar_lift +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.special.bessel import besselk +from sympy.functions.special.gamma_functions import gamma +from sympy.matrices.dense import eye +from sympy.matrices.expressions.determinant import Determinant +from sympy.sets.fancysets import Range +from sympy.sets.sets import (Interval, ProductSet) +from sympy.simplify.simplify import simplify +from sympy.tensor.indexed import (Indexed, IndexedBase) +from sympy.core.numbers import comp +from sympy.integrals.integrals import integrate +from sympy.matrices import Matrix, MatrixSymbol +from sympy.matrices.expressions.matexpr import MatrixElement +from sympy.stats import density, median, marginal_distribution, Normal, Laplace, E, sample +from sympy.stats.joint_rv_types import (JointRV, MultivariateNormalDistribution, + JointDistributionHandmade, MultivariateT, NormalGamma, + GeneralizedMultivariateLogGammaOmega as GMVLGO, MultivariateBeta, + GeneralizedMultivariateLogGamma as GMVLG, MultivariateEwens, + Multinomial, NegativeMultinomial, MultivariateNormal, + MultivariateLaplace) +from sympy.testing.pytest import raises, XFAIL, skip, slow +from sympy.external import import_module + +from sympy.abc import x, y + + + +def test_Normal(): + m = Normal('A', [1, 2], [[1, 0], [0, 1]]) + A = MultivariateNormal('A', [1, 2], [[1, 0], [0, 1]]) + assert m == A + assert density(m)(1, 2) == 1/(2*pi) + assert m.pspace.distribution.set == ProductSet(S.Reals, S.Reals) + raises (ValueError, lambda:m[2]) + n = Normal('B', [1, 2, 3], [[1, 0, 0], [0, 1, 0], [0, 0, 1]]) + p = Normal('C', Matrix([1, 2]), Matrix([[1, 0], [0, 1]])) + assert density(m)(x, y) == density(p)(x, y) + assert marginal_distribution(n, 0, 1)(1, 2) == 1/(2*pi) + raises(ValueError, lambda: marginal_distribution(m)) + assert integrate(density(m)(x, y), (x, -oo, oo), (y, -oo, oo)).evalf() == 1.0 + N = Normal('N', [1, 2], [[x, 0], [0, y]]) + assert density(N)(0, 0) == exp(-((4*x + y)/(2*x*y)))/(2*pi*sqrt(x*y)) + + raises (ValueError, lambda: Normal('M', [1, 2], [[1, 1], [1, -1]])) + # symbolic + n = symbols('n', integer=True, positive=True) + mu = MatrixSymbol('mu', n, 1) + sigma = MatrixSymbol('sigma', n, n) + X = Normal('X', mu, sigma) + assert density(X) == MultivariateNormalDistribution(mu, sigma) + raises (NotImplementedError, lambda: median(m)) + # Below tests should work after issue #17267 is resolved + # assert E(X) == mu + # assert variance(X) == sigma + + # test symbolic multivariate normal densities + n = 3 + + Sg = MatrixSymbol('Sg', n, n) + mu = MatrixSymbol('mu', n, 1) + obs = MatrixSymbol('obs', n, 1) + + X = MultivariateNormal('X', mu, Sg) + density_X = density(X) + + eval_a = density_X(obs).subs({Sg: eye(3), + mu: Matrix([0, 0, 0]), obs: Matrix([0, 0, 0])}).doit() + eval_b = density_X(0, 0, 0).subs({Sg: eye(3), mu: Matrix([0, 0, 0])}).doit() + + assert eval_a == sqrt(2)/(4*pi**Rational(3/2)) + assert eval_b == sqrt(2)/(4*pi**Rational(3/2)) + + n = symbols('n', integer=True, positive=True) + + Sg = MatrixSymbol('Sg', n, n) + mu = MatrixSymbol('mu', n, 1) + obs = MatrixSymbol('obs', n, 1) + + X = MultivariateNormal('X', mu, Sg) + density_X_at_obs = density(X)(obs) + + expected_density = MatrixElement( + exp((S(1)/2) * (mu.T - obs.T) * Sg**(-1) * (-mu + obs)) / \ + sqrt((2*pi)**n * Determinant(Sg)), 0, 0) + + assert density_X_at_obs == expected_density + + +def test_MultivariateTDist(): + t1 = MultivariateT('T', [0, 0], [[1, 0], [0, 1]], 2) + assert(density(t1))(1, 1) == 1/(8*pi) + assert t1.pspace.distribution.set == ProductSet(S.Reals, S.Reals) + assert integrate(density(t1)(x, y), (x, -oo, oo), \ + (y, -oo, oo)).evalf() == 1.0 + raises(ValueError, lambda: MultivariateT('T', [1, 2], [[1, 1], [1, -1]], 1)) + t2 = MultivariateT('t2', [1, 2], [[x, 0], [0, y]], 1) + assert density(t2)(1, 2) == 1/(2*pi*sqrt(x*y)) + + +def test_multivariate_laplace(): + raises(ValueError, lambda: Laplace('T', [1, 2], [[1, 2], [2, 1]])) + L = Laplace('L', [1, 0], [[1, 0], [0, 1]]) + L2 = MultivariateLaplace('L2', [1, 0], [[1, 0], [0, 1]]) + assert density(L)(2, 3) == exp(2)*besselk(0, sqrt(39))/pi + L1 = Laplace('L1', [1, 2], [[x, 0], [0, y]]) + assert density(L1)(0, 1) == \ + exp(2/y)*besselk(0, sqrt((2 + 4/y + 1/x)/y))/(pi*sqrt(x*y)) + assert L.pspace.distribution.set == ProductSet(S.Reals, S.Reals) + assert L.pspace.distribution == L2.pspace.distribution + + +def test_NormalGamma(): + ng = NormalGamma('G', 1, 2, 3, 4) + assert density(ng)(1, 1) == 32*exp(-4)/sqrt(pi) + assert ng.pspace.distribution.set == ProductSet(S.Reals, Interval(0, oo)) + raises(ValueError, lambda:NormalGamma('G', 1, 2, 3, -1)) + assert marginal_distribution(ng, 0)(1) == \ + 3*sqrt(10)*gamma(Rational(7, 4))/(10*sqrt(pi)*gamma(Rational(5, 4))) + assert marginal_distribution(ng, y)(1) == exp(Rational(-1, 4))/128 + assert marginal_distribution(ng,[0,1])(x) == x**2*exp(-x/4)/128 + + +def test_GeneralizedMultivariateLogGammaDistribution(): + h = S.Half + omega = Matrix([[1, h, h, h], + [h, 1, h, h], + [h, h, 1, h], + [h, h, h, 1]]) + v, l, mu = (4, [1, 2, 3, 4], [1, 2, 3, 4]) + y_1, y_2, y_3, y_4 = symbols('y_1:5', real=True) + delta = symbols('d', positive=True) + G = GMVLGO('G', omega, v, l, mu) + Gd = GMVLG('Gd', delta, v, l, mu) + dend = ("d**4*Sum(4*24**(-n - 4)*(1 - d)**n*exp((n + 4)*(y_1 + 2*y_2 + 3*y_3 " + "+ 4*y_4) - exp(y_1) - exp(2*y_2)/2 - exp(3*y_3)/3 - exp(4*y_4)/4)/" + "(gamma(n + 1)*gamma(n + 4)**3), (n, 0, oo))") + assert str(density(Gd)(y_1, y_2, y_3, y_4)) == dend + den = ("5*2**(2/3)*5**(1/3)*Sum(4*24**(-n - 4)*(-2**(2/3)*5**(1/3)/4 + 1)**n*" + "exp((n + 4)*(y_1 + 2*y_2 + 3*y_3 + 4*y_4) - exp(y_1) - exp(2*y_2)/2 - " + "exp(3*y_3)/3 - exp(4*y_4)/4)/(gamma(n + 1)*gamma(n + 4)**3), (n, 0, oo))/64") + assert str(density(G)(y_1, y_2, y_3, y_4)) == den + marg = ("5*2**(2/3)*5**(1/3)*exp(4*y_1)*exp(-exp(y_1))*Integral(exp(-exp(4*G[3])" + "/4)*exp(16*G[3])*Integral(exp(-exp(3*G[2])/3)*exp(12*G[2])*Integral(exp(" + "-exp(2*G[1])/2)*exp(8*G[1])*Sum((-1/4)**n*(-4 + 2**(2/3)*5**(1/3" + "))**n*exp(n*y_1)*exp(2*n*G[1])*exp(3*n*G[2])*exp(4*n*G[3])/(24**n*gamma(n + 1)" + "*gamma(n + 4)**3), (n, 0, oo)), (G[1], -oo, oo)), (G[2], -oo, oo)), (G[3]" + ", -oo, oo))/5308416") + assert str(marginal_distribution(G, G[0])(y_1)) == marg + omega_f1 = Matrix([[1, h, h]]) + omega_f2 = Matrix([[1, h, h, h], + [h, 1, 2, h], + [h, h, 1, h], + [h, h, h, 1]]) + omega_f3 = Matrix([[6, h, h, h], + [h, 1, 2, h], + [h, h, 1, h], + [h, h, h, 1]]) + v_f = symbols("v_f", positive=False, real=True) + l_f = [1, 2, v_f, 4] + m_f = [v_f, 2, 3, 4] + omega_f4 = Matrix([[1, h, h, h, h], + [h, 1, h, h, h], + [h, h, 1, h, h], + [h, h, h, 1, h], + [h, h, h, h, 1]]) + l_f1 = [1, 2, 3, 4, 5] + omega_f5 = Matrix([[1]]) + mu_f5 = l_f5 = [1] + + raises(ValueError, lambda: GMVLGO('G', omega_f1, v, l, mu)) + raises(ValueError, lambda: GMVLGO('G', omega_f2, v, l, mu)) + raises(ValueError, lambda: GMVLGO('G', omega_f3, v, l, mu)) + raises(ValueError, lambda: GMVLGO('G', omega, v_f, l, mu)) + raises(ValueError, lambda: GMVLGO('G', omega, v, l_f, mu)) + raises(ValueError, lambda: GMVLGO('G', omega, v, l, m_f)) + raises(ValueError, lambda: GMVLGO('G', omega_f4, v, l, mu)) + raises(ValueError, lambda: GMVLGO('G', omega, v, l_f1, mu)) + raises(ValueError, lambda: GMVLGO('G', omega_f5, v, l_f5, mu_f5)) + raises(ValueError, lambda: GMVLG('G', Rational(3, 2), v, l, mu)) + + +def test_MultivariateBeta(): + a1, a2 = symbols('a1, a2', positive=True) + a1_f, a2_f = symbols('a1, a2', positive=False, real=True) + mb = MultivariateBeta('B', [a1, a2]) + mb_c = MultivariateBeta('C', a1, a2) + assert density(mb)(1, 2) == S(2)**(a2 - 1)*gamma(a1 + a2)/\ + (gamma(a1)*gamma(a2)) + assert marginal_distribution(mb_c, 0)(3) == S(3)**(a1 - 1)*gamma(a1 + a2)/\ + (a2*gamma(a1)*gamma(a2)) + raises(ValueError, lambda: MultivariateBeta('b1', [a1_f, a2])) + raises(ValueError, lambda: MultivariateBeta('b2', [a1, a2_f])) + raises(ValueError, lambda: MultivariateBeta('b3', [0, 0])) + raises(ValueError, lambda: MultivariateBeta('b4', [a1_f, a2_f])) + assert mb.pspace.distribution.set == ProductSet(Interval(0, 1), Interval(0, 1)) + + +def test_MultivariateEwens(): + n, theta, i = symbols('n theta i', positive=True) + + # tests for integer dimensions + theta_f = symbols('t_f', negative=True) + a = symbols('a_1:4', positive = True, integer = True) + ed = MultivariateEwens('E', 3, theta) + assert density(ed)(a[0], a[1], a[2]) == Piecewise((6*2**(-a[1])*3**(-a[2])* + theta**a[0]*theta**a[1]*theta**a[2]/ + (theta*(theta + 1)*(theta + 2)* + factorial(a[0])*factorial(a[1])* + factorial(a[2])), Eq(a[0] + 2*a[1] + + 3*a[2], 3)), (0, True)) + assert marginal_distribution(ed, ed[1])(a[1]) == Piecewise((6*2**(-a[1])* + theta**a[1]/((theta + 1)* + (theta + 2)*factorial(a[1])), + Eq(2*a[1] + 1, 3)), (0, True)) + raises(ValueError, lambda: MultivariateEwens('e1', 5, theta_f)) + assert ed.pspace.distribution.set == ProductSet(Range(0, 4, 1), + Range(0, 2, 1), Range(0, 2, 1)) + + # tests for symbolic dimensions + eds = MultivariateEwens('E', n, theta) + a = IndexedBase('a') + j, k = symbols('j, k') + den = Piecewise((factorial(n)*Product(theta**a[j]*(j + 1)**(-a[j])/ + factorial(a[j]), (j, 0, n - 1))/RisingFactorial(theta, n), + Eq(n, Sum((k + 1)*a[k], (k, 0, n - 1)))), (0, True)) + assert density(eds)(a).dummy_eq(den) + + +def test_Multinomial(): + n, x1, x2, x3, x4 = symbols('n, x1, x2, x3, x4', nonnegative=True, integer=True) + p1, p2, p3, p4 = symbols('p1, p2, p3, p4', positive=True) + p1_f, n_f = symbols('p1_f, n_f', negative=True) + M = Multinomial('M', n, [p1, p2, p3, p4]) + C = Multinomial('C', 3, p1, p2, p3) + f = factorial + assert density(M)(x1, x2, x3, x4) == Piecewise((p1**x1*p2**x2*p3**x3*p4**x4* + f(n)/(f(x1)*f(x2)*f(x3)*f(x4)), + Eq(n, x1 + x2 + x3 + x4)), (0, True)) + assert marginal_distribution(C, C[0])(x1).subs(x1, 1) ==\ + 3*p1*p2**2 +\ + 6*p1*p2*p3 +\ + 3*p1*p3**2 + raises(ValueError, lambda: Multinomial('b1', 5, [p1, p2, p3, p1_f])) + raises(ValueError, lambda: Multinomial('b2', n_f, [p1, p2, p3, p4])) + raises(ValueError, lambda: Multinomial('b3', n, 0.5, 0.4, 0.3, 0.1)) + + +def test_NegativeMultinomial(): + k0, x1, x2, x3, x4 = symbols('k0, x1, x2, x3, x4', nonnegative=True, integer=True) + p1, p2, p3, p4 = symbols('p1, p2, p3, p4', positive=True) + p1_f = symbols('p1_f', negative=True) + N = NegativeMultinomial('N', 4, [p1, p2, p3, p4]) + C = NegativeMultinomial('C', 4, 0.1, 0.2, 0.3) + g = gamma + f = factorial + assert simplify(density(N)(x1, x2, x3, x4) - + p1**x1*p2**x2*p3**x3*p4**x4*(-p1 - p2 - p3 - p4 + 1)**4*g(x1 + x2 + + x3 + x4 + 4)/(6*f(x1)*f(x2)*f(x3)*f(x4))) is S.Zero + assert comp(marginal_distribution(C, C[0])(1).evalf(), 0.33, .01) + raises(ValueError, lambda: NegativeMultinomial('b1', 5, [p1, p2, p3, p1_f])) + raises(ValueError, lambda: NegativeMultinomial('b2', k0, 0.5, 0.4, 0.3, 0.4)) + assert N.pspace.distribution.set == ProductSet(Range(0, oo, 1), + Range(0, oo, 1), Range(0, oo, 1), Range(0, oo, 1)) + + +@slow +def test_JointPSpace_marginal_distribution(): + T = MultivariateT('T', [0, 0], [[1, 0], [0, 1]], 2) + got = marginal_distribution(T, T[1])(x) + ans = sqrt(2)*(x**2/2 + 1)/(4*polar_lift(x**2/2 + 1)**(S(5)/2)) + assert got == ans, got + assert integrate(marginal_distribution(T, 1)(x), (x, -oo, oo)) == 1 + + t = MultivariateT('T', [0, 0, 0], [[1, 0, 0], [0, 1, 0], [0, 0, 1]], 3) + assert comp(marginal_distribution(t, 0)(1).evalf(), 0.2, .01) + + +def test_JointRV(): + x1, x2 = (Indexed('x', i) for i in (1, 2)) + pdf = exp(-x1**2/2 + x1 - x2**2/2 - S.Half)/(2*pi) + X = JointRV('x', pdf) + assert density(X)(1, 2) == exp(-2)/(2*pi) + assert isinstance(X.pspace.distribution, JointDistributionHandmade) + assert marginal_distribution(X, 0)(2) == sqrt(2)*exp(Rational(-1, 2))/(2*sqrt(pi)) + + +def test_expectation(): + m = Normal('A', [x, y], [[1, 0], [0, 1]]) + assert simplify(E(m[1])) == y + + +@XFAIL +def test_joint_vector_expectation(): + m = Normal('A', [x, y], [[1, 0], [0, 1]]) + assert E(m) == (x, y) + + +def test_sample_numpy(): + distribs_numpy = [ + MultivariateNormal("M", [3, 4], [[2, 1], [1, 2]]), + MultivariateBeta("B", [0.4, 5, 15, 50, 203]), + Multinomial("N", 50, [0.3, 0.2, 0.1, 0.25, 0.15]) + ] + size = 3 + numpy = import_module('numpy') + if not numpy: + skip('Numpy is not installed. Abort tests for _sample_numpy.') + else: + for X in distribs_numpy: + samps = sample(X, size=size, library='numpy') + for sam in samps: + assert tuple(sam) in X.pspace.distribution.set + N_c = NegativeMultinomial('N', 3, 0.1, 0.1, 0.1) + raises(NotImplementedError, lambda: sample(N_c, library='numpy')) + + +def test_sample_scipy(): + distribs_scipy = [ + MultivariateNormal("M", [0, 0], [[0.1, 0.025], [0.025, 0.1]]), + MultivariateBeta("B", [0.4, 5, 15]), + Multinomial("N", 8, [0.3, 0.2, 0.1, 0.4]) + ] + + size = 3 + scipy = import_module('scipy') + if not scipy: + skip('Scipy not installed. Abort tests for _sample_scipy.') + else: + for X in distribs_scipy: + samps = sample(X, size=size) + samps2 = sample(X, size=(2, 2)) + for sam in samps: + assert tuple(sam) in X.pspace.distribution.set + for i in range(2): + for j in range(2): + assert tuple(samps2[i][j]) in X.pspace.distribution.set + N_c = NegativeMultinomial('N', 3, 0.1, 0.1, 0.1) + raises(NotImplementedError, lambda: sample(N_c)) + + +def test_sample_pymc(): + distribs_pymc = [ + MultivariateNormal("M", [5, 2], [[1, 0], [0, 1]]), + MultivariateBeta("B", [0.4, 5, 15]), + Multinomial("N", 4, [0.3, 0.2, 0.1, 0.4]) + ] + size = 3 + pymc = import_module('pymc') + if not pymc: + skip('PyMC is not installed. Abort tests for _sample_pymc.') + else: + for X in distribs_pymc: + samps = sample(X, size=size, library='pymc') + for sam in samps: + assert tuple(sam.flatten()) in X.pspace.distribution.set + N_c = NegativeMultinomial('N', 3, 0.1, 0.1, 0.1) + raises(NotImplementedError, lambda: sample(N_c, library='pymc')) + + +def test_sample_seed(): + x1, x2 = (Indexed('x', i) for i in (1, 2)) + pdf = exp(-x1**2/2 + x1 - x2**2/2 - S.Half)/(2*pi) + X = JointRV('x', pdf) + + libraries = ['scipy', 'numpy', 'pymc'] + for lib in libraries: + try: + imported_lib = import_module(lib) + if imported_lib: + s0, s1, s2 = [], [], [] + s0 = sample(X, size=10, library=lib, seed=0) + s1 = sample(X, size=10, library=lib, seed=0) + s2 = sample(X, size=10, library=lib, seed=1) + assert all(s0 == s1) + assert all(s1 != s2) + except NotImplementedError: + continue + +# +# XXX: This fails for pymc. Previously the test appeared to pass but that is +# just because the library argument was not passed so the test always used +# scipy. +# +def test_issue_21057(): + m = Normal("x", [0, 0], [[0, 0], [0, 0]]) + n = MultivariateNormal("x", [0, 0], [[0, 0], [0, 0]]) + p = Normal("x", [0, 0], [[0, 0], [0, 1]]) + assert m == n + libraries = ('scipy', 'numpy') # , 'pymc') # <-- pymc fails + for library in libraries: + try: + imported_lib = import_module(library) + if imported_lib: + s1 = sample(m, size=8, library=library) + s2 = sample(n, size=8, library=library) + s3 = sample(p, size=8, library=library) + assert tuple(s1.flatten()) == tuple(s2.flatten()) + for s in s3: + assert tuple(s.flatten()) in p.pspace.distribution.set + except NotImplementedError: + continue + + +# +# When this passes the pymc part can be uncommented in test_issue_21057 above +# and this can be deleted. +# +@XFAIL +def test_issue_21057_pymc(): + m = Normal("x", [0, 0], [[0, 0], [0, 0]]) + n = MultivariateNormal("x", [0, 0], [[0, 0], [0, 0]]) + p = Normal("x", [0, 0], [[0, 0], [0, 1]]) + assert m == n + libraries = ('pymc',) + for library in libraries: + try: + imported_lib = import_module(library) + if imported_lib: + s1 = sample(m, size=8, library=library) + s2 = sample(n, size=8, library=library) + s3 = sample(p, size=8, library=library) + assert tuple(s1.flatten()) == tuple(s2.flatten()) + for s in s3: + assert tuple(s.flatten()) in p.pspace.distribution.set + except NotImplementedError: + continue diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_matrix_distributions.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_matrix_distributions.py new file mode 100644 index 0000000000000000000000000000000000000000..a2a2dcdd853793d9f77e1a88adf63158ed68e3ba --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_matrix_distributions.py @@ -0,0 +1,186 @@ +from sympy.concrete.products import Product +from sympy.core.numbers import pi +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, symbols) +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.special.gamma_functions import gamma +from sympy.matrices import Determinant, Matrix, Trace, MatrixSymbol, MatrixSet +from sympy.stats import density, sample +from sympy.stats.matrix_distributions import (MatrixGammaDistribution, + MatrixGamma, MatrixPSpace, Wishart, MatrixNormal, MatrixStudentT) +from sympy.testing.pytest import raises, skip +from sympy.external import import_module + + +def test_MatrixPSpace(): + M = MatrixGammaDistribution(1, 2, [[2, 1], [1, 2]]) + MP = MatrixPSpace('M', M, 2, 2) + assert MP.distribution == M + raises(ValueError, lambda: MatrixPSpace('M', M, 1.2, 2)) + +def test_MatrixGamma(): + M = MatrixGamma('M', 1, 2, [[1, 0], [0, 1]]) + assert M.pspace.distribution.set == MatrixSet(2, 2, S.Reals) + assert isinstance(density(M), MatrixGammaDistribution) + X = MatrixSymbol('X', 2, 2) + num = exp(Trace(Matrix([[-S(1)/2, 0], [0, -S(1)/2]])*X)) + assert density(M)(X).doit() == num/(4*pi*sqrt(Determinant(X))) + assert density(M)([[2, 1], [1, 2]]).doit() == sqrt(3)*exp(-2)/(12*pi) + X = MatrixSymbol('X', 1, 2) + Y = MatrixSymbol('Y', 1, 2) + assert density(M)([X, Y]).doit() == exp(-X[0, 0]/2 - Y[0, 1]/2)/(4*pi*sqrt( + X[0, 0]*Y[0, 1] - X[0, 1]*Y[0, 0])) + # symbolic + a, b = symbols('a b', positive=True) + d = symbols('d', positive=True, integer=True) + Y = MatrixSymbol('Y', d, d) + Z = MatrixSymbol('Z', 2, 2) + SM = MatrixSymbol('SM', d, d) + M2 = MatrixGamma('M2', a, b, SM) + M3 = MatrixGamma('M3', 2, 3, [[2, 1], [1, 2]]) + k = Dummy('k') + exprd = pi**(-d*(d - 1)/4)*b**(-a*d)*exp(Trace((-1/b)*SM**(-1)*Y) + )*Determinant(SM)**(-a)*Determinant(Y)**(a - d/2 - S(1)/2)/Product( + gamma(-k/2 + a + S(1)/2), (k, 1, d)) + assert density(M2)(Y).dummy_eq(exprd) + raises(NotImplementedError, lambda: density(M3 + M)(Z)) + raises(ValueError, lambda: density(M)(1)) + raises(ValueError, lambda: MatrixGamma('M', -1, 2, [[1, 0], [0, 1]])) + raises(ValueError, lambda: MatrixGamma('M', -1, -2, [[1, 0], [0, 1]])) + raises(ValueError, lambda: MatrixGamma('M', -1, 2, [[1, 0], [2, 1]])) + raises(ValueError, lambda: MatrixGamma('M', -1, 2, [[1, 0], [0]])) + +def test_Wishart(): + W = Wishart('W', 5, [[1, 0], [0, 1]]) + assert W.pspace.distribution.set == MatrixSet(2, 2, S.Reals) + X = MatrixSymbol('X', 2, 2) + term1 = exp(Trace(Matrix([[-S(1)/2, 0], [0, -S(1)/2]])*X)) + assert density(W)(X).doit() == term1 * Determinant(X)/(24*pi) + assert density(W)([[2, 1], [1, 2]]).doit() == exp(-2)/(8*pi) + n = symbols('n', positive=True) + d = symbols('d', positive=True, integer=True) + Y = MatrixSymbol('Y', d, d) + SM = MatrixSymbol('SM', d, d) + W = Wishart('W', n, SM) + k = Dummy('k') + exprd = 2**(-d*n/2)*pi**(-d*(d - 1)/4)*exp(Trace(-(S(1)/2)*SM**(-1)*Y) + )*Determinant(SM)**(-n/2)*Determinant(Y)**( + -d/2 + n/2 - S(1)/2)/Product(gamma(-k/2 + n/2 + S(1)/2), (k, 1, d)) + assert density(W)(Y).dummy_eq(exprd) + raises(ValueError, lambda: density(W)(1)) + raises(ValueError, lambda: Wishart('W', -1, [[1, 0], [0, 1]])) + raises(ValueError, lambda: Wishart('W', -1, [[1, 0], [2, 1]])) + raises(ValueError, lambda: Wishart('W', 2, [[1, 0], [0]])) + +def test_MatrixNormal(): + M = MatrixNormal('M', [[5, 6]], [4], [[2, 1], [1, 2]]) + assert M.pspace.distribution.set == MatrixSet(1, 2, S.Reals) + X = MatrixSymbol('X', 1, 2) + term1 = exp(-Trace(Matrix([[ S(2)/3, -S(1)/3], [-S(1)/3, S(2)/3]])*( + Matrix([[-5], [-6]]) + X.T)*Matrix([[S(1)/4]])*(Matrix([[-5, -6]]) + X))/2) + assert density(M)(X).doit() == (sqrt(3)) * term1/(24*pi) + assert density(M)([[7, 8]]).doit() == sqrt(3)*exp(-S(1)/3)/(24*pi) + d, n = symbols('d n', positive=True, integer=True) + SM2 = MatrixSymbol('SM2', d, d) + SM1 = MatrixSymbol('SM1', n, n) + LM = MatrixSymbol('LM', n, d) + Y = MatrixSymbol('Y', n, d) + M = MatrixNormal('M', LM, SM1, SM2) + exprd = (2*pi)**(-d*n/2)*exp(-Trace(SM2**(-1)*(-LM.T + Y.T)*SM1**(-1)*(-LM + Y) + )/2)*Determinant(SM1)**(-d/2)*Determinant(SM2)**(-n/2) + assert density(M)(Y).doit() == exprd + raises(ValueError, lambda: density(M)(1)) + raises(ValueError, lambda: MatrixNormal('M', [1, 2], [[1, 0], [0, 1]], [[1, 0], [2, 1]])) + raises(ValueError, lambda: MatrixNormal('M', [1, 2], [[1, 0], [2, 1]], [[1, 0], [0, 1]])) + raises(ValueError, lambda: MatrixNormal('M', [1, 2], [[1, 0], [0, 1]], [[1, 0], [0, 1]])) + raises(ValueError, lambda: MatrixNormal('M', [1, 2], [[1, 0], [2]], [[1, 0], [0, 1]])) + raises(ValueError, lambda: MatrixNormal('M', [1, 2], [[1, 0], [2, 1]], [[1, 0], [0]])) + raises(ValueError, lambda: MatrixNormal('M', [[1, 2]], [[1, 0], [0, 1]], [[1, 0]])) + raises(ValueError, lambda: MatrixNormal('M', [[1, 2]], [1], [[1, 0]])) + +def test_MatrixStudentT(): + M = MatrixStudentT('M', 2, [[5, 6]], [[2, 1], [1, 2]], [4]) + assert M.pspace.distribution.set == MatrixSet(1, 2, S.Reals) + X = MatrixSymbol('X', 1, 2) + D = pi ** (-1.0) * Determinant(Matrix([[4]])) ** (-1.0) * Determinant(Matrix([[2, 1], [1, 2]])) \ + ** (-0.5) / Determinant(Matrix([[S(1) / 4]]) * (Matrix([[-5, -6]]) + X) + * Matrix([[S(2) / 3, -S(1) / 3], [-S(1) / 3, S(2) / 3]]) * ( + Matrix([[-5], [-6]]) + X.T) + Matrix([[1]])) ** 2 + assert density(M)(X) == D + + v = symbols('v', positive=True) + n, p = 1, 2 + Omega = MatrixSymbol('Omega', p, p) + Sigma = MatrixSymbol('Sigma', n, n) + Location = MatrixSymbol('Location', n, p) + Y = MatrixSymbol('Y', n, p) + M = MatrixStudentT('M', v, Location, Omega, Sigma) + + exprd = gamma(v/2 + 1)*Determinant(Matrix([[1]]) + Sigma**(-1)*(-Location + Y)*Omega**(-1)*(-Location.T + Y.T))**(-v/2 - 1) / \ + (pi*gamma(v/2)*sqrt(Determinant(Omega))*Determinant(Sigma)) + + assert density(M)(Y) == exprd + raises(ValueError, lambda: density(M)(1)) + raises(ValueError, lambda: MatrixStudentT('M', 1, [1, 2], [[1, 0], [0, 1]], [[1, 0], [2, 1]])) + raises(ValueError, lambda: MatrixStudentT('M', 1, [1, 2], [[1, 0], [2, 1]], [[1, 0], [0, 1]])) + raises(ValueError, lambda: MatrixStudentT('M', 1, [1, 2], [[1, 0], [0, 1]], [[1, 0], [0, 1]])) + raises(ValueError, lambda: MatrixStudentT('M', 1, [1, 2], [[1, 0], [2]], [[1, 0], [0, 1]])) + raises(ValueError, lambda: MatrixStudentT('M', 1, [1, 2], [[1, 0], [2, 1]], [[1], [2]])) + raises(ValueError, lambda: MatrixStudentT('M', 1, [[1, 2]], [[1, 0], [0, 1]], [[1, 0]])) + raises(ValueError, lambda: MatrixStudentT('M', 1, [[1, 2]], [1], [[1, 0]])) + raises(ValueError, lambda: MatrixStudentT('M', -1, [1, 2], [[1, 0], [0, 1]], [4])) + +def test_sample_scipy(): + distribs_scipy = [ + MatrixNormal('M', [[5, 6]], [4], [[2, 1], [1, 2]]), + Wishart('W', 5, [[1, 0], [0, 1]]) + ] + + size = 5 + scipy = import_module('scipy') + if not scipy: + skip('Scipy not installed. Abort tests for _sample_scipy.') + else: + for X in distribs_scipy: + samps = sample(X, size=size) + for sam in samps: + assert Matrix(sam) in X.pspace.distribution.set + M = MatrixGamma('M', 1, 2, [[1, 0], [0, 1]]) + raises(NotImplementedError, lambda: sample(M, size=3)) + +def test_sample_pymc(): + distribs_pymc = [ + MatrixNormal('M', [[5, 6], [3, 4]], [[1, 0], [0, 1]], [[2, 1], [1, 2]]), + Wishart('W', 7, [[2, 1], [1, 2]]) + ] + size = 3 + pymc = import_module('pymc') + if not pymc: + skip('PyMC is not installed. Abort tests for _sample_pymc.') + else: + for X in distribs_pymc: + samps = sample(X, size=size, library='pymc') + for sam in samps: + assert Matrix(sam) in X.pspace.distribution.set + M = MatrixGamma('M', 1, 2, [[1, 0], [0, 1]]) + raises(NotImplementedError, lambda: sample(M, size=3)) + +def test_sample_seed(): + X = MatrixNormal('M', [[5, 6], [3, 4]], [[1, 0], [0, 1]], [[2, 1], [1, 2]]) + + libraries = ['scipy', 'numpy', 'pymc'] + for lib in libraries: + try: + imported_lib = import_module(lib) + if imported_lib: + s0, s1, s2 = [], [], [] + s0 = sample(X, size=10, library=lib, seed=0) + s1 = sample(X, size=10, library=lib, seed=0) + s2 = sample(X, size=10, library=lib, seed=1) + for i in range(10): + assert (s0[i] == s1[i]).all() + assert (s1[i] != s2[i]).all() + + except NotImplementedError: + continue diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_mix.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_mix.py new file mode 100644 index 0000000000000000000000000000000000000000..4334d9b144a5ddaad938f195f0276e0e8993aa35 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_mix.py @@ -0,0 +1,82 @@ +from sympy.concrete.summations import Sum +from sympy.core.add import Add +from sympy.core.mul import Mul +from sympy.core.numbers import (Integer, oo, pi) +from sympy.core.power import Pow +from sympy.core.relational import (Eq, Ne) +from sympy.core.symbol import (Dummy, Symbol, symbols) +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.special.delta_functions import DiracDelta +from sympy.functions.special.gamma_functions import gamma +from sympy.integrals.integrals import Integral +from sympy.simplify.simplify import simplify +from sympy.tensor.indexed import (Indexed, IndexedBase) +from sympy.functions.elementary.piecewise import ExprCondPair +from sympy.stats import (Poisson, Beta, Exponential, P, + Multinomial, MultivariateBeta) +from sympy.stats.crv_types import Normal +from sympy.stats.drv_types import PoissonDistribution +from sympy.stats.compound_rv import CompoundPSpace, CompoundDistribution +from sympy.stats.joint_rv import MarginalDistribution +from sympy.stats.rv import pspace, density +from sympy.testing.pytest import ignore_warnings + +def test_density(): + x = Symbol('x') + l = Symbol('l', positive=True) + rate = Beta(l, 2, 3) + X = Poisson(x, rate) + assert isinstance(pspace(X), CompoundPSpace) + assert density(X, Eq(rate, rate.symbol)) == PoissonDistribution(l) + N1 = Normal('N1', 0, 1) + N2 = Normal('N2', N1, 2) + assert density(N2)(0).doit() == sqrt(10)/(10*sqrt(pi)) + assert simplify(density(N2, Eq(N1, 1))(x)) == \ + sqrt(2)*exp(-(x - 1)**2/8)/(4*sqrt(pi)) + assert simplify(density(N2)(x)) == sqrt(10)*exp(-x**2/10)/(10*sqrt(pi)) + +def test_MarginalDistribution(): + a1, p1, p2 = symbols('a1 p1 p2', positive=True) + C = Multinomial('C', 2, p1, p2) + B = MultivariateBeta('B', a1, C[0]) + MGR = MarginalDistribution(B, (C[0],)) + mgrc = Mul(Symbol('B'), Piecewise(ExprCondPair(Mul(Integer(2), + Pow(Symbol('p1', positive=True), Indexed(IndexedBase(Symbol('C')), + Integer(0))), Pow(Symbol('p2', positive=True), + Indexed(IndexedBase(Symbol('C')), Integer(1))), + Pow(factorial(Indexed(IndexedBase(Symbol('C')), Integer(0))), Integer(-1)), + Pow(factorial(Indexed(IndexedBase(Symbol('C')), Integer(1))), Integer(-1))), + Eq(Add(Indexed(IndexedBase(Symbol('C')), Integer(0)), + Indexed(IndexedBase(Symbol('C')), Integer(1))), Integer(2))), + ExprCondPair(Integer(0), True)), Pow(gamma(Symbol('a1', positive=True)), + Integer(-1)), gamma(Add(Symbol('a1', positive=True), + Indexed(IndexedBase(Symbol('C')), Integer(0)))), + Pow(gamma(Indexed(IndexedBase(Symbol('C')), Integer(0))), Integer(-1)), + Pow(Indexed(IndexedBase(Symbol('B')), Integer(0)), + Add(Symbol('a1', positive=True), Integer(-1))), + Pow(Indexed(IndexedBase(Symbol('B')), Integer(1)), + Add(Indexed(IndexedBase(Symbol('C')), Integer(0)), Integer(-1)))) + assert MGR(C) == mgrc + +def test_compound_distribution(): + Y = Poisson('Y', 1) + Z = Poisson('Z', Y) + assert isinstance(pspace(Z), CompoundPSpace) + assert isinstance(pspace(Z).distribution, CompoundDistribution) + assert Z.pspace.distribution.pdf(1).doit() == exp(-2)*exp(exp(-1)) + +def test_mix_expression(): + Y, E = Poisson('Y', 1), Exponential('E', 1) + k = Dummy('k') + expr1 = Integral(Sum(exp(-1)*Integral(exp(-k)*DiracDelta(k - 2), (k, 0, oo) + )/factorial(k), (k, 0, oo)), (k, -oo, 0)) + expr2 = Integral(Sum(exp(-1)*Integral(exp(-k)*DiracDelta(k - 2), (k, 0, oo) + )/factorial(k), (k, 0, oo)), (k, 0, oo)) + assert P(Eq(Y + E, 1)) == 0 + assert P(Ne(Y + E, 2)) == 1 + with ignore_warnings(UserWarning): ### TODO: Restore tests once warnings are removed + assert P(E + Y < 2, evaluate=False).rewrite(Integral).dummy_eq(expr1) + assert P(E + Y > 2, evaluate=False).rewrite(Integral).dummy_eq(expr2) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_random_matrix.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_random_matrix.py new file mode 100644 index 0000000000000000000000000000000000000000..ba570a16bc42620d53bce19be71e7d125965ede1 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_random_matrix.py @@ -0,0 +1,135 @@ +from sympy.concrete.products import Product +from sympy.core.function import Lambda +from sympy.core.numbers import (I, Rational, pi) +from sympy.core.singleton import S +from sympy.core.symbol import Dummy +from sympy.functions.elementary.complexes import Abs +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.integrals.integrals import Integral +from sympy.matrices.dense import Matrix +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.matrices.expressions.trace import Trace +from sympy.tensor.indexed import IndexedBase +from sympy.stats import (GaussianUnitaryEnsemble as GUE, density, + GaussianOrthogonalEnsemble as GOE, + GaussianSymplecticEnsemble as GSE, + joint_eigen_distribution, + CircularUnitaryEnsemble as CUE, + CircularOrthogonalEnsemble as COE, + CircularSymplecticEnsemble as CSE, + JointEigenDistribution, + level_spacing_distribution, + Normal, Beta) +from sympy.stats.joint_rv_types import JointDistributionHandmade +from sympy.stats.rv import RandomMatrixSymbol +from sympy.stats.random_matrix_models import GaussianEnsemble, RandomMatrixPSpace +from sympy.testing.pytest import raises + +def test_GaussianEnsemble(): + G = GaussianEnsemble('G', 3) + assert density(G) == G.pspace.model + raises(ValueError, lambda: GaussianEnsemble('G', 3.5)) + +def test_GaussianUnitaryEnsemble(): + H = RandomMatrixSymbol('H', 3, 3) + G = GUE('U', 3) + assert density(G)(H) == sqrt(2)*exp(-3*Trace(H**2)/2)/(4*pi**Rational(9, 2)) + i, j = (Dummy('i', integer=True, positive=True), + Dummy('j', integer=True, positive=True)) + l = IndexedBase('l') + assert joint_eigen_distribution(G).dummy_eq( + Lambda((l[1], l[2], l[3]), + 27*sqrt(6)*exp(-3*(l[1]**2)/2 - 3*(l[2]**2)/2 - 3*(l[3]**2)/2)* + Product(Abs(l[i] - l[j])**2, (j, i + 1, 3), (i, 1, 2))/(16*pi**Rational(3, 2)))) + s = Dummy('s') + assert level_spacing_distribution(G).dummy_eq(Lambda(s, 32*s**2*exp(-4*s**2/pi)/pi**2)) + + +def test_GaussianOrthogonalEnsemble(): + H = RandomMatrixSymbol('H', 3, 3) + _H = MatrixSymbol('_H', 3, 3) + G = GOE('O', 3) + assert density(G)(H) == exp(-3*Trace(H**2)/4)/Integral(exp(-3*Trace(_H**2)/4), _H) + i, j = (Dummy('i', integer=True, positive=True), + Dummy('j', integer=True, positive=True)) + l = IndexedBase('l') + assert joint_eigen_distribution(G).dummy_eq( + Lambda((l[1], l[2], l[3]), + 9*sqrt(2)*exp(-3*l[1]**2/2 - 3*l[2]**2/2 - 3*l[3]**2/2)* + Product(Abs(l[i] - l[j]), (j, i + 1, 3), (i, 1, 2))/(32*pi))) + s = Dummy('s') + assert level_spacing_distribution(G).dummy_eq(Lambda(s, s*pi*exp(-s**2*pi/4)/2)) + +def test_GaussianSymplecticEnsemble(): + H = RandomMatrixSymbol('H', 3, 3) + _H = MatrixSymbol('_H', 3, 3) + G = GSE('O', 3) + assert density(G)(H) == exp(-3*Trace(H**2))/Integral(exp(-3*Trace(_H**2)), _H) + i, j = (Dummy('i', integer=True, positive=True), + Dummy('j', integer=True, positive=True)) + l = IndexedBase('l') + assert joint_eigen_distribution(G).dummy_eq( + Lambda((l[1], l[2], l[3]), + 162*sqrt(3)*exp(-3*l[1]**2/2 - 3*l[2]**2/2 - 3*l[3]**2/2)* + Product(Abs(l[i] - l[j])**4, (j, i + 1, 3), (i, 1, 2))/(5*pi**Rational(3, 2)))) + s = Dummy('s') + assert level_spacing_distribution(G).dummy_eq(Lambda(s, S(262144)*s**4*exp(-64*s**2/(9*pi))/(729*pi**3))) + +def test_CircularUnitaryEnsemble(): + CU = CUE('U', 3) + j, k = (Dummy('j', integer=True, positive=True), + Dummy('k', integer=True, positive=True)) + t = IndexedBase('t') + assert joint_eigen_distribution(CU).dummy_eq( + Lambda((t[1], t[2], t[3]), + Product(Abs(exp(I*t[j]) - exp(I*t[k]))**2, + (j, k + 1, 3), (k, 1, 2))/(48*pi**3)) + ) + +def test_CircularOrthogonalEnsemble(): + CO = COE('U', 3) + j, k = (Dummy('j', integer=True, positive=True), + Dummy('k', integer=True, positive=True)) + t = IndexedBase('t') + assert joint_eigen_distribution(CO).dummy_eq( + Lambda((t[1], t[2], t[3]), + Product(Abs(exp(I*t[j]) - exp(I*t[k])), + (j, k + 1, 3), (k, 1, 2))/(48*pi**2)) + ) + +def test_CircularSymplecticEnsemble(): + CS = CSE('U', 3) + j, k = (Dummy('j', integer=True, positive=True), + Dummy('k', integer=True, positive=True)) + t = IndexedBase('t') + assert joint_eigen_distribution(CS).dummy_eq( + Lambda((t[1], t[2], t[3]), + Product(Abs(exp(I*t[j]) - exp(I*t[k]))**4, + (j, k + 1, 3), (k, 1, 2))/(720*pi**3)) + ) + +def test_JointEigenDistribution(): + A = Matrix([[Normal('A00', 0, 1), Normal('A01', 1, 1)], + [Beta('A10', 1, 1), Beta('A11', 1, 1)]]) + assert JointEigenDistribution(A) == \ + JointDistributionHandmade(-sqrt(A[0, 0]**2 - 2*A[0, 0]*A[1, 1] + 4*A[0, 1]*A[1, 0] + A[1, 1]**2)/2 + + A[0, 0]/2 + A[1, 1]/2, sqrt(A[0, 0]**2 - 2*A[0, 0]*A[1, 1] + 4*A[0, 1]*A[1, 0] + A[1, 1]**2)/2 + A[0, 0]/2 + A[1, 1]/2) + raises(ValueError, lambda: JointEigenDistribution(Matrix([[1, 0], [2, 1]]))) + +def test_issue_19841(): + G1 = GUE('U', 2) + G2 = G1.xreplace({2: 2}) + assert G1.args == G2.args + + X = MatrixSymbol('X', 2, 2) + G = GSE('U', 2) + h_pspace = RandomMatrixPSpace('P', model=density(G)) + H = RandomMatrixSymbol('H', 2, 2, pspace=h_pspace) + H2 = RandomMatrixSymbol('H', 2, 2, pspace=None) + assert H.doit() == H + + assert (2*H).xreplace({H: X}) == 2*X + assert (2*H).xreplace({H2: X}) == 2*H + assert (2*H2).xreplace({H: X}) == 2*H2 + assert (2*H2).xreplace({H2: X}) == 2*X diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_rv.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_rv.py new file mode 100644 index 0000000000000000000000000000000000000000..185756300556f2fe70b76c402113ec2bb2501ef4 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_rv.py @@ -0,0 +1,441 @@ +from sympy.concrete.summations import Sum +from sympy.core.basic import Basic +from sympy.core.containers import Tuple +from sympy.core.function import Lambda +from sympy.core.numbers import (Rational, nan, oo, pi) +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.combinatorial.factorials import (FallingFactorial, binomial) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.functions.special.delta_functions import DiracDelta +from sympy.integrals.integrals import integrate +from sympy.logic.boolalg import (And, Or) +from sympy.matrices.dense import Matrix +from sympy.sets.sets import Interval +from sympy.tensor.indexed import Indexed +from sympy.stats import (Die, Normal, Exponential, FiniteRV, P, E, H, variance, + density, given, independent, dependent, where, pspace, GaussianUnitaryEnsemble, + random_symbols, sample, Geometric, factorial_moment, Binomial, Hypergeometric, + DiscreteUniform, Poisson, characteristic_function, moment_generating_function, + BernoulliProcess, Variance, Expectation, Probability, Covariance, covariance, cmoment, + moment, median) +from sympy.stats.rv import (IndependentProductPSpace, rs_swap, Density, NamedArgsMixin, + RandomSymbol, sample_iter, PSpace, is_random, RandomIndexedSymbol, RandomMatrixSymbol) +from sympy.testing.pytest import raises, skip, XFAIL, warns_deprecated_sympy +from sympy.external import import_module +from sympy.core.numbers import comp +from sympy.stats.frv_types import BernoulliDistribution +from sympy.core.symbol import Dummy +from sympy.functions.elementary.piecewise import Piecewise + +def test_where(): + X, Y = Die('X'), Die('Y') + Z = Normal('Z', 0, 1) + + assert where(Z**2 <= 1).set == Interval(-1, 1) + assert where(Z**2 <= 1).as_boolean() == Interval(-1, 1).as_relational(Z.symbol) + assert where(And(X > Y, Y > 4)).as_boolean() == And( + Eq(X.symbol, 6), Eq(Y.symbol, 5)) + + assert len(where(X < 3).set) == 2 + assert 1 in where(X < 3).set + + X, Y = Normal('X', 0, 1), Normal('Y', 0, 1) + assert where(And(X**2 <= 1, X >= 0)).set == Interval(0, 1) + XX = given(X, And(X**2 <= 1, X >= 0)) + assert XX.pspace.domain.set == Interval(0, 1) + assert XX.pspace.domain.as_boolean() == \ + And(0 <= X.symbol, X.symbol**2 <= 1, -oo < X.symbol, X.symbol < oo) + + with raises(TypeError): + XX = given(X, X + 3) + + +def test_random_symbols(): + X, Y = Normal('X', 0, 1), Normal('Y', 0, 1) + + assert set(random_symbols(2*X + 1)) == {X} + assert set(random_symbols(2*X + Y)) == {X, Y} + assert set(random_symbols(2*X + Y.symbol)) == {X} + assert set(random_symbols(2)) == set() + + +def test_characteristic_function(): + # Imports I from sympy + from sympy.core.numbers import I + X = Normal('X',0,1) + Y = DiscreteUniform('Y', [1,2,7]) + Z = Poisson('Z', 2) + t = symbols('_t') + P = Lambda(t, exp(-t**2/2)) + Q = Lambda(t, exp(7*t*I)/3 + exp(2*t*I)/3 + exp(t*I)/3) + R = Lambda(t, exp(2 * exp(t*I) - 2)) + + + assert characteristic_function(X).dummy_eq(P) + assert characteristic_function(Y).dummy_eq(Q) + assert characteristic_function(Z).dummy_eq(R) + + +def test_moment_generating_function(): + + X = Normal('X',0,1) + Y = DiscreteUniform('Y', [1,2,7]) + Z = Poisson('Z', 2) + t = symbols('_t') + P = Lambda(t, exp(t**2/2)) + Q = Lambda(t, (exp(7*t)/3 + exp(2*t)/3 + exp(t)/3)) + R = Lambda(t, exp(2 * exp(t) - 2)) + + + assert moment_generating_function(X).dummy_eq(P) + assert moment_generating_function(Y).dummy_eq(Q) + assert moment_generating_function(Z).dummy_eq(R) + +def test_sample_iter(): + + X = Normal('X',0,1) + Y = DiscreteUniform('Y', [1, 2, 7]) + Z = Poisson('Z', 2) + + scipy = import_module('scipy') + if not scipy: + skip('Scipy is not installed. Abort tests') + expr = X**2 + 3 + iterator = sample_iter(expr) + + expr2 = Y**2 + 5*Y + 4 + iterator2 = sample_iter(expr2) + + expr3 = Z**3 + 4 + iterator3 = sample_iter(expr3) + + def is_iterator(obj): + if ( + hasattr(obj, '__iter__') and + (hasattr(obj, 'next') or + hasattr(obj, '__next__')) and + callable(obj.__iter__) and + obj.__iter__() is obj + ): + return True + else: + return False + assert is_iterator(iterator) + assert is_iterator(iterator2) + assert is_iterator(iterator3) + +def test_pspace(): + X, Y = Normal('X', 0, 1), Normal('Y', 0, 1) + x = Symbol('x') + + raises(ValueError, lambda: pspace(5 + 3)) + raises(ValueError, lambda: pspace(x < 1)) + assert pspace(X) == X.pspace + assert pspace(2*X + 1) == X.pspace + assert pspace(2*X + Y) == IndependentProductPSpace(Y.pspace, X.pspace) + +def test_rs_swap(): + X = Normal('x', 0, 1) + Y = Exponential('y', 1) + + XX = Normal('x', 0, 2) + YY = Normal('y', 0, 3) + + expr = 2*X + Y + assert expr.subs(rs_swap((X, Y), (YY, XX))) == 2*XX + YY + + +def test_RandomSymbol(): + + X = Normal('x', 0, 1) + Y = Normal('x', 0, 2) + assert X.symbol == Y.symbol + assert X != Y + + assert X.name == X.symbol.name + + X = Normal('lambda', 0, 1) # make sure we can use protected terms + X = Normal('Lambda', 0, 1) # make sure we can use SymPy terms + + +def test_RandomSymbol_diff(): + X = Normal('x', 0, 1) + assert (2*X).diff(X) + + +def test_random_symbol_no_pspace(): + x = RandomSymbol(Symbol('x')) + assert x.pspace == PSpace() + +def test_overlap(): + X = Normal('x', 0, 1) + Y = Normal('x', 0, 2) + + raises(ValueError, lambda: P(X > Y)) + + +def test_IndependentProductPSpace(): + X = Normal('X', 0, 1) + Y = Normal('Y', 0, 1) + px = X.pspace + py = Y.pspace + assert pspace(X + Y) == IndependentProductPSpace(px, py) + assert pspace(X + Y) == IndependentProductPSpace(py, px) + + +def test_E(): + assert E(5) == 5 + + +def test_H(): + X = Normal('X', 0, 1) + D = Die('D', sides = 4) + G = Geometric('G', 0.5) + assert H(X, X > 0) == -log(2)/2 + S.Half + log(pi)/2 + assert H(D, D > 2) == log(2) + assert comp(H(G).evalf().round(2), 1.39) + + +def test_Sample(): + X = Die('X', 6) + Y = Normal('Y', 0, 1) + z = Symbol('z', integer=True) + + scipy = import_module('scipy') + if not scipy: + skip('Scipy is not installed. Abort tests') + assert sample(X) in [1, 2, 3, 4, 5, 6] + assert isinstance(sample(X + Y), float) + + assert P(X + Y > 0, Y < 0, numsamples=10).is_number + assert E(X + Y, numsamples=10).is_number + assert E(X**2 + Y, numsamples=10).is_number + assert E((X + Y)**2, numsamples=10).is_number + assert variance(X + Y, numsamples=10).is_number + + raises(TypeError, lambda: P(Y > z, numsamples=5)) + + assert P(sin(Y) <= 1, numsamples=10) == 1.0 + assert P(sin(Y) <= 1, cos(Y) < 1, numsamples=10) == 1.0 + + assert all(i in range(1, 7) for i in density(X, numsamples=10)) + assert all(i in range(4, 7) for i in density(X, X>3, numsamples=10)) + + numpy = import_module('numpy') + if not numpy: + skip('Numpy is not installed. Abort tests') + #Test Issue #21563: Output of sample must be a float or array + assert isinstance(sample(X), (numpy.int32, numpy.int64)) + assert isinstance(sample(Y), numpy.float64) + assert isinstance(sample(X, size=2), numpy.ndarray) + + with warns_deprecated_sympy(): + sample(X, numsamples=2) + +@XFAIL +def test_samplingE(): + scipy = import_module('scipy') + if not scipy: + skip('Scipy is not installed. Abort tests') + Y = Normal('Y', 0, 1) + z = Symbol('z', integer=True) + assert E(Sum(1/z**Y, (z, 1, oo)), Y > 2, numsamples=3).is_number + + +def test_given(): + X = Normal('X', 0, 1) + Y = Normal('Y', 0, 1) + A = given(X, True) + B = given(X, Y > 2) + + assert X == A == B + + +def test_factorial_moment(): + X = Poisson('X', 2) + Y = Binomial('Y', 2, S.Half) + Z = Hypergeometric('Z', 4, 2, 2) + assert factorial_moment(X, 2) == 4 + assert factorial_moment(Y, 2) == S.Half + assert factorial_moment(Z, 2) == Rational(1, 3) + + x, y, z, l = symbols('x y z l') + Y = Binomial('Y', 2, y) + Z = Hypergeometric('Z', 10, 2, 3) + assert factorial_moment(Y, l) == y**2*FallingFactorial( + 2, l) + 2*y*(1 - y)*FallingFactorial(1, l) + (1 - y)**2*\ + FallingFactorial(0, l) + assert factorial_moment(Z, l) == 7*FallingFactorial(0, l)/\ + 15 + 7*FallingFactorial(1, l)/15 + FallingFactorial(2, l)/15 + + +def test_dependence(): + X, Y = Die('X'), Die('Y') + assert independent(X, 2*Y) + assert not dependent(X, 2*Y) + + X, Y = Normal('X', 0, 1), Normal('Y', 0, 1) + assert independent(X, Y) + assert dependent(X, 2*X) + + # Create a dependency + XX, YY = given(Tuple(X, Y), Eq(X + Y, 3)) + assert dependent(XX, YY) + +def test_dependent_finite(): + X, Y = Die('X'), Die('Y') + # Dependence testing requires symbolic conditions which currently break + # finite random variables + assert dependent(X, Y + X) + + XX, YY = given(Tuple(X, Y), X + Y > 5) # Create a dependency + assert dependent(XX, YY) + + +def test_normality(): + X, Y = Normal('X', 0, 1), Normal('Y', 0, 1) + x = Symbol('x', real=True) + z = Symbol('z', real=True) + dens = density(X - Y, Eq(X + Y, z)) + + assert integrate(dens(x), (x, -oo, oo)) == 1 + + +def test_Density(): + X = Die('X', 6) + d = Density(X) + assert d.doit() == density(X) + +def test_NamedArgsMixin(): + class Foo(Basic, NamedArgsMixin): + _argnames = 'foo', 'bar' + + a = Foo(S(1), S(2)) + + assert a.foo == 1 + assert a.bar == 2 + + raises(AttributeError, lambda: a.baz) + + class Bar(Basic, NamedArgsMixin): + pass + + raises(AttributeError, lambda: Bar(S(1), S(2)).foo) + +def test_density_constant(): + assert density(3)(2) == 0 + assert density(3)(3) == DiracDelta(0) + +def test_cmoment_constant(): + assert variance(3) == 0 + assert cmoment(3, 3) == 0 + assert cmoment(3, 4) == 0 + x = Symbol('x') + assert variance(x) == 0 + assert cmoment(x, 15) == 0 + assert cmoment(x, 0) == 1 + +def test_moment_constant(): + assert moment(3, 0) == 1 + assert moment(3, 1) == 3 + assert moment(3, 2) == 9 + x = Symbol('x') + assert moment(x, 2) == x**2 + +def test_median_constant(): + assert median(3) == 3 + x = Symbol('x') + assert median(x) == x + +def test_real(): + x = Normal('x', 0, 1) + assert x.is_real + + +def test_issue_10052(): + X = Exponential('X', 3) + assert P(X < oo) == 1 + assert P(X > oo) == 0 + assert P(X < 2, X > oo) == 0 + assert P(X < oo, X > oo) == 0 + assert P(X < oo, X > 2) == 1 + assert P(X < 3, X == 2) == 0 + raises(ValueError, lambda: P(1)) + raises(ValueError, lambda: P(X < 1, 2)) + +def test_issue_11934(): + density = {0: .5, 1: .5} + X = FiniteRV('X', density) + assert E(X) == 0.5 + assert P( X>= 2) == 0 + +def test_issue_8129(): + X = Exponential('X', 4) + assert P(X >= X) == 1 + assert P(X > X) == 0 + assert P(X > X+1) == 0 + +def test_issue_12237(): + X = Normal('X', 0, 1) + Y = Normal('Y', 0, 1) + U = P(X > 0, X) + V = P(Y < 0, X) + W = P(X + Y > 0, X) + assert W == P(X + Y > 0, X) + assert U == BernoulliDistribution(S.Half, S.Zero, S.One) + assert V == S.Half + +def test_is_random(): + X = Normal('X', 0, 1) + Y = Normal('Y', 0, 1) + a, b = symbols('a, b') + G = GaussianUnitaryEnsemble('U', 2) + B = BernoulliProcess('B', 0.9) + assert not is_random(a) + assert not is_random(a + b) + assert not is_random(a * b) + assert not is_random(Matrix([a**2, b**2])) + assert is_random(X) + assert is_random(X**2 + Y) + assert is_random(Y + b**2) + assert is_random(Y > 5) + assert is_random(B[3] < 1) + assert is_random(G) + assert is_random(X * Y * B[1]) + assert is_random(Matrix([[X, B[2]], [G, Y]])) + assert is_random(Eq(X, 4)) + +def test_issue_12283(): + x = symbols('x') + X = RandomSymbol(x) + Y = RandomSymbol('Y') + Z = RandomMatrixSymbol('Z', 2, 1) + W = RandomMatrixSymbol('W', 2, 1) + RI = RandomIndexedSymbol(Indexed('RI', 3)) + assert pspace(Z) == PSpace() + assert pspace(RI) == PSpace() + assert pspace(X) == PSpace() + assert E(X) == Expectation(X) + assert P(Y > 3) == Probability(Y > 3) + assert variance(X) == Variance(X) + assert variance(RI) == Variance(RI) + assert covariance(X, Y) == Covariance(X, Y) + assert covariance(W, Z) == Covariance(W, Z) + +def test_issue_6810(): + X = Die('X', 6) + Y = Normal('Y', 0, 1) + assert P(Eq(X, 2)) == S(1)/6 + assert P(Eq(Y, 0)) == 0 + assert P(Or(X > 2, X < 3)) == 1 + assert P(And(X > 3, X > 2)) == S(1)/2 + +def test_issue_20286(): + n, p = symbols('n p') + B = Binomial('B', n, p) + k = Dummy('k', integer = True) + eq = Sum(Piecewise((-p**k*(1 - p)**(-k + n)*log(p**k*(1 - p)**(-k + n)*binomial(n, k))*binomial(n, k), (k >= 0) & (k <= n)), (nan, True)), (k, 0, n)) + assert eq.dummy_eq(H(B)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_stochastic_process.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_stochastic_process.py new file mode 100644 index 0000000000000000000000000000000000000000..3e42ffc8632240b1d85a774467a057e9857c567c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_stochastic_process.py @@ -0,0 +1,763 @@ +from sympy.concrete.summations import Sum +from sympy.core.containers import Tuple +from sympy.core.function import Lambda +from sympy.core.numbers import (Float, Rational, oo, pi) +from sympy.core.relational import (Eq, Ge, Gt, Le, Lt, Ne) +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.functions.combinatorial.factorials import factorial +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.integers import ceiling +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.special.error_functions import erf +from sympy.functions.special.gamma_functions import (gamma, lowergamma) +from sympy.logic.boolalg import (And, Not) +from sympy.matrices.dense import Matrix +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.matrices.immutable import ImmutableMatrix +from sympy.sets.contains import Contains +from sympy.sets.fancysets import Range +from sympy.sets.sets import (FiniteSet, Interval) +from sympy.stats import (DiscreteMarkovChain, P, TransitionMatrixOf, E, + StochasticStateSpaceOf, variance, ContinuousMarkovChain, + BernoulliProcess, PoissonProcess, WienerProcess, + GammaProcess, sample_stochastic_process) +from sympy.stats.joint_rv import JointDistribution +from sympy.stats.joint_rv_types import JointDistributionHandmade +from sympy.stats.rv import RandomIndexedSymbol +from sympy.stats.symbolic_probability import Probability, Expectation +from sympy.testing.pytest import (raises, skip, ignore_warnings, + warns_deprecated_sympy) +from sympy.external import import_module +from sympy.stats.frv_types import BernoulliDistribution +from sympy.stats.drv_types import PoissonDistribution +from sympy.stats.crv_types import NormalDistribution, GammaDistribution +from sympy.core.symbol import Str + + +def test_DiscreteMarkovChain(): + + # pass only the name + X = DiscreteMarkovChain("X") + assert isinstance(X.state_space, Range) + assert X.index_set == S.Naturals0 + assert isinstance(X.transition_probabilities, MatrixSymbol) + t = symbols('t', positive=True, integer=True) + assert isinstance(X[t], RandomIndexedSymbol) + assert E(X[0]) == Expectation(X[0]) + raises(TypeError, lambda: DiscreteMarkovChain(1)) + raises(NotImplementedError, lambda: X(t)) + raises(NotImplementedError, lambda: X.communication_classes()) + raises(NotImplementedError, lambda: X.canonical_form()) + raises(NotImplementedError, lambda: X.decompose()) + + nz = Symbol('n', integer=True) + TZ = MatrixSymbol('M', nz, nz) + SZ = Range(nz) + YZ = DiscreteMarkovChain('Y', SZ, TZ) + assert P(Eq(YZ[2], 1), Eq(YZ[1], 0)) == TZ[0, 1] + + raises(ValueError, lambda: sample_stochastic_process(t)) + raises(ValueError, lambda: next(sample_stochastic_process(X))) + # pass name and state_space + # any hashable object should be a valid state + # states should be valid as a tuple/set/list/Tuple/Range + sym, rainy, cloudy, sunny = symbols('a Rainy Cloudy Sunny', real=True) + state_spaces = [(1, 2, 3), [Str('Hello'), sym, DiscreteMarkovChain("Y", (1,2,3))], + Tuple(S(1), exp(sym), Str('World'), sympify=False), Range(-1, 5, 2), + [rainy, cloudy, sunny]] + chains = [DiscreteMarkovChain("Y", state_space) for state_space in state_spaces] + + for i, Y in enumerate(chains): + assert isinstance(Y.transition_probabilities, MatrixSymbol) + assert Y.state_space == state_spaces[i] or Y.state_space == FiniteSet(*state_spaces[i]) + assert Y.number_of_states == 3 + + with ignore_warnings(UserWarning): # TODO: Restore tests once warnings are removed + assert P(Eq(Y[2], 1), Eq(Y[0], 2), evaluate=False) == Probability(Eq(Y[2], 1), Eq(Y[0], 2)) + assert E(Y[0]) == Expectation(Y[0]) + + raises(ValueError, lambda: next(sample_stochastic_process(Y))) + + raises(TypeError, lambda: DiscreteMarkovChain("Y", {1: 1})) + Y = DiscreteMarkovChain("Y", Range(1, t, 2)) + assert Y.number_of_states == ceiling((t-1)/2) + + # pass name and transition_probabilities + chains = [DiscreteMarkovChain("Y", trans_probs=Matrix([])), + DiscreteMarkovChain("Y", trans_probs=Matrix([[0, 1], [1, 0]])), + DiscreteMarkovChain("Y", trans_probs=Matrix([[pi, 1-pi], [sym, 1-sym]]))] + for Z in chains: + assert Z.number_of_states == Z.transition_probabilities.shape[0] + assert isinstance(Z.transition_probabilities, ImmutableMatrix) + + # pass name, state_space and transition_probabilities + T = Matrix([[0.5, 0.2, 0.3],[0.2, 0.5, 0.3],[0.2, 0.3, 0.5]]) + TS = MatrixSymbol('T', 3, 3) + Y = DiscreteMarkovChain("Y", [0, 1, 2], T) + YS = DiscreteMarkovChain("Y", ['One', 'Two', 3], TS) + assert Y.joint_distribution(1, Y[2], 3) == JointDistribution(Y[1], Y[2], Y[3]) + raises(ValueError, lambda: Y.joint_distribution(Y[1].symbol, Y[2].symbol)) + assert P(Eq(Y[3], 2), Eq(Y[1], 1)).round(2) == Float(0.36, 2) + assert (P(Eq(YS[3], 2), Eq(YS[1], 1)) - + (TS[0, 2]*TS[1, 0] + TS[1, 1]*TS[1, 2] + TS[1, 2]*TS[2, 2])).simplify() == 0 + assert P(Eq(YS[1], 1), Eq(YS[2], 2)) == Probability(Eq(YS[1], 1)) + assert P(Eq(YS[3], 3), Eq(YS[1], 1)) == TS[0, 2]*TS[1, 0] + TS[1, 1]*TS[1, 2] + TS[1, 2]*TS[2, 2] + TO = Matrix([[0.25, 0.75, 0],[0, 0.25, 0.75],[0.75, 0, 0.25]]) + assert P(Eq(Y[3], 2), Eq(Y[1], 1) & TransitionMatrixOf(Y, TO)).round(3) == Float(0.375, 3) + with ignore_warnings(UserWarning): ### TODO: Restore tests once warnings are removed + assert E(Y[3], evaluate=False) == Expectation(Y[3]) + assert E(Y[3], Eq(Y[2], 1)).round(2) == Float(1.1, 3) + TSO = MatrixSymbol('T', 4, 4) + raises(ValueError, lambda: str(P(Eq(YS[3], 2), Eq(YS[1], 1) & TransitionMatrixOf(YS, TSO)))) + raises(TypeError, lambda: DiscreteMarkovChain("Z", [0, 1, 2], symbols('M'))) + raises(ValueError, lambda: DiscreteMarkovChain("Z", [0, 1, 2], MatrixSymbol('T', 3, 4))) + raises(ValueError, lambda: E(Y[3], Eq(Y[2], 6))) + raises(ValueError, lambda: E(Y[2], Eq(Y[3], 1))) + + + # extended tests for probability queries + TO1 = Matrix([[Rational(1, 4), Rational(3, 4), 0],[Rational(1, 3), Rational(1, 3), Rational(1, 3)],[0, Rational(1, 4), Rational(3, 4)]]) + assert P(And(Eq(Y[2], 1), Eq(Y[1], 1), Eq(Y[0], 0)), + Eq(Probability(Eq(Y[0], 0)), Rational(1, 4)) & TransitionMatrixOf(Y, TO1)) == Rational(1, 16) + assert P(And(Eq(Y[2], 1), Eq(Y[1], 1), Eq(Y[0], 0)), TransitionMatrixOf(Y, TO1)) == \ + Probability(Eq(Y[0], 0))/4 + assert P(Lt(X[1], 2) & Gt(X[1], 0), Eq(X[0], 2) & + StochasticStateSpaceOf(X, [0, 1, 2]) & TransitionMatrixOf(X, TO1)) == Rational(1, 4) + assert P(Lt(X[1], 2) & Gt(X[1], 0), Eq(X[0], 2) & + StochasticStateSpaceOf(X, [S(0), '0', 1]) & TransitionMatrixOf(X, TO1)) == Rational(1, 4) + assert P(Ne(X[1], 2) & Ne(X[1], 1), Eq(X[0], 2) & + StochasticStateSpaceOf(X, [0, 1, 2]) & TransitionMatrixOf(X, TO1)) is S.Zero + assert P(Ne(X[1], 2) & Ne(X[1], 1), Eq(X[0], 2) & + StochasticStateSpaceOf(X, [S(0), '0', 1]) & TransitionMatrixOf(X, TO1)) is S.Zero + assert P(And(Eq(Y[2], 1), Eq(Y[1], 1), Eq(Y[0], 0)), Eq(Y[1], 1)) == 0.1*Probability(Eq(Y[0], 0)) + + # testing properties of Markov chain + TO2 = Matrix([[S.One, 0, 0],[Rational(1, 3), Rational(1, 3), Rational(1, 3)],[0, Rational(1, 4), Rational(3, 4)]]) + TO3 = Matrix([[Rational(1, 4), Rational(3, 4), 0],[Rational(1, 3), Rational(1, 3), Rational(1, 3)], [0, Rational(1, 4), Rational(3, 4)]]) + Y2 = DiscreteMarkovChain('Y', trans_probs=TO2) + Y3 = DiscreteMarkovChain('Y', trans_probs=TO3) + assert Y3.fundamental_matrix() == ImmutableMatrix([[176, 81, -132], [36, 141, -52], [-44, -39, 208]])/125 + assert Y2.is_absorbing_chain() == True + assert Y3.is_absorbing_chain() == False + assert Y2.canonical_form() == ([0, 1, 2], TO2) + assert Y3.canonical_form() == ([0, 1, 2], TO3) + assert Y2.decompose() == ([0, 1, 2], TO2[0:1, 0:1], TO2[1:3, 0:1], TO2[1:3, 1:3]) + assert Y3.decompose() == ([0, 1, 2], TO3, Matrix(0, 3, []), Matrix(0, 0, [])) + TO4 = Matrix([[Rational(1, 5), Rational(2, 5), Rational(2, 5)], [Rational(1, 10), S.Half, Rational(2, 5)], [Rational(3, 5), Rational(3, 10), Rational(1, 10)]]) + Y4 = DiscreteMarkovChain('Y', trans_probs=TO4) + w = ImmutableMatrix([[Rational(11, 39), Rational(16, 39), Rational(4, 13)]]) + assert Y4.limiting_distribution == w + assert Y4.is_regular() == True + assert Y4.is_ergodic() == True + TS1 = MatrixSymbol('T', 3, 3) + Y5 = DiscreteMarkovChain('Y', trans_probs=TS1) + assert Y5.limiting_distribution(w, TO4).doit() == True + assert Y5.stationary_distribution(condition_set=True).subs(TS1, TO4).contains(w).doit() == S.true + TO6 = Matrix([[S.One, 0, 0, 0, 0],[S.Half, 0, S.Half, 0, 0],[0, S.Half, 0, S.Half, 0], [0, 0, S.Half, 0, S.Half], [0, 0, 0, 0, 1]]) + Y6 = DiscreteMarkovChain('Y', trans_probs=TO6) + assert Y6.fundamental_matrix() == ImmutableMatrix([[Rational(3, 2), S.One, S.Half], [S.One, S(2), S.One], [S.Half, S.One, Rational(3, 2)]]) + assert Y6.absorbing_probabilities() == ImmutableMatrix([[Rational(3, 4), Rational(1, 4)], [S.Half, S.Half], [Rational(1, 4), Rational(3, 4)]]) + with warns_deprecated_sympy(): + Y6.absorbing_probabilites() + TO7 = Matrix([[Rational(1, 2), Rational(1, 4), Rational(1, 4)], [Rational(1, 2), 0, Rational(1, 2)], [Rational(1, 4), Rational(1, 4), Rational(1, 2)]]) + Y7 = DiscreteMarkovChain('Y', trans_probs=TO7) + assert Y7.is_absorbing_chain() == False + assert Y7.fundamental_matrix() == ImmutableMatrix([[Rational(86, 75), Rational(1, 25), Rational(-14, 75)], + [Rational(2, 25), Rational(21, 25), Rational(2, 25)], + [Rational(-14, 75), Rational(1, 25), Rational(86, 75)]]) + + # test for zero-sized matrix functionality + X = DiscreteMarkovChain('X', trans_probs=Matrix([])) + assert X.number_of_states == 0 + assert X.stationary_distribution() == Matrix([[]]) + assert X.communication_classes() == [] + assert X.canonical_form() == ([], Matrix([])) + assert X.decompose() == ([], Matrix([]), Matrix([]), Matrix([])) + assert X.is_regular() == False + assert X.is_ergodic() == False + + # test communication_class + # see https://drive.google.com/drive/folders/1HbxLlwwn2b3U8Lj7eb_ASIUb5vYaNIjg?usp=sharing + # tutorial 2.pdf + TO7 = Matrix([[0, 5, 5, 0, 0], + [0, 0, 0, 10, 0], + [5, 0, 5, 0, 0], + [0, 10, 0, 0, 0], + [0, 3, 0, 3, 4]])/10 + Y7 = DiscreteMarkovChain('Y', trans_probs=TO7) + tuples = Y7.communication_classes() + classes, recurrence, periods = list(zip(*tuples)) + assert classes == ([1, 3], [0, 2], [4]) + assert recurrence == (True, False, False) + assert periods == (2, 1, 1) + + TO8 = Matrix([[0, 0, 0, 10, 0, 0], + [5, 0, 5, 0, 0, 0], + [0, 4, 0, 0, 0, 6], + [10, 0, 0, 0, 0, 0], + [0, 10, 0, 0, 0, 0], + [0, 0, 0, 5, 5, 0]])/10 + Y8 = DiscreteMarkovChain('Y', trans_probs=TO8) + tuples = Y8.communication_classes() + classes, recurrence, periods = list(zip(*tuples)) + assert classes == ([0, 3], [1, 2, 5, 4]) + assert recurrence == (True, False) + assert periods == (2, 2) + + TO9 = Matrix([[2, 0, 0, 3, 0, 0, 3, 2, 0, 0], + [0, 10, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 2, 2, 0, 0, 0, 0, 0, 3, 3], + [0, 0, 0, 3, 0, 0, 6, 1, 0, 0], + [0, 0, 0, 0, 5, 5, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 10, 0, 0, 0, 0], + [4, 0, 0, 5, 0, 0, 1, 0, 0, 0], + [2, 0, 0, 4, 0, 0, 2, 2, 0, 0], + [3, 0, 1, 0, 0, 0, 0, 0, 4, 2], + [0, 0, 4, 0, 0, 0, 0, 0, 3, 3]])/10 + Y9 = DiscreteMarkovChain('Y', trans_probs=TO9) + tuples = Y9.communication_classes() + classes, recurrence, periods = list(zip(*tuples)) + assert classes == ([0, 3, 6, 7], [1], [2, 8, 9], [5], [4]) + assert recurrence == (True, True, False, True, False) + assert periods == (1, 1, 1, 1, 1) + + # test canonical form + # see https://web.archive.org/web/20201230182007/https://www.dartmouth.edu/~chance/teaching_aids/books_articles/probability_book/Chapter11.pdf + # example 11.13 + T = Matrix([[1, 0, 0, 0, 0], + [S(1) / 2, 0, S(1) / 2, 0, 0], + [0, S(1) / 2, 0, S(1) / 2, 0], + [0, 0, S(1) / 2, 0, S(1) / 2], + [0, 0, 0, 0, S(1)]]) + DW = DiscreteMarkovChain('DW', [0, 1, 2, 3, 4], T) + states, A, B, C = DW.decompose() + assert states == [0, 4, 1, 2, 3] + assert A == Matrix([[1, 0], [0, 1]]) + assert B == Matrix([[S(1)/2, 0], [0, 0], [0, S(1)/2]]) + assert C == Matrix([[0, S(1)/2, 0], [S(1)/2, 0, S(1)/2], [0, S(1)/2, 0]]) + states, new_matrix = DW.canonical_form() + assert states == [0, 4, 1, 2, 3] + assert new_matrix == Matrix([[1, 0, 0, 0, 0], + [0, 1, 0, 0, 0], + [S(1)/2, 0, 0, S(1)/2, 0], + [0, 0, S(1)/2, 0, S(1)/2], + [0, S(1)/2, 0, S(1)/2, 0]]) + + # test regular and ergodic + # https://web.archive.org/web/20201230182007/https://www.dartmouth.edu/~chance/teaching_aids/books_articles/probability_book/Chapter11.pdf + T = Matrix([[0, 4, 0, 0, 0], + [1, 0, 3, 0, 0], + [0, 2, 0, 2, 0], + [0, 0, 3, 0, 1], + [0, 0, 0, 4, 0]])/4 + X = DiscreteMarkovChain('X', trans_probs=T) + assert not X.is_regular() + assert X.is_ergodic() + T = Matrix([[0, 1], [1, 0]]) + X = DiscreteMarkovChain('X', trans_probs=T) + assert not X.is_regular() + assert X.is_ergodic() + # http://www.math.wisc.edu/~valko/courses/331/MC2.pdf + T = Matrix([[2, 1, 1], + [2, 0, 2], + [1, 1, 2]])/4 + X = DiscreteMarkovChain('X', trans_probs=T) + assert X.is_regular() + assert X.is_ergodic() + # https://docs.ufpr.br/~lucambio/CE222/1S2014/Kemeny-Snell1976.pdf + T = Matrix([[1, 1], [1, 1]])/2 + X = DiscreteMarkovChain('X', trans_probs=T) + assert X.is_regular() + assert X.is_ergodic() + + # test is_absorbing_chain + T = Matrix([[0, 1, 0], + [1, 0, 0], + [0, 0, 1]]) + X = DiscreteMarkovChain('X', trans_probs=T) + assert not X.is_absorbing_chain() + # https://en.wikipedia.org/wiki/Absorbing_Markov_chain + T = Matrix([[1, 1, 0, 0], + [0, 1, 1, 0], + [1, 0, 0, 1], + [0, 0, 0, 2]])/2 + X = DiscreteMarkovChain('X', trans_probs=T) + assert X.is_absorbing_chain() + T = Matrix([[2, 0, 0, 0, 0], + [1, 0, 1, 0, 0], + [0, 1, 0, 1, 0], + [0, 0, 1, 0, 1], + [0, 0, 0, 0, 2]])/2 + X = DiscreteMarkovChain('X', trans_probs=T) + assert X.is_absorbing_chain() + + # test custom state space + Y10 = DiscreteMarkovChain('Y', [1, 2, 3], TO2) + tuples = Y10.communication_classes() + classes, recurrence, periods = list(zip(*tuples)) + assert classes == ([1], [2, 3]) + assert recurrence == (True, False) + assert periods == (1, 1) + assert Y10.canonical_form() == ([1, 2, 3], TO2) + assert Y10.decompose() == ([1, 2, 3], TO2[0:1, 0:1], TO2[1:3, 0:1], TO2[1:3, 1:3]) + + # testing miscellaneous queries + T = Matrix([[S.Half, Rational(1, 4), Rational(1, 4)], + [Rational(1, 3), 0, Rational(2, 3)], + [S.Half, S.Half, 0]]) + X = DiscreteMarkovChain('X', [0, 1, 2], T) + assert P(Eq(X[1], 2) & Eq(X[2], 1) & Eq(X[3], 0), + Eq(P(Eq(X[1], 0)), Rational(1, 4)) & Eq(P(Eq(X[1], 1)), Rational(1, 4))) == Rational(1, 12) + assert P(Eq(X[2], 1) | Eq(X[2], 2), Eq(X[1], 1)) == Rational(2, 3) + assert P(Eq(X[2], 1) & Eq(X[2], 2), Eq(X[1], 1)) is S.Zero + assert P(Ne(X[2], 2), Eq(X[1], 1)) == Rational(1, 3) + assert E(X[1]**2, Eq(X[0], 1)) == Rational(8, 3) + assert variance(X[1], Eq(X[0], 1)) == Rational(8, 9) + raises(ValueError, lambda: E(X[1], Eq(X[2], 1))) + raises(ValueError, lambda: DiscreteMarkovChain('X', [0, 1], T)) + + # testing miscellaneous queries with different state space + X = DiscreteMarkovChain('X', ['A', 'B', 'C'], T) + assert P(Eq(X[1], 2) & Eq(X[2], 1) & Eq(X[3], 0), + Eq(P(Eq(X[1], 0)), Rational(1, 4)) & Eq(P(Eq(X[1], 1)), Rational(1, 4))) == Rational(1, 12) + assert P(Eq(X[2], 1) | Eq(X[2], 2), Eq(X[1], 1)) == Rational(2, 3) + assert P(Eq(X[2], 1) & Eq(X[2], 2), Eq(X[1], 1)) is S.Zero + assert P(Ne(X[2], 2), Eq(X[1], 1)) == Rational(1, 3) + a = X.state_space.args[0] + c = X.state_space.args[2] + assert (E(X[1] ** 2, Eq(X[0], 1)) - (a**2/3 + 2*c**2/3)).simplify() == 0 + assert (variance(X[1], Eq(X[0], 1)) - (2*(-a/3 + c/3)**2/3 + (2*a/3 - 2*c/3)**2/3)).simplify() == 0 + raises(ValueError, lambda: E(X[1], Eq(X[2], 1))) + + #testing queries with multiple RandomIndexedSymbols + T = Matrix([[Rational(5, 10), Rational(3, 10), Rational(2, 10)], [Rational(2, 10), Rational(7, 10), Rational(1, 10)], [Rational(3, 10), Rational(3, 10), Rational(4, 10)]]) + Y = DiscreteMarkovChain("Y", [0, 1, 2], T) + assert P(Eq(Y[7], Y[5]), Eq(Y[2], 0)).round(5) == Float(0.44428, 5) + assert P(Gt(Y[3], Y[1]), Eq(Y[0], 0)).round(2) == Float(0.36, 2) + assert P(Le(Y[5], Y[10]), Eq(Y[4], 2)).round(6) == Float(0.583120, 6) + assert Float(P(Eq(Y[10], Y[5]), Eq(Y[4], 1)), 14) == Float(1 - P(Ne(Y[10], Y[5]), Eq(Y[4], 1)), 14) + assert Float(P(Gt(Y[8], Y[9]), Eq(Y[3], 2)), 14) == Float(1 - P(Le(Y[8], Y[9]), Eq(Y[3], 2)), 14) + assert Float(P(Lt(Y[1], Y[4]), Eq(Y[0], 0)), 14) == Float(1 - P(Ge(Y[1], Y[4]), Eq(Y[0], 0)), 14) + assert P(Eq(Y[5], Y[10]), Eq(Y[2], 1)) == P(Eq(Y[10], Y[5]), Eq(Y[2], 1)) + assert P(Gt(Y[1], Y[2]), Eq(Y[0], 1)) == P(Lt(Y[2], Y[1]), Eq(Y[0], 1)) + assert P(Ge(Y[7], Y[6]), Eq(Y[4], 1)) == P(Le(Y[6], Y[7]), Eq(Y[4], 1)) + + #test symbolic queries + a, b, c, d = symbols('a b c d') + T = Matrix([[Rational(1, 10), Rational(4, 10), Rational(5, 10)], [Rational(3, 10), Rational(4, 10), Rational(3, 10)], [Rational(7, 10), Rational(2, 10), Rational(1, 10)]]) + Y = DiscreteMarkovChain("Y", [0, 1, 2], T) + query = P(Eq(Y[a], b), Eq(Y[c], d)) + assert query.subs({a:10, b:2, c:5, d:1}).evalf().round(4) == P(Eq(Y[10], 2), Eq(Y[5], 1)).round(4) + assert query.subs({a:15, b:0, c:10, d:1}).evalf().round(4) == P(Eq(Y[15], 0), Eq(Y[10], 1)).round(4) + query_gt = P(Gt(Y[a], b), Eq(Y[c], d)) + query_le = P(Le(Y[a], b), Eq(Y[c], d)) + assert query_gt.subs({a:5, b:2, c:1, d:0}).evalf() + query_le.subs({a:5, b:2, c:1, d:0}).evalf() == 1.0 + query_ge = P(Ge(Y[a], b), Eq(Y[c], d)) + query_lt = P(Lt(Y[a], b), Eq(Y[c], d)) + assert query_ge.subs({a:4, b:1, c:0, d:2}).evalf() + query_lt.subs({a:4, b:1, c:0, d:2}).evalf() == 1.0 + + #test issue 20078 + assert (2*Y[1] + 3*Y[1]).simplify() == 5*Y[1] + assert (2*Y[1] - 3*Y[1]).simplify() == -Y[1] + assert (2*(0.25*Y[1])).simplify() == 0.5*Y[1] + assert ((2*Y[1]) * (0.25*Y[1])).simplify() == 0.5*Y[1]**2 + assert (Y[1]**2 + Y[1]**3).simplify() == (Y[1] + 1)*Y[1]**2 + +def test_sample_stochastic_process(): + if not import_module('scipy'): + skip('SciPy Not installed. Skip sampling tests') + import random + random.seed(0) + numpy = import_module('numpy') + if numpy: + numpy.random.seed(0) # scipy uses numpy to sample so to set its seed + T = Matrix([[0.5, 0.2, 0.3],[0.2, 0.5, 0.3],[0.2, 0.3, 0.5]]) + Y = DiscreteMarkovChain("Y", [0, 1, 2], T) + for samps in range(10): + assert next(sample_stochastic_process(Y)) in Y.state_space + Z = DiscreteMarkovChain("Z", ['1', 1, 0], T) + for samps in range(10): + assert next(sample_stochastic_process(Z)) in Z.state_space + + T = Matrix([[S.Half, Rational(1, 4), Rational(1, 4)], + [Rational(1, 3), 0, Rational(2, 3)], + [S.Half, S.Half, 0]]) + X = DiscreteMarkovChain('X', [0, 1, 2], T) + for samps in range(10): + assert next(sample_stochastic_process(X)) in X.state_space + W = DiscreteMarkovChain('W', [1, pi, oo], T) + for samps in range(10): + assert next(sample_stochastic_process(W)) in W.state_space + + +def test_ContinuousMarkovChain(): + T1 = Matrix([[S(-2), S(2), S.Zero], + [S.Zero, S.NegativeOne, S.One], + [Rational(3, 2), Rational(3, 2), S(-3)]]) + C1 = ContinuousMarkovChain('C', [0, 1, 2], T1) + assert C1.limiting_distribution() == ImmutableMatrix([[Rational(3, 19), Rational(12, 19), Rational(4, 19)]]) + + T2 = Matrix([[-S.One, S.One, S.Zero], [S.One, -S.One, S.Zero], [S.Zero, S.One, -S.One]]) + C2 = ContinuousMarkovChain('C', [0, 1, 2], T2) + A, t = C2.generator_matrix, symbols('t', positive=True) + assert C2.transition_probabilities(A)(t) == Matrix([[S.Half + exp(-2*t)/2, S.Half - exp(-2*t)/2, 0], + [S.Half - exp(-2*t)/2, S.Half + exp(-2*t)/2, 0], + [S.Half - exp(-t) + exp(-2*t)/2, S.Half - exp(-2*t)/2, exp(-t)]]) + with ignore_warnings(UserWarning): ### TODO: Restore tests once warnings are removed + assert P(Eq(C2(1), 1), Eq(C2(0), 1), evaluate=False) == Probability(Eq(C2(1), 1), Eq(C2(0), 1)) + assert P(Eq(C2(1), 1), Eq(C2(0), 1)) == exp(-2)/2 + S.Half + assert P(Eq(C2(1), 0) & Eq(C2(2), 1) & Eq(C2(3), 1), + Eq(P(Eq(C2(1), 0)), S.Half)) == (Rational(1, 4) - exp(-2)/4)*(exp(-2)/2 + S.Half) + assert P(Not(Eq(C2(1), 0) & Eq(C2(2), 1) & Eq(C2(3), 2)) | + (Eq(C2(1), 0) & Eq(C2(2), 1) & Eq(C2(3), 2)), + Eq(P(Eq(C2(1), 0)), Rational(1, 4)) & Eq(P(Eq(C2(1), 1)), Rational(1, 4))) is S.One + assert E(C2(Rational(3, 2)), Eq(C2(0), 2)) == -exp(-3)/2 + 2*exp(Rational(-3, 2)) + S.Half + assert variance(C2(Rational(3, 2)), Eq(C2(0), 1)) == ((S.Half - exp(-3)/2)**2*(exp(-3)/2 + S.Half) + + (Rational(-1, 2) - exp(-3)/2)**2*(S.Half - exp(-3)/2)) + raises(KeyError, lambda: P(Eq(C2(1), 0), Eq(P(Eq(C2(1), 1)), S.Half))) + assert P(Eq(C2(1), 0), Eq(P(Eq(C2(5), 1)), S.Half)) == Probability(Eq(C2(1), 0)) + TS1 = MatrixSymbol('G', 3, 3) + CS1 = ContinuousMarkovChain('C', [0, 1, 2], TS1) + A = CS1.generator_matrix + assert CS1.transition_probabilities(A)(t) == exp(t*A) + + C3 = ContinuousMarkovChain('C', [Symbol('0'), Symbol('1'), Symbol('2')], T2) + assert P(Eq(C3(1), 1), Eq(C3(0), 1)) == exp(-2)/2 + S.Half + assert P(Eq(C3(1), Symbol('1')), Eq(C3(0), Symbol('1'))) == exp(-2)/2 + S.Half + + #test probability queries + G = Matrix([[-S(1), Rational(1, 10), Rational(9, 10)], [Rational(2, 5), -S(1), Rational(3, 5)], [Rational(1, 2), Rational(1, 2), -S(1)]]) + C = ContinuousMarkovChain('C', state_space=[0, 1, 2], gen_mat=G) + assert P(Eq(C(7.385), C(3.19)), Eq(C(0.862), 0)).round(5) == Float(0.35469, 5) + assert P(Gt(C(98.715), C(19.807)), Eq(C(11.314), 2)).round(5) == Float(0.32452, 5) + assert P(Le(C(5.9), C(10.112)), Eq(C(4), 1)).round(6) == Float(0.675214, 6) + assert Float(P(Eq(C(7.32), C(2.91)), Eq(C(2.63), 1)), 14) == Float(1 - P(Ne(C(7.32), C(2.91)), Eq(C(2.63), 1)), 14) + assert Float(P(Gt(C(3.36), C(1.101)), Eq(C(0.8), 2)), 14) == Float(1 - P(Le(C(3.36), C(1.101)), Eq(C(0.8), 2)), 14) + assert Float(P(Lt(C(4.9), C(2.79)), Eq(C(1.61), 0)), 14) == Float(1 - P(Ge(C(4.9), C(2.79)), Eq(C(1.61), 0)), 14) + assert P(Eq(C(5.243), C(10.912)), Eq(C(2.174), 1)) == P(Eq(C(10.912), C(5.243)), Eq(C(2.174), 1)) + assert P(Gt(C(2.344), C(9.9)), Eq(C(1.102), 1)) == P(Lt(C(9.9), C(2.344)), Eq(C(1.102), 1)) + assert P(Ge(C(7.87), C(1.008)), Eq(C(0.153), 1)) == P(Le(C(1.008), C(7.87)), Eq(C(0.153), 1)) + + #test symbolic queries + a, b, c, d = symbols('a b c d') + query = P(Eq(C(a), b), Eq(C(c), d)) + assert query.subs({a:3.65, b:2, c:1.78, d:1}).evalf().round(10) == P(Eq(C(3.65), 2), Eq(C(1.78), 1)).round(10) + query_gt = P(Gt(C(a), b), Eq(C(c), d)) + query_le = P(Le(C(a), b), Eq(C(c), d)) + assert query_gt.subs({a:13.2, b:0, c:3.29, d:2}).evalf() + query_le.subs({a:13.2, b:0, c:3.29, d:2}).evalf() == 1.0 + query_ge = P(Ge(C(a), b), Eq(C(c), d)) + query_lt = P(Lt(C(a), b), Eq(C(c), d)) + assert query_ge.subs({a:7.43, b:1, c:1.45, d:0}).evalf() + query_lt.subs({a:7.43, b:1, c:1.45, d:0}).evalf() == 1.0 + + #test issue 20078 + assert (2*C(1) + 3*C(1)).simplify() == 5*C(1) + assert (2*C(1) - 3*C(1)).simplify() == -C(1) + assert (2*(0.25*C(1))).simplify() == 0.5*C(1) + assert (2*C(1) * 0.25*C(1)).simplify() == 0.5*C(1)**2 + assert (C(1)**2 + C(1)**3).simplify() == (C(1) + 1)*C(1)**2 + +def test_BernoulliProcess(): + + B = BernoulliProcess("B", p=0.6, success=1, failure=0) + assert B.state_space == FiniteSet(0, 1) + assert B.index_set == S.Naturals0 + assert B.success == 1 + assert B.failure == 0 + + X = BernoulliProcess("X", p=Rational(1,3), success='H', failure='T') + assert X.state_space == FiniteSet('H', 'T') + H, T = symbols("H,T") + assert E(X[1]+X[2]*X[3]) == H**2/9 + 4*H*T/9 + H/3 + 4*T**2/9 + 2*T/3 + + t, x = symbols('t, x', positive=True, integer=True) + assert isinstance(B[t], RandomIndexedSymbol) + + raises(ValueError, lambda: BernoulliProcess("X", p=1.1, success=1, failure=0)) + raises(NotImplementedError, lambda: B(t)) + + raises(IndexError, lambda: B[-3]) + assert B.joint_distribution(B[3], B[9]) == JointDistributionHandmade(Lambda((B[3], B[9]), + Piecewise((0.6, Eq(B[3], 1)), (0.4, Eq(B[3], 0)), (0, True)) + *Piecewise((0.6, Eq(B[9], 1)), (0.4, Eq(B[9], 0)), (0, True)))) + + assert B.joint_distribution(2, B[4]) == JointDistributionHandmade(Lambda((B[2], B[4]), + Piecewise((0.6, Eq(B[2], 1)), (0.4, Eq(B[2], 0)), (0, True)) + *Piecewise((0.6, Eq(B[4], 1)), (0.4, Eq(B[4], 0)), (0, True)))) + + # Test for the sum distribution of Bernoulli Process RVs + Y = B[1] + B[2] + B[3] + assert P(Eq(Y, 0)).round(2) == Float(0.06, 1) + assert P(Eq(Y, 2)).round(2) == Float(0.43, 2) + assert P(Eq(Y, 4)).round(2) == 0 + assert P(Gt(Y, 1)).round(2) == Float(0.65, 2) + # Test for independency of each Random Indexed variable + assert P(Eq(B[1], 0) & Eq(B[2], 1) & Eq(B[3], 0) & Eq(B[4], 1)).round(2) == Float(0.06, 1) + + assert E(2 * B[1] + B[2]).round(2) == Float(1.80, 3) + assert E(2 * B[1] + B[2] + 5).round(2) == Float(6.80, 3) + assert E(B[2] * B[4] + B[10]).round(2) == Float(0.96, 2) + assert E(B[2] > 0, Eq(B[1],1) & Eq(B[2],1)).round(2) == Float(0.60,2) + assert E(B[1]) == 0.6 + assert P(B[1] > 0).round(2) == Float(0.60, 2) + assert P(B[1] < 1).round(2) == Float(0.40, 2) + assert P(B[1] > 0, B[2] <= 1).round(2) == Float(0.60, 2) + assert P(B[12] * B[5] > 0).round(2) == Float(0.36, 2) + assert P(B[12] * B[5] > 0, B[4] < 1).round(2) == Float(0.36, 2) + assert P(Eq(B[2], 1), B[2] > 0) == 1.0 + assert P(Eq(B[5], 3)) == 0 + assert P(Eq(B[1], 1), B[1] < 0) == 0 + assert P(B[2] > 0, Eq(B[2], 1)) == 1 + assert P(B[2] < 0, Eq(B[2], 1)) == 0 + assert P(B[2] > 0, B[2]==7) == 0 + assert P(B[5] > 0, B[5]) == BernoulliDistribution(0.6, 0, 1) + raises(ValueError, lambda: P(3)) + raises(ValueError, lambda: P(B[3] > 0, 3)) + + # test issue 19456 + expr = Sum(B[t], (t, 0, 4)) + expr2 = Sum(B[t], (t, 1, 3)) + expr3 = Sum(B[t]**2, (t, 1, 3)) + assert expr.doit() == B[0] + B[1] + B[2] + B[3] + B[4] + assert expr2.doit() == Y + assert expr3.doit() == B[1]**2 + B[2]**2 + B[3]**2 + assert B[2*t].free_symbols == {B[2*t], t} + assert B[4].free_symbols == {B[4]} + assert B[x*t].free_symbols == {B[x*t], x, t} + + #test issue 20078 + assert (2*B[t] + 3*B[t]).simplify() == 5*B[t] + assert (2*B[t] - 3*B[t]).simplify() == -B[t] + assert (2*(0.25*B[t])).simplify() == 0.5*B[t] + assert (2*B[t] * 0.25*B[t]).simplify() == 0.5*B[t]**2 + assert (B[t]**2 + B[t]**3).simplify() == (B[t] + 1)*B[t]**2 + +def test_PoissonProcess(): + X = PoissonProcess("X", 3) + assert X.state_space == S.Naturals0 + assert X.index_set == Interval(0, oo) + assert X.lamda == 3 + + t, d, x, y = symbols('t d x y', positive=True) + assert isinstance(X(t), RandomIndexedSymbol) + assert X.distribution(t) == PoissonDistribution(3*t) + with warns_deprecated_sympy(): + X.distribution(X(t)) + raises(ValueError, lambda: PoissonProcess("X", -1)) + raises(NotImplementedError, lambda: X[t]) + raises(IndexError, lambda: X(-5)) + + assert X.joint_distribution(X(2), X(3)) == JointDistributionHandmade(Lambda((X(2), X(3)), + 6**X(2)*9**X(3)*exp(-15)/(factorial(X(2))*factorial(X(3))))) + + assert X.joint_distribution(4, 6) == JointDistributionHandmade(Lambda((X(4), X(6)), + 12**X(4)*18**X(6)*exp(-30)/(factorial(X(4))*factorial(X(6))))) + + assert P(X(t) < 1) == exp(-3*t) + assert P(Eq(X(t), 0), Contains(t, Interval.Lopen(3, 5))) == exp(-6) # exp(-2*lamda) + res = P(Eq(X(t), 1), Contains(t, Interval.Lopen(3, 4))) + assert res == 3*exp(-3) + + # Equivalent to P(Eq(X(t), 1))**4 because of non-overlapping intervals + assert P(Eq(X(t), 1) & Eq(X(d), 1) & Eq(X(x), 1) & Eq(X(y), 1), Contains(t, Interval.Lopen(0, 1)) + & Contains(d, Interval.Lopen(1, 2)) & Contains(x, Interval.Lopen(2, 3)) + & Contains(y, Interval.Lopen(3, 4))) == res**4 + + # Return Probability because of overlapping intervals + assert P(Eq(X(t), 2) & Eq(X(d), 3), Contains(t, Interval.Lopen(0, 2)) + & Contains(d, Interval.Ropen(2, 4))) == \ + Probability(Eq(X(d), 3) & Eq(X(t), 2), Contains(t, Interval.Lopen(0, 2)) + & Contains(d, Interval.Ropen(2, 4))) + + raises(ValueError, lambda: P(Eq(X(t), 2) & Eq(X(d), 3), + Contains(t, Interval.Lopen(0, 4)) & Contains(d, Interval.Lopen(3, oo)))) # no bound on d + assert P(Eq(X(3), 2)) == 81*exp(-9)/2 + assert P(Eq(X(t), 2), Contains(t, Interval.Lopen(0, 5))) == 225*exp(-15)/2 + + # Check that probability works correctly by adding it to 1 + res1 = P(X(t) <= 3, Contains(t, Interval.Lopen(0, 5))) + res2 = P(X(t) > 3, Contains(t, Interval.Lopen(0, 5))) + assert res1 == 691*exp(-15) + assert (res1 + res2).simplify() == 1 + + # Check Not and Or + assert P(Not(Eq(X(t), 2) & (X(d) > 3)), Contains(t, Interval.Ropen(2, 4)) & \ + Contains(d, Interval.Lopen(7, 8))).simplify() == -18*exp(-6) + 234*exp(-9) + 1 + assert P(Eq(X(t), 2) | Ne(X(t), 4), Contains(t, Interval.Ropen(2, 4))) == 1 - 36*exp(-6) + raises(ValueError, lambda: P(X(t) > 2, X(t) + X(d))) + assert E(X(t)) == 3*t # property of the distribution at a given timestamp + assert E(X(t)**2 + X(d)*2 + X(y)**3, Contains(t, Interval.Lopen(0, 1)) + & Contains(d, Interval.Lopen(1, 2)) & Contains(y, Interval.Ropen(3, 4))) == 75 + assert E(X(t)**2, Contains(t, Interval.Lopen(0, 1))) == 12 + assert E(x*(X(t) + X(d))*(X(t)**2+X(d)**2), Contains(t, Interval.Lopen(0, 1)) + & Contains(d, Interval.Ropen(1, 2))) == \ + Expectation(x*(X(d) + X(t))*(X(d)**2 + X(t)**2), Contains(t, Interval.Lopen(0, 1)) + & Contains(d, Interval.Ropen(1, 2))) + + # Value Error because of infinite time bound + raises(ValueError, lambda: E(X(t)**3, Contains(t, Interval.Lopen(1, oo)))) + + # Equivalent to E(X(t)**2) - E(X(d)**2) == E(X(1)**2) - E(X(1)**2) == 0 + assert E((X(t) + X(d))*(X(t) - X(d)), Contains(t, Interval.Lopen(0, 1)) + & Contains(d, Interval.Lopen(1, 2))) == 0 + assert E(X(2) + x*E(X(5))) == 15*x + 6 + assert E(x*X(1) + y) == 3*x + y + assert P(Eq(X(1), 2) & Eq(X(t), 3), Contains(t, Interval.Lopen(1, 2))) == 81*exp(-6)/4 + Y = PoissonProcess("Y", 6) + Z = X + Y + assert Z.lamda == X.lamda + Y.lamda == 9 + raises(ValueError, lambda: X + 5) # should be added be only PoissonProcess instance + N, M = Z.split(4, 5) + assert N.lamda == 4 + assert M.lamda == 5 + raises(ValueError, lambda: Z.split(3, 2)) # 2+3 != 9 + + raises(ValueError, lambda :P(Eq(X(t), 0), Contains(t, Interval.Lopen(1, 3)) & Eq(X(1), 0))) + # check if it handles queries with two random variables in one args + res1 = P(Eq(N(3), N(5))) + assert res1 == P(Eq(N(t), 0), Contains(t, Interval(3, 5))) + res2 = P(N(3) > N(1)) + assert res2 == P((N(t) > 0), Contains(t, Interval(1, 3))) + assert P(N(3) < N(1)) == 0 # condition is not possible + res3 = P(N(3) <= N(1)) # holds only for Eq(N(3), N(1)) + assert res3 == P(Eq(N(t), 0), Contains(t, Interval(1, 3))) + + # tests from https://www.probabilitycourse.com/chapter11/11_1_2_basic_concepts_of_the_poisson_process.php + X = PoissonProcess('X', 10) # 11.1 + assert P(Eq(X(S(1)/3), 3) & Eq(X(1), 10)) == exp(-10)*Rational(8000000000, 11160261) + assert P(Eq(X(1), 1), Eq(X(S(1)/3), 3)) == 0 + assert P(Eq(X(1), 10), Eq(X(S(1)/3), 3)) == P(Eq(X(S(2)/3), 7)) + + X = PoissonProcess('X', 2) # 11.2 + assert P(X(S(1)/2) < 1) == exp(-1) + assert P(X(3) < 1, Eq(X(1), 0)) == exp(-4) + assert P(Eq(X(4), 3), Eq(X(2), 3)) == exp(-4) + + X = PoissonProcess('X', 3) + assert P(Eq(X(2), 5) & Eq(X(1), 2)) == Rational(81, 4)*exp(-6) + + # check few properties + assert P(X(2) <= 3, X(1)>=1) == 3*P(Eq(X(1), 0)) + 2*P(Eq(X(1), 1)) + P(Eq(X(1), 2)) + assert P(X(2) <= 3, X(1) > 1) == 2*P(Eq(X(1), 0)) + 1*P(Eq(X(1), 1)) + assert P(Eq(X(2), 5) & Eq(X(1), 2)) == P(Eq(X(1), 3))*P(Eq(X(1), 2)) + assert P(Eq(X(3), 4), Eq(X(1), 3)) == P(Eq(X(2), 1)) + + #test issue 20078 + assert (2*X(t) + 3*X(t)).simplify() == 5*X(t) + assert (2*X(t) - 3*X(t)).simplify() == -X(t) + assert (2*(0.25*X(t))).simplify() == 0.5*X(t) + assert (2*X(t) * 0.25*X(t)).simplify() == 0.5*X(t)**2 + assert (X(t)**2 + X(t)**3).simplify() == (X(t) + 1)*X(t)**2 + +def test_WienerProcess(): + X = WienerProcess("X") + assert X.state_space == S.Reals + assert X.index_set == Interval(0, oo) + + t, d, x, y = symbols('t d x y', positive=True) + assert isinstance(X(t), RandomIndexedSymbol) + assert X.distribution(t) == NormalDistribution(0, sqrt(t)) + with warns_deprecated_sympy(): + X.distribution(X(t)) + raises(ValueError, lambda: PoissonProcess("X", -1)) + raises(NotImplementedError, lambda: X[t]) + raises(IndexError, lambda: X(-2)) + + assert X.joint_distribution(X(2), X(3)) == JointDistributionHandmade( + Lambda((X(2), X(3)), sqrt(6)*exp(-X(2)**2/4)*exp(-X(3)**2/6)/(12*pi))) + assert X.joint_distribution(4, 6) == JointDistributionHandmade( + Lambda((X(4), X(6)), sqrt(6)*exp(-X(4)**2/8)*exp(-X(6)**2/12)/(24*pi))) + + assert P(X(t) < 3).simplify() == erf(3*sqrt(2)/(2*sqrt(t)))/2 + S(1)/2 + assert P(X(t) > 2, Contains(t, Interval.Lopen(3, 7))).simplify() == S(1)/2 -\ + erf(sqrt(2)/2)/2 + + # Equivalent to P(X(1)>1)**4 + assert P((X(t) > 4) & (X(d) > 3) & (X(x) > 2) & (X(y) > 1), + Contains(t, Interval.Lopen(0, 1)) & Contains(d, Interval.Lopen(1, 2)) + & Contains(x, Interval.Lopen(2, 3)) & Contains(y, Interval.Lopen(3, 4))).simplify() ==\ + (1 - erf(sqrt(2)/2))*(1 - erf(sqrt(2)))*(1 - erf(3*sqrt(2)/2))*(1 - erf(2*sqrt(2)))/16 + + # Contains an overlapping interval so, return Probability + assert P((X(t)< 2) & (X(d)> 3), Contains(t, Interval.Lopen(0, 2)) + & Contains(d, Interval.Ropen(2, 4))) == Probability((X(d) > 3) & (X(t) < 2), + Contains(d, Interval.Ropen(2, 4)) & Contains(t, Interval.Lopen(0, 2))) + + assert str(P(Not((X(t) < 5) & (X(d) > 3)), Contains(t, Interval.Ropen(2, 4)) & + Contains(d, Interval.Lopen(7, 8))).simplify()) == \ + '-(1 - erf(3*sqrt(2)/2))*(2 - erfc(5/2))/4 + 1' + # Distribution has mean 0 at each timestamp + assert E(X(t)) == 0 + assert E(x*(X(t) + X(d))*(X(t)**2+X(d)**2), Contains(t, Interval.Lopen(0, 1)) + & Contains(d, Interval.Ropen(1, 2))) == Expectation(x*(X(d) + X(t))*(X(d)**2 + X(t)**2), + Contains(d, Interval.Ropen(1, 2)) & Contains(t, Interval.Lopen(0, 1))) + assert E(X(t) + x*E(X(3))) == 0 + + #test issue 20078 + assert (2*X(t) + 3*X(t)).simplify() == 5*X(t) + assert (2*X(t) - 3*X(t)).simplify() == -X(t) + assert (2*(0.25*X(t))).simplify() == 0.5*X(t) + assert (2*X(t) * 0.25*X(t)).simplify() == 0.5*X(t)**2 + assert (X(t)**2 + X(t)**3).simplify() == (X(t) + 1)*X(t)**2 + + +def test_GammaProcess_symbolic(): + t, d, x, y, g, l = symbols('t d x y g l', positive=True) + X = GammaProcess("X", l, g) + + raises(NotImplementedError, lambda: X[t]) + raises(IndexError, lambda: X(-1)) + assert isinstance(X(t), RandomIndexedSymbol) + assert X.state_space == Interval(0, oo) + assert X.distribution(t) == GammaDistribution(g*t, 1/l) + with warns_deprecated_sympy(): + X.distribution(X(t)) + assert X.joint_distribution(5, X(3)) == JointDistributionHandmade(Lambda( + (X(5), X(3)), l**(8*g)*exp(-l*X(3))*exp(-l*X(5))*X(3)**(3*g - 1)*X(5)**(5*g + - 1)/(gamma(3*g)*gamma(5*g)))) + # property of the gamma process at any given timestamp + assert E(X(t)) == g*t/l + assert variance(X(t)).simplify() == g*t/l**2 + + # Equivalent to E(2*X(1)) + E(X(1)**2) + E(X(1)**3), where E(X(1)) == g/l + assert E(X(t)**2 + X(d)*2 + X(y)**3, Contains(t, Interval.Lopen(0, 1)) + & Contains(d, Interval.Lopen(1, 2)) & Contains(y, Interval.Ropen(3, 4))) == \ + 2*g/l + (g**2 + g)/l**2 + (g**3 + 3*g**2 + 2*g)/l**3 + + assert P(X(t) > 3, Contains(t, Interval.Lopen(3, 4))).simplify() == \ + 1 - lowergamma(g, 3*l)/gamma(g) # equivalent to P(X(1)>3) + + + #test issue 20078 + assert (2*X(t) + 3*X(t)).simplify() == 5*X(t) + assert (2*X(t) - 3*X(t)).simplify() == -X(t) + assert (2*(0.25*X(t))).simplify() == 0.5*X(t) + assert (2*X(t) * 0.25*X(t)).simplify() == 0.5*X(t)**2 + assert (X(t)**2 + X(t)**3).simplify() == (X(t) + 1)*X(t)**2 +def test_GammaProcess_numeric(): + t, d, x, y = symbols('t d x y', positive=True) + X = GammaProcess("X", 1, 2) + assert X.state_space == Interval(0, oo) + assert X.index_set == Interval(0, oo) + assert X.lamda == 1 + assert X.gamma == 2 + + raises(ValueError, lambda: GammaProcess("X", -1, 2)) + raises(ValueError, lambda: GammaProcess("X", 0, -2)) + raises(ValueError, lambda: GammaProcess("X", -1, -2)) + + # all are independent because of non-overlapping intervals + assert P((X(t) > 4) & (X(d) > 3) & (X(x) > 2) & (X(y) > 1), Contains(t, + Interval.Lopen(0, 1)) & Contains(d, Interval.Lopen(1, 2)) & Contains(x, + Interval.Lopen(2, 3)) & Contains(y, Interval.Lopen(3, 4))).simplify() == \ + 120*exp(-10) + + # Check working with Not and Or + assert P(Not((X(t) < 5) & (X(d) > 3)), Contains(t, Interval.Ropen(2, 4)) & + Contains(d, Interval.Lopen(7, 8))).simplify() == -4*exp(-3) + 472*exp(-8)/3 + 1 + assert P((X(t) > 2) | (X(t) < 4), Contains(t, Interval.Ropen(1, 4))).simplify() == \ + -643*exp(-4)/15 + 109*exp(-2)/15 + 1 + + assert E(X(t)) == 2*t # E(X(t)) == gamma*t/l + assert E(X(2) + x*E(X(5))) == 10*x + 4 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_symbolic_multivariate.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_symbolic_multivariate.py new file mode 100644 index 0000000000000000000000000000000000000000..79979e20a6f10d2a2ddfe85ce4c8df145e98c3fd --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_symbolic_multivariate.py @@ -0,0 +1,172 @@ +from sympy.stats import Expectation, Normal, Variance, Covariance +from sympy.testing.pytest import raises +from sympy.core.symbol import symbols +from sympy.matrices.exceptions import ShapeError +from sympy.matrices.dense import Matrix +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.matrices.expressions.special import ZeroMatrix +from sympy.stats.rv import RandomMatrixSymbol +from sympy.stats.symbolic_multivariate_probability import (ExpectationMatrix, + VarianceMatrix, CrossCovarianceMatrix) + +j, k = symbols("j,k") + +A = MatrixSymbol("A", k, k) +B = MatrixSymbol("B", k, k) +C = MatrixSymbol("C", k, k) +D = MatrixSymbol("D", k, k) + +a = MatrixSymbol("a", k, 1) +b = MatrixSymbol("b", k, 1) + +A2 = MatrixSymbol("A2", 2, 2) +B2 = MatrixSymbol("B2", 2, 2) + +X = RandomMatrixSymbol("X", k, 1) +Y = RandomMatrixSymbol("Y", k, 1) +Z = RandomMatrixSymbol("Z", k, 1) +W = RandomMatrixSymbol("W", k, 1) + +R = RandomMatrixSymbol("R", k, k) + +X2 = RandomMatrixSymbol("X2", 2, 1) + +normal = Normal("normal", 0, 1) + +m1 = Matrix([ + [1, j*Normal("normal2", 2, 1)], + [normal, 0] +]) + +def test_multivariate_expectation(): + expr = Expectation(a) + assert expr == Expectation(a) == ExpectationMatrix(a) + assert expr.expand() == a + + expr = Expectation(X) + assert expr == Expectation(X) == ExpectationMatrix(X) + assert expr.shape == (k, 1) + assert expr.rows == k + assert expr.cols == 1 + assert isinstance(expr, ExpectationMatrix) + + expr = Expectation(A*X + b) + assert expr == ExpectationMatrix(A*X + b) + assert expr.expand() == A*ExpectationMatrix(X) + b + assert isinstance(expr, ExpectationMatrix) + assert expr.shape == (k, 1) + + expr = Expectation(m1*X2) + assert expr.expand() == expr + + expr = Expectation(A2*m1*B2*X2) + assert expr.args[0].args == (A2, m1, B2, X2) + assert expr.expand() == A2*ExpectationMatrix(m1*B2*X2) + + expr = Expectation((X + Y)*(X - Y).T) + assert expr.expand() == ExpectationMatrix(X*X.T) - ExpectationMatrix(X*Y.T) +\ + ExpectationMatrix(Y*X.T) - ExpectationMatrix(Y*Y.T) + + expr = Expectation(A*X + B*Y) + assert expr.expand() == A*ExpectationMatrix(X) + B*ExpectationMatrix(Y) + + assert Expectation(m1).doit() == Matrix([[1, 2*j], [0, 0]]) + + x1 = Matrix([ + [Normal('N11', 11, 1), Normal('N12', 12, 1)], + [Normal('N21', 21, 1), Normal('N22', 22, 1)] + ]) + x2 = Matrix([ + [Normal('M11', 1, 1), Normal('M12', 2, 1)], + [Normal('M21', 3, 1), Normal('M22', 4, 1)] + ]) + + assert Expectation(Expectation(x1 + x2)).doit(deep=False) == ExpectationMatrix(x1 + x2) + assert Expectation(Expectation(x1 + x2)).doit() == Matrix([[12, 14], [24, 26]]) + + +def test_multivariate_variance(): + raises(ShapeError, lambda: Variance(A)) + + expr = Variance(a) + assert expr == Variance(a) == VarianceMatrix(a) + assert expr.expand() == ZeroMatrix(k, k) + expr = Variance(a.T) + assert expr == Variance(a.T) == VarianceMatrix(a.T) + assert expr.expand() == ZeroMatrix(k, k) + + expr = Variance(X) + assert expr == Variance(X) == VarianceMatrix(X) + assert expr.shape == (k, k) + assert expr.rows == k + assert expr.cols == k + assert isinstance(expr, VarianceMatrix) + + expr = Variance(A*X) + assert expr == VarianceMatrix(A*X) + assert expr.expand() == A*VarianceMatrix(X)*A.T + assert isinstance(expr, VarianceMatrix) + assert expr.shape == (k, k) + + expr = Variance(A*B*X) + assert expr.expand() == A*B*VarianceMatrix(X)*B.T*A.T + + expr = Variance(m1*X2) + assert expr.expand() == expr + + expr = Variance(A2*m1*B2*X2) + assert expr.args[0].args == (A2, m1, B2, X2) + assert expr.expand() == expr + + expr = Variance(A*X + B*Y) + assert expr.expand() == 2*A*CrossCovarianceMatrix(X, Y)*B.T +\ + A*VarianceMatrix(X)*A.T + B*VarianceMatrix(Y)*B.T + +def test_multivariate_crosscovariance(): + raises(ShapeError, lambda: Covariance(X, Y.T)) + raises(ShapeError, lambda: Covariance(X, A)) + + + expr = Covariance(a.T, b.T) + assert expr.shape == (1, 1) + assert expr.expand() == ZeroMatrix(1, 1) + + expr = Covariance(a, b) + assert expr == Covariance(a, b) == CrossCovarianceMatrix(a, b) + assert expr.expand() == ZeroMatrix(k, k) + assert expr.shape == (k, k) + assert expr.rows == k + assert expr.cols == k + assert isinstance(expr, CrossCovarianceMatrix) + + expr = Covariance(A*X + a, b) + assert expr.expand() == ZeroMatrix(k, k) + + expr = Covariance(X, Y) + assert isinstance(expr, CrossCovarianceMatrix) + assert expr.expand() == expr + + expr = Covariance(X, X) + assert isinstance(expr, CrossCovarianceMatrix) + assert expr.expand() == VarianceMatrix(X) + + expr = Covariance(X + Y, Z) + assert isinstance(expr, CrossCovarianceMatrix) + assert expr.expand() == CrossCovarianceMatrix(X, Z) + CrossCovarianceMatrix(Y, Z) + + expr = Covariance(A*X, Y) + assert isinstance(expr, CrossCovarianceMatrix) + assert expr.expand() == A*CrossCovarianceMatrix(X, Y) + + expr = Covariance(X, B*Y) + assert isinstance(expr, CrossCovarianceMatrix) + assert expr.expand() == CrossCovarianceMatrix(X, Y)*B.T + + expr = Covariance(A*X + a, B.T*Y + b) + assert isinstance(expr, CrossCovarianceMatrix) + assert expr.expand() == A*CrossCovarianceMatrix(X, Y)*B + + expr = Covariance(A*X + B*Y + a, C.T*Z + D.T*W + b) + assert isinstance(expr, CrossCovarianceMatrix) + assert expr.expand() == A*CrossCovarianceMatrix(X, W)*D + A*CrossCovarianceMatrix(X, Z)*C \ + + B*CrossCovarianceMatrix(Y, W)*D + B*CrossCovarianceMatrix(Y, Z)*C diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_symbolic_probability.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_symbolic_probability.py new file mode 100644 index 0000000000000000000000000000000000000000..edac942ac081c0d44cafd31761b77bc577b6a3fd --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/stats/tests/test_symbolic_probability.py @@ -0,0 +1,175 @@ +from sympy.concrete.summations import Sum +from sympy.core.mul import Mul +from sympy.core.numbers import (oo, pi) +from sympy.core.relational import Eq +from sympy.core.symbol import (Dummy, symbols) +from sympy.functions.elementary.exponential import exp +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import sin +from sympy.integrals.integrals import Integral +from sympy.core.expr import unchanged +from sympy.stats import (Normal, Poisson, variance, Covariance, Variance, + Probability, Expectation, Moment, CentralMoment) +from sympy.stats.rv import probability, expectation + + +def test_literal_probability(): + X = Normal('X', 2, 3) + Y = Normal('Y', 3, 4) + Z = Poisson('Z', 4) + W = Poisson('W', 3) + x = symbols('x', real=True) + y, w, z = symbols('y, w, z') + + assert Probability(X > 0).evaluate_integral() == probability(X > 0) + assert Probability(X > x).evaluate_integral() == probability(X > x) + assert Probability(X > 0).rewrite(Integral).doit() == probability(X > 0) + assert Probability(X > x).rewrite(Integral).doit() == probability(X > x) + + assert Expectation(X).evaluate_integral() == expectation(X) + assert Expectation(X).rewrite(Integral).doit() == expectation(X) + assert Expectation(X**2).evaluate_integral() == expectation(X**2) + assert Expectation(x*X).args == (x*X,) + assert Expectation(x*X).expand() == x*Expectation(X) + assert Expectation(2*X + 3*Y + z*X*Y).expand() == 2*Expectation(X) + 3*Expectation(Y) + z*Expectation(X*Y) + assert Expectation(2*X + 3*Y + z*X*Y).args == (2*X + 3*Y + z*X*Y,) + assert Expectation(sin(X)) == Expectation(sin(X)).expand() + assert Expectation(2*x*sin(X)*Y + y*X**2 + z*X*Y).expand() == 2*x*Expectation(sin(X)*Y) \ + + y*Expectation(X**2) + z*Expectation(X*Y) + assert Expectation(X + Y).expand() == Expectation(X) + Expectation(Y) + assert Expectation((X + Y)*(X - Y)).expand() == Expectation(X**2) - Expectation(Y**2) + assert Expectation((X + Y)*(X - Y)).expand().doit() == -12 + assert Expectation(X + Y, evaluate=True).doit() == 5 + assert Expectation(X + Expectation(Y)).doit() == 5 + assert Expectation(X + Expectation(Y)).doit(deep=False) == 2 + Expectation(Expectation(Y)) + assert Expectation(X + Expectation(Y + Expectation(2*X))).doit(deep=False) == 2 \ + + Expectation(Expectation(Y + Expectation(2*X))) + assert Expectation(X + Expectation(Y + Expectation(2*X))).doit() == 9 + assert Expectation(Expectation(2*X)).doit() == 4 + assert Expectation(Expectation(2*X)).doit(deep=False) == Expectation(2*X) + assert Expectation(4*Expectation(2*X)).doit(deep=False) == 4*Expectation(2*X) + assert Expectation((X + Y)**3).expand() == 3*Expectation(X*Y**2) +\ + 3*Expectation(X**2*Y) + Expectation(X**3) + Expectation(Y**3) + assert Expectation((X - Y)**3).expand() == 3*Expectation(X*Y**2) -\ + 3*Expectation(X**2*Y) + Expectation(X**3) - Expectation(Y**3) + assert Expectation((X - Y)**2).expand() == -2*Expectation(X*Y) +\ + Expectation(X**2) + Expectation(Y**2) + + assert Variance(w).args == (w,) + assert Variance(w).expand() == 0 + assert Variance(X).evaluate_integral() == Variance(X).rewrite(Integral).doit() == variance(X) + assert Variance(X + z).args == (X + z,) + assert Variance(X + z).expand() == Variance(X) + assert Variance(X*Y).args == (Mul(X, Y),) + assert type(Variance(X*Y)) == Variance + assert Variance(z*X).expand() == z**2*Variance(X) + assert Variance(X + Y).expand() == Variance(X) + Variance(Y) + 2*Covariance(X, Y) + assert Variance(X + Y + Z + W).expand() == (Variance(X) + Variance(Y) + Variance(Z) + Variance(W) + + 2 * Covariance(X, Y) + 2 * Covariance(X, Z) + 2 * Covariance(X, W) + + 2 * Covariance(Y, Z) + 2 * Covariance(Y, W) + 2 * Covariance(W, Z)) + assert Variance(X**2).evaluate_integral() == variance(X**2) + assert unchanged(Variance, X**2) + assert Variance(x*X**2).expand() == x**2*Variance(X**2) + assert Variance(sin(X)).args == (sin(X),) + assert Variance(sin(X)).expand() == Variance(sin(X)) + assert Variance(x*sin(X)).expand() == x**2*Variance(sin(X)) + + assert Covariance(w, z).args == (w, z) + assert Covariance(w, z).expand() == 0 + assert Covariance(X, w).expand() == 0 + assert Covariance(w, X).expand() == 0 + assert Covariance(X, Y).args == (X, Y) + assert type(Covariance(X, Y)) == Covariance + assert Covariance(z*X + 3, Y).expand() == z*Covariance(X, Y) + assert Covariance(X, X).args == (X, X) + assert Covariance(X, X).expand() == Variance(X) + assert Covariance(z*X + 3, w*Y + 4).expand() == w*z*Covariance(X,Y) + assert Covariance(X, Y) == Covariance(Y, X) + assert Covariance(X + Y, Z + W).expand() == Covariance(W, X) + Covariance(W, Y) + Covariance(X, Z) + Covariance(Y, Z) + assert Covariance(x*X + y*Y, z*Z + w*W).expand() == (x*w*Covariance(W, X) + w*y*Covariance(W, Y) + + x*z*Covariance(X, Z) + y*z*Covariance(Y, Z)) + assert Covariance(x*X**2 + y*sin(Y), z*Y*Z**2 + w*W).expand() == (w*x*Covariance(W, X**2) + w*y*Covariance(sin(Y), W) + + x*z*Covariance(Y*Z**2, X**2) + y*z*Covariance(Y*Z**2, sin(Y))) + assert Covariance(X, X**2).expand() == Covariance(X, X**2) + assert Covariance(X, sin(X)).expand() == Covariance(sin(X), X) + assert Covariance(X**2, sin(X)*Y).expand() == Covariance(sin(X)*Y, X**2) + assert Covariance(w, X).evaluate_integral() == 0 + + +def test_probability_rewrite(): + X = Normal('X', 2, 3) + Y = Normal('Y', 3, 4) + Z = Poisson('Z', 4) + W = Poisson('W', 3) + x, y, w, z = symbols('x, y, w, z') + + assert Variance(w).rewrite(Expectation) == 0 + assert Variance(X).rewrite(Expectation) == Expectation(X ** 2) - Expectation(X) ** 2 + assert Variance(X, condition=Y).rewrite(Expectation) == Expectation(X ** 2, Y) - Expectation(X, Y) ** 2 + assert Variance(X, Y) != Expectation(X**2) - Expectation(X)**2 + assert Variance(X + z).rewrite(Expectation) == Expectation((X + z) ** 2) - Expectation(X + z) ** 2 + assert Variance(X * Y).rewrite(Expectation) == Expectation(X ** 2 * Y ** 2) - Expectation(X * Y) ** 2 + + assert Covariance(w, X).rewrite(Expectation) == -w*Expectation(X) + Expectation(w*X) + assert Covariance(X, Y).rewrite(Expectation) == Expectation(X*Y) - Expectation(X)*Expectation(Y) + assert Covariance(X, Y, condition=W).rewrite(Expectation) == Expectation(X * Y, W) - Expectation(X, W) * Expectation(Y, W) + + w, x, z = symbols("W, x, z") + px = Probability(Eq(X, x)) + pz = Probability(Eq(Z, z)) + + assert Expectation(X).rewrite(Probability) == Integral(x*px, (x, -oo, oo)) + assert Expectation(Z).rewrite(Probability) == Sum(z*pz, (z, 0, oo)) + assert Variance(X).rewrite(Probability) == Integral(x**2*px, (x, -oo, oo)) - Integral(x*px, (x, -oo, oo))**2 + assert Variance(Z).rewrite(Probability) == Sum(z**2*pz, (z, 0, oo)) - Sum(z*pz, (z, 0, oo))**2 + assert Covariance(w, X).rewrite(Probability) == \ + -w*Integral(x*Probability(Eq(X, x)), (x, -oo, oo)) + Integral(w*x*Probability(Eq(X, x)), (x, -oo, oo)) + + # To test rewrite as sum function + assert Variance(X).rewrite(Sum) == Variance(X).rewrite(Integral) + assert Expectation(X).rewrite(Sum) == Expectation(X).rewrite(Integral) + + assert Covariance(w, X).rewrite(Sum) == 0 + + assert Covariance(w, X).rewrite(Integral) == 0 + + assert Variance(X, condition=Y).rewrite(Probability) == Integral(x**2*Probability(Eq(X, x), Y), (x, -oo, oo)) - \ + Integral(x*Probability(Eq(X, x), Y), (x, -oo, oo))**2 + + +def test_symbolic_Moment(): + mu = symbols('mu', real=True) + sigma = symbols('sigma', positive=True) + x = symbols('x') + X = Normal('X', mu, sigma) + M = Moment(X, 4, 2) + assert M.rewrite(Expectation) == Expectation((X - 2)**4) + assert M.rewrite(Probability) == Integral((x - 2)**4*Probability(Eq(X, x)), + (x, -oo, oo)) + k = Dummy('k') + expri = Integral(sqrt(2)*(k - 2)**4*exp(-(k - \ + mu)**2/(2*sigma**2))/(2*sqrt(pi)*sigma), (k, -oo, oo)) + assert M.rewrite(Integral).dummy_eq(expri) + assert M.doit() == (mu**4 - 8*mu**3 + 6*mu**2*sigma**2 + \ + 24*mu**2 - 24*mu*sigma**2 - 32*mu + 3*sigma**4 + 24*sigma**2 + 16) + M = Moment(2, 5) + assert M.doit() == 2**5 + + +def test_symbolic_CentralMoment(): + mu = symbols('mu', real=True) + sigma = symbols('sigma', positive=True) + x = symbols('x') + X = Normal('X', mu, sigma) + CM = CentralMoment(X, 6) + assert CM.rewrite(Expectation) == Expectation((X - Expectation(X))**6) + assert CM.rewrite(Probability) == Integral((x - Integral(x*Probability(True), + (x, -oo, oo)))**6*Probability(Eq(X, x)), (x, -oo, oo)) + k = Dummy('k') + expri = Integral(sqrt(2)*(k - Integral(sqrt(2)*k*exp(-(k - \ + mu)**2/(2*sigma**2))/(2*sqrt(pi)*sigma), (k, -oo, oo)))**6*exp(-(k - \ + mu)**2/(2*sigma**2))/(2*sqrt(pi)*sigma), (k, -oo, oo)) + assert CM.rewrite(Integral).dummy_eq(expri) + assert CM.doit().simplify() == 15*sigma**6 + CM = Moment(5, 5) + assert CM.doit() == 5**5 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bb4c5aa8afe6fd818e136ec0797b7429e2da76cf --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/__init__.py @@ -0,0 +1,50 @@ +""" Rewrite Rules + +DISCLAIMER: This module is experimental. The interface is subject to change. + +A rule is a function that transforms one expression into another + + Rule :: Expr -> Expr + +A strategy is a function that says how a rule should be applied to a syntax +tree. In general strategies take rules and produce a new rule + + Strategy :: [Rules], Other-stuff -> Rule + +This allows developers to separate a mathematical transformation from the +algorithmic details of applying that transformation. The goal is to separate +the work of mathematical programming from algorithmic programming. + +Submodules + +strategies.rl - some fundamental rules +strategies.core - generic non-SymPy specific strategies +strategies.traverse - strategies that traverse a SymPy tree +strategies.tools - some conglomerate strategies that do depend on SymPy +""" + +from . import rl +from . import traverse +from .rl import rm_id, unpack, flatten, sort, glom, distribute, rebuild +from .util import new +from .core import ( + condition, debug, chain, null_safe, do_one, exhaust, minimize, tryit) +from .tools import canon, typed +from . import branch + +__all__ = [ + 'rl', + + 'traverse', + + 'rm_id', 'unpack', 'flatten', 'sort', 'glom', 'distribute', 'rebuild', + + 'new', + + 'condition', 'debug', 'chain', 'null_safe', 'do_one', 'exhaust', + 'minimize', 'tryit', + + 'canon', 'typed', + + 'branch', +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..fec5afe84a58f3d887a8c762692a3673a2b6d4c8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/__init__.py @@ -0,0 +1,14 @@ +from . import traverse +from .core import ( + condition, debug, multiplex, exhaust, notempty, + chain, onaction, sfilter, yieldify, do_one, identity) +from .tools import canon + +__all__ = [ + 'traverse', + + 'condition', 'debug', 'multiplex', 'exhaust', 'notempty', 'chain', + 'onaction', 'sfilter', 'yieldify', 'do_one', 'identity', + + 'canon', +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/core.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/core.py new file mode 100644 index 0000000000000000000000000000000000000000..2dabaef69b60d994799f71414699223f84e1809b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/core.py @@ -0,0 +1,116 @@ +""" Generic SymPy-Independent Strategies """ + + +def identity(x): + yield x + + +def exhaust(brule): + """ Apply a branching rule repeatedly until it has no effect """ + def exhaust_brl(expr): + seen = {expr} + for nexpr in brule(expr): + if nexpr not in seen: + seen.add(nexpr) + yield from exhaust_brl(nexpr) + if seen == {expr}: + yield expr + return exhaust_brl + + +def onaction(brule, fn): + def onaction_brl(expr): + for result in brule(expr): + if result != expr: + fn(brule, expr, result) + yield result + return onaction_brl + + +def debug(brule, file=None): + """ Print the input and output expressions at each rule application """ + if not file: + from sys import stdout + file = stdout + + def write(brl, expr, result): + file.write("Rule: %s\n" % brl.__name__) + file.write("In: %s\nOut: %s\n\n" % (expr, result)) + + return onaction(brule, write) + + +def multiplex(*brules): + """ Multiplex many branching rules into one """ + def multiplex_brl(expr): + seen = set() + for brl in brules: + for nexpr in brl(expr): + if nexpr not in seen: + seen.add(nexpr) + yield nexpr + return multiplex_brl + + +def condition(cond, brule): + """ Only apply branching rule if condition is true """ + def conditioned_brl(expr): + if cond(expr): + yield from brule(expr) + else: + pass + return conditioned_brl + + +def sfilter(pred, brule): + """ Yield only those results which satisfy the predicate """ + def filtered_brl(expr): + yield from filter(pred, brule(expr)) + return filtered_brl + + +def notempty(brule): + def notempty_brl(expr): + yielded = False + for nexpr in brule(expr): + yielded = True + yield nexpr + if not yielded: + yield expr + return notempty_brl + + +def do_one(*brules): + """ Execute one of the branching rules """ + def do_one_brl(expr): + yielded = False + for brl in brules: + for nexpr in brl(expr): + yielded = True + yield nexpr + if yielded: + return + return do_one_brl + + +def chain(*brules): + """ + Compose a sequence of brules so that they apply to the expr sequentially + """ + def chain_brl(expr): + if not brules: + yield expr + return + + head, tail = brules[0], brules[1:] + for nexpr in head(expr): + yield from chain(*tail)(nexpr) + + return chain_brl + + +def yieldify(rl): + """ Turn a rule into a branching rule """ + def brl(expr): + yield rl(expr) + return brl diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/tests/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/tests/test_core.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/tests/test_core.py new file mode 100644 index 0000000000000000000000000000000000000000..ac620b0afb6dbadc4d97b29ddbb341cd920b6588 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/tests/test_core.py @@ -0,0 +1,117 @@ +from sympy.strategies.branch.core import ( + exhaust, debug, multiplex, condition, notempty, chain, onaction, sfilter, + yieldify, do_one, identity) + + +def posdec(x): + if x > 0: + yield x - 1 + else: + yield x + + +def branch5(x): + if 0 < x < 5: + yield x - 1 + elif 5 < x < 10: + yield x + 1 + elif x == 5: + yield x + 1 + yield x - 1 + else: + yield x + + +def even(x): + return x % 2 == 0 + + +def inc(x): + yield x + 1 + + +def one_to_n(n): + yield from range(n) + + +def test_exhaust(): + brl = exhaust(branch5) + assert set(brl(3)) == {0} + assert set(brl(7)) == {10} + assert set(brl(5)) == {0, 10} + + +def test_debug(): + from io import StringIO + file = StringIO() + rl = debug(posdec, file) + list(rl(5)) + log = file.getvalue() + file.close() + + assert posdec.__name__ in log + assert '5' in log + assert '4' in log + + +def test_multiplex(): + brl = multiplex(posdec, branch5) + assert set(brl(3)) == {2} + assert set(brl(7)) == {6, 8} + assert set(brl(5)) == {4, 6} + + +def test_condition(): + brl = condition(even, branch5) + assert set(brl(4)) == set(branch5(4)) + assert set(brl(5)) == set() + + +def test_sfilter(): + brl = sfilter(even, one_to_n) + assert set(brl(10)) == {0, 2, 4, 6, 8} + + +def test_notempty(): + def ident_if_even(x): + if even(x): + yield x + + brl = notempty(ident_if_even) + assert set(brl(4)) == {4} + assert set(brl(5)) == {5} + + +def test_chain(): + assert list(chain()(2)) == [2] # identity + assert list(chain(inc, inc)(2)) == [4] + assert list(chain(branch5, inc)(4)) == [4] + assert set(chain(branch5, inc)(5)) == {5, 7} + assert list(chain(inc, branch5)(5)) == [7] + + +def test_onaction(): + L = [] + + def record(fn, input, output): + L.append((input, output)) + + list(onaction(inc, record)(2)) + assert L == [(2, 3)] + + list(onaction(identity, record)(2)) + assert L == [(2, 3)] + + +def test_yieldify(): + yinc = yieldify(lambda x: x + 1) + assert list(yinc(3)) == [4] + + +def test_do_one(): + def bad(expr): + raise ValueError + + assert list(do_one(inc)(3)) == [4] + assert list(do_one(inc, bad)(3)) == [4] + assert list(do_one(inc, posdec)(3)) == [4] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/tests/test_tools.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/tests/test_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..c2bd224030c337f0a000d94f6e7e65f3b8bd118f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/tests/test_tools.py @@ -0,0 +1,42 @@ +from sympy.strategies.branch.tools import canon +from sympy.core.basic import Basic +from sympy.core.numbers import Integer +from sympy.core.singleton import S + + +def posdec(x): + if isinstance(x, Integer) and x > 0: + yield x - 1 + else: + yield x + + +def branch5(x): + if isinstance(x, Integer): + if 0 < x < 5: + yield x - 1 + elif 5 < x < 10: + yield x + 1 + elif x == 5: + yield x + 1 + yield x - 1 + else: + yield x + + +def test_zero_ints(): + expr = Basic(S(2), Basic(S(5), S(3)), S(8)) + expected = {Basic(S(0), Basic(S(0), S(0)), S(0))} + + brl = canon(posdec) + assert set(brl(expr)) == expected + + +def test_split5(): + expr = Basic(S(2), Basic(S(5), S(3)), S(8)) + expected = { + Basic(S(0), Basic(S(0), S(0)), S(10)), + Basic(S(0), Basic(S(10), S(0)), S(10))} + + brl = canon(branch5) + assert set(brl(expr)) == expected diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/tests/test_traverse.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/tests/test_traverse.py new file mode 100644 index 0000000000000000000000000000000000000000..e051928210981223004de28b8c617d0438e11ac6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/tests/test_traverse.py @@ -0,0 +1,53 @@ +from sympy.core.basic import Basic +from sympy.core.numbers import Integer +from sympy.core.singleton import S +from sympy.strategies.branch.traverse import top_down, sall +from sympy.strategies.branch.core import do_one, identity + + +def inc(x): + if isinstance(x, Integer): + yield x + 1 + + +def test_top_down_easy(): + expr = Basic(S(1), S(2)) + expected = Basic(S(2), S(3)) + brl = top_down(inc) + + assert set(brl(expr)) == {expected} + + +def test_top_down_big_tree(): + expr = Basic(S(1), Basic(S(2)), Basic(S(3), Basic(S(4)), S(5))) + expected = Basic(S(2), Basic(S(3)), Basic(S(4), Basic(S(5)), S(6))) + brl = top_down(inc) + + assert set(brl(expr)) == {expected} + + +def test_top_down_harder_function(): + def split5(x): + if x == 5: + yield x - 1 + yield x + 1 + + expr = Basic(Basic(S(5), S(6)), S(1)) + expected = {Basic(Basic(S(4), S(6)), S(1)), Basic(Basic(S(6), S(6)), S(1))} + brl = top_down(split5) + + assert set(brl(expr)) == expected + + +def test_sall(): + expr = Basic(S(1), S(2)) + expected = Basic(S(2), S(3)) + brl = sall(inc) + + assert list(brl(expr)) == [expected] + + expr = Basic(S(1), S(2), Basic(S(3), S(4))) + expected = Basic(S(2), S(3), Basic(S(3), S(4))) + brl = sall(do_one(inc, identity)) + + assert list(brl(expr)) == [expected] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/tools.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/tools.py new file mode 100644 index 0000000000000000000000000000000000000000..a6c9097323a7962080ae4497ead976818e386518 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/tools.py @@ -0,0 +1,12 @@ +from .core import exhaust, multiplex +from .traverse import top_down + + +def canon(*rules): + """ Strategy for canonicalization + + Apply each branching rule in a top-down fashion through the tree. + Multiplex through all branching rule traversals + Keep doing this until there is no change. + """ + return exhaust(multiplex(*map(top_down, rules))) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/traverse.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/traverse.py new file mode 100644 index 0000000000000000000000000000000000000000..28b7098dbda401fc0f0b6d27988d8c37e2f231ae --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/branch/traverse.py @@ -0,0 +1,25 @@ +""" Branching Strategies to Traverse a Tree """ +from itertools import product +from sympy.strategies.util import basic_fns +from .core import chain, identity, do_one + + +def top_down(brule, fns=basic_fns): + """ Apply a rule down a tree running it on the top nodes first """ + return chain(do_one(brule, identity), + lambda expr: sall(top_down(brule, fns), fns)(expr)) + + +def sall(brule, fns=basic_fns): + """ Strategic all - apply rule to args """ + op, new, children, leaf = map(fns.get, ('op', 'new', 'children', 'leaf')) + + def all_rl(expr): + if leaf(expr): + yield expr + else: + myop = op(expr) + argss = product(*map(brule, children(expr))) + for args in argss: + yield new(myop, *args) + return all_rl diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/core.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/core.py new file mode 100644 index 0000000000000000000000000000000000000000..75b75cb5f2e0693eea98a7b1c9b3e7f036ec26f6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/core.py @@ -0,0 +1,151 @@ +""" Generic SymPy-Independent Strategies """ +from __future__ import annotations +from collections.abc import Callable, Mapping +from typing import TypeVar +from sys import stdout + + +_S = TypeVar('_S') +_T = TypeVar('_T') + + +def identity(x: _T) -> _T: + return x + + +def exhaust(rule: Callable[[_T], _T]) -> Callable[[_T], _T]: + """ Apply a rule repeatedly until it has no effect """ + def exhaustive_rl(expr: _T) -> _T: + new, old = rule(expr), expr + while new != old: + new, old = rule(new), new + return new + return exhaustive_rl + + +def memoize(rule: Callable[[_S], _T]) -> Callable[[_S], _T]: + """Memoized version of a rule + + Notes + ===== + + This cache can grow infinitely, so it is not recommended to use this + than ``functools.lru_cache`` unless you need very heavy computation. + """ + cache: dict[_S, _T] = {} + + def memoized_rl(expr: _S) -> _T: + if expr in cache: + return cache[expr] + else: + result = rule(expr) + cache[expr] = result + return result + return memoized_rl + + +def condition( + cond: Callable[[_T], bool], rule: Callable[[_T], _T] +) -> Callable[[_T], _T]: + """ Only apply rule if condition is true """ + def conditioned_rl(expr: _T) -> _T: + if cond(expr): + return rule(expr) + return expr + return conditioned_rl + + +def chain(*rules: Callable[[_T], _T]) -> Callable[[_T], _T]: + """ + Compose a sequence of rules so that they apply to the expr sequentially + """ + def chain_rl(expr: _T) -> _T: + for rule in rules: + expr = rule(expr) + return expr + return chain_rl + + +def debug(rule, file=None): + """ Print out before and after expressions each time rule is used """ + if file is None: + file = stdout + + def debug_rl(*args, **kwargs): + expr = args[0] + result = rule(*args, **kwargs) + if result != expr: + file.write("Rule: %s\n" % rule.__name__) + file.write("In: %s\nOut: %s\n\n" % (expr, result)) + return result + return debug_rl + + +def null_safe(rule: Callable[[_T], _T | None]) -> Callable[[_T], _T]: + """ Return original expr if rule returns None """ + def null_safe_rl(expr: _T) -> _T: + result = rule(expr) + if result is None: + return expr + return result + return null_safe_rl + + +def tryit(rule: Callable[[_T], _T], exception) -> Callable[[_T], _T]: + """ Return original expr if rule raises exception """ + def try_rl(expr: _T) -> _T: + try: + return rule(expr) + except exception: + return expr + return try_rl + + +def do_one(*rules: Callable[[_T], _T]) -> Callable[[_T], _T]: + """ Try each of the rules until one works. Then stop. """ + def do_one_rl(expr: _T) -> _T: + for rl in rules: + result = rl(expr) + if result != expr: + return result + return expr + return do_one_rl + + +def switch( + key: Callable[[_S], _T], + ruledict: Mapping[_T, Callable[[_S], _S]] +) -> Callable[[_S], _S]: + """ Select a rule based on the result of key called on the function """ + def switch_rl(expr: _S) -> _S: + rl = ruledict.get(key(expr), identity) + return rl(expr) + return switch_rl + + +# XXX Untyped default argument for minimize function +# where python requires SupportsRichComparison type +def _identity(x): + return x + + +def minimize( + *rules: Callable[[_S], _T], + objective=_identity +) -> Callable[[_S], _T]: + """ Select result of rules that minimizes objective + + >>> from sympy.strategies import minimize + >>> inc = lambda x: x + 1 + >>> dec = lambda x: x - 1 + >>> rl = minimize(inc, dec) + >>> rl(4) + 3 + + >>> rl = minimize(inc, dec, objective=lambda x: -x) # maximize + >>> rl(4) + 5 + """ + def minrule(expr: _S) -> _T: + return min([rule(expr) for rule in rules], key=objective) + return minrule diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/rl.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/rl.py new file mode 100644 index 0000000000000000000000000000000000000000..e84ee90582fafcf36fd4205a58b05b650875a9a5 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/rl.py @@ -0,0 +1,176 @@ +""" Generic Rules for SymPy + +This file assumes knowledge of Basic and little else. +""" +from sympy.utilities.iterables import sift +from .util import new + + +# Functions that create rules +def rm_id(isid, new=new): + """ Create a rule to remove identities. + + isid - fn :: x -> Bool --- whether or not this element is an identity. + + Examples + ======== + + >>> from sympy.strategies import rm_id + >>> from sympy import Basic, S + >>> remove_zeros = rm_id(lambda x: x==0) + >>> remove_zeros(Basic(S(1), S(0), S(2))) + Basic(1, 2) + >>> remove_zeros(Basic(S(0), S(0))) # If only identities then we keep one + Basic(0) + + See Also: + unpack + """ + def ident_remove(expr): + """ Remove identities """ + ids = list(map(isid, expr.args)) + if sum(ids) == 0: # No identities. Common case + return expr + elif sum(ids) != len(ids): # there is at least one non-identity + return new(expr.__class__, + *[arg for arg, x in zip(expr.args, ids) if not x]) + else: + return new(expr.__class__, expr.args[0]) + + return ident_remove + + +def glom(key, count, combine): + """ Create a rule to conglomerate identical args. + + Examples + ======== + + >>> from sympy.strategies import glom + >>> from sympy import Add + >>> from sympy.abc import x + + >>> key = lambda x: x.as_coeff_Mul()[1] + >>> count = lambda x: x.as_coeff_Mul()[0] + >>> combine = lambda cnt, arg: cnt * arg + >>> rl = glom(key, count, combine) + + >>> rl(Add(x, -x, 3*x, 2, 3, evaluate=False)) + 3*x + 5 + + Wait, how are key, count and combine supposed to work? + + >>> key(2*x) + x + >>> count(2*x) + 2 + >>> combine(2, x) + 2*x + """ + def conglomerate(expr): + """ Conglomerate together identical args x + x -> 2x """ + groups = sift(expr.args, key) + counts = {k: sum(map(count, args)) for k, args in groups.items()} + newargs = [combine(cnt, mat) for mat, cnt in counts.items()] + if set(newargs) != set(expr.args): + return new(type(expr), *newargs) + else: + return expr + + return conglomerate + + +def sort(key, new=new): + """ Create a rule to sort by a key function. + + Examples + ======== + + >>> from sympy.strategies import sort + >>> from sympy import Basic, S + >>> sort_rl = sort(str) + >>> sort_rl(Basic(S(3), S(1), S(2))) + Basic(1, 2, 3) + """ + + def sort_rl(expr): + return new(expr.__class__, *sorted(expr.args, key=key)) + return sort_rl + + +def distribute(A, B): + """ Turns an A containing Bs into a B of As + + where A, B are container types + + >>> from sympy.strategies import distribute + >>> from sympy import Add, Mul, symbols + >>> x, y = symbols('x,y') + >>> dist = distribute(Mul, Add) + >>> expr = Mul(2, x+y, evaluate=False) + >>> expr + 2*(x + y) + >>> dist(expr) + 2*x + 2*y + """ + + def distribute_rl(expr): + for i, arg in enumerate(expr.args): + if isinstance(arg, B): + first, b, tail = expr.args[:i], expr.args[i], expr.args[i + 1:] + return B(*[A(*(first + (arg,) + tail)) for arg in b.args]) + return expr + return distribute_rl + + +def subs(a, b): + """ Replace expressions exactly """ + def subs_rl(expr): + if expr == a: + return b + else: + return expr + return subs_rl + + +# Functions that are rules +def unpack(expr): + """ Rule to unpack singleton args + + >>> from sympy.strategies import unpack + >>> from sympy import Basic, S + >>> unpack(Basic(S(2))) + 2 + """ + if len(expr.args) == 1: + return expr.args[0] + else: + return expr + + +def flatten(expr, new=new): + """ Flatten T(a, b, T(c, d), T2(e)) to T(a, b, c, d, T2(e)) """ + cls = expr.__class__ + args = [] + for arg in expr.args: + if arg.__class__ == cls: + args.extend(arg.args) + else: + args.append(arg) + return new(expr.__class__, *args) + + +def rebuild(expr): + """ Rebuild a SymPy tree. + + Explanation + =========== + + This function recursively calls constructors in the expression tree. + This forces canonicalization and removes ugliness introduced by the use of + Basic.__new__ + """ + if expr.is_Atom: + return expr + else: + return expr.func(*list(map(rebuild, expr.args))) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tests/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tests/test_core.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tests/test_core.py new file mode 100644 index 0000000000000000000000000000000000000000..1e19bcab1940c476a8996b7ba92e7645a6230034 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tests/test_core.py @@ -0,0 +1,118 @@ +from __future__ import annotations +from sympy.core.singleton import S +from sympy.core.basic import Basic +from sympy.strategies.core import ( + null_safe, exhaust, memoize, condition, + chain, tryit, do_one, debug, switch, minimize) +from io import StringIO + + +def posdec(x: int) -> int: + if x > 0: + return x - 1 + return x + + +def inc(x: int) -> int: + return x + 1 + + +def dec(x: int) -> int: + return x - 1 + + +def test_null_safe(): + def rl(expr: int) -> int | None: + if expr == 1: + return 2 + return None + + safe_rl = null_safe(rl) + assert rl(1) == safe_rl(1) + assert rl(3) is None + assert safe_rl(3) == 3 + + +def test_exhaust(): + sink = exhaust(posdec) + assert sink(5) == 0 + assert sink(10) == 0 + + +def test_memoize(): + rl = memoize(posdec) + assert rl(5) == posdec(5) + assert rl(5) == posdec(5) + assert rl(-2) == posdec(-2) + + +def test_condition(): + rl = condition(lambda x: x % 2 == 0, posdec) + assert rl(5) == 5 + assert rl(4) == 3 + + +def test_chain(): + rl = chain(posdec, posdec) + assert rl(5) == 3 + assert rl(1) == 0 + + +def test_tryit(): + def rl(expr: Basic) -> Basic: + assert False + + safe_rl = tryit(rl, AssertionError) + assert safe_rl(S(1)) == S(1) + + +def test_do_one(): + rl = do_one(posdec, posdec) + assert rl(5) == 4 + + def rl1(x: int) -> int: + if x == 1: + return 2 + return x + + def rl2(x: int) -> int: + if x == 2: + return 3 + return x + + rule = do_one(rl1, rl2) + assert rule(1) == 2 + assert rule(rule(1)) == 3 + + +def test_debug(): + file = StringIO() + rl = debug(posdec, file) + rl(5) + log = file.getvalue() + file.close() + + assert posdec.__name__ in log + assert '5' in log + assert '4' in log + + +def test_switch(): + def key(x: int) -> int: + return x % 3 + + rl = switch(key, {0: inc, 1: dec}) + assert rl(3) == 4 + assert rl(4) == 3 + assert rl(5) == 5 + + +def test_minimize(): + def key(x: int) -> int: + return -x + + rl = minimize(inc, dec) + assert rl(4) == 3 + + rl = minimize(inc, dec, objective=key) + assert rl(4) == 5 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tests/test_rl.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tests/test_rl.py new file mode 100644 index 0000000000000000000000000000000000000000..8bfa90ad4d970b21396e0e1b6427b5a5c68fe381 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tests/test_rl.py @@ -0,0 +1,78 @@ +from sympy.core.singleton import S +from sympy.strategies.rl import ( + rm_id, glom, flatten, unpack, sort, distribute, subs, rebuild) +from sympy.core.basic import Basic +from sympy.core.add import Add +from sympy.core.mul import Mul +from sympy.core.symbol import symbols +from sympy.abc import x + + +def test_rm_id(): + rmzeros = rm_id(lambda x: x == 0) + assert rmzeros(Basic(S(0), S(1))) == Basic(S(1)) + assert rmzeros(Basic(S(0), S(0))) == Basic(S(0)) + assert rmzeros(Basic(S(2), S(1))) == Basic(S(2), S(1)) + + +def test_glom(): + def key(x): + return x.as_coeff_Mul()[1] + + def count(x): + return x.as_coeff_Mul()[0] + + def newargs(cnt, arg): + return cnt * arg + + rl = glom(key, count, newargs) + + result = rl(Add(x, -x, 3 * x, 2, 3, evaluate=False)) + expected = Add(3 * x, 5) + assert set(result.args) == set(expected.args) + + +def test_flatten(): + assert flatten(Basic(S(1), S(2), Basic(S(3), S(4)))) == \ + Basic(S(1), S(2), S(3), S(4)) + + +def test_unpack(): + assert unpack(Basic(S(2))) == 2 + assert unpack(Basic(S(2), S(3))) == Basic(S(2), S(3)) + + +def test_sort(): + assert sort(str)(Basic(S(3), S(1), S(2))) == Basic(S(1), S(2), S(3)) + + +def test_distribute(): + class T1(Basic): + pass + + class T2(Basic): + pass + + distribute_t12 = distribute(T1, T2) + assert distribute_t12(T1(S(1), S(2), T2(S(3), S(4)), S(5))) == \ + T2(T1(S(1), S(2), S(3), S(5)), T1(S(1), S(2), S(4), S(5))) + assert distribute_t12(T1(S(1), S(2), S(3))) == T1(S(1), S(2), S(3)) + + +def test_distribute_add_mul(): + x, y = symbols('x, y') + expr = Mul(2, Add(x, y), evaluate=False) + expected = Add(Mul(2, x), Mul(2, y)) + distribute_mul = distribute(Mul, Add) + assert distribute_mul(expr) == expected + + +def test_subs(): + rl = subs(1, 2) + assert rl(1) == 2 + assert rl(3) == 3 + + +def test_rebuild(): + expr = Basic.__new__(Add, S(1), S(2)) + assert rebuild(expr) == 3 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tests/test_tools.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tests/test_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..89774aeb92ead5781966e4f48ad32dc63e1bf7e2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tests/test_tools.py @@ -0,0 +1,32 @@ +from sympy.strategies.tools import subs, typed +from sympy.strategies.rl import rm_id +from sympy.core.basic import Basic +from sympy.core.singleton import S + + +def test_subs(): + from sympy.core.symbol import symbols + a, b, c, d, e, f = symbols('a,b,c,d,e,f') + mapping = {a: d, d: a, Basic(e): Basic(f)} + expr = Basic(a, Basic(b, c), Basic(d, Basic(e))) + result = Basic(d, Basic(b, c), Basic(a, Basic(f))) + assert subs(mapping)(expr) == result + + +def test_subs_empty(): + assert subs({})(Basic(S(1), S(2))) == Basic(S(1), S(2)) + + +def test_typed(): + class A(Basic): + pass + + class B(Basic): + pass + + rmzeros = rm_id(lambda x: x == S(0)) + rmones = rm_id(lambda x: x == S(1)) + remove_something = typed({A: rmzeros, B: rmones}) + + assert remove_something(A(S(0), S(1))) == A(S(1)) + assert remove_something(B(S(0), S(1))) == B(S(0)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tests/test_traverse.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tests/test_traverse.py new file mode 100644 index 0000000000000000000000000000000000000000..ee2409616a8b4f750af8baea149b2ea52c56be9d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tests/test_traverse.py @@ -0,0 +1,84 @@ +from sympy.strategies.traverse import ( + top_down, bottom_up, sall, top_down_once, bottom_up_once, basic_fns) +from sympy.strategies.rl import rebuild +from sympy.strategies.util import expr_fns +from sympy.core.add import Add +from sympy.core.basic import Basic +from sympy.core.numbers import Integer +from sympy.core.singleton import S +from sympy.core.symbol import Str, Symbol +from sympy.abc import x, y, z + + +def zero_symbols(expression): + return S.Zero if isinstance(expression, Symbol) else expression + + +def test_sall(): + zero_onelevel = sall(zero_symbols) + + assert zero_onelevel(Basic(x, y, Basic(x, z))) == \ + Basic(S(0), S(0), Basic(x, z)) + + +def test_bottom_up(): + _test_global_traversal(bottom_up) + _test_stop_on_non_basics(bottom_up) + + +def test_top_down(): + _test_global_traversal(top_down) + _test_stop_on_non_basics(top_down) + + +def _test_global_traversal(trav): + zero_all_symbols = trav(zero_symbols) + + assert zero_all_symbols(Basic(x, y, Basic(x, z))) == \ + Basic(S(0), S(0), Basic(S(0), S(0))) + + +def _test_stop_on_non_basics(trav): + def add_one_if_can(expr): + try: + return expr + 1 + except TypeError: + return expr + + expr = Basic(S(1), Str('a'), Basic(S(2), Str('b'))) + expected = Basic(S(2), Str('a'), Basic(S(3), Str('b'))) + rl = trav(add_one_if_can) + + assert rl(expr) == expected + + +class Basic2(Basic): + pass + + +def rl(x): + if x.args and not isinstance(x.args[0], Integer): + return Basic2(*x.args) + return x + + +def test_top_down_once(): + top_rl = top_down_once(rl) + + assert top_rl(Basic(S(1.0), S(2.0), Basic(S(3), S(4)))) == \ + Basic2(S(1.0), S(2.0), Basic(S(3), S(4))) + + +def test_bottom_up_once(): + bottom_rl = bottom_up_once(rl) + + assert bottom_rl(Basic(S(1), S(2), Basic(S(3.0), S(4.0)))) == \ + Basic(S(1), S(2), Basic2(S(3.0), S(4.0))) + + +def test_expr_fns(): + expr = x + y**3 + e = bottom_up(lambda v: v + 1, expr_fns)(expr) + b = bottom_up(lambda v: Basic.__new__(Add, v, S(1)), basic_fns)(expr) + + assert rebuild(b) == e diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tests/test_tree.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tests/test_tree.py new file mode 100644 index 0000000000000000000000000000000000000000..d5cdde747fe3ab90c8fd181701194403bc526067 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tests/test_tree.py @@ -0,0 +1,92 @@ +from sympy.strategies.tree import treeapply, greedy, allresults, brute +from functools import partial, reduce + + +def inc(x): + return x + 1 + + +def dec(x): + return x - 1 + + +def double(x): + return 2 * x + + +def square(x): + return x**2 + + +def add(*args): + return sum(args) + + +def mul(*args): + return reduce(lambda a, b: a * b, args, 1) + + +def test_treeapply(): + tree = ([3, 3], [4, 1], 2) + assert treeapply(tree, {list: min, tuple: max}) == 3 + assert treeapply(tree, {list: add, tuple: mul}) == 60 + + +def test_treeapply_leaf(): + assert treeapply(3, {}, leaf=lambda x: x**2) == 9 + tree = ([3, 3], [4, 1], 2) + treep1 = ([4, 4], [5, 2], 3) + assert treeapply(tree, {list: min, tuple: max}, leaf=lambda x: x + 1) == \ + treeapply(treep1, {list: min, tuple: max}) + + +def test_treeapply_strategies(): + from sympy.strategies import chain, minimize + join = {list: chain, tuple: minimize} + + assert treeapply(inc, join) == inc + assert treeapply((inc, dec), join)(5) == minimize(inc, dec)(5) + assert treeapply([inc, dec], join)(5) == chain(inc, dec)(5) + tree = (inc, [dec, double]) # either inc or dec-then-double + assert treeapply(tree, join)(5) == 6 + assert treeapply(tree, join)(1) == 0 + + maximize = partial(minimize, objective=lambda x: -x) + join = {list: chain, tuple: maximize} + fn = treeapply(tree, join) + assert fn(4) == 6 # highest value comes from the dec then double + assert fn(1) == 2 # highest value comes from the inc + + +def test_greedy(): + tree = [inc, (dec, double)] # either inc or dec-then-double + + fn = greedy(tree, objective=lambda x: -x) + assert fn(4) == 6 # highest value comes from the dec then double + assert fn(1) == 2 # highest value comes from the inc + + tree = [inc, dec, [inc, dec, [(inc, inc), (dec, dec)]]] + lowest = greedy(tree) + assert lowest(10) == 8 + + highest = greedy(tree, objective=lambda x: -x) + assert highest(10) == 12 + + +def test_allresults(): + # square = lambda x: x**2 + + assert set(allresults(inc)(3)) == {inc(3)} + assert set(allresults([inc, dec])(3)) == {2, 4} + assert set(allresults((inc, dec))(3)) == {3} + assert set(allresults([inc, (dec, double)])(4)) == {5, 6} + + +def test_brute(): + tree = ([inc, dec], square) + fn = brute(tree, lambda x: -x) + + assert fn(2) == (2 + 1)**2 + assert fn(-2) == (-2 - 1)**2 + + assert brute(inc)(1) == 2 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tools.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tools.py new file mode 100644 index 0000000000000000000000000000000000000000..e6a94c16db57206d7c83c8a5e13930c4cffdde47 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tools.py @@ -0,0 +1,53 @@ +from . import rl +from .core import do_one, exhaust, switch +from .traverse import top_down + + +def subs(d, **kwargs): + """ Full simultaneous exact substitution. + + Examples + ======== + + >>> from sympy.strategies.tools import subs + >>> from sympy import Basic, S + >>> mapping = {S(1): S(4), S(4): S(1), Basic(S(5)): Basic(S(6), S(7))} + >>> expr = Basic(S(1), Basic(S(2), S(3)), Basic(S(4), Basic(S(5)))) + >>> subs(mapping)(expr) + Basic(4, Basic(2, 3), Basic(1, Basic(6, 7))) + """ + if d: + return top_down(do_one(*map(rl.subs, *zip(*d.items()))), **kwargs) + else: + return lambda x: x + + +def canon(*rules, **kwargs): + """ Strategy for canonicalization. + + Explanation + =========== + + Apply each rule in a bottom_up fashion through the tree. + Do each one in turn. + Keep doing this until there is no change. + """ + return exhaust(top_down(exhaust(do_one(*rules)), **kwargs)) + + +def typed(ruletypes): + """ Apply rules based on the expression type + + inputs: + ruletypes -- a dict mapping {Type: rule} + + Examples + ======== + + >>> from sympy.strategies import rm_id, typed + >>> from sympy import Add, Mul + >>> rm_zeros = rm_id(lambda x: x==0) + >>> rm_ones = rm_id(lambda x: x==1) + >>> remove_idents = typed({Add: rm_zeros, Mul: rm_ones}) + """ + return switch(type, ruletypes) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/traverse.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/traverse.py new file mode 100644 index 0000000000000000000000000000000000000000..869361f443742b5b7346c9c970f103b955e8473e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/traverse.py @@ -0,0 +1,37 @@ +"""Strategies to Traverse a Tree.""" +from sympy.strategies.util import basic_fns +from sympy.strategies.core import chain, do_one + + +def top_down(rule, fns=basic_fns): + """Apply a rule down a tree running it on the top nodes first.""" + return chain(rule, lambda expr: sall(top_down(rule, fns), fns)(expr)) + + +def bottom_up(rule, fns=basic_fns): + """Apply a rule down a tree running it on the bottom nodes first.""" + return chain(lambda expr: sall(bottom_up(rule, fns), fns)(expr), rule) + + +def top_down_once(rule, fns=basic_fns): + """Apply a rule down a tree - stop on success.""" + return do_one(rule, lambda expr: sall(top_down(rule, fns), fns)(expr)) + + +def bottom_up_once(rule, fns=basic_fns): + """Apply a rule up a tree - stop on success.""" + return do_one(lambda expr: sall(bottom_up(rule, fns), fns)(expr), rule) + + +def sall(rule, fns=basic_fns): + """Strategic all - apply rule to args.""" + op, new, children, leaf = map(fns.get, ('op', 'new', 'children', 'leaf')) + + def all_rl(expr): + if leaf(expr): + return expr + else: + args = map(rule, children(expr)) + return new(op(expr), *args) + + return all_rl diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tree.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tree.py new file mode 100644 index 0000000000000000000000000000000000000000..c2006fde4fc5d09f3d38baae4d7335b4cbd971b7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/tree.py @@ -0,0 +1,139 @@ +from functools import partial +from sympy.strategies import chain, minimize +from sympy.strategies.core import identity +import sympy.strategies.branch as branch +from sympy.strategies.branch import yieldify + + +def treeapply(tree, join, leaf=identity): + """ Apply functions onto recursive containers (tree). + + Explanation + =========== + + join - a dictionary mapping container types to functions + e.g. ``{list: minimize, tuple: chain}`` + + Keys are containers/iterables. Values are functions [a] -> a. + + Examples + ======== + + >>> from sympy.strategies.tree import treeapply + >>> tree = [(3, 2), (4, 1)] + >>> treeapply(tree, {list: max, tuple: min}) + 2 + + >>> add = lambda *args: sum(args) + >>> def mul(*args): + ... total = 1 + ... for arg in args: + ... total *= arg + ... return total + >>> treeapply(tree, {list: mul, tuple: add}) + 25 + """ + for typ in join: + if isinstance(tree, typ): + return join[typ](*map(partial(treeapply, join=join, leaf=leaf), + tree)) + return leaf(tree) + + +def greedy(tree, objective=identity, **kwargs): + """ Execute a strategic tree. Select alternatives greedily + + Trees + ----- + + Nodes in a tree can be either + + function - a leaf + list - a selection among operations + tuple - a sequence of chained operations + + Textual examples + ---------------- + + Text: Run f, then run g, e.g. ``lambda x: g(f(x))`` + Code: ``(f, g)`` + + Text: Run either f or g, whichever minimizes the objective + Code: ``[f, g]`` + + Textx: Run either f or g, whichever is better, then run h + Code: ``([f, g], h)`` + + Text: Either expand then simplify or try factor then foosimp. Finally print + Code: ``([(expand, simplify), (factor, foosimp)], print)`` + + Objective + --------- + + "Better" is determined by the objective keyword. This function makes + choices to minimize the objective. It defaults to the identity. + + Examples + ======== + + >>> from sympy.strategies.tree import greedy + >>> inc = lambda x: x + 1 + >>> dec = lambda x: x - 1 + >>> double = lambda x: 2*x + + >>> tree = [inc, (dec, double)] # either inc or dec-then-double + >>> fn = greedy(tree) + >>> fn(4) # lowest value comes from the inc + 5 + >>> fn(1) # lowest value comes from dec then double + 0 + + This function selects between options in a tuple. The result is chosen + that minimizes the objective function. + + >>> fn = greedy(tree, objective=lambda x: -x) # maximize + >>> fn(4) # highest value comes from the dec then double + 6 + >>> fn(1) # highest value comes from the inc + 2 + + Greediness + ---------- + + This is a greedy algorithm. In the example: + + ([a, b], c) # do either a or b, then do c + + the choice between running ``a`` or ``b`` is made without foresight to c + """ + optimize = partial(minimize, objective=objective) + return treeapply(tree, {list: optimize, tuple: chain}, **kwargs) + + +def allresults(tree, leaf=yieldify): + """ Execute a strategic tree. Return all possibilities. + + Returns a lazy iterator of all possible results + + Exhaustiveness + -------------- + + This is an exhaustive algorithm. In the example + + ([a, b], [c, d]) + + All of the results from + + (a, c), (b, c), (a, d), (b, d) + + are returned. This can lead to combinatorial blowup. + + See sympy.strategies.greedy for details on input + """ + return treeapply(tree, {list: branch.multiplex, tuple: branch.chain}, + leaf=leaf) + + +def brute(tree, objective=identity, **kwargs): + return lambda expr: min(tuple(allresults(tree, **kwargs)(expr)), + key=objective) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/util.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/util.py new file mode 100644 index 0000000000000000000000000000000000000000..13aab5f6a49650c5ded9cd913c23c6682f18d40a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/strategies/util.py @@ -0,0 +1,17 @@ +from sympy.core.basic import Basic + +new = Basic.__new__ + + +def assoc(d, k, v): + d = d.copy() + d[k] = v + return d + + +basic_fns = {'op': type, + 'new': Basic.__new__, + 'leaf': lambda x: not isinstance(x, Basic) or x.is_Atom, + 'children': lambda x: x.args} + +expr_fns = assoc(basic_fns, 'new', lambda op, *args: op(*args)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a832614b1d48e26bf01e16f040f34dd412e8e32b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/__init__.py @@ -0,0 +1,23 @@ +"""A module to manipulate symbolic objects with indices including tensors + +""" +from .indexed import IndexedBase, Idx, Indexed +from .index_methods import get_contraction_structure, get_indices +from .functions import shape +from .array import (MutableDenseNDimArray, ImmutableDenseNDimArray, + MutableSparseNDimArray, ImmutableSparseNDimArray, NDimArray, tensorproduct, + tensorcontraction, tensordiagonal, derive_by_array, permutedims, Array, + DenseNDimArray, SparseNDimArray,) + +__all__ = [ + 'IndexedBase', 'Idx', 'Indexed', + + 'get_contraction_structure', 'get_indices', + + 'shape', + + 'MutableDenseNDimArray', 'ImmutableDenseNDimArray', + 'MutableSparseNDimArray', 'ImmutableSparseNDimArray', 'NDimArray', + 'tensorproduct', 'tensorcontraction', 'tensordiagonal', 'derive_by_array', 'permutedims', + 'Array', 'DenseNDimArray', 'SparseNDimArray', +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..eca2eb4c6c58cb113517b6e41737e9d97abbb84e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/__init__.py @@ -0,0 +1,271 @@ +r""" +N-dim array module for SymPy. + +Four classes are provided to handle N-dim arrays, given by the combinations +dense/sparse (i.e. whether to store all elements or only the non-zero ones in +memory) and mutable/immutable (immutable classes are SymPy objects, but cannot +change after they have been created). + +Examples +======== + +The following examples show the usage of ``Array``. This is an abbreviation for +``ImmutableDenseNDimArray``, that is an immutable and dense N-dim array, the +other classes are analogous. For mutable classes it is also possible to change +element values after the object has been constructed. + +Array construction can detect the shape of nested lists and tuples: + +>>> from sympy import Array +>>> a1 = Array([[1, 2], [3, 4], [5, 6]]) +>>> a1 +[[1, 2], [3, 4], [5, 6]] +>>> a1.shape +(3, 2) +>>> a1.rank() +2 +>>> from sympy.abc import x, y, z +>>> a2 = Array([[[x, y], [z, x*z]], [[1, x*y], [1/x, x/y]]]) +>>> a2 +[[[x, y], [z, x*z]], [[1, x*y], [1/x, x/y]]] +>>> a2.shape +(2, 2, 2) +>>> a2.rank() +3 + +Otherwise one could pass a 1-dim array followed by a shape tuple: + +>>> m1 = Array(range(12), (3, 4)) +>>> m1 +[[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]] +>>> m2 = Array(range(12), (3, 2, 2)) +>>> m2 +[[[0, 1], [2, 3]], [[4, 5], [6, 7]], [[8, 9], [10, 11]]] +>>> m2[1,1,1] +7 +>>> m2.reshape(4, 3) +[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]] + +Slice support: + +>>> m2[:, 1, 1] +[3, 7, 11] + +Elementwise derivative: + +>>> from sympy.abc import x, y, z +>>> m3 = Array([x**3, x*y, z]) +>>> m3.diff(x) +[3*x**2, y, 0] +>>> m3.diff(z) +[0, 0, 1] + +Multiplication with other SymPy expressions is applied elementwisely: + +>>> (1+x)*m3 +[x**3*(x + 1), x*y*(x + 1), z*(x + 1)] + +To apply a function to each element of the N-dim array, use ``applyfunc``: + +>>> m3.applyfunc(lambda x: x/2) +[x**3/2, x*y/2, z/2] + +N-dim arrays can be converted to nested lists by the ``tolist()`` method: + +>>> m2.tolist() +[[[0, 1], [2, 3]], [[4, 5], [6, 7]], [[8, 9], [10, 11]]] +>>> isinstance(m2.tolist(), list) +True + +If the rank is 2, it is possible to convert them to matrices with ``tomatrix()``: + +>>> m1.tomatrix() +Matrix([ +[0, 1, 2, 3], +[4, 5, 6, 7], +[8, 9, 10, 11]]) + +Products and contractions +------------------------- + +Tensor product between arrays `A_{i_1,\ldots,i_n}` and `B_{j_1,\ldots,j_m}` +creates the combined array `P = A \otimes B` defined as + +`P_{i_1,\ldots,i_n,j_1,\ldots,j_m} := A_{i_1,\ldots,i_n}\cdot B_{j_1,\ldots,j_m}.` + +It is available through ``tensorproduct(...)``: + +>>> from sympy import Array, tensorproduct +>>> from sympy.abc import x,y,z,t +>>> A = Array([x, y, z, t]) +>>> B = Array([1, 2, 3, 4]) +>>> tensorproduct(A, B) +[[x, 2*x, 3*x, 4*x], [y, 2*y, 3*y, 4*y], [z, 2*z, 3*z, 4*z], [t, 2*t, 3*t, 4*t]] + +In case you don't want to evaluate the tensor product immediately, you can use +``ArrayTensorProduct``, which creates an unevaluated tensor product expression: + +>>> from sympy.tensor.array.expressions import ArrayTensorProduct +>>> ArrayTensorProduct(A, B) +ArrayTensorProduct([x, y, z, t], [1, 2, 3, 4]) + +Calling ``.as_explicit()`` on ``ArrayTensorProduct`` is equivalent to just calling +``tensorproduct(...)``: + +>>> ArrayTensorProduct(A, B).as_explicit() +[[x, 2*x, 3*x, 4*x], [y, 2*y, 3*y, 4*y], [z, 2*z, 3*z, 4*z], [t, 2*t, 3*t, 4*t]] + +Tensor product between a rank-1 array and a matrix creates a rank-3 array: + +>>> from sympy import eye +>>> p1 = tensorproduct(A, eye(4)) +>>> p1 +[[[x, 0, 0, 0], [0, x, 0, 0], [0, 0, x, 0], [0, 0, 0, x]], [[y, 0, 0, 0], [0, y, 0, 0], [0, 0, y, 0], [0, 0, 0, y]], [[z, 0, 0, 0], [0, z, 0, 0], [0, 0, z, 0], [0, 0, 0, z]], [[t, 0, 0, 0], [0, t, 0, 0], [0, 0, t, 0], [0, 0, 0, t]]] + +Now, to get back `A_0 \otimes \mathbf{1}` one can access `p_{0,m,n}` by slicing: + +>>> p1[0,:,:] +[[x, 0, 0, 0], [0, x, 0, 0], [0, 0, x, 0], [0, 0, 0, x]] + +Tensor contraction sums over the specified axes, for example contracting +positions `a` and `b` means + +`A_{i_1,\ldots,i_a,\ldots,i_b,\ldots,i_n} \implies \sum_k A_{i_1,\ldots,k,\ldots,k,\ldots,i_n}` + +Remember that Python indexing is zero starting, to contract the a-th and b-th +axes it is therefore necessary to specify `a-1` and `b-1` + +>>> from sympy import tensorcontraction +>>> C = Array([[x, y], [z, t]]) + +The matrix trace is equivalent to the contraction of a rank-2 array: + +`A_{m,n} \implies \sum_k A_{k,k}` + +>>> tensorcontraction(C, (0, 1)) +t + x + +To create an expression representing a tensor contraction that does not get +evaluated immediately, use ``ArrayContraction``, which is equivalent to +``tensorcontraction(...)`` if it is followed by ``.as_explicit()``: + +>>> from sympy.tensor.array.expressions import ArrayContraction +>>> ArrayContraction(C, (0, 1)) +ArrayContraction([[x, y], [z, t]], (0, 1)) +>>> ArrayContraction(C, (0, 1)).as_explicit() +t + x + +Matrix product is equivalent to a tensor product of two rank-2 arrays, followed +by a contraction of the 2nd and 3rd axes (in Python indexing axes number 1, 2). + +`A_{m,n}\cdot B_{i,j} \implies \sum_k A_{m, k}\cdot B_{k, j}` + +>>> D = Array([[2, 1], [0, -1]]) +>>> tensorcontraction(tensorproduct(C, D), (1, 2)) +[[2*x, x - y], [2*z, -t + z]] + +One may verify that the matrix product is equivalent: + +>>> from sympy import Matrix +>>> Matrix([[x, y], [z, t]])*Matrix([[2, 1], [0, -1]]) +Matrix([ +[2*x, x - y], +[2*z, -t + z]]) + +or equivalently + +>>> C.tomatrix()*D.tomatrix() +Matrix([ +[2*x, x - y], +[2*z, -t + z]]) + +Diagonal operator +----------------- + +The ``tensordiagonal`` function acts in a similar manner as ``tensorcontraction``, +but the joined indices are not summed over, for example diagonalizing +positions `a` and `b` means + +`A_{i_1,\ldots,i_a,\ldots,i_b,\ldots,i_n} \implies A_{i_1,\ldots,k,\ldots,k,\ldots,i_n} +\implies \tilde{A}_{i_1,\ldots,i_{a-1},i_{a+1},\ldots,i_{b-1},i_{b+1},\ldots,i_n,k}` + +where `\tilde{A}` is the array equivalent to the diagonal of `A` at positions +`a` and `b` moved to the last index slot. + +Compare the difference between contraction and diagonal operators: + +>>> from sympy import tensordiagonal +>>> from sympy.abc import a, b, c, d +>>> m = Matrix([[a, b], [c, d]]) +>>> tensorcontraction(m, [0, 1]) +a + d +>>> tensordiagonal(m, [0, 1]) +[a, d] + +In short, no summation occurs with ``tensordiagonal``. + + +Derivatives by array +-------------------- + +The usual derivative operation may be extended to support derivation with +respect to arrays, provided that all elements in the that array are symbols or +expressions suitable for derivations. + +The definition of a derivative by an array is as follows: given the array +`A_{i_1, \ldots, i_N}` and the array `X_{j_1, \ldots, j_M}` +the derivative of arrays will return a new array `B` defined by + +`B_{j_1,\ldots,j_M,i_1,\ldots,i_N} := \frac{\partial A_{i_1,\ldots,i_N}}{\partial X_{j_1,\ldots,j_M}}` + +The function ``derive_by_array`` performs such an operation: + +>>> from sympy import derive_by_array +>>> from sympy.abc import x, y, z, t +>>> from sympy import sin, exp + +With scalars, it behaves exactly as the ordinary derivative: + +>>> derive_by_array(sin(x*y), x) +y*cos(x*y) + +Scalar derived by an array basis: + +>>> derive_by_array(sin(x*y), [x, y, z]) +[y*cos(x*y), x*cos(x*y), 0] + +Deriving array by an array basis: `B^{nm} := \frac{\partial A^m}{\partial x^n}` + +>>> basis = [x, y, z] +>>> ax = derive_by_array([exp(x), sin(y*z), t], basis) +>>> ax +[[exp(x), 0, 0], [0, z*cos(y*z), 0], [0, y*cos(y*z), 0]] + +Contraction of the resulting array: `\sum_m \frac{\partial A^m}{\partial x^m}` + +>>> tensorcontraction(ax, (0, 1)) +z*cos(y*z) + exp(x) + +""" + +from .dense_ndim_array import MutableDenseNDimArray, ImmutableDenseNDimArray, DenseNDimArray +from .sparse_ndim_array import MutableSparseNDimArray, ImmutableSparseNDimArray, SparseNDimArray +from .ndim_array import NDimArray, ArrayKind +from .arrayop import tensorproduct, tensorcontraction, tensordiagonal, derive_by_array, permutedims +from .array_comprehension import ArrayComprehension, ArrayComprehensionMap + +Array = ImmutableDenseNDimArray + +__all__ = [ + 'MutableDenseNDimArray', 'ImmutableDenseNDimArray', 'DenseNDimArray', + + 'MutableSparseNDimArray', 'ImmutableSparseNDimArray', 'SparseNDimArray', + + 'NDimArray', 'ArrayKind', + + 'tensorproduct', 'tensorcontraction', 'tensordiagonal', 'derive_by_array', + + 'permutedims', 'ArrayComprehension', 'ArrayComprehensionMap', + + 'Array', +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/array_comprehension.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/array_comprehension.py new file mode 100644 index 0000000000000000000000000000000000000000..95702f499f3e40597fd0144929138ac1329962ee --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/array_comprehension.py @@ -0,0 +1,399 @@ +import functools, itertools +from sympy.core.sympify import _sympify, sympify +from sympy.core.expr import Expr +from sympy.core import Basic, Tuple +from sympy.tensor.array import ImmutableDenseNDimArray +from sympy.core.symbol import Symbol +from sympy.core.numbers import Integer + + +class ArrayComprehension(Basic): + """ + Generate a list comprehension. + + Explanation + =========== + + If there is a symbolic dimension, for example, say [i for i in range(1, N)] where + N is a Symbol, then the expression will not be expanded to an array. Otherwise, + calling the doit() function will launch the expansion. + + Examples + ======== + + >>> from sympy.tensor.array import ArrayComprehension + >>> from sympy import symbols + >>> i, j, k = symbols('i j k') + >>> a = ArrayComprehension(10*i + j, (i, 1, 4), (j, 1, 3)) + >>> a + ArrayComprehension(10*i + j, (i, 1, 4), (j, 1, 3)) + >>> a.doit() + [[11, 12, 13], [21, 22, 23], [31, 32, 33], [41, 42, 43]] + >>> b = ArrayComprehension(10*i + j, (i, 1, 4), (j, 1, k)) + >>> b.doit() + ArrayComprehension(10*i + j, (i, 1, 4), (j, 1, k)) + """ + def __new__(cls, function, *symbols, **assumptions): + if any(len(l) != 3 or None for l in symbols): + raise ValueError('ArrayComprehension requires values lower and upper bound' + ' for the expression') + arglist = [sympify(function)] + arglist.extend(cls._check_limits_validity(function, symbols)) + obj = Basic.__new__(cls, *arglist, **assumptions) + obj._limits = obj._args[1:] + obj._shape = cls._calculate_shape_from_limits(obj._limits) + obj._rank = len(obj._shape) + obj._loop_size = cls._calculate_loop_size(obj._shape) + return obj + + @property + def function(self): + """The function applied across limits. + + Examples + ======== + + >>> from sympy.tensor.array import ArrayComprehension + >>> from sympy import symbols + >>> i, j = symbols('i j') + >>> a = ArrayComprehension(10*i + j, (i, 1, 4), (j, 1, 3)) + >>> a.function + 10*i + j + """ + return self._args[0] + + @property + def limits(self): + """ + The list of limits that will be applied while expanding the array. + + Examples + ======== + + >>> from sympy.tensor.array import ArrayComprehension + >>> from sympy import symbols + >>> i, j = symbols('i j') + >>> a = ArrayComprehension(10*i + j, (i, 1, 4), (j, 1, 3)) + >>> a.limits + ((i, 1, 4), (j, 1, 3)) + """ + return self._limits + + @property + def free_symbols(self): + """ + The set of the free_symbols in the array. + Variables appeared in the bounds are supposed to be excluded + from the free symbol set. + + Examples + ======== + + >>> from sympy.tensor.array import ArrayComprehension + >>> from sympy import symbols + >>> i, j, k = symbols('i j k') + >>> a = ArrayComprehension(10*i + j, (i, 1, 4), (j, 1, 3)) + >>> a.free_symbols + set() + >>> b = ArrayComprehension(10*i + j, (i, 1, 4), (j, 1, k+3)) + >>> b.free_symbols + {k} + """ + expr_free_sym = self.function.free_symbols + for var, inf, sup in self._limits: + expr_free_sym.discard(var) + curr_free_syms = inf.free_symbols.union(sup.free_symbols) + expr_free_sym = expr_free_sym.union(curr_free_syms) + return expr_free_sym + + @property + def variables(self): + """The tuples of the variables in the limits. + + Examples + ======== + + >>> from sympy.tensor.array import ArrayComprehension + >>> from sympy import symbols + >>> i, j, k = symbols('i j k') + >>> a = ArrayComprehension(10*i + j, (i, 1, 4), (j, 1, 3)) + >>> a.variables + [i, j] + """ + return [l[0] for l in self._limits] + + @property + def bound_symbols(self): + """The list of dummy variables. + + Note + ==== + + Note that all variables are dummy variables since a limit without + lower bound or upper bound is not accepted. + """ + return [l[0] for l in self._limits if len(l) != 1] + + @property + def shape(self): + """ + The shape of the expanded array, which may have symbols. + + Note + ==== + + Both the lower and the upper bounds are included while + calculating the shape. + + Examples + ======== + + >>> from sympy.tensor.array import ArrayComprehension + >>> from sympy import symbols + >>> i, j, k = symbols('i j k') + >>> a = ArrayComprehension(10*i + j, (i, 1, 4), (j, 1, 3)) + >>> a.shape + (4, 3) + >>> b = ArrayComprehension(10*i + j, (i, 1, 4), (j, 1, k+3)) + >>> b.shape + (4, k + 3) + """ + return self._shape + + @property + def is_shape_numeric(self): + """ + Test if the array is shape-numeric which means there is no symbolic + dimension. + + Examples + ======== + + >>> from sympy.tensor.array import ArrayComprehension + >>> from sympy import symbols + >>> i, j, k = symbols('i j k') + >>> a = ArrayComprehension(10*i + j, (i, 1, 4), (j, 1, 3)) + >>> a.is_shape_numeric + True + >>> b = ArrayComprehension(10*i + j, (i, 1, 4), (j, 1, k+3)) + >>> b.is_shape_numeric + False + """ + for _, inf, sup in self._limits: + if Basic(inf, sup).atoms(Symbol): + return False + return True + + def rank(self): + """The rank of the expanded array. + + Examples + ======== + + >>> from sympy.tensor.array import ArrayComprehension + >>> from sympy import symbols + >>> i, j, k = symbols('i j k') + >>> a = ArrayComprehension(10*i + j, (i, 1, 4), (j, 1, 3)) + >>> a.rank() + 2 + """ + return self._rank + + def __len__(self): + """ + The length of the expanded array which means the number + of elements in the array. + + Raises + ====== + + ValueError : When the length of the array is symbolic + + Examples + ======== + + >>> from sympy.tensor.array import ArrayComprehension + >>> from sympy import symbols + >>> i, j = symbols('i j') + >>> a = ArrayComprehension(10*i + j, (i, 1, 4), (j, 1, 3)) + >>> len(a) + 12 + """ + if self._loop_size.free_symbols: + raise ValueError('Symbolic length is not supported') + return self._loop_size + + @classmethod + def _check_limits_validity(cls, function, limits): + #limits = sympify(limits) + new_limits = [] + for var, inf, sup in limits: + var = _sympify(var) + inf = _sympify(inf) + #since this is stored as an argument, it should be + #a Tuple + if isinstance(sup, list): + sup = Tuple(*sup) + else: + sup = _sympify(sup) + new_limits.append(Tuple(var, inf, sup)) + if any((not isinstance(i, Expr)) or i.atoms(Symbol, Integer) != i.atoms() + for i in [inf, sup]): + raise TypeError('Bounds should be an Expression(combination of Integer and Symbol)') + if (inf > sup) == True: + raise ValueError('Lower bound should be inferior to upper bound') + if var in inf.free_symbols or var in sup.free_symbols: + raise ValueError('Variable should not be part of its bounds') + return new_limits + + @classmethod + def _calculate_shape_from_limits(cls, limits): + return tuple([sup - inf + 1 for _, inf, sup in limits]) + + @classmethod + def _calculate_loop_size(cls, shape): + if not shape: + return 0 + loop_size = 1 + for l in shape: + loop_size = loop_size * l + + return loop_size + + def doit(self, **hints): + if not self.is_shape_numeric: + return self + + return self._expand_array() + + def _expand_array(self): + res = [] + for values in itertools.product(*[range(inf, sup+1) + for var, inf, sup + in self._limits]): + res.append(self._get_element(values)) + + return ImmutableDenseNDimArray(res, self.shape) + + def _get_element(self, values): + temp = self.function + for var, val in zip(self.variables, values): + temp = temp.subs(var, val) + return temp + + def tolist(self): + """Transform the expanded array to a list. + + Raises + ====== + + ValueError : When there is a symbolic dimension + + Examples + ======== + + >>> from sympy.tensor.array import ArrayComprehension + >>> from sympy import symbols + >>> i, j = symbols('i j') + >>> a = ArrayComprehension(10*i + j, (i, 1, 4), (j, 1, 3)) + >>> a.tolist() + [[11, 12, 13], [21, 22, 23], [31, 32, 33], [41, 42, 43]] + """ + if self.is_shape_numeric: + return self._expand_array().tolist() + + raise ValueError("A symbolic array cannot be expanded to a list") + + def tomatrix(self): + """Transform the expanded array to a matrix. + + Raises + ====== + + ValueError : When there is a symbolic dimension + ValueError : When the rank of the expanded array is not equal to 2 + + Examples + ======== + + >>> from sympy.tensor.array import ArrayComprehension + >>> from sympy import symbols + >>> i, j = symbols('i j') + >>> a = ArrayComprehension(10*i + j, (i, 1, 4), (j, 1, 3)) + >>> a.tomatrix() + Matrix([ + [11, 12, 13], + [21, 22, 23], + [31, 32, 33], + [41, 42, 43]]) + """ + from sympy.matrices import Matrix + + if not self.is_shape_numeric: + raise ValueError("A symbolic array cannot be expanded to a matrix") + if self._rank != 2: + raise ValueError('Dimensions must be of size of 2') + + return Matrix(self._expand_array().tomatrix()) + + +def isLambda(v): + LAMBDA = lambda: 0 + return isinstance(v, type(LAMBDA)) and v.__name__ == LAMBDA.__name__ + +class ArrayComprehensionMap(ArrayComprehension): + ''' + A subclass of ArrayComprehension dedicated to map external function lambda. + + Notes + ===== + + Only the lambda function is considered. + At most one argument in lambda function is accepted in order to avoid ambiguity + in value assignment. + + Examples + ======== + + >>> from sympy.tensor.array import ArrayComprehensionMap + >>> from sympy import symbols + >>> i, j, k = symbols('i j k') + >>> a = ArrayComprehensionMap(lambda: 1, (i, 1, 4)) + >>> a.doit() + [1, 1, 1, 1] + >>> b = ArrayComprehensionMap(lambda a: a+1, (j, 1, 4)) + >>> b.doit() + [2, 3, 4, 5] + + ''' + def __new__(cls, function, *symbols, **assumptions): + if any(len(l) != 3 or None for l in symbols): + raise ValueError('ArrayComprehension requires values lower and upper bound' + ' for the expression') + + if not isLambda(function): + raise ValueError('Data type not supported') + + arglist = cls._check_limits_validity(function, symbols) + obj = Basic.__new__(cls, *arglist, **assumptions) + obj._limits = obj._args + obj._shape = cls._calculate_shape_from_limits(obj._limits) + obj._rank = len(obj._shape) + obj._loop_size = cls._calculate_loop_size(obj._shape) + obj._lambda = function + return obj + + @property + def func(self): + class _(ArrayComprehensionMap): + def __new__(cls, *args, **kwargs): + return ArrayComprehensionMap(self._lambda, *args, **kwargs) + return _ + + def _get_element(self, values): + temp = self._lambda + if self._lambda.__code__.co_argcount == 0: + temp = temp() + elif self._lambda.__code__.co_argcount == 1: + temp = temp(functools.reduce(lambda a, b: a*b, values)) + return temp diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/array_derivatives.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/array_derivatives.py new file mode 100644 index 0000000000000000000000000000000000000000..a38db6caefe256a8c7e1f3415b78351b3787fee9 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/array_derivatives.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +from sympy.core.expr import Expr +from sympy.core.function import Derivative +from sympy.core.numbers import Integer +from sympy.matrices.matrixbase import MatrixBase +from .ndim_array import NDimArray +from .arrayop import derive_by_array +from sympy.matrices.expressions.matexpr import MatrixExpr +from sympy.matrices.expressions.special import ZeroMatrix +from sympy.matrices.expressions.matexpr import _matrix_derivative + + +class ArrayDerivative(Derivative): + + is_scalar = False + + def __new__(cls, expr, *variables, **kwargs): + obj = super().__new__(cls, expr, *variables, **kwargs) + if isinstance(obj, ArrayDerivative): + obj._shape = obj._get_shape() + return obj + + def _get_shape(self): + shape = () + for v, count in self.variable_count: + if hasattr(v, "shape"): + for i in range(count): + shape += v.shape + if hasattr(self.expr, "shape"): + shape += self.expr.shape + return shape + + @property + def shape(self): + return self._shape + + @classmethod + def _get_zero_with_shape_like(cls, expr): + if isinstance(expr, (MatrixBase, NDimArray)): + return expr.zeros(*expr.shape) + elif isinstance(expr, MatrixExpr): + return ZeroMatrix(*expr.shape) + else: + raise RuntimeError("Unable to determine shape of array-derivative.") + + @staticmethod + def _call_derive_scalar_by_matrix(expr: Expr, v: MatrixBase) -> Expr: + return v.applyfunc(lambda x: expr.diff(x)) + + @staticmethod + def _call_derive_scalar_by_matexpr(expr: Expr, v: MatrixExpr) -> Expr: + if expr.has(v): + return _matrix_derivative(expr, v) + else: + return ZeroMatrix(*v.shape) + + @staticmethod + def _call_derive_scalar_by_array(expr: Expr, v: NDimArray) -> Expr: + return v.applyfunc(lambda x: expr.diff(x)) + + @staticmethod + def _call_derive_matrix_by_scalar(expr: MatrixBase, v: Expr) -> Expr: + return _matrix_derivative(expr, v) + + @staticmethod + def _call_derive_matexpr_by_scalar(expr: MatrixExpr, v: Expr) -> Expr: + return expr._eval_derivative(v) + + @staticmethod + def _call_derive_array_by_scalar(expr: NDimArray, v: Expr) -> Expr: + return expr.applyfunc(lambda x: x.diff(v)) + + @staticmethod + def _call_derive_default(expr: Expr, v: Expr) -> Expr | None: + if expr.has(v): + return _matrix_derivative(expr, v) + else: + return None + + @classmethod + def _dispatch_eval_derivative_n_times(cls, expr, v, count): + # Evaluate the derivative `n` times. If + # `_eval_derivative_n_times` is not overridden by the current + # object, the default in `Basic` will call a loop over + # `_eval_derivative`: + + if not isinstance(count, (int, Integer)) or ((count <= 0) == True): + return None + + # TODO: this could be done with multiple-dispatching: + if expr.is_scalar: + if isinstance(v, MatrixBase): + result = cls._call_derive_scalar_by_matrix(expr, v) + elif isinstance(v, MatrixExpr): + result = cls._call_derive_scalar_by_matexpr(expr, v) + elif isinstance(v, NDimArray): + result = cls._call_derive_scalar_by_array(expr, v) + elif v.is_scalar: + # scalar by scalar has a special + return super()._dispatch_eval_derivative_n_times(expr, v, count) + else: + return None + elif v.is_scalar: + if isinstance(expr, MatrixBase): + result = cls._call_derive_matrix_by_scalar(expr, v) + elif isinstance(expr, MatrixExpr): + result = cls._call_derive_matexpr_by_scalar(expr, v) + elif isinstance(expr, NDimArray): + result = cls._call_derive_array_by_scalar(expr, v) + else: + return None + else: + # Both `expr` and `v` are some array/matrix type: + if isinstance(expr, MatrixBase) or isinstance(v, MatrixBase): + result = derive_by_array(expr, v) + elif isinstance(expr, MatrixExpr) and isinstance(v, MatrixExpr): + result = cls._call_derive_default(expr, v) + elif isinstance(expr, MatrixExpr) or isinstance(v, MatrixExpr): + # if one expression is a symbolic matrix expression while the other isn't, don't evaluate: + return None + else: + result = derive_by_array(expr, v) + if result is None: + return None + if count == 1: + return result + else: + return cls._dispatch_eval_derivative_n_times(result, v, count - 1) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/arrayop.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/arrayop.py new file mode 100644 index 0000000000000000000000000000000000000000..a81e6b381a8a93f0cd585278a4be0259b06406dd --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/arrayop.py @@ -0,0 +1,528 @@ +import itertools +from collections.abc import Iterable + +from sympy.core._print_helpers import Printable +from sympy.core.containers import Tuple +from sympy.core.function import diff +from sympy.core.singleton import S +from sympy.core.sympify import _sympify + +from sympy.tensor.array.ndim_array import NDimArray +from sympy.tensor.array.dense_ndim_array import DenseNDimArray, ImmutableDenseNDimArray +from sympy.tensor.array.sparse_ndim_array import SparseNDimArray + + +def _arrayfy(a): + from sympy.matrices import MatrixBase + + if isinstance(a, NDimArray): + return a + if isinstance(a, (MatrixBase, list, tuple, Tuple)): + return ImmutableDenseNDimArray(a) + return a + + +def tensorproduct(*args): + """ + Tensor product among scalars or array-like objects. + + The equivalent operator for array expressions is ``ArrayTensorProduct``, + which can be used to keep the expression unevaluated. + + Examples + ======== + + >>> from sympy.tensor.array import tensorproduct, Array + >>> from sympy.abc import x, y, z, t + >>> A = Array([[1, 2], [3, 4]]) + >>> B = Array([x, y]) + >>> tensorproduct(A, B) + [[[x, y], [2*x, 2*y]], [[3*x, 3*y], [4*x, 4*y]]] + >>> tensorproduct(A, x) + [[x, 2*x], [3*x, 4*x]] + >>> tensorproduct(A, B, B) + [[[[x**2, x*y], [x*y, y**2]], [[2*x**2, 2*x*y], [2*x*y, 2*y**2]]], [[[3*x**2, 3*x*y], [3*x*y, 3*y**2]], [[4*x**2, 4*x*y], [4*x*y, 4*y**2]]]] + + Applying this function on two matrices will result in a rank 4 array. + + >>> from sympy import Matrix, eye + >>> m = Matrix([[x, y], [z, t]]) + >>> p = tensorproduct(eye(3), m) + >>> p + [[[[x, y], [z, t]], [[0, 0], [0, 0]], [[0, 0], [0, 0]]], [[[0, 0], [0, 0]], [[x, y], [z, t]], [[0, 0], [0, 0]]], [[[0, 0], [0, 0]], [[0, 0], [0, 0]], [[x, y], [z, t]]]] + + See Also + ======== + + sympy.tensor.array.expressions.array_expressions.ArrayTensorProduct + + """ + from sympy.tensor.array import SparseNDimArray, ImmutableSparseNDimArray + + if len(args) == 0: + return S.One + if len(args) == 1: + return _arrayfy(args[0]) + from sympy.tensor.array.expressions.array_expressions import _CodegenArrayAbstract + from sympy.tensor.array.expressions.array_expressions import ArrayTensorProduct + from sympy.tensor.array.expressions.array_expressions import _ArrayExpr + from sympy.matrices.expressions.matexpr import MatrixSymbol + if any(isinstance(arg, (_ArrayExpr, _CodegenArrayAbstract, MatrixSymbol)) for arg in args): + return ArrayTensorProduct(*args) + if len(args) > 2: + return tensorproduct(tensorproduct(args[0], args[1]), *args[2:]) + + # length of args is 2: + a, b = map(_arrayfy, args) + + if not isinstance(a, NDimArray) or not isinstance(b, NDimArray): + return a*b + + if isinstance(a, SparseNDimArray) and isinstance(b, SparseNDimArray): + lp = len(b) + new_array = {k1*lp + k2: v1*v2 for k1, v1 in a._sparse_array.items() for k2, v2 in b._sparse_array.items()} + return ImmutableSparseNDimArray(new_array, a.shape + b.shape) + + product_list = [i*j for i in Flatten(a) for j in Flatten(b)] + return ImmutableDenseNDimArray(product_list, a.shape + b.shape) + + +def _util_contraction_diagonal(array, *contraction_or_diagonal_axes): + array = _arrayfy(array) + + # Verify contraction_axes: + taken_dims = set() + for axes_group in contraction_or_diagonal_axes: + if not isinstance(axes_group, Iterable): + raise ValueError("collections of contraction/diagonal axes expected") + + dim = array.shape[axes_group[0]] + + for d in axes_group: + if d in taken_dims: + raise ValueError("dimension specified more than once") + if dim != array.shape[d]: + raise ValueError("cannot contract or diagonalize between axes of different dimension") + taken_dims.add(d) + + rank = array.rank() + + remaining_shape = [dim for i, dim in enumerate(array.shape) if i not in taken_dims] + cum_shape = [0]*rank + _cumul = 1 + for i in range(rank): + cum_shape[rank - i - 1] = _cumul + _cumul *= int(array.shape[rank - i - 1]) + + # DEFINITION: by absolute position it is meant the position along the one + # dimensional array containing all the tensor components. + + # Possible future work on this module: move computation of absolute + # positions to a class method. + + # Determine absolute positions of the uncontracted indices: + remaining_indices = [[cum_shape[i]*j for j in range(array.shape[i])] + for i in range(rank) if i not in taken_dims] + + # Determine absolute positions of the contracted indices: + summed_deltas = [] + for axes_group in contraction_or_diagonal_axes: + lidx = [] + for js in range(array.shape[axes_group[0]]): + lidx.append(sum(cum_shape[ig] * js for ig in axes_group)) + summed_deltas.append(lidx) + + return array, remaining_indices, remaining_shape, summed_deltas + + +def tensorcontraction(array, *contraction_axes): + """ + Contraction of an array-like object on the specified axes. + + The equivalent operator for array expressions is ``ArrayContraction``, + which can be used to keep the expression unevaluated. + + Examples + ======== + + >>> from sympy import Array, tensorcontraction + >>> from sympy import Matrix, eye + >>> tensorcontraction(eye(3), (0, 1)) + 3 + >>> A = Array(range(18), (3, 2, 3)) + >>> A + [[[0, 1, 2], [3, 4, 5]], [[6, 7, 8], [9, 10, 11]], [[12, 13, 14], [15, 16, 17]]] + >>> tensorcontraction(A, (0, 2)) + [21, 30] + + Matrix multiplication may be emulated with a proper combination of + ``tensorcontraction`` and ``tensorproduct`` + + >>> from sympy import tensorproduct + >>> from sympy.abc import a,b,c,d,e,f,g,h + >>> m1 = Matrix([[a, b], [c, d]]) + >>> m2 = Matrix([[e, f], [g, h]]) + >>> p = tensorproduct(m1, m2) + >>> p + [[[[a*e, a*f], [a*g, a*h]], [[b*e, b*f], [b*g, b*h]]], [[[c*e, c*f], [c*g, c*h]], [[d*e, d*f], [d*g, d*h]]]] + >>> tensorcontraction(p, (1, 2)) + [[a*e + b*g, a*f + b*h], [c*e + d*g, c*f + d*h]] + >>> m1*m2 + Matrix([ + [a*e + b*g, a*f + b*h], + [c*e + d*g, c*f + d*h]]) + + See Also + ======== + + sympy.tensor.array.expressions.array_expressions.ArrayContraction + + """ + from sympy.tensor.array.expressions.array_expressions import _array_contraction + from sympy.tensor.array.expressions.array_expressions import _CodegenArrayAbstract + from sympy.tensor.array.expressions.array_expressions import _ArrayExpr + from sympy.matrices.expressions.matexpr import MatrixSymbol + if isinstance(array, (_ArrayExpr, _CodegenArrayAbstract, MatrixSymbol)): + return _array_contraction(array, *contraction_axes) + + array, remaining_indices, remaining_shape, summed_deltas = _util_contraction_diagonal(array, *contraction_axes) + + # Compute the contracted array: + # + # 1. external for loops on all uncontracted indices. + # Uncontracted indices are determined by the combinatorial product of + # the absolute positions of the remaining indices. + # 2. internal loop on all contracted indices. + # It sums the values of the absolute contracted index and the absolute + # uncontracted index for the external loop. + contracted_array = [] + for icontrib in itertools.product(*remaining_indices): + index_base_position = sum(icontrib) + isum = S.Zero + for sum_to_index in itertools.product(*summed_deltas): + idx = array._get_tuple_index(index_base_position + sum(sum_to_index)) + isum += array[idx] + + contracted_array.append(isum) + + if len(remaining_indices) == 0: + assert len(contracted_array) == 1 + return contracted_array[0] + + return type(array)(contracted_array, remaining_shape) + + +def tensordiagonal(array, *diagonal_axes): + """ + Diagonalization of an array-like object on the specified axes. + + This is equivalent to multiplying the expression by Kronecker deltas + uniting the axes. + + The diagonal indices are put at the end of the axes. + + The equivalent operator for array expressions is ``ArrayDiagonal``, which + can be used to keep the expression unevaluated. + + Examples + ======== + + ``tensordiagonal`` acting on a 2-dimensional array by axes 0 and 1 is + equivalent to the diagonal of the matrix: + + >>> from sympy import Array, tensordiagonal + >>> from sympy import Matrix, eye + >>> tensordiagonal(eye(3), (0, 1)) + [1, 1, 1] + + >>> from sympy.abc import a,b,c,d + >>> m1 = Matrix([[a, b], [c, d]]) + >>> tensordiagonal(m1, [0, 1]) + [a, d] + + In case of higher dimensional arrays, the diagonalized out dimensions + are appended removed and appended as a single dimension at the end: + + >>> A = Array(range(18), (3, 2, 3)) + >>> A + [[[0, 1, 2], [3, 4, 5]], [[6, 7, 8], [9, 10, 11]], [[12, 13, 14], [15, 16, 17]]] + >>> tensordiagonal(A, (0, 2)) + [[0, 7, 14], [3, 10, 17]] + >>> from sympy import permutedims + >>> tensordiagonal(A, (0, 2)) == permutedims(Array([A[0, :, 0], A[1, :, 1], A[2, :, 2]]), [1, 0]) + True + + See Also + ======== + + sympy.tensor.array.expressions.array_expressions.ArrayDiagonal + + """ + if any(len(i) <= 1 for i in diagonal_axes): + raise ValueError("need at least two axes to diagonalize") + + from sympy.tensor.array.expressions.array_expressions import _ArrayExpr + from sympy.tensor.array.expressions.array_expressions import _CodegenArrayAbstract + from sympy.tensor.array.expressions.array_expressions import ArrayDiagonal, _array_diagonal + from sympy.matrices.expressions.matexpr import MatrixSymbol + if isinstance(array, (_ArrayExpr, _CodegenArrayAbstract, MatrixSymbol)): + return _array_diagonal(array, *diagonal_axes) + + ArrayDiagonal._validate(array, *diagonal_axes) + + array, remaining_indices, remaining_shape, diagonal_deltas = _util_contraction_diagonal(array, *diagonal_axes) + + # Compute the diagonalized array: + # + # 1. external for loops on all undiagonalized indices. + # Undiagonalized indices are determined by the combinatorial product of + # the absolute positions of the remaining indices. + # 2. internal loop on all diagonal indices. + # It appends the values of the absolute diagonalized index and the absolute + # undiagonalized index for the external loop. + diagonalized_array = [] + diagonal_shape = [len(i) for i in diagonal_deltas] + for icontrib in itertools.product(*remaining_indices): + index_base_position = sum(icontrib) + isum = [] + for sum_to_index in itertools.product(*diagonal_deltas): + idx = array._get_tuple_index(index_base_position + sum(sum_to_index)) + isum.append(array[idx]) + + isum = type(array)(isum).reshape(*diagonal_shape) + diagonalized_array.append(isum) + + return type(array)(diagonalized_array, remaining_shape + diagonal_shape) + + +def derive_by_array(expr, dx): + r""" + Derivative by arrays. Supports both arrays and scalars. + + The equivalent operator for array expressions is ``array_derive``. + + Explanation + =========== + + Given the array `A_{i_1, \ldots, i_N}` and the array `X_{j_1, \ldots, j_M}` + this function will return a new array `B` defined by + + `B_{j_1,\ldots,j_M,i_1,\ldots,i_N} := \frac{\partial A_{i_1,\ldots,i_N}}{\partial X_{j_1,\ldots,j_M}}` + + Examples + ======== + + >>> from sympy import derive_by_array + >>> from sympy.abc import x, y, z, t + >>> from sympy import cos + >>> derive_by_array(cos(x*t), x) + -t*sin(t*x) + >>> derive_by_array(cos(x*t), [x, y, z, t]) + [-t*sin(t*x), 0, 0, -x*sin(t*x)] + >>> derive_by_array([x, y**2*z], [[x, y], [z, t]]) + [[[1, 0], [0, 2*y*z]], [[0, y**2], [0, 0]]] + + """ + from sympy.matrices import MatrixBase + from sympy.tensor.array import SparseNDimArray + array_types = (Iterable, MatrixBase, NDimArray) + + if isinstance(dx, array_types): + dx = ImmutableDenseNDimArray(dx) + for i in dx: + if not i._diff_wrt: + raise ValueError("cannot derive by this array") + + if isinstance(expr, array_types): + if isinstance(expr, NDimArray): + expr = expr.as_immutable() + else: + expr = ImmutableDenseNDimArray(expr) + + if isinstance(dx, array_types): + if isinstance(expr, SparseNDimArray): + lp = len(expr) + new_array = {k + i*lp: v + for i, x in enumerate(Flatten(dx)) + for k, v in expr.diff(x)._sparse_array.items()} + else: + new_array = [[y.diff(x) for y in Flatten(expr)] for x in Flatten(dx)] + return type(expr)(new_array, dx.shape + expr.shape) + else: + return expr.diff(dx) + else: + expr = _sympify(expr) + if isinstance(dx, array_types): + return ImmutableDenseNDimArray([expr.diff(i) for i in Flatten(dx)], dx.shape) + else: + dx = _sympify(dx) + return diff(expr, dx) + + +def permutedims(expr, perm=None, index_order_old=None, index_order_new=None): + """ + Permutes the indices of an array. + + Parameter specifies the permutation of the indices. + + The equivalent operator for array expressions is ``PermuteDims``, which can + be used to keep the expression unevaluated. + + Examples + ======== + + >>> from sympy.abc import x, y, z, t + >>> from sympy import sin + >>> from sympy import Array, permutedims + >>> a = Array([[x, y, z], [t, sin(x), 0]]) + >>> a + [[x, y, z], [t, sin(x), 0]] + >>> permutedims(a, (1, 0)) + [[x, t], [y, sin(x)], [z, 0]] + + If the array is of second order, ``transpose`` can be used: + + >>> from sympy import transpose + >>> transpose(a) + [[x, t], [y, sin(x)], [z, 0]] + + Examples on higher dimensions: + + >>> b = Array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) + >>> permutedims(b, (2, 1, 0)) + [[[1, 5], [3, 7]], [[2, 6], [4, 8]]] + >>> permutedims(b, (1, 2, 0)) + [[[1, 5], [2, 6]], [[3, 7], [4, 8]]] + + An alternative way to specify the same permutations as in the previous + lines involves passing the *old* and *new* indices, either as a list or as + a string: + + >>> permutedims(b, index_order_old="cba", index_order_new="abc") + [[[1, 5], [3, 7]], [[2, 6], [4, 8]]] + >>> permutedims(b, index_order_old="cab", index_order_new="abc") + [[[1, 5], [2, 6]], [[3, 7], [4, 8]]] + + ``Permutation`` objects are also allowed: + + >>> from sympy.combinatorics import Permutation + >>> permutedims(b, Permutation([1, 2, 0])) + [[[1, 5], [2, 6]], [[3, 7], [4, 8]]] + + See Also + ======== + + sympy.tensor.array.expressions.array_expressions.PermuteDims + + """ + from sympy.tensor.array import SparseNDimArray + + from sympy.tensor.array.expressions.array_expressions import _ArrayExpr + from sympy.tensor.array.expressions.array_expressions import _CodegenArrayAbstract + from sympy.tensor.array.expressions.array_expressions import _permute_dims + from sympy.matrices.expressions.matexpr import MatrixSymbol + from sympy.tensor.array.expressions import PermuteDims + from sympy.tensor.array.expressions.array_expressions import get_rank + perm = PermuteDims._get_permutation_from_arguments(perm, index_order_old, index_order_new, get_rank(expr)) + if isinstance(expr, (_ArrayExpr, _CodegenArrayAbstract, MatrixSymbol)): + return _permute_dims(expr, perm) + + if not isinstance(expr, NDimArray): + expr = ImmutableDenseNDimArray(expr) + + from sympy.combinatorics import Permutation + if not isinstance(perm, Permutation): + perm = Permutation(list(perm)) + + if perm.size != expr.rank(): + raise ValueError("wrong permutation size") + + # Get the inverse permutation: + iperm = ~perm + new_shape = perm(expr.shape) + + if isinstance(expr, SparseNDimArray): + return type(expr)({tuple(perm(expr._get_tuple_index(k))): v + for k, v in expr._sparse_array.items()}, new_shape) + + indices_span = perm([range(i) for i in expr.shape]) + + new_array = [None]*len(expr) + for i, idx in enumerate(itertools.product(*indices_span)): + t = iperm(idx) + new_array[i] = expr[t] + + return type(expr)(new_array, new_shape) + + +class Flatten(Printable): + """ + Flatten an iterable object to a list in a lazy-evaluation way. + + Notes + ===== + + This class is an iterator with which the memory cost can be economised. + Optimisation has been considered to ameliorate the performance for some + specific data types like DenseNDimArray and SparseNDimArray. + + Examples + ======== + + >>> from sympy.tensor.array.arrayop import Flatten + >>> from sympy.tensor.array import Array + >>> A = Array(range(6)).reshape(2, 3) + >>> Flatten(A) + Flatten([[0, 1, 2], [3, 4, 5]]) + >>> [i for i in Flatten(A)] + [0, 1, 2, 3, 4, 5] + """ + def __init__(self, iterable): + from sympy.matrices.matrixbase import MatrixBase + from sympy.tensor.array import NDimArray + + if not isinstance(iterable, (Iterable, MatrixBase)): + raise NotImplementedError("Data type not yet supported") + + if isinstance(iterable, list): + iterable = NDimArray(iterable) + + self._iter = iterable + self._idx = 0 + + def __iter__(self): + return self + + def __next__(self): + from sympy.matrices.matrixbase import MatrixBase + + if len(self._iter) > self._idx: + if isinstance(self._iter, DenseNDimArray): + result = self._iter._array[self._idx] + + elif isinstance(self._iter, SparseNDimArray): + if self._idx in self._iter._sparse_array: + result = self._iter._sparse_array[self._idx] + else: + result = 0 + + elif isinstance(self._iter, MatrixBase): + result = self._iter[self._idx] + + elif hasattr(self._iter, '__next__'): + result = next(self._iter) + + else: + result = self._iter[self._idx] + + else: + raise StopIteration + + self._idx += 1 + return result + + def next(self): + return self.__next__() + + def _sympystr(self, printer): + return type(self).__name__ + '(' + printer._print(self._iter) + ')' diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/dense_ndim_array.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/dense_ndim_array.py new file mode 100644 index 0000000000000000000000000000000000000000..576e452c55d8d374ca1f72c553f3a64de7227d43 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/dense_ndim_array.py @@ -0,0 +1,206 @@ +import functools +from typing import List + +from sympy.core.basic import Basic +from sympy.core.containers import Tuple +from sympy.core.singleton import S +from sympy.core.sympify import _sympify +from sympy.tensor.array.mutable_ndim_array import MutableNDimArray +from sympy.tensor.array.ndim_array import NDimArray, ImmutableNDimArray, ArrayKind +from sympy.utilities.iterables import flatten + + +class DenseNDimArray(NDimArray): + + _array: List[Basic] + + def __new__(self, *args, **kwargs): + return ImmutableDenseNDimArray(*args, **kwargs) + + @property + def kind(self) -> ArrayKind: + return ArrayKind._union(self._array) + + def __getitem__(self, index): + """ + Allows to get items from N-dim array. + + Examples + ======== + + >>> from sympy import MutableDenseNDimArray + >>> a = MutableDenseNDimArray([0, 1, 2, 3], (2, 2)) + >>> a + [[0, 1], [2, 3]] + >>> a[0, 0] + 0 + >>> a[1, 1] + 3 + >>> a[0] + [0, 1] + >>> a[1] + [2, 3] + + + Symbolic index: + + >>> from sympy.abc import i, j + >>> a[i, j] + [[0, 1], [2, 3]][i, j] + + Replace `i` and `j` to get element `(1, 1)`: + + >>> a[i, j].subs({i: 1, j: 1}) + 3 + + """ + syindex = self._check_symbolic_index(index) + if syindex is not None: + return syindex + + index = self._check_index_for_getitem(index) + + if isinstance(index, tuple) and any(isinstance(i, slice) for i in index): + sl_factors, eindices = self._get_slice_data_for_array_access(index) + array = [self._array[self._parse_index(i)] for i in eindices] + nshape = [len(el) for i, el in enumerate(sl_factors) if isinstance(index[i], slice)] + return type(self)(array, nshape) + else: + index = self._parse_index(index) + return self._array[index] + + @classmethod + def zeros(cls, *shape): + list_length = functools.reduce(lambda x, y: x*y, shape, S.One) + return cls._new(([0]*list_length,), shape) + + def tomatrix(self): + """ + Converts MutableDenseNDimArray to Matrix. Can convert only 2-dim array, else will raise error. + + Examples + ======== + + >>> from sympy import MutableDenseNDimArray + >>> a = MutableDenseNDimArray([1 for i in range(9)], (3, 3)) + >>> b = a.tomatrix() + >>> b + Matrix([ + [1, 1, 1], + [1, 1, 1], + [1, 1, 1]]) + + """ + from sympy.matrices import Matrix + + if self.rank() != 2: + raise ValueError('Dimensions must be of size of 2') + + return Matrix(self.shape[0], self.shape[1], self._array) + + def reshape(self, *newshape): + """ + Returns MutableDenseNDimArray instance with new shape. Elements number + must be suitable to new shape. The only argument of method sets + new shape. + + Examples + ======== + + >>> from sympy import MutableDenseNDimArray + >>> a = MutableDenseNDimArray([1, 2, 3, 4, 5, 6], (2, 3)) + >>> a.shape + (2, 3) + >>> a + [[1, 2, 3], [4, 5, 6]] + >>> b = a.reshape(3, 2) + >>> b.shape + (3, 2) + >>> b + [[1, 2], [3, 4], [5, 6]] + + """ + new_total_size = functools.reduce(lambda x,y: x*y, newshape) + if new_total_size != self._loop_size: + raise ValueError('Expecting reshape size to %d but got prod(%s) = %d' % ( + self._loop_size, str(newshape), new_total_size)) + + # there is no `.func` as this class does not subtype `Basic`: + return type(self)(self._array, newshape) + + +class ImmutableDenseNDimArray(DenseNDimArray, ImmutableNDimArray): # type: ignore + def __new__(cls, iterable, shape=None, **kwargs): + return cls._new(iterable, shape, **kwargs) + + @classmethod + def _new(cls, iterable, shape, **kwargs): + shape, flat_list = cls._handle_ndarray_creation_inputs(iterable, shape, **kwargs) + shape = Tuple(*map(_sympify, shape)) + cls._check_special_bounds(flat_list, shape) + flat_list = flatten(flat_list) + flat_list = Tuple(*flat_list) + self = Basic.__new__(cls, flat_list, shape, **kwargs) + self._shape = shape + self._array = list(flat_list) + self._rank = len(shape) + self._loop_size = functools.reduce(lambda x,y: x*y, shape, 1) + return self + + def __setitem__(self, index, value): + raise TypeError('immutable N-dim array') + + def as_mutable(self): + return MutableDenseNDimArray(self) + + def _eval_simplify(self, **kwargs): + from sympy.simplify.simplify import simplify + return self.applyfunc(simplify) + +class MutableDenseNDimArray(DenseNDimArray, MutableNDimArray): + + def __new__(cls, iterable=None, shape=None, **kwargs): + return cls._new(iterable, shape, **kwargs) + + @classmethod + def _new(cls, iterable, shape, **kwargs): + shape, flat_list = cls._handle_ndarray_creation_inputs(iterable, shape, **kwargs) + flat_list = flatten(flat_list) + self = object.__new__(cls) + self._shape = shape + self._array = list(flat_list) + self._rank = len(shape) + self._loop_size = functools.reduce(lambda x,y: x*y, shape) if shape else len(flat_list) + return self + + def __setitem__(self, index, value): + """Allows to set items to MutableDenseNDimArray. + + Examples + ======== + + >>> from sympy import MutableDenseNDimArray + >>> a = MutableDenseNDimArray.zeros(2, 2) + >>> a[0,0] = 1 + >>> a[1,1] = 1 + >>> a + [[1, 0], [0, 1]] + + """ + if isinstance(index, tuple) and any(isinstance(i, slice) for i in index): + value, eindices, slice_offsets = self._get_slice_data_for_array_assignment(index, value) + for i in eindices: + other_i = [ind - j for ind, j in zip(i, slice_offsets) if j is not None] + self._array[self._parse_index(i)] = value[other_i] + else: + index = self._parse_index(index) + self._setter_iterable_check(value) + value = _sympify(value) + self._array[index] = value + + def as_immutable(self): + return ImmutableDenseNDimArray(self) + + @property + def free_symbols(self): + return {i for j in self._array for i in j.free_symbols} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f1658241782cdf0e38a30c43a6d67f9811297f4c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/__init__.py @@ -0,0 +1,178 @@ +r""" +Array expressions are expressions representing N-dimensional arrays, without +evaluating them. These expressions represent in a certain way abstract syntax +trees of operations on N-dimensional arrays. + +Every N-dimensional array operator has a corresponding array expression object. + +Table of correspondences: + +=============================== ============================= + Array operator Array expression operator +=============================== ============================= + tensorproduct ArrayTensorProduct + tensorcontraction ArrayContraction + tensordiagonal ArrayDiagonal + permutedims PermuteDims +=============================== ============================= + +Examples +======== + +``ArraySymbol`` objects are the N-dimensional equivalent of ``MatrixSymbol`` +objects in the matrix module: + +>>> from sympy.tensor.array.expressions import ArraySymbol +>>> from sympy.abc import i, j, k +>>> A = ArraySymbol("A", (3, 2, 4)) +>>> A.shape +(3, 2, 4) +>>> A[i, j, k] +A[i, j, k] +>>> A.as_explicit() +[[[A[0, 0, 0], A[0, 0, 1], A[0, 0, 2], A[0, 0, 3]], + [A[0, 1, 0], A[0, 1, 1], A[0, 1, 2], A[0, 1, 3]]], + [[A[1, 0, 0], A[1, 0, 1], A[1, 0, 2], A[1, 0, 3]], + [A[1, 1, 0], A[1, 1, 1], A[1, 1, 2], A[1, 1, 3]]], + [[A[2, 0, 0], A[2, 0, 1], A[2, 0, 2], A[2, 0, 3]], + [A[2, 1, 0], A[2, 1, 1], A[2, 1, 2], A[2, 1, 3]]]] + +Component-explicit arrays can be added inside array expressions: + +>>> from sympy import Array +>>> from sympy import tensorproduct +>>> from sympy.tensor.array.expressions import ArrayTensorProduct +>>> a = Array([1, 2, 3]) +>>> b = Array([i, j, k]) +>>> expr = ArrayTensorProduct(a, b, b) +>>> expr +ArrayTensorProduct([1, 2, 3], [i, j, k], [i, j, k]) +>>> expr.as_explicit() == tensorproduct(a, b, b) +True + +Constructing array expressions from index-explicit forms +-------------------------------------------------------- + +Array expressions are index-implicit. This means they do not use any indices to +represent array operations. The function ``convert_indexed_to_array( ... )`` +may be used to convert index-explicit expressions to array expressions. +It takes as input two parameters: the index-explicit expression and the order +of the indices: + +>>> from sympy.tensor.array.expressions import convert_indexed_to_array +>>> from sympy import Sum +>>> A = ArraySymbol("A", (3, 3)) +>>> B = ArraySymbol("B", (3, 3)) +>>> convert_indexed_to_array(A[i, j], [i, j]) +A +>>> convert_indexed_to_array(A[i, j], [j, i]) +PermuteDims(A, (0 1)) +>>> convert_indexed_to_array(A[i, j] + B[j, i], [i, j]) +ArrayAdd(A, PermuteDims(B, (0 1))) +>>> convert_indexed_to_array(Sum(A[i, j]*B[j, k], (j, 0, 2)), [i, k]) +ArrayContraction(ArrayTensorProduct(A, B), (1, 2)) + +The diagonal of a matrix in the array expression form: + +>>> convert_indexed_to_array(A[i, i], [i]) +ArrayDiagonal(A, (0, 1)) + +The trace of a matrix in the array expression form: + +>>> convert_indexed_to_array(Sum(A[i, i], (i, 0, 2)), [i]) +ArrayContraction(A, (0, 1)) + +Compatibility with matrices +--------------------------- + +Array expressions can be mixed with objects from the matrix module: + +>>> from sympy import MatrixSymbol +>>> from sympy.tensor.array.expressions import ArrayContraction +>>> M = MatrixSymbol("M", 3, 3) +>>> N = MatrixSymbol("N", 3, 3) + +Express the matrix product in the array expression form: + +>>> from sympy.tensor.array.expressions import convert_matrix_to_array +>>> expr = convert_matrix_to_array(M*N) +>>> expr +ArrayContraction(ArrayTensorProduct(M, N), (1, 2)) + +The expression can be converted back to matrix form: + +>>> from sympy.tensor.array.expressions import convert_array_to_matrix +>>> convert_array_to_matrix(expr) +M*N + +Add a second contraction on the remaining axes in order to get the trace of `M \cdot N`: + +>>> expr_tr = ArrayContraction(expr, (0, 1)) +>>> expr_tr +ArrayContraction(ArrayContraction(ArrayTensorProduct(M, N), (1, 2)), (0, 1)) + +Flatten the expression by calling ``.doit()`` and remove the nested array contraction operations: + +>>> expr_tr.doit() +ArrayContraction(ArrayTensorProduct(M, N), (0, 3), (1, 2)) + +Get the explicit form of the array expression: + +>>> expr.as_explicit() +[[M[0, 0]*N[0, 0] + M[0, 1]*N[1, 0] + M[0, 2]*N[2, 0], M[0, 0]*N[0, 1] + M[0, 1]*N[1, 1] + M[0, 2]*N[2, 1], M[0, 0]*N[0, 2] + M[0, 1]*N[1, 2] + M[0, 2]*N[2, 2]], + [M[1, 0]*N[0, 0] + M[1, 1]*N[1, 0] + M[1, 2]*N[2, 0], M[1, 0]*N[0, 1] + M[1, 1]*N[1, 1] + M[1, 2]*N[2, 1], M[1, 0]*N[0, 2] + M[1, 1]*N[1, 2] + M[1, 2]*N[2, 2]], + [M[2, 0]*N[0, 0] + M[2, 1]*N[1, 0] + M[2, 2]*N[2, 0], M[2, 0]*N[0, 1] + M[2, 1]*N[1, 1] + M[2, 2]*N[2, 1], M[2, 0]*N[0, 2] + M[2, 1]*N[1, 2] + M[2, 2]*N[2, 2]]] + +Express the trace of a matrix: + +>>> from sympy import Trace +>>> convert_matrix_to_array(Trace(M)) +ArrayContraction(M, (0, 1)) +>>> convert_matrix_to_array(Trace(M*N)) +ArrayContraction(ArrayTensorProduct(M, N), (0, 3), (1, 2)) + +Express the transposition of a matrix (will be expressed as a permutation of the axes: + +>>> convert_matrix_to_array(M.T) +PermuteDims(M, (0 1)) + +Compute the derivative array expressions: + +>>> from sympy.tensor.array.expressions import array_derive +>>> d = array_derive(M, M) +>>> d +PermuteDims(ArrayTensorProduct(I, I), (3)(1 2)) + +Verify that the derivative corresponds to the form computed with explicit matrices: + +>>> d.as_explicit() +[[[[1, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 1, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 1], [0, 0, 0], [0, 0, 0]]], [[[0, 0, 0], [1, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 1, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 1], [0, 0, 0]]], [[[0, 0, 0], [0, 0, 0], [1, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 1, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 1]]]] +>>> Me = M.as_explicit() +>>> Me.diff(Me) +[[[[1, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 1, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 1], [0, 0, 0], [0, 0, 0]]], [[[0, 0, 0], [1, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 1, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 1], [0, 0, 0]]], [[[0, 0, 0], [0, 0, 0], [1, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 1, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 1]]]] + +""" + +__all__ = [ + "ArraySymbol", "ArrayElement", "ZeroArray", "OneArray", + "ArrayTensorProduct", + "ArrayContraction", + "ArrayDiagonal", + "PermuteDims", + "ArrayAdd", + "ArrayElementwiseApplyFunc", + "Reshape", + "convert_array_to_matrix", + "convert_matrix_to_array", + "convert_array_to_indexed", + "convert_indexed_to_array", + "array_derive", +] + +from sympy.tensor.array.expressions.array_expressions import ArrayTensorProduct, ArrayAdd, PermuteDims, ArrayDiagonal, \ + ArrayContraction, Reshape, ArraySymbol, ArrayElement, ZeroArray, OneArray, ArrayElementwiseApplyFunc +from sympy.tensor.array.expressions.arrayexpr_derivatives import array_derive +from sympy.tensor.array.expressions.from_array_to_indexed import convert_array_to_indexed +from sympy.tensor.array.expressions.from_array_to_matrix import convert_array_to_matrix +from sympy.tensor.array.expressions.from_indexed_to_array import convert_indexed_to_array +from sympy.tensor.array.expressions.from_matrix_to_array import convert_matrix_to_array diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/array_expressions.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/array_expressions.py new file mode 100644 index 0000000000000000000000000000000000000000..f062e3de4c24987d62ba0b3a19fe474fb4687940 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/array_expressions.py @@ -0,0 +1,1969 @@ +from __future__ import annotations +import collections.abc +import operator +from collections import defaultdict, Counter +from functools import reduce +import itertools +from itertools import accumulate + +import typing + +from sympy.core.numbers import Integer +from sympy.core.relational import Equality +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.core.basic import Basic +from sympy.core.containers import Tuple +from sympy.core.expr import Expr +from sympy.core.function import (Function, Lambda) +from sympy.core.mul import Mul +from sympy.core.singleton import S +from sympy.core.sorting import default_sort_key +from sympy.core.symbol import (Dummy, Symbol) +from sympy.matrices.matrixbase import MatrixBase +from sympy.matrices.expressions.diagonal import diagonalize_vector +from sympy.matrices.expressions.matexpr import MatrixExpr +from sympy.matrices.expressions.special import ZeroMatrix +from sympy.tensor.array.arrayop import (permutedims, tensorcontraction, tensordiagonal, tensorproduct) +from sympy.tensor.array.dense_ndim_array import ImmutableDenseNDimArray +from sympy.tensor.array.ndim_array import NDimArray +from sympy.tensor.indexed import (Indexed, IndexedBase) +from sympy.matrices.expressions.matexpr import MatrixElement +from sympy.tensor.array.expressions.utils import _apply_recursively_over_nested_lists, _sort_contraction_indices, \ + _get_mapping_from_subranks, _build_push_indices_up_func_transformation, _get_contraction_links, \ + _build_push_indices_down_func_transformation +from sympy.combinatorics import Permutation +from sympy.combinatorics.permutations import _af_invert +from sympy.core.sympify import _sympify + + +class _ArrayExpr(Expr): + shape: tuple[Expr, ...] + + def __getitem__(self, item): + if not isinstance(item, collections.abc.Iterable): + item = (item,) + ArrayElement._check_shape(self, item) + return self._get(item) + + def _get(self, item): + return _get_array_element_or_slice(self, item) + + +class ArraySymbol(_ArrayExpr): + """ + Symbol representing an array expression + """ + + _iterable = False + + def __new__(cls, symbol, shape: typing.Iterable) -> "ArraySymbol": + if isinstance(symbol, str): + symbol = Symbol(symbol) + # symbol = _sympify(symbol) + shape = Tuple(*map(_sympify, shape)) + obj = Expr.__new__(cls, symbol, shape) + return obj + + @property + def name(self): + return self._args[0] + + @property + def shape(self): + return self._args[1] + + def as_explicit(self): + if not all(i.is_Integer for i in self.shape): + raise ValueError("cannot express explicit array with symbolic shape") + data = [self[i] for i in itertools.product(*[range(j) for j in self.shape])] + return ImmutableDenseNDimArray(data).reshape(*self.shape) + + +class ArrayElement(Expr): + """ + An element of an array. + """ + + _diff_wrt = True + is_symbol = True + is_commutative = True + + def __new__(cls, name, indices): + if isinstance(name, str): + name = Symbol(name) + name = _sympify(name) + if not isinstance(indices, collections.abc.Iterable): + indices = (indices,) + indices = _sympify(tuple(indices)) + cls._check_shape(name, indices) + obj = Expr.__new__(cls, name, indices) + return obj + + @classmethod + def _check_shape(cls, name, indices): + indices = tuple(indices) + if hasattr(name, "shape"): + index_error = IndexError("number of indices does not match shape of the array") + if len(indices) != len(name.shape): + raise index_error + if any((i >= s) == True for i, s in zip(indices, name.shape)): + raise ValueError("shape is out of bounds") + if any((i < 0) == True for i in indices): + raise ValueError("shape contains negative values") + + @property + def name(self): + return self._args[0] + + @property + def indices(self): + return self._args[1] + + def _eval_derivative(self, s): + if not isinstance(s, ArrayElement): + return S.Zero + + if s == self: + return S.One + + if s.name != self.name: + return S.Zero + + return Mul.fromiter(KroneckerDelta(i, j) for i, j in zip(self.indices, s.indices)) + + +class ZeroArray(_ArrayExpr): + """ + Symbolic array of zeros. Equivalent to ``ZeroMatrix`` for matrices. + """ + + def __new__(cls, *shape): + if len(shape) == 0: + return S.Zero + shape = map(_sympify, shape) + obj = Expr.__new__(cls, *shape) + return obj + + @property + def shape(self): + return self._args + + def as_explicit(self): + if not all(i.is_Integer for i in self.shape): + raise ValueError("Cannot return explicit form for symbolic shape.") + return ImmutableDenseNDimArray.zeros(*self.shape) + + def _get(self, item): + return S.Zero + + +class OneArray(_ArrayExpr): + """ + Symbolic array of ones. + """ + + def __new__(cls, *shape): + if len(shape) == 0: + return S.One + shape = map(_sympify, shape) + obj = Expr.__new__(cls, *shape) + return obj + + @property + def shape(self): + return self._args + + def as_explicit(self): + if not all(i.is_Integer for i in self.shape): + raise ValueError("Cannot return explicit form for symbolic shape.") + return ImmutableDenseNDimArray([S.One for i in range(reduce(operator.mul, self.shape))]).reshape(*self.shape) + + def _get(self, item): + return S.One + + +class _CodegenArrayAbstract(Basic): + + @property + def subranks(self): + """ + Returns the ranks of the objects in the uppermost tensor product inside + the current object. In case no tensor products are contained, return + the atomic ranks. + + Examples + ======== + + >>> from sympy.tensor.array import tensorproduct, tensorcontraction + >>> from sympy import MatrixSymbol + >>> M = MatrixSymbol("M", 3, 3) + >>> N = MatrixSymbol("N", 3, 3) + >>> P = MatrixSymbol("P", 3, 3) + + Important: do not confuse the rank of the matrix with the rank of an array. + + >>> tp = tensorproduct(M, N, P) + >>> tp.subranks + [2, 2, 2] + + >>> co = tensorcontraction(tp, (1, 2), (3, 4)) + >>> co.subranks + [2, 2, 2] + """ + return self._subranks[:] + + def subrank(self): + """ + The sum of ``subranks``. + """ + return sum(self.subranks) + + @property + def shape(self): + return self._shape + + def doit(self, **hints): + deep = hints.get("deep", True) + if deep: + return self.func(*[arg.doit(**hints) for arg in self.args])._canonicalize() + else: + return self._canonicalize() + +class ArrayTensorProduct(_CodegenArrayAbstract): + r""" + Class to represent the tensor product of array-like objects. + """ + + def __new__(cls, *args, **kwargs): + args = [_sympify(arg) for arg in args] + + canonicalize = kwargs.pop("canonicalize", False) + + ranks = [get_rank(arg) for arg in args] + + obj = Basic.__new__(cls, *args) + obj._subranks = ranks + shapes = [get_shape(i) for i in args] + + if any(i is None for i in shapes): + obj._shape = None + else: + obj._shape = tuple(j for i in shapes for j in i) + if canonicalize: + return obj._canonicalize() + return obj + + def _canonicalize(self): + args = self.args + args = self._flatten(args) + + ranks = [get_rank(arg) for arg in args] + + # Check if there are nested permutation and lift them up: + permutation_cycles = [] + for i, arg in enumerate(args): + if not isinstance(arg, PermuteDims): + continue + permutation_cycles.extend([[k + sum(ranks[:i]) for k in j] for j in arg.permutation.cyclic_form]) + args[i] = arg.expr + if permutation_cycles: + return _permute_dims(_array_tensor_product(*args), Permutation(sum(ranks)-1)*Permutation(permutation_cycles)) + + if len(args) == 1: + return args[0] + + # If any object is a ZeroArray, return a ZeroArray: + if any(isinstance(arg, (ZeroArray, ZeroMatrix)) for arg in args): + shapes = reduce(operator.add, [get_shape(i) for i in args], ()) + return ZeroArray(*shapes) + + # If there are contraction objects inside, transform the whole + # expression into `ArrayContraction`: + contractions = {i: arg for i, arg in enumerate(args) if isinstance(arg, ArrayContraction)} + if contractions: + ranks = [_get_subrank(arg) if isinstance(arg, ArrayContraction) else get_rank(arg) for arg in args] + cumulative_ranks = list(accumulate([0] + ranks))[:-1] + tp = _array_tensor_product(*[arg.expr if isinstance(arg, ArrayContraction) else arg for arg in args]) + contraction_indices = [tuple(cumulative_ranks[i] + k for k in j) for i, arg in contractions.items() for j in arg.contraction_indices] + return _array_contraction(tp, *contraction_indices) + + diagonals = {i: arg for i, arg in enumerate(args) if isinstance(arg, ArrayDiagonal)} + if diagonals: + inverse_permutation = [] + last_perm = [] + ranks = [get_rank(arg) for arg in args] + cumulative_ranks = list(accumulate([0] + ranks))[:-1] + for i, arg in enumerate(args): + if isinstance(arg, ArrayDiagonal): + i1 = get_rank(arg) - len(arg.diagonal_indices) + i2 = len(arg.diagonal_indices) + inverse_permutation.extend([cumulative_ranks[i] + j for j in range(i1)]) + last_perm.extend([cumulative_ranks[i] + j for j in range(i1, i1 + i2)]) + else: + inverse_permutation.extend([cumulative_ranks[i] + j for j in range(get_rank(arg))]) + inverse_permutation.extend(last_perm) + tp = _array_tensor_product(*[arg.expr if isinstance(arg, ArrayDiagonal) else arg for arg in args]) + ranks2 = [_get_subrank(arg) if isinstance(arg, ArrayDiagonal) else get_rank(arg) for arg in args] + cumulative_ranks2 = list(accumulate([0] + ranks2))[:-1] + diagonal_indices = [tuple(cumulative_ranks2[i] + k for k in j) for i, arg in diagonals.items() for j in arg.diagonal_indices] + return _permute_dims(_array_diagonal(tp, *diagonal_indices), _af_invert(inverse_permutation)) + + return self.func(*args, canonicalize=False) + + @classmethod + def _flatten(cls, args): + args = [i for arg in args for i in (arg.args if isinstance(arg, cls) else [arg])] + return args + + def as_explicit(self): + return tensorproduct(*[arg.as_explicit() if hasattr(arg, "as_explicit") else arg for arg in self.args]) + + +class ArrayAdd(_CodegenArrayAbstract): + r""" + Class for elementwise array additions. + """ + + def __new__(cls, *args, **kwargs): + args = [_sympify(arg) for arg in args] + ranks = [get_rank(arg) for arg in args] + ranks = list(set(ranks)) + if len(ranks) != 1: + raise ValueError("summing arrays of different ranks") + shapes = [arg.shape for arg in args] + if len({i for i in shapes if i is not None}) > 1: + raise ValueError("mismatching shapes in addition") + + canonicalize = kwargs.pop("canonicalize", False) + + obj = Basic.__new__(cls, *args) + obj._subranks = ranks + if any(i is None for i in shapes): + obj._shape = None + else: + obj._shape = shapes[0] + if canonicalize: + return obj._canonicalize() + return obj + + def _canonicalize(self): + args = self.args + + # Flatten: + args = self._flatten_args(args) + + shapes = [get_shape(arg) for arg in args] + args = [arg for arg in args if not isinstance(arg, (ZeroArray, ZeroMatrix))] + if len(args) == 0: + if any(i for i in shapes if i is None): + raise NotImplementedError("cannot handle addition of ZeroMatrix/ZeroArray and undefined shape object") + return ZeroArray(*shapes[0]) + elif len(args) == 1: + return args[0] + return self.func(*args, canonicalize=False) + + @classmethod + def _flatten_args(cls, args): + new_args = [] + for arg in args: + if isinstance(arg, ArrayAdd): + new_args.extend(arg.args) + else: + new_args.append(arg) + return new_args + + def as_explicit(self): + return reduce( + operator.add, + [arg.as_explicit() if hasattr(arg, "as_explicit") else arg for arg in self.args]) + + +class PermuteDims(_CodegenArrayAbstract): + r""" + Class to represent permutation of axes of arrays. + + Examples + ======== + + >>> from sympy.tensor.array import permutedims + >>> from sympy import MatrixSymbol + >>> M = MatrixSymbol("M", 3, 3) + >>> cg = permutedims(M, [1, 0]) + + The object ``cg`` represents the transposition of ``M``, as the permutation + ``[1, 0]`` will act on its indices by switching them: + + `M_{ij} \Rightarrow M_{ji}` + + This is evident when transforming back to matrix form: + + >>> from sympy.tensor.array.expressions.from_array_to_matrix import convert_array_to_matrix + >>> convert_array_to_matrix(cg) + M.T + + >>> N = MatrixSymbol("N", 3, 2) + >>> cg = permutedims(N, [1, 0]) + >>> cg.shape + (2, 3) + + There are optional parameters that can be used as alternative to the permutation: + + >>> from sympy.tensor.array.expressions import ArraySymbol, PermuteDims + >>> M = ArraySymbol("M", (1, 2, 3, 4, 5)) + >>> expr = PermuteDims(M, index_order_old="ijklm", index_order_new="kijml") + >>> expr + PermuteDims(M, (0 2 1)(3 4)) + >>> expr.shape + (3, 1, 2, 5, 4) + + Permutations of tensor products are simplified in order to achieve a + standard form: + + >>> from sympy.tensor.array import tensorproduct + >>> M = MatrixSymbol("M", 4, 5) + >>> tp = tensorproduct(M, N) + >>> tp.shape + (4, 5, 3, 2) + >>> perm1 = permutedims(tp, [2, 3, 1, 0]) + + The args ``(M, N)`` have been sorted and the permutation has been + simplified, the expression is equivalent: + + >>> perm1.expr.args + (N, M) + >>> perm1.shape + (3, 2, 5, 4) + >>> perm1.permutation + (2 3) + + The permutation in its array form has been simplified from + ``[2, 3, 1, 0]`` to ``[0, 1, 3, 2]``, as the arguments of the tensor + product `M` and `N` have been switched: + + >>> perm1.permutation.array_form + [0, 1, 3, 2] + + We can nest a second permutation: + + >>> perm2 = permutedims(perm1, [1, 0, 2, 3]) + >>> perm2.shape + (2, 3, 5, 4) + >>> perm2.permutation.array_form + [1, 0, 3, 2] + """ + + def __new__(cls, expr, permutation=None, index_order_old=None, index_order_new=None, **kwargs): + from sympy.combinatorics import Permutation + expr = _sympify(expr) + expr_rank = get_rank(expr) + permutation = cls._get_permutation_from_arguments(permutation, index_order_old, index_order_new, expr_rank) + permutation = Permutation(permutation) + permutation_size = permutation.size + if permutation_size != expr_rank: + raise ValueError("Permutation size must be the length of the shape of expr") + + canonicalize = kwargs.pop("canonicalize", False) + + obj = Basic.__new__(cls, expr, permutation) + obj._subranks = [get_rank(expr)] + shape = get_shape(expr) + if shape is None: + obj._shape = None + else: + obj._shape = tuple(shape[permutation(i)] for i in range(len(shape))) + if canonicalize: + return obj._canonicalize() + return obj + + def _canonicalize(self): + expr = self.expr + permutation = self.permutation + if isinstance(expr, PermuteDims): + subexpr = expr.expr + subperm = expr.permutation + permutation = permutation * subperm + expr = subexpr + if isinstance(expr, ArrayContraction): + expr, permutation = self._PermuteDims_denestarg_ArrayContraction(expr, permutation) + if isinstance(expr, ArrayTensorProduct): + expr, permutation = self._PermuteDims_denestarg_ArrayTensorProduct(expr, permutation) + if isinstance(expr, (ZeroArray, ZeroMatrix)): + return ZeroArray(*[expr.shape[i] for i in permutation.array_form]) + plist = permutation.array_form + if plist == sorted(plist): + return expr + return self.func(expr, permutation, canonicalize=False) + + @property + def expr(self): + return self.args[0] + + @property + def permutation(self): + return self.args[1] + + @classmethod + def _PermuteDims_denestarg_ArrayTensorProduct(cls, expr, permutation): + # Get the permutation in its image-form: + perm_image_form = _af_invert(permutation.array_form) + args = list(expr.args) + # Starting index global position for every arg: + cumul = list(accumulate([0] + expr.subranks)) + # Split `perm_image_form` into a list of list corresponding to the indices + # of every argument: + perm_image_form_in_components = [perm_image_form[cumul[i]:cumul[i+1]] for i in range(len(args))] + # Create an index, target-position-key array: + ps = [(i, sorted(comp)) for i, comp in enumerate(perm_image_form_in_components)] + # Sort the array according to the target-position-key: + # In this way, we define a canonical way to sort the arguments according + # to the permutation. + ps.sort(key=lambda x: x[1]) + # Read the inverse-permutation (i.e. image-form) of the args: + perm_args_image_form = [i[0] for i in ps] + # Apply the args-permutation to the `args`: + args_sorted = [args[i] for i in perm_args_image_form] + # Apply the args-permutation to the array-form of the permutation of the axes (of `expr`): + perm_image_form_sorted_args = [perm_image_form_in_components[i] for i in perm_args_image_form] + new_permutation = Permutation(_af_invert([j for i in perm_image_form_sorted_args for j in i])) + return _array_tensor_product(*args_sorted), new_permutation + + @classmethod + def _PermuteDims_denestarg_ArrayContraction(cls, expr, permutation): + if not isinstance(expr, ArrayContraction): + return expr, permutation + if not isinstance(expr.expr, ArrayTensorProduct): + return expr, permutation + args = expr.expr.args + subranks = [get_rank(arg) for arg in expr.expr.args] + + contraction_indices = expr.contraction_indices + contraction_indices_flat = [j for i in contraction_indices for j in i] + cumul = list(accumulate([0] + subranks)) + + # Spread the permutation in its array form across the args in the corresponding + # tensor-product arguments with free indices: + permutation_array_blocks_up = [] + image_form = _af_invert(permutation.array_form) + counter = 0 + for i in range(len(subranks)): + current = [] + for j in range(cumul[i], cumul[i+1]): + if j in contraction_indices_flat: + continue + current.append(image_form[counter]) + counter += 1 + permutation_array_blocks_up.append(current) + + # Get the map of axis repositioning for every argument of tensor-product: + index_blocks = [list(range(cumul[i], cumul[i+1])) for i, e in enumerate(expr.subranks)] + index_blocks_up = expr._push_indices_up(expr.contraction_indices, index_blocks) + inverse_permutation = permutation**(-1) + index_blocks_up_permuted = [[inverse_permutation(j) for j in i if j is not None] for i in index_blocks_up] + + # Sorting key is a list of tuple, first element is the index of `args`, second element of + # the tuple is the sorting key to sort `args` of the tensor product: + sorting_keys = list(enumerate(index_blocks_up_permuted)) + sorting_keys.sort(key=lambda x: x[1]) + + # Now we can get the permutation acting on the args in its image-form: + new_perm_image_form = [i[0] for i in sorting_keys] + # Apply the args-level permutation to various elements: + new_index_blocks = [index_blocks[i] for i in new_perm_image_form] + new_index_perm_array_form = _af_invert([j for i in new_index_blocks for j in i]) + new_args = [args[i] for i in new_perm_image_form] + new_contraction_indices = [tuple(new_index_perm_array_form[j] for j in i) for i in contraction_indices] + new_expr = _array_contraction(_array_tensor_product(*new_args), *new_contraction_indices) + new_permutation = Permutation(_af_invert([j for i in [permutation_array_blocks_up[k] for k in new_perm_image_form] for j in i])) + return new_expr, new_permutation + + @classmethod + def _check_permutation_mapping(cls, expr, permutation): + subranks = expr.subranks + index2arg = [i for i, arg in enumerate(expr.args) for j in range(expr.subranks[i])] + permuted_indices = [permutation(i) for i in range(expr.subrank())] + new_args = list(expr.args) + arg_candidate_index = index2arg[permuted_indices[0]] + current_indices = [] + new_permutation = [] + inserted_arg_cand_indices = set() + for i, idx in enumerate(permuted_indices): + if index2arg[idx] != arg_candidate_index: + new_permutation.extend(current_indices) + current_indices = [] + arg_candidate_index = index2arg[idx] + current_indices.append(idx) + arg_candidate_rank = subranks[arg_candidate_index] + if len(current_indices) == arg_candidate_rank: + new_permutation.extend(sorted(current_indices)) + local_current_indices = [j - min(current_indices) for j in current_indices] + i1 = index2arg[i] + new_args[i1] = _permute_dims(new_args[i1], Permutation(local_current_indices)) + inserted_arg_cand_indices.add(arg_candidate_index) + current_indices = [] + new_permutation.extend(current_indices) + + # TODO: swap args positions in order to simplify the expression: + # TODO: this should be in a function + args_positions = list(range(len(new_args))) + # Get possible shifts: + maps = {} + cumulative_subranks = [0] + list(accumulate(subranks)) + for i in range(len(subranks)): + s = {index2arg[new_permutation[j]] for j in range(cumulative_subranks[i], cumulative_subranks[i+1])} + if len(s) != 1: + continue + elem = next(iter(s)) + if i != elem: + maps[i] = elem + + # Find cycles in the map: + lines = [] + current_line = [] + while maps: + if len(current_line) == 0: + k, v = maps.popitem() + current_line.append(k) + else: + k = current_line[-1] + if k not in maps: + current_line = [] + continue + v = maps.pop(k) + if v in current_line: + lines.append(current_line) + current_line = [] + continue + current_line.append(v) + for line in lines: + for i, e in enumerate(line): + args_positions[line[(i + 1) % len(line)]] = e + + # TODO: function in order to permute the args: + permutation_blocks = [[new_permutation[cumulative_subranks[i] + j] for j in range(e)] for i, e in enumerate(subranks)] + new_args = [new_args[i] for i in args_positions] + new_permutation_blocks = [permutation_blocks[i] for i in args_positions] + new_permutation2 = [j for i in new_permutation_blocks for j in i] + return _array_tensor_product(*new_args), Permutation(new_permutation2) # **(-1) + + @classmethod + def _check_if_there_are_closed_cycles(cls, expr, permutation): + args = list(expr.args) + subranks = expr.subranks + cyclic_form = permutation.cyclic_form + cumulative_subranks = [0] + list(accumulate(subranks)) + cyclic_min = [min(i) for i in cyclic_form] + cyclic_max = [max(i) for i in cyclic_form] + cyclic_keep = [] + for i, cycle in enumerate(cyclic_form): + flag = True + for j in range(len(cumulative_subranks) - 1): + if cyclic_min[i] >= cumulative_subranks[j] and cyclic_max[i] < cumulative_subranks[j+1]: + # Found a sinkable cycle. + args[j] = _permute_dims(args[j], Permutation([[k - cumulative_subranks[j] for k in cycle]])) + flag = False + break + if flag: + cyclic_keep.append(cycle) + return _array_tensor_product(*args), Permutation(cyclic_keep, size=permutation.size) + + def nest_permutation(self): + r""" + DEPRECATED. + """ + ret = self._nest_permutation(self.expr, self.permutation) + if ret is None: + return self + return ret + + @classmethod + def _nest_permutation(cls, expr, permutation): + if isinstance(expr, ArrayTensorProduct): + return _permute_dims(*cls._check_if_there_are_closed_cycles(expr, permutation)) + elif isinstance(expr, ArrayContraction): + # Invert tree hierarchy: put the contraction above. + cycles = permutation.cyclic_form + newcycles = ArrayContraction._convert_outer_indices_to_inner_indices(expr, *cycles) + newpermutation = Permutation(newcycles) + new_contr_indices = [tuple(newpermutation(j) for j in i) for i in expr.contraction_indices] + return _array_contraction(PermuteDims(expr.expr, newpermutation), *new_contr_indices) + elif isinstance(expr, ArrayAdd): + return _array_add(*[PermuteDims(arg, permutation) for arg in expr.args]) + return None + + def as_explicit(self): + expr = self.expr + if hasattr(expr, "as_explicit"): + expr = expr.as_explicit() + return permutedims(expr, self.permutation) + + @classmethod + def _get_permutation_from_arguments(cls, permutation, index_order_old, index_order_new, dim): + if permutation is None: + if index_order_new is None or index_order_old is None: + raise ValueError("Permutation not defined") + return PermuteDims._get_permutation_from_index_orders(index_order_old, index_order_new, dim) + else: + if index_order_new is not None: + raise ValueError("index_order_new cannot be defined with permutation") + if index_order_old is not None: + raise ValueError("index_order_old cannot be defined with permutation") + return permutation + + @classmethod + def _get_permutation_from_index_orders(cls, index_order_old, index_order_new, dim): + if len(set(index_order_new)) != dim: + raise ValueError("wrong number of indices in index_order_new") + if len(set(index_order_old)) != dim: + raise ValueError("wrong number of indices in index_order_old") + if len(set.symmetric_difference(set(index_order_new), set(index_order_old))) > 0: + raise ValueError("index_order_new and index_order_old must have the same indices") + permutation = [index_order_old.index(i) for i in index_order_new] + return permutation + + +class ArrayDiagonal(_CodegenArrayAbstract): + r""" + Class to represent the diagonal operator. + + Explanation + =========== + + In a 2-dimensional array it returns the diagonal, this looks like the + operation: + + `A_{ij} \rightarrow A_{ii}` + + The diagonal over axes 1 and 2 (the second and third) of the tensor product + of two 2-dimensional arrays `A \otimes B` is + + `\Big[ A_{ab} B_{cd} \Big]_{abcd} \rightarrow \Big[ A_{ai} B_{id} \Big]_{adi}` + + In this last example the array expression has been reduced from + 4-dimensional to 3-dimensional. Notice that no contraction has occurred, + rather there is a new index `i` for the diagonal, contraction would have + reduced the array to 2 dimensions. + + Notice that the diagonalized out dimensions are added as new dimensions at + the end of the indices. + """ + + def __new__(cls, expr, *diagonal_indices, **kwargs): + expr = _sympify(expr) + diagonal_indices = [Tuple(*sorted(i)) for i in diagonal_indices] + canonicalize = kwargs.get("canonicalize", False) + + shape = get_shape(expr) + if shape is not None: + cls._validate(expr, *diagonal_indices, **kwargs) + # Get new shape: + positions, shape = cls._get_positions_shape(shape, diagonal_indices) + else: + positions = None + if len(diagonal_indices) == 0: + return expr + obj = Basic.__new__(cls, expr, *diagonal_indices) + obj._positions = positions + obj._subranks = _get_subranks(expr) + obj._shape = shape + if canonicalize: + return obj._canonicalize() + return obj + + def _canonicalize(self): + expr = self.expr + diagonal_indices = self.diagonal_indices + trivial_diags = [i for i in diagonal_indices if len(i) == 1] + if len(trivial_diags) > 0: + trivial_pos = {e[0]: i for i, e in enumerate(diagonal_indices) if len(e) == 1} + diag_pos = {e: i for i, e in enumerate(diagonal_indices) if len(e) > 1} + diagonal_indices_short = [i for i in diagonal_indices if len(i) > 1] + rank1 = get_rank(self) + rank2 = len(diagonal_indices) + rank3 = rank1 - rank2 + inv_permutation = [] + counter1 = 0 + indices_down = ArrayDiagonal._push_indices_down(diagonal_indices_short, list(range(rank1)), get_rank(expr)) + for i in indices_down: + if i in trivial_pos: + inv_permutation.append(rank3 + trivial_pos[i]) + elif isinstance(i, (Integer, int)): + inv_permutation.append(counter1) + counter1 += 1 + else: + inv_permutation.append(rank3 + diag_pos[i]) + permutation = _af_invert(inv_permutation) + if len(diagonal_indices_short) > 0: + return _permute_dims(_array_diagonal(expr, *diagonal_indices_short), permutation) + else: + return _permute_dims(expr, permutation) + if isinstance(expr, ArrayAdd): + return self._ArrayDiagonal_denest_ArrayAdd(expr, *diagonal_indices) + if isinstance(expr, ArrayDiagonal): + return self._ArrayDiagonal_denest_ArrayDiagonal(expr, *diagonal_indices) + if isinstance(expr, PermuteDims): + return self._ArrayDiagonal_denest_PermuteDims(expr, *diagonal_indices) + if isinstance(expr, (ZeroArray, ZeroMatrix)): + positions, shape = self._get_positions_shape(expr.shape, diagonal_indices) + return ZeroArray(*shape) + return self.func(expr, *diagonal_indices, canonicalize=False) + + @staticmethod + def _validate(expr, *diagonal_indices, **kwargs): + # Check that no diagonalization happens on indices with mismatched + # dimensions: + shape = get_shape(expr) + for i in diagonal_indices: + if any(j >= len(shape) for j in i): + raise ValueError("index is larger than expression shape") + if len({shape[j] for j in i}) != 1: + raise ValueError("diagonalizing indices of different dimensions") + if not kwargs.get("allow_trivial_diags", False) and len(i) <= 1: + raise ValueError("need at least two axes to diagonalize") + if len(set(i)) != len(i): + raise ValueError("axis index cannot be repeated") + + @staticmethod + def _remove_trivial_dimensions(shape, *diagonal_indices): + return [tuple(j for j in i) for i in diagonal_indices if shape[i[0]] != 1] + + @property + def expr(self): + return self.args[0] + + @property + def diagonal_indices(self): + return self.args[1:] + + @staticmethod + def _flatten(expr, *outer_diagonal_indices): + inner_diagonal_indices = expr.diagonal_indices + all_inner = [j for i in inner_diagonal_indices for j in i] + all_inner.sort() + # TODO: add API for total rank and cumulative rank: + total_rank = _get_subrank(expr) + inner_rank = len(all_inner) + outer_rank = total_rank - inner_rank + shifts = [0 for i in range(outer_rank)] + counter = 0 + pointer = 0 + for i in range(outer_rank): + while pointer < inner_rank and counter >= all_inner[pointer]: + counter += 1 + pointer += 1 + shifts[i] += pointer + counter += 1 + outer_diagonal_indices = tuple(tuple(shifts[j] + j for j in i) for i in outer_diagonal_indices) + diagonal_indices = inner_diagonal_indices + outer_diagonal_indices + return _array_diagonal(expr.expr, *diagonal_indices) + + @classmethod + def _ArrayDiagonal_denest_ArrayAdd(cls, expr, *diagonal_indices): + return _array_add(*[_array_diagonal(arg, *diagonal_indices) for arg in expr.args]) + + @classmethod + def _ArrayDiagonal_denest_ArrayDiagonal(cls, expr, *diagonal_indices): + return cls._flatten(expr, *diagonal_indices) + + @classmethod + def _ArrayDiagonal_denest_PermuteDims(cls, expr: PermuteDims, *diagonal_indices): + back_diagonal_indices = [[expr.permutation(j) for j in i] for i in diagonal_indices] + nondiag = [i for i in range(get_rank(expr)) if not any(i in j for j in diagonal_indices)] + back_nondiag = [expr.permutation(i) for i in nondiag] + remap = {e: i for i, e in enumerate(sorted(back_nondiag))} + new_permutation1 = [remap[i] for i in back_nondiag] + shift = len(new_permutation1) + diag_block_perm = [i + shift for i in range(len(back_diagonal_indices))] + new_permutation = new_permutation1 + diag_block_perm + return _permute_dims( + _array_diagonal( + expr.expr, + *back_diagonal_indices + ), + new_permutation + ) + + def _push_indices_down_nonstatic(self, indices): + transform = lambda x: self._positions[x] if x < len(self._positions) else None + return _apply_recursively_over_nested_lists(transform, indices) + + def _push_indices_up_nonstatic(self, indices): + + def transform(x): + for i, e in enumerate(self._positions): + if (isinstance(e, int) and x == e) or (isinstance(e, tuple) and x in e): + return i + + return _apply_recursively_over_nested_lists(transform, indices) + + @classmethod + def _push_indices_down(cls, diagonal_indices, indices, rank): + positions, shape = cls._get_positions_shape(range(rank), diagonal_indices) + transform = lambda x: positions[x] if x < len(positions) else None + return _apply_recursively_over_nested_lists(transform, indices) + + @classmethod + def _push_indices_up(cls, diagonal_indices, indices, rank): + positions, shape = cls._get_positions_shape(range(rank), diagonal_indices) + + def transform(x): + for i, e in enumerate(positions): + if (isinstance(e, int) and x == e) or (isinstance(e, (tuple, Tuple)) and (x in e)): + return i + + return _apply_recursively_over_nested_lists(transform, indices) + + @classmethod + def _get_positions_shape(cls, shape, diagonal_indices): + data1 = tuple((i, shp) for i, shp in enumerate(shape) if not any(i in j for j in diagonal_indices)) + pos1, shp1 = zip(*data1) if data1 else ((), ()) + data2 = tuple((i, shape[i[0]]) for i in diagonal_indices) + pos2, shp2 = zip(*data2) if data2 else ((), ()) + positions = pos1 + pos2 + shape = shp1 + shp2 + return positions, shape + + def as_explicit(self): + expr = self.expr + if hasattr(expr, "as_explicit"): + expr = expr.as_explicit() + return tensordiagonal(expr, *self.diagonal_indices) + + +class ArrayElementwiseApplyFunc(_CodegenArrayAbstract): + + def __new__(cls, function, element): + + if not isinstance(function, Lambda): + d = Dummy('d') + function = Lambda(d, function(d)) + + obj = _CodegenArrayAbstract.__new__(cls, function, element) + obj._subranks = _get_subranks(element) + return obj + + @property + def function(self): + return self.args[0] + + @property + def expr(self): + return self.args[1] + + @property + def shape(self): + return self.expr.shape + + def _get_function_fdiff(self): + d = Dummy("d") + function = self.function(d) + fdiff = function.diff(d) + if isinstance(fdiff, Function): + fdiff = type(fdiff) + else: + fdiff = Lambda(d, fdiff) + return fdiff + + def as_explicit(self): + expr = self.expr + if hasattr(expr, "as_explicit"): + expr = expr.as_explicit() + return expr.applyfunc(self.function) + + +class ArrayContraction(_CodegenArrayAbstract): + r""" + This class is meant to represent contractions of arrays in a form easily + processable by the code printers. + """ + + def __new__(cls, expr, *contraction_indices, **kwargs): + contraction_indices = _sort_contraction_indices(contraction_indices) + expr = _sympify(expr) + + canonicalize = kwargs.get("canonicalize", False) + + obj = Basic.__new__(cls, expr, *contraction_indices) + obj._subranks = _get_subranks(expr) + obj._mapping = _get_mapping_from_subranks(obj._subranks) + + free_indices_to_position = {i: i for i in range(sum(obj._subranks)) if all(i not in cind for cind in contraction_indices)} + obj._free_indices_to_position = free_indices_to_position + + shape = get_shape(expr) + cls._validate(expr, *contraction_indices) + if shape: + shape = tuple(shp for i, shp in enumerate(shape) if not any(i in j for j in contraction_indices)) + obj._shape = shape + if canonicalize: + return obj._canonicalize() + return obj + + def _canonicalize(self): + expr = self.expr + contraction_indices = self.contraction_indices + + if len(contraction_indices) == 0: + return expr + + if isinstance(expr, ArrayContraction): + return self._ArrayContraction_denest_ArrayContraction(expr, *contraction_indices) + + if isinstance(expr, (ZeroArray, ZeroMatrix)): + return self._ArrayContraction_denest_ZeroArray(expr, *contraction_indices) + + if isinstance(expr, PermuteDims): + return self._ArrayContraction_denest_PermuteDims(expr, *contraction_indices) + + if isinstance(expr, ArrayTensorProduct): + expr, contraction_indices = self._sort_fully_contracted_args(expr, contraction_indices) + expr, contraction_indices = self._lower_contraction_to_addends(expr, contraction_indices) + if len(contraction_indices) == 0: + return expr + + if isinstance(expr, ArrayDiagonal): + return self._ArrayContraction_denest_ArrayDiagonal(expr, *contraction_indices) + + if isinstance(expr, ArrayAdd): + return self._ArrayContraction_denest_ArrayAdd(expr, *contraction_indices) + + # Check single index contractions on 1-dimensional axes: + contraction_indices = [i for i in contraction_indices if len(i) > 1 or get_shape(expr)[i[0]] != 1] + if len(contraction_indices) == 0: + return expr + + return self.func(expr, *contraction_indices, canonicalize=False) + + def __mul__(self, other): + if other == 1: + return self + else: + raise NotImplementedError("Product of N-dim arrays is not uniquely defined. Use another method.") + + def __rmul__(self, other): + if other == 1: + return self + else: + raise NotImplementedError("Product of N-dim arrays is not uniquely defined. Use another method.") + + @staticmethod + def _validate(expr, *contraction_indices): + shape = get_shape(expr) + if shape is None: + return + + # Check that no contraction happens when the shape is mismatched: + for i in contraction_indices: + if len({shape[j] for j in i if shape[j] != -1}) != 1: + raise ValueError("contracting indices of different dimensions") + + @classmethod + def _push_indices_down(cls, contraction_indices, indices): + flattened_contraction_indices = [j for i in contraction_indices for j in i] + flattened_contraction_indices.sort() + transform = _build_push_indices_down_func_transformation(flattened_contraction_indices) + return _apply_recursively_over_nested_lists(transform, indices) + + @classmethod + def _push_indices_up(cls, contraction_indices, indices): + flattened_contraction_indices = [j for i in contraction_indices for j in i] + flattened_contraction_indices.sort() + transform = _build_push_indices_up_func_transformation(flattened_contraction_indices) + return _apply_recursively_over_nested_lists(transform, indices) + + @classmethod + def _lower_contraction_to_addends(cls, expr, contraction_indices): + if isinstance(expr, ArrayAdd): + raise NotImplementedError() + if not isinstance(expr, ArrayTensorProduct): + return expr, contraction_indices + subranks = expr.subranks + cumranks = list(accumulate([0] + subranks)) + contraction_indices_remaining = [] + contraction_indices_args = [[] for i in expr.args] + backshift = set() + for contraction_group in contraction_indices: + for j in range(len(expr.args)): + if not isinstance(expr.args[j], ArrayAdd): + continue + if all(cumranks[j] <= k < cumranks[j+1] for k in contraction_group): + contraction_indices_args[j].append([k - cumranks[j] for k in contraction_group]) + backshift.update(contraction_group) + break + else: + contraction_indices_remaining.append(contraction_group) + if len(contraction_indices_remaining) == len(contraction_indices): + return expr, contraction_indices + total_rank = get_rank(expr) + shifts = list(accumulate([1 if i in backshift else 0 for i in range(total_rank)])) + contraction_indices_remaining = [Tuple.fromiter(j - shifts[j] for j in i) for i in contraction_indices_remaining] + ret = _array_tensor_product(*[ + _array_contraction(arg, *contr) for arg, contr in zip(expr.args, contraction_indices_args) + ]) + return ret, contraction_indices_remaining + + def split_multiple_contractions(self): + """ + Recognize multiple contractions and attempt at rewriting them as paired-contractions. + + This allows some contractions involving more than two indices to be + rewritten as multiple contractions involving two indices, thus allowing + the expression to be rewritten as a matrix multiplication line. + + Examples: + + * `A_ij b_j0 C_jk` ===> `A*DiagMatrix(b)*C` + + Care for: + - matrix being diagonalized (i.e. `A_ii`) + - vectors being diagonalized (i.e. `a_i0`) + + Multiple contractions can be split into matrix multiplications if + not more than two arguments are non-diagonals or non-vectors. + Vectors get diagonalized while diagonal matrices remain diagonal. + The non-diagonal matrices can be at the beginning or at the end + of the final matrix multiplication line. + """ + + editor = _EditArrayContraction(self) + + contraction_indices = self.contraction_indices + + onearray_insert = [] + + for indl, links in enumerate(contraction_indices): + if len(links) <= 2: + continue + + # Check multiple contractions: + # + # Examples: + # + # * `A_ij b_j0 C_jk` ===> `A*DiagMatrix(b)*C \otimes OneArray(1)` with permutation (1 2) + # + # Care for: + # - matrix being diagonalized (i.e. `A_ii`) + # - vectors being diagonalized (i.e. `a_i0`) + + # Multiple contractions can be split into matrix multiplications if + # not more than three arguments are non-diagonals or non-vectors. + # + # Vectors get diagonalized while diagonal matrices remain diagonal. + # The non-diagonal matrices can be at the beginning or at the end + # of the final matrix multiplication line. + + positions = editor.get_mapping_for_index(indl) + + # Also consider the case of diagonal matrices being contracted: + current_dimension = self.expr.shape[links[0]] + + not_vectors = [] + vectors = [] + for arg_ind, rel_ind in positions: + arg = editor.args_with_ind[arg_ind] + mat = arg.element + abs_arg_start, abs_arg_end = editor.get_absolute_range(arg) + other_arg_pos = 1-rel_ind + other_arg_abs = abs_arg_start + other_arg_pos + if ((1 not in mat.shape) or + ((current_dimension == 1) is True and mat.shape != (1, 1)) or + any(other_arg_abs in l for li, l in enumerate(contraction_indices) if li != indl) + ): + not_vectors.append((arg, rel_ind)) + else: + vectors.append((arg, rel_ind)) + if len(not_vectors) > 2: + # If more than two arguments in the multiple contraction are + # non-vectors and non-diagonal matrices, we cannot find a way + # to split this contraction into a matrix multiplication line: + continue + # Three cases to handle: + # - zero non-vectors + # - one non-vector + # - two non-vectors + for v, rel_ind in vectors: + v.element = diagonalize_vector(v.element) + vectors_to_loop = not_vectors[:1] + vectors + not_vectors[1:] + first_not_vector, rel_ind = vectors_to_loop[0] + new_index = first_not_vector.indices[rel_ind] + + for v, rel_ind in vectors_to_loop[1:-1]: + v.indices[rel_ind] = new_index + new_index = editor.get_new_contraction_index() + assert v.indices.index(None) == 1 - rel_ind + v.indices[v.indices.index(None)] = new_index + onearray_insert.append(v) + + last_vec, rel_ind = vectors_to_loop[-1] + last_vec.indices[rel_ind] = new_index + + for v in onearray_insert: + editor.insert_after(v, _ArgE(OneArray(1), [None])) + + return editor.to_array_contraction() + + def flatten_contraction_of_diagonal(self): + if not isinstance(self.expr, ArrayDiagonal): + return self + contraction_down = self.expr._push_indices_down(self.expr.diagonal_indices, self.contraction_indices) + new_contraction_indices = [] + diagonal_indices = self.expr.diagonal_indices[:] + for i in contraction_down: + contraction_group = list(i) + for j in i: + diagonal_with = [k for k in diagonal_indices if j in k] + contraction_group.extend([l for k in diagonal_with for l in k]) + diagonal_indices = [k for k in diagonal_indices if k not in diagonal_with] + new_contraction_indices.append(sorted(set(contraction_group))) + + new_contraction_indices = ArrayDiagonal._push_indices_up(diagonal_indices, new_contraction_indices) + return _array_contraction( + _array_diagonal( + self.expr.expr, + *diagonal_indices + ), + *new_contraction_indices + ) + + @staticmethod + def _get_free_indices_to_position_map(free_indices, contraction_indices): + free_indices_to_position = {} + flattened_contraction_indices = [j for i in contraction_indices for j in i] + counter = 0 + for ind in free_indices: + while counter in flattened_contraction_indices: + counter += 1 + free_indices_to_position[ind] = counter + counter += 1 + return free_indices_to_position + + @staticmethod + def _get_index_shifts(expr): + """ + Get the mapping of indices at the positions before the contraction + occurs. + + Examples + ======== + + >>> from sympy.tensor.array import tensorproduct, tensorcontraction + >>> from sympy import MatrixSymbol + >>> M = MatrixSymbol("M", 3, 3) + >>> N = MatrixSymbol("N", 3, 3) + >>> cg = tensorcontraction(tensorproduct(M, N), [1, 2]) + >>> cg._get_index_shifts(cg) + [0, 2] + + Indeed, ``cg`` after the contraction has two dimensions, 0 and 1. They + need to be shifted by 0 and 2 to get the corresponding positions before + the contraction (that is, 0 and 3). + """ + inner_contraction_indices = expr.contraction_indices + all_inner = [j for i in inner_contraction_indices for j in i] + all_inner.sort() + # TODO: add API for total rank and cumulative rank: + total_rank = _get_subrank(expr) + inner_rank = len(all_inner) + outer_rank = total_rank - inner_rank + shifts = [0 for i in range(outer_rank)] + counter = 0 + pointer = 0 + for i in range(outer_rank): + while pointer < inner_rank and counter >= all_inner[pointer]: + counter += 1 + pointer += 1 + shifts[i] += pointer + counter += 1 + return shifts + + @staticmethod + def _convert_outer_indices_to_inner_indices(expr, *outer_contraction_indices): + shifts = ArrayContraction._get_index_shifts(expr) + outer_contraction_indices = tuple(tuple(shifts[j] + j for j in i) for i in outer_contraction_indices) + return outer_contraction_indices + + @staticmethod + def _flatten(expr, *outer_contraction_indices): + inner_contraction_indices = expr.contraction_indices + outer_contraction_indices = ArrayContraction._convert_outer_indices_to_inner_indices(expr, *outer_contraction_indices) + contraction_indices = inner_contraction_indices + outer_contraction_indices + return _array_contraction(expr.expr, *contraction_indices) + + @classmethod + def _ArrayContraction_denest_ArrayContraction(cls, expr, *contraction_indices): + return cls._flatten(expr, *contraction_indices) + + @classmethod + def _ArrayContraction_denest_ZeroArray(cls, expr, *contraction_indices): + contraction_indices_flat = [j for i in contraction_indices for j in i] + shape = [e for i, e in enumerate(expr.shape) if i not in contraction_indices_flat] + return ZeroArray(*shape) + + @classmethod + def _ArrayContraction_denest_ArrayAdd(cls, expr, *contraction_indices): + return _array_add(*[_array_contraction(i, *contraction_indices) for i in expr.args]) + + @classmethod + def _ArrayContraction_denest_PermuteDims(cls, expr, *contraction_indices): + permutation = expr.permutation + plist = permutation.array_form + new_contraction_indices = [tuple(permutation(j) for j in i) for i in contraction_indices] + new_plist = [i for i in plist if not any(i in j for j in new_contraction_indices)] + new_plist = cls._push_indices_up(new_contraction_indices, new_plist) + return _permute_dims( + _array_contraction(expr.expr, *new_contraction_indices), + Permutation(new_plist) + ) + + @classmethod + def _ArrayContraction_denest_ArrayDiagonal(cls, expr: 'ArrayDiagonal', *contraction_indices): + diagonal_indices = list(expr.diagonal_indices) + down_contraction_indices = expr._push_indices_down(expr.diagonal_indices, contraction_indices, get_rank(expr.expr)) + # Flatten diagonally contracted indices: + down_contraction_indices = [[k for j in i for k in (j if isinstance(j, (tuple, Tuple)) else [j])] for i in down_contraction_indices] + new_contraction_indices = [] + for contr_indgrp in down_contraction_indices: + ind = contr_indgrp[:] + for j, diag_indgrp in enumerate(diagonal_indices): + if diag_indgrp is None: + continue + if any(i in diag_indgrp for i in contr_indgrp): + ind.extend(diag_indgrp) + diagonal_indices[j] = None + new_contraction_indices.append(sorted(set(ind))) + + new_diagonal_indices_down = [i for i in diagonal_indices if i is not None] + new_diagonal_indices = ArrayContraction._push_indices_up(new_contraction_indices, new_diagonal_indices_down) + return _array_diagonal( + _array_contraction(expr.expr, *new_contraction_indices), + *new_diagonal_indices + ) + + @classmethod + def _sort_fully_contracted_args(cls, expr, contraction_indices): + if expr.shape is None: + return expr, contraction_indices + cumul = list(accumulate([0] + expr.subranks)) + index_blocks = [list(range(cumul[i], cumul[i+1])) for i in range(len(expr.args))] + contraction_indices_flat = {j for i in contraction_indices for j in i} + fully_contracted = [all(j in contraction_indices_flat for j in range(cumul[i], cumul[i+1])) for i, arg in enumerate(expr.args)] + new_pos = sorted(range(len(expr.args)), key=lambda x: (0, default_sort_key(expr.args[x])) if fully_contracted[x] else (1,)) + new_args = [expr.args[i] for i in new_pos] + new_index_blocks_flat = [j for i in new_pos for j in index_blocks[i]] + index_permutation_array_form = _af_invert(new_index_blocks_flat) + new_contraction_indices = [tuple(index_permutation_array_form[j] for j in i) for i in contraction_indices] + new_contraction_indices = _sort_contraction_indices(new_contraction_indices) + return _array_tensor_product(*new_args), new_contraction_indices + + def _get_contraction_tuples(self): + r""" + Return tuples containing the argument index and position within the + argument of the index position. + + Examples + ======== + + >>> from sympy import MatrixSymbol + >>> from sympy.abc import N + >>> from sympy.tensor.array import tensorproduct, tensorcontraction + >>> A = MatrixSymbol("A", N, N) + >>> B = MatrixSymbol("B", N, N) + + >>> cg = tensorcontraction(tensorproduct(A, B), (1, 2)) + >>> cg._get_contraction_tuples() + [[(0, 1), (1, 0)]] + + Notes + ===== + + Here the contraction pair `(1, 2)` meaning that the 2nd and 3rd indices + of the tensor product `A\otimes B` are contracted, has been transformed + into `(0, 1)` and `(1, 0)`, identifying the same indices in a different + notation. `(0, 1)` is the second index (1) of the first argument (i.e. + 0 or `A`). `(1, 0)` is the first index (i.e. 0) of the second + argument (i.e. 1 or `B`). + """ + mapping = self._mapping + return [[mapping[j] for j in i] for i in self.contraction_indices] + + @staticmethod + def _contraction_tuples_to_contraction_indices(expr, contraction_tuples): + # TODO: check that `expr` has `.subranks`: + ranks = expr.subranks + cumulative_ranks = [0] + list(accumulate(ranks)) + return [tuple(cumulative_ranks[j]+k for j, k in i) for i in contraction_tuples] + + @property + def free_indices(self): + return self._free_indices[:] + + @property + def free_indices_to_position(self): + return dict(self._free_indices_to_position) + + @property + def expr(self): + return self.args[0] + + @property + def contraction_indices(self): + return self.args[1:] + + def _contraction_indices_to_components(self): + expr = self.expr + if not isinstance(expr, ArrayTensorProduct): + raise NotImplementedError("only for contractions of tensor products") + ranks = expr.subranks + mapping = {} + counter = 0 + for i, rank in enumerate(ranks): + for j in range(rank): + mapping[counter] = (i, j) + counter += 1 + return mapping + + def sort_args_by_name(self): + """ + Sort arguments in the tensor product so that their order is lexicographical. + + Examples + ======== + + >>> from sympy.tensor.array.expressions.from_matrix_to_array import convert_matrix_to_array + >>> from sympy import MatrixSymbol + >>> from sympy.abc import N + >>> A = MatrixSymbol("A", N, N) + >>> B = MatrixSymbol("B", N, N) + >>> C = MatrixSymbol("C", N, N) + >>> D = MatrixSymbol("D", N, N) + + >>> cg = convert_matrix_to_array(C*D*A*B) + >>> cg + ArrayContraction(ArrayTensorProduct(A, D, C, B), (0, 3), (1, 6), (2, 5)) + >>> cg.sort_args_by_name() + ArrayContraction(ArrayTensorProduct(A, D, B, C), (0, 3), (1, 4), (2, 7)) + """ + expr = self.expr + if not isinstance(expr, ArrayTensorProduct): + return self + args = expr.args + sorted_data = sorted(enumerate(args), key=lambda x: default_sort_key(x[1])) + pos_sorted, args_sorted = zip(*sorted_data) + reordering_map = {i: pos_sorted.index(i) for i, arg in enumerate(args)} + contraction_tuples = self._get_contraction_tuples() + contraction_tuples = [[(reordering_map[j], k) for j, k in i] for i in contraction_tuples] + c_tp = _array_tensor_product(*args_sorted) + new_contr_indices = self._contraction_tuples_to_contraction_indices( + c_tp, + contraction_tuples + ) + return _array_contraction(c_tp, *new_contr_indices) + + def _get_contraction_links(self): + r""" + Returns a dictionary of links between arguments in the tensor product + being contracted. + + See the example for an explanation of the values. + + Examples + ======== + + >>> from sympy import MatrixSymbol + >>> from sympy.abc import N + >>> from sympy.tensor.array.expressions.from_matrix_to_array import convert_matrix_to_array + >>> A = MatrixSymbol("A", N, N) + >>> B = MatrixSymbol("B", N, N) + >>> C = MatrixSymbol("C", N, N) + >>> D = MatrixSymbol("D", N, N) + + Matrix multiplications are pairwise contractions between neighboring + matrices: + + `A_{ij} B_{jk} C_{kl} D_{lm}` + + >>> cg = convert_matrix_to_array(A*B*C*D) + >>> cg + ArrayContraction(ArrayTensorProduct(B, C, A, D), (0, 5), (1, 2), (3, 6)) + + >>> cg._get_contraction_links() + {0: {0: (2, 1), 1: (1, 0)}, 1: {0: (0, 1), 1: (3, 0)}, 2: {1: (0, 0)}, 3: {0: (1, 1)}} + + This dictionary is interpreted as follows: argument in position 0 (i.e. + matrix `A`) has its second index (i.e. 1) contracted to `(1, 0)`, that + is argument in position 1 (matrix `B`) on the first index slot of `B`, + this is the contraction provided by the index `j` from `A`. + + The argument in position 1 (that is, matrix `B`) has two contractions, + the ones provided by the indices `j` and `k`, respectively the first + and second indices (0 and 1 in the sub-dict). The link `(0, 1)` and + `(2, 0)` respectively. `(0, 1)` is the index slot 1 (the 2nd) of + argument in position 0 (that is, `A_{\ldot j}`), and so on. + """ + args, dlinks = _get_contraction_links([self], self.subranks, *self.contraction_indices) + return dlinks + + def as_explicit(self): + expr = self.expr + if hasattr(expr, "as_explicit"): + expr = expr.as_explicit() + return tensorcontraction(expr, *self.contraction_indices) + + +class Reshape(_CodegenArrayAbstract): + """ + Reshape the dimensions of an array expression. + + Examples + ======== + + >>> from sympy.tensor.array.expressions import ArraySymbol, Reshape + >>> A = ArraySymbol("A", (6,)) + >>> A.shape + (6,) + >>> Reshape(A, (3, 2)).shape + (3, 2) + + Check the component-explicit forms: + + >>> A.as_explicit() + [A[0], A[1], A[2], A[3], A[4], A[5]] + >>> Reshape(A, (3, 2)).as_explicit() + [[A[0], A[1]], [A[2], A[3]], [A[4], A[5]]] + + """ + + def __new__(cls, expr, shape): + expr = _sympify(expr) + if not isinstance(shape, Tuple): + shape = Tuple(*shape) + if Equality(Mul.fromiter(expr.shape), Mul.fromiter(shape)) == False: + raise ValueError("shape mismatch") + obj = Expr.__new__(cls, expr, shape) + obj._shape = tuple(shape) + obj._expr = expr + return obj + + @property + def shape(self): + return self._shape + + @property + def expr(self): + return self._expr + + def doit(self, *args, **kwargs): + if kwargs.get("deep", True): + expr = self.expr.doit(*args, **kwargs) + else: + expr = self.expr + if isinstance(expr, (MatrixBase, NDimArray)): + return expr.reshape(*self.shape) + return Reshape(expr, self.shape) + + def as_explicit(self): + ee = self.expr + if hasattr(ee, "as_explicit"): + ee = ee.as_explicit() + if isinstance(ee, MatrixBase): + from sympy import Array + ee = Array(ee) + elif isinstance(ee, MatrixExpr): + return self + return ee.reshape(*self.shape) + + +class _ArgE: + """ + The ``_ArgE`` object contains references to the array expression + (``.element``) and a list containing the information about index + contractions (``.indices``). + + Index contractions are numbered and contracted indices show the number of + the contraction. Uncontracted indices have ``None`` value. + + For example: + ``_ArgE(M, [None, 3])`` + This object means that expression ``M`` is part of an array contraction + and has two indices, the first is not contracted (value ``None``), + the second index is contracted to the 4th (i.e. number ``3``) group of the + array contraction object. + """ + indices: list[int | None] + + def __init__(self, element, indices: list[int | None] | None = None): + self.element = element + if indices is None: + self.indices = [None for i in range(get_rank(element))] + else: + self.indices = indices + + def __str__(self): + return "_ArgE(%s, %s)" % (self.element, self.indices) + + __repr__ = __str__ + + +class _IndPos: + """ + Index position, requiring two integers in the constructor: + + - arg: the position of the argument in the tensor product, + - rel: the relative position of the index inside the argument. + """ + def __init__(self, arg: int, rel: int): + self.arg = arg + self.rel = rel + + def __str__(self): + return "_IndPos(%i, %i)" % (self.arg, self.rel) + + __repr__ = __str__ + + def __iter__(self): + yield from [self.arg, self.rel] + + +class _EditArrayContraction: + """ + Utility class to help manipulate array contraction objects. + + This class takes as input an ``ArrayContraction`` object and turns it into + an editable object. + + The field ``args_with_ind`` of this class is a list of ``_ArgE`` objects + which can be used to easily edit the contraction structure of the + expression. + + Once editing is finished, the ``ArrayContraction`` object may be recreated + by calling the ``.to_array_contraction()`` method. + """ + + def __init__(self, base_array: typing.Union[ArrayContraction, ArrayDiagonal, ArrayTensorProduct]): + + expr: Basic + diagonalized: tuple[tuple[int, ...], ...] + contraction_indices: list[tuple[int]] + if isinstance(base_array, ArrayContraction): + mapping = _get_mapping_from_subranks(base_array.subranks) + expr = base_array.expr + contraction_indices = base_array.contraction_indices + diagonalized = () + elif isinstance(base_array, ArrayDiagonal): + + if isinstance(base_array.expr, ArrayContraction): + mapping = _get_mapping_from_subranks(base_array.expr.subranks) + expr = base_array.expr.expr + diagonalized = ArrayContraction._push_indices_down(base_array.expr.contraction_indices, base_array.diagonal_indices) + contraction_indices = base_array.expr.contraction_indices + elif isinstance(base_array.expr, ArrayTensorProduct): + mapping = {} + expr = base_array.expr + diagonalized = base_array.diagonal_indices + contraction_indices = [] + else: + mapping = {} + expr = base_array.expr + diagonalized = base_array.diagonal_indices + contraction_indices = [] + + elif isinstance(base_array, ArrayTensorProduct): + expr = base_array + contraction_indices = [] + diagonalized = () + else: + raise NotImplementedError() + + if isinstance(expr, ArrayTensorProduct): + args = list(expr.args) + else: + args = [expr] + + args_with_ind: list[_ArgE] = [_ArgE(arg) for arg in args] + for i, contraction_tuple in enumerate(contraction_indices): + for j in contraction_tuple: + arg_pos, rel_pos = mapping[j] + args_with_ind[arg_pos].indices[rel_pos] = i + self.args_with_ind: list[_ArgE] = args_with_ind + self.number_of_contraction_indices: int = len(contraction_indices) + self._track_permutation: list[list[int]] | None = None + + mapping = _get_mapping_from_subranks(base_array.subranks) + + # Trick: add diagonalized indices as negative indices into the editor object: + for i, e in enumerate(diagonalized): + for j in e: + arg_pos, rel_pos = mapping[j] + self.args_with_ind[arg_pos].indices[rel_pos] = -1 - i + + def insert_after(self, arg: _ArgE, new_arg: _ArgE): + pos = self.args_with_ind.index(arg) + self.args_with_ind.insert(pos + 1, new_arg) + + def get_new_contraction_index(self): + self.number_of_contraction_indices += 1 + return self.number_of_contraction_indices - 1 + + def refresh_indices(self): + updates = {} + for arg_with_ind in self.args_with_ind: + updates.update({i: -1 for i in arg_with_ind.indices if i is not None}) + for i, e in enumerate(sorted(updates)): + updates[e] = i + self.number_of_contraction_indices = len(updates) + for arg_with_ind in self.args_with_ind: + arg_with_ind.indices = [updates.get(i, None) for i in arg_with_ind.indices] + + def merge_scalars(self): + scalars = [] + for arg_with_ind in self.args_with_ind: + if len(arg_with_ind.indices) == 0: + scalars.append(arg_with_ind) + for i in scalars: + self.args_with_ind.remove(i) + scalar = Mul.fromiter([i.element for i in scalars]) + if len(self.args_with_ind) == 0: + self.args_with_ind.append(_ArgE(scalar)) + else: + from sympy.tensor.array.expressions.from_array_to_matrix import _a2m_tensor_product + self.args_with_ind[0].element = _a2m_tensor_product(scalar, self.args_with_ind[0].element) + + def to_array_contraction(self): + + # Count the ranks of the arguments: + counter = 0 + # Create a collector for the new diagonal indices: + diag_indices = defaultdict(list) + + count_index_freq = Counter() + for arg_with_ind in self.args_with_ind: + count_index_freq.update(Counter(arg_with_ind.indices)) + + free_index_count = count_index_freq[None] + + # Construct the inverse permutation: + inv_perm1 = [] + inv_perm2 = [] + # Keep track of which diagonal indices have already been processed: + done = set() + + # Counter for the diagonal indices: + counter4 = 0 + + for arg_with_ind in self.args_with_ind: + # If some diagonalization axes have been removed, they should be + # permuted in order to keep the permutation. + # Add permutation here + counter2 = 0 # counter for the indices + for i in arg_with_ind.indices: + if i is None: + inv_perm1.append(counter4) + counter2 += 1 + counter4 += 1 + continue + if i >= 0: + continue + # Reconstruct the diagonal indices: + diag_indices[-1 - i].append(counter + counter2) + if count_index_freq[i] == 1 and i not in done: + inv_perm1.append(free_index_count - 1 - i) + done.add(i) + elif i not in done: + inv_perm2.append(free_index_count - 1 - i) + done.add(i) + counter2 += 1 + # Remove negative indices to restore a proper editor object: + arg_with_ind.indices = [i if i is not None and i >= 0 else None for i in arg_with_ind.indices] + counter += len([i for i in arg_with_ind.indices if i is None or i < 0]) + + inverse_permutation = inv_perm1 + inv_perm2 + permutation = _af_invert(inverse_permutation) + + # Get the diagonal indices after the detection of HadamardProduct in the expression: + diag_indices_filtered = [tuple(v) for v in diag_indices.values() if len(v) > 1] + + self.merge_scalars() + self.refresh_indices() + args = [arg.element for arg in self.args_with_ind] + contraction_indices = self.get_contraction_indices() + expr = _array_contraction(_array_tensor_product(*args), *contraction_indices) + expr2 = _array_diagonal(expr, *diag_indices_filtered) + if self._track_permutation is not None: + permutation2 = _af_invert([j for i in self._track_permutation for j in i]) + expr2 = _permute_dims(expr2, permutation2) + + expr3 = _permute_dims(expr2, permutation) + return expr3 + + def get_contraction_indices(self) -> list[list[int]]: + contraction_indices: list[list[int]] = [[] for i in range(self.number_of_contraction_indices)] + current_position: int = 0 + for arg_with_ind in self.args_with_ind: + for j in arg_with_ind.indices: + if j is not None: + contraction_indices[j].append(current_position) + current_position += 1 + return contraction_indices + + def get_mapping_for_index(self, ind) -> list[_IndPos]: + if ind >= self.number_of_contraction_indices: + raise ValueError("index value exceeding the index range") + positions: list[_IndPos] = [] + for i, arg_with_ind in enumerate(self.args_with_ind): + for j, arg_ind in enumerate(arg_with_ind.indices): + if ind == arg_ind: + positions.append(_IndPos(i, j)) + return positions + + def get_contraction_indices_to_ind_rel_pos(self) -> list[list[_IndPos]]: + contraction_indices: list[list[_IndPos]] = [[] for i in range(self.number_of_contraction_indices)] + for i, arg_with_ind in enumerate(self.args_with_ind): + for j, ind in enumerate(arg_with_ind.indices): + if ind is not None: + contraction_indices[ind].append(_IndPos(i, j)) + return contraction_indices + + def count_args_with_index(self, index: int) -> int: + """ + Count the number of arguments that have the given index. + """ + counter: int = 0 + for arg_with_ind in self.args_with_ind: + if index in arg_with_ind.indices: + counter += 1 + return counter + + def get_args_with_index(self, index: int) -> list[_ArgE]: + """ + Get a list of arguments having the given index. + """ + ret: list[_ArgE] = [i for i in self.args_with_ind if index in i.indices] + return ret + + @property + def number_of_diagonal_indices(self): + data = set() + for arg in self.args_with_ind: + data.update({i for i in arg.indices if i is not None and i < 0}) + return len(data) + + def track_permutation_start(self): + permutation = [] + perm_diag = [] + counter = 0 + counter2 = -1 + for arg_with_ind in self.args_with_ind: + perm = [] + for i in arg_with_ind.indices: + if i is not None: + if i < 0: + perm_diag.append(counter2) + counter2 -= 1 + continue + perm.append(counter) + counter += 1 + permutation.append(perm) + max_ind = max(max(i) if i else -1 for i in permutation) if permutation else -1 + perm_diag = [max_ind - i for i in perm_diag] + self._track_permutation = permutation + [perm_diag] + + def track_permutation_merge(self, destination: _ArgE, from_element: _ArgE): + index_destination = self.args_with_ind.index(destination) + index_element = self.args_with_ind.index(from_element) + self._track_permutation[index_destination].extend(self._track_permutation[index_element]) # type: ignore + self._track_permutation.pop(index_element) # type: ignore + + def get_absolute_free_range(self, arg: _ArgE) -> typing.Tuple[int, int]: + """ + Return the range of the free indices of the arg as absolute positions + among all free indices. + """ + counter = 0 + for arg_with_ind in self.args_with_ind: + number_free_indices = len([i for i in arg_with_ind.indices if i is None]) + if arg_with_ind == arg: + return counter, counter + number_free_indices + counter += number_free_indices + raise IndexError("argument not found") + + def get_absolute_range(self, arg: _ArgE) -> typing.Tuple[int, int]: + """ + Return the absolute range of indices for arg, disregarding dummy + indices. + """ + counter = 0 + for arg_with_ind in self.args_with_ind: + number_indices = len(arg_with_ind.indices) + if arg_with_ind == arg: + return counter, counter + number_indices + counter += number_indices + raise IndexError("argument not found") + + +def get_rank(expr): + if isinstance(expr, (MatrixExpr, MatrixElement)): + return 2 + if isinstance(expr, _CodegenArrayAbstract): + return len(expr.shape) + if isinstance(expr, NDimArray): + return expr.rank() + if isinstance(expr, Indexed): + return expr.rank + if isinstance(expr, IndexedBase): + shape = expr.shape + if shape is None: + return -1 + else: + return len(shape) + if hasattr(expr, "shape"): + return len(expr.shape) + return 0 + + +def _get_subrank(expr): + if isinstance(expr, _CodegenArrayAbstract): + return expr.subrank() + return get_rank(expr) + + +def _get_subranks(expr): + if isinstance(expr, _CodegenArrayAbstract): + return expr.subranks + else: + return [get_rank(expr)] + + +def get_shape(expr): + if hasattr(expr, "shape"): + return expr.shape + return () + + +def nest_permutation(expr): + if isinstance(expr, PermuteDims): + return expr.nest_permutation() + else: + return expr + + +def _array_tensor_product(*args, **kwargs): + return ArrayTensorProduct(*args, canonicalize=True, **kwargs) + + +def _array_contraction(expr, *contraction_indices, **kwargs): + return ArrayContraction(expr, *contraction_indices, canonicalize=True, **kwargs) + + +def _array_diagonal(expr, *diagonal_indices, **kwargs): + return ArrayDiagonal(expr, *diagonal_indices, canonicalize=True, **kwargs) + + +def _permute_dims(expr, permutation, **kwargs): + return PermuteDims(expr, permutation, canonicalize=True, **kwargs) + + +def _array_add(*args, **kwargs): + return ArrayAdd(*args, canonicalize=True, **kwargs) + + +def _get_array_element_or_slice(expr, indices): + return ArrayElement(expr, indices) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/arrayexpr_derivatives.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/arrayexpr_derivatives.py new file mode 100644 index 0000000000000000000000000000000000000000..ab44a6fbf715ac7f2b8c287dcc84a49289f2dd76 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/arrayexpr_derivatives.py @@ -0,0 +1,194 @@ +import operator +from functools import reduce, singledispatch + +from sympy.core.expr import Expr +from sympy.core.singleton import S +from sympy.matrices.expressions.hadamard import HadamardProduct +from sympy.matrices.expressions.inverse import Inverse +from sympy.matrices.expressions.matexpr import (MatrixExpr, MatrixSymbol) +from sympy.matrices.expressions.special import Identity, OneMatrix +from sympy.matrices.expressions.transpose import Transpose +from sympy.combinatorics.permutations import _af_invert +from sympy.matrices.expressions.applyfunc import ElementwiseApplyFunction +from sympy.tensor.array.expressions.array_expressions import ( + _ArrayExpr, ZeroArray, ArraySymbol, ArrayTensorProduct, ArrayAdd, + PermuteDims, ArrayDiagonal, ArrayElementwiseApplyFunc, get_rank, + get_shape, ArrayContraction, _array_tensor_product, _array_contraction, + _array_diagonal, _array_add, _permute_dims, Reshape) +from sympy.tensor.array.expressions.from_matrix_to_array import convert_matrix_to_array + + +@singledispatch +def array_derive(expr, x): + """ + Derivatives (gradients) for array expressions. + """ + raise NotImplementedError(f"not implemented for type {type(expr)}") + + +@array_derive.register(Expr) +def _(expr: Expr, x: _ArrayExpr): + return ZeroArray(*x.shape) + + +@array_derive.register(ArrayTensorProduct) +def _(expr: ArrayTensorProduct, x: Expr): + args = expr.args + addend_list = [] + for i, arg in enumerate(expr.args): + darg = array_derive(arg, x) + if darg == 0: + continue + args_prev = args[:i] + args_succ = args[i+1:] + shape_prev = reduce(operator.add, map(get_shape, args_prev), ()) + shape_succ = reduce(operator.add, map(get_shape, args_succ), ()) + addend = _array_tensor_product(*args_prev, darg, *args_succ) + tot1 = len(get_shape(x)) + tot2 = tot1 + len(shape_prev) + tot3 = tot2 + len(get_shape(arg)) + tot4 = tot3 + len(shape_succ) + perm = list(range(tot1, tot2)) + \ + list(range(tot1)) + list(range(tot2, tot3)) + \ + list(range(tot3, tot4)) + addend = _permute_dims(addend, _af_invert(perm)) + addend_list.append(addend) + if len(addend_list) == 1: + return addend_list[0] + elif len(addend_list) == 0: + return S.Zero + else: + return _array_add(*addend_list) + + +@array_derive.register(ArraySymbol) +def _(expr: ArraySymbol, x: _ArrayExpr): + if expr == x: + return _permute_dims( + ArrayTensorProduct.fromiter(Identity(i) for i in expr.shape), + [2*i for i in range(len(expr.shape))] + [2*i+1 for i in range(len(expr.shape))] + ) + return ZeroArray(*(x.shape + expr.shape)) + + +@array_derive.register(MatrixSymbol) +def _(expr: MatrixSymbol, x: _ArrayExpr): + m, n = expr.shape + if expr == x: + return _permute_dims( + _array_tensor_product(Identity(m), Identity(n)), + [0, 2, 1, 3] + ) + return ZeroArray(*(x.shape + expr.shape)) + + +@array_derive.register(Identity) +def _(expr: Identity, x: _ArrayExpr): + return ZeroArray(*(x.shape + expr.shape)) + + +@array_derive.register(OneMatrix) +def _(expr: OneMatrix, x: _ArrayExpr): + return ZeroArray(*(x.shape + expr.shape)) + + +@array_derive.register(Transpose) +def _(expr: Transpose, x: Expr): + # D(A.T, A) ==> (m,n,i,j) ==> D(A_ji, A_mn) = d_mj d_ni + # D(B.T, A) ==> (m,n,i,j) ==> D(B_ji, A_mn) + fd = array_derive(expr.arg, x) + return _permute_dims(fd, [0, 1, 3, 2]) + + +@array_derive.register(Inverse) +def _(expr: Inverse, x: Expr): + mat = expr.I + dexpr = array_derive(mat, x) + tp = _array_tensor_product(-expr, dexpr, expr) + mp = _array_contraction(tp, (1, 4), (5, 6)) + pp = _permute_dims(mp, [1, 2, 0, 3]) + return pp + + +@array_derive.register(ElementwiseApplyFunction) +def _(expr: ElementwiseApplyFunction, x: Expr): + assert get_rank(expr) == 2 + assert get_rank(x) == 2 + fdiff = expr._get_function_fdiff() + dexpr = array_derive(expr.expr, x) + tp = _array_tensor_product( + ElementwiseApplyFunction(fdiff, expr.expr), + dexpr + ) + td = _array_diagonal( + tp, (0, 4), (1, 5) + ) + return td + + +@array_derive.register(ArrayElementwiseApplyFunc) +def _(expr: ArrayElementwiseApplyFunc, x: Expr): + fdiff = expr._get_function_fdiff() + subexpr = expr.expr + dsubexpr = array_derive(subexpr, x) + tp = _array_tensor_product( + dsubexpr, + ArrayElementwiseApplyFunc(fdiff, subexpr) + ) + b = get_rank(x) + c = get_rank(expr) + diag_indices = [(b + i, b + c + i) for i in range(c)] + return _array_diagonal(tp, *diag_indices) + + +@array_derive.register(MatrixExpr) +def _(expr: MatrixExpr, x: Expr): + cg = convert_matrix_to_array(expr) + return array_derive(cg, x) + + +@array_derive.register(HadamardProduct) +def _(expr: HadamardProduct, x: Expr): + raise NotImplementedError() + + +@array_derive.register(ArrayContraction) +def _(expr: ArrayContraction, x: Expr): + fd = array_derive(expr.expr, x) + rank_x = len(get_shape(x)) + contraction_indices = expr.contraction_indices + new_contraction_indices = [tuple(j + rank_x for j in i) for i in contraction_indices] + return _array_contraction(fd, *new_contraction_indices) + + +@array_derive.register(ArrayDiagonal) +def _(expr: ArrayDiagonal, x: Expr): + dsubexpr = array_derive(expr.expr, x) + rank_x = len(get_shape(x)) + diag_indices = [[j + rank_x for j in i] for i in expr.diagonal_indices] + return _array_diagonal(dsubexpr, *diag_indices) + + +@array_derive.register(ArrayAdd) +def _(expr: ArrayAdd, x: Expr): + return _array_add(*[array_derive(arg, x) for arg in expr.args]) + + +@array_derive.register(PermuteDims) +def _(expr: PermuteDims, x: Expr): + de = array_derive(expr.expr, x) + perm = [0, 1] + [i + 2 for i in expr.permutation.array_form] + return _permute_dims(de, perm) + + +@array_derive.register(Reshape) +def _(expr: Reshape, x: Expr): + de = array_derive(expr.expr, x) + return Reshape(de, get_shape(x) + expr.shape) + + +def matrix_derive(expr, x): + from sympy.tensor.array.expressions.from_array_to_matrix import convert_array_to_matrix + ce = convert_matrix_to_array(expr) + dce = array_derive(ce, x) + return convert_array_to_matrix(dce).doit() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/conv_array_to_indexed.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/conv_array_to_indexed.py new file mode 100644 index 0000000000000000000000000000000000000000..1929c3401e131cca0a83080131ead9198b37bcbb --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/conv_array_to_indexed.py @@ -0,0 +1,12 @@ +from sympy.tensor.array.expressions import from_array_to_indexed +from sympy.utilities.decorator import deprecated + + +_conv_to_from_decorator = deprecated( + "module has been renamed by replacing 'conv_' with 'from_' in its name", + deprecated_since_version="1.11", + active_deprecations_target="deprecated-conv-array-expr-module-names", +) + + +convert_array_to_indexed = _conv_to_from_decorator(from_array_to_indexed.convert_array_to_indexed) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/conv_array_to_matrix.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/conv_array_to_matrix.py new file mode 100644 index 0000000000000000000000000000000000000000..2708e74aaa98d6ee38eae46d97d4483a546e0776 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/conv_array_to_matrix.py @@ -0,0 +1,6 @@ +from sympy.tensor.array.expressions import from_array_to_matrix +from sympy.tensor.array.expressions.conv_array_to_indexed import _conv_to_from_decorator + +convert_array_to_matrix = _conv_to_from_decorator(from_array_to_matrix.convert_array_to_matrix) +_array2matrix = _conv_to_from_decorator(from_array_to_matrix._array2matrix) +_remove_trivial_dims = _conv_to_from_decorator(from_array_to_matrix._remove_trivial_dims) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/conv_indexed_to_array.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/conv_indexed_to_array.py new file mode 100644 index 0000000000000000000000000000000000000000..6058b31f20778834ea23a01553d594b7965eb6bb --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/conv_indexed_to_array.py @@ -0,0 +1,4 @@ +from sympy.tensor.array.expressions import from_indexed_to_array +from sympy.tensor.array.expressions.conv_array_to_indexed import _conv_to_from_decorator + +convert_indexed_to_array = _conv_to_from_decorator(from_indexed_to_array.convert_indexed_to_array) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/conv_matrix_to_array.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/conv_matrix_to_array.py new file mode 100644 index 0000000000000000000000000000000000000000..46469df60703c237527c0b2834235309640afe7c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/conv_matrix_to_array.py @@ -0,0 +1,4 @@ +from sympy.tensor.array.expressions import from_matrix_to_array +from sympy.tensor.array.expressions.conv_array_to_indexed import _conv_to_from_decorator + +convert_matrix_to_array = _conv_to_from_decorator(from_matrix_to_array.convert_matrix_to_array) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/from_array_to_indexed.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/from_array_to_indexed.py new file mode 100644 index 0000000000000000000000000000000000000000..9eb86e7cfbe31ebfe7c9649803d9cb5e34b98276 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/from_array_to_indexed.py @@ -0,0 +1,84 @@ +import collections.abc +import operator +from itertools import accumulate + +from sympy import Mul, Sum, Dummy, Add +from sympy.tensor.array.expressions import PermuteDims, ArrayAdd, ArrayElementwiseApplyFunc, Reshape +from sympy.tensor.array.expressions.array_expressions import ArrayTensorProduct, get_rank, ArrayContraction, \ + ArrayDiagonal, get_shape, _get_array_element_or_slice, _ArrayExpr +from sympy.tensor.array.expressions.utils import _apply_permutation_to_list + + +def convert_array_to_indexed(expr, indices): + return _ConvertArrayToIndexed().do_convert(expr, indices) + + +class _ConvertArrayToIndexed: + + def __init__(self): + self.count_dummies = 0 + + def do_convert(self, expr, indices): + if isinstance(expr, ArrayTensorProduct): + cumul = list(accumulate([0] + [get_rank(arg) for arg in expr.args])) + indices_grp = [indices[cumul[i]:cumul[i+1]] for i in range(len(expr.args))] + return Mul.fromiter(self.do_convert(arg, ind) for arg, ind in zip(expr.args, indices_grp)) + if isinstance(expr, ArrayContraction): + new_indices = [None for i in range(get_rank(expr.expr))] + limits = [] + bottom_shape = get_shape(expr.expr) + for contraction_index_grp in expr.contraction_indices: + d = Dummy(f"d{self.count_dummies}") + self.count_dummies += 1 + dim = bottom_shape[contraction_index_grp[0]] + limits.append((d, 0, dim-1)) + for i in contraction_index_grp: + new_indices[i] = d + j = 0 + for i in range(len(new_indices)): + if new_indices[i] is None: + new_indices[i] = indices[j] + j += 1 + newexpr = self.do_convert(expr.expr, new_indices) + return Sum(newexpr, *limits) + if isinstance(expr, ArrayDiagonal): + new_indices = [None for i in range(get_rank(expr.expr))] + ind_pos = expr._push_indices_down(expr.diagonal_indices, list(range(len(indices))), get_rank(expr)) + for i, index in zip(ind_pos, indices): + if isinstance(i, collections.abc.Iterable): + for j in i: + new_indices[j] = index + else: + new_indices[i] = index + newexpr = self.do_convert(expr.expr, new_indices) + return newexpr + if isinstance(expr, PermuteDims): + permuted_indices = _apply_permutation_to_list(expr.permutation, indices) + return self.do_convert(expr.expr, permuted_indices) + if isinstance(expr, ArrayAdd): + return Add.fromiter(self.do_convert(arg, indices) for arg in expr.args) + if isinstance(expr, _ArrayExpr): + return expr.__getitem__(tuple(indices)) + if isinstance(expr, ArrayElementwiseApplyFunc): + return expr.function(self.do_convert(expr.expr, indices)) + if isinstance(expr, Reshape): + shape_up = expr.shape + shape_down = get_shape(expr.expr) + cumul = list(accumulate([1] + list(reversed(shape_up)), operator.mul)) + one_index = Add.fromiter(i*s for i, s in zip(reversed(indices), cumul)) + dest_indices = [None for _ in shape_down] + c = 1 + for i, e in enumerate(reversed(shape_down)): + if c == 1: + if i == len(shape_down) - 1: + dest_indices[i] = one_index + else: + dest_indices[i] = one_index % e + elif i == len(shape_down) - 1: + dest_indices[i] = one_index // c + else: + dest_indices[i] = one_index // c % e + c *= e + dest_indices.reverse() + return self.do_convert(expr.expr, dest_indices) + return _get_array_element_or_slice(expr, indices) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/from_array_to_matrix.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/from_array_to_matrix.py new file mode 100644 index 0000000000000000000000000000000000000000..debfdd7eb5c4533996b3d72b55d679be3daf3afe --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/from_array_to_matrix.py @@ -0,0 +1,1004 @@ +from __future__ import annotations +import itertools +from collections import defaultdict +from typing import FrozenSet +from functools import singledispatch +from itertools import accumulate + +from sympy import MatMul, Basic, Wild, KroneckerProduct +from sympy.assumptions.ask import (Q, ask) +from sympy.core.mul import Mul +from sympy.core.singleton import S +from sympy.matrices.expressions.diagonal import DiagMatrix +from sympy.matrices.expressions.hadamard import hadamard_product, HadamardPower +from sympy.matrices.expressions.matexpr import MatrixExpr +from sympy.matrices.expressions.special import (Identity, ZeroMatrix, OneMatrix) +from sympy.matrices.expressions.trace import Trace +from sympy.matrices.expressions.transpose import Transpose +from sympy.combinatorics.permutations import _af_invert, Permutation +from sympy.matrices.matrixbase import MatrixBase +from sympy.matrices.expressions.applyfunc import ElementwiseApplyFunction +from sympy.matrices.expressions.matexpr import MatrixElement +from sympy.tensor.array.expressions.array_expressions import PermuteDims, ArrayDiagonal, \ + ArrayTensorProduct, OneArray, get_rank, _get_subrank, ZeroArray, ArrayContraction, \ + ArrayAdd, _CodegenArrayAbstract, get_shape, ArrayElementwiseApplyFunc, _ArrayExpr, _EditArrayContraction, _ArgE, \ + ArrayElement, _array_tensor_product, _array_contraction, _array_diagonal, _array_add, _permute_dims +from sympy.tensor.array.expressions.utils import _get_mapping_from_subranks + + +def _get_candidate_for_matmul_from_contraction(scan_indices: list[int | None], remaining_args: list[_ArgE]) -> tuple[_ArgE | None, bool, int]: + + scan_indices_int: list[int] = [i for i in scan_indices if i is not None] + if len(scan_indices_int) == 0: + return None, False, -1 + + transpose: bool = False + candidate: _ArgE | None = None + candidate_index: int = -1 + for arg_with_ind2 in remaining_args: + if not isinstance(arg_with_ind2.element, MatrixExpr): + continue + for index in scan_indices_int: + if candidate_index != -1 and candidate_index != index: + # A candidate index has already been selected, check + # repetitions only for that index: + continue + if index in arg_with_ind2.indices: + if set(arg_with_ind2.indices) == {index}: + # Index repeated twice in arg_with_ind2 + candidate = None + break + if candidate is None: + candidate = arg_with_ind2 + candidate_index = index + transpose = (index == arg_with_ind2.indices[1]) + else: + # Index repeated more than twice, break + candidate = None + break + return candidate, transpose, candidate_index + + +def _insert_candidate_into_editor(editor: _EditArrayContraction, arg_with_ind: _ArgE, candidate: _ArgE, transpose1: bool, transpose2: bool): + other = candidate.element + other_index: int | None + if transpose2: + other = Transpose(other) + other_index = candidate.indices[0] + else: + other_index = candidate.indices[1] + new_element = (Transpose(arg_with_ind.element) if transpose1 else arg_with_ind.element) * other + editor.args_with_ind.remove(candidate) + new_arge = _ArgE(new_element) + return new_arge, other_index + + +def _support_function_tp1_recognize(contraction_indices, args): + if len(contraction_indices) == 0: + return _a2m_tensor_product(*args) + + ac = _array_contraction(_array_tensor_product(*args), *contraction_indices) + editor = _EditArrayContraction(ac) + editor.track_permutation_start() + + while True: + flag_stop = True + for i, arg_with_ind in enumerate(editor.args_with_ind): + if not isinstance(arg_with_ind.element, MatrixExpr): + continue + + first_index = arg_with_ind.indices[0] + second_index = arg_with_ind.indices[1] + + first_frequency = editor.count_args_with_index(first_index) + second_frequency = editor.count_args_with_index(second_index) + + if first_index is not None and first_frequency == 1 and first_index == second_index: + flag_stop = False + arg_with_ind.element = Trace(arg_with_ind.element)._normalize() + arg_with_ind.indices = [] + break + + scan_indices = [] + if first_frequency == 2: + scan_indices.append(first_index) + if second_frequency == 2: + scan_indices.append(second_index) + + candidate, transpose, found_index = _get_candidate_for_matmul_from_contraction(scan_indices, editor.args_with_ind[i+1:]) + if candidate is not None: + flag_stop = False + editor.track_permutation_merge(arg_with_ind, candidate) + transpose1 = found_index == first_index + new_arge, other_index = _insert_candidate_into_editor(editor, arg_with_ind, candidate, transpose1, transpose) + if found_index == first_index: + new_arge.indices = [second_index, other_index] + else: + new_arge.indices = [first_index, other_index] + set_indices = set(new_arge.indices) + if len(set_indices) == 1 and set_indices != {None}: + # This is a trace: + new_arge.element = Trace(new_arge.element)._normalize() + new_arge.indices = [] + editor.args_with_ind[i] = new_arge + # TODO: is this break necessary? + break + + if flag_stop: + break + + editor.refresh_indices() + return editor.to_array_contraction() + + +def _find_trivial_matrices_rewrite(expr: ArrayTensorProduct): + # If there are matrices of trivial shape in the tensor product (i.e. shape + # (1, 1)), try to check if there is a suitable non-trivial MatMul where the + # expression can be inserted. + + # For example, if "a" has shape (1, 1) and "b" has shape (k, 1), the + # expressions "_array_tensor_product(a, b*b.T)" can be rewritten as + # "b*a*b.T" + + trivial_matrices = [] + pos: int | None = None # must be initialized else causes UnboundLocalError + first: MatrixExpr | None = None # may cause UnboundLocalError if not initialized + second: MatrixExpr | None = None # may cause UnboundLocalError if not initialized + removed: list[int] = [] + counter: int = 0 + args: list[Basic | None] = list(expr.args) + for i, arg in enumerate(expr.args): + if isinstance(arg, MatrixExpr): + if arg.shape == (1, 1): + trivial_matrices.append(arg) + args[i] = None + removed.extend([counter, counter+1]) + elif pos is None and isinstance(arg, MatMul): + margs = arg.args + for j, e in enumerate(margs): + if isinstance(e, MatrixExpr) and e.shape[1] == 1: + pos = i + first = MatMul.fromiter(margs[:j+1]) + second = MatMul.fromiter(margs[j+1:]) + break + counter += get_rank(arg) + if pos is None: + return expr, [] + args[pos] = (first*MatMul.fromiter(i for i in trivial_matrices)*second).doit() + return _array_tensor_product(*[i for i in args if i is not None]), removed + + +def _find_trivial_kronecker_products_broadcast(expr: ArrayTensorProduct): + newargs: list[Basic] = [] + removed = [] + count_dims = 0 + for arg in expr.args: + count_dims += get_rank(arg) + shape = get_shape(arg) + current_range = [count_dims-i for i in range(len(shape), 0, -1)] + if (shape == (1, 1) and len(newargs) > 0 and 1 not in get_shape(newargs[-1]) and + isinstance(newargs[-1], MatrixExpr) and isinstance(arg, MatrixExpr)): + # KroneckerProduct object allows the trick of broadcasting: + newargs[-1] = KroneckerProduct(newargs[-1], arg) + removed.extend(current_range) + elif 1 not in shape and len(newargs) > 0 and get_shape(newargs[-1]) == (1, 1): + # Broadcast: + newargs[-1] = KroneckerProduct(newargs[-1], arg) + prev_range = [i for i in range(min(current_range)) if i not in removed] + removed.extend(prev_range[-2:]) + else: + newargs.append(arg) + return _array_tensor_product(*newargs), removed + + +@singledispatch +def _array2matrix(expr): + return expr + + +@_array2matrix.register(ZeroArray) +def _(expr: ZeroArray): + if get_rank(expr) == 2: + return ZeroMatrix(*expr.shape) + else: + return expr + + +@_array2matrix.register(ArrayTensorProduct) +def _(expr: ArrayTensorProduct): + return _a2m_tensor_product(*[_array2matrix(arg) for arg in expr.args]) + + +@_array2matrix.register(ArrayContraction) +def _(expr: ArrayContraction): + expr = expr.flatten_contraction_of_diagonal() + expr = identify_removable_identity_matrices(expr) + expr = expr.split_multiple_contractions() + expr = identify_hadamard_products(expr) + if not isinstance(expr, ArrayContraction): + return _array2matrix(expr) + subexpr = expr.expr + contraction_indices: tuple[tuple[int]] = expr.contraction_indices + if contraction_indices == ((0,), (1,)) or ( + contraction_indices == ((0,),) and subexpr.shape[1] == 1 + ) or ( + contraction_indices == ((1,),) and subexpr.shape[0] == 1 + ): + shape = subexpr.shape + subexpr = _array2matrix(subexpr) + if isinstance(subexpr, MatrixExpr): + return OneMatrix(1, shape[0])*subexpr*OneMatrix(shape[1], 1) + if isinstance(subexpr, ArrayTensorProduct): + newexpr = _array_contraction(_array2matrix(subexpr), *contraction_indices) + contraction_indices = newexpr.contraction_indices + if any(i > 2 for i in newexpr.subranks): + addends = _array_add(*[_a2m_tensor_product(*j) for j in itertools.product(*[i.args if isinstance(i, + ArrayAdd) else [i] for i in expr.expr.args])]) + newexpr = _array_contraction(addends, *contraction_indices) + if isinstance(newexpr, ArrayAdd): + ret = _array2matrix(newexpr) + return ret + assert isinstance(newexpr, ArrayContraction) + ret = _support_function_tp1_recognize(contraction_indices, list(newexpr.expr.args)) + return ret + elif not isinstance(subexpr, _CodegenArrayAbstract): + ret = _array2matrix(subexpr) + if isinstance(ret, MatrixExpr): + assert expr.contraction_indices == ((0, 1),) + return _a2m_trace(ret) + else: + return _array_contraction(ret, *expr.contraction_indices) + + +@_array2matrix.register(ArrayDiagonal) +def _(expr: ArrayDiagonal): + pexpr = _array_diagonal(_array2matrix(expr.expr), *expr.diagonal_indices) + pexpr = identify_hadamard_products(pexpr) + if isinstance(pexpr, ArrayDiagonal): + pexpr = _array_diag2contr_diagmatrix(pexpr) + if expr == pexpr: + return expr + return _array2matrix(pexpr) + + +@_array2matrix.register(PermuteDims) +def _(expr: PermuteDims): + if expr.permutation.array_form == [1, 0]: + return _a2m_transpose(_array2matrix(expr.expr)) + elif isinstance(expr.expr, ArrayTensorProduct): + ranks = expr.expr.subranks + inv_permutation = expr.permutation**(-1) + newrange = [inv_permutation(i) for i in range(sum(ranks))] + newpos = [] + counter = 0 + for rank in ranks: + newpos.append(newrange[counter:counter+rank]) + counter += rank + newargs = [] + newperm = [] + scalars = [] + for pos, arg in zip(newpos, expr.expr.args): + if len(pos) == 0: + scalars.append(_array2matrix(arg)) + elif pos == sorted(pos): + newargs.append((_array2matrix(arg), pos[0])) + newperm.extend(pos) + elif len(pos) == 2: + newargs.append((_a2m_transpose(_array2matrix(arg)), pos[0])) + newperm.extend(reversed(pos)) + else: + raise NotImplementedError() + newargs = [i[0] for i in newargs] + return _permute_dims(_a2m_tensor_product(*scalars, *newargs), _af_invert(newperm)) + elif isinstance(expr.expr, ArrayContraction): + mat_mul_lines = _array2matrix(expr.expr) + if not isinstance(mat_mul_lines, ArrayTensorProduct): + return _permute_dims(mat_mul_lines, expr.permutation) + # TODO: this assumes that all arguments are matrices, it may not be the case: + permutation = Permutation(2*len(mat_mul_lines.args)-1)*expr.permutation + permuted = [permutation(i) for i in range(2*len(mat_mul_lines.args))] + args_array = [None for i in mat_mul_lines.args] + for i in range(len(mat_mul_lines.args)): + p1 = permuted[2*i] + p2 = permuted[2*i+1] + if p1 // 2 != p2 // 2: + return _permute_dims(mat_mul_lines, permutation) + if p1 > p2: + args_array[i] = _a2m_transpose(mat_mul_lines.args[p1 // 2]) + else: + args_array[i] = mat_mul_lines.args[p1 // 2] + return _a2m_tensor_product(*args_array) + else: + return expr + + +@_array2matrix.register(ArrayAdd) +def _(expr: ArrayAdd): + addends = [_array2matrix(arg) for arg in expr.args] + return _a2m_add(*addends) + + +@_array2matrix.register(ArrayElementwiseApplyFunc) +def _(expr: ArrayElementwiseApplyFunc): + subexpr = _array2matrix(expr.expr) + if isinstance(subexpr, MatrixExpr): + if subexpr.shape != (1, 1): + d = expr.function.bound_symbols[0] + w = Wild("w", exclude=[d]) + p = Wild("p", exclude=[d]) + m = expr.function.expr.match(w*d**p) + if m is not None: + return m[w]*HadamardPower(subexpr, m[p]) + return ElementwiseApplyFunction(expr.function, subexpr) + else: + return ArrayElementwiseApplyFunc(expr.function, subexpr) + + +@_array2matrix.register(ArrayElement) +def _(expr: ArrayElement): + ret = _array2matrix(expr.name) + if isinstance(ret, MatrixExpr): + return MatrixElement(ret, *expr.indices) + return ArrayElement(ret, expr.indices) + + +@singledispatch +def _remove_trivial_dims(expr): + return expr, [] + + +@_remove_trivial_dims.register(ArrayTensorProduct) +def _(expr: ArrayTensorProduct): + # Recognize expressions like [x, y] with shape (k, 1, k, 1) as `x*y.T`. + # The matrix expression has to be equivalent to the tensor product of the + # matrices, with trivial dimensions (i.e. dim=1) dropped. + # That is, add contractions over trivial dimensions: + + removed = [] + newargs = [] + cumul = list(accumulate([0] + [get_rank(arg) for arg in expr.args])) + pending = None + prev_i = None + for i, arg in enumerate(expr.args): + current_range = list(range(cumul[i], cumul[i+1])) + if isinstance(arg, OneArray): + removed.extend(current_range) + continue + if not isinstance(arg, (MatrixExpr, MatrixBase)): + rarg, rem = _remove_trivial_dims(arg) + removed.extend(rem) + newargs.append(rarg) + continue + elif getattr(arg, "is_Identity", False) and arg.shape == (1, 1): + if arg.shape == (1, 1): + # Ignore identity matrices of shape (1, 1) - they are equivalent to scalar 1. + removed.extend(current_range) + continue + elif arg.shape == (1, 1): + arg, _ = _remove_trivial_dims(arg) + # Matrix is equivalent to scalar: + if len(newargs) == 0: + newargs.append(arg) + elif 1 in get_shape(newargs[-1]): + if newargs[-1].shape[1] == 1: + newargs[-1] = newargs[-1]*arg + else: + newargs[-1] = arg*newargs[-1] + removed.extend(current_range) + else: + newargs.append(arg) + elif 1 in arg.shape: + k = [i for i in arg.shape if i != 1][0] + if pending is None: + pending = k + prev_i = i + newargs.append(arg) + elif pending == k: + prev = newargs[-1] + if prev.shape[0] == 1: + d1 = cumul[prev_i] # type: ignore + prev = _a2m_transpose(prev) + else: + d1 = cumul[prev_i] + 1 # type: ignore + if arg.shape[1] == 1: + d2 = cumul[i] + 1 + arg = _a2m_transpose(arg) + else: + d2 = cumul[i] + newargs[-1] = prev*arg + pending = None + removed.extend([d1, d2]) + else: + newargs.append(arg) + pending = k + prev_i = i + else: + newargs.append(arg) + pending = None + newexpr, newremoved = _a2m_tensor_product(*newargs), sorted(removed) + if isinstance(newexpr, ArrayTensorProduct): + newexpr, newremoved2 = _find_trivial_matrices_rewrite(newexpr) + newremoved = _combine_removed(-1, newremoved, newremoved2) + if isinstance(newexpr, ArrayTensorProduct): + newexpr, newremoved2 = _find_trivial_kronecker_products_broadcast(newexpr) + newremoved = _combine_removed(-1, newremoved, newremoved2) + return newexpr, newremoved + + +@_remove_trivial_dims.register(ArrayAdd) +def _(expr: ArrayAdd): + rec = [_remove_trivial_dims(arg) for arg in expr.args] + newargs, removed = zip(*rec) + if len({get_shape(i) for i in newargs}) > 1: + return expr, [] + if len(removed) == 0: + return expr, removed + removed1 = removed[0] + return _a2m_add(*newargs), removed1 + + +@_remove_trivial_dims.register(PermuteDims) +def _(expr: PermuteDims): + subexpr, subremoved = _remove_trivial_dims(expr.expr) + p = expr.permutation.array_form + pinv = _af_invert(expr.permutation.array_form) + shift = list(accumulate([1 if i in subremoved else 0 for i in range(len(p))])) + premoved = [pinv[i] for i in subremoved] + p2 = [e - shift[e] for e in p if e not in subremoved] + # TODO: check if subremoved should be permuted as well... + newexpr = _permute_dims(subexpr, p2) + premoved = sorted(premoved) + if newexpr != expr: + newexpr, removed2 = _remove_trivial_dims(_array2matrix(newexpr)) + premoved = _combine_removed(-1, premoved, removed2) + return newexpr, premoved + + +@_remove_trivial_dims.register(ArrayContraction) +def _(expr: ArrayContraction): + new_expr, removed0 = _array_contraction_to_diagonal_multiple_identity(expr) + if new_expr != expr: + new_expr2, removed1 = _remove_trivial_dims(_array2matrix(new_expr)) + removed = _combine_removed(-1, removed0, removed1) + return new_expr2, removed + rank1 = get_rank(expr) + expr, removed1 = remove_identity_matrices(expr) + if not isinstance(expr, ArrayContraction): + expr2, removed2 = _remove_trivial_dims(expr) + return expr2, _combine_removed(rank1, removed1, removed2) + newexpr, removed2 = _remove_trivial_dims(expr.expr) + shifts = list(accumulate([1 if i in removed2 else 0 for i in range(get_rank(expr.expr))])) + new_contraction_indices = [tuple(j for j in i if j not in removed2) for i in expr.contraction_indices] + # Remove possible empty tuples "()": + new_contraction_indices = [i for i in new_contraction_indices if len(i) > 0] + contraction_indices_flat = [j for i in expr.contraction_indices for j in i] + removed2 = [i for i in removed2 if i not in contraction_indices_flat] + new_contraction_indices = [tuple(j - shifts[j] for j in i) for i in new_contraction_indices] + # Shift removed2: + removed2 = ArrayContraction._push_indices_up(expr.contraction_indices, removed2) + removed = _combine_removed(rank1, removed1, removed2) + return _array_contraction(newexpr, *new_contraction_indices), list(removed) + + +def _remove_diagonalized_identity_matrices(expr: ArrayDiagonal): + assert isinstance(expr, ArrayDiagonal) + editor = _EditArrayContraction(expr) + mapping = {i: {j for j in editor.args_with_ind if i in j.indices} for i in range(-1, -1-editor.number_of_diagonal_indices, -1)} + removed = [] + counter: int = 0 + for i, arg_with_ind in enumerate(editor.args_with_ind): + counter += len(arg_with_ind.indices) + if isinstance(arg_with_ind.element, Identity): + if None in arg_with_ind.indices and any(i is not None and (i < 0) == True for i in arg_with_ind.indices): + diag_ind = [j for j in arg_with_ind.indices if j is not None][0] + other = [j for j in mapping[diag_ind] if j != arg_with_ind][0] + if not isinstance(other.element, MatrixExpr): + continue + if 1 not in other.element.shape: + continue + if None not in other.indices: + continue + editor.args_with_ind[i].element = None + none_index = other.indices.index(None) + other.element = DiagMatrix(other.element) + other_range = editor.get_absolute_range(other) + removed.extend([other_range[0] + none_index]) + editor.args_with_ind = [i for i in editor.args_with_ind if i.element is not None] + removed = ArrayDiagonal._push_indices_up(expr.diagonal_indices, removed, get_rank(expr.expr)) + return editor.to_array_contraction(), removed + + +@_remove_trivial_dims.register(ArrayDiagonal) +def _(expr: ArrayDiagonal): + newexpr, removed = _remove_trivial_dims(expr.expr) + shifts = list(accumulate([0] + [1 if i in removed else 0 for i in range(get_rank(expr.expr))])) + new_diag_indices_map = {i: tuple(j for j in i if j not in removed) for i in expr.diagonal_indices} + for old_diag_tuple, new_diag_tuple in new_diag_indices_map.items(): + if len(new_diag_tuple) == 1: + removed = [i for i in removed if i not in old_diag_tuple] + new_diag_indices = [tuple(j - shifts[j] for j in i) for i in new_diag_indices_map.values()] + rank = get_rank(expr.expr) + removed = ArrayDiagonal._push_indices_up(expr.diagonal_indices, removed, rank) + removed = sorted(set(removed)) + # If there are single axes to diagonalize remaining, it means that their + # corresponding dimension has been removed, they no longer need diagonalization: + new_diag_indices = [i for i in new_diag_indices if len(i) > 0] + if len(new_diag_indices) > 0: + newexpr2 = _array_diagonal(newexpr, *new_diag_indices, allow_trivial_diags=True) + else: + newexpr2 = newexpr + if isinstance(newexpr2, ArrayDiagonal): + newexpr3, removed2 = _remove_diagonalized_identity_matrices(newexpr2) + removed = _combine_removed(-1, removed, removed2) + return newexpr3, removed + else: + return newexpr2, removed + + +@_remove_trivial_dims.register(ElementwiseApplyFunction) +def _(expr: ElementwiseApplyFunction): + subexpr, removed = _remove_trivial_dims(expr.expr) + if subexpr.shape == (1, 1): + # TODO: move this to ElementwiseApplyFunction + return expr.function(subexpr), removed + [0, 1] + return ElementwiseApplyFunction(expr.function, subexpr), [] + + +@_remove_trivial_dims.register(ArrayElementwiseApplyFunc) +def _(expr: ArrayElementwiseApplyFunc): + subexpr, removed = _remove_trivial_dims(expr.expr) + return ArrayElementwiseApplyFunc(expr.function, subexpr), removed + + +def convert_array_to_matrix(expr): + r""" + Recognize matrix expressions in codegen objects. + + If more than one matrix multiplication line have been detected, return a + list with the matrix expressions. + + Examples + ======== + + >>> from sympy.tensor.array.expressions.from_indexed_to_array import convert_indexed_to_array + >>> from sympy.tensor.array import tensorcontraction, tensorproduct + >>> from sympy import MatrixSymbol, Sum + >>> from sympy.abc import i, j, k, l, N + >>> from sympy.tensor.array.expressions.from_matrix_to_array import convert_matrix_to_array + >>> from sympy.tensor.array.expressions.from_array_to_matrix import convert_array_to_matrix + >>> A = MatrixSymbol("A", N, N) + >>> B = MatrixSymbol("B", N, N) + >>> C = MatrixSymbol("C", N, N) + >>> D = MatrixSymbol("D", N, N) + + >>> expr = Sum(A[i, j]*B[j, k], (j, 0, N-1)) + >>> cg = convert_indexed_to_array(expr) + >>> convert_array_to_matrix(cg) + A*B + >>> cg = convert_indexed_to_array(expr, first_indices=[k]) + >>> convert_array_to_matrix(cg) + B.T*A.T + + Transposition is detected: + + >>> expr = Sum(A[j, i]*B[j, k], (j, 0, N-1)) + >>> cg = convert_indexed_to_array(expr) + >>> convert_array_to_matrix(cg) + A.T*B + >>> cg = convert_indexed_to_array(expr, first_indices=[k]) + >>> convert_array_to_matrix(cg) + B.T*A + + Detect the trace: + + >>> expr = Sum(A[i, i], (i, 0, N-1)) + >>> cg = convert_indexed_to_array(expr) + >>> convert_array_to_matrix(cg) + Trace(A) + + Recognize some more complex traces: + + >>> expr = Sum(A[i, j]*B[j, i], (i, 0, N-1), (j, 0, N-1)) + >>> cg = convert_indexed_to_array(expr) + >>> convert_array_to_matrix(cg) + Trace(A*B) + + More complicated expressions: + + >>> expr = Sum(A[i, j]*B[k, j]*A[l, k], (j, 0, N-1), (k, 0, N-1)) + >>> cg = convert_indexed_to_array(expr) + >>> convert_array_to_matrix(cg) + A*B.T*A.T + + Expressions constructed from matrix expressions do not contain literal + indices, the positions of free indices are returned instead: + + >>> expr = A*B + >>> cg = convert_matrix_to_array(expr) + >>> convert_array_to_matrix(cg) + A*B + + If more than one line of matrix multiplications is detected, return + separate matrix multiplication factors embedded in a tensor product object: + + >>> cg = tensorcontraction(tensorproduct(A, B, C, D), (1, 2), (5, 6)) + >>> convert_array_to_matrix(cg) + ArrayTensorProduct(A*B, C*D) + + The two lines have free indices at axes 0, 3 and 4, 7, respectively. + """ + rec = _array2matrix(expr) + rec, removed = _remove_trivial_dims(rec) + return rec + + +def _array_diag2contr_diagmatrix(expr: ArrayDiagonal): + if isinstance(expr.expr, ArrayTensorProduct): + args = list(expr.expr.args) + diag_indices = list(expr.diagonal_indices) + mapping = _get_mapping_from_subranks([_get_subrank(arg) for arg in args]) + tuple_links = [[mapping[j] for j in i] for i in diag_indices] + contr_indices = [] + total_rank = get_rank(expr) + replaced = [False for arg in args] + for i, (abs_pos, rel_pos) in enumerate(zip(diag_indices, tuple_links)): + if len(abs_pos) != 2: + continue + (pos1_outer, pos1_inner), (pos2_outer, pos2_inner) = rel_pos + arg1 = args[pos1_outer] + arg2 = args[pos2_outer] + if get_rank(arg1) != 2 or get_rank(arg2) != 2: + if replaced[pos1_outer]: + diag_indices[i] = None + if replaced[pos2_outer]: + diag_indices[i] = None + continue + pos1_in2 = 1 - pos1_inner + pos2_in2 = 1 - pos2_inner + if arg1.shape[pos1_in2] == 1: + if arg1.shape[pos1_inner] != 1: + darg1 = DiagMatrix(arg1) + else: + darg1 = arg1 + args.append(darg1) + contr_indices.append(((pos2_outer, pos2_inner), (len(args)-1, pos1_inner))) + total_rank += 1 + diag_indices[i] = None + args[pos1_outer] = OneArray(arg1.shape[pos1_in2]) + replaced[pos1_outer] = True + elif arg2.shape[pos2_in2] == 1: + if arg2.shape[pos2_inner] != 1: + darg2 = DiagMatrix(arg2) + else: + darg2 = arg2 + args.append(darg2) + contr_indices.append(((pos1_outer, pos1_inner), (len(args)-1, pos2_inner))) + total_rank += 1 + diag_indices[i] = None + args[pos2_outer] = OneArray(arg2.shape[pos2_in2]) + replaced[pos2_outer] = True + diag_indices_new = [i for i in diag_indices if i is not None] + cumul = list(accumulate([0] + [get_rank(arg) for arg in args])) + contr_indices2 = [tuple(cumul[a] + b for a, b in i) for i in contr_indices] + tc = _array_contraction( + _array_tensor_product(*args), *contr_indices2 + ) + td = _array_diagonal(tc, *diag_indices_new) + return td + return expr + + +def _a2m_mul(*args): + if not any(isinstance(i, _CodegenArrayAbstract) for i in args): + from sympy.matrices.expressions.matmul import MatMul + return MatMul(*args).doit() + else: + return _array_contraction( + _array_tensor_product(*args), + *[(2*i-1, 2*i) for i in range(1, len(args))] + ) + + +def _a2m_tensor_product(*args): + scalars = [] + arrays = [] + for arg in args: + if isinstance(arg, (MatrixExpr, _ArrayExpr, _CodegenArrayAbstract)): + arrays.append(arg) + else: + scalars.append(arg) + scalar = Mul.fromiter(scalars) + if len(arrays) == 0: + return scalar + if scalar != 1: + if isinstance(arrays[0], _CodegenArrayAbstract): + arrays = [scalar] + arrays + else: + arrays[0] *= scalar + return _array_tensor_product(*arrays) + + +def _a2m_add(*args): + if not any(isinstance(i, _CodegenArrayAbstract) for i in args): + from sympy.matrices.expressions.matadd import MatAdd + return MatAdd(*args).doit() + else: + return _array_add(*args) + + +def _a2m_trace(arg): + if isinstance(arg, _CodegenArrayAbstract): + return _array_contraction(arg, (0, 1)) + else: + from sympy.matrices.expressions.trace import Trace + return Trace(arg) + + +def _a2m_transpose(arg): + if isinstance(arg, _CodegenArrayAbstract): + return _permute_dims(arg, [1, 0]) + else: + from sympy.matrices.expressions.transpose import Transpose + return Transpose(arg).doit() + + +def identify_hadamard_products(expr: ArrayContraction | ArrayDiagonal): + + editor: _EditArrayContraction = _EditArrayContraction(expr) + + map_contr_to_args: dict[FrozenSet, list[_ArgE]] = defaultdict(list) + map_ind_to_inds: dict[int | None, int] = defaultdict(int) + for arg_with_ind in editor.args_with_ind: + for ind in arg_with_ind.indices: + map_ind_to_inds[ind] += 1 + if None in arg_with_ind.indices: + continue + map_contr_to_args[frozenset(arg_with_ind.indices)].append(arg_with_ind) + + k: FrozenSet[int] + v: list[_ArgE] + for k, v in map_contr_to_args.items(): + make_trace: bool = False + if len(k) == 1 and next(iter(k)) >= 0 and sum(next(iter(k)) in i for i in map_contr_to_args) == 1: + # This is a trace: the arguments are fully contracted with only one + # index, and the index isn't used anywhere else: + make_trace = True + first_element = S.One + elif len(k) != 2: + # Hadamard product only defined for matrices: + continue + if len(v) == 1: + # Hadamard product with a single argument makes no sense: + continue + for ind in k: + if map_ind_to_inds[ind] <= 2: + # There is no other contraction, skip: + continue + + def check_transpose(x): + x = [i if i >= 0 else -1-i for i in x] + return x == sorted(x) + + # Check if expression is a trace: + if all(map_ind_to_inds[j] == len(v) and j >= 0 for j in k) and all(j >= 0 for j in k): + # This is a trace + make_trace = True + first_element = v[0].element + if not check_transpose(v[0].indices): + first_element = first_element.T # type: ignore + hadamard_factors = v[1:] + else: + hadamard_factors = v + + # This is a Hadamard product: + + hp = hadamard_product(*[i.element if check_transpose(i.indices) else Transpose(i.element) for i in hadamard_factors]) + hp_indices = v[0].indices + if not check_transpose(hadamard_factors[0].indices): + hp_indices = list(reversed(hp_indices)) + if make_trace: + hp = Trace(first_element*hp.T)._normalize() + hp_indices = [] + editor.insert_after(v[0], _ArgE(hp, hp_indices)) + for i in v: + editor.args_with_ind.remove(i) + + return editor.to_array_contraction() + + +def identify_removable_identity_matrices(expr): + editor = _EditArrayContraction(expr) + + flag = True + while flag: + flag = False + for arg_with_ind in editor.args_with_ind: + if isinstance(arg_with_ind.element, Identity): + k = arg_with_ind.element.shape[0] + # Candidate for removal: + if arg_with_ind.indices == [None, None]: + # Free identity matrix, will be cleared by _remove_trivial_dims: + continue + elif None in arg_with_ind.indices: + ind = [j for j in arg_with_ind.indices if j is not None][0] + counted = editor.count_args_with_index(ind) + if counted == 1: + # Identity matrix contracted only on one index with itself, + # transform to a OneArray(k) element: + editor.insert_after(arg_with_ind, OneArray(k)) + editor.args_with_ind.remove(arg_with_ind) + flag = True + break + elif counted > 2: + # Case counted = 2 is a matrix multiplication by identity matrix, skip it. + # Case counted > 2 is a multiple contraction, + # this is a case where the contraction becomes a diagonalization if the + # identity matrix is dropped. + continue + elif arg_with_ind.indices[0] == arg_with_ind.indices[1]: + ind = arg_with_ind.indices[0] + counted = editor.count_args_with_index(ind) + if counted > 1: + editor.args_with_ind.remove(arg_with_ind) + flag = True + break + else: + # This is a trace, skip it as it will be recognized somewhere else: + pass + elif ask(Q.diagonal(arg_with_ind.element)): + if arg_with_ind.indices == [None, None]: + continue + elif None in arg_with_ind.indices: + pass + elif arg_with_ind.indices[0] == arg_with_ind.indices[1]: + ind = arg_with_ind.indices[0] + counted = editor.count_args_with_index(ind) + if counted == 3: + # A_ai B_bi D_ii ==> A_ai D_ij B_bj + ind_new = editor.get_new_contraction_index() + other_args = [j for j in editor.args_with_ind if j != arg_with_ind] + other_args[1].indices = [ind_new if j == ind else j for j in other_args[1].indices] + arg_with_ind.indices = [ind, ind_new] + flag = True + break + + return editor.to_array_contraction() + + +def remove_identity_matrices(expr: ArrayContraction): + editor = _EditArrayContraction(expr) + removed: list[int] = [] + + permutation_map = {} + + free_indices = list(accumulate([0] + [sum(i is None for i in arg.indices) for arg in editor.args_with_ind])) + free_map = dict(zip(editor.args_with_ind, free_indices[:-1])) + + update_pairs = {} + + for ind in range(editor.number_of_contraction_indices): + args = editor.get_args_with_index(ind) + identity_matrices = [i for i in args if isinstance(i.element, Identity)] + number_identity_matrices = len(identity_matrices) + # If the contraction involves a non-identity matrix and multiple identity matrices: + if number_identity_matrices != len(args) - 1 or number_identity_matrices == 0: + continue + # Get the non-identity element: + non_identity = [i for i in args if not isinstance(i.element, Identity)][0] + # Check that all identity matrices have at least one free index + # (otherwise they would be contractions to some other elements) + if any(None not in i.indices for i in identity_matrices): + continue + # Mark the identity matrices for removal: + for i in identity_matrices: + i.element = None + removed.extend(range(free_map[i], free_map[i] + len([j for j in i.indices if j is None]))) + last_removed = removed.pop(-1) + update_pairs[last_removed, ind] = non_identity.indices[:] + # Remove the indices from the non-identity matrix, as the contraction + # no longer exists: + non_identity.indices = [None if i == ind else i for i in non_identity.indices] + + removed.sort() + + shifts = list(accumulate([1 if i in removed else 0 for i in range(get_rank(expr))])) + for (last_removed, ind), non_identity_indices in update_pairs.items(): + pos = [free_map[non_identity] + i for i, e in enumerate(non_identity_indices) if e == ind] + assert len(pos) == 1 + for j in pos: + permutation_map[j] = last_removed + + editor.args_with_ind = [i for i in editor.args_with_ind if i.element is not None] + ret_expr = editor.to_array_contraction() + permutation = [] + counter = 0 + counter2 = 0 + for j in range(get_rank(expr)): + if j in removed: + continue + if counter2 in permutation_map: + target = permutation_map[counter2] + permutation.append(target - shifts[target]) + counter2 += 1 + else: + while counter in permutation_map.values(): + counter += 1 + permutation.append(counter) + counter += 1 + counter2 += 1 + ret_expr2 = _permute_dims(ret_expr, _af_invert(permutation)) + return ret_expr2, removed + + +def _combine_removed(dim: int, removed1: list[int], removed2: list[int]) -> list[int]: + # Concatenate two axis removal operations as performed by + # _remove_trivial_dims, + removed1 = sorted(removed1) + removed2 = sorted(removed2) + i = 0 + j = 0 + removed = [] + while True: + if j >= len(removed2): + while i < len(removed1): + removed.append(removed1[i]) + i += 1 + break + elif i < len(removed1) and removed1[i] <= i + removed2[j]: + removed.append(removed1[i]) + i += 1 + else: + removed.append(i + removed2[j]) + j += 1 + return removed + + +def _array_contraction_to_diagonal_multiple_identity(expr: ArrayContraction): + editor = _EditArrayContraction(expr) + editor.track_permutation_start() + removed: list[int] = [] + diag_index_counter: int = 0 + for i in range(editor.number_of_contraction_indices): + identities = [] + args = [] + for j, arg in enumerate(editor.args_with_ind): + if i not in arg.indices: + continue + if isinstance(arg.element, Identity): + identities.append(arg) + else: + args.append(arg) + if len(identities) == 0: + continue + if len(args) + len(identities) < 3: + continue + new_diag_ind = -1 - diag_index_counter + diag_index_counter += 1 + # Variable "flag" to control whether to skip this contraction set: + flag: bool = True + for i1, id1 in enumerate(identities): + if None not in id1.indices: + flag = True + break + free_pos = list(range(*editor.get_absolute_free_range(id1)))[0] + editor._track_permutation[-1].append(free_pos) # type: ignore + id1.element = None + flag = False + break + if flag: + continue + for arg in identities[:i1] + identities[i1+1:]: + arg.element = None + removed.extend(range(*editor.get_absolute_free_range(arg))) + for arg in args: + arg.indices = [new_diag_ind if j == i else j for j in arg.indices] + for j, e in enumerate(editor.args_with_ind): + if e.element is None: + editor._track_permutation[j] = None # type: ignore + editor._track_permutation = [i for i in editor._track_permutation if i is not None] # type: ignore + # Renumber permutation array form in order to deal with deleted positions: + remap = {e: i for i, e in enumerate(sorted({k for j in editor._track_permutation for k in j}))} + editor._track_permutation = [[remap[j] for j in i] for i in editor._track_permutation] + editor.args_with_ind = [i for i in editor.args_with_ind if i.element is not None] + new_expr = editor.to_array_contraction() + return new_expr, removed diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/from_indexed_to_array.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/from_indexed_to_array.py new file mode 100644 index 0000000000000000000000000000000000000000..c219a205c4305bd7070e5117978146224521c58c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/from_indexed_to_array.py @@ -0,0 +1,257 @@ +from collections import defaultdict + +from sympy import Function +from sympy.combinatorics.permutations import _af_invert +from sympy.concrete.summations import Sum +from sympy.core.add import Add +from sympy.core.mul import Mul +from sympy.core.numbers import Integer +from sympy.core.power import Pow +from sympy.core.sorting import default_sort_key +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.tensor.array.expressions import ArrayElementwiseApplyFunc +from sympy.tensor.indexed import (Indexed, IndexedBase) +from sympy.combinatorics import Permutation +from sympy.matrices.expressions.matexpr import MatrixElement +from sympy.tensor.array.expressions.array_expressions import ArrayDiagonal, \ + get_shape, ArrayElement, _array_tensor_product, _array_diagonal, _array_contraction, _array_add, \ + _permute_dims, OneArray, ArrayAdd +from sympy.tensor.array.expressions.utils import _get_argindex, _get_diagonal_indices + + +def convert_indexed_to_array(expr, first_indices=None): + r""" + Parse indexed expression into a form useful for code generation. + + Examples + ======== + + >>> from sympy.tensor.array.expressions.from_indexed_to_array import convert_indexed_to_array + >>> from sympy import MatrixSymbol, Sum, symbols + + >>> i, j, k, d = symbols("i j k d") + >>> M = MatrixSymbol("M", d, d) + >>> N = MatrixSymbol("N", d, d) + + Recognize the trace in summation form: + + >>> expr = Sum(M[i, i], (i, 0, d-1)) + >>> convert_indexed_to_array(expr) + ArrayContraction(M, (0, 1)) + + Recognize the extraction of the diagonal by using the same index `i` on + both axes of the matrix: + + >>> expr = M[i, i] + >>> convert_indexed_to_array(expr) + ArrayDiagonal(M, (0, 1)) + + This function can help perform the transformation expressed in two + different mathematical notations as: + + `\sum_{j=0}^{N-1} A_{i,j} B_{j,k} \Longrightarrow \mathbf{A}\cdot \mathbf{B}` + + Recognize the matrix multiplication in summation form: + + >>> expr = Sum(M[i, j]*N[j, k], (j, 0, d-1)) + >>> convert_indexed_to_array(expr) + ArrayContraction(ArrayTensorProduct(M, N), (1, 2)) + + Specify that ``k`` has to be the starting index: + + >>> convert_indexed_to_array(expr, first_indices=[k]) + ArrayContraction(ArrayTensorProduct(N, M), (0, 3)) + """ + + result, indices = _convert_indexed_to_array(expr) + + if any(isinstance(i, (int, Integer)) for i in indices): + result = ArrayElement(result, indices) + indices = [] + + if not first_indices: + return result + + def _check_is_in(elem, indices): + if elem in indices: + return True + if any(elem in i for i in indices if isinstance(i, frozenset)): + return True + return False + + repl = {j: i for i in indices if isinstance(i, frozenset) for j in i} + first_indices = [repl.get(i, i) for i in first_indices] + for i in first_indices: + if not _check_is_in(i, indices): + first_indices.remove(i) + first_indices.extend([i for i in indices if not _check_is_in(i, first_indices)]) + + def _get_pos(elem, indices): + if elem in indices: + return indices.index(elem) + for i, e in enumerate(indices): + if not isinstance(e, frozenset): + continue + if elem in e: + return i + raise ValueError("not found") + + permutation = _af_invert([_get_pos(i, first_indices) for i in indices]) + if isinstance(result, ArrayAdd): + return _array_add(*[_permute_dims(arg, permutation) for arg in result.args]) + else: + return _permute_dims(result, permutation) + + +def _convert_indexed_to_array(expr): + if isinstance(expr, Sum): + function = expr.function + summation_indices = expr.variables + subexpr, subindices = _convert_indexed_to_array(function) + subindicessets = {j: i for i in subindices if isinstance(i, frozenset) for j in i} + summation_indices = sorted({subindicessets.get(i, i) for i in summation_indices}, key=default_sort_key) + # TODO: check that Kronecker delta is only contracted to one other element: + kronecker_indices = set() + if isinstance(function, Mul): + for arg in function.args: + if not isinstance(arg, KroneckerDelta): + continue + arg_indices = sorted(set(arg.indices), key=default_sort_key) + if len(arg_indices) == 2: + kronecker_indices.update(arg_indices) + kronecker_indices = sorted(kronecker_indices, key=default_sort_key) + # Check dimensional consistency: + shape = get_shape(subexpr) + if shape: + for ind, istart, iend in expr.limits: + i = _get_argindex(subindices, ind) + if istart != 0 or iend+1 != shape[i]: + raise ValueError("summation index and array dimension mismatch: %s" % ind) + contraction_indices = [] + subindices = list(subindices) + if isinstance(subexpr, ArrayDiagonal): + diagonal_indices = list(subexpr.diagonal_indices) + dindices = subindices[-len(diagonal_indices):] + subindices = subindices[:-len(diagonal_indices)] + for index in summation_indices: + if index in dindices: + position = dindices.index(index) + contraction_indices.append(diagonal_indices[position]) + diagonal_indices[position] = None + diagonal_indices = [i for i in diagonal_indices if i is not None] + for i, ind in enumerate(subindices): + if ind in summation_indices: + pass + if diagonal_indices: + subexpr = _array_diagonal(subexpr.expr, *diagonal_indices) + else: + subexpr = subexpr.expr + + axes_contraction = defaultdict(list) + for i, ind in enumerate(subindices): + include = all(j not in kronecker_indices for j in ind) if isinstance(ind, frozenset) else ind not in kronecker_indices + if ind in summation_indices and include: + axes_contraction[ind].append(i) + subindices[i] = None + for k, v in axes_contraction.items(): + if any(i in kronecker_indices for i in k) if isinstance(k, frozenset) else k in kronecker_indices: + continue + contraction_indices.append(tuple(v)) + free_indices = [i for i in subindices if i is not None] + indices_ret = list(free_indices) + indices_ret.sort(key=lambda x: free_indices.index(x)) + return _array_contraction( + subexpr, + *contraction_indices, + free_indices=free_indices + ), tuple(indices_ret) + if isinstance(expr, Mul): + args, indices = zip(*[_convert_indexed_to_array(arg) for arg in expr.args]) + # Check if there are KroneckerDelta objects: + kronecker_delta_repl = {} + for arg in args: + if not isinstance(arg, KroneckerDelta): + continue + # Diagonalize two indices: + i, j = arg.indices + kindices = set(arg.indices) + if i in kronecker_delta_repl: + kindices.update(kronecker_delta_repl[i]) + if j in kronecker_delta_repl: + kindices.update(kronecker_delta_repl[j]) + kindices = frozenset(kindices) + for index in kindices: + kronecker_delta_repl[index] = kindices + # Remove KroneckerDelta objects, their relations should be handled by + # ArrayDiagonal: + newargs = [] + newindices = [] + for arg, loc_indices in zip(args, indices): + if isinstance(arg, KroneckerDelta): + continue + newargs.append(arg) + newindices.append(loc_indices) + flattened_indices = [kronecker_delta_repl.get(j, j) for i in newindices for j in i] + diagonal_indices, ret_indices = _get_diagonal_indices(flattened_indices) + tp = _array_tensor_product(*newargs) + if diagonal_indices: + return _array_diagonal(tp, *diagonal_indices), ret_indices + else: + return tp, ret_indices + if isinstance(expr, MatrixElement): + indices = expr.args[1:] + diagonal_indices, ret_indices = _get_diagonal_indices(indices) + if diagonal_indices: + return _array_diagonal(expr.args[0], *diagonal_indices), ret_indices + else: + return expr.args[0], ret_indices + if isinstance(expr, ArrayElement): + indices = expr.indices + diagonal_indices, ret_indices = _get_diagonal_indices(indices) + if diagonal_indices: + return _array_diagonal(expr.name, *diagonal_indices), ret_indices + else: + return expr.name, ret_indices + if isinstance(expr, Indexed): + indices = expr.indices + diagonal_indices, ret_indices = _get_diagonal_indices(indices) + if diagonal_indices: + return _array_diagonal(expr.base, *diagonal_indices), ret_indices + else: + return expr.args[0], ret_indices + if isinstance(expr, IndexedBase): + raise NotImplementedError + if isinstance(expr, KroneckerDelta): + return expr, expr.indices + if isinstance(expr, Add): + args, indices = zip(*[_convert_indexed_to_array(arg) for arg in expr.args]) + args = list(args) + # Check if all indices are compatible. Otherwise expand the dimensions: + index0 = [] + shape0 = [] + for arg, arg_indices in zip(args, indices): + arg_indices_set = set(arg_indices) + arg_indices_missing = arg_indices_set.difference(index0) + index0.extend([i for i in arg_indices if i in arg_indices_missing]) + arg_shape = get_shape(arg) + shape0.extend([arg_shape[i] for i, e in enumerate(arg_indices) if e in arg_indices_missing]) + for i, (arg, arg_indices) in enumerate(zip(args, indices)): + if len(arg_indices) < len(index0): + missing_indices_pos = [i for i, e in enumerate(index0) if e not in arg_indices] + missing_shape = [shape0[i] for i in missing_indices_pos] + arg_indices = tuple(index0[j] for j in missing_indices_pos) + arg_indices + args[i] = _array_tensor_product(OneArray(*missing_shape), args[i]) + permutation = Permutation([arg_indices.index(j) for j in index0]) + # Perform index permutations: + args[i] = _permute_dims(args[i], permutation) + return _array_add(*args), tuple(index0) + if isinstance(expr, Pow): + subexpr, subindices = _convert_indexed_to_array(expr.base) + if isinstance(expr.exp, (int, Integer)): + diags = zip(*[(2*i, 2*i + 1) for i in range(expr.exp)]) + arr = _array_diagonal(_array_tensor_product(*[subexpr for i in range(expr.exp)]), *diags) + return arr, subindices + if isinstance(expr, Function): + subexpr, subindices = _convert_indexed_to_array(expr.args[0]) + return ArrayElementwiseApplyFunc(type(expr), subexpr), subindices + return expr, () diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/from_matrix_to_array.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/from_matrix_to_array.py new file mode 100644 index 0000000000000000000000000000000000000000..8f66961727f6338318d65876a7768802773e4f2d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/from_matrix_to_array.py @@ -0,0 +1,87 @@ +from sympy import KroneckerProduct +from sympy.core.basic import Basic +from sympy.core.function import Lambda +from sympy.core.mul import Mul +from sympy.core.numbers import Integer +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, symbols) +from sympy.matrices.expressions.hadamard import (HadamardPower, HadamardProduct) +from sympy.matrices.expressions.matadd import MatAdd +from sympy.matrices.expressions.matmul import MatMul +from sympy.matrices.expressions.matpow import MatPow +from sympy.matrices.expressions.trace import Trace +from sympy.matrices.expressions.transpose import Transpose +from sympy.matrices.expressions.matexpr import MatrixExpr +from sympy.tensor.array.expressions.array_expressions import \ + ArrayElementwiseApplyFunc, _array_tensor_product, _array_contraction, \ + _array_diagonal, _array_add, _permute_dims, Reshape + + +def convert_matrix_to_array(expr: Basic) -> Basic: + if isinstance(expr, MatMul): + args_nonmat = [] + args = [] + for arg in expr.args: + if isinstance(arg, MatrixExpr): + args.append(arg) + else: + args_nonmat.append(convert_matrix_to_array(arg)) + contractions = [(2*i+1, 2*i+2) for i in range(len(args)-1)] + scalar = _array_tensor_product(*args_nonmat) if args_nonmat else S.One + if scalar == 1: + tprod = _array_tensor_product( + *[convert_matrix_to_array(arg) for arg in args]) + else: + tprod = _array_tensor_product( + scalar, + *[convert_matrix_to_array(arg) for arg in args]) + return _array_contraction( + tprod, + *contractions + ) + elif isinstance(expr, MatAdd): + return _array_add( + *[convert_matrix_to_array(arg) for arg in expr.args] + ) + elif isinstance(expr, Transpose): + return _permute_dims( + convert_matrix_to_array(expr.args[0]), [1, 0] + ) + elif isinstance(expr, Trace): + inner_expr: MatrixExpr = convert_matrix_to_array(expr.arg) # type: ignore + return _array_contraction(inner_expr, (0, len(inner_expr.shape) - 1)) + elif isinstance(expr, Mul): + return _array_tensor_product(*[convert_matrix_to_array(i) for i in expr.args]) + elif isinstance(expr, Pow): + base = convert_matrix_to_array(expr.base) + if (expr.exp > 0) == True: + return _array_tensor_product(*[base for i in range(expr.exp)]) + else: + return expr + elif isinstance(expr, MatPow): + base = convert_matrix_to_array(expr.base) + if expr.exp.is_Integer != True: + b = symbols("b", cls=Dummy) + return ArrayElementwiseApplyFunc(Lambda(b, b**expr.exp), convert_matrix_to_array(base)) + elif (expr.exp > 0) == True: + return convert_matrix_to_array(MatMul.fromiter(base for i in range(expr.exp))) + else: + return expr + elif isinstance(expr, HadamardProduct): + tp = _array_tensor_product(*[convert_matrix_to_array(arg) for arg in expr.args]) + diag = [[2*i for i in range(len(expr.args))], [2*i+1 for i in range(len(expr.args))]] + return _array_diagonal(tp, *diag) + elif isinstance(expr, HadamardPower): + base, exp = expr.args + if isinstance(exp, Integer) and exp > 0: + return convert_matrix_to_array(HadamardProduct.fromiter(base for i in range(exp))) + else: + d = Dummy("d") + return ArrayElementwiseApplyFunc(Lambda(d, d**exp), base) + elif isinstance(expr, KroneckerProduct): + kp_args = [convert_matrix_to_array(arg) for arg in expr.args] + permutation = [2*i for i in range(len(kp_args))] + [2*i + 1 for i in range(len(kp_args))] + return Reshape(_permute_dims(_array_tensor_product(*kp_args), permutation), expr.shape) + else: + return expr diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_array_expressions.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_array_expressions.py new file mode 100644 index 0000000000000000000000000000000000000000..63fb79ab7ced7bff5ecb55b1764f43e29f98609d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_array_expressions.py @@ -0,0 +1,808 @@ +import random + +from sympy import tensordiagonal, eye, KroneckerDelta, Array +from sympy.core.symbol import symbols +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.matrices.expressions.diagonal import DiagMatrix +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.matrices.expressions.special import ZeroMatrix +from sympy.tensor.array.arrayop import (permutedims, tensorcontraction, tensorproduct) +from sympy.tensor.array.dense_ndim_array import ImmutableDenseNDimArray +from sympy.combinatorics import Permutation +from sympy.tensor.array.expressions.array_expressions import ZeroArray, OneArray, ArraySymbol, ArrayElement, \ + PermuteDims, ArrayContraction, ArrayTensorProduct, ArrayDiagonal, \ + ArrayAdd, nest_permutation, ArrayElementwiseApplyFunc, _EditArrayContraction, _ArgE, _array_tensor_product, \ + _array_contraction, _array_diagonal, _array_add, _permute_dims, Reshape +from sympy.testing.pytest import raises + +i, j, k, l, m, n = symbols("i j k l m n") + + +M = ArraySymbol("M", (k, k)) +N = ArraySymbol("N", (k, k)) +P = ArraySymbol("P", (k, k)) +Q = ArraySymbol("Q", (k, k)) + +A = ArraySymbol("A", (k, k)) +B = ArraySymbol("B", (k, k)) +C = ArraySymbol("C", (k, k)) +D = ArraySymbol("D", (k, k)) + +X = ArraySymbol("X", (k, k)) +Y = ArraySymbol("Y", (k, k)) + +a = ArraySymbol("a", (k, 1)) +b = ArraySymbol("b", (k, 1)) +c = ArraySymbol("c", (k, 1)) +d = ArraySymbol("d", (k, 1)) + + +def test_array_symbol_and_element(): + A = ArraySymbol("A", (2,)) + A0 = ArrayElement(A, (0,)) + A1 = ArrayElement(A, (1,)) + assert A[0] == A0 + assert A[1] != A0 + assert A.as_explicit() == ImmutableDenseNDimArray([A0, A1]) + + A2 = tensorproduct(A, A) + assert A2.shape == (2, 2) + # TODO: not yet supported: + # assert A2.as_explicit() == Array([[A[0]*A[0], A[1]*A[0]], [A[0]*A[1], A[1]*A[1]]]) + A3 = tensorcontraction(A2, (0, 1)) + assert A3.shape == () + # TODO: not yet supported: + # assert A3.as_explicit() == Array([]) + + A = ArraySymbol("A", (2, 3, 4)) + Ae = A.as_explicit() + assert Ae == ImmutableDenseNDimArray( + [[[ArrayElement(A, (i, j, k)) for k in range(4)] for j in range(3)] for i in range(2)]) + + p = _permute_dims(A, Permutation(0, 2, 1)) + assert isinstance(p, PermuteDims) + + A = ArraySymbol("A", (2,)) + raises(IndexError, lambda: A[()]) + raises(IndexError, lambda: A[0, 1]) + raises(ValueError, lambda: A[-1]) + raises(ValueError, lambda: A[2]) + + O = OneArray(3, 4) + Z = ZeroArray(m, n) + + raises(IndexError, lambda: O[()]) + raises(IndexError, lambda: O[1, 2, 3]) + raises(ValueError, lambda: O[3, 0]) + raises(ValueError, lambda: O[0, 4]) + + assert O[1, 2] == 1 + assert Z[1, 2] == 0 + + +def test_zero_array(): + assert ZeroArray() == 0 + assert ZeroArray().is_Integer + + za = ZeroArray(3, 2, 4) + assert za.shape == (3, 2, 4) + za_e = za.as_explicit() + assert za_e.shape == (3, 2, 4) + + m, n, k = symbols("m n k") + za = ZeroArray(m, n, k, 2) + assert za.shape == (m, n, k, 2) + raises(ValueError, lambda: za.as_explicit()) + + +def test_one_array(): + assert OneArray() == 1 + assert OneArray().is_Integer + + oa = OneArray(3, 2, 4) + assert oa.shape == (3, 2, 4) + oa_e = oa.as_explicit() + assert oa_e.shape == (3, 2, 4) + + m, n, k = symbols("m n k") + oa = OneArray(m, n, k, 2) + assert oa.shape == (m, n, k, 2) + raises(ValueError, lambda: oa.as_explicit()) + + +def test_arrayexpr_contraction_construction(): + + cg = _array_contraction(A) + assert cg == A + + cg = _array_contraction(_array_tensor_product(A, B), (1, 0)) + assert cg == _array_contraction(_array_tensor_product(A, B), (0, 1)) + + cg = _array_contraction(_array_tensor_product(M, N), (0, 1)) + indtup = cg._get_contraction_tuples() + assert indtup == [[(0, 0), (0, 1)]] + assert cg._contraction_tuples_to_contraction_indices(cg.expr, indtup) == [(0, 1)] + + cg = _array_contraction(_array_tensor_product(M, N), (1, 2)) + indtup = cg._get_contraction_tuples() + assert indtup == [[(0, 1), (1, 0)]] + assert cg._contraction_tuples_to_contraction_indices(cg.expr, indtup) == [(1, 2)] + + cg = _array_contraction(_array_tensor_product(M, M, N), (1, 4), (2, 5)) + indtup = cg._get_contraction_tuples() + assert indtup == [[(0, 0), (1, 1)], [(0, 1), (2, 0)]] + assert cg._contraction_tuples_to_contraction_indices(cg.expr, indtup) == [(0, 3), (1, 4)] + + # Test removal of trivial contraction: + assert _array_contraction(a, (1,)) == a + assert _array_contraction( + _array_tensor_product(a, b), (0, 2), (1,), (3,)) == _array_contraction( + _array_tensor_product(a, b), (0, 2)) + + +def test_arrayexpr_array_flatten(): + + # Flatten nested ArrayTensorProduct objects: + expr1 = _array_tensor_product(M, N) + expr2 = _array_tensor_product(P, Q) + expr = _array_tensor_product(expr1, expr2) + assert expr == _array_tensor_product(M, N, P, Q) + assert expr.args == (M, N, P, Q) + + # Flatten mixed ArrayTensorProduct and ArrayContraction objects: + cg1 = _array_contraction(expr1, (1, 2)) + cg2 = _array_contraction(expr2, (0, 3)) + + expr = _array_tensor_product(cg1, cg2) + assert expr == _array_contraction(_array_tensor_product(M, N, P, Q), (1, 2), (4, 7)) + + expr = _array_tensor_product(M, cg1) + assert expr == _array_contraction(_array_tensor_product(M, M, N), (3, 4)) + + # Flatten nested ArrayContraction objects: + cgnested = _array_contraction(cg1, (0, 1)) + assert cgnested == _array_contraction(_array_tensor_product(M, N), (0, 3), (1, 2)) + + cgnested = _array_contraction(_array_tensor_product(cg1, cg2), (0, 3)) + assert cgnested == _array_contraction(_array_tensor_product(M, N, P, Q), (0, 6), (1, 2), (4, 7)) + + cg3 = _array_contraction(_array_tensor_product(M, N, P, Q), (1, 3), (2, 4)) + cgnested = _array_contraction(cg3, (0, 1)) + assert cgnested == _array_contraction(_array_tensor_product(M, N, P, Q), (0, 5), (1, 3), (2, 4)) + + cgnested = _array_contraction(cg3, (0, 3), (1, 2)) + assert cgnested == _array_contraction(_array_tensor_product(M, N, P, Q), (0, 7), (1, 3), (2, 4), (5, 6)) + + cg4 = _array_contraction(_array_tensor_product(M, N, P, Q), (1, 5), (3, 7)) + cgnested = _array_contraction(cg4, (0, 1)) + assert cgnested == _array_contraction(_array_tensor_product(M, N, P, Q), (0, 2), (1, 5), (3, 7)) + + cgnested = _array_contraction(cg4, (0, 1), (2, 3)) + assert cgnested == _array_contraction(_array_tensor_product(M, N, P, Q), (0, 2), (1, 5), (3, 7), (4, 6)) + + cg = _array_diagonal(cg4) + assert cg == cg4 + assert isinstance(cg, type(cg4)) + + # Flatten nested ArrayDiagonal objects: + cg1 = _array_diagonal(expr1, (1, 2)) + cg2 = _array_diagonal(expr2, (0, 3)) + cg3 = _array_diagonal(_array_tensor_product(M, N, P, Q), (1, 3), (2, 4)) + cg4 = _array_diagonal(_array_tensor_product(M, N, P, Q), (1, 5), (3, 7)) + + cgnested = _array_diagonal(cg1, (0, 1)) + assert cgnested == _array_diagonal(_array_tensor_product(M, N), (1, 2), (0, 3)) + + cgnested = _array_diagonal(cg3, (1, 2)) + assert cgnested == _array_diagonal(_array_tensor_product(M, N, P, Q), (1, 3), (2, 4), (5, 6)) + + cgnested = _array_diagonal(cg4, (1, 2)) + assert cgnested == _array_diagonal(_array_tensor_product(M, N, P, Q), (1, 5), (3, 7), (2, 4)) + + cg = _array_add(M, N) + cg2 = _array_add(cg, P) + assert isinstance(cg2, ArrayAdd) + assert cg2.args == (M, N, P) + assert cg2.shape == (k, k) + + expr = _array_tensor_product(_array_diagonal(X, (0, 1)), _array_diagonal(A, (0, 1))) + assert expr == _array_diagonal(_array_tensor_product(X, A), (0, 1), (2, 3)) + + expr1 = _array_diagonal(_array_tensor_product(X, A), (1, 2)) + expr2 = _array_tensor_product(expr1, a) + assert expr2 == _permute_dims(_array_diagonal(_array_tensor_product(X, A, a), (1, 2)), [0, 1, 4, 2, 3]) + + expr1 = _array_contraction(_array_tensor_product(X, A), (1, 2)) + expr2 = _array_tensor_product(expr1, a) + assert isinstance(expr2, ArrayContraction) + assert isinstance(expr2.expr, ArrayTensorProduct) + + cg = _array_tensor_product(_array_diagonal(_array_tensor_product(A, X, Y), (0, 3), (1, 5)), a, b) + assert cg == _permute_dims(_array_diagonal(_array_tensor_product(A, X, Y, a, b), (0, 3), (1, 5)), [0, 1, 6, 7, 2, 3, 4, 5]) + + +def test_arrayexpr_array_diagonal(): + cg = _array_diagonal(M, (1, 0)) + assert cg == _array_diagonal(M, (0, 1)) + + cg = _array_diagonal(_array_tensor_product(M, N, P), (4, 1), (2, 0)) + assert cg == _array_diagonal(_array_tensor_product(M, N, P), (1, 4), (0, 2)) + + cg = _array_diagonal(_array_tensor_product(M, N), (1, 2), (3,), allow_trivial_diags=True) + assert cg == _permute_dims(_array_diagonal(_array_tensor_product(M, N), (1, 2)), [0, 2, 1]) + + Ax = ArraySymbol("Ax", shape=(1, 2, 3, 4, 3, 5, 6, 2, 7)) + cg = _array_diagonal(Ax, (1, 7), (3,), (2, 4), (6,), allow_trivial_diags=True) + assert cg == _permute_dims(_array_diagonal(Ax, (1, 7), (2, 4)), [0, 2, 4, 5, 1, 6, 3]) + + cg = _array_diagonal(M, (0,), allow_trivial_diags=True) + assert cg == _permute_dims(M, [1, 0]) + + raises(ValueError, lambda: _array_diagonal(M, (0, 0))) + + +def test_arrayexpr_array_shape(): + expr = _array_tensor_product(M, N, P, Q) + assert expr.shape == (k, k, k, k, k, k, k, k) + Z = MatrixSymbol("Z", m, n) + expr = _array_tensor_product(M, Z) + assert expr.shape == (k, k, m, n) + expr2 = _array_contraction(expr, (0, 1)) + assert expr2.shape == (m, n) + expr2 = _array_diagonal(expr, (0, 1)) + assert expr2.shape == (m, n, k) + exprp = _permute_dims(expr, [2, 1, 3, 0]) + assert exprp.shape == (m, k, n, k) + expr3 = _array_tensor_product(N, Z) + expr2 = _array_add(expr, expr3) + assert expr2.shape == (k, k, m, n) + + # Contraction along axes with discordant dimensions: + raises(ValueError, lambda: _array_contraction(expr, (1, 2))) + # Also diagonal needs the same dimensions: + raises(ValueError, lambda: _array_diagonal(expr, (1, 2))) + # Diagonal requires at least to axes to compute the diagonal: + raises(ValueError, lambda: _array_diagonal(expr, (1,))) + + +def test_arrayexpr_permutedims_sink(): + + cg = _permute_dims(_array_tensor_product(M, N), [0, 1, 3, 2], nest_permutation=False) + sunk = nest_permutation(cg) + assert sunk == _array_tensor_product(M, _permute_dims(N, [1, 0])) + + cg = _permute_dims(_array_tensor_product(M, N), [1, 0, 3, 2], nest_permutation=False) + sunk = nest_permutation(cg) + assert sunk == _array_tensor_product(_permute_dims(M, [1, 0]), _permute_dims(N, [1, 0])) + + cg = _permute_dims(_array_tensor_product(M, N), [3, 2, 1, 0], nest_permutation=False) + sunk = nest_permutation(cg) + assert sunk == _array_tensor_product(_permute_dims(N, [1, 0]), _permute_dims(M, [1, 0])) + + cg = _permute_dims(_array_contraction(_array_tensor_product(M, N), (1, 2)), [1, 0], nest_permutation=False) + sunk = nest_permutation(cg) + assert sunk == _array_contraction(_permute_dims(_array_tensor_product(M, N), [[0, 3]]), (1, 2)) + + cg = _permute_dims(_array_tensor_product(M, N), [1, 0, 3, 2], nest_permutation=False) + sunk = nest_permutation(cg) + assert sunk == _array_tensor_product(_permute_dims(M, [1, 0]), _permute_dims(N, [1, 0])) + + cg = _permute_dims(_array_contraction(_array_tensor_product(M, N, P), (1, 2), (3, 4)), [1, 0], nest_permutation=False) + sunk = nest_permutation(cg) + assert sunk == _array_contraction(_permute_dims(_array_tensor_product(M, N, P), [[0, 5]]), (1, 2), (3, 4)) + + +def test_arrayexpr_push_indices_up_and_down(): + + indices = list(range(12)) + + contr_diag_indices = [(0, 6), (2, 8)] + assert ArrayContraction._push_indices_down(contr_diag_indices, indices) == (1, 3, 4, 5, 7, 9, 10, 11, 12, 13, 14, 15) + assert ArrayContraction._push_indices_up(contr_diag_indices, indices) == (None, 0, None, 1, 2, 3, None, 4, None, 5, 6, 7) + + assert ArrayDiagonal._push_indices_down(contr_diag_indices, indices, 10) == (1, 3, 4, 5, 7, 9, (0, 6), (2, 8), None, None, None, None) + assert ArrayDiagonal._push_indices_up(contr_diag_indices, indices, 10) == (6, 0, 7, 1, 2, 3, 6, 4, 7, 5, None, None) + + contr_diag_indices = [(1, 2), (7, 8)] + assert ArrayContraction._push_indices_down(contr_diag_indices, indices) == (0, 3, 4, 5, 6, 9, 10, 11, 12, 13, 14, 15) + assert ArrayContraction._push_indices_up(contr_diag_indices, indices) == (0, None, None, 1, 2, 3, 4, None, None, 5, 6, 7) + + assert ArrayDiagonal._push_indices_down(contr_diag_indices, indices, 10) == (0, 3, 4, 5, 6, 9, (1, 2), (7, 8), None, None, None, None) + assert ArrayDiagonal._push_indices_up(contr_diag_indices, indices, 10) == (0, 6, 6, 1, 2, 3, 4, 7, 7, 5, None, None) + + +def test_arrayexpr_split_multiple_contractions(): + a = MatrixSymbol("a", k, 1) + b = MatrixSymbol("b", k, 1) + A = MatrixSymbol("A", k, k) + B = MatrixSymbol("B", k, k) + C = MatrixSymbol("C", k, k) + X = MatrixSymbol("X", k, k) + + cg = _array_contraction(_array_tensor_product(A.T, a, b, b.T, (A*X*b).applyfunc(cos)), (1, 2, 8), (5, 6, 9)) + expected = _array_contraction(_array_tensor_product(A.T, DiagMatrix(a), OneArray(1), b, b.T, (A*X*b).applyfunc(cos)), (1, 3), (2, 9), (6, 7, 10)) + assert cg.split_multiple_contractions().dummy_eq(expected) + + # Check no overlap of lines: + + cg = _array_contraction(_array_tensor_product(A, a, C, a, B), (1, 2, 4), (5, 6, 8), (3, 7)) + assert cg.split_multiple_contractions() == cg + + cg = _array_contraction(_array_tensor_product(a, b, A), (0, 2, 4), (1, 3)) + assert cg.split_multiple_contractions() == cg + + +def test_arrayexpr_nested_permutations(): + + cg = _permute_dims(_permute_dims(M, (1, 0)), (1, 0)) + assert cg == M + + times = 3 + plist1 = [list(range(6)) for i in range(times)] + plist2 = [list(range(6)) for i in range(times)] + + for i in range(times): + random.shuffle(plist1[i]) + random.shuffle(plist2[i]) + + plist1.append([2, 5, 4, 1, 0, 3]) + plist2.append([3, 5, 0, 4, 1, 2]) + + plist1.append([2, 5, 4, 0, 3, 1]) + plist2.append([3, 0, 5, 1, 2, 4]) + + plist1.append([5, 4, 2, 0, 3, 1]) + plist2.append([4, 5, 0, 2, 3, 1]) + + Me = M.subs(k, 3).as_explicit() + Ne = N.subs(k, 3).as_explicit() + Pe = P.subs(k, 3).as_explicit() + cge = tensorproduct(Me, Ne, Pe) + + for permutation_array1, permutation_array2 in zip(plist1, plist2): + p1 = Permutation(permutation_array1) + p2 = Permutation(permutation_array2) + + cg = _permute_dims( + _permute_dims( + _array_tensor_product(M, N, P), + p1), + p2 + ) + result = _permute_dims( + _array_tensor_product(M, N, P), + p2*p1 + ) + assert cg == result + + # Check that `permutedims` behaves the same way with explicit-component arrays: + result1 = _permute_dims(_permute_dims(cge, p1), p2) + result2 = _permute_dims(cge, p2*p1) + assert result1 == result2 + + +def test_arrayexpr_contraction_permutation_mix(): + + Me = M.subs(k, 3).as_explicit() + Ne = N.subs(k, 3).as_explicit() + + cg1 = _array_contraction(PermuteDims(_array_tensor_product(M, N), Permutation([0, 2, 1, 3])), (2, 3)) + cg2 = _array_contraction(_array_tensor_product(M, N), (1, 3)) + assert cg1 == cg2 + cge1 = tensorcontraction(permutedims(tensorproduct(Me, Ne), Permutation([0, 2, 1, 3])), (2, 3)) + cge2 = tensorcontraction(tensorproduct(Me, Ne), (1, 3)) + assert cge1 == cge2 + + cg1 = _permute_dims(_array_tensor_product(M, N), Permutation([0, 1, 3, 2])) + cg2 = _array_tensor_product(M, _permute_dims(N, Permutation([1, 0]))) + assert cg1 == cg2 + + cg1 = _array_contraction( + _permute_dims( + _array_tensor_product(M, N, P, Q), Permutation([0, 2, 3, 1, 4, 5, 7, 6])), + (1, 2), (3, 5) + ) + cg2 = _array_contraction( + _array_tensor_product(M, N, P, _permute_dims(Q, Permutation([1, 0]))), + (1, 5), (2, 3) + ) + assert cg1 == cg2 + + cg1 = _array_contraction( + _permute_dims( + _array_tensor_product(M, N, P, Q), Permutation([1, 0, 4, 6, 2, 7, 5, 3])), + (0, 1), (2, 6), (3, 7) + ) + cg2 = _permute_dims( + _array_contraction( + _array_tensor_product(M, P, Q, N), + (0, 1), (2, 3), (4, 7)), + [1, 0] + ) + assert cg1 == cg2 + + cg1 = _array_contraction( + _permute_dims( + _array_tensor_product(M, N, P, Q), Permutation([1, 0, 4, 6, 7, 2, 5, 3])), + (0, 1), (2, 6), (3, 7) + ) + cg2 = _permute_dims( + _array_contraction( + _array_tensor_product(_permute_dims(M, [1, 0]), N, P, Q), + (0, 1), (3, 6), (4, 5) + ), + Permutation([1, 0]) + ) + assert cg1 == cg2 + + +def test_arrayexpr_permute_tensor_product(): + cg1 = _permute_dims(_array_tensor_product(M, N, P, Q), Permutation([2, 3, 1, 0, 5, 4, 6, 7])) + cg2 = _array_tensor_product(N, _permute_dims(M, [1, 0]), + _permute_dims(P, [1, 0]), Q) + assert cg1 == cg2 + + # TODO: reverse operation starting with `PermuteDims` and getting down to `bb`... + cg1 = _permute_dims(_array_tensor_product(M, N, P, Q), Permutation([2, 3, 4, 5, 0, 1, 6, 7])) + cg2 = _array_tensor_product(N, P, M, Q) + assert cg1 == cg2 + + cg1 = _permute_dims(_array_tensor_product(M, N, P, Q), Permutation([2, 3, 4, 6, 5, 7, 0, 1])) + assert cg1.expr == _array_tensor_product(N, P, Q, M) + assert cg1.permutation == Permutation([0, 1, 2, 4, 3, 5, 6, 7]) + + cg1 = _array_contraction( + _permute_dims( + _array_tensor_product(N, Q, Q, M), + [2, 1, 5, 4, 0, 3, 6, 7]), + [1, 2, 6]) + cg2 = _permute_dims(_array_contraction(_array_tensor_product(Q, Q, N, M), (3, 5, 6)), [0, 2, 3, 1, 4]) + assert cg1 == cg2 + + cg1 = _array_contraction( + _array_contraction( + _array_contraction( + _array_contraction( + _permute_dims( + _array_tensor_product(N, Q, Q, M), + [2, 1, 5, 4, 0, 3, 6, 7]), + [1, 2, 6]), + [1, 3, 4]), + [1]), + [0]) + cg2 = _array_contraction(_array_tensor_product(M, N, Q, Q), (0, 3, 5), (1, 4, 7), (2,), (6,)) + assert cg1 == cg2 + + +def test_arrayexpr_canonicalize_diagonal__permute_dims(): + tp = _array_tensor_product(M, Q, N, P) + expr = _array_diagonal( + _permute_dims(tp, [0, 1, 2, 4, 7, 6, 3, 5]), (2, 4, 5), (6, 7), + (0, 3)) + result = _array_diagonal(tp, (2, 6, 7), (3, 5), (0, 4)) + assert expr == result + + tp = _array_tensor_product(M, N, P, Q) + expr = _array_diagonal(_permute_dims(tp, [0, 5, 2, 4, 1, 6, 3, 7]), (1, 2, 6), (3, 4)) + result = _array_diagonal(_array_tensor_product(M, P, N, Q), (3, 4, 5), (1, 2)) + assert expr == result + + +def test_arrayexpr_canonicalize_diagonal_contraction(): + tp = _array_tensor_product(M, N, P, Q) + expr = _array_contraction(_array_diagonal(tp, (1, 3, 4)), (0, 3)) + result = _array_diagonal(_array_contraction(_array_tensor_product(M, N, P, Q), (0, 6)), (0, 2, 3)) + assert expr == result + + expr = _array_contraction(_array_diagonal(tp, (0, 1, 2, 3, 7)), (1, 2, 3)) + result = _array_contraction(_array_tensor_product(M, N, P, Q), (0, 1, 2, 3, 5, 6, 7)) + assert expr == result + + expr = _array_contraction(_array_diagonal(tp, (0, 2, 6, 7)), (1, 2, 3)) + result = _array_diagonal(_array_contraction(tp, (3, 4, 5)), (0, 2, 3, 4)) + assert expr == result + + td = _array_diagonal(_array_tensor_product(M, N, P, Q), (0, 3)) + expr = _array_contraction(td, (2, 1), (0, 4, 6, 5, 3)) + result = _array_contraction(_array_tensor_product(M, N, P, Q), (0, 1, 3, 5, 6, 7), (2, 4)) + assert expr == result + + +def test_arrayexpr_array_wrong_permutation_size(): + cg = _array_tensor_product(M, N) + raises(ValueError, lambda: _permute_dims(cg, [1, 0])) + raises(ValueError, lambda: _permute_dims(cg, [1, 0, 2, 3, 5, 4])) + + +def test_arrayexpr_nested_array_elementwise_add(): + cg = _array_contraction(_array_add( + _array_tensor_product(M, N), + _array_tensor_product(N, M) + ), (1, 2)) + result = _array_add( + _array_contraction(_array_tensor_product(M, N), (1, 2)), + _array_contraction(_array_tensor_product(N, M), (1, 2)) + ) + assert cg == result + + cg = _array_diagonal(_array_add( + _array_tensor_product(M, N), + _array_tensor_product(N, M) + ), (1, 2)) + result = _array_add( + _array_diagonal(_array_tensor_product(M, N), (1, 2)), + _array_diagonal(_array_tensor_product(N, M), (1, 2)) + ) + assert cg == result + + +def test_arrayexpr_array_expr_zero_array(): + za1 = ZeroArray(k, l, m, n) + zm1 = ZeroMatrix(m, n) + + za2 = ZeroArray(k, m, m, n) + zm2 = ZeroMatrix(m, m) + zm3 = ZeroMatrix(k, k) + + assert _array_tensor_product(M, N, za1) == ZeroArray(k, k, k, k, k, l, m, n) + assert _array_tensor_product(M, N, zm1) == ZeroArray(k, k, k, k, m, n) + + assert _array_contraction(za1, (3,)) == ZeroArray(k, l, m) + assert _array_contraction(zm1, (1,)) == ZeroArray(m) + assert _array_contraction(za2, (1, 2)) == ZeroArray(k, n) + assert _array_contraction(zm2, (0, 1)) == 0 + + assert _array_diagonal(za2, (1, 2)) == ZeroArray(k, n, m) + assert _array_diagonal(zm2, (0, 1)) == ZeroArray(m) + + assert _permute_dims(za1, [2, 1, 3, 0]) == ZeroArray(m, l, n, k) + assert _permute_dims(zm1, [1, 0]) == ZeroArray(n, m) + + assert _array_add(za1) == za1 + assert _array_add(zm1) == ZeroArray(m, n) + tp1 = _array_tensor_product(MatrixSymbol("A", k, l), MatrixSymbol("B", m, n)) + assert _array_add(tp1, za1) == tp1 + tp2 = _array_tensor_product(MatrixSymbol("C", k, l), MatrixSymbol("D", m, n)) + assert _array_add(tp1, za1, tp2) == _array_add(tp1, tp2) + assert _array_add(M, zm3) == M + assert _array_add(M, N, zm3) == _array_add(M, N) + + +def test_arrayexpr_array_expr_applyfunc(): + + A = ArraySymbol("A", (3, k, 2)) + aaf = ArrayElementwiseApplyFunc(sin, A) + assert aaf.shape == (3, k, 2) + + +def test_edit_array_contraction(): + cg = _array_contraction(_array_tensor_product(A, B, C, D), (1, 2, 5)) + ecg = _EditArrayContraction(cg) + assert ecg.to_array_contraction() == cg + + ecg.args_with_ind[1], ecg.args_with_ind[2] = ecg.args_with_ind[2], ecg.args_with_ind[1] + assert ecg.to_array_contraction() == _array_contraction(_array_tensor_product(A, C, B, D), (1, 3, 4)) + + ci = ecg.get_new_contraction_index() + new_arg = _ArgE(X) + new_arg.indices = [ci, ci] + ecg.args_with_ind.insert(2, new_arg) + assert ecg.to_array_contraction() == _array_contraction(_array_tensor_product(A, C, X, B, D), (1, 3, 6), (4, 5)) + + assert ecg.get_contraction_indices() == [[1, 3, 6], [4, 5]] + assert [[tuple(j) for j in i] for i in ecg.get_contraction_indices_to_ind_rel_pos()] == [[(0, 1), (1, 1), (3, 0)], [(2, 0), (2, 1)]] + assert [list(i) for i in ecg.get_mapping_for_index(0)] == [[0, 1], [1, 1], [3, 0]] + assert [list(i) for i in ecg.get_mapping_for_index(1)] == [[2, 0], [2, 1]] + raises(ValueError, lambda: ecg.get_mapping_for_index(2)) + + ecg.args_with_ind.pop(1) + assert ecg.to_array_contraction() == _array_contraction(_array_tensor_product(A, X, B, D), (1, 4), (2, 3)) + + ecg.args_with_ind[0].indices[1] = ecg.args_with_ind[1].indices[0] + ecg.args_with_ind[1].indices[1] = ecg.args_with_ind[2].indices[0] + assert ecg.to_array_contraction() == _array_contraction(_array_tensor_product(A, X, B, D), (1, 2), (3, 4)) + + ecg.insert_after(ecg.args_with_ind[1], _ArgE(C)) + assert ecg.to_array_contraction() == _array_contraction(_array_tensor_product(A, X, C, B, D), (1, 2), (3, 6)) + + +def test_array_expressions_no_canonicalization(): + + tp = _array_tensor_product(M, N, P) + + # ArrayTensorProduct: + + expr = ArrayTensorProduct(tp, N) + assert str(expr) == "ArrayTensorProduct(ArrayTensorProduct(M, N, P), N)" + assert expr.doit() == ArrayTensorProduct(M, N, P, N) + + expr = ArrayTensorProduct(ArrayContraction(M, (0, 1)), N) + assert str(expr) == "ArrayTensorProduct(ArrayContraction(M, (0, 1)), N)" + assert expr.doit() == ArrayContraction(ArrayTensorProduct(M, N), (0, 1)) + + expr = ArrayTensorProduct(ArrayDiagonal(M, (0, 1)), N) + assert str(expr) == "ArrayTensorProduct(ArrayDiagonal(M, (0, 1)), N)" + assert expr.doit() == PermuteDims(ArrayDiagonal(ArrayTensorProduct(M, N), (0, 1)), [2, 0, 1]) + + expr = ArrayTensorProduct(PermuteDims(M, [1, 0]), N) + assert str(expr) == "ArrayTensorProduct(PermuteDims(M, (0 1)), N)" + assert expr.doit() == PermuteDims(ArrayTensorProduct(M, N), [1, 0, 2, 3]) + + # ArrayContraction: + + expr = ArrayContraction(_array_contraction(tp, (0, 2)), (0, 1)) + assert isinstance(expr, ArrayContraction) + assert isinstance(expr.expr, ArrayContraction) + assert str(expr) == "ArrayContraction(ArrayContraction(ArrayTensorProduct(M, N, P), (0, 2)), (0, 1))" + assert expr.doit() == ArrayContraction(tp, (0, 2), (1, 3)) + + expr = ArrayContraction(ArrayContraction(ArrayContraction(tp, (0, 1)), (0, 1)), (0, 1)) + assert expr.doit() == ArrayContraction(tp, (0, 1), (2, 3), (4, 5)) + # assert expr._canonicalize() == ArrayContraction(ArrayContraction(tp, (0, 1)), (0, 1), (2, 3)) + + expr = ArrayContraction(ArrayDiagonal(tp, (0, 1)), (0, 1)) + assert str(expr) == "ArrayContraction(ArrayDiagonal(ArrayTensorProduct(M, N, P), (0, 1)), (0, 1))" + assert expr.doit() == ArrayDiagonal(ArrayContraction(ArrayTensorProduct(N, M, P), (0, 1)), (0, 1)) + + expr = ArrayContraction(PermuteDims(M, [1, 0]), (0, 1)) + assert str(expr) == "ArrayContraction(PermuteDims(M, (0 1)), (0, 1))" + assert expr.doit() == ArrayContraction(M, (0, 1)) + + # ArrayDiagonal: + + expr = ArrayDiagonal(ArrayDiagonal(tp, (0, 2)), (0, 1)) + assert str(expr) == "ArrayDiagonal(ArrayDiagonal(ArrayTensorProduct(M, N, P), (0, 2)), (0, 1))" + assert expr.doit() == ArrayDiagonal(tp, (0, 2), (1, 3)) + + expr = ArrayDiagonal(ArrayDiagonal(ArrayDiagonal(tp, (0, 1)), (0, 1)), (0, 1)) + assert expr.doit() == ArrayDiagonal(tp, (0, 1), (2, 3), (4, 5)) + assert expr._canonicalize() == expr.doit() + + expr = ArrayDiagonal(ArrayContraction(tp, (0, 1)), (0, 1)) + assert str(expr) == "ArrayDiagonal(ArrayContraction(ArrayTensorProduct(M, N, P), (0, 1)), (0, 1))" + assert expr.doit() == expr + + expr = ArrayDiagonal(PermuteDims(M, [1, 0]), (0, 1)) + assert str(expr) == "ArrayDiagonal(PermuteDims(M, (0 1)), (0, 1))" + assert expr.doit() == ArrayDiagonal(M, (0, 1)) + + # ArrayAdd: + + expr = ArrayAdd(M) + assert isinstance(expr, ArrayAdd) + assert expr.doit() == M + + expr = ArrayAdd(ArrayAdd(M, N), P) + assert str(expr) == "ArrayAdd(ArrayAdd(M, N), P)" + assert expr.doit() == ArrayAdd(M, N, P) + + expr = ArrayAdd(M, ArrayAdd(N, ArrayAdd(P, M))) + assert expr.doit() == ArrayAdd(M, N, P, M) + assert expr._canonicalize() == ArrayAdd(M, N, ArrayAdd(P, M)) + + expr = ArrayAdd(M, ZeroArray(k, k), N) + assert str(expr) == "ArrayAdd(M, ZeroArray(k, k), N)" + assert expr.doit() == ArrayAdd(M, N) + + # PermuteDims: + + expr = PermuteDims(PermuteDims(M, [1, 0]), [1, 0]) + assert str(expr) == "PermuteDims(PermuteDims(M, (0 1)), (0 1))" + assert expr.doit() == M + + expr = PermuteDims(PermuteDims(PermuteDims(M, [1, 0]), [1, 0]), [1, 0]) + assert expr.doit() == PermuteDims(M, [1, 0]) + assert expr._canonicalize() == expr.doit() + + # Reshape + + expr = Reshape(A, (k**2,)) + assert expr.shape == (k**2,) + assert isinstance(expr, Reshape) + + +def test_array_expr_construction_with_functions(): + + tp = tensorproduct(M, N) + assert tp == ArrayTensorProduct(M, N) + + expr = tensorproduct(A, eye(2)) + assert expr == ArrayTensorProduct(A, eye(2)) + + # Contraction: + + expr = tensorcontraction(M, (0, 1)) + assert expr == ArrayContraction(M, (0, 1)) + + expr = tensorcontraction(tp, (1, 2)) + assert expr == ArrayContraction(tp, (1, 2)) + + expr = tensorcontraction(tensorcontraction(tp, (1, 2)), (0, 1)) + assert expr == ArrayContraction(tp, (0, 3), (1, 2)) + + # Diagonalization: + + expr = tensordiagonal(M, (0, 1)) + assert expr == ArrayDiagonal(M, (0, 1)) + + expr = tensordiagonal(tensordiagonal(tp, (0, 1)), (0, 1)) + assert expr == ArrayDiagonal(tp, (0, 1), (2, 3)) + + # Permutation of dimensions: + + expr = permutedims(M, [1, 0]) + assert expr == PermuteDims(M, [1, 0]) + + expr = permutedims(PermuteDims(tp, [1, 0, 2, 3]), [0, 1, 3, 2]) + assert expr == PermuteDims(tp, [1, 0, 3, 2]) + + expr = PermuteDims(tp, index_order_new=["a", "b", "c", "d"], index_order_old=["d", "c", "b", "a"]) + assert expr == PermuteDims(tp, [3, 2, 1, 0]) + + arr = Array(range(32)).reshape(2, 2, 2, 2, 2) + expr = PermuteDims(arr, index_order_new=["a", "b", "c", "d", "e"], index_order_old=['b', 'e', 'a', 'd', 'c']) + assert expr == PermuteDims(arr, [2, 0, 4, 3, 1]) + assert expr.as_explicit() == permutedims(arr, index_order_new=["a", "b", "c", "d", "e"], index_order_old=['b', 'e', 'a', 'd', 'c']) + + +def test_array_element_expressions(): + # Check commutative property: + assert M[0, 0]*N[0, 0] == N[0, 0]*M[0, 0] + + # Check derivatives: + assert M[0, 0].diff(M[0, 0]) == 1 + assert M[0, 0].diff(M[1, 0]) == 0 + assert M[0, 0].diff(N[0, 0]) == 0 + assert M[0, 1].diff(M[i, j]) == KroneckerDelta(i, 0)*KroneckerDelta(j, 1) + assert M[0, 1].diff(N[i, j]) == 0 + + K4 = ArraySymbol("K4", shape=(k, k, k, k)) + + assert K4[i, j, k, l].diff(K4[1, 2, 3, 4]) == ( + KroneckerDelta(i, 1)*KroneckerDelta(j, 2)*KroneckerDelta(k, 3)*KroneckerDelta(l, 4) + ) + + +def test_array_expr_reshape(): + + A = MatrixSymbol("A", 2, 2) + B = ArraySymbol("B", (2, 2, 2)) + C = Array([1, 2, 3, 4]) + + expr = Reshape(A, (4,)) + assert expr.expr == A + assert expr.shape == (4,) + assert expr.as_explicit() == Array([A[0, 0], A[0, 1], A[1, 0], A[1, 1]]) + + expr = Reshape(B, (2, 4)) + assert expr.expr == B + assert expr.shape == (2, 4) + ee = expr.as_explicit() + assert isinstance(ee, ImmutableDenseNDimArray) + assert ee.shape == (2, 4) + assert ee == Array([[B[0, 0, 0], B[0, 0, 1], B[0, 1, 0], B[0, 1, 1]], [B[1, 0, 0], B[1, 0, 1], B[1, 1, 0], B[1, 1, 1]]]) + + expr = Reshape(A, (k, 2)) + assert expr.shape == (k, 2) + + raises(ValueError, lambda: Reshape(A, (2, 3))) + raises(ValueError, lambda: Reshape(A, (3,))) + + expr = Reshape(C, (2, 2)) + assert expr.expr == C + assert expr.shape == (2, 2) + assert expr.doit() == Array([[1, 2], [3, 4]]) + + +def test_array_expr_as_explicit_with_explicit_component_arrays(): + # Test if .as_explicit() works with explicit-component arrays + # nested in array expressions: + from sympy.abc import x, y, z, t + A = Array([[x, y], [z, t]]) + assert ArrayTensorProduct(A, A).as_explicit() == tensorproduct(A, A) + assert ArrayDiagonal(A, (0, 1)).as_explicit() == tensordiagonal(A, (0, 1)) + assert ArrayContraction(A, (0, 1)).as_explicit() == tensorcontraction(A, (0, 1)) + assert ArrayAdd(A, A).as_explicit() == A + A + assert ArrayElementwiseApplyFunc(sin, A).as_explicit() == A.applyfunc(sin) + assert PermuteDims(A, [1, 0]).as_explicit() == permutedims(A, [1, 0]) + assert Reshape(A, [4]).as_explicit() == A.reshape(4) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_arrayexpr_derivatives.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_arrayexpr_derivatives.py new file mode 100644 index 0000000000000000000000000000000000000000..bc0fcf63f2607b23feb38758e4f0994de4f0384b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_arrayexpr_derivatives.py @@ -0,0 +1,78 @@ +from sympy.core.symbol import symbols +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.matrices.expressions.special import Identity +from sympy.matrices.expressions.applyfunc import ElementwiseApplyFunction +from sympy.tensor.array.expressions.array_expressions import ArraySymbol, ArrayTensorProduct, \ + PermuteDims, ArrayDiagonal, ArrayElementwiseApplyFunc, ArrayContraction, _permute_dims, Reshape +from sympy.tensor.array.expressions.arrayexpr_derivatives import array_derive + +k = symbols("k") + +I = Identity(k) +X = MatrixSymbol("X", k, k) +x = MatrixSymbol("x", k, 1) + +A = MatrixSymbol("A", k, k) +B = MatrixSymbol("B", k, k) +C = MatrixSymbol("C", k, k) +D = MatrixSymbol("D", k, k) + +A1 = ArraySymbol("A", (3, 2, k)) + + +def test_arrayexpr_derivatives1(): + + res = array_derive(X, X) + assert res == PermuteDims(ArrayTensorProduct(I, I), [0, 2, 1, 3]) + + cg = ArrayTensorProduct(A, X, B) + res = array_derive(cg, X) + assert res == _permute_dims( + ArrayTensorProduct(I, A, I, B), + [0, 4, 2, 3, 1, 5, 6, 7]) + + cg = ArrayContraction(X, (0, 1)) + res = array_derive(cg, X) + assert res == ArrayContraction(ArrayTensorProduct(I, I), (1, 3)) + + cg = ArrayDiagonal(X, (0, 1)) + res = array_derive(cg, X) + assert res == ArrayDiagonal(ArrayTensorProduct(I, I), (1, 3)) + + cg = ElementwiseApplyFunction(sin, X) + res = array_derive(cg, X) + assert res.dummy_eq(ArrayDiagonal( + ArrayTensorProduct( + ElementwiseApplyFunction(cos, X), + I, + I + ), (0, 3), (1, 5))) + + cg = ArrayElementwiseApplyFunc(sin, X) + res = array_derive(cg, X) + assert res.dummy_eq(ArrayDiagonal( + ArrayTensorProduct( + I, + I, + ArrayElementwiseApplyFunc(cos, X) + ), (1, 4), (3, 5))) + + res = array_derive(A1, A1) + assert res == PermuteDims( + ArrayTensorProduct(Identity(3), Identity(2), Identity(k)), + [0, 2, 4, 1, 3, 5] + ) + + cg = ArrayElementwiseApplyFunc(sin, A1) + res = array_derive(cg, A1) + assert res.dummy_eq(ArrayDiagonal( + ArrayTensorProduct( + Identity(3), Identity(2), Identity(k), + ArrayElementwiseApplyFunc(cos, A1) + ), (1, 6), (3, 7), (5, 8) + )) + + cg = Reshape(A, (k**2,)) + res = array_derive(cg, A) + assert res == Reshape(PermuteDims(ArrayTensorProduct(I, I), [0, 2, 1, 3]), (k, k, k**2)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_as_explicit.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_as_explicit.py new file mode 100644 index 0000000000000000000000000000000000000000..30cc61b1ee651ca032e165cd67926fa33c71354f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_as_explicit.py @@ -0,0 +1,63 @@ +from sympy.core.symbol import Symbol +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.tensor.array.arrayop import (permutedims, tensorcontraction, tensordiagonal, tensorproduct) +from sympy.tensor.array.dense_ndim_array import ImmutableDenseNDimArray +from sympy.tensor.array.expressions.array_expressions import ZeroArray, OneArray, ArraySymbol, \ + ArrayTensorProduct, PermuteDims, ArrayDiagonal, ArrayContraction, ArrayAdd +from sympy.testing.pytest import raises + + +def test_array_as_explicit_call(): + + assert ZeroArray(3, 2, 4).as_explicit() == ImmutableDenseNDimArray.zeros(3, 2, 4) + assert OneArray(3, 2, 4).as_explicit() == ImmutableDenseNDimArray([1 for i in range(3*2*4)]).reshape(3, 2, 4) + + k = Symbol("k") + X = ArraySymbol("X", (k, 3, 2)) + raises(ValueError, lambda: X.as_explicit()) + raises(ValueError, lambda: ZeroArray(k, 2, 3).as_explicit()) + raises(ValueError, lambda: OneArray(2, k, 2).as_explicit()) + + A = ArraySymbol("A", (3, 3)) + B = ArraySymbol("B", (3, 3)) + + texpr = tensorproduct(A, B) + assert isinstance(texpr, ArrayTensorProduct) + assert texpr.as_explicit() == tensorproduct(A.as_explicit(), B.as_explicit()) + + texpr = tensorcontraction(A, (0, 1)) + assert isinstance(texpr, ArrayContraction) + assert texpr.as_explicit() == A[0, 0] + A[1, 1] + A[2, 2] + + texpr = tensordiagonal(A, (0, 1)) + assert isinstance(texpr, ArrayDiagonal) + assert texpr.as_explicit() == ImmutableDenseNDimArray([A[0, 0], A[1, 1], A[2, 2]]) + + texpr = permutedims(A, [1, 0]) + assert isinstance(texpr, PermuteDims) + assert texpr.as_explicit() == permutedims(A.as_explicit(), [1, 0]) + + +def test_array_as_explicit_matrix_symbol(): + + A = MatrixSymbol("A", 3, 3) + B = MatrixSymbol("B", 3, 3) + + texpr = tensorproduct(A, B) + assert isinstance(texpr, ArrayTensorProduct) + assert texpr.as_explicit() == tensorproduct(A.as_explicit(), B.as_explicit()) + + texpr = tensorcontraction(A, (0, 1)) + assert isinstance(texpr, ArrayContraction) + assert texpr.as_explicit() == A[0, 0] + A[1, 1] + A[2, 2] + + texpr = tensordiagonal(A, (0, 1)) + assert isinstance(texpr, ArrayDiagonal) + assert texpr.as_explicit() == ImmutableDenseNDimArray([A[0, 0], A[1, 1], A[2, 2]]) + + texpr = permutedims(A, [1, 0]) + assert isinstance(texpr, PermuteDims) + assert texpr.as_explicit() == permutedims(A.as_explicit(), [1, 0]) + + expr = ArrayAdd(ArrayTensorProduct(A, B), ArrayTensorProduct(B, A)) + assert expr.as_explicit() == expr.args[0].as_explicit() + expr.args[1].as_explicit() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_convert_array_to_indexed.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_convert_array_to_indexed.py new file mode 100644 index 0000000000000000000000000000000000000000..a6b713fbec94ab7808c5a8a778b3313402d9d0c7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_convert_array_to_indexed.py @@ -0,0 +1,61 @@ +from sympy import Sum, Dummy, sin +from sympy.tensor.array.expressions import ArraySymbol, ArrayTensorProduct, ArrayContraction, PermuteDims, \ + ArrayDiagonal, ArrayAdd, OneArray, ZeroArray, convert_indexed_to_array, ArrayElementwiseApplyFunc, Reshape +from sympy.tensor.array.expressions.from_array_to_indexed import convert_array_to_indexed + +from sympy.abc import i, j, k, l, m, n, o + + +def test_convert_array_to_indexed_main(): + A = ArraySymbol("A", (3, 3, 3)) + B = ArraySymbol("B", (3, 3)) + C = ArraySymbol("C", (3, 3)) + + d_ = Dummy("d_") + + assert convert_array_to_indexed(A, [i, j, k]) == A[i, j, k] + + expr = ArrayTensorProduct(A, B, C) + conv = convert_array_to_indexed(expr, [i,j,k,l,m,n,o]) + assert conv == A[i,j,k]*B[l,m]*C[n,o] + assert convert_indexed_to_array(conv, [i,j,k,l,m,n,o]) == expr + + expr = ArrayContraction(A, (0, 2)) + assert convert_array_to_indexed(expr, [i]).dummy_eq(Sum(A[d_, i, d_], (d_, 0, 2))) + + expr = ArrayDiagonal(A, (0, 2)) + assert convert_array_to_indexed(expr, [i, j]) == A[j, i, j] + + expr = PermuteDims(A, [1, 2, 0]) + conv = convert_array_to_indexed(expr, [i, j, k]) + assert conv == A[k, i, j] + assert convert_indexed_to_array(conv, [i, j, k]) == expr + + expr = ArrayAdd(B, C, PermuteDims(C, [1, 0])) + conv = convert_array_to_indexed(expr, [i, j]) + assert conv == B[i, j] + C[i, j] + C[j, i] + assert convert_indexed_to_array(conv, [i, j]) == expr + + expr = ArrayElementwiseApplyFunc(sin, A) + conv = convert_array_to_indexed(expr, [i, j, k]) + assert conv == sin(A[i, j, k]) + assert convert_indexed_to_array(conv, [i, j, k]).dummy_eq(expr) + + assert convert_array_to_indexed(OneArray(3, 3), [i, j]) == 1 + assert convert_array_to_indexed(ZeroArray(3, 3), [i, j]) == 0 + + expr = Reshape(A, (27,)) + assert convert_array_to_indexed(expr, [i]) == A[i // 9, i // 3 % 3, i % 3] + + X = ArraySymbol("X", (2, 3, 4, 5, 6)) + expr = Reshape(X, (2*3*4*5*6,)) + assert convert_array_to_indexed(expr, [i]) == X[i // 360, i // 120 % 3, i // 30 % 4, i // 6 % 5, i % 6] + + expr = Reshape(X, (4, 9, 2, 2, 5)) + one_index = 180*i + 20*j + 10*k + 5*l + m + expected = X[one_index // (3*4*5*6), one_index // (4*5*6) % 3, one_index // (5*6) % 4, one_index // 6 % 5, one_index % 6] + assert convert_array_to_indexed(expr, [i, j, k, l, m]) == expected + + X = ArraySymbol("X", (2*3*5,)) + expr = Reshape(X, (2, 3, 5)) + assert convert_array_to_indexed(expr, [i, j, k]) == X[15*i + 5*j + k] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_convert_array_to_matrix.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_convert_array_to_matrix.py new file mode 100644 index 0000000000000000000000000000000000000000..26839d5e7cec0554948c6b726482f9d8ca250b1c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_convert_array_to_matrix.py @@ -0,0 +1,689 @@ +from sympy import Lambda, S, Dummy, KroneckerProduct +from sympy.core.symbol import symbols +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import cos, sin +from sympy.matrices.expressions.hadamard import HadamardProduct, HadamardPower +from sympy.matrices.expressions.special import (Identity, OneMatrix, ZeroMatrix) +from sympy.matrices.expressions.matexpr import MatrixElement +from sympy.tensor.array.expressions.from_matrix_to_array import convert_matrix_to_array +from sympy.tensor.array.expressions.from_array_to_matrix import _support_function_tp1_recognize, \ + _array_diag2contr_diagmatrix, convert_array_to_matrix, _remove_trivial_dims, _array2matrix, \ + _combine_removed, identify_removable_identity_matrices, _array_contraction_to_diagonal_multiple_identity +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.combinatorics import Permutation +from sympy.matrices.expressions.diagonal import DiagMatrix, DiagonalMatrix +from sympy.matrices import Trace, MatMul, Transpose +from sympy.tensor.array.expressions.array_expressions import ZeroArray, OneArray, \ + ArrayElement, ArraySymbol, ArrayElementwiseApplyFunc, _array_tensor_product, _array_contraction, \ + _array_diagonal, _permute_dims, PermuteDims, ArrayAdd, ArrayDiagonal, ArrayContraction, ArrayTensorProduct +from sympy.testing.pytest import raises + + +i, j, k, l, m, n = symbols("i j k l m n") + +I = Identity(k) +I1 = Identity(1) + +M = MatrixSymbol("M", k, k) +N = MatrixSymbol("N", k, k) +P = MatrixSymbol("P", k, k) +Q = MatrixSymbol("Q", k, k) + +A = MatrixSymbol("A", k, k) +B = MatrixSymbol("B", k, k) +C = MatrixSymbol("C", k, k) +D = MatrixSymbol("D", k, k) + +X = MatrixSymbol("X", k, k) +Y = MatrixSymbol("Y", k, k) + +a = MatrixSymbol("a", k, 1) +b = MatrixSymbol("b", k, 1) +c = MatrixSymbol("c", k, 1) +d = MatrixSymbol("d", k, 1) + +x = MatrixSymbol("x", k, 1) +y = MatrixSymbol("y", k, 1) + + +def test_arrayexpr_convert_array_to_matrix(): + + cg = _array_contraction(_array_tensor_product(M), (0, 1)) + assert convert_array_to_matrix(cg) == Trace(M) + + cg = _array_contraction(_array_tensor_product(M, N), (0, 1), (2, 3)) + assert convert_array_to_matrix(cg) == Trace(M) * Trace(N) + + cg = _array_contraction(_array_tensor_product(M, N), (0, 3), (1, 2)) + assert convert_array_to_matrix(cg) == Trace(M * N) + + cg = _array_contraction(_array_tensor_product(M, N), (0, 2), (1, 3)) + assert convert_array_to_matrix(cg) == Trace(M * N.T) + + cg = convert_matrix_to_array(M * N * P) + assert convert_array_to_matrix(cg) == M * N * P + + cg = convert_matrix_to_array(M * N.T * P) + assert convert_array_to_matrix(cg) == M * N.T * P + + cg = _array_contraction(_array_tensor_product(M,N,P,Q), (1, 2), (5, 6)) + assert convert_array_to_matrix(cg) == _array_tensor_product(M * N, P * Q) + + cg = _array_contraction(_array_tensor_product(-2, M, N), (1, 2)) + assert convert_array_to_matrix(cg) == -2 * M * N + + a = MatrixSymbol("a", k, 1) + b = MatrixSymbol("b", k, 1) + c = MatrixSymbol("c", k, 1) + cg = PermuteDims( + _array_contraction( + _array_tensor_product( + a, + ArrayAdd( + _array_tensor_product(b, c), + _array_tensor_product(c, b), + ) + ), (2, 4)), [0, 1, 3, 2]) + assert convert_array_to_matrix(cg) == a * (b.T * c + c.T * b) + + za = ZeroArray(m, n) + assert convert_array_to_matrix(za) == ZeroMatrix(m, n) + + cg = _array_tensor_product(3, M) + assert convert_array_to_matrix(cg) == 3 * M + + # Partial conversion to matrix multiplication: + expr = _array_contraction(_array_tensor_product(M, N, P, Q), (0, 2), (1, 4, 6)) + assert convert_array_to_matrix(expr) == _array_contraction(_array_tensor_product(M.T*N, P, Q), (0, 2, 4)) + + x = MatrixSymbol("x", k, 1) + cg = PermuteDims( + _array_contraction(_array_tensor_product(OneArray(1), x, OneArray(1), DiagMatrix(Identity(1))), + (0, 5)), Permutation(1, 2, 3)) + assert convert_array_to_matrix(cg) == x + + expr = ArrayAdd(M, PermuteDims(M, [1, 0])) + assert convert_array_to_matrix(expr) == M + Transpose(M) + + +def test_arrayexpr_convert_array_to_matrix2(): + cg = _array_contraction(_array_tensor_product(M, N), (1, 3)) + assert convert_array_to_matrix(cg) == M * N.T + + cg = PermuteDims(_array_tensor_product(M, N), Permutation([0, 1, 3, 2])) + assert convert_array_to_matrix(cg) == _array_tensor_product(M, N.T) + + cg = _array_tensor_product(M, PermuteDims(N, Permutation([1, 0]))) + assert convert_array_to_matrix(cg) == _array_tensor_product(M, N.T) + + cg = _array_contraction( + PermuteDims( + _array_tensor_product(M, N, P, Q), Permutation([0, 2, 3, 1, 4, 5, 7, 6])), + (1, 2), (3, 5) + ) + assert convert_array_to_matrix(cg) == _array_tensor_product(M * P.T * Trace(N), Q.T) + + cg = _array_contraction( + _array_tensor_product(M, N, P, PermuteDims(Q, Permutation([1, 0]))), + (1, 5), (2, 3) + ) + assert convert_array_to_matrix(cg) == _array_tensor_product(M * P.T * Trace(N), Q.T) + + cg = _array_tensor_product(M, PermuteDims(N, [1, 0])) + assert convert_array_to_matrix(cg) == _array_tensor_product(M, N.T) + + cg = _array_tensor_product(PermuteDims(M, [1, 0]), PermuteDims(N, [1, 0])) + assert convert_array_to_matrix(cg) == _array_tensor_product(M.T, N.T) + + cg = _array_tensor_product(PermuteDims(N, [1, 0]), PermuteDims(M, [1, 0])) + assert convert_array_to_matrix(cg) == _array_tensor_product(N.T, M.T) + + cg = _array_contraction(M, (0,), (1,)) + assert convert_array_to_matrix(cg) == OneMatrix(1, k)*M*OneMatrix(k, 1) + + cg = _array_contraction(x, (0,), (1,)) + assert convert_array_to_matrix(cg) == OneMatrix(1, k)*x + + Xm = MatrixSymbol("Xm", m, n) + cg = _array_contraction(Xm, (0,), (1,)) + assert convert_array_to_matrix(cg) == OneMatrix(1, m)*Xm*OneMatrix(n, 1) + + +def test_arrayexpr_convert_array_to_diagonalized_vector(): + + # Check matrix recognition over trivial dimensions: + + cg = _array_tensor_product(a, b) + assert convert_array_to_matrix(cg) == a * b.T + + cg = _array_tensor_product(I1, a, b) + assert convert_array_to_matrix(cg) == a * b.T + + # Recognize trace inside a tensor product: + + cg = _array_contraction(_array_tensor_product(A, B, C), (0, 3), (1, 2)) + assert convert_array_to_matrix(cg) == Trace(A * B) * C + + # Transform diagonal operator to contraction: + + cg = _array_diagonal(_array_tensor_product(A, a), (1, 2)) + assert _array_diag2contr_diagmatrix(cg) == _array_contraction(_array_tensor_product(A, OneArray(1), DiagMatrix(a)), (1, 3)) + assert convert_array_to_matrix(cg) == A * DiagMatrix(a) + + cg = _array_diagonal(_array_tensor_product(a, b), (0, 2)) + assert _array_diag2contr_diagmatrix(cg) == _permute_dims( + _array_contraction(_array_tensor_product(DiagMatrix(a), OneArray(1), b), (0, 3)), [1, 2, 0] + ) + assert convert_array_to_matrix(cg) == b.T * DiagMatrix(a) + + cg = _array_diagonal(_array_tensor_product(A, a), (0, 2)) + assert _array_diag2contr_diagmatrix(cg) == _array_contraction(_array_tensor_product(A, OneArray(1), DiagMatrix(a)), (0, 3)) + assert convert_array_to_matrix(cg) == A.T * DiagMatrix(a) + + cg = _array_diagonal(_array_tensor_product(I, x, I1), (0, 2), (3, 5)) + assert _array_diag2contr_diagmatrix(cg) == _array_contraction(_array_tensor_product(I, OneArray(1), I1, DiagMatrix(x)), (0, 5)) + assert convert_array_to_matrix(cg) == DiagMatrix(x) + + cg = _array_diagonal(_array_tensor_product(I, x, A, B), (1, 2), (5, 6)) + assert _array_diag2contr_diagmatrix(cg) == _array_diagonal(_array_contraction(_array_tensor_product(I, OneArray(1), A, B, DiagMatrix(x)), (1, 7)), (5, 6)) + # TODO: this is returning a wrong result: + # convert_array_to_matrix(cg) + + cg = _array_diagonal(_array_tensor_product(I1, a, b), (1, 3, 5)) + assert convert_array_to_matrix(cg) == a*b.T + + cg = _array_diagonal(_array_tensor_product(I1, a, b), (1, 3)) + assert _array_diag2contr_diagmatrix(cg) == _array_contraction(_array_tensor_product(OneArray(1), a, b, I1), (2, 6)) + assert convert_array_to_matrix(cg) == a*b.T + + cg = _array_diagonal(_array_tensor_product(x, I1), (1, 2)) + assert isinstance(cg, ArrayDiagonal) + assert cg.diagonal_indices == ((1, 2),) + assert convert_array_to_matrix(cg) == x + + cg = _array_diagonal(_array_tensor_product(x, I), (0, 2)) + assert _array_diag2contr_diagmatrix(cg) == _array_contraction(_array_tensor_product(OneArray(1), I, DiagMatrix(x)), (1, 3)) + assert convert_array_to_matrix(cg).doit() == DiagMatrix(x) + + raises(ValueError, lambda: _array_diagonal(x, (1,))) + + # Ignore identity matrices with contractions: + + cg = _array_contraction(_array_tensor_product(I, A, I, I), (0, 2), (1, 3), (5, 7)) + assert cg.split_multiple_contractions() == cg + assert convert_array_to_matrix(cg) == Trace(A) * I + + cg = _array_contraction(_array_tensor_product(Trace(A) * I, I, I), (1, 5), (3, 4)) + assert cg.split_multiple_contractions() == cg + assert convert_array_to_matrix(cg).doit() == Trace(A) * I + + # Add DiagMatrix when required: + + cg = _array_contraction(_array_tensor_product(A, a), (1, 2)) + assert cg.split_multiple_contractions() == cg + assert convert_array_to_matrix(cg) == A * a + + cg = _array_contraction(_array_tensor_product(A, a, B), (1, 2, 4)) + assert cg.split_multiple_contractions() == _array_contraction(_array_tensor_product(A, DiagMatrix(a), OneArray(1), B), (1, 2), (3, 5)) + assert convert_array_to_matrix(cg) == A * DiagMatrix(a) * B + + cg = _array_contraction(_array_tensor_product(A, a, B), (0, 2, 4)) + assert cg.split_multiple_contractions() == _array_contraction(_array_tensor_product(A, DiagMatrix(a), OneArray(1), B), (0, 2), (3, 5)) + assert convert_array_to_matrix(cg) == A.T * DiagMatrix(a) * B + + cg = _array_contraction(_array_tensor_product(A, a, b, a.T, B), (0, 2, 4, 7, 9)) + assert cg.split_multiple_contractions() == _array_contraction(_array_tensor_product(A, DiagMatrix(a), OneArray(1), + DiagMatrix(b), OneArray(1), DiagMatrix(a), OneArray(1), B), + (0, 2), (3, 5), (6, 9), (8, 12)) + assert convert_array_to_matrix(cg) == A.T * DiagMatrix(a) * DiagMatrix(b) * DiagMatrix(a) * B.T + + cg = _array_contraction(_array_tensor_product(I1, I1, I1), (1, 2, 4)) + assert cg.split_multiple_contractions() == _array_contraction(_array_tensor_product(I1, I1, OneArray(1), I1), (1, 2), (3, 5)) + assert convert_array_to_matrix(cg) == 1 + + cg = _array_contraction(_array_tensor_product(I, I, I, I, A), (1, 2, 8), (5, 6, 9)) + assert convert_array_to_matrix(cg.split_multiple_contractions()).doit() == A + + cg = _array_contraction(_array_tensor_product(A, a, C, a, B), (1, 2, 4), (5, 6, 8)) + expected = _array_contraction(_array_tensor_product(A, DiagMatrix(a), OneArray(1), C, DiagMatrix(a), OneArray(1), B), (1, 3), (2, 5), (6, 7), (8, 10)) + assert cg.split_multiple_contractions() == expected + assert convert_array_to_matrix(cg) == A * DiagMatrix(a) * C * DiagMatrix(a) * B + + cg = _array_contraction(_array_tensor_product(a, I1, b, I1, (a.T*b).applyfunc(cos)), (1, 2, 8), (5, 6, 9)) + expected = _array_contraction(_array_tensor_product(a, I1, OneArray(1), b, I1, OneArray(1), (a.T*b).applyfunc(cos)), + (1, 3), (2, 10), (6, 8), (7, 11)) + assert cg.split_multiple_contractions().dummy_eq(expected) + assert convert_array_to_matrix(cg).doit().dummy_eq(MatMul(a, (a.T * b).applyfunc(cos), b.T)) + + +def test_arrayexpr_convert_array_contraction_tp_additions(): + a = ArrayAdd( + _array_tensor_product(M, N), + _array_tensor_product(N, M) + ) + tp = _array_tensor_product(P, a, Q) + expr = _array_contraction(tp, (3, 4)) + expected = _array_tensor_product( + P, + ArrayAdd( + _array_contraction(_array_tensor_product(M, N), (1, 2)), + _array_contraction(_array_tensor_product(N, M), (1, 2)), + ), + Q + ) + assert expr == expected + assert convert_array_to_matrix(expr) == _array_tensor_product(P, M * N + N * M, Q) + + expr = _array_contraction(tp, (1, 2), (3, 4), (5, 6)) + result = _array_contraction( + _array_tensor_product( + P, + ArrayAdd( + _array_contraction(_array_tensor_product(M, N), (1, 2)), + _array_contraction(_array_tensor_product(N, M), (1, 2)), + ), + Q + ), (1, 2), (3, 4)) + assert expr == result + assert convert_array_to_matrix(expr) == P * (M * N + N * M) * Q + + +def test_arrayexpr_convert_array_to_implicit_matmul(): + # Trivial dimensions are suppressed, so the result can be expressed in matrix form: + + cg = _array_tensor_product(a, b) + assert convert_array_to_matrix(cg) == a * b.T + + cg = _array_tensor_product(a, b, I) + assert convert_array_to_matrix(cg) == _array_tensor_product(a*b.T, I) + + cg = _array_tensor_product(I, a, b) + assert convert_array_to_matrix(cg) == _array_tensor_product(I, a*b.T) + + cg = _array_tensor_product(a, I, b) + assert convert_array_to_matrix(cg) == _array_tensor_product(a, I, b) + + cg = _array_contraction(_array_tensor_product(I, I), (1, 2)) + assert convert_array_to_matrix(cg) == I + + cg = PermuteDims(_array_tensor_product(I, Identity(1)), [0, 2, 1, 3]) + assert convert_array_to_matrix(cg) == I + + +def test_arrayexpr_convert_array_to_matrix_remove_trivial_dims(): + + # Tensor Product: + assert _remove_trivial_dims(_array_tensor_product(a, b)) == (a * b.T, [1, 3]) + assert _remove_trivial_dims(_array_tensor_product(a.T, b)) == (a * b.T, [0, 3]) + assert _remove_trivial_dims(_array_tensor_product(a, b.T)) == (a * b.T, [1, 2]) + assert _remove_trivial_dims(_array_tensor_product(a.T, b.T)) == (a * b.T, [0, 2]) + + assert _remove_trivial_dims(_array_tensor_product(I, a.T, b.T)) == (_array_tensor_product(I, a * b.T), [2, 4]) + assert _remove_trivial_dims(_array_tensor_product(a.T, I, b.T)) == (_array_tensor_product(a.T, I, b.T), []) + + assert _remove_trivial_dims(_array_tensor_product(a, I)) == (_array_tensor_product(a, I), []) + assert _remove_trivial_dims(_array_tensor_product(I, a)) == (_array_tensor_product(I, a), []) + + assert _remove_trivial_dims(_array_tensor_product(a.T, b.T, c, d)) == ( + _array_tensor_product(a * b.T, c * d.T), [0, 2, 5, 7]) + assert _remove_trivial_dims(_array_tensor_product(a.T, I, b.T, c, d, I)) == ( + _array_tensor_product(a.T, I, b*c.T, d, I), [4, 7]) + + # Addition: + + cg = ArrayAdd(_array_tensor_product(a, b), _array_tensor_product(c, d)) + assert _remove_trivial_dims(cg) == (a * b.T + c * d.T, [1, 3]) + + # Permute Dims: + + cg = PermuteDims(_array_tensor_product(a, b), Permutation(3)(1, 2)) + assert _remove_trivial_dims(cg) == (a * b.T, [2, 3]) + + cg = PermuteDims(_array_tensor_product(a, I, b), Permutation(5)(1, 2, 3, 4)) + assert _remove_trivial_dims(cg) == (cg, []) + + cg = PermuteDims(_array_tensor_product(I, b, a), Permutation(5)(1, 2, 4, 5, 3)) + assert _remove_trivial_dims(cg) == (PermuteDims(_array_tensor_product(I, b * a.T), [0, 2, 3, 1]), [4, 5]) + + # Diagonal: + + cg = _array_diagonal(_array_tensor_product(M, a), (1, 2)) + assert _remove_trivial_dims(cg) == (cg, []) + + # Contraction: + + cg = _array_contraction(_array_tensor_product(M, a), (1, 2)) + assert _remove_trivial_dims(cg) == (cg, []) + + # A few more cases to test the removal and shift of nested removed axes + # with array contractions and array diagonals: + tp = _array_tensor_product( + OneMatrix(1, 1), + M, + x, + OneMatrix(1, 1), + Identity(1), + ) + + expr = _array_contraction(tp, (1, 8)) + rexpr, removed = _remove_trivial_dims(expr) + assert removed == [0, 5, 6, 7] + + expr = _array_contraction(tp, (1, 8), (3, 4)) + rexpr, removed = _remove_trivial_dims(expr) + assert removed == [0, 3, 4, 5] + + expr = _array_diagonal(tp, (1, 8)) + rexpr, removed = _remove_trivial_dims(expr) + assert removed == [0, 5, 6, 7, 8] + + expr = _array_diagonal(tp, (1, 8), (3, 4)) + rexpr, removed = _remove_trivial_dims(expr) + assert removed == [0, 3, 4, 5, 6] + + expr = _array_diagonal(_array_contraction(_array_tensor_product(A, x, I, I1), (1, 2, 5)), (1, 4)) + rexpr, removed = _remove_trivial_dims(expr) + assert removed == [2, 3] + + cg = _array_diagonal(_array_tensor_product(PermuteDims(_array_tensor_product(x, I1), Permutation(1, 2, 3)), (x.T*x).applyfunc(sqrt)), (2, 4), (3, 5)) + rexpr, removed = _remove_trivial_dims(cg) + assert removed == [1, 2] + + # Contractions with identity matrices need to be followed by a permutation + # in order + cg = _array_contraction(_array_tensor_product(A, B, C, M, I), (1, 8)) + ret, removed = _remove_trivial_dims(cg) + assert ret == PermuteDims(_array_tensor_product(A, B, C, M), [0, 2, 3, 4, 5, 6, 7, 1]) + assert removed == [] + + cg = _array_contraction(_array_tensor_product(A, B, C, M, I), (1, 8), (3, 4)) + ret, removed = _remove_trivial_dims(cg) + assert ret == PermuteDims(_array_contraction(_array_tensor_product(A, B, C, M), (3, 4)), [0, 2, 3, 4, 5, 1]) + assert removed == [] + + # Trivial matrices are sometimes inserted into MatMul expressions: + + cg = _array_tensor_product(b*b.T, a.T*a) + ret, removed = _remove_trivial_dims(cg) + assert ret == b*a.T*a*b.T + assert removed == [2, 3] + + Xs = ArraySymbol("X", (3, 2, k)) + cg = _array_tensor_product(M, Xs, b.T*c, a*a.T, b*b.T, c.T*d) + ret, removed = _remove_trivial_dims(cg) + assert ret == _array_tensor_product(M, Xs, a*b.T*c*c.T*d*a.T, b*b.T) + assert removed == [5, 6, 11, 12] + + cg = _array_diagonal(_array_tensor_product(I, I1, x), (1, 4), (3, 5)) + assert _remove_trivial_dims(cg) == (PermuteDims(_array_diagonal(_array_tensor_product(I, x), (1, 2)), Permutation(1, 2)), [1]) + + expr = _array_diagonal(_array_tensor_product(x, I, y), (0, 2)) + assert _remove_trivial_dims(expr) == (PermuteDims(_array_tensor_product(DiagMatrix(x), y), [1, 2, 3, 0]), [0]) + + expr = _array_diagonal(_array_tensor_product(x, I, y), (0, 2), (3, 4)) + assert _remove_trivial_dims(expr) == (expr, []) + + +def test_arrayexpr_convert_array_to_matrix_diag2contraction_diagmatrix(): + cg = _array_diagonal(_array_tensor_product(M, a), (1, 2)) + res = _array_diag2contr_diagmatrix(cg) + assert res.shape == cg.shape + assert res == _array_contraction(_array_tensor_product(M, OneArray(1), DiagMatrix(a)), (1, 3)) + + raises(ValueError, lambda: _array_diagonal(_array_tensor_product(a, M), (1, 2))) + + cg = _array_diagonal(_array_tensor_product(a.T, M), (1, 2)) + res = _array_diag2contr_diagmatrix(cg) + assert res.shape == cg.shape + assert res == _array_contraction(_array_tensor_product(OneArray(1), M, DiagMatrix(a.T)), (1, 4)) + + cg = _array_diagonal(_array_tensor_product(a.T, M, N, b.T), (1, 2), (4, 7)) + res = _array_diag2contr_diagmatrix(cg) + assert res.shape == cg.shape + assert res == _array_contraction( + _array_tensor_product(OneArray(1), M, N, OneArray(1), DiagMatrix(a.T), DiagMatrix(b.T)), (1, 7), (3, 9)) + + cg = _array_diagonal(_array_tensor_product(a, M, N, b.T), (0, 2), (4, 7)) + res = _array_diag2contr_diagmatrix(cg) + assert res.shape == cg.shape + assert res == _array_contraction( + _array_tensor_product(OneArray(1), M, N, OneArray(1), DiagMatrix(a), DiagMatrix(b.T)), (1, 6), (3, 9)) + + cg = _array_diagonal(_array_tensor_product(a, M, N, b.T), (0, 4), (3, 7)) + res = _array_diag2contr_diagmatrix(cg) + assert res.shape == cg.shape + assert res == _array_contraction( + _array_tensor_product(OneArray(1), M, N, OneArray(1), DiagMatrix(a), DiagMatrix(b.T)), (3, 6), (2, 9)) + + I1 = Identity(1) + x = MatrixSymbol("x", k, 1) + A = MatrixSymbol("A", k, k) + cg = _array_diagonal(_array_tensor_product(x, A.T, I1), (0, 2)) + assert _array_diag2contr_diagmatrix(cg).shape == cg.shape + assert _array2matrix(cg).shape == cg.shape + + +def test_arrayexpr_convert_array_to_matrix_support_function(): + + assert _support_function_tp1_recognize([], [2 * k]) == 2 * k + + assert _support_function_tp1_recognize([(1, 2)], [A, 2 * k, B, 3]) == 6 * k * A * B + + assert _support_function_tp1_recognize([(0, 3), (1, 2)], [A, B]) == Trace(A * B) + + assert _support_function_tp1_recognize([(1, 2)], [A, B]) == A * B + assert _support_function_tp1_recognize([(0, 2)], [A, B]) == A.T * B + assert _support_function_tp1_recognize([(1, 3)], [A, B]) == A * B.T + assert _support_function_tp1_recognize([(0, 3)], [A, B]) == A.T * B.T + + assert _support_function_tp1_recognize([(1, 2), (5, 6)], [A, B, C, D]) == _array_tensor_product(A * B, C * D) + assert _support_function_tp1_recognize([(1, 4), (3, 6)], [A, B, C, D]) == PermuteDims( + _array_tensor_product(A * C, B * D), [0, 2, 1, 3]) + + assert _support_function_tp1_recognize([(0, 3), (1, 4)], [A, B, C]) == B * A * C + + assert _support_function_tp1_recognize([(9, 10), (1, 2), (5, 6), (3, 4), (7, 8)], + [X, Y, A, B, C, D]) == X * Y * A * B * C * D + + assert _support_function_tp1_recognize([(9, 10), (1, 2), (5, 6), (3, 4)], + [X, Y, A, B, C, D]) == _array_tensor_product(X * Y * A * B, C * D) + + assert _support_function_tp1_recognize([(1, 7), (3, 8), (4, 11)], [X, Y, A, B, C, D]) == PermuteDims( + _array_tensor_product(X * B.T, Y * C, A.T * D.T), [0, 2, 4, 1, 3, 5] + ) + + assert _support_function_tp1_recognize([(0, 1), (3, 6), (5, 8)], [X, A, B, C, D]) == PermuteDims( + _array_tensor_product(Trace(X) * A * C, B * D), [0, 2, 1, 3]) + + assert _support_function_tp1_recognize([(1, 2), (3, 4), (5, 6), (7, 8)], [A, A, B, C, D]) == A ** 2 * B * C * D + assert _support_function_tp1_recognize([(1, 2), (3, 4), (5, 6), (7, 8)], [X, A, B, C, D]) == X * A * B * C * D + + assert _support_function_tp1_recognize([(1, 6), (3, 8), (5, 10)], [X, Y, A, B, C, D]) == PermuteDims( + _array_tensor_product(X * B, Y * C, A * D), [0, 2, 4, 1, 3, 5] + ) + + assert _support_function_tp1_recognize([(1, 4), (3, 6)], [A, B, C, D]) == PermuteDims( + _array_tensor_product(A * C, B * D), [0, 2, 1, 3]) + + assert _support_function_tp1_recognize([(0, 4), (1, 7), (2, 5), (3, 8)], [X, A, B, C, D]) == C*X.T*B*A*D + + assert _support_function_tp1_recognize([(0, 4), (1, 7), (2, 5), (3, 8)], [X, A, B, C, D]) == C*X.T*B*A*D + + +def test_convert_array_to_hadamard_products(): + + expr = HadamardProduct(M, N) + cg = convert_matrix_to_array(expr) + ret = convert_array_to_matrix(cg) + assert ret == expr + + expr = HadamardProduct(M, N)*P + cg = convert_matrix_to_array(expr) + ret = convert_array_to_matrix(cg) + assert ret == expr + + expr = Q*HadamardProduct(M, N)*P + cg = convert_matrix_to_array(expr) + ret = convert_array_to_matrix(cg) + assert ret == expr + + expr = Q*HadamardProduct(M, N.T)*P + cg = convert_matrix_to_array(expr) + ret = convert_array_to_matrix(cg) + assert ret == expr + + expr = HadamardProduct(M, N)*HadamardProduct(Q, P) + cg = convert_matrix_to_array(expr) + ret = convert_array_to_matrix(cg) + assert expr == ret + + expr = P.T*HadamardProduct(M, N)*HadamardProduct(Q, P) + cg = convert_matrix_to_array(expr) + ret = convert_array_to_matrix(cg) + assert expr == ret + + # ArrayDiagonal should be converted + cg = _array_diagonal(_array_tensor_product(M, N, Q), (1, 3), (0, 2, 4)) + ret = convert_array_to_matrix(cg) + expected = PermuteDims(_array_diagonal(_array_tensor_product(HadamardProduct(M.T, N.T), Q), (1, 2)), [1, 0, 2]) + assert expected == ret + + # Special case that should return the same expression: + cg = _array_diagonal(_array_tensor_product(HadamardProduct(M, N), Q), (0, 2)) + ret = convert_array_to_matrix(cg) + assert ret == cg + + # Hadamard products with traces: + + expr = Trace(HadamardProduct(M, N)) + cg = convert_matrix_to_array(expr) + ret = convert_array_to_matrix(cg) + assert ret == Trace(HadamardProduct(M.T, N.T)) + + expr = Trace(A*HadamardProduct(M, N)) + cg = convert_matrix_to_array(expr) + ret = convert_array_to_matrix(cg) + assert ret == Trace(HadamardProduct(M, N)*A) + + expr = Trace(HadamardProduct(A, M)*N) + cg = convert_matrix_to_array(expr) + ret = convert_array_to_matrix(cg) + assert ret == Trace(HadamardProduct(M.T, N)*A) + + # These should not be converted into Hadamard products: + + cg = _array_diagonal(_array_tensor_product(M, N), (0, 1, 2, 3)) + ret = convert_array_to_matrix(cg) + assert ret == cg + + cg = _array_diagonal(_array_tensor_product(A), (0, 1)) + ret = convert_array_to_matrix(cg) + assert ret == cg + + cg = _array_diagonal(_array_tensor_product(M, N, P), (0, 2, 4), (1, 3, 5)) + assert convert_array_to_matrix(cg) == HadamardProduct(M, N, P) + + cg = _array_diagonal(_array_tensor_product(M, N, P), (0, 3, 4), (1, 2, 5)) + assert convert_array_to_matrix(cg) == HadamardProduct(M, P, N.T) + + cg = _array_diagonal(_array_tensor_product(I, I1, x), (1, 4), (3, 5)) + assert convert_array_to_matrix(cg) == DiagMatrix(x) + + +def test_identify_removable_identity_matrices(): + + D = DiagonalMatrix(MatrixSymbol("D", k, k)) + + cg = _array_contraction(_array_tensor_product(A, B, I), (1, 2, 4, 5)) + expected = _array_contraction(_array_tensor_product(A, B), (1, 2)) + assert identify_removable_identity_matrices(cg) == expected + + cg = _array_contraction(_array_tensor_product(A, B, C, I), (1, 3, 5, 6, 7)) + expected = _array_contraction(_array_tensor_product(A, B, C), (1, 3, 5)) + assert identify_removable_identity_matrices(cg) == expected + + # Tests with diagonal matrices: + + cg = _array_contraction(_array_tensor_product(A, B, D), (1, 2, 4, 5)) + ret = identify_removable_identity_matrices(cg) + expected = _array_contraction(_array_tensor_product(A, B, D), (1, 4), (2, 5)) + assert ret == expected + + cg = _array_contraction(_array_tensor_product(A, B, D, M, N), (1, 2, 4, 5, 6, 8)) + ret = identify_removable_identity_matrices(cg) + assert ret == cg + + +def test_combine_removed(): + + assert _combine_removed(6, [0, 1, 2], [0, 1, 2]) == [0, 1, 2, 3, 4, 5] + assert _combine_removed(8, [2, 5], [1, 3, 4]) == [1, 2, 4, 5, 6] + assert _combine_removed(8, [7], []) == [7] + + +def test_array_contraction_to_diagonal_multiple_identities(): + + expr = _array_contraction(_array_tensor_product(A, B, I, C), (1, 2, 4), (5, 6)) + assert _array_contraction_to_diagonal_multiple_identity(expr) == (expr, []) + assert convert_array_to_matrix(expr) == _array_contraction(_array_tensor_product(A, B, C), (1, 2, 4)) + + expr = _array_contraction(_array_tensor_product(A, I, I), (1, 2, 4)) + assert _array_contraction_to_diagonal_multiple_identity(expr) == (A, [2]) + assert convert_array_to_matrix(expr) == A + + expr = _array_contraction(_array_tensor_product(A, I, I, B), (1, 2, 4), (3, 6)) + assert _array_contraction_to_diagonal_multiple_identity(expr) == (expr, []) + + expr = _array_contraction(_array_tensor_product(A, I, I, B), (1, 2, 3, 4, 6)) + assert _array_contraction_to_diagonal_multiple_identity(expr) == (expr, []) + + +def test_convert_array_element_to_matrix(): + + expr = ArrayElement(M, (i, j)) + assert convert_array_to_matrix(expr) == MatrixElement(M, i, j) + + expr = ArrayElement(_array_contraction(_array_tensor_product(M, N), (1, 3)), (i, j)) + assert convert_array_to_matrix(expr) == MatrixElement(M*N.T, i, j) + + expr = ArrayElement(_array_tensor_product(M, N), (i, j, m, n)) + assert convert_array_to_matrix(expr) == expr + + +def test_convert_array_elementwise_function_to_matrix(): + + d = Dummy("d") + + expr = ArrayElementwiseApplyFunc(Lambda(d, sin(d)), x.T*y) + assert convert_array_to_matrix(expr) == sin(x.T*y) + + expr = ArrayElementwiseApplyFunc(Lambda(d, d**2), x.T*y) + assert convert_array_to_matrix(expr) == (x.T*y)**2 + + expr = ArrayElementwiseApplyFunc(Lambda(d, sin(d)), x) + assert convert_array_to_matrix(expr).dummy_eq(x.applyfunc(sin)) + + expr = ArrayElementwiseApplyFunc(Lambda(d, 1 / (2 * sqrt(d))), x) + assert convert_array_to_matrix(expr) == S.Half * HadamardPower(x, -S.Half) + + +def test_array2matrix(): + # See issue https://github.com/sympy/sympy/pull/22877 + expr = PermuteDims(ArrayContraction(ArrayTensorProduct(x, I, I1, x), (0, 3), (1, 7)), Permutation(2, 3)) + expected = PermuteDims(ArrayTensorProduct(x*x.T, I1), Permutation(3)(1, 2)) + assert _array2matrix(expr) == expected + + +def test_recognize_broadcasting(): + expr = ArrayTensorProduct(x.T*x, A) + assert _remove_trivial_dims(expr) == (KroneckerProduct(x.T*x, A), [0, 1]) + + expr = ArrayTensorProduct(A, x.T*x) + assert _remove_trivial_dims(expr) == (KroneckerProduct(A, x.T*x), [2, 3]) + + expr = ArrayTensorProduct(A, B, x.T*x, C) + assert _remove_trivial_dims(expr) == (ArrayTensorProduct(A, KroneckerProduct(B, x.T*x), C), [4, 5]) + + # Always prefer matrix multiplication to Kronecker product, if possible: + expr = ArrayTensorProduct(a, b, x.T*x) + assert _remove_trivial_dims(expr) == (a*x.T*x*b.T, [1, 3, 4, 5]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_convert_indexed_to_array.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_convert_indexed_to_array.py new file mode 100644 index 0000000000000000000000000000000000000000..258062eadeca041ae3c864dabeefd5165f1cef11 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_convert_indexed_to_array.py @@ -0,0 +1,205 @@ +from sympy import tanh +from sympy.concrete.summations import Sum +from sympy.core.symbol import symbols +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.matrices.expressions.special import Identity +from sympy.tensor.array.expressions import ArrayElementwiseApplyFunc +from sympy.tensor.indexed import IndexedBase +from sympy.combinatorics import Permutation +from sympy.tensor.array.expressions.array_expressions import ArrayContraction, ArrayTensorProduct, \ + ArrayDiagonal, ArrayAdd, PermuteDims, ArrayElement, _array_tensor_product, _array_contraction, _array_diagonal, \ + _array_add, _permute_dims, ArraySymbol, OneArray +from sympy.tensor.array.expressions.from_array_to_matrix import convert_array_to_matrix +from sympy.tensor.array.expressions.from_indexed_to_array import convert_indexed_to_array, _convert_indexed_to_array +from sympy.testing.pytest import raises + + +A, B = symbols("A B", cls=IndexedBase) +i, j, k, l, m, n = symbols("i j k l m n") +d0, d1, d2, d3 = symbols("d0:4") + +I = Identity(k) + +M = MatrixSymbol("M", k, k) +N = MatrixSymbol("N", k, k) +P = MatrixSymbol("P", k, k) +Q = MatrixSymbol("Q", k, k) + +a = MatrixSymbol("a", k, 1) +b = MatrixSymbol("b", k, 1) +c = MatrixSymbol("c", k, 1) +d = MatrixSymbol("d", k, 1) + + +def test_arrayexpr_convert_index_to_array_support_function(): + expr = M[i, j] + assert _convert_indexed_to_array(expr) == (M, (i, j)) + expr = M[i, j]*N[k, l] + assert _convert_indexed_to_array(expr) == (ArrayTensorProduct(M, N), (i, j, k, l)) + expr = M[i, j]*N[j, k] + assert _convert_indexed_to_array(expr) == (ArrayDiagonal(ArrayTensorProduct(M, N), (1, 2)), (i, k, j)) + expr = Sum(M[i, j]*N[j, k], (j, 0, k-1)) + assert _convert_indexed_to_array(expr) == (ArrayContraction(ArrayTensorProduct(M, N), (1, 2)), (i, k)) + expr = M[i, j] + N[i, j] + assert _convert_indexed_to_array(expr) == (ArrayAdd(M, N), (i, j)) + expr = M[i, j] + N[j, i] + assert _convert_indexed_to_array(expr) == (ArrayAdd(M, PermuteDims(N, Permutation([1, 0]))), (i, j)) + expr = M[i, j] + M[j, i] + assert _convert_indexed_to_array(expr) == (ArrayAdd(M, PermuteDims(M, Permutation([1, 0]))), (i, j)) + expr = (M*N*P)[i, j] + assert _convert_indexed_to_array(expr) == (_array_contraction(ArrayTensorProduct(M, N, P), (1, 2), (3, 4)), (i, j)) + expr = expr.function # Disregard summation in previous expression + ret1, ret2 = _convert_indexed_to_array(expr) + assert ret1 == ArrayDiagonal(ArrayTensorProduct(M, N, P), (1, 2), (3, 4)) + assert str(ret2) == "(i, j, _i_1, _i_2)" + expr = KroneckerDelta(i, j)*M[i, k] + assert _convert_indexed_to_array(expr) == (M, ({i, j}, k)) + expr = KroneckerDelta(i, j)*KroneckerDelta(j, k)*M[i, l] + assert _convert_indexed_to_array(expr) == (M, ({i, j, k}, l)) + expr = KroneckerDelta(j, k)*(M[i, j]*N[k, l] + N[i, j]*M[k, l]) + assert _convert_indexed_to_array(expr) == (_array_diagonal(_array_add( + ArrayTensorProduct(M, N), + _permute_dims(ArrayTensorProduct(M, N), Permutation(0, 2)(1, 3)) + ), (1, 2)), (i, l, frozenset({j, k}))) + expr = KroneckerDelta(j, m)*KroneckerDelta(m, k)*(M[i, j]*N[k, l] + N[i, j]*M[k, l]) + assert _convert_indexed_to_array(expr) == (_array_diagonal(_array_add( + ArrayTensorProduct(M, N), + _permute_dims(ArrayTensorProduct(M, N), Permutation(0, 2)(1, 3)) + ), (1, 2)), (i, l, frozenset({j, m, k}))) + expr = KroneckerDelta(i, j)*KroneckerDelta(j, k)*KroneckerDelta(k,m)*M[i, 0]*KroneckerDelta(m, n) + assert _convert_indexed_to_array(expr) == (M, ({i, j, k, m, n}, 0)) + expr = M[i, i] + assert _convert_indexed_to_array(expr) == (ArrayDiagonal(M, (0, 1)), (i,)) + + +def test_arrayexpr_convert_indexed_to_array_expression(): + + s = Sum(A[i]*B[i], (i, 0, 3)) + cg = convert_indexed_to_array(s) + assert cg == ArrayContraction(ArrayTensorProduct(A, B), (0, 1)) + + expr = M*N + result = ArrayContraction(ArrayTensorProduct(M, N), (1, 2)) + elem = expr[i, j] + assert convert_indexed_to_array(elem) == result + + expr = M*N*M + elem = expr[i, j] + result = _array_contraction(_array_tensor_product(M, M, N), (1, 4), (2, 5)) + cg = convert_indexed_to_array(elem) + assert cg == result + + cg = convert_indexed_to_array((M * N * P)[i, j]) + assert cg == _array_contraction(ArrayTensorProduct(M, N, P), (1, 2), (3, 4)) + + cg = convert_indexed_to_array((M * N.T * P)[i, j]) + assert cg == _array_contraction(ArrayTensorProduct(M, N, P), (1, 3), (2, 4)) + + expr = -2*M*N + elem = expr[i, j] + cg = convert_indexed_to_array(elem) + assert cg == ArrayContraction(ArrayTensorProduct(-2, M, N), (1, 2)) + + +def test_arrayexpr_convert_array_element_to_array_expression(): + A = ArraySymbol("A", (k,)) + B = ArraySymbol("B", (k,)) + + s = Sum(A[i]*B[i], (i, 0, k-1)) + cg = convert_indexed_to_array(s) + assert cg == ArrayContraction(ArrayTensorProduct(A, B), (0, 1)) + + s = A[i]*B[i] + cg = convert_indexed_to_array(s) + assert cg == ArrayDiagonal(ArrayTensorProduct(A, B), (0, 1)) + + s = A[i]*B[j] + cg = convert_indexed_to_array(s, [i, j]) + assert cg == ArrayTensorProduct(A, B) + cg = convert_indexed_to_array(s, [j, i]) + assert cg == ArrayTensorProduct(B, A) + + s = tanh(A[i]*B[j]) + cg = convert_indexed_to_array(s, [i, j]) + assert cg.dummy_eq(ArrayElementwiseApplyFunc(tanh, ArrayTensorProduct(A, B))) + + +def test_arrayexpr_convert_indexed_to_array_and_back_to_matrix(): + + expr = a.T*b + elem = expr[0, 0] + cg = convert_indexed_to_array(elem) + assert cg == ArrayElement(ArrayContraction(ArrayTensorProduct(a, b), (0, 2)), [0, 0]) + + expr = M[i,j] + N[i,j] + p1, p2 = _convert_indexed_to_array(expr) + assert convert_array_to_matrix(p1) == M + N + + expr = M[i,j] + N[j,i] + p1, p2 = _convert_indexed_to_array(expr) + assert convert_array_to_matrix(p1) == M + N.T + + expr = M[i,j]*N[k,l] + N[i,j]*M[k,l] + p1, p2 = _convert_indexed_to_array(expr) + assert convert_array_to_matrix(p1) == ArrayAdd( + ArrayTensorProduct(M, N), + ArrayTensorProduct(N, M)) + + expr = (M*N*P)[i, j] + p1, p2 = _convert_indexed_to_array(expr) + assert convert_array_to_matrix(p1) == M * N * P + + expr = Sum(M[i,j]*(N*P)[j,m], (j, 0, k-1)) + p1, p2 = _convert_indexed_to_array(expr) + assert convert_array_to_matrix(p1) == M * N * P + + expr = Sum((P[j, m] + P[m, j])*(M[i,j]*N[m,n] + N[i,j]*M[m,n]), (j, 0, k-1), (m, 0, k-1)) + p1, p2 = _convert_indexed_to_array(expr) + assert convert_array_to_matrix(p1) == M * P * N + M * P.T * N + N * P * M + N * P.T * M + + +def test_arrayexpr_convert_indexed_to_array_out_of_bounds(): + + expr = Sum(M[i, i], (i, 0, 4)) + raises(ValueError, lambda: convert_indexed_to_array(expr)) + expr = Sum(M[i, i], (i, 0, k)) + raises(ValueError, lambda: convert_indexed_to_array(expr)) + expr = Sum(M[i, i], (i, 1, k-1)) + raises(ValueError, lambda: convert_indexed_to_array(expr)) + + expr = Sum(M[i, j]*N[j,m], (j, 0, 4)) + raises(ValueError, lambda: convert_indexed_to_array(expr)) + expr = Sum(M[i, j]*N[j,m], (j, 0, k)) + raises(ValueError, lambda: convert_indexed_to_array(expr)) + expr = Sum(M[i, j]*N[j,m], (j, 1, k-1)) + raises(ValueError, lambda: convert_indexed_to_array(expr)) + + +def test_arrayexpr_convert_indexed_to_array_broadcast(): + A = ArraySymbol("A", (3, 3)) + B = ArraySymbol("B", (3, 3)) + + expr = A[i, j] + B[k, l] + O2 = OneArray(3, 3) + expected = ArrayAdd(ArrayTensorProduct(A, O2), ArrayTensorProduct(O2, B)) + assert convert_indexed_to_array(expr) == expected + assert convert_indexed_to_array(expr, [i, j, k, l]) == expected + assert convert_indexed_to_array(expr, [l, k, i, j]) == ArrayAdd(PermuteDims(ArrayTensorProduct(O2, A), [1, 0, 2, 3]), PermuteDims(ArrayTensorProduct(B, O2), [1, 0, 2, 3])) + + expr = A[i, j] + B[j, k] + O1 = OneArray(3) + assert convert_indexed_to_array(expr, [i, j, k]) == ArrayAdd(ArrayTensorProduct(A, O1), ArrayTensorProduct(O1, B)) + + C = ArraySymbol("C", (d0, d1)) + D = ArraySymbol("D", (d3, d1)) + + expr = C[i, j] + D[k, j] + assert convert_indexed_to_array(expr, [i, j, k]) == ArrayAdd(ArrayTensorProduct(C, OneArray(d3)), PermuteDims(ArrayTensorProduct(OneArray(d0), D), [0, 2, 1])) + + X = ArraySymbol("X", (5, 3)) + + expr = X[i, n] - X[j, n] + assert convert_indexed_to_array(expr, [i, j, n]) == ArrayAdd(ArrayTensorProduct(-1, OneArray(5), X), PermuteDims(ArrayTensorProduct(X, OneArray(5)), [0, 2, 1])) + + raises(ValueError, lambda: convert_indexed_to_array(C[i, j] + D[i, j])) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_convert_matrix_to_array.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_convert_matrix_to_array.py new file mode 100644 index 0000000000000000000000000000000000000000..142585882588df6aa0e4648d9d8881ea755f42a0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_convert_matrix_to_array.py @@ -0,0 +1,128 @@ +from sympy import Lambda, KroneckerProduct +from sympy.core.symbol import symbols, Dummy +from sympy.matrices.expressions.hadamard import (HadamardPower, HadamardProduct) +from sympy.matrices.expressions.inverse import Inverse +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.matrices.expressions.matpow import MatPow +from sympy.matrices.expressions.special import Identity +from sympy.matrices.expressions.trace import Trace +from sympy.matrices.expressions.transpose import Transpose +from sympy.tensor.array.expressions.array_expressions import ArrayTensorProduct, ArrayContraction, \ + PermuteDims, ArrayDiagonal, ArrayElementwiseApplyFunc, _array_contraction, _array_tensor_product, Reshape +from sympy.tensor.array.expressions.from_array_to_matrix import convert_array_to_matrix +from sympy.tensor.array.expressions.from_matrix_to_array import convert_matrix_to_array + +i, j, k, l, m, n = symbols("i j k l m n") + +I = Identity(k) + +M = MatrixSymbol("M", k, k) +N = MatrixSymbol("N", k, k) +P = MatrixSymbol("P", k, k) +Q = MatrixSymbol("Q", k, k) + +A = MatrixSymbol("A", k, k) +B = MatrixSymbol("B", k, k) +C = MatrixSymbol("C", k, k) +D = MatrixSymbol("D", k, k) + +X = MatrixSymbol("X", k, k) +Y = MatrixSymbol("Y", k, k) + +a = MatrixSymbol("a", k, 1) +b = MatrixSymbol("b", k, 1) +c = MatrixSymbol("c", k, 1) +d = MatrixSymbol("d", k, 1) + + +def test_arrayexpr_convert_matrix_to_array(): + + expr = M*N + result = ArrayContraction(ArrayTensorProduct(M, N), (1, 2)) + assert convert_matrix_to_array(expr) == result + + expr = M*N*M + result = _array_contraction(ArrayTensorProduct(M, N, M), (1, 2), (3, 4)) + assert convert_matrix_to_array(expr) == result + + expr = Transpose(M) + assert convert_matrix_to_array(expr) == PermuteDims(M, [1, 0]) + + expr = M*Transpose(N) + assert convert_matrix_to_array(expr) == _array_contraction(_array_tensor_product(M, PermuteDims(N, [1, 0])), (1, 2)) + + expr = 3*M*N + res = convert_matrix_to_array(expr) + rexpr = convert_array_to_matrix(res) + assert expr == rexpr + + expr = 3*M + N*M.T*M + 4*k*N + res = convert_matrix_to_array(expr) + rexpr = convert_array_to_matrix(res) + assert expr == rexpr + + expr = Inverse(M)*N + rexpr = convert_array_to_matrix(convert_matrix_to_array(expr)) + assert expr == rexpr + + expr = M**2 + rexpr = convert_array_to_matrix(convert_matrix_to_array(expr)) + assert expr == rexpr + + expr = M*(2*N + 3*M) + res = convert_matrix_to_array(expr) + rexpr = convert_array_to_matrix(res) + assert expr == rexpr + + expr = Trace(M) + result = ArrayContraction(M, (0, 1)) + assert convert_matrix_to_array(expr) == result + + expr = 3*Trace(M) + result = ArrayContraction(ArrayTensorProduct(3, M), (0, 1)) + assert convert_matrix_to_array(expr) == result + + expr = 3*Trace(Trace(M) * M) + result = ArrayContraction(ArrayTensorProduct(3, M, M), (0, 1), (2, 3)) + assert convert_matrix_to_array(expr) == result + + expr = 3*Trace(M)**2 + result = ArrayContraction(ArrayTensorProduct(3, M, M), (0, 1), (2, 3)) + assert convert_matrix_to_array(expr) == result + + expr = HadamardProduct(M, N) + result = ArrayDiagonal(ArrayTensorProduct(M, N), (0, 2), (1, 3)) + assert convert_matrix_to_array(expr) == result + + expr = HadamardProduct(M*N, N*M) + result = ArrayDiagonal(ArrayContraction(ArrayTensorProduct(M, N, N, M), (1, 2), (5, 6)), (0, 2), (1, 3)) + assert convert_matrix_to_array(expr) == result + + expr = HadamardPower(M, 2) + result = ArrayDiagonal(ArrayTensorProduct(M, M), (0, 2), (1, 3)) + assert convert_matrix_to_array(expr) == result + + expr = HadamardPower(M*N, 2) + result = ArrayDiagonal(ArrayContraction(ArrayTensorProduct(M, N, M, N), (1, 2), (5, 6)), (0, 2), (1, 3)) + assert convert_matrix_to_array(expr) == result + + expr = HadamardPower(M, n) + d0 = Dummy("d0") + result = ArrayElementwiseApplyFunc(Lambda(d0, d0**n), M) + assert convert_matrix_to_array(expr).dummy_eq(result) + + expr = M**2 + assert isinstance(expr, MatPow) + assert convert_matrix_to_array(expr) == ArrayContraction(ArrayTensorProduct(M, M), (1, 2)) + + expr = a.T*b + cg = convert_matrix_to_array(expr) + assert cg == ArrayContraction(ArrayTensorProduct(a, b), (0, 2)) + + expr = KroneckerProduct(A, B) + cg = convert_matrix_to_array(expr) + assert cg == Reshape(PermuteDims(ArrayTensorProduct(A, B), [0, 2, 1, 3]), (k**2, k**2)) + + expr = KroneckerProduct(A, B, C, D) + cg = convert_matrix_to_array(expr) + assert cg == Reshape(PermuteDims(ArrayTensorProduct(A, B, C, D), [0, 2, 4, 6, 1, 3, 5, 7]), (k**4, k**4)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_deprecated_conv_modules.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_deprecated_conv_modules.py new file mode 100644 index 0000000000000000000000000000000000000000..b41b6105410a308e7774fce760b235497d0303bb --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/tests/test_deprecated_conv_modules.py @@ -0,0 +1,22 @@ +from sympy import MatrixSymbol, symbols, Sum +from sympy.tensor.array.expressions import conv_array_to_indexed, from_array_to_indexed, ArrayTensorProduct, \ + ArrayContraction, conv_array_to_matrix, from_array_to_matrix, conv_matrix_to_array, from_matrix_to_array, \ + conv_indexed_to_array, from_indexed_to_array +from sympy.testing.pytest import warns +from sympy.utilities.exceptions import SymPyDeprecationWarning + + +def test_deprecated_conv_module_results(): + + M = MatrixSymbol("M", 3, 3) + N = MatrixSymbol("N", 3, 3) + i, j, d = symbols("i j d") + + x = ArrayContraction(ArrayTensorProduct(M, N), (1, 2)) + y = Sum(M[i, d]*N[d, j], (d, 0, 2)) + + with warns(SymPyDeprecationWarning, test_stacklevel=False): + assert conv_array_to_indexed.convert_array_to_indexed(x, [i, j]).dummy_eq(from_array_to_indexed.convert_array_to_indexed(x, [i, j])) + assert conv_array_to_matrix.convert_array_to_matrix(x) == from_array_to_matrix.convert_array_to_matrix(x) + assert conv_matrix_to_array.convert_matrix_to_array(M*N) == from_matrix_to_array.convert_matrix_to_array(M*N) + assert conv_indexed_to_array.convert_indexed_to_array(y) == from_indexed_to_array.convert_indexed_to_array(y) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/utils.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..e55c0e6ed47cdc9ff1c24cc92f006998aeb86822 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/expressions/utils.py @@ -0,0 +1,123 @@ +import bisect +from collections import defaultdict + +from sympy.combinatorics import Permutation +from sympy.core.containers import Tuple +from sympy.core.numbers import Integer + + +def _get_mapping_from_subranks(subranks): + mapping = {} + counter = 0 + for i, rank in enumerate(subranks): + for j in range(rank): + mapping[counter] = (i, j) + counter += 1 + return mapping + + +def _get_contraction_links(args, subranks, *contraction_indices): + mapping = _get_mapping_from_subranks(subranks) + contraction_tuples = [[mapping[j] for j in i] for i in contraction_indices] + dlinks = defaultdict(dict) + for links in contraction_tuples: + if len(links) == 2: + (arg1, pos1), (arg2, pos2) = links + dlinks[arg1][pos1] = (arg2, pos2) + dlinks[arg2][pos2] = (arg1, pos1) + continue + + return args, dict(dlinks) + + +def _sort_contraction_indices(pairing_indices): + pairing_indices = [Tuple(*sorted(i)) for i in pairing_indices] + pairing_indices.sort(key=lambda x: min(x)) + return pairing_indices + + +def _get_diagonal_indices(flattened_indices): + axes_contraction = defaultdict(list) + for i, ind in enumerate(flattened_indices): + if isinstance(ind, (int, Integer)): + # If the indices is a number, there can be no diagonal operation: + continue + axes_contraction[ind].append(i) + axes_contraction = {k: v for k, v in axes_contraction.items() if len(v) > 1} + # Put the diagonalized indices at the end: + ret_indices = [i for i in flattened_indices if i not in axes_contraction] + diag_indices = list(axes_contraction) + diag_indices.sort(key=lambda x: flattened_indices.index(x)) + diagonal_indices = [tuple(axes_contraction[i]) for i in diag_indices] + ret_indices += diag_indices + ret_indices = tuple(ret_indices) + return diagonal_indices, ret_indices + + +def _get_argindex(subindices, ind): + for i, sind in enumerate(subindices): + if ind == sind: + return i + if isinstance(sind, (set, frozenset)) and ind in sind: + return i + raise IndexError("%s not found in %s" % (ind, subindices)) + + +def _apply_recursively_over_nested_lists(func, arr): + if isinstance(arr, (tuple, list, Tuple)): + return tuple(_apply_recursively_over_nested_lists(func, i) for i in arr) + elif isinstance(arr, Tuple): + return Tuple.fromiter(_apply_recursively_over_nested_lists(func, i) for i in arr) + else: + return func(arr) + + +def _build_push_indices_up_func_transformation(flattened_contraction_indices): + shifts = {0: 0} + i = 0 + cumulative = 0 + while i < len(flattened_contraction_indices): + j = 1 + while i+j < len(flattened_contraction_indices): + if flattened_contraction_indices[i] + j != flattened_contraction_indices[i+j]: + break + j += 1 + cumulative += j + shifts[flattened_contraction_indices[i]] = cumulative + i += j + shift_keys = sorted(shifts.keys()) + + def func(idx): + return shifts[shift_keys[bisect.bisect_right(shift_keys, idx)-1]] + + def transform(j): + if j in flattened_contraction_indices: + return None + else: + return j - func(j) + + return transform + + +def _build_push_indices_down_func_transformation(flattened_contraction_indices): + N = flattened_contraction_indices[-1]+2 + + shifts = [i for i in range(N) if i not in flattened_contraction_indices] + + def transform(j): + if j < len(shifts): + return shifts[j] + else: + return j + shifts[-1] - len(shifts) + 1 + + return transform + + +def _apply_permutation_to_list(perm: Permutation, target_list: list): + """ + Permute a list according to the given permutation. + """ + new_list = [None for i in range(perm.size)] + for i, e in enumerate(target_list): + new_list[perm(i)] = e + return new_list diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/mutable_ndim_array.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/mutable_ndim_array.py new file mode 100644 index 0000000000000000000000000000000000000000..e1eaaf7241bc3b4a48234178d18da3aa5736e189 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/mutable_ndim_array.py @@ -0,0 +1,13 @@ +from sympy.tensor.array.ndim_array import NDimArray + + +class MutableNDimArray(NDimArray): + + def as_immutable(self): + raise NotImplementedError("abstract method") + + def as_mutable(self): + return self + + def _sympy_(self): + return self.as_immutable() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/ndim_array.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/ndim_array.py new file mode 100644 index 0000000000000000000000000000000000000000..2b9a857b8cfd9ee46646c46f274636d6b9962b6e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/ndim_array.py @@ -0,0 +1,601 @@ +from sympy.core.basic import Basic +from sympy.core.containers import (Dict, Tuple) +from sympy.core.expr import Expr +from sympy.core.kind import Kind, NumberKind, UndefinedKind +from sympy.core.numbers import Integer +from sympy.core.singleton import S +from sympy.core.sympify import sympify +from sympy.external.gmpy import SYMPY_INTS +from sympy.printing.defaults import Printable + +import itertools +from collections.abc import Iterable + + +class ArrayKind(Kind): + """ + Kind for N-dimensional array in SymPy. + + This kind represents the multidimensional array that algebraic + operations are defined. Basic class for this kind is ``NDimArray``, + but any expression representing the array can have this. + + Parameters + ========== + + element_kind : Kind + Kind of the element. Default is :obj:NumberKind ``, + which means that the array contains only numbers. + + Examples + ======== + + Any instance of array class has ``ArrayKind``. + + >>> from sympy import NDimArray + >>> NDimArray([1,2,3]).kind + ArrayKind(NumberKind) + + Although expressions representing an array may be not instance of + array class, it will have ``ArrayKind`` as well. + + >>> from sympy import Integral + >>> from sympy.tensor.array import NDimArray + >>> from sympy.abc import x + >>> intA = Integral(NDimArray([1,2,3]), x) + >>> isinstance(intA, NDimArray) + False + >>> intA.kind + ArrayKind(NumberKind) + + Use ``isinstance()`` to check for ``ArrayKind` without specifying + the element kind. Use ``is`` with specifying the element kind. + + >>> from sympy.tensor.array import ArrayKind + >>> from sympy.core import NumberKind + >>> boolA = NDimArray([True, False]) + >>> isinstance(boolA.kind, ArrayKind) + True + >>> boolA.kind is ArrayKind(NumberKind) + False + + See Also + ======== + + shape : Function to return the shape of objects with ``MatrixKind``. + + """ + def __new__(cls, element_kind=NumberKind): + obj = super().__new__(cls, element_kind) + obj.element_kind = element_kind + return obj + + def __repr__(self): + return "ArrayKind(%s)" % self.element_kind + + @classmethod + def _union(cls, kinds) -> 'ArrayKind': + elem_kinds = {e.kind for e in kinds} + if len(elem_kinds) == 1: + elemkind, = elem_kinds + else: + elemkind = UndefinedKind + return ArrayKind(elemkind) + + +class NDimArray(Printable): + """N-dimensional array. + + Examples + ======== + + Create an N-dim array of zeros: + + >>> from sympy import MutableDenseNDimArray + >>> a = MutableDenseNDimArray.zeros(2, 3, 4) + >>> a + [[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]] + + Create an N-dim array from a list; + + >>> a = MutableDenseNDimArray([[2, 3], [4, 5]]) + >>> a + [[2, 3], [4, 5]] + + >>> b = MutableDenseNDimArray([[[1, 2], [3, 4], [5, 6]], [[7, 8], [9, 10], [11, 12]]]) + >>> b + [[[1, 2], [3, 4], [5, 6]], [[7, 8], [9, 10], [11, 12]]] + + Create an N-dim array from a flat list with dimension shape: + + >>> a = MutableDenseNDimArray([1, 2, 3, 4, 5, 6], (2, 3)) + >>> a + [[1, 2, 3], [4, 5, 6]] + + Create an N-dim array from a matrix: + + >>> from sympy import Matrix + >>> a = Matrix([[1,2],[3,4]]) + >>> a + Matrix([ + [1, 2], + [3, 4]]) + >>> b = MutableDenseNDimArray(a) + >>> b + [[1, 2], [3, 4]] + + Arithmetic operations on N-dim arrays + + >>> a = MutableDenseNDimArray([1, 1, 1, 1], (2, 2)) + >>> b = MutableDenseNDimArray([4, 4, 4, 4], (2, 2)) + >>> c = a + b + >>> c + [[5, 5], [5, 5]] + >>> a - b + [[-3, -3], [-3, -3]] + + """ + + _diff_wrt = True + is_scalar = False + + def __new__(cls, iterable, shape=None, **kwargs): + from sympy.tensor.array import ImmutableDenseNDimArray + return ImmutableDenseNDimArray(iterable, shape, **kwargs) + + def __getitem__(self, index): + raise NotImplementedError("A subclass of NDimArray should implement __getitem__") + + def _parse_index(self, index): + if isinstance(index, (SYMPY_INTS, Integer)): + if index >= self._loop_size: + raise ValueError("Only a tuple index is accepted") + return index + + if self._loop_size == 0: + raise ValueError("Index not valid with an empty array") + + if len(index) != self._rank: + raise ValueError('Wrong number of array axes') + + real_index = 0 + # check if input index can exist in current indexing + for i in range(self._rank): + if (index[i] >= self.shape[i]) or (index[i] < -self.shape[i]): + raise ValueError('Index ' + str(index) + ' out of border') + if index[i] < 0: + real_index += 1 + real_index = real_index*self.shape[i] + index[i] + + return real_index + + def _get_tuple_index(self, integer_index): + index = [] + for sh in reversed(self.shape): + index.append(integer_index % sh) + integer_index //= sh + index.reverse() + return tuple(index) + + def _check_symbolic_index(self, index): + # Check if any index is symbolic: + tuple_index = (index if isinstance(index, tuple) else (index,)) + if any((isinstance(i, Expr) and (not i.is_number)) for i in tuple_index): + for i, nth_dim in zip(tuple_index, self.shape): + if ((i < 0) == True) or ((i >= nth_dim) == True): + raise ValueError("index out of range") + from sympy.tensor import Indexed + return Indexed(self, *tuple_index) + return None + + def _setter_iterable_check(self, value): + from sympy.matrices.matrixbase import MatrixBase + if isinstance(value, (Iterable, MatrixBase, NDimArray)): + raise NotImplementedError + + @classmethod + def _scan_iterable_shape(cls, iterable): + def f(pointer): + if not isinstance(pointer, Iterable): + return [pointer], () + + if len(pointer) == 0: + return [], (0,) + + result = [] + elems, shapes = zip(*[f(i) for i in pointer]) + if len(set(shapes)) != 1: + raise ValueError("could not determine shape unambiguously") + for i in elems: + result.extend(i) + return result, (len(shapes),)+shapes[0] + + return f(iterable) + + @classmethod + def _handle_ndarray_creation_inputs(cls, iterable=None, shape=None, **kwargs): + from sympy.matrices.matrixbase import MatrixBase + from sympy.tensor.array import SparseNDimArray + + if shape is None: + if iterable is None: + shape = () + iterable = () + # Construction of a sparse array from a sparse array + elif isinstance(iterable, SparseNDimArray): + return iterable._shape, iterable._sparse_array + + # Construct N-dim array from another N-dim array: + elif isinstance(iterable, NDimArray): + shape = iterable.shape + + # Construct N-dim array from an iterable (numpy arrays included): + elif isinstance(iterable, Iterable): + iterable, shape = cls._scan_iterable_shape(iterable) + + # Construct N-dim array from a Matrix: + elif isinstance(iterable, MatrixBase): + shape = iterable.shape + + else: + shape = () + iterable = (iterable,) + + if isinstance(iterable, (Dict, dict)) and shape is not None: + new_dict = iterable.copy() + for k in new_dict: + if isinstance(k, (tuple, Tuple)): + new_key = 0 + for i, idx in enumerate(k): + new_key = new_key * shape[i] + idx + iterable[new_key] = iterable[k] + del iterable[k] + + if isinstance(shape, (SYMPY_INTS, Integer)): + shape = (shape,) + + if not all(isinstance(dim, (SYMPY_INTS, Integer)) for dim in shape): + raise TypeError("Shape should contain integers only.") + + return tuple(shape), iterable + + def __len__(self): + """Overload common function len(). Returns number of elements in array. + + Examples + ======== + + >>> from sympy import MutableDenseNDimArray + >>> a = MutableDenseNDimArray.zeros(3, 3) + >>> a + [[0, 0, 0], [0, 0, 0], [0, 0, 0]] + >>> len(a) + 9 + + """ + return self._loop_size + + @property + def shape(self): + """ + Returns array shape (dimension). + + Examples + ======== + + >>> from sympy import MutableDenseNDimArray + >>> a = MutableDenseNDimArray.zeros(3, 3) + >>> a.shape + (3, 3) + + """ + return self._shape + + def rank(self): + """ + Returns rank of array. + + Examples + ======== + + >>> from sympy import MutableDenseNDimArray + >>> a = MutableDenseNDimArray.zeros(3,4,5,6,3) + >>> a.rank() + 5 + + """ + return self._rank + + def diff(self, *args, **kwargs): + """ + Calculate the derivative of each element in the array. + + Examples + ======== + + >>> from sympy import ImmutableDenseNDimArray + >>> from sympy.abc import x, y + >>> M = ImmutableDenseNDimArray([[x, y], [1, x*y]]) + >>> M.diff(x) + [[1, 0], [0, y]] + + """ + from sympy.tensor.array.array_derivatives import ArrayDerivative + kwargs.setdefault('evaluate', True) + return ArrayDerivative(self.as_immutable(), *args, **kwargs) + + def _eval_derivative(self, base): + # Types are (base: scalar, self: array) + return self.applyfunc(lambda x: base.diff(x)) + + def _eval_derivative_n_times(self, s, n): + return Basic._eval_derivative_n_times(self, s, n) + + def applyfunc(self, f): + """Apply a function to each element of the N-dim array. + + Examples + ======== + + >>> from sympy import ImmutableDenseNDimArray + >>> m = ImmutableDenseNDimArray([i*2+j for i in range(2) for j in range(2)], (2, 2)) + >>> m + [[0, 1], [2, 3]] + >>> m.applyfunc(lambda i: 2*i) + [[0, 2], [4, 6]] + """ + from sympy.tensor.array import SparseNDimArray + from sympy.tensor.array.arrayop import Flatten + + if isinstance(self, SparseNDimArray) and f(S.Zero) == 0: + return type(self)({k: f(v) for k, v in self._sparse_array.items() if f(v) != 0}, self.shape) + + return type(self)(map(f, Flatten(self)), self.shape) + + def _sympystr(self, printer): + def f(sh, shape_left, i, j): + if len(shape_left) == 1: + return "["+", ".join([printer._print(self[self._get_tuple_index(e)]) for e in range(i, j)])+"]" + + sh //= shape_left[0] + return "[" + ", ".join([f(sh, shape_left[1:], i+e*sh, i+(e+1)*sh) for e in range(shape_left[0])]) + "]" # + "\n"*len(shape_left) + + if self.rank() == 0: + return printer._print(self[()]) + if 0 in self.shape: + return f"{self.__class__.__name__}([], {self.shape})" + return f(self._loop_size, self.shape, 0, self._loop_size) + + def tolist(self): + """ + Converting MutableDenseNDimArray to one-dim list + + Examples + ======== + + >>> from sympy import MutableDenseNDimArray + >>> a = MutableDenseNDimArray([1, 2, 3, 4], (2, 2)) + >>> a + [[1, 2], [3, 4]] + >>> b = a.tolist() + >>> b + [[1, 2], [3, 4]] + """ + + def f(sh, shape_left, i, j): + if len(shape_left) == 1: + return [self[self._get_tuple_index(e)] for e in range(i, j)] + result = [] + sh //= shape_left[0] + for e in range(shape_left[0]): + result.append(f(sh, shape_left[1:], i+e*sh, i+(e+1)*sh)) + return result + + return f(self._loop_size, self.shape, 0, self._loop_size) + + def __add__(self, other): + from sympy.tensor.array.arrayop import Flatten + + if not isinstance(other, NDimArray): + return NotImplemented + + if self.shape != other.shape: + raise ValueError("array shape mismatch") + result_list = [i+j for i,j in zip(Flatten(self), Flatten(other))] + + return type(self)(result_list, self.shape) + + def __sub__(self, other): + from sympy.tensor.array.arrayop import Flatten + + if not isinstance(other, NDimArray): + return NotImplemented + + if self.shape != other.shape: + raise ValueError("array shape mismatch") + result_list = [i-j for i,j in zip(Flatten(self), Flatten(other))] + + return type(self)(result_list, self.shape) + + def __mul__(self, other): + from sympy.matrices.matrixbase import MatrixBase + from sympy.tensor.array import SparseNDimArray + from sympy.tensor.array.arrayop import Flatten + + if isinstance(other, (Iterable, NDimArray, MatrixBase)): + raise ValueError("scalar expected, use tensorproduct(...) for tensorial product") + + other = sympify(other) + if isinstance(self, SparseNDimArray): + if other.is_zero: + return type(self)({}, self.shape) + return type(self)({k: other*v for (k, v) in self._sparse_array.items()}, self.shape) + + result_list = [i*other for i in Flatten(self)] + return type(self)(result_list, self.shape) + + def __rmul__(self, other): + from sympy.matrices.matrixbase import MatrixBase + from sympy.tensor.array import SparseNDimArray + from sympy.tensor.array.arrayop import Flatten + + if isinstance(other, (Iterable, NDimArray, MatrixBase)): + raise ValueError("scalar expected, use tensorproduct(...) for tensorial product") + + other = sympify(other) + if isinstance(self, SparseNDimArray): + if other.is_zero: + return type(self)({}, self.shape) + return type(self)({k: other*v for (k, v) in self._sparse_array.items()}, self.shape) + + result_list = [other*i for i in Flatten(self)] + return type(self)(result_list, self.shape) + + def __truediv__(self, other): + from sympy.matrices.matrixbase import MatrixBase + from sympy.tensor.array import SparseNDimArray + from sympy.tensor.array.arrayop import Flatten + + if isinstance(other, (Iterable, NDimArray, MatrixBase)): + raise ValueError("scalar expected") + + other = sympify(other) + if isinstance(self, SparseNDimArray) and other != S.Zero: + return type(self)({k: v/other for (k, v) in self._sparse_array.items()}, self.shape) + + result_list = [i/other for i in Flatten(self)] + return type(self)(result_list, self.shape) + + def __rtruediv__(self, other): + raise NotImplementedError('unsupported operation on NDimArray') + + def __neg__(self): + from sympy.tensor.array import SparseNDimArray + from sympy.tensor.array.arrayop import Flatten + + if isinstance(self, SparseNDimArray): + return type(self)({k: -v for (k, v) in self._sparse_array.items()}, self.shape) + + result_list = [-i for i in Flatten(self)] + return type(self)(result_list, self.shape) + + def __iter__(self): + def iterator(): + if self._shape: + for i in range(self._shape[0]): + yield self[i] + else: + yield self[()] + + return iterator() + + def __eq__(self, other): + """ + NDimArray instances can be compared to each other. + Instances equal if they have same shape and data. + + Examples + ======== + + >>> from sympy import MutableDenseNDimArray + >>> a = MutableDenseNDimArray.zeros(2, 3) + >>> b = MutableDenseNDimArray.zeros(2, 3) + >>> a == b + True + >>> c = a.reshape(3, 2) + >>> c == b + False + >>> a[0,0] = 1 + >>> b[0,0] = 2 + >>> a == b + False + """ + from sympy.tensor.array import SparseNDimArray + if not isinstance(other, NDimArray): + return False + + if not self.shape == other.shape: + return False + + if isinstance(self, SparseNDimArray) and isinstance(other, SparseNDimArray): + return dict(self._sparse_array) == dict(other._sparse_array) + + return list(self) == list(other) + + def __ne__(self, other): + return not self == other + + def _eval_transpose(self): + if self.rank() != 2: + raise ValueError("array rank not 2") + from .arrayop import permutedims + return permutedims(self, (1, 0)) + + def transpose(self): + return self._eval_transpose() + + def _eval_conjugate(self): + from sympy.tensor.array.arrayop import Flatten + + return self.func([i.conjugate() for i in Flatten(self)], self.shape) + + def conjugate(self): + return self._eval_conjugate() + + def _eval_adjoint(self): + return self.transpose().conjugate() + + def adjoint(self): + return self._eval_adjoint() + + def _slice_expand(self, s, dim): + if not isinstance(s, slice): + return (s,) + start, stop, step = s.indices(dim) + return [start + i*step for i in range((stop-start)//step)] + + def _get_slice_data_for_array_access(self, index): + sl_factors = [self._slice_expand(i, dim) for (i, dim) in zip(index, self.shape)] + eindices = itertools.product(*sl_factors) + return sl_factors, eindices + + def _get_slice_data_for_array_assignment(self, index, value): + if not isinstance(value, NDimArray): + value = type(self)(value) + sl_factors, eindices = self._get_slice_data_for_array_access(index) + slice_offsets = [min(i) if isinstance(i, list) else None for i in sl_factors] + # TODO: add checks for dimensions for `value`? + return value, eindices, slice_offsets + + @classmethod + def _check_special_bounds(cls, flat_list, shape): + if shape == () and len(flat_list) != 1: + raise ValueError("arrays without shape need one scalar value") + if shape == (0,) and len(flat_list) > 0: + raise ValueError("if array shape is (0,) there cannot be elements") + + def _check_index_for_getitem(self, index): + if isinstance(index, (SYMPY_INTS, Integer, slice)): + index = (index,) + + if len(index) < self.rank(): + index = tuple(index) + \ + tuple(slice(None) for i in range(len(index), self.rank())) + + if len(index) > self.rank(): + raise ValueError('Dimension of index greater than rank of array') + + return index + + +class ImmutableNDimArray(NDimArray, Basic): + _op_priority = 11.0 + + def __hash__(self): + return Basic.__hash__(self) + + def as_immutable(self): + return self + + def as_mutable(self): + raise NotImplementedError("abstract method") diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/sparse_ndim_array.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/sparse_ndim_array.py new file mode 100644 index 0000000000000000000000000000000000000000..f11aa95be8ec9d10a9104d48fb28f406fe43845e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/sparse_ndim_array.py @@ -0,0 +1,196 @@ +from sympy.core.basic import Basic +from sympy.core.containers import (Dict, Tuple) +from sympy.core.singleton import S +from sympy.core.sympify import _sympify +from sympy.tensor.array.mutable_ndim_array import MutableNDimArray +from sympy.tensor.array.ndim_array import NDimArray, ImmutableNDimArray +from sympy.utilities.iterables import flatten + +import functools + +class SparseNDimArray(NDimArray): + + def __new__(self, *args, **kwargs): + return ImmutableSparseNDimArray(*args, **kwargs) + + def __getitem__(self, index): + """ + Get an element from a sparse N-dim array. + + Examples + ======== + + >>> from sympy import MutableSparseNDimArray + >>> a = MutableSparseNDimArray(range(4), (2, 2)) + >>> a + [[0, 1], [2, 3]] + >>> a[0, 0] + 0 + >>> a[1, 1] + 3 + >>> a[0] + [0, 1] + >>> a[1] + [2, 3] + + Symbolic indexing: + + >>> from sympy.abc import i, j + >>> a[i, j] + [[0, 1], [2, 3]][i, j] + + Replace `i` and `j` to get element `(0, 0)`: + + >>> a[i, j].subs({i: 0, j: 0}) + 0 + + """ + syindex = self._check_symbolic_index(index) + if syindex is not None: + return syindex + + index = self._check_index_for_getitem(index) + + # `index` is a tuple with one or more slices: + if isinstance(index, tuple) and any(isinstance(i, slice) for i in index): + sl_factors, eindices = self._get_slice_data_for_array_access(index) + array = [self._sparse_array.get(self._parse_index(i), S.Zero) for i in eindices] + nshape = [len(el) for i, el in enumerate(sl_factors) if isinstance(index[i], slice)] + return type(self)(array, nshape) + else: + index = self._parse_index(index) + return self._sparse_array.get(index, S.Zero) + + @classmethod + def zeros(cls, *shape): + """ + Return a sparse N-dim array of zeros. + """ + return cls({}, shape) + + def tomatrix(self): + """ + Converts MutableDenseNDimArray to Matrix. Can convert only 2-dim array, else will raise error. + + Examples + ======== + + >>> from sympy import MutableSparseNDimArray + >>> a = MutableSparseNDimArray([1 for i in range(9)], (3, 3)) + >>> b = a.tomatrix() + >>> b + Matrix([ + [1, 1, 1], + [1, 1, 1], + [1, 1, 1]]) + """ + from sympy.matrices import SparseMatrix + if self.rank() != 2: + raise ValueError('Dimensions must be of size of 2') + + mat_sparse = {} + for key, value in self._sparse_array.items(): + mat_sparse[self._get_tuple_index(key)] = value + + return SparseMatrix(self.shape[0], self.shape[1], mat_sparse) + + def reshape(self, *newshape): + new_total_size = functools.reduce(lambda x,y: x*y, newshape) + if new_total_size != self._loop_size: + raise ValueError("Invalid reshape parameters " + newshape) + + return type(self)(self._sparse_array, newshape) + +class ImmutableSparseNDimArray(SparseNDimArray, ImmutableNDimArray): # type: ignore + + def __new__(cls, iterable=None, shape=None, **kwargs): + shape, flat_list = cls._handle_ndarray_creation_inputs(iterable, shape, **kwargs) + shape = Tuple(*map(_sympify, shape)) + cls._check_special_bounds(flat_list, shape) + loop_size = functools.reduce(lambda x,y: x*y, shape) if shape else len(flat_list) + + # Sparse array: + if isinstance(flat_list, (dict, Dict)): + sparse_array = Dict(flat_list) + else: + sparse_array = {} + for i, el in enumerate(flatten(flat_list)): + if el != 0: + sparse_array[i] = _sympify(el) + + sparse_array = Dict(sparse_array) + + self = Basic.__new__(cls, sparse_array, shape, **kwargs) + self._shape = shape + self._rank = len(shape) + self._loop_size = loop_size + self._sparse_array = sparse_array + + return self + + def __setitem__(self, index, value): + raise TypeError("immutable N-dim array") + + def as_mutable(self): + return MutableSparseNDimArray(self) + + +class MutableSparseNDimArray(MutableNDimArray, SparseNDimArray): + + def __new__(cls, iterable=None, shape=None, **kwargs): + shape, flat_list = cls._handle_ndarray_creation_inputs(iterable, shape, **kwargs) + self = object.__new__(cls) + self._shape = shape + self._rank = len(shape) + self._loop_size = functools.reduce(lambda x,y: x*y, shape) if shape else len(flat_list) + + # Sparse array: + if isinstance(flat_list, (dict, Dict)): + self._sparse_array = dict(flat_list) + return self + + self._sparse_array = {} + + for i, el in enumerate(flatten(flat_list)): + if el != 0: + self._sparse_array[i] = _sympify(el) + + return self + + def __setitem__(self, index, value): + """Allows to set items to MutableDenseNDimArray. + + Examples + ======== + + >>> from sympy import MutableSparseNDimArray + >>> a = MutableSparseNDimArray.zeros(2, 2) + >>> a[0, 0] = 1 + >>> a[1, 1] = 1 + >>> a + [[1, 0], [0, 1]] + """ + if isinstance(index, tuple) and any(isinstance(i, slice) for i in index): + value, eindices, slice_offsets = self._get_slice_data_for_array_assignment(index, value) + for i in eindices: + other_i = [ind - j for ind, j in zip(i, slice_offsets) if j is not None] + other_value = value[other_i] + complete_index = self._parse_index(i) + if other_value != 0: + self._sparse_array[complete_index] = other_value + elif complete_index in self._sparse_array: + self._sparse_array.pop(complete_index) + else: + index = self._parse_index(index) + value = _sympify(value) + if value == 0 and index in self._sparse_array: + self._sparse_array.pop(index) + else: + self._sparse_array[index] = value + + def as_immutable(self): + return ImmutableSparseNDimArray(self) + + @property + def free_symbols(self): + return {i for j in self._sparse_array.values() for i in j.free_symbols} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/test_array_comprehension.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/test_array_comprehension.py new file mode 100644 index 0000000000000000000000000000000000000000..510e068f287fa04419712e5e9a16a314e522a62d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/test_array_comprehension.py @@ -0,0 +1,78 @@ +from sympy.tensor.array.array_comprehension import ArrayComprehension, ArrayComprehensionMap +from sympy.tensor.array import ImmutableDenseNDimArray +from sympy.abc import i, j, k, l +from sympy.testing.pytest import raises +from sympy.matrices import Matrix + + +def test_array_comprehension(): + a = ArrayComprehension(i*j, (i, 1, 3), (j, 2, 4)) + b = ArrayComprehension(i, (i, 1, j+1)) + c = ArrayComprehension(i+j+k+l, (i, 1, 2), (j, 1, 3), (k, 1, 4), (l, 1, 5)) + d = ArrayComprehension(k, (i, 1, 5)) + e = ArrayComprehension(i, (j, k+1, k+5)) + assert a.doit().tolist() == [[2, 3, 4], [4, 6, 8], [6, 9, 12]] + assert a.shape == (3, 3) + assert a.is_shape_numeric == True + assert a.tolist() == [[2, 3, 4], [4, 6, 8], [6, 9, 12]] + assert a.tomatrix() == Matrix([ + [2, 3, 4], + [4, 6, 8], + [6, 9, 12]]) + assert len(a) == 9 + assert isinstance(b.doit(), ArrayComprehension) + assert isinstance(a.doit(), ImmutableDenseNDimArray) + assert b.subs(j, 3) == ArrayComprehension(i, (i, 1, 4)) + assert b.free_symbols == {j} + assert b.shape == (j + 1,) + assert b.rank() == 1 + assert b.is_shape_numeric == False + assert c.free_symbols == set() + assert c.function == i + j + k + l + assert c.limits == ((i, 1, 2), (j, 1, 3), (k, 1, 4), (l, 1, 5)) + assert c.doit().tolist() == [[[[4, 5, 6, 7, 8], [5, 6, 7, 8, 9], [6, 7, 8, 9, 10], [7, 8, 9, 10, 11]], + [[5, 6, 7, 8, 9], [6, 7, 8, 9, 10], [7, 8, 9, 10, 11], [8, 9, 10, 11, 12]], + [[6, 7, 8, 9, 10], [7, 8, 9, 10, 11], [8, 9, 10, 11, 12], [9, 10, 11, 12, 13]]], + [[[5, 6, 7, 8, 9], [6, 7, 8, 9, 10], [7, 8, 9, 10, 11], [8, 9, 10, 11, 12]], + [[6, 7, 8, 9, 10], [7, 8, 9, 10, 11], [8, 9, 10, 11, 12], [9, 10, 11, 12, 13]], + [[7, 8, 9, 10, 11], [8, 9, 10, 11, 12], [9, 10, 11, 12, 13], [10, 11, 12, 13, 14]]]] + assert c.free_symbols == set() + assert c.variables == [i, j, k, l] + assert c.bound_symbols == [i, j, k, l] + assert d.doit().tolist() == [k, k, k, k, k] + assert len(e) == 5 + raises(TypeError, lambda: ArrayComprehension(i*j, (i, 1, 3), (j, 2, [1, 3, 2]))) + raises(ValueError, lambda: ArrayComprehension(i*j, (i, 1, 3), (j, 2, 1))) + raises(ValueError, lambda: ArrayComprehension(i*j, (i, 1, 3), (j, 2, j+1))) + raises(ValueError, lambda: len(ArrayComprehension(i*j, (i, 1, 3), (j, 2, j+4)))) + raises(TypeError, lambda: ArrayComprehension(i*j, (i, 0, i + 1.5), (j, 0, 2))) + raises(ValueError, lambda: b.tolist()) + raises(ValueError, lambda: b.tomatrix()) + raises(ValueError, lambda: c.tomatrix()) + +def test_arraycomprehensionmap(): + a = ArrayComprehensionMap(lambda i: i+1, (i, 1, 5)) + assert a.doit().tolist() == [2, 3, 4, 5, 6] + assert a.shape == (5,) + assert a.is_shape_numeric + assert a.tolist() == [2, 3, 4, 5, 6] + assert len(a) == 5 + assert isinstance(a.doit(), ImmutableDenseNDimArray) + expr = ArrayComprehensionMap(lambda i: i+1, (i, 1, k)) + assert expr.doit() == expr + assert expr.subs(k, 4) == ArrayComprehensionMap(lambda i: i+1, (i, 1, 4)) + assert expr.subs(k, 4).doit() == ImmutableDenseNDimArray([2, 3, 4, 5]) + b = ArrayComprehensionMap(lambda i: i+1, (i, 1, 2), (i, 1, 3), (i, 1, 4), (i, 1, 5)) + assert b.doit().tolist() == [[[[2, 3, 4, 5, 6], [3, 5, 7, 9, 11], [4, 7, 10, 13, 16], [5, 9, 13, 17, 21]], + [[3, 5, 7, 9, 11], [5, 9, 13, 17, 21], [7, 13, 19, 25, 31], [9, 17, 25, 33, 41]], + [[4, 7, 10, 13, 16], [7, 13, 19, 25, 31], [10, 19, 28, 37, 46], [13, 25, 37, 49, 61]]], + [[[3, 5, 7, 9, 11], [5, 9, 13, 17, 21], [7, 13, 19, 25, 31], [9, 17, 25, 33, 41]], + [[5, 9, 13, 17, 21], [9, 17, 25, 33, 41], [13, 25, 37, 49, 61], [17, 33, 49, 65, 81]], + [[7, 13, 19, 25, 31], [13, 25, 37, 49, 61], [19, 37, 55, 73, 91], [25, 49, 73, 97, 121]]]] + + # tests about lambda expression + assert ArrayComprehensionMap(lambda: 3, (i, 1, 5)).doit().tolist() == [3, 3, 3, 3, 3] + assert ArrayComprehensionMap(lambda i: i+1, (i, 1, 5)).doit().tolist() == [2, 3, 4, 5, 6] + raises(ValueError, lambda: ArrayComprehensionMap(i*j, (i, 1, 3), (j, 2, 4))) + a = ArrayComprehensionMap(lambda i, j: i+j, (i, 1, 5)) + raises(ValueError, lambda: a.doit()) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/test_array_derivatives.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/test_array_derivatives.py new file mode 100644 index 0000000000000000000000000000000000000000..7f6c777c55a9170704f309bf74387d140bf2ec32 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/test_array_derivatives.py @@ -0,0 +1,52 @@ +from sympy.core.symbol import symbols +from sympy.matrices.dense import Matrix +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.tensor.array.ndim_array import NDimArray +from sympy.matrices.matrixbase import MatrixBase +from sympy.tensor.array.array_derivatives import ArrayDerivative + +x, y, z, t = symbols("x y z t") + +m = Matrix([[x, y], [z, t]]) + +M = MatrixSymbol("M", 3, 2) +N = MatrixSymbol("N", 4, 3) + + +def test_array_derivative_construction(): + + d = ArrayDerivative(x, m, evaluate=False) + assert d.shape == (2, 2) + expr = d.doit() + assert isinstance(expr, MatrixBase) + assert expr.shape == (2, 2) + + d = ArrayDerivative(m, m, evaluate=False) + assert d.shape == (2, 2, 2, 2) + expr = d.doit() + assert isinstance(expr, NDimArray) + assert expr.shape == (2, 2, 2, 2) + + d = ArrayDerivative(m, x, evaluate=False) + assert d.shape == (2, 2) + expr = d.doit() + assert isinstance(expr, MatrixBase) + assert expr.shape == (2, 2) + + d = ArrayDerivative(M, N, evaluate=False) + assert d.shape == (4, 3, 3, 2) + expr = d.doit() + assert isinstance(expr, ArrayDerivative) + assert expr.shape == (4, 3, 3, 2) + + d = ArrayDerivative(M, (N, 2), evaluate=False) + assert d.shape == (4, 3, 4, 3, 3, 2) + expr = d.doit() + assert isinstance(expr, ArrayDerivative) + assert expr.shape == (4, 3, 4, 3, 3, 2) + + d = ArrayDerivative(M.as_explicit(), (N.as_explicit(), 2), evaluate=False) + assert d.doit().shape == (4, 3, 4, 3, 3, 2) + expr = d.doit() + assert isinstance(expr, NDimArray) + assert expr.shape == (4, 3, 4, 3, 3, 2) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/test_arrayop.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/test_arrayop.py new file mode 100644 index 0000000000000000000000000000000000000000..de56e81e0064f1e303a7a58e41932d15f2d0b41e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/test_arrayop.py @@ -0,0 +1,361 @@ +import itertools +import random + +from sympy.combinatorics import Permutation +from sympy.combinatorics.permutations import _af_invert +from sympy.testing.pytest import raises + +from sympy.core.function import diff +from sympy.core.symbol import symbols +from sympy.functions.elementary.complexes import (adjoint, conjugate, transpose) +from sympy.functions.elementary.exponential import (exp, log) +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.tensor.array import Array, ImmutableDenseNDimArray, ImmutableSparseNDimArray, MutableSparseNDimArray + +from sympy.tensor.array.arrayop import tensorproduct, tensorcontraction, derive_by_array, permutedims, Flatten, \ + tensordiagonal + + +def test_import_NDimArray(): + from sympy.tensor.array import NDimArray + del NDimArray + + +def test_tensorproduct(): + x,y,z,t = symbols('x y z t') + from sympy.abc import a,b,c,d + assert tensorproduct() == 1 + assert tensorproduct([x]) == Array([x]) + assert tensorproduct([x], [y]) == Array([[x*y]]) + assert tensorproduct([x], [y], [z]) == Array([[[x*y*z]]]) + assert tensorproduct([x], [y], [z], [t]) == Array([[[[x*y*z*t]]]]) + + assert tensorproduct(x) == x + assert tensorproduct(x, y) == x*y + assert tensorproduct(x, y, z) == x*y*z + assert tensorproduct(x, y, z, t) == x*y*z*t + + for ArrayType in [ImmutableDenseNDimArray, ImmutableSparseNDimArray]: + A = ArrayType([x, y]) + B = ArrayType([1, 2, 3]) + C = ArrayType([a, b, c, d]) + + assert tensorproduct(A, B, C) == ArrayType([[[a*x, b*x, c*x, d*x], [2*a*x, 2*b*x, 2*c*x, 2*d*x], [3*a*x, 3*b*x, 3*c*x, 3*d*x]], + [[a*y, b*y, c*y, d*y], [2*a*y, 2*b*y, 2*c*y, 2*d*y], [3*a*y, 3*b*y, 3*c*y, 3*d*y]]]) + + assert tensorproduct([x, y], [1, 2, 3]) == tensorproduct(A, B) + + assert tensorproduct(A, 2) == ArrayType([2*x, 2*y]) + assert tensorproduct(A, [2]) == ArrayType([[2*x], [2*y]]) + assert tensorproduct([2], A) == ArrayType([[2*x, 2*y]]) + assert tensorproduct(a, A) == ArrayType([a*x, a*y]) + assert tensorproduct(a, A, B) == ArrayType([[a*x, 2*a*x, 3*a*x], [a*y, 2*a*y, 3*a*y]]) + assert tensorproduct(A, B, a) == ArrayType([[a*x, 2*a*x, 3*a*x], [a*y, 2*a*y, 3*a*y]]) + assert tensorproduct(B, a, A) == ArrayType([[a*x, a*y], [2*a*x, 2*a*y], [3*a*x, 3*a*y]]) + + # tests for large scale sparse array + for SparseArrayType in [ImmutableSparseNDimArray, MutableSparseNDimArray]: + a = SparseArrayType({1:2, 3:4},(1000, 2000)) + b = SparseArrayType({1:2, 3:4},(1000, 2000)) + assert tensorproduct(a, b) == ImmutableSparseNDimArray({2000001: 4, 2000003: 8, 6000001: 8, 6000003: 16}, (1000, 2000, 1000, 2000)) + + +def test_tensorcontraction(): + from sympy.abc import a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x + B = Array(range(18), (2, 3, 3)) + assert tensorcontraction(B, (1, 2)) == Array([12, 39]) + C1 = Array([a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x], (2, 3, 2, 2)) + + assert tensorcontraction(C1, (0, 2)) == Array([[a + o, b + p], [e + s, f + t], [i + w, j + x]]) + assert tensorcontraction(C1, (0, 2, 3)) == Array([a + p, e + t, i + x]) + assert tensorcontraction(C1, (2, 3)) == Array([[a + d, e + h, i + l], [m + p, q + t, u + x]]) + + +def test_derivative_by_array(): + from sympy.abc import i, j, t, x, y, z + + bexpr = x*y**2*exp(z)*log(t) + sexpr = sin(bexpr) + cexpr = cos(bexpr) + + a = Array([sexpr]) + + assert derive_by_array(sexpr, t) == x*y**2*exp(z)*cos(x*y**2*exp(z)*log(t))/t + assert derive_by_array(sexpr, [x, y, z]) == Array([bexpr/x*cexpr, 2*y*bexpr/y**2*cexpr, bexpr*cexpr]) + assert derive_by_array(a, [x, y, z]) == Array([[bexpr/x*cexpr], [2*y*bexpr/y**2*cexpr], [bexpr*cexpr]]) + + assert derive_by_array(sexpr, [[x, y], [z, t]]) == Array([[bexpr/x*cexpr, 2*y*bexpr/y**2*cexpr], [bexpr*cexpr, bexpr/log(t)/t*cexpr]]) + assert derive_by_array(a, [[x, y], [z, t]]) == Array([[[bexpr/x*cexpr], [2*y*bexpr/y**2*cexpr]], [[bexpr*cexpr], [bexpr/log(t)/t*cexpr]]]) + assert derive_by_array([[x, y], [z, t]], [x, y]) == Array([[[1, 0], [0, 0]], [[0, 1], [0, 0]]]) + assert derive_by_array([[x, y], [z, t]], [[x, y], [z, t]]) == Array([[[[1, 0], [0, 0]], [[0, 1], [0, 0]]], + [[[0, 0], [1, 0]], [[0, 0], [0, 1]]]]) + + assert diff(sexpr, t) == x*y**2*exp(z)*cos(x*y**2*exp(z)*log(t))/t + assert diff(sexpr, Array([x, y, z])) == Array([bexpr/x*cexpr, 2*y*bexpr/y**2*cexpr, bexpr*cexpr]) + assert diff(a, Array([x, y, z])) == Array([[bexpr/x*cexpr], [2*y*bexpr/y**2*cexpr], [bexpr*cexpr]]) + + assert diff(sexpr, Array([[x, y], [z, t]])) == Array([[bexpr/x*cexpr, 2*y*bexpr/y**2*cexpr], [bexpr*cexpr, bexpr/log(t)/t*cexpr]]) + assert diff(a, Array([[x, y], [z, t]])) == Array([[[bexpr/x*cexpr], [2*y*bexpr/y**2*cexpr]], [[bexpr*cexpr], [bexpr/log(t)/t*cexpr]]]) + assert diff(Array([[x, y], [z, t]]), Array([x, y])) == Array([[[1, 0], [0, 0]], [[0, 1], [0, 0]]]) + assert diff(Array([[x, y], [z, t]]), Array([[x, y], [z, t]])) == Array([[[[1, 0], [0, 0]], [[0, 1], [0, 0]]], + [[[0, 0], [1, 0]], [[0, 0], [0, 1]]]]) + + # test for large scale sparse array + for SparseArrayType in [ImmutableSparseNDimArray, MutableSparseNDimArray]: + b = MutableSparseNDimArray({0:i, 1:j}, (10000, 20000)) + assert derive_by_array(b, i) == ImmutableSparseNDimArray({0: 1}, (10000, 20000)) + assert derive_by_array(b, (i, j)) == ImmutableSparseNDimArray({0: 1, 200000001: 1}, (2, 10000, 20000)) + + #https://github.com/sympy/sympy/issues/20655 + U = Array([x, y, z]) + E = 2 + assert derive_by_array(E, U) == ImmutableDenseNDimArray([0, 0, 0]) + + +def test_issue_emerged_while_discussing_10972(): + ua = Array([-1,0]) + Fa = Array([[0, 1], [-1, 0]]) + po = tensorproduct(Fa, ua, Fa, ua) + assert tensorcontraction(po, (1, 2), (4, 5)) == Array([[0, 0], [0, 1]]) + + sa = symbols('a0:144') + po = Array(sa, [2, 2, 3, 3, 2, 2]) + assert tensorcontraction(po, (0, 1), (2, 3), (4, 5)) == sa[0] + sa[108] + sa[111] + sa[124] + sa[127] + sa[140] + sa[143] + sa[16] + sa[19] + sa[3] + sa[32] + sa[35] + assert tensorcontraction(po, (0, 1, 4, 5), (2, 3)) == sa[0] + sa[111] + sa[127] + sa[143] + sa[16] + sa[32] + assert tensorcontraction(po, (0, 1), (4, 5)) == Array([[sa[0] + sa[108] + sa[111] + sa[3], sa[112] + sa[115] + sa[4] + sa[7], + sa[11] + sa[116] + sa[119] + sa[8]], [sa[12] + sa[120] + sa[123] + sa[15], + sa[124] + sa[127] + sa[16] + sa[19], sa[128] + sa[131] + sa[20] + sa[23]], + [sa[132] + sa[135] + sa[24] + sa[27], sa[136] + sa[139] + sa[28] + sa[31], + sa[140] + sa[143] + sa[32] + sa[35]]]) + assert tensorcontraction(po, (0, 1), (2, 3)) == Array([[sa[0] + sa[108] + sa[124] + sa[140] + sa[16] + sa[32], sa[1] + sa[109] + sa[125] + sa[141] + sa[17] + sa[33]], + [sa[110] + sa[126] + sa[142] + sa[18] + sa[2] + sa[34], sa[111] + sa[127] + sa[143] + sa[19] + sa[3] + sa[35]]]) + + +def test_array_permutedims(): + sa = symbols('a0:144') + + for ArrayType in [ImmutableDenseNDimArray, ImmutableSparseNDimArray]: + m1 = ArrayType(sa[:6], (2, 3)) + assert permutedims(m1, (1, 0)) == transpose(m1) + assert m1.tomatrix().T == permutedims(m1, (1, 0)).tomatrix() + + assert m1.tomatrix().T == transpose(m1).tomatrix() + assert m1.tomatrix().C == conjugate(m1).tomatrix() + assert m1.tomatrix().H == adjoint(m1).tomatrix() + + assert m1.tomatrix().T == m1.transpose().tomatrix() + assert m1.tomatrix().C == m1.conjugate().tomatrix() + assert m1.tomatrix().H == m1.adjoint().tomatrix() + + raises(ValueError, lambda: permutedims(m1, (0,))) + raises(ValueError, lambda: permutedims(m1, (0, 0))) + raises(ValueError, lambda: permutedims(m1, (1, 2, 0))) + + # Some tests with random arrays: + dims = 6 + shape = [random.randint(1,5) for i in range(dims)] + elems = [random.random() for i in range(tensorproduct(*shape))] + ra = ArrayType(elems, shape) + perm = list(range(dims)) + # Randomize the permutation: + random.shuffle(perm) + # Test inverse permutation: + assert permutedims(permutedims(ra, perm), _af_invert(perm)) == ra + # Test that permuted shape corresponds to action by `Permutation`: + assert permutedims(ra, perm).shape == tuple(Permutation(perm)(shape)) + + z = ArrayType.zeros(4,5,6,7) + + assert permutedims(z, (2, 3, 1, 0)).shape == (6, 7, 5, 4) + assert permutedims(z, [2, 3, 1, 0]).shape == (6, 7, 5, 4) + assert permutedims(z, Permutation([2, 3, 1, 0])).shape == (6, 7, 5, 4) + + po = ArrayType(sa, [2, 2, 3, 3, 2, 2]) + + raises(ValueError, lambda: permutedims(po, (1, 1))) + raises(ValueError, lambda: po.transpose()) + raises(ValueError, lambda: po.adjoint()) + + assert permutedims(po, reversed(range(po.rank()))) == ArrayType( + [[[[[[sa[0], sa[72]], [sa[36], sa[108]]], [[sa[12], sa[84]], [sa[48], sa[120]]], [[sa[24], + sa[96]], [sa[60], sa[132]]]], + [[[sa[4], sa[76]], [sa[40], sa[112]]], [[sa[16], + sa[88]], [sa[52], sa[124]]], + [[sa[28], sa[100]], [sa[64], sa[136]]]], + [[[sa[8], + sa[80]], [sa[44], sa[116]]], [[sa[20], sa[92]], [sa[56], sa[128]]], [[sa[32], + sa[104]], [sa[68], sa[140]]]]], + [[[[sa[2], sa[74]], [sa[38], sa[110]]], [[sa[14], + sa[86]], [sa[50], sa[122]]], [[sa[26], sa[98]], [sa[62], sa[134]]]], + [[[sa[6], + sa[78]], [sa[42], sa[114]]], [[sa[18], sa[90]], [sa[54], sa[126]]], [[sa[30], + sa[102]], [sa[66], sa[138]]]], + [[[sa[10], sa[82]], [sa[46], sa[118]]], [[sa[22], + sa[94]], [sa[58], sa[130]]], + [[sa[34], sa[106]], [sa[70], sa[142]]]]]], + [[[[[sa[1], + sa[73]], [sa[37], sa[109]]], [[sa[13], sa[85]], [sa[49], sa[121]]], [[sa[25], + sa[97]], [sa[61], sa[133]]]], + [[[sa[5], sa[77]], [sa[41], sa[113]]], [[sa[17], + sa[89]], [sa[53], sa[125]]], + [[sa[29], sa[101]], [sa[65], sa[137]]]], + [[[sa[9], + sa[81]], [sa[45], sa[117]]], [[sa[21], sa[93]], [sa[57], sa[129]]], [[sa[33], + sa[105]], [sa[69], sa[141]]]]], + [[[[sa[3], sa[75]], [sa[39], sa[111]]], [[sa[15], + sa[87]], [sa[51], sa[123]]], [[sa[27], sa[99]], [sa[63], sa[135]]]], + [[[sa[7], + sa[79]], [sa[43], sa[115]]], [[sa[19], sa[91]], [sa[55], sa[127]]], [[sa[31], + sa[103]], [sa[67], sa[139]]]], + [[[sa[11], sa[83]], [sa[47], sa[119]]], [[sa[23], + sa[95]], [sa[59], sa[131]]], + [[sa[35], sa[107]], [sa[71], sa[143]]]]]]]) + + assert permutedims(po, (1, 0, 2, 3, 4, 5)) == ArrayType( + [[[[[[sa[0], sa[1]], [sa[2], sa[3]]], [[sa[4], sa[5]], [sa[6], sa[7]]], [[sa[8], sa[9]], [sa[10], + sa[11]]]], + [[[sa[12], sa[13]], [sa[14], sa[15]]], [[sa[16], sa[17]], [sa[18], + sa[19]]], [[sa[20], sa[21]], [sa[22], sa[23]]]], + [[[sa[24], sa[25]], [sa[26], + sa[27]]], [[sa[28], sa[29]], [sa[30], sa[31]]], [[sa[32], sa[33]], [sa[34], + sa[35]]]]], + [[[[sa[72], sa[73]], [sa[74], sa[75]]], [[sa[76], sa[77]], [sa[78], + sa[79]]], [[sa[80], sa[81]], [sa[82], sa[83]]]], + [[[sa[84], sa[85]], [sa[86], + sa[87]]], [[sa[88], sa[89]], [sa[90], sa[91]]], [[sa[92], sa[93]], [sa[94], + sa[95]]]], + [[[sa[96], sa[97]], [sa[98], sa[99]]], [[sa[100], sa[101]], [sa[102], + sa[103]]], + [[sa[104], sa[105]], [sa[106], sa[107]]]]]], [[[[[sa[36], sa[37]], [sa[38], + sa[39]]], + [[sa[40], sa[41]], [sa[42], sa[43]]], + [[sa[44], sa[45]], [sa[46], + sa[47]]]], + [[[sa[48], sa[49]], [sa[50], sa[51]]], + [[sa[52], sa[53]], [sa[54], + sa[55]]], + [[sa[56], sa[57]], [sa[58], sa[59]]]], + [[[sa[60], sa[61]], [sa[62], + sa[63]]], + [[sa[64], sa[65]], [sa[66], sa[67]]], + [[sa[68], sa[69]], [sa[70], + sa[71]]]]], [ + [[[sa[108], sa[109]], [sa[110], sa[111]]], + [[sa[112], sa[113]], [sa[114], + sa[115]]], + [[sa[116], sa[117]], [sa[118], sa[119]]]], + [[[sa[120], sa[121]], [sa[122], + sa[123]]], + [[sa[124], sa[125]], [sa[126], sa[127]]], + [[sa[128], sa[129]], [sa[130], + sa[131]]]], + [[[sa[132], sa[133]], [sa[134], sa[135]]], + [[sa[136], sa[137]], [sa[138], + sa[139]]], + [[sa[140], sa[141]], [sa[142], sa[143]]]]]]]) + + assert permutedims(po, (0, 2, 1, 4, 3, 5)) == ArrayType( + [[[[[[sa[0], sa[1]], [sa[4], sa[5]], [sa[8], sa[9]]], [[sa[2], sa[3]], [sa[6], sa[7]], [sa[10], + sa[11]]]], + [[[sa[36], sa[37]], [sa[40], sa[41]], [sa[44], sa[45]]], [[sa[38], + sa[39]], [sa[42], sa[43]], [sa[46], sa[47]]]]], + [[[[sa[12], sa[13]], [sa[16], + sa[17]], [sa[20], sa[21]]], [[sa[14], sa[15]], [sa[18], sa[19]], [sa[22], + sa[23]]]], + [[[sa[48], sa[49]], [sa[52], sa[53]], [sa[56], sa[57]]], [[sa[50], + sa[51]], [sa[54], sa[55]], [sa[58], sa[59]]]]], + [[[[sa[24], sa[25]], [sa[28], + sa[29]], [sa[32], sa[33]]], [[sa[26], sa[27]], [sa[30], sa[31]], [sa[34], + sa[35]]]], + [[[sa[60], sa[61]], [sa[64], sa[65]], [sa[68], sa[69]]], [[sa[62], + sa[63]], [sa[66], sa[67]], [sa[70], sa[71]]]]]], + [[[[[sa[72], sa[73]], [sa[76], + sa[77]], [sa[80], sa[81]]], [[sa[74], sa[75]], [sa[78], sa[79]], [sa[82], + sa[83]]]], + [[[sa[108], sa[109]], [sa[112], sa[113]], [sa[116], sa[117]]], [[sa[110], + sa[111]], [sa[114], sa[115]], + [sa[118], sa[119]]]]], + [[[[sa[84], sa[85]], [sa[88], + sa[89]], [sa[92], sa[93]]], [[sa[86], sa[87]], [sa[90], sa[91]], [sa[94], + sa[95]]]], + [[[sa[120], sa[121]], [sa[124], sa[125]], [sa[128], sa[129]]], [[sa[122], + sa[123]], [sa[126], sa[127]], + [sa[130], sa[131]]]]], + [[[[sa[96], sa[97]], [sa[100], + sa[101]], [sa[104], sa[105]]], [[sa[98], sa[99]], [sa[102], sa[103]], [sa[106], + sa[107]]]], + [[[sa[132], sa[133]], [sa[136], sa[137]], [sa[140], sa[141]]], [[sa[134], + sa[135]], [sa[138], sa[139]], + [sa[142], sa[143]]]]]]]) + + po2 = po.reshape(4, 9, 2, 2) + assert po2 == ArrayType([[[[sa[0], sa[1]], [sa[2], sa[3]]], [[sa[4], sa[5]], [sa[6], sa[7]]], [[sa[8], sa[9]], [sa[10], sa[11]]], [[sa[12], sa[13]], [sa[14], sa[15]]], [[sa[16], sa[17]], [sa[18], sa[19]]], [[sa[20], sa[21]], [sa[22], sa[23]]], [[sa[24], sa[25]], [sa[26], sa[27]]], [[sa[28], sa[29]], [sa[30], sa[31]]], [[sa[32], sa[33]], [sa[34], sa[35]]]], [[[sa[36], sa[37]], [sa[38], sa[39]]], [[sa[40], sa[41]], [sa[42], sa[43]]], [[sa[44], sa[45]], [sa[46], sa[47]]], [[sa[48], sa[49]], [sa[50], sa[51]]], [[sa[52], sa[53]], [sa[54], sa[55]]], [[sa[56], sa[57]], [sa[58], sa[59]]], [[sa[60], sa[61]], [sa[62], sa[63]]], [[sa[64], sa[65]], [sa[66], sa[67]]], [[sa[68], sa[69]], [sa[70], sa[71]]]], [[[sa[72], sa[73]], [sa[74], sa[75]]], [[sa[76], sa[77]], [sa[78], sa[79]]], [[sa[80], sa[81]], [sa[82], sa[83]]], [[sa[84], sa[85]], [sa[86], sa[87]]], [[sa[88], sa[89]], [sa[90], sa[91]]], [[sa[92], sa[93]], [sa[94], sa[95]]], [[sa[96], sa[97]], [sa[98], sa[99]]], [[sa[100], sa[101]], [sa[102], sa[103]]], [[sa[104], sa[105]], [sa[106], sa[107]]]], [[[sa[108], sa[109]], [sa[110], sa[111]]], [[sa[112], sa[113]], [sa[114], sa[115]]], [[sa[116], sa[117]], [sa[118], sa[119]]], [[sa[120], sa[121]], [sa[122], sa[123]]], [[sa[124], sa[125]], [sa[126], sa[127]]], [[sa[128], sa[129]], [sa[130], sa[131]]], [[sa[132], sa[133]], [sa[134], sa[135]]], [[sa[136], sa[137]], [sa[138], sa[139]]], [[sa[140], sa[141]], [sa[142], sa[143]]]]]) + + assert permutedims(po2, (3, 2, 0, 1)) == ArrayType([[[[sa[0], sa[4], sa[8], sa[12], sa[16], sa[20], sa[24], sa[28], sa[32]], [sa[36], sa[40], sa[44], sa[48], sa[52], sa[56], sa[60], sa[64], sa[68]], [sa[72], sa[76], sa[80], sa[84], sa[88], sa[92], sa[96], sa[100], sa[104]], [sa[108], sa[112], sa[116], sa[120], sa[124], sa[128], sa[132], sa[136], sa[140]]], [[sa[2], sa[6], sa[10], sa[14], sa[18], sa[22], sa[26], sa[30], sa[34]], [sa[38], sa[42], sa[46], sa[50], sa[54], sa[58], sa[62], sa[66], sa[70]], [sa[74], sa[78], sa[82], sa[86], sa[90], sa[94], sa[98], sa[102], sa[106]], [sa[110], sa[114], sa[118], sa[122], sa[126], sa[130], sa[134], sa[138], sa[142]]]], [[[sa[1], sa[5], sa[9], sa[13], sa[17], sa[21], sa[25], sa[29], sa[33]], [sa[37], sa[41], sa[45], sa[49], sa[53], sa[57], sa[61], sa[65], sa[69]], [sa[73], sa[77], sa[81], sa[85], sa[89], sa[93], sa[97], sa[101], sa[105]], [sa[109], sa[113], sa[117], sa[121], sa[125], sa[129], sa[133], sa[137], sa[141]]], [[sa[3], sa[7], sa[11], sa[15], sa[19], sa[23], sa[27], sa[31], sa[35]], [sa[39], sa[43], sa[47], sa[51], sa[55], sa[59], sa[63], sa[67], sa[71]], [sa[75], sa[79], sa[83], sa[87], sa[91], sa[95], sa[99], sa[103], sa[107]], [sa[111], sa[115], sa[119], sa[123], sa[127], sa[131], sa[135], sa[139], sa[143]]]]]) + + # test for large scale sparse array + for SparseArrayType in [ImmutableSparseNDimArray, MutableSparseNDimArray]: + A = SparseArrayType({1:1, 10000:2}, (10000, 20000, 10000)) + assert permutedims(A, (0, 1, 2)) == A + assert permutedims(A, (1, 0, 2)) == SparseArrayType({1: 1, 100000000: 2}, (20000, 10000, 10000)) + B = SparseArrayType({1:1, 20000:2}, (10000, 20000)) + assert B.transpose() == SparseArrayType({10000: 1, 1: 2}, (20000, 10000)) + + +def test_permutedims_with_indices(): + A = Array(range(32)).reshape(2, 2, 2, 2, 2) + indices_new = list("abcde") + indices_old = list("ebdac") + new_A = permutedims(A, index_order_new=indices_new, index_order_old=indices_old) + for a, b, c, d, e in itertools.product(range(2), range(2), range(2), range(2), range(2)): + assert new_A[a, b, c, d, e] == A[e, b, d, a, c] + indices_old = list("cabed") + new_A = permutedims(A, index_order_new=indices_new, index_order_old=indices_old) + for a, b, c, d, e in itertools.product(range(2), range(2), range(2), range(2), range(2)): + assert new_A[a, b, c, d, e] == A[c, a, b, e, d] + raises(ValueError, lambda: permutedims(A, index_order_old=list("aacde"), index_order_new=list("abcde"))) + raises(ValueError, lambda: permutedims(A, index_order_old=list("abcde"), index_order_new=list("abcce"))) + raises(ValueError, lambda: permutedims(A, index_order_old=list("abcde"), index_order_new=list("abce"))) + raises(ValueError, lambda: permutedims(A, index_order_old=list("abce"), index_order_new=list("abce"))) + raises(ValueError, lambda: permutedims(A, [2, 1, 0, 3, 4], index_order_old=list("abcde"))) + raises(ValueError, lambda: permutedims(A, [2, 1, 0, 3, 4], index_order_new=list("abcde"))) + + +def test_flatten(): + from sympy.matrices.dense import Matrix + for ArrayType in [ImmutableDenseNDimArray, ImmutableSparseNDimArray, Matrix]: + A = ArrayType(range(24)).reshape(4, 6) + assert list(Flatten(A)) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23] + + for i, v in enumerate(Flatten(A)): + assert i == v + + +def test_tensordiagonal(): + from sympy.matrices.dense import eye + expr = Array(range(9)).reshape(3, 3) + raises(ValueError, lambda: tensordiagonal(expr, [0], [1])) + raises(ValueError, lambda: tensordiagonal(expr, [0, 0])) + assert tensordiagonal(eye(3), [0, 1]) == Array([1, 1, 1]) + assert tensordiagonal(expr, [0, 1]) == Array([0, 4, 8]) + x, y, z = symbols("x y z") + expr2 = tensorproduct([x, y, z], expr) + assert tensordiagonal(expr2, [1, 2]) == Array([[0, 4*x, 8*x], [0, 4*y, 8*y], [0, 4*z, 8*z]]) + assert tensordiagonal(expr2, [0, 1]) == Array([[0, 3*y, 6*z], [x, 4*y, 7*z], [2*x, 5*y, 8*z]]) + assert tensordiagonal(expr2, [0, 1, 2]) == Array([0, 4*y, 8*z]) + # assert tensordiagonal(expr2, [0]) == permutedims(expr2, [1, 2, 0]) + # assert tensordiagonal(expr2, [1]) == permutedims(expr2, [0, 2, 1]) + # assert tensordiagonal(expr2, [2]) == expr2 + # assert tensordiagonal(expr2, [1], [2]) == expr2 + # assert tensordiagonal(expr2, [0], [1]) == permutedims(expr2, [2, 0, 1]) + + a, b, c, X, Y, Z = symbols("a b c X Y Z") + expr3 = tensorproduct([x, y, z], [1, 2, 3], [a, b, c], [X, Y, Z]) + assert tensordiagonal(expr3, [0, 1, 2, 3]) == Array([x*a*X, 2*y*b*Y, 3*z*c*Z]) + assert tensordiagonal(expr3, [0, 1], [2, 3]) == tensorproduct([x, 2*y, 3*z], [a*X, b*Y, c*Z]) + + # assert tensordiagonal(expr3, [0], [1, 2], [3]) == tensorproduct([x, y, z], [a, 2*b, 3*c], [X, Y, Z]) + assert tensordiagonal(tensordiagonal(expr3, [2, 3]), [0, 1]) == tensorproduct([a*X, b*Y, c*Z], [x, 2*y, 3*z]) + + raises(ValueError, lambda: tensordiagonal([[1, 2, 3], [4, 5, 6]], [0, 1])) + raises(ValueError, lambda: tensordiagonal(expr3.reshape(3, 3, 9), [1, 2])) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/test_immutable_ndim_array.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/test_immutable_ndim_array.py new file mode 100644 index 0000000000000000000000000000000000000000..c6bed4b605c424284b4752592b03b13a9178aac8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/test_immutable_ndim_array.py @@ -0,0 +1,452 @@ +from copy import copy + +from sympy.tensor.array.dense_ndim_array import ImmutableDenseNDimArray +from sympy.core.containers import Dict +from sympy.core.function import diff +from sympy.core.numbers import Rational +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.matrices import SparseMatrix +from sympy.tensor.indexed import (Indexed, IndexedBase) +from sympy.matrices import Matrix +from sympy.tensor.array.sparse_ndim_array import ImmutableSparseNDimArray +from sympy.testing.pytest import raises + + +def test_ndim_array_initiation(): + arr_with_no_elements = ImmutableDenseNDimArray([], shape=(0,)) + assert len(arr_with_no_elements) == 0 + assert arr_with_no_elements.rank() == 1 + + raises(ValueError, lambda: ImmutableDenseNDimArray([0], shape=(0,))) + raises(ValueError, lambda: ImmutableDenseNDimArray([1, 2, 3], shape=(0,))) + raises(ValueError, lambda: ImmutableDenseNDimArray([], shape=())) + + raises(ValueError, lambda: ImmutableSparseNDimArray([0], shape=(0,))) + raises(ValueError, lambda: ImmutableSparseNDimArray([1, 2, 3], shape=(0,))) + raises(ValueError, lambda: ImmutableSparseNDimArray([], shape=())) + + arr_with_one_element = ImmutableDenseNDimArray([23]) + assert len(arr_with_one_element) == 1 + assert arr_with_one_element[0] == 23 + assert arr_with_one_element[:] == ImmutableDenseNDimArray([23]) + assert arr_with_one_element.rank() == 1 + + arr_with_symbol_element = ImmutableDenseNDimArray([Symbol('x')]) + assert len(arr_with_symbol_element) == 1 + assert arr_with_symbol_element[0] == Symbol('x') + assert arr_with_symbol_element[:] == ImmutableDenseNDimArray([Symbol('x')]) + assert arr_with_symbol_element.rank() == 1 + + number5 = 5 + vector = ImmutableDenseNDimArray.zeros(number5) + assert len(vector) == number5 + assert vector.shape == (number5,) + assert vector.rank() == 1 + + vector = ImmutableSparseNDimArray.zeros(number5) + assert len(vector) == number5 + assert vector.shape == (number5,) + assert vector._sparse_array == Dict() + assert vector.rank() == 1 + + n_dim_array = ImmutableDenseNDimArray(range(3**4), (3, 3, 3, 3,)) + assert len(n_dim_array) == 3 * 3 * 3 * 3 + assert n_dim_array.shape == (3, 3, 3, 3) + assert n_dim_array.rank() == 4 + + array_shape = (3, 3, 3, 3) + sparse_array = ImmutableSparseNDimArray.zeros(*array_shape) + assert len(sparse_array._sparse_array) == 0 + assert len(sparse_array) == 3 * 3 * 3 * 3 + assert n_dim_array.shape == array_shape + assert n_dim_array.rank() == 4 + + one_dim_array = ImmutableDenseNDimArray([2, 3, 1]) + assert len(one_dim_array) == 3 + assert one_dim_array.shape == (3,) + assert one_dim_array.rank() == 1 + assert one_dim_array.tolist() == [2, 3, 1] + + shape = (3, 3) + array_with_many_args = ImmutableSparseNDimArray.zeros(*shape) + assert len(array_with_many_args) == 3 * 3 + assert array_with_many_args.shape == shape + assert array_with_many_args[0, 0] == 0 + assert array_with_many_args.rank() == 2 + + shape = (int(3), int(3)) + array_with_long_shape = ImmutableSparseNDimArray.zeros(*shape) + assert len(array_with_long_shape) == 3 * 3 + assert array_with_long_shape.shape == shape + assert array_with_long_shape[int(0), int(0)] == 0 + assert array_with_long_shape.rank() == 2 + + vector_with_long_shape = ImmutableDenseNDimArray(range(5), int(5)) + assert len(vector_with_long_shape) == 5 + assert vector_with_long_shape.shape == (int(5),) + assert vector_with_long_shape.rank() == 1 + raises(ValueError, lambda: vector_with_long_shape[int(5)]) + + from sympy.abc import x + for ArrayType in [ImmutableDenseNDimArray, ImmutableSparseNDimArray]: + rank_zero_array = ArrayType(x) + assert len(rank_zero_array) == 1 + assert rank_zero_array.shape == () + assert rank_zero_array.rank() == 0 + assert rank_zero_array[()] == x + raises(ValueError, lambda: rank_zero_array[0]) + + +def test_reshape(): + array = ImmutableDenseNDimArray(range(50), 50) + assert array.shape == (50,) + assert array.rank() == 1 + + array = array.reshape(5, 5, 2) + assert array.shape == (5, 5, 2) + assert array.rank() == 3 + assert len(array) == 50 + + +def test_getitem(): + for ArrayType in [ImmutableDenseNDimArray, ImmutableSparseNDimArray]: + array = ArrayType(range(24)).reshape(2, 3, 4) + assert array.tolist() == [[[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]], [[12, 13, 14, 15], [16, 17, 18, 19], [20, 21, 22, 23]]] + assert array[0] == ArrayType([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]]) + assert array[0, 0] == ArrayType([0, 1, 2, 3]) + value = 0 + for i in range(2): + for j in range(3): + for k in range(4): + assert array[i, j, k] == value + value += 1 + + raises(ValueError, lambda: array[3, 4, 5]) + raises(ValueError, lambda: array[3, 4, 5, 6]) + raises(ValueError, lambda: array[3, 4, 5, 3:4]) + + +def test_iterator(): + array = ImmutableDenseNDimArray(range(4), (2, 2)) + assert array[0] == ImmutableDenseNDimArray([0, 1]) + assert array[1] == ImmutableDenseNDimArray([2, 3]) + + array = array.reshape(4) + j = 0 + for i in array: + assert i == j + j += 1 + + +def test_sparse(): + sparse_array = ImmutableSparseNDimArray([0, 0, 0, 1], (2, 2)) + assert len(sparse_array) == 2 * 2 + # dictionary where all data is, only non-zero entries are actually stored: + assert len(sparse_array._sparse_array) == 1 + + assert sparse_array.tolist() == [[0, 0], [0, 1]] + + for i, j in zip(sparse_array, [[0, 0], [0, 1]]): + assert i == ImmutableSparseNDimArray(j) + + def sparse_assignment(): + sparse_array[0, 0] = 123 + + assert len(sparse_array._sparse_array) == 1 + raises(TypeError, sparse_assignment) + assert len(sparse_array._sparse_array) == 1 + assert sparse_array[0, 0] == 0 + assert sparse_array/0 == ImmutableSparseNDimArray([[S.NaN, S.NaN], [S.NaN, S.ComplexInfinity]], (2, 2)) + + # test for large scale sparse array + # equality test + assert ImmutableSparseNDimArray.zeros(100000, 200000) == ImmutableSparseNDimArray.zeros(100000, 200000) + + # __mul__ and __rmul__ + a = ImmutableSparseNDimArray({200001: 1}, (100000, 200000)) + assert a * 3 == ImmutableSparseNDimArray({200001: 3}, (100000, 200000)) + assert 3 * a == ImmutableSparseNDimArray({200001: 3}, (100000, 200000)) + assert a * 0 == ImmutableSparseNDimArray({}, (100000, 200000)) + assert 0 * a == ImmutableSparseNDimArray({}, (100000, 200000)) + + # __truediv__ + assert a/3 == ImmutableSparseNDimArray({200001: Rational(1, 3)}, (100000, 200000)) + + # __neg__ + assert -a == ImmutableSparseNDimArray({200001: -1}, (100000, 200000)) + + +def test_calculation(): + + a = ImmutableDenseNDimArray([1]*9, (3, 3)) + b = ImmutableDenseNDimArray([9]*9, (3, 3)) + + c = a + b + for i in c: + assert i == ImmutableDenseNDimArray([10, 10, 10]) + + assert c == ImmutableDenseNDimArray([10]*9, (3, 3)) + assert c == ImmutableSparseNDimArray([10]*9, (3, 3)) + + c = b - a + for i in c: + assert i == ImmutableDenseNDimArray([8, 8, 8]) + + assert c == ImmutableDenseNDimArray([8]*9, (3, 3)) + assert c == ImmutableSparseNDimArray([8]*9, (3, 3)) + + +def test_ndim_array_converting(): + dense_array = ImmutableDenseNDimArray([1, 2, 3, 4], (2, 2)) + alist = dense_array.tolist() + + assert alist == [[1, 2], [3, 4]] + + matrix = dense_array.tomatrix() + assert (isinstance(matrix, Matrix)) + + for i in range(len(dense_array)): + assert dense_array[dense_array._get_tuple_index(i)] == matrix[i] + assert matrix.shape == dense_array.shape + + assert ImmutableDenseNDimArray(matrix) == dense_array + assert ImmutableDenseNDimArray(matrix.as_immutable()) == dense_array + assert ImmutableDenseNDimArray(matrix.as_mutable()) == dense_array + + sparse_array = ImmutableSparseNDimArray([1, 2, 3, 4], (2, 2)) + alist = sparse_array.tolist() + + assert alist == [[1, 2], [3, 4]] + + matrix = sparse_array.tomatrix() + assert(isinstance(matrix, SparseMatrix)) + + for i in range(len(sparse_array)): + assert sparse_array[sparse_array._get_tuple_index(i)] == matrix[i] + assert matrix.shape == sparse_array.shape + + assert ImmutableSparseNDimArray(matrix) == sparse_array + assert ImmutableSparseNDimArray(matrix.as_immutable()) == sparse_array + assert ImmutableSparseNDimArray(matrix.as_mutable()) == sparse_array + + +def test_converting_functions(): + arr_list = [1, 2, 3, 4] + arr_matrix = Matrix(((1, 2), (3, 4))) + + # list + arr_ndim_array = ImmutableDenseNDimArray(arr_list, (2, 2)) + assert (isinstance(arr_ndim_array, ImmutableDenseNDimArray)) + assert arr_matrix.tolist() == arr_ndim_array.tolist() + + # Matrix + arr_ndim_array = ImmutableDenseNDimArray(arr_matrix) + assert (isinstance(arr_ndim_array, ImmutableDenseNDimArray)) + assert arr_matrix.tolist() == arr_ndim_array.tolist() + assert arr_matrix.shape == arr_ndim_array.shape + + +def test_equality(): + first_list = [1, 2, 3, 4] + second_list = [1, 2, 3, 4] + third_list = [4, 3, 2, 1] + assert first_list == second_list + assert first_list != third_list + + first_ndim_array = ImmutableDenseNDimArray(first_list, (2, 2)) + second_ndim_array = ImmutableDenseNDimArray(second_list, (2, 2)) + fourth_ndim_array = ImmutableDenseNDimArray(first_list, (2, 2)) + + assert first_ndim_array == second_ndim_array + + def assignment_attempt(a): + a[0, 0] = 0 + + raises(TypeError, lambda: assignment_attempt(second_ndim_array)) + assert first_ndim_array == second_ndim_array + assert first_ndim_array == fourth_ndim_array + + +def test_arithmetic(): + a = ImmutableDenseNDimArray([3 for i in range(9)], (3, 3)) + b = ImmutableDenseNDimArray([7 for i in range(9)], (3, 3)) + + c1 = a + b + c2 = b + a + assert c1 == c2 + + d1 = a - b + d2 = b - a + assert d1 == d2 * (-1) + + e1 = a * 5 + e2 = 5 * a + e3 = copy(a) + e3 *= 5 + assert e1 == e2 == e3 + + f1 = a / 5 + f2 = copy(a) + f2 /= 5 + assert f1 == f2 + assert f1[0, 0] == f1[0, 1] == f1[0, 2] == f1[1, 0] == f1[1, 1] == \ + f1[1, 2] == f1[2, 0] == f1[2, 1] == f1[2, 2] == Rational(3, 5) + + assert type(a) == type(b) == type(c1) == type(c2) == type(d1) == type(d2) \ + == type(e1) == type(e2) == type(e3) == type(f1) + + z0 = -a + assert z0 == ImmutableDenseNDimArray([-3 for i in range(9)], (3, 3)) + + +def test_higher_dimenions(): + m3 = ImmutableDenseNDimArray(range(10, 34), (2, 3, 4)) + + assert m3.tolist() == [[[10, 11, 12, 13], + [14, 15, 16, 17], + [18, 19, 20, 21]], + + [[22, 23, 24, 25], + [26, 27, 28, 29], + [30, 31, 32, 33]]] + + assert m3._get_tuple_index(0) == (0, 0, 0) + assert m3._get_tuple_index(1) == (0, 0, 1) + assert m3._get_tuple_index(4) == (0, 1, 0) + assert m3._get_tuple_index(12) == (1, 0, 0) + + assert str(m3) == '[[[10, 11, 12, 13], [14, 15, 16, 17], [18, 19, 20, 21]], [[22, 23, 24, 25], [26, 27, 28, 29], [30, 31, 32, 33]]]' + + m3_rebuilt = ImmutableDenseNDimArray([[[10, 11, 12, 13], [14, 15, 16, 17], [18, 19, 20, 21]], [[22, 23, 24, 25], [26, 27, 28, 29], [30, 31, 32, 33]]]) + assert m3 == m3_rebuilt + + m3_other = ImmutableDenseNDimArray([[[10, 11, 12, 13], [14, 15, 16, 17], [18, 19, 20, 21]], [[22, 23, 24, 25], [26, 27, 28, 29], [30, 31, 32, 33]]], (2, 3, 4)) + + assert m3 == m3_other + + +def test_rebuild_immutable_arrays(): + sparr = ImmutableSparseNDimArray(range(10, 34), (2, 3, 4)) + densarr = ImmutableDenseNDimArray(range(10, 34), (2, 3, 4)) + + assert sparr == sparr.func(*sparr.args) + assert densarr == densarr.func(*densarr.args) + + +def test_slices(): + md = ImmutableDenseNDimArray(range(10, 34), (2, 3, 4)) + + assert md[:] == ImmutableDenseNDimArray(range(10, 34), (2, 3, 4)) + assert md[:, :, 0].tomatrix() == Matrix([[10, 14, 18], [22, 26, 30]]) + assert md[0, 1:2, :].tomatrix() == Matrix([[14, 15, 16, 17]]) + assert md[0, 1:3, :].tomatrix() == Matrix([[14, 15, 16, 17], [18, 19, 20, 21]]) + assert md[:, :, :] == md + + sd = ImmutableSparseNDimArray(range(10, 34), (2, 3, 4)) + assert sd == ImmutableSparseNDimArray(md) + + assert sd[:] == ImmutableSparseNDimArray(range(10, 34), (2, 3, 4)) + assert sd[:, :, 0].tomatrix() == Matrix([[10, 14, 18], [22, 26, 30]]) + assert sd[0, 1:2, :].tomatrix() == Matrix([[14, 15, 16, 17]]) + assert sd[0, 1:3, :].tomatrix() == Matrix([[14, 15, 16, 17], [18, 19, 20, 21]]) + assert sd[:, :, :] == sd + + +def test_diff_and_applyfunc(): + from sympy.abc import x, y, z + md = ImmutableDenseNDimArray([[x, y], [x*z, x*y*z]]) + assert md.diff(x) == ImmutableDenseNDimArray([[1, 0], [z, y*z]]) + assert diff(md, x) == ImmutableDenseNDimArray([[1, 0], [z, y*z]]) + + sd = ImmutableSparseNDimArray(md) + assert sd == ImmutableSparseNDimArray([x, y, x*z, x*y*z], (2, 2)) + assert sd.diff(x) == ImmutableSparseNDimArray([[1, 0], [z, y*z]]) + assert diff(sd, x) == ImmutableSparseNDimArray([[1, 0], [z, y*z]]) + + mdn = md.applyfunc(lambda x: x*3) + assert mdn == ImmutableDenseNDimArray([[3*x, 3*y], [3*x*z, 3*x*y*z]]) + assert md != mdn + + sdn = sd.applyfunc(lambda x: x/2) + assert sdn == ImmutableSparseNDimArray([[x/2, y/2], [x*z/2, x*y*z/2]]) + assert sd != sdn + + sdp = sd.applyfunc(lambda x: x+1) + assert sdp == ImmutableSparseNDimArray([[x + 1, y + 1], [x*z + 1, x*y*z + 1]]) + assert sd != sdp + + +def test_op_priority(): + from sympy.abc import x + md = ImmutableDenseNDimArray([1, 2, 3]) + e1 = (1+x)*md + e2 = md*(1+x) + assert e1 == ImmutableDenseNDimArray([1+x, 2+2*x, 3+3*x]) + assert e1 == e2 + + sd = ImmutableSparseNDimArray([1, 2, 3]) + e3 = (1+x)*sd + e4 = sd*(1+x) + assert e3 == ImmutableDenseNDimArray([1+x, 2+2*x, 3+3*x]) + assert e3 == e4 + + +def test_symbolic_indexing(): + x, y, z, w = symbols("x y z w") + M = ImmutableDenseNDimArray([[x, y], [z, w]]) + i, j = symbols("i, j") + Mij = M[i, j] + assert isinstance(Mij, Indexed) + Ms = ImmutableSparseNDimArray([[2, 3*x], [4, 5]]) + msij = Ms[i, j] + assert isinstance(msij, Indexed) + for oi, oj in [(0, 0), (0, 1), (1, 0), (1, 1)]: + assert Mij.subs({i: oi, j: oj}) == M[oi, oj] + assert msij.subs({i: oi, j: oj}) == Ms[oi, oj] + A = IndexedBase("A", (0, 2)) + assert A[0, 0].subs(A, M) == x + assert A[i, j].subs(A, M) == M[i, j] + assert M[i, j].subs(M, A) == A[i, j] + + assert isinstance(M[3 * i - 2, j], Indexed) + assert M[3 * i - 2, j].subs({i: 1, j: 0}) == M[1, 0] + assert isinstance(M[i, 0], Indexed) + assert M[i, 0].subs(i, 0) == M[0, 0] + assert M[0, i].subs(i, 1) == M[0, 1] + + assert M[i, j].diff(x) == ImmutableDenseNDimArray([[1, 0], [0, 0]])[i, j] + assert Ms[i, j].diff(x) == ImmutableSparseNDimArray([[0, 3], [0, 0]])[i, j] + + Mo = ImmutableDenseNDimArray([1, 2, 3]) + assert Mo[i].subs(i, 1) == 2 + Mos = ImmutableSparseNDimArray([1, 2, 3]) + assert Mos[i].subs(i, 1) == 2 + + raises(ValueError, lambda: M[i, 2]) + raises(ValueError, lambda: M[i, -1]) + raises(ValueError, lambda: M[2, i]) + raises(ValueError, lambda: M[-1, i]) + + raises(ValueError, lambda: Ms[i, 2]) + raises(ValueError, lambda: Ms[i, -1]) + raises(ValueError, lambda: Ms[2, i]) + raises(ValueError, lambda: Ms[-1, i]) + + +def test_issue_12665(): + # Testing Python 3 hash of immutable arrays: + arr = ImmutableDenseNDimArray([1, 2, 3]) + # This should NOT raise an exception: + hash(arr) + + +def test_zeros_without_shape(): + arr = ImmutableDenseNDimArray.zeros() + assert arr == ImmutableDenseNDimArray(0) + +def test_issue_21870(): + a0 = ImmutableDenseNDimArray(0) + assert a0.rank() == 0 + a1 = ImmutableDenseNDimArray(a0) + assert a1.rank() == 0 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/test_mutable_ndim_array.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/test_mutable_ndim_array.py new file mode 100644 index 0000000000000000000000000000000000000000..9a232f399bbc0639d326217975fb0a12e645a984 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/test_mutable_ndim_array.py @@ -0,0 +1,374 @@ +from copy import copy + +from sympy.tensor.array.dense_ndim_array import MutableDenseNDimArray +from sympy.core.function import diff +from sympy.core.numbers import Rational +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.core.sympify import sympify +from sympy.matrices import SparseMatrix +from sympy.matrices import Matrix +from sympy.tensor.array.sparse_ndim_array import MutableSparseNDimArray +from sympy.testing.pytest import raises + + +def test_ndim_array_initiation(): + arr_with_one_element = MutableDenseNDimArray([23]) + assert len(arr_with_one_element) == 1 + assert arr_with_one_element[0] == 23 + assert arr_with_one_element.rank() == 1 + raises(ValueError, lambda: arr_with_one_element[1]) + + arr_with_symbol_element = MutableDenseNDimArray([Symbol('x')]) + assert len(arr_with_symbol_element) == 1 + assert arr_with_symbol_element[0] == Symbol('x') + assert arr_with_symbol_element.rank() == 1 + + number5 = 5 + vector = MutableDenseNDimArray.zeros(number5) + assert len(vector) == number5 + assert vector.shape == (number5,) + assert vector.rank() == 1 + raises(ValueError, lambda: arr_with_one_element[5]) + + vector = MutableSparseNDimArray.zeros(number5) + assert len(vector) == number5 + assert vector.shape == (number5,) + assert vector._sparse_array == {} + assert vector.rank() == 1 + + n_dim_array = MutableDenseNDimArray(range(3**4), (3, 3, 3, 3,)) + assert len(n_dim_array) == 3 * 3 * 3 * 3 + assert n_dim_array.shape == (3, 3, 3, 3) + assert n_dim_array.rank() == 4 + raises(ValueError, lambda: n_dim_array[0, 0, 0, 3]) + raises(ValueError, lambda: n_dim_array[3, 0, 0, 0]) + raises(ValueError, lambda: n_dim_array[3**4]) + + array_shape = (3, 3, 3, 3) + sparse_array = MutableSparseNDimArray.zeros(*array_shape) + assert len(sparse_array._sparse_array) == 0 + assert len(sparse_array) == 3 * 3 * 3 * 3 + assert n_dim_array.shape == array_shape + assert n_dim_array.rank() == 4 + + one_dim_array = MutableDenseNDimArray([2, 3, 1]) + assert len(one_dim_array) == 3 + assert one_dim_array.shape == (3,) + assert one_dim_array.rank() == 1 + assert one_dim_array.tolist() == [2, 3, 1] + + shape = (3, 3) + array_with_many_args = MutableSparseNDimArray.zeros(*shape) + assert len(array_with_many_args) == 3 * 3 + assert array_with_many_args.shape == shape + assert array_with_many_args[0, 0] == 0 + assert array_with_many_args.rank() == 2 + + shape = (int(3), int(3)) + array_with_long_shape = MutableSparseNDimArray.zeros(*shape) + assert len(array_with_long_shape) == 3 * 3 + assert array_with_long_shape.shape == shape + assert array_with_long_shape[int(0), int(0)] == 0 + assert array_with_long_shape.rank() == 2 + + vector_with_long_shape = MutableDenseNDimArray(range(5), int(5)) + assert len(vector_with_long_shape) == 5 + assert vector_with_long_shape.shape == (int(5),) + assert vector_with_long_shape.rank() == 1 + raises(ValueError, lambda: vector_with_long_shape[int(5)]) + + from sympy.abc import x + for ArrayType in [MutableDenseNDimArray, MutableSparseNDimArray]: + rank_zero_array = ArrayType(x) + assert len(rank_zero_array) == 1 + assert rank_zero_array.shape == () + assert rank_zero_array.rank() == 0 + assert rank_zero_array[()] == x + raises(ValueError, lambda: rank_zero_array[0]) + +def test_sympify(): + from sympy.abc import x, y, z, t + arr = MutableDenseNDimArray([[x, y], [1, z*t]]) + arr_other = sympify(arr) + assert arr_other.shape == (2, 2) + assert arr_other == arr + + +def test_reshape(): + array = MutableDenseNDimArray(range(50), 50) + assert array.shape == (50,) + assert array.rank() == 1 + + array = array.reshape(5, 5, 2) + assert array.shape == (5, 5, 2) + assert array.rank() == 3 + assert len(array) == 50 + + +def test_iterator(): + array = MutableDenseNDimArray(range(4), (2, 2)) + assert array[0] == MutableDenseNDimArray([0, 1]) + assert array[1] == MutableDenseNDimArray([2, 3]) + + array = array.reshape(4) + j = 0 + for i in array: + assert i == j + j += 1 + + +def test_getitem(): + for ArrayType in [MutableDenseNDimArray, MutableSparseNDimArray]: + array = ArrayType(range(24)).reshape(2, 3, 4) + assert array.tolist() == [[[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]], [[12, 13, 14, 15], [16, 17, 18, 19], [20, 21, 22, 23]]] + assert array[0] == ArrayType([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]]) + assert array[0, 0] == ArrayType([0, 1, 2, 3]) + value = 0 + for i in range(2): + for j in range(3): + for k in range(4): + assert array[i, j, k] == value + value += 1 + + raises(ValueError, lambda: array[3, 4, 5]) + raises(ValueError, lambda: array[3, 4, 5, 6]) + raises(ValueError, lambda: array[3, 4, 5, 3:4]) + + +def test_sparse(): + sparse_array = MutableSparseNDimArray([0, 0, 0, 1], (2, 2)) + assert len(sparse_array) == 2 * 2 + # dictionary where all data is, only non-zero entries are actually stored: + assert len(sparse_array._sparse_array) == 1 + + assert sparse_array.tolist() == [[0, 0], [0, 1]] + + for i, j in zip(sparse_array, [[0, 0], [0, 1]]): + assert i == MutableSparseNDimArray(j) + + sparse_array[0, 0] = 123 + assert len(sparse_array._sparse_array) == 2 + assert sparse_array[0, 0] == 123 + assert sparse_array/0 == MutableSparseNDimArray([[S.ComplexInfinity, S.NaN], [S.NaN, S.ComplexInfinity]], (2, 2)) + + # when element in sparse array become zero it will disappear from + # dictionary + sparse_array[0, 0] = 0 + assert len(sparse_array._sparse_array) == 1 + sparse_array[1, 1] = 0 + assert len(sparse_array._sparse_array) == 0 + assert sparse_array[0, 0] == 0 + + # test for large scale sparse array + # equality test + a = MutableSparseNDimArray.zeros(100000, 200000) + b = MutableSparseNDimArray.zeros(100000, 200000) + assert a == b + a[1, 1] = 1 + b[1, 1] = 2 + assert a != b + + # __mul__ and __rmul__ + assert a * 3 == MutableSparseNDimArray({200001: 3}, (100000, 200000)) + assert 3 * a == MutableSparseNDimArray({200001: 3}, (100000, 200000)) + assert a * 0 == MutableSparseNDimArray({}, (100000, 200000)) + assert 0 * a == MutableSparseNDimArray({}, (100000, 200000)) + + # __truediv__ + assert a/3 == MutableSparseNDimArray({200001: Rational(1, 3)}, (100000, 200000)) + + # __neg__ + assert -a == MutableSparseNDimArray({200001: -1}, (100000, 200000)) + + +def test_calculation(): + + a = MutableDenseNDimArray([1]*9, (3, 3)) + b = MutableDenseNDimArray([9]*9, (3, 3)) + + c = a + b + for i in c: + assert i == MutableDenseNDimArray([10, 10, 10]) + + assert c == MutableDenseNDimArray([10]*9, (3, 3)) + assert c == MutableSparseNDimArray([10]*9, (3, 3)) + + c = b - a + for i in c: + assert i == MutableSparseNDimArray([8, 8, 8]) + + assert c == MutableDenseNDimArray([8]*9, (3, 3)) + assert c == MutableSparseNDimArray([8]*9, (3, 3)) + + +def test_ndim_array_converting(): + dense_array = MutableDenseNDimArray([1, 2, 3, 4], (2, 2)) + alist = dense_array.tolist() + + assert alist == [[1, 2], [3, 4]] + + matrix = dense_array.tomatrix() + assert (isinstance(matrix, Matrix)) + + for i in range(len(dense_array)): + assert dense_array[dense_array._get_tuple_index(i)] == matrix[i] + assert matrix.shape == dense_array.shape + + assert MutableDenseNDimArray(matrix) == dense_array + assert MutableDenseNDimArray(matrix.as_immutable()) == dense_array + assert MutableDenseNDimArray(matrix.as_mutable()) == dense_array + + sparse_array = MutableSparseNDimArray([1, 2, 3, 4], (2, 2)) + alist = sparse_array.tolist() + + assert alist == [[1, 2], [3, 4]] + + matrix = sparse_array.tomatrix() + assert(isinstance(matrix, SparseMatrix)) + + for i in range(len(sparse_array)): + assert sparse_array[sparse_array._get_tuple_index(i)] == matrix[i] + assert matrix.shape == sparse_array.shape + + assert MutableSparseNDimArray(matrix) == sparse_array + assert MutableSparseNDimArray(matrix.as_immutable()) == sparse_array + assert MutableSparseNDimArray(matrix.as_mutable()) == sparse_array + + +def test_converting_functions(): + arr_list = [1, 2, 3, 4] + arr_matrix = Matrix(((1, 2), (3, 4))) + + # list + arr_ndim_array = MutableDenseNDimArray(arr_list, (2, 2)) + assert (isinstance(arr_ndim_array, MutableDenseNDimArray)) + assert arr_matrix.tolist() == arr_ndim_array.tolist() + + # Matrix + arr_ndim_array = MutableDenseNDimArray(arr_matrix) + assert (isinstance(arr_ndim_array, MutableDenseNDimArray)) + assert arr_matrix.tolist() == arr_ndim_array.tolist() + assert arr_matrix.shape == arr_ndim_array.shape + + +def test_equality(): + first_list = [1, 2, 3, 4] + second_list = [1, 2, 3, 4] + third_list = [4, 3, 2, 1] + assert first_list == second_list + assert first_list != third_list + + first_ndim_array = MutableDenseNDimArray(first_list, (2, 2)) + second_ndim_array = MutableDenseNDimArray(second_list, (2, 2)) + third_ndim_array = MutableDenseNDimArray(third_list, (2, 2)) + fourth_ndim_array = MutableDenseNDimArray(first_list, (2, 2)) + + assert first_ndim_array == second_ndim_array + second_ndim_array[0, 0] = 0 + assert first_ndim_array != second_ndim_array + assert first_ndim_array != third_ndim_array + assert first_ndim_array == fourth_ndim_array + + +def test_arithmetic(): + a = MutableDenseNDimArray([3 for i in range(9)], (3, 3)) + b = MutableDenseNDimArray([7 for i in range(9)], (3, 3)) + + c1 = a + b + c2 = b + a + assert c1 == c2 + + d1 = a - b + d2 = b - a + assert d1 == d2 * (-1) + + e1 = a * 5 + e2 = 5 * a + e3 = copy(a) + e3 *= 5 + assert e1 == e2 == e3 + + f1 = a / 5 + f2 = copy(a) + f2 /= 5 + assert f1 == f2 + assert f1[0, 0] == f1[0, 1] == f1[0, 2] == f1[1, 0] == f1[1, 1] == \ + f1[1, 2] == f1[2, 0] == f1[2, 1] == f1[2, 2] == Rational(3, 5) + + assert type(a) == type(b) == type(c1) == type(c2) == type(d1) == type(d2) \ + == type(e1) == type(e2) == type(e3) == type(f1) + + z0 = -a + assert z0 == MutableDenseNDimArray([-3 for i in range(9)], (3, 3)) + + +def test_higher_dimenions(): + m3 = MutableDenseNDimArray(range(10, 34), (2, 3, 4)) + + assert m3.tolist() == [[[10, 11, 12, 13], + [14, 15, 16, 17], + [18, 19, 20, 21]], + + [[22, 23, 24, 25], + [26, 27, 28, 29], + [30, 31, 32, 33]]] + + assert m3._get_tuple_index(0) == (0, 0, 0) + assert m3._get_tuple_index(1) == (0, 0, 1) + assert m3._get_tuple_index(4) == (0, 1, 0) + assert m3._get_tuple_index(12) == (1, 0, 0) + + assert str(m3) == '[[[10, 11, 12, 13], [14, 15, 16, 17], [18, 19, 20, 21]], [[22, 23, 24, 25], [26, 27, 28, 29], [30, 31, 32, 33]]]' + + m3_rebuilt = MutableDenseNDimArray([[[10, 11, 12, 13], [14, 15, 16, 17], [18, 19, 20, 21]], [[22, 23, 24, 25], [26, 27, 28, 29], [30, 31, 32, 33]]]) + assert m3 == m3_rebuilt + + m3_other = MutableDenseNDimArray([[[10, 11, 12, 13], [14, 15, 16, 17], [18, 19, 20, 21]], [[22, 23, 24, 25], [26, 27, 28, 29], [30, 31, 32, 33]]], (2, 3, 4)) + + assert m3 == m3_other + + +def test_slices(): + md = MutableDenseNDimArray(range(10, 34), (2, 3, 4)) + + assert md[:] == MutableDenseNDimArray(range(10, 34), (2, 3, 4)) + assert md[:, :, 0].tomatrix() == Matrix([[10, 14, 18], [22, 26, 30]]) + assert md[0, 1:2, :].tomatrix() == Matrix([[14, 15, 16, 17]]) + assert md[0, 1:3, :].tomatrix() == Matrix([[14, 15, 16, 17], [18, 19, 20, 21]]) + assert md[:, :, :] == md + + sd = MutableSparseNDimArray(range(10, 34), (2, 3, 4)) + assert sd == MutableSparseNDimArray(md) + + assert sd[:] == MutableSparseNDimArray(range(10, 34), (2, 3, 4)) + assert sd[:, :, 0].tomatrix() == Matrix([[10, 14, 18], [22, 26, 30]]) + assert sd[0, 1:2, :].tomatrix() == Matrix([[14, 15, 16, 17]]) + assert sd[0, 1:3, :].tomatrix() == Matrix([[14, 15, 16, 17], [18, 19, 20, 21]]) + assert sd[:, :, :] == sd + + +def test_slices_assign(): + a = MutableDenseNDimArray(range(12), shape=(4, 3)) + b = MutableSparseNDimArray(range(12), shape=(4, 3)) + + for i in [a, b]: + assert i.tolist() == [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]] + i[0, :] = [2, 2, 2] + assert i.tolist() == [[2, 2, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]] + i[0, 1:] = [8, 8] + assert i.tolist() == [[2, 8, 8], [3, 4, 5], [6, 7, 8], [9, 10, 11]] + i[1:3, 1] = [20, 44] + assert i.tolist() == [[2, 8, 8], [3, 20, 5], [6, 44, 8], [9, 10, 11]] + + +def test_diff(): + from sympy.abc import x, y, z + md = MutableDenseNDimArray([[x, y], [x*z, x*y*z]]) + assert md.diff(x) == MutableDenseNDimArray([[1, 0], [z, y*z]]) + assert diff(md, x) == MutableDenseNDimArray([[1, 0], [z, y*z]]) + + sd = MutableSparseNDimArray(md) + assert sd == MutableSparseNDimArray([x, y, x*z, x*y*z], (2, 2)) + assert sd.diff(x) == MutableSparseNDimArray([[1, 0], [z, y*z]]) + assert diff(sd, x) == MutableSparseNDimArray([[1, 0], [z, y*z]]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/test_ndim_array.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/test_ndim_array.py new file mode 100644 index 0000000000000000000000000000000000000000..7ff9b032631c01272c00478e4cdf0dcbc6997990 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/test_ndim_array.py @@ -0,0 +1,73 @@ +from sympy.testing.pytest import raises +from sympy.functions.elementary.trigonometric import sin, cos +from sympy.matrices.dense import Matrix +from sympy.simplify import simplify +from sympy.tensor.array import Array +from sympy.tensor.array.dense_ndim_array import ( + ImmutableDenseNDimArray, MutableDenseNDimArray) +from sympy.tensor.array.sparse_ndim_array import ( + ImmutableSparseNDimArray, MutableSparseNDimArray) + +from sympy.abc import x, y + +mutable_array_types = [ + MutableDenseNDimArray, + MutableSparseNDimArray +] + +array_types = [ + ImmutableDenseNDimArray, + ImmutableSparseNDimArray, + MutableDenseNDimArray, + MutableSparseNDimArray +] + + +def test_array_negative_indices(): + for ArrayType in array_types: + test_array = ArrayType([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]) + assert test_array[:, -1] == Array([5, 10]) + assert test_array[:, -2] == Array([4, 9]) + assert test_array[:, -3] == Array([3, 8]) + assert test_array[:, -4] == Array([2, 7]) + assert test_array[:, -5] == Array([1, 6]) + assert test_array[:, 0] == Array([1, 6]) + assert test_array[:, 1] == Array([2, 7]) + assert test_array[:, 2] == Array([3, 8]) + assert test_array[:, 3] == Array([4, 9]) + assert test_array[:, 4] == Array([5, 10]) + + raises(ValueError, lambda: test_array[:, -6]) + raises(ValueError, lambda: test_array[-3, :]) + + assert test_array[-1, -1] == 10 + + +def test_issue_18361(): + A = Array([sin(2 * x) - 2 * sin(x) * cos(x)]) + B = Array([sin(x)**2 + cos(x)**2, 0]) + C = Array([(x + x**2)/(x*sin(y)**2 + x*cos(y)**2), 2*sin(x)*cos(x)]) + assert simplify(A) == Array([0]) + assert simplify(B) == Array([1, 0]) + assert simplify(C) == Array([x + 1, sin(2*x)]) + + +def test_issue_20222(): + A = Array([[1, 2], [3, 4]]) + B = Matrix([[1,2],[3,4]]) + raises(TypeError, lambda: A - B) + + +def test_issue_17851(): + for array_type in array_types: + A = array_type([]) + assert isinstance(A, array_type) + assert A.shape == (0,) + assert list(A) == [] + + +def test_issue_and_18715(): + for array_type in mutable_array_types: + A = array_type([0, 1, 2]) + A[0] += 5 + assert A[0] == 5 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/test_ndim_array_conversions.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/test_ndim_array_conversions.py new file mode 100644 index 0000000000000000000000000000000000000000..f43260ccc636ac461ba0c06dbfcf3fe3a8d5338d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/array/tests/test_ndim_array_conversions.py @@ -0,0 +1,22 @@ +from sympy.tensor.array import (ImmutableDenseNDimArray, + ImmutableSparseNDimArray, MutableDenseNDimArray, MutableSparseNDimArray) +from sympy.abc import x, y, z + + +def test_NDim_array_conv(): + MD = MutableDenseNDimArray([x, y, z]) + MS = MutableSparseNDimArray([x, y, z]) + ID = ImmutableDenseNDimArray([x, y, z]) + IS = ImmutableSparseNDimArray([x, y, z]) + + assert MD.as_immutable() == ID + assert MD.as_mutable() == MD + + assert MS.as_immutable() == IS + assert MS.as_mutable() == MS + + assert ID.as_immutable() == ID + assert ID.as_mutable() == MD + + assert IS.as_immutable() == IS + assert IS.as_mutable() == MS diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/functions.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/functions.py new file mode 100644 index 0000000000000000000000000000000000000000..f14599d69152db1713f21c9dd785683901c5eeb9 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/functions.py @@ -0,0 +1,154 @@ +from collections.abc import Iterable +from functools import singledispatch + +from sympy.core.expr import Expr +from sympy.core.mul import Mul +from sympy.core.singleton import S +from sympy.core.sympify import sympify +from sympy.core.parameters import global_parameters + + +class TensorProduct(Expr): + """ + Generic class for tensor products. + """ + is_number = False + + def __new__(cls, *args, **kwargs): + from sympy.tensor.array import NDimArray, tensorproduct, Array + from sympy.matrices.expressions.matexpr import MatrixExpr + from sympy.matrices.matrixbase import MatrixBase + from sympy.strategies import flatten + + args = [sympify(arg) for arg in args] + evaluate = kwargs.get("evaluate", global_parameters.evaluate) + + if not evaluate: + obj = Expr.__new__(cls, *args) + return obj + + arrays = [] + other = [] + scalar = S.One + for arg in args: + if isinstance(arg, (Iterable, MatrixBase, NDimArray)): + arrays.append(Array(arg)) + elif isinstance(arg, (MatrixExpr,)): + other.append(arg) + else: + scalar *= arg + + coeff = scalar*tensorproduct(*arrays) + if len(other) == 0: + return coeff + if coeff != 1: + newargs = [coeff] + other + else: + newargs = other + obj = Expr.__new__(cls, *newargs, **kwargs) + return flatten(obj) + + def rank(self): + return len(self.shape) + + def _get_args_shapes(self): + from sympy.tensor.array import Array + return [i.shape if hasattr(i, "shape") else Array(i).shape for i in self.args] + + @property + def shape(self): + shape_list = self._get_args_shapes() + return sum(shape_list, ()) + + def __getitem__(self, index): + index = iter(index) + return Mul.fromiter( + arg.__getitem__(tuple(next(index) for i in shp)) + for arg, shp in zip(self.args, self._get_args_shapes()) + ) + + +@singledispatch +def shape(expr): + """ + Return the shape of the *expr* as a tuple. *expr* should represent + suitable object such as matrix or array. + + Parameters + ========== + + expr : SymPy object having ``MatrixKind`` or ``ArrayKind``. + + Raises + ====== + + NoShapeError : Raised when object with wrong kind is passed. + + Examples + ======== + + This function returns the shape of any object representing matrix or array. + + >>> from sympy import shape, Array, ImmutableDenseMatrix, Integral + >>> from sympy.abc import x + >>> A = Array([1, 2]) + >>> shape(A) + (2,) + >>> shape(Integral(A, x)) + (2,) + >>> M = ImmutableDenseMatrix([1, 2]) + >>> shape(M) + (2, 1) + >>> shape(Integral(M, x)) + (2, 1) + + You can support new type by dispatching. + + >>> from sympy import Expr + >>> class NewExpr(Expr): + ... pass + >>> @shape.register(NewExpr) + ... def _(expr): + ... return shape(expr.args[0]) + >>> shape(NewExpr(M)) + (2, 1) + + If unsuitable expression is passed, ``NoShapeError()`` will be raised. + + >>> shape(Integral(x, x)) + Traceback (most recent call last): + ... + sympy.tensor.functions.NoShapeError: shape() called on non-array object: Integral(x, x) + + Notes + ===== + + Array-like classes (such as ``Matrix`` or ``NDimArray``) has ``shape`` + property which returns its shape, but it cannot be used for non-array + classes containing array. This function returns the shape of any + registered object representing array. + + """ + if hasattr(expr, "shape"): + return expr.shape + raise NoShapeError( + "%s does not have shape, or its type is not registered to shape()." % expr) + + +class NoShapeError(Exception): + """ + Raised when ``shape()`` is called on non-array object. + + This error can be imported from ``sympy.tensor.functions``. + + Examples + ======== + + >>> from sympy import shape + >>> from sympy.abc import x + >>> shape(x) + Traceback (most recent call last): + ... + sympy.tensor.functions.NoShapeError: shape() called on non-array object: x + """ + pass diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/index_methods.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/index_methods.py new file mode 100644 index 0000000000000000000000000000000000000000..12f707b60b4ad0bcadc35a222d9abe0cc5e033fc --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/index_methods.py @@ -0,0 +1,469 @@ +"""Module with functions operating on IndexedBase, Indexed and Idx objects + + - Check shape conformance + - Determine indices in resulting expression + + etc. + + Methods in this module could be implemented by calling methods on Expr + objects instead. When things stabilize this could be a useful + refactoring. +""" + +from functools import reduce + +from sympy.core.function import Function +from sympy.functions import exp, Piecewise +from sympy.tensor.indexed import Idx, Indexed +from sympy.utilities import sift + +from collections import OrderedDict + +class IndexConformanceException(Exception): + pass + +def _unique_and_repeated(inds): + """ + Returns the unique and repeated indices. Also note, from the examples given below + that the order of indices is maintained as given in the input. + + Examples + ======== + + >>> from sympy.tensor.index_methods import _unique_and_repeated + >>> _unique_and_repeated([2, 3, 1, 3, 0, 4, 0]) + ([2, 1, 4], [3, 0]) + """ + uniq = OrderedDict() + for i in inds: + if i in uniq: + uniq[i] = 0 + else: + uniq[i] = 1 + return sift(uniq, lambda x: uniq[x], binary=True) + +def _remove_repeated(inds): + """ + Removes repeated objects from sequences + + Returns a set of the unique objects and a tuple of all that have been + removed. + + Examples + ======== + + >>> from sympy.tensor.index_methods import _remove_repeated + >>> l1 = [1, 2, 3, 2] + >>> _remove_repeated(l1) + ({1, 3}, (2,)) + + """ + u, r = _unique_and_repeated(inds) + return set(u), tuple(r) + + +def _get_indices_Mul(expr, return_dummies=False): + """Determine the outer indices of a Mul object. + + Examples + ======== + + >>> from sympy.tensor.index_methods import _get_indices_Mul + >>> from sympy.tensor.indexed import IndexedBase, Idx + >>> i, j, k = map(Idx, ['i', 'j', 'k']) + >>> x = IndexedBase('x') + >>> y = IndexedBase('y') + >>> _get_indices_Mul(x[i, k]*y[j, k]) + ({i, j}, {}) + >>> _get_indices_Mul(x[i, k]*y[j, k], return_dummies=True) + ({i, j}, {}, (k,)) + + """ + + inds = list(map(get_indices, expr.args)) + inds, syms = list(zip(*inds)) + + inds = list(map(list, inds)) + inds = list(reduce(lambda x, y: x + y, inds)) + inds, dummies = _remove_repeated(inds) + + symmetry = {} + for s in syms: + for pair in s: + if pair in symmetry: + symmetry[pair] *= s[pair] + else: + symmetry[pair] = s[pair] + + if return_dummies: + return inds, symmetry, dummies + else: + return inds, symmetry + + +def _get_indices_Pow(expr): + """Determine outer indices of a power or an exponential. + + A power is considered a universal function, so that the indices of a Pow is + just the collection of indices present in the expression. This may be + viewed as a bit inconsistent in the special case: + + x[i]**2 = x[i]*x[i] (1) + + The above expression could have been interpreted as the contraction of x[i] + with itself, but we choose instead to interpret it as a function + + lambda y: y**2 + + applied to each element of x (a universal function in numpy terms). In + order to allow an interpretation of (1) as a contraction, we need + contravariant and covariant Idx subclasses. (FIXME: this is not yet + implemented) + + Expressions in the base or exponent are subject to contraction as usual, + but an index that is present in the exponent, will not be considered + contractable with its own base. Note however, that indices in the same + exponent can be contracted with each other. + + Examples + ======== + + >>> from sympy.tensor.index_methods import _get_indices_Pow + >>> from sympy import Pow, exp, IndexedBase, Idx + >>> A = IndexedBase('A') + >>> x = IndexedBase('x') + >>> i, j, k = map(Idx, ['i', 'j', 'k']) + >>> _get_indices_Pow(exp(A[i, j]*x[j])) + ({i}, {}) + >>> _get_indices_Pow(Pow(x[i], x[i])) + ({i}, {}) + >>> _get_indices_Pow(Pow(A[i, j]*x[j], x[i])) + ({i}, {}) + + """ + base, exp = expr.as_base_exp() + binds, bsyms = get_indices(base) + einds, esyms = get_indices(exp) + + inds = binds | einds + + # FIXME: symmetries from power needs to check special cases, else nothing + symmetries = {} + + return inds, symmetries + + +def _get_indices_Add(expr): + """Determine outer indices of an Add object. + + In a sum, each term must have the same set of outer indices. A valid + expression could be + + x(i)*y(j) - x(j)*y(i) + + But we do not allow expressions like: + + x(i)*y(j) - z(j)*z(j) + + FIXME: Add support for Numpy broadcasting + + Examples + ======== + + >>> from sympy.tensor.index_methods import _get_indices_Add + >>> from sympy.tensor.indexed import IndexedBase, Idx + >>> i, j, k = map(Idx, ['i', 'j', 'k']) + >>> x = IndexedBase('x') + >>> y = IndexedBase('y') + >>> _get_indices_Add(x[i] + x[k]*y[i, k]) + ({i}, {}) + + """ + + inds = list(map(get_indices, expr.args)) + inds, syms = list(zip(*inds)) + + # allow broadcast of scalars + non_scalars = [x for x in inds if x != set()] + if not non_scalars: + return set(), {} + + if not all(x == non_scalars[0] for x in non_scalars[1:]): + raise IndexConformanceException("Indices are not consistent: %s" % expr) + if not reduce(lambda x, y: x != y or y, syms): + symmetries = syms[0] + else: + # FIXME: search for symmetries + symmetries = {} + + return non_scalars[0], symmetries + + +def get_indices(expr): + """Determine the outer indices of expression ``expr`` + + By *outer* we mean indices that are not summation indices. Returns a set + and a dict. The set contains outer indices and the dict contains + information about index symmetries. + + Examples + ======== + + >>> from sympy.tensor.index_methods import get_indices + >>> from sympy import symbols + >>> from sympy.tensor import IndexedBase + >>> x, y, A = map(IndexedBase, ['x', 'y', 'A']) + >>> i, j, a, z = symbols('i j a z', integer=True) + + The indices of the total expression is determined, Repeated indices imply a + summation, for instance the trace of a matrix A: + + >>> get_indices(A[i, i]) + (set(), {}) + + In the case of many terms, the terms are required to have identical + outer indices. Else an IndexConformanceException is raised. + + >>> get_indices(x[i] + A[i, j]*y[j]) + ({i}, {}) + + :Exceptions: + + An IndexConformanceException means that the terms ar not compatible, e.g. + + >>> get_indices(x[i] + y[j]) #doctest: +SKIP + (...) + IndexConformanceException: Indices are not consistent: x(i) + y(j) + + .. warning:: + The concept of *outer* indices applies recursively, starting on the deepest + level. This implies that dummies inside parenthesis are assumed to be + summed first, so that the following expression is handled gracefully: + + >>> get_indices((x[i] + A[i, j]*y[j])*x[j]) + ({i, j}, {}) + + This is correct and may appear convenient, but you need to be careful + with this as SymPy will happily .expand() the product, if requested. The + resulting expression would mix the outer ``j`` with the dummies inside + the parenthesis, which makes it a different expression. To be on the + safe side, it is best to avoid such ambiguities by using unique indices + for all contractions that should be held separate. + + """ + # We call ourself recursively to determine indices of sub expressions. + + # break recursion + if isinstance(expr, Indexed): + c = expr.indices + inds, dummies = _remove_repeated(c) + return inds, {} + elif expr is None: + return set(), {} + elif isinstance(expr, Idx): + return {expr}, {} + elif expr.is_Atom: + return set(), {} + + + # recurse via specialized functions + else: + if expr.is_Mul: + return _get_indices_Mul(expr) + elif expr.is_Add: + return _get_indices_Add(expr) + elif expr.is_Pow or isinstance(expr, exp): + return _get_indices_Pow(expr) + + elif isinstance(expr, Piecewise): + # FIXME: No support for Piecewise yet + return set(), {} + elif isinstance(expr, Function): + # Support ufunc like behaviour by returning indices from arguments. + # Functions do not interpret repeated indices across arguments + # as summation + ind0 = set() + for arg in expr.args: + ind, sym = get_indices(arg) + ind0 |= ind + return ind0, sym + + # this test is expensive, so it should be at the end + elif not expr.has(Indexed): + return set(), {} + raise NotImplementedError( + "FIXME: No specialized handling of type %s" % type(expr)) + + +def get_contraction_structure(expr): + """Determine dummy indices of ``expr`` and describe its structure + + By *dummy* we mean indices that are summation indices. + + The structure of the expression is determined and described as follows: + + 1) A conforming summation of Indexed objects is described with a dict where + the keys are summation indices and the corresponding values are sets + containing all terms for which the summation applies. All Add objects + in the SymPy expression tree are described like this. + + 2) For all nodes in the SymPy expression tree that are *not* of type Add, the + following applies: + + If a node discovers contractions in one of its arguments, the node + itself will be stored as a key in the dict. For that key, the + corresponding value is a list of dicts, each of which is the result of a + recursive call to get_contraction_structure(). The list contains only + dicts for the non-trivial deeper contractions, omitting dicts with None + as the one and only key. + + .. Note:: The presence of expressions among the dictionary keys indicates + multiple levels of index contractions. A nested dict displays nested + contractions and may itself contain dicts from a deeper level. In + practical calculations the summation in the deepest nested level must be + calculated first so that the outer expression can access the resulting + indexed object. + + Examples + ======== + + >>> from sympy.tensor.index_methods import get_contraction_structure + >>> from sympy import default_sort_key + >>> from sympy.tensor import IndexedBase, Idx + >>> x, y, A = map(IndexedBase, ['x', 'y', 'A']) + >>> i, j, k, l = map(Idx, ['i', 'j', 'k', 'l']) + >>> get_contraction_structure(x[i]*y[i] + A[j, j]) + {(i,): {x[i]*y[i]}, (j,): {A[j, j]}} + >>> get_contraction_structure(x[i]*y[j]) + {None: {x[i]*y[j]}} + + A multiplication of contracted factors results in nested dicts representing + the internal contractions. + + >>> d = get_contraction_structure(x[i, i]*y[j, j]) + >>> sorted(d.keys(), key=default_sort_key) + [None, x[i, i]*y[j, j]] + + In this case, the product has no contractions: + + >>> d[None] + {x[i, i]*y[j, j]} + + Factors are contracted "first": + + >>> sorted(d[x[i, i]*y[j, j]], key=default_sort_key) + [{(i,): {x[i, i]}}, {(j,): {y[j, j]}}] + + A parenthesized Add object is also returned as a nested dictionary. The + term containing the parenthesis is a Mul with a contraction among the + arguments, so it will be found as a key in the result. It stores the + dictionary resulting from a recursive call on the Add expression. + + >>> d = get_contraction_structure(x[i]*(y[i] + A[i, j]*x[j])) + >>> sorted(d.keys(), key=default_sort_key) + [(A[i, j]*x[j] + y[i])*x[i], (i,)] + >>> d[(i,)] + {(A[i, j]*x[j] + y[i])*x[i]} + >>> d[x[i]*(A[i, j]*x[j] + y[i])] + [{None: {y[i]}, (j,): {A[i, j]*x[j]}}] + + Powers with contractions in either base or exponent will also be found as + keys in the dictionary, mapping to a list of results from recursive calls: + + >>> d = get_contraction_structure(A[j, j]**A[i, i]) + >>> d[None] + {A[j, j]**A[i, i]} + >>> nested_contractions = d[A[j, j]**A[i, i]] + >>> nested_contractions[0] + {(j,): {A[j, j]}} + >>> nested_contractions[1] + {(i,): {A[i, i]}} + + The description of the contraction structure may appear complicated when + represented with a string in the above examples, but it is easy to iterate + over: + + >>> from sympy import Expr + >>> for key in d: + ... if isinstance(key, Expr): + ... continue + ... for term in d[key]: + ... if term in d: + ... # treat deepest contraction first + ... pass + ... # treat outermost contactions here + + """ + + # We call ourself recursively to inspect sub expressions. + + if isinstance(expr, Indexed): + junk, key = _remove_repeated(expr.indices) + return {key or None: {expr}} + elif expr.is_Atom: + return {None: {expr}} + elif expr.is_Mul: + junk, junk, key = _get_indices_Mul(expr, return_dummies=True) + result = {key or None: {expr}} + # recurse on every factor + nested = [] + for fac in expr.args: + facd = get_contraction_structure(fac) + if not (None in facd and len(facd) == 1): + nested.append(facd) + if nested: + result[expr] = nested + return result + elif expr.is_Pow or isinstance(expr, exp): + # recurse in base and exp separately. If either has internal + # contractions we must include ourselves as a key in the returned dict + b, e = expr.as_base_exp() + dbase = get_contraction_structure(b) + dexp = get_contraction_structure(e) + + dicts = [] + for d in dbase, dexp: + if not (None in d and len(d) == 1): + dicts.append(d) + result = {None: {expr}} + if dicts: + result[expr] = dicts + return result + elif expr.is_Add: + # Note: we just collect all terms with identical summation indices, We + # do nothing to identify equivalent terms here, as this would require + # substitutions or pattern matching in expressions of unknown + # complexity. + result = {} + for term in expr.args: + # recurse on every term + d = get_contraction_structure(term) + for key in d: + if key in result: + result[key] |= d[key] + else: + result[key] = d[key] + return result + + elif isinstance(expr, Piecewise): + # FIXME: No support for Piecewise yet + return {None: expr} + elif isinstance(expr, Function): + # Collect non-trivial contraction structures in each argument + # We do not report repeated indices in separate arguments as a + # contraction + deeplist = [] + for arg in expr.args: + deep = get_contraction_structure(arg) + if not (None in deep and len(deep) == 1): + deeplist.append(deep) + d = {None: {expr}} + if deeplist: + d[expr] = deeplist + return d + + # this test is expensive, so it should be at the end + elif not expr.has(Indexed): + return {None: {expr}} + raise NotImplementedError( + "FIXME: No specialized handling of type %s" % type(expr)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/indexed.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/indexed.py new file mode 100644 index 0000000000000000000000000000000000000000..feddad21e52bbab2e1243beafdb11f30b2eded4d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/indexed.py @@ -0,0 +1,793 @@ +r"""Module that defines indexed objects. + +The classes ``IndexedBase``, ``Indexed``, and ``Idx`` represent a +matrix element ``M[i, j]`` as in the following diagram:: + + 1) The Indexed class represents the entire indexed object. + | + ___|___ + ' ' + M[i, j] + / \__\______ + | | + | | + | 2) The Idx class represents indices; each Idx can + | optionally contain information about its range. + | + 3) IndexedBase represents the 'stem' of an indexed object, here `M`. + The stem used by itself is usually taken to represent the entire + array. + +There can be any number of indices on an Indexed object. No +transformation properties are implemented in these Base objects, but +implicit contraction of repeated indices is supported. + +Note that the support for complicated (i.e. non-atomic) integer +expressions as indices is limited. (This should be improved in +future releases.) + +Examples +======== + +To express the above matrix element example you would write: + +>>> from sympy import symbols, IndexedBase, Idx +>>> M = IndexedBase('M') +>>> i, j = symbols('i j', cls=Idx) +>>> M[i, j] +M[i, j] + +Repeated indices in a product implies a summation, so to express a +matrix-vector product in terms of Indexed objects: + +>>> x = IndexedBase('x') +>>> M[i, j]*x[j] +M[i, j]*x[j] + +If the indexed objects will be converted to component based arrays, e.g. +with the code printers or the autowrap framework, you also need to provide +(symbolic or numerical) dimensions. This can be done by passing an +optional shape parameter to IndexedBase upon construction: + +>>> dim1, dim2 = symbols('dim1 dim2', integer=True) +>>> A = IndexedBase('A', shape=(dim1, 2*dim1, dim2)) +>>> A.shape +(dim1, 2*dim1, dim2) +>>> A[i, j, 3].shape +(dim1, 2*dim1, dim2) + +If an IndexedBase object has no shape information, it is assumed that the +array is as large as the ranges of its indices: + +>>> n, m = symbols('n m', integer=True) +>>> i = Idx('i', m) +>>> j = Idx('j', n) +>>> M[i, j].shape +(m, n) +>>> M[i, j].ranges +[(0, m - 1), (0, n - 1)] + +The above can be compared with the following: + +>>> A[i, 2, j].shape +(dim1, 2*dim1, dim2) +>>> A[i, 2, j].ranges +[(0, m - 1), None, (0, n - 1)] + +To analyze the structure of indexed expressions, you can use the methods +get_indices() and get_contraction_structure(): + +>>> from sympy.tensor import get_indices, get_contraction_structure +>>> get_indices(A[i, j, j]) +({i}, {}) +>>> get_contraction_structure(A[i, j, j]) +{(j,): {A[i, j, j]}} + +See the appropriate docstrings for a detailed explanation of the output. +""" + +# TODO: (some ideas for improvement) +# +# o test and guarantee numpy compatibility +# - implement full support for broadcasting +# - strided arrays +# +# o more functions to analyze indexed expressions +# - identify standard constructs, e.g matrix-vector product in a subexpression +# +# o functions to generate component based arrays (numpy and sympy.Matrix) +# - generate a single array directly from Indexed +# - convert simple sub-expressions +# +# o sophisticated indexing (possibly in subclasses to preserve simplicity) +# - Idx with range smaller than dimension of Indexed +# - Idx with stepsize != 1 +# - Idx with step determined by function call +from collections.abc import Iterable + +from sympy.core.numbers import Number +from sympy.core.assumptions import StdFactKB +from sympy.core import Expr, Tuple, sympify, S +from sympy.core.symbol import _filter_assumptions, Symbol +from sympy.core.logic import fuzzy_bool, fuzzy_not +from sympy.core.sympify import _sympify +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.multipledispatch import dispatch +from sympy.utilities.iterables import is_sequence, NotIterable +from sympy.utilities.misc import filldedent + + +class IndexException(Exception): + pass + + +class Indexed(Expr): + """Represents a mathematical object with indices. + + >>> from sympy import Indexed, IndexedBase, Idx, symbols + >>> i, j = symbols('i j', cls=Idx) + >>> Indexed('A', i, j) + A[i, j] + + It is recommended that ``Indexed`` objects be created by indexing ``IndexedBase``: + ``IndexedBase('A')[i, j]`` instead of ``Indexed(IndexedBase('A'), i, j)``. + + >>> A = IndexedBase('A') + >>> a_ij = A[i, j] # Prefer this, + >>> b_ij = Indexed(A, i, j) # over this. + >>> a_ij == b_ij + True + + """ + is_Indexed = True + is_symbol = True + is_Atom = True + + def __new__(cls, base, *args, **kw_args): + from sympy.tensor.array.ndim_array import NDimArray + from sympy.matrices.matrixbase import MatrixBase + + if not args: + raise IndexException("Indexed needs at least one index.") + if isinstance(base, (str, Symbol)): + base = IndexedBase(base) + elif not hasattr(base, '__getitem__') and not isinstance(base, IndexedBase): + raise TypeError(filldedent(""" + The base can only be replaced with a string, Symbol, + IndexedBase or an object with a method for getting + items (i.e. an object with a `__getitem__` method). + """)) + args = list(map(sympify, args)) + if isinstance(base, (NDimArray, Iterable, Tuple, MatrixBase)) and all(i.is_number for i in args): + if len(args) == 1: + return base[args[0]] + else: + return base[args] + + base = _sympify(base) + + obj = Expr.__new__(cls, base, *args, **kw_args) + + IndexedBase._set_assumptions(obj, base.assumptions0) + + return obj + + def _hashable_content(self): + return super()._hashable_content() + tuple(sorted(self.assumptions0.items())) + + @property + def name(self): + return str(self) + + @property + def _diff_wrt(self): + """Allow derivatives with respect to an ``Indexed`` object.""" + return True + + def _eval_derivative(self, wrt): + from sympy.tensor.array.ndim_array import NDimArray + + if isinstance(wrt, Indexed) and wrt.base == self.base: + if len(self.indices) != len(wrt.indices): + msg = "Different # of indices: d({!s})/d({!s})".format(self, + wrt) + raise IndexException(msg) + result = S.One + for index1, index2 in zip(self.indices, wrt.indices): + result *= KroneckerDelta(index1, index2) + return result + elif isinstance(self.base, NDimArray): + from sympy.tensor.array import derive_by_array + return Indexed(derive_by_array(self.base, wrt), *self.args[1:]) + else: + if Tuple(self.indices).has(wrt): + return S.NaN + return S.Zero + + @property + def assumptions0(self): + return {k: v for k, v in self._assumptions.items() if v is not None} + + @property + def base(self): + """Returns the ``IndexedBase`` of the ``Indexed`` object. + + Examples + ======== + + >>> from sympy import Indexed, IndexedBase, Idx, symbols + >>> i, j = symbols('i j', cls=Idx) + >>> Indexed('A', i, j).base + A + >>> B = IndexedBase('B') + >>> B == B[i, j].base + True + + """ + return self.args[0] + + @property + def indices(self): + """ + Returns the indices of the ``Indexed`` object. + + Examples + ======== + + >>> from sympy import Indexed, Idx, symbols + >>> i, j = symbols('i j', cls=Idx) + >>> Indexed('A', i, j).indices + (i, j) + + """ + return self.args[1:] + + @property + def rank(self): + """ + Returns the rank of the ``Indexed`` object. + + Examples + ======== + + >>> from sympy import Indexed, Idx, symbols + >>> i, j, k, l, m = symbols('i:m', cls=Idx) + >>> Indexed('A', i, j).rank + 2 + >>> q = Indexed('A', i, j, k, l, m) + >>> q.rank + 5 + >>> q.rank == len(q.indices) + True + + """ + return len(self.args) - 1 + + @property + def shape(self): + """Returns a list with dimensions of each index. + + Dimensions is a property of the array, not of the indices. Still, if + the ``IndexedBase`` does not define a shape attribute, it is assumed + that the ranges of the indices correspond to the shape of the array. + + >>> from sympy import IndexedBase, Idx, symbols + >>> n, m = symbols('n m', integer=True) + >>> i = Idx('i', m) + >>> j = Idx('j', m) + >>> A = IndexedBase('A', shape=(n, n)) + >>> B = IndexedBase('B') + >>> A[i, j].shape + (n, n) + >>> B[i, j].shape + (m, m) + """ + + if self.base.shape: + return self.base.shape + sizes = [] + for i in self.indices: + upper = getattr(i, 'upper', None) + lower = getattr(i, 'lower', None) + if None in (upper, lower): + raise IndexException(filldedent(""" + Range is not defined for all indices in: %s""" % self)) + try: + size = upper - lower + 1 + except TypeError: + raise IndexException(filldedent(""" + Shape cannot be inferred from Idx with + undefined range: %s""" % self)) + sizes.append(size) + return Tuple(*sizes) + + @property + def ranges(self): + """Returns a list of tuples with lower and upper range of each index. + + If an index does not define the data members upper and lower, the + corresponding slot in the list contains ``None`` instead of a tuple. + + Examples + ======== + + >>> from sympy import Indexed,Idx, symbols + >>> Indexed('A', Idx('i', 2), Idx('j', 4), Idx('k', 8)).ranges + [(0, 1), (0, 3), (0, 7)] + >>> Indexed('A', Idx('i', 3), Idx('j', 3), Idx('k', 3)).ranges + [(0, 2), (0, 2), (0, 2)] + >>> x, y, z = symbols('x y z', integer=True) + >>> Indexed('A', x, y, z).ranges + [None, None, None] + + """ + ranges = [] + sentinel = object() + for i in self.indices: + upper = getattr(i, 'upper', sentinel) + lower = getattr(i, 'lower', sentinel) + if sentinel not in (upper, lower): + ranges.append((lower, upper)) + else: + ranges.append(None) + return ranges + + def _sympystr(self, p): + indices = list(map(p.doprint, self.indices)) + return "%s[%s]" % (p.doprint(self.base), ", ".join(indices)) + + @property + def free_symbols(self): + base_free_symbols = self.base.free_symbols + indices_free_symbols = { + fs for i in self.indices for fs in i.free_symbols} + if base_free_symbols: + return {self} | base_free_symbols | indices_free_symbols + else: + return indices_free_symbols + + @property + def expr_free_symbols(self): + from sympy.utilities.exceptions import sympy_deprecation_warning + sympy_deprecation_warning(""" + The expr_free_symbols property is deprecated. Use free_symbols to get + the free symbols of an expression. + """, + deprecated_since_version="1.9", + active_deprecations_target="deprecated-expr-free-symbols") + + return {self} + + +class IndexedBase(Expr, NotIterable): + """Represent the base or stem of an indexed object + + The IndexedBase class represent an array that contains elements. The main purpose + of this class is to allow the convenient creation of objects of the Indexed + class. The __getitem__ method of IndexedBase returns an instance of + Indexed. Alone, without indices, the IndexedBase class can be used as a + notation for e.g. matrix equations, resembling what you could do with the + Symbol class. But, the IndexedBase class adds functionality that is not + available for Symbol instances: + + - An IndexedBase object can optionally store shape information. This can + be used in to check array conformance and conditions for numpy + broadcasting. (TODO) + - An IndexedBase object implements syntactic sugar that allows easy symbolic + representation of array operations, using implicit summation of + repeated indices. + - The IndexedBase object symbolizes a mathematical structure equivalent + to arrays, and is recognized as such for code generation and automatic + compilation and wrapping. + + >>> from sympy.tensor import IndexedBase, Idx + >>> from sympy import symbols + >>> A = IndexedBase('A'); A + A + >>> type(A) + + + When an IndexedBase object receives indices, it returns an array with named + axes, represented by an Indexed object: + + >>> i, j = symbols('i j', integer=True) + >>> A[i, j, 2] + A[i, j, 2] + >>> type(A[i, j, 2]) + + + The IndexedBase constructor takes an optional shape argument. If given, + it overrides any shape information in the indices. (But not the index + ranges!) + + >>> m, n, o, p = symbols('m n o p', integer=True) + >>> i = Idx('i', m) + >>> j = Idx('j', n) + >>> A[i, j].shape + (m, n) + >>> B = IndexedBase('B', shape=(o, p)) + >>> B[i, j].shape + (o, p) + + Assumptions can be specified with keyword arguments the same way as for Symbol: + + >>> A_real = IndexedBase('A', real=True) + >>> A_real.is_real + True + >>> A != A_real + True + + Assumptions can also be inherited if a Symbol is used to initialize the IndexedBase: + + >>> I = symbols('I', integer=True) + >>> C_inherit = IndexedBase(I) + >>> C_explicit = IndexedBase('I', integer=True) + >>> C_inherit == C_explicit + True + """ + is_symbol = True + is_Atom = True + + @staticmethod + def _set_assumptions(obj, assumptions): + """Set assumptions on obj, making sure to apply consistent values.""" + tmp_asm_copy = assumptions.copy() + is_commutative = fuzzy_bool(assumptions.get('commutative', True)) + assumptions['commutative'] = is_commutative + obj._assumptions = StdFactKB(assumptions) + obj._assumptions._generator = tmp_asm_copy # Issue #8873 + + def __new__(cls, label, shape=None, *, offset=S.Zero, strides=None, **kw_args): + from sympy.matrices.matrixbase import MatrixBase + from sympy.tensor.array.ndim_array import NDimArray + + assumptions, kw_args = _filter_assumptions(kw_args) + if isinstance(label, str): + label = Symbol(label, **assumptions) + elif isinstance(label, Symbol): + assumptions = label._merge(assumptions) + elif isinstance(label, (MatrixBase, NDimArray)): + return label + elif isinstance(label, Iterable): + return _sympify(label) + else: + label = _sympify(label) + + if is_sequence(shape): + shape = Tuple(*shape) + elif shape is not None: + shape = Tuple(shape) + + if shape is not None: + obj = Expr.__new__(cls, label, shape) + else: + obj = Expr.__new__(cls, label) + obj._shape = shape + obj._offset = offset + obj._strides = strides + obj._name = str(label) + + IndexedBase._set_assumptions(obj, assumptions) + return obj + + @property + def name(self): + return self._name + + def _hashable_content(self): + return super()._hashable_content() + tuple(sorted(self.assumptions0.items())) + + @property + def assumptions0(self): + return {k: v for k, v in self._assumptions.items() if v is not None} + + def __getitem__(self, indices, **kw_args): + if is_sequence(indices): + # Special case needed because M[*my_tuple] is a syntax error. + if self.shape and len(self.shape) != len(indices): + raise IndexException("Rank mismatch.") + return Indexed(self, *indices, **kw_args) + else: + if self.shape and len(self.shape) != 1: + raise IndexException("Rank mismatch.") + return Indexed(self, indices, **kw_args) + + @property + def shape(self): + """Returns the shape of the ``IndexedBase`` object. + + Examples + ======== + + >>> from sympy import IndexedBase, Idx + >>> from sympy.abc import x, y + >>> IndexedBase('A', shape=(x, y)).shape + (x, y) + + Note: If the shape of the ``IndexedBase`` is specified, it will override + any shape information given by the indices. + + >>> A = IndexedBase('A', shape=(x, y)) + >>> B = IndexedBase('B') + >>> i = Idx('i', 2) + >>> j = Idx('j', 1) + >>> A[i, j].shape + (x, y) + >>> B[i, j].shape + (2, 1) + + """ + return self._shape + + @property + def strides(self): + """Returns the strided scheme for the ``IndexedBase`` object. + + Normally this is a tuple denoting the number of + steps to take in the respective dimension when traversing + an array. For code generation purposes strides='C' and + strides='F' can also be used. + + strides='C' would mean that code printer would unroll + in row-major order and 'F' means unroll in column major + order. + + """ + + return self._strides + + @property + def offset(self): + """Returns the offset for the ``IndexedBase`` object. + + This is the value added to the resulting index when the + 2D Indexed object is unrolled to a 1D form. Used in code + generation. + + Examples + ========== + >>> from sympy.printing import ccode + >>> from sympy.tensor import IndexedBase, Idx + >>> from sympy import symbols + >>> l, m, n, o = symbols('l m n o', integer=True) + >>> A = IndexedBase('A', strides=(l, m, n), offset=o) + >>> i, j, k = map(Idx, 'ijk') + >>> ccode(A[i, j, k]) + 'A[l*i + m*j + n*k + o]' + + """ + return self._offset + + @property + def label(self): + """Returns the label of the ``IndexedBase`` object. + + Examples + ======== + + >>> from sympy import IndexedBase + >>> from sympy.abc import x, y + >>> IndexedBase('A', shape=(x, y)).label + A + + """ + return self.args[0] + + def _sympystr(self, p): + return p.doprint(self.label) + + +class Idx(Expr): + """Represents an integer index as an ``Integer`` or integer expression. + + There are a number of ways to create an ``Idx`` object. The constructor + takes two arguments: + + ``label`` + An integer or a symbol that labels the index. + ``range`` + Optionally you can specify a range as either + + * ``Symbol`` or integer: This is interpreted as a dimension. Lower and + upper bounds are set to ``0`` and ``range - 1``, respectively. + * ``tuple``: The two elements are interpreted as the lower and upper + bounds of the range, respectively. + + Note: bounds of the range are assumed to be either integer or infinite (oo + and -oo are allowed to specify an unbounded range). If ``n`` is given as a + bound, then ``n.is_integer`` must not return false. + + For convenience, if the label is given as a string it is automatically + converted to an integer symbol. (Note: this conversion is not done for + range or dimension arguments.) + + Examples + ======== + + >>> from sympy import Idx, symbols, oo + >>> n, i, L, U = symbols('n i L U', integer=True) + + If a string is given for the label an integer ``Symbol`` is created and the + bounds are both ``None``: + + >>> idx = Idx('qwerty'); idx + qwerty + >>> idx.lower, idx.upper + (None, None) + + Both upper and lower bounds can be specified: + + >>> idx = Idx(i, (L, U)); idx + i + >>> idx.lower, idx.upper + (L, U) + + When only a single bound is given it is interpreted as the dimension + and the lower bound defaults to 0: + + >>> idx = Idx(i, n); idx.lower, idx.upper + (0, n - 1) + >>> idx = Idx(i, 4); idx.lower, idx.upper + (0, 3) + >>> idx = Idx(i, oo); idx.lower, idx.upper + (0, oo) + + """ + + is_integer = True + is_finite = True + is_real = True + is_symbol = True + is_Atom = True + _diff_wrt = True + + def __new__(cls, label, range=None, **kw_args): + + if isinstance(label, str): + label = Symbol(label, integer=True) + label, range = list(map(sympify, (label, range))) + + if label.is_Number: + if not label.is_integer: + raise TypeError("Index is not an integer number.") + return label + + if not label.is_integer: + raise TypeError("Idx object requires an integer label.") + + elif is_sequence(range): + if len(range) != 2: + raise ValueError(filldedent(""" + Idx range tuple must have length 2, but got %s""" % len(range))) + for bound in range: + if (bound.is_integer is False and bound is not S.Infinity + and bound is not S.NegativeInfinity): + raise TypeError("Idx object requires integer bounds.") + args = label, Tuple(*range) + elif isinstance(range, Expr): + if range is not S.Infinity and fuzzy_not(range.is_integer): + raise TypeError("Idx object requires an integer dimension.") + args = label, Tuple(0, range - 1) + elif range: + raise TypeError(filldedent(""" + The range must be an ordered iterable or + integer SymPy expression.""")) + else: + args = label, + + obj = Expr.__new__(cls, *args, **kw_args) + obj._assumptions["finite"] = True + obj._assumptions["real"] = True + return obj + + @property + def label(self): + """Returns the label (Integer or integer expression) of the Idx object. + + Examples + ======== + + >>> from sympy import Idx, Symbol + >>> x = Symbol('x', integer=True) + >>> Idx(x).label + x + >>> j = Symbol('j', integer=True) + >>> Idx(j).label + j + >>> Idx(j + 1).label + j + 1 + + """ + return self.args[0] + + @property + def lower(self): + """Returns the lower bound of the ``Idx``. + + Examples + ======== + + >>> from sympy import Idx + >>> Idx('j', 2).lower + 0 + >>> Idx('j', 5).lower + 0 + >>> Idx('j').lower is None + True + + """ + try: + return self.args[1][0] + except IndexError: + return + + @property + def upper(self): + """Returns the upper bound of the ``Idx``. + + Examples + ======== + + >>> from sympy import Idx + >>> Idx('j', 2).upper + 1 + >>> Idx('j', 5).upper + 4 + >>> Idx('j').upper is None + True + + """ + try: + return self.args[1][1] + except IndexError: + return + + def _sympystr(self, p): + return p.doprint(self.label) + + @property + def name(self): + return self.label.name if self.label.is_Symbol else str(self.label) + + @property + def free_symbols(self): + return {self} + + +@dispatch(Idx, Idx) +def _eval_is_ge(lhs, rhs): # noqa:F811 + + other_upper = rhs if rhs.upper is None else rhs.upper + other_lower = rhs if rhs.lower is None else rhs.lower + + if lhs.lower is not None and (lhs.lower >= other_upper) == True: + return True + if lhs.upper is not None and (lhs.upper < other_lower) == True: + return False + return None + + +@dispatch(Idx, Number) # type:ignore +def _eval_is_ge(lhs, rhs): # noqa:F811 + + other_upper = rhs + other_lower = rhs + + if lhs.lower is not None and (lhs.lower >= other_upper) == True: + return True + if lhs.upper is not None and (lhs.upper < other_lower) == True: + return False + return None + + +@dispatch(Number, Idx) # type:ignore +def _eval_is_ge(lhs, rhs): # noqa:F811 + + other_upper = lhs + other_lower = lhs + + if rhs.upper is not None and (rhs.upper <= other_lower) == True: + return True + if rhs.lower is not None and (rhs.lower > other_upper) == True: + return False + return None diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/tensor.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/tensor.py new file mode 100644 index 0000000000000000000000000000000000000000..579e7c7a86c2a1f18ab889af32ce0053a729ff5f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/tensor.py @@ -0,0 +1,5265 @@ +""" +This module defines tensors with abstract index notation. + +The abstract index notation has been first formalized by Penrose. + +Tensor indices are formal objects, with a tensor type; there is no +notion of index range, it is only possible to assign the dimension, +used to trace the Kronecker delta; the dimension can be a Symbol. + +The Einstein summation convention is used. +The covariant indices are indicated with a minus sign in front of the index. + +For instance the tensor ``t = p(a)*A(b,c)*q(-c)`` has the index ``c`` +contracted. + +A tensor expression ``t`` can be called; called with its +indices in sorted order it is equal to itself: +in the above example ``t(a, b) == t``; +one can call ``t`` with different indices; ``t(c, d) == p(c)*A(d,a)*q(-a)``. + +The contracted indices are dummy indices, internally they have no name, +the indices being represented by a graph-like structure. + +Tensors are put in canonical form using ``canon_bp``, which uses +the Butler-Portugal algorithm for canonicalization using the monoterm +symmetries of the tensors. + +If there is a (anti)symmetric metric, the indices can be raised and +lowered when the tensor is put in canonical form. +""" + +from __future__ import annotations +from typing import Any +from functools import reduce +from math import prod + +from abc import abstractmethod, ABC +from collections import defaultdict +import operator +import itertools + +from sympy.core.numbers import (Integer, Rational) +from sympy.combinatorics import Permutation +from sympy.combinatorics.tensor_can import get_symmetric_group_sgs, \ + bsgs_direct_product, canonicalize, riemann_bsgs +from sympy.core import Basic, Expr, sympify, Add, Mul, S +from sympy.core.cache import clear_cache +from sympy.core.containers import Tuple, Dict +from sympy.core.function import WildFunction +from sympy.core.sorting import default_sort_key +from sympy.core.symbol import Symbol, symbols, Wild +from sympy.core.sympify import CantSympify, _sympify +from sympy.core.operations import AssocOp +from sympy.external.gmpy import SYMPY_INTS +from sympy.matrices import eye +from sympy.utilities.exceptions import (sympy_deprecation_warning, + SymPyDeprecationWarning, + ignore_warnings) +from sympy.utilities.decorator import memoize_property, deprecated +from sympy.utilities.iterables import sift + + +def deprecate_data(): + sympy_deprecation_warning( + """ + The data attribute of TensorIndexType is deprecated. Use The + replace_with_arrays() method instead. + """, + deprecated_since_version="1.4", + active_deprecations_target="deprecated-tensorindextype-attrs", + stacklevel=4, + ) + +def deprecate_fun_eval(): + sympy_deprecation_warning( + """ + The Tensor.fun_eval() method is deprecated. Use + Tensor.substitute_indices() instead. + """, + deprecated_since_version="1.5", + active_deprecations_target="deprecated-tensor-fun-eval", + stacklevel=4, + ) + + +def deprecate_call(): + sympy_deprecation_warning( + """ + Calling a tensor like Tensor(*indices) is deprecated. Use + Tensor.substitute_indices() instead. + """, + deprecated_since_version="1.5", + active_deprecations_target="deprecated-tensor-fun-eval", + stacklevel=4, + ) + + +class _IndexStructure(CantSympify): + """ + This class handles the indices (free and dummy ones). It contains the + algorithms to manage the dummy indices replacements and contractions of + free indices under multiplications of tensor expressions, as well as stuff + related to canonicalization sorting, getting the permutation of the + expression and so on. It also includes tools to get the ``TensorIndex`` + objects corresponding to the given index structure. + """ + + def __init__(self, free, dum, index_types, indices, canon_bp=False): + self.free = free + self.dum = dum + self.index_types = index_types + self.indices = indices + self._ext_rank = len(self.free) + 2*len(self.dum) + self.dum.sort(key=lambda x: x[0]) + + @staticmethod + def from_indices(*indices): + """ + Create a new ``_IndexStructure`` object from a list of ``indices``. + + Explanation + =========== + + ``indices`` ``TensorIndex`` objects, the indices. Contractions are + detected upon construction. + + Examples + ======== + + >>> from sympy.tensor.tensor import TensorIndexType, tensor_indices, _IndexStructure + >>> Lorentz = TensorIndexType('Lorentz', dummy_name='L') + >>> m0, m1, m2, m3 = tensor_indices('m0,m1,m2,m3', Lorentz) + >>> _IndexStructure.from_indices(m0, m1, -m1, m3) + _IndexStructure([(m0, 0), (m3, 3)], [(1, 2)], [Lorentz, Lorentz, Lorentz, Lorentz]) + """ + + free, dum = _IndexStructure._free_dum_from_indices(*indices) + index_types = [i.tensor_index_type for i in indices] + indices = _IndexStructure._replace_dummy_names(indices, free, dum) + return _IndexStructure(free, dum, index_types, indices) + + @staticmethod + def from_components_free_dum(components, free, dum): + index_types = [] + for component in components: + index_types.extend(component.index_types) + indices = _IndexStructure.generate_indices_from_free_dum_index_types(free, dum, index_types) + return _IndexStructure(free, dum, index_types, indices) + + @staticmethod + def _free_dum_from_indices(*indices): + """ + Convert ``indices`` into ``free``, ``dum`` for single component tensor. + + Explanation + =========== + + ``free`` list of tuples ``(index, pos, 0)``, + where ``pos`` is the position of index in + the list of indices formed by the component tensors + + ``dum`` list of tuples ``(pos_contr, pos_cov, 0, 0)`` + + Examples + ======== + + >>> from sympy.tensor.tensor import TensorIndexType, tensor_indices, \ + _IndexStructure + >>> Lorentz = TensorIndexType('Lorentz', dummy_name='L') + >>> m0, m1, m2, m3 = tensor_indices('m0,m1,m2,m3', Lorentz) + >>> _IndexStructure._free_dum_from_indices(m0, m1, -m1, m3) + ([(m0, 0), (m3, 3)], [(1, 2)]) + """ + n = len(indices) + if n == 1: + return [(indices[0], 0)], [] + + # find the positions of the free indices and of the dummy indices + free = [True]*len(indices) + index_dict = {} + dum = [] + for i, index in enumerate(indices): + name = index.name + typ = index.tensor_index_type + contr = index.is_up + if (name, typ) in index_dict: + # found a pair of dummy indices + is_contr, pos = index_dict[(name, typ)] + # check consistency and update free + if is_contr: + if contr: + raise ValueError('two equal contravariant indices in slots %d and %d' %(pos, i)) + else: + free[pos] = False + free[i] = False + else: + if contr: + free[pos] = False + free[i] = False + else: + raise ValueError('two equal covariant indices in slots %d and %d' %(pos, i)) + if contr: + dum.append((i, pos)) + else: + dum.append((pos, i)) + else: + index_dict[(name, typ)] = index.is_up, i + + free = [(index, i) for i, index in enumerate(indices) if free[i]] + free.sort() + return free, dum + + def get_indices(self): + """ + Get a list of indices, creating new tensor indices to complete dummy indices. + """ + return self.indices[:] + + @staticmethod + def generate_indices_from_free_dum_index_types(free, dum, index_types): + indices = [None]*(len(free)+2*len(dum)) + for idx, pos in free: + indices[pos] = idx + + generate_dummy_name = _IndexStructure._get_generator_for_dummy_indices(free) + for pos1, pos2 in dum: + typ1 = index_types[pos1] + indname = generate_dummy_name(typ1) + indices[pos1] = TensorIndex(indname, typ1, True) + indices[pos2] = TensorIndex(indname, typ1, False) + + return _IndexStructure._replace_dummy_names(indices, free, dum) + + @staticmethod + def _get_generator_for_dummy_indices(free): + cdt = defaultdict(int) + # if the free indices have names with dummy_name, start with an + # index higher than those for the dummy indices + # to avoid name collisions + for indx, ipos in free: + if indx.name.split('_')[0] == indx.tensor_index_type.dummy_name: + cdt[indx.tensor_index_type] = max(cdt[indx.tensor_index_type], int(indx.name.split('_')[1]) + 1) + + def dummy_name_gen(tensor_index_type): + nd = str(cdt[tensor_index_type]) + cdt[tensor_index_type] += 1 + return tensor_index_type.dummy_name + '_' + nd + + return dummy_name_gen + + @staticmethod + def _replace_dummy_names(indices, free, dum): + dum.sort(key=lambda x: x[0]) + new_indices = list(indices) + assert len(indices) == len(free) + 2*len(dum) + generate_dummy_name = _IndexStructure._get_generator_for_dummy_indices(free) + for ipos1, ipos2 in dum: + typ1 = new_indices[ipos1].tensor_index_type + indname = generate_dummy_name(typ1) + new_indices[ipos1] = TensorIndex(indname, typ1, True) + new_indices[ipos2] = TensorIndex(indname, typ1, False) + return new_indices + + def get_free_indices(self) -> list[TensorIndex]: + """ + Get a list of free indices. + """ + # get sorted indices according to their position: + free = sorted(self.free, key=lambda x: x[1]) + return [i[0] for i in free] + + def __str__(self): + return "_IndexStructure({}, {}, {})".format(self.free, self.dum, self.index_types) + + def __repr__(self): + return self.__str__() + + def _get_sorted_free_indices_for_canon(self): + sorted_free = self.free[:] + sorted_free.sort(key=lambda x: x[0]) + return sorted_free + + def _get_sorted_dum_indices_for_canon(self): + return sorted(self.dum, key=lambda x: x[0]) + + def _get_lexicographically_sorted_index_types(self): + permutation = self.indices_canon_args()[0] + index_types = [None]*self._ext_rank + for i, it in enumerate(self.index_types): + index_types[permutation(i)] = it + return index_types + + def _get_lexicographically_sorted_indices(self): + permutation = self.indices_canon_args()[0] + indices = [None]*self._ext_rank + for i, it in enumerate(self.indices): + indices[permutation(i)] = it + return indices + + def perm2tensor(self, g, is_canon_bp=False): + """ + Returns a ``_IndexStructure`` instance corresponding to the permutation ``g``. + + Explanation + =========== + + ``g`` permutation corresponding to the tensor in the representation + used in canonicalization + + ``is_canon_bp`` if True, then ``g`` is the permutation + corresponding to the canonical form of the tensor + """ + sorted_free = [i[0] for i in self._get_sorted_free_indices_for_canon()] + lex_index_types = self._get_lexicographically_sorted_index_types() + lex_indices = self._get_lexicographically_sorted_indices() + nfree = len(sorted_free) + rank = self._ext_rank + dum = [[None]*2 for i in range((rank - nfree)//2)] + free = [] + + index_types = [None]*rank + indices = [None]*rank + for i in range(rank): + gi = g[i] + index_types[i] = lex_index_types[gi] + indices[i] = lex_indices[gi] + if gi < nfree: + ind = sorted_free[gi] + assert index_types[i] == sorted_free[gi].tensor_index_type + free.append((ind, i)) + else: + j = gi - nfree + idum, cov = divmod(j, 2) + if cov: + dum[idum][1] = i + else: + dum[idum][0] = i + dum = [tuple(x) for x in dum] + + return _IndexStructure(free, dum, index_types, indices) + + def indices_canon_args(self): + """ + Returns ``(g, dummies, msym, v)``, the entries of ``canonicalize`` + + See ``canonicalize`` in ``tensor_can.py`` in combinatorics module. + """ + # to be called after sorted_components + from sympy.combinatorics.permutations import _af_new + n = self._ext_rank + g = [None]*n + [n, n+1] + + # Converts the symmetry of the metric into msym from .canonicalize() + # method in the combinatorics module + def metric_symmetry_to_msym(metric): + if metric is None: + return None + sym = metric.symmetry + if sym == TensorSymmetry.fully_symmetric(2): + return 0 + if sym == TensorSymmetry.fully_symmetric(-2): + return 1 + return None + + # ordered indices: first the free indices, ordered by types + # then the dummy indices, ordered by types and contravariant before + # covariant + # g[position in tensor] = position in ordered indices + for i, (indx, ipos) in enumerate(self._get_sorted_free_indices_for_canon()): + g[ipos] = i + pos = len(self.free) + j = len(self.free) + dummies = [] + prev = None + a = [] + msym = [] + for ipos1, ipos2 in self._get_sorted_dum_indices_for_canon(): + g[ipos1] = j + g[ipos2] = j + 1 + j += 2 + typ = self.index_types[ipos1] + if typ != prev: + if a: + dummies.append(a) + a = [pos, pos + 1] + prev = typ + msym.append(metric_symmetry_to_msym(typ.metric)) + else: + a.extend([pos, pos + 1]) + pos += 2 + if a: + dummies.append(a) + + return _af_new(g), dummies, msym + + +def components_canon_args(components): + numtyp = [] + prev = None + for t in components: + if t == prev: + numtyp[-1][1] += 1 + else: + prev = t + numtyp.append([prev, 1]) + v = [] + for h, n in numtyp: + if h.comm in (0, 1): + comm = h.comm + else: + comm = TensorManager.get_comm(h.comm, h.comm) + v.append((h.symmetry.base, h.symmetry.generators, n, comm)) + return v + + +class _TensorDataLazyEvaluator(CantSympify): + """ + EXPERIMENTAL: do not rely on this class, it may change without deprecation + warnings in future versions of SymPy. + + Explanation + =========== + + This object contains the logic to associate components data to a tensor + expression. Components data are set via the ``.data`` property of tensor + expressions, is stored inside this class as a mapping between the tensor + expression and the ``ndarray``. + + Computations are executed lazily: whereas the tensor expressions can have + contractions, tensor products, and additions, components data are not + computed until they are accessed by reading the ``.data`` property + associated to the tensor expression. + """ + _substitutions_dict: dict[Any, Any] = {} + _substitutions_dict_tensmul: dict[Any, Any] = {} + + def __getitem__(self, key): + dat = self._get(key) + if dat is None: + return None + + from .array import NDimArray + if not isinstance(dat, NDimArray): + return dat + + if dat.rank() == 0: + return dat[()] + elif dat.rank() == 1 and len(dat) == 1: + return dat[0] + return dat + + def _get(self, key): + """ + Retrieve ``data`` associated with ``key``. + + Explanation + =========== + + This algorithm looks into ``self._substitutions_dict`` for all + ``TensorHead`` in the ``TensExpr`` (or just ``TensorHead`` if key is a + TensorHead instance). It reconstructs the components data that the + tensor expression should have by performing on components data the + operations that correspond to the abstract tensor operations applied. + + Metric tensor is handled in a different manner: it is pre-computed in + ``self._substitutions_dict_tensmul``. + """ + if key in self._substitutions_dict: + return self._substitutions_dict[key] + + if isinstance(key, TensorHead): + return None + + if isinstance(key, Tensor): + # special case to handle metrics. Metric tensors cannot be + # constructed through contraction by the metric, their + # components show if they are a matrix or its inverse. + signature = tuple([i.is_up for i in key.get_indices()]) + srch = (key.component,) + signature + if srch in self._substitutions_dict_tensmul: + return self._substitutions_dict_tensmul[srch] + array_list = [self.data_from_tensor(key)] + return self.data_contract_dum(array_list, key.dum, key.ext_rank) + + if isinstance(key, TensMul): + tensmul_args = key.args + if len(tensmul_args) == 1 and len(tensmul_args[0].components) == 1: + # special case to handle metrics. Metric tensors cannot be + # constructed through contraction by the metric, their + # components show if they are a matrix or its inverse. + signature = tuple([i.is_up for i in tensmul_args[0].get_indices()]) + srch = (tensmul_args[0].components[0],) + signature + if srch in self._substitutions_dict_tensmul: + return self._substitutions_dict_tensmul[srch] + #data_list = [self.data_from_tensor(i) for i in tensmul_args if isinstance(i, TensExpr)] + data_list = [self.data_from_tensor(i) if isinstance(i, Tensor) else i.data for i in tensmul_args if isinstance(i, TensExpr)] + coeff = prod([i for i in tensmul_args if not isinstance(i, TensExpr)]) + if all(i is None for i in data_list): + return None + if any(i is None for i in data_list): + raise ValueError("Mixing tensors with associated components "\ + "data with tensors without components data") + data_result = self.data_contract_dum(data_list, key.dum, key.ext_rank) + return coeff*data_result + + if isinstance(key, TensAdd): + data_list = [] + free_args_list = [] + for arg in key.args: + if isinstance(arg, TensExpr): + data_list.append(arg.data) + free_args_list.append([x[0] for x in arg.free]) + else: + data_list.append(arg) + free_args_list.append([]) + if all(i is None for i in data_list): + return None + if any(i is None for i in data_list): + raise ValueError("Mixing tensors with associated components "\ + "data with tensors without components data") + + sum_list = [] + from .array import permutedims + for data, free_args in zip(data_list, free_args_list): + if len(free_args) < 2: + sum_list.append(data) + else: + free_args_pos = {y: x for x, y in enumerate(free_args)} + axes = [free_args_pos[arg] for arg in key.free_args] + sum_list.append(permutedims(data, axes)) + return reduce(lambda x, y: x+y, sum_list) + + return None + + @staticmethod + def data_contract_dum(ndarray_list, dum, ext_rank): + from .array import tensorproduct, tensorcontraction, MutableDenseNDimArray + arrays = list(map(MutableDenseNDimArray, ndarray_list)) + prodarr = tensorproduct(*arrays) + return tensorcontraction(prodarr, *dum) + + def data_tensorhead_from_tensmul(self, data, tensmul, tensorhead): + """ + This method is used when assigning components data to a ``TensMul`` + object, it converts components data to a fully contravariant ndarray, + which is then stored according to the ``TensorHead`` key. + """ + if data is None: + return None + + return self._correct_signature_from_indices( + data, + tensmul.get_indices(), + tensmul.free, + tensmul.dum, + True) + + def data_from_tensor(self, tensor): + """ + This method corrects the components data to the right signature + (covariant/contravariant) using the metric associated with each + ``TensorIndexType``. + """ + tensorhead = tensor.component + + if tensorhead.data is None: + return None + + return self._correct_signature_from_indices( + tensorhead.data, + tensor.get_indices(), + tensor.free, + tensor.dum) + + def _assign_data_to_tensor_expr(self, key, data): + if isinstance(key, TensAdd): + raise ValueError('cannot assign data to TensAdd') + # here it is assumed that `key` is a `TensMul` instance. + if len(key.components) != 1: + raise ValueError('cannot assign data to TensMul with multiple components') + tensorhead = key.components[0] + newdata = self.data_tensorhead_from_tensmul(data, key, tensorhead) + return tensorhead, newdata + + def _check_permutations_on_data(self, tens, data): + from .array import permutedims + from .array.arrayop import Flatten + + if isinstance(tens, TensorHead): + rank = tens.rank + generators = tens.symmetry.generators + elif isinstance(tens, Tensor): + rank = tens.rank + generators = tens.components[0].symmetry.generators + elif isinstance(tens, TensorIndexType): + rank = tens.metric.rank + generators = tens.metric.symmetry.generators + + # Every generator is a permutation, check that by permuting the array + # by that permutation, the array will be the same, except for a + # possible sign change if the permutation admits it. + for gener in generators: + sign_change = +1 if (gener(rank) == rank) else -1 + data_swapped = data + last_data = data + permute_axes = list(map(gener, range(rank))) + # the order of a permutation is the number of times to get the + # identity by applying that permutation. + for i in range(gener.order()-1): + data_swapped = permutedims(data_swapped, permute_axes) + # if any value in the difference array is non-zero, raise an error: + if any(Flatten(last_data - sign_change*data_swapped)): + raise ValueError("Component data symmetry structure error") + last_data = data_swapped + + def __setitem__(self, key, value): + """ + Set the components data of a tensor object/expression. + + Explanation + =========== + + Components data are transformed to the all-contravariant form and stored + with the corresponding ``TensorHead`` object. If a ``TensorHead`` object + cannot be uniquely identified, it will raise an error. + """ + data = _TensorDataLazyEvaluator.parse_data(value) + self._check_permutations_on_data(key, data) + + # TensorHead and TensorIndexType can be assigned data directly, while + # TensMul must first convert data to a fully contravariant form, and + # assign it to its corresponding TensorHead single component. + if not isinstance(key, (TensorHead, TensorIndexType)): + key, data = self._assign_data_to_tensor_expr(key, data) + + if isinstance(key, TensorHead): + for dim, indextype in zip(data.shape, key.index_types): + if indextype.data is None: + raise ValueError("index type {} has no components data"\ + " associated (needed to raise/lower index)".format(indextype)) + if not indextype.dim.is_number: + continue + if dim != indextype.dim: + raise ValueError("wrong dimension of ndarray") + self._substitutions_dict[key] = data + + def __delitem__(self, key): + del self._substitutions_dict[key] + + def __contains__(self, key): + return key in self._substitutions_dict + + def add_metric_data(self, metric, data): + """ + Assign data to the ``metric`` tensor. The metric tensor behaves in an + anomalous way when raising and lowering indices. + + Explanation + =========== + + A fully covariant metric is the inverse transpose of the fully + contravariant metric (it is meant matrix inverse). If the metric is + symmetric, the transpose is not necessary and mixed + covariant/contravariant metrics are Kronecker deltas. + """ + # hard assignment, data should not be added to `TensorHead` for metric: + # the problem with `TensorHead` is that the metric is anomalous, i.e. + # raising and lowering the index means considering the metric or its + # inverse, this is not the case for other tensors. + self._substitutions_dict_tensmul[metric, True, True] = data + inverse_transpose = self.inverse_transpose_matrix(data) + # in symmetric spaces, the transpose is the same as the original matrix, + # the full covariant metric tensor is the inverse transpose, so this + # code will be able to handle non-symmetric metrics. + self._substitutions_dict_tensmul[metric, False, False] = inverse_transpose + # now mixed cases, these are identical to the unit matrix if the metric + # is symmetric. + m = data.tomatrix() + invt = inverse_transpose.tomatrix() + self._substitutions_dict_tensmul[metric, True, False] = m * invt + self._substitutions_dict_tensmul[metric, False, True] = invt * m + + @staticmethod + def _flip_index_by_metric(data, metric, pos): + from .array import tensorproduct, tensorcontraction + + mdim = metric.rank() + ddim = data.rank() + + if pos == 0: + data = tensorcontraction( + tensorproduct( + metric, + data + ), + (1, mdim+pos) + ) + else: + data = tensorcontraction( + tensorproduct( + data, + metric + ), + (pos, ddim) + ) + return data + + @staticmethod + def inverse_matrix(ndarray): + m = ndarray.tomatrix().inv() + return _TensorDataLazyEvaluator.parse_data(m) + + @staticmethod + def inverse_transpose_matrix(ndarray): + m = ndarray.tomatrix().inv().T + return _TensorDataLazyEvaluator.parse_data(m) + + @staticmethod + def _correct_signature_from_indices(data, indices, free, dum, inverse=False): + """ + Utility function to correct the values inside the components data + ndarray according to whether indices are covariant or contravariant. + + It uses the metric matrix to lower values of covariant indices. + """ + # change the ndarray values according covariantness/contravariantness of the indices + # use the metric + for i, indx in enumerate(indices): + if not indx.is_up and not inverse: + data = _TensorDataLazyEvaluator._flip_index_by_metric(data, indx.tensor_index_type.data, i) + elif not indx.is_up and inverse: + data = _TensorDataLazyEvaluator._flip_index_by_metric( + data, + _TensorDataLazyEvaluator.inverse_matrix(indx.tensor_index_type.data), + i + ) + return data + + @staticmethod + def _sort_data_axes(old, new): + from .array import permutedims + + new_data = old.data.copy() + + old_free = [i[0] for i in old.free] + new_free = [i[0] for i in new.free] + + for i in range(len(new_free)): + for j in range(i, len(old_free)): + if old_free[j] == new_free[i]: + old_free[i], old_free[j] = old_free[j], old_free[i] + new_data = permutedims(new_data, (i, j)) + break + return new_data + + @staticmethod + def add_rearrange_tensmul_parts(new_tensmul, old_tensmul): + def sorted_compo(): + return _TensorDataLazyEvaluator._sort_data_axes(old_tensmul, new_tensmul) + + _TensorDataLazyEvaluator._substitutions_dict[new_tensmul] = sorted_compo() + + @staticmethod + def parse_data(data): + """ + Transform ``data`` to array. The parameter ``data`` may + contain data in various formats, e.g. nested lists, SymPy ``Matrix``, + and so on. + + Examples + ======== + + >>> from sympy.tensor.tensor import _TensorDataLazyEvaluator + >>> _TensorDataLazyEvaluator.parse_data([1, 3, -6, 12]) + [1, 3, -6, 12] + + >>> _TensorDataLazyEvaluator.parse_data([[1, 2], [4, 7]]) + [[1, 2], [4, 7]] + """ + from .array import MutableDenseNDimArray + + if not isinstance(data, MutableDenseNDimArray): + if len(data) == 2 and hasattr(data[0], '__call__'): + data = MutableDenseNDimArray(data[0], data[1]) + else: + data = MutableDenseNDimArray(data) + return data + +_tensor_data_substitution_dict = _TensorDataLazyEvaluator() + + +class _TensorManager: + """ + Class to manage tensor properties. + + Notes + ===== + + Tensors belong to tensor commutation groups; each group has a label + ``comm``; there are predefined labels: + + ``0`` tensors commuting with any other tensor + + ``1`` tensors anticommuting among themselves + + ``2`` tensors not commuting, apart with those with ``comm=0`` + + Other groups can be defined using ``set_comm``; tensors in those + groups commute with those with ``comm=0``; by default they + do not commute with any other group. + """ + def __init__(self): + self._comm_init() + + def _comm_init(self): + self._comm = [{} for i in range(3)] + for i in range(3): + self._comm[0][i] = 0 + self._comm[i][0] = 0 + self._comm[1][1] = 1 + self._comm[2][1] = None + self._comm[1][2] = None + self._comm_symbols2i = {0:0, 1:1, 2:2} + self._comm_i2symbol = {0:0, 1:1, 2:2} + + @property + def comm(self): + return self._comm + + def comm_symbols2i(self, i): + """ + Get the commutation group number corresponding to ``i``. + + ``i`` can be a symbol or a number or a string. + + If ``i`` is not already defined its commutation group number + is set. + """ + if i not in self._comm_symbols2i: + n = len(self._comm) + self._comm.append({}) + self._comm[n][0] = 0 + self._comm[0][n] = 0 + self._comm_symbols2i[i] = n + self._comm_i2symbol[n] = i + return n + return self._comm_symbols2i[i] + + def comm_i2symbol(self, i): + """ + Returns the symbol corresponding to the commutation group number. + """ + return self._comm_i2symbol[i] + + def set_comm(self, i, j, c): + """ + Set the commutation parameter ``c`` for commutation groups ``i, j``. + + Parameters + ========== + + i, j : symbols representing commutation groups + + c : group commutation number + + Notes + ===== + + ``i, j`` can be symbols, strings or numbers, + apart from ``0, 1`` and ``2`` which are reserved respectively + for commuting, anticommuting tensors and tensors not commuting + with any other group apart with the commuting tensors. + For the remaining cases, use this method to set the commutation rules; + by default ``c=None``. + + The group commutation number ``c`` is assigned in correspondence + to the group commutation symbols; it can be + + 0 commuting + + 1 anticommuting + + None no commutation property + + Examples + ======== + + ``G`` and ``GH`` do not commute with themselves and commute with + each other; A is commuting. + + >>> from sympy.tensor.tensor import TensorIndexType, tensor_indices, TensorHead, TensorManager, TensorSymmetry + >>> Lorentz = TensorIndexType('Lorentz') + >>> i0,i1,i2,i3,i4 = tensor_indices('i0:5', Lorentz) + >>> A = TensorHead('A', [Lorentz]) + >>> G = TensorHead('G', [Lorentz], TensorSymmetry.no_symmetry(1), 'Gcomm') + >>> GH = TensorHead('GH', [Lorentz], TensorSymmetry.no_symmetry(1), 'GHcomm') + >>> TensorManager.set_comm('Gcomm', 'GHcomm', 0) + >>> (GH(i1)*G(i0)).canon_bp() + G(i0)*GH(i1) + >>> (G(i1)*G(i0)).canon_bp() + G(i1)*G(i0) + >>> (G(i1)*A(i0)).canon_bp() + A(i0)*G(i1) + """ + if c not in (0, 1, None): + raise ValueError('`c` can assume only the values 0, 1 or None') + + i = sympify(i) + j = sympify(j) + + if i not in self._comm_symbols2i: + n = len(self._comm) + self._comm.append({}) + self._comm[n][0] = 0 + self._comm[0][n] = 0 + self._comm_symbols2i[i] = n + self._comm_i2symbol[n] = i + if j not in self._comm_symbols2i: + n = len(self._comm) + self._comm.append({}) + self._comm[0][n] = 0 + self._comm[n][0] = 0 + self._comm_symbols2i[j] = n + self._comm_i2symbol[n] = j + ni = self._comm_symbols2i[i] + nj = self._comm_symbols2i[j] + self._comm[ni][nj] = c + self._comm[nj][ni] = c + + """ + Cached sympy functions (e.g. expand) may have cached the results of + expressions involving tensors, but those results may not be valid after + changing the commutation properties. To stay on the safe side, we clear + the cache of all functions. + """ + clear_cache() + + def set_comms(self, *args): + """ + Set the commutation group numbers ``c`` for symbols ``i, j``. + + Parameters + ========== + + args : sequence of ``(i, j, c)`` + """ + for i, j, c in args: + self.set_comm(i, j, c) + + def get_comm(self, i, j): + """ + Return the commutation parameter for commutation group numbers ``i, j`` + + see ``_TensorManager.set_comm`` + """ + return self._comm[i].get(j, 0 if i == 0 or j == 0 else None) + + def clear(self): + """ + Clear the TensorManager. + """ + self._comm_init() + + +TensorManager = _TensorManager() + + +class TensorIndexType(Basic): + """ + A TensorIndexType is characterized by its name and its metric. + + Parameters + ========== + + name : name of the tensor type + dummy_name : name of the head of dummy indices + dim : dimension, it can be a symbol or an integer or ``None`` + eps_dim : dimension of the epsilon tensor + metric_symmetry : integer that denotes metric symmetry or ``None`` for no metric + metric_name : string with the name of the metric tensor + + Attributes + ========== + + ``metric`` : the metric tensor + ``delta`` : ``Kronecker delta`` + ``epsilon`` : the ``Levi-Civita epsilon`` tensor + ``data`` : (deprecated) a property to add ``ndarray`` values, to work in a specified basis. + + Notes + ===== + + The possible values of the ``metric_symmetry`` parameter are: + + ``1`` : metric tensor is fully symmetric + ``0`` : metric tensor possesses no index symmetry + ``-1`` : metric tensor is fully antisymmetric + ``None``: there is no metric tensor (metric equals to ``None``) + + The metric is assumed to be symmetric by default. It can also be set + to a custom tensor by the ``.set_metric()`` method. + + If there is a metric the metric is used to raise and lower indices. + + In the case of non-symmetric metric, the following raising and + lowering conventions will be adopted: + + ``psi(a) = g(a, b)*psi(-b); chi(-a) = chi(b)*g(-b, -a)`` + + From these it is easy to find: + + ``g(-a, b) = delta(-a, b)`` + + where ``delta(-a, b) = delta(b, -a)`` is the ``Kronecker delta`` + (see ``TensorIndex`` for the conventions on indices). + For antisymmetric metrics there is also the following equality: + + ``g(a, -b) = -delta(a, -b)`` + + If there is no metric it is not possible to raise or lower indices; + e.g. the index of the defining representation of ``SU(N)`` + is 'covariant' and the conjugate representation is + 'contravariant'; for ``N > 2`` they are linearly independent. + + ``eps_dim`` is by default equal to ``dim``, if the latter is an integer; + else it can be assigned (for use in naive dimensional regularization); + if ``eps_dim`` is not an integer ``epsilon`` is ``None``. + + Examples + ======== + + >>> from sympy.tensor.tensor import TensorIndexType + >>> Lorentz = TensorIndexType('Lorentz', dummy_name='L') + >>> Lorentz.metric + metric(Lorentz,Lorentz) + """ + + def __new__(cls, name, dummy_name=None, dim=None, eps_dim=None, + metric_symmetry=1, metric_name='metric', **kwargs): + if 'dummy_fmt' in kwargs: + dummy_fmt = kwargs['dummy_fmt'] + sympy_deprecation_warning( + f""" + The dummy_fmt keyword to TensorIndexType is deprecated. Use + dummy_name={dummy_fmt} instead. + """, + deprecated_since_version="1.5", + active_deprecations_target="deprecated-tensorindextype-dummy-fmt", + ) + dummy_name = dummy_fmt + + if isinstance(name, str): + name = Symbol(name) + + if dummy_name is None: + dummy_name = str(name)[0] + if isinstance(dummy_name, str): + dummy_name = Symbol(dummy_name) + + if dim is None: + dim = Symbol("dim_" + dummy_name.name) + else: + dim = sympify(dim) + + if eps_dim is None: + eps_dim = dim + else: + eps_dim = sympify(eps_dim) + + metric_symmetry = sympify(metric_symmetry) + + if isinstance(metric_name, str): + metric_name = Symbol(metric_name) + + if 'metric' in kwargs: + SymPyDeprecationWarning( + """ + The 'metric' keyword argument to TensorIndexType is + deprecated. Use the 'metric_symmetry' keyword argument or the + TensorIndexType.set_metric() method instead. + """, + deprecated_since_version="1.5", + active_deprecations_target="deprecated-tensorindextype-metric", + ) + metric = kwargs.get('metric') + if metric is not None: + if metric in (True, False, 0, 1): + metric_name = 'metric' + #metric_antisym = metric + else: + metric_name = metric.name + #metric_antisym = metric.antisym + + if metric: + metric_symmetry = -1 + else: + metric_symmetry = 1 + + obj = Basic.__new__(cls, name, dummy_name, dim, eps_dim, + metric_symmetry, metric_name) + + obj._autogenerated = [] + return obj + + @property + def name(self): + return self.args[0].name + + @property + def dummy_name(self): + return self.args[1].name + + @property + def dim(self): + return self.args[2] + + @property + def eps_dim(self): + return self.args[3] + + @memoize_property + def metric(self): + metric_symmetry = self.args[4] + metric_name = self.args[5] + if metric_symmetry is None: + return None + + if metric_symmetry == 0: + symmetry = TensorSymmetry.no_symmetry(2) + elif metric_symmetry == 1: + symmetry = TensorSymmetry.fully_symmetric(2) + elif metric_symmetry == -1: + symmetry = TensorSymmetry.fully_symmetric(-2) + + return TensorHead(metric_name, [self]*2, symmetry) + + @memoize_property + def delta(self): + return TensorHead('KD', [self]*2, TensorSymmetry.fully_symmetric(2)) + + @memoize_property + def epsilon(self): + if not isinstance(self.eps_dim, (SYMPY_INTS, Integer)): + return None + symmetry = TensorSymmetry.fully_symmetric(-self.eps_dim) + return TensorHead('Eps', [self]*self.eps_dim, symmetry) + + def set_metric(self, tensor): + self._metric = tensor + + def __lt__(self, other): + return self.name < other.name + + def __str__(self): + return self.name + + __repr__ = __str__ + + # Everything below this line is deprecated + + @property + def data(self): + deprecate_data() + with ignore_warnings(SymPyDeprecationWarning): + return _tensor_data_substitution_dict[self] + + @data.setter + def data(self, data): + deprecate_data() + # This assignment is a bit controversial, should metric components be assigned + # to the metric only or also to the TensorIndexType object? The advantage here + # is the ability to assign a 1D array and transform it to a 2D diagonal array. + from .array import MutableDenseNDimArray + + data = _TensorDataLazyEvaluator.parse_data(data) + if data.rank() > 2: + raise ValueError("data have to be of rank 1 (diagonal metric) or 2.") + if data.rank() == 1: + if self.dim.is_number: + nda_dim = data.shape[0] + if nda_dim != self.dim: + raise ValueError("Dimension mismatch") + + dim = data.shape[0] + newndarray = MutableDenseNDimArray.zeros(dim, dim) + for i, val in enumerate(data): + newndarray[i, i] = val + data = newndarray + dim1, dim2 = data.shape + if dim1 != dim2: + raise ValueError("Non-square matrix tensor.") + if self.dim.is_number: + if self.dim != dim1: + raise ValueError("Dimension mismatch") + _tensor_data_substitution_dict[self] = data + _tensor_data_substitution_dict.add_metric_data(self.metric, data) + with ignore_warnings(SymPyDeprecationWarning): + delta = self.get_kronecker_delta() + i1 = TensorIndex('i1', self) + i2 = TensorIndex('i2', self) + with ignore_warnings(SymPyDeprecationWarning): + delta(i1, -i2).data = _TensorDataLazyEvaluator.parse_data(eye(dim1)) + + @data.deleter + def data(self): + deprecate_data() + with ignore_warnings(SymPyDeprecationWarning): + if self in _tensor_data_substitution_dict: + del _tensor_data_substitution_dict[self] + if self.metric in _tensor_data_substitution_dict: + del _tensor_data_substitution_dict[self.metric] + + @deprecated( + """ + The TensorIndexType.get_kronecker_delta() method is deprecated. Use + the TensorIndexType.delta attribute instead. + """, + deprecated_since_version="1.5", + active_deprecations_target="deprecated-tensorindextype-methods", + ) + def get_kronecker_delta(self): + sym2 = TensorSymmetry(get_symmetric_group_sgs(2)) + delta = TensorHead('KD', [self]*2, sym2) + return delta + + @deprecated( + """ + The TensorIndexType.get_epsilon() method is deprecated. Use + the TensorIndexType.epsilon attribute instead. + """, + deprecated_since_version="1.5", + active_deprecations_target="deprecated-tensorindextype-methods", + ) + def get_epsilon(self): + if not isinstance(self._eps_dim, (SYMPY_INTS, Integer)): + return None + sym = TensorSymmetry(get_symmetric_group_sgs(self._eps_dim, 1)) + epsilon = TensorHead('Eps', [self]*self._eps_dim, sym) + return epsilon + + def _components_data_full_destroy(self): + """ + EXPERIMENTAL: do not rely on this API method. + + This destroys components data associated to the ``TensorIndexType``, if + any, specifically: + + * metric tensor data + * Kronecker tensor data + """ + if self in _tensor_data_substitution_dict: + del _tensor_data_substitution_dict[self] + + def delete_tensmul_data(key): + if key in _tensor_data_substitution_dict._substitutions_dict_tensmul: + del _tensor_data_substitution_dict._substitutions_dict_tensmul[key] + + # delete metric data: + delete_tensmul_data((self.metric, True, True)) + delete_tensmul_data((self.metric, True, False)) + delete_tensmul_data((self.metric, False, True)) + delete_tensmul_data((self.metric, False, False)) + + # delete delta tensor data: + delta = self.get_kronecker_delta() + if delta in _tensor_data_substitution_dict: + del _tensor_data_substitution_dict[delta] + + +class TensorIndex(Basic): + """ + Represents a tensor index + + Parameters + ========== + + name : name of the index, or ``True`` if you want it to be automatically assigned + tensor_index_type : ``TensorIndexType`` of the index + is_up : flag for contravariant index (is_up=True by default) + + Attributes + ========== + + ``name`` + ``tensor_index_type`` + ``is_up`` + + Notes + ===== + + Tensor indices are contracted with the Einstein summation convention. + + An index can be in contravariant or in covariant form; in the latter + case it is represented prepending a ``-`` to the index name. Adding + ``-`` to a covariant (is_up=False) index makes it contravariant. + + Dummy indices have a name with head given by + ``tensor_inde_type.dummy_name`` with underscore and a number. + + Similar to ``symbols`` multiple contravariant indices can be created + at once using ``tensor_indices(s, typ)``, where ``s`` is a string + of names. + + + Examples + ======== + + >>> from sympy.tensor.tensor import TensorIndexType, TensorIndex, TensorHead, tensor_indices + >>> Lorentz = TensorIndexType('Lorentz', dummy_name='L') + >>> mu = TensorIndex('mu', Lorentz, is_up=False) + >>> nu, rho = tensor_indices('nu, rho', Lorentz) + >>> A = TensorHead('A', [Lorentz, Lorentz]) + >>> A(mu, nu) + A(-mu, nu) + >>> A(-mu, -rho) + A(mu, -rho) + >>> A(mu, -mu) + A(-L_0, L_0) + """ + def __new__(cls, name, tensor_index_type, is_up=True): + if isinstance(name, str): + name_symbol = Symbol(name) + elif isinstance(name, Symbol): + name_symbol = name + elif name is True: + name = "_i{}".format(len(tensor_index_type._autogenerated)) + name_symbol = Symbol(name) + tensor_index_type._autogenerated.append(name_symbol) + else: + raise ValueError("invalid name") + + is_up = sympify(is_up) + return Basic.__new__(cls, name_symbol, tensor_index_type, is_up) + + @property + def name(self): + return self.args[0].name + + @property + def tensor_index_type(self): + return self.args[1] + + @property + def is_up(self): + return self.args[2] + + def _print(self): + s = self.name + if not self.is_up: + s = '-%s' % s + return s + + def __lt__(self, other): + return ((self.tensor_index_type, self.name) < + (other.tensor_index_type, other.name)) + + def __neg__(self): + t1 = TensorIndex(self.name, self.tensor_index_type, + (not self.is_up)) + return t1 + + +def tensor_indices(s, typ): + """ + Returns list of tensor indices given their names and their types. + + Parameters + ========== + + s : string of comma separated names of indices + + typ : ``TensorIndexType`` of the indices + + Examples + ======== + + >>> from sympy.tensor.tensor import TensorIndexType, tensor_indices + >>> Lorentz = TensorIndexType('Lorentz', dummy_name='L') + >>> a, b, c, d = tensor_indices('a,b,c,d', Lorentz) + """ + if isinstance(s, str): + a = [x.name for x in symbols(s, seq=True)] + else: + raise ValueError('expecting a string') + + tilist = [TensorIndex(i, typ) for i in a] + if len(tilist) == 1: + return tilist[0] + return tilist + + +class TensorSymmetry(Basic): + """ + Monoterm symmetry of a tensor (i.e. any symmetric or anti-symmetric + index permutation). For the relevant terminology see ``tensor_can.py`` + section of the combinatorics module. + + Parameters + ========== + + bsgs : tuple ``(base, sgs)`` BSGS of the symmetry of the tensor + + Attributes + ========== + + ``base`` : base of the BSGS + ``generators`` : generators of the BSGS + ``rank`` : rank of the tensor + + Notes + ===== + + A tensor can have an arbitrary monoterm symmetry provided by its BSGS. + Multiterm symmetries, like the cyclic symmetry of the Riemann tensor + (i.e., Bianchi identity), are not covered. See combinatorics module for + information on how to generate BSGS for a general index permutation group. + Simple symmetries can be generated using built-in methods. + + See Also + ======== + + sympy.combinatorics.tensor_can.get_symmetric_group_sgs + + Examples + ======== + + Define a symmetric tensor of rank 2 + + >>> from sympy.tensor.tensor import TensorIndexType, TensorSymmetry, get_symmetric_group_sgs, TensorHead + >>> Lorentz = TensorIndexType('Lorentz', dummy_name='L') + >>> sym = TensorSymmetry(get_symmetric_group_sgs(2)) + >>> T = TensorHead('T', [Lorentz]*2, sym) + + Note, that the same can also be done using built-in TensorSymmetry methods + + >>> sym2 = TensorSymmetry.fully_symmetric(2) + >>> sym == sym2 + True + """ + def __new__(cls, *args, **kw_args): + if len(args) == 1: + base, generators = args[0] + elif len(args) == 2: + base, generators = args + else: + raise TypeError("bsgs required, either two separate parameters or one tuple") + + if not isinstance(base, Tuple): + base = Tuple(*base) + if not isinstance(generators, Tuple): + generators = Tuple(*generators) + + return Basic.__new__(cls, base, generators, **kw_args) + + @property + def base(self): + return self.args[0] + + @property + def generators(self): + return self.args[1] + + @property + def rank(self): + return self.generators[0].size - 2 + + @classmethod + def fully_symmetric(cls, rank): + """ + Returns a fully symmetric (antisymmetric if ``rank``<0) + TensorSymmetry object for ``abs(rank)`` indices. + """ + if rank > 0: + bsgs = get_symmetric_group_sgs(rank, False) + elif rank < 0: + bsgs = get_symmetric_group_sgs(-rank, True) + elif rank == 0: + bsgs = ([], [Permutation(1)]) + return TensorSymmetry(bsgs) + + @classmethod + def direct_product(cls, *args): + """ + Returns a TensorSymmetry object that is being a direct product of + fully (anti-)symmetric index permutation groups. + + Notes + ===== + + Some examples for different values of ``(*args)``: + ``(1)`` vector, equivalent to ``TensorSymmetry.fully_symmetric(1)`` + ``(2)`` tensor with 2 symmetric indices, equivalent to ``.fully_symmetric(2)`` + ``(-2)`` tensor with 2 antisymmetric indices, equivalent to ``.fully_symmetric(-2)`` + ``(2, -2)`` tensor with the first 2 indices commuting and the last 2 anticommuting + ``(1, 1, 1)`` tensor with 3 indices without any symmetry + """ + base, sgs = [], [Permutation(1)] + for arg in args: + if arg > 0: + bsgs2 = get_symmetric_group_sgs(arg, False) + elif arg < 0: + bsgs2 = get_symmetric_group_sgs(-arg, True) + else: + continue + base, sgs = bsgs_direct_product(base, sgs, *bsgs2) + + return TensorSymmetry(base, sgs) + + @classmethod + def riemann(cls): + """ + Returns a monotorem symmetry of the Riemann tensor + """ + return TensorSymmetry(riemann_bsgs) + + @classmethod + def no_symmetry(cls, rank): + """ + TensorSymmetry object for ``rank`` indices with no symmetry + """ + return TensorSymmetry([], [Permutation(rank+1)]) + + +@deprecated( + """ + The tensorsymmetry() function is deprecated. Use the TensorSymmetry + constructor instead. + """, + deprecated_since_version="1.5", + active_deprecations_target="deprecated-tensorsymmetry", +) +def tensorsymmetry(*args): + """ + Returns a ``TensorSymmetry`` object. This method is deprecated, use + ``TensorSymmetry.direct_product()`` or ``.riemann()`` instead. + + Explanation + =========== + + One can represent a tensor with any monoterm slot symmetry group + using a BSGS. + + ``args`` can be a BSGS + ``args[0]`` base + ``args[1]`` sgs + + Usually tensors are in (direct products of) representations + of the symmetric group; + ``args`` can be a list of lists representing the shapes of Young tableaux + + Notes + ===== + + For instance: + ``[[1]]`` vector + ``[[1]*n]`` symmetric tensor of rank ``n`` + ``[[n]]`` antisymmetric tensor of rank ``n`` + ``[[2, 2]]`` monoterm slot symmetry of the Riemann tensor + ``[[1],[1]]`` vector*vector + ``[[2],[1],[1]`` (antisymmetric tensor)*vector*vector + + Notice that with the shape ``[2, 2]`` we associate only the monoterm + symmetries of the Riemann tensor; this is an abuse of notation, + since the shape ``[2, 2]`` corresponds usually to the irreducible + representation characterized by the monoterm symmetries and by the + cyclic symmetry. + """ + from sympy.combinatorics import Permutation + + def tableau2bsgs(a): + if len(a) == 1: + # antisymmetric vector + n = a[0] + bsgs = get_symmetric_group_sgs(n, 1) + else: + if all(x == 1 for x in a): + # symmetric vector + n = len(a) + bsgs = get_symmetric_group_sgs(n) + elif a == [2, 2]: + bsgs = riemann_bsgs + else: + raise NotImplementedError + return bsgs + + if not args: + return TensorSymmetry(Tuple(), Tuple(Permutation(1))) + + if len(args) == 2 and isinstance(args[1][0], Permutation): + return TensorSymmetry(args) + base, sgs = tableau2bsgs(args[0]) + for a in args[1:]: + basex, sgsx = tableau2bsgs(a) + base, sgs = bsgs_direct_product(base, sgs, basex, sgsx) + return TensorSymmetry(Tuple(base, sgs)) + +@deprecated( + "TensorType is deprecated. Use tensor_heads() instead.", + deprecated_since_version="1.5", + active_deprecations_target="deprecated-tensortype", +) +class TensorType(Basic): + """ + Class of tensor types. Deprecated, use tensor_heads() instead. + + Parameters + ========== + + index_types : list of ``TensorIndexType`` of the tensor indices + symmetry : ``TensorSymmetry`` of the tensor + + Attributes + ========== + + ``index_types`` + ``symmetry`` + ``types`` : list of ``TensorIndexType`` without repetitions + """ + is_commutative = False + + def __new__(cls, index_types, symmetry, **kw_args): + assert symmetry.rank == len(index_types) + obj = Basic.__new__(cls, Tuple(*index_types), symmetry, **kw_args) + return obj + + @property + def index_types(self): + return self.args[0] + + @property + def symmetry(self): + return self.args[1] + + @property + def types(self): + return sorted(set(self.index_types), key=lambda x: x.name) + + def __str__(self): + return 'TensorType(%s)' % ([str(x) for x in self.index_types]) + + def __call__(self, s, comm=0): + """ + Return a TensorHead object or a list of TensorHead objects. + + Parameters + ========== + + s : name or string of names. + + comm : Commutation group. + + see ``_TensorManager.set_comm`` + """ + if isinstance(s, str): + names = [x.name for x in symbols(s, seq=True)] + else: + raise ValueError('expecting a string') + if len(names) == 1: + return TensorHead(names[0], self.index_types, self.symmetry, comm) + else: + return [TensorHead(name, self.index_types, self.symmetry, comm) for name in names] + + +@deprecated( + """ + The tensorhead() function is deprecated. Use tensor_heads() instead. + """, + deprecated_since_version="1.5", + active_deprecations_target="deprecated-tensorhead", +) +def tensorhead(name, typ, sym=None, comm=0): + """ + Function generating tensorhead(s). This method is deprecated, + use TensorHead constructor or tensor_heads() instead. + + Parameters + ========== + + name : name or sequence of names (as in ``symbols``) + + typ : index types + + sym : same as ``*args`` in ``tensorsymmetry`` + + comm : commutation group number + see ``_TensorManager.set_comm`` + """ + if sym is None: + sym = [[1] for i in range(len(typ))] + with ignore_warnings(SymPyDeprecationWarning): + sym = tensorsymmetry(*sym) + return TensorHead(name, typ, sym, comm) + + +class TensorHead(Basic): + """ + Tensor head of the tensor. + + Parameters + ========== + + name : name of the tensor + index_types : list of TensorIndexType + symmetry : TensorSymmetry of the tensor + comm : commutation group number + + Attributes + ========== + + ``name`` + ``index_types`` + ``rank`` : total number of indices + ``symmetry`` + ``comm`` : commutation group + + Notes + ===== + + Similar to ``symbols`` multiple TensorHeads can be created using + ``tensorhead(s, typ, sym=None, comm=0)`` function, where ``s`` + is the string of names and ``sym`` is the monoterm tensor symmetry + (see ``tensorsymmetry``). + + A ``TensorHead`` belongs to a commutation group, defined by a + symbol on number ``comm`` (see ``_TensorManager.set_comm``); + tensors in a commutation group have the same commutation properties; + by default ``comm`` is ``0``, the group of the commuting tensors. + + Examples + ======== + + Define a fully antisymmetric tensor of rank 2: + + >>> from sympy.tensor.tensor import TensorIndexType, TensorHead, TensorSymmetry + >>> Lorentz = TensorIndexType('Lorentz', dummy_name='L') + >>> asym2 = TensorSymmetry.fully_symmetric(-2) + >>> A = TensorHead('A', [Lorentz, Lorentz], asym2) + + Examples with ndarray values, the components data assigned to the + ``TensorHead`` object are assumed to be in a fully-contravariant + representation. In case it is necessary to assign components data which + represents the values of a non-fully covariant tensor, see the other + examples. + + >>> from sympy.tensor.tensor import tensor_indices + >>> from sympy import diag + >>> Lorentz = TensorIndexType('Lorentz', dummy_name='L') + >>> i0, i1 = tensor_indices('i0:2', Lorentz) + + Specify a replacement dictionary to keep track of the arrays to use for + replacements in the tensorial expression. The ``TensorIndexType`` is + associated to the metric used for contractions (in fully covariant form): + + >>> repl = {Lorentz: diag(1, -1, -1, -1)} + + Let's see some examples of working with components with the electromagnetic + tensor: + + >>> from sympy import symbols + >>> Ex, Ey, Ez, Bx, By, Bz = symbols('E_x E_y E_z B_x B_y B_z') + >>> c = symbols('c', positive=True) + + Let's define `F`, an antisymmetric tensor: + + >>> F = TensorHead('F', [Lorentz, Lorentz], asym2) + + Let's update the dictionary to contain the matrix to use in the + replacements: + + >>> repl.update({F(-i0, -i1): [ + ... [0, Ex/c, Ey/c, Ez/c], + ... [-Ex/c, 0, -Bz, By], + ... [-Ey/c, Bz, 0, -Bx], + ... [-Ez/c, -By, Bx, 0]]}) + + Now it is possible to retrieve the contravariant form of the Electromagnetic + tensor: + + >>> F(i0, i1).replace_with_arrays(repl, [i0, i1]) + [[0, -E_x/c, -E_y/c, -E_z/c], [E_x/c, 0, -B_z, B_y], [E_y/c, B_z, 0, -B_x], [E_z/c, -B_y, B_x, 0]] + + and the mixed contravariant-covariant form: + + >>> F(i0, -i1).replace_with_arrays(repl, [i0, -i1]) + [[0, E_x/c, E_y/c, E_z/c], [E_x/c, 0, B_z, -B_y], [E_y/c, -B_z, 0, B_x], [E_z/c, B_y, -B_x, 0]] + + Energy-momentum of a particle may be represented as: + + >>> from sympy import symbols + >>> P = TensorHead('P', [Lorentz], TensorSymmetry.no_symmetry(1)) + >>> E, px, py, pz = symbols('E p_x p_y p_z', positive=True) + >>> repl.update({P(i0): [E, px, py, pz]}) + + The contravariant and covariant components are, respectively: + + >>> P(i0).replace_with_arrays(repl, [i0]) + [E, p_x, p_y, p_z] + >>> P(-i0).replace_with_arrays(repl, [-i0]) + [E, -p_x, -p_y, -p_z] + + The contraction of a 1-index tensor by itself: + + >>> expr = P(i0)*P(-i0) + >>> expr.replace_with_arrays(repl, []) + E**2 - p_x**2 - p_y**2 - p_z**2 + """ + is_commutative = False + + def __new__(cls, name, index_types, symmetry=None, comm=0): + if isinstance(name, str): + name_symbol = Symbol(name) + elif isinstance(name, Symbol): + name_symbol = name + else: + raise ValueError("invalid name") + + if symmetry is None: + symmetry = TensorSymmetry.no_symmetry(len(index_types)) + else: + assert symmetry.rank == len(index_types) + + obj = Basic.__new__(cls, name_symbol, Tuple(*index_types), symmetry, sympify(comm)) + return obj + + @property + def name(self): + return self.args[0].name + + @property + def index_types(self): + return list(self.args[1]) + + @property + def symmetry(self): + return self.args[2] + + @property + def comm(self): + return TensorManager.comm_symbols2i(self.args[3]) + + @property + def rank(self): + return len(self.index_types) + + def __lt__(self, other): + return (self.name, self.index_types) < (other.name, other.index_types) + + def commutes_with(self, other): + """ + Returns ``0`` if ``self`` and ``other`` commute, ``1`` if they anticommute. + + Returns ``None`` if ``self`` and ``other`` neither commute nor anticommute. + """ + r = TensorManager.get_comm(self.comm, other.comm) + return r + + def _print(self): + return '%s(%s)' %(self.name, ','.join([str(x) for x in self.index_types])) + + def __call__(self, *indices, **kw_args): + """ + Returns a tensor with indices. + + Explanation + =========== + + There is a special behavior in case of indices denoted by ``True``, + they are considered auto-matrix indices, their slots are automatically + filled, and confer to the tensor the behavior of a matrix or vector + upon multiplication with another tensor containing auto-matrix indices + of the same ``TensorIndexType``. This means indices get summed over the + same way as in matrix multiplication. For matrix behavior, define two + auto-matrix indices, for vector behavior define just one. + + Indices can also be strings, in which case the attribute + ``index_types`` is used to convert them to proper ``TensorIndex``. + + Examples + ======== + + >>> from sympy.tensor.tensor import TensorIndexType, tensor_indices, TensorSymmetry, TensorHead + >>> Lorentz = TensorIndexType('Lorentz', dummy_name='L') + >>> a, b = tensor_indices('a,b', Lorentz) + >>> A = TensorHead('A', [Lorentz]*2, TensorSymmetry.no_symmetry(2)) + >>> t = A(a, -b) + >>> t + A(a, -b) + + """ + + updated_indices = [] + for idx, typ in zip(indices, self.index_types): + if isinstance(idx, str): + idx = idx.strip().replace(" ", "") + if idx.startswith('-'): + updated_indices.append(TensorIndex(idx[1:], typ, + is_up=False)) + else: + updated_indices.append(TensorIndex(idx, typ)) + else: + updated_indices.append(idx) + + updated_indices += indices[len(updated_indices):] + + tensor = Tensor(self, updated_indices, **kw_args) + return tensor.doit() + + # Everything below this line is deprecated + + def __pow__(self, other): + deprecate_data() + with ignore_warnings(SymPyDeprecationWarning): + if self.data is None: + raise ValueError("No power on abstract tensors.") + from .array import tensorproduct, tensorcontraction + metrics = [_.data for _ in self.index_types] + + marray = self.data + marraydim = marray.rank() + for metric in metrics: + marray = tensorproduct(marray, metric, marray) + marray = tensorcontraction(marray, (0, marraydim), (marraydim+1, marraydim+2)) + + return marray ** (other * S.Half) + + @property + def data(self): + deprecate_data() + with ignore_warnings(SymPyDeprecationWarning): + return _tensor_data_substitution_dict[self] + + @data.setter + def data(self, data): + deprecate_data() + with ignore_warnings(SymPyDeprecationWarning): + _tensor_data_substitution_dict[self] = data + + @data.deleter + def data(self): + deprecate_data() + if self in _tensor_data_substitution_dict: + del _tensor_data_substitution_dict[self] + + def __iter__(self): + deprecate_data() + with ignore_warnings(SymPyDeprecationWarning): + return self.data.__iter__() + + def _components_data_full_destroy(self): + """ + EXPERIMENTAL: do not rely on this API method. + + Destroy components data associated to the ``TensorHead`` object, this + checks for attached components data, and destroys components data too. + """ + # do not garbage collect Kronecker tensor (it should be done by + # ``TensorIndexType`` garbage collection) + deprecate_data() + if self.name == "KD": + return + + # the data attached to a tensor must be deleted only by the TensorHead + # destructor. If the TensorHead is deleted, it means that there are no + # more instances of that tensor anywhere. + if self in _tensor_data_substitution_dict: + del _tensor_data_substitution_dict[self] + + +def tensor_heads(s, index_types, symmetry=None, comm=0): + """ + Returns a sequence of TensorHeads from a string `s` + """ + if isinstance(s, str): + names = [x.name for x in symbols(s, seq=True)] + else: + raise ValueError('expecting a string') + + thlist = [TensorHead(name, index_types, symmetry, comm) for name in names] + if len(thlist) == 1: + return thlist[0] + return thlist + + +class TensExpr(Expr, ABC): + """ + Abstract base class for tensor expressions + + Notes + ===== + + A tensor expression is an expression formed by tensors; + currently the sums of tensors are distributed. + + A ``TensExpr`` can be a ``TensAdd`` or a ``TensMul``. + + ``TensMul`` objects are formed by products of component tensors, + and include a coefficient, which is a SymPy expression. + + + In the internal representation contracted indices are represented + by ``(ipos1, ipos2, icomp1, icomp2)``, where ``icomp1`` is the position + of the component tensor with contravariant index, ``ipos1`` is the + slot which the index occupies in that component tensor. + + Contracted indices are therefore nameless in the internal representation. + """ + + _op_priority = 12.0 + is_commutative = False + + def __neg__(self): + return self*S.NegativeOne + + def __abs__(self): + raise NotImplementedError + + def __add__(self, other): + return TensAdd(self, other).doit(deep=False) + + def __radd__(self, other): + return TensAdd(other, self).doit(deep=False) + + def __sub__(self, other): + return TensAdd(self, -other).doit(deep=False) + + def __rsub__(self, other): + return TensAdd(other, -self).doit(deep=False) + + def __mul__(self, other): + """ + Multiply two tensors using Einstein summation convention. + + Explanation + =========== + + If the two tensors have an index in common, one contravariant + and the other covariant, in their product the indices are summed + + Examples + ======== + + >>> from sympy.tensor.tensor import TensorIndexType, tensor_indices, tensor_heads + >>> Lorentz = TensorIndexType('Lorentz', dummy_name='L') + >>> m0, m1, m2 = tensor_indices('m0,m1,m2', Lorentz) + >>> g = Lorentz.metric + >>> p, q = tensor_heads('p,q', [Lorentz]) + >>> t1 = p(m0) + >>> t2 = q(-m0) + >>> t1*t2 + p(L_0)*q(-L_0) + """ + return TensMul(self, other).doit(deep=False) + + def __rmul__(self, other): + return TensMul(other, self).doit(deep=False) + + def __truediv__(self, other): + other = _sympify(other) + if isinstance(other, TensExpr): + raise ValueError('cannot divide by a tensor') + return TensMul(self, S.One/other).doit(deep=False) + + def __rtruediv__(self, other): + raise ValueError('cannot divide by a tensor') + + def __pow__(self, other): + deprecate_data() + with ignore_warnings(SymPyDeprecationWarning): + if self.data is None: + raise ValueError("No power without ndarray data.") + from .array import tensorproduct, tensorcontraction + free = self.free + marray = self.data + mdim = marray.rank() + for metric in free: + marray = tensorcontraction( + tensorproduct( + marray, + metric[0].tensor_index_type.data, + marray), + (0, mdim), (mdim+1, mdim+2) + ) + return marray ** (other * S.Half) + + def __rpow__(self, other): + raise NotImplementedError + + @property + @abstractmethod + def nocoeff(self): + raise NotImplementedError("abstract method") + + @property + @abstractmethod + def coeff(self): + raise NotImplementedError("abstract method") + + @abstractmethod + def get_indices(self): + raise NotImplementedError("abstract method") + + @abstractmethod + def get_free_indices(self) -> list[TensorIndex]: + raise NotImplementedError("abstract method") + + @abstractmethod + def _replace_indices(self, repl: dict[TensorIndex, TensorIndex]) -> TensExpr: + raise NotImplementedError("abstract method") + + def fun_eval(self, *index_tuples): + deprecate_fun_eval() + return self.substitute_indices(*index_tuples) + + def get_matrix(self): + """ + DEPRECATED: do not use. + + Returns ndarray components data as a matrix, if components data are + available and ndarray dimension does not exceed 2. + """ + from sympy.matrices.dense import Matrix + deprecate_data() + with ignore_warnings(SymPyDeprecationWarning): + if 0 < self.rank <= 2: + rows = self.data.shape[0] + columns = self.data.shape[1] if self.rank == 2 else 1 + if self.rank == 2: + mat_list = [] * rows + for i in range(rows): + mat_list.append([]) + for j in range(columns): + mat_list[i].append(self[i, j]) + else: + mat_list = [None] * rows + for i in range(rows): + mat_list[i] = self[i] + return Matrix(mat_list) + else: + raise NotImplementedError( + "missing multidimensional reduction to matrix.") + + @staticmethod + def _get_indices_permutation(indices1, indices2): + return [indices1.index(i) for i in indices2] + + def _get_free_indices_set(self): + indset = set() + for arg in self.args: + if isinstance(arg, TensExpr): + indset.update(arg._get_free_indices_set()) + return indset + + def _get_dummy_indices_set(self): + indset = set() + for arg in self.args: + if isinstance(arg, TensExpr): + indset.update(arg._get_dummy_indices_set()) + return indset + + def _get_indices_set(self): + indset = set() + for arg in self.args: + if isinstance(arg, TensExpr): + indset.update(arg._get_indices_set()) + return indset + + @property + def _iterate_dummy_indices(self): + dummy_set = self._get_dummy_indices_set() + + def recursor(expr, pos): + if isinstance(expr, TensorIndex): + if expr in dummy_set: + yield (expr, pos) + elif isinstance(expr, (Tuple, TensExpr)): + for p, arg in enumerate(expr.args): + yield from recursor(arg, pos+(p,)) + + return recursor(self, ()) + + @property + def _iterate_free_indices(self): + free_set = self._get_free_indices_set() + + def recursor(expr, pos): + if isinstance(expr, TensorIndex): + if expr in free_set: + yield (expr, pos) + elif isinstance(expr, (Tuple, TensExpr)): + for p, arg in enumerate(expr.args): + yield from recursor(arg, pos+(p,)) + + return recursor(self, ()) + + @property + def _iterate_indices(self): + def recursor(expr, pos): + if isinstance(expr, TensorIndex): + yield (expr, pos) + elif isinstance(expr, (Tuple, TensExpr)): + for p, arg in enumerate(expr.args): + yield from recursor(arg, pos+(p,)) + + return recursor(self, ()) + + @staticmethod + def _contract_and_permute_with_metric(metric, array, pos, dim): + # TODO: add possibility of metric after (spinors) + from .array import tensorcontraction, tensorproduct, permutedims + + array = tensorcontraction(tensorproduct(metric, array), (1, 2+pos)) + permu = list(range(dim)) + permu[0], permu[pos] = permu[pos], permu[0] + return permutedims(array, permu) + + @staticmethod + def _match_indices_with_other_tensor(array, free_ind1, free_ind2, replacement_dict): + from .array import permutedims + + index_types1 = [i.tensor_index_type for i in free_ind1] + + # Check if variance of indices needs to be fixed: + pos2up = [] + pos2down = [] + free2remaining = free_ind2[:] + for pos1, index1 in enumerate(free_ind1): + if index1 in free2remaining: + pos2 = free2remaining.index(index1) + free2remaining[pos2] = None + continue + if -index1 in free2remaining: + pos2 = free2remaining.index(-index1) + free2remaining[pos2] = None + free_ind2[pos2] = index1 + if index1.is_up: + pos2up.append(pos2) + else: + pos2down.append(pos2) + else: + index2 = free2remaining[pos1] + if index2 is None: + raise ValueError("incompatible indices: %s and %s" % (free_ind1, free_ind2)) + free2remaining[pos1] = None + free_ind2[pos1] = index1 + if index1.is_up ^ index2.is_up: + if index1.is_up: + pos2up.append(pos1) + else: + pos2down.append(pos1) + + if len(set(free_ind1) & set(free_ind2)) < len(free_ind1): + raise ValueError("incompatible indices: %s and %s" % (free_ind1, free_ind2)) + + # Raise indices: + for pos in pos2up: + index_type_pos = index_types1[pos] + if index_type_pos not in replacement_dict: + raise ValueError("No metric provided to lower index") + metric = replacement_dict[index_type_pos] + metric_inverse = _TensorDataLazyEvaluator.inverse_matrix(metric) + array = TensExpr._contract_and_permute_with_metric(metric_inverse, array, pos, len(free_ind1)) + # Lower indices: + for pos in pos2down: + index_type_pos = index_types1[pos] + if index_type_pos not in replacement_dict: + raise ValueError("No metric provided to lower index") + metric = replacement_dict[index_type_pos] + array = TensExpr._contract_and_permute_with_metric(metric, array, pos, len(free_ind1)) + + if free_ind1: + permutation = TensExpr._get_indices_permutation(free_ind2, free_ind1) + array = permutedims(array, permutation) + + if hasattr(array, "rank") and array.rank() == 0: + array = array[()] + + return free_ind2, array + + def replace_with_arrays(self, replacement_dict, indices=None): + """ + Replace the tensorial expressions with arrays. The final array will + correspond to the N-dimensional array with indices arranged according + to ``indices``. + + Parameters + ========== + + replacement_dict + dictionary containing the replacement rules for tensors. + indices + the index order with respect to which the array is read. The + original index order will be used if no value is passed. + + Examples + ======== + + >>> from sympy.tensor.tensor import TensorIndexType, tensor_indices + >>> from sympy.tensor.tensor import TensorHead + >>> from sympy import symbols, diag + + >>> L = TensorIndexType("L") + >>> i, j = tensor_indices("i j", L) + >>> A = TensorHead("A", [L]) + >>> A(i).replace_with_arrays({A(i): [1, 2]}, [i]) + [1, 2] + + Since 'indices' is optional, we can also call replace_with_arrays by + this way if no specific index order is needed: + + >>> A(i).replace_with_arrays({A(i): [1, 2]}) + [1, 2] + + >>> expr = A(i)*A(j) + >>> expr.replace_with_arrays({A(i): [1, 2]}) + [[1, 2], [2, 4]] + + For contractions, specify the metric of the ``TensorIndexType``, which + in this case is ``L``, in its covariant form: + + >>> expr = A(i)*A(-i) + >>> expr.replace_with_arrays({A(i): [1, 2], L: diag(1, -1)}) + -3 + + Symmetrization of an array: + + >>> H = TensorHead("H", [L, L]) + >>> a, b, c, d = symbols("a b c d") + >>> expr = H(i, j)/2 + H(j, i)/2 + >>> expr.replace_with_arrays({H(i, j): [[a, b], [c, d]]}) + [[a, b/2 + c/2], [b/2 + c/2, d]] + + Anti-symmetrization of an array: + + >>> expr = H(i, j)/2 - H(j, i)/2 + >>> repl = {H(i, j): [[a, b], [c, d]]} + >>> expr.replace_with_arrays(repl) + [[0, b/2 - c/2], [-b/2 + c/2, 0]] + + The same expression can be read as the transpose by inverting ``i`` and + ``j``: + + >>> expr.replace_with_arrays(repl, [j, i]) + [[0, -b/2 + c/2], [b/2 - c/2, 0]] + """ + from .array import Array + + indices = indices or [] + remap = {k.args[0] if k.is_up else -k.args[0]: k for k in self.get_free_indices()} + for i, index in enumerate(indices): + if isinstance(index, (Symbol, Mul)): + if index in remap: + indices[i] = remap[index] + else: + indices[i] = -remap[-index] + + replacement_dict = {tensor: Array(array) for tensor, array in replacement_dict.items()} + + # Check dimensions of replaced arrays: + for tensor, array in replacement_dict.items(): + if isinstance(tensor, TensorIndexType): + expected_shape = [tensor.dim for i in range(2)] + else: + expected_shape = [index_type.dim for index_type in tensor.index_types] + if len(expected_shape) != array.rank() or (not all(dim1 == dim2 if + dim1.is_number else True for dim1, dim2 in zip(expected_shape, + array.shape))): + raise ValueError("shapes for tensor %s expected to be %s, "\ + "replacement array shape is %s" % (tensor, expected_shape, + array.shape)) + + ret_indices, array = self._extract_data(replacement_dict) + + last_indices, array = self._match_indices_with_other_tensor(array, indices, ret_indices, replacement_dict) + return array + + def _check_add_Sum(self, expr, index_symbols): + from sympy.concrete.summations import Sum + indices = self.get_indices() + dum = self.dum + sum_indices = [ (index_symbols[i], 0, + indices[i].tensor_index_type.dim-1) for i, j in dum] + if sum_indices: + expr = Sum(expr, *sum_indices) + return expr + + def _expand_partial_derivative(self): + # simply delegate the _expand_partial_derivative() to + # its arguments to expand a possibly found PartialDerivative + return self.func(*[ + a._expand_partial_derivative() + if isinstance(a, TensExpr) else a + for a in self.args]) + + def _matches_simple(self, expr, repl_dict=None, old=False): + """ + Matches assuming there are no wild objects in self. + """ + if repl_dict is None: + repl_dict = {} + else: + repl_dict = repl_dict.copy() + + if not isinstance(expr, TensExpr): + if len(self.get_free_indices()) > 0: + #self has indices, but expr does not. + return None + elif set(self.get_free_indices()) != set(expr.get_free_indices()): + #If there are no wilds and the free indices are not the same, they cannot match. + return None + + if canon_bp(self - expr) == S.Zero: + return repl_dict + else: + return None + + +class TensAdd(TensExpr, AssocOp): + """ + Sum of tensors. + + Parameters + ========== + + free_args : list of the free indices + + Attributes + ========== + + ``args`` : tuple of addends + ``rank`` : rank of the tensor + ``free_args`` : list of the free indices in sorted order + + Examples + ======== + + >>> from sympy.tensor.tensor import TensorIndexType, tensor_heads, tensor_indices + >>> Lorentz = TensorIndexType('Lorentz', dummy_name='L') + >>> a, b = tensor_indices('a,b', Lorentz) + >>> p, q = tensor_heads('p,q', [Lorentz]) + >>> t = p(a) + q(a); t + p(a) + q(a) + + Examples with components data added to the tensor expression: + + >>> from sympy import symbols, diag + >>> x, y, z, t = symbols("x y z t") + >>> repl = {} + >>> repl[Lorentz] = diag(1, -1, -1, -1) + >>> repl[p(a)] = [1, 2, 3, 4] + >>> repl[q(a)] = [x, y, z, t] + + The following are: 2**2 - 3**2 - 2**2 - 7**2 ==> -58 + + >>> expr = p(a) + q(a) + >>> expr.replace_with_arrays(repl, [a]) + [x + 1, y + 2, z + 3, t + 4] + """ + + def __new__(cls, *args, **kw_args): + args = [_sympify(x) for x in args if x] + args = TensAdd._tensAdd_flatten(args) + args.sort(key=default_sort_key) + if not args: + return S.Zero + if len(args) == 1: + return args[0] + + return Basic.__new__(cls, *args, **kw_args) + + @property + def coeff(self): + return S.One + + @property + def nocoeff(self): + return self + + def get_free_indices(self) -> list[TensorIndex]: + return self.free_indices + + def _replace_indices(self, repl: dict[TensorIndex, TensorIndex]) -> TensExpr: + newargs = [arg._replace_indices(repl) if isinstance(arg, TensExpr) else arg for arg in self.args] + return self.func(*newargs) + + @memoize_property + def rank(self): + if isinstance(self.args[0], TensExpr): + return self.args[0].rank + else: + return 0 + + @memoize_property + def free_args(self): + if isinstance(self.args[0], TensExpr): + return self.args[0].free_args + else: + return [] + + @memoize_property + def free_indices(self): + if isinstance(self.args[0], TensExpr): + return self.args[0].get_free_indices() + else: + return set() + + def doit(self, **hints) -> Expr: + deep = hints.get('deep', True) + if deep: + args = [arg.doit(**hints) for arg in self.args] + else: + args = self.args # type: ignore + + # if any of the args are zero (after doit), drop them. Otherwise, _tensAdd_check will complain about non-matching indices, even though the TensAdd is correctly formed. + args = [arg for arg in args if arg != S.Zero] + + if len(args) == 0: + return S.Zero + elif len(args) == 1: + return args[0] + + # now check that all addends have the same indices: + TensAdd._tensAdd_check(args) + + # Collect terms appearing more than once, differing by their coefficients: + args = TensAdd._tensAdd_collect_terms(args) + + # collect canonicalized terms + def sort_key(t): + if not isinstance(t, TensExpr): + return [], [], [] + if hasattr(t, "_index_structure") and hasattr(t, "components"): + x = get_index_structure(t) + return t.components, x.free, x.dum + return [], [], [] + args.sort(key=sort_key) + + if not args: + return S.Zero + # it there is only a component tensor return it + if len(args) == 1: + return args[0] + + obj = self.func(*args) + return obj + + @staticmethod + def _tensAdd_flatten(args): + # flatten TensAdd, coerce terms which are not tensors to tensors + a = [] + for x in args: + if isinstance(x, (Add, TensAdd)): + a.extend(list(x.args)) + else: + a.append(x) + args = [x for x in a if x.coeff] + return args + + @staticmethod + def _tensAdd_check(args): + # check that all addends have the same free indices + + def get_indices_set(x: Expr) -> set[TensorIndex]: + if isinstance(x, TensExpr): + return set(x.get_free_indices()) + return set() + + indices0 = get_indices_set(args[0]) + list_indices = [get_indices_set(arg) for arg in args[1:]] + if not all(x == indices0 for x in list_indices): + raise ValueError('all tensors must have the same indices') + + @staticmethod + def _tensAdd_collect_terms(args): + # collect TensMul terms differing at most by their coefficient + terms_dict = defaultdict(list) + scalars = S.Zero + if isinstance(args[0], TensExpr): + free_indices = set(args[0].get_free_indices()) + else: + free_indices = set() + + for arg in args: + if not isinstance(arg, TensExpr): + if free_indices != set(): + raise ValueError("wrong valence") + scalars += arg + continue + if free_indices != set(arg.get_free_indices()): + raise ValueError("wrong valence") + # TODO: what is the part which is not a coeff? + # needs an implementation similar to .as_coeff_Mul() + terms_dict[arg.nocoeff].append(arg.coeff) + + new_args = [TensMul(Add(*coeff), t).doit(deep=False) for t, coeff in terms_dict.items() if Add(*coeff) != 0] + if isinstance(scalars, Add): + new_args = list(scalars.args) + new_args + elif scalars != 0: + new_args = [scalars] + new_args + return new_args + + def get_indices(self): + indices = [] + for arg in self.args: + indices.extend([i for i in get_indices(arg) if i not in indices]) + return indices + + + def __call__(self, *indices): + deprecate_call() + free_args = self.free_args + indices = list(indices) + if [x.tensor_index_type for x in indices] != [x.tensor_index_type for x in free_args]: + raise ValueError('incompatible types') + if indices == free_args: + return self + index_tuples = list(zip(free_args, indices)) + a = [x.func(*x.substitute_indices(*index_tuples).args) for x in self.args] + res = TensAdd(*a).doit(deep=False) + return res + + def canon_bp(self): + """ + Canonicalize using the Butler-Portugal algorithm for canonicalization + under monoterm symmetries. + """ + expr = self.expand() + if isinstance(expr, self.func): + args = [canon_bp(x) for x in expr.args] + res = TensAdd(*args).doit(deep=False) + return res + else: + return canon_bp(expr) + + def equals(self, other): + other = _sympify(other) + if isinstance(other, TensMul) and other.coeff == 0: + return all(x.coeff == 0 for x in self.args) + if isinstance(other, TensExpr): + if self.rank != other.rank: + return False + if isinstance(other, TensAdd): + if set(self.args) != set(other.args): + return False + else: + return True + t = self - other + if not isinstance(t, TensExpr): + return t == 0 + else: + if isinstance(t, TensMul): + return t.coeff == 0 + else: + return all(x.coeff == 0 for x in t.args) + + def __getitem__(self, item): + deprecate_data() + with ignore_warnings(SymPyDeprecationWarning): + return self.data[item] + + def contract_delta(self, delta): + args = [x.contract_delta(delta) if isinstance(x, TensExpr) else x for x in self.args] + t = TensAdd(*args).doit(deep=False) + return canon_bp(t) + + def contract_metric(self, g): + """ + Raise or lower indices with the metric ``g``. + + Parameters + ========== + + g : metric + + contract_all : if True, eliminate all ``g`` which are contracted + + Notes + ===== + + see the ``TensorIndexType`` docstring for the contraction conventions + """ + + args = [contract_metric(x, g) for x in self.args] + t = TensAdd(*args).doit(deep=False) + return canon_bp(t) + + def substitute_indices(self, *index_tuples): + new_args = [] + for arg in self.args: + if isinstance(arg, TensExpr): + arg = arg.substitute_indices(*index_tuples) + new_args.append(arg) + return TensAdd(*new_args).doit(deep=False) + + def _print(self): + a = [] + args = self.args + for x in args: + a.append(str(x)) + s = ' + '.join(a) + s = s.replace('+ -', '- ') + return s + + def _extract_data(self, replacement_dict): + from sympy.tensor.array import Array, permutedims + args_indices, arrays = zip(*[ + arg._extract_data(replacement_dict) if + isinstance(arg, TensExpr) else ([], arg) for arg in self.args + ]) + arrays = [Array(i) for i in arrays] + ref_indices = args_indices[0] + for i in range(1, len(args_indices)): + indices = args_indices[i] + array = arrays[i] + permutation = TensMul._get_indices_permutation(indices, ref_indices) + arrays[i] = permutedims(array, permutation) + return ref_indices, sum(arrays, Array.zeros(*array.shape)) + + @property + def data(self): + deprecate_data() + with ignore_warnings(SymPyDeprecationWarning): + return _tensor_data_substitution_dict[self.expand()] + + @data.setter + def data(self, data): + deprecate_data() + with ignore_warnings(SymPyDeprecationWarning): + _tensor_data_substitution_dict[self] = data + + @data.deleter + def data(self): + deprecate_data() + with ignore_warnings(SymPyDeprecationWarning): + if self in _tensor_data_substitution_dict: + del _tensor_data_substitution_dict[self] + + def __iter__(self): + deprecate_data() + if not self.data: + raise ValueError("No iteration on abstract tensors") + return self.data.flatten().__iter__() + + def _eval_rewrite_as_Indexed(self, *args, **kwargs): + return Add.fromiter(args) + + def _eval_partial_derivative(self, s): + # Evaluation like Add + list_addends = [] + for a in self.args: + if isinstance(a, TensExpr): + list_addends.append(a._eval_partial_derivative(s)) + # do not call diff if s is no symbol + elif s._diff_wrt: + list_addends.append(a._eval_derivative(s)) + + return self.func(*list_addends).doit(deep=False) + + def matches(self, expr, repl_dict=None, old=False): + expr = sympify(expr) + + if repl_dict is None: + repl_dict = {} + else: + repl_dict = repl_dict.copy() + + if not isinstance(expr, TensAdd): + return None + + if len(_get_wilds(self)) == 0: + return self._matches_simple(expr, repl_dict, old) + + def siftkey(arg): + wildatoms = _get_wilds(arg) + wildatom_types = sift(wildatoms, type) + if len(wildatoms) == 0: + return "nonwild" + elif WildTensor in wildatom_types.keys(): + for w in wildatom_types["WildTensor"]: + if len(w.get_indices()) == 0: + return "indexless_wildtensor" + return "wildtensor" + else: + return "otherwild" + + query_sifted = sift(self.args, siftkey) + expr_sifted = sift(expr.args, siftkey) + + #First try to match the terms without WildTensors + matched_e_tensors = [] #Used to make sure that the same tensor in expr is not matched with more than one tensor in self. + for q_tensor in query_sifted["nonwild"]: + matched_this_q = False + for e_tensor in expr_sifted["nonwild"]: + if e_tensor in matched_e_tensors: + continue + + m = q_tensor.matches(e_tensor, repl_dict=repl_dict, old=old) + if m is None: + continue + else: + matched_this_q = True + repl_dict.update(m) + matched_e_tensors.append(e_tensor) + break + + if not matched_this_q: + return None + + remaining_e_tensors = [t for t in expr_sifted["nonwild"] if t not in matched_e_tensors] + for w in query_sifted["otherwild"]: + for e in remaining_e_tensors: + m = w.matches(e) + if m is not None: + matched_e_tensors.append(e) + if w in repl_dict.keys(): + repl_dict[w] += m.pop(w) + repl_dict.update(m) + + remaining_e_tensors = [t for t in expr_sifted["nonwild"] if t not in matched_e_tensors] + for w in query_sifted["wildtensor"]: + for e in remaining_e_tensors: + m = w.matches(e) + if m is not None: + matched_e_tensors.append(e) + if w.component in repl_dict.keys(): + repl_dict[w.component] += m.pop(w.component) + repl_dict.update(m) + + remaining_e_tensors = [t for t in expr_sifted["nonwild"] if t not in matched_e_tensors] + for w in query_sifted["indexless_wildtensor"]: + for e in remaining_e_tensors: + m = w.matches(e) + if m is not None: + matched_e_tensors.append(e) + if w.component in repl_dict.keys(): + repl_dict[w.component] += m.pop(w.component) + repl_dict.update(m) + + remaining_e_tensors = [t for t in expr_sifted["nonwild"] if t not in matched_e_tensors] + if len(remaining_e_tensors) > 0: + return None + else: + return repl_dict + + +class Tensor(TensExpr): + """ + Base tensor class, i.e. this represents a tensor, the single unit to be + put into an expression. + + Explanation + =========== + + This object is usually created from a ``TensorHead``, by attaching indices + to it. Indices preceded by a minus sign are considered contravariant, + otherwise covariant. + + Examples + ======== + + >>> from sympy.tensor.tensor import TensorIndexType, tensor_indices, TensorHead + >>> Lorentz = TensorIndexType("Lorentz", dummy_name="L") + >>> mu, nu = tensor_indices('mu nu', Lorentz) + >>> A = TensorHead("A", [Lorentz, Lorentz]) + >>> A(mu, -nu) + A(mu, -nu) + >>> A(mu, -mu) + A(L_0, -L_0) + + It is also possible to use symbols instead of inidices (appropriate indices + are then generated automatically). + + >>> from sympy import Symbol + >>> x = Symbol('x') + >>> A(x, mu) + A(x, mu) + >>> A(x, -x) + A(L_0, -L_0) + + """ + + is_commutative = False + + _index_structure: _IndexStructure + args: tuple[TensorHead, Tuple] + + def __new__(cls, tensor_head, indices, *, is_canon_bp=False, **kw_args): + indices = cls._parse_indices(tensor_head, indices) + obj = Basic.__new__(cls, tensor_head, Tuple(*indices), **kw_args) + obj._index_structure = _IndexStructure.from_indices(*indices) + obj._free = obj._index_structure.free[:] + obj._dum = obj._index_structure.dum[:] + obj._ext_rank = obj._index_structure._ext_rank + obj._coeff = S.One + obj._nocoeff = obj + obj._component = tensor_head + obj._components = [tensor_head] + if tensor_head.rank != len(indices): + raise ValueError("wrong number of indices") + obj.is_canon_bp = is_canon_bp + obj._index_map = Tensor._build_index_map(indices, obj._index_structure) + return obj + + @property + def free(self): + return self._free + + @property + def dum(self): + return self._dum + + @property + def ext_rank(self): + return self._ext_rank + + @property + def coeff(self): + return self._coeff + + @property + def nocoeff(self): + return self._nocoeff + + @property + def component(self): + return self._component + + @property + def components(self): + return self._components + + @property + def head(self): + return self.args[0] + + @property + def indices(self): + return self.args[1] + + @property + def free_indices(self): + return set(self._index_structure.get_free_indices()) + + @property + def index_types(self): + return self.head.index_types + + @property + def rank(self): + return len(self.free_indices) + + @staticmethod + def _build_index_map(indices, index_structure): + index_map = {} + for idx in indices: + index_map[idx] = (indices.index(idx),) + return index_map + + def doit(self, **hints): + args, indices, free, dum = TensMul._tensMul_contract_indices([self]) + return args[0] + + @staticmethod + def _parse_indices(tensor_head, indices): + if not isinstance(indices, (tuple, list, Tuple)): + raise TypeError("indices should be an array, got %s" % type(indices)) + indices = list(indices) + for i, index in enumerate(indices): + if isinstance(index, Symbol): + indices[i] = TensorIndex(index, tensor_head.index_types[i], True) + elif isinstance(index, Mul): + c, e = index.as_coeff_Mul() + if c == -1 and isinstance(e, Symbol): + indices[i] = TensorIndex(e, tensor_head.index_types[i], False) + else: + raise ValueError("index not understood: %s" % index) + elif not isinstance(index, TensorIndex): + raise TypeError("wrong type for index: %s is %s" % (index, type(index))) + return indices + + def _set_new_index_structure(self, im, is_canon_bp=False): + indices = im.get_indices() + return self._set_indices(*indices, is_canon_bp=is_canon_bp) + + def _set_indices(self, *indices, is_canon_bp=False, **kw_args): + if len(indices) != self.ext_rank: + raise ValueError("indices length mismatch") + return self.func(self.args[0], indices, is_canon_bp=is_canon_bp).doit() + + def _get_free_indices_set(self): + return {i[0] for i in self._index_structure.free} + + def _get_dummy_indices_set(self): + dummy_pos = set(itertools.chain(*self._index_structure.dum)) + return {idx for i, idx in enumerate(self.args[1]) if i in dummy_pos} + + def _get_indices_set(self): + return set(self.args[1].args) + + @property + def free_in_args(self): + return [(ind, pos, 0) for ind, pos in self.free] + + @property + def dum_in_args(self): + return [(p1, p2, 0, 0) for p1, p2 in self.dum] + + @property + def free_args(self): + return sorted([x[0] for x in self.free]) + + def commutes_with(self, other): + """ + :param other: + :return: + 0 commute + 1 anticommute + None neither commute nor anticommute + """ + if not isinstance(other, TensExpr): + return 0 + elif isinstance(other, Tensor): + return self.component.commutes_with(other.component) + return NotImplementedError + + def perm2tensor(self, g, is_canon_bp=False): + """ + Returns the tensor corresponding to the permutation ``g``. + + For further details, see the method in ``TIDS`` with the same name. + """ + return perm2tensor(self, g, is_canon_bp) + + def canon_bp(self): + if self.is_canon_bp: + return self + expr = self.expand() + g, dummies, msym = expr._index_structure.indices_canon_args() + v = components_canon_args([expr.component]) + can = canonicalize(g, dummies, msym, *v) + if can == 0: + return S.Zero + tensor = self.perm2tensor(can, True) + return tensor + + def split(self): + return [self] + + def sorted_components(self): + return self + + def get_indices(self) -> list[TensorIndex]: + """ + Get a list of indices, corresponding to those of the tensor. + """ + return list(self.args[1]) + + def get_free_indices(self) -> list[TensorIndex]: + """ + Get a list of free indices, corresponding to those of the tensor. + """ + return self._index_structure.get_free_indices() + + def _replace_indices(self, repl: dict[TensorIndex, TensorIndex]) -> TensExpr: + # TODO: this could be optimized by only swapping the indices + # instead of visiting the whole expression tree: + return self.xreplace(repl) + + def as_base_exp(self): + return self, S.One + + def substitute_indices(self, *index_tuples): + """ + Return a tensor with free indices substituted according to ``index_tuples``. + + ``index_types`` list of tuples ``(old_index, new_index)``. + + Examples + ======== + + >>> from sympy.tensor.tensor import TensorIndexType, tensor_indices, tensor_heads, TensorSymmetry + >>> Lorentz = TensorIndexType('Lorentz', dummy_name='L') + >>> i, j, k, l = tensor_indices('i,j,k,l', Lorentz) + >>> A, B = tensor_heads('A,B', [Lorentz]*2, TensorSymmetry.fully_symmetric(2)) + >>> t = A(i, k)*B(-k, -j); t + A(i, L_0)*B(-L_0, -j) + >>> t.substitute_indices((i, k),(-j, l)) + A(k, L_0)*B(-L_0, l) + """ + indices = [] + for index in self.indices: + for ind_old, ind_new in index_tuples: + if (index.name == ind_old.name and index.tensor_index_type == + ind_old.tensor_index_type): + if index.is_up == ind_old.is_up: + indices.append(ind_new) + else: + indices.append(-ind_new) + break + else: + indices.append(index) + return self.head(*indices) + + def _get_symmetrized_forms(self): + """ + Return a list giving all possible permutations of self that are allowed by its symmetries. + """ + comp = self.component + gens = comp.symmetry.generators + rank = comp.rank + + old_perms = None + new_perms = {self} + while new_perms != old_perms: + old_perms = new_perms.copy() + for tens in old_perms: + for gen in gens: + inds = tens.get_indices() + per = [gen.apply(i) for i in range(0,rank)] + sign = (-1)**(gen.apply(rank) - rank) + ind_map = dict(zip(inds, [inds[i] for i in per])) + new_perms.add( sign * tens._replace_indices(ind_map) ) + + return new_perms + + def matches(self, expr, repl_dict=None, old=False): + expr = sympify(expr) + + if repl_dict is None: + repl_dict = {} + else: + repl_dict = repl_dict.copy() + + #simple checks + if self == expr: + return repl_dict + if not isinstance(expr, Tensor): + return None + if self.head != expr.head: + return None + + #Now consider all index symmetries of expr, and see if any of them allow a match. + for new_expr in expr._get_symmetrized_forms(): + m = self._matches(new_expr, repl_dict, old=old) + if m is not None: + repl_dict.update(m) + return repl_dict + + return None + + def _matches(self, expr, repl_dict=None, old=False): + """ + This does not account for index symmetries of expr + """ + expr = sympify(expr) + + if repl_dict is None: + repl_dict = {} + else: + repl_dict = repl_dict.copy() + + #simple checks + if self == expr: + return repl_dict + if not isinstance(expr, Tensor): + return None + if self.head != expr.head: + return None + + s_indices = self.get_indices() + e_indices = expr.get_indices() + + if len(s_indices) != len(e_indices): + return None + + for i in range(len(s_indices)): + s_ind = s_indices[i] + m = s_ind.matches(e_indices[i]) + if m is None: + return None + elif -s_ind in repl_dict.keys() and -repl_dict[-s_ind] != m[s_ind]: + return None + else: + repl_dict.update(m) + + return repl_dict + + def __call__(self, *indices): + deprecate_call() + free_args = self.free_args + indices = list(indices) + if [x.tensor_index_type for x in indices] != [x.tensor_index_type for x in free_args]: + raise ValueError('incompatible types') + if indices == free_args: + return self + t = self.substitute_indices(*list(zip(free_args, indices))) + + # object is rebuilt in order to make sure that all contracted indices + # get recognized as dummies, but only if there are contracted indices. + if len({i if i.is_up else -i for i in indices}) != len(indices): + return t.func(*t.args) + return t + + # TODO: put this into TensExpr? + def __iter__(self): + deprecate_data() + with ignore_warnings(SymPyDeprecationWarning): + return self.data.__iter__() + + # TODO: put this into TensExpr? + def __getitem__(self, item): + deprecate_data() + with ignore_warnings(SymPyDeprecationWarning): + return self.data[item] + + def _extract_data(self, replacement_dict): + from .array import Array + for k, v in replacement_dict.items(): + if isinstance(k, Tensor) and k.args[0] == self.args[0]: + other = k + array = v + break + else: + raise ValueError("%s not found in %s" % (self, replacement_dict)) + + # TODO: inefficient, this should be done at root level only: + replacement_dict = {k: Array(v) for k, v in replacement_dict.items()} + array = Array(array) + + dum1 = self.dum + dum2 = other.dum + + if len(dum2) > 0: + for pair in dum2: + # allow `dum2` if the contained values are also in `dum1`. + if pair not in dum1: + raise NotImplementedError("%s with contractions is not implemented" % other) + # Remove elements in `dum2` from `dum1`: + dum1 = [pair for pair in dum1 if pair not in dum2] + if len(dum1) > 0: + indices1 = self.get_indices() + indices2 = other.get_indices() + repl = {} + for p1, p2 in dum1: + repl[indices2[p2]] = -indices2[p1] + for pos in (p1, p2): + if indices1[pos].is_up ^ indices2[pos].is_up: + metric = replacement_dict[indices1[pos].tensor_index_type] + if indices1[pos].is_up: + metric = _TensorDataLazyEvaluator.inverse_matrix(metric) + array = self._contract_and_permute_with_metric(metric, array, pos, len(indices2)) + other = other.xreplace(repl).doit() + array = _TensorDataLazyEvaluator.data_contract_dum([array], dum1, len(indices2)) + + free_ind1 = self.get_free_indices() + free_ind2 = other.get_free_indices() + + return self._match_indices_with_other_tensor(array, free_ind1, free_ind2, replacement_dict) + + @property + def data(self): + deprecate_data() + with ignore_warnings(SymPyDeprecationWarning): + return _tensor_data_substitution_dict[self] + + @data.setter + def data(self, data): + deprecate_data() + # TODO: check data compatibility with properties of tensor. + with ignore_warnings(SymPyDeprecationWarning): + _tensor_data_substitution_dict[self] = data + + @data.deleter + def data(self): + deprecate_data() + with ignore_warnings(SymPyDeprecationWarning): + if self in _tensor_data_substitution_dict: + del _tensor_data_substitution_dict[self] + if self.metric in _tensor_data_substitution_dict: + del _tensor_data_substitution_dict[self.metric] + + def _print(self): + indices = [str(ind) for ind in self.indices] + component = self.component + if component.rank > 0: + return ('%s(%s)' % (component.name, ', '.join(indices))) + else: + return ('%s' % component.name) + + def equals(self, other): + if other == 0: + return self.coeff == 0 + other = _sympify(other) + if not isinstance(other, TensExpr): + assert not self.components + return S.One == other + + def _get_compar_comp(self): + t = self.canon_bp() + r = (t.coeff, tuple(t.components), \ + tuple(sorted(t.free)), tuple(sorted(t.dum))) + return r + + return _get_compar_comp(self) == _get_compar_comp(other) + + def contract_metric(self, g): + # if metric is not the same, ignore this step: + if self.component != g: + return self + # in case there are free components, do not perform anything: + if len(self.free) != 0: + return self + + #antisym = g.index_types[0].metric_antisym + if g.symmetry == TensorSymmetry.fully_symmetric(-2): + antisym = 1 + elif g.symmetry == TensorSymmetry.fully_symmetric(2): + antisym = 0 + elif g.symmetry == TensorSymmetry.no_symmetry(2): + antisym = None + else: + raise NotImplementedError + sign = S.One + typ = g.index_types[0] + + if not antisym: + # g(i, -i) + sign = sign*typ.dim + else: + # g(i, -i) + sign = sign*typ.dim + + dp0, dp1 = self.dum[0] + if dp0 < dp1: + # g(i, -i) = -D with antisymmetric metric + sign = -sign + + return sign + + def contract_delta(self, metric): + return self.contract_metric(metric) + + def _eval_rewrite_as_Indexed(self, tens, indices, **kwargs): + from sympy.tensor.indexed import Indexed + # TODO: replace .args[0] with .name: + index_symbols = [i.args[0] for i in self.get_indices()] + expr = Indexed(tens.args[0], *index_symbols) + return self._check_add_Sum(expr, index_symbols) + + def _eval_partial_derivative(self, s: Tensor) -> Expr: + + if not isinstance(s, Tensor): + return S.Zero + else: + + # @a_i/@a_k = delta_i^k + # @a_i/@a^k = g_ij delta^j_k + # @a^i/@a^k = delta^i_k + # @a^i/@a_k = g^ij delta_j^k + # TODO: if there is no metric present, the derivative should be zero? + + if self.head != s.head: + return S.Zero + + # if heads are the same, provide delta and/or metric products + # for every free index pair in the appropriate tensor + # assumed that the free indices are in proper order + # A contravariante index in the derivative becomes covariant + # after performing the derivative and vice versa + + kronecker_delta_list = [1] + + # not guarantee a correct index order + + for (count, (iself, iother)) in enumerate(zip(self.get_free_indices(), s.get_free_indices())): + if iself.tensor_index_type != iother.tensor_index_type: + raise ValueError("index types not compatible") + else: + tensor_index_type = iself.tensor_index_type + tensor_metric = tensor_index_type.metric + dummy = TensorIndex("d_" + str(count), tensor_index_type, + is_up=iself.is_up) + if iself.is_up == iother.is_up: + kroneckerdelta = tensor_index_type.delta(iself, -iother) + else: + kroneckerdelta = ( + TensMul(tensor_metric(iself, dummy), + tensor_index_type.delta(-dummy, -iother)) + ) + kronecker_delta_list.append(kroneckerdelta) + return TensMul.fromiter(kronecker_delta_list).doit(deep=False) + # doit necessary to rename dummy indices accordingly + + +class TensMul(TensExpr, AssocOp): + """ + Product of tensors. + + Parameters + ========== + + coeff : SymPy coefficient of the tensor + args + + Attributes + ========== + + ``components`` : list of ``TensorHead`` of the component tensors + ``types`` : list of nonrepeated ``TensorIndexType`` + ``free`` : list of ``(ind, ipos, icomp)``, see Notes + ``dum`` : list of ``(ipos1, ipos2, icomp1, icomp2)``, see Notes + ``ext_rank`` : rank of the tensor counting the dummy indices + ``rank`` : rank of the tensor + ``coeff`` : SymPy coefficient of the tensor + ``free_args`` : list of the free indices in sorted order + ``is_canon_bp`` : ``True`` if the tensor in in canonical form + + Notes + ===== + + ``args[0]`` list of ``TensorHead`` of the component tensors. + + ``args[1]`` list of ``(ind, ipos, icomp)`` + where ``ind`` is a free index, ``ipos`` is the slot position + of ``ind`` in the ``icomp``-th component tensor. + + ``args[2]`` list of tuples representing dummy indices. + ``(ipos1, ipos2, icomp1, icomp2)`` indicates that the contravariant + dummy index is the ``ipos1``-th slot position in the ``icomp1``-th + component tensor; the corresponding covariant index is + in the ``ipos2`` slot position in the ``icomp2``-th component tensor. + + """ + identity = S.One + + _index_structure: _IndexStructure + + def __new__(cls, *args, **kw_args): + is_canon_bp = kw_args.get('is_canon_bp', False) + args = list(map(_sympify, args)) + + """ + If the internal dummy indices in one arg conflict with the free indices + of the remaining args, we need to rename those internal dummy indices. + """ + free = [get_free_indices(arg) for arg in args] + free = set(itertools.chain(*free)) #flatten free + newargs = [] + for arg in args: + dum_this = set(get_dummy_indices(arg)) + dum_other = [get_dummy_indices(a) for a in newargs] + dum_other = set(itertools.chain(*dum_other)) #flatten dum_other + free_this = set(get_free_indices(arg)) + if len(dum_this.intersection(free)) > 0: + exclude = free_this.union(free, dum_other) + newarg = TensMul._dedupe_indices(arg, exclude) + else: + newarg = arg + newargs.append(newarg) + + args = newargs + + # Flatten: + args = [i for arg in args for i in (arg.args if isinstance(arg, (TensMul, Mul)) else [arg])] + + args, indices, free, dum = TensMul._tensMul_contract_indices(args, replace_indices=False) + + # Data for indices: + index_types = [i.tensor_index_type for i in indices] + index_structure = _IndexStructure(free, dum, index_types, indices, canon_bp=is_canon_bp) + + obj = TensExpr.__new__(cls, *args) + obj._indices = indices + obj._index_types = index_types.copy() + obj._index_structure = index_structure + obj._free = index_structure.free[:] + obj._dum = index_structure.dum[:] + obj._free_indices = {x[0] for x in obj.free} + obj._rank = len(obj.free) + obj._ext_rank = len(obj._index_structure.free) + 2*len(obj._index_structure.dum) + obj._coeff = S.One + obj._is_canon_bp = is_canon_bp + return obj + + index_types = property(lambda self: self._index_types) + free = property(lambda self: self._free) + dum = property(lambda self: self._dum) + free_indices = property(lambda self: self._free_indices) + rank = property(lambda self: self._rank) + ext_rank = property(lambda self: self._ext_rank) + + @staticmethod + def _indices_to_free_dum(args_indices): + free2pos1 = {} + free2pos2 = {} + dummy_data = [] + indices = [] + + # Notation for positions (to better understand the code): + # `pos1`: position in the `args`. + # `pos2`: position in the indices. + + # Example: + # A(i, j)*B(k, m, n)*C(p) + # `pos1` of `n` is 1 because it's in `B` (second `args` of TensMul). + # `pos2` of `n` is 4 because it's the fifth overall index. + + # Counter for the index position wrt the whole expression: + pos2 = 0 + + for pos1, arg_indices in enumerate(args_indices): + + for index in arg_indices: + if not isinstance(index, TensorIndex): + raise TypeError("expected TensorIndex") + if -index in free2pos1: + # Dummy index detected: + other_pos1 = free2pos1.pop(-index) + other_pos2 = free2pos2.pop(-index) + if index.is_up: + dummy_data.append((index, pos1, other_pos1, pos2, other_pos2)) + else: + dummy_data.append((-index, other_pos1, pos1, other_pos2, pos2)) + indices.append(index) + elif index in free2pos1: + raise ValueError("Repeated index: %s" % index) + else: + free2pos1[index] = pos1 + free2pos2[index] = pos2 + indices.append(index) + pos2 += 1 + + free = list(free2pos2.items()) + free_names = [i.name for i in free2pos2.keys()] + + dummy_data.sort(key=lambda x: x[3]) + return indices, free, free_names, dummy_data + + @staticmethod + def _dummy_data_to_dum(dummy_data): + return [(p2a, p2b) for (i, p1a, p1b, p2a, p2b) in dummy_data] + + @staticmethod + def _tensMul_contract_indices(args, replace_indices=True): + replacements = [{} for _ in args] + + #_index_order = all(_has_index_order(arg) for arg in args) + + args_indices = [get_indices(arg) for arg in args] + indices, free, free_names, dummy_data = TensMul._indices_to_free_dum(args_indices) + + cdt = defaultdict(int) + + def dummy_name_gen(tensor_index_type): + nd = str(cdt[tensor_index_type]) + cdt[tensor_index_type] += 1 + return tensor_index_type.dummy_name + '_' + nd + + if replace_indices: + for old_index, pos1cov, pos1contra, pos2cov, pos2contra in dummy_data: + index_type = old_index.tensor_index_type + while True: + dummy_name = dummy_name_gen(index_type) + if dummy_name not in free_names: + break + dummy = old_index.func(dummy_name, index_type, *old_index.args[2:]) + replacements[pos1cov][old_index] = dummy + replacements[pos1contra][-old_index] = -dummy + indices[pos2cov] = dummy + indices[pos2contra] = -dummy + args = [ + arg._replace_indices(repl) if isinstance(arg, TensExpr) else arg + for arg, repl in zip(args, replacements)] + + """ + The order of indices might've changed due to the replacements (e.g. if one of the args is a TensAdd, replacing an index can change the sort order of the terms, thus changing the order of indices returned by its get_indices() method). + To stay on the safe side, we calculate these quantities again. + """ + args_indices = [get_indices(arg) for arg in args] + indices, free, free_names, dummy_data = TensMul._indices_to_free_dum(args_indices) + + dum = TensMul._dummy_data_to_dum(dummy_data) + return args, indices, free, dum + + @staticmethod + def _get_components_from_args(args): + """ + Get a list of ``Tensor`` objects having the same ``TIDS`` if multiplied + by one another. + """ + components = [] + for arg in args: + if not isinstance(arg, TensExpr): + continue + if isinstance(arg, TensAdd): + continue + components.extend(arg.components) + return components + + @staticmethod + def _rebuild_tensors_list(args, index_structure): + indices = index_structure.get_indices() + #tensors = [None for i in components] # pre-allocate list + ind_pos = 0 + for i, arg in enumerate(args): + if not isinstance(arg, TensExpr): + continue + prev_pos = ind_pos + ind_pos += arg.ext_rank + args[i] = Tensor(arg.component, indices[prev_pos:ind_pos]) + + def doit(self, **hints): + is_canon_bp = self._is_canon_bp + deep = hints.get('deep', True) + if deep: + args = [arg.doit(**hints) for arg in self.args] + + """ + There may now be conflicts between dummy indices of different args + (each arg's doit method does not have any information about which + dummy indices are already used in the other args), so we + deduplicate them. + """ + rule = dict(zip(self.args, args)) + rule = self._dedupe_indices_in_rule(rule) + args = [rule[a] for a in self.args] + + else: + args = self.args + + args = [arg for arg in args if arg != self.identity] + + # Extract non-tensor coefficients: + coeff = reduce(lambda a, b: a*b, [arg for arg in args if not isinstance(arg, TensExpr)], S.One) + args = [arg for arg in args if isinstance(arg, TensExpr)] + + if len(args) == 0: + return coeff + + if coeff != self.identity: + args = [coeff] + args + if coeff == 0: + return S.Zero + + if len(args) == 1: + return args[0] + + args, indices, free, dum = TensMul._tensMul_contract_indices(args) + + # Data for indices: + index_types = [i.tensor_index_type for i in indices] + index_structure = _IndexStructure(free, dum, index_types, indices, canon_bp=is_canon_bp) + + obj = self.func(*args) + obj._index_types = index_types + obj._index_structure = index_structure + obj._ext_rank = len(obj._index_structure.free) + 2*len(obj._index_structure.dum) + obj._coeff = coeff + obj._is_canon_bp = is_canon_bp + return obj + + # TODO: this method should be private + # TODO: should this method be renamed _from_components_free_dum ? + @staticmethod + def from_data(coeff, components, free, dum, **kw_args): + return TensMul(coeff, *TensMul._get_tensors_from_components_free_dum(components, free, dum), **kw_args).doit(deep=False) + + @staticmethod + def _get_tensors_from_components_free_dum(components, free, dum): + """ + Get a list of ``Tensor`` objects by distributing ``free`` and ``dum`` indices on the ``components``. + """ + index_structure = _IndexStructure.from_components_free_dum(components, free, dum) + indices = index_structure.get_indices() + tensors = [None for i in components] # pre-allocate list + + # distribute indices on components to build a list of tensors: + ind_pos = 0 + for i, component in enumerate(components): + prev_pos = ind_pos + ind_pos += component.rank + tensors[i] = Tensor(component, indices[prev_pos:ind_pos]) + return tensors + + def _get_free_indices_set(self): + return {i[0] for i in self.free} + + def _get_dummy_indices_set(self): + dummy_pos = set(itertools.chain(*self.dum)) + return {idx for i, idx in enumerate(self._index_structure.get_indices()) if i in dummy_pos} + + def _get_position_offset_for_indices(self): + arg_offset = [None for i in range(self.ext_rank)] + counter = 0 + for arg in self.args: + if not isinstance(arg, TensExpr): + continue + for j in range(arg.ext_rank): + arg_offset[j + counter] = counter + counter += arg.ext_rank + return arg_offset + + @property + def free_args(self): + return sorted([x[0] for x in self.free]) + + @property + def components(self): + return self._get_components_from_args(self.args) + + @property + def free_in_args(self): + arg_offset = self._get_position_offset_for_indices() + argpos = self._get_indices_to_args_pos() + return [(ind, pos-arg_offset[pos], argpos[pos]) for (ind, pos) in self.free] + + @property + def coeff(self): + # return Mul.fromiter([c for c in self.args if not isinstance(c, TensExpr)]) + return self._coeff + + @property + def nocoeff(self): + return self.func(*self.args, 1/self.coeff).doit(deep=False) + + @property + def dum_in_args(self): + arg_offset = self._get_position_offset_for_indices() + argpos = self._get_indices_to_args_pos() + return [(p1-arg_offset[p1], p2-arg_offset[p2], argpos[p1], argpos[p2]) for p1, p2 in self.dum] + + def equals(self, other): + if other == 0: + return self.coeff == 0 + other = _sympify(other) + if not isinstance(other, TensExpr): + assert not self.components + return self.coeff == other + + return self.canon_bp() == other.canon_bp() + + def get_indices(self): + """ + Returns the list of indices of the tensor. + + Explanation + =========== + + The indices are listed in the order in which they appear in the + component tensors. + The dummy indices are given a name which does not collide with + the names of the free indices. + + Examples + ======== + + >>> from sympy.tensor.tensor import TensorIndexType, tensor_indices, tensor_heads + >>> Lorentz = TensorIndexType('Lorentz', dummy_name='L') + >>> m0, m1, m2 = tensor_indices('m0,m1,m2', Lorentz) + >>> g = Lorentz.metric + >>> p, q = tensor_heads('p,q', [Lorentz]) + >>> t = p(m1)*g(m0,m2) + >>> t.get_indices() + [m1, m0, m2] + >>> t2 = p(m1)*g(-m1, m2) + >>> t2.get_indices() + [L_0, -L_0, m2] + """ + return self._indices + + def get_free_indices(self) -> list[TensorIndex]: + """ + Returns the list of free indices of the tensor. + + Explanation + =========== + + The indices are listed in the order in which they appear in the + component tensors. + + Examples + ======== + + >>> from sympy.tensor.tensor import TensorIndexType, tensor_indices, tensor_heads + >>> Lorentz = TensorIndexType('Lorentz', dummy_name='L') + >>> m0, m1, m2 = tensor_indices('m0,m1,m2', Lorentz) + >>> g = Lorentz.metric + >>> p, q = tensor_heads('p,q', [Lorentz]) + >>> t = p(m1)*g(m0,m2) + >>> t.get_free_indices() + [m1, m0, m2] + >>> t2 = p(m1)*g(-m1, m2) + >>> t2.get_free_indices() + [m2] + """ + return self._index_structure.get_free_indices() + + def _replace_indices(self, repl: dict[TensorIndex, TensorIndex]) -> TensExpr: + return self.func(*[arg._replace_indices(repl) if isinstance(arg, TensExpr) else arg for arg in self.args]) + + def split(self): + """ + Returns a list of tensors, whose product is ``self``. + + Explanation + =========== + + Dummy indices contracted among different tensor components + become free indices with the same name as the one used to + represent the dummy indices. + + Examples + ======== + + >>> from sympy.tensor.tensor import TensorIndexType, tensor_indices, tensor_heads, TensorSymmetry + >>> Lorentz = TensorIndexType('Lorentz', dummy_name='L') + >>> a, b, c, d = tensor_indices('a,b,c,d', Lorentz) + >>> A, B = tensor_heads('A,B', [Lorentz]*2, TensorSymmetry.fully_symmetric(2)) + >>> t = A(a,b)*B(-b,c) + >>> t + A(a, L_0)*B(-L_0, c) + >>> t.split() + [A(a, L_0), B(-L_0, c)] + """ + if self.args == (): + return [self] + splitp = [] + res = 1 + for arg in self.args: + if isinstance(arg, Tensor): + splitp.append(res*arg) + res = 1 + else: + res *= arg + return splitp + + def _eval_expand_mul(self, **hints): + args1 = [arg.args if isinstance(arg, (Add, TensAdd)) else (arg,) for arg in self.args] + return TensAdd(*[ + TensMul(*i).doit(deep=False) for i in itertools.product(*args1)] + ) + + def __neg__(self): + return TensMul(S.NegativeOne, self, is_canon_bp=self._is_canon_bp).doit(deep=False) + + def __getitem__(self, item): + deprecate_data() + with ignore_warnings(SymPyDeprecationWarning): + return self.data[item] + + def _get_args_for_traditional_printer(self): + args = list(self.args) + if self.coeff.could_extract_minus_sign(): + # expressions like "-A(a)" + sign = "-" + if args[0] == S.NegativeOne: + args = args[1:] + else: + args[0] = -args[0] + else: + sign = "" + return sign, args + + def _sort_args_for_sorted_components(self): + """ + Returns the ``args`` sorted according to the components commutation + properties. + + Explanation + =========== + + The sorting is done taking into account the commutation group + of the component tensors. + """ + cv = [arg for arg in self.args if isinstance(arg, TensExpr)] + sign = 1 + n = len(cv) - 1 + for i in range(n): + for j in range(n, i, -1): + c = cv[j-1].commutes_with(cv[j]) + # if `c` is `None`, it does neither commute nor anticommute, skip: + if c not in (0, 1): + continue + typ1 = sorted(set(cv[j-1].component.index_types), key=lambda x: x.name) + typ2 = sorted(set(cv[j].component.index_types), key=lambda x: x.name) + if (typ1, cv[j-1].component.name) > (typ2, cv[j].component.name): + cv[j-1], cv[j] = cv[j], cv[j-1] + # if `c` is 1, the anticommute, so change sign: + if c: + sign = -sign + + coeff = sign * self.coeff + if coeff != 1: + return [coeff] + cv + return cv + + def sorted_components(self): + """ + Returns a tensor product with sorted components. + """ + return TensMul(*self._sort_args_for_sorted_components()).doit(deep=False) + + def perm2tensor(self, g, is_canon_bp=False): + """ + Returns the tensor corresponding to the permutation ``g`` + + For further details, see the method in ``TIDS`` with the same name. + """ + return perm2tensor(self, g, is_canon_bp=is_canon_bp) + + def canon_bp(self): + """ + Canonicalize using the Butler-Portugal algorithm for canonicalization + under monoterm symmetries. + + Examples + ======== + + >>> from sympy.tensor.tensor import TensorIndexType, tensor_indices, TensorHead, TensorSymmetry + >>> Lorentz = TensorIndexType('Lorentz', dummy_name='L') + >>> m0, m1, m2 = tensor_indices('m0,m1,m2', Lorentz) + >>> A = TensorHead('A', [Lorentz]*2, TensorSymmetry.fully_symmetric(-2)) + >>> t = A(m0,-m1)*A(m1,-m0) + >>> t.canon_bp() + -A(L_0, L_1)*A(-L_0, -L_1) + >>> t = A(m0,-m1)*A(m1,-m2)*A(m2,-m0) + >>> t.canon_bp() + 0 + """ + if self._is_canon_bp: + return self + expr = self.expand() + if isinstance(expr, TensAdd): + return expr.canon_bp() + if not expr.components: + return expr + expr = expr.doit(deep=False) #make sure self.coeff is populated correctly + t = expr.sorted_components() + g, dummies, msym = t._index_structure.indices_canon_args() + v = components_canon_args(t.components) + can = canonicalize(g, dummies, msym, *v) + if can == 0: + return S.Zero + tmul = t.perm2tensor(can, True) + return tmul + + def contract_delta(self, delta): + t = self.contract_metric(delta) + return t + + def _get_indices_to_args_pos(self): + """ + Get a dict mapping the index position to TensMul's argument number. + """ + pos_map = {} + pos_counter = 0 + for arg_i, arg in enumerate(self.args): + if not isinstance(arg, TensExpr): + continue + assert isinstance(arg, Tensor) + for i in range(arg.ext_rank): + pos_map[pos_counter] = arg_i + pos_counter += 1 + return pos_map + + def contract_metric(self, g): + """ + Raise or lower indices with the metric ``g``. + + Parameters + ========== + + g : metric + + Notes + ===== + + See the ``TensorIndexType`` docstring for the contraction conventions. + + Examples + ======== + + >>> from sympy.tensor.tensor import TensorIndexType, tensor_indices, tensor_heads + >>> Lorentz = TensorIndexType('Lorentz', dummy_name='L') + >>> m0, m1, m2 = tensor_indices('m0,m1,m2', Lorentz) + >>> g = Lorentz.metric + >>> p, q = tensor_heads('p,q', [Lorentz]) + >>> t = p(m0)*q(m1)*g(-m0, -m1) + >>> t.canon_bp() + metric(L_0, L_1)*p(-L_0)*q(-L_1) + >>> t.contract_metric(g).canon_bp() + p(L_0)*q(-L_0) + """ + expr = self.expand().doit(deep=False) + if self != expr: + expr = canon_bp(expr) + return contract_metric(expr, g) + pos_map = self._get_indices_to_args_pos() + args = list(self.args) + + #antisym = g.index_types[0].metric_antisym + if g.symmetry == TensorSymmetry.fully_symmetric(-2): + antisym = 1 + elif g.symmetry == TensorSymmetry.fully_symmetric(2): + antisym = 0 + elif g.symmetry == TensorSymmetry.no_symmetry(2): + antisym = None + else: + raise NotImplementedError + + # list of positions of the metric ``g`` inside ``args`` + gpos = [i for i, x in enumerate(self.args) if isinstance(x, Tensor) and x.component == g] + if not gpos: + return self + + # Sign is either 1 or -1, to correct the sign after metric contraction + # (for spinor indices). + sign = 1 + dum = self.dum[:] + free = self.free[:] + elim = set() + for gposx in gpos: + if gposx in elim: + continue + free1 = [x for x in free if pos_map[x[1]] == gposx] + dum1 = [x for x in dum if pos_map[x[0]] == gposx or pos_map[x[1]] == gposx] + if not dum1: + continue + elim.add(gposx) + # subs with the multiplication neutral element, that is, remove it: + args[gposx] = 1 + if len(dum1) == 2: + if not antisym: + dum10, dum11 = dum1 + if pos_map[dum10[1]] == gposx: + # the index with pos p0 contravariant + p0 = dum10[0] + else: + # the index with pos p0 is covariant + p0 = dum10[1] + if pos_map[dum11[1]] == gposx: + # the index with pos p1 is contravariant + p1 = dum11[0] + else: + # the index with pos p1 is covariant + p1 = dum11[1] + + dum.append((p0, p1)) + else: + dum10, dum11 = dum1 + # change the sign to bring the indices of the metric to contravariant + # form; change the sign if dum10 has the metric index in position 0 + if pos_map[dum10[1]] == gposx: + # the index with pos p0 is contravariant + p0 = dum10[0] + if dum10[1] == 1: + sign = -sign + else: + # the index with pos p0 is covariant + p0 = dum10[1] + if dum10[0] == 0: + sign = -sign + if pos_map[dum11[1]] == gposx: + # the index with pos p1 is contravariant + p1 = dum11[0] + sign = -sign + else: + # the index with pos p1 is covariant + p1 = dum11[1] + + dum.append((p0, p1)) + + elif len(dum1) == 1: + if not antisym: + dp0, dp1 = dum1[0] + if pos_map[dp0] == pos_map[dp1]: + # g(i, -i) + typ = g.index_types[0] + sign = sign*typ.dim + + else: + # g(i0, i1)*p(-i1) + if pos_map[dp0] == gposx: + p1 = dp1 + else: + p1 = dp0 + + ind, p = free1[0] + free.append((ind, p1)) + else: + dp0, dp1 = dum1[0] + if pos_map[dp0] == pos_map[dp1]: + # g(i, -i) + typ = g.index_types[0] + sign = sign*typ.dim + + if dp0 < dp1: + # g(i, -i) = -D with antisymmetric metric + sign = -sign + else: + # g(i0, i1)*p(-i1) + if pos_map[dp0] == gposx: + p1 = dp1 + if dp0 == 0: + sign = -sign + else: + p1 = dp0 + ind, p = free1[0] + free.append((ind, p1)) + dum = [x for x in dum if x not in dum1] + free = [x for x in free if x not in free1] + + # shift positions: + shift = 0 + shifts = [0]*len(args) + for i in range(len(args)): + if i in elim: + shift += 2 + continue + shifts[i] = shift + free = [(ind, p - shifts[pos_map[p]]) for (ind, p) in free if pos_map[p] not in elim] + dum = [(p0 - shifts[pos_map[p0]], p1 - shifts[pos_map[p1]]) for p0, p1 in dum if pos_map[p0] not in elim and pos_map[p1] not in elim] + + res = ( sign*TensMul(*args) ).doit(deep=False) + if not isinstance(res, TensExpr): + return res + im = _IndexStructure.from_components_free_dum(res.components, free, dum) + return res._set_new_index_structure(im) + + def _set_new_index_structure(self, im, is_canon_bp=False): + indices = im.get_indices() + return self._set_indices(*indices, is_canon_bp=is_canon_bp) + + def _set_indices(self, *indices, is_canon_bp=False, **kw_args): + if len(indices) != self.ext_rank: + raise ValueError("indices length mismatch") + args = list(self.args) + pos = 0 + for i, arg in enumerate(args): + if not isinstance(arg, TensExpr): + continue + assert isinstance(arg, Tensor) + ext_rank = arg.ext_rank + args[i] = arg._set_indices(*indices[pos:pos+ext_rank]) + pos += ext_rank + return TensMul(*args, is_canon_bp=is_canon_bp).doit(deep=False) + + @staticmethod + def _index_replacement_for_contract_metric(args, free, dum): + for arg in args: + if not isinstance(arg, TensExpr): + continue + assert isinstance(arg, Tensor) + + def substitute_indices(self, *index_tuples): + new_args = [] + for arg in self.args: + if isinstance(arg, TensExpr): + arg = arg.substitute_indices(*index_tuples) + new_args.append(arg) + return TensMul(*new_args).doit(deep=False) + + def __call__(self, *indices): + deprecate_call() + free_args = self.free_args + indices = list(indices) + if [x.tensor_index_type for x in indices] != [x.tensor_index_type for x in free_args]: + raise ValueError('incompatible types') + if indices == free_args: + return self + t = self.substitute_indices(*list(zip(free_args, indices))) + + # object is rebuilt in order to make sure that all contracted indices + # get recognized as dummies, but only if there are contracted indices. + if len({i if i.is_up else -i for i in indices}) != len(indices): + return t.func(*t.args) + return t + + def _extract_data(self, replacement_dict): + args_indices, arrays = zip(*[arg._extract_data(replacement_dict) for arg in self.args if isinstance(arg, TensExpr)]) + coeff = reduce(operator.mul, [a for a in self.args if not isinstance(a, TensExpr)], S.One) + indices, free, free_names, dummy_data = TensMul._indices_to_free_dum(args_indices) + dum = TensMul._dummy_data_to_dum(dummy_data) + ext_rank = self.ext_rank + free.sort(key=lambda x: x[1]) + free_indices = [i[0] for i in free] + return free_indices, coeff*_TensorDataLazyEvaluator.data_contract_dum(arrays, dum, ext_rank) + + @property + def data(self): + deprecate_data() + with ignore_warnings(SymPyDeprecationWarning): + dat = _tensor_data_substitution_dict[self.expand()] + return dat + + @data.setter + def data(self, data): + deprecate_data() + raise ValueError("Not possible to set component data to a tensor expression") + + @data.deleter + def data(self): + deprecate_data() + raise ValueError("Not possible to delete component data to a tensor expression") + + def __iter__(self): + deprecate_data() + with ignore_warnings(SymPyDeprecationWarning): + if self.data is None: + raise ValueError("No iteration on abstract tensors") + return self.data.__iter__() + + @staticmethod + def _dedupe_indices(new, exclude): + """ + exclude: set + new: TensExpr + + If ``new`` has any dummy indices that are in ``exclude``, return a version + of new with those indices replaced. If no replacements are needed, + return None + + """ + exclude = set(exclude) + dums_new = set(get_dummy_indices(new)) + free_new = set(get_free_indices(new)) + + conflicts = dums_new.intersection(exclude) + if len(conflicts) == 0: + return None + + """ + ``exclude_for_gen`` is to be passed to ``_IndexStructure._get_generator_for_dummy_indices()``. + Since the latter does not use the index position for anything, we just + set it as ``None`` here. + """ + exclude.update(dums_new) + exclude.update(free_new) + exclude_for_gen = [(i, None) for i in exclude] + gen = _IndexStructure._get_generator_for_dummy_indices(exclude_for_gen) + repl = {} + for d in conflicts: + if -d in repl.keys(): + continue + newname = gen(d.tensor_index_type) + new_d = d.func(newname, *d.args[1:]) + repl[d] = new_d + repl[-d] = -new_d + + if len(repl) == 0: + return None + + new_renamed = new._replace_indices(repl) + return new_renamed + + def _dedupe_indices_in_rule(self, rule): + """ + rule: dict + + This applies TensMul._dedupe_indices on all values of rule. + + """ + index_rules = {k:v for k,v in rule.items() if isinstance(k, TensorIndex)} + other_rules = {k:v for k,v in rule.items() if k not in index_rules.keys()} + exclude = set(self.get_indices()) + + newrule = {} + newrule.update(index_rules) + exclude.update(index_rules.keys()) + exclude.update(index_rules.values()) + for old, new in other_rules.items(): + new_renamed = TensMul._dedupe_indices(new, exclude) + if old == new or new_renamed is None: + newrule[old] = new + else: + newrule[old] = new_renamed + exclude.update(get_indices(new_renamed)) + return newrule + + def _eval_subs(self, old, new): + """ + If new is an index which is already present in self as a dummy, the dummies in self should be renamed. + """ + + if not isinstance(new, TensorIndex): + return None + + exclude = {new} + self_renamed = self._dedupe_indices(self, exclude) + if self_renamed is None: + return None + else: + return self_renamed._subs(old, new).doit(deep=False) + + def _eval_rewrite_as_Indexed(self, *args, **kwargs): + from sympy.concrete.summations import Sum + index_symbols = [i.args[0] for i in self.get_indices()] + args = [arg.args[0] if isinstance(arg, Sum) else arg for arg in args] + expr = Mul.fromiter(args) + return self._check_add_Sum(expr, index_symbols) + + def _eval_partial_derivative(self, s): + # Evaluation like Mul + terms = [] + for i, arg in enumerate(self.args): + # checking whether some tensor instance is differentiated + # or some other thing is necessary, but ugly + if isinstance(arg, TensExpr): + d = arg._eval_partial_derivative(s) + else: + # do not call diff is s is no symbol + if s._diff_wrt: + d = arg._eval_derivative(s) + else: + d = S.Zero + if d: + terms.append(TensMul.fromiter(self.args[:i] + (d,) + self.args[i + 1:]).doit(deep=False)) + return TensAdd.fromiter(terms).doit(deep=False) + + + def _matches_commutative(self, expr, repl_dict=None, old=False): + """ + Match assuming all tensors commute. But note that we are not assuming anything about their symmetry under index permutations. + """ + #Take care of the various possible types for expr. + if not isinstance(expr, TensMul): + if isinstance(expr, (TensExpr, Expr)): + expr = TensMul(expr) + else: + return None + + #The code that follows assumes expr is a TensMul + + if repl_dict is None: + repl_dict = {} + else: + repl_dict = repl_dict.copy() + + #Make sure that none of the dummy indices in self, expr conflict with the values already present in repl_dict. This may happen due to automatic index relabelling when rem_query and rem_expr are formed later on in this function (it calls itself recursively). + indices = [k for k in repl_dict.values() if isinstance(k ,TensorIndex)] + + def dedupe(expr): + renamed = TensMul._dedupe_indices(expr, indices) + if renamed is not None: + return renamed + else: + return expr + + self = dedupe(self) + expr = dedupe(expr) + + #Find the non-tensor part of expr. This need not be the same as expr.coeff when expr.doit() has not been called. + expr_coeff = reduce(lambda a, b: a*b, [arg for arg in expr.args if not isinstance(arg, TensExpr)], S.One) + + # handle simple patterns + if self == expr: + return repl_dict + + if len(_get_wilds(self)) == 0: + return self._matches_simple(expr, repl_dict, old) + + def siftkey(arg): + if isinstance(arg, WildTensor): + return "WildTensor" + elif isinstance(arg, (Tensor, TensExpr)): + return "Tensor" + else: + return "coeff" + + query_sifted = sift(self.args, siftkey) + expr_sifted = sift(expr.args, siftkey) + + #Sanity checks + if "coeff" in query_sifted.keys(): + if TensMul(*query_sifted["coeff"]).doit(deep=False) != self.coeff: + raise NotImplementedError(f"Found something that we do not know to handle: {query_sifted['coeff']}") + if "coeff" in expr_sifted.keys(): + if TensMul(*expr_sifted["coeff"]).doit(deep=False) != expr_coeff: + raise NotImplementedError(f"Found something that we do not know to handle: {expr_sifted['coeff']}") + + query_tens_heads = {tuple(getattr(x, "components", [])) for x in query_sifted["Tensor"]} #We use getattr because, e.g. TensAdd does not have the 'components' attribute. + expr_tens_heads = {tuple(getattr(x, "components", [])) for x in expr_sifted["Tensor"]} + if not query_tens_heads.issubset(expr_tens_heads): + #Some tensorheads in self are not present in the expr + return None + + #Try to match all non-wild tensors of self with tensors that compose expr + if len(query_sifted["Tensor"]) > 0: + q_tensor = query_sifted["Tensor"][0] + """ + We need to iterate over all possible symmetrized forms of q_tensor since the matches given by some of them may map dummy indices to free indices; the information about which indices are dummy/free will only be available later, when we are doing rem_q.matches(rem_e) + """ + for q_tens in q_tensor._get_symmetrized_forms(): + for e in expr_sifted["Tensor"]: + if isinstance(q_tens, TensMul): + #q_tensor got a minus sign due to this permutation. + sign = -1 + else: + sign = 1 + + """ + _matches is used here since we are already iterating over index permutations of q_tensor. Also note that the sign is removed from q_tensor, and will later be put into rem_q. + """ + m = (sign*q_tens)._matches(e) + if m is None: + continue + + rem_query = self.func(sign, *[a for a in self.args if a != q_tensor]).doit(deep=False) + rem_expr = expr.func(*[a for a in expr.args if a != e]).doit(deep=False) + tmp_repl = {} + tmp_repl.update(repl_dict) + tmp_repl.update(m) + rem_m = rem_query.matches(rem_expr, repl_dict=tmp_repl) + if rem_m is not None: + #Check that contracted indices are not mapped to different indices. + internally_consistent = True + for k in rem_m.keys(): + if isinstance(k,TensorIndex): + if -k in rem_m.keys() and rem_m[-k] != -rem_m[k]: + internally_consistent = False + break + if internally_consistent: + repl_dict.update(rem_m) + return repl_dict + + return None + + #Try to match WildTensor instances which have indices + matched_e_tensors = [] + remaining_e_tensors = expr_sifted["Tensor"] + indexless_wilds, wilds = sift(query_sifted["WildTensor"], lambda x: len(x.get_free_indices()) == 0, binary=True) + + for w in wilds: + free_this_wild = set(w.get_free_indices()) + tensors_to_try = [] + for t in remaining_e_tensors: + free = t.get_free_indices() + shares_indices_with_wild = True + for i in free: + if all(j.matches(i) is None for j in free_this_wild): + #The index i matches none of the indices in free_this_wild + shares_indices_with_wild = False + if shares_indices_with_wild: + tensors_to_try.append(t) + + m = w.matches(TensMul(*tensors_to_try).doit(deep=False) ) + if m is None: + return None + else: + for tens in tensors_to_try: + matched_e_tensors.append(tens) + repl_dict.update(m) + + #Try to match indexless WildTensor instances + remaining_e_tensors = [t for t in expr_sifted["Tensor"] if t not in matched_e_tensors] + if len(indexless_wilds) > 0: + #If there are any remaining tensors, match them with the indexless WildTensor + m = indexless_wilds[0].matches( TensMul(1,*remaining_e_tensors).doit(deep=False) ) + if m is None: + return None + else: + repl_dict.update(m) + elif len(remaining_e_tensors) > 0: + return None + + #Try to match the non-tensorial coefficient + m = self.coeff.matches(expr_coeff, old=old) + if m is None: + return None + else: + repl_dict.update(m) + + return repl_dict + + def matches(self, expr, repl_dict=None, old=False): + expr = sympify(expr) + + if repl_dict is None: + repl_dict = {} + else: + repl_dict = repl_dict.copy() + + commute = all(arg.component.comm == 0 for arg in expr.args if isinstance(arg, Tensor)) + if commute: + return self._matches_commutative(expr, repl_dict, old) + else: + raise NotImplementedError("Tensor matching not implemented for non-commuting tensors") + +class TensorElement(TensExpr): + """ + Tensor with evaluated components. + + Examples + ======== + + >>> from sympy.tensor.tensor import TensorIndexType, TensorHead, TensorSymmetry + >>> from sympy import symbols + >>> L = TensorIndexType("L") + >>> i, j, k = symbols("i j k") + >>> A = TensorHead("A", [L, L], TensorSymmetry.fully_symmetric(2)) + >>> A(i, j).get_free_indices() + [i, j] + + If we want to set component ``i`` to a specific value, use the + ``TensorElement`` class: + + >>> from sympy.tensor.tensor import TensorElement + >>> te = TensorElement(A(i, j), {i: 2}) + + As index ``i`` has been accessed (``{i: 2}`` is the evaluation of its 3rd + element), the free indices will only contain ``j``: + + >>> te.get_free_indices() + [j] + """ + + def __new__(cls, expr, index_map): + if not isinstance(expr, Tensor): + # remap + if not isinstance(expr, TensExpr): + raise TypeError("%s is not a tensor expression" % expr) + return expr.func(*[TensorElement(arg, index_map) for arg in expr.args]) + expr_free_indices = expr.get_free_indices() + name_translation = {i.args[0]: i for i in expr_free_indices} + index_map = {name_translation.get(index, index): value for index, value in index_map.items()} + index_map = {index: value for index, value in index_map.items() if index in expr_free_indices} + if len(index_map) == 0: + return expr + free_indices = [i for i in expr_free_indices if i not in index_map.keys()] + index_map = Dict(index_map) + obj = TensExpr.__new__(cls, expr, index_map) + obj._free_indices = free_indices + return obj + + @property + def free(self): + return [(index, i) for i, index in enumerate(self.get_free_indices())] + + @property + def dum(self): + # TODO: inherit dummies from expr + return [] + + @property + def expr(self): + return self._args[0] + + @property + def index_map(self): + return self._args[1] + + @property + def coeff(self): + return S.One + + @property + def nocoeff(self): + return self + + def get_free_indices(self): + return self._free_indices + + def _replace_indices(self, repl: dict[TensorIndex, TensorIndex]) -> TensExpr: + # TODO: can be improved: + return self.xreplace(repl) + + def get_indices(self): + return self.get_free_indices() + + def _extract_data(self, replacement_dict): + ret_indices, array = self.expr._extract_data(replacement_dict) + index_map = self.index_map + slice_tuple = tuple(index_map.get(i, slice(None)) for i in ret_indices) + ret_indices = [i for i in ret_indices if i not in index_map] + array = array.__getitem__(slice_tuple) + return ret_indices, array + + +class WildTensorHead(TensorHead): + """ + A wild object that is used to create ``WildTensor`` instances + + Explanation + =========== + + Examples + ======== + >>> from sympy.tensor.tensor import TensorHead, TensorIndex, WildTensorHead, TensorIndexType + >>> R3 = TensorIndexType('R3', dim=3) + >>> p = TensorIndex('p', R3) + >>> q = TensorIndex('q', R3) + + A WildTensorHead can be created without specifying a ``TensorIndexType`` + + >>> W = WildTensorHead("W") + + Calling it with a ``TensorIndex`` creates a ``WildTensor`` instance. + + >>> type(W(p)) + + + The ``TensorIndexType`` is automatically detected from the index that is passed + + >>> W(p).component + W(R3) + + Calling it with no indices returns an object that can match tensors with any number of indices. + + >>> K = TensorHead('K', [R3]) + >>> Q = TensorHead('Q', [R3, R3]) + >>> W().matches(K(p)) + {W: K(p)} + >>> W().matches(Q(p,q)) + {W: Q(p, q)} + + If you want to ignore the order of indices while matching, pass ``unordered_indices=True``. + + >>> U = WildTensorHead("U", unordered_indices=True) + >>> W(p,q).matches(Q(q,p)) + >>> U(p,q).matches(Q(q,p)) + {U(R3,R3): _WildTensExpr(Q(q, p))} + + Parameters + ========== + name : name of the tensor + unordered_indices : whether the order of the indices matters for matching + (default: False) + + See also + ======== + ``WildTensor`` + ``TensorHead`` + + """ + def __new__(cls, name, index_types=None, symmetry=None, comm=0, unordered_indices=False): + if isinstance(name, str): + name_symbol = Symbol(name) + elif isinstance(name, Symbol): + name_symbol = name + else: + raise ValueError("invalid name") + + if index_types is None: + index_types = [] + + if symmetry is None: + symmetry = TensorSymmetry.no_symmetry(len(index_types)) + else: + assert symmetry.rank == len(index_types) + + if symmetry != TensorSymmetry.no_symmetry(len(index_types)): + raise NotImplementedError("Wild matching based on symmetry is not implemented.") + + obj = Basic.__new__(cls, name_symbol, Tuple(*index_types), sympify(symmetry), sympify(comm), sympify(unordered_indices)) + + return obj + + @property + def unordered_indices(self): + return self.args[4] + + def __call__(self, *indices, **kwargs): + tensor = WildTensor(self, indices, **kwargs) + return tensor.doit() + + +class WildTensor(Tensor): + """ + A wild object which matches ``Tensor`` instances + + Explanation + =========== + This is instantiated by attaching indices to a ``WildTensorHead`` instance. + + Examples + ======== + >>> from sympy.tensor.tensor import TensorHead, TensorIndex, WildTensorHead, TensorIndexType + >>> W = WildTensorHead("W") + >>> R3 = TensorIndexType('R3', dim=3) + >>> p = TensorIndex('p', R3) + >>> q = TensorIndex('q', R3) + >>> K = TensorHead('K', [R3]) + >>> Q = TensorHead('Q', [R3, R3]) + + Matching also takes the indices into account + >>> W(p).matches(K(p)) + {W(R3): _WildTensExpr(K(p))} + >>> W(p).matches(K(q)) + >>> W(p).matches(K(-p)) + + If you want to match objects with any number of indices, just use a ``WildTensor`` with no indices. + >>> W().matches(K(p)) + {W: K(p)} + >>> W().matches(Q(p,q)) + {W: Q(p, q)} + + See Also + ======== + ``WildTensorHead`` + ``Tensor`` + + """ + def __new__(cls, tensor_head, indices, **kw_args): + is_canon_bp = kw_args.pop("is_canon_bp", False) + + if tensor_head.func == TensorHead: + """ + If someone tried to call WildTensor by supplying a TensorHead (not a WildTensorHead), return a normal tensor instead. This is helpful when using subs on an expression to replace occurrences of a WildTensorHead with a TensorHead. + """ + return Tensor(tensor_head, indices, is_canon_bp=is_canon_bp, **kw_args) + elif tensor_head.func == _WildTensExpr: + return tensor_head(*indices) + + indices = cls._parse_indices(tensor_head, indices) + index_types = [ind.tensor_index_type for ind in indices] + tensor_head = tensor_head.func( + tensor_head.name, + index_types, + symmetry=None, + comm=tensor_head.comm, + unordered_indices=tensor_head.unordered_indices, + ) + + obj = Basic.__new__(cls, tensor_head, Tuple(*indices)) + obj.name = tensor_head.name + obj._index_structure = _IndexStructure.from_indices(*indices) + obj._free = obj._index_structure.free[:] + obj._dum = obj._index_structure.dum[:] + obj._ext_rank = obj._index_structure._ext_rank + obj._coeff = S.One + obj._nocoeff = obj + obj._component = tensor_head + obj._components = [tensor_head] + if tensor_head.rank != len(indices): + raise ValueError("wrong number of indices") + obj.is_canon_bp = is_canon_bp + obj._index_map = obj._build_index_map(indices, obj._index_structure) + + return obj + + + def matches(self, expr, repl_dict=None, old=False): + if not isinstance(expr, TensExpr) and expr != S(1): + return None + + if repl_dict is None: + repl_dict = {} + else: + repl_dict = repl_dict.copy() + + if len(self.indices) > 0: + if not hasattr(expr, "get_free_indices"): + return None + expr_indices = expr.get_free_indices() + if len(expr_indices) != len(self.indices): + return None + if self._component.unordered_indices: + m = self._match_indices_ignoring_order(expr) + if m is None: + return None + else: + repl_dict.update(m) + else: + for i in range(len(expr_indices)): + m = self.indices[i].matches(expr_indices[i]) + if m is None: + return None + else: + repl_dict.update(m) + + repl_dict[self.component] = _WildTensExpr(expr) + else: + #If no indices were passed to the WildTensor, it may match tensors with any number of indices. + repl_dict[self] = expr + + return repl_dict + + def _match_indices_ignoring_order(self, expr, repl_dict=None, old=False): + """ + Helper method for matches. Checks if the indices of self and expr + match disregarding index ordering. + """ + if repl_dict is None: + repl_dict = {} + else: + repl_dict = repl_dict.copy() + + def siftkey(ind): + if isinstance(ind, WildTensorIndex): + if ind.ignore_updown: + return "wild, updown" + else: + return "wild" + else: + return "nonwild" + + indices_sifted = sift(self.indices, siftkey) + + matched_indices = [] + expr_indices_remaining = expr.get_indices() + for ind in indices_sifted["nonwild"]: + matched_this_ind = False + for e_ind in expr_indices_remaining: + if e_ind in matched_indices: + continue + m = ind.matches(e_ind) + if m is not None: + matched_this_ind = True + repl_dict.update(m) + matched_indices.append(e_ind) + break + if not matched_this_ind: + return None + + expr_indices_remaining = [i for i in expr_indices_remaining if i not in matched_indices] + for ind in indices_sifted["wild"]: + matched_this_ind = False + for e_ind in expr_indices_remaining: + m = ind.matches(e_ind) + if m is not None: + if -ind in repl_dict.keys() and -repl_dict[-ind] != m[ind]: + return None + matched_this_ind = True + repl_dict.update(m) + matched_indices.append(e_ind) + break + if not matched_this_ind: + return None + + expr_indices_remaining = [i for i in expr_indices_remaining if i not in matched_indices] + for ind in indices_sifted["wild, updown"]: + matched_this_ind = False + for e_ind in expr_indices_remaining: + m = ind.matches(e_ind) + if m is not None: + if -ind in repl_dict.keys() and -repl_dict[-ind] != m[ind]: + return None + matched_this_ind = True + repl_dict.update(m) + matched_indices.append(e_ind) + break + if not matched_this_ind: + return None + + if len(matched_indices) < len(self.indices): + return None + else: + return repl_dict + +class WildTensorIndex(TensorIndex): + """ + A wild object that matches TensorIndex instances. + + Examples + ======== + >>> from sympy.tensor.tensor import TensorIndex, TensorIndexType, WildTensorIndex + >>> R3 = TensorIndexType('R3', dim=3) + >>> p = TensorIndex("p", R3) + + By default, covariant indices only match with covariant indices (and + similarly for contravariant) + + >>> q = WildTensorIndex("q", R3) + >>> (q).matches(p) + {q: p} + >>> (q).matches(-p) + + If you want matching to ignore whether the index is co/contra-variant, set + ignore_updown=True + + >>> r = WildTensorIndex("r", R3, ignore_updown=True) + >>> (r).matches(-p) + {r: -p} + >>> (r).matches(p) + {r: p} + + Parameters + ========== + name : name of the index (string), or ``True`` if you want it to be + automatically assigned + tensor_index_type : ``TensorIndexType`` of the index + is_up : flag for contravariant index (is_up=True by default) + ignore_updown : bool, Whether this should match both co- and contra-variant + indices (default:False) + """ + def __new__(cls, name, tensor_index_type, is_up=True, ignore_updown=False): + if isinstance(name, str): + name_symbol = Symbol(name) + elif isinstance(name, Symbol): + name_symbol = name + elif name is True: + name = "_i{}".format(len(tensor_index_type._autogenerated)) + name_symbol = Symbol(name) + tensor_index_type._autogenerated.append(name_symbol) + else: + raise ValueError("invalid name") + + is_up = sympify(is_up) + ignore_updown = sympify(ignore_updown) + return Basic.__new__(cls, name_symbol, tensor_index_type, is_up, ignore_updown) + + @property + def ignore_updown(self): + return self.args[3] + + def __neg__(self): + t1 = WildTensorIndex(self.name, self.tensor_index_type, + (not self.is_up), self.ignore_updown) + return t1 + + def matches(self, expr, repl_dict=None, old=False): + if not isinstance(expr, TensorIndex): + return None + if self.tensor_index_type != expr.tensor_index_type: + return None + if not self.ignore_updown: + if self.is_up != expr.is_up: + return None + + if repl_dict is None: + repl_dict = {} + else: + repl_dict = repl_dict.copy() + + repl_dict[self] = expr + return repl_dict + + +class _WildTensExpr(Basic): + """ + INTERNAL USE ONLY + + This is an object that helps with replacement of WildTensors in expressions. + When this object is set as the tensor_head of a WildTensor, it replaces the + WildTensor by a TensExpr (passed when initializing this object). + + Examples + ======== + >>> from sympy.tensor.tensor import WildTensorHead, TensorIndex, TensorHead, TensorIndexType + >>> W = WildTensorHead("W") + >>> R3 = TensorIndexType('R3', dim=3) + >>> p = TensorIndex('p', R3) + >>> q = TensorIndex('q', R3) + >>> K = TensorHead('K', [R3]) + >>> print( ( K(p) ).replace( W(p), W(q)*W(-q)*W(p) ) ) + K(R_0)*K(-R_0)*K(p) + + """ + def __init__(self, expr): + if not isinstance(expr, TensExpr): + raise TypeError("_WildTensExpr expects a TensExpr as argument") + self.expr = expr + + def __call__(self, *indices): + return self.expr._replace_indices(dict(zip(self.expr.get_free_indices(), indices))) + + def __neg__(self): + return self.func(self.expr*S.NegativeOne) + + def __abs__(self): + raise NotImplementedError + + def __add__(self, other): + if other.func != self.func: + raise TypeError(f"Cannot add {self.func} to {other.func}") + return self.func(self.expr+other.expr) + + def __radd__(self, other): + if other.func != self.func: + raise TypeError(f"Cannot add {self.func} to {other.func}") + return self.func(other.expr+self.expr) + + def __sub__(self, other): + return self + (-other) + + def __rsub__(self, other): + return other + (-self) + + def __mul__(self, other): + raise NotImplementedError + + def __rmul__(self, other): + raise NotImplementedError + + def __truediv__(self, other): + raise NotImplementedError + + def __rtruediv__(self, other): + raise NotImplementedError + + def __pow__(self, other): + raise NotImplementedError + + def __rpow__(self, other): + raise NotImplementedError + + +def canon_bp(p): + """ + Butler-Portugal canonicalization. See ``tensor_can.py`` from the + combinatorics module for the details. + """ + if isinstance(p, TensExpr): + return p.canon_bp() + return p + + +def tensor_mul(*a): + """ + product of tensors + """ + if not a: + return TensMul.from_data(S.One, [], [], []) + t = a[0] + for tx in a[1:]: + t = t*tx + return t + + +def riemann_cyclic_replace(t_r): + """ + replace Riemann tensor with an equivalent expression + + ``R(m,n,p,q) -> 2/3*R(m,n,p,q) - 1/3*R(m,q,n,p) + 1/3*R(m,p,n,q)`` + + """ + free = sorted(t_r.free, key=lambda x: x[1]) + m, n, p, q = [x[0] for x in free] + t0 = t_r*Rational(2, 3) + t1 = -t_r.substitute_indices((m,m),(n,q),(p,n),(q,p))*Rational(1, 3) + t2 = t_r.substitute_indices((m,m),(n,p),(p,n),(q,q))*Rational(1, 3) + t3 = t0 + t1 + t2 + return t3 + +def riemann_cyclic(t2): + """ + Replace each Riemann tensor with an equivalent expression + satisfying the cyclic identity. + + This trick is discussed in the reference guide to Cadabra. + + Examples + ======== + + >>> from sympy.tensor.tensor import TensorIndexType, tensor_indices, TensorHead, riemann_cyclic, TensorSymmetry + >>> Lorentz = TensorIndexType('Lorentz', dummy_name='L') + >>> i, j, k, l = tensor_indices('i,j,k,l', Lorentz) + >>> R = TensorHead('R', [Lorentz]*4, TensorSymmetry.riemann()) + >>> t = R(i,j,k,l)*(R(-i,-j,-k,-l) - 2*R(-i,-k,-j,-l)) + >>> riemann_cyclic(t) + 0 + """ + t2 = t2.expand() + if isinstance(t2, (TensMul, Tensor)): + args = [t2] + else: + args = t2.args + a1 = [x.split() for x in args] + a2 = [[riemann_cyclic_replace(tx) for tx in y] for y in a1] + a3 = [tensor_mul(*v) for v in a2] + t3 = TensAdd(*a3).doit(deep=False) + if not t3: + return t3 + else: + return canon_bp(t3) + + +def get_lines(ex, index_type): + """ + Returns ``(lines, traces, rest)`` for an index type, + where ``lines`` is the list of list of positions of a matrix line, + ``traces`` is the list of list of traced matrix lines, + ``rest`` is the rest of the elements of the tensor. + """ + def _join_lines(a): + i = 0 + while i < len(a): + x = a[i] + xend = x[-1] + xstart = x[0] + hit = True + while hit: + hit = False + for j in range(i + 1, len(a)): + if j >= len(a): + break + if a[j][0] == xend: + hit = True + x.extend(a[j][1:]) + xend = x[-1] + a.pop(j) + continue + if a[j][0] == xstart: + hit = True + a[i] = reversed(a[j][1:]) + x + x = a[i] + xstart = a[i][0] + a.pop(j) + continue + if a[j][-1] == xend: + hit = True + x.extend(reversed(a[j][:-1])) + xend = x[-1] + a.pop(j) + continue + if a[j][-1] == xstart: + hit = True + a[i] = a[j][:-1] + x + x = a[i] + xstart = x[0] + a.pop(j) + continue + i += 1 + return a + + arguments = ex.args + dt = {} + for c in ex.args: + if not isinstance(c, TensExpr): + continue + if c in dt: + continue + index_types = c.index_types + a = [] + for i in range(len(index_types)): + if index_types[i] is index_type: + a.append(i) + if len(a) > 2: + raise ValueError('at most two indices of type %s allowed' % index_type) + if len(a) == 2: + dt[c] = a + #dum = ex.dum + lines = [] + traces = [] + traces1 = [] + #indices_to_args_pos = ex._get_indices_to_args_pos() + # TODO: add a dum_to_components_map ? + for p0, p1, c0, c1 in ex.dum_in_args: + if arguments[c0] not in dt: + continue + if c0 == c1: + traces.append([c0]) + continue + ta0 = dt[arguments[c0]] + ta1 = dt[arguments[c1]] + if p0 not in ta0: + continue + if ta0.index(p0) == ta1.index(p1): + # case gamma(i,s0,-s1) in c0, gamma(j,-s0,s2) in c1; + # to deal with this case one could add to the position + # a flag for transposition; + # one could write [(c0, False), (c1, True)] + raise NotImplementedError + # if p0 == ta0[1] then G in pos c0 is mult on the right by G in c1 + # if p0 == ta0[0] then G in pos c1 is mult on the right by G in c0 + ta0 = dt[arguments[c0]] + b0, b1 = (c0, c1) if p0 == ta0[1] else (c1, c0) + lines1 = lines.copy() + for line in lines: + if line[-1] == b0: + if line[0] == b1: + n = line.index(min(line)) + traces1.append(line) + traces.append(line[n:] + line[:n]) + else: + line.append(b1) + break + elif line[0] == b1: + line.insert(0, b0) + break + else: + lines1.append([b0, b1]) + + lines = [x for x in lines1 if x not in traces1] + lines = _join_lines(lines) + rest = [] + for line in lines: + for y in line: + rest.append(y) + for line in traces: + for y in line: + rest.append(y) + rest = [x for x in range(len(arguments)) if x not in rest] + + return lines, traces, rest + + +def get_free_indices(t): + if not isinstance(t, TensExpr): + return () + return t.get_free_indices() + + +def get_indices(t): + if not isinstance(t, TensExpr): + return () + return t.get_indices() + +def get_dummy_indices(t): + if not isinstance(t, TensExpr): + return () + inds = t.get_indices() + free = t.get_free_indices() + return [i for i in inds if i not in free] + +def get_index_structure(t): + if isinstance(t, TensExpr): + return t._index_structure + return _IndexStructure([], [], [], []) + + +def get_coeff(t): + if isinstance(t, Tensor): + return S.One + if isinstance(t, TensMul): + return t.coeff + if isinstance(t, TensExpr): + raise ValueError("no coefficient associated to this tensor expression") + return t + +def contract_metric(t, g): + if isinstance(t, TensExpr): + return t.contract_metric(g) + return t + +def perm2tensor(t, g, is_canon_bp=False): + """ + Returns the tensor corresponding to the permutation ``g`` + + For further details, see the method in ``TIDS`` with the same name. + """ + if not isinstance(t, TensExpr): + return t + elif isinstance(t, (Tensor, TensMul)): + nim = get_index_structure(t).perm2tensor(g, is_canon_bp=is_canon_bp) + res = t._set_new_index_structure(nim, is_canon_bp=is_canon_bp) + if g[-1] != len(g) - 1: + return -res + + return res + raise NotImplementedError() + + +def substitute_indices(t, *index_tuples): + if not isinstance(t, TensExpr): + return t + return t.substitute_indices(*index_tuples) + + +def _get_wilds(expr): + return list(expr.atoms(Wild, WildFunction, WildTensor, WildTensorIndex, WildTensorHead)) + + +def get_postprocessor(cls): + def _postprocessor(expr): + tens_class = {Mul: TensMul, Add: TensAdd}[cls] + if any(isinstance(a, TensExpr) for a in expr.args): + return tens_class(*expr.args) + else: + return expr + + return _postprocessor + +Basic._constructor_postprocessor_mapping[TensExpr] = { + "Mul": [get_postprocessor(Mul)], +} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/tests/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/tests/test_functions.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/tests/test_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..ae40865d1bddffaa976dc3d94ae1ef1b6c97ca35 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/tests/test_functions.py @@ -0,0 +1,57 @@ +from sympy.tensor.functions import TensorProduct +from sympy.matrices.dense import Matrix +from sympy.matrices.expressions.matexpr import MatrixSymbol +from sympy.tensor.array import Array +from sympy.abc import x, y, z +from sympy.abc import i, j, k, l + + +A = MatrixSymbol("A", 3, 3) +B = MatrixSymbol("B", 3, 3) +C = MatrixSymbol("C", 3, 3) + + +def test_TensorProduct_construction(): + assert TensorProduct(3, 4) == 12 + assert isinstance(TensorProduct(A, A), TensorProduct) + + expr = TensorProduct(TensorProduct(x, y), z) + assert expr == x*y*z + + expr = TensorProduct(TensorProduct(A, B), C) + assert expr == TensorProduct(A, B, C) + + expr = TensorProduct(Matrix.eye(2), Array([[0, -1], [1, 0]])) + assert expr == Array([ + [ + [[0, -1], [1, 0]], + [[0, 0], [0, 0]] + ], + [ + [[0, 0], [0, 0]], + [[0, -1], [1, 0]] + ] + ]) + + +def test_TensorProduct_shape(): + + expr = TensorProduct(3, 4, evaluate=False) + assert expr.shape == () + assert expr.rank() == 0 + + expr = TensorProduct(Array([1, 2]), Array([x, y]), evaluate=False) + assert expr.shape == (2, 2) + assert expr.rank() == 2 + expr = TensorProduct(expr, expr, evaluate=False) + assert expr.shape == (2, 2, 2, 2) + assert expr.rank() == 4 + + expr = TensorProduct(Matrix.eye(2), Array([[0, -1], [1, 0]]), evaluate=False) + assert expr.shape == (2, 2, 2, 2) + assert expr.rank() == 4 + + +def test_TensorProduct_getitem(): + expr = TensorProduct(A, B) + assert expr[i, j, k, l] == A[i, j]*B[k, l] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/tests/test_index_methods.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/tests/test_index_methods.py new file mode 100644 index 0000000000000000000000000000000000000000..df20f7e7c1ab392321e8350b95dd07c5639c1865 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/tests/test_index_methods.py @@ -0,0 +1,227 @@ +from sympy.core import symbols, S, Pow, Function +from sympy.functions import exp +from sympy.testing.pytest import raises +from sympy.tensor.indexed import Idx, IndexedBase +from sympy.tensor.index_methods import IndexConformanceException + +from sympy.tensor.index_methods import (get_contraction_structure, get_indices) + + +def test_trivial_indices(): + x, y = symbols('x y') + assert get_indices(x) == (set(), {}) + assert get_indices(x*y) == (set(), {}) + assert get_indices(x + y) == (set(), {}) + assert get_indices(x**y) == (set(), {}) + + +def test_get_indices_Indexed(): + x = IndexedBase('x') + i, j = Idx('i'), Idx('j') + assert get_indices(x[i, j]) == ({i, j}, {}) + assert get_indices(x[j, i]) == ({j, i}, {}) + + +def test_get_indices_Idx(): + f = Function('f') + i, j = Idx('i'), Idx('j') + assert get_indices(f(i)*j) == ({i, j}, {}) + assert get_indices(f(j, i)) == ({j, i}, {}) + assert get_indices(f(i)*i) == (set(), {}) + + +def test_get_indices_mul(): + x = IndexedBase('x') + y = IndexedBase('y') + i, j = Idx('i'), Idx('j') + assert get_indices(x[j]*y[i]) == ({i, j}, {}) + assert get_indices(x[i]*y[j]) == ({i, j}, {}) + + +def test_get_indices_exceptions(): + x = IndexedBase('x') + y = IndexedBase('y') + i, j = Idx('i'), Idx('j') + raises(IndexConformanceException, lambda: get_indices(x[i] + y[j])) + + +def test_scalar_broadcast(): + x = IndexedBase('x') + y = IndexedBase('y') + i, j = Idx('i'), Idx('j') + assert get_indices(x[i] + y[i, i]) == ({i}, {}) + assert get_indices(x[i] + y[j, j]) == ({i}, {}) + + +def test_get_indices_add(): + x = IndexedBase('x') + y = IndexedBase('y') + A = IndexedBase('A') + i, j, k = Idx('i'), Idx('j'), Idx('k') + assert get_indices(x[i] + 2*y[i]) == ({i}, {}) + assert get_indices(y[i] + 2*A[i, j]*x[j]) == ({i}, {}) + assert get_indices(y[i] + 2*(x[i] + A[i, j]*x[j])) == ({i}, {}) + assert get_indices(y[i] + x[i]*(A[j, j] + 1)) == ({i}, {}) + assert get_indices( + y[i] + x[i]*x[j]*(y[j] + A[j, k]*x[k])) == ({i}, {}) + + +def test_get_indices_Pow(): + x = IndexedBase('x') + y = IndexedBase('y') + A = IndexedBase('A') + i, j, k = Idx('i'), Idx('j'), Idx('k') + assert get_indices(Pow(x[i], y[j])) == ({i, j}, {}) + assert get_indices(Pow(x[i, k], y[j, k])) == ({i, j, k}, {}) + assert get_indices(Pow(A[i, k], y[k] + A[k, j]*x[j])) == ({i, k}, {}) + assert get_indices(Pow(2, x[i])) == get_indices(exp(x[i])) + + # test of a design decision, this may change: + assert get_indices(Pow(x[i], 2)) == ({i}, {}) + + +def test_get_contraction_structure_basic(): + x = IndexedBase('x') + y = IndexedBase('y') + i, j = Idx('i'), Idx('j') + assert get_contraction_structure(x[i]*y[j]) == {None: {x[i]*y[j]}} + assert get_contraction_structure(x[i] + y[j]) == {None: {x[i], y[j]}} + assert get_contraction_structure(x[i]*y[i]) == {(i,): {x[i]*y[i]}} + assert get_contraction_structure( + 1 + x[i]*y[i]) == {None: {S.One}, (i,): {x[i]*y[i]}} + assert get_contraction_structure(x[i]**y[i]) == {None: {x[i]**y[i]}} + + +def test_get_contraction_structure_complex(): + x = IndexedBase('x') + y = IndexedBase('y') + A = IndexedBase('A') + i, j, k = Idx('i'), Idx('j'), Idx('k') + expr1 = y[i] + A[i, j]*x[j] + d1 = {None: {y[i]}, (j,): {A[i, j]*x[j]}} + assert get_contraction_structure(expr1) == d1 + expr2 = expr1*A[k, i] + x[k] + d2 = {None: {x[k]}, (i,): {expr1*A[k, i]}, expr1*A[k, i]: [d1]} + assert get_contraction_structure(expr2) == d2 + + +def test_contraction_structure_simple_Pow(): + x = IndexedBase('x') + y = IndexedBase('y') + i, j, k = Idx('i'), Idx('j'), Idx('k') + ii_jj = x[i, i]**y[j, j] + assert get_contraction_structure(ii_jj) == { + None: {ii_jj}, + ii_jj: [ + {(i,): {x[i, i]}}, + {(j,): {y[j, j]}} + ] + } + + ii_jk = x[i, i]**y[j, k] + assert get_contraction_structure(ii_jk) == { + None: {x[i, i]**y[j, k]}, + x[i, i]**y[j, k]: [ + {(i,): {x[i, i]}} + ] + } + + +def test_contraction_structure_Mul_and_Pow(): + x = IndexedBase('x') + y = IndexedBase('y') + i, j, k = Idx('i'), Idx('j'), Idx('k') + + i_ji = x[i]**(y[j]*x[i]) + assert get_contraction_structure(i_ji) == {None: {i_ji}} + ij_i = (x[i]*y[j])**(y[i]) + assert get_contraction_structure(ij_i) == {None: {ij_i}} + j_ij_i = x[j]*(x[i]*y[j])**(y[i]) + assert get_contraction_structure(j_ij_i) == {(j,): {j_ij_i}} + j_i_ji = x[j]*x[i]**(y[j]*x[i]) + assert get_contraction_structure(j_i_ji) == {(j,): {j_i_ji}} + ij_exp_kki = x[i]*y[j]*exp(y[i]*y[k, k]) + result = get_contraction_structure(ij_exp_kki) + expected = { + (i,): {ij_exp_kki}, + ij_exp_kki: [{ + None: {exp(y[i]*y[k, k])}, + exp(y[i]*y[k, k]): [{ + None: {y[i]*y[k, k]}, + y[i]*y[k, k]: [{(k,): {y[k, k]}}] + }]} + ] + } + assert result == expected + + +def test_contraction_structure_Add_in_Pow(): + x = IndexedBase('x') + y = IndexedBase('y') + i, j, k = Idx('i'), Idx('j'), Idx('k') + s_ii_jj_s = (1 + x[i, i])**(1 + y[j, j]) + expected = { + None: {s_ii_jj_s}, + s_ii_jj_s: [ + {None: {S.One}, (i,): {x[i, i]}}, + {None: {S.One}, (j,): {y[j, j]}} + ] + } + result = get_contraction_structure(s_ii_jj_s) + assert result == expected + + s_ii_jk_s = (1 + x[i, i]) ** (1 + y[j, k]) + expected_2 = { + None: {(x[i, i] + 1)**(y[j, k] + 1)}, + s_ii_jk_s: [ + {None: {S.One}, (i,): {x[i, i]}} + ] + } + result_2 = get_contraction_structure(s_ii_jk_s) + assert result_2 == expected_2 + + +def test_contraction_structure_Pow_in_Pow(): + x = IndexedBase('x') + y = IndexedBase('y') + z = IndexedBase('z') + i, j, k = Idx('i'), Idx('j'), Idx('k') + ii_jj_kk = x[i, i]**y[j, j]**z[k, k] + expected = { + None: {ii_jj_kk}, + ii_jj_kk: [ + {(i,): {x[i, i]}}, + { + None: {y[j, j]**z[k, k]}, + y[j, j]**z[k, k]: [ + {(j,): {y[j, j]}}, + {(k,): {z[k, k]}} + ] + } + ] + } + assert get_contraction_structure(ii_jj_kk) == expected + + +def test_ufunc_support(): + f = Function('f') + g = Function('g') + x = IndexedBase('x') + y = IndexedBase('y') + i, j = Idx('i'), Idx('j') + a = symbols('a') + + assert get_indices(f(x[i])) == ({i}, {}) + assert get_indices(f(x[i], y[j])) == ({i, j}, {}) + assert get_indices(f(y[i])*g(x[i])) == (set(), {}) + assert get_indices(f(a, x[i])) == ({i}, {}) + assert get_indices(f(a, y[i], x[j])*g(x[i])) == ({j}, {}) + assert get_indices(g(f(x[i]))) == ({i}, {}) + + assert get_contraction_structure(f(x[i])) == {None: {f(x[i])}} + assert get_contraction_structure( + f(y[i])*g(x[i])) == {(i,): {f(y[i])*g(x[i])}} + assert get_contraction_structure( + f(y[i])*g(f(x[i]))) == {(i,): {f(y[i])*g(f(x[i]))}} + assert get_contraction_structure( + f(x[j], y[i])*g(x[i])) == {(i,): {f(x[j], y[i])*g(x[i])}} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/tests/test_indexed.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/tests/test_indexed.py new file mode 100644 index 0000000000000000000000000000000000000000..689ec932c8fcefe0a24de289dd2ffd6820c63f19 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/tests/test_indexed.py @@ -0,0 +1,511 @@ +from sympy.core import symbols, Symbol, Tuple, oo, Dummy +from sympy.tensor.indexed import IndexException +from sympy.testing.pytest import raises +from sympy.utilities.iterables import iterable + +# import test: +from sympy.concrete.summations import Sum +from sympy.core.function import Function, Subs, Derivative +from sympy.core.relational import (StrictLessThan, GreaterThan, + StrictGreaterThan, LessThan) +from sympy.core.singleton import S +from sympy.functions.elementary.exponential import exp, log +from sympy.functions.elementary.trigonometric import cos, sin +from sympy.functions.special.tensor_functions import KroneckerDelta +from sympy.series.order import Order +from sympy.sets.fancysets import Range +from sympy.tensor.indexed import IndexedBase, Idx, Indexed + + +def test_Idx_construction(): + i, a, b = symbols('i a b', integer=True) + assert Idx(i) != Idx(i, 1) + assert Idx(i, a) == Idx(i, (0, a - 1)) + assert Idx(i, oo) == Idx(i, (0, oo)) + + x = symbols('x', integer=False) + raises(TypeError, lambda: Idx(x)) + raises(TypeError, lambda: Idx(0.5)) + raises(TypeError, lambda: Idx(i, x)) + raises(TypeError, lambda: Idx(i, 0.5)) + raises(TypeError, lambda: Idx(i, (x, 5))) + raises(TypeError, lambda: Idx(i, (2, x))) + raises(TypeError, lambda: Idx(i, (2, 3.5))) + + +def test_Idx_properties(): + i, a, b = symbols('i a b', integer=True) + assert Idx(i).is_integer + assert Idx(i).name == 'i' + assert Idx(i + 2).name == 'i + 2' + assert Idx('foo').name == 'foo' + + +def test_Idx_bounds(): + i, a, b = symbols('i a b', integer=True) + assert Idx(i).lower is None + assert Idx(i).upper is None + assert Idx(i, a).lower == 0 + assert Idx(i, a).upper == a - 1 + assert Idx(i, 5).lower == 0 + assert Idx(i, 5).upper == 4 + assert Idx(i, oo).lower == 0 + assert Idx(i, oo).upper is oo + assert Idx(i, (a, b)).lower == a + assert Idx(i, (a, b)).upper == b + assert Idx(i, (1, 5)).lower == 1 + assert Idx(i, (1, 5)).upper == 5 + assert Idx(i, (-oo, oo)).lower is -oo + assert Idx(i, (-oo, oo)).upper is oo + + +def test_Idx_fixed_bounds(): + i, a, b, x = symbols('i a b x', integer=True) + assert Idx(x).lower is None + assert Idx(x).upper is None + assert Idx(x, a).lower == 0 + assert Idx(x, a).upper == a - 1 + assert Idx(x, 5).lower == 0 + assert Idx(x, 5).upper == 4 + assert Idx(x, oo).lower == 0 + assert Idx(x, oo).upper is oo + assert Idx(x, (a, b)).lower == a + assert Idx(x, (a, b)).upper == b + assert Idx(x, (1, 5)).lower == 1 + assert Idx(x, (1, 5)).upper == 5 + assert Idx(x, (-oo, oo)).lower is -oo + assert Idx(x, (-oo, oo)).upper is oo + + +def test_Idx_inequalities(): + i14 = Idx("i14", (1, 4)) + i79 = Idx("i79", (7, 9)) + i46 = Idx("i46", (4, 6)) + i35 = Idx("i35", (3, 5)) + + assert i14 <= 5 + assert i14 < 5 + assert not (i14 >= 5) + assert not (i14 > 5) + + assert 5 >= i14 + assert 5 > i14 + assert not (5 <= i14) + assert not (5 < i14) + + assert LessThan(i14, 5) + assert StrictLessThan(i14, 5) + assert not GreaterThan(i14, 5) + assert not StrictGreaterThan(i14, 5) + + assert i14 <= 4 + assert isinstance(i14 < 4, StrictLessThan) + assert isinstance(i14 >= 4, GreaterThan) + assert not (i14 > 4) + + assert isinstance(i14 <= 1, LessThan) + assert not (i14 < 1) + assert i14 >= 1 + assert isinstance(i14 > 1, StrictGreaterThan) + + assert not (i14 <= 0) + assert not (i14 < 0) + assert i14 >= 0 + assert i14 > 0 + + from sympy.abc import x + + assert isinstance(i14 < x, StrictLessThan) + assert isinstance(i14 > x, StrictGreaterThan) + assert isinstance(i14 <= x, LessThan) + assert isinstance(i14 >= x, GreaterThan) + + assert i14 < i79 + assert i14 <= i79 + assert not (i14 > i79) + assert not (i14 >= i79) + + assert i14 <= i46 + assert isinstance(i14 < i46, StrictLessThan) + assert isinstance(i14 >= i46, GreaterThan) + assert not (i14 > i46) + + assert isinstance(i14 < i35, StrictLessThan) + assert isinstance(i14 > i35, StrictGreaterThan) + assert isinstance(i14 <= i35, LessThan) + assert isinstance(i14 >= i35, GreaterThan) + + iNone1 = Idx("iNone1") + iNone2 = Idx("iNone2") + + assert isinstance(iNone1 < iNone2, StrictLessThan) + assert isinstance(iNone1 > iNone2, StrictGreaterThan) + assert isinstance(iNone1 <= iNone2, LessThan) + assert isinstance(iNone1 >= iNone2, GreaterThan) + + +def test_Idx_inequalities_current_fails(): + i14 = Idx("i14", (1, 4)) + + assert S(5) >= i14 + assert S(5) > i14 + assert not (S(5) <= i14) + assert not (S(5) < i14) + + +def test_Idx_func_args(): + i, a, b = symbols('i a b', integer=True) + ii = Idx(i) + assert ii.func(*ii.args) == ii + ii = Idx(i, a) + assert ii.func(*ii.args) == ii + ii = Idx(i, (a, b)) + assert ii.func(*ii.args) == ii + + +def test_Idx_subs(): + i, a, b = symbols('i a b', integer=True) + assert Idx(i, a).subs(a, b) == Idx(i, b) + assert Idx(i, a).subs(i, b) == Idx(b, a) + + assert Idx(i).subs(i, 2) == Idx(2) + assert Idx(i, a).subs(a, 2) == Idx(i, 2) + assert Idx(i, (a, b)).subs(i, 2) == Idx(2, (a, b)) + + +def test_IndexedBase_sugar(): + i, j = symbols('i j', integer=True) + a = symbols('a') + A1 = Indexed(a, i, j) + A2 = IndexedBase(a) + assert A1 == A2[i, j] + assert A1 == A2[(i, j)] + assert A1 == A2[[i, j]] + assert A1 == A2[Tuple(i, j)] + assert all(a.is_Integer for a in A2[1, 0].args[1:]) + + +def test_IndexedBase_subs(): + i = symbols('i', integer=True) + a, b = symbols('a b') + A = IndexedBase(a) + B = IndexedBase(b) + assert A[i] == B[i].subs(b, a) + C = {1: 2} + assert C[1] == A[1].subs(A, C) + + +def test_IndexedBase_shape(): + i, j, m, n = symbols('i j m n', integer=True) + a = IndexedBase('a', shape=(m, m)) + b = IndexedBase('a', shape=(m, n)) + assert b.shape == Tuple(m, n) + assert a[i, j] != b[i, j] + assert a[i, j] == b[i, j].subs(n, m) + assert b.func(*b.args) == b + assert b[i, j].func(*b[i, j].args) == b[i, j] + raises(IndexException, lambda: b[i]) + raises(IndexException, lambda: b[i, i, j]) + F = IndexedBase("F", shape=m) + assert F.shape == Tuple(m) + assert F[i].subs(i, j) == F[j] + raises(IndexException, lambda: F[i, j]) + + +def test_IndexedBase_assumptions(): + i = Symbol('i', integer=True) + a = Symbol('a') + A = IndexedBase(a, positive=True) + for c in (A, A[i]): + assert c.is_real + assert c.is_complex + assert not c.is_imaginary + assert c.is_nonnegative + assert c.is_nonzero + assert c.is_commutative + assert log(exp(c)) == c + + assert A != IndexedBase(a) + assert A == IndexedBase(a, positive=True, real=True) + assert A[i] != Indexed(a, i) + + +def test_IndexedBase_assumptions_inheritance(): + I = Symbol('I', integer=True) + I_inherit = IndexedBase(I) + I_explicit = IndexedBase('I', integer=True) + + assert I_inherit.is_integer + assert I_explicit.is_integer + assert I_inherit.label.is_integer + assert I_explicit.label.is_integer + assert I_inherit == I_explicit + + +def test_issue_17652(): + """Regression test issue #17652. + + IndexedBase.label should not upcast subclasses of Symbol + """ + class SubClass(Symbol): + pass + + x = SubClass('X') + assert type(x) == SubClass + base = IndexedBase(x) + assert type(x) == SubClass + assert type(base.label) == SubClass + + +def test_Indexed_constructor(): + i, j = symbols('i j', integer=True) + A = Indexed('A', i, j) + assert A == Indexed(Symbol('A'), i, j) + assert A == Indexed(IndexedBase('A'), i, j) + raises(TypeError, lambda: Indexed(A, i, j)) + raises(IndexException, lambda: Indexed("A")) + assert A.free_symbols == {A, A.base.label, i, j} + + +def test_Indexed_func_args(): + i, j = symbols('i j', integer=True) + a = symbols('a') + A = Indexed(a, i, j) + assert A == A.func(*A.args) + + +def test_Indexed_subs(): + i, j, k = symbols('i j k', integer=True) + a, b = symbols('a b') + A = IndexedBase(a) + B = IndexedBase(b) + assert A[i, j] == B[i, j].subs(b, a) + assert A[i, j] == A[i, k].subs(k, j) + + +def test_Indexed_properties(): + i, j = symbols('i j', integer=True) + A = Indexed('A', i, j) + assert A.name == 'A[i, j]' + assert A.rank == 2 + assert A.indices == (i, j) + assert A.base == IndexedBase('A') + assert A.ranges == [None, None] + raises(IndexException, lambda: A.shape) + + n, m = symbols('n m', integer=True) + assert Indexed('A', Idx( + i, m), Idx(j, n)).ranges == [Tuple(0, m - 1), Tuple(0, n - 1)] + assert Indexed('A', Idx(i, m), Idx(j, n)).shape == Tuple(m, n) + raises(IndexException, lambda: Indexed("A", Idx(i, m), Idx(j)).shape) + + +def test_Indexed_shape_precedence(): + i, j = symbols('i j', integer=True) + o, p = symbols('o p', integer=True) + n, m = symbols('n m', integer=True) + a = IndexedBase('a', shape=(o, p)) + assert a.shape == Tuple(o, p) + assert Indexed( + a, Idx(i, m), Idx(j, n)).ranges == [Tuple(0, m - 1), Tuple(0, n - 1)] + assert Indexed(a, Idx(i, m), Idx(j, n)).shape == Tuple(o, p) + assert Indexed( + a, Idx(i, m), Idx(j)).ranges == [Tuple(0, m - 1), (None, None)] + assert Indexed(a, Idx(i, m), Idx(j)).shape == Tuple(o, p) + + +def test_complex_indices(): + i, j = symbols('i j', integer=True) + A = Indexed('A', i, i + j) + assert A.rank == 2 + assert A.indices == (i, i + j) + + +def test_not_interable(): + i, j = symbols('i j', integer=True) + A = Indexed('A', i, i + j) + assert not iterable(A) + + +def test_Indexed_coeff(): + N = Symbol('N', integer=True) + len_y = N + i = Idx('i', len_y-1) + y = IndexedBase('y', shape=(len_y,)) + a = (1/y[i+1]*y[i]).coeff(y[i]) + b = (y[i]/y[i+1]).coeff(y[i]) + assert a == b + + +def test_differentiation(): + from sympy.functions.special.tensor_functions import KroneckerDelta + i, j, k, l = symbols('i j k l', cls=Idx) + a = symbols('a') + m, n = symbols("m, n", integer=True, finite=True) + assert m.is_real + h, L = symbols('h L', cls=IndexedBase) + hi, hj = h[i], h[j] + + expr = hi + assert expr.diff(hj) == KroneckerDelta(i, j) + assert expr.diff(hi) == KroneckerDelta(i, i) + + expr = S(2) * hi + assert expr.diff(hj) == S(2) * KroneckerDelta(i, j) + assert expr.diff(hi) == S(2) * KroneckerDelta(i, i) + assert expr.diff(a) is S.Zero + + assert Sum(expr, (i, -oo, oo)).diff(hj) == Sum(2*KroneckerDelta(i, j), (i, -oo, oo)) + assert Sum(expr.diff(hj), (i, -oo, oo)) == Sum(2*KroneckerDelta(i, j), (i, -oo, oo)) + assert Sum(expr, (i, -oo, oo)).diff(hj).doit() == 2 + + assert Sum(expr.diff(hi), (i, -oo, oo)).doit() == Sum(2, (i, -oo, oo)).doit() + assert Sum(expr, (i, -oo, oo)).diff(hi).doit() is oo + + expr = a * hj * hj / S(2) + assert expr.diff(hi) == a * h[j] * KroneckerDelta(i, j) + assert expr.diff(a) == hj * hj / S(2) + assert expr.diff(a, 2) is S.Zero + + assert Sum(expr, (i, -oo, oo)).diff(hi) == Sum(a*KroneckerDelta(i, j)*h[j], (i, -oo, oo)) + assert Sum(expr.diff(hi), (i, -oo, oo)) == Sum(a*KroneckerDelta(i, j)*h[j], (i, -oo, oo)) + assert Sum(expr, (i, -oo, oo)).diff(hi).doit() == a*h[j] + + assert Sum(expr, (j, -oo, oo)).diff(hi) == Sum(a*KroneckerDelta(i, j)*h[j], (j, -oo, oo)) + assert Sum(expr.diff(hi), (j, -oo, oo)) == Sum(a*KroneckerDelta(i, j)*h[j], (j, -oo, oo)) + assert Sum(expr, (j, -oo, oo)).diff(hi).doit() == a*h[i] + + expr = a * sin(hj * hj) + assert expr.diff(hi) == 2*a*cos(hj * hj) * hj * KroneckerDelta(i, j) + assert expr.diff(hj) == 2*a*cos(hj * hj) * hj + + expr = a * L[i, j] * h[j] + assert expr.diff(hi) == a*L[i, j]*KroneckerDelta(i, j) + assert expr.diff(hj) == a*L[i, j] + assert expr.diff(L[i, j]) == a*h[j] + assert expr.diff(L[k, l]) == a*KroneckerDelta(i, k)*KroneckerDelta(j, l)*h[j] + assert expr.diff(L[i, l]) == a*KroneckerDelta(j, l)*h[j] + + assert Sum(expr, (j, -oo, oo)).diff(L[k, l]) == Sum(a * KroneckerDelta(i, k) * KroneckerDelta(j, l) * h[j], (j, -oo, oo)) + assert Sum(expr, (j, -oo, oo)).diff(L[k, l]).doit() == a * KroneckerDelta(i, k) * h[l] + + assert h[m].diff(h[m]) == 1 + assert h[m].diff(h[n]) == KroneckerDelta(m, n) + assert Sum(a*h[m], (m, -oo, oo)).diff(h[n]) == Sum(a*KroneckerDelta(m, n), (m, -oo, oo)) + assert Sum(a*h[m], (m, -oo, oo)).diff(h[n]).doit() == a + assert Sum(a*h[m], (n, -oo, oo)).diff(h[n]) == Sum(a*KroneckerDelta(m, n), (n, -oo, oo)) + assert Sum(a*h[m], (m, -oo, oo)).diff(h[m]).doit() == oo*a + + +def test_indexed_series(): + A = IndexedBase("A") + i = symbols("i", integer=True) + assert sin(A[i]).series(A[i]) == A[i] - A[i]**3/6 + A[i]**5/120 + Order(A[i]**6, A[i]) + + +def test_indexed_is_constant(): + A = IndexedBase("A") + i, j, k = symbols("i,j,k") + assert not A[i].is_constant() + assert A[i].is_constant(j) + assert not A[1+2*i, k].is_constant() + assert not A[1+2*i, k].is_constant(i) + assert A[1+2*i, k].is_constant(j) + assert not A[1+2*i, k].is_constant(k) + + +def test_issue_12533(): + d = IndexedBase('d') + assert IndexedBase(range(5)) == Range(0, 5, 1) + assert d[0].subs(Symbol("d"), range(5)) == 0 + assert d[0].subs(d, range(5)) == 0 + assert d[1].subs(d, range(5)) == 1 + assert Indexed(Range(5), 2) == 2 + + +def test_issue_12780(): + n = symbols("n") + i = Idx("i", (0, n)) + raises(TypeError, lambda: i.subs(n, 1.5)) + + +def test_issue_18604(): + m = symbols("m") + assert Idx("i", m).name == 'i' + assert Idx("i", m).lower == 0 + assert Idx("i", m).upper == m - 1 + m = symbols("m", real=False) + raises(TypeError, lambda: Idx("i", m)) + +def test_Subs_with_Indexed(): + A = IndexedBase("A") + i, j, k = symbols("i,j,k") + x, y, z = symbols("x,y,z") + f = Function("f") + + assert Subs(A[i], A[i], A[j]).diff(A[j]) == 1 + assert Subs(A[i], A[i], x).diff(A[i]) == 0 + assert Subs(A[i], A[i], x).diff(A[j]) == 0 + assert Subs(A[i], A[i], x).diff(x) == 1 + assert Subs(A[i], A[i], x).diff(y) == 0 + assert Subs(A[i], A[i], A[j]).diff(A[k]) == KroneckerDelta(j, k) + assert Subs(x, x, A[i]).diff(A[j]) == KroneckerDelta(i, j) + assert Subs(f(A[i]), A[i], x).diff(A[j]) == 0 + assert Subs(f(A[i]), A[i], A[k]).diff(A[j]) == Derivative(f(A[k]), A[k])*KroneckerDelta(j, k) + assert Subs(x, x, A[i]**2).diff(A[j]) == 2*KroneckerDelta(i, j)*A[i] + assert Subs(A[i], A[i], A[j]**2).diff(A[k]) == 2*KroneckerDelta(j, k)*A[j] + + assert Subs(A[i]*x, x, A[i]).diff(A[i]) == 2*A[i] + assert Subs(A[i]*x, x, A[i]).diff(A[j]) == 2*A[i]*KroneckerDelta(i, j) + assert Subs(A[i]*x, x, A[j]).diff(A[i]) == A[j] + A[i]*KroneckerDelta(i, j) + assert Subs(A[i]*x, x, A[j]).diff(A[j]) == A[i] + A[j]*KroneckerDelta(i, j) + assert Subs(A[i]*x, x, A[i]).diff(A[k]) == 2*A[i]*KroneckerDelta(i, k) + assert Subs(A[i]*x, x, A[j]).diff(A[k]) == KroneckerDelta(i, k)*A[j] + KroneckerDelta(j, k)*A[i] + + assert Subs(A[i]*x, A[i], x).diff(A[i]) == 0 + assert Subs(A[i]*x, A[i], x).diff(A[j]) == 0 + assert Subs(A[i]*x, A[j], x).diff(A[i]) == x + assert Subs(A[i]*x, A[j], x).diff(A[j]) == x*KroneckerDelta(i, j) + assert Subs(A[i]*x, A[i], x).diff(A[k]) == 0 + assert Subs(A[i]*x, A[j], x).diff(A[k]) == x*KroneckerDelta(i, k) + + +def test_complicated_derivative_with_Indexed(): + x, y = symbols("x,y", cls=IndexedBase) + sigma = symbols("sigma") + i, j, k = symbols("i,j,k") + m0,m1,m2,m3,m4,m5 = symbols("m0:6") + f = Function("f") + + expr = f((x[i] - y[i])**2/sigma) + _xi_1 = symbols("xi_1", cls=Dummy) + assert expr.diff(x[m0]).dummy_eq( + (x[i] - y[i])*KroneckerDelta(i, m0)*\ + 2*Subs( + Derivative(f(_xi_1), _xi_1), + (_xi_1,), + ((x[i] - y[i])**2/sigma,) + )/sigma + ) + assert expr.diff(x[m0]).diff(x[m1]).dummy_eq( + 2*KroneckerDelta(i, m0)*\ + KroneckerDelta(i, m1)*Subs( + Derivative(f(_xi_1), _xi_1), + (_xi_1,), + ((x[i] - y[i])**2/sigma,) + )/sigma + \ + 4*(x[i] - y[i])**2*KroneckerDelta(i, m0)*KroneckerDelta(i, m1)*\ + Subs( + Derivative(f(_xi_1), _xi_1, _xi_1), + (_xi_1,), + ((x[i] - y[i])**2/sigma,) + )/sigma**2 + ) + + +def test_IndexedBase_commutative(): + t = IndexedBase('t', commutative=False) + u = IndexedBase('u', commutative=False) + v = IndexedBase('v') + assert t[0]*v[0] == v[0]*t[0] + assert t[0]*u[0] != u[0]*t[0] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/tests/test_printing.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/tests/test_printing.py new file mode 100644 index 0000000000000000000000000000000000000000..9f3cf7f0591a7012c93354ab7b8d7e010def38bb --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/tests/test_printing.py @@ -0,0 +1,13 @@ +from sympy.tensor.tensor import TensorIndexType, tensor_indices, TensorHead +from sympy import I + +def test_printing_TensMul(): + R3 = TensorIndexType('R3', dim=3) + p, q = tensor_indices("p q", R3) + K = TensorHead("K", [R3]) + + assert repr(2*K(p)) == "2*K(p)" + assert repr(-K(p)) == "-K(p)" + assert repr(-2*K(p)*K(q)) == "-2*K(p)*K(q)" + assert repr(-I*K(p)) == "-I*K(p)" + assert repr(I*K(p)) == "I*K(p)" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/tests/test_tensor.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/tests/test_tensor.py new file mode 100644 index 0000000000000000000000000000000000000000..3113f5be9bcd32224f3525b5d831b6d7476c39e3 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/tensor/tests/test_tensor.py @@ -0,0 +1,2218 @@ +from sympy.concrete.summations import Sum +from sympy.core.function import expand +from sympy.core.numbers import Integer +from sympy.matrices.dense import (Matrix, eye) +from sympy.tensor.indexed import Indexed +from sympy.combinatorics import Permutation +from sympy.core import S, Rational, Symbol, Basic, Add, Wild, Function +from sympy.core.containers import Tuple +from sympy.core.symbol import symbols +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.integrals import integrate +from sympy.tensor.array import Array +from sympy.tensor.tensor import TensorIndexType, tensor_indices, TensorSymmetry, \ + get_symmetric_group_sgs, TensorIndex, tensor_mul, TensAdd, \ + riemann_cyclic_replace, riemann_cyclic, TensMul, tensor_heads, \ + TensorManager, TensExpr, TensorHead, canon_bp, \ + tensorhead, tensorsymmetry, TensorType, substitute_indices, \ + WildTensorIndex, WildTensorHead, _WildTensExpr +from sympy.testing.pytest import raises, XFAIL, warns_deprecated_sympy +from sympy.matrices import diag + +def _is_equal(arg1, arg2): + if isinstance(arg1, TensExpr): + return arg1.equals(arg2) + elif isinstance(arg2, TensExpr): + return arg2.equals(arg1) + return arg1 == arg2 + + +#################### Tests from tensor_can.py ####################### +def test_canonicalize_no_slot_sym(): + # A_d0 * B^d0; T_c = A^d0*B_d0 + Lorentz = TensorIndexType('Lorentz', dummy_name='L') + a, b, d0, d1 = tensor_indices('a,b,d0,d1', Lorentz) + A, B = tensor_heads('A,B', [Lorentz], TensorSymmetry.no_symmetry(1)) + t = A(-d0)*B(d0) + tc = t.canon_bp() + assert str(tc) == 'A(L_0)*B(-L_0)' + + # A^a * B^b; T_c = T + t = A(a)*B(b) + tc = t.canon_bp() + assert tc == t + # B^b * A^a + t1 = B(b)*A(a) + tc = t1.canon_bp() + assert str(tc) == 'A(a)*B(b)' + + # A symmetric + # A^{b}_{d0}*A^{d0, a}; T_c = A^{a d0}*A{b}_{d0} + A = TensorHead('A', [Lorentz]*2, TensorSymmetry.fully_symmetric(2)) + t = A(b, -d0)*A(d0, a) + tc = t.canon_bp() + assert str(tc) == 'A(a, L_0)*A(b, -L_0)' + + # A^{d1}_{d0}*B^d0*C_d1 + # T_c = A^{d0 d1}*B_d0*C_d1 + B, C = tensor_heads('B,C', [Lorentz], TensorSymmetry.no_symmetry(1)) + t = A(d1, -d0)*B(d0)*C(-d1) + tc = t.canon_bp() + assert str(tc) == 'A(L_0, L_1)*B(-L_0)*C(-L_1)' + + # A without symmetry + # A^{d1}_{d0}*B^d0*C_d1 ord=[d0,-d0,d1,-d1]; g = [2,1,0,3,4,5] + # T_c = A^{d0 d1}*B_d1*C_d0; can = [0,2,3,1,4,5] + A = TensorHead('A', [Lorentz]*2, TensorSymmetry.no_symmetry(2)) + t = A(d1, -d0)*B(d0)*C(-d1) + tc = t.canon_bp() + assert str(tc) == 'A(L_0, L_1)*B(-L_1)*C(-L_0)' + + # A, B without symmetry + # A^{d1}_{d0}*B_{d1}^{d0} + # T_c = A^{d0 d1}*B_{d0 d1} + B = TensorHead('B', [Lorentz]*2, TensorSymmetry.no_symmetry(2)) + t = A(d1, -d0)*B(-d1, d0) + tc = t.canon_bp() + assert str(tc) == 'A(L_0, L_1)*B(-L_0, -L_1)' + # A_{d0}^{d1}*B_{d1}^{d0} + # T_c = A^{d0 d1}*B_{d1 d0} + t = A(-d0, d1)*B(-d1, d0) + tc = t.canon_bp() + assert str(tc) == 'A(L_0, L_1)*B(-L_1, -L_0)' + + # A, B, C without symmetry + # A^{d1 d0}*B_{a d0}*C_{d1 b} + # T_c=A^{d0 d1}*B_{a d1}*C_{d0 b} + C = TensorHead('C', [Lorentz]*2, TensorSymmetry.no_symmetry(2)) + t = A(d1, d0)*B(-a, -d0)*C(-d1, -b) + tc = t.canon_bp() + assert str(tc) == 'A(L_0, L_1)*B(-a, -L_1)*C(-L_0, -b)' + + # A symmetric, B and C without symmetry + # A^{d1 d0}*B_{a d0}*C_{d1 b} + # T_c = A^{d0 d1}*B_{a d0}*C_{d1 b} + A = TensorHead('A', [Lorentz]*2, TensorSymmetry.fully_symmetric(2)) + t = A(d1, d0)*B(-a, -d0)*C(-d1, -b) + tc = t.canon_bp() + assert str(tc) == 'A(L_0, L_1)*B(-a, -L_0)*C(-L_1, -b)' + + # A and C symmetric, B without symmetry + # A^{d1 d0}*B_{a d0}*C_{d1 b} ord=[a,b,d0,-d0,d1,-d1] + # T_c = A^{d0 d1}*B_{a d0}*C_{b d1} + C = TensorHead('C', [Lorentz]*2, TensorSymmetry.fully_symmetric(2)) + t = A(d1, d0)*B(-a, -d0)*C(-d1, -b) + tc = t.canon_bp() + assert str(tc) == 'A(L_0, L_1)*B(-a, -L_0)*C(-b, -L_1)' + +def test_canonicalize_no_dummies(): + Lorentz = TensorIndexType('Lorentz', dummy_name='L') + a, b, c, d = tensor_indices('a, b, c, d', Lorentz) + + # A commuting + # A^c A^b A^a + # T_c = A^a A^b A^c + A = TensorHead('A', [Lorentz], TensorSymmetry.no_symmetry(1)) + t = A(c)*A(b)*A(a) + tc = t.canon_bp() + assert str(tc) == 'A(a)*A(b)*A(c)' + + # A anticommuting + # A^c A^b A^a + # T_c = -A^a A^b A^c + A = TensorHead('A', [Lorentz], TensorSymmetry.no_symmetry(1), 1) + t = A(c)*A(b)*A(a) + tc = t.canon_bp() + assert str(tc) == '-A(a)*A(b)*A(c)' + + # A commuting and symmetric + # A^{b,d}*A^{c,a} + # T_c = A^{a c}*A^{b d} + A = TensorHead('A', [Lorentz]*2, TensorSymmetry.fully_symmetric(2)) + t = A(b, d)*A(c, a) + tc = t.canon_bp() + assert str(tc) == 'A(a, c)*A(b, d)' + + # A anticommuting and symmetric + # A^{b,d}*A^{c,a} + # T_c = -A^{a c}*A^{b d} + A = TensorHead('A', [Lorentz]*2, TensorSymmetry.fully_symmetric(2), 1) + t = A(b, d)*A(c, a) + tc = t.canon_bp() + assert str(tc) == '-A(a, c)*A(b, d)' + + # A^{c,a}*A^{b,d} + # T_c = A^{a c}*A^{b d} + t = A(c, a)*A(b, d) + tc = t.canon_bp() + assert str(tc) == 'A(a, c)*A(b, d)' + +def test_tensorhead_construction_without_symmetry(): + L = TensorIndexType('Lorentz') + A1 = TensorHead('A', [L, L]) + A2 = TensorHead('A', [L, L], TensorSymmetry.no_symmetry(2)) + assert A1 == A2 + A3 = TensorHead('A', [L, L], TensorSymmetry.fully_symmetric(2)) # Symmetric + assert A1 != A3 + +def test_no_metric_symmetry(): + # no metric symmetry; A no symmetry + # A^d1_d0 * A^d0_d1 + # T_c = A^d0_d1 * A^d1_d0 + Lorentz = TensorIndexType('Lorentz', dummy_name='L', metric_symmetry=0) + d0, d1, d2, d3 = tensor_indices('d:4', Lorentz) + A = TensorHead('A', [Lorentz]*2, TensorSymmetry.no_symmetry(2)) + t = A(d1, -d0)*A(d0, -d1) + tc = t.canon_bp() + assert str(tc) == 'A(L_0, -L_1)*A(L_1, -L_0)' + + # A^d1_d2 * A^d0_d3 * A^d2_d1 * A^d3_d0 + # T_c = A^d0_d1 * A^d1_d0 * A^d2_d3 * A^d3_d2 + t = A(d1, -d2)*A(d0, -d3)*A(d2, -d1)*A(d3, -d0) + tc = t.canon_bp() + assert str(tc) == 'A(L_0, -L_1)*A(L_1, -L_0)*A(L_2, -L_3)*A(L_3, -L_2)' + + # A^d0_d2 * A^d1_d3 * A^d3_d0 * A^d2_d1 + # T_c = A^d0_d1 * A^d1_d2 * A^d2_d3 * A^d3_d0 + t = A(d0, -d1)*A(d1, -d2)*A(d2, -d3)*A(d3, -d0) + tc = t.canon_bp() + assert str(tc) == 'A(L_0, -L_1)*A(L_1, -L_2)*A(L_2, -L_3)*A(L_3, -L_0)' + +def test_canonicalize1(): + Lorentz = TensorIndexType('Lorentz', dummy_name='L') + a, a0, a1, a2, a3, b, d0, d1, d2, d3 = \ + tensor_indices('a,a0,a1,a2,a3,b,d0,d1,d2,d3', Lorentz) + + # A_d0*A^d0; ord = [d0,-d0] + # T_c = A^d0*A_d0 + A = TensorHead('A', [Lorentz], TensorSymmetry.no_symmetry(1)) + t = A(-d0)*A(d0) + tc = t.canon_bp() + assert str(tc) == 'A(L_0)*A(-L_0)' + + # A commuting + # A_d0*A_d1*A_d2*A^d2*A^d1*A^d0 + # T_c = A^d0*A_d0*A^d1*A_d1*A^d2*A_d2 + t = A(-d0)*A(-d1)*A(-d2)*A(d2)*A(d1)*A(d0) + tc = t.canon_bp() + assert str(tc) == 'A(L_0)*A(-L_0)*A(L_1)*A(-L_1)*A(L_2)*A(-L_2)' + + # A anticommuting + # A_d0*A_d1*A_d2*A^d2*A^d1*A^d0 + # T_c 0 + A = TensorHead('A', [Lorentz], TensorSymmetry.no_symmetry(1), 1) + t = A(-d0)*A(-d1)*A(-d2)*A(d2)*A(d1)*A(d0) + tc = t.canon_bp() + assert tc == 0 + + # A commuting symmetric + # A^{d0 b}*A^a_d1*A^d1_d0 + # T_c = A^{a d0}*A^{b d1}*A_{d0 d1} + A = TensorHead('A', [Lorentz]*2, TensorSymmetry.fully_symmetric(2)) + t = A(d0, b)*A(a, -d1)*A(d1, -d0) + tc = t.canon_bp() + assert str(tc) == 'A(a, L_0)*A(b, L_1)*A(-L_0, -L_1)' + + # A, B commuting symmetric + # A^{d0 b}*A^d1_d0*B^a_d1 + # T_c = A^{b d0}*A_d0^d1*B^a_d1 + B = TensorHead('B', [Lorentz]*2, TensorSymmetry.fully_symmetric(2)) + t = A(d0, b)*A(d1, -d0)*B(a, -d1) + tc = t.canon_bp() + assert str(tc) == 'A(b, L_0)*A(-L_0, L_1)*B(a, -L_1)' + + # A commuting symmetric + # A^{d1 d0 b}*A^{a}_{d1 d0}; ord=[a,b, d0,-d0,d1,-d1] + # T_c = A^{a d0 d1}*A^{b}_{d0 d1} + A = TensorHead('A', [Lorentz]*3, TensorSymmetry.fully_symmetric(3)) + t = A(d1, d0, b)*A(a, -d1, -d0) + tc = t.canon_bp() + assert str(tc) == 'A(a, L_0, L_1)*A(b, -L_0, -L_1)' + + # A^{d3 d0 d2}*A^a0_{d1 d2}*A^d1_d3^a1*A^{a2 a3}_d0 + # T_c = A^{a0 d0 d1}*A^a1_d0^d2*A^{a2 a3 d3}*A_{d1 d2 d3} + t = A(d3, d0, d2)*A(a0, -d1, -d2)*A(d1, -d3, a1)*A(a2, a3, -d0) + tc = t.canon_bp() + assert str(tc) == 'A(a0, L_0, L_1)*A(a1, -L_0, L_2)*A(a2, a3, L_3)*A(-L_1, -L_2, -L_3)' + + # A commuting symmetric, B antisymmetric + # A^{d0 d1 d2} * A_{d2 d3 d1} * B_d0^d3 + # in this esxample and in the next three, + # renaming dummy indices and using symmetry of A, + # T = A^{d0 d1 d2} * A_{d0 d1 d3} * B_d2^d3 + # can = 0 + A = TensorHead('A', [Lorentz]*3, TensorSymmetry.fully_symmetric(3)) + B = TensorHead('B', [Lorentz]*2, TensorSymmetry.fully_symmetric(-2)) + t = A(d0, d1, d2)*A(-d2, -d3, -d1)*B(-d0, d3) + tc = t.canon_bp() + assert tc == 0 + + # A anticommuting symmetric, B antisymmetric + # A^{d0 d1 d2} * A_{d2 d3 d1} * B_d0^d3 + # T_c = A^{d0 d1 d2} * A_{d0 d1}^d3 * B_{d2 d3} + A = TensorHead('A', [Lorentz]*3, TensorSymmetry.fully_symmetric(3), 1) + B = TensorHead('B', [Lorentz]*2, TensorSymmetry.fully_symmetric(-2)) + t = A(d0, d1, d2)*A(-d2, -d3, -d1)*B(-d0, d3) + tc = t.canon_bp() + assert str(tc) == 'A(L_0, L_1, L_2)*A(-L_0, -L_1, L_3)*B(-L_2, -L_3)' + + # A anticommuting symmetric, B antisymmetric commuting, antisymmetric metric + # A^{d0 d1 d2} * A_{d2 d3 d1} * B_d0^d3 + # T_c = -A^{d0 d1 d2} * A_{d0 d1}^d3 * B_{d2 d3} + Spinor = TensorIndexType('Spinor', dummy_name='S', metric_symmetry=-1) + a, a0, a1, a2, a3, b, d0, d1, d2, d3 = \ + tensor_indices('a,a0,a1,a2,a3,b,d0,d1,d2,d3', Spinor) + A = TensorHead('A', [Spinor]*3, TensorSymmetry.fully_symmetric(3), 1) + B = TensorHead('B', [Spinor]*2, TensorSymmetry.fully_symmetric(-2)) + t = A(d0, d1, d2)*A(-d2, -d3, -d1)*B(-d0, d3) + tc = t.canon_bp() + assert str(tc) == '-A(S_0, S_1, S_2)*A(-S_0, -S_1, S_3)*B(-S_2, -S_3)' + + # A anticommuting symmetric, B antisymmetric anticommuting, + # no metric symmetry + # A^{d0 d1 d2} * A_{d2 d3 d1} * B_d0^d3 + # T_c = A^{d0 d1 d2} * A_{d0 d1 d3} * B_d2^d3 + Mat = TensorIndexType('Mat', metric_symmetry=0, dummy_name='M') + a, a0, a1, a2, a3, b, d0, d1, d2, d3 = \ + tensor_indices('a,a0,a1,a2,a3,b,d0,d1,d2,d3', Mat) + A = TensorHead('A', [Mat]*3, TensorSymmetry.fully_symmetric(3), 1) + B = TensorHead('B', [Mat]*2, TensorSymmetry.fully_symmetric(-2)) + t = A(d0, d1, d2)*A(-d2, -d3, -d1)*B(-d0, d3) + tc = t.canon_bp() + assert str(tc) == 'A(M_0, M_1, M_2)*A(-M_0, -M_1, -M_3)*B(-M_2, M_3)' + + # Gamma anticommuting + # Gamma_{mu nu} * gamma^rho * Gamma^{nu mu alpha} + # T_c = -Gamma^{mu nu} * gamma^rho * Gamma_{alpha mu nu} + alpha, beta, gamma, mu, nu, rho = \ + tensor_indices('alpha,beta,gamma,mu,nu,rho', Lorentz) + Gamma = TensorHead('Gamma', [Lorentz], + TensorSymmetry.fully_symmetric(1), 2) + Gamma2 = TensorHead('Gamma', [Lorentz]*2, + TensorSymmetry.fully_symmetric(-2), 2) + Gamma3 = TensorHead('Gamma', [Lorentz]*3, + TensorSymmetry.fully_symmetric(-3), 2) + t = Gamma2(-mu, -nu)*Gamma(rho)*Gamma3(nu, mu, alpha) + tc = t.canon_bp() + assert str(tc) == '-Gamma(L_0, L_1)*Gamma(rho)*Gamma(alpha, -L_0, -L_1)' + + # Gamma_{mu nu} * Gamma^{gamma beta} * gamma_rho * Gamma^{nu mu alpha} + # T_c = Gamma^{mu nu} * Gamma^{beta gamma} * gamma_rho * Gamma^alpha_{mu nu} + t = Gamma2(mu, nu)*Gamma2(beta, gamma)*Gamma(-rho)*Gamma3(alpha, -mu, -nu) + tc = t.canon_bp() + assert str(tc) == 'Gamma(L_0, L_1)*Gamma(beta, gamma)*Gamma(-rho)*Gamma(alpha, -L_0, -L_1)' + + # f^a_{b,c} antisymmetric in b,c; A_mu^a no symmetry + # f^c_{d a} * f_{c e b} * A_mu^d * A_nu^a * A^{nu e} * A^{mu b} + # g = [8,11,5, 9,13,7, 1,10, 3,4, 2,12, 0,6, 14,15] + # T_c = -f^{a b c} * f_a^{d e} * A^mu_b * A_{mu d} * A^nu_c * A_{nu e} + Flavor = TensorIndexType('Flavor', dummy_name='F') + a, b, c, d, e, ff = tensor_indices('a,b,c,d,e,f', Flavor) + mu, nu = tensor_indices('mu,nu', Lorentz) + f = TensorHead('f', [Flavor]*3, TensorSymmetry.direct_product(1, -2)) + A = TensorHead('A', [Lorentz, Flavor], TensorSymmetry.no_symmetry(2)) + t = f(c, -d, -a)*f(-c, -e, -b)*A(-mu, d)*A(-nu, a)*A(nu, e)*A(mu, b) + tc = t.canon_bp() + assert str(tc) == '-f(F_0, F_1, F_2)*f(-F_0, F_3, F_4)*A(L_0, -F_1)*A(-L_0, -F_3)*A(L_1, -F_2)*A(-L_1, -F_4)' + + +def test_bug_correction_tensor_indices(): + # to make sure that tensor_indices does not return a list if creating + # only one index: + A = TensorIndexType("A") + i = tensor_indices('i', A) + assert not isinstance(i, (tuple, list)) + assert isinstance(i, TensorIndex) + + +def test_riemann_invariants(): + Lorentz = TensorIndexType('Lorentz', dummy_name='L') + d0, d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11 = \ + tensor_indices('d0:12', Lorentz) + # R^{d0 d1}_{d1 d0}; ord = [d0,-d0,d1,-d1] + # T_c = -R^{d0 d1}_{d0 d1} + R = TensorHead('R', [Lorentz]*4, TensorSymmetry.riemann()) + t = R(d0, d1, -d1, -d0) + tc = t.canon_bp() + assert str(tc) == '-R(L_0, L_1, -L_0, -L_1)' + + # R_d11^d1_d0^d5 * R^{d6 d4 d0}_d5 * R_{d7 d2 d8 d9} * + # R_{d10 d3 d6 d4} * R^{d2 d7 d11}_d1 * R^{d8 d9 d3 d10} + # can = [0,2,4,6, 1,3,8,10, 5,7,12,14, 9,11,16,18, 13,15,20,22, + # 17,19,21>> from sympy.tensor.tensor import TensorIndexType, TensorHead + >>> from sympy.tensor.toperators import PartialDerivative + >>> from sympy import symbols + >>> L = TensorIndexType("L") + >>> A = TensorHead("A", [L]) + >>> B = TensorHead("B", [L]) + >>> i, j, k = symbols("i j k") + + >>> expr = PartialDerivative(A(i), A(j)) + >>> expr + PartialDerivative(A(i), A(j)) + + The ``PartialDerivative`` object behaves like a tensorial expression: + + >>> expr.get_indices() + [i, -j] + + Notice that the deriving variables have opposite valence than the + printed one: ``A(j)`` is printed as covariant, but the index of the + derivative is actually contravariant, i.e. ``-j``. + + Indices can be contracted: + + >>> expr = PartialDerivative(A(i), A(i)) + >>> expr + PartialDerivative(A(L_0), A(L_0)) + >>> expr.get_indices() + [L_0, -L_0] + + The method ``.get_indices()`` always returns all indices (even the + contracted ones). If only uncontracted indices are needed, call + ``.get_free_indices()``: + + >>> expr.get_free_indices() + [] + + Nested partial derivatives are flattened: + + >>> expr = PartialDerivative(PartialDerivative(A(i), A(j)), A(k)) + >>> expr + PartialDerivative(A(i), A(j), A(k)) + >>> expr.get_indices() + [i, -j, -k] + + Replace a derivative with array values: + + >>> from sympy.abc import x, y + >>> from sympy import sin, log + >>> compA = [sin(x), log(x)*y**3] + >>> compB = [x, y] + >>> expr = PartialDerivative(A(i), B(j)) + >>> expr.replace_with_arrays({A(i): compA, B(i): compB}) + [[cos(x), 0], [y**3/x, 3*y**2*log(x)]] + + The returned array is indexed by `(i, -j)`. + + Be careful that other SymPy modules put the indices of the deriving + variables before the indices of the derivand in the derivative result. + For example: + + >>> expr.get_free_indices() + [i, -j] + + >>> from sympy import Matrix, Array + >>> Matrix(compA).diff(Matrix(compB)).reshape(2, 2) + [[cos(x), y**3/x], [0, 3*y**2*log(x)]] + >>> Array(compA).diff(Array(compB)) + [[cos(x), y**3/x], [0, 3*y**2*log(x)]] + + These are the transpose of the result of ``PartialDerivative``, + as the matrix and the array modules put the index `-j` before `i` in the + derivative result. An array read with index order `(-j, i)` is indeed the + transpose of the same array read with index order `(i, -j)`. By specifying + the index order to ``.replace_with_arrays`` one can get a compatible + expression: + + >>> expr.replace_with_arrays({A(i): compA, B(i): compB}, [-j, i]) + [[cos(x), y**3/x], [0, 3*y**2*log(x)]] + """ + + def __new__(cls, expr, *variables): + + # Flatten: + if isinstance(expr, PartialDerivative): + variables = expr.variables + variables + expr = expr.expr + + args, indices, free, dum = cls._contract_indices_for_derivative( + S(expr), variables) + + obj = TensExpr.__new__(cls, *args) + + obj._indices = indices + obj._free = free + obj._dum = dum + return obj + + @property + def coeff(self): + return S.One + + @property + def nocoeff(self): + return self + + @classmethod + def _contract_indices_for_derivative(cls, expr, variables): + variables_opposite_valence = [] + + for i in variables: + if isinstance(i, Tensor): + i_free_indices = i.get_free_indices() + variables_opposite_valence.append( + i.xreplace({k: -k for k in i_free_indices})) + elif isinstance(i, Symbol): + variables_opposite_valence.append(i) + + args, indices, free, dum = TensMul._tensMul_contract_indices( + [expr] + variables_opposite_valence, replace_indices=True) + + for i in range(1, len(args)): + args_i = args[i] + if isinstance(args_i, Tensor): + i_indices = args[i].get_free_indices() + args[i] = args[i].xreplace({k: -k for k in i_indices}) + + return args, indices, free, dum + + def doit(self, **hints): + args, indices, free, dum = self._contract_indices_for_derivative(self.expr, self.variables) + + obj = self.func(*args) + obj._indices = indices + obj._free = free + obj._dum = dum + + return obj + + def _expand_partial_derivative(self): + args, indices, free, dum = self._contract_indices_for_derivative(self.expr, self.variables) + + obj = self.func(*args) + obj._indices = indices + obj._free = free + obj._dum = dum + + result = obj + + if not args[0].free_symbols: + return S.Zero + elif isinstance(obj.expr, TensAdd): + # take care of sums of multi PDs + result = obj.expr.func(*[ + self.func(a, *obj.variables)._expand_partial_derivative() + for a in result.expr.args]) + elif isinstance(obj.expr, TensMul): + # take care of products of multi PDs + if len(obj.variables) == 1: + # derivative with respect to single variable + terms = [] + mulargs = list(obj.expr.args) + for ind in range(len(mulargs)): + if not isinstance(sympify(mulargs[ind]), Number): + # a number coefficient is not considered for + # expansion of PartialDerivative + d = self.func(mulargs[ind], *obj.variables)._expand_partial_derivative() + terms.append(TensMul(*(mulargs[:ind] + + [d] + + mulargs[(ind + 1):]))) + result = TensAdd.fromiter(terms) + else: + # derivative with respect to multiple variables + # decompose: + # partial(expr, (u, v)) + # = partial(partial(expr, u).doit(), v).doit() + result = obj.expr # init with expr + for v in obj.variables: + result = self.func(result, v)._expand_partial_derivative() + # then throw PD on it + + return result + + def _perform_derivative(self): + result = self.expr + for v in self.variables: + if isinstance(result, TensExpr): + result = result._eval_partial_derivative(v) + else: + if v._diff_wrt: + result = result._eval_derivative(v) + else: + result = S.Zero + return result + + def get_indices(self): + return self._indices + + def get_free_indices(self): + free = sorted(self._free, key=lambda x: x[1]) + return [i[0] for i in free] + + def _replace_indices(self, repl): + expr = self.expr.xreplace(repl) + mirrored = {-k: -v for k, v in repl.items()} + variables = [i.xreplace(mirrored) for i in self.variables] + return self.func(expr, *variables) + + @property + def expr(self): + return self.args[0] + + @property + def variables(self): + return self.args[1:] + + def _extract_data(self, replacement_dict): + from .array import derive_by_array, tensorcontraction + indices, array = self.expr._extract_data(replacement_dict) + for variable in self.variables: + var_indices, var_array = variable._extract_data(replacement_dict) + var_indices = [-i for i in var_indices] + coeff_array, var_array = zip(*[i.as_coeff_Mul() for i in var_array]) + dim_before = len(array.shape) + array = derive_by_array(array, var_array) + dim_after = len(array.shape) + dim_increase = dim_after - dim_before + array = permutedims(array, [i + dim_increase for i in range(dim_before)] + list(range(dim_increase))) + array = array.as_mutable() + varindex = var_indices[0] + # Remove coefficients of base vector: + coeff_index = [0] + [slice(None) for i in range(len(indices))] + for i, coeff in enumerate(coeff_array): + coeff_index[0] = i + array[tuple(coeff_index)] /= coeff + if -varindex in indices: + pos = indices.index(-varindex) + array = tensorcontraction(array, (0, pos+1)) + indices.pop(pos) + else: + indices.append(varindex) + return indices, array diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..aa66402955e7d5d2b27cccc6627b42d33f4cd855 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/__init__.py @@ -0,0 +1,10 @@ +"""This module contains code for running the tests in SymPy.""" + + +from .runtests import doctest +from .runtests_pytest import test + + +__all__ = [ + 'test', 'doctest', +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/matrices.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/matrices.py new file mode 100644 index 0000000000000000000000000000000000000000..236a384366df7f69d0d92f43f7e007e95c12388c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/matrices.py @@ -0,0 +1,8 @@ +def allclose(A, B, rtol=1e-05, atol=1e-08): + if len(A) != len(B): + return False + + for x, y in zip(A, B): + if abs(x-y) > atol + rtol * max(abs(x), abs(y)): + return False + return True diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/pytest.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/pytest.py new file mode 100644 index 0000000000000000000000000000000000000000..498515a2d3c0a167a5f7067753736898cfa64799 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/pytest.py @@ -0,0 +1,392 @@ +"""py.test hacks to support XFAIL/XPASS""" + +import platform +import sys +import re +import functools +import os +import contextlib +import warnings +import inspect +import pathlib +from typing import Any, Callable + +from sympy.utilities.exceptions import SymPyDeprecationWarning +# Imported here for backwards compatibility. Note: do not import this from +# here in library code (importing sympy.pytest in library code will break the +# pytest integration). +from sympy.utilities.exceptions import ignore_warnings # noqa:F401 + +ON_CI = os.getenv('CI', None) == "true" + +try: + import pytest + USE_PYTEST = getattr(sys, '_running_pytest', False) +except ImportError: + USE_PYTEST = False + +IS_WASM: bool = sys.platform == 'emscripten' or platform.machine() in ["wasm32", "wasm64"] + +raises: Callable[[Any, Any], Any] +XFAIL: Callable[[Any], Any] +skip: Callable[[Any], Any] +SKIP: Callable[[Any], Any] +slow: Callable[[Any], Any] +tooslow: Callable[[Any], Any] +nocache_fail: Callable[[Any], Any] + + +if USE_PYTEST: + raises = pytest.raises + skip = pytest.skip + XFAIL = pytest.mark.xfail + SKIP = pytest.mark.skip + slow = pytest.mark.slow + tooslow = pytest.mark.tooslow + nocache_fail = pytest.mark.nocache_fail + from _pytest.outcomes import Failed + +else: + # Not using pytest so define the things that would have been imported from + # there. + + # _pytest._code.code.ExceptionInfo + class ExceptionInfo: + def __init__(self, value): + self.value = value + + def __repr__(self): + return "".format(self.value) + + + def raises(expectedException, code=None): + """ + Tests that ``code`` raises the exception ``expectedException``. + + ``code`` may be a callable, such as a lambda expression or function + name. + + If ``code`` is not given or None, ``raises`` will return a context + manager for use in ``with`` statements; the code to execute then + comes from the scope of the ``with``. + + ``raises()`` does nothing if the callable raises the expected exception, + otherwise it raises an AssertionError. + + Examples + ======== + + >>> from sympy.testing.pytest import raises + + >>> raises(ZeroDivisionError, lambda: 1/0) + + >>> raises(ZeroDivisionError, lambda: 1/2) + Traceback (most recent call last): + ... + Failed: DID NOT RAISE + + >>> with raises(ZeroDivisionError): + ... n = 1/0 + >>> with raises(ZeroDivisionError): + ... n = 1/2 + Traceback (most recent call last): + ... + Failed: DID NOT RAISE + + Note that you cannot test multiple statements via + ``with raises``: + + >>> with raises(ZeroDivisionError): + ... n = 1/0 # will execute and raise, aborting the ``with`` + ... n = 9999/0 # never executed + + This is just what ``with`` is supposed to do: abort the + contained statement sequence at the first exception and let + the context manager deal with the exception. + + To test multiple statements, you'll need a separate ``with`` + for each: + + >>> with raises(ZeroDivisionError): + ... n = 1/0 # will execute and raise + >>> with raises(ZeroDivisionError): + ... n = 9999/0 # will also execute and raise + + """ + if code is None: + return RaisesContext(expectedException) + elif callable(code): + try: + code() + except expectedException as e: + return ExceptionInfo(e) + raise Failed("DID NOT RAISE") + elif isinstance(code, str): + raise TypeError( + '\'raises(xxx, "code")\' has been phased out; ' + 'change \'raises(xxx, "expression")\' ' + 'to \'raises(xxx, lambda: expression)\', ' + '\'raises(xxx, "statement")\' ' + 'to \'with raises(xxx): statement\'') + else: + raise TypeError( + 'raises() expects a callable for the 2nd argument.') + + class RaisesContext: + def __init__(self, expectedException): + self.expectedException = expectedException + + def __enter__(self): + return None + + def __exit__(self, exc_type, exc_value, traceback): + if exc_type is None: + raise Failed("DID NOT RAISE") + return issubclass(exc_type, self.expectedException) + + class XFail(Exception): + pass + + class XPass(Exception): + pass + + class Skipped(Exception): + pass + + class Failed(Exception): # type: ignore + pass + + def XFAIL(func): + def wrapper(): + try: + func() + except Exception as e: + message = str(e) + if message != "Timeout": + raise XFail(func.__name__) + else: + raise Skipped("Timeout") + raise XPass(func.__name__) + + wrapper = functools.update_wrapper(wrapper, func) + return wrapper + + def skip(str): + raise Skipped(str) + + def SKIP(reason): + """Similar to ``skip()``, but this is a decorator. """ + def wrapper(func): + def func_wrapper(): + raise Skipped(reason) + + func_wrapper = functools.update_wrapper(func_wrapper, func) + return func_wrapper + + return wrapper + + def slow(func): + func._slow = True + + def func_wrapper(): + func() + + func_wrapper = functools.update_wrapper(func_wrapper, func) + func_wrapper.__wrapped__ = func + return func_wrapper + + def tooslow(func): + func._slow = True + func._tooslow = True + + def func_wrapper(): + skip("Too slow") + + func_wrapper = functools.update_wrapper(func_wrapper, func) + func_wrapper.__wrapped__ = func + return func_wrapper + + def nocache_fail(func): + "Dummy decorator for marking tests that fail when cache is disabled" + return func + +@contextlib.contextmanager +def warns(warningcls, *, match='', test_stacklevel=True): + ''' + Like raises but tests that warnings are emitted. + + >>> from sympy.testing.pytest import warns + >>> import warnings + + >>> with warns(UserWarning): + ... warnings.warn('deprecated', UserWarning, stacklevel=2) + + >>> with warns(UserWarning): + ... pass + Traceback (most recent call last): + ... + Failed: DID NOT WARN. No warnings of type UserWarning\ + was emitted. The list of emitted warnings is: []. + + ``test_stacklevel`` makes it check that the ``stacklevel`` parameter to + ``warn()`` is set so that the warning shows the user line of code (the + code under the warns() context manager). Set this to False if this is + ambiguous or if the context manager does not test the direct user code + that emits the warning. + + If the warning is a ``SymPyDeprecationWarning``, this additionally tests + that the ``active_deprecations_target`` is a real target in the + ``active-deprecations.md`` file. + + ''' + # Absorbs all warnings in warnrec + with warnings.catch_warnings(record=True) as warnrec: + # Any warning other than the one we are looking for is an error + warnings.simplefilter("error") + warnings.filterwarnings("always", category=warningcls) + # Now run the test + yield warnrec + + # Raise if expected warning not found + if not any(issubclass(w.category, warningcls) for w in warnrec): + msg = ('Failed: DID NOT WARN.' + ' No warnings of type %s was emitted.' + ' The list of emitted warnings is: %s.' + ) % (warningcls, [w.message for w in warnrec]) + raise Failed(msg) + + # We don't include the match in the filter above because it would then + # fall to the error filter, so we instead manually check that it matches + # here + for w in warnrec: + # Should always be true due to the filters above + assert issubclass(w.category, warningcls) + if not re.compile(match, re.IGNORECASE).match(str(w.message)): + raise Failed(f"Failed: WRONG MESSAGE. A warning with of the correct category ({warningcls.__name__}) was issued, but it did not match the given match regex ({match!r})") + + if test_stacklevel: + for f in inspect.stack(): + thisfile = f.filename + file = os.path.split(thisfile)[1] + if file.startswith('test_'): + break + elif file == 'doctest.py': + # skip the stacklevel testing in the doctests of this + # function + return + else: + raise RuntimeError("Could not find the file for the given warning to test the stacklevel") + for w in warnrec: + if w.filename != thisfile: + msg = f'''\ +Failed: Warning has the wrong stacklevel. The warning stacklevel needs to be +set so that the line of code shown in the warning message is user code that +calls the deprecated code (the current stacklevel is showing code from +{w.filename} (line {w.lineno}), expected {thisfile})'''.replace('\n', ' ') + raise Failed(msg) + + if warningcls == SymPyDeprecationWarning: + this_file = pathlib.Path(__file__) + active_deprecations_file = (this_file.parent.parent.parent / 'doc' / + 'src' / 'explanation' / + 'active-deprecations.md') + if not active_deprecations_file.exists(): + # We can only test that the active_deprecations_target works if we are + # in the git repo. + return + targets = [] + for w in warnrec: + targets.append(w.message.active_deprecations_target) + text = pathlib.Path(active_deprecations_file).read_text(encoding="utf-8") + for target in targets: + if f'({target})=' not in text: + raise Failed(f"The active deprecations target {target!r} does not appear to be a valid target in the active-deprecations.md file ({active_deprecations_file}).") + +def _both_exp_pow(func): + """ + Decorator used to run the test twice: the first time `e^x` is represented + as ``Pow(E, x)``, the second time as ``exp(x)`` (exponential object is not + a power). + + This is a temporary trick helping to manage the elimination of the class + ``exp`` in favor of a replacement by ``Pow(E, ...)``. + """ + from sympy.core.parameters import _exp_is_pow + + def func_wrap(): + with _exp_is_pow(True): + func() + with _exp_is_pow(False): + func() + + wrapper = functools.update_wrapper(func_wrap, func) + return wrapper + + +@contextlib.contextmanager +def warns_deprecated_sympy(): + ''' + Shorthand for ``warns(SymPyDeprecationWarning)`` + + This is the recommended way to test that ``SymPyDeprecationWarning`` is + emitted for deprecated features in SymPy. To test for other warnings use + ``warns``. To suppress warnings without asserting that they are emitted + use ``ignore_warnings``. + + .. note:: + + ``warns_deprecated_sympy()`` is only intended for internal use in the + SymPy test suite to test that a deprecation warning triggers properly. + All other code in the SymPy codebase, including documentation examples, + should not use deprecated behavior. + + If you are a user of SymPy and you want to disable + SymPyDeprecationWarnings, use ``warnings`` filters (see + :ref:`silencing-sympy-deprecation-warnings`). + + >>> from sympy.testing.pytest import warns_deprecated_sympy + >>> from sympy.utilities.exceptions import sympy_deprecation_warning + >>> with warns_deprecated_sympy(): + ... sympy_deprecation_warning("Don't use", + ... deprecated_since_version="1.0", + ... active_deprecations_target="active-deprecations") + + >>> with warns_deprecated_sympy(): + ... pass + Traceback (most recent call last): + ... + Failed: DID NOT WARN. No warnings of type \ + SymPyDeprecationWarning was emitted. The list of emitted warnings is: []. + + .. note:: + + Sometimes the stacklevel test will fail because the same warning is + emitted multiple times. In this case, you can use + :func:`sympy.utilities.exceptions.ignore_warnings` in the code to + prevent the ``SymPyDeprecationWarning`` from being emitted again + recursively. In rare cases it is impossible to have a consistent + ``stacklevel`` for deprecation warnings because different ways of + calling a function will produce different call stacks.. In those cases, + use ``warns(SymPyDeprecationWarning)`` instead. + + See Also + ======== + sympy.utilities.exceptions.SymPyDeprecationWarning + sympy.utilities.exceptions.sympy_deprecation_warning + sympy.utilities.decorator.deprecated + + ''' + with warns(SymPyDeprecationWarning): + yield + + +def skip_under_pyodide(message): + """Decorator to skip a test if running under Pyodide/WASM.""" + def decorator(test_func): + @functools.wraps(test_func) + def test_wrapper(): + if IS_WASM: + skip(message) + return test_func() + return test_wrapper + return decorator diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/quality_unicode.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/quality_unicode.py new file mode 100644 index 0000000000000000000000000000000000000000..d43623ff5112610e377347f50c6a40a15810644b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/quality_unicode.py @@ -0,0 +1,102 @@ +import re +import fnmatch + + +message_unicode_B = \ + "File contains a unicode character : %s, line %s. " \ + "But not in the whitelist. " \ + "Add the file to the whitelist in " + __file__ +message_unicode_D = \ + "File does not contain a unicode character : %s." \ + "but is in the whitelist. " \ + "Remove the file from the whitelist in " + __file__ + + +encoding_header_re = re.compile( + r'^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)') + +# Whitelist pattern for files which can have unicode. +unicode_whitelist = [ + # Author names can include non-ASCII characters + r'*/bin/authors_update.py', + r'*/bin/mailmap_check.py', + + # These files have functions and test functions for unicode input and + # output. + r'*/sympy/testing/tests/test_code_quality.py', + r'*/sympy/physics/vector/tests/test_printing.py', + r'*/physics/quantum/tests/test_printing.py', + r'*/sympy/vector/tests/test_printing.py', + r'*/sympy/parsing/tests/test_sympy_parser.py', + r'*/sympy/printing/pretty/stringpict.py', + r'*/sympy/printing/pretty/tests/test_pretty.py', + r'*/sympy/printing/tests/test_conventions.py', + r'*/sympy/printing/tests/test_preview.py', + r'*/liealgebras/type_g.py', + r'*/liealgebras/weyl_group.py', + r'*/liealgebras/tests/test_type_G.py', + + # wigner.py and polarization.py have unicode doctests. These probably + # don't need to be there but some of the examples that are there are + # pretty ugly without use_unicode (matrices need to be wrapped across + # multiple lines etc) + r'*/sympy/physics/wigner.py', + r'*/sympy/physics/optics/polarization.py', + + # joint.py uses some unicode for variable names in the docstrings + r'*/sympy/physics/mechanics/joint.py', + + # lll method has unicode in docstring references and author name + r'*/sympy/polys/matrices/domainmatrix.py', + r'*/sympy/matrices/repmatrix.py', + + # Explanation of symbols uses greek letters + r'*/sympy/core/symbol.py', +] + +unicode_strict_whitelist = [ + r'*/sympy/parsing/latex/_antlr/__init__.py', + # test_mathematica.py uses some unicode for testing Greek characters are working #24055 + r'*/sympy/parsing/tests/test_mathematica.py', +] + + +def _test_this_file_encoding( + fname, test_file, + unicode_whitelist=unicode_whitelist, + unicode_strict_whitelist=unicode_strict_whitelist): + """Test helper function for unicode test + + The test may have to operate on filewise manner, so it had moved + to a separate process. + """ + has_unicode = False + + is_in_whitelist = False + is_in_strict_whitelist = False + for patt in unicode_whitelist: + if fnmatch.fnmatch(fname, patt): + is_in_whitelist = True + break + for patt in unicode_strict_whitelist: + if fnmatch.fnmatch(fname, patt): + is_in_strict_whitelist = True + is_in_whitelist = True + break + + if is_in_whitelist: + for idx, line in enumerate(test_file): + try: + line.encode(encoding='ascii') + except (UnicodeEncodeError, UnicodeDecodeError): + has_unicode = True + + if not has_unicode and not is_in_strict_whitelist: + assert False, message_unicode_D % fname + + else: + for idx, line in enumerate(test_file): + try: + line.encode(encoding='ascii') + except (UnicodeEncodeError, UnicodeDecodeError): + assert False, message_unicode_B % (fname, idx + 1) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/randtest.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/randtest.py new file mode 100644 index 0000000000000000000000000000000000000000..3ce2c8c031eec1c886532daba32c96d83e9cf85c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/randtest.py @@ -0,0 +1,19 @@ +""" +.. deprecated:: 1.10 + + ``sympy.testing.randtest`` functions have been moved to + :mod:`sympy.core.random`. + +""" +from sympy.utilities.exceptions import sympy_deprecation_warning + +sympy_deprecation_warning("The sympy.testing.randtest submodule is deprecated. Use sympy.core.random instead.", + deprecated_since_version="1.10", + active_deprecations_target="deprecated-sympy-testing-randtest") + +from sympy.core.random import ( # noqa:F401 + random_complex_number, + verify_numerically, + test_derivative_numerically, + _randrange, + _randint) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/runtests.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/runtests.py new file mode 100644 index 0000000000000000000000000000000000000000..e2650e4e6dabaaa07fc25c76cce3d9d28723b0d5 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/runtests.py @@ -0,0 +1,2409 @@ +""" +This is our testing framework. + +Goals: + +* it should be compatible with py.test and operate very similarly + (or identically) +* does not require any external dependencies +* preferably all the functionality should be in this file only +* no magic, just import the test file and execute the test functions, that's it +* portable + +""" + +import os +import sys +import platform +import inspect +import traceback +import pdb +import re +import linecache +import time +from fnmatch import fnmatch +from timeit import default_timer as clock +import doctest as pdoctest # avoid clashing with our doctest() function +from doctest import DocTestFinder, DocTestRunner +import random +import subprocess +import shutil +import signal +import stat +import tempfile +import warnings +from contextlib import contextmanager +from inspect import unwrap +from pathlib import Path + +from sympy.core.cache import clear_cache +from sympy.external import import_module +from sympy.external.gmpy import GROUND_TYPES + +IS_WINDOWS = (os.name == 'nt') +ON_CI = os.getenv('CI', None) + +# empirically generated list of the proportion of time spent running +# an even split of tests. This should periodically be regenerated. +# A list of [.6, .1, .3] would mean that if the tests are evenly split +# into '1/3', '2/3', '3/3', the first split would take 60% of the time, +# the second 10% and the third 30%. These lists are normalized to sum +# to 1, so [60, 10, 30] has the same behavior as [6, 1, 3] or [.6, .1, .3]. +# +# This list can be generated with the code: +# from time import time +# import sympy +# import os +# os.environ["CI"] = 'true' # Mock CI to get more correct densities +# delays, num_splits = [], 30 +# for i in range(1, num_splits + 1): +# tic = time() +# sympy.test(split='{}/{}'.format(i, num_splits), time_balance=False) # Add slow=True for slow tests +# delays.append(time() - tic) +# tot = sum(delays) +# print([round(x / tot, 4) for x in delays]) +SPLIT_DENSITY = [ + 0.0059, 0.0027, 0.0068, 0.0011, 0.0006, + 0.0058, 0.0047, 0.0046, 0.004, 0.0257, + 0.0017, 0.0026, 0.004, 0.0032, 0.0016, + 0.0015, 0.0004, 0.0011, 0.0016, 0.0014, + 0.0077, 0.0137, 0.0217, 0.0074, 0.0043, + 0.0067, 0.0236, 0.0004, 0.1189, 0.0142, + 0.0234, 0.0003, 0.0003, 0.0047, 0.0006, + 0.0013, 0.0004, 0.0008, 0.0007, 0.0006, + 0.0139, 0.0013, 0.0007, 0.0051, 0.002, + 0.0004, 0.0005, 0.0213, 0.0048, 0.0016, + 0.0012, 0.0014, 0.0024, 0.0015, 0.0004, + 0.0005, 0.0007, 0.011, 0.0062, 0.0015, + 0.0021, 0.0049, 0.0006, 0.0006, 0.0011, + 0.0006, 0.0019, 0.003, 0.0044, 0.0054, + 0.0057, 0.0049, 0.0016, 0.0006, 0.0009, + 0.0006, 0.0012, 0.0006, 0.0149, 0.0532, + 0.0076, 0.0041, 0.0024, 0.0135, 0.0081, + 0.2209, 0.0459, 0.0438, 0.0488, 0.0137, + 0.002, 0.0003, 0.0008, 0.0039, 0.0024, + 0.0005, 0.0004, 0.003, 0.056, 0.0026] +SPLIT_DENSITY_SLOW = [0.0086, 0.0004, 0.0568, 0.0003, 0.0032, 0.0005, 0.0004, 0.0013, 0.0016, 0.0648, 0.0198, 0.1285, 0.098, 0.0005, 0.0064, 0.0003, 0.0004, 0.0026, 0.0007, 0.0051, 0.0089, 0.0024, 0.0033, 0.0057, 0.0005, 0.0003, 0.001, 0.0045, 0.0091, 0.0006, 0.0005, 0.0321, 0.0059, 0.1105, 0.216, 0.1489, 0.0004, 0.0003, 0.0006, 0.0483] + +class Skipped(Exception): + pass + +class TimeOutError(Exception): + pass + +class DependencyError(Exception): + pass + + +def _indent(s, indent=4): + """ + Add the given number of space characters to the beginning of + every non-blank line in ``s``, and return the result. + If the string ``s`` is Unicode, it is encoded using the stdout + encoding and the ``backslashreplace`` error handler. + """ + # This regexp matches the start of non-blank lines: + return re.sub('(?m)^(?!$)', indent*' ', s) + + +pdoctest._indent = _indent # type: ignore + +# override reporter to maintain windows and python3 + + +def _report_failure(self, out, test, example, got): + """ + Report that the given example failed. + """ + s = self._checker.output_difference(example, got, self.optionflags) + s = s.encode('raw_unicode_escape').decode('utf8', 'ignore') + out(self._failure_header(test, example) + s) + + +if IS_WINDOWS: + DocTestRunner.report_failure = _report_failure # type: ignore + + +def convert_to_native_paths(lst): + """ + Converts a list of '/' separated paths into a list of + native (os.sep separated) paths and converts to lowercase + if the system is case insensitive. + """ + newlst = [] + for rv in lst: + rv = os.path.join(*rv.split("/")) + # on windows the slash after the colon is dropped + if sys.platform == "win32": + pos = rv.find(':') + if pos != -1: + if rv[pos + 1] != '\\': + rv = rv[:pos + 1] + '\\' + rv[pos + 1:] + newlst.append(os.path.normcase(rv)) + return newlst + + +def get_sympy_dir(): + """ + Returns the root SymPy directory and set the global value + indicating whether the system is case sensitive or not. + """ + this_file = os.path.abspath(__file__) + sympy_dir = os.path.join(os.path.dirname(this_file), "..", "..") + sympy_dir = os.path.normpath(sympy_dir) + return os.path.normcase(sympy_dir) + + +def setup_pprint(disable_line_wrap=True): + from sympy.interactive.printing import init_printing + from sympy.printing.pretty.pretty import pprint_use_unicode + import sympy.interactive.printing as interactive_printing + from sympy.printing.pretty import stringpict + + # Prevent init_printing() in doctests from affecting other doctests + interactive_printing.NO_GLOBAL = True + + # force pprint to be in ascii mode in doctests + use_unicode_prev = pprint_use_unicode(False) + + # disable line wrapping for pprint() outputs + wrap_line_prev = stringpict._GLOBAL_WRAP_LINE + if disable_line_wrap: + stringpict._GLOBAL_WRAP_LINE = False + + # hook our nice, hash-stable strprinter + init_printing(pretty_print=False) + + return use_unicode_prev, wrap_line_prev + + +@contextmanager +def raise_on_deprecated(): + """Context manager to make DeprecationWarning raise an error + + This is to catch SymPyDeprecationWarning from library code while running + tests and doctests. It is important to use this context manager around + each individual test/doctest in case some tests modify the warning + filters. + """ + with warnings.catch_warnings(): + warnings.filterwarnings('error', '.*', DeprecationWarning, module='sympy.*') + yield + + +def run_in_subprocess_with_hash_randomization( + function, function_args=(), + function_kwargs=None, command=sys.executable, + module='sympy.testing.runtests', force=False): + """ + Run a function in a Python subprocess with hash randomization enabled. + + If hash randomization is not supported by the version of Python given, it + returns False. Otherwise, it returns the exit value of the command. The + function is passed to sys.exit(), so the return value of the function will + be the return value. + + The environment variable PYTHONHASHSEED is used to seed Python's hash + randomization. If it is set, this function will return False, because + starting a new subprocess is unnecessary in that case. If it is not set, + one is set at random, and the tests are run. Note that if this + environment variable is set when Python starts, hash randomization is + automatically enabled. To force a subprocess to be created even if + PYTHONHASHSEED is set, pass ``force=True``. This flag will not force a + subprocess in Python versions that do not support hash randomization (see + below), because those versions of Python do not support the ``-R`` flag. + + ``function`` should be a string name of a function that is importable from + the module ``module``, like "_test". The default for ``module`` is + "sympy.testing.runtests". ``function_args`` and ``function_kwargs`` + should be a repr-able tuple and dict, respectively. The default Python + command is sys.executable, which is the currently running Python command. + + This function is necessary because the seed for hash randomization must be + set by the environment variable before Python starts. Hence, in order to + use a predetermined seed for tests, we must start Python in a separate + subprocess. + + Hash randomization was added in the minor Python versions 2.6.8, 2.7.3, + 3.1.5, and 3.2.3, and is enabled by default in all Python versions after + and including 3.3.0. + + Examples + ======== + + >>> from sympy.testing.runtests import ( + ... run_in_subprocess_with_hash_randomization) + >>> # run the core tests in verbose mode + >>> run_in_subprocess_with_hash_randomization("_test", + ... function_args=("core",), + ... function_kwargs={'verbose': True}) # doctest: +SKIP + # Will return 0 if sys.executable supports hash randomization and tests + # pass, 1 if they fail, and False if it does not support hash + # randomization. + + """ + cwd = get_sympy_dir() + # Note, we must return False everywhere, not None, as subprocess.call will + # sometimes return None. + + # First check if the Python version supports hash randomization + # If it does not have this support, it won't recognize the -R flag + p = subprocess.Popen([command, "-RV"], stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, cwd=cwd) + p.communicate() + if p.returncode != 0: + return False + + hash_seed = os.getenv("PYTHONHASHSEED") + if not hash_seed: + os.environ["PYTHONHASHSEED"] = str(random.randrange(2**32)) + else: + if not force: + return False + + function_kwargs = function_kwargs or {} + + # Now run the command + commandstring = ("import sys; from %s import %s;sys.exit(%s(*%s, **%s))" % + (module, function, function, repr(function_args), + repr(function_kwargs))) + + try: + p = subprocess.Popen([command, "-R", "-c", commandstring], cwd=cwd) + p.communicate() + except KeyboardInterrupt: + p.wait() + finally: + # Put the environment variable back, so that it reads correctly for + # the current Python process. + if hash_seed is None: + del os.environ["PYTHONHASHSEED"] + else: + os.environ["PYTHONHASHSEED"] = hash_seed + return p.returncode + + +def run_all_tests(test_args=(), test_kwargs=None, + doctest_args=(), doctest_kwargs=None, + examples_args=(), examples_kwargs=None): + """ + Run all tests. + + Right now, this runs the regular tests (bin/test), the doctests + (bin/doctest), and the examples (examples/all.py). + + This is what ``setup.py test`` uses. + + You can pass arguments and keyword arguments to the test functions that + support them (for now, test, doctest, and the examples). See the + docstrings of those functions for a description of the available options. + + For example, to run the solvers tests with colors turned off: + + >>> from sympy.testing.runtests import run_all_tests + >>> run_all_tests(test_args=("solvers",), + ... test_kwargs={"colors:False"}) # doctest: +SKIP + + """ + tests_successful = True + + test_kwargs = test_kwargs or {} + doctest_kwargs = doctest_kwargs or {} + examples_kwargs = examples_kwargs or {'quiet': True} + + try: + # Regular tests + if not test(*test_args, **test_kwargs): + # some regular test fails, so set the tests_successful + # flag to false and continue running the doctests + tests_successful = False + + # Doctests + print() + if not doctest(*doctest_args, **doctest_kwargs): + tests_successful = False + + # Examples + print() + sys.path.append("examples") # examples/all.py + from all import run_examples # type: ignore + if not run_examples(*examples_args, **examples_kwargs): + tests_successful = False + + if tests_successful: + return + else: + # Return nonzero exit code + sys.exit(1) + except KeyboardInterrupt: + print() + print("DO *NOT* COMMIT!") + sys.exit(1) + + +def test(*paths, subprocess=True, rerun=0, **kwargs): + """ + Run tests in the specified test_*.py files. + + Tests in a particular test_*.py file are run if any of the given strings + in ``paths`` matches a part of the test file's path. If ``paths=[]``, + tests in all test_*.py files are run. + + Notes: + + - If sort=False, tests are run in random order (not default). + - Paths can be entered in native system format or in unix, + forward-slash format. + - Files that are on the blacklist can be tested by providing + their path; they are only excluded if no paths are given. + + **Explanation of test results** + + ====== =============================================================== + Output Meaning + ====== =============================================================== + . passed + F failed + X XPassed (expected to fail but passed) + f XFAILed (expected to fail and indeed failed) + s skipped + w slow + T timeout (e.g., when ``--timeout`` is used) + K KeyboardInterrupt (when running the slow tests with ``--slow``, + you can interrupt one of them without killing the test runner) + ====== =============================================================== + + + Colors have no additional meaning and are used just to facilitate + interpreting the output. + + Examples + ======== + + >>> import sympy + + Run all tests: + + >>> sympy.test() # doctest: +SKIP + + Run one file: + + >>> sympy.test("sympy/core/tests/test_basic.py") # doctest: +SKIP + >>> sympy.test("_basic") # doctest: +SKIP + + Run all tests in sympy/functions/ and some particular file: + + >>> sympy.test("sympy/core/tests/test_basic.py", + ... "sympy/functions") # doctest: +SKIP + + Run all tests in sympy/core and sympy/utilities: + + >>> sympy.test("/core", "/util") # doctest: +SKIP + + Run specific test from a file: + + >>> sympy.test("sympy/core/tests/test_basic.py", + ... kw="test_equality") # doctest: +SKIP + + Run specific test from any file: + + >>> sympy.test(kw="subs") # doctest: +SKIP + + Run the tests with verbose mode on: + + >>> sympy.test(verbose=True) # doctest: +SKIP + + Do not sort the test output: + + >>> sympy.test(sort=False) # doctest: +SKIP + + Turn on post-mortem pdb: + + >>> sympy.test(pdb=True) # doctest: +SKIP + + Turn off colors: + + >>> sympy.test(colors=False) # doctest: +SKIP + + Force colors, even when the output is not to a terminal (this is useful, + e.g., if you are piping to ``less -r`` and you still want colors) + + >>> sympy.test(force_colors=False) # doctest: +SKIP + + The traceback verboseness can be set to "short" or "no" (default is + "short") + + >>> sympy.test(tb='no') # doctest: +SKIP + + The ``split`` option can be passed to split the test run into parts. The + split currently only splits the test files, though this may change in the + future. ``split`` should be a string of the form 'a/b', which will run + part ``a`` of ``b``. For instance, to run the first half of the test suite: + + >>> sympy.test(split='1/2') # doctest: +SKIP + + The ``time_balance`` option can be passed in conjunction with ``split``. + If ``time_balance=True`` (the default for ``sympy.test``), SymPy will attempt + to split the tests such that each split takes equal time. This heuristic + for balancing is based on pre-recorded test data. + + >>> sympy.test(split='1/2', time_balance=True) # doctest: +SKIP + + You can disable running the tests in a separate subprocess using + ``subprocess=False``. This is done to support seeding hash randomization, + which is enabled by default in the Python versions where it is supported. + If subprocess=False, hash randomization is enabled/disabled according to + whether it has been enabled or not in the calling Python process. + However, even if it is enabled, the seed cannot be printed unless it is + called from a new Python process. + + Hash randomization was added in the minor Python versions 2.6.8, 2.7.3, + 3.1.5, and 3.2.3, and is enabled by default in all Python versions after + and including 3.3.0. + + If hash randomization is not supported ``subprocess=False`` is used + automatically. + + >>> sympy.test(subprocess=False) # doctest: +SKIP + + To set the hash randomization seed, set the environment variable + ``PYTHONHASHSEED`` before running the tests. This can be done from within + Python using + + >>> import os + >>> os.environ['PYTHONHASHSEED'] = '42' # doctest: +SKIP + + Or from the command line using + + $ PYTHONHASHSEED=42 ./bin/test + + If the seed is not set, a random seed will be chosen. + + Note that to reproduce the same hash values, you must use both the same seed + as well as the same architecture (32-bit vs. 64-bit). + + """ + # count up from 0, do not print 0 + print_counter = lambda i : (print("rerun %d" % (rerun-i)) + if rerun-i else None) + + if subprocess: + # loop backwards so last i is 0 + for i in range(rerun, -1, -1): + print_counter(i) + ret = run_in_subprocess_with_hash_randomization("_test", + function_args=paths, function_kwargs=kwargs) + if ret is False: + break + val = not bool(ret) + # exit on the first failure or if done + if not val or i == 0: + return val + + # rerun even if hash randomization is not supported + for i in range(rerun, -1, -1): + print_counter(i) + val = not bool(_test(*paths, **kwargs)) + if not val or i == 0: + return val + + +def _test(*paths, + verbose=False, tb="short", kw=None, pdb=False, colors=True, + force_colors=False, sort=True, seed=None, timeout=False, + fail_on_timeout=False, slow=False, enhance_asserts=False, split=None, + time_balance=True, blacklist=(), + fast_threshold=None, slow_threshold=None): + """ + Internal function that actually runs the tests. + + All keyword arguments from ``test()`` are passed to this function except for + ``subprocess``. + + Returns 0 if tests passed and 1 if they failed. See the docstring of + ``test()`` for more information. + """ + kw = kw or () + # ensure that kw is a tuple + if isinstance(kw, str): + kw = (kw,) + post_mortem = pdb + if seed is None: + seed = random.randrange(100000000) + if ON_CI and timeout is False: + timeout = 595 + fail_on_timeout = True + if ON_CI: + blacklist = list(blacklist) + ['sympy/plotting/pygletplot/tests'] + blacklist = convert_to_native_paths(blacklist) + r = PyTestReporter(verbose=verbose, tb=tb, colors=colors, + force_colors=force_colors, split=split) + # This won't strictly run the test for the corresponding file, but it is + # good enough for copying and pasting the failing test. + _paths = [] + for path in paths: + if '::' in path: + path, _kw = path.split('::', 1) + kw += (_kw,) + _paths.append(path) + paths = _paths + + t = SymPyTests(r, kw, post_mortem, seed, + fast_threshold=fast_threshold, + slow_threshold=slow_threshold) + + test_files = t.get_test_files('sympy') + + not_blacklisted = [f for f in test_files + if not any(b in f for b in blacklist)] + + if len(paths) == 0: + matched = not_blacklisted + else: + paths = convert_to_native_paths(paths) + matched = [] + for f in not_blacklisted: + basename = os.path.basename(f) + for p in paths: + if p in f or fnmatch(basename, p): + matched.append(f) + break + + density = None + if time_balance: + if slow: + density = SPLIT_DENSITY_SLOW + else: + density = SPLIT_DENSITY + + if split: + matched = split_list(matched, split, density=density) + + t._testfiles.extend(matched) + + return int(not t.test(sort=sort, timeout=timeout, slow=slow, + enhance_asserts=enhance_asserts, fail_on_timeout=fail_on_timeout)) + + +def doctest(*paths, subprocess=True, rerun=0, **kwargs): + r""" + Runs doctests in all \*.py files in the SymPy directory which match + any of the given strings in ``paths`` or all tests if paths=[]. + + Notes: + + - Paths can be entered in native system format or in unix, + forward-slash format. + - Files that are on the blacklist can be tested by providing + their path; they are only excluded if no paths are given. + + Examples + ======== + + >>> import sympy + + Run all tests: + + >>> sympy.doctest() # doctest: +SKIP + + Run one file: + + >>> sympy.doctest("sympy/core/basic.py") # doctest: +SKIP + >>> sympy.doctest("polynomial.rst") # doctest: +SKIP + + Run all tests in sympy/functions/ and some particular file: + + >>> sympy.doctest("/functions", "basic.py") # doctest: +SKIP + + Run any file having polynomial in its name, doc/src/modules/polynomial.rst, + sympy/functions/special/polynomials.py, and sympy/polys/polynomial.py: + + >>> sympy.doctest("polynomial") # doctest: +SKIP + + The ``split`` option can be passed to split the test run into parts. The + split currently only splits the test files, though this may change in the + future. ``split`` should be a string of the form 'a/b', which will run + part ``a`` of ``b``. Note that the regular doctests and the Sphinx + doctests are split independently. For instance, to run the first half of + the test suite: + + >>> sympy.doctest(split='1/2') # doctest: +SKIP + + The ``subprocess`` and ``verbose`` options are the same as with the function + ``test()`` (see the docstring of that function for more information) except + that ``verbose`` may also be set equal to ``2`` in order to print + individual doctest lines, as they are being tested. + """ + # count up from 0, do not print 0 + print_counter = lambda i : (print("rerun %d" % (rerun-i)) + if rerun-i else None) + + if subprocess: + # loop backwards so last i is 0 + for i in range(rerun, -1, -1): + print_counter(i) + ret = run_in_subprocess_with_hash_randomization("_doctest", + function_args=paths, function_kwargs=kwargs) + if ret is False: + break + val = not bool(ret) + # exit on the first failure or if done + if not val or i == 0: + return val + + # rerun even if hash randomization is not supported + for i in range(rerun, -1, -1): + print_counter(i) + val = not bool(_doctest(*paths, **kwargs)) + if not val or i == 0: + return val + + +def _get_doctest_blacklist(): + '''Get the default blacklist for the doctests''' + blacklist = [] + + blacklist.extend([ + "doc/src/modules/plotting.rst", # generates live plots + "doc/src/modules/physics/mechanics/autolev_parser.rst", + "sympy/codegen/array_utils.py", # raises deprecation warning + "sympy/core/compatibility.py", # backwards compatibility shim, importing it triggers a deprecation warning + "sympy/core/trace.py", # backwards compatibility shim, importing it triggers a deprecation warning + "sympy/galgebra.py", # no longer part of SymPy + "sympy/parsing/autolev/_antlr/autolevlexer.py", # generated code + "sympy/parsing/autolev/_antlr/autolevlistener.py", # generated code + "sympy/parsing/autolev/_antlr/autolevparser.py", # generated code + "sympy/parsing/latex/_antlr/latexlexer.py", # generated code + "sympy/parsing/latex/_antlr/latexparser.py", # generated code + "sympy/plotting/pygletplot/__init__.py", # crashes on some systems + "sympy/plotting/pygletplot/plot.py", # crashes on some systems + "sympy/printing/ccode.py", # backwards compatibility shim, importing it breaks the codegen doctests + "sympy/printing/cxxcode.py", # backwards compatibility shim, importing it breaks the codegen doctests + "sympy/printing/fcode.py", # backwards compatibility shim, importing it breaks the codegen doctests + "sympy/testing/randtest.py", # backwards compatibility shim, importing it triggers a deprecation warning + "sympy/this.py", # prints text + ]) + # autolev parser tests + num = 12 + for i in range (1, num+1): + blacklist.append("sympy/parsing/autolev/test-examples/ruletest" + str(i) + ".py") + blacklist.extend(["sympy/parsing/autolev/test-examples/pydy-example-repo/mass_spring_damper.py", + "sympy/parsing/autolev/test-examples/pydy-example-repo/chaos_pendulum.py", + "sympy/parsing/autolev/test-examples/pydy-example-repo/double_pendulum.py", + "sympy/parsing/autolev/test-examples/pydy-example-repo/non_min_pendulum.py"]) + + if import_module('numpy') is None: + blacklist.extend([ + "sympy/plotting/experimental_lambdify.py", + "sympy/plotting/plot_implicit.py", + "examples/advanced/autowrap_integrators.py", + "examples/advanced/autowrap_ufuncify.py", + "examples/intermediate/sample.py", + "examples/intermediate/mplot2d.py", + "examples/intermediate/mplot3d.py", + "doc/src/modules/numeric-computation.rst", + "doc/src/explanation/best-practices.md", + "doc/src/tutorials/physics/biomechanics/biomechanical-model-example.rst", + "doc/src/tutorials/physics/biomechanics/biomechanics.rst", + ]) + else: + if import_module('matplotlib') is None: + blacklist.extend([ + "examples/intermediate/mplot2d.py", + "examples/intermediate/mplot3d.py" + ]) + else: + # Use a non-windowed backend, so that the tests work on CI + import matplotlib + matplotlib.use('Agg') + + if ON_CI or import_module('pyglet') is None: + blacklist.extend(["sympy/plotting/pygletplot"]) + + if import_module('aesara') is None: + blacklist.extend([ + "sympy/printing/aesaracode.py", + "doc/src/modules/numeric-computation.rst", + ]) + + if import_module('cupy') is None: + blacklist.extend([ + "doc/src/modules/numeric-computation.rst", + ]) + + if import_module('jax') is None: + blacklist.extend([ + "doc/src/modules/numeric-computation.rst", + ]) + + if import_module('antlr4') is None: + blacklist.extend([ + "sympy/parsing/autolev/__init__.py", + "sympy/parsing/latex/_parse_latex_antlr.py", + ]) + + if import_module('lfortran') is None: + #throws ImportError when lfortran not installed + blacklist.extend([ + "sympy/parsing/sym_expr.py", + ]) + + if import_module("scipy") is None: + # throws ModuleNotFoundError when scipy not installed + blacklist.extend([ + "doc/src/guides/solving/solve-numerically.md", + "doc/src/guides/solving/solve-ode.md", + ]) + + if import_module("numpy") is None: + # throws ModuleNotFoundError when numpy not installed + blacklist.extend([ + "doc/src/guides/solving/solve-ode.md", + "doc/src/guides/solving/solve-numerically.md", + ]) + + # disabled because of doctest failures in asmeurer's bot + blacklist.extend([ + "sympy/utilities/autowrap.py", + "examples/advanced/autowrap_integrators.py", + "examples/advanced/autowrap_ufuncify.py" + ]) + + blacklist.extend([ + "sympy/conftest.py", # Depends on pytest + ]) + + # These are deprecated stubs to be removed: + blacklist.extend([ + "sympy/utilities/tmpfiles.py", + "sympy/utilities/pytest.py", + "sympy/utilities/runtests.py", + "sympy/utilities/quality_unicode.py", + "sympy/utilities/randtest.py", + ]) + + blacklist = convert_to_native_paths(blacklist) + return blacklist + + +def _doctest(*paths, **kwargs): + """ + Internal function that actually runs the doctests. + + All keyword arguments from ``doctest()`` are passed to this function + except for ``subprocess``. + + Returns 0 if tests passed and 1 if they failed. See the docstrings of + ``doctest()`` and ``test()`` for more information. + """ + from sympy.printing.pretty.pretty import pprint_use_unicode + from sympy.printing.pretty import stringpict + + normal = kwargs.get("normal", False) + verbose = kwargs.get("verbose", False) + colors = kwargs.get("colors", True) + force_colors = kwargs.get("force_colors", False) + blacklist = kwargs.get("blacklist", []) + split = kwargs.get('split', None) + + blacklist.extend(_get_doctest_blacklist()) + + # Use a non-windowed backend, so that the tests work on CI + if import_module('matplotlib') is not None: + import matplotlib + matplotlib.use('Agg') + + # Disable warnings for external modules + import sympy.external + sympy.external.importtools.WARN_OLD_VERSION = False + sympy.external.importtools.WARN_NOT_INSTALLED = False + + # Disable showing up of plots + from sympy.plotting.plot import unset_show + unset_show() + + r = PyTestReporter(verbose, split=split, colors=colors,\ + force_colors=force_colors) + t = SymPyDocTests(r, normal) + + test_files = t.get_test_files('sympy') + test_files.extend(t.get_test_files('examples', init_only=False)) + + not_blacklisted = [f for f in test_files + if not any(b in f for b in blacklist)] + if len(paths) == 0: + matched = not_blacklisted + else: + # take only what was requested...but not blacklisted items + # and allow for partial match anywhere or fnmatch of name + paths = convert_to_native_paths(paths) + matched = [] + for f in not_blacklisted: + basename = os.path.basename(f) + for p in paths: + if p in f or fnmatch(basename, p): + matched.append(f) + break + + matched.sort() + + if split: + matched = split_list(matched, split) + + t._testfiles.extend(matched) + + # run the tests and record the result for this *py portion of the tests + if t._testfiles: + failed = not t.test() + else: + failed = False + + # N.B. + # -------------------------------------------------------------------- + # Here we test *.rst and *.md files at or below doc/src. Code from these + # must be self supporting in terms of imports since there is no importing + # of necessary modules by doctest.testfile. If you try to pass *.py files + # through this they might fail because they will lack the needed imports + # and smarter parsing that can be done with source code. + # + test_files_rst = t.get_test_files('doc/src', '*.rst', init_only=False) + test_files_md = t.get_test_files('doc/src', '*.md', init_only=False) + test_files = test_files_rst + test_files_md + test_files.sort() + + not_blacklisted = [f for f in test_files + if not any(b in f for b in blacklist)] + + if len(paths) == 0: + matched = not_blacklisted + else: + # Take only what was requested as long as it's not on the blacklist. + # Paths were already made native in *py tests so don't repeat here. + # There's no chance of having a *py file slip through since we + # only have *rst files in test_files. + matched = [] + for f in not_blacklisted: + basename = os.path.basename(f) + for p in paths: + if p in f or fnmatch(basename, p): + matched.append(f) + break + + if split: + matched = split_list(matched, split) + + first_report = True + for rst_file in matched: + if not os.path.isfile(rst_file): + continue + old_displayhook = sys.displayhook + try: + use_unicode_prev, wrap_line_prev = setup_pprint() + out = sympytestfile( + rst_file, module_relative=False, encoding='utf-8', + optionflags=pdoctest.ELLIPSIS | pdoctest.NORMALIZE_WHITESPACE | + pdoctest.IGNORE_EXCEPTION_DETAIL) + finally: + # make sure we return to the original displayhook in case some + # doctest has changed that + sys.displayhook = old_displayhook + # The NO_GLOBAL flag overrides the no_global flag to init_printing + # if True + import sympy.interactive.printing as interactive_printing + interactive_printing.NO_GLOBAL = False + pprint_use_unicode(use_unicode_prev) + stringpict._GLOBAL_WRAP_LINE = wrap_line_prev + + rstfailed, tested = out + if tested: + failed = rstfailed or failed + if first_report: + first_report = False + msg = 'rst/md doctests start' + if not t._testfiles: + r.start(msg=msg) + else: + r.write_center(msg) + print() + # use as the id, everything past the first 'sympy' + file_id = rst_file[rst_file.find('sympy') + len('sympy') + 1:] + print(file_id, end=" ") + # get at least the name out so it is know who is being tested + wid = r.terminal_width - len(file_id) - 1 # update width + test_file = '[%s]' % (tested) + report = '[%s]' % (rstfailed or 'OK') + print(''.join( + [test_file, ' '*(wid - len(test_file) - len(report)), report]) + ) + + # the doctests for *py will have printed this message already if there was + # a failure, so now only print it if there was intervening reporting by + # testing the *rst as evidenced by first_report no longer being True. + if not first_report and failed: + print() + print("DO *NOT* COMMIT!") + + return int(failed) + +sp = re.compile(r'([0-9]+)/([1-9][0-9]*)') + +def split_list(l, split, density=None): + """ + Splits a list into part a of b + + split should be a string of the form 'a/b'. For instance, '1/3' would give + the split one of three. + + If the length of the list is not divisible by the number of splits, the + last split will have more items. + + `density` may be specified as a list. If specified, + tests will be balanced so that each split has as equal-as-possible + amount of mass according to `density`. + + >>> from sympy.testing.runtests import split_list + >>> a = list(range(10)) + >>> split_list(a, '1/3') + [0, 1, 2] + >>> split_list(a, '2/3') + [3, 4, 5] + >>> split_list(a, '3/3') + [6, 7, 8, 9] + """ + m = sp.match(split) + if not m: + raise ValueError("split must be a string of the form a/b where a and b are ints") + i, t = map(int, m.groups()) + + if not density: + return l[(i - 1)*len(l)//t : i*len(l)//t] + + # normalize density + tot = sum(density) + density = [x / tot for x in density] + + def density_inv(x): + """Interpolate the inverse to the cumulative + distribution function given by density""" + if x <= 0: + return 0 + if x >= sum(density): + return 1 + + # find the first time the cumulative sum surpasses x + # and linearly interpolate + cumm = 0 + for i, d in enumerate(density): + cumm += d + if cumm >= x: + break + frac = (d - (cumm - x)) / d + return (i + frac) / len(density) + + lower_frac = density_inv((i - 1) / t) + higher_frac = density_inv(i / t) + return l[int(lower_frac*len(l)) : int(higher_frac*len(l))] + +from collections import namedtuple +SymPyTestResults = namedtuple('SymPyTestResults', 'failed attempted') + +def sympytestfile(filename, module_relative=True, name=None, package=None, + globs=None, verbose=None, report=True, optionflags=0, + extraglobs=None, raise_on_error=False, + parser=pdoctest.DocTestParser(), encoding=None): + + """ + Test examples in the given file. Return (#failures, #tests). + + Optional keyword arg ``module_relative`` specifies how filenames + should be interpreted: + + - If ``module_relative`` is True (the default), then ``filename`` + specifies a module-relative path. By default, this path is + relative to the calling module's directory; but if the + ``package`` argument is specified, then it is relative to that + package. To ensure os-independence, ``filename`` should use + "/" characters to separate path segments, and should not + be an absolute path (i.e., it may not begin with "/"). + + - If ``module_relative`` is False, then ``filename`` specifies an + os-specific path. The path may be absolute or relative (to + the current working directory). + + Optional keyword arg ``name`` gives the name of the test; by default + use the file's basename. + + Optional keyword argument ``package`` is a Python package or the + name of a Python package whose directory should be used as the + base directory for a module relative filename. If no package is + specified, then the calling module's directory is used as the base + directory for module relative filenames. It is an error to + specify ``package`` if ``module_relative`` is False. + + Optional keyword arg ``globs`` gives a dict to be used as the globals + when executing examples; by default, use {}. A copy of this dict + is actually used for each docstring, so that each docstring's + examples start with a clean slate. + + Optional keyword arg ``extraglobs`` gives a dictionary that should be + merged into the globals that are used to execute examples. By + default, no extra globals are used. + + Optional keyword arg ``verbose`` prints lots of stuff if true, prints + only failures if false; by default, it's true iff "-v" is in sys.argv. + + Optional keyword arg ``report`` prints a summary at the end when true, + else prints nothing at the end. In verbose mode, the summary is + detailed, else very brief (in fact, empty if all tests passed). + + Optional keyword arg ``optionflags`` or's together module constants, + and defaults to 0. Possible values (see the docs for details): + + - DONT_ACCEPT_TRUE_FOR_1 + - DONT_ACCEPT_BLANKLINE + - NORMALIZE_WHITESPACE + - ELLIPSIS + - SKIP + - IGNORE_EXCEPTION_DETAIL + - REPORT_UDIFF + - REPORT_CDIFF + - REPORT_NDIFF + - REPORT_ONLY_FIRST_FAILURE + + Optional keyword arg ``raise_on_error`` raises an exception on the + first unexpected exception or failure. This allows failures to be + post-mortem debugged. + + Optional keyword arg ``parser`` specifies a DocTestParser (or + subclass) that should be used to extract tests from the files. + + Optional keyword arg ``encoding`` specifies an encoding that should + be used to convert the file to unicode. + + Advanced tomfoolery: testmod runs methods of a local instance of + class doctest.Tester, then merges the results into (or creates) + global Tester instance doctest.master. Methods of doctest.master + can be called directly too, if you want to do something unusual. + Passing report=0 to testmod is especially useful then, to delay + displaying a summary. Invoke doctest.master.summarize(verbose) + when you're done fiddling. + """ + if package and not module_relative: + raise ValueError("Package may only be specified for module-" + "relative paths.") + + # Relativize the path + text, filename = pdoctest._load_testfile( + filename, package, module_relative, encoding) + + # If no name was given, then use the file's name. + if name is None: + name = os.path.basename(filename) + + # Assemble the globals. + if globs is None: + globs = {} + else: + globs = globs.copy() + if extraglobs is not None: + globs.update(extraglobs) + if '__name__' not in globs: + globs['__name__'] = '__main__' + + if raise_on_error: + runner = pdoctest.DebugRunner(verbose=verbose, optionflags=optionflags) + else: + runner = SymPyDocTestRunner(verbose=verbose, optionflags=optionflags) + runner._checker = SymPyOutputChecker() + + # Read the file, convert it to a test, and run it. + test = parser.get_doctest(text, globs, name, filename, 0) + runner.run(test) + + if report: + runner.summarize() + + if pdoctest.master is None: + pdoctest.master = runner + else: + pdoctest.master.merge(runner) + + return SymPyTestResults(runner.failures, runner.tries) + + +class SymPyTests: + + def __init__(self, reporter, kw="", post_mortem=False, + seed=None, fast_threshold=None, slow_threshold=None): + self._post_mortem = post_mortem + self._kw = kw + self._count = 0 + self._root_dir = get_sympy_dir() + self._reporter = reporter + self._reporter.root_dir(self._root_dir) + self._testfiles = [] + self._seed = seed if seed is not None else random.random() + + # Defaults in seconds, from human / UX design limits + # http://www.nngroup.com/articles/response-times-3-important-limits/ + # + # These defaults are *NOT* set in stone as we are measuring different + # things, so others feel free to come up with a better yardstick :) + if fast_threshold: + self._fast_threshold = float(fast_threshold) + else: + self._fast_threshold = 8 + if slow_threshold: + self._slow_threshold = float(slow_threshold) + else: + self._slow_threshold = 10 + + def test(self, sort=False, timeout=False, slow=False, + enhance_asserts=False, fail_on_timeout=False): + """ + Runs the tests returning True if all tests pass, otherwise False. + + If sort=False run tests in random order. + """ + if sort: + self._testfiles.sort() + elif slow: + pass + else: + random.seed(self._seed) + random.shuffle(self._testfiles) + self._reporter.start(self._seed) + for f in self._testfiles: + try: + self.test_file(f, sort, timeout, slow, + enhance_asserts, fail_on_timeout) + except KeyboardInterrupt: + print(" interrupted by user") + self._reporter.finish() + raise + return self._reporter.finish() + + def _enhance_asserts(self, source): + from ast import (NodeTransformer, Compare, Name, Store, Load, Tuple, + Assign, BinOp, Str, Mod, Assert, parse, fix_missing_locations) + + ops = {"Eq": '==', "NotEq": '!=', "Lt": '<', "LtE": '<=', + "Gt": '>', "GtE": '>=', "Is": 'is', "IsNot": 'is not', + "In": 'in', "NotIn": 'not in'} + + class Transform(NodeTransformer): + def visit_Assert(self, stmt): + if isinstance(stmt.test, Compare): + compare = stmt.test + values = [compare.left] + compare.comparators + names = [ "_%s" % i for i, _ in enumerate(values) ] + names_store = [ Name(n, Store()) for n in names ] + names_load = [ Name(n, Load()) for n in names ] + target = Tuple(names_store, Store()) + value = Tuple(values, Load()) + assign = Assign([target], value) + new_compare = Compare(names_load[0], compare.ops, names_load[1:]) + msg_format = "\n%s " + "\n%s ".join([ ops[op.__class__.__name__] for op in compare.ops ]) + "\n%s" + msg = BinOp(Str(msg_format), Mod(), Tuple(names_load, Load())) + test = Assert(new_compare, msg, lineno=stmt.lineno, col_offset=stmt.col_offset) + return [assign, test] + else: + return stmt + + tree = parse(source) + new_tree = Transform().visit(tree) + return fix_missing_locations(new_tree) + + def test_file(self, filename, sort=True, timeout=False, slow=False, + enhance_asserts=False, fail_on_timeout=False): + reporter = self._reporter + funcs = [] + try: + gl = {'__file__': filename} + try: + open_file = lambda: open(filename, encoding="utf8") + + with open_file() as f: + source = f.read() + if self._kw: + for l in source.splitlines(): + if l.lstrip().startswith('def '): + if any(l.lower().find(k.lower()) != -1 for k in self._kw): + break + else: + return + + if enhance_asserts: + try: + source = self._enhance_asserts(source) + except ImportError: + pass + + code = compile(source, filename, "exec", flags=0, dont_inherit=True) + exec(code, gl) + except (SystemExit, KeyboardInterrupt): + raise + except ImportError: + reporter.import_error(filename, sys.exc_info()) + return + except Exception: + reporter.test_exception(sys.exc_info()) + + clear_cache() + self._count += 1 + random.seed(self._seed) + disabled = gl.get("disabled", False) + if not disabled: + # we need to filter only those functions that begin with 'test_' + # We have to be careful about decorated functions. As long as + # the decorator uses functools.wraps, we can detect it. + funcs = [] + for f in gl: + if (f.startswith("test_") and (inspect.isfunction(gl[f]) + or inspect.ismethod(gl[f]))): + func = gl[f] + # Handle multiple decorators + while hasattr(func, '__wrapped__'): + func = func.__wrapped__ + + if inspect.getsourcefile(func) == filename: + funcs.append(gl[f]) + if slow: + funcs = [f for f in funcs if getattr(f, '_slow', False)] + # Sorting of XFAILed functions isn't fixed yet :-( + funcs.sort(key=lambda x: inspect.getsourcelines(x)[1]) + i = 0 + while i < len(funcs): + if inspect.isgeneratorfunction(funcs[i]): + # some tests can be generators, that return the actual + # test functions. We unpack it below: + f = funcs.pop(i) + for fg in f(): + func = fg[0] + args = fg[1:] + fgw = lambda: func(*args) + funcs.insert(i, fgw) + i += 1 + else: + i += 1 + # drop functions that are not selected with the keyword expression: + funcs = [x for x in funcs if self.matches(x)] + + if not funcs: + return + except Exception: + reporter.entering_filename(filename, len(funcs)) + raise + + reporter.entering_filename(filename, len(funcs)) + if not sort: + random.shuffle(funcs) + + for f in funcs: + start = time.time() + reporter.entering_test(f) + try: + if getattr(f, '_slow', False) and not slow: + raise Skipped("Slow") + with raise_on_deprecated(): + if timeout: + self._timeout(f, timeout, fail_on_timeout) + else: + random.seed(self._seed) + f() + except KeyboardInterrupt: + if getattr(f, '_slow', False): + reporter.test_skip("KeyboardInterrupt") + else: + raise + except Exception: + if timeout: + signal.alarm(0) # Disable the alarm. It could not be handled before. + t, v, tr = sys.exc_info() + if t is AssertionError: + reporter.test_fail((t, v, tr)) + if self._post_mortem: + pdb.post_mortem(tr) + elif t.__name__ == "Skipped": + reporter.test_skip(v) + elif t.__name__ == "XFail": + reporter.test_xfail() + elif t.__name__ == "XPass": + reporter.test_xpass(v) + else: + reporter.test_exception((t, v, tr)) + if self._post_mortem: + pdb.post_mortem(tr) + else: + reporter.test_pass() + taken = time.time() - start + if taken > self._slow_threshold: + filename = os.path.relpath(filename, reporter._root_dir) + reporter.slow_test_functions.append( + (filename + "::" + f.__name__, taken)) + if getattr(f, '_slow', False) and slow: + if taken < self._fast_threshold: + filename = os.path.relpath(filename, reporter._root_dir) + reporter.fast_test_functions.append( + (filename + "::" + f.__name__, taken)) + reporter.leaving_filename() + + def _timeout(self, function, timeout, fail_on_timeout): + def callback(x, y): + signal.alarm(0) + if fail_on_timeout: + raise TimeOutError("Timed out after %d seconds" % timeout) + else: + raise Skipped("Timeout") + signal.signal(signal.SIGALRM, callback) + signal.alarm(timeout) # Set an alarm with a given timeout + function() + signal.alarm(0) # Disable the alarm + + def matches(self, x): + """ + Does the keyword expression self._kw match "x"? Returns True/False. + + Always returns True if self._kw is "". + """ + if not self._kw: + return True + for kw in self._kw: + if x.__name__.lower().find(kw.lower()) != -1: + return True + return False + + def get_test_files(self, dir, pat='test_*.py'): + """ + Returns the list of test_*.py (default) files at or below directory + ``dir`` relative to the SymPy home directory. + """ + dir = os.path.join(self._root_dir, convert_to_native_paths([dir])[0]) + + g = [] + for path, folders, files in os.walk(dir): + g.extend([os.path.join(path, f) for f in files if fnmatch(f, pat)]) + + return sorted([os.path.normcase(gi) for gi in g]) + + +class SymPyDocTests: + + def __init__(self, reporter, normal): + self._count = 0 + self._root_dir = get_sympy_dir() + self._reporter = reporter + self._reporter.root_dir(self._root_dir) + self._normal = normal + + self._testfiles = [] + + def test(self): + """ + Runs the tests and returns True if all tests pass, otherwise False. + """ + self._reporter.start() + for f in self._testfiles: + try: + self.test_file(f) + except KeyboardInterrupt: + print(" interrupted by user") + self._reporter.finish() + raise + return self._reporter.finish() + + def test_file(self, filename): + clear_cache() + + from io import StringIO + import sympy.interactive.printing as interactive_printing + from sympy.printing.pretty.pretty import pprint_use_unicode + from sympy.printing.pretty import stringpict + + rel_name = filename[len(self._root_dir) + 1:] + dirname, file = os.path.split(filename) + module = rel_name.replace(os.sep, '.')[:-3] + + if rel_name.startswith("examples"): + # Examples files do not have __init__.py files, + # So we have to temporarily extend sys.path to import them + sys.path.insert(0, dirname) + module = file[:-3] # remove ".py" + try: + module = pdoctest._normalize_module(module) + tests = SymPyDocTestFinder().find(module) + except (SystemExit, KeyboardInterrupt): + raise + except ImportError: + self._reporter.import_error(filename, sys.exc_info()) + return + finally: + if rel_name.startswith("examples"): + del sys.path[0] + + tests = [test for test in tests if len(test.examples) > 0] + # By default tests are sorted by alphabetical order by function name. + # We sort by line number so one can edit the file sequentially from + # bottom to top. However, if there are decorated functions, their line + # numbers will be too large and for now one must just search for these + # by text and function name. + tests.sort(key=lambda x: -x.lineno) + + if not tests: + return + self._reporter.entering_filename(filename, len(tests)) + for test in tests: + assert len(test.examples) != 0 + + if self._reporter._verbose: + self._reporter.write("\n{} ".format(test.name)) + + # check if there are external dependencies which need to be met + if '_doctest_depends_on' in test.globs: + try: + self._check_dependencies(**test.globs['_doctest_depends_on']) + except DependencyError as e: + self._reporter.test_skip(v=str(e)) + continue + + runner = SymPyDocTestRunner(verbose=self._reporter._verbose==2, + optionflags=pdoctest.ELLIPSIS | + pdoctest.NORMALIZE_WHITESPACE | + pdoctest.IGNORE_EXCEPTION_DETAIL) + runner._checker = SymPyOutputChecker() + old = sys.stdout + new = old if self._reporter._verbose==2 else StringIO() + sys.stdout = new + # If the testing is normal, the doctests get importing magic to + # provide the global namespace. If not normal (the default) then + # then must run on their own; all imports must be explicit within + # a function's docstring. Once imported that import will be + # available to the rest of the tests in a given function's + # docstring (unless clear_globs=True below). + if not self._normal: + test.globs = {} + # if this is uncommented then all the test would get is what + # comes by default with a "from sympy import *" + #exec('from sympy import *') in test.globs + old_displayhook = sys.displayhook + use_unicode_prev, wrap_line_prev = setup_pprint() + + try: + f, t = runner.run(test, + out=new.write, clear_globs=False) + except KeyboardInterrupt: + raise + finally: + sys.stdout = old + if f > 0: + self._reporter.doctest_fail(test.name, new.getvalue()) + else: + self._reporter.test_pass() + sys.displayhook = old_displayhook + interactive_printing.NO_GLOBAL = False + pprint_use_unicode(use_unicode_prev) + stringpict._GLOBAL_WRAP_LINE = wrap_line_prev + + self._reporter.leaving_filename() + + def get_test_files(self, dir, pat='*.py', init_only=True): + r""" + Returns the list of \*.py files (default) from which docstrings + will be tested which are at or below directory ``dir``. By default, + only those that have an __init__.py in their parent directory + and do not start with ``test_`` will be included. + """ + def importable(x): + """ + Checks if given pathname x is an importable module by checking for + __init__.py file. + + Returns True/False. + + Currently we only test if the __init__.py file exists in the + directory with the file "x" (in theory we should also test all the + parent dirs). + """ + init_py = os.path.join(os.path.dirname(x), "__init__.py") + return os.path.exists(init_py) + + dir = os.path.join(self._root_dir, convert_to_native_paths([dir])[0]) + + g = [] + for path, folders, files in os.walk(dir): + g.extend([os.path.join(path, f) for f in files + if not f.startswith('test_') and fnmatch(f, pat)]) + if init_only: + # skip files that are not importable (i.e. missing __init__.py) + g = [x for x in g if importable(x)] + + return [os.path.normcase(gi) for gi in g] + + def _check_dependencies(self, + executables=(), + modules=(), + disable_viewers=(), + python_version=(3, 5), + ground_types=None): + """ + Checks if the dependencies for the test are installed. + + Raises ``DependencyError`` it at least one dependency is not installed. + """ + + for executable in executables: + if not shutil.which(executable): + raise DependencyError("Could not find %s" % executable) + + for module in modules: + if module == 'matplotlib': + matplotlib = import_module( + 'matplotlib', + import_kwargs={'fromlist': + ['pyplot', 'cm', 'collections']}, + min_module_version='1.0.0', catch=(RuntimeError,)) + if matplotlib is None: + raise DependencyError("Could not import matplotlib") + else: + if not import_module(module): + raise DependencyError("Could not import %s" % module) + + if disable_viewers: + tempdir = tempfile.mkdtemp() + os.environ['PATH'] = '%s:%s' % (tempdir, os.environ['PATH']) + + vw = ('#!/usr/bin/env python3\n' + 'import sys\n' + 'if len(sys.argv) <= 1:\n' + ' exit("wrong number of args")\n') + + for viewer in disable_viewers: + Path(os.path.join(tempdir, viewer)).write_text(vw) + + # make the file executable + os.chmod(os.path.join(tempdir, viewer), + stat.S_IREAD | stat.S_IWRITE | stat.S_IXUSR) + + if python_version: + if sys.version_info < python_version: + raise DependencyError("Requires Python >= " + '.'.join(map(str, python_version))) + + if ground_types is not None: + if GROUND_TYPES not in ground_types: + raise DependencyError("Requires ground_types in " + str(ground_types)) + + if 'pyglet' in modules: + # monkey-patch pyglet s.t. it does not open a window during + # doctesting + import pyglet + class DummyWindow: + def __init__(self, *args, **kwargs): + self.has_exit = True + self.width = 600 + self.height = 400 + + def set_vsync(self, x): + pass + + def switch_to(self): + pass + + def push_handlers(self, x): + pass + + def close(self): + pass + + pyglet.window.Window = DummyWindow + + +class SymPyDocTestFinder(DocTestFinder): + """ + A class used to extract the DocTests that are relevant to a given + object, from its docstring and the docstrings of its contained + objects. Doctests can currently be extracted from the following + object types: modules, functions, classes, methods, staticmethods, + classmethods, and properties. + + Modified from doctest's version to look harder for code that + appears comes from a different module. For example, the @vectorize + decorator makes it look like functions come from multidimensional.py + even though their code exists elsewhere. + """ + + def _find(self, tests, obj, name, module, source_lines, globs, seen): + """ + Find tests for the given object and any contained objects, and + add them to ``tests``. + """ + if self._verbose: + print('Finding tests in %s' % name) + + # If we've already processed this object, then ignore it. + if id(obj) in seen: + return + seen[id(obj)] = 1 + + # Make sure we don't run doctests for classes outside of sympy, such + # as in numpy or scipy. + if inspect.isclass(obj): + if obj.__module__.split('.')[0] != 'sympy': + return + + # Find a test for this object, and add it to the list of tests. + test = self._get_test(obj, name, module, globs, source_lines) + if test is not None: + tests.append(test) + + if not self._recurse: + return + + # Look for tests in a module's contained objects. + if inspect.ismodule(obj): + for rawname, val in obj.__dict__.items(): + # Recurse to functions & classes. + if inspect.isfunction(val) or inspect.isclass(val): + # Make sure we don't run doctests functions or classes + # from different modules + if val.__module__ != module.__name__: + continue + + assert self._from_module(module, val), \ + "%s is not in module %s (rawname %s)" % (val, module, rawname) + + try: + valname = '%s.%s' % (name, rawname) + self._find(tests, val, valname, module, + source_lines, globs, seen) + except KeyboardInterrupt: + raise + + # Look for tests in a module's __test__ dictionary. + for valname, val in getattr(obj, '__test__', {}).items(): + if not isinstance(valname, str): + raise ValueError("SymPyDocTestFinder.find: __test__ keys " + "must be strings: %r" % + (type(valname),)) + if not (inspect.isfunction(val) or inspect.isclass(val) or + inspect.ismethod(val) or inspect.ismodule(val) or + isinstance(val, str)): + raise ValueError("SymPyDocTestFinder.find: __test__ values " + "must be strings, functions, methods, " + "classes, or modules: %r" % + (type(val),)) + valname = '%s.__test__.%s' % (name, valname) + self._find(tests, val, valname, module, source_lines, + globs, seen) + + + # Look for tests in a class's contained objects. + if inspect.isclass(obj): + for valname, val in obj.__dict__.items(): + # Special handling for staticmethod/classmethod. + if isinstance(val, staticmethod): + val = getattr(obj, valname) + if isinstance(val, classmethod): + val = getattr(obj, valname).__func__ + + + # Recurse to methods, properties, and nested classes. + if ((inspect.isfunction(unwrap(val)) or + inspect.isclass(val) or + isinstance(val, property)) and + self._from_module(module, val)): + # Make sure we don't run doctests functions or classes + # from different modules + if isinstance(val, property): + if hasattr(val.fget, '__module__'): + if val.fget.__module__ != module.__name__: + continue + else: + if val.__module__ != module.__name__: + continue + + assert self._from_module(module, val), \ + "%s is not in module %s (valname %s)" % ( + val, module, valname) + + valname = '%s.%s' % (name, valname) + self._find(tests, val, valname, module, source_lines, + globs, seen) + + def _get_test(self, obj, name, module, globs, source_lines): + """ + Return a DocTest for the given object, if it defines a docstring; + otherwise, return None. + """ + + lineno = None + + # Extract the object's docstring. If it does not have one, + # then return None (no test for this object). + if isinstance(obj, str): + # obj is a string in the case for objects in the polys package. + # Note that source_lines is a binary string (compiled polys + # modules), which can't be handled by _find_lineno so determine + # the line number here. + + docstring = obj + + matches = re.findall(r"line \d+", name) + assert len(matches) == 1, \ + "string '%s' does not contain lineno " % name + + # NOTE: this is not the exact linenumber but its better than no + # lineno ;) + lineno = int(matches[0][5:]) + + else: + docstring = getattr(obj, '__doc__', '') + if docstring is None: + docstring = '' + if not isinstance(docstring, str): + docstring = str(docstring) + + # Don't bother if the docstring is empty. + if self._exclude_empty and not docstring: + return None + + # check that properties have a docstring because _find_lineno + # assumes it + if isinstance(obj, property): + if obj.fget.__doc__ is None: + return None + + # Find the docstring's location in the file. + if lineno is None: + obj = unwrap(obj) + # handling of properties is not implemented in _find_lineno so do + # it here + if hasattr(obj, 'func_closure') and obj.func_closure is not None: + tobj = obj.func_closure[0].cell_contents + elif isinstance(obj, property): + tobj = obj.fget + else: + tobj = obj + lineno = self._find_lineno(tobj, source_lines) + + if lineno is None: + return None + + # Return a DocTest for this object. + if module is None: + filename = None + else: + filename = getattr(module, '__file__', module.__name__) + if filename[-4:] in (".pyc", ".pyo"): + filename = filename[:-1] + + globs['_doctest_depends_on'] = getattr(obj, '_doctest_depends_on', {}) + + return self._parser.get_doctest(docstring, globs, name, + filename, lineno) + + +class SymPyDocTestRunner(DocTestRunner): + """ + A class used to run DocTest test cases, and accumulate statistics. + The ``run`` method is used to process a single DocTest case. It + returns a tuple ``(f, t)``, where ``t`` is the number of test cases + tried, and ``f`` is the number of test cases that failed. + + Modified from the doctest version to not reset the sys.displayhook (see + issue 5140). + + See the docstring of the original DocTestRunner for more information. + """ + + def run(self, test, compileflags=None, out=None, clear_globs=True): + """ + Run the examples in ``test``, and display the results using the + writer function ``out``. + + The examples are run in the namespace ``test.globs``. If + ``clear_globs`` is true (the default), then this namespace will + be cleared after the test runs, to help with garbage + collection. If you would like to examine the namespace after + the test completes, then use ``clear_globs=False``. + + ``compileflags`` gives the set of flags that should be used by + the Python compiler when running the examples. If not + specified, then it will default to the set of future-import + flags that apply to ``globs``. + + The output of each example is checked using + ``SymPyDocTestRunner.check_output``, and the results are + formatted by the ``SymPyDocTestRunner.report_*`` methods. + """ + self.test = test + + # Remove ``` from the end of example, which may appear in Markdown + # files + for example in test.examples: + example.want = example.want.replace('```\n', '') + example.exc_msg = example.exc_msg and example.exc_msg.replace('```\n', '') + + + if compileflags is None: + compileflags = pdoctest._extract_future_flags(test.globs) + + save_stdout = sys.stdout + if out is None: + out = save_stdout.write + sys.stdout = self._fakeout + + # Patch pdb.set_trace to restore sys.stdout during interactive + # debugging (so it's not still redirected to self._fakeout). + # Note that the interactive output will go to *our* + # save_stdout, even if that's not the real sys.stdout; this + # allows us to write test cases for the set_trace behavior. + save_set_trace = pdb.set_trace + self.debugger = pdoctest._OutputRedirectingPdb(save_stdout) + self.debugger.reset() + pdb.set_trace = self.debugger.set_trace + + # Patch linecache.getlines, so we can see the example's source + # when we're inside the debugger. + self.save_linecache_getlines = pdoctest.linecache.getlines + linecache.getlines = self.__patched_linecache_getlines + + # Fail for deprecation warnings + with raise_on_deprecated(): + try: + return self.__run(test, compileflags, out) + finally: + sys.stdout = save_stdout + pdb.set_trace = save_set_trace + linecache.getlines = self.save_linecache_getlines + if clear_globs: + test.globs.clear() + + +# We have to override the name mangled methods. +monkeypatched_methods = [ + 'patched_linecache_getlines', + 'run', + 'record_outcome' +] +for method in monkeypatched_methods: + oldname = '_DocTestRunner__' + method + newname = '_SymPyDocTestRunner__' + method + setattr(SymPyDocTestRunner, newname, getattr(DocTestRunner, oldname)) + + +class SymPyOutputChecker(pdoctest.OutputChecker): + """ + Compared to the OutputChecker from the stdlib our OutputChecker class + supports numerical comparison of floats occurring in the output of the + doctest examples + """ + + def __init__(self): + # NOTE OutputChecker is an old-style class with no __init__ method, + # so we can't call the base class version of __init__ here + + got_floats = r'(\d+\.\d*|\.\d+)' + + # floats in the 'want' string may contain ellipses + want_floats = got_floats + r'(\.{3})?' + + front_sep = r'\s|\+|\-|\*|,' + back_sep = front_sep + r'|j|e' + + fbeg = r'^%s(?=%s|$)' % (got_floats, back_sep) + fmidend = r'(?<=%s)%s(?=%s|$)' % (front_sep, got_floats, back_sep) + self.num_got_rgx = re.compile(r'(%s|%s)' %(fbeg, fmidend)) + + fbeg = r'^%s(?=%s|$)' % (want_floats, back_sep) + fmidend = r'(?<=%s)%s(?=%s|$)' % (front_sep, want_floats, back_sep) + self.num_want_rgx = re.compile(r'(%s|%s)' %(fbeg, fmidend)) + + def check_output(self, want, got, optionflags): + """ + Return True iff the actual output from an example (`got`) + matches the expected output (`want`). These strings are + always considered to match if they are identical; but + depending on what option flags the test runner is using, + several non-exact match types are also possible. See the + documentation for `TestRunner` for more information about + option flags. + """ + # Handle the common case first, for efficiency: + # if they're string-identical, always return true. + if got == want: + return True + + # TODO parse integers as well ? + # Parse floats and compare them. If some of the parsed floats contain + # ellipses, skip the comparison. + matches = self.num_got_rgx.finditer(got) + numbers_got = [match.group(1) for match in matches] # list of strs + matches = self.num_want_rgx.finditer(want) + numbers_want = [match.group(1) for match in matches] # list of strs + if len(numbers_got) != len(numbers_want): + return False + + if len(numbers_got) > 0: + nw_ = [] + for ng, nw in zip(numbers_got, numbers_want): + if '...' in nw: + nw_.append(ng) + continue + else: + nw_.append(nw) + + if abs(float(ng)-float(nw)) > 1e-5: + return False + + got = self.num_got_rgx.sub(r'%s', got) + got = got % tuple(nw_) + + # can be used as a special sequence to signify a + # blank line, unless the DONT_ACCEPT_BLANKLINE flag is used. + if not (optionflags & pdoctest.DONT_ACCEPT_BLANKLINE): + # Replace in want with a blank line. + want = re.sub(r'(?m)^%s\s*?$' % re.escape(pdoctest.BLANKLINE_MARKER), + '', want) + # If a line in got contains only spaces, then remove the + # spaces. + got = re.sub(r'(?m)^\s*?$', '', got) + if got == want: + return True + + # This flag causes doctest to ignore any differences in the + # contents of whitespace strings. Note that this can be used + # in conjunction with the ELLIPSIS flag. + if optionflags & pdoctest.NORMALIZE_WHITESPACE: + got = ' '.join(got.split()) + want = ' '.join(want.split()) + if got == want: + return True + + # The ELLIPSIS flag says to let the sequence "..." in `want` + # match any substring in `got`. + if optionflags & pdoctest.ELLIPSIS: + if pdoctest._ellipsis_match(want, got): + return True + + # We didn't find any match; return false. + return False + + +class Reporter: + """ + Parent class for all reporters. + """ + pass + + +class PyTestReporter(Reporter): + """ + Py.test like reporter. Should produce output identical to py.test. + """ + + def __init__(self, verbose=False, tb="short", colors=True, + force_colors=False, split=None): + self._verbose = verbose + self._tb_style = tb + self._colors = colors + self._force_colors = force_colors + self._xfailed = 0 + self._xpassed = [] + self._failed = [] + self._failed_doctest = [] + self._passed = 0 + self._skipped = 0 + self._exceptions = [] + self._terminal_width = None + self._default_width = 80 + self._split = split + self._active_file = '' + self._active_f = None + + # TODO: Should these be protected? + self.slow_test_functions = [] + self.fast_test_functions = [] + + # this tracks the x-position of the cursor (useful for positioning + # things on the screen), without the need for any readline library: + self._write_pos = 0 + self._line_wrap = False + + def root_dir(self, dir): + self._root_dir = dir + + @property + def terminal_width(self): + if self._terminal_width is not None: + return self._terminal_width + + def findout_terminal_width(): + if sys.platform == "win32": + # Windows support is based on: + # + # http://code.activestate.com/recipes/ + # 440694-determine-size-of-console-window-on-windows/ + + from ctypes import windll, create_string_buffer + + h = windll.kernel32.GetStdHandle(-12) + csbi = create_string_buffer(22) + res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) + + if res: + import struct + (_, _, _, _, _, left, _, right, _, _, _) = \ + struct.unpack("hhhhHhhhhhh", csbi.raw) + return right - left + else: + return self._default_width + + if hasattr(sys.stdout, 'isatty') and not sys.stdout.isatty(): + return self._default_width # leave PIPEs alone + + try: + process = subprocess.Popen(['stty', '-a'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + stdout = stdout.decode("utf-8") + except OSError: + pass + else: + # We support the following output formats from stty: + # + # 1) Linux -> columns 80 + # 2) OS X -> 80 columns + # 3) Solaris -> columns = 80 + + re_linux = r"columns\s+(?P\d+);" + re_osx = r"(?P\d+)\s*columns;" + re_solaris = r"columns\s+=\s+(?P\d+);" + + for regex in (re_linux, re_osx, re_solaris): + match = re.search(regex, stdout) + + if match is not None: + columns = match.group('columns') + + try: + width = int(columns) + except ValueError: + pass + if width != 0: + return width + + return self._default_width + + width = findout_terminal_width() + self._terminal_width = width + + return width + + def write(self, text, color="", align="left", width=None, + force_colors=False): + """ + Prints a text on the screen. + + It uses sys.stdout.write(), so no readline library is necessary. + + Parameters + ========== + + color : choose from the colors below, "" means default color + align : "left"/"right", "left" is a normal print, "right" is aligned on + the right-hand side of the screen, filled with spaces if + necessary + width : the screen width + + """ + color_templates = ( + ("Black", "0;30"), + ("Red", "0;31"), + ("Green", "0;32"), + ("Brown", "0;33"), + ("Blue", "0;34"), + ("Purple", "0;35"), + ("Cyan", "0;36"), + ("LightGray", "0;37"), + ("DarkGray", "1;30"), + ("LightRed", "1;31"), + ("LightGreen", "1;32"), + ("Yellow", "1;33"), + ("LightBlue", "1;34"), + ("LightPurple", "1;35"), + ("LightCyan", "1;36"), + ("White", "1;37"), + ) + + colors = {} + + for name, value in color_templates: + colors[name] = value + c_normal = '\033[0m' + c_color = '\033[%sm' + + if width is None: + width = self.terminal_width + + if align == "right": + if self._write_pos + len(text) > width: + # we don't fit on the current line, create a new line + self.write("\n") + self.write(" "*(width - self._write_pos - len(text))) + + if not self._force_colors and hasattr(sys.stdout, 'isatty') and not \ + sys.stdout.isatty(): + # the stdout is not a terminal, this for example happens if the + # output is piped to less, e.g. "bin/test | less". In this case, + # the terminal control sequences would be printed verbatim, so + # don't use any colors. + color = "" + elif sys.platform == "win32": + # Windows consoles don't support ANSI escape sequences + color = "" + elif not self._colors: + color = "" + + if self._line_wrap: + if text[0] != "\n": + sys.stdout.write("\n") + + # Avoid UnicodeEncodeError when printing out test failures + if IS_WINDOWS: + text = text.encode('raw_unicode_escape').decode('utf8', 'ignore') + elif not sys.stdout.encoding.lower().startswith('utf'): + text = text.encode(sys.stdout.encoding, 'backslashreplace' + ).decode(sys.stdout.encoding) + + if color == "": + sys.stdout.write(text) + else: + sys.stdout.write("%s%s%s" % + (c_color % colors[color], text, c_normal)) + sys.stdout.flush() + l = text.rfind("\n") + if l == -1: + self._write_pos += len(text) + else: + self._write_pos = len(text) - l - 1 + self._line_wrap = self._write_pos >= width + self._write_pos %= width + + def write_center(self, text, delim="="): + width = self.terminal_width + if text != "": + text = " %s " % text + idx = (width - len(text)) // 2 + t = delim*idx + text + delim*(width - idx - len(text)) + self.write(t + "\n") + + def write_exception(self, e, val, tb): + # remove the first item, as that is always runtests.py + tb = tb.tb_next + t = traceback.format_exception(e, val, tb) + self.write("".join(t)) + + def start(self, seed=None, msg="test process starts"): + self.write_center(msg) + executable = sys.executable + v = tuple(sys.version_info) + python_version = "%s.%s.%s-%s-%s" % v + implementation = platform.python_implementation() + if implementation == 'PyPy': + implementation += " %s.%s.%s-%s-%s" % sys.pypy_version_info + self.write("executable: %s (%s) [%s]\n" % + (executable, python_version, implementation)) + from sympy.utilities.misc import ARCH + self.write("architecture: %s\n" % ARCH) + from sympy.core.cache import USE_CACHE + self.write("cache: %s\n" % USE_CACHE) + version = '' + if GROUND_TYPES =='gmpy': + import gmpy2 as gmpy + version = gmpy.version() + self.write("ground types: %s %s\n" % (GROUND_TYPES, version)) + numpy = import_module('numpy') + self.write("numpy: %s\n" % (None if not numpy else numpy.__version__)) + if seed is not None: + self.write("random seed: %d\n" % seed) + from sympy.utilities.misc import HASH_RANDOMIZATION + self.write("hash randomization: ") + hash_seed = os.getenv("PYTHONHASHSEED") or '0' + if HASH_RANDOMIZATION and (hash_seed == "random" or int(hash_seed)): + self.write("on (PYTHONHASHSEED=%s)\n" % hash_seed) + else: + self.write("off\n") + if self._split: + self.write("split: %s\n" % self._split) + self.write('\n') + self._t_start = clock() + + def finish(self): + self._t_end = clock() + self.write("\n") + global text, linelen + text = "tests finished: %d passed, " % self._passed + linelen = len(text) + + def add_text(mytext): + global text, linelen + """Break new text if too long.""" + if linelen + len(mytext) > self.terminal_width: + text += '\n' + linelen = 0 + text += mytext + linelen += len(mytext) + + if len(self._failed) > 0: + add_text("%d failed, " % len(self._failed)) + if len(self._failed_doctest) > 0: + add_text("%d failed, " % len(self._failed_doctest)) + if self._skipped > 0: + add_text("%d skipped, " % self._skipped) + if self._xfailed > 0: + add_text("%d expected to fail, " % self._xfailed) + if len(self._xpassed) > 0: + add_text("%d expected to fail but passed, " % len(self._xpassed)) + if len(self._exceptions) > 0: + add_text("%d exceptions, " % len(self._exceptions)) + add_text("in %.2f seconds" % (self._t_end - self._t_start)) + + if self.slow_test_functions: + self.write_center('slowest tests', '_') + sorted_slow = sorted(self.slow_test_functions, key=lambda r: r[1]) + for slow_func_name, taken in sorted_slow: + print('%s - Took %.3f seconds' % (slow_func_name, taken)) + + if self.fast_test_functions: + self.write_center('unexpectedly fast tests', '_') + sorted_fast = sorted(self.fast_test_functions, + key=lambda r: r[1]) + for fast_func_name, taken in sorted_fast: + print('%s - Took %.3f seconds' % (fast_func_name, taken)) + + if len(self._xpassed) > 0: + self.write_center("xpassed tests", "_") + for e in self._xpassed: + self.write("%s: %s\n" % (e[0], e[1])) + self.write("\n") + + if self._tb_style != "no" and len(self._exceptions) > 0: + for e in self._exceptions: + filename, f, (t, val, tb) = e + self.write_center("", "_") + if f is None: + s = "%s" % filename + else: + s = "%s:%s" % (filename, f.__name__) + self.write_center(s, "_") + self.write_exception(t, val, tb) + self.write("\n") + + if self._tb_style != "no" and len(self._failed) > 0: + for e in self._failed: + filename, f, (t, val, tb) = e + self.write_center("", "_") + self.write_center("%s::%s" % (filename, f.__name__), "_") + self.write_exception(t, val, tb) + self.write("\n") + + if self._tb_style != "no" and len(self._failed_doctest) > 0: + for e in self._failed_doctest: + filename, msg = e + self.write_center("", "_") + self.write_center("%s" % filename, "_") + self.write(msg) + self.write("\n") + + self.write_center(text) + ok = len(self._failed) == 0 and len(self._exceptions) == 0 and \ + len(self._failed_doctest) == 0 + if not ok: + self.write("DO *NOT* COMMIT!\n") + return ok + + def entering_filename(self, filename, n): + rel_name = filename[len(self._root_dir) + 1:] + self._active_file = rel_name + self._active_file_error = False + self.write(rel_name) + self.write("[%d] " % n) + + def leaving_filename(self): + self.write(" ") + if self._active_file_error: + self.write("[FAIL]", "Red", align="right") + else: + self.write("[OK]", "Green", align="right") + self.write("\n") + if self._verbose: + self.write("\n") + + def entering_test(self, f): + self._active_f = f + if self._verbose: + self.write("\n" + f.__name__ + " ") + + def test_xfail(self): + self._xfailed += 1 + self.write("f", "Green") + + def test_xpass(self, v): + message = str(v) + self._xpassed.append((self._active_file, message)) + self.write("X", "Green") + + def test_fail(self, exc_info): + self._failed.append((self._active_file, self._active_f, exc_info)) + self.write("F", "Red") + self._active_file_error = True + + def doctest_fail(self, name, error_msg): + # the first line contains "******", remove it: + error_msg = "\n".join(error_msg.split("\n")[1:]) + self._failed_doctest.append((name, error_msg)) + self.write("F", "Red") + self._active_file_error = True + + def test_pass(self, char="."): + self._passed += 1 + if self._verbose: + self.write("ok", "Green") + else: + self.write(char, "Green") + + def test_skip(self, v=None): + char = "s" + self._skipped += 1 + if v is not None: + message = str(v) + if message == "KeyboardInterrupt": + char = "K" + elif message == "Timeout": + char = "T" + elif message == "Slow": + char = "w" + if self._verbose: + if v is not None: + self.write(message + ' ', "Blue") + else: + self.write(" - ", "Blue") + self.write(char, "Blue") + + def test_exception(self, exc_info): + self._exceptions.append((self._active_file, self._active_f, exc_info)) + if exc_info[0] is TimeOutError: + self.write("T", "Red") + else: + self.write("E", "Red") + self._active_file_error = True + + def import_error(self, filename, exc_info): + self._exceptions.append((filename, None, exc_info)) + rel_name = filename[len(self._root_dir) + 1:] + self.write(rel_name) + self.write("[?] Failed to import", "Red") + self.write(" ") + self.write("[FAIL]", "Red", align="right") + self.write("\n") diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/runtests_pytest.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/runtests_pytest.py new file mode 100644 index 0000000000000000000000000000000000000000..635f27864ca86571128e6c9a055199dfbde1ed63 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/runtests_pytest.py @@ -0,0 +1,461 @@ +"""Backwards compatible functions for running tests from SymPy using pytest. + +SymPy historically had its own testing framework that aimed to: +- be compatible with pytest; +- operate similarly (or identically) to pytest; +- not require any external dependencies; +- have all the functionality in one file only; +- have no magic, just import the test file and execute the test functions; and +- be portable. + +To reduce the maintenance burden of developing an independent testing framework +and to leverage the benefits of existing Python testing infrastructure, SymPy +now uses pytest (and various of its plugins) to run the test suite. + +To maintain backwards compatibility with the legacy testing interface of SymPy, +which implemented functions that allowed users to run the tests on their +installed version of SymPy, the functions in this module are implemented to +match the existing API while thinly wrapping pytest. + +These two key functions are `test` and `doctest`. + +""" + +import functools +import importlib.util +import os +import pathlib +import re +from fnmatch import fnmatch +from typing import List, Optional, Tuple + +try: + import pytest +except ImportError: + + class NoPytestError(Exception): + """Raise when an internal test helper function is called with pytest.""" + + class pytest: # type: ignore + """Shadow to support pytest features when pytest can't be imported.""" + + @staticmethod + def main(*args, **kwargs): + msg = 'pytest must be installed to run tests via this function' + raise NoPytestError(msg) + +from sympy.testing.runtests import test as test_sympy + + +TESTPATHS_DEFAULT = ( + pathlib.Path('sympy'), + pathlib.Path('doc', 'src'), +) +BLACKLIST_DEFAULT = ( + 'sympy/integrals/rubi/rubi_tests/tests', +) + + +class PytestPluginManager: + """Module names for pytest plugins used by SymPy.""" + PYTEST: str = 'pytest' + RANDOMLY: str = 'pytest_randomly' + SPLIT: str = 'pytest_split' + TIMEOUT: str = 'pytest_timeout' + XDIST: str = 'xdist' + + @functools.cached_property + def has_pytest(self) -> bool: + return bool(importlib.util.find_spec(self.PYTEST)) + + @functools.cached_property + def has_randomly(self) -> bool: + return bool(importlib.util.find_spec(self.RANDOMLY)) + + @functools.cached_property + def has_split(self) -> bool: + return bool(importlib.util.find_spec(self.SPLIT)) + + @functools.cached_property + def has_timeout(self) -> bool: + return bool(importlib.util.find_spec(self.TIMEOUT)) + + @functools.cached_property + def has_xdist(self) -> bool: + return bool(importlib.util.find_spec(self.XDIST)) + + +split_pattern = re.compile(r'([1-9][0-9]*)/([1-9][0-9]*)') + + +@functools.lru_cache +def sympy_dir() -> pathlib.Path: + """Returns the root SymPy directory.""" + return pathlib.Path(__file__).parents[2] + + +def update_args_with_paths( + paths: List[str], + keywords: Optional[Tuple[str]], + args: List[str], +) -> List[str]: + """Appends valid paths and flags to the args `list` passed to `pytest.main`. + + The are three different types of "path" that a user may pass to the `paths` + positional arguments, all of which need to be handled slightly differently: + + 1. Nothing is passed + The paths to the `testpaths` defined in `pytest.ini` need to be appended + to the arguments list. + 2. Full, valid paths are passed + These paths need to be validated but can then be directly appended to + the arguments list. + 3. Partial paths are passed. + The `testpaths` defined in `pytest.ini` need to be recursed and any + matches be appended to the arguments list. + + """ + + def find_paths_matching_partial(partial_paths): + partial_path_file_patterns = [] + for partial_path in partial_paths: + if len(partial_path) >= 4: + has_test_prefix = partial_path[:4] == 'test' + has_py_suffix = partial_path[-3:] == '.py' + elif len(partial_path) >= 3: + has_test_prefix = False + has_py_suffix = partial_path[-3:] == '.py' + else: + has_test_prefix = False + has_py_suffix = False + if has_test_prefix and has_py_suffix: + partial_path_file_patterns.append(partial_path) + elif has_test_prefix: + partial_path_file_patterns.append(f'{partial_path}*.py') + elif has_py_suffix: + partial_path_file_patterns.append(f'test*{partial_path}') + else: + partial_path_file_patterns.append(f'test*{partial_path}*.py') + matches = [] + for testpath in valid_testpaths_default: + for path, dirs, files in os.walk(testpath, topdown=True): + zipped = zip(partial_paths, partial_path_file_patterns) + for (partial_path, partial_path_file) in zipped: + if fnmatch(path, f'*{partial_path}*'): + matches.append(str(pathlib.Path(path))) + dirs[:] = [] + else: + for file in files: + if fnmatch(file, partial_path_file): + matches.append(str(pathlib.Path(path, file))) + return matches + + def is_tests_file(filepath: str) -> bool: + path = pathlib.Path(filepath) + if not path.is_file(): + return False + if not path.parts[-1].startswith('test_'): + return False + if not path.suffix == '.py': + return False + return True + + def find_tests_matching_keywords(keywords, filepath): + matches = [] + source = pathlib.Path(filepath).read_text(encoding='utf-8') + for line in source.splitlines(): + if line.lstrip().startswith('def '): + for kw in keywords: + if line.lower().find(kw.lower()) != -1: + test_name = line.split(' ')[1].split('(')[0] + full_test_path = filepath + '::' + test_name + matches.append(full_test_path) + return matches + + valid_testpaths_default = [] + for testpath in TESTPATHS_DEFAULT: + absolute_testpath = pathlib.Path(sympy_dir(), testpath) + if absolute_testpath.exists(): + valid_testpaths_default.append(str(absolute_testpath)) + + candidate_paths = [] + if paths: + full_paths = [] + partial_paths = [] + for path in paths: + if pathlib.Path(path).exists(): + full_paths.append(str(pathlib.Path(sympy_dir(), path))) + else: + partial_paths.append(path) + matched_paths = find_paths_matching_partial(partial_paths) + candidate_paths.extend(full_paths) + candidate_paths.extend(matched_paths) + else: + candidate_paths.extend(valid_testpaths_default) + + if keywords is not None and keywords != (): + matches = [] + for path in candidate_paths: + if is_tests_file(path): + test_matches = find_tests_matching_keywords(keywords, path) + matches.extend(test_matches) + else: + for root, dirnames, filenames in os.walk(path): + for filename in filenames: + absolute_filepath = str(pathlib.Path(root, filename)) + if is_tests_file(absolute_filepath): + test_matches = find_tests_matching_keywords( + keywords, + absolute_filepath, + ) + matches.extend(test_matches) + args.extend(matches) + else: + args.extend(candidate_paths) + + return args + + +def make_absolute_path(partial_path: str) -> str: + """Convert a partial path to an absolute path. + + A path such a `sympy/core` might be needed. However, absolute paths should + be used in the arguments to pytest in all cases as it avoids errors that + arise from nonexistent paths. + + This function assumes that partial_paths will be passed in such that they + begin with the explicit `sympy` directory, i.e. `sympy/...`. + + """ + + def is_valid_partial_path(partial_path: str) -> bool: + """Assumption that partial paths are defined from the `sympy` root.""" + return pathlib.Path(partial_path).parts[0] == 'sympy' + + if not is_valid_partial_path(partial_path): + msg = ( + f'Partial path {dir(partial_path)} is invalid, partial paths are ' + f'expected to be defined with the `sympy` directory as the root.' + ) + raise ValueError(msg) + + absolute_path = str(pathlib.Path(sympy_dir(), partial_path)) + return absolute_path + + +def test(*paths, subprocess=True, rerun=0, **kwargs): + """Interface to run tests via pytest compatible with SymPy's test runner. + + Explanation + =========== + + Note that a `pytest.ExitCode`, which is an `enum`, is returned. This is + different to the legacy SymPy test runner which would return a `bool`. If + all tests successfully pass the `pytest.ExitCode.OK` with value `0` is + returned, whereas the legacy SymPy test runner would return `True`. In any + other scenario, a non-zero `enum` value is returned, whereas the legacy + SymPy test runner would return `False`. Users need to, therefore, be careful + if treating the pytest exit codes as booleans because + `bool(pytest.ExitCode.OK)` evaluates to `False`, the opposite of legacy + behaviour. + + Examples + ======== + + >>> import sympy # doctest: +SKIP + + Run one file: + + >>> sympy.test('sympy/core/tests/test_basic.py') # doctest: +SKIP + >>> sympy.test('_basic') # doctest: +SKIP + + Run all tests in sympy/functions/ and some particular file: + + >>> sympy.test("sympy/core/tests/test_basic.py", + ... "sympy/functions") # doctest: +SKIP + + Run all tests in sympy/core and sympy/utilities: + + >>> sympy.test("/core", "/util") # doctest: +SKIP + + Run specific test from a file: + + >>> sympy.test("sympy/core/tests/test_basic.py", + ... kw="test_equality") # doctest: +SKIP + + Run specific test from any file: + + >>> sympy.test(kw="subs") # doctest: +SKIP + + Run the tests using the legacy SymPy runner: + + >>> sympy.test(use_sympy_runner=True) # doctest: +SKIP + + Note that this option is slated for deprecation in the near future and is + only currently provided to ensure users have an alternative option while the + pytest-based runner receives real-world testing. + + Parameters + ========== + paths : first n positional arguments of strings + Paths, both partial and absolute, describing which subset(s) of the test + suite are to be run. + subprocess : bool, default is True + Legacy option, is currently ignored. + rerun : int, default is 0 + Legacy option, is ignored. + use_sympy_runner : bool or None, default is None + Temporary option to invoke the legacy SymPy test runner instead of + `pytest.main`. Will be removed in the near future. + verbose : bool, default is False + Sets the verbosity of the pytest output. Using `True` will add the + `--verbose` option to the pytest call. + tb : str, 'auto', 'long', 'short', 'line', 'native', or 'no' + Sets the traceback print mode of pytest using the `--tb` option. + kw : str + Only run tests which match the given substring expression. An expression + is a Python evaluatable expression where all names are substring-matched + against test names and their parent classes. Example: -k 'test_method or + test_other' matches all test functions and classes whose name contains + 'test_method' or 'test_other', while -k 'not test_method' matches those + that don't contain 'test_method' in their names. -k 'not test_method and + not test_other' will eliminate the matches. Additionally keywords are + matched to classes and functions containing extra names in their + 'extra_keyword_matches' set, as well as functions which have names + assigned directly to them. The matching is case-insensitive. + pdb : bool, default is False + Start the interactive Python debugger on errors or `KeyboardInterrupt`. + colors : bool, default is True + Color terminal output. + force_colors : bool, default is False + Legacy option, is ignored. + sort : bool, default is True + Run the tests in sorted order. pytest uses a sorted test order by + default. Requires pytest-randomly. + seed : int + Seed to use for random number generation. Requires pytest-randomly. + timeout : int, default is 0 + Timeout in seconds before dumping the stacks. 0 means no timeout. + Requires pytest-timeout. + fail_on_timeout : bool, default is False + Legacy option, is currently ignored. + slow : bool, default is False + Run the subset of tests marked as `slow`. + enhance_asserts : bool, default is False + Legacy option, is currently ignored. + split : string in form `/` or None, default is None + Used to split the tests up. As an example, if `split='2/3' is used then + only the middle third of tests are run. Requires pytest-split. + time_balance : bool, default is True + Legacy option, is currently ignored. + blacklist : iterable of test paths as strings, default is BLACKLIST_DEFAULT + Blacklisted test paths are ignored using the `--ignore` option. Paths + may be partial or absolute. If partial then they are matched against + all paths in the pytest tests path. + parallel : bool, default is False + Parallelize the test running using pytest-xdist. If `True` then pytest + will automatically detect the number of CPU cores available and use them + all. Requires pytest-xdist. + store_durations : bool, False + Store test durations into the file `.test_durations`. The is used by + `pytest-split` to help determine more even splits when more than one + test group is being used. Requires pytest-split. + + """ + # NOTE: to be removed alongside SymPy test runner + if kwargs.get('use_sympy_runner', False): + kwargs.pop('parallel', False) + kwargs.pop('store_durations', False) + kwargs.pop('use_sympy_runner', True) + if kwargs.get('slow') is None: + kwargs['slow'] = False + return test_sympy(*paths, subprocess=True, rerun=0, **kwargs) + + pytest_plugin_manager = PytestPluginManager() + if not pytest_plugin_manager.has_pytest: + pytest.main() + + args = [] + + if kwargs.get('verbose', False): + args.append('--verbose') + + if tb := kwargs.get('tb'): + args.extend(['--tb', tb]) + + if kwargs.get('pdb'): + args.append('--pdb') + + if not kwargs.get('colors', True): + args.extend(['--color', 'no']) + + if seed := kwargs.get('seed'): + if not pytest_plugin_manager.has_randomly: + msg = '`pytest-randomly` plugin required to control random seed.' + raise ModuleNotFoundError(msg) + args.extend(['--randomly-seed', str(seed)]) + + if kwargs.get('sort', True) and pytest_plugin_manager.has_randomly: + args.append('--randomly-dont-reorganize') + elif not kwargs.get('sort', True) and not pytest_plugin_manager.has_randomly: + msg = '`pytest-randomly` plugin required to randomize test order.' + raise ModuleNotFoundError(msg) + + if timeout := kwargs.get('timeout', None): + if not pytest_plugin_manager.has_timeout: + msg = '`pytest-timeout` plugin required to apply timeout to tests.' + raise ModuleNotFoundError(msg) + args.extend(['--timeout', str(int(timeout))]) + + # Skip slow tests by default and always skip tooslow tests + if kwargs.get('slow', False): + args.extend(['-m', 'slow and not tooslow']) + else: + args.extend(['-m', 'not slow and not tooslow']) + + if (split := kwargs.get('split')) is not None: + if not pytest_plugin_manager.has_split: + msg = '`pytest-split` plugin required to run tests as groups.' + raise ModuleNotFoundError(msg) + match = split_pattern.match(split) + if not match: + msg = ('split must be a string of the form a/b where a and b are ' + 'positive nonzero ints') + raise ValueError(msg) + group, splits = map(str, match.groups()) + args.extend(['--group', group, '--splits', splits]) + if group > splits: + msg = (f'cannot have a group number {group} with only {splits} ' + 'splits') + raise ValueError(msg) + + if blacklist := kwargs.get('blacklist', BLACKLIST_DEFAULT): + for path in blacklist: + args.extend(['--ignore', make_absolute_path(path)]) + + if kwargs.get('parallel', False): + if not pytest_plugin_manager.has_xdist: + msg = '`pytest-xdist` plugin required to run tests in parallel.' + raise ModuleNotFoundError(msg) + args.extend(['-n', 'auto']) + + if kwargs.get('store_durations', False): + if not pytest_plugin_manager.has_split: + msg = '`pytest-split` plugin required to store test durations.' + raise ModuleNotFoundError(msg) + args.append('--store-durations') + + if (keywords := kwargs.get('kw')) is not None: + keywords = tuple(str(kw) for kw in keywords) + else: + keywords = () + + args = update_args_with_paths(paths, keywords, args) + exit_code = pytest.main(args) + return exit_code + + +def doctest(): + """Interface to run doctests via pytest compatible with SymPy's test runner. + """ + raise NotImplementedError diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tests/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tests/diagnose_imports.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tests/diagnose_imports.py new file mode 100644 index 0000000000000000000000000000000000000000..a31b66c66690c082800ae36eee37dad6927e0b37 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tests/diagnose_imports.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python + +""" +Import diagnostics. Run bin/diagnose_imports.py --help for details. +""" + +from __future__ import annotations + +if __name__ == "__main__": + + import sys + import inspect + import builtins + + import optparse + + from os.path import abspath, dirname, join, normpath + this_file = abspath(__file__) + sympy_dir = join(dirname(this_file), '..', '..', '..') + sympy_dir = normpath(sympy_dir) + sys.path.insert(0, sympy_dir) + + option_parser = optparse.OptionParser( + usage= + "Usage: %prog option [options]\n" + "\n" + "Import analysis for imports between SymPy modules.") + option_group = optparse.OptionGroup( + option_parser, + 'Analysis options', + 'Options that define what to do. Exactly one of these must be given.') + option_group.add_option( + '--problems', + help= + 'Print all import problems, that is: ' + 'If an import pulls in a package instead of a module ' + '(e.g. sympy.core instead of sympy.core.add); ' # see ##PACKAGE## + 'if it imports a symbol that is already present; ' # see ##DUPLICATE## + 'if it imports a symbol ' + 'from somewhere other than the defining module.', # see ##ORIGIN## + action='count') + option_group.add_option( + '--origins', + help= + 'For each imported symbol in each module, ' + 'print the module that defined it. ' + '(This is useful for import refactoring.)', + action='count') + option_parser.add_option_group(option_group) + option_group = optparse.OptionGroup( + option_parser, + 'Sort options', + 'These options define the sort order for output lines. ' + 'At most one of these options is allowed. ' + 'Unsorted output will reflect the order in which imports happened.') + option_group.add_option( + '--by-importer', + help='Sort output lines by name of importing module.', + action='count') + option_group.add_option( + '--by-origin', + help='Sort output lines by name of imported module.', + action='count') + option_parser.add_option_group(option_group) + (options, args) = option_parser.parse_args() + if args: + option_parser.error( + 'Unexpected arguments %s (try %s --help)' % (args, sys.argv[0])) + if options.problems > 1: + option_parser.error('--problems must not be given more than once.') + if options.origins > 1: + option_parser.error('--origins must not be given more than once.') + if options.by_importer > 1: + option_parser.error('--by-importer must not be given more than once.') + if options.by_origin > 1: + option_parser.error('--by-origin must not be given more than once.') + options.problems = options.problems == 1 + options.origins = options.origins == 1 + options.by_importer = options.by_importer == 1 + options.by_origin = options.by_origin == 1 + if not options.problems and not options.origins: + option_parser.error( + 'At least one of --problems and --origins is required') + if options.problems and options.origins: + option_parser.error( + 'At most one of --problems and --origins is allowed') + if options.by_importer and options.by_origin: + option_parser.error( + 'At most one of --by-importer and --by-origin is allowed') + options.by_process = not options.by_importer and not options.by_origin + + builtin_import = builtins.__import__ + + class Definition: + """Information about a symbol's definition.""" + def __init__(self, name, value, definer): + self.name = name + self.value = value + self.definer = definer + def __hash__(self): + return hash(self.name) + def __eq__(self, other): + return self.name == other.name and self.value == other.value + def __ne__(self, other): + return not (self == other) + def __repr__(self): + return 'Definition(%s, ..., %s)' % ( + repr(self.name), repr(self.definer)) + + # Maps each function/variable to name of module to define it + symbol_definers: dict[Definition, str] = {} + + def in_module(a, b): + """Is a the same module as or a submodule of b?""" + return a == b or a != None and b != None and a.startswith(b + '.') + + def relevant(module): + """Is module relevant for import checking? + + Only imports between relevant modules will be checked.""" + return in_module(module, 'sympy') + + sorted_messages = [] + + def msg(msg, *args): + if options.by_process: + print(msg % args) + else: + sorted_messages.append(msg % args) + + def tracking_import(module, globals=globals(), locals=[], fromlist=None, level=-1): + """__import__ wrapper - does not change imports at all, but tracks them. + + Default order is implemented by doing output directly. + All other orders are implemented by collecting output information into + a sorted list that will be emitted after all imports are processed. + + Indirect imports can only occur after the requested symbol has been + imported directly (because the indirect import would not have a module + to pick the symbol up from). + So this code detects indirect imports by checking whether the symbol in + question was already imported. + + Keeps the semantics of __import__ unchanged.""" + caller_frame = inspect.getframeinfo(sys._getframe(1)) + importer_filename = caller_frame.filename + importer_module = globals['__name__'] + if importer_filename == caller_frame.filename: + importer_reference = '%s line %s' % ( + importer_filename, str(caller_frame.lineno)) + else: + importer_reference = importer_filename + result = builtin_import(module, globals, locals, fromlist, level) + importee_module = result.__name__ + # We're only interested if importer and importee are in SymPy + if relevant(importer_module) and relevant(importee_module): + for symbol in result.__dict__.iterkeys(): + definition = Definition( + symbol, result.__dict__[symbol], importer_module) + if definition not in symbol_definers: + symbol_definers[definition] = importee_module + if hasattr(result, '__path__'): + ##PACKAGE## + # The existence of __path__ is documented in the tutorial on modules. + # Python 3.3 documents this in http://docs.python.org/3.3/reference/import.html + if options.by_origin: + msg('Error: %s (a package) is imported by %s', + module, importer_reference) + else: + msg('Error: %s contains package import %s', + importer_reference, module) + if fromlist != None: + symbol_list = fromlist + if '*' in symbol_list: + if (importer_filename.endswith(("__init__.py", "__init__.pyc", "__init__.pyo"))): + # We do not check starred imports inside __init__ + # That's the normal "please copy over its imports to my namespace" + symbol_list = [] + else: + symbol_list = result.__dict__.iterkeys() + for symbol in symbol_list: + if symbol not in result.__dict__: + if options.by_origin: + msg('Error: %s.%s is not defined (yet), but %s tries to import it', + importee_module, symbol, importer_reference) + else: + msg('Error: %s tries to import %s.%s, which did not define it (yet)', + importer_reference, importee_module, symbol) + else: + definition = Definition( + symbol, result.__dict__[symbol], importer_module) + symbol_definer = symbol_definers[definition] + if symbol_definer == importee_module: + ##DUPLICATE## + if options.by_origin: + msg('Error: %s.%s is imported again into %s', + importee_module, symbol, importer_reference) + else: + msg('Error: %s imports %s.%s again', + importer_reference, importee_module, symbol) + else: + ##ORIGIN## + if options.by_origin: + msg('Error: %s.%s is imported by %s, which should import %s.%s instead', + importee_module, symbol, importer_reference, symbol_definer, symbol) + else: + msg('Error: %s imports %s.%s but should import %s.%s instead', + importer_reference, importee_module, symbol, symbol_definer, symbol) + return result + + builtins.__import__ = tracking_import + __import__('sympy') + + sorted_messages.sort() + for message in sorted_messages: + print(message) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tests/test_code_quality.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tests/test_code_quality.py new file mode 100644 index 0000000000000000000000000000000000000000..9a9f363f0b9a802553f8643186b7d858d1ad0694 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tests/test_code_quality.py @@ -0,0 +1,510 @@ +# coding=utf-8 +from os import walk, sep, pardir +from os.path import split, join, abspath, exists, isfile +from glob import glob +import re +import random +import ast + +from sympy.testing.pytest import raises +from sympy.testing.quality_unicode import _test_this_file_encoding + +# System path separator (usually slash or backslash) to be +# used with excluded files, e.g. +# exclude = set([ +# "%(sep)smpmath%(sep)s" % sepd, +# ]) +sepd = {"sep": sep} + +# path and sympy_path +SYMPY_PATH = abspath(join(split(__file__)[0], pardir, pardir)) # go to sympy/ +assert exists(SYMPY_PATH) + +TOP_PATH = abspath(join(SYMPY_PATH, pardir)) +BIN_PATH = join(TOP_PATH, "bin") +EXAMPLES_PATH = join(TOP_PATH, "examples") + +# Error messages +message_space = "File contains trailing whitespace: %s, line %s." +message_implicit = "File contains an implicit import: %s, line %s." +message_tabs = "File contains tabs instead of spaces: %s, line %s." +message_carriage = "File contains carriage returns at end of line: %s, line %s" +message_str_raise = "File contains string exception: %s, line %s" +message_gen_raise = "File contains generic exception: %s, line %s" +message_old_raise = "File contains old-style raise statement: %s, line %s, \"%s\"" +message_eof = "File does not end with a newline: %s, line %s" +message_multi_eof = "File ends with more than 1 newline: %s, line %s" +message_test_suite_def = "Function should start with 'test_' or '_': %s, line %s" +message_duplicate_test = "This is a duplicate test function: %s, line %s" +message_self_assignments = "File contains assignments to self/cls: %s, line %s." +message_func_is = "File contains '.func is': %s, line %s." +message_bare_expr = "File contains bare expression: %s, line %s." + +implicit_test_re = re.compile(r'^\s*(>>> )?(\.\.\. )?from .* import .*\*') +str_raise_re = re.compile( + r'^\s*(>>> )?(\.\.\. )?raise(\s+(\'|\")|\s*(\(\s*)+(\'|\"))') +gen_raise_re = re.compile( + r'^\s*(>>> )?(\.\.\. )?raise(\s+Exception|\s*(\(\s*)+Exception)') +old_raise_re = re.compile(r'^\s*(>>> )?(\.\.\. )?raise((\s*\(\s*)|\s+)\w+\s*,') +test_suite_def_re = re.compile(r'^def\s+(?!(_|test))[^(]*\(\s*\)\s*:$') +test_ok_def_re = re.compile(r'^def\s+test_.*:$') +test_file_re = re.compile(r'.*[/\\]test_.*\.py$') +func_is_re = re.compile(r'\.\s*func\s+is') + + +def tab_in_leading(s): + """Returns True if there are tabs in the leading whitespace of a line, + including the whitespace of docstring code samples.""" + n = len(s) - len(s.lstrip()) + if not s[n:n + 3] in ['...', '>>>']: + check = s[:n] + else: + smore = s[n + 3:] + check = s[:n] + smore[:len(smore) - len(smore.lstrip())] + return not (check.expandtabs() == check) + + +def find_self_assignments(s): + """Returns a list of "bad" assignments: if there are instances + of assigning to the first argument of the class method (except + for staticmethod's). + """ + t = [n for n in ast.parse(s).body if isinstance(n, ast.ClassDef)] + + bad = [] + for c in t: + for n in c.body: + if not isinstance(n, ast.FunctionDef): + continue + if any(d.id == 'staticmethod' + for d in n.decorator_list if isinstance(d, ast.Name)): + continue + if n.name == '__new__': + continue + if not n.args.args: + continue + first_arg = n.args.args[0].arg + + for m in ast.walk(n): + if isinstance(m, ast.Assign): + for a in m.targets: + if isinstance(a, ast.Name) and a.id == first_arg: + bad.append(m) + elif (isinstance(a, ast.Tuple) and + any(q.id == first_arg for q in a.elts + if isinstance(q, ast.Name))): + bad.append(m) + + return bad + + +def check_directory_tree(base_path, file_check, exclusions=set(), pattern="*.py"): + """ + Checks all files in the directory tree (with base_path as starting point) + with the file_check function provided, skipping files that contain + any of the strings in the set provided by exclusions. + """ + if not base_path: + return + for root, dirs, files in walk(base_path): + check_files(glob(join(root, pattern)), file_check, exclusions) + + +def check_files(files, file_check, exclusions=set(), pattern=None): + """ + Checks all files with the file_check function provided, skipping files + that contain any of the strings in the set provided by exclusions. + """ + if not files: + return + for fname in files: + if not exists(fname) or not isfile(fname): + continue + if any(ex in fname for ex in exclusions): + continue + if pattern is None or re.match(pattern, fname): + file_check(fname) + + +class _Visit(ast.NodeVisitor): + """return the line number corresponding to the + line on which a bare expression appears if it is a binary op + or a comparison that is not in a with block. + + EXAMPLES + ======== + + >>> import ast + >>> class _Visit(ast.NodeVisitor): + ... def visit_Expr(self, node): + ... if isinstance(node.value, (ast.BinOp, ast.Compare)): + ... print(node.lineno) + ... def visit_With(self, node): + ... pass # no checking there + ... + >>> code='''x = 1 # line 1 + ... for i in range(3): + ... x == 2 # <-- 3 + ... if x == 2: + ... x == 3 # <-- 5 + ... x + 1 # <-- 6 + ... x = 1 + ... if x == 1: + ... print(1) + ... while x != 1: + ... x == 1 # <-- 11 + ... with raises(TypeError): + ... c == 1 + ... raise TypeError + ... assert x == 1 + ... ''' + >>> _Visit().visit(ast.parse(code)) + 3 + 5 + 6 + 11 + """ + def visit_Expr(self, node): + if isinstance(node.value, (ast.BinOp, ast.Compare)): + assert None, message_bare_expr % ('', node.lineno) + def visit_With(self, node): + pass + + +BareExpr = _Visit() + + +def line_with_bare_expr(code): + """return None or else 0-based line number of code on which + a bare expression appeared. + """ + tree = ast.parse(code) + try: + BareExpr.visit(tree) + except AssertionError as msg: + assert msg.args + msg = msg.args[0] + assert msg.startswith(message_bare_expr.split(':', 1)[0]) + return int(msg.rsplit(' ', 1)[1].rstrip('.')) # the line number + + +def test_files(): + """ + This test tests all files in SymPy and checks that: + o no lines contains a trailing whitespace + o no lines end with \r\n + o no line uses tabs instead of spaces + o that the file ends with a single newline + o there are no general or string exceptions + o there are no old style raise statements + o name of arg-less test suite functions start with _ or test_ + o no duplicate function names that start with test_ + o no assignments to self variable in class methods + o no lines contain ".func is" except in the test suite + o there is no do-nothing expression like `a == b` or `x + 1` + """ + + def test(fname): + with open(fname, encoding="utf8") as test_file: + test_this_file(fname, test_file) + with open(fname, encoding='utf8') as test_file: + _test_this_file_encoding(fname, test_file) + + def test_this_file(fname, test_file): + idx = None + code = test_file.read() + test_file.seek(0) # restore reader to head + py = fname if sep not in fname else fname.rsplit(sep, 1)[-1] + if py.startswith('test_'): + idx = line_with_bare_expr(code) + if idx is not None: + assert False, message_bare_expr % (fname, idx + 1) + + line = None # to flag the case where there were no lines in file + tests = 0 + test_set = set() + for idx, line in enumerate(test_file): + if test_file_re.match(fname): + if test_suite_def_re.match(line): + assert False, message_test_suite_def % (fname, idx + 1) + if test_ok_def_re.match(line): + tests += 1 + test_set.add(line[3:].split('(')[0].strip()) + if len(test_set) != tests: + assert False, message_duplicate_test % (fname, idx + 1) + if line.endswith((" \n", "\t\n")): + assert False, message_space % (fname, idx + 1) + if line.endswith("\r\n"): + assert False, message_carriage % (fname, idx + 1) + if tab_in_leading(line): + assert False, message_tabs % (fname, idx + 1) + if str_raise_re.search(line): + assert False, message_str_raise % (fname, idx + 1) + if gen_raise_re.search(line): + assert False, message_gen_raise % (fname, idx + 1) + if (implicit_test_re.search(line) and + not list(filter(lambda ex: ex in fname, import_exclude))): + assert False, message_implicit % (fname, idx + 1) + if func_is_re.search(line) and not test_file_re.search(fname): + assert False, message_func_is % (fname, idx + 1) + + result = old_raise_re.search(line) + + if result is not None: + assert False, message_old_raise % ( + fname, idx + 1, result.group(2)) + + if line is not None: + if line == '\n' and idx > 0: + assert False, message_multi_eof % (fname, idx + 1) + elif not line.endswith('\n'): + # eof newline check + assert False, message_eof % (fname, idx + 1) + + + # Files to test at top level + top_level_files = [join(TOP_PATH, file) for file in [ + "isympy.py", + "build.py", + "setup.py", + ]] + # Files to exclude from all tests + exclude = { + "%(sep)ssympy%(sep)sparsing%(sep)sautolev%(sep)s_antlr%(sep)sautolevparser.py" % sepd, + "%(sep)ssympy%(sep)sparsing%(sep)sautolev%(sep)s_antlr%(sep)sautolevlexer.py" % sepd, + "%(sep)ssympy%(sep)sparsing%(sep)sautolev%(sep)s_antlr%(sep)sautolevlistener.py" % sepd, + "%(sep)ssympy%(sep)sparsing%(sep)slatex%(sep)s_antlr%(sep)slatexparser.py" % sepd, + "%(sep)ssympy%(sep)sparsing%(sep)slatex%(sep)s_antlr%(sep)slatexlexer.py" % sepd, + } + # Files to exclude from the implicit import test + import_exclude = { + # glob imports are allowed in top-level __init__.py: + "%(sep)ssympy%(sep)s__init__.py" % sepd, + # these __init__.py should be fixed: + # XXX: not really, they use useful import pattern (DRY) + "%(sep)svector%(sep)s__init__.py" % sepd, + "%(sep)smechanics%(sep)s__init__.py" % sepd, + "%(sep)squantum%(sep)s__init__.py" % sepd, + "%(sep)spolys%(sep)s__init__.py" % sepd, + "%(sep)spolys%(sep)sdomains%(sep)s__init__.py" % sepd, + # interactive SymPy executes ``from sympy import *``: + "%(sep)sinteractive%(sep)ssession.py" % sepd, + # isympy.py executes ``from sympy import *``: + "%(sep)sisympy.py" % sepd, + # these two are import timing tests: + "%(sep)sbin%(sep)ssympy_time.py" % sepd, + "%(sep)sbin%(sep)ssympy_time_cache.py" % sepd, + # Taken from Python stdlib: + "%(sep)sparsing%(sep)ssympy_tokenize.py" % sepd, + # this one should be fixed: + "%(sep)splotting%(sep)spygletplot%(sep)s" % sepd, + # False positive in the docstring + "%(sep)sbin%(sep)stest_external_imports.py" % sepd, + "%(sep)sbin%(sep)stest_submodule_imports.py" % sepd, + # These are deprecated stubs that can be removed at some point: + "%(sep)sutilities%(sep)sruntests.py" % sepd, + "%(sep)sutilities%(sep)spytest.py" % sepd, + "%(sep)sutilities%(sep)srandtest.py" % sepd, + "%(sep)sutilities%(sep)stmpfiles.py" % sepd, + "%(sep)sutilities%(sep)squality_unicode.py" % sepd, + } + check_files(top_level_files, test) + check_directory_tree(BIN_PATH, test, {"~", ".pyc", ".sh"}, "*") + check_directory_tree(SYMPY_PATH, test, exclude) + check_directory_tree(EXAMPLES_PATH, test, exclude) + + +def _with_space(c): + # return c with a random amount of leading space + return random.randint(0, 10)*' ' + c + + +def test_raise_statement_regular_expression(): + candidates_ok = [ + "some text # raise Exception, 'text'", + "raise ValueError('text') # raise Exception, 'text'", + "raise ValueError('text')", + "raise ValueError", + "raise ValueError('text')", + "raise ValueError('text') #,", + # Talking about an exception in a docstring + ''''"""This function will raise ValueError, except when it doesn't"""''', + "raise (ValueError('text')", + ] + str_candidates_fail = [ + "raise 'exception'", + "raise 'Exception'", + 'raise "exception"', + 'raise "Exception"', + "raise 'ValueError'", + ] + gen_candidates_fail = [ + "raise Exception('text') # raise Exception, 'text'", + "raise Exception('text')", + "raise Exception", + "raise Exception('text')", + "raise Exception('text') #,", + "raise Exception, 'text'", + "raise Exception, 'text' # raise Exception('text')", + "raise Exception, 'text' # raise Exception, 'text'", + ">>> raise Exception, 'text'", + ">>> raise Exception, 'text' # raise Exception('text')", + ">>> raise Exception, 'text' # raise Exception, 'text'", + ] + old_candidates_fail = [ + "raise Exception, 'text'", + "raise Exception, 'text' # raise Exception('text')", + "raise Exception, 'text' # raise Exception, 'text'", + ">>> raise Exception, 'text'", + ">>> raise Exception, 'text' # raise Exception('text')", + ">>> raise Exception, 'text' # raise Exception, 'text'", + "raise ValueError, 'text'", + "raise ValueError, 'text' # raise Exception('text')", + "raise ValueError, 'text' # raise Exception, 'text'", + ">>> raise ValueError, 'text'", + ">>> raise ValueError, 'text' # raise Exception('text')", + ">>> raise ValueError, 'text' # raise Exception, 'text'", + "raise(ValueError,", + "raise (ValueError,", + "raise( ValueError,", + "raise ( ValueError,", + "raise(ValueError ,", + "raise (ValueError ,", + "raise( ValueError ,", + "raise ( ValueError ,", + ] + + for c in candidates_ok: + assert str_raise_re.search(_with_space(c)) is None, c + assert gen_raise_re.search(_with_space(c)) is None, c + assert old_raise_re.search(_with_space(c)) is None, c + for c in str_candidates_fail: + assert str_raise_re.search(_with_space(c)) is not None, c + for c in gen_candidates_fail: + assert gen_raise_re.search(_with_space(c)) is not None, c + for c in old_candidates_fail: + assert old_raise_re.search(_with_space(c)) is not None, c + + +def test_implicit_imports_regular_expression(): + candidates_ok = [ + "from sympy import something", + ">>> from sympy import something", + "from sympy.somewhere import something", + ">>> from sympy.somewhere import something", + "import sympy", + ">>> import sympy", + "import sympy.something.something", + "... import sympy", + "... import sympy.something.something", + "... from sympy import something", + "... from sympy.somewhere import something", + ">> from sympy import *", # To allow 'fake' docstrings + "# from sympy import *", + "some text # from sympy import *", + ] + candidates_fail = [ + "from sympy import *", + ">>> from sympy import *", + "from sympy.somewhere import *", + ">>> from sympy.somewhere import *", + "... from sympy import *", + "... from sympy.somewhere import *", + ] + for c in candidates_ok: + assert implicit_test_re.search(_with_space(c)) is None, c + for c in candidates_fail: + assert implicit_test_re.search(_with_space(c)) is not None, c + + +def test_test_suite_defs(): + candidates_ok = [ + " def foo():\n", + "def foo(arg):\n", + "def _foo():\n", + "def test_foo():\n", + ] + candidates_fail = [ + "def foo():\n", + "def foo() :\n", + "def foo( ):\n", + "def foo():\n", + ] + for c in candidates_ok: + assert test_suite_def_re.search(c) is None, c + for c in candidates_fail: + assert test_suite_def_re.search(c) is not None, c + + +def test_test_duplicate_defs(): + candidates_ok = [ + "def foo():\ndef foo():\n", + "def test():\ndef test_():\n", + "def test_():\ndef test__():\n", + ] + candidates_fail = [ + "def test_():\ndef test_ ():\n", + "def test_1():\ndef test_1():\n", + ] + ok = (None, 'check') + def check(file): + tests = 0 + test_set = set() + for idx, line in enumerate(file.splitlines()): + if test_ok_def_re.match(line): + tests += 1 + test_set.add(line[3:].split('(')[0].strip()) + if len(test_set) != tests: + return False, message_duplicate_test % ('check', idx + 1) + return None, 'check' + for c in candidates_ok: + assert check(c) == ok + for c in candidates_fail: + assert check(c) != ok + + +def test_find_self_assignments(): + candidates_ok = [ + "class A(object):\n def foo(self, arg): arg = self\n", + "class A(object):\n def foo(self, arg): self.prop = arg\n", + "class A(object):\n def foo(self, arg): obj, obj2 = arg, self\n", + "class A(object):\n @classmethod\n def bar(cls, arg): arg = cls\n", + "class A(object):\n def foo(var, arg): arg = var\n", + ] + candidates_fail = [ + "class A(object):\n def foo(self, arg): self = arg\n", + "class A(object):\n def foo(self, arg): obj, self = arg, arg\n", + "class A(object):\n def foo(self, arg):\n if arg: self = arg", + "class A(object):\n @classmethod\n def foo(cls, arg): cls = arg\n", + "class A(object):\n def foo(var, arg): var = arg\n", + ] + + for c in candidates_ok: + assert find_self_assignments(c) == [] + for c in candidates_fail: + assert find_self_assignments(c) != [] + + +def test_test_unicode_encoding(): + unicode_whitelist = ['foo'] + unicode_strict_whitelist = ['bar'] + + fname = 'abc' + test_file = ['α'] + raises(AssertionError, lambda: _test_this_file_encoding( + fname, test_file, unicode_whitelist, unicode_strict_whitelist)) + + fname = 'abc' + test_file = ['abc'] + _test_this_file_encoding( + fname, test_file, unicode_whitelist, unicode_strict_whitelist) + + fname = 'foo' + test_file = ['abc'] + raises(AssertionError, lambda: _test_this_file_encoding( + fname, test_file, unicode_whitelist, unicode_strict_whitelist)) + + fname = 'bar' + test_file = ['abc'] + _test_this_file_encoding( + fname, test_file, unicode_whitelist, unicode_strict_whitelist) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tests/test_deprecated.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tests/test_deprecated.py new file mode 100644 index 0000000000000000000000000000000000000000..696933d96d6232ea869da1002ec9ebee5309724d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tests/test_deprecated.py @@ -0,0 +1,5 @@ +from sympy.testing.pytest import warns_deprecated_sympy + +def test_deprecated_testing_randtest(): + with warns_deprecated_sympy(): + import sympy.testing.randtest # noqa:F401 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tests/test_module_imports.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tests/test_module_imports.py new file mode 100644 index 0000000000000000000000000000000000000000..d16dbaa98156c287c18b46ff07c0ede5d26e069a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tests/test_module_imports.py @@ -0,0 +1,42 @@ +""" +Checks that SymPy does not contain indirect imports. + +An indirect import is importing a symbol from a module that itself imported the +symbol from elsewhere. Such a constellation makes it harder to diagnose +inter-module dependencies and import order problems, and is therefore strongly +discouraged. + +(Indirect imports from end-user code is fine and in fact a best practice.) + +Implementation note: Forcing Python into actually unloading already-imported +submodules is a tricky and partly undocumented process. To avoid these issues, +the actual diagnostic code is in bin/diagnose_imports, which is run as a +separate, pristine Python process. +""" + +import subprocess +import sys +from os.path import abspath, dirname, join, normpath +import inspect + +from sympy.testing.pytest import XFAIL + +@XFAIL +def test_module_imports_are_direct(): + my_filename = abspath(inspect.getfile(inspect.currentframe())) + my_dirname = dirname(my_filename) + diagnose_imports_filename = join(my_dirname, 'diagnose_imports.py') + diagnose_imports_filename = normpath(diagnose_imports_filename) + + process = subprocess.Popen( + [ + sys.executable, + normpath(diagnose_imports_filename), + '--problems', + '--by-importer' + ], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=-1) + output, _ = process.communicate() + assert output == '', "There are import problems:\n" + output.decode() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tests/test_pytest.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tests/test_pytest.py new file mode 100644 index 0000000000000000000000000000000000000000..7080a4ed4602707a3efb4d9025e8e9cbe23a5ef8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tests/test_pytest.py @@ -0,0 +1,211 @@ +import warnings + +from sympy.testing.pytest import (raises, warns, ignore_warnings, + warns_deprecated_sympy, Failed) +from sympy.utilities.exceptions import sympy_deprecation_warning + + + +# Test callables + + +def test_expected_exception_is_silent_callable(): + def f(): + raise ValueError() + raises(ValueError, f) + + +# Under pytest raises will raise Failed rather than AssertionError +def test_lack_of_exception_triggers_AssertionError_callable(): + try: + raises(Exception, lambda: 1 + 1) + assert False + except Failed as e: + assert "DID NOT RAISE" in str(e) + + +def test_unexpected_exception_is_passed_through_callable(): + def f(): + raise ValueError("some error message") + try: + raises(TypeError, f) + assert False + except ValueError as e: + assert str(e) == "some error message" + +# Test with statement + +def test_expected_exception_is_silent_with(): + with raises(ValueError): + raise ValueError() + + +def test_lack_of_exception_triggers_AssertionError_with(): + try: + with raises(Exception): + 1 + 1 + assert False + except Failed as e: + assert "DID NOT RAISE" in str(e) + + +def test_unexpected_exception_is_passed_through_with(): + try: + with raises(TypeError): + raise ValueError("some error message") + assert False + except ValueError as e: + assert str(e) == "some error message" + +# Now we can use raises() instead of try/catch +# to test that a specific exception class is raised + + +def test_second_argument_should_be_callable_or_string(): + raises(TypeError, lambda: raises("irrelevant", 42)) + + +def test_warns_catches_warning(): + with warnings.catch_warnings(record=True) as w: + with warns(UserWarning): + warnings.warn('this is the warning message') + assert len(w) == 0 + + +def test_warns_raises_without_warning(): + with raises(Failed): + with warns(UserWarning): + pass + + +def test_warns_hides_other_warnings(): + with raises(RuntimeWarning): + with warns(UserWarning): + warnings.warn('this is the warning message', UserWarning) + warnings.warn('this is the other message', RuntimeWarning) + + +def test_warns_continues_after_warning(): + with warnings.catch_warnings(record=True) as w: + finished = False + with warns(UserWarning): + warnings.warn('this is the warning message') + finished = True + assert finished + assert len(w) == 0 + + +def test_warns_many_warnings(): + with warns(UserWarning): + warnings.warn('this is the warning message', UserWarning) + warnings.warn('this is the other warning message', UserWarning) + + +def test_warns_match_matching(): + with warnings.catch_warnings(record=True) as w: + with warns(UserWarning, match='this is the warning message'): + warnings.warn('this is the warning message', UserWarning) + assert len(w) == 0 + + +def test_warns_match_non_matching(): + with warnings.catch_warnings(record=True) as w: + with raises(Failed): + with warns(UserWarning, match='this is the warning message'): + warnings.warn('this is not the expected warning message', UserWarning) + assert len(w) == 0 + +def _warn_sympy_deprecation(stacklevel=3): + sympy_deprecation_warning( + "feature", + active_deprecations_target="active-deprecations", + deprecated_since_version="0.0.0", + stacklevel=stacklevel, + ) + +def test_warns_deprecated_sympy_catches_warning(): + with warnings.catch_warnings(record=True) as w: + with warns_deprecated_sympy(): + _warn_sympy_deprecation() + assert len(w) == 0 + + +def test_warns_deprecated_sympy_raises_without_warning(): + with raises(Failed): + with warns_deprecated_sympy(): + pass + +def test_warns_deprecated_sympy_wrong_stacklevel(): + with raises(Failed): + with warns_deprecated_sympy(): + _warn_sympy_deprecation(stacklevel=1) + +def test_warns_deprecated_sympy_doesnt_hide_other_warnings(): + # Unlike pytest's deprecated_call, we should not hide other warnings. + with raises(RuntimeWarning): + with warns_deprecated_sympy(): + _warn_sympy_deprecation() + warnings.warn('this is the other message', RuntimeWarning) + + +def test_warns_deprecated_sympy_continues_after_warning(): + with warnings.catch_warnings(record=True) as w: + finished = False + with warns_deprecated_sympy(): + _warn_sympy_deprecation() + finished = True + assert finished + assert len(w) == 0 + +def test_ignore_ignores_warning(): + with warnings.catch_warnings(record=True) as w: + with ignore_warnings(UserWarning): + warnings.warn('this is the warning message') + assert len(w) == 0 + + +def test_ignore_does_not_raise_without_warning(): + with warnings.catch_warnings(record=True) as w: + with ignore_warnings(UserWarning): + pass + assert len(w) == 0 + + +def test_ignore_allows_other_warnings(): + with warnings.catch_warnings(record=True) as w: + # This is needed when pytest is run as -Werror + # the setting is reverted at the end of the catch_Warnings block. + warnings.simplefilter("always") + with ignore_warnings(UserWarning): + warnings.warn('this is the warning message', UserWarning) + warnings.warn('this is the other message', RuntimeWarning) + assert len(w) == 1 + assert isinstance(w[0].message, RuntimeWarning) + assert str(w[0].message) == 'this is the other message' + + +def test_ignore_continues_after_warning(): + with warnings.catch_warnings(record=True) as w: + finished = False + with ignore_warnings(UserWarning): + warnings.warn('this is the warning message') + finished = True + assert finished + assert len(w) == 0 + + +def test_ignore_many_warnings(): + with warnings.catch_warnings(record=True) as w: + # This is needed when pytest is run as -Werror + # the setting is reverted at the end of the catch_Warnings block. + warnings.simplefilter("always") + with ignore_warnings(UserWarning): + warnings.warn('this is the warning message', UserWarning) + warnings.warn('this is the other message', RuntimeWarning) + warnings.warn('this is the warning message', UserWarning) + warnings.warn('this is the other message', RuntimeWarning) + warnings.warn('this is the other message', RuntimeWarning) + assert len(w) == 3 + for wi in w: + assert isinstance(wi.message, RuntimeWarning) + assert str(wi.message) == 'this is the other message' diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tests/test_runtests_pytest.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tests/test_runtests_pytest.py new file mode 100644 index 0000000000000000000000000000000000000000..cd56d831f3618c9dbb8a1dffe63d95a0befc6adf --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tests/test_runtests_pytest.py @@ -0,0 +1,171 @@ +import pathlib +from typing import List + +import pytest + +from sympy.testing.runtests_pytest import ( + make_absolute_path, + sympy_dir, + update_args_with_paths, +) + + +class TestMakeAbsolutePath: + + @staticmethod + @pytest.mark.parametrize( + 'partial_path', ['sympy', 'sympy/core', 'sympy/nonexistant_directory'], + ) + def test_valid_partial_path(partial_path: str): + """Paths that start with `sympy` are valid.""" + _ = make_absolute_path(partial_path) + + @staticmethod + @pytest.mark.parametrize( + 'partial_path', ['not_sympy', 'also/not/sympy'], + ) + def test_invalid_partial_path_raises_value_error(partial_path: str): + """A `ValueError` is raises on paths that don't start with `sympy`.""" + with pytest.raises(ValueError): + _ = make_absolute_path(partial_path) + + +class TestUpdateArgsWithPaths: + + @staticmethod + def test_no_paths(): + """If no paths are passed, only `sympy` and `doc/src` are appended. + + `sympy` and `doc/src` are the `testpaths` stated in `pytest.ini`. They + need to be manually added as if any path-related arguments are passed + to `pytest.main` then the settings in `pytest.ini` may be ignored. + + """ + paths = [] + args = update_args_with_paths(paths=paths, keywords=None, args=[]) + expected = [ + str(pathlib.Path(sympy_dir(), 'sympy')), + str(pathlib.Path(sympy_dir(), 'doc/src')), + ] + assert args == expected + + @staticmethod + @pytest.mark.parametrize( + 'path', + ['sympy/core/tests/test_basic.py', '_basic'] + ) + def test_one_file(path: str): + """Single files/paths, full or partial, are matched correctly.""" + args = update_args_with_paths(paths=[path], keywords=None, args=[]) + expected = [ + str(pathlib.Path(sympy_dir(), 'sympy/core/tests/test_basic.py')), + ] + assert args == expected + + @staticmethod + def test_partial_path_from_root(): + """Partial paths from the root directly are matched correctly.""" + args = update_args_with_paths(paths=['sympy/functions'], keywords=None, args=[]) + expected = [str(pathlib.Path(sympy_dir(), 'sympy/functions'))] + assert args == expected + + @staticmethod + def test_multiple_paths_from_root(): + """Multiple paths, partial or full, are matched correctly.""" + paths = ['sympy/core/tests/test_basic.py', 'sympy/functions'] + args = update_args_with_paths(paths=paths, keywords=None, args=[]) + expected = [ + str(pathlib.Path(sympy_dir(), 'sympy/core/tests/test_basic.py')), + str(pathlib.Path(sympy_dir(), 'sympy/functions')), + ] + assert args == expected + + @staticmethod + @pytest.mark.parametrize( + 'paths, expected_paths', + [ + ( + ['/core', '/util'], + [ + 'doc/src/modules/utilities', + 'doc/src/reference/public/utilities', + 'sympy/core', + 'sympy/logic/utilities', + 'sympy/utilities', + ] + ), + ] + ) + def test_multiple_paths_from_non_root(paths: List[str], expected_paths: List[str]): + """Multiple partial paths are matched correctly.""" + args = update_args_with_paths(paths=paths, keywords=None, args=[]) + assert len(args) == len(expected_paths) + for arg, expected in zip(sorted(args), expected_paths): + assert expected in arg + + @staticmethod + @pytest.mark.parametrize( + 'paths', + [ + + [], + ['sympy/physics'], + ['sympy/physics/mechanics'], + ['sympy/physics/mechanics/tests'], + ['sympy/physics/mechanics/tests/test_kane3.py'], + ] + ) + def test_string_as_keyword(paths: List[str]): + """String keywords are matched correctly.""" + keywords = ('bicycle', ) + args = update_args_with_paths(paths=paths, keywords=keywords, args=[]) + expected_args = ['sympy/physics/mechanics/tests/test_kane3.py::test_bicycle'] + assert len(args) == len(expected_args) + for arg, expected in zip(sorted(args), expected_args): + assert expected in arg + + @staticmethod + @pytest.mark.parametrize( + 'paths', + [ + + [], + ['sympy/core'], + ['sympy/core/tests'], + ['sympy/core/tests/test_sympify.py'], + ] + ) + def test_integer_as_keyword(paths: List[str]): + """Integer keywords are matched correctly.""" + keywords = ('3538', ) + args = update_args_with_paths(paths=paths, keywords=keywords, args=[]) + expected_args = ['sympy/core/tests/test_sympify.py::test_issue_3538'] + assert len(args) == len(expected_args) + for arg, expected in zip(sorted(args), expected_args): + assert expected in arg + + @staticmethod + def test_multiple_keywords(): + """Multiple keywords are matched correctly.""" + keywords = ('bicycle', '3538') + args = update_args_with_paths(paths=[], keywords=keywords, args=[]) + expected_args = [ + 'sympy/core/tests/test_sympify.py::test_issue_3538', + 'sympy/physics/mechanics/tests/test_kane3.py::test_bicycle', + ] + assert len(args) == len(expected_args) + for arg, expected in zip(sorted(args), expected_args): + assert expected in arg + + @staticmethod + def test_keyword_match_in_multiple_files(): + """Keywords are matched across multiple files.""" + keywords = ('1130', ) + args = update_args_with_paths(paths=[], keywords=keywords, args=[]) + expected_args = [ + 'sympy/integrals/tests/test_heurisch.py::test_heurisch_symbolic_coeffs_1130', + 'sympy/utilities/tests/test_lambdify.py::test_python_div_zero_issue_11306', + ] + assert len(args) == len(expected_args) + for arg, expected in zip(sorted(args), expected_args): + assert expected in arg diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tmpfiles.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tmpfiles.py new file mode 100644 index 0000000000000000000000000000000000000000..1d5c69cb58aa11f77679855f3df21f03a10d3b2b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/testing/tmpfiles.py @@ -0,0 +1,46 @@ +""" +This module adds context manager for temporary files generated by the tests. +""" + +import shutil +import os + + +class TmpFileManager: + """ + A class to track record of every temporary files created by the tests. + """ + tmp_files = set('') + tmp_folders = set('') + + @classmethod + def tmp_file(cls, name=''): + cls.tmp_files.add(name) + return name + + @classmethod + def tmp_folder(cls, name=''): + cls.tmp_folders.add(name) + return name + + @classmethod + def cleanup(cls): + while cls.tmp_files: + file = cls.tmp_files.pop() + if os.path.isfile(file): + os.remove(file) + while cls.tmp_folders: + folder = cls.tmp_folders.pop() + shutil.rmtree(folder) + +def cleanup_tmp_files(test_func): + """ + A decorator to help test codes remove temporary files after the tests. + """ + def wrapper_function(): + try: + test_func() + finally: + TmpFileManager.cleanup() + + return wrapper_function diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/this.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/this.py new file mode 100644 index 0000000000000000000000000000000000000000..c2a5504ac42c7d57790d65524b066a6df86ab539 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/this.py @@ -0,0 +1,21 @@ +""" +The Zen of SymPy. +""" + +s = """The Zen of SymPy + +Unevaluated is better than evaluated. +The user interface matters. +Printing matters. +Pure Python can be fast enough. +If it's too slow, it's (probably) your fault. +Documentation matters. +Correctness is more important than speed. +Push it in now and improve upon it later. +Coverage by testing matters. +Smart tests are better than random tests. +But random tests sometimes find what your smartest test missed. +The Python way is probably the right way. +Community is more important than code.""" + +print(s) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5c166f9163785f4aa5744324eb817bef79b33525 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/__init__.py @@ -0,0 +1,15 @@ +""" Unification in SymPy + +See sympy.unify.core docstring for algorithmic details + +See http://matthewrocklin.com/blog/work/2012/11/01/Unification/ for discussion +""" + +from .usympy import unify, rebuild +from .rewrite import rewriterule + +__all__ = [ + 'unify', 'rebuild', + + 'rewriterule', +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/core.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/core.py new file mode 100644 index 0000000000000000000000000000000000000000..5359d0bbd376e9fa9efacff1d90c0bf51414cebf --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/core.py @@ -0,0 +1,234 @@ +""" Generic Unification algorithm for expression trees with lists of children + +This implementation is a direct translation of + +Artificial Intelligence: A Modern Approach by Stuart Russel and Peter Norvig +Second edition, section 9.2, page 276 + +It is modified in the following ways: + +1. We allow associative and commutative Compound expressions. This results in + combinatorial blowup. +2. We explore the tree lazily. +3. We provide generic interfaces to symbolic algebra libraries in Python. + +A more traditional version can be found here +http://aima.cs.berkeley.edu/python/logic.html +""" + +from sympy.utilities.iterables import kbins + +class Compound: + """ A little class to represent an interior node in the tree + + This is analogous to SymPy.Basic for non-Atoms + """ + def __init__(self, op, args): + self.op = op + self.args = args + + def __eq__(self, other): + return (type(self) is type(other) and self.op == other.op and + self.args == other.args) + + def __hash__(self): + return hash((type(self), self.op, self.args)) + + def __str__(self): + return "%s[%s]" % (str(self.op), ', '.join(map(str, self.args))) + +class Variable: + """ A Wild token """ + def __init__(self, arg): + self.arg = arg + + def __eq__(self, other): + return type(self) is type(other) and self.arg == other.arg + + def __hash__(self): + return hash((type(self), self.arg)) + + def __str__(self): + return "Variable(%s)" % str(self.arg) + +class CondVariable: + """ A wild token that matches conditionally. + + arg - a wild token. + valid - an additional constraining function on a match. + """ + def __init__(self, arg, valid): + self.arg = arg + self.valid = valid + + def __eq__(self, other): + return (type(self) is type(other) and + self.arg == other.arg and + self.valid == other.valid) + + def __hash__(self): + return hash((type(self), self.arg, self.valid)) + + def __str__(self): + return "CondVariable(%s)" % str(self.arg) + +def unify(x, y, s=None, **fns): + """ Unify two expressions. + + Parameters + ========== + + x, y - expression trees containing leaves, Compounds and Variables. + s - a mapping of variables to subtrees. + + Returns + ======= + + lazy sequence of mappings {Variable: subtree} + + Examples + ======== + + >>> from sympy.unify.core import unify, Compound, Variable + >>> expr = Compound("Add", ("x", "y")) + >>> pattern = Compound("Add", ("x", Variable("a"))) + >>> next(unify(expr, pattern, {})) + {Variable(a): 'y'} + """ + s = s or {} + + if x == y: + yield s + elif isinstance(x, (Variable, CondVariable)): + yield from unify_var(x, y, s, **fns) + elif isinstance(y, (Variable, CondVariable)): + yield from unify_var(y, x, s, **fns) + elif isinstance(x, Compound) and isinstance(y, Compound): + is_commutative = fns.get('is_commutative', lambda x: False) + is_associative = fns.get('is_associative', lambda x: False) + for sop in unify(x.op, y.op, s, **fns): + if is_associative(x) and is_associative(y): + a, b = (x, y) if len(x.args) < len(y.args) else (y, x) + if is_commutative(x) and is_commutative(y): + combs = allcombinations(a.args, b.args, 'commutative') + else: + combs = allcombinations(a.args, b.args, 'associative') + for aaargs, bbargs in combs: + aa = [unpack(Compound(a.op, arg)) for arg in aaargs] + bb = [unpack(Compound(b.op, arg)) for arg in bbargs] + yield from unify(aa, bb, sop, **fns) + elif len(x.args) == len(y.args): + yield from unify(x.args, y.args, sop, **fns) + + elif is_args(x) and is_args(y) and len(x) == len(y): + if len(x) == 0: + yield s + else: + for shead in unify(x[0], y[0], s, **fns): + yield from unify(x[1:], y[1:], shead, **fns) + +def unify_var(var, x, s, **fns): + if var in s: + yield from unify(s[var], x, s, **fns) + elif occur_check(var, x): + pass + elif isinstance(var, CondVariable) and var.valid(x): + yield assoc(s, var, x) + elif isinstance(var, Variable): + yield assoc(s, var, x) + +def occur_check(var, x): + """ var occurs in subtree owned by x? """ + if var == x: + return True + elif isinstance(x, Compound): + return occur_check(var, x.args) + elif is_args(x): + if any(occur_check(var, xi) for xi in x): return True + return False + +def assoc(d, key, val): + """ Return copy of d with key associated to val """ + d = d.copy() + d[key] = val + return d + +def is_args(x): + """ Is x a traditional iterable? """ + return type(x) in (tuple, list, set) + +def unpack(x): + if isinstance(x, Compound) and len(x.args) == 1: + return x.args[0] + else: + return x + +def allcombinations(A, B, ordered): + """ + Restructure A and B to have the same number of elements. + + Parameters + ========== + + ordered must be either 'commutative' or 'associative'. + + A and B can be rearranged so that the larger of the two lists is + reorganized into smaller sublists. + + Examples + ======== + + >>> from sympy.unify.core import allcombinations + >>> for x in allcombinations((1, 2, 3), (5, 6), 'associative'): print(x) + (((1,), (2, 3)), ((5,), (6,))) + (((1, 2), (3,)), ((5,), (6,))) + + >>> for x in allcombinations((1, 2, 3), (5, 6), 'commutative'): print(x) + (((1,), (2, 3)), ((5,), (6,))) + (((1, 2), (3,)), ((5,), (6,))) + (((1,), (3, 2)), ((5,), (6,))) + (((1, 3), (2,)), ((5,), (6,))) + (((2,), (1, 3)), ((5,), (6,))) + (((2, 1), (3,)), ((5,), (6,))) + (((2,), (3, 1)), ((5,), (6,))) + (((2, 3), (1,)), ((5,), (6,))) + (((3,), (1, 2)), ((5,), (6,))) + (((3, 1), (2,)), ((5,), (6,))) + (((3,), (2, 1)), ((5,), (6,))) + (((3, 2), (1,)), ((5,), (6,))) + """ + + if ordered == "commutative": + ordered = 11 + if ordered == "associative": + ordered = None + sm, bg = (A, B) if len(A) < len(B) else (B, A) + for part in kbins(list(range(len(bg))), len(sm), ordered=ordered): + if bg == B: + yield tuple((a,) for a in A), partition(B, part) + else: + yield partition(A, part), tuple((b,) for b in B) + +def partition(it, part): + """ Partition a tuple/list into pieces defined by indices. + + Examples + ======== + + >>> from sympy.unify.core import partition + >>> partition((10, 20, 30, 40), [[0, 1, 2], [3]]) + ((10, 20, 30), (40,)) + """ + return type(it)([index(it, ind) for ind in part]) + +def index(it, ind): + """ Fancy indexing into an indexable iterable (tuple, list). + + Examples + ======== + + >>> from sympy.unify.core import index + >>> index([10, 20, 30], (1, 2, 0)) + [20, 30, 10] + """ + return type(it)([it[i] for i in ind]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/rewrite.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/rewrite.py new file mode 100644 index 0000000000000000000000000000000000000000..95a6fa5ffd6a3fde94d17ee845c03bb2b44cf009 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/rewrite.py @@ -0,0 +1,55 @@ +""" Functions to support rewriting of SymPy expressions """ + +from sympy.core.expr import Expr +from sympy.assumptions import ask +from sympy.strategies.tools import subs +from sympy.unify.usympy import rebuild, unify + +def rewriterule(source, target, variables=(), condition=None, assume=None): + """ Rewrite rule. + + Transform expressions that match source into expressions that match target + treating all ``variables`` as wilds. + + Examples + ======== + + >>> from sympy.abc import w, x, y, z + >>> from sympy.unify.rewrite import rewriterule + >>> from sympy import default_sort_key + >>> rl = rewriterule(x + y, x**y, [x, y]) + >>> sorted(rl(z + 3), key=default_sort_key) + [3**z, z**3] + + Use ``condition`` to specify additional requirements. Inputs are taken in + the same order as is found in variables. + + >>> rl = rewriterule(x + y, x**y, [x, y], lambda x, y: x.is_integer) + >>> list(rl(z + 3)) + [3**z] + + Use ``assume`` to specify additional requirements using new assumptions. + + >>> from sympy.assumptions import Q + >>> rl = rewriterule(x + y, x**y, [x, y], assume=Q.integer(x)) + >>> list(rl(z + 3)) + [3**z] + + Assumptions for the local context are provided at rule runtime + + >>> list(rl(w + z, Q.integer(z))) + [z**w] + """ + + def rewrite_rl(expr, assumptions=True): + for match in unify(source, expr, {}, variables=variables): + if (condition and + not condition(*[match.get(var, var) for var in variables])): + continue + if (assume and not ask(assume.xreplace(match), assumptions)): + continue + expr2 = subs(match)(target) + if isinstance(expr2, Expr): + expr2 = rebuild(expr2) + yield expr2 + return rewrite_rl diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/tests/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/tests/test_rewrite.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/tests/test_rewrite.py new file mode 100644 index 0000000000000000000000000000000000000000..7b73e2856d5f6380c576220fa2780324df98091a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/tests/test_rewrite.py @@ -0,0 +1,74 @@ +from sympy.unify.rewrite import rewriterule +from sympy.core.basic import Basic +from sympy.core.singleton import S +from sympy.core.symbol import Symbol +from sympy.functions.elementary.trigonometric import sin +from sympy.abc import x, y +from sympy.strategies.rl import rebuild +from sympy.assumptions import Q + +p, q = Symbol('p'), Symbol('q') + +def test_simple(): + rl = rewriterule(Basic(p, S(1)), Basic(p, S(2)), variables=(p,)) + assert list(rl(Basic(S(3), S(1)))) == [Basic(S(3), S(2))] + + p1 = p**2 + p2 = p**3 + rl = rewriterule(p1, p2, variables=(p,)) + + expr = x**2 + assert list(rl(expr)) == [x**3] + +def test_simple_variables(): + rl = rewriterule(Basic(x, S(1)), Basic(x, S(2)), variables=(x,)) + assert list(rl(Basic(S(3), S(1)))) == [Basic(S(3), S(2))] + + rl = rewriterule(x**2, x**3, variables=(x,)) + assert list(rl(y**2)) == [y**3] + +def test_moderate(): + p1 = p**2 + q**3 + p2 = (p*q)**4 + rl = rewriterule(p1, p2, (p, q)) + + expr = x**2 + y**3 + assert list(rl(expr)) == [(x*y)**4] + +def test_sincos(): + p1 = sin(p)**2 + sin(p)**2 + p2 = 1 + rl = rewriterule(p1, p2, (p, q)) + + assert list(rl(sin(x)**2 + sin(x)**2)) == [1] + assert list(rl(sin(y)**2 + sin(y)**2)) == [1] + +def test_Exprs_ok(): + rl = rewriterule(p+q, q+p, (p, q)) + next(rl(x+y)).is_commutative + str(next(rl(x+y))) + +def test_condition_simple(): + rl = rewriterule(x, x+1, [x], lambda x: x < 10) + assert not list(rl(S(15))) + assert rebuild(next(rl(S(5)))) == 6 + + +def test_condition_multiple(): + rl = rewriterule(x + y, x**y, [x,y], lambda x, y: x.is_integer) + + a = Symbol('a') + b = Symbol('b', integer=True) + expr = a + b + assert list(rl(expr)) == [b**a] + + c = Symbol('c', integer=True) + d = Symbol('d', integer=True) + assert set(rl(c + d)) == {c**d, d**c} + +def test_assumptions(): + rl = rewriterule(x + y, x**y, [x, y], assume=Q.integer(x)) + + a, b = map(Symbol, 'ab') + expr = a + b + assert list(rl(expr, Q.integer(b))) == [b**a] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/tests/test_sympy.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/tests/test_sympy.py new file mode 100644 index 0000000000000000000000000000000000000000..eca3933a91abfabdbad96f626e4da761a41b3fd2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/tests/test_sympy.py @@ -0,0 +1,162 @@ +from sympy.core.add import Add +from sympy.core.basic import Basic +from sympy.core.containers import Tuple +from sympy.core.singleton import S +from sympy.core.symbol import (Symbol, symbols) +from sympy.logic.boolalg import And +from sympy.core.symbol import Str +from sympy.unify.core import Compound, Variable +from sympy.unify.usympy import (deconstruct, construct, unify, is_associative, + is_commutative) +from sympy.abc import x, y, z, n + +def test_deconstruct(): + expr = Basic(S(1), S(2), S(3)) + expected = Compound(Basic, (1, 2, 3)) + assert deconstruct(expr) == expected + + assert deconstruct(1) == 1 + assert deconstruct(x) == x + assert deconstruct(x, variables=(x,)) == Variable(x) + assert deconstruct(Add(1, x, evaluate=False)) == Compound(Add, (1, x)) + assert deconstruct(Add(1, x, evaluate=False), variables=(x,)) == \ + Compound(Add, (1, Variable(x))) + +def test_construct(): + expr = Compound(Basic, (S(1), S(2), S(3))) + expected = Basic(S(1), S(2), S(3)) + assert construct(expr) == expected + +def test_nested(): + expr = Basic(S(1), Basic(S(2)), S(3)) + cmpd = Compound(Basic, (S(1), Compound(Basic, Tuple(2)), S(3))) + assert deconstruct(expr) == cmpd + assert construct(cmpd) == expr + +def test_unify(): + expr = Basic(S(1), S(2), S(3)) + a, b, c = map(Symbol, 'abc') + pattern = Basic(a, b, c) + assert list(unify(expr, pattern, {}, (a, b, c))) == [{a: 1, b: 2, c: 3}] + assert list(unify(expr, pattern, variables=(a, b, c))) == \ + [{a: 1, b: 2, c: 3}] + +def test_unify_variables(): + assert list(unify(Basic(S(1), S(2)), Basic(S(1), x), {}, variables=(x,))) == [{x: 2}] + +def test_s_input(): + expr = Basic(S(1), S(2)) + a, b = map(Symbol, 'ab') + pattern = Basic(a, b) + assert list(unify(expr, pattern, {}, (a, b))) == [{a: 1, b: 2}] + assert list(unify(expr, pattern, {a: 5}, (a, b))) == [] + +def iterdicteq(a, b): + a = tuple(a) + b = tuple(b) + return len(a) == len(b) and all(x in b for x in a) + +def test_unify_commutative(): + expr = Add(1, 2, 3, evaluate=False) + a, b, c = map(Symbol, 'abc') + pattern = Add(a, b, c, evaluate=False) + + result = tuple(unify(expr, pattern, {}, (a, b, c))) + expected = ({a: 1, b: 2, c: 3}, + {a: 1, b: 3, c: 2}, + {a: 2, b: 1, c: 3}, + {a: 2, b: 3, c: 1}, + {a: 3, b: 1, c: 2}, + {a: 3, b: 2, c: 1}) + + assert iterdicteq(result, expected) + +def test_unify_iter(): + expr = Add(1, 2, 3, evaluate=False) + a, b, c = map(Symbol, 'abc') + pattern = Add(a, c, evaluate=False) + assert is_associative(deconstruct(pattern)) + assert is_commutative(deconstruct(pattern)) + + result = list(unify(expr, pattern, {}, (a, c))) + expected = [{a: 1, c: Add(2, 3, evaluate=False)}, + {a: 1, c: Add(3, 2, evaluate=False)}, + {a: 2, c: Add(1, 3, evaluate=False)}, + {a: 2, c: Add(3, 1, evaluate=False)}, + {a: 3, c: Add(1, 2, evaluate=False)}, + {a: 3, c: Add(2, 1, evaluate=False)}, + {a: Add(1, 2, evaluate=False), c: 3}, + {a: Add(2, 1, evaluate=False), c: 3}, + {a: Add(1, 3, evaluate=False), c: 2}, + {a: Add(3, 1, evaluate=False), c: 2}, + {a: Add(2, 3, evaluate=False), c: 1}, + {a: Add(3, 2, evaluate=False), c: 1}] + + assert iterdicteq(result, expected) + +def test_hard_match(): + from sympy.functions.elementary.trigonometric import (cos, sin) + expr = sin(x) + cos(x)**2 + p, q = map(Symbol, 'pq') + pattern = sin(p) + cos(p)**2 + assert list(unify(expr, pattern, {}, (p, q))) == [{p: x}] + +def test_matrix(): + from sympy.matrices.expressions.matexpr import MatrixSymbol + X = MatrixSymbol('X', n, n) + Y = MatrixSymbol('Y', 2, 2) + Z = MatrixSymbol('Z', 2, 3) + assert list(unify(X, Y, {}, variables=[n, Str('X')])) == [{Str('X'): Str('Y'), n: 2}] + assert list(unify(X, Z, {}, variables=[n, Str('X')])) == [] + +def test_non_frankenAdds(): + # the is_commutative property used to fail because of Basic.__new__ + # This caused is_commutative and str calls to fail + expr = x+y*2 + rebuilt = construct(deconstruct(expr)) + # Ensure that we can run these commands without causing an error + str(rebuilt) + rebuilt.is_commutative + +def test_FiniteSet_commutivity(): + from sympy.sets.sets import FiniteSet + a, b, c, x, y = symbols('a,b,c,x,y') + s = FiniteSet(a, b, c) + t = FiniteSet(x, y) + variables = (x, y) + assert {x: FiniteSet(a, c), y: b} in tuple(unify(s, t, variables=variables)) + +def test_FiniteSet_complex(): + from sympy.sets.sets import FiniteSet + a, b, c, x, y, z = symbols('a,b,c,x,y,z') + expr = FiniteSet(Basic(S(1), x), y, Basic(x, z)) + pattern = FiniteSet(a, Basic(x, b)) + variables = a, b + expected = ({b: 1, a: FiniteSet(y, Basic(x, z))}, + {b: z, a: FiniteSet(y, Basic(S(1), x))}) + assert iterdicteq(unify(expr, pattern, variables=variables), expected) + + +def test_and(): + variables = x, y + expected = ({x: z > 0, y: n < 3},) + assert iterdicteq(unify((z>0) & (n<3), And(x, y), variables=variables), + expected) + +def test_Union(): + from sympy.sets.sets import Interval + assert list(unify(Interval(0, 1) + Interval(10, 11), + Interval(0, 1) + Interval(12, 13), + variables=(Interval(12, 13),))) + +def test_is_commutative(): + assert is_commutative(deconstruct(x+y)) + assert is_commutative(deconstruct(x*y)) + assert not is_commutative(deconstruct(x**y)) + +def test_commutative_in_commutative(): + from sympy.abc import a,b,c,d + from sympy.functions.elementary.trigonometric import (cos, sin) + eq = sin(3)*sin(4)*sin(5) + 4*cos(3)*cos(4) + pat = a*cos(b)*cos(c) + d*sin(b)*sin(c) + assert next(unify(eq, pat, variables=(a,b,c,d))) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/tests/test_unify.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/tests/test_unify.py new file mode 100644 index 0000000000000000000000000000000000000000..31153242576e1ff55dd3097efbc985aced5d574a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/tests/test_unify.py @@ -0,0 +1,88 @@ +from sympy.unify.core import Compound, Variable, CondVariable, allcombinations +from sympy.unify import core + +a,b,c = 'a', 'b', 'c' +w,x,y,z = map(Variable, 'wxyz') + +C = Compound + +def is_associative(x): + return isinstance(x, Compound) and (x.op in ('Add', 'Mul', 'CAdd', 'CMul')) +def is_commutative(x): + return isinstance(x, Compound) and (x.op in ('CAdd', 'CMul')) + + +def unify(a, b, s={}): + return core.unify(a, b, s=s, is_associative=is_associative, + is_commutative=is_commutative) + +def test_basic(): + assert list(unify(a, x, {})) == [{x: a}] + assert list(unify(a, x, {x: 10})) == [] + assert list(unify(1, x, {})) == [{x: 1}] + assert list(unify(a, a, {})) == [{}] + assert list(unify((w, x), (y, z), {})) == [{w: y, x: z}] + assert list(unify(x, (a, b), {})) == [{x: (a, b)}] + + assert list(unify((a, b), (x, x), {})) == [] + assert list(unify((y, z), (x, x), {}))!= [] + assert list(unify((a, (b, c)), (a, (x, y)), {})) == [{x: b, y: c}] + +def test_ops(): + assert list(unify(C('Add', (a,b,c)), C('Add', (a,x,y)), {})) == \ + [{x:b, y:c}] + assert list(unify(C('Add', (C('Mul', (1,2)), b,c)), C('Add', (x,y,c)), {})) == \ + [{x: C('Mul', (1,2)), y:b}] + +def test_associative(): + c1 = C('Add', (1,2,3)) + c2 = C('Add', (x,y)) + assert tuple(unify(c1, c2, {})) == ({x: 1, y: C('Add', (2, 3))}, + {x: C('Add', (1, 2)), y: 3}) + +def test_commutative(): + c1 = C('CAdd', (1,2,3)) + c2 = C('CAdd', (x,y)) + result = list(unify(c1, c2, {})) + assert {x: 1, y: C('CAdd', (2, 3))} in result + assert ({x: 2, y: C('CAdd', (1, 3))} in result or + {x: 2, y: C('CAdd', (3, 1))} in result) + +def _test_combinations_assoc(): + assert set(allcombinations((1,2,3), (a,b), True)) == \ + {(((1, 2), (3,)), (a, b)), (((1,), (2, 3)), (a, b))} + +def _test_combinations_comm(): + assert set(allcombinations((1,2,3), (a,b), None)) == \ + {(((1,), (2, 3)), ('a', 'b')), (((2,), (3, 1)), ('a', 'b')), + (((3,), (1, 2)), ('a', 'b')), (((1, 2), (3,)), ('a', 'b')), + (((2, 3), (1,)), ('a', 'b')), (((3, 1), (2,)), ('a', 'b'))} + +def test_allcombinations(): + assert set(allcombinations((1,2), (1,2), 'commutative')) ==\ + {(((1,),(2,)), ((1,),(2,))), (((1,),(2,)), ((2,),(1,)))} + + +def test_commutativity(): + c1 = Compound('CAdd', (a, b)) + c2 = Compound('CAdd', (x, y)) + assert is_commutative(c1) and is_commutative(c2) + assert len(list(unify(c1, c2, {}))) == 2 + + +def test_CondVariable(): + expr = C('CAdd', (1, 2)) + x = Variable('x') + y = CondVariable('y', lambda a: a % 2 == 0) + z = CondVariable('z', lambda a: a > 3) + pattern = C('CAdd', (x, y)) + assert list(unify(expr, pattern, {})) == \ + [{x: 1, y: 2}] + + z = CondVariable('z', lambda a: a > 3) + pattern = C('CAdd', (z, y)) + + assert list(unify(expr, pattern, {})) == [] + +def test_defaultdict(): + assert next(unify(Variable('x'), 'foo')) == {Variable('x'): 'foo'} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/usympy.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/usympy.py new file mode 100644 index 0000000000000000000000000000000000000000..3942b35ec549e5dbd08a3cf1cad2b2ecea733c7a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/unify/usympy.py @@ -0,0 +1,124 @@ +""" SymPy interface to Unification engine + +See sympy.unify for module level docstring +See sympy.unify.core for algorithmic docstring """ + +from sympy.core import Basic, Add, Mul, Pow +from sympy.core.operations import AssocOp, LatticeOp +from sympy.matrices import MatAdd, MatMul, MatrixExpr +from sympy.sets.sets import Union, Intersection, FiniteSet +from sympy.unify.core import Compound, Variable, CondVariable +from sympy.unify import core + +basic_new_legal = [MatrixExpr] +eval_false_legal = [AssocOp, Pow, FiniteSet] +illegal = [LatticeOp] + +def sympy_associative(op): + assoc_ops = (AssocOp, MatAdd, MatMul, Union, Intersection, FiniteSet) + return any(issubclass(op, aop) for aop in assoc_ops) + +def sympy_commutative(op): + comm_ops = (Add, MatAdd, Union, Intersection, FiniteSet) + return any(issubclass(op, cop) for cop in comm_ops) + +def is_associative(x): + return isinstance(x, Compound) and sympy_associative(x.op) + +def is_commutative(x): + if not isinstance(x, Compound): + return False + if sympy_commutative(x.op): + return True + if issubclass(x.op, Mul): + return all(construct(arg).is_commutative for arg in x.args) + +def mk_matchtype(typ): + def matchtype(x): + return (isinstance(x, typ) or + isinstance(x, Compound) and issubclass(x.op, typ)) + return matchtype + +def deconstruct(s, variables=()): + """ Turn a SymPy object into a Compound """ + if s in variables: + return Variable(s) + if isinstance(s, (Variable, CondVariable)): + return s + if not isinstance(s, Basic) or s.is_Atom: + return s + return Compound(s.__class__, + tuple(deconstruct(arg, variables) for arg in s.args)) + +def construct(t): + """ Turn a Compound into a SymPy object """ + if isinstance(t, (Variable, CondVariable)): + return t.arg + if not isinstance(t, Compound): + return t + if any(issubclass(t.op, cls) for cls in eval_false_legal): + return t.op(*map(construct, t.args), evaluate=False) + elif any(issubclass(t.op, cls) for cls in basic_new_legal): + return Basic.__new__(t.op, *map(construct, t.args)) + else: + return t.op(*map(construct, t.args)) + +def rebuild(s): + """ Rebuild a SymPy expression. + + This removes harm caused by Expr-Rules interactions. + """ + return construct(deconstruct(s)) + +def unify(x, y, s=None, variables=(), **kwargs): + """ Structural unification of two expressions/patterns. + + Examples + ======== + + >>> from sympy.unify.usympy import unify + >>> from sympy import Basic, S + >>> from sympy.abc import x, y, z, p, q + + >>> next(unify(Basic(S(1), S(2)), Basic(S(1), x), variables=[x])) + {x: 2} + + >>> expr = 2*x + y + z + >>> pattern = 2*p + q + >>> next(unify(expr, pattern, {}, variables=(p, q))) + {p: x, q: y + z} + + Unification supports commutative and associative matching + + >>> expr = x + y + z + >>> pattern = p + q + >>> len(list(unify(expr, pattern, {}, variables=(p, q)))) + 12 + + Symbols not indicated to be variables are treated as literal, + else they are wild-like and match anything in a sub-expression. + + >>> expr = x*y*z + 3 + >>> pattern = x*y + 3 + >>> next(unify(expr, pattern, {}, variables=[x, y])) + {x: y, y: x*z} + + The x and y of the pattern above were in a Mul and matched factors + in the Mul of expr. Here, a single symbol matches an entire term: + + >>> expr = x*y + 3 + >>> pattern = p + 3 + >>> next(unify(expr, pattern, {}, variables=[p])) + {p: x*y} + + """ + decons = lambda x: deconstruct(x, variables) + s = s or {} + s = {decons(k): decons(v) for k, v in s.items()} + + ds = core.unify(decons(x), decons(y), s, + is_associative=is_associative, + is_commutative=is_commutative, + **kwargs) + for d in ds: + yield {construct(k): construct(v) for k, v in d.items()} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8f35da4a84396618a33a12c3c6b5cf58e9d4742c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/__init__.py @@ -0,0 +1,30 @@ +"""This module contains some general purpose utilities that are used across +SymPy. +""" +from .iterables import (flatten, group, take, subsets, + variations, numbered_symbols, cartes, capture, dict_merge, + prefixes, postfixes, sift, topological_sort, unflatten, + has_dups, has_variety, reshape, rotations) + +from .misc import filldedent + +from .lambdify import lambdify + +from .decorator import threaded, xthreaded, public, memoize_property + +from .timeutils import timed + +__all__ = [ + 'flatten', 'group', 'take', 'subsets', 'variations', 'numbered_symbols', + 'cartes', 'capture', 'dict_merge', 'prefixes', 'postfixes', 'sift', + 'topological_sort', 'unflatten', 'has_dups', 'has_variety', 'reshape', + 'rotations', + + 'filldedent', + + 'lambdify', + + 'threaded', 'xthreaded', 'public', 'memoize_property', + + 'timed', +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/_compilation/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/_compilation/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d2c05ad48a93493bb5434256c88dfd614ac47b2d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/_compilation/__init__.py @@ -0,0 +1,22 @@ +""" This sub-module is private, i.e. external code should not depend on it. + +These functions are used by tests run as part of continuous integration. +Once the implementation is mature (it should support the major +platforms: Windows, OS X & Linux) it may become official API which + may be relied upon by downstream libraries. Until then API may break +without prior notice. + +TODO: +- (optionally) clean up after tempfile.mkdtemp() +- cross-platform testing +- caching of compiler choice and intermediate files + +""" + +from .compilation import compile_link_import_strings, compile_run_strings +from .availability import has_fortran, has_c, has_cxx + +__all__ = [ + 'compile_link_import_strings', 'compile_run_strings', + 'has_fortran', 'has_c', 'has_cxx', +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/_compilation/availability.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/_compilation/availability.py new file mode 100644 index 0000000000000000000000000000000000000000..dc97b3e7b8c7e7307c6c21352ed4035d977aabb3 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/_compilation/availability.py @@ -0,0 +1,77 @@ +import os +from .compilation import compile_run_strings +from .util import CompilerNotFoundError + +def has_fortran(): + if not hasattr(has_fortran, 'result'): + try: + (stdout, stderr), info = compile_run_strings( + [('main.f90', ( + 'program foo\n' + 'print *, "hello world"\n' + 'end program' + ))], clean=True + ) + except CompilerNotFoundError: + has_fortran.result = False + if os.environ.get('SYMPY_STRICT_COMPILER_CHECKS', '0') == '1': + raise + else: + if info['exit_status'] != os.EX_OK or 'hello world' not in stdout: + if os.environ.get('SYMPY_STRICT_COMPILER_CHECKS', '0') == '1': + raise ValueError("Failed to compile test program:\n%s\n%s\n" % (stdout, stderr)) + has_fortran.result = False + else: + has_fortran.result = True + return has_fortran.result + + +def has_c(): + if not hasattr(has_c, 'result'): + try: + (stdout, stderr), info = compile_run_strings( + [('main.c', ( + '#include \n' + 'int main(){\n' + 'printf("hello world\\n");\n' + 'return 0;\n' + '}' + ))], clean=True + ) + except CompilerNotFoundError: + has_c.result = False + if os.environ.get('SYMPY_STRICT_COMPILER_CHECKS', '0') == '1': + raise + else: + if info['exit_status'] != os.EX_OK or 'hello world' not in stdout: + if os.environ.get('SYMPY_STRICT_COMPILER_CHECKS', '0') == '1': + raise ValueError("Failed to compile test program:\n%s\n%s\n" % (stdout, stderr)) + has_c.result = False + else: + has_c.result = True + return has_c.result + + +def has_cxx(): + if not hasattr(has_cxx, 'result'): + try: + (stdout, stderr), info = compile_run_strings( + [('main.cxx', ( + '#include \n' + 'int main(){\n' + 'std::cout << "hello world" << std::endl;\n' + '}' + ))], clean=True + ) + except CompilerNotFoundError: + has_cxx.result = False + if os.environ.get('SYMPY_STRICT_COMPILER_CHECKS', '0') == '1': + raise + else: + if info['exit_status'] != os.EX_OK or 'hello world' not in stdout: + if os.environ.get('SYMPY_STRICT_COMPILER_CHECKS', '0') == '1': + raise ValueError("Failed to compile test program:\n%s\n%s\n" % (stdout, stderr)) + has_cxx.result = False + else: + has_cxx.result = True + return has_cxx.result diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/_compilation/compilation.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/_compilation/compilation.py new file mode 100644 index 0000000000000000000000000000000000000000..2d50a20467c08086d6cb5fb5b0d478e7a953d720 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/_compilation/compilation.py @@ -0,0 +1,657 @@ +import glob +import os +import shutil +import subprocess +import sys +import tempfile +import warnings +from pathlib import Path +from sysconfig import get_config_var, get_config_vars, get_path + +from .runners import ( + CCompilerRunner, + CppCompilerRunner, + FortranCompilerRunner +) +from .util import ( + get_abspath, make_dirs, copy, Glob, ArbitraryDepthGlob, + glob_at_depth, import_module_from_file, pyx_is_cplus, + sha256_of_string, sha256_of_file, CompileError +) + +if os.name == 'posix': + objext = '.o' +elif os.name == 'nt': + objext = '.obj' +else: + warnings.warn("Unknown os.name: {}".format(os.name)) + objext = '.o' + + +def compile_sources(files, Runner=None, destdir=None, cwd=None, keep_dir_struct=False, + per_file_kwargs=None, **kwargs): + """ Compile source code files to object files. + + Parameters + ========== + + files : iterable of str + Paths to source files, if ``cwd`` is given, the paths are taken as relative. + Runner: CompilerRunner subclass (optional) + Could be e.g. ``FortranCompilerRunner``. Will be inferred from filename + extensions if missing. + destdir: str + Output directory, if cwd is given, the path is taken as relative. + cwd: str + Working directory. Specify to have compiler run in other directory. + also used as root of relative paths. + keep_dir_struct: bool + Reproduce directory structure in `destdir`. default: ``False`` + per_file_kwargs: dict + Dict mapping instances in ``files`` to keyword arguments. + \\*\\*kwargs: dict + Default keyword arguments to pass to ``Runner``. + + Returns + ======= + List of strings (paths of object files). + """ + _per_file_kwargs = {} + + if per_file_kwargs is not None: + for k, v in per_file_kwargs.items(): + if isinstance(k, Glob): + for path in glob.glob(k.pathname): + _per_file_kwargs[path] = v + elif isinstance(k, ArbitraryDepthGlob): + for path in glob_at_depth(k.filename, cwd): + _per_file_kwargs[path] = v + else: + _per_file_kwargs[k] = v + + # Set up destination directory + destdir = destdir or '.' + if not os.path.isdir(destdir): + if os.path.exists(destdir): + raise OSError("{} is not a directory".format(destdir)) + else: + make_dirs(destdir) + if cwd is None: + cwd = '.' + for f in files: + copy(f, destdir, only_update=True, dest_is_dir=True) + + # Compile files and return list of paths to the objects + dstpaths = [] + for f in files: + if keep_dir_struct: + name, ext = os.path.splitext(f) + else: + name, ext = os.path.splitext(os.path.basename(f)) + file_kwargs = kwargs.copy() + file_kwargs.update(_per_file_kwargs.get(f, {})) + dstpaths.append(src2obj(f, Runner, cwd=cwd, **file_kwargs)) + return dstpaths + + +def get_mixed_fort_c_linker(vendor=None, cplus=False, cwd=None): + vendor = vendor or os.environ.get('SYMPY_COMPILER_VENDOR', 'gnu') + + if vendor.lower() == 'intel': + if cplus: + return (FortranCompilerRunner, + {'flags': ['-nofor_main', '-cxxlib']}, vendor) + else: + return (FortranCompilerRunner, + {'flags': ['-nofor_main']}, vendor) + elif vendor.lower() == 'gnu' or 'llvm': + if cplus: + return (CppCompilerRunner, + {'lib_options': ['fortran']}, vendor) + else: + return (FortranCompilerRunner, + {}, vendor) + else: + raise ValueError("No vendor found.") + + +def link(obj_files, out_file=None, shared=False, Runner=None, + cwd=None, cplus=False, fort=False, extra_objs=None, **kwargs): + """ Link object files. + + Parameters + ========== + + obj_files: iterable of str + Paths to object files. + out_file: str (optional) + Path to executable/shared library, if ``None`` it will be + deduced from the last item in obj_files. + shared: bool + Generate a shared library? + Runner: CompilerRunner subclass (optional) + If not given the ``cplus`` and ``fort`` flags will be inspected + (fallback is the C compiler). + cwd: str + Path to the root of relative paths and working directory for compiler. + cplus: bool + C++ objects? default: ``False``. + fort: bool + Fortran objects? default: ``False``. + extra_objs: list + List of paths to extra object files / static libraries. + \\*\\*kwargs: dict + Keyword arguments passed to ``Runner``. + + Returns + ======= + + The absolute path to the generated shared object / executable. + + """ + if out_file is None: + out_file, ext = os.path.splitext(os.path.basename(obj_files[-1])) + if shared: + out_file += get_config_var('EXT_SUFFIX') + + if not Runner: + if fort: + Runner, extra_kwargs, vendor = \ + get_mixed_fort_c_linker( + vendor=kwargs.get('vendor', None), + cplus=cplus, + cwd=cwd, + ) + for k, v in extra_kwargs.items(): + if k in kwargs: + kwargs[k].expand(v) + else: + kwargs[k] = v + else: + if cplus: + Runner = CppCompilerRunner + else: + Runner = CCompilerRunner + + flags = kwargs.pop('flags', []) + if shared: + if '-shared' not in flags: + flags.append('-shared') + run_linker = kwargs.pop('run_linker', True) + if not run_linker: + raise ValueError("run_linker was set to False (nonsensical).") + + out_file = get_abspath(out_file, cwd=cwd) + runner = Runner(obj_files+(extra_objs or []), out_file, flags, cwd=cwd, **kwargs) + runner.run() + return out_file + + +def link_py_so(obj_files, so_file=None, cwd=None, libraries=None, + cplus=False, fort=False, extra_objs=None, **kwargs): + """ Link Python extension module (shared object) for importing + + Parameters + ========== + + obj_files: iterable of str + Paths to object files to be linked. + so_file: str + Name (path) of shared object file to create. If not specified it will + have the basname of the last object file in `obj_files` but with the + extension '.so' (Unix). + cwd: path string + Root of relative paths and working directory of linker. + libraries: iterable of strings + Libraries to link against, e.g. ['m']. + cplus: bool + Any C++ objects? default: ``False``. + fort: bool + Any Fortran objects? default: ``False``. + extra_objs: list + List of paths of extra object files / static libraries to link against. + kwargs**: dict + Keyword arguments passed to ``link(...)``. + + Returns + ======= + + Absolute path to the generate shared object. + """ + libraries = libraries or [] + + include_dirs = kwargs.pop('include_dirs', []) + library_dirs = kwargs.pop('library_dirs', []) + + # Add Python include and library directories + # PY_LDFLAGS does not available on all python implementations + # e.g. when with pypy, so it's LDFLAGS we need to use + if sys.platform == "win32": + warnings.warn("Windows not yet supported.") + elif sys.platform == 'darwin': + cfgDict = get_config_vars() + kwargs['linkline'] = kwargs.get('linkline', []) + [cfgDict['LDFLAGS']] + library_dirs += [cfgDict['LIBDIR']] + + # In macOS, linker needs to compile frameworks + # e.g. "-framework CoreFoundation" + is_framework = False + for opt in cfgDict['LIBS'].split(): + if is_framework: + kwargs['linkline'] = kwargs.get('linkline', []) + ['-framework', opt] + is_framework = False + elif opt.startswith('-l'): + libraries.append(opt[2:]) + elif opt.startswith('-framework'): + is_framework = True + # The python library is not included in LIBS + libfile = cfgDict['LIBRARY'] + libname = ".".join(libfile.split('.')[:-1])[3:] + libraries.append(libname) + + elif sys.platform[:3] == 'aix': + # Don't use the default code below + pass + else: + if get_config_var('Py_ENABLE_SHARED'): + cfgDict = get_config_vars() + kwargs['linkline'] = kwargs.get('linkline', []) + [cfgDict['LDFLAGS']] + library_dirs += [cfgDict['LIBDIR']] + for opt in cfgDict['BLDLIBRARY'].split(): + if opt.startswith('-l'): + libraries += [opt[2:]] + else: + pass + + flags = kwargs.pop('flags', []) + needed_flags = ('-pthread',) + for flag in needed_flags: + if flag not in flags: + flags.append(flag) + + return link(obj_files, shared=True, flags=flags, cwd=cwd, cplus=cplus, fort=fort, + include_dirs=include_dirs, libraries=libraries, + library_dirs=library_dirs, extra_objs=extra_objs, **kwargs) + + +def simple_cythonize(src, destdir=None, cwd=None, **cy_kwargs): + """ Generates a C file from a Cython source file. + + Parameters + ========== + + src: str + Path to Cython source. + destdir: str (optional) + Path to output directory (default: '.'). + cwd: path string (optional) + Root of relative paths (default: '.'). + **cy_kwargs: + Second argument passed to cy_compile. Generates a .cpp file if ``cplus=True`` in ``cy_kwargs``, + else a .c file. + """ + from Cython.Compiler.Main import ( + default_options, CompilationOptions + ) + from Cython.Compiler.Main import compile as cy_compile + + assert src.lower().endswith('.pyx') or src.lower().endswith('.py') + cwd = cwd or '.' + destdir = destdir or '.' + + ext = '.cpp' if cy_kwargs.get('cplus', False) else '.c' + c_name = os.path.splitext(os.path.basename(src))[0] + ext + + dstfile = os.path.join(destdir, c_name) + + if cwd: + ori_dir = os.getcwd() + else: + ori_dir = '.' + os.chdir(cwd) + try: + cy_options = CompilationOptions(default_options) + cy_options.__dict__.update(cy_kwargs) + # Set language_level if not set by cy_kwargs + # as not setting it is deprecated + if 'language_level' not in cy_kwargs: + cy_options.__dict__['language_level'] = 3 + cy_result = cy_compile([src], cy_options) + if cy_result.num_errors > 0: + raise ValueError("Cython compilation failed.") + + # Move generated C file to destination + # In macOS, the generated C file is in the same directory as the source + # but the /var is a symlink to /private/var, so we need to use realpath + if os.path.realpath(os.path.dirname(src)) != os.path.realpath(destdir): + if os.path.exists(dstfile): + os.unlink(dstfile) + shutil.move(os.path.join(os.path.dirname(src), c_name), destdir) + finally: + os.chdir(ori_dir) + return dstfile + + +extension_mapping = { + '.c': (CCompilerRunner, None), + '.cpp': (CppCompilerRunner, None), + '.cxx': (CppCompilerRunner, None), + '.f': (FortranCompilerRunner, None), + '.for': (FortranCompilerRunner, None), + '.ftn': (FortranCompilerRunner, None), + '.f90': (FortranCompilerRunner, None), # ifort only knows about .f90 + '.f95': (FortranCompilerRunner, 'f95'), + '.f03': (FortranCompilerRunner, 'f2003'), + '.f08': (FortranCompilerRunner, 'f2008'), +} + + +def src2obj(srcpath, Runner=None, objpath=None, cwd=None, inc_py=False, **kwargs): + """ Compiles a source code file to an object file. + + Files ending with '.pyx' assumed to be cython files and + are dispatched to pyx2obj. + + Parameters + ========== + + srcpath: str + Path to source file. + Runner: CompilerRunner subclass (optional) + If ``None``: deduced from extension of srcpath. + objpath : str (optional) + Path to generated object. If ``None``: deduced from ``srcpath``. + cwd: str (optional) + Working directory and root of relative paths. If ``None``: current dir. + inc_py: bool + Add Python include path to kwarg "include_dirs". Default: False + \\*\\*kwargs: dict + keyword arguments passed to Runner or pyx2obj + + """ + name, ext = os.path.splitext(os.path.basename(srcpath)) + if objpath is None: + if os.path.isabs(srcpath): + objpath = '.' + else: + objpath = os.path.dirname(srcpath) + objpath = objpath or '.' # avoid objpath == '' + + if os.path.isdir(objpath): + objpath = os.path.join(objpath, name + objext) + + include_dirs = kwargs.pop('include_dirs', []) + if inc_py: + py_inc_dir = get_path('include') + if py_inc_dir not in include_dirs: + include_dirs.append(py_inc_dir) + + if ext.lower() == '.pyx': + return pyx2obj(srcpath, objpath=objpath, include_dirs=include_dirs, cwd=cwd, + **kwargs) + + if Runner is None: + Runner, std = extension_mapping[ext.lower()] + if 'std' not in kwargs: + kwargs['std'] = std + + flags = kwargs.pop('flags', []) + needed_flags = ('-fPIC',) + for flag in needed_flags: + if flag not in flags: + flags.append(flag) + + # src2obj implies not running the linker... + run_linker = kwargs.pop('run_linker', False) + if run_linker: + raise CompileError("src2obj called with run_linker=True") + + runner = Runner([srcpath], objpath, include_dirs=include_dirs, + run_linker=run_linker, cwd=cwd, flags=flags, **kwargs) + runner.run() + return objpath + + +def pyx2obj(pyxpath, objpath=None, destdir=None, cwd=None, + include_dirs=None, cy_kwargs=None, cplus=None, **kwargs): + """ + Convenience function + + If cwd is specified, pyxpath and dst are taken to be relative + If only_update is set to `True` the modification time is checked + and compilation is only run if the source is newer than the + destination + + Parameters + ========== + + pyxpath: str + Path to Cython source file. + objpath: str (optional) + Path to object file to generate. + destdir: str (optional) + Directory to put generated C file. When ``None``: directory of ``objpath``. + cwd: str (optional) + Working directory and root of relative paths. + include_dirs: iterable of path strings (optional) + Passed onto src2obj and via cy_kwargs['include_path'] + to simple_cythonize. + cy_kwargs: dict (optional) + Keyword arguments passed onto `simple_cythonize` + cplus: bool (optional) + Indicate whether C++ is used. default: auto-detect using ``.util.pyx_is_cplus``. + compile_kwargs: dict + keyword arguments passed onto src2obj + + Returns + ======= + + Absolute path of generated object file. + + """ + assert pyxpath.endswith('.pyx') + cwd = cwd or '.' + objpath = objpath or '.' + destdir = destdir or os.path.dirname(objpath) + + abs_objpath = get_abspath(objpath, cwd=cwd) + + if os.path.isdir(abs_objpath): + pyx_fname = os.path.basename(pyxpath) + name, ext = os.path.splitext(pyx_fname) + objpath = os.path.join(objpath, name + objext) + + cy_kwargs = cy_kwargs or {} + cy_kwargs['output_dir'] = cwd + if cplus is None: + cplus = pyx_is_cplus(pyxpath) + cy_kwargs['cplus'] = cplus + + interm_c_file = simple_cythonize(pyxpath, destdir=destdir, cwd=cwd, **cy_kwargs) + + include_dirs = include_dirs or [] + flags = kwargs.pop('flags', []) + needed_flags = ('-fwrapv', '-pthread', '-fPIC') + for flag in needed_flags: + if flag not in flags: + flags.append(flag) + + options = kwargs.pop('options', []) + + if kwargs.pop('strict_aliasing', False): + raise CompileError("Cython requires strict aliasing to be disabled.") + + # Let's be explicit about standard + if cplus: + std = kwargs.pop('std', 'c++98') + else: + std = kwargs.pop('std', 'c99') + + return src2obj(interm_c_file, objpath=objpath, cwd=cwd, + include_dirs=include_dirs, flags=flags, std=std, + options=options, inc_py=True, strict_aliasing=False, + **kwargs) + + +def _any_X(srcs, cls): + for src in srcs: + name, ext = os.path.splitext(src) + key = ext.lower() + if key in extension_mapping: + if extension_mapping[key][0] == cls: + return True + return False + + +def any_fortran_src(srcs): + return _any_X(srcs, FortranCompilerRunner) + + +def any_cplus_src(srcs): + return _any_X(srcs, CppCompilerRunner) + + +def compile_link_import_py_ext(sources, extname=None, build_dir='.', compile_kwargs=None, + link_kwargs=None, extra_objs=None): + """ Compiles sources to a shared object (Python extension) and imports it + + Sources in ``sources`` which is imported. If shared object is newer than the sources, they + are not recompiled but instead it is imported. + + Parameters + ========== + + sources : list of strings + List of paths to sources. + extname : string + Name of extension (default: ``None``). + If ``None``: taken from the last file in ``sources`` without extension. + build_dir: str + Path to directory in which objects files etc. are generated. + compile_kwargs: dict + keyword arguments passed to ``compile_sources`` + link_kwargs: dict + keyword arguments passed to ``link_py_so`` + extra_objs: list + List of paths to (prebuilt) object files / static libraries to link against. + + Returns + ======= + + The imported module from of the Python extension. + """ + if extname is None: + extname = os.path.splitext(os.path.basename(sources[-1]))[0] + + compile_kwargs = compile_kwargs or {} + link_kwargs = link_kwargs or {} + + try: + mod = import_module_from_file(os.path.join(build_dir, extname), sources) + except ImportError: + objs = compile_sources(list(map(get_abspath, sources)), destdir=build_dir, + cwd=build_dir, **compile_kwargs) + so = link_py_so(objs, cwd=build_dir, fort=any_fortran_src(sources), + cplus=any_cplus_src(sources), extra_objs=extra_objs, **link_kwargs) + mod = import_module_from_file(so) + return mod + + +def _write_sources_to_build_dir(sources, build_dir): + build_dir = build_dir or tempfile.mkdtemp() + if not os.path.isdir(build_dir): + raise OSError("Non-existent directory: ", build_dir) + + source_files = [] + for name, src in sources: + dest = os.path.join(build_dir, name) + differs = True + sha256_in_mem = sha256_of_string(src.encode('utf-8')).hexdigest() + if os.path.exists(dest): + if os.path.exists(dest + '.sha256'): + sha256_on_disk = Path(dest + '.sha256').read_text() + else: + sha256_on_disk = sha256_of_file(dest).hexdigest() + + differs = sha256_on_disk != sha256_in_mem + if differs: + with open(dest, 'wt') as fh: + fh.write(src) + with open(dest + '.sha256', 'wt') as fh: + fh.write(sha256_in_mem) + source_files.append(dest) + return source_files, build_dir + + +def compile_link_import_strings(sources, build_dir=None, **kwargs): + """ Compiles, links and imports extension module from source. + + Parameters + ========== + + sources : iterable of name/source pair tuples + build_dir : string (default: None) + Path. ``None`` implies use a temporary directory. + **kwargs: + Keyword arguments passed onto `compile_link_import_py_ext`. + + Returns + ======= + + mod : module + The compiled and imported extension module. + info : dict + Containing ``build_dir`` as 'build_dir'. + + """ + source_files, build_dir = _write_sources_to_build_dir(sources, build_dir) + mod = compile_link_import_py_ext(source_files, build_dir=build_dir, **kwargs) + info = {"build_dir": build_dir} + return mod, info + + +def compile_run_strings(sources, build_dir=None, clean=False, compile_kwargs=None, link_kwargs=None): + """ Compiles, links and runs a program built from sources. + + Parameters + ========== + + sources : iterable of name/source pair tuples + build_dir : string (default: None) + Path. ``None`` implies use a temporary directory. + clean : bool + Whether to remove build_dir after use. This will only have an + effect if ``build_dir`` is ``None`` (which creates a temporary directory). + Passing ``clean == True`` and ``build_dir != None`` raises a ``ValueError``. + This will also set ``build_dir`` in returned info dictionary to ``None``. + compile_kwargs: dict + Keyword arguments passed onto ``compile_sources`` + link_kwargs: dict + Keyword arguments passed onto ``link`` + + Returns + ======= + + (stdout, stderr): pair of strings + info: dict + Containing exit status as 'exit_status' and ``build_dir`` as 'build_dir' + + """ + if clean and build_dir is not None: + raise ValueError("Automatic removal of build_dir is only available for temporary directory.") + try: + source_files, build_dir = _write_sources_to_build_dir(sources, build_dir) + objs = compile_sources(list(map(get_abspath, source_files)), destdir=build_dir, + cwd=build_dir, **(compile_kwargs or {})) + prog = link(objs, cwd=build_dir, + fort=any_fortran_src(source_files), + cplus=any_cplus_src(source_files), **(link_kwargs or {})) + p = subprocess.Popen([prog], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + exit_status = p.wait() + stdout, stderr = [txt.decode('utf-8') for txt in p.communicate()] + finally: + if clean and os.path.isdir(build_dir): + shutil.rmtree(build_dir) + build_dir = None + info = {"exit_status": exit_status, "build_dir": build_dir} + return (stdout, stderr), info diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/_compilation/runners.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/_compilation/runners.py new file mode 100644 index 0000000000000000000000000000000000000000..1f37d6cf8ac47807da7f3f00dfc5cd847c03fa8d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/_compilation/runners.py @@ -0,0 +1,301 @@ +from __future__ import annotations +from typing import Callable, Optional + +from collections import OrderedDict +import os +import re +import subprocess +import warnings + +from .util import ( + find_binary_of_command, unique_list, CompileError +) + + +class CompilerRunner: + """ CompilerRunner base class. + + Parameters + ========== + + sources : list of str + Paths to sources. + out : str + flags : iterable of str + Compiler flags. + run_linker : bool + compiler_name_exe : (str, str) tuple + Tuple of compiler name & command to call. + cwd : str + Path of root of relative paths. + include_dirs : list of str + Include directories. + libraries : list of str + Libraries to link against. + library_dirs : list of str + Paths to search for shared libraries. + std : str + Standard string, e.g. ``'c++11'``, ``'c99'``, ``'f2003'``. + define: iterable of strings + macros to define + undef : iterable of strings + macros to undefine + preferred_vendor : string + name of preferred vendor e.g. 'gnu' or 'intel' + + Methods + ======= + + run(): + Invoke compilation as a subprocess. + + """ + + environ_key_compiler: str # e.g. 'CC', 'CXX', ... + environ_key_flags: str # e.g. 'CFLAGS', 'CXXFLAGS', ... + environ_key_ldflags: str = "LDFLAGS" # typically 'LDFLAGS' + + # Subclass to vendor/binary dict + compiler_dict: dict[str, str] + + # Standards should be a tuple of supported standards + # (first one will be the default) + standards: tuple[None | str, ...] + + # Subclass to dict of binary/formater-callback + std_formater: dict[str, Callable[[Optional[str]], str]] + + # subclass to be e.g. {'gcc': 'gnu', ...} + compiler_name_vendor_mapping: dict[str, str] + + def __init__(self, sources, out, flags=None, run_linker=True, compiler=None, cwd='.', + include_dirs=None, libraries=None, library_dirs=None, std=None, define=None, + undef=None, strict_aliasing=None, preferred_vendor=None, linkline=None, **kwargs): + if isinstance(sources, str): + raise ValueError("Expected argument sources to be a list of strings.") + self.sources = list(sources) + self.out = out + self.flags = flags or [] + if os.environ.get(self.environ_key_flags): + self.flags += os.environ[self.environ_key_flags].split() + self.cwd = cwd + if compiler: + self.compiler_name, self.compiler_binary = compiler + elif os.environ.get(self.environ_key_compiler): + self.compiler_binary = os.environ[self.environ_key_compiler] + for k, v in self.compiler_dict.items(): + if k in self.compiler_binary: + self.compiler_vendor = k + self.compiler_name = v + break + else: + self.compiler_vendor, self.compiler_name = list(self.compiler_dict.items())[0] + warnings.warn("failed to determine what kind of compiler %s is, assuming %s" % + (self.compiler_binary, self.compiler_name)) + else: + # Find a compiler + if preferred_vendor is None: + preferred_vendor = os.environ.get('SYMPY_COMPILER_VENDOR', None) + self.compiler_name, self.compiler_binary, self.compiler_vendor = self.find_compiler(preferred_vendor) + if self.compiler_binary is None: + raise ValueError("No compiler found (searched: {})".format(', '.join(self.compiler_dict.values()))) + self.define = define or [] + self.undef = undef or [] + self.include_dirs = include_dirs or [] + self.libraries = libraries or [] + self.library_dirs = library_dirs or [] + self.std = std or self.standards[0] + self.run_linker = run_linker + if self.run_linker: + # both gnu and intel compilers use '-c' for disabling linker + self.flags = list(filter(lambda x: x != '-c', self.flags)) + else: + if '-c' not in self.flags: + self.flags.append('-c') + + if self.std: + self.flags.append(self.std_formater[ + self.compiler_name](self.std)) + + self.linkline = (linkline or []) + [lf for lf in map( + str.strip, os.environ.get(self.environ_key_ldflags, "").split() + ) if lf != ""] + + if strict_aliasing is not None: + nsa_re = re.compile("no-strict-aliasing$") + sa_re = re.compile("strict-aliasing$") + if strict_aliasing is True: + if any(map(nsa_re.match, flags)): + raise CompileError("Strict aliasing cannot be both enforced and disabled") + elif any(map(sa_re.match, flags)): + pass # already enforced + else: + flags.append('-fstrict-aliasing') + elif strict_aliasing is False: + if any(map(nsa_re.match, flags)): + pass # already disabled + else: + if any(map(sa_re.match, flags)): + raise CompileError("Strict aliasing cannot be both enforced and disabled") + else: + flags.append('-fno-strict-aliasing') + else: + msg = "Expected argument strict_aliasing to be True/False, got {}" + raise ValueError(msg.format(strict_aliasing)) + + @classmethod + def find_compiler(cls, preferred_vendor=None): + """ Identify a suitable C/fortran/other compiler. """ + candidates = list(cls.compiler_dict.keys()) + if preferred_vendor: + if preferred_vendor in candidates: + candidates = [preferred_vendor]+candidates + else: + raise ValueError("Unknown vendor {}".format(preferred_vendor)) + name, path = find_binary_of_command([cls.compiler_dict[x] for x in candidates]) + return name, path, cls.compiler_name_vendor_mapping[name] + + def cmd(self): + """ List of arguments (str) to be passed to e.g. ``subprocess.Popen``. """ + cmd = ( + [self.compiler_binary] + + self.flags + + ['-U'+x for x in self.undef] + + ['-D'+x for x in self.define] + + ['-I'+x for x in self.include_dirs] + + self.sources + ) + if self.run_linker: + cmd += (['-L'+x for x in self.library_dirs] + + ['-l'+x for x in self.libraries] + + self.linkline) + counted = [] + for envvar in re.findall(r'\$\{(\w+)\}', ' '.join(cmd)): + if os.getenv(envvar) is None: + if envvar not in counted: + counted.append(envvar) + msg = "Environment variable '{}' undefined.".format(envvar) + raise CompileError(msg) + return cmd + + def run(self): + self.flags = unique_list(self.flags) + + # Append output flag and name to tail of flags + self.flags.extend(['-o', self.out]) + env = os.environ.copy() + env['PWD'] = self.cwd + + # NOTE: intel compilers seems to need shell=True + p = subprocess.Popen(' '.join(self.cmd()), + shell=True, + cwd=self.cwd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env=env) + comm = p.communicate() + try: + self.cmd_outerr = comm[0].decode('utf-8') + except UnicodeDecodeError: + self.cmd_outerr = comm[0].decode('iso-8859-1') # win32 + self.cmd_returncode = p.returncode + + # Error handling + if self.cmd_returncode != 0: + msg = "Error executing '{}' in {} (exited status {}):\n {}\n".format( + ' '.join(self.cmd()), self.cwd, str(self.cmd_returncode), self.cmd_outerr + ) + raise CompileError(msg) + + return self.cmd_outerr, self.cmd_returncode + + +class CCompilerRunner(CompilerRunner): + + environ_key_compiler = 'CC' + environ_key_flags = 'CFLAGS' + + compiler_dict = OrderedDict([ + ('gnu', 'gcc'), + ('intel', 'icc'), + ('llvm', 'clang'), + ]) + + standards = ('c89', 'c90', 'c99', 'c11') # First is default + + std_formater = { + 'gcc': '-std={}'.format, + 'icc': '-std={}'.format, + 'clang': '-std={}'.format, + } + + compiler_name_vendor_mapping = { + 'gcc': 'gnu', + 'icc': 'intel', + 'clang': 'llvm' + } + + +def _mk_flag_filter(cmplr_name): # helper for class initialization + not_welcome = {'g++': ("Wimplicit-interface",)} # "Wstrict-prototypes",)} + if cmplr_name in not_welcome: + def fltr(x): + for nw in not_welcome[cmplr_name]: + if nw in x: + return False + return True + else: + def fltr(x): + return True + return fltr + + +class CppCompilerRunner(CompilerRunner): + + environ_key_compiler = 'CXX' + environ_key_flags = 'CXXFLAGS' + + compiler_dict = OrderedDict([ + ('gnu', 'g++'), + ('intel', 'icpc'), + ('llvm', 'clang++'), + ]) + + # First is the default, c++0x == c++11 + standards = ('c++98', 'c++0x') + + std_formater = { + 'g++': '-std={}'.format, + 'icpc': '-std={}'.format, + 'clang++': '-std={}'.format, + } + + compiler_name_vendor_mapping = { + 'g++': 'gnu', + 'icpc': 'intel', + 'clang++': 'llvm' + } + + +class FortranCompilerRunner(CompilerRunner): + + environ_key_compiler = 'FC' + environ_key_flags = 'FFLAGS' + + standards = (None, 'f77', 'f95', 'f2003', 'f2008') + + std_formater = { + 'gfortran': lambda x: '-std=gnu' if x is None else '-std=legacy' if x == 'f77' else '-std={}'.format(x), + 'ifort': lambda x: '-stand f08' if x is None else '-stand f{}'.format(x[-2:]), # f2008 => f08 + } + + compiler_dict = OrderedDict([ + ('gnu', 'gfortran'), + ('intel', 'ifort'), + ]) + + compiler_name_vendor_mapping = { + 'gfortran': 'gnu', + 'ifort': 'intel', + } diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/_compilation/tests/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/_compilation/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/_compilation/tests/test_compilation.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/_compilation/tests/test_compilation.py new file mode 100644 index 0000000000000000000000000000000000000000..ff7cf86644a9645665e62b49cfc4e7ea73b2c1ab --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/_compilation/tests/test_compilation.py @@ -0,0 +1,104 @@ +import shutil +import os +import subprocess +import tempfile +from sympy.external import import_module +from sympy.testing.pytest import skip, skip_under_pyodide + +from sympy.utilities._compilation.compilation import compile_link_import_py_ext, compile_link_import_strings, compile_sources, get_abspath + +numpy = import_module('numpy') +cython = import_module('cython') + +_sources1 = [ + ('sigmoid.c', r""" +#include + +void sigmoid(int n, const double * const restrict in, + double * const restrict out, double lim){ + for (int i=0; i 0: + if not os.path.exists(parent): + make_dirs(parent) + + if not os.path.exists(path): + os.mkdir(path, 0o777) + else: + assert os.path.isdir(path) + +def missing_or_other_newer(path, other_path, cwd=None): + """ + Investigate if path is non-existent or older than provided reference + path. + + Parameters + ========== + path: string + path to path which might be missing or too old + other_path: string + reference path + cwd: string + working directory (root of relative paths) + + Returns + ======= + True if path is older or missing. + """ + cwd = cwd or '.' + path = get_abspath(path, cwd=cwd) + other_path = get_abspath(other_path, cwd=cwd) + if not os.path.exists(path): + return True + if os.path.getmtime(other_path) - 1e-6 >= os.path.getmtime(path): + # 1e-6 is needed because http://stackoverflow.com/questions/17086426/ + return True + return False + +def copy(src, dst, only_update=False, copystat=True, cwd=None, + dest_is_dir=False, create_dest_dirs=False): + """ Variation of ``shutil.copy`` with extra options. + + Parameters + ========== + + src : str + Path to source file. + dst : str + Path to destination. + only_update : bool + Only copy if source is newer than destination + (returns None if it was newer), default: ``False``. + copystat : bool + See ``shutil.copystat``. default: ``True``. + cwd : str + Path to working directory (root of relative paths). + dest_is_dir : bool + Ensures that dst is treated as a directory. default: ``False`` + create_dest_dirs : bool + Creates directories if needed. + + Returns + ======= + + Path to the copied file. + + """ + if cwd: # Handle working directory + if not os.path.isabs(src): + src = os.path.join(cwd, src) + if not os.path.isabs(dst): + dst = os.path.join(cwd, dst) + + if not os.path.exists(src): # Make sure source file exists + raise FileNotFoundError("Source: `{}` does not exist".format(src)) + + # We accept both (re)naming destination file _or_ + # passing a (possible non-existent) destination directory + if dest_is_dir: + if not dst[-1] == '/': + dst = dst+'/' + else: + if os.path.exists(dst) and os.path.isdir(dst): + dest_is_dir = True + + if dest_is_dir: + dest_dir = dst + dest_fname = os.path.basename(src) + dst = os.path.join(dest_dir, dest_fname) + else: + dest_dir = os.path.dirname(dst) + + if not os.path.exists(dest_dir): + if create_dest_dirs: + make_dirs(dest_dir) + else: + raise FileNotFoundError("You must create directory first.") + + if only_update: + if not missing_or_other_newer(dst, src): + return + + if os.path.islink(dst): + dst = os.path.abspath(os.path.realpath(dst), cwd=cwd) + + shutil.copy(src, dst) + if copystat: + shutil.copystat(src, dst) + + return dst + +Glob = namedtuple('Glob', 'pathname') +ArbitraryDepthGlob = namedtuple('ArbitraryDepthGlob', 'filename') + +def glob_at_depth(filename_glob, cwd=None): + if cwd is not None: + cwd = '.' + globbed = [] + for root, dirs, filenames in os.walk(cwd): + for fn in filenames: + # This is not tested: + if fnmatch.fnmatch(fn, filename_glob): + globbed.append(os.path.join(root, fn)) + return globbed + +def sha256_of_file(path, nblocks=128): + """ Computes the SHA256 hash of a file. + + Parameters + ========== + + path : string + Path to file to compute hash of. + nblocks : int + Number of blocks to read per iteration. + + Returns + ======= + + hashlib sha256 hash object. Use ``.digest()`` or ``.hexdigest()`` + on returned object to get binary or hex encoded string. + """ + sh = sha256() + with open(path, 'rb') as f: + for chunk in iter(lambda: f.read(nblocks*sh.block_size), b''): + sh.update(chunk) + return sh + + +def sha256_of_string(string): + """ Computes the SHA256 hash of a string. """ + sh = sha256() + sh.update(string) + return sh + + +def pyx_is_cplus(path): + """ + Inspect a Cython source file (.pyx) and look for comment line like: + + # distutils: language = c++ + + Returns True if such a file is present in the file, else False. + """ + with open(path) as fh: + for line in fh: + if line.startswith('#') and '=' in line: + splitted = line.split('=') + if len(splitted) != 2: + continue + lhs, rhs = splitted + if lhs.strip().split()[-1].lower() == 'language' and \ + rhs.strip().split()[0].lower() == 'c++': + return True + return False + +def import_module_from_file(filename, only_if_newer_than=None): + """ Imports Python extension (from shared object file) + + Provide a list of paths in `only_if_newer_than` to check + timestamps of dependencies. import_ raises an ImportError + if any is newer. + + Word of warning: The OS may cache shared objects which makes + reimporting same path of an shared object file very problematic. + + It will not detect the new time stamp, nor new checksum, but will + instead silently use old module. Use unique names for this reason. + + Parameters + ========== + + filename : str + Path to shared object. + only_if_newer_than : iterable of strings + Paths to dependencies of the shared object. + + Raises + ====== + + ``ImportError`` if any of the files specified in ``only_if_newer_than`` are newer + than the file given by filename. + """ + path, name = os.path.split(filename) + name, ext = os.path.splitext(name) + name = name.split('.')[0] + if sys.version_info[0] == 2: + from imp import find_module, load_module + fobj, filename, data = find_module(name, [path]) + if only_if_newer_than: + for dep in only_if_newer_than: + if os.path.getmtime(filename) < os.path.getmtime(dep): + raise ImportError("{} is newer than {}".format(dep, filename)) + mod = load_module(name, fobj, filename, data) + else: + import importlib.util + spec = importlib.util.spec_from_file_location(name, filename) + if spec is None: + raise ImportError("Failed to import: '%s'" % filename) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +def find_binary_of_command(candidates): + """ Finds binary first matching name among candidates. + + Calls ``which`` from shutils for provided candidates and returns + first hit. + + Parameters + ========== + + candidates : iterable of str + Names of candidate commands + + Raises + ====== + + CompilerNotFoundError if no candidates match. + """ + from shutil import which + for c in candidates: + binary_path = which(c) + if c and binary_path: + return c, binary_path + + raise CompilerNotFoundError('No binary located for candidates: {}'.format(candidates)) + + +def unique_list(l): + """ Uniquify a list (skip duplicate items). """ + result = [] + for x in l: + if x not in result: + result.append(x) + return result diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/autowrap.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/autowrap.py new file mode 100644 index 0000000000000000000000000000000000000000..b6c33b2f0f72f89b5680f910929618a821e924c3 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/autowrap.py @@ -0,0 +1,1178 @@ +"""Module for compiling codegen output, and wrap the binary for use in +python. + +.. note:: To use the autowrap module it must first be imported + + >>> from sympy.utilities.autowrap import autowrap + +This module provides a common interface for different external backends, such +as f2py, fwrap, Cython, SWIG(?) etc. (Currently only f2py and Cython are +implemented) The goal is to provide access to compiled binaries of acceptable +performance with a one-button user interface, e.g., + + >>> from sympy.abc import x,y + >>> expr = (x - y)**25 + >>> flat = expr.expand() + >>> binary_callable = autowrap(flat) + >>> binary_callable(2, 3) + -1.0 + +Although a SymPy user might primarily be interested in working with +mathematical expressions and not in the details of wrapping tools +needed to evaluate such expressions efficiently in numerical form, +the user cannot do so without some understanding of the +limits in the target language. For example, the expanded expression +contains large coefficients which result in loss of precision when +computing the expression: + + >>> binary_callable(3, 2) + 0.0 + >>> binary_callable(4, 5), binary_callable(5, 4) + (-22925376.0, 25165824.0) + +Wrapping the unexpanded expression gives the expected behavior: + + >>> e = autowrap(expr) + >>> e(4, 5), e(5, 4) + (-1.0, 1.0) + +The callable returned from autowrap() is a binary Python function, not a +SymPy object. If it is desired to use the compiled function in symbolic +expressions, it is better to use binary_function() which returns a SymPy +Function object. The binary callable is attached as the _imp_ attribute and +invoked when a numerical evaluation is requested with evalf(), or with +lambdify(). + + >>> from sympy.utilities.autowrap import binary_function + >>> f = binary_function('f', expr) + >>> 2*f(x, y) + y + y + 2*f(x, y) + >>> (2*f(x, y) + y).evalf(2, subs={x: 1, y:2}) + 0.e-110 + +When is this useful? + + 1) For computations on large arrays, Python iterations may be too slow, + and depending on the mathematical expression, it may be difficult to + exploit the advanced index operations provided by NumPy. + + 2) For *really* long expressions that will be called repeatedly, the + compiled binary should be significantly faster than SymPy's .evalf() + + 3) If you are generating code with the codegen utility in order to use + it in another project, the automatic Python wrappers let you test the + binaries immediately from within SymPy. + + 4) To create customized ufuncs for use with numpy arrays. + See *ufuncify*. + +When is this module NOT the best approach? + + 1) If you are really concerned about speed or memory optimizations, + you will probably get better results by working directly with the + wrapper tools and the low level code. However, the files generated + by this utility may provide a useful starting point and reference + code. Temporary files will be left intact if you supply the keyword + tempdir="path/to/files/". + + 2) If the array computation can be handled easily by numpy, and you + do not need the binaries for another project. + +""" + +import sys +import os +import shutil +import tempfile +from pathlib import Path +from subprocess import STDOUT, CalledProcessError, check_output +from string import Template +from warnings import warn + +from sympy.core.cache import cacheit +from sympy.core.function import Lambda +from sympy.core.relational import Eq +from sympy.core.symbol import Dummy, Symbol +from sympy.tensor.indexed import Idx, IndexedBase +from sympy.utilities.codegen import (make_routine, get_code_generator, + OutputArgument, InOutArgument, + InputArgument, CodeGenArgumentListError, + Result, ResultBase, C99CodeGen) +from sympy.utilities.iterables import iterable +from sympy.utilities.lambdify import implemented_function +from sympy.utilities.decorator import doctest_depends_on + +_doctest_depends_on = {'exe': ('f2py', 'gfortran', 'gcc'), + 'modules': ('numpy',)} + + +class CodeWrapError(Exception): + pass + + +class CodeWrapper: + """Base Class for code wrappers""" + _filename = "wrapped_code" + _module_basename = "wrapper_module" + _module_counter = 0 + + @property + def filename(self): + return "%s_%s" % (self._filename, CodeWrapper._module_counter) + + @property + def module_name(self): + return "%s_%s" % (self._module_basename, CodeWrapper._module_counter) + + def __init__(self, generator, filepath=None, flags=[], verbose=False): + """ + generator -- the code generator to use + """ + self.generator = generator + self.filepath = filepath + self.flags = flags + self.quiet = not verbose + + @property + def include_header(self): + return bool(self.filepath) + + @property + def include_empty(self): + return bool(self.filepath) + + def _generate_code(self, main_routine, routines): + routines.append(main_routine) + self.generator.write( + routines, self.filename, True, self.include_header, + self.include_empty) + + def wrap_code(self, routine, helpers=None): + helpers = helpers or [] + if self.filepath: + workdir = os.path.abspath(self.filepath) + else: + workdir = tempfile.mkdtemp("_sympy_compile") + if not os.access(workdir, os.F_OK): + os.mkdir(workdir) + oldwork = os.getcwd() + os.chdir(workdir) + try: + sys.path.append(workdir) + self._generate_code(routine, helpers) + self._prepare_files(routine) + self._process_files(routine) + mod = __import__(self.module_name) + finally: + sys.path.remove(workdir) + CodeWrapper._module_counter += 1 + os.chdir(oldwork) + if not self.filepath: + try: + shutil.rmtree(workdir) + except OSError: + # Could be some issues on Windows + pass + + return self._get_wrapped_function(mod, routine.name) + + def _process_files(self, routine): + command = self.command + command.extend(self.flags) + try: + retoutput = check_output(command, stderr=STDOUT) + except CalledProcessError as e: + raise CodeWrapError( + "Error while executing command: %s. Command output is:\n%s" % ( + " ".join(command), e.output.decode('utf-8'))) + if not self.quiet: + print(retoutput) + + +class DummyWrapper(CodeWrapper): + """Class used for testing independent of backends """ + + template = """# dummy module for testing of SymPy +def %(name)s(): + return "%(expr)s" +%(name)s.args = "%(args)s" +%(name)s.returns = "%(retvals)s" +""" + + def _prepare_files(self, routine): + return + + def _generate_code(self, routine, helpers): + with open('%s.py' % self.module_name, 'w') as f: + printed = ", ".join( + [str(res.expr) for res in routine.result_variables]) + # convert OutputArguments to return value like f2py + args = filter(lambda x: not isinstance( + x, OutputArgument), routine.arguments) + retvals = [] + for val in routine.result_variables: + if isinstance(val, Result): + retvals.append('nameless') + else: + retvals.append(val.result_var) + + print(DummyWrapper.template % { + 'name': routine.name, + 'expr': printed, + 'args': ", ".join([str(a.name) for a in args]), + 'retvals': ", ".join([str(val) for val in retvals]) + }, end="", file=f) + + def _process_files(self, routine): + return + + @classmethod + def _get_wrapped_function(cls, mod, name): + return getattr(mod, name) + + +class CythonCodeWrapper(CodeWrapper): + """Wrapper that uses Cython""" + + setup_template = """\ +from setuptools import setup +from setuptools import Extension +from Cython.Build import cythonize +cy_opts = {cythonize_options} +{np_import} +ext_mods = [Extension( + {ext_args}, + include_dirs={include_dirs}, + library_dirs={library_dirs}, + libraries={libraries}, + extra_compile_args={extra_compile_args}, + extra_link_args={extra_link_args} +)] +setup(ext_modules=cythonize(ext_mods, **cy_opts)) +""" + + _cythonize_options = {'compiler_directives':{'language_level' : "3"}} + + pyx_imports = ( + "import numpy as np\n" + "cimport numpy as np\n\n") + + pyx_header = ( + "cdef extern from '{header_file}.h':\n" + " {prototype}\n\n") + + pyx_func = ( + "def {name}_c({arg_string}):\n" + "\n" + "{declarations}" + "{body}") + + std_compile_flag = '-std=c99' + + def __init__(self, *args, **kwargs): + """Instantiates a Cython code wrapper. + + The following optional parameters get passed to ``setuptools.Extension`` + for building the Python extension module. Read its documentation to + learn more. + + Parameters + ========== + include_dirs : [list of strings] + A list of directories to search for C/C++ header files (in Unix + form for portability). + library_dirs : [list of strings] + A list of directories to search for C/C++ libraries at link time. + libraries : [list of strings] + A list of library names (not filenames or paths) to link against. + extra_compile_args : [list of strings] + Any extra platform- and compiler-specific information to use when + compiling the source files in 'sources'. For platforms and + compilers where "command line" makes sense, this is typically a + list of command-line arguments, but for other platforms it could be + anything. Note that the attribute ``std_compile_flag`` will be + appended to this list. + extra_link_args : [list of strings] + Any extra platform- and compiler-specific information to use when + linking object files together to create the extension (or to create + a new static Python interpreter). Similar interpretation as for + 'extra_compile_args'. + cythonize_options : [dictionary] + Keyword arguments passed on to cythonize. + + """ + + self._include_dirs = kwargs.pop('include_dirs', []) + self._library_dirs = kwargs.pop('library_dirs', []) + self._libraries = kwargs.pop('libraries', []) + self._extra_compile_args = kwargs.pop('extra_compile_args', []) + self._extra_compile_args.append(self.std_compile_flag) + self._extra_link_args = kwargs.pop('extra_link_args', []) + self._cythonize_options = kwargs.pop('cythonize_options', self._cythonize_options) + + self._need_numpy = False + + super().__init__(*args, **kwargs) + + @property + def command(self): + command = [sys.executable, "setup.py", "build_ext", "--inplace"] + return command + + def _prepare_files(self, routine, build_dir=os.curdir): + # NOTE : build_dir is used for testing purposes. + pyxfilename = self.module_name + '.pyx' + codefilename = "%s.%s" % (self.filename, self.generator.code_extension) + + # pyx + with open(os.path.join(build_dir, pyxfilename), 'w') as f: + self.dump_pyx([routine], f, self.filename) + + # setup.py + ext_args = [repr(self.module_name), repr([pyxfilename, codefilename])] + if self._need_numpy: + np_import = 'import numpy as np\n' + self._include_dirs.append('np.get_include()') + else: + np_import = '' + + includes = str(self._include_dirs).replace("'np.get_include()'", + 'np.get_include()') + code = self.setup_template.format( + ext_args=", ".join(ext_args), + np_import=np_import, + include_dirs=includes, + library_dirs=self._library_dirs, + libraries=self._libraries, + extra_compile_args=self._extra_compile_args, + extra_link_args=self._extra_link_args, + cythonize_options=self._cythonize_options) + Path(os.path.join(build_dir, 'setup.py')).write_text(code) + + @classmethod + def _get_wrapped_function(cls, mod, name): + return getattr(mod, name + '_c') + + def dump_pyx(self, routines, f, prefix): + """Write a Cython file with Python wrappers + + This file contains all the definitions of the routines in c code and + refers to the header file. + + Arguments + --------- + routines + List of Routine instances + f + File-like object to write the file to + prefix + The filename prefix, used to refer to the proper header file. + Only the basename of the prefix is used. + """ + headers = [] + functions = [] + for routine in routines: + prototype = self.generator.get_prototype(routine) + + # C Function Header Import + headers.append(self.pyx_header.format(header_file=prefix, + prototype=prototype)) + + # Partition the C function arguments into categories + py_rets, py_args, py_loc, py_inf = self._partition_args(routine.arguments) + + # Function prototype + name = routine.name + arg_string = ", ".join(self._prototype_arg(arg) for arg in py_args) + + # Local Declarations + local_decs = [] + for arg, val in py_inf.items(): + proto = self._prototype_arg(arg) + mat, ind = [self._string_var(v) for v in val] + local_decs.append(" cdef {} = {}.shape[{}]".format(proto, mat, ind)) + local_decs.extend([" cdef {}".format(self._declare_arg(a)) for a in py_loc]) + declarations = "\n".join(local_decs) + if declarations: + declarations = declarations + "\n" + + # Function Body + args_c = ", ".join([self._call_arg(a) for a in routine.arguments]) + rets = ", ".join([self._string_var(r.name) for r in py_rets]) + if routine.results: + body = ' return %s(%s)' % (routine.name, args_c) + if rets: + body = body + ', ' + rets + else: + body = ' %s(%s)\n' % (routine.name, args_c) + body = body + ' return ' + rets + + functions.append(self.pyx_func.format(name=name, arg_string=arg_string, + declarations=declarations, body=body)) + + # Write text to file + if self._need_numpy: + # Only import numpy if required + f.write(self.pyx_imports) + f.write('\n'.join(headers)) + f.write('\n'.join(functions)) + + def _partition_args(self, args): + """Group function arguments into categories.""" + py_args = [] + py_returns = [] + py_locals = [] + py_inferred = {} + for arg in args: + if isinstance(arg, OutputArgument): + py_returns.append(arg) + py_locals.append(arg) + elif isinstance(arg, InOutArgument): + py_returns.append(arg) + py_args.append(arg) + else: + py_args.append(arg) + # Find arguments that are array dimensions. These can be inferred + # locally in the Cython code. + if isinstance(arg, (InputArgument, InOutArgument)) and arg.dimensions: + dims = [d[1] + 1 for d in arg.dimensions] + sym_dims = [(i, d) for (i, d) in enumerate(dims) if + isinstance(d, Symbol)] + for (i, d) in sym_dims: + py_inferred[d] = (arg.name, i) + for arg in args: + if arg.name in py_inferred: + py_inferred[arg] = py_inferred.pop(arg.name) + # Filter inferred arguments from py_args + py_args = [a for a in py_args if a not in py_inferred] + return py_returns, py_args, py_locals, py_inferred + + def _prototype_arg(self, arg): + mat_dec = "np.ndarray[{mtype}, ndim={ndim}] {name}" + np_types = {'double': 'np.double_t', + 'int': 'np.int_t'} + t = arg.get_datatype('c') + if arg.dimensions: + self._need_numpy = True + ndim = len(arg.dimensions) + mtype = np_types[t] + return mat_dec.format(mtype=mtype, ndim=ndim, name=self._string_var(arg.name)) + else: + return "%s %s" % (t, self._string_var(arg.name)) + + def _declare_arg(self, arg): + proto = self._prototype_arg(arg) + if arg.dimensions: + shape = '(' + ','.join(self._string_var(i[1] + 1) for i in arg.dimensions) + ')' + return proto + " = np.empty({shape})".format(shape=shape) + else: + return proto + " = 0" + + def _call_arg(self, arg): + if arg.dimensions: + t = arg.get_datatype('c') + return "<{}*> {}.data".format(t, self._string_var(arg.name)) + elif isinstance(arg, ResultBase): + return "&{}".format(self._string_var(arg.name)) + else: + return self._string_var(arg.name) + + def _string_var(self, var): + printer = self.generator.printer.doprint + return printer(var) + + +class F2PyCodeWrapper(CodeWrapper): + """Wrapper that uses f2py""" + + def __init__(self, *args, **kwargs): + + ext_keys = ['include_dirs', 'library_dirs', 'libraries', + 'extra_compile_args', 'extra_link_args'] + msg = ('The compilation option kwarg {} is not supported with the f2py ' + 'backend.') + + for k in ext_keys: + if k in kwargs.keys(): + warn(msg.format(k)) + kwargs.pop(k, None) + + super().__init__(*args, **kwargs) + + @property + def command(self): + filename = self.filename + '.' + self.generator.code_extension + args = ['-c', '-m', self.module_name, filename] + command = [sys.executable, "-c", "import numpy.f2py as f2py2e;f2py2e.main()"]+args + return command + + def _prepare_files(self, routine): + pass + + @classmethod + def _get_wrapped_function(cls, mod, name): + return getattr(mod, name) + + +# Here we define a lookup of backends -> tuples of languages. For now, each +# tuple is of length 1, but if a backend supports more than one language, +# the most preferable language is listed first. +_lang_lookup = {'CYTHON': ('C99', 'C89', 'C'), + 'F2PY': ('F95',), + 'NUMPY': ('C99', 'C89', 'C'), + 'DUMMY': ('F95',)} # Dummy here just for testing + + +def _infer_language(backend): + """For a given backend, return the top choice of language""" + langs = _lang_lookup.get(backend.upper(), False) + if not langs: + raise ValueError("Unrecognized backend: " + backend) + return langs[0] + + +def _validate_backend_language(backend, language): + """Throws error if backend and language are incompatible""" + langs = _lang_lookup.get(backend.upper(), False) + if not langs: + raise ValueError("Unrecognized backend: " + backend) + if language.upper() not in langs: + raise ValueError(("Backend {} and language {} are " + "incompatible").format(backend, language)) + + +@cacheit +@doctest_depends_on(exe=('f2py', 'gfortran'), modules=('numpy',)) +def autowrap(expr, language=None, backend='f2py', tempdir=None, args=None, + flags=None, verbose=False, helpers=None, code_gen=None, **kwargs): + """Generates Python callable binaries based on the math expression. + + Parameters + ========== + + expr + The SymPy expression that should be wrapped as a binary routine. + language : string, optional + If supplied, (options: 'C' or 'F95'), specifies the language of the + generated code. If ``None`` [default], the language is inferred based + upon the specified backend. + backend : string, optional + Backend used to wrap the generated code. Either 'f2py' [default], + or 'cython'. + tempdir : string, optional + Path to directory for temporary files. If this argument is supplied, + the generated code and the wrapper input files are left intact in the + specified path. + args : iterable, optional + An ordered iterable of symbols. Specifies the argument sequence for the + function. + flags : iterable, optional + Additional option flags that will be passed to the backend. + verbose : bool, optional + If True, autowrap will not mute the command line backends. This can be + helpful for debugging. + helpers : 3-tuple or iterable of 3-tuples, optional + Used to define auxiliary functions needed for the main expression. + Each tuple should be of the form (name, expr, args) where: + + - name : str, the function name + - expr : sympy expression, the function + - args : iterable, the function arguments (can be any iterable of symbols) + + code_gen : CodeGen instance + An instance of a CodeGen subclass. Overrides ``language``. + include_dirs : [string] + A list of directories to search for C/C++ header files (in Unix form + for portability). + library_dirs : [string] + A list of directories to search for C/C++ libraries at link time. + libraries : [string] + A list of library names (not filenames or paths) to link against. + extra_compile_args : [string] + Any extra platform- and compiler-specific information to use when + compiling the source files in 'sources'. For platforms and compilers + where "command line" makes sense, this is typically a list of + command-line arguments, but for other platforms it could be anything. + extra_link_args : [string] + Any extra platform- and compiler-specific information to use when + linking object files together to create the extension (or to create a + new static Python interpreter). Similar interpretation as for + 'extra_compile_args'. + + Examples + ======== + + Basic usage: + + >>> from sympy.abc import x, y, z + >>> from sympy.utilities.autowrap import autowrap + >>> expr = ((x - y + z)**(13)).expand() + >>> binary_func = autowrap(expr) + >>> binary_func(1, 4, 2) + -1.0 + + Using helper functions: + + >>> from sympy.abc import x, t + >>> from sympy import Function + >>> helper_func = Function('helper_func') # Define symbolic function + >>> expr = 3*x + helper_func(t) # Main expression using helper function + >>> # Define helper_func(x) = 4*x using f2py backend + >>> binary_func = autowrap(expr, args=[x, t], + ... helpers=('helper_func', 4*x, [x])) + >>> binary_func(2, 5) # 3*2 + helper_func(5) = 6 + 20 + 26.0 + >>> # Same example using cython backend + >>> binary_func = autowrap(expr, args=[x, t], backend='cython', + ... helpers=[('helper_func', 4*x, [x])]) + >>> binary_func(2, 5) # 3*2 + helper_func(5) = 6 + 20 + 26.0 + + Type handling example: + + >>> import numpy as np + >>> expr = x + y + >>> f_cython = autowrap(expr, backend='cython') + >>> f_cython(1, 2) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + TypeError: Argument '_x' has incorrect type (expected numpy.ndarray, got int) + >>> f_cython(np.array([1.0]), np.array([2.0])) + array([ 3.]) + + """ + if language: + if not isinstance(language, type): + _validate_backend_language(backend, language) + else: + language = _infer_language(backend) + + # two cases 1) helpers is an iterable of 3-tuples and 2) helpers is a + # 3-tuple + if iterable(helpers) and len(helpers) != 0 and iterable(helpers[0]): + helpers = helpers if helpers else () + else: + helpers = [helpers] if helpers else () + args = list(args) if iterable(args, exclude=set) else args + + if code_gen is None: + code_gen = get_code_generator(language, "autowrap") + + CodeWrapperClass = { + 'F2PY': F2PyCodeWrapper, + 'CYTHON': CythonCodeWrapper, + 'DUMMY': DummyWrapper + }[backend.upper()] + code_wrapper = CodeWrapperClass(code_gen, tempdir, flags if flags else (), + verbose, **kwargs) + + helps = [] + for name_h, expr_h, args_h in helpers: + helps.append(code_gen.routine(name_h, expr_h, args_h)) + + for name_h, expr_h, args_h in helpers: + if expr.has(expr_h): + name_h = binary_function(name_h, expr_h, backend='dummy') + expr = expr.subs(expr_h, name_h(*args_h)) + try: + routine = code_gen.routine('autofunc', expr, args) + except CodeGenArgumentListError as e: + # if all missing arguments are for pure output, we simply attach them + # at the end and try again, because the wrappers will silently convert + # them to return values anyway. + new_args = [] + for missing in e.missing_args: + if not isinstance(missing, OutputArgument): + raise + new_args.append(missing.name) + routine = code_gen.routine('autofunc', expr, args + new_args) + + return code_wrapper.wrap_code(routine, helpers=helps) + + +@doctest_depends_on(exe=('f2py', 'gfortran'), modules=('numpy',)) +def binary_function(symfunc, expr, **kwargs): + """Returns a SymPy function with expr as binary implementation + + This is a convenience function that automates the steps needed to + autowrap the SymPy expression and attaching it to a Function object + with implemented_function(). + + Parameters + ========== + + symfunc : SymPy Function + The function to bind the callable to. + expr : SymPy Expression + The expression used to generate the function. + kwargs : dict + Any kwargs accepted by autowrap. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy.utilities.autowrap import binary_function + >>> expr = ((x - y)**(25)).expand() + >>> f = binary_function('f', expr) + >>> type(f) + + >>> 2*f(x, y) + 2*f(x, y) + >>> f(x, y).evalf(2, subs={x: 1, y: 2}) + -1.0 + + """ + binary = autowrap(expr, **kwargs) + return implemented_function(symfunc, binary) + +################################################################# +# UFUNCIFY # +################################################################# + +_ufunc_top = Template("""\ +#include "Python.h" +#include "math.h" +#include "numpy/ndarraytypes.h" +#include "numpy/ufuncobject.h" +#include "numpy/halffloat.h" +#include ${include_file} + +static PyMethodDef ${module}Methods[] = { + {NULL, NULL, 0, NULL} +};""") + +_ufunc_outcalls = Template("*((double *)out${outnum}) = ${funcname}(${call_args});") + +_ufunc_body = Template("""\ +#ifdef NPY_1_19_API_VERSION +static void ${funcname}_ufunc(char **args, const npy_intp *dimensions, const npy_intp* steps, void* data) +#else +static void ${funcname}_ufunc(char **args, npy_intp *dimensions, npy_intp* steps, void* data) +#endif +{ + npy_intp i; + npy_intp n = dimensions[0]; + ${declare_args} + ${declare_steps} + for (i = 0; i < n; i++) { + ${outcalls} + ${step_increments} + } +} +PyUFuncGenericFunction ${funcname}_funcs[1] = {&${funcname}_ufunc}; +static char ${funcname}_types[${n_types}] = ${types} +static void *${funcname}_data[1] = {NULL};""") + +_ufunc_bottom = Template("""\ +#if PY_VERSION_HEX >= 0x03000000 +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "${module}", + NULL, + -1, + ${module}Methods, + NULL, + NULL, + NULL, + NULL +}; + +PyMODINIT_FUNC PyInit_${module}(void) +{ + PyObject *m, *d; + ${function_creation} + m = PyModule_Create(&moduledef); + if (!m) { + return NULL; + } + import_array(); + import_umath(); + d = PyModule_GetDict(m); + ${ufunc_init} + return m; +} +#else +PyMODINIT_FUNC init${module}(void) +{ + PyObject *m, *d; + ${function_creation} + m = Py_InitModule("${module}", ${module}Methods); + if (m == NULL) { + return; + } + import_array(); + import_umath(); + d = PyModule_GetDict(m); + ${ufunc_init} +} +#endif\ +""") + +_ufunc_init_form = Template("""\ +ufunc${ind} = PyUFunc_FromFuncAndData(${funcname}_funcs, ${funcname}_data, ${funcname}_types, 1, ${n_in}, ${n_out}, + PyUFunc_None, "${module}", ${docstring}, 0); + PyDict_SetItemString(d, "${funcname}", ufunc${ind}); + Py_DECREF(ufunc${ind});""") + +_ufunc_setup = Template("""\ +from setuptools.extension import Extension +from setuptools import setup + +from numpy import get_include + +if __name__ == "__main__": + setup(ext_modules=[ + Extension('${module}', + sources=['${module}.c', '${filename}.c'], + include_dirs=[get_include()])]) +""") + + +class UfuncifyCodeWrapper(CodeWrapper): + """Wrapper for Ufuncify""" + + def __init__(self, *args, **kwargs): + + ext_keys = ['include_dirs', 'library_dirs', 'libraries', + 'extra_compile_args', 'extra_link_args'] + msg = ('The compilation option kwarg {} is not supported with the numpy' + ' backend.') + + for k in ext_keys: + if k in kwargs.keys(): + warn(msg.format(k)) + kwargs.pop(k, None) + + super().__init__(*args, **kwargs) + + @property + def command(self): + command = [sys.executable, "setup.py", "build_ext", "--inplace"] + return command + + def wrap_code(self, routines, helpers=None): + # This routine overrides CodeWrapper because we can't assume funcname == routines[0].name + # Therefore we have to break the CodeWrapper private API. + # There isn't an obvious way to extend multi-expr support to + # the other autowrap backends, so we limit this change to ufuncify. + helpers = helpers if helpers is not None else [] + # We just need a consistent name + funcname = 'wrapped_' + str(id(routines) + id(helpers)) + + workdir = self.filepath or tempfile.mkdtemp("_sympy_compile") + if not os.access(workdir, os.F_OK): + os.mkdir(workdir) + oldwork = os.getcwd() + os.chdir(workdir) + try: + sys.path.append(workdir) + self._generate_code(routines, helpers) + self._prepare_files(routines, funcname) + self._process_files(routines) + mod = __import__(self.module_name) + finally: + sys.path.remove(workdir) + CodeWrapper._module_counter += 1 + os.chdir(oldwork) + if not self.filepath: + try: + shutil.rmtree(workdir) + except OSError: + # Could be some issues on Windows + pass + + return self._get_wrapped_function(mod, funcname) + + def _generate_code(self, main_routines, helper_routines): + all_routines = main_routines + helper_routines + self.generator.write( + all_routines, self.filename, True, self.include_header, + self.include_empty) + + def _prepare_files(self, routines, funcname): + + # C + codefilename = self.module_name + '.c' + with open(codefilename, 'w') as f: + self.dump_c(routines, f, self.filename, funcname=funcname) + + # setup.py + with open('setup.py', 'w') as f: + self.dump_setup(f) + + @classmethod + def _get_wrapped_function(cls, mod, name): + return getattr(mod, name) + + def dump_setup(self, f): + setup = _ufunc_setup.substitute(module=self.module_name, + filename=self.filename) + f.write(setup) + + def dump_c(self, routines, f, prefix, funcname=None): + """Write a C file with Python wrappers + + This file contains all the definitions of the routines in c code. + + Arguments + --------- + routines + List of Routine instances + f + File-like object to write the file to + prefix + The filename prefix, used to name the imported module. + funcname + Name of the main function to be returned. + """ + if funcname is None: + if len(routines) == 1: + funcname = routines[0].name + else: + msg = 'funcname must be specified for multiple output routines' + raise ValueError(msg) + functions = [] + function_creation = [] + ufunc_init = [] + module = self.module_name + include_file = "\"{}.h\"".format(prefix) + top = _ufunc_top.substitute(include_file=include_file, module=module) + + name = funcname + + # Partition the C function arguments into categories + # Here we assume all routines accept the same arguments + r_index = 0 + py_in, _ = self._partition_args(routines[0].arguments) + n_in = len(py_in) + n_out = len(routines) + + # Declare Args + form = "char *{0}{1} = args[{2}];" + arg_decs = [form.format('in', i, i) for i in range(n_in)] + arg_decs.extend([form.format('out', i, i+n_in) for i in range(n_out)]) + declare_args = '\n '.join(arg_decs) + + # Declare Steps + form = "npy_intp {0}{1}_step = steps[{2}];" + step_decs = [form.format('in', i, i) for i in range(n_in)] + step_decs.extend([form.format('out', i, i+n_in) for i in range(n_out)]) + declare_steps = '\n '.join(step_decs) + + # Call Args + form = "*(double *)in{0}" + call_args = ', '.join([form.format(a) for a in range(n_in)]) + + # Step Increments + form = "{0}{1} += {0}{1}_step;" + step_incs = [form.format('in', i) for i in range(n_in)] + step_incs.extend([form.format('out', i, i) for i in range(n_out)]) + step_increments = '\n '.join(step_incs) + + # Types + n_types = n_in + n_out + types = "{" + ', '.join(["NPY_DOUBLE"]*n_types) + "};" + + # Docstring + docstring = '"Created in SymPy with Ufuncify"' + + # Function Creation + function_creation.append("PyObject *ufunc{};".format(r_index)) + + # Ufunc initialization + init_form = _ufunc_init_form.substitute(module=module, + funcname=name, + docstring=docstring, + n_in=n_in, n_out=n_out, + ind=r_index) + ufunc_init.append(init_form) + + outcalls = [_ufunc_outcalls.substitute( + outnum=i, call_args=call_args, funcname=routines[i].name) for i in + range(n_out)] + + body = _ufunc_body.substitute(module=module, funcname=name, + declare_args=declare_args, + declare_steps=declare_steps, + call_args=call_args, + step_increments=step_increments, + n_types=n_types, types=types, + outcalls='\n '.join(outcalls)) + functions.append(body) + + body = '\n\n'.join(functions) + ufunc_init = '\n '.join(ufunc_init) + function_creation = '\n '.join(function_creation) + bottom = _ufunc_bottom.substitute(module=module, + ufunc_init=ufunc_init, + function_creation=function_creation) + text = [top, body, bottom] + f.write('\n\n'.join(text)) + + def _partition_args(self, args): + """Group function arguments into categories.""" + py_in = [] + py_out = [] + for arg in args: + if isinstance(arg, OutputArgument): + py_out.append(arg) + elif isinstance(arg, InOutArgument): + raise ValueError("Ufuncify doesn't support InOutArguments") + else: + py_in.append(arg) + return py_in, py_out + + +@cacheit +@doctest_depends_on(exe=('f2py', 'gfortran', 'gcc'), modules=('numpy',)) +def ufuncify(args, expr, language=None, backend='numpy', tempdir=None, + flags=None, verbose=False, helpers=None, **kwargs): + """Generates a binary function that supports broadcasting on numpy arrays. + + Parameters + ========== + + args : iterable + Either a Symbol or an iterable of symbols. Specifies the argument + sequence for the function. + expr + A SymPy expression that defines the element wise operation. + language : string, optional + If supplied, (options: 'C' or 'F95'), specifies the language of the + generated code. If ``None`` [default], the language is inferred based + upon the specified backend. + backend : string, optional + Backend used to wrap the generated code. Either 'numpy' [default], + 'cython', or 'f2py'. + tempdir : string, optional + Path to directory for temporary files. If this argument is supplied, + the generated code and the wrapper input files are left intact in + the specified path. + flags : iterable, optional + Additional option flags that will be passed to the backend. + verbose : bool, optional + If True, autowrap will not mute the command line backends. This can + be helpful for debugging. + helpers : 3-tuple or iterable of 3-tuples, optional + Used to define auxiliary functions needed for the main expression. + Each tuple should be of the form (name, expr, args) where: + + - name : str, the function name + - expr : sympy expression, the function + - args : iterable, the function arguments (can be any iterable of symbols) + + kwargs : dict + These kwargs will be passed to autowrap if the `f2py` or `cython` + backend is used and ignored if the `numpy` backend is used. + + Notes + ===== + + The default backend ('numpy') will create actual instances of + ``numpy.ufunc``. These support ndimensional broadcasting, and implicit type + conversion. Use of the other backends will result in a "ufunc-like" + function, which requires equal length 1-dimensional arrays for all + arguments, and will not perform any type conversions. + + References + ========== + + .. [1] https://numpy.org/doc/stable/reference/ufuncs.html + + Examples + ======== + + Basic usage: + + >>> from sympy.utilities.autowrap import ufuncify + >>> from sympy.abc import x, y + >>> import numpy as np + >>> f = ufuncify((x, y), y + x**2) + >>> type(f) + + >>> f([1, 2, 3], 2) + array([ 3., 6., 11.]) + >>> f(np.arange(5), 3) + array([ 3., 4., 7., 12., 19.]) + + Using helper functions: + + >>> from sympy import Function + >>> helper_func = Function('helper_func') # Define symbolic function + >>> expr = x**2 + y*helper_func(x) # Main expression using helper function + >>> # Define helper_func(x) = x**3 + >>> f = ufuncify((x, y), expr, helpers=[('helper_func', x**3, [x])]) + >>> f([1, 2], [3, 4]) + array([ 4., 36.]) + + Type handling with different backends: + + For the 'f2py' and 'cython' backends, inputs are required to be equal length + 1-dimensional arrays. The 'f2py' backend will perform type conversion, but + the Cython backend will error if the inputs are not of the expected type. + + >>> f_fortran = ufuncify((x, y), y + x**2, backend='f2py') + >>> f_fortran(1, 2) + array([ 3.]) + >>> f_fortran(np.array([1, 2, 3]), np.array([1.0, 2.0, 3.0])) + array([ 2., 6., 12.]) + >>> f_cython = ufuncify((x, y), y + x**2, backend='Cython') + >>> f_cython(1, 2) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + TypeError: Argument '_x' has incorrect type (expected numpy.ndarray, got int) + >>> f_cython(np.array([1.0]), np.array([2.0])) + array([ 3.]) + + """ + + if isinstance(args, Symbol): + args = (args,) + else: + args = tuple(args) + + if language: + _validate_backend_language(backend, language) + else: + language = _infer_language(backend) + + helpers = helpers if helpers else () + flags = flags if flags else () + + if backend.upper() == 'NUMPY': + # maxargs is set by numpy compile-time constant NPY_MAXARGS + # If a future version of numpy modifies or removes this restriction + # this variable should be changed or removed + maxargs = 32 + helps = [] + for name, expr, args in helpers: + helps.append(make_routine(name, expr, args)) + code_wrapper = UfuncifyCodeWrapper(C99CodeGen("ufuncify"), tempdir, + flags, verbose) + if not isinstance(expr, (list, tuple)): + expr = [expr] + if len(expr) == 0: + raise ValueError('Expression iterable has zero length') + if len(expr) + len(args) > maxargs: + msg = ('Cannot create ufunc with more than {0} total arguments: ' + 'got {1} in, {2} out') + raise ValueError(msg.format(maxargs, len(args), len(expr))) + routines = [make_routine('autofunc{}'.format(idx), exprx, args) for + idx, exprx in enumerate(expr)] + return code_wrapper.wrap_code(routines, helpers=helps) + else: + # Dummies are used for all added expressions to prevent name clashes + # within the original expression. + y = IndexedBase(Dummy('y')) + m = Dummy('m', integer=True) + i = Idx(Dummy('i', integer=True), m) + f_dummy = Dummy('f') + f = implemented_function('%s_%d' % (f_dummy.name, f_dummy.dummy_index), Lambda(args, expr)) + # For each of the args create an indexed version. + indexed_args = [IndexedBase(Dummy(str(a))) for a in args] + # Order the arguments (out, args, dim) + args = [y] + indexed_args + [m] + args_with_indices = [a[i] for a in indexed_args] + return autowrap(Eq(y[i], f(*args_with_indices)), language, backend, + tempdir, args, flags, verbose, helpers, **kwargs) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/codegen.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/codegen.py new file mode 100644 index 0000000000000000000000000000000000000000..9ac8772fc000d707ae33c67eaa44b4c281157ab0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/codegen.py @@ -0,0 +1,2237 @@ +""" +module for generating C, C++, Fortran77, Fortran90, Julia, Rust +and Octave/Matlab routines that evaluate SymPy expressions. +This module is work in progress. +Only the milestones with a '+' character in the list below have been completed. + +--- How is sympy.utilities.codegen different from sympy.printing.ccode? --- + +We considered the idea to extend the printing routines for SymPy functions in +such a way that it prints complete compilable code, but this leads to a few +unsurmountable issues that can only be tackled with dedicated code generator: + +- For C, one needs both a code and a header file, while the printing routines + generate just one string. This code generator can be extended to support + .pyf files for f2py. + +- SymPy functions are not concerned with programming-technical issues, such + as input, output and input-output arguments. Other examples are contiguous + or non-contiguous arrays, including headers of other libraries such as gsl + or others. + +- It is highly interesting to evaluate several SymPy functions in one C + routine, eventually sharing common intermediate results with the help + of the cse routine. This is more than just printing. + +- From the programming perspective, expressions with constants should be + evaluated in the code generator as much as possible. This is different + for printing. + +--- Basic assumptions --- + +* A generic Routine data structure describes the routine that must be + translated into C/Fortran/... code. This data structure covers all + features present in one or more of the supported languages. + +* Descendants from the CodeGen class transform multiple Routine instances + into compilable code. Each derived class translates into a specific + language. + +* In many cases, one wants a simple workflow. The friendly functions in the + last part are a simple api on top of the Routine/CodeGen stuff. They are + easier to use, but are less powerful. + +--- Milestones --- + ++ First working version with scalar input arguments, generating C code, + tests ++ Friendly functions that are easier to use than the rigorous + Routine/CodeGen workflow. ++ Integer and Real numbers as input and output ++ Output arguments ++ InputOutput arguments ++ Sort input/output arguments properly ++ Contiguous array arguments (numpy matrices) ++ Also generate .pyf code for f2py (in autowrap module) ++ Isolate constants and evaluate them beforehand in double precision ++ Fortran 90 ++ Octave/Matlab + +- Common Subexpression Elimination +- User defined comments in the generated code +- Optional extra include lines for libraries/objects that can eval special + functions +- Test other C compilers and libraries: gcc, tcc, libtcc, gcc+gsl, ... +- Contiguous array arguments (SymPy matrices) +- Non-contiguous array arguments (SymPy matrices) +- ccode must raise an error when it encounters something that cannot be + translated into c. ccode(integrate(sin(x)/x, x)) does not make sense. +- Complex numbers as input and output +- A default complex datatype +- Include extra information in the header: date, user, hostname, sha1 + hash, ... +- Fortran 77 +- C++ +- Python +- Julia +- Rust +- ... + +""" + +import os +import textwrap +from io import StringIO + +from sympy import __version__ as sympy_version +from sympy.core import Symbol, S, Tuple, Equality, Function, Basic +from sympy.printing.c import c_code_printers +from sympy.printing.codeprinter import AssignmentError +from sympy.printing.fortran import FCodePrinter +from sympy.printing.julia import JuliaCodePrinter +from sympy.printing.octave import OctaveCodePrinter +from sympy.printing.rust import RustCodePrinter +from sympy.tensor import Idx, Indexed, IndexedBase +from sympy.matrices import (MatrixSymbol, ImmutableMatrix, MatrixBase, + MatrixExpr, MatrixSlice) +from sympy.utilities.iterables import is_sequence + + +__all__ = [ + # description of routines + "Routine", "DataType", "default_datatypes", "get_default_datatype", + "Argument", "InputArgument", "OutputArgument", "Result", + # routines -> code + "CodeGen", "CCodeGen", "FCodeGen", "JuliaCodeGen", "OctaveCodeGen", + "RustCodeGen", + # friendly functions + "codegen", "make_routine", +] + + +# +# Description of routines +# + + +class Routine: + """Generic description of evaluation routine for set of expressions. + + A CodeGen class can translate instances of this class into code in a + particular language. The routine specification covers all the features + present in these languages. The CodeGen part must raise an exception + when certain features are not present in the target language. For + example, multiple return values are possible in Python, but not in C or + Fortran. Another example: Fortran and Python support complex numbers, + while C does not. + + """ + + def __init__(self, name, arguments, results, local_vars, global_vars): + """Initialize a Routine instance. + + Parameters + ========== + + name : string + Name of the routine. + + arguments : list of Arguments + These are things that appear in arguments of a routine, often + appearing on the right-hand side of a function call. These are + commonly InputArguments but in some languages, they can also be + OutputArguments or InOutArguments (e.g., pass-by-reference in C + code). + + results : list of Results + These are the return values of the routine, often appearing on + the left-hand side of a function call. The difference between + Results and OutputArguments and when you should use each is + language-specific. + + local_vars : list of Results + These are variables that will be defined at the beginning of the + function. + + global_vars : list of Symbols + Variables which will not be passed into the function. + + """ + + # extract all input symbols and all symbols appearing in an expression + input_symbols = set() + symbols = set() + for arg in arguments: + if isinstance(arg, OutputArgument): + symbols.update(arg.expr.free_symbols - arg.expr.atoms(Indexed)) + elif isinstance(arg, InputArgument): + input_symbols.add(arg.name) + elif isinstance(arg, InOutArgument): + input_symbols.add(arg.name) + symbols.update(arg.expr.free_symbols - arg.expr.atoms(Indexed)) + else: + raise ValueError("Unknown Routine argument: %s" % arg) + + for r in results: + if not isinstance(r, Result): + raise ValueError("Unknown Routine result: %s" % r) + symbols.update(r.expr.free_symbols - r.expr.atoms(Indexed)) + + local_symbols = set() + for r in local_vars: + if isinstance(r, Result): + symbols.update(r.expr.free_symbols - r.expr.atoms(Indexed)) + local_symbols.add(r.name) + else: + local_symbols.add(r) + + symbols = {s.label if isinstance(s, Idx) else s for s in symbols} + + # Check that all symbols in the expressions are covered by + # InputArguments/InOutArguments---subset because user could + # specify additional (unused) InputArguments or local_vars. + notcovered = symbols.difference( + input_symbols.union(local_symbols).union(global_vars)) + if notcovered != set(): + raise ValueError("Symbols needed for output are not in input " + + ", ".join([str(x) for x in notcovered])) + + self.name = name + self.arguments = arguments + self.results = results + self.local_vars = local_vars + self.global_vars = global_vars + + def __str__(self): + return self.__class__.__name__ + "({name!r}, {arguments}, {results}, {local_vars}, {global_vars})".format(**self.__dict__) + + __repr__ = __str__ + + @property + def variables(self): + """Returns a set of all variables possibly used in the routine. + + For routines with unnamed return values, the dummies that may or + may not be used will be included in the set. + + """ + v = set(self.local_vars) + v.update(arg.name for arg in self.arguments) + v.update(res.result_var for res in self.results) + return v + + @property + def result_variables(self): + """Returns a list of OutputArgument, InOutArgument and Result. + + If return values are present, they are at the end of the list. + """ + args = [arg for arg in self.arguments if isinstance( + arg, (OutputArgument, InOutArgument))] + args.extend(self.results) + return args + + +class DataType: + """Holds strings for a certain datatype in different languages.""" + def __init__(self, cname, fname, pyname, jlname, octname, rsname): + self.cname = cname + self.fname = fname + self.pyname = pyname + self.jlname = jlname + self.octname = octname + self.rsname = rsname + + +default_datatypes = { + "int": DataType("int", "INTEGER*4", "int", "", "", "i32"), + "float": DataType("double", "REAL*8", "float", "", "", "f64"), + "complex": DataType("double", "COMPLEX*16", "complex", "", "", "float") #FIXME: + # complex is only supported in fortran, python, julia, and octave. + # So to not break c or rust code generation, we stick with double or + # float, respectively (but actually should raise an exception for + # explicitly complex variables (x.is_complex==True)) +} + + +COMPLEX_ALLOWED = False +def get_default_datatype(expr, complex_allowed=None): + """Derives an appropriate datatype based on the expression.""" + if complex_allowed is None: + complex_allowed = COMPLEX_ALLOWED + if complex_allowed: + final_dtype = "complex" + else: + final_dtype = "float" + if expr.is_integer: + return default_datatypes["int"] + elif expr.is_real: + return default_datatypes["float"] + elif isinstance(expr, MatrixBase): + #check all entries + dt = "int" + for element in expr: + if dt == "int" and not element.is_integer: + dt = "float" + if dt == "float" and not element.is_real: + return default_datatypes[final_dtype] + return default_datatypes[dt] + else: + return default_datatypes[final_dtype] + + +class Variable: + """Represents a typed variable.""" + + def __init__(self, name, datatype=None, dimensions=None, precision=None): + """Return a new variable. + + Parameters + ========== + + name : Symbol or MatrixSymbol + + datatype : optional + When not given, the data type will be guessed based on the + assumptions on the symbol argument. + + dimensions : sequence containing tuples, optional + If present, the argument is interpreted as an array, where this + sequence of tuples specifies (lower, upper) bounds for each + index of the array. + + precision : int, optional + Controls the precision of floating point constants. + + """ + if not isinstance(name, (Symbol, MatrixSymbol)): + raise TypeError("The first argument must be a SymPy symbol.") + if datatype is None: + datatype = get_default_datatype(name) + elif not isinstance(datatype, DataType): + raise TypeError("The (optional) `datatype' argument must be an " + "instance of the DataType class.") + if dimensions and not isinstance(dimensions, (tuple, list)): + raise TypeError( + "The dimensions argument must be a sequence of tuples") + + self._name = name + self._datatype = { + 'C': datatype.cname, + 'FORTRAN': datatype.fname, + 'JULIA': datatype.jlname, + 'OCTAVE': datatype.octname, + 'PYTHON': datatype.pyname, + 'RUST': datatype.rsname, + } + self.dimensions = dimensions + self.precision = precision + + def __str__(self): + return "%s(%r)" % (self.__class__.__name__, self.name) + + __repr__ = __str__ + + @property + def name(self): + return self._name + + def get_datatype(self, language): + """Returns the datatype string for the requested language. + + Examples + ======== + + >>> from sympy import Symbol + >>> from sympy.utilities.codegen import Variable + >>> x = Variable(Symbol('x')) + >>> x.get_datatype('c') + 'double' + >>> x.get_datatype('fortran') + 'REAL*8' + + """ + try: + return self._datatype[language.upper()] + except KeyError: + raise CodeGenError("Has datatypes for languages: %s" % + ", ".join(self._datatype)) + + +class Argument(Variable): + """An abstract Argument data structure: a name and a data type. + + This structure is refined in the descendants below. + + """ + pass + + +class InputArgument(Argument): + pass + + +class ResultBase: + """Base class for all "outgoing" information from a routine. + + Objects of this class stores a SymPy expression, and a SymPy object + representing a result variable that will be used in the generated code + only if necessary. + + """ + def __init__(self, expr, result_var): + self.expr = expr + self.result_var = result_var + + def __str__(self): + return "%s(%r, %r)" % (self.__class__.__name__, self.expr, + self.result_var) + + __repr__ = __str__ + + +class OutputArgument(Argument, ResultBase): + """OutputArgument are always initialized in the routine.""" + + def __init__(self, name, result_var, expr, datatype=None, dimensions=None, precision=None): + """Return a new variable. + + Parameters + ========== + + name : Symbol, MatrixSymbol + The name of this variable. When used for code generation, this + might appear, for example, in the prototype of function in the + argument list. + + result_var : Symbol, Indexed + Something that can be used to assign a value to this variable. + Typically the same as `name` but for Indexed this should be e.g., + "y[i]" whereas `name` should be the Symbol "y". + + expr : object + The expression that should be output, typically a SymPy + expression. + + datatype : optional + When not given, the data type will be guessed based on the + assumptions on the symbol argument. + + dimensions : sequence containing tuples, optional + If present, the argument is interpreted as an array, where this + sequence of tuples specifies (lower, upper) bounds for each + index of the array. + + precision : int, optional + Controls the precision of floating point constants. + + """ + + Argument.__init__(self, name, datatype, dimensions, precision) + ResultBase.__init__(self, expr, result_var) + + def __str__(self): + return "%s(%r, %r, %r)" % (self.__class__.__name__, self.name, self.result_var, self.expr) + + __repr__ = __str__ + + +class InOutArgument(Argument, ResultBase): + """InOutArgument are never initialized in the routine.""" + + def __init__(self, name, result_var, expr, datatype=None, dimensions=None, precision=None): + if not datatype: + datatype = get_default_datatype(expr) + Argument.__init__(self, name, datatype, dimensions, precision) + ResultBase.__init__(self, expr, result_var) + __init__.__doc__ = OutputArgument.__init__.__doc__ + + + def __str__(self): + return "%s(%r, %r, %r)" % (self.__class__.__name__, self.name, self.expr, + self.result_var) + + __repr__ = __str__ + + +class Result(Variable, ResultBase): + """An expression for a return value. + + The name result is used to avoid conflicts with the reserved word + "return" in the Python language. It is also shorter than ReturnValue. + + These may or may not need a name in the destination (e.g., "return(x*y)" + might return a value without ever naming it). + + """ + + def __init__(self, expr, name=None, result_var=None, datatype=None, + dimensions=None, precision=None): + """Initialize a return value. + + Parameters + ========== + + expr : SymPy expression + + name : Symbol, MatrixSymbol, optional + The name of this return variable. When used for code generation, + this might appear, for example, in the prototype of function in a + list of return values. A dummy name is generated if omitted. + + result_var : Symbol, Indexed, optional + Something that can be used to assign a value to this variable. + Typically the same as `name` but for Indexed this should be e.g., + "y[i]" whereas `name` should be the Symbol "y". Defaults to + `name` if omitted. + + datatype : optional + When not given, the data type will be guessed based on the + assumptions on the expr argument. + + dimensions : sequence containing tuples, optional + If present, this variable is interpreted as an array, + where this sequence of tuples specifies (lower, upper) + bounds for each index of the array. + + precision : int, optional + Controls the precision of floating point constants. + + """ + # Basic because it is the base class for all types of expressions + if not isinstance(expr, (Basic, MatrixBase)): + raise TypeError("The first argument must be a SymPy expression.") + + if name is None: + name = 'result_%d' % abs(hash(expr)) + + if datatype is None: + #try to infer data type from the expression + datatype = get_default_datatype(expr) + + if isinstance(name, str): + if isinstance(expr, (MatrixBase, MatrixExpr)): + name = MatrixSymbol(name, *expr.shape) + else: + name = Symbol(name) + + if result_var is None: + result_var = name + + Variable.__init__(self, name, datatype=datatype, + dimensions=dimensions, precision=precision) + ResultBase.__init__(self, expr, result_var) + + def __str__(self): + return "%s(%r, %r, %r)" % (self.__class__.__name__, self.expr, self.name, + self.result_var) + + __repr__ = __str__ + + +# +# Transformation of routine objects into code +# + +class CodeGen: + """Abstract class for the code generators.""" + + printer = None # will be set to an instance of a CodePrinter subclass + + def _indent_code(self, codelines): + return self.printer.indent_code(codelines) + + def _printer_method_with_settings(self, method, settings=None, *args, **kwargs): + settings = settings or {} + ori = {k: self.printer._settings[k] for k in settings} + for k, v in settings.items(): + self.printer._settings[k] = v + result = getattr(self.printer, method)(*args, **kwargs) + for k, v in ori.items(): + self.printer._settings[k] = v + return result + + def _get_symbol(self, s): + """Returns the symbol as fcode prints it.""" + if self.printer._settings['human']: + expr_str = self.printer.doprint(s) + else: + constants, not_supported, expr_str = self.printer.doprint(s) + if constants or not_supported: + raise ValueError("Failed to print %s" % str(s)) + return expr_str.strip() + + def __init__(self, project="project", cse=False): + """Initialize a code generator. + + Derived classes will offer more options that affect the generated + code. + + """ + self.project = project + self.cse = cse + + def routine(self, name, expr, argument_sequence=None, global_vars=None): + """Creates an Routine object that is appropriate for this language. + + This implementation is appropriate for at least C/Fortran. Subclasses + can override this if necessary. + + Here, we assume at most one return value (the l-value) which must be + scalar. Additional outputs are OutputArguments (e.g., pointers on + right-hand-side or pass-by-reference). Matrices are always returned + via OutputArguments. If ``argument_sequence`` is None, arguments will + be ordered alphabetically, but with all InputArguments first, and then + OutputArgument and InOutArguments. + + """ + + if self.cse: + from sympy.simplify.cse_main import cse + + if is_sequence(expr) and not isinstance(expr, (MatrixBase, MatrixExpr)): + if not expr: + raise ValueError("No expression given") + for e in expr: + if not e.is_Equality: + raise CodeGenError("Lists of expressions must all be Equalities. {} is not.".format(e)) + + # create a list of right hand sides and simplify them + rhs = [e.rhs for e in expr] + common, simplified = cse(rhs) + + # pack the simplified expressions back up with their left hand sides + expr = [Equality(e.lhs, rhs) for e, rhs in zip(expr, simplified)] + else: + if isinstance(expr, Equality): + common, simplified = cse(expr.rhs) #, ignore=in_out_args) + expr = Equality(expr.lhs, simplified[0]) + else: + common, simplified = cse(expr) + expr = simplified + + local_vars = [Result(b,a) for a,b in common] + local_symbols = {a for a,_ in common} + local_expressions = Tuple(*[b for _,b in common]) + else: + local_expressions = Tuple() + + if is_sequence(expr) and not isinstance(expr, (MatrixBase, MatrixExpr)): + if not expr: + raise ValueError("No expression given") + expressions = Tuple(*expr) + else: + expressions = Tuple(expr) + + if self.cse: + if {i.label for i in expressions.atoms(Idx)} != set(): + raise CodeGenError("CSE and Indexed expressions do not play well together yet") + else: + # local variables for indexed expressions + local_vars = {i.label for i in expressions.atoms(Idx)} + local_symbols = local_vars + + # global variables + global_vars = set() if global_vars is None else set(global_vars) + + # symbols that should be arguments + symbols = (expressions.free_symbols | local_expressions.free_symbols) - local_symbols - global_vars + new_symbols = set() + new_symbols.update(symbols) + + for symbol in symbols: + if isinstance(symbol, Idx): + new_symbols.remove(symbol) + new_symbols.update(symbol.args[1].free_symbols) + if isinstance(symbol, Indexed): + new_symbols.remove(symbol) + symbols = new_symbols + + # Decide whether to use output argument or return value + return_val = [] + output_args = [] + for expr in expressions: + if isinstance(expr, Equality): + out_arg = expr.lhs + expr = expr.rhs + if isinstance(out_arg, Indexed): + dims = tuple([ (S.Zero, dim - 1) for dim in out_arg.shape]) + symbol = out_arg.base.label + elif isinstance(out_arg, Symbol): + dims = [] + symbol = out_arg + elif isinstance(out_arg, MatrixSymbol): + dims = tuple([ (S.Zero, dim - 1) for dim in out_arg.shape]) + symbol = out_arg + else: + raise CodeGenError("Only Indexed, Symbol, or MatrixSymbol " + "can define output arguments.") + + if expr.has(symbol): + output_args.append( + InOutArgument(symbol, out_arg, expr, dimensions=dims)) + else: + output_args.append( + OutputArgument(symbol, out_arg, expr, dimensions=dims)) + + # remove duplicate arguments when they are not local variables + if symbol not in local_vars: + # avoid duplicate arguments + symbols.remove(symbol) + elif isinstance(expr, (ImmutableMatrix, MatrixSlice)): + # Create a "dummy" MatrixSymbol to use as the Output arg + out_arg = MatrixSymbol('out_%s' % abs(hash(expr)), *expr.shape) + dims = tuple([(S.Zero, dim - 1) for dim in out_arg.shape]) + output_args.append( + OutputArgument(out_arg, out_arg, expr, dimensions=dims)) + else: + return_val.append(Result(expr)) + + arg_list = [] + + # setup input argument list + + # helper to get dimensions for data for array-like args + def dimensions(s): + return [(S.Zero, dim - 1) for dim in s.shape] + + array_symbols = {} + for array in expressions.atoms(Indexed) | local_expressions.atoms(Indexed): + array_symbols[array.base.label] = array + for array in expressions.atoms(MatrixSymbol) | local_expressions.atoms(MatrixSymbol): + array_symbols[array] = array + + for symbol in sorted(symbols, key=str): + if symbol in array_symbols: + array = array_symbols[symbol] + metadata = {'dimensions': dimensions(array)} + else: + metadata = {} + + arg_list.append(InputArgument(symbol, **metadata)) + + output_args.sort(key=lambda x: str(x.name)) + arg_list.extend(output_args) + + if argument_sequence is not None: + # if the user has supplied IndexedBase instances, we'll accept that + new_sequence = [] + for arg in argument_sequence: + if isinstance(arg, IndexedBase): + new_sequence.append(arg.label) + else: + new_sequence.append(arg) + argument_sequence = new_sequence + + missing = [x for x in arg_list if x.name not in argument_sequence] + if missing: + msg = "Argument list didn't specify: {0} " + msg = msg.format(", ".join([str(m.name) for m in missing])) + raise CodeGenArgumentListError(msg, missing) + + # create redundant arguments to produce the requested sequence + name_arg_dict = {x.name: x for x in arg_list} + new_args = [] + for symbol in argument_sequence: + try: + new_args.append(name_arg_dict[symbol]) + except KeyError: + if isinstance(symbol, (IndexedBase, MatrixSymbol)): + metadata = {'dimensions': dimensions(symbol)} + else: + metadata = {} + new_args.append(InputArgument(symbol, **metadata)) + arg_list = new_args + + return Routine(name, arg_list, return_val, local_vars, global_vars) + + def write(self, routines, prefix, to_files=False, header=True, empty=True): + """Writes all the source code files for the given routines. + + The generated source is returned as a list of (filename, contents) + tuples, or is written to files (see below). Each filename consists + of the given prefix, appended with an appropriate extension. + + Parameters + ========== + + routines : list + A list of Routine instances to be written + + prefix : string + The prefix for the output files + + to_files : bool, optional + When True, the output is written to files. Otherwise, a list + of (filename, contents) tuples is returned. [default: False] + + header : bool, optional + When True, a header comment is included on top of each source + file. [default: True] + + empty : bool, optional + When True, empty lines are included to structure the source + files. [default: True] + + """ + if to_files: + for dump_fn in self.dump_fns: + filename = "%s.%s" % (prefix, dump_fn.extension) + with open(filename, "w") as f: + dump_fn(self, routines, f, prefix, header, empty) + else: + result = [] + for dump_fn in self.dump_fns: + filename = "%s.%s" % (prefix, dump_fn.extension) + contents = StringIO() + dump_fn(self, routines, contents, prefix, header, empty) + result.append((filename, contents.getvalue())) + return result + + def dump_code(self, routines, f, prefix, header=True, empty=True): + """Write the code by calling language specific methods. + + The generated file contains all the definitions of the routines in + low-level code and refers to the header file if appropriate. + + Parameters + ========== + + routines : list + A list of Routine instances. + + f : file-like + Where to write the file. + + prefix : string + The filename prefix, used to refer to the proper header file. + Only the basename of the prefix is used. + + header : bool, optional + When True, a header comment is included on top of each source + file. [default : True] + + empty : bool, optional + When True, empty lines are included to structure the source + files. [default : True] + + """ + + code_lines = self._preprocessor_statements(prefix) + + for routine in routines: + if empty: + code_lines.append("\n") + code_lines.extend(self._get_routine_opening(routine)) + code_lines.extend(self._declare_arguments(routine)) + code_lines.extend(self._declare_globals(routine)) + code_lines.extend(self._declare_locals(routine)) + if empty: + code_lines.append("\n") + code_lines.extend(self._call_printer(routine)) + if empty: + code_lines.append("\n") + code_lines.extend(self._get_routine_ending(routine)) + + code_lines = self._indent_code(''.join(code_lines)) + + if header: + code_lines = ''.join(self._get_header() + [code_lines]) + + if code_lines: + f.write(code_lines) + + +class CodeGenError(Exception): + pass + + +class CodeGenArgumentListError(Exception): + @property + def missing_args(self): + return self.args[1] + + +header_comment = """Code generated with SymPy %(version)s + +See http://www.sympy.org/ for more information. + +This file is part of '%(project)s' +""" + + +class CCodeGen(CodeGen): + """Generator for C code. + + The .write() method inherited from CodeGen will output a code file and + an interface file, .c and .h respectively. + + """ + + code_extension = "c" + interface_extension = "h" + standard = 'c99' + + def __init__(self, project="project", printer=None, + preprocessor_statements=None, cse=False): + super().__init__(project=project, cse=cse) + self.printer = printer or c_code_printers[self.standard.lower()]() + + self.preprocessor_statements = preprocessor_statements + if preprocessor_statements is None: + self.preprocessor_statements = ['#include '] + + def _get_header(self): + """Writes a common header for the generated files.""" + code_lines = [] + code_lines.append("/" + "*"*78 + '\n') + tmp = header_comment % {"version": sympy_version, + "project": self.project} + for line in tmp.splitlines(): + code_lines.append(" *%s*\n" % line.center(76)) + code_lines.append(" " + "*"*78 + "/\n") + return code_lines + + def get_prototype(self, routine): + """Returns a string for the function prototype of the routine. + + If the routine has multiple result objects, an CodeGenError is + raised. + + See: https://en.wikipedia.org/wiki/Function_prototype + + """ + if len(routine.results) > 1: + raise CodeGenError("C only supports a single or no return value.") + elif len(routine.results) == 1: + ctype = routine.results[0].get_datatype('C') + else: + ctype = "void" + + type_args = [] + for arg in routine.arguments: + name = self.printer.doprint(arg.name) + if arg.dimensions or isinstance(arg, ResultBase): + type_args.append((arg.get_datatype('C'), "*%s" % name)) + else: + type_args.append((arg.get_datatype('C'), name)) + arguments = ", ".join([ "%s %s" % t for t in type_args]) + return "%s %s(%s)" % (ctype, routine.name, arguments) + + def _preprocessor_statements(self, prefix): + code_lines = [] + code_lines.append('#include "{}.h"'.format(os.path.basename(prefix))) + code_lines.extend(self.preprocessor_statements) + code_lines = ['{}\n'.format(l) for l in code_lines] + return code_lines + + def _get_routine_opening(self, routine): + prototype = self.get_prototype(routine) + return ["%s {\n" % prototype] + + def _declare_arguments(self, routine): + # arguments are declared in prototype + return [] + + def _declare_globals(self, routine): + # global variables are not explicitly declared within C functions + return [] + + def _declare_locals(self, routine): + + # Compose a list of symbols to be dereferenced in the function + # body. These are the arguments that were passed by a reference + # pointer, excluding arrays. + dereference = [] + for arg in routine.arguments: + if isinstance(arg, ResultBase) and not arg.dimensions: + dereference.append(arg.name) + + code_lines = [] + for result in routine.local_vars: + + # local variables that are simple symbols such as those used as indices into + # for loops are defined declared elsewhere. + if not isinstance(result, Result): + continue + + if result.name != result.result_var: + raise CodeGen("Result variable and name should match: {}".format(result)) + assign_to = result.name + t = result.get_datatype('c') + if isinstance(result.expr, (MatrixBase, MatrixExpr)): + dims = result.expr.shape + code_lines.append("{} {}[{}];\n".format(t, str(assign_to), dims[0]*dims[1])) + prefix = "" + else: + prefix = "const {} ".format(t) + + constants, not_c, c_expr = self._printer_method_with_settings( + 'doprint', {"human": False, "dereference": dereference, "strict": False}, + result.expr, assign_to=assign_to) + + for name, value in sorted(constants, key=str): + code_lines.append("double const %s = %s;\n" % (name, value)) + + code_lines.append("{}{}\n".format(prefix, c_expr)) + + return code_lines + + def _call_printer(self, routine): + code_lines = [] + + # Compose a list of symbols to be dereferenced in the function + # body. These are the arguments that were passed by a reference + # pointer, excluding arrays. + dereference = [] + for arg in routine.arguments: + if isinstance(arg, ResultBase) and not arg.dimensions: + dereference.append(arg.name) + + return_val = None + for result in routine.result_variables: + if isinstance(result, Result): + assign_to = routine.name + "_result" + t = result.get_datatype('c') + code_lines.append("{} {};\n".format(t, str(assign_to))) + return_val = assign_to + else: + assign_to = result.result_var + + try: + constants, not_c, c_expr = self._printer_method_with_settings( + 'doprint', {"human": False, "dereference": dereference, "strict": False}, + result.expr, assign_to=assign_to) + except AssignmentError: + assign_to = result.result_var + code_lines.append( + "%s %s;\n" % (result.get_datatype('c'), str(assign_to))) + constants, not_c, c_expr = self._printer_method_with_settings( + 'doprint', {"human": False, "dereference": dereference, "strict": False}, + result.expr, assign_to=assign_to) + + for name, value in sorted(constants, key=str): + code_lines.append("double const %s = %s;\n" % (name, value)) + code_lines.append("%s\n" % c_expr) + + if return_val: + code_lines.append(" return %s;\n" % return_val) + return code_lines + + def _get_routine_ending(self, routine): + return ["}\n"] + + def dump_c(self, routines, f, prefix, header=True, empty=True): + self.dump_code(routines, f, prefix, header, empty) + dump_c.extension = code_extension # type: ignore + dump_c.__doc__ = CodeGen.dump_code.__doc__ + + def dump_h(self, routines, f, prefix, header=True, empty=True): + """Writes the C header file. + + This file contains all the function declarations. + + Parameters + ========== + + routines : list + A list of Routine instances. + + f : file-like + Where to write the file. + + prefix : string + The filename prefix, used to construct the include guards. + Only the basename of the prefix is used. + + header : bool, optional + When True, a header comment is included on top of each source + file. [default : True] + + empty : bool, optional + When True, empty lines are included to structure the source + files. [default : True] + + """ + if header: + print(''.join(self._get_header()), file=f) + guard_name = "%s__%s__H" % (self.project.replace( + " ", "_").upper(), prefix.replace("/", "_").upper()) + # include guards + if empty: + print(file=f) + print("#ifndef %s" % guard_name, file=f) + print("#define %s" % guard_name, file=f) + if empty: + print(file=f) + # declaration of the function prototypes + for routine in routines: + prototype = self.get_prototype(routine) + print("%s;" % prototype, file=f) + # end if include guards + if empty: + print(file=f) + print("#endif", file=f) + if empty: + print(file=f) + dump_h.extension = interface_extension # type: ignore + + # This list of dump functions is used by CodeGen.write to know which dump + # functions it has to call. + dump_fns = [dump_c, dump_h] + +class C89CodeGen(CCodeGen): + standard = 'C89' + +class C99CodeGen(CCodeGen): + standard = 'C99' + +class FCodeGen(CodeGen): + """Generator for Fortran 95 code + + The .write() method inherited from CodeGen will output a code file and + an interface file, .f90 and .h respectively. + + """ + + code_extension = "f90" + interface_extension = "h" + + def __init__(self, project='project', printer=None): + super().__init__(project) + self.printer = printer or FCodePrinter() + + def _get_header(self): + """Writes a common header for the generated files.""" + code_lines = [] + code_lines.append("!" + "*"*78 + '\n') + tmp = header_comment % {"version": sympy_version, + "project": self.project} + for line in tmp.splitlines(): + code_lines.append("!*%s*\n" % line.center(76)) + code_lines.append("!" + "*"*78 + '\n') + return code_lines + + def _preprocessor_statements(self, prefix): + return [] + + def _get_routine_opening(self, routine): + """Returns the opening statements of the fortran routine.""" + code_list = [] + if len(routine.results) > 1: + raise CodeGenError( + "Fortran only supports a single or no return value.") + elif len(routine.results) == 1: + result = routine.results[0] + code_list.append(result.get_datatype('fortran')) + code_list.append("function") + else: + code_list.append("subroutine") + + args = ", ".join("%s" % self._get_symbol(arg.name) + for arg in routine.arguments) + + call_sig = "{}({})\n".format(routine.name, args) + # Fortran 95 requires all lines be less than 132 characters, so wrap + # this line before appending. + call_sig = ' &\n'.join(textwrap.wrap(call_sig, + width=60, + break_long_words=False)) + '\n' + code_list.append(call_sig) + code_list = [' '.join(code_list)] + code_list.append('implicit none\n') + return code_list + + def _declare_arguments(self, routine): + # argument type declarations + code_list = [] + array_list = [] + scalar_list = [] + for arg in routine.arguments: + + if isinstance(arg, InputArgument): + typeinfo = "%s, intent(in)" % arg.get_datatype('fortran') + elif isinstance(arg, InOutArgument): + typeinfo = "%s, intent(inout)" % arg.get_datatype('fortran') + elif isinstance(arg, OutputArgument): + typeinfo = "%s, intent(out)" % arg.get_datatype('fortran') + else: + raise CodeGenError("Unknown Argument type: %s" % type(arg)) + + fprint = self._get_symbol + + if arg.dimensions: + # fortran arrays start at 1 + dimstr = ", ".join(["%s:%s" % ( + fprint(dim[0] + 1), fprint(dim[1] + 1)) + for dim in arg.dimensions]) + typeinfo += ", dimension(%s)" % dimstr + array_list.append("%s :: %s\n" % (typeinfo, fprint(arg.name))) + else: + scalar_list.append("%s :: %s\n" % (typeinfo, fprint(arg.name))) + + # scalars first, because they can be used in array declarations + code_list.extend(scalar_list) + code_list.extend(array_list) + + return code_list + + def _declare_globals(self, routine): + # Global variables not explicitly declared within Fortran 90 functions. + # Note: a future F77 mode may need to generate "common" blocks. + return [] + + def _declare_locals(self, routine): + code_list = [] + for var in sorted(routine.local_vars, key=str): + typeinfo = get_default_datatype(var) + code_list.append("%s :: %s\n" % ( + typeinfo.fname, self._get_symbol(var))) + return code_list + + def _get_routine_ending(self, routine): + """Returns the closing statements of the fortran routine.""" + if len(routine.results) == 1: + return ["end function\n"] + else: + return ["end subroutine\n"] + + def get_interface(self, routine): + """Returns a string for the function interface. + + The routine should have a single result object, which can be None. + If the routine has multiple result objects, a CodeGenError is + raised. + + See: https://en.wikipedia.org/wiki/Function_prototype + + """ + prototype = [ "interface\n" ] + prototype.extend(self._get_routine_opening(routine)) + prototype.extend(self._declare_arguments(routine)) + prototype.extend(self._get_routine_ending(routine)) + prototype.append("end interface\n") + + return "".join(prototype) + + def _call_printer(self, routine): + declarations = [] + code_lines = [] + for result in routine.result_variables: + if isinstance(result, Result): + assign_to = routine.name + elif isinstance(result, (OutputArgument, InOutArgument)): + assign_to = result.result_var + + constants, not_fortran, f_expr = self._printer_method_with_settings( + 'doprint', {"human": False, "source_format": 'free', "standard": 95, "strict": False}, + result.expr, assign_to=assign_to) + + for obj, v in sorted(constants, key=str): + t = get_default_datatype(obj) + declarations.append( + "%s, parameter :: %s = %s\n" % (t.fname, obj, v)) + for obj in sorted(not_fortran, key=str): + t = get_default_datatype(obj) + if isinstance(obj, Function): + name = obj.func + else: + name = obj + declarations.append("%s :: %s\n" % (t.fname, name)) + + code_lines.append("%s\n" % f_expr) + return declarations + code_lines + + def _indent_code(self, codelines): + return self._printer_method_with_settings( + 'indent_code', {"human": False, "source_format": 'free', "strict": False}, codelines) + + def dump_f95(self, routines, f, prefix, header=True, empty=True): + # check that symbols are unique with ignorecase + for r in routines: + lowercase = {str(x).lower() for x in r.variables} + orig_case = {str(x) for x in r.variables} + if len(lowercase) < len(orig_case): + raise CodeGenError("Fortran ignores case. Got symbols: %s" % + (", ".join([str(var) for var in r.variables]))) + self.dump_code(routines, f, prefix, header, empty) + dump_f95.extension = code_extension # type: ignore + dump_f95.__doc__ = CodeGen.dump_code.__doc__ + + def dump_h(self, routines, f, prefix, header=True, empty=True): + """Writes the interface to a header file. + + This file contains all the function declarations. + + Parameters + ========== + + routines : list + A list of Routine instances. + + f : file-like + Where to write the file. + + prefix : string + The filename prefix. + + header : bool, optional + When True, a header comment is included on top of each source + file. [default : True] + + empty : bool, optional + When True, empty lines are included to structure the source + files. [default : True] + + """ + if header: + print(''.join(self._get_header()), file=f) + if empty: + print(file=f) + # declaration of the function prototypes + for routine in routines: + prototype = self.get_interface(routine) + f.write(prototype) + if empty: + print(file=f) + dump_h.extension = interface_extension # type: ignore + + # This list of dump functions is used by CodeGen.write to know which dump + # functions it has to call. + dump_fns = [dump_f95, dump_h] + + +class JuliaCodeGen(CodeGen): + """Generator for Julia code. + + The .write() method inherited from CodeGen will output a code file + .jl. + + """ + + code_extension = "jl" + + def __init__(self, project='project', printer=None): + super().__init__(project) + self.printer = printer or JuliaCodePrinter() + + def routine(self, name, expr, argument_sequence, global_vars): + """Specialized Routine creation for Julia.""" + + if is_sequence(expr) and not isinstance(expr, (MatrixBase, MatrixExpr)): + if not expr: + raise ValueError("No expression given") + expressions = Tuple(*expr) + else: + expressions = Tuple(expr) + + # local variables + local_vars = {i.label for i in expressions.atoms(Idx)} + + # global variables + global_vars = set() if global_vars is None else set(global_vars) + + # symbols that should be arguments + old_symbols = expressions.free_symbols - local_vars - global_vars + symbols = set() + for s in old_symbols: + if isinstance(s, Idx): + symbols.update(s.args[1].free_symbols) + elif not isinstance(s, Indexed): + symbols.add(s) + + # Julia supports multiple return values + return_vals = [] + output_args = [] + for (i, expr) in enumerate(expressions): + if isinstance(expr, Equality): + out_arg = expr.lhs + expr = expr.rhs + symbol = out_arg + if isinstance(out_arg, Indexed): + dims = tuple([ (S.One, dim) for dim in out_arg.shape]) + symbol = out_arg.base.label + output_args.append(InOutArgument(symbol, out_arg, expr, dimensions=dims)) + if not isinstance(out_arg, (Indexed, Symbol, MatrixSymbol)): + raise CodeGenError("Only Indexed, Symbol, or MatrixSymbol " + "can define output arguments.") + + return_vals.append(Result(expr, name=symbol, result_var=out_arg)) + if not expr.has(symbol): + # this is a pure output: remove from the symbols list, so + # it doesn't become an input. + symbols.remove(symbol) + + else: + # we have no name for this output + return_vals.append(Result(expr, name='out%d' % (i+1))) + + # setup input argument list + output_args.sort(key=lambda x: str(x.name)) + arg_list = list(output_args) + array_symbols = {} + for array in expressions.atoms(Indexed): + array_symbols[array.base.label] = array + for array in expressions.atoms(MatrixSymbol): + array_symbols[array] = array + + for symbol in sorted(symbols, key=str): + arg_list.append(InputArgument(symbol)) + + if argument_sequence is not None: + # if the user has supplied IndexedBase instances, we'll accept that + new_sequence = [] + for arg in argument_sequence: + if isinstance(arg, IndexedBase): + new_sequence.append(arg.label) + else: + new_sequence.append(arg) + argument_sequence = new_sequence + + missing = [x for x in arg_list if x.name not in argument_sequence] + if missing: + msg = "Argument list didn't specify: {0} " + msg = msg.format(", ".join([str(m.name) for m in missing])) + raise CodeGenArgumentListError(msg, missing) + + # create redundant arguments to produce the requested sequence + name_arg_dict = {x.name: x for x in arg_list} + new_args = [] + for symbol in argument_sequence: + try: + new_args.append(name_arg_dict[symbol]) + except KeyError: + new_args.append(InputArgument(symbol)) + arg_list = new_args + + return Routine(name, arg_list, return_vals, local_vars, global_vars) + + def _get_header(self): + """Writes a common header for the generated files.""" + code_lines = [] + tmp = header_comment % {"version": sympy_version, + "project": self.project} + for line in tmp.splitlines(): + if line == '': + code_lines.append("#\n") + else: + code_lines.append("# %s\n" % line) + return code_lines + + def _preprocessor_statements(self, prefix): + return [] + + def _get_routine_opening(self, routine): + """Returns the opening statements of the routine.""" + code_list = [] + code_list.append("function ") + + # Inputs + args = [] + for arg in routine.arguments: + if isinstance(arg, OutputArgument): + raise CodeGenError("Julia: invalid argument of type %s" % + str(type(arg))) + if isinstance(arg, (InputArgument, InOutArgument)): + args.append("%s" % self._get_symbol(arg.name)) + args = ", ".join(args) + code_list.append("%s(%s)\n" % (routine.name, args)) + code_list = [ "".join(code_list) ] + + return code_list + + def _declare_arguments(self, routine): + return [] + + def _declare_globals(self, routine): + return [] + + def _declare_locals(self, routine): + return [] + + def _get_routine_ending(self, routine): + outs = [] + for result in routine.results: + if isinstance(result, Result): + # Note: name not result_var; want `y` not `y[i]` for Indexed + s = self._get_symbol(result.name) + else: + raise CodeGenError("unexpected object in Routine results") + outs.append(s) + return ["return " + ", ".join(outs) + "\nend\n"] + + def _call_printer(self, routine): + declarations = [] + code_lines = [] + for result in routine.results: + if isinstance(result, Result): + assign_to = result.result_var + else: + raise CodeGenError("unexpected object in Routine results") + + constants, not_supported, jl_expr = self._printer_method_with_settings( + 'doprint', {"human": False, "strict": False}, result.expr, assign_to=assign_to) + + for obj, v in sorted(constants, key=str): + declarations.append( + "%s = %s\n" % (obj, v)) + for obj in sorted(not_supported, key=str): + if isinstance(obj, Function): + name = obj.func + else: + name = obj + declarations.append( + "# unsupported: %s\n" % (name)) + code_lines.append("%s\n" % (jl_expr)) + return declarations + code_lines + + def _indent_code(self, codelines): + # Note that indenting seems to happen twice, first + # statement-by-statement by JuliaPrinter then again here. + p = JuliaCodePrinter({'human': False, "strict": False}) + return p.indent_code(codelines) + + def dump_jl(self, routines, f, prefix, header=True, empty=True): + self.dump_code(routines, f, prefix, header, empty) + + dump_jl.extension = code_extension # type: ignore + dump_jl.__doc__ = CodeGen.dump_code.__doc__ + + # This list of dump functions is used by CodeGen.write to know which dump + # functions it has to call. + dump_fns = [dump_jl] + + +class OctaveCodeGen(CodeGen): + """Generator for Octave code. + + The .write() method inherited from CodeGen will output a code file + .m. + + Octave .m files usually contain one function. That function name should + match the filename (``prefix``). If you pass multiple ``name_expr`` pairs, + the latter ones are presumed to be private functions accessed by the + primary function. + + You should only pass inputs to ``argument_sequence``: outputs are ordered + according to their order in ``name_expr``. + + """ + + code_extension = "m" + + def __init__(self, project='project', printer=None): + super().__init__(project) + self.printer = printer or OctaveCodePrinter() + + def routine(self, name, expr, argument_sequence, global_vars): + """Specialized Routine creation for Octave.""" + + # FIXME: this is probably general enough for other high-level + # languages, perhaps its the C/Fortran one that is specialized! + + if is_sequence(expr) and not isinstance(expr, (MatrixBase, MatrixExpr)): + if not expr: + raise ValueError("No expression given") + expressions = Tuple(*expr) + else: + expressions = Tuple(expr) + + # local variables + local_vars = {i.label for i in expressions.atoms(Idx)} + + # global variables + global_vars = set() if global_vars is None else set(global_vars) + + # symbols that should be arguments + old_symbols = expressions.free_symbols - local_vars - global_vars + symbols = set() + for s in old_symbols: + if isinstance(s, Idx): + symbols.update(s.args[1].free_symbols) + elif not isinstance(s, Indexed): + symbols.add(s) + + # Octave supports multiple return values + return_vals = [] + for (i, expr) in enumerate(expressions): + if isinstance(expr, Equality): + out_arg = expr.lhs + expr = expr.rhs + symbol = out_arg + if isinstance(out_arg, Indexed): + symbol = out_arg.base.label + if not isinstance(out_arg, (Indexed, Symbol, MatrixSymbol)): + raise CodeGenError("Only Indexed, Symbol, or MatrixSymbol " + "can define output arguments.") + + return_vals.append(Result(expr, name=symbol, result_var=out_arg)) + if not expr.has(symbol): + # this is a pure output: remove from the symbols list, so + # it doesn't become an input. + symbols.remove(symbol) + + else: + # we have no name for this output + return_vals.append(Result(expr, name='out%d' % (i+1))) + + # setup input argument list + arg_list = [] + array_symbols = {} + for array in expressions.atoms(Indexed): + array_symbols[array.base.label] = array + for array in expressions.atoms(MatrixSymbol): + array_symbols[array] = array + + for symbol in sorted(symbols, key=str): + arg_list.append(InputArgument(symbol)) + + if argument_sequence is not None: + # if the user has supplied IndexedBase instances, we'll accept that + new_sequence = [] + for arg in argument_sequence: + if isinstance(arg, IndexedBase): + new_sequence.append(arg.label) + else: + new_sequence.append(arg) + argument_sequence = new_sequence + + missing = [x for x in arg_list if x.name not in argument_sequence] + if missing: + msg = "Argument list didn't specify: {0} " + msg = msg.format(", ".join([str(m.name) for m in missing])) + raise CodeGenArgumentListError(msg, missing) + + # create redundant arguments to produce the requested sequence + name_arg_dict = {x.name: x for x in arg_list} + new_args = [] + for symbol in argument_sequence: + try: + new_args.append(name_arg_dict[symbol]) + except KeyError: + new_args.append(InputArgument(symbol)) + arg_list = new_args + + return Routine(name, arg_list, return_vals, local_vars, global_vars) + + def _get_header(self): + """Writes a common header for the generated files.""" + code_lines = [] + tmp = header_comment % {"version": sympy_version, + "project": self.project} + for line in tmp.splitlines(): + if line == '': + code_lines.append("%\n") + else: + code_lines.append("%% %s\n" % line) + return code_lines + + def _preprocessor_statements(self, prefix): + return [] + + def _get_routine_opening(self, routine): + """Returns the opening statements of the routine.""" + code_list = [] + code_list.append("function ") + + # Outputs + outs = [] + for result in routine.results: + if isinstance(result, Result): + # Note: name not result_var; want `y` not `y(i)` for Indexed + s = self._get_symbol(result.name) + else: + raise CodeGenError("unexpected object in Routine results") + outs.append(s) + if len(outs) > 1: + code_list.append("[" + (", ".join(outs)) + "]") + else: + code_list.append("".join(outs)) + code_list.append(" = ") + + # Inputs + args = [] + for arg in routine.arguments: + if isinstance(arg, (OutputArgument, InOutArgument)): + raise CodeGenError("Octave: invalid argument of type %s" % + str(type(arg))) + if isinstance(arg, InputArgument): + args.append("%s" % self._get_symbol(arg.name)) + args = ", ".join(args) + code_list.append("%s(%s)\n" % (routine.name, args)) + code_list = [ "".join(code_list) ] + + return code_list + + def _declare_arguments(self, routine): + return [] + + def _declare_globals(self, routine): + if not routine.global_vars: + return [] + s = " ".join(sorted([self._get_symbol(g) for g in routine.global_vars])) + return ["global " + s + "\n"] + + def _declare_locals(self, routine): + return [] + + def _get_routine_ending(self, routine): + return ["end\n"] + + def _call_printer(self, routine): + declarations = [] + code_lines = [] + for result in routine.results: + if isinstance(result, Result): + assign_to = result.result_var + else: + raise CodeGenError("unexpected object in Routine results") + + constants, not_supported, oct_expr = self._printer_method_with_settings( + 'doprint', {"human": False, "strict": False}, result.expr, assign_to=assign_to) + + for obj, v in sorted(constants, key=str): + declarations.append( + " %s = %s; %% constant\n" % (obj, v)) + for obj in sorted(not_supported, key=str): + if isinstance(obj, Function): + name = obj.func + else: + name = obj + declarations.append( + " %% unsupported: %s\n" % (name)) + code_lines.append("%s\n" % (oct_expr)) + return declarations + code_lines + + def _indent_code(self, codelines): + return self._printer_method_with_settings( + 'indent_code', {"human": False, "strict": False}, codelines) + + def dump_m(self, routines, f, prefix, header=True, empty=True, inline=True): + # Note used to call self.dump_code() but we need more control for header + + code_lines = self._preprocessor_statements(prefix) + + for i, routine in enumerate(routines): + if i > 0: + if empty: + code_lines.append("\n") + code_lines.extend(self._get_routine_opening(routine)) + if i == 0: + if routine.name != prefix: + raise ValueError('Octave function name should match prefix') + if header: + code_lines.append("%" + prefix.upper() + + " Autogenerated by SymPy\n") + code_lines.append(''.join(self._get_header())) + code_lines.extend(self._declare_arguments(routine)) + code_lines.extend(self._declare_globals(routine)) + code_lines.extend(self._declare_locals(routine)) + if empty: + code_lines.append("\n") + code_lines.extend(self._call_printer(routine)) + if empty: + code_lines.append("\n") + code_lines.extend(self._get_routine_ending(routine)) + + code_lines = self._indent_code(''.join(code_lines)) + + if code_lines: + f.write(code_lines) + + dump_m.extension = code_extension # type: ignore + dump_m.__doc__ = CodeGen.dump_code.__doc__ + + # This list of dump functions is used by CodeGen.write to know which dump + # functions it has to call. + dump_fns = [dump_m] + +class RustCodeGen(CodeGen): + """Generator for Rust code. + + The .write() method inherited from CodeGen will output a code file + .rs + + """ + + code_extension = "rs" + + def __init__(self, project="project", printer=None): + super().__init__(project=project) + self.printer = printer or RustCodePrinter() + + def routine(self, name, expr, argument_sequence, global_vars): + """Specialized Routine creation for Rust.""" + + if is_sequence(expr) and not isinstance(expr, (MatrixBase, MatrixExpr)): + if not expr: + raise ValueError("No expression given") + expressions = Tuple(*expr) + else: + expressions = Tuple(expr) + + # local variables + local_vars = {i.label for i in expressions.atoms(Idx)} + + # global variables + global_vars = set() if global_vars is None else set(global_vars) + + # symbols that should be arguments + symbols = expressions.free_symbols - local_vars - global_vars - expressions.atoms(Indexed) + + # Rust supports multiple return values + return_vals = [] + output_args = [] + for (i, expr) in enumerate(expressions): + if isinstance(expr, Equality): + out_arg = expr.lhs + expr = expr.rhs + symbol = out_arg + if isinstance(out_arg, Indexed): + dims = tuple([ (S.One, dim) for dim in out_arg.shape]) + symbol = out_arg.base.label + output_args.append(InOutArgument(symbol, out_arg, expr, dimensions=dims)) + if not isinstance(out_arg, (Indexed, Symbol, MatrixSymbol)): + raise CodeGenError("Only Indexed, Symbol, or MatrixSymbol " + "can define output arguments.") + + return_vals.append(Result(expr, name=symbol, result_var=out_arg)) + if not expr.has(symbol): + # this is a pure output: remove from the symbols list, so + # it doesn't become an input. + symbols.remove(symbol) + + else: + # we have no name for this output + return_vals.append(Result(expr, name='out%d' % (i+1))) + + # setup input argument list + output_args.sort(key=lambda x: str(x.name)) + arg_list = list(output_args) + array_symbols = {} + for array in expressions.atoms(Indexed): + array_symbols[array.base.label] = array + for array in expressions.atoms(MatrixSymbol): + array_symbols[array] = array + + for symbol in sorted(symbols, key=str): + arg_list.append(InputArgument(symbol)) + + if argument_sequence is not None: + # if the user has supplied IndexedBase instances, we'll accept that + new_sequence = [] + for arg in argument_sequence: + if isinstance(arg, IndexedBase): + new_sequence.append(arg.label) + else: + new_sequence.append(arg) + argument_sequence = new_sequence + + missing = [x for x in arg_list if x.name not in argument_sequence] + if missing: + msg = "Argument list didn't specify: {0} " + msg = msg.format(", ".join([str(m.name) for m in missing])) + raise CodeGenArgumentListError(msg, missing) + + # create redundant arguments to produce the requested sequence + name_arg_dict = {x.name: x for x in arg_list} + new_args = [] + for symbol in argument_sequence: + try: + new_args.append(name_arg_dict[symbol]) + except KeyError: + new_args.append(InputArgument(symbol)) + arg_list = new_args + + return Routine(name, arg_list, return_vals, local_vars, global_vars) + + + def _get_header(self): + """Writes a common header for the generated files.""" + code_lines = [] + code_lines.append("/*\n") + tmp = header_comment % {"version": sympy_version, + "project": self.project} + for line in tmp.splitlines(): + code_lines.append((" *%s" % line.center(76)).rstrip() + "\n") + code_lines.append(" */\n") + return code_lines + + def get_prototype(self, routine): + """Returns a string for the function prototype of the routine. + + If the routine has multiple result objects, an CodeGenError is + raised. + + See: https://en.wikipedia.org/wiki/Function_prototype + + """ + results = [i.get_datatype('Rust') for i in routine.results] + + if len(results) == 1: + rstype = " -> " + results[0] + elif len(routine.results) > 1: + rstype = " -> (" + ", ".join(results) + ")" + else: + rstype = "" + + type_args = [] + for arg in routine.arguments: + name = self.printer.doprint(arg.name) + if arg.dimensions or isinstance(arg, ResultBase): + type_args.append(("*%s" % name, arg.get_datatype('Rust'))) + else: + type_args.append((name, arg.get_datatype('Rust'))) + arguments = ", ".join([ "%s: %s" % t for t in type_args]) + return "fn %s(%s)%s" % (routine.name, arguments, rstype) + + def _preprocessor_statements(self, prefix): + code_lines = [] + # code_lines.append("use std::f64::consts::*;\n") + return code_lines + + def _get_routine_opening(self, routine): + prototype = self.get_prototype(routine) + return ["%s {\n" % prototype] + + def _declare_arguments(self, routine): + # arguments are declared in prototype + return [] + + def _declare_globals(self, routine): + # global variables are not explicitly declared within C functions + return [] + + def _declare_locals(self, routine): + # loop variables are declared in loop statement + return [] + + def _call_printer(self, routine): + + code_lines = [] + declarations = [] + returns = [] + + # Compose a list of symbols to be dereferenced in the function + # body. These are the arguments that were passed by a reference + # pointer, excluding arrays. + dereference = [] + for arg in routine.arguments: + if isinstance(arg, ResultBase) and not arg.dimensions: + dereference.append(arg.name) + + for result in routine.results: + if isinstance(result, Result): + assign_to = result.result_var + returns.append(str(result.result_var)) + else: + raise CodeGenError("unexpected object in Routine results") + + constants, not_supported, rs_expr = self._printer_method_with_settings( + 'doprint', {"human": False, "strict": False}, result.expr, assign_to=assign_to) + + for name, value in sorted(constants, key=str): + declarations.append("const %s: f64 = %s;\n" % (name, value)) + + for obj in sorted(not_supported, key=str): + if isinstance(obj, Function): + name = obj.func + else: + name = obj + declarations.append("// unsupported: %s\n" % (name)) + + code_lines.append("let %s\n" % rs_expr) + + if len(returns) > 1: + returns = ['(' + ', '.join(returns) + ')'] + + returns.append('\n') + + return declarations + code_lines + returns + + def _get_routine_ending(self, routine): + return ["}\n"] + + def dump_rs(self, routines, f, prefix, header=True, empty=True): + self.dump_code(routines, f, prefix, header, empty) + + dump_rs.extension = code_extension # type: ignore + dump_rs.__doc__ = CodeGen.dump_code.__doc__ + + # This list of dump functions is used by CodeGen.write to know which dump + # functions it has to call. + dump_fns = [dump_rs] + + + + +def get_code_generator(language, project=None, standard=None, printer = None): + if language == 'C': + if standard is None: + pass + elif standard.lower() == 'c89': + language = 'C89' + elif standard.lower() == 'c99': + language = 'C99' + CodeGenClass = {"C": CCodeGen, "C89": C89CodeGen, "C99": C99CodeGen, + "F95": FCodeGen, "JULIA": JuliaCodeGen, + "OCTAVE": OctaveCodeGen, + "RUST": RustCodeGen}.get(language.upper()) + if CodeGenClass is None: + raise ValueError("Language '%s' is not supported." % language) + return CodeGenClass(project, printer) + + +# +# Friendly functions +# + + +def codegen(name_expr, language=None, prefix=None, project="project", + to_files=False, header=True, empty=True, argument_sequence=None, + global_vars=None, standard=None, code_gen=None, printer=None): + """Generate source code for expressions in a given language. + + Parameters + ========== + + name_expr : tuple, or list of tuples + A single (name, expression) tuple or a list of (name, expression) + tuples. Each tuple corresponds to a routine. If the expression is + an equality (an instance of class Equality) the left hand side is + considered an output argument. If expression is an iterable, then + the routine will have multiple outputs. + + language : string, + A string that indicates the source code language. This is case + insensitive. Currently, 'C', 'F95' and 'Octave' are supported. + 'Octave' generates code compatible with both Octave and Matlab. + + prefix : string, optional + A prefix for the names of the files that contain the source code. + Language-dependent suffixes will be appended. If omitted, the name + of the first name_expr tuple is used. + + project : string, optional + A project name, used for making unique preprocessor instructions. + [default: "project"] + + to_files : bool, optional + When True, the code will be written to one or more files with the + given prefix, otherwise strings with the names and contents of + these files are returned. [default: False] + + header : bool, optional + When True, a header is written on top of each source file. + [default: True] + + empty : bool, optional + When True, empty lines are used to structure the code. + [default: True] + + argument_sequence : iterable, optional + Sequence of arguments for the routine in a preferred order. A + CodeGenError is raised if required arguments are missing. + Redundant arguments are used without warning. If omitted, + arguments will be ordered alphabetically, but with all input + arguments first, and then output or in-out arguments. + + global_vars : iterable, optional + Sequence of global variables used by the routine. Variables + listed here will not show up as function arguments. + + standard : string, optional + + code_gen : CodeGen instance, optional + An instance of a CodeGen subclass. Overrides ``language``. + + printer : Printer instance, optional + An instance of a Printer subclass. + + Examples + ======== + + >>> from sympy.utilities.codegen import codegen + >>> from sympy.abc import x, y, z + >>> [(c_name, c_code), (h_name, c_header)] = codegen( + ... ("f", x+y*z), "C89", "test", header=False, empty=False) + >>> print(c_name) + test.c + >>> print(c_code) + #include "test.h" + #include + double f(double x, double y, double z) { + double f_result; + f_result = x + y*z; + return f_result; + } + + >>> print(h_name) + test.h + >>> print(c_header) + #ifndef PROJECT__TEST__H + #define PROJECT__TEST__H + double f(double x, double y, double z); + #endif + + + Another example using Equality objects to give named outputs. Here the + filename (prefix) is taken from the first (name, expr) pair. + + >>> from sympy.abc import f, g + >>> from sympy import Eq + >>> [(c_name, c_code), (h_name, c_header)] = codegen( + ... [("myfcn", x + y), ("fcn2", [Eq(f, 2*x), Eq(g, y)])], + ... "C99", header=False, empty=False) + >>> print(c_name) + myfcn.c + >>> print(c_code) + #include "myfcn.h" + #include + double myfcn(double x, double y) { + double myfcn_result; + myfcn_result = x + y; + return myfcn_result; + } + void fcn2(double x, double y, double *f, double *g) { + (*f) = 2*x; + (*g) = y; + } + + + If the generated function(s) will be part of a larger project where various + global variables have been defined, the 'global_vars' option can be used + to remove the specified variables from the function signature + + >>> from sympy.utilities.codegen import codegen + >>> from sympy.abc import x, y, z + >>> [(f_name, f_code), header] = codegen( + ... ("f", x+y*z), "F95", header=False, empty=False, + ... argument_sequence=(x, y), global_vars=(z,)) + >>> print(f_code) + REAL*8 function f(x, y) + implicit none + REAL*8, intent(in) :: x + REAL*8, intent(in) :: y + f = x + y*z + end function + + + """ + + # Initialize the code generator. + if language is None: + if code_gen is None: + raise ValueError("Need either language or code_gen") + else: + if code_gen is not None: + raise ValueError("You cannot specify both language and code_gen.") + code_gen = get_code_generator(language, project, standard, printer) + + if isinstance(name_expr[0], str): + # single tuple is given, turn it into a singleton list with a tuple. + name_expr = [name_expr] + + if prefix is None: + prefix = name_expr[0][0] + + # Construct Routines appropriate for this code_gen from (name, expr) pairs. + routines = [] + for name, expr in name_expr: + routines.append(code_gen.routine(name, expr, argument_sequence, + global_vars)) + + # Write the code. + return code_gen.write(routines, prefix, to_files, header, empty) + + +def make_routine(name, expr, argument_sequence=None, + global_vars=None, language="F95"): + """A factory that makes an appropriate Routine from an expression. + + Parameters + ========== + + name : string + The name of this routine in the generated code. + + expr : expression or list/tuple of expressions + A SymPy expression that the Routine instance will represent. If + given a list or tuple of expressions, the routine will be + considered to have multiple return values and/or output arguments. + + argument_sequence : list or tuple, optional + List arguments for the routine in a preferred order. If omitted, + the results are language dependent, for example, alphabetical order + or in the same order as the given expressions. + + global_vars : iterable, optional + Sequence of global variables used by the routine. Variables + listed here will not show up as function arguments. + + language : string, optional + Specify a target language. The Routine itself should be + language-agnostic but the precise way one is created, error + checking, etc depend on the language. [default: "F95"]. + + Notes + ===== + + A decision about whether to use output arguments or return values is made + depending on both the language and the particular mathematical expressions. + For an expression of type Equality, the left hand side is typically made + into an OutputArgument (or perhaps an InOutArgument if appropriate). + Otherwise, typically, the calculated expression is made a return values of + the routine. + + Examples + ======== + + >>> from sympy.utilities.codegen import make_routine + >>> from sympy.abc import x, y, f, g + >>> from sympy import Eq + >>> r = make_routine('test', [Eq(f, 2*x), Eq(g, x + y)]) + >>> [arg.result_var for arg in r.results] + [] + >>> [arg.name for arg in r.arguments] + [x, y, f, g] + >>> [arg.name for arg in r.result_variables] + [f, g] + >>> r.local_vars + set() + + Another more complicated example with a mixture of specified and + automatically-assigned names. Also has Matrix output. + + >>> from sympy import Matrix + >>> r = make_routine('fcn', [x*y, Eq(f, 1), Eq(g, x + g), Matrix([[x, 2]])]) + >>> [arg.result_var for arg in r.results] # doctest: +SKIP + [result_5397460570204848505] + >>> [arg.expr for arg in r.results] + [x*y] + >>> [arg.name for arg in r.arguments] # doctest: +SKIP + [x, y, f, g, out_8598435338387848786] + + We can examine the various arguments more closely: + + >>> from sympy.utilities.codegen import (InputArgument, OutputArgument, + ... InOutArgument) + >>> [a.name for a in r.arguments if isinstance(a, InputArgument)] + [x, y] + + >>> [a.name for a in r.arguments if isinstance(a, OutputArgument)] # doctest: +SKIP + [f, out_8598435338387848786] + >>> [a.expr for a in r.arguments if isinstance(a, OutputArgument)] + [1, Matrix([[x, 2]])] + + >>> [a.name for a in r.arguments if isinstance(a, InOutArgument)] + [g] + >>> [a.expr for a in r.arguments if isinstance(a, InOutArgument)] + [g + x] + + """ + + # initialize a new code generator + code_gen = get_code_generator(language) + + return code_gen.routine(name, expr, argument_sequence, global_vars) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/decorator.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/decorator.py new file mode 100644 index 0000000000000000000000000000000000000000..82636c35e9f235a1d23aa7df5182418db3ef6b4f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/decorator.py @@ -0,0 +1,339 @@ +"""Useful utility decorators. """ + +from typing import TypeVar +import sys +import types +import inspect +from functools import wraps, update_wrapper + +from sympy.utilities.exceptions import sympy_deprecation_warning + + +T = TypeVar('T') +"""A generic type""" + + +def threaded_factory(func, use_add): + """A factory for ``threaded`` decorators. """ + from sympy.core import sympify + from sympy.matrices import MatrixBase + from sympy.utilities.iterables import iterable + + @wraps(func) + def threaded_func(expr, *args, **kwargs): + if isinstance(expr, MatrixBase): + return expr.applyfunc(lambda f: func(f, *args, **kwargs)) + elif iterable(expr): + try: + return expr.__class__([func(f, *args, **kwargs) for f in expr]) + except TypeError: + return expr + else: + expr = sympify(expr) + + if use_add and expr.is_Add: + return expr.__class__(*[ func(f, *args, **kwargs) for f in expr.args ]) + elif expr.is_Relational: + return expr.__class__(func(expr.lhs, *args, **kwargs), + func(expr.rhs, *args, **kwargs)) + else: + return func(expr, *args, **kwargs) + + return threaded_func + + +def threaded(func): + """Apply ``func`` to sub--elements of an object, including :class:`~.Add`. + + This decorator is intended to make it uniformly possible to apply a + function to all elements of composite objects, e.g. matrices, lists, tuples + and other iterable containers, or just expressions. + + This version of :func:`threaded` decorator allows threading over + elements of :class:`~.Add` class. If this behavior is not desirable + use :func:`xthreaded` decorator. + + Functions using this decorator must have the following signature:: + + @threaded + def function(expr, *args, **kwargs): + + """ + return threaded_factory(func, True) + + +def xthreaded(func): + """Apply ``func`` to sub--elements of an object, excluding :class:`~.Add`. + + This decorator is intended to make it uniformly possible to apply a + function to all elements of composite objects, e.g. matrices, lists, tuples + and other iterable containers, or just expressions. + + This version of :func:`threaded` decorator disallows threading over + elements of :class:`~.Add` class. If this behavior is not desirable + use :func:`threaded` decorator. + + Functions using this decorator must have the following signature:: + + @xthreaded + def function(expr, *args, **kwargs): + + """ + return threaded_factory(func, False) + + +def conserve_mpmath_dps(func): + """After the function finishes, resets the value of ``mpmath.mp.dps`` to + the value it had before the function was run.""" + import mpmath + + def func_wrapper(*args, **kwargs): + dps = mpmath.mp.dps + try: + return func(*args, **kwargs) + finally: + mpmath.mp.dps = dps + + func_wrapper = update_wrapper(func_wrapper, func) + return func_wrapper + + +class no_attrs_in_subclass: + """Don't 'inherit' certain attributes from a base class + + >>> from sympy.utilities.decorator import no_attrs_in_subclass + + >>> class A(object): + ... x = 'test' + + >>> A.x = no_attrs_in_subclass(A, A.x) + + >>> class B(A): + ... pass + + >>> hasattr(A, 'x') + True + >>> hasattr(B, 'x') + False + + """ + def __init__(self, cls, f): + self.cls = cls + self.f = f + + def __get__(self, instance, owner=None): + if owner == self.cls: + if hasattr(self.f, '__get__'): + return self.f.__get__(instance, owner) + return self.f + raise AttributeError + + +def doctest_depends_on(exe=None, modules=None, disable_viewers=None, + python_version=None, ground_types=None): + """ + Adds metadata about the dependencies which need to be met for doctesting + the docstrings of the decorated objects. + + ``exe`` should be a list of executables + + ``modules`` should be a list of modules + + ``disable_viewers`` should be a list of viewers for :func:`~sympy.printing.preview.preview` to disable + + ``python_version`` should be the minimum Python version required, as a tuple + (like ``(3, 0)``) + """ + dependencies = {} + if exe is not None: + dependencies['executables'] = exe + if modules is not None: + dependencies['modules'] = modules + if disable_viewers is not None: + dependencies['disable_viewers'] = disable_viewers + if python_version is not None: + dependencies['python_version'] = python_version + if ground_types is not None: + dependencies['ground_types'] = ground_types + + def skiptests(): + from sympy.testing.runtests import DependencyError, SymPyDocTests, PyTestReporter # lazy import + r = PyTestReporter() + t = SymPyDocTests(r, None) + try: + t._check_dependencies(**dependencies) + except DependencyError: + return True # Skip doctests + else: + return False # Run doctests + + def depends_on_deco(fn): + fn._doctest_depends_on = dependencies + fn.__doctest_skip__ = skiptests + + if inspect.isclass(fn): + fn._doctest_depdends_on = no_attrs_in_subclass( + fn, fn._doctest_depends_on) + fn.__doctest_skip__ = no_attrs_in_subclass( + fn, fn.__doctest_skip__) + return fn + + return depends_on_deco + + +def public(obj: T) -> T: + """ + Append ``obj``'s name to global ``__all__`` variable (call site). + + By using this decorator on functions or classes you achieve the same goal + as by filling ``__all__`` variables manually, you just do not have to repeat + yourself (object's name). You also know if object is public at definition + site, not at some random location (where ``__all__`` was set). + + Note that in multiple decorator setup (in almost all cases) ``@public`` + decorator must be applied before any other decorators, because it relies + on the pointer to object's global namespace. If you apply other decorators + first, ``@public`` may end up modifying the wrong namespace. + + Examples + ======== + + >>> from sympy.utilities.decorator import public + + >>> __all__ # noqa: F821 + Traceback (most recent call last): + ... + NameError: name '__all__' is not defined + + >>> @public + ... def some_function(): + ... pass + + >>> __all__ # noqa: F821 + ['some_function'] + + """ + if isinstance(obj, types.FunctionType): + ns = obj.__globals__ + name = obj.__name__ + elif isinstance(obj, (type(type), type)): + ns = sys.modules[obj.__module__].__dict__ + name = obj.__name__ + else: + raise TypeError("expected a function or a class, got %s" % obj) + + if "__all__" not in ns: + ns["__all__"] = [name] + else: + ns["__all__"].append(name) + + return obj + + +def memoize_property(propfunc): + """Property decorator that caches the value of potentially expensive + ``propfunc`` after the first evaluation. The cached value is stored in + the corresponding property name with an attached underscore.""" + attrname = '_' + propfunc.__name__ + sentinel = object() + + @wraps(propfunc) + def accessor(self): + val = getattr(self, attrname, sentinel) + if val is sentinel: + val = propfunc(self) + setattr(self, attrname, val) + return val + + return property(accessor) + + +def deprecated(message, *, deprecated_since_version, + active_deprecations_target, stacklevel=3): + ''' + Mark a function as deprecated. + + This decorator should be used if an entire function or class is + deprecated. If only a certain functionality is deprecated, you should use + :func:`~.warns_deprecated_sympy` directly. This decorator is just a + convenience. There is no functional difference between using this + decorator and calling ``warns_deprecated_sympy()`` at the top of the + function. + + The decorator takes the same arguments as + :func:`~.warns_deprecated_sympy`. See its + documentation for details on what the keywords to this decorator do. + + See the :ref:`deprecation-policy` document for details on when and how + things should be deprecated in SymPy. + + Examples + ======== + + >>> from sympy.utilities.decorator import deprecated + >>> from sympy import simplify + >>> @deprecated("""\ + ... The simplify_this(expr) function is deprecated. Use simplify(expr) + ... instead.""", deprecated_since_version="1.1", + ... active_deprecations_target='simplify-this-deprecation') + ... def simplify_this(expr): + ... """ + ... Simplify ``expr``. + ... + ... .. deprecated:: 1.1 + ... + ... The ``simplify_this`` function is deprecated. Use :func:`simplify` + ... instead. See its documentation for more information. See + ... :ref:`simplify-this-deprecation` for details. + ... + ... """ + ... return simplify(expr) + >>> from sympy.abc import x + >>> simplify_this(x*(x + 1) - x**2) # doctest: +SKIP + :1: SymPyDeprecationWarning: + + The simplify_this(expr) function is deprecated. Use simplify(expr) + instead. + + See https://docs.sympy.org/latest/explanation/active-deprecations.html#simplify-this-deprecation + for details. + + This has been deprecated since SymPy version 1.1. It + will be removed in a future version of SymPy. + + simplify_this(x) + x + + See Also + ======== + sympy.utilities.exceptions.SymPyDeprecationWarning + sympy.utilities.exceptions.sympy_deprecation_warning + sympy.utilities.exceptions.ignore_warnings + sympy.testing.pytest.warns_deprecated_sympy + + ''' + decorator_kwargs = {"deprecated_since_version": deprecated_since_version, + "active_deprecations_target": active_deprecations_target} + def deprecated_decorator(wrapped): + if hasattr(wrapped, '__mro__'): # wrapped is actually a class + class wrapper(wrapped): + __doc__ = wrapped.__doc__ + __module__ = wrapped.__module__ + _sympy_deprecated_func = wrapped + if '__new__' in wrapped.__dict__: + def __new__(cls, *args, **kwargs): + sympy_deprecation_warning(message, **decorator_kwargs, stacklevel=stacklevel) + return super().__new__(cls, *args, **kwargs) + else: + def __init__(self, *args, **kwargs): + sympy_deprecation_warning(message, **decorator_kwargs, stacklevel=stacklevel) + super().__init__(*args, **kwargs) + wrapper.__name__ = wrapped.__name__ + else: + @wraps(wrapped) + def wrapper(*args, **kwargs): + sympy_deprecation_warning(message, **decorator_kwargs, stacklevel=stacklevel) + return wrapped(*args, **kwargs) + wrapper._sympy_deprecated_func = wrapped + return wrapper + return deprecated_decorator diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/enumerative.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/enumerative.py new file mode 100644 index 0000000000000000000000000000000000000000..dcdabf064ad6291282b5905e7aed0ba9eb412d1a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/enumerative.py @@ -0,0 +1,1155 @@ +""" +Algorithms and classes to support enumerative combinatorics. + +Currently just multiset partitions, but more could be added. + +Terminology (following Knuth, algorithm 7.1.2.5M TAOCP) +*multiset* aaabbcccc has a *partition* aaabc | bccc + +The submultisets, aaabc and bccc of the partition are called +*parts*, or sometimes *vectors*. (Knuth notes that multiset +partitions can be thought of as partitions of vectors of integers, +where the ith element of the vector gives the multiplicity of +element i.) + +The values a, b and c are *components* of the multiset. These +correspond to elements of a set, but in a multiset can be present +with a multiplicity greater than 1. + +The algorithm deserves some explanation. + +Think of the part aaabc from the multiset above. If we impose an +ordering on the components of the multiset, we can represent a part +with a vector, in which the value of the first element of the vector +corresponds to the multiplicity of the first component in that +part. Thus, aaabc can be represented by the vector [3, 1, 1]. We +can also define an ordering on parts, based on the lexicographic +ordering of the vector (leftmost vector element, i.e., the element +with the smallest component number, is the most significant), so +that [3, 1, 1] > [3, 1, 0] and [3, 1, 1] > [2, 1, 4]. The ordering +on parts can be extended to an ordering on partitions: First, sort +the parts in each partition, left-to-right in decreasing order. Then +partition A is greater than partition B if A's leftmost/greatest +part is greater than B's leftmost part. If the leftmost parts are +equal, compare the second parts, and so on. + +In this ordering, the greatest partition of a given multiset has only +one part. The least partition is the one in which the components +are spread out, one per part. + +The enumeration algorithms in this file yield the partitions of the +argument multiset in decreasing order. The main data structure is a +stack of parts, corresponding to the current partition. An +important invariant is that the parts on the stack are themselves in +decreasing order. This data structure is decremented to find the +next smaller partition. Most often, decrementing the partition will +only involve adjustments to the smallest parts at the top of the +stack, much as adjacent integers *usually* differ only in their last +few digits. + +Knuth's algorithm uses two main operations on parts: + +Decrement - change the part so that it is smaller in the + (vector) lexicographic order, but reduced by the smallest amount possible. + For example, if the multiset has vector [5, + 3, 1], and the bottom/greatest part is [4, 2, 1], this part would + decrement to [4, 2, 0], while [4, 0, 0] would decrement to [3, 3, + 1]. A singleton part is never decremented -- [1, 0, 0] is not + decremented to [0, 3, 1]. Instead, the decrement operator needs + to fail for this case. In Knuth's pseudocode, the decrement + operator is step m5. + +Spread unallocated multiplicity - Once a part has been decremented, + it cannot be the rightmost part in the partition. There is some + multiplicity that has not been allocated, and new parts must be + created above it in the stack to use up this multiplicity. To + maintain the invariant that the parts on the stack are in + decreasing order, these new parts must be less than or equal to + the decremented part. + For example, if the multiset is [5, 3, 1], and its most + significant part has just been decremented to [5, 3, 0], the + spread operation will add a new part so that the stack becomes + [[5, 3, 0], [0, 0, 1]]. If the most significant part (for the + same multiset) has been decremented to [2, 0, 0] the stack becomes + [[2, 0, 0], [2, 0, 0], [1, 3, 1]]. In the pseudocode, the spread + operation for one part is step m2. The complete spread operation + is a loop of steps m2 and m3. + +In order to facilitate the spread operation, Knuth stores, for each +component of each part, not just the multiplicity of that component +in the part, but also the total multiplicity available for this +component in this part or any lesser part above it on the stack. + +One added twist is that Knuth does not represent the part vectors as +arrays. Instead, he uses a sparse representation, in which a +component of a part is represented as a component number (c), plus +the multiplicity of the component in that part (v) as well as the +total multiplicity available for that component (u). This saves +time that would be spent skipping over zeros. + +""" + +class PartComponent: + """Internal class used in support of the multiset partitions + enumerators and the associated visitor functions. + + Represents one component of one part of the current partition. + + A stack of these, plus an auxiliary frame array, f, represents a + partition of the multiset. + + Knuth's pseudocode makes c, u, and v separate arrays. + """ + + __slots__ = ('c', 'u', 'v') + + def __init__(self): + self.c = 0 # Component number + self.u = 0 # The as yet unpartitioned amount in component c + # *before* it is allocated by this triple + self.v = 0 # Amount of c component in the current part + # (v<=u). An invariant of the representation is + # that the next higher triple for this component + # (if there is one) will have a value of u-v in + # its u attribute. + + def __repr__(self): + "for debug/algorithm animation purposes" + return 'c:%d u:%d v:%d' % (self.c, self.u, self.v) + + def __eq__(self, other): + """Define value oriented equality, which is useful for testers""" + return (isinstance(other, self.__class__) and + self.c == other.c and + self.u == other.u and + self.v == other.v) + + def __ne__(self, other): + """Defined for consistency with __eq__""" + return not self == other + + +# This function tries to be a faithful implementation of algorithm +# 7.1.2.5M in Volume 4A, Combinatoral Algorithms, Part 1, of The Art +# of Computer Programming, by Donald Knuth. This includes using +# (mostly) the same variable names, etc. This makes for rather +# low-level Python. + +# Changes from Knuth's pseudocode include +# - use PartComponent struct/object instead of 3 arrays +# - make the function a generator +# - map (with some difficulty) the GOTOs to Python control structures. +# - Knuth uses 1-based numbering for components, this code is 0-based +# - renamed variable l to lpart. +# - flag variable x takes on values True/False instead of 1/0 +# +def multiset_partitions_taocp(multiplicities): + """Enumerates partitions of a multiset. + + Parameters + ========== + + multiplicities + list of integer multiplicities of the components of the multiset. + + Yields + ====== + + state + Internal data structure which encodes a particular partition. + This output is then usually processed by a visitor function + which combines the information from this data structure with + the components themselves to produce an actual partition. + + Unless they wish to create their own visitor function, users will + have little need to look inside this data structure. But, for + reference, it is a 3-element list with components: + + f + is a frame array, which is used to divide pstack into parts. + + lpart + points to the base of the topmost part. + + pstack + is an array of PartComponent objects. + + The ``state`` output offers a peek into the internal data + structures of the enumeration function. The client should + treat this as read-only; any modification of the data + structure will cause unpredictable (and almost certainly + incorrect) results. Also, the components of ``state`` are + modified in place at each iteration. Hence, the visitor must + be called at each loop iteration. Accumulating the ``state`` + instances and processing them later will not work. + + Examples + ======== + + >>> from sympy.utilities.enumerative import list_visitor + >>> from sympy.utilities.enumerative import multiset_partitions_taocp + >>> # variables components and multiplicities represent the multiset 'abb' + >>> components = 'ab' + >>> multiplicities = [1, 2] + >>> states = multiset_partitions_taocp(multiplicities) + >>> list(list_visitor(state, components) for state in states) + [[['a', 'b', 'b']], + [['a', 'b'], ['b']], + [['a'], ['b', 'b']], + [['a'], ['b'], ['b']]] + + See Also + ======== + + sympy.utilities.iterables.multiset_partitions: Takes a multiset + as input and directly yields multiset partitions. It + dispatches to a number of functions, including this one, for + implementation. Most users will find it more convenient to + use than multiset_partitions_taocp. + + """ + + # Important variables. + # m is the number of components, i.e., number of distinct elements + m = len(multiplicities) + # n is the cardinality, total number of elements whether or not distinct + n = sum(multiplicities) + + # The main data structure, f segments pstack into parts. See + # list_visitor() for example code indicating how this internal + # state corresponds to a partition. + + # Note: allocation of space for stack is conservative. Knuth's + # exercise 7.2.1.5.68 gives some indication of how to tighten this + # bound, but this is not implemented. + pstack = [PartComponent() for i in range(n * m + 1)] + f = [0] * (n + 1) + + # Step M1 in Knuth (Initialize) + # Initial state - entire multiset in one part. + for j in range(m): + ps = pstack[j] + ps.c = j + ps.u = multiplicities[j] + ps.v = multiplicities[j] + + # Other variables + f[0] = 0 + a = 0 + lpart = 0 + f[1] = m + b = m # in general, current stack frame is from a to b - 1 + + while True: + while True: + # Step M2 (Subtract v from u) + k = b + x = False + for j in range(a, b): + pstack[k].u = pstack[j].u - pstack[j].v + if pstack[k].u == 0: + x = True + elif not x: + pstack[k].c = pstack[j].c + pstack[k].v = min(pstack[j].v, pstack[k].u) + x = pstack[k].u < pstack[j].v + k = k + 1 + else: # x is True + pstack[k].c = pstack[j].c + pstack[k].v = pstack[k].u + k = k + 1 + # Note: x is True iff v has changed + + # Step M3 (Push if nonzero.) + if k > b: + a = b + b = k + lpart = lpart + 1 + f[lpart + 1] = b + # Return to M2 + else: + break # Continue to M4 + + # M4 Visit a partition + state = [f, lpart, pstack] + yield state + + # M5 (Decrease v) + while True: + j = b-1 + while (pstack[j].v == 0): + j = j - 1 + if j == a and pstack[j].v == 1: + # M6 (Backtrack) + if lpart == 0: + return + lpart = lpart - 1 + b = a + a = f[lpart] + # Return to M5 + else: + pstack[j].v = pstack[j].v - 1 + for k in range(j + 1, b): + pstack[k].v = pstack[k].u + break # GOTO M2 + +# --------------- Visitor functions for multiset partitions --------------- +# A visitor takes the partition state generated by +# multiset_partitions_taocp or other enumerator, and produces useful +# output (such as the actual partition). + + +def factoring_visitor(state, primes): + """Use with multiset_partitions_taocp to enumerate the ways a + number can be expressed as a product of factors. For this usage, + the exponents of the prime factors of a number are arguments to + the partition enumerator, while the corresponding prime factors + are input here. + + Examples + ======== + + To enumerate the factorings of a number we can think of the elements of the + partition as being the prime factors and the multiplicities as being their + exponents. + + >>> from sympy.utilities.enumerative import factoring_visitor + >>> from sympy.utilities.enumerative import multiset_partitions_taocp + >>> from sympy import factorint + >>> primes, multiplicities = zip(*factorint(24).items()) + >>> primes + (2, 3) + >>> multiplicities + (3, 1) + >>> states = multiset_partitions_taocp(multiplicities) + >>> list(factoring_visitor(state, primes) for state in states) + [[24], [8, 3], [12, 2], [4, 6], [4, 2, 3], [6, 2, 2], [2, 2, 2, 3]] + """ + f, lpart, pstack = state + factoring = [] + for i in range(lpart + 1): + factor = 1 + for ps in pstack[f[i]: f[i + 1]]: + if ps.v > 0: + factor *= primes[ps.c] ** ps.v + factoring.append(factor) + return factoring + + +def list_visitor(state, components): + """Return a list of lists to represent the partition. + + Examples + ======== + + >>> from sympy.utilities.enumerative import list_visitor + >>> from sympy.utilities.enumerative import multiset_partitions_taocp + >>> states = multiset_partitions_taocp([1, 2, 1]) + >>> s = next(states) + >>> list_visitor(s, 'abc') # for multiset 'a b b c' + [['a', 'b', 'b', 'c']] + >>> s = next(states) + >>> list_visitor(s, [1, 2, 3]) # for multiset '1 2 2 3 + [[1, 2, 2], [3]] + """ + f, lpart, pstack = state + + partition = [] + for i in range(lpart+1): + part = [] + for ps in pstack[f[i]:f[i+1]]: + if ps.v > 0: + part.extend([components[ps.c]] * ps.v) + partition.append(part) + + return partition + + +class MultisetPartitionTraverser(): + """ + Has methods to ``enumerate`` and ``count`` the partitions of a multiset. + + This implements a refactored and extended version of Knuth's algorithm + 7.1.2.5M [AOCP]_." + + The enumeration methods of this class are generators and return + data structures which can be interpreted by the same visitor + functions used for the output of ``multiset_partitions_taocp``. + + Examples + ======== + + >>> from sympy.utilities.enumerative import MultisetPartitionTraverser + >>> m = MultisetPartitionTraverser() + >>> m.count_partitions([4,4,4,2]) + 127750 + >>> m.count_partitions([3,3,3]) + 686 + + See Also + ======== + + multiset_partitions_taocp + sympy.utilities.iterables.multiset_partitions + + References + ========== + + .. [AOCP] Algorithm 7.1.2.5M in Volume 4A, Combinatoral Algorithms, + Part 1, of The Art of Computer Programming, by Donald Knuth. + + .. [Factorisatio] On a Problem of Oppenheim concerning + "Factorisatio Numerorum" E. R. Canfield, Paul Erdos, Carl + Pomerance, JOURNAL OF NUMBER THEORY, Vol. 17, No. 1. August + 1983. See section 7 for a description of an algorithm + similar to Knuth's. + + .. [Yorgey] Generating Multiset Partitions, Brent Yorgey, The + Monad.Reader, Issue 8, September 2007. + + """ + + def __init__(self): + self.debug = False + # TRACING variables. These are useful for gathering + # statistics on the algorithm itself, but have no particular + # benefit to a user of the code. + self.k1 = 0 + self.k2 = 0 + self.p1 = 0 + self.pstack = None + self.f = None + self.lpart = 0 + self.discarded = 0 + # dp_stack is list of lists of (part_key, start_count) pairs + self.dp_stack = [] + + # dp_map is map part_key-> count, where count represents the + # number of multiset which are descendants of a part with this + # key, **or any of its decrements** + + # Thus, when we find a part in the map, we add its count + # value to the running total, cut off the enumeration, and + # backtrack + + if not hasattr(self, 'dp_map'): + self.dp_map = {} + + def db_trace(self, msg): + """Useful for understanding/debugging the algorithms. Not + generally activated in end-user code.""" + if self.debug: + # XXX: animation_visitor is undefined... Clearly this does not + # work and was not tested. Previous code in comments below. + raise RuntimeError + #letters = 'abcdefghijklmnopqrstuvwxyz' + #state = [self.f, self.lpart, self.pstack] + #print("DBG:", msg, + # ["".join(part) for part in list_visitor(state, letters)], + # animation_visitor(state)) + + # + # Helper methods for enumeration + # + def _initialize_enumeration(self, multiplicities): + """Allocates and initializes the partition stack. + + This is called from the enumeration/counting routines, so + there is no need to call it separately.""" + + num_components = len(multiplicities) + # cardinality is the total number of elements, whether or not distinct + cardinality = sum(multiplicities) + + # pstack is the partition stack, which is segmented by + # f into parts. + self.pstack = [PartComponent() for i in + range(num_components * cardinality + 1)] + self.f = [0] * (cardinality + 1) + + # Initial state - entire multiset in one part. + for j in range(num_components): + ps = self.pstack[j] + ps.c = j + ps.u = multiplicities[j] + ps.v = multiplicities[j] + + self.f[0] = 0 + self.f[1] = num_components + self.lpart = 0 + + # The decrement_part() method corresponds to step M5 in Knuth's + # algorithm. This is the base version for enum_all(). Modified + # versions of this method are needed if we want to restrict + # sizes of the partitions produced. + def decrement_part(self, part): + """Decrements part (a subrange of pstack), if possible, returning + True iff the part was successfully decremented. + + If you think of the v values in the part as a multi-digit + integer (least significant digit on the right) this is + basically decrementing that integer, but with the extra + constraint that the leftmost digit cannot be decremented to 0. + + Parameters + ========== + + part + The part, represented as a list of PartComponent objects, + which is to be decremented. + + """ + plen = len(part) + for j in range(plen - 1, -1, -1): + if j == 0 and part[j].v > 1 or j > 0 and part[j].v > 0: + # found val to decrement + part[j].v -= 1 + # Reset trailing parts back to maximum + for k in range(j + 1, plen): + part[k].v = part[k].u + return True + return False + + # Version to allow number of parts to be bounded from above. + # Corresponds to (a modified) step M5. + def decrement_part_small(self, part, ub): + """Decrements part (a subrange of pstack), if possible, returning + True iff the part was successfully decremented. + + Parameters + ========== + + part + part to be decremented (topmost part on the stack) + + ub + the maximum number of parts allowed in a partition + returned by the calling traversal. + + Notes + ===== + + The goal of this modification of the ordinary decrement method + is to fail (meaning that the subtree rooted at this part is to + be skipped) when it can be proved that this part can only have + child partitions which are larger than allowed by ``ub``. If a + decision is made to fail, it must be accurate, otherwise the + enumeration will miss some partitions. But, it is OK not to + capture all the possible failures -- if a part is passed that + should not be, the resulting too-large partitions are filtered + by the enumeration one level up. However, as is usual in + constrained enumerations, failing early is advantageous. + + The tests used by this method catch the most common cases, + although this implementation is by no means the last word on + this problem. The tests include: + + 1) ``lpart`` must be less than ``ub`` by at least 2. This is because + once a part has been decremented, the partition + will gain at least one child in the spread step. + + 2) If the leading component of the part is about to be + decremented, check for how many parts will be added in + order to use up the unallocated multiplicity in that + leading component, and fail if this number is greater than + allowed by ``ub``. (See code for the exact expression.) This + test is given in the answer to Knuth's problem 7.2.1.5.69. + + 3) If there is *exactly* enough room to expand the leading + component by the above test, check the next component (if + it exists) once decrementing has finished. If this has + ``v == 0``, this next component will push the expansion over the + limit by 1, so fail. + """ + if self.lpart >= ub - 1: + self.p1 += 1 # increment to keep track of usefulness of tests + return False + plen = len(part) + for j in range(plen - 1, -1, -1): + # Knuth's mod, (answer to problem 7.2.1.5.69) + if j == 0 and (part[0].v - 1)*(ub - self.lpart) < part[0].u: + self.k1 += 1 + return False + + if j == 0 and part[j].v > 1 or j > 0 and part[j].v > 0: + # found val to decrement + part[j].v -= 1 + # Reset trailing parts back to maximum + for k in range(j + 1, plen): + part[k].v = part[k].u + + # Have now decremented part, but are we doomed to + # failure when it is expanded? Check one oddball case + # that turns out to be surprisingly common - exactly + # enough room to expand the leading component, but no + # room for the second component, which has v=0. + if (plen > 1 and part[1].v == 0 and + (part[0].u - part[0].v) == + ((ub - self.lpart - 1) * part[0].v)): + self.k2 += 1 + self.db_trace("Decrement fails test 3") + return False + return True + return False + + def decrement_part_large(self, part, amt, lb): + """Decrements part, while respecting size constraint. + + A part can have no children which are of sufficient size (as + indicated by ``lb``) unless that part has sufficient + unallocated multiplicity. When enforcing the size constraint, + this method will decrement the part (if necessary) by an + amount needed to ensure sufficient unallocated multiplicity. + + Returns True iff the part was successfully decremented. + + Parameters + ========== + + part + part to be decremented (topmost part on the stack) + + amt + Can only take values 0 or 1. A value of 1 means that the + part must be decremented, and then the size constraint is + enforced. A value of 0 means just to enforce the ``lb`` + size constraint. + + lb + The partitions produced by the calling enumeration must + have more parts than this value. + + """ + + if amt == 1: + # In this case we always need to decrement, *before* + # enforcing the "sufficient unallocated multiplicity" + # constraint. Easiest for this is just to call the + # regular decrement method. + if not self.decrement_part(part): + return False + + # Next, perform any needed additional decrementing to respect + # "sufficient unallocated multiplicity" (or fail if this is + # not possible). + min_unalloc = lb - self.lpart + if min_unalloc <= 0: + return True + total_mult = sum(pc.u for pc in part) + total_alloc = sum(pc.v for pc in part) + if total_mult <= min_unalloc: + return False + + deficit = min_unalloc - (total_mult - total_alloc) + if deficit <= 0: + return True + + for i in range(len(part) - 1, -1, -1): + if i == 0: + if part[0].v > deficit: + part[0].v -= deficit + return True + else: + return False # This shouldn't happen, due to above check + else: + if part[i].v >= deficit: + part[i].v -= deficit + return True + else: + deficit -= part[i].v + part[i].v = 0 + + def decrement_part_range(self, part, lb, ub): + """Decrements part (a subrange of pstack), if possible, returning + True iff the part was successfully decremented. + + Parameters + ========== + + part + part to be decremented (topmost part on the stack) + + ub + the maximum number of parts allowed in a partition + returned by the calling traversal. + + lb + The partitions produced by the calling enumeration must + have more parts than this value. + + Notes + ===== + + Combines the constraints of _small and _large decrement + methods. If returns success, part has been decremented at + least once, but perhaps by quite a bit more if needed to meet + the lb constraint. + """ + + # Constraint in the range case is just enforcing both the + # constraints from _small and _large cases. Note the 0 as the + # second argument to the _large call -- this is the signal to + # decrement only as needed to for constraint enforcement. The + # short circuiting and left-to-right order of the 'and' + # operator is important for this to work correctly. + return self.decrement_part_small(part, ub) and \ + self.decrement_part_large(part, 0, lb) + + def spread_part_multiplicity(self): + """Returns True if a new part has been created, and + adjusts pstack, f and lpart as needed. + + Notes + ===== + + Spreads unallocated multiplicity from the current top part + into a new part created above the current on the stack. This + new part is constrained to be less than or equal to the old in + terms of the part ordering. + + This call does nothing (and returns False) if the current top + part has no unallocated multiplicity. + + """ + j = self.f[self.lpart] # base of current top part + k = self.f[self.lpart + 1] # ub of current; potential base of next + base = k # save for later comparison + + changed = False # Set to true when the new part (so far) is + # strictly less than (as opposed to less than + # or equal) to the old. + for j in range(self.f[self.lpart], self.f[self.lpart + 1]): + self.pstack[k].u = self.pstack[j].u - self.pstack[j].v + if self.pstack[k].u == 0: + changed = True + else: + self.pstack[k].c = self.pstack[j].c + if changed: # Put all available multiplicity in this part + self.pstack[k].v = self.pstack[k].u + else: # Still maintaining ordering constraint + if self.pstack[k].u < self.pstack[j].v: + self.pstack[k].v = self.pstack[k].u + changed = True + else: + self.pstack[k].v = self.pstack[j].v + k = k + 1 + if k > base: + # Adjust for the new part on stack + self.lpart = self.lpart + 1 + self.f[self.lpart + 1] = k + return True + return False + + def top_part(self): + """Return current top part on the stack, as a slice of pstack. + + """ + return self.pstack[self.f[self.lpart]:self.f[self.lpart + 1]] + + # Same interface and functionality as multiset_partitions_taocp(), + # but some might find this refactored version easier to follow. + def enum_all(self, multiplicities): + """Enumerate the partitions of a multiset. + + Examples + ======== + + >>> from sympy.utilities.enumerative import list_visitor + >>> from sympy.utilities.enumerative import MultisetPartitionTraverser + >>> m = MultisetPartitionTraverser() + >>> states = m.enum_all([2,2]) + >>> list(list_visitor(state, 'ab') for state in states) + [[['a', 'a', 'b', 'b']], + [['a', 'a', 'b'], ['b']], + [['a', 'a'], ['b', 'b']], + [['a', 'a'], ['b'], ['b']], + [['a', 'b', 'b'], ['a']], + [['a', 'b'], ['a', 'b']], + [['a', 'b'], ['a'], ['b']], + [['a'], ['a'], ['b', 'b']], + [['a'], ['a'], ['b'], ['b']]] + + See Also + ======== + + multiset_partitions_taocp: + which provides the same result as this method, but is + about twice as fast. Hence, enum_all is primarily useful + for testing. Also see the function for a discussion of + states and visitors. + + """ + self._initialize_enumeration(multiplicities) + while True: + while self.spread_part_multiplicity(): + pass + + # M4 Visit a partition + state = [self.f, self.lpart, self.pstack] + yield state + + # M5 (Decrease v) + while not self.decrement_part(self.top_part()): + # M6 (Backtrack) + if self.lpart == 0: + return + self.lpart -= 1 + + def enum_small(self, multiplicities, ub): + """Enumerate multiset partitions with no more than ``ub`` parts. + + Equivalent to enum_range(multiplicities, 0, ub) + + Parameters + ========== + + multiplicities + list of multiplicities of the components of the multiset. + + ub + Maximum number of parts + + Examples + ======== + + >>> from sympy.utilities.enumerative import list_visitor + >>> from sympy.utilities.enumerative import MultisetPartitionTraverser + >>> m = MultisetPartitionTraverser() + >>> states = m.enum_small([2,2], 2) + >>> list(list_visitor(state, 'ab') for state in states) + [[['a', 'a', 'b', 'b']], + [['a', 'a', 'b'], ['b']], + [['a', 'a'], ['b', 'b']], + [['a', 'b', 'b'], ['a']], + [['a', 'b'], ['a', 'b']]] + + The implementation is based, in part, on the answer given to + exercise 69, in Knuth [AOCP]_. + + See Also + ======== + + enum_all, enum_large, enum_range + + """ + + # Keep track of iterations which do not yield a partition. + # Clearly, we would like to keep this number small. + self.discarded = 0 + if ub <= 0: + return + self._initialize_enumeration(multiplicities) + while True: + while self.spread_part_multiplicity(): + self.db_trace('spread 1') + if self.lpart >= ub: + self.discarded += 1 + self.db_trace(' Discarding') + self.lpart = ub - 2 + break + else: + # M4 Visit a partition + state = [self.f, self.lpart, self.pstack] + yield state + + # M5 (Decrease v) + while not self.decrement_part_small(self.top_part(), ub): + self.db_trace("Failed decrement, going to backtrack") + # M6 (Backtrack) + if self.lpart == 0: + return + self.lpart -= 1 + self.db_trace("Backtracked to") + self.db_trace("decrement ok, about to expand") + + def enum_large(self, multiplicities, lb): + """Enumerate the partitions of a multiset with lb < num(parts) + + Equivalent to enum_range(multiplicities, lb, sum(multiplicities)) + + Parameters + ========== + + multiplicities + list of multiplicities of the components of the multiset. + + lb + Number of parts in the partition must be greater than + this lower bound. + + + Examples + ======== + + >>> from sympy.utilities.enumerative import list_visitor + >>> from sympy.utilities.enumerative import MultisetPartitionTraverser + >>> m = MultisetPartitionTraverser() + >>> states = m.enum_large([2,2], 2) + >>> list(list_visitor(state, 'ab') for state in states) + [[['a', 'a'], ['b'], ['b']], + [['a', 'b'], ['a'], ['b']], + [['a'], ['a'], ['b', 'b']], + [['a'], ['a'], ['b'], ['b']]] + + See Also + ======== + + enum_all, enum_small, enum_range + + """ + self.discarded = 0 + if lb >= sum(multiplicities): + return + self._initialize_enumeration(multiplicities) + self.decrement_part_large(self.top_part(), 0, lb) + while True: + good_partition = True + while self.spread_part_multiplicity(): + if not self.decrement_part_large(self.top_part(), 0, lb): + # Failure here should be rare/impossible + self.discarded += 1 + good_partition = False + break + + # M4 Visit a partition + if good_partition: + state = [self.f, self.lpart, self.pstack] + yield state + + # M5 (Decrease v) + while not self.decrement_part_large(self.top_part(), 1, lb): + # M6 (Backtrack) + if self.lpart == 0: + return + self.lpart -= 1 + + def enum_range(self, multiplicities, lb, ub): + + """Enumerate the partitions of a multiset with + ``lb < num(parts) <= ub``. + + In particular, if partitions with exactly ``k`` parts are + desired, call with ``(multiplicities, k - 1, k)``. This + method generalizes enum_all, enum_small, and enum_large. + + Examples + ======== + + >>> from sympy.utilities.enumerative import list_visitor + >>> from sympy.utilities.enumerative import MultisetPartitionTraverser + >>> m = MultisetPartitionTraverser() + >>> states = m.enum_range([2,2], 1, 2) + >>> list(list_visitor(state, 'ab') for state in states) + [[['a', 'a', 'b'], ['b']], + [['a', 'a'], ['b', 'b']], + [['a', 'b', 'b'], ['a']], + [['a', 'b'], ['a', 'b']]] + + """ + # combine the constraints of the _large and _small + # enumerations. + self.discarded = 0 + if ub <= 0 or lb >= sum(multiplicities): + return + self._initialize_enumeration(multiplicities) + self.decrement_part_large(self.top_part(), 0, lb) + while True: + good_partition = True + while self.spread_part_multiplicity(): + self.db_trace("spread 1") + if not self.decrement_part_large(self.top_part(), 0, lb): + # Failure here - possible in range case? + self.db_trace(" Discarding (large cons)") + self.discarded += 1 + good_partition = False + break + elif self.lpart >= ub: + self.discarded += 1 + good_partition = False + self.db_trace(" Discarding small cons") + self.lpart = ub - 2 + break + + # M4 Visit a partition + if good_partition: + state = [self.f, self.lpart, self.pstack] + yield state + + # M5 (Decrease v) + while not self.decrement_part_range(self.top_part(), lb, ub): + self.db_trace("Failed decrement, going to backtrack") + # M6 (Backtrack) + if self.lpart == 0: + return + self.lpart -= 1 + self.db_trace("Backtracked to") + self.db_trace("decrement ok, about to expand") + + def count_partitions_slow(self, multiplicities): + """Returns the number of partitions of a multiset whose elements + have the multiplicities given in ``multiplicities``. + + Primarily for comparison purposes. It follows the same path as + enumerate, and counts, rather than generates, the partitions. + + See Also + ======== + + count_partitions + Has the same calling interface, but is much faster. + + """ + # number of partitions so far in the enumeration + self.pcount = 0 + self._initialize_enumeration(multiplicities) + while True: + while self.spread_part_multiplicity(): + pass + + # M4 Visit (count) a partition + self.pcount += 1 + + # M5 (Decrease v) + while not self.decrement_part(self.top_part()): + # M6 (Backtrack) + if self.lpart == 0: + return self.pcount + self.lpart -= 1 + + def count_partitions(self, multiplicities): + """Returns the number of partitions of a multiset whose components + have the multiplicities given in ``multiplicities``. + + For larger counts, this method is much faster than calling one + of the enumerators and counting the result. Uses dynamic + programming to cut down on the number of nodes actually + explored. The dictionary used in order to accelerate the + counting process is stored in the ``MultisetPartitionTraverser`` + object and persists across calls. If the user does not + expect to call ``count_partitions`` for any additional + multisets, the object should be cleared to save memory. On + the other hand, the cache built up from one count run can + significantly speed up subsequent calls to ``count_partitions``, + so it may be advantageous not to clear the object. + + Examples + ======== + + >>> from sympy.utilities.enumerative import MultisetPartitionTraverser + >>> m = MultisetPartitionTraverser() + >>> m.count_partitions([9,8,2]) + 288716 + >>> m.count_partitions([2,2]) + 9 + >>> del m + + Notes + ===== + + If one looks at the workings of Knuth's algorithm M [AOCP]_, it + can be viewed as a traversal of a binary tree of parts. A + part has (up to) two children, the left child resulting from + the spread operation, and the right child from the decrement + operation. The ordinary enumeration of multiset partitions is + an in-order traversal of this tree, and with the partitions + corresponding to paths from the root to the leaves. The + mapping from paths to partitions is a little complicated, + since the partition would contain only those parts which are + leaves or the parents of a spread link, not those which are + parents of a decrement link. + + For counting purposes, it is sufficient to count leaves, and + this can be done with a recursive in-order traversal. The + number of leaves of a subtree rooted at a particular part is a + function only of that part itself, so memoizing has the + potential to speed up the counting dramatically. + + This method follows a computational approach which is similar + to the hypothetical memoized recursive function, but with two + differences: + + 1) This method is iterative, borrowing its structure from the + other enumerations and maintaining an explicit stack of + parts which are in the process of being counted. (There + may be multisets which can be counted reasonably quickly by + this implementation, but which would overflow the default + Python recursion limit with a recursive implementation.) + + 2) Instead of using the part data structure directly, a more + compact key is constructed. This saves space, but more + importantly coalesces some parts which would remain + separate with physical keys. + + Unlike the enumeration functions, there is currently no _range + version of count_partitions. If someone wants to stretch + their brain, it should be possible to construct one by + memoizing with a histogram of counts rather than a single + count, and combining the histograms. + """ + # number of partitions so far in the enumeration + self.pcount = 0 + + # dp_stack is list of lists of (part_key, start_count) pairs + self.dp_stack = [] + + self._initialize_enumeration(multiplicities) + pkey = part_key(self.top_part()) + self.dp_stack.append([(pkey, 0), ]) + while True: + while self.spread_part_multiplicity(): + pkey = part_key(self.top_part()) + if pkey in self.dp_map: + # Already have a cached value for the count of the + # subtree rooted at this part. Add it to the + # running counter, and break out of the spread + # loop. The -1 below is to compensate for the + # leaf that this code path would otherwise find, + # and which gets incremented for below. + + self.pcount += (self.dp_map[pkey] - 1) + self.lpart -= 1 + break + else: + self.dp_stack.append([(pkey, self.pcount), ]) + + # M4 count a leaf partition + self.pcount += 1 + + # M5 (Decrease v) + while not self.decrement_part(self.top_part()): + # M6 (Backtrack) + for key, oldcount in self.dp_stack.pop(): + self.dp_map[key] = self.pcount - oldcount + if self.lpart == 0: + return self.pcount + self.lpart -= 1 + + # At this point have successfully decremented the part on + # the stack and it does not appear in the cache. It needs + # to be added to the list at the top of dp_stack + pkey = part_key(self.top_part()) + self.dp_stack[-1].append((pkey, self.pcount),) + + +def part_key(part): + """Helper for MultisetPartitionTraverser.count_partitions that + creates a key for ``part``, that only includes information which can + affect the count for that part. (Any irrelevant information just + reduces the effectiveness of dynamic programming.) + + Notes + ===== + + This member function is a candidate for future exploration. There + are likely symmetries that can be exploited to coalesce some + ``part_key`` values, and thereby save space and improve + performance. + + """ + # The component number is irrelevant for counting partitions, so + # leave it out of the memo key. + rval = [] + for ps in part: + rval.append(ps.u) + rval.append(ps.v) + return tuple(rval) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/exceptions.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..f764a31371ed109d69102777bd9ec0dfb3d75380 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/exceptions.py @@ -0,0 +1,271 @@ +""" +General SymPy exceptions and warnings. +""" + +import warnings +import contextlib + +from textwrap import dedent + + +class SymPyDeprecationWarning(DeprecationWarning): + r""" + A warning for deprecated features of SymPy. + + See the :ref:`deprecation-policy` document for details on when and how + things should be deprecated in SymPy. + + Note that simply constructing this class will not cause a warning to be + issued. To do that, you must call the :func`sympy_deprecation_warning` + function. For this reason, it is not recommended to ever construct this + class directly. + + Explanation + =========== + + The ``SymPyDeprecationWarning`` class is a subclass of + ``DeprecationWarning`` that is used for all deprecations in SymPy. A + special subclass is used so that we can automatically augment the warning + message with additional metadata about the version the deprecation was + introduced in and a link to the documentation. This also allows users to + explicitly filter deprecation warnings from SymPy using ``warnings`` + filters (see :ref:`silencing-sympy-deprecation-warnings`). + + Additionally, ``SymPyDeprecationWarning`` is enabled to be shown by + default, unlike normal ``DeprecationWarning``\s, which are only shown by + default in interactive sessions. This ensures that deprecation warnings in + SymPy will actually be seen by users. + + See the documentation of :func:`sympy_deprecation_warning` for a + description of the parameters to this function. + + To mark a function as deprecated, you can use the :func:`@deprecated + ` decorator. + + See Also + ======== + sympy.utilities.exceptions.sympy_deprecation_warning + sympy.utilities.exceptions.ignore_warnings + sympy.utilities.decorator.deprecated + sympy.testing.pytest.warns_deprecated_sympy + + """ + def __init__(self, message, *, deprecated_since_version, active_deprecations_target): + + super().__init__(message, deprecated_since_version, + active_deprecations_target) + self.message = message + if not isinstance(deprecated_since_version, str): + raise TypeError(f"'deprecated_since_version' should be a string, got {deprecated_since_version!r}") + self.deprecated_since_version = deprecated_since_version + self.active_deprecations_target = active_deprecations_target + if any(i in active_deprecations_target for i in '()='): + raise ValueError("active_deprecations_target be the part inside of the '(...)='") + + self.full_message = f""" + +{dedent(message).strip()} + +See https://docs.sympy.org/latest/explanation/active-deprecations.html#{active_deprecations_target} +for details. + +This has been deprecated since SymPy version {deprecated_since_version}. It +will be removed in a future version of SymPy. +""" + + def __str__(self): + return self.full_message + + def __repr__(self): + return f"{self.__class__.__name__}({self.message!r}, deprecated_since_version={self.deprecated_since_version!r}, active_deprecations_target={self.active_deprecations_target!r})" + + def __eq__(self, other): + return isinstance(other, SymPyDeprecationWarning) and self.args == other.args + + # Make pickling work. The by default, it tries to recreate the expression + # from its args, but this doesn't work because of our keyword-only + # arguments. + @classmethod + def _new(cls, message, deprecated_since_version, + active_deprecations_target): + return cls(message, deprecated_since_version=deprecated_since_version, active_deprecations_target=active_deprecations_target) + + def __reduce__(self): + return (self._new, (self.message, self.deprecated_since_version, self.active_deprecations_target)) + +# Python by default hides DeprecationWarnings, which we do not want. +warnings.simplefilter("once", SymPyDeprecationWarning) + +def sympy_deprecation_warning(message, *, deprecated_since_version, + active_deprecations_target, stacklevel=3): + r''' + Warn that a feature is deprecated in SymPy. + + See the :ref:`deprecation-policy` document for details on when and how + things should be deprecated in SymPy. + + To mark an entire function or class as deprecated, you can use the + :func:`@deprecated ` decorator. + + Parameters + ========== + + message : str + The deprecation message. This may span multiple lines and contain + code examples. Messages should be wrapped to 80 characters. The + message is automatically dedented and leading and trailing whitespace + stripped. Messages may include dynamic content based on the user + input, but avoid using ``str(expression)`` if an expression can be + arbitrary, as it might be huge and make the warning message + unreadable. + + deprecated_since_version : str + The version of SymPy the feature has been deprecated since. For new + deprecations, this should be the version in `sympy/release.py + `_ + without the ``.dev``. If the next SymPy version ends up being + different from this, the release manager will need to update any + ``SymPyDeprecationWarning``\s using the incorrect version. This + argument is required and must be passed as a keyword argument. + (example: ``deprecated_since_version="1.10"``). + + active_deprecations_target : str + The Sphinx target corresponding to the section for the deprecation in + the :ref:`active-deprecations` document (see + ``doc/src/explanation/active-deprecations.md``). This is used to + automatically generate a URL to the page in the warning message. This + argument is required and must be passed as a keyword argument. + (example: ``active_deprecations_target="deprecated-feature-abc"``) + + stacklevel : int, default: 3 + The ``stacklevel`` parameter that is passed to ``warnings.warn``. If + you create a wrapper that calls this function, this should be + increased so that the warning message shows the user line of code that + produced the warning. Note that in some cases there will be multiple + possible different user code paths that could result in the warning. + In that case, just choose the smallest common stacklevel. + + Examples + ======== + + >>> from sympy.utilities.exceptions import sympy_deprecation_warning + >>> def is_this_zero(x, y=0): + ... """ + ... Determine if x = 0. + ... + ... Parameters + ... ========== + ... + ... x : Expr + ... The expression to check. + ... + ... y : Expr, optional + ... If provided, check if x = y. + ... + ... .. deprecated:: 1.1 + ... + ... The ``y`` argument to ``is_this_zero`` is deprecated. Use + ... ``is_this_zero(x - y)`` instead. + ... + ... """ + ... from sympy import simplify + ... + ... if y != 0: + ... sympy_deprecation_warning(""" + ... The y argument to is_zero() is deprecated. Use is_zero(x - y) instead.""", + ... deprecated_since_version="1.1", + ... active_deprecations_target='is-this-zero-y-deprecation') + ... return simplify(x - y) == 0 + >>> is_this_zero(0) + True + >>> is_this_zero(1, 1) # doctest: +SKIP + :1: SymPyDeprecationWarning: + + The y argument to is_zero() is deprecated. Use is_zero(x - y) instead. + + See https://docs.sympy.org/latest/explanation/active-deprecations.html#is-this-zero-y-deprecation + for details. + + This has been deprecated since SymPy version 1.1. It + will be removed in a future version of SymPy. + + is_this_zero(1, 1) + True + + See Also + ======== + + sympy.utilities.exceptions.SymPyDeprecationWarning + sympy.utilities.exceptions.ignore_warnings + sympy.utilities.decorator.deprecated + sympy.testing.pytest.warns_deprecated_sympy + + ''' + w = SymPyDeprecationWarning(message, + deprecated_since_version=deprecated_since_version, + active_deprecations_target=active_deprecations_target) + warnings.warn(w, stacklevel=stacklevel) + + +@contextlib.contextmanager +def ignore_warnings(warningcls): + ''' + Context manager to suppress warnings during tests. + + .. note:: + + Do not use this with SymPyDeprecationWarning in the tests. + warns_deprecated_sympy() should be used instead. + + This function is useful for suppressing warnings during tests. The warns + function should be used to assert that a warning is raised. The + ignore_warnings function is useful in situation when the warning is not + guaranteed to be raised (e.g. on importing a module) or if the warning + comes from third-party code. + + This function is also useful to prevent the same or similar warnings from + being issue twice due to recursive calls. + + When the warning is coming (reliably) from SymPy the warns function should + be preferred to ignore_warnings. + + >>> from sympy.utilities.exceptions import ignore_warnings + >>> import warnings + + Here's a warning: + + >>> with warnings.catch_warnings(): # reset warnings in doctest + ... warnings.simplefilter('error') + ... warnings.warn('deprecated', UserWarning) + Traceback (most recent call last): + ... + UserWarning: deprecated + + Let's suppress it with ignore_warnings: + + >>> with warnings.catch_warnings(): # reset warnings in doctest + ... warnings.simplefilter('error') + ... with ignore_warnings(UserWarning): + ... warnings.warn('deprecated', UserWarning) + + (No warning emitted) + + See Also + ======== + sympy.utilities.exceptions.SymPyDeprecationWarning + sympy.utilities.exceptions.sympy_deprecation_warning + sympy.utilities.decorator.deprecated + sympy.testing.pytest.warns_deprecated_sympy + + ''' + # Absorbs all warnings in warnrec + with warnings.catch_warnings(record=True) as warnrec: + # Make sure our warning doesn't get filtered + warnings.simplefilter("always", warningcls) + # Now run the test + yield + + # Reissue any warnings that we aren't testing for + for w in warnrec: + if not issubclass(w.category, warningcls): + warnings.warn_explicit(w.message, w.category, w.filename, w.lineno) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/iterables.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/iterables.py new file mode 100644 index 0000000000000000000000000000000000000000..cc5c1152f03b10f62f9dd04c842b6fc74b0b9242 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/iterables.py @@ -0,0 +1,3179 @@ +from collections import Counter, defaultdict, OrderedDict +from itertools import ( + chain, combinations, combinations_with_replacement, cycle, islice, + permutations, product, groupby +) +# For backwards compatibility +from itertools import product as cartes # noqa: F401 +from operator import gt + + + +# this is the logical location of these functions +from sympy.utilities.enumerative import ( + multiset_partitions_taocp, list_visitor, MultisetPartitionTraverser) + +from sympy.utilities.misc import as_int +from sympy.utilities.decorator import deprecated + + +def is_palindromic(s, i=0, j=None): + """ + Return True if the sequence is the same from left to right as it + is from right to left in the whole sequence (default) or in the + Python slice ``s[i: j]``; else False. + + Examples + ======== + + >>> from sympy.utilities.iterables import is_palindromic + >>> is_palindromic([1, 0, 1]) + True + >>> is_palindromic('abcbb') + False + >>> is_palindromic('abcbb', 1) + False + + Normal Python slicing is performed in place so there is no need to + create a slice of the sequence for testing: + + >>> is_palindromic('abcbb', 1, -1) + True + >>> is_palindromic('abcbb', -4, -1) + True + + See Also + ======== + + sympy.ntheory.digits.is_palindromic: tests integers + + """ + i, j, _ = slice(i, j).indices(len(s)) + m = (j - i)//2 + # if length is odd, middle element will be ignored + return all(s[i + k] == s[j - 1 - k] for k in range(m)) + + +def flatten(iterable, levels=None, cls=None): # noqa: F811 + """ + Recursively denest iterable containers. + + >>> from sympy import flatten + + >>> flatten([1, 2, 3]) + [1, 2, 3] + >>> flatten([1, 2, [3]]) + [1, 2, 3] + >>> flatten([1, [2, 3], [4, 5]]) + [1, 2, 3, 4, 5] + >>> flatten([1.0, 2, (1, None)]) + [1.0, 2, 1, None] + + If you want to denest only a specified number of levels of + nested containers, then set ``levels`` flag to the desired + number of levels:: + + >>> ls = [[(-2, -1), (1, 2)], [(0, 0)]] + + >>> flatten(ls, levels=1) + [(-2, -1), (1, 2), (0, 0)] + + If cls argument is specified, it will only flatten instances of that + class, for example: + + >>> from sympy import Basic, S + >>> class MyOp(Basic): + ... pass + ... + >>> flatten([MyOp(S(1), MyOp(S(2), S(3)))], cls=MyOp) + [1, 2, 3] + + adapted from https://kogs-www.informatik.uni-hamburg.de/~meine/python_tricks + """ + from sympy.tensor.array import NDimArray + if levels is not None: + if not levels: + return iterable + elif levels > 0: + levels -= 1 + else: + raise ValueError( + "expected non-negative number of levels, got %s" % levels) + + if cls is None: + def reducible(x): + return is_sequence(x, set) + else: + def reducible(x): + return isinstance(x, cls) + + result = [] + + for el in iterable: + if reducible(el): + if hasattr(el, 'args') and not isinstance(el, NDimArray): + el = el.args + result.extend(flatten(el, levels=levels, cls=cls)) + else: + result.append(el) + + return result + + +def unflatten(iter, n=2): + """Group ``iter`` into tuples of length ``n``. Raise an error if + the length of ``iter`` is not a multiple of ``n``. + """ + if n < 1 or len(iter) % n: + raise ValueError('iter length is not a multiple of %i' % n) + return list(zip(*(iter[i::n] for i in range(n)))) + + +def reshape(seq, how): + """Reshape the sequence according to the template in ``how``. + + Examples + ======== + + >>> from sympy.utilities import reshape + >>> seq = list(range(1, 9)) + + >>> reshape(seq, [4]) # lists of 4 + [[1, 2, 3, 4], [5, 6, 7, 8]] + + >>> reshape(seq, (4,)) # tuples of 4 + [(1, 2, 3, 4), (5, 6, 7, 8)] + + >>> reshape(seq, (2, 2)) # tuples of 4 + [(1, 2, 3, 4), (5, 6, 7, 8)] + + >>> reshape(seq, (2, [2])) # (i, i, [i, i]) + [(1, 2, [3, 4]), (5, 6, [7, 8])] + + >>> reshape(seq, ((2,), [2])) # etc.... + [((1, 2), [3, 4]), ((5, 6), [7, 8])] + + >>> reshape(seq, (1, [2], 1)) + [(1, [2, 3], 4), (5, [6, 7], 8)] + + >>> reshape(tuple(seq), ([[1], 1, (2,)],)) + (([[1], 2, (3, 4)],), ([[5], 6, (7, 8)],)) + + >>> reshape(tuple(seq), ([1], 1, (2,))) + (([1], 2, (3, 4)), ([5], 6, (7, 8))) + + >>> reshape(list(range(12)), [2, [3], {2}, (1, (3,), 1)]) + [[0, 1, [2, 3, 4], {5, 6}, (7, (8, 9, 10), 11)]] + + """ + m = sum(flatten(how)) + n, rem = divmod(len(seq), m) + if m < 0 or rem: + raise ValueError('template must sum to positive number ' + 'that divides the length of the sequence') + i = 0 + container = type(how) + rv = [None]*n + for k in range(len(rv)): + _rv = [] + for hi in how: + if isinstance(hi, int): + _rv.extend(seq[i: i + hi]) + i += hi + else: + n = sum(flatten(hi)) + hi_type = type(hi) + _rv.append(hi_type(reshape(seq[i: i + n], hi)[0])) + i += n + rv[k] = container(_rv) + return type(seq)(rv) + + +def group(seq, multiple=True): + """ + Splits a sequence into a list of lists of equal, adjacent elements. + + Examples + ======== + + >>> from sympy import group + + >>> group([1, 1, 1, 2, 2, 3]) + [[1, 1, 1], [2, 2], [3]] + >>> group([1, 1, 1, 2, 2, 3], multiple=False) + [(1, 3), (2, 2), (3, 1)] + >>> group([1, 1, 3, 2, 2, 1], multiple=False) + [(1, 2), (3, 1), (2, 2), (1, 1)] + + See Also + ======== + + multiset + + """ + if multiple: + return [(list(g)) for _, g in groupby(seq)] + return [(k, len(list(g))) for k, g in groupby(seq)] + + +def _iproduct2(iterable1, iterable2): + '''Cartesian product of two possibly infinite iterables''' + + it1 = iter(iterable1) + it2 = iter(iterable2) + + elems1 = [] + elems2 = [] + + sentinel = object() + def append(it, elems): + e = next(it, sentinel) + if e is not sentinel: + elems.append(e) + + n = 0 + append(it1, elems1) + append(it2, elems2) + + while n <= len(elems1) + len(elems2): + for m in range(n-len(elems1)+1, len(elems2)): + yield (elems1[n-m], elems2[m]) + n += 1 + append(it1, elems1) + append(it2, elems2) + + +def iproduct(*iterables): + ''' + Cartesian product of iterables. + + Generator of the Cartesian product of iterables. This is analogous to + itertools.product except that it works with infinite iterables and will + yield any item from the infinite product eventually. + + Examples + ======== + + >>> from sympy.utilities.iterables import iproduct + >>> sorted(iproduct([1,2], [3,4])) + [(1, 3), (1, 4), (2, 3), (2, 4)] + + With an infinite iterator: + + >>> from sympy import S + >>> (3,) in iproduct(S.Integers) + True + >>> (3, 4) in iproduct(S.Integers, S.Integers) + True + + .. seealso:: + + `itertools.product + `_ + ''' + if len(iterables) == 0: + yield () + return + elif len(iterables) == 1: + for e in iterables[0]: + yield (e,) + elif len(iterables) == 2: + yield from _iproduct2(*iterables) + else: + first, others = iterables[0], iterables[1:] + for ef, eo in _iproduct2(first, iproduct(*others)): + yield (ef,) + eo + + +def multiset(seq): + """Return the hashable sequence in multiset form with values being the + multiplicity of the item in the sequence. + + Examples + ======== + + >>> from sympy.utilities.iterables import multiset + >>> multiset('mississippi') + {'i': 4, 'm': 1, 'p': 2, 's': 4} + + See Also + ======== + + group + + """ + return dict(Counter(seq).items()) + + + + +def ibin(n, bits=None, str=False): + """Return a list of length ``bits`` corresponding to the binary value + of ``n`` with small bits to the right (last). If bits is omitted, the + length will be the number required to represent ``n``. If the bits are + desired in reversed order, use the ``[::-1]`` slice of the returned list. + + If a sequence of all bits-length lists starting from ``[0, 0,..., 0]`` + through ``[1, 1, ..., 1]`` are desired, pass a non-integer for bits, e.g. + ``'all'``. + + If the bit *string* is desired pass ``str=True``. + + Examples + ======== + + >>> from sympy.utilities.iterables import ibin + >>> ibin(2) + [1, 0] + >>> ibin(2, 4) + [0, 0, 1, 0] + + If all lists corresponding to 0 to 2**n - 1, pass a non-integer + for bits: + + >>> bits = 2 + >>> for i in ibin(2, 'all'): + ... print(i) + (0, 0) + (0, 1) + (1, 0) + (1, 1) + + If a bit string is desired of a given length, use str=True: + + >>> n = 123 + >>> bits = 10 + >>> ibin(n, bits, str=True) + '0001111011' + >>> ibin(n, bits, str=True)[::-1] # small bits left + '1101111000' + >>> list(ibin(3, 'all', str=True)) + ['000', '001', '010', '011', '100', '101', '110', '111'] + + """ + if n < 0: + raise ValueError("negative numbers are not allowed") + n = as_int(n) + + if bits is None: + bits = 0 + else: + try: + bits = as_int(bits) + except ValueError: + bits = -1 + else: + if n.bit_length() > bits: + raise ValueError( + "`bits` must be >= {}".format(n.bit_length())) + + if not str: + if bits >= 0: + return [1 if i == "1" else 0 for i in bin(n)[2:].rjust(bits, "0")] + else: + return variations(range(2), n, repetition=True) + else: + if bits >= 0: + return bin(n)[2:].rjust(bits, "0") + else: + return (bin(i)[2:].rjust(n, "0") for i in range(2**n)) + + +def variations(seq, n, repetition=False): + r"""Returns an iterator over the n-sized variations of ``seq`` (size N). + ``repetition`` controls whether items in ``seq`` can appear more than once; + + Examples + ======== + + ``variations(seq, n)`` will return `\frac{N!}{(N - n)!}` permutations without + repetition of ``seq``'s elements: + + >>> from sympy import variations + >>> list(variations([1, 2], 2)) + [(1, 2), (2, 1)] + + ``variations(seq, n, True)`` will return the `N^n` permutations obtained + by allowing repetition of elements: + + >>> list(variations([1, 2], 2, repetition=True)) + [(1, 1), (1, 2), (2, 1), (2, 2)] + + If you ask for more items than are in the set you get the empty set unless + you allow repetitions: + + >>> list(variations([0, 1], 3, repetition=False)) + [] + >>> list(variations([0, 1], 3, repetition=True))[:4] + [(0, 0, 0), (0, 0, 1), (0, 1, 0), (0, 1, 1)] + + .. seealso:: + + `itertools.permutations + `_, + `itertools.product + `_ + """ + if not repetition: + seq = tuple(seq) + if len(seq) < n: + return iter(()) # 0 length iterator + return permutations(seq, n) + else: + if n == 0: + return iter(((),)) # yields 1 empty tuple + else: + return product(seq, repeat=n) + + +def subsets(seq, k=None, repetition=False): + r"""Generates all `k`-subsets (combinations) from an `n`-element set, ``seq``. + + A `k`-subset of an `n`-element set is any subset of length exactly `k`. The + number of `k`-subsets of an `n`-element set is given by ``binomial(n, k)``, + whereas there are `2^n` subsets all together. If `k` is ``None`` then all + `2^n` subsets will be returned from shortest to longest. + + Examples + ======== + + >>> from sympy import subsets + + ``subsets(seq, k)`` will return the + `\frac{n!}{k!(n - k)!}` `k`-subsets (combinations) + without repetition, i.e. once an item has been removed, it can no + longer be "taken": + + >>> list(subsets([1, 2], 2)) + [(1, 2)] + >>> list(subsets([1, 2])) + [(), (1,), (2,), (1, 2)] + >>> list(subsets([1, 2, 3], 2)) + [(1, 2), (1, 3), (2, 3)] + + + ``subsets(seq, k, repetition=True)`` will return the + `\frac{(n - 1 + k)!}{k!(n - 1)!}` + combinations *with* repetition: + + >>> list(subsets([1, 2], 2, repetition=True)) + [(1, 1), (1, 2), (2, 2)] + + If you ask for more items than are in the set you get the empty set unless + you allow repetitions: + + >>> list(subsets([0, 1], 3, repetition=False)) + [] + >>> list(subsets([0, 1], 3, repetition=True)) + [(0, 0, 0), (0, 0, 1), (0, 1, 1), (1, 1, 1)] + + """ + if k is None: + if not repetition: + return chain.from_iterable((combinations(seq, k) + for k in range(len(seq) + 1))) + else: + return chain.from_iterable((combinations_with_replacement(seq, k) + for k in range(len(seq) + 1))) + else: + if not repetition: + return combinations(seq, k) + else: + return combinations_with_replacement(seq, k) + + +def filter_symbols(iterator, exclude): + """ + Only yield elements from `iterator` that do not occur in `exclude`. + + Parameters + ========== + + iterator : iterable + iterator to take elements from + + exclude : iterable + elements to exclude + + Returns + ======= + + iterator : iterator + filtered iterator + """ + exclude = set(exclude) + for s in iterator: + if s not in exclude: + yield s + +def numbered_symbols(prefix='x', cls=None, start=0, exclude=(), *args, **assumptions): + """ + Generate an infinite stream of Symbols consisting of a prefix and + increasing subscripts provided that they do not occur in ``exclude``. + + Parameters + ========== + + prefix : str, optional + The prefix to use. By default, this function will generate symbols of + the form "x0", "x1", etc. + + cls : class, optional + The class to use. By default, it uses ``Symbol``, but you can also use ``Wild`` + or ``Dummy``. + + start : int, optional + The start number. By default, it is 0. + + exclude : list, tuple, set of cls, optional + Symbols to be excluded. + + *args, **kwargs + Additional positional and keyword arguments are passed to the *cls* class. + + Returns + ======= + + sym : Symbol + The subscripted symbols. + """ + exclude = set(exclude or []) + if cls is None: + # We can't just make the default cls=Symbol because it isn't + # imported yet. + from sympy.core import Symbol + cls = Symbol + + while True: + name = '%s%s' % (prefix, start) + s = cls(name, *args, **assumptions) + if s not in exclude: + yield s + start += 1 + + +def capture(func): + """Return the printed output of func(). + + ``func`` should be a function without arguments that produces output with + print statements. + + >>> from sympy.utilities.iterables import capture + >>> from sympy import pprint + >>> from sympy.abc import x + >>> def foo(): + ... print('hello world!') + ... + >>> 'hello' in capture(foo) # foo, not foo() + True + >>> capture(lambda: pprint(2/x)) + '2\\n-\\nx\\n' + + """ + from io import StringIO + import sys + + stdout = sys.stdout + sys.stdout = file = StringIO() + try: + func() + finally: + sys.stdout = stdout + return file.getvalue() + + +def sift(seq, keyfunc, binary=False): + """ + Sift the sequence, ``seq`` according to ``keyfunc``. + + Returns + ======= + + When ``binary`` is ``False`` (default), the output is a dictionary + where elements of ``seq`` are stored in a list keyed to the value + of keyfunc for that element. If ``binary`` is True then a tuple + with lists ``T`` and ``F`` are returned where ``T`` is a list + containing elements of seq for which ``keyfunc`` was ``True`` and + ``F`` containing those elements for which ``keyfunc`` was ``False``; + a ValueError is raised if the ``keyfunc`` is not binary. + + Examples + ======== + + >>> from sympy.utilities import sift + >>> from sympy.abc import x, y + >>> from sympy import sqrt, exp, pi, Tuple + + >>> sift(range(5), lambda x: x % 2) + {0: [0, 2, 4], 1: [1, 3]} + + sift() returns a defaultdict() object, so any key that has no matches will + give []. + + >>> sift([x], lambda x: x.is_commutative) + {True: [x]} + >>> _[False] + [] + + Sometimes you will not know how many keys you will get: + + >>> sift([sqrt(x), exp(x), (y**x)**2], + ... lambda x: x.as_base_exp()[0]) + {E: [exp(x)], x: [sqrt(x)], y: [y**(2*x)]} + + Sometimes you expect the results to be binary; the + results can be unpacked by setting ``binary`` to True: + + >>> sift(range(4), lambda x: x % 2, binary=True) + ([1, 3], [0, 2]) + >>> sift(Tuple(1, pi), lambda x: x.is_rational, binary=True) + ([1], [pi]) + + A ValueError is raised if the predicate was not actually binary + (which is a good test for the logic where sifting is used and + binary results were expected): + + >>> unknown = exp(1) - pi # the rationality of this is unknown + >>> args = Tuple(1, pi, unknown) + >>> sift(args, lambda x: x.is_rational, binary=True) + Traceback (most recent call last): + ... + ValueError: keyfunc gave non-binary output + + The non-binary sifting shows that there were 3 keys generated: + + >>> set(sift(args, lambda x: x.is_rational).keys()) + {None, False, True} + + If you need to sort the sifted items it might be better to use + ``ordered`` which can economically apply multiple sort keys + to a sequence while sorting. + + See Also + ======== + + ordered + + """ + if not binary: + m = defaultdict(list) + for i in seq: + m[keyfunc(i)].append(i) + return m + sift = F, T = [], [] + for i in seq: + try: + sift[keyfunc(i)].append(i) + except (IndexError, TypeError): + raise ValueError('keyfunc gave non-binary output') + return T, F + + +def take(iter, n): + """Return ``n`` items from ``iter`` iterator. """ + return [ value for _, value in zip(range(n), iter) ] + + +def dict_merge(*dicts): + """Merge dictionaries into a single dictionary. """ + merged = {} + + for dict in dicts: + merged.update(dict) + + return merged + + +def common_prefix(*seqs): + """Return the subsequence that is a common start of sequences in ``seqs``. + + >>> from sympy.utilities.iterables import common_prefix + >>> common_prefix(list(range(3))) + [0, 1, 2] + >>> common_prefix(list(range(3)), list(range(4))) + [0, 1, 2] + >>> common_prefix([1, 2, 3], [1, 2, 5]) + [1, 2] + >>> common_prefix([1, 2, 3], [1, 3, 5]) + [1] + """ + if not all(seqs): + return [] + elif len(seqs) == 1: + return seqs[0] + i = 0 + for i in range(min(len(s) for s in seqs)): + if not all(seqs[j][i] == seqs[0][i] for j in range(len(seqs))): + break + else: + i += 1 + return seqs[0][:i] + + +def common_suffix(*seqs): + """Return the subsequence that is a common ending of sequences in ``seqs``. + + >>> from sympy.utilities.iterables import common_suffix + >>> common_suffix(list(range(3))) + [0, 1, 2] + >>> common_suffix(list(range(3)), list(range(4))) + [] + >>> common_suffix([1, 2, 3], [9, 2, 3]) + [2, 3] + >>> common_suffix([1, 2, 3], [9, 7, 3]) + [3] + """ + + if not all(seqs): + return [] + elif len(seqs) == 1: + return seqs[0] + i = 0 + for i in range(-1, -min(len(s) for s in seqs) - 1, -1): + if not all(seqs[j][i] == seqs[0][i] for j in range(len(seqs))): + break + else: + i -= 1 + if i == -1: + return [] + else: + return seqs[0][i + 1:] + + +def prefixes(seq): + """ + Generate all prefixes of a sequence. + + Examples + ======== + + >>> from sympy.utilities.iterables import prefixes + + >>> list(prefixes([1,2,3,4])) + [[1], [1, 2], [1, 2, 3], [1, 2, 3, 4]] + + """ + n = len(seq) + + for i in range(n): + yield seq[:i + 1] + + +def postfixes(seq): + """ + Generate all postfixes of a sequence. + + Examples + ======== + + >>> from sympy.utilities.iterables import postfixes + + >>> list(postfixes([1,2,3,4])) + [[4], [3, 4], [2, 3, 4], [1, 2, 3, 4]] + + """ + n = len(seq) + + for i in range(n): + yield seq[n - i - 1:] + + +def topological_sort(graph, key=None): + r""" + Topological sort of graph's vertices. + + Parameters + ========== + + graph : tuple[list, list[tuple[T, T]] + A tuple consisting of a list of vertices and a list of edges of + a graph to be sorted topologically. + + key : callable[T] (optional) + Ordering key for vertices on the same level. By default the natural + (e.g. lexicographic) ordering is used (in this case the base type + must implement ordering relations). + + Examples + ======== + + Consider a graph:: + + +---+ +---+ +---+ + | 7 |\ | 5 | | 3 | + +---+ \ +---+ +---+ + | _\___/ ____ _/ | + | / \___/ \ / | + V V V V | + +----+ +---+ | + | 11 | | 8 | | + +----+ +---+ | + | | \____ ___/ _ | + | \ \ / / \ | + V \ V V / V V + +---+ \ +---+ | +----+ + | 2 | | | 9 | | | 10 | + +---+ | +---+ | +----+ + \________/ + + where vertices are integers. This graph can be encoded using + elementary Python's data structures as follows:: + + >>> V = [2, 3, 5, 7, 8, 9, 10, 11] + >>> E = [(7, 11), (7, 8), (5, 11), (3, 8), (3, 10), + ... (11, 2), (11, 9), (11, 10), (8, 9)] + + To compute a topological sort for graph ``(V, E)`` issue:: + + >>> from sympy.utilities.iterables import topological_sort + + >>> topological_sort((V, E)) + [3, 5, 7, 8, 11, 2, 9, 10] + + If specific tie breaking approach is needed, use ``key`` parameter:: + + >>> topological_sort((V, E), key=lambda v: -v) + [7, 5, 11, 3, 10, 8, 9, 2] + + Only acyclic graphs can be sorted. If the input graph has a cycle, + then ``ValueError`` will be raised:: + + >>> topological_sort((V, E + [(10, 7)])) + Traceback (most recent call last): + ... + ValueError: cycle detected + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Topological_sorting + + """ + V, E = graph + + L = [] + S = set(V) + E = list(E) + + S.difference_update(u for v, u in E) + + if key is None: + def key(value): + return value + + S = sorted(S, key=key, reverse=True) + + while S: + node = S.pop() + L.append(node) + + for u, v in list(E): + if u == node: + E.remove((u, v)) + + for _u, _v in E: + if v == _v: + break + else: + kv = key(v) + + for i, s in enumerate(S): + ks = key(s) + + if kv > ks: + S.insert(i, v) + break + else: + S.append(v) + + if E: + raise ValueError("cycle detected") + else: + return L + + +def strongly_connected_components(G): + r""" + Strongly connected components of a directed graph in reverse topological + order. + + + Parameters + ========== + + G : tuple[list, list[tuple[T, T]] + A tuple consisting of a list of vertices and a list of edges of + a graph whose strongly connected components are to be found. + + + Examples + ======== + + Consider a directed graph (in dot notation):: + + digraph { + A -> B + A -> C + B -> C + C -> B + B -> D + } + + .. graphviz:: + + digraph { + A -> B + A -> C + B -> C + C -> B + B -> D + } + + where vertices are the letters A, B, C and D. This graph can be encoded + using Python's elementary data structures as follows:: + + >>> V = ['A', 'B', 'C', 'D'] + >>> E = [('A', 'B'), ('A', 'C'), ('B', 'C'), ('C', 'B'), ('B', 'D')] + + The strongly connected components of this graph can be computed as + + >>> from sympy.utilities.iterables import strongly_connected_components + + >>> strongly_connected_components((V, E)) + [['D'], ['B', 'C'], ['A']] + + This also gives the components in reverse topological order. + + Since the subgraph containing B and C has a cycle they must be together in + a strongly connected component. A and D are connected to the rest of the + graph but not in a cyclic manner so they appear as their own strongly + connected components. + + + Notes + ===== + + The vertices of the graph must be hashable for the data structures used. + If the vertices are unhashable replace them with integer indices. + + This function uses Tarjan's algorithm to compute the strongly connected + components in `O(|V|+|E|)` (linear) time. + + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Strongly_connected_component + .. [2] https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm + + + See Also + ======== + + sympy.utilities.iterables.connected_components + + """ + # Map from a vertex to its neighbours + V, E = G + Gmap = {vi: [] for vi in V} + for v1, v2 in E: + Gmap[v1].append(v2) + return _strongly_connected_components(V, Gmap) + + +def _strongly_connected_components(V, Gmap): + """More efficient internal routine for strongly_connected_components""" + # + # Here V is an iterable of vertices and Gmap is a dict mapping each vertex + # to a list of neighbours e.g.: + # + # V = [0, 1, 2, 3] + # Gmap = {0: [2, 3], 1: [0]} + # + # For a large graph these data structures can often be created more + # efficiently then those expected by strongly_connected_components() which + # in this case would be + # + # V = [0, 1, 2, 3] + # Gmap = [(0, 2), (0, 3), (1, 0)] + # + # XXX: Maybe this should be the recommended function to use instead... + # + + # Non-recursive Tarjan's algorithm: + lowlink = {} + indices = {} + stack = OrderedDict() + callstack = [] + components = [] + nomore = object() + + def start(v): + index = len(stack) + indices[v] = lowlink[v] = index + stack[v] = None + callstack.append((v, iter(Gmap[v]))) + + def finish(v1): + # Finished a component? + if lowlink[v1] == indices[v1]: + component = [stack.popitem()[0]] + while component[-1] is not v1: + component.append(stack.popitem()[0]) + components.append(component[::-1]) + v2, _ = callstack.pop() + if callstack: + v1, _ = callstack[-1] + lowlink[v1] = min(lowlink[v1], lowlink[v2]) + + for v in V: + if v in indices: + continue + start(v) + while callstack: + v1, it1 = callstack[-1] + v2 = next(it1, nomore) + # Finished children of v1? + if v2 is nomore: + finish(v1) + # Recurse on v2 + elif v2 not in indices: + start(v2) + elif v2 in stack: + lowlink[v1] = min(lowlink[v1], indices[v2]) + + # Reverse topological sort order: + return components + + +def connected_components(G): + r""" + Connected components of an undirected graph or weakly connected components + of a directed graph. + + + Parameters + ========== + + G : tuple[list, list[tuple[T, T]] + A tuple consisting of a list of vertices and a list of edges of + a graph whose connected components are to be found. + + + Examples + ======== + + + Given an undirected graph:: + + graph { + A -- B + C -- D + } + + .. graphviz:: + + graph { + A -- B + C -- D + } + + We can find the connected components using this function if we include + each edge in both directions:: + + >>> from sympy.utilities.iterables import connected_components + + >>> V = ['A', 'B', 'C', 'D'] + >>> E = [('A', 'B'), ('B', 'A'), ('C', 'D'), ('D', 'C')] + >>> connected_components((V, E)) + [['A', 'B'], ['C', 'D']] + + The weakly connected components of a directed graph can found the same + way. + + + Notes + ===== + + The vertices of the graph must be hashable for the data structures used. + If the vertices are unhashable replace them with integer indices. + + This function uses Tarjan's algorithm to compute the connected components + in `O(|V|+|E|)` (linear) time. + + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Component_%28graph_theory%29 + .. [2] https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm + + + See Also + ======== + + sympy.utilities.iterables.strongly_connected_components + + """ + # Duplicate edges both ways so that the graph is effectively undirected + # and return the strongly connected components: + V, E = G + E_undirected = [] + for v1, v2 in E: + E_undirected.extend([(v1, v2), (v2, v1)]) + return strongly_connected_components((V, E_undirected)) + + +def rotate_left(x, y): + """ + Left rotates a list x by the number of steps specified + in y. + + Examples + ======== + + >>> from sympy.utilities.iterables import rotate_left + >>> a = [0, 1, 2] + >>> rotate_left(a, 1) + [1, 2, 0] + """ + if len(x) == 0: + return [] + y = y % len(x) + return x[y:] + x[:y] + + +def rotate_right(x, y): + """ + Right rotates a list x by the number of steps specified + in y. + + Examples + ======== + + >>> from sympy.utilities.iterables import rotate_right + >>> a = [0, 1, 2] + >>> rotate_right(a, 1) + [2, 0, 1] + """ + if len(x) == 0: + return [] + y = len(x) - y % len(x) + return x[y:] + x[:y] + + +def least_rotation(x, key=None): + ''' + Returns the number of steps of left rotation required to + obtain lexicographically minimal string/list/tuple, etc. + + Examples + ======== + + >>> from sympy.utilities.iterables import least_rotation, rotate_left + >>> a = [3, 1, 5, 1, 2] + >>> least_rotation(a) + 3 + >>> rotate_left(a, _) + [1, 2, 3, 1, 5] + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Lexicographically_minimal_string_rotation + + ''' + from sympy.functions.elementary.miscellaneous import Id + if key is None: key = Id + S = x + x # Concatenate string to it self to avoid modular arithmetic + f = [-1] * len(S) # Failure function + k = 0 # Least rotation of string found so far + for j in range(1,len(S)): + sj = S[j] + i = f[j-k-1] + while i != -1 and sj != S[k+i+1]: + if key(sj) < key(S[k+i+1]): + k = j-i-1 + i = f[i] + if sj != S[k+i+1]: + if key(sj) < key(S[k]): + k = j + f[j-k] = -1 + else: + f[j-k] = i+1 + return k + + +def multiset_combinations(m, n, g=None): + """ + Return the unique combinations of size ``n`` from multiset ``m``. + + Examples + ======== + + >>> from sympy.utilities.iterables import multiset_combinations + >>> from itertools import combinations + >>> [''.join(i) for i in multiset_combinations('baby', 3)] + ['abb', 'aby', 'bby'] + + >>> def count(f, s): return len(list(f(s, 3))) + + The number of combinations depends on the number of letters; the + number of unique combinations depends on how the letters are + repeated. + + >>> s1 = 'abracadabra' + >>> s2 = 'banana tree' + >>> count(combinations, s1), count(multiset_combinations, s1) + (165, 23) + >>> count(combinations, s2), count(multiset_combinations, s2) + (165, 54) + + """ + from sympy.core.sorting import ordered + if g is None: + if isinstance(m, dict): + if any(as_int(v) < 0 for v in m.values()): + raise ValueError('counts cannot be negative') + N = sum(m.values()) + if n > N: + return + g = [[k, m[k]] for k in ordered(m)] + else: + m = list(m) + N = len(m) + if n > N: + return + try: + m = multiset(m) + g = [(k, m[k]) for k in ordered(m)] + except TypeError: + m = list(ordered(m)) + g = [list(i) for i in group(m, multiple=False)] + del m + else: + # not checking counts since g is intended for internal use + N = sum(v for k, v in g) + if n > N or not n: + yield [] + else: + for i, (k, v) in enumerate(g): + if v >= n: + yield [k]*n + v = n - 1 + for v in range(min(n, v), 0, -1): + for j in multiset_combinations(None, n - v, g[i + 1:]): + rv = [k]*v + j + if len(rv) == n: + yield rv + +def multiset_permutations(m, size=None, g=None): + """ + Return the unique permutations of multiset ``m``. + + Examples + ======== + + >>> from sympy.utilities.iterables import multiset_permutations + >>> from sympy import factorial + >>> [''.join(i) for i in multiset_permutations('aab')] + ['aab', 'aba', 'baa'] + >>> factorial(len('banana')) + 720 + >>> len(list(multiset_permutations('banana'))) + 60 + """ + from sympy.core.sorting import ordered + if g is None: + if isinstance(m, dict): + if any(as_int(v) < 0 for v in m.values()): + raise ValueError('counts cannot be negative') + g = [[k, m[k]] for k in ordered(m)] + else: + m = list(ordered(m)) + g = [list(i) for i in group(m, multiple=False)] + del m + do = [gi for gi in g if gi[1] > 0] + SUM = sum(gi[1] for gi in do) + if not do or size is not None and (size > SUM or size < 1): + if not do and size is None or size == 0: + yield [] + return + elif size == 1: + for k, v in do: + yield [k] + elif len(do) == 1: + k, v = do[0] + v = v if size is None else (size if size <= v else 0) + yield [k for i in range(v)] + elif all(v == 1 for k, v in do): + for p in permutations([k for k, v in do], size): + yield list(p) + else: + size = size if size is not None else SUM + for i, (k, v) in enumerate(do): + do[i][1] -= 1 + for j in multiset_permutations(None, size - 1, do): + if j: + yield [k] + j + do[i][1] += 1 + + +def _partition(seq, vector, m=None): + """ + Return the partition of seq as specified by the partition vector. + + Examples + ======== + + >>> from sympy.utilities.iterables import _partition + >>> _partition('abcde', [1, 0, 1, 2, 0]) + [['b', 'e'], ['a', 'c'], ['d']] + + Specifying the number of bins in the partition is optional: + + >>> _partition('abcde', [1, 0, 1, 2, 0], 3) + [['b', 'e'], ['a', 'c'], ['d']] + + The output of _set_partitions can be passed as follows: + + >>> output = (3, [1, 0, 1, 2, 0]) + >>> _partition('abcde', *output) + [['b', 'e'], ['a', 'c'], ['d']] + + See Also + ======== + + combinatorics.partitions.Partition.from_rgs + + """ + if m is None: + m = max(vector) + 1 + elif isinstance(vector, int): # entered as m, vector + vector, m = m, vector + p = [[] for i in range(m)] + for i, v in enumerate(vector): + p[v].append(seq[i]) + return p + + +def _set_partitions(n): + """Cycle through all partitions of n elements, yielding the + current number of partitions, ``m``, and a mutable list, ``q`` + such that ``element[i]`` is in part ``q[i]`` of the partition. + + NOTE: ``q`` is modified in place and generally should not be changed + between function calls. + + Examples + ======== + + >>> from sympy.utilities.iterables import _set_partitions, _partition + >>> for m, q in _set_partitions(3): + ... print('%s %s %s' % (m, q, _partition('abc', q, m))) + 1 [0, 0, 0] [['a', 'b', 'c']] + 2 [0, 0, 1] [['a', 'b'], ['c']] + 2 [0, 1, 0] [['a', 'c'], ['b']] + 2 [0, 1, 1] [['a'], ['b', 'c']] + 3 [0, 1, 2] [['a'], ['b'], ['c']] + + Notes + ===== + + This algorithm is similar to, and solves the same problem as, + Algorithm 7.2.1.5H, from volume 4A of Knuth's The Art of Computer + Programming. Knuth uses the term "restricted growth string" where + this code refers to a "partition vector". In each case, the meaning is + the same: the value in the ith element of the vector specifies to + which part the ith set element is to be assigned. + + At the lowest level, this code implements an n-digit big-endian + counter (stored in the array q) which is incremented (with carries) to + get the next partition in the sequence. A special twist is that a + digit is constrained to be at most one greater than the maximum of all + the digits to the left of it. The array p maintains this maximum, so + that the code can efficiently decide when a digit can be incremented + in place or whether it needs to be reset to 0 and trigger a carry to + the next digit. The enumeration starts with all the digits 0 (which + corresponds to all the set elements being assigned to the same 0th + part), and ends with 0123...n, which corresponds to each set element + being assigned to a different, singleton, part. + + This routine was rewritten to use 0-based lists while trying to + preserve the beauty and efficiency of the original algorithm. + + References + ========== + + .. [1] Nijenhuis, Albert and Wilf, Herbert. (1978) Combinatorial Algorithms, + 2nd Ed, p 91, algorithm "nexequ". Available online from + https://www.math.upenn.edu/~wilf/website/CombAlgDownld.html (viewed + November 17, 2012). + + """ + p = [0]*n + q = [0]*n + nc = 1 + yield nc, q + while nc != n: + m = n + while 1: + m -= 1 + i = q[m] + if p[i] != 1: + break + q[m] = 0 + i += 1 + q[m] = i + m += 1 + nc += m - n + p[0] += n - m + if i == nc: + p[nc] = 0 + nc += 1 + p[i - 1] -= 1 + p[i] += 1 + yield nc, q + + +def multiset_partitions(multiset, m=None): + """ + Return unique partitions of the given multiset (in list form). + If ``m`` is None, all multisets will be returned, otherwise only + partitions with ``m`` parts will be returned. + + If ``multiset`` is an integer, a range [0, 1, ..., multiset - 1] + will be supplied. + + Examples + ======== + + >>> from sympy.utilities.iterables import multiset_partitions + >>> list(multiset_partitions([1, 2, 3, 4], 2)) + [[[1, 2, 3], [4]], [[1, 2, 4], [3]], [[1, 2], [3, 4]], + [[1, 3, 4], [2]], [[1, 3], [2, 4]], [[1, 4], [2, 3]], + [[1], [2, 3, 4]]] + >>> list(multiset_partitions([1, 2, 3, 4], 1)) + [[[1, 2, 3, 4]]] + + Only unique partitions are returned and these will be returned in a + canonical order regardless of the order of the input: + + >>> a = [1, 2, 2, 1] + >>> ans = list(multiset_partitions(a, 2)) + >>> a.sort() + >>> list(multiset_partitions(a, 2)) == ans + True + >>> a = range(3, 1, -1) + >>> (list(multiset_partitions(a)) == + ... list(multiset_partitions(sorted(a)))) + True + + If m is omitted then all partitions will be returned: + + >>> list(multiset_partitions([1, 1, 2])) + [[[1, 1, 2]], [[1, 1], [2]], [[1, 2], [1]], [[1], [1], [2]]] + >>> list(multiset_partitions([1]*3)) + [[[1, 1, 1]], [[1], [1, 1]], [[1], [1], [1]]] + + Counting + ======== + + The number of partitions of a set is given by the bell number: + + >>> from sympy import bell + >>> len(list(multiset_partitions(5))) == bell(5) == 52 + True + + The number of partitions of length k from a set of size n is given by the + Stirling Number of the 2nd kind: + + >>> from sympy.functions.combinatorial.numbers import stirling + >>> stirling(5, 2) == len(list(multiset_partitions(5, 2))) == 15 + True + + These comments on counting apply to *sets*, not multisets. + + Notes + ===== + + When all the elements are the same in the multiset, the order + of the returned partitions is determined by the ``partitions`` + routine. If one is counting partitions then it is better to use + the ``nT`` function. + + See Also + ======== + + partitions + sympy.combinatorics.partitions.Partition + sympy.combinatorics.partitions.IntegerPartition + sympy.functions.combinatorial.numbers.nT + + """ + # This function looks at the supplied input and dispatches to + # several special-case routines as they apply. + if isinstance(multiset, int): + n = multiset + if m and m > n: + return + multiset = list(range(n)) + if m == 1: + yield [multiset[:]] + return + + # If m is not None, it can sometimes be faster to use + # MultisetPartitionTraverser.enum_range() even for inputs + # which are sets. Since the _set_partitions code is quite + # fast, this is only advantageous when the overall set + # partitions outnumber those with the desired number of parts + # by a large factor. (At least 60.) Such a switch is not + # currently implemented. + for nc, q in _set_partitions(n): + if m is None or nc == m: + rv = [[] for i in range(nc)] + for i in range(n): + rv[q[i]].append(multiset[i]) + yield rv + return + + if len(multiset) == 1 and isinstance(multiset, str): + multiset = [multiset] + + if not has_variety(multiset): + # Only one component, repeated n times. The resulting + # partitions correspond to partitions of integer n. + n = len(multiset) + if m and m > n: + return + if m == 1: + yield [multiset[:]] + return + x = multiset[:1] + for size, p in partitions(n, m, size=True): + if m is None or size == m: + rv = [] + for k in sorted(p): + rv.extend([x*k]*p[k]) + yield rv + else: + from sympy.core.sorting import ordered + multiset = list(ordered(multiset)) + n = len(multiset) + if m and m > n: + return + if m == 1: + yield [multiset[:]] + return + + # Split the information of the multiset into two lists - + # one of the elements themselves, and one (of the same length) + # giving the number of repeats for the corresponding element. + elements, multiplicities = zip(*group(multiset, False)) + + if len(elements) < len(multiset): + # General case - multiset with more than one distinct element + # and at least one element repeated more than once. + if m: + mpt = MultisetPartitionTraverser() + for state in mpt.enum_range(multiplicities, m-1, m): + yield list_visitor(state, elements) + else: + for state in multiset_partitions_taocp(multiplicities): + yield list_visitor(state, elements) + else: + # Set partitions case - no repeated elements. Pretty much + # same as int argument case above, with same possible, but + # currently unimplemented optimization for some cases when + # m is not None + for nc, q in _set_partitions(n): + if m is None or nc == m: + rv = [[] for i in range(nc)] + for i in range(n): + rv[q[i]].append(i) + yield [[multiset[j] for j in i] for i in rv] + + +def partitions(n, m=None, k=None, size=False): + """Generate all partitions of positive integer, n. + + Each partition is represented as a dictionary, mapping an integer + to the number of copies of that integer in the partition. For example, + the first partition of 4 returned is {4: 1}, "4: one of them". + + Parameters + ========== + n : int + m : int, optional + limits number of parts in partition (mnemonic: m, maximum parts) + k : int, optional + limits the numbers that are kept in the partition (mnemonic: k, keys) + size : bool, default: False + If ``True``, (M, P) is returned where M is the sum of the + multiplicities and P is the generated partition. + If ``False``, only the generated partition is returned. + + Examples + ======== + + >>> from sympy.utilities.iterables import partitions + + The numbers appearing in the partition (the key of the returned dict) + are limited with k: + + >>> for p in partitions(6, k=2): # doctest: +SKIP + ... print(p) + {2: 3} + {1: 2, 2: 2} + {1: 4, 2: 1} + {1: 6} + + The maximum number of parts in the partition (the sum of the values in + the returned dict) are limited with m (default value, None, gives + partitions from 1 through n): + + >>> for p in partitions(6, m=2): # doctest: +SKIP + ... print(p) + ... + {6: 1} + {1: 1, 5: 1} + {2: 1, 4: 1} + {3: 2} + + References + ========== + + .. [1] modified from Tim Peter's version to allow for k and m values: + https://code.activestate.com/recipes/218332-generator-for-integer-partitions/ + + See Also + ======== + + sympy.combinatorics.partitions.Partition + sympy.combinatorics.partitions.IntegerPartition + + """ + if (n <= 0 or + m is not None and m < 1 or + k is not None and k < 1 or + m and k and m*k < n): + # the empty set is the only way to handle these inputs + # and returning {} to represent it is consistent with + # the counting convention, e.g. nT(0) == 1. + if size: + yield 0, {} + else: + yield {} + return + + if m is None: + m = n + else: + m = min(m, n) + k = min(k or n, n) + + n, m, k = as_int(n), as_int(m), as_int(k) + q, r = divmod(n, k) + ms = {k: q} + keys = [k] # ms.keys(), from largest to smallest + if r: + ms[r] = 1 + keys.append(r) + room = m - q - bool(r) + if size: + yield sum(ms.values()), ms.copy() + else: + yield ms.copy() + + while keys != [1]: + # Reuse any 1's. + if keys[-1] == 1: + del keys[-1] + reuse = ms.pop(1) + room += reuse + else: + reuse = 0 + + while 1: + # Let i be the smallest key larger than 1. Reuse one + # instance of i. + i = keys[-1] + newcount = ms[i] = ms[i] - 1 + reuse += i + if newcount == 0: + del keys[-1], ms[i] + room += 1 + + # Break the remainder into pieces of size i-1. + i -= 1 + q, r = divmod(reuse, i) + need = q + bool(r) + if need > room: + if not keys: + return + continue + + ms[i] = q + keys.append(i) + if r: + ms[r] = 1 + keys.append(r) + break + room -= need + if size: + yield sum(ms.values()), ms.copy() + else: + yield ms.copy() + + +def ordered_partitions(n, m=None, sort=True): + """Generates ordered partitions of integer *n*. + + Parameters + ========== + n : int + m : int, optional + The default value gives partitions of all sizes else only + those with size m. In addition, if *m* is not None then + partitions are generated *in place* (see examples). + sort : bool, default: True + Controls whether partitions are + returned in sorted order when *m* is not None; when False, + the partitions are returned as fast as possible with elements + sorted, but when m|n the partitions will not be in + ascending lexicographical order. + + Examples + ======== + + >>> from sympy.utilities.iterables import ordered_partitions + + All partitions of 5 in ascending lexicographical: + + >>> for p in ordered_partitions(5): + ... print(p) + [1, 1, 1, 1, 1] + [1, 1, 1, 2] + [1, 1, 3] + [1, 2, 2] + [1, 4] + [2, 3] + [5] + + Only partitions of 5 with two parts: + + >>> for p in ordered_partitions(5, 2): + ... print(p) + [1, 4] + [2, 3] + + When ``m`` is given, a given list objects will be used more than + once for speed reasons so you will not see the correct partitions + unless you make a copy of each as it is generated: + + >>> [p for p in ordered_partitions(7, 3)] + [[1, 1, 1], [1, 1, 1], [1, 1, 1], [2, 2, 2]] + >>> [list(p) for p in ordered_partitions(7, 3)] + [[1, 1, 5], [1, 2, 4], [1, 3, 3], [2, 2, 3]] + + When ``n`` is a multiple of ``m``, the elements are still sorted + but the partitions themselves will be *unordered* if sort is False; + the default is to return them in ascending lexicographical order. + + >>> for p in ordered_partitions(6, 2): + ... print(p) + [1, 5] + [2, 4] + [3, 3] + + But if speed is more important than ordering, sort can be set to + False: + + >>> for p in ordered_partitions(6, 2, sort=False): + ... print(p) + [1, 5] + [3, 3] + [2, 4] + + References + ========== + + .. [1] Generating Integer Partitions, [online], + Available: https://jeromekelleher.net/generating-integer-partitions.html + .. [2] Jerome Kelleher and Barry O'Sullivan, "Generating All + Partitions: A Comparison Of Two Encodings", [online], + Available: https://arxiv.org/pdf/0909.2331v2.pdf + """ + if n < 1 or m is not None and m < 1: + # the empty set is the only way to handle these inputs + # and returning {} to represent it is consistent with + # the counting convention, e.g. nT(0) == 1. + yield [] + return + + if m is None: + # The list `a`'s leading elements contain the partition in which + # y is the biggest element and x is either the same as y or the + # 2nd largest element; v and w are adjacent element indices + # to which x and y are being assigned, respectively. + a = [1]*n + y = -1 + v = n + while v > 0: + v -= 1 + x = a[v] + 1 + while y >= 2 * x: + a[v] = x + y -= x + v += 1 + w = v + 1 + while x <= y: + a[v] = x + a[w] = y + yield a[:w + 1] + x += 1 + y -= 1 + a[v] = x + y + y = a[v] - 1 + yield a[:w] + elif m == 1: + yield [n] + elif n == m: + yield [1]*n + else: + # recursively generate partitions of size m + for b in range(1, n//m + 1): + a = [b]*m + x = n - b*m + if not x: + if sort: + yield a + elif not sort and x <= m: + for ax in ordered_partitions(x, sort=False): + mi = len(ax) + a[-mi:] = [i + b for i in ax] + yield a + a[-mi:] = [b]*mi + else: + for mi in range(1, m): + for ax in ordered_partitions(x, mi, sort=True): + a[-mi:] = [i + b for i in ax] + yield a + a[-mi:] = [b]*mi + + +def binary_partitions(n): + """ + Generates the binary partition of *n*. + + A binary partition consists only of numbers that are + powers of two. Each step reduces a `2^{k+1}` to `2^k` and + `2^k`. Thus 16 is converted to 8 and 8. + + Examples + ======== + + >>> from sympy.utilities.iterables import binary_partitions + >>> for i in binary_partitions(5): + ... print(i) + ... + [4, 1] + [2, 2, 1] + [2, 1, 1, 1] + [1, 1, 1, 1, 1] + + References + ========== + + .. [1] TAOCP 4, section 7.2.1.5, problem 64 + + """ + from math import ceil, log2 + power = int(2**(ceil(log2(n)))) + acc = 0 + partition = [] + while power: + if acc + power <= n: + partition.append(power) + acc += power + power >>= 1 + + last_num = len(partition) - 1 - (n & 1) + while last_num >= 0: + yield partition + if partition[last_num] == 2: + partition[last_num] = 1 + partition.append(1) + last_num -= 1 + continue + partition.append(1) + partition[last_num] >>= 1 + x = partition[last_num + 1] = partition[last_num] + last_num += 1 + while x > 1: + if x <= len(partition) - last_num - 1: + del partition[-x + 1:] + last_num += 1 + partition[last_num] = x + else: + x >>= 1 + yield [1]*n + + +def has_dups(seq): + """Return True if there are any duplicate elements in ``seq``. + + Examples + ======== + + >>> from sympy import has_dups, Dict, Set + >>> has_dups((1, 2, 1)) + True + >>> has_dups(range(3)) + False + >>> all(has_dups(c) is False for c in (set(), Set(), dict(), Dict())) + True + """ + from sympy.core.containers import Dict + from sympy.sets.sets import Set + if isinstance(seq, (dict, set, Dict, Set)): + return False + unique = set() + try: + return any(True for s in seq if s in unique or unique.add(s)) + except TypeError: + return len(seq) != len(list(uniq(seq))) + + +def has_variety(seq): + """Return True if there are any different elements in ``seq``. + + Examples + ======== + + >>> from sympy import has_variety + + >>> has_variety((1, 2, 1)) + True + >>> has_variety((1, 1, 1)) + False + """ + for i, s in enumerate(seq): + if i == 0: + sentinel = s + else: + if s != sentinel: + return True + return False + + +def uniq(seq, result=None): + """ + Yield unique elements from ``seq`` as an iterator. The second + parameter ``result`` is used internally; it is not necessary + to pass anything for this. + + Note: changing the sequence during iteration will raise a + RuntimeError if the size of the sequence is known; if you pass + an iterator and advance the iterator you will change the + output of this routine but there will be no warning. + + Examples + ======== + + >>> from sympy.utilities.iterables import uniq + >>> dat = [1, 4, 1, 5, 4, 2, 1, 2] + >>> type(uniq(dat)) in (list, tuple) + False + + >>> list(uniq(dat)) + [1, 4, 5, 2] + >>> list(uniq(x for x in dat)) + [1, 4, 5, 2] + >>> list(uniq([[1], [2, 1], [1]])) + [[1], [2, 1]] + """ + try: + n = len(seq) + except TypeError: + n = None + def check(): + # check that size of seq did not change during iteration; + # if n == None the object won't support size changing, e.g. + # an iterator can't be changed + if n is not None and len(seq) != n: + raise RuntimeError('sequence changed size during iteration') + try: + seen = set() + result = result or [] + for i, s in enumerate(seq): + if not (s in seen or seen.add(s)): + yield s + check() + except TypeError: + if s not in result: + yield s + check() + result.append(s) + if hasattr(seq, '__getitem__'): + yield from uniq(seq[i + 1:], result) + else: + yield from uniq(seq, result) + + +def generate_bell(n): + """Return permutations of [0, 1, ..., n - 1] such that each permutation + differs from the last by the exchange of a single pair of neighbors. + The ``n!`` permutations are returned as an iterator. In order to obtain + the next permutation from a random starting permutation, use the + ``next_trotterjohnson`` method of the Permutation class (which generates + the same sequence in a different manner). + + Examples + ======== + + >>> from itertools import permutations + >>> from sympy.utilities.iterables import generate_bell + >>> from sympy import zeros, Matrix + + This is the sort of permutation used in the ringing of physical bells, + and does not produce permutations in lexicographical order. Rather, the + permutations differ from each other by exactly one inversion, and the + position at which the swapping occurs varies periodically in a simple + fashion. Consider the first few permutations of 4 elements generated + by ``permutations`` and ``generate_bell``: + + >>> list(permutations(range(4)))[:5] + [(0, 1, 2, 3), (0, 1, 3, 2), (0, 2, 1, 3), (0, 2, 3, 1), (0, 3, 1, 2)] + >>> list(generate_bell(4))[:5] + [(0, 1, 2, 3), (0, 1, 3, 2), (0, 3, 1, 2), (3, 0, 1, 2), (3, 0, 2, 1)] + + Notice how the 2nd and 3rd lexicographical permutations have 3 elements + out of place whereas each "bell" permutation always has only two + elements out of place relative to the previous permutation (and so the + signature (+/-1) of a permutation is opposite of the signature of the + previous permutation). + + How the position of inversion varies across the elements can be seen + by tracing out where the largest number appears in the permutations: + + >>> m = zeros(4, 24) + >>> for i, p in enumerate(generate_bell(4)): + ... m[:, i] = Matrix([j - 3 for j in list(p)]) # make largest zero + >>> m.print_nonzero('X') + [XXX XXXXXX XXXXXX XXX] + [XX XX XXXX XX XXXX XX XX] + [X XXXX XX XXXX XX XXXX X] + [ XXXXXX XXXXXX XXXXXX ] + + See Also + ======== + + sympy.combinatorics.permutations.Permutation.next_trotterjohnson + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Method_ringing + + .. [2] https://stackoverflow.com/questions/4856615/recursive-permutation/4857018 + + .. [3] https://web.archive.org/web/20160313023044/http://programminggeeks.com/bell-algorithm-for-permutation/ + + .. [4] https://en.wikipedia.org/wiki/Steinhaus%E2%80%93Johnson%E2%80%93Trotter_algorithm + + .. [5] Generating involutions, derangements, and relatives by ECO + Vincent Vajnovszki, DMTCS vol 1 issue 12, 2010 + + """ + n = as_int(n) + if n < 1: + raise ValueError('n must be a positive integer') + if n == 1: + yield (0,) + elif n == 2: + yield (0, 1) + yield (1, 0) + elif n == 3: + yield from [(0, 1, 2), (0, 2, 1), (2, 0, 1), (2, 1, 0), (1, 2, 0), (1, 0, 2)] + else: + m = n - 1 + op = [0] + [-1]*m + l = list(range(n)) + while True: + yield tuple(l) + # find biggest element with op + big = None, -1 # idx, value + for i in range(n): + if op[i] and l[i] > big[1]: + big = i, l[i] + i, _ = big + if i is None: + break # there are no ops left + # swap it with neighbor in the indicated direction + j = i + op[i] + l[i], l[j] = l[j], l[i] + op[i], op[j] = op[j], op[i] + # if it landed at the end or if the neighbor in the same + # direction is bigger then turn off op + if j == 0 or j == m or l[j + op[j]] > l[j]: + op[j] = 0 + # any element bigger to the left gets +1 op + for i in range(j): + if l[i] > l[j]: + op[i] = 1 + # any element bigger to the right gets -1 op + for i in range(j + 1, n): + if l[i] > l[j]: + op[i] = -1 + + +def generate_involutions(n): + """ + Generates involutions. + + An involution is a permutation that when multiplied + by itself equals the identity permutation. In this + implementation the involutions are generated using + Fixed Points. + + Alternatively, an involution can be considered as + a permutation that does not contain any cycles with + a length that is greater than two. + + Examples + ======== + + >>> from sympy.utilities.iterables import generate_involutions + >>> list(generate_involutions(3)) + [(0, 1, 2), (0, 2, 1), (1, 0, 2), (2, 1, 0)] + >>> len(list(generate_involutions(4))) + 10 + + References + ========== + + .. [1] https://mathworld.wolfram.com/PermutationInvolution.html + + """ + idx = list(range(n)) + for p in permutations(idx): + for i in idx: + if p[p[i]] != i: + break + else: + yield p + + +def multiset_derangements(s): + """Generate derangements of the elements of s *in place*. + + Examples + ======== + + >>> from sympy.utilities.iterables import multiset_derangements, uniq + + Because the derangements of multisets (not sets) are generated + in place, copies of the return value must be made if a collection + of derangements is desired or else all values will be the same: + + >>> list(uniq([i for i in multiset_derangements('1233')])) + [[None, None, None, None]] + >>> [i.copy() for i in multiset_derangements('1233')] + [['3', '3', '1', '2'], ['3', '3', '2', '1']] + >>> [''.join(i) for i in multiset_derangements('1233')] + ['3312', '3321'] + """ + from sympy.core.sorting import ordered + # create multiset dictionary of hashable elements or else + # remap elements to integers + try: + ms = multiset(s) + except TypeError: + # give each element a canonical integer value + key = dict(enumerate(ordered(uniq(s)))) + h = [] + for si in s: + for k in key: + if key[k] == si: + h.append(k) + break + for i in multiset_derangements(h): + yield [key[j] for j in i] + return + + mx = max(ms.values()) # max repetition of any element + n = len(s) # the number of elements + + ## special cases + + # 1) one element has more than half the total cardinality of s: no + # derangements are possible. + if mx*2 > n: + return + + # 2) all elements appear once: singletons + if len(ms) == n: + yield from _set_derangements(s) + return + + # find the first element that is repeated the most to place + # in the following two special cases where the selection + # is unambiguous: either there are two elements with multiplicity + # of mx or else there is only one with multiplicity mx + for M in ms: + if ms[M] == mx: + break + + inonM = [i for i in range(n) if s[i] != M] # location of non-M + iM = [i for i in range(n) if s[i] == M] # locations of M + rv = [None]*n + + # 3) half are the same + if 2*mx == n: + # M goes into non-M locations + for i in inonM: + rv[i] = M + # permutations of non-M go to M locations + for p in multiset_permutations([s[i] for i in inonM]): + for i, pi in zip(iM, p): + rv[i] = pi + yield rv + # clean-up (and encourages proper use of routine) + rv[:] = [None]*n + return + + # 4) single repeat covers all but 1 of the non-repeats: + # if there is one repeat then the multiset of the values + # of ms would be {mx: 1, 1: n - mx}, i.e. there would + # be n - mx + 1 values with the condition that n - 2*mx = 1 + if n - 2*mx == 1 and len(ms.values()) == n - mx + 1: + for i, i1 in enumerate(inonM): + ifill = inonM[:i] + inonM[i+1:] + for j in ifill: + rv[j] = M + for p in permutations([s[j] for j in ifill]): + rv[i1] = s[i1] + for j, pi in zip(iM, p): + rv[j] = pi + k = i1 + for j in iM: + rv[j], rv[k] = rv[k], rv[j] + yield rv + k = j + # clean-up (and encourages proper use of routine) + rv[:] = [None]*n + return + + ## general case is handled with 3 helpers: + # 1) `finish_derangements` will place the last two elements + # which have arbitrary multiplicities, e.g. for multiset + # {c: 3, a: 2, b: 2}, the last two elements are a and b + # 2) `iopen` will tell where a given element can be placed + # 3) `do` will recursively place elements into subsets of + # valid locations + + def finish_derangements(): + """Place the last two elements into the partially completed + derangement, and yield the results. + """ + + a = take[1][0] # penultimate element + a_ct = take[1][1] + b = take[0][0] # last element to be placed + b_ct = take[0][1] + + # split the indexes of the not-already-assigned elements of rv into + # three categories + forced_a = [] # positions which must have an a + forced_b = [] # positions which must have a b + open_free = [] # positions which could take either + for i in range(len(s)): + if rv[i] is None: + if s[i] == a: + forced_b.append(i) + elif s[i] == b: + forced_a.append(i) + else: + open_free.append(i) + + if len(forced_a) > a_ct or len(forced_b) > b_ct: + # No derangement possible + return + + for i in forced_a: + rv[i] = a + for i in forced_b: + rv[i] = b + for a_place in combinations(open_free, a_ct - len(forced_a)): + for a_pos in a_place: + rv[a_pos] = a + for i in open_free: + if rv[i] is None: # anything not in the subset is set to b + rv[i] = b + yield rv + # Clean up/undo the final placements + for i in open_free: + rv[i] = None + + # additional cleanup - clear forced_a, forced_b + for i in forced_a: + rv[i] = None + for i in forced_b: + rv[i] = None + + def iopen(v): + # return indices at which element v can be placed in rv: + # locations which are not already occupied if that location + # does not already contain v in the same location of s + return [i for i in range(n) if rv[i] is None and s[i] != v] + + def do(j): + if j == 1: + # handle the last two elements (regardless of multiplicity) + # with a special method + yield from finish_derangements() + else: + # place the mx elements of M into a subset of places + # into which it can be replaced + M, mx = take[j] + for i in combinations(iopen(M), mx): + # place M + for ii in i: + rv[ii] = M + # recursively place the next element + yield from do(j - 1) + # mark positions where M was placed as once again + # open for placement of other elements + for ii in i: + rv[ii] = None + + # process elements in order of canonically decreasing multiplicity + take = sorted(ms.items(), key=lambda x:(x[1], x[0])) + yield from do(len(take) - 1) + rv[:] = [None]*n + + +def random_derangement(t, choice=None, strict=True): + """Return a list of elements in which none are in the same positions + as they were originally. If an element fills more than half of the positions + then an error will be raised since no derangement is possible. To obtain + a derangement of as many items as possible--with some of the most numerous + remaining in their original positions--pass `strict=False`. To produce a + pseudorandom derangment, pass a pseudorandom selector like `choice` (see + below). + + Examples + ======== + + >>> from sympy.utilities.iterables import random_derangement + >>> t = 'SymPy: a CAS in pure Python' + >>> d = random_derangement(t) + >>> all(i != j for i, j in zip(d, t)) + True + + A predictable result can be obtained by using a pseudorandom + generator for the choice: + + >>> from sympy.core.random import seed, choice as c + >>> seed(1) + >>> d = [''.join(random_derangement(t, c)) for i in range(5)] + >>> assert len(set(d)) != 1 # we got different values + + By reseeding, the same sequence can be obtained: + + >>> seed(1) + >>> d2 = [''.join(random_derangement(t, c)) for i in range(5)] + >>> assert d == d2 + """ + if choice is None: + import secrets + choice = secrets.choice + def shuffle(rv): + '''Knuth shuffle''' + for i in range(len(rv) - 1, 0, -1): + x = choice(rv[:i + 1]) + j = rv.index(x) + rv[i], rv[j] = rv[j], rv[i] + def pick(rv, n): + '''shuffle rv and return the first n values + ''' + shuffle(rv) + return rv[:n] + ms = multiset(t) + tot = len(t) + ms = sorted(ms.items(), key=lambda x: x[1]) + # if there are not enough spaces for the most + # plentiful element to move to then some of them + # will have to stay in place + M, mx = ms[-1] + n = len(t) + xs = 2*mx - tot + if xs > 0: + if strict: + raise ValueError('no derangement possible') + opts = [i for (i, c) in enumerate(t) if c == ms[-1][0]] + pick(opts, xs) + stay = sorted(opts[:xs]) + rv = list(t) + for i in reversed(stay): + rv.pop(i) + rv = random_derangement(rv, choice) + for i in stay: + rv.insert(i, ms[-1][0]) + return ''.join(rv) if type(t) is str else rv + # the normal derangement calculated from here + if n == len(ms): + # approx 1/3 will succeed + rv = list(t) + while True: + shuffle(rv) + if all(i != j for i,j in zip(rv, t)): + break + else: + # general case + rv = [None]*n + while True: + j = 0 + while j > -len(ms): # do most numerous first + j -= 1 + e, c = ms[j] + opts = [i for i in range(n) if rv[i] is None and t[i] != e] + if len(opts) < c: + for i in range(n): + rv[i] = None + break # try again + pick(opts, c) + for i in range(c): + rv[opts[i]] = e + else: + return rv + return rv + + +def _set_derangements(s): + """ + yield derangements of items in ``s`` which are assumed to contain + no repeated elements + """ + if len(s) < 2: + return + if len(s) == 2: + yield [s[1], s[0]] + return + if len(s) == 3: + yield [s[1], s[2], s[0]] + yield [s[2], s[0], s[1]] + return + for p in permutations(s): + if not any(i == j for i, j in zip(p, s)): + yield list(p) + + +def generate_derangements(s): + """ + Return unique derangements of the elements of iterable ``s``. + + Examples + ======== + + >>> from sympy.utilities.iterables import generate_derangements + >>> list(generate_derangements([0, 1, 2])) + [[1, 2, 0], [2, 0, 1]] + >>> list(generate_derangements([0, 1, 2, 2])) + [[2, 2, 0, 1], [2, 2, 1, 0]] + >>> list(generate_derangements([0, 1, 1])) + [] + + See Also + ======== + + sympy.functions.combinatorial.factorials.subfactorial + + """ + if not has_dups(s): + yield from _set_derangements(s) + else: + for p in multiset_derangements(s): + yield list(p) + + +def necklaces(n, k, free=False): + """ + A routine to generate necklaces that may (free=True) or may not + (free=False) be turned over to be viewed. The "necklaces" returned + are comprised of ``n`` integers (beads) with ``k`` different + values (colors). Only unique necklaces are returned. + + Examples + ======== + + >>> from sympy.utilities.iterables import necklaces, bracelets + >>> def show(s, i): + ... return ''.join(s[j] for j in i) + + The "unrestricted necklace" is sometimes also referred to as a + "bracelet" (an object that can be turned over, a sequence that can + be reversed) and the term "necklace" is used to imply a sequence + that cannot be reversed. So ACB == ABC for a bracelet (rotate and + reverse) while the two are different for a necklace since rotation + alone cannot make the two sequences the same. + + (mnemonic: Bracelets can be viewed Backwards, but Not Necklaces.) + + >>> B = [show('ABC', i) for i in bracelets(3, 3)] + >>> N = [show('ABC', i) for i in necklaces(3, 3)] + >>> set(N) - set(B) + {'ACB'} + + >>> list(necklaces(4, 2)) + [(0, 0, 0, 0), (0, 0, 0, 1), (0, 0, 1, 1), + (0, 1, 0, 1), (0, 1, 1, 1), (1, 1, 1, 1)] + + >>> [show('.o', i) for i in bracelets(4, 2)] + ['....', '...o', '..oo', '.o.o', '.ooo', 'oooo'] + + References + ========== + + .. [1] https://mathworld.wolfram.com/Necklace.html + + .. [2] Frank Ruskey, Carla Savage, and Terry Min Yih Wang, + Generating necklaces, Journal of Algorithms 13 (1992), 414-430; + https://doi.org/10.1016/0196-6774(92)90047-G + + """ + # The FKM algorithm + if k == 0 and n > 0: + return + a = [0]*n + yield tuple(a) + if n == 0: + return + while True: + i = n - 1 + while a[i] == k - 1: + i -= 1 + if i == -1: + return + a[i] += 1 + for j in range(n - i - 1): + a[j + i + 1] = a[j] + if n % (i + 1) == 0 and (not free or all(a <= a[j::-1] + a[-1:j:-1] for j in range(n - 1))): + # No need to test j = n - 1. + yield tuple(a) + + +def bracelets(n, k): + """Wrapper to necklaces to return a free (unrestricted) necklace.""" + return necklaces(n, k, free=True) + + +def generate_oriented_forest(n): + """ + This algorithm generates oriented forests. + + An oriented graph is a directed graph having no symmetric pair of directed + edges. A forest is an acyclic graph, i.e., it has no cycles. A forest can + also be described as a disjoint union of trees, which are graphs in which + any two vertices are connected by exactly one simple path. + + Examples + ======== + + >>> from sympy.utilities.iterables import generate_oriented_forest + >>> list(generate_oriented_forest(4)) + [[0, 1, 2, 3], [0, 1, 2, 2], [0, 1, 2, 1], [0, 1, 2, 0], \ + [0, 1, 1, 1], [0, 1, 1, 0], [0, 1, 0, 1], [0, 1, 0, 0], [0, 0, 0, 0]] + + References + ========== + + .. [1] T. Beyer and S.M. Hedetniemi: constant time generation of + rooted trees, SIAM J. Computing Vol. 9, No. 4, November 1980 + + .. [2] https://stackoverflow.com/questions/1633833/oriented-forest-taocp-algorithm-in-python + + """ + P = list(range(-1, n)) + while True: + yield P[1:] + if P[n] > 0: + P[n] = P[P[n]] + else: + for p in range(n - 1, 0, -1): + if P[p] != 0: + target = P[p] - 1 + for q in range(p - 1, 0, -1): + if P[q] == target: + break + offset = p - q + for i in range(p, n + 1): + P[i] = P[i - offset] + break + else: + break + + +def minlex(seq, directed=True, key=None): + r""" + Return the rotation of the sequence in which the lexically smallest + elements appear first, e.g. `cba \rightarrow acb`. + + The sequence returned is a tuple, unless the input sequence is a string + in which case a string is returned. + + If ``directed`` is False then the smaller of the sequence and the + reversed sequence is returned, e.g. `cba \rightarrow abc`. + + If ``key`` is not None then it is used to extract a comparison key from each element in iterable. + + Examples + ======== + + >>> from sympy.combinatorics.polyhedron import minlex + >>> minlex((1, 2, 0)) + (0, 1, 2) + >>> minlex((1, 0, 2)) + (0, 2, 1) + >>> minlex((1, 0, 2), directed=False) + (0, 1, 2) + + >>> minlex('11010011000', directed=True) + '00011010011' + >>> minlex('11010011000', directed=False) + '00011001011' + + >>> minlex(('bb', 'aaa', 'c', 'a')) + ('a', 'bb', 'aaa', 'c') + >>> minlex(('bb', 'aaa', 'c', 'a'), key=len) + ('c', 'a', 'bb', 'aaa') + + """ + from sympy.functions.elementary.miscellaneous import Id + if key is None: key = Id + best = rotate_left(seq, least_rotation(seq, key=key)) + if not directed: + rseq = seq[::-1] + rbest = rotate_left(rseq, least_rotation(rseq, key=key)) + best = min(best, rbest, key=key) + + # Convert to tuple, unless we started with a string. + return tuple(best) if not isinstance(seq, str) else best + + +def runs(seq, op=gt): + """Group the sequence into lists in which successive elements + all compare the same with the comparison operator, ``op``: + op(seq[i + 1], seq[i]) is True from all elements in a run. + + Examples + ======== + + >>> from sympy.utilities.iterables import runs + >>> from operator import ge + >>> runs([0, 1, 2, 2, 1, 4, 3, 2, 2]) + [[0, 1, 2], [2], [1, 4], [3], [2], [2]] + >>> runs([0, 1, 2, 2, 1, 4, 3, 2, 2], op=ge) + [[0, 1, 2, 2], [1, 4], [3], [2, 2]] + """ + cycles = [] + seq = iter(seq) + try: + run = [next(seq)] + except StopIteration: + return [] + while True: + try: + ei = next(seq) + except StopIteration: + break + if op(ei, run[-1]): + run.append(ei) + continue + else: + cycles.append(run) + run = [ei] + if run: + cycles.append(run) + return cycles + + +def sequence_partitions(l, n, /): + r"""Returns the partition of sequence $l$ into $n$ bins + + Explanation + =========== + + Given the sequence $l_1 \cdots l_m \in V^+$ where + $V^+$ is the Kleene plus of $V$ + + The set of $n$ partitions of $l$ is defined as: + + .. math:: + \{(s_1, \cdots, s_n) | s_1 \in V^+, \cdots, s_n \in V^+, + s_1 \cdots s_n = l_1 \cdots l_m\} + + Parameters + ========== + + l : Sequence[T] + A nonempty sequence of any Python objects + + n : int + A positive integer + + Yields + ====== + + out : list[Sequence[T]] + A list of sequences with concatenation equals $l$. + This should conform with the type of $l$. + + Examples + ======== + + >>> from sympy.utilities.iterables import sequence_partitions + >>> for out in sequence_partitions([1, 2, 3, 4], 2): + ... print(out) + [[1], [2, 3, 4]] + [[1, 2], [3, 4]] + [[1, 2, 3], [4]] + + Notes + ===== + + This is modified version of EnricoGiampieri's partition generator + from https://stackoverflow.com/questions/13131491/partition-n-items-into-k-bins-in-python-lazily + + See Also + ======== + + sequence_partitions_empty + """ + # Asserting l is nonempty is done only for sanity check + if n == 1 and l: + yield [l] + return + for i in range(1, len(l)): + for part in sequence_partitions(l[i:], n - 1): + yield [l[:i]] + part + + +def sequence_partitions_empty(l, n, /): + r"""Returns the partition of sequence $l$ into $n$ bins with + empty sequence + + Explanation + =========== + + Given the sequence $l_1 \cdots l_m \in V^*$ where + $V^*$ is the Kleene star of $V$ + + The set of $n$ partitions of $l$ is defined as: + + .. math:: + \{(s_1, \cdots, s_n) | s_1 \in V^*, \cdots, s_n \in V^*, + s_1 \cdots s_n = l_1 \cdots l_m\} + + There are more combinations than :func:`sequence_partitions` because + empty sequence can fill everywhere, so we try to provide different + utility for this. + + Parameters + ========== + + l : Sequence[T] + A sequence of any Python objects (can be possibly empty) + + n : int + A positive integer + + Yields + ====== + + out : list[Sequence[T]] + A list of sequences with concatenation equals $l$. + This should conform with the type of $l$. + + Examples + ======== + + >>> from sympy.utilities.iterables import sequence_partitions_empty + >>> for out in sequence_partitions_empty([1, 2, 3, 4], 2): + ... print(out) + [[], [1, 2, 3, 4]] + [[1], [2, 3, 4]] + [[1, 2], [3, 4]] + [[1, 2, 3], [4]] + [[1, 2, 3, 4], []] + + See Also + ======== + + sequence_partitions + """ + if n < 1: + return + if n == 1: + yield [l] + return + for i in range(0, len(l) + 1): + for part in sequence_partitions_empty(l[i:], n - 1): + yield [l[:i]] + part + + +def kbins(l, k, ordered=None): + """ + Return sequence ``l`` partitioned into ``k`` bins. + + Examples + ======== + + The default is to give the items in the same order, but grouped + into k partitions without any reordering: + + >>> from sympy.utilities.iterables import kbins + >>> for p in kbins(list(range(5)), 2): + ... print(p) + ... + [[0], [1, 2, 3, 4]] + [[0, 1], [2, 3, 4]] + [[0, 1, 2], [3, 4]] + [[0, 1, 2, 3], [4]] + + The ``ordered`` flag is either None (to give the simple partition + of the elements) or is a 2 digit integer indicating whether the order of + the bins and the order of the items in the bins matters. Given:: + + A = [[0], [1, 2]] + B = [[1, 2], [0]] + C = [[2, 1], [0]] + D = [[0], [2, 1]] + + the following values for ``ordered`` have the shown meanings:: + + 00 means A == B == C == D + 01 means A == B + 10 means A == D + 11 means A == A + + >>> for ordered_flag in [None, 0, 1, 10, 11]: + ... print('ordered = %s' % ordered_flag) + ... for p in kbins(list(range(3)), 2, ordered=ordered_flag): + ... print(' %s' % p) + ... + ordered = None + [[0], [1, 2]] + [[0, 1], [2]] + ordered = 0 + [[0, 1], [2]] + [[0, 2], [1]] + [[0], [1, 2]] + ordered = 1 + [[0], [1, 2]] + [[0], [2, 1]] + [[1], [0, 2]] + [[1], [2, 0]] + [[2], [0, 1]] + [[2], [1, 0]] + ordered = 10 + [[0, 1], [2]] + [[2], [0, 1]] + [[0, 2], [1]] + [[1], [0, 2]] + [[0], [1, 2]] + [[1, 2], [0]] + ordered = 11 + [[0], [1, 2]] + [[0, 1], [2]] + [[0], [2, 1]] + [[0, 2], [1]] + [[1], [0, 2]] + [[1, 0], [2]] + [[1], [2, 0]] + [[1, 2], [0]] + [[2], [0, 1]] + [[2, 0], [1]] + [[2], [1, 0]] + [[2, 1], [0]] + + See Also + ======== + + partitions, multiset_partitions + + """ + if ordered is None: + yield from sequence_partitions(l, k) + elif ordered == 11: + for pl in multiset_permutations(l): + pl = list(pl) + yield from sequence_partitions(pl, k) + elif ordered == 00: + yield from multiset_partitions(l, k) + elif ordered == 10: + for p in multiset_partitions(l, k): + for perm in permutations(p): + yield list(perm) + elif ordered == 1: + for kgot, p in partitions(len(l), k, size=True): + if kgot != k: + continue + for li in multiset_permutations(l): + rv = [] + i = j = 0 + li = list(li) + for size, multiplicity in sorted(p.items()): + for m in range(multiplicity): + j = i + size + rv.append(li[i: j]) + i = j + yield rv + else: + raise ValueError( + 'ordered must be one of 00, 01, 10 or 11, not %s' % ordered) + + +def permute_signs(t): + """Return iterator in which the signs of non-zero elements + of t are permuted. + + Examples + ======== + + >>> from sympy.utilities.iterables import permute_signs + >>> list(permute_signs((0, 1, 2))) + [(0, 1, 2), (0, -1, 2), (0, 1, -2), (0, -1, -2)] + """ + for signs in product(*[(1, -1)]*(len(t) - t.count(0))): + signs = list(signs) + yield type(t)([i*signs.pop() if i else i for i in t]) + + +def signed_permutations(t): + """Return iterator in which the signs of non-zero elements + of t and the order of the elements are permuted and all + returned values are unique. + + Examples + ======== + + >>> from sympy.utilities.iterables import signed_permutations + >>> list(signed_permutations((0, 1, 2))) + [(0, 1, 2), (0, -1, 2), (0, 1, -2), (0, -1, -2), (0, 2, 1), + (0, -2, 1), (0, 2, -1), (0, -2, -1), (1, 0, 2), (-1, 0, 2), + (1, 0, -2), (-1, 0, -2), (1, 2, 0), (-1, 2, 0), (1, -2, 0), + (-1, -2, 0), (2, 0, 1), (-2, 0, 1), (2, 0, -1), (-2, 0, -1), + (2, 1, 0), (-2, 1, 0), (2, -1, 0), (-2, -1, 0)] + """ + return (type(t)(i) for j in multiset_permutations(t) + for i in permute_signs(j)) + + +def rotations(s, dir=1): + """Return a generator giving the items in s as list where + each subsequent list has the items rotated to the left (default) + or right (``dir=-1``) relative to the previous list. + + Examples + ======== + + >>> from sympy import rotations + >>> list(rotations([1,2,3])) + [[1, 2, 3], [2, 3, 1], [3, 1, 2]] + >>> list(rotations([1,2,3], -1)) + [[1, 2, 3], [3, 1, 2], [2, 3, 1]] + """ + seq = list(s) + for i in range(len(seq)): + yield seq + seq = rotate_left(seq, dir) + + +def roundrobin(*iterables): + """roundrobin recipe taken from itertools documentation: + https://docs.python.org/3/library/itertools.html#itertools-recipes + + roundrobin('ABC', 'D', 'EF') --> A D E B F C + + Recipe credited to George Sakkis + """ + nexts = cycle(iter(it).__next__ for it in iterables) + + pending = len(iterables) + while pending: + try: + for nxt in nexts: + yield nxt() + except StopIteration: + pending -= 1 + nexts = cycle(islice(nexts, pending)) + + + +class NotIterable: + """ + Use this as mixin when creating a class which is not supposed to + return true when iterable() is called on its instances because + calling list() on the instance, for example, would result in + an infinite loop. + """ + pass + + +def iterable(i, exclude=(str, dict, NotIterable)): + """ + Return a boolean indicating whether ``i`` is SymPy iterable. + True also indicates that the iterator is finite, e.g. you can + call list(...) on the instance. + + When SymPy is working with iterables, it is almost always assuming + that the iterable is not a string or a mapping, so those are excluded + by default. If you want a pure Python definition, make exclude=None. To + exclude multiple items, pass them as a tuple. + + You can also set the _iterable attribute to True or False on your class, + which will override the checks here, including the exclude test. + + As a rule of thumb, some SymPy functions use this to check if they should + recursively map over an object. If an object is technically iterable in + the Python sense but does not desire this behavior (e.g., because its + iteration is not finite, or because iteration might induce an unwanted + computation), it should disable it by setting the _iterable attribute to False. + + See also: is_sequence + + Examples + ======== + + >>> from sympy.utilities.iterables import iterable + >>> from sympy import Tuple + >>> things = [[1], (1,), set([1]), Tuple(1), (j for j in [1, 2]), {1:2}, '1', 1] + >>> for i in things: + ... print('%s %s' % (iterable(i), type(i))) + True <... 'list'> + True <... 'tuple'> + True <... 'set'> + True + True <... 'generator'> + False <... 'dict'> + False <... 'str'> + False <... 'int'> + + >>> iterable({}, exclude=None) + True + >>> iterable({}, exclude=str) + True + >>> iterable("no", exclude=str) + False + + """ + if hasattr(i, '_iterable'): + return i._iterable + try: + iter(i) + except TypeError: + return False + if exclude: + return not isinstance(i, exclude) + return True + + +def is_sequence(i, include=None): + """ + Return a boolean indicating whether ``i`` is a sequence in the SymPy + sense. If anything that fails the test below should be included as + being a sequence for your application, set 'include' to that object's + type; multiple types should be passed as a tuple of types. + + Note: although generators can generate a sequence, they often need special + handling to make sure their elements are captured before the generator is + exhausted, so these are not included by default in the definition of a + sequence. + + See also: iterable + + Examples + ======== + + >>> from sympy.utilities.iterables import is_sequence + >>> from types import GeneratorType + >>> is_sequence([]) + True + >>> is_sequence(set()) + False + >>> is_sequence('abc') + False + >>> is_sequence('abc', include=str) + True + >>> generator = (c for c in 'abc') + >>> is_sequence(generator) + False + >>> is_sequence(generator, include=(str, GeneratorType)) + True + + """ + return (hasattr(i, '__getitem__') and + iterable(i) or + bool(include) and + isinstance(i, include)) + + +@deprecated( + """ + Using postorder_traversal from the sympy.utilities.iterables submodule is + deprecated. + + Instead, use postorder_traversal from the top-level sympy namespace, like + + sympy.postorder_traversal + """, + deprecated_since_version="1.10", + active_deprecations_target="deprecated-traversal-functions-moved") +def postorder_traversal(node, keys=None): + from sympy.core.traversal import postorder_traversal as _postorder_traversal + return _postorder_traversal(node, keys=keys) + + +@deprecated( + """ + Using interactive_traversal from the sympy.utilities.iterables submodule + is deprecated. + + Instead, use interactive_traversal from the top-level sympy namespace, + like + + sympy.interactive_traversal + """, + deprecated_since_version="1.10", + active_deprecations_target="deprecated-traversal-functions-moved") +def interactive_traversal(expr): + from sympy.interactive.traversal import interactive_traversal as _interactive_traversal + return _interactive_traversal(expr) + + +@deprecated( + """ + Importing default_sort_key from sympy.utilities.iterables is deprecated. + Use from sympy import default_sort_key instead. + """, + deprecated_since_version="1.10", +active_deprecations_target="deprecated-sympy-core-compatibility", +) +def default_sort_key(*args, **kwargs): + from sympy import default_sort_key as _default_sort_key + return _default_sort_key(*args, **kwargs) + + +@deprecated( + """ + Importing default_sort_key from sympy.utilities.iterables is deprecated. + Use from sympy import default_sort_key instead. + """, + deprecated_since_version="1.10", +active_deprecations_target="deprecated-sympy-core-compatibility", +) +def ordered(*args, **kwargs): + from sympy import ordered as _ordered + return _ordered(*args, **kwargs) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/lambdify.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/lambdify.py new file mode 100644 index 0000000000000000000000000000000000000000..6807fdaec10963a60ba88d6e1ec6b2951c19241e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/lambdify.py @@ -0,0 +1,1592 @@ +""" +This module provides convenient functions to transform SymPy expressions to +lambda functions which can be used to calculate numerical values very fast. +""" + +from __future__ import annotations +from typing import Any + +import builtins +import inspect +import keyword +import textwrap +import linecache +import weakref + +# Required despite static analysis claiming it is not used +from sympy.external import import_module # noqa:F401 +from sympy.utilities.exceptions import sympy_deprecation_warning +from sympy.utilities.decorator import doctest_depends_on +from sympy.utilities.iterables import (is_sequence, iterable, + NotIterable, flatten) +from sympy.utilities.misc import filldedent + + +__doctest_requires__ = {('lambdify',): ['numpy', 'tensorflow']} + + +# Default namespaces, letting us define translations that can't be defined +# by simple variable maps, like I => 1j +MATH_DEFAULT: dict[str, Any] = {} +CMATH_DEFAULT: dict[str,Any] = {} +MPMATH_DEFAULT: dict[str, Any] = {} +NUMPY_DEFAULT: dict[str, Any] = {"I": 1j} +SCIPY_DEFAULT: dict[str, Any] = {"I": 1j} +CUPY_DEFAULT: dict[str, Any] = {"I": 1j} +JAX_DEFAULT: dict[str, Any] = {"I": 1j} +TENSORFLOW_DEFAULT: dict[str, Any] = {} +TORCH_DEFAULT: dict[str, Any] = {"I": 1j} +SYMPY_DEFAULT: dict[str, Any] = {} +NUMEXPR_DEFAULT: dict[str, Any] = {} + +# These are the namespaces the lambda functions will use. +# These are separate from the names above because they are modified +# throughout this file, whereas the defaults should remain unmodified. + +MATH = MATH_DEFAULT.copy() +CMATH = CMATH_DEFAULT.copy() +MPMATH = MPMATH_DEFAULT.copy() +NUMPY = NUMPY_DEFAULT.copy() +SCIPY = SCIPY_DEFAULT.copy() +CUPY = CUPY_DEFAULT.copy() +JAX = JAX_DEFAULT.copy() +TENSORFLOW = TENSORFLOW_DEFAULT.copy() +TORCH = TORCH_DEFAULT.copy() +SYMPY = SYMPY_DEFAULT.copy() +NUMEXPR = NUMEXPR_DEFAULT.copy() + + +# Mappings between SymPy and other modules function names. +MATH_TRANSLATIONS = { + "ceiling": "ceil", + "E": "e", + "ln": "log", +} + +CMATH_TRANSLATIONS: dict[str, str] = {} + +# NOTE: This dictionary is reused in Function._eval_evalf to allow subclasses +# of Function to automatically evalf. +MPMATH_TRANSLATIONS = { + "Abs": "fabs", + "elliptic_k": "ellipk", + "elliptic_f": "ellipf", + "elliptic_e": "ellipe", + "elliptic_pi": "ellippi", + "ceiling": "ceil", + "chebyshevt": "chebyt", + "chebyshevu": "chebyu", + "assoc_legendre": "legenp", + "E": "e", + "I": "j", + "ln": "log", + #"lowergamma":"lower_gamma", + "oo": "inf", + #"uppergamma":"upper_gamma", + "LambertW": "lambertw", + "MutableDenseMatrix": "matrix", + "ImmutableDenseMatrix": "matrix", + "conjugate": "conj", + "dirichlet_eta": "altzeta", + "Ei": "ei", + "Shi": "shi", + "Chi": "chi", + "Si": "si", + "Ci": "ci", + "RisingFactorial": "rf", + "FallingFactorial": "ff", + "betainc_regularized": "betainc", +} + +NUMPY_TRANSLATIONS: dict[str, str] = { + "Heaviside": "heaviside", +} +SCIPY_TRANSLATIONS: dict[str, str] = { + "jn" : "spherical_jn", + "yn" : "spherical_yn" +} +CUPY_TRANSLATIONS: dict[str, str] = {} +JAX_TRANSLATIONS: dict[str, str] = {} + +TENSORFLOW_TRANSLATIONS: dict[str, str] = {} +TORCH_TRANSLATIONS: dict[str, str] = {} + +NUMEXPR_TRANSLATIONS: dict[str, str] = {} + +# Available modules: +MODULES = { + "math": (MATH, MATH_DEFAULT, MATH_TRANSLATIONS, ("from math import *",)), + "cmath": (CMATH, CMATH_DEFAULT, CMATH_TRANSLATIONS, ("import cmath; from cmath import *",)), + "mpmath": (MPMATH, MPMATH_DEFAULT, MPMATH_TRANSLATIONS, ("from mpmath import *",)), + "numpy": (NUMPY, NUMPY_DEFAULT, NUMPY_TRANSLATIONS, ("import numpy; from numpy import *; from numpy.linalg import *",)), + "scipy": (SCIPY, SCIPY_DEFAULT, SCIPY_TRANSLATIONS, ("import scipy; import numpy; from scipy.special import *",)), + "cupy": (CUPY, CUPY_DEFAULT, CUPY_TRANSLATIONS, ("import cupy",)), + "jax": (JAX, JAX_DEFAULT, JAX_TRANSLATIONS, ("import jax",)), + "tensorflow": (TENSORFLOW, TENSORFLOW_DEFAULT, TENSORFLOW_TRANSLATIONS, ("import tensorflow",)), + "torch": (TORCH, TORCH_DEFAULT, TORCH_TRANSLATIONS, ("import torch",)), + "sympy": (SYMPY, SYMPY_DEFAULT, {}, ( + "from sympy.functions import *", + "from sympy.matrices import *", + "from sympy import Integral, pi, oo, nan, zoo, E, I",)), + "numexpr" : (NUMEXPR, NUMEXPR_DEFAULT, NUMEXPR_TRANSLATIONS, + ("import_module('numexpr')", )), +} + + +def _import(module, reload=False): + """ + Creates a global translation dictionary for module. + + The argument module has to be one of the following strings: "math","cmath" + "mpmath", "numpy", "sympy", "tensorflow", "jax". + These dictionaries map names of Python functions to their equivalent in + other modules. + """ + try: + namespace, namespace_default, translations, import_commands = MODULES[ + module] + except KeyError: + raise NameError( + "'%s' module cannot be used for lambdification" % module) + + # Clear namespace or exit + if namespace != namespace_default: + # The namespace was already generated, don't do it again if not forced. + if reload: + namespace.clear() + namespace.update(namespace_default) + else: + return + + for import_command in import_commands: + if import_command.startswith('import_module'): + module = eval(import_command) + + if module is not None: + namespace.update(module.__dict__) + continue + else: + try: + exec(import_command, {}, namespace) + continue + except ImportError: + pass + + raise ImportError( + "Cannot import '%s' with '%s' command" % (module, import_command)) + + # Add translated names to namespace + for sympyname, translation in translations.items(): + namespace[sympyname] = namespace[translation] + + # For computing the modulus of a SymPy expression we use the builtin abs + # function, instead of the previously used fabs function for all + # translation modules. This is because the fabs function in the math + # module does not accept complex valued arguments. (see issue 9474). The + # only exception, where we don't use the builtin abs function is the + # mpmath translation module, because mpmath.fabs returns mpf objects in + # contrast to abs(). + if 'Abs' not in namespace: + namespace['Abs'] = abs + +# Used for dynamically generated filenames that are inserted into the +# linecache. +_lambdify_generated_counter = 1 + + +@doctest_depends_on(modules=('numpy', 'scipy', 'tensorflow',), python_version=(3,)) +def lambdify(args, expr, modules=None, printer=None, use_imps=True, + dummify=False, cse=False, docstring_limit=1000): + """Convert a SymPy expression into a function that allows for fast + numeric evaluation. + + .. warning:: + This function uses ``exec``, and thus should not be used on + unsanitized input. + + .. deprecated:: 1.7 + Passing a set for the *args* parameter is deprecated as sets are + unordered. Use an ordered iterable such as a list or tuple. + + Explanation + =========== + + For example, to convert the SymPy expression ``sin(x) + cos(x)`` to an + equivalent NumPy function that numerically evaluates it: + + >>> from sympy import sin, cos, symbols, lambdify + >>> import numpy as np + >>> x = symbols('x') + >>> expr = sin(x) + cos(x) + >>> expr + sin(x) + cos(x) + >>> f = lambdify(x, expr, 'numpy') + >>> a = np.array([1, 2]) + >>> f(a) + [1.38177329 0.49315059] + + The primary purpose of this function is to provide a bridge from SymPy + expressions to numerical libraries such as NumPy, SciPy, NumExpr, mpmath, + and tensorflow. In general, SymPy functions do not work with objects from + other libraries, such as NumPy arrays, and functions from numeric + libraries like NumPy or mpmath do not work on SymPy expressions. + ``lambdify`` bridges the two by converting a SymPy expression to an + equivalent numeric function. + + The basic workflow with ``lambdify`` is to first create a SymPy expression + representing whatever mathematical function you wish to evaluate. This + should be done using only SymPy functions and expressions. Then, use + ``lambdify`` to convert this to an equivalent function for numerical + evaluation. For instance, above we created ``expr`` using the SymPy symbol + ``x`` and SymPy functions ``sin`` and ``cos``, then converted it to an + equivalent NumPy function ``f``, and called it on a NumPy array ``a``. + + Parameters + ========== + + args : List[Symbol] + A variable or a list of variables whose nesting represents the + nesting of the arguments that will be passed to the function. + + Variables can be symbols, undefined functions, or matrix symbols. + + >>> from sympy import Eq + >>> from sympy.abc import x, y, z + + The list of variables should match the structure of how the + arguments will be passed to the function. Simply enclose the + parameters as they will be passed in a list. + + To call a function like ``f(x)`` then ``[x]`` + should be the first argument to ``lambdify``; for this + case a single ``x`` can also be used: + + >>> f = lambdify(x, x + 1) + >>> f(1) + 2 + >>> f = lambdify([x], x + 1) + >>> f(1) + 2 + + To call a function like ``f(x, y)`` then ``[x, y]`` will + be the first argument of the ``lambdify``: + + >>> f = lambdify([x, y], x + y) + >>> f(1, 1) + 2 + + To call a function with a single 3-element tuple like + ``f((x, y, z))`` then ``[(x, y, z)]`` will be the first + argument of the ``lambdify``: + + >>> f = lambdify([(x, y, z)], Eq(z**2, x**2 + y**2)) + >>> f((3, 4, 5)) + True + + If two args will be passed and the first is a scalar but + the second is a tuple with two arguments then the items + in the list should match that structure: + + >>> f = lambdify([x, (y, z)], x + y + z) + >>> f(1, (2, 3)) + 6 + + expr : Expr + An expression, list of expressions, or matrix to be evaluated. + + Lists may be nested. + If the expression is a list, the output will also be a list. + + >>> f = lambdify(x, [x, [x + 1, x + 2]]) + >>> f(1) + [1, [2, 3]] + + If it is a matrix, an array will be returned (for the NumPy module). + + >>> from sympy import Matrix + >>> f = lambdify(x, Matrix([x, x + 1])) + >>> f(1) + [[1] + [2]] + + Note that the argument order here (variables then expression) is used + to emulate the Python ``lambda`` keyword. ``lambdify(x, expr)`` works + (roughly) like ``lambda x: expr`` + (see :ref:`lambdify-how-it-works` below). + + modules : str, optional + Specifies the numeric library to use. + + If not specified, *modules* defaults to: + + - ``["scipy", "numpy"]`` if SciPy is installed + - ``["numpy"]`` if only NumPy is installed + - ``["math","cmath", "mpmath", "sympy"]`` if neither is installed. + + That is, SymPy functions are replaced as far as possible by + either ``scipy`` or ``numpy`` functions if available, and Python's + standard library ``math`` and ``cmath``, or ``mpmath`` functions otherwise. + + *modules* can be one of the following types: + + - The strings ``"math"``, ``"cmath"``, ``"mpmath"``, ``"numpy"``, ``"numexpr"``, + ``"scipy"``, ``"sympy"``, or ``"tensorflow"`` or ``"jax"``. This uses the + corresponding printer and namespace mapping for that module. + - A module (e.g., ``math``). This uses the global namespace of the + module. If the module is one of the above known modules, it will + also use the corresponding printer and namespace mapping + (i.e., ``modules=numpy`` is equivalent to ``modules="numpy"``). + - A dictionary that maps names of SymPy functions to arbitrary + functions + (e.g., ``{'sin': custom_sin}``). + - A list that contains a mix of the arguments above, with higher + priority given to entries appearing first + (e.g., to use the NumPy module but override the ``sin`` function + with a custom version, you can use + ``[{'sin': custom_sin}, 'numpy']``). + + dummify : bool, optional + Whether or not the variables in the provided expression that are not + valid Python identifiers are substituted with dummy symbols. + + This allows for undefined functions like ``Function('f')(t)`` to be + supplied as arguments. By default, the variables are only dummified + if they are not valid Python identifiers. + + Set ``dummify=True`` to replace all arguments with dummy symbols + (if ``args`` is not a string) - for example, to ensure that the + arguments do not redefine any built-in names. + + cse : bool, or callable, optional + Large expressions can be computed more efficiently when + common subexpressions are identified and precomputed before + being used multiple time. Finding the subexpressions will make + creation of the 'lambdify' function slower, however. + + When ``True``, ``sympy.simplify.cse`` is used, otherwise (the default) + the user may pass a function matching the ``cse`` signature. + + docstring_limit : int or None + When lambdifying large expressions, a significant proportion of the time + spent inside ``lambdify`` is spent producing a string representation of + the expression for use in the automatically generated docstring of the + returned function. For expressions containing hundreds or more nodes the + resulting docstring often becomes so long and dense that it is difficult + to read. To reduce the runtime of lambdify, the rendering of the full + expression inside the docstring can be disabled. + + When ``None``, the full expression is rendered in the docstring. When + ``0`` or a negative ``int``, an ellipsis is rendering in the docstring + instead of the expression. When a strictly positive ``int``, if the + number of nodes in the expression exceeds ``docstring_limit`` an + ellipsis is rendered in the docstring, otherwise a string representation + of the expression is rendered as normal. The default is ``1000``. + + Examples + ======== + + >>> from sympy.utilities.lambdify import implemented_function + >>> from sympy import sqrt, sin, Matrix + >>> from sympy import Function + >>> from sympy.abc import w, x, y, z + + >>> f = lambdify(x, x**2) + >>> f(2) + 4 + >>> f = lambdify((x, y, z), [z, y, x]) + >>> f(1,2,3) + [3, 2, 1] + >>> f = lambdify(x, sqrt(x)) + >>> f(4) + 2.0 + >>> f = lambdify((x, y), sin(x*y)**2) + >>> f(0, 5) + 0.0 + >>> row = lambdify((x, y), Matrix((x, x + y)).T, modules='sympy') + >>> row(1, 2) + Matrix([[1, 3]]) + + ``lambdify`` can be used to translate SymPy expressions into mpmath + functions. This may be preferable to using ``evalf`` (which uses mpmath on + the backend) in some cases. + + >>> f = lambdify(x, sin(x), 'mpmath') + >>> f(1) + 0.8414709848078965 + + Tuple arguments are handled and the lambdified function should + be called with the same type of arguments as were used to create + the function: + + >>> f = lambdify((x, (y, z)), x + y) + >>> f(1, (2, 4)) + 3 + + The ``flatten`` function can be used to always work with flattened + arguments: + + >>> from sympy.utilities.iterables import flatten + >>> args = w, (x, (y, z)) + >>> vals = 1, (2, (3, 4)) + >>> f = lambdify(flatten(args), w + x + y + z) + >>> f(*flatten(vals)) + 10 + + Functions present in ``expr`` can also carry their own numerical + implementations, in a callable attached to the ``_imp_`` attribute. This + can be used with undefined functions using the ``implemented_function`` + factory: + + >>> f = implemented_function(Function('f'), lambda x: x+1) + >>> func = lambdify(x, f(x)) + >>> func(4) + 5 + + ``lambdify`` always prefers ``_imp_`` implementations to implementations + in other namespaces, unless the ``use_imps`` input parameter is False. + + Usage with Tensorflow: + + >>> import tensorflow as tf + >>> from sympy import Max, sin, lambdify + >>> from sympy.abc import x + + >>> f = Max(x, sin(x)) + >>> func = lambdify(x, f, 'tensorflow') + + After tensorflow v2, eager execution is enabled by default. + If you want to get the compatible result across tensorflow v1 and v2 + as same as this tutorial, run this line. + + >>> tf.compat.v1.enable_eager_execution() + + If you have eager execution enabled, you can get the result out + immediately as you can use numpy. + + If you pass tensorflow objects, you may get an ``EagerTensor`` + object instead of value. + + >>> result = func(tf.constant(1.0)) + >>> print(result) + tf.Tensor(1.0, shape=(), dtype=float32) + >>> print(result.__class__) + + + You can use ``.numpy()`` to get the numpy value of the tensor. + + >>> result.numpy() + 1.0 + + >>> var = tf.Variable(2.0) + >>> result = func(var) # also works for tf.Variable and tf.Placeholder + >>> result.numpy() + 2.0 + + And it works with any shape array. + + >>> tensor = tf.constant([[1.0, 2.0], [3.0, 4.0]]) + >>> result = func(tensor) + >>> result.numpy() + [[1. 2.] + [3. 4.]] + + Notes + ===== + + - For functions involving large array calculations, numexpr can provide a + significant speedup over numpy. Please note that the available functions + for numexpr are more limited than numpy but can be expanded with + ``implemented_function`` and user defined subclasses of Function. If + specified, numexpr may be the only option in modules. The official list + of numexpr functions can be found at: + https://numexpr.readthedocs.io/en/latest/user_guide.html#supported-functions + + - In the above examples, the generated functions can accept scalar + values or numpy arrays as arguments. However, in some cases + the generated function relies on the input being a numpy array: + + >>> import numpy + >>> from sympy import Piecewise + >>> from sympy.testing.pytest import ignore_warnings + >>> f = lambdify(x, Piecewise((x, x <= 1), (1/x, x > 1)), "numpy") + + >>> with ignore_warnings(RuntimeWarning): + ... f(numpy.array([-1, 0, 1, 2])) + [-1. 0. 1. 0.5] + + >>> f(0) + Traceback (most recent call last): + ... + ZeroDivisionError: division by zero + + In such cases, the input should be wrapped in a numpy array: + + >>> with ignore_warnings(RuntimeWarning): + ... float(f(numpy.array([0]))) + 0.0 + + Or if numpy functionality is not required another module can be used: + + >>> f = lambdify(x, Piecewise((x, x <= 1), (1/x, x > 1)), "math") + >>> f(0) + 0 + + .. _lambdify-how-it-works: + + How it works + ============ + + When using this function, it helps a great deal to have an idea of what it + is doing. At its core, lambdify is nothing more than a namespace + translation, on top of a special printer that makes some corner cases work + properly. + + To understand lambdify, first we must properly understand how Python + namespaces work. Say we had two files. One called ``sin_cos_sympy.py``, + with + + .. code:: python + + # sin_cos_sympy.py + + from sympy.functions.elementary.trigonometric import (cos, sin) + + def sin_cos(x): + return sin(x) + cos(x) + + + and one called ``sin_cos_numpy.py`` with + + .. code:: python + + # sin_cos_numpy.py + + from numpy import sin, cos + + def sin_cos(x): + return sin(x) + cos(x) + + The two files define an identical function ``sin_cos``. However, in the + first file, ``sin`` and ``cos`` are defined as the SymPy ``sin`` and + ``cos``. In the second, they are defined as the NumPy versions. + + If we were to import the first file and use the ``sin_cos`` function, we + would get something like + + >>> from sin_cos_sympy import sin_cos # doctest: +SKIP + >>> sin_cos(1) # doctest: +SKIP + cos(1) + sin(1) + + On the other hand, if we imported ``sin_cos`` from the second file, we + would get + + >>> from sin_cos_numpy import sin_cos # doctest: +SKIP + >>> sin_cos(1) # doctest: +SKIP + 1.38177329068 + + In the first case we got a symbolic output, because it used the symbolic + ``sin`` and ``cos`` functions from SymPy. In the second, we got a numeric + result, because ``sin_cos`` used the numeric ``sin`` and ``cos`` functions + from NumPy. But notice that the versions of ``sin`` and ``cos`` that were + used was not inherent to the ``sin_cos`` function definition. Both + ``sin_cos`` definitions are exactly the same. Rather, it was based on the + names defined at the module where the ``sin_cos`` function was defined. + + The key point here is that when function in Python references a name that + is not defined in the function, that name is looked up in the "global" + namespace of the module where that function is defined. + + Now, in Python, we can emulate this behavior without actually writing a + file to disk using the ``exec`` function. ``exec`` takes a string + containing a block of Python code, and a dictionary that should contain + the global variables of the module. It then executes the code "in" that + dictionary, as if it were the module globals. The following is equivalent + to the ``sin_cos`` defined in ``sin_cos_sympy.py``: + + >>> import sympy + >>> module_dictionary = {'sin': sympy.sin, 'cos': sympy.cos} + >>> exec(''' + ... def sin_cos(x): + ... return sin(x) + cos(x) + ... ''', module_dictionary) + >>> sin_cos = module_dictionary['sin_cos'] + >>> sin_cos(1) + cos(1) + sin(1) + + and similarly with ``sin_cos_numpy``: + + >>> import numpy + >>> module_dictionary = {'sin': numpy.sin, 'cos': numpy.cos} + >>> exec(''' + ... def sin_cos(x): + ... return sin(x) + cos(x) + ... ''', module_dictionary) + >>> sin_cos = module_dictionary['sin_cos'] + >>> sin_cos(1) + 1.38177329068 + + So now we can get an idea of how ``lambdify`` works. The name "lambdify" + comes from the fact that we can think of something like ``lambdify(x, + sin(x) + cos(x), 'numpy')`` as ``lambda x: sin(x) + cos(x)``, where + ``sin`` and ``cos`` come from the ``numpy`` namespace. This is also why + the symbols argument is first in ``lambdify``, as opposed to most SymPy + functions where it comes after the expression: to better mimic the + ``lambda`` keyword. + + ``lambdify`` takes the input expression (like ``sin(x) + cos(x)``) and + + 1. Converts it to a string + 2. Creates a module globals dictionary based on the modules that are + passed in (by default, it uses the NumPy module) + 3. Creates the string ``"def func({vars}): return {expr}"``, where ``{vars}`` is the + list of variables separated by commas, and ``{expr}`` is the string + created in step 1., then ``exec``s that string with the module globals + namespace and returns ``func``. + + In fact, functions returned by ``lambdify`` support inspection. So you can + see exactly how they are defined by using ``inspect.getsource``, or ``??`` if you + are using IPython or the Jupyter notebook. + + >>> f = lambdify(x, sin(x) + cos(x)) + >>> import inspect + >>> print(inspect.getsource(f)) + def _lambdifygenerated(x): + return sin(x) + cos(x) + + This shows us the source code of the function, but not the namespace it + was defined in. We can inspect that by looking at the ``__globals__`` + attribute of ``f``: + + >>> f.__globals__['sin'] + + >>> f.__globals__['cos'] + + >>> f.__globals__['sin'] is numpy.sin + True + + This shows us that ``sin`` and ``cos`` in the namespace of ``f`` will be + ``numpy.sin`` and ``numpy.cos``. + + Note that there are some convenience layers in each of these steps, but at + the core, this is how ``lambdify`` works. Step 1 is done using the + ``LambdaPrinter`` printers defined in the printing module (see + :mod:`sympy.printing.lambdarepr`). This allows different SymPy expressions + to define how they should be converted to a string for different modules. + You can change which printer ``lambdify`` uses by passing a custom printer + in to the ``printer`` argument. + + Step 2 is augmented by certain translations. There are default + translations for each module, but you can provide your own by passing a + list to the ``modules`` argument. For instance, + + >>> def mysin(x): + ... print('taking the sin of', x) + ... return numpy.sin(x) + ... + >>> f = lambdify(x, sin(x), [{'sin': mysin}, 'numpy']) + >>> f(1) + taking the sin of 1 + 0.8414709848078965 + + The globals dictionary is generated from the list by merging the + dictionary ``{'sin': mysin}`` and the module dictionary for NumPy. The + merging is done so that earlier items take precedence, which is why + ``mysin`` is used above instead of ``numpy.sin``. + + If you want to modify the way ``lambdify`` works for a given function, it + is usually easiest to do so by modifying the globals dictionary as such. + In more complicated cases, it may be necessary to create and pass in a + custom printer. + + Finally, step 3 is augmented with certain convenience operations, such as + the addition of a docstring. + + Understanding how ``lambdify`` works can make it easier to avoid certain + gotchas when using it. For instance, a common mistake is to create a + lambdified function for one module (say, NumPy), and pass it objects from + another (say, a SymPy expression). + + For instance, say we create + + >>> from sympy.abc import x + >>> f = lambdify(x, x + 1, 'numpy') + + Now if we pass in a NumPy array, we get that array plus 1 + + >>> import numpy + >>> a = numpy.array([1, 2]) + >>> f(a) + [2 3] + + But what happens if you make the mistake of passing in a SymPy expression + instead of a NumPy array: + + >>> f(x + 1) + x + 2 + + This worked, but it was only by accident. Now take a different lambdified + function: + + >>> from sympy import sin + >>> g = lambdify(x, x + sin(x), 'numpy') + + This works as expected on NumPy arrays: + + >>> g(a) + [1.84147098 2.90929743] + + But if we try to pass in a SymPy expression, it fails + + >>> g(x + 1) + Traceback (most recent call last): + ... + TypeError: loop of ufunc does not support argument 0 of type Add which has + no callable sin method + + Now, let's look at what happened. The reason this fails is that ``g`` + calls ``numpy.sin`` on the input expression, and ``numpy.sin`` does not + know how to operate on a SymPy object. **As a general rule, NumPy + functions do not know how to operate on SymPy expressions, and SymPy + functions do not know how to operate on NumPy arrays. This is why lambdify + exists: to provide a bridge between SymPy and NumPy.** + + However, why is it that ``f`` did work? That's because ``f`` does not call + any functions, it only adds 1. So the resulting function that is created, + ``def _lambdifygenerated(x): return x + 1`` does not depend on the globals + namespace it is defined in. Thus it works, but only by accident. A future + version of ``lambdify`` may remove this behavior. + + Be aware that certain implementation details described here may change in + future versions of SymPy. The API of passing in custom modules and + printers will not change, but the details of how a lambda function is + created may change. However, the basic idea will remain the same, and + understanding it will be helpful to understanding the behavior of + lambdify. + + **In general: you should create lambdified functions for one module (say, + NumPy), and only pass it input types that are compatible with that module + (say, NumPy arrays).** Remember that by default, if the ``module`` + argument is not provided, ``lambdify`` creates functions using the NumPy + and SciPy namespaces. + """ + from sympy.core.symbol import Symbol + from sympy.core.expr import Expr + + # If the user hasn't specified any modules, use what is available. + if modules is None: + try: + _import("scipy") + except ImportError: + try: + _import("numpy") + except ImportError: + # Use either numpy (if available) or python.math where possible. + # XXX: This leads to different behaviour on different systems and + # might be the reason for irreproducible errors. + modules = ["math", "mpmath", "sympy"] + else: + modules = ["numpy"] + else: + modules = ["numpy", "scipy"] + + # Get the needed namespaces. + namespaces = [] + # First find any function implementations + if use_imps: + namespaces.append(_imp_namespace(expr)) + # Check for dict before iterating + if isinstance(modules, (dict, str)) or not hasattr(modules, '__iter__'): + namespaces.append(modules) + else: + # consistency check + if _module_present('numexpr', modules) and len(modules) > 1: + raise TypeError("numexpr must be the only item in 'modules'") + namespaces += list(modules) + # fill namespace with first having highest priority + namespace = {} + for m in namespaces[::-1]: + buf = _get_namespace(m) + namespace.update(buf) + + if hasattr(expr, "atoms"): + #Try if you can extract symbols from the expression. + #Move on if expr.atoms in not implemented. + syms = expr.atoms(Symbol) + for term in syms: + namespace.update({str(term): term}) + + if printer is None: + if _module_present('mpmath', namespaces): + from sympy.printing.pycode import MpmathPrinter as Printer # type: ignore + elif _module_present('scipy', namespaces): + from sympy.printing.numpy import SciPyPrinter as Printer # type: ignore + elif _module_present('numpy', namespaces): + from sympy.printing.numpy import NumPyPrinter as Printer # type: ignore + elif _module_present('cupy', namespaces): + from sympy.printing.numpy import CuPyPrinter as Printer # type: ignore + elif _module_present('jax', namespaces): + from sympy.printing.numpy import JaxPrinter as Printer # type: ignore + elif _module_present('numexpr', namespaces): + from sympy.printing.lambdarepr import NumExprPrinter as Printer # type: ignore + elif _module_present('tensorflow', namespaces): + from sympy.printing.tensorflow import TensorflowPrinter as Printer # type: ignore + elif _module_present('torch', namespaces): + from sympy.printing.pytorch import TorchPrinter as Printer # type: ignore + elif _module_present('sympy', namespaces): + from sympy.printing.pycode import SymPyPrinter as Printer # type: ignore + elif _module_present('cmath', namespaces): + from sympy.printing.pycode import CmathPrinter as Printer # type: ignore + else: + from sympy.printing.pycode import PythonCodePrinter as Printer # type: ignore + user_functions = {} + for m in namespaces[::-1]: + if isinstance(m, dict): + for k in m: + user_functions[k] = k + printer = Printer({'fully_qualified_modules': False, 'inline': True, + 'allow_unknown_functions': True, + 'user_functions': user_functions}) + + if isinstance(args, set): + sympy_deprecation_warning( + """ +Passing the function arguments to lambdify() as a set is deprecated. This +leads to unpredictable results since sets are unordered. Instead, use a list +or tuple for the function arguments. + """, + deprecated_since_version="1.6.3", + active_deprecations_target="deprecated-lambdify-arguments-set", + ) + + # Get the names of the args, for creating a docstring + iterable_args = (args,) if isinstance(args, Expr) else args + names = [] + + # Grab the callers frame, for getting the names by inspection (if needed) + callers_local_vars = inspect.currentframe().f_back.f_locals.items() # type: ignore + for n, var in enumerate(iterable_args): + if hasattr(var, 'name'): + names.append(var.name) + else: + # It's an iterable. Try to get name by inspection of calling frame. + name_list = [var_name for var_name, var_val in callers_local_vars + if var_val is var] + if len(name_list) == 1: + names.append(name_list[0]) + else: + # Cannot infer name with certainty. arg_# will have to do. + names.append('arg_' + str(n)) + + # Create the function definition code and execute it + funcname = '_lambdifygenerated' + if _module_present('tensorflow', namespaces): + funcprinter = _TensorflowEvaluatorPrinter(printer, dummify) + else: + funcprinter = _EvaluatorPrinter(printer, dummify) + + if cse == True: + from sympy.simplify.cse_main import cse as _cse + cses, _expr = _cse(expr, list=False) + elif callable(cse): + cses, _expr = cse(expr) + else: + cses, _expr = (), expr + funcstr = funcprinter.doprint(funcname, iterable_args, _expr, cses=cses) + + # Collect the module imports from the code printers. + imp_mod_lines = [] + for mod, keys in (getattr(printer, 'module_imports', None) or {}).items(): + for k in keys: + if k not in namespace: + ln = "from %s import %s" % (mod, k) + try: + exec(ln, {}, namespace) + except ImportError: + # Tensorflow 2.0 has issues with importing a specific + # function from its submodule. + # https://github.com/tensorflow/tensorflow/issues/33022 + ln = "%s = %s.%s" % (k, mod, k) + exec(ln, {}, namespace) + imp_mod_lines.append(ln) + + # Provide lambda expression with builtins, and compatible implementation of range + namespace.update({'builtins':builtins, 'range':range}) + + funclocals = {} + global _lambdify_generated_counter + filename = '' % _lambdify_generated_counter + _lambdify_generated_counter += 1 + c = compile(funcstr, filename, 'exec') + exec(c, namespace, funclocals) + # mtime has to be None or else linecache.checkcache will remove it + linecache.cache[filename] = (len(funcstr), None, funcstr.splitlines(True), filename) # type: ignore + + # Remove the entry from the linecache when the object is garbage collected + def cleanup_linecache(filename): + def _cleanup(): + if filename in linecache.cache: + del linecache.cache[filename] + return _cleanup + + func = funclocals[funcname] + + weakref.finalize(func, cleanup_linecache(filename)) + + # Apply the docstring + sig = "func({})".format(", ".join(str(i) for i in names)) + sig = textwrap.fill(sig, subsequent_indent=' '*8) + if _too_large_for_docstring(expr, docstring_limit): + expr_str = "EXPRESSION REDACTED DUE TO LENGTH, (see lambdify's `docstring_limit`)" + src_str = "SOURCE CODE REDACTED DUE TO LENGTH, (see lambdify's `docstring_limit`)" + else: + expr_str = str(expr) + if len(expr_str) > 78: + expr_str = textwrap.wrap(expr_str, 75)[0] + '...' + src_str = funcstr + func.__doc__ = ( + "Created with lambdify. Signature:\n\n" + "{sig}\n\n" + "Expression:\n\n" + "{expr}\n\n" + "Source code:\n\n" + "{src}\n\n" + "Imported modules:\n\n" + "{imp_mods}" + ).format(sig=sig, expr=expr_str, src=src_str, imp_mods='\n'.join(imp_mod_lines)) + return func + +def _module_present(modname, modlist): + if modname in modlist: + return True + for m in modlist: + if hasattr(m, '__name__') and m.__name__ == modname: + return True + return False + +def _get_namespace(m): + """ + This is used by _lambdify to parse its arguments. + """ + if isinstance(m, str): + _import(m) + return MODULES[m][0] + elif isinstance(m, dict): + return m + elif hasattr(m, "__dict__"): + return m.__dict__ + else: + raise TypeError("Argument must be either a string, dict or module but it is: %s" % m) + + +def _recursive_to_string(doprint, arg): + """Functions in lambdify accept both SymPy types and non-SymPy types such as python + lists and tuples. This method ensures that we only call the doprint method of the + printer with SymPy types (so that the printer safely can use SymPy-methods).""" + from sympy.matrices.matrixbase import MatrixBase + from sympy.core.basic import Basic + + if isinstance(arg, (Basic, MatrixBase)): + return doprint(arg) + elif iterable(arg): + if isinstance(arg, list): + left, right = "[", "]" + elif isinstance(arg, tuple): + left, right = "(", ",)" + if not arg: + return "()" + else: + raise NotImplementedError("unhandled type: %s, %s" % (type(arg), arg)) + return left +', '.join(_recursive_to_string(doprint, e) for e in arg) + right + elif isinstance(arg, str): + return arg + else: + return doprint(arg) + + +def lambdastr(args, expr, printer=None, dummify=None): + """ + Returns a string that can be evaluated to a lambda function. + + Examples + ======== + + >>> from sympy.abc import x, y, z + >>> from sympy.utilities.lambdify import lambdastr + >>> lambdastr(x, x**2) + 'lambda x: (x**2)' + >>> lambdastr((x,y,z), [z,y,x]) + 'lambda x,y,z: ([z, y, x])' + + Although tuples may not appear as arguments to lambda in Python 3, + lambdastr will create a lambda function that will unpack the original + arguments so that nested arguments can be handled: + + >>> lambdastr((x, (y, z)), x + y) + 'lambda _0,_1: (lambda x,y,z: (x + y))(_0,_1[0],_1[1])' + """ + # Transforming everything to strings. + from sympy.matrices import DeferredVector + from sympy.core.basic import Basic + from sympy.core.function import (Derivative, Function) + from sympy.core.symbol import (Dummy, Symbol) + from sympy.core.sympify import sympify + + if printer is not None: + if inspect.isfunction(printer): + lambdarepr = printer + else: + if inspect.isclass(printer): + lambdarepr = lambda expr: printer().doprint(expr) + else: + lambdarepr = lambda expr: printer.doprint(expr) + else: + #XXX: This has to be done here because of circular imports + from sympy.printing.lambdarepr import lambdarepr + + def sub_args(args, dummies_dict): + if isinstance(args, str): + return args + elif isinstance(args, DeferredVector): + return str(args) + elif iterable(args): + dummies = flatten([sub_args(a, dummies_dict) for a in args]) + return ",".join(str(a) for a in dummies) + else: + # replace these with Dummy symbols + if isinstance(args, (Function, Symbol, Derivative)): + dummies = Dummy() + dummies_dict.update({args : dummies}) + return str(dummies) + else: + return str(args) + + def sub_expr(expr, dummies_dict): + expr = sympify(expr) + # dict/tuple are sympified to Basic + if isinstance(expr, Basic): + expr = expr.xreplace(dummies_dict) + # list is not sympified to Basic + elif isinstance(expr, list): + expr = [sub_expr(a, dummies_dict) for a in expr] + return expr + + # Transform args + def isiter(l): + return iterable(l, exclude=(str, DeferredVector, NotIterable)) + + def flat_indexes(iterable): + n = 0 + + for el in iterable: + if isiter(el): + for ndeep in flat_indexes(el): + yield (n,) + ndeep + else: + yield (n,) + + n += 1 + + if dummify is None: + dummify = any(isinstance(a, Basic) and + a.atoms(Function, Derivative) for a in ( + args if isiter(args) else [args])) + + if isiter(args) and any(isiter(i) for i in args): + dum_args = [str(Dummy(str(i))) for i in range(len(args))] + + indexed_args = ','.join([ + dum_args[ind[0]] + ''.join(["[%s]" % k for k in ind[1:]]) + for ind in flat_indexes(args)]) + + lstr = lambdastr(flatten(args), expr, printer=printer, dummify=dummify) + + return 'lambda %s: (%s)(%s)' % (','.join(dum_args), lstr, indexed_args) + + dummies_dict = {} + if dummify: + args = sub_args(args, dummies_dict) + else: + if isinstance(args, str): + pass + elif iterable(args, exclude=DeferredVector): + args = ",".join(str(a) for a in args) + + # Transform expr + if dummify: + if isinstance(expr, str): + pass + else: + expr = sub_expr(expr, dummies_dict) + expr = _recursive_to_string(lambdarepr, expr) + return "lambda %s: (%s)" % (args, expr) + +class _EvaluatorPrinter: + def __init__(self, printer=None, dummify=False): + self._dummify = dummify + + #XXX: This has to be done here because of circular imports + from sympy.printing.lambdarepr import LambdaPrinter + + if printer is None: + printer = LambdaPrinter() + + if inspect.isfunction(printer): + self._exprrepr = printer + else: + if inspect.isclass(printer): + printer = printer() + + self._exprrepr = printer.doprint + + #if hasattr(printer, '_print_Symbol'): + # symbolrepr = printer._print_Symbol + + #if hasattr(printer, '_print_Dummy'): + # dummyrepr = printer._print_Dummy + + # Used to print the generated function arguments in a standard way + self._argrepr = LambdaPrinter().doprint + + def doprint(self, funcname, args, expr, *, cses=()): + """ + Returns the function definition code as a string. + """ + from sympy.core.symbol import Dummy + + funcbody = [] + + if not iterable(args): + args = [args] + + if cses: + cses = list(cses) + subvars, subexprs = zip(*cses) + exprs = [expr] + list(subexprs) + argstrs, exprs = self._preprocess(args, exprs, cses=cses) + expr, subexprs = exprs[0], exprs[1:] + cses = zip(subvars, subexprs) + else: + argstrs, expr = self._preprocess(args, expr) + + # Generate argument unpacking and final argument list + funcargs = [] + unpackings = [] + + for argstr in argstrs: + if iterable(argstr): + funcargs.append(self._argrepr(Dummy())) + unpackings.extend(self._print_unpacking(argstr, funcargs[-1])) + else: + funcargs.append(argstr) + + funcsig = 'def {}({}):'.format(funcname, ', '.join(funcargs)) + + # Wrap input arguments before unpacking + funcbody.extend(self._print_funcargwrapping(funcargs)) + + funcbody.extend(unpackings) + + for s, e in cses: + if e is None: + funcbody.append('del {}'.format(self._exprrepr(s))) + else: + funcbody.append('{} = {}'.format(self._exprrepr(s), self._exprrepr(e))) + + # Subs may appear in expressions generated by .diff() + subs_assignments = [] + expr = self._handle_Subs(expr, out=subs_assignments) + for lhs, rhs in subs_assignments: + funcbody.append('{} = {}'.format(self._exprrepr(lhs), self._exprrepr(rhs))) + + str_expr = _recursive_to_string(self._exprrepr, expr) + + if '\n' in str_expr: + str_expr = '({})'.format(str_expr) + funcbody.append('return {}'.format(str_expr)) + + funclines = [funcsig] + funclines.extend([' ' + line for line in funcbody]) + + return '\n'.join(funclines) + '\n' + + @classmethod + def _is_safe_ident(cls, ident): + return isinstance(ident, str) and ident.isidentifier() \ + and not keyword.iskeyword(ident) + + def _preprocess(self, args, expr, cses=(), _dummies_dict=None): + """Preprocess args, expr to replace arguments that do not map + to valid Python identifiers. + + Returns string form of args, and updated expr. + """ + from sympy.core.basic import Basic + from sympy.core.sorting import ordered + from sympy.core.function import (Derivative, Function) + from sympy.core.symbol import Dummy, uniquely_named_symbol + from sympy.matrices import DeferredVector + from sympy.core.expr import Expr + + # Args of type Dummy can cause name collisions with args + # of type Symbol. Force dummify of everything in this + # situation. + dummify = self._dummify or any( + isinstance(arg, Dummy) for arg in flatten(args)) + + argstrs = [None]*len(args) + if _dummies_dict is None: + _dummies_dict = {} + + def update_dummies(arg, dummy): + _dummies_dict[arg] = dummy + for repl, sub in cses: + arg = arg.xreplace({sub: repl}) + _dummies_dict[arg] = dummy + + for arg, i in reversed(list(ordered(zip(args, range(len(args)))))): + if iterable(arg): + s, expr = self._preprocess(arg, expr, cses=cses, _dummies_dict=_dummies_dict) + elif isinstance(arg, DeferredVector): + s = str(arg) + elif isinstance(arg, Basic) and arg.is_symbol: + s = str(arg) + if dummify or not self._is_safe_ident(s): + dummy = Dummy() + if isinstance(expr, Expr): + dummy = uniquely_named_symbol( + dummy.name, expr, modify=lambda s: '_' + s) + s = self._argrepr(dummy) + update_dummies(arg, dummy) + expr = self._subexpr(expr, _dummies_dict) + elif dummify or isinstance(arg, (Function, Derivative)): + dummy = Dummy() + s = self._argrepr(dummy) + update_dummies(arg, dummy) + expr = self._subexpr(expr, _dummies_dict) + else: + s = str(arg) + argstrs[i] = s + return argstrs, expr + + def _subexpr(self, expr, dummies_dict): + from sympy.matrices import DeferredVector + from sympy.core.sympify import sympify + + expr = sympify(expr) + xreplace = getattr(expr, 'xreplace', None) + if xreplace is not None: + expr = xreplace(dummies_dict) + else: + if isinstance(expr, DeferredVector): + pass + elif isinstance(expr, dict): + k = [self._subexpr(sympify(a), dummies_dict) for a in expr.keys()] + v = [self._subexpr(sympify(a), dummies_dict) for a in expr.values()] + expr = dict(zip(k, v)) + elif isinstance(expr, tuple): + expr = tuple(self._subexpr(sympify(a), dummies_dict) for a in expr) + elif isinstance(expr, list): + expr = [self._subexpr(sympify(a), dummies_dict) for a in expr] + return expr + + def _print_funcargwrapping(self, args): + """Generate argument wrapping code. + + args is the argument list of the generated function (strings). + + Return value is a list of lines of code that will be inserted at + the beginning of the function definition. + """ + return [] + + def _print_unpacking(self, unpackto, arg): + """Generate argument unpacking code. + + arg is the function argument to be unpacked (a string), and + unpackto is a list or nested lists of the variable names (strings) to + unpack to. + """ + def unpack_lhs(lvalues): + return '[{}]'.format(', '.join( + unpack_lhs(val) if iterable(val) else val for val in lvalues)) + + return ['{} = {}'.format(unpack_lhs(unpackto), arg)] + + def _handle_Subs(self, expr, out): + """Any instance of Subs is extracted and returned as assignment pairs.""" + from sympy.core.basic import Basic + from sympy.core.function import Subs + from sympy.core.symbol import Dummy + from sympy.matrices.matrixbase import MatrixBase + + def _replace(ex, variables, point): + safe = {} + for lhs, rhs in zip(variables, point): + dummy = Dummy() + safe[lhs] = dummy + out.append((dummy, rhs)) + return ex.xreplace(safe) + + if isinstance(expr, (Basic, MatrixBase)): + expr = expr.replace(Subs, _replace) + elif iterable(expr): + expr = type(expr)([self._handle_Subs(e, out) for e in expr]) + return expr + +class _TensorflowEvaluatorPrinter(_EvaluatorPrinter): + def _print_unpacking(self, lvalues, rvalue): + """Generate argument unpacking code. + + This method is used when the input value is not iterable, + but can be indexed (see issue #14655). + """ + + def flat_indexes(elems): + n = 0 + + for el in elems: + if iterable(el): + for ndeep in flat_indexes(el): + yield (n,) + ndeep + else: + yield (n,) + + n += 1 + + indexed = ', '.join('{}[{}]'.format(rvalue, ']['.join(map(str, ind))) + for ind in flat_indexes(lvalues)) + + return ['[{}] = [{}]'.format(', '.join(flatten(lvalues)), indexed)] + +def _imp_namespace(expr, namespace=None): + """ Return namespace dict with function implementations + + We need to search for functions in anything that can be thrown at + us - that is - anything that could be passed as ``expr``. Examples + include SymPy expressions, as well as tuples, lists and dicts that may + contain SymPy expressions. + + Parameters + ---------- + expr : object + Something passed to lambdify, that will generate valid code from + ``str(expr)``. + namespace : None or mapping + Namespace to fill. None results in new empty dict + + Returns + ------- + namespace : dict + dict with keys of implemented function names within ``expr`` and + corresponding values being the numerical implementation of + function + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy.utilities.lambdify import implemented_function, _imp_namespace + >>> from sympy import Function + >>> f = implemented_function(Function('f'), lambda x: x+1) + >>> g = implemented_function(Function('g'), lambda x: x*10) + >>> namespace = _imp_namespace(f(g(x))) + >>> sorted(namespace.keys()) + ['f', 'g'] + """ + # Delayed import to avoid circular imports + from sympy.core.function import FunctionClass + if namespace is None: + namespace = {} + # tuples, lists, dicts are valid expressions + if is_sequence(expr): + for arg in expr: + _imp_namespace(arg, namespace) + return namespace + elif isinstance(expr, dict): + for key, val in expr.items(): + # functions can be in dictionary keys + _imp_namespace(key, namespace) + _imp_namespace(val, namespace) + return namespace + # SymPy expressions may be Functions themselves + func = getattr(expr, 'func', None) + if isinstance(func, FunctionClass): + imp = getattr(func, '_imp_', None) + if imp is not None: + name = expr.func.__name__ + if name in namespace and namespace[name] != imp: + raise ValueError('We found more than one ' + 'implementation with name ' + '"%s"' % name) + namespace[name] = imp + # and / or they may take Functions as arguments + if hasattr(expr, 'args'): + for arg in expr.args: + _imp_namespace(arg, namespace) + return namespace + + +def implemented_function(symfunc, implementation): + """ Add numerical ``implementation`` to function ``symfunc``. + + ``symfunc`` can be an ``UndefinedFunction`` instance, or a name string. + In the latter case we create an ``UndefinedFunction`` instance with that + name. + + Be aware that this is a quick workaround, not a general method to create + special symbolic functions. If you want to create a symbolic function to be + used by all the machinery of SymPy you should subclass the ``Function`` + class. + + Parameters + ---------- + symfunc : ``str`` or ``UndefinedFunction`` instance + If ``str``, then create new ``UndefinedFunction`` with this as + name. If ``symfunc`` is an Undefined function, create a new function + with the same name and the implemented function attached. + implementation : callable + numerical implementation to be called by ``evalf()`` or ``lambdify`` + + Returns + ------- + afunc : sympy.FunctionClass instance + function with attached implementation + + Examples + ======== + + >>> from sympy.abc import x + >>> from sympy.utilities.lambdify import implemented_function + >>> from sympy import lambdify + >>> f = implemented_function('f', lambda x: x+1) + >>> lam_f = lambdify(x, f(x)) + >>> lam_f(4) + 5 + """ + # Delayed import to avoid circular imports + from sympy.core.function import UndefinedFunction + # if name, create function to hold implementation + kwargs = {} + if isinstance(symfunc, UndefinedFunction): + kwargs = symfunc._kwargs + symfunc = symfunc.__name__ + if isinstance(symfunc, str): + # Keyword arguments to UndefinedFunction are added as attributes to + # the created class. + symfunc = UndefinedFunction( + symfunc, _imp_=staticmethod(implementation), **kwargs) + elif not isinstance(symfunc, UndefinedFunction): + raise ValueError(filldedent(''' + symfunc should be either a string or + an UndefinedFunction instance.''')) + return symfunc + + +def _too_large_for_docstring(expr, limit): + """Decide whether an ``Expr`` is too large to be fully rendered in a + ``lambdify`` docstring. + + This is a fast alternative to ``count_ops``, which can become prohibitively + slow for large expressions, because in this instance we only care whether + ``limit`` is exceeded rather than counting the exact number of nodes in the + expression. + + Parameters + ========== + expr : ``Expr``, (nested) ``list`` of ``Expr``, or ``Matrix`` + The same objects that can be passed to the ``expr`` argument of + ``lambdify``. + limit : ``int`` or ``None`` + The threshold above which an expression contains too many nodes to be + usefully rendered in the docstring. If ``None`` then there is no limit. + + Returns + ======= + bool + ``True`` if the number of nodes in the expression exceeds the limit, + ``False`` otherwise. + + Examples + ======== + + >>> from sympy.abc import x, y, z + >>> from sympy.utilities.lambdify import _too_large_for_docstring + >>> expr = x + >>> _too_large_for_docstring(expr, None) + False + >>> _too_large_for_docstring(expr, 100) + False + >>> _too_large_for_docstring(expr, 1) + False + >>> _too_large_for_docstring(expr, 0) + True + >>> _too_large_for_docstring(expr, -1) + True + + Does this split it? + + >>> expr = [x, y, z] + >>> _too_large_for_docstring(expr, None) + False + >>> _too_large_for_docstring(expr, 100) + False + >>> _too_large_for_docstring(expr, 1) + True + >>> _too_large_for_docstring(expr, 0) + True + >>> _too_large_for_docstring(expr, -1) + True + + >>> expr = [x, [y], z, [[x+y], [x*y*z, [x+y+z]]]] + >>> _too_large_for_docstring(expr, None) + False + >>> _too_large_for_docstring(expr, 100) + False + >>> _too_large_for_docstring(expr, 1) + True + >>> _too_large_for_docstring(expr, 0) + True + >>> _too_large_for_docstring(expr, -1) + True + + >>> expr = ((x + y + z)**5).expand() + >>> _too_large_for_docstring(expr, None) + False + >>> _too_large_for_docstring(expr, 100) + True + >>> _too_large_for_docstring(expr, 1) + True + >>> _too_large_for_docstring(expr, 0) + True + >>> _too_large_for_docstring(expr, -1) + True + + >>> from sympy import Matrix + >>> expr = Matrix([[(x + y + z), ((x + y + z)**2).expand(), + ... ((x + y + z)**3).expand(), ((x + y + z)**4).expand()]]) + >>> _too_large_for_docstring(expr, None) + False + >>> _too_large_for_docstring(expr, 1000) + False + >>> _too_large_for_docstring(expr, 100) + True + >>> _too_large_for_docstring(expr, 1) + True + >>> _too_large_for_docstring(expr, 0) + True + >>> _too_large_for_docstring(expr, -1) + True + + """ + # Must be imported here to avoid a circular import error + from sympy.core.traversal import postorder_traversal + + if limit is None: + return False + + i = 0 + for _ in postorder_traversal(expr): + i += 1 + if i > limit: + return True + return False diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/magic.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/magic.py new file mode 100644 index 0000000000000000000000000000000000000000..e853a0ad9a85bc252dcb24e8a1ecbfca422ac3fd --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/magic.py @@ -0,0 +1,12 @@ +"""Functions that involve magic. """ + +def pollute(names, objects): + """Pollute the global namespace with symbols -> objects mapping. """ + from inspect import currentframe + frame = currentframe().f_back.f_back + + try: + for name, obj in zip(names, objects): + frame.f_globals[name] = obj + finally: + del frame # break cyclic dependencies as stated in inspect docs diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/matchpy_connector.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/matchpy_connector.py new file mode 100644 index 0000000000000000000000000000000000000000..35aa013294b93bbcfe4a1bf4ec96b629ea5a468f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/matchpy_connector.py @@ -0,0 +1,340 @@ +""" +The objects in this module allow the usage of the MatchPy pattern matching +library on SymPy expressions. +""" +import re +from typing import List, Callable, NamedTuple, Any, Dict + +from sympy.core.sympify import _sympify +from sympy.external import import_module +from sympy.functions import (log, sin, cos, tan, cot, csc, sec, erf, gamma, uppergamma) +from sympy.functions.elementary.hyperbolic import acosh, asinh, atanh, acoth, acsch, asech, cosh, sinh, tanh, coth, sech, csch +from sympy.functions.elementary.trigonometric import atan, acsc, asin, acot, acos, asec +from sympy.functions.special.error_functions import fresnelc, fresnels, erfc, erfi, Ei +from sympy.core.add import Add +from sympy.core.basic import Basic +from sympy.core.expr import Expr +from sympy.core.mul import Mul +from sympy.core.power import Pow +from sympy.core.relational import (Equality, Unequality) +from sympy.core.symbol import Symbol +from sympy.functions.elementary.exponential import exp +from sympy.integrals.integrals import Integral +from sympy.printing.repr import srepr +from sympy.utilities.decorator import doctest_depends_on + + +matchpy = import_module("matchpy") + + +__doctest_requires__ = {('*',): ['matchpy']} + + +if matchpy: + from matchpy import Operation, CommutativeOperation, AssociativeOperation, OneIdentityOperation + from matchpy.expressions.functions import op_iter, create_operation_expression, op_len + + Operation.register(Integral) + Operation.register(Pow) + OneIdentityOperation.register(Pow) + + Operation.register(Add) + OneIdentityOperation.register(Add) + CommutativeOperation.register(Add) + AssociativeOperation.register(Add) + + Operation.register(Mul) + OneIdentityOperation.register(Mul) + CommutativeOperation.register(Mul) + AssociativeOperation.register(Mul) + + Operation.register(Equality) + CommutativeOperation.register(Equality) + Operation.register(Unequality) + CommutativeOperation.register(Unequality) + + Operation.register(exp) + Operation.register(log) + Operation.register(gamma) + Operation.register(uppergamma) + Operation.register(fresnels) + Operation.register(fresnelc) + Operation.register(erf) + Operation.register(Ei) + Operation.register(erfc) + Operation.register(erfi) + Operation.register(sin) + Operation.register(cos) + Operation.register(tan) + Operation.register(cot) + Operation.register(csc) + Operation.register(sec) + Operation.register(sinh) + Operation.register(cosh) + Operation.register(tanh) + Operation.register(coth) + Operation.register(csch) + Operation.register(sech) + Operation.register(asin) + Operation.register(acos) + Operation.register(atan) + Operation.register(acot) + Operation.register(acsc) + Operation.register(asec) + Operation.register(asinh) + Operation.register(acosh) + Operation.register(atanh) + Operation.register(acoth) + Operation.register(acsch) + Operation.register(asech) + + @op_iter.register(Integral) # type: ignore + def _(operation): + return iter((operation._args[0],) + operation._args[1]) + + @op_iter.register(Basic) # type: ignore + def _(operation): + return iter(operation._args) + + @op_len.register(Integral) # type: ignore + def _(operation): + return 1 + len(operation._args[1]) + + @op_len.register(Basic) # type: ignore + def _(operation): + return len(operation._args) + + @create_operation_expression.register(Basic) + def sympy_op_factory(old_operation, new_operands, variable_name=True): + return type(old_operation)(*new_operands) + + +if matchpy: + from matchpy import Wildcard +else: + class Wildcard: # type: ignore + def __init__(self, min_length, fixed_size, variable_name, optional): + self.min_count = min_length + self.fixed_size = fixed_size + self.variable_name = variable_name + self.optional = optional + + +@doctest_depends_on(modules=('matchpy',)) +class _WildAbstract(Wildcard, Symbol): + min_length: int # abstract field required in subclasses + fixed_size: bool # abstract field required in subclasses + + def __init__(self, variable_name=None, optional=None, **assumptions): + min_length = self.min_length + fixed_size = self.fixed_size + if optional is not None: + optional = _sympify(optional) + Wildcard.__init__(self, min_length, fixed_size, str(variable_name), optional) + + def __getstate__(self): + return { + "min_length": self.min_length, + "fixed_size": self.fixed_size, + "min_count": self.min_count, + "variable_name": self.variable_name, + "optional": self.optional, + } + + def __new__(cls, variable_name=None, optional=None, **assumptions): + cls._sanitize(assumptions, cls) + return _WildAbstract.__xnew__(cls, variable_name, optional, **assumptions) + + def __getnewargs__(self): + return self.variable_name, self.optional + + @staticmethod + def __xnew__(cls, variable_name=None, optional=None, **assumptions): + obj = Symbol.__xnew__(cls, variable_name, **assumptions) + return obj + + def _hashable_content(self): + if self.optional: + return super()._hashable_content() + (self.min_count, self.fixed_size, self.variable_name, self.optional) + else: + return super()._hashable_content() + (self.min_count, self.fixed_size, self.variable_name) + + def __copy__(self) -> '_WildAbstract': + return type(self)(variable_name=self.variable_name, optional=self.optional) + + def __repr__(self): + return str(self) + + def __str__(self): + return self.name + + +@doctest_depends_on(modules=('matchpy',)) +class WildDot(_WildAbstract): + min_length = 1 + fixed_size = True + + +@doctest_depends_on(modules=('matchpy',)) +class WildPlus(_WildAbstract): + min_length = 1 + fixed_size = False + + +@doctest_depends_on(modules=('matchpy',)) +class WildStar(_WildAbstract): + min_length = 0 + fixed_size = False + + +def _get_srepr(expr): + s = srepr(expr) + s = re.sub(r"WildDot\('(\w+)'\)", r"\1", s) + s = re.sub(r"WildPlus\('(\w+)'\)", r"*\1", s) + s = re.sub(r"WildStar\('(\w+)'\)", r"*\1", s) + return s + + +class ReplacementInfo(NamedTuple): + replacement: Any + info: Any + + +@doctest_depends_on(modules=('matchpy',)) +class Replacer: + """ + Replacer object to perform multiple pattern matching and subexpression + replacements in SymPy expressions. + + Examples + ======== + + Example to construct a simple first degree equation solver: + + >>> from sympy.utilities.matchpy_connector import WildDot, Replacer + >>> from sympy import Equality, Symbol + >>> x = Symbol("x") + >>> a_ = WildDot("a_", optional=1) + >>> b_ = WildDot("b_", optional=0) + + The lines above have defined two wildcards, ``a_`` and ``b_``, the + coefficients of the equation `a x + b = 0`. The optional values specified + indicate which expression to return in case no match is found, they are + necessary in equations like `a x = 0` and `x + b = 0`. + + Create two constraints to make sure that ``a_`` and ``b_`` will not match + any expression containing ``x``: + + >>> from matchpy import CustomConstraint + >>> free_x_a = CustomConstraint(lambda a_: not a_.has(x)) + >>> free_x_b = CustomConstraint(lambda b_: not b_.has(x)) + + Now create the rule replacer with the constraints: + + >>> replacer = Replacer(common_constraints=[free_x_a, free_x_b]) + + Add the matching rule: + + >>> replacer.add(Equality(a_*x + b_, 0), -b_/a_) + + Let's try it: + + >>> replacer.replace(Equality(3*x + 4, 0)) + -4/3 + + Notice that it will not match equations expressed with other patterns: + + >>> eq = Equality(3*x, 4) + >>> replacer.replace(eq) + Eq(3*x, 4) + + In order to extend the matching patterns, define another one (we also need + to clear the cache, because the previous result has already been memorized + and the pattern matcher will not iterate again if given the same expression) + + >>> replacer.add(Equality(a_*x, b_), b_/a_) + >>> replacer._matcher.clear() + >>> replacer.replace(eq) + 4/3 + """ + + def __init__(self, common_constraints: list = [], lambdify: bool = False, info: bool = False): + self._matcher = matchpy.ManyToOneMatcher() + self._common_constraint = common_constraints + self._lambdify = lambdify + self._info = info + self._wildcards: Dict[str, Wildcard] = {} + + def _get_lambda(self, lambda_str: str) -> Callable[..., Expr]: + exec("from sympy import *") + return eval(lambda_str, locals()) + + def _get_custom_constraint(self, constraint_expr: Expr, condition_template: str) -> Callable[..., Expr]: + wilds = [x.name for x in constraint_expr.atoms(_WildAbstract)] + lambdaargs = ', '.join(wilds) + fullexpr = _get_srepr(constraint_expr) + condition = condition_template.format(fullexpr) + return matchpy.CustomConstraint( + self._get_lambda(f"lambda {lambdaargs}: ({condition})")) + + def _get_custom_constraint_nonfalse(self, constraint_expr: Expr) -> Callable[..., Expr]: + return self._get_custom_constraint(constraint_expr, "({}) != False") + + def _get_custom_constraint_true(self, constraint_expr: Expr) -> Callable[..., Expr]: + return self._get_custom_constraint(constraint_expr, "({}) == True") + + def add(self, expr: Expr, replacement, conditions_true: List[Expr] = [], + conditions_nonfalse: List[Expr] = [], info: Any = None) -> None: + expr = _sympify(expr) + replacement = _sympify(replacement) + constraints = self._common_constraint[:] + constraint_conditions_true = [ + self._get_custom_constraint_true(cond) for cond in conditions_true] + constraint_conditions_nonfalse = [ + self._get_custom_constraint_nonfalse(cond) for cond in conditions_nonfalse] + constraints.extend(constraint_conditions_true) + constraints.extend(constraint_conditions_nonfalse) + pattern = matchpy.Pattern(expr, *constraints) + if self._lambdify: + lambda_str = f"lambda {', '.join((x.name for x in expr.atoms(_WildAbstract)))}: {_get_srepr(replacement)}" + lambda_expr = self._get_lambda(lambda_str) + replacement = lambda_expr + else: + self._wildcards.update({str(i): i for i in expr.atoms(Wildcard)}) + if self._info: + replacement = ReplacementInfo(replacement, info) + self._matcher.add(pattern, replacement) + + def replace(self, expression, max_count: int = -1): + # This method partly rewrites the .replace method of ManyToOneReplacer + # in MatchPy. + # License: https://github.com/HPAC/matchpy/blob/master/LICENSE + infos = [] + replaced = True + replace_count = 0 + while replaced and (max_count < 0 or replace_count < max_count): + replaced = False + for subexpr, pos in matchpy.preorder_iter_with_position(expression): + try: + replacement_data, subst = next(iter(self._matcher.match(subexpr))) + if self._info: + replacement = replacement_data.replacement + infos.append(replacement_data.info) + else: + replacement = replacement_data + + if self._lambdify: + result = replacement(**subst) + else: + result = replacement.xreplace({self._wildcards[k]: v for k, v in subst.items()}) + + expression = matchpy.functions.replace(expression, pos, result) + replaced = True + break + except StopIteration: + pass + replace_count += 1 + if self._info: + return expression, infos + else: + return expression diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/mathml/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/mathml/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..eded44ee3c0f34ad1324765ba06ee9d6eb5e9899 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/mathml/__init__.py @@ -0,0 +1,122 @@ +"""Module with some functions for MathML, like transforming MathML +content in MathML presentation. + +To use this module, you will need lxml. +""" + +from pathlib import Path + +from sympy.utilities.decorator import doctest_depends_on + + +__doctest_requires__ = {('apply_xsl', 'c2p'): ['lxml']} + + +def add_mathml_headers(s): + return """""" + s + "" + + +def _read_binary(pkgname, filename): + import sys + + if sys.version_info >= (3, 10): + # files was added in Python 3.9 but only seems to work here in 3.10+ + from importlib.resources import files + return files(pkgname).joinpath(filename).read_bytes() + else: + # read_binary was deprecated in Python 3.11 + from importlib.resources import read_binary + return read_binary(pkgname, filename) + + +def _read_xsl(xsl): + # Previously these values were allowed: + if xsl == 'mathml/data/simple_mmlctop.xsl': + xsl = 'simple_mmlctop.xsl' + elif xsl == 'mathml/data/mmlctop.xsl': + xsl = 'mmlctop.xsl' + elif xsl == 'mathml/data/mmltex.xsl': + xsl = 'mmltex.xsl' + + if xsl in ['simple_mmlctop.xsl', 'mmlctop.xsl', 'mmltex.xsl']: + xslbytes = _read_binary('sympy.utilities.mathml.data', xsl) + else: + xslbytes = Path(xsl).read_bytes() + + return xslbytes + + +@doctest_depends_on(modules=('lxml',)) +def apply_xsl(mml, xsl): + """Apply a xsl to a MathML string. + + Parameters + ========== + + mml + A string with MathML code. + xsl + A string giving the name of an xsl (xml stylesheet) file which can be + found in sympy/utilities/mathml/data. The following files are supplied + with SymPy: + + - mmlctop.xsl + - mmltex.xsl + - simple_mmlctop.xsl + + Alternatively, a full path to an xsl file can be given. + + Examples + ======== + + >>> from sympy.utilities.mathml import apply_xsl + >>> xsl = 'simple_mmlctop.xsl' + >>> mml = ' a b ' + >>> res = apply_xsl(mml,xsl) + >>> print(res) + + + a + + + b + + """ + from lxml import etree + + parser = etree.XMLParser(resolve_entities=False) + ac = etree.XSLTAccessControl.DENY_ALL + + s = etree.XML(_read_xsl(xsl), parser=parser) + transform = etree.XSLT(s, access_control=ac) + doc = etree.XML(mml, parser=parser) + result = transform(doc) + s = str(result) + return s + + +@doctest_depends_on(modules=('lxml',)) +def c2p(mml, simple=False): + """Transforms a document in MathML content (like the one that sympy produces) + in one document in MathML presentation, more suitable for printing, and more + widely accepted + + Examples + ======== + + >>> from sympy.utilities.mathml import c2p + >>> mml = ' 2 ' + >>> c2p(mml,simple=True) != c2p(mml,simple=False) + True + + """ + + if not mml.startswith(' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + e + + + + + + + + + + + + + - + + + + + + + + &#x2062; + &#x2148; + + + + + + + + + + + + + - + + + + + + + + &#x2062; + &#x2148; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Polar + &#x2062; + + + + + + + + + + + + + + + Polar + &#x2062; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x2061; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [ + + + ] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x03BB; + &#x2061; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x2218; + + + + + + + + + + + + + + + + + + &#x2218; + + + + + + + + + + + + + + + + id + + + id + + + + + + + + + + + + + + domain + + + codomain + + + image + + &#x2061; + + + + + + + + + + + + + + + + + + + + + + + + + + + { + + + + + + + + if + + + + + + + + + + + otherwise + + + + + + + + + + + + + + + + + + + + + + + &#x230A; + + + + + + + + + + + + + + + + + + + &#x230B; + + + + + + + + + + + + &#x2147; + + + + + + + + + + + + + + + + + ! + + + + + + + + + + + + + + + + max + + + min + + + + + + + max + + + min + + + + + + + + + + + + + | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x2062; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + gcd + + + lcm + + + + + + gcd + + + lcm + + + + + &#x2061; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x2227; + &#x2061; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x2228; + &#x2061; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x22BB; + &#x2061; + + + + + + + + + + + + + + + + + + + + + + + &#x00AC; + &#x2061; + + + + + + + + + + + + + + + &#x00AC; + &#x2061; + + + + + + + + + + + + + + + + + + + &#x2200; + + + + + + + + + + + + : + + + + , + + + + + + + + + + + + + + &#x2203; + + + + + + + + + + + + : + + + + , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x00AF; + + + + + + + + + + + + + + + + + &#x211C; + + + &#x2111; + + + &#x2061; + + + + + + + + + + + + + + + + + &#x230A; + + + &#x2308; + + + + + + &#x230B; + + + &#x2309; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x2260; + + + &#x2248; + + + &#x2223; + + + + + &#x2198; + + + &#x2197; + + + &#x2192; + + + + + &#x21D2; + + + &#x2208; + + + &#x2209; + + + &#x2284; + + + &#x2288; + + + + + + + + + + &#x2286; + + + &#x2282; + + + + + + + + + + + + &#x2265; + + + &#x2264; + + + &#x2261; + + + + + + + + + + + + + + + + + + + + + + ln + + + + + ln + + + + + + + + + + + + + + + + + + + + + + log + + + + + + log + + + + + + + + log + + + + log + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x2146; + + &#x2146; + + + + + + + + &#x2146; + + + + &#x2146; + + + + + + + + + + + + + + + + + + &#x2032; + + + + + + + + + + + + + + + + + &#x2145; + + + + + + + + &#x2202; + + + + + &#x2202; + + + + + + + + + + + + + + + + + + + &#x2202; + + + + &#x2202; + + + + + + + + + + &#x2202; + + &#x2202; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x2207; + 2 + + &#x2061; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x222A; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x2229; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x00D7; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x2211; + + + &#x220F; + + + + + = + + + + + + + + + + + &#x2211; + + + &#x220F; + + + + + + + + + + + &#x2211; + + + &#x220F; + + + + + + + + + + + + + + + + + + + + + + + + &#x222B; + + + + + + &#x222B; + + + + + + &#x222B; + + + + + + + + + + + + &#x222B; + + + + + + + + + &#x222B; + + + + + + + + + &#x2146; + + + + + + + + + + + + + + + + lim + + + + &#x2192; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x03C3; + + + + + + + + + + + + + + + + + &#x03C3; + + + + + + + 2 + + + + + + + + + + + + + median + + + + + + + + + + + + + + + + + mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + det + + + + + + + + + + + + + + + T + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x00D7; + + + &#x22C5; + + + &#x2297; + + + + + + + + + + + + &#x2124; + + + + &#x211D; + + + + &#x211A; + + + + &#x2115; + + + + &#x2102; + + + + &#x2119; + + + + &#x2147; + + + + &#x2148; + + + + NaN + + + + true + + + + false + + + + &#x2205; + + + + &#x03C0; + + + + &#x213D; + + + + &#x221E; + + + diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/mathml/data/mmltex.xsl b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/mathml/data/mmltex.xsl new file mode 100644 index 0000000000000000000000000000000000000000..5e6b85e02efd5196fe76b6ce4e10def27b9a8497 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/mathml/data/mmltex.xsl @@ -0,0 +1,2360 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $ + + $ + + + + + + + + + + + + + + + + i + + + + + / + + + + + + _{} + + + + + e^{i + + } + + + + + E + + + + + + + + \mathrm{} + + + + + + + + + + + + + ( + + + , + + ) + + + + + () + + + + + + + \left( + + \left[ + + + , + + + + \right) + + \right] + + + + + \left\{\right\} + + + + + ^{(-1)} + + + + + + + + \mathrm{lambda}\: + + .\: + + + + + + + + + + \circ + + + + +\mathrm{id} + + + + \mathop{\mathrm{ + + }} + + + + + + + + \begin{cases} + + + \end{cases} + + + + + & \text{if $ + + $} + \\ + + + + + & \text{otherwise} + + + + + \left\lfloor\frac{ + + }{ + + }\right\rfloor + + + + + + + + ! + + + + + + + \left( + \frac{ + + + }{ + + + } + \right) + + + + + \ + + \{ + + + + , + + + + + + , + + + + \} + + + + + - + + + + + + + + + - + + + + + + + + + + ( + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) + + + + + + + + + ^{ + + + + } + + + + + + + \mod + + + + + + + + + + ( + + + + \times + + + + + + + + + + ) + + + + + \sqrt + + [ + + ] + + { + + } + + + +\gcd + + + + + + + + \land + + + + + + + + + + \lor + + + + + + + + + + \mathop{\mathrm{xor}} + + + + + + \neg + + + + + + + + + + \implies + + + + + + + + \ + + + + + , + + + \colon + + + + + + + \left| + + \right| + + + + + \overline{} + + + +\Re + + +\Im + + + + \left\lfloor + + \right\rfloor + + + + + \left\lceil + + \right\rceil + + + + + + + + + = + + + + + + + + + + \neq + + + + + + + + + + > + + + + + + + + + + < + + + + + + + + + + \ge + + + + + + + + + + \le + + + + + + + + + + \equiv + + + + + + + + + + \approx + + + + + + + + | + + + + + + + + \int + + _{ + + } + + + ^{ + + } + + + + \,d + + + + + + + ^\prime + + + + \frac{ + + + d^{ + + } + + }{d + + ^{ + + } + + + d + + }{d + + } + + + } + + + + + D_{ + + + , + + } + + + + + \frac{\partial^{ + + + + + + + + + + + + + + + + + + + + + } + + }{ + + \partial + + + ^{ + + } + + + } + + + + + + + + + , + + + +\mathop{\mathrm{div}} + + +\nabla^2 + + + + \{\} + + + + + \left[\right] + + + + + + + \colon + + + + + + , + + + + + + + + + + + + \cup + + + + + + + + + + \cap + + + + + + + + \in + + + + + + + + + + \notin + + + + + + + + + + + + \subseteq + + + + + + + + + + \subset + + + + + + + + + + \nsubseteq + + + + + + + + + + \not\subset + + + + + + + + + + \setminus + + + + + + | + + | + + + + + + + + + \times + + + + + + + + ^{ + + } + + + + + \sum + + + + + \prod + + + + + _{ + + + = + + + } + + + ^{ + + } + + + + + + + + \lim_{ + + } + + + + + + \to + + + + + + + + + + + + \searrow + \nearrow + \rightarrow + \to + + + + + + + + \ + + + + + + + + + \ + + + + + + \mathrm{ + + \,} + + + + + + + \mathrm{ + + } + + + + + e^{} + + + + + \lg + + + + + + + \log_{ + + } + + + + + + + + \left\langle + + + , + + \right\rangle + + + +\sigma + + + + \sigma( + + )^2 + + + + + \left\langle + + ^{ + + }\right\rangle + + _{ + + } + + + + + + + \left(\begin{array}{c} + + + \\ + + \end{array}\right) + + + + + \begin{pmatrix} + + \end{pmatrix} + + + + + + + & + + \\ + + + + + \det + + + + + + + \begin{vmatrix} + + \end{vmatrix} + + + + + + + + ^T + + + + + + + + _{ + + + , + + } + + + + + + + + + \dot + + + + + + + + + + + +\mathbb{Z} + + +\mathbb{R} + + +\mathbb{Q} + + +\mathbb{N} + + +\mathbb{C} + + +\mathbb{P} + + +e + + +i + + +NaN + + +\mbox{true} + + +\mbox{false} + + +\emptyset + + +\pi + + +\gamma + + +\infty + + + + + + + ( + + + + + + + + + ) + + + + + + + ( + + + + + + + + ) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \multicolumn{ + + }{c}{ + + } + + & + + + + + + + \hfill + + + + \hfill + + + + & + + + + + + + \\ + + + + + \begin{array}{ + + | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | + + } + + \hline + + + + \\ \hline + + \end{array} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \overline{ + + + + + } + + + \overbrace{ + + + + + } + + + \underline{ + + + + + + } + + + \underbrace{ + + + + + + } + + + + + _{ + + }^{ + + } + + + \underset{ + + }{\overset{ + + }{ + + }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \overline{ + + } + + + \overbrace{ + + } + + + + + ^{ + + } + + + \stackrel{ + + }{ + + } + + + + + + + + + + + \underline{ + + } + + + \underbrace{ + + } + + + + + _{ + + } + + + \underset{ + + }{ + + } + + + + + + { + + }_{ + + }^{ + + } + + + + { + + }^{ + + } + + + + { + + }_{ + + } + + + + + + {}_{ + + } + + + {}^{ + + } + + + + + + {} + + + _{ + + } + + + ^{ + + } + + + + + + + + + + + + + + {} + + + _{ + + } + + + ^{ + + } + + + + + + + + + + + + + + + + + + \genfrac{}{}{ + + + + ex + + + .05ex + + + + .2ex + + + + + + }{}{ + + + \frac{ + + + + \hfill + + + + \hfill + + }{ + + \hfill + + + + \hfill + + } + + + + + + \sqrt[ + + ]{ + + } + + + + exception 25: + \text{exception 25:} + + + + + + \sqrt{ + + } + + + + + + + \left + + + \ + + + + \left( + + + + + + + + + + + , + + + + + + + + + + + + + + + + + + + + + + + + \right + + + \ + + + + \right) + + + + + \phantom{ + + } + + + + + + \overline{ + + \hspace{.2em}|} + + + \sqrt{ + + } + + + \overline{) + + } + + + + + + + + + + + \colorbox[rgb]{ + + + + }{$ + + + \textcolor[rgb]{ + + + + }{ + + + + } + + + $} + + + + + + + + + + + + + + + + + + + + + \mathrm{ + + } + + + + + + + + + + + + + + + + + + + + + + \text{ + + } + + + + \phantom{\rule + + [- + + ] + + { + + 0ex + + + }{ + + 0ex + + + }} + + + + + + " + + + " + + + + + + \colorbox[rgb]{ + + + + }{$ + + + \textcolor[rgb]{ + + + + }{ + + + + + \mathrm{ + + + \mathbf{ + + + \mathit{ + + + \mathbit{ + + + \mathbb{ + + + { + + + \mathcal{ + + + \mathsc{ + + + \mathfrak{ + + + \mathsf{ + + + \mathbsf{ + + + \mathsfit{ + + + \mathbsfit{ + + + \mathtt{ + + + { + + + + + + } + + + } + + + $} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + , + + + + + + , + + + + + + + + + + + + + + + + + + + , + + + + + + + + + + + , + + + + + + + + + + + + + + 0,1,1 + 0,0,0 + 0,0,1 + 1,0,1 + .5,.5,.5 + 0,.5,0 + 0,1,0 + .5,0,0 + 0,0,.5 + .5,.5,0 + .5,0,.5 + 1,0,0 + .75,.75,.75 + 0,.5,.5 + 1,1,1 + 1,1,0 + + Exception at color template + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Exception at Hex2Decimal template + + + + + + + + + + + diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/mathml/data/simple_mmlctop.xsl b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/mathml/data/simple_mmlctop.xsl new file mode 100644 index 0000000000000000000000000000000000000000..0cd73bccc24c963ed9c6c4121cc410997b94261c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/mathml/data/simple_mmlctop.xsl @@ -0,0 +1,3166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + e + + + + + + + + + + + + + - + + + + + + + + &#x2062; + &#x2148; + + + + + + + + + + + + + - + + + + + + + + &#x2062; + &#x2148; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Polar + &#x2062; + + + + + + + + + + + + + + + Polar + &#x2062; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x2061; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [ + + + ] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x03BB; + &#x2061; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x2218; + + + + + + + + + + + + + + + + + + &#x2218; + + + + + + + + + + + + + + + + id + + + id + + + + + + + + + + + + + + domain + + + codomain + + + image + + &#x2061; + + + + + + + + + + + + + + + + + + + + + + + + + + + { + + + + + + + + if + + + + + + + + + + + otherwise + + + + + + + + + + + + + + + + + + + + + + + &#x230A; + + + + + + + + + + + + + + + + + + + &#x230B; + + + + + + + + + + + + e + + + + + + + + + + + + + + + + + ! + + + + + + + + + + + + + + + + max + + + min + + + + + + + max + + + min + + + + + + + + + + + + + | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x2062; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + gcd + + + lcm + + + + + + gcd + + + lcm + + + + + &#x2061; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x2227; + &#x2061; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x2228; + &#x2061; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x22BB; + &#x2061; + + + + + + + + + + + + + + + + + + + + + + + &#x00AC; + &#x2061; + + + + + + + + + + + + + + + &#x00AC; + &#x2061; + + + + + + + + + + + + + + + + + + + &#x2200; + + + + + + + + + + + + : + + + + , + + + + + + + + + + + + + + &#x2203; + + + + + + + + + + + + : + + + + , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x00AF; + + + + + + + + + + + + + + + + + &#x211C; + + + &#x2111; + + + &#x2061; + + + + + + + + + + + + + + + + + &#x230A; + + + &#x2308; + + + + + + &#x230B; + + + &#x2309; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x2260; + + + &#x2248; + + + &#x2223; + + + + + &#x2198; + + + &#x2197; + + + &#x2192; + + + + + &#x21D2; + + + &#x2208; + + + &#x2209; + + + &#x2284; + + + &#x2288; + + + + + + + + + + &#x2286; + + + &#x2282; + + + + + + + + + + + + &#x2265; + + + &#x2264; + + + &#x2261; + + + + + + + + + + + + + + + + + + + + + + ln + + + + + ln + + + + + + + + + + + + + + + + + + + + + + log + + + + + + log + + + + + + + + log + + + + log + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + d + + d + + + + + + + + d + + + + d + + + + + + + + + + + + + + + + + + &#x2032; + + + + + + + + + + + + + + + + + &#x2145; + + + + + + + + &#x2202; + + + + + &#x2202; + + + + + + + + + + + + + + + + + + + &#x2202; + + + + &#x2202; + + + + + + + + + + &#x2202; + + &#x2202; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x2207; + 2 + + &#x2061; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x222A; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x2229; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x00D7; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x2211; + + + &#x220F; + + + + + = + + + + + + + + + + + &#x2211; + + + &#x220F; + + + + + + + + + + + &#x2211; + + + &#x220F; + + + + + + + + + + + + + + + + + + + + + + + + &#x222B; + + + + + + &#x222B; + + + + + + &#x222B; + + + + + + + + + + + + &#x222B; + + + + + + + + + &#x222B; + + + + + + + + + d + + + + + + + + + + + + + + + + lim + + + + &#x2192; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x03C3; + + + + + + + + + + + + + + + + + &#x03C3; + + + + + + + 2 + + + + + + + + + + + + + median + + + + + + + + + + + + + + + + + mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + det + + + + + + + + + + + + + + + T + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + &#x00D7; + + + &#x22C5; + + + &#x2297; + + + + + + + + + + + + &#x2124; + + + + &#x211D; + + + + &#x211A; + + + + &#x2115; + + + + &#x2102; + + + + &#x2119; + + + + e + + + + &#x2148; + + + + NaN + + + + true + + + + false + + + + &#x2205; + + + + &#x03C0; + + + + &#x213D; + + + + &#x221E; + + + diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/memoization.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/memoization.py new file mode 100644 index 0000000000000000000000000000000000000000..b638dfe244628096108ea689b664782f6538b7b8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/memoization.py @@ -0,0 +1,76 @@ +from functools import wraps + + +def recurrence_memo(initial): + """ + Memo decorator for sequences defined by recurrence + + Examples + ======== + + >>> from sympy.utilities.memoization import recurrence_memo + >>> @recurrence_memo([1]) # 0! = 1 + ... def factorial(n, prev): + ... return n * prev[-1] + >>> factorial(4) + 24 + >>> factorial(3) # use cache values + 6 + >>> factorial.cache_length() # cache length can be obtained + 5 + >>> factorial.fetch_item(slice(2, 4)) + [2, 6] + + """ + cache = initial + + def decorator(f): + @wraps(f) + def g(n): + L = len(cache) + if n < L: + return cache[n] + for i in range(L, n + 1): + cache.append(f(i, cache)) + return cache[-1] + g.cache_length = lambda: len(cache) + g.fetch_item = lambda x: cache[x] + return g + return decorator + + +def assoc_recurrence_memo(base_seq): + """ + Memo decorator for associated sequences defined by recurrence starting from base + + base_seq(n) -- callable to get base sequence elements + + XXX works only for Pn0 = base_seq(0) cases + XXX works only for m <= n cases + """ + + cache = [] + + def decorator(f): + @wraps(f) + def g(n, m): + L = len(cache) + if n < L: + return cache[n][m] + + for i in range(L, n + 1): + # get base sequence + F_i0 = base_seq(i) + F_i_cache = [F_i0] + cache.append(F_i_cache) + + # XXX only works for m <= n cases + # generate assoc sequence + for j in range(1, i + 1): + F_ij = f(i, j, cache) + F_i_cache.append(F_ij) + + return cache[n][m] + + return g + return decorator diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/misc.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/misc.py new file mode 100644 index 0000000000000000000000000000000000000000..741b983f03272890b22f2545f67b141767507634 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/misc.py @@ -0,0 +1,564 @@ +"""Miscellaneous stuff that does not really fit anywhere else.""" + +from __future__ import annotations + +import operator +import sys +import os +import re as _re +import struct +from textwrap import fill, dedent + + +class Undecidable(ValueError): + # an error to be raised when a decision cannot be made definitively + # where a definitive answer is needed + pass + + +def filldedent(s, w=70, **kwargs): + """ + Strips leading and trailing empty lines from a copy of ``s``, then dedents, + fills and returns it. + + Empty line stripping serves to deal with docstrings like this one that + start with a newline after the initial triple quote, inserting an empty + line at the beginning of the string. + + Additional keyword arguments will be passed to ``textwrap.fill()``. + + See Also + ======== + strlines, rawlines + + """ + return '\n' + fill(dedent(str(s)).strip('\n'), width=w, **kwargs) + + +def strlines(s, c=64, short=False): + """Return a cut-and-pastable string that, when printed, is + equivalent to the input. The lines will be surrounded by + parentheses and no line will be longer than c (default 64) + characters. If the line contains newlines characters, the + `rawlines` result will be returned. If ``short`` is True + (default is False) then if there is one line it will be + returned without bounding parentheses. + + Examples + ======== + + >>> from sympy.utilities.misc import strlines + >>> q = 'this is a long string that should be broken into shorter lines' + >>> print(strlines(q, 40)) + ( + 'this is a long string that should be b' + 'roken into shorter lines' + ) + >>> q == ( + ... 'this is a long string that should be b' + ... 'roken into shorter lines' + ... ) + True + + See Also + ======== + filldedent, rawlines + """ + if not isinstance(s, str): + raise ValueError('expecting string input') + if '\n' in s: + return rawlines(s) + q = '"' if repr(s).startswith('"') else "'" + q = (q,)*2 + if '\\' in s: # use r-string + m = '(\nr%s%%s%s\n)' % q + j = '%s\nr%s' % q + c -= 3 + else: + m = '(\n%s%%s%s\n)' % q + j = '%s\n%s' % q + c -= 2 + out = [] + while s: + out.append(s[:c]) + s=s[c:] + if short and len(out) == 1: + return (m % out[0]).splitlines()[1] # strip bounding (\n...\n) + return m % j.join(out) + + +def rawlines(s): + """Return a cut-and-pastable string that, when printed, is equivalent + to the input. Use this when there is more than one line in the + string. The string returned is formatted so it can be indented + nicely within tests; in some cases it is wrapped in the dedent + function which has to be imported from textwrap. + + Examples + ======== + + Note: because there are characters in the examples below that need + to be escaped because they are themselves within a triple quoted + docstring, expressions below look more complicated than they would + be if they were printed in an interpreter window. + + >>> from sympy.utilities.misc import rawlines + >>> from sympy import TableForm + >>> s = str(TableForm([[1, 10]], headings=(None, ['a', 'bee']))) + >>> print(rawlines(s)) + ( + 'a bee\\n' + '-----\\n' + '1 10 ' + ) + >>> print(rawlines('''this + ... that''')) + dedent('''\\ + this + that''') + + >>> print(rawlines('''this + ... that + ... ''')) + dedent('''\\ + this + that + ''') + + >>> s = \"\"\"this + ... is a triple ''' + ... \"\"\" + >>> print(rawlines(s)) + dedent(\"\"\"\\ + this + is a triple ''' + \"\"\") + + >>> print(rawlines('''this + ... that + ... ''')) + ( + 'this\\n' + 'that\\n' + ' ' + ) + + See Also + ======== + filldedent, strlines + """ + lines = s.split('\n') + if len(lines) == 1: + return repr(lines[0]) + triple = ["'''" in s, '"""' in s] + if any(li.endswith(' ') for li in lines) or '\\' in s or all(triple): + rv = [] + # add on the newlines + trailing = s.endswith('\n') + last = len(lines) - 1 + for i, li in enumerate(lines): + if i != last or trailing: + rv.append(repr(li + '\n')) + else: + rv.append(repr(li)) + return '(\n %s\n)' % '\n '.join(rv) + else: + rv = '\n '.join(lines) + if triple[0]: + return 'dedent("""\\\n %s""")' % rv + else: + return "dedent('''\\\n %s''')" % rv + +ARCH = str(struct.calcsize('P') * 8) + "-bit" + + +# XXX: PyPy does not support hash randomization +HASH_RANDOMIZATION = getattr(sys.flags, 'hash_randomization', False) + +_debug_tmp: list[str] = [] +_debug_iter = 0 + +def debug_decorator(func): + """If SYMPY_DEBUG is True, it will print a nice execution tree with + arguments and results of all decorated functions, else do nothing. + """ + from sympy import SYMPY_DEBUG + + if not SYMPY_DEBUG: + return func + + def maketree(f, *args, **kw): + global _debug_tmp, _debug_iter + oldtmp = _debug_tmp + _debug_tmp = [] + _debug_iter += 1 + + def tree(subtrees): + def indent(s, variant=1): + x = s.split("\n") + r = "+-%s\n" % x[0] + for a in x[1:]: + if a == "": + continue + if variant == 1: + r += "| %s\n" % a + else: + r += " %s\n" % a + return r + if len(subtrees) == 0: + return "" + f = [] + for a in subtrees[:-1]: + f.append(indent(a)) + f.append(indent(subtrees[-1], 2)) + return ''.join(f) + + # If there is a bug and the algorithm enters an infinite loop, enable the + # following lines. It will print the names and parameters of all major functions + # that are called, *before* they are called + #from functools import reduce + #print("%s%s %s%s" % (_debug_iter, reduce(lambda x, y: x + y, \ + # map(lambda x: '-', range(1, 2 + _debug_iter))), f.__name__, args)) + + r = f(*args, **kw) + + _debug_iter -= 1 + s = "%s%s = %s\n" % (f.__name__, args, r) + if _debug_tmp != []: + s += tree(_debug_tmp) + _debug_tmp = oldtmp + _debug_tmp.append(s) + if _debug_iter == 0: + print(_debug_tmp[0]) + _debug_tmp = [] + return r + + def decorated(*args, **kwargs): + return maketree(func, *args, **kwargs) + + return decorated + + +def debug(*args): + """ + Print ``*args`` if SYMPY_DEBUG is True, else do nothing. + """ + from sympy import SYMPY_DEBUG + if SYMPY_DEBUG: + print(*args, file=sys.stderr) + + +def debugf(string, args): + """ + Print ``string%args`` if SYMPY_DEBUG is True, else do nothing. This is + intended for debug messages using formatted strings. + """ + from sympy import SYMPY_DEBUG + if SYMPY_DEBUG: + print(string%args, file=sys.stderr) + + +def find_executable(executable, path=None): + """Try to find 'executable' in the directories listed in 'path' (a + string listing directories separated by 'os.pathsep'; defaults to + os.environ['PATH']). Returns the complete filename or None if not + found + """ + from .exceptions import sympy_deprecation_warning + sympy_deprecation_warning( + """ + sympy.utilities.misc.find_executable() is deprecated. Use the standard + library shutil.which() function instead. + """, + deprecated_since_version="1.7", + active_deprecations_target="deprecated-find-executable", + ) + if path is None: + path = os.environ['PATH'] + paths = path.split(os.pathsep) + extlist = [''] + if os.name == 'os2': + (base, ext) = os.path.splitext(executable) + # executable files on OS/2 can have an arbitrary extension, but + # .exe is automatically appended if no dot is present in the name + if not ext: + executable = executable + ".exe" + elif sys.platform == 'win32': + pathext = os.environ['PATHEXT'].lower().split(os.pathsep) + (base, ext) = os.path.splitext(executable) + if ext.lower() not in pathext: + extlist = pathext + for ext in extlist: + execname = executable + ext + if os.path.isfile(execname): + return execname + else: + for p in paths: + f = os.path.join(p, execname) + if os.path.isfile(f): + return f + + return None + + +def func_name(x, short=False): + """Return function name of `x` (if defined) else the `type(x)`. + If short is True and there is a shorter alias for the result, + return the alias. + + Examples + ======== + + >>> from sympy.utilities.misc import func_name + >>> from sympy import Matrix + >>> from sympy.abc import x + >>> func_name(Matrix.eye(3)) + 'MutableDenseMatrix' + >>> func_name(x < 1) + 'StrictLessThan' + >>> func_name(x < 1, short=True) + 'Lt' + """ + alias = { + 'GreaterThan': 'Ge', + 'StrictGreaterThan': 'Gt', + 'LessThan': 'Le', + 'StrictLessThan': 'Lt', + 'Equality': 'Eq', + 'Unequality': 'Ne', + } + typ = type(x) + if str(typ).startswith(">> from sympy.utilities.misc import _replace + >>> f = _replace(dict(foo='bar', d='t')) + >>> f('food') + 'bart' + >>> f = _replace({}) + >>> f('food') + 'food' + """ + if not reps: + return lambda x: x + D = lambda match: reps[match.group(0)] + pattern = _re.compile("|".join( + [_re.escape(k) for k, v in reps.items()]), _re.MULTILINE) + return lambda string: pattern.sub(D, string) + + +def replace(string, *reps): + """Return ``string`` with all keys in ``reps`` replaced with + their corresponding values, longer strings first, irrespective + of the order they are given. ``reps`` may be passed as tuples + or a single mapping. + + Examples + ======== + + >>> from sympy.utilities.misc import replace + >>> replace('foo', {'oo': 'ar', 'f': 'b'}) + 'bar' + >>> replace("spamham sha", ("spam", "eggs"), ("sha","md5")) + 'eggsham md5' + + There is no guarantee that a unique answer will be + obtained if keys in a mapping overlap (i.e. are the same + length and have some identical sequence at the + beginning/end): + + >>> reps = [ + ... ('ab', 'x'), + ... ('bc', 'y')] + >>> replace('abc', *reps) in ('xc', 'ay') + True + + References + ========== + + .. [1] https://stackoverflow.com/questions/6116978/how-to-replace-multiple-substrings-of-a-string + """ + if len(reps) == 1: + kv = reps[0] + if isinstance(kv, dict): + reps = kv + else: + return string.replace(*kv) + else: + reps = dict(reps) + return _replace(reps)(string) + + +def translate(s, a, b=None, c=None): + """Return ``s`` where characters have been replaced or deleted. + + SYNTAX + ====== + + translate(s, None, deletechars): + all characters in ``deletechars`` are deleted + translate(s, map [,deletechars]): + all characters in ``deletechars`` (if provided) are deleted + then the replacements defined by map are made; if the keys + of map are strings then the longer ones are handled first. + Multicharacter deletions should have a value of ''. + translate(s, oldchars, newchars, deletechars) + all characters in ``deletechars`` are deleted + then each character in ``oldchars`` is replaced with the + corresponding character in ``newchars`` + + Examples + ======== + + >>> from sympy.utilities.misc import translate + >>> abc = 'abc' + >>> translate(abc, None, 'a') + 'bc' + >>> translate(abc, {'a': 'x'}, 'c') + 'xb' + >>> translate(abc, {'abc': 'x', 'a': 'y'}) + 'x' + + >>> translate('abcd', 'ac', 'AC', 'd') + 'AbC' + + There is no guarantee that a unique answer will be + obtained if keys in a mapping overlap are the same + length and have some identical sequences at the + beginning/end: + + >>> translate(abc, {'ab': 'x', 'bc': 'y'}) in ('xc', 'ay') + True + """ + + mr = {} + if a is None: + if c is not None: + raise ValueError('c should be None when a=None is passed, instead got %s' % c) + if b is None: + return s + c = b + a = b = '' + else: + if isinstance(a, dict): + short = {} + for k in list(a.keys()): + if len(k) == 1 and len(a[k]) == 1: + short[k] = a.pop(k) + mr = a + c = b + if short: + a, b = [''.join(i) for i in list(zip(*short.items()))] + else: + a = b = '' + elif len(a) != len(b): + raise ValueError('oldchars and newchars have different lengths') + + if c: + val = str.maketrans('', '', c) + s = s.translate(val) + s = replace(s, mr) + n = str.maketrans(a, b) + return s.translate(n) + + +def ordinal(num): + """Return ordinal number string of num, e.g. 1 becomes 1st. + """ + # modified from https://codereview.stackexchange.com/questions/41298/producing-ordinal-numbers + n = as_int(num) + k = abs(n) % 100 + if 11 <= k <= 13: + suffix = 'th' + elif k % 10 == 1: + suffix = 'st' + elif k % 10 == 2: + suffix = 'nd' + elif k % 10 == 3: + suffix = 'rd' + else: + suffix = 'th' + return str(n) + suffix + + +def as_int(n, strict=True): + """ + Convert the argument to a builtin integer. + + The return value is guaranteed to be equal to the input. ValueError is + raised if the input has a non-integral value. When ``strict`` is True, this + uses `__index__ `_ + and when it is False it uses ``int``. + + + Examples + ======== + + >>> from sympy.utilities.misc import as_int + >>> from sympy import sqrt, S + + The function is primarily concerned with sanitizing input for + functions that need to work with builtin integers, so anything that + is unambiguously an integer should be returned as an int: + + >>> as_int(S(3)) + 3 + + Floats, being of limited precision, are not assumed to be exact and + will raise an error unless the ``strict`` flag is False. This + precision issue becomes apparent for large floating point numbers: + + >>> big = 1e23 + >>> type(big) is float + True + >>> big == int(big) + True + >>> as_int(big) + Traceback (most recent call last): + ... + ValueError: ... is not an integer + >>> as_int(big, strict=False) + 99999999999999991611392 + + Input that might be a complex representation of an integer value is + also rejected by default: + + >>> one = sqrt(3 + 2*sqrt(2)) - sqrt(2) + >>> int(one) == 1 + True + >>> as_int(one) + Traceback (most recent call last): + ... + ValueError: ... is not an integer + """ + if strict: + try: + if isinstance(n, bool): + raise TypeError + return operator.index(n) + except TypeError: + raise ValueError('%s is not an integer' % (n,)) + else: + try: + result = int(n) + except TypeError: + raise ValueError('%s is not an integer' % (n,)) + if n - result: + raise ValueError('%s is not an integer' % (n,)) + return result diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/pkgdata.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/pkgdata.py new file mode 100644 index 0000000000000000000000000000000000000000..8bf2065759362ee09a252d4736bd612b8d271e72 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/pkgdata.py @@ -0,0 +1,33 @@ +# This module is deprecated and will be removed. + +import sys +import os +from io import StringIO + +from sympy.utilities.decorator import deprecated + + +@deprecated( + """ + The sympy.utilities.pkgdata module and its get_resource function are + deprecated. Use the stdlib importlib.resources module instead. + """, + deprecated_since_version="1.12", + active_deprecations_target="pkgdata", +) +def get_resource(identifier, pkgname=__name__): + + mod = sys.modules[pkgname] + fn = getattr(mod, '__file__', None) + if fn is None: + raise OSError("%r has no __file__!") + path = os.path.join(os.path.dirname(fn), identifier) + loader = getattr(mod, '__loader__', None) + if loader is not None: + try: + data = loader.get_data(path) + except (OSError, AttributeError): + pass + else: + return StringIO(data.decode('utf-8')) + return open(os.path.normpath(path), 'rb') diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/pytest.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/pytest.py new file mode 100644 index 0000000000000000000000000000000000000000..75494c8e987c7b89bddf18febe09fdc3df2b194e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/pytest.py @@ -0,0 +1,12 @@ +""" +.. deprecated:: 1.6 + + sympy.utilities.pytest has been renamed to sympy.testing.pytest. +""" +from sympy.utilities.exceptions import sympy_deprecation_warning + +sympy_deprecation_warning("The sympy.utilities.pytest submodule is deprecated. Use sympy.testing.pytest instead.", + deprecated_since_version="1.6", + active_deprecations_target="deprecated-sympy-utilities-submodules") + +from sympy.testing.pytest import * # noqa:F401,F403 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/randtest.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/randtest.py new file mode 100644 index 0000000000000000000000000000000000000000..1bd3472ed8a89198d9e78e125f16dae1a56f6bad --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/randtest.py @@ -0,0 +1,12 @@ +""" +.. deprecated:: 1.6 + + sympy.utilities.randtest has been renamed to sympy.core.random. +""" +from sympy.utilities.exceptions import sympy_deprecation_warning + +sympy_deprecation_warning("The sympy.utilities.randtest submodule is deprecated. Use sympy.core.random instead.", + deprecated_since_version="1.6", + active_deprecations_target="deprecated-sympy-utilities-submodules") + +from sympy.core.random import * # noqa:F401,F403 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/runtests.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/runtests.py new file mode 100644 index 0000000000000000000000000000000000000000..bb9f760f070be528e8cd51ab38e00bc944dda9ac --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/runtests.py @@ -0,0 +1,13 @@ +""" +.. deprecated:: 1.6 + + sympy.utilities.runtests has been renamed to sympy.testing.runtests. +""" + +from sympy.utilities.exceptions import sympy_deprecation_warning + +sympy_deprecation_warning("The sympy.utilities.runtests submodule is deprecated. Use sympy.testing.runtests instead.", + deprecated_since_version="1.6", + active_deprecations_target="deprecated-sympy-utilities-submodules") + +from sympy.testing.runtests import * # noqa: F401,F403 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/source.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/source.py new file mode 100644 index 0000000000000000000000000000000000000000..71692b4aaad07df70b63d7e1eaa86c402ad03c4f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/source.py @@ -0,0 +1,40 @@ +""" +This module adds several functions for interactive source code inspection. +""" + + +def get_class(lookup_view): + """ + Convert a string version of a class name to the object. + + For example, get_class('sympy.core.Basic') will return + class Basic located in module sympy.core + """ + if isinstance(lookup_view, str): + mod_name, func_name = get_mod_func(lookup_view) + if func_name != '': + lookup_view = getattr( + __import__(mod_name, {}, {}, ['*']), func_name) + if not callable(lookup_view): + raise AttributeError( + "'%s.%s' is not a callable." % (mod_name, func_name)) + return lookup_view + + +def get_mod_func(callback): + """ + splits the string path to a class into a string path to the module + and the name of the class. + + Examples + ======== + + >>> from sympy.utilities.source import get_mod_func + >>> get_mod_func('sympy.core.basic.Basic') + ('sympy.core.basic', 'Basic') + + """ + dot = callback.rfind('.') + if dot == -1: + return callback, '' + return callback[:dot], callback[dot + 1:] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_autowrap.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_autowrap.py new file mode 100644 index 0000000000000000000000000000000000000000..b5a33d77adb46710cfa3cfeb1ea39402e35c76cf --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_autowrap.py @@ -0,0 +1,467 @@ +# Tests that require installed backends go into +# sympy/test_external/test_autowrap + +import os +import tempfile +import shutil +from io import StringIO +from pathlib import Path + +from sympy.core import symbols, Eq +from sympy.utilities.autowrap import (autowrap, binary_function, + CythonCodeWrapper, UfuncifyCodeWrapper, CodeWrapper) +from sympy.utilities.codegen import ( + CCodeGen, C99CodeGen, CodeGenArgumentListError, make_routine +) +from sympy.testing.pytest import raises +from sympy.testing.tmpfiles import TmpFileManager + + +def get_string(dump_fn, routines, prefix="file", **kwargs): + """Wrapper for dump_fn. dump_fn writes its results to a stream object and + this wrapper returns the contents of that stream as a string. This + auxiliary function is used by many tests below. + + The header and the empty lines are not generator to facilitate the + testing of the output. + """ + output = StringIO() + dump_fn(routines, output, prefix, **kwargs) + source = output.getvalue() + output.close() + return source + + +def test_cython_wrapper_scalar_function(): + x, y, z = symbols('x,y,z') + expr = (x + y)*z + routine = make_routine("test", expr) + code_gen = CythonCodeWrapper(CCodeGen()) + source = get_string(code_gen.dump_pyx, [routine]) + + expected = ( + "cdef extern from 'file.h':\n" + " double test(double x, double y, double z)\n" + "\n" + "def test_c(double x, double y, double z):\n" + "\n" + " return test(x, y, z)") + assert source == expected + + +def test_cython_wrapper_outarg(): + from sympy.core.relational import Equality + x, y, z = symbols('x,y,z') + code_gen = CythonCodeWrapper(C99CodeGen()) + + routine = make_routine("test", Equality(z, x + y)) + source = get_string(code_gen.dump_pyx, [routine]) + expected = ( + "cdef extern from 'file.h':\n" + " void test(double x, double y, double *z)\n" + "\n" + "def test_c(double x, double y):\n" + "\n" + " cdef double z = 0\n" + " test(x, y, &z)\n" + " return z") + assert source == expected + + +def test_cython_wrapper_inoutarg(): + from sympy.core.relational import Equality + x, y, z = symbols('x,y,z') + code_gen = CythonCodeWrapper(C99CodeGen()) + routine = make_routine("test", Equality(z, x + y + z)) + source = get_string(code_gen.dump_pyx, [routine]) + expected = ( + "cdef extern from 'file.h':\n" + " void test(double x, double y, double *z)\n" + "\n" + "def test_c(double x, double y, double z):\n" + "\n" + " test(x, y, &z)\n" + " return z") + assert source == expected + + +def test_cython_wrapper_compile_flags(): + from sympy.core.relational import Equality + x, y, z = symbols('x,y,z') + routine = make_routine("test", Equality(z, x + y)) + + code_gen = CythonCodeWrapper(CCodeGen()) + + expected = """\ +from setuptools import setup +from setuptools import Extension +from Cython.Build import cythonize +cy_opts = {'compiler_directives': {'language_level': '3'}} + +ext_mods = [Extension( + 'wrapper_module_%(num)s', ['wrapper_module_%(num)s.pyx', 'wrapped_code_%(num)s.c'], + include_dirs=[], + library_dirs=[], + libraries=[], + extra_compile_args=['-std=c99'], + extra_link_args=[] +)] +setup(ext_modules=cythonize(ext_mods, **cy_opts)) +""" % {'num': CodeWrapper._module_counter} + + temp_dir = tempfile.mkdtemp() + TmpFileManager.tmp_folder(temp_dir) + setup_file_path = os.path.join(temp_dir, 'setup.py') + + code_gen._prepare_files(routine, build_dir=temp_dir) + setup_text = Path(setup_file_path).read_text() + assert setup_text == expected + + code_gen = CythonCodeWrapper(CCodeGen(), + include_dirs=['/usr/local/include', '/opt/booger/include'], + library_dirs=['/user/local/lib'], + libraries=['thelib', 'nilib'], + extra_compile_args=['-slow-math'], + extra_link_args=['-lswamp', '-ltrident'], + cythonize_options={'compiler_directives': {'boundscheck': False}} + ) + expected = """\ +from setuptools import setup +from setuptools import Extension +from Cython.Build import cythonize +cy_opts = {'compiler_directives': {'boundscheck': False}} + +ext_mods = [Extension( + 'wrapper_module_%(num)s', ['wrapper_module_%(num)s.pyx', 'wrapped_code_%(num)s.c'], + include_dirs=['/usr/local/include', '/opt/booger/include'], + library_dirs=['/user/local/lib'], + libraries=['thelib', 'nilib'], + extra_compile_args=['-slow-math', '-std=c99'], + extra_link_args=['-lswamp', '-ltrident'] +)] +setup(ext_modules=cythonize(ext_mods, **cy_opts)) +""" % {'num': CodeWrapper._module_counter} + + code_gen._prepare_files(routine, build_dir=temp_dir) + setup_text = Path(setup_file_path).read_text() + assert setup_text == expected + + expected = """\ +from setuptools import setup +from setuptools import Extension +from Cython.Build import cythonize +cy_opts = {'compiler_directives': {'boundscheck': False}} +import numpy as np + +ext_mods = [Extension( + 'wrapper_module_%(num)s', ['wrapper_module_%(num)s.pyx', 'wrapped_code_%(num)s.c'], + include_dirs=['/usr/local/include', '/opt/booger/include', np.get_include()], + library_dirs=['/user/local/lib'], + libraries=['thelib', 'nilib'], + extra_compile_args=['-slow-math', '-std=c99'], + extra_link_args=['-lswamp', '-ltrident'] +)] +setup(ext_modules=cythonize(ext_mods, **cy_opts)) +""" % {'num': CodeWrapper._module_counter} + + code_gen._need_numpy = True + code_gen._prepare_files(routine, build_dir=temp_dir) + setup_text = Path(setup_file_path).read_text() + assert setup_text == expected + + TmpFileManager.cleanup() + +def test_cython_wrapper_unique_dummyvars(): + from sympy.core.relational import Equality + from sympy.core.symbol import Dummy + x, y, z = Dummy('x'), Dummy('y'), Dummy('z') + x_id, y_id, z_id = [str(d.dummy_index) for d in [x, y, z]] + expr = Equality(z, x + y) + routine = make_routine("test", expr) + code_gen = CythonCodeWrapper(CCodeGen()) + source = get_string(code_gen.dump_pyx, [routine]) + expected_template = ( + "cdef extern from 'file.h':\n" + " void test(double x_{x_id}, double y_{y_id}, double *z_{z_id})\n" + "\n" + "def test_c(double x_{x_id}, double y_{y_id}):\n" + "\n" + " cdef double z_{z_id} = 0\n" + " test(x_{x_id}, y_{y_id}, &z_{z_id})\n" + " return z_{z_id}") + expected = expected_template.format(x_id=x_id, y_id=y_id, z_id=z_id) + assert source == expected + +def test_autowrap_dummy(): + x, y, z = symbols('x y z') + + # Uses DummyWrapper to test that codegen works as expected + + f = autowrap(x + y, backend='dummy') + assert f() == str(x + y) + assert f.args == "x, y" + assert f.returns == "nameless" + f = autowrap(Eq(z, x + y), backend='dummy') + assert f() == str(x + y) + assert f.args == "x, y" + assert f.returns == "z" + f = autowrap(Eq(z, x + y + z), backend='dummy') + assert f() == str(x + y + z) + assert f.args == "x, y, z" + assert f.returns == "z" + + +def test_autowrap_args(): + x, y, z = symbols('x y z') + + raises(CodeGenArgumentListError, lambda: autowrap(Eq(z, x + y), + backend='dummy', args=[x])) + f = autowrap(Eq(z, x + y), backend='dummy', args=[y, x]) + assert f() == str(x + y) + assert f.args == "y, x" + assert f.returns == "z" + + raises(CodeGenArgumentListError, lambda: autowrap(Eq(z, x + y + z), + backend='dummy', args=[x, y])) + f = autowrap(Eq(z, x + y + z), backend='dummy', args=[y, x, z]) + assert f() == str(x + y + z) + assert f.args == "y, x, z" + assert f.returns == "z" + + f = autowrap(Eq(z, x + y + z), backend='dummy', args=(y, x, z)) + assert f() == str(x + y + z) + assert f.args == "y, x, z" + assert f.returns == "z" + +def test_autowrap_store_files(): + x, y = symbols('x y') + tmp = tempfile.mkdtemp() + TmpFileManager.tmp_folder(tmp) + + f = autowrap(x + y, backend='dummy', tempdir=tmp) + assert f() == str(x + y) + assert os.access(tmp, os.F_OK) + + TmpFileManager.cleanup() + +def test_autowrap_store_files_issue_gh12939(): + x, y = symbols('x y') + tmp = './tmp' + saved_cwd = os.getcwd() + temp_cwd = tempfile.mkdtemp() + try: + os.chdir(temp_cwd) + f = autowrap(x + y, backend='dummy', tempdir=tmp) + assert f() == str(x + y) + assert os.access(tmp, os.F_OK) + finally: + os.chdir(saved_cwd) + shutil.rmtree(temp_cwd) + + +def test_binary_function(): + x, y = symbols('x y') + f = binary_function('f', x + y, backend='dummy') + assert f._imp_() == str(x + y) + + +def test_ufuncify_source(): + x, y, z = symbols('x,y,z') + code_wrapper = UfuncifyCodeWrapper(C99CodeGen("ufuncify")) + routine = make_routine("test", x + y + z) + source = get_string(code_wrapper.dump_c, [routine]) + expected = """\ +#include "Python.h" +#include "math.h" +#include "numpy/ndarraytypes.h" +#include "numpy/ufuncobject.h" +#include "numpy/halffloat.h" +#include "file.h" + +static PyMethodDef wrapper_module_%(num)sMethods[] = { + {NULL, NULL, 0, NULL} +}; + +#ifdef NPY_1_19_API_VERSION +static void test_ufunc(char **args, const npy_intp *dimensions, const npy_intp* steps, void* data) +#else +static void test_ufunc(char **args, npy_intp *dimensions, npy_intp* steps, void* data) +#endif +{ + npy_intp i; + npy_intp n = dimensions[0]; + char *in0 = args[0]; + char *in1 = args[1]; + char *in2 = args[2]; + char *out0 = args[3]; + npy_intp in0_step = steps[0]; + npy_intp in1_step = steps[1]; + npy_intp in2_step = steps[2]; + npy_intp out0_step = steps[3]; + for (i = 0; i < n; i++) { + *((double *)out0) = test(*(double *)in0, *(double *)in1, *(double *)in2); + in0 += in0_step; + in1 += in1_step; + in2 += in2_step; + out0 += out0_step; + } +} +PyUFuncGenericFunction test_funcs[1] = {&test_ufunc}; +static char test_types[4] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; +static void *test_data[1] = {NULL}; + +#if PY_VERSION_HEX >= 0x03000000 +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "wrapper_module_%(num)s", + NULL, + -1, + wrapper_module_%(num)sMethods, + NULL, + NULL, + NULL, + NULL +}; + +PyMODINIT_FUNC PyInit_wrapper_module_%(num)s(void) +{ + PyObject *m, *d; + PyObject *ufunc0; + m = PyModule_Create(&moduledef); + if (!m) { + return NULL; + } + import_array(); + import_umath(); + d = PyModule_GetDict(m); + ufunc0 = PyUFunc_FromFuncAndData(test_funcs, test_data, test_types, 1, 3, 1, + PyUFunc_None, "wrapper_module_%(num)s", "Created in SymPy with Ufuncify", 0); + PyDict_SetItemString(d, "test", ufunc0); + Py_DECREF(ufunc0); + return m; +} +#else +PyMODINIT_FUNC initwrapper_module_%(num)s(void) +{ + PyObject *m, *d; + PyObject *ufunc0; + m = Py_InitModule("wrapper_module_%(num)s", wrapper_module_%(num)sMethods); + if (m == NULL) { + return; + } + import_array(); + import_umath(); + d = PyModule_GetDict(m); + ufunc0 = PyUFunc_FromFuncAndData(test_funcs, test_data, test_types, 1, 3, 1, + PyUFunc_None, "wrapper_module_%(num)s", "Created in SymPy with Ufuncify", 0); + PyDict_SetItemString(d, "test", ufunc0); + Py_DECREF(ufunc0); +} +#endif""" % {'num': CodeWrapper._module_counter} + assert source == expected + + +def test_ufuncify_source_multioutput(): + x, y, z = symbols('x,y,z') + var_symbols = (x, y, z) + expr = x + y**3 + 10*z**2 + code_wrapper = UfuncifyCodeWrapper(C99CodeGen("ufuncify")) + routines = [make_routine("func{}".format(i), expr.diff(var_symbols[i]), var_symbols) for i in range(len(var_symbols))] + source = get_string(code_wrapper.dump_c, routines, funcname='multitest') + expected = """\ +#include "Python.h" +#include "math.h" +#include "numpy/ndarraytypes.h" +#include "numpy/ufuncobject.h" +#include "numpy/halffloat.h" +#include "file.h" + +static PyMethodDef wrapper_module_%(num)sMethods[] = { + {NULL, NULL, 0, NULL} +}; + +#ifdef NPY_1_19_API_VERSION +static void multitest_ufunc(char **args, const npy_intp *dimensions, const npy_intp* steps, void* data) +#else +static void multitest_ufunc(char **args, npy_intp *dimensions, npy_intp* steps, void* data) +#endif +{ + npy_intp i; + npy_intp n = dimensions[0]; + char *in0 = args[0]; + char *in1 = args[1]; + char *in2 = args[2]; + char *out0 = args[3]; + char *out1 = args[4]; + char *out2 = args[5]; + npy_intp in0_step = steps[0]; + npy_intp in1_step = steps[1]; + npy_intp in2_step = steps[2]; + npy_intp out0_step = steps[3]; + npy_intp out1_step = steps[4]; + npy_intp out2_step = steps[5]; + for (i = 0; i < n; i++) { + *((double *)out0) = func0(*(double *)in0, *(double *)in1, *(double *)in2); + *((double *)out1) = func1(*(double *)in0, *(double *)in1, *(double *)in2); + *((double *)out2) = func2(*(double *)in0, *(double *)in1, *(double *)in2); + in0 += in0_step; + in1 += in1_step; + in2 += in2_step; + out0 += out0_step; + out1 += out1_step; + out2 += out2_step; + } +} +PyUFuncGenericFunction multitest_funcs[1] = {&multitest_ufunc}; +static char multitest_types[6] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; +static void *multitest_data[1] = {NULL}; + +#if PY_VERSION_HEX >= 0x03000000 +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "wrapper_module_%(num)s", + NULL, + -1, + wrapper_module_%(num)sMethods, + NULL, + NULL, + NULL, + NULL +}; + +PyMODINIT_FUNC PyInit_wrapper_module_%(num)s(void) +{ + PyObject *m, *d; + PyObject *ufunc0; + m = PyModule_Create(&moduledef); + if (!m) { + return NULL; + } + import_array(); + import_umath(); + d = PyModule_GetDict(m); + ufunc0 = PyUFunc_FromFuncAndData(multitest_funcs, multitest_data, multitest_types, 1, 3, 3, + PyUFunc_None, "wrapper_module_%(num)s", "Created in SymPy with Ufuncify", 0); + PyDict_SetItemString(d, "multitest", ufunc0); + Py_DECREF(ufunc0); + return m; +} +#else +PyMODINIT_FUNC initwrapper_module_%(num)s(void) +{ + PyObject *m, *d; + PyObject *ufunc0; + m = Py_InitModule("wrapper_module_%(num)s", wrapper_module_%(num)sMethods); + if (m == NULL) { + return; + } + import_array(); + import_umath(); + d = PyModule_GetDict(m); + ufunc0 = PyUFunc_FromFuncAndData(multitest_funcs, multitest_data, multitest_types, 1, 3, 3, + PyUFunc_None, "wrapper_module_%(num)s", "Created in SymPy with Ufuncify", 0); + PyDict_SetItemString(d, "multitest", ufunc0); + Py_DECREF(ufunc0); +} +#endif""" % {'num': CodeWrapper._module_counter} + assert source == expected diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_codegen.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_codegen.py new file mode 100644 index 0000000000000000000000000000000000000000..4ccc6f9a90fb0a0bec39cea22420da8091ede740 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_codegen.py @@ -0,0 +1,1632 @@ +from io import StringIO + +from sympy.core import symbols, Eq, pi, Catalan, Lambda, Dummy +from sympy.core.relational import Equality +from sympy.core.symbol import Symbol +from sympy.functions.special.error_functions import erf +from sympy.integrals.integrals import Integral +from sympy.matrices import Matrix, MatrixSymbol +from sympy.utilities.codegen import ( + codegen, make_routine, CCodeGen, C89CodeGen, C99CodeGen, InputArgument, + CodeGenError, FCodeGen, CodeGenArgumentListError, OutputArgument, + InOutArgument) +from sympy.testing.pytest import raises +from sympy.utilities.lambdify import implemented_function + +#FIXME: Fails due to circular import in with core +# from sympy import codegen + + +def get_string(dump_fn, routines, prefix="file", header=False, empty=False): + """Wrapper for dump_fn. dump_fn writes its results to a stream object and + this wrapper returns the contents of that stream as a string. This + auxiliary function is used by many tests below. + + The header and the empty lines are not generated to facilitate the + testing of the output. + """ + output = StringIO() + dump_fn(routines, output, prefix, header, empty) + source = output.getvalue() + output.close() + return source + + +def test_Routine_argument_order(): + a, x, y, z = symbols('a x y z') + expr = (x + y)*z + raises(CodeGenArgumentListError, lambda: make_routine("test", expr, + argument_sequence=[z, x])) + raises(CodeGenArgumentListError, lambda: make_routine("test", Eq(a, + expr), argument_sequence=[z, x, y])) + r = make_routine('test', Eq(a, expr), argument_sequence=[z, x, a, y]) + assert [ arg.name for arg in r.arguments ] == [z, x, a, y] + assert [ type(arg) for arg in r.arguments ] == [ + InputArgument, InputArgument, OutputArgument, InputArgument ] + r = make_routine('test', Eq(z, expr), argument_sequence=[z, x, y]) + assert [ type(arg) for arg in r.arguments ] == [ + InOutArgument, InputArgument, InputArgument ] + + from sympy.tensor import IndexedBase, Idx + A, B = map(IndexedBase, ['A', 'B']) + m = symbols('m', integer=True) + i = Idx('i', m) + r = make_routine('test', Eq(A[i], B[i]), argument_sequence=[B, A, m]) + assert [ arg.name for arg in r.arguments ] == [B.label, A.label, m] + + expr = Integral(x*y*z, (x, 1, 2), (y, 1, 3)) + r = make_routine('test', Eq(a, expr), argument_sequence=[z, x, a, y]) + assert [ arg.name for arg in r.arguments ] == [z, x, a, y] + + +def test_empty_c_code(): + code_gen = C89CodeGen() + source = get_string(code_gen.dump_c, []) + assert source == "#include \"file.h\"\n#include \n" + + +def test_empty_c_code_with_comment(): + code_gen = C89CodeGen() + source = get_string(code_gen.dump_c, [], header=True) + assert source[:82] == ( + "/******************************************************************************\n *" + ) + # " Code generated with SymPy 0.7.2-git " + assert source[158:] == ( "*\n" + " * *\n" + " * See http://www.sympy.org/ for more information. *\n" + " * *\n" + " * This file is part of 'project' *\n" + " ******************************************************************************/\n" + "#include \"file.h\"\n" + "#include \n" + ) + + +def test_empty_c_header(): + code_gen = C99CodeGen() + source = get_string(code_gen.dump_h, []) + assert source == "#ifndef PROJECT__FILE__H\n#define PROJECT__FILE__H\n#endif\n" + + +def test_simple_c_code(): + x, y, z = symbols('x,y,z') + expr = (x + y)*z + routine = make_routine("test", expr) + code_gen = C89CodeGen() + source = get_string(code_gen.dump_c, [routine]) + expected = ( + "#include \"file.h\"\n" + "#include \n" + "double test(double x, double y, double z) {\n" + " double test_result;\n" + " test_result = z*(x + y);\n" + " return test_result;\n" + "}\n" + ) + assert source == expected + + +def test_c_code_reserved_words(): + x, y, z = symbols('if, typedef, while') + expr = (x + y) * z + routine = make_routine("test", expr) + code_gen = C99CodeGen() + source = get_string(code_gen.dump_c, [routine]) + expected = ( + "#include \"file.h\"\n" + "#include \n" + "double test(double if_, double typedef_, double while_) {\n" + " double test_result;\n" + " test_result = while_*(if_ + typedef_);\n" + " return test_result;\n" + "}\n" + ) + assert source == expected + + +def test_numbersymbol_c_code(): + routine = make_routine("test", pi**Catalan) + code_gen = C89CodeGen() + source = get_string(code_gen.dump_c, [routine]) + expected = ( + "#include \"file.h\"\n" + "#include \n" + "double test() {\n" + " double test_result;\n" + " double const Catalan = %s;\n" + " test_result = pow(M_PI, Catalan);\n" + " return test_result;\n" + "}\n" + ) % Catalan.evalf(17) + assert source == expected + + +def test_c_code_argument_order(): + x, y, z = symbols('x,y,z') + expr = x + y + routine = make_routine("test", expr, argument_sequence=[z, x, y]) + code_gen = C89CodeGen() + source = get_string(code_gen.dump_c, [routine]) + expected = ( + "#include \"file.h\"\n" + "#include \n" + "double test(double z, double x, double y) {\n" + " double test_result;\n" + " test_result = x + y;\n" + " return test_result;\n" + "}\n" + ) + assert source == expected + + +def test_simple_c_header(): + x, y, z = symbols('x,y,z') + expr = (x + y)*z + routine = make_routine("test", expr) + code_gen = C89CodeGen() + source = get_string(code_gen.dump_h, [routine]) + expected = ( + "#ifndef PROJECT__FILE__H\n" + "#define PROJECT__FILE__H\n" + "double test(double x, double y, double z);\n" + "#endif\n" + ) + assert source == expected + + +def test_simple_c_codegen(): + x, y, z = symbols('x,y,z') + expr = (x + y)*z + expected = [ + ("file.c", + "#include \"file.h\"\n" + "#include \n" + "double test(double x, double y, double z) {\n" + " double test_result;\n" + " test_result = z*(x + y);\n" + " return test_result;\n" + "}\n"), + ("file.h", + "#ifndef PROJECT__FILE__H\n" + "#define PROJECT__FILE__H\n" + "double test(double x, double y, double z);\n" + "#endif\n") + ] + result = codegen(("test", expr), "C", "file", header=False, empty=False) + assert result == expected + + +def test_multiple_results_c(): + x, y, z = symbols('x,y,z') + expr1 = (x + y)*z + expr2 = (x - y)*z + routine = make_routine( + "test", + [expr1, expr2] + ) + code_gen = C99CodeGen() + raises(CodeGenError, lambda: get_string(code_gen.dump_h, [routine])) + + +def test_no_results_c(): + raises(ValueError, lambda: make_routine("test", [])) + + +def test_ansi_math1_codegen(): + # not included: log10 + from sympy.functions.elementary.complexes import Abs + from sympy.functions.elementary.exponential import log + from sympy.functions.elementary.hyperbolic import (cosh, sinh, tanh) + from sympy.functions.elementary.integers import (ceiling, floor) + from sympy.functions.elementary.miscellaneous import sqrt + from sympy.functions.elementary.trigonometric import (acos, asin, atan, cos, sin, tan) + x = symbols('x') + name_expr = [ + ("test_fabs", Abs(x)), + ("test_acos", acos(x)), + ("test_asin", asin(x)), + ("test_atan", atan(x)), + ("test_ceil", ceiling(x)), + ("test_cos", cos(x)), + ("test_cosh", cosh(x)), + ("test_floor", floor(x)), + ("test_log", log(x)), + ("test_ln", log(x)), + ("test_sin", sin(x)), + ("test_sinh", sinh(x)), + ("test_sqrt", sqrt(x)), + ("test_tan", tan(x)), + ("test_tanh", tanh(x)), + ] + result = codegen(name_expr, "C89", "file", header=False, empty=False) + assert result[0][0] == "file.c" + assert result[0][1] == ( + '#include "file.h"\n#include \n' + 'double test_fabs(double x) {\n double test_fabs_result;\n test_fabs_result = fabs(x);\n return test_fabs_result;\n}\n' + 'double test_acos(double x) {\n double test_acos_result;\n test_acos_result = acos(x);\n return test_acos_result;\n}\n' + 'double test_asin(double x) {\n double test_asin_result;\n test_asin_result = asin(x);\n return test_asin_result;\n}\n' + 'double test_atan(double x) {\n double test_atan_result;\n test_atan_result = atan(x);\n return test_atan_result;\n}\n' + 'double test_ceil(double x) {\n double test_ceil_result;\n test_ceil_result = ceil(x);\n return test_ceil_result;\n}\n' + 'double test_cos(double x) {\n double test_cos_result;\n test_cos_result = cos(x);\n return test_cos_result;\n}\n' + 'double test_cosh(double x) {\n double test_cosh_result;\n test_cosh_result = cosh(x);\n return test_cosh_result;\n}\n' + 'double test_floor(double x) {\n double test_floor_result;\n test_floor_result = floor(x);\n return test_floor_result;\n}\n' + 'double test_log(double x) {\n double test_log_result;\n test_log_result = log(x);\n return test_log_result;\n}\n' + 'double test_ln(double x) {\n double test_ln_result;\n test_ln_result = log(x);\n return test_ln_result;\n}\n' + 'double test_sin(double x) {\n double test_sin_result;\n test_sin_result = sin(x);\n return test_sin_result;\n}\n' + 'double test_sinh(double x) {\n double test_sinh_result;\n test_sinh_result = sinh(x);\n return test_sinh_result;\n}\n' + 'double test_sqrt(double x) {\n double test_sqrt_result;\n test_sqrt_result = sqrt(x);\n return test_sqrt_result;\n}\n' + 'double test_tan(double x) {\n double test_tan_result;\n test_tan_result = tan(x);\n return test_tan_result;\n}\n' + 'double test_tanh(double x) {\n double test_tanh_result;\n test_tanh_result = tanh(x);\n return test_tanh_result;\n}\n' + ) + assert result[1][0] == "file.h" + assert result[1][1] == ( + '#ifndef PROJECT__FILE__H\n#define PROJECT__FILE__H\n' + 'double test_fabs(double x);\ndouble test_acos(double x);\n' + 'double test_asin(double x);\ndouble test_atan(double x);\n' + 'double test_ceil(double x);\ndouble test_cos(double x);\n' + 'double test_cosh(double x);\ndouble test_floor(double x);\n' + 'double test_log(double x);\ndouble test_ln(double x);\n' + 'double test_sin(double x);\ndouble test_sinh(double x);\n' + 'double test_sqrt(double x);\ndouble test_tan(double x);\n' + 'double test_tanh(double x);\n#endif\n' + ) + + +def test_ansi_math2_codegen(): + # not included: frexp, ldexp, modf, fmod + from sympy.functions.elementary.trigonometric import atan2 + x, y = symbols('x,y') + name_expr = [ + ("test_atan2", atan2(x, y)), + ("test_pow", x**y), + ] + result = codegen(name_expr, "C89", "file", header=False, empty=False) + assert result[0][0] == "file.c" + assert result[0][1] == ( + '#include "file.h"\n#include \n' + 'double test_atan2(double x, double y) {\n double test_atan2_result;\n test_atan2_result = atan2(x, y);\n return test_atan2_result;\n}\n' + 'double test_pow(double x, double y) {\n double test_pow_result;\n test_pow_result = pow(x, y);\n return test_pow_result;\n}\n' + ) + assert result[1][0] == "file.h" + assert result[1][1] == ( + '#ifndef PROJECT__FILE__H\n#define PROJECT__FILE__H\n' + 'double test_atan2(double x, double y);\n' + 'double test_pow(double x, double y);\n' + '#endif\n' + ) + + +def test_complicated_codegen(): + from sympy.functions.elementary.trigonometric import (cos, sin, tan) + x, y, z = symbols('x,y,z') + name_expr = [ + ("test1", ((sin(x) + cos(y) + tan(z))**7).expand()), + ("test2", cos(cos(cos(cos(cos(cos(cos(cos(x + y + z))))))))), + ] + result = codegen(name_expr, "C89", "file", header=False, empty=False) + assert result[0][0] == "file.c" + assert result[0][1] == ( + '#include "file.h"\n#include \n' + 'double test1(double x, double y, double z) {\n' + ' double test1_result;\n' + ' test1_result = ' + 'pow(sin(x), 7) + ' + '7*pow(sin(x), 6)*cos(y) + ' + '7*pow(sin(x), 6)*tan(z) + ' + '21*pow(sin(x), 5)*pow(cos(y), 2) + ' + '42*pow(sin(x), 5)*cos(y)*tan(z) + ' + '21*pow(sin(x), 5)*pow(tan(z), 2) + ' + '35*pow(sin(x), 4)*pow(cos(y), 3) + ' + '105*pow(sin(x), 4)*pow(cos(y), 2)*tan(z) + ' + '105*pow(sin(x), 4)*cos(y)*pow(tan(z), 2) + ' + '35*pow(sin(x), 4)*pow(tan(z), 3) + ' + '35*pow(sin(x), 3)*pow(cos(y), 4) + ' + '140*pow(sin(x), 3)*pow(cos(y), 3)*tan(z) + ' + '210*pow(sin(x), 3)*pow(cos(y), 2)*pow(tan(z), 2) + ' + '140*pow(sin(x), 3)*cos(y)*pow(tan(z), 3) + ' + '35*pow(sin(x), 3)*pow(tan(z), 4) + ' + '21*pow(sin(x), 2)*pow(cos(y), 5) + ' + '105*pow(sin(x), 2)*pow(cos(y), 4)*tan(z) + ' + '210*pow(sin(x), 2)*pow(cos(y), 3)*pow(tan(z), 2) + ' + '210*pow(sin(x), 2)*pow(cos(y), 2)*pow(tan(z), 3) + ' + '105*pow(sin(x), 2)*cos(y)*pow(tan(z), 4) + ' + '21*pow(sin(x), 2)*pow(tan(z), 5) + ' + '7*sin(x)*pow(cos(y), 6) + ' + '42*sin(x)*pow(cos(y), 5)*tan(z) + ' + '105*sin(x)*pow(cos(y), 4)*pow(tan(z), 2) + ' + '140*sin(x)*pow(cos(y), 3)*pow(tan(z), 3) + ' + '105*sin(x)*pow(cos(y), 2)*pow(tan(z), 4) + ' + '42*sin(x)*cos(y)*pow(tan(z), 5) + ' + '7*sin(x)*pow(tan(z), 6) + ' + 'pow(cos(y), 7) + ' + '7*pow(cos(y), 6)*tan(z) + ' + '21*pow(cos(y), 5)*pow(tan(z), 2) + ' + '35*pow(cos(y), 4)*pow(tan(z), 3) + ' + '35*pow(cos(y), 3)*pow(tan(z), 4) + ' + '21*pow(cos(y), 2)*pow(tan(z), 5) + ' + '7*cos(y)*pow(tan(z), 6) + ' + 'pow(tan(z), 7);\n' + ' return test1_result;\n' + '}\n' + 'double test2(double x, double y, double z) {\n' + ' double test2_result;\n' + ' test2_result = cos(cos(cos(cos(cos(cos(cos(cos(x + y + z))))))));\n' + ' return test2_result;\n' + '}\n' + ) + assert result[1][0] == "file.h" + assert result[1][1] == ( + '#ifndef PROJECT__FILE__H\n' + '#define PROJECT__FILE__H\n' + 'double test1(double x, double y, double z);\n' + 'double test2(double x, double y, double z);\n' + '#endif\n' + ) + + +def test_loops_c(): + from sympy.tensor import IndexedBase, Idx + from sympy.core.symbol import symbols + n, m = symbols('n m', integer=True) + A = IndexedBase('A') + x = IndexedBase('x') + y = IndexedBase('y') + i = Idx('i', m) + j = Idx('j', n) + + (f1, code), (f2, interface) = codegen( + ('matrix_vector', Eq(y[i], A[i, j]*x[j])), "C99", "file", header=False, empty=False) + + assert f1 == 'file.c' + expected = ( + '#include "file.h"\n' + '#include \n' + 'void matrix_vector(double *A, int m, int n, double *x, double *y) {\n' + ' for (int i=0; i\n' + 'void test_dummies(int m_%(mno)i, double *x, double *y) {\n' + ' for (int i_%(ino)i=0; i_%(ino)i\n' + 'void matrix_vector(double *A, int m, int n, int o, int p, double *x, double *y) {\n' + ' for (int i=o; i<%(upperi)s; i++){\n' + ' y[i] = 0;\n' + ' }\n' + ' for (int i=o; i<%(upperi)s; i++){\n' + ' for (int j=0; j\n' + 'double foo(double x, double *y) {\n' + ' (*y) = sin(x);\n' + ' double foo_result;\n' + ' foo_result = cos(x);\n' + ' return foo_result;\n' + '}\n' + ) + assert result[0][1] == expected + + +def test_output_arg_c_reserved_words(): + from sympy.core.relational import Equality + from sympy.functions.elementary.trigonometric import (cos, sin) + x, y, z = symbols("if, while, z") + r = make_routine("foo", [Equality(y, sin(x)), cos(x)]) + c = C89CodeGen() + result = c.write([r], "test", header=False, empty=False) + assert result[0][0] == "test.c" + expected = ( + '#include "test.h"\n' + '#include \n' + 'double foo(double if_, double *while_) {\n' + ' (*while_) = sin(if_);\n' + ' double foo_result;\n' + ' foo_result = cos(if_);\n' + ' return foo_result;\n' + '}\n' + ) + assert result[0][1] == expected + + +def test_multidim_c_argument_cse(): + A_sym = MatrixSymbol('A', 3, 3) + b_sym = MatrixSymbol('b', 3, 1) + A = Matrix(A_sym) + b = Matrix(b_sym) + c = A*b + cgen = CCodeGen(project="test", cse=True) + r = cgen.routine("c", c) + r.arguments[-1].result_var = "out" + r.arguments[-1]._name = "out" + code = get_string(cgen.dump_c, [r], prefix="test") + expected = ( + '#include "test.h"\n' + "#include \n" + "void c(double *A, double *b, double *out) {\n" + " out[0] = A[0]*b[0] + A[1]*b[1] + A[2]*b[2];\n" + " out[1] = A[3]*b[0] + A[4]*b[1] + A[5]*b[2];\n" + " out[2] = A[6]*b[0] + A[7]*b[1] + A[8]*b[2];\n" + "}\n" + ) + assert code == expected + + +def test_ccode_results_named_ordered(): + x, y, z = symbols('x,y,z') + B, C = symbols('B,C') + A = MatrixSymbol('A', 1, 3) + expr1 = Equality(A, Matrix([[1, 2, x]])) + expr2 = Equality(C, (x + y)*z) + expr3 = Equality(B, 2*x) + name_expr = ("test", [expr1, expr2, expr3]) + expected = ( + '#include "test.h"\n' + '#include \n' + 'void test(double x, double *C, double z, double y, double *A, double *B) {\n' + ' (*C) = z*(x + y);\n' + ' A[0] = 1;\n' + ' A[1] = 2;\n' + ' A[2] = x;\n' + ' (*B) = 2*x;\n' + '}\n' + ) + + result = codegen(name_expr, "c", "test", header=False, empty=False, + argument_sequence=(x, C, z, y, A, B)) + source = result[0][1] + assert source == expected + + +def test_ccode_matrixsymbol_slice(): + A = MatrixSymbol('A', 5, 3) + B = MatrixSymbol('B', 1, 3) + C = MatrixSymbol('C', 1, 3) + D = MatrixSymbol('D', 5, 1) + name_expr = ("test", [Equality(B, A[0, :]), + Equality(C, A[1, :]), + Equality(D, A[:, 2])]) + result = codegen(name_expr, "c99", "test", header=False, empty=False) + source = result[0][1] + expected = ( + '#include "test.h"\n' + '#include \n' + 'void test(double *A, double *B, double *C, double *D) {\n' + ' B[0] = A[0];\n' + ' B[1] = A[1];\n' + ' B[2] = A[2];\n' + ' C[0] = A[3];\n' + ' C[1] = A[4];\n' + ' C[2] = A[5];\n' + ' D[0] = A[2];\n' + ' D[1] = A[5];\n' + ' D[2] = A[8];\n' + ' D[3] = A[11];\n' + ' D[4] = A[14];\n' + '}\n' + ) + assert source == expected + +def test_ccode_cse(): + a, b, c, d = symbols('a b c d') + e = MatrixSymbol('e', 3, 1) + name_expr = ("test", [Equality(e, Matrix([[a*b], [a*b + c*d], [a*b*c*d]]))]) + generator = CCodeGen(cse=True) + result = codegen(name_expr, code_gen=generator, header=False, empty=False) + source = result[0][1] + expected = ( + '#include "test.h"\n' + '#include \n' + 'void test(double a, double b, double c, double d, double *e) {\n' + ' const double x0 = a*b;\n' + ' const double x1 = c*d;\n' + ' e[0] = x0;\n' + ' e[1] = x0 + x1;\n' + ' e[2] = x0*x1;\n' + '}\n' + ) + assert source == expected + +def test_ccode_unused_array_arg(): + x = MatrixSymbol('x', 2, 1) + # x does not appear in output + name_expr = ("test", 1.0) + generator = CCodeGen() + result = codegen(name_expr, code_gen=generator, header=False, empty=False, argument_sequence=(x,)) + source = result[0][1] + # note: x should appear as (double *) + expected = ( + '#include "test.h"\n' + '#include \n' + 'double test(double *x) {\n' + ' double test_result;\n' + ' test_result = 1.0;\n' + ' return test_result;\n' + '}\n' + ) + assert source == expected + +def test_ccode_unused_array_arg_func(): + # issue 16689 + X = MatrixSymbol('X',3,1) + Y = MatrixSymbol('Y',3,1) + z = symbols('z',integer = True) + name_expr = ('testBug', X[0] + X[1]) + result = codegen(name_expr, language='C', header=False, empty=False, argument_sequence=(X, Y, z)) + source = result[0][1] + expected = ( + '#include "testBug.h"\n' + '#include \n' + 'double testBug(double *X, double *Y, int z) {\n' + ' double testBug_result;\n' + ' testBug_result = X[0] + X[1];\n' + ' return testBug_result;\n' + '}\n' + ) + assert source == expected + +def test_empty_f_code(): + code_gen = FCodeGen() + source = get_string(code_gen.dump_f95, []) + assert source == "" + + +def test_empty_f_code_with_header(): + code_gen = FCodeGen() + source = get_string(code_gen.dump_f95, [], header=True) + assert source[:82] == ( + "!******************************************************************************\n!*" + ) + # " Code generated with SymPy 0.7.2-git " + assert source[158:] == ( "*\n" + "!* *\n" + "!* See http://www.sympy.org/ for more information. *\n" + "!* *\n" + "!* This file is part of 'project' *\n" + "!******************************************************************************\n" + ) + + +def test_empty_f_header(): + code_gen = FCodeGen() + source = get_string(code_gen.dump_h, []) + assert source == "" + + +def test_simple_f_code(): + x, y, z = symbols('x,y,z') + expr = (x + y)*z + routine = make_routine("test", expr) + code_gen = FCodeGen() + source = get_string(code_gen.dump_f95, [routine]) + expected = ( + "REAL*8 function test(x, y, z)\n" + "implicit none\n" + "REAL*8, intent(in) :: x\n" + "REAL*8, intent(in) :: y\n" + "REAL*8, intent(in) :: z\n" + "test = z*(x + y)\n" + "end function\n" + ) + assert source == expected + + +def test_numbersymbol_f_code(): + routine = make_routine("test", pi**Catalan) + code_gen = FCodeGen() + source = get_string(code_gen.dump_f95, [routine]) + expected = ( + "REAL*8 function test()\n" + "implicit none\n" + "REAL*8, parameter :: Catalan = %sd0\n" + "REAL*8, parameter :: pi = %sd0\n" + "test = pi**Catalan\n" + "end function\n" + ) % (Catalan.evalf(17), pi.evalf(17)) + assert source == expected + +def test_erf_f_code(): + x = symbols('x') + routine = make_routine("test", erf(x) - erf(-2 * x)) + code_gen = FCodeGen() + source = get_string(code_gen.dump_f95, [routine]) + expected = ( + "REAL*8 function test(x)\n" + "implicit none\n" + "REAL*8, intent(in) :: x\n" + "test = erf(x) + erf(2.0d0*x)\n" + "end function\n" + ) + assert source == expected, source + +def test_f_code_argument_order(): + x, y, z = symbols('x,y,z') + expr = x + y + routine = make_routine("test", expr, argument_sequence=[z, x, y]) + code_gen = FCodeGen() + source = get_string(code_gen.dump_f95, [routine]) + expected = ( + "REAL*8 function test(z, x, y)\n" + "implicit none\n" + "REAL*8, intent(in) :: z\n" + "REAL*8, intent(in) :: x\n" + "REAL*8, intent(in) :: y\n" + "test = x + y\n" + "end function\n" + ) + assert source == expected + + +def test_simple_f_header(): + x, y, z = symbols('x,y,z') + expr = (x + y)*z + routine = make_routine("test", expr) + code_gen = FCodeGen() + source = get_string(code_gen.dump_h, [routine]) + expected = ( + "interface\n" + "REAL*8 function test(x, y, z)\n" + "implicit none\n" + "REAL*8, intent(in) :: x\n" + "REAL*8, intent(in) :: y\n" + "REAL*8, intent(in) :: z\n" + "end function\n" + "end interface\n" + ) + assert source == expected + + +def test_simple_f_codegen(): + x, y, z = symbols('x,y,z') + expr = (x + y)*z + result = codegen( + ("test", expr), "F95", "file", header=False, empty=False) + expected = [ + ("file.f90", + "REAL*8 function test(x, y, z)\n" + "implicit none\n" + "REAL*8, intent(in) :: x\n" + "REAL*8, intent(in) :: y\n" + "REAL*8, intent(in) :: z\n" + "test = z*(x + y)\n" + "end function\n"), + ("file.h", + "interface\n" + "REAL*8 function test(x, y, z)\n" + "implicit none\n" + "REAL*8, intent(in) :: x\n" + "REAL*8, intent(in) :: y\n" + "REAL*8, intent(in) :: z\n" + "end function\n" + "end interface\n") + ] + assert result == expected + + +def test_multiple_results_f(): + x, y, z = symbols('x,y,z') + expr1 = (x + y)*z + expr2 = (x - y)*z + routine = make_routine( + "test", + [expr1, expr2] + ) + code_gen = FCodeGen() + raises(CodeGenError, lambda: get_string(code_gen.dump_h, [routine])) + + +def test_no_results_f(): + raises(ValueError, lambda: make_routine("test", [])) + + +def test_intrinsic_math_codegen(): + # not included: log10 + from sympy.functions.elementary.complexes import Abs + from sympy.functions.elementary.exponential import log + from sympy.functions.elementary.hyperbolic import (cosh, sinh, tanh) + from sympy.functions.elementary.miscellaneous import sqrt + from sympy.functions.elementary.trigonometric import (acos, asin, atan, cos, sin, tan) + x = symbols('x') + name_expr = [ + ("test_abs", Abs(x)), + ("test_acos", acos(x)), + ("test_asin", asin(x)), + ("test_atan", atan(x)), + ("test_cos", cos(x)), + ("test_cosh", cosh(x)), + ("test_log", log(x)), + ("test_ln", log(x)), + ("test_sin", sin(x)), + ("test_sinh", sinh(x)), + ("test_sqrt", sqrt(x)), + ("test_tan", tan(x)), + ("test_tanh", tanh(x)), + ] + result = codegen(name_expr, "F95", "file", header=False, empty=False) + assert result[0][0] == "file.f90" + expected = ( + 'REAL*8 function test_abs(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'test_abs = abs(x)\n' + 'end function\n' + 'REAL*8 function test_acos(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'test_acos = acos(x)\n' + 'end function\n' + 'REAL*8 function test_asin(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'test_asin = asin(x)\n' + 'end function\n' + 'REAL*8 function test_atan(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'test_atan = atan(x)\n' + 'end function\n' + 'REAL*8 function test_cos(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'test_cos = cos(x)\n' + 'end function\n' + 'REAL*8 function test_cosh(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'test_cosh = cosh(x)\n' + 'end function\n' + 'REAL*8 function test_log(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'test_log = log(x)\n' + 'end function\n' + 'REAL*8 function test_ln(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'test_ln = log(x)\n' + 'end function\n' + 'REAL*8 function test_sin(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'test_sin = sin(x)\n' + 'end function\n' + 'REAL*8 function test_sinh(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'test_sinh = sinh(x)\n' + 'end function\n' + 'REAL*8 function test_sqrt(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'test_sqrt = sqrt(x)\n' + 'end function\n' + 'REAL*8 function test_tan(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'test_tan = tan(x)\n' + 'end function\n' + 'REAL*8 function test_tanh(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'test_tanh = tanh(x)\n' + 'end function\n' + ) + assert result[0][1] == expected + + assert result[1][0] == "file.h" + expected = ( + 'interface\n' + 'REAL*8 function test_abs(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'end function\n' + 'end interface\n' + 'interface\n' + 'REAL*8 function test_acos(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'end function\n' + 'end interface\n' + 'interface\n' + 'REAL*8 function test_asin(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'end function\n' + 'end interface\n' + 'interface\n' + 'REAL*8 function test_atan(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'end function\n' + 'end interface\n' + 'interface\n' + 'REAL*8 function test_cos(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'end function\n' + 'end interface\n' + 'interface\n' + 'REAL*8 function test_cosh(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'end function\n' + 'end interface\n' + 'interface\n' + 'REAL*8 function test_log(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'end function\n' + 'end interface\n' + 'interface\n' + 'REAL*8 function test_ln(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'end function\n' + 'end interface\n' + 'interface\n' + 'REAL*8 function test_sin(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'end function\n' + 'end interface\n' + 'interface\n' + 'REAL*8 function test_sinh(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'end function\n' + 'end interface\n' + 'interface\n' + 'REAL*8 function test_sqrt(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'end function\n' + 'end interface\n' + 'interface\n' + 'REAL*8 function test_tan(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'end function\n' + 'end interface\n' + 'interface\n' + 'REAL*8 function test_tanh(x)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'end function\n' + 'end interface\n' + ) + assert result[1][1] == expected + + +def test_intrinsic_math2_codegen(): + # not included: frexp, ldexp, modf, fmod + from sympy.functions.elementary.trigonometric import atan2 + x, y = symbols('x,y') + name_expr = [ + ("test_atan2", atan2(x, y)), + ("test_pow", x**y), + ] + result = codegen(name_expr, "F95", "file", header=False, empty=False) + assert result[0][0] == "file.f90" + expected = ( + 'REAL*8 function test_atan2(x, y)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'REAL*8, intent(in) :: y\n' + 'test_atan2 = atan2(x, y)\n' + 'end function\n' + 'REAL*8 function test_pow(x, y)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'REAL*8, intent(in) :: y\n' + 'test_pow = x**y\n' + 'end function\n' + ) + assert result[0][1] == expected + + assert result[1][0] == "file.h" + expected = ( + 'interface\n' + 'REAL*8 function test_atan2(x, y)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'REAL*8, intent(in) :: y\n' + 'end function\n' + 'end interface\n' + 'interface\n' + 'REAL*8 function test_pow(x, y)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'REAL*8, intent(in) :: y\n' + 'end function\n' + 'end interface\n' + ) + assert result[1][1] == expected + + +def test_complicated_codegen_f95(): + from sympy.functions.elementary.trigonometric import (cos, sin, tan) + x, y, z = symbols('x,y,z') + name_expr = [ + ("test1", ((sin(x) + cos(y) + tan(z))**7).expand()), + ("test2", cos(cos(cos(cos(cos(cos(cos(cos(x + y + z))))))))), + ] + result = codegen(name_expr, "F95", "file", header=False, empty=False) + assert result[0][0] == "file.f90" + expected = ( + 'REAL*8 function test1(x, y, z)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'REAL*8, intent(in) :: y\n' + 'REAL*8, intent(in) :: z\n' + 'test1 = sin(x)**7 + 7*sin(x)**6*cos(y) + 7*sin(x)**6*tan(z) + 21*sin(x) &\n' + ' **5*cos(y)**2 + 42*sin(x)**5*cos(y)*tan(z) + 21*sin(x)**5*tan(z) &\n' + ' **2 + 35*sin(x)**4*cos(y)**3 + 105*sin(x)**4*cos(y)**2*tan(z) + &\n' + ' 105*sin(x)**4*cos(y)*tan(z)**2 + 35*sin(x)**4*tan(z)**3 + 35*sin( &\n' + ' x)**3*cos(y)**4 + 140*sin(x)**3*cos(y)**3*tan(z) + 210*sin(x)**3* &\n' + ' cos(y)**2*tan(z)**2 + 140*sin(x)**3*cos(y)*tan(z)**3 + 35*sin(x) &\n' + ' **3*tan(z)**4 + 21*sin(x)**2*cos(y)**5 + 105*sin(x)**2*cos(y)**4* &\n' + ' tan(z) + 210*sin(x)**2*cos(y)**3*tan(z)**2 + 210*sin(x)**2*cos(y) &\n' + ' **2*tan(z)**3 + 105*sin(x)**2*cos(y)*tan(z)**4 + 21*sin(x)**2*tan &\n' + ' (z)**5 + 7*sin(x)*cos(y)**6 + 42*sin(x)*cos(y)**5*tan(z) + 105* &\n' + ' sin(x)*cos(y)**4*tan(z)**2 + 140*sin(x)*cos(y)**3*tan(z)**3 + 105 &\n' + ' *sin(x)*cos(y)**2*tan(z)**4 + 42*sin(x)*cos(y)*tan(z)**5 + 7*sin( &\n' + ' x)*tan(z)**6 + cos(y)**7 + 7*cos(y)**6*tan(z) + 21*cos(y)**5*tan( &\n' + ' z)**2 + 35*cos(y)**4*tan(z)**3 + 35*cos(y)**3*tan(z)**4 + 21*cos( &\n' + ' y)**2*tan(z)**5 + 7*cos(y)*tan(z)**6 + tan(z)**7\n' + 'end function\n' + 'REAL*8 function test2(x, y, z)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'REAL*8, intent(in) :: y\n' + 'REAL*8, intent(in) :: z\n' + 'test2 = cos(cos(cos(cos(cos(cos(cos(cos(x + y + z))))))))\n' + 'end function\n' + ) + assert result[0][1] == expected + assert result[1][0] == "file.h" + expected = ( + 'interface\n' + 'REAL*8 function test1(x, y, z)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'REAL*8, intent(in) :: y\n' + 'REAL*8, intent(in) :: z\n' + 'end function\n' + 'end interface\n' + 'interface\n' + 'REAL*8 function test2(x, y, z)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'REAL*8, intent(in) :: y\n' + 'REAL*8, intent(in) :: z\n' + 'end function\n' + 'end interface\n' + ) + assert result[1][1] == expected + + +def test_loops(): + from sympy.tensor import IndexedBase, Idx + from sympy.core.symbol import symbols + + n, m = symbols('n,m', integer=True) + A, x, y = map(IndexedBase, 'Axy') + i = Idx('i', m) + j = Idx('j', n) + + (f1, code), (f2, interface) = codegen( + ('matrix_vector', Eq(y[i], A[i, j]*x[j])), "F95", "file", header=False, empty=False) + + assert f1 == 'file.f90' + expected = ( + 'subroutine matrix_vector(A, m, n, x, y)\n' + 'implicit none\n' + 'INTEGER*4, intent(in) :: m\n' + 'INTEGER*4, intent(in) :: n\n' + 'REAL*8, intent(in), dimension(1:m, 1:n) :: A\n' + 'REAL*8, intent(in), dimension(1:n) :: x\n' + 'REAL*8, intent(out), dimension(1:m) :: y\n' + 'INTEGER*4 :: i\n' + 'INTEGER*4 :: j\n' + 'do i = 1, m\n' + ' y(i) = 0\n' + 'end do\n' + 'do i = 1, m\n' + ' do j = 1, n\n' + ' y(i) = %(rhs)s + y(i)\n' + ' end do\n' + 'end do\n' + 'end subroutine\n' + ) + + assert code == expected % {'rhs': 'A(i, j)*x(j)'} or\ + code == expected % {'rhs': 'x(j)*A(i, j)'} + assert f2 == 'file.h' + assert interface == ( + 'interface\n' + 'subroutine matrix_vector(A, m, n, x, y)\n' + 'implicit none\n' + 'INTEGER*4, intent(in) :: m\n' + 'INTEGER*4, intent(in) :: n\n' + 'REAL*8, intent(in), dimension(1:m, 1:n) :: A\n' + 'REAL*8, intent(in), dimension(1:n) :: x\n' + 'REAL*8, intent(out), dimension(1:m) :: y\n' + 'end subroutine\n' + 'end interface\n' + ) + + +def test_dummy_loops_f95(): + from sympy.tensor import IndexedBase, Idx + i, m = symbols('i m', integer=True, cls=Dummy) + x = IndexedBase('x') + y = IndexedBase('y') + i = Idx(i, m) + expected = ( + 'subroutine test_dummies(m_%(mcount)i, x, y)\n' + 'implicit none\n' + 'INTEGER*4, intent(in) :: m_%(mcount)i\n' + 'REAL*8, intent(in), dimension(1:m_%(mcount)i) :: x\n' + 'REAL*8, intent(out), dimension(1:m_%(mcount)i) :: y\n' + 'INTEGER*4 :: i_%(icount)i\n' + 'do i_%(icount)i = 1, m_%(mcount)i\n' + ' y(i_%(icount)i) = x(i_%(icount)i)\n' + 'end do\n' + 'end subroutine\n' + ) % {'icount': i.label.dummy_index, 'mcount': m.dummy_index} + r = make_routine('test_dummies', Eq(y[i], x[i])) + c = FCodeGen() + code = get_string(c.dump_f95, [r]) + assert code == expected + + +def test_loops_InOut(): + from sympy.tensor import IndexedBase, Idx + from sympy.core.symbol import symbols + + i, j, n, m = symbols('i,j,n,m', integer=True) + A, x, y = symbols('A,x,y') + A = IndexedBase(A)[Idx(i, m), Idx(j, n)] + x = IndexedBase(x)[Idx(j, n)] + y = IndexedBase(y)[Idx(i, m)] + + (f1, code), (f2, interface) = codegen( + ('matrix_vector', Eq(y, y + A*x)), "F95", "file", header=False, empty=False) + + assert f1 == 'file.f90' + expected = ( + 'subroutine matrix_vector(A, m, n, x, y)\n' + 'implicit none\n' + 'INTEGER*4, intent(in) :: m\n' + 'INTEGER*4, intent(in) :: n\n' + 'REAL*8, intent(in), dimension(1:m, 1:n) :: A\n' + 'REAL*8, intent(in), dimension(1:n) :: x\n' + 'REAL*8, intent(inout), dimension(1:m) :: y\n' + 'INTEGER*4 :: i\n' + 'INTEGER*4 :: j\n' + 'do i = 1, m\n' + ' do j = 1, n\n' + ' y(i) = %(rhs)s + y(i)\n' + ' end do\n' + 'end do\n' + 'end subroutine\n' + ) + + assert (code == expected % {'rhs': 'A(i, j)*x(j)'} or + code == expected % {'rhs': 'x(j)*A(i, j)'}) + assert f2 == 'file.h' + assert interface == ( + 'interface\n' + 'subroutine matrix_vector(A, m, n, x, y)\n' + 'implicit none\n' + 'INTEGER*4, intent(in) :: m\n' + 'INTEGER*4, intent(in) :: n\n' + 'REAL*8, intent(in), dimension(1:m, 1:n) :: A\n' + 'REAL*8, intent(in), dimension(1:n) :: x\n' + 'REAL*8, intent(inout), dimension(1:m) :: y\n' + 'end subroutine\n' + 'end interface\n' + ) + + +def test_partial_loops_f(): + # check that loop boundaries are determined by Idx, and array strides + # determined by shape of IndexedBase object. + from sympy.tensor import IndexedBase, Idx + from sympy.core.symbol import symbols + n, m, o, p = symbols('n m o p', integer=True) + A = IndexedBase('A', shape=(m, p)) + x = IndexedBase('x') + y = IndexedBase('y') + i = Idx('i', (o, m - 5)) # Note: bounds are inclusive + j = Idx('j', n) # dimension n corresponds to bounds (0, n - 1) + + (f1, code), (f2, interface) = codegen( + ('matrix_vector', Eq(y[i], A[i, j]*x[j])), "F95", "file", header=False, empty=False) + + expected = ( + 'subroutine matrix_vector(A, m, n, o, p, x, y)\n' + 'implicit none\n' + 'INTEGER*4, intent(in) :: m\n' + 'INTEGER*4, intent(in) :: n\n' + 'INTEGER*4, intent(in) :: o\n' + 'INTEGER*4, intent(in) :: p\n' + 'REAL*8, intent(in), dimension(1:m, 1:p) :: A\n' + 'REAL*8, intent(in), dimension(1:n) :: x\n' + 'REAL*8, intent(out), dimension(1:%(iup-ilow)s) :: y\n' + 'INTEGER*4 :: i\n' + 'INTEGER*4 :: j\n' + 'do i = %(ilow)s, %(iup)s\n' + ' y(i) = 0\n' + 'end do\n' + 'do i = %(ilow)s, %(iup)s\n' + ' do j = 1, n\n' + ' y(i) = %(rhs)s + y(i)\n' + ' end do\n' + 'end do\n' + 'end subroutine\n' + ) % { + 'rhs': '%(rhs)s', + 'iup': str(m - 4), + 'ilow': str(1 + o), + 'iup-ilow': str(m - 4 - o) + } + + assert code == expected % {'rhs': 'A(i, j)*x(j)'} or\ + code == expected % {'rhs': 'x(j)*A(i, j)'} + + +def test_output_arg_f(): + from sympy.core.relational import Equality + from sympy.functions.elementary.trigonometric import (cos, sin) + x, y, z = symbols("x,y,z") + r = make_routine("foo", [Equality(y, sin(x)), cos(x)]) + c = FCodeGen() + result = c.write([r], "test", header=False, empty=False) + assert result[0][0] == "test.f90" + assert result[0][1] == ( + 'REAL*8 function foo(x, y)\n' + 'implicit none\n' + 'REAL*8, intent(in) :: x\n' + 'REAL*8, intent(out) :: y\n' + 'y = sin(x)\n' + 'foo = cos(x)\n' + 'end function\n' + ) + + +def test_inline_function(): + from sympy.tensor import IndexedBase, Idx + from sympy.core.symbol import symbols + n, m = symbols('n m', integer=True) + A, x, y = map(IndexedBase, 'Axy') + i = Idx('i', m) + p = FCodeGen() + func = implemented_function('func', Lambda(n, n*(n + 1))) + routine = make_routine('test_inline', Eq(y[i], func(x[i]))) + code = get_string(p.dump_f95, [routine]) + expected = ( + 'subroutine test_inline(m, x, y)\n' + 'implicit none\n' + 'INTEGER*4, intent(in) :: m\n' + 'REAL*8, intent(in), dimension(1:m) :: x\n' + 'REAL*8, intent(out), dimension(1:m) :: y\n' + 'INTEGER*4 :: i\n' + 'do i = 1, m\n' + ' y(i) = %s*%s\n' + 'end do\n' + 'end subroutine\n' + ) + args = ('x(i)', '(x(i) + 1)') + assert code == expected % args or\ + code == expected % args[::-1] + + +def test_f_code_call_signature_wrap(): + # Issue #7934 + x = symbols('x:20') + expr = 0 + for sym in x: + expr += sym + routine = make_routine("test", expr) + code_gen = FCodeGen() + source = get_string(code_gen.dump_f95, [routine]) + expected = """\ +REAL*8 function test(x0, x1, x10, x11, x12, x13, x14, x15, x16, x17, x18, & + x19, x2, x3, x4, x5, x6, x7, x8, x9) +implicit none +REAL*8, intent(in) :: x0 +REAL*8, intent(in) :: x1 +REAL*8, intent(in) :: x10 +REAL*8, intent(in) :: x11 +REAL*8, intent(in) :: x12 +REAL*8, intent(in) :: x13 +REAL*8, intent(in) :: x14 +REAL*8, intent(in) :: x15 +REAL*8, intent(in) :: x16 +REAL*8, intent(in) :: x17 +REAL*8, intent(in) :: x18 +REAL*8, intent(in) :: x19 +REAL*8, intent(in) :: x2 +REAL*8, intent(in) :: x3 +REAL*8, intent(in) :: x4 +REAL*8, intent(in) :: x5 +REAL*8, intent(in) :: x6 +REAL*8, intent(in) :: x7 +REAL*8, intent(in) :: x8 +REAL*8, intent(in) :: x9 +test = x0 + x1 + x10 + x11 + x12 + x13 + x14 + x15 + x16 + x17 + x18 + & + x19 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9 +end function +""" + assert source == expected + + +def test_check_case(): + x, X = symbols('x,X') + raises(CodeGenError, lambda: codegen(('test', x*X), 'f95', 'prefix')) + + +def test_check_case_false_positive(): + # The upper case/lower case exception should not be triggered by SymPy + # objects that differ only because of assumptions. (It may be useful to + # have a check for that as well, but here we only want to test against + # false positives with respect to case checking.) + x1 = symbols('x') + x2 = symbols('x', my_assumption=True) + try: + codegen(('test', x1*x2), 'f95', 'prefix') + except CodeGenError as e: + if e.args[0].startswith("Fortran ignores case."): + raise AssertionError("This exception should not be raised!") + + +def test_c_fortran_omit_routine_name(): + x, y = symbols("x,y") + name_expr = [("foo", 2*x)] + result = codegen(name_expr, "F95", header=False, empty=False) + expresult = codegen(name_expr, "F95", "foo", header=False, empty=False) + assert result[0][1] == expresult[0][1] + + name_expr = ("foo", x*y) + result = codegen(name_expr, "F95", header=False, empty=False) + expresult = codegen(name_expr, "F95", "foo", header=False, empty=False) + assert result[0][1] == expresult[0][1] + + name_expr = ("foo", Matrix([[x, y], [x+y, x-y]])) + result = codegen(name_expr, "C89", header=False, empty=False) + expresult = codegen(name_expr, "C89", "foo", header=False, empty=False) + assert result[0][1] == expresult[0][1] + + +def test_fcode_matrix_output(): + x, y, z = symbols('x,y,z') + e1 = x + y + e2 = Matrix([[x, y], [z, 16]]) + name_expr = ("test", (e1, e2)) + result = codegen(name_expr, "f95", "test", header=False, empty=False) + source = result[0][1] + expected = ( + "REAL*8 function test(x, y, z, out_%(hash)s)\n" + "implicit none\n" + "REAL*8, intent(in) :: x\n" + "REAL*8, intent(in) :: y\n" + "REAL*8, intent(in) :: z\n" + "REAL*8, intent(out), dimension(1:2, 1:2) :: out_%(hash)s\n" + "out_%(hash)s(1, 1) = x\n" + "out_%(hash)s(2, 1) = z\n" + "out_%(hash)s(1, 2) = y\n" + "out_%(hash)s(2, 2) = 16\n" + "test = x + y\n" + "end function\n" + ) + # look for the magic number + a = source.splitlines()[5] + b = a.split('_') + out = b[1] + expected = expected % {'hash': out} + assert source == expected + + +def test_fcode_results_named_ordered(): + x, y, z = symbols('x,y,z') + B, C = symbols('B,C') + A = MatrixSymbol('A', 1, 3) + expr1 = Equality(A, Matrix([[1, 2, x]])) + expr2 = Equality(C, (x + y)*z) + expr3 = Equality(B, 2*x) + name_expr = ("test", [expr1, expr2, expr3]) + result = codegen(name_expr, "f95", "test", header=False, empty=False, + argument_sequence=(x, z, y, C, A, B)) + source = result[0][1] + expected = ( + "subroutine test(x, z, y, C, A, B)\n" + "implicit none\n" + "REAL*8, intent(in) :: x\n" + "REAL*8, intent(in) :: z\n" + "REAL*8, intent(in) :: y\n" + "REAL*8, intent(out) :: C\n" + "REAL*8, intent(out) :: B\n" + "REAL*8, intent(out), dimension(1:1, 1:3) :: A\n" + "C = z*(x + y)\n" + "A(1, 1) = 1\n" + "A(1, 2) = 2\n" + "A(1, 3) = x\n" + "B = 2*x\n" + "end subroutine\n" + ) + assert source == expected + + +def test_fcode_matrixsymbol_slice(): + A = MatrixSymbol('A', 2, 3) + B = MatrixSymbol('B', 1, 3) + C = MatrixSymbol('C', 1, 3) + D = MatrixSymbol('D', 2, 1) + name_expr = ("test", [Equality(B, A[0, :]), + Equality(C, A[1, :]), + Equality(D, A[:, 2])]) + result = codegen(name_expr, "f95", "test", header=False, empty=False) + source = result[0][1] + expected = ( + "subroutine test(A, B, C, D)\n" + "implicit none\n" + "REAL*8, intent(in), dimension(1:2, 1:3) :: A\n" + "REAL*8, intent(out), dimension(1:1, 1:3) :: B\n" + "REAL*8, intent(out), dimension(1:1, 1:3) :: C\n" + "REAL*8, intent(out), dimension(1:2, 1:1) :: D\n" + "B(1, 1) = A(1, 1)\n" + "B(1, 2) = A(1, 2)\n" + "B(1, 3) = A(1, 3)\n" + "C(1, 1) = A(2, 1)\n" + "C(1, 2) = A(2, 2)\n" + "C(1, 3) = A(2, 3)\n" + "D(1, 1) = A(1, 3)\n" + "D(2, 1) = A(2, 3)\n" + "end subroutine\n" + ) + assert source == expected + + +def test_fcode_matrixsymbol_slice_autoname(): + # see issue #8093 + A = MatrixSymbol('A', 2, 3) + name_expr = ("test", A[:, 1]) + result = codegen(name_expr, "f95", "test", header=False, empty=False) + source = result[0][1] + expected = ( + "subroutine test(A, out_%(hash)s)\n" + "implicit none\n" + "REAL*8, intent(in), dimension(1:2, 1:3) :: A\n" + "REAL*8, intent(out), dimension(1:2, 1:1) :: out_%(hash)s\n" + "out_%(hash)s(1, 1) = A(1, 2)\n" + "out_%(hash)s(2, 1) = A(2, 2)\n" + "end subroutine\n" + ) + # look for the magic number + a = source.splitlines()[3] + b = a.split('_') + out = b[1] + expected = expected % {'hash': out} + assert source == expected + + +def test_global_vars(): + x, y, z, t = symbols("x y z t") + result = codegen(('f', x*y), "F95", header=False, empty=False, + global_vars=(y,)) + source = result[0][1] + expected = ( + "REAL*8 function f(x)\n" + "implicit none\n" + "REAL*8, intent(in) :: x\n" + "f = x*y\n" + "end function\n" + ) + assert source == expected + + expected = ( + '#include "f.h"\n' + '#include \n' + 'double f(double x, double y) {\n' + ' double f_result;\n' + ' f_result = x*y + z;\n' + ' return f_result;\n' + '}\n' + ) + result = codegen(('f', x*y+z), "C", header=False, empty=False, + global_vars=(z, t)) + source = result[0][1] + assert source == expected + +def test_custom_codegen(): + from sympy.printing.c import C99CodePrinter + from sympy.functions.elementary.exponential import exp + + printer = C99CodePrinter(settings={'user_functions': {'exp': 'fastexp'}}) + + x, y = symbols('x y') + expr = exp(x + y) + + # replace math.h with a different header + gen = C99CodeGen(printer=printer, + preprocessor_statements=['#include "fastexp.h"']) + + expected = ( + '#include "expr.h"\n' + '#include "fastexp.h"\n' + 'double expr(double x, double y) {\n' + ' double expr_result;\n' + ' expr_result = fastexp(x + y);\n' + ' return expr_result;\n' + '}\n' + ) + + result = codegen(('expr', expr), header=False, empty=False, code_gen=gen) + source = result[0][1] + assert source == expected + + # use both math.h and an external header + gen = C99CodeGen(printer=printer) + gen.preprocessor_statements.append('#include "fastexp.h"') + + expected = ( + '#include "expr.h"\n' + '#include \n' + '#include "fastexp.h"\n' + 'double expr(double x, double y) {\n' + ' double expr_result;\n' + ' expr_result = fastexp(x + y);\n' + ' return expr_result;\n' + '}\n' + ) + + result = codegen(('expr', expr), header=False, empty=False, code_gen=gen) + source = result[0][1] + assert source == expected + +def test_c_with_printer(): + # issue 13586 + from sympy.printing.c import C99CodePrinter + class CustomPrinter(C99CodePrinter): + def _print_Pow(self, expr): + return "fastpow({}, {})".format(self._print(expr.base), + self._print(expr.exp)) + + x = symbols('x') + expr = x**3 + expected =[ + ("file.c", + "#include \"file.h\"\n" + "#include \n" + "double test(double x) {\n" + " double test_result;\n" + " test_result = fastpow(x, 3);\n" + " return test_result;\n" + "}\n"), + ("file.h", + "#ifndef PROJECT__FILE__H\n" + "#define PROJECT__FILE__H\n" + "double test(double x);\n" + "#endif\n") + ] + result = codegen(("test", expr), "C","file", header=False, empty=False, printer = CustomPrinter()) + assert result == expected + + +def test_fcode_complex(): + import sympy.utilities.codegen + sympy.utilities.codegen.COMPLEX_ALLOWED = True + x = Symbol('x', real=True) + y = Symbol('y',real=True) + result = codegen(('test',x+y), 'f95', 'test', header=False, empty=False) + source = (result[0][1]) + expected = ( + "REAL*8 function test(x, y)\n" + "implicit none\n" + "REAL*8, intent(in) :: x\n" + "REAL*8, intent(in) :: y\n" + "test = x + y\n" + "end function\n") + assert source == expected + x = Symbol('x') + y = Symbol('y',real=True) + result = codegen(('test',x+y), 'f95', 'test', header=False, empty=False) + source = (result[0][1]) + expected = ( + "COMPLEX*16 function test(x, y)\n" + "implicit none\n" + "COMPLEX*16, intent(in) :: x\n" + "REAL*8, intent(in) :: y\n" + "test = x + y\n" + "end function\n" + ) + assert source==expected + sympy.utilities.codegen.COMPLEX_ALLOWED = False diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_codegen_julia.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_codegen_julia.py new file mode 100644 index 0000000000000000000000000000000000000000..12841cb7d476107e3866d91b998bed1f997f3901 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_codegen_julia.py @@ -0,0 +1,620 @@ +from io import StringIO + +from sympy.core import S, symbols, Eq, pi, Catalan, EulerGamma, Function +from sympy.core.relational import Equality +from sympy.functions.elementary.piecewise import Piecewise +from sympy.matrices import Matrix, MatrixSymbol +from sympy.utilities.codegen import JuliaCodeGen, codegen, make_routine +from sympy.testing.pytest import XFAIL +import sympy + + +x, y, z = symbols('x,y,z') + + +def test_empty_jl_code(): + code_gen = JuliaCodeGen() + output = StringIO() + code_gen.dump_jl([], output, "file", header=False, empty=False) + source = output.getvalue() + assert source == "" + + +def test_jl_simple_code(): + name_expr = ("test", (x + y)*z) + result, = codegen(name_expr, "Julia", header=False, empty=False) + assert result[0] == "test.jl" + source = result[1] + expected = ( + "function test(x, y, z)\n" + " out1 = z .* (x + y)\n" + " return out1\n" + "end\n" + ) + assert source == expected + + +def test_jl_simple_code_with_header(): + name_expr = ("test", (x + y)*z) + result, = codegen(name_expr, "Julia", header=True, empty=False) + assert result[0] == "test.jl" + source = result[1] + expected = ( + "# Code generated with SymPy " + sympy.__version__ + "\n" + "#\n" + "# See http://www.sympy.org/ for more information.\n" + "#\n" + "# This file is part of 'project'\n" + "function test(x, y, z)\n" + " out1 = z .* (x + y)\n" + " return out1\n" + "end\n" + ) + assert source == expected + + +def test_jl_simple_code_nameout(): + expr = Equality(z, (x + y)) + name_expr = ("test", expr) + result, = codegen(name_expr, "Julia", header=False, empty=False) + source = result[1] + expected = ( + "function test(x, y)\n" + " z = x + y\n" + " return z\n" + "end\n" + ) + assert source == expected + + +def test_jl_numbersymbol(): + name_expr = ("test", pi**Catalan) + result, = codegen(name_expr, "Julia", header=False, empty=False) + source = result[1] + expected = ( + "function test()\n" + " out1 = pi ^ catalan\n" + " return out1\n" + "end\n" + ) + assert source == expected + + +@XFAIL +def test_jl_numbersymbol_no_inline(): + # FIXME: how to pass inline=False to the JuliaCodePrinter? + name_expr = ("test", [pi**Catalan, EulerGamma]) + result, = codegen(name_expr, "Julia", header=False, + empty=False, inline=False) + source = result[1] + expected = ( + "function test()\n" + " Catalan = 0.915965594177219\n" + " EulerGamma = 0.5772156649015329\n" + " out1 = pi ^ Catalan\n" + " out2 = EulerGamma\n" + " return out1, out2\n" + "end\n" + ) + assert source == expected + + +def test_jl_code_argument_order(): + expr = x + y + routine = make_routine("test", expr, argument_sequence=[z, x, y], language="julia") + code_gen = JuliaCodeGen() + output = StringIO() + code_gen.dump_jl([routine], output, "test", header=False, empty=False) + source = output.getvalue() + expected = ( + "function test(z, x, y)\n" + " out1 = x + y\n" + " return out1\n" + "end\n" + ) + assert source == expected + + +def test_multiple_results_m(): + # Here the output order is the input order + expr1 = (x + y)*z + expr2 = (x - y)*z + name_expr = ("test", [expr1, expr2]) + result, = codegen(name_expr, "Julia", header=False, empty=False) + source = result[1] + expected = ( + "function test(x, y, z)\n" + " out1 = z .* (x + y)\n" + " out2 = z .* (x - y)\n" + " return out1, out2\n" + "end\n" + ) + assert source == expected + + +def test_results_named_unordered(): + # Here output order is based on name_expr + A, B, C = symbols('A,B,C') + expr1 = Equality(C, (x + y)*z) + expr2 = Equality(A, (x - y)*z) + expr3 = Equality(B, 2*x) + name_expr = ("test", [expr1, expr2, expr3]) + result, = codegen(name_expr, "Julia", header=False, empty=False) + source = result[1] + expected = ( + "function test(x, y, z)\n" + " C = z .* (x + y)\n" + " A = z .* (x - y)\n" + " B = 2 * x\n" + " return C, A, B\n" + "end\n" + ) + assert source == expected + + +def test_results_named_ordered(): + A, B, C = symbols('A,B,C') + expr1 = Equality(C, (x + y)*z) + expr2 = Equality(A, (x - y)*z) + expr3 = Equality(B, 2*x) + name_expr = ("test", [expr1, expr2, expr3]) + result = codegen(name_expr, "Julia", header=False, empty=False, + argument_sequence=(x, z, y)) + assert result[0][0] == "test.jl" + source = result[0][1] + expected = ( + "function test(x, z, y)\n" + " C = z .* (x + y)\n" + " A = z .* (x - y)\n" + " B = 2 * x\n" + " return C, A, B\n" + "end\n" + ) + assert source == expected + + +def test_complicated_jl_codegen(): + from sympy.functions.elementary.trigonometric import (cos, sin, tan) + name_expr = ("testlong", + [ ((sin(x) + cos(y) + tan(z))**3).expand(), + cos(cos(cos(cos(cos(cos(cos(cos(x + y + z)))))))) + ]) + result = codegen(name_expr, "Julia", header=False, empty=False) + assert result[0][0] == "testlong.jl" + source = result[0][1] + expected = ( + "function testlong(x, y, z)\n" + " out1 = sin(x) .^ 3 + 3 * sin(x) .^ 2 .* cos(y) + 3 * sin(x) .^ 2 .* tan(z)" + " + 3 * sin(x) .* cos(y) .^ 2 + 6 * sin(x) .* cos(y) .* tan(z) + 3 * sin(x) .* tan(z) .^ 2" + " + cos(y) .^ 3 + 3 * cos(y) .^ 2 .* tan(z) + 3 * cos(y) .* tan(z) .^ 2 + tan(z) .^ 3\n" + " out2 = cos(cos(cos(cos(cos(cos(cos(cos(x + y + z))))))))\n" + " return out1, out2\n" + "end\n" + ) + assert source == expected + + +def test_jl_output_arg_mixed_unordered(): + # named outputs are alphabetical, unnamed output appear in the given order + from sympy.functions.elementary.trigonometric import (cos, sin) + a = symbols("a") + name_expr = ("foo", [cos(2*x), Equality(y, sin(x)), cos(x), Equality(a, sin(2*x))]) + result, = codegen(name_expr, "Julia", header=False, empty=False) + assert result[0] == "foo.jl" + source = result[1] + expected = ( + 'function foo(x)\n' + ' out1 = cos(2 * x)\n' + ' y = sin(x)\n' + ' out3 = cos(x)\n' + ' a = sin(2 * x)\n' + ' return out1, y, out3, a\n' + 'end\n' + ) + assert source == expected + + +def test_jl_piecewise_(): + pw = Piecewise((0, x < -1), (x**2, x <= 1), (-x+2, x > 1), (1, True), evaluate=False) + name_expr = ("pwtest", pw) + result, = codegen(name_expr, "Julia", header=False, empty=False) + source = result[1] + expected = ( + "function pwtest(x)\n" + " out1 = ((x < -1) ? (0) :\n" + " (x <= 1) ? (x .^ 2) :\n" + " (x > 1) ? (2 - x) : (1))\n" + " return out1\n" + "end\n" + ) + assert source == expected + + +@XFAIL +def test_jl_piecewise_no_inline(): + # FIXME: how to pass inline=False to the JuliaCodePrinter? + pw = Piecewise((0, x < -1), (x**2, x <= 1), (-x+2, x > 1), (1, True)) + name_expr = ("pwtest", pw) + result, = codegen(name_expr, "Julia", header=False, empty=False, + inline=False) + source = result[1] + expected = ( + "function pwtest(x)\n" + " if (x < -1)\n" + " out1 = 0\n" + " elseif (x <= 1)\n" + " out1 = x .^ 2\n" + " elseif (x > 1)\n" + " out1 = -x + 2\n" + " else\n" + " out1 = 1\n" + " end\n" + " return out1\n" + "end\n" + ) + assert source == expected + + +def test_jl_multifcns_per_file(): + name_expr = [ ("foo", [2*x, 3*y]), ("bar", [y**2, 4*y]) ] + result = codegen(name_expr, "Julia", header=False, empty=False) + assert result[0][0] == "foo.jl" + source = result[0][1] + expected = ( + "function foo(x, y)\n" + " out1 = 2 * x\n" + " out2 = 3 * y\n" + " return out1, out2\n" + "end\n" + "function bar(y)\n" + " out1 = y .^ 2\n" + " out2 = 4 * y\n" + " return out1, out2\n" + "end\n" + ) + assert source == expected + + +def test_jl_multifcns_per_file_w_header(): + name_expr = [ ("foo", [2*x, 3*y]), ("bar", [y**2, 4*y]) ] + result = codegen(name_expr, "Julia", header=True, empty=False) + assert result[0][0] == "foo.jl" + source = result[0][1] + expected = ( + "# Code generated with SymPy " + sympy.__version__ + "\n" + "#\n" + "# See http://www.sympy.org/ for more information.\n" + "#\n" + "# This file is part of 'project'\n" + "function foo(x, y)\n" + " out1 = 2 * x\n" + " out2 = 3 * y\n" + " return out1, out2\n" + "end\n" + "function bar(y)\n" + " out1 = y .^ 2\n" + " out2 = 4 * y\n" + " return out1, out2\n" + "end\n" + ) + assert source == expected + + +def test_jl_filename_match_prefix(): + name_expr = [ ("foo", [2*x, 3*y]), ("bar", [y**2, 4*y]) ] + result, = codegen(name_expr, "Julia", prefix="baz", header=False, + empty=False) + assert result[0] == "baz.jl" + + +def test_jl_matrix_named(): + e2 = Matrix([[x, 2*y, pi*z]]) + name_expr = ("test", Equality(MatrixSymbol('myout1', 1, 3), e2)) + result = codegen(name_expr, "Julia", header=False, empty=False) + assert result[0][0] == "test.jl" + source = result[0][1] + expected = ( + "function test(x, y, z)\n" + " myout1 = [x 2 * y pi * z]\n" + " return myout1\n" + "end\n" + ) + assert source == expected + + +def test_jl_matrix_named_matsym(): + myout1 = MatrixSymbol('myout1', 1, 3) + e2 = Matrix([[x, 2*y, pi*z]]) + name_expr = ("test", Equality(myout1, e2, evaluate=False)) + result, = codegen(name_expr, "Julia", header=False, empty=False) + source = result[1] + expected = ( + "function test(x, y, z)\n" + " myout1 = [x 2 * y pi * z]\n" + " return myout1\n" + "end\n" + ) + assert source == expected + + +def test_jl_matrix_output_autoname(): + expr = Matrix([[x, x+y, 3]]) + name_expr = ("test", expr) + result, = codegen(name_expr, "Julia", header=False, empty=False) + source = result[1] + expected = ( + "function test(x, y)\n" + " out1 = [x x + y 3]\n" + " return out1\n" + "end\n" + ) + assert source == expected + + +def test_jl_matrix_output_autoname_2(): + e1 = (x + y) + e2 = Matrix([[2*x, 2*y, 2*z]]) + e3 = Matrix([[x], [y], [z]]) + e4 = Matrix([[x, y], [z, 16]]) + name_expr = ("test", (e1, e2, e3, e4)) + result, = codegen(name_expr, "Julia", header=False, empty=False) + source = result[1] + expected = ( + "function test(x, y, z)\n" + " out1 = x + y\n" + " out2 = [2 * x 2 * y 2 * z]\n" + " out3 = [x, y, z]\n" + " out4 = [x y;\n" + " z 16]\n" + " return out1, out2, out3, out4\n" + "end\n" + ) + assert source == expected + + +def test_jl_results_matrix_named_ordered(): + B, C = symbols('B,C') + A = MatrixSymbol('A', 1, 3) + expr1 = Equality(C, (x + y)*z) + expr2 = Equality(A, Matrix([[1, 2, x]])) + expr3 = Equality(B, 2*x) + name_expr = ("test", [expr1, expr2, expr3]) + result, = codegen(name_expr, "Julia", header=False, empty=False, + argument_sequence=(x, z, y)) + source = result[1] + expected = ( + "function test(x, z, y)\n" + " C = z .* (x + y)\n" + " A = [1 2 x]\n" + " B = 2 * x\n" + " return C, A, B\n" + "end\n" + ) + assert source == expected + + +def test_jl_matrixsymbol_slice(): + A = MatrixSymbol('A', 2, 3) + B = MatrixSymbol('B', 1, 3) + C = MatrixSymbol('C', 1, 3) + D = MatrixSymbol('D', 2, 1) + name_expr = ("test", [Equality(B, A[0, :]), + Equality(C, A[1, :]), + Equality(D, A[:, 2])]) + result, = codegen(name_expr, "Julia", header=False, empty=False) + source = result[1] + expected = ( + "function test(A)\n" + " B = A[1,:]\n" + " C = A[2,:]\n" + " D = A[:,3]\n" + " return B, C, D\n" + "end\n" + ) + assert source == expected + + +def test_jl_matrixsymbol_slice2(): + A = MatrixSymbol('A', 3, 4) + B = MatrixSymbol('B', 2, 2) + C = MatrixSymbol('C', 2, 2) + name_expr = ("test", [Equality(B, A[0:2, 0:2]), + Equality(C, A[0:2, 1:3])]) + result, = codegen(name_expr, "Julia", header=False, empty=False) + source = result[1] + expected = ( + "function test(A)\n" + " B = A[1:2,1:2]\n" + " C = A[1:2,2:3]\n" + " return B, C\n" + "end\n" + ) + assert source == expected + + +def test_jl_matrixsymbol_slice3(): + A = MatrixSymbol('A', 8, 7) + B = MatrixSymbol('B', 2, 2) + C = MatrixSymbol('C', 4, 2) + name_expr = ("test", [Equality(B, A[6:, 1::3]), + Equality(C, A[::2, ::3])]) + result, = codegen(name_expr, "Julia", header=False, empty=False) + source = result[1] + expected = ( + "function test(A)\n" + " B = A[7:end,2:3:end]\n" + " C = A[1:2:end,1:3:end]\n" + " return B, C\n" + "end\n" + ) + assert source == expected + + +def test_jl_matrixsymbol_slice_autoname(): + A = MatrixSymbol('A', 2, 3) + B = MatrixSymbol('B', 1, 3) + name_expr = ("test", [Equality(B, A[0,:]), A[1,:], A[:,0], A[:,1]]) + result, = codegen(name_expr, "Julia", header=False, empty=False) + source = result[1] + expected = ( + "function test(A)\n" + " B = A[1,:]\n" + " out2 = A[2,:]\n" + " out3 = A[:,1]\n" + " out4 = A[:,2]\n" + " return B, out2, out3, out4\n" + "end\n" + ) + assert source == expected + + +def test_jl_loops(): + # Note: an Julia programmer would probably vectorize this across one or + # more dimensions. Also, size(A) would be used rather than passing in m + # and n. Perhaps users would expect us to vectorize automatically here? + # Or is it possible to represent such things using IndexedBase? + from sympy.tensor import IndexedBase, Idx + from sympy.core.symbol import symbols + n, m = symbols('n m', integer=True) + A = IndexedBase('A') + x = IndexedBase('x') + y = IndexedBase('y') + i = Idx('i', m) + j = Idx('j', n) + result, = codegen(('mat_vec_mult', Eq(y[i], A[i, j]*x[j])), "Julia", + header=False, empty=False) + source = result[1] + expected = ( + 'function mat_vec_mult(y, A, m, n, x)\n' + ' for i = 1:m\n' + ' y[i] = 0\n' + ' end\n' + ' for i = 1:m\n' + ' for j = 1:n\n' + ' y[i] = %(rhs)s + y[i]\n' + ' end\n' + ' end\n' + ' return y\n' + 'end\n' + ) + assert (source == expected % {'rhs': 'A[%s,%s] .* x[j]' % (i, j)} or + source == expected % {'rhs': 'x[j] .* A[%s,%s]' % (i, j)}) + + +def test_jl_tensor_loops_multiple_contractions(): + # see comments in previous test about vectorizing + from sympy.tensor import IndexedBase, Idx + from sympy.core.symbol import symbols + n, m, o, p = symbols('n m o p', integer=True) + A = IndexedBase('A') + B = IndexedBase('B') + y = IndexedBase('y') + i = Idx('i', m) + j = Idx('j', n) + k = Idx('k', o) + l = Idx('l', p) + result, = codegen(('tensorthing', Eq(y[i], B[j, k, l]*A[i, j, k, l])), + "Julia", header=False, empty=False) + source = result[1] + expected = ( + 'function tensorthing(y, A, B, m, n, o, p)\n' + ' for i = 1:m\n' + ' y[i] = 0\n' + ' end\n' + ' for i = 1:m\n' + ' for j = 1:n\n' + ' for k = 1:o\n' + ' for l = 1:p\n' + ' y[i] = A[i,j,k,l] .* B[j,k,l] + y[i]\n' + ' end\n' + ' end\n' + ' end\n' + ' end\n' + ' return y\n' + 'end\n' + ) + assert source == expected + + +def test_jl_InOutArgument(): + expr = Equality(x, x**2) + name_expr = ("mysqr", expr) + result, = codegen(name_expr, "Julia", header=False, empty=False) + source = result[1] + expected = ( + "function mysqr(x)\n" + " x = x .^ 2\n" + " return x\n" + "end\n" + ) + assert source == expected + + +def test_jl_InOutArgument_order(): + # can specify the order as (x, y) + expr = Equality(x, x**2 + y) + name_expr = ("test", expr) + result, = codegen(name_expr, "Julia", header=False, + empty=False, argument_sequence=(x,y)) + source = result[1] + expected = ( + "function test(x, y)\n" + " x = x .^ 2 + y\n" + " return x\n" + "end\n" + ) + assert source == expected + # make sure it gives (x, y) not (y, x) + expr = Equality(x, x**2 + y) + name_expr = ("test", expr) + result, = codegen(name_expr, "Julia", header=False, empty=False) + source = result[1] + expected = ( + "function test(x, y)\n" + " x = x .^ 2 + y\n" + " return x\n" + "end\n" + ) + assert source == expected + + +def test_jl_not_supported(): + f = Function('f') + name_expr = ("test", [f(x).diff(x), S.ComplexInfinity]) + result, = codegen(name_expr, "Julia", header=False, empty=False) + source = result[1] + expected = ( + "function test(x)\n" + " # unsupported: Derivative(f(x), x)\n" + " # unsupported: zoo\n" + " out1 = Derivative(f(x), x)\n" + " out2 = zoo\n" + " return out1, out2\n" + "end\n" + ) + assert source == expected + + +def test_global_vars_octave(): + x, y, z, t = symbols("x y z t") + result = codegen(('f', x*y), "Julia", header=False, empty=False, + global_vars=(y,)) + source = result[0][1] + expected = ( + "function f(x)\n" + " out1 = x .* y\n" + " return out1\n" + "end\n" + ) + assert source == expected + + result = codegen(('f', x*y+z), "Julia", header=False, empty=False, + argument_sequence=(x, y), global_vars=(z, t)) + source = result[0][1] + expected = ( + "function f(x, y)\n" + " out1 = x .* y + z\n" + " return out1\n" + "end\n" + ) + assert source == expected diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_codegen_octave.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_codegen_octave.py new file mode 100644 index 0000000000000000000000000000000000000000..77aaef7dd0d81d6855024e49fbb3d6d4c09f3384 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_codegen_octave.py @@ -0,0 +1,589 @@ +from io import StringIO + +from sympy.core import S, symbols, Eq, pi, Catalan, EulerGamma, Function +from sympy.core.relational import Equality +from sympy.functions.elementary.piecewise import Piecewise +from sympy.matrices import Matrix, MatrixSymbol +from sympy.utilities.codegen import OctaveCodeGen, codegen, make_routine +from sympy.testing.pytest import raises +from sympy.testing.pytest import XFAIL +import sympy + + +x, y, z = symbols('x,y,z') + + +def test_empty_m_code(): + code_gen = OctaveCodeGen() + output = StringIO() + code_gen.dump_m([], output, "file", header=False, empty=False) + source = output.getvalue() + assert source == "" + + +def test_m_simple_code(): + name_expr = ("test", (x + y)*z) + result, = codegen(name_expr, "Octave", header=False, empty=False) + assert result[0] == "test.m" + source = result[1] + expected = ( + "function out1 = test(x, y, z)\n" + " out1 = z.*(x + y);\n" + "end\n" + ) + assert source == expected + + +def test_m_simple_code_with_header(): + name_expr = ("test", (x + y)*z) + result, = codegen(name_expr, "Octave", header=True, empty=False) + assert result[0] == "test.m" + source = result[1] + expected = ( + "function out1 = test(x, y, z)\n" + " %TEST Autogenerated by SymPy\n" + " % Code generated with SymPy " + sympy.__version__ + "\n" + " %\n" + " % See http://www.sympy.org/ for more information.\n" + " %\n" + " % This file is part of 'project'\n" + " out1 = z.*(x + y);\n" + "end\n" + ) + assert source == expected + + +def test_m_simple_code_nameout(): + expr = Equality(z, (x + y)) + name_expr = ("test", expr) + result, = codegen(name_expr, "Octave", header=False, empty=False) + source = result[1] + expected = ( + "function z = test(x, y)\n" + " z = x + y;\n" + "end\n" + ) + assert source == expected + + +def test_m_numbersymbol(): + name_expr = ("test", pi**Catalan) + result, = codegen(name_expr, "Octave", header=False, empty=False) + source = result[1] + expected = ( + "function out1 = test()\n" + " out1 = pi^%s;\n" + "end\n" + ) % Catalan.evalf(17) + assert source == expected + + +@XFAIL +def test_m_numbersymbol_no_inline(): + # FIXME: how to pass inline=False to the OctaveCodePrinter? + name_expr = ("test", [pi**Catalan, EulerGamma]) + result, = codegen(name_expr, "Octave", header=False, + empty=False, inline=False) + source = result[1] + expected = ( + "function [out1, out2] = test()\n" + " Catalan = 0.915965594177219; % constant\n" + " EulerGamma = 0.5772156649015329; % constant\n" + " out1 = pi^Catalan;\n" + " out2 = EulerGamma;\n" + "end\n" + ) + assert source == expected + + +def test_m_code_argument_order(): + expr = x + y + routine = make_routine("test", expr, argument_sequence=[z, x, y], language="octave") + code_gen = OctaveCodeGen() + output = StringIO() + code_gen.dump_m([routine], output, "test", header=False, empty=False) + source = output.getvalue() + expected = ( + "function out1 = test(z, x, y)\n" + " out1 = x + y;\n" + "end\n" + ) + assert source == expected + + +def test_multiple_results_m(): + # Here the output order is the input order + expr1 = (x + y)*z + expr2 = (x - y)*z + name_expr = ("test", [expr1, expr2]) + result, = codegen(name_expr, "Octave", header=False, empty=False) + source = result[1] + expected = ( + "function [out1, out2] = test(x, y, z)\n" + " out1 = z.*(x + y);\n" + " out2 = z.*(x - y);\n" + "end\n" + ) + assert source == expected + + +def test_results_named_unordered(): + # Here output order is based on name_expr + A, B, C = symbols('A,B,C') + expr1 = Equality(C, (x + y)*z) + expr2 = Equality(A, (x - y)*z) + expr3 = Equality(B, 2*x) + name_expr = ("test", [expr1, expr2, expr3]) + result, = codegen(name_expr, "Octave", header=False, empty=False) + source = result[1] + expected = ( + "function [C, A, B] = test(x, y, z)\n" + " C = z.*(x + y);\n" + " A = z.*(x - y);\n" + " B = 2*x;\n" + "end\n" + ) + assert source == expected + + +def test_results_named_ordered(): + A, B, C = symbols('A,B,C') + expr1 = Equality(C, (x + y)*z) + expr2 = Equality(A, (x - y)*z) + expr3 = Equality(B, 2*x) + name_expr = ("test", [expr1, expr2, expr3]) + result = codegen(name_expr, "Octave", header=False, empty=False, + argument_sequence=(x, z, y)) + assert result[0][0] == "test.m" + source = result[0][1] + expected = ( + "function [C, A, B] = test(x, z, y)\n" + " C = z.*(x + y);\n" + " A = z.*(x - y);\n" + " B = 2*x;\n" + "end\n" + ) + assert source == expected + + +def test_complicated_m_codegen(): + from sympy.functions.elementary.trigonometric import (cos, sin, tan) + name_expr = ("testlong", + [ ((sin(x) + cos(y) + tan(z))**3).expand(), + cos(cos(cos(cos(cos(cos(cos(cos(x + y + z)))))))) + ]) + result = codegen(name_expr, "Octave", header=False, empty=False) + assert result[0][0] == "testlong.m" + source = result[0][1] + expected = ( + "function [out1, out2] = testlong(x, y, z)\n" + " out1 = sin(x).^3 + 3*sin(x).^2.*cos(y) + 3*sin(x).^2.*tan(z)" + " + 3*sin(x).*cos(y).^2 + 6*sin(x).*cos(y).*tan(z) + 3*sin(x).*tan(z).^2" + " + cos(y).^3 + 3*cos(y).^2.*tan(z) + 3*cos(y).*tan(z).^2 + tan(z).^3;\n" + " out2 = cos(cos(cos(cos(cos(cos(cos(cos(x + y + z))))))));\n" + "end\n" + ) + assert source == expected + + +def test_m_output_arg_mixed_unordered(): + # named outputs are alphabetical, unnamed output appear in the given order + from sympy.functions.elementary.trigonometric import (cos, sin) + a = symbols("a") + name_expr = ("foo", [cos(2*x), Equality(y, sin(x)), cos(x), Equality(a, sin(2*x))]) + result, = codegen(name_expr, "Octave", header=False, empty=False) + assert result[0] == "foo.m" + source = result[1] + expected = ( + 'function [out1, y, out3, a] = foo(x)\n' + ' out1 = cos(2*x);\n' + ' y = sin(x);\n' + ' out3 = cos(x);\n' + ' a = sin(2*x);\n' + 'end\n' + ) + assert source == expected + + +def test_m_piecewise_(): + pw = Piecewise((0, x < -1), (x**2, x <= 1), (-x+2, x > 1), (1, True), evaluate=False) + name_expr = ("pwtest", pw) + result, = codegen(name_expr, "Octave", header=False, empty=False) + source = result[1] + expected = ( + "function out1 = pwtest(x)\n" + " out1 = ((x < -1).*(0) + (~(x < -1)).*( ...\n" + " (x <= 1).*(x.^2) + (~(x <= 1)).*( ...\n" + " (x > 1).*(2 - x) + (~(x > 1)).*(1))));\n" + "end\n" + ) + assert source == expected + + +@XFAIL +def test_m_piecewise_no_inline(): + # FIXME: how to pass inline=False to the OctaveCodePrinter? + pw = Piecewise((0, x < -1), (x**2, x <= 1), (-x+2, x > 1), (1, True)) + name_expr = ("pwtest", pw) + result, = codegen(name_expr, "Octave", header=False, empty=False, + inline=False) + source = result[1] + expected = ( + "function out1 = pwtest(x)\n" + " if (x < -1)\n" + " out1 = 0;\n" + " elseif (x <= 1)\n" + " out1 = x.^2;\n" + " elseif (x > 1)\n" + " out1 = -x + 2;\n" + " else\n" + " out1 = 1;\n" + " end\n" + "end\n" + ) + assert source == expected + + +def test_m_multifcns_per_file(): + name_expr = [ ("foo", [2*x, 3*y]), ("bar", [y**2, 4*y]) ] + result = codegen(name_expr, "Octave", header=False, empty=False) + assert result[0][0] == "foo.m" + source = result[0][1] + expected = ( + "function [out1, out2] = foo(x, y)\n" + " out1 = 2*x;\n" + " out2 = 3*y;\n" + "end\n" + "function [out1, out2] = bar(y)\n" + " out1 = y.^2;\n" + " out2 = 4*y;\n" + "end\n" + ) + assert source == expected + + +def test_m_multifcns_per_file_w_header(): + name_expr = [ ("foo", [2*x, 3*y]), ("bar", [y**2, 4*y]) ] + result = codegen(name_expr, "Octave", header=True, empty=False) + assert result[0][0] == "foo.m" + source = result[0][1] + expected = ( + "function [out1, out2] = foo(x, y)\n" + " %FOO Autogenerated by SymPy\n" + " % Code generated with SymPy " + sympy.__version__ + "\n" + " %\n" + " % See http://www.sympy.org/ for more information.\n" + " %\n" + " % This file is part of 'project'\n" + " out1 = 2*x;\n" + " out2 = 3*y;\n" + "end\n" + "function [out1, out2] = bar(y)\n" + " out1 = y.^2;\n" + " out2 = 4*y;\n" + "end\n" + ) + assert source == expected + + +def test_m_filename_match_first_fcn(): + name_expr = [ ("foo", [2*x, 3*y]), ("bar", [y**2, 4*y]) ] + raises(ValueError, lambda: codegen(name_expr, + "Octave", prefix="bar", header=False, empty=False)) + + +def test_m_matrix_named(): + e2 = Matrix([[x, 2*y, pi*z]]) + name_expr = ("test", Equality(MatrixSymbol('myout1', 1, 3), e2)) + result = codegen(name_expr, "Octave", header=False, empty=False) + assert result[0][0] == "test.m" + source = result[0][1] + expected = ( + "function myout1 = test(x, y, z)\n" + " myout1 = [x 2*y pi*z];\n" + "end\n" + ) + assert source == expected + + +def test_m_matrix_named_matsym(): + myout1 = MatrixSymbol('myout1', 1, 3) + e2 = Matrix([[x, 2*y, pi*z]]) + name_expr = ("test", Equality(myout1, e2, evaluate=False)) + result, = codegen(name_expr, "Octave", header=False, empty=False) + source = result[1] + expected = ( + "function myout1 = test(x, y, z)\n" + " myout1 = [x 2*y pi*z];\n" + "end\n" + ) + assert source == expected + + +def test_m_matrix_output_autoname(): + expr = Matrix([[x, x+y, 3]]) + name_expr = ("test", expr) + result, = codegen(name_expr, "Octave", header=False, empty=False) + source = result[1] + expected = ( + "function out1 = test(x, y)\n" + " out1 = [x x + y 3];\n" + "end\n" + ) + assert source == expected + + +def test_m_matrix_output_autoname_2(): + e1 = (x + y) + e2 = Matrix([[2*x, 2*y, 2*z]]) + e3 = Matrix([[x], [y], [z]]) + e4 = Matrix([[x, y], [z, 16]]) + name_expr = ("test", (e1, e2, e3, e4)) + result, = codegen(name_expr, "Octave", header=False, empty=False) + source = result[1] + expected = ( + "function [out1, out2, out3, out4] = test(x, y, z)\n" + " out1 = x + y;\n" + " out2 = [2*x 2*y 2*z];\n" + " out3 = [x; y; z];\n" + " out4 = [x y; z 16];\n" + "end\n" + ) + assert source == expected + + +def test_m_results_matrix_named_ordered(): + B, C = symbols('B,C') + A = MatrixSymbol('A', 1, 3) + expr1 = Equality(C, (x + y)*z) + expr2 = Equality(A, Matrix([[1, 2, x]])) + expr3 = Equality(B, 2*x) + name_expr = ("test", [expr1, expr2, expr3]) + result, = codegen(name_expr, "Octave", header=False, empty=False, + argument_sequence=(x, z, y)) + source = result[1] + expected = ( + "function [C, A, B] = test(x, z, y)\n" + " C = z.*(x + y);\n" + " A = [1 2 x];\n" + " B = 2*x;\n" + "end\n" + ) + assert source == expected + + +def test_m_matrixsymbol_slice(): + A = MatrixSymbol('A', 2, 3) + B = MatrixSymbol('B', 1, 3) + C = MatrixSymbol('C', 1, 3) + D = MatrixSymbol('D', 2, 1) + name_expr = ("test", [Equality(B, A[0, :]), + Equality(C, A[1, :]), + Equality(D, A[:, 2])]) + result, = codegen(name_expr, "Octave", header=False, empty=False) + source = result[1] + expected = ( + "function [B, C, D] = test(A)\n" + " B = A(1, :);\n" + " C = A(2, :);\n" + " D = A(:, 3);\n" + "end\n" + ) + assert source == expected + + +def test_m_matrixsymbol_slice2(): + A = MatrixSymbol('A', 3, 4) + B = MatrixSymbol('B', 2, 2) + C = MatrixSymbol('C', 2, 2) + name_expr = ("test", [Equality(B, A[0:2, 0:2]), + Equality(C, A[0:2, 1:3])]) + result, = codegen(name_expr, "Octave", header=False, empty=False) + source = result[1] + expected = ( + "function [B, C] = test(A)\n" + " B = A(1:2, 1:2);\n" + " C = A(1:2, 2:3);\n" + "end\n" + ) + assert source == expected + + +def test_m_matrixsymbol_slice3(): + A = MatrixSymbol('A', 8, 7) + B = MatrixSymbol('B', 2, 2) + C = MatrixSymbol('C', 4, 2) + name_expr = ("test", [Equality(B, A[6:, 1::3]), + Equality(C, A[::2, ::3])]) + result, = codegen(name_expr, "Octave", header=False, empty=False) + source = result[1] + expected = ( + "function [B, C] = test(A)\n" + " B = A(7:end, 2:3:end);\n" + " C = A(1:2:end, 1:3:end);\n" + "end\n" + ) + assert source == expected + + +def test_m_matrixsymbol_slice_autoname(): + A = MatrixSymbol('A', 2, 3) + B = MatrixSymbol('B', 1, 3) + name_expr = ("test", [Equality(B, A[0,:]), A[1,:], A[:,0], A[:,1]]) + result, = codegen(name_expr, "Octave", header=False, empty=False) + source = result[1] + expected = ( + "function [B, out2, out3, out4] = test(A)\n" + " B = A(1, :);\n" + " out2 = A(2, :);\n" + " out3 = A(:, 1);\n" + " out4 = A(:, 2);\n" + "end\n" + ) + assert source == expected + + +def test_m_loops(): + # Note: an Octave programmer would probably vectorize this across one or + # more dimensions. Also, size(A) would be used rather than passing in m + # and n. Perhaps users would expect us to vectorize automatically here? + # Or is it possible to represent such things using IndexedBase? + from sympy.tensor import IndexedBase, Idx + from sympy.core.symbol import symbols + n, m = symbols('n m', integer=True) + A = IndexedBase('A') + x = IndexedBase('x') + y = IndexedBase('y') + i = Idx('i', m) + j = Idx('j', n) + result, = codegen(('mat_vec_mult', Eq(y[i], A[i, j]*x[j])), "Octave", + header=False, empty=False) + source = result[1] + expected = ( + 'function y = mat_vec_mult(A, m, n, x)\n' + ' for i = 1:m\n' + ' y(i) = 0;\n' + ' end\n' + ' for i = 1:m\n' + ' for j = 1:n\n' + ' y(i) = %(rhs)s + y(i);\n' + ' end\n' + ' end\n' + 'end\n' + ) + assert (source == expected % {'rhs': 'A(%s, %s).*x(j)' % (i, j)} or + source == expected % {'rhs': 'x(j).*A(%s, %s)' % (i, j)}) + + +def test_m_tensor_loops_multiple_contractions(): + # see comments in previous test about vectorizing + from sympy.tensor import IndexedBase, Idx + from sympy.core.symbol import symbols + n, m, o, p = symbols('n m o p', integer=True) + A = IndexedBase('A') + B = IndexedBase('B') + y = IndexedBase('y') + i = Idx('i', m) + j = Idx('j', n) + k = Idx('k', o) + l = Idx('l', p) + result, = codegen(('tensorthing', Eq(y[i], B[j, k, l]*A[i, j, k, l])), + "Octave", header=False, empty=False) + source = result[1] + expected = ( + 'function y = tensorthing(A, B, m, n, o, p)\n' + ' for i = 1:m\n' + ' y(i) = 0;\n' + ' end\n' + ' for i = 1:m\n' + ' for j = 1:n\n' + ' for k = 1:o\n' + ' for l = 1:p\n' + ' y(i) = A(i, j, k, l).*B(j, k, l) + y(i);\n' + ' end\n' + ' end\n' + ' end\n' + ' end\n' + 'end\n' + ) + assert source == expected + + +def test_m_InOutArgument(): + expr = Equality(x, x**2) + name_expr = ("mysqr", expr) + result, = codegen(name_expr, "Octave", header=False, empty=False) + source = result[1] + expected = ( + "function x = mysqr(x)\n" + " x = x.^2;\n" + "end\n" + ) + assert source == expected + + +def test_m_InOutArgument_order(): + # can specify the order as (x, y) + expr = Equality(x, x**2 + y) + name_expr = ("test", expr) + result, = codegen(name_expr, "Octave", header=False, + empty=False, argument_sequence=(x,y)) + source = result[1] + expected = ( + "function x = test(x, y)\n" + " x = x.^2 + y;\n" + "end\n" + ) + assert source == expected + # make sure it gives (x, y) not (y, x) + expr = Equality(x, x**2 + y) + name_expr = ("test", expr) + result, = codegen(name_expr, "Octave", header=False, empty=False) + source = result[1] + expected = ( + "function x = test(x, y)\n" + " x = x.^2 + y;\n" + "end\n" + ) + assert source == expected + + +def test_m_not_supported(): + f = Function('f') + name_expr = ("test", [f(x).diff(x), S.ComplexInfinity]) + result, = codegen(name_expr, "Octave", header=False, empty=False) + source = result[1] + expected = ( + "function [out1, out2] = test(x)\n" + " % unsupported: Derivative(f(x), x)\n" + " % unsupported: zoo\n" + " out1 = Derivative(f(x), x);\n" + " out2 = zoo;\n" + "end\n" + ) + assert source == expected + + +def test_global_vars_octave(): + x, y, z, t = symbols("x y z t") + result = codegen(('f', x*y), "Octave", header=False, empty=False, + global_vars=(y,)) + source = result[0][1] + expected = ( + "function out1 = f(x)\n" + " global y\n" + " out1 = x.*y;\n" + "end\n" + ) + assert source == expected + + result = codegen(('f', x*y+z), "Octave", header=False, empty=False, + argument_sequence=(x, y), global_vars=(z, t)) + source = result[0][1] + expected = ( + "function out1 = f(x, y)\n" + " global t z\n" + " out1 = x.*y + z;\n" + "end\n" + ) + assert source == expected diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_codegen_rust.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_codegen_rust.py new file mode 100644 index 0000000000000000000000000000000000000000..bc7f82158ae8fa7dfe34bf909aa695b119fb9526 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_codegen_rust.py @@ -0,0 +1,401 @@ +from io import StringIO + +from sympy.core import S, symbols, pi, Catalan, EulerGamma, Function +from sympy.core.relational import Equality +from sympy.functions.elementary.piecewise import Piecewise +from sympy.utilities.codegen import RustCodeGen, codegen, make_routine +from sympy.testing.pytest import XFAIL +import sympy + + +x, y, z = symbols('x,y,z') + + +def test_empty_rust_code(): + code_gen = RustCodeGen() + output = StringIO() + code_gen.dump_rs([], output, "file", header=False, empty=False) + source = output.getvalue() + assert source == "" + + +def test_simple_rust_code(): + name_expr = ("test", (x + y)*z) + result, = codegen(name_expr, "Rust", header=False, empty=False) + assert result[0] == "test.rs" + source = result[1] + expected = ( + "fn test(x: f64, y: f64, z: f64) -> f64 {\n" + " let out1 = z*(x + y);\n" + " out1\n" + "}\n" + ) + assert source == expected + + +def test_simple_code_with_header(): + name_expr = ("test", (x + y)*z) + result, = codegen(name_expr, "Rust", header=True, empty=False) + assert result[0] == "test.rs" + source = result[1] + version_str = "Code generated with SymPy %s" % sympy.__version__ + version_line = version_str.center(76).rstrip() + expected = ( + "/*\n" + " *%(version_line)s\n" + " *\n" + " * See http://www.sympy.org/ for more information.\n" + " *\n" + " * This file is part of 'project'\n" + " */\n" + "fn test(x: f64, y: f64, z: f64) -> f64 {\n" + " let out1 = z*(x + y);\n" + " out1\n" + "}\n" + ) % {'version_line': version_line} + assert source == expected + + +def test_simple_code_nameout(): + expr = Equality(z, (x + y)) + name_expr = ("test", expr) + result, = codegen(name_expr, "Rust", header=False, empty=False) + source = result[1] + expected = ( + "fn test(x: f64, y: f64) -> f64 {\n" + " let z = x + y;\n" + " z\n" + "}\n" + ) + assert source == expected + + +def test_numbersymbol(): + name_expr = ("test", pi**Catalan) + result, = codegen(name_expr, "Rust", header=False, empty=False) + source = result[1] + expected = ( + "fn test() -> f64 {\n" + " const Catalan: f64 = %s;\n" + " let out1 = PI.powf(Catalan);\n" + " out1\n" + "}\n" + ) % Catalan.evalf(17) + assert source == expected + + +@XFAIL +def test_numbersymbol_inline(): + # FIXME: how to pass inline to the RustCodePrinter? + name_expr = ("test", [pi**Catalan, EulerGamma]) + result, = codegen(name_expr, "Rust", header=False, + empty=False, inline=True) + source = result[1] + expected = ( + "fn test() -> (f64, f64) {\n" + " const Catalan: f64 = %s;\n" + " const EulerGamma: f64 = %s;\n" + " let out1 = PI.powf(Catalan);\n" + " let out2 = EulerGamma);\n" + " (out1, out2)\n" + "}\n" + ) % (Catalan.evalf(17), EulerGamma.evalf(17)) + assert source == expected + + +def test_argument_order(): + expr = x + y + routine = make_routine("test", expr, argument_sequence=[z, x, y], language="rust") + code_gen = RustCodeGen() + output = StringIO() + code_gen.dump_rs([routine], output, "test", header=False, empty=False) + source = output.getvalue() + expected = ( + "fn test(z: f64, x: f64, y: f64) -> f64 {\n" + " let out1 = x + y;\n" + " out1\n" + "}\n" + ) + assert source == expected + + +def test_multiple_results_rust(): + # Here the output order is the input order + expr1 = (x + y)*z + expr2 = (x - y)*z + name_expr = ("test", [expr1, expr2]) + result, = codegen(name_expr, "Rust", header=False, empty=False) + source = result[1] + expected = ( + "fn test(x: f64, y: f64, z: f64) -> (f64, f64) {\n" + " let out1 = z*(x + y);\n" + " let out2 = z*(x - y);\n" + " (out1, out2)\n" + "}\n" + ) + assert source == expected + + +def test_results_named_unordered(): + # Here output order is based on name_expr + A, B, C = symbols('A,B,C') + expr1 = Equality(C, (x + y)*z) + expr2 = Equality(A, (x - y)*z) + expr3 = Equality(B, 2*x) + name_expr = ("test", [expr1, expr2, expr3]) + result, = codegen(name_expr, "Rust", header=False, empty=False) + source = result[1] + expected = ( + "fn test(x: f64, y: f64, z: f64) -> (f64, f64, f64) {\n" + " let C = z*(x + y);\n" + " let A = z*(x - y);\n" + " let B = 2*x;\n" + " (C, A, B)\n" + "}\n" + ) + assert source == expected + + +def test_results_named_ordered(): + A, B, C = symbols('A,B,C') + expr1 = Equality(C, (x + y)*z) + expr2 = Equality(A, (x - y)*z) + expr3 = Equality(B, 2*x) + name_expr = ("test", [expr1, expr2, expr3]) + result = codegen(name_expr, "Rust", header=False, empty=False, + argument_sequence=(x, z, y)) + assert result[0][0] == "test.rs" + source = result[0][1] + expected = ( + "fn test(x: f64, z: f64, y: f64) -> (f64, f64, f64) {\n" + " let C = z*(x + y);\n" + " let A = z*(x - y);\n" + " let B = 2*x;\n" + " (C, A, B)\n" + "}\n" + ) + assert source == expected + + +def test_complicated_rs_codegen(): + from sympy.functions.elementary.trigonometric import (cos, sin, tan) + name_expr = ("testlong", + [ ((sin(x) + cos(y) + tan(z))**3).expand(), + cos(cos(cos(cos(cos(cos(cos(cos(x + y + z)))))))) + ]) + result = codegen(name_expr, "Rust", header=False, empty=False) + assert result[0][0] == "testlong.rs" + source = result[0][1] + expected = ( + "fn testlong(x: f64, y: f64, z: f64) -> (f64, f64) {\n" + " let out1 = x.sin().powi(3) + 3*x.sin().powi(2)*y.cos()" + " + 3*x.sin().powi(2)*z.tan() + 3*x.sin()*y.cos().powi(2)" + " + 6*x.sin()*y.cos()*z.tan() + 3*x.sin()*z.tan().powi(2)" + " + y.cos().powi(3) + 3*y.cos().powi(2)*z.tan()" + " + 3*y.cos()*z.tan().powi(2) + z.tan().powi(3);\n" + " let out2 = (x + y + z).cos().cos().cos().cos()" + ".cos().cos().cos().cos();\n" + " (out1, out2)\n" + "}\n" + ) + assert source == expected + + +def test_output_arg_mixed_unordered(): + # named outputs are alphabetical, unnamed output appear in the given order + from sympy.functions.elementary.trigonometric import (cos, sin) + a = symbols("a") + name_expr = ("foo", [cos(2*x), Equality(y, sin(x)), cos(x), Equality(a, sin(2*x))]) + result, = codegen(name_expr, "Rust", header=False, empty=False) + assert result[0] == "foo.rs" + source = result[1] + expected = ( + "fn foo(x: f64) -> (f64, f64, f64, f64) {\n" + " let out1 = (2*x).cos();\n" + " let y = x.sin();\n" + " let out3 = x.cos();\n" + " let a = (2*x).sin();\n" + " (out1, y, out3, a)\n" + "}\n" + ) + assert source == expected + + +def test_piecewise_(): + pw = Piecewise((0, x < -1), (x**2, x <= 1), (-x+2, x > 1), (1, True), evaluate=False) + name_expr = ("pwtest", pw) + result, = codegen(name_expr, "Rust", header=False, empty=False) + source = result[1] + expected = ( + "fn pwtest(x: f64) -> f64 {\n" + " let out1 = if (x < -1.0) {\n" + " 0\n" + " } else if (x <= 1.0) {\n" + " x.powi(2)\n" + " } else if (x > 1.0) {\n" + " 2 - x\n" + " } else {\n" + " 1\n" + " };\n" + " out1\n" + "}\n" + ) + assert source == expected + + +@XFAIL +def test_piecewise_inline(): + # FIXME: how to pass inline to the RustCodePrinter? + pw = Piecewise((0, x < -1), (x**2, x <= 1), (-x+2, x > 1), (1, True)) + name_expr = ("pwtest", pw) + result, = codegen(name_expr, "Rust", header=False, empty=False, + inline=True) + source = result[1] + expected = ( + "fn pwtest(x: f64) -> f64 {\n" + " let out1 = if (x < -1) { 0 } else if (x <= 1) { x.powi(2) }" + " else if (x > 1) { -x + 2 } else { 1 };\n" + " out1\n" + "}\n" + ) + assert source == expected + + +def test_multifcns_per_file(): + name_expr = [ ("foo", [2*x, 3*y]), ("bar", [y**2, 4*y]) ] + result = codegen(name_expr, "Rust", header=False, empty=False) + assert result[0][0] == "foo.rs" + source = result[0][1] + expected = ( + "fn foo(x: f64, y: f64) -> (f64, f64) {\n" + " let out1 = 2*x;\n" + " let out2 = 3*y;\n" + " (out1, out2)\n" + "}\n" + "fn bar(y: f64) -> (f64, f64) {\n" + " let out1 = y.powi(2);\n" + " let out2 = 4*y;\n" + " (out1, out2)\n" + "}\n" + ) + assert source == expected + + +def test_multifcns_per_file_w_header(): + name_expr = [ ("foo", [2*x, 3*y]), ("bar", [y**2, 4*y]) ] + result = codegen(name_expr, "Rust", header=True, empty=False) + assert result[0][0] == "foo.rs" + source = result[0][1] + version_str = "Code generated with SymPy %s" % sympy.__version__ + version_line = version_str.center(76).rstrip() + expected = ( + "/*\n" + " *%(version_line)s\n" + " *\n" + " * See http://www.sympy.org/ for more information.\n" + " *\n" + " * This file is part of 'project'\n" + " */\n" + "fn foo(x: f64, y: f64) -> (f64, f64) {\n" + " let out1 = 2*x;\n" + " let out2 = 3*y;\n" + " (out1, out2)\n" + "}\n" + "fn bar(y: f64) -> (f64, f64) {\n" + " let out1 = y.powi(2);\n" + " let out2 = 4*y;\n" + " (out1, out2)\n" + "}\n" + ) % {'version_line': version_line} + assert source == expected + + +def test_filename_match_prefix(): + name_expr = [ ("foo", [2*x, 3*y]), ("bar", [y**2, 4*y]) ] + result, = codegen(name_expr, "Rust", prefix="baz", header=False, + empty=False) + assert result[0] == "baz.rs" + + +def test_InOutArgument(): + expr = Equality(x, x**2) + name_expr = ("mysqr", expr) + result, = codegen(name_expr, "Rust", header=False, empty=False) + source = result[1] + expected = ( + "fn mysqr(x: f64) -> f64 {\n" + " let x = x.powi(2);\n" + " x\n" + "}\n" + ) + assert source == expected + + +def test_InOutArgument_order(): + # can specify the order as (x, y) + expr = Equality(x, x**2 + y) + name_expr = ("test", expr) + result, = codegen(name_expr, "Rust", header=False, + empty=False, argument_sequence=(x,y)) + source = result[1] + expected = ( + "fn test(x: f64, y: f64) -> f64 {\n" + " let x = x.powi(2) + y;\n" + " x\n" + "}\n" + ) + assert source == expected + # make sure it gives (x, y) not (y, x) + expr = Equality(x, x**2 + y) + name_expr = ("test", expr) + result, = codegen(name_expr, "Rust", header=False, empty=False) + source = result[1] + expected = ( + "fn test(x: f64, y: f64) -> f64 {\n" + " let x = x.powi(2) + y;\n" + " x\n" + "}\n" + ) + assert source == expected + + +def test_not_supported(): + f = Function('f') + name_expr = ("test", [f(x).diff(x), S.ComplexInfinity]) + result, = codegen(name_expr, "Rust", header=False, empty=False) + source = result[1] + expected = ( + "fn test(x: f64) -> (f64, f64) {\n" + " // unsupported: Derivative(f(x), x)\n" + " // unsupported: zoo\n" + " let out1 = Derivative(f(x), x);\n" + " let out2 = zoo;\n" + " (out1, out2)\n" + "}\n" + ) + assert source == expected + + +def test_global_vars_rust(): + x, y, z, t = symbols("x y z t") + result = codegen(('f', x*y), "Rust", header=False, empty=False, + global_vars=(y,)) + source = result[0][1] + expected = ( + "fn f(x: f64) -> f64 {\n" + " let out1 = x*y;\n" + " out1\n" + "}\n" + ) + assert source == expected + + result = codegen(('f', x*y+z), "Rust", header=False, empty=False, + argument_sequence=(x, y), global_vars=(z, t)) + source = result[0][1] + expected = ( + "fn f(x: f64, y: f64) -> f64 {\n" + " let out1 = x*y + z;\n" + " out1\n" + "}\n" + ) + assert source == expected diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_decorator.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_decorator.py new file mode 100644 index 0000000000000000000000000000000000000000..b1870d4db8f719fdabfeab14120bfb3ce10131a9 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_decorator.py @@ -0,0 +1,129 @@ +from functools import wraps + +from sympy.utilities.decorator import threaded, xthreaded, memoize_property, deprecated +from sympy.testing.pytest import warns_deprecated_sympy + +from sympy.core.basic import Basic +from sympy.core.relational import Eq +from sympy.matrices.dense import Matrix + +from sympy.abc import x, y + + +def test_threaded(): + @threaded + def function(expr, *args): + return 2*expr + sum(args) + + assert function(Matrix([[x, y], [1, x]]), 1, 2) == \ + Matrix([[2*x + 3, 2*y + 3], [5, 2*x + 3]]) + + assert function(Eq(x, y), 1, 2) == Eq(2*x + 3, 2*y + 3) + + assert function([x, y], 1, 2) == [2*x + 3, 2*y + 3] + assert function((x, y), 1, 2) == (2*x + 3, 2*y + 3) + + assert function({x, y}, 1, 2) == {2*x + 3, 2*y + 3} + + @threaded + def function(expr, n): + return expr**n + + assert function(x + y, 2) == x**2 + y**2 + assert function(x, 2) == x**2 + + +def test_xthreaded(): + @xthreaded + def function(expr, n): + return expr**n + + assert function(x + y, 2) == (x + y)**2 + + +def test_wraps(): + def my_func(x): + """My function. """ + + my_func.is_my_func = True + + new_my_func = threaded(my_func) + new_my_func = wraps(my_func)(new_my_func) + + assert new_my_func.__name__ == 'my_func' + assert new_my_func.__doc__ == 'My function. ' + assert hasattr(new_my_func, 'is_my_func') + assert new_my_func.is_my_func is True + + +def test_memoize_property(): + class TestMemoize(Basic): + @memoize_property + def prop(self): + return Basic() + + member = TestMemoize() + obj1 = member.prop + obj2 = member.prop + assert obj1 is obj2 + +def test_deprecated(): + @deprecated('deprecated_function is deprecated', + deprecated_since_version='1.10', + # This is the target at the top of the file, which will never + # go away. + active_deprecations_target='active-deprecations') + def deprecated_function(x): + return x + + with warns_deprecated_sympy(): + assert deprecated_function(1) == 1 + + @deprecated('deprecated_class is deprecated', + deprecated_since_version='1.10', + active_deprecations_target='active-deprecations') + class deprecated_class: + pass + + with warns_deprecated_sympy(): + assert isinstance(deprecated_class(), deprecated_class) + + # Ensure the class decorator works even when the class never returns + # itself + @deprecated('deprecated_class_new is deprecated', + deprecated_since_version='1.10', + active_deprecations_target='active-deprecations') + class deprecated_class_new: + def __new__(cls, arg): + return arg + + with warns_deprecated_sympy(): + assert deprecated_class_new(1) == 1 + + @deprecated('deprecated_class_init is deprecated', + deprecated_since_version='1.10', + active_deprecations_target='active-deprecations') + class deprecated_class_init: + def __init__(self, arg): + self.arg = 1 + + with warns_deprecated_sympy(): + assert deprecated_class_init(1).arg == 1 + + @deprecated('deprecated_class_new_init is deprecated', + deprecated_since_version='1.10', + active_deprecations_target='active-deprecations') + class deprecated_class_new_init: + def __new__(cls, arg): + if arg == 0: + return arg + return object.__new__(cls) + + def __init__(self, arg): + self.arg = 1 + + with warns_deprecated_sympy(): + assert deprecated_class_new_init(0) == 0 + + with warns_deprecated_sympy(): + assert deprecated_class_new_init(1).arg == 1 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_deprecated.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_deprecated.py new file mode 100644 index 0000000000000000000000000000000000000000..dd4534ef1abc38ff368011b3ef9d11c497f3675b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_deprecated.py @@ -0,0 +1,13 @@ +from sympy.testing.pytest import warns_deprecated_sympy + +# See https://github.com/sympy/sympy/pull/18095 + +def test_deprecated_utilities(): + with warns_deprecated_sympy(): + import sympy.utilities.pytest # noqa:F401 + with warns_deprecated_sympy(): + import sympy.utilities.runtests # noqa:F401 + with warns_deprecated_sympy(): + import sympy.utilities.randtest # noqa:F401 + with warns_deprecated_sympy(): + import sympy.utilities.tmpfiles # noqa:F401 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_enumerative.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_enumerative.py new file mode 100644 index 0000000000000000000000000000000000000000..357499d5fd400b14e2bcad067f3015b74b0e9003 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_enumerative.py @@ -0,0 +1,179 @@ +import string +from itertools import zip_longest + +from sympy.utilities.enumerative import ( + list_visitor, + MultisetPartitionTraverser, + multiset_partitions_taocp + ) +from sympy.utilities.iterables import _set_partitions + +# first some functions only useful as test scaffolding - these provide +# straightforward, but slow reference implementations against which to +# compare the real versions, and also a comparison to verify that +# different versions are giving identical results. + +def part_range_filter(partition_iterator, lb, ub): + """ + Filters (on the number of parts) a multiset partition enumeration + + Arguments + ========= + + lb, and ub are a range (in the Python slice sense) on the lpart + variable returned from a multiset partition enumeration. Recall + that lpart is 0-based (it points to the topmost part on the part + stack), so if you want to return parts of sizes 2,3,4,5 you would + use lb=1 and ub=5. + """ + for state in partition_iterator: + f, lpart, pstack = state + if lpart >= lb and lpart < ub: + yield state + +def multiset_partitions_baseline(multiplicities, components): + """Enumerates partitions of a multiset + + Parameters + ========== + + multiplicities + list of integer multiplicities of the components of the multiset. + + components + the components (elements) themselves + + Returns + ======= + + Set of partitions. Each partition is tuple of parts, and each + part is a tuple of components (with repeats to indicate + multiplicity) + + Notes + ===== + + Multiset partitions can be created as equivalence classes of set + partitions, and this function does just that. This approach is + slow and memory intensive compared to the more advanced algorithms + available, but the code is simple and easy to understand. Hence + this routine is strictly for testing -- to provide a + straightforward baseline against which to regress the production + versions. (This code is a simplified version of an earlier + production implementation.) + """ + + canon = [] # list of components with repeats + for ct, elem in zip(multiplicities, components): + canon.extend([elem]*ct) + + # accumulate the multiset partitions in a set to eliminate dups + cache = set() + n = len(canon) + for nc, q in _set_partitions(n): + rv = [[] for i in range(nc)] + for i in range(n): + rv[q[i]].append(canon[i]) + canonical = tuple( + sorted([tuple(p) for p in rv])) + cache.add(canonical) + return cache + + +def compare_multiset_w_baseline(multiplicities): + """ + Enumerates the partitions of multiset with AOCP algorithm and + baseline implementation, and compare the results. + + """ + letters = string.ascii_lowercase + bl_partitions = multiset_partitions_baseline(multiplicities, letters) + + # The partitions returned by the different algorithms may have + # their parts in different orders. Also, they generate partitions + # in different orders. Hence the sorting, and set comparison. + + aocp_partitions = set() + for state in multiset_partitions_taocp(multiplicities): + p1 = tuple(sorted( + [tuple(p) for p in list_visitor(state, letters)])) + aocp_partitions.add(p1) + + assert bl_partitions == aocp_partitions + +def compare_multiset_states(s1, s2): + """compare for equality two instances of multiset partition states + + This is useful for comparing different versions of the algorithm + to verify correctness.""" + # Comparison is physical, the only use of semantics is to ignore + # trash off the top of the stack. + f1, lpart1, pstack1 = s1 + f2, lpart2, pstack2 = s2 + + if (lpart1 == lpart2) and (f1[0:lpart1+1] == f2[0:lpart2+1]): + if pstack1[0:f1[lpart1+1]] == pstack2[0:f2[lpart2+1]]: + return True + return False + +def test_multiset_partitions_taocp(): + """Compares the output of multiset_partitions_taocp with a baseline + (set partition based) implementation.""" + + # Test cases should not be too large, since the baseline + # implementation is fairly slow. + multiplicities = [2,2] + compare_multiset_w_baseline(multiplicities) + + multiplicities = [4,3,1] + compare_multiset_w_baseline(multiplicities) + +def test_multiset_partitions_versions(): + """Compares Knuth-based versions of multiset_partitions""" + multiplicities = [5,2,2,1] + m = MultisetPartitionTraverser() + for s1, s2 in zip_longest(m.enum_all(multiplicities), + multiset_partitions_taocp(multiplicities)): + assert compare_multiset_states(s1, s2) + +def subrange_exercise(mult, lb, ub): + """Compare filter-based and more optimized subrange implementations + + Helper for tests, called with both small and larger multisets. + """ + m = MultisetPartitionTraverser() + assert m.count_partitions(mult) == \ + m.count_partitions_slow(mult) + + # Note - multiple traversals from the same + # MultisetPartitionTraverser object cannot execute at the same + # time, hence make several instances here. + ma = MultisetPartitionTraverser() + mc = MultisetPartitionTraverser() + md = MultisetPartitionTraverser() + + # Several paths to compute just the size two partitions + a_it = ma.enum_range(mult, lb, ub) + b_it = part_range_filter(multiset_partitions_taocp(mult), lb, ub) + c_it = part_range_filter(mc.enum_small(mult, ub), lb, sum(mult)) + d_it = part_range_filter(md.enum_large(mult, lb), 0, ub) + + for sa, sb, sc, sd in zip_longest(a_it, b_it, c_it, d_it): + assert compare_multiset_states(sa, sb) + assert compare_multiset_states(sa, sc) + assert compare_multiset_states(sa, sd) + +def test_subrange(): + # Quick, but doesn't hit some of the corner cases + mult = [4,4,2,1] # mississippi + lb = 1 + ub = 2 + subrange_exercise(mult, lb, ub) + + +def test_subrange_large(): + # takes a second or so, depending on cpu, Python version, etc. + mult = [6,3,2,1] + lb = 4 + ub = 7 + subrange_exercise(mult, lb, ub) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_exceptions.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..d91e55e95d0ae4ac57cdd1989e0b3d39a55cd07d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_exceptions.py @@ -0,0 +1,12 @@ +from sympy.testing.pytest import raises +from sympy.utilities.exceptions import sympy_deprecation_warning + +# Only test exceptions here because the other cases are tested in the +# warns_deprecated_sympy tests +def test_sympy_deprecation_warning(): + raises(TypeError, lambda: sympy_deprecation_warning('test', + deprecated_since_version=1.10, + active_deprecations_target='active-deprecations')) + + raises(ValueError, lambda: sympy_deprecation_warning('test', + deprecated_since_version="1.10", active_deprecations_target='(active-deprecations)=')) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_iterables.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_iterables.py new file mode 100644 index 0000000000000000000000000000000000000000..1003522bcd556c6f63e04de7da57b43498575fee --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_iterables.py @@ -0,0 +1,945 @@ +from textwrap import dedent +from itertools import islice, product + +from sympy.core.basic import Basic +from sympy.core.numbers import Integer +from sympy.core.sorting import ordered +from sympy.core.symbol import (Dummy, symbols) +from sympy.functions.combinatorial.factorials import factorial +from sympy.matrices.dense import Matrix +from sympy.combinatorics import RGS_enum, RGS_unrank, Permutation +from sympy.utilities.iterables import ( + _partition, _set_partitions, binary_partitions, bracelets, capture, + cartes, common_prefix, common_suffix, connected_components, dict_merge, + filter_symbols, flatten, generate_bell, generate_derangements, + generate_involutions, generate_oriented_forest, group, has_dups, ibin, + iproduct, kbins, minlex, multiset, multiset_combinations, + multiset_partitions, multiset_permutations, necklaces, numbered_symbols, + partitions, permutations, postfixes, + prefixes, reshape, rotate_left, rotate_right, runs, sift, + strongly_connected_components, subsets, take, topological_sort, unflatten, + uniq, variations, ordered_partitions, rotations, is_palindromic, iterable, + NotIterable, multiset_derangements, signed_permutations, + sequence_partitions, sequence_partitions_empty) +from sympy.utilities.enumerative import ( + factoring_visitor, multiset_partitions_taocp ) + +from sympy.core.singleton import S +from sympy.testing.pytest import raises, warns_deprecated_sympy + +w, x, y, z = symbols('w,x,y,z') + + +def test_deprecated_iterables(): + from sympy.utilities.iterables import default_sort_key, ordered + with warns_deprecated_sympy(): + assert list(ordered([y, x])) == [x, y] + with warns_deprecated_sympy(): + assert sorted([y, x], key=default_sort_key) == [x, y] + + +def test_is_palindromic(): + assert is_palindromic('') + assert is_palindromic('x') + assert is_palindromic('xx') + assert is_palindromic('xyx') + assert not is_palindromic('xy') + assert not is_palindromic('xyzx') + assert is_palindromic('xxyzzyx', 1) + assert not is_palindromic('xxyzzyx', 2) + assert is_palindromic('xxyzzyx', 2, -1) + assert is_palindromic('xxyzzyx', 2, 6) + assert is_palindromic('xxyzyx', 1) + assert not is_palindromic('xxyzyx', 2) + assert is_palindromic('xxyzyx', 2, 2 + 3) + + +def test_flatten(): + assert flatten((1, (1,))) == [1, 1] + assert flatten((x, (x,))) == [x, x] + + ls = [[(-2, -1), (1, 2)], [(0, 0)]] + + assert flatten(ls, levels=0) == ls + assert flatten(ls, levels=1) == [(-2, -1), (1, 2), (0, 0)] + assert flatten(ls, levels=2) == [-2, -1, 1, 2, 0, 0] + assert flatten(ls, levels=3) == [-2, -1, 1, 2, 0, 0] + + raises(ValueError, lambda: flatten(ls, levels=-1)) + + class MyOp(Basic): + pass + + assert flatten([MyOp(x, y), z]) == [MyOp(x, y), z] + assert flatten([MyOp(x, y), z], cls=MyOp) == [x, y, z] + + assert flatten({1, 11, 2}) == list({1, 11, 2}) + + +def test_iproduct(): + assert list(iproduct()) == [()] + assert list(iproduct([])) == [] + assert list(iproduct([1,2,3])) == [(1,),(2,),(3,)] + assert sorted(iproduct([1, 2], [3, 4, 5])) == [ + (1,3),(1,4),(1,5),(2,3),(2,4),(2,5)] + assert sorted(iproduct([0,1],[0,1],[0,1])) == [ + (0,0,0),(0,0,1),(0,1,0),(0,1,1),(1,0,0),(1,0,1),(1,1,0),(1,1,1)] + assert iterable(iproduct(S.Integers)) is True + assert iterable(iproduct(S.Integers, S.Integers)) is True + assert (3,) in iproduct(S.Integers) + assert (4, 5) in iproduct(S.Integers, S.Integers) + assert (1, 2, 3) in iproduct(S.Integers, S.Integers, S.Integers) + triples = set(islice(iproduct(S.Integers, S.Integers, S.Integers), 1000)) + for n1, n2, n3 in triples: + assert isinstance(n1, Integer) + assert isinstance(n2, Integer) + assert isinstance(n3, Integer) + for t in set(product(*([range(-2, 3)]*3))): + assert t in iproduct(S.Integers, S.Integers, S.Integers) + + +def test_group(): + assert group([]) == [] + assert group([], multiple=False) == [] + + assert group([1]) == [[1]] + assert group([1], multiple=False) == [(1, 1)] + + assert group([1, 1]) == [[1, 1]] + assert group([1, 1], multiple=False) == [(1, 2)] + + assert group([1, 1, 1]) == [[1, 1, 1]] + assert group([1, 1, 1], multiple=False) == [(1, 3)] + + assert group([1, 2, 1]) == [[1], [2], [1]] + assert group([1, 2, 1], multiple=False) == [(1, 1), (2, 1), (1, 1)] + + assert group([1, 1, 2, 2, 2, 1, 3, 3]) == [[1, 1], [2, 2, 2], [1], [3, 3]] + assert group([1, 1, 2, 2, 2, 1, 3, 3], multiple=False) == [(1, 2), + (2, 3), (1, 1), (3, 2)] + + +def test_subsets(): + # combinations + assert list(subsets([1, 2, 3], 0)) == [()] + assert list(subsets([1, 2, 3], 1)) == [(1,), (2,), (3,)] + assert list(subsets([1, 2, 3], 2)) == [(1, 2), (1, 3), (2, 3)] + assert list(subsets([1, 2, 3], 3)) == [(1, 2, 3)] + l = list(range(4)) + assert list(subsets(l, 0, repetition=True)) == [()] + assert list(subsets(l, 1, repetition=True)) == [(0,), (1,), (2,), (3,)] + assert list(subsets(l, 2, repetition=True)) == [(0, 0), (0, 1), (0, 2), + (0, 3), (1, 1), (1, 2), + (1, 3), (2, 2), (2, 3), + (3, 3)] + assert list(subsets(l, 3, repetition=True)) == [(0, 0, 0), (0, 0, 1), + (0, 0, 2), (0, 0, 3), + (0, 1, 1), (0, 1, 2), + (0, 1, 3), (0, 2, 2), + (0, 2, 3), (0, 3, 3), + (1, 1, 1), (1, 1, 2), + (1, 1, 3), (1, 2, 2), + (1, 2, 3), (1, 3, 3), + (2, 2, 2), (2, 2, 3), + (2, 3, 3), (3, 3, 3)] + assert len(list(subsets(l, 4, repetition=True))) == 35 + + assert list(subsets(l[:2], 3, repetition=False)) == [] + assert list(subsets(l[:2], 3, repetition=True)) == [(0, 0, 0), + (0, 0, 1), + (0, 1, 1), + (1, 1, 1)] + assert list(subsets([1, 2], repetition=True)) == \ + [(), (1,), (2,), (1, 1), (1, 2), (2, 2)] + assert list(subsets([1, 2], repetition=False)) == \ + [(), (1,), (2,), (1, 2)] + assert list(subsets([1, 2, 3], 2)) == \ + [(1, 2), (1, 3), (2, 3)] + assert list(subsets([1, 2, 3], 2, repetition=True)) == \ + [(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)] + + +def test_variations(): + # permutations + l = list(range(4)) + assert list(variations(l, 0, repetition=False)) == [()] + assert list(variations(l, 1, repetition=False)) == [(0,), (1,), (2,), (3,)] + assert list(variations(l, 2, repetition=False)) == [(0, 1), (0, 2), (0, 3), (1, 0), (1, 2), (1, 3), (2, 0), (2, 1), (2, 3), (3, 0), (3, 1), (3, 2)] + assert list(variations(l, 3, repetition=False)) == [(0, 1, 2), (0, 1, 3), (0, 2, 1), (0, 2, 3), (0, 3, 1), (0, 3, 2), (1, 0, 2), (1, 0, 3), (1, 2, 0), (1, 2, 3), (1, 3, 0), (1, 3, 2), (2, 0, 1), (2, 0, 3), (2, 1, 0), (2, 1, 3), (2, 3, 0), (2, 3, 1), (3, 0, 1), (3, 0, 2), (3, 1, 0), (3, 1, 2), (3, 2, 0), (3, 2, 1)] + assert list(variations(l, 0, repetition=True)) == [()] + assert list(variations(l, 1, repetition=True)) == [(0,), (1,), (2,), (3,)] + assert list(variations(l, 2, repetition=True)) == [(0, 0), (0, 1), (0, 2), + (0, 3), (1, 0), (1, 1), + (1, 2), (1, 3), (2, 0), + (2, 1), (2, 2), (2, 3), + (3, 0), (3, 1), (3, 2), + (3, 3)] + assert len(list(variations(l, 3, repetition=True))) == 64 + assert len(list(variations(l, 4, repetition=True))) == 256 + assert list(variations(l[:2], 3, repetition=False)) == [] + assert list(variations(l[:2], 3, repetition=True)) == [ + (0, 0, 0), (0, 0, 1), (0, 1, 0), (0, 1, 1), + (1, 0, 0), (1, 0, 1), (1, 1, 0), (1, 1, 1) + ] + + +def test_cartes(): + assert list(cartes([1, 2], [3, 4, 5])) == \ + [(1, 3), (1, 4), (1, 5), (2, 3), (2, 4), (2, 5)] + assert list(cartes()) == [()] + assert list(cartes('a')) == [('a',)] + assert list(cartes('a', repeat=2)) == [('a', 'a')] + assert list(cartes(list(range(2)))) == [(0,), (1,)] + + +def test_filter_symbols(): + s = numbered_symbols() + filtered = filter_symbols(s, symbols("x0 x2 x3")) + assert take(filtered, 3) == list(symbols("x1 x4 x5")) + + +def test_numbered_symbols(): + s = numbered_symbols(cls=Dummy) + assert isinstance(next(s), Dummy) + assert next(numbered_symbols('C', start=1, exclude=[symbols('C1')])) == \ + symbols('C2') + + +def test_sift(): + assert sift(list(range(5)), lambda _: _ % 2) == {1: [1, 3], 0: [0, 2, 4]} + assert sift([x, y], lambda _: _.has(x)) == {False: [y], True: [x]} + assert sift([S.One], lambda _: _.has(x)) == {False: [1]} + assert sift([0, 1, 2, 3], lambda x: x % 2, binary=True) == ( + [1, 3], [0, 2]) + assert sift([0, 1, 2, 3], lambda x: x % 3 == 1, binary=True) == ( + [1], [0, 2, 3]) + raises(ValueError, lambda: + sift([0, 1, 2, 3], lambda x: x % 3, binary=True)) + + +def test_take(): + X = numbered_symbols() + + assert take(X, 5) == list(symbols('x0:5')) + assert take(X, 5) == list(symbols('x5:10')) + + assert take([1, 2, 3, 4, 5], 5) == [1, 2, 3, 4, 5] + + +def test_dict_merge(): + assert dict_merge({}, {1: x, y: z}) == {1: x, y: z} + assert dict_merge({1: x, y: z}, {}) == {1: x, y: z} + + assert dict_merge({2: z}, {1: x, y: z}) == {1: x, 2: z, y: z} + assert dict_merge({1: x, y: z}, {2: z}) == {1: x, 2: z, y: z} + + assert dict_merge({1: y, 2: z}, {1: x, y: z}) == {1: x, 2: z, y: z} + assert dict_merge({1: x, y: z}, {1: y, 2: z}) == {1: y, 2: z, y: z} + + +def test_prefixes(): + assert list(prefixes([])) == [] + assert list(prefixes([1])) == [[1]] + assert list(prefixes([1, 2])) == [[1], [1, 2]] + + assert list(prefixes([1, 2, 3, 4, 5])) == \ + [[1], [1, 2], [1, 2, 3], [1, 2, 3, 4], [1, 2, 3, 4, 5]] + + +def test_postfixes(): + assert list(postfixes([])) == [] + assert list(postfixes([1])) == [[1]] + assert list(postfixes([1, 2])) == [[2], [1, 2]] + + assert list(postfixes([1, 2, 3, 4, 5])) == \ + [[5], [4, 5], [3, 4, 5], [2, 3, 4, 5], [1, 2, 3, 4, 5]] + + +def test_topological_sort(): + V = [2, 3, 5, 7, 8, 9, 10, 11] + E = [(7, 11), (7, 8), (5, 11), + (3, 8), (3, 10), (11, 2), + (11, 9), (11, 10), (8, 9)] + + assert topological_sort((V, E)) == [3, 5, 7, 8, 11, 2, 9, 10] + assert topological_sort((V, E), key=lambda v: -v) == \ + [7, 5, 11, 3, 10, 8, 9, 2] + + raises(ValueError, lambda: topological_sort((V, E + [(10, 7)]))) + + +def test_strongly_connected_components(): + assert strongly_connected_components(([], [])) == [] + assert strongly_connected_components(([1, 2, 3], [])) == [[1], [2], [3]] + + V = [1, 2, 3] + E = [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1)] + assert strongly_connected_components((V, E)) == [[1, 2, 3]] + + V = [1, 2, 3, 4] + E = [(1, 2), (2, 3), (3, 2), (3, 4)] + assert strongly_connected_components((V, E)) == [[4], [2, 3], [1]] + + V = [1, 2, 3, 4] + E = [(1, 2), (2, 1), (3, 4), (4, 3)] + assert strongly_connected_components((V, E)) == [[1, 2], [3, 4]] + + +def test_connected_components(): + assert connected_components(([], [])) == [] + assert connected_components(([1, 2, 3], [])) == [[1], [2], [3]] + + V = [1, 2, 3] + E = [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1)] + assert connected_components((V, E)) == [[1, 2, 3]] + + V = [1, 2, 3, 4] + E = [(1, 2), (2, 3), (3, 2), (3, 4)] + assert connected_components((V, E)) == [[1, 2, 3, 4]] + + V = [1, 2, 3, 4] + E = [(1, 2), (3, 4)] + assert connected_components((V, E)) == [[1, 2], [3, 4]] + + +def test_rotate(): + A = [0, 1, 2, 3, 4] + + assert rotate_left(A, 2) == [2, 3, 4, 0, 1] + assert rotate_right(A, 1) == [4, 0, 1, 2, 3] + A = [] + B = rotate_right(A, 1) + assert B == [] + B.append(1) + assert A == [] + B = rotate_left(A, 1) + assert B == [] + B.append(1) + assert A == [] + + +def test_multiset_partitions(): + A = [0, 1, 2, 3, 4] + + assert list(multiset_partitions(A, 5)) == [[[0], [1], [2], [3], [4]]] + assert len(list(multiset_partitions(A, 4))) == 10 + assert len(list(multiset_partitions(A, 3))) == 25 + + assert list(multiset_partitions([1, 1, 1, 2, 2], 2)) == [ + [[1, 1, 1, 2], [2]], [[1, 1, 1], [2, 2]], [[1, 1, 2, 2], [1]], + [[1, 1, 2], [1, 2]], [[1, 1], [1, 2, 2]]] + + assert list(multiset_partitions([1, 1, 2, 2], 2)) == [ + [[1, 1, 2], [2]], [[1, 1], [2, 2]], [[1, 2, 2], [1]], + [[1, 2], [1, 2]]] + + assert list(multiset_partitions([1, 2, 3, 4], 2)) == [ + [[1, 2, 3], [4]], [[1, 2, 4], [3]], [[1, 2], [3, 4]], + [[1, 3, 4], [2]], [[1, 3], [2, 4]], [[1, 4], [2, 3]], + [[1], [2, 3, 4]]] + + assert list(multiset_partitions([1, 2, 2], 2)) == [ + [[1, 2], [2]], [[1], [2, 2]]] + + assert list(multiset_partitions(3)) == [ + [[0, 1, 2]], [[0, 1], [2]], [[0, 2], [1]], [[0], [1, 2]], + [[0], [1], [2]]] + assert list(multiset_partitions(3, 2)) == [ + [[0, 1], [2]], [[0, 2], [1]], [[0], [1, 2]]] + assert list(multiset_partitions([1] * 3, 2)) == [[[1], [1, 1]]] + assert list(multiset_partitions([1] * 3)) == [ + [[1, 1, 1]], [[1], [1, 1]], [[1], [1], [1]]] + a = [3, 2, 1] + assert list(multiset_partitions(a)) == \ + list(multiset_partitions(sorted(a))) + assert list(multiset_partitions(a, 5)) == [] + assert list(multiset_partitions(a, 1)) == [[[1, 2, 3]]] + assert list(multiset_partitions(a + [4], 5)) == [] + assert list(multiset_partitions(a + [4], 1)) == [[[1, 2, 3, 4]]] + assert list(multiset_partitions(2, 5)) == [] + assert list(multiset_partitions(2, 1)) == [[[0, 1]]] + assert list(multiset_partitions('a')) == [[['a']]] + assert list(multiset_partitions('a', 2)) == [] + assert list(multiset_partitions('ab')) == [[['a', 'b']], [['a'], ['b']]] + assert list(multiset_partitions('ab', 1)) == [[['a', 'b']]] + assert list(multiset_partitions('aaa', 1)) == [['aaa']] + assert list(multiset_partitions([1, 1], 1)) == [[[1, 1]]] + ans = [('mpsyy',), ('mpsy', 'y'), ('mps', 'yy'), ('mps', 'y', 'y'), + ('mpyy', 's'), ('mpy', 'sy'), ('mpy', 's', 'y'), ('mp', 'syy'), + ('mp', 'sy', 'y'), ('mp', 's', 'yy'), ('mp', 's', 'y', 'y'), + ('msyy', 'p'), ('msy', 'py'), ('msy', 'p', 'y'), ('ms', 'pyy'), + ('ms', 'py', 'y'), ('ms', 'p', 'yy'), ('ms', 'p', 'y', 'y'), + ('myy', 'ps'), ('myy', 'p', 's'), ('my', 'psy'), ('my', 'ps', 'y'), + ('my', 'py', 's'), ('my', 'p', 'sy'), ('my', 'p', 's', 'y'), + ('m', 'psyy'), ('m', 'psy', 'y'), ('m', 'ps', 'yy'), + ('m', 'ps', 'y', 'y'), ('m', 'pyy', 's'), ('m', 'py', 'sy'), + ('m', 'py', 's', 'y'), ('m', 'p', 'syy'), + ('m', 'p', 'sy', 'y'), ('m', 'p', 's', 'yy'), + ('m', 'p', 's', 'y', 'y')] + assert [tuple("".join(part) for part in p) + for p in multiset_partitions('sympy')] == ans + factorings = [[24], [8, 3], [12, 2], [4, 6], [4, 2, 3], + [6, 2, 2], [2, 2, 2, 3]] + assert [factoring_visitor(p, [2,3]) for + p in multiset_partitions_taocp([3, 1])] == factorings + + +def test_multiset_combinations(): + ans = ['iii', 'iim', 'iip', 'iis', 'imp', 'ims', 'ipp', 'ips', + 'iss', 'mpp', 'mps', 'mss', 'pps', 'pss', 'sss'] + assert [''.join(i) for i in + list(multiset_combinations('mississippi', 3))] == ans + M = multiset('mississippi') + assert [''.join(i) for i in + list(multiset_combinations(M, 3))] == ans + assert [''.join(i) for i in multiset_combinations(M, 30)] == [] + assert list(multiset_combinations([[1], [2, 3]], 2)) == [[[1], [2, 3]]] + assert len(list(multiset_combinations('a', 3))) == 0 + assert len(list(multiset_combinations('a', 0))) == 1 + assert list(multiset_combinations('abc', 1)) == [['a'], ['b'], ['c']] + raises(ValueError, lambda: list(multiset_combinations({0: 3, 1: -1}, 2))) + + +def test_multiset_permutations(): + ans = ['abby', 'abyb', 'aybb', 'baby', 'bayb', 'bbay', 'bbya', 'byab', + 'byba', 'yabb', 'ybab', 'ybba'] + assert [''.join(i) for i in multiset_permutations('baby')] == ans + assert [''.join(i) for i in multiset_permutations(multiset('baby'))] == ans + assert list(multiset_permutations([0, 0, 0], 2)) == [[0, 0]] + assert list(multiset_permutations([0, 2, 1], 2)) == [ + [0, 1], [0, 2], [1, 0], [1, 2], [2, 0], [2, 1]] + assert len(list(multiset_permutations('a', 0))) == 1 + assert len(list(multiset_permutations('a', 3))) == 0 + for nul in ([], {}, ''): + assert list(multiset_permutations(nul)) == [[]] + assert list(multiset_permutations(nul, 0)) == [[]] + # impossible requests give no result + assert list(multiset_permutations(nul, 1)) == [] + assert list(multiset_permutations(nul, -1)) == [] + + def test(): + for i in range(1, 7): + print(i) + for p in multiset_permutations([0, 0, 1, 0, 1], i): + print(p) + assert capture(lambda: test()) == dedent('''\ + 1 + [0] + [1] + 2 + [0, 0] + [0, 1] + [1, 0] + [1, 1] + 3 + [0, 0, 0] + [0, 0, 1] + [0, 1, 0] + [0, 1, 1] + [1, 0, 0] + [1, 0, 1] + [1, 1, 0] + 4 + [0, 0, 0, 1] + [0, 0, 1, 0] + [0, 0, 1, 1] + [0, 1, 0, 0] + [0, 1, 0, 1] + [0, 1, 1, 0] + [1, 0, 0, 0] + [1, 0, 0, 1] + [1, 0, 1, 0] + [1, 1, 0, 0] + 5 + [0, 0, 0, 1, 1] + [0, 0, 1, 0, 1] + [0, 0, 1, 1, 0] + [0, 1, 0, 0, 1] + [0, 1, 0, 1, 0] + [0, 1, 1, 0, 0] + [1, 0, 0, 0, 1] + [1, 0, 0, 1, 0] + [1, 0, 1, 0, 0] + [1, 1, 0, 0, 0] + 6\n''') + raises(ValueError, lambda: list(multiset_permutations({0: 3, 1: -1}))) + + +def test_partitions(): + ans = [[{}], [(0, {})]] + for i in range(2): + assert list(partitions(0, size=i)) == ans[i] + assert list(partitions(1, 0, size=i)) == ans[i] + assert list(partitions(6, 2, 2, size=i)) == ans[i] + assert list(partitions(6, 2, None, size=i)) != ans[i] + assert list(partitions(6, None, 2, size=i)) != ans[i] + assert list(partitions(6, 2, 0, size=i)) == ans[i] + + assert list(partitions(6, k=2)) == [ + {2: 3}, {1: 2, 2: 2}, {1: 4, 2: 1}, {1: 6}] + + assert list(partitions(6, k=3)) == [ + {3: 2}, {1: 1, 2: 1, 3: 1}, {1: 3, 3: 1}, {2: 3}, {1: 2, 2: 2}, + {1: 4, 2: 1}, {1: 6}] + + assert list(partitions(8, k=4, m=3)) == [ + {4: 2}, {1: 1, 3: 1, 4: 1}, {2: 2, 4: 1}, {2: 1, 3: 2}] == [ + i for i in partitions(8, k=4, m=3) if all(k <= 4 for k in i) + and sum(i.values()) <=3] + + assert list(partitions(S(3), m=2)) == [ + {3: 1}, {1: 1, 2: 1}] + + assert list(partitions(4, k=3)) == [ + {1: 1, 3: 1}, {2: 2}, {1: 2, 2: 1}, {1: 4}] == [ + i for i in partitions(4) if all(k <= 3 for k in i)] + + + # Consistency check on output of _partitions and RGS_unrank. + # This provides a sanity test on both routines. Also verifies that + # the total number of partitions is the same in each case. + # (from pkrathmann2) + + for n in range(2, 6): + i = 0 + for m, q in _set_partitions(n): + assert q == RGS_unrank(i, n) + i += 1 + assert i == RGS_enum(n) + + +def test_binary_partitions(): + assert [i[:] for i in binary_partitions(10)] == [[8, 2], [8, 1, 1], + [4, 4, 2], [4, 4, 1, 1], [4, 2, 2, 2], [4, 2, 2, 1, 1], + [4, 2, 1, 1, 1, 1], [4, 1, 1, 1, 1, 1, 1], [2, 2, 2, 2, 2], + [2, 2, 2, 2, 1, 1], [2, 2, 2, 1, 1, 1, 1], [2, 2, 1, 1, 1, 1, 1, 1], + [2, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]] + + assert len([j[:] for j in binary_partitions(16)]) == 36 + + +def test_bell_perm(): + assert [len(set(generate_bell(i))) for i in range(1, 7)] == [ + factorial(i) for i in range(1, 7)] + assert list(generate_bell(3)) == [ + (0, 1, 2), (0, 2, 1), (2, 0, 1), (2, 1, 0), (1, 2, 0), (1, 0, 2)] + # generate_bell and trotterjohnson are advertised to return the same + # permutations; this is not technically necessary so this test could + # be removed + for n in range(1, 5): + p = Permutation(range(n)) + b = generate_bell(n) + for bi in b: + assert bi == tuple(p.array_form) + p = p.next_trotterjohnson() + raises(ValueError, lambda: list(generate_bell(0))) # XXX is this consistent with other permutation algorithms? + + +def test_involutions(): + lengths = [1, 2, 4, 10, 26, 76] + for n, N in enumerate(lengths): + i = list(generate_involutions(n + 1)) + assert len(i) == N + assert len({Permutation(j)**2 for j in i}) == 1 + + +def test_derangements(): + assert len(list(generate_derangements(list(range(6))))) == 265 + assert ''.join(''.join(i) for i in generate_derangements('abcde')) == ( + 'badecbaecdbcaedbcdeabceadbdaecbdeacbdecabeacdbedacbedcacabedcadebcaebd' + 'cdaebcdbeacdeabcdebaceabdcebadcedabcedbadabecdaebcdaecbdcaebdcbeadceab' + 'dcebadeabcdeacbdebacdebcaeabcdeadbceadcbecabdecbadecdabecdbaedabcedacb' + 'edbacedbca') + assert list(generate_derangements([0, 1, 2, 3])) == [ + [1, 0, 3, 2], [1, 2, 3, 0], [1, 3, 0, 2], [2, 0, 3, 1], + [2, 3, 0, 1], [2, 3, 1, 0], [3, 0, 1, 2], [3, 2, 0, 1], [3, 2, 1, 0]] + assert list(generate_derangements([0, 1, 2, 2])) == [ + [2, 2, 0, 1], [2, 2, 1, 0]] + assert list(generate_derangements('ba')) == [list('ab')] + # multiset_derangements + D = multiset_derangements + assert list(D('abb')) == [] + assert [''.join(i) for i in D('ab')] == ['ba'] + assert [''.join(i) for i in D('abc')] == ['bca', 'cab'] + assert [''.join(i) for i in D('aabb')] == ['bbaa'] + assert [''.join(i) for i in D('aabbcccc')] == [ + 'ccccaabb', 'ccccabab', 'ccccabba', 'ccccbaab', 'ccccbaba', + 'ccccbbaa'] + assert [''.join(i) for i in D('aabbccc')] == [ + 'cccabba', 'cccabab', 'cccaabb', 'ccacbba', 'ccacbab', + 'ccacabb', 'cbccbaa', 'cbccaba', 'cbccaab', 'bcccbaa', + 'bcccaba', 'bcccaab'] + assert [''.join(i) for i in D('books')] == ['kbsoo', 'ksboo', + 'sbkoo', 'skboo', 'oksbo', 'oskbo', 'okbso', 'obkso', 'oskob', + 'oksob', 'osbok', 'obsok'] + assert list(generate_derangements([[3], [2], [2], [1]])) == [ + [[2], [1], [3], [2]], [[2], [3], [1], [2]]] + + +def test_necklaces(): + def count(n, k, f): + return len(list(necklaces(n, k, f))) + m = [] + for i in range(1, 8): + m.append(( + i, count(i, 2, 0), count(i, 2, 1), count(i, 3, 1))) + assert Matrix(m) == Matrix([ + [1, 2, 2, 3], + [2, 3, 3, 6], + [3, 4, 4, 10], + [4, 6, 6, 21], + [5, 8, 8, 39], + [6, 14, 13, 92], + [7, 20, 18, 198]]) + + +def test_bracelets(): + bc = list(bracelets(2, 4)) + assert Matrix(bc) == Matrix([ + [0, 0], + [0, 1], + [0, 2], + [0, 3], + [1, 1], + [1, 2], + [1, 3], + [2, 2], + [2, 3], + [3, 3] + ]) + bc = list(bracelets(4, 2)) + assert Matrix(bc) == Matrix([ + [0, 0, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 1], + [0, 1, 0, 1], + [0, 1, 1, 1], + [1, 1, 1, 1] + ]) + + +def test_generate_oriented_forest(): + assert list(generate_oriented_forest(5)) == [[0, 1, 2, 3, 4], + [0, 1, 2, 3, 3], [0, 1, 2, 3, 2], [0, 1, 2, 3, 1], [0, 1, 2, 3, 0], + [0, 1, 2, 2, 2], [0, 1, 2, 2, 1], [0, 1, 2, 2, 0], [0, 1, 2, 1, 2], + [0, 1, 2, 1, 1], [0, 1, 2, 1, 0], [0, 1, 2, 0, 1], [0, 1, 2, 0, 0], + [0, 1, 1, 1, 1], [0, 1, 1, 1, 0], [0, 1, 1, 0, 1], [0, 1, 1, 0, 0], + [0, 1, 0, 1, 0], [0, 1, 0, 0, 0], [0, 0, 0, 0, 0]] + assert len(list(generate_oriented_forest(10))) == 1842 + + +def test_unflatten(): + r = list(range(10)) + assert unflatten(r) == list(zip(r[::2], r[1::2])) + assert unflatten(r, 5) == [tuple(r[:5]), tuple(r[5:])] + raises(ValueError, lambda: unflatten(list(range(10)), 3)) + raises(ValueError, lambda: unflatten(list(range(10)), -2)) + + +def test_common_prefix_suffix(): + assert common_prefix([], [1]) == [] + assert common_prefix(list(range(3))) == [0, 1, 2] + assert common_prefix(list(range(3)), list(range(4))) == [0, 1, 2] + assert common_prefix([1, 2, 3], [1, 2, 5]) == [1, 2] + assert common_prefix([1, 2, 3], [1, 3, 5]) == [1] + + assert common_suffix([], [1]) == [] + assert common_suffix(list(range(3))) == [0, 1, 2] + assert common_suffix(list(range(3)), list(range(3))) == [0, 1, 2] + assert common_suffix(list(range(3)), list(range(4))) == [] + assert common_suffix([1, 2, 3], [9, 2, 3]) == [2, 3] + assert common_suffix([1, 2, 3], [9, 7, 3]) == [3] + + +def test_minlex(): + assert minlex([1, 2, 0]) == (0, 1, 2) + assert minlex((1, 2, 0)) == (0, 1, 2) + assert minlex((1, 0, 2)) == (0, 2, 1) + assert minlex((1, 0, 2), directed=False) == (0, 1, 2) + assert minlex('aba') == 'aab' + assert minlex(('bb', 'aaa', 'c', 'a'), key=len) == ('c', 'a', 'bb', 'aaa') + + +def test_ordered(): + assert list(ordered((x, y), hash, default=False)) in [[x, y], [y, x]] + assert list(ordered((x, y), hash, default=False)) == \ + list(ordered((y, x), hash, default=False)) + assert list(ordered((x, y))) == [x, y] + + seq, keys = [[[1, 2, 1], [0, 3, 1], [1, 1, 3], [2], [1]], + (lambda x: len(x), lambda x: sum(x))] + assert list(ordered(seq, keys, default=False, warn=False)) == \ + [[1], [2], [1, 2, 1], [0, 3, 1], [1, 1, 3]] + raises(ValueError, lambda: + list(ordered(seq, keys, default=False, warn=True))) + + +def test_runs(): + assert runs([]) == [] + assert runs([1]) == [[1]] + assert runs([1, 1]) == [[1], [1]] + assert runs([1, 1, 2]) == [[1], [1, 2]] + assert runs([1, 2, 1]) == [[1, 2], [1]] + assert runs([2, 1, 1]) == [[2], [1], [1]] + from operator import lt + assert runs([2, 1, 1], lt) == [[2, 1], [1]] + + +def test_reshape(): + seq = list(range(1, 9)) + assert reshape(seq, [4]) == \ + [[1, 2, 3, 4], [5, 6, 7, 8]] + assert reshape(seq, (4,)) == \ + [(1, 2, 3, 4), (5, 6, 7, 8)] + assert reshape(seq, (2, 2)) == \ + [(1, 2, 3, 4), (5, 6, 7, 8)] + assert reshape(seq, (2, [2])) == \ + [(1, 2, [3, 4]), (5, 6, [7, 8])] + assert reshape(seq, ((2,), [2])) == \ + [((1, 2), [3, 4]), ((5, 6), [7, 8])] + assert reshape(seq, (1, [2], 1)) == \ + [(1, [2, 3], 4), (5, [6, 7], 8)] + assert reshape(tuple(seq), ([[1], 1, (2,)],)) == \ + (([[1], 2, (3, 4)],), ([[5], 6, (7, 8)],)) + assert reshape(tuple(seq), ([1], 1, (2,))) == \ + (([1], 2, (3, 4)), ([5], 6, (7, 8))) + assert reshape(list(range(12)), [2, [3], {2}, (1, (3,), 1)]) == \ + [[0, 1, [2, 3, 4], {5, 6}, (7, (8, 9, 10), 11)]] + raises(ValueError, lambda: reshape([0, 1], [-1])) + raises(ValueError, lambda: reshape([0, 1], [3])) + + +def test_uniq(): + assert list(uniq(p for p in partitions(4))) == \ + [{4: 1}, {1: 1, 3: 1}, {2: 2}, {1: 2, 2: 1}, {1: 4}] + assert list(uniq(x % 2 for x in range(5))) == [0, 1] + assert list(uniq('a')) == ['a'] + assert list(uniq('ababc')) == list('abc') + assert list(uniq([[1], [2, 1], [1]])) == [[1], [2, 1]] + assert list(uniq(permutations(i for i in [[1], 2, 2]))) == \ + [([1], 2, 2), (2, [1], 2), (2, 2, [1])] + assert list(uniq([2, 3, 2, 4, [2], [1], [2], [3], [1]])) == \ + [2, 3, 4, [2], [1], [3]] + f = [1] + raises(RuntimeError, lambda: [f.remove(i) for i in uniq(f)]) + f = [[1]] + raises(RuntimeError, lambda: [f.remove(i) for i in uniq(f)]) + + +def test_kbins(): + assert len(list(kbins('1123', 2, ordered=1))) == 24 + assert len(list(kbins('1123', 2, ordered=11))) == 36 + assert len(list(kbins('1123', 2, ordered=10))) == 10 + assert len(list(kbins('1123', 2, ordered=0))) == 5 + assert len(list(kbins('1123', 2, ordered=None))) == 3 + + def test1(): + for orderedval in [None, 0, 1, 10, 11]: + print('ordered =', orderedval) + for p in kbins([0, 0, 1], 2, ordered=orderedval): + print(' ', p) + assert capture(lambda : test1()) == dedent('''\ + ordered = None + [[0], [0, 1]] + [[0, 0], [1]] + ordered = 0 + [[0, 0], [1]] + [[0, 1], [0]] + ordered = 1 + [[0], [0, 1]] + [[0], [1, 0]] + [[1], [0, 0]] + ordered = 10 + [[0, 0], [1]] + [[1], [0, 0]] + [[0, 1], [0]] + [[0], [0, 1]] + ordered = 11 + [[0], [0, 1]] + [[0, 0], [1]] + [[0], [1, 0]] + [[0, 1], [0]] + [[1], [0, 0]] + [[1, 0], [0]]\n''') + + def test2(): + for orderedval in [None, 0, 1, 10, 11]: + print('ordered =', orderedval) + for p in kbins(list(range(3)), 2, ordered=orderedval): + print(' ', p) + assert capture(lambda : test2()) == dedent('''\ + ordered = None + [[0], [1, 2]] + [[0, 1], [2]] + ordered = 0 + [[0, 1], [2]] + [[0, 2], [1]] + [[0], [1, 2]] + ordered = 1 + [[0], [1, 2]] + [[0], [2, 1]] + [[1], [0, 2]] + [[1], [2, 0]] + [[2], [0, 1]] + [[2], [1, 0]] + ordered = 10 + [[0, 1], [2]] + [[2], [0, 1]] + [[0, 2], [1]] + [[1], [0, 2]] + [[0], [1, 2]] + [[1, 2], [0]] + ordered = 11 + [[0], [1, 2]] + [[0, 1], [2]] + [[0], [2, 1]] + [[0, 2], [1]] + [[1], [0, 2]] + [[1, 0], [2]] + [[1], [2, 0]] + [[1, 2], [0]] + [[2], [0, 1]] + [[2, 0], [1]] + [[2], [1, 0]] + [[2, 1], [0]]\n''') + + +def test_has_dups(): + assert has_dups(set()) is False + assert has_dups(list(range(3))) is False + assert has_dups([1, 2, 1]) is True + assert has_dups([[1], [1]]) is True + assert has_dups([[1], [2]]) is False + + +def test__partition(): + assert _partition('abcde', [1, 0, 1, 2, 0]) == [ + ['b', 'e'], ['a', 'c'], ['d']] + assert _partition('abcde', [1, 0, 1, 2, 0], 3) == [ + ['b', 'e'], ['a', 'c'], ['d']] + output = (3, [1, 0, 1, 2, 0]) + assert _partition('abcde', *output) == [['b', 'e'], ['a', 'c'], ['d']] + + +def test_ordered_partitions(): + from sympy.functions.combinatorial.numbers import nT + f = ordered_partitions + assert list(f(0, 1)) == [[]] + assert list(f(1, 0)) == [[]] + for i in range(1, 7): + for j in [None] + list(range(1, i)): + assert ( + sum(1 for p in f(i, j, 1)) == + sum(1 for p in f(i, j, 0)) == + nT(i, j)) + + +def test_rotations(): + assert list(rotations('ab')) == [['a', 'b'], ['b', 'a']] + assert list(rotations(range(3))) == [[0, 1, 2], [1, 2, 0], [2, 0, 1]] + assert list(rotations(range(3), dir=-1)) == [[0, 1, 2], [2, 0, 1], [1, 2, 0]] + + +def test_ibin(): + assert ibin(3) == [1, 1] + assert ibin(3, 3) == [0, 1, 1] + assert ibin(3, str=True) == '11' + assert ibin(3, 3, str=True) == '011' + assert list(ibin(2, 'all')) == [(0, 0), (0, 1), (1, 0), (1, 1)] + assert list(ibin(2, '', str=True)) == ['00', '01', '10', '11'] + raises(ValueError, lambda: ibin(-.5)) + raises(ValueError, lambda: ibin(2, 1)) + + +def test_iterable(): + assert iterable(0) is False + assert iterable(1) is False + assert iterable(None) is False + + class Test1(NotIterable): + pass + + assert iterable(Test1()) is False + + class Test2(NotIterable): + _iterable = True + + assert iterable(Test2()) is True + + class Test3: + pass + + assert iterable(Test3()) is False + + class Test4: + _iterable = True + + assert iterable(Test4()) is True + + class Test5: + def __iter__(self): + yield 1 + + assert iterable(Test5()) is True + + class Test6(Test5): + _iterable = False + + assert iterable(Test6()) is False + + +def test_sequence_partitions(): + assert list(sequence_partitions([1], 1)) == [[[1]]] + assert list(sequence_partitions([1, 2], 1)) == [[[1, 2]]] + assert list(sequence_partitions([1, 2], 2)) == [[[1], [2]]] + assert list(sequence_partitions([1, 2, 3], 1)) == [[[1, 2, 3]]] + assert list(sequence_partitions([1, 2, 3], 2)) == \ + [[[1], [2, 3]], [[1, 2], [3]]] + assert list(sequence_partitions([1, 2, 3], 3)) == [[[1], [2], [3]]] + + # Exceptional cases + assert list(sequence_partitions([], 0)) == [] + assert list(sequence_partitions([], 1)) == [] + assert list(sequence_partitions([1, 2], 0)) == [] + assert list(sequence_partitions([1, 2], 3)) == [] + + +def test_sequence_partitions_empty(): + assert list(sequence_partitions_empty([], 1)) == [[[]]] + assert list(sequence_partitions_empty([], 2)) == [[[], []]] + assert list(sequence_partitions_empty([], 3)) == [[[], [], []]] + assert list(sequence_partitions_empty([1], 1)) == [[[1]]] + assert list(sequence_partitions_empty([1], 2)) == [[[], [1]], [[1], []]] + assert list(sequence_partitions_empty([1], 3)) == \ + [[[], [], [1]], [[], [1], []], [[1], [], []]] + assert list(sequence_partitions_empty([1, 2], 1)) == [[[1, 2]]] + assert list(sequence_partitions_empty([1, 2], 2)) == \ + [[[], [1, 2]], [[1], [2]], [[1, 2], []]] + assert list(sequence_partitions_empty([1, 2], 3)) == [ + [[], [], [1, 2]], [[], [1], [2]], [[], [1, 2], []], + [[1], [], [2]], [[1], [2], []], [[1, 2], [], []] + ] + assert list(sequence_partitions_empty([1, 2, 3], 1)) == [[[1, 2, 3]]] + assert list(sequence_partitions_empty([1, 2, 3], 2)) == \ + [[[], [1, 2, 3]], [[1], [2, 3]], [[1, 2], [3]], [[1, 2, 3], []]] + assert list(sequence_partitions_empty([1, 2, 3], 3)) == [ + [[], [], [1, 2, 3]], [[], [1], [2, 3]], + [[], [1, 2], [3]], [[], [1, 2, 3], []], + [[1], [], [2, 3]], [[1], [2], [3]], + [[1], [2, 3], []], [[1, 2], [], [3]], + [[1, 2], [3], []], [[1, 2, 3], [], []] + ] + + # Exceptional cases + assert list(sequence_partitions([], 0)) == [] + assert list(sequence_partitions([1], 0)) == [] + assert list(sequence_partitions([1, 2], 0)) == [] + + +def test_signed_permutations(): + ans = [(0, 1, 1), (0, -1, 1), (0, 1, -1), (0, -1, -1), + (1, 0, 1), (-1, 0, 1), (1, 0, -1), (-1, 0, -1), + (1, 1, 0), (-1, 1, 0), (1, -1, 0), (-1, -1, 0)] + assert list(signed_permutations((0, 1, 1))) == ans + assert list(signed_permutations((1, 0, 1))) == ans + assert list(signed_permutations((1, 1, 0))) == ans diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_lambdify.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_lambdify.py new file mode 100644 index 0000000000000000000000000000000000000000..b094c67d39f09c24bcb9abc1e755cb5328e143e7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_lambdify.py @@ -0,0 +1,2263 @@ +from itertools import product +import math +import inspect +import linecache +import gc + +import mpmath +import cmath + +from sympy.testing.pytest import raises, warns_deprecated_sympy +from sympy.concrete.summations import Sum +from sympy.core.function import (Function, Lambda, diff) +from sympy.core.numbers import (E, Float, I, Rational, all_close, oo, pi) +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.core.symbol import (Dummy, symbols) +from sympy.functions.combinatorial.factorials import (RisingFactorial, factorial) +from sympy.functions.combinatorial.numbers import bernoulli, harmonic +from sympy.functions.elementary.complexes import Abs, sign +from sympy.functions.elementary.exponential import exp, log +from sympy.functions.elementary.hyperbolic import asinh,acosh,atanh +from sympy.functions.elementary.integers import floor +from sympy.functions.elementary.miscellaneous import (Max, Min, sqrt) +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (asin, acos, atan, cos, cot, sin, + sinc, tan) +from sympy.functions import sinh,cosh,tanh +from sympy.functions.special.bessel import (besseli, besselj, besselk, bessely, jn, yn) +from sympy.functions.special.beta_functions import (beta, betainc, betainc_regularized) +from sympy.functions.special.delta_functions import (Heaviside) +from sympy.functions.special.error_functions import (Ei, erf, erfc, fresnelc, fresnels, Si, Ci) +from sympy.functions.special.gamma_functions import (digamma, gamma, loggamma, polygamma) +from sympy.functions.special.zeta_functions import zeta +from sympy.integrals.integrals import Integral +from sympy.logic.boolalg import (And, false, ITE, Not, Or, true) +from sympy.matrices.expressions.dotproduct import DotProduct +from sympy.simplify.cse_main import cse +from sympy.tensor.array import derive_by_array, Array +from sympy.tensor.array.expressions import ArraySymbol +from sympy.tensor.indexed import IndexedBase, Idx +from sympy.utilities.lambdify import lambdify +from sympy.utilities.iterables import numbered_symbols +from sympy.vector import CoordSys3D +from sympy.core.expr import UnevaluatedExpr +from sympy.codegen.cfunctions import expm1, log1p, exp2, log2, log10, hypot, isnan, isinf +from sympy.codegen.numpy_nodes import logaddexp, logaddexp2, amin, amax, minimum, maximum +from sympy.codegen.scipy_nodes import cosm1, powm1 +from sympy.functions.elementary.complexes import re, im, arg +from sympy.functions.special.polynomials import \ + chebyshevt, chebyshevu, legendre, hermite, laguerre, gegenbauer, \ + assoc_legendre, assoc_laguerre, jacobi +from sympy.matrices import Matrix, MatrixSymbol, SparseMatrix +from sympy.printing.codeprinter import PrintMethodNotImplementedError +from sympy.printing.lambdarepr import LambdaPrinter +from sympy.printing.numpy import NumPyPrinter +from sympy.utilities.lambdify import implemented_function, lambdastr +from sympy.testing.pytest import skip +from sympy.utilities.decorator import conserve_mpmath_dps +from sympy.utilities.exceptions import ignore_warnings +from sympy.external import import_module +from sympy.functions.special.gamma_functions import uppergamma, lowergamma + + +import sympy + + +MutableDenseMatrix = Matrix + +numpy = import_module('numpy') +scipy = import_module('scipy', import_kwargs={'fromlist': ['sparse']}) +numexpr = import_module('numexpr') +tensorflow = import_module('tensorflow') +cupy = import_module('cupy') +jax = import_module('jax') +numba = import_module('numba') + +if tensorflow: + # Hide Tensorflow warnings + import os + os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' + +w, x, y, z = symbols('w,x,y,z') + +#================== Test different arguments ======================= + + +def test_no_args(): + f = lambdify([], 1) + raises(TypeError, lambda: f(-1)) + assert f() == 1 + + +def test_single_arg(): + f = lambdify(x, 2*x) + assert f(1) == 2 + + +def test_list_args(): + f = lambdify([x, y], x + y) + assert f(1, 2) == 3 + + +def test_nested_args(): + f1 = lambdify([[w, x]], [w, x]) + assert f1([91, 2]) == [91, 2] + raises(TypeError, lambda: f1(1, 2)) + + f2 = lambdify([(w, x), (y, z)], [w, x, y, z]) + assert f2((18, 12), (73, 4)) == [18, 12, 73, 4] + raises(TypeError, lambda: f2(3, 4)) + + f3 = lambdify([w, [[[x]], y], z], [w, x, y, z]) + assert f3(10, [[[52]], 31], 44) == [10, 52, 31, 44] + + +def test_str_args(): + f = lambdify('x,y,z', 'z,y,x') + assert f(3, 2, 1) == (1, 2, 3) + assert f(1.0, 2.0, 3.0) == (3.0, 2.0, 1.0) + # make sure correct number of args required + raises(TypeError, lambda: f(0)) + + +def test_own_namespace_1(): + myfunc = lambda x: 1 + f = lambdify(x, sin(x), {"sin": myfunc}) + assert f(0.1) == 1 + assert f(100) == 1 + + +def test_own_namespace_2(): + def myfunc(x): + return 1 + f = lambdify(x, sin(x), {'sin': myfunc}) + assert f(0.1) == 1 + assert f(100) == 1 + + +def test_own_module(): + f = lambdify(x, sin(x), math) + assert f(0) == 0.0 + + p, q, r = symbols("p q r", real=True) + ae = abs(exp(p+UnevaluatedExpr(q+r))) + f = lambdify([p, q, r], [ae, ae], modules=math) + results = f(1.0, 1e18, -1e18) + refvals = [math.exp(1.0)]*2 + for res, ref in zip(results, refvals): + assert abs((res-ref)/ref) < 1e-15 + + +def test_bad_args(): + # no vargs given + raises(TypeError, lambda: lambdify(1)) + # same with vector exprs + raises(TypeError, lambda: lambdify([1, 2])) + + +def test_atoms(): + # Non-Symbol atoms should not be pulled out from the expression namespace + f = lambdify(x, pi + x, {"pi": 3.14}) + assert f(0) == 3.14 + f = lambdify(x, I + x, {"I": 1j}) + assert f(1) == 1 + 1j + +#================== Test different modules ========================= + +# high precision output of sin(0.2*pi) is used to detect if precision is lost unwanted + + +@conserve_mpmath_dps +def test_sympy_lambda(): + mpmath.mp.dps = 50 + sin02 = mpmath.mpf("0.19866933079506121545941262711838975037020672954020") + f = lambdify(x, sin(x), "sympy") + assert f(x) == sin(x) + prec = 1e-15 + assert -prec < f(Rational(1, 5)).evalf() - Float(str(sin02)) < prec + # arctan is in numpy module and should not be available + # The arctan below gives NameError. What is this supposed to test? + # raises(NameError, lambda: lambdify(x, arctan(x), "sympy")) + + +@conserve_mpmath_dps +def test_math_lambda(): + mpmath.mp.dps = 50 + sin02 = mpmath.mpf("0.19866933079506121545941262711838975037020672954020") + f = lambdify(x, sin(x), "math") + prec = 1e-15 + assert -prec < f(0.2) - sin02 < prec + raises(TypeError, lambda: f(x)) + # if this succeeds, it can't be a Python math function + + +@conserve_mpmath_dps +def test_mpmath_lambda(): + mpmath.mp.dps = 50 + sin02 = mpmath.mpf("0.19866933079506121545941262711838975037020672954020") + f = lambdify(x, sin(x), "mpmath") + prec = 1e-49 # mpmath precision is around 50 decimal places + assert -prec < f(mpmath.mpf("0.2")) - sin02 < prec + raises(TypeError, lambda: f(x)) + # if this succeeds, it can't be a mpmath function + + ref2 = (mpmath.mpf("1e-30") + - mpmath.mpf("1e-45")/2 + + 5*mpmath.mpf("1e-60")/6 + - 3*mpmath.mpf("1e-75")/4 + + 33*mpmath.mpf("1e-90")/40 + ) + f2a = lambdify((x, y), x**y - 1, "mpmath") + f2b = lambdify((x, y), powm1(x, y), "mpmath") + f2c = lambdify((x,), expm1(x*log1p(x)), "mpmath") + ans2a = f2a(mpmath.mpf("1")+mpmath.mpf("1e-15"), mpmath.mpf("1e-15")) + ans2b = f2b(mpmath.mpf("1")+mpmath.mpf("1e-15"), mpmath.mpf("1e-15")) + ans2c = f2c(mpmath.mpf("1e-15")) + assert abs(ans2a - ref2) < 1e-51 + assert abs(ans2b - ref2) < 1e-67 + assert abs(ans2c - ref2) < 1e-80 + + +@conserve_mpmath_dps +def test_number_precision(): + mpmath.mp.dps = 50 + sin02 = mpmath.mpf("0.19866933079506121545941262711838975037020672954020") + f = lambdify(x, sin02, "mpmath") + prec = 1e-49 # mpmath precision is around 50 decimal places + assert -prec < f(0) - sin02 < prec + +@conserve_mpmath_dps +def test_mpmath_precision(): + mpmath.mp.dps = 100 + assert str(lambdify((), pi.evalf(100), 'mpmath')()) == str(pi.evalf(100)) + +#================== Test Translations ============================== +# We can only check if all translated functions are valid. It has to be checked +# by hand if they are complete. + + +def test_math_transl(): + from sympy.utilities.lambdify import MATH_TRANSLATIONS + for sym, mat in MATH_TRANSLATIONS.items(): + assert sym in sympy.__dict__ + assert mat in math.__dict__ + + +def test_mpmath_transl(): + from sympy.utilities.lambdify import MPMATH_TRANSLATIONS + for sym, mat in MPMATH_TRANSLATIONS.items(): + assert sym in sympy.__dict__ or sym == 'Matrix' + assert mat in mpmath.__dict__ + + +def test_numpy_transl(): + if not numpy: + skip("numpy not installed.") + + from sympy.utilities.lambdify import NUMPY_TRANSLATIONS + for sym, nump in NUMPY_TRANSLATIONS.items(): + assert sym in sympy.__dict__ + assert nump in numpy.__dict__ + + +def test_scipy_transl(): + if not scipy: + skip("scipy not installed.") + + from sympy.utilities.lambdify import SCIPY_TRANSLATIONS + for sym, scip in SCIPY_TRANSLATIONS.items(): + assert sym in sympy.__dict__ + assert scip in scipy.__dict__ or scip in scipy.special.__dict__ + + +def test_numpy_translation_abs(): + if not numpy: + skip("numpy not installed.") + + f = lambdify(x, Abs(x), "numpy") + assert f(-1) == 1 + assert f(1) == 1 + + +def test_numexpr_printer(): + if not numexpr: + skip("numexpr not installed.") + + # if translation/printing is done incorrectly then evaluating + # a lambdified numexpr expression will throw an exception + from sympy.printing.lambdarepr import NumExprPrinter + + blacklist = ('where', 'complex', 'contains') + arg_tuple = (x, y, z) # some functions take more than one argument + for sym in NumExprPrinter._numexpr_functions.keys(): + if sym in blacklist: + continue + ssym = S(sym) + if hasattr(ssym, '_nargs'): + nargs = ssym._nargs[0] + else: + nargs = 1 + args = arg_tuple[:nargs] + f = lambdify(args, ssym(*args), modules='numexpr') + assert f(*(1, )*nargs) is not None + + +def test_cmath_sqrt(): + f = lambdify(x, sqrt(x), "cmath") + assert f(0) == 0 + assert f(1) == 1 + assert f(4) == 2 + assert abs(f(2) - 1.414) < 0.001 + assert f(-1) == 1j + assert f(-4) == 2j + + +def test_cmath_log(): + f = lambdify(x, log(x), "cmath") + assert abs(f(1) - 0) < 1e-15 + assert abs(f(cmath.e) - 1) < 1e-15 + assert abs(f(-1) - cmath.log(-1)) < 1e-15 + + +def test_cmath_sinh(): + f = lambdify(x, sinh(x), "cmath") + assert abs(f(0) - cmath.sinh(0)) < 1e-15 + assert abs(f(pi) - cmath.sinh(pi)) < 1e-15 + assert abs(f(-pi) - cmath.sinh(-pi)) < 1e-15 + assert abs(f(1j) - cmath.sinh(1j)) < 1e-15 + + +def test_cmath_cosh(): + f = lambdify(x, cosh(x), "cmath") + assert abs(f(0) - cmath.cosh(0)) < 1e-15 + assert abs(f(pi) - cmath.cosh(pi)) < 1e-15 + assert abs(f(-pi) - cmath.cosh(-pi)) < 1e-15 + assert abs(f(1j) - cmath.cosh(1j)) < 1e-15 + + +def test_cmath_tanh(): + f = lambdify(x, tanh(x), "cmath") + assert abs(f(0) - cmath.tanh(0)) < 1e-15 + assert abs(f(pi) - cmath.tanh(pi)) < 1e-15 + assert abs(f(-pi) - cmath.tanh(-pi)) < 1e-15 + assert abs(f(1j) - cmath.tanh(1j)) < 1e-15 + + +def test_cmath_sin(): + f = lambdify(x, sin(x), "cmath") + assert abs(f(0) - cmath.sin(0)) < 1e-15 + assert abs(f(pi) - cmath.sin(pi)) < 1e-15 + assert abs(f(-pi) - cmath.sin(-pi)) < 1e-15 + assert abs(f(1j) - cmath.sin(1j)) < 1e-15 + + +def test_cmath_cos(): + f = lambdify(x, cos(x), "cmath") + assert abs(f(0) - cmath.cos(0)) < 1e-15 + assert abs(f(pi) - cmath.cos(pi)) < 1e-15 + assert abs(f(-pi) - cmath.cos(-pi)) < 1e-15 + assert abs(f(1j) - cmath.cos(1j)) < 1e-15 + + +def test_cmath_tan(): + f = lambdify(x, tan(x), "cmath") + assert abs(f(0) - cmath.tan(0)) < 1e-15 + assert abs(f(1j) - cmath.tan(1j)) < 1e-15 + + +def test_cmath_asin(): + f = lambdify(x, asin(x), "cmath") + assert abs(f(0) - cmath.asin(0)) < 1e-15 + assert abs(f(1) - cmath.asin(1)) < 1e-15 + assert abs(f(-1) - cmath.asin(-1)) < 1e-15 + assert abs(f(2) - cmath.asin(2)) < 1e-15 + assert abs(f(1j) - cmath.asin(1j)) < 1e-15 + + +def test_cmath_acos(): + f = lambdify(x, acos(x), "cmath") + assert abs(f(1) - cmath.acos(1)) < 1e-15 + assert abs(f(-1) - cmath.acos(-1)) < 1e-15 + assert abs(f(2) - cmath.acos(2)) < 1e-15 + assert abs(f(1j) - cmath.acos(1j)) < 1e-15 + + +def test_cmath_atan(): + f = lambdify(x, atan(x), "cmath") + assert abs(f(0) - cmath.atan(0)) < 1e-15 + assert abs(f(1) - cmath.atan(1)) < 1e-15 + assert abs(f(-1) - cmath.atan(-1)) < 1e-15 + assert abs(f(2) - cmath.atan(2)) < 1e-15 + assert abs(f(2j) - cmath.atan(2j)) < 1e-15 + + +def test_cmath_asinh(): + f = lambdify(x, asinh(x), "cmath") + assert abs(f(0) - cmath.asinh(0)) < 1e-15 + assert abs(f(1) - cmath.asinh(1)) < 1e-15 + assert abs(f(-1) - cmath.asinh(-1)) < 1e-15 + assert abs(f(2) - cmath.asinh(2)) < 1e-15 + assert abs(f(2j) - cmath.asinh(2j)) < 1e-15 + + +def test_cmath_acosh(): + f = lambdify(x, acosh(x), "cmath") + assert abs(f(1) - cmath.acosh(1)) < 1e-15 + assert abs(f(2) - cmath.acosh(2)) < 1e-15 + assert abs(f(-1) - cmath.acosh(-1)) < 1e-15 + assert abs(f(2j) - cmath.acosh(2j)) < 1e-15 + + +def test_cmath_atanh(): + f = lambdify(x, atanh(x), "cmath") + assert abs(f(0) - cmath.atanh(0)) < 1e-15 + assert abs(f(0.5) - cmath.atanh(0.5)) < 1e-15 + assert abs(f(-0.5) - cmath.atanh(-0.5)) < 1e-15 + assert abs(f(2) - cmath.atanh(2)) < 1e-15 + assert abs(f(-2) - cmath.atanh(-2)) < 1e-15 + assert abs(f(2j) - cmath.atanh(2j)) < 1e-15 + + +def test_cmath_complex_identities(): + # Define symbol + z = symbols('z') + + # Trigonometric identity using re(z) and im(z) + expr = cos(z) - cos(re(z)) * cosh(im(z)) + I * sin(re(z)) * sinh(im(z)) + func = lambdify([z], expr, modules=["cmath", "math"]) + hpi = math.pi / 2 + assert abs(func(hpi + 1j * hpi)) < 4e-16 + + # Euler's Formula: e^(i*z) = cos(z) + i*sin(z) + func = lambdify([z], exp(I * z) - (cos(z) + I * sin(z)), modules=["cmath", "math"]) + assert abs(func(hpi)) < 4e-16 + + # Exponential Identity: e^z = e^(Re(z)) * (cos(Im(z)) + i*sin(Im(z))) + func_exp = lambdify([z], exp(z) - exp(re(z)) * (cos(im(z)) + I * sin(im(z))), + modules=["cmath", "math"]) + assert abs(func_exp(hpi + 1j * hpi)) < 4e-16 + + # Complex Cosine Identity: cos(z) = cos(Re(z)) * cosh(Im(z)) - i*sin(Re(z)) * sinh(Im(z)) + func_cos = lambdify([z], cos(z) - (cos(re(z)) * cosh(im(z)) - I * sin(re(z)) * sinh(im(z))), + modules=["cmath", "math"]) + assert abs(func_cos(hpi + 1j * hpi)) < 4e-16 + + # Complex Sine Identity: sin(z) = sin(Re(z)) * cosh(Im(z)) + i*cos(Re(z)) * sinh(Im(z)) + func_sin = lambdify([z], sin(z) - (sin(re(z)) * cosh(im(z)) + I * cos(re(z)) * sinh(im(z))), + modules=["cmath", "math"]) + assert abs(func_sin(hpi + 1j * hpi)) < 4e-16 + + # Complex Hyperbolic Cosine Identity: cosh(z) = cosh(Re(z)) * cos(Im(z)) + i*sinh(Re(z)) * sin(Im(z)) + func_cosh_1 = lambdify([z], cosh(z) - (cosh(re(z)) * cos(im(z)) + I * sinh(re(z)) * sin(im(z))), + modules=["cmath", "math"]) + assert abs(func_cosh_1(hpi + 1j * hpi)) < 4e-16 + + # Complex Hyperbolic Sine Identity: sinh(z) = sinh(Re(z)) * cos(Im(z)) + i*cosh(Re(z)) * sin(Im(z)) + func_sinh = lambdify([z], sinh(z) - (sinh(re(z)) * cos(im(z)) + I * cosh(re(z)) * sin(im(z))), + modules=["cmath", "math"]) + assert abs(func_sinh(hpi + 1j * hpi)) < 4e-16 + + # cosh(z) = (e^z + e^(-z)) / 2 + func_cosh_2 = lambdify([z], cosh(z) - (exp(z) + exp(-z)) / 2, modules=["cmath", "math"]) + assert abs(func_cosh_2(hpi)) < 4e-16 + + # Additional expressions testing log and exp with real and imaginary parts + expr1 = log(re(z)) + log(im(z)) - log(re(z) * im(z)) + expr2 = exp(re(z)) * exp(im(z) * I) - exp(z) + expr3 = log(exp(re(z))) - re(z) + expr4 = exp(log(re(z))) - re(z) + expr5 = log(exp(re(z) + im(z))) - (re(z) + im(z)) + expr6 = exp(log(re(z) + im(z))) - (re(z) + im(z)) + func1 = lambdify([z], expr1, modules=["cmath", "math"]) + func2 = lambdify([z], expr2, modules=["cmath", "math"]) + func3 = lambdify([z], expr3, modules=["cmath", "math"]) + func4 = lambdify([z], expr4, modules=["cmath", "math"]) + func5 = lambdify([z], expr5, modules=["cmath", "math"]) + func6 = lambdify([z], expr6, modules=["cmath", "math"]) + test_value = 3 + 4j + assert abs(func1(test_value)) < 4e-16 + assert abs(func2(test_value)) < 4e-16 + assert abs(func3(test_value)) < 4e-16 + assert abs(func4(test_value)) < 4e-16 + assert abs(func5(test_value)) < 4e-16 + assert abs(func6(test_value)) < 4e-16 + + +def test_issue_9334(): + if not numexpr: + skip("numexpr not installed.") + if not numpy: + skip("numpy not installed.") + expr = S('b*a - sqrt(a**2)') + a, b = sorted(expr.free_symbols, key=lambda s: s.name) + func_numexpr = lambdify((a,b), expr, modules=[numexpr], dummify=False) + foo, bar = numpy.random.random((2, 4)) + func_numexpr(foo, bar) + + +def test_issue_12984(): + if not numexpr: + skip("numexpr not installed.") + func_numexpr = lambdify((x,y,z), Piecewise((y, x >= 0), (z, x > -1)), numexpr) + with ignore_warnings(RuntimeWarning): + assert func_numexpr(1, 24, 42) == 24 + assert str(func_numexpr(-1, 24, 42)) == 'nan' + + +def test_empty_modules(): + x, y = symbols('x y') + expr = -(x % y) + + no_modules = lambdify([x, y], expr) + empty_modules = lambdify([x, y], expr, modules=[]) + assert no_modules(3, 7) == empty_modules(3, 7) + assert no_modules(3, 7) == -3 + + +def test_exponentiation(): + f = lambdify(x, x**2) + assert f(-1) == 1 + assert f(0) == 0 + assert f(1) == 1 + assert f(-2) == 4 + assert f(2) == 4 + assert f(2.5) == 6.25 + + +def test_sqrt(): + f = lambdify(x, sqrt(x)) + assert f(0) == 0.0 + assert f(1) == 1.0 + assert f(4) == 2.0 + assert abs(f(2) - 1.414) < 0.001 + assert f(6.25) == 2.5 + + +def test_trig(): + f = lambdify([x], [cos(x), sin(x)], 'math') + d = f(pi) + prec = 1e-11 + assert -prec < d[0] + 1 < prec + assert -prec < d[1] < prec + d = f(3.14159) + prec = 1e-5 + assert -prec < d[0] + 1 < prec + assert -prec < d[1] < prec + + +def test_integral(): + if numpy and not scipy: + skip("scipy not installed.") + f = Lambda(x, exp(-x**2)) + l = lambdify(y, Integral(f(x), (x, y, oo))) + d = l(-oo) + assert 1.77245385 < d < 1.772453851 + + +def test_double_integral(): + if numpy and not scipy: + skip("scipy not installed.") + # example from http://mpmath.org/doc/current/calculus/integration.html + i = Integral(1/(1 - x**2*y**2), (x, 0, 1), (y, 0, z)) + l = lambdify([z], i) + d = l(1) + assert 1.23370055 < d < 1.233700551 + +def test_spherical_bessel(): + if numpy and not scipy: + skip("scipy not installed.") + test_point = 4.2 #randomly selected + x = symbols("x") + jtest = jn(2, x) + assert abs(lambdify(x,jtest)(test_point) - + jtest.subs(x,test_point).evalf()) < 1e-8 + ytest = yn(2, x) + assert abs(lambdify(x,ytest)(test_point) - + ytest.subs(x,test_point).evalf()) < 1e-8 + + +#================== Test vectors =================================== + + +def test_vector_simple(): + f = lambdify((x, y, z), (z, y, x)) + assert f(3, 2, 1) == (1, 2, 3) + assert f(1.0, 2.0, 3.0) == (3.0, 2.0, 1.0) + # make sure correct number of args required + raises(TypeError, lambda: f(0)) + + +def test_vector_discontinuous(): + f = lambdify(x, (-1/x, 1/x)) + raises(ZeroDivisionError, lambda: f(0)) + assert f(1) == (-1.0, 1.0) + assert f(2) == (-0.5, 0.5) + assert f(-2) == (0.5, -0.5) + + +def test_trig_symbolic(): + f = lambdify([x], [cos(x), sin(x)], 'math') + d = f(pi) + assert abs(d[0] + 1) < 0.0001 + assert abs(d[1] - 0) < 0.0001 + + +def test_trig_float(): + f = lambdify([x], [cos(x), sin(x)]) + d = f(3.14159) + assert abs(d[0] + 1) < 0.0001 + assert abs(d[1] - 0) < 0.0001 + + +def test_docs(): + f = lambdify(x, x**2) + assert f(2) == 4 + f = lambdify([x, y, z], [z, y, x]) + assert f(1, 2, 3) == [3, 2, 1] + f = lambdify(x, sqrt(x)) + assert f(4) == 2.0 + f = lambdify((x, y), sin(x*y)**2) + assert f(0, 5) == 0 + + +def test_math(): + f = lambdify((x, y), sin(x), modules="math") + assert f(0, 5) == 0 + + +def test_sin(): + f = lambdify(x, sin(x)**2) + assert isinstance(f(2), float) + f = lambdify(x, sin(x)**2, modules="math") + assert isinstance(f(2), float) + + +def test_matrix(): + A = Matrix([[x, x*y], [sin(z) + 4, x**z]]) + sol = Matrix([[1, 2], [sin(3) + 4, 1]]) + f = lambdify((x, y, z), A, modules="sympy") + assert f(1, 2, 3) == sol + f = lambdify((x, y, z), (A, [A]), modules="sympy") + assert f(1, 2, 3) == (sol, [sol]) + J = Matrix((x, x + y)).jacobian((x, y)) + v = Matrix((x, y)) + sol = Matrix([[1, 0], [1, 1]]) + assert lambdify(v, J, modules='sympy')(1, 2) == sol + assert lambdify(v.T, J, modules='sympy')(1, 2) == sol + + +def test_numpy_matrix(): + if not numpy: + skip("numpy not installed.") + A = Matrix([[x, x*y], [sin(z) + 4, x**z]]) + sol_arr = numpy.array([[1, 2], [numpy.sin(3) + 4, 1]]) + #Lambdify array first, to ensure return to array as default + f = lambdify((x, y, z), A, ['numpy']) + numpy.testing.assert_allclose(f(1, 2, 3), sol_arr) + #Check that the types are arrays and matrices + assert isinstance(f(1, 2, 3), numpy.ndarray) + + # gh-15071 + class dot(Function): + pass + x_dot_mtx = dot(x, Matrix([[2], [1], [0]])) + f_dot1 = lambdify(x, x_dot_mtx) + inp = numpy.zeros((17, 3)) + assert numpy.all(f_dot1(inp) == 0) + + strict_kw = {"allow_unknown_functions": False, "inline": True, "fully_qualified_modules": False} + p2 = NumPyPrinter(dict(user_functions={'dot': 'dot'}, **strict_kw)) + f_dot2 = lambdify(x, x_dot_mtx, printer=p2) + assert numpy.all(f_dot2(inp) == 0) + + p3 = NumPyPrinter(strict_kw) + # The line below should probably fail upon construction (before calling with "(inp)"): + raises(Exception, lambda: lambdify(x, x_dot_mtx, printer=p3)(inp)) + + +def test_numpy_transpose(): + if not numpy: + skip("numpy not installed.") + A = Matrix([[1, x], [0, 1]]) + f = lambdify((x), A.T, modules="numpy") + numpy.testing.assert_array_equal(f(2), numpy.array([[1, 0], [2, 1]])) + + +def test_numpy_dotproduct(): + if not numpy: + skip("numpy not installed") + A = Matrix([x, y, z]) + f1 = lambdify([x, y, z], DotProduct(A, A), modules='numpy') + f2 = lambdify([x, y, z], DotProduct(A, A.T), modules='numpy') + f3 = lambdify([x, y, z], DotProduct(A.T, A), modules='numpy') + f4 = lambdify([x, y, z], DotProduct(A, A.T), modules='numpy') + + assert f1(1, 2, 3) == \ + f2(1, 2, 3) == \ + f3(1, 2, 3) == \ + f4(1, 2, 3) == \ + numpy.array([14]) + + +def test_numpy_inverse(): + if not numpy: + skip("numpy not installed.") + A = Matrix([[1, x], [0, 1]]) + f = lambdify((x), A**-1, modules="numpy") + numpy.testing.assert_array_equal(f(2), numpy.array([[1, -2], [0, 1]])) + + +def test_numpy_old_matrix(): + if not numpy: + skip("numpy not installed.") + A = Matrix([[x, x*y], [sin(z) + 4, x**z]]) + sol_arr = numpy.array([[1, 2], [numpy.sin(3) + 4, 1]]) + f = lambdify((x, y, z), A, [{'ImmutableDenseMatrix': numpy.matrix}, 'numpy']) + with ignore_warnings(PendingDeprecationWarning): + numpy.testing.assert_allclose(f(1, 2, 3), sol_arr) + assert isinstance(f(1, 2, 3), numpy.matrix) + + +def test_scipy_sparse_matrix(): + if not scipy: + skip("scipy not installed.") + A = SparseMatrix([[x, 0], [0, y]]) + f = lambdify((x, y), A, modules="scipy") + B = f(1, 2) + assert isinstance(B, scipy.sparse.coo_matrix) + + +def test_python_div_zero_issue_11306(): + if not numpy: + skip("numpy not installed.") + p = Piecewise((1 / x, y < -1), (x, y < 1), (1 / x, True)) + f = lambdify([x, y], p, modules='numpy') + with numpy.errstate(divide='ignore'): + assert float(f(numpy.array(0), numpy.array(0.5))) == 0 + assert float(f(numpy.array(0), numpy.array(1))) == float('inf') + + +def test_issue9474(): + mods = [None, 'math'] + if numpy: + mods.append('numpy') + if mpmath: + mods.append('mpmath') + for mod in mods: + f = lambdify(x, S.One/x, modules=mod) + assert f(2) == 0.5 + f = lambdify(x, floor(S.One/x), modules=mod) + assert f(2) == 0 + + for absfunc, modules in product([Abs, abs], mods): + f = lambdify(x, absfunc(x), modules=modules) + assert f(-1) == 1 + assert f(1) == 1 + assert f(3+4j) == 5 + + +def test_issue_9871(): + if not numexpr: + skip("numexpr not installed.") + if not numpy: + skip("numpy not installed.") + + r = sqrt(x**2 + y**2) + expr = diff(1/r, x) + + xn = yn = numpy.linspace(1, 10, 16) + # expr(xn, xn) = -xn/(sqrt(2)*xn)^3 + fv_exact = -numpy.sqrt(2.)**-3 * xn**-2 + + fv_numpy = lambdify((x, y), expr, modules='numpy')(xn, yn) + fv_numexpr = lambdify((x, y), expr, modules='numexpr')(xn, yn) + numpy.testing.assert_allclose(fv_numpy, fv_exact, rtol=1e-10) + numpy.testing.assert_allclose(fv_numexpr, fv_exact, rtol=1e-10) + + +def test_numpy_piecewise(): + if not numpy: + skip("numpy not installed.") + pieces = Piecewise((x, x < 3), (x**2, x > 5), (0, True)) + f = lambdify(x, pieces, modules="numpy") + numpy.testing.assert_array_equal(f(numpy.arange(10)), + numpy.array([0, 1, 2, 0, 0, 0, 36, 49, 64, 81])) + # If we evaluate somewhere all conditions are False, we should get back NaN + nodef_func = lambdify(x, Piecewise((x, x > 0), (-x, x < 0))) + numpy.testing.assert_array_equal(nodef_func(numpy.array([-1, 0, 1])), + numpy.array([1, numpy.nan, 1])) + + +def test_numpy_logical_ops(): + if not numpy: + skip("numpy not installed.") + and_func = lambdify((x, y), And(x, y), modules="numpy") + and_func_3 = lambdify((x, y, z), And(x, y, z), modules="numpy") + or_func = lambdify((x, y), Or(x, y), modules="numpy") + or_func_3 = lambdify((x, y, z), Or(x, y, z), modules="numpy") + not_func = lambdify((x), Not(x), modules="numpy") + arr1 = numpy.array([True, True]) + arr2 = numpy.array([False, True]) + arr3 = numpy.array([True, False]) + numpy.testing.assert_array_equal(and_func(arr1, arr2), numpy.array([False, True])) + numpy.testing.assert_array_equal(and_func_3(arr1, arr2, arr3), numpy.array([False, False])) + numpy.testing.assert_array_equal(or_func(arr1, arr2), numpy.array([True, True])) + numpy.testing.assert_array_equal(or_func_3(arr1, arr2, arr3), numpy.array([True, True])) + numpy.testing.assert_array_equal(not_func(arr2), numpy.array([True, False])) + + +def test_numpy_matmul(): + if not numpy: + skip("numpy not installed.") + xmat = Matrix([[x, y], [z, 1+z]]) + ymat = Matrix([[x**2], [Abs(x)]]) + mat_func = lambdify((x, y, z), xmat*ymat, modules="numpy") + numpy.testing.assert_array_equal(mat_func(0.5, 3, 4), numpy.array([[1.625], [3.5]])) + numpy.testing.assert_array_equal(mat_func(-0.5, 3, 4), numpy.array([[1.375], [3.5]])) + # Multiple matrices chained together in multiplication + f = lambdify((x, y, z), xmat*xmat*xmat, modules="numpy") + numpy.testing.assert_array_equal(f(0.5, 3, 4), numpy.array([[72.125, 119.25], + [159, 251]])) + + +def test_numpy_numexpr(): + if not numpy: + skip("numpy not installed.") + if not numexpr: + skip("numexpr not installed.") + a, b, c = numpy.random.randn(3, 128, 128) + # ensure that numpy and numexpr return same value for complicated expression + expr = sin(x) + cos(y) + tan(z)**2 + Abs(z-y)*acos(sin(y*z)) + \ + Abs(y-z)*acosh(2+exp(y-x))- sqrt(x**2+I*y**2) + npfunc = lambdify((x, y, z), expr, modules='numpy') + nefunc = lambdify((x, y, z), expr, modules='numexpr') + assert numpy.allclose(npfunc(a, b, c), nefunc(a, b, c)) + + +def test_numexpr_userfunctions(): + if not numpy: + skip("numpy not installed.") + if not numexpr: + skip("numexpr not installed.") + a, b = numpy.random.randn(2, 10) + uf = type('uf', (Function, ), + {'eval' : classmethod(lambda x, y : y**2+1)}) + func = lambdify(x, 1-uf(x), modules='numexpr') + assert numpy.allclose(func(a), -(a**2)) + + uf = implemented_function(Function('uf'), lambda x, y : 2*x*y+1) + func = lambdify((x, y), uf(x, y), modules='numexpr') + assert numpy.allclose(func(a, b), 2*a*b+1) + + +def test_tensorflow_basic_math(): + if not tensorflow: + skip("tensorflow not installed.") + expr = Max(sin(x), Abs(1/(x+2))) + func = lambdify(x, expr, modules="tensorflow") + + with tensorflow.compat.v1.Session() as s: + a = tensorflow.constant(0, dtype=tensorflow.float32) + assert func(a).eval(session=s) == 0.5 + + +def test_tensorflow_placeholders(): + if not tensorflow: + skip("tensorflow not installed.") + expr = Max(sin(x), Abs(1/(x+2))) + func = lambdify(x, expr, modules="tensorflow") + + with tensorflow.compat.v1.Session() as s: + a = tensorflow.compat.v1.placeholder(dtype=tensorflow.float32) + assert func(a).eval(session=s, feed_dict={a: 0}) == 0.5 + + +def test_tensorflow_variables(): + if not tensorflow: + skip("tensorflow not installed.") + expr = Max(sin(x), Abs(1/(x+2))) + func = lambdify(x, expr, modules="tensorflow") + + with tensorflow.compat.v1.Session() as s: + a = tensorflow.Variable(0, dtype=tensorflow.float32) + s.run(a.initializer) + assert func(a).eval(session=s, feed_dict={a: 0}) == 0.5 + + +def test_tensorflow_logical_operations(): + if not tensorflow: + skip("tensorflow not installed.") + expr = Not(And(Or(x, y), y)) + func = lambdify([x, y], expr, modules="tensorflow") + + with tensorflow.compat.v1.Session() as s: + assert func(False, True).eval(session=s) == False + + +def test_tensorflow_piecewise(): + if not tensorflow: + skip("tensorflow not installed.") + expr = Piecewise((0, Eq(x,0)), (-1, x < 0), (1, x > 0)) + func = lambdify(x, expr, modules="tensorflow") + + with tensorflow.compat.v1.Session() as s: + assert func(-1).eval(session=s) == -1 + assert func(0).eval(session=s) == 0 + assert func(1).eval(session=s) == 1 + + +def test_tensorflow_multi_max(): + if not tensorflow: + skip("tensorflow not installed.") + expr = Max(x, -x, x**2) + func = lambdify(x, expr, modules="tensorflow") + + with tensorflow.compat.v1.Session() as s: + assert func(-2).eval(session=s) == 4 + + +def test_tensorflow_multi_min(): + if not tensorflow: + skip("tensorflow not installed.") + expr = Min(x, -x, x**2) + func = lambdify(x, expr, modules="tensorflow") + + with tensorflow.compat.v1.Session() as s: + assert func(-2).eval(session=s) == -2 + + +def test_tensorflow_relational(): + if not tensorflow: + skip("tensorflow not installed.") + expr = x >= 0 + func = lambdify(x, expr, modules="tensorflow") + + with tensorflow.compat.v1.Session() as s: + assert func(1).eval(session=s) == True + + +def test_tensorflow_complexes(): + if not tensorflow: + skip("tensorflow not installed") + + func1 = lambdify(x, re(x), modules="tensorflow") + func2 = lambdify(x, im(x), modules="tensorflow") + func3 = lambdify(x, Abs(x), modules="tensorflow") + func4 = lambdify(x, arg(x), modules="tensorflow") + + with tensorflow.compat.v1.Session() as s: + # For versions before + # https://github.com/tensorflow/tensorflow/issues/30029 + # resolved, using Python numeric types may not work + a = tensorflow.constant(1+2j) + assert func1(a).eval(session=s) == 1 + assert func2(a).eval(session=s) == 2 + + tensorflow_result = func3(a).eval(session=s) + sympy_result = Abs(1 + 2j).evalf() + assert abs(tensorflow_result-sympy_result) < 10**-6 + + tensorflow_result = func4(a).eval(session=s) + sympy_result = arg(1 + 2j).evalf() + assert abs(tensorflow_result-sympy_result) < 10**-6 + + +def test_tensorflow_array_arg(): + # Test for issue 14655 (tensorflow part) + if not tensorflow: + skip("tensorflow not installed.") + + f = lambdify([[x, y]], x*x + y, 'tensorflow') + + with tensorflow.compat.v1.Session() as s: + fcall = f(tensorflow.constant([2.0, 1.0])) + assert fcall.eval(session=s) == 5.0 + + +#================== Test symbolic ================================== + + +def test_sym_single_arg(): + f = lambdify(x, x * y) + assert f(z) == z * y + + +def test_sym_list_args(): + f = lambdify([x, y], x + y + z) + assert f(1, 2) == 3 + z + + +def test_sym_integral(): + f = Lambda(x, exp(-x**2)) + l = lambdify(x, Integral(f(x), (x, -oo, oo)), modules="sympy") + assert l(y) == Integral(exp(-y**2), (y, -oo, oo)) + assert l(y).doit() == sqrt(pi) + + +def test_namespace_order(): + # lambdify had a bug, such that module dictionaries or cached module + # dictionaries would pull earlier namespaces into themselves. + # Because the module dictionaries form the namespace of the + # generated lambda, this meant that the behavior of a previously + # generated lambda function could change as a result of later calls + # to lambdify. + n1 = {'f': lambda x: 'first f'} + n2 = {'f': lambda x: 'second f', + 'g': lambda x: 'function g'} + f = sympy.Function('f') + g = sympy.Function('g') + if1 = lambdify(x, f(x), modules=(n1, "sympy")) + assert if1(1) == 'first f' + if2 = lambdify(x, g(x), modules=(n2, "sympy")) + # previously gave 'second f' + assert if1(1) == 'first f' + + assert if2(1) == 'function g' + + +def test_imps(): + # Here we check if the default returned functions are anonymous - in + # the sense that we can have more than one function with the same name + f = implemented_function('f', lambda x: 2*x) + g = implemented_function('f', lambda x: math.sqrt(x)) + l1 = lambdify(x, f(x)) + l2 = lambdify(x, g(x)) + assert str(f(x)) == str(g(x)) + assert l1(3) == 6 + assert l2(3) == math.sqrt(3) + # check that we can pass in a Function as input + func = sympy.Function('myfunc') + assert not hasattr(func, '_imp_') + my_f = implemented_function(func, lambda x: 2*x) + assert hasattr(my_f, '_imp_') + # Error for functions with same name and different implementation + f2 = implemented_function("f", lambda x: x + 101) + raises(ValueError, lambda: lambdify(x, f(f2(x)))) + + +def test_imps_errors(): + # Test errors that implemented functions can return, and still be able to + # form expressions. + # See: https://github.com/sympy/sympy/issues/10810 + # + # XXX: Removed AttributeError here. This test was added due to issue 10810 + # but that issue was about ValueError. It doesn't seem reasonable to + # "support" catching AttributeError in the same context... + for val, error_class in product((0, 0., 2, 2.0), (TypeError, ValueError)): + + def myfunc(a): + if a == 0: + raise error_class + return 1 + + f = implemented_function('f', myfunc) + expr = f(val) + assert expr == f(val) + + +def test_imps_wrong_args(): + raises(ValueError, lambda: implemented_function(sin, lambda x: x)) + + +def test_lambdify_imps(): + # Test lambdify with implemented functions + # first test basic (sympy) lambdify + f = sympy.cos + assert lambdify(x, f(x))(0) == 1 + assert lambdify(x, 1 + f(x))(0) == 2 + assert lambdify((x, y), y + f(x))(0, 1) == 2 + # make an implemented function and test + f = implemented_function("f", lambda x: x + 100) + assert lambdify(x, f(x))(0) == 100 + assert lambdify(x, 1 + f(x))(0) == 101 + assert lambdify((x, y), y + f(x))(0, 1) == 101 + # Can also handle tuples, lists, dicts as expressions + lam = lambdify(x, (f(x), x)) + assert lam(3) == (103, 3) + lam = lambdify(x, [f(x), x]) + assert lam(3) == [103, 3] + lam = lambdify(x, [f(x), (f(x), x)]) + assert lam(3) == [103, (103, 3)] + lam = lambdify(x, {f(x): x}) + assert lam(3) == {103: 3} + lam = lambdify(x, {f(x): x}) + assert lam(3) == {103: 3} + lam = lambdify(x, {x: f(x)}) + assert lam(3) == {3: 103} + # Check that imp preferred to other namespaces by default + d = {'f': lambda x: x + 99} + lam = lambdify(x, f(x), d) + assert lam(3) == 103 + # Unless flag passed + lam = lambdify(x, f(x), d, use_imps=False) + assert lam(3) == 102 + + +def test_dummification(): + t = symbols('t') + F = Function('F') + G = Function('G') + #"\alpha" is not a valid Python variable name + #lambdify should sub in a dummy for it, and return + #without a syntax error + alpha = symbols(r'\alpha') + some_expr = 2 * F(t)**2 / G(t) + lam = lambdify((F(t), G(t)), some_expr) + assert lam(3, 9) == 2 + lam = lambdify(sin(t), 2 * sin(t)**2) + assert lam(F(t)) == 2 * F(t)**2 + #Test that \alpha was properly dummified + lam = lambdify((alpha, t), 2*alpha + t) + assert lam(2, 1) == 5 + raises(SyntaxError, lambda: lambdify(F(t) * G(t), F(t) * G(t) + 5)) + raises(SyntaxError, lambda: lambdify(2 * F(t), 2 * F(t) + 5)) + raises(SyntaxError, lambda: lambdify(2 * F(t), 4 * F(t) + 5)) + + +def test_lambdify__arguments_with_invalid_python_identifiers(): + # see sympy/sympy#26690 + N = CoordSys3D('N') + xn, yn, zn = N.base_scalars() + expr = xn + yn + f = lambdify([xn, yn], expr) + res = f(0.2, 0.3) + ref = 0.2 + 0.3 + assert abs(res-ref) < 1e-15 + + +def test_curly_matrix_symbol(): + # Issue #15009 + curlyv = sympy.MatrixSymbol("{v}", 2, 1) + lam = lambdify(curlyv, curlyv) + assert lam(1)==1 + lam = lambdify(curlyv, curlyv, dummify=True) + assert lam(1)==1 + + +def test_python_keywords(): + # Test for issue 7452. The automatic dummification should ensure use of + # Python reserved keywords as symbol names will create valid lambda + # functions. This is an additional regression test. + python_if = symbols('if') + expr = python_if / 2 + f = lambdify(python_if, expr) + assert f(4.0) == 2.0 + + +def test_lambdify_docstring(): + func = lambdify((w, x, y, z), w + x + y + z) + ref = ( + "Created with lambdify. Signature:\n\n" + "func(w, x, y, z)\n\n" + "Expression:\n\n" + "w + x + y + z" + ).splitlines() + assert func.__doc__.splitlines()[:len(ref)] == ref + syms = symbols('a1:26') + func = lambdify(syms, sum(syms)) + ref = ( + "Created with lambdify. Signature:\n\n" + "func(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15,\n" + " a16, a17, a18, a19, a20, a21, a22, a23, a24, a25)\n\n" + "Expression:\n\n" + "a1 + a10 + a11 + a12 + a13 + a14 + a15 + a16 + a17 + a18 + a19 + a2 + a20 +..." + ).splitlines() + assert func.__doc__.splitlines()[:len(ref)] == ref + + +def test_lambdify_linecache(): + func = lambdify(x, x + 1) + source = 'def _lambdifygenerated(x):\n return x + 1\n' + assert inspect.getsource(func) == source + filename = inspect.getsourcefile(func) + assert filename.startswith('= (1, 10) + + if have_scipy_1_10plus: + cm2 = lambdify((x, y), powm1(x, y), modules='scipy') + assert abs(cm2(1.2, 1e-9) - 1.82321557e-10) < 1e-17 + + +def test_scipy_bernoulli(): + if not scipy: + skip("scipy not installed") + + bern = lambdify((x,), bernoulli(x), modules='scipy') + assert bern(1) == 0.5 + + +def test_scipy_harmonic(): + if not scipy: + skip("scipy not installed") + + hn = lambdify((x,), harmonic(x), modules='scipy') + assert hn(2) == 1.5 + hnm = lambdify((x, y), harmonic(x, y), modules='scipy') + assert hnm(2, 2) == 1.25 + + +def test_cupy_array_arg(): + if not cupy: + skip("CuPy not installed") + + f = lambdify([[x, y]], x*x + y, 'cupy') + result = f(cupy.array([2.0, 1.0])) + assert result == 5 + assert "cupy" in str(type(result)) + + +def test_cupy_array_arg_using_numpy(): + # numpy functions can be run on cupy arrays + # unclear if we can "officially" support this, + # depends on numpy __array_function__ support + if not cupy: + skip("CuPy not installed") + + f = lambdify([[x, y]], x*x + y, 'numpy') + result = f(cupy.array([2.0, 1.0])) + assert result == 5 + assert "cupy" in str(type(result)) + +def test_cupy_dotproduct(): + if not cupy: + skip("CuPy not installed") + + A = Matrix([x, y, z]) + f1 = lambdify([x, y, z], DotProduct(A, A), modules='cupy') + f2 = lambdify([x, y, z], DotProduct(A, A.T), modules='cupy') + f3 = lambdify([x, y, z], DotProduct(A.T, A), modules='cupy') + f4 = lambdify([x, y, z], DotProduct(A, A.T), modules='cupy') + + assert f1(1, 2, 3) == \ + f2(1, 2, 3) == \ + f3(1, 2, 3) == \ + f4(1, 2, 3) == \ + cupy.array([14]) + + +def test_jax_array_arg(): + if not jax: + skip("JAX not installed") + + f = lambdify([[x, y]], x*x + y, 'jax') + result = f(jax.numpy.array([2.0, 1.0])) + assert result == 5 + assert "jax" in str(type(result)) + + +def test_jax_array_arg_using_numpy(): + if not jax: + skip("JAX not installed") + + f = lambdify([[x, y]], x*x + y, 'numpy') + result = f(jax.numpy.array([2.0, 1.0])) + assert result == 5 + assert "jax" in str(type(result)) + + +def test_jax_dotproduct(): + if not jax: + skip("JAX not installed") + + A = Matrix([x, y, z]) + f1 = lambdify([x, y, z], DotProduct(A, A), modules='jax') + f2 = lambdify([x, y, z], DotProduct(A, A.T), modules='jax') + f3 = lambdify([x, y, z], DotProduct(A.T, A), modules='jax') + f4 = lambdify([x, y, z], DotProduct(A, A.T), modules='jax') + + assert f1(1, 2, 3) == \ + f2(1, 2, 3) == \ + f3(1, 2, 3) == \ + f4(1, 2, 3) == \ + jax.numpy.array([14]) + + +def test_lambdify_cse(): + def no_op_cse(exprs): + return (), exprs + + def dummy_cse(exprs): + from sympy.simplify.cse_main import cse + return cse(exprs, symbols=numbered_symbols(cls=Dummy)) + + def minmem(exprs): + from sympy.simplify.cse_main import cse_release_variables, cse + return cse(exprs, postprocess=cse_release_variables) + + class Case: + def __init__(self, *, args, exprs, num_args, requires_numpy=False): + self.args = args + self.exprs = exprs + self.num_args = num_args + subs_dict = dict(zip(self.args, self.num_args)) + self.ref = [e.subs(subs_dict).evalf() for e in exprs] + self.requires_numpy = requires_numpy + + def lambdify(self, *, cse): + return lambdify(self.args, self.exprs, cse=cse) + + def assertAllClose(self, result, *, abstol=1e-15, reltol=1e-15): + if self.requires_numpy: + assert all(numpy.allclose(result[i], numpy.asarray(r, dtype=float), + rtol=reltol, atol=abstol) + for i, r in enumerate(self.ref)) + return + + for i, r in enumerate(self.ref): + abs_err = abs(result[i] - r) + if r == 0: + assert abs_err < abstol + else: + assert abs_err/abs(r) < reltol + + cases = [ + Case( + args=(x, y, z), + exprs=[ + x + y + z, + x + y - z, + 2*x + 2*y - z, + (x+y)**2 + (y+z)**2, + ], + num_args=(2., 3., 4.) + ), + Case( + args=(x, y, z), + exprs=[ + x + sympy.Heaviside(x), + y + sympy.Heaviside(x), + z + sympy.Heaviside(x, 1), + z/sympy.Heaviside(x, 1) + ], + num_args=(0., 3., 4.) + ), + Case( + args=(x, y, z), + exprs=[ + x + sinc(y), + y + sinc(y), + z - sinc(y) + ], + num_args=(0.1, 0.2, 0.3) + ), + Case( + args=(x, y, z), + exprs=[ + Matrix([[x, x*y], [sin(z) + 4, x**z]]), + x*y+sin(z)-x**z, + Matrix([x*x, sin(z), x**z]) + ], + num_args=(1.,2.,3.), + requires_numpy=True + ), + Case( + args=(x, y), + exprs=[(x + y - 1)**2, x, x + y, + (x + y)/(2*x + 1) + (x + y - 1)**2, (2*x + 1)**(x + y)], + num_args=(1,2) + ) + ] + for case in cases: + if not numpy and case.requires_numpy: + continue + for _cse in [False, True, minmem, no_op_cse, dummy_cse]: + f = case.lambdify(cse=_cse) + result = f(*case.num_args) + case.assertAllClose(result) + +def test_issue_25288(): + syms = numbered_symbols(cls=Dummy) + ok = lambdify(x, [x**2, sin(x**2)], cse=lambda e: cse(e, symbols=syms))(2) + assert ok + + +def test_deprecated_set(): + with warns_deprecated_sympy(): + lambdify({x, y}, x + y) + +def test_issue_13881(): + if not numpy: + skip("numpy not installed.") + + X = MatrixSymbol('X', 3, 1) + + f = lambdify(X, X.T*X, 'numpy') + assert f(numpy.array([1, 2, 3])) == 14 + assert f(numpy.array([3, 2, 1])) == 14 + + f = lambdify(X, X*X.T, 'numpy') + assert f(numpy.array([1, 2, 3])) == 14 + assert f(numpy.array([3, 2, 1])) == 14 + + f = lambdify(X, (X*X.T)*X, 'numpy') + arr1 = numpy.array([[1], [2], [3]]) + arr2 = numpy.array([[14],[28],[42]]) + + assert numpy.array_equal(f(arr1), arr2) + + +def test_23536_lambdify_cse_dummy(): + + f = Function('x')(y) + g = Function('w')(y) + expr = z + (f**4 + g**5)*(f**3 + (g*f)**3) + expr = expr.expand() + eval_expr = lambdify(((f, g), z), expr, cse=True) + ans = eval_expr((1.0, 2.0), 3.0) # shouldn't raise NameError + assert ans == 300.0 # not a list and value is 300 + + +class LambdifyDocstringTestCase: + SIGNATURE = None + EXPR = None + SRC = None + + def __init__(self, docstring_limit, expected_redacted): + self.docstring_limit = docstring_limit + self.expected_redacted = expected_redacted + + @property + def expected_expr(self): + expr_redacted_msg = "EXPRESSION REDACTED DUE TO LENGTH, (see lambdify's `docstring_limit`)" + return self.EXPR if not self.expected_redacted else expr_redacted_msg + + @property + def expected_src(self): + src_redacted_msg = "SOURCE CODE REDACTED DUE TO LENGTH, (see lambdify's `docstring_limit`)" + return self.SRC if not self.expected_redacted else src_redacted_msg + + @property + def expected_docstring(self): + expected_docstring = ( + f'Created with lambdify. Signature:\n\n' + f'func({self.SIGNATURE})\n\n' + f'Expression:\n\n' + f'{self.expected_expr}\n\n' + f'Source code:\n\n' + f'{self.expected_src}\n\n' + f'Imported modules:\n\n' + ) + return expected_docstring + + def __len__(self): + return len(self.expected_docstring) + + def __repr__(self): + return ( + f'{self.__class__.__name__}(' + f'docstring_limit={self.docstring_limit}, ' + f'expected_redacted={self.expected_redacted})' + ) + + +def test_lambdify_docstring_size_limit_simple_symbol(): + + class SimpleSymbolTestCase(LambdifyDocstringTestCase): + SIGNATURE = 'x' + EXPR = 'x' + SRC = ( + 'def _lambdifygenerated(x):\n' + ' return x\n' + ) + + x = symbols('x') + + test_cases = ( + SimpleSymbolTestCase(docstring_limit=None, expected_redacted=False), + SimpleSymbolTestCase(docstring_limit=100, expected_redacted=False), + SimpleSymbolTestCase(docstring_limit=1, expected_redacted=False), + SimpleSymbolTestCase(docstring_limit=0, expected_redacted=True), + SimpleSymbolTestCase(docstring_limit=-1, expected_redacted=True), + ) + for test_case in test_cases: + lambdified_expr = lambdify( + [x], + x, + 'sympy', + docstring_limit=test_case.docstring_limit, + ) + assert lambdified_expr.__doc__ == test_case.expected_docstring + + +def test_lambdify_docstring_size_limit_nested_expr(): + + class ExprListTestCase(LambdifyDocstringTestCase): + SIGNATURE = 'x, y, z' + EXPR = ( + '[x, [y], z, x**3 + 3*x**2*y + 3*x**2*z + 3*x*y**2 + 6*x*y*z ' + '+ 3*x*z**2 +...' + ) + SRC = ( + 'def _lambdifygenerated(x, y, z):\n' + ' return [x, [y], z, x**3 + 3*x**2*y + 3*x**2*z + 3*x*y**2 ' + '+ 6*x*y*z + 3*x*z**2 + y**3 + 3*y**2*z + 3*y*z**2 + z**3]\n' + ) + + x, y, z = symbols('x, y, z') + expr = [x, [y], z, ((x + y + z)**3).expand()] + + test_cases = ( + ExprListTestCase(docstring_limit=None, expected_redacted=False), + ExprListTestCase(docstring_limit=200, expected_redacted=False), + ExprListTestCase(docstring_limit=50, expected_redacted=True), + ExprListTestCase(docstring_limit=0, expected_redacted=True), + ExprListTestCase(docstring_limit=-1, expected_redacted=True), + ) + for test_case in test_cases: + lambdified_expr = lambdify( + [x, y, z], + expr, + 'sympy', + docstring_limit=test_case.docstring_limit, + ) + assert lambdified_expr.__doc__ == test_case.expected_docstring + + +def test_lambdify_docstring_size_limit_matrix(): + + class MatrixTestCase(LambdifyDocstringTestCase): + SIGNATURE = 'x, y, z' + EXPR = ( + 'Matrix([[0, x], [x + y + z, x**3 + 3*x**2*y + 3*x**2*z + 3*x*y**2 ' + '+ 6*x*y*z...' + ) + SRC = ( + 'def _lambdifygenerated(x, y, z):\n' + ' return ImmutableDenseMatrix([[0, x], [x + y + z, x**3 ' + '+ 3*x**2*y + 3*x**2*z + 3*x*y**2 + 6*x*y*z + 3*x*z**2 + y**3 ' + '+ 3*y**2*z + 3*y*z**2 + z**3]])\n' + ) + + x, y, z = symbols('x, y, z') + expr = Matrix([[S.Zero, x], [x + y + z, ((x + y + z)**3).expand()]]) + + test_cases = ( + MatrixTestCase(docstring_limit=None, expected_redacted=False), + MatrixTestCase(docstring_limit=200, expected_redacted=False), + MatrixTestCase(docstring_limit=50, expected_redacted=True), + MatrixTestCase(docstring_limit=0, expected_redacted=True), + MatrixTestCase(docstring_limit=-1, expected_redacted=True), + ) + for test_case in test_cases: + lambdified_expr = lambdify( + [x, y, z], + expr, + 'sympy', + docstring_limit=test_case.docstring_limit, + ) + assert lambdified_expr.__doc__ == test_case.expected_docstring + + +def test_lambdify_empty_tuple(): + a = symbols("a") + expr = ((), (a,)) + f = lambdify(a, expr) + result = f(1) + assert result == ((), (1,)), "Lambdify did not handle the empty tuple correctly." + + +def test_assoc_legendre_numerical_evaluation(): + + tol = 1e-10 + + sympy_result_integer = assoc_legendre(1, 1/2, 0.1).evalf() + sympy_result_complex = assoc_legendre(2, 1, 3).evalf() + mpmath_result_integer = -0.474572528387641 + mpmath_result_complex = -25.45584412271571*I + + assert all_close(sympy_result_integer, mpmath_result_integer, tol) + assert all_close(sympy_result_complex, mpmath_result_complex, tol) + + +def test_Piecewise(): + + modules = [math] + if numpy: + modules.append('numpy') + + for mod in modules: + # test isinf + f = lambdify(x, Piecewise((7.0, isinf(x)), (3.0, True)), mod) + assert f(+float('inf')) == +7.0 + assert f(-float('inf')) == +7.0 + assert f(42.) == 3.0 + + f2 = lambdify(x, Piecewise((7.0*sign(x), isinf(x)), (3.0, True)), mod) + assert f2(+float('inf')) == +7.0 + assert f2(-float('inf')) == -7.0 + assert f2(42.) == 3.0 + + # test isnan (gh-26784) + g = lambdify(x, Piecewise((7.0, isnan(x)), (3.0, True)), mod) + assert g(float('nan')) == 7.0 + assert g(42.) == 3.0 + + +def test_array_symbol(): + if not numpy: + skip("numpy not installed.") + a = ArraySymbol('a', (3,)) + f = lambdify((a), a) + assert numpy.all(f(numpy.array([1,2,3])) == numpy.array([1,2,3])) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_matchpy_connector.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_matchpy_connector.py new file mode 100644 index 0000000000000000000000000000000000000000..3648bd49f9e56ca20fbf428ed46c01429dbe8b15 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_matchpy_connector.py @@ -0,0 +1,164 @@ +import pickle + +from sympy.core.relational import (Eq, Ne) +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.external import import_module +from sympy.testing.pytest import skip +from sympy.utilities.matchpy_connector import WildDot, WildPlus, WildStar, Replacer + +matchpy = import_module("matchpy") + +x, y, z = symbols("x y z") + + +def _get_first_match(expr, pattern): + from matchpy import ManyToOneMatcher, Pattern + + matcher = ManyToOneMatcher() + matcher.add(Pattern(pattern)) + return next(iter(matcher.match(expr))) + + +def test_matchpy_connector(): + if matchpy is None: + skip("matchpy not installed") + + from multiset import Multiset + from matchpy import Pattern, Substitution + + w_ = WildDot("w_") + w__ = WildPlus("w__") + w___ = WildStar("w___") + + expr = x + y + pattern = x + w_ + p, subst = _get_first_match(expr, pattern) + assert p == Pattern(pattern) + assert subst == Substitution({'w_': y}) + + expr = x + y + z + pattern = x + w__ + p, subst = _get_first_match(expr, pattern) + assert p == Pattern(pattern) + assert subst == Substitution({'w__': Multiset([y, z])}) + + expr = x + y + z + pattern = x + y + z + w___ + p, subst = _get_first_match(expr, pattern) + assert p == Pattern(pattern) + assert subst == Substitution({'w___': Multiset()}) + + +def test_matchpy_optional(): + if matchpy is None: + skip("matchpy not installed") + + from matchpy import Pattern, Substitution + from matchpy import ManyToOneReplacer, ReplacementRule + + p = WildDot("p", optional=1) + q = WildDot("q", optional=0) + + pattern = p*x + q + + expr1 = 2*x + pa, subst = _get_first_match(expr1, pattern) + assert pa == Pattern(pattern) + assert subst == Substitution({'p': 2, 'q': 0}) + + expr2 = x + 3 + pa, subst = _get_first_match(expr2, pattern) + assert pa == Pattern(pattern) + assert subst == Substitution({'p': 1, 'q': 3}) + + expr3 = x + pa, subst = _get_first_match(expr3, pattern) + assert pa == Pattern(pattern) + assert subst == Substitution({'p': 1, 'q': 0}) + + expr4 = x*y + z + pa, subst = _get_first_match(expr4, pattern) + assert pa == Pattern(pattern) + assert subst == Substitution({'p': y, 'q': z}) + + replacer = ManyToOneReplacer() + replacer.add(ReplacementRule(Pattern(pattern), lambda p, q: sin(p)*cos(q))) + assert replacer.replace(expr1) == sin(2)*cos(0) + assert replacer.replace(expr2) == sin(1)*cos(3) + assert replacer.replace(expr3) == sin(1)*cos(0) + assert replacer.replace(expr4) == sin(y)*cos(z) + + +def test_replacer(): + if matchpy is None: + skip("matchpy not installed") + + for info in [True, False]: + for lambdify in [True, False]: + _perform_test_replacer(info, lambdify) + + +def _perform_test_replacer(info, lambdify): + + x1_ = WildDot("x1_") + x2_ = WildDot("x2_") + + a_ = WildDot("a_", optional=S.One) + b_ = WildDot("b_", optional=S.One) + c_ = WildDot("c_", optional=S.Zero) + + replacer = Replacer(common_constraints=[ + matchpy.CustomConstraint(lambda a_: not a_.has(x)), + matchpy.CustomConstraint(lambda b_: not b_.has(x)), + matchpy.CustomConstraint(lambda c_: not c_.has(x)), + ], lambdify=lambdify, info=info) + + # Rewrite the equation into implicit form, unless it's already solved: + replacer.add(Eq(x1_, x2_), Eq(x1_ - x2_, 0), conditions_nonfalse=[Ne(x2_, 0), Ne(x1_, 0), Ne(x1_, x), Ne(x2_, x)], info=1) + + # Simple equation solver for real numbers: + replacer.add(Eq(a_*x + b_, 0), Eq(x, -b_/a_), info=2) + disc = b_**2 - 4*a_*c_ + replacer.add( + Eq(a_*x**2 + b_*x + c_, 0), + Eq(x, (-b_ - sqrt(disc))/(2*a_)) | Eq(x, (-b_ + sqrt(disc))/(2*a_)), + conditions_nonfalse=[disc >= 0], + info=3 + ) + replacer.add( + Eq(a_*x**2 + c_, 0), + Eq(x, sqrt(-c_/a_)) | Eq(x, -sqrt(-c_/a_)), + conditions_nonfalse=[-c_*a_ > 0], + info=4 + ) + + g = lambda expr, infos: (expr, infos) if info else expr + + assert replacer.replace(Eq(3*x, y)) == g(Eq(x, y/3), [1, 2]) + assert replacer.replace(Eq(x**2 + 1, 0)) == g(Eq(x**2 + 1, 0), []) + assert replacer.replace(Eq(x**2, 4)) == g((Eq(x, 2) | Eq(x, -2)), [1, 4]) + assert replacer.replace(Eq(x**2 + 4*y*x + 4*y**2, 0)) == g(Eq(x, -2*y), [3]) + + +def test_matchpy_object_pickle(): + if matchpy is None: + return + + a1 = WildDot("a") + a2 = pickle.loads(pickle.dumps(a1)) + assert a1 == a2 + + a1 = WildDot("a", S(1)) + a2 = pickle.loads(pickle.dumps(a1)) + assert a1 == a2 + + a1 = WildPlus("a", S(1)) + a2 = pickle.loads(pickle.dumps(a1)) + assert a1 == a2 + + a1 = WildStar("a", S(1)) + a2 = pickle.loads(pickle.dumps(a1)) + assert a1 == a2 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_mathml.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_mathml.py new file mode 100644 index 0000000000000000000000000000000000000000..e4a7598f175be34f8bb34c0bd9c003d1c0238c7b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_mathml.py @@ -0,0 +1,33 @@ +import os +from textwrap import dedent +from sympy.external import import_module +from sympy.testing.pytest import skip +from sympy.utilities.mathml import apply_xsl + + + +lxml = import_module('lxml') + +path = os.path.abspath(os.path.join(os.path.dirname(__file__), "test_xxe.py")) + + +def test_xxe(): + assert os.path.isfile(path) + if not lxml: + skip("lxml not installed.") + + mml = dedent( + rf""" + + ]> + + John + &ent; + + """ + ) + xsl = 'mathml/data/simple_mmlctop.xsl' + + res = apply_xsl(mml, xsl) + assert res == \ + '\n\nJohn\n\n\n' diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_misc.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_misc.py new file mode 100644 index 0000000000000000000000000000000000000000..0a3e059419303c33cbd7b770679b5efc1b03486d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_misc.py @@ -0,0 +1,148 @@ +from textwrap import dedent +import sys +from subprocess import Popen, PIPE +import os + +from sympy.core.singleton import S +from sympy.testing.pytest import (raises, warns_deprecated_sympy, + skip_under_pyodide) +from sympy.utilities.misc import (translate, replace, ordinal, rawlines, + strlines, as_int, find_executable) + + +def test_translate(): + abc = 'abc' + assert translate(abc, None, 'a') == 'bc' + assert translate(abc, None, '') == 'abc' + assert translate(abc, {'a': 'x'}, 'c') == 'xb' + assert translate(abc, {'a': 'bc'}, 'c') == 'bcb' + assert translate(abc, {'ab': 'x'}, 'c') == 'x' + assert translate(abc, {'ab': ''}, 'c') == '' + assert translate(abc, {'bc': 'x'}, 'c') == 'ab' + assert translate(abc, {'abc': 'x', 'a': 'y'}) == 'x' + u = chr(4096) + assert translate(abc, 'a', 'x', u) == 'xbc' + assert (u in translate(abc, 'a', u, u)) is True + + +def test_replace(): + assert replace('abc', ('a', 'b')) == 'bbc' + assert replace('abc', {'a': 'Aa'}) == 'Aabc' + assert replace('abc', ('a', 'b'), ('c', 'C')) == 'bbC' + + +def test_ordinal(): + assert ordinal(-1) == '-1st' + assert ordinal(0) == '0th' + assert ordinal(1) == '1st' + assert ordinal(2) == '2nd' + assert ordinal(3) == '3rd' + assert all(ordinal(i).endswith('th') for i in range(4, 21)) + assert ordinal(100) == '100th' + assert ordinal(101) == '101st' + assert ordinal(102) == '102nd' + assert ordinal(103) == '103rd' + assert ordinal(104) == '104th' + assert ordinal(200) == '200th' + assert all(ordinal(i) == str(i) + 'th' for i in range(-220, -203)) + + +def test_rawlines(): + assert rawlines('a a\na') == "dedent('''\\\n a a\n a''')" + assert rawlines('a a') == "'a a'" + assert rawlines(strlines('\\le"ft')) == ( + '(\n' + " '(\\n'\n" + ' \'r\\\'\\\\le"ft\\\'\\n\'\n' + " ')'\n" + ')') + + +def test_strlines(): + q = 'this quote (") is in the middle' + # the following assert rhs was prepared with + # print(rawlines(strlines(q, 10))) + assert strlines(q, 10) == dedent('''\ + ( + 'this quo' + 'te (") i' + 's in the' + ' middle' + )''') + assert q == ( + 'this quo' + 'te (") i' + 's in the' + ' middle' + ) + q = "this quote (') is in the middle" + assert strlines(q, 20) == dedent('''\ + ( + "this quote (') is " + "in the middle" + )''') + assert strlines('\\left') == ( + '(\n' + "r'\\left'\n" + ')') + assert strlines('\\left', short=True) == r"r'\left'" + assert strlines('\\le"ft') == ( + '(\n' + 'r\'\\le"ft\'\n' + ')') + q = 'this\nother line' + assert strlines(q) == rawlines(q) + + +def test_translate_args(): + try: + translate(None, None, None, 'not_none') + except ValueError: + pass # Exception raised successfully + else: + assert False + + assert translate('s', None, None, None) == 's' + + try: + translate('s', 'a', 'bc') + except ValueError: + pass # Exception raised successfully + else: + assert False + + +@skip_under_pyodide("Cannot create subprocess under pyodide.") +def test_debug_output(): + env = os.environ.copy() + env['SYMPY_DEBUG'] = 'True' + cmd = 'from sympy import *; x = Symbol("x"); print(integrate((1-cos(x))/x, x))' + cmdline = [sys.executable, '-c', cmd] + proc = Popen(cmdline, env=env, stdout=PIPE, stderr=PIPE) + out, err = proc.communicate() + out = out.decode('ascii') # utf-8? + err = err.decode('ascii') + expected = 'substituted: -x*(1 - cos(x)), u: 1/x, u_var: _u' + assert expected in err, err + + +def test_as_int(): + raises(ValueError, lambda : as_int(True)) + raises(ValueError, lambda : as_int(1.1)) + raises(ValueError, lambda : as_int([])) + raises(ValueError, lambda : as_int(S.NaN)) + raises(ValueError, lambda : as_int(S.Infinity)) + raises(ValueError, lambda : as_int(S.NegativeInfinity)) + raises(ValueError, lambda : as_int(S.ComplexInfinity)) + # for the following, limited precision makes int(arg) == arg + # but the int value is not necessarily what a user might have + # expected; Q.prime is more nuanced in its response for + # expressions which might be complex representations of an + # integer. This is not -- by design -- as_ints role. + raises(ValueError, lambda : as_int(1e23)) + raises(ValueError, lambda : as_int(S('1.'+'0'*20+'1'))) + assert as_int(True, strict=False) == 1 + +def test_deprecated_find_executable(): + with warns_deprecated_sympy(): + find_executable('python') diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_pickling.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_pickling.py new file mode 100644 index 0000000000000000000000000000000000000000..7ea2d062286cbcf802eb0f42f2d7d130123599af --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_pickling.py @@ -0,0 +1,723 @@ +import inspect +import copy +import pickle + +from sympy.physics.units import meter + +from sympy.testing.pytest import XFAIL, raises, ignore_warnings + +from sympy.core.basic import Atom, Basic +from sympy.core.singleton import SingletonRegistry +from sympy.core.symbol import Str, Dummy, Symbol, Wild +from sympy.core.numbers import (E, I, pi, oo, zoo, nan, Integer, + Rational, Float, AlgebraicNumber) +from sympy.core.relational import (Equality, GreaterThan, LessThan, Relational, + StrictGreaterThan, StrictLessThan, Unequality) +from sympy.core.add import Add +from sympy.core.mul import Mul +from sympy.core.power import Pow +from sympy.core.function import Derivative, Function, FunctionClass, Lambda, \ + WildFunction +from sympy.sets.sets import Interval +from sympy.core.multidimensional import vectorize + +from sympy.external.gmpy import gmpy as _gmpy +from sympy.utilities.exceptions import SymPyDeprecationWarning + +from sympy.core.singleton import S +from sympy.core.symbol import symbols + +from sympy.external import import_module +cloudpickle = import_module('cloudpickle') + + +not_equal_attrs = { + '_assumptions', # This is a local cache that isn't automatically filled on creation + '_mhash', # Cached after __hash__ is called but set to None after creation +} + + +deprecated_attrs = { + 'is_EmptySet', # Deprecated from SymPy 1.5. This can be removed when is_EmptySet is removed. + 'expr_free_symbols', # Deprecated from SymPy 1.9. This can be removed when exr_free_symbols is removed. +} + +dont_check_attrs = { + '_sage_', # Fails because Sage is not installed +} + + +def check(a, exclude=[], check_attr=True, deprecated=()): + """ Check that pickling and copying round-trips. + """ + # Pickling with protocols 0 and 1 is disabled for Basic instances: + if isinstance(a, Basic): + for protocol in [0, 1]: + raises(NotImplementedError, lambda: pickle.dumps(a, protocol)) + + protocols = [2, copy.copy, copy.deepcopy, 3, 4] + if cloudpickle: + protocols.extend([cloudpickle]) + + for protocol in protocols: + if protocol in exclude: + continue + + if callable(protocol): + if isinstance(a, type): + # Classes can't be copied, but that's okay. + continue + b = protocol(a) + elif inspect.ismodule(protocol): + b = protocol.loads(protocol.dumps(a)) + else: + b = pickle.loads(pickle.dumps(a, protocol)) + + d1 = dir(a) + d2 = dir(b) + assert set(d1) == set(d2) + + if not check_attr: + continue + + def c(a, b, d): + for i in d: + if i in dont_check_attrs: + continue + elif i in not_equal_attrs: + if hasattr(a, i): + assert hasattr(b, i), i + elif i in deprecated_attrs or i in deprecated: + with ignore_warnings(SymPyDeprecationWarning): + assert getattr(a, i) == getattr(b, i), i + elif not hasattr(a, i): + continue + else: + attr = getattr(a, i) + if not hasattr(attr, "__call__"): + assert hasattr(b, i), i + assert getattr(b, i) == attr, "%s != %s, protocol: %s" % (getattr(b, i), attr, protocol) + + c(a, b, d1) + c(b, a, d2) + + + +#================== core ========================= + + +def test_core_basic(): + for c in (Atom, Atom(), Basic, Basic(), SingletonRegistry, S): + check(c) + +def test_core_Str(): + check(Str('x')) + +def test_core_symbol(): + # make the Symbol a unique name that doesn't class with any other + # testing variable in this file since after this test the symbol + # having the same name will be cached as noncommutative + for c in (Dummy, Dummy("x", commutative=False), Symbol, + Symbol("_issue_3130", commutative=False), Wild, Wild("x")): + check(c) + + +def test_core_numbers(): + for c in (Integer(2), Rational(2, 3), Float("1.2")): + check(c) + for c in (AlgebraicNumber, AlgebraicNumber(sqrt(3))): + check(c, check_attr=False) + + +def test_core_float_copy(): + # See gh-7457 + y = Symbol("x") + 1.0 + check(y) # does not raise TypeError ("argument is not an mpz") + + +def test_core_relational(): + x = Symbol("x") + y = Symbol("y") + for c in (Equality, Equality(x, y), GreaterThan, GreaterThan(x, y), + LessThan, LessThan(x, y), Relational, Relational(x, y), + StrictGreaterThan, StrictGreaterThan(x, y), StrictLessThan, + StrictLessThan(x, y), Unequality, Unequality(x, y)): + check(c) + + +def test_core_add(): + x = Symbol("x") + for c in (Add, Add(x, 4)): + check(c) + + +def test_core_mul(): + x = Symbol("x") + for c in (Mul, Mul(x, 4)): + check(c) + + +def test_core_power(): + x = Symbol("x") + for c in (Pow, Pow(x, 4)): + check(c) + + +def test_core_function(): + x = Symbol("x") + for f in (Derivative, Derivative(x), Function, FunctionClass, Lambda, + WildFunction): + check(f) + + +def test_core_undefinedfunctions(): + f = Function("f") + check(f) + + +def test_core_appliedundef(): + x = Symbol("_long_unique_name_1") + f = Function("_long_unique_name_2") + check(f(x)) + + +def test_core_interval(): + for c in (Interval, Interval(0, 2)): + check(c) + + +def test_core_multidimensional(): + for c in (vectorize, vectorize(0)): + check(c) + + +def test_Singletons(): + protocols = [0, 1, 2, 3, 4] + copiers = [copy.copy, copy.deepcopy] + copiers += [lambda x: pickle.loads(pickle.dumps(x, proto)) + for proto in protocols] + if cloudpickle: + copiers += [lambda x: cloudpickle.loads(cloudpickle.dumps(x))] + + for obj in (Integer(-1), Integer(0), Integer(1), Rational(1, 2), pi, E, I, + oo, -oo, zoo, nan, S.GoldenRatio, S.TribonacciConstant, + S.EulerGamma, S.Catalan, S.EmptySet, S.IdentityFunction): + for func in copiers: + assert func(obj) is obj + +#================== combinatorics =================== +from sympy.combinatorics.free_groups import FreeGroup + +def test_free_group(): + check(FreeGroup("x, y, z"), check_attr=False) + +#================== functions =================== +from sympy.functions import (Piecewise, lowergamma, acosh, chebyshevu, + chebyshevt, ln, chebyshevt_root, legendre, Heaviside, bernoulli, coth, + tanh, assoc_legendre, sign, arg, asin, DiracDelta, re, rf, Abs, + uppergamma, binomial, sinh, cos, cot, acos, acot, gamma, bell, + hermite, harmonic, LambertW, zeta, log, factorial, asinh, acoth, cosh, + dirichlet_eta, Eijk, loggamma, erf, ceiling, im, fibonacci, + tribonacci, conjugate, tan, chebyshevu_root, floor, atanh, sqrt, sin, + atan, ff, lucas, atan2, polygamma, exp) + + +def test_functions(): + one_var = (acosh, ln, Heaviside, factorial, bernoulli, coth, tanh, + sign, arg, asin, DiracDelta, re, Abs, sinh, cos, cot, acos, acot, + gamma, bell, harmonic, LambertW, zeta, log, factorial, asinh, + acoth, cosh, dirichlet_eta, loggamma, erf, ceiling, im, fibonacci, + tribonacci, conjugate, tan, floor, atanh, sin, atan, lucas, exp) + two_var = (rf, ff, lowergamma, chebyshevu, chebyshevt, binomial, + atan2, polygamma, hermite, legendre, uppergamma) + x, y, z = symbols("x,y,z") + others = (chebyshevt_root, chebyshevu_root, Eijk(x, y, z), + Piecewise( (0, x < -1), (x**2, x <= 1), (x**3, True)), + assoc_legendre) + for cls in one_var: + check(cls) + c = cls(x) + check(c) + for cls in two_var: + check(cls) + c = cls(x, y) + check(c) + for cls in others: + check(cls) + +#================== geometry ==================== +from sympy.geometry.entity import GeometryEntity +from sympy.geometry.point import Point +from sympy.geometry.ellipse import Circle, Ellipse +from sympy.geometry.line import Line, LinearEntity, Ray, Segment +from sympy.geometry.polygon import Polygon, RegularPolygon, Triangle + + +def test_geometry(): + p1 = Point(1, 2) + p2 = Point(2, 3) + p3 = Point(0, 0) + p4 = Point(0, 1) + for c in ( + GeometryEntity, GeometryEntity(), Point, p1, Circle, Circle(p1, 2), + Ellipse, Ellipse(p1, 3, 4), Line, Line(p1, p2), LinearEntity, + LinearEntity(p1, p2), Ray, Ray(p1, p2), Segment, Segment(p1, p2), + Polygon, Polygon(p1, p2, p3, p4), RegularPolygon, + RegularPolygon(p1, 4, 5), Triangle, Triangle(p1, p2, p3)): + check(c, check_attr=False) + +#================== integrals ==================== +from sympy.integrals.integrals import Integral + + +def test_integrals(): + x = Symbol("x") + for c in (Integral, Integral(x)): + check(c) + +#==================== logic ===================== +from sympy.core.logic import Logic + + +def test_logic(): + for c in (Logic, Logic(1)): + check(c) + +#================== matrices ==================== +from sympy.matrices import Matrix, SparseMatrix + + +def test_matrices(): + for c in (Matrix, Matrix([1, 2, 3]), SparseMatrix, SparseMatrix([[1, 2], [3, 4]])): + check(c, deprecated=['_smat', '_mat']) + +#================== ntheory ===================== +from sympy.ntheory.generate import Sieve + + +def test_ntheory(): + for c in (Sieve, Sieve()): + check(c) + +#================== physics ===================== +from sympy.physics.paulialgebra import Pauli +from sympy.physics.units import Unit + + +def test_physics(): + for c in (Unit, meter, Pauli, Pauli(1)): + check(c) + +#================== plotting ==================== +# XXX: These tests are not complete, so XFAIL them + + +@XFAIL +def test_plotting(): + from sympy.plotting.pygletplot.color_scheme import ColorGradient, ColorScheme + from sympy.plotting.pygletplot.managed_window import ManagedWindow + from sympy.plotting.plot import Plot, ScreenShot + from sympy.plotting.pygletplot.plot_axes import PlotAxes, PlotAxesBase, PlotAxesFrame, PlotAxesOrdinate + from sympy.plotting.pygletplot.plot_camera import PlotCamera + from sympy.plotting.pygletplot.plot_controller import PlotController + from sympy.plotting.pygletplot.plot_curve import PlotCurve + from sympy.plotting.pygletplot.plot_interval import PlotInterval + from sympy.plotting.pygletplot.plot_mode import PlotMode + from sympy.plotting.pygletplot.plot_modes import Cartesian2D, Cartesian3D, Cylindrical, \ + ParametricCurve2D, ParametricCurve3D, ParametricSurface, Polar, Spherical + from sympy.plotting.pygletplot.plot_object import PlotObject + from sympy.plotting.pygletplot.plot_surface import PlotSurface + from sympy.plotting.pygletplot.plot_window import PlotWindow + for c in ( + ColorGradient, ColorGradient(0.2, 0.4), ColorScheme, ManagedWindow, + ManagedWindow, Plot, ScreenShot, PlotAxes, PlotAxesBase, + PlotAxesFrame, PlotAxesOrdinate, PlotCamera, PlotController, + PlotCurve, PlotInterval, PlotMode, Cartesian2D, Cartesian3D, + Cylindrical, ParametricCurve2D, ParametricCurve3D, + ParametricSurface, Polar, Spherical, PlotObject, PlotSurface, + PlotWindow): + check(c) + + +@XFAIL +def test_plotting2(): + #from sympy.plotting.color_scheme import ColorGradient + from sympy.plotting.pygletplot.color_scheme import ColorScheme + #from sympy.plotting.managed_window import ManagedWindow + from sympy.plotting.plot import Plot + #from sympy.plotting.plot import ScreenShot + from sympy.plotting.pygletplot.plot_axes import PlotAxes + #from sympy.plotting.plot_axes import PlotAxesBase, PlotAxesFrame, PlotAxesOrdinate + #from sympy.plotting.plot_camera import PlotCamera + #from sympy.plotting.plot_controller import PlotController + #from sympy.plotting.plot_curve import PlotCurve + #from sympy.plotting.plot_interval import PlotInterval + #from sympy.plotting.plot_mode import PlotMode + #from sympy.plotting.plot_modes import Cartesian2D, Cartesian3D, Cylindrical, \ + # ParametricCurve2D, ParametricCurve3D, ParametricSurface, Polar, Spherical + #from sympy.plotting.plot_object import PlotObject + #from sympy.plotting.plot_surface import PlotSurface + # from sympy.plotting.plot_window import PlotWindow + check(ColorScheme("rainbow")) + check(Plot(1, visible=False)) + check(PlotAxes()) + +#================== polys ======================= +from sympy.polys.domains.integerring import ZZ +from sympy.polys.domains.rationalfield import QQ +from sympy.polys.orderings import lex +from sympy.polys.polytools import Poly + +def test_pickling_polys_polytools(): + from sympy.polys.polytools import PurePoly + # from sympy.polys.polytools import GroebnerBasis + x = Symbol('x') + + for c in (Poly, Poly(x, x)): + check(c) + + for c in (PurePoly, PurePoly(x)): + check(c) + + # TODO: fix pickling of Options class (see GroebnerBasis._options) + # for c in (GroebnerBasis, GroebnerBasis([x**2 - 1], x, order=lex)): + # check(c) + +def test_pickling_polys_polyclasses(): + from sympy.polys.polyclasses import DMP, DMF, ANP + + for c in (DMP, DMP([[ZZ(1)], [ZZ(2)], [ZZ(3)]], ZZ)): + check(c, deprecated=['rep']) + for c in (DMF, DMF(([ZZ(1), ZZ(2)], [ZZ(1), ZZ(3)]), ZZ)): + check(c) + for c in (ANP, ANP([QQ(1), QQ(2)], [QQ(1), QQ(2), QQ(3)], QQ)): + check(c) + +@XFAIL +def test_pickling_polys_rings(): + # NOTE: can't use protocols < 2 because we have to execute __new__ to + # make sure caching of rings works properly. + + from sympy.polys.rings import PolyRing + + ring = PolyRing("x,y,z", ZZ, lex) + + for c in (PolyRing, ring): + check(c, exclude=[0, 1]) + + for c in (ring.dtype, ring.one): + check(c, exclude=[0, 1], check_attr=False) # TODO: Py3k + +def test_pickling_polys_fields(): + pass + # NOTE: can't use protocols < 2 because we have to execute __new__ to + # make sure caching of fields works properly. + + # from sympy.polys.fields import FracField + + # field = FracField("x,y,z", ZZ, lex) + + # TODO: AssertionError: assert id(obj) not in self.memo + # for c in (FracField, field): + # check(c, exclude=[0, 1]) + + # TODO: AssertionError: assert id(obj) not in self.memo + # for c in (field.dtype, field.one): + # check(c, exclude=[0, 1]) + +def test_pickling_polys_elements(): + from sympy.polys.domains.pythonrational import PythonRational + #from sympy.polys.domains.pythonfinitefield import PythonFiniteField + #from sympy.polys.domains.mpelements import MPContext + + for c in (PythonRational, PythonRational(1, 7)): + check(c) + + #gf = PythonFiniteField(17) + + # TODO: fix pickling of ModularInteger + # for c in (gf.dtype, gf(5)): + # check(c) + + #mp = MPContext() + + # TODO: fix pickling of RealElement + # for c in (mp.mpf, mp.mpf(1.0)): + # check(c) + + # TODO: fix pickling of ComplexElement + # for c in (mp.mpc, mp.mpc(1.0, -1.5)): + # check(c) + +def test_pickling_polys_domains(): + # from sympy.polys.domains.pythonfinitefield import PythonFiniteField + from sympy.polys.domains.pythonintegerring import PythonIntegerRing + from sympy.polys.domains.pythonrationalfield import PythonRationalField + + # TODO: fix pickling of ModularInteger + # for c in (PythonFiniteField, PythonFiniteField(17)): + # check(c) + + for c in (PythonIntegerRing, PythonIntegerRing()): + check(c, check_attr=False) + + for c in (PythonRationalField, PythonRationalField()): + check(c, check_attr=False) + + if _gmpy is not None: + # from sympy.polys.domains.gmpyfinitefield import GMPYFiniteField + from sympy.polys.domains.gmpyintegerring import GMPYIntegerRing + from sympy.polys.domains.gmpyrationalfield import GMPYRationalField + + # TODO: fix pickling of ModularInteger + # for c in (GMPYFiniteField, GMPYFiniteField(17)): + # check(c) + + for c in (GMPYIntegerRing, GMPYIntegerRing()): + check(c, check_attr=False) + + for c in (GMPYRationalField, GMPYRationalField()): + check(c, check_attr=False) + + #from sympy.polys.domains.realfield import RealField + #from sympy.polys.domains.complexfield import ComplexField + from sympy.polys.domains.algebraicfield import AlgebraicField + #from sympy.polys.domains.polynomialring import PolynomialRing + #from sympy.polys.domains.fractionfield import FractionField + from sympy.polys.domains.expressiondomain import ExpressionDomain + + # TODO: fix pickling of RealElement + # for c in (RealField, RealField(100)): + # check(c) + + # TODO: fix pickling of ComplexElement + # for c in (ComplexField, ComplexField(100)): + # check(c) + + for c in (AlgebraicField, AlgebraicField(QQ, sqrt(3))): + check(c, check_attr=False) + + # TODO: AssertionError + # for c in (PolynomialRing, PolynomialRing(ZZ, "x,y,z")): + # check(c) + + # TODO: AttributeError: 'PolyElement' object has no attribute 'ring' + # for c in (FractionField, FractionField(ZZ, "x,y,z")): + # check(c) + + for c in (ExpressionDomain, ExpressionDomain()): + check(c, check_attr=False) + + +def test_pickling_polys_orderings(): + from sympy.polys.orderings import (LexOrder, GradedLexOrder, + ReversedGradedLexOrder, InverseOrder) + # from sympy.polys.orderings import ProductOrder + + for c in (LexOrder, LexOrder()): + check(c) + + for c in (GradedLexOrder, GradedLexOrder()): + check(c) + + for c in (ReversedGradedLexOrder, ReversedGradedLexOrder()): + check(c) + + # TODO: Argh, Python is so naive. No lambdas nor inner function support in + # pickling module. Maybe someone could figure out what to do with this. + # + # for c in (ProductOrder, ProductOrder((LexOrder(), lambda m: m[:2]), + # (GradedLexOrder(), lambda m: m[2:]))): + # check(c) + + for c in (InverseOrder, InverseOrder(LexOrder())): + check(c) + +def test_pickling_polys_monomials(): + from sympy.polys.monomials import MonomialOps, Monomial + x, y, z = symbols("x,y,z") + + for c in (MonomialOps, MonomialOps(3)): + check(c) + + for c in (Monomial, Monomial((1, 2, 3), (x, y, z))): + check(c) + +def test_pickling_polys_errors(): + from sympy.polys.polyerrors import (HeuristicGCDFailed, + HomomorphismFailed, IsomorphismFailed, ExtraneousFactors, + EvaluationFailed, RefinementFailed, CoercionFailed, NotInvertible, + NotReversible, NotAlgebraic, DomainError, PolynomialError, + UnificationFailed, GeneratorsError, GeneratorsNeeded, + UnivariatePolynomialError, MultivariatePolynomialError, OptionError, + FlagError) + # from sympy.polys.polyerrors import (ExactQuotientFailed, + # OperationNotSupported, ComputationFailed, PolificationFailed) + + # x = Symbol('x') + + # TODO: TypeError: __init__() takes at least 3 arguments (1 given) + # for c in (ExactQuotientFailed, ExactQuotientFailed(x, 3*x, ZZ)): + # check(c) + + # TODO: TypeError: can't pickle instancemethod objects + # for c in (OperationNotSupported, OperationNotSupported(Poly(x), Poly.gcd)): + # check(c) + + for c in (HeuristicGCDFailed, HeuristicGCDFailed()): + check(c) + + for c in (HomomorphismFailed, HomomorphismFailed()): + check(c) + + for c in (IsomorphismFailed, IsomorphismFailed()): + check(c) + + for c in (ExtraneousFactors, ExtraneousFactors()): + check(c) + + for c in (EvaluationFailed, EvaluationFailed()): + check(c) + + for c in (RefinementFailed, RefinementFailed()): + check(c) + + for c in (CoercionFailed, CoercionFailed()): + check(c) + + for c in (NotInvertible, NotInvertible()): + check(c) + + for c in (NotReversible, NotReversible()): + check(c) + + for c in (NotAlgebraic, NotAlgebraic()): + check(c) + + for c in (DomainError, DomainError()): + check(c) + + for c in (PolynomialError, PolynomialError()): + check(c) + + for c in (UnificationFailed, UnificationFailed()): + check(c) + + for c in (GeneratorsError, GeneratorsError()): + check(c) + + for c in (GeneratorsNeeded, GeneratorsNeeded()): + check(c) + + # TODO: PicklingError: Can't pickle at 0x38578c0>: it's not found as __main__. + # for c in (ComputationFailed, ComputationFailed(lambda t: t, 3, None)): + # check(c) + + for c in (UnivariatePolynomialError, UnivariatePolynomialError()): + check(c) + + for c in (MultivariatePolynomialError, MultivariatePolynomialError()): + check(c) + + # TODO: TypeError: __init__() takes at least 3 arguments (1 given) + # for c in (PolificationFailed, PolificationFailed({}, x, x, False)): + # check(c) + + for c in (OptionError, OptionError()): + check(c) + + for c in (FlagError, FlagError()): + check(c) + +#def test_pickling_polys_options(): + #from sympy.polys.polyoptions import Options + + # TODO: fix pickling of `symbols' flag + # for c in (Options, Options((), dict(domain='ZZ', polys=False))): + # check(c) + +# TODO: def test_pickling_polys_rootisolation(): +# RealInterval +# ComplexInterval + +def test_pickling_polys_rootoftools(): + from sympy.polys.rootoftools import CRootOf, RootSum + + x = Symbol('x') + f = x**3 + x + 3 + + for c in (CRootOf, CRootOf(f, 0)): + check(c) + + for c in (RootSum, RootSum(f, exp)): + check(c) + +#================== printing ==================== +from sympy.printing.latex import LatexPrinter +from sympy.printing.mathml import MathMLContentPrinter, MathMLPresentationPrinter +from sympy.printing.pretty.pretty import PrettyPrinter +from sympy.printing.pretty.stringpict import prettyForm, stringPict +from sympy.printing.printer import Printer +from sympy.printing.python import PythonPrinter + + +def test_printing(): + for c in (LatexPrinter, LatexPrinter(), MathMLContentPrinter, + MathMLPresentationPrinter, PrettyPrinter, prettyForm, stringPict, + stringPict("a"), Printer, Printer(), PythonPrinter, + PythonPrinter()): + check(c) + + +@XFAIL +def test_printing1(): + check(MathMLContentPrinter()) + + +@XFAIL +def test_printing2(): + check(MathMLPresentationPrinter()) + + +@XFAIL +def test_printing3(): + check(PrettyPrinter()) + +#================== series ====================== +from sympy.series.limits import Limit +from sympy.series.order import Order + + +def test_series(): + e = Symbol("e") + x = Symbol("x") + for c in (Limit, Limit(e, x, 1), Order, Order(e)): + check(c) + +#================== concrete ================== +from sympy.concrete.products import Product +from sympy.concrete.summations import Sum + + +def test_concrete(): + x = Symbol("x") + for c in (Product, Product(x, (x, 2, 4)), Sum, Sum(x, (x, 2, 4))): + check(c) + +def test_deprecation_warning(): + w = SymPyDeprecationWarning("message", deprecated_since_version='1.0', active_deprecations_target="active-deprecations") + check(w) + +def test_issue_18438(): + assert pickle.loads(pickle.dumps(S.Half)) == S.Half + + +#================= old pickles ================= +def test_unpickle_from_older_versions(): + data = ( + b'\x80\x04\x95^\x00\x00\x00\x00\x00\x00\x00\x8c\x10sympy.core.power' + b'\x94\x8c\x03Pow\x94\x93\x94\x8c\x12sympy.core.numbers\x94\x8c' + b'\x07Integer\x94\x93\x94K\x02\x85\x94R\x94}\x94bh\x03\x8c\x04Half' + b'\x94\x93\x94)R\x94}\x94b\x86\x94R\x94}\x94b.' + ) + assert pickle.loads(data) == sqrt(2) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_source.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_source.py new file mode 100644 index 0000000000000000000000000000000000000000..468185bc579fc325aee21024dfa15ebf14287b5f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_source.py @@ -0,0 +1,11 @@ +from sympy.utilities.source import get_mod_func, get_class + + +def test_get_mod_func(): + assert get_mod_func( + 'sympy.core.basic.Basic') == ('sympy.core.basic', 'Basic') + + +def test_get_class(): + _basic = get_class('sympy.core.basic.Basic') + assert _basic.__name__ == 'Basic' diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_timeutils.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_timeutils.py new file mode 100644 index 0000000000000000000000000000000000000000..14edfd089c7315ee9f39a4298af0289f8919da6b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_timeutils.py @@ -0,0 +1,10 @@ +"""Tests for simple tools for timing functions' execution. """ + +from sympy.utilities.timeutils import timed + +def test_timed(): + result = timed(lambda: 1 + 1, limit=100000) + assert result[0] == 100000 and result[3] == "ns", str(result) + + result = timed("1 + 1", limit=100000) + assert result[0] == 100000 and result[3] == "ns" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_wester.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_wester.py new file mode 100644 index 0000000000000000000000000000000000000000..c5699a4eb0824e507967ecd4ff8f7a5f32cc9a54 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_wester.py @@ -0,0 +1,3104 @@ +""" Tests from Michael Wester's 1999 paper "Review of CAS mathematical +capabilities". + +http://www.math.unm.edu/~wester/cas/book/Wester.pdf +See also http://math.unm.edu/~wester/cas_review.html for detailed output of +each tested system. +""" + +from sympy.assumptions.ask import Q, ask +from sympy.assumptions.refine import refine +from sympy.concrete.products import product +from sympy.core import EulerGamma +from sympy.core.evalf import N +from sympy.core.function import (Derivative, Function, Lambda, Subs, + diff, expand, expand_func) +from sympy.core.mul import Mul +from sympy.core.intfunc import igcd +from sympy.core.numbers import (AlgebraicNumber, E, I, Rational, + nan, oo, pi, zoo) +from sympy.core.relational import Eq, Lt +from sympy.core.singleton import S +from sympy.core.symbol import Dummy, Symbol, symbols +from sympy.functions.combinatorial.factorials import (rf, binomial, + factorial, factorial2) +from sympy.functions.combinatorial.numbers import bernoulli, fibonacci, totient, partition +from sympy.functions.elementary.complexes import (conjugate, im, re, + sign) +from sympy.functions.elementary.exponential import LambertW, exp, log +from sympy.functions.elementary.hyperbolic import (asinh, cosh, sinh, + tanh) +from sympy.functions.elementary.integers import ceiling, floor +from sympy.functions.elementary.miscellaneous import Max, Min, sqrt +from sympy.functions.elementary.piecewise import Piecewise +from sympy.functions.elementary.trigonometric import (acos, acot, asin, + atan, cos, cot, csc, sec, sin, tan) +from sympy.functions.special.bessel import besselj +from sympy.functions.special.delta_functions import DiracDelta +from sympy.functions.special.elliptic_integrals import (elliptic_e, + elliptic_f) +from sympy.functions.special.gamma_functions import gamma, polygamma +from sympy.functions.special.hyper import hyper +from sympy.functions.special.polynomials import (assoc_legendre, + chebyshevt) +from sympy.functions.special.zeta_functions import polylog +from sympy.geometry.util import idiff +from sympy.logic.boolalg import And +from sympy.matrices.dense import hessian, wronskian +from sympy.matrices.expressions.matmul import MatMul +from sympy.ntheory.continued_fraction import ( + continued_fraction_convergents as cf_c, + continued_fraction_iterator as cf_i, continued_fraction_periodic as + cf_p, continued_fraction_reduce as cf_r) +from sympy.ntheory.factor_ import factorint +from sympy.ntheory.generate import primerange +from sympy.polys.domains.integerring import ZZ +from sympy.polys.orthopolys import legendre_poly +from sympy.polys.partfrac import apart +from sympy.polys.polytools import Poly, factor, gcd, resultant +from sympy.series.limits import limit +from sympy.series.order import O +from sympy.series.residues import residue +from sympy.series.series import series +from sympy.sets.fancysets import ImageSet +from sympy.sets.sets import FiniteSet, Intersection, Interval, Union +from sympy.simplify.combsimp import combsimp +from sympy.simplify.hyperexpand import hyperexpand +from sympy.simplify.powsimp import powdenest, powsimp +from sympy.simplify.radsimp import radsimp +from sympy.simplify.simplify import logcombine, simplify +from sympy.simplify.sqrtdenest import sqrtdenest +from sympy.simplify.trigsimp import trigsimp +from sympy.solvers.solvers import solve + +import mpmath +from sympy.functions.combinatorial.numbers import stirling +from sympy.functions.special.delta_functions import Heaviside +from sympy.functions.special.error_functions import Ci, Si, erf +from sympy.functions.special.zeta_functions import zeta +from sympy.testing.pytest import (XFAIL, slow, SKIP, tooslow, raises) +from sympy.utilities.iterables import partitions +from mpmath import mpi, mpc +from sympy.matrices import Matrix, GramSchmidt, eye +from sympy.matrices.expressions.blockmatrix import BlockMatrix, block_collapse +from sympy.matrices.expressions import MatrixSymbol, ZeroMatrix +from sympy.physics.quantum import Commutator +from sympy.polys.rings import PolyRing +from sympy.polys.fields import FracField +from sympy.polys.solvers import solve_lin_sys +from sympy.concrete import Sum +from sympy.concrete.products import Product +from sympy.integrals import integrate +from sympy.integrals.transforms import laplace_transform,\ + inverse_laplace_transform, LaplaceTransform, fourier_transform,\ + mellin_transform, laplace_correspondence, laplace_initial_conds +from sympy.solvers.recurr import rsolve +from sympy.solvers.solveset import solveset, solveset_real, linsolve +from sympy.solvers.ode import dsolve +from sympy.core.relational import Equality +from itertools import islice, takewhile +from sympy.series.formal import fps +from sympy.series.fourier import fourier_series +from sympy.calculus.util import minimum + + +EmptySet = S.EmptySet +R = Rational +x, y, z = symbols('x y z') +i, j, k, l, m, n = symbols('i j k l m n', integer=True) +f = Function('f') +g = Function('g') + +# A. Boolean Logic and Quantifier Elimination +# Not implemented. + +# B. Set Theory + + +def test_B1(): + assert (FiniteSet(i, j, j, k, k, k) | FiniteSet(l, k, j) | + FiniteSet(j, m, j)) == FiniteSet(i, j, k, l, m) + + +def test_B2(): + assert (FiniteSet(i, j, j, k, k, k) & FiniteSet(l, k, j) & + FiniteSet(j, m, j)) == Intersection({j, m}, {i, j, k}, {j, k, l}) + # Previous output below. Not sure why that should be the expected output. + # There should probably be a way to rewrite Intersections that way but I + # don't see why an Intersection should evaluate like that: + # + # == Union({j}, Intersection({m}, Union({j, k}, Intersection({i}, {l})))) + + +def test_B3(): + assert (FiniteSet(i, j, k, l, m) - FiniteSet(j) == + FiniteSet(i, k, l, m)) + + +def test_B4(): + assert (FiniteSet(*(FiniteSet(i, j)*FiniteSet(k, l))) == + FiniteSet((i, k), (i, l), (j, k), (j, l))) + + +# C. Numbers + + +def test_C1(): + assert (factorial(50) == + 30414093201713378043612608166064768844377641568960512000000000000) + + +def test_C2(): + assert (factorint(factorial(50)) == {2: 47, 3: 22, 5: 12, 7: 8, + 11: 4, 13: 3, 17: 2, 19: 2, 23: 2, 29: 1, 31: 1, 37: 1, + 41: 1, 43: 1, 47: 1}) + + +def test_C3(): + assert (factorial2(10), factorial2(9)) == (3840, 945) + + +# Base conversions; not really implemented by SymPy +# Whatever. Take credit! +def test_C4(): + assert 0xABC == 2748 + + +def test_C5(): + assert 123 == int('234', 7) + + +def test_C6(): + assert int('677', 8) == int('1BF', 16) == 447 + + +def test_C7(): + assert log(32768, 8) == 5 + + +def test_C8(): + # Modular multiplicative inverse. Would be nice if divmod could do this. + assert ZZ.invert(5, 7) == 3 + assert ZZ.invert(5, 6) == 5 + + +def test_C9(): + assert igcd(igcd(1776, 1554), 5698) == 74 + + +def test_C10(): + x = 0 + for n in range(2, 11): + x += R(1, n) + assert x == R(4861, 2520) + + +def test_C11(): + assert R(1, 7) == S('0.[142857]') + + +def test_C12(): + assert R(7, 11) * R(22, 7) == 2 + + +def test_C13(): + test = R(10, 7) * (1 + R(29, 1000)) ** R(1, 3) + good = 3 ** R(1, 3) + assert test == good + + +def test_C14(): + assert sqrtdenest(sqrt(2*sqrt(3) + 4)) == 1 + sqrt(3) + + +def test_C15(): + test = sqrtdenest(sqrt(14 + 3*sqrt(3 + 2*sqrt(5 - 12*sqrt(3 - 2*sqrt(2)))))) + good = sqrt(2) + 3 + assert test == good + + +def test_C16(): + test = sqrtdenest(sqrt(10 + 2*sqrt(6) + 2*sqrt(10) + 2*sqrt(15))) + good = sqrt(2) + sqrt(3) + sqrt(5) + assert test == good + + +def test_C17(): + test = radsimp((sqrt(3) + sqrt(2)) / (sqrt(3) - sqrt(2))) + good = 5 + 2*sqrt(6) + assert test == good + + +def test_C18(): + assert simplify((sqrt(-2 + sqrt(-5)) * sqrt(-2 - sqrt(-5))).expand(complex=True)) == 3 + + +@XFAIL +def test_C19(): + assert radsimp(simplify((90 + 34*sqrt(7)) ** R(1, 3))) == 3 + sqrt(7) + + +def test_C20(): + inside = (135 + 78*sqrt(3)) + test = AlgebraicNumber((inside**R(2, 3) + 3) * sqrt(3) / inside**R(1, 3)) + assert simplify(test) == AlgebraicNumber(12) + + +def test_C21(): + assert simplify(AlgebraicNumber((41 + 29*sqrt(2)) ** R(1, 5))) == \ + AlgebraicNumber(1 + sqrt(2)) + + +@XFAIL +def test_C22(): + test = simplify(((6 - 4*sqrt(2))*log(3 - 2*sqrt(2)) + (3 - 2*sqrt(2))*log(17 + - 12*sqrt(2)) + 32 - 24*sqrt(2)) / (48*sqrt(2) - 72)) + good = sqrt(2)/3 - log(sqrt(2) - 1)/3 + assert test == good + + +def test_C23(): + assert 2 * oo - 3 is oo + + +@XFAIL +def test_C24(): + raise NotImplementedError("2**aleph_null == aleph_1") + +# D. Numerical Analysis + + +def test_D1(): + assert 0.0 / sqrt(2) == 0 + + +def test_D2(): + assert str(exp(-1000000).evalf()) == '3.29683147808856e-434295' + + +def test_D3(): + assert exp(pi*sqrt(163)).evalf(50).num.ae(262537412640768744) + + +def test_D4(): + assert floor(R(-5, 3)) == -2 + assert ceiling(R(-5, 3)) == -1 + + +@XFAIL +def test_D5(): + raise NotImplementedError("cubic_spline([1, 2, 4, 5], [1, 4, 2, 3], x)(3) == 27/8") + + +@XFAIL +def test_D6(): + raise NotImplementedError("translate sum(a[i]*x**i, (i,1,n)) to FORTRAN") + + +@XFAIL +def test_D7(): + raise NotImplementedError("translate sum(a[i]*x**i, (i,1,n)) to C") + + +@XFAIL +def test_D8(): + # One way is to cheat by converting the sum to a string, + # and replacing the '[' and ']' with ''. + # E.g., horner(S(str(_).replace('[','').replace(']',''))) + raise NotImplementedError("apply Horner's rule to sum(a[i]*x**i, (i,1,5))") + + +@XFAIL +def test_D9(): + raise NotImplementedError("translate D8 to FORTRAN") + + +@XFAIL +def test_D10(): + raise NotImplementedError("translate D8 to C") + + +@XFAIL +def test_D11(): + #Is there a way to use count_ops? + raise NotImplementedError("flops(sum(product(f[i][k], (i,1,k)), (k,1,n)))") + + +@XFAIL +def test_D12(): + assert (mpi(-4, 2) * x + mpi(1, 3)) ** 2 == mpi(-8, 16)*x**2 + mpi(-24, 12)*x + mpi(1, 9) + + +@XFAIL +def test_D13(): + raise NotImplementedError("discretize a PDE: diff(f(x,t),t) == diff(diff(f(x,t),x),x)") + +# E. Statistics +# See scipy; all of this is numerical. + +# F. Combinatorial Theory. + + +def test_F1(): + assert rf(x, 3) == x*(1 + x)*(2 + x) + + +def test_F2(): + assert expand_func(binomial(n, 3)) == n*(n - 1)*(n - 2)/6 + + +@XFAIL +def test_F3(): + assert combsimp(2**n * factorial(n) * factorial2(2*n - 1)) == factorial(2*n) + + +@XFAIL +def test_F4(): + assert combsimp(2**n * factorial(n) * product(2*k - 1, (k, 1, n))) == factorial(2*n) + + +@XFAIL +def test_F5(): + assert gamma(n + R(1, 2)) / sqrt(pi) / factorial(n) == factorial(2*n)/2**(2*n)/factorial(n)**2 + + +def test_F6(): + partTest = [p.copy() for p in partitions(4)] + partDesired = [{4: 1}, {1: 1, 3: 1}, {2: 2}, {1: 2, 2:1}, {1: 4}] + assert partTest == partDesired + + +def test_F7(): + assert partition(4) == 5 + + +def test_F8(): + assert stirling(5, 2, signed=True) == -50 # if signed, then kind=1 + + +def test_F9(): + assert totient(1776) == 576 + +# G. Number Theory + + +def test_G1(): + assert list(primerange(999983, 1000004)) == [999983, 1000003] + + +@XFAIL +def test_G2(): + raise NotImplementedError("find the primitive root of 191 == 19") + + +@XFAIL +def test_G3(): + raise NotImplementedError("(a+b)**p mod p == a**p + b**p mod p; p prime") + +# ... G14 Modular equations are not implemented. + +def test_G15(): + assert Rational(sqrt(3).evalf()).limit_denominator(15) == R(26, 15) + assert list(takewhile(lambda x: x.q <= 15, cf_c(cf_i(sqrt(3)))))[-1] == \ + R(26, 15) + + +def test_G16(): + assert list(islice(cf_i(pi),10)) == [3, 7, 15, 1, 292, 1, 1, 1, 2, 1] + + +def test_G17(): + assert cf_p(0, 1, 23) == [4, [1, 3, 1, 8]] + + +def test_G18(): + assert cf_p(1, 2, 5) == [[1]] + assert cf_r([[1]]).expand() == S.Half + sqrt(5)/2 + + +@XFAIL +def test_G19(): + s = symbols('s', integer=True, positive=True) + it = cf_i((exp(1/s) - 1)/(exp(1/s) + 1)) + assert list(islice(it, 5)) == [0, 2*s, 6*s, 10*s, 14*s] + + +def test_G20(): + s = symbols('s', integer=True, positive=True) + # Wester erroneously has this as -s + sqrt(s**2 + 1) + assert cf_r([[2*s]]) == s + sqrt(s**2 + 1) + + +@XFAIL +def test_G20b(): + s = symbols('s', integer=True, positive=True) + assert cf_p(s, 1, s**2 + 1) == [[2*s]] + + +# H. Algebra + + +def test_H1(): + assert simplify(2*2**n) == simplify(2**(n + 1)) + assert powdenest(2*2**n) == simplify(2**(n + 1)) + + +def test_H2(): + assert powsimp(4 * 2**n) == 2**(n + 2) + + +def test_H3(): + assert (-1)**(n*(n + 1)) == 1 + + +def test_H4(): + expr = factor(6*x - 10) + assert type(expr) is Mul + assert expr.args[0] == 2 + assert expr.args[1] == 3*x - 5 + +p1 = 64*x**34 - 21*x**47 - 126*x**8 - 46*x**5 - 16*x**60 - 81 +p2 = 72*x**60 - 25*x**25 - 19*x**23 - 22*x**39 - 83*x**52 + 54*x**10 + 81 +q = 34*x**19 - 25*x**16 + 70*x**7 + 20*x**3 - 91*x - 86 + + +def test_H5(): + assert gcd(p1, p2, x) == 1 + + +def test_H6(): + assert gcd(expand(p1 * q), expand(p2 * q)) == q + + +def test_H7(): + p1 = 24*x*y**19*z**8 - 47*x**17*y**5*z**8 + 6*x**15*y**9*z**2 - 3*x**22 + 5 + p2 = 34*x**5*y**8*z**13 + 20*x**7*y**7*z**7 + 12*x**9*y**16*z**4 + 80*y**14*z + assert gcd(p1, p2, x, y, z) == 1 + + +def test_H8(): + p1 = 24*x*y**19*z**8 - 47*x**17*y**5*z**8 + 6*x**15*y**9*z**2 - 3*x**22 + 5 + p2 = 34*x**5*y**8*z**13 + 20*x**7*y**7*z**7 + 12*x**9*y**16*z**4 + 80*y**14*z + q = 11*x**12*y**7*z**13 - 23*x**2*y**8*z**10 + 47*x**17*y**5*z**8 + assert gcd(p1 * q, p2 * q, x, y, z) == q + + +def test_H9(): + x = Symbol('x', zero=False) + p1 = 2*x**(n + 4) - x**(n + 2) + p2 = 4*x**(n + 1) + 3*x**n + assert gcd(p1, p2) == x**n + + +def test_H10(): + p1 = 3*x**4 + 3*x**3 + x**2 - x - 2 + p2 = x**3 - 3*x**2 + x + 5 + assert resultant(p1, p2, x) == 0 + + +def test_H11(): + assert resultant(p1 * q, p2 * q, x) == 0 + + +def test_H12(): + num = x**2 - 4 + den = x**2 + 4*x + 4 + assert simplify(num/den) == (x - 2)/(x + 2) + + +@XFAIL +def test_H13(): + assert simplify((exp(x) - 1) / (exp(x/2) + 1)) == exp(x/2) - 1 + + +def test_H14(): + p = (x + 1) ** 20 + ep = expand(p) + assert ep == (1 + 20*x + 190*x**2 + 1140*x**3 + 4845*x**4 + 15504*x**5 + + 38760*x**6 + 77520*x**7 + 125970*x**8 + 167960*x**9 + 184756*x**10 + + 167960*x**11 + 125970*x**12 + 77520*x**13 + 38760*x**14 + 15504*x**15 + + 4845*x**16 + 1140*x**17 + 190*x**18 + 20*x**19 + x**20) + dep = diff(ep, x) + assert dep == (20 + 380*x + 3420*x**2 + 19380*x**3 + 77520*x**4 + + 232560*x**5 + 542640*x**6 + 1007760*x**7 + 1511640*x**8 + 1847560*x**9 + + 1847560*x**10 + 1511640*x**11 + 1007760*x**12 + 542640*x**13 + + 232560*x**14 + 77520*x**15 + 19380*x**16 + 3420*x**17 + 380*x**18 + + 20*x**19) + assert factor(dep) == 20*(1 + x)**19 + + +def test_H15(): + assert simplify(Mul(*[x - r for r in solveset(x**3 + x**2 - 7)])) == x**3 + x**2 - 7 + + +def test_H16(): + assert factor(x**100 - 1) == ((x - 1)*(x + 1)*(x**2 + 1)*(x**4 - x**3 + + x**2 - x + 1)*(x**4 + x**3 + x**2 + x + 1)*(x**8 - x**6 + x**4 + - x**2 + 1)*(x**20 - x**15 + x**10 - x**5 + 1)*(x**20 + x**15 + x**10 + + x**5 + 1)*(x**40 - x**30 + x**20 - x**10 + 1)) + + +def test_H17(): + assert simplify(factor(expand(p1 * p2)) - p1*p2) == 0 + + +@XFAIL +def test_H18(): + # Factor over complex rationals. + test = factor(4*x**4 + 8*x**3 + 77*x**2 + 18*x + 153) + good = (2*x + 3*I)*(2*x - 3*I)*(x + 1 - 4*I)*(x + 1 + 4*I) + assert test == good + + +def test_H19(): + a = symbols('a') + # The idea is to let a**2 == 2, then solve 1/(a-1). Answer is a+1") + assert Poly(a - 1).invert(Poly(a**2 - 2)) == a + 1 + + +@XFAIL +def test_H20(): + raise NotImplementedError("let a**2==2; (x**3 + (a-2)*x**2 - " + + "(2*a+3)*x - 3*a) / (x**2-2) = (x**2 - 2*x - 3) / (x-a)") + + +@XFAIL +def test_H21(): + raise NotImplementedError("evaluate (b+c)**4 assuming b**3==2, c**2==3. \ + Answer is 2*b + 8*c + 18*b**2 + 12*b*c + 9") + + +def test_H22(): + assert factor(x**4 - 3*x**2 + 1, modulus=5) == (x - 2)**2 * (x + 2)**2 + + +def test_H23(): + f = x**11 + x + 1 + g = (x**2 + x + 1) * (x**9 - x**8 + x**6 - x**5 + x**3 - x**2 + 1) + assert factor(f, modulus=65537) == g + + +def test_H24(): + phi = AlgebraicNumber(S.GoldenRatio.expand(func=True), alias='phi') + assert factor(x**4 - 3*x**2 + 1, extension=phi) == \ + (x - phi)*(x + 1 - phi)*(x - 1 + phi)*(x + phi) + + +def test_H25(): + e = (x - 2*y**2 + 3*z**3) ** 20 + assert factor(expand(e)) == e + + +def test_H26(): + g = expand((sin(x) - 2*cos(y)**2 + 3*tan(z)**3)**20) + assert factor(g, expand=False) == (-sin(x) + 2*cos(y)**2 - 3*tan(z)**3)**20 + + +def test_H27(): + f = 24*x*y**19*z**8 - 47*x**17*y**5*z**8 + 6*x**15*y**9*z**2 - 3*x**22 + 5 + g = 34*x**5*y**8*z**13 + 20*x**7*y**7*z**7 + 12*x**9*y**16*z**4 + 80*y**14*z + h = -2*z*y**7 \ + *(6*x**9*y**9*z**3 + 10*x**7*z**6 + 17*y*x**5*z**12 + 40*y**7) \ + *(3*x**22 + 47*x**17*y**5*z**8 - 6*x**15*y**9*z**2 - 24*x*y**19*z**8 - 5) + assert factor(expand(f*g)) == h + + +@XFAIL +def test_H28(): + raise NotImplementedError("expand ((1 - c**2)**5 * (1 - s**2)**5 * " + + "(c**2 + s**2)**10) with c**2 + s**2 = 1. Answer is c**10*s**10.") + + +@XFAIL +def test_H29(): + assert factor(4*x**2 - 21*x*y + 20*y**2, modulus=3) == (x + y)*(x - y) + + +def test_H30(): + test = factor(x**3 + y**3, extension=sqrt(-3)) + answer = (x + y)*(x + y*(-R(1, 2) - sqrt(3)/2*I))*(x + y*(-R(1, 2) + sqrt(3)/2*I)) + assert answer == test + + +def test_H31(): + f = (x**2 + 2*x + 3)/(x**3 + 4*x**2 + 5*x + 2) + g = 2 / (x + 1)**2 - 2 / (x + 1) + 3 / (x + 2) + assert apart(f) == g + + +@XFAIL +def test_H32(): # issue 6558 + raise NotImplementedError("[A*B*C - (A*B*C)**(-1)]*A*C*B (product \ + of a non-commuting product and its inverse)") + + +def test_H33(): + A, B, C = symbols('A, B, C', commutative=False) + assert (Commutator(A, Commutator(B, C)) + + Commutator(B, Commutator(C, A)) + + Commutator(C, Commutator(A, B))).doit().expand() == 0 + + +# I. Trigonometry + +def test_I1(): + assert tan(pi*R(7, 10)) == -sqrt(1 + 2/sqrt(5)) + + +@XFAIL +def test_I2(): + assert sqrt((1 + cos(6))/2) == -cos(3) + + +def test_I3(): + assert cos(n*pi) + sin((4*n - 1)*pi/2) == (-1)**n - 1 + + +def test_I4(): + assert refine(cos(pi*cos(n*pi)) + sin(pi/2*cos(n*pi)), Q.integer(n)) == (-1)**n - 1 + + +@XFAIL +def test_I5(): + assert sin((n**5/5 + n**4/2 + n**3/3 - n/30) * pi) == 0 + + +@XFAIL +def test_I6(): + raise NotImplementedError("assuming -3*pi pi**E) + + +@XFAIL +def test_N2(): + x = symbols('x', real=True) + assert ask(x**4 - x + 1 > 0) is True + assert ask(x**4 - x + 1 > 1) is False + + +@XFAIL +def test_N3(): + x = symbols('x', real=True) + assert ask(And(Lt(-1, x), Lt(x, 1)), abs(x) < 1 ) + +@XFAIL +def test_N4(): + x, y = symbols('x y', real=True) + assert ask(2*x**2 > 2*y**2, (x > y) & (y > 0)) is True + + +@XFAIL +def test_N5(): + x, y, k = symbols('x y k', real=True) + assert ask(k*x**2 > k*y**2, (x > y) & (y > 0) & (k > 0)) is True + + +@slow +@XFAIL +def test_N6(): + x, y, k, n = symbols('x y k n', real=True) + assert ask(k*x**n > k*y**n, (x > y) & (y > 0) & (k > 0) & (n > 0)) is True + + +@XFAIL +def test_N7(): + x, y = symbols('x y', real=True) + assert ask(y > 0, (x > 1) & (y >= x - 1)) is True + + +@XFAIL +@slow +def test_N8(): + x, y, z = symbols('x y z', real=True) + assert ask(Eq(x, y) & Eq(y, z), + (x >= y) & (y >= z) & (z >= x)) + + +def test_N9(): + x = Symbol('x') + assert solveset(abs(x - 1) > 2, domain=S.Reals) == Union(Interval(-oo, -1, False, True), + Interval(3, oo, True)) + + +def test_N10(): + x = Symbol('x') + p = (x - 1)*(x - 2)*(x - 3)*(x - 4)*(x - 5) + assert solveset(expand(p) < 0, domain=S.Reals) == Union(Interval(-oo, 1, True, True), + Interval(2, 3, True, True), + Interval(4, 5, True, True)) + + +def test_N11(): + x = Symbol('x') + assert solveset(6/(x - 3) <= 3, domain=S.Reals) == Union(Interval(-oo, 3, True, True), Interval(5, oo)) + + +def test_N12(): + x = Symbol('x') + assert solveset(sqrt(x) < 2, domain=S.Reals) == Interval(0, 4, False, True) + + +def test_N13(): + x = Symbol('x') + assert solveset(sin(x) < 2, domain=S.Reals) == S.Reals + + +@XFAIL +def test_N14(): + x = Symbol('x') + # Gives 'Union(Interval(Integer(0), Mul(Rational(1, 2), pi), false, true), + # Interval(Mul(Rational(1, 2), pi), Mul(Integer(2), pi), true, false))' + # which is not the correct answer, but the provided also seems wrong. + assert solveset(sin(x) < 1, x, domain=S.Reals) == Union(Interval(-oo, pi/2, True, True), + Interval(pi/2, oo, True, True)) + + +def test_N15(): + r, t = symbols('r t') + # raises NotImplementedError: only univariate inequalities are supported + solveset(abs(2*r*(cos(t) - 1) + 1) <= 1, r, S.Reals) + + +def test_N16(): + r, t = symbols('r t') + solveset((r**2)*((cos(t) - 4)**2)*sin(t)**2 < 9, r, S.Reals) + + +@XFAIL +def test_N17(): + # currently only univariate inequalities are supported + assert solveset((x + y > 0, x - y < 0), (x, y)) == (abs(x) < y) + + +def test_O1(): + M = Matrix((1 + I, -2, 3*I)) + assert sqrt(expand(M.dot(M.H))) == sqrt(15) + + +def test_O2(): + assert Matrix((2, 2, -3)).cross(Matrix((1, 3, 1))) == Matrix([[11], + [-5], + [4]]) + +# The vector module has no way of representing vectors symbolically (without +# respect to a basis) +@XFAIL +def test_O3(): + # assert (va ^ vb) | (vc ^ vd) == -(va | vc)*(vb | vd) + (va | vd)*(vb | vc) + raise NotImplementedError("""The vector module has no way of representing + vectors symbolically (without respect to a basis)""") + +def test_O4(): + from sympy.vector import CoordSys3D, Del + N = CoordSys3D("N") + delop = Del() + i, j, k = N.base_vectors() + x, y, z = N.base_scalars() + F = i*(x*y*z) + j*((x*y*z)**2) + k*((y**2)*(z**3)) + assert delop.cross(F).doit() == (-2*x**2*y**2*z + 2*y*z**3)*i + x*y*j + (2*x*y**2*z**2 - x*z)*k + +@XFAIL +def test_O5(): + #assert grad|(f^g)-g|(grad^f)+f|(grad^g) == 0 + raise NotImplementedError("""The vector module has no way of representing + vectors symbolically (without respect to a basis)""") + +#testO8-O9 MISSING!! + + +def test_O10(): + L = [Matrix([2, 3, 5]), Matrix([3, 6, 2]), Matrix([8, 3, 6])] + assert GramSchmidt(L) == [Matrix([ + [2], + [3], + [5]]), + Matrix([ + [R(23, 19)], + [R(63, 19)], + [R(-47, 19)]]), + Matrix([ + [R(1692, 353)], + [R(-1551, 706)], + [R(-423, 706)]])] + + +def test_P1(): + assert Matrix(3, 3, lambda i, j: j - i).diagonal(-1) == Matrix( + 1, 2, [-1, -1]) + + +def test_P2(): + M = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + M.row_del(1) + M.col_del(2) + assert M == Matrix([[1, 2], + [7, 8]]) + + +def test_P3(): + A = Matrix([ + [11, 12, 13, 14], + [21, 22, 23, 24], + [31, 32, 33, 34], + [41, 42, 43, 44]]) + + A11 = A[0:3, 1:4] + A12 = A[(0, 1, 3), (2, 0, 3)] + A21 = A + A221 = -A[0:2, 2:4] + A222 = -A[(3, 0), (2, 1)] + A22 = BlockMatrix([[A221, A222]]).T + rows = [[-A11, A12], [A21, A22]] + raises(ValueError, lambda: BlockMatrix(rows)) + B = Matrix(rows) + assert B == Matrix([ + [-12, -13, -14, 13, 11, 14], + [-22, -23, -24, 23, 21, 24], + [-32, -33, -34, 43, 41, 44], + [11, 12, 13, 14, -13, -23], + [21, 22, 23, 24, -14, -24], + [31, 32, 33, 34, -43, -13], + [41, 42, 43, 44, -42, -12]]) + + +@XFAIL +def test_P4(): + raise NotImplementedError("Block matrix diagonalization not supported") + + +def test_P5(): + M = Matrix([[7, 11], + [3, 8]]) + assert M % 2 == Matrix([[1, 1], + [1, 0]]) + + +def test_P6(): + M = Matrix([[cos(x), sin(x)], + [-sin(x), cos(x)]]) + assert M.diff(x, 2) == Matrix([[-cos(x), -sin(x)], + [sin(x), -cos(x)]]) + + +def test_P7(): + M = Matrix([[x, y]])*( + z*Matrix([[1, 3, 5], + [2, 4, 6]]) + Matrix([[7, -9, 11], + [-8, 10, -12]])) + assert M == Matrix([[x*(z + 7) + y*(2*z - 8), x*(3*z - 9) + y*(4*z + 10), + x*(5*z + 11) + y*(6*z - 12)]]) + + +def test_P8(): + M = Matrix([[1, -2*I], + [-3*I, 4]]) + assert M.norm(ord=S.Infinity) == 7 + + +def test_P9(): + a, b, c = symbols('a b c', nonzero=True) + M = Matrix([[a/(b*c), 1/c, 1/b], + [1/c, b/(a*c), 1/a], + [1/b, 1/a, c/(a*b)]]) + assert factor(M.norm('fro')) == (a**2 + b**2 + c**2)/(abs(a)*abs(b)*abs(c)) + + +@XFAIL +def test_P10(): + M = Matrix([[1, 2 + 3*I], + [f(4 - 5*I), 6]]) + # conjugate(f(4 - 5*i)) is not simplified to f(4+5*I) + assert M.H == Matrix([[1, f(4 + 5*I)], + [2 + 3*I, 6]]) + + +@XFAIL +def test_P11(): + # raises NotImplementedError("Matrix([[x,y],[1,x*y]]).inv() + # not simplifying to extract common factor") + assert Matrix([[x, y], + [1, x*y]]).inv() == (1/(x**2 - 1))*Matrix([[x, -1], + [-1/y, x/y]]) + + +def test_P11_workaround(): + # This test was changed to inverse method ADJ because it depended on the + # specific form of inverse returned from the 'GE' method which has changed. + M = Matrix([[x, y], [1, x*y]]).inv('ADJ') + c = gcd(tuple(M)) + assert MatMul(c, M/c, evaluate=False) == MatMul(c, Matrix([ + [x*y, -y], + [ -1, x]]), evaluate=False) + + +def test_P12(): + A11 = MatrixSymbol('A11', n, n) + A12 = MatrixSymbol('A12', n, n) + A22 = MatrixSymbol('A22', n, n) + B = BlockMatrix([[A11, A12], + [ZeroMatrix(n, n), A22]]) + assert block_collapse(B.I) == BlockMatrix([[A11.I, (-1)*A11.I*A12*A22.I], + [ZeroMatrix(n, n), A22.I]]) + + +def test_P13(): + M = Matrix([[1, x - 2, x - 3], + [x - 1, x**2 - 3*x + 6, x**2 - 3*x - 2], + [x - 2, x**2 - 8, 2*(x**2) - 12*x + 14]]) + L, U, _ = M.LUdecomposition() + assert simplify(L) == Matrix([[1, 0, 0], + [x - 1, 1, 0], + [x - 2, x - 3, 1]]) + assert simplify(U) == Matrix([[1, x - 2, x - 3], + [0, 4, x - 5], + [0, 0, x - 7]]) + + +def test_P14(): + M = Matrix([[1, 2, 3, 1, 3], + [3, 2, 1, 1, 7], + [0, 2, 4, 1, 1], + [1, 1, 1, 1, 4]]) + R, _ = M.rref() + assert R == Matrix([[1, 0, -1, 0, 2], + [0, 1, 2, 0, -1], + [0, 0, 0, 1, 3], + [0, 0, 0, 0, 0]]) + + +def test_P15(): + M = Matrix([[-1, 3, 7, -5], + [4, -2, 1, 3], + [2, 4, 15, -7]]) + assert M.rank() == 2 + + +def test_P16(): + M = Matrix([[2*sqrt(2), 8], + [6*sqrt(6), 24*sqrt(3)]]) + assert M.rank() == 1 + + +def test_P17(): + t = symbols('t', real=True) + M=Matrix([ + [sin(2*t), cos(2*t)], + [2*(1 - (cos(t)**2))*cos(t), (1 - 2*(sin(t)**2))*sin(t)]]) + assert M.rank() == 1 + + +def test_P18(): + M = Matrix([[1, 0, -2, 0], + [-2, 1, 0, 3], + [-1, 2, -6, 6]]) + assert M.nullspace() == [Matrix([[2], + [4], + [1], + [0]]), + Matrix([[0], + [-3], + [0], + [1]])] + + +def test_P19(): + w = symbols('w') + M = Matrix([[1, 1, 1, 1], + [w, x, y, z], + [w**2, x**2, y**2, z**2], + [w**3, x**3, y**3, z**3]]) + assert M.det() == (w**3*x**2*y - w**3*x**2*z - w**3*x*y**2 + w**3*x*z**2 + + w**3*y**2*z - w**3*y*z**2 - w**2*x**3*y + w**2*x**3*z + + w**2*x*y**3 - w**2*x*z**3 - w**2*y**3*z + w**2*y*z**3 + + w*x**3*y**2 - w*x**3*z**2 - w*x**2*y**3 + w*x**2*z**3 + + w*y**3*z**2 - w*y**2*z**3 - x**3*y**2*z + x**3*y*z**2 + + x**2*y**3*z - x**2*y*z**3 - x*y**3*z**2 + x*y**2*z**3 + ) + + +@XFAIL +def test_P20(): + raise NotImplementedError("Matrix minimal polynomial not supported") + + +def test_P21(): + M = Matrix([[5, -3, -7], + [-2, 1, 2], + [2, -3, -4]]) + assert M.charpoly(x).as_expr() == x**3 - 2*x**2 - 5*x + 6 + + +def test_P22(): + d = 100 + M = (2 - x)*eye(d) + assert M.eigenvals() == {-x + 2: d} + + +def test_P23(): + M = Matrix([ + [2, 1, 0, 0, 0], + [1, 2, 1, 0, 0], + [0, 1, 2, 1, 0], + [0, 0, 1, 2, 1], + [0, 0, 0, 1, 2]]) + assert M.eigenvals() == { + S('1'): 1, + S('2'): 1, + S('3'): 1, + S('sqrt(3) + 2'): 1, + S('-sqrt(3) + 2'): 1} + + +def test_P24(): + M = Matrix([[611, 196, -192, 407, -8, -52, -49, 29], + [196, 899, 113, -192, -71, -43, -8, -44], + [-192, 113, 899, 196, 61, 49, 8, 52], + [ 407, -192, 196, 611, 8, 44, 59, -23], + [ -8, -71, 61, 8, 411, -599, 208, 208], + [ -52, -43, 49, 44, -599, 411, 208, 208], + [ -49, -8, 8, 59, 208, 208, 99, -911], + [ 29, -44, 52, -23, 208, 208, -911, 99]]) + assert M.eigenvals() == { + S('0'): 1, + S('10*sqrt(10405)'): 1, + S('100*sqrt(26) + 510'): 1, + S('1000'): 2, + S('-100*sqrt(26) + 510'): 1, + S('-10*sqrt(10405)'): 1, + S('1020'): 1} + + +def test_P25(): + MF = N(Matrix([[ 611, 196, -192, 407, -8, -52, -49, 29], + [ 196, 899, 113, -192, -71, -43, -8, -44], + [-192, 113, 899, 196, 61, 49, 8, 52], + [ 407, -192, 196, 611, 8, 44, 59, -23], + [ -8, -71, 61, 8, 411, -599, 208, 208], + [ -52, -43, 49, 44, -599, 411, 208, 208], + [ -49, -8, 8, 59, 208, 208, 99, -911], + [ 29, -44, 52, -23, 208, 208, -911, 99]])) + + ev_1 = sorted(MF.eigenvals(multiple=True)) + ev_2 = sorted( + [-1020.0490184299969, 0.0, 0.09804864072151699, 1000.0, 1000.0, + 1019.9019513592784, 1020.0, 1020.0490184299969]) + + for x, y in zip(ev_1, ev_2): + assert abs(x - y) < 1e-12 + + +def test_P26(): + a0, a1, a2, a3, a4 = symbols('a0 a1 a2 a3 a4') + M = Matrix([[-a4, -a3, -a2, -a1, -a0, 0, 0, 0, 0], + [ 1, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 1, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 1, 0, 0, 0, 0, 0, 0], + [ 0, 0, 0, 1, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, -1, -1, 0, 0], + [ 0, 0, 0, 0, 0, 1, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 1, -1, -1], + [ 0, 0, 0, 0, 0, 0, 0, 1, 0]]) + assert M.eigenvals(error_when_incomplete=False) == { + S('-1/2 - sqrt(3)*I/2'): 2, + S('-1/2 + sqrt(3)*I/2'): 2} + + +def test_P27(): + a = symbols('a') + M = Matrix([[a, 0, 0, 0, 0], + [0, 0, 0, 0, 1], + [0, 0, a, 0, 0], + [0, 0, 0, a, 0], + [0, -2, 0, 0, 2]]) + + assert M.eigenvects() == [ + (a, 3, [ + Matrix([1, 0, 0, 0, 0]), + Matrix([0, 0, 1, 0, 0]), + Matrix([0, 0, 0, 1, 0]) + ]), + (1 - I, 1, [ + Matrix([0, (1 + I)/2, 0, 0, 1]) + ]), + (1 + I, 1, [ + Matrix([0, (1 - I)/2, 0, 0, 1]) + ]), + ] + + +@XFAIL +def test_P28(): + raise NotImplementedError("Generalized eigenvectors not supported \ +https://github.com/sympy/sympy/issues/5293") + + +@XFAIL +def test_P29(): + raise NotImplementedError("Generalized eigenvectors not supported \ +https://github.com/sympy/sympy/issues/5293") + + +def test_P30(): + M = Matrix([[1, 0, 0, 1, -1], + [0, 1, -2, 3, -3], + [0, 0, -1, 2, -2], + [1, -1, 1, 0, 1], + [1, -1, 1, -1, 2]]) + _, J = M.jordan_form() + assert J == Matrix([[-1, 0, 0, 0, 0], + [0, 1, 1, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 1, 1], + [0, 0, 0, 0, 1]]) + + +@XFAIL +def test_P31(): + raise NotImplementedError("Smith normal form not implemented") + + +def test_P32(): + M = Matrix([[1, -2], + [2, 1]]) + assert exp(M).rewrite(cos).simplify() == Matrix([[E*cos(2), -E*sin(2)], + [E*sin(2), E*cos(2)]]) + + +def test_P33(): + w, t = symbols('w t') + M = Matrix([[0, 1, 0, 0], + [0, 0, 0, 2*w], + [0, 0, 0, 1], + [0, -2*w, 3*w**2, 0]]) + assert exp(M*t).rewrite(cos).expand() == Matrix([ + [1, -3*t + 4*sin(t*w)/w, 6*t*w - 6*sin(t*w), -2*cos(t*w)/w + 2/w], + [0, 4*cos(t*w) - 3, -6*w*cos(t*w) + 6*w, 2*sin(t*w)], + [0, 2*cos(t*w)/w - 2/w, -3*cos(t*w) + 4, sin(t*w)/w], + [0, -2*sin(t*w), 3*w*sin(t*w), cos(t*w)]]) + + +@XFAIL +def test_P34(): + a, b, c = symbols('a b c', real=True) + M = Matrix([[a, 1, 0, 0, 0, 0], + [0, a, 0, 0, 0, 0], + [0, 0, b, 0, 0, 0], + [0, 0, 0, c, 1, 0], + [0, 0, 0, 0, c, 1], + [0, 0, 0, 0, 0, c]]) + # raises exception, sin(M) not supported. exp(M*I) also not supported + # https://github.com/sympy/sympy/issues/6218 + assert sin(M) == Matrix([[sin(a), cos(a), 0, 0, 0, 0], + [0, sin(a), 0, 0, 0, 0], + [0, 0, sin(b), 0, 0, 0], + [0, 0, 0, sin(c), cos(c), -sin(c)/2], + [0, 0, 0, 0, sin(c), cos(c)], + [0, 0, 0, 0, 0, sin(c)]]) + + +@XFAIL +def test_P35(): + M = pi/2*Matrix([[2, 1, 1], + [2, 3, 2], + [1, 1, 2]]) + # raises exception, sin(M) not supported. exp(M*I) also not supported + # https://github.com/sympy/sympy/issues/6218 + assert sin(M) == eye(3) + + +@XFAIL +def test_P36(): + M = Matrix([[10, 7], + [7, 17]]) + assert sqrt(M) == Matrix([[3, 1], + [1, 4]]) + + +def test_P37(): + M = Matrix([[1, 1, 0], + [0, 1, 0], + [0, 0, 1]]) + assert M**S.Half == Matrix([[1, R(1, 2), 0], + [0, 1, 0], + [0, 0, 1]]) + + +@XFAIL +def test_P38(): + M=Matrix([[0, 1, 0], + [0, 0, 0], + [0, 0, 0]]) + + with raises(AssertionError): + # raises ValueError: Matrix det == 0; not invertible + M**S.Half + # if it doesn't raise then this assertion will be + # raised and the test will be flagged as not XFAILing + assert None + +@XFAIL +def test_P39(): + """ + M=Matrix([ + [1, 1], + [2, 2], + [3, 3]]) + M.SVD() + """ + raise NotImplementedError("Singular value decomposition not implemented") + + +def test_P40(): + r, t = symbols('r t', real=True) + M = Matrix([r*cos(t), r*sin(t)]) + assert M.jacobian(Matrix([r, t])) == Matrix([[cos(t), -r*sin(t)], + [sin(t), r*cos(t)]]) + + +def test_P41(): + r, t = symbols('r t', real=True) + assert hessian(r**2*sin(t),(r,t)) == Matrix([[ 2*sin(t), 2*r*cos(t)], + [2*r*cos(t), -r**2*sin(t)]]) + + +def test_P42(): + assert wronskian([cos(x), sin(x)], x).simplify() == 1 + + +def test_P43(): + def __my_jacobian(M, Y): + return Matrix([M.diff(v).T for v in Y]).T + r, t = symbols('r t', real=True) + M = Matrix([r*cos(t), r*sin(t)]) + assert __my_jacobian(M,[r,t]) == Matrix([[cos(t), -r*sin(t)], + [sin(t), r*cos(t)]]) + + +def test_P44(): + def __my_hessian(f, Y): + V = Matrix([diff(f, v) for v in Y]) + return Matrix([V.T.diff(v) for v in Y]) + r, t = symbols('r t', real=True) + assert __my_hessian(r**2*sin(t), (r, t)) == Matrix([ + [ 2*sin(t), 2*r*cos(t)], + [2*r*cos(t), -r**2*sin(t)]]) + + +def test_P45(): + def __my_wronskian(Y, v): + M = Matrix([Matrix(Y).T.diff(x, n) for n in range(0, len(Y))]) + return M.det() + assert __my_wronskian([cos(x), sin(x)], x).simplify() == 1 + +# Q1-Q6 Tensor tests missing + + +@XFAIL +def test_R1(): + i, j, n = symbols('i j n', integer=True, positive=True) + xn = MatrixSymbol('xn', n, 1) + Sm = Sum((xn[i, 0] - Sum(xn[j, 0], (j, 0, n - 1))/n)**2, (i, 0, n - 1)) + # sum does not calculate + # Unknown result + Sm.doit() + raise NotImplementedError('Unknown result') + +@XFAIL +def test_R2(): + m, b = symbols('m b') + i, n = symbols('i n', integer=True, positive=True) + xn = MatrixSymbol('xn', n, 1) + yn = MatrixSymbol('yn', n, 1) + f = Sum((yn[i, 0] - m*xn[i, 0] - b)**2, (i, 0, n - 1)) + f1 = diff(f, m) + f2 = diff(f, b) + # raises TypeError: solveset() takes at most 2 arguments (3 given) + solveset((f1, f2), (m, b), domain=S.Reals) + + +@XFAIL +def test_R3(): + n, k = symbols('n k', integer=True, positive=True) + sk = ((-1)**k) * (binomial(2*n, k))**2 + Sm = Sum(sk, (k, 1, oo)) + T = Sm.doit() + T2 = T.combsimp() + # returns -((-1)**n*factorial(2*n) + # - (factorial(n))**2)*exp_polar(-I*pi)/(factorial(n))**2 + assert T2 == (-1)**n*binomial(2*n, n) + + +@XFAIL +def test_R4(): +# Macsyma indefinite sum test case: +#(c15) /* Check whether the full Gosper algorithm is implemented +# => 1/2^(n + 1) binomial(n, k - 1) */ +#closedform(indefsum(binomial(n, k)/2^n - binomial(n + 1, k)/2^(n + 1), k)); +#Time= 2690 msecs +# (- n + k - 1) binomial(n + 1, k) +#(d15) - -------------------------------- +# n +# 2 2 (n + 1) +# +#(c16) factcomb(makefact(%)); +#Time= 220 msecs +# n! +#(d16) ---------------- +# n +# 2 k! 2 (n - k)! +# Might be possible after fixing https://github.com/sympy/sympy/pull/1879 + raise NotImplementedError("Indefinite sum not supported") + + +@XFAIL +def test_R5(): + a, b, c, n, k = symbols('a b c n k', integer=True, positive=True) + sk = ((-1)**k)*(binomial(a + b, a + k) + *binomial(b + c, b + k)*binomial(c + a, c + k)) + Sm = Sum(sk, (k, 1, oo)) + T = Sm.doit() # hypergeometric series not calculated + assert T == factorial(a+b+c)/(factorial(a)*factorial(b)*factorial(c)) + + +def test_R6(): + n, k = symbols('n k', integer=True, positive=True) + gn = MatrixSymbol('gn', n + 2, 1) + Sm = Sum(gn[k, 0] - gn[k - 1, 0], (k, 1, n + 1)) + assert Sm.doit() == -gn[0, 0] + gn[n + 1, 0] + + +def test_R7(): + n, k = symbols('n k', integer=True, positive=True) + T = Sum(k**3,(k,1,n)).doit() + assert T.factor() == n**2*(n + 1)**2/4 + +@XFAIL +def test_R8(): + n, k = symbols('n k', integer=True, positive=True) + Sm = Sum(k**2*binomial(n, k), (k, 1, n)) + T = Sm.doit() #returns Piecewise function + assert T.combsimp() == n*(n + 1)*2**(n - 2) + + +def test_R9(): + n, k = symbols('n k', integer=True, positive=True) + Sm = Sum(binomial(n, k - 1)/k, (k, 1, n + 1)) + assert Sm.doit().simplify() == (2**(n + 1) - 1)/(n + 1) + + +@XFAIL +def test_R10(): + n, m, r, k = symbols('n m r k', integer=True, positive=True) + Sm = Sum(binomial(n, k)*binomial(m, r - k), (k, 0, r)) + T = Sm.doit() + T2 = T.combsimp().rewrite(factorial) + assert T2 == factorial(m + n)/(factorial(r)*factorial(m + n - r)) + assert T2 == binomial(m + n, r).rewrite(factorial) + # rewrite(binomial) is not working. + # https://github.com/sympy/sympy/issues/7135 + T3 = T2.rewrite(binomial) + assert T3 == binomial(m + n, r) + + +@XFAIL +def test_R11(): + n, k = symbols('n k', integer=True, positive=True) + sk = binomial(n, k)*fibonacci(k) + Sm = Sum(sk, (k, 0, n)) + T = Sm.doit() + # Fibonacci simplification not implemented + # https://github.com/sympy/sympy/issues/7134 + assert T == fibonacci(2*n) + + +@XFAIL +def test_R12(): + n, k = symbols('n k', integer=True, positive=True) + Sm = Sum(fibonacci(k)**2, (k, 0, n)) + T = Sm.doit() + assert T == fibonacci(n)*fibonacci(n + 1) + + +@XFAIL +def test_R13(): + n, k = symbols('n k', integer=True, positive=True) + Sm = Sum(sin(k*x), (k, 1, n)) + T = Sm.doit() # Sum is not calculated + assert T.simplify() == cot(x/2)/2 - cos(x*(2*n + 1)/2)/(2*sin(x/2)) + + +@XFAIL +def test_R14(): + n, k = symbols('n k', integer=True, positive=True) + Sm = Sum(sin((2*k - 1)*x), (k, 1, n)) + T = Sm.doit() # Sum is not calculated + assert T.simplify() == sin(n*x)**2/sin(x) + + +@XFAIL +def test_R15(): + n, k = symbols('n k', integer=True, positive=True) + Sm = Sum(binomial(n - k, k), (k, 0, floor(n/2))) + T = Sm.doit() # Sum is not calculated + assert T.simplify() == fibonacci(n + 1) + + +def test_R16(): + k = symbols('k', integer=True, positive=True) + Sm = Sum(1/k**2 + 1/k**3, (k, 1, oo)) + assert Sm.doit() == zeta(3) + pi**2/6 + + +def test_R17(): + k = symbols('k', integer=True, positive=True) + assert abs(float(Sum(1/k**2 + 1/k**3, (k, 1, oo))) + - 2.8469909700078206) < 1e-15 + + +def test_R18(): + k = symbols('k', integer=True, positive=True) + Sm = Sum(1/(2**k*k**2), (k, 1, oo)) + T = Sm.doit() + assert T.simplify() == -log(2)**2/2 + pi**2/12 + + +@slow +@XFAIL +def test_R19(): + k = symbols('k', integer=True, positive=True) + Sm = Sum(1/((3*k + 1)*(3*k + 2)*(3*k + 3)), (k, 0, oo)) + T = Sm.doit() + # assert fails, T not simplified + assert T.simplify() == -log(3)/4 + sqrt(3)*pi/12 + + +@XFAIL +def test_R20(): + n, k = symbols('n k', integer=True, positive=True) + Sm = Sum(binomial(n, 4*k), (k, 0, oo)) + T = Sm.doit() + # assert fails, T not simplified + assert T.simplify() == 2**(n/2)*cos(pi*n/4)/2 + 2**(n - 1)/2 + + +@XFAIL +def test_R21(): + k = symbols('k', integer=True, positive=True) + Sm = Sum(1/(sqrt(k*(k + 1)) * (sqrt(k) + sqrt(k + 1))), (k, 1, oo)) + T = Sm.doit() # Sum not calculated + assert T.simplify() == 1 + + +# test_R22 answer not available in Wester samples +# Sum(Sum(binomial(n, k)*binomial(n - k, n - 2*k)*x**n*y**(n - 2*k), +# (k, 0, floor(n/2))), (n, 0, oo)) with abs(x*y)<1? + + +@XFAIL +def test_R23(): + n, k = symbols('n k', integer=True, positive=True) + Sm = Sum(Sum((factorial(n)/(factorial(k)**2*factorial(n - 2*k)))* + (x/y)**k*(x*y)**(n - k), (n, 2*k, oo)), (k, 0, oo)) + # Missing how to express constraint abs(x*y)<1? + T = Sm.doit() # Sum not calculated + assert T == -1/sqrt(x**2*y**2 - 4*x**2 - 2*x*y + 1) + + +def test_R24(): + m, k = symbols('m k', integer=True, positive=True) + Sm = Sum(Product(k/(2*k - 1), (k, 1, m)), (m, 2, oo)) + assert Sm.doit() == pi/2 + + +def test_S1(): + k = symbols('k', integer=True, positive=True) + Pr = Product(gamma(k/3), (k, 1, 8)) + assert Pr.doit().simplify() == 640*sqrt(3)*pi**3/6561 + + +def test_S2(): + n, k = symbols('n k', integer=True, positive=True) + assert Product(k, (k, 1, n)).doit() == factorial(n) + + +def test_S3(): + n, k = symbols('n k', integer=True, positive=True) + assert Product(x**k, (k, 1, n)).doit().simplify() == x**(n*(n + 1)/2) + + +def test_S4(): + n, k = symbols('n k', integer=True, positive=True) + assert Product(1 + 1/k, (k, 1, n -1)).doit().simplify() == n + + +def test_S5(): + n, k = symbols('n k', integer=True, positive=True) + assert (Product((2*k - 1)/(2*k), (k, 1, n)).doit().gammasimp() == + gamma(n + S.Half)/(sqrt(pi)*gamma(n + 1))) + + +@XFAIL +def test_S6(): + n, k = symbols('n k', integer=True, positive=True) + # Product does not evaluate + assert (Product(x**2 -2*x*cos(k*pi/n) + 1, (k, 1, n - 1)).doit().simplify() + == (x**(2*n) - 1)/(x**2 - 1)) + + +@XFAIL +def test_S7(): + k = symbols('k', integer=True, positive=True) + Pr = Product((k**3 - 1)/(k**3 + 1), (k, 2, oo)) + T = Pr.doit() # Product does not evaluate + assert T.simplify() == R(2, 3) + + +@XFAIL +def test_S8(): + k = symbols('k', integer=True, positive=True) + Pr = Product(1 - 1/(2*k)**2, (k, 1, oo)) + T = Pr.doit() + # Product does not evaluate + assert T.simplify() == 2/pi + + +@XFAIL +def test_S9(): + k = symbols('k', integer=True, positive=True) + Pr = Product(1 + (-1)**(k + 1)/(2*k - 1), (k, 1, oo)) + T = Pr.doit() + # Product produces 0 + # https://github.com/sympy/sympy/issues/7133 + assert T.simplify() == sqrt(2) + + +@XFAIL +def test_S10(): + k = symbols('k', integer=True, positive=True) + Pr = Product((k*(k + 1) + 1 + I)/(k*(k + 1) + 1 - I), (k, 0, oo)) + T = Pr.doit() + # Product does not evaluate + assert T.simplify() == -1 + + +def test_T1(): + assert limit((1 + 1/n)**n, n, oo) == E + assert limit((1 - cos(x))/x**2, x, 0) == S.Half + + +def test_T2(): + assert limit((3**x + 5**x)**(1/x), x, oo) == 5 + + +def test_T3(): + assert limit(log(x)/(log(x) + sin(x)), x, oo) == 1 + + +def test_T4(): + assert limit((exp(x*exp(-x)/(exp(-x) + exp(-2*x**2/(x + 1)))) + - exp(x))/x, x, oo) == -exp(2) + + +def test_T5(): + assert limit(x*log(x)*log(x*exp(x) - x**2)**2/log(log(x**2 + + 2*exp(exp(3*x**3*log(x))))), x, oo) == R(1, 3) + + +def test_T6(): + assert limit(1/n * factorial(n)**(1/n), n, oo) == exp(-1) + + +def test_T7(): + limit(1/n * gamma(n + 1)**(1/n), n, oo) + + +def test_T8(): + a, z = symbols('a z', positive=True) + assert limit(gamma(z + a)/gamma(z)*exp(-a*log(z)), z, oo) == 1 + + +@XFAIL +def test_T9(): + z, k = symbols('z k', positive=True) + # raises NotImplementedError: + # Don't know how to calculate the mrv of '(1, k)' + assert limit(hyper((1, k), (1,), z/k), k, oo) == exp(z) + + +@XFAIL +def test_T10(): + # No longer raises PoleError, but should return euler-mascheroni constant + assert limit(zeta(x) - 1/(x - 1), x, 1) == integrate(-1/x + 1/floor(x), (x, 1, oo)) + +@XFAIL +def test_T11(): + n, k = symbols('n k', integer=True, positive=True) + # evaluates to 0 + assert limit(n**x/(x*product((1 + x/k), (k, 1, n))), n, oo) == gamma(x) + + +def test_T12(): + x, t = symbols('x t', real=True) + # Does not evaluate the limit but returns an expression with erf + assert limit(x * integrate(exp(-t**2), (t, 0, x))/(1 - exp(-x**2)), + x, 0) == 1 + + +def test_T13(): + x = symbols('x', real=True) + assert [limit(x/abs(x), x, 0, dir='-'), + limit(x/abs(x), x, 0, dir='+')] == [-1, 1] + + +def test_T14(): + x = symbols('x', real=True) + assert limit(atan(-log(x)), x, 0, dir='+') == pi/2 + + +def test_U1(): + x = symbols('x', real=True) + assert diff(abs(x), x) == sign(x) + + +def test_U2(): + f = Lambda(x, Piecewise((-x, x < 0), (x, x >= 0))) + assert diff(f(x), x) == Piecewise((-1, x < 0), (1, x >= 0)) + + +def test_U3(): + f = Lambda(x, Piecewise((x**2 - 1, x == 1), (x**3, x != 1))) + f1 = Lambda(x, diff(f(x), x)) + assert f1(x) == 3*x**2 + assert f1(1) == 3 + + +@XFAIL +def test_U4(): + n = symbols('n', integer=True, positive=True) + x = symbols('x', real=True) + d = diff(x**n, x, n) + assert d.rewrite(factorial) == factorial(n) + + +def test_U5(): + # issue 6681 + t = symbols('t') + ans = ( + Derivative(f(g(t)), g(t))*Derivative(g(t), (t, 2)) + + Derivative(f(g(t)), (g(t), 2))*Derivative(g(t), t)**2) + assert f(g(t)).diff(t, 2) == ans + assert ans.doit() == ans + + +def test_U6(): + h = Function('h') + T = integrate(f(y), (y, h(x), g(x))) + assert T.diff(x) == ( + f(g(x))*Derivative(g(x), x) - f(h(x))*Derivative(h(x), x)) + + +@XFAIL +def test_U7(): + p, t = symbols('p t', real=True) + # Exact differential => d(V(P, T)) => dV/dP DP + dV/dT DT + # raises ValueError: Since there is more than one variable in the + # expression, the variable(s) of differentiation must be supplied to + # differentiate f(p,t) + diff(f(p, t)) + + +def test_U8(): + x, y = symbols('x y', real=True) + eq = cos(x*y) + x + # If SymPy had implicit_diff() function this hack could be avoided + # TODO: Replace solve with solveset, current test fails for solveset + assert idiff(y - eq, y, x) == (-y*sin(x*y) + 1)/(x*sin(x*y) + 1) + + +def test_U9(): + # Wester sample case for Maple: + # O29 := diff(f(x, y), x) + diff(f(x, y), y); + # /d \ /d \ + # |-- f(x, y)| + |-- f(x, y)| + # \dx / \dy / + # + # O30 := factor(subs(f(x, y) = g(x^2 + y^2), %)); + # 2 2 + # 2 D(g)(x + y ) (x + y) + x, y = symbols('x y', real=True) + su = diff(f(x, y), x) + diff(f(x, y), y) + s2 = su.subs(f(x, y), g(x**2 + y**2)) + s3 = s2.doit().factor() + # Subs not performed, s3 = 2*(x + y)*Subs(Derivative( + # g(_xi_1), _xi_1), _xi_1, x**2 + y**2) + # Derivative(g(x*2 + y**2), x**2 + y**2) is not valid in SymPy, + # and probably will remain that way. You can take derivatives with respect + # to other expressions only if they are atomic, like a symbol or a + # function. + # D operator should be added to SymPy + # See https://github.com/sympy/sympy/issues/4719. + assert s3 == (x + y)*Subs(Derivative(g(x), x), x, x**2 + y**2)*2 + + +def test_U10(): + # see issue 2519: + assert residue((z**3 + 5)/((z**4 - 1)*(z + 1)), z, -1) == R(-9, 4) + +@XFAIL +def test_U11(): + # assert (2*dx + dz) ^ (3*dx + dy + dz) ^ (dx + dy + 4*dz) == 8*dx ^ dy ^dz + raise NotImplementedError + + +@XFAIL +def test_U12(): + # Wester sample case: + # (c41) /* d(3 x^5 dy /\ dz + 5 x y^2 dz /\ dx + 8 z dx /\ dy) + # => (15 x^4 + 10 x y + 8) dx /\ dy /\ dz */ + # factor(ext_diff(3*x^5 * dy ~ dz + 5*x*y^2 * dz ~ dx + 8*z * dx ~ dy)); + # 4 + # (d41) (10 x y + 15 x + 8) dx dy dz + raise NotImplementedError( + "External diff of differential form not supported") + + +def test_U13(): + assert minimum(x**4 - x + 1, x) == -3*2**R(1,3)/8 + 1 + + +@XFAIL +def test_U14(): + #f = 1/(x**2 + y**2 + 1) + #assert [minimize(f), maximize(f)] == [0,1] + raise NotImplementedError("minimize(), maximize() not supported") + + +@XFAIL +def test_U15(): + raise NotImplementedError("minimize() not supported and also solve does \ +not support multivariate inequalities") + + +@XFAIL +def test_U16(): + raise NotImplementedError("minimize() not supported in SymPy and also \ +solve does not support multivariate inequalities") + + +@XFAIL +def test_U17(): + raise NotImplementedError("Linear programming, symbolic simplex not \ +supported in SymPy") + + +def test_V1(): + x = symbols('x', real=True) + assert integrate(abs(x), x) == Piecewise((-x**2/2, x <= 0), (x**2/2, True)) + + +def test_V2(): + assert integrate(Piecewise((-x, x < 0), (x, x >= 0)), x + ) == Piecewise((-x**2/2, x < 0), (x**2/2, True)) + + +def test_V3(): + assert integrate(1/(x**3 + 2),x).diff().simplify() == 1/(x**3 + 2) + + +def test_V4(): + assert integrate(2**x/sqrt(1 + 4**x), x) == asinh(2**x)/log(2) + + +@XFAIL +def test_V5(): + # Returns (-45*x**2 + 80*x - 41)/(5*sqrt(2*x - 1)*(4*x**2 - 4*x + 1)) + assert (integrate((3*x - 5)**2/(2*x - 1)**R(7, 2), x).simplify() == + (-41 + 80*x - 45*x**2)/(5*(2*x - 1)**R(5, 2))) + + +@XFAIL +def test_V6(): + # returns RootSum(40*_z**2 - 1, Lambda(_i, _i*log(-4*_i + exp(-m*x))))/m + assert (integrate(1/(2*exp(m*x) - 5*exp(-m*x)), x) == sqrt(10)*( + log(2*exp(m*x) - sqrt(10)) - log(2*exp(m*x) + sqrt(10)))/(20*m)) + + +def test_V7(): + r1 = integrate(sinh(x)**4/cosh(x)**2) + assert r1.simplify() == x*R(-3, 2) + sinh(x)**3/(2*cosh(x)) + 3*tanh(x)/2 + + +@XFAIL +def test_V8_V9(): +#Macsyma test case: +#(c27) /* This example involves several symbolic parameters +# => 1/sqrt(b^2 - a^2) log([sqrt(b^2 - a^2) tan(x/2) + a + b]/ +# [sqrt(b^2 - a^2) tan(x/2) - a - b]) (a^2 < b^2) +# [Gradshteyn and Ryzhik 2.553(3)] */ +#assume(b^2 > a^2)$ +#(c28) integrate(1/(a + b*cos(x)), x); +#(c29) trigsimp(ratsimp(diff(%, x))); +# 1 +#(d29) ------------ +# b cos(x) + a + raise NotImplementedError( + "Integrate with assumption not supported") + + +def test_V10(): + assert integrate(1/(3 + 3*cos(x) + 4*sin(x)), x) == log(4*tan(x/2) + 3)/4 + + +def test_V11(): + r1 = integrate(1/(4 + 3*cos(x) + 4*sin(x)), x) + r2 = factor(r1) + assert (logcombine(r2, force=True) == + log(((tan(x/2) + 1)/(tan(x/2) + 7))**R(1, 3))) + + +def test_V12(): + r1 = integrate(1/(5 + 3*cos(x) + 4*sin(x)), x) + assert r1 == -1/(tan(x/2) + 2) + + +@XFAIL +def test_V13(): + r1 = integrate(1/(6 + 3*cos(x) + 4*sin(x)), x) + # expression not simplified, returns: -sqrt(11)*I*log(tan(x/2) + 4/3 + # - sqrt(11)*I/3)/11 + sqrt(11)*I*log(tan(x/2) + 4/3 + sqrt(11)*I/3)/11 + assert r1.simplify() == 2*sqrt(11)*atan(sqrt(11)*(3*tan(x/2) + 4)/11)/11 + + +@slow +@XFAIL +def test_V14(): + r1 = integrate(log(abs(x**2 - y**2)), x) + # Piecewise result does not simplify to the desired result. + assert (r1.simplify() == x*log(abs(x**2 - y**2)) + + y*log(x + y) - y*log(x - y) - 2*x) + + +def test_V15(): + r1 = integrate(x*acot(x/y), x) + assert simplify(r1 - (x*y + (x**2 + y**2)*acot(x/y))/2) == 0 + + +@XFAIL +def test_V16(): + # Integral not calculated + assert integrate(cos(5*x)*Ci(2*x), x) == Ci(2*x)*sin(5*x)/5 - (Si(3*x) + Si(7*x))/10 + +@XFAIL +def test_V17(): + r1 = integrate((diff(f(x), x)*g(x) + - f(x)*diff(g(x), x))/(f(x)**2 - g(x)**2), x) + # integral not calculated + assert simplify(r1 - (f(x) - g(x))/(f(x) + g(x))/2) == 0 + + +@XFAIL +def test_W1(): + # The function has a pole at y. + # The integral has a Cauchy principal value of zero but SymPy returns -I*pi + # https://github.com/sympy/sympy/issues/7159 + assert integrate(1/(x - y), (x, y - 1, y + 1)) == 0 + + +@XFAIL +def test_W2(): + # The function has a pole at y. + # The integral is divergent but SymPy returns -2 + # https://github.com/sympy/sympy/issues/7160 + # Test case in Macsyma: + # (c6) errcatch(integrate(1/(x - a)^2, x, a - 1, a + 1)); + # Integral is divergent + assert integrate(1/(x - y)**2, (x, y - 1, y + 1)) is zoo + + +@XFAIL +@slow +def test_W3(): + # integral is not calculated + # https://github.com/sympy/sympy/issues/7161 + assert integrate(sqrt(x + 1/x - 2), (x, 0, 1)) == R(4, 3) + + +@XFAIL +@slow +def test_W4(): + # integral is not calculated + assert integrate(sqrt(x + 1/x - 2), (x, 1, 2)) == -2*sqrt(2)/3 + R(4, 3) + + +@XFAIL +@slow +def test_W5(): + # integral is not calculated + assert integrate(sqrt(x + 1/x - 2), (x, 0, 2)) == -2*sqrt(2)/3 + R(8, 3) + + +@XFAIL +@slow +def test_W6(): + # integral is not calculated + assert integrate(sqrt(2 - 2*cos(2*x))/2, (x, pi*R(-3, 4), -pi/4)) == sqrt(2) + + +def test_W7(): + a = symbols('a', positive=True) + r1 = integrate(cos(x)/(x**2 + a**2), (x, -oo, oo)) + assert r1.simplify() == pi*exp(-a)/a + + +@XFAIL +def test_W8(): + # Test case in Mathematica: + # In[19]:= Integrate[t^(a - 1)/(1 + t), {t, 0, Infinity}, + # Assumptions -> 0 < a < 1] + # Out[19]= Pi Csc[a Pi] + raise NotImplementedError( + "Integrate with assumption 0 < a < 1 not supported") + + +@XFAIL +@slow +def test_W9(): + # Integrand with a residue at infinity => -2 pi [sin(pi/5) + sin(2pi/5)] + # (principal value) [Levinson and Redheffer, p. 234] *) + r1 = integrate(5*x**3/(1 + x + x**2 + x**3 + x**4), (x, -oo, oo)) + r2 = r1.doit() + assert r2 == -2*pi*(sqrt(-sqrt(5)/8 + 5/8) + sqrt(sqrt(5)/8 + 5/8)) + + +@XFAIL +def test_W10(): + # integrate(1/[1 + x + x^2 + ... + x^(2 n)], x = -infinity..infinity) = + # 2 pi/(2 n + 1) [1 + cos(pi/[2 n + 1])] csc(2 pi/[2 n + 1]) + # [Levinson and Redheffer, p. 255] => 2 pi/5 [1 + cos(pi/5)] csc(2 pi/5) */ + r1 = integrate(x/(1 + x + x**2 + x**4), (x, -oo, oo)) + r2 = r1.doit() + assert r2 == 2*pi*(sqrt(5)/4 + 5/4)*csc(pi*R(2, 5))/5 + + +@XFAIL +def test_W11(): + # integral not calculated + assert (integrate(sqrt(1 - x**2)/(1 + x**2), (x, -1, 1)) == + pi*(-1 + sqrt(2))) + + +def test_W12(): + p = symbols('p', positive=True) + q = symbols('q', real=True) + r1 = integrate(x*exp(-p*x**2 + 2*q*x), (x, -oo, oo)) + assert r1.simplify() == sqrt(pi)*q*exp(q**2/p)/p**R(3, 2) + + +@XFAIL +def test_W13(): + # Integral not calculated. Expected result is 2*(Euler_mascheroni_constant) + r1 = integrate(1/log(x) + 1/(1 - x) - log(log(1/x)), (x, 0, 1)) + assert r1 == 2*EulerGamma + + +def test_W14(): + assert integrate(sin(x)/x*exp(2*I*x), (x, -oo, oo)) == 0 + + +@XFAIL +def test_W15(): + # integral not calculated + assert integrate(log(gamma(x))*cos(6*pi*x), (x, 0, 1)) == R(1, 12) + + +def test_W16(): + assert integrate((1 + x)**3*legendre_poly(1, x)*legendre_poly(2, x), + (x, -1, 1)) == R(36, 35) + + +def test_W17(): + a, b = symbols('a b', positive=True) + assert integrate(exp(-a*x)*besselj(0, b*x), + (x, 0, oo)) == 1/(b*sqrt(a**2/b**2 + 1)) + + +def test_W18(): + assert integrate((besselj(1, x)/x)**2, (x, 0, oo)) == 4/(3*pi) + + +@XFAIL +def test_W19(): + # Integral not calculated + # Expected result is (cos 7 - 1)/7 [Gradshteyn and Ryzhik 6.782(3)] + assert integrate(Ci(x)*besselj(0, 2*sqrt(7*x)), (x, 0, oo)) == (cos(7) - 1)/7 + + +@XFAIL +def test_W20(): + # integral not calculated + assert (integrate(x**2*polylog(3, 1/(x + 1)), (x, 0, 1)) == + -pi**2/36 - R(17, 108) + zeta(3)/4 + + (-pi**2/2 - 4*log(2) + log(2)**2 + 35/3)*log(2)/9) + + +def test_W21(): + assert abs(N(integrate(x**2*polylog(3, 1/(x + 1)), (x, 0, 1))) + - 0.210882859565594) < 1e-15 + + +def test_W22(): + t, u = symbols('t u', real=True) + s = Lambda(x, Piecewise((1, And(x >= 1, x <= 2)), (0, True))) + assert integrate(s(t)*cos(t), (t, 0, u)) == Piecewise( + (0, u < 0), + (-sin(Min(1, u)) + sin(Min(2, u)), True)) + + +@slow +def test_W23(): + a, b = symbols('a b', positive=True) + r1 = integrate(integrate(x/(x**2 + y**2), (x, a, b)), (y, -oo, oo)) + assert r1.collect(pi).cancel() == -pi*a + pi*b + + +def test_W23b(): + # like W23 but limits are reversed + a, b = symbols('a b', positive=True) + r2 = integrate(integrate(x/(x**2 + y**2), (y, -oo, oo)), (x, a, b)) + assert r2.collect(pi) == pi*(-a + b) + + +@XFAIL +@tooslow +def test_W24(): + # Not that slow, but does not fully evaluate so simplify is slow. + # Maybe also require doit() + x, y = symbols('x y', real=True) + r1 = integrate(integrate(sqrt(x**2 + y**2), (x, 0, 1)), (y, 0, 1)) + assert (r1 - (sqrt(2) + asinh(1))/3).simplify() == 0 + + +@XFAIL +@tooslow +def test_W25(): + a, x, y = symbols('a x y', real=True) + i1 = integrate( + sin(a)*sin(y)/sqrt(1 - sin(a)**2*sin(x)**2*sin(y)**2), + (x, 0, pi/2)) + i2 = integrate(i1, (y, 0, pi/2)) + assert (i2 - pi*a/2).simplify() == 0 + + +def test_W26(): + x, y = symbols('x y', real=True) + assert integrate(integrate(abs(y - x**2), (y, 0, 2)), + (x, -1, 1)) == R(46, 15) + + +def test_W27(): + a, b, c = symbols('a b c') + assert integrate(integrate(integrate(1, (z, 0, c*(1 - x/a - y/b))), + (y, 0, b*(1 - x/a))), + (x, 0, a)) == a*b*c/6 + + +def test_X1(): + v, c = symbols('v c', real=True) + assert (series(1/sqrt(1 - (v/c)**2), v, x0=0, n=8) == + 5*v**6/(16*c**6) + 3*v**4/(8*c**4) + v**2/(2*c**2) + 1 + O(v**8)) + + +def test_X2(): + v, c = symbols('v c', real=True) + s1 = series(1/sqrt(1 - (v/c)**2), v, x0=0, n=8) + assert (1/s1**2).series(v, x0=0, n=8) == -v**2/c**2 + 1 + O(v**8) + + +def test_X3(): + s1 = (sin(x).series()/cos(x).series()).series() + s2 = tan(x).series() + assert s2 == x + x**3/3 + 2*x**5/15 + O(x**6) + assert s1 == s2 + + +def test_X4(): + s1 = log(sin(x)/x).series() + assert s1 == -x**2/6 - x**4/180 + O(x**6) + assert log(series(sin(x)/x)).series() == s1 + + +@XFAIL +def test_X5(): + # test case in Mathematica syntax: + # In[21]:= (* => [a f'(a d) + g(b d) + integrate(h(c y), y = 0..d)] + # + [a^2 f''(a d) + b g'(b d) + h(c d)] (x - d) *) + # In[22]:= D[f[a*x], x] + g[b*x] + Integrate[h[c*y], {y, 0, x}] + # Out[22]= g[b x] + Integrate[h[c y], {y, 0, x}] + a f'[a x] + # In[23]:= Series[%, {x, d, 1}] + # Out[23]= (g[b d] + Integrate[h[c y], {y, 0, d}] + a f'[a d]) + + # 2 2 + # (h[c d] + b g'[b d] + a f''[a d]) (-d + x) + O[-d + x] + h = Function('h') + a, b, c, d = symbols('a b c d', real=True) + # series() raises NotImplementedError: + # The _eval_nseries method should be added to to give terms up to O(x**n) at x=0 + series(diff(f(a*x), x) + g(b*x) + integrate(h(c*y), (y, 0, x)), + x, x0=d, n=2) + # assert missing, until exception is removed + + +def test_X6(): + # Taylor series of nonscalar objects (noncommutative multiplication) + # expected result => (B A - A B) t^2/2 + O(t^3) [Stanly Steinberg] + a, b = symbols('a b', commutative=False, scalar=False) + assert (series(exp((a + b)*x) - exp(a*x) * exp(b*x), x, x0=0, n=3) == + x**2*(-a*b/2 + b*a/2) + O(x**3)) + + +def test_X7(): + # => sum( Bernoulli[k]/k! x^(k - 2), k = 1..infinity ) + # = 1/x^2 - 1/(2 x) + 1/12 - x^2/720 + x^4/30240 + O(x^6) + # [Levinson and Redheffer, p. 173] + assert (series(1/(x*(exp(x) - 1)), x, 0, 7) == x**(-2) - 1/(2*x) + + R(1, 12) - x**2/720 + x**4/30240 - x**6/1209600 + O(x**7)) + + +def test_X8(): + # Puiseux series (terms with fractional degree): + # => 1/sqrt(x - 3/2 pi) + (x - 3/2 pi)^(3/2) / 12 + O([x - 3/2 pi]^(7/2)) + + # see issue 7167: + x = symbols('x', real=True) + assert (series(sqrt(sec(x)), x, x0=pi*3/2, n=4) == + 1/sqrt(x - pi*R(3, 2)) + (x - pi*R(3, 2))**R(3, 2)/12 + + (x - pi*R(3, 2))**R(7, 2)/160 + O((x - pi*R(3, 2))**4, (x, pi*R(3, 2)))) + + +def test_X9(): + assert (series(x**x, x, x0=0, n=4) == 1 + x*log(x) + x**2*log(x)**2/2 + + x**3*log(x)**3/6 + O(x**4*log(x)**4)) + + +def test_X10(): + z, w = symbols('z w') + assert (series(log(sinh(z)) + log(cosh(z + w)), z, x0=0, n=2) == + log(cosh(w)) + log(z) + z*sinh(w)/cosh(w) + O(z**2)) + + +def test_X11(): + z, w = symbols('z w') + assert (series(log(sinh(z) * cosh(z + w)), z, x0=0, n=2) == + log(cosh(w)) + log(z) + z*sinh(w)/cosh(w) + O(z**2)) + + +@XFAIL +def test_X12(): + # Look at the generalized Taylor series around x = 1 + # Result => (x - 1)^a/e^b [1 - (a + 2 b) (x - 1) / 2 + O((x - 1)^2)] + a, b, x = symbols('a b x', real=True) + # series returns O(log(x-1)**2) + # https://github.com/sympy/sympy/issues/7168 + assert (series(log(x)**a*exp(-b*x), x, x0=1, n=2) == + (x - 1)**a/exp(b)*(1 - (a + 2*b)*(x - 1)/2 + O((x - 1)**2))) + + +def test_X13(): + assert series(sqrt(2*x**2 + 1), x, x0=oo, n=1) == sqrt(2)*x + O(1/x, (x, oo)) + + +@XFAIL +def test_X14(): + # Wallis' product => 1/sqrt(pi n) + ... [Knopp, p. 385] + assert series(1/2**(2*n)*binomial(2*n, n), + n, x==oo, n=1) == 1/(sqrt(pi)*sqrt(n)) + O(1/x, (x, oo)) + + +@SKIP("https://github.com/sympy/sympy/issues/7164") +def test_X15(): + # => 0!/x - 1!/x^2 + 2!/x^3 - 3!/x^4 + O(1/x^5) [Knopp, p. 544] + x, t = symbols('x t', real=True) + # raises RuntimeError: maximum recursion depth exceeded + # https://github.com/sympy/sympy/issues/7164 + # 2019-02-17: Raises + # PoleError: + # Asymptotic expansion of Ei around [-oo] is not implemented. + e1 = integrate(exp(-t)/t, (t, x, oo)) + assert (series(e1, x, x0=oo, n=5) == + 6/x**4 + 2/x**3 - 1/x**2 + 1/x + O(x**(-5), (x, oo))) + + +def test_X16(): + # Multivariate Taylor series expansion => 1 - (x^2 + 2 x y + y^2)/2 + O(x^4) + assert (series(cos(x + y), x + y, x0=0, n=4) == 1 - (x + y)**2/2 + + O(x**4 + x**3*y + x**2*y**2 + x*y**3 + y**4, x, y)) + + +@XFAIL +def test_X17(): + # Power series (compute the general formula) + # (c41) powerseries(log(sin(x)/x), x, 0); + # /aquarius/data2/opt/local/macsyma_422/library1/trgred.so being loaded. + # inf + # ==== i1 2 i1 2 i1 + # \ (- 1) 2 bern(2 i1) x + # (d41) > ------------------------------ + # / 2 i1 (2 i1)! + # ==== + # i1 = 1 + # fps does not calculate + assert fps(log(sin(x)/x)) == \ + Sum((-1)**k*2**(2*k - 1)*bernoulli(2*k)*x**(2*k)/(k*factorial(2*k)), (k, 1, oo)) + + +@XFAIL +def test_X18(): + # Power series (compute the general formula). Maple FPS: + # > FormalPowerSeries(exp(-x)*sin(x), x = 0); + # infinity + # ----- (1/2 k) k + # \ 2 sin(3/4 k Pi) x + # ) ------------------------- + # / k! + # ----- + # + # Now, SymPy returns + # oo + # _____ + # \ ` + # \ / k k\ + # \ k |I*(-1 - I) I*(-1 + I) | + # \ x *|----------- - -----------| + # / \ 2 2 / + # / ------------------------------ + # / k! + # /____, + # k = 0 + k = Dummy('k') + assert fps(exp(-x)*sin(x)) == \ + Sum(2**(S.Half*k)*sin(R(3, 4)*k*pi)*x**k/factorial(k), (k, 0, oo)) + + +@XFAIL +def test_X19(): + # (c45) /* Derive an explicit Taylor series solution of y as a function of + # x from the following implicit relation: + # y = x - 1 + (x - 1)^2/2 + 2/3 (x - 1)^3 + (x - 1)^4 + + # 17/10 (x - 1)^5 + ... + # */ + # x = sin(y) + cos(y); + # Time= 0 msecs + # (d45) x = sin(y) + cos(y) + # + # (c46) taylor_revert(%, y, 7); + raise NotImplementedError("Solve using series not supported. \ +Inverse Taylor series expansion also not supported") + + +@XFAIL +def test_X20(): + # Pade (rational function) approximation => (2 - x)/(2 + x) + # > numapprox[pade](exp(-x), x = 0, [1, 1]); + # bytes used=9019816, alloc=3669344, time=13.12 + # 1 - 1/2 x + # --------- + # 1 + 1/2 x + # mpmath support numeric Pade approximant but there is + # no symbolic implementation in SymPy + # https://en.wikipedia.org/wiki/Pad%C3%A9_approximant + raise NotImplementedError("Symbolic Pade approximant not supported") + + +def test_X21(): + """ + Test whether `fourier_series` of x periodical on the [-p, p] interval equals + `- (2 p / pi) sum( (-1)^n / n sin(n pi x / p), n = 1..infinity )`. + """ + p = symbols('p', positive=True) + n = symbols('n', positive=True, integer=True) + s = fourier_series(x, (x, -p, p)) + + # All cosine coefficients are equal to 0 + assert s.an.formula == 0 + + # Check for sine coefficients + assert s.bn.formula.subs(s.bn.variables[0], 0) == 0 + assert s.bn.formula.subs(s.bn.variables[0], n) == \ + -2*p/pi * (-1)**n / n * sin(n*pi*x/p) + + +@XFAIL +def test_X22(): + # (c52) /* => p / 2 + # - (2 p / pi^2) sum( [1 - (-1)^n] cos(n pi x / p) / n^2, + # n = 1..infinity ) */ + # fourier_series(abs(x), x, p); + # p + # (e52) a = - + # 0 2 + # + # %nn + # (2 (- 1) - 2) p + # (e53) a = ------------------ + # %nn 2 2 + # %pi %nn + # + # (e54) b = 0 + # %nn + # + # Time= 5290 msecs + # inf %nn %pi %nn x + # ==== (2 (- 1) - 2) cos(---------) + # \ p + # p > ------------------------------- + # / 2 + # ==== %nn + # %nn = 1 p + # (d54) ----------------------------------------- + - + # 2 2 + # %pi + raise NotImplementedError("Fourier series not supported") + + +def test_Y1(): + t = symbols('t', positive=True) + w = symbols('w', real=True) + s = symbols('s') + F, _, _ = laplace_transform(cos((w - 1)*t), t, s) + assert F == s/(s**2 + (w - 1)**2) + + +def test_Y2(): + t = symbols('t', positive=True) + w = symbols('w', real=True) + s = symbols('s') + f = inverse_laplace_transform(s/(s**2 + (w - 1)**2), s, t, simplify=True) + assert f == cos(t*(w - 1)) + + +def test_Y3(): + t = symbols('t', positive=True) + w = symbols('w', real=True) + s = symbols('s') + F, _, _ = laplace_transform(sinh(w*t)*cosh(w*t), t, s, simplify=True) + assert F == w/(s**2 - 4*w**2) + + +def test_Y4(): + t = symbols('t', positive=True) + s = symbols('s') + F, _, _ = laplace_transform(erf(3/sqrt(t)), t, s, simplify=True) + assert F == 1/s - exp(-6*sqrt(s))/s + + +def test_Y5_Y6(): +# Solve y'' + y = 4 [H(t - 1) - H(t - 2)], y(0) = 1, y'(0) = 0 where H is the +# Heaviside (unit step) function (the RHS describes a pulse of magnitude 4 and +# duration 1). See David A. Sanchez, Richard C. Allen, Jr. and Walter T. +# Kyner, _Differential Equations: An Introduction_, Addison-Wesley Publishing +# Company, 1983, p. 211. First, take the Laplace transform of the ODE +# => s^2 Y(s) - s + Y(s) = 4/s [e^(-s) - e^(-2 s)] +# where Y(s) is the Laplace transform of y(t) + t = symbols('t', real=True) + s = symbols('s') + y = Function('y') + Y = Function('Y') + F = laplace_correspondence(laplace_transform(diff(y(t), t, 2) + y(t) + - 4*(Heaviside(t - 1) - Heaviside(t - 2)), + t, s, noconds=True), {y: Y}) + D = ( + -F + s**2*Y(s) - s*y(0) + Y(s) - Subs(Derivative(y(t), t), t, 0) - + 4*exp(-s)/s + 4*exp(-2*s)/s) + assert D == 0 +# Now, solve for Y(s) and then take the inverse Laplace transform +# => Y(s) = s/(s^2 + 1) + 4 [1/s - s/(s^2 + 1)] [e^(-s) - e^(-2 s)] +# => y(t) = cos t + 4 {[1 - cos(t - 1)] H(t - 1) - [1 - cos(t - 2)] H(t - 2)} + Yf = solve(F, Y(s))[0] + Yf = laplace_initial_conds(Yf, t, {y: [1, 0]}) + assert Yf == (s**2*exp(2*s) + 4*exp(s) - 4)*exp(-2*s)/(s*(s**2 + 1)) + yf = inverse_laplace_transform(Yf, s, t) + yf = yf.collect(Heaviside(t-1)).collect(Heaviside(t-2)) + assert yf == ( + (4 - 4*cos(t - 1))*Heaviside(t - 1) + + (4*cos(t - 2) - 4)*Heaviside(t - 2) + + cos(t)*Heaviside(t)) + + +@XFAIL +def test_Y7(): + # What is the Laplace transform of an infinite square wave? + # => 1/s + 2 sum( (-1)^n e^(- s n a)/s, n = 1..infinity ) + # [Sanchez, Allen and Kyner, p. 213] + t = symbols('t', positive=True) + a = symbols('a', real=True) + s = symbols('s') + F, _, _ = laplace_transform(1 + 2*Sum((-1)**n*Heaviside(t - n*a), + (n, 1, oo)), t, s) + # returns 2*LaplaceTransform(Sum((-1)**n*Heaviside(-a*n + t), + # (n, 1, oo)), t, s) + 1/s + # https://github.com/sympy/sympy/issues/7177 + assert F == 2*Sum((-1)**n*exp(-a*n*s)/s, (n, 1, oo)) + 1/s + + +@XFAIL +def test_Y8(): + assert fourier_transform(1, x, z) == DiracDelta(z) + + +def test_Y9(): + assert (fourier_transform(exp(-9*x**2), x, z) == + sqrt(pi)*exp(-pi**2*z**2/9)/3) + + +def test_Y10(): + assert (fourier_transform(abs(x)*exp(-3*abs(x)), x, z).cancel() == + (-8*pi**2*z**2 + 18)/(16*pi**4*z**4 + 72*pi**2*z**2 + 81)) + + +@SKIP("https://github.com/sympy/sympy/issues/7181") +@slow +def test_Y11(): + # => pi cot(pi s) (0 < Re s < 1) [Gradshteyn and Ryzhik 17.43(5)] + x, s = symbols('x s') + # raises RuntimeError: maximum recursion depth exceeded + # https://github.com/sympy/sympy/issues/7181 + # Update 2019-02-17 raises: + # TypeError: cannot unpack non-iterable MellinTransform object + F, _, _ = mellin_transform(1/(1 - x), x, s) + assert F == pi*cot(pi*s) + + +@XFAIL +def test_Y12(): + # => 2^(s - 4) gamma(s/2)/gamma(4 - s/2) (0 < Re s < 1) + # [Gradshteyn and Ryzhik 17.43(16)] + x, s = symbols('x s') + # returns Wrong value -2**(s - 4)*gamma(s/2 - 3)/gamma(-s/2 + 1) + # https://github.com/sympy/sympy/issues/7182 + F, _, _ = mellin_transform(besselj(3, x)/x**3, x, s) + assert F == -2**(s - 4)*gamma(s/2)/gamma(-s/2 + 4) + + +@XFAIL +def test_Y13(): +# Z[H(t - m T)] => z/[z^m (z - 1)] (H is the Heaviside (unit step) function) z + raise NotImplementedError("z-transform not supported") + + +@XFAIL +def test_Y14(): +# Z[H(t - m T)] => z/[z^m (z - 1)] (H is the Heaviside (unit step) function) + raise NotImplementedError("z-transform not supported") + + +def test_Z1(): + r = Function('r') + assert (rsolve(r(n + 2) - 2*r(n + 1) + r(n) - 2, r(n), + {r(0): 1, r(1): m}).simplify() == n**2 + n*(m - 2) + 1) + + +def test_Z2(): + r = Function('r') + assert (rsolve(r(n) - (5*r(n - 1) - 6*r(n - 2)), r(n), {r(0): 0, r(1): 1}) + == -2**n + 3**n) + + +def test_Z3(): + # => r(n) = Fibonacci[n + 1] [Cohen, p. 83] + r = Function('r') + # recurrence solution is correct, Wester expects it to be simplified to + # fibonacci(n+1), but that is quite hard + expected = ((S(1)/2 - sqrt(5)/2)**n*(S(1)/2 - sqrt(5)/10) + + (S(1)/2 + sqrt(5)/2)**n*(sqrt(5)/10 + S(1)/2)) + sol = rsolve(r(n) - (r(n - 1) + r(n - 2)), r(n), {r(1): 1, r(2): 2}) + assert sol == expected + + +@XFAIL +def test_Z4(): +# => [c^(n+1) [c^(n+1) - 2 c - 2] + (n+1) c^2 + 2 c - n] / [(c-1)^3 (c+1)] +# [Joan Z. Yu and Robert Israel in sci.math.symbolic] + r = Function('r') + c = symbols('c') + # raises ValueError: Polynomial or rational function expected, + # got '(c**2 - c**n)/(c - c**n) + s = rsolve(r(n) - ((1 + c - c**(n-1) - c**(n+1))/(1 - c**n)*r(n - 1) + - c*(1 - c**(n-2))/(1 - c**(n-1))*r(n - 2) + 1), + r(n), {r(1): 1, r(2): (2 + 2*c + c**2)/(1 + c)}) + assert (s - (c*(n + 1)*(c*(n + 1) - 2*c - 2) + + (n + 1)*c**2 + 2*c - n)/((c-1)**3*(c+1)) == 0) + + +@XFAIL +def test_Z5(): + # Second order ODE with initial conditions---solve directly + # transform: f(t) = sin(2 t)/8 - t cos(2 t)/4 + C1, C2 = symbols('C1 C2') + # initial conditions not supported, this is a manual workaround + # https://github.com/sympy/sympy/issues/4720 + eq = Derivative(f(x), x, 2) + 4*f(x) - sin(2*x) + sol = dsolve(eq, f(x)) + f0 = Lambda(x, sol.rhs) + assert f0(x) == C2*sin(2*x) + (C1 - x/4)*cos(2*x) + f1 = Lambda(x, diff(f0(x), x)) + # TODO: Replace solve with solveset, when it works for solveset + const_dict = solve((f0(0), f1(0))) + result = f0(x).subs(C1, const_dict[C1]).subs(C2, const_dict[C2]) + assert result == -x*cos(2*x)/4 + sin(2*x)/8 + # Result is OK, but ODE solving with initial conditions should be + # supported without all this manual work + raise NotImplementedError('ODE solving with initial conditions \ +not supported') + + +@XFAIL +def test_Z6(): + # Second order ODE with initial conditions---solve using Laplace + # transform: f(t) = sin(2 t)/8 - t cos(2 t)/4 + t = symbols('t', positive=True) + s = symbols('s') + eq = Derivative(f(t), t, 2) + 4*f(t) - sin(2*t) + F, _, _ = laplace_transform(eq, t, s) + # Laplace transform for diff() not calculated + # https://github.com/sympy/sympy/issues/7176 + assert (F == s**2*LaplaceTransform(f(t), t, s) + + 4*LaplaceTransform(f(t), t, s) - 2/(s**2 + 4)) + # rest of test case not implemented diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_xxe.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_xxe.py new file mode 100644 index 0000000000000000000000000000000000000000..3936e8aa135dde5f22c71548e2f90ed58ac25cb8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tests/test_xxe.py @@ -0,0 +1,3 @@ +# A test file for XXE injection +# Username: Test +# Password: Test diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/timeutils.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/timeutils.py new file mode 100644 index 0000000000000000000000000000000000000000..1caae825103335f3e8afedeaf741cff2b0414fb2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/timeutils.py @@ -0,0 +1,75 @@ +"""Simple tools for timing functions' execution, when IPython is not available. """ + + +import timeit +import math + + +_scales = [1e0, 1e3, 1e6, 1e9] +_units = ['s', 'ms', '\N{GREEK SMALL LETTER MU}s', 'ns'] + + +def timed(func, setup="pass", limit=None): + """Adaptively measure execution time of a function. """ + timer = timeit.Timer(func, setup=setup) + repeat, number = 3, 1 + + for i in range(1, 10): + if timer.timeit(number) >= 0.2: + break + elif limit is not None and number >= limit: + break + else: + number *= 10 + + time = min(timer.repeat(repeat, number)) / number + + if time > 0.0: + order = min(-int(math.floor(math.log10(time)) // 3), 3) + else: + order = 3 + + return (number, time, time*_scales[order], _units[order]) + + +# Code for doing inline timings of recursive algorithms. + +def __do_timings(): + import os + res = os.getenv('SYMPY_TIMINGS', '') + res = [x.strip() for x in res.split(',')] + return set(res) + +_do_timings = __do_timings() +_timestack = None + + +def _print_timestack(stack, level=1): + print('-'*level, '%.2f %s%s' % (stack[2], stack[0], stack[3])) + for s in stack[1]: + _print_timestack(s, level + 1) + + +def timethis(name): + def decorator(func): + if name not in _do_timings: + return func + + def wrapper(*args, **kwargs): + from time import time + global _timestack + oldtimestack = _timestack + _timestack = [func.func_name, [], 0, args] + t1 = time() + r = func(*args, **kwargs) + t2 = time() + _timestack[2] = t2 - t1 + if oldtimestack is not None: + oldtimestack[1].append(_timestack) + _timestack = oldtimestack + else: + _print_timestack(_timestack) + _timestack = None + return r + return wrapper + return decorator diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tmpfiles.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tmpfiles.py new file mode 100644 index 0000000000000000000000000000000000000000..81c97efbf9d67cc20f25ff251abc94c2f5bafe06 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/utilities/tmpfiles.py @@ -0,0 +1,12 @@ +""" +.. deprecated:: 1.6 + + sympy.utilities.tmpfiles has been renamed to sympy.testing.tmpfiles. +""" +from sympy.utilities.exceptions import sympy_deprecation_warning + +sympy_deprecation_warning("The sympy.utilities.tmpfiles submodule is deprecated. Use sympy.testing.tmpfiles instead.", + deprecated_since_version="1.6", + active_deprecations_target="deprecated-sympy-utilities-submodules") + +from sympy.testing.tmpfiles import * # noqa:F401,F403 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f6757bbeb35022481b1cf183373ecccd19779faa --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/__init__.py @@ -0,0 +1,50 @@ +from sympy.vector.coordsysrect import CoordSys3D +from sympy.vector.vector import (Vector, VectorAdd, VectorMul, + BaseVector, VectorZero, Cross, Dot, cross, dot) +from sympy.vector.dyadic import (Dyadic, DyadicAdd, DyadicMul, + BaseDyadic, DyadicZero) +from sympy.vector.scalar import BaseScalar +from sympy.vector.deloperator import Del +from sympy.vector.functions import (express, matrix_to_vector, + laplacian, is_conservative, + is_solenoidal, scalar_potential, + directional_derivative, + scalar_potential_difference) +from sympy.vector.point import Point +from sympy.vector.orienters import (AxisOrienter, BodyOrienter, + SpaceOrienter, QuaternionOrienter) +from sympy.vector.operators import Gradient, Divergence, Curl, Laplacian, gradient, curl, divergence +from sympy.vector.implicitregion import ImplicitRegion +from sympy.vector.parametricregion import (ParametricRegion, parametric_region_list) +from sympy.vector.integrals import (ParametricIntegral, vector_integrate) +from sympy.vector.kind import VectorKind + +__all__ = [ + 'Vector', 'VectorAdd', 'VectorMul', 'BaseVector', 'VectorZero', 'Cross', + 'Dot', 'cross', 'dot', + + 'VectorKind', + + 'Dyadic', 'DyadicAdd', 'DyadicMul', 'BaseDyadic', 'DyadicZero', + + 'BaseScalar', + + 'Del', + + 'CoordSys3D', + + 'express', 'matrix_to_vector', 'laplacian', 'is_conservative', + 'is_solenoidal', 'scalar_potential', 'directional_derivative', + 'scalar_potential_difference', + + 'Point', + + 'AxisOrienter', 'BodyOrienter', 'SpaceOrienter', 'QuaternionOrienter', + + 'Gradient', 'Divergence', 'Curl', 'Laplacian', 'gradient', 'curl', + 'divergence', + + 'ParametricRegion', 'parametric_region_list', 'ImplicitRegion', + + 'ParametricIntegral', 'vector_integrate', +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/basisdependent.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/basisdependent.py new file mode 100644 index 0000000000000000000000000000000000000000..53e4efc0bf839fb5a5de2d1af1487683fabd8cf1 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/basisdependent.py @@ -0,0 +1,374 @@ +from __future__ import annotations +from typing import TYPE_CHECKING + +from sympy.simplify import simplify as simp, trigsimp as tsimp # type: ignore +from sympy.core.decorators import call_highest_priority, _sympifyit +from sympy.core.assumptions import StdFactKB +from sympy.core.function import diff as df +from sympy.integrals.integrals import Integral +from sympy.polys.polytools import factor as fctr +from sympy.core import S, Add, Mul +from sympy.core.expr import Expr + +if TYPE_CHECKING: + from sympy.vector.vector import BaseVector + + +class BasisDependent(Expr): + """ + Super class containing functionality common to vectors and + dyadics. + Named so because the representation of these quantities in + sympy.vector is dependent on the basis they are expressed in. + """ + + zero: BasisDependentZero + + @call_highest_priority('__radd__') + def __add__(self, other): + return self._add_func(self, other) + + @call_highest_priority('__add__') + def __radd__(self, other): + return self._add_func(other, self) + + @call_highest_priority('__rsub__') + def __sub__(self, other): + return self._add_func(self, -other) + + @call_highest_priority('__sub__') + def __rsub__(self, other): + return self._add_func(other, -self) + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__rmul__') + def __mul__(self, other): + return self._mul_func(self, other) + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__mul__') + def __rmul__(self, other): + return self._mul_func(other, self) + + def __neg__(self): + return self._mul_func(S.NegativeOne, self) + + @_sympifyit('other', NotImplemented) + @call_highest_priority('__rtruediv__') + def __truediv__(self, other): + return self._div_helper(other) + + @call_highest_priority('__truediv__') + def __rtruediv__(self, other): + return TypeError("Invalid divisor for division") + + def evalf(self, n=15, subs=None, maxn=100, chop=False, strict=False, quad=None, verbose=False): + """ + Implements the SymPy evalf routine for this quantity. + + evalf's documentation + ===================== + + """ + options = {'subs':subs, 'maxn':maxn, 'chop':chop, 'strict':strict, + 'quad':quad, 'verbose':verbose} + vec = self.zero + for k, v in self.components.items(): + vec += v.evalf(n, **options) * k + return vec + + evalf.__doc__ += Expr.evalf.__doc__ # type: ignore + + n = evalf # type: ignore + + def simplify(self, **kwargs): + """ + Implements the SymPy simplify routine for this quantity. + + simplify's documentation + ======================== + + """ + simp_components = [simp(v, **kwargs) * k for + k, v in self.components.items()] + return self._add_func(*simp_components) + + simplify.__doc__ += simp.__doc__ # type: ignore + + def trigsimp(self, **opts): + """ + Implements the SymPy trigsimp routine, for this quantity. + + trigsimp's documentation + ======================== + + """ + trig_components = [tsimp(v, **opts) * k for + k, v in self.components.items()] + return self._add_func(*trig_components) + + trigsimp.__doc__ += tsimp.__doc__ # type: ignore + + def _eval_simplify(self, **kwargs): + return self.simplify(**kwargs) + + def _eval_trigsimp(self, **opts): + return self.trigsimp(**opts) + + def _eval_derivative(self, wrt): + return self.diff(wrt) + + def _eval_Integral(self, *symbols, **assumptions): + integral_components = [Integral(v, *symbols, **assumptions) * k + for k, v in self.components.items()] + return self._add_func(*integral_components) + + def as_numer_denom(self): + """ + Returns the expression as a tuple wrt the following + transformation - + + expression -> a/b -> a, b + + """ + return self, S.One + + def factor(self, *args, **kwargs): + """ + Implements the SymPy factor routine, on the scalar parts + of a basis-dependent expression. + + factor's documentation + ======================== + + """ + fctr_components = [fctr(v, *args, **kwargs) * k for + k, v in self.components.items()] + return self._add_func(*fctr_components) + + factor.__doc__ += fctr.__doc__ # type: ignore + + def as_coeff_Mul(self, rational=False): + """Efficiently extract the coefficient of a product.""" + return (S.One, self) + + def as_coeff_add(self, *deps): + """Efficiently extract the coefficient of a summation.""" + return 0, tuple(x * self.components[x] for x in self.components) + + def diff(self, *args, **kwargs): + """ + Implements the SymPy diff routine, for vectors. + + diff's documentation + ======================== + + """ + for x in args: + if isinstance(x, BasisDependent): + raise TypeError("Invalid arg for differentiation") + diff_components = [df(v, *args, **kwargs) * k for + k, v in self.components.items()] + return self._add_func(*diff_components) + + diff.__doc__ += df.__doc__ # type: ignore + + def doit(self, **hints): + """Calls .doit() on each term in the Dyadic""" + doit_components = [self.components[x].doit(**hints) * x + for x in self.components] + return self._add_func(*doit_components) + + +class BasisDependentAdd(BasisDependent, Add): + """ + Denotes sum of basis dependent quantities such that they cannot + be expressed as base or Mul instances. + """ + + def __new__(cls, *args, **options): + components = {} + + # Check each arg and simultaneously learn the components + for arg in args: + if not isinstance(arg, cls._expr_type): + if isinstance(arg, Mul): + arg = cls._mul_func(*(arg.args)) + elif isinstance(arg, Add): + arg = cls._add_func(*(arg.args)) + else: + raise TypeError(str(arg) + + " cannot be interpreted correctly") + # If argument is zero, ignore + if arg == cls.zero: + continue + # Else, update components accordingly + for x in arg.components: + components[x] = components.get(x, 0) + arg.components[x] + + temp = list(components.keys()) + for x in temp: + if components[x] == 0: + del components[x] + + # Handle case of zero vector + if len(components) == 0: + return cls.zero + + # Build object + newargs = [x * components[x] for x in components] + obj = super().__new__(cls, *newargs, **options) + if isinstance(obj, Mul): + return cls._mul_func(*obj.args) + assumptions = {'commutative': True} + obj._assumptions = StdFactKB(assumptions) + obj._components = components + obj._sys = (list(components.keys()))[0]._sys + + return obj + + +class BasisDependentMul(BasisDependent, Mul): + """ + Denotes product of base- basis dependent quantity with a scalar. + """ + + def __new__(cls, *args, **options): + obj = cls._new(*args, **options) + return obj + + def _new_rawargs(self, *args): + # XXX: This is needed because Add.flatten() uses it but the default + # implementation does not work for Vectors because they assign + # attributes outside of .args. + return type(self)(*args) + + @classmethod + def _new(cls, *args, **options): + from sympy.vector import Cross, Dot, Curl, Gradient + count = 0 + measure_number = S.One + zeroflag = False + extra_args = [] + + # Determine the component and check arguments + # Also keep a count to ensure two vectors aren't + # being multiplied + for arg in args: + if isinstance(arg, cls._zero_func): + count += 1 + zeroflag = True + elif arg == S.Zero: + zeroflag = True + elif isinstance(arg, (cls._base_func, cls._mul_func)): + count += 1 + expr = arg._base_instance + measure_number *= arg._measure_number + elif isinstance(arg, cls._add_func): + count += 1 + expr = arg + elif isinstance(arg, (Cross, Dot, Curl, Gradient)): + extra_args.append(arg) + else: + measure_number *= arg + # Make sure incompatible types weren't multiplied + if count > 1: + raise ValueError("Invalid multiplication") + elif count == 0: + return Mul(*args, **options) + # Handle zero vector case + if zeroflag: + return cls.zero + + # If one of the args was a VectorAdd, return an + # appropriate VectorAdd instance + if isinstance(expr, cls._add_func): + newargs = [cls._mul_func(measure_number, x) for + x in expr.args] + return cls._add_func(*newargs) + + obj = super().__new__(cls, measure_number, + expr._base_instance, + *extra_args, + **options) + if isinstance(obj, Add): + return cls._add_func(*obj.args) + obj._base_instance = expr._base_instance + obj._measure_number = measure_number + assumptions = {'commutative': True} + obj._assumptions = StdFactKB(assumptions) + obj._components = {expr._base_instance: measure_number} + obj._sys = expr._base_instance._sys + + return obj + + def _sympystr(self, printer): + measure_str = printer._print(self._measure_number) + if ('(' in measure_str or '-' in measure_str or + '+' in measure_str): + measure_str = '(' + measure_str + ')' + return measure_str + '*' + printer._print(self._base_instance) + + +class BasisDependentZero(BasisDependent): + """ + Class to denote a zero basis dependent instance. + """ + components: dict['BaseVector', Expr] = {} + _latex_form: str + + def __new__(cls): + obj = super().__new__(cls) + # Pre-compute a specific hash value for the zero vector + # Use the same one always + obj._hash = (S.Zero, cls).__hash__() + return obj + + def __hash__(self): + return self._hash + + @call_highest_priority('__req__') + def __eq__(self, other): + return isinstance(other, self._zero_func) + + __req__ = __eq__ + + @call_highest_priority('__radd__') + def __add__(self, other): + if isinstance(other, self._expr_type): + return other + else: + raise TypeError("Invalid argument types for addition") + + @call_highest_priority('__add__') + def __radd__(self, other): + if isinstance(other, self._expr_type): + return other + else: + raise TypeError("Invalid argument types for addition") + + @call_highest_priority('__rsub__') + def __sub__(self, other): + if isinstance(other, self._expr_type): + return -other + else: + raise TypeError("Invalid argument types for subtraction") + + @call_highest_priority('__sub__') + def __rsub__(self, other): + if isinstance(other, self._expr_type): + return other + else: + raise TypeError("Invalid argument types for subtraction") + + def __neg__(self): + return self + + def normalize(self): + """ + Returns the normalized version of this vector. + """ + return self + + def _sympystr(self, printer): + return '0' diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/coordsysrect.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/coordsysrect.py new file mode 100644 index 0000000000000000000000000000000000000000..55539fb19dc4221de69437111f44d6a6cc70b3e4 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/coordsysrect.py @@ -0,0 +1,1031 @@ +from collections.abc import Callable + +from sympy.core.basic import Basic +from sympy.core.cache import cacheit +from sympy.core import S, Dummy, Lambda +from sympy.core.symbol import Str +from sympy.core.symbol import symbols +from sympy.matrices.immutable import ImmutableDenseMatrix as Matrix +from sympy.matrices.matrixbase import MatrixBase +from sympy.solvers import solve +from sympy.vector.scalar import BaseScalar +from sympy.core.containers import Tuple +from sympy.core.function import diff +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (acos, atan2, cos, sin) +from sympy.matrices.dense import eye +from sympy.matrices.immutable import ImmutableDenseMatrix +from sympy.simplify.simplify import simplify +from sympy.simplify.trigsimp import trigsimp +import sympy.vector +from sympy.vector.orienters import (Orienter, AxisOrienter, BodyOrienter, + SpaceOrienter, QuaternionOrienter) + + +class CoordSys3D(Basic): + """ + Represents a coordinate system in 3-D space. + """ + + def __new__(cls, name, transformation=None, parent=None, location=None, + rotation_matrix=None, vector_names=None, variable_names=None): + """ + The orientation/location parameters are necessary if this system + is being defined at a certain orientation or location wrt another. + + Parameters + ========== + + name : str + The name of the new CoordSys3D instance. + + transformation : Lambda, Tuple, str + Transformation defined by transformation equations or chosen + from predefined ones. + + location : Vector + The position vector of the new system's origin wrt the parent + instance. + + rotation_matrix : SymPy ImmutableMatrix + The rotation matrix of the new coordinate system with respect + to the parent. In other words, the output of + new_system.rotation_matrix(parent). + + parent : CoordSys3D + The coordinate system wrt which the orientation/location + (or both) is being defined. + + vector_names, variable_names : iterable(optional) + Iterables of 3 strings each, with custom names for base + vectors and base scalars of the new system respectively. + Used for simple str printing. + + """ + + name = str(name) + Vector = sympy.vector.Vector + Point = sympy.vector.Point + + if not isinstance(name, str): + raise TypeError("name should be a string") + + if transformation is not None: + if (location is not None) or (rotation_matrix is not None): + raise ValueError("specify either `transformation` or " + "`location`/`rotation_matrix`") + if isinstance(transformation, (Tuple, tuple, list)): + if isinstance(transformation[0], MatrixBase): + rotation_matrix = transformation[0] + location = transformation[1] + else: + transformation = Lambda(transformation[0], + transformation[1]) + elif isinstance(transformation, Callable): + x1, x2, x3 = symbols('x1 x2 x3', cls=Dummy) + transformation = Lambda((x1, x2, x3), + transformation(x1, x2, x3)) + elif isinstance(transformation, str): + transformation = Str(transformation) + elif isinstance(transformation, (Str, Lambda)): + pass + else: + raise TypeError("transformation: " + "wrong type {}".format(type(transformation))) + + # If orientation information has been provided, store + # the rotation matrix accordingly + if rotation_matrix is None: + rotation_matrix = ImmutableDenseMatrix(eye(3)) + else: + if not isinstance(rotation_matrix, MatrixBase): + raise TypeError("rotation_matrix should be an Immutable" + + "Matrix instance") + rotation_matrix = rotation_matrix.as_immutable() + + # If location information is not given, adjust the default + # location as Vector.zero + if parent is not None: + if not isinstance(parent, CoordSys3D): + raise TypeError("parent should be a " + + "CoordSys3D/None") + if location is None: + location = Vector.zero + else: + if not isinstance(location, Vector): + raise TypeError("location should be a Vector") + # Check that location does not contain base + # scalars + for x in location.free_symbols: + if isinstance(x, BaseScalar): + raise ValueError("location should not contain" + + " BaseScalars") + origin = parent.origin.locate_new(name + '.origin', + location) + else: + location = Vector.zero + origin = Point(name + '.origin') + + if transformation is None: + transformation = Tuple(rotation_matrix, location) + + if isinstance(transformation, Tuple): + lambda_transformation = CoordSys3D._compose_rotation_and_translation( + transformation[0], + transformation[1], + parent + ) + r, l = transformation + l = l._projections + lambda_lame = CoordSys3D._get_lame_coeff('cartesian') + lambda_inverse = lambda x, y, z: r.inv()*Matrix( + [x-l[0], y-l[1], z-l[2]]) + elif isinstance(transformation, Str): + trname = transformation.name + lambda_transformation = CoordSys3D._get_transformation_lambdas(trname) + if parent is not None: + if parent.lame_coefficients() != (S.One, S.One, S.One): + raise ValueError('Parent for pre-defined coordinate ' + 'system should be Cartesian.') + lambda_lame = CoordSys3D._get_lame_coeff(trname) + lambda_inverse = CoordSys3D._set_inv_trans_equations(trname) + elif isinstance(transformation, Lambda): + if not CoordSys3D._check_orthogonality(transformation): + raise ValueError("The transformation equation does not " + "create orthogonal coordinate system") + lambda_transformation = transformation + lambda_lame = CoordSys3D._calculate_lame_coeff(lambda_transformation) + lambda_inverse = None + else: + lambda_transformation = lambda x, y, z: transformation(x, y, z) + lambda_lame = CoordSys3D._get_lame_coeff(transformation) + lambda_inverse = None + + if variable_names is None: + if isinstance(transformation, Lambda): + variable_names = ["x1", "x2", "x3"] + elif isinstance(transformation, Str): + if transformation.name == 'spherical': + variable_names = ["r", "theta", "phi"] + elif transformation.name == 'cylindrical': + variable_names = ["r", "theta", "z"] + else: + variable_names = ["x", "y", "z"] + else: + variable_names = ["x", "y", "z"] + if vector_names is None: + vector_names = ["i", "j", "k"] + + # All systems that are defined as 'roots' are unequal, unless + # they have the same name. + # Systems defined at same orientation/position wrt the same + # 'parent' are equal, irrespective of the name. + # This is true even if the same orientation is provided via + # different methods like Axis/Body/Space/Quaternion. + # However, coincident systems may be seen as unequal if + # positioned/oriented wrt different parents, even though + # they may actually be 'coincident' wrt the root system. + if parent is not None: + obj = super().__new__( + cls, Str(name), transformation, parent) + else: + obj = super().__new__( + cls, Str(name), transformation) + obj._name = name + # Initialize the base vectors + + _check_strings('vector_names', vector_names) + vector_names = list(vector_names) + latex_vects = [(r'\mathbf{\hat{%s}_{%s}}' % (x, name)) for + x in vector_names] + pretty_vects = ['%s_%s' % (x, name) for x in vector_names] + + obj._vector_names = vector_names + + v1 = BaseVector(0, obj, pretty_vects[0], latex_vects[0]) + v2 = BaseVector(1, obj, pretty_vects[1], latex_vects[1]) + v3 = BaseVector(2, obj, pretty_vects[2], latex_vects[2]) + + obj._base_vectors = (v1, v2, v3) + + # Initialize the base scalars + + _check_strings('variable_names', vector_names) + variable_names = list(variable_names) + latex_scalars = [(r"\mathbf{{%s}_{%s}}" % (x, name)) for + x in variable_names] + pretty_scalars = ['%s_%s' % (x, name) for x in variable_names] + + obj._variable_names = variable_names + obj._vector_names = vector_names + + x1 = BaseScalar(0, obj, pretty_scalars[0], latex_scalars[0]) + x2 = BaseScalar(1, obj, pretty_scalars[1], latex_scalars[1]) + x3 = BaseScalar(2, obj, pretty_scalars[2], latex_scalars[2]) + + obj._base_scalars = (x1, x2, x3) + + obj._transformation = transformation + obj._transformation_lambda = lambda_transformation + obj._lame_coefficients = lambda_lame(x1, x2, x3) + obj._transformation_from_parent_lambda = lambda_inverse + + setattr(obj, variable_names[0], x1) + setattr(obj, variable_names[1], x2) + setattr(obj, variable_names[2], x3) + + setattr(obj, vector_names[0], v1) + setattr(obj, vector_names[1], v2) + setattr(obj, vector_names[2], v3) + + # Assign params + obj._parent = parent + if obj._parent is not None: + obj._root = obj._parent._root + else: + obj._root = obj + + obj._parent_rotation_matrix = rotation_matrix + obj._origin = origin + + # Return the instance + return obj + + def _sympystr(self, printer): + return self._name + + def __iter__(self): + return iter(self.base_vectors()) + + @staticmethod + def _check_orthogonality(equations): + """ + Helper method for _connect_to_cartesian. It checks if + set of transformation equations create orthogonal curvilinear + coordinate system + + Parameters + ========== + + equations : Lambda + Lambda of transformation equations + + """ + + x1, x2, x3 = symbols("x1, x2, x3", cls=Dummy) + equations = equations(x1, x2, x3) + v1 = Matrix([diff(equations[0], x1), + diff(equations[1], x1), diff(equations[2], x1)]) + + v2 = Matrix([diff(equations[0], x2), + diff(equations[1], x2), diff(equations[2], x2)]) + + v3 = Matrix([diff(equations[0], x3), + diff(equations[1], x3), diff(equations[2], x3)]) + + if any(simplify(i[0] + i[1] + i[2]) == 0 for i in (v1, v2, v3)): + return False + else: + if simplify(v1.dot(v2)) == 0 and simplify(v2.dot(v3)) == 0 \ + and simplify(v3.dot(v1)) == 0: + return True + else: + return False + + @staticmethod + def _set_inv_trans_equations(curv_coord_name): + """ + Store information about inverse transformation equations for + pre-defined coordinate systems. + + Parameters + ========== + + curv_coord_name : str + Name of coordinate system + + """ + if curv_coord_name == 'cartesian': + return lambda x, y, z: (x, y, z) + + if curv_coord_name == 'spherical': + return lambda x, y, z: ( + sqrt(x**2 + y**2 + z**2), + acos(z/sqrt(x**2 + y**2 + z**2)), + atan2(y, x) + ) + if curv_coord_name == 'cylindrical': + return lambda x, y, z: ( + sqrt(x**2 + y**2), + atan2(y, x), + z + ) + raise ValueError('Wrong set of parameters.' + 'Type of coordinate system is defined') + + def _calculate_inv_trans_equations(self): + """ + Helper method for set_coordinate_type. It calculates inverse + transformation equations for given transformations equations. + + """ + x1, x2, x3 = symbols("x1, x2, x3", cls=Dummy, reals=True) + x, y, z = symbols("x, y, z", cls=Dummy) + + equations = self._transformation(x1, x2, x3) + + solved = solve([equations[0] - x, + equations[1] - y, + equations[2] - z], (x1, x2, x3), dict=True)[0] + solved = solved[x1], solved[x2], solved[x3] + self._transformation_from_parent_lambda = \ + lambda x1, x2, x3: tuple(i.subs(list(zip((x, y, z), (x1, x2, x3)))) for i in solved) + + @staticmethod + def _get_lame_coeff(curv_coord_name): + """ + Store information about Lame coefficients for pre-defined + coordinate systems. + + Parameters + ========== + + curv_coord_name : str + Name of coordinate system + + """ + if isinstance(curv_coord_name, str): + if curv_coord_name == 'cartesian': + return lambda x, y, z: (S.One, S.One, S.One) + if curv_coord_name == 'spherical': + return lambda r, theta, phi: (S.One, r, r*sin(theta)) + if curv_coord_name == 'cylindrical': + return lambda r, theta, h: (S.One, r, S.One) + raise ValueError('Wrong set of parameters.' + ' Type of coordinate system is not defined') + return CoordSys3D._calculate_lame_coefficients(curv_coord_name) + + @staticmethod + def _calculate_lame_coeff(equations): + """ + It calculates Lame coefficients + for given transformations equations. + + Parameters + ========== + + equations : Lambda + Lambda of transformation equations. + + """ + return lambda x1, x2, x3: ( + sqrt(diff(equations(x1, x2, x3)[0], x1)**2 + + diff(equations(x1, x2, x3)[1], x1)**2 + + diff(equations(x1, x2, x3)[2], x1)**2), + sqrt(diff(equations(x1, x2, x3)[0], x2)**2 + + diff(equations(x1, x2, x3)[1], x2)**2 + + diff(equations(x1, x2, x3)[2], x2)**2), + sqrt(diff(equations(x1, x2, x3)[0], x3)**2 + + diff(equations(x1, x2, x3)[1], x3)**2 + + diff(equations(x1, x2, x3)[2], x3)**2) + ) + + def _inverse_rotation_matrix(self): + """ + Returns inverse rotation matrix. + """ + return simplify(self._parent_rotation_matrix**-1) + + @staticmethod + def _get_transformation_lambdas(curv_coord_name): + """ + Store information about transformation equations for pre-defined + coordinate systems. + + Parameters + ========== + + curv_coord_name : str + Name of coordinate system + + """ + if isinstance(curv_coord_name, str): + if curv_coord_name == 'cartesian': + return lambda x, y, z: (x, y, z) + if curv_coord_name == 'spherical': + return lambda r, theta, phi: ( + r*sin(theta)*cos(phi), + r*sin(theta)*sin(phi), + r*cos(theta) + ) + if curv_coord_name == 'cylindrical': + return lambda r, theta, h: ( + r*cos(theta), + r*sin(theta), + h + ) + raise ValueError('Wrong set of parameters.' + 'Type of coordinate system is defined') + + @classmethod + def _rotation_trans_equations(cls, matrix, equations): + """ + Returns the transformation equations obtained from rotation matrix. + + Parameters + ========== + + matrix : Matrix + Rotation matrix + + equations : tuple + Transformation equations + + """ + return tuple(matrix * Matrix(equations)) + + @property + def origin(self): + return self._origin + + def base_vectors(self): + return self._base_vectors + + def base_scalars(self): + return self._base_scalars + + def lame_coefficients(self): + return self._lame_coefficients + + def transformation_to_parent(self): + return self._transformation_lambda(*self.base_scalars()) + + def transformation_from_parent(self): + if self._parent is None: + raise ValueError("no parent coordinate system, use " + "`transformation_from_parent_function()`") + return self._transformation_from_parent_lambda( + *self._parent.base_scalars()) + + def transformation_from_parent_function(self): + return self._transformation_from_parent_lambda + + def rotation_matrix(self, other): + """ + Returns the direction cosine matrix(DCM), also known as the + 'rotation matrix' of this coordinate system with respect to + another system. + + If v_a is a vector defined in system 'A' (in matrix format) + and v_b is the same vector defined in system 'B', then + v_a = A.rotation_matrix(B) * v_b. + + A SymPy Matrix is returned. + + Parameters + ========== + + other : CoordSys3D + The system which the DCM is generated to. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> from sympy import symbols + >>> q1 = symbols('q1') + >>> N = CoordSys3D('N') + >>> A = N.orient_new_axis('A', q1, N.i) + >>> N.rotation_matrix(A) + Matrix([ + [1, 0, 0], + [0, cos(q1), -sin(q1)], + [0, sin(q1), cos(q1)]]) + + """ + from sympy.vector.functions import _path + if not isinstance(other, CoordSys3D): + raise TypeError(str(other) + + " is not a CoordSys3D") + # Handle special cases + if other == self: + return eye(3) + elif other == self._parent: + return self._parent_rotation_matrix + elif other._parent == self: + return other._parent_rotation_matrix.T + # Else, use tree to calculate position + rootindex, path = _path(self, other) + result = eye(3) + for i in range(rootindex): + result *= path[i]._parent_rotation_matrix + for i in range(rootindex + 1, len(path)): + result *= path[i]._parent_rotation_matrix.T + return result + + @cacheit + def position_wrt(self, other): + """ + Returns the position vector of the origin of this coordinate + system with respect to another Point/CoordSys3D. + + Parameters + ========== + + other : Point/CoordSys3D + If other is a Point, the position of this system's origin + wrt it is returned. If its an instance of CoordSyRect, + the position wrt its origin is returned. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> N = CoordSys3D('N') + >>> N1 = N.locate_new('N1', 10 * N.i) + >>> N.position_wrt(N1) + (-10)*N.i + + """ + return self.origin.position_wrt(other) + + def scalar_map(self, other): + """ + Returns a dictionary which expresses the coordinate variables + (base scalars) of this frame in terms of the variables of + otherframe. + + Parameters + ========== + + otherframe : CoordSys3D + The other system to map the variables to. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> from sympy import Symbol + >>> A = CoordSys3D('A') + >>> q = Symbol('q') + >>> B = A.orient_new_axis('B', q, A.k) + >>> A.scalar_map(B) + {A.x: B.x*cos(q) - B.y*sin(q), A.y: B.x*sin(q) + B.y*cos(q), A.z: B.z} + + """ + + origin_coords = tuple(self.position_wrt(other).to_matrix(other)) + relocated_scalars = [x - origin_coords[i] + for i, x in enumerate(other.base_scalars())] + + vars_matrix = (self.rotation_matrix(other) * + Matrix(relocated_scalars)) + return {x: trigsimp(vars_matrix[i]) + for i, x in enumerate(self.base_scalars())} + + def locate_new(self, name, position, vector_names=None, + variable_names=None): + """ + Returns a CoordSys3D with its origin located at the given + position wrt this coordinate system's origin. + + Parameters + ========== + + name : str + The name of the new CoordSys3D instance. + + position : Vector + The position vector of the new system's origin wrt this + one. + + vector_names, variable_names : iterable(optional) + Iterables of 3 strings each, with custom names for base + vectors and base scalars of the new system respectively. + Used for simple str printing. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> A = CoordSys3D('A') + >>> B = A.locate_new('B', 10 * A.i) + >>> B.origin.position_wrt(A.origin) + 10*A.i + + """ + if variable_names is None: + variable_names = self._variable_names + if vector_names is None: + vector_names = self._vector_names + + return CoordSys3D(name, location=position, + vector_names=vector_names, + variable_names=variable_names, + parent=self) + + def orient_new(self, name, orienters, location=None, + vector_names=None, variable_names=None): + """ + Creates a new CoordSys3D oriented in the user-specified way + with respect to this system. + + Please refer to the documentation of the orienter classes + for more information about the orientation procedure. + + Parameters + ========== + + name : str + The name of the new CoordSys3D instance. + + orienters : iterable/Orienter + An Orienter or an iterable of Orienters for orienting the + new coordinate system. + If an Orienter is provided, it is applied to get the new + system. + If an iterable is provided, the orienters will be applied + in the order in which they appear in the iterable. + + location : Vector(optional) + The location of the new coordinate system's origin wrt this + system's origin. If not specified, the origins are taken to + be coincident. + + vector_names, variable_names : iterable(optional) + Iterables of 3 strings each, with custom names for base + vectors and base scalars of the new system respectively. + Used for simple str printing. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> from sympy import symbols + >>> q0, q1, q2, q3 = symbols('q0 q1 q2 q3') + >>> N = CoordSys3D('N') + + Using an AxisOrienter + + >>> from sympy.vector import AxisOrienter + >>> axis_orienter = AxisOrienter(q1, N.i + 2 * N.j) + >>> A = N.orient_new('A', (axis_orienter, )) + + Using a BodyOrienter + + >>> from sympy.vector import BodyOrienter + >>> body_orienter = BodyOrienter(q1, q2, q3, '123') + >>> B = N.orient_new('B', (body_orienter, )) + + Using a SpaceOrienter + + >>> from sympy.vector import SpaceOrienter + >>> space_orienter = SpaceOrienter(q1, q2, q3, '312') + >>> C = N.orient_new('C', (space_orienter, )) + + Using a QuaternionOrienter + + >>> from sympy.vector import QuaternionOrienter + >>> q_orienter = QuaternionOrienter(q0, q1, q2, q3) + >>> D = N.orient_new('D', (q_orienter, )) + """ + if variable_names is None: + variable_names = self._variable_names + if vector_names is None: + vector_names = self._vector_names + + if isinstance(orienters, Orienter): + if isinstance(orienters, AxisOrienter): + final_matrix = orienters.rotation_matrix(self) + else: + final_matrix = orienters.rotation_matrix() + # TODO: trigsimp is needed here so that the matrix becomes + # canonical (scalar_map also calls trigsimp; without this, you can + # end up with the same CoordinateSystem that compares differently + # due to a differently formatted matrix). However, this is + # probably not so good for performance. + final_matrix = trigsimp(final_matrix) + else: + final_matrix = Matrix(eye(3)) + for orienter in orienters: + if isinstance(orienter, AxisOrienter): + final_matrix *= orienter.rotation_matrix(self) + else: + final_matrix *= orienter.rotation_matrix() + + return CoordSys3D(name, rotation_matrix=final_matrix, + vector_names=vector_names, + variable_names=variable_names, + location=location, + parent=self) + + def orient_new_axis(self, name, angle, axis, location=None, + vector_names=None, variable_names=None): + """ + Axis rotation is a rotation about an arbitrary axis by + some angle. The angle is supplied as a SymPy expr scalar, and + the axis is supplied as a Vector. + + Parameters + ========== + + name : string + The name of the new coordinate system + + angle : Expr + The angle by which the new system is to be rotated + + axis : Vector + The axis around which the rotation has to be performed + + location : Vector(optional) + The location of the new coordinate system's origin wrt this + system's origin. If not specified, the origins are taken to + be coincident. + + vector_names, variable_names : iterable(optional) + Iterables of 3 strings each, with custom names for base + vectors and base scalars of the new system respectively. + Used for simple str printing. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> from sympy import symbols + >>> q1 = symbols('q1') + >>> N = CoordSys3D('N') + >>> B = N.orient_new_axis('B', q1, N.i + 2 * N.j) + + """ + if variable_names is None: + variable_names = self._variable_names + if vector_names is None: + vector_names = self._vector_names + + orienter = AxisOrienter(angle, axis) + return self.orient_new(name, orienter, + location=location, + vector_names=vector_names, + variable_names=variable_names) + + def orient_new_body(self, name, angle1, angle2, angle3, + rotation_order, location=None, + vector_names=None, variable_names=None): + """ + Body orientation takes this coordinate system through three + successive simple rotations. + + Body fixed rotations include both Euler Angles and + Tait-Bryan Angles, see https://en.wikipedia.org/wiki/Euler_angles. + + Parameters + ========== + + name : string + The name of the new coordinate system + + angle1, angle2, angle3 : Expr + Three successive angles to rotate the coordinate system by + + rotation_order : string + String defining the order of axes for rotation + + location : Vector(optional) + The location of the new coordinate system's origin wrt this + system's origin. If not specified, the origins are taken to + be coincident. + + vector_names, variable_names : iterable(optional) + Iterables of 3 strings each, with custom names for base + vectors and base scalars of the new system respectively. + Used for simple str printing. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> from sympy import symbols + >>> q1, q2, q3 = symbols('q1 q2 q3') + >>> N = CoordSys3D('N') + + A 'Body' fixed rotation is described by three angles and + three body-fixed rotation axes. To orient a coordinate system D + with respect to N, each sequential rotation is always about + the orthogonal unit vectors fixed to D. For example, a '123' + rotation will specify rotations about N.i, then D.j, then + D.k. (Initially, D.i is same as N.i) + Therefore, + + >>> D = N.orient_new_body('D', q1, q2, q3, '123') + + is same as + + >>> D = N.orient_new_axis('D', q1, N.i) + >>> D = D.orient_new_axis('D', q2, D.j) + >>> D = D.orient_new_axis('D', q3, D.k) + + Acceptable rotation orders are of length 3, expressed in XYZ or + 123, and cannot have a rotation about about an axis twice in a row. + + >>> B = N.orient_new_body('B', q1, q2, q3, '123') + >>> B = N.orient_new_body('B', q1, q2, 0, 'ZXZ') + >>> B = N.orient_new_body('B', 0, 0, 0, 'XYX') + + """ + + orienter = BodyOrienter(angle1, angle2, angle3, rotation_order) + return self.orient_new(name, orienter, + location=location, + vector_names=vector_names, + variable_names=variable_names) + + def orient_new_space(self, name, angle1, angle2, angle3, + rotation_order, location=None, + vector_names=None, variable_names=None): + """ + Space rotation is similar to Body rotation, but the rotations + are applied in the opposite order. + + Parameters + ========== + + name : string + The name of the new coordinate system + + angle1, angle2, angle3 : Expr + Three successive angles to rotate the coordinate system by + + rotation_order : string + String defining the order of axes for rotation + + location : Vector(optional) + The location of the new coordinate system's origin wrt this + system's origin. If not specified, the origins are taken to + be coincident. + + vector_names, variable_names : iterable(optional) + Iterables of 3 strings each, with custom names for base + vectors and base scalars of the new system respectively. + Used for simple str printing. + + See Also + ======== + + CoordSys3D.orient_new_body : method to orient via Euler + angles + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> from sympy import symbols + >>> q1, q2, q3 = symbols('q1 q2 q3') + >>> N = CoordSys3D('N') + + To orient a coordinate system D with respect to N, each + sequential rotation is always about N's orthogonal unit vectors. + For example, a '123' rotation will specify rotations about + N.i, then N.j, then N.k. + Therefore, + + >>> D = N.orient_new_space('D', q1, q2, q3, '312') + + is same as + + >>> B = N.orient_new_axis('B', q1, N.i) + >>> C = B.orient_new_axis('C', q2, N.j) + >>> D = C.orient_new_axis('D', q3, N.k) + + """ + + orienter = SpaceOrienter(angle1, angle2, angle3, rotation_order) + return self.orient_new(name, orienter, + location=location, + vector_names=vector_names, + variable_names=variable_names) + + def orient_new_quaternion(self, name, q0, q1, q2, q3, location=None, + vector_names=None, variable_names=None): + """ + Quaternion orientation orients the new CoordSys3D with + Quaternions, defined as a finite rotation about lambda, a unit + vector, by some amount theta. + + This orientation is described by four parameters: + + q0 = cos(theta/2) + + q1 = lambda_x sin(theta/2) + + q2 = lambda_y sin(theta/2) + + q3 = lambda_z sin(theta/2) + + Quaternion does not take in a rotation order. + + Parameters + ========== + + name : string + The name of the new coordinate system + + q0, q1, q2, q3 : Expr + The quaternions to rotate the coordinate system by + + location : Vector(optional) + The location of the new coordinate system's origin wrt this + system's origin. If not specified, the origins are taken to + be coincident. + + vector_names, variable_names : iterable(optional) + Iterables of 3 strings each, with custom names for base + vectors and base scalars of the new system respectively. + Used for simple str printing. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> from sympy import symbols + >>> q0, q1, q2, q3 = symbols('q0 q1 q2 q3') + >>> N = CoordSys3D('N') + >>> B = N.orient_new_quaternion('B', q0, q1, q2, q3) + + """ + + orienter = QuaternionOrienter(q0, q1, q2, q3) + return self.orient_new(name, orienter, + location=location, + vector_names=vector_names, + variable_names=variable_names) + + def create_new(self, name, transformation, variable_names=None, vector_names=None): + """ + Returns a CoordSys3D which is connected to self by transformation. + + Parameters + ========== + + name : str + The name of the new CoordSys3D instance. + + transformation : Lambda, Tuple, str + Transformation defined by transformation equations or chosen + from predefined ones. + + vector_names, variable_names : iterable(optional) + Iterables of 3 strings each, with custom names for base + vectors and base scalars of the new system respectively. + Used for simple str printing. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> a = CoordSys3D('a') + >>> b = a.create_new('b', transformation='spherical') + >>> b.transformation_to_parent() + (b.r*sin(b.theta)*cos(b.phi), b.r*sin(b.phi)*sin(b.theta), b.r*cos(b.theta)) + >>> b.transformation_from_parent() + (sqrt(a.x**2 + a.y**2 + a.z**2), acos(a.z/sqrt(a.x**2 + a.y**2 + a.z**2)), atan2(a.y, a.x)) + + """ + return CoordSys3D(name, parent=self, transformation=transformation, + variable_names=variable_names, vector_names=vector_names) + + def __init__(self, name, location=None, rotation_matrix=None, + parent=None, vector_names=None, variable_names=None, + latex_vects=None, pretty_vects=None, latex_scalars=None, + pretty_scalars=None, transformation=None): + # Dummy initializer for setting docstring + pass + + __init__.__doc__ = __new__.__doc__ + + @staticmethod + def _compose_rotation_and_translation(rot, translation, parent): + r = lambda x, y, z: CoordSys3D._rotation_trans_equations(rot, (x, y, z)) + if parent is None: + return r + + dx, dy, dz = [translation.dot(i) for i in parent.base_vectors()] + t = lambda x, y, z: ( + x + dx, + y + dy, + z + dz, + ) + return lambda x, y, z: t(*r(x, y, z)) + + +def _check_strings(arg_name, arg): + errorstr = arg_name + " must be an iterable of 3 string-types" + if len(arg) != 3: + raise ValueError(errorstr) + for s in arg: + if not isinstance(s, str): + raise TypeError(errorstr) + + +# Delayed import to avoid cyclic import problems: +from sympy.vector.vector import BaseVector diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/deloperator.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/deloperator.py new file mode 100644 index 0000000000000000000000000000000000000000..51c3c0caf42b5e5d372bd65907d8bae2bd563562 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/deloperator.py @@ -0,0 +1,121 @@ +from sympy.core import Basic +from sympy.vector.operators import gradient, divergence, curl + + +class Del(Basic): + """ + Represents the vector differential operator, usually represented in + mathematical expressions as the 'nabla' symbol. + """ + + def __new__(cls): + obj = super().__new__(cls) + obj._name = "delop" + return obj + + def gradient(self, scalar_field, doit=False): + """ + Returns the gradient of the given scalar field, as a + Vector instance. + + Parameters + ========== + + scalar_field : SymPy expression + The scalar field to calculate the gradient of. + + doit : bool + If True, the result is returned after calling .doit() on + each component. Else, the returned expression contains + Derivative instances + + Examples + ======== + + >>> from sympy.vector import CoordSys3D, Del + >>> C = CoordSys3D('C') + >>> delop = Del() + >>> delop.gradient(9) + 0 + >>> delop(C.x*C.y*C.z).doit() + C.y*C.z*C.i + C.x*C.z*C.j + C.x*C.y*C.k + + """ + + return gradient(scalar_field, doit=doit) + + __call__ = gradient + __call__.__doc__ = gradient.__doc__ + + def dot(self, vect, doit=False): + """ + Represents the dot product between this operator and a given + vector - equal to the divergence of the vector field. + + Parameters + ========== + + vect : Vector + The vector whose divergence is to be calculated. + + doit : bool + If True, the result is returned after calling .doit() on + each component. Else, the returned expression contains + Derivative instances + + Examples + ======== + + >>> from sympy.vector import CoordSys3D, Del + >>> delop = Del() + >>> C = CoordSys3D('C') + >>> delop.dot(C.x*C.i) + Derivative(C.x, C.x) + >>> v = C.x*C.y*C.z * (C.i + C.j + C.k) + >>> (delop & v).doit() + C.x*C.y + C.x*C.z + C.y*C.z + + """ + return divergence(vect, doit=doit) + + __and__ = dot + __and__.__doc__ = dot.__doc__ + + def cross(self, vect, doit=False): + """ + Represents the cross product between this operator and a given + vector - equal to the curl of the vector field. + + Parameters + ========== + + vect : Vector + The vector whose curl is to be calculated. + + doit : bool + If True, the result is returned after calling .doit() on + each component. Else, the returned expression contains + Derivative instances + + Examples + ======== + + >>> from sympy.vector import CoordSys3D, Del + >>> C = CoordSys3D('C') + >>> delop = Del() + >>> v = C.x*C.y*C.z * (C.i + C.j + C.k) + >>> delop.cross(v, doit = True) + (-C.x*C.y + C.x*C.z)*C.i + (C.x*C.y - C.y*C.z)*C.j + + (-C.x*C.z + C.y*C.z)*C.k + >>> (delop ^ C.i).doit() + 0 + + """ + + return curl(vect, doit=doit) + + __xor__ = cross + __xor__.__doc__ = cross.__doc__ + + def _sympystr(self, printer): + return self._name diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/dyadic.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/dyadic.py new file mode 100644 index 0000000000000000000000000000000000000000..980c6e6dad90ac095b7bd6d4228f507a7831b39f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/dyadic.py @@ -0,0 +1,285 @@ +from __future__ import annotations + +from sympy.vector.basisdependent import (BasisDependent, BasisDependentAdd, + BasisDependentMul, BasisDependentZero) +from sympy.core import S, Pow +from sympy.core.expr import AtomicExpr +from sympy.matrices.immutable import ImmutableDenseMatrix as Matrix +import sympy.vector + + +class Dyadic(BasisDependent): + """ + Super class for all Dyadic-classes. + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Dyadic_tensor + .. [2] Kane, T., Levinson, D. Dynamics Theory and Applications. 1985 + McGraw-Hill + + """ + + _op_priority = 13.0 + + _expr_type: type[Dyadic] + _mul_func: type[Dyadic] + _add_func: type[Dyadic] + _zero_func: type[Dyadic] + _base_func: type[Dyadic] + zero: DyadicZero + + @property + def components(self): + """ + Returns the components of this dyadic in the form of a + Python dictionary mapping BaseDyadic instances to the + corresponding measure numbers. + + """ + # The '_components' attribute is defined according to the + # subclass of Dyadic the instance belongs to. + return self._components + + def dot(self, other): + """ + Returns the dot product(also called inner product) of this + Dyadic, with another Dyadic or Vector. + If 'other' is a Dyadic, this returns a Dyadic. Else, it returns + a Vector (unless an error is encountered). + + Parameters + ========== + + other : Dyadic/Vector + The other Dyadic or Vector to take the inner product with + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> N = CoordSys3D('N') + >>> D1 = N.i.outer(N.j) + >>> D2 = N.j.outer(N.j) + >>> D1.dot(D2) + (N.i|N.j) + >>> D1.dot(N.j) + N.i + + """ + + Vector = sympy.vector.Vector + if isinstance(other, BasisDependentZero): + return Vector.zero + elif isinstance(other, Vector): + outvec = Vector.zero + for k, v in self.components.items(): + vect_dot = k.args[1].dot(other) + outvec += vect_dot * v * k.args[0] + return outvec + elif isinstance(other, Dyadic): + outdyad = Dyadic.zero + for k1, v1 in self.components.items(): + for k2, v2 in other.components.items(): + vect_dot = k1.args[1].dot(k2.args[0]) + outer_product = k1.args[0].outer(k2.args[1]) + outdyad += vect_dot * v1 * v2 * outer_product + return outdyad + else: + raise TypeError("Inner product is not defined for " + + str(type(other)) + " and Dyadics.") + + def __and__(self, other): + return self.dot(other) + + __and__.__doc__ = dot.__doc__ + + def cross(self, other): + """ + Returns the cross product between this Dyadic, and a Vector, as a + Vector instance. + + Parameters + ========== + + other : Vector + The Vector that we are crossing this Dyadic with + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> N = CoordSys3D('N') + >>> d = N.i.outer(N.i) + >>> d.cross(N.j) + (N.i|N.k) + + """ + + Vector = sympy.vector.Vector + if other == Vector.zero: + return Dyadic.zero + elif isinstance(other, Vector): + outdyad = Dyadic.zero + for k, v in self.components.items(): + cross_product = k.args[1].cross(other) + outer = k.args[0].outer(cross_product) + outdyad += v * outer + return outdyad + else: + raise TypeError(str(type(other)) + " not supported for " + + "cross with dyadics") + + def __xor__(self, other): + return self.cross(other) + + __xor__.__doc__ = cross.__doc__ + + def to_matrix(self, system, second_system=None): + """ + Returns the matrix form of the dyadic with respect to one or two + coordinate systems. + + Parameters + ========== + + system : CoordSys3D + The coordinate system that the rows and columns of the matrix + correspond to. If a second system is provided, this + only corresponds to the rows of the matrix. + second_system : CoordSys3D, optional, default=None + The coordinate system that the columns of the matrix correspond + to. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> N = CoordSys3D('N') + >>> v = N.i + 2*N.j + >>> d = v.outer(N.i) + >>> d.to_matrix(N) + Matrix([ + [1, 0, 0], + [2, 0, 0], + [0, 0, 0]]) + >>> from sympy import Symbol + >>> q = Symbol('q') + >>> P = N.orient_new_axis('P', q, N.k) + >>> d.to_matrix(N, P) + Matrix([ + [ cos(q), -sin(q), 0], + [2*cos(q), -2*sin(q), 0], + [ 0, 0, 0]]) + + """ + + if second_system is None: + second_system = system + + return Matrix([i.dot(self).dot(j) for i in system for j in + second_system]).reshape(3, 3) + + def _div_helper(one, other): + """ Helper for division involving dyadics """ + if isinstance(one, Dyadic) and isinstance(other, Dyadic): + raise TypeError("Cannot divide two dyadics") + elif isinstance(one, Dyadic): + return DyadicMul(one, Pow(other, S.NegativeOne)) + else: + raise TypeError("Cannot divide by a dyadic") + + +class BaseDyadic(Dyadic, AtomicExpr): + """ + Class to denote a base dyadic tensor component. + """ + + def __new__(cls, vector1, vector2): + Vector = sympy.vector.Vector + BaseVector = sympy.vector.BaseVector + VectorZero = sympy.vector.VectorZero + # Verify arguments + if not isinstance(vector1, (BaseVector, VectorZero)) or \ + not isinstance(vector2, (BaseVector, VectorZero)): + raise TypeError("BaseDyadic cannot be composed of non-base " + + "vectors") + # Handle special case of zero vector + elif vector1 == Vector.zero or vector2 == Vector.zero: + return Dyadic.zero + # Initialize instance + obj = super().__new__(cls, vector1, vector2) + obj._base_instance = obj + obj._measure_number = 1 + obj._components = {obj: S.One} + obj._sys = vector1._sys + obj._pretty_form = ('(' + vector1._pretty_form + '|' + + vector2._pretty_form + ')') + obj._latex_form = (r'\left(' + vector1._latex_form + r"{\middle|}" + + vector2._latex_form + r'\right)') + + return obj + + def _sympystr(self, printer): + return "({}|{})".format( + printer._print(self.args[0]), printer._print(self.args[1])) + + def _sympyrepr(self, printer): + return "BaseDyadic({}, {})".format( + printer._print(self.args[0]), printer._print(self.args[1])) + + +class DyadicMul(BasisDependentMul, Dyadic): + """ Products of scalars and BaseDyadics """ + + def __new__(cls, *args, **options): + obj = BasisDependentMul.__new__(cls, *args, **options) + return obj + + @property + def base_dyadic(self): + """ The BaseDyadic involved in the product. """ + return self._base_instance + + @property + def measure_number(self): + """ The scalar expression involved in the definition of + this DyadicMul. + """ + return self._measure_number + + +class DyadicAdd(BasisDependentAdd, Dyadic): + """ Class to hold dyadic sums """ + + def __new__(cls, *args, **options): + obj = BasisDependentAdd.__new__(cls, *args, **options) + return obj + + def _sympystr(self, printer): + items = list(self.components.items()) + items.sort(key=lambda x: x[0].__str__()) + return " + ".join(printer._print(k * v) for k, v in items) + + +class DyadicZero(BasisDependentZero, Dyadic): + """ + Class to denote a zero dyadic + """ + + _op_priority = 13.1 + _pretty_form = '(0|0)' + _latex_form = r'(\mathbf{\hat{0}}|\mathbf{\hat{0}})' + + def __new__(cls): + obj = BasisDependentZero.__new__(cls) + return obj + + +Dyadic._expr_type = Dyadic +Dyadic._mul_func = DyadicMul +Dyadic._add_func = DyadicAdd +Dyadic._zero_func = DyadicZero +Dyadic._base_func = BaseDyadic +Dyadic.zero = DyadicZero() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/functions.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/functions.py new file mode 100644 index 0000000000000000000000000000000000000000..b78df8ae2e182f3e571ca7fa8bfabd39bf99d26e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/functions.py @@ -0,0 +1,513 @@ +from sympy.vector.coordsysrect import CoordSys3D +from sympy.vector.deloperator import Del +from sympy.vector.scalar import BaseScalar +from sympy.vector.vector import Vector, BaseVector +from sympy.vector.operators import gradient, curl, divergence +from sympy.core.function import diff +from sympy.core.singleton import S +from sympy.integrals.integrals import integrate +from sympy.core import sympify +from sympy.vector.dyadic import Dyadic + + +def express(expr, system, system2=None, variables=False): + """ + Global function for 'express' functionality. + + Re-expresses a Vector, Dyadic or scalar(sympyfiable) in the given + coordinate system. + + If 'variables' is True, then the coordinate variables (base scalars) + of other coordinate systems present in the vector/scalar field or + dyadic are also substituted in terms of the base scalars of the + given system. + + Parameters + ========== + + expr : Vector/Dyadic/scalar(sympyfiable) + The expression to re-express in CoordSys3D 'system' + + system: CoordSys3D + The coordinate system the expr is to be expressed in + + system2: CoordSys3D + The other coordinate system required for re-expression + (only for a Dyadic Expr) + + variables : boolean + Specifies whether to substitute the coordinate variables present + in expr, in terms of those of parameter system + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> from sympy import Symbol, cos, sin + >>> N = CoordSys3D('N') + >>> q = Symbol('q') + >>> B = N.orient_new_axis('B', q, N.k) + >>> from sympy.vector import express + >>> express(B.i, N) + (cos(q))*N.i + (sin(q))*N.j + >>> express(N.x, B, variables=True) + B.x*cos(q) - B.y*sin(q) + >>> d = N.i.outer(N.i) + >>> express(d, B, N) == (cos(q))*(B.i|N.i) + (-sin(q))*(B.j|N.i) + True + + """ + + if expr in (0, Vector.zero): + return expr + + if not isinstance(system, CoordSys3D): + raise TypeError("system should be a CoordSys3D \ + instance") + + if isinstance(expr, Vector): + if system2 is not None: + raise ValueError("system2 should not be provided for \ + Vectors") + # Given expr is a Vector + if variables: + # If variables attribute is True, substitute + # the coordinate variables in the Vector + system_list = {x.system for x in expr.atoms(BaseScalar, BaseVector)} - {system} + subs_dict = {} + for f in system_list: + subs_dict.update(f.scalar_map(system)) + expr = expr.subs(subs_dict) + # Re-express in this coordinate system + outvec = Vector.zero + parts = expr.separate() + for x in parts: + if x != system: + temp = system.rotation_matrix(x) * parts[x].to_matrix(x) + outvec += matrix_to_vector(temp, system) + else: + outvec += parts[x] + return outvec + + elif isinstance(expr, Dyadic): + if system2 is None: + system2 = system + if not isinstance(system2, CoordSys3D): + raise TypeError("system2 should be a CoordSys3D \ + instance") + outdyad = Dyadic.zero + var = variables + for k, v in expr.components.items(): + outdyad += (express(v, system, variables=var) * + (express(k.args[0], system, variables=var) | + express(k.args[1], system2, variables=var))) + + return outdyad + + else: + if system2 is not None: + raise ValueError("system2 should not be provided for \ + Vectors") + if variables: + # Given expr is a scalar field + system_set = set() + expr = sympify(expr) + # Substitute all the coordinate variables + for x in expr.atoms(BaseScalar): + if x.system != system: + system_set.add(x.system) + subs_dict = {} + for f in system_set: + subs_dict.update(f.scalar_map(system)) + return expr.subs(subs_dict) + return expr + + +def directional_derivative(field, direction_vector): + """ + Returns the directional derivative of a scalar or vector field computed + along a given vector in coordinate system which parameters are expressed. + + Parameters + ========== + + field : Vector or Scalar + The scalar or vector field to compute the directional derivative of + + direction_vector : Vector + The vector to calculated directional derivative along them. + + + Examples + ======== + + >>> from sympy.vector import CoordSys3D, directional_derivative + >>> R = CoordSys3D('R') + >>> f1 = R.x*R.y*R.z + >>> v1 = 3*R.i + 4*R.j + R.k + >>> directional_derivative(f1, v1) + R.x*R.y + 4*R.x*R.z + 3*R.y*R.z + >>> f2 = 5*R.x**2*R.z + >>> directional_derivative(f2, v1) + 5*R.x**2 + 30*R.x*R.z + + """ + from sympy.vector.operators import _get_coord_systems + coord_sys = _get_coord_systems(field) + if len(coord_sys) > 0: + # TODO: This gets a random coordinate system in case of multiple ones: + coord_sys = next(iter(coord_sys)) + field = express(field, coord_sys, variables=True) + i, j, k = coord_sys.base_vectors() + x, y, z = coord_sys.base_scalars() + out = Vector.dot(direction_vector, i) * diff(field, x) + out += Vector.dot(direction_vector, j) * diff(field, y) + out += Vector.dot(direction_vector, k) * diff(field, z) + if out == 0 and isinstance(field, Vector): + out = Vector.zero + return out + elif isinstance(field, Vector): + return Vector.zero + else: + return S.Zero + + +def laplacian(expr): + """ + Return the laplacian of the given field computed in terms of + the base scalars of the given coordinate system. + + Parameters + ========== + + expr : SymPy Expr or Vector + expr denotes a scalar or vector field. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D, laplacian + >>> R = CoordSys3D('R') + >>> f = R.x**2*R.y**5*R.z + >>> laplacian(f) + 20*R.x**2*R.y**3*R.z + 2*R.y**5*R.z + >>> f = R.x**2*R.i + R.y**3*R.j + R.z**4*R.k + >>> laplacian(f) + 2*R.i + 6*R.y*R.j + 12*R.z**2*R.k + + """ + + delop = Del() + if expr.is_Vector: + return (gradient(divergence(expr)) - curl(curl(expr))).doit() + return delop.dot(delop(expr)).doit() + + +def is_conservative(field): + """ + Checks if a field is conservative. + + Parameters + ========== + + field : Vector + The field to check for conservative property + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> from sympy.vector import is_conservative + >>> R = CoordSys3D('R') + >>> is_conservative(R.y*R.z*R.i + R.x*R.z*R.j + R.x*R.y*R.k) + True + >>> is_conservative(R.z*R.j) + False + + """ + + # Field is conservative irrespective of system + # Take the first coordinate system in the result of the + # separate method of Vector + if not isinstance(field, Vector): + raise TypeError("field should be a Vector") + if field == Vector.zero: + return True + return curl(field).simplify() == Vector.zero + + +def is_solenoidal(field): + """ + Checks if a field is solenoidal. + + Parameters + ========== + + field : Vector + The field to check for solenoidal property + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> from sympy.vector import is_solenoidal + >>> R = CoordSys3D('R') + >>> is_solenoidal(R.y*R.z*R.i + R.x*R.z*R.j + R.x*R.y*R.k) + True + >>> is_solenoidal(R.y * R.j) + False + + """ + + # Field is solenoidal irrespective of system + # Take the first coordinate system in the result of the + # separate method in Vector + if not isinstance(field, Vector): + raise TypeError("field should be a Vector") + if field == Vector.zero: + return True + return divergence(field).simplify() is S.Zero + + +def scalar_potential(field, coord_sys): + """ + Returns the scalar potential function of a field in a given + coordinate system (without the added integration constant). + + Parameters + ========== + + field : Vector + The vector field whose scalar potential function is to be + calculated + + coord_sys : CoordSys3D + The coordinate system to do the calculation in + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> from sympy.vector import scalar_potential, gradient + >>> R = CoordSys3D('R') + >>> scalar_potential(R.k, R) == R.z + True + >>> scalar_field = 2*R.x**2*R.y*R.z + >>> grad_field = gradient(scalar_field) + >>> scalar_potential(grad_field, R) + 2*R.x**2*R.y*R.z + + """ + + # Check whether field is conservative + if not is_conservative(field): + raise ValueError("Field is not conservative") + if field == Vector.zero: + return S.Zero + # Express the field exntirely in coord_sys + # Substitute coordinate variables also + if not isinstance(coord_sys, CoordSys3D): + raise TypeError("coord_sys must be a CoordSys3D") + field = express(field, coord_sys, variables=True) + dimensions = coord_sys.base_vectors() + scalars = coord_sys.base_scalars() + # Calculate scalar potential function + temp_function = integrate(field.dot(dimensions[0]), scalars[0]) + for i, dim in enumerate(dimensions[1:]): + partial_diff = diff(temp_function, scalars[i + 1]) + partial_diff = field.dot(dim) - partial_diff + temp_function += integrate(partial_diff, scalars[i + 1]) + return temp_function + + +def scalar_potential_difference(field, coord_sys, point1, point2): + """ + Returns the scalar potential difference between two points in a + certain coordinate system, wrt a given field. + + If a scalar field is provided, its values at the two points are + considered. If a conservative vector field is provided, the values + of its scalar potential function at the two points are used. + + Returns (potential at point2) - (potential at point1) + + The position vectors of the two Points are calculated wrt the + origin of the coordinate system provided. + + Parameters + ========== + + field : Vector/Expr + The field to calculate wrt + + coord_sys : CoordSys3D + The coordinate system to do the calculations in + + point1 : Point + The initial Point in given coordinate system + + position2 : Point + The second Point in the given coordinate system + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> from sympy.vector import scalar_potential_difference + >>> R = CoordSys3D('R') + >>> P = R.origin.locate_new('P', R.x*R.i + R.y*R.j + R.z*R.k) + >>> vectfield = 4*R.x*R.y*R.i + 2*R.x**2*R.j + >>> scalar_potential_difference(vectfield, R, R.origin, P) + 2*R.x**2*R.y + >>> Q = R.origin.locate_new('O', 3*R.i + R.j + 2*R.k) + >>> scalar_potential_difference(vectfield, R, P, Q) + -2*R.x**2*R.y + 18 + + """ + + if not isinstance(coord_sys, CoordSys3D): + raise TypeError("coord_sys must be a CoordSys3D") + if isinstance(field, Vector): + # Get the scalar potential function + scalar_fn = scalar_potential(field, coord_sys) + else: + # Field is a scalar + scalar_fn = field + # Express positions in required coordinate system + origin = coord_sys.origin + position1 = express(point1.position_wrt(origin), coord_sys, + variables=True) + position2 = express(point2.position_wrt(origin), coord_sys, + variables=True) + # Get the two positions as substitution dicts for coordinate variables + subs_dict1 = {} + subs_dict2 = {} + scalars = coord_sys.base_scalars() + for i, x in enumerate(coord_sys.base_vectors()): + subs_dict1[scalars[i]] = x.dot(position1) + subs_dict2[scalars[i]] = x.dot(position2) + return scalar_fn.subs(subs_dict2) - scalar_fn.subs(subs_dict1) + + +def matrix_to_vector(matrix, system): + """ + Converts a vector in matrix form to a Vector instance. + + It is assumed that the elements of the Matrix represent the + measure numbers of the components of the vector along basis + vectors of 'system'. + + Parameters + ========== + + matrix : SymPy Matrix, Dimensions: (3, 1) + The matrix to be converted to a vector + + system : CoordSys3D + The coordinate system the vector is to be defined in + + Examples + ======== + + >>> from sympy import ImmutableMatrix as Matrix + >>> m = Matrix([1, 2, 3]) + >>> from sympy.vector import CoordSys3D, matrix_to_vector + >>> C = CoordSys3D('C') + >>> v = matrix_to_vector(m, C) + >>> v + C.i + 2*C.j + 3*C.k + >>> v.to_matrix(C) == m + True + + """ + + outvec = Vector.zero + vects = system.base_vectors() + for i, x in enumerate(matrix): + outvec += x * vects[i] + return outvec + + +def _path(from_object, to_object): + """ + Calculates the 'path' of objects starting from 'from_object' + to 'to_object', along with the index of the first common + ancestor in the tree. + + Returns (index, list) tuple. + """ + + if from_object._root != to_object._root: + raise ValueError("No connecting path found between " + + str(from_object) + " and " + str(to_object)) + + other_path = [] + obj = to_object + while obj._parent is not None: + other_path.append(obj) + obj = obj._parent + other_path.append(obj) + object_set = set(other_path) + from_path = [] + obj = from_object + while obj not in object_set: + from_path.append(obj) + obj = obj._parent + index = len(from_path) + from_path.extend(other_path[other_path.index(obj)::-1]) + return index, from_path + + +def orthogonalize(*vlist, orthonormal=False): + """ + Takes a sequence of independent vectors and orthogonalizes them + using the Gram - Schmidt process. Returns a list of + orthogonal or orthonormal vectors. + + Parameters + ========== + + vlist : sequence of independent vectors to be made orthogonal. + + orthonormal : Optional parameter + Set to True if the vectors returned should be + orthonormal. + Default: False + + Examples + ======== + + >>> from sympy.vector.coordsysrect import CoordSys3D + >>> from sympy.vector.functions import orthogonalize + >>> C = CoordSys3D('C') + >>> i, j, k = C.base_vectors() + >>> v1 = i + 2*j + >>> v2 = 2*i + 3*j + >>> orthogonalize(v1, v2) + [C.i + 2*C.j, 2/5*C.i + (-1/5)*C.j] + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Gram-Schmidt_process + + """ + + if not all(isinstance(vec, Vector) for vec in vlist): + raise TypeError('Each element must be of Type Vector') + + ortho_vlist = [] + for i, term in enumerate(vlist): + for j in range(i): + term -= ortho_vlist[j].projection(vlist[i]) + # TODO : The following line introduces a performance issue + # and needs to be changed once a good solution for issue #10279 is + # found. + if term.equals(Vector.zero): + raise ValueError("Vector set not linearly independent") + ortho_vlist.append(term) + + if orthonormal: + ortho_vlist = [vec.normalize() for vec in ortho_vlist] + + return ortho_vlist diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/implicitregion.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/implicitregion.py new file mode 100644 index 0000000000000000000000000000000000000000..ed2d55a1be8b1eaca71d08b632a94886a2b0269c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/implicitregion.py @@ -0,0 +1,506 @@ +from sympy.core.numbers import Rational +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.complexes import sign +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.polys.polytools import gcd +from sympy.sets.sets import Complement +from sympy.core import Basic, Tuple, diff, expand, Eq, Integer +from sympy.core.sorting import ordered +from sympy.core.symbol import _symbol +from sympy.solvers import solveset, nonlinsolve, diophantine +from sympy.polys import total_degree +from sympy.geometry import Point +from sympy.ntheory.factor_ import core + + +class ImplicitRegion(Basic): + """ + Represents an implicit region in space. + + Examples + ======== + + >>> from sympy import Eq + >>> from sympy.abc import x, y, z, t + >>> from sympy.vector import ImplicitRegion + + >>> ImplicitRegion((x, y), x**2 + y**2 - 4) + ImplicitRegion((x, y), x**2 + y**2 - 4) + >>> ImplicitRegion((x, y), Eq(y*x, 1)) + ImplicitRegion((x, y), x*y - 1) + + >>> parabola = ImplicitRegion((x, y), y**2 - 4*x) + >>> parabola.degree + 2 + >>> parabola.equation + -4*x + y**2 + >>> parabola.rational_parametrization(t) + (4/t**2, 4/t) + + >>> r = ImplicitRegion((x, y, z), Eq(z, x**2 + y**2)) + >>> r.variables + (x, y, z) + >>> r.singular_points() + EmptySet + >>> r.regular_point() + (-10, -10, 200) + + Parameters + ========== + + variables : tuple to map variables in implicit equation to base scalars. + + equation : An expression or Eq denoting the implicit equation of the region. + + """ + def __new__(cls, variables, equation): + if not isinstance(variables, Tuple): + variables = Tuple(*variables) + + if isinstance(equation, Eq): + equation = equation.lhs - equation.rhs + + return super().__new__(cls, variables, equation) + + @property + def variables(self): + return self.args[0] + + @property + def equation(self): + return self.args[1] + + @property + def degree(self): + return total_degree(self.equation) + + def regular_point(self): + """ + Returns a point on the implicit region. + + Examples + ======== + + >>> from sympy.abc import x, y, z + >>> from sympy.vector import ImplicitRegion + >>> circle = ImplicitRegion((x, y), (x + 2)**2 + (y - 3)**2 - 16) + >>> circle.regular_point() + (-2, -1) + >>> parabola = ImplicitRegion((x, y), x**2 - 4*y) + >>> parabola.regular_point() + (0, 0) + >>> r = ImplicitRegion((x, y, z), (x + y + z)**4) + >>> r.regular_point() + (-10, -10, 20) + + References + ========== + + - Erik Hillgarter, "Rational Points on Conics", Diploma Thesis, RISC-Linz, + J. Kepler Universitat Linz, 1996. Available: + https://www3.risc.jku.at/publications/download/risc_1355/Rational%20Points%20on%20Conics.pdf + + """ + equation = self.equation + + if len(self.variables) == 1: + return (list(solveset(equation, self.variables[0], domain=S.Reals))[0],) + elif len(self.variables) == 2: + + if self.degree == 2: + coeffs = a, b, c, d, e, f = conic_coeff(self.variables, equation) + + if b**2 == 4*a*c: + x_reg, y_reg = self._regular_point_parabola(*coeffs) + else: + x_reg, y_reg = self._regular_point_ellipse(*coeffs) + return x_reg, y_reg + + if len(self.variables) == 3: + x, y, z = self.variables + + for x_reg in range(-10, 10): + for y_reg in range(-10, 10): + if not solveset(equation.subs({x: x_reg, y: y_reg}), self.variables[2], domain=S.Reals).is_empty: + return (x_reg, y_reg, list(solveset(equation.subs({x: x_reg, y: y_reg})))[0]) + + if len(self.singular_points()) != 0: + return list[self.singular_points()][0] + + raise NotImplementedError() + + def _regular_point_parabola(self, a, b, c, d, e, f): + ok = (a, d) != (0, 0) and (c, e) != (0, 0) and b**2 == 4*a*c and (a, c) != (0, 0) + + if not ok: + raise ValueError("Rational Point on the conic does not exist") + + if a != 0: + d_dash, f_dash = (4*a*e - 2*b*d, 4*a*f - d**2) + if d_dash != 0: + y_reg = -f_dash/d_dash + x_reg = -(d + b*y_reg)/(2*a) + else: + ok = False + elif c != 0: + d_dash, f_dash = (4*c*d - 2*b*e, 4*c*f - e**2) + if d_dash != 0: + x_reg = -f_dash/d_dash + y_reg = -(e + b*x_reg)/(2*c) + else: + ok = False + + if ok: + return x_reg, y_reg + else: + raise ValueError("Rational Point on the conic does not exist") + + def _regular_point_ellipse(self, a, b, c, d, e, f): + D = 4*a*c - b**2 + ok = D + + if not ok: + raise ValueError("Rational Point on the conic does not exist") + + if a == 0 and c == 0: + K = -1 + L = 4*(d*e - b*f) + elif c != 0: + K = D + L = 4*c**2*d**2 - 4*b*c*d*e + 4*a*c*e**2 + 4*b**2*c*f - 16*a*c**2*f + else: + K = D + L = 4*a**2*e**2 - 4*b*a*d*e + 4*b**2*a*f + + ok = L != 0 and not(K > 0 and L < 0) + if not ok: + raise ValueError("Rational Point on the conic does not exist") + + K = Rational(K).limit_denominator(10**12) + L = Rational(L).limit_denominator(10**12) + + k1, k2 = K.p, K.q + l1, l2 = L.p, L.q + g = gcd(k2, l2) + + a1 = (l2*k2)/g + b1 = (k1*l2)/g + c1 = -(l1*k2)/g + a2 = sign(a1)*core(abs(a1), 2) + r1 = sqrt(a1/a2) + b2 = sign(b1)*core(abs(b1), 2) + r2 = sqrt(b1/b2) + c2 = sign(c1)*core(abs(c1), 2) + r3 = sqrt(c1/c2) + + g = gcd(gcd(a2, b2), c2) + a2 = a2/g + b2 = b2/g + c2 = c2/g + + g1 = gcd(a2, b2) + a2 = a2/g1 + b2 = b2/g1 + c2 = c2*g1 + + g2 = gcd(a2,c2) + a2 = a2/g2 + b2 = b2*g2 + c2 = c2/g2 + + g3 = gcd(b2, c2) + a2 = a2*g3 + b2 = b2/g3 + c2 = c2/g3 + + x, y, z = symbols("x y z") + eq = a2*x**2 + b2*y**2 + c2*z**2 + + solutions = diophantine(eq) + + if len(solutions) == 0: + raise ValueError("Rational Point on the conic does not exist") + + flag = False + for sol in solutions: + syms = Tuple(*sol).free_symbols + rep = dict.fromkeys(syms, 3) + sol_z = sol[2] + + if sol_z == 0: + flag = True + continue + + if not isinstance(sol_z, (int, Integer)): + syms_z = sol_z.free_symbols + + if len(syms_z) == 1: + p = next(iter(syms_z)) + p_values = Complement(S.Integers, solveset(Eq(sol_z, 0), p, S.Integers)) + rep[p] = next(iter(p_values)) + + if len(syms_z) == 2: + p, q = list(ordered(syms_z)) + + for i in S.Integers: + subs_sol_z = sol_z.subs(p, i) + q_values = Complement(S.Integers, solveset(Eq(subs_sol_z, 0), q, S.Integers)) + + if not q_values.is_empty: + rep[p] = i + rep[q] = next(iter(q_values)) + break + + if len(syms) != 0: + x, y, z = tuple(s.subs(rep) for s in sol) + else: + x, y, z = sol + flag = False + break + + if flag: + raise ValueError("Rational Point on the conic does not exist") + + x = (x*g3)/r1 + y = (y*g2)/r2 + z = (z*g1)/r3 + x = x/z + y = y/z + + if a == 0 and c == 0: + x_reg = (x + y - 2*e)/(2*b) + y_reg = (x - y - 2*d)/(2*b) + elif c != 0: + x_reg = (x - 2*d*c + b*e)/K + y_reg = (y - b*x_reg - e)/(2*c) + else: + y_reg = (x - 2*e*a + b*d)/K + x_reg = (y - b*y_reg - d)/(2*a) + + return x_reg, y_reg + + def singular_points(self): + """ + Returns a set of singular points of the region. + + The singular points are those points on the region + where all partial derivatives vanish. + + Examples + ======== + + >>> from sympy.abc import x, y + >>> from sympy.vector import ImplicitRegion + >>> I = ImplicitRegion((x, y), (y-1)**2 -x**3 + 2*x**2 -x) + >>> I.singular_points() + {(1, 1)} + + """ + eq_list = [self.equation] + for var in self.variables: + eq_list += [diff(self.equation, var)] + + return nonlinsolve(eq_list, list(self.variables)) + + def multiplicity(self, point): + """ + Returns the multiplicity of a singular point on the region. + + A singular point (x,y) of region is said to be of multiplicity m + if all the partial derivatives off to order m - 1 vanish there. + + Examples + ======== + + >>> from sympy.abc import x, y, z + >>> from sympy.vector import ImplicitRegion + >>> I = ImplicitRegion((x, y, z), x**2 + y**3 - z**4) + >>> I.singular_points() + {(0, 0, 0)} + >>> I.multiplicity((0, 0, 0)) + 2 + + """ + if isinstance(point, Point): + point = point.args + + modified_eq = self.equation + + for i, var in enumerate(self.variables): + modified_eq = modified_eq.subs(var, var + point[i]) + modified_eq = expand(modified_eq) + + if len(modified_eq.args) != 0: + terms = modified_eq.args + m = min(total_degree(term) for term in terms) + else: + terms = modified_eq + m = total_degree(terms) + + return m + + def rational_parametrization(self, parameters=('t', 's'), reg_point=None): + """ + Returns the rational parametrization of implicit region. + + Examples + ======== + + >>> from sympy import Eq + >>> from sympy.abc import x, y, z, s, t + >>> from sympy.vector import ImplicitRegion + + >>> parabola = ImplicitRegion((x, y), y**2 - 4*x) + >>> parabola.rational_parametrization() + (4/t**2, 4/t) + + >>> circle = ImplicitRegion((x, y), Eq(x**2 + y**2, 4)) + >>> circle.rational_parametrization() + (4*t/(t**2 + 1), 4*t**2/(t**2 + 1) - 2) + + >>> I = ImplicitRegion((x, y), x**3 + x**2 - y**2) + >>> I.rational_parametrization() + (t**2 - 1, t*(t**2 - 1)) + + >>> cubic_curve = ImplicitRegion((x, y), x**3 + x**2 - y**2) + >>> cubic_curve.rational_parametrization(parameters=(t)) + (t**2 - 1, t*(t**2 - 1)) + + >>> sphere = ImplicitRegion((x, y, z), x**2 + y**2 + z**2 - 4) + >>> sphere.rational_parametrization(parameters=(t, s)) + (-2 + 4/(s**2 + t**2 + 1), 4*s/(s**2 + t**2 + 1), 4*t/(s**2 + t**2 + 1)) + + For some conics, regular_points() is unable to find a point on curve. + To calulcate the parametric representation in such cases, user need + to determine a point on the region and pass it using reg_point. + + >>> c = ImplicitRegion((x, y), (x - 1/2)**2 + (y)**2 - (1/4)**2) + >>> c.rational_parametrization(reg_point=(3/4, 0)) + (0.75 - 0.5/(t**2 + 1), -0.5*t/(t**2 + 1)) + + References + ========== + + - Christoph M. Hoffmann, "Conversion Methods between Parametric and + Implicit Curves and Surfaces", Purdue e-Pubs, 1990. Available: + https://docs.lib.purdue.edu/cgi/viewcontent.cgi?article=1827&context=cstech + + """ + equation = self.equation + degree = self.degree + + if degree == 1: + if len(self.variables) == 1: + return (equation,) + elif len(self.variables) == 2: + x, y = self.variables + y_par = list(solveset(equation, y))[0] + return x, y_par + else: + raise NotImplementedError() + + point = () + + # Finding the (n - 1) fold point of the monoid of degree + if degree == 2: + # For degree 2 curves, either a regular point or a singular point can be used. + if reg_point is not None: + # Using point provided by the user as regular point + point = reg_point + else: + if len(self.singular_points()) != 0: + point = list(self.singular_points())[0] + else: + point = self.regular_point() + + if len(self.singular_points()) != 0: + singular_points = self.singular_points() + for spoint in singular_points: + syms = Tuple(*spoint).free_symbols + rep = dict.fromkeys(syms, 2) + + if len(syms) != 0: + spoint = tuple(s.subs(rep) for s in spoint) + + if self.multiplicity(spoint) == degree - 1: + point = spoint + break + + if len(point) == 0: + # The region in not a monoid + raise NotImplementedError() + + modified_eq = equation + + # Shifting the region such that fold point moves to origin + for i, var in enumerate(self.variables): + modified_eq = modified_eq.subs(var, var + point[i]) + modified_eq = expand(modified_eq) + + hn = hn_1 = 0 + for term in modified_eq.args: + if total_degree(term) == degree: + hn += term + else: + hn_1 += term + + hn_1 = -1*hn_1 + + if not isinstance(parameters, tuple): + parameters = (parameters,) + + if len(self.variables) == 2: + + parameter1 = parameters[0] + if parameter1 == 's': + # To avoid name conflict between parameters + s = _symbol('s_', real=True) + else: + s = _symbol('s', real=True) + t = _symbol(parameter1, real=True) + + hn = hn.subs({self.variables[0]: s, self.variables[1]: t}) + hn_1 = hn_1.subs({self.variables[0]: s, self.variables[1]: t}) + + x_par = (s*(hn_1/hn)).subs(s, 1) + point[0] + y_par = (t*(hn_1/hn)).subs(s, 1) + point[1] + + return x_par, y_par + + elif len(self.variables) == 3: + + parameter1, parameter2 = parameters + if 'r' in parameters: + # To avoid name conflict between parameters + r = _symbol('r_', real=True) + else: + r = _symbol('r', real=True) + s = _symbol(parameter2, real=True) + t = _symbol(parameter1, real=True) + + hn = hn.subs({self.variables[0]: r, self.variables[1]: s, self.variables[2]: t}) + hn_1 = hn_1.subs({self.variables[0]: r, self.variables[1]: s, self.variables[2]: t}) + + x_par = (r*(hn_1/hn)).subs(r, 1) + point[0] + y_par = (s*(hn_1/hn)).subs(r, 1) + point[1] + z_par = (t*(hn_1/hn)).subs(r, 1) + point[2] + + return x_par, y_par, z_par + + raise NotImplementedError() + +def conic_coeff(variables, equation): + if total_degree(equation) != 2: + raise ValueError() + x = variables[0] + y = variables[1] + + equation = expand(equation) + a = equation.coeff(x**2) + b = equation.coeff(x*y) + c = equation.coeff(y**2) + d = equation.coeff(x, 1).coeff(y, 0) + e = equation.coeff(y, 1).coeff(x, 0) + f = equation.coeff(x, 0).coeff(y, 0) + return a, b, c, d, e, f diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/integrals.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/integrals.py new file mode 100644 index 0000000000000000000000000000000000000000..a6451c182f214b20b1105eb0a4dc243455c9d126 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/integrals.py @@ -0,0 +1,206 @@ +from sympy.core import Basic, diff +from sympy.core.singleton import S +from sympy.core.sorting import default_sort_key +from sympy.matrices import Matrix +from sympy.integrals import Integral, integrate +from sympy.geometry.entity import GeometryEntity +from sympy.simplify.simplify import simplify +from sympy.utilities.iterables import topological_sort +from sympy.vector import (CoordSys3D, Vector, ParametricRegion, + parametric_region_list, ImplicitRegion) +from sympy.vector.operators import _get_coord_systems + + +class ParametricIntegral(Basic): + """ + Represents integral of a scalar or vector field + over a Parametric Region + + Examples + ======== + + >>> from sympy import cos, sin, pi + >>> from sympy.vector import CoordSys3D, ParametricRegion, ParametricIntegral + >>> from sympy.abc import r, t, theta, phi + + >>> C = CoordSys3D('C') + >>> curve = ParametricRegion((3*t - 2, t + 1), (t, 1, 2)) + >>> ParametricIntegral(C.x, curve) + 5*sqrt(10)/2 + >>> length = ParametricIntegral(1, curve) + >>> length + sqrt(10) + >>> semisphere = ParametricRegion((2*sin(phi)*cos(theta), 2*sin(phi)*sin(theta), 2*cos(phi)),\ + (theta, 0, 2*pi), (phi, 0, pi/2)) + >>> ParametricIntegral(C.z, semisphere) + 8*pi + + >>> ParametricIntegral(C.j + C.k, ParametricRegion((r*cos(theta), r*sin(theta)), r, theta)) + 0 + + """ + + def __new__(cls, field, parametricregion): + + coord_set = _get_coord_systems(field) + + if len(coord_set) == 0: + coord_sys = CoordSys3D('C') + elif len(coord_set) > 1: + raise ValueError + else: + coord_sys = next(iter(coord_set)) + + if parametricregion.dimensions == 0: + return S.Zero + + base_vectors = coord_sys.base_vectors() + base_scalars = coord_sys.base_scalars() + + parametricfield = field + + r = Vector.zero + for i in range(len(parametricregion.definition)): + r += base_vectors[i]*parametricregion.definition[i] + + if len(coord_set) != 0: + for i in range(len(parametricregion.definition)): + parametricfield = parametricfield.subs(base_scalars[i], parametricregion.definition[i]) + + if parametricregion.dimensions == 1: + parameter = parametricregion.parameters[0] + + r_diff = diff(r, parameter) + lower, upper = parametricregion.limits[parameter][0], parametricregion.limits[parameter][1] + + if isinstance(parametricfield, Vector): + integrand = simplify(r_diff.dot(parametricfield)) + else: + integrand = simplify(r_diff.magnitude()*parametricfield) + + result = integrate(integrand, (parameter, lower, upper)) + + elif parametricregion.dimensions == 2: + u, v = cls._bounds_case(parametricregion.parameters, parametricregion.limits) + + r_u = diff(r, u) + r_v = diff(r, v) + normal_vector = simplify(r_u.cross(r_v)) + + if isinstance(parametricfield, Vector): + integrand = parametricfield.dot(normal_vector) + else: + integrand = parametricfield*normal_vector.magnitude() + + integrand = simplify(integrand) + + lower_u, upper_u = parametricregion.limits[u][0], parametricregion.limits[u][1] + lower_v, upper_v = parametricregion.limits[v][0], parametricregion.limits[v][1] + + result = integrate(integrand, (u, lower_u, upper_u), (v, lower_v, upper_v)) + + else: + variables = cls._bounds_case(parametricregion.parameters, parametricregion.limits) + coeff = Matrix(parametricregion.definition).jacobian(variables).det() + integrand = simplify(parametricfield*coeff) + + l = [(var, parametricregion.limits[var][0], parametricregion.limits[var][1]) for var in variables] + result = integrate(integrand, *l) + + if not isinstance(result, Integral): + return result + else: + return super().__new__(cls, field, parametricregion) + + @classmethod + def _bounds_case(cls, parameters, limits): + + V = list(limits.keys()) + E = [] + + for p in V: + lower_p = limits[p][0] + upper_p = limits[p][1] + + lower_p = lower_p.atoms() + upper_p = upper_p.atoms() + E.extend((p, q) for q in V if p != q and + (lower_p.issuperset({q}) or upper_p.issuperset({q}))) + + if not E: + return parameters + else: + return topological_sort((V, E), key=default_sort_key) + + @property + def field(self): + return self.args[0] + + @property + def parametricregion(self): + return self.args[1] + + +def vector_integrate(field, *region): + """ + Compute the integral of a vector/scalar field + over a a region or a set of parameters. + + Examples + ======== + >>> from sympy.vector import CoordSys3D, ParametricRegion, vector_integrate + >>> from sympy.abc import x, y, t + >>> C = CoordSys3D('C') + + >>> region = ParametricRegion((t, t**2), (t, 1, 5)) + >>> vector_integrate(C.x*C.i, region) + 12 + + Integrals over some objects of geometry module can also be calculated. + + >>> from sympy.geometry import Point, Circle, Triangle + >>> c = Circle(Point(0, 2), 5) + >>> vector_integrate(C.x**2 + C.y**2, c) + 290*pi + >>> triangle = Triangle(Point(-2, 3), Point(2, 3), Point(0, 5)) + >>> vector_integrate(3*C.x**2*C.y*C.i + C.j, triangle) + -8 + + Integrals over some simple implicit regions can be computed. But in most cases, + it takes too long to compute over them. This is due to the expressions of parametric + representation becoming large. + + >>> from sympy.vector import ImplicitRegion + >>> c2 = ImplicitRegion((x, y), (x - 2)**2 + (y - 1)**2 - 9) + >>> vector_integrate(1, c2) + 6*pi + + Integral of fields with respect to base scalars: + + >>> vector_integrate(12*C.y**3, (C.y, 1, 3)) + 240 + >>> vector_integrate(C.x**2*C.z, C.x) + C.x**3*C.z/3 + >>> vector_integrate(C.x*C.i - C.y*C.k, C.x) + (Integral(C.x, C.x))*C.i + (Integral(-C.y, C.x))*C.k + >>> _.doit() + C.x**2/2*C.i + (-C.x*C.y)*C.k + + """ + if len(region) == 1: + if isinstance(region[0], ParametricRegion): + return ParametricIntegral(field, region[0]) + + if isinstance(region[0], ImplicitRegion): + region = parametric_region_list(region[0])[0] + return vector_integrate(field, region) + + if isinstance(region[0], GeometryEntity): + regions_list = parametric_region_list(region[0]) + + result = 0 + for reg in regions_list: + result += vector_integrate(field, reg) + return result + + return integrate(field, *region) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/kind.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/kind.py new file mode 100644 index 0000000000000000000000000000000000000000..c6c04896b34c9c92c3fb340d94985df859e5877d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/kind.py @@ -0,0 +1,67 @@ +#sympy.vector.kind + +from sympy.core.kind import Kind, _NumberKind, NumberKind +from sympy.core.mul import Mul + +class VectorKind(Kind): + """ + Kind for all vector objects in SymPy. + + Parameters + ========== + + element_kind : Kind + Kind of the element. Default is + :class:`sympy.core.kind.NumberKind`, + which means that the vector contains only numbers. + + Examples + ======== + + Any instance of Vector class has kind ``VectorKind``: + + >>> from sympy.vector.coordsysrect import CoordSys3D + >>> Sys = CoordSys3D('Sys') + >>> Sys.i.kind + VectorKind(NumberKind) + + Operations between instances of Vector keep also have the kind ``VectorKind``: + + >>> from sympy.core.add import Add + >>> v1 = Sys.i * 2 + Sys.j * 3 + Sys.k * 4 + >>> v2 = Sys.i * Sys.x + Sys.j * Sys.y + Sys.k * Sys.z + >>> v1.kind + VectorKind(NumberKind) + >>> v2.kind + VectorKind(NumberKind) + >>> Add(v1, v2).kind + VectorKind(NumberKind) + + Subclasses of Vector also have the kind ``VectorKind``, such as + Cross, VectorAdd, VectorMul or VectorZero. + + See Also + ======== + + sympy.core.kind.Kind + sympy.matrices.kind.MatrixKind + + """ + def __new__(cls, element_kind=NumberKind): + obj = super().__new__(cls, element_kind) + obj.element_kind = element_kind + return obj + + def __repr__(self): + return "VectorKind(%s)" % self.element_kind + +@Mul._kind_dispatcher.register(_NumberKind, VectorKind) +def num_vec_mul(k1, k2): + """ + The result of a multiplication between a number and a Vector should be of VectorKind. + The element kind is selected by recursive dispatching. + """ + if not isinstance(k2, VectorKind): + k1, k2 = k2, k1 + elemk = Mul._kind_dispatcher(k1, k2.element_kind) + return VectorKind(elemk) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/operators.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/operators.py new file mode 100644 index 0000000000000000000000000000000000000000..3ca42d20302f972cef66e0b4a35ac75606b2da94 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/operators.py @@ -0,0 +1,335 @@ +import collections +from sympy.core.expr import Expr +from sympy.core import sympify, S, preorder_traversal +from sympy.vector.coordsysrect import CoordSys3D +from sympy.vector.vector import Vector, VectorMul, VectorAdd, Cross, Dot +from sympy.core.function import Derivative +from sympy.core.add import Add +from sympy.core.mul import Mul + + +def _get_coord_systems(expr): + g = preorder_traversal(expr) + ret = set() + for i in g: + if isinstance(i, CoordSys3D): + ret.add(i) + g.skip() + return frozenset(ret) + + +def _split_mul_args_wrt_coordsys(expr): + d = collections.defaultdict(lambda: S.One) + for i in expr.args: + d[_get_coord_systems(i)] *= i + return list(d.values()) + + +class Gradient(Expr): + """ + Represents unevaluated Gradient. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D, Gradient + >>> R = CoordSys3D('R') + >>> s = R.x*R.y*R.z + >>> Gradient(s) + Gradient(R.x*R.y*R.z) + + """ + + def __new__(cls, expr): + expr = sympify(expr) + obj = Expr.__new__(cls, expr) + obj._expr = expr + return obj + + def doit(self, **hints): + return gradient(self._expr, doit=True) + + +class Divergence(Expr): + """ + Represents unevaluated Divergence. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D, Divergence + >>> R = CoordSys3D('R') + >>> v = R.y*R.z*R.i + R.x*R.z*R.j + R.x*R.y*R.k + >>> Divergence(v) + Divergence(R.y*R.z*R.i + R.x*R.z*R.j + R.x*R.y*R.k) + + """ + + def __new__(cls, expr): + expr = sympify(expr) + obj = Expr.__new__(cls, expr) + obj._expr = expr + return obj + + def doit(self, **hints): + return divergence(self._expr, doit=True) + + +class Curl(Expr): + """ + Represents unevaluated Curl. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D, Curl + >>> R = CoordSys3D('R') + >>> v = R.y*R.z*R.i + R.x*R.z*R.j + R.x*R.y*R.k + >>> Curl(v) + Curl(R.y*R.z*R.i + R.x*R.z*R.j + R.x*R.y*R.k) + + """ + + def __new__(cls, expr): + expr = sympify(expr) + obj = Expr.__new__(cls, expr) + obj._expr = expr + return obj + + def doit(self, **hints): + return curl(self._expr, doit=True) + + +def curl(vect, doit=True): + """ + Returns the curl of a vector field computed wrt the base scalars + of the given coordinate system. + + Parameters + ========== + + vect : Vector + The vector operand + + doit : bool + If True, the result is returned after calling .doit() on + each component. Else, the returned expression contains + Derivative instances + + Examples + ======== + + >>> from sympy.vector import CoordSys3D, curl + >>> R = CoordSys3D('R') + >>> v1 = R.y*R.z*R.i + R.x*R.z*R.j + R.x*R.y*R.k + >>> curl(v1) + 0 + >>> v2 = R.x*R.y*R.z*R.i + >>> curl(v2) + R.x*R.y*R.j + (-R.x*R.z)*R.k + + """ + + coord_sys = _get_coord_systems(vect) + + if len(coord_sys) == 0: + return Vector.zero + elif len(coord_sys) == 1: + coord_sys = next(iter(coord_sys)) + i, j, k = coord_sys.base_vectors() + x, y, z = coord_sys.base_scalars() + h1, h2, h3 = coord_sys.lame_coefficients() + vectx = vect.dot(i) + vecty = vect.dot(j) + vectz = vect.dot(k) + outvec = Vector.zero + outvec += (Derivative(vectz * h3, y) - + Derivative(vecty * h2, z)) * i / (h2 * h3) + outvec += (Derivative(vectx * h1, z) - + Derivative(vectz * h3, x)) * j / (h1 * h3) + outvec += (Derivative(vecty * h2, x) - + Derivative(vectx * h1, y)) * k / (h2 * h1) + + if doit: + return outvec.doit() + return outvec + else: + if isinstance(vect, (Add, VectorAdd)): + from sympy.vector import express + try: + cs = next(iter(coord_sys)) + args = [express(i, cs, variables=True) for i in vect.args] + except ValueError: + args = vect.args + return VectorAdd.fromiter(curl(i, doit=doit) for i in args) + elif isinstance(vect, (Mul, VectorMul)): + vector = [i for i in vect.args if isinstance(i, (Vector, Cross, Gradient))][0] + scalar = Mul.fromiter(i for i in vect.args if not isinstance(i, (Vector, Cross, Gradient))) + res = Cross(gradient(scalar), vector).doit() + scalar*curl(vector, doit=doit) + if doit: + return res.doit() + return res + elif isinstance(vect, (Cross, Curl, Gradient)): + return Curl(vect) + else: + raise ValueError("Invalid argument for curl") + + +def divergence(vect, doit=True): + """ + Returns the divergence of a vector field computed wrt the base + scalars of the given coordinate system. + + Parameters + ========== + + vector : Vector + The vector operand + + doit : bool + If True, the result is returned after calling .doit() on + each component. Else, the returned expression contains + Derivative instances + + Examples + ======== + + >>> from sympy.vector import CoordSys3D, divergence + >>> R = CoordSys3D('R') + >>> v1 = R.x*R.y*R.z * (R.i+R.j+R.k) + + >>> divergence(v1) + R.x*R.y + R.x*R.z + R.y*R.z + >>> v2 = 2*R.y*R.z*R.j + >>> divergence(v2) + 2*R.z + + """ + coord_sys = _get_coord_systems(vect) + if len(coord_sys) == 0: + return S.Zero + elif len(coord_sys) == 1: + if isinstance(vect, (Cross, Curl, Gradient)): + return Divergence(vect) + # TODO: is case of many coord systems, this gets a random one: + coord_sys = next(iter(coord_sys)) + i, j, k = coord_sys.base_vectors() + x, y, z = coord_sys.base_scalars() + h1, h2, h3 = coord_sys.lame_coefficients() + vx = _diff_conditional(vect.dot(i), x, h2, h3) \ + / (h1 * h2 * h3) + vy = _diff_conditional(vect.dot(j), y, h3, h1) \ + / (h1 * h2 * h3) + vz = _diff_conditional(vect.dot(k), z, h1, h2) \ + / (h1 * h2 * h3) + res = vx + vy + vz + if doit: + return res.doit() + return res + else: + if isinstance(vect, (Add, VectorAdd)): + return Add.fromiter(divergence(i, doit=doit) for i in vect.args) + elif isinstance(vect, (Mul, VectorMul)): + vector = [i for i in vect.args if isinstance(i, (Vector, Cross, Gradient))][0] + scalar = Mul.fromiter(i for i in vect.args if not isinstance(i, (Vector, Cross, Gradient))) + res = Dot(vector, gradient(scalar)) + scalar*divergence(vector, doit=doit) + if doit: + return res.doit() + return res + elif isinstance(vect, (Cross, Curl, Gradient)): + return Divergence(vect) + else: + raise ValueError("Invalid argument for divergence") + + +def gradient(scalar_field, doit=True): + """ + Returns the vector gradient of a scalar field computed wrt the + base scalars of the given coordinate system. + + Parameters + ========== + + scalar_field : SymPy Expr + The scalar field to compute the gradient of + + doit : bool + If True, the result is returned after calling .doit() on + each component. Else, the returned expression contains + Derivative instances + + Examples + ======== + + >>> from sympy.vector import CoordSys3D, gradient + >>> R = CoordSys3D('R') + >>> s1 = R.x*R.y*R.z + >>> gradient(s1) + R.y*R.z*R.i + R.x*R.z*R.j + R.x*R.y*R.k + >>> s2 = 5*R.x**2*R.z + >>> gradient(s2) + 10*R.x*R.z*R.i + 5*R.x**2*R.k + + """ + coord_sys = _get_coord_systems(scalar_field) + + if len(coord_sys) == 0: + return Vector.zero + elif len(coord_sys) == 1: + coord_sys = next(iter(coord_sys)) + h1, h2, h3 = coord_sys.lame_coefficients() + i, j, k = coord_sys.base_vectors() + x, y, z = coord_sys.base_scalars() + vx = Derivative(scalar_field, x) / h1 + vy = Derivative(scalar_field, y) / h2 + vz = Derivative(scalar_field, z) / h3 + + if doit: + return (vx * i + vy * j + vz * k).doit() + return vx * i + vy * j + vz * k + else: + if isinstance(scalar_field, (Add, VectorAdd)): + return VectorAdd.fromiter(gradient(i) for i in scalar_field.args) + if isinstance(scalar_field, (Mul, VectorMul)): + s = _split_mul_args_wrt_coordsys(scalar_field) + return VectorAdd.fromiter(scalar_field / i * gradient(i) for i in s) + return Gradient(scalar_field) + + +class Laplacian(Expr): + """ + Represents unevaluated Laplacian. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D, Laplacian + >>> R = CoordSys3D('R') + >>> v = 3*R.x**3*R.y**2*R.z**3 + >>> Laplacian(v) + Laplacian(3*R.x**3*R.y**2*R.z**3) + + """ + + def __new__(cls, expr): + expr = sympify(expr) + obj = Expr.__new__(cls, expr) + obj._expr = expr + return obj + + def doit(self, **hints): + from sympy.vector.functions import laplacian + return laplacian(self._expr) + + +def _diff_conditional(expr, base_scalar, coeff_1, coeff_2): + """ + First re-expresses expr in the system that base_scalar belongs to. + If base_scalar appears in the re-expressed form, differentiates + it wrt base_scalar. + Else, returns 0 + """ + from sympy.vector.functions import express + new_expr = express(expr, base_scalar.system, variables=True) + arg = coeff_1 * coeff_2 * new_expr + return Derivative(arg, base_scalar) if arg else S.Zero diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/orienters.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/orienters.py new file mode 100644 index 0000000000000000000000000000000000000000..0c22089e568bc817c943c1beecebde0fea46b6ae --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/orienters.py @@ -0,0 +1,398 @@ +from sympy.core.basic import Basic +from sympy.core.sympify import sympify +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.matrices.dense import (eye, rot_axis1, rot_axis2, rot_axis3) +from sympy.matrices.immutable import ImmutableDenseMatrix as Matrix +from sympy.core.cache import cacheit +from sympy.core.symbol import Str +import sympy.vector + + +class Orienter(Basic): + """ + Super-class for all orienter classes. + """ + + def rotation_matrix(self): + """ + The rotation matrix corresponding to this orienter + instance. + """ + return self._parent_orient + + +class AxisOrienter(Orienter): + """ + Class to denote an axis orienter. + """ + + def __new__(cls, angle, axis): + if not isinstance(axis, sympy.vector.Vector): + raise TypeError("axis should be a Vector") + angle = sympify(angle) + + obj = super().__new__(cls, angle, axis) + obj._angle = angle + obj._axis = axis + + return obj + + def __init__(self, angle, axis): + """ + Axis rotation is a rotation about an arbitrary axis by + some angle. The angle is supplied as a SymPy expr scalar, and + the axis is supplied as a Vector. + + Parameters + ========== + + angle : Expr + The angle by which the new system is to be rotated + + axis : Vector + The axis around which the rotation has to be performed + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> from sympy import symbols + >>> q1 = symbols('q1') + >>> N = CoordSys3D('N') + >>> from sympy.vector import AxisOrienter + >>> orienter = AxisOrienter(q1, N.i + 2 * N.j) + >>> B = N.orient_new('B', (orienter, )) + + """ + # Dummy initializer for docstrings + pass + + @cacheit + def rotation_matrix(self, system): + """ + The rotation matrix corresponding to this orienter + instance. + + Parameters + ========== + + system : CoordSys3D + The coordinate system wrt which the rotation matrix + is to be computed + """ + + axis = sympy.vector.express(self.axis, system).normalize() + axis = axis.to_matrix(system) + theta = self.angle + parent_orient = ((eye(3) - axis * axis.T) * cos(theta) + + Matrix([[0, -axis[2], axis[1]], + [axis[2], 0, -axis[0]], + [-axis[1], axis[0], 0]]) * sin(theta) + + axis * axis.T) + parent_orient = parent_orient.T + return parent_orient + + @property + def angle(self): + return self._angle + + @property + def axis(self): + return self._axis + + +class ThreeAngleOrienter(Orienter): + """ + Super-class for Body and Space orienters. + """ + + def __new__(cls, angle1, angle2, angle3, rot_order): + if isinstance(rot_order, Str): + rot_order = rot_order.name + + approved_orders = ('123', '231', '312', '132', '213', + '321', '121', '131', '212', '232', + '313', '323', '') + original_rot_order = rot_order + rot_order = str(rot_order).upper() + if not (len(rot_order) == 3): + raise TypeError('rot_order should be a str of length 3') + rot_order = [i.replace('X', '1') for i in rot_order] + rot_order = [i.replace('Y', '2') for i in rot_order] + rot_order = [i.replace('Z', '3') for i in rot_order] + rot_order = ''.join(rot_order) + if rot_order not in approved_orders: + raise TypeError('Invalid rot_type parameter') + a1 = int(rot_order[0]) + a2 = int(rot_order[1]) + a3 = int(rot_order[2]) + angle1 = sympify(angle1) + angle2 = sympify(angle2) + angle3 = sympify(angle3) + if cls._in_order: + parent_orient = (_rot(a1, angle1) * + _rot(a2, angle2) * + _rot(a3, angle3)) + else: + parent_orient = (_rot(a3, angle3) * + _rot(a2, angle2) * + _rot(a1, angle1)) + parent_orient = parent_orient.T + + obj = super().__new__( + cls, angle1, angle2, angle3, Str(rot_order)) + obj._angle1 = angle1 + obj._angle2 = angle2 + obj._angle3 = angle3 + obj._rot_order = original_rot_order + obj._parent_orient = parent_orient + + return obj + + @property + def angle1(self): + return self._angle1 + + @property + def angle2(self): + return self._angle2 + + @property + def angle3(self): + return self._angle3 + + @property + def rot_order(self): + return self._rot_order + + +class BodyOrienter(ThreeAngleOrienter): + """ + Class to denote a body-orienter. + """ + + _in_order = True + + def __new__(cls, angle1, angle2, angle3, rot_order): + obj = ThreeAngleOrienter.__new__(cls, angle1, angle2, angle3, + rot_order) + return obj + + def __init__(self, angle1, angle2, angle3, rot_order): + """ + Body orientation takes this coordinate system through three + successive simple rotations. + + Body fixed rotations include both Euler Angles and + Tait-Bryan Angles, see https://en.wikipedia.org/wiki/Euler_angles. + + Parameters + ========== + + angle1, angle2, angle3 : Expr + Three successive angles to rotate the coordinate system by + + rotation_order : string + String defining the order of axes for rotation + + Examples + ======== + + >>> from sympy.vector import CoordSys3D, BodyOrienter + >>> from sympy import symbols + >>> q1, q2, q3 = symbols('q1 q2 q3') + >>> N = CoordSys3D('N') + + A 'Body' fixed rotation is described by three angles and + three body-fixed rotation axes. To orient a coordinate system D + with respect to N, each sequential rotation is always about + the orthogonal unit vectors fixed to D. For example, a '123' + rotation will specify rotations about N.i, then D.j, then + D.k. (Initially, D.i is same as N.i) + Therefore, + + >>> body_orienter = BodyOrienter(q1, q2, q3, '123') + >>> D = N.orient_new('D', (body_orienter, )) + + is same as + + >>> from sympy.vector import AxisOrienter + >>> axis_orienter1 = AxisOrienter(q1, N.i) + >>> D = N.orient_new('D', (axis_orienter1, )) + >>> axis_orienter2 = AxisOrienter(q2, D.j) + >>> D = D.orient_new('D', (axis_orienter2, )) + >>> axis_orienter3 = AxisOrienter(q3, D.k) + >>> D = D.orient_new('D', (axis_orienter3, )) + + Acceptable rotation orders are of length 3, expressed in XYZ or + 123, and cannot have a rotation about about an axis twice in a row. + + >>> body_orienter1 = BodyOrienter(q1, q2, q3, '123') + >>> body_orienter2 = BodyOrienter(q1, q2, 0, 'ZXZ') + >>> body_orienter3 = BodyOrienter(0, 0, 0, 'XYX') + + """ + # Dummy initializer for docstrings + pass + + +class SpaceOrienter(ThreeAngleOrienter): + """ + Class to denote a space-orienter. + """ + + _in_order = False + + def __new__(cls, angle1, angle2, angle3, rot_order): + obj = ThreeAngleOrienter.__new__(cls, angle1, angle2, angle3, + rot_order) + return obj + + def __init__(self, angle1, angle2, angle3, rot_order): + """ + Space rotation is similar to Body rotation, but the rotations + are applied in the opposite order. + + Parameters + ========== + + angle1, angle2, angle3 : Expr + Three successive angles to rotate the coordinate system by + + rotation_order : string + String defining the order of axes for rotation + + See Also + ======== + + BodyOrienter : Orienter to orient systems wrt Euler angles. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D, SpaceOrienter + >>> from sympy import symbols + >>> q1, q2, q3 = symbols('q1 q2 q3') + >>> N = CoordSys3D('N') + + To orient a coordinate system D with respect to N, each + sequential rotation is always about N's orthogonal unit vectors. + For example, a '123' rotation will specify rotations about + N.i, then N.j, then N.k. + Therefore, + + >>> space_orienter = SpaceOrienter(q1, q2, q3, '312') + >>> D = N.orient_new('D', (space_orienter, )) + + is same as + + >>> from sympy.vector import AxisOrienter + >>> axis_orienter1 = AxisOrienter(q1, N.i) + >>> B = N.orient_new('B', (axis_orienter1, )) + >>> axis_orienter2 = AxisOrienter(q2, N.j) + >>> C = B.orient_new('C', (axis_orienter2, )) + >>> axis_orienter3 = AxisOrienter(q3, N.k) + >>> D = C.orient_new('C', (axis_orienter3, )) + + """ + # Dummy initializer for docstrings + pass + + +class QuaternionOrienter(Orienter): + """ + Class to denote a quaternion-orienter. + """ + + def __new__(cls, q0, q1, q2, q3): + q0 = sympify(q0) + q1 = sympify(q1) + q2 = sympify(q2) + q3 = sympify(q3) + parent_orient = (Matrix([[q0 ** 2 + q1 ** 2 - q2 ** 2 - + q3 ** 2, + 2 * (q1 * q2 - q0 * q3), + 2 * (q0 * q2 + q1 * q3)], + [2 * (q1 * q2 + q0 * q3), + q0 ** 2 - q1 ** 2 + + q2 ** 2 - q3 ** 2, + 2 * (q2 * q3 - q0 * q1)], + [2 * (q1 * q3 - q0 * q2), + 2 * (q0 * q1 + q2 * q3), + q0 ** 2 - q1 ** 2 - + q2 ** 2 + q3 ** 2]])) + parent_orient = parent_orient.T + + obj = super().__new__(cls, q0, q1, q2, q3) + obj._q0 = q0 + obj._q1 = q1 + obj._q2 = q2 + obj._q3 = q3 + obj._parent_orient = parent_orient + + return obj + + def __init__(self, angle1, angle2, angle3, rot_order): + """ + Quaternion orientation orients the new CoordSys3D with + Quaternions, defined as a finite rotation about lambda, a unit + vector, by some amount theta. + + This orientation is described by four parameters: + + q0 = cos(theta/2) + + q1 = lambda_x sin(theta/2) + + q2 = lambda_y sin(theta/2) + + q3 = lambda_z sin(theta/2) + + Quaternion does not take in a rotation order. + + Parameters + ========== + + q0, q1, q2, q3 : Expr + The quaternions to rotate the coordinate system by + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> from sympy import symbols + >>> q0, q1, q2, q3 = symbols('q0 q1 q2 q3') + >>> N = CoordSys3D('N') + >>> from sympy.vector import QuaternionOrienter + >>> q_orienter = QuaternionOrienter(q0, q1, q2, q3) + >>> B = N.orient_new('B', (q_orienter, )) + + """ + # Dummy initializer for docstrings + pass + + @property + def q0(self): + return self._q0 + + @property + def q1(self): + return self._q1 + + @property + def q2(self): + return self._q2 + + @property + def q3(self): + return self._q3 + + +def _rot(axis, angle): + """DCM for simple axis 1, 2 or 3 rotations. """ + if axis == 1: + return Matrix(rot_axis1(angle).T) + elif axis == 2: + return Matrix(rot_axis2(angle).T) + elif axis == 3: + return Matrix(rot_axis3(angle).T) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/parametricregion.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/parametricregion.py new file mode 100644 index 0000000000000000000000000000000000000000..5246769dabe208fe630f7c33b2da3ef4e11b3f67 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/parametricregion.py @@ -0,0 +1,189 @@ +from functools import singledispatch +from sympy.core.numbers import pi +from sympy.functions.elementary.trigonometric import tan +from sympy.simplify import trigsimp +from sympy.core import Basic, Tuple +from sympy.core.symbol import _symbol +from sympy.solvers import solve +from sympy.geometry import Point, Segment, Curve, Ellipse, Polygon +from sympy.vector import ImplicitRegion + + +class ParametricRegion(Basic): + """ + Represents a parametric region in space. + + Examples + ======== + + >>> from sympy import cos, sin, pi + >>> from sympy.abc import r, theta, t, a, b, x, y + >>> from sympy.vector import ParametricRegion + + >>> ParametricRegion((t, t**2), (t, -1, 2)) + ParametricRegion((t, t**2), (t, -1, 2)) + >>> ParametricRegion((x, y), (x, 3, 4), (y, 5, 6)) + ParametricRegion((x, y), (x, 3, 4), (y, 5, 6)) + >>> ParametricRegion((r*cos(theta), r*sin(theta)), (r, -2, 2), (theta, 0, pi)) + ParametricRegion((r*cos(theta), r*sin(theta)), (r, -2, 2), (theta, 0, pi)) + >>> ParametricRegion((a*cos(t), b*sin(t)), t) + ParametricRegion((a*cos(t), b*sin(t)), t) + + >>> circle = ParametricRegion((r*cos(theta), r*sin(theta)), r, (theta, 0, pi)) + >>> circle.parameters + (r, theta) + >>> circle.definition + (r*cos(theta), r*sin(theta)) + >>> circle.limits + {theta: (0, pi)} + + Dimension of a parametric region determines whether a region is a curve, surface + or volume region. It does not represent its dimensions in space. + + >>> circle.dimensions + 1 + + Parameters + ========== + + definition : tuple to define base scalars in terms of parameters. + + bounds : Parameter or a tuple of length 3 to define parameter and corresponding lower and upper bound. + + """ + def __new__(cls, definition, *bounds): + parameters = () + limits = {} + + if not isinstance(bounds, Tuple): + bounds = Tuple(*bounds) + + for bound in bounds: + if isinstance(bound, (tuple, Tuple)): + if len(bound) != 3: + raise ValueError("Tuple should be in the form (parameter, lowerbound, upperbound)") + parameters += (bound[0],) + limits[bound[0]] = (bound[1], bound[2]) + else: + parameters += (bound,) + + if not isinstance(definition, (tuple, Tuple)): + definition = (definition,) + + obj = super().__new__(cls, Tuple(*definition), *bounds) + obj._parameters = parameters + obj._limits = limits + + return obj + + @property + def definition(self): + return self.args[0] + + @property + def limits(self): + return self._limits + + @property + def parameters(self): + return self._parameters + + @property + def dimensions(self): + return len(self.limits) + + +@singledispatch +def parametric_region_list(reg): + """ + Returns a list of ParametricRegion objects representing the geometric region. + + Examples + ======== + + >>> from sympy.abc import t + >>> from sympy.vector import parametric_region_list + >>> from sympy.geometry import Point, Curve, Ellipse, Segment, Polygon + + >>> p = Point(2, 5) + >>> parametric_region_list(p) + [ParametricRegion((2, 5))] + + >>> c = Curve((t**3, 4*t), (t, -3, 4)) + >>> parametric_region_list(c) + [ParametricRegion((t**3, 4*t), (t, -3, 4))] + + >>> e = Ellipse(Point(1, 3), 2, 3) + >>> parametric_region_list(e) + [ParametricRegion((2*cos(t) + 1, 3*sin(t) + 3), (t, 0, 2*pi))] + + >>> s = Segment(Point(1, 3), Point(2, 6)) + >>> parametric_region_list(s) + [ParametricRegion((t + 1, 3*t + 3), (t, 0, 1))] + + >>> p1, p2, p3, p4 = [(0, 1), (2, -3), (5, 3), (-2, 3)] + >>> poly = Polygon(p1, p2, p3, p4) + >>> parametric_region_list(poly) + [ParametricRegion((2*t, 1 - 4*t), (t, 0, 1)), ParametricRegion((3*t + 2, 6*t - 3), (t, 0, 1)),\ + ParametricRegion((5 - 7*t, 3), (t, 0, 1)), ParametricRegion((2*t - 2, 3 - 2*t), (t, 0, 1))] + + """ + raise ValueError("SymPy cannot determine parametric representation of the region.") + + +@parametric_region_list.register(Point) +def _(obj): + return [ParametricRegion(obj.args)] + + +@parametric_region_list.register(Curve) # type: ignore +def _(obj): + definition = obj.arbitrary_point(obj.parameter).args + bounds = obj.limits + return [ParametricRegion(definition, bounds)] + + +@parametric_region_list.register(Ellipse) # type: ignore +def _(obj, parameter='t'): + definition = obj.arbitrary_point(parameter).args + t = _symbol(parameter, real=True) + bounds = (t, 0, 2*pi) + return [ParametricRegion(definition, bounds)] + + +@parametric_region_list.register(Segment) # type: ignore +def _(obj, parameter='t'): + t = _symbol(parameter, real=True) + definition = obj.arbitrary_point(t).args + + for i in range(0, 3): + lower_bound = solve(definition[i] - obj.points[0].args[i], t) + upper_bound = solve(definition[i] - obj.points[1].args[i], t) + + if len(lower_bound) == 1 and len(upper_bound) == 1: + bounds = t, lower_bound[0], upper_bound[0] + break + + definition_tuple = obj.arbitrary_point(parameter).args + return [ParametricRegion(definition_tuple, bounds)] + + +@parametric_region_list.register(Polygon) # type: ignore +def _(obj, parameter='t'): + l = [parametric_region_list(side, parameter)[0] for side in obj.sides] + return l + + +@parametric_region_list.register(ImplicitRegion) # type: ignore +def _(obj, parameters=('t', 's')): + definition = obj.rational_parametrization(parameters) + bounds = [] + + for i in range(len(obj.variables) - 1): + # Each parameter is replaced by its tangent to simplify integration + parameter = _symbol(parameters[i], real=True) + definition = [trigsimp(elem.subs(parameter, tan(parameter/2))) for elem in definition] + bounds.append((parameter, 0, 2*pi),) + + definition = Tuple(*definition) + return [ParametricRegion(definition, *bounds)] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/point.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/point.py new file mode 100644 index 0000000000000000000000000000000000000000..442ea4e8edc0e33c4f83f774ea3f11a01725ac3a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/point.py @@ -0,0 +1,148 @@ +from sympy.core.basic import Basic +from sympy.core.symbol import Str +from sympy.vector.vector import Vector +from sympy.vector.coordsysrect import CoordSys3D +from sympy.vector.functions import _path +from sympy.core.cache import cacheit + + +class Point(Basic): + """ + Represents a point in 3-D space. + """ + + def __new__(cls, name, position=Vector.zero, parent_point=None): + name = str(name) + # Check the args first + if not isinstance(position, Vector): + raise TypeError( + "position should be an instance of Vector, not %s" % type( + position)) + if (not isinstance(parent_point, Point) and + parent_point is not None): + raise TypeError( + "parent_point should be an instance of Point, not %s" % type( + parent_point)) + # Super class construction + if parent_point is None: + obj = super().__new__(cls, Str(name), position) + else: + obj = super().__new__(cls, Str(name), position, parent_point) + # Decide the object parameters + obj._name = name + obj._pos = position + if parent_point is None: + obj._parent = None + obj._root = obj + else: + obj._parent = parent_point + obj._root = parent_point._root + # Return object + return obj + + @cacheit + def position_wrt(self, other): + """ + Returns the position vector of this Point with respect to + another Point/CoordSys3D. + + Parameters + ========== + + other : Point/CoordSys3D + If other is a Point, the position of this Point wrt it is + returned. If its an instance of CoordSyRect, the position + wrt its origin is returned. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> N = CoordSys3D('N') + >>> p1 = N.origin.locate_new('p1', 10 * N.i) + >>> N.origin.position_wrt(p1) + (-10)*N.i + + """ + + if (not isinstance(other, Point) and + not isinstance(other, CoordSys3D)): + raise TypeError(str(other) + + "is not a Point or CoordSys3D") + if isinstance(other, CoordSys3D): + other = other.origin + # Handle special cases + if other == self: + return Vector.zero + elif other == self._parent: + return self._pos + elif other._parent == self: + return -1 * other._pos + # Else, use point tree to calculate position + rootindex, path = _path(self, other) + result = Vector.zero + for i in range(rootindex): + result += path[i]._pos + for i in range(rootindex + 1, len(path)): + result -= path[i]._pos + return result + + def locate_new(self, name, position): + """ + Returns a new Point located at the given position wrt this + Point. + Thus, the position vector of the new Point wrt this one will + be equal to the given 'position' parameter. + + Parameters + ========== + + name : str + Name of the new point + + position : Vector + The position vector of the new Point wrt this one + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> N = CoordSys3D('N') + >>> p1 = N.origin.locate_new('p1', 10 * N.i) + >>> p1.position_wrt(N.origin) + 10*N.i + + """ + return Point(name, position, self) + + def express_coordinates(self, coordinate_system): + """ + Returns the Cartesian/rectangular coordinates of this point + wrt the origin of the given CoordSys3D instance. + + Parameters + ========== + + coordinate_system : CoordSys3D + The coordinate system to express the coordinates of this + Point in. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> N = CoordSys3D('N') + >>> p1 = N.origin.locate_new('p1', 10 * N.i) + >>> p2 = p1.locate_new('p2', 5 * N.j) + >>> p2.express_coordinates(N) + (10, 5, 0) + + """ + + # Determine the position vector + pos_vect = self.position_wrt(coordinate_system.origin) + # Express it in the given coordinate system + return tuple(pos_vect.to_matrix(coordinate_system)) + + def _sympystr(self, printer): + return self._name diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/scalar.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/scalar.py new file mode 100644 index 0000000000000000000000000000000000000000..bcfb56cf177b9378a24f81ca1e6524fe048a5f94 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/scalar.py @@ -0,0 +1,72 @@ +from sympy.core import AtomicExpr, Symbol, S +from sympy.core.sympify import _sympify +from sympy.printing.pretty.stringpict import prettyForm +from sympy.printing.precedence import PRECEDENCE +from sympy.core.kind import NumberKind + + +class BaseScalar(AtomicExpr): + """ + A coordinate symbol/base scalar. + + Ideally, users should not instantiate this class. + + """ + + kind = NumberKind + + def __new__(cls, index, system, pretty_str=None, latex_str=None): + from sympy.vector.coordsysrect import CoordSys3D + if pretty_str is None: + pretty_str = "x{}".format(index) + elif isinstance(pretty_str, Symbol): + pretty_str = pretty_str.name + if latex_str is None: + latex_str = "x_{}".format(index) + elif isinstance(latex_str, Symbol): + latex_str = latex_str.name + + index = _sympify(index) + system = _sympify(system) + obj = super().__new__(cls, index, system) + if not isinstance(system, CoordSys3D): + raise TypeError("system should be a CoordSys3D") + if index not in range(0, 3): + raise ValueError("Invalid index specified.") + # The _id is used for equating purposes, and for hashing + obj._id = (index, system) + obj._name = obj.name = system._name + '.' + system._variable_names[index] + obj._pretty_form = '' + pretty_str + obj._latex_form = latex_str + obj._system = system + + return obj + + is_commutative = True + is_symbol = True + + @property + def free_symbols(self): + return {self} + + _diff_wrt = True + + def _eval_derivative(self, s): + if self == s: + return S.One + return S.Zero + + def _latex(self, printer=None): + return self._latex_form + + def _pretty(self, printer=None): + return prettyForm(self._pretty_form) + + precedence = PRECEDENCE['Atom'] + + @property + def system(self): + return self._system + + def _sympystr(self, printer): + return self._name diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_coordsysrect.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_coordsysrect.py new file mode 100644 index 0000000000000000000000000000000000000000..53eb8c89ec1643a71800efe3e370acff3cb6f9c0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_coordsysrect.py @@ -0,0 +1,464 @@ +from sympy.testing.pytest import raises +from sympy.vector.coordsysrect import CoordSys3D +from sympy.vector.scalar import BaseScalar +from sympy.core.function import expand +from sympy.core.numbers import pi +from sympy.core.symbol import symbols +from sympy.functions.elementary.hyperbolic import (cosh, sinh) +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (acos, atan2, cos, sin) +from sympy.matrices.dense import zeros +from sympy.matrices.immutable import ImmutableDenseMatrix as Matrix +from sympy.simplify.simplify import simplify +from sympy.vector.functions import express +from sympy.vector.point import Point +from sympy.vector.vector import Vector +from sympy.vector.orienters import (AxisOrienter, BodyOrienter, + SpaceOrienter, QuaternionOrienter) + + +x, y, z = symbols('x y z') +a, b, c, q = symbols('a b c q') +q1, q2, q3, q4 = symbols('q1 q2 q3 q4') + + +def test_func_args(): + A = CoordSys3D('A') + assert A.x.func(*A.x.args) == A.x + expr = 3*A.x + 4*A.y + assert expr.func(*expr.args) == expr + assert A.i.func(*A.i.args) == A.i + v = A.x*A.i + A.y*A.j + A.z*A.k + assert v.func(*v.args) == v + assert A.origin.func(*A.origin.args) == A.origin + + +def test_coordsys3d_equivalence(): + A = CoordSys3D('A') + A1 = CoordSys3D('A') + assert A1 == A + B = CoordSys3D('B') + assert A != B + + +def test_orienters(): + A = CoordSys3D('A') + axis_orienter = AxisOrienter(a, A.k) + body_orienter = BodyOrienter(a, b, c, '123') + space_orienter = SpaceOrienter(a, b, c, '123') + q_orienter = QuaternionOrienter(q1, q2, q3, q4) + assert axis_orienter.rotation_matrix(A) == Matrix([ + [ cos(a), sin(a), 0], + [-sin(a), cos(a), 0], + [ 0, 0, 1]]) + assert body_orienter.rotation_matrix() == Matrix([ + [ cos(b)*cos(c), sin(a)*sin(b)*cos(c) + sin(c)*cos(a), + sin(a)*sin(c) - sin(b)*cos(a)*cos(c)], + [-sin(c)*cos(b), -sin(a)*sin(b)*sin(c) + cos(a)*cos(c), + sin(a)*cos(c) + sin(b)*sin(c)*cos(a)], + [ sin(b), -sin(a)*cos(b), + cos(a)*cos(b)]]) + assert space_orienter.rotation_matrix() == Matrix([ + [cos(b)*cos(c), sin(c)*cos(b), -sin(b)], + [sin(a)*sin(b)*cos(c) - sin(c)*cos(a), + sin(a)*sin(b)*sin(c) + cos(a)*cos(c), sin(a)*cos(b)], + [sin(a)*sin(c) + sin(b)*cos(a)*cos(c), -sin(a)*cos(c) + + sin(b)*sin(c)*cos(a), cos(a)*cos(b)]]) + assert q_orienter.rotation_matrix() == Matrix([ + [q1**2 + q2**2 - q3**2 - q4**2, 2*q1*q4 + 2*q2*q3, + -2*q1*q3 + 2*q2*q4], + [-2*q1*q4 + 2*q2*q3, q1**2 - q2**2 + q3**2 - q4**2, + 2*q1*q2 + 2*q3*q4], + [2*q1*q3 + 2*q2*q4, + -2*q1*q2 + 2*q3*q4, q1**2 - q2**2 - q3**2 + q4**2]]) + + +def test_coordinate_vars(): + """ + Tests the coordinate variables functionality with respect to + reorientation of coordinate systems. + """ + A = CoordSys3D('A') + # Note that the name given on the lhs is different from A.x._name + assert BaseScalar(0, A, 'A_x', r'\mathbf{{x}_{A}}') == A.x + assert BaseScalar(1, A, 'A_y', r'\mathbf{{y}_{A}}') == A.y + assert BaseScalar(2, A, 'A_z', r'\mathbf{{z}_{A}}') == A.z + assert BaseScalar(0, A, 'A_x', r'\mathbf{{x}_{A}}').__hash__() == A.x.__hash__() + assert isinstance(A.x, BaseScalar) and \ + isinstance(A.y, BaseScalar) and \ + isinstance(A.z, BaseScalar) + assert A.x*A.y == A.y*A.x + assert A.scalar_map(A) == {A.x: A.x, A.y: A.y, A.z: A.z} + assert A.x.system == A + assert A.x.diff(A.x) == 1 + B = A.orient_new_axis('B', q, A.k) + assert B.scalar_map(A) == {B.z: A.z, B.y: -A.x*sin(q) + A.y*cos(q), + B.x: A.x*cos(q) + A.y*sin(q)} + assert A.scalar_map(B) == {A.x: B.x*cos(q) - B.y*sin(q), + A.y: B.x*sin(q) + B.y*cos(q), A.z: B.z} + assert express(B.x, A, variables=True) == A.x*cos(q) + A.y*sin(q) + assert express(B.y, A, variables=True) == -A.x*sin(q) + A.y*cos(q) + assert express(B.z, A, variables=True) == A.z + assert expand(express(B.x*B.y*B.z, A, variables=True)) == \ + expand(A.z*(-A.x*sin(q) + A.y*cos(q))*(A.x*cos(q) + A.y*sin(q))) + assert express(B.x*B.i + B.y*B.j + B.z*B.k, A) == \ + (B.x*cos(q) - B.y*sin(q))*A.i + (B.x*sin(q) + \ + B.y*cos(q))*A.j + B.z*A.k + assert simplify(express(B.x*B.i + B.y*B.j + B.z*B.k, A, \ + variables=True)) == \ + A.x*A.i + A.y*A.j + A.z*A.k + assert express(A.x*A.i + A.y*A.j + A.z*A.k, B) == \ + (A.x*cos(q) + A.y*sin(q))*B.i + \ + (-A.x*sin(q) + A.y*cos(q))*B.j + A.z*B.k + assert simplify(express(A.x*A.i + A.y*A.j + A.z*A.k, B, \ + variables=True)) == \ + B.x*B.i + B.y*B.j + B.z*B.k + N = B.orient_new_axis('N', -q, B.k) + assert N.scalar_map(A) == \ + {N.x: A.x, N.z: A.z, N.y: A.y} + C = A.orient_new_axis('C', q, A.i + A.j + A.k) + mapping = A.scalar_map(C) + assert mapping[A.x].equals(C.x*(2*cos(q) + 1)/3 + + C.y*(-2*sin(q + pi/6) + 1)/3 + + C.z*(-2*cos(q + pi/3) + 1)/3) + assert mapping[A.y].equals(C.x*(-2*cos(q + pi/3) + 1)/3 + + C.y*(2*cos(q) + 1)/3 + + C.z*(-2*sin(q + pi/6) + 1)/3) + assert mapping[A.z].equals(C.x*(-2*sin(q + pi/6) + 1)/3 + + C.y*(-2*cos(q + pi/3) + 1)/3 + + C.z*(2*cos(q) + 1)/3) + D = A.locate_new('D', a*A.i + b*A.j + c*A.k) + assert D.scalar_map(A) == {D.z: A.z - c, D.x: A.x - a, D.y: A.y - b} + E = A.orient_new_axis('E', a, A.k, a*A.i + b*A.j + c*A.k) + assert A.scalar_map(E) == {A.z: E.z + c, + A.x: E.x*cos(a) - E.y*sin(a) + a, + A.y: E.x*sin(a) + E.y*cos(a) + b} + assert E.scalar_map(A) == {E.x: (A.x - a)*cos(a) + (A.y - b)*sin(a), + E.y: (-A.x + a)*sin(a) + (A.y - b)*cos(a), + E.z: A.z - c} + F = A.locate_new('F', Vector.zero) + assert A.scalar_map(F) == {A.z: F.z, A.x: F.x, A.y: F.y} + + +def test_rotation_matrix(): + N = CoordSys3D('N') + A = N.orient_new_axis('A', q1, N.k) + B = A.orient_new_axis('B', q2, A.i) + C = B.orient_new_axis('C', q3, B.j) + D = N.orient_new_axis('D', q4, N.j) + E = N.orient_new_space('E', q1, q2, q3, '123') + F = N.orient_new_quaternion('F', q1, q2, q3, q4) + G = N.orient_new_body('G', q1, q2, q3, '123') + assert N.rotation_matrix(C) == Matrix([ + [- sin(q1) * sin(q2) * sin(q3) + cos(q1) * cos(q3), - sin(q1) * + cos(q2), sin(q1) * sin(q2) * cos(q3) + sin(q3) * cos(q1)], \ + [sin(q1) * cos(q3) + sin(q2) * sin(q3) * cos(q1), \ + cos(q1) * cos(q2), sin(q1) * sin(q3) - sin(q2) * cos(q1) * \ + cos(q3)], [- sin(q3) * cos(q2), sin(q2), cos(q2) * cos(q3)]]) + test_mat = D.rotation_matrix(C) - Matrix( + [[cos(q1) * cos(q3) * cos(q4) - sin(q3) * (- sin(q4) * cos(q2) + + sin(q1) * sin(q2) * cos(q4)), - sin(q2) * sin(q4) - sin(q1) * + cos(q2) * cos(q4), sin(q3) * cos(q1) * cos(q4) + cos(q3) * \ + (- sin(q4) * cos(q2) + sin(q1) * sin(q2) * cos(q4))], \ + [sin(q1) * cos(q3) + sin(q2) * sin(q3) * cos(q1), cos(q1) * \ + cos(q2), sin(q1) * sin(q3) - sin(q2) * cos(q1) * cos(q3)], \ + [sin(q4) * cos(q1) * cos(q3) - sin(q3) * (cos(q2) * cos(q4) + \ + sin(q1) * sin(q2) * \ + sin(q4)), sin(q2) * + cos(q4) - sin(q1) * sin(q4) * cos(q2), sin(q3) * \ + sin(q4) * cos(q1) + cos(q3) * (cos(q2) * cos(q4) + \ + sin(q1) * sin(q2) * sin(q4))]]) + assert test_mat.expand() == zeros(3, 3) + assert E.rotation_matrix(N) == Matrix( + [[cos(q2)*cos(q3), sin(q3)*cos(q2), -sin(q2)], + [sin(q1)*sin(q2)*cos(q3) - sin(q3)*cos(q1), \ + sin(q1)*sin(q2)*sin(q3) + cos(q1)*cos(q3), sin(q1)*cos(q2)], \ + [sin(q1)*sin(q3) + sin(q2)*cos(q1)*cos(q3), - \ + sin(q1)*cos(q3) + sin(q2)*sin(q3)*cos(q1), cos(q1)*cos(q2)]]) + assert F.rotation_matrix(N) == Matrix([[ + q1**2 + q2**2 - q3**2 - q4**2, + 2*q1*q4 + 2*q2*q3, -2*q1*q3 + 2*q2*q4],[ -2*q1*q4 + 2*q2*q3, + q1**2 - q2**2 + q3**2 - q4**2, 2*q1*q2 + 2*q3*q4], + [2*q1*q3 + 2*q2*q4, + -2*q1*q2 + 2*q3*q4, + q1**2 - q2**2 - q3**2 + q4**2]]) + assert G.rotation_matrix(N) == Matrix([[ + cos(q2)*cos(q3), sin(q1)*sin(q2)*cos(q3) + sin(q3)*cos(q1), + sin(q1)*sin(q3) - sin(q2)*cos(q1)*cos(q3)], [ + -sin(q3)*cos(q2), -sin(q1)*sin(q2)*sin(q3) + cos(q1)*cos(q3), + sin(q1)*cos(q3) + sin(q2)*sin(q3)*cos(q1)],[ + sin(q2), -sin(q1)*cos(q2), cos(q1)*cos(q2)]]) + + +def test_vector_with_orientation(): + """ + Tests the effects of orientation of coordinate systems on + basic vector operations. + """ + N = CoordSys3D('N') + A = N.orient_new_axis('A', q1, N.k) + B = A.orient_new_axis('B', q2, A.i) + C = B.orient_new_axis('C', q3, B.j) + + # Test to_matrix + v1 = a*N.i + b*N.j + c*N.k + assert v1.to_matrix(A) == Matrix([[ a*cos(q1) + b*sin(q1)], + [-a*sin(q1) + b*cos(q1)], + [ c]]) + + # Test dot + assert N.i.dot(A.i) == cos(q1) + assert N.i.dot(A.j) == -sin(q1) + assert N.i.dot(A.k) == 0 + assert N.j.dot(A.i) == sin(q1) + assert N.j.dot(A.j) == cos(q1) + assert N.j.dot(A.k) == 0 + assert N.k.dot(A.i) == 0 + assert N.k.dot(A.j) == 0 + assert N.k.dot(A.k) == 1 + + assert N.i.dot(A.i + A.j) == -sin(q1) + cos(q1) == \ + (A.i + A.j).dot(N.i) + + assert A.i.dot(C.i) == cos(q3) + assert A.i.dot(C.j) == 0 + assert A.i.dot(C.k) == sin(q3) + assert A.j.dot(C.i) == sin(q2)*sin(q3) + assert A.j.dot(C.j) == cos(q2) + assert A.j.dot(C.k) == -sin(q2)*cos(q3) + assert A.k.dot(C.i) == -cos(q2)*sin(q3) + assert A.k.dot(C.j) == sin(q2) + assert A.k.dot(C.k) == cos(q2)*cos(q3) + + # Test cross + assert N.i.cross(A.i) == sin(q1)*A.k + assert N.i.cross(A.j) == cos(q1)*A.k + assert N.i.cross(A.k) == -sin(q1)*A.i - cos(q1)*A.j + assert N.j.cross(A.i) == -cos(q1)*A.k + assert N.j.cross(A.j) == sin(q1)*A.k + assert N.j.cross(A.k) == cos(q1)*A.i - sin(q1)*A.j + assert N.k.cross(A.i) == A.j + assert N.k.cross(A.j) == -A.i + assert N.k.cross(A.k) == Vector.zero + + assert N.i.cross(A.i) == sin(q1)*A.k + assert N.i.cross(A.j) == cos(q1)*A.k + assert N.i.cross(A.i + A.j) == sin(q1)*A.k + cos(q1)*A.k + assert (A.i + A.j).cross(N.i) == (-sin(q1) - cos(q1))*N.k + + assert A.i.cross(C.i) == sin(q3)*C.j + assert A.i.cross(C.j) == -sin(q3)*C.i + cos(q3)*C.k + assert A.i.cross(C.k) == -cos(q3)*C.j + assert C.i.cross(A.i) == (-sin(q3)*cos(q2))*A.j + \ + (-sin(q2)*sin(q3))*A.k + assert C.j.cross(A.i) == (sin(q2))*A.j + (-cos(q2))*A.k + assert express(C.k.cross(A.i), C).trigsimp() == cos(q3)*C.j + + +def test_orient_new_methods(): + N = CoordSys3D('N') + orienter1 = AxisOrienter(q4, N.j) + orienter2 = SpaceOrienter(q1, q2, q3, '123') + orienter3 = QuaternionOrienter(q1, q2, q3, q4) + orienter4 = BodyOrienter(q1, q2, q3, '123') + D = N.orient_new('D', (orienter1, )) + E = N.orient_new('E', (orienter2, )) + F = N.orient_new('F', (orienter3, )) + G = N.orient_new('G', (orienter4, )) + assert D == N.orient_new_axis('D', q4, N.j) + assert E == N.orient_new_space('E', q1, q2, q3, '123') + assert F == N.orient_new_quaternion('F', q1, q2, q3, q4) + assert G == N.orient_new_body('G', q1, q2, q3, '123') + + +def test_locatenew_point(): + """ + Tests Point class, and locate_new method in CoordSys3D. + """ + A = CoordSys3D('A') + assert isinstance(A.origin, Point) + v = a*A.i + b*A.j + c*A.k + C = A.locate_new('C', v) + assert C.origin.position_wrt(A) == \ + C.position_wrt(A) == \ + C.origin.position_wrt(A.origin) == v + assert A.origin.position_wrt(C) == \ + A.position_wrt(C) == \ + A.origin.position_wrt(C.origin) == -v + assert A.origin.express_coordinates(C) == (-a, -b, -c) + p = A.origin.locate_new('p', -v) + assert p.express_coordinates(A) == (-a, -b, -c) + assert p.position_wrt(C.origin) == p.position_wrt(C) == \ + -2 * v + p1 = p.locate_new('p1', 2*v) + assert p1.position_wrt(C.origin) == Vector.zero + assert p1.express_coordinates(C) == (0, 0, 0) + p2 = p.locate_new('p2', A.i) + assert p1.position_wrt(p2) == 2*v - A.i + assert p2.express_coordinates(C) == (-2*a + 1, -2*b, -2*c) + + +def test_create_new(): + a = CoordSys3D('a') + c = a.create_new('c', transformation='spherical') + assert c._parent == a + assert c.transformation_to_parent() == \ + (c.r*sin(c.theta)*cos(c.phi), c.r*sin(c.theta)*sin(c.phi), c.r*cos(c.theta)) + assert c.transformation_from_parent() == \ + (sqrt(a.x**2 + a.y**2 + a.z**2), acos(a.z/sqrt(a.x**2 + a.y**2 + a.z**2)), atan2(a.y, a.x)) + + +def test_evalf(): + A = CoordSys3D('A') + v = 3*A.i + 4*A.j + a*A.k + assert v.n() == v.evalf() + assert v.evalf(subs={a:1}) == v.subs(a, 1).evalf() + + +def test_lame_coefficients(): + a = CoordSys3D('a', 'spherical') + assert a.lame_coefficients() == (1, a.r, sin(a.theta)*a.r) + a = CoordSys3D('a') + assert a.lame_coefficients() == (1, 1, 1) + a = CoordSys3D('a', 'cartesian') + assert a.lame_coefficients() == (1, 1, 1) + a = CoordSys3D('a', 'cylindrical') + assert a.lame_coefficients() == (1, a.r, 1) + + +def test_transformation_equations(): + + x, y, z = symbols('x y z') + # Str + a = CoordSys3D('a', transformation='spherical', + variable_names=["r", "theta", "phi"]) + r, theta, phi = a.base_scalars() + + assert r == a.r + assert theta == a.theta + assert phi == a.phi + + raises(AttributeError, lambda: a.x) + raises(AttributeError, lambda: a.y) + raises(AttributeError, lambda: a.z) + + assert a.transformation_to_parent() == ( + r*sin(theta)*cos(phi), + r*sin(theta)*sin(phi), + r*cos(theta) + ) + assert a.lame_coefficients() == (1, r, r*sin(theta)) + assert a.transformation_from_parent_function()(x, y, z) == ( + sqrt(x ** 2 + y ** 2 + z ** 2), + acos((z) / sqrt(x**2 + y**2 + z**2)), + atan2(y, x) + ) + a = CoordSys3D('a', transformation='cylindrical', + variable_names=["r", "theta", "z"]) + r, theta, z = a.base_scalars() + assert a.transformation_to_parent() == ( + r*cos(theta), + r*sin(theta), + z + ) + assert a.lame_coefficients() == (1, a.r, 1) + assert a.transformation_from_parent_function()(x, y, z) == (sqrt(x**2 + y**2), + atan2(y, x), z) + + a = CoordSys3D('a', 'cartesian') + assert a.transformation_to_parent() == (a.x, a.y, a.z) + assert a.lame_coefficients() == (1, 1, 1) + assert a.transformation_from_parent_function()(x, y, z) == (x, y, z) + + # Variables and expressions + + # Cartesian with equation tuple: + x, y, z = symbols('x y z') + a = CoordSys3D('a', ((x, y, z), (x, y, z))) + a._calculate_inv_trans_equations() + assert a.transformation_to_parent() == (a.x1, a.x2, a.x3) + assert a.lame_coefficients() == (1, 1, 1) + assert a.transformation_from_parent_function()(x, y, z) == (x, y, z) + r, theta, z = symbols("r theta z") + + # Cylindrical with equation tuple: + a = CoordSys3D('a', [(r, theta, z), (r*cos(theta), r*sin(theta), z)], + variable_names=["r", "theta", "z"]) + r, theta, z = a.base_scalars() + assert a.transformation_to_parent() == ( + r*cos(theta), r*sin(theta), z + ) + assert a.lame_coefficients() == ( + sqrt(sin(theta)**2 + cos(theta)**2), + sqrt(r**2*sin(theta)**2 + r**2*cos(theta)**2), + 1 + ) # ==> this should simplify to (1, r, 1), tests are too slow with `simplify`. + + # Definitions with `lambda`: + + # Cartesian with `lambda` + a = CoordSys3D('a', lambda x, y, z: (x, y, z)) + assert a.transformation_to_parent() == (a.x1, a.x2, a.x3) + assert a.lame_coefficients() == (1, 1, 1) + a._calculate_inv_trans_equations() + assert a.transformation_from_parent_function()(x, y, z) == (x, y, z) + + # Spherical with `lambda` + a = CoordSys3D('a', lambda r, theta, phi: (r*sin(theta)*cos(phi), r*sin(theta)*sin(phi), r*cos(theta)), + variable_names=["r", "theta", "phi"]) + r, theta, phi = a.base_scalars() + assert a.transformation_to_parent() == ( + r*sin(theta)*cos(phi), r*sin(phi)*sin(theta), r*cos(theta) + ) + assert a.lame_coefficients() == ( + sqrt(sin(phi)**2*sin(theta)**2 + sin(theta)**2*cos(phi)**2 + cos(theta)**2), + sqrt(r**2*sin(phi)**2*cos(theta)**2 + r**2*sin(theta)**2 + r**2*cos(phi)**2*cos(theta)**2), + sqrt(r**2*sin(phi)**2*sin(theta)**2 + r**2*sin(theta)**2*cos(phi)**2) + ) # ==> this should simplify to (1, r, sin(theta)*r), `simplify` is too slow. + + # Cylindrical with `lambda` + a = CoordSys3D('a', lambda r, theta, z: + (r*cos(theta), r*sin(theta), z), + variable_names=["r", "theta", "z"] + ) + r, theta, z = a.base_scalars() + assert a.transformation_to_parent() == (r*cos(theta), r*sin(theta), z) + assert a.lame_coefficients() == ( + sqrt(sin(theta)**2 + cos(theta)**2), + sqrt(r**2*sin(theta)**2 + r**2*cos(theta)**2), + 1 + ) # ==> this should simplify to (1, a.x, 1) + + raises(TypeError, lambda: CoordSys3D('a', transformation={ + x: x*sin(y)*cos(z), y:x*sin(y)*sin(z), z: x*cos(y)})) + + +def test_check_orthogonality(): + x, y, z = symbols('x y z') + u,v = symbols('u, v') + a = CoordSys3D('a', transformation=((x, y, z), (x*sin(y)*cos(z), x*sin(y)*sin(z), x*cos(y)))) + assert a._check_orthogonality(a._transformation) is True + a = CoordSys3D('a', transformation=((x, y, z), (x * cos(y), x * sin(y), z))) + assert a._check_orthogonality(a._transformation) is True + a = CoordSys3D('a', transformation=((u, v, z), (cosh(u) * cos(v), sinh(u) * sin(v), z))) + assert a._check_orthogonality(a._transformation) is True + + raises(ValueError, lambda: CoordSys3D('a', transformation=((x, y, z), (x, x, z)))) + raises(ValueError, lambda: CoordSys3D('a', transformation=( + (x, y, z), (x*sin(y/2)*cos(z), x*sin(y)*sin(z), x*cos(y))))) + + +def test_rotation_trans_equations(): + a = CoordSys3D('a') + from sympy.core.symbol import symbols + q0 = symbols('q0') + assert a._rotation_trans_equations(a._parent_rotation_matrix, a.base_scalars()) == (a.x, a.y, a.z) + assert a._rotation_trans_equations(a._inverse_rotation_matrix(), a.base_scalars()) == (a.x, a.y, a.z) + b = a.orient_new_axis('b', 0, -a.k) + assert b._rotation_trans_equations(b._parent_rotation_matrix, b.base_scalars()) == (b.x, b.y, b.z) + assert b._rotation_trans_equations(b._inverse_rotation_matrix(), b.base_scalars()) == (b.x, b.y, b.z) + c = a.orient_new_axis('c', q0, -a.k) + assert c._rotation_trans_equations(c._parent_rotation_matrix, c.base_scalars()) == \ + (-sin(q0) * c.y + cos(q0) * c.x, sin(q0) * c.x + cos(q0) * c.y, c.z) + assert c._rotation_trans_equations(c._inverse_rotation_matrix(), c.base_scalars()) == \ + (sin(q0) * c.y + cos(q0) * c.x, -sin(q0) * c.x + cos(q0) * c.y, c.z) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_dyadic.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_dyadic.py new file mode 100644 index 0000000000000000000000000000000000000000..2e396fcf2a81af897b59c0065f6b15f5c6933222 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_dyadic.py @@ -0,0 +1,134 @@ +from sympy.core.numbers import pi +from sympy.core.symbol import symbols +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.matrices.immutable import ImmutableDenseMatrix as Matrix +from sympy.simplify.simplify import simplify +from sympy.vector import (CoordSys3D, Vector, Dyadic, + DyadicAdd, DyadicMul, DyadicZero, + BaseDyadic, express) + + +A = CoordSys3D('A') + + +def test_dyadic(): + a, b = symbols('a, b') + assert Dyadic.zero != 0 + assert isinstance(Dyadic.zero, DyadicZero) + assert BaseDyadic(A.i, A.j) != BaseDyadic(A.j, A.i) + assert (BaseDyadic(Vector.zero, A.i) == + BaseDyadic(A.i, Vector.zero) == Dyadic.zero) + + d1 = A.i | A.i + d2 = A.j | A.j + d3 = A.i | A.j + + assert isinstance(d1, BaseDyadic) + d_mul = a*d1 + assert isinstance(d_mul, DyadicMul) + assert d_mul.base_dyadic == d1 + assert d_mul.measure_number == a + assert isinstance(a*d1 + b*d3, DyadicAdd) + assert d1 == A.i.outer(A.i) + assert d3 == A.i.outer(A.j) + v1 = a*A.i - A.k + v2 = A.i + b*A.j + assert v1 | v2 == v1.outer(v2) == a * (A.i|A.i) + (a*b) * (A.i|A.j) +\ + - (A.k|A.i) - b * (A.k|A.j) + assert d1 * 0 == Dyadic.zero + assert d1 != Dyadic.zero + assert d1 * 2 == 2 * (A.i | A.i) + assert d1 / 2. == 0.5 * d1 + + assert d1.dot(0 * d1) == Vector.zero + assert d1 & d2 == Dyadic.zero + assert d1.dot(A.i) == A.i == d1 & A.i + + assert d1.cross(Vector.zero) == Dyadic.zero + assert d1.cross(A.i) == Dyadic.zero + assert d1 ^ A.j == d1.cross(A.j) + assert d1.cross(A.k) == - A.i | A.j + assert d2.cross(A.i) == - A.j | A.k == d2 ^ A.i + + assert A.i ^ d1 == Dyadic.zero + assert A.j.cross(d1) == - A.k | A.i == A.j ^ d1 + assert Vector.zero.cross(d1) == Dyadic.zero + assert A.k ^ d1 == A.j | A.i + assert A.i.dot(d1) == A.i & d1 == A.i + assert A.j.dot(d1) == Vector.zero + assert Vector.zero.dot(d1) == Vector.zero + assert A.j & d2 == A.j + + assert d1.dot(d3) == d1 & d3 == A.i | A.j == d3 + assert d3 & d1 == Dyadic.zero + + q = symbols('q') + B = A.orient_new_axis('B', q, A.k) + assert express(d1, B) == express(d1, B, B) + + expr1 = ((cos(q)**2) * (B.i | B.i) + (-sin(q) * cos(q)) * + (B.i | B.j) + (-sin(q) * cos(q)) * (B.j | B.i) + (sin(q)**2) * + (B.j | B.j)) + assert (express(d1, B) - expr1).simplify() == Dyadic.zero + + expr2 = (cos(q)) * (B.i | A.i) + (-sin(q)) * (B.j | A.i) + assert (express(d1, B, A) - expr2).simplify() == Dyadic.zero + + expr3 = (cos(q)) * (A.i | B.i) + (-sin(q)) * (A.i | B.j) + assert (express(d1, A, B) - expr3).simplify() == Dyadic.zero + + assert d1.to_matrix(A) == Matrix([[1, 0, 0], [0, 0, 0], [0, 0, 0]]) + assert d1.to_matrix(A, B) == Matrix([[cos(q), -sin(q), 0], + [0, 0, 0], + [0, 0, 0]]) + assert d3.to_matrix(A) == Matrix([[0, 1, 0], [0, 0, 0], [0, 0, 0]]) + a, b, c, d, e, f = symbols('a, b, c, d, e, f') + v1 = a * A.i + b * A.j + c * A.k + v2 = d * A.i + e * A.j + f * A.k + d4 = v1.outer(v2) + assert d4.to_matrix(A) == Matrix([[a * d, a * e, a * f], + [b * d, b * e, b * f], + [c * d, c * e, c * f]]) + d5 = v1.outer(v1) + C = A.orient_new_axis('C', q, A.i) + for expected, actual in zip(C.rotation_matrix(A) * d5.to_matrix(A) * \ + C.rotation_matrix(A).T, d5.to_matrix(C)): + assert (expected - actual).simplify() == 0 + + +def test_dyadic_simplify(): + x, y, z, k, n, m, w, f, s, A = symbols('x, y, z, k, n, m, w, f, s, A') + N = CoordSys3D('N') + + dy = N.i | N.i + test1 = (1 / x + 1 / y) * dy + assert (N.i & test1 & N.i) != (x + y) / (x * y) + test1 = test1.simplify() + assert test1.simplify() == simplify(test1) + assert (N.i & test1 & N.i) == (x + y) / (x * y) + + test2 = (A**2 * s**4 / (4 * pi * k * m**3)) * dy + test2 = test2.simplify() + assert (N.i & test2 & N.i) == (A**2 * s**4 / (4 * pi * k * m**3)) + + test3 = ((4 + 4 * x - 2 * (2 + 2 * x)) / (2 + 2 * x)) * dy + test3 = test3.simplify() + assert (N.i & test3 & N.i) == 0 + + test4 = ((-4 * x * y**2 - 2 * y**3 - 2 * x**2 * y) / (x + y)**2) * dy + test4 = test4.simplify() + assert (N.i & test4 & N.i) == -2 * y + + +def test_dyadic_srepr(): + from sympy.printing.repr import srepr + N = CoordSys3D('N') + + dy = N.i | N.j + res = "BaseDyadic(CoordSys3D(Str('N'), Tuple(ImmutableDenseMatrix([["\ + "Integer(1), Integer(0), Integer(0)], [Integer(0), Integer(1), "\ + "Integer(0)], [Integer(0), Integer(0), Integer(1)]]), "\ + "VectorZero())).i, CoordSys3D(Str('N'), Tuple(ImmutableDenseMatrix("\ + "[[Integer(1), Integer(0), Integer(0)], [Integer(0), Integer(1), "\ + "Integer(0)], [Integer(0), Integer(0), Integer(1)]]), VectorZero())).j)" + assert srepr(dy) == res diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_field_functions.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_field_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..035c2ce0234b81069c5ad8dcb1c74f4de0164a8f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_field_functions.py @@ -0,0 +1,321 @@ +from sympy.core.function import Derivative +from sympy.vector.vector import Vector +from sympy.vector.coordsysrect import CoordSys3D +from sympy.simplify import simplify +from sympy.core.symbol import symbols +from sympy.core import S +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.vector.vector import Dot +from sympy.vector.operators import curl, divergence, gradient, Gradient, Divergence, Cross +from sympy.vector.deloperator import Del +from sympy.vector.functions import (is_conservative, is_solenoidal, + scalar_potential, directional_derivative, + laplacian, scalar_potential_difference) +from sympy.testing.pytest import raises + +C = CoordSys3D('C') +i, j, k = C.base_vectors() +x, y, z = C.base_scalars() +delop = Del() +a, b, c, q = symbols('a b c q') + + +def test_del_operator(): + # Tests for curl + + assert delop ^ Vector.zero == Vector.zero + assert ((delop ^ Vector.zero).doit() == Vector.zero == + curl(Vector.zero)) + assert delop.cross(Vector.zero) == delop ^ Vector.zero + assert (delop ^ i).doit() == Vector.zero + assert delop.cross(2*y**2*j, doit=True) == Vector.zero + assert delop.cross(2*y**2*j) == delop ^ 2*y**2*j + v = x*y*z * (i + j + k) + assert ((delop ^ v).doit() == + (-x*y + x*z)*i + (x*y - y*z)*j + (-x*z + y*z)*k == + curl(v)) + assert delop ^ v == delop.cross(v) + assert (delop.cross(2*x**2*j) == + (Derivative(0, C.y) - Derivative(2*C.x**2, C.z))*C.i + + (-Derivative(0, C.x) + Derivative(0, C.z))*C.j + + (-Derivative(0, C.y) + Derivative(2*C.x**2, C.x))*C.k) + assert (delop.cross(2*x**2*j, doit=True) == 4*x*k == + curl(2*x**2*j)) + + #Tests for divergence + assert delop & Vector.zero is S.Zero == divergence(Vector.zero) + assert (delop & Vector.zero).doit() is S.Zero + assert delop.dot(Vector.zero) == delop & Vector.zero + assert (delop & i).doit() is S.Zero + assert (delop & x**2*i).doit() == 2*x == divergence(x**2*i) + assert (delop.dot(v, doit=True) == x*y + y*z + z*x == + divergence(v)) + assert delop & v == delop.dot(v) + assert delop.dot(1/(x*y*z) * (i + j + k), doit=True) == \ + - 1 / (x*y*z**2) - 1 / (x*y**2*z) - 1 / (x**2*y*z) + v = x*i + y*j + z*k + assert (delop & v == Derivative(C.x, C.x) + + Derivative(C.y, C.y) + Derivative(C.z, C.z)) + assert delop.dot(v, doit=True) == 3 == divergence(v) + assert delop & v == delop.dot(v) + assert simplify((delop & v).doit()) == 3 + + #Tests for gradient + assert (delop.gradient(0, doit=True) == Vector.zero == + gradient(0)) + assert delop.gradient(0) == delop(0) + assert (delop(S.Zero)).doit() == Vector.zero + assert (delop(x) == (Derivative(C.x, C.x))*C.i + + (Derivative(C.x, C.y))*C.j + (Derivative(C.x, C.z))*C.k) + assert (delop(x)).doit() == i == gradient(x) + assert (delop(x*y*z) == + (Derivative(C.x*C.y*C.z, C.x))*C.i + + (Derivative(C.x*C.y*C.z, C.y))*C.j + + (Derivative(C.x*C.y*C.z, C.z))*C.k) + assert (delop.gradient(x*y*z, doit=True) == + y*z*i + z*x*j + x*y*k == + gradient(x*y*z)) + assert delop(x*y*z) == delop.gradient(x*y*z) + assert (delop(2*x**2)).doit() == 4*x*i + assert ((delop(a*sin(y) / x)).doit() == + -a*sin(y)/x**2 * i + a*cos(y)/x * j) + + #Tests for directional derivative + assert (Vector.zero & delop)(a) is S.Zero + assert ((Vector.zero & delop)(a)).doit() is S.Zero + assert ((v & delop)(Vector.zero)).doit() == Vector.zero + assert ((v & delop)(S.Zero)).doit() is S.Zero + assert ((i & delop)(x)).doit() == 1 + assert ((j & delop)(y)).doit() == 1 + assert ((k & delop)(z)).doit() == 1 + assert ((i & delop)(x*y*z)).doit() == y*z + assert ((v & delop)(x)).doit() == x + assert ((v & delop)(x*y*z)).doit() == 3*x*y*z + assert (v & delop)(x + y + z) == C.x + C.y + C.z + assert ((v & delop)(x + y + z)).doit() == x + y + z + assert ((v & delop)(v)).doit() == v + assert ((i & delop)(v)).doit() == i + assert ((j & delop)(v)).doit() == j + assert ((k & delop)(v)).doit() == k + assert ((v & delop)(Vector.zero)).doit() == Vector.zero + + # Tests for laplacian on scalar fields + assert laplacian(x*y*z) is S.Zero + assert laplacian(x**2) == S(2) + assert laplacian(x**2*y**2*z**2) == \ + 2*y**2*z**2 + 2*x**2*z**2 + 2*x**2*y**2 + A = CoordSys3D('A', transformation="spherical", variable_names=["r", "theta", "phi"]) + B = CoordSys3D('B', transformation='cylindrical', variable_names=["r", "theta", "z"]) + assert laplacian(A.r + A.theta + A.phi) == 2/A.r + cos(A.theta)/(A.r**2*sin(A.theta)) + assert laplacian(B.r + B.theta + B.z) == 1/B.r + + # Tests for laplacian on vector fields + assert laplacian(x*y*z*(i + j + k)) == Vector.zero + assert laplacian(x*y**2*z*(i + j + k)) == \ + 2*x*z*i + 2*x*z*j + 2*x*z*k + + +def test_product_rules(): + """ + Tests the six product rules defined with respect to the Del + operator + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Del + + """ + + #Define the scalar and vector functions + f = 2*x*y*z + g = x*y + y*z + z*x + u = x**2*i + 4*j - y**2*z*k + v = 4*i + x*y*z*k + + # First product rule + lhs = delop(f * g, doit=True) + rhs = (f * delop(g) + g * delop(f)).doit() + assert simplify(lhs) == simplify(rhs) + + # Second product rule + lhs = delop(u & v).doit() + rhs = ((u ^ (delop ^ v)) + (v ^ (delop ^ u)) + \ + ((u & delop)(v)) + ((v & delop)(u))).doit() + assert simplify(lhs) == simplify(rhs) + + # Third product rule + lhs = (delop & (f*v)).doit() + rhs = ((f * (delop & v)) + (v & (delop(f)))).doit() + assert simplify(lhs) == simplify(rhs) + + # Fourth product rule + lhs = (delop & (u ^ v)).doit() + rhs = ((v & (delop ^ u)) - (u & (delop ^ v))).doit() + assert simplify(lhs) == simplify(rhs) + + # Fifth product rule + lhs = (delop ^ (f * v)).doit() + rhs = (((delop(f)) ^ v) + (f * (delop ^ v))).doit() + assert simplify(lhs) == simplify(rhs) + + # Sixth product rule + lhs = (delop ^ (u ^ v)).doit() + rhs = (u * (delop & v) - v * (delop & u) + + (v & delop)(u) - (u & delop)(v)).doit() + assert simplify(lhs) == simplify(rhs) + + +P = C.orient_new_axis('P', q, C.k) # type: ignore +scalar_field = 2*x**2*y*z +grad_field = gradient(scalar_field) +vector_field = y**2*i + 3*x*j + 5*y*z*k +curl_field = curl(vector_field) + + +def test_conservative(): + assert is_conservative(Vector.zero) is True + assert is_conservative(i) is True + assert is_conservative(2 * i + 3 * j + 4 * k) is True + assert (is_conservative(y*z*i + x*z*j + x*y*k) is + True) + assert is_conservative(x * j) is False + assert is_conservative(grad_field) is True + assert is_conservative(curl_field) is False + assert (is_conservative(4*x*y*z*i + 2*x**2*z*j) is + False) + assert is_conservative(z*P.i + P.x*k) is True + + +def test_solenoidal(): + assert is_solenoidal(Vector.zero) is True + assert is_solenoidal(i) is True + assert is_solenoidal(2 * i + 3 * j + 4 * k) is True + assert (is_solenoidal(y*z*i + x*z*j + x*y*k) is + True) + assert is_solenoidal(y * j) is False + assert is_solenoidal(grad_field) is False + assert is_solenoidal(curl_field) is True + assert is_solenoidal((-2*y + 3)*k) is True + assert is_solenoidal(cos(q)*i + sin(q)*j + cos(q)*P.k) is True + assert is_solenoidal(z*P.i + P.x*k) is True + + +def test_directional_derivative(): + assert directional_derivative(C.x*C.y*C.z, 3*C.i + 4*C.j + C.k) == C.x*C.y + 4*C.x*C.z + 3*C.y*C.z + assert directional_derivative(5*C.x**2*C.z, 3*C.i + 4*C.j + C.k) == 5*C.x**2 + 30*C.x*C.z + assert directional_derivative(5*C.x**2*C.z, 4*C.j) is S.Zero + + D = CoordSys3D("D", "spherical", variable_names=["r", "theta", "phi"], + vector_names=["e_r", "e_theta", "e_phi"]) + r, theta, phi = D.base_scalars() + e_r, e_theta, e_phi = D.base_vectors() + assert directional_derivative(r**2*e_r, e_r) == 2*r*e_r + assert directional_derivative(5*r**2*phi, 3*e_r + 4*e_theta + e_phi) == 5*r**2 + 30*r*phi + + +def test_scalar_potential(): + assert scalar_potential(Vector.zero, C) == 0 + assert scalar_potential(i, C) == x + assert scalar_potential(j, C) == y + assert scalar_potential(k, C) == z + assert scalar_potential(y*z*i + x*z*j + x*y*k, C) == x*y*z + assert scalar_potential(grad_field, C) == scalar_field + assert scalar_potential(z*P.i + P.x*k, C) == x*z*cos(q) + y*z*sin(q) + assert scalar_potential(z*P.i + P.x*k, P) == P.x*P.z + raises(ValueError, lambda: scalar_potential(x*j, C)) + + +def test_scalar_potential_difference(): + point1 = C.origin.locate_new('P1', 1*i + 2*j + 3*k) + point2 = C.origin.locate_new('P2', 4*i + 5*j + 6*k) + genericpointC = C.origin.locate_new('RP', x*i + y*j + z*k) + genericpointP = P.origin.locate_new('PP', P.x*P.i + P.y*P.j + P.z*P.k) + assert scalar_potential_difference(S.Zero, C, point1, point2) == 0 + assert (scalar_potential_difference(scalar_field, C, C.origin, + genericpointC) == + scalar_field) + assert (scalar_potential_difference(grad_field, C, C.origin, + genericpointC) == + scalar_field) + assert scalar_potential_difference(grad_field, C, point1, point2) == 948 + assert (scalar_potential_difference(y*z*i + x*z*j + + x*y*k, C, point1, + genericpointC) == + x*y*z - 6) + potential_diff_P = (2*P.z*(P.x*sin(q) + P.y*cos(q))* + (P.x*cos(q) - P.y*sin(q))**2) + assert (scalar_potential_difference(grad_field, P, P.origin, + genericpointP).simplify() == + potential_diff_P.simplify()) + + +def test_differential_operators_curvilinear_system(): + A = CoordSys3D('A', transformation="spherical", variable_names=["r", "theta", "phi"]) + B = CoordSys3D('B', transformation='cylindrical', variable_names=["r", "theta", "z"]) + # Test for spherical coordinate system and gradient + assert gradient(3*A.r + 4*A.theta) == 3*A.i + 4/A.r*A.j + assert gradient(3*A.r*A.phi + 4*A.theta) == 3*A.phi*A.i + 4/A.r*A.j + (3/sin(A.theta))*A.k + assert gradient(0*A.r + 0*A.theta+0*A.phi) == Vector.zero + assert gradient(A.r*A.theta*A.phi) == A.theta*A.phi*A.i + A.phi*A.j + (A.theta/sin(A.theta))*A.k + # Test for spherical coordinate system and divergence + assert divergence(A.r * A.i + A.theta * A.j + A.phi * A.k) == \ + (sin(A.theta)*A.r + cos(A.theta)*A.r*A.theta)/(sin(A.theta)*A.r**2) + 3 + 1/(sin(A.theta)*A.r) + assert divergence(3*A.r*A.phi*A.i + A.theta*A.j + A.r*A.theta*A.phi*A.k) == \ + (sin(A.theta)*A.r + cos(A.theta)*A.r*A.theta)/(sin(A.theta)*A.r**2) + 9*A.phi + A.theta/sin(A.theta) + assert divergence(Vector.zero) == 0 + assert divergence(0*A.i + 0*A.j + 0*A.k) == 0 + # Test for spherical coordinate system and curl + assert curl(A.r*A.i + A.theta*A.j + A.phi*A.k) == \ + (cos(A.theta)*A.phi/(sin(A.theta)*A.r))*A.i + (-A.phi/A.r)*A.j + A.theta/A.r*A.k + assert curl(A.r*A.j + A.phi*A.k) == (cos(A.theta)*A.phi/(sin(A.theta)*A.r))*A.i + (-A.phi/A.r)*A.j + 2*A.k + + # Test for cylindrical coordinate system and gradient + assert gradient(0*B.r + 0*B.theta+0*B.z) == Vector.zero + assert gradient(B.r*B.theta*B.z) == B.theta*B.z*B.i + B.z*B.j + B.r*B.theta*B.k + assert gradient(3*B.r) == 3*B.i + assert gradient(2*B.theta) == 2/B.r * B.j + assert gradient(4*B.z) == 4*B.k + # Test for cylindrical coordinate system and divergence + assert divergence(B.r*B.i + B.theta*B.j + B.z*B.k) == 3 + 1/B.r + assert divergence(B.r*B.j + B.z*B.k) == 1 + # Test for cylindrical coordinate system and curl + assert curl(B.r*B.j + B.z*B.k) == 2*B.k + assert curl(3*B.i + 2/B.r*B.j + 4*B.k) == Vector.zero + +def test_mixed_coordinates(): + # gradient + a = CoordSys3D('a') + b = CoordSys3D('b') + c = CoordSys3D('c') + assert gradient(a.x*b.y) == b.y*a.i + a.x*b.j + assert gradient(3*cos(q)*a.x*b.x+a.y*(a.x+(cos(q)+b.x))) ==\ + (a.y + 3*b.x*cos(q))*a.i + (a.x + b.x + cos(q))*a.j + (3*a.x*cos(q) + a.y)*b.i + # Some tests need further work: + # assert gradient(a.x*(cos(a.x+b.x))) == (cos(a.x + b.x))*a.i + a.x*Gradient(cos(a.x + b.x)) + # assert gradient(cos(a.x + b.x)*cos(a.x + b.z)) == Gradient(cos(a.x + b.x)*cos(a.x + b.z)) + assert gradient(a.x**b.y) == Gradient(a.x**b.y) + # assert gradient(cos(a.x+b.y)*a.z) == None + assert gradient(cos(a.x*b.y)) == Gradient(cos(a.x*b.y)) + assert gradient(3*cos(q)*a.x*b.x*a.z*a.y+ b.y*b.z + cos(a.x+a.y)*b.z) == \ + (3*a.y*a.z*b.x*cos(q) - b.z*sin(a.x + a.y))*a.i + \ + (3*a.x*a.z*b.x*cos(q) - b.z*sin(a.x + a.y))*a.j + (3*a.x*a.y*b.x*cos(q))*a.k + \ + (3*a.x*a.y*a.z*cos(q))*b.i + b.z*b.j + (b.y + cos(a.x + a.y))*b.k + # divergence + assert divergence(a.i*a.x+a.j*a.y+a.z*a.k + b.i*b.x+b.j*b.y+b.z*b.k + c.i*c.x+c.j*c.y+c.z*c.k) == S(9) + # assert divergence(3*a.i*a.x*cos(a.x+b.z) + a.j*b.x*c.z) == None + assert divergence(3*a.i*a.x*a.z + b.j*b.x*c.z + 3*a.j*a.z*a.y) == \ + 6*a.z + b.x*Dot(b.j, c.k) + assert divergence(3*cos(q)*a.x*b.x*b.i*c.x) == \ + 3*a.x*b.x*cos(q)*Dot(b.i, c.i) + 3*a.x*c.x*cos(q) + 3*b.x*c.x*cos(q)*Dot(b.i, a.i) + assert divergence(a.x*b.x*c.x*Cross(a.x*a.i, a.y*b.j)) ==\ + a.x*b.x*c.x*Divergence(Cross(a.x*a.i, a.y*b.j)) + \ + b.x*c.x*Dot(Cross(a.x*a.i, a.y*b.j), a.i) + \ + a.x*c.x*Dot(Cross(a.x*a.i, a.y*b.j), b.i) + \ + a.x*b.x*Dot(Cross(a.x*a.i, a.y*b.j), c.i) + assert divergence(a.x*b.x*c.x*(a.x*a.i + b.x*b.i)) == \ + 4*a.x*b.x*c.x +\ + a.x**2*c.x*Dot(a.i, b.i) +\ + a.x**2*b.x*Dot(a.i, c.i) +\ + b.x**2*c.x*Dot(b.i, a.i) +\ + a.x*b.x**2*Dot(b.i, c.i) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_functions.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..dfdf9821b6c853755ce12d0cbdfa599bd4f312e4 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_functions.py @@ -0,0 +1,184 @@ +from sympy.vector.vector import Vector +from sympy.vector.coordsysrect import CoordSys3D +from sympy.vector.functions import express, matrix_to_vector, orthogonalize +from sympy.core.numbers import Rational +from sympy.core.singleton import S +from sympy.core.symbol import symbols +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.matrices.immutable import ImmutableDenseMatrix as Matrix +from sympy.testing.pytest import raises + +N = CoordSys3D('N') +q1, q2, q3, q4, q5 = symbols('q1 q2 q3 q4 q5') +A = N.orient_new_axis('A', q1, N.k) # type: ignore +B = A.orient_new_axis('B', q2, A.i) +C = B.orient_new_axis('C', q3, B.j) + + +def test_express(): + assert express(Vector.zero, N) == Vector.zero + assert express(S.Zero, N) is S.Zero + assert express(A.i, C) == cos(q3)*C.i + sin(q3)*C.k + assert express(A.j, C) == sin(q2)*sin(q3)*C.i + cos(q2)*C.j - \ + sin(q2)*cos(q3)*C.k + assert express(A.k, C) == -sin(q3)*cos(q2)*C.i + sin(q2)*C.j + \ + cos(q2)*cos(q3)*C.k + assert express(A.i, N) == cos(q1)*N.i + sin(q1)*N.j + assert express(A.j, N) == -sin(q1)*N.i + cos(q1)*N.j + assert express(A.k, N) == N.k + assert express(A.i, A) == A.i + assert express(A.j, A) == A.j + assert express(A.k, A) == A.k + assert express(A.i, B) == B.i + assert express(A.j, B) == cos(q2)*B.j - sin(q2)*B.k + assert express(A.k, B) == sin(q2)*B.j + cos(q2)*B.k + assert express(A.i, C) == cos(q3)*C.i + sin(q3)*C.k + assert express(A.j, C) == sin(q2)*sin(q3)*C.i + cos(q2)*C.j - \ + sin(q2)*cos(q3)*C.k + assert express(A.k, C) == -sin(q3)*cos(q2)*C.i + sin(q2)*C.j + \ + cos(q2)*cos(q3)*C.k + # Check to make sure UnitVectors get converted properly + assert express(N.i, N) == N.i + assert express(N.j, N) == N.j + assert express(N.k, N) == N.k + assert express(N.i, A) == (cos(q1)*A.i - sin(q1)*A.j) + assert express(N.j, A) == (sin(q1)*A.i + cos(q1)*A.j) + assert express(N.k, A) == A.k + assert express(N.i, B) == (cos(q1)*B.i - sin(q1)*cos(q2)*B.j + + sin(q1)*sin(q2)*B.k) + assert express(N.j, B) == (sin(q1)*B.i + cos(q1)*cos(q2)*B.j - + sin(q2)*cos(q1)*B.k) + assert express(N.k, B) == (sin(q2)*B.j + cos(q2)*B.k) + assert express(N.i, C) == ( + (cos(q1)*cos(q3) - sin(q1)*sin(q2)*sin(q3))*C.i - + sin(q1)*cos(q2)*C.j + + (sin(q3)*cos(q1) + sin(q1)*sin(q2)*cos(q3))*C.k) + assert express(N.j, C) == ( + (sin(q1)*cos(q3) + sin(q2)*sin(q3)*cos(q1))*C.i + + cos(q1)*cos(q2)*C.j + + (sin(q1)*sin(q3) - sin(q2)*cos(q1)*cos(q3))*C.k) + assert express(N.k, C) == (-sin(q3)*cos(q2)*C.i + sin(q2)*C.j + + cos(q2)*cos(q3)*C.k) + + assert express(A.i, N) == (cos(q1)*N.i + sin(q1)*N.j) + assert express(A.j, N) == (-sin(q1)*N.i + cos(q1)*N.j) + assert express(A.k, N) == N.k + assert express(A.i, A) == A.i + assert express(A.j, A) == A.j + assert express(A.k, A) == A.k + assert express(A.i, B) == B.i + assert express(A.j, B) == (cos(q2)*B.j - sin(q2)*B.k) + assert express(A.k, B) == (sin(q2)*B.j + cos(q2)*B.k) + assert express(A.i, C) == (cos(q3)*C.i + sin(q3)*C.k) + assert express(A.j, C) == (sin(q2)*sin(q3)*C.i + cos(q2)*C.j - + sin(q2)*cos(q3)*C.k) + assert express(A.k, C) == (-sin(q3)*cos(q2)*C.i + sin(q2)*C.j + + cos(q2)*cos(q3)*C.k) + + assert express(B.i, N) == (cos(q1)*N.i + sin(q1)*N.j) + assert express(B.j, N) == (-sin(q1)*cos(q2)*N.i + + cos(q1)*cos(q2)*N.j + sin(q2)*N.k) + assert express(B.k, N) == (sin(q1)*sin(q2)*N.i - + sin(q2)*cos(q1)*N.j + cos(q2)*N.k) + assert express(B.i, A) == A.i + assert express(B.j, A) == (cos(q2)*A.j + sin(q2)*A.k) + assert express(B.k, A) == (-sin(q2)*A.j + cos(q2)*A.k) + assert express(B.i, B) == B.i + assert express(B.j, B) == B.j + assert express(B.k, B) == B.k + assert express(B.i, C) == (cos(q3)*C.i + sin(q3)*C.k) + assert express(B.j, C) == C.j + assert express(B.k, C) == (-sin(q3)*C.i + cos(q3)*C.k) + + assert express(C.i, N) == ( + (cos(q1)*cos(q3) - sin(q1)*sin(q2)*sin(q3))*N.i + + (sin(q1)*cos(q3) + sin(q2)*sin(q3)*cos(q1))*N.j - + sin(q3)*cos(q2)*N.k) + assert express(C.j, N) == ( + -sin(q1)*cos(q2)*N.i + cos(q1)*cos(q2)*N.j + sin(q2)*N.k) + assert express(C.k, N) == ( + (sin(q3)*cos(q1) + sin(q1)*sin(q2)*cos(q3))*N.i + + (sin(q1)*sin(q3) - sin(q2)*cos(q1)*cos(q3))*N.j + + cos(q2)*cos(q3)*N.k) + assert express(C.i, A) == (cos(q3)*A.i + sin(q2)*sin(q3)*A.j - + sin(q3)*cos(q2)*A.k) + assert express(C.j, A) == (cos(q2)*A.j + sin(q2)*A.k) + assert express(C.k, A) == (sin(q3)*A.i - sin(q2)*cos(q3)*A.j + + cos(q2)*cos(q3)*A.k) + assert express(C.i, B) == (cos(q3)*B.i - sin(q3)*B.k) + assert express(C.j, B) == B.j + assert express(C.k, B) == (sin(q3)*B.i + cos(q3)*B.k) + assert express(C.i, C) == C.i + assert express(C.j, C) == C.j + assert express(C.k, C) == C.k == (C.k) + + # Check to make sure Vectors get converted back to UnitVectors + assert N.i == express((cos(q1)*A.i - sin(q1)*A.j), N).simplify() + assert N.j == express((sin(q1)*A.i + cos(q1)*A.j), N).simplify() + assert N.i == express((cos(q1)*B.i - sin(q1)*cos(q2)*B.j + + sin(q1)*sin(q2)*B.k), N).simplify() + assert N.j == express((sin(q1)*B.i + cos(q1)*cos(q2)*B.j - + sin(q2)*cos(q1)*B.k), N).simplify() + assert N.k == express((sin(q2)*B.j + cos(q2)*B.k), N).simplify() + + + assert A.i == express((cos(q1)*N.i + sin(q1)*N.j), A).simplify() + assert A.j == express((-sin(q1)*N.i + cos(q1)*N.j), A).simplify() + + assert A.j == express((cos(q2)*B.j - sin(q2)*B.k), A).simplify() + assert A.k == express((sin(q2)*B.j + cos(q2)*B.k), A).simplify() + + assert A.i == express((cos(q3)*C.i + sin(q3)*C.k), A).simplify() + assert A.j == express((sin(q2)*sin(q3)*C.i + cos(q2)*C.j - + sin(q2)*cos(q3)*C.k), A).simplify() + + assert A.k == express((-sin(q3)*cos(q2)*C.i + sin(q2)*C.j + + cos(q2)*cos(q3)*C.k), A).simplify() + assert B.i == express((cos(q1)*N.i + sin(q1)*N.j), B).simplify() + assert B.j == express((-sin(q1)*cos(q2)*N.i + + cos(q1)*cos(q2)*N.j + sin(q2)*N.k), B).simplify() + + assert B.k == express((sin(q1)*sin(q2)*N.i - + sin(q2)*cos(q1)*N.j + cos(q2)*N.k), B).simplify() + + assert B.j == express((cos(q2)*A.j + sin(q2)*A.k), B).simplify() + assert B.k == express((-sin(q2)*A.j + cos(q2)*A.k), B).simplify() + assert B.i == express((cos(q3)*C.i + sin(q3)*C.k), B).simplify() + assert B.k == express((-sin(q3)*C.i + cos(q3)*C.k), B).simplify() + assert C.i == express((cos(q3)*A.i + sin(q2)*sin(q3)*A.j - + sin(q3)*cos(q2)*A.k), C).simplify() + assert C.j == express((cos(q2)*A.j + sin(q2)*A.k), C).simplify() + assert C.k == express((sin(q3)*A.i - sin(q2)*cos(q3)*A.j + + cos(q2)*cos(q3)*A.k), C).simplify() + assert C.i == express((cos(q3)*B.i - sin(q3)*B.k), C).simplify() + assert C.k == express((sin(q3)*B.i + cos(q3)*B.k), C).simplify() + + +def test_matrix_to_vector(): + m = Matrix([[1], [2], [3]]) + assert matrix_to_vector(m, C) == C.i + 2*C.j + 3*C.k + m = Matrix([[0], [0], [0]]) + assert matrix_to_vector(m, N) == matrix_to_vector(m, C) == \ + Vector.zero + m = Matrix([[q1], [q2], [q3]]) + assert matrix_to_vector(m, N) == q1*N.i + q2*N.j + q3*N.k + + +def test_orthogonalize(): + C = CoordSys3D('C') + a, b = symbols('a b', integer=True) + i, j, k = C.base_vectors() + v1 = i + 2*j + v2 = 2*i + 3*j + v3 = 3*i + 5*j + v4 = 3*i + j + v5 = 2*i + 2*j + v6 = a*i + b*j + v7 = 4*a*i + 4*b*j + assert orthogonalize(v1, v2) == [C.i + 2*C.j, C.i*Rational(2, 5) + -C.j/5] + # from wikipedia + assert orthogonalize(v4, v5, orthonormal=True) == \ + [(3*sqrt(10))*C.i/10 + (sqrt(10))*C.j/10, (-sqrt(10))*C.i/10 + (3*sqrt(10))*C.j/10] + raises(ValueError, lambda: orthogonalize(v1, v2, v3)) + raises(ValueError, lambda: orthogonalize(v6, v7)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_implicitregion.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_implicitregion.py new file mode 100644 index 0000000000000000000000000000000000000000..3686d847a7f165cb5ba9aeb813e5922aaa17e1e0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_implicitregion.py @@ -0,0 +1,90 @@ +from sympy.core.relational import Eq +from sympy.core.singleton import S +from sympy.abc import x, y, z, s, t +from sympy.sets import FiniteSet, EmptySet +from sympy.geometry import Point +from sympy.vector import ImplicitRegion +from sympy.testing.pytest import raises + + +def test_ImplicitRegion(): + ellipse = ImplicitRegion((x, y), (x**2/4 + y**2/16 - 1)) + assert ellipse.equation == x**2/4 + y**2/16 - 1 + assert ellipse.variables == (x, y) + assert ellipse.degree == 2 + r = ImplicitRegion((x, y, z), Eq(x**4 + y**2 - x*y, 6)) + assert r.equation == x**4 + y**2 - x*y - 6 + assert r.variables == (x, y, z) + assert r.degree == 4 + + +def test_regular_point(): + r1 = ImplicitRegion((x,), x**2 - 16) + assert r1.regular_point() == (-4,) + c1 = ImplicitRegion((x, y), x**2 + y**2 - 4) + assert c1.regular_point() == (0, -2) + c2 = ImplicitRegion((x, y), (x - S(5)/2)**2 + y**2 - (S(1)/4)**2) + assert c2.regular_point() == (S(5)/2, -S(1)/4) + c3 = ImplicitRegion((x, y), (y - 5)**2 - 16*(x - 5)) + assert c3.regular_point() == (5, 5) + r2 = ImplicitRegion((x, y), x**2 - 4*x*y - 3*y**2 + 4*x + 8*y - 5) + assert r2.regular_point() == (S(4)/7, S(9)/7) + r3 = ImplicitRegion((x, y), x**2 - 2*x*y + 3*y**2 - 2*x - 5*y + 3/2) + raises(ValueError, lambda: r3.regular_point()) + + +def test_singular_points_and_multiplicty(): + r1 = ImplicitRegion((x, y, z), Eq(x + y + z, 0)) + assert r1.singular_points() == EmptySet + r2 = ImplicitRegion((x, y, z), x*y*z + y**4 -x**2*z**2) + assert r2.singular_points() == FiniteSet((0, 0, z), (x, 0, 0)) + assert r2.multiplicity((0, 0, 0)) == 3 + assert r2.multiplicity((0, 0, 6)) == 2 + r3 = ImplicitRegion((x, y, z), z**2 - x**2 - y**2) + assert r3.singular_points() == FiniteSet((0, 0, 0)) + assert r3.multiplicity((0, 0, 0)) == 2 + r4 = ImplicitRegion((x, y), x**2 + y**2 - 2*x) + assert r4.singular_points() == EmptySet + assert r4.multiplicity(Point(1, 3)) == 0 + + +def test_rational_parametrization(): + p = ImplicitRegion((x,), x - 2) + assert p.rational_parametrization() == (x - 2,) + + line = ImplicitRegion((x, y), Eq(y, 3*x + 2)) + assert line.rational_parametrization() == (x, 3*x + 2) + + circle1 = ImplicitRegion((x, y), (x-2)**2 + (y+3)**2 - 4) + assert circle1.rational_parametrization(parameters=t) == (4*t/(t**2 + 1) + 2, 4*t**2/(t**2 + 1) - 5) + circle2 = ImplicitRegion((x, y), (x - S.Half)**2 + y**2 - (S(1)/2)**2) + + assert circle2.rational_parametrization(parameters=t) == (t/(t**2 + 1) + S(1)/2, t**2/(t**2 + 1) - S(1)/2) + circle3 = ImplicitRegion((x, y), Eq(x**2 + y**2, 2*x)) + assert circle3.rational_parametrization(parameters=(t,)) == (2*t/(t**2 + 1) + 1, 2*t**2/(t**2 + 1) - 1) + + parabola = ImplicitRegion((x, y), (y - 3)**2 - 4*(x + 6)) + assert parabola.rational_parametrization(t) == (-6 + 4/t**2, 3 + 4/t) + + rect_hyperbola = ImplicitRegion((x, y), x*y - 1) + assert rect_hyperbola.rational_parametrization(t) == (-1 + (t + 1)/t, t) + + cubic_curve = ImplicitRegion((x, y), x**3 + x**2 - y**2) + assert cubic_curve.rational_parametrization(parameters=(t)) == (t**2 - 1, t*(t**2 - 1)) + cuspidal = ImplicitRegion((x, y), (x**3 - y**2)) + assert cuspidal.rational_parametrization(t) == (t**2, t**3) + + I = ImplicitRegion((x, y), x**3 + x**2 - y**2) + assert I.rational_parametrization(t) == (t**2 - 1, t*(t**2 - 1)) + + sphere = ImplicitRegion((x, y, z), Eq(x**2 + y**2 + z**2, 2*x)) + assert sphere.rational_parametrization(parameters=(s, t)) == (2/(s**2 + t**2 + 1), 2*t/(s**2 + t**2 + 1), 2*s/(s**2 + t**2 + 1)) + + conic = ImplicitRegion((x, y), Eq(x**2 + 4*x*y + 3*y**2 + x - y + 10, 0)) + assert conic.rational_parametrization(t) == ( + S(17)/2 + 4/(3*t**2 + 4*t + 1), 4*t/(3*t**2 + 4*t + 1) - S(11)/2) + + r1 = ImplicitRegion((x, y), y**2 - x**3 + x) + raises(NotImplementedError, lambda: r1.rational_parametrization()) + r2 = ImplicitRegion((x, y), y**2 - x**3 - x**2 + 1) + raises(NotImplementedError, lambda: r2.rational_parametrization()) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_integrals.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_integrals.py new file mode 100644 index 0000000000000000000000000000000000000000..84c900d038e214df1ea59a8cd8fb2929005c3674 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_integrals.py @@ -0,0 +1,106 @@ +from sympy.core.numbers import pi +from sympy.core.singleton import S +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.testing.pytest import raises +from sympy.vector.coordsysrect import CoordSys3D +from sympy.vector.integrals import ParametricIntegral, vector_integrate +from sympy.vector.parametricregion import ParametricRegion +from sympy.vector.implicitregion import ImplicitRegion +from sympy.abc import x, y, z, u, v, r, t, theta, phi +from sympy.geometry import Point, Segment, Curve, Circle, Polygon, Plane + +C = CoordSys3D('C') + +def test_parametric_lineintegrals(): + halfcircle = ParametricRegion((4*cos(theta), 4*sin(theta)), (theta, -pi/2, pi/2)) + assert ParametricIntegral(C.x*C.y**4, halfcircle) == S(8192)/5 + + curve = ParametricRegion((t, t**2, t**3), (t, 0, 1)) + field1 = 8*C.x**2*C.y*C.z*C.i + 5*C.z*C.j - 4*C.x*C.y*C.k + assert ParametricIntegral(field1, curve) == 1 + line = ParametricRegion((4*t - 1, 2 - 2*t, t), (t, 0, 1)) + assert ParametricIntegral(C.x*C.z*C.i - C.y*C.z*C.k, line) == 3 + + assert ParametricIntegral(4*C.x**3, ParametricRegion((1, t), (t, 0, 2))) == 8 + + helix = ParametricRegion((cos(t), sin(t), 3*t), (t, 0, 4*pi)) + assert ParametricIntegral(C.x*C.y*C.z, helix) == -3*sqrt(10)*pi + + field2 = C.y*C.i + C.z*C.j + C.z*C.k + assert ParametricIntegral(field2, ParametricRegion((cos(t), sin(t), t**2), (t, 0, pi))) == -5*pi/2 + pi**4/2 + +def test_parametric_surfaceintegrals(): + + semisphere = ParametricRegion((2*sin(phi)*cos(theta), 2*sin(phi)*sin(theta), 2*cos(phi)),\ + (theta, 0, 2*pi), (phi, 0, pi/2)) + assert ParametricIntegral(C.z, semisphere) == 8*pi + + cylinder = ParametricRegion((sqrt(3)*cos(theta), sqrt(3)*sin(theta), z), (z, 0, 6), (theta, 0, 2*pi)) + assert ParametricIntegral(C.y, cylinder) == 0 + + cone = ParametricRegion((v*cos(u), v*sin(u), v), (u, 0, 2*pi), (v, 0, 1)) + assert ParametricIntegral(C.x*C.i + C.y*C.j + C.z**4*C.k, cone) == pi/3 + + triangle1 = ParametricRegion((x, y), (x, 0, 2), (y, 0, 10 - 5*x)) + triangle2 = ParametricRegion((x, y), (y, 0, 10 - 5*x), (x, 0, 2)) + assert ParametricIntegral(-15.6*C.y*C.k, triangle1) == ParametricIntegral(-15.6*C.y*C.k, triangle2) + assert ParametricIntegral(C.z, triangle1) == 10*C.z + +def test_parametric_volumeintegrals(): + + cube = ParametricRegion((x, y, z), (x, 0, 1), (y, 0, 1), (z, 0, 1)) + assert ParametricIntegral(1, cube) == 1 + + solidsphere1 = ParametricRegion((r*sin(phi)*cos(theta), r*sin(phi)*sin(theta), r*cos(phi)),\ + (r, 0, 2), (theta, 0, 2*pi), (phi, 0, pi)) + solidsphere2 = ParametricRegion((r*sin(phi)*cos(theta), r*sin(phi)*sin(theta), r*cos(phi)),\ + (r, 0, 2), (phi, 0, pi), (theta, 0, 2*pi)) + assert ParametricIntegral(C.x**2 + C.y**2, solidsphere1) == -256*pi/15 + assert ParametricIntegral(C.x**2 + C.y**2, solidsphere2) == 256*pi/15 + + region_under_plane1 = ParametricRegion((x, y, z), (x, 0, 3), (y, 0, -2*x/3 + 2),\ + (z, 0, 6 - 2*x - 3*y)) + region_under_plane2 = ParametricRegion((x, y, z), (x, 0, 3), (z, 0, 6 - 2*x - 3*y),\ + (y, 0, -2*x/3 + 2)) + + assert ParametricIntegral(C.x*C.i + C.j - 100*C.k, region_under_plane1) == \ + ParametricIntegral(C.x*C.i + C.j - 100*C.k, region_under_plane2) + assert ParametricIntegral(2*C.x, region_under_plane2) == -9 + +def test_vector_integrate(): + halfdisc = ParametricRegion((r*cos(theta), r* sin(theta)), (r, -2, 2), (theta, 0, pi)) + assert vector_integrate(C.x**2, halfdisc) == 4*pi + assert vector_integrate(C.x, ParametricRegion((t, t**2), (t, 2, 3))) == -17*sqrt(17)/12 + 37*sqrt(37)/12 + + assert vector_integrate(C.y**3*C.z, (C.x, 0, 3), (C.y, -1, 4)) == 765*C.z/4 + + s1 = Segment(Point(0, 0), Point(0, 1)) + assert vector_integrate(-15*C.y, s1) == S(-15)/2 + s2 = Segment(Point(4, 3, 9), Point(1, 1, 7)) + assert vector_integrate(C.y*C.i, s2) == -6 + + curve = Curve((sin(t), cos(t)), (t, 0, 2)) + assert vector_integrate(5*C.z, curve) == 10*C.z + + c1 = Circle(Point(2, 3), 6) + assert vector_integrate(C.x*C.y, c1) == 72*pi + c2 = Circle(Point(0, 0), Point(1, 1), Point(1, 0)) + assert vector_integrate(1, c2) == c2.circumference + + triangle = Polygon((0, 0), (1, 0), (1, 1)) + assert vector_integrate(C.x*C.i - 14*C.y*C.j, triangle) == 0 + p1, p2, p3, p4 = [(0, 0), (1, 0), (5, 1), (0, 1)] + poly = Polygon(p1, p2, p3, p4) + assert vector_integrate(-23*C.z, poly) == -161*C.z - 23*sqrt(17)*C.z + + point = Point(2, 3) + assert vector_integrate(C.i*C.y, point) == ParametricIntegral(C.y*C.i, ParametricRegion((2, 3))) + + c3 = ImplicitRegion((x, y), x**2 + y**2 - 4) + assert vector_integrate(45, c3) == 180*pi + c4 = ImplicitRegion((x, y), (x - 3)**2 + (y - 4)**2 - 9) + assert vector_integrate(1, c4) == 6*pi + + pl = Plane(Point(1, 1, 1), Point(2, 3, 4), Point(2, 2, 2)) + raises(ValueError, lambda: vector_integrate(C.x*C.z*C.i + C.k, pl)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_operators.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_operators.py new file mode 100644 index 0000000000000000000000000000000000000000..5734edadd00547c67d6f864b50afd966ad8392a6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_operators.py @@ -0,0 +1,43 @@ +from sympy.vector import CoordSys3D, Gradient, Divergence, Curl, VectorZero, Laplacian +from sympy.printing.repr import srepr + +R = CoordSys3D('R') +s1 = R.x*R.y*R.z # type: ignore +s2 = R.x + 3*R.y**2 # type: ignore +s3 = R.x**2 + R.y**2 + R.z**2 # type: ignore +v1 = R.x*R.i + R.z*R.z*R.j # type: ignore +v2 = R.x*R.i + R.y*R.j + R.z*R.k # type: ignore +v3 = R.x**2*R.i + R.y**2*R.j + R.z**2*R.k # type: ignore + + +def test_Gradient(): + assert Gradient(s1) == Gradient(R.x*R.y*R.z) + assert Gradient(s2) == Gradient(R.x + 3*R.y**2) + assert Gradient(s1).doit() == R.y*R.z*R.i + R.x*R.z*R.j + R.x*R.y*R.k + assert Gradient(s2).doit() == R.i + 6*R.y*R.j + + +def test_Divergence(): + assert Divergence(v1) == Divergence(R.x*R.i + R.z*R.z*R.j) + assert Divergence(v2) == Divergence(R.x*R.i + R.y*R.j + R.z*R.k) + assert Divergence(v1).doit() == 1 + assert Divergence(v2).doit() == 3 + # issue 22384 + Rc = CoordSys3D('R', transformation='cylindrical') + assert Divergence(Rc.i).doit() == 1/Rc.r + + +def test_Curl(): + assert Curl(v1) == Curl(R.x*R.i + R.z*R.z*R.j) + assert Curl(v2) == Curl(R.x*R.i + R.y*R.j + R.z*R.k) + assert Curl(v1).doit() == (-2*R.z)*R.i + assert Curl(v2).doit() == VectorZero() + + +def test_Laplacian(): + assert Laplacian(s3) == Laplacian(R.x**2 + R.y**2 + R.z**2) + assert Laplacian(v3) == Laplacian(R.x**2*R.i + R.y**2*R.j + R.z**2*R.k) + assert Laplacian(s3).doit() == 6 + assert Laplacian(v3).doit() == 2*R.i + 2*R.j + 2*R.k + assert srepr(Laplacian(s3)) == \ + 'Laplacian(Add(Pow(R.x, Integer(2)), Pow(R.y, Integer(2)), Pow(R.z, Integer(2))))' diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_parametricregion.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_parametricregion.py new file mode 100644 index 0000000000000000000000000000000000000000..e785b96744f9e2c39e91b997fcb70f8a921256bd --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_parametricregion.py @@ -0,0 +1,97 @@ +from sympy.core.numbers import pi +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.vector.coordsysrect import CoordSys3D +from sympy.vector.parametricregion import ParametricRegion, parametric_region_list +from sympy.geometry import Point, Segment, Curve, Ellipse, Line, Parabola, Polygon +from sympy.testing.pytest import raises +from sympy.abc import a, b, r, t, x, y, z, theta, phi + + +C = CoordSys3D('C') + +def test_ParametricRegion(): + + point = ParametricRegion((3, 4)) + assert point.definition == (3, 4) + assert point.parameters == () + assert point.limits == {} + assert point.dimensions == 0 + + # line x = y + line_xy = ParametricRegion((y, y), (y, 1, 5)) + assert line_xy .definition == (y, y) + assert line_xy.parameters == (y,) + assert line_xy.dimensions == 1 + + # line y = z + line_yz = ParametricRegion((x,t,t), x, (t, 1, 2)) + assert line_yz.definition == (x,t,t) + assert line_yz.parameters == (x, t) + assert line_yz.limits == {t: (1, 2)} + assert line_yz.dimensions == 1 + + p1 = ParametricRegion((9*a, -16*b), (a, 0, 2), (b, -1, 5)) + assert p1.definition == (9*a, -16*b) + assert p1.parameters == (a, b) + assert p1.limits == {a: (0, 2), b: (-1, 5)} + assert p1.dimensions == 2 + + p2 = ParametricRegion((t, t**3), t) + assert p2.parameters == (t,) + assert p2.limits == {} + assert p2.dimensions == 0 + + circle = ParametricRegion((r*cos(theta), r*sin(theta)), r, (theta, 0, 2*pi)) + assert circle.definition == (r*cos(theta), r*sin(theta)) + assert circle.dimensions == 1 + + halfdisc = ParametricRegion((r*cos(theta), r*sin(theta)), (r, -2, 2), (theta, 0, pi)) + assert halfdisc.definition == (r*cos(theta), r*sin(theta)) + assert halfdisc.parameters == (r, theta) + assert halfdisc.limits == {r: (-2, 2), theta: (0, pi)} + assert halfdisc.dimensions == 2 + + ellipse = ParametricRegion((a*cos(t), b*sin(t)), (t, 0, 8)) + assert ellipse.parameters == (t,) + assert ellipse.limits == {t: (0, 8)} + assert ellipse.dimensions == 1 + + cylinder = ParametricRegion((r*cos(theta), r*sin(theta), z), (r, 0, 1), (theta, 0, 2*pi), (z, 0, 4)) + assert cylinder.parameters == (r, theta, z) + assert cylinder.dimensions == 3 + + sphere = ParametricRegion((r*sin(phi)*cos(theta),r*sin(phi)*sin(theta), r*cos(phi)), + r, (theta, 0, 2*pi), (phi, 0, pi)) + assert sphere.definition == (r*sin(phi)*cos(theta),r*sin(phi)*sin(theta), r*cos(phi)) + assert sphere.parameters == (r, theta, phi) + assert sphere.dimensions == 2 + + raises(ValueError, lambda: ParametricRegion((a*t**2, 2*a*t), (a, -2))) + raises(ValueError, lambda: ParametricRegion((a, b), (a**2, sin(b)), (a, 2, 4, 6))) + + +def test_parametric_region_list(): + + point = Point(-5, 12) + assert parametric_region_list(point) == [ParametricRegion((-5, 12))] + + e = Ellipse(Point(2, 8), 2, 6) + assert parametric_region_list(e, t) == [ParametricRegion((2*cos(t) + 2, 6*sin(t) + 8), (t, 0, 2*pi))] + + c = Curve((t, t**3), (t, 5, 3)) + assert parametric_region_list(c) == [ParametricRegion((t, t**3), (t, 5, 3))] + + s = Segment(Point(2, 11, -6), Point(0, 2, 5)) + assert parametric_region_list(s, t) == [ParametricRegion((2 - 2*t, 11 - 9*t, 11*t - 6), (t, 0, 1))] + s1 = Segment(Point(0, 0), (1, 0)) + assert parametric_region_list(s1, t) == [ParametricRegion((t, 0), (t, 0, 1))] + s2 = Segment(Point(1, 2, 3), Point(1, 2, 5)) + assert parametric_region_list(s2, t) == [ParametricRegion((1, 2, 2*t + 3), (t, 0, 1))] + s3 = Segment(Point(12, 56), Point(12, 56)) + assert parametric_region_list(s3) == [ParametricRegion((12, 56))] + + poly = Polygon((1,3), (-3, 8), (2, 4)) + assert parametric_region_list(poly, t) == [ParametricRegion((1 - 4*t, 5*t + 3), (t, 0, 1)), ParametricRegion((5*t - 3, 8 - 4*t), (t, 0, 1)), ParametricRegion((2 - t, 4 - t), (t, 0, 1))] + + p1 = Parabola(Point(0, 0), Line(Point(5, 8), Point(7,8))) + raises(ValueError, lambda: parametric_region_list(p1)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_printing.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_printing.py new file mode 100644 index 0000000000000000000000000000000000000000..ae76905e967bdf93485f135c6a69f968e1208986 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_printing.py @@ -0,0 +1,221 @@ +# -*- coding: utf-8 -*- +from sympy.core.function import Function +from sympy.integrals.integrals import Integral +from sympy.printing.latex import latex +from sympy.printing.pretty import pretty as xpretty +from sympy.vector import CoordSys3D, Del, Vector, express +from sympy.abc import a, b, c +from sympy.testing.pytest import XFAIL + + +def pretty(expr): + """ASCII pretty-printing""" + return xpretty(expr, use_unicode=False, wrap_line=False) + + +def upretty(expr): + """Unicode pretty-printing""" + return xpretty(expr, use_unicode=True, wrap_line=False) + + +# Initialize the basic and tedious vector/dyadic expressions +# needed for testing. +# Some of the pretty forms shown denote how the expressions just +# above them should look with pretty printing. +N = CoordSys3D('N') +C = N.orient_new_axis('C', a, N.k) # type: ignore +v = [] +d = [] +v.append(Vector.zero) +v.append(N.i) # type: ignore +v.append(-N.i) # type: ignore +v.append(N.i + N.j) # type: ignore +v.append(a*N.i) # type: ignore +v.append(a*N.i - b*N.j) # type: ignore +v.append((a**2 + N.x)*N.i + N.k) # type: ignore +v.append((a**2 + b)*N.i + 3*(C.y - c)*N.k) # type: ignore +f = Function('f') +v.append(N.j - (Integral(f(b)) - C.x**2)*N.k) # type: ignore +upretty_v_8 = """\ + ⎛ 2 ⌠ ⎞ \n\ +j_N + ⎜x_C - ⎮ f(b) db⎟ k_N\n\ + ⎝ ⌡ ⎠ \ +""" +pretty_v_8 = """\ +j_N + / / \\\n\ + | 2 | |\n\ + |x_C - | f(b) db|\n\ + | | |\n\ + \\ / / \ +""" + +v.append(N.i + C.k) # type: ignore +v.append(express(N.i, C)) # type: ignore +v.append((a**2 + b)*N.i + (Integral(f(b)))*N.k) # type: ignore +upretty_v_11 = """\ +⎛ 2 ⎞ ⎛⌠ ⎞ \n\ +⎝a + b⎠ i_N + ⎜⎮ f(b) db⎟ k_N\n\ + ⎝⌡ ⎠ \ +""" +pretty_v_11 = """\ +/ 2 \\ + / / \\\n\ +\\a + b/ i_N| | |\n\ + | | f(b) db|\n\ + | | |\n\ + \\/ / \ +""" + +for x in v: + d.append(x | N.k) # type: ignore +s = 3*N.x**2*C.y # type: ignore +upretty_s = """\ + 2\n\ +3⋅y_C⋅x_N \ +""" +pretty_s = """\ + 2\n\ +3*y_C*x_N \ +""" + +# This is the pretty form for ((a**2 + b)*N.i + 3*(C.y - c)*N.k) | N.k +upretty_d_7 = """\ +⎛ 2 ⎞ \n\ +⎝a + b⎠ (i_N|k_N) + (3⋅y_C - 3⋅c) (k_N|k_N)\ +""" +pretty_d_7 = """\ +/ 2 \\ (i_N|k_N) + (3*y_C - 3*c) (k_N|k_N)\n\ +\\a + b/ \ +""" + + +def test_str_printing(): + assert str(v[0]) == '0' + assert str(v[1]) == 'N.i' + assert str(v[2]) == '(-1)*N.i' + assert str(v[3]) == 'N.i + N.j' + assert str(v[8]) == 'N.j + (C.x**2 - Integral(f(b), b))*N.k' + assert str(v[9]) == 'C.k + N.i' + assert str(s) == '3*C.y*N.x**2' + assert str(d[0]) == '0' + assert str(d[1]) == '(N.i|N.k)' + assert str(d[4]) == 'a*(N.i|N.k)' + assert str(d[5]) == 'a*(N.i|N.k) + (-b)*(N.j|N.k)' + assert str(d[8]) == ('(N.j|N.k) + (C.x**2 - ' + + 'Integral(f(b), b))*(N.k|N.k)') + + +@XFAIL +def test_pretty_printing_ascii(): + assert pretty(v[0]) == '0' + assert pretty(v[1]) == 'i_N' + assert pretty(v[5]) == '(a) i_N + (-b) j_N' + assert pretty(v[8]) == pretty_v_8 + assert pretty(v[2]) == '(-1) i_N' + assert pretty(v[11]) == pretty_v_11 + assert pretty(s) == pretty_s + assert pretty(d[0]) == '(0|0)' + assert pretty(d[5]) == '(a) (i_N|k_N) + (-b) (j_N|k_N)' + assert pretty(d[7]) == pretty_d_7 + assert pretty(d[10]) == '(cos(a)) (i_C|k_N) + (-sin(a)) (j_C|k_N)' + + +def test_pretty_print_unicode_v(): + assert upretty(v[0]) == '0' + assert upretty(v[1]) == 'i_N' + assert upretty(v[5]) == '(a) i_N + (-b) j_N' + # Make sure the printing works in other objects + assert upretty(v[5].args) == '((a) i_N, (-b) j_N)' + assert upretty(v[8]) == upretty_v_8 + assert upretty(v[2]) == '(-1) i_N' + assert upretty(v[11]) == upretty_v_11 + assert upretty(s) == upretty_s + assert upretty(d[0]) == '(0|0)' + assert upretty(d[5]) == '(a) (i_N|k_N) + (-b) (j_N|k_N)' + assert upretty(d[7]) == upretty_d_7 + assert upretty(d[10]) == '(cos(a)) (i_C|k_N) + (-sin(a)) (j_C|k_N)' + + +def test_latex_printing(): + assert latex(v[0]) == '\\mathbf{\\hat{0}}' + assert latex(v[1]) == '\\mathbf{\\hat{i}_{N}}' + assert latex(v[2]) == '- \\mathbf{\\hat{i}_{N}}' + assert latex(v[5]) == ('\\left(a\\right)\\mathbf{\\hat{i}_{N}} + ' + + '\\left(- b\\right)\\mathbf{\\hat{j}_{N}}') + assert latex(v[6]) == ('\\left(\\mathbf{{x}_{N}} + a^{2}\\right)\\mathbf{\\hat{i}_' + + '{N}} + \\mathbf{\\hat{k}_{N}}') + assert latex(v[8]) == ('\\mathbf{\\hat{j}_{N}} + \\left(\\mathbf{{x}_' + + '{C}}^{2} - \\int f{\\left(b \\right)}\\,' + + ' db\\right)\\mathbf{\\hat{k}_{N}}') + assert latex(s) == '3 \\mathbf{{y}_{C}} \\mathbf{{x}_{N}}^{2}' + assert latex(d[0]) == '(\\mathbf{\\hat{0}}|\\mathbf{\\hat{0}})' + assert latex(d[4]) == ('\\left(a\\right)\\left(\\mathbf{\\hat{i}_{N}}{\\middle|}' + + '\\mathbf{\\hat{k}_{N}}\\right)') + assert latex(d[9]) == ('\\left(\\mathbf{\\hat{k}_{C}}{\\middle|}' + + '\\mathbf{\\hat{k}_{N}}\\right) + \\left(' + + '\\mathbf{\\hat{i}_{N}}{\\middle|}\\mathbf{' + + '\\hat{k}_{N}}\\right)') + assert latex(d[11]) == ('\\left(a^{2} + b\\right)\\left(\\mathbf{\\hat{i}_{N}}' + + '{\\middle|}\\mathbf{\\hat{k}_{N}}\\right) + ' + + '\\left(\\int f{\\left(b \\right)}\\, db\\right)\\left(' + + '\\mathbf{\\hat{k}_{N}}{\\middle|}\\mathbf{' + + '\\hat{k}_{N}}\\right)') + +def test_issue_23058(): + from sympy import symbols, sin, cos, pi, UnevaluatedExpr + + delop = Del() + CC_ = CoordSys3D("C") + y = CC_.y + xhat = CC_.i + + t = symbols("t") + ten = symbols("10", positive=True) + eps, mu = 4*pi*ten**(-11), ten**(-5) + + Bx = 2 * ten**(-4) * cos(ten**5 * t) * sin(ten**(-3) * y) + vecB = Bx * xhat + vecE = (1/eps) * Integral(delop.cross(vecB/mu).doit(), t) + vecE = vecE.doit() + + vecB_str = """\ +⎛ ⎛y_C⎞ ⎛ 5 ⎞⎞ \n\ +⎜2⋅sin⎜───⎟⋅cos⎝10 ⋅t⎠⎟ i_C\n\ +⎜ ⎜ 3⎟ ⎟ \n\ +⎜ ⎝10 ⎠ ⎟ \n\ +⎜─────────────────────⎟ \n\ +⎜ 4 ⎟ \n\ +⎝ 10 ⎠ \ +""" + vecE_str = """\ +⎛ 4 ⎛ 5 ⎞ ⎛y_C⎞ ⎞ \n\ +⎜-10 ⋅sin⎝10 ⋅t⎠⋅cos⎜───⎟ ⎟ k_C\n\ +⎜ ⎜ 3⎟ ⎟ \n\ +⎜ ⎝10 ⎠ ⎟ \n\ +⎜─────────────────────────⎟ \n\ +⎝ 2⋅π ⎠ \ +""" + + assert upretty(vecB) == vecB_str + assert upretty(vecE) == vecE_str + + ten = UnevaluatedExpr(10) + eps, mu = 4*pi*ten**(-11), ten**(-5) + + Bx = 2 * ten**(-4) * cos(ten**5 * t) * sin(ten**(-3) * y) + vecB = Bx * xhat + + vecB_str = """\ +⎛ -4 ⎛ 5⎞ ⎛ -3⎞⎞ \n\ +⎝2⋅10 ⋅cos⎝t⋅10 ⎠⋅sin⎝y_C⋅10 ⎠⎠ i_C \ +""" + assert upretty(vecB) == vecB_str + +def test_custom_names(): + A = CoordSys3D('A', vector_names=['x', 'y', 'z'], + variable_names=['i', 'j', 'k']) + assert A.i.__str__() == 'A.i' + assert A.x.__str__() == 'A.x' + assert A.i._pretty_form == 'i_A' + assert A.x._pretty_form == 'x_A' + assert A.i._latex_form == r'\mathbf{{i}_{A}}' + assert A.x._latex_form == r"\mathbf{\hat{x}_{A}}" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_vector.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_vector.py new file mode 100644 index 0000000000000000000000000000000000000000..daba6d6a02c87b41a8bf801eee9b9045897d0003 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/tests/test_vector.py @@ -0,0 +1,342 @@ +from sympy.core import Rational, S, Add, Mul, I +from sympy.simplify import simplify, trigsimp +from sympy.core.function import (Derivative, Function, diff) +from sympy.core.numbers import pi +from sympy.core.symbol import symbols +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.functions.elementary.trigonometric import (cos, sin) +from sympy.integrals.integrals import Integral +from sympy.matrices.immutable import ImmutableDenseMatrix as Matrix +from sympy.vector.vector import Vector, BaseVector, VectorAdd, \ + VectorMul, VectorZero +from sympy.vector.coordsysrect import CoordSys3D +from sympy.vector.vector import Cross, Dot, cross +from sympy.testing.pytest import raises +from sympy.vector.kind import VectorKind +from sympy.core.kind import NumberKind +from sympy.testing.pytest import XFAIL + + +C = CoordSys3D('C') + +i, j, k = C.base_vectors() +a, b, c = symbols('a b c') + + +def test_cross(): + v1 = C.x * i + C.z * C.z * j + v2 = C.x * i + C.y * j + C.z * k + assert Cross(v1, v2) == Cross(C.x*C.i + C.z**2*C.j, C.x*C.i + C.y*C.j + C.z*C.k) + assert Cross(v1, v2).doit() == C.z**3*C.i + (-C.x*C.z)*C.j + (C.x*C.y - C.x*C.z**2)*C.k + assert cross(v1, v2) == C.z**3*C.i + (-C.x*C.z)*C.j + (C.x*C.y - C.x*C.z**2)*C.k + assert Cross(v1, v2) == -Cross(v2, v1) + # XXX: Cannot use Cross here. See XFAIL test below: + assert cross(v1, v2) + cross(v2, v1) == Vector.zero + + +@XFAIL +def test_cross_xfail(): + v1 = C.x * i + C.z * C.z * j + v2 = C.x * i + C.y * j + C.z * k + assert Cross(v1, v2) + Cross(v2, v1) == Vector.zero + + +def test_dot(): + v1 = C.x * i + C.z * C.z * j + v2 = C.x * i + C.y * j + C.z * k + assert Dot(v1, v2) == Dot(C.x*C.i + C.z**2*C.j, C.x*C.i + C.y*C.j + C.z*C.k) + assert Dot(v1, v2).doit() == C.x**2 + C.y*C.z**2 + assert Dot(v2, v1).doit() == C.x**2 + C.y*C.z**2 + assert Dot(v1, v2) == Dot(v2, v1) + + +def test_vector_sympy(): + """ + Test whether the Vector framework confirms to the hashing + and equality testing properties of SymPy. + """ + v1 = 3*j + assert v1 == j*3 + assert v1.components == {j: 3} + v2 = 3*i + 4*j + 5*k + v3 = 2*i + 4*j + i + 4*k + k + assert v3 == v2 + assert v3.__hash__() == v2.__hash__() + + +def test_kind(): + assert C.i.kind is VectorKind(NumberKind) + assert C.j.kind is VectorKind(NumberKind) + assert C.k.kind is VectorKind(NumberKind) + + assert C.x.kind is NumberKind + assert C.y.kind is NumberKind + assert C.z.kind is NumberKind + + assert Mul._kind_dispatcher(NumberKind, VectorKind(NumberKind)) is VectorKind(NumberKind) + assert Mul(2, C.i).kind is VectorKind(NumberKind) + + v1 = C.x * i + C.z * C.z * j + v2 = C.x * i + C.y * j + C.z * k + assert v1.kind is VectorKind(NumberKind) + assert v2.kind is VectorKind(NumberKind) + + assert (v1 + v2).kind is VectorKind(NumberKind) + assert Add(v1, v2).kind is VectorKind(NumberKind) + assert Cross(v1, v2).doit().kind is VectorKind(NumberKind) + assert VectorAdd(v1, v2).kind is VectorKind(NumberKind) + assert VectorMul(2, v1).kind is VectorKind(NumberKind) + assert VectorZero().kind is VectorKind(NumberKind) + + assert v1.projection(v2).kind is VectorKind(NumberKind) + assert v2.projection(v1).kind is VectorKind(NumberKind) + + +def test_vectoradd(): + assert isinstance(Add(C.i, C.j), VectorAdd) + v1 = C.x * i + C.z * C.z * j + v2 = C.x * i + C.y * j + C.z * k + assert isinstance(Add(v1, v2), VectorAdd) + + # https://github.com/sympy/sympy/issues/26121 + + E = Matrix([C.i, C.j, C.k]).T + a = Matrix([1, 2, 3]) + av = E*a + + assert av[0].kind == VectorKind() + assert isinstance(av[0], VectorAdd) + + +def test_vector(): + assert isinstance(i, BaseVector) + assert i != j + assert j != k + assert k != i + assert i - i == Vector.zero + assert i + Vector.zero == i + assert i - Vector.zero == i + assert Vector.zero != 0 + assert -Vector.zero == Vector.zero + + v1 = a*i + b*j + c*k + v2 = a**2*i + b**2*j + c**2*k + v3 = v1 + v2 + v4 = 2 * v1 + v5 = a * i + + assert isinstance(v1, VectorAdd) + assert v1 - v1 == Vector.zero + assert v1 + Vector.zero == v1 + assert v1.dot(i) == a + assert v1.dot(j) == b + assert v1.dot(k) == c + assert i.dot(v2) == a**2 + assert j.dot(v2) == b**2 + assert k.dot(v2) == c**2 + assert v3.dot(i) == a**2 + a + assert v3.dot(j) == b**2 + b + assert v3.dot(k) == c**2 + c + + assert v1 + v2 == v2 + v1 + assert v1 - v2 == -1 * (v2 - v1) + assert a * v1 == v1 * a + + assert isinstance(v5, VectorMul) + assert v5.base_vector == i + assert v5.measure_number == a + assert isinstance(v4, Vector) + assert isinstance(v4, VectorAdd) + assert isinstance(v4, Vector) + assert isinstance(Vector.zero, VectorZero) + assert isinstance(Vector.zero, Vector) + assert isinstance(v1 * 0, VectorZero) + + assert v1.to_matrix(C) == Matrix([[a], [b], [c]]) + + assert i.components == {i: 1} + assert v5.components == {i: a} + assert v1.components == {i: a, j: b, k: c} + + assert VectorAdd(v1, Vector.zero) == v1 + assert VectorMul(a, v1) == v1*a + assert VectorMul(1, i) == i + assert VectorAdd(v1, Vector.zero) == v1 + assert VectorMul(0, Vector.zero) == Vector.zero + raises(TypeError, lambda: v1.outer(1)) + raises(TypeError, lambda: v1.dot(1)) + + +def test_vector_magnitude_normalize(): + assert Vector.zero.magnitude() == 0 + assert Vector.zero.normalize() == Vector.zero + + assert i.magnitude() == 1 + assert j.magnitude() == 1 + assert k.magnitude() == 1 + assert i.normalize() == i + assert j.normalize() == j + assert k.normalize() == k + + v1 = a * i + assert v1.normalize() == (a/sqrt(a**2))*i + assert v1.magnitude() == sqrt(a**2) + + v2 = a*i + b*j + c*k + assert v2.magnitude() == sqrt(a**2 + b**2 + c**2) + assert v2.normalize() == v2 / v2.magnitude() + + v3 = i + j + assert v3.normalize() == (sqrt(2)/2)*C.i + (sqrt(2)/2)*C.j + + +def test_vector_simplify(): + A, s, k, m = symbols('A, s, k, m') + + test1 = (1 / a + 1 / b) * i + assert (test1 & i) != (a + b) / (a * b) + test1 = simplify(test1) + assert (test1 & i) == (a + b) / (a * b) + assert test1.simplify() == simplify(test1) + + test2 = (A**2 * s**4 / (4 * pi * k * m**3)) * i + test2 = simplify(test2) + assert (test2 & i) == (A**2 * s**4 / (4 * pi * k * m**3)) + + test3 = ((4 + 4 * a - 2 * (2 + 2 * a)) / (2 + 2 * a)) * i + test3 = simplify(test3) + assert (test3 & i) == 0 + + test4 = ((-4 * a * b**2 - 2 * b**3 - 2 * a**2 * b) / (a + b)**2) * i + test4 = simplify(test4) + assert (test4 & i) == -2 * b + + v = (sin(a)+cos(a))**2*i - j + assert trigsimp(v) == (2*sin(a + pi/4)**2)*i + (-1)*j + assert trigsimp(v) == v.trigsimp() + + assert simplify(Vector.zero) == Vector.zero + + +def test_vector_equals(): + assert (2*i).equals(j) is False + assert i.equals(i) is True + + # https://github.com/sympy/sympy/issues/25915 + A = (sqrt(2) + sqrt(6)) / sqrt(sqrt(3) + 2) + assert (A*i).equals(2*i) is True + assert (A*i).equals(3*i) is False + + # Test comparing vectors in different coordinate systems + D = C.orient_new_axis('D', pi/2, C.k) + assert (D.i).equals(C.j) is True + assert (D.i).equals(C.i) is False + + +def test_vector_conjugate(): + # https://github.com/sympy/sympy/issues/27094 + assert (I*i + (1 + I)*j + 2*k).conjugate() == -I*i + (1 - I)*j + 2*k + + +def test_vector_dot(): + assert i.dot(Vector.zero) == 0 + assert Vector.zero.dot(i) == 0 + assert i & Vector.zero == 0 + + assert i.dot(i) == 1 + assert i.dot(j) == 0 + assert i.dot(k) == 0 + assert i & i == 1 + assert i & j == 0 + assert i & k == 0 + + assert j.dot(i) == 0 + assert j.dot(j) == 1 + assert j.dot(k) == 0 + assert j & i == 0 + assert j & j == 1 + assert j & k == 0 + + assert k.dot(i) == 0 + assert k.dot(j) == 0 + assert k.dot(k) == 1 + assert k & i == 0 + assert k & j == 0 + assert k & k == 1 + + raises(TypeError, lambda: k.dot(1)) + + +def test_vector_cross(): + assert i.cross(Vector.zero) == Vector.zero + assert Vector.zero.cross(i) == Vector.zero + + assert i.cross(i) == Vector.zero + assert i.cross(j) == k + assert i.cross(k) == -j + assert i ^ i == Vector.zero + assert i ^ j == k + assert i ^ k == -j + + assert j.cross(i) == -k + assert j.cross(j) == Vector.zero + assert j.cross(k) == i + assert j ^ i == -k + assert j ^ j == Vector.zero + assert j ^ k == i + + assert k.cross(i) == j + assert k.cross(j) == -i + assert k.cross(k) == Vector.zero + assert k ^ i == j + assert k ^ j == -i + assert k ^ k == Vector.zero + + assert k.cross(1) == Cross(k, 1) + + +def test_projection(): + v1 = i + j + k + v2 = 3*i + 4*j + v3 = 0*i + 0*j + assert v1.projection(v1) == i + j + k + assert v1.projection(v2) == Rational(7, 3)*C.i + Rational(7, 3)*C.j + Rational(7, 3)*C.k + assert v1.projection(v1, scalar=True) == S.One + assert v1.projection(v2, scalar=True) == Rational(7, 3) + assert v3.projection(v1) == Vector.zero + assert v3.projection(v1, scalar=True) == S.Zero + + +def test_vector_diff_integrate(): + f = Function('f') + v = f(a)*C.i + a**2*C.j - C.k + assert Derivative(v, a) == Derivative((f(a))*C.i + + a**2*C.j + (-1)*C.k, a) + assert (diff(v, a) == v.diff(a) == Derivative(v, a).doit() == + (Derivative(f(a), a))*C.i + 2*a*C.j) + assert (Integral(v, a) == (Integral(f(a), a))*C.i + + (Integral(a**2, a))*C.j + (Integral(-1, a))*C.k) + + +def test_vector_args(): + raises(ValueError, lambda: BaseVector(3, C)) + raises(TypeError, lambda: BaseVector(0, Vector.zero)) + + +def test_srepr(): + from sympy.printing.repr import srepr + res = "CoordSys3D(Str('C'), Tuple(ImmutableDenseMatrix([[Integer(1), "\ + "Integer(0), Integer(0)], [Integer(0), Integer(1), Integer(0)], "\ + "[Integer(0), Integer(0), Integer(1)]]), VectorZero())).i" + assert srepr(C.i) == res + + +def test_scalar(): + from sympy.vector import CoordSys3D + C = CoordSys3D('C') + v1 = 3*C.i + 4*C.j + 5*C.k + v2 = 3*C.i - 4*C.j + 5*C.k + assert v1.is_Vector is True + assert v1.is_scalar is False + assert (v1.dot(v2)).is_scalar is True + assert (v1.cross(v2)).is_scalar is False diff --git a/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/vector.py b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/vector.py new file mode 100644 index 0000000000000000000000000000000000000000..c035ef48d2edd511f6cdbca19e965d99a2c8c66e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/sympy/vector/vector.py @@ -0,0 +1,714 @@ +from __future__ import annotations +from itertools import product + +from sympy.core import Add, Basic +from sympy.core.assumptions import StdFactKB +from sympy.core.expr import AtomicExpr, Expr +from sympy.core.power import Pow +from sympy.core.singleton import S +from sympy.core.sorting import default_sort_key +from sympy.core.sympify import sympify +from sympy.functions.elementary.miscellaneous import sqrt +from sympy.matrices.immutable import ImmutableDenseMatrix as Matrix +from sympy.vector.basisdependent import (BasisDependentZero, + BasisDependent, BasisDependentMul, BasisDependentAdd) +from sympy.vector.coordsysrect import CoordSys3D +from sympy.vector.dyadic import Dyadic, BaseDyadic, DyadicAdd +from sympy.vector.kind import VectorKind + + +class Vector(BasisDependent): + """ + Super class for all Vector classes. + Ideally, neither this class nor any of its subclasses should be + instantiated by the user. + """ + + is_scalar = False + is_Vector = True + _op_priority = 12.0 + + _expr_type: type[Vector] + _mul_func: type[Vector] + _add_func: type[Vector] + _zero_func: type[Vector] + _base_func: type[Vector] + zero: VectorZero + + kind: VectorKind = VectorKind() + + @property + def components(self): + """ + Returns the components of this vector in the form of a + Python dictionary mapping BaseVector instances to the + corresponding measure numbers. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> C = CoordSys3D('C') + >>> v = 3*C.i + 4*C.j + 5*C.k + >>> v.components + {C.i: 3, C.j: 4, C.k: 5} + + """ + # The '_components' attribute is defined according to the + # subclass of Vector the instance belongs to. + return self._components + + def magnitude(self): + """ + Returns the magnitude of this vector. + """ + return sqrt(self & self) + + def normalize(self): + """ + Returns the normalized version of this vector. + """ + return self / self.magnitude() + + def equals(self, other): + """ + Check if ``self`` and ``other`` are identically equal vectors. + + Explanation + =========== + + Checks if two vector expressions are equal for all possible values of + the symbols present in the expressions. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> from sympy.abc import x, y + >>> from sympy import pi + >>> C = CoordSys3D('C') + + Compare vectors that are equal or not: + + >>> C.i.equals(C.j) + False + >>> C.i.equals(C.i) + True + + These two vectors are equal if `x = y` but are not identically equal + as expressions since for some values of `x` and `y` they are unequal: + + >>> v1 = x*C.i + C.j + >>> v2 = y*C.i + C.j + >>> v1.equals(v1) + True + >>> v1.equals(v2) + False + + Vectors from different coordinate systems can be compared: + + >>> D = C.orient_new_axis('D', pi/2, C.i) + >>> D.j.equals(C.j) + False + >>> D.j.equals(C.k) + True + + Parameters + ========== + + other: Vector + The other vector expression to compare with. + + Returns + ======= + + ``True``, ``False`` or ``None``. A return value of ``True`` indicates + that the two vectors are identically equal. A return value of ``False`` + indicates that they are not. In some cases it is not possible to + determine if the two vectors are identically equal and ``None`` is + returned. + + See Also + ======== + + sympy.core.expr.Expr.equals + """ + diff = self - other + diff_mag2 = diff.dot(diff) + return diff_mag2.equals(0) + + def dot(self, other): + """ + Returns the dot product of this Vector, either with another + Vector, or a Dyadic, or a Del operator. + If 'other' is a Vector, returns the dot product scalar (SymPy + expression). + If 'other' is a Dyadic, the dot product is returned as a Vector. + If 'other' is an instance of Del, returns the directional + derivative operator as a Python function. If this function is + applied to a scalar expression, it returns the directional + derivative of the scalar field wrt this Vector. + + Parameters + ========== + + other: Vector/Dyadic/Del + The Vector or Dyadic we are dotting with, or a Del operator . + + Examples + ======== + + >>> from sympy.vector import CoordSys3D, Del + >>> C = CoordSys3D('C') + >>> delop = Del() + >>> C.i.dot(C.j) + 0 + >>> C.i & C.i + 1 + >>> v = 3*C.i + 4*C.j + 5*C.k + >>> v.dot(C.k) + 5 + >>> (C.i & delop)(C.x*C.y*C.z) + C.y*C.z + >>> d = C.i.outer(C.i) + >>> C.i.dot(d) + C.i + + """ + + # Check special cases + if isinstance(other, Dyadic): + if isinstance(self, VectorZero): + return Vector.zero + outvec = Vector.zero + for k, v in other.components.items(): + vect_dot = k.args[0].dot(self) + outvec += vect_dot * v * k.args[1] + return outvec + from sympy.vector.deloperator import Del + if not isinstance(other, (Del, Vector)): + raise TypeError(str(other) + " is not a vector, dyadic or " + + "del operator") + + # Check if the other is a del operator + if isinstance(other, Del): + def directional_derivative(field): + from sympy.vector.functions import directional_derivative + return directional_derivative(field, self) + return directional_derivative + + return dot(self, other) + + def __and__(self, other): + return self.dot(other) + + __and__.__doc__ = dot.__doc__ + + def cross(self, other): + """ + Returns the cross product of this Vector with another Vector or + Dyadic instance. + The cross product is a Vector, if 'other' is a Vector. If 'other' + is a Dyadic, this returns a Dyadic instance. + + Parameters + ========== + + other: Vector/Dyadic + The Vector or Dyadic we are crossing with. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> C = CoordSys3D('C') + >>> C.i.cross(C.j) + C.k + >>> C.i ^ C.i + 0 + >>> v = 3*C.i + 4*C.j + 5*C.k + >>> v ^ C.i + 5*C.j + (-4)*C.k + >>> d = C.i.outer(C.i) + >>> C.j.cross(d) + (-1)*(C.k|C.i) + + """ + + # Check special cases + if isinstance(other, Dyadic): + if isinstance(self, VectorZero): + return Dyadic.zero + outdyad = Dyadic.zero + for k, v in other.components.items(): + cross_product = self.cross(k.args[0]) + outer = cross_product.outer(k.args[1]) + outdyad += v * outer + return outdyad + + return cross(self, other) + + def __xor__(self, other): + return self.cross(other) + + __xor__.__doc__ = cross.__doc__ + + def outer(self, other): + """ + Returns the outer product of this vector with another, in the + form of a Dyadic instance. + + Parameters + ========== + + other : Vector + The Vector with respect to which the outer product is to + be computed. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> N = CoordSys3D('N') + >>> N.i.outer(N.j) + (N.i|N.j) + + """ + + # Handle the special cases + if not isinstance(other, Vector): + raise TypeError("Invalid operand for outer product") + elif (isinstance(self, VectorZero) or + isinstance(other, VectorZero)): + return Dyadic.zero + + # Iterate over components of both the vectors to generate + # the required Dyadic instance + args = [(v1 * v2) * BaseDyadic(k1, k2) for (k1, v1), (k2, v2) + in product(self.components.items(), other.components.items())] + + return DyadicAdd(*args) + + def projection(self, other, scalar=False): + """ + Returns the vector or scalar projection of the 'other' on 'self'. + + Examples + ======== + + >>> from sympy.vector.coordsysrect import CoordSys3D + >>> C = CoordSys3D('C') + >>> i, j, k = C.base_vectors() + >>> v1 = i + j + k + >>> v2 = 3*i + 4*j + >>> v1.projection(v2) + 7/3*C.i + 7/3*C.j + 7/3*C.k + >>> v1.projection(v2, scalar=True) + 7/3 + + """ + if self.equals(Vector.zero): + return S.Zero if scalar else Vector.zero + + if scalar: + return self.dot(other) / self.dot(self) + else: + return self.dot(other) / self.dot(self) * self + + @property + def _projections(self): + """ + Returns the components of this vector but the output includes + also zero values components. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D, Vector + >>> C = CoordSys3D('C') + >>> v1 = 3*C.i + 4*C.j + 5*C.k + >>> v1._projections + (3, 4, 5) + >>> v2 = C.x*C.y*C.z*C.i + >>> v2._projections + (C.x*C.y*C.z, 0, 0) + >>> v3 = Vector.zero + >>> v3._projections + (0, 0, 0) + """ + + from sympy.vector.operators import _get_coord_systems + if isinstance(self, VectorZero): + return (S.Zero, S.Zero, S.Zero) + base_vec = next(iter(_get_coord_systems(self))).base_vectors() + return tuple([self.dot(i) for i in base_vec]) + + def __or__(self, other): + return self.outer(other) + + __or__.__doc__ = outer.__doc__ + + def to_matrix(self, system): + """ + Returns the matrix form of this vector with respect to the + specified coordinate system. + + Parameters + ========== + + system : CoordSys3D + The system wrt which the matrix form is to be computed + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> C = CoordSys3D('C') + >>> from sympy.abc import a, b, c + >>> v = a*C.i + b*C.j + c*C.k + >>> v.to_matrix(C) + Matrix([ + [a], + [b], + [c]]) + + """ + + return Matrix([self.dot(unit_vec) for unit_vec in + system.base_vectors()]) + + def separate(self): + """ + The constituents of this vector in different coordinate systems, + as per its definition. + + Returns a dict mapping each CoordSys3D to the corresponding + constituent Vector. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> R1 = CoordSys3D('R1') + >>> R2 = CoordSys3D('R2') + >>> v = R1.i + R2.i + >>> v.separate() == {R1: R1.i, R2: R2.i} + True + + """ + + parts = {} + for vect, measure in self.components.items(): + parts[vect.system] = (parts.get(vect.system, Vector.zero) + + vect * measure) + return parts + + def _div_helper(one, other): + """ Helper for division involving vectors. """ + if isinstance(one, Vector) and isinstance(other, Vector): + raise TypeError("Cannot divide two vectors") + elif isinstance(one, Vector): + if other == S.Zero: + raise ValueError("Cannot divide a vector by zero") + return VectorMul(one, Pow(other, S.NegativeOne)) + else: + raise TypeError("Invalid division involving a vector") + +# The following is adapted from the matrices.expressions.matexpr file + +def get_postprocessor(cls): + def _postprocessor(expr): + vec_class = {Add: VectorAdd}[cls] + vectors = [] + for term in expr.args: + if isinstance(term.kind, VectorKind): + vectors.append(term) + + if vec_class == VectorAdd: + return VectorAdd(*vectors).doit(deep=False) + return _postprocessor + + +Basic._constructor_postprocessor_mapping[Vector] = { + "Add": [get_postprocessor(Add)], +} + +class BaseVector(Vector, AtomicExpr): + """ + Class to denote a base vector. + + """ + + def __new__(cls, index, system, pretty_str=None, latex_str=None): + if pretty_str is None: + pretty_str = "x{}".format(index) + if latex_str is None: + latex_str = "x_{}".format(index) + pretty_str = str(pretty_str) + latex_str = str(latex_str) + # Verify arguments + if index not in range(0, 3): + raise ValueError("index must be 0, 1 or 2") + if not isinstance(system, CoordSys3D): + raise TypeError("system should be a CoordSys3D") + name = system._vector_names[index] + # Initialize an object + obj = super().__new__(cls, S(index), system) + # Assign important attributes + obj._base_instance = obj + obj._components = {obj: S.One} + obj._measure_number = S.One + obj._name = system._name + '.' + name + obj._pretty_form = '' + pretty_str + obj._latex_form = latex_str + obj._system = system + # The _id is used for printing purposes + obj._id = (index, system) + assumptions = {'commutative': True} + obj._assumptions = StdFactKB(assumptions) + + # This attr is used for re-expression to one of the systems + # involved in the definition of the Vector. Applies to + # VectorMul and VectorAdd too. + obj._sys = system + + return obj + + @property + def system(self): + return self._system + + def _sympystr(self, printer): + return self._name + + def _sympyrepr(self, printer): + index, system = self._id + return printer._print(system) + '.' + system._vector_names[index] + + @property + def free_symbols(self): + return {self} + + def _eval_conjugate(self): + return self + + +class VectorAdd(BasisDependentAdd, Vector): + """ + Class to denote sum of Vector instances. + """ + + def __new__(cls, *args, **options): + obj = BasisDependentAdd.__new__(cls, *args, **options) + return obj + + def _sympystr(self, printer): + ret_str = '' + items = list(self.separate().items()) + items.sort(key=lambda x: x[0].__str__()) + for system, vect in items: + base_vects = system.base_vectors() + for x in base_vects: + if x in vect.components: + temp_vect = self.components[x] * x + ret_str += printer._print(temp_vect) + " + " + return ret_str[:-3] + + +class VectorMul(BasisDependentMul, Vector): + """ + Class to denote products of scalars and BaseVectors. + """ + + def __new__(cls, *args, **options): + obj = BasisDependentMul.__new__(cls, *args, **options) + return obj + + @property + def base_vector(self): + """ The BaseVector involved in the product. """ + return self._base_instance + + @property + def measure_number(self): + """ The scalar expression involved in the definition of + this VectorMul. + """ + return self._measure_number + + +class VectorZero(BasisDependentZero, Vector): + """ + Class to denote a zero vector + """ + + _op_priority = 12.1 + _pretty_form = '0' + _latex_form = r'\mathbf{\hat{0}}' + + def __new__(cls): + obj = BasisDependentZero.__new__(cls) + return obj + + +class Cross(Vector): + """ + Represents unevaluated Cross product. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D, Cross + >>> R = CoordSys3D('R') + >>> v1 = R.i + R.j + R.k + >>> v2 = R.x * R.i + R.y * R.j + R.z * R.k + >>> Cross(v1, v2) + Cross(R.i + R.j + R.k, R.x*R.i + R.y*R.j + R.z*R.k) + >>> Cross(v1, v2).doit() + (-R.y + R.z)*R.i + (R.x - R.z)*R.j + (-R.x + R.y)*R.k + + """ + + def __new__(cls, expr1, expr2): + expr1 = sympify(expr1) + expr2 = sympify(expr2) + if default_sort_key(expr1) > default_sort_key(expr2): + return -Cross(expr2, expr1) + obj = Expr.__new__(cls, expr1, expr2) + obj._expr1 = expr1 + obj._expr2 = expr2 + return obj + + def doit(self, **hints): + return cross(self._expr1, self._expr2) + + +class Dot(Expr): + """ + Represents unevaluated Dot product. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D, Dot + >>> from sympy import symbols + >>> R = CoordSys3D('R') + >>> a, b, c = symbols('a b c') + >>> v1 = R.i + R.j + R.k + >>> v2 = a * R.i + b * R.j + c * R.k + >>> Dot(v1, v2) + Dot(R.i + R.j + R.k, a*R.i + b*R.j + c*R.k) + >>> Dot(v1, v2).doit() + a + b + c + + """ + + def __new__(cls, expr1, expr2): + expr1 = sympify(expr1) + expr2 = sympify(expr2) + expr1, expr2 = sorted([expr1, expr2], key=default_sort_key) + obj = Expr.__new__(cls, expr1, expr2) + obj._expr1 = expr1 + obj._expr2 = expr2 + return obj + + def doit(self, **hints): + return dot(self._expr1, self._expr2) + + +def cross(vect1, vect2): + """ + Returns cross product of two vectors. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> from sympy.vector.vector import cross + >>> R = CoordSys3D('R') + >>> v1 = R.i + R.j + R.k + >>> v2 = R.x * R.i + R.y * R.j + R.z * R.k + >>> cross(v1, v2) + (-R.y + R.z)*R.i + (R.x - R.z)*R.j + (-R.x + R.y)*R.k + + """ + if isinstance(vect1, Add): + return VectorAdd.fromiter(cross(i, vect2) for i in vect1.args) + if isinstance(vect2, Add): + return VectorAdd.fromiter(cross(vect1, i) for i in vect2.args) + if isinstance(vect1, BaseVector) and isinstance(vect2, BaseVector): + if vect1._sys == vect2._sys: + n1 = vect1.args[0] + n2 = vect2.args[0] + if n1 == n2: + return Vector.zero + n3 = ({0,1,2}.difference({n1, n2})).pop() + sign = 1 if ((n1 + 1) % 3 == n2) else -1 + return sign*vect1._sys.base_vectors()[n3] + from .functions import express + try: + v = express(vect1, vect2._sys) + except ValueError: + return Cross(vect1, vect2) + else: + return cross(v, vect2) + if isinstance(vect1, VectorZero) or isinstance(vect2, VectorZero): + return Vector.zero + if isinstance(vect1, VectorMul): + v1, m1 = next(iter(vect1.components.items())) + return m1*cross(v1, vect2) + if isinstance(vect2, VectorMul): + v2, m2 = next(iter(vect2.components.items())) + return m2*cross(vect1, v2) + + return Cross(vect1, vect2) + + +def dot(vect1, vect2): + """ + Returns dot product of two vectors. + + Examples + ======== + + >>> from sympy.vector import CoordSys3D + >>> from sympy.vector.vector import dot + >>> R = CoordSys3D('R') + >>> v1 = R.i + R.j + R.k + >>> v2 = R.x * R.i + R.y * R.j + R.z * R.k + >>> dot(v1, v2) + R.x + R.y + R.z + + """ + if isinstance(vect1, Add): + return Add.fromiter(dot(i, vect2) for i in vect1.args) + if isinstance(vect2, Add): + return Add.fromiter(dot(vect1, i) for i in vect2.args) + if isinstance(vect1, BaseVector) and isinstance(vect2, BaseVector): + if vect1._sys == vect2._sys: + return S.One if vect1 == vect2 else S.Zero + from .functions import express + try: + v = express(vect2, vect1._sys) + except ValueError: + return Dot(vect1, vect2) + else: + return dot(vect1, v) + if isinstance(vect1, VectorZero) or isinstance(vect2, VectorZero): + return S.Zero + if isinstance(vect1, VectorMul): + v1, m1 = next(iter(vect1.components.items())) + return m1*dot(v1, vect2) + if isinstance(vect2, VectorMul): + v2, m2 = next(iter(vect2.components.items())) + return m2*dot(vect1, v2) + + return Dot(vect1, vect2) + + +Vector._expr_type = Vector +Vector._mul_func = VectorMul +Vector._add_func = VectorAdd +Vector._zero_func = VectorZero +Vector._base_func = BaseVector +Vector.zero = VectorZero() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C.cpython-311-x86_64-linux-gnu.so b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C.cpython-311-x86_64-linux-gnu.so new file mode 100644 index 0000000000000000000000000000000000000000..1bd8a06029d4f0a5886e0ef2633c0ea6c6e9acec Binary files /dev/null and b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C.cpython-311-x86_64-linux-gnu.so differ diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_VariableFunctions.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_VariableFunctions.pyi new file mode 100644 index 0000000000000000000000000000000000000000..9585b9e75133e8c2f19513e64e98911f50fa4d4c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_VariableFunctions.pyi @@ -0,0 +1,33889 @@ +# @generated by tools/pyi/gen_pyi.py from torch/_C/_VariableFunctions.pyi.in +# mypy: disable-error-code="type-arg" +# mypy: allow-untyped-defs +# ruff: noqa: F401,PYI054 + +from collections.abc import Callable, Sequence +from types import EllipsisType +from typing import Any, Literal, overload, TypeVar + +import torch +from torch import ( + contiguous_format, + Generator, + inf, + memory_format, + strided, + SymInt, + Tensor, +) +from torch._prims_common import DeviceLikeType +from torch.types import ( + _bool, + _complex, + _device, + _dtype, + _float, + _int, + _layout, + _qscheme, + _size, + Device, + Number, +) + +__all__ = [ + "__and__", + "__lshift__", + "__or__", + "__rshift__", + "__xor__", + "_adaptive_avg_pool2d", + "_adaptive_avg_pool3d", + "_add_batch_dim", + "_add_relu", + "_add_relu_", + "_addmm_activation", + "_aminmax", + "_amp_foreach_non_finite_check_and_unscale_", + "_amp_update_scale_", + "_assert_async", + "_assert_scalar", + "_assert_tensor_metadata", + "_batch_norm_impl_index", + "_cast_Byte", + "_cast_Char", + "_cast_Double", + "_cast_Float", + "_cast_Half", + "_cast_Int", + "_cast_Long", + "_cast_Short", + "_choose_qparams_per_tensor", + "_chunk_cat", + "_coalesce", + "_compute_linear_combination", + "_conj", + "_conj_copy", + "_conj_physical", + "_convert_indices_from_coo_to_csr", + "_convert_indices_from_csr_to_coo", + "_convert_weight_to_int4pack", + "_convert_weight_to_int4pack_for_cpu", + "_convolution", + "_convolution_mode", + "_copy_from", + "_copy_from_and_resize", + "_cslt_compress", + "_cslt_sparse_mm", + "_cslt_sparse_mm_search", + "_ctc_loss", + "_cudnn_ctc_loss", + "_cudnn_init_dropout_state", + "_cudnn_rnn", + "_cudnn_rnn_flatten_weight", + "_cufft_clear_plan_cache", + "_cufft_get_plan_cache_max_size", + "_cufft_get_plan_cache_size", + "_cufft_set_plan_cache_max_size", + "_cummax_helper", + "_cummin_helper", + "_debug_has_internal_overlap", + "_dim_arange", + "_dirichlet_grad", + "_disable_functionalization", + "_dyn_quant_matmul_4bit", + "_dyn_quant_pack_4bit_weight", + "_efficientzerotensor", + "_embedding_bag", + "_embedding_bag_forward_only", + "_empty_affine_quantized", + "_empty_per_channel_affine_quantized", + "_enable_functionalization", + "_euclidean_dist", + "_fake_quantize_learnable_per_channel_affine", + "_fake_quantize_learnable_per_tensor_affine", + "_fake_quantize_per_tensor_affine_cachemask_tensor_qparams", + "_fft_c2c", + "_fft_c2r", + "_fft_r2c", + "_fill_mem_eff_dropout_mask_", + "_flash_attention_forward_no_dropout_inplace", + "_foobar", + "_foreach_abs", + "_foreach_abs_", + "_foreach_acos", + "_foreach_acos_", + "_foreach_add", + "_foreach_add_", + "_foreach_addcdiv", + "_foreach_addcdiv_", + "_foreach_addcmul", + "_foreach_addcmul_", + "_foreach_asin", + "_foreach_asin_", + "_foreach_atan", + "_foreach_atan_", + "_foreach_ceil", + "_foreach_ceil_", + "_foreach_clamp_max", + "_foreach_clamp_max_", + "_foreach_clamp_min", + "_foreach_clamp_min_", + "_foreach_clone", + "_foreach_copy_", + "_foreach_cos", + "_foreach_cos_", + "_foreach_cosh", + "_foreach_cosh_", + "_foreach_div", + "_foreach_div_", + "_foreach_erf", + "_foreach_erf_", + "_foreach_erfc", + "_foreach_erfc_", + "_foreach_exp", + "_foreach_exp_", + "_foreach_expm1", + "_foreach_expm1_", + "_foreach_floor", + "_foreach_floor_", + "_foreach_frac", + "_foreach_frac_", + "_foreach_lerp", + "_foreach_lerp_", + "_foreach_lgamma", + "_foreach_lgamma_", + "_foreach_log", + "_foreach_log10", + "_foreach_log10_", + "_foreach_log1p", + "_foreach_log1p_", + "_foreach_log2", + "_foreach_log2_", + "_foreach_log_", + "_foreach_max", + "_foreach_maximum", + "_foreach_maximum_", + "_foreach_minimum", + "_foreach_minimum_", + "_foreach_mul", + "_foreach_mul_", + "_foreach_neg", + "_foreach_neg_", + "_foreach_norm", + "_foreach_pow", + "_foreach_pow_", + "_foreach_powsum", + "_foreach_reciprocal", + "_foreach_reciprocal_", + "_foreach_round", + "_foreach_round_", + "_foreach_rsqrt", + "_foreach_rsqrt_", + "_foreach_sigmoid", + "_foreach_sigmoid_", + "_foreach_sign", + "_foreach_sign_", + "_foreach_sin", + "_foreach_sin_", + "_foreach_sinh", + "_foreach_sinh_", + "_foreach_sqrt", + "_foreach_sqrt_", + "_foreach_sub", + "_foreach_sub_", + "_foreach_tan", + "_foreach_tan_", + "_foreach_tanh", + "_foreach_tanh_", + "_foreach_trunc", + "_foreach_trunc_", + "_foreach_zero_", + "_from_functional_tensor", + "_functional_assert_async", + "_functional_assert_scalar", + "_functional_sym_constrain_range", + "_functional_sym_constrain_range_for_size", + "_functionalize_apply_view_metas", + "_functionalize_are_all_mutations_hidden_from_autograd", + "_functionalize_are_all_mutations_under_no_grad_or_inference_mode", + "_functionalize_commit_update", + "_functionalize_has_metadata_mutation", + "_functionalize_inductor_storage_resized_counter", + "_functionalize_is_symbolic", + "_functionalize_mark_mutation_hidden_from_autograd", + "_functionalize_mark_storage_changed", + "_functionalize_mutation_counter", + "_functionalize_replace", + "_functionalize_storage_changed_counter", + "_functionalize_sync", + "_functionalize_unsafe_set", + "_functionalize_was_inductor_storage_resized", + "_functionalize_was_storage_changed", + "_fused_adagrad_", + "_fused_adam_", + "_fused_adamw_", + "_fused_dropout", + "_fused_moving_avg_obs_fq_helper", + "_fused_rms_norm", + "_fused_sdp_choice", + "_fused_sgd_", + "_fw_primal_copy", + "_grid_sampler_2d_cpu_fallback", + "_grouped_mm", + "_has_compatible_shallow_copy_type", + "_histogramdd_bin_edges", + "_histogramdd_from_bin_cts", + "_histogramdd_from_bin_tensors", + "_index_put_impl_", + "_indices_copy", + "_int_mm", + "_is_all_true", + "_is_any_true", + "_is_functional_tensor", + "_is_functional_tensor_base", + "_is_zerotensor", + "_lazy_clone", + "_linalg_check_errors", + "_linalg_det", + "_linalg_eigh", + "_linalg_slogdet", + "_linalg_solve_ex", + "_linalg_svd", + "_log_softmax", + "_log_softmax_backward_data", + "_logcumsumexp", + "_lstm_mps", + "_lu_with_info", + "_make_dep_token", + "_make_dual", + "_make_dual_copy", + "_make_per_channel_quantized_tensor", + "_make_per_tensor_quantized_tensor", + "_masked_scale", + "_masked_softmax", + "_mixed_dtypes_linear", + "_mkldnn_reshape", + "_mkldnn_transpose", + "_mkldnn_transpose_", + "_mps_convolution", + "_mps_convolution_transpose", + "_native_batch_norm_legit", + "_native_batch_norm_legit_no_training", + "_native_multi_head_attention", + "_neg_view", + "_neg_view_copy", + "_nested_compute_contiguous_strides_offsets", + "_nested_from_padded", + "_nested_from_padded_and_nested_example", + "_nested_from_padded_tensor", + "_nested_get_jagged_dummy", + "_nested_get_lengths", + "_nested_get_max_seqlen", + "_nested_get_min_seqlen", + "_nested_get_offsets", + "_nested_get_ragged_idx", + "_nested_get_values", + "_nested_get_values_copy", + "_nested_tensor_from_mask", + "_nested_tensor_from_mask_left_aligned", + "_nested_tensor_from_tensor_list", + "_nested_tensor_softmax_with_shape", + "_nested_view_from_buffer", + "_nested_view_from_buffer_copy", + "_nested_view_from_jagged", + "_nested_view_from_jagged_copy", + "_nnpack_available", + "_nnpack_spatial_convolution", + "_pack_padded_sequence", + "_pad_packed_sequence", + "_philox_key_fold_in", + "_philox_key_split", + "_philox_normal_", + "_philox_uniform_", + "_pin_memory", + "_prelu_kernel", + "_print", + "_propagate_xla_data", + "_remove_batch_dim", + "_reshape_alias_copy", + "_reshape_from_tensor", + "_resize_output_", + "_rowwise_prune", + "_safe_softmax", + "_sample_dirichlet", + "_saturate_weight_to_fp16", + "_scaled_dot_product_attention_math", + "_scaled_dot_product_attention_math_for_mps", + "_scaled_dot_product_cudnn_attention", + "_scaled_dot_product_efficient_attention", + "_scaled_dot_product_flash_attention", + "_scaled_dot_product_flash_attention_for_cpu", + "_scaled_grouped_mm", + "_scaled_grouped_mm_v2", + "_scaled_mm", + "_scaled_mm_v2", + "_shape_as_tensor", + "_sobol_engine_draw", + "_sobol_engine_ff_", + "_sobol_engine_initialize_state_", + "_sobol_engine_scramble_", + "_softmax", + "_softmax_backward_data", + "_sparse_broadcast_to", + "_sparse_broadcast_to_copy", + "_sparse_csr_prod", + "_sparse_csr_sum", + "_sparse_log_softmax_backward_data", + "_sparse_semi_structured_addmm", + "_sparse_semi_structured_apply", + "_sparse_semi_structured_apply_dense", + "_sparse_semi_structured_linear", + "_sparse_semi_structured_mm", + "_sparse_semi_structured_tile", + "_sparse_softmax_backward_data", + "_sparse_sparse_matmul", + "_sparse_sum", + "_stack", + "_standard_gamma", + "_standard_gamma_grad", + "_sync", + "_test_autograd_multiple_dispatch", + "_test_autograd_multiple_dispatch_view", + "_test_autograd_multiple_dispatch_view_copy", + "_test_check_tensor", + "_test_functorch_fallback", + "_test_parallel_materialize", + "_test_serialization_subcmul", + "_to_cpu", + "_to_functional_tensor", + "_to_sparse_semi_structured", + "_transform_bias_rescale_qkv", + "_transformer_encoder_layer_fwd", + "_trilinear", + "_triton_multi_head_attention", + "_triton_scaled_dot_attention", + "_unique", + "_unique2", + "_unpack_dual", + "_unsafe_index", + "_unsafe_index_put", + "_unsafe_masked_index", + "_unsafe_masked_index_put_accumulate", + "_use_cudnn_ctc_loss", + "_use_cudnn_rnn_flatten_weight", + "_use_miopen_ctc_loss", + "_validate_compressed_sparse_indices", + "_validate_sparse_bsc_tensor_args", + "_validate_sparse_bsr_tensor_args", + "_validate_sparse_compressed_tensor_args", + "_validate_sparse_coo_tensor_args", + "_validate_sparse_csc_tensor_args", + "_validate_sparse_csr_tensor_args", + "_values_copy", + "_weight_int4pack_mm", + "_weight_int4pack_mm_for_cpu", + "_weight_int4pack_mm_with_scales_and_zeros", + "_weight_int8pack_mm", + "_weight_norm", + "_weight_norm_interface", + "_wrapped_linear_prepack", + "_wrapped_quantized_linear_prepacked", + "abs", + "abs_", + "absolute", + "acos", + "acos_", + "acosh", + "acosh_", + "adaptive_avg_pool1d", + "adaptive_max_pool1d", + "add", + "addbmm", + "addcdiv", + "addcmul", + "addmm", + "addmv", + "addmv_", + "addr", + "adjoint", + "affine_grid_generator", + "alias_copy", + "all", + "allclose", + "alpha_dropout", + "alpha_dropout_", + "amax", + "amin", + "aminmax", + "angle", + "any", + "arange", + "arccos", + "arccos_", + "arccosh", + "arccosh_", + "arcsin", + "arcsin_", + "arcsinh", + "arcsinh_", + "arctan", + "arctan2", + "arctan_", + "arctanh", + "arctanh_", + "argmax", + "argmin", + "argsort", + "argwhere", + "as_strided", + "as_strided_", + "as_strided_copy", + "as_strided_scatter", + "as_tensor", + "asarray", + "asin", + "asin_", + "asinh", + "asinh_", + "atan", + "atan2", + "atan_", + "atanh", + "atanh_", + "avg_pool1d", + "baddbmm", + "bartlett_window", + "batch_norm", + "batch_norm_backward_elemt", + "batch_norm_backward_reduce", + "batch_norm_elemt", + "batch_norm_gather_stats", + "batch_norm_gather_stats_with_counts", + "batch_norm_stats", + "batch_norm_update_stats", + "bernoulli", + "bilinear", + "binary_cross_entropy_with_logits", + "bincount", + "binomial", + "bitwise_and", + "bitwise_left_shift", + "bitwise_not", + "bitwise_or", + "bitwise_right_shift", + "bitwise_xor", + "blackman_window", + "bmm", + "broadcast_to", + "bucketize", + "can_cast", + "cat", + "ccol_indices_copy", + "ceil", + "ceil_", + "celu", + "celu_", + "channel_shuffle", + "cholesky", + "cholesky_inverse", + "cholesky_solve", + "choose_qparams_optimized", + "chunk", + "clamp", + "clamp_", + "clamp_max", + "clamp_max_", + "clamp_min", + "clamp_min_", + "clip", + "clip_", + "clone", + "col_indices_copy", + "column_stack", + "combinations", + "complex", + "concat", + "concatenate", + "conj", + "conj_physical", + "conj_physical_", + "constant_pad_nd", + "conv1d", + "conv2d", + "conv3d", + "conv_tbc", + "conv_transpose1d", + "conv_transpose2d", + "conv_transpose3d", + "convolution", + "copysign", + "corrcoef", + "cos", + "cos_", + "cosh", + "cosh_", + "cosine_embedding_loss", + "cosine_similarity", + "count_nonzero", + "cov", + "cross", + "crow_indices_copy", + "ctc_loss", + "cudnn_affine_grid_generator", + "cudnn_batch_norm", + "cudnn_convolution", + "cudnn_convolution_add_relu", + "cudnn_convolution_relu", + "cudnn_convolution_transpose", + "cudnn_grid_sampler", + "cudnn_is_acceptable", + "cummax", + "cummin", + "cumprod", + "cumsum", + "cumulative_trapezoid", + "deg2rad", + "deg2rad_", + "dequantize", + "det", + "detach", + "detach_", + "detach_copy", + "diag", + "diag_embed", + "diagflat", + "diagonal", + "diagonal_copy", + "diagonal_scatter", + "diff", + "digamma", + "dist", + "div", + "divide", + "dot", + "dropout", + "dropout_", + "dsmm", + "dsplit", + "dstack", + "embedding", + "embedding_bag", + "embedding_renorm_", + "empty", + "empty_like", + "empty_permuted", + "empty_quantized", + "empty_strided", + "eq", + "equal", + "erf", + "erf_", + "erfc", + "erfc_", + "erfinv", + "exp", + "exp2", + "exp2_", + "exp_", + "expand_copy", + "expm1", + "expm1_", + "eye", + "fake_quantize_per_channel_affine", + "fake_quantize_per_tensor_affine", + "fbgemm_linear_fp16_weight", + "fbgemm_linear_fp16_weight_fp32_activation", + "fbgemm_linear_int8_weight", + "fbgemm_linear_int8_weight_fp32_activation", + "fbgemm_linear_quantize_weight", + "fbgemm_pack_gemm_matrix_fp16", + "fbgemm_pack_quantized_matrix", + "feature_alpha_dropout", + "feature_alpha_dropout_", + "feature_dropout", + "feature_dropout_", + "fill", + "fill_", + "fix", + "fix_", + "flatten", + "flip", + "fliplr", + "flipud", + "float_power", + "floor", + "floor_", + "floor_divide", + "fmax", + "fmin", + "fmod", + "frac", + "frac_", + "frexp", + "frobenius_norm", + "from_file", + "from_numpy", + "frombuffer", + "full", + "full_like", + "fused_moving_avg_obs_fake_quant", + "gather", + "gcd", + "gcd_", + "ge", + "geqrf", + "ger", + "get_default_dtype", + "get_num_interop_threads", + "get_num_threads", + "gradient", + "greater", + "greater_equal", + "grid_sampler", + "grid_sampler_2d", + "grid_sampler_3d", + "group_norm", + "gru", + "gru_cell", + "gt", + "hamming_window", + "hann_window", + "hardshrink", + "hash_tensor", + "heaviside", + "hinge_embedding_loss", + "histc", + "histogram", + "histogramdd", + "hsmm", + "hsplit", + "hspmm", + "hstack", + "hypot", + "i0", + "i0_", + "igamma", + "igammac", + "imag", + "index_add", + "index_copy", + "index_fill", + "index_put", + "index_put_", + "index_reduce", + "index_select", + "indices_copy", + "init_num_threads", + "inner", + "instance_norm", + "int_repr", + "inverse", + "is_complex", + "is_conj", + "is_distributed", + "is_floating_point", + "is_grad_enabled", + "is_inference", + "is_inference_mode_enabled", + "is_neg", + "is_nonzero", + "is_same_size", + "is_signed", + "is_vulkan_available", + "isclose", + "isfinite", + "isin", + "isinf", + "isnan", + "isneginf", + "isposinf", + "isreal", + "istft", + "kaiser_window", + "kl_div", + "kron", + "kthvalue", + "layer_norm", + "lcm", + "lcm_", + "ldexp", + "ldexp_", + "le", + "lerp", + "less", + "less_equal", + "lgamma", + "linspace", + "log", + "log10", + "log10_", + "log1p", + "log1p_", + "log2", + "log2_", + "log_", + "log_softmax", + "logaddexp", + "logaddexp2", + "logcumsumexp", + "logdet", + "logical_and", + "logical_not", + "logical_or", + "logical_xor", + "logit", + "logit_", + "logspace", + "logsumexp", + "lstm", + "lstm_cell", + "lt", + "lu_solve", + "lu_unpack", + "margin_ranking_loss", + "masked_fill", + "masked_scatter", + "masked_select", + "matmul", + "matrix_exp", + "matrix_power", + "max", + "max_pool1d", + "max_pool1d_with_indices", + "max_pool2d", + "max_pool3d", + "maximum", + "mean", + "median", + "min", + "minimum", + "miopen_batch_norm", + "miopen_convolution", + "miopen_convolution_add_relu", + "miopen_convolution_relu", + "miopen_convolution_transpose", + "miopen_ctc_loss", + "miopen_depthwise_convolution", + "miopen_rnn", + "mkldnn_adaptive_avg_pool2d", + "mkldnn_convolution", + "mkldnn_linear_backward_weights", + "mkldnn_max_pool2d", + "mkldnn_max_pool3d", + "mkldnn_rnn_layer", + "mm", + "mode", + "moveaxis", + "movedim", + "msort", + "mul", + "multinomial", + "multiply", + "mv", + "mvlgamma", + "nan_to_num", + "nan_to_num_", + "nanmean", + "nanmedian", + "nanquantile", + "nansum", + "narrow", + "narrow_copy", + "native_batch_norm", + "native_channel_shuffle", + "native_dropout", + "native_group_norm", + "native_layer_norm", + "native_norm", + "ne", + "neg", + "neg_", + "negative", + "negative_", + "nextafter", + "nonzero", + "nonzero_static", + "norm_except_dim", + "normal", + "not_equal", + "nuclear_norm", + "numel", + "ones", + "ones_like", + "orgqr", + "ormqr", + "outer", + "pairwise_distance", + "pdist", + "permute", + "permute_copy", + "pinverse", + "pixel_shuffle", + "pixel_unshuffle", + "poisson", + "poisson_nll_loss", + "polar", + "polygamma", + "positive", + "pow", + "prelu", + "prod", + "promote_types", + "put", + "q_per_channel_axis", + "q_per_channel_scales", + "q_per_channel_zero_points", + "q_scale", + "q_zero_point", + "qr", + "quantile", + "quantize_per_channel", + "quantize_per_tensor", + "quantize_per_tensor_dynamic", + "quantized_batch_norm", + "quantized_gru_cell", + "quantized_lstm_cell", + "quantized_max_pool1d", + "quantized_max_pool2d", + "quantized_max_pool3d", + "quantized_rnn_relu_cell", + "quantized_rnn_tanh_cell", + "rad2deg", + "rad2deg_", + "rand", + "rand_like", + "randint", + "randint_like", + "randn", + "randn_like", + "randperm", + "range", + "ravel", + "real", + "reciprocal", + "reciprocal_", + "relu", + "relu_", + "remainder", + "renorm", + "repeat_interleave", + "reshape", + "resize_as_", + "resize_as_sparse_", + "resolve_conj", + "resolve_neg", + "result_type", + "rms_norm", + "rnn_relu", + "rnn_relu_cell", + "rnn_tanh", + "rnn_tanh_cell", + "roll", + "rot90", + "round", + "round_", + "row_indices_copy", + "row_stack", + "rrelu", + "rrelu_", + "rsqrt", + "rsqrt_", + "rsub", + "saddmm", + "scalar_tensor", + "scatter", + "scatter_add", + "scatter_reduce", + "searchsorted", + "segment_reduce", + "select", + "select_copy", + "select_scatter", + "selu", + "selu_", + "set_flush_denormal", + "set_num_interop_threads", + "set_num_threads", + "sgn", + "sigmoid", + "sigmoid_", + "sign", + "signbit", + "sin", + "sin_", + "sinc", + "sinc_", + "sinh", + "sinh_", + "slice_copy", + "slice_inverse", + "slice_scatter", + "slogdet", + "smm", + "softmax", + "sort", + "sparse_bsc_tensor", + "sparse_bsr_tensor", + "sparse_compressed_tensor", + "sparse_coo_tensor", + "sparse_csc_tensor", + "sparse_csr_tensor", + "split_copy", + "split_with_sizes", + "split_with_sizes_copy", + "spmm", + "sqrt", + "sqrt_", + "square", + "square_", + "squeeze", + "squeeze_copy", + "sspaddmm", + "stack", + "std", + "std_mean", + "sub", + "subtract", + "sum", + "svd", + "swapaxes", + "swapdims", + "sym_constrain_range", + "sym_constrain_range_for_size", + "t", + "t_copy", + "take", + "take_along_dim", + "tan", + "tan_", + "tanh", + "tanh_", + "tensor", + "tensor_split", + "threshold", + "threshold_", + "tile", + "topk", + "trace", + "transpose", + "transpose_copy", + "trapezoid", + "trapz", + "triangular_solve", + "tril", + "tril_indices", + "triplet_margin_loss", + "triu", + "triu_indices", + "true_divide", + "trunc", + "trunc_", + "unbind", + "unbind_copy", + "unflatten", + "unfold_copy", + "unique_dim", + "unsafe_chunk", + "unsafe_split", + "unsafe_split_with_sizes", + "unsqueeze", + "unsqueeze_copy", + "values_copy", + "vander", + "var", + "var_mean", + "vdot", + "view_as_complex", + "view_as_complex_copy", + "view_as_real", + "view_as_real_copy", + "view_copy", + "vsplit", + "vstack", + "where", + "xlogy", + "xlogy_", + "zero_", + "zeros", + "zeros_like", +] + +@overload +def __and__(input: Tensor, other: Tensor) -> Tensor: ... +@overload +def __and__(input: Tensor, other: Number | _complex) -> Tensor: ... +@overload +def __lshift__(input: Tensor, other: Tensor) -> Tensor: ... +@overload +def __lshift__(input: Tensor, other: Number | _complex) -> Tensor: ... +@overload +def __or__(input: Tensor, other: Tensor) -> Tensor: ... +@overload +def __or__(input: Tensor, other: Number | _complex) -> Tensor: ... +@overload +def __rshift__(input: Tensor, other: Tensor) -> Tensor: ... +@overload +def __rshift__(input: Tensor, other: Number | _complex) -> Tensor: ... +@overload +def __xor__(input: Tensor, other: Tensor) -> Tensor: ... +@overload +def __xor__(input: Tensor, other: Number | _complex) -> Tensor: ... +def _adaptive_avg_pool2d( + input: Tensor, + output_size: _int | SymInt | Sequence[_int | SymInt], +) -> Tensor: ... +def _adaptive_avg_pool3d( + input: Tensor, + output_size: _int | SymInt | Sequence[_int | SymInt], +) -> Tensor: ... +def _add_batch_dim(input: Tensor, batch_dim: _int, level: _int) -> Tensor: ... +@overload +def _add_relu( + input: Tensor, + other: Tensor, + *, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: ... +@overload +def _add_relu( + input: Tensor, + other: Number | _complex, + alpha: Number | _complex = 1, +) -> Tensor: ... +@overload +def _add_relu_( + input: Tensor, + other: Tensor, + *, + alpha: Number | _complex = 1, +) -> Tensor: ... +@overload +def _add_relu_( + input: Tensor, + other: Number | _complex, + alpha: Number | _complex = 1, +) -> Tensor: ... +def _addmm_activation( + input: Tensor, + mat1: Tensor, + mat2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + use_gelu: _bool = False, + out: Tensor | None = None, +) -> Tensor: ... +@overload +def _aminmax(input: Tensor) -> tuple[Tensor, Tensor]: ... +@overload +def _aminmax( + input: Tensor, + dim: _int, + keepdim: _bool = False, +) -> tuple[Tensor, Tensor]: ... +def _amp_foreach_non_finite_check_and_unscale_( + self: tuple[Tensor, ...] | list[Tensor], + found_inf: Tensor, + inv_scale: Tensor, +) -> None: ... +def _amp_update_scale_( + input: Tensor, + growth_tracker: Tensor, + found_inf: Tensor, + scale_growth_factor: _float, + scale_backoff_factor: _float, + growth_interval: _int, +) -> Tensor: ... +@overload +def _assert_async(input: Tensor) -> None: + r""" + _assert_async(tensor) -> void + + Asynchronously assert that the contents of tensor are nonzero. For CPU tensors, + this is equivalent to ``assert tensor`` or ``assert tensor.is_nonzero()``; for + CUDA tensors, we DO NOT synchronize and you may only find out the assertion + failed at a later CUDA kernel launch. Asynchronous assertion can be helpful for + testing invariants in CUDA tensors without giving up performance. This function + is NOT intended to be used for regular error checking, as it will trash your CUDA + context if the assert fails (forcing you to restart your PyTorch process.) + + Args: + tensor (Tensor): a one element tensor to test to see if it is nonzero. Zero + elements (including False for boolean tensors) cause an assertion failure + to be raised. + """ + +@overload +def _assert_async(input: Tensor, assert_msg: str) -> None: + r""" + _assert_async(tensor) -> void + + Asynchronously assert that the contents of tensor are nonzero. For CPU tensors, + this is equivalent to ``assert tensor`` or ``assert tensor.is_nonzero()``; for + CUDA tensors, we DO NOT synchronize and you may only find out the assertion + failed at a later CUDA kernel launch. Asynchronous assertion can be helpful for + testing invariants in CUDA tensors without giving up performance. This function + is NOT intended to be used for regular error checking, as it will trash your CUDA + context if the assert fails (forcing you to restart your PyTorch process.) + + Args: + tensor (Tensor): a one element tensor to test to see if it is nonzero. Zero + elements (including False for boolean tensors) cause an assertion failure + to be raised. + """ + +def _assert_scalar(self: Number | _complex, assert_msg: str) -> None: ... +def _assert_tensor_metadata( + a: Tensor, + size: Sequence[_int | SymInt] | None = None, + stride: Sequence[_int | SymInt] | None = None, + dtype: _dtype | None = None, + *, + device: DeviceLikeType | None = None, + layout: _layout | None = None, +) -> None: ... +def _batch_norm_impl_index( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor | None, + running_var: Tensor | None, + training: _bool, + momentum: _float, + eps: _float, + cudnn_enabled: _bool, +) -> tuple[Tensor, Tensor, Tensor, Tensor, _int]: ... +def _cast_Byte(input: Tensor, non_blocking: _bool = False) -> Tensor: ... +def _cast_Char(input: Tensor, non_blocking: _bool = False) -> Tensor: ... +def _cast_Double(input: Tensor, non_blocking: _bool = False) -> Tensor: ... +def _cast_Float(input: Tensor, non_blocking: _bool = False) -> Tensor: ... +def _cast_Half(input: Tensor, non_blocking: _bool = False) -> Tensor: ... +def _cast_Int(input: Tensor, non_blocking: _bool = False) -> Tensor: ... +def _cast_Long(input: Tensor, non_blocking: _bool = False) -> Tensor: ... +def _cast_Short(input: Tensor, non_blocking: _bool = False) -> Tensor: ... +def _choose_qparams_per_tensor( + input: Tensor, + reduce_range: _bool = False, +) -> tuple[_float, _int]: ... +def _chunk_cat( + tensors: tuple[Tensor, ...] | list[Tensor], + dim: _int, + num_chunks: _int, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _coalesce(input: Tensor) -> Tensor: ... +def _compute_linear_combination( + input: Tensor, + coefficients: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _conj(input: Tensor) -> Tensor: ... +def _conj_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: ... +def _conj_physical(input: Tensor) -> Tensor: ... +def _convert_indices_from_coo_to_csr( + input: Tensor, + size: _int, + *, + out_int32: _bool = False, + out: Tensor | None = None, +) -> Tensor: ... +def _convert_indices_from_csr_to_coo( + crow_indices: Tensor, + col_indices: Tensor, + *, + out_int32: _bool = False, + transpose: _bool = False, + out: Tensor | None = None, +) -> Tensor: ... +def _convert_weight_to_int4pack(input: Tensor, innerKTiles: _int) -> Tensor: ... +def _convert_weight_to_int4pack_for_cpu( + input: Tensor, + innerKTiles: _int, +) -> Tensor: ... +@overload +def _convolution( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + stride: Sequence[_int | SymInt], + padding: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + transposed: _bool, + output_padding: _size, + groups: _int | SymInt, + benchmark: _bool, + deterministic: _bool, + cudnn_enabled: _bool, +) -> Tensor: ... +@overload +def _convolution( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + stride: Sequence[_int | SymInt], + padding: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + transposed: _bool, + output_padding: Sequence[_int | SymInt], + groups: _int | SymInt, + benchmark: _bool, + deterministic: _bool, + cudnn_enabled: _bool, + allow_tf32: _bool, +) -> Tensor: ... +def _convolution_mode( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + stride: Sequence[_int | SymInt], + padding: str, + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, +) -> Tensor: ... +def _copy_from( + input: Tensor, + dst: Tensor, + non_blocking: _bool = False, +) -> Tensor: ... +def _copy_from_and_resize(input: Tensor, dst: Tensor) -> Tensor: ... +def _cslt_compress(input: Tensor) -> Tensor: ... +def _cslt_sparse_mm( + compressed_A: Tensor, + dense_B: Tensor, + bias: Tensor | None = None, + alpha: Tensor | None = None, + out_dtype: _dtype | None = None, + transpose_result: _bool = False, + alg_id: _int = 0, + split_k: _int = 1, + split_k_mode: _int = -1, +) -> Tensor: ... +def _cslt_sparse_mm_search( + compressed_A: Tensor, + dense_B: Tensor, + bias: Tensor | None = None, + alpha: Tensor | None = None, + out_dtype: _dtype | None = None, + transpose_result: _bool = False, +) -> _int: ... +@overload +def _ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: _size, + target_lengths: _size, + blank: _int = 0, + zero_infinity: _bool = False, +) -> tuple[Tensor, Tensor]: ... +@overload +def _ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: Tensor, + target_lengths: Tensor, + blank: _int = 0, + zero_infinity: _bool = False, +) -> tuple[Tensor, Tensor]: ... +@overload +def _cudnn_ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: _size, + target_lengths: _size, + blank: _int, + deterministic: _bool, + zero_infinity: _bool, +) -> tuple[Tensor, Tensor]: ... +@overload +def _cudnn_ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: Tensor, + target_lengths: Tensor, + blank: _int, + deterministic: _bool, + zero_infinity: _bool, +) -> tuple[Tensor, Tensor]: ... +def _cudnn_init_dropout_state( + dropout: _float, + train: _bool, + dropout_seed: _int, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: ... +def _cudnn_rnn( + input: Tensor, + weight: tuple[Tensor, ...] | list[Tensor], + weight_stride0: _int, + weight_buf: Tensor | None, + hx: Tensor, + cx: Tensor | None, + mode: _int, + hidden_size: _int | SymInt, + proj_size: _int | SymInt, + num_layers: _int, + batch_first: _bool, + dropout: _float, + train: _bool, + bidirectional: _bool, + batch_sizes: Sequence[_int | SymInt], + dropout_state: Tensor | None, +) -> tuple[Tensor, Tensor, Tensor, Tensor, Tensor]: ... +def _cudnn_rnn_flatten_weight( + weight_arr: tuple[Tensor, ...] | list[Tensor], + weight_stride0: _int, + input_size: _int | SymInt, + mode: _int, + hidden_size: _int | SymInt, + proj_size: _int | SymInt, + num_layers: _int, + batch_first: _bool, + bidirectional: _bool, +) -> Tensor: ... +def _cufft_clear_plan_cache(device_index: _int) -> None: ... +def _cufft_get_plan_cache_max_size(device_index: _int) -> _int: ... +def _cufft_get_plan_cache_size(device_index: _int) -> _int: ... +def _cufft_set_plan_cache_max_size( + device_index: _int, + max_size: _int, +) -> None: ... +def _cummax_helper( + input: Tensor, + values: Tensor, + indices: Tensor, + dim: _int, +) -> None: ... +def _cummin_helper( + input: Tensor, + values: Tensor, + indices: Tensor, + dim: _int, +) -> None: ... +def _debug_has_internal_overlap(input: Tensor) -> _int: ... +def _dim_arange(like: Tensor, dim: _int) -> Tensor: ... +def _dirichlet_grad(x: Tensor, alpha: Tensor, total: Tensor) -> Tensor: ... +def _disable_functionalization(): ... +def _dyn_quant_matmul_4bit( + inp: Tensor, + packed_weights: Tensor, + block_size: _int, + in_features: _int, + out_features: _int, +) -> Tensor: ... +def _dyn_quant_pack_4bit_weight( + weights: Tensor, + scales_zeros: Tensor, + bias: Tensor | None, + block_size: _int, + in_features: _int, + out_features: _int, +) -> Tensor: ... +@overload +def _efficientzerotensor( + size: Sequence[_int | SymInt], + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: ... +@overload +def _efficientzerotensor( + *size: _int | SymInt, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: ... +def _embedding_bag( + weight: Tensor, + indices: Tensor, + offsets: Tensor, + scale_grad_by_freq: _bool = False, + mode: _int = 0, + sparse: _bool = False, + per_sample_weights: Tensor | None = None, + include_last_offset: _bool = False, + padding_idx: _int = -1, +) -> tuple[Tensor, Tensor, Tensor, Tensor]: ... +def _embedding_bag_forward_only( + weight: Tensor, + indices: Tensor, + offsets: Tensor, + scale_grad_by_freq: _bool = False, + mode: _int = 0, + sparse: _bool = False, + per_sample_weights: Tensor | None = None, + include_last_offset: _bool = False, + padding_idx: _int = -1, +) -> tuple[Tensor, Tensor, Tensor, Tensor]: ... +@overload +def _empty_affine_quantized( + size: Sequence[_int | SymInt], + *, + scale: _float = 1, + zero_point: _int = 0, + memory_format: memory_format | None = contiguous_format, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: ... +@overload +def _empty_affine_quantized( + *size: _int | SymInt, + scale: _float = 1, + zero_point: _int = 0, + memory_format: memory_format | None = contiguous_format, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: ... +@overload +def _empty_per_channel_affine_quantized( + size: Sequence[_int | SymInt], + *, + scales: Tensor, + zero_points: Tensor, + axis: _int, + memory_format: memory_format | None = contiguous_format, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: ... +@overload +def _empty_per_channel_affine_quantized( + *size: _int | SymInt, + scales: Tensor, + zero_points: Tensor, + axis: _int, + memory_format: memory_format | None = contiguous_format, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: ... +def _enable_functionalization(*, reapply_views: _bool = False) -> None: ... +def _euclidean_dist(x1: Tensor, x2: Tensor) -> Tensor: ... +def _fake_quantize_learnable_per_channel_affine( + input: Tensor, + scale: Tensor, + zero_point: Tensor, + axis: _int, + quant_min: _int, + quant_max: _int, + grad_factor: _float = 1.0, +) -> Tensor: ... +def _fake_quantize_learnable_per_tensor_affine( + input: Tensor, + scale: Tensor, + zero_point: Tensor, + quant_min: _int, + quant_max: _int, + grad_factor: _float = 1.0, +) -> Tensor: ... +def _fake_quantize_per_tensor_affine_cachemask_tensor_qparams( + input: Tensor, + scale: Tensor, + zero_point: Tensor, + fake_quant_enabled: Tensor, + quant_min: _int, + quant_max: _int, +) -> torch.return_types._fake_quantize_per_tensor_affine_cachemask_tensor_qparams: # fmt: skip + ... +def _fft_c2c( + input: Tensor, + dim: Sequence[_int | SymInt], + normalization: _int, + forward: _bool, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _fft_c2r( + input: Tensor, + dim: _size, + normalization: _int, + last_dim_size: _int | SymInt, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _fft_r2c( + input: Tensor, + dim: _size, + normalization: _int, + onesided: _bool, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _fill_mem_eff_dropout_mask_( + input: Tensor, + dropout_p: _float, + seed: _int, + offset: _int, +) -> Tensor: ... +def _flash_attention_forward_no_dropout_inplace( + out: Tensor | None, + query: Tensor, + key: Tensor, + value: Tensor, + cum_seq_q: Tensor | None, + cum_seq_k: Tensor | None, + max_q: _int | SymInt, + max_k: _int | SymInt, + dropout_p: _float, + is_causal: _bool, + return_debug_mask: _bool, + *, + scale: _float | None = None, + window_size_left: _int | SymInt | None = None, + window_size_right: _int | SymInt | None = None, + seqused_k: Tensor | None = None, + alibi_slopes: Tensor | None = None, + block_table: Tensor | None = None, + num_splits: _int | None = None, +) -> Tensor: ... +def _foobar( + input: Tensor, + arg1: _bool = True, + arg2: _bool = True, + *, + arg3: _bool = True, +) -> Tensor: ... +def _foreach_abs( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_abs(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.abs` to each Tensor of the input list. + """ + +def _foreach_abs_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_abs_(self: List[Tensor]) -> None + + Apply :func:`torch.abs` to each Tensor of the input list. + """ + +def _foreach_acos( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_acos(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.acos` to each Tensor of the input list. + """ + +def _foreach_acos_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_acos_(self: List[Tensor]) -> None + + Apply :func:`torch.acos` to each Tensor of the input list. + """ + +@overload +def _foreach_add( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_add( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], + *, + alpha: Number | _complex = 1, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_add( + self: tuple[Tensor, ...] | list[Tensor], + other: Tensor, + *, + alpha: Number | _complex = 1, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_add( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_add_( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_add_( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], + *, + alpha: Number | _complex = 1, +) -> None: ... +@overload +def _foreach_add_( + self: tuple[Tensor, ...] | list[Tensor], + other: Tensor, + *, + alpha: Number | _complex = 1, +) -> None: ... +@overload +def _foreach_add_( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> None: ... +@overload +def _foreach_addcdiv( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_addcdiv( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + scalars: Tensor, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_addcdiv( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + value: Number | _complex = 1, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_addcdiv_( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_addcdiv_( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + scalars: Tensor, +) -> None: ... +@overload +def _foreach_addcdiv_( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + value: Number | _complex = 1, +) -> None: ... +@overload +def _foreach_addcmul( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_addcmul( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + scalars: Tensor, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_addcmul( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + value: Number | _complex = 1, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_addcmul_( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_addcmul_( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + scalars: Tensor, +) -> None: ... +@overload +def _foreach_addcmul_( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + value: Number | _complex = 1, +) -> None: ... +def _foreach_asin( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_asin(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.asin` to each Tensor of the input list. + """ + +def _foreach_asin_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_asin_(self: List[Tensor]) -> None + + Apply :func:`torch.asin` to each Tensor of the input list. + """ + +def _foreach_atan( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_atan(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.atan` to each Tensor of the input list. + """ + +def _foreach_atan_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_atan_(self: List[Tensor]) -> None + + Apply :func:`torch.atan` to each Tensor of the input list. + """ + +def _foreach_ceil( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_ceil(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.ceil` to each Tensor of the input list. + """ + +def _foreach_ceil_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_ceil_(self: List[Tensor]) -> None + + Apply :func:`torch.ceil` to each Tensor of the input list. + """ + +@overload +def _foreach_clamp_max( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_clamp_max( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_clamp_max( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_clamp_max_( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_clamp_max_( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> None: ... +@overload +def _foreach_clamp_max_( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> None: ... +@overload +def _foreach_clamp_min( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_clamp_min( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_clamp_min( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_clamp_min_( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_clamp_min_( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> None: ... +@overload +def _foreach_clamp_min_( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> None: ... +def _foreach_clone( + self: tuple[Tensor, ...] | list[Tensor], + *, + memory_format: memory_format | None = None, +) -> tuple[Tensor, ...]: ... +def _foreach_copy_( + self: tuple[Tensor, ...] | list[Tensor], + src: tuple[Tensor, ...] | list[Tensor], + non_blocking: _bool = False, +) -> None: ... +def _foreach_cos( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_cos(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.cos` to each Tensor of the input list. + """ + +def _foreach_cos_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_cos_(self: List[Tensor]) -> None + + Apply :func:`torch.cos` to each Tensor of the input list. + """ + +def _foreach_cosh( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_cosh(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.cosh` to each Tensor of the input list. + """ + +def _foreach_cosh_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_cosh_(self: List[Tensor]) -> None + + Apply :func:`torch.cosh` to each Tensor of the input list. + """ + +@overload +def _foreach_div( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_div( + self: tuple[Tensor, ...] | list[Tensor], + other: Tensor, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_div( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_div( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_div_( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_div_( + self: tuple[Tensor, ...] | list[Tensor], + other: Tensor, +) -> None: ... +@overload +def _foreach_div_( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> None: ... +@overload +def _foreach_div_( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> None: ... +def _foreach_erf( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_erf(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.erf` to each Tensor of the input list. + """ + +def _foreach_erf_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_erf_(self: List[Tensor]) -> None + + Apply :func:`torch.erf` to each Tensor of the input list. + """ + +def _foreach_erfc( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_erfc(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.erfc` to each Tensor of the input list. + """ + +def _foreach_erfc_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_erfc_(self: List[Tensor]) -> None + + Apply :func:`torch.erfc` to each Tensor of the input list. + """ + +def _foreach_exp( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_exp(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.exp` to each Tensor of the input list. + """ + +def _foreach_exp_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_exp_(self: List[Tensor]) -> None + + Apply :func:`torch.exp` to each Tensor of the input list. + """ + +def _foreach_expm1( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_expm1(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.expm1` to each Tensor of the input list. + """ + +def _foreach_expm1_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_expm1_(self: List[Tensor]) -> None + + Apply :func:`torch.expm1` to each Tensor of the input list. + """ + +def _foreach_floor( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_floor(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.floor` to each Tensor of the input list. + """ + +def _foreach_floor_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_floor_(self: List[Tensor]) -> None + + Apply :func:`torch.floor` to each Tensor of the input list. + """ + +def _foreach_frac( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_frac(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.frac` to each Tensor of the input list. + """ + +def _foreach_frac_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_frac_(self: List[Tensor]) -> None + + Apply :func:`torch.frac` to each Tensor of the input list. + """ + +@overload +def _foreach_lerp( + self: tuple[Tensor, ...] | list[Tensor], + tensors1: tuple[Tensor, ...] | list[Tensor], + weight: Number | _complex, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_lerp( + self: tuple[Tensor, ...] | list[Tensor], + tensors1: tuple[Tensor, ...] | list[Tensor], + weight: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_lerp( + self: tuple[Tensor, ...] | list[Tensor], + tensors1: tuple[Tensor, ...] | list[Tensor], + weights: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_lerp_( + self: tuple[Tensor, ...] | list[Tensor], + tensors1: tuple[Tensor, ...] | list[Tensor], + weight: Number | _complex, +) -> None: ... +@overload +def _foreach_lerp_( + self: tuple[Tensor, ...] | list[Tensor], + tensors1: tuple[Tensor, ...] | list[Tensor], + weight: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_lerp_( + self: tuple[Tensor, ...] | list[Tensor], + tensors1: tuple[Tensor, ...] | list[Tensor], + weights: tuple[Tensor, ...] | list[Tensor], +) -> None: ... +def _foreach_lgamma( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_lgamma(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.lgamma` to each Tensor of the input list. + """ + +def _foreach_lgamma_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_lgamma_(self: List[Tensor]) -> None + + Apply :func:`torch.lgamma` to each Tensor of the input list. + """ + +def _foreach_log( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_log(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.log` to each Tensor of the input list. + """ + +def _foreach_log10( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_log10(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.log10` to each Tensor of the input list. + """ + +def _foreach_log10_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_log10_(self: List[Tensor]) -> None + + Apply :func:`torch.log10` to each Tensor of the input list. + """ + +def _foreach_log1p( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_log1p(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.log1p` to each Tensor of the input list. + """ + +def _foreach_log1p_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_log1p_(self: List[Tensor]) -> None + + Apply :func:`torch.log1p` to each Tensor of the input list. + """ + +def _foreach_log2( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_log2(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.log2` to each Tensor of the input list. + """ + +def _foreach_log2_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_log2_(self: List[Tensor]) -> None + + Apply :func:`torch.log2` to each Tensor of the input list. + """ + +def _foreach_log_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_log_(self: List[Tensor]) -> None + + Apply :func:`torch.log` to each Tensor of the input list. + """ + +def _foreach_max( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_maximum( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_maximum( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_maximum( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_maximum_( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_maximum_( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> None: ... +@overload +def _foreach_maximum_( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> None: ... +@overload +def _foreach_minimum( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_minimum( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_minimum( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_minimum_( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_minimum_( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> None: ... +@overload +def _foreach_minimum_( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> None: ... +@overload +def _foreach_mul( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_mul( + self: tuple[Tensor, ...] | list[Tensor], + other: Tensor, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_mul( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_mul( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_mul_( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_mul_( + self: tuple[Tensor, ...] | list[Tensor], + other: Tensor, +) -> None: ... +@overload +def _foreach_mul_( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> None: ... +@overload +def _foreach_mul_( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> None: ... +def _foreach_neg( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_neg(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.neg` to each Tensor of the input list. + """ + +def _foreach_neg_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_neg_(self: List[Tensor]) -> None + + Apply :func:`torch.neg` to each Tensor of the input list. + """ + +def _foreach_norm( + self: tuple[Tensor, ...] | list[Tensor], + ord: Number | _complex = 2, + dtype: _dtype | None = None, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_pow( + self: tuple[Tensor, ...] | list[Tensor], + exponent: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_pow( + self: tuple[Tensor, ...] | list[Tensor], + exponent: Number | _complex, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_pow( + self: tuple[Tensor, ...] | list[Tensor], + exponent: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_pow( + self: Number | _complex, + exponent: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_pow_( + self: tuple[Tensor, ...] | list[Tensor], + exponent: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_pow_( + self: tuple[Tensor, ...] | list[Tensor], + exponent: Number | _complex, +) -> None: ... +@overload +def _foreach_pow_( + self: tuple[Tensor, ...] | list[Tensor], + exponent: tuple[Tensor, ...] | list[Tensor], +) -> None: ... +def _foreach_powsum( + self: tuple[Tensor, ...] | list[Tensor], + ord: Number | _complex = 2, + dtype: _dtype | None = None, +) -> tuple[Tensor, ...]: ... +def _foreach_reciprocal( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_reciprocal(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.reciprocal` to each Tensor of the input list. + """ + +def _foreach_reciprocal_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_reciprocal_(self: List[Tensor]) -> None + + Apply :func:`torch.reciprocal` to each Tensor of the input list. + """ + +def _foreach_round( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_round(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.round` to each Tensor of the input list. + """ + +def _foreach_round_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_round_(self: List[Tensor]) -> None + + Apply :func:`torch.round` to each Tensor of the input list. + """ + +def _foreach_rsqrt( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +def _foreach_rsqrt_(self: tuple[Tensor, ...] | list[Tensor]) -> None: ... +def _foreach_sigmoid( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_sigmoid(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.sigmoid` to each Tensor of the input list. + """ + +def _foreach_sigmoid_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_sigmoid_(self: List[Tensor]) -> None + + Apply :func:`torch.sigmoid` to each Tensor of the input list. + """ + +def _foreach_sign( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +def _foreach_sign_(self: tuple[Tensor, ...] | list[Tensor]) -> None: ... +def _foreach_sin( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_sin(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.sin` to each Tensor of the input list. + """ + +def _foreach_sin_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_sin_(self: List[Tensor]) -> None + + Apply :func:`torch.sin` to each Tensor of the input list. + """ + +def _foreach_sinh( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_sinh(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.sinh` to each Tensor of the input list. + """ + +def _foreach_sinh_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_sinh_(self: List[Tensor]) -> None + + Apply :func:`torch.sinh` to each Tensor of the input list. + """ + +def _foreach_sqrt( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_sqrt(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.sqrt` to each Tensor of the input list. + """ + +def _foreach_sqrt_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_sqrt_(self: List[Tensor]) -> None + + Apply :func:`torch.sqrt` to each Tensor of the input list. + """ + +@overload +def _foreach_sub( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_sub( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], + *, + alpha: Number | _complex = 1, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_sub( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_sub_( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_sub_( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], + *, + alpha: Number | _complex = 1, +) -> None: ... +@overload +def _foreach_sub_( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> None: ... +def _foreach_tan( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_tan(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.tan` to each Tensor of the input list. + """ + +def _foreach_tan_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_tan_(self: List[Tensor]) -> None + + Apply :func:`torch.tan` to each Tensor of the input list. + """ + +def _foreach_tanh( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_tanh(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.tanh` to each Tensor of the input list. + """ + +def _foreach_tanh_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_tanh_(self: List[Tensor]) -> None + + Apply :func:`torch.tanh` to each Tensor of the input list. + """ + +def _foreach_trunc( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_trunc(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.trunc` to each Tensor of the input list. + """ + +def _foreach_trunc_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_trunc_(self: List[Tensor]) -> None + + Apply :func:`torch.trunc` to each Tensor of the input list. + """ + +def _foreach_zero_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_zero_(self: List[Tensor]) -> None + + Apply :func:`torch.zero` to each Tensor of the input list. + """ + +def _from_functional_tensor(t: Tensor) -> Tensor: ... +def _functional_assert_async( + input: Tensor, + assert_msg: str, + dep_token: Tensor, +) -> Tensor: ... +def _functional_assert_scalar( + self: Number | _complex, + assert_msg: str, + dep_token: Tensor, +) -> Tensor: ... +def _functional_sym_constrain_range( + size: Number | _complex, + min: _int | None, + max: _int | None, + dep_token: Tensor, +) -> Tensor: ... +def _functional_sym_constrain_range_for_size( + size: Number | _complex, + min: _int | None, + max: _int | None, + dep_token: Tensor, +) -> Tensor: ... +def _functionalize_apply_view_metas(tensor: Tensor, base: Tensor) -> Tensor: ... +def _functionalize_are_all_mutations_hidden_from_autograd( + t: Tensor, +) -> _bool: ... +def _functionalize_are_all_mutations_under_no_grad_or_inference_mode( + t: Tensor, +) -> _bool: ... +def _functionalize_commit_update(t: Tensor) -> None: ... +def _functionalize_has_metadata_mutation(tensor: Tensor) -> _bool: ... +def _functionalize_inductor_storage_resized_counter(t: Tensor) -> _int: ... +def _functionalize_is_symbolic(tensor: Tensor) -> _bool: ... +def _functionalize_mark_mutation_hidden_from_autograd(t: Tensor) -> None: ... +def _functionalize_mark_storage_changed(tensor: Tensor) -> _bool: ... +def _functionalize_mutation_counter(t: Tensor) -> _int: ... +def _functionalize_replace(self_: Tensor, other: Tensor) -> None: ... +def _functionalize_storage_changed_counter(t: Tensor) -> _int: ... +def _functionalize_sync(t: Tensor) -> None: ... +def _functionalize_unsafe_set(dst: Tensor, src: Tensor) -> None: ... +def _functionalize_was_inductor_storage_resized(t: Tensor) -> _bool: ... +def _functionalize_was_storage_changed(tensor: Tensor) -> _bool: ... +@overload +def _fused_adagrad_( + self: tuple[Tensor, ...] | list[Tensor], + grads: tuple[Tensor, ...] | list[Tensor], + state_sums: tuple[Tensor, ...] | list[Tensor], + state_steps: tuple[Tensor, ...] | list[Tensor], + *, + lr: Tensor, + lr_decay: _float, + weight_decay: _float, + eps: _float, + maximize: _bool, + grad_scale: Tensor | None = None, + found_inf: Tensor | None = None, +) -> None: ... +@overload +def _fused_adagrad_( + self: tuple[Tensor, ...] | list[Tensor], + grads: tuple[Tensor, ...] | list[Tensor], + state_sums: tuple[Tensor, ...] | list[Tensor], + state_steps: tuple[Tensor, ...] | list[Tensor], + *, + lr: _float, + lr_decay: _float, + weight_decay: _float, + eps: _float, + maximize: _bool, + grad_scale: Tensor | None = None, + found_inf: Tensor | None = None, +) -> None: ... +@overload +def _fused_adam_( + self: tuple[Tensor, ...] | list[Tensor], + grads: tuple[Tensor, ...] | list[Tensor], + exp_avgs: tuple[Tensor, ...] | list[Tensor], + exp_avg_sqs: tuple[Tensor, ...] | list[Tensor], + max_exp_avg_sqs: tuple[Tensor, ...] | list[Tensor], + state_steps: tuple[Tensor, ...] | list[Tensor], + *, + lr: Tensor, + beta1: _float, + beta2: _float, + weight_decay: _float, + eps: _float, + amsgrad: _bool, + maximize: _bool, + grad_scale: Tensor | None = None, + found_inf: Tensor | None = None, +) -> None: ... +@overload +def _fused_adam_( + self: tuple[Tensor, ...] | list[Tensor], + grads: tuple[Tensor, ...] | list[Tensor], + exp_avgs: tuple[Tensor, ...] | list[Tensor], + exp_avg_sqs: tuple[Tensor, ...] | list[Tensor], + max_exp_avg_sqs: tuple[Tensor, ...] | list[Tensor], + state_steps: tuple[Tensor, ...] | list[Tensor], + *, + lr: _float, + beta1: _float, + beta2: _float, + weight_decay: _float, + eps: _float, + amsgrad: _bool, + maximize: _bool, + grad_scale: Tensor | None = None, + found_inf: Tensor | None = None, +) -> None: ... +@overload +def _fused_adamw_( + self: tuple[Tensor, ...] | list[Tensor], + grads: tuple[Tensor, ...] | list[Tensor], + exp_avgs: tuple[Tensor, ...] | list[Tensor], + exp_avg_sqs: tuple[Tensor, ...] | list[Tensor], + max_exp_avg_sqs: tuple[Tensor, ...] | list[Tensor], + state_steps: tuple[Tensor, ...] | list[Tensor], + *, + lr: Tensor, + beta1: _float, + beta2: _float, + weight_decay: _float, + eps: _float, + amsgrad: _bool, + maximize: _bool, + grad_scale: Tensor | None = None, + found_inf: Tensor | None = None, +) -> None: ... +@overload +def _fused_adamw_( + self: tuple[Tensor, ...] | list[Tensor], + grads: tuple[Tensor, ...] | list[Tensor], + exp_avgs: tuple[Tensor, ...] | list[Tensor], + exp_avg_sqs: tuple[Tensor, ...] | list[Tensor], + max_exp_avg_sqs: tuple[Tensor, ...] | list[Tensor], + state_steps: tuple[Tensor, ...] | list[Tensor], + *, + lr: _float, + beta1: _float, + beta2: _float, + weight_decay: _float, + eps: _float, + amsgrad: _bool, + maximize: _bool, + grad_scale: Tensor | None = None, + found_inf: Tensor | None = None, +) -> None: ... +def _fused_dropout( + input: Tensor, + p: _float, + generator: Generator | None = None, +) -> tuple[Tensor, Tensor]: ... +def _fused_moving_avg_obs_fq_helper( + input: Tensor, + observer_on: Tensor, + fake_quant_on: Tensor, + running_min: Tensor, + running_max: Tensor, + scale: Tensor, + zero_point: Tensor, + averaging_const: _float, + quant_min: _int, + quant_max: _int, + ch_axis: _int, + per_row_fake_quant: _bool = False, + symmetric_quant: _bool = False, +) -> torch.return_types._fused_moving_avg_obs_fq_helper: ... +def _fused_rms_norm( + input: Tensor, + normalized_shape: _size, + weight: Tensor | None, + eps: _float | None, +) -> tuple[Tensor, Tensor]: ... +def _fused_sdp_choice( + query: Tensor, + key: Tensor, + value: Tensor, + attn_mask: Tensor | None = None, + dropout_p: _float = 0.0, + is_causal: _bool = False, + *, + scale: _float | None = None, + enable_gqa: _bool = False, +) -> _int: ... +@overload +def _fused_sgd_( + self: tuple[Tensor, ...] | list[Tensor], + grads: tuple[Tensor, ...] | list[Tensor], + momentum_buffer_list: tuple[Tensor, ...] | list[Tensor], + *, + weight_decay: _float, + momentum: _float, + lr: Tensor, + dampening: _float, + nesterov: _bool, + maximize: _bool, + is_first_step: _bool, + grad_scale: Tensor | None = None, + found_inf: Tensor | None = None, +) -> None: ... +@overload +def _fused_sgd_( + self: tuple[Tensor, ...] | list[Tensor], + grads: tuple[Tensor, ...] | list[Tensor], + momentum_buffer_list: tuple[Tensor, ...] | list[Tensor], + *, + weight_decay: _float, + momentum: _float, + lr: _float, + dampening: _float, + nesterov: _bool, + maximize: _bool, + is_first_step: _bool, + grad_scale: Tensor | None = None, + found_inf: Tensor | None = None, +) -> None: ... +def _fw_primal_copy( + input: Tensor, + level: _int, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _grid_sampler_2d_cpu_fallback( + input: Tensor, + grid: Tensor, + interpolation_mode: _int, + padding_mode: _int, + align_corners: _bool, +) -> Tensor: ... +def _grouped_mm( + input: Tensor, + mat2: Tensor, + offs: Tensor | None = None, + bias: Tensor | None = None, + out_dtype: _dtype | None = None, +) -> Tensor: ... +def _has_compatible_shallow_copy_type( + input: Tensor, + from_: Tensor, +) -> _bool: ... +def _histogramdd_bin_edges( + input: Tensor, + bins: _size, + *, + range: Sequence[_float] | None = None, + weight: Tensor | None = None, + density: _bool = False, +) -> tuple[Tensor, ...]: ... +def _histogramdd_from_bin_cts( + input: Tensor, + bins: _size, + *, + range: Sequence[_float] | None = None, + weight: Tensor | None = None, + density: _bool = False, +) -> Tensor: ... +def _histogramdd_from_bin_tensors( + input: Tensor, + bins: tuple[Tensor, ...] | list[Tensor], + *, + weight: Tensor | None = None, + density: _bool = False, +) -> Tensor: ... +def _index_put_impl_( + input: Tensor, + indices: tuple[Tensor | None, ...] | list[Tensor | None] | None, + values: Tensor, + accumulate: _bool = False, + unsafe: _bool = False, +) -> Tensor: ... +def _indices_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: ... +def _int_mm( + input: Tensor, + mat2: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _is_all_true(input: Tensor) -> Tensor: ... +def _is_any_true(input: Tensor) -> Tensor: ... +def _is_functional_tensor(t: Tensor) -> _bool: ... +def _is_functional_tensor_base(t: Tensor) -> _bool: ... +def _is_zerotensor(input: Tensor) -> _bool: ... +def _lazy_clone(input: Tensor) -> Tensor: ... +def _linalg_check_errors( + info: Tensor, + api_name: str, + *, + is_matrix: _bool, +) -> None: ... +def _linalg_det( + A: Tensor, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types._linalg_det: ... +def _linalg_eigh( + A: Tensor, + UPLO: str = "L", + compute_v: _bool = True, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types._linalg_eigh: ... +def _linalg_slogdet( + A: Tensor, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types._linalg_slogdet: ... +def _linalg_solve_ex( + A: Tensor, + B: Tensor, + *, + left: _bool = True, + check_errors: _bool = False, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types._linalg_solve_ex: ... +def _linalg_svd( + A: Tensor, + full_matrices: _bool = False, + compute_uv: _bool = True, + *, + driver: str | None = None, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types._linalg_svd: ... +def _log_softmax( + input: Tensor, + dim: _int, + half_to_float: _bool, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _log_softmax_backward_data( + grad_output: Tensor, + output: Tensor, + dim: _int, + input_dtype: _dtype, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _logcumsumexp( + input: Tensor, + dim: _int, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _lstm_mps( + input: Tensor, + hx: tuple[Tensor, ...] | list[Tensor], + params: tuple[Tensor, ...] | list[Tensor], + has_biases: _bool, + num_layers: _int, + dropout: _float, + train: _bool, + bidirectional: _bool, + batch_first: _bool, +) -> tuple[Tensor, Tensor, Tensor, Tensor, Tensor, Tensor]: ... +def _lu_with_info( + input: Tensor, + pivot: _bool = True, + check_errors: _bool = True, +) -> torch.return_types._lu_with_info: ... +def _make_dep_token( + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: ... +def _make_dual(primal: Tensor, tangent: Tensor, level: _int) -> Tensor: ... +def _make_dual_copy( + primal: Tensor, + tangent: Tensor, + level: _int, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _make_per_channel_quantized_tensor( + input: Tensor, + scale: Tensor, + zero_point: Tensor, + axis: _int, +) -> Tensor: ... +def _make_per_tensor_quantized_tensor( + input: Tensor, + scale: _float, + zero_point: _int, +) -> Tensor: ... +def _masked_scale(input: Tensor, mask: Tensor, scale: _float) -> Tensor: ... +def _masked_softmax( + input: Tensor, + mask: Tensor, + dim: _int | None = None, + mask_type: _int | None = None, +) -> Tensor: ... +def _mixed_dtypes_linear( + input: Tensor, + weight: Tensor, + scale: Tensor, + *, + bias: Tensor | None = None, + activation: str | None = None, +) -> Tensor: ... +def _mkldnn_reshape(input: Tensor, shape: _size) -> Tensor: ... +def _mkldnn_transpose(input: Tensor, dim0: _int, dim1: _int) -> Tensor: ... +def _mkldnn_transpose_(input: Tensor, dim0: _int, dim1: _int) -> Tensor: ... +def _mps_convolution( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + padding: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, +) -> Tensor: ... +def _mps_convolution_transpose( + input: Tensor, + weight: Tensor, + padding: Sequence[_int | SymInt], + output_padding: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, +) -> Tensor: ... +@overload +def _native_batch_norm_legit( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor, + running_var: Tensor, + training: _bool, + momentum: _float, + eps: _float, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> tuple[Tensor, Tensor, Tensor]: ... +@overload +def _native_batch_norm_legit( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + training: _bool, + momentum: _float, + eps: _float, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> tuple[Tensor, Tensor, Tensor]: ... +def _native_batch_norm_legit_no_training( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor, + running_var: Tensor, + momentum: _float, + eps: _float, +) -> tuple[Tensor, Tensor, Tensor]: ... +def _native_multi_head_attention( + query: Tensor, + key: Tensor, + value: Tensor, + embed_dim: _int, + num_head: _int, + qkv_weight: Tensor, + qkv_bias: Tensor, + proj_weight: Tensor, + proj_bias: Tensor, + mask: Tensor | None = None, + need_weights: _bool = True, + average_attn_weights: _bool = True, + mask_type: _int | None = None, +) -> tuple[Tensor, Tensor]: ... +def _neg_view(input: Tensor) -> Tensor: ... +def _neg_view_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: ... +def _nested_compute_contiguous_strides_offsets( + nested_size: Tensor, +) -> tuple[Tensor, Tensor]: ... +def _nested_from_padded( + padded: Tensor, + cpu_nested_shape_example: Tensor, + fuse_transform_0213: _bool = False, +) -> Tensor: ... +def _nested_from_padded_and_nested_example( + padded: Tensor, + nt_example: Tensor, +) -> Tensor: ... +def _nested_from_padded_tensor( + padded: Tensor, + offsets: Tensor, + dummy: Tensor, + ragged_idx: _int = 1, + min_seqlen: Tensor | None = None, + max_seqlen: Tensor | None = None, + sum_S: _int | SymInt | None = None, +) -> Tensor: ... +def _nested_get_jagged_dummy(any: Tensor) -> Tensor: ... +def _nested_get_lengths(input: Tensor) -> Tensor: ... +def _nested_get_max_seqlen(input: Tensor) -> Tensor: ... +def _nested_get_min_seqlen(input: Tensor) -> Tensor: ... +def _nested_get_offsets(input: Tensor) -> Tensor: ... +def _nested_get_ragged_idx(input: Tensor) -> _int: ... +def _nested_get_values(input: Tensor) -> Tensor: ... +def _nested_get_values_copy( + input: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _nested_tensor_from_mask( + t: Tensor, + mask: Tensor, + mask_check: _bool = True, +) -> Tensor: ... +def _nested_tensor_from_mask_left_aligned(t: Tensor, mask: Tensor) -> _bool: ... +def _nested_tensor_from_tensor_list( + list: tuple[Tensor, ...] | list[Tensor], + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = None, +) -> Tensor: ... +def _nested_tensor_softmax_with_shape( + input: Tensor, + query: Tensor, +) -> Tensor: ... +def _nested_view_from_buffer( + input: Tensor, + nested_size: Tensor, + nested_strides: Tensor, + offsets: Tensor, +) -> Tensor: ... +def _nested_view_from_buffer_copy( + input: Tensor, + nested_size: Tensor, + nested_strides: Tensor, + offsets: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _nested_view_from_jagged( + input: Tensor, + offsets: Tensor, + dummy: Tensor, + lengths: Tensor | None = None, + ragged_idx: _int = 1, + min_seqlen: Tensor | None = None, + max_seqlen: Tensor | None = None, +) -> Tensor: ... +def _nested_view_from_jagged_copy( + input: Tensor, + offsets: Tensor, + dummy: Tensor, + lengths: Tensor | None = None, + ragged_idx: _int = 1, + min_seqlen: Tensor | None = None, + max_seqlen: Tensor | None = None, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _nnpack_available() -> _bool: ... +def _nnpack_spatial_convolution( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + padding: _int | SymInt | Sequence[_int | SymInt], + stride: _int | SymInt | Sequence[_int | SymInt] = 1, +) -> Tensor: ... +def _pack_padded_sequence( + input: Tensor, + lengths: Tensor, + batch_first: _bool, +) -> tuple[Tensor, Tensor]: ... +def _pad_packed_sequence( + data: Tensor, + batch_sizes: Tensor, + batch_first: _bool, + padding_value: Number | _complex, + total_length: _int, +) -> tuple[Tensor, Tensor]: ... +def _philox_key_fold_in(key: Tensor, data: _int) -> Tensor: ... +def _philox_key_split(key: Tensor, num_splits: _int) -> Tensor: ... +def _philox_normal_( + input: Tensor, + key: Tensor, + mean: _float = 0, + std: _float = 1, +) -> Tensor: ... +def _philox_uniform_( + input: Tensor, + key: Tensor, + low: _float = 0, + high: _float = 1, +) -> Tensor: ... +def _pin_memory( + input: Tensor, + device: DeviceLikeType | None = None, +) -> Tensor: ... +def _prelu_kernel(input: Tensor, weight: Tensor) -> Tensor: ... +def _print(s: str) -> None: ... +def _propagate_xla_data(input: Tensor, output: Tensor) -> None: ... +def _remove_batch_dim( + input: Tensor, + level: _int, + batch_size: _int | SymInt, + out_dim: _int, +) -> Tensor: ... +def _reshape_alias_copy( + input: Tensor, + size: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + *, + out: Tensor | None = None, +) -> Tensor: ... +def _reshape_from_tensor(input: Tensor, shape: Tensor) -> Tensor: ... +def _resize_output_( + input: Tensor, + size: Sequence[_int | SymInt], + device: DeviceLikeType | None, +) -> Tensor: ... +def _rowwise_prune( + weight: Tensor, + mask: Tensor, + compressed_indices_dtype: _dtype, +) -> tuple[Tensor, Tensor]: ... +def _safe_softmax( + input: Tensor, + dim: _int, + dtype: _dtype | None = None, +) -> Tensor: ... +def _sample_dirichlet( + input: Tensor, + generator: Generator | None = None, +) -> Tensor: ... +def _saturate_weight_to_fp16(weight: Tensor) -> Tensor: ... +def _scaled_dot_product_attention_math( + query: Tensor, + key: Tensor, + value: Tensor, + attn_mask: Tensor | None = None, + dropout_p: _float = 0.0, + is_causal: _bool = False, + dropout_mask: Tensor | None = None, + *, + scale: _float | None = None, + enable_gqa: _bool = False, +) -> tuple[Tensor, Tensor]: ... +def _scaled_dot_product_attention_math_for_mps( + query: Tensor, + key: Tensor, + value: Tensor, + attn_mask: Tensor | None = None, + dropout_p: _float = 0.0, + is_causal: _bool = False, + dropout_mask: Tensor | None = None, + *, + scale: _float | None = None, + enable_gqa: _bool = False, +) -> tuple[Tensor, Tensor]: ... +def _scaled_dot_product_cudnn_attention( + query: Tensor, + key: Tensor, + value: Tensor, + attn_bias: Tensor | None, + compute_log_sumexp: _bool, + dropout_p: _float = 0.0, + is_causal: _bool = False, + return_debug_mask: _bool = False, + *, + scale: _float | None = None, +) -> torch.return_types._scaled_dot_product_cudnn_attention: ... +def _scaled_dot_product_efficient_attention( + query: Tensor, + key: Tensor, + value: Tensor, + attn_bias: Tensor | None, + compute_log_sumexp: _bool, + dropout_p: _float = 0.0, + is_causal: _bool = False, + *, + scale: _float | None = None, +) -> torch.return_types._scaled_dot_product_efficient_attention: ... +@overload +def _scaled_dot_product_flash_attention( + query: Tensor, + key: Tensor, + value: Tensor, + q_descale: Tensor | None, + k_descale: Tensor | None, + v_descale: Tensor | None, + dropout_p: _float = 0.0, + is_causal: _bool = False, + return_debug_mask: _bool = False, + *, + scale: _float | None = None, +) -> torch.return_types._scaled_dot_product_flash_attention: ... +@overload +def _scaled_dot_product_flash_attention( + query: Tensor, + key: Tensor, + value: Tensor, + dropout_p: _float = 0.0, + is_causal: _bool = False, + return_debug_mask: _bool = False, + *, + scale: _float | None = None, +) -> torch.return_types._scaled_dot_product_flash_attention: ... +def _scaled_dot_product_flash_attention_for_cpu( + query: Tensor, + key: Tensor, + value: Tensor, + dropout_p: _float = 0.0, + is_causal: _bool = False, + *, + attn_mask: Tensor | None = None, + scale: _float | None = None, +) -> torch.return_types._scaled_dot_product_flash_attention_for_cpu: ... +def _scaled_grouped_mm( + input: Tensor, + mat2: Tensor, + scale_a: Tensor, + scale_b: Tensor, + offs: Tensor | None = None, + bias: Tensor | None = None, + scale_result: Tensor | None = None, + out_dtype: _dtype | None = None, + use_fast_accum: _bool = False, +) -> Tensor: ... +def _scaled_grouped_mm_v2( + input: Tensor, + mat2: Tensor, + scale_a: tuple[Tensor, ...] | list[Tensor], + recipe_a: _size, + swizzle_a: _size, + scale_b: tuple[Tensor, ...] | list[Tensor], + recipe_b: _size, + swizzle_b: _size, + offs: Tensor | None = None, + bias: Tensor | None = None, + out_dtype: _dtype | None = None, + contraction_dim: _size = (), + use_fast_accum: _bool = False, +) -> Tensor: ... +def _scaled_mm( + input: Tensor, + mat2: Tensor, + scale_a: Tensor, + scale_b: Tensor, + bias: Tensor | None = None, + scale_result: Tensor | None = None, + out_dtype: _dtype | None = None, + use_fast_accum: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _scaled_mm_v2( + input: Tensor, + mat2: Tensor, + scale_a: tuple[Tensor, ...] | list[Tensor], + recipe_a: _size, + swizzle_a: _size, + scale_b: tuple[Tensor, ...] | list[Tensor], + recipe_b: _size, + swizzle_b: _size, + bias: Tensor | None, + out_dtype: _dtype | None, + contraction_dim: _size = (), + use_fast_accum: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _shape_as_tensor(input: Tensor) -> Tensor: ... +def _sobol_engine_draw( + quasi: Tensor, + n: _int, + sobolstate: Tensor, + dimension: _int, + num_generated: _int, + dtype: _dtype | None, +) -> tuple[Tensor, Tensor]: ... +def _sobol_engine_ff_( + input: Tensor, + n: _int, + sobolstate: Tensor, + dimension: _int, + num_generated: _int, +) -> Tensor: ... +def _sobol_engine_initialize_state_( + input: Tensor, + dimension: _int, +) -> Tensor: ... +def _sobol_engine_scramble_( + input: Tensor, + ltm: Tensor, + dimension: _int, +) -> Tensor: ... +def _softmax( + input: Tensor, + dim: _int, + half_to_float: _bool, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _softmax_backward_data( + grad_output: Tensor, + output: Tensor, + dim: _int, + input_dtype: _dtype, + *, + grad_input: Tensor | None = None, +) -> Tensor: ... +def _sparse_broadcast_to(input: Tensor, size: _size) -> Tensor: ... +def _sparse_broadcast_to_copy( + input: Tensor, + size: _size, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _sparse_csr_prod( + input: Tensor, + dim: _int | _size, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, +) -> Tensor: ... +def _sparse_csr_sum( + input: Tensor, + dim: _int | _size, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, +) -> Tensor: ... +def _sparse_log_softmax_backward_data( + grad_output: Tensor, + output: Tensor, + dim: _int, + input: Tensor, +) -> Tensor: ... +def _sparse_semi_structured_addmm( + input: Tensor, + mat1: Tensor, + mat1_meta: Tensor, + mat2: Tensor, + *, + alpha: Number | _complex = 1, + beta: Number | _complex = 1, + out_dtype: _dtype | None = None, +) -> Tensor: ... +def _sparse_semi_structured_apply( + input: Tensor, + thread_masks: Tensor, +) -> tuple[Tensor, Tensor]: ... +def _sparse_semi_structured_apply_dense( + input: Tensor, + thread_masks: Tensor, +) -> Tensor: ... +def _sparse_semi_structured_linear( + input: Tensor, + weight: Tensor, + meta: Tensor, + *, + bias: Tensor | None = None, + activation: str | None = None, + out_dtype: _dtype | None = None, +) -> Tensor: ... +def _sparse_semi_structured_mm( + mat1: Tensor, + mat1_meta: Tensor, + mat2: Tensor, + *, + out_dtype: _dtype | None = None, +) -> Tensor: ... +def _sparse_semi_structured_tile( + input: Tensor, + algorithm: str = "", + use_cutlass: _bool = True, +) -> tuple[Tensor, Tensor, Tensor, Tensor, Tensor]: ... +def _sparse_softmax_backward_data( + grad_output: Tensor, + output: Tensor, + dim: _int, + input: Tensor, +) -> Tensor: ... +def _sparse_sparse_matmul(input: Tensor, other: Tensor) -> Tensor: ... +@overload +def _sparse_sum(input: Tensor) -> Tensor: ... +@overload +def _sparse_sum(input: Tensor, *, dtype: _dtype) -> Tensor: ... +@overload +def _sparse_sum(input: Tensor, dim: _int | _size) -> Tensor: ... +@overload +def _sparse_sum( + input: Tensor, + dim: _int | _size, + *, + dtype: _dtype, +) -> Tensor: ... +def _stack( + tensors: tuple[Tensor, ...] | list[Tensor], + dim: _int = 0, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _standard_gamma( + input: Tensor, + generator: Generator | None = None, +) -> Tensor: ... +def _standard_gamma_grad(input: Tensor, output: Tensor) -> Tensor: ... +def _sync(t: Tensor) -> None: ... +@overload +def _test_autograd_multiple_dispatch(input: Tensor) -> Tensor: ... +@overload +def _test_autograd_multiple_dispatch(input: Tensor, b: _bool) -> Tensor: ... +def _test_autograd_multiple_dispatch_view(input: Tensor) -> Tensor: ... +def _test_autograd_multiple_dispatch_view_copy( + input: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _test_check_tensor(input: Tensor) -> Tensor: ... +def _test_functorch_fallback(input: Tensor, other: Tensor) -> Tensor: ... +def _test_parallel_materialize( + input: Tensor, + num_parallel: _int, + skip_first: _bool = False, +) -> Tensor: ... +def _test_serialization_subcmul( + input: Tensor, + other: Tensor, + alpha: Number | _complex = 1, +) -> Tensor: ... +def _to_cpu( + tensors: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +def _to_functional_tensor(t: Tensor) -> Tensor: ... +def _to_sparse_semi_structured(dense: Tensor) -> tuple[Tensor, Tensor]: ... +def _transform_bias_rescale_qkv( + qkv: Tensor, + qkv_bias: Tensor, + num_heads: _int, +) -> tuple[Tensor, Tensor, Tensor]: ... +def _transformer_encoder_layer_fwd( + src: Tensor, + embed_dim: _int, + num_heads: _int, + qkv_weight: Tensor, + qkv_bias: Tensor, + proj_weight: Tensor, + proj_bias: Tensor, + use_gelu: _bool, + norm_first: _bool, + eps: _float, + norm_weight_1: Tensor, + norm_bias_1: Tensor, + norm_weight_2: Tensor, + norm_bias_2: Tensor, + ffn_weight_1: Tensor, + ffn_bias_1: Tensor, + ffn_weight_2: Tensor, + ffn_bias_2: Tensor, + mask: Tensor | None = None, + mask_type: _int | None = None, +) -> Tensor: ... +def _trilinear( + i1: Tensor, + i2: Tensor, + i3: Tensor, + expand1: _size, + expand2: _size, + expand3: _size, + sumdim: _size, + unroll_dim: _int = 1, +) -> Tensor: ... +def _triton_multi_head_attention( + query: Tensor, + key: Tensor, + value: Tensor, + embed_dim: _int, + num_head: _int, + qkv_weight: Tensor, + qkv_bias: Tensor, + proj_weight: Tensor, + proj_bias: Tensor, + mask: Tensor | None = None, +) -> Tensor: ... +def _triton_scaled_dot_attention( + q: Tensor, + k: Tensor, + v: Tensor, + dropout_p: _float = 0.0, +) -> Tensor: ... +def _unique( + input: Tensor, + sorted: _bool = True, + return_inverse: _bool = False, +) -> tuple[Tensor, Tensor]: ... +def _unique2( + input: Tensor, + sorted: _bool = True, + return_inverse: _bool = False, + return_counts: _bool = False, +) -> tuple[Tensor, Tensor, Tensor]: ... +def _unpack_dual( + dual: Tensor, + level: _int, +) -> torch.return_types._unpack_dual: ... +def _unsafe_index( + input: Tensor, + indices: tuple[Tensor | None, ...] | list[Tensor | None] | None, +) -> Tensor: ... +def _unsafe_index_put( + input: Tensor, + indices: tuple[Tensor | None, ...] | list[Tensor | None] | None, + values: Tensor, + accumulate: _bool = False, +) -> Tensor: ... +def _unsafe_masked_index( + input: Tensor, + mask: Tensor, + indices: tuple[Tensor | None, ...] | list[Tensor | None] | None, + fill: Number | _complex, +) -> Tensor: ... +def _unsafe_masked_index_put_accumulate( + input: Tensor, + mask: Tensor, + indices: tuple[Tensor | None, ...] | list[Tensor | None] | None, + values: Tensor, +) -> Tensor: ... +@overload +def _use_cudnn_ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: Tensor, + target_lengths: Tensor, + blank: _int, +) -> _bool: ... +@overload +def _use_cudnn_ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: _size, + target_lengths: _size, + blank: _int, +) -> _bool: ... +def _use_cudnn_rnn_flatten_weight() -> _bool: ... +@overload +def _use_miopen_ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: Tensor, + target_lengths: Tensor, + blank: _int, +) -> _bool: ... +@overload +def _use_miopen_ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: _size, + target_lengths: _size, + blank: _int, +) -> _bool: ... +def _validate_compressed_sparse_indices( + is_crow: _bool, + compressed_idx: Tensor, + plain_idx: Tensor, + cdim: _int, + dim: _int, + nnz: _int, +) -> None: ... +def _validate_sparse_bsc_tensor_args( + ccol_indices: Tensor, + row_indices: Tensor, + values: Tensor, + size: _size, + check_pinning: _bool | None = None, +) -> None: ... +def _validate_sparse_bsr_tensor_args( + crow_indices: Tensor, + col_indices: Tensor, + values: Tensor, + size: _size, + check_pinning: _bool | None = None, +) -> None: ... +def _validate_sparse_compressed_tensor_args( + compressed_indices: Tensor, + plain_indices: Tensor, + values: Tensor, + size: _size, + layout: _layout, + check_pinning: _bool | None = None, +) -> None: ... +def _validate_sparse_coo_tensor_args( + indices: Tensor, + values: Tensor, + size: _size, + is_coalesced: _bool | None = None, + check_pinning: _bool | None = None, +) -> None: ... +def _validate_sparse_csc_tensor_args( + ccol_indices: Tensor, + row_indices: Tensor, + values: Tensor, + size: _size, + check_pinning: _bool | None = None, +) -> None: ... +def _validate_sparse_csr_tensor_args( + crow_indices: Tensor, + col_indices: Tensor, + values: Tensor, + size: _size, + check_pinning: _bool | None = None, +) -> None: ... +def _values_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: ... +def _weight_int4pack_mm( + input: Tensor, + mat2: Tensor, + qGroupSize: _int, + qScaleAndZeros: Tensor, +) -> Tensor: ... +def _weight_int4pack_mm_for_cpu( + input: Tensor, + mat2: Tensor, + qGroupSize: _int, + qScaleAndZeros: Tensor, +) -> Tensor: ... +def _weight_int4pack_mm_with_scales_and_zeros( + input: Tensor, + mat2: Tensor, + qGroupSize: _int, + qScale: Tensor, + qZeros: Tensor, +) -> Tensor: ... +def _weight_int8pack_mm( + input: Tensor, + mat2: Tensor, + scales: Tensor, +) -> Tensor: ... +def _weight_norm(v: Tensor, g: Tensor, dim: _int = 0) -> Tensor: ... +def _weight_norm_interface( + v: Tensor, + g: Tensor, + dim: _int = 0, +) -> tuple[Tensor, Tensor]: ... +def _wrapped_linear_prepack( + weight: Tensor, + weight_scale: Tensor, + weight_zero_point: Tensor, + bias: Tensor, +) -> Tensor: ... +def _wrapped_quantized_linear_prepacked( + input: Tensor, + input_scale: Tensor, + input_zero_point: Tensor, + packed_weight: Tensor, + output_scale: Tensor, + output_zero_point: Tensor, + out_channel: _int, +) -> Tensor: ... +def abs(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + abs(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Computes the absolute value of each element in :attr:`input`. + + .. math:: + \text{out}_{i} = |\text{input}_{i}| + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.abs(torch.tensor([-1, -2, 3])) + tensor([ 1, 2, 3]) + """ + +def abs_(input: Tensor) -> Tensor: ... +def absolute(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + absolute(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Alias for :func:`torch.abs` + """ + +def acos(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + acos(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Returns a new tensor with the arccosine (in radians) of each element in :attr:`input`. + + .. math:: + \text{out}_{i} = \cos^{-1}(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.3348, -0.5889, 0.2005, -0.1584]) + >>> torch.acos(a) + tensor([ 1.2294, 2.2004, 1.3690, 1.7298]) + """ + +def acos_(input: Tensor) -> Tensor: ... +def acosh(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + acosh(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Returns a new tensor with the inverse hyperbolic cosine of the elements of :attr:`input`. + + .. math:: + \text{out}_{i} = \cosh^{-1}(\text{input}_{i}) + + Note: + The domain of the inverse hyperbolic cosine is `[1, inf)` and values outside this range + will be mapped to ``NaN``, except for `+ INF` for which the output is mapped to `+ INF`. + + Args: + input (Tensor): the input tensor. + + Keyword arguments: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4).uniform_(1, 2) + >>> a + tensor([ 1.3192, 1.9915, 1.9674, 1.7151 ]) + >>> torch.acosh(a) + tensor([ 0.7791, 1.3120, 1.2979, 1.1341 ]) + """ + +def acosh_(input: Tensor) -> Tensor: ... +def adaptive_avg_pool1d(input: Tensor, output_size: _int | _size) -> Tensor: ... +def adaptive_max_pool1d( + input: Tensor, + output_size: _int | _size, +) -> tuple[Tensor, Tensor]: ... +@overload +def add( + input: Tensor | Number | _complex, + other: Tensor | Number | _complex, + *, + alpha: Number | _complex | None = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + add(input, other, *, alpha=1, out=None) -> Tensor + + Adds :attr:`other`, scaled by :attr:`alpha`, to :attr:`input`. + + .. math:: + \text{{out}}_i = \text{{input}}_i + \text{{alpha}} \times \text{{other}}_i + + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer, float, and complex inputs. + + Args: + input (Tensor): the input tensor. + other (Tensor or Number): the tensor or number to add to :attr:`input`. + + Keyword arguments: + alpha (Number): the multiplier for :attr:`other`. + out (Tensor, optional): the output tensor. + + Examples:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.0202, 1.0985, 1.3506, -0.6056]) + >>> torch.add(a, 20) + tensor([ 20.0202, 21.0985, 21.3506, 19.3944]) + + >>> b = torch.randn(4) + >>> b + tensor([-0.9732, -0.3497, 0.6245, 0.4022]) + >>> c = torch.randn(4, 1) + >>> c + tensor([[ 0.3743], + [-1.7724], + [-0.5811], + [-0.8017]]) + >>> torch.add(b, c, alpha=10) + tensor([[ 2.7695, 3.3930, 4.3672, 4.1450], + [-18.6971, -18.0736, -17.0994, -17.3216], + [ -6.7845, -6.1610, -5.1868, -5.4090], + [ -8.9902, -8.3667, -7.3925, -7.6147]]) + """ + +@overload +def add(self: Tensor, alpha: Number | _complex, other: Tensor) -> Tensor: + r""" + add(input, other, *, alpha=1, out=None) -> Tensor + + Adds :attr:`other`, scaled by :attr:`alpha`, to :attr:`input`. + + .. math:: + \text{{out}}_i = \text{{input}}_i + \text{{alpha}} \times \text{{other}}_i + + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer, float, and complex inputs. + + Args: + input (Tensor): the input tensor. + other (Tensor or Number): the tensor or number to add to :attr:`input`. + + Keyword arguments: + alpha (Number): the multiplier for :attr:`other`. + out (Tensor, optional): the output tensor. + + Examples:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.0202, 1.0985, 1.3506, -0.6056]) + >>> torch.add(a, 20) + tensor([ 20.0202, 21.0985, 21.3506, 19.3944]) + + >>> b = torch.randn(4) + >>> b + tensor([-0.9732, -0.3497, 0.6245, 0.4022]) + >>> c = torch.randn(4, 1) + >>> c + tensor([[ 0.3743], + [-1.7724], + [-0.5811], + [-0.8017]]) + >>> torch.add(b, c, alpha=10) + tensor([[ 2.7695, 3.3930, 4.3672, 4.1450], + [-18.6971, -18.0736, -17.0994, -17.3216], + [ -6.7845, -6.1610, -5.1868, -5.4090], + [ -8.9902, -8.3667, -7.3925, -7.6147]]) + """ + +@overload +def add( + self: Tensor, + alpha: Number | _complex, + other: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + add(input, other, *, alpha=1, out=None) -> Tensor + + Adds :attr:`other`, scaled by :attr:`alpha`, to :attr:`input`. + + .. math:: + \text{{out}}_i = \text{{input}}_i + \text{{alpha}} \times \text{{other}}_i + + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer, float, and complex inputs. + + Args: + input (Tensor): the input tensor. + other (Tensor or Number): the tensor or number to add to :attr:`input`. + + Keyword arguments: + alpha (Number): the multiplier for :attr:`other`. + out (Tensor, optional): the output tensor. + + Examples:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.0202, 1.0985, 1.3506, -0.6056]) + >>> torch.add(a, 20) + tensor([ 20.0202, 21.0985, 21.3506, 19.3944]) + + >>> b = torch.randn(4) + >>> b + tensor([-0.9732, -0.3497, 0.6245, 0.4022]) + >>> c = torch.randn(4, 1) + >>> c + tensor([[ 0.3743], + [-1.7724], + [-0.5811], + [-0.8017]]) + >>> torch.add(b, c, alpha=10) + tensor([[ 2.7695, 3.3930, 4.3672, 4.1450], + [-18.6971, -18.0736, -17.0994, -17.3216], + [ -6.7845, -6.1610, -5.1868, -5.4090], + [ -8.9902, -8.3667, -7.3925, -7.6147]]) + """ + +@overload +def addbmm( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + batch1: Tensor, + batch2: Tensor, +) -> Tensor: + r""" + addbmm(input, batch1, batch2, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices stored + in :attr:`batch1` and :attr:`batch2`, + with a reduced add step (all matrix multiplications get accumulated + along the first dimension). + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the + same number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + .. math:: + out = \beta\ \text{input} + \alpha\ (\sum_{i=0}^{b-1} \text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and :attr:`alpha` + must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for `batch1 @ batch2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.addbmm(M, batch1, batch2) + tensor([[ 6.6311, 0.0503, 6.9768, -12.0362, -2.1653], + [ -4.8185, -1.4255, -6.6760, 8.9453, 2.5743], + [ -3.8202, 4.3691, 1.0943, -1.1109, 5.4730]]) + """ + +@overload +def addbmm( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + batch1: Tensor, + batch2: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + addbmm(input, batch1, batch2, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices stored + in :attr:`batch1` and :attr:`batch2`, + with a reduced add step (all matrix multiplications get accumulated + along the first dimension). + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the + same number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + .. math:: + out = \beta\ \text{input} + \alpha\ (\sum_{i=0}^{b-1} \text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and :attr:`alpha` + must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for `batch1 @ batch2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.addbmm(M, batch1, batch2) + tensor([[ 6.6311, 0.0503, 6.9768, -12.0362, -2.1653], + [ -4.8185, -1.4255, -6.6760, 8.9453, 2.5743], + [ -3.8202, 4.3691, 1.0943, -1.1109, 5.4730]]) + """ + +@overload +def addbmm( + input: Tensor, + batch1: Tensor, + batch2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + addbmm(input, batch1, batch2, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices stored + in :attr:`batch1` and :attr:`batch2`, + with a reduced add step (all matrix multiplications get accumulated + along the first dimension). + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the + same number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + .. math:: + out = \beta\ \text{input} + \alpha\ (\sum_{i=0}^{b-1} \text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and :attr:`alpha` + must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for `batch1 @ batch2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.addbmm(M, batch1, batch2) + tensor([[ 6.6311, 0.0503, 6.9768, -12.0362, -2.1653], + [ -4.8185, -1.4255, -6.6760, 8.9453, 2.5743], + [ -3.8202, 4.3691, 1.0943, -1.1109, 5.4730]]) + """ + +@overload +def addbmm( + beta: Number | _complex, + self: Tensor, + batch1: Tensor, + batch2: Tensor, +) -> Tensor: + r""" + addbmm(input, batch1, batch2, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices stored + in :attr:`batch1` and :attr:`batch2`, + with a reduced add step (all matrix multiplications get accumulated + along the first dimension). + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the + same number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + .. math:: + out = \beta\ \text{input} + \alpha\ (\sum_{i=0}^{b-1} \text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and :attr:`alpha` + must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for `batch1 @ batch2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.addbmm(M, batch1, batch2) + tensor([[ 6.6311, 0.0503, 6.9768, -12.0362, -2.1653], + [ -4.8185, -1.4255, -6.6760, 8.9453, 2.5743], + [ -3.8202, 4.3691, 1.0943, -1.1109, 5.4730]]) + """ + +@overload +def addbmm( + beta: Number | _complex, + self: Tensor, + batch1: Tensor, + batch2: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + addbmm(input, batch1, batch2, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices stored + in :attr:`batch1` and :attr:`batch2`, + with a reduced add step (all matrix multiplications get accumulated + along the first dimension). + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the + same number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + .. math:: + out = \beta\ \text{input} + \alpha\ (\sum_{i=0}^{b-1} \text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and :attr:`alpha` + must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for `batch1 @ batch2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.addbmm(M, batch1, batch2) + tensor([[ 6.6311, 0.0503, 6.9768, -12.0362, -2.1653], + [ -4.8185, -1.4255, -6.6760, 8.9453, 2.5743], + [ -3.8202, 4.3691, 1.0943, -1.1109, 5.4730]]) + """ + +@overload +def addcdiv( + self: Tensor, + value: Number | _complex, + tensor1: Tensor, + tensor2: Tensor, +) -> Tensor: + r""" + addcdiv(input, tensor1, tensor2, *, value=1, out=None) -> Tensor + + Performs the element-wise division of :attr:`tensor1` by :attr:`tensor2`, + multiplies the result by the scalar :attr:`value` and adds it to :attr:`input`. + + .. warning:: + Integer division with addcdiv is no longer supported, and in a future + release addcdiv will perform a true division of tensor1 and tensor2. + The historic addcdiv behavior can be implemented as + (input + value * torch.trunc(tensor1 / tensor2)).to(input.dtype) + for integer inputs and as (input + value * tensor1 / tensor2) for float inputs. + The future addcdiv behavior is just the latter implementation: + (input + value * tensor1 / tensor2), for all dtypes. + + .. math:: + \text{out}_i = \text{input}_i + \text{value} \times \frac{\text{tensor1}_i}{\text{tensor2}_i} + + + The shapes of :attr:`input`, :attr:`tensor1`, and :attr:`tensor2` must be + :ref:`broadcastable `. + + For inputs of type `FloatTensor` or `DoubleTensor`, :attr:`value` must be + a real number, otherwise an integer. + + Args: + input (Tensor): the tensor to be added + tensor1 (Tensor): the numerator tensor + tensor2 (Tensor): the denominator tensor + + Keyword args: + value (Number, optional): multiplier for :math:`\text{tensor1} / \text{tensor2}` + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.randn(1, 3) + >>> t1 = torch.randn(3, 1) + >>> t2 = torch.randn(1, 3) + >>> torch.addcdiv(t, t1, t2, value=0.1) + tensor([[-0.2312, -3.6496, 0.1312], + [-1.0428, 3.4292, -0.1030], + [-0.5369, -0.9829, 0.0430]]) + """ + +@overload +def addcdiv( + self: Tensor, + value: Number | _complex, + tensor1: Tensor, + tensor2: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + addcdiv(input, tensor1, tensor2, *, value=1, out=None) -> Tensor + + Performs the element-wise division of :attr:`tensor1` by :attr:`tensor2`, + multiplies the result by the scalar :attr:`value` and adds it to :attr:`input`. + + .. warning:: + Integer division with addcdiv is no longer supported, and in a future + release addcdiv will perform a true division of tensor1 and tensor2. + The historic addcdiv behavior can be implemented as + (input + value * torch.trunc(tensor1 / tensor2)).to(input.dtype) + for integer inputs and as (input + value * tensor1 / tensor2) for float inputs. + The future addcdiv behavior is just the latter implementation: + (input + value * tensor1 / tensor2), for all dtypes. + + .. math:: + \text{out}_i = \text{input}_i + \text{value} \times \frac{\text{tensor1}_i}{\text{tensor2}_i} + + + The shapes of :attr:`input`, :attr:`tensor1`, and :attr:`tensor2` must be + :ref:`broadcastable `. + + For inputs of type `FloatTensor` or `DoubleTensor`, :attr:`value` must be + a real number, otherwise an integer. + + Args: + input (Tensor): the tensor to be added + tensor1 (Tensor): the numerator tensor + tensor2 (Tensor): the denominator tensor + + Keyword args: + value (Number, optional): multiplier for :math:`\text{tensor1} / \text{tensor2}` + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.randn(1, 3) + >>> t1 = torch.randn(3, 1) + >>> t2 = torch.randn(1, 3) + >>> torch.addcdiv(t, t1, t2, value=0.1) + tensor([[-0.2312, -3.6496, 0.1312], + [-1.0428, 3.4292, -0.1030], + [-0.5369, -0.9829, 0.0430]]) + """ + +@overload +def addcdiv( + input: Tensor, + tensor1: Tensor, + tensor2: Tensor, + *, + value: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + addcdiv(input, tensor1, tensor2, *, value=1, out=None) -> Tensor + + Performs the element-wise division of :attr:`tensor1` by :attr:`tensor2`, + multiplies the result by the scalar :attr:`value` and adds it to :attr:`input`. + + .. warning:: + Integer division with addcdiv is no longer supported, and in a future + release addcdiv will perform a true division of tensor1 and tensor2. + The historic addcdiv behavior can be implemented as + (input + value * torch.trunc(tensor1 / tensor2)).to(input.dtype) + for integer inputs and as (input + value * tensor1 / tensor2) for float inputs. + The future addcdiv behavior is just the latter implementation: + (input + value * tensor1 / tensor2), for all dtypes. + + .. math:: + \text{out}_i = \text{input}_i + \text{value} \times \frac{\text{tensor1}_i}{\text{tensor2}_i} + + + The shapes of :attr:`input`, :attr:`tensor1`, and :attr:`tensor2` must be + :ref:`broadcastable `. + + For inputs of type `FloatTensor` or `DoubleTensor`, :attr:`value` must be + a real number, otherwise an integer. + + Args: + input (Tensor): the tensor to be added + tensor1 (Tensor): the numerator tensor + tensor2 (Tensor): the denominator tensor + + Keyword args: + value (Number, optional): multiplier for :math:`\text{tensor1} / \text{tensor2}` + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.randn(1, 3) + >>> t1 = torch.randn(3, 1) + >>> t2 = torch.randn(1, 3) + >>> torch.addcdiv(t, t1, t2, value=0.1) + tensor([[-0.2312, -3.6496, 0.1312], + [-1.0428, 3.4292, -0.1030], + [-0.5369, -0.9829, 0.0430]]) + """ + +@overload +def addcmul( + self: Tensor, + value: Number | _complex, + tensor1: Tensor, + tensor2: Tensor, +) -> Tensor: + r""" + addcmul(input, tensor1, tensor2, *, value=1, out=None) -> Tensor + + Performs the element-wise multiplication of :attr:`tensor1` + by :attr:`tensor2`, multiplies the result by the scalar :attr:`value` + and adds it to :attr:`input`. + + .. math:: + \text{out}_i = \text{input}_i + \text{value} \times \text{tensor1}_i \times \text{tensor2}_i + + The shapes of :attr:`tensor`, :attr:`tensor1`, and :attr:`tensor2` must be + :ref:`broadcastable `. + + For inputs of type `FloatTensor` or `DoubleTensor`, :attr:`value` must be + a real number, otherwise an integer. + + Args: + input (Tensor): the tensor to be added + tensor1 (Tensor): the tensor to be multiplied + tensor2 (Tensor): the tensor to be multiplied + + Keyword args: + value (Number, optional): multiplier for :math:`tensor1 .* tensor2` + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.randn(1, 3) + >>> t1 = torch.randn(3, 1) + >>> t2 = torch.randn(1, 3) + >>> torch.addcmul(t, t1, t2, value=0.1) + tensor([[-0.8635, -0.6391, 1.6174], + [-0.7617, -0.5879, 1.7388], + [-0.8353, -0.6249, 1.6511]]) + """ + +@overload +def addcmul( + self: Tensor, + value: Number | _complex, + tensor1: Tensor, + tensor2: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + addcmul(input, tensor1, tensor2, *, value=1, out=None) -> Tensor + + Performs the element-wise multiplication of :attr:`tensor1` + by :attr:`tensor2`, multiplies the result by the scalar :attr:`value` + and adds it to :attr:`input`. + + .. math:: + \text{out}_i = \text{input}_i + \text{value} \times \text{tensor1}_i \times \text{tensor2}_i + + The shapes of :attr:`tensor`, :attr:`tensor1`, and :attr:`tensor2` must be + :ref:`broadcastable `. + + For inputs of type `FloatTensor` or `DoubleTensor`, :attr:`value` must be + a real number, otherwise an integer. + + Args: + input (Tensor): the tensor to be added + tensor1 (Tensor): the tensor to be multiplied + tensor2 (Tensor): the tensor to be multiplied + + Keyword args: + value (Number, optional): multiplier for :math:`tensor1 .* tensor2` + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.randn(1, 3) + >>> t1 = torch.randn(3, 1) + >>> t2 = torch.randn(1, 3) + >>> torch.addcmul(t, t1, t2, value=0.1) + tensor([[-0.8635, -0.6391, 1.6174], + [-0.7617, -0.5879, 1.7388], + [-0.8353, -0.6249, 1.6511]]) + """ + +@overload +def addcmul( + input: Tensor, + tensor1: Tensor, + tensor2: Tensor, + *, + value: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + addcmul(input, tensor1, tensor2, *, value=1, out=None) -> Tensor + + Performs the element-wise multiplication of :attr:`tensor1` + by :attr:`tensor2`, multiplies the result by the scalar :attr:`value` + and adds it to :attr:`input`. + + .. math:: + \text{out}_i = \text{input}_i + \text{value} \times \text{tensor1}_i \times \text{tensor2}_i + + The shapes of :attr:`tensor`, :attr:`tensor1`, and :attr:`tensor2` must be + :ref:`broadcastable `. + + For inputs of type `FloatTensor` or `DoubleTensor`, :attr:`value` must be + a real number, otherwise an integer. + + Args: + input (Tensor): the tensor to be added + tensor1 (Tensor): the tensor to be multiplied + tensor2 (Tensor): the tensor to be multiplied + + Keyword args: + value (Number, optional): multiplier for :math:`tensor1 .* tensor2` + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.randn(1, 3) + >>> t1 = torch.randn(3, 1) + >>> t2 = torch.randn(1, 3) + >>> torch.addcmul(t, t1, t2, value=0.1) + tensor([[-0.8635, -0.6391, 1.6174], + [-0.7617, -0.5879, 1.7388], + [-0.8353, -0.6249, 1.6511]]) + """ + +@overload +def addmm( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + mat1: Tensor, + mat2: Tensor, +) -> Tensor: + r""" + addmm(input, mat1, mat2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix multiplication of the matrices :attr:`mat1` and :attr:`mat2`. + The matrix :attr:`input` is added to the final result. + + If :attr:`mat1` is a :math:`(n \times m)` tensor, :attr:`mat2` is a + :math:`(m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat1` and :attr:`mat2` and the added matrix :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat1}_i \mathbin{@} \text{mat2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operation has support for arguments with :ref:`sparse layouts`. If + :attr:`input` is sparse the result will have the same layout and if :attr:`out` + is provided it must have the same layout as :attr:`input`. + + + .. warning:: + Sparse support is a beta feature and some layout(s)/dtype/device combinations may not be supported, + or may not have autograd support. If you notice missing functionality please + open a feature request. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + mat1 (Tensor): the first matrix to be matrix multiplied + mat2 (Tensor): the second matrix to be matrix multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat1 @ mat2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2, 3) + >>> mat1 = torch.randn(2, 3) + >>> mat2 = torch.randn(3, 3) + >>> torch.addmm(M, mat1, mat2) + tensor([[-4.8716, 1.4671, -1.3746], + [ 0.7573, -3.9555, -2.8681]]) + """ + +@overload +def addmm( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + mat1: Tensor, + mat2: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + addmm(input, mat1, mat2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix multiplication of the matrices :attr:`mat1` and :attr:`mat2`. + The matrix :attr:`input` is added to the final result. + + If :attr:`mat1` is a :math:`(n \times m)` tensor, :attr:`mat2` is a + :math:`(m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat1` and :attr:`mat2` and the added matrix :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat1}_i \mathbin{@} \text{mat2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operation has support for arguments with :ref:`sparse layouts`. If + :attr:`input` is sparse the result will have the same layout and if :attr:`out` + is provided it must have the same layout as :attr:`input`. + + + .. warning:: + Sparse support is a beta feature and some layout(s)/dtype/device combinations may not be supported, + or may not have autograd support. If you notice missing functionality please + open a feature request. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + mat1 (Tensor): the first matrix to be matrix multiplied + mat2 (Tensor): the second matrix to be matrix multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat1 @ mat2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2, 3) + >>> mat1 = torch.randn(2, 3) + >>> mat2 = torch.randn(3, 3) + >>> torch.addmm(M, mat1, mat2) + tensor([[-4.8716, 1.4671, -1.3746], + [ 0.7573, -3.9555, -2.8681]]) + """ + +@overload +def addmm( + input: Tensor, + mat1: Tensor, + mat2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + addmm(input, mat1, mat2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix multiplication of the matrices :attr:`mat1` and :attr:`mat2`. + The matrix :attr:`input` is added to the final result. + + If :attr:`mat1` is a :math:`(n \times m)` tensor, :attr:`mat2` is a + :math:`(m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat1` and :attr:`mat2` and the added matrix :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat1}_i \mathbin{@} \text{mat2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operation has support for arguments with :ref:`sparse layouts`. If + :attr:`input` is sparse the result will have the same layout and if :attr:`out` + is provided it must have the same layout as :attr:`input`. + + + .. warning:: + Sparse support is a beta feature and some layout(s)/dtype/device combinations may not be supported, + or may not have autograd support. If you notice missing functionality please + open a feature request. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + mat1 (Tensor): the first matrix to be matrix multiplied + mat2 (Tensor): the second matrix to be matrix multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat1 @ mat2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2, 3) + >>> mat1 = torch.randn(2, 3) + >>> mat2 = torch.randn(3, 3) + >>> torch.addmm(M, mat1, mat2) + tensor([[-4.8716, 1.4671, -1.3746], + [ 0.7573, -3.9555, -2.8681]]) + """ + +@overload +def addmm( + input: Tensor, + mat1: Tensor, + mat2: Tensor, + out_dtype: _dtype, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + addmm(input, mat1, mat2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix multiplication of the matrices :attr:`mat1` and :attr:`mat2`. + The matrix :attr:`input` is added to the final result. + + If :attr:`mat1` is a :math:`(n \times m)` tensor, :attr:`mat2` is a + :math:`(m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat1` and :attr:`mat2` and the added matrix :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat1}_i \mathbin{@} \text{mat2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operation has support for arguments with :ref:`sparse layouts`. If + :attr:`input` is sparse the result will have the same layout and if :attr:`out` + is provided it must have the same layout as :attr:`input`. + + + .. warning:: + Sparse support is a beta feature and some layout(s)/dtype/device combinations may not be supported, + or may not have autograd support. If you notice missing functionality please + open a feature request. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + mat1 (Tensor): the first matrix to be matrix multiplied + mat2 (Tensor): the second matrix to be matrix multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat1 @ mat2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2, 3) + >>> mat1 = torch.randn(2, 3) + >>> mat2 = torch.randn(3, 3) + >>> torch.addmm(M, mat1, mat2) + tensor([[-4.8716, 1.4671, -1.3746], + [ 0.7573, -3.9555, -2.8681]]) + """ + +@overload +def addmm( + beta: Number | _complex, + self: Tensor, + mat1: Tensor, + mat2: Tensor, +) -> Tensor: + r""" + addmm(input, mat1, mat2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix multiplication of the matrices :attr:`mat1` and :attr:`mat2`. + The matrix :attr:`input` is added to the final result. + + If :attr:`mat1` is a :math:`(n \times m)` tensor, :attr:`mat2` is a + :math:`(m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat1` and :attr:`mat2` and the added matrix :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat1}_i \mathbin{@} \text{mat2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operation has support for arguments with :ref:`sparse layouts`. If + :attr:`input` is sparse the result will have the same layout and if :attr:`out` + is provided it must have the same layout as :attr:`input`. + + + .. warning:: + Sparse support is a beta feature and some layout(s)/dtype/device combinations may not be supported, + or may not have autograd support. If you notice missing functionality please + open a feature request. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + mat1 (Tensor): the first matrix to be matrix multiplied + mat2 (Tensor): the second matrix to be matrix multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat1 @ mat2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2, 3) + >>> mat1 = torch.randn(2, 3) + >>> mat2 = torch.randn(3, 3) + >>> torch.addmm(M, mat1, mat2) + tensor([[-4.8716, 1.4671, -1.3746], + [ 0.7573, -3.9555, -2.8681]]) + """ + +@overload +def addmm( + beta: Number | _complex, + self: Tensor, + mat1: Tensor, + mat2: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + addmm(input, mat1, mat2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix multiplication of the matrices :attr:`mat1` and :attr:`mat2`. + The matrix :attr:`input` is added to the final result. + + If :attr:`mat1` is a :math:`(n \times m)` tensor, :attr:`mat2` is a + :math:`(m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat1` and :attr:`mat2` and the added matrix :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat1}_i \mathbin{@} \text{mat2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operation has support for arguments with :ref:`sparse layouts`. If + :attr:`input` is sparse the result will have the same layout and if :attr:`out` + is provided it must have the same layout as :attr:`input`. + + + .. warning:: + Sparse support is a beta feature and some layout(s)/dtype/device combinations may not be supported, + or may not have autograd support. If you notice missing functionality please + open a feature request. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + mat1 (Tensor): the first matrix to be matrix multiplied + mat2 (Tensor): the second matrix to be matrix multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat1 @ mat2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2, 3) + >>> mat1 = torch.randn(2, 3) + >>> mat2 = torch.randn(3, 3) + >>> torch.addmm(M, mat1, mat2) + tensor([[-4.8716, 1.4671, -1.3746], + [ 0.7573, -3.9555, -2.8681]]) + """ + +@overload +def addmv( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + mat: Tensor, + vec: Tensor, +) -> Tensor: + r""" + addmv(input, mat, vec, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix-vector product of the matrix :attr:`mat` and + the vector :attr:`vec`. + The vector :attr:`input` is added to the final result. + + If :attr:`mat` is a :math:`(n \times m)` tensor, :attr:`vec` is a 1-D tensor of + size `m`, then :attr:`input` must be + :ref:`broadcastable ` with a 1-D tensor of size `n` and + :attr:`out` will be 1-D tensor of size `n`. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat` and :attr:`vec` and the added tensor :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat} \mathbin{@} \text{vec}) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + Args: + input (Tensor): vector to be added + mat (Tensor): matrix to be matrix multiplied + vec (Tensor): vector to be matrix multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat @ vec` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2) + >>> mat = torch.randn(2, 3) + >>> vec = torch.randn(3) + >>> torch.addmv(M, mat, vec) + tensor([-0.3768, -5.5565]) + """ + +@overload +def addmv( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + mat: Tensor, + vec: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + addmv(input, mat, vec, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix-vector product of the matrix :attr:`mat` and + the vector :attr:`vec`. + The vector :attr:`input` is added to the final result. + + If :attr:`mat` is a :math:`(n \times m)` tensor, :attr:`vec` is a 1-D tensor of + size `m`, then :attr:`input` must be + :ref:`broadcastable ` with a 1-D tensor of size `n` and + :attr:`out` will be 1-D tensor of size `n`. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat` and :attr:`vec` and the added tensor :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat} \mathbin{@} \text{vec}) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + Args: + input (Tensor): vector to be added + mat (Tensor): matrix to be matrix multiplied + vec (Tensor): vector to be matrix multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat @ vec` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2) + >>> mat = torch.randn(2, 3) + >>> vec = torch.randn(3) + >>> torch.addmv(M, mat, vec) + tensor([-0.3768, -5.5565]) + """ + +@overload +def addmv( + input: Tensor, + mat: Tensor, + vec: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + addmv(input, mat, vec, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix-vector product of the matrix :attr:`mat` and + the vector :attr:`vec`. + The vector :attr:`input` is added to the final result. + + If :attr:`mat` is a :math:`(n \times m)` tensor, :attr:`vec` is a 1-D tensor of + size `m`, then :attr:`input` must be + :ref:`broadcastable ` with a 1-D tensor of size `n` and + :attr:`out` will be 1-D tensor of size `n`. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat` and :attr:`vec` and the added tensor :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat} \mathbin{@} \text{vec}) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + Args: + input (Tensor): vector to be added + mat (Tensor): matrix to be matrix multiplied + vec (Tensor): vector to be matrix multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat @ vec` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2) + >>> mat = torch.randn(2, 3) + >>> vec = torch.randn(3) + >>> torch.addmv(M, mat, vec) + tensor([-0.3768, -5.5565]) + """ + +@overload +def addmv( + beta: Number | _complex, + self: Tensor, + mat: Tensor, + vec: Tensor, +) -> Tensor: + r""" + addmv(input, mat, vec, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix-vector product of the matrix :attr:`mat` and + the vector :attr:`vec`. + The vector :attr:`input` is added to the final result. + + If :attr:`mat` is a :math:`(n \times m)` tensor, :attr:`vec` is a 1-D tensor of + size `m`, then :attr:`input` must be + :ref:`broadcastable ` with a 1-D tensor of size `n` and + :attr:`out` will be 1-D tensor of size `n`. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat` and :attr:`vec` and the added tensor :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat} \mathbin{@} \text{vec}) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + Args: + input (Tensor): vector to be added + mat (Tensor): matrix to be matrix multiplied + vec (Tensor): vector to be matrix multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat @ vec` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2) + >>> mat = torch.randn(2, 3) + >>> vec = torch.randn(3) + >>> torch.addmv(M, mat, vec) + tensor([-0.3768, -5.5565]) + """ + +@overload +def addmv( + beta: Number | _complex, + self: Tensor, + mat: Tensor, + vec: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + addmv(input, mat, vec, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix-vector product of the matrix :attr:`mat` and + the vector :attr:`vec`. + The vector :attr:`input` is added to the final result. + + If :attr:`mat` is a :math:`(n \times m)` tensor, :attr:`vec` is a 1-D tensor of + size `m`, then :attr:`input` must be + :ref:`broadcastable ` with a 1-D tensor of size `n` and + :attr:`out` will be 1-D tensor of size `n`. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat` and :attr:`vec` and the added tensor :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat} \mathbin{@} \text{vec}) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + Args: + input (Tensor): vector to be added + mat (Tensor): matrix to be matrix multiplied + vec (Tensor): vector to be matrix multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat @ vec` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2) + >>> mat = torch.randn(2, 3) + >>> vec = torch.randn(3) + >>> torch.addmv(M, mat, vec) + tensor([-0.3768, -5.5565]) + """ + +@overload +def addmv_( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + mat: Tensor, + vec: Tensor, +) -> Tensor: ... +@overload +def addmv_( + input: Tensor, + mat: Tensor, + vec: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, +) -> Tensor: ... +@overload +def addmv_( + beta: Number | _complex, + self: Tensor, + mat: Tensor, + vec: Tensor, +) -> Tensor: ... +@overload +def addr( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + vec1: Tensor, + vec2: Tensor, +) -> Tensor: + r""" + addr(input, vec1, vec2, *, beta=1, alpha=1, out=None) -> Tensor + + Performs the outer-product of vectors :attr:`vec1` and :attr:`vec2` + and adds it to the matrix :attr:`input`. + + Optional values :attr:`beta` and :attr:`alpha` are scaling factors on the + outer product between :attr:`vec1` and :attr:`vec2` and the added matrix + :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{vec1} \otimes \text{vec2}) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + If :attr:`vec1` is a vector of size `n` and :attr:`vec2` is a vector + of size `m`, then :attr:`input` must be + :ref:`broadcastable ` with a matrix of size + :math:`(n \times m)` and :attr:`out` will be a matrix of size + :math:`(n \times m)`. + + Args: + input (Tensor): matrix to be added + vec1 (Tensor): the first vector of the outer product + vec2 (Tensor): the second vector of the outer product + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{vec1} \otimes \text{vec2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> vec1 = torch.arange(1., 4.) + >>> vec2 = torch.arange(1., 3.) + >>> M = torch.zeros(3, 2) + >>> torch.addr(M, vec1, vec2) + tensor([[ 1., 2.], + [ 2., 4.], + [ 3., 6.]]) + """ + +@overload +def addr( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + vec1: Tensor, + vec2: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + addr(input, vec1, vec2, *, beta=1, alpha=1, out=None) -> Tensor + + Performs the outer-product of vectors :attr:`vec1` and :attr:`vec2` + and adds it to the matrix :attr:`input`. + + Optional values :attr:`beta` and :attr:`alpha` are scaling factors on the + outer product between :attr:`vec1` and :attr:`vec2` and the added matrix + :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{vec1} \otimes \text{vec2}) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + If :attr:`vec1` is a vector of size `n` and :attr:`vec2` is a vector + of size `m`, then :attr:`input` must be + :ref:`broadcastable ` with a matrix of size + :math:`(n \times m)` and :attr:`out` will be a matrix of size + :math:`(n \times m)`. + + Args: + input (Tensor): matrix to be added + vec1 (Tensor): the first vector of the outer product + vec2 (Tensor): the second vector of the outer product + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{vec1} \otimes \text{vec2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> vec1 = torch.arange(1., 4.) + >>> vec2 = torch.arange(1., 3.) + >>> M = torch.zeros(3, 2) + >>> torch.addr(M, vec1, vec2) + tensor([[ 1., 2.], + [ 2., 4.], + [ 3., 6.]]) + """ + +@overload +def addr( + input: Tensor, + vec1: Tensor, + vec2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + addr(input, vec1, vec2, *, beta=1, alpha=1, out=None) -> Tensor + + Performs the outer-product of vectors :attr:`vec1` and :attr:`vec2` + and adds it to the matrix :attr:`input`. + + Optional values :attr:`beta` and :attr:`alpha` are scaling factors on the + outer product between :attr:`vec1` and :attr:`vec2` and the added matrix + :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{vec1} \otimes \text{vec2}) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + If :attr:`vec1` is a vector of size `n` and :attr:`vec2` is a vector + of size `m`, then :attr:`input` must be + :ref:`broadcastable ` with a matrix of size + :math:`(n \times m)` and :attr:`out` will be a matrix of size + :math:`(n \times m)`. + + Args: + input (Tensor): matrix to be added + vec1 (Tensor): the first vector of the outer product + vec2 (Tensor): the second vector of the outer product + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{vec1} \otimes \text{vec2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> vec1 = torch.arange(1., 4.) + >>> vec2 = torch.arange(1., 3.) + >>> M = torch.zeros(3, 2) + >>> torch.addr(M, vec1, vec2) + tensor([[ 1., 2.], + [ 2., 4.], + [ 3., 6.]]) + """ + +@overload +def addr( + beta: Number | _complex, + self: Tensor, + vec1: Tensor, + vec2: Tensor, +) -> Tensor: + r""" + addr(input, vec1, vec2, *, beta=1, alpha=1, out=None) -> Tensor + + Performs the outer-product of vectors :attr:`vec1` and :attr:`vec2` + and adds it to the matrix :attr:`input`. + + Optional values :attr:`beta` and :attr:`alpha` are scaling factors on the + outer product between :attr:`vec1` and :attr:`vec2` and the added matrix + :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{vec1} \otimes \text{vec2}) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + If :attr:`vec1` is a vector of size `n` and :attr:`vec2` is a vector + of size `m`, then :attr:`input` must be + :ref:`broadcastable ` with a matrix of size + :math:`(n \times m)` and :attr:`out` will be a matrix of size + :math:`(n \times m)`. + + Args: + input (Tensor): matrix to be added + vec1 (Tensor): the first vector of the outer product + vec2 (Tensor): the second vector of the outer product + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{vec1} \otimes \text{vec2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> vec1 = torch.arange(1., 4.) + >>> vec2 = torch.arange(1., 3.) + >>> M = torch.zeros(3, 2) + >>> torch.addr(M, vec1, vec2) + tensor([[ 1., 2.], + [ 2., 4.], + [ 3., 6.]]) + """ + +@overload +def addr( + beta: Number | _complex, + self: Tensor, + vec1: Tensor, + vec2: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + addr(input, vec1, vec2, *, beta=1, alpha=1, out=None) -> Tensor + + Performs the outer-product of vectors :attr:`vec1` and :attr:`vec2` + and adds it to the matrix :attr:`input`. + + Optional values :attr:`beta` and :attr:`alpha` are scaling factors on the + outer product between :attr:`vec1` and :attr:`vec2` and the added matrix + :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{vec1} \otimes \text{vec2}) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + If :attr:`vec1` is a vector of size `n` and :attr:`vec2` is a vector + of size `m`, then :attr:`input` must be + :ref:`broadcastable ` with a matrix of size + :math:`(n \times m)` and :attr:`out` will be a matrix of size + :math:`(n \times m)`. + + Args: + input (Tensor): matrix to be added + vec1 (Tensor): the first vector of the outer product + vec2 (Tensor): the second vector of the outer product + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{vec1} \otimes \text{vec2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> vec1 = torch.arange(1., 4.) + >>> vec2 = torch.arange(1., 3.) + >>> M = torch.zeros(3, 2) + >>> torch.addr(M, vec1, vec2) + tensor([[ 1., 2.], + [ 2., 4.], + [ 3., 6.]]) + """ + +def adjoint(input: Tensor) -> Tensor: + r""" + adjoint(input: Tensor) -> Tensor + Returns a view of the tensor conjugated and with the last two dimensions transposed. + + ``x.adjoint()`` is equivalent to ``x.transpose(-2, -1).conj()`` for complex tensors and + to ``x.transpose(-2, -1)`` for real tensors. + + Args: + {input} + + Example:: + + >>> x = torch.arange(4, dtype=torch.float) + >>> A = torch.complex(x, x).reshape(2, 2) + >>> A + tensor([[0.+0.j, 1.+1.j], + [2.+2.j, 3.+3.j]]) + >>> A.adjoint() + tensor([[0.-0.j, 2.-2.j], + [1.-1.j, 3.-3.j]]) + >>> (A.adjoint() == A.mH).all() + tensor(True) + """ + +def affine_grid_generator( + theta: Tensor, + size: Sequence[_int | SymInt], + align_corners: _bool, +) -> Tensor: ... +def alias_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + Performs the same operation as :func:`torch.alias`, but all output tensors + are freshly created instead of aliasing the input. + """ + +@overload +def all(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + all(input: Tensor, *, out=None) -> Tensor + + Tests if all elements in :attr:`input` evaluate to `True`. + + .. note:: This function matches the behaviour of NumPy in returning + output of dtype `bool` for all supported dtypes except `uint8`. + For `uint8` the dtype of output is `uint8` itself. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(1, 2).bool() + >>> a + tensor([[False, True]], dtype=torch.bool) + >>> torch.all(a) + tensor(False, dtype=torch.bool) + >>> a = torch.arange(0, 3) + >>> a + tensor([0, 1, 2]) + >>> torch.all(a) + tensor(False) + + .. function:: all(input, dim, keepdim=False, *, out=None) -> Tensor + :noindex: + + For each row of :attr:`input` in the given dimension :attr:`dim`, + returns `True` if all elements in the row evaluate to `True` and `False` otherwise. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(4, 2).bool() + >>> a + tensor([[True, True], + [True, False], + [True, True], + [True, True]], dtype=torch.bool) + >>> torch.all(a, dim=1) + tensor([ True, False, True, True], dtype=torch.bool) + >>> torch.all(a, dim=0) + tensor([ True, False], dtype=torch.bool) + """ + +@overload +def all( + input: Tensor, + dim: _size | None = None, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + all(input: Tensor, *, out=None) -> Tensor + + Tests if all elements in :attr:`input` evaluate to `True`. + + .. note:: This function matches the behaviour of NumPy in returning + output of dtype `bool` for all supported dtypes except `uint8`. + For `uint8` the dtype of output is `uint8` itself. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(1, 2).bool() + >>> a + tensor([[False, True]], dtype=torch.bool) + >>> torch.all(a) + tensor(False, dtype=torch.bool) + >>> a = torch.arange(0, 3) + >>> a + tensor([0, 1, 2]) + >>> torch.all(a) + tensor(False) + + .. function:: all(input, dim, keepdim=False, *, out=None) -> Tensor + :noindex: + + For each row of :attr:`input` in the given dimension :attr:`dim`, + returns `True` if all elements in the row evaluate to `True` and `False` otherwise. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(4, 2).bool() + >>> a + tensor([[True, True], + [True, False], + [True, True], + [True, True]], dtype=torch.bool) + >>> torch.all(a, dim=1) + tensor([ True, False, True, True], dtype=torch.bool) + >>> torch.all(a, dim=0) + tensor([ True, False], dtype=torch.bool) + """ + +@overload +def all( + input: Tensor, + dim: _int, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + all(input: Tensor, *, out=None) -> Tensor + + Tests if all elements in :attr:`input` evaluate to `True`. + + .. note:: This function matches the behaviour of NumPy in returning + output of dtype `bool` for all supported dtypes except `uint8`. + For `uint8` the dtype of output is `uint8` itself. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(1, 2).bool() + >>> a + tensor([[False, True]], dtype=torch.bool) + >>> torch.all(a) + tensor(False, dtype=torch.bool) + >>> a = torch.arange(0, 3) + >>> a + tensor([0, 1, 2]) + >>> torch.all(a) + tensor(False) + + .. function:: all(input, dim, keepdim=False, *, out=None) -> Tensor + :noindex: + + For each row of :attr:`input` in the given dimension :attr:`dim`, + returns `True` if all elements in the row evaluate to `True` and `False` otherwise. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(4, 2).bool() + >>> a + tensor([[True, True], + [True, False], + [True, True], + [True, True]], dtype=torch.bool) + >>> torch.all(a, dim=1) + tensor([ True, False, True, True], dtype=torch.bool) + >>> torch.all(a, dim=0) + tensor([ True, False], dtype=torch.bool) + """ + +@overload +def all( + input: Tensor, + dim: str | EllipsisType | None, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + all(input: Tensor, *, out=None) -> Tensor + + Tests if all elements in :attr:`input` evaluate to `True`. + + .. note:: This function matches the behaviour of NumPy in returning + output of dtype `bool` for all supported dtypes except `uint8`. + For `uint8` the dtype of output is `uint8` itself. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(1, 2).bool() + >>> a + tensor([[False, True]], dtype=torch.bool) + >>> torch.all(a) + tensor(False, dtype=torch.bool) + >>> a = torch.arange(0, 3) + >>> a + tensor([0, 1, 2]) + >>> torch.all(a) + tensor(False) + + .. function:: all(input, dim, keepdim=False, *, out=None) -> Tensor + :noindex: + + For each row of :attr:`input` in the given dimension :attr:`dim`, + returns `True` if all elements in the row evaluate to `True` and `False` otherwise. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(4, 2).bool() + >>> a + tensor([[True, True], + [True, False], + [True, True], + [True, True]], dtype=torch.bool) + >>> torch.all(a, dim=1) + tensor([ True, False, True, True], dtype=torch.bool) + >>> torch.all(a, dim=0) + tensor([ True, False], dtype=torch.bool) + """ + +def allclose( + input: Tensor, + other: Tensor, + rtol: _float = 1e-05, + atol: _float = 1e-08, + equal_nan: _bool = False, +) -> _bool: + r""" + allclose(input: Tensor, other: Tensor, rtol: float = 1e-05, atol: float = 1e-08, equal_nan: bool = False) -> bool + + This function checks if :attr:`input` and :attr:`other` satisfy the condition: + + .. math:: + \lvert \text{input}_i - \text{other}_i \rvert \leq \texttt{atol} + \texttt{rtol} \times \lvert \text{other}_i \rvert + + elementwise, for all elements of :attr:`input` and :attr:`other`. The behaviour of this function is analogous to + `numpy.allclose `_ + + Args: + input (Tensor): first tensor to compare + other (Tensor): second tensor to compare + atol (float, optional): absolute tolerance. Default: 1e-08 + rtol (float, optional): relative tolerance. Default: 1e-05 + equal_nan (bool, optional): if ``True``, then two ``NaN`` s will be considered equal. Default: ``False`` + + Example:: + + >>> torch.allclose(torch.tensor([10000., 1e-07]), torch.tensor([10000.1, 1e-08])) + False + >>> torch.allclose(torch.tensor([10000., 1e-08]), torch.tensor([10000.1, 1e-09])) + True + >>> torch.allclose(torch.tensor([1.0, float('nan')]), torch.tensor([1.0, float('nan')])) + False + >>> torch.allclose(torch.tensor([1.0, float('nan')]), torch.tensor([1.0, float('nan')]), equal_nan=True) + True + """ + +def alpha_dropout(input: Tensor, p: _float, train: _bool) -> Tensor: ... +def alpha_dropout_(input: Tensor, p: _float, train: _bool) -> Tensor: ... +def amax( + input: Tensor, + dim: _int | _size = (), + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + amax(input, dim=None, keepdim=False, *, out=None) -> Tensor + + Returns the maximum value of each slice of the :attr:`input` tensor in the given + dimension(s) :attr:`dim`. + + .. note:: + The difference between ``max``/``min`` and ``amax``/``amin`` is: + - ``amax``/``amin`` supports reducing on multiple dimensions, + - ``amax``/``amin`` does not return indices. + + Both ``amax``/``amin`` evenly distribute gradients between equal values + when there are multiple input elements with the same minimum or maximum value. + + For ``max``/``min``: + - If reduce over all dimensions(no dim specified), gradients evenly distribute between equally ``max``/``min`` values. + - If reduce over one specified axis, only propagate to the indexed element. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.8177, 1.4878, -0.2491, 0.9130], + [-0.7158, 1.1775, 2.0992, 0.4817], + [-0.0053, 0.0164, -1.3738, -0.0507], + [ 1.9700, 1.1106, -1.0318, -1.0816]]) + >>> torch.amax(a, 1) + tensor([1.4878, 2.0992, 0.0164, 1.9700]) + """ + +def amin( + input: Tensor, + dim: _int | _size = (), + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + amin(input, dim=None, keepdim=False, *, out=None) -> Tensor + + Returns the minimum value of each slice of the :attr:`input` tensor in the given + dimension(s) :attr:`dim`. + + .. note:: + The difference between ``max``/``min`` and ``amax``/``amin`` is: + - ``amax``/``amin`` supports reducing on multiple dimensions, + - ``amax``/``amin`` does not return indices. + + Both ``amax``/``amin`` evenly distribute gradients between equal values + when there are multiple input elements with the same minimum or maximum value. + + For ``max``/``min``: + - If reduce over all dimensions(no dim specified), gradients evenly distribute between equally ``max``/``min`` values. + - If reduce over one specified axis, only propagate to the indexed element. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.6451, -0.4866, 0.2987, -1.3312], + [-0.5744, 1.2980, 1.8397, -0.2713], + [ 0.9128, 0.9214, -1.7268, -0.2995], + [ 0.9023, 0.4853, 0.9075, -1.6165]]) + >>> torch.amin(a, 1) + tensor([-1.3312, -0.5744, -1.7268, -1.6165]) + """ + +def aminmax( + input: Tensor, + *, + dim: _int | None = None, + keepdim: _bool = False, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.aminmax: + r""" + aminmax(input, *, dim=None, keepdim=False, out=None) -> (Tensor min, Tensor max) + + Computes the minimum and maximum values of the :attr:`input` tensor. + + Args: + input (Tensor): + The input tensor + + Keyword Args: + dim (Optional[int]): + The dimension along which to compute the values. If `None`, + computes the values over the entire :attr:`input` tensor. + Default is `None`. + keepdim (bool): + If `True`, the reduced dimensions will be kept in the output + tensor as dimensions with size 1 for broadcasting, otherwise + they will be removed, as if calling (:func:`torch.squeeze`). + Default is `False`. + out (Optional[Tuple[Tensor, Tensor]]): + Optional tensors on which to write the result. Must have the same + shape and dtype as the expected output. + Default is `None`. + + Returns: + A named tuple `(min, max)` containing the minimum and maximum values. + + Raises: + RuntimeError + If any of the dimensions to compute the values over has size 0. + + .. note:: + NaN values are propagated to the output if at least one value is NaN. + + .. seealso:: + :func:`torch.amin` computes just the minimum value + :func:`torch.amax` computes just the maximum value + + Example:: + + >>> torch.aminmax(torch.tensor([1, -3, 5])) + torch.return_types.aminmax( + min=tensor(-3), + max=tensor(5)) + + >>> # aminmax propagates NaNs + >>> torch.aminmax(torch.tensor([1, -3, 5, torch.nan])) + torch.return_types.aminmax( + min=tensor(nan), + max=tensor(nan)) + + >>> t = torch.arange(10).view(2, 5) + >>> t + tensor([[0, 1, 2, 3, 4], + [5, 6, 7, 8, 9]]) + >>> t.aminmax(dim=0, keepdim=True) + torch.return_types.aminmax( + min=tensor([[0, 1, 2, 3, 4]]), + max=tensor([[5, 6, 7, 8, 9]])) + """ + +def angle(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + angle(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Computes the element-wise angle (in radians) of the given :attr:`input` tensor. + + .. math:: + \text{out}_{i} = angle(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + .. note:: Starting in PyTorch 1.8, angle returns pi for negative real numbers, + zero for non-negative real numbers, and propagates NaNs. Previously + the function would return zero for all real numbers and not propagate + floating-point NaNs. + + Example:: + + >>> torch.angle(torch.tensor([-1 + 1j, -2 + 2j, 3 - 3j]))*180/3.14159 + tensor([ 135., 135, -45]) + """ + +@overload +def any(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + any(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Tests if any element in :attr:`input` evaluates to `True`. + + .. note:: This function matches the behaviour of NumPy in returning + output of dtype `bool` for all supported dtypes except `uint8`. + For `uint8` the dtype of output is `uint8` itself. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(1, 2).bool() + >>> a + tensor([[False, True]], dtype=torch.bool) + >>> torch.any(a) + tensor(True, dtype=torch.bool) + >>> a = torch.arange(0, 3) + >>> a + tensor([0, 1, 2]) + >>> torch.any(a) + tensor(True) + + .. function:: any(input, dim, keepdim=False, *, out=None) -> Tensor + :noindex: + + For each row of :attr:`input` in the given dimension :attr:`dim`, + returns `True` if any element in the row evaluate to `True` and `False` otherwise. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4, 2) < 0 + >>> a + tensor([[ True, True], + [False, True], + [ True, True], + [False, False]]) + >>> torch.any(a, 1) + tensor([ True, True, True, False]) + >>> torch.any(a, 0) + tensor([True, True]) + """ + +@overload +def any( + input: Tensor, + dim: _size | None = None, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + any(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Tests if any element in :attr:`input` evaluates to `True`. + + .. note:: This function matches the behaviour of NumPy in returning + output of dtype `bool` for all supported dtypes except `uint8`. + For `uint8` the dtype of output is `uint8` itself. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(1, 2).bool() + >>> a + tensor([[False, True]], dtype=torch.bool) + >>> torch.any(a) + tensor(True, dtype=torch.bool) + >>> a = torch.arange(0, 3) + >>> a + tensor([0, 1, 2]) + >>> torch.any(a) + tensor(True) + + .. function:: any(input, dim, keepdim=False, *, out=None) -> Tensor + :noindex: + + For each row of :attr:`input` in the given dimension :attr:`dim`, + returns `True` if any element in the row evaluate to `True` and `False` otherwise. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4, 2) < 0 + >>> a + tensor([[ True, True], + [False, True], + [ True, True], + [False, False]]) + >>> torch.any(a, 1) + tensor([ True, True, True, False]) + >>> torch.any(a, 0) + tensor([True, True]) + """ + +@overload +def any( + input: Tensor, + dim: _int, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + any(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Tests if any element in :attr:`input` evaluates to `True`. + + .. note:: This function matches the behaviour of NumPy in returning + output of dtype `bool` for all supported dtypes except `uint8`. + For `uint8` the dtype of output is `uint8` itself. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(1, 2).bool() + >>> a + tensor([[False, True]], dtype=torch.bool) + >>> torch.any(a) + tensor(True, dtype=torch.bool) + >>> a = torch.arange(0, 3) + >>> a + tensor([0, 1, 2]) + >>> torch.any(a) + tensor(True) + + .. function:: any(input, dim, keepdim=False, *, out=None) -> Tensor + :noindex: + + For each row of :attr:`input` in the given dimension :attr:`dim`, + returns `True` if any element in the row evaluate to `True` and `False` otherwise. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4, 2) < 0 + >>> a + tensor([[ True, True], + [False, True], + [ True, True], + [False, False]]) + >>> torch.any(a, 1) + tensor([ True, True, True, False]) + >>> torch.any(a, 0) + tensor([True, True]) + """ + +@overload +def any( + input: Tensor, + dim: str | EllipsisType | None, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + any(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Tests if any element in :attr:`input` evaluates to `True`. + + .. note:: This function matches the behaviour of NumPy in returning + output of dtype `bool` for all supported dtypes except `uint8`. + For `uint8` the dtype of output is `uint8` itself. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(1, 2).bool() + >>> a + tensor([[False, True]], dtype=torch.bool) + >>> torch.any(a) + tensor(True, dtype=torch.bool) + >>> a = torch.arange(0, 3) + >>> a + tensor([0, 1, 2]) + >>> torch.any(a) + tensor(True) + + .. function:: any(input, dim, keepdim=False, *, out=None) -> Tensor + :noindex: + + For each row of :attr:`input` in the given dimension :attr:`dim`, + returns `True` if any element in the row evaluate to `True` and `False` otherwise. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4, 2) < 0 + >>> a + tensor([[ True, True], + [False, True], + [ True, True], + [False, False]]) + >>> torch.any(a, 1) + tensor([ True, True, True, False]) + >>> torch.any(a, 0) + tensor([True, True]) + """ + +@overload +def arange( + start: Number, + end: Number, + step: Number, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + arange(start=0, end, step=1, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a 1-D tensor of size :math:`\left\lceil \frac{\text{end} - \text{start}}{\text{step}} \right\rceil` + with values from the interval ``[start, end)`` taken with common difference + :attr:`step` beginning from `start`. + + Note: When using floating-point dtypes (especially reduced precision types like ``bfloat16``), + the results may be affected by floating-point rounding behavior. Some values in the sequence + might not be exactly representable in certain floating-point formats, which can lead to + repeated values or unexpected rounding. For precise sequences, it is recommended to use + integer dtypes instead of floating-point dtypes. + + Note that non-integer :attr:`step` is subject to floating point rounding errors when + comparing against :attr:`end`; to avoid inconsistency, we advise subtracting a small epsilon from :attr:`end` + in such cases. + + .. math:: + \text{out}_{{i+1}} = \text{out}_{i} + \text{step} + + Args: + start (Number, optional): the starting value for the set of points. Default: ``0``. + end (Number): the ending value for the set of points + step (Number, optional): the gap between each pair of adjacent points. Default: ``1``. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). If `dtype` is not given, infer the data type from the other input + arguments. If any of `start`, `end`, or `stop` are floating-point, the + `dtype` is inferred to be the default dtype, see + :meth:`~torch.get_default_dtype`. Otherwise, the `dtype` is inferred to + be `torch.int64`. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.arange(5) + tensor([ 0, 1, 2, 3, 4]) + >>> torch.arange(1, 4) + tensor([ 1, 2, 3]) + >>> torch.arange(1, 2.5, 0.5) + tensor([ 1.0000, 1.5000, 2.0000]) + """ + +@overload +def arange( + start: Number, + end: Number, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + arange(start=0, end, step=1, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a 1-D tensor of size :math:`\left\lceil \frac{\text{end} - \text{start}}{\text{step}} \right\rceil` + with values from the interval ``[start, end)`` taken with common difference + :attr:`step` beginning from `start`. + + Note: When using floating-point dtypes (especially reduced precision types like ``bfloat16``), + the results may be affected by floating-point rounding behavior. Some values in the sequence + might not be exactly representable in certain floating-point formats, which can lead to + repeated values or unexpected rounding. For precise sequences, it is recommended to use + integer dtypes instead of floating-point dtypes. + + Note that non-integer :attr:`step` is subject to floating point rounding errors when + comparing against :attr:`end`; to avoid inconsistency, we advise subtracting a small epsilon from :attr:`end` + in such cases. + + .. math:: + \text{out}_{{i+1}} = \text{out}_{i} + \text{step} + + Args: + start (Number, optional): the starting value for the set of points. Default: ``0``. + end (Number): the ending value for the set of points + step (Number, optional): the gap between each pair of adjacent points. Default: ``1``. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). If `dtype` is not given, infer the data type from the other input + arguments. If any of `start`, `end`, or `stop` are floating-point, the + `dtype` is inferred to be the default dtype, see + :meth:`~torch.get_default_dtype`. Otherwise, the `dtype` is inferred to + be `torch.int64`. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.arange(5) + tensor([ 0, 1, 2, 3, 4]) + >>> torch.arange(1, 4) + tensor([ 1, 2, 3]) + >>> torch.arange(1, 2.5, 0.5) + tensor([ 1.0000, 1.5000, 2.0000]) + """ + +@overload +def arange( + end: Number, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + arange(start=0, end, step=1, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a 1-D tensor of size :math:`\left\lceil \frac{\text{end} - \text{start}}{\text{step}} \right\rceil` + with values from the interval ``[start, end)`` taken with common difference + :attr:`step` beginning from `start`. + + Note: When using floating-point dtypes (especially reduced precision types like ``bfloat16``), + the results may be affected by floating-point rounding behavior. Some values in the sequence + might not be exactly representable in certain floating-point formats, which can lead to + repeated values or unexpected rounding. For precise sequences, it is recommended to use + integer dtypes instead of floating-point dtypes. + + Note that non-integer :attr:`step` is subject to floating point rounding errors when + comparing against :attr:`end`; to avoid inconsistency, we advise subtracting a small epsilon from :attr:`end` + in such cases. + + .. math:: + \text{out}_{{i+1}} = \text{out}_{i} + \text{step} + + Args: + start (Number, optional): the starting value for the set of points. Default: ``0``. + end (Number): the ending value for the set of points + step (Number, optional): the gap between each pair of adjacent points. Default: ``1``. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). If `dtype` is not given, infer the data type from the other input + arguments. If any of `start`, `end`, or `stop` are floating-point, the + `dtype` is inferred to be the default dtype, see + :meth:`~torch.get_default_dtype`. Otherwise, the `dtype` is inferred to + be `torch.int64`. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.arange(5) + tensor([ 0, 1, 2, 3, 4]) + >>> torch.arange(1, 4) + tensor([ 1, 2, 3]) + >>> torch.arange(1, 2.5, 0.5) + tensor([ 1.0000, 1.5000, 2.0000]) + """ + +@overload +def arange( + end: Number | _complex, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + arange(start=0, end, step=1, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a 1-D tensor of size :math:`\left\lceil \frac{\text{end} - \text{start}}{\text{step}} \right\rceil` + with values from the interval ``[start, end)`` taken with common difference + :attr:`step` beginning from `start`. + + Note: When using floating-point dtypes (especially reduced precision types like ``bfloat16``), + the results may be affected by floating-point rounding behavior. Some values in the sequence + might not be exactly representable in certain floating-point formats, which can lead to + repeated values or unexpected rounding. For precise sequences, it is recommended to use + integer dtypes instead of floating-point dtypes. + + Note that non-integer :attr:`step` is subject to floating point rounding errors when + comparing against :attr:`end`; to avoid inconsistency, we advise subtracting a small epsilon from :attr:`end` + in such cases. + + .. math:: + \text{out}_{{i+1}} = \text{out}_{i} + \text{step} + + Args: + start (Number, optional): the starting value for the set of points. Default: ``0``. + end (Number): the ending value for the set of points + step (Number, optional): the gap between each pair of adjacent points. Default: ``1``. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). If `dtype` is not given, infer the data type from the other input + arguments. If any of `start`, `end`, or `stop` are floating-point, the + `dtype` is inferred to be the default dtype, see + :meth:`~torch.get_default_dtype`. Otherwise, the `dtype` is inferred to + be `torch.int64`. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.arange(5) + tensor([ 0, 1, 2, 3, 4]) + >>> torch.arange(1, 4) + tensor([ 1, 2, 3]) + >>> torch.arange(1, 2.5, 0.5) + tensor([ 1.0000, 1.5000, 2.0000]) + """ + +@overload +def arange( + start: Number | _complex, + end: Number | _complex, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + arange(start=0, end, step=1, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a 1-D tensor of size :math:`\left\lceil \frac{\text{end} - \text{start}}{\text{step}} \right\rceil` + with values from the interval ``[start, end)`` taken with common difference + :attr:`step` beginning from `start`. + + Note: When using floating-point dtypes (especially reduced precision types like ``bfloat16``), + the results may be affected by floating-point rounding behavior. Some values in the sequence + might not be exactly representable in certain floating-point formats, which can lead to + repeated values or unexpected rounding. For precise sequences, it is recommended to use + integer dtypes instead of floating-point dtypes. + + Note that non-integer :attr:`step` is subject to floating point rounding errors when + comparing against :attr:`end`; to avoid inconsistency, we advise subtracting a small epsilon from :attr:`end` + in such cases. + + .. math:: + \text{out}_{{i+1}} = \text{out}_{i} + \text{step} + + Args: + start (Number, optional): the starting value for the set of points. Default: ``0``. + end (Number): the ending value for the set of points + step (Number, optional): the gap between each pair of adjacent points. Default: ``1``. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). If `dtype` is not given, infer the data type from the other input + arguments. If any of `start`, `end`, or `stop` are floating-point, the + `dtype` is inferred to be the default dtype, see + :meth:`~torch.get_default_dtype`. Otherwise, the `dtype` is inferred to + be `torch.int64`. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.arange(5) + tensor([ 0, 1, 2, 3, 4]) + >>> torch.arange(1, 4) + tensor([ 1, 2, 3]) + >>> torch.arange(1, 2.5, 0.5) + tensor([ 1.0000, 1.5000, 2.0000]) + """ + +@overload +def arange( + start: Number | _complex, + end: Number | _complex, + step: Number | _complex = 1, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + arange(start=0, end, step=1, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a 1-D tensor of size :math:`\left\lceil \frac{\text{end} - \text{start}}{\text{step}} \right\rceil` + with values from the interval ``[start, end)`` taken with common difference + :attr:`step` beginning from `start`. + + Note: When using floating-point dtypes (especially reduced precision types like ``bfloat16``), + the results may be affected by floating-point rounding behavior. Some values in the sequence + might not be exactly representable in certain floating-point formats, which can lead to + repeated values or unexpected rounding. For precise sequences, it is recommended to use + integer dtypes instead of floating-point dtypes. + + Note that non-integer :attr:`step` is subject to floating point rounding errors when + comparing against :attr:`end`; to avoid inconsistency, we advise subtracting a small epsilon from :attr:`end` + in such cases. + + .. math:: + \text{out}_{{i+1}} = \text{out}_{i} + \text{step} + + Args: + start (Number, optional): the starting value for the set of points. Default: ``0``. + end (Number): the ending value for the set of points + step (Number, optional): the gap between each pair of adjacent points. Default: ``1``. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). If `dtype` is not given, infer the data type from the other input + arguments. If any of `start`, `end`, or `stop` are floating-point, the + `dtype` is inferred to be the default dtype, see + :meth:`~torch.get_default_dtype`. Otherwise, the `dtype` is inferred to + be `torch.int64`. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.arange(5) + tensor([ 0, 1, 2, 3, 4]) + >>> torch.arange(1, 4) + tensor([ 1, 2, 3]) + >>> torch.arange(1, 2.5, 0.5) + tensor([ 1.0000, 1.5000, 2.0000]) + """ + +def arccos(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + arccos(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Alias for :func:`torch.acos`. + """ + +def arccos_(input: Tensor) -> Tensor: ... +def arccosh(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + arccosh(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Alias for :func:`torch.acosh`. + """ + +def arccosh_(input: Tensor) -> Tensor: ... +def arcsin(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + arcsin(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Alias for :func:`torch.asin`. + """ + +def arcsin_(input: Tensor) -> Tensor: ... +def arcsinh(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + arcsinh(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Alias for :func:`torch.asinh`. + """ + +def arcsinh_(input: Tensor) -> Tensor: ... +def arctan(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + arctan(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Alias for :func:`torch.atan`. + """ + +def arctan2( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + arctan2(input: Tensor, other: Tensor, *, out: Optional[Tensor]) -> Tensor + Alias for :func:`torch.atan2`. + """ + +def arctan_(input: Tensor) -> Tensor: ... +def arctanh(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + arctanh(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Alias for :func:`torch.atanh`. + """ + +def arctanh_(input: Tensor) -> Tensor: ... +def argmax( + input: Tensor, + dim: _int | None = None, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + argmax(input) -> LongTensor + + Returns the indices of the maximum value of all elements in the :attr:`input` tensor. + + This is the second value returned by :meth:`torch.max`. See its + documentation for the exact semantics of this method. + + .. note:: If there are multiple maximal values then the indices of the first maximal value are returned. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 1.3398, 0.2663, -0.2686, 0.2450], + [-0.7401, -0.8805, -0.3402, -1.1936], + [ 0.4907, -1.3948, -1.0691, -0.3132], + [-1.6092, 0.5419, -0.2993, 0.3195]]) + >>> torch.argmax(a) + tensor(0) + + .. function:: argmax(input, dim, keepdim=False) -> LongTensor + :noindex: + + Returns the indices of the maximum values of a tensor across a dimension. + + This is the second value returned by :meth:`torch.max`. See its + documentation for the exact semantics of this method. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, the argmax of the flattened input is returned. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 1.3398, 0.2663, -0.2686, 0.2450], + [-0.7401, -0.8805, -0.3402, -1.1936], + [ 0.4907, -1.3948, -1.0691, -0.3132], + [-1.6092, 0.5419, -0.2993, 0.3195]]) + >>> torch.argmax(a, dim=1) + tensor([ 0, 2, 0, 1]) + """ + +def argmin( + input: Tensor, + dim: _int | None = None, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + argmin(input, dim=None, keepdim=False) -> LongTensor + + Returns the indices of the minimum value(s) of the flattened tensor or along a dimension + + This is the second value returned by :meth:`torch.min`. See its + documentation for the exact semantics of this method. + + .. note:: If there are multiple minimal values then the indices of the first minimal value are returned. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, the argmin of the flattened input is returned. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.1139, 0.2254, -0.1381, 0.3687], + [ 1.0100, -1.1975, -0.0102, -0.4732], + [-0.9240, 0.1207, -0.7506, -1.0213], + [ 1.7809, -1.2960, 0.9384, 0.1438]]) + >>> torch.argmin(a) + tensor(13) + >>> torch.argmin(a, dim=1) + tensor([ 2, 1, 3, 1]) + >>> torch.argmin(a, dim=1, keepdim=True) + tensor([[2], + [1], + [3], + [1]]) + """ + +@overload +def argsort( + input: Tensor, + *, + stable: _bool, + dim: _int = -1, + descending: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + argsort(input, dim=-1, descending=False, *, stable=False) -> Tensor + + Returns the indices that sort a tensor along a given dimension in ascending + order by value. + + This is the second value returned by :meth:`torch.sort`. See its documentation + for the exact semantics of this method. + + If :attr:`stable` is ``True`` then the sorting routine becomes stable, preserving + the order of equivalent elements. If ``False``, the relative order of values + which compare equal is not guaranteed. ``True`` is slower. + + Args: + input (Tensor): the input tensor. + dim (int, optional): the dimension to sort along + descending (bool, optional): controls the sorting order (ascending or descending) + + Keyword args: + stable (bool, optional): controls the relative order of equivalent elements + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.0785, 1.5267, -0.8521, 0.4065], + [ 0.1598, 0.0788, -0.0745, -1.2700], + [ 1.2208, 1.0722, -0.7064, 1.2564], + [ 0.0669, -0.2318, -0.8229, -0.9280]]) + + + >>> torch.argsort(a, dim=1) + tensor([[2, 0, 3, 1], + [3, 2, 1, 0], + [2, 1, 0, 3], + [3, 2, 1, 0]]) + """ + +@overload +def argsort( + input: Tensor, + dim: _int = -1, + descending: _bool = False, +) -> Tensor: + r""" + argsort(input, dim=-1, descending=False, *, stable=False) -> Tensor + + Returns the indices that sort a tensor along a given dimension in ascending + order by value. + + This is the second value returned by :meth:`torch.sort`. See its documentation + for the exact semantics of this method. + + If :attr:`stable` is ``True`` then the sorting routine becomes stable, preserving + the order of equivalent elements. If ``False``, the relative order of values + which compare equal is not guaranteed. ``True`` is slower. + + Args: + input (Tensor): the input tensor. + dim (int, optional): the dimension to sort along + descending (bool, optional): controls the sorting order (ascending or descending) + + Keyword args: + stable (bool, optional): controls the relative order of equivalent elements + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.0785, 1.5267, -0.8521, 0.4065], + [ 0.1598, 0.0788, -0.0745, -1.2700], + [ 1.2208, 1.0722, -0.7064, 1.2564], + [ 0.0669, -0.2318, -0.8229, -0.9280]]) + + + >>> torch.argsort(a, dim=1) + tensor([[2, 0, 3, 1], + [3, 2, 1, 0], + [2, 1, 0, 3], + [3, 2, 1, 0]]) + """ + +@overload +def argsort( + input: Tensor, + dim: str | EllipsisType | None, + descending: _bool = False, +) -> Tensor: + r""" + argsort(input, dim=-1, descending=False, *, stable=False) -> Tensor + + Returns the indices that sort a tensor along a given dimension in ascending + order by value. + + This is the second value returned by :meth:`torch.sort`. See its documentation + for the exact semantics of this method. + + If :attr:`stable` is ``True`` then the sorting routine becomes stable, preserving + the order of equivalent elements. If ``False``, the relative order of values + which compare equal is not guaranteed. ``True`` is slower. + + Args: + input (Tensor): the input tensor. + dim (int, optional): the dimension to sort along + descending (bool, optional): controls the sorting order (ascending or descending) + + Keyword args: + stable (bool, optional): controls the relative order of equivalent elements + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.0785, 1.5267, -0.8521, 0.4065], + [ 0.1598, 0.0788, -0.0745, -1.2700], + [ 1.2208, 1.0722, -0.7064, 1.2564], + [ 0.0669, -0.2318, -0.8229, -0.9280]]) + + + >>> torch.argsort(a, dim=1) + tensor([[2, 0, 3, 1], + [3, 2, 1, 0], + [2, 1, 0, 3], + [3, 2, 1, 0]]) + """ + +def argwhere(input: Tensor) -> Tensor: + r""" + argwhere(input) -> Tensor + + Returns a tensor containing the indices of all non-zero elements of + :attr:`input`. Each row in the result contains the indices of a non-zero + element in :attr:`input`. The result is sorted lexicographically, with + the last index changing the fastest (C-style). + + If :attr:`input` has :math:`n` dimensions, then the resulting indices tensor + :attr:`out` is of size :math:`(z \times n)`, where :math:`z` is the total number of + non-zero elements in the :attr:`input` tensor. + + .. note:: + This function is similar to NumPy's `argwhere`. + + When :attr:`input` is on CUDA, this function causes host-device synchronization. + + Args: + {input} + + Example:: + + >>> t = torch.tensor([1, 0, 1]) + >>> torch.argwhere(t) + tensor([[0], + [2]]) + >>> t = torch.tensor([[1, 0, 1], [0, 1, 1]]) + >>> torch.argwhere(t) + tensor([[0, 0], + [0, 2], + [1, 1], + [1, 2]]) + """ + +def as_strided( + input: Tensor, + size: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + storage_offset: _int | SymInt | None = None, +) -> Tensor: + r""" + as_strided(input, size, stride, storage_offset=None) -> Tensor + + Create a view of an existing `torch.Tensor` :attr:`input` with specified + :attr:`size`, :attr:`stride` and :attr:`storage_offset`. + + .. warning:: + Prefer using other view functions, like :meth:`torch.Tensor.view` or + :meth:`torch.Tensor.expand`, to setting a view's strides manually with + `as_strided`, as this function will throw an error on non-standard Pytorch + backends (that do not have a concept of stride) and the result will depend + on the current layout in memory. The constructed view must only refer to + elements within the Tensor's storage or a runtime error will be thrown. + If the generated view is "overlapped" (with multiple indices referring to + the same element in memory), the behavior of inplace operations on this view + is undefined (and might not throw runtime errors). + + Args: + input (Tensor): the input tensor. + size (tuple or ints): the shape of the output tensor + stride (tuple or ints): the stride of the output tensor + storage_offset (int, optional): the offset in the underlying storage of the output tensor. + If ``None``, the storage_offset of the output tensor will match the input tensor. + + Example:: + + >>> x = torch.randn(3, 3) + >>> x + tensor([[ 0.9039, 0.6291, 1.0795], + [ 0.1586, 2.1939, -0.4900], + [-0.1909, -0.7503, 1.9355]]) + >>> t = torch.as_strided(x, (2, 2), (1, 2)) + >>> t + tensor([[0.9039, 1.0795], + [0.6291, 0.1586]]) + >>> t = torch.as_strided(x, (2, 2), (1, 2), 1) + tensor([[0.6291, 0.1586], + [1.0795, 2.1939]]) + """ + +def as_strided_( + input: Tensor, + size: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + storage_offset: _int | SymInt | None = None, +) -> Tensor: ... +def as_strided_copy( + input: Tensor, + size: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + storage_offset: _int | SymInt | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.as_strided`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def as_strided_scatter( + input: Tensor, + src: Tensor, + size: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + storage_offset: _int | SymInt | None = None, +) -> Tensor: + r""" + as_strided_scatter(input, src, size, stride, storage_offset=None) -> Tensor + + Embeds the values of the :attr:`src` tensor into :attr:`input` along + the elements corresponding to the result of calling + input.as_strided(size, stride, storage_offset). + + This function returns a tensor with fresh storage; it does not + return a view. + + Args: + input (Tensor): the input tensor. + size (tuple or ints): the shape of the output tensor + stride (tuple or ints): the stride of the output tensor + storage_offset (int, optional): the offset in the underlying storage of the output tensor + + .. note:: + + :attr:`src` must be of the proper size in order to be embedded + into :attr:`input`. Specifically, it should have the same shape as + `torch.as_strided(input, size, stride, storage_offset)` + + Example:: + + >>> a = torch.arange(4).reshape(2, 2) + 1 + >>> a + tensor([[1, 2], + [3, 4]]) + >>> b = torch.zeros(3, 3) + >>> b + tensor([[0., 0., 0.], + [0., 0., 0.], + [0., 0., 0.]]) + >>> torch.as_strided_scatter(b, a, (2, 2), (1, 2)) + tensor([[1., 3., 2.], + [4., 0., 0.], + [0., 0., 0.]]) + """ + +def as_tensor( + data: Any, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, +) -> Tensor: + r""" + as_tensor(data: Any, *, dtype: Optional[dtype] = None, device: Optional[DeviceLikeType]) -> Tensor + + Converts :attr:`data` into a tensor, sharing data and preserving autograd + history if possible. + + If :attr:`data` is already a tensor with the requested dtype and device + then :attr:`data` itself is returned, but if :attr:`data` is a + tensor with a different dtype or device then it's copied as if using + `data.to(dtype=dtype, device=device)`. + + If :attr:`data` is a NumPy array (an ndarray) with the same dtype and device then a + tensor is constructed using :func:`torch.from_numpy`. + + If :attr:`data` is a CuPy array, the returned tensor will be located on the same device as the CuPy array unless + specifically overwritten by :attr:`device` or a default device. The device of the CuPy array is inferred from the + pointer of the array using `cudaPointerGetAttributes` unless :attr:`device` is provided with an explicit device index. + + .. seealso:: + + :func:`torch.tensor` never shares its data and creates a new "leaf tensor" (see :doc:`/notes/autograd`). + + + Args: + data (array_like): Initial data for the tensor. Can be a list, tuple, + NumPy ``ndarray``, scalar, and other types. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, infers data type from :attr:`data`. + device (:class:`torch.device`, optional): the device of the constructed tensor. If None and data is a tensor + then the device of data is used. If None and data is not a tensor then + the result tensor is constructed on the current device. + + + Example:: + + >>> a = numpy.array([1, 2, 3]) + >>> t = torch.as_tensor(a) + >>> t + tensor([ 1, 2, 3]) + >>> t[0] = -1 + >>> a + array([-1, 2, 3]) + + >>> a = numpy.array([1, 2, 3]) + >>> t = torch.as_tensor(a, device=torch.device('cuda')) + >>> t + tensor([ 1, 2, 3]) + >>> t[0] = -1 + >>> a + array([1, 2, 3]) + """ + +def asarray( + obj: Any, + *, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + copy: _bool | None = None, + requires_grad: _bool | None = None, +) -> Tensor: + r""" + asarray(obj: Any, *, dtype: Optional[dtype], device: Optional[DeviceLikeType], copy: Optional[bool] = None, requires_grad: Optional[bool] = None) -> Tensor # noqa: B950 + + Converts :attr:`obj` to a tensor. + + :attr:`obj` can be one of: + + 1. a tensor + 2. a NumPy array or a NumPy scalar + 3. a DLPack capsule + 4. an object that implements Python's buffer protocol + 5. a scalar + 6. a sequence of scalars + + When :attr:`obj` is a tensor, NumPy array, or DLPack capsule the returned tensor will, + by default, have the same requires_grad as :attr:`obj` (defaulting to False), have the + same datatype, be on the same device, and share memory with it. These properties can be + controlled with the :attr:`dtype`, :attr:`device`, :attr:`copy`, and + :attr:`requires_grad` keyword arguments. If the returned tensor is of a different + datatype, on a different device, or a copy is requested then it will not share its + memory with :attr:`obj`. If :attr:`requires_grad` is ``True`` (or ``None``, and + :attr:`obj` was a tensor with requires_grad set), then the returned tensor will require + a gradient, and if :attr:`obj` is also a tensor with an autograd history then the + returned tensor will have the same history. + + When :attr:`obj` is not a tensor, NumPy array, or DLPack capsule but implements Python's + buffer protocol then the buffer is interpreted as an array of bytes grouped according to + the size of the datatype passed to the :attr:`dtype` keyword argument. (If no datatype is + passed then the default floating point datatype is used, instead.) The returned tensor + will have the specified datatype (or default floating point datatype if none is specified) + and, by default, be on the CPU device and share memory with the buffer. + + When :attr:`obj` is a NumPy scalar, the returned tensor will be a 0-dimensional tensor on + the CPU and that doesn't share its memory (i.e. ``copy=True``). By default datatype will + be the PyTorch datatype corresponding to the NumPy's scalar's datatype. + + When :attr:`obj` is none of the above but a scalar, or a sequence of scalars then the + returned tensor will, by default, infer its datatype from the scalar values, be on the + current default device, and not share its memory. + + .. seealso:: + + :func:`torch.tensor` creates a tensor that always copies the data from the input object. + :func:`torch.from_numpy` creates a tensor that always shares memory from NumPy arrays. + :func:`torch.frombuffer` creates a tensor that always shares memory from objects that + implement the buffer protocol. + :func:`torch.from_dlpack` creates a tensor that always shares memory from + DLPack capsules. + + Args: + obj (object): a tensor, NumPy array, DLPack Capsule, object that implements Python's + buffer protocol, scalar, or sequence of scalars. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the datatype of the returned tensor. + Default: ``None``, which causes the datatype of the returned tensor to be + inferred from :attr:`obj`. + copy (bool, optional): controls whether the returned tensor shares memory with :attr:`obj`. + Default: ``None``, which causes the returned tensor to share memory with :attr:`obj` + whenever possible. If ``True`` then the returned tensor does not share its memory. + If ``False`` then the returned tensor shares its memory with :attr:`obj` and an + error is thrown if it cannot. + device (:class:`torch.device`, optional): the device of the returned tensor. + Default: ``None``, which causes the device of :attr:`obj` to be used. Or, if + :attr:`obj` is a Python sequence, the current default device will be used. + requires_grad (bool, optional): whether the returned tensor requires grad. + Default: ``None``, which causes requires_grad for the returned tensor to be + inferred from :attr:`obj`. If ``True``, then the returned tensor will require + a gradient, and if :attr:`obj` is also a tensor with an autograd history then + the returned tensor will have the same history. + + Example:: + + >>> a = torch.tensor([1, 2, 3]) + >>> # Shares memory with tensor 'a' + >>> b = torch.asarray(a) + >>> a.data_ptr() == b.data_ptr() + True + >>> # Forces memory copy + >>> c = torch.asarray(a, copy=True) + >>> a.data_ptr() == c.data_ptr() + False + + >>> a = torch.tensor([1., 2., 3.], requires_grad=True) + >>> b = a + 2 + >>> b + tensor([3., 4., 5.], grad_fn=) + >>> # Shares memory with tensor 'b', with no grad + >>> c = torch.asarray(b, requires_grad=False) + >>> c + tensor([3., 4., 5.]) + >>> # Shares memory with tensor 'b', retaining autograd history + >>> d = torch.asarray(b, requires_grad=True) + >>> d + tensor([3., 4., 5.], grad_fn=) + >>> # Shares memory with tensor 'b', retaining autograd history + >>> e = torch.asarray(b) + >>> e + tensor([3., 4., 5.], grad_fn=) + + >>> array = numpy.array([1, 2, 3]) + >>> # Shares memory with array 'array' + >>> t1 = torch.asarray(array) + >>> array.__array_interface__['data'][0] == t1.data_ptr() + True + >>> # Copies memory due to dtype mismatch + >>> t2 = torch.asarray(array, dtype=torch.float32) + >>> array.__array_interface__['data'][0] == t2.data_ptr() + False + + >>> scalar = numpy.float64(0.5) + >>> torch.asarray(scalar) + tensor(0.5000, dtype=torch.float64) + """ + +def asin(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + asin(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Returns a new tensor with the arcsine of the elements (in radians) in the :attr:`input` tensor. + + .. math:: + \text{out}_{i} = \sin^{-1}(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-0.5962, 1.4985, -0.4396, 1.4525]) + >>> torch.asin(a) + tensor([-0.6387, nan, -0.4552, nan]) + """ + +def asin_(input: Tensor) -> Tensor: ... +def asinh(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + asinh(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Returns a new tensor with the inverse hyperbolic sine of the elements of :attr:`input`. + + .. math:: + \text{out}_{i} = \sinh^{-1}(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword arguments: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.1606, -1.4267, -1.0899, -1.0250 ]) + >>> torch.asinh(a) + tensor([ 0.1599, -1.1534, -0.9435, -0.8990 ]) + """ + +def asinh_(input: Tensor) -> Tensor: ... +def atan(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + atan(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Returns a new tensor with the arctangent of the elements (in radians) in the :attr:`input` tensor. + + .. math:: + \text{out}_{i} = \tan^{-1}(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.2341, 0.2539, -0.6256, -0.6448]) + >>> torch.atan(a) + tensor([ 0.2299, 0.2487, -0.5591, -0.5727]) + """ + +def atan2( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + atan2(input: Tensor, other: Tensor, *, out: Optional[Tensor]) -> Tensor + + Element-wise arctangent of :math:`\text{input}_{i} / \text{other}_{i}` + with consideration of the quadrant. Returns a new tensor with the signed angles + in radians between vector :math:`(\text{other}_{i}, \text{input}_{i})` + and vector :math:`(1, 0)`. (Note that :math:`\text{other}_{i}`, the second + parameter, is the x-coordinate, while :math:`\text{input}_{i}`, the first + parameter, is the y-coordinate.) + + The shapes of ``input`` and ``other`` must be + :ref:`broadcastable `. + + Args: + input (Tensor): the first input tensor + other (Tensor): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.9041, 0.0196, -0.3108, -2.4423]) + >>> torch.atan2(a, torch.randn(4)) + tensor([ 0.9833, 0.0811, -1.9743, -1.4151]) + """ + +def atan_(input: Tensor) -> Tensor: ... +def atanh(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + atanh(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Returns a new tensor with the inverse hyperbolic tangent of the elements of :attr:`input`. + + Note: + The domain of the inverse hyperbolic tangent is `(-1, 1)` and values outside this range + will be mapped to ``NaN``, except for the values `1` and `-1` for which the output is + mapped to `+/-INF` respectively. + + .. math:: + \text{out}_{i} = \tanh^{-1}(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword arguments: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4).uniform_(-1, 1) + >>> a + tensor([ -0.9385, 0.2968, -0.8591, -0.1871 ]) + >>> torch.atanh(a) + tensor([ -1.7253, 0.3060, -1.2899, -0.1893 ]) + """ + +def atanh_(input: Tensor) -> Tensor: ... +def avg_pool1d( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size = (), + padding: _int | _size = 0, + ceil_mode: _bool = False, + count_include_pad: _bool = True, +) -> Tensor: ... +@overload +def baddbmm( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + batch1: Tensor, + batch2: Tensor, +) -> Tensor: + r""" + baddbmm(input, batch1, batch2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices in :attr:`batch1` + and :attr:`batch2`. + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the same + number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a + :math:`(b \times n \times p)` tensor and :attr:`out` will be a + :math:`(b \times n \times p)` tensor. Both :attr:`alpha` and :attr:`beta` mean the + same as the scaling factors used in :meth:`torch.addbmm`. + + .. math:: + \text{out}_i = \beta\ \text{input}_i + \alpha\ (\text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): the tensor to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{batch1} \mathbin{@} \text{batch2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(10, 3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.baddbmm(M, batch1, batch2).size() + torch.Size([10, 3, 5]) + """ + +@overload +def baddbmm( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + batch1: Tensor, + batch2: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + baddbmm(input, batch1, batch2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices in :attr:`batch1` + and :attr:`batch2`. + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the same + number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a + :math:`(b \times n \times p)` tensor and :attr:`out` will be a + :math:`(b \times n \times p)` tensor. Both :attr:`alpha` and :attr:`beta` mean the + same as the scaling factors used in :meth:`torch.addbmm`. + + .. math:: + \text{out}_i = \beta\ \text{input}_i + \alpha\ (\text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): the tensor to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{batch1} \mathbin{@} \text{batch2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(10, 3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.baddbmm(M, batch1, batch2).size() + torch.Size([10, 3, 5]) + """ + +@overload +def baddbmm( + input: Tensor, + batch1: Tensor, + batch2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + baddbmm(input, batch1, batch2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices in :attr:`batch1` + and :attr:`batch2`. + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the same + number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a + :math:`(b \times n \times p)` tensor and :attr:`out` will be a + :math:`(b \times n \times p)` tensor. Both :attr:`alpha` and :attr:`beta` mean the + same as the scaling factors used in :meth:`torch.addbmm`. + + .. math:: + \text{out}_i = \beta\ \text{input}_i + \alpha\ (\text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): the tensor to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{batch1} \mathbin{@} \text{batch2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(10, 3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.baddbmm(M, batch1, batch2).size() + torch.Size([10, 3, 5]) + """ + +@overload +def baddbmm( + input: Tensor, + batch1: Tensor, + batch2: Tensor, + out_dtype: _dtype, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + baddbmm(input, batch1, batch2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices in :attr:`batch1` + and :attr:`batch2`. + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the same + number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a + :math:`(b \times n \times p)` tensor and :attr:`out` will be a + :math:`(b \times n \times p)` tensor. Both :attr:`alpha` and :attr:`beta` mean the + same as the scaling factors used in :meth:`torch.addbmm`. + + .. math:: + \text{out}_i = \beta\ \text{input}_i + \alpha\ (\text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): the tensor to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{batch1} \mathbin{@} \text{batch2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(10, 3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.baddbmm(M, batch1, batch2).size() + torch.Size([10, 3, 5]) + """ + +@overload +def baddbmm( + beta: Number | _complex, + self: Tensor, + batch1: Tensor, + batch2: Tensor, +) -> Tensor: + r""" + baddbmm(input, batch1, batch2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices in :attr:`batch1` + and :attr:`batch2`. + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the same + number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a + :math:`(b \times n \times p)` tensor and :attr:`out` will be a + :math:`(b \times n \times p)` tensor. Both :attr:`alpha` and :attr:`beta` mean the + same as the scaling factors used in :meth:`torch.addbmm`. + + .. math:: + \text{out}_i = \beta\ \text{input}_i + \alpha\ (\text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): the tensor to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{batch1} \mathbin{@} \text{batch2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(10, 3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.baddbmm(M, batch1, batch2).size() + torch.Size([10, 3, 5]) + """ + +@overload +def baddbmm( + beta: Number | _complex, + self: Tensor, + batch1: Tensor, + batch2: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + baddbmm(input, batch1, batch2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices in :attr:`batch1` + and :attr:`batch2`. + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the same + number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a + :math:`(b \times n \times p)` tensor and :attr:`out` will be a + :math:`(b \times n \times p)` tensor. Both :attr:`alpha` and :attr:`beta` mean the + same as the scaling factors used in :meth:`torch.addbmm`. + + .. math:: + \text{out}_i = \beta\ \text{input}_i + \alpha\ (\text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): the tensor to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{batch1} \mathbin{@} \text{batch2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(10, 3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.baddbmm(M, batch1, batch2).size() + torch.Size([10, 3, 5]) + """ + +@overload +def bartlett_window( + window_length: _int, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + bartlett_window(window_length, periodic=True, *, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Bartlett window function. + + .. math:: + w[n] = 1 - \left| \frac{2n}{N-1} - 1 \right| = \begin{cases} + \frac{2n}{N - 1} & \text{if } 0 \leq n \leq \frac{N - 1}{2} \\ + 2 - \frac{2n}{N - 1} & \text{if } \frac{N - 1}{2} < n < N \\ + \end{cases}, + + where :math:`N` is the full window size. + + The input :attr:`window_length` is a positive integer controlling the + returned window size. :attr:`periodic` flag determines whether the returned + window trims off the last duplicate value from the symmetric window and is + ready to be used as a periodic window with functions like + :meth:`torch.stft`. Therefore, if :attr:`periodic` is true, the :math:`N` in + above formula is in fact :math:`\text{window\_length} + 1`. Also, we always have + ``torch.bartlett_window(L, periodic=True)`` equal to + ``torch.bartlett_window(L + 1, periodic=False)[:-1])``. + + .. note:: + If :attr:`window_length` :math:`=1`, the returned window contains a single value 1. + + Arguments: + window_length (int): the size of returned window + periodic (bool, optional): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window + """ + +@overload +def bartlett_window( + window_length: _int, + periodic: _bool, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + bartlett_window(window_length, periodic=True, *, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Bartlett window function. + + .. math:: + w[n] = 1 - \left| \frac{2n}{N-1} - 1 \right| = \begin{cases} + \frac{2n}{N - 1} & \text{if } 0 \leq n \leq \frac{N - 1}{2} \\ + 2 - \frac{2n}{N - 1} & \text{if } \frac{N - 1}{2} < n < N \\ + \end{cases}, + + where :math:`N` is the full window size. + + The input :attr:`window_length` is a positive integer controlling the + returned window size. :attr:`periodic` flag determines whether the returned + window trims off the last duplicate value from the symmetric window and is + ready to be used as a periodic window with functions like + :meth:`torch.stft`. Therefore, if :attr:`periodic` is true, the :math:`N` in + above formula is in fact :math:`\text{window\_length} + 1`. Also, we always have + ``torch.bartlett_window(L, periodic=True)`` equal to + ``torch.bartlett_window(L + 1, periodic=False)[:-1])``. + + .. note:: + If :attr:`window_length` :math:`=1`, the returned window contains a single value 1. + + Arguments: + window_length (int): the size of returned window + periodic (bool, optional): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window + """ + +def batch_norm( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor | None, + running_var: Tensor | None, + training: _bool, + momentum: _float, + eps: _float, + cudnn_enabled: _bool, +) -> Tensor: ... +def batch_norm_backward_elemt( + grad_out: Tensor, + input: Tensor, + mean: Tensor, + invstd: Tensor, + weight: Tensor | None, + sum_dy: Tensor, + sum_dy_xmu: Tensor, + count: Tensor, +) -> Tensor: ... +def batch_norm_backward_reduce( + grad_out: Tensor, + input: Tensor, + mean: Tensor, + invstd: Tensor, + weight: Tensor | None, + input_g: _bool, + weight_g: _bool, + bias_g: _bool, +) -> tuple[Tensor, Tensor, Tensor, Tensor]: ... +def batch_norm_elemt( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + mean: Tensor, + invstd: Tensor, + eps: _float, + *, + out: Tensor | None = None, +) -> Tensor: ... +def batch_norm_gather_stats( + input: Tensor, + mean: Tensor, + invstd: Tensor, + running_mean: Tensor | None, + running_var: Tensor | None, + momentum: _float, + eps: _float, + count: _int, +) -> tuple[Tensor, Tensor]: ... +def batch_norm_gather_stats_with_counts( + input: Tensor, + mean: Tensor, + invstd: Tensor, + running_mean: Tensor | None, + running_var: Tensor | None, + momentum: _float, + eps: _float, + counts: Tensor, +) -> tuple[Tensor, Tensor]: ... +def batch_norm_stats(input: Tensor, eps: _float) -> tuple[Tensor, Tensor]: ... +def batch_norm_update_stats( + input: Tensor, + running_mean: Tensor | None, + running_var: Tensor | None, + momentum: _float, +) -> tuple[Tensor, Tensor]: ... +@overload +def bernoulli( + input: Tensor, + *, + generator: Generator | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + bernoulli(input: Tensor, *, generator: Optional[Generator], out: Optional[Tensor]) -> Tensor + + Draws binary random numbers (0 or 1) from a Bernoulli distribution. + + The :attr:`input` tensor should be a tensor containing probabilities + to be used for drawing the binary random number. + Hence, all values in :attr:`input` have to be in the range: + :math:`0 \leq \text{input}_i \leq 1`. + + The :math:`\text{i}^{th}` element of the output tensor will draw a + value :math:`1` according to the :math:`\text{i}^{th}` probability value given + in :attr:`input`. + + .. math:: + \text{out}_{i} \sim \mathrm{Bernoulli}(p = \text{input}_{i}) + + The returned :attr:`out` tensor only has values 0 or 1 and is of the same + shape as :attr:`input`. + + :attr:`out` can have integral ``dtype``, but :attr:`input` must have floating + point ``dtype``. + + Args: + input (Tensor): the input tensor of probability values for the Bernoulli distribution + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.empty(3, 3).uniform_(0, 1) # generate a uniform random matrix with range [0, 1] + >>> a + tensor([[ 0.1737, 0.0950, 0.3609], + [ 0.7148, 0.0289, 0.2676], + [ 0.9456, 0.8937, 0.7202]]) + >>> torch.bernoulli(a) + tensor([[ 1., 0., 0.], + [ 0., 0., 0.], + [ 1., 1., 1.]]) + + >>> a = torch.ones(3, 3) # probability of drawing "1" is 1 + >>> torch.bernoulli(a) + tensor([[ 1., 1., 1.], + [ 1., 1., 1.], + [ 1., 1., 1.]]) + >>> a = torch.zeros(3, 3) # probability of drawing "1" is 0 + >>> torch.bernoulli(a) + tensor([[ 0., 0., 0.], + [ 0., 0., 0.], + [ 0., 0., 0.]]) + """ + +@overload +def bernoulli( + input: Tensor, + p: _float, + *, + generator: Generator | None = None, +) -> Tensor: + r""" + bernoulli(input: Tensor, *, generator: Optional[Generator], out: Optional[Tensor]) -> Tensor + + Draws binary random numbers (0 or 1) from a Bernoulli distribution. + + The :attr:`input` tensor should be a tensor containing probabilities + to be used for drawing the binary random number. + Hence, all values in :attr:`input` have to be in the range: + :math:`0 \leq \text{input}_i \leq 1`. + + The :math:`\text{i}^{th}` element of the output tensor will draw a + value :math:`1` according to the :math:`\text{i}^{th}` probability value given + in :attr:`input`. + + .. math:: + \text{out}_{i} \sim \mathrm{Bernoulli}(p = \text{input}_{i}) + + The returned :attr:`out` tensor only has values 0 or 1 and is of the same + shape as :attr:`input`. + + :attr:`out` can have integral ``dtype``, but :attr:`input` must have floating + point ``dtype``. + + Args: + input (Tensor): the input tensor of probability values for the Bernoulli distribution + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.empty(3, 3).uniform_(0, 1) # generate a uniform random matrix with range [0, 1] + >>> a + tensor([[ 0.1737, 0.0950, 0.3609], + [ 0.7148, 0.0289, 0.2676], + [ 0.9456, 0.8937, 0.7202]]) + >>> torch.bernoulli(a) + tensor([[ 1., 0., 0.], + [ 0., 0., 0.], + [ 1., 1., 1.]]) + + >>> a = torch.ones(3, 3) # probability of drawing "1" is 1 + >>> torch.bernoulli(a) + tensor([[ 1., 1., 1.], + [ 1., 1., 1.], + [ 1., 1., 1.]]) + >>> a = torch.zeros(3, 3) # probability of drawing "1" is 0 + >>> torch.bernoulli(a) + tensor([[ 0., 0., 0.], + [ 0., 0., 0.], + [ 0., 0., 0.]]) + """ + +def bilinear( + input1: Tensor, + input2: Tensor, + weight: Tensor, + bias: Tensor | None = None, +) -> Tensor: ... +def binary_cross_entropy_with_logits( + input: Tensor, + target: Tensor, + weight: Tensor | None = None, + pos_weight: Tensor | None = None, + reduction: _int = 1, +) -> Tensor: ... +def bincount( + input: Tensor, + weights: Tensor | None = None, + minlength: _int | SymInt = 0, +) -> Tensor: + r""" + bincount(input, weights=None, minlength=0) -> Tensor + + Count the frequency of each value in an array of non-negative ints. + + The number of bins (size 1) is one larger than the largest value in + :attr:`input` unless :attr:`input` is empty, in which case the result is a + tensor of size 0. If :attr:`minlength` is specified, the number of bins is at least + :attr:`minlength` and if :attr:`input` is empty, then the result is tensor of size + :attr:`minlength` filled with zeros. If ``n`` is the value at position ``i``, + ``out[n] += weights[i]`` if :attr:`weights` is specified else + ``out[n] += 1``. + + Note: + This operation may produce nondeterministic gradients when given tensors on a CUDA device. See :doc:`/notes/randomness` for more information. + + Arguments: + input (Tensor): 1-d int tensor + weights (Tensor): optional, weight for each value in the input tensor. + Should be of same size as input tensor. + minlength (int): optional, minimum number of bins. Should be non-negative. + + Returns: + output (Tensor): a tensor of shape ``Size([max(input) + 1])`` if + :attr:`input` is non-empty, else ``Size(0)`` + + Example:: + + >>> input = torch.randint(0, 8, (5,), dtype=torch.int64) + >>> weights = torch.linspace(0, 1, steps=5) + >>> input, weights + (tensor([4, 3, 6, 3, 4]), + tensor([ 0.0000, 0.2500, 0.5000, 0.7500, 1.0000]) + + >>> torch.bincount(input) + tensor([0, 0, 0, 2, 2, 0, 1]) + + >>> input.bincount(weights) + tensor([0.0000, 0.0000, 0.0000, 1.0000, 1.0000, 0.0000, 0.5000]) + """ + +def binomial( + count: Tensor, + prob: Tensor, + generator: Generator | None = None, +) -> Tensor: ... +@overload +def bitwise_and( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bitwise_and(input, other, *, out=None) -> Tensor + + Computes the bitwise AND of :attr:`input` and :attr:`other`. The input tensor must be of + integral or Boolean types. For bool tensors, it computes the logical AND. + + Args: + input: the first input tensor + other: the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_and(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([1, 0, 3], dtype=torch.int8) + >>> torch.bitwise_and(torch.tensor([True, True, False]), torch.tensor([False, True, False])) + tensor([ False, True, False]) + """ + +@overload +def bitwise_and(self: Number | _complex, other: Tensor) -> Tensor: + r""" + bitwise_and(input, other, *, out=None) -> Tensor + + Computes the bitwise AND of :attr:`input` and :attr:`other`. The input tensor must be of + integral or Boolean types. For bool tensors, it computes the logical AND. + + Args: + input: the first input tensor + other: the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_and(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([1, 0, 3], dtype=torch.int8) + >>> torch.bitwise_and(torch.tensor([True, True, False]), torch.tensor([False, True, False])) + tensor([ False, True, False]) + """ + +@overload +def bitwise_and( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bitwise_and(input, other, *, out=None) -> Tensor + + Computes the bitwise AND of :attr:`input` and :attr:`other`. The input tensor must be of + integral or Boolean types. For bool tensors, it computes the logical AND. + + Args: + input: the first input tensor + other: the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_and(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([1, 0, 3], dtype=torch.int8) + >>> torch.bitwise_and(torch.tensor([True, True, False]), torch.tensor([False, True, False])) + tensor([ False, True, False]) + """ + +@overload +def bitwise_left_shift( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bitwise_left_shift(input, other, *, out=None) -> Tensor + + Computes the left arithmetic shift of :attr:`input` by :attr:`other` bits. + The input tensor must be of integral type. This operator supports + :ref:`broadcasting to a common shape ` and + :ref:`type promotion `. + + The operation applied is: + + .. math:: + \text{out}_i = \text{input}_i << \text{other}_i + + Args: + input (Tensor or Scalar): the first input tensor + other (Tensor or Scalar): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_left_shift(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-2, -2, 24], dtype=torch.int8) + """ + +@overload +def bitwise_left_shift(self: Number | _complex, other: Tensor) -> Tensor: + r""" + bitwise_left_shift(input, other, *, out=None) -> Tensor + + Computes the left arithmetic shift of :attr:`input` by :attr:`other` bits. + The input tensor must be of integral type. This operator supports + :ref:`broadcasting to a common shape ` and + :ref:`type promotion `. + + The operation applied is: + + .. math:: + \text{out}_i = \text{input}_i << \text{other}_i + + Args: + input (Tensor or Scalar): the first input tensor + other (Tensor or Scalar): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_left_shift(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-2, -2, 24], dtype=torch.int8) + """ + +@overload +def bitwise_left_shift( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bitwise_left_shift(input, other, *, out=None) -> Tensor + + Computes the left arithmetic shift of :attr:`input` by :attr:`other` bits. + The input tensor must be of integral type. This operator supports + :ref:`broadcasting to a common shape ` and + :ref:`type promotion `. + + The operation applied is: + + .. math:: + \text{out}_i = \text{input}_i << \text{other}_i + + Args: + input (Tensor or Scalar): the first input tensor + other (Tensor or Scalar): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_left_shift(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-2, -2, 24], dtype=torch.int8) + """ + +def bitwise_not(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + bitwise_not(input, *, out=None) -> Tensor + + Computes the bitwise NOT of the given input tensor. The input tensor must be of + integral or Boolean types. For bool tensors, it computes the logical NOT. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_not(torch.tensor([-1, -2, 3], dtype=torch.int8)) + tensor([ 0, 1, -4], dtype=torch.int8) + """ + +@overload +def bitwise_or( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bitwise_or(input: Tensor, other: Tensor, *, out: Optional[Tensor]) -> Tensor + + Computes the bitwise OR of :attr:`input` and :attr:`other`. The input tensor must be of + integral or Boolean types. For bool tensors, it computes the logical OR. + + Args: + input: the first input tensor + other: the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_or(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-1, -2, 3], dtype=torch.int8) + >>> torch.bitwise_or(torch.tensor([True, True, False]), torch.tensor([False, True, False])) + tensor([ True, True, False]) + """ + +@overload +def bitwise_or(self: Number | _complex, other: Tensor) -> Tensor: + r""" + bitwise_or(input: Tensor, other: Tensor, *, out: Optional[Tensor]) -> Tensor + + Computes the bitwise OR of :attr:`input` and :attr:`other`. The input tensor must be of + integral or Boolean types. For bool tensors, it computes the logical OR. + + Args: + input: the first input tensor + other: the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_or(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-1, -2, 3], dtype=torch.int8) + >>> torch.bitwise_or(torch.tensor([True, True, False]), torch.tensor([False, True, False])) + tensor([ True, True, False]) + """ + +@overload +def bitwise_or( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bitwise_or(input: Tensor, other: Tensor, *, out: Optional[Tensor]) -> Tensor + + Computes the bitwise OR of :attr:`input` and :attr:`other`. The input tensor must be of + integral or Boolean types. For bool tensors, it computes the logical OR. + + Args: + input: the first input tensor + other: the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_or(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-1, -2, 3], dtype=torch.int8) + >>> torch.bitwise_or(torch.tensor([True, True, False]), torch.tensor([False, True, False])) + tensor([ True, True, False]) + """ + +@overload +def bitwise_right_shift( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bitwise_right_shift(input, other, *, out=None) -> Tensor + + Computes the right arithmetic shift of :attr:`input` by :attr:`other` bits. + The input tensor must be of integral type. This operator supports + :ref:`broadcasting to a common shape ` and + :ref:`type promotion `. + In any case, if the value of the right operand is negative or is greater + or equal to the number of bits in the promoted left operand, the behavior is undefined. + + The operation applied is: + + .. math:: + \text{out}_i = \text{input}_i >> \text{other}_i + + Args: + input (Tensor or Scalar): the first input tensor + other (Tensor or Scalar): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_right_shift(torch.tensor([-2, -7, 31], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-1, -7, 3], dtype=torch.int8) + """ + +@overload +def bitwise_right_shift(self: Number | _complex, other: Tensor) -> Tensor: + r""" + bitwise_right_shift(input, other, *, out=None) -> Tensor + + Computes the right arithmetic shift of :attr:`input` by :attr:`other` bits. + The input tensor must be of integral type. This operator supports + :ref:`broadcasting to a common shape ` and + :ref:`type promotion `. + In any case, if the value of the right operand is negative or is greater + or equal to the number of bits in the promoted left operand, the behavior is undefined. + + The operation applied is: + + .. math:: + \text{out}_i = \text{input}_i >> \text{other}_i + + Args: + input (Tensor or Scalar): the first input tensor + other (Tensor or Scalar): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_right_shift(torch.tensor([-2, -7, 31], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-1, -7, 3], dtype=torch.int8) + """ + +@overload +def bitwise_right_shift( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bitwise_right_shift(input, other, *, out=None) -> Tensor + + Computes the right arithmetic shift of :attr:`input` by :attr:`other` bits. + The input tensor must be of integral type. This operator supports + :ref:`broadcasting to a common shape ` and + :ref:`type promotion `. + In any case, if the value of the right operand is negative or is greater + or equal to the number of bits in the promoted left operand, the behavior is undefined. + + The operation applied is: + + .. math:: + \text{out}_i = \text{input}_i >> \text{other}_i + + Args: + input (Tensor or Scalar): the first input tensor + other (Tensor or Scalar): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_right_shift(torch.tensor([-2, -7, 31], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-1, -7, 3], dtype=torch.int8) + """ + +@overload +def bitwise_xor( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bitwise_xor(input, other, *, out=None) -> Tensor + + Computes the bitwise XOR of :attr:`input` and :attr:`other`. The input tensor must be of + integral or Boolean types. For bool tensors, it computes the logical XOR. + + Args: + input: the first input tensor + other: the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_xor(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-2, -2, 0], dtype=torch.int8) + >>> torch.bitwise_xor(torch.tensor([True, True, False]), torch.tensor([False, True, False])) + tensor([ True, False, False]) + """ + +@overload +def bitwise_xor(self: Number | _complex, other: Tensor) -> Tensor: + r""" + bitwise_xor(input, other, *, out=None) -> Tensor + + Computes the bitwise XOR of :attr:`input` and :attr:`other`. The input tensor must be of + integral or Boolean types. For bool tensors, it computes the logical XOR. + + Args: + input: the first input tensor + other: the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_xor(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-2, -2, 0], dtype=torch.int8) + >>> torch.bitwise_xor(torch.tensor([True, True, False]), torch.tensor([False, True, False])) + tensor([ True, False, False]) + """ + +@overload +def bitwise_xor( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bitwise_xor(input, other, *, out=None) -> Tensor + + Computes the bitwise XOR of :attr:`input` and :attr:`other`. The input tensor must be of + integral or Boolean types. For bool tensors, it computes the logical XOR. + + Args: + input: the first input tensor + other: the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_xor(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-2, -2, 0], dtype=torch.int8) + >>> torch.bitwise_xor(torch.tensor([True, True, False]), torch.tensor([False, True, False])) + tensor([ True, False, False]) + """ + +@overload +def blackman_window( + window_length: _int, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + blackman_window(window_length, periodic=True, *, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Blackman window function. + + .. math:: + w[n] = 0.42 - 0.5 \cos \left( \frac{2 \pi n}{N - 1} \right) + 0.08 \cos \left( \frac{4 \pi n}{N - 1} \right) + + where :math:`N` is the full window size. + + The input :attr:`window_length` is a positive integer controlling the + returned window size. :attr:`periodic` flag determines whether the returned + window trims off the last duplicate value from the symmetric window and is + ready to be used as a periodic window with functions like + :meth:`torch.stft`. Therefore, if :attr:`periodic` is true, the :math:`N` in + above formula is in fact :math:`\text{window\_length} + 1`. Also, we always have + ``torch.blackman_window(L, periodic=True)`` equal to + ``torch.blackman_window(L + 1, periodic=False)[:-1]``. + + .. note:: + If :attr:`window_length` :math:`=1`, the returned window contains a single value 1. + + Arguments: + window_length (int): the size of returned window + periodic (bool, optional): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window + """ + +@overload +def blackman_window( + window_length: _int, + periodic: _bool, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + blackman_window(window_length, periodic=True, *, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Blackman window function. + + .. math:: + w[n] = 0.42 - 0.5 \cos \left( \frac{2 \pi n}{N - 1} \right) + 0.08 \cos \left( \frac{4 \pi n}{N - 1} \right) + + where :math:`N` is the full window size. + + The input :attr:`window_length` is a positive integer controlling the + returned window size. :attr:`periodic` flag determines whether the returned + window trims off the last duplicate value from the symmetric window and is + ready to be used as a periodic window with functions like + :meth:`torch.stft`. Therefore, if :attr:`periodic` is true, the :math:`N` in + above formula is in fact :math:`\text{window\_length} + 1`. Also, we always have + ``torch.blackman_window(L, periodic=True)`` equal to + ``torch.blackman_window(L + 1, periodic=False)[:-1]``. + + .. note:: + If :attr:`window_length` :math:`=1`, the returned window contains a single value 1. + + Arguments: + window_length (int): the size of returned window + periodic (bool, optional): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window + """ + +@overload +def bmm( + input: Tensor, + mat2: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bmm(input, mat2, out_dtype=None, *, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices stored in :attr:`input` + and :attr:`mat2`. + + :attr:`input` and :attr:`mat2` must be 3-D tensors each containing + the same number of matrices. + + If :attr:`input` is a :math:`(b \times n \times m)` tensor, :attr:`mat2` is a + :math:`(b \times m \times p)` tensor, :attr:`out` will be a + :math:`(b \times n \times p)` tensor. + + .. math:: + \text{out}_i = \text{input}_i \mathbin{@} \text{mat2}_i + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + .. note:: This function does not :ref:`broadcast `. + For broadcasting matrix products, see :func:`torch.matmul`. + + Args: + input (Tensor): the first batch of matrices to be multiplied + mat2 (Tensor): the second batch of matrices to be multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword Args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> input = torch.randn(10, 3, 4) + >>> mat2 = torch.randn(10, 4, 5) + >>> res = torch.bmm(input, mat2) + >>> res.size() + torch.Size([10, 3, 5]) + """ + +@overload +def bmm( + input: Tensor, + mat2: Tensor, + out_dtype: _dtype, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bmm(input, mat2, out_dtype=None, *, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices stored in :attr:`input` + and :attr:`mat2`. + + :attr:`input` and :attr:`mat2` must be 3-D tensors each containing + the same number of matrices. + + If :attr:`input` is a :math:`(b \times n \times m)` tensor, :attr:`mat2` is a + :math:`(b \times m \times p)` tensor, :attr:`out` will be a + :math:`(b \times n \times p)` tensor. + + .. math:: + \text{out}_i = \text{input}_i \mathbin{@} \text{mat2}_i + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + .. note:: This function does not :ref:`broadcast `. + For broadcasting matrix products, see :func:`torch.matmul`. + + Args: + input (Tensor): the first batch of matrices to be multiplied + mat2 (Tensor): the second batch of matrices to be multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword Args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> input = torch.randn(10, 3, 4) + >>> mat2 = torch.randn(10, 4, 5) + >>> res = torch.bmm(input, mat2) + >>> res.size() + torch.Size([10, 3, 5]) + """ + +def broadcast_to(input: Tensor, size: Sequence[_int | SymInt]) -> Tensor: + r""" + broadcast_to(input, shape) -> Tensor + + Broadcasts :attr:`input` to the shape :attr:`\shape`. + Equivalent to calling ``input.expand(shape)``. See :meth:`~Tensor.expand` for details. + + Args: + input (Tensor): the input tensor. + shape (list, tuple, or :class:`torch.Size`): the new shape. + + Example:: + + >>> x = torch.tensor([1, 2, 3]) + >>> torch.broadcast_to(x, (3, 3)) + tensor([[1, 2, 3], + [1, 2, 3], + [1, 2, 3]]) + """ + +@overload +def bucketize( + input: Tensor, + boundaries: Tensor, + *, + out_int32: _bool = False, + right: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + bucketize(input, boundaries, *, out_int32=False, right=False, out=None) -> Tensor + + Returns the indices of the buckets to which each value in the :attr:`input` belongs, where the + boundaries of the buckets are set by :attr:`boundaries`. Return a new tensor with the same size + as :attr:`input`. If :attr:`right` is False (default), then the left boundary is open. Note that + this behavior is opposite the behavior of + `numpy.digitize `_. + More formally, the returned index satisfies the following rules: + + .. list-table:: + :widths: 15 85 + :header-rows: 1 + + * - :attr:`right` + - *returned index satisfies* + * - False + - ``boundaries[i-1] < input[m][n]...[l][x] <= boundaries[i]`` + * - True + - ``boundaries[i-1] <= input[m][n]...[l][x] < boundaries[i]`` + + Args: + input (Tensor or Scalar): N-D tensor or a Scalar containing the search value(s). + boundaries (Tensor): 1-D tensor, must contain a strictly increasing sequence, or the return value is undefined. + + Keyword args: + out_int32 (bool, optional): indicate the output data type. torch.int32 if True, torch.int64 otherwise. + Default value is False, i.e. default output data type is torch.int64. + right (bool, optional): determines the behavior for values in :attr:`boundaries`. See the table above. + out (Tensor, optional): the output tensor, must be the same size as :attr:`input` if provided. + + + Example:: + + >>> boundaries = torch.tensor([1, 3, 5, 7, 9]) + >>> boundaries + tensor([1, 3, 5, 7, 9]) + >>> v = torch.tensor([[3, 6, 9], [3, 6, 9]]) + >>> v + tensor([[3, 6, 9], + [3, 6, 9]]) + >>> torch.bucketize(v, boundaries) + tensor([[1, 3, 4], + [1, 3, 4]]) + >>> torch.bucketize(v, boundaries, right=True) + tensor([[2, 3, 5], + [2, 3, 5]]) + """ + +@overload +def bucketize( + self: Number | _complex, + boundaries: Tensor, + *, + out_int32: _bool = False, + right: _bool = False, +) -> Tensor: + r""" + bucketize(input, boundaries, *, out_int32=False, right=False, out=None) -> Tensor + + Returns the indices of the buckets to which each value in the :attr:`input` belongs, where the + boundaries of the buckets are set by :attr:`boundaries`. Return a new tensor with the same size + as :attr:`input`. If :attr:`right` is False (default), then the left boundary is open. Note that + this behavior is opposite the behavior of + `numpy.digitize `_. + More formally, the returned index satisfies the following rules: + + .. list-table:: + :widths: 15 85 + :header-rows: 1 + + * - :attr:`right` + - *returned index satisfies* + * - False + - ``boundaries[i-1] < input[m][n]...[l][x] <= boundaries[i]`` + * - True + - ``boundaries[i-1] <= input[m][n]...[l][x] < boundaries[i]`` + + Args: + input (Tensor or Scalar): N-D tensor or a Scalar containing the search value(s). + boundaries (Tensor): 1-D tensor, must contain a strictly increasing sequence, or the return value is undefined. + + Keyword args: + out_int32 (bool, optional): indicate the output data type. torch.int32 if True, torch.int64 otherwise. + Default value is False, i.e. default output data type is torch.int64. + right (bool, optional): determines the behavior for values in :attr:`boundaries`. See the table above. + out (Tensor, optional): the output tensor, must be the same size as :attr:`input` if provided. + + + Example:: + + >>> boundaries = torch.tensor([1, 3, 5, 7, 9]) + >>> boundaries + tensor([1, 3, 5, 7, 9]) + >>> v = torch.tensor([[3, 6, 9], [3, 6, 9]]) + >>> v + tensor([[3, 6, 9], + [3, 6, 9]]) + >>> torch.bucketize(v, boundaries) + tensor([[1, 3, 4], + [1, 3, 4]]) + >>> torch.bucketize(v, boundaries, right=True) + tensor([[2, 3, 5], + [2, 3, 5]]) + """ + +def can_cast(from_: _dtype, to: _dtype) -> _bool: + r""" + can_cast(from_, to) -> bool + + Determines if a type conversion is allowed under PyTorch casting rules + described in the type promotion :ref:`documentation `. + + Args: + from\_ (dtype): The original :class:`torch.dtype`. + to (dtype): The target :class:`torch.dtype`. + + Example:: + + >>> torch.can_cast(torch.double, torch.float) + True + >>> torch.can_cast(torch.float, torch.int) + False + """ + +@overload +def cat( + tensors: tuple[Tensor, ...] | list[Tensor], + dim: _int = 0, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + cat(tensors, dim=0, *, out=None) -> Tensor + + Concatenates the given sequence of tensors in :attr:`tensors` in the given dimension. + All tensors must either have the same shape (except in the concatenating + dimension) or be a 1-D empty tensor with size ``(0,)``. + + :func:`torch.cat` can be seen as an inverse operation for :func:`torch.split` + and :func:`torch.chunk`. + + :func:`torch.cat` can be best understood via examples. + + .. seealso:: + + :func:`torch.stack` concatenates the given sequence along a new dimension. + + Args: + tensors (sequence of Tensors): Non-empty tensors provided must have the same shape, + except in the cat dimension. + + dim (int, optional): the dimension over which the tensors are concatenated + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> x = torch.randn(2, 3) + >>> x + tensor([[ 0.6580, -1.0969, -0.4614], + [-0.1034, -0.5790, 0.1497]]) + >>> torch.cat((x, x, x), 0) + tensor([[ 0.6580, -1.0969, -0.4614], + [-0.1034, -0.5790, 0.1497], + [ 0.6580, -1.0969, -0.4614], + [-0.1034, -0.5790, 0.1497], + [ 0.6580, -1.0969, -0.4614], + [-0.1034, -0.5790, 0.1497]]) + >>> torch.cat((x, x, x), 1) + tensor([[ 0.6580, -1.0969, -0.4614, 0.6580, -1.0969, -0.4614, 0.6580, + -1.0969, -0.4614], + [-0.1034, -0.5790, 0.1497, -0.1034, -0.5790, 0.1497, -0.1034, + -0.5790, 0.1497]]) + """ + +@overload +def cat( + tensors: tuple[Tensor, ...] | list[Tensor], + dim: str | EllipsisType | None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + cat(tensors, dim=0, *, out=None) -> Tensor + + Concatenates the given sequence of tensors in :attr:`tensors` in the given dimension. + All tensors must either have the same shape (except in the concatenating + dimension) or be a 1-D empty tensor with size ``(0,)``. + + :func:`torch.cat` can be seen as an inverse operation for :func:`torch.split` + and :func:`torch.chunk`. + + :func:`torch.cat` can be best understood via examples. + + .. seealso:: + + :func:`torch.stack` concatenates the given sequence along a new dimension. + + Args: + tensors (sequence of Tensors): Non-empty tensors provided must have the same shape, + except in the cat dimension. + + dim (int, optional): the dimension over which the tensors are concatenated + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> x = torch.randn(2, 3) + >>> x + tensor([[ 0.6580, -1.0969, -0.4614], + [-0.1034, -0.5790, 0.1497]]) + >>> torch.cat((x, x, x), 0) + tensor([[ 0.6580, -1.0969, -0.4614], + [-0.1034, -0.5790, 0.1497], + [ 0.6580, -1.0969, -0.4614], + [-0.1034, -0.5790, 0.1497], + [ 0.6580, -1.0969, -0.4614], + [-0.1034, -0.5790, 0.1497]]) + >>> torch.cat((x, x, x), 1) + tensor([[ 0.6580, -1.0969, -0.4614, 0.6580, -1.0969, -0.4614, 0.6580, + -1.0969, -0.4614], + [-0.1034, -0.5790, 0.1497, -0.1034, -0.5790, 0.1497, -0.1034, + -0.5790, 0.1497]]) + """ + +def ccol_indices_copy( + input: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: ... +def ceil(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + ceil(input, *, out=None) -> Tensor + + Returns a new tensor with the ceil of the elements of :attr:`input`, + the smallest integer greater than or equal to each element. + + For integer inputs, follows the array-api convention of returning a + copy of the input tensor. + + .. math:: + \text{out}_{i} = \left\lceil \text{input}_{i} \right\rceil + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-0.6341, -1.4208, -1.0900, 0.5826]) + >>> torch.ceil(a) + tensor([-0., -1., -1., 1.]) + """ + +def ceil_(input: Tensor) -> Tensor: ... +def celu(input: Tensor, alpha: Number | _complex = 1.0) -> Tensor: ... +def celu_(input: Tensor, alpha: Number | _complex = 1.0) -> Tensor: ... +def channel_shuffle(input: Tensor, groups: _int | SymInt) -> Tensor: ... +def cholesky( + input: Tensor, + upper: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + cholesky(input, upper=False, *, out=None) -> Tensor + + Computes the Cholesky decomposition of a symmetric positive-definite + matrix :math:`A` or for batches of symmetric positive-definite matrices. + + If :attr:`upper` is ``True``, the returned matrix ``U`` is upper-triangular, and + the decomposition has the form: + + .. math:: + + A = U^TU + + If :attr:`upper` is ``False``, the returned matrix ``L`` is lower-triangular, and + the decomposition has the form: + + .. math:: + + A = LL^T + + If :attr:`upper` is ``True``, and :math:`A` is a batch of symmetric positive-definite + matrices, then the returned tensor will be composed of upper-triangular Cholesky factors + of each of the individual matrices. Similarly, when :attr:`upper` is ``False``, the returned + tensor will be composed of lower-triangular Cholesky factors of each of the individual + matrices. + + .. warning:: + + :func:`torch.cholesky` is deprecated in favor of :func:`torch.linalg.cholesky` + and will be removed in a future PyTorch release. + + ``L = torch.cholesky(A)`` should be replaced with + + .. code:: python + + L = torch.linalg.cholesky(A) + + ``U = torch.cholesky(A, upper=True)`` should be replaced with + + .. code:: python + + U = torch.linalg.cholesky(A).mH + + This transform will produce equivalent results for all valid (symmetric positive definite) inputs. + + Args: + input (Tensor): the input tensor :math:`A` of size :math:`(*, n, n)` where `*` is zero or more + batch dimensions consisting of symmetric positive-definite matrices. + upper (bool, optional): flag that indicates whether to return a + upper or lower triangular matrix. Default: ``False`` + + Keyword args: + out (Tensor, optional): the output matrix + + Example:: + + >>> a = torch.randn(3, 3) + >>> a = a @ a.mT + 1e-3 # make symmetric positive-definite + >>> l = torch.cholesky(a) + >>> a + tensor([[ 2.4112, -0.7486, 1.4551], + [-0.7486, 1.3544, 0.1294], + [ 1.4551, 0.1294, 1.6724]]) + >>> l + tensor([[ 1.5528, 0.0000, 0.0000], + [-0.4821, 1.0592, 0.0000], + [ 0.9371, 0.5487, 0.7023]]) + >>> l @ l.mT + tensor([[ 2.4112, -0.7486, 1.4551], + [-0.7486, 1.3544, 0.1294], + [ 1.4551, 0.1294, 1.6724]]) + >>> a = torch.randn(3, 2, 2) # Example for batched input + >>> a = a @ a.mT + 1e-03 # make symmetric positive-definite + >>> l = torch.cholesky(a) + >>> z = l @ l.mT + >>> torch.dist(z, a) + tensor(2.3842e-07) + """ + +def cholesky_inverse( + input: Tensor, + upper: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + cholesky_inverse(L, upper=False, *, out=None) -> Tensor + + Computes the inverse of a complex Hermitian or real symmetric + positive-definite matrix given its Cholesky decomposition. + + Let :math:`A` be a complex Hermitian or real symmetric positive-definite matrix, + and :math:`L` its Cholesky decomposition such that: + + .. math:: + + A = LL^{\text{H}} + + where :math:`L^{\text{H}}` is the conjugate transpose when :math:`L` is complex, + and the transpose when :math:`L` is real-valued. + + Computes the inverse matrix :math:`A^{-1}`. + + Supports input of float, double, cfloat and cdouble dtypes. + Also supports batches of matrices, and if :math:`A` is a batch of matrices + then the output has the same batch dimensions. + + Args: + L (Tensor): tensor of shape `(*, n, n)` where `*` is zero or more batch dimensions + consisting of lower or upper triangular Cholesky decompositions of + symmetric or Hermitian positive-definite matrices. + upper (bool, optional): flag that indicates whether :math:`L` is lower triangular + or upper triangular. Default: ``False`` + + Keyword args: + out (Tensor, optional): output tensor. Ignored if `None`. Default: `None`. + + Example:: + + >>> A = torch.randn(3, 3) + >>> A = A @ A.T + torch.eye(3) * 1e-3 # Creates a symmetric positive-definite matrix + >>> L = torch.linalg.cholesky(A) # Extract Cholesky decomposition + >>> torch.cholesky_inverse(L) + tensor([[ 1.9314, 1.2251, -0.0889], + [ 1.2251, 2.4439, 0.2122], + [-0.0889, 0.2122, 0.1412]]) + >>> A.inverse() + tensor([[ 1.9314, 1.2251, -0.0889], + [ 1.2251, 2.4439, 0.2122], + [-0.0889, 0.2122, 0.1412]]) + + >>> A = torch.randn(3, 2, 2, dtype=torch.complex64) + >>> A = A @ A.mH + torch.eye(2) * 1e-3 # Batch of Hermitian positive-definite matrices + >>> L = torch.linalg.cholesky(A) + >>> torch.dist(torch.inverse(A), torch.cholesky_inverse(L)) + tensor(5.6358e-7) + """ + +def cholesky_solve( + input: Tensor, + input2: Tensor, + upper: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + cholesky_solve(B, L, upper=False, *, out=None) -> Tensor + + Computes the solution of a system of linear equations with complex Hermitian + or real symmetric positive-definite lhs given its Cholesky decomposition. + + Let :math:`A` be a complex Hermitian or real symmetric positive-definite matrix, + and :math:`L` its Cholesky decomposition such that: + + .. math:: + + A = LL^{\text{H}} + + where :math:`L^{\text{H}}` is the conjugate transpose when :math:`L` is complex, + and the transpose when :math:`L` is real-valued. + + Returns the solution :math:`X` of the following linear system: + + .. math:: + + AX = B + + Supports inputs of float, double, cfloat and cdouble dtypes. + Also supports batches of matrices, and if :math:`A` or :math:`B` is a batch of matrices + then the output has the same batch dimensions. + + Args: + B (Tensor): right-hand side tensor of shape `(*, n, k)` + where :math:`*` is zero or more batch dimensions + L (Tensor): tensor of shape `(*, n, n)` where `*` is zero or more batch dimensions + consisting of lower or upper triangular Cholesky decompositions of + symmetric or Hermitian positive-definite matrices. + upper (bool, optional): flag that indicates whether :math:`L` is lower triangular + or upper triangular. Default: ``False``. + + Keyword args: + out (Tensor, optional): output tensor. Ignored if `None`. Default: `None`. + + Example:: + + >>> A = torch.randn(3, 3) + >>> A = A @ A.T + torch.eye(3) * 1e-3 # Creates a symmetric positive-definite matrix + >>> L = torch.linalg.cholesky(A) # Extract Cholesky decomposition + >>> B = torch.randn(3, 2) + >>> torch.cholesky_solve(B, L) + tensor([[ -8.1625, 19.6097], + [ -5.8398, 14.2387], + [ -4.3771, 10.4173]]) + >>> A.inverse() @ B + tensor([[ -8.1626, 19.6097], + [ -5.8398, 14.2387], + [ -4.3771, 10.4173]]) + + >>> A = torch.randn(3, 2, 2, dtype=torch.complex64) + >>> A = A @ A.mH + torch.eye(2) * 1e-3 # Batch of Hermitian positive-definite matrices + >>> L = torch.linalg.cholesky(A) + >>> B = torch.randn(2, 1, dtype=torch.complex64) + >>> X = torch.cholesky_solve(B, L) + >>> torch.dist(X, A.inverse() @ B) + tensor(1.6881e-5) + """ + +def choose_qparams_optimized( + input: Tensor, + numel: _int, + n_bins: _int, + ratio: _float, + bit_width: _int, +) -> tuple[Tensor, Tensor]: ... +def chunk(input: Tensor, chunks: _int, dim: _int = 0) -> tuple[Tensor, ...]: + r""" + chunk(input: Tensor, chunks: int, dim: int = 0) -> Tuple[Tensor, ...] + + Attempts to split a tensor into the specified number of chunks. Each chunk is a view of + the input tensor. + + + .. note:: + + This function may return fewer than the specified number of chunks! + + .. seealso:: + + :func:`torch.tensor_split` a function that always returns exactly the specified number of chunks + + If the tensor size along the given dimension :attr:`dim` is divisible by :attr:`chunks`, + all returned chunks will be the same size. + If the tensor size along the given dimension :attr:`dim` is not divisible by :attr:`chunks`, + all returned chunks will be the same size, except the last one. + If such division is not possible, this function may return fewer + than the specified number of chunks. + + Arguments: + input (Tensor): the tensor to split + chunks (int): number of chunks to return + dim (int): dimension along which to split the tensor + + Example: + >>> torch.arange(11).chunk(6) + (tensor([0, 1]), + tensor([2, 3]), + tensor([4, 5]), + tensor([6, 7]), + tensor([8, 9]), + tensor([10])) + >>> torch.arange(12).chunk(6) + (tensor([0, 1]), + tensor([2, 3]), + tensor([4, 5]), + tensor([6, 7]), + tensor([8, 9]), + tensor([10, 11])) + >>> torch.arange(13).chunk(6) + (tensor([0, 1, 2]), + tensor([3, 4, 5]), + tensor([6, 7, 8]), + tensor([ 9, 10, 11]), + tensor([12])) + """ + +@overload +def clamp( + input: Tensor, + min: Tensor | None = None, + max: Tensor | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + clamp(input, min=None, max=None, *, out=None) -> Tensor + + Clamps all elements in :attr:`input` into the range `[` :attr:`min`, :attr:`max` `]`. + Letting min_value and max_value be :attr:`min` and :attr:`max`, respectively, this returns: + + .. math:: + y_i = \min(\max(x_i, \text{min\_value}_i), \text{max\_value}_i) + + If :attr:`min` is ``None``, there is no lower bound. + Or, if :attr:`max` is ``None`` there is no upper bound. + + + .. note:: + If :attr:`min` is greater than :attr:`max` :func:`torch.clamp(..., min, max) ` + sets all elements in :attr:`input` to the value of :attr:`max`. + + Args: + input (Tensor): the input tensor. + min (Number or Tensor, optional): lower-bound of the range to be clamped to + max (Number or Tensor, optional): upper-bound of the range to be clamped to + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-1.7120, 0.1734, -0.0478, -0.0922]) + >>> torch.clamp(a, min=-0.5, max=0.5) + tensor([-0.5000, 0.1734, -0.0478, -0.0922]) + + >>> min = torch.linspace(-1, 1, steps=4) + >>> torch.clamp(a, min=min) + tensor([-1.0000, 0.1734, 0.3333, 1.0000]) + """ + +@overload +def clamp( + input: Tensor, + min: Number | _complex | None = None, + max: Number | _complex | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + clamp(input, min=None, max=None, *, out=None) -> Tensor + + Clamps all elements in :attr:`input` into the range `[` :attr:`min`, :attr:`max` `]`. + Letting min_value and max_value be :attr:`min` and :attr:`max`, respectively, this returns: + + .. math:: + y_i = \min(\max(x_i, \text{min\_value}_i), \text{max\_value}_i) + + If :attr:`min` is ``None``, there is no lower bound. + Or, if :attr:`max` is ``None`` there is no upper bound. + + + .. note:: + If :attr:`min` is greater than :attr:`max` :func:`torch.clamp(..., min, max) ` + sets all elements in :attr:`input` to the value of :attr:`max`. + + Args: + input (Tensor): the input tensor. + min (Number or Tensor, optional): lower-bound of the range to be clamped to + max (Number or Tensor, optional): upper-bound of the range to be clamped to + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-1.7120, 0.1734, -0.0478, -0.0922]) + >>> torch.clamp(a, min=-0.5, max=0.5) + tensor([-0.5000, 0.1734, -0.0478, -0.0922]) + + >>> min = torch.linspace(-1, 1, steps=4) + >>> torch.clamp(a, min=min) + tensor([-1.0000, 0.1734, 0.3333, 1.0000]) + """ + +@overload +def clamp_( + input: Tensor, + min: Tensor | None = None, + max: Tensor | None = None, +) -> Tensor: ... +@overload +def clamp_( + input: Tensor, + min: Number | _complex | None = None, + max: Number | _complex | None = None, +) -> Tensor: ... +@overload +def clamp_max( + input: Tensor, + max: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: ... +@overload +def clamp_max( + input: Tensor, + max: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: ... +@overload +def clamp_max_(input: Tensor, max: Tensor) -> Tensor: ... +@overload +def clamp_max_(input: Tensor, max: Number | _complex) -> Tensor: ... +@overload +def clamp_min( + input: Tensor, + min: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: ... +@overload +def clamp_min( + input: Tensor, + min: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: ... +@overload +def clamp_min_(input: Tensor, min: Tensor) -> Tensor: ... +@overload +def clamp_min_(input: Tensor, min: Number | _complex) -> Tensor: ... +@overload +def clip( + input: Tensor, + min: Tensor | None = None, + max: Tensor | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + clip(input, min=None, max=None, *, out=None) -> Tensor + + Alias for :func:`torch.clamp`. + """ + +@overload +def clip( + input: Tensor, + min: Number | _complex | None = None, + max: Number | _complex | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + clip(input, min=None, max=None, *, out=None) -> Tensor + + Alias for :func:`torch.clamp`. + """ + +@overload +def clip_( + input: Tensor, + min: Tensor | None = None, + max: Tensor | None = None, +) -> Tensor: ... +@overload +def clip_( + input: Tensor, + min: Number | _complex | None = None, + max: Number | _complex | None = None, +) -> Tensor: ... +def clone( + input: Tensor, + *, + memory_format: memory_format | None = None, +) -> Tensor: + r""" + clone(input, *, memory_format=torch.preserve_format) -> Tensor + + Returns a copy of :attr:`input`. + + .. note:: + + This function is differentiable, so gradients will flow back from the + result of this operation to :attr:`input`. To create a tensor without an + autograd relationship to :attr:`input` see :meth:`~Tensor.detach`. + + In addition, when ``torch.preserve_format`` is used: + If the input tensor is dense (i.e., non-overlapping strided), + its memory format (including strides) is retained. + Otherwise (e.g., a non-dense view like a stepped slice), + the output is converted to the dense (contiguous) format. + + Args: + input (Tensor): the input tensor. + + Keyword args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned tensor. Default: ``torch.preserve_format``. + """ + +def col_indices_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + Performs the same operation as :func:`torch.col_indices`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def column_stack( + tensors: tuple[Tensor, ...] | list[Tensor], + *, + out: Tensor | None = None, +) -> Tensor: + r""" + column_stack(tensors, *, out=None) -> Tensor + + Creates a new tensor by horizontally stacking the tensors in :attr:`tensors`. + + Equivalent to ``torch.hstack(tensors)``, except each zero or one dimensional tensor ``t`` + in :attr:`tensors` is first reshaped into a ``(t.numel(), 1)`` column before being stacked horizontally. + + Args: + tensors (sequence of Tensors): sequence of tensors to concatenate + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([1, 2, 3]) + >>> b = torch.tensor([4, 5, 6]) + >>> torch.column_stack((a, b)) + tensor([[1, 4], + [2, 5], + [3, 6]]) + >>> a = torch.arange(5) + >>> b = torch.arange(10).reshape(5, 2) + >>> torch.column_stack((a, b, b)) + tensor([[0, 0, 1, 0, 1], + [1, 2, 3, 2, 3], + [2, 4, 5, 4, 5], + [3, 6, 7, 6, 7], + [4, 8, 9, 8, 9]]) + """ + +def combinations( + input: Tensor, + r: _int = 2, + with_replacement: _bool = False, +) -> Tensor: + r""" + combinations(input: Tensor, r: int = 2, with_replacement: bool = False) -> seq + + Compute combinations of length :math:`r` of the given tensor. The behavior is similar to + python's `itertools.combinations` when `with_replacement` is set to `False`, and + `itertools.combinations_with_replacement` when `with_replacement` is set to `True`. + + Arguments: + input (Tensor): 1D vector. + r (int, optional): number of elements to combine + with_replacement (bool, optional): whether to allow duplication in combination + + Returns: + Tensor: A tensor equivalent to converting all the input tensors into lists, do + `itertools.combinations` or `itertools.combinations_with_replacement` on these + lists, and finally convert the resulting list into tensor. + + Example:: + + >>> a = [1, 2, 3] + >>> list(itertools.combinations(a, r=2)) + [(1, 2), (1, 3), (2, 3)] + >>> list(itertools.combinations(a, r=3)) + [(1, 2, 3)] + >>> list(itertools.combinations_with_replacement(a, r=2)) + [(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)] + >>> tensor_a = torch.tensor(a) + >>> torch.combinations(tensor_a) + tensor([[1, 2], + [1, 3], + [2, 3]]) + >>> torch.combinations(tensor_a, r=3) + tensor([[1, 2, 3]]) + >>> torch.combinations(tensor_a, with_replacement=True) + tensor([[1, 1], + [1, 2], + [1, 3], + [2, 2], + [2, 3], + [3, 3]]) + """ + +def complex( + real: Tensor, + imag: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + complex(real, imag, *, out=None) -> Tensor + + Constructs a complex tensor with its real part equal to :attr:`real` and its + imaginary part equal to :attr:`imag`. + + Args: + real (Tensor): The real part of the complex tensor. Must be half, float or double. + imag (Tensor): The imaginary part of the complex tensor. Must be same dtype + as :attr:`real`. + + Keyword args: + out (Tensor): If the inputs are ``torch.float32``, must be + ``torch.complex64``. If the inputs are ``torch.float64``, must be + ``torch.complex128``. + + Example:: + + >>> real = torch.tensor([1, 2], dtype=torch.float32) + >>> imag = torch.tensor([3, 4], dtype=torch.float32) + >>> z = torch.complex(real, imag) + >>> z + tensor([(1.+3.j), (2.+4.j)]) + >>> z.dtype + torch.complex64 + """ + +@overload +def concat( + tensors: tuple[Tensor, ...] | list[Tensor], + dim: _int = 0, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + concat(tensors, dim=0, *, out=None) -> Tensor + + Alias of :func:`torch.cat`. + """ + +@overload +def concat( + tensors: tuple[Tensor, ...] | list[Tensor], + dim: str | EllipsisType | None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + concat(tensors, dim=0, *, out=None) -> Tensor + + Alias of :func:`torch.cat`. + """ + +@overload +def concatenate( + tensors: tuple[Tensor, ...] | list[Tensor], + dim: _int = 0, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + concatenate(tensors, axis=0, out=None) -> Tensor + + Alias of :func:`torch.cat`. + """ + +@overload +def concatenate( + tensors: tuple[Tensor, ...] | list[Tensor], + dim: str | EllipsisType | None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + concatenate(tensors, axis=0, out=None) -> Tensor + + Alias of :func:`torch.cat`. + """ + +def conj(input: Tensor) -> Tensor: + r""" + conj(input) -> Tensor + + Returns a view of :attr:`input` with a flipped conjugate bit. If :attr:`input` has a non-complex dtype, + this function just returns :attr:`input`. + + .. note:: + :func:`torch.conj` performs a lazy conjugation, but the actual conjugated tensor can be materialized + at any time using :func:`torch.resolve_conj`. + + .. warning:: In the future, :func:`torch.conj` may return a non-writeable view for an :attr:`input` of + non-complex dtype. It's recommended that programs not modify the tensor returned by :func:`torch.conj_physical` + when :attr:`input` is of non-complex dtype to be compatible with this change. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> x = torch.tensor([-1 + 1j, -2 + 2j, 3 - 3j]) + >>> x.is_conj() + False + >>> y = torch.conj(x) + >>> y.is_conj() + True + """ + +def conj_physical(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + conj_physical(input, *, out=None) -> Tensor + + Computes the element-wise conjugate of the given :attr:`input` tensor. + If :attr:`input` has a non-complex dtype, this function just returns :attr:`input`. + + .. note:: + This performs the conjugate operation regardless of the fact conjugate bit is set or not. + + .. warning:: In the future, :func:`torch.conj_physical` may return a non-writeable view for an :attr:`input` of + non-complex dtype. It's recommended that programs not modify the tensor returned by :func:`torch.conj_physical` + when :attr:`input` is of non-complex dtype to be compatible with this change. + + .. math:: + \text{out}_{i} = conj(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.conj_physical(torch.tensor([-1 + 1j, -2 + 2j, 3 - 3j])) + tensor([-1 - 1j, -2 - 2j, 3 + 3j]) + """ + +def conj_physical_(input: Tensor) -> Tensor: ... +def constant_pad_nd( + input: Tensor, + pad: Sequence[_int | SymInt], + value: Number | _complex = 0, +) -> Tensor: ... +@overload +def conv1d( + input: Tensor, + weight: Tensor, + bias: Tensor | None = None, + stride: _int | SymInt | Sequence[_int | SymInt] = 1, + padding: _int | SymInt | Sequence[_int | SymInt] = 0, + dilation: _int | SymInt | Sequence[_int | SymInt] = 1, + groups: _int | SymInt = 1, +) -> Tensor: ... +@overload +def conv1d( + input: Tensor, + weight: Tensor, + bias: Tensor | None = None, + stride: _int | SymInt | Sequence[_int | SymInt] = 1, + padding: str = "valid", + dilation: _int | SymInt | Sequence[_int | SymInt] = 1, + groups: _int | SymInt = 1, +) -> Tensor: ... +@overload +def conv2d( + input: Tensor, + weight: Tensor, + bias: Tensor | None = None, + stride: _int | SymInt | Sequence[_int | SymInt] = 1, + padding: _int | SymInt | Sequence[_int | SymInt] = 0, + dilation: _int | SymInt | Sequence[_int | SymInt] = 1, + groups: _int | SymInt = 1, +) -> Tensor: ... +@overload +def conv2d( + input: Tensor, + weight: Tensor, + bias: Tensor | None = None, + stride: _int | SymInt | Sequence[_int | SymInt] = 1, + padding: str = "valid", + dilation: _int | SymInt | Sequence[_int | SymInt] = 1, + groups: _int | SymInt = 1, +) -> Tensor: ... +@overload +def conv3d( + input: Tensor, + weight: Tensor, + bias: Tensor | None = None, + stride: _int | SymInt | Sequence[_int | SymInt] = 1, + padding: _int | SymInt | Sequence[_int | SymInt] = 0, + dilation: _int | SymInt | Sequence[_int | SymInt] = 1, + groups: _int | SymInt = 1, +) -> Tensor: ... +@overload +def conv3d( + input: Tensor, + weight: Tensor, + bias: Tensor | None = None, + stride: _int | SymInt | Sequence[_int | SymInt] = 1, + padding: str = "valid", + dilation: _int | SymInt | Sequence[_int | SymInt] = 1, + groups: _int | SymInt = 1, +) -> Tensor: ... +def conv_tbc( + input: Tensor, + weight: Tensor, + bias: Tensor, + pad: _int = 0, +) -> Tensor: ... +def conv_transpose1d( + input: Tensor, + weight: Tensor, + bias: Tensor | None = None, + stride: _int | SymInt | Sequence[_int | SymInt] = 1, + padding: _int | SymInt | Sequence[_int | SymInt] = 0, + output_padding: _int | SymInt | Sequence[_int | SymInt] = 0, + groups: _int | SymInt = 1, + dilation: _int | SymInt | Sequence[_int | SymInt] = 1, +) -> Tensor: ... +def conv_transpose2d( + input: Tensor, + weight: Tensor, + bias: Tensor | None = None, + stride: _int | SymInt | Sequence[_int | SymInt] = 1, + padding: _int | SymInt | Sequence[_int | SymInt] = 0, + output_padding: _int | SymInt | Sequence[_int | SymInt] = 0, + groups: _int | SymInt = 1, + dilation: _int | SymInt | Sequence[_int | SymInt] = 1, +) -> Tensor: ... +def conv_transpose3d( + input: Tensor, + weight: Tensor, + bias: Tensor | None = None, + stride: _int | SymInt | Sequence[_int | SymInt] = 1, + padding: _int | SymInt | Sequence[_int | SymInt] = 0, + output_padding: _int | SymInt | Sequence[_int | SymInt] = 0, + groups: _int | SymInt = 1, + dilation: _int | SymInt | Sequence[_int | SymInt] = 1, +) -> Tensor: ... +def convolution( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + stride: Sequence[_int | SymInt], + padding: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + transposed: _bool, + output_padding: Sequence[_int | SymInt], + groups: _int | SymInt, +) -> Tensor: ... +@overload +def copysign( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + copysign(input, other, *, out=None) -> Tensor + + Create a new floating-point tensor with the magnitude of :attr:`input` and the sign of :attr:`other`, elementwise. + + .. math:: + \text{out}_{i} = \begin{cases} + -|\text{input}_{i}| & \text{if } \text{other}_{i} \leq -0.0 \\ + |\text{input}_{i}| & \text{if } \text{other}_{i} \geq 0.0 \\ + \end{cases} + + + Supports :ref:`broadcasting to a common shape `, + and integer and float inputs. + + Args: + input (Tensor): magnitudes. + other (Tensor or Number): contains value(s) whose signbit(s) are + applied to the magnitudes in :attr:`input`. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(5) + >>> a + tensor([-1.2557, -0.0026, -0.5387, 0.4740, -0.9244]) + >>> torch.copysign(a, 1) + tensor([1.2557, 0.0026, 0.5387, 0.4740, 0.9244]) + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.7079, 0.2778, -1.0249, 0.5719], + [-0.0059, -0.2600, -0.4475, -1.3948], + [ 0.3667, -0.9567, -2.5757, -0.1751], + [ 0.2046, -0.0742, 0.2998, -0.1054]]) + >>> b = torch.randn(4) + tensor([ 0.2373, 0.3120, 0.3190, -1.1128]) + >>> torch.copysign(a, b) + tensor([[ 0.7079, 0.2778, 1.0249, -0.5719], + [ 0.0059, 0.2600, 0.4475, -1.3948], + [ 0.3667, 0.9567, 2.5757, -0.1751], + [ 0.2046, 0.0742, 0.2998, -0.1054]]) + >>> a = torch.tensor([1.]) + >>> b = torch.tensor([-0.]) + >>> torch.copysign(a, b) + tensor([-1.]) + + .. note:: + copysign handles signed zeros. If the other argument has a negative zero (-0), + the corresponding output value will be negative. + """ + +@overload +def copysign( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + copysign(input, other, *, out=None) -> Tensor + + Create a new floating-point tensor with the magnitude of :attr:`input` and the sign of :attr:`other`, elementwise. + + .. math:: + \text{out}_{i} = \begin{cases} + -|\text{input}_{i}| & \text{if } \text{other}_{i} \leq -0.0 \\ + |\text{input}_{i}| & \text{if } \text{other}_{i} \geq 0.0 \\ + \end{cases} + + + Supports :ref:`broadcasting to a common shape `, + and integer and float inputs. + + Args: + input (Tensor): magnitudes. + other (Tensor or Number): contains value(s) whose signbit(s) are + applied to the magnitudes in :attr:`input`. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(5) + >>> a + tensor([-1.2557, -0.0026, -0.5387, 0.4740, -0.9244]) + >>> torch.copysign(a, 1) + tensor([1.2557, 0.0026, 0.5387, 0.4740, 0.9244]) + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.7079, 0.2778, -1.0249, 0.5719], + [-0.0059, -0.2600, -0.4475, -1.3948], + [ 0.3667, -0.9567, -2.5757, -0.1751], + [ 0.2046, -0.0742, 0.2998, -0.1054]]) + >>> b = torch.randn(4) + tensor([ 0.2373, 0.3120, 0.3190, -1.1128]) + >>> torch.copysign(a, b) + tensor([[ 0.7079, 0.2778, 1.0249, -0.5719], + [ 0.0059, 0.2600, 0.4475, -1.3948], + [ 0.3667, 0.9567, 2.5757, -0.1751], + [ 0.2046, 0.0742, 0.2998, -0.1054]]) + >>> a = torch.tensor([1.]) + >>> b = torch.tensor([-0.]) + >>> torch.copysign(a, b) + tensor([-1.]) + + .. note:: + copysign handles signed zeros. If the other argument has a negative zero (-0), + the corresponding output value will be negative. + """ + +def corrcoef(input: Tensor) -> Tensor: + r""" + corrcoef(input) -> Tensor + + Estimates the Pearson product-moment correlation coefficient matrix of the variables given by the :attr:`input` matrix, + where rows are the variables and columns are the observations. + + .. note:: + + The correlation coefficient matrix R is computed using the covariance matrix C as given by + :math:`R_{ij} = \frac{ C_{ij} } { \sqrt{ C_{ii} * C_{jj} } }` + + .. note:: + + Due to floating point rounding, the resulting array may not be Hermitian and its diagonal elements may not be 1. + The real and imaginary values are clipped to the interval [-1, 1] in an attempt to improve this situation. + + Args: + input (Tensor): A 2D matrix containing multiple variables and observations, or a + Scalar or 1D vector representing a single variable. + + Returns: + (Tensor) The correlation coefficient matrix of the variables. + + .. seealso:: + + :func:`torch.cov` covariance matrix. + + Example:: + + >>> x = torch.tensor([[0, 1, 2], [2, 1, 0]]) + >>> torch.corrcoef(x) + tensor([[ 1., -1.], + [-1., 1.]]) + >>> x = torch.randn(2, 4) + >>> x + tensor([[-0.2678, -0.0908, -0.3766, 0.2780], + [-0.5812, 0.1535, 0.2387, 0.2350]]) + >>> torch.corrcoef(x) + tensor([[1.0000, 0.3582], + [0.3582, 1.0000]]) + >>> torch.corrcoef(x[0]) + tensor(1.) + """ + +def cos(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + cos(input, *, out=None) -> Tensor + + Returns a new tensor with the cosine of the elements of :attr:`input` given in radians. + + .. math:: + \text{out}_{i} = \cos(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 1.4309, 1.2706, -0.8562, 0.9796]) + >>> torch.cos(a) + tensor([ 0.1395, 0.2957, 0.6553, 0.5574]) + """ + +def cos_(input: Tensor) -> Tensor: ... +def cosh(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + cosh(input, *, out=None) -> Tensor + + Returns a new tensor with the hyperbolic cosine of the elements of + :attr:`input`. + + .. math:: + \text{out}_{i} = \cosh(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.1632, 1.1835, -0.6979, -0.7325]) + >>> torch.cosh(a) + tensor([ 1.0133, 1.7860, 1.2536, 1.2805]) + + .. note:: + When :attr:`input` is on the CPU, the implementation of torch.cosh may use + the Sleef library, which rounds very large results to infinity or negative + infinity. See `here `_ for details. + """ + +def cosh_(input: Tensor) -> Tensor: ... +def cosine_embedding_loss( + input1: Tensor, + input2: Tensor, + target: Tensor, + margin: _float = 0.0, + reduction: _int = 1, +) -> Tensor: ... +def cosine_similarity( + x1: Tensor, + x2: Tensor, + dim: _int = 1, + eps: _float = 1e-08, +) -> Tensor: ... +@overload +def count_nonzero(input: Tensor, dim: _int | None = None) -> Tensor: + r""" + count_nonzero(input, dim=None) -> Tensor + + Counts the number of non-zero values in the tensor :attr:`input` along the given :attr:`dim`. + If no dim is specified then all non-zeros in the tensor are counted. + + Args: + input (Tensor): the input tensor. + dim (int or tuple of ints, optional): Dim or tuple of dims along which to count non-zeros. + + Example:: + + >>> x = torch.zeros(3,3) + >>> x[torch.randn(3,3) > 0.5] = 1 + >>> x + tensor([[0., 1., 1.], + [0., 0., 0.], + [0., 0., 1.]]) + >>> torch.count_nonzero(x) + tensor(3) + >>> torch.count_nonzero(x, dim=0) + tensor([0, 1, 2]) + """ + +@overload +def count_nonzero(input: Tensor, dim: _size) -> Tensor: + r""" + count_nonzero(input, dim=None) -> Tensor + + Counts the number of non-zero values in the tensor :attr:`input` along the given :attr:`dim`. + If no dim is specified then all non-zeros in the tensor are counted. + + Args: + input (Tensor): the input tensor. + dim (int or tuple of ints, optional): Dim or tuple of dims along which to count non-zeros. + + Example:: + + >>> x = torch.zeros(3,3) + >>> x[torch.randn(3,3) > 0.5] = 1 + >>> x + tensor([[0., 1., 1.], + [0., 0., 0.], + [0., 0., 1.]]) + >>> torch.count_nonzero(x) + tensor(3) + >>> torch.count_nonzero(x, dim=0) + tensor([0, 1, 2]) + """ + +def cov( + input: Tensor, + *, + correction: _int = 1, + fweights: Tensor | None = None, + aweights: Tensor | None = None, +) -> Tensor: + r""" + cov(input, *, correction=1, fweights=None, aweights=None) -> Tensor + + Estimates the covariance matrix of the variables given by the :attr:`input` matrix, where rows are + the variables and columns are the observations. + + A covariance matrix is a square matrix giving the covariance of each pair of variables. The diagonal contains + the variance of each variable (covariance of a variable with itself). By definition, if :attr:`input` represents + a single variable (Scalar or 1D) then its variance is returned. + + The sample covariance of the variables :math:`x` and :math:`y` is given by: + + .. math:: + \text{cov}(x,y) = \frac{\sum^{N}_{i = 1}(x_{i} - \bar{x})(y_{i} - \bar{y})}{\max(0,~N~-~\delta N)} + + where :math:`\bar{x}` and :math:`\bar{y}` are the simple means of the :math:`x` and :math:`y` respectively, and + :math:`\delta N` is the :attr:`correction`. + + If :attr:`fweights` and/or :attr:`aweights` are provided, the weighted covariance + is calculated, which is given by: + + .. math:: + \text{cov}_w(x,y) = \frac{\sum^{N}_{i = 1}w_i(x_{i} - \mu_x^*)(y_{i} - \mu_y^*)} + {\max(0,~\sum^{N}_{i = 1}w_i~-~\frac{\sum^{N}_{i = 1}w_ia_i}{\sum^{N}_{i = 1}w_i}~\delta N)} + + where :math:`w` denotes :attr:`fweights` or :attr:`aweights` (``f`` and ``a`` for brevity) based on whichever is + provided, or :math:`w = f \times a` if both are provided, and + :math:`\mu_x^* = \frac{\sum^{N}_{i = 1}w_ix_{i} }{\sum^{N}_{i = 1}w_i}` is the weighted mean of the variable. If not + provided, ``f`` and/or ``a`` can be seen as a :math:`\mathbb{1}` vector of appropriate size. + + Args: + input (Tensor): A 2D matrix containing multiple variables and observations, or a + Scalar or 1D vector representing a single variable. + + Keyword Args: + correction (int, optional): difference between the sample size and sample degrees of freedom. + Defaults to Bessel's correction, ``correction = 1`` which returns the unbiased estimate, + even if both :attr:`fweights` and :attr:`aweights` are specified. ``correction = 0`` + will return the simple average. Defaults to ``1``. + fweights (tensor, optional): A Scalar or 1D tensor of observation vector frequencies representing the number of + times each observation should be repeated. Its numel must equal the number of columns of :attr:`input`. + Must have integral dtype. Ignored if ``None``. Defaults to ``None``. + aweights (tensor, optional): A Scalar or 1D array of observation vector weights. + These relative weights are typically large for observations considered "important" and smaller for + observations considered less "important". Its numel must equal the number of columns of :attr:`input`. + Must have floating point dtype. Ignored if ``None``. Defaults to ``None``. + + Returns: + (Tensor) The covariance matrix of the variables. + + .. seealso:: + + :func:`torch.corrcoef` normalized covariance matrix. + + Example:: + + >>> x = torch.tensor([[0, 2], [1, 1], [2, 0]]).T + >>> x + tensor([[0, 1, 2], + [2, 1, 0]]) + >>> torch.cov(x) + tensor([[ 1., -1.], + [-1., 1.]]) + >>> torch.cov(x, correction=0) + tensor([[ 0.6667, -0.6667], + [-0.6667, 0.6667]]) + >>> fw = torch.randint(1, 10, (3,)) + >>> fw + tensor([1, 6, 9]) + >>> aw = torch.rand(3) + >>> aw + tensor([0.4282, 0.0255, 0.4144]) + >>> torch.cov(x, fweights=fw, aweights=aw) + tensor([[ 0.4169, -0.4169], + [-0.4169, 0.4169]]) + """ + +def cross( + input: Tensor, + other: Tensor, + dim: _int | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + cross(input, other, dim=None, *, out=None) -> Tensor + + + Returns the cross product of vectors in dimension :attr:`dim` of :attr:`input` + and :attr:`other`. + + Supports input of float, double, cfloat and cdouble dtypes. Also supports batches + of vectors, for which it computes the product along the dimension :attr:`dim`. + In this case, the output has the same batch dimensions as the inputs. + + .. warning:: + If :attr:`dim` is not given, it defaults to the first dimension found + with the size 3. Note that this might be unexpected. + + This behavior is deprecated and will be changed to match that of :func:`torch.linalg.cross` + in a future release. + + .. seealso:: + :func:`torch.linalg.cross` which has dim=-1 as default. + + + Args: + input (Tensor): the input tensor. + other (Tensor): the second input tensor + dim (int, optional): the dimension to take the cross-product in. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4, 3) + >>> a + tensor([[-0.3956, 1.1455, 1.6895], + [-0.5849, 1.3672, 0.3599], + [-1.1626, 0.7180, -0.0521], + [-0.1339, 0.9902, -2.0225]]) + >>> b = torch.randn(4, 3) + >>> b + tensor([[-0.0257, -1.4725, -1.2251], + [-1.1479, -0.7005, -1.9757], + [-1.3904, 0.3726, -1.1836], + [-0.9688, -0.7153, 0.2159]]) + >>> torch.cross(a, b, dim=1) + tensor([[ 1.0844, -0.5281, 0.6120], + [-2.4490, -1.5687, 1.9792], + [-0.8304, -1.3037, 0.5650], + [-1.2329, 1.9883, 1.0551]]) + >>> torch.cross(a, b) + tensor([[ 1.0844, -0.5281, 0.6120], + [-2.4490, -1.5687, 1.9792], + [-0.8304, -1.3037, 0.5650], + [-1.2329, 1.9883, 1.0551]]) + """ + +def crow_indices_copy( + input: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.crow_indices`, but all output tensors + are freshly created instead of aliasing the input. + """ + +@overload +def ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: _size, + target_lengths: _size, + blank: _int = 0, + reduction: _int = 1, + zero_infinity: _bool = False, +) -> Tensor: ... +@overload +def ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: Tensor, + target_lengths: Tensor, + blank: _int = 0, + reduction: _int = 1, + zero_infinity: _bool = False, +) -> Tensor: ... +def cudnn_affine_grid_generator( + theta: Tensor, + N: _int, + C: _int, + H: _int, + W: _int, +) -> Tensor: ... +def cudnn_batch_norm( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + running_mean: Tensor | None, + running_var: Tensor | None, + training: _bool, + exponential_average_factor: _float, + epsilon: _float, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> tuple[Tensor, Tensor, Tensor, Tensor]: ... +def cudnn_convolution( + input: Tensor, + weight: Tensor, + padding: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, + benchmark: _bool, + deterministic: _bool, + allow_tf32: _bool, + *, + out: Tensor | None = None, +) -> Tensor: ... +def cudnn_convolution_add_relu( + input: Tensor, + weight: Tensor, + z: Tensor, + alpha: Number | _complex | None, + bias: Tensor | None, + stride: Sequence[_int | SymInt], + padding: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, +) -> Tensor: ... +def cudnn_convolution_relu( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + stride: Sequence[_int | SymInt], + padding: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, +) -> Tensor: ... +def cudnn_convolution_transpose( + input: Tensor, + weight: Tensor, + padding: Sequence[_int | SymInt], + output_padding: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, + benchmark: _bool, + deterministic: _bool, + allow_tf32: _bool, +) -> Tensor: ... +def cudnn_grid_sampler(input: Tensor, grid: Tensor) -> Tensor: ... +def cudnn_is_acceptable(input: Tensor) -> _bool: ... +@overload +def cummax( + input: Tensor, + dim: _int, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.cummax: + r""" + cummax(input, dim, *, out=None) -> (Tensor, LongTensor) + Returns a namedtuple ``(values, indices)`` where ``values`` is the cumulative maximum of + elements of :attr:`input` in the dimension :attr:`dim`. And ``indices`` is the index + location of each maximum value found in the dimension :attr:`dim`. + + .. math:: + y_i = max(x_1, x_2, x_3, \dots, x_i) + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to do the operation over + + Keyword args: + out (tuple, optional): the result tuple of two output tensors (values, indices) + + Example:: + + >>> a = torch.randn(10) + >>> a + tensor([-0.3449, -1.5447, 0.0685, -1.5104, -1.1706, 0.2259, 1.4696, -1.3284, + 1.9946, -0.8209]) + >>> torch.cummax(a, dim=0) + torch.return_types.cummax( + values=tensor([-0.3449, -0.3449, 0.0685, 0.0685, 0.0685, 0.2259, 1.4696, 1.4696, + 1.9946, 1.9946]), + indices=tensor([0, 0, 2, 2, 2, 5, 6, 6, 8, 8])) + """ + +@overload +def cummax( + input: Tensor, + dim: str | EllipsisType | None, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.cummax: + r""" + cummax(input, dim, *, out=None) -> (Tensor, LongTensor) + Returns a namedtuple ``(values, indices)`` where ``values`` is the cumulative maximum of + elements of :attr:`input` in the dimension :attr:`dim`. And ``indices`` is the index + location of each maximum value found in the dimension :attr:`dim`. + + .. math:: + y_i = max(x_1, x_2, x_3, \dots, x_i) + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to do the operation over + + Keyword args: + out (tuple, optional): the result tuple of two output tensors (values, indices) + + Example:: + + >>> a = torch.randn(10) + >>> a + tensor([-0.3449, -1.5447, 0.0685, -1.5104, -1.1706, 0.2259, 1.4696, -1.3284, + 1.9946, -0.8209]) + >>> torch.cummax(a, dim=0) + torch.return_types.cummax( + values=tensor([-0.3449, -0.3449, 0.0685, 0.0685, 0.0685, 0.2259, 1.4696, 1.4696, + 1.9946, 1.9946]), + indices=tensor([0, 0, 2, 2, 2, 5, 6, 6, 8, 8])) + """ + +@overload +def cummin( + input: Tensor, + dim: _int, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.cummin: + r""" + cummin(input, dim, *, out=None) -> (Tensor, LongTensor) + Returns a namedtuple ``(values, indices)`` where ``values`` is the cumulative minimum of + elements of :attr:`input` in the dimension :attr:`dim`. And ``indices`` is the index + location of each maximum value found in the dimension :attr:`dim`. + + .. math:: + y_i = min(x_1, x_2, x_3, \dots, x_i) + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to do the operation over + + Keyword args: + out (tuple, optional): the result tuple of two output tensors (values, indices) + + Example:: + + >>> a = torch.randn(10) + >>> a + tensor([-0.2284, -0.6628, 0.0975, 0.2680, -1.3298, -0.4220, -0.3885, 1.1762, + 0.9165, 1.6684]) + >>> torch.cummin(a, dim=0) + torch.return_types.cummin( + values=tensor([-0.2284, -0.6628, -0.6628, -0.6628, -1.3298, -1.3298, -1.3298, -1.3298, + -1.3298, -1.3298]), + indices=tensor([0, 1, 1, 1, 4, 4, 4, 4, 4, 4])) + """ + +@overload +def cummin( + input: Tensor, + dim: str | EllipsisType | None, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.cummin: + r""" + cummin(input, dim, *, out=None) -> (Tensor, LongTensor) + Returns a namedtuple ``(values, indices)`` where ``values`` is the cumulative minimum of + elements of :attr:`input` in the dimension :attr:`dim`. And ``indices`` is the index + location of each maximum value found in the dimension :attr:`dim`. + + .. math:: + y_i = min(x_1, x_2, x_3, \dots, x_i) + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to do the operation over + + Keyword args: + out (tuple, optional): the result tuple of two output tensors (values, indices) + + Example:: + + >>> a = torch.randn(10) + >>> a + tensor([-0.2284, -0.6628, 0.0975, 0.2680, -1.3298, -0.4220, -0.3885, 1.1762, + 0.9165, 1.6684]) + >>> torch.cummin(a, dim=0) + torch.return_types.cummin( + values=tensor([-0.2284, -0.6628, -0.6628, -0.6628, -1.3298, -1.3298, -1.3298, -1.3298, + -1.3298, -1.3298]), + indices=tensor([0, 1, 1, 1, 4, 4, 4, 4, 4, 4])) + """ + +@overload +def cumprod( + input: Tensor, + dim: _int, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + cumprod(input, dim, *, dtype=None, out=None) -> Tensor + + Returns the cumulative product of elements of :attr:`input` in the dimension + :attr:`dim`. + + For example, if :attr:`input` is a vector of size N, the result will also be + a vector of size N, with elements. + + .. math:: + y_i = x_1 \times x_2\times x_3\times \dots \times x_i + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to do the operation over + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(10) + >>> a + tensor([ 0.6001, 0.2069, -0.1919, 0.9792, 0.6727, 1.0062, 0.4126, + -0.2129, -0.4206, 0.1968]) + >>> torch.cumprod(a, dim=0) + tensor([ 0.6001, 0.1241, -0.0238, -0.0233, -0.0157, -0.0158, -0.0065, + 0.0014, -0.0006, -0.0001]) + + >>> a[5] = 0.0 + >>> torch.cumprod(a, dim=0) + tensor([ 0.6001, 0.1241, -0.0238, -0.0233, -0.0157, -0.0000, -0.0000, + 0.0000, -0.0000, -0.0000]) + """ + +@overload +def cumprod( + input: Tensor, + dim: str | EllipsisType | None, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + cumprod(input, dim, *, dtype=None, out=None) -> Tensor + + Returns the cumulative product of elements of :attr:`input` in the dimension + :attr:`dim`. + + For example, if :attr:`input` is a vector of size N, the result will also be + a vector of size N, with elements. + + .. math:: + y_i = x_1 \times x_2\times x_3\times \dots \times x_i + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to do the operation over + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(10) + >>> a + tensor([ 0.6001, 0.2069, -0.1919, 0.9792, 0.6727, 1.0062, 0.4126, + -0.2129, -0.4206, 0.1968]) + >>> torch.cumprod(a, dim=0) + tensor([ 0.6001, 0.1241, -0.0238, -0.0233, -0.0157, -0.0158, -0.0065, + 0.0014, -0.0006, -0.0001]) + + >>> a[5] = 0.0 + >>> torch.cumprod(a, dim=0) + tensor([ 0.6001, 0.1241, -0.0238, -0.0233, -0.0157, -0.0000, -0.0000, + 0.0000, -0.0000, -0.0000]) + """ + +@overload +def cumsum( + input: Tensor, + dim: _int, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + cumsum(input, dim, *, dtype=None, out=None) -> Tensor + + Returns the cumulative sum of elements of :attr:`input` in the dimension + :attr:`dim`. + + For example, if :attr:`input` is a vector of size N, the result will also be + a vector of size N, with elements. + + .. math:: + y_i = x_1 + x_2 + x_3 + \dots + x_i + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to do the operation over + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randint(1, 20, (10,)) + >>> a + tensor([13, 7, 3, 10, 13, 3, 15, 10, 9, 10]) + >>> torch.cumsum(a, dim=0) + tensor([13, 20, 23, 33, 46, 49, 64, 74, 83, 93]) + """ + +@overload +def cumsum( + input: Tensor, + dim: str | EllipsisType | None, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + cumsum(input, dim, *, dtype=None, out=None) -> Tensor + + Returns the cumulative sum of elements of :attr:`input` in the dimension + :attr:`dim`. + + For example, if :attr:`input` is a vector of size N, the result will also be + a vector of size N, with elements. + + .. math:: + y_i = x_1 + x_2 + x_3 + \dots + x_i + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to do the operation over + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randint(1, 20, (10,)) + >>> a + tensor([13, 7, 3, 10, 13, 3, 15, 10, 9, 10]) + >>> torch.cumsum(a, dim=0) + tensor([13, 20, 23, 33, 46, 49, 64, 74, 83, 93]) + """ + +@overload +def cumulative_trapezoid(y: Tensor, x: Tensor, *, dim: _int = -1) -> Tensor: + r""" + cumulative_trapezoid(y, x=None, *, dx=None, dim=-1) -> Tensor + + Cumulatively computes the `trapezoidal rule `_ + along :attr:`dim`. By default the spacing between elements is assumed to be 1, but + :attr:`dx` can be used to specify a different constant spacing, and :attr:`x` can be + used to specify arbitrary spacing along :attr:`dim`. + + For more details, please read :func:`torch.trapezoid`. The difference between :func:`torch.trapezoid` + and this function is that, :func:`torch.trapezoid` returns a value for each integration, + where as this function returns a cumulative value for every spacing within the integration. This + is analogous to how `.sum` returns a value and `.cumsum` returns a cumulative sum. + + Arguments: + y (Tensor): Values to use when computing the trapezoidal rule. + x (Tensor): If specified, defines spacing between values as specified above. + + Keyword arguments: + dx (float): constant spacing between values. If neither :attr:`x` or :attr:`dx` + are specified then this defaults to 1. Effectively multiplies the result by its value. + dim (int): The dimension along which to compute the trapezoidal rule. + The last (inner-most) dimension by default. + + Examples:: + + >>> # Cumulatively computes the trapezoidal rule in 1D, spacing is implicitly 1. + >>> y = torch.tensor([1, 5, 10]) + >>> torch.cumulative_trapezoid(y) + tensor([3., 10.5]) + + >>> # Computes the same trapezoidal rule directly up to each element to verify + >>> (1 + 5) / 2 + 3.0 + >>> (1 + 10 + 10) / 2 + 10.5 + + >>> # Cumulatively computes the trapezoidal rule in 1D with constant spacing of 2 + >>> # NOTE: the result is the same as before, but multiplied by 2 + >>> torch.cumulative_trapezoid(y, dx=2) + tensor([6., 21.]) + + >>> # Cumulatively computes the trapezoidal rule in 1D with arbitrary spacing + >>> x = torch.tensor([1, 3, 6]) + >>> torch.cumulative_trapezoid(y, x) + tensor([6., 28.5]) + + >>> # Computes the same trapezoidal rule directly up to each element to verify + >>> ((3 - 1) * (1 + 5)) / 2 + 6.0 + >>> ((3 - 1) * (1 + 5) + (6 - 3) * (5 + 10)) / 2 + 28.5 + + >>> # Cumulatively computes the trapezoidal rule for each row of a 3x3 matrix + >>> y = torch.arange(9).reshape(3, 3) + tensor([[0, 1, 2], + [3, 4, 5], + [6, 7, 8]]) + >>> torch.cumulative_trapezoid(y) + tensor([[ 0.5, 2.], + [ 3.5, 8.], + [ 6.5, 14.]]) + + >>> # Cumulatively computes the trapezoidal rule for each column of the matrix + >>> torch.cumulative_trapezoid(y, dim=0) + tensor([[ 1.5, 2.5, 3.5], + [ 6.0, 8.0, 10.0]]) + + >>> # Cumulatively computes the trapezoidal rule for each row of a 3x3 ones matrix + >>> # with the same arbitrary spacing + >>> y = torch.ones(3, 3) + >>> x = torch.tensor([1, 3, 6]) + >>> torch.cumulative_trapezoid(y, x) + tensor([[2., 5.], + [2., 5.], + [2., 5.]]) + + >>> # Cumulatively computes the trapezoidal rule for each row of a 3x3 ones matrix + >>> # with different arbitrary spacing per row + >>> y = torch.ones(3, 3) + >>> x = torch.tensor([[1, 2, 3], [1, 3, 5], [1, 4, 7]]) + >>> torch.cumulative_trapezoid(y, x) + tensor([[1., 2.], + [2., 4.], + [3., 6.]]) + """ + +@overload +def cumulative_trapezoid( + y: Tensor, + *, + dx: Number | _complex = 1, + dim: _int = -1, +) -> Tensor: + r""" + cumulative_trapezoid(y, x=None, *, dx=None, dim=-1) -> Tensor + + Cumulatively computes the `trapezoidal rule `_ + along :attr:`dim`. By default the spacing between elements is assumed to be 1, but + :attr:`dx` can be used to specify a different constant spacing, and :attr:`x` can be + used to specify arbitrary spacing along :attr:`dim`. + + For more details, please read :func:`torch.trapezoid`. The difference between :func:`torch.trapezoid` + and this function is that, :func:`torch.trapezoid` returns a value for each integration, + where as this function returns a cumulative value for every spacing within the integration. This + is analogous to how `.sum` returns a value and `.cumsum` returns a cumulative sum. + + Arguments: + y (Tensor): Values to use when computing the trapezoidal rule. + x (Tensor): If specified, defines spacing between values as specified above. + + Keyword arguments: + dx (float): constant spacing between values. If neither :attr:`x` or :attr:`dx` + are specified then this defaults to 1. Effectively multiplies the result by its value. + dim (int): The dimension along which to compute the trapezoidal rule. + The last (inner-most) dimension by default. + + Examples:: + + >>> # Cumulatively computes the trapezoidal rule in 1D, spacing is implicitly 1. + >>> y = torch.tensor([1, 5, 10]) + >>> torch.cumulative_trapezoid(y) + tensor([3., 10.5]) + + >>> # Computes the same trapezoidal rule directly up to each element to verify + >>> (1 + 5) / 2 + 3.0 + >>> (1 + 10 + 10) / 2 + 10.5 + + >>> # Cumulatively computes the trapezoidal rule in 1D with constant spacing of 2 + >>> # NOTE: the result is the same as before, but multiplied by 2 + >>> torch.cumulative_trapezoid(y, dx=2) + tensor([6., 21.]) + + >>> # Cumulatively computes the trapezoidal rule in 1D with arbitrary spacing + >>> x = torch.tensor([1, 3, 6]) + >>> torch.cumulative_trapezoid(y, x) + tensor([6., 28.5]) + + >>> # Computes the same trapezoidal rule directly up to each element to verify + >>> ((3 - 1) * (1 + 5)) / 2 + 6.0 + >>> ((3 - 1) * (1 + 5) + (6 - 3) * (5 + 10)) / 2 + 28.5 + + >>> # Cumulatively computes the trapezoidal rule for each row of a 3x3 matrix + >>> y = torch.arange(9).reshape(3, 3) + tensor([[0, 1, 2], + [3, 4, 5], + [6, 7, 8]]) + >>> torch.cumulative_trapezoid(y) + tensor([[ 0.5, 2.], + [ 3.5, 8.], + [ 6.5, 14.]]) + + >>> # Cumulatively computes the trapezoidal rule for each column of the matrix + >>> torch.cumulative_trapezoid(y, dim=0) + tensor([[ 1.5, 2.5, 3.5], + [ 6.0, 8.0, 10.0]]) + + >>> # Cumulatively computes the trapezoidal rule for each row of a 3x3 ones matrix + >>> # with the same arbitrary spacing + >>> y = torch.ones(3, 3) + >>> x = torch.tensor([1, 3, 6]) + >>> torch.cumulative_trapezoid(y, x) + tensor([[2., 5.], + [2., 5.], + [2., 5.]]) + + >>> # Cumulatively computes the trapezoidal rule for each row of a 3x3 ones matrix + >>> # with different arbitrary spacing per row + >>> y = torch.ones(3, 3) + >>> x = torch.tensor([[1, 2, 3], [1, 3, 5], [1, 4, 7]]) + >>> torch.cumulative_trapezoid(y, x) + tensor([[1., 2.], + [2., 4.], + [3., 6.]]) + """ + +def deg2rad(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + deg2rad(input, *, out=None) -> Tensor + + Returns a new tensor with each of the elements of :attr:`input` + converted from angles in degrees to radians. + + Args: + input (Tensor): the input tensor. + + Keyword arguments: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([[180.0, -180.0], [360.0, -360.0], [90.0, -90.0]]) + >>> torch.deg2rad(a) + tensor([[ 3.1416, -3.1416], + [ 6.2832, -6.2832], + [ 1.5708, -1.5708]]) + """ + +def deg2rad_(input: Tensor) -> Tensor: ... +@overload +def dequantize(input: Tensor) -> Tensor: + r""" + dequantize(tensor) -> Tensor + + Returns an fp32 Tensor by dequantizing a quantized Tensor + + Args: + tensor (Tensor): A quantized Tensor + + .. function:: dequantize(tensors) -> sequence of Tensors + :noindex: + + Given a list of quantized Tensors, dequantize them and return a list of fp32 Tensors + + Args: + tensors (sequence of Tensors): A list of quantized Tensors + """ + +@overload +def dequantize( + tensors: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + dequantize(tensor) -> Tensor + + Returns an fp32 Tensor by dequantizing a quantized Tensor + + Args: + tensor (Tensor): A quantized Tensor + + .. function:: dequantize(tensors) -> sequence of Tensors + :noindex: + + Given a list of quantized Tensors, dequantize them and return a list of fp32 Tensors + + Args: + tensors (sequence of Tensors): A list of quantized Tensors + """ + +def det(input: Tensor) -> Tensor: + r""" + det(input) -> Tensor + + Alias for :func:`torch.linalg.det` + """ + +def detach(input: Tensor) -> Tensor: ... +def detach_(input: Tensor) -> Tensor: ... +def detach_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + Performs the same operation as :func:`torch.detach`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def diag( + input: Tensor, + diagonal: _int = 0, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + diag(input, diagonal=0, *, out=None) -> Tensor + + - If :attr:`input` is a vector (1-D tensor), then returns a 2-D square tensor + with the elements of :attr:`input` as the diagonal. + - If :attr:`input` is a matrix (2-D tensor), then returns a 1-D tensor with + the diagonal elements of :attr:`input`. + + The argument :attr:`diagonal` controls which diagonal to consider: + + - If :attr:`diagonal` = 0, it is the main diagonal. + - If :attr:`diagonal` > 0, it is above the main diagonal. + - If :attr:`diagonal` < 0, it is below the main diagonal. + + Args: + input (Tensor): the input tensor. + diagonal (int, optional): the diagonal to consider + + Keyword args: + out (Tensor, optional): the output tensor. + + .. seealso:: + + :func:`torch.diagonal` always returns the diagonal of its input. + + :func:`torch.diagflat` always constructs a tensor with diagonal elements + specified by the input. + + Examples: + + Get the square matrix where the input vector is the diagonal:: + + >>> a = torch.randn(3) + >>> a + tensor([ 0.5950,-0.0872, 2.3298]) + >>> torch.diag(a) + tensor([[ 0.5950, 0.0000, 0.0000], + [ 0.0000,-0.0872, 0.0000], + [ 0.0000, 0.0000, 2.3298]]) + >>> torch.diag(a, 1) + tensor([[ 0.0000, 0.5950, 0.0000, 0.0000], + [ 0.0000, 0.0000,-0.0872, 0.0000], + [ 0.0000, 0.0000, 0.0000, 2.3298], + [ 0.0000, 0.0000, 0.0000, 0.0000]]) + + Get the k-th diagonal of a given matrix:: + + >>> a = torch.randn(3, 3) + >>> a + tensor([[-0.4264, 0.0255,-0.1064], + [ 0.8795,-0.2429, 0.1374], + [ 0.1029,-0.6482,-1.6300]]) + >>> torch.diag(a, 0) + tensor([-0.4264,-0.2429,-1.6300]) + >>> torch.diag(a, 1) + tensor([ 0.0255, 0.1374]) + """ + +def diag_embed( + input: Tensor, + offset: _int = 0, + dim1: _int = -2, + dim2: _int = -1, +) -> Tensor: + r""" + diag_embed(input, offset=0, dim1=-2, dim2=-1) -> Tensor + + Creates a tensor whose diagonals of certain 2D planes (specified by + :attr:`dim1` and :attr:`dim2`) are filled by :attr:`input`. + To facilitate creating batched diagonal matrices, the 2D planes formed by + the last two dimensions of the returned tensor are chosen by default. + + The argument :attr:`offset` controls which diagonal to consider: + + - If :attr:`offset` = 0, it is the main diagonal. + - If :attr:`offset` > 0, it is above the main diagonal. + - If :attr:`offset` < 0, it is below the main diagonal. + + The size of the new matrix will be calculated to make the specified diagonal + of the size of the last input dimension. + Note that for :attr:`offset` other than :math:`0`, the order of :attr:`dim1` + and :attr:`dim2` matters. Exchanging them is equivalent to changing the + sign of :attr:`offset`. + + Applying :meth:`torch.diagonal` to the output of this function with + the same arguments yields a matrix identical to input. However, + :meth:`torch.diagonal` has different default dimensions, so those + need to be explicitly specified. + + Args: + input (Tensor): the input tensor. Must be at least 1-dimensional. + offset (int, optional): which diagonal to consider. Default: 0 + (main diagonal). + dim1 (int, optional): first dimension with respect to which to + take diagonal. Default: -2. + dim2 (int, optional): second dimension with respect to which to + take diagonal. Default: -1. + + Example:: + + >>> a = torch.randn(2, 3) + >>> torch.diag_embed(a) + tensor([[[ 1.5410, 0.0000, 0.0000], + [ 0.0000, -0.2934, 0.0000], + [ 0.0000, 0.0000, -2.1788]], + + [[ 0.5684, 0.0000, 0.0000], + [ 0.0000, -1.0845, 0.0000], + [ 0.0000, 0.0000, -1.3986]]]) + + >>> torch.diag_embed(a, offset=1, dim1=0, dim2=2) + tensor([[[ 0.0000, 1.5410, 0.0000, 0.0000], + [ 0.0000, 0.5684, 0.0000, 0.0000]], + + [[ 0.0000, 0.0000, -0.2934, 0.0000], + [ 0.0000, 0.0000, -1.0845, 0.0000]], + + [[ 0.0000, 0.0000, 0.0000, -2.1788], + [ 0.0000, 0.0000, 0.0000, -1.3986]], + + [[ 0.0000, 0.0000, 0.0000, 0.0000], + [ 0.0000, 0.0000, 0.0000, 0.0000]]]) + """ + +def diagflat(input: Tensor, offset: _int = 0) -> Tensor: + r""" + diagflat(input, offset=0) -> Tensor + + - If :attr:`input` is a vector (1-D tensor), then returns a 2-D square tensor + with the elements of :attr:`input` as the diagonal. + - If :attr:`input` is a tensor with more than one dimension, then returns a + 2-D tensor with diagonal elements equal to a flattened :attr:`input`. + + The argument :attr:`offset` controls which diagonal to consider: + + - If :attr:`offset` = 0, it is the main diagonal. + - If :attr:`offset` > 0, it is above the main diagonal. + - If :attr:`offset` < 0, it is below the main diagonal. + + Args: + input (Tensor): the input tensor. + offset (int, optional): the diagonal to consider. Default: 0 (main + diagonal). + + Examples:: + + >>> a = torch.randn(3) + >>> a + tensor([-0.2956, -0.9068, 0.1695]) + >>> torch.diagflat(a) + tensor([[-0.2956, 0.0000, 0.0000], + [ 0.0000, -0.9068, 0.0000], + [ 0.0000, 0.0000, 0.1695]]) + >>> torch.diagflat(a, 1) + tensor([[ 0.0000, -0.2956, 0.0000, 0.0000], + [ 0.0000, 0.0000, -0.9068, 0.0000], + [ 0.0000, 0.0000, 0.0000, 0.1695], + [ 0.0000, 0.0000, 0.0000, 0.0000]]) + + >>> a = torch.randn(2, 2) + >>> a + tensor([[ 0.2094, -0.3018], + [-0.1516, 1.9342]]) + >>> torch.diagflat(a) + tensor([[ 0.2094, 0.0000, 0.0000, 0.0000], + [ 0.0000, -0.3018, 0.0000, 0.0000], + [ 0.0000, 0.0000, -0.1516, 0.0000], + [ 0.0000, 0.0000, 0.0000, 1.9342]]) + """ + +@overload +def diagonal( + input: Tensor, + offset: _int = 0, + dim1: _int = 0, + dim2: _int = 1, +) -> Tensor: + r""" + diagonal(input, offset=0, dim1=0, dim2=1) -> Tensor + + Returns a partial view of :attr:`input` with the its diagonal elements + with respect to :attr:`dim1` and :attr:`dim2` appended as a dimension + at the end of the shape. + + The argument :attr:`offset` controls which diagonal to consider: + + - If :attr:`offset` = 0, it is the main diagonal. + - If :attr:`offset` > 0, it is above the main diagonal. + - If :attr:`offset` < 0, it is below the main diagonal. + + Applying :meth:`torch.diag_embed` to the output of this function with + the same arguments yields a diagonal matrix with the diagonal entries + of the input. However, :meth:`torch.diag_embed` has different default + dimensions, so those need to be explicitly specified. + + Args: + input (Tensor): the input tensor. Must be at least 2-dimensional. + offset (int, optional): which diagonal to consider. Default: 0 + (main diagonal). + dim1 (int, optional): first dimension with respect to which to + take diagonal. Default: 0. + dim2 (int, optional): second dimension with respect to which to + take diagonal. Default: 1. + + .. note:: To take a batch diagonal, pass in dim1=-2, dim2=-1. + + Examples:: + + >>> a = torch.randn(3, 3) + >>> a + tensor([[-1.0854, 1.1431, -0.1752], + [ 0.8536, -0.0905, 0.0360], + [ 0.6927, -0.3735, -0.4945]]) + + + >>> torch.diagonal(a) + tensor([-1.0854, -0.0905, -0.4945]) + + + >>> torch.diagonal(a, 1) + tensor([ 1.1431, 0.0360]) + + >>> b = torch.randn(2, 5) + >>> b + tensor([[-1.7948, -1.2731, -0.3181, 2.0200, -1.6745], + [ 1.8262, -1.5049, 0.4114, 1.0704, -1.2607]]) + + >>> torch.diagonal(b, 1, 1, 0) + tensor([1.8262]) + + >>> x = torch.randn(2, 5, 4, 2) + >>> torch.diagonal(x, offset=-1, dim1=1, dim2=2) + tensor([[[-1.2631, 0.3755, -1.5977, -1.8172], + [-1.1065, 1.0401, -0.2235, -0.7938]], + + [[-1.7325, -0.3081, 0.6166, 0.2335], + [ 1.0500, 0.7336, -0.3836, -1.1015]]]) + """ + +@overload +def diagonal( + input: Tensor, + *, + outdim: str | EllipsisType | None, + dim1: str | EllipsisType | None, + dim2: str | EllipsisType | None, + offset: _int = 0, +) -> Tensor: + r""" + diagonal(input, offset=0, dim1=0, dim2=1) -> Tensor + + Returns a partial view of :attr:`input` with the its diagonal elements + with respect to :attr:`dim1` and :attr:`dim2` appended as a dimension + at the end of the shape. + + The argument :attr:`offset` controls which diagonal to consider: + + - If :attr:`offset` = 0, it is the main diagonal. + - If :attr:`offset` > 0, it is above the main diagonal. + - If :attr:`offset` < 0, it is below the main diagonal. + + Applying :meth:`torch.diag_embed` to the output of this function with + the same arguments yields a diagonal matrix with the diagonal entries + of the input. However, :meth:`torch.diag_embed` has different default + dimensions, so those need to be explicitly specified. + + Args: + input (Tensor): the input tensor. Must be at least 2-dimensional. + offset (int, optional): which diagonal to consider. Default: 0 + (main diagonal). + dim1 (int, optional): first dimension with respect to which to + take diagonal. Default: 0. + dim2 (int, optional): second dimension with respect to which to + take diagonal. Default: 1. + + .. note:: To take a batch diagonal, pass in dim1=-2, dim2=-1. + + Examples:: + + >>> a = torch.randn(3, 3) + >>> a + tensor([[-1.0854, 1.1431, -0.1752], + [ 0.8536, -0.0905, 0.0360], + [ 0.6927, -0.3735, -0.4945]]) + + + >>> torch.diagonal(a) + tensor([-1.0854, -0.0905, -0.4945]) + + + >>> torch.diagonal(a, 1) + tensor([ 1.1431, 0.0360]) + + >>> b = torch.randn(2, 5) + >>> b + tensor([[-1.7948, -1.2731, -0.3181, 2.0200, -1.6745], + [ 1.8262, -1.5049, 0.4114, 1.0704, -1.2607]]) + + >>> torch.diagonal(b, 1, 1, 0) + tensor([1.8262]) + + >>> x = torch.randn(2, 5, 4, 2) + >>> torch.diagonal(x, offset=-1, dim1=1, dim2=2) + tensor([[[-1.2631, 0.3755, -1.5977, -1.8172], + [-1.1065, 1.0401, -0.2235, -0.7938]], + + [[-1.7325, -0.3081, 0.6166, 0.2335], + [ 1.0500, 0.7336, -0.3836, -1.1015]]]) + """ + +def diagonal_copy( + input: Tensor, + offset: _int = 0, + dim1: _int = 0, + dim2: _int = 1, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.diagonal`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def diagonal_scatter( + input: Tensor, + src: Tensor, + offset: _int = 0, + dim1: _int = 0, + dim2: _int = 1, +) -> Tensor: + r""" + diagonal_scatter(input, src, offset=0, dim1=0, dim2=1) -> Tensor + + Embeds the values of the :attr:`src` tensor into :attr:`input` along + the diagonal elements of :attr:`input`, with respect to :attr:`dim1` + and :attr:`dim2`. + + This function returns a tensor with fresh storage; it does not + return a view. + + The argument :attr:`offset` controls which diagonal to consider: + + - If :attr:`offset` = 0, it is the main diagonal. + - If :attr:`offset` > 0, it is above the main diagonal. + - If :attr:`offset` < 0, it is below the main diagonal. + + Args: + input (Tensor): the input tensor. Must be at least 2-dimensional. + src (Tensor): the tensor to embed into :attr:`input`. + offset (int, optional): which diagonal to consider. Default: 0 + (main diagonal). + dim1 (int, optional): first dimension with respect to which to + take diagonal. Default: 0. + dim2 (int, optional): second dimension with respect to which to + take diagonal. Default: 1. + + .. note:: + + :attr:`src` must be of the proper size in order to be embedded + into :attr:`input`. Specifically, it should have the same shape as + ``torch.diagonal(input, offset, dim1, dim2)`` + + Examples:: + + >>> a = torch.zeros(3, 3) + >>> a + tensor([[0., 0., 0.], + [0., 0., 0.], + [0., 0., 0.]]) + + >>> torch.diagonal_scatter(a, torch.ones(3), 0) + tensor([[1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.]]) + + >>> torch.diagonal_scatter(a, torch.ones(2), 1) + tensor([[0., 1., 0.], + [0., 0., 1.], + [0., 0., 0.]]) + """ + +def diff( + input: Tensor, + n: _int = 1, + dim: _int = -1, + prepend: Tensor | None = None, + append: Tensor | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + diff(input, n=1, dim=-1, prepend=None, append=None) -> Tensor + + Computes the n-th forward difference along the given dimension. + + The first-order differences are given by `out[i] = input[i + 1] - input[i]`. Higher-order + differences are calculated by using :func:`torch.diff` recursively. + + Args: + input (Tensor): the tensor to compute the differences on + n (int, optional): the number of times to recursively compute the difference + dim (int, optional): the dimension to compute the difference along. + Default is the last dimension. + prepend, append (Tensor, optional): values to prepend or append to + :attr:`input` along :attr:`dim` before computing the difference. + Their dimensions must be equivalent to that of input, and their shapes + must match input's shape except on :attr:`dim`. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([1, 3, 2]) + >>> torch.diff(a) + tensor([ 2, -1]) + >>> b = torch.tensor([4, 5]) + >>> torch.diff(a, append=b) + tensor([ 2, -1, 2, 1]) + >>> c = torch.tensor([[1, 2, 3], [3, 4, 5]]) + >>> torch.diff(c, dim=0) + tensor([[2, 2, 2]]) + >>> torch.diff(c, dim=1) + tensor([[1, 1], + [1, 1]]) + """ + +def digamma(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + digamma(input, *, out=None) -> Tensor + + Alias for :func:`torch.special.digamma`. + """ + +def dist(input: Tensor, other: Tensor, p: Number | _complex = 2) -> Tensor: + r""" + dist(input, other, p=2) -> Tensor + + Returns the p-norm of (:attr:`input` - :attr:`other`) + + The shapes of :attr:`input` and :attr:`other` must be + :ref:`broadcastable `. + + Args: + input (Tensor): the input tensor. + other (Tensor): the Right-hand-side input tensor + p (float, optional): the norm to be computed + + Example:: + + >>> x = torch.randn(4) + >>> x + tensor([-1.5393, -0.8675, 0.5916, 1.6321]) + >>> y = torch.randn(4) + >>> y + tensor([ 0.0967, -1.0511, 0.6295, 0.8360]) + >>> torch.dist(x, y, 3.5) + tensor(1.6727) + >>> torch.dist(x, y, 3) + tensor(1.6973) + >>> torch.dist(x, y, 0) + tensor(4.) + >>> torch.dist(x, y, 1) + tensor(2.6537) + """ + +def div( + input: Tensor | Number, + other: Tensor | Number, + *, + rounding_mode: str | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + div(input, other, *, rounding_mode=None, out=None) -> Tensor + + Divides each element of the input ``input`` by the corresponding element of + :attr:`other`. + + .. math:: + \text{out}_i = \frac{\text{input}_i}{\text{other}_i} + + .. note:: + By default, this performs a "true" division like Python 3. + See the :attr:`rounding_mode` argument for floor division. + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer, float, and complex inputs. + Always promotes integer types to the default scalar type. + + Args: + input (Tensor): the dividend + other (Tensor or Number): the divisor + + Keyword args: + rounding_mode (str, optional): Type of rounding applied to the result: + + * None - default behavior. Performs no rounding and, if both :attr:`input` and + :attr:`other` are integer types, promotes the inputs to the default scalar type. + Equivalent to true division in Python (the ``/`` operator) and NumPy's ``np.true_divide``. + * ``"trunc"`` - rounds the results of the division towards zero. + Equivalent to C-style integer division. + * ``"floor"`` - rounds the results of the division down. + Equivalent to floor division in Python (the ``//`` operator) and NumPy's ``np.floor_divide``. + + out (Tensor, optional): the output tensor. + + Examples:: + + >>> x = torch.tensor([ 0.3810, 1.2774, -0.2972, -0.3719, 0.4637]) + >>> torch.div(x, 0.5) + tensor([ 0.7620, 2.5548, -0.5944, -0.7438, 0.9274]) + + >>> a = torch.tensor([[-0.3711, -1.9353, -0.4605, -0.2917], + ... [ 0.1815, -1.0111, 0.9805, -1.5923], + ... [ 0.1062, 1.4581, 0.7759, -1.2344], + ... [-0.1830, -0.0313, 1.1908, -1.4757]]) + >>> b = torch.tensor([ 0.8032, 0.2930, -0.8113, -0.2308]) + >>> torch.div(a, b) + tensor([[-0.4620, -6.6051, 0.5676, 1.2639], + [ 0.2260, -3.4509, -1.2086, 6.8990], + [ 0.1322, 4.9764, -0.9564, 5.3484], + [-0.2278, -0.1068, -1.4678, 6.3938]]) + + >>> torch.div(a, b, rounding_mode='trunc') + tensor([[-0., -6., 0., 1.], + [ 0., -3., -1., 6.], + [ 0., 4., -0., 5.], + [-0., -0., -1., 6.]]) + + >>> torch.div(a, b, rounding_mode='floor') + tensor([[-1., -7., 0., 1.], + [ 0., -4., -2., 6.], + [ 0., 4., -1., 5.], + [-1., -1., -2., 6.]]) + """ + +@overload +def divide( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + divide(input, other, *, rounding_mode=None, out=None) -> Tensor + + Alias for :func:`torch.div`. + """ + +@overload +def divide( + input: Tensor, + other: Tensor, + *, + rounding_mode: str | None, + out: Tensor | None = None, +) -> Tensor: + r""" + divide(input, other, *, rounding_mode=None, out=None) -> Tensor + + Alias for :func:`torch.div`. + """ + +@overload +def divide( + input: Tensor, + other: Number | _complex, + *, + rounding_mode: str | None, +) -> Tensor: + r""" + divide(input, other, *, rounding_mode=None, out=None) -> Tensor + + Alias for :func:`torch.div`. + """ + +@overload +def divide(input: Tensor, other: Number | _complex) -> Tensor: + r""" + divide(input, other, *, rounding_mode=None, out=None) -> Tensor + + Alias for :func:`torch.div`. + """ + +def dot( + input: Tensor, + tensor: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + dot(input, tensor, *, out=None) -> Tensor + + Computes the dot product of two 1D tensors. + + .. note:: + + Unlike NumPy's dot, torch.dot intentionally only supports computing the dot product + of two 1D tensors with the same number of elements. + + Args: + input (Tensor): first tensor in the dot product, must be 1D. + tensor (Tensor): second tensor in the dot product, must be 1D. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.dot(torch.tensor([2, 3]), torch.tensor([2, 1])) + tensor(7) + + >>> t1, t2 = torch.tensor([0, 1]), torch.tensor([2, 3]) + >>> torch.dot(t1, t2) + tensor(3) + """ + +def dropout(input: Tensor, p: _float, train: _bool) -> Tensor: ... +def dropout_(input: Tensor, p: _float, train: _bool) -> Tensor: ... +def dsmm(input: Tensor, mat2: Tensor) -> Tensor: ... +@overload +def dsplit(input: Tensor, sections: _int) -> tuple[Tensor, ...]: + r""" + dsplit(input, indices_or_sections) -> List of Tensors + + Splits :attr:`input`, a tensor with three or more dimensions, into multiple tensors + depthwise according to :attr:`indices_or_sections`. Each split is a view of + :attr:`input`. + + This is equivalent to calling torch.tensor_split(input, indices_or_sections, dim=2) + (the split dimension is 2), except that if :attr:`indices_or_sections` is an integer + it must evenly divide the split dimension or a runtime error will be thrown. + + This function is based on NumPy's :func:`numpy.dsplit`. + + Args: + input (Tensor): tensor to split. + indices_or_sections (int or list or tuple of ints): See argument in :func:`torch.tensor_split`. + + Example:: + + >>> t = torch.arange(16.0).reshape(2, 2, 4) + >>> t + tensor([[[ 0., 1., 2., 3.], + [ 4., 5., 6., 7.]], + [[ 8., 9., 10., 11.], + [12., 13., 14., 15.]]]) + >>> torch.dsplit(t, 2) + (tensor([[[ 0., 1.], + [ 4., 5.]], + [[ 8., 9.], + [12., 13.]]]), + tensor([[[ 2., 3.], + [ 6., 7.]], + [[10., 11.], + [14., 15.]]])) + + >>> torch.dsplit(t, [3, 6]) + (tensor([[[ 0., 1., 2.], + [ 4., 5., 6.]], + [[ 8., 9., 10.], + [12., 13., 14.]]]), + tensor([[[ 3.], + [ 7.]], + [[11.], + [15.]]]), + tensor([], size=(2, 2, 0))) + """ + +@overload +def dsplit(input: Tensor, indices: _size) -> tuple[Tensor, ...]: + r""" + dsplit(input, indices_or_sections) -> List of Tensors + + Splits :attr:`input`, a tensor with three or more dimensions, into multiple tensors + depthwise according to :attr:`indices_or_sections`. Each split is a view of + :attr:`input`. + + This is equivalent to calling torch.tensor_split(input, indices_or_sections, dim=2) + (the split dimension is 2), except that if :attr:`indices_or_sections` is an integer + it must evenly divide the split dimension or a runtime error will be thrown. + + This function is based on NumPy's :func:`numpy.dsplit`. + + Args: + input (Tensor): tensor to split. + indices_or_sections (int or list or tuple of ints): See argument in :func:`torch.tensor_split`. + + Example:: + + >>> t = torch.arange(16.0).reshape(2, 2, 4) + >>> t + tensor([[[ 0., 1., 2., 3.], + [ 4., 5., 6., 7.]], + [[ 8., 9., 10., 11.], + [12., 13., 14., 15.]]]) + >>> torch.dsplit(t, 2) + (tensor([[[ 0., 1.], + [ 4., 5.]], + [[ 8., 9.], + [12., 13.]]]), + tensor([[[ 2., 3.], + [ 6., 7.]], + [[10., 11.], + [14., 15.]]])) + + >>> torch.dsplit(t, [3, 6]) + (tensor([[[ 0., 1., 2.], + [ 4., 5., 6.]], + [[ 8., 9., 10.], + [12., 13., 14.]]]), + tensor([[[ 3.], + [ 7.]], + [[11.], + [15.]]]), + tensor([], size=(2, 2, 0))) + """ + +def dstack( + tensors: tuple[Tensor, ...] | list[Tensor], + *, + out: Tensor | None = None, +) -> Tensor: + r""" + dstack(tensors, *, out=None) -> Tensor + + Stack tensors in sequence depthwise (along third axis). + + This is equivalent to concatenation along the third axis after 1-D and 2-D tensors have been reshaped by :func:`torch.atleast_3d`. + + Args: + tensors (sequence of Tensors): sequence of tensors to concatenate + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([1, 2, 3]) + >>> b = torch.tensor([4, 5, 6]) + >>> torch.dstack((a,b)) + tensor([[[1, 4], + [2, 5], + [3, 6]]]) + >>> a = torch.tensor([[1],[2],[3]]) + >>> b = torch.tensor([[4],[5],[6]]) + >>> torch.dstack((a,b)) + tensor([[[1, 4]], + [[2, 5]], + [[3, 6]]]) + """ + +def embedding( + weight: Tensor, + indices: Tensor, + padding_idx: _int | SymInt = -1, + scale_grad_by_freq: _bool = False, + sparse: _bool = False, +) -> Tensor: ... +@overload +def embedding_bag( + weight: Tensor, + indices: Tensor, + offsets: Tensor, + scale_grad_by_freq: _bool, + mode: _int, + sparse: _bool, + per_sample_weights: Tensor | None, + include_last_offset: _bool, + padding_idx: _int | None, +) -> tuple[Tensor, Tensor, Tensor, Tensor]: ... +@overload +def embedding_bag( + weight: Tensor, + indices: Tensor, + offsets: Tensor, + scale_grad_by_freq: _bool = False, + mode: _int = 0, + sparse: _bool = False, + per_sample_weights: Tensor | None = None, + include_last_offset: _bool = False, +) -> tuple[Tensor, Tensor, Tensor, Tensor]: ... +def embedding_renorm_( + input: Tensor, + indices: Tensor, + max_norm: _float, + norm_type: _float, +) -> Tensor: ... +@overload +def empty( + size: Sequence[_int | SymInt], + *, + memory_format: memory_format | None = None, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + empty(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False, memory_format=torch.contiguous_format) -> Tensor + + Returns a tensor filled with uninitialized data. The shape of the tensor is + defined by the variable argument :attr:`size`. + + .. note:: + If :func:`torch.use_deterministic_algorithms()` and + :attr:`torch.utils.deterministic.fill_uninitialized_memory` are both set to + ``True``, the output tensor is initialized to prevent any possible + nondeterministic behavior from using the data as an input to an operation. + Floating point and complex tensors are filled with NaN, and integer tensors + are filled with the maximum value. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.contiguous_format``. + + Example:: + + >>> torch.empty((2,3), dtype=torch.int64) + tensor([[ 9.4064e+13, 2.8000e+01, 9.3493e+13], + [ 7.5751e+18, 7.1428e+18, 7.5955e+18]]) + """ + +@overload +def empty( + *size: _int | SymInt, + memory_format: memory_format | None = None, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + empty(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False, memory_format=torch.contiguous_format) -> Tensor + + Returns a tensor filled with uninitialized data. The shape of the tensor is + defined by the variable argument :attr:`size`. + + .. note:: + If :func:`torch.use_deterministic_algorithms()` and + :attr:`torch.utils.deterministic.fill_uninitialized_memory` are both set to + ``True``, the output tensor is initialized to prevent any possible + nondeterministic behavior from using the data as an input to an operation. + Floating point and complex tensors are filled with NaN, and integer tensors + are filled with the maximum value. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.contiguous_format``. + + Example:: + + >>> torch.empty((2,3), dtype=torch.int64) + tensor([[ 9.4064e+13, 2.8000e+01, 9.3493e+13], + [ 7.5751e+18, 7.1428e+18, 7.5955e+18]]) + """ + +@overload +def empty( + size: _size, + *, + names: Sequence[str | EllipsisType | None] | None, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + empty(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False, memory_format=torch.contiguous_format) -> Tensor + + Returns a tensor filled with uninitialized data. The shape of the tensor is + defined by the variable argument :attr:`size`. + + .. note:: + If :func:`torch.use_deterministic_algorithms()` and + :attr:`torch.utils.deterministic.fill_uninitialized_memory` are both set to + ``True``, the output tensor is initialized to prevent any possible + nondeterministic behavior from using the data as an input to an operation. + Floating point and complex tensors are filled with NaN, and integer tensors + are filled with the maximum value. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.contiguous_format``. + + Example:: + + >>> torch.empty((2,3), dtype=torch.int64) + tensor([[ 9.4064e+13, 2.8000e+01, 9.3493e+13], + [ 7.5751e+18, 7.1428e+18, 7.5955e+18]]) + """ + +@overload +def empty( + *size: _int, + names: Sequence[str | EllipsisType | None] | None, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + empty(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False, memory_format=torch.contiguous_format) -> Tensor + + Returns a tensor filled with uninitialized data. The shape of the tensor is + defined by the variable argument :attr:`size`. + + .. note:: + If :func:`torch.use_deterministic_algorithms()` and + :attr:`torch.utils.deterministic.fill_uninitialized_memory` are both set to + ``True``, the output tensor is initialized to prevent any possible + nondeterministic behavior from using the data as an input to an operation. + Floating point and complex tensors are filled with NaN, and integer tensors + are filled with the maximum value. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.contiguous_format``. + + Example:: + + >>> torch.empty((2,3), dtype=torch.int64) + tensor([[ 9.4064e+13, 2.8000e+01, 9.3493e+13], + [ 7.5751e+18, 7.1428e+18, 7.5955e+18]]) + """ + +def empty_like( + input: Tensor, + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + empty_like(input, *, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns an uninitialized tensor with the same size as :attr:`input`. + ``torch.empty_like(input)`` is equivalent to + ``torch.empty(input.size(), dtype=input.dtype, layout=input.layout, device=input.device)``. + + .. note:: + If :func:`torch.use_deterministic_algorithms()` and + :attr:`torch.utils.deterministic.fill_uninitialized_memory` are both set to + ``True``, the output tensor is initialized to prevent any possible + nondeterministic behavior from using the data as an input to an operation. + Floating point and complex tensors are filled with NaN, and integer tensors + are filled with the maximum value. + + When ``torch.preserve_format`` is used: + If the input tensor is dense (i.e., non-overlapping strided), + its memory format (including strides) is retained. + Otherwise (e.g., a non-dense view like a stepped slice), + the output is converted to the dense format. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + + Example:: + + >>> a=torch.empty((2,3), dtype=torch.int32, device = 'cuda') + >>> torch.empty_like(a) + tensor([[0, 0, 0], + [0, 0, 0]], device='cuda:0', dtype=torch.int32) + """ + +def empty_permuted( + size: Sequence[_int | SymInt], + physical_layout: _size, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + empty_permuted(size, physical_layout, *, dtype=None, layout=None, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Creates an uninitialized, non-overlapping and dense tensor with the + specified :attr:`size`, with :attr:`physical_layout` specifying how the + dimensions are physically laid out in memory (each logical dimension is listed + from outermost to innermost). :attr:`physical_layout` is a generalization + of NCHW/NHWC notation: if each dimension is assigned a number according to + what order they occur in size (N=0, C=1, H=2, W=3), then NCHW is ``(0, 1, 2, 3)`` + while NHWC is ``(0, 2, 3, 1)``. Equivalently, the strides of the output + tensor ``t`` are such that ``t.stride(physical_layout[i]) == contiguous_strides[i]`` + (notably, this function is *not* equivalent to ``torch.empty(size).permute(physical_layout)``). + + Unlike :func:`torch.empty_strided`, this is guaranteed to produce a dense + tensor with no overlaps. If possible, prefer using this function over + :func:`torch.empty_strided` or manual use of :func:`torch.as_strided`. + + .. note:: + If :func:`torch.use_deterministic_algorithms()` and + :attr:`torch.utils.deterministic.fill_uninitialized_memory` are both set to + ``True``, the output tensor is initialized to prevent any possible + nondeterministic behavior from using the data as an input to an operation. + Floating point and complex tensors are filled with NaN, and integer tensors + are filled with the maximum value. + + Args: + size (tuple of int): the shape of the output tensor + physical_layout (tuple of int): the ordering of dimensions physically in memory + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Examples: + + >>> torch.empty((2, 3, 5, 7)).stride() + (105, 35, 7, 1) + >>> torch.empty_permuted((2, 3, 5, 7), (0, 1, 2, 3)).stride() + (105, 35, 7, 1) + >>> torch.empty((2, 3, 5, 7), memory_format=torch.channels_last).stride() + (105, 1, 21, 3) + >>> torch.empty_permuted((2, 3, 5, 7), (0, 2, 3, 1)).stride() + (105, 1, 21, 3) + >>> torch.empty_permuted((2, 3, 5, 7), (0, 2, 3, 1)).dim_order() + (0, 2, 3, 1) + """ + +def empty_quantized( + size: _size, + qtensor: Tensor, + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: ... +def empty_strided( + size: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + empty_strided(size, stride, *, dtype=None, layout=None, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Creates a tensor with the specified :attr:`size` and :attr:`stride` and filled with undefined data. + + .. warning:: + If the constructed tensor is "overlapped" (with multiple indices referring to the same element + in memory) its behavior is undefined. + + .. note:: + If :func:`torch.use_deterministic_algorithms()` and + :attr:`torch.utils.deterministic.fill_uninitialized_memory` are both set to + ``True``, the output tensor is initialized to prevent any possible + nondeterministic behavior from using the data as an input to an operation. + Floating point and complex tensors are filled with NaN, and integer tensors + are filled with the maximum value. + + Args: + size (tuple of int): the shape of the output tensor + stride (tuple of int): the strides of the output tensor + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> a = torch.empty_strided((2, 3), (1, 2)) + >>> a + tensor([[8.9683e-44, 4.4842e-44, 5.1239e+07], + [0.0000e+00, 0.0000e+00, 3.0705e-41]]) + >>> a.stride() + (1, 2) + >>> a.size() + torch.Size([2, 3]) + """ + +@overload +def eq( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + eq(input, other, *, out=None) -> Tensor + + Computes element-wise equality + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or float): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is equal to :attr:`other` and False elsewhere + + Example:: + + >>> torch.eq(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[ True, False], + [False, True]]) + """ + +@overload +def eq( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + eq(input, other, *, out=None) -> Tensor + + Computes element-wise equality + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or float): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is equal to :attr:`other` and False elsewhere + + Example:: + + >>> torch.eq(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[ True, False], + [False, True]]) + """ + +def equal(input: Tensor, other: Tensor) -> _bool: + r""" + equal(input, other) -> bool + + ``True`` if two tensors have the same size and elements, ``False`` otherwise. + + .. note:: + + Tensors containing NaNs are never equal to each other. Additionally, this function does not + differentiate between the data types of the tensors during comparison. For more thorough tensor checks, + use :meth:`torch.testing.assert_close`. + + Example:: + + >>> torch.equal(torch.tensor([1, 2]), torch.tensor([1, 2])) + True + >>> torch.equal(torch.tensor([3, torch.nan]), torch.tensor([3, torch.nan])) + False + >>> torch.equal(torch.tensor([1, 2, 3], dtype=torch.int32), torch.tensor([1, 2, 3], dtype=torch.float32)) + True + """ + +def erf(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + erf(input, *, out=None) -> Tensor + + Alias for :func:`torch.special.erf`. + """ + +def erf_(input: Tensor) -> Tensor: ... +def erfc(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + erfc(input, *, out=None) -> Tensor + + Alias for :func:`torch.special.erfc`. + """ + +def erfc_(input: Tensor) -> Tensor: ... +def erfinv(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + erfinv(input, *, out=None) -> Tensor + + Alias for :func:`torch.special.erfinv`. + """ + +def exp(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + exp(input, *, out=None) -> Tensor + + Returns a new tensor with the exponential of the elements + of the input tensor :attr:`input`. + + .. math:: + y_{i} = e^{x_{i}} + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.exp(torch.tensor([0, math.log(2.)])) + tensor([ 1., 2.]) + """ + +def exp2(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + exp2(input, *, out=None) -> Tensor + + Alias for :func:`torch.special.exp2`. + """ + +def exp2_(input: Tensor) -> Tensor: ... +def exp_(input: Tensor) -> Tensor: ... +def expand_copy( + input: Tensor, + size: Sequence[_int | SymInt], + *, + implicit: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.Tensor.expand`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def expm1(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + expm1(input, *, out=None) -> Tensor + + Alias for :func:`torch.special.expm1`. + """ + +def expm1_(input: Tensor) -> Tensor: ... +@overload +def eye( + n: _int | SymInt, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + eye(n, m=None, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a 2-D tensor with ones on the diagonal and zeros elsewhere. + + Args: + n (int): the number of rows + m (int, optional): the number of columns with default being :attr:`n` + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 2-D tensor with ones on the diagonal and zeros elsewhere + + Example:: + + >>> torch.eye(3) + tensor([[ 1., 0., 0.], + [ 0., 1., 0.], + [ 0., 0., 1.]]) + """ + +@overload +def eye( + n: _int | SymInt, + m: _int | SymInt, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + eye(n, m=None, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a 2-D tensor with ones on the diagonal and zeros elsewhere. + + Args: + n (int): the number of rows + m (int, optional): the number of columns with default being :attr:`n` + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 2-D tensor with ones on the diagonal and zeros elsewhere + + Example:: + + >>> torch.eye(3) + tensor([[ 1., 0., 0.], + [ 0., 1., 0.], + [ 0., 0., 1.]]) + """ + +def fake_quantize_per_channel_affine( + input: Tensor, + scale: Tensor, + zero_point: Tensor, + axis: _int, + quant_min: _int, + quant_max: _int, +) -> Tensor: + r""" + fake_quantize_per_channel_affine(input, scale, zero_point, axis, quant_min, quant_max) -> Tensor + + Returns a new tensor with the data in :attr:`input` fake quantized per channel using :attr:`scale`, + :attr:`zero_point`, :attr:`quant_min` and :attr:`quant_max`, across the channel specified by :attr:`axis`. + + .. math:: + \text{output} = ( + min( + \text{quant\_max}, + max( + \text{quant\_min}, + \text{std::nearby\_int}(\text{input} / \text{scale}) + \text{zero\_point} + ) + ) - \text{zero\_point} + ) \times \text{scale} + + Args: + input (Tensor): the input value(s), in ``torch.float32`` + scale (Tensor): quantization scale, per channel in ``torch.float32`` + zero_point (Tensor): quantization zero_point, per channel in ``torch.int32`` or ``torch.half`` or ``torch.float32`` + axis (int32): channel axis + quant_min (int64): lower bound of the quantized domain + quant_max (int64): upper bound of the quantized domain + + Returns: + Tensor: A newly fake_quantized per channel ``torch.float32`` tensor + + Example:: + + >>> x = torch.randn(2, 2, 2) + >>> x + tensor([[[-0.2525, -0.0466], + [ 0.3491, -0.2168]], + + [[-0.5906, 1.6258], + [ 0.6444, -0.0542]]]) + >>> scales = (torch.randn(2) + 1) * 0.05 + >>> scales + tensor([0.0475, 0.0486]) + >>> zero_points = torch.zeros(2).to(torch.int32) + >>> zero_points + tensor([0, 0]) + >>> torch.fake_quantize_per_channel_affine(x, scales, zero_points, 1, 0, 255) + tensor([[[0.0000, 0.0000], + [0.3405, 0.0000]], + + [[0.0000, 1.6134], + [0.6323, 0.0000]]]) + """ + +@overload +def fake_quantize_per_tensor_affine( + input: Tensor, + scale: _float, + zero_point: _int, + quant_min: _int, + quant_max: _int, +) -> Tensor: + r""" + fake_quantize_per_tensor_affine(input, scale, zero_point, quant_min, quant_max) -> Tensor + + Returns a new tensor with the data in :attr:`input` fake quantized using :attr:`scale`, + :attr:`zero_point`, :attr:`quant_min` and :attr:`quant_max`. + + .. math:: + \text{output} = ( + min( + \text{quant\_max}, + max( + \text{quant\_min}, + \text{std::nearby\_int}(\text{input} / \text{scale}) + \text{zero\_point} + ) + ) - \text{zero\_point} + ) \times \text{scale} + + Args: + input (Tensor): the input value(s), ``torch.float32`` tensor + scale (double scalar or ``float32`` Tensor): quantization scale + zero_point (int64 scalar or ``int32`` Tensor): quantization zero_point + quant_min (int64): lower bound of the quantized domain + quant_max (int64): upper bound of the quantized domain + + Returns: + Tensor: A newly fake_quantized ``torch.float32`` tensor + + Example:: + + >>> x = torch.randn(4) + >>> x + tensor([ 0.0552, 0.9730, 0.3973, -1.0780]) + >>> torch.fake_quantize_per_tensor_affine(x, 0.1, 0, 0, 255) + tensor([0.1000, 1.0000, 0.4000, 0.0000]) + >>> torch.fake_quantize_per_tensor_affine(x, torch.tensor(0.1), torch.tensor(0), 0, 255) + tensor([0.1000, 1.0000, 0.4000, 0.0000]) + """ + +@overload +def fake_quantize_per_tensor_affine( + input: Tensor, + scale: Tensor, + zero_point: Tensor, + quant_min: _int, + quant_max: _int, +) -> Tensor: + r""" + fake_quantize_per_tensor_affine(input, scale, zero_point, quant_min, quant_max) -> Tensor + + Returns a new tensor with the data in :attr:`input` fake quantized using :attr:`scale`, + :attr:`zero_point`, :attr:`quant_min` and :attr:`quant_max`. + + .. math:: + \text{output} = ( + min( + \text{quant\_max}, + max( + \text{quant\_min}, + \text{std::nearby\_int}(\text{input} / \text{scale}) + \text{zero\_point} + ) + ) - \text{zero\_point} + ) \times \text{scale} + + Args: + input (Tensor): the input value(s), ``torch.float32`` tensor + scale (double scalar or ``float32`` Tensor): quantization scale + zero_point (int64 scalar or ``int32`` Tensor): quantization zero_point + quant_min (int64): lower bound of the quantized domain + quant_max (int64): upper bound of the quantized domain + + Returns: + Tensor: A newly fake_quantized ``torch.float32`` tensor + + Example:: + + >>> x = torch.randn(4) + >>> x + tensor([ 0.0552, 0.9730, 0.3973, -1.0780]) + >>> torch.fake_quantize_per_tensor_affine(x, 0.1, 0, 0, 255) + tensor([0.1000, 1.0000, 0.4000, 0.0000]) + >>> torch.fake_quantize_per_tensor_affine(x, torch.tensor(0.1), torch.tensor(0), 0, 255) + tensor([0.1000, 1.0000, 0.4000, 0.0000]) + """ + +@overload +def fbgemm_linear_fp16_weight( + input: Tensor, + packed_weight: Tensor, + bias: Tensor, +) -> Tensor: ... +@overload +def fbgemm_linear_fp16_weight( + input: Tensor, + packed_weight: Tensor, + bias: Tensor, + output: Tensor, +) -> Tensor: ... +@overload +def fbgemm_linear_fp16_weight_fp32_activation( + input: Tensor, + packed_weight: Tensor, + bias: Tensor | None, +) -> Tensor: ... +@overload +def fbgemm_linear_fp16_weight_fp32_activation( + input: Tensor, + packed_weight: Tensor, + bias: Tensor | None, + output: Tensor, +) -> Tensor: ... +def fbgemm_linear_int8_weight( + input: Tensor, + weight: Tensor, + packed: Tensor, + col_offsets: Tensor, + weight_scale: Number | _complex, + weight_zero_point: Number | _complex, + bias: Tensor, +) -> Tensor: ... +def fbgemm_linear_int8_weight_fp32_activation( + input: Tensor, + weight: Tensor, + packed: Tensor, + col_offsets: Tensor, + weight_scale: Number | _complex, + weight_zero_point: Number | _complex, + bias: Tensor, +) -> Tensor: ... +def fbgemm_linear_quantize_weight( + input: Tensor, +) -> tuple[Tensor, Tensor, _float, _int]: ... +def fbgemm_pack_gemm_matrix_fp16(input: Tensor) -> Tensor: ... +@overload +def fbgemm_pack_quantized_matrix(input: Tensor) -> Tensor: ... +@overload +def fbgemm_pack_quantized_matrix(input: Tensor, K: _int, N: _int) -> Tensor: ... +def feature_alpha_dropout(input: Tensor, p: _float, train: _bool) -> Tensor: ... +def feature_alpha_dropout_( + input: Tensor, + p: _float, + train: _bool, +) -> Tensor: ... +def feature_dropout(input: Tensor, p: _float, train: _bool) -> Tensor: ... +def feature_dropout_(input: Tensor, p: _float, train: _bool) -> Tensor: ... +@overload +def fill(input: Tensor, value: Tensor) -> Tensor: ... +@overload +def fill(input: Tensor, value: Number | _complex) -> Tensor: ... +@overload +def fill_(input: Tensor, value: Tensor) -> Tensor: ... +@overload +def fill_(input: Tensor, value: Number | _complex) -> Tensor: ... +def fix(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + fix(input, *, out=None) -> Tensor + + Alias for :func:`torch.trunc` + """ + +def fix_(input: Tensor) -> Tensor: ... +@overload +def flatten( + input: Tensor, + start_dim: _int = 0, + end_dim: _int = -1, +) -> Tensor: + r""" + flatten(input, start_dim=0, end_dim=-1) -> Tensor + + Flattens :attr:`input` by reshaping it into a one-dimensional tensor. If :attr:`start_dim` or :attr:`end_dim` + are passed, only dimensions starting with :attr:`start_dim` and ending with :attr:`end_dim` are flattened. + The order of elements in :attr:`input` is unchanged. + + Unlike NumPy's flatten, which always copies input's data, this function may return the original object, a view, + or copy. If no dimensions are flattened, then the original object :attr:`input` is returned. Otherwise, if input can + be viewed as the flattened shape, then that view is returned. Finally, only if the input cannot be viewed as the + flattened shape is input's data copied. See :meth:`torch.Tensor.view` for details on when a view will be returned. + + .. note:: + Flattening a zero-dimensional tensor will return a one-dimensional view. + + Args: + input (Tensor): the input tensor. + start_dim (int): the first dim to flatten + end_dim (int): the last dim to flatten + + Example:: + + >>> t = torch.tensor([[[1, 2], + ... [3, 4]], + ... [[5, 6], + ... [7, 8]]]) + >>> torch.flatten(t) + tensor([1, 2, 3, 4, 5, 6, 7, 8]) + >>> torch.flatten(t, start_dim=1) + tensor([[1, 2, 3, 4], + [5, 6, 7, 8]]) + """ + +@overload +def flatten( + input: Tensor, + start_dim: _int, + end_dim: _int, + out_dim: str | EllipsisType | None, +) -> Tensor: + r""" + flatten(input, start_dim=0, end_dim=-1) -> Tensor + + Flattens :attr:`input` by reshaping it into a one-dimensional tensor. If :attr:`start_dim` or :attr:`end_dim` + are passed, only dimensions starting with :attr:`start_dim` and ending with :attr:`end_dim` are flattened. + The order of elements in :attr:`input` is unchanged. + + Unlike NumPy's flatten, which always copies input's data, this function may return the original object, a view, + or copy. If no dimensions are flattened, then the original object :attr:`input` is returned. Otherwise, if input can + be viewed as the flattened shape, then that view is returned. Finally, only if the input cannot be viewed as the + flattened shape is input's data copied. See :meth:`torch.Tensor.view` for details on when a view will be returned. + + .. note:: + Flattening a zero-dimensional tensor will return a one-dimensional view. + + Args: + input (Tensor): the input tensor. + start_dim (int): the first dim to flatten + end_dim (int): the last dim to flatten + + Example:: + + >>> t = torch.tensor([[[1, 2], + ... [3, 4]], + ... [[5, 6], + ... [7, 8]]]) + >>> torch.flatten(t) + tensor([1, 2, 3, 4, 5, 6, 7, 8]) + >>> torch.flatten(t, start_dim=1) + tensor([[1, 2, 3, 4], + [5, 6, 7, 8]]) + """ + +@overload +def flatten( + input: Tensor, + start_dim: str | EllipsisType | None, + end_dim: str | EllipsisType | None, + out_dim: str | EllipsisType | None, +) -> Tensor: + r""" + flatten(input, start_dim=0, end_dim=-1) -> Tensor + + Flattens :attr:`input` by reshaping it into a one-dimensional tensor. If :attr:`start_dim` or :attr:`end_dim` + are passed, only dimensions starting with :attr:`start_dim` and ending with :attr:`end_dim` are flattened. + The order of elements in :attr:`input` is unchanged. + + Unlike NumPy's flatten, which always copies input's data, this function may return the original object, a view, + or copy. If no dimensions are flattened, then the original object :attr:`input` is returned. Otherwise, if input can + be viewed as the flattened shape, then that view is returned. Finally, only if the input cannot be viewed as the + flattened shape is input's data copied. See :meth:`torch.Tensor.view` for details on when a view will be returned. + + .. note:: + Flattening a zero-dimensional tensor will return a one-dimensional view. + + Args: + input (Tensor): the input tensor. + start_dim (int): the first dim to flatten + end_dim (int): the last dim to flatten + + Example:: + + >>> t = torch.tensor([[[1, 2], + ... [3, 4]], + ... [[5, 6], + ... [7, 8]]]) + >>> torch.flatten(t) + tensor([1, 2, 3, 4, 5, 6, 7, 8]) + >>> torch.flatten(t, start_dim=1) + tensor([[1, 2, 3, 4], + [5, 6, 7, 8]]) + """ + +@overload +def flatten( + input: Tensor, + dims: Sequence[str | EllipsisType | None], + out_dim: str | EllipsisType | None, +) -> Tensor: + r""" + flatten(input, start_dim=0, end_dim=-1) -> Tensor + + Flattens :attr:`input` by reshaping it into a one-dimensional tensor. If :attr:`start_dim` or :attr:`end_dim` + are passed, only dimensions starting with :attr:`start_dim` and ending with :attr:`end_dim` are flattened. + The order of elements in :attr:`input` is unchanged. + + Unlike NumPy's flatten, which always copies input's data, this function may return the original object, a view, + or copy. If no dimensions are flattened, then the original object :attr:`input` is returned. Otherwise, if input can + be viewed as the flattened shape, then that view is returned. Finally, only if the input cannot be viewed as the + flattened shape is input's data copied. See :meth:`torch.Tensor.view` for details on when a view will be returned. + + .. note:: + Flattening a zero-dimensional tensor will return a one-dimensional view. + + Args: + input (Tensor): the input tensor. + start_dim (int): the first dim to flatten + end_dim (int): the last dim to flatten + + Example:: + + >>> t = torch.tensor([[[1, 2], + ... [3, 4]], + ... [[5, 6], + ... [7, 8]]]) + >>> torch.flatten(t) + tensor([1, 2, 3, 4, 5, 6, 7, 8]) + >>> torch.flatten(t, start_dim=1) + tensor([[1, 2, 3, 4], + [5, 6, 7, 8]]) + """ + +def flip(input: Tensor, dims: _size) -> Tensor: + r""" + flip(input, dims) -> Tensor + + Reverse the order of an n-D tensor along given axis in dims. + + .. note:: + `torch.flip` makes a copy of :attr:`input`'s data. This is different from NumPy's `np.flip`, + which returns a view in constant time. Since copying a tensor's data is more work than viewing that data, + `torch.flip` is expected to be slower than `np.flip`. + + Args: + input (Tensor): the input tensor. + dims (a list or tuple): axis to flip on + + Example:: + + >>> x = torch.arange(8).view(2, 2, 2) + >>> x + tensor([[[ 0, 1], + [ 2, 3]], + + [[ 4, 5], + [ 6, 7]]]) + >>> torch.flip(x, [0, 1]) + tensor([[[ 6, 7], + [ 4, 5]], + + [[ 2, 3], + [ 0, 1]]]) + """ + +def fliplr(input: Tensor) -> Tensor: + r""" + fliplr(input) -> Tensor + + Flip tensor in the left/right direction, returning a new tensor. + + Flip the entries in each row in the left/right direction. + Columns are preserved, but appear in a different order than before. + + Note: + Requires the tensor to be at least 2-D. + + .. note:: + `torch.fliplr` makes a copy of :attr:`input`'s data. This is different from NumPy's `np.fliplr`, + which returns a view in constant time. Since copying a tensor's data is more work than viewing that data, + `torch.fliplr` is expected to be slower than `np.fliplr`. + + Args: + input (Tensor): Must be at least 2-dimensional. + + Example:: + + >>> x = torch.arange(4).view(2, 2) + >>> x + tensor([[0, 1], + [2, 3]]) + >>> torch.fliplr(x) + tensor([[1, 0], + [3, 2]]) + """ + +def flipud(input: Tensor) -> Tensor: + r""" + flipud(input) -> Tensor + + Flip tensor in the up/down direction, returning a new tensor. + + Flip the entries in each column in the up/down direction. + Rows are preserved, but appear in a different order than before. + + Note: + Requires the tensor to be at least 1-D. + + .. note:: + `torch.flipud` makes a copy of :attr:`input`'s data. This is different from NumPy's `np.flipud`, + which returns a view in constant time. Since copying a tensor's data is more work than viewing that data, + `torch.flipud` is expected to be slower than `np.flipud`. + + Args: + input (Tensor): Must be at least 1-dimensional. + + Example:: + + >>> x = torch.arange(4).view(2, 2) + >>> x + tensor([[0, 1], + [2, 3]]) + >>> torch.flipud(x) + tensor([[2, 3], + [0, 1]]) + """ + +@overload +def float_power( + input: Tensor, + exponent: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + float_power(input, exponent, *, out=None) -> Tensor + + Raises :attr:`input` to the power of :attr:`exponent`, elementwise, in double precision. + If neither input is complex returns a ``torch.float64`` tensor, + and if one or more inputs is complex returns a ``torch.complex128`` tensor. + + .. note:: + This function always computes in double precision, unlike :func:`torch.pow`, + which implements more typical :ref:`type promotion `. + This is useful when the computation needs to be performed in a wider or more precise dtype, + or the results of the computation may contain fractional values not representable in the input dtypes, + like when an integer base is raised to a negative integer exponent. + + Args: + input (Tensor or Number): the base value(s) + exponent (Tensor or Number): the exponent value(s) + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randint(10, (4,)) + >>> a + tensor([6, 4, 7, 1]) + >>> torch.float_power(a, 2) + tensor([36., 16., 49., 1.], dtype=torch.float64) + + >>> a = torch.arange(1, 5) + >>> a + tensor([ 1, 2, 3, 4]) + >>> exp = torch.tensor([2, -3, 4, -5]) + >>> exp + tensor([ 2, -3, 4, -5]) + >>> torch.float_power(a, exp) + tensor([1.0000e+00, 1.2500e-01, 8.1000e+01, 9.7656e-04], dtype=torch.float64) + """ + +@overload +def float_power( + self: Number | _complex, + exponent: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + float_power(input, exponent, *, out=None) -> Tensor + + Raises :attr:`input` to the power of :attr:`exponent`, elementwise, in double precision. + If neither input is complex returns a ``torch.float64`` tensor, + and if one or more inputs is complex returns a ``torch.complex128`` tensor. + + .. note:: + This function always computes in double precision, unlike :func:`torch.pow`, + which implements more typical :ref:`type promotion `. + This is useful when the computation needs to be performed in a wider or more precise dtype, + or the results of the computation may contain fractional values not representable in the input dtypes, + like when an integer base is raised to a negative integer exponent. + + Args: + input (Tensor or Number): the base value(s) + exponent (Tensor or Number): the exponent value(s) + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randint(10, (4,)) + >>> a + tensor([6, 4, 7, 1]) + >>> torch.float_power(a, 2) + tensor([36., 16., 49., 1.], dtype=torch.float64) + + >>> a = torch.arange(1, 5) + >>> a + tensor([ 1, 2, 3, 4]) + >>> exp = torch.tensor([2, -3, 4, -5]) + >>> exp + tensor([ 2, -3, 4, -5]) + >>> torch.float_power(a, exp) + tensor([1.0000e+00, 1.2500e-01, 8.1000e+01, 9.7656e-04], dtype=torch.float64) + """ + +@overload +def float_power( + input: Tensor, + exponent: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + float_power(input, exponent, *, out=None) -> Tensor + + Raises :attr:`input` to the power of :attr:`exponent`, elementwise, in double precision. + If neither input is complex returns a ``torch.float64`` tensor, + and if one or more inputs is complex returns a ``torch.complex128`` tensor. + + .. note:: + This function always computes in double precision, unlike :func:`torch.pow`, + which implements more typical :ref:`type promotion `. + This is useful when the computation needs to be performed in a wider or more precise dtype, + or the results of the computation may contain fractional values not representable in the input dtypes, + like when an integer base is raised to a negative integer exponent. + + Args: + input (Tensor or Number): the base value(s) + exponent (Tensor or Number): the exponent value(s) + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randint(10, (4,)) + >>> a + tensor([6, 4, 7, 1]) + >>> torch.float_power(a, 2) + tensor([36., 16., 49., 1.], dtype=torch.float64) + + >>> a = torch.arange(1, 5) + >>> a + tensor([ 1, 2, 3, 4]) + >>> exp = torch.tensor([2, -3, 4, -5]) + >>> exp + tensor([ 2, -3, 4, -5]) + >>> torch.float_power(a, exp) + tensor([1.0000e+00, 1.2500e-01, 8.1000e+01, 9.7656e-04], dtype=torch.float64) + """ + +def floor(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + floor(input, *, out=None) -> Tensor + + Returns a new tensor with the floor of the elements of :attr:`input`, + the largest integer less than or equal to each element. + + For integer inputs, follows the array-api convention of returning a + copy of the input tensor. + + .. math:: + \text{out}_{i} = \left\lfloor \text{input}_{i} \right\rfloor + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-0.8166, 1.5308, -0.2530, -0.2091]) + >>> torch.floor(a) + tensor([-1., 1., -1., -1.]) + """ + +def floor_(input: Tensor) -> Tensor: ... +def floor_divide( + input: Tensor | Number, + other: Tensor | Number, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + floor_divide(input, other, *, out=None) -> Tensor + + .. note:: + + Before PyTorch 1.13 :func:`torch.floor_divide` incorrectly performed + truncation division. To restore the previous behavior use + :func:`torch.div` with ``rounding_mode='trunc'``. + + Computes :attr:`input` divided by :attr:`other`, elementwise, and floors + the result. + + .. math:: + \text{{out}}_i = \text{floor} \left( \frac{{\text{{input}}_i}}{{\text{{other}}_i}} \right) + + + + Supports broadcasting to a common shape, type promotion, and integer and float inputs. + + Args: + input (Tensor or Number): the dividend + other (Tensor or Number): the divisor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([4.0, 3.0]) + >>> b = torch.tensor([2.0, 2.0]) + >>> torch.floor_divide(a, b) + tensor([2.0, 1.0]) + >>> torch.floor_divide(a, 1.4) + tensor([2.0, 2.0]) + """ + +def fmax( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + fmax(input, other, *, out=None) -> Tensor + + Computes the element-wise maximum of :attr:`input` and :attr:`other`. + + This is like :func:`torch.maximum` except it handles NaNs differently: + if exactly one of the two elements being compared is a NaN then the non-NaN element is taken as the maximum. + Only if both elements are NaN is NaN propagated. + + This function is a wrapper around C++'s ``std::fmax`` and is similar to NumPy's ``fmax`` function. + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer and floating-point inputs. + + Args: + input (Tensor): the input tensor. + other (Tensor): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([9.7, float('nan'), 3.1, float('nan')]) + >>> b = torch.tensor([-2.2, 0.5, float('nan'), float('nan')]) + >>> torch.fmax(a, b) + tensor([9.7000, 0.5000, 3.1000, nan]) + """ + +def fmin( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + fmin(input, other, *, out=None) -> Tensor + + Computes the element-wise minimum of :attr:`input` and :attr:`other`. + + This is like :func:`torch.minimum` except it handles NaNs differently: + if exactly one of the two elements being compared is a NaN then the non-NaN element is taken as the minimum. + Only if both elements are NaN is NaN propagated. + + This function is a wrapper around C++'s ``std::fmin`` and is similar to NumPy's ``fmin`` function. + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer and floating-point inputs. + + Args: + input (Tensor): the input tensor. + other (Tensor): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([2.2, float('nan'), 2.1, float('nan')]) + >>> b = torch.tensor([-9.3, 0.1, float('nan'), float('nan')]) + >>> torch.fmin(a, b) + tensor([-9.3000, 0.1000, 2.1000, nan]) + """ + +@overload +def fmod( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + fmod(input, other, *, out=None) -> Tensor + + Applies C++'s `std::fmod `_ entrywise. + The result has the same sign as the dividend :attr:`input` and its absolute value + is less than that of :attr:`other`. + + This function may be defined in terms of :func:`torch.div` as + + .. code:: python + + torch.fmod(a, b) == a - a.div(b, rounding_mode="trunc") * b + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer and float inputs. + + .. note:: + + When the divisor is zero, returns ``NaN`` for floating point dtypes + on both CPU and GPU; raises ``RuntimeError`` for integer division by + zero on CPU; Integer division by zero on GPU may return any value. + + .. note:: + + Complex inputs are not supported. In some cases, it is not mathematically + possible to satisfy the definition of a modulo operation with complex numbers. + + .. seealso:: + + :func:`torch.remainder` which implements Python's modulus operator. + This one is defined using division rounding down the result. + + Args: + input (Tensor): the dividend + other (Tensor or Scalar): the divisor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.fmod(torch.tensor([-3., -2, -1, 1, 2, 3]), 2) + tensor([-1., -0., -1., 1., 0., 1.]) + >>> torch.fmod(torch.tensor([1, 2, 3, 4, 5]), -1.5) + tensor([1.0000, 0.5000, 0.0000, 1.0000, 0.5000]) + """ + +@overload +def fmod( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + fmod(input, other, *, out=None) -> Tensor + + Applies C++'s `std::fmod `_ entrywise. + The result has the same sign as the dividend :attr:`input` and its absolute value + is less than that of :attr:`other`. + + This function may be defined in terms of :func:`torch.div` as + + .. code:: python + + torch.fmod(a, b) == a - a.div(b, rounding_mode="trunc") * b + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer and float inputs. + + .. note:: + + When the divisor is zero, returns ``NaN`` for floating point dtypes + on both CPU and GPU; raises ``RuntimeError`` for integer division by + zero on CPU; Integer division by zero on GPU may return any value. + + .. note:: + + Complex inputs are not supported. In some cases, it is not mathematically + possible to satisfy the definition of a modulo operation with complex numbers. + + .. seealso:: + + :func:`torch.remainder` which implements Python's modulus operator. + This one is defined using division rounding down the result. + + Args: + input (Tensor): the dividend + other (Tensor or Scalar): the divisor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.fmod(torch.tensor([-3., -2, -1, 1, 2, 3]), 2) + tensor([-1., -0., -1., 1., 0., 1.]) + >>> torch.fmod(torch.tensor([1, 2, 3, 4, 5]), -1.5) + tensor([1.0000, 0.5000, 0.0000, 1.0000, 0.5000]) + """ + +def frac(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + frac(input, *, out=None) -> Tensor + + Computes the fractional portion of each element in :attr:`input`. + + .. math:: + \text{out}_{i} = \text{input}_{i} - \left\lfloor |\text{input}_{i}| \right\rfloor * \operatorname{sgn}(\text{input}_{i}) + + Example:: + + >>> torch.frac(torch.tensor([1, 2.5, -3.2])) + tensor([ 0.0000, 0.5000, -0.2000]) + """ + +def frac_(input: Tensor) -> Tensor: ... +def frexp( + input: Tensor, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.frexp: + r""" + frexp(input, *, out=None) -> (Tensor mantissa, Tensor exponent) + + Decomposes :attr:`input` into mantissa and exponent tensors + such that :math:`\text{input} = \text{mantissa} \times 2^{\text{exponent}}`. + + The range of mantissa is the open interval (-1, 1). + + Supports float inputs. + + Args: + input (Tensor): the input tensor + + + Keyword args: + out (tuple, optional): the output tensors + + Example:: + + >>> x = torch.arange(9.) + >>> mantissa, exponent = torch.frexp(x) + >>> mantissa + tensor([0.0000, 0.5000, 0.5000, 0.7500, 0.5000, 0.6250, 0.7500, 0.8750, 0.5000]) + >>> exponent + tensor([0, 1, 2, 2, 3, 3, 3, 3, 4], dtype=torch.int32) + >>> torch.ldexp(mantissa, exponent) + tensor([0., 1., 2., 3., 4., 5., 6., 7., 8.]) + """ + +def frobenius_norm( + input: Tensor, + dim: _int | _size, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: ... +def from_file( + filename: str, + shared: _bool | None = None, + size: _int | None = 0, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + from_file(filename, shared=None, size=0, *, dtype=None, layout=None, device=None, pin_memory=False) + + Creates a CPU tensor with a storage backed by a memory-mapped file. + + If ``shared`` is True, then memory is shared between processes. All changes are written to the file. + If ``shared`` is False, then changes to the tensor do not affect the file. + + ``size`` is the number of elements in the Tensor. If ``shared`` is ``False``, then the file must contain + at least ``size * sizeof(dtype)`` bytes. If ``shared`` is ``True`` the file will be created if needed. + + .. note:: + Only CPU tensors can be mapped to files. + + .. note:: + For now, tensors with storages backed by a memory-mapped file cannot be created in pinned memory. + + + Args: + filename (str): file name to map + shared (bool): whether to share memory (whether ``MAP_SHARED`` or ``MAP_PRIVATE`` is passed to the + underlying `mmap(2) call `_) + size (int): number of elements in the tensor + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> t = torch.randn(2, 5, dtype=torch.float64) + >>> t.numpy().tofile('storage.pt') + >>> t_mapped = torch.from_file('storage.pt', shared=False, size=10, dtype=torch.float64) + """ + +def from_numpy(ndarray) -> Tensor: + r""" + from_numpy(ndarray) -> Tensor + + Creates a :class:`Tensor` from a :class:`numpy.ndarray`. + + The returned tensor and :attr:`ndarray` share the same memory. Modifications to + the tensor will be reflected in the :attr:`ndarray` and vice versa. The returned + tensor is not resizable. + + It currently accepts :attr:`ndarray` with dtypes of ``numpy.float64``, + ``numpy.float32``, ``numpy.float16``, ``numpy.complex64``, ``numpy.complex128``, + ``numpy.int64``, ``numpy.int32``, ``numpy.int16``, ``numpy.int8``, ``numpy.uint8``, + and ``bool``. + + .. warning:: + Writing to a tensor created from a read-only NumPy array is not supported and will result in undefined behavior. + + Example:: + + >>> a = numpy.array([1, 2, 3]) + >>> t = torch.from_numpy(a) + >>> t + tensor([ 1, 2, 3]) + >>> t[0] = -1 + >>> a + array([-1, 2, 3]) + """ + +def frombuffer( + buffer: Any, + *, + dtype: _dtype, + count: int = -1, + offset: int = 0, + requires_grad: _bool = False, +) -> Tensor: + r""" + frombuffer(buffer, *, dtype, count=-1, offset=0, requires_grad=False) -> Tensor + + Creates a 1-dimensional :class:`Tensor` from an object that implements + the Python buffer protocol. + + Skips the first :attr:`offset` bytes in the buffer, and interprets the rest of + the raw bytes as a 1-dimensional tensor of type :attr:`dtype` with :attr:`count` + elements. + + Note that either of the following must be true: + + 1. :attr:`count` is a positive non-zero number, and the total number of bytes + in the buffer is more than :attr:`offset` plus :attr:`count` times the size + (in bytes) of :attr:`dtype`. + + 2. :attr:`count` is negative, and the length (number of bytes) of the buffer + subtracted by the :attr:`offset` is a multiple of the size (in bytes) of + :attr:`dtype`. + + The returned tensor and buffer share the same memory. Modifications to + the tensor will be reflected in the buffer and vice versa. The returned + tensor is not resizable. + + .. note:: + This function increments the reference count for the object that + owns the shared memory. Therefore, such memory will not be deallocated + before the returned tensor goes out of scope. + + .. warning:: + This function's behavior is undefined when passed an object implementing + the buffer protocol whose data is not on the CPU. Doing so is likely to + cause a segmentation fault. + + .. warning:: + This function does not try to infer the :attr:`dtype` (hence, it is not + optional). Passing a different :attr:`dtype` than its source may result + in unexpected behavior. + + Args: + buffer (object): a Python object that exposes the buffer interface. + + Keyword args: + dtype (:class:`torch.dtype`): the desired data type of returned tensor. + count (int, optional): the number of desired elements to be read. + If negative, all the elements (until the end of the buffer) will be + read. Default: -1. + offset (int, optional): the number of bytes to skip at the start of + the buffer. Default: 0. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> import array + >>> a = array.array('i', [1, 2, 3]) + >>> t = torch.frombuffer(a, dtype=torch.int32) + >>> t + tensor([ 1, 2, 3]) + >>> t[0] = -1 + >>> a + array([-1, 2, 3]) + + >>> # Interprets the signed char bytes as 32-bit integers. + >>> # Each 4 signed char elements will be interpreted as + >>> # 1 signed 32-bit integer. + >>> import array + >>> a = array.array('b', [-1, 0, 0, 0]) + >>> torch.frombuffer(a, dtype=torch.int32) + tensor([255], dtype=torch.int32) + """ + +@overload +def full( + size: _size, + fill_value: Number | _complex, + *, + out: Tensor | None = None, + layout: _layout = strided, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + full(size, fill_value, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Creates a tensor of size :attr:`size` filled with :attr:`fill_value`. The + tensor's dtype is inferred from :attr:`fill_value`. + + Args: + size (int...): a list, tuple, or :class:`torch.Size` of integers defining the + shape of the output tensor. + fill_value (Scalar): the value to fill the output tensor with. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.full((2, 3), 3.141592) + tensor([[ 3.1416, 3.1416, 3.1416], + [ 3.1416, 3.1416, 3.1416]]) + """ + +@overload +def full( + size: _size, + fill_value: Number | _complex, + *, + names: list[str | None], + layout: _layout = strided, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + full(size, fill_value, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Creates a tensor of size :attr:`size` filled with :attr:`fill_value`. The + tensor's dtype is inferred from :attr:`fill_value`. + + Args: + size (int...): a list, tuple, or :class:`torch.Size` of integers defining the + shape of the output tensor. + fill_value (Scalar): the value to fill the output tensor with. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.full((2, 3), 3.141592) + tensor([[ 3.1416, 3.1416, 3.1416], + [ 3.1416, 3.1416, 3.1416]]) + """ + +@overload +def full( + size: Sequence[_int | SymInt], + fill_value: Number | _complex, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + full(size, fill_value, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Creates a tensor of size :attr:`size` filled with :attr:`fill_value`. The + tensor's dtype is inferred from :attr:`fill_value`. + + Args: + size (int...): a list, tuple, or :class:`torch.Size` of integers defining the + shape of the output tensor. + fill_value (Scalar): the value to fill the output tensor with. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.full((2, 3), 3.141592) + tensor([[ 3.1416, 3.1416, 3.1416], + [ 3.1416, 3.1416, 3.1416]]) + """ + +@overload +def full( + size: _size, + fill_value: Number | _complex, + *, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + full(size, fill_value, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Creates a tensor of size :attr:`size` filled with :attr:`fill_value`. The + tensor's dtype is inferred from :attr:`fill_value`. + + Args: + size (int...): a list, tuple, or :class:`torch.Size` of integers defining the + shape of the output tensor. + fill_value (Scalar): the value to fill the output tensor with. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.full((2, 3), 3.141592) + tensor([[ 3.1416, 3.1416, 3.1416], + [ 3.1416, 3.1416, 3.1416]]) + """ + +def full_like( + input: Tensor, + fill_value: Number | _complex, + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + full_like(input, fill_value, \*, dtype=None, layout=torch.strided, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same size as :attr:`input` filled with :attr:`fill_value`. + ``torch.full_like(input, fill_value)`` is equivalent to + ``torch.full(input.size(), fill_value, dtype=input.dtype, layout=input.layout, device=input.device)``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + fill_value: the number to fill the output tensor with. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + + Example:: + + >>> x = torch.ones(2, 3) + >>> torch.full_like(x, 3.141592) + tensor([[ 3.1416, 3.1416, 3.1416], + [ 3.1416, 3.1416, 3.1416]]) + >>> torch.full_like(x, 7) + tensor([[7., 7., 7.], + [7., 7., 7.]]) + >>> torch.full_like(x, 0.5, dtype=torch.int32) + tensor([[0, 0, 0], + [0, 0, 0]], dtype=torch.int32) + >>> y = torch.randn(3, 4, dtype=torch.float64) + >>> torch.full_like(y, -1.0) + tensor([[-1., -1., -1., -1.], + [-1., -1., -1., -1.], + [-1., -1., -1., -1.]], dtype=torch.float64) + """ + +def fused_moving_avg_obs_fake_quant( + input: Tensor, + observer_on: Tensor, + fake_quant_on: Tensor, + running_min: Tensor, + running_max: Tensor, + scale: Tensor, + zero_point: Tensor, + averaging_const: _float, + quant_min: _int, + quant_max: _int, + ch_axis: _int, + per_row_fake_quant: _bool = False, + symmetric_quant: _bool = False, +) -> Tensor: ... +@overload +def gather( + input: Tensor, + dim: _int, + index: Tensor, + *, + sparse_grad: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + gather(input, dim, index, *, sparse_grad=False, out=None) -> Tensor + + Gathers values along an axis specified by `dim`. + + For a 3-D tensor the output is specified by:: + + out[i][j][k] = input[index[i][j][k]][j][k] # if dim == 0 + out[i][j][k] = input[i][index[i][j][k]][k] # if dim == 1 + out[i][j][k] = input[i][j][index[i][j][k]] # if dim == 2 + + :attr:`input` and :attr:`index` must have the same number of dimensions. + It is also required that ``index.size(d) <= input.size(d)`` for all + dimensions ``d != dim``. :attr:`out` will have the same shape as :attr:`index`. + Note that ``input`` and ``index`` do not broadcast against each other. + When :attr:`index` is empty, we always return an empty output with the same shape + without further error checking. + + Args: + input (Tensor): the source tensor + dim (int): the axis along which to index + index (LongTensor): the indices of elements to gather + + Keyword arguments: + sparse_grad (bool, optional): If ``True``, gradient w.r.t. :attr:`input` will be a sparse tensor. + out (Tensor, optional): the destination tensor + + Example:: + + >>> t = torch.tensor([[1, 2], [3, 4]]) + >>> torch.gather(t, 1, torch.tensor([[0, 0], [1, 0]])) + tensor([[ 1, 1], + [ 4, 3]]) + """ + +@overload +def gather( + input: Tensor, + dim: str | EllipsisType | None, + index: Tensor, + *, + sparse_grad: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + gather(input, dim, index, *, sparse_grad=False, out=None) -> Tensor + + Gathers values along an axis specified by `dim`. + + For a 3-D tensor the output is specified by:: + + out[i][j][k] = input[index[i][j][k]][j][k] # if dim == 0 + out[i][j][k] = input[i][index[i][j][k]][k] # if dim == 1 + out[i][j][k] = input[i][j][index[i][j][k]] # if dim == 2 + + :attr:`input` and :attr:`index` must have the same number of dimensions. + It is also required that ``index.size(d) <= input.size(d)`` for all + dimensions ``d != dim``. :attr:`out` will have the same shape as :attr:`index`. + Note that ``input`` and ``index`` do not broadcast against each other. + When :attr:`index` is empty, we always return an empty output with the same shape + without further error checking. + + Args: + input (Tensor): the source tensor + dim (int): the axis along which to index + index (LongTensor): the indices of elements to gather + + Keyword arguments: + sparse_grad (bool, optional): If ``True``, gradient w.r.t. :attr:`input` will be a sparse tensor. + out (Tensor, optional): the destination tensor + + Example:: + + >>> t = torch.tensor([[1, 2], [3, 4]]) + >>> torch.gather(t, 1, torch.tensor([[0, 0], [1, 0]])) + tensor([[ 1, 1], + [ 4, 3]]) + """ + +def gcd( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + gcd(input, other, *, out=None) -> Tensor + + Computes the element-wise greatest common divisor (GCD) of :attr:`input` and :attr:`other`. + + Both :attr:`input` and :attr:`other` must have integer types. + + .. note:: + This defines :math:`gcd(0, 0) = 0`. + + Args: + input (Tensor): the input tensor. + other (Tensor): the second input tensor + + Keyword arguments: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([5, 10, 15]) + >>> b = torch.tensor([3, 4, 5]) + >>> torch.gcd(a, b) + tensor([1, 2, 5]) + >>> c = torch.tensor([3]) + >>> torch.gcd(a, c) + tensor([1, 1, 3]) + """ + +def gcd_(input: Tensor, other: Tensor) -> Tensor: ... +@overload +def ge( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + ge(input, other, *, out=None) -> Tensor + + Computes :math:`\text{input} \geq \text{other}` element-wise. + + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or float): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is greater than or equal to :attr:`other` and False elsewhere + + Example:: + + >>> torch.ge(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[True, True], [False, True]]) + """ + +@overload +def ge( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + ge(input, other, *, out=None) -> Tensor + + Computes :math:`\text{input} \geq \text{other}` element-wise. + + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or float): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is greater than or equal to :attr:`other` and False elsewhere + + Example:: + + >>> torch.ge(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[True, True], [False, True]]) + """ + +def geqrf( + input: Tensor, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.geqrf: + r""" + geqrf(input, *, out=None) -> (Tensor, Tensor) + + This is a low-level function for calling LAPACK's geqrf directly. This function + returns a namedtuple (a, tau) as defined in `LAPACK documentation for geqrf`_ . + + Computes a QR decomposition of :attr:`input`. + Both `Q` and `R` matrices are stored in the same output tensor `a`. + The elements of `R` are stored on and above the diagonal. + Elementary reflectors (or Householder vectors) implicitly defining matrix `Q` + are stored below the diagonal. + The results of this function can be used together with :func:`torch.linalg.householder_product` + to obtain the `Q` matrix or + with :func:`torch.ormqr`, which uses an implicit representation of the `Q` matrix, + for an efficient matrix-matrix multiplication. + + See `LAPACK documentation for geqrf`_ for further details. + + .. note:: + See also :func:`torch.linalg.qr`, which computes Q and R matrices, and :func:`torch.linalg.lstsq` + with the ``driver="gels"`` option for a function that can solve matrix equations using a QR decomposition. + + Args: + input (Tensor): the input matrix + + Keyword args: + out (tuple, optional): the output tuple of (Tensor, Tensor). Ignored if `None`. Default: `None`. + + .. _LAPACK documentation for geqrf: + http://www.netlib.org/lapack/explore-html/df/dc5/group__variants_g_ecomputational_ga3766ea903391b5cf9008132f7440ec7b.html + """ + +def ger( + input: Tensor, + vec2: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + ger(input, vec2, *, out=None) -> Tensor + + Alias of :func:`torch.outer`. + + .. warning:: + This function is deprecated and will be removed in a future PyTorch release. + Use :func:`torch.outer` instead. + """ + +def get_default_dtype() -> _dtype: + r""" + get_default_dtype() -> torch.dtype + + Get the current default floating point :class:`torch.dtype`. + + Example:: + + >>> torch.get_default_dtype() # initial default for floating point is torch.float32 + torch.float32 + >>> torch.set_default_dtype(torch.float64) + >>> torch.get_default_dtype() # default is now changed to torch.float64 + torch.float64 + """ + +def get_num_interop_threads() -> _int: + r""" + get_num_interop_threads() -> int + + Returns the number of threads used for inter-op parallelism on CPU + (e.g. in JIT interpreter) + """ + +def get_num_threads() -> _int: + r""" + get_num_threads() -> int + + Returns the number of threads used for parallelizing CPU operations + """ + +@overload +def gradient( + input: Tensor, + *, + spacing: Number | _complex | None = None, + dim: _int | None = None, + edge_order: _int = 1, +) -> tuple[Tensor, ...]: + r""" + gradient(input, *, spacing=1, dim=None, edge_order=1) -> List of Tensors + + Estimates the gradient of a function :math:`g : \mathbb{R}^n \rightarrow \mathbb{R}` in + one or more dimensions using the `second-order accurate central differences method + `_ and + either first or second order estimates at the boundaries. + + The gradient of :math:`g` is estimated using samples. By default, when :attr:`spacing` is not + specified, the samples are entirely described by :attr:`input`, and the mapping of input coordinates + to an output is the same as the tensor's mapping of indices to values. For example, for a three-dimensional + :attr:`input` the function described is :math:`g : \mathbb{R}^3 \rightarrow \mathbb{R}`, and + :math:`g(1, 2, 3)\ == input[1, 2, 3]`. + + When :attr:`spacing` is specified, it modifies the relationship between :attr:`input` and input coordinates. + This is detailed in the "Keyword Arguments" section below. + + The gradient is estimated by estimating each partial derivative of :math:`g` independently. This estimation is + accurate if :math:`g` is in :math:`C^3` (it has at least 3 continuous derivatives), and the estimation can be + improved by providing closer samples. Mathematically, the value at each interior point of a partial derivative + is estimated using `Taylor's theorem with remainder `_. + Letting :math:`x` be an interior point with :math:`x-h_l` and :math:`x+h_r` be points neighboring + it to the left and right respectively, :math:`f(x+h_r)` and :math:`f(x-h_l)` can be estimated using: + + .. math:: + \begin{aligned} + f(x+h_r) = f(x) + h_r f'(x) + {h_r}^2 \frac{f''(x)}{2} + {h_r}^3 \frac{f'''(\xi_1)}{6}, \xi_1 \in (x, x+h_r) \\ + f(x-h_l) = f(x) - h_l f'(x) + {h_l}^2 \frac{f''(x)}{2} - {h_l}^3 \frac{f'''(\xi_2)}{6}, \xi_2 \in (x, x-h_l) \\ + \end{aligned} + + Using the fact that :math:`f \in C^3` and solving the linear system, we derive: + + .. math:: + f'(x) \approx \frac{ {h_l}^2 f(x+h_r) - {h_r}^2 f(x-h_l) + + ({h_r}^2-{h_l}^2 ) f(x) }{ {h_r} {h_l}^2 + {h_r}^2 {h_l} } + + .. note:: + We estimate the gradient of functions in complex domain + :math:`g : \mathbb{C}^n \rightarrow \mathbb{C}` in the same way. + + The value of each partial derivative at the boundary points is computed differently. See edge_order below. + + Args: + input (``Tensor``): the tensor that represents the values of the function + + Keyword args: + spacing (``scalar``, ``list of scalar``, ``list of Tensor``, optional): :attr:`spacing` can be used to modify + how the :attr:`input` tensor's indices relate to sample coordinates. If :attr:`spacing` is a scalar then + the indices are multiplied by the scalar to produce the coordinates. For example, if :attr:`spacing=2` the + indices (1, 2, 3) become coordinates (2, 4, 6). If :attr:`spacing` is a list of scalars then the corresponding + indices are multiplied. For example, if :attr:`spacing=(2, -1, 3)` the indices (1, 2, 3) become coordinates (2, -2, 9). + Finally, if :attr:`spacing` is a list of one-dimensional tensors then each tensor specifies the coordinates for + the corresponding dimension. For example, if the indices are (1, 2, 3) and the tensors are (t0, t1, t2), then + the coordinates are (t0[1], t1[2], t2[3]) + + dim (``int``, ``list of int``, optional): the dimension or dimensions to approximate the gradient over. By default + the partial gradient in every dimension is computed. Note that when :attr:`dim` is specified the elements of + the :attr:`spacing` argument must correspond with the specified dims." + + edge_order (``int``, optional): 1 or 2, for `first-order + `_ or + `second-order `_ + estimation of the boundary ("edge") values, respectively. Note that when :attr:`edge_order` is specified, each + dimension size of :attr:`input` should be at least edge_order+1 + + Examples:: + + >>> # Estimates the gradient of f(x)=x^2 at points [-2, -1, 2, 4] + >>> coordinates = (torch.tensor([-2., -1., 1., 4.]),) + >>> values = torch.tensor([4., 1., 1., 16.], ) + >>> torch.gradient(values, spacing = coordinates) + (tensor([-3., -2., 2., 5.]),) + + >>> # Estimates the gradient of the R^2 -> R function whose samples are + >>> # described by the tensor t. Implicit coordinates are [0, 1] for the outermost + >>> # dimension and [0, 1, 2, 3] for the innermost dimension, and function estimates + >>> # partial derivative for both dimensions. + >>> t = torch.tensor([[1, 2, 4, 8], [10, 20, 40, 80]]) + >>> torch.gradient(t) + (tensor([[ 9., 18., 36., 72.], + [ 9., 18., 36., 72.]]), + tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]])) + + >>> # A scalar value for spacing modifies the relationship between tensor indices + >>> # and input coordinates by multiplying the indices to find the + >>> # coordinates. For example, below the indices of the innermost + >>> # 0, 1, 2, 3 translate to coordinates of [0, 2, 4, 6], and the indices of + >>> # the outermost dimension 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = 2.0) # dim = None (implicitly [0, 1]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.5000, 0.7500, 1.5000, 2.0000], + [ 5.0000, 7.5000, 15.0000, 20.0000]])) + >>> # doubling the spacing between samples halves the estimated partial gradients. + + >>> + >>> # Estimates only the partial derivative for dimension 1 + >>> torch.gradient(t, dim = 1) # spacing = None (implicitly 1.) + (tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]]),) + + >>> # When spacing is a list of scalars, the relationship between the tensor + >>> # indices and input coordinates changes based on dimension. + >>> # For example, below, the indices of the innermost dimension 0, 1, 2, 3 translate + >>> # to coordinates of [0, 3, 6, 9], and the indices of the outermost dimension + >>> # 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = [3., 2.]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + + >>> # The following example is a replication of the previous one with explicit + >>> # coordinates. + >>> coords = (torch.tensor([0, 2]), torch.tensor([0, 3, 6, 9])) + >>> torch.gradient(t, spacing = coords) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + """ + +@overload +def gradient( + input: Tensor, + *, + spacing: Sequence[Number | _complex], + dim: _int | None = None, + edge_order: _int = 1, +) -> tuple[Tensor, ...]: + r""" + gradient(input, *, spacing=1, dim=None, edge_order=1) -> List of Tensors + + Estimates the gradient of a function :math:`g : \mathbb{R}^n \rightarrow \mathbb{R}` in + one or more dimensions using the `second-order accurate central differences method + `_ and + either first or second order estimates at the boundaries. + + The gradient of :math:`g` is estimated using samples. By default, when :attr:`spacing` is not + specified, the samples are entirely described by :attr:`input`, and the mapping of input coordinates + to an output is the same as the tensor's mapping of indices to values. For example, for a three-dimensional + :attr:`input` the function described is :math:`g : \mathbb{R}^3 \rightarrow \mathbb{R}`, and + :math:`g(1, 2, 3)\ == input[1, 2, 3]`. + + When :attr:`spacing` is specified, it modifies the relationship between :attr:`input` and input coordinates. + This is detailed in the "Keyword Arguments" section below. + + The gradient is estimated by estimating each partial derivative of :math:`g` independently. This estimation is + accurate if :math:`g` is in :math:`C^3` (it has at least 3 continuous derivatives), and the estimation can be + improved by providing closer samples. Mathematically, the value at each interior point of a partial derivative + is estimated using `Taylor's theorem with remainder `_. + Letting :math:`x` be an interior point with :math:`x-h_l` and :math:`x+h_r` be points neighboring + it to the left and right respectively, :math:`f(x+h_r)` and :math:`f(x-h_l)` can be estimated using: + + .. math:: + \begin{aligned} + f(x+h_r) = f(x) + h_r f'(x) + {h_r}^2 \frac{f''(x)}{2} + {h_r}^3 \frac{f'''(\xi_1)}{6}, \xi_1 \in (x, x+h_r) \\ + f(x-h_l) = f(x) - h_l f'(x) + {h_l}^2 \frac{f''(x)}{2} - {h_l}^3 \frac{f'''(\xi_2)}{6}, \xi_2 \in (x, x-h_l) \\ + \end{aligned} + + Using the fact that :math:`f \in C^3` and solving the linear system, we derive: + + .. math:: + f'(x) \approx \frac{ {h_l}^2 f(x+h_r) - {h_r}^2 f(x-h_l) + + ({h_r}^2-{h_l}^2 ) f(x) }{ {h_r} {h_l}^2 + {h_r}^2 {h_l} } + + .. note:: + We estimate the gradient of functions in complex domain + :math:`g : \mathbb{C}^n \rightarrow \mathbb{C}` in the same way. + + The value of each partial derivative at the boundary points is computed differently. See edge_order below. + + Args: + input (``Tensor``): the tensor that represents the values of the function + + Keyword args: + spacing (``scalar``, ``list of scalar``, ``list of Tensor``, optional): :attr:`spacing` can be used to modify + how the :attr:`input` tensor's indices relate to sample coordinates. If :attr:`spacing` is a scalar then + the indices are multiplied by the scalar to produce the coordinates. For example, if :attr:`spacing=2` the + indices (1, 2, 3) become coordinates (2, 4, 6). If :attr:`spacing` is a list of scalars then the corresponding + indices are multiplied. For example, if :attr:`spacing=(2, -1, 3)` the indices (1, 2, 3) become coordinates (2, -2, 9). + Finally, if :attr:`spacing` is a list of one-dimensional tensors then each tensor specifies the coordinates for + the corresponding dimension. For example, if the indices are (1, 2, 3) and the tensors are (t0, t1, t2), then + the coordinates are (t0[1], t1[2], t2[3]) + + dim (``int``, ``list of int``, optional): the dimension or dimensions to approximate the gradient over. By default + the partial gradient in every dimension is computed. Note that when :attr:`dim` is specified the elements of + the :attr:`spacing` argument must correspond with the specified dims." + + edge_order (``int``, optional): 1 or 2, for `first-order + `_ or + `second-order `_ + estimation of the boundary ("edge") values, respectively. Note that when :attr:`edge_order` is specified, each + dimension size of :attr:`input` should be at least edge_order+1 + + Examples:: + + >>> # Estimates the gradient of f(x)=x^2 at points [-2, -1, 2, 4] + >>> coordinates = (torch.tensor([-2., -1., 1., 4.]),) + >>> values = torch.tensor([4., 1., 1., 16.], ) + >>> torch.gradient(values, spacing = coordinates) + (tensor([-3., -2., 2., 5.]),) + + >>> # Estimates the gradient of the R^2 -> R function whose samples are + >>> # described by the tensor t. Implicit coordinates are [0, 1] for the outermost + >>> # dimension and [0, 1, 2, 3] for the innermost dimension, and function estimates + >>> # partial derivative for both dimensions. + >>> t = torch.tensor([[1, 2, 4, 8], [10, 20, 40, 80]]) + >>> torch.gradient(t) + (tensor([[ 9., 18., 36., 72.], + [ 9., 18., 36., 72.]]), + tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]])) + + >>> # A scalar value for spacing modifies the relationship between tensor indices + >>> # and input coordinates by multiplying the indices to find the + >>> # coordinates. For example, below the indices of the innermost + >>> # 0, 1, 2, 3 translate to coordinates of [0, 2, 4, 6], and the indices of + >>> # the outermost dimension 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = 2.0) # dim = None (implicitly [0, 1]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.5000, 0.7500, 1.5000, 2.0000], + [ 5.0000, 7.5000, 15.0000, 20.0000]])) + >>> # doubling the spacing between samples halves the estimated partial gradients. + + >>> + >>> # Estimates only the partial derivative for dimension 1 + >>> torch.gradient(t, dim = 1) # spacing = None (implicitly 1.) + (tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]]),) + + >>> # When spacing is a list of scalars, the relationship between the tensor + >>> # indices and input coordinates changes based on dimension. + >>> # For example, below, the indices of the innermost dimension 0, 1, 2, 3 translate + >>> # to coordinates of [0, 3, 6, 9], and the indices of the outermost dimension + >>> # 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = [3., 2.]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + + >>> # The following example is a replication of the previous one with explicit + >>> # coordinates. + >>> coords = (torch.tensor([0, 2]), torch.tensor([0, 3, 6, 9])) + >>> torch.gradient(t, spacing = coords) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + """ + +@overload +def gradient( + input: Tensor, + *, + spacing: Sequence[Number | _complex], + dim: _size, + edge_order: _int = 1, +) -> tuple[Tensor, ...]: + r""" + gradient(input, *, spacing=1, dim=None, edge_order=1) -> List of Tensors + + Estimates the gradient of a function :math:`g : \mathbb{R}^n \rightarrow \mathbb{R}` in + one or more dimensions using the `second-order accurate central differences method + `_ and + either first or second order estimates at the boundaries. + + The gradient of :math:`g` is estimated using samples. By default, when :attr:`spacing` is not + specified, the samples are entirely described by :attr:`input`, and the mapping of input coordinates + to an output is the same as the tensor's mapping of indices to values. For example, for a three-dimensional + :attr:`input` the function described is :math:`g : \mathbb{R}^3 \rightarrow \mathbb{R}`, and + :math:`g(1, 2, 3)\ == input[1, 2, 3]`. + + When :attr:`spacing` is specified, it modifies the relationship between :attr:`input` and input coordinates. + This is detailed in the "Keyword Arguments" section below. + + The gradient is estimated by estimating each partial derivative of :math:`g` independently. This estimation is + accurate if :math:`g` is in :math:`C^3` (it has at least 3 continuous derivatives), and the estimation can be + improved by providing closer samples. Mathematically, the value at each interior point of a partial derivative + is estimated using `Taylor's theorem with remainder `_. + Letting :math:`x` be an interior point with :math:`x-h_l` and :math:`x+h_r` be points neighboring + it to the left and right respectively, :math:`f(x+h_r)` and :math:`f(x-h_l)` can be estimated using: + + .. math:: + \begin{aligned} + f(x+h_r) = f(x) + h_r f'(x) + {h_r}^2 \frac{f''(x)}{2} + {h_r}^3 \frac{f'''(\xi_1)}{6}, \xi_1 \in (x, x+h_r) \\ + f(x-h_l) = f(x) - h_l f'(x) + {h_l}^2 \frac{f''(x)}{2} - {h_l}^3 \frac{f'''(\xi_2)}{6}, \xi_2 \in (x, x-h_l) \\ + \end{aligned} + + Using the fact that :math:`f \in C^3` and solving the linear system, we derive: + + .. math:: + f'(x) \approx \frac{ {h_l}^2 f(x+h_r) - {h_r}^2 f(x-h_l) + + ({h_r}^2-{h_l}^2 ) f(x) }{ {h_r} {h_l}^2 + {h_r}^2 {h_l} } + + .. note:: + We estimate the gradient of functions in complex domain + :math:`g : \mathbb{C}^n \rightarrow \mathbb{C}` in the same way. + + The value of each partial derivative at the boundary points is computed differently. See edge_order below. + + Args: + input (``Tensor``): the tensor that represents the values of the function + + Keyword args: + spacing (``scalar``, ``list of scalar``, ``list of Tensor``, optional): :attr:`spacing` can be used to modify + how the :attr:`input` tensor's indices relate to sample coordinates. If :attr:`spacing` is a scalar then + the indices are multiplied by the scalar to produce the coordinates. For example, if :attr:`spacing=2` the + indices (1, 2, 3) become coordinates (2, 4, 6). If :attr:`spacing` is a list of scalars then the corresponding + indices are multiplied. For example, if :attr:`spacing=(2, -1, 3)` the indices (1, 2, 3) become coordinates (2, -2, 9). + Finally, if :attr:`spacing` is a list of one-dimensional tensors then each tensor specifies the coordinates for + the corresponding dimension. For example, if the indices are (1, 2, 3) and the tensors are (t0, t1, t2), then + the coordinates are (t0[1], t1[2], t2[3]) + + dim (``int``, ``list of int``, optional): the dimension or dimensions to approximate the gradient over. By default + the partial gradient in every dimension is computed. Note that when :attr:`dim` is specified the elements of + the :attr:`spacing` argument must correspond with the specified dims." + + edge_order (``int``, optional): 1 or 2, for `first-order + `_ or + `second-order `_ + estimation of the boundary ("edge") values, respectively. Note that when :attr:`edge_order` is specified, each + dimension size of :attr:`input` should be at least edge_order+1 + + Examples:: + + >>> # Estimates the gradient of f(x)=x^2 at points [-2, -1, 2, 4] + >>> coordinates = (torch.tensor([-2., -1., 1., 4.]),) + >>> values = torch.tensor([4., 1., 1., 16.], ) + >>> torch.gradient(values, spacing = coordinates) + (tensor([-3., -2., 2., 5.]),) + + >>> # Estimates the gradient of the R^2 -> R function whose samples are + >>> # described by the tensor t. Implicit coordinates are [0, 1] for the outermost + >>> # dimension and [0, 1, 2, 3] for the innermost dimension, and function estimates + >>> # partial derivative for both dimensions. + >>> t = torch.tensor([[1, 2, 4, 8], [10, 20, 40, 80]]) + >>> torch.gradient(t) + (tensor([[ 9., 18., 36., 72.], + [ 9., 18., 36., 72.]]), + tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]])) + + >>> # A scalar value for spacing modifies the relationship between tensor indices + >>> # and input coordinates by multiplying the indices to find the + >>> # coordinates. For example, below the indices of the innermost + >>> # 0, 1, 2, 3 translate to coordinates of [0, 2, 4, 6], and the indices of + >>> # the outermost dimension 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = 2.0) # dim = None (implicitly [0, 1]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.5000, 0.7500, 1.5000, 2.0000], + [ 5.0000, 7.5000, 15.0000, 20.0000]])) + >>> # doubling the spacing between samples halves the estimated partial gradients. + + >>> + >>> # Estimates only the partial derivative for dimension 1 + >>> torch.gradient(t, dim = 1) # spacing = None (implicitly 1.) + (tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]]),) + + >>> # When spacing is a list of scalars, the relationship between the tensor + >>> # indices and input coordinates changes based on dimension. + >>> # For example, below, the indices of the innermost dimension 0, 1, 2, 3 translate + >>> # to coordinates of [0, 3, 6, 9], and the indices of the outermost dimension + >>> # 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = [3., 2.]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + + >>> # The following example is a replication of the previous one with explicit + >>> # coordinates. + >>> coords = (torch.tensor([0, 2]), torch.tensor([0, 3, 6, 9])) + >>> torch.gradient(t, spacing = coords) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + """ + +@overload +def gradient( + input: Tensor, + *, + spacing: tuple[Tensor, ...] | list[Tensor], + dim: _int | None = None, + edge_order: _int = 1, +) -> tuple[Tensor, ...]: + r""" + gradient(input, *, spacing=1, dim=None, edge_order=1) -> List of Tensors + + Estimates the gradient of a function :math:`g : \mathbb{R}^n \rightarrow \mathbb{R}` in + one or more dimensions using the `second-order accurate central differences method + `_ and + either first or second order estimates at the boundaries. + + The gradient of :math:`g` is estimated using samples. By default, when :attr:`spacing` is not + specified, the samples are entirely described by :attr:`input`, and the mapping of input coordinates + to an output is the same as the tensor's mapping of indices to values. For example, for a three-dimensional + :attr:`input` the function described is :math:`g : \mathbb{R}^3 \rightarrow \mathbb{R}`, and + :math:`g(1, 2, 3)\ == input[1, 2, 3]`. + + When :attr:`spacing` is specified, it modifies the relationship between :attr:`input` and input coordinates. + This is detailed in the "Keyword Arguments" section below. + + The gradient is estimated by estimating each partial derivative of :math:`g` independently. This estimation is + accurate if :math:`g` is in :math:`C^3` (it has at least 3 continuous derivatives), and the estimation can be + improved by providing closer samples. Mathematically, the value at each interior point of a partial derivative + is estimated using `Taylor's theorem with remainder `_. + Letting :math:`x` be an interior point with :math:`x-h_l` and :math:`x+h_r` be points neighboring + it to the left and right respectively, :math:`f(x+h_r)` and :math:`f(x-h_l)` can be estimated using: + + .. math:: + \begin{aligned} + f(x+h_r) = f(x) + h_r f'(x) + {h_r}^2 \frac{f''(x)}{2} + {h_r}^3 \frac{f'''(\xi_1)}{6}, \xi_1 \in (x, x+h_r) \\ + f(x-h_l) = f(x) - h_l f'(x) + {h_l}^2 \frac{f''(x)}{2} - {h_l}^3 \frac{f'''(\xi_2)}{6}, \xi_2 \in (x, x-h_l) \\ + \end{aligned} + + Using the fact that :math:`f \in C^3` and solving the linear system, we derive: + + .. math:: + f'(x) \approx \frac{ {h_l}^2 f(x+h_r) - {h_r}^2 f(x-h_l) + + ({h_r}^2-{h_l}^2 ) f(x) }{ {h_r} {h_l}^2 + {h_r}^2 {h_l} } + + .. note:: + We estimate the gradient of functions in complex domain + :math:`g : \mathbb{C}^n \rightarrow \mathbb{C}` in the same way. + + The value of each partial derivative at the boundary points is computed differently. See edge_order below. + + Args: + input (``Tensor``): the tensor that represents the values of the function + + Keyword args: + spacing (``scalar``, ``list of scalar``, ``list of Tensor``, optional): :attr:`spacing` can be used to modify + how the :attr:`input` tensor's indices relate to sample coordinates. If :attr:`spacing` is a scalar then + the indices are multiplied by the scalar to produce the coordinates. For example, if :attr:`spacing=2` the + indices (1, 2, 3) become coordinates (2, 4, 6). If :attr:`spacing` is a list of scalars then the corresponding + indices are multiplied. For example, if :attr:`spacing=(2, -1, 3)` the indices (1, 2, 3) become coordinates (2, -2, 9). + Finally, if :attr:`spacing` is a list of one-dimensional tensors then each tensor specifies the coordinates for + the corresponding dimension. For example, if the indices are (1, 2, 3) and the tensors are (t0, t1, t2), then + the coordinates are (t0[1], t1[2], t2[3]) + + dim (``int``, ``list of int``, optional): the dimension or dimensions to approximate the gradient over. By default + the partial gradient in every dimension is computed. Note that when :attr:`dim` is specified the elements of + the :attr:`spacing` argument must correspond with the specified dims." + + edge_order (``int``, optional): 1 or 2, for `first-order + `_ or + `second-order `_ + estimation of the boundary ("edge") values, respectively. Note that when :attr:`edge_order` is specified, each + dimension size of :attr:`input` should be at least edge_order+1 + + Examples:: + + >>> # Estimates the gradient of f(x)=x^2 at points [-2, -1, 2, 4] + >>> coordinates = (torch.tensor([-2., -1., 1., 4.]),) + >>> values = torch.tensor([4., 1., 1., 16.], ) + >>> torch.gradient(values, spacing = coordinates) + (tensor([-3., -2., 2., 5.]),) + + >>> # Estimates the gradient of the R^2 -> R function whose samples are + >>> # described by the tensor t. Implicit coordinates are [0, 1] for the outermost + >>> # dimension and [0, 1, 2, 3] for the innermost dimension, and function estimates + >>> # partial derivative for both dimensions. + >>> t = torch.tensor([[1, 2, 4, 8], [10, 20, 40, 80]]) + >>> torch.gradient(t) + (tensor([[ 9., 18., 36., 72.], + [ 9., 18., 36., 72.]]), + tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]])) + + >>> # A scalar value for spacing modifies the relationship between tensor indices + >>> # and input coordinates by multiplying the indices to find the + >>> # coordinates. For example, below the indices of the innermost + >>> # 0, 1, 2, 3 translate to coordinates of [0, 2, 4, 6], and the indices of + >>> # the outermost dimension 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = 2.0) # dim = None (implicitly [0, 1]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.5000, 0.7500, 1.5000, 2.0000], + [ 5.0000, 7.5000, 15.0000, 20.0000]])) + >>> # doubling the spacing between samples halves the estimated partial gradients. + + >>> + >>> # Estimates only the partial derivative for dimension 1 + >>> torch.gradient(t, dim = 1) # spacing = None (implicitly 1.) + (tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]]),) + + >>> # When spacing is a list of scalars, the relationship between the tensor + >>> # indices and input coordinates changes based on dimension. + >>> # For example, below, the indices of the innermost dimension 0, 1, 2, 3 translate + >>> # to coordinates of [0, 3, 6, 9], and the indices of the outermost dimension + >>> # 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = [3., 2.]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + + >>> # The following example is a replication of the previous one with explicit + >>> # coordinates. + >>> coords = (torch.tensor([0, 2]), torch.tensor([0, 3, 6, 9])) + >>> torch.gradient(t, spacing = coords) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + """ + +@overload +def gradient( + input: Tensor, + *, + spacing: Number | _complex, + dim: _size, + edge_order: _int = 1, +) -> tuple[Tensor, ...]: + r""" + gradient(input, *, spacing=1, dim=None, edge_order=1) -> List of Tensors + + Estimates the gradient of a function :math:`g : \mathbb{R}^n \rightarrow \mathbb{R}` in + one or more dimensions using the `second-order accurate central differences method + `_ and + either first or second order estimates at the boundaries. + + The gradient of :math:`g` is estimated using samples. By default, when :attr:`spacing` is not + specified, the samples are entirely described by :attr:`input`, and the mapping of input coordinates + to an output is the same as the tensor's mapping of indices to values. For example, for a three-dimensional + :attr:`input` the function described is :math:`g : \mathbb{R}^3 \rightarrow \mathbb{R}`, and + :math:`g(1, 2, 3)\ == input[1, 2, 3]`. + + When :attr:`spacing` is specified, it modifies the relationship between :attr:`input` and input coordinates. + This is detailed in the "Keyword Arguments" section below. + + The gradient is estimated by estimating each partial derivative of :math:`g` independently. This estimation is + accurate if :math:`g` is in :math:`C^3` (it has at least 3 continuous derivatives), and the estimation can be + improved by providing closer samples. Mathematically, the value at each interior point of a partial derivative + is estimated using `Taylor's theorem with remainder `_. + Letting :math:`x` be an interior point with :math:`x-h_l` and :math:`x+h_r` be points neighboring + it to the left and right respectively, :math:`f(x+h_r)` and :math:`f(x-h_l)` can be estimated using: + + .. math:: + \begin{aligned} + f(x+h_r) = f(x) + h_r f'(x) + {h_r}^2 \frac{f''(x)}{2} + {h_r}^3 \frac{f'''(\xi_1)}{6}, \xi_1 \in (x, x+h_r) \\ + f(x-h_l) = f(x) - h_l f'(x) + {h_l}^2 \frac{f''(x)}{2} - {h_l}^3 \frac{f'''(\xi_2)}{6}, \xi_2 \in (x, x-h_l) \\ + \end{aligned} + + Using the fact that :math:`f \in C^3` and solving the linear system, we derive: + + .. math:: + f'(x) \approx \frac{ {h_l}^2 f(x+h_r) - {h_r}^2 f(x-h_l) + + ({h_r}^2-{h_l}^2 ) f(x) }{ {h_r} {h_l}^2 + {h_r}^2 {h_l} } + + .. note:: + We estimate the gradient of functions in complex domain + :math:`g : \mathbb{C}^n \rightarrow \mathbb{C}` in the same way. + + The value of each partial derivative at the boundary points is computed differently. See edge_order below. + + Args: + input (``Tensor``): the tensor that represents the values of the function + + Keyword args: + spacing (``scalar``, ``list of scalar``, ``list of Tensor``, optional): :attr:`spacing` can be used to modify + how the :attr:`input` tensor's indices relate to sample coordinates. If :attr:`spacing` is a scalar then + the indices are multiplied by the scalar to produce the coordinates. For example, if :attr:`spacing=2` the + indices (1, 2, 3) become coordinates (2, 4, 6). If :attr:`spacing` is a list of scalars then the corresponding + indices are multiplied. For example, if :attr:`spacing=(2, -1, 3)` the indices (1, 2, 3) become coordinates (2, -2, 9). + Finally, if :attr:`spacing` is a list of one-dimensional tensors then each tensor specifies the coordinates for + the corresponding dimension. For example, if the indices are (1, 2, 3) and the tensors are (t0, t1, t2), then + the coordinates are (t0[1], t1[2], t2[3]) + + dim (``int``, ``list of int``, optional): the dimension or dimensions to approximate the gradient over. By default + the partial gradient in every dimension is computed. Note that when :attr:`dim` is specified the elements of + the :attr:`spacing` argument must correspond with the specified dims." + + edge_order (``int``, optional): 1 or 2, for `first-order + `_ or + `second-order `_ + estimation of the boundary ("edge") values, respectively. Note that when :attr:`edge_order` is specified, each + dimension size of :attr:`input` should be at least edge_order+1 + + Examples:: + + >>> # Estimates the gradient of f(x)=x^2 at points [-2, -1, 2, 4] + >>> coordinates = (torch.tensor([-2., -1., 1., 4.]),) + >>> values = torch.tensor([4., 1., 1., 16.], ) + >>> torch.gradient(values, spacing = coordinates) + (tensor([-3., -2., 2., 5.]),) + + >>> # Estimates the gradient of the R^2 -> R function whose samples are + >>> # described by the tensor t. Implicit coordinates are [0, 1] for the outermost + >>> # dimension and [0, 1, 2, 3] for the innermost dimension, and function estimates + >>> # partial derivative for both dimensions. + >>> t = torch.tensor([[1, 2, 4, 8], [10, 20, 40, 80]]) + >>> torch.gradient(t) + (tensor([[ 9., 18., 36., 72.], + [ 9., 18., 36., 72.]]), + tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]])) + + >>> # A scalar value for spacing modifies the relationship between tensor indices + >>> # and input coordinates by multiplying the indices to find the + >>> # coordinates. For example, below the indices of the innermost + >>> # 0, 1, 2, 3 translate to coordinates of [0, 2, 4, 6], and the indices of + >>> # the outermost dimension 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = 2.0) # dim = None (implicitly [0, 1]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.5000, 0.7500, 1.5000, 2.0000], + [ 5.0000, 7.5000, 15.0000, 20.0000]])) + >>> # doubling the spacing between samples halves the estimated partial gradients. + + >>> + >>> # Estimates only the partial derivative for dimension 1 + >>> torch.gradient(t, dim = 1) # spacing = None (implicitly 1.) + (tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]]),) + + >>> # When spacing is a list of scalars, the relationship between the tensor + >>> # indices and input coordinates changes based on dimension. + >>> # For example, below, the indices of the innermost dimension 0, 1, 2, 3 translate + >>> # to coordinates of [0, 3, 6, 9], and the indices of the outermost dimension + >>> # 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = [3., 2.]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + + >>> # The following example is a replication of the previous one with explicit + >>> # coordinates. + >>> coords = (torch.tensor([0, 2]), torch.tensor([0, 3, 6, 9])) + >>> torch.gradient(t, spacing = coords) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + """ + +@overload +def gradient( + input: Tensor, + *, + spacing: tuple[Tensor, ...] | list[Tensor], + dim: _size, + edge_order: _int = 1, +) -> tuple[Tensor, ...]: + r""" + gradient(input, *, spacing=1, dim=None, edge_order=1) -> List of Tensors + + Estimates the gradient of a function :math:`g : \mathbb{R}^n \rightarrow \mathbb{R}` in + one or more dimensions using the `second-order accurate central differences method + `_ and + either first or second order estimates at the boundaries. + + The gradient of :math:`g` is estimated using samples. By default, when :attr:`spacing` is not + specified, the samples are entirely described by :attr:`input`, and the mapping of input coordinates + to an output is the same as the tensor's mapping of indices to values. For example, for a three-dimensional + :attr:`input` the function described is :math:`g : \mathbb{R}^3 \rightarrow \mathbb{R}`, and + :math:`g(1, 2, 3)\ == input[1, 2, 3]`. + + When :attr:`spacing` is specified, it modifies the relationship between :attr:`input` and input coordinates. + This is detailed in the "Keyword Arguments" section below. + + The gradient is estimated by estimating each partial derivative of :math:`g` independently. This estimation is + accurate if :math:`g` is in :math:`C^3` (it has at least 3 continuous derivatives), and the estimation can be + improved by providing closer samples. Mathematically, the value at each interior point of a partial derivative + is estimated using `Taylor's theorem with remainder `_. + Letting :math:`x` be an interior point with :math:`x-h_l` and :math:`x+h_r` be points neighboring + it to the left and right respectively, :math:`f(x+h_r)` and :math:`f(x-h_l)` can be estimated using: + + .. math:: + \begin{aligned} + f(x+h_r) = f(x) + h_r f'(x) + {h_r}^2 \frac{f''(x)}{2} + {h_r}^3 \frac{f'''(\xi_1)}{6}, \xi_1 \in (x, x+h_r) \\ + f(x-h_l) = f(x) - h_l f'(x) + {h_l}^2 \frac{f''(x)}{2} - {h_l}^3 \frac{f'''(\xi_2)}{6}, \xi_2 \in (x, x-h_l) \\ + \end{aligned} + + Using the fact that :math:`f \in C^3` and solving the linear system, we derive: + + .. math:: + f'(x) \approx \frac{ {h_l}^2 f(x+h_r) - {h_r}^2 f(x-h_l) + + ({h_r}^2-{h_l}^2 ) f(x) }{ {h_r} {h_l}^2 + {h_r}^2 {h_l} } + + .. note:: + We estimate the gradient of functions in complex domain + :math:`g : \mathbb{C}^n \rightarrow \mathbb{C}` in the same way. + + The value of each partial derivative at the boundary points is computed differently. See edge_order below. + + Args: + input (``Tensor``): the tensor that represents the values of the function + + Keyword args: + spacing (``scalar``, ``list of scalar``, ``list of Tensor``, optional): :attr:`spacing` can be used to modify + how the :attr:`input` tensor's indices relate to sample coordinates. If :attr:`spacing` is a scalar then + the indices are multiplied by the scalar to produce the coordinates. For example, if :attr:`spacing=2` the + indices (1, 2, 3) become coordinates (2, 4, 6). If :attr:`spacing` is a list of scalars then the corresponding + indices are multiplied. For example, if :attr:`spacing=(2, -1, 3)` the indices (1, 2, 3) become coordinates (2, -2, 9). + Finally, if :attr:`spacing` is a list of one-dimensional tensors then each tensor specifies the coordinates for + the corresponding dimension. For example, if the indices are (1, 2, 3) and the tensors are (t0, t1, t2), then + the coordinates are (t0[1], t1[2], t2[3]) + + dim (``int``, ``list of int``, optional): the dimension or dimensions to approximate the gradient over. By default + the partial gradient in every dimension is computed. Note that when :attr:`dim` is specified the elements of + the :attr:`spacing` argument must correspond with the specified dims." + + edge_order (``int``, optional): 1 or 2, for `first-order + `_ or + `second-order `_ + estimation of the boundary ("edge") values, respectively. Note that when :attr:`edge_order` is specified, each + dimension size of :attr:`input` should be at least edge_order+1 + + Examples:: + + >>> # Estimates the gradient of f(x)=x^2 at points [-2, -1, 2, 4] + >>> coordinates = (torch.tensor([-2., -1., 1., 4.]),) + >>> values = torch.tensor([4., 1., 1., 16.], ) + >>> torch.gradient(values, spacing = coordinates) + (tensor([-3., -2., 2., 5.]),) + + >>> # Estimates the gradient of the R^2 -> R function whose samples are + >>> # described by the tensor t. Implicit coordinates are [0, 1] for the outermost + >>> # dimension and [0, 1, 2, 3] for the innermost dimension, and function estimates + >>> # partial derivative for both dimensions. + >>> t = torch.tensor([[1, 2, 4, 8], [10, 20, 40, 80]]) + >>> torch.gradient(t) + (tensor([[ 9., 18., 36., 72.], + [ 9., 18., 36., 72.]]), + tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]])) + + >>> # A scalar value for spacing modifies the relationship between tensor indices + >>> # and input coordinates by multiplying the indices to find the + >>> # coordinates. For example, below the indices of the innermost + >>> # 0, 1, 2, 3 translate to coordinates of [0, 2, 4, 6], and the indices of + >>> # the outermost dimension 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = 2.0) # dim = None (implicitly [0, 1]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.5000, 0.7500, 1.5000, 2.0000], + [ 5.0000, 7.5000, 15.0000, 20.0000]])) + >>> # doubling the spacing between samples halves the estimated partial gradients. + + >>> + >>> # Estimates only the partial derivative for dimension 1 + >>> torch.gradient(t, dim = 1) # spacing = None (implicitly 1.) + (tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]]),) + + >>> # When spacing is a list of scalars, the relationship between the tensor + >>> # indices and input coordinates changes based on dimension. + >>> # For example, below, the indices of the innermost dimension 0, 1, 2, 3 translate + >>> # to coordinates of [0, 3, 6, 9], and the indices of the outermost dimension + >>> # 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = [3., 2.]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + + >>> # The following example is a replication of the previous one with explicit + >>> # coordinates. + >>> coords = (torch.tensor([0, 2]), torch.tensor([0, 3, 6, 9])) + >>> torch.gradient(t, spacing = coords) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + """ + +@overload +def gradient( + input: Tensor, + *, + dim: _size, + edge_order: _int = 1, +) -> tuple[Tensor, ...]: + r""" + gradient(input, *, spacing=1, dim=None, edge_order=1) -> List of Tensors + + Estimates the gradient of a function :math:`g : \mathbb{R}^n \rightarrow \mathbb{R}` in + one or more dimensions using the `second-order accurate central differences method + `_ and + either first or second order estimates at the boundaries. + + The gradient of :math:`g` is estimated using samples. By default, when :attr:`spacing` is not + specified, the samples are entirely described by :attr:`input`, and the mapping of input coordinates + to an output is the same as the tensor's mapping of indices to values. For example, for a three-dimensional + :attr:`input` the function described is :math:`g : \mathbb{R}^3 \rightarrow \mathbb{R}`, and + :math:`g(1, 2, 3)\ == input[1, 2, 3]`. + + When :attr:`spacing` is specified, it modifies the relationship between :attr:`input` and input coordinates. + This is detailed in the "Keyword Arguments" section below. + + The gradient is estimated by estimating each partial derivative of :math:`g` independently. This estimation is + accurate if :math:`g` is in :math:`C^3` (it has at least 3 continuous derivatives), and the estimation can be + improved by providing closer samples. Mathematically, the value at each interior point of a partial derivative + is estimated using `Taylor's theorem with remainder `_. + Letting :math:`x` be an interior point with :math:`x-h_l` and :math:`x+h_r` be points neighboring + it to the left and right respectively, :math:`f(x+h_r)` and :math:`f(x-h_l)` can be estimated using: + + .. math:: + \begin{aligned} + f(x+h_r) = f(x) + h_r f'(x) + {h_r}^2 \frac{f''(x)}{2} + {h_r}^3 \frac{f'''(\xi_1)}{6}, \xi_1 \in (x, x+h_r) \\ + f(x-h_l) = f(x) - h_l f'(x) + {h_l}^2 \frac{f''(x)}{2} - {h_l}^3 \frac{f'''(\xi_2)}{6}, \xi_2 \in (x, x-h_l) \\ + \end{aligned} + + Using the fact that :math:`f \in C^3` and solving the linear system, we derive: + + .. math:: + f'(x) \approx \frac{ {h_l}^2 f(x+h_r) - {h_r}^2 f(x-h_l) + + ({h_r}^2-{h_l}^2 ) f(x) }{ {h_r} {h_l}^2 + {h_r}^2 {h_l} } + + .. note:: + We estimate the gradient of functions in complex domain + :math:`g : \mathbb{C}^n \rightarrow \mathbb{C}` in the same way. + + The value of each partial derivative at the boundary points is computed differently. See edge_order below. + + Args: + input (``Tensor``): the tensor that represents the values of the function + + Keyword args: + spacing (``scalar``, ``list of scalar``, ``list of Tensor``, optional): :attr:`spacing` can be used to modify + how the :attr:`input` tensor's indices relate to sample coordinates. If :attr:`spacing` is a scalar then + the indices are multiplied by the scalar to produce the coordinates. For example, if :attr:`spacing=2` the + indices (1, 2, 3) become coordinates (2, 4, 6). If :attr:`spacing` is a list of scalars then the corresponding + indices are multiplied. For example, if :attr:`spacing=(2, -1, 3)` the indices (1, 2, 3) become coordinates (2, -2, 9). + Finally, if :attr:`spacing` is a list of one-dimensional tensors then each tensor specifies the coordinates for + the corresponding dimension. For example, if the indices are (1, 2, 3) and the tensors are (t0, t1, t2), then + the coordinates are (t0[1], t1[2], t2[3]) + + dim (``int``, ``list of int``, optional): the dimension or dimensions to approximate the gradient over. By default + the partial gradient in every dimension is computed. Note that when :attr:`dim` is specified the elements of + the :attr:`spacing` argument must correspond with the specified dims." + + edge_order (``int``, optional): 1 or 2, for `first-order + `_ or + `second-order `_ + estimation of the boundary ("edge") values, respectively. Note that when :attr:`edge_order` is specified, each + dimension size of :attr:`input` should be at least edge_order+1 + + Examples:: + + >>> # Estimates the gradient of f(x)=x^2 at points [-2, -1, 2, 4] + >>> coordinates = (torch.tensor([-2., -1., 1., 4.]),) + >>> values = torch.tensor([4., 1., 1., 16.], ) + >>> torch.gradient(values, spacing = coordinates) + (tensor([-3., -2., 2., 5.]),) + + >>> # Estimates the gradient of the R^2 -> R function whose samples are + >>> # described by the tensor t. Implicit coordinates are [0, 1] for the outermost + >>> # dimension and [0, 1, 2, 3] for the innermost dimension, and function estimates + >>> # partial derivative for both dimensions. + >>> t = torch.tensor([[1, 2, 4, 8], [10, 20, 40, 80]]) + >>> torch.gradient(t) + (tensor([[ 9., 18., 36., 72.], + [ 9., 18., 36., 72.]]), + tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]])) + + >>> # A scalar value for spacing modifies the relationship between tensor indices + >>> # and input coordinates by multiplying the indices to find the + >>> # coordinates. For example, below the indices of the innermost + >>> # 0, 1, 2, 3 translate to coordinates of [0, 2, 4, 6], and the indices of + >>> # the outermost dimension 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = 2.0) # dim = None (implicitly [0, 1]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.5000, 0.7500, 1.5000, 2.0000], + [ 5.0000, 7.5000, 15.0000, 20.0000]])) + >>> # doubling the spacing between samples halves the estimated partial gradients. + + >>> + >>> # Estimates only the partial derivative for dimension 1 + >>> torch.gradient(t, dim = 1) # spacing = None (implicitly 1.) + (tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]]),) + + >>> # When spacing is a list of scalars, the relationship between the tensor + >>> # indices and input coordinates changes based on dimension. + >>> # For example, below, the indices of the innermost dimension 0, 1, 2, 3 translate + >>> # to coordinates of [0, 3, 6, 9], and the indices of the outermost dimension + >>> # 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = [3., 2.]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + + >>> # The following example is a replication of the previous one with explicit + >>> # coordinates. + >>> coords = (torch.tensor([0, 2]), torch.tensor([0, 3, 6, 9])) + >>> torch.gradient(t, spacing = coords) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + """ + +@overload +def greater( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + greater(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.gt`. + """ + +@overload +def greater( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + greater(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.gt`. + """ + +@overload +def greater_equal( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + greater_equal(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.ge`. + """ + +@overload +def greater_equal( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + greater_equal(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.ge`. + """ + +def grid_sampler( + input: Tensor, + grid: Tensor, + interpolation_mode: _int, + padding_mode: _int, + align_corners: _bool, +) -> Tensor: ... +def grid_sampler_2d( + input: Tensor, + grid: Tensor, + interpolation_mode: _int, + padding_mode: _int, + align_corners: _bool, +) -> Tensor: ... +def grid_sampler_3d( + input: Tensor, + grid: Tensor, + interpolation_mode: _int, + padding_mode: _int, + align_corners: _bool, +) -> Tensor: ... +def group_norm( + input: Tensor, + num_groups: _int, + weight: Tensor | None = None, + bias: Tensor | None = None, + eps: _float = 1e-05, + cudnn_enabled: _bool = True, +) -> Tensor: ... +@overload +def gru( + data: Tensor, + batch_sizes: Tensor, + hx: Tensor, + params: tuple[Tensor, ...] | list[Tensor], + has_biases: _bool, + num_layers: _int, + dropout: _float, + train: _bool, + bidirectional: _bool, +) -> tuple[Tensor, Tensor]: ... +@overload +def gru( + input: Tensor, + hx: Tensor, + params: tuple[Tensor, ...] | list[Tensor], + has_biases: _bool, + num_layers: _int, + dropout: _float, + train: _bool, + bidirectional: _bool, + batch_first: _bool, +) -> tuple[Tensor, Tensor]: ... +def gru_cell( + input: Tensor, + hx: Tensor, + w_ih: Tensor, + w_hh: Tensor, + b_ih: Tensor | None = None, + b_hh: Tensor | None = None, +) -> Tensor: ... +@overload +def gt( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + gt(input, other, *, out=None) -> Tensor + + Computes :math:`\text{input} > \text{other}` element-wise. + + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or float): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is greater than :attr:`other` and False elsewhere + + Example:: + + >>> torch.gt(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[False, True], [False, False]]) + """ + +@overload +def gt( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + gt(input, other, *, out=None) -> Tensor + + Computes :math:`\text{input} > \text{other}` element-wise. + + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or float): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is greater than :attr:`other` and False elsewhere + + Example:: + + >>> torch.gt(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[False, True], [False, False]]) + """ + +@overload +def hamming_window( + window_length: _int, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + hamming_window(window_length, *, dtype=None, layout=None, device=None, pin_memory=False, requires_grad=False) -> Tensor + + Hamming window function. + + .. math:: + w[n] = \alpha - \beta\ \cos \left( \frac{2 \pi n}{N - 1} \right), + + where :math:`N` is the full window size. + + The input :attr:`window_length` is a positive integer controlling the + returned window size. :attr:`periodic` flag determines whether the returned + window trims off the last duplicate value from the symmetric window and is + ready to be used as a periodic window with functions like + :meth:`torch.stft`. Therefore, if :attr:`periodic` is true, the :math:`N` in + above formula is in fact :math:`\text{window\_length} + 1`. Also, we always have + ``torch.hamming_window(L, periodic=True)`` equal to + ``torch.hamming_window(L + 1, periodic=False)[:-1])``. + + .. note:: + If :attr:`window_length` :math:`=1`, the returned window contains a single value 1. + + .. note:: + This is a generalized version of :meth:`torch.hann_window`. + + Arguments: + window_length (int): the size of returned window + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, *, dtype=None, layout=None, device=None, \ + pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, float alpha, *, dtype=None, layout=None, device=None, \ + pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic and alpha specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + alpha (float): The coefficient :math:`\alpha` in the equation above + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, float alpha, float beta, *, dtype=None, layout=None, \ + device=None, pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic, alpha and beta specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + alpha (float): The coefficient :math:`\alpha` in the equation above + beta (float): The coefficient :math:`\beta` in the equation above + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + """ + +@overload +def hamming_window( + window_length: _int, + periodic: _bool, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + hamming_window(window_length, *, dtype=None, layout=None, device=None, pin_memory=False, requires_grad=False) -> Tensor + + Hamming window function. + + .. math:: + w[n] = \alpha - \beta\ \cos \left( \frac{2 \pi n}{N - 1} \right), + + where :math:`N` is the full window size. + + The input :attr:`window_length` is a positive integer controlling the + returned window size. :attr:`periodic` flag determines whether the returned + window trims off the last duplicate value from the symmetric window and is + ready to be used as a periodic window with functions like + :meth:`torch.stft`. Therefore, if :attr:`periodic` is true, the :math:`N` in + above formula is in fact :math:`\text{window\_length} + 1`. Also, we always have + ``torch.hamming_window(L, periodic=True)`` equal to + ``torch.hamming_window(L + 1, periodic=False)[:-1])``. + + .. note:: + If :attr:`window_length` :math:`=1`, the returned window contains a single value 1. + + .. note:: + This is a generalized version of :meth:`torch.hann_window`. + + Arguments: + window_length (int): the size of returned window + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, *, dtype=None, layout=None, device=None, \ + pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, float alpha, *, dtype=None, layout=None, device=None, \ + pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic and alpha specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + alpha (float): The coefficient :math:`\alpha` in the equation above + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, float alpha, float beta, *, dtype=None, layout=None, \ + device=None, pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic, alpha and beta specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + alpha (float): The coefficient :math:`\alpha` in the equation above + beta (float): The coefficient :math:`\beta` in the equation above + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + """ + +@overload +def hamming_window( + window_length: _int, + periodic: _bool, + alpha: _float, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + hamming_window(window_length, *, dtype=None, layout=None, device=None, pin_memory=False, requires_grad=False) -> Tensor + + Hamming window function. + + .. math:: + w[n] = \alpha - \beta\ \cos \left( \frac{2 \pi n}{N - 1} \right), + + where :math:`N` is the full window size. + + The input :attr:`window_length` is a positive integer controlling the + returned window size. :attr:`periodic` flag determines whether the returned + window trims off the last duplicate value from the symmetric window and is + ready to be used as a periodic window with functions like + :meth:`torch.stft`. Therefore, if :attr:`periodic` is true, the :math:`N` in + above formula is in fact :math:`\text{window\_length} + 1`. Also, we always have + ``torch.hamming_window(L, periodic=True)`` equal to + ``torch.hamming_window(L + 1, periodic=False)[:-1])``. + + .. note:: + If :attr:`window_length` :math:`=1`, the returned window contains a single value 1. + + .. note:: + This is a generalized version of :meth:`torch.hann_window`. + + Arguments: + window_length (int): the size of returned window + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, *, dtype=None, layout=None, device=None, \ + pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, float alpha, *, dtype=None, layout=None, device=None, \ + pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic and alpha specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + alpha (float): The coefficient :math:`\alpha` in the equation above + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, float alpha, float beta, *, dtype=None, layout=None, \ + device=None, pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic, alpha and beta specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + alpha (float): The coefficient :math:`\alpha` in the equation above + beta (float): The coefficient :math:`\beta` in the equation above + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + """ + +@overload +def hamming_window( + window_length: _int, + periodic: _bool, + alpha: _float, + beta: _float, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + hamming_window(window_length, *, dtype=None, layout=None, device=None, pin_memory=False, requires_grad=False) -> Tensor + + Hamming window function. + + .. math:: + w[n] = \alpha - \beta\ \cos \left( \frac{2 \pi n}{N - 1} \right), + + where :math:`N` is the full window size. + + The input :attr:`window_length` is a positive integer controlling the + returned window size. :attr:`periodic` flag determines whether the returned + window trims off the last duplicate value from the symmetric window and is + ready to be used as a periodic window with functions like + :meth:`torch.stft`. Therefore, if :attr:`periodic` is true, the :math:`N` in + above formula is in fact :math:`\text{window\_length} + 1`. Also, we always have + ``torch.hamming_window(L, periodic=True)`` equal to + ``torch.hamming_window(L + 1, periodic=False)[:-1])``. + + .. note:: + If :attr:`window_length` :math:`=1`, the returned window contains a single value 1. + + .. note:: + This is a generalized version of :meth:`torch.hann_window`. + + Arguments: + window_length (int): the size of returned window + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, *, dtype=None, layout=None, device=None, \ + pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, float alpha, *, dtype=None, layout=None, device=None, \ + pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic and alpha specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + alpha (float): The coefficient :math:`\alpha` in the equation above + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, float alpha, float beta, *, dtype=None, layout=None, \ + device=None, pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic, alpha and beta specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + alpha (float): The coefficient :math:`\alpha` in the equation above + beta (float): The coefficient :math:`\beta` in the equation above + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + """ + +@overload +def hann_window( + window_length: _int, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + hann_window(window_length, periodic=True, *, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Hann window function. + + .. math:: + w[n] = \frac{1}{2}\ \left[1 - \cos \left( \frac{2 \pi n}{N - 1} \right)\right] = + \sin^2 \left( \frac{\pi n}{N - 1} \right), + + where :math:`N` is the full window size. + + The input :attr:`window_length` is a positive integer controlling the + returned window size. :attr:`periodic` flag determines whether the returned + window trims off the last duplicate value from the symmetric window and is + ready to be used as a periodic window with functions like + :meth:`torch.stft`. Therefore, if :attr:`periodic` is true, the :math:`N` in + above formula is in fact :math:`\text{window\_length} + 1`. Also, we always have + ``torch.hann_window(L, periodic=True)`` equal to + ``torch.hann_window(L + 1, periodic=False)[:-1])``. + + .. note:: + If :attr:`window_length` :math:`=1`, the returned window contains a single value 1. + + Arguments: + window_length (int): the size of returned window + periodic (bool, optional): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window + """ + +@overload +def hann_window( + window_length: _int, + periodic: _bool, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + hann_window(window_length, periodic=True, *, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Hann window function. + + .. math:: + w[n] = \frac{1}{2}\ \left[1 - \cos \left( \frac{2 \pi n}{N - 1} \right)\right] = + \sin^2 \left( \frac{\pi n}{N - 1} \right), + + where :math:`N` is the full window size. + + The input :attr:`window_length` is a positive integer controlling the + returned window size. :attr:`periodic` flag determines whether the returned + window trims off the last duplicate value from the symmetric window and is + ready to be used as a periodic window with functions like + :meth:`torch.stft`. Therefore, if :attr:`periodic` is true, the :math:`N` in + above formula is in fact :math:`\text{window\_length} + 1`. Also, we always have + ``torch.hann_window(L, periodic=True)`` equal to + ``torch.hann_window(L + 1, periodic=False)[:-1])``. + + .. note:: + If :attr:`window_length` :math:`=1`, the returned window contains a single value 1. + + Arguments: + window_length (int): the size of returned window + periodic (bool, optional): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window + """ + +def hardshrink( + input: Tensor, + lambd: Number | _complex = 0.5, + *, + out: Tensor | None = None, +) -> Tensor: ... +def hash_tensor( + input: Tensor, + dim: _int | _size = (), + *, + keepdim: _bool = False, + mode: _int = 0, + out: Tensor | None = None, +) -> Tensor: + r""" + hash_tensor(input, *, mode=0) -> Tensor + + Returns a hash of all elements in the :attr:`input` tensor. + + Currently only mode=0 (reduction via xor) is supported. The output will always + be of type ``torch.uint64``. The elements of ``input`` are upcasted to their + 64 bit float / integer equivalent and bitcasted to ``torch.uint64`` before + reduction via xor. + + Args: + input (Tensor): the input tensor. + + Keyword Args: + mode (int) : The hash to use. Default: 0 (xor_reduction) + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 1.1918, -1.1813, 0.3373]]) + >>> torch.hash_tensor(a) + tensor(13822780554648485888, dtype=torch.uint64) + + .. function:: hash_tensor(input, dim, *, keepdim=False, mode=0) -> Tensor + :noindex: + + Returns the hash of each row of the :attr:`input` tensor in the given + dimension :attr:`dim` given by mode. If :attr:`dim` is a list of dimensions, + reduce over all of them. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword Args: + mode (int) : The hash to use. Default: 0 (xor_reduction) + + Example:: + + >>> a = torch.randn(2, 4) + >>> a + tensor([[ 0.1317, -0.5554, -1.4724, -1.1391], + [ 0.0778, -0.6070, 0.6375, 0.1798]]) + >>> torch.hash_tensor(a, 1) + tensor([9233691267014066176, 9255993250844508160], dtype=torch.uint64) + """ + +def heaviside( + input: Tensor, + values: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + heaviside(input, values, *, out=None) -> Tensor + + Computes the Heaviside step function for each element in :attr:`input`. + The Heaviside step function is defined as: + + .. math:: + \text{{heaviside}}(input, values) = \begin{cases} + 0, & \text{if input < 0}\\ + values, & \text{if input == 0}\\ + 1, & \text{if input > 0} + \end{cases} + + + Args: + input (Tensor): the input tensor. + values (Tensor): The values to use where :attr:`input` is zero. + + Keyword arguments: + out (Tensor, optional): the output tensor. + + Example:: + + >>> input = torch.tensor([-1.5, 0, 2.0]) + >>> values = torch.tensor([0.5]) + >>> torch.heaviside(input, values) + tensor([0.0000, 0.5000, 1.0000]) + >>> values = torch.tensor([1.2, -2.0, 3.5]) + >>> torch.heaviside(input, values) + tensor([0., -2., 1.]) + """ + +def hinge_embedding_loss( + input: Tensor, + target: Tensor, + margin: _float = 1.0, + reduction: _int = 1, +) -> Tensor: ... +def histc( + input: Tensor, + bins: _int = 100, + min: Number | _complex = 0, + max: Number | _complex = 0, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + histc(input, bins=100, min=0, max=0, *, out=None) -> Tensor + + Computes the histogram of a tensor. + + The elements are sorted into equal width bins between :attr:`min` and + :attr:`max`. If :attr:`min` and :attr:`max` are both zero, the minimum and + maximum values of the data are used. + + Elements lower than min and higher than max and ``NaN`` elements are ignored. + + Args: + input (Tensor): the input tensor. + bins (int): number of histogram bins + min (Scalar): lower end of the range (inclusive) + max (Scalar): upper end of the range (inclusive) + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + Tensor: Histogram represented as a tensor + + Example:: + + >>> torch.histc(torch.tensor([1., 2, 1]), bins=4, min=0, max=3) + tensor([ 0., 2., 1., 0.]) + """ + +@overload +def histogram( + input: Tensor, + bins: Tensor, + *, + weight: Tensor | None = None, + density: _bool = False, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.histogram: + r""" + histogram(input, bins, *, range=None, weight=None, density=False, out=None) -> (Tensor, Tensor) + + Computes a histogram of the values in a tensor. + + :attr:`bins` can be an integer or a 1D tensor. + + If :attr:`bins` is an int, it specifies the number of equal-width bins. + By default, the lower and upper range of the bins is determined by the + minimum and maximum elements of the input tensor. The :attr:`range` + argument can be provided to specify a range for the bins. + + If :attr:`bins` is a 1D tensor, it specifies the sequence of bin edges + including the rightmost edge. It should contain at least 2 elements + and its elements should be increasing. + + Args: + input (Tensor): the input tensor. + bins: int or 1D Tensor. If int, defines the number of equal-width bins. If tensor, + defines the sequence of bin edges including the rightmost edge. + + Keyword args: + range (tuple of float): Defines the range of the bins. + weight (Tensor): If provided, weight should have the same shape as input. Each value in + input contributes its associated weight towards its bin's result. + density (bool): If False, the result will contain the count (or total weight) in each bin. + If True, the result is the value of the probability density function over the bins, + normalized such that the integral over the range of the bins is 1. + out (Tensor, optional): the output tensor. (tuple, optional): The result tuple of two output tensors (hist, bin_edges). + + Returns: + hist (Tensor): 1D Tensor containing the values of the histogram. + bin_edges(Tensor): 1D Tensor containing the edges of the histogram bins. + + Example:: + + >>> torch.histogram(torch.tensor([1., 2, 1]), bins=4, range=(0., 3.), weight=torch.tensor([1., 2., 4.])) + (tensor([ 0., 5., 2., 0.]), tensor([0., 0.75, 1.5, 2.25, 3.])) + >>> torch.histogram(torch.tensor([1., 2, 1]), bins=4, range=(0., 3.), weight=torch.tensor([1., 2., 4.]), density=True) + (tensor([ 0., 0.9524, 0.3810, 0.]), tensor([0., 0.75, 1.5, 2.25, 3.])) + """ + +@overload +def histogram( + input: Tensor, + bins: _int = 100, + *, + range: Sequence[_float] | None = None, + weight: Tensor | None = None, + density: _bool = False, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.histogram: + r""" + histogram(input, bins, *, range=None, weight=None, density=False, out=None) -> (Tensor, Tensor) + + Computes a histogram of the values in a tensor. + + :attr:`bins` can be an integer or a 1D tensor. + + If :attr:`bins` is an int, it specifies the number of equal-width bins. + By default, the lower and upper range of the bins is determined by the + minimum and maximum elements of the input tensor. The :attr:`range` + argument can be provided to specify a range for the bins. + + If :attr:`bins` is a 1D tensor, it specifies the sequence of bin edges + including the rightmost edge. It should contain at least 2 elements + and its elements should be increasing. + + Args: + input (Tensor): the input tensor. + bins: int or 1D Tensor. If int, defines the number of equal-width bins. If tensor, + defines the sequence of bin edges including the rightmost edge. + + Keyword args: + range (tuple of float): Defines the range of the bins. + weight (Tensor): If provided, weight should have the same shape as input. Each value in + input contributes its associated weight towards its bin's result. + density (bool): If False, the result will contain the count (or total weight) in each bin. + If True, the result is the value of the probability density function over the bins, + normalized such that the integral over the range of the bins is 1. + out (Tensor, optional): the output tensor. (tuple, optional): The result tuple of two output tensors (hist, bin_edges). + + Returns: + hist (Tensor): 1D Tensor containing the values of the histogram. + bin_edges(Tensor): 1D Tensor containing the edges of the histogram bins. + + Example:: + + >>> torch.histogram(torch.tensor([1., 2, 1]), bins=4, range=(0., 3.), weight=torch.tensor([1., 2., 4.])) + (tensor([ 0., 5., 2., 0.]), tensor([0., 0.75, 1.5, 2.25, 3.])) + >>> torch.histogram(torch.tensor([1., 2, 1]), bins=4, range=(0., 3.), weight=torch.tensor([1., 2., 4.]), density=True) + (tensor([ 0., 0.9524, 0.3810, 0.]), tensor([0., 0.75, 1.5, 2.25, 3.])) + """ + +@overload +def histogramdd( + input: Tensor, + bins: _int, + range: Sequence[_float] | None = None, + weight: Tensor | None = None, + density: _bool = False, +) -> torch.return_types.histogramdd: + r""" + histogramdd(input, bins, *, range=None, weight=None, density=False, out=None) -> (Tensor, Tensor[]) + + Computes a multi-dimensional histogram of the values in a tensor. + + Interprets the elements of an input tensor whose innermost dimension has size N + as a collection of N-dimensional points. Maps each of the points into a set of + N-dimensional bins and returns the number of points (or total weight) in each bin. + + :attr:`input` must be a tensor with at least 2 dimensions. + If input has shape (M, N), each of its M rows defines a point in N-dimensional space. + If input has three or more dimensions, all but the last dimension are flattened. + + Each dimension is independently associated with its own strictly increasing sequence + of bin edges. Bin edges may be specified explicitly by passing a sequence of 1D + tensors. Alternatively, bin edges may be constructed automatically by passing a + sequence of integers specifying the number of equal-width bins in each dimension. + + For each N-dimensional point in input: + - Each of its coordinates is binned independently among the bin edges + corresponding to its dimension + - Binning results are combined to identify the N-dimensional bin (if any) + into which the point falls + - If the point falls into a bin, the bin's count (or total weight) is incremented + - Points which do not fall into any bin do not contribute to the output + + :attr:`bins` can be a sequence of N 1D tensors, a sequence of N ints, or a single int. + + If :attr:`bins` is a sequence of N 1D tensors, it explicitly specifies the N sequences + of bin edges. Each 1D tensor should contain a strictly increasing sequence with at + least one element. A sequence of K bin edges defines K-1 bins, explicitly specifying + the left and right edges of all bins. Every bin is inclusive of its left edge. Only + the rightmost bin is inclusive of its right edge. + + If :attr:`bins` is a sequence of N ints, it specifies the number of equal-width bins + in each dimension. By default, the leftmost and rightmost bin edges in each dimension + are determined by the minimum and maximum elements of the input tensor in the + corresponding dimension. The :attr:`range` argument can be provided to manually + specify the leftmost and rightmost bin edges in each dimension. + + If :attr:`bins` is an int, it specifies the number of equal-width bins for all dimensions. + + .. note:: + See also :func:`torch.histogram`, which specifically computes 1D histograms. + While :func:`torch.histogramdd` infers the dimensionality of its bins and + binned values from the shape of :attr:`input`, :func:`torch.histogram` + accepts and flattens :attr:`input` of any shape. + + Args: + input (Tensor): the input tensor. + bins: Tensor[], int[], or int. + If Tensor[], defines the sequences of bin edges. + If int[], defines the number of equal-width bins in each dimension. + If int, defines the number of equal-width bins for all dimensions. + Keyword args: + range (sequence of float): Defines the leftmost and rightmost bin edges + in each dimension. + weight (Tensor): By default, each value in the input has weight 1. If a weight + tensor is passed, each N-dimensional coordinate in input + contributes its associated weight towards its bin's result. + The weight tensor should have the same shape as the :attr:`input` + tensor excluding its innermost dimension N. + density (bool): If False (default), the result will contain the count (or total weight) + in each bin. If True, each count (weight) is divided by the total count + (total weight), then divided by the volume of its associated bin. + Returns: + hist (Tensor): N-dimensional Tensor containing the values of the histogram. + bin_edges(Tensor[]): sequence of N 1D Tensors containing the bin edges. + + Example:: + + >>> torch.histogramdd(torch.tensor([[0., 1.], [1., 0.], [2., 0.], [2., 2.]]), bins=[3, 3], + ... weight=torch.tensor([1., 2., 4., 8.])) + torch.return_types.histogramdd( + hist=tensor([[0., 1., 0.], + [2., 0., 0.], + [4., 0., 8.]]), + bin_edges=(tensor([0.0000, 0.6667, 1.3333, 2.0000]), + tensor([0.0000, 0.6667, 1.3333, 2.0000]))) + + >>> torch.histogramdd(torch.tensor([[0., 0.], [1., 1.], [2., 2.]]), bins=[2, 2], + ... range=[0., 1., 0., 1.], density=True) + torch.return_types.histogramdd( + hist=tensor([[2., 0.], + [0., 2.]]), + bin_edges=(tensor([0.0000, 0.5000, 1.0000]), + tensor([0.0000, 0.5000, 1.0000]))) + """ + +@overload +def histogramdd( + input: Tensor, + bins: _size, + range: Sequence[_float] | None = None, + weight: Tensor | None = None, + density: _bool = False, +) -> torch.return_types.histogramdd: + r""" + histogramdd(input, bins, *, range=None, weight=None, density=False, out=None) -> (Tensor, Tensor[]) + + Computes a multi-dimensional histogram of the values in a tensor. + + Interprets the elements of an input tensor whose innermost dimension has size N + as a collection of N-dimensional points. Maps each of the points into a set of + N-dimensional bins and returns the number of points (or total weight) in each bin. + + :attr:`input` must be a tensor with at least 2 dimensions. + If input has shape (M, N), each of its M rows defines a point in N-dimensional space. + If input has three or more dimensions, all but the last dimension are flattened. + + Each dimension is independently associated with its own strictly increasing sequence + of bin edges. Bin edges may be specified explicitly by passing a sequence of 1D + tensors. Alternatively, bin edges may be constructed automatically by passing a + sequence of integers specifying the number of equal-width bins in each dimension. + + For each N-dimensional point in input: + - Each of its coordinates is binned independently among the bin edges + corresponding to its dimension + - Binning results are combined to identify the N-dimensional bin (if any) + into which the point falls + - If the point falls into a bin, the bin's count (or total weight) is incremented + - Points which do not fall into any bin do not contribute to the output + + :attr:`bins` can be a sequence of N 1D tensors, a sequence of N ints, or a single int. + + If :attr:`bins` is a sequence of N 1D tensors, it explicitly specifies the N sequences + of bin edges. Each 1D tensor should contain a strictly increasing sequence with at + least one element. A sequence of K bin edges defines K-1 bins, explicitly specifying + the left and right edges of all bins. Every bin is inclusive of its left edge. Only + the rightmost bin is inclusive of its right edge. + + If :attr:`bins` is a sequence of N ints, it specifies the number of equal-width bins + in each dimension. By default, the leftmost and rightmost bin edges in each dimension + are determined by the minimum and maximum elements of the input tensor in the + corresponding dimension. The :attr:`range` argument can be provided to manually + specify the leftmost and rightmost bin edges in each dimension. + + If :attr:`bins` is an int, it specifies the number of equal-width bins for all dimensions. + + .. note:: + See also :func:`torch.histogram`, which specifically computes 1D histograms. + While :func:`torch.histogramdd` infers the dimensionality of its bins and + binned values from the shape of :attr:`input`, :func:`torch.histogram` + accepts and flattens :attr:`input` of any shape. + + Args: + input (Tensor): the input tensor. + bins: Tensor[], int[], or int. + If Tensor[], defines the sequences of bin edges. + If int[], defines the number of equal-width bins in each dimension. + If int, defines the number of equal-width bins for all dimensions. + Keyword args: + range (sequence of float): Defines the leftmost and rightmost bin edges + in each dimension. + weight (Tensor): By default, each value in the input has weight 1. If a weight + tensor is passed, each N-dimensional coordinate in input + contributes its associated weight towards its bin's result. + The weight tensor should have the same shape as the :attr:`input` + tensor excluding its innermost dimension N. + density (bool): If False (default), the result will contain the count (or total weight) + in each bin. If True, each count (weight) is divided by the total count + (total weight), then divided by the volume of its associated bin. + Returns: + hist (Tensor): N-dimensional Tensor containing the values of the histogram. + bin_edges(Tensor[]): sequence of N 1D Tensors containing the bin edges. + + Example:: + + >>> torch.histogramdd(torch.tensor([[0., 1.], [1., 0.], [2., 0.], [2., 2.]]), bins=[3, 3], + ... weight=torch.tensor([1., 2., 4., 8.])) + torch.return_types.histogramdd( + hist=tensor([[0., 1., 0.], + [2., 0., 0.], + [4., 0., 8.]]), + bin_edges=(tensor([0.0000, 0.6667, 1.3333, 2.0000]), + tensor([0.0000, 0.6667, 1.3333, 2.0000]))) + + >>> torch.histogramdd(torch.tensor([[0., 0.], [1., 1.], [2., 2.]]), bins=[2, 2], + ... range=[0., 1., 0., 1.], density=True) + torch.return_types.histogramdd( + hist=tensor([[2., 0.], + [0., 2.]]), + bin_edges=(tensor([0.0000, 0.5000, 1.0000]), + tensor([0.0000, 0.5000, 1.0000]))) + """ + +@overload +def histogramdd( + input: Tensor, + bins: tuple[Tensor, ...] | list[Tensor], + range: Sequence[_float] | None = None, + weight: Tensor | None = None, + density: _bool = False, +) -> torch.return_types.histogramdd: + r""" + histogramdd(input, bins, *, range=None, weight=None, density=False, out=None) -> (Tensor, Tensor[]) + + Computes a multi-dimensional histogram of the values in a tensor. + + Interprets the elements of an input tensor whose innermost dimension has size N + as a collection of N-dimensional points. Maps each of the points into a set of + N-dimensional bins and returns the number of points (or total weight) in each bin. + + :attr:`input` must be a tensor with at least 2 dimensions. + If input has shape (M, N), each of its M rows defines a point in N-dimensional space. + If input has three or more dimensions, all but the last dimension are flattened. + + Each dimension is independently associated with its own strictly increasing sequence + of bin edges. Bin edges may be specified explicitly by passing a sequence of 1D + tensors. Alternatively, bin edges may be constructed automatically by passing a + sequence of integers specifying the number of equal-width bins in each dimension. + + For each N-dimensional point in input: + - Each of its coordinates is binned independently among the bin edges + corresponding to its dimension + - Binning results are combined to identify the N-dimensional bin (if any) + into which the point falls + - If the point falls into a bin, the bin's count (or total weight) is incremented + - Points which do not fall into any bin do not contribute to the output + + :attr:`bins` can be a sequence of N 1D tensors, a sequence of N ints, or a single int. + + If :attr:`bins` is a sequence of N 1D tensors, it explicitly specifies the N sequences + of bin edges. Each 1D tensor should contain a strictly increasing sequence with at + least one element. A sequence of K bin edges defines K-1 bins, explicitly specifying + the left and right edges of all bins. Every bin is inclusive of its left edge. Only + the rightmost bin is inclusive of its right edge. + + If :attr:`bins` is a sequence of N ints, it specifies the number of equal-width bins + in each dimension. By default, the leftmost and rightmost bin edges in each dimension + are determined by the minimum and maximum elements of the input tensor in the + corresponding dimension. The :attr:`range` argument can be provided to manually + specify the leftmost and rightmost bin edges in each dimension. + + If :attr:`bins` is an int, it specifies the number of equal-width bins for all dimensions. + + .. note:: + See also :func:`torch.histogram`, which specifically computes 1D histograms. + While :func:`torch.histogramdd` infers the dimensionality of its bins and + binned values from the shape of :attr:`input`, :func:`torch.histogram` + accepts and flattens :attr:`input` of any shape. + + Args: + input (Tensor): the input tensor. + bins: Tensor[], int[], or int. + If Tensor[], defines the sequences of bin edges. + If int[], defines the number of equal-width bins in each dimension. + If int, defines the number of equal-width bins for all dimensions. + Keyword args: + range (sequence of float): Defines the leftmost and rightmost bin edges + in each dimension. + weight (Tensor): By default, each value in the input has weight 1. If a weight + tensor is passed, each N-dimensional coordinate in input + contributes its associated weight towards its bin's result. + The weight tensor should have the same shape as the :attr:`input` + tensor excluding its innermost dimension N. + density (bool): If False (default), the result will contain the count (or total weight) + in each bin. If True, each count (weight) is divided by the total count + (total weight), then divided by the volume of its associated bin. + Returns: + hist (Tensor): N-dimensional Tensor containing the values of the histogram. + bin_edges(Tensor[]): sequence of N 1D Tensors containing the bin edges. + + Example:: + + >>> torch.histogramdd(torch.tensor([[0., 1.], [1., 0.], [2., 0.], [2., 2.]]), bins=[3, 3], + ... weight=torch.tensor([1., 2., 4., 8.])) + torch.return_types.histogramdd( + hist=tensor([[0., 1., 0.], + [2., 0., 0.], + [4., 0., 8.]]), + bin_edges=(tensor([0.0000, 0.6667, 1.3333, 2.0000]), + tensor([0.0000, 0.6667, 1.3333, 2.0000]))) + + >>> torch.histogramdd(torch.tensor([[0., 0.], [1., 1.], [2., 2.]]), bins=[2, 2], + ... range=[0., 1., 0., 1.], density=True) + torch.return_types.histogramdd( + hist=tensor([[2., 0.], + [0., 2.]]), + bin_edges=(tensor([0.0000, 0.5000, 1.0000]), + tensor([0.0000, 0.5000, 1.0000]))) + """ + +def hsmm(input: Tensor, mat2: Tensor) -> Tensor: ... +@overload +def hsplit(input: Tensor, sections: _int) -> tuple[Tensor, ...]: + r""" + hsplit(input, indices_or_sections) -> List of Tensors + + Splits :attr:`input`, a tensor with one or more dimensions, into multiple tensors + horizontally according to :attr:`indices_or_sections`. Each split is a view of + :attr:`input`. + + If :attr:`input` is one dimensional this is equivalent to calling + torch.tensor_split(input, indices_or_sections, dim=0) (the split dimension is + zero), and if :attr:`input` has two or more dimensions it's equivalent to calling + torch.tensor_split(input, indices_or_sections, dim=1) (the split dimension is 1), + except that if :attr:`indices_or_sections` is an integer it must evenly divide + the split dimension or a runtime error will be thrown. + + This function is based on NumPy's :func:`numpy.hsplit`. + + Args: + input (Tensor): tensor to split. + indices_or_sections (int or list or tuple of ints): See argument in :func:`torch.tensor_split`. + + Example:: + + >>> t = torch.arange(16.0).reshape(4,4) + >>> t + tensor([[ 0., 1., 2., 3.], + [ 4., 5., 6., 7.], + [ 8., 9., 10., 11.], + [12., 13., 14., 15.]]) + >>> torch.hsplit(t, 2) + (tensor([[ 0., 1.], + [ 4., 5.], + [ 8., 9.], + [12., 13.]]), + tensor([[ 2., 3.], + [ 6., 7.], + [10., 11.], + [14., 15.]])) + >>> torch.hsplit(t, [3, 6]) + (tensor([[ 0., 1., 2.], + [ 4., 5., 6.], + [ 8., 9., 10.], + [12., 13., 14.]]), + tensor([[ 3.], + [ 7.], + [11.], + [15.]]), + tensor([], size=(4, 0))) + """ + +@overload +def hsplit(input: Tensor, indices: _size) -> tuple[Tensor, ...]: + r""" + hsplit(input, indices_or_sections) -> List of Tensors + + Splits :attr:`input`, a tensor with one or more dimensions, into multiple tensors + horizontally according to :attr:`indices_or_sections`. Each split is a view of + :attr:`input`. + + If :attr:`input` is one dimensional this is equivalent to calling + torch.tensor_split(input, indices_or_sections, dim=0) (the split dimension is + zero), and if :attr:`input` has two or more dimensions it's equivalent to calling + torch.tensor_split(input, indices_or_sections, dim=1) (the split dimension is 1), + except that if :attr:`indices_or_sections` is an integer it must evenly divide + the split dimension or a runtime error will be thrown. + + This function is based on NumPy's :func:`numpy.hsplit`. + + Args: + input (Tensor): tensor to split. + indices_or_sections (int or list or tuple of ints): See argument in :func:`torch.tensor_split`. + + Example:: + + >>> t = torch.arange(16.0).reshape(4,4) + >>> t + tensor([[ 0., 1., 2., 3.], + [ 4., 5., 6., 7.], + [ 8., 9., 10., 11.], + [12., 13., 14., 15.]]) + >>> torch.hsplit(t, 2) + (tensor([[ 0., 1.], + [ 4., 5.], + [ 8., 9.], + [12., 13.]]), + tensor([[ 2., 3.], + [ 6., 7.], + [10., 11.], + [14., 15.]])) + >>> torch.hsplit(t, [3, 6]) + (tensor([[ 0., 1., 2.], + [ 4., 5., 6.], + [ 8., 9., 10.], + [12., 13., 14.]]), + tensor([[ 3.], + [ 7.], + [11.], + [15.]]), + tensor([], size=(4, 0))) + """ + +def hspmm( + mat1: Tensor, + mat2: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + hspmm(mat1, mat2, *, out=None) -> Tensor + + Performs a matrix multiplication of a :ref:`sparse COO matrix + ` :attr:`mat1` and a strided matrix :attr:`mat2`. The + result is a (1 + 1)-dimensional :ref:`hybrid COO matrix + `. + + Args: + mat1 (Tensor): the first sparse matrix to be matrix multiplied + mat2 (Tensor): the second strided matrix to be matrix multiplied + + Keyword args: + out (Tensor, optional): the output tensor. + """ + +def hstack( + tensors: tuple[Tensor, ...] | list[Tensor], + *, + out: Tensor | None = None, +) -> Tensor: + r""" + hstack(tensors, *, out=None) -> Tensor + + Stack tensors in sequence horizontally (column wise). + + This is equivalent to concatenation along the first axis for 1-D tensors, and along the second axis for all other tensors. + + Args: + tensors (sequence of Tensors): sequence of tensors to concatenate + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([1, 2, 3]) + >>> b = torch.tensor([4, 5, 6]) + >>> torch.hstack((a,b)) + tensor([1, 2, 3, 4, 5, 6]) + >>> a = torch.tensor([[1],[2],[3]]) + >>> b = torch.tensor([[4],[5],[6]]) + >>> torch.hstack((a,b)) + tensor([[1, 4], + [2, 5], + [3, 6]]) + """ + +def hypot( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + hypot(input, other, *, out=None) -> Tensor + + Given the legs of a right triangle, return its hypotenuse. + + .. math:: + \text{out}_{i} = \sqrt{\text{input}_{i}^{2} + \text{other}_{i}^{2}} + + The shapes of ``input`` and ``other`` must be + :ref:`broadcastable `. + + Args: + input (Tensor): the first input tensor + other (Tensor): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.hypot(torch.tensor([4.0]), torch.tensor([3.0, 4.0, 5.0])) + tensor([5.0000, 5.6569, 6.4031]) + """ + +def i0(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + i0(input, *, out=None) -> Tensor + + Alias for :func:`torch.special.i0`. + """ + +def i0_(input: Tensor) -> Tensor: ... +def igamma( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + igamma(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.special.gammainc`. + """ + +def igammac( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + igammac(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.special.gammaincc`. + """ + +def imag(input: Tensor) -> Tensor: + r""" + imag(input) -> Tensor + + Returns a new tensor containing imaginary values of the :attr:`self` tensor. + The returned tensor and :attr:`self` share the same underlying storage. + + .. warning:: + :func:`imag` is only supported for tensors with complex dtypes. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> x=torch.randn(4, dtype=torch.cfloat) + >>> x + tensor([(0.3100+0.3553j), (-0.5445-0.7896j), (-1.6492-0.0633j), (-0.0638-0.8119j)]) + >>> x.imag + tensor([ 0.3553, -0.7896, -0.0633, -0.8119]) + """ + +@overload +def index_add( + input: Tensor, + dim: _int, + index: Tensor, + source: Tensor, + *, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + index_add(input: Tensor, dim: int, index: Tensor, source: Tensor, *, alpha: Union[Number, _complex] = 1, out: Optional[Tensor]) -> Tensor # noqa: B950 + + See :meth:`~Tensor.index_add_` for function description. + """ + +@overload +def index_add( + input: Tensor, + dim: str | EllipsisType | None, + index: Tensor, + source: Tensor, + *, + alpha: Number | _complex = 1, +) -> Tensor: + r""" + index_add(input: Tensor, dim: int, index: Tensor, source: Tensor, *, alpha: Union[Number, _complex] = 1, out: Optional[Tensor]) -> Tensor # noqa: B950 + + See :meth:`~Tensor.index_add_` for function description. + """ + +@overload +def index_copy( + input: Tensor, + dim: _int, + index: Tensor, + source: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + index_copy(input: Tensor, dim: int, index: Tensor, source: Tensor, *, out: Optional[Tensor]) -> Tensor + + See :meth:`~Tensor.index_copy_` for function description. + """ + +@overload +def index_copy( + input: Tensor, + dim: str | EllipsisType | None, + index: Tensor, + source: Tensor, +) -> Tensor: + r""" + index_copy(input: Tensor, dim: int, index: Tensor, source: Tensor, *, out: Optional[Tensor]) -> Tensor + + See :meth:`~Tensor.index_copy_` for function description. + """ + +@overload +def index_fill( + input: Tensor, + dim: _int, + index: Tensor, + value: Tensor, +) -> Tensor: ... +@overload +def index_fill( + input: Tensor, + dim: str | EllipsisType | None, + index: Tensor, + value: Tensor, +) -> Tensor: ... +@overload +def index_fill( + input: Tensor, + dim: _int, + index: Tensor, + value: Number | _complex, +) -> Tensor: ... +@overload +def index_fill( + input: Tensor, + dim: str | EllipsisType | None, + index: Tensor, + value: Number | _complex, +) -> Tensor: ... +def index_put( + input: Tensor, + indices: tuple[Tensor | None, ...] | list[Tensor | None] | None, + values: Tensor, + accumulate: _bool = False, +) -> Tensor: ... +def index_put_( + input: Tensor, + indices: tuple[Tensor | None, ...] | list[Tensor | None] | None, + values: Tensor, + accumulate: _bool = False, +) -> Tensor: ... +def index_reduce( + input: Tensor, + dim: _int, + index: Tensor, + source: Tensor, + reduce: str, + *, + include_self: _bool = True, + out: Tensor | None = None, +) -> Tensor: + r""" + index_reduce(input: Tensor, dim: int, index: Tensor, source: Tensor, reduce: str, *, include_self: bool = True, out: Optional[Tensor]) -> Tensor # noqa: B950 + + See :meth:`~Tensor.index_reduce_` for function description. + """ + +@overload +def index_select( + input: Tensor, + dim: _int, + index: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + index_select(input, dim, index, *, out=None) -> Tensor + + Returns a new tensor which indexes the :attr:`input` tensor along dimension + :attr:`dim` using the entries in :attr:`index`. + + The returned tensor has the same number of dimensions as the original tensor + (:attr:`input`). The :attr:`dim`\ th dimension has the same size as the length + of :attr:`index`; other dimensions have the same size as in the original tensor. + + .. note:: The returned tensor does **not** use the same storage as the original + tensor. If :attr:`out` has a different shape than expected, we + silently change it to the correct shape, reallocating the underlying + storage if necessary. + + Args: + input (Tensor): the input tensor. + dim (int): the dimension in which we index + index (IntTensor or LongTensor): the 1-D tensor containing the indices to index + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> x = torch.randn(3, 4) + >>> x + tensor([[ 0.1427, 0.0231, -0.5414, -1.0009], + [-0.4664, 0.2647, -0.1228, -1.1068], + [-1.1734, -0.6571, 0.7230, -0.6004]]) + >>> indices = torch.tensor([0, 2]) + >>> torch.index_select(x, 0, indices) + tensor([[ 0.1427, 0.0231, -0.5414, -1.0009], + [-1.1734, -0.6571, 0.7230, -0.6004]]) + >>> torch.index_select(x, 1, indices) + tensor([[ 0.1427, -0.5414], + [-0.4664, -0.1228], + [-1.1734, 0.7230]]) + """ + +@overload +def index_select( + input: Tensor, + dim: str | EllipsisType | None, + index: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + index_select(input, dim, index, *, out=None) -> Tensor + + Returns a new tensor which indexes the :attr:`input` tensor along dimension + :attr:`dim` using the entries in :attr:`index`. + + The returned tensor has the same number of dimensions as the original tensor + (:attr:`input`). The :attr:`dim`\ th dimension has the same size as the length + of :attr:`index`; other dimensions have the same size as in the original tensor. + + .. note:: The returned tensor does **not** use the same storage as the original + tensor. If :attr:`out` has a different shape than expected, we + silently change it to the correct shape, reallocating the underlying + storage if necessary. + + Args: + input (Tensor): the input tensor. + dim (int): the dimension in which we index + index (IntTensor or LongTensor): the 1-D tensor containing the indices to index + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> x = torch.randn(3, 4) + >>> x + tensor([[ 0.1427, 0.0231, -0.5414, -1.0009], + [-0.4664, 0.2647, -0.1228, -1.1068], + [-1.1734, -0.6571, 0.7230, -0.6004]]) + >>> indices = torch.tensor([0, 2]) + >>> torch.index_select(x, 0, indices) + tensor([[ 0.1427, 0.0231, -0.5414, -1.0009], + [-1.1734, -0.6571, 0.7230, -0.6004]]) + >>> torch.index_select(x, 1, indices) + tensor([[ 0.1427, -0.5414], + [-0.4664, -0.1228], + [-1.1734, 0.7230]]) + """ + +def indices_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + Performs the same operation as :func:`torch.indices`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def init_num_threads() -> None: ... +def inner( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + inner(input, other, *, out=None) -> Tensor + + Computes the dot product for 1D tensors. For higher dimensions, sums the product + of elements from :attr:`input` and :attr:`other` along their last dimension. + + .. note:: + + If either :attr:`input` or :attr:`other` is a scalar, the result is equivalent + to `torch.mul(input, other)`. + + If both :attr:`input` and :attr:`other` are non-scalars, the size of their last + dimension must match and the result is equivalent to `torch.tensordot(input, + other, dims=([-1], [-1]))` + + Args: + input (Tensor): First input tensor + other (Tensor): Second input tensor + + Keyword args: + out (Tensor, optional): Optional output tensor to write result into. The output + shape is `input.shape[:-1] + other.shape[:-1]`. + + Example:: + + # Dot product + >>> torch.inner(torch.tensor([1, 2, 3]), torch.tensor([0, 2, 1])) + tensor(7) + + # Multidimensional input tensors + >>> a = torch.randn(2, 3) + >>> a + tensor([[0.8173, 1.0874, 1.1784], + [0.3279, 0.1234, 2.7894]]) + >>> b = torch.randn(2, 4, 3) + >>> b + tensor([[[-0.4682, -0.7159, 0.1506], + [ 0.4034, -0.3657, 1.0387], + [ 0.9892, -0.6684, 0.1774], + [ 0.9482, 1.3261, 0.3917]], + + [[ 0.4537, 0.7493, 1.1724], + [ 0.2291, 0.5749, -0.2267], + [-0.7920, 0.3607, -0.3701], + [ 1.3666, -0.5850, -1.7242]]]) + >>> torch.inner(a, b) + tensor([[[-0.9837, 1.1560, 0.2907, 2.6785], + [ 2.5671, 0.5452, -0.6912, -1.5509]], + + [[ 0.1782, 2.9843, 0.7366, 1.5672], + [ 3.5115, -0.4864, -1.2476, -4.4337]]]) + + # Scalar input + >>> torch.inner(a, torch.tensor(2)) + tensor([[1.6347, 2.1748, 2.3567], + [0.6558, 0.2469, 5.5787]]) + """ + +def instance_norm( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor | None, + running_var: Tensor | None, + use_input_stats: _bool, + momentum: _float, + eps: _float, + cudnn_enabled: _bool, +) -> Tensor: ... +def int_repr(input: Tensor) -> Tensor: ... +def inverse(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + inverse(input, *, out=None) -> Tensor + + Alias for :func:`torch.linalg.inv` + """ + +def is_complex(input: Tensor) -> _bool: + r""" + is_complex(input: Tensor) -> bool + + Returns True if the data type of :attr:`input` is a complex data type i.e., + one of ``torch.complex64``, and ``torch.complex128``. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> torch.is_complex(torch.tensor([1, 2, 3], dtype=torch.complex64)) + True + >>> torch.is_complex(torch.tensor([1, 2, 3], dtype=torch.complex128)) + True + >>> torch.is_complex(torch.tensor([1, 2, 3], dtype=torch.int32)) + False + >>> torch.is_complex(torch.tensor([1.0, 2.0, 3.0], dtype=torch.float16)) + False + """ + +def is_conj(input: Tensor) -> _bool: + r""" + is_conj(input) -> (bool) + + Returns True if the :attr:`input` is a conjugated tensor, i.e. its conjugate bit is set to `True`. + + Args: + input (Tensor): the input tensor. + """ + +def is_distributed(input: Tensor) -> _bool: ... +def is_floating_point(input: Tensor) -> _bool: + r""" + is_floating_point(input: Tensor) -> bool + + Returns True if the data type of :attr:`input` is a floating point data type i.e., + one of ``torch.float64``, ``torch.float32``, ``torch.float16``, and ``torch.bfloat16``. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> torch.is_floating_point(torch.tensor([1.0, 2.0, 3.0])) + True + >>> torch.is_floating_point(torch.tensor([1, 2, 3], dtype=torch.int32)) + False + >>> torch.is_floating_point(torch.tensor([1.0, 2.0, 3.0], dtype=torch.float16)) + True + >>> torch.is_floating_point(torch.tensor([1, 2, 3], dtype=torch.complex64)) + False + """ + +def is_grad_enabled() -> _bool: + r""" + is_grad_enabled() -> (bool) + + Returns True if grad mode is currently enabled. + """ + +def is_inference(input: Tensor) -> _bool: + r""" + is_inference(input) -> (bool) + + Returns True if :attr:`input` is an inference tensor. + + A non-view tensor is an inference tensor if and only if it was + allocated during inference mode. A view tensor is an inference + tensor if and only if the tensor it is a view of is an inference tensor. + + For details on inference mode please see + `Inference Mode `_. + + Args: + input (Tensor): the input tensor. + """ + +def is_inference_mode_enabled() -> _bool: + r""" + is_inference_mode_enabled() -> (bool) + + Returns True if inference mode is currently enabled. + """ + +def is_neg(input: Tensor) -> _bool: ... +def is_nonzero(input: Tensor) -> _bool: + r""" + is_nonzero(input) -> (bool) + + Returns True if the :attr:`input` is a single element tensor which is not equal to zero + after type conversions. + i.e. not equal to ``torch.tensor([0.])`` or ``torch.tensor([0])`` or + ``torch.tensor([False])``. + Throws a ``RuntimeError`` if ``torch.numel() != 1`` (even in case + of sparse tensors). + + Args: + input (Tensor): the input tensor. + + Examples:: + + >>> torch.is_nonzero(torch.tensor([0.])) + False + >>> torch.is_nonzero(torch.tensor([1.5])) + True + >>> torch.is_nonzero(torch.tensor([False])) + False + >>> torch.is_nonzero(torch.tensor([3])) + True + >>> torch.is_nonzero(torch.tensor([1, 3, 5])) + Traceback (most recent call last): + ... + RuntimeError: Boolean value of Tensor with more than one value is ambiguous + >>> torch.is_nonzero(torch.tensor([])) + Traceback (most recent call last): + ... + RuntimeError: Boolean value of Tensor with no values is ambiguous + """ + +def is_same_size(input: Tensor, other: Tensor) -> _bool: ... +def is_signed(input: Tensor) -> _bool: ... +def is_vulkan_available() -> _bool: ... +def isclose( + input: Tensor, + other: Tensor, + rtol: _float = 1e-05, + atol: _float = 1e-08, + equal_nan: _bool = False, +) -> Tensor: + r""" + isclose(input, other, rtol=1e-05, atol=1e-08, equal_nan=False) -> Tensor + + Returns a new tensor with boolean elements representing if each element of + :attr:`input` is "close" to the corresponding element of :attr:`other`. + Closeness is defined as: + + .. math:: + \lvert \text{input}_i - \text{other}_i \rvert \leq \texttt{rtol} \times \lvert \text{other}_i \rvert + \texttt{atol} + + + where :attr:`input` and :attr:`other` are finite. Where :attr:`input` + and/or :attr:`other` are nonfinite they are close if and only if + they are equal, with NaNs being considered equal to each other when + :attr:`equal_nan` is True. + + Args: + input (Tensor): first tensor to compare + other (Tensor): second tensor to compare + rtol (float, optional): relative tolerance. Default: 1e-05 + atol (float, optional): absolute tolerance. Default: 1e-08 + equal_nan (bool, optional): if ``True``, then two ``NaN`` s will be considered equal. Default: ``False`` + + Examples:: + + >>> torch.isclose(torch.tensor((1., 2, 3)), torch.tensor((1 + 1e-10, 3, 4))) + tensor([ True, False, False]) + >>> torch.isclose(torch.tensor((float('inf'), 4)), torch.tensor((float('inf'), 6)), rtol=.5) + tensor([True, True]) + """ + +def isfinite(input: Tensor) -> Tensor: + r""" + isfinite(input) -> Tensor + + Returns a new tensor with boolean elements representing if each element is `finite` or not. + + Real values are finite when they are not NaN, negative infinity, or infinity. + Complex values are finite when both their real and imaginary parts are finite. + + Args: + input (Tensor): the input tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is finite and False elsewhere + + Example:: + + >>> torch.isfinite(torch.tensor([1, float('inf'), 2, float('-inf'), float('nan')])) + tensor([True, False, True, False, False]) + """ + +@overload +def isin( + elements: Tensor, + test_elements: Tensor, + *, + assume_unique: _bool = False, + invert: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + isin(elements, test_elements, *, assume_unique=False, invert=False) -> Tensor + + Tests if each element of :attr:`elements` is in :attr:`test_elements`. Returns + a boolean tensor of the same shape as :attr:`elements` that is True for elements + in :attr:`test_elements` and False otherwise. + + .. note:: + One of :attr:`elements` or :attr:`test_elements` can be a scalar, but not both. + + Args: + elements (Tensor or Scalar): Input elements + test_elements (Tensor or Scalar): Values against which to test for each input element + assume_unique (bool, optional): If True, assumes both :attr:`elements` and + :attr:`test_elements` contain unique elements, which can speed up the + calculation. Default: False + invert (bool, optional): If True, inverts the boolean return tensor, resulting in True + values for elements *not* in :attr:`test_elements`. Default: False + + Returns: + A boolean tensor of the same shape as :attr:`elements` that is True for elements in + :attr:`test_elements` and False otherwise + + Example: + >>> torch.isin(torch.tensor([[1, 2], [3, 4]]), torch.tensor([2, 3])) + tensor([[False, True], + [ True, False]]) + """ + +@overload +def isin( + element: Number | _complex, + test_elements: Tensor, + *, + assume_unique: _bool = False, + invert: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + isin(elements, test_elements, *, assume_unique=False, invert=False) -> Tensor + + Tests if each element of :attr:`elements` is in :attr:`test_elements`. Returns + a boolean tensor of the same shape as :attr:`elements` that is True for elements + in :attr:`test_elements` and False otherwise. + + .. note:: + One of :attr:`elements` or :attr:`test_elements` can be a scalar, but not both. + + Args: + elements (Tensor or Scalar): Input elements + test_elements (Tensor or Scalar): Values against which to test for each input element + assume_unique (bool, optional): If True, assumes both :attr:`elements` and + :attr:`test_elements` contain unique elements, which can speed up the + calculation. Default: False + invert (bool, optional): If True, inverts the boolean return tensor, resulting in True + values for elements *not* in :attr:`test_elements`. Default: False + + Returns: + A boolean tensor of the same shape as :attr:`elements` that is True for elements in + :attr:`test_elements` and False otherwise + + Example: + >>> torch.isin(torch.tensor([[1, 2], [3, 4]]), torch.tensor([2, 3])) + tensor([[False, True], + [ True, False]]) + """ + +@overload +def isin( + elements: Tensor, + test_element: Number | _complex, + *, + assume_unique: _bool = False, + invert: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + isin(elements, test_elements, *, assume_unique=False, invert=False) -> Tensor + + Tests if each element of :attr:`elements` is in :attr:`test_elements`. Returns + a boolean tensor of the same shape as :attr:`elements` that is True for elements + in :attr:`test_elements` and False otherwise. + + .. note:: + One of :attr:`elements` or :attr:`test_elements` can be a scalar, but not both. + + Args: + elements (Tensor or Scalar): Input elements + test_elements (Tensor or Scalar): Values against which to test for each input element + assume_unique (bool, optional): If True, assumes both :attr:`elements` and + :attr:`test_elements` contain unique elements, which can speed up the + calculation. Default: False + invert (bool, optional): If True, inverts the boolean return tensor, resulting in True + values for elements *not* in :attr:`test_elements`. Default: False + + Returns: + A boolean tensor of the same shape as :attr:`elements` that is True for elements in + :attr:`test_elements` and False otherwise + + Example: + >>> torch.isin(torch.tensor([[1, 2], [3, 4]]), torch.tensor([2, 3])) + tensor([[False, True], + [ True, False]]) + """ + +def isinf(input: Tensor) -> Tensor: + r""" + isinf(input) -> Tensor + + Tests if each element of :attr:`input` is infinite + (positive or negative infinity) or not. + + .. note:: + Complex values are infinite when their real or imaginary part is + infinite. + + Args: + input (Tensor): the input tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is infinite and False elsewhere + + Example:: + + >>> torch.isinf(torch.tensor([1, float('inf'), 2, float('-inf'), float('nan')])) + tensor([False, True, False, True, False]) + """ + +def isnan(input: Tensor) -> Tensor: + r""" + isnan(input) -> Tensor + + Returns a new tensor with boolean elements representing if each element of :attr:`input` + is NaN or not. Complex values are considered NaN when either their real + and/or imaginary part is NaN. + + Arguments: + input (Tensor): the input tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is NaN and False elsewhere + + Example:: + + >>> torch.isnan(torch.tensor([1, float('nan'), 2])) + tensor([False, True, False]) + """ + +def isneginf(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + isneginf(input, *, out=None) -> Tensor + Tests if each element of :attr:`input` is negative infinity or not. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([-float('inf'), float('inf'), 1.2]) + >>> torch.isneginf(a) + tensor([ True, False, False]) + """ + +def isposinf(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + isposinf(input, *, out=None) -> Tensor + Tests if each element of :attr:`input` is positive infinity or not. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([-float('inf'), float('inf'), 1.2]) + >>> torch.isposinf(a) + tensor([False, True, False]) + """ + +def isreal(input: Tensor) -> Tensor: + r""" + isreal(input) -> Tensor + + Returns a new tensor with boolean elements representing if each element of :attr:`input` is real-valued or not. + All real-valued types are considered real. Complex values are considered real when their imaginary part is 0. + + Arguments: + input (Tensor): the input tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is real and False elsewhere + + Example:: + + >>> torch.isreal(torch.tensor([1, 1+1j, 2+0j])) + tensor([True, False, True]) + """ + +def istft( + input: Tensor, + n_fft: _int, + hop_length: _int | None = None, + win_length: _int | None = None, + window: Tensor | None = None, + center: _bool = True, + normalized: _bool = False, + onesided: _bool | None = None, + length: _int | None = None, + return_complex: _bool = False, +) -> Tensor: ... +@overload +def kaiser_window( + window_length: _int, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + kaiser_window(window_length, periodic=True, beta=12.0, *, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Computes the Kaiser window with window length :attr:`window_length` and shape parameter :attr:`beta`. + + Let I_0 be the zeroth order modified Bessel function of the first kind (see :func:`torch.i0`) and + ``N = L - 1`` if :attr:`periodic` is False and ``L`` if :attr:`periodic` is True, + where ``L`` is the :attr:`window_length`. This function computes: + + .. math:: + out_i = I_0 \left( \beta \sqrt{1 - \left( {\frac{i - N/2}{N/2}} \right) ^2 } \right) / I_0( \beta ) + + Calling ``torch.kaiser_window(L, B, periodic=True)`` is equivalent to calling + ``torch.kaiser_window(L + 1, B, periodic=False)[:-1])``. + The :attr:`periodic` argument is intended as a helpful shorthand + to produce a periodic window as input to functions like :func:`torch.stft`. + + .. note:: + If :attr:`window_length` is one, then the returned window is a single element tensor containing a one. + + + Args: + window_length (int): length of the window. + periodic (bool, optional): If True, returns a periodic window suitable for use in spectral analysis. + If False, returns a symmetric window suitable for use in filter design. + beta (float, optional): shape parameter for the window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + """ + +@overload +def kaiser_window( + window_length: _int, + periodic: _bool, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + kaiser_window(window_length, periodic=True, beta=12.0, *, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Computes the Kaiser window with window length :attr:`window_length` and shape parameter :attr:`beta`. + + Let I_0 be the zeroth order modified Bessel function of the first kind (see :func:`torch.i0`) and + ``N = L - 1`` if :attr:`periodic` is False and ``L`` if :attr:`periodic` is True, + where ``L`` is the :attr:`window_length`. This function computes: + + .. math:: + out_i = I_0 \left( \beta \sqrt{1 - \left( {\frac{i - N/2}{N/2}} \right) ^2 } \right) / I_0( \beta ) + + Calling ``torch.kaiser_window(L, B, periodic=True)`` is equivalent to calling + ``torch.kaiser_window(L + 1, B, periodic=False)[:-1])``. + The :attr:`periodic` argument is intended as a helpful shorthand + to produce a periodic window as input to functions like :func:`torch.stft`. + + .. note:: + If :attr:`window_length` is one, then the returned window is a single element tensor containing a one. + + + Args: + window_length (int): length of the window. + periodic (bool, optional): If True, returns a periodic window suitable for use in spectral analysis. + If False, returns a symmetric window suitable for use in filter design. + beta (float, optional): shape parameter for the window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + """ + +@overload +def kaiser_window( + window_length: _int, + periodic: _bool, + beta: _float, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + kaiser_window(window_length, periodic=True, beta=12.0, *, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Computes the Kaiser window with window length :attr:`window_length` and shape parameter :attr:`beta`. + + Let I_0 be the zeroth order modified Bessel function of the first kind (see :func:`torch.i0`) and + ``N = L - 1`` if :attr:`periodic` is False and ``L`` if :attr:`periodic` is True, + where ``L`` is the :attr:`window_length`. This function computes: + + .. math:: + out_i = I_0 \left( \beta \sqrt{1 - \left( {\frac{i - N/2}{N/2}} \right) ^2 } \right) / I_0( \beta ) + + Calling ``torch.kaiser_window(L, B, periodic=True)`` is equivalent to calling + ``torch.kaiser_window(L + 1, B, periodic=False)[:-1])``. + The :attr:`periodic` argument is intended as a helpful shorthand + to produce a periodic window as input to functions like :func:`torch.stft`. + + .. note:: + If :attr:`window_length` is one, then the returned window is a single element tensor containing a one. + + + Args: + window_length (int): length of the window. + periodic (bool, optional): If True, returns a periodic window suitable for use in spectral analysis. + If False, returns a symmetric window suitable for use in filter design. + beta (float, optional): shape parameter for the window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + """ + +def kl_div( + input: Tensor, + target: Tensor, + reduction: _int = 1, + *, + log_target: _bool = False, +) -> Tensor: ... +def kron( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + kron(input, other, *, out=None) -> Tensor + + Computes the Kronecker product, denoted by :math:`\otimes`, of :attr:`input` and :attr:`other`. + + If :attr:`input` is a :math:`(a_0 \times a_1 \times \dots \times a_n)` tensor and :attr:`other` is a + :math:`(b_0 \times b_1 \times \dots \times b_n)` tensor, the result will be a + :math:`(a_0*b_0 \times a_1*b_1 \times \dots \times a_n*b_n)` tensor with the following entries: + + .. math:: + (\text{input} \otimes \text{other})_{k_0, k_1, \dots, k_n} = + \text{input}_{i_0, i_1, \dots, i_n} * \text{other}_{j_0, j_1, \dots, j_n}, + + where :math:`k_t = i_t * b_t + j_t` for :math:`0 \leq t \leq n`. + If one tensor has fewer dimensions than the other it is unsqueezed until it has the same number of dimensions. + + Supports real-valued and complex-valued inputs. + + .. note:: + This function generalizes the typical definition of the Kronecker product for two matrices to two tensors, + as described above. When :attr:`input` is a :math:`(m \times n)` matrix and :attr:`other` is a + :math:`(p \times q)` matrix, the result will be a :math:`(p*m \times q*n)` block matrix: + + .. math:: + \mathbf{A} \otimes \mathbf{B}=\begin{bmatrix} + a_{11} \mathbf{B} & \cdots & a_{1 n} \mathbf{B} \\ + \vdots & \ddots & \vdots \\ + a_{m 1} \mathbf{B} & \cdots & a_{m n} \mathbf{B} \end{bmatrix} + + where :attr:`input` is :math:`\mathbf{A}` and :attr:`other` is :math:`\mathbf{B}`. + + Arguments: + input (Tensor) + other (Tensor) + + Keyword args: + out (Tensor, optional): The output tensor. Ignored if ``None``. Default: ``None`` + + Examples:: + + >>> mat1 = torch.eye(2) + >>> mat2 = torch.ones(2, 2) + >>> torch.kron(mat1, mat2) + tensor([[1., 1., 0., 0.], + [1., 1., 0., 0.], + [0., 0., 1., 1.], + [0., 0., 1., 1.]]) + + >>> mat1 = torch.eye(2) + >>> mat2 = torch.arange(1, 5).reshape(2, 2) + >>> torch.kron(mat1, mat2) + tensor([[1., 2., 0., 0.], + [3., 4., 0., 0.], + [0., 0., 1., 2.], + [0., 0., 3., 4.]]) + """ + +@overload +def kthvalue( + input: Tensor, + k: _int | SymInt, + dim: _int = -1, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.kthvalue: + r""" + kthvalue(input, k, dim=None, keepdim=False, *, out=None) -> (Tensor, LongTensor) + + Returns a namedtuple ``(values, indices)`` where ``values`` is the :attr:`k` th + smallest element of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`. And ``indices`` is the index location of each element found. + + If :attr:`dim` is not given, the last dimension of the `input` is chosen. + + If :attr:`keepdim` is ``True``, both the :attr:`values` and :attr:`indices` tensors + are the same size as :attr:`input`, except in the dimension :attr:`dim` where + they are of size 1. Otherwise, :attr:`dim` is squeezed + (see :func:`torch.squeeze`), resulting in both the :attr:`values` and + :attr:`indices` tensors having 1 fewer dimension than the :attr:`input` tensor. + + .. note:: + When :attr:`input` is a CUDA tensor and there are multiple valid + :attr:`k` th values, this function may nondeterministically return + :attr:`indices` for any of them. + + Args: + input (Tensor): the input tensor. + k (int): k for the k-th smallest element + dim (int, optional): the dimension to find the kth value along + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the output tuple of (Tensor, LongTensor) + can be optionally given to be used as output buffers + + Example:: + + >>> x = torch.arange(1., 6.) + >>> x + tensor([ 1., 2., 3., 4., 5.]) + >>> torch.kthvalue(x, 4) + torch.return_types.kthvalue(values=tensor(4.), indices=tensor(3)) + + >>> x=torch.arange(1.,7.).resize_(2,3) + >>> x + tensor([[ 1., 2., 3.], + [ 4., 5., 6.]]) + >>> torch.kthvalue(x, 2, 0, True) + torch.return_types.kthvalue(values=tensor([[4., 5., 6.]]), indices=tensor([[1, 1, 1]])) + """ + +@overload +def kthvalue( + input: Tensor, + k: _int | SymInt, + dim: str | EllipsisType | None, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.kthvalue: + r""" + kthvalue(input, k, dim=None, keepdim=False, *, out=None) -> (Tensor, LongTensor) + + Returns a namedtuple ``(values, indices)`` where ``values`` is the :attr:`k` th + smallest element of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`. And ``indices`` is the index location of each element found. + + If :attr:`dim` is not given, the last dimension of the `input` is chosen. + + If :attr:`keepdim` is ``True``, both the :attr:`values` and :attr:`indices` tensors + are the same size as :attr:`input`, except in the dimension :attr:`dim` where + they are of size 1. Otherwise, :attr:`dim` is squeezed + (see :func:`torch.squeeze`), resulting in both the :attr:`values` and + :attr:`indices` tensors having 1 fewer dimension than the :attr:`input` tensor. + + .. note:: + When :attr:`input` is a CUDA tensor and there are multiple valid + :attr:`k` th values, this function may nondeterministically return + :attr:`indices` for any of them. + + Args: + input (Tensor): the input tensor. + k (int): k for the k-th smallest element + dim (int, optional): the dimension to find the kth value along + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the output tuple of (Tensor, LongTensor) + can be optionally given to be used as output buffers + + Example:: + + >>> x = torch.arange(1., 6.) + >>> x + tensor([ 1., 2., 3., 4., 5.]) + >>> torch.kthvalue(x, 4) + torch.return_types.kthvalue(values=tensor(4.), indices=tensor(3)) + + >>> x=torch.arange(1.,7.).resize_(2,3) + >>> x + tensor([[ 1., 2., 3.], + [ 4., 5., 6.]]) + >>> torch.kthvalue(x, 2, 0, True) + torch.return_types.kthvalue(values=tensor([[4., 5., 6.]]), indices=tensor([[1, 1, 1]])) + """ + +def layer_norm( + input: Tensor, + normalized_shape: Sequence[_int | SymInt], + weight: Tensor | None = None, + bias: Tensor | None = None, + eps: _float = 1e-05, + cudnn_enable: _bool = True, +) -> Tensor: ... +def lcm( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + lcm(input, other, *, out=None) -> Tensor + + Computes the element-wise least common multiple (LCM) of :attr:`input` and :attr:`other`. + + Both :attr:`input` and :attr:`other` must have integer types. + + .. note:: + This defines :math:`lcm(0, 0) = 0` and :math:`lcm(0, a) = 0`. + + Args: + input (Tensor): the input tensor. + other (Tensor): the second input tensor + + Keyword arguments: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([5, 10, 15]) + >>> b = torch.tensor([3, 4, 5]) + >>> torch.lcm(a, b) + tensor([15, 20, 15]) + >>> c = torch.tensor([3]) + >>> torch.lcm(a, c) + tensor([15, 30, 15]) + """ + +def lcm_(input: Tensor, other: Tensor) -> Tensor: ... +def ldexp( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + ldexp(input, other, *, out=None) -> Tensor + + Multiplies :attr:`input` by 2 ** :attr:`other`. + + .. math:: + \text{{out}}_i = \text{{input}}_i * 2^\text{{other}}_i + + + Typically this function is used to construct floating point numbers by multiplying + mantissas in :attr:`input` with integral powers of two created from the exponents + in :attr:`other`. + + Args: + input (Tensor): the input tensor. + other (Tensor): a tensor of exponents, typically integers. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.ldexp(torch.tensor([1.]), torch.tensor([1])) + tensor([2.]) + >>> torch.ldexp(torch.tensor([1.0]), torch.tensor([1, 2, 3, 4])) + tensor([ 2., 4., 8., 16.]) + """ + +def ldexp_(input: Tensor, other: Tensor) -> Tensor: ... +@overload +def le( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + le(input, other, *, out=None) -> Tensor + + Computes :math:`\text{input} \leq \text{other}` element-wise. + + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or Scalar): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is less than or equal to + :attr:`other` and False elsewhere + + Example:: + + >>> torch.le(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[True, False], [True, True]]) + """ + +@overload +def le( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + le(input, other, *, out=None) -> Tensor + + Computes :math:`\text{input} \leq \text{other}` element-wise. + + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or Scalar): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is less than or equal to + :attr:`other` and False elsewhere + + Example:: + + >>> torch.le(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[True, False], [True, True]]) + """ + +@overload +def lerp( + input: Tensor, + end: Tensor, + weight: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + lerp(input, end, weight, *, out=None) + + Does a linear interpolation of two tensors :attr:`start` (given by :attr:`input`) and :attr:`end` based + on a scalar or tensor :attr:`weight` and returns the resulting :attr:`out` tensor. + + .. math:: + \text{out}_i = \text{start}_i + \text{weight}_i \times (\text{end}_i - \text{start}_i) + + The shapes of :attr:`start` and :attr:`end` must be + :ref:`broadcastable `. If :attr:`weight` is a tensor, then + the shapes of :attr:`weight`, :attr:`start`, and :attr:`end` must be :ref:`broadcastable `. + + Args: + input (Tensor): the tensor with the starting points + end (Tensor): the tensor with the ending points + weight (float or tensor): the weight for the interpolation formula + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> start = torch.arange(1., 5.) + >>> end = torch.empty(4).fill_(10) + >>> start + tensor([ 1., 2., 3., 4.]) + >>> end + tensor([ 10., 10., 10., 10.]) + >>> torch.lerp(start, end, 0.5) + tensor([ 5.5000, 6.0000, 6.5000, 7.0000]) + >>> torch.lerp(start, end, torch.full_like(start, 0.5)) + tensor([ 5.5000, 6.0000, 6.5000, 7.0000]) + """ + +@overload +def lerp( + input: Tensor, + end: Tensor, + weight: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + lerp(input, end, weight, *, out=None) + + Does a linear interpolation of two tensors :attr:`start` (given by :attr:`input`) and :attr:`end` based + on a scalar or tensor :attr:`weight` and returns the resulting :attr:`out` tensor. + + .. math:: + \text{out}_i = \text{start}_i + \text{weight}_i \times (\text{end}_i - \text{start}_i) + + The shapes of :attr:`start` and :attr:`end` must be + :ref:`broadcastable `. If :attr:`weight` is a tensor, then + the shapes of :attr:`weight`, :attr:`start`, and :attr:`end` must be :ref:`broadcastable `. + + Args: + input (Tensor): the tensor with the starting points + end (Tensor): the tensor with the ending points + weight (float or tensor): the weight for the interpolation formula + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> start = torch.arange(1., 5.) + >>> end = torch.empty(4).fill_(10) + >>> start + tensor([ 1., 2., 3., 4.]) + >>> end + tensor([ 10., 10., 10., 10.]) + >>> torch.lerp(start, end, 0.5) + tensor([ 5.5000, 6.0000, 6.5000, 7.0000]) + >>> torch.lerp(start, end, torch.full_like(start, 0.5)) + tensor([ 5.5000, 6.0000, 6.5000, 7.0000]) + """ + +@overload +def less( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + less(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.lt`. + """ + +@overload +def less( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + less(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.lt`. + """ + +@overload +def less_equal( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + less_equal(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.le`. + """ + +@overload +def less_equal( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + less_equal(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.le`. + """ + +def lgamma(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + lgamma(input, *, out=None) -> Tensor + + Computes the natural logarithm of the absolute value of the gamma function on :attr:`input`. + + .. math:: + \text{out}_{i} = \ln |\Gamma(\text{input}_{i})| + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.arange(0.5, 2, 0.5) + >>> torch.lgamma(a) + tensor([ 0.5724, 0.0000, -0.1208]) + """ + +@overload +def linspace( + start: Number, + end: Number, + steps: _int | None = None, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + linspace(start, end, steps, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Creates a one-dimensional tensor of size :attr:`steps` whose values are evenly + spaced from :attr:`start` to :attr:`end`, inclusive. That is, the value are: + + .. math:: + (\text{start}, + \text{start} + \frac{\text{end} - \text{start}}{\text{steps} - 1}, + \ldots, + \text{start} + (\text{steps} - 2) * \frac{\text{end} - \text{start}}{\text{steps} - 1}, + \text{end}) + + + From PyTorch 1.11 linspace requires the steps argument. Use steps=100 to restore the previous behavior. + + Args: + start (float or Tensor): the starting value for the set of points. If `Tensor`, it must be 0-dimensional + end (float or Tensor): the ending value for the set of points. If `Tensor`, it must be 0-dimensional + steps (int): size of the constructed tensor + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the data type to perform the computation in. + Default: if None, uses the global default dtype (see torch.get_default_dtype()) + when both :attr:`start` and :attr:`end` are real, + and corresponding complex dtype when either is complex. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + + Example:: + + >>> torch.linspace(3, 10, steps=5) + tensor([ 3.0000, 4.7500, 6.5000, 8.2500, 10.0000]) + >>> torch.linspace(-10, 10, steps=5) + tensor([-10., -5., 0., 5., 10.]) + >>> torch.linspace(start=-10, end=10, steps=5) + tensor([-10., -5., 0., 5., 10.]) + >>> torch.linspace(start=-10, end=10, steps=1) + tensor([-10.]) + """ + +@overload +def linspace( + start: Tensor, + end: Tensor, + steps: _int, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + linspace(start, end, steps, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Creates a one-dimensional tensor of size :attr:`steps` whose values are evenly + spaced from :attr:`start` to :attr:`end`, inclusive. That is, the value are: + + .. math:: + (\text{start}, + \text{start} + \frac{\text{end} - \text{start}}{\text{steps} - 1}, + \ldots, + \text{start} + (\text{steps} - 2) * \frac{\text{end} - \text{start}}{\text{steps} - 1}, + \text{end}) + + + From PyTorch 1.11 linspace requires the steps argument. Use steps=100 to restore the previous behavior. + + Args: + start (float or Tensor): the starting value for the set of points. If `Tensor`, it must be 0-dimensional + end (float or Tensor): the ending value for the set of points. If `Tensor`, it must be 0-dimensional + steps (int): size of the constructed tensor + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the data type to perform the computation in. + Default: if None, uses the global default dtype (see torch.get_default_dtype()) + when both :attr:`start` and :attr:`end` are real, + and corresponding complex dtype when either is complex. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + + Example:: + + >>> torch.linspace(3, 10, steps=5) + tensor([ 3.0000, 4.7500, 6.5000, 8.2500, 10.0000]) + >>> torch.linspace(-10, 10, steps=5) + tensor([-10., -5., 0., 5., 10.]) + >>> torch.linspace(start=-10, end=10, steps=5) + tensor([-10., -5., 0., 5., 10.]) + >>> torch.linspace(start=-10, end=10, steps=1) + tensor([-10.]) + """ + +@overload +def linspace( + start: Number | _complex, + end: Tensor, + steps: _int, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + linspace(start, end, steps, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Creates a one-dimensional tensor of size :attr:`steps` whose values are evenly + spaced from :attr:`start` to :attr:`end`, inclusive. That is, the value are: + + .. math:: + (\text{start}, + \text{start} + \frac{\text{end} - \text{start}}{\text{steps} - 1}, + \ldots, + \text{start} + (\text{steps} - 2) * \frac{\text{end} - \text{start}}{\text{steps} - 1}, + \text{end}) + + + From PyTorch 1.11 linspace requires the steps argument. Use steps=100 to restore the previous behavior. + + Args: + start (float or Tensor): the starting value for the set of points. If `Tensor`, it must be 0-dimensional + end (float or Tensor): the ending value for the set of points. If `Tensor`, it must be 0-dimensional + steps (int): size of the constructed tensor + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the data type to perform the computation in. + Default: if None, uses the global default dtype (see torch.get_default_dtype()) + when both :attr:`start` and :attr:`end` are real, + and corresponding complex dtype when either is complex. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + + Example:: + + >>> torch.linspace(3, 10, steps=5) + tensor([ 3.0000, 4.7500, 6.5000, 8.2500, 10.0000]) + >>> torch.linspace(-10, 10, steps=5) + tensor([-10., -5., 0., 5., 10.]) + >>> torch.linspace(start=-10, end=10, steps=5) + tensor([-10., -5., 0., 5., 10.]) + >>> torch.linspace(start=-10, end=10, steps=1) + tensor([-10.]) + """ + +@overload +def linspace( + start: Tensor, + end: Number | _complex, + steps: _int, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + linspace(start, end, steps, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Creates a one-dimensional tensor of size :attr:`steps` whose values are evenly + spaced from :attr:`start` to :attr:`end`, inclusive. That is, the value are: + + .. math:: + (\text{start}, + \text{start} + \frac{\text{end} - \text{start}}{\text{steps} - 1}, + \ldots, + \text{start} + (\text{steps} - 2) * \frac{\text{end} - \text{start}}{\text{steps} - 1}, + \text{end}) + + + From PyTorch 1.11 linspace requires the steps argument. Use steps=100 to restore the previous behavior. + + Args: + start (float or Tensor): the starting value for the set of points. If `Tensor`, it must be 0-dimensional + end (float or Tensor): the ending value for the set of points. If `Tensor`, it must be 0-dimensional + steps (int): size of the constructed tensor + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the data type to perform the computation in. + Default: if None, uses the global default dtype (see torch.get_default_dtype()) + when both :attr:`start` and :attr:`end` are real, + and corresponding complex dtype when either is complex. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + + Example:: + + >>> torch.linspace(3, 10, steps=5) + tensor([ 3.0000, 4.7500, 6.5000, 8.2500, 10.0000]) + >>> torch.linspace(-10, 10, steps=5) + tensor([-10., -5., 0., 5., 10.]) + >>> torch.linspace(start=-10, end=10, steps=5) + tensor([-10., -5., 0., 5., 10.]) + >>> torch.linspace(start=-10, end=10, steps=1) + tensor([-10.]) + """ + +@overload +def linspace( + start: Number | _complex, + end: Number | _complex, + steps: _int, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + linspace(start, end, steps, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Creates a one-dimensional tensor of size :attr:`steps` whose values are evenly + spaced from :attr:`start` to :attr:`end`, inclusive. That is, the value are: + + .. math:: + (\text{start}, + \text{start} + \frac{\text{end} - \text{start}}{\text{steps} - 1}, + \ldots, + \text{start} + (\text{steps} - 2) * \frac{\text{end} - \text{start}}{\text{steps} - 1}, + \text{end}) + + + From PyTorch 1.11 linspace requires the steps argument. Use steps=100 to restore the previous behavior. + + Args: + start (float or Tensor): the starting value for the set of points. If `Tensor`, it must be 0-dimensional + end (float or Tensor): the ending value for the set of points. If `Tensor`, it must be 0-dimensional + steps (int): size of the constructed tensor + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the data type to perform the computation in. + Default: if None, uses the global default dtype (see torch.get_default_dtype()) + when both :attr:`start` and :attr:`end` are real, + and corresponding complex dtype when either is complex. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + + Example:: + + >>> torch.linspace(3, 10, steps=5) + tensor([ 3.0000, 4.7500, 6.5000, 8.2500, 10.0000]) + >>> torch.linspace(-10, 10, steps=5) + tensor([-10., -5., 0., 5., 10.]) + >>> torch.linspace(start=-10, end=10, steps=5) + tensor([-10., -5., 0., 5., 10.]) + >>> torch.linspace(start=-10, end=10, steps=1) + tensor([-10.]) + """ + +def log(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + log(input, *, out=None) -> Tensor + + Returns a new tensor with the natural logarithm of the elements + of :attr:`input`. + + .. math:: + y_{i} = \log_{e} (x_{i}) + + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(5) * 5 + >>> a + tensor([4.7767, 4.3234, 1.2156, 0.2411, 4.5739]) + >>> torch.log(a) + tensor([ 1.5637, 1.4640, 0.1952, -1.4226, 1.5204]) + """ + +def log10(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + log10(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Returns a new tensor with the logarithm to the base 10 of the elements + of :attr:`input`. + + .. math:: + y_{i} = \log_{10} (x_{i}) + + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(5) + >>> a + tensor([ 0.5224, 0.9354, 0.7257, 0.1301, 0.2251]) + + + >>> torch.log10(a) + tensor([-0.2820, -0.0290, -0.1392, -0.8857, -0.6476]) + """ + +def log10_(input: Tensor) -> Tensor: ... +def log1p(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + log1p(input, *, out=None) -> Tensor + + Returns a new tensor with the natural logarithm of (1 + :attr:`input`). + + .. math:: + y_i = \log_{e} (x_i + 1) + + .. note:: This function is more accurate than :func:`torch.log` for small + values of :attr:`input` + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(5) + >>> a + tensor([-1.0090, -0.9923, 1.0249, -0.5372, 0.2492]) + >>> torch.log1p(a) + tensor([ nan, -4.8653, 0.7055, -0.7705, 0.2225]) + """ + +def log1p_(input: Tensor) -> Tensor: ... +def log2(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + log2(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Returns a new tensor with the logarithm to the base 2 of the elements + of :attr:`input`. + + .. math:: + y_{i} = \log_{2} (x_{i}) + + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(5) + >>> a + tensor([ 0.8419, 0.8003, 0.9971, 0.5287, 0.0490]) + + + >>> torch.log2(a) + tensor([-0.2483, -0.3213, -0.0042, -0.9196, -4.3504]) + """ + +def log2_(input: Tensor) -> Tensor: ... +def log_(input: Tensor) -> Tensor: ... +@overload +def log_softmax( + input: Tensor, + dim: _int, + dtype: _dtype | None = None, + *, + out: Tensor | None = None, +) -> Tensor: ... +@overload +def log_softmax( + input: Tensor, + dim: str | EllipsisType | None, + *, + dtype: _dtype | None = None, +) -> Tensor: ... +def logaddexp( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + logaddexp(input, other, *, out=None) -> Tensor + + Logarithm of the sum of exponentiations of the inputs. + + Calculates pointwise :math:`\log\left(e^x + e^y\right)`. This function is useful + in statistics where the calculated probabilities of events may be so small as to + exceed the range of normal floating point numbers. In such cases the logarithm + of the calculated probability is stored. This function allows adding + probabilities stored in such a fashion. + + This op should be disambiguated with :func:`torch.logsumexp` which performs a + reduction on a single tensor. + + Args: + input (Tensor): the input tensor. + other (Tensor): the second input tensor + + Keyword arguments: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.logaddexp(torch.tensor([-1.0]), torch.tensor([-1.0, -2, -3])) + tensor([-0.3069, -0.6867, -0.8731]) + >>> torch.logaddexp(torch.tensor([-100.0, -200, -300]), torch.tensor([-1.0, -2, -3])) + tensor([-1., -2., -3.]) + >>> torch.logaddexp(torch.tensor([1.0, 2000, 30000]), torch.tensor([-1.0, -2, -3])) + tensor([1.1269e+00, 2.0000e+03, 3.0000e+04]) + """ + +def logaddexp2( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + logaddexp2(input, other, *, out=None) -> Tensor + + Logarithm of the sum of exponentiations of the inputs in base-2. + + Calculates pointwise :math:`\log_2\left(2^x + 2^y\right)`. See + :func:`torch.logaddexp` for more details. + + Args: + input (Tensor): the input tensor. + other (Tensor): the second input tensor + + Keyword arguments: + out (Tensor, optional): the output tensor. + """ + +@overload +def logcumsumexp( + input: Tensor, + dim: _int, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + logcumsumexp(input, dim, *, out=None) -> Tensor + Returns the logarithm of the cumulative summation of the exponentiation of + elements of :attr:`input` in the dimension :attr:`dim`. + + For summation index :math:`j` given by `dim` and other indices :math:`i`, the result is + + .. math:: + \text{logcumsumexp}(x)_{ij} = \log \sum\limits_{k=0}^{j} \exp(x_{ik}) + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to do the operation over + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(10) + >>> torch.logcumsumexp(a, dim=0) + tensor([-0.42296738, -0.04462666, 0.86278635, 0.94622083, 1.05277811, + 1.39202815, 1.83525007, 1.84492621, 2.06084887, 2.06844475])) + """ + +@overload +def logcumsumexp( + input: Tensor, + dim: str | EllipsisType | None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + logcumsumexp(input, dim, *, out=None) -> Tensor + Returns the logarithm of the cumulative summation of the exponentiation of + elements of :attr:`input` in the dimension :attr:`dim`. + + For summation index :math:`j` given by `dim` and other indices :math:`i`, the result is + + .. math:: + \text{logcumsumexp}(x)_{ij} = \log \sum\limits_{k=0}^{j} \exp(x_{ik}) + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to do the operation over + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(10) + >>> torch.logcumsumexp(a, dim=0) + tensor([-0.42296738, -0.04462666, 0.86278635, 0.94622083, 1.05277811, + 1.39202815, 1.83525007, 1.84492621, 2.06084887, 2.06844475])) + """ + +def logdet(input: Tensor) -> Tensor: + r""" + logdet(input) -> Tensor + + Calculates log determinant of a square matrix or batches of square matrices. + + It returns ``-inf`` if the input has a determinant of zero, and ``NaN`` if it has + a negative determinant. + + .. note:: + Backward through :meth:`logdet` internally uses SVD results when :attr:`input` + is not invertible. In this case, double backward through :meth:`logdet` will + be unstable in when :attr:`input` doesn't have distinct singular values. See + :func:`torch.linalg.svd` for details. + + .. seealso:: + + :func:`torch.linalg.slogdet` computes the sign (resp. angle) and natural logarithm of the + absolute value of the determinant of real-valued (resp. complex) square matrices. + + Arguments: + input (Tensor): the input tensor of size ``(*, n, n)`` where ``*`` is zero or more + batch dimensions. + + Example:: + + >>> A = torch.randn(3, 3) + >>> torch.det(A) + tensor(0.2611) + >>> torch.logdet(A) + tensor(-1.3430) + >>> A + tensor([[[ 0.9254, -0.6213], + [-0.5787, 1.6843]], + + [[ 0.3242, -0.9665], + [ 0.4539, -0.0887]], + + [[ 1.1336, -0.4025], + [-0.7089, 0.9032]]]) + >>> A.det() + tensor([1.1990, 0.4099, 0.7386]) + >>> A.det().log() + tensor([ 0.1815, -0.8917, -0.3031]) + """ + +def logical_and( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + logical_and(input, other, *, out=None) -> Tensor + + Computes the element-wise logical AND of the given input tensors. Zeros are treated as ``False`` and nonzeros are + treated as ``True``. + + Args: + input (Tensor): the input tensor. + other (Tensor): the tensor to compute AND with + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.logical_and(torch.tensor([True, False, True]), torch.tensor([True, False, False])) + tensor([ True, False, False]) + >>> a = torch.tensor([0, 1, 10, 0], dtype=torch.int8) + >>> b = torch.tensor([4, 0, 1, 0], dtype=torch.int8) + >>> torch.logical_and(a, b) + tensor([False, False, True, False]) + >>> torch.logical_and(a.double(), b.double()) + tensor([False, False, True, False]) + >>> torch.logical_and(a.double(), b) + tensor([False, False, True, False]) + >>> torch.logical_and(a, b, out=torch.empty(4, dtype=torch.bool)) + tensor([False, False, True, False]) + """ + +def logical_not(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + logical_not(input, *, out=None) -> Tensor + + Computes the element-wise logical NOT of the given input tensor. If not specified, the output tensor will have the bool + dtype. If the input tensor is not a bool tensor, zeros are treated as ``False`` and non-zeros are treated as ``True``. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.logical_not(torch.tensor([True, False])) + tensor([False, True]) + >>> torch.logical_not(torch.tensor([0, 1, -10], dtype=torch.int8)) + tensor([ True, False, False]) + >>> torch.logical_not(torch.tensor([0., 1.5, -10.], dtype=torch.double)) + tensor([ True, False, False]) + >>> torch.logical_not(torch.tensor([0., 1., -10.], dtype=torch.double), out=torch.empty(3, dtype=torch.int16)) + tensor([1, 0, 0], dtype=torch.int16) + """ + +def logical_or( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + logical_or(input, other, *, out=None) -> Tensor + + Computes the element-wise logical OR of the given input tensors. Zeros are treated as ``False`` and nonzeros are + treated as ``True``. + + Args: + input (Tensor): the input tensor. + other (Tensor): the tensor to compute OR with + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.logical_or(torch.tensor([True, False, True]), torch.tensor([True, False, False])) + tensor([ True, False, True]) + >>> a = torch.tensor([0, 1, 10, 0], dtype=torch.int8) + >>> b = torch.tensor([4, 0, 1, 0], dtype=torch.int8) + >>> torch.logical_or(a, b) + tensor([ True, True, True, False]) + >>> torch.logical_or(a.double(), b.double()) + tensor([ True, True, True, False]) + >>> torch.logical_or(a.double(), b) + tensor([ True, True, True, False]) + >>> torch.logical_or(a, b, out=torch.empty(4, dtype=torch.bool)) + tensor([ True, True, True, False]) + """ + +def logical_xor( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + logical_xor(input: Tensor, other: Tensor, *, out: Optional[Tensor]) -> Tensor + + Computes the element-wise logical XOR of the given input tensors. Zeros are treated as ``False`` and nonzeros are + treated as ``True``. + + Args: + input (Tensor): the input tensor. + other (Tensor): the tensor to compute XOR with + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.logical_xor(torch.tensor([True, False, True]), torch.tensor([True, False, False])) + tensor([False, False, True]) + >>> a = torch.tensor([0, 1, 10, 0], dtype=torch.int8) + >>> b = torch.tensor([4, 0, 1, 0], dtype=torch.int8) + >>> torch.logical_xor(a, b) + tensor([ True, True, False, False]) + >>> torch.logical_xor(a.double(), b.double()) + tensor([ True, True, False, False]) + >>> torch.logical_xor(a.double(), b) + tensor([ True, True, False, False]) + >>> torch.logical_xor(a, b, out=torch.empty(4, dtype=torch.bool)) + tensor([ True, True, False, False]) + """ + +def logit( + input: Tensor, + eps: _float | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + logit(input, eps=None, *, out=None) -> Tensor + + Alias for :func:`torch.special.logit`. + """ + +def logit_(input: Tensor, eps: _float | None = None) -> Tensor: ... +@overload +def logspace( + start: Number, + end: Number, + steps: _int | None = None, + base: _float = 10.0, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + logspace(start, end, steps, base=10.0, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + + Creates a one-dimensional tensor of size :attr:`steps` whose values are evenly + spaced from :math:`{{\text{{base}}}}^{{\text{{start}}}}` to + :math:`{{\text{{base}}}}^{{\text{{end}}}}`, inclusive, on a logarithmic scale + with base :attr:`base`. That is, the values are: + + .. math:: + (\text{base}^{\text{start}}, + \text{base}^{(\text{start} + \frac{\text{end} - \text{start}}{ \text{steps} - 1})}, + \ldots, + \text{base}^{(\text{start} + (\text{steps} - 2) * \frac{\text{end} - \text{start}}{ \text{steps} - 1})}, + \text{base}^{\text{end}}) + + + + From PyTorch 1.11 logspace requires the steps argument. Use steps=100 to restore the previous behavior. + + Args: + start (float or Tensor): the starting value for the set of points. If `Tensor`, it must be 0-dimensional + end (float or Tensor): the ending value for the set of points. If `Tensor`, it must be 0-dimensional + steps (int): size of the constructed tensor + base (float, optional): base of the logarithm function. Default: ``10.0``. + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the data type to perform the computation in. + Default: if None, uses the global default dtype (see torch.get_default_dtype()) + when both :attr:`start` and :attr:`end` are real, + and corresponding complex dtype when either is complex. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.logspace(start=-10, end=10, steps=5) + tensor([ 1.0000e-10, 1.0000e-05, 1.0000e+00, 1.0000e+05, 1.0000e+10]) + >>> torch.logspace(start=0.1, end=1.0, steps=5) + tensor([ 1.2589, 2.1135, 3.5481, 5.9566, 10.0000]) + >>> torch.logspace(start=0.1, end=1.0, steps=1) + tensor([1.2589]) + >>> torch.logspace(start=2, end=2, steps=1, base=2) + tensor([4.0]) + """ + +@overload +def logspace( + start: Tensor, + end: Tensor, + steps: _int, + base: _float = 10.0, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + logspace(start, end, steps, base=10.0, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + + Creates a one-dimensional tensor of size :attr:`steps` whose values are evenly + spaced from :math:`{{\text{{base}}}}^{{\text{{start}}}}` to + :math:`{{\text{{base}}}}^{{\text{{end}}}}`, inclusive, on a logarithmic scale + with base :attr:`base`. That is, the values are: + + .. math:: + (\text{base}^{\text{start}}, + \text{base}^{(\text{start} + \frac{\text{end} - \text{start}}{ \text{steps} - 1})}, + \ldots, + \text{base}^{(\text{start} + (\text{steps} - 2) * \frac{\text{end} - \text{start}}{ \text{steps} - 1})}, + \text{base}^{\text{end}}) + + + + From PyTorch 1.11 logspace requires the steps argument. Use steps=100 to restore the previous behavior. + + Args: + start (float or Tensor): the starting value for the set of points. If `Tensor`, it must be 0-dimensional + end (float or Tensor): the ending value for the set of points. If `Tensor`, it must be 0-dimensional + steps (int): size of the constructed tensor + base (float, optional): base of the logarithm function. Default: ``10.0``. + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the data type to perform the computation in. + Default: if None, uses the global default dtype (see torch.get_default_dtype()) + when both :attr:`start` and :attr:`end` are real, + and corresponding complex dtype when either is complex. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.logspace(start=-10, end=10, steps=5) + tensor([ 1.0000e-10, 1.0000e-05, 1.0000e+00, 1.0000e+05, 1.0000e+10]) + >>> torch.logspace(start=0.1, end=1.0, steps=5) + tensor([ 1.2589, 2.1135, 3.5481, 5.9566, 10.0000]) + >>> torch.logspace(start=0.1, end=1.0, steps=1) + tensor([1.2589]) + >>> torch.logspace(start=2, end=2, steps=1, base=2) + tensor([4.0]) + """ + +@overload +def logspace( + start: Number | _complex, + end: Tensor, + steps: _int, + base: _float = 10.0, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + logspace(start, end, steps, base=10.0, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + + Creates a one-dimensional tensor of size :attr:`steps` whose values are evenly + spaced from :math:`{{\text{{base}}}}^{{\text{{start}}}}` to + :math:`{{\text{{base}}}}^{{\text{{end}}}}`, inclusive, on a logarithmic scale + with base :attr:`base`. That is, the values are: + + .. math:: + (\text{base}^{\text{start}}, + \text{base}^{(\text{start} + \frac{\text{end} - \text{start}}{ \text{steps} - 1})}, + \ldots, + \text{base}^{(\text{start} + (\text{steps} - 2) * \frac{\text{end} - \text{start}}{ \text{steps} - 1})}, + \text{base}^{\text{end}}) + + + + From PyTorch 1.11 logspace requires the steps argument. Use steps=100 to restore the previous behavior. + + Args: + start (float or Tensor): the starting value for the set of points. If `Tensor`, it must be 0-dimensional + end (float or Tensor): the ending value for the set of points. If `Tensor`, it must be 0-dimensional + steps (int): size of the constructed tensor + base (float, optional): base of the logarithm function. Default: ``10.0``. + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the data type to perform the computation in. + Default: if None, uses the global default dtype (see torch.get_default_dtype()) + when both :attr:`start` and :attr:`end` are real, + and corresponding complex dtype when either is complex. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.logspace(start=-10, end=10, steps=5) + tensor([ 1.0000e-10, 1.0000e-05, 1.0000e+00, 1.0000e+05, 1.0000e+10]) + >>> torch.logspace(start=0.1, end=1.0, steps=5) + tensor([ 1.2589, 2.1135, 3.5481, 5.9566, 10.0000]) + >>> torch.logspace(start=0.1, end=1.0, steps=1) + tensor([1.2589]) + >>> torch.logspace(start=2, end=2, steps=1, base=2) + tensor([4.0]) + """ + +@overload +def logspace( + start: Tensor, + end: Number | _complex, + steps: _int, + base: _float = 10.0, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + logspace(start, end, steps, base=10.0, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + + Creates a one-dimensional tensor of size :attr:`steps` whose values are evenly + spaced from :math:`{{\text{{base}}}}^{{\text{{start}}}}` to + :math:`{{\text{{base}}}}^{{\text{{end}}}}`, inclusive, on a logarithmic scale + with base :attr:`base`. That is, the values are: + + .. math:: + (\text{base}^{\text{start}}, + \text{base}^{(\text{start} + \frac{\text{end} - \text{start}}{ \text{steps} - 1})}, + \ldots, + \text{base}^{(\text{start} + (\text{steps} - 2) * \frac{\text{end} - \text{start}}{ \text{steps} - 1})}, + \text{base}^{\text{end}}) + + + + From PyTorch 1.11 logspace requires the steps argument. Use steps=100 to restore the previous behavior. + + Args: + start (float or Tensor): the starting value for the set of points. If `Tensor`, it must be 0-dimensional + end (float or Tensor): the ending value for the set of points. If `Tensor`, it must be 0-dimensional + steps (int): size of the constructed tensor + base (float, optional): base of the logarithm function. Default: ``10.0``. + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the data type to perform the computation in. + Default: if None, uses the global default dtype (see torch.get_default_dtype()) + when both :attr:`start` and :attr:`end` are real, + and corresponding complex dtype when either is complex. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.logspace(start=-10, end=10, steps=5) + tensor([ 1.0000e-10, 1.0000e-05, 1.0000e+00, 1.0000e+05, 1.0000e+10]) + >>> torch.logspace(start=0.1, end=1.0, steps=5) + tensor([ 1.2589, 2.1135, 3.5481, 5.9566, 10.0000]) + >>> torch.logspace(start=0.1, end=1.0, steps=1) + tensor([1.2589]) + >>> torch.logspace(start=2, end=2, steps=1, base=2) + tensor([4.0]) + """ + +@overload +def logspace( + start: Number | _complex, + end: Number | _complex, + steps: _int, + base: _float = 10.0, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + logspace(start, end, steps, base=10.0, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + + Creates a one-dimensional tensor of size :attr:`steps` whose values are evenly + spaced from :math:`{{\text{{base}}}}^{{\text{{start}}}}` to + :math:`{{\text{{base}}}}^{{\text{{end}}}}`, inclusive, on a logarithmic scale + with base :attr:`base`. That is, the values are: + + .. math:: + (\text{base}^{\text{start}}, + \text{base}^{(\text{start} + \frac{\text{end} - \text{start}}{ \text{steps} - 1})}, + \ldots, + \text{base}^{(\text{start} + (\text{steps} - 2) * \frac{\text{end} - \text{start}}{ \text{steps} - 1})}, + \text{base}^{\text{end}}) + + + + From PyTorch 1.11 logspace requires the steps argument. Use steps=100 to restore the previous behavior. + + Args: + start (float or Tensor): the starting value for the set of points. If `Tensor`, it must be 0-dimensional + end (float or Tensor): the ending value for the set of points. If `Tensor`, it must be 0-dimensional + steps (int): size of the constructed tensor + base (float, optional): base of the logarithm function. Default: ``10.0``. + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the data type to perform the computation in. + Default: if None, uses the global default dtype (see torch.get_default_dtype()) + when both :attr:`start` and :attr:`end` are real, + and corresponding complex dtype when either is complex. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.logspace(start=-10, end=10, steps=5) + tensor([ 1.0000e-10, 1.0000e-05, 1.0000e+00, 1.0000e+05, 1.0000e+10]) + >>> torch.logspace(start=0.1, end=1.0, steps=5) + tensor([ 1.2589, 2.1135, 3.5481, 5.9566, 10.0000]) + >>> torch.logspace(start=0.1, end=1.0, steps=1) + tensor([1.2589]) + >>> torch.logspace(start=2, end=2, steps=1, base=2) + tensor([4.0]) + """ + +@overload +def logsumexp( + input: Tensor, + dim: _int | _size, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + logsumexp(input, dim, keepdim=False, *, out=None) + + Returns the log of summed exponentials of each row of the :attr:`input` + tensor in the given dimension :attr:`dim`. The computation is numerically + stabilized. + + For summation index :math:`j` given by `dim` and other indices :math:`i`, the result is + + .. math:: + \text{logsumexp}(x)_{i} = \log \sum_j \exp(x_{ij}) + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + dim (int or tuple of ints): the dimension or dimensions to reduce. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(3, 3) + >>> torch.logsumexp(a, 1) + tensor([1.4907, 1.0593, 1.5696]) + >>> torch.dist(torch.logsumexp(a, 1), torch.log(torch.sum(torch.exp(a), 1))) + tensor(1.6859e-07) + """ + +@overload +def logsumexp( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + logsumexp(input, dim, keepdim=False, *, out=None) + + Returns the log of summed exponentials of each row of the :attr:`input` + tensor in the given dimension :attr:`dim`. The computation is numerically + stabilized. + + For summation index :math:`j` given by `dim` and other indices :math:`i`, the result is + + .. math:: + \text{logsumexp}(x)_{i} = \log \sum_j \exp(x_{ij}) + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + dim (int or tuple of ints): the dimension or dimensions to reduce. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(3, 3) + >>> torch.logsumexp(a, 1) + tensor([1.4907, 1.0593, 1.5696]) + >>> torch.dist(torch.logsumexp(a, 1), torch.log(torch.sum(torch.exp(a), 1))) + tensor(1.6859e-07) + """ + +@overload +def lstm( + data: Tensor, + batch_sizes: Tensor, + hx: tuple[Tensor, ...] | list[Tensor], + params: tuple[Tensor, ...] | list[Tensor], + has_biases: _bool, + num_layers: _int, + dropout: _float, + train: _bool, + bidirectional: _bool, +) -> tuple[Tensor, Tensor, Tensor]: ... +@overload +def lstm( + input: Tensor, + hx: tuple[Tensor, ...] | list[Tensor], + params: tuple[Tensor, ...] | list[Tensor], + has_biases: _bool, + num_layers: _int, + dropout: _float, + train: _bool, + bidirectional: _bool, + batch_first: _bool, +) -> tuple[Tensor, Tensor, Tensor]: ... +def lstm_cell( + input: Tensor, + hx: tuple[Tensor, ...] | list[Tensor], + w_ih: Tensor, + w_hh: Tensor, + b_ih: Tensor | None = None, + b_hh: Tensor | None = None, +) -> tuple[Tensor, Tensor]: ... +@overload +def lt( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + lt(input, other, *, out=None) -> Tensor + + Computes :math:`\text{input} < \text{other}` element-wise. + + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or float): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is less than :attr:`other` and False elsewhere + + Example:: + + >>> torch.lt(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[False, False], [True, False]]) + """ + +@overload +def lt( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + lt(input, other, *, out=None) -> Tensor + + Computes :math:`\text{input} < \text{other}` element-wise. + + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or float): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is less than :attr:`other` and False elsewhere + + Example:: + + >>> torch.lt(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[False, False], [True, False]]) + """ + +def lu_solve( + input: Tensor, + LU_data: Tensor, + LU_pivots: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + lu_solve(b, LU_data, LU_pivots, *, out=None) -> Tensor + + Returns the LU solve of the linear system :math:`Ax = b` using the partially pivoted + LU factorization of A from :func:`~linalg.lu_factor`. + + This function supports ``float``, ``double``, ``cfloat`` and ``cdouble`` dtypes for :attr:`input`. + + .. warning:: + + :func:`torch.lu_solve` is deprecated in favor of :func:`torch.linalg.lu_solve`. + :func:`torch.lu_solve` will be removed in a future PyTorch release. + ``X = torch.lu_solve(B, LU, pivots)`` should be replaced with + + .. code:: python + + X = linalg.lu_solve(LU, pivots, B) + + Arguments: + b (Tensor): the RHS tensor of size :math:`(*, m, k)`, where :math:`*` + is zero or more batch dimensions. + LU_data (Tensor): the pivoted LU factorization of A from :meth:`~linalg.lu_factor` of size :math:`(*, m, m)`, + where :math:`*` is zero or more batch dimensions. + LU_pivots (IntTensor): the pivots of the LU factorization from :meth:`~linalg.lu_factor` of size :math:`(*, m)`, + where :math:`*` is zero or more batch dimensions. + The batch dimensions of :attr:`LU_pivots` must be equal to the batch dimensions of + :attr:`LU_data`. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> A = torch.randn(2, 3, 3) + >>> b = torch.randn(2, 3, 1) + >>> LU, pivots = torch.linalg.lu_factor(A) + >>> x = torch.lu_solve(b, LU, pivots) + >>> torch.dist(A @ x, b) + tensor(1.00000e-07 * + 2.8312) + """ + +def lu_unpack( + LU_data: Tensor, + LU_pivots: Tensor, + unpack_data: _bool = True, + unpack_pivots: _bool = True, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.lu_unpack: + r""" + lu_unpack(LU_data, LU_pivots, unpack_data=True, unpack_pivots=True, *, out=None) -> (Tensor, Tensor, Tensor) + + Unpacks the LU decomposition returned by :func:`~linalg.lu_factor` into the `P, L, U` matrices. + + .. seealso:: + + :func:`~linalg.lu` returns the matrices from the LU decomposition. Its gradient formula is more efficient + than that of doing :func:`~linalg.lu_factor` followed by :func:`~linalg.lu_unpack`. + + Args: + LU_data (Tensor): the packed LU factorization data + LU_pivots (Tensor): the packed LU factorization pivots + unpack_data (bool): flag indicating if the data should be unpacked. + If ``False``, then the returned ``L`` and ``U`` are empty tensors. + Default: ``True`` + unpack_pivots (bool): flag indicating if the pivots should be unpacked into a permutation matrix ``P``. + If ``False``, then the returned ``P`` is an empty tensor. + Default: ``True`` + + Keyword args: + out (tuple, optional): output tuple of three tensors. Ignored if `None`. + + Returns: + A namedtuple ``(P, L, U)`` + + Examples:: + + >>> A = torch.randn(2, 3, 3) + >>> LU, pivots = torch.linalg.lu_factor(A) + >>> P, L, U = torch.lu_unpack(LU, pivots) + >>> # We can recover A from the factorization + >>> A_ = P @ L @ U + >>> torch.allclose(A, A_) + True + + >>> # LU factorization of a rectangular matrix: + >>> A = torch.randn(2, 3, 2) + >>> LU, pivots = torch.linalg.lu_factor(A) + >>> P, L, U = torch.lu_unpack(LU, pivots) + >>> # P, L, U are the same as returned by linalg.lu + >>> P_, L_, U_ = torch.linalg.lu(A) + >>> torch.allclose(P, P_) and torch.allclose(L, L_) and torch.allclose(U, U_) + True + """ + +def margin_ranking_loss( + input1: Tensor, + input2: Tensor, + target: Tensor, + margin: _float = 0.0, + reduction: _int = 1, +) -> Tensor: ... +@overload +def masked_fill(input: Tensor, mask: Tensor, value: Tensor) -> Tensor: ... +@overload +def masked_fill( + input: Tensor, + mask: Tensor, + value: Number | _complex, +) -> Tensor: ... +def masked_scatter(input: Tensor, mask: Tensor, source: Tensor) -> Tensor: ... +def masked_select( + input: Tensor, + mask: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + masked_select(input, mask, *, out=None) -> Tensor + + Returns a new 1-D tensor which indexes the :attr:`input` tensor according to + the boolean mask :attr:`mask` which is a `BoolTensor`. + + The shapes of the :attr:`mask` tensor and the :attr:`input` tensor don't need + to match, but they must be :ref:`broadcastable `. + + .. note:: The returned tensor does **not** use the same storage + as the original tensor + + Args: + input (Tensor): the input tensor. + mask (BoolTensor): the tensor containing the binary mask to index with + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> x = torch.randn(3, 4) + >>> x + tensor([[ 0.3552, -2.3825, -0.8297, 0.3477], + [-1.2035, 1.2252, 0.5002, 0.6248], + [ 0.1307, -2.0608, 0.1244, 2.0139]]) + >>> mask = x.ge(0.5) + >>> mask + tensor([[False, False, False, False], + [False, True, True, True], + [False, False, False, True]]) + >>> torch.masked_select(x, mask) + tensor([ 1.2252, 0.5002, 0.6248, 2.0139]) + """ + +def matmul( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + matmul(input, other, *, out=None) -> Tensor + + Matrix product of two tensors. + + The behavior depends on the dimensionality of the tensors as follows: + + - If both tensors are 1-dimensional, the dot product (scalar) is returned. + - If both arguments are 2-dimensional, the matrix-matrix product is returned. + - If the first argument is 1-dimensional and the second argument is 2-dimensional, + a 1 is prepended to its dimension for the purpose of the matrix multiply. + After the matrix multiply, the prepended dimension is removed. + - If the first argument is 2-dimensional and the second argument is 1-dimensional, + the matrix-vector product is returned. + - If both arguments are at least 1-dimensional and at least one argument is + N-dimensional (where N > 2), then a batched matrix multiply is returned. If the first + argument is 1-dimensional, a 1 is prepended to its dimension for the purpose of the + batched matrix multiply and removed after. If the second argument is 1-dimensional, a + 1 is appended to its dimension for the purpose of the batched matrix multiply and removed after. + + The first N-2 dimensions of each argument, the batch dimensions, are + :ref:`broadcast ` (and thus must be broadcastable). + The last 2, the matrix dimensions, are handled as in the matrix-matrix product. + + For example, if :attr:`input` is a + :math:`(j \times 1 \times n \times m)` tensor and :attr:`other` is a :math:`(k \times m \times p)` + tensor, the batch dimensions are :math:`(j \times 1)` and :math:`(k)`, + and the matrix dimensions are :math:`(n \times m)` and :math:`(m \times p)`. + :attr:`out` will be a :math:`(j \times k \times n \times p)` tensor. + + This operation has support for arguments with :ref:`sparse layouts`. In particular the + matrix-matrix (both arguments 2-dimensional) supports sparse arguments with the same restrictions + as :func:`torch.mm` + + + .. warning:: + Sparse support is a beta feature and some layout(s)/dtype/device combinations may not be supported, + or may not have autograd support. If you notice missing functionality please + open a feature request. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + .. note:: + + The 1-dimensional dot product version of this function does not support an :attr:`out` parameter. + + Arguments: + input (Tensor): the first tensor to be multiplied + other (Tensor): the second tensor to be multiplied + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> # vector x vector + >>> tensor1 = torch.randn(3) + >>> tensor2 = torch.randn(3) + >>> torch.matmul(tensor1, tensor2).size() + torch.Size([]) + >>> # matrix x vector + >>> tensor1 = torch.randn(3, 4) + >>> tensor2 = torch.randn(4) + >>> torch.matmul(tensor1, tensor2).size() + torch.Size([3]) + >>> # batched matrix x broadcasted vector + >>> tensor1 = torch.randn(10, 3, 4) + >>> tensor2 = torch.randn(4) + >>> torch.matmul(tensor1, tensor2).size() + torch.Size([10, 3]) + >>> # batched matrix x batched matrix + >>> tensor1 = torch.randn(10, 3, 4) + >>> tensor2 = torch.randn(10, 4, 5) + >>> torch.matmul(tensor1, tensor2).size() + torch.Size([10, 3, 5]) + >>> # batched matrix x broadcasted matrix + >>> tensor1 = torch.randn(10, 3, 4) + >>> tensor2 = torch.randn(4, 5) + >>> torch.matmul(tensor1, tensor2).size() + torch.Size([10, 3, 5]) + """ + +def matrix_exp(input: Tensor) -> Tensor: + r""" + matrix_exp(A) -> Tensor + + Alias for :func:`torch.linalg.matrix_exp`. + """ + +def matrix_power( + input: Tensor, + n: _int, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + matrix_power(input, n, *, out=None) -> Tensor + + Alias for :func:`torch.linalg.matrix_power` + """ + +@overload +def max(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + max(input, *, out=None) -> Tensor + + Returns the maximum value of all elements in the ``input`` tensor. + + .. note:: + The difference between ``max``/``min`` and ``amax``/``amin`` is: + - ``amax``/``amin`` supports reducing on multiple dimensions, + - ``amax``/``amin`` does not return indices. + + Both ``amax``/``amin`` evenly distribute gradients between equal values + when there are multiple input elements with the same minimum or maximum value. + + For ``max``/``min``: + - If reduce over all dimensions(no dim specified), gradients evenly distribute between equally ``max``/``min`` values. + - If reduce over one specified axis, only propagate to the indexed element. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.6763, 0.7445, -2.2369]]) + >>> torch.max(a) + tensor(0.7445) + + .. function:: max(input, dim, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` is the maximum + value of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`. And ``indices`` is the index location of each maximum value found + (argmax). + + If ``keepdim`` is ``True``, the output tensors are of the same size + as ``input`` except in the dimension ``dim`` where they are of size 1. + Otherwise, ``dim`` is squeezed (see :func:`torch.squeeze`), resulting + in the output tensors having 1 fewer dimension than ``input``. + + .. note:: If there are multiple maximal values in a reduced row then + the indices of the first maximal value are returned. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. If omitted, all dimensions are reduced. Explicit ``None`` is not supported. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the result tuple of two output tensors (max, max_indices) + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-1.2360, -0.2942, -0.1222, 0.8475], + [ 1.1949, -1.1127, -2.2379, -0.6702], + [ 1.5717, -0.9207, 0.1297, -1.8768], + [-0.6172, 1.0036, -0.6060, -0.2432]]) + >>> torch.max(a, 1) + torch.return_types.max(values=tensor([0.8475, 1.1949, 1.5717, 1.0036]), indices=tensor([3, 0, 0, 1])) + >>> a = torch.tensor([[1.0, 2.0], [3.0, 4.0]]) + >>> a.max(dim=1, keepdim=True) + torch.return_types.max( + values=tensor([[2.], [4.]]), + indices=tensor([[1], [1]])) + >>> a.max(dim=1, keepdim=False) + torch.return_types.max( + values=tensor([2., 4.]), + indices=tensor([1, 1])) + + .. function:: max(input, other, *, out=None) -> Tensor + :noindex: + + See :func:`torch.maximum`. + """ + +@overload +def max( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + max(input, *, out=None) -> Tensor + + Returns the maximum value of all elements in the ``input`` tensor. + + .. note:: + The difference between ``max``/``min`` and ``amax``/``amin`` is: + - ``amax``/``amin`` supports reducing on multiple dimensions, + - ``amax``/``amin`` does not return indices. + + Both ``amax``/``amin`` evenly distribute gradients between equal values + when there are multiple input elements with the same minimum or maximum value. + + For ``max``/``min``: + - If reduce over all dimensions(no dim specified), gradients evenly distribute between equally ``max``/``min`` values. + - If reduce over one specified axis, only propagate to the indexed element. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.6763, 0.7445, -2.2369]]) + >>> torch.max(a) + tensor(0.7445) + + .. function:: max(input, dim, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` is the maximum + value of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`. And ``indices`` is the index location of each maximum value found + (argmax). + + If ``keepdim`` is ``True``, the output tensors are of the same size + as ``input`` except in the dimension ``dim`` where they are of size 1. + Otherwise, ``dim`` is squeezed (see :func:`torch.squeeze`), resulting + in the output tensors having 1 fewer dimension than ``input``. + + .. note:: If there are multiple maximal values in a reduced row then + the indices of the first maximal value are returned. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. If omitted, all dimensions are reduced. Explicit ``None`` is not supported. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the result tuple of two output tensors (max, max_indices) + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-1.2360, -0.2942, -0.1222, 0.8475], + [ 1.1949, -1.1127, -2.2379, -0.6702], + [ 1.5717, -0.9207, 0.1297, -1.8768], + [-0.6172, 1.0036, -0.6060, -0.2432]]) + >>> torch.max(a, 1) + torch.return_types.max(values=tensor([0.8475, 1.1949, 1.5717, 1.0036]), indices=tensor([3, 0, 0, 1])) + >>> a = torch.tensor([[1.0, 2.0], [3.0, 4.0]]) + >>> a.max(dim=1, keepdim=True) + torch.return_types.max( + values=tensor([[2.], [4.]]), + indices=tensor([[1], [1]])) + >>> a.max(dim=1, keepdim=False) + torch.return_types.max( + values=tensor([2., 4.]), + indices=tensor([1, 1])) + + .. function:: max(input, other, *, out=None) -> Tensor + :noindex: + + See :func:`torch.maximum`. + """ + +@overload +def max( + input: Tensor, + dim: _int, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.max: + r""" + max(input, *, out=None) -> Tensor + + Returns the maximum value of all elements in the ``input`` tensor. + + .. note:: + The difference between ``max``/``min`` and ``amax``/``amin`` is: + - ``amax``/``amin`` supports reducing on multiple dimensions, + - ``amax``/``amin`` does not return indices. + + Both ``amax``/``amin`` evenly distribute gradients between equal values + when there are multiple input elements with the same minimum or maximum value. + + For ``max``/``min``: + - If reduce over all dimensions(no dim specified), gradients evenly distribute between equally ``max``/``min`` values. + - If reduce over one specified axis, only propagate to the indexed element. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.6763, 0.7445, -2.2369]]) + >>> torch.max(a) + tensor(0.7445) + + .. function:: max(input, dim, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` is the maximum + value of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`. And ``indices`` is the index location of each maximum value found + (argmax). + + If ``keepdim`` is ``True``, the output tensors are of the same size + as ``input`` except in the dimension ``dim`` where they are of size 1. + Otherwise, ``dim`` is squeezed (see :func:`torch.squeeze`), resulting + in the output tensors having 1 fewer dimension than ``input``. + + .. note:: If there are multiple maximal values in a reduced row then + the indices of the first maximal value are returned. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. If omitted, all dimensions are reduced. Explicit ``None`` is not supported. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the result tuple of two output tensors (max, max_indices) + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-1.2360, -0.2942, -0.1222, 0.8475], + [ 1.1949, -1.1127, -2.2379, -0.6702], + [ 1.5717, -0.9207, 0.1297, -1.8768], + [-0.6172, 1.0036, -0.6060, -0.2432]]) + >>> torch.max(a, 1) + torch.return_types.max(values=tensor([0.8475, 1.1949, 1.5717, 1.0036]), indices=tensor([3, 0, 0, 1])) + >>> a = torch.tensor([[1.0, 2.0], [3.0, 4.0]]) + >>> a.max(dim=1, keepdim=True) + torch.return_types.max( + values=tensor([[2.], [4.]]), + indices=tensor([[1], [1]])) + >>> a.max(dim=1, keepdim=False) + torch.return_types.max( + values=tensor([2., 4.]), + indices=tensor([1, 1])) + + .. function:: max(input, other, *, out=None) -> Tensor + :noindex: + + See :func:`torch.maximum`. + """ + +@overload +def max( + input: Tensor, + dim: str | EllipsisType | None, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.max: + r""" + max(input, *, out=None) -> Tensor + + Returns the maximum value of all elements in the ``input`` tensor. + + .. note:: + The difference between ``max``/``min`` and ``amax``/``amin`` is: + - ``amax``/``amin`` supports reducing on multiple dimensions, + - ``amax``/``amin`` does not return indices. + + Both ``amax``/``amin`` evenly distribute gradients between equal values + when there are multiple input elements with the same minimum or maximum value. + + For ``max``/``min``: + - If reduce over all dimensions(no dim specified), gradients evenly distribute between equally ``max``/``min`` values. + - If reduce over one specified axis, only propagate to the indexed element. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.6763, 0.7445, -2.2369]]) + >>> torch.max(a) + tensor(0.7445) + + .. function:: max(input, dim, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` is the maximum + value of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`. And ``indices`` is the index location of each maximum value found + (argmax). + + If ``keepdim`` is ``True``, the output tensors are of the same size + as ``input`` except in the dimension ``dim`` where they are of size 1. + Otherwise, ``dim`` is squeezed (see :func:`torch.squeeze`), resulting + in the output tensors having 1 fewer dimension than ``input``. + + .. note:: If there are multiple maximal values in a reduced row then + the indices of the first maximal value are returned. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. If omitted, all dimensions are reduced. Explicit ``None`` is not supported. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the result tuple of two output tensors (max, max_indices) + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-1.2360, -0.2942, -0.1222, 0.8475], + [ 1.1949, -1.1127, -2.2379, -0.6702], + [ 1.5717, -0.9207, 0.1297, -1.8768], + [-0.6172, 1.0036, -0.6060, -0.2432]]) + >>> torch.max(a, 1) + torch.return_types.max(values=tensor([0.8475, 1.1949, 1.5717, 1.0036]), indices=tensor([3, 0, 0, 1])) + >>> a = torch.tensor([[1.0, 2.0], [3.0, 4.0]]) + >>> a.max(dim=1, keepdim=True) + torch.return_types.max( + values=tensor([[2.], [4.]]), + indices=tensor([[1], [1]])) + >>> a.max(dim=1, keepdim=False) + torch.return_types.max( + values=tensor([2., 4.]), + indices=tensor([1, 1])) + + .. function:: max(input, other, *, out=None) -> Tensor + :noindex: + + See :func:`torch.maximum`. + """ + +def max_pool1d( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size = (), + padding: _int | _size = 0, + dilation: _int | _size = 1, + ceil_mode: _bool = False, +) -> Tensor: ... +def max_pool1d_with_indices( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size = (), + padding: _int | _size = 0, + dilation: _int | _size = 1, + ceil_mode: _bool = False, +) -> tuple[Tensor, Tensor]: ... +def max_pool2d( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size = (), + padding: _int | _size = 0, + dilation: _int | _size = 1, + ceil_mode: _bool = False, +) -> Tensor: ... +def max_pool3d( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size = (), + padding: _int | _size = 0, + dilation: _int | _size = 1, + ceil_mode: _bool = False, +) -> Tensor: ... +def maximum( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + maximum(input, other, *, out=None) -> Tensor + + Computes the element-wise maximum of :attr:`input` and :attr:`other`. + + .. note:: + If one of the elements being compared is a NaN, then that element is returned. + :func:`maximum` is not supported for tensors with complex dtypes. + + Args: + input (Tensor): the input tensor. + other (Tensor): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor((1, 2, -1)) + >>> b = torch.tensor((3, 0, 4)) + >>> torch.maximum(a, b) + tensor([3, 2, 4]) + """ + +@overload +def mean( + input: Tensor, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + mean(input, *, dtype=None) -> Tensor + + .. note:: + If the `input` tensor is empty, ``torch.mean()`` returns ``nan``. + This behavior is consistent with NumPy and follows the definition + that the mean over an empty set is undefined. + + + Returns the mean value of all elements in the :attr:`input` tensor. Input must be floating point or complex. + + Args: + input (Tensor): + the input tensor, either of floating point or complex dtype + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.2294, -0.5481, 1.3288]]) + >>> torch.mean(a) + tensor(0.3367) + + .. function:: mean(input, dim, keepdim=False, *, dtype=None, out=None) -> Tensor + :noindex: + + Returns the mean value of each row of the :attr:`input` tensor in the given + dimension :attr:`dim`. If :attr:`dim` is a list of dimensions, + reduce over all of them. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + out (Tensor, optional): the output tensor. + + .. seealso:: + + :func:`torch.nanmean` computes the mean value of `non-NaN` elements. + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-0.3841, 0.6320, 0.4254, -0.7384], + [-0.9644, 1.0131, -0.6549, -1.4279], + [-0.2951, -1.3350, -0.7694, 0.5600], + [ 1.0842, -0.9580, 0.3623, 0.2343]]) + >>> torch.mean(a, 1) + tensor([-0.0163, -0.5085, -0.4599, 0.1807]) + >>> torch.mean(a, 1, True) + tensor([[-0.0163], + [-0.5085], + [-0.4599], + [ 0.1807]]) + """ + +@overload +def mean( + input: Tensor, + dim: _int | _size | None, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + mean(input, *, dtype=None) -> Tensor + + .. note:: + If the `input` tensor is empty, ``torch.mean()`` returns ``nan``. + This behavior is consistent with NumPy and follows the definition + that the mean over an empty set is undefined. + + + Returns the mean value of all elements in the :attr:`input` tensor. Input must be floating point or complex. + + Args: + input (Tensor): + the input tensor, either of floating point or complex dtype + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.2294, -0.5481, 1.3288]]) + >>> torch.mean(a) + tensor(0.3367) + + .. function:: mean(input, dim, keepdim=False, *, dtype=None, out=None) -> Tensor + :noindex: + + Returns the mean value of each row of the :attr:`input` tensor in the given + dimension :attr:`dim`. If :attr:`dim` is a list of dimensions, + reduce over all of them. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + out (Tensor, optional): the output tensor. + + .. seealso:: + + :func:`torch.nanmean` computes the mean value of `non-NaN` elements. + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-0.3841, 0.6320, 0.4254, -0.7384], + [-0.9644, 1.0131, -0.6549, -1.4279], + [-0.2951, -1.3350, -0.7694, 0.5600], + [ 1.0842, -0.9580, 0.3623, 0.2343]]) + >>> torch.mean(a, 1) + tensor([-0.0163, -0.5085, -0.4599, 0.1807]) + >>> torch.mean(a, 1, True) + tensor([[-0.0163], + [-0.5085], + [-0.4599], + [ 0.1807]]) + """ + +@overload +def mean( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + mean(input, *, dtype=None) -> Tensor + + .. note:: + If the `input` tensor is empty, ``torch.mean()`` returns ``nan``. + This behavior is consistent with NumPy and follows the definition + that the mean over an empty set is undefined. + + + Returns the mean value of all elements in the :attr:`input` tensor. Input must be floating point or complex. + + Args: + input (Tensor): + the input tensor, either of floating point or complex dtype + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.2294, -0.5481, 1.3288]]) + >>> torch.mean(a) + tensor(0.3367) + + .. function:: mean(input, dim, keepdim=False, *, dtype=None, out=None) -> Tensor + :noindex: + + Returns the mean value of each row of the :attr:`input` tensor in the given + dimension :attr:`dim`. If :attr:`dim` is a list of dimensions, + reduce over all of them. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + out (Tensor, optional): the output tensor. + + .. seealso:: + + :func:`torch.nanmean` computes the mean value of `non-NaN` elements. + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-0.3841, 0.6320, 0.4254, -0.7384], + [-0.9644, 1.0131, -0.6549, -1.4279], + [-0.2951, -1.3350, -0.7694, 0.5600], + [ 1.0842, -0.9580, 0.3623, 0.2343]]) + >>> torch.mean(a, 1) + tensor([-0.0163, -0.5085, -0.4599, 0.1807]) + >>> torch.mean(a, 1, True) + tensor([[-0.0163], + [-0.5085], + [-0.4599], + [ 0.1807]]) + """ + +@overload +def median(input: Tensor) -> Tensor: + r""" + median(input) -> Tensor + + Returns the median of the values in :attr:`input`. + + .. note:: + The median is not unique for :attr:`input` tensors with an even number + of elements. In this case the lower of the two medians is returned. To + compute the mean of both medians, use :func:`torch.quantile` with ``q=0.5`` instead. + + .. warning:: + This function produces deterministic (sub)gradients unlike ``median(dim=0)`` + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 1.5219, -1.5212, 0.2202]]) + >>> torch.median(a) + tensor(0.2202) + + .. function:: median(input, dim=-1, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` contains the median of each row of :attr:`input` + in the dimension :attr:`dim`, and ``indices`` contains the index of the median values found in the dimension :attr:`dim`. + + By default, :attr:`dim` is the last dimension of the :attr:`input` tensor. + + If :attr:`keepdim` is ``True``, the output tensors are of the same size + as :attr:`input` except in the dimension :attr:`dim` where they are of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in + the outputs tensor having 1 fewer dimension than :attr:`input`. + + .. note:: + The median is not unique for :attr:`input` tensors with an even number + of elements in the dimension :attr:`dim`. In this case the lower of the + two medians is returned. To compute the mean of both medians in + :attr:`input`, use :func:`torch.quantile` with ``q=0.5`` instead. + + .. warning:: + ``indices`` does not necessarily contain the first occurrence of each + median value found, unless it is unique. + The exact implementation details are device-specific. + Do not expect the same result when run on CPU and GPU in general. + For the same reason do not expect the gradients to be deterministic. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out ((Tensor, Tensor), optional): The first tensor will be populated with the median values and the second + tensor, which must have dtype long, with their indices in the dimension + :attr:`dim` of :attr:`input`. + + Example:: + + >>> a = torch.randn(4, 5) + >>> a + tensor([[ 0.2505, -0.3982, -0.9948, 0.3518, -1.3131], + [ 0.3180, -0.6993, 1.0436, 0.0438, 0.2270], + [-0.2751, 0.7303, 0.2192, 0.3321, 0.2488], + [ 1.0778, -1.9510, 0.7048, 0.4742, -0.7125]]) + >>> torch.median(a, 1) + torch.return_types.median(values=tensor([-0.3982, 0.2270, 0.2488, 0.4742]), indices=tensor([1, 4, 4, 3])) + """ + +@overload +def median( + input: Tensor, + dim: _int, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.median: + r""" + median(input) -> Tensor + + Returns the median of the values in :attr:`input`. + + .. note:: + The median is not unique for :attr:`input` tensors with an even number + of elements. In this case the lower of the two medians is returned. To + compute the mean of both medians, use :func:`torch.quantile` with ``q=0.5`` instead. + + .. warning:: + This function produces deterministic (sub)gradients unlike ``median(dim=0)`` + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 1.5219, -1.5212, 0.2202]]) + >>> torch.median(a) + tensor(0.2202) + + .. function:: median(input, dim=-1, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` contains the median of each row of :attr:`input` + in the dimension :attr:`dim`, and ``indices`` contains the index of the median values found in the dimension :attr:`dim`. + + By default, :attr:`dim` is the last dimension of the :attr:`input` tensor. + + If :attr:`keepdim` is ``True``, the output tensors are of the same size + as :attr:`input` except in the dimension :attr:`dim` where they are of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in + the outputs tensor having 1 fewer dimension than :attr:`input`. + + .. note:: + The median is not unique for :attr:`input` tensors with an even number + of elements in the dimension :attr:`dim`. In this case the lower of the + two medians is returned. To compute the mean of both medians in + :attr:`input`, use :func:`torch.quantile` with ``q=0.5`` instead. + + .. warning:: + ``indices`` does not necessarily contain the first occurrence of each + median value found, unless it is unique. + The exact implementation details are device-specific. + Do not expect the same result when run on CPU and GPU in general. + For the same reason do not expect the gradients to be deterministic. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out ((Tensor, Tensor), optional): The first tensor will be populated with the median values and the second + tensor, which must have dtype long, with their indices in the dimension + :attr:`dim` of :attr:`input`. + + Example:: + + >>> a = torch.randn(4, 5) + >>> a + tensor([[ 0.2505, -0.3982, -0.9948, 0.3518, -1.3131], + [ 0.3180, -0.6993, 1.0436, 0.0438, 0.2270], + [-0.2751, 0.7303, 0.2192, 0.3321, 0.2488], + [ 1.0778, -1.9510, 0.7048, 0.4742, -0.7125]]) + >>> torch.median(a, 1) + torch.return_types.median(values=tensor([-0.3982, 0.2270, 0.2488, 0.4742]), indices=tensor([1, 4, 4, 3])) + """ + +@overload +def median( + input: Tensor, + dim: str | EllipsisType | None, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.median: + r""" + median(input) -> Tensor + + Returns the median of the values in :attr:`input`. + + .. note:: + The median is not unique for :attr:`input` tensors with an even number + of elements. In this case the lower of the two medians is returned. To + compute the mean of both medians, use :func:`torch.quantile` with ``q=0.5`` instead. + + .. warning:: + This function produces deterministic (sub)gradients unlike ``median(dim=0)`` + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 1.5219, -1.5212, 0.2202]]) + >>> torch.median(a) + tensor(0.2202) + + .. function:: median(input, dim=-1, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` contains the median of each row of :attr:`input` + in the dimension :attr:`dim`, and ``indices`` contains the index of the median values found in the dimension :attr:`dim`. + + By default, :attr:`dim` is the last dimension of the :attr:`input` tensor. + + If :attr:`keepdim` is ``True``, the output tensors are of the same size + as :attr:`input` except in the dimension :attr:`dim` where they are of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in + the outputs tensor having 1 fewer dimension than :attr:`input`. + + .. note:: + The median is not unique for :attr:`input` tensors with an even number + of elements in the dimension :attr:`dim`. In this case the lower of the + two medians is returned. To compute the mean of both medians in + :attr:`input`, use :func:`torch.quantile` with ``q=0.5`` instead. + + .. warning:: + ``indices`` does not necessarily contain the first occurrence of each + median value found, unless it is unique. + The exact implementation details are device-specific. + Do not expect the same result when run on CPU and GPU in general. + For the same reason do not expect the gradients to be deterministic. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out ((Tensor, Tensor), optional): The first tensor will be populated with the median values and the second + tensor, which must have dtype long, with their indices in the dimension + :attr:`dim` of :attr:`input`. + + Example:: + + >>> a = torch.randn(4, 5) + >>> a + tensor([[ 0.2505, -0.3982, -0.9948, 0.3518, -1.3131], + [ 0.3180, -0.6993, 1.0436, 0.0438, 0.2270], + [-0.2751, 0.7303, 0.2192, 0.3321, 0.2488], + [ 1.0778, -1.9510, 0.7048, 0.4742, -0.7125]]) + >>> torch.median(a, 1) + torch.return_types.median(values=tensor([-0.3982, 0.2270, 0.2488, 0.4742]), indices=tensor([1, 4, 4, 3])) + """ + +@overload +def min(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + min(input, *, out=None) -> Tensor + + Returns the minimum value of all elements in the :attr:`input` tensor. + + .. note:: + The difference between ``max``/``min`` and ``amax``/``amin`` is: + - ``amax``/``amin`` supports reducing on multiple dimensions, + - ``amax``/``amin`` does not return indices. + + Both ``amax``/``amin`` evenly distribute gradients between equal values + when there are multiple input elements with the same minimum or maximum value. + + For ``max``/``min``: + - If reduce over all dimensions(no dim specified), gradients evenly distribute between equally ``max``/``min`` values. + - If reduce over one specified axis, only propagate to the indexed element. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.6750, 1.0857, 1.7197]]) + >>> torch.min(a) + tensor(0.6750) + + .. function:: min(input, dim, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` is the minimum + value of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`. And ``indices`` is the index location of each minimum value found + (argmin). + + If :attr:`keepdim` is ``True``, the output tensors are of the same size as + :attr:`input` except in the dimension :attr:`dim` where they are of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in + the output tensors having 1 fewer dimension than :attr:`input`. + + .. note:: If there are multiple minimal values in a reduced row then + the indices of the first minimal value are returned. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. If omitted, all dimensions are reduced. Explicit ``None`` is not supported. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the tuple of two output tensors (min, min_indices) + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-0.6248, 1.1334, -1.1899, -0.2803], + [-1.4644, -0.2635, -0.3651, 0.6134], + [ 0.2457, 0.0384, 1.0128, 0.7015], + [-0.1153, 2.9849, 2.1458, 0.5788]]) + >>> torch.min(a, 1) + torch.return_types.min(values=tensor([-1.1899, -1.4644, 0.0384, -0.1153]), indices=tensor([2, 0, 1, 0])) + + .. function:: min(input, other, *, out=None) -> Tensor + :noindex: + + See :func:`torch.minimum`. + """ + +@overload +def min( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + min(input, *, out=None) -> Tensor + + Returns the minimum value of all elements in the :attr:`input` tensor. + + .. note:: + The difference between ``max``/``min`` and ``amax``/``amin`` is: + - ``amax``/``amin`` supports reducing on multiple dimensions, + - ``amax``/``amin`` does not return indices. + + Both ``amax``/``amin`` evenly distribute gradients between equal values + when there are multiple input elements with the same minimum or maximum value. + + For ``max``/``min``: + - If reduce over all dimensions(no dim specified), gradients evenly distribute between equally ``max``/``min`` values. + - If reduce over one specified axis, only propagate to the indexed element. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.6750, 1.0857, 1.7197]]) + >>> torch.min(a) + tensor(0.6750) + + .. function:: min(input, dim, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` is the minimum + value of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`. And ``indices`` is the index location of each minimum value found + (argmin). + + If :attr:`keepdim` is ``True``, the output tensors are of the same size as + :attr:`input` except in the dimension :attr:`dim` where they are of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in + the output tensors having 1 fewer dimension than :attr:`input`. + + .. note:: If there are multiple minimal values in a reduced row then + the indices of the first minimal value are returned. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. If omitted, all dimensions are reduced. Explicit ``None`` is not supported. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the tuple of two output tensors (min, min_indices) + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-0.6248, 1.1334, -1.1899, -0.2803], + [-1.4644, -0.2635, -0.3651, 0.6134], + [ 0.2457, 0.0384, 1.0128, 0.7015], + [-0.1153, 2.9849, 2.1458, 0.5788]]) + >>> torch.min(a, 1) + torch.return_types.min(values=tensor([-1.1899, -1.4644, 0.0384, -0.1153]), indices=tensor([2, 0, 1, 0])) + + .. function:: min(input, other, *, out=None) -> Tensor + :noindex: + + See :func:`torch.minimum`. + """ + +@overload +def min( + input: Tensor, + dim: _int, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.min: + r""" + min(input, *, out=None) -> Tensor + + Returns the minimum value of all elements in the :attr:`input` tensor. + + .. note:: + The difference between ``max``/``min`` and ``amax``/``amin`` is: + - ``amax``/``amin`` supports reducing on multiple dimensions, + - ``amax``/``amin`` does not return indices. + + Both ``amax``/``amin`` evenly distribute gradients between equal values + when there are multiple input elements with the same minimum or maximum value. + + For ``max``/``min``: + - If reduce over all dimensions(no dim specified), gradients evenly distribute between equally ``max``/``min`` values. + - If reduce over one specified axis, only propagate to the indexed element. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.6750, 1.0857, 1.7197]]) + >>> torch.min(a) + tensor(0.6750) + + .. function:: min(input, dim, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` is the minimum + value of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`. And ``indices`` is the index location of each minimum value found + (argmin). + + If :attr:`keepdim` is ``True``, the output tensors are of the same size as + :attr:`input` except in the dimension :attr:`dim` where they are of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in + the output tensors having 1 fewer dimension than :attr:`input`. + + .. note:: If there are multiple minimal values in a reduced row then + the indices of the first minimal value are returned. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. If omitted, all dimensions are reduced. Explicit ``None`` is not supported. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the tuple of two output tensors (min, min_indices) + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-0.6248, 1.1334, -1.1899, -0.2803], + [-1.4644, -0.2635, -0.3651, 0.6134], + [ 0.2457, 0.0384, 1.0128, 0.7015], + [-0.1153, 2.9849, 2.1458, 0.5788]]) + >>> torch.min(a, 1) + torch.return_types.min(values=tensor([-1.1899, -1.4644, 0.0384, -0.1153]), indices=tensor([2, 0, 1, 0])) + + .. function:: min(input, other, *, out=None) -> Tensor + :noindex: + + See :func:`torch.minimum`. + """ + +@overload +def min( + input: Tensor, + dim: str | EllipsisType | None, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.min: + r""" + min(input, *, out=None) -> Tensor + + Returns the minimum value of all elements in the :attr:`input` tensor. + + .. note:: + The difference between ``max``/``min`` and ``amax``/``amin`` is: + - ``amax``/``amin`` supports reducing on multiple dimensions, + - ``amax``/``amin`` does not return indices. + + Both ``amax``/``amin`` evenly distribute gradients between equal values + when there are multiple input elements with the same minimum or maximum value. + + For ``max``/``min``: + - If reduce over all dimensions(no dim specified), gradients evenly distribute between equally ``max``/``min`` values. + - If reduce over one specified axis, only propagate to the indexed element. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.6750, 1.0857, 1.7197]]) + >>> torch.min(a) + tensor(0.6750) + + .. function:: min(input, dim, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` is the minimum + value of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`. And ``indices`` is the index location of each minimum value found + (argmin). + + If :attr:`keepdim` is ``True``, the output tensors are of the same size as + :attr:`input` except in the dimension :attr:`dim` where they are of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in + the output tensors having 1 fewer dimension than :attr:`input`. + + .. note:: If there are multiple minimal values in a reduced row then + the indices of the first minimal value are returned. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. If omitted, all dimensions are reduced. Explicit ``None`` is not supported. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the tuple of two output tensors (min, min_indices) + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-0.6248, 1.1334, -1.1899, -0.2803], + [-1.4644, -0.2635, -0.3651, 0.6134], + [ 0.2457, 0.0384, 1.0128, 0.7015], + [-0.1153, 2.9849, 2.1458, 0.5788]]) + >>> torch.min(a, 1) + torch.return_types.min(values=tensor([-1.1899, -1.4644, 0.0384, -0.1153]), indices=tensor([2, 0, 1, 0])) + + .. function:: min(input, other, *, out=None) -> Tensor + :noindex: + + See :func:`torch.minimum`. + """ + +def minimum( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + minimum(input, other, *, out=None) -> Tensor + + Computes the element-wise minimum of :attr:`input` and :attr:`other`. + + .. note:: + If one of the elements being compared is a NaN, then that element is returned. + :func:`minimum` is not supported for tensors with complex dtypes. + + Args: + input (Tensor): the input tensor. + other (Tensor): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor((1, 2, -1)) + >>> b = torch.tensor((3, 0, 4)) + >>> torch.minimum(a, b) + tensor([1, 0, -1]) + """ + +def miopen_batch_norm( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + running_mean: Tensor | None, + running_var: Tensor | None, + training: _bool, + exponential_average_factor: _float, + epsilon: _float, +) -> tuple[Tensor, Tensor, Tensor]: ... +def miopen_convolution( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + padding: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, + benchmark: _bool, + deterministic: _bool, +) -> Tensor: ... +def miopen_convolution_add_relu( + input: Tensor, + weight: Tensor, + z: Tensor, + alpha: Number | _complex | None, + bias: Tensor | None, + stride: Sequence[_int | SymInt], + padding: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, +) -> Tensor: ... +def miopen_convolution_relu( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + stride: Sequence[_int | SymInt], + padding: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, +) -> Tensor: ... +def miopen_convolution_transpose( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + padding: Sequence[_int | SymInt], + output_padding: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, + benchmark: _bool, + deterministic: _bool, +) -> Tensor: ... +@overload +def miopen_ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: _size, + target_lengths: _size, + blank: _int, + deterministic: _bool, + zero_infinity: _bool, +) -> tuple[Tensor, Tensor]: ... +@overload +def miopen_ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: Tensor, + target_lengths: Tensor, + blank: _int, + deterministic: _bool, + zero_infinity: _bool, +) -> tuple[Tensor, Tensor]: ... +def miopen_depthwise_convolution( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + padding: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, + benchmark: _bool, + deterministic: _bool, +) -> Tensor: ... +def miopen_rnn( + input: Tensor, + weight: tuple[Tensor, ...] | list[Tensor], + weight_stride0: _int, + hx: Tensor, + cx: Tensor | None, + mode: _int, + hidden_size: _int, + num_layers: _int, + batch_first: _bool, + dropout: _float, + train: _bool, + bidirectional: _bool, + batch_sizes: _size, + dropout_state: Tensor | None, +) -> tuple[Tensor, Tensor, Tensor, Tensor, Tensor]: ... +def mkldnn_adaptive_avg_pool2d( + input: Tensor, + output_size: _int | _size, + *, + out: Tensor | None = None, +) -> Tensor: ... +def mkldnn_convolution( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + padding: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, +) -> Tensor: ... +def mkldnn_linear_backward_weights( + grad_output: Tensor, + input: Tensor, + weight: Tensor, + bias_defined: _bool, +) -> tuple[Tensor, Tensor]: ... +def mkldnn_max_pool2d( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size = (), + padding: _int | _size = 0, + dilation: _int | _size = 1, + ceil_mode: _bool = False, +) -> Tensor: ... +def mkldnn_max_pool3d( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size = (), + padding: _int | _size = 0, + dilation: _int | _size = 1, + ceil_mode: _bool = False, +) -> Tensor: ... +def mkldnn_rnn_layer( + input: Tensor, + weight0: Tensor, + weight1: Tensor, + weight2: Tensor, + weight3: Tensor, + hx_: Tensor, + cx_: Tensor, + reverse: _bool, + batch_sizes: _size, + mode: _int, + hidden_size: _int, + num_layers: _int, + has_biases: _bool, + bidirectional: _bool, + batch_first: _bool, + train: _bool, +) -> tuple[Tensor, Tensor, Tensor, Tensor]: ... +@overload +def mm(input: Tensor, mat2: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + mm(input, mat2, out_dtype=None, *, out=None) -> Tensor + + Performs a matrix multiplication of the matrices :attr:`input` and :attr:`mat2`. + + If :attr:`input` is a :math:`(n \times m)` tensor, :attr:`mat2` is a + :math:`(m \times p)` tensor, :attr:`out` will be a :math:`(n \times p)` tensor. + + .. note:: This function does not :ref:`broadcast `. + For broadcasting matrix products, see :func:`torch.matmul`. + + Supports strided and sparse 2-D tensors as inputs, autograd with + respect to strided inputs. + + This operation has support for arguments with :ref:`sparse layouts`. + If :attr:`out` is provided its layout will be used. Otherwise, the result + layout will be deduced from that of :attr:`input`. + + + .. warning:: + Sparse support is a beta feature and some layout(s)/dtype/device combinations may not be supported, + or may not have autograd support. If you notice missing functionality please + open a feature request. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): the first matrix to be matrix multiplied + mat2 (Tensor): the second matrix to be matrix multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> mat1 = torch.randn(2, 3) + >>> mat2 = torch.randn(3, 3) + >>> torch.mm(mat1, mat2) + tensor([[ 0.4851, 0.5037, -0.3633], + [-0.0760, -3.6705, 2.4784]]) + """ + +@overload +def mm( + input: Tensor, + mat2: Tensor, + out_dtype: _dtype, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + mm(input, mat2, out_dtype=None, *, out=None) -> Tensor + + Performs a matrix multiplication of the matrices :attr:`input` and :attr:`mat2`. + + If :attr:`input` is a :math:`(n \times m)` tensor, :attr:`mat2` is a + :math:`(m \times p)` tensor, :attr:`out` will be a :math:`(n \times p)` tensor. + + .. note:: This function does not :ref:`broadcast `. + For broadcasting matrix products, see :func:`torch.matmul`. + + Supports strided and sparse 2-D tensors as inputs, autograd with + respect to strided inputs. + + This operation has support for arguments with :ref:`sparse layouts`. + If :attr:`out` is provided its layout will be used. Otherwise, the result + layout will be deduced from that of :attr:`input`. + + + .. warning:: + Sparse support is a beta feature and some layout(s)/dtype/device combinations may not be supported, + or may not have autograd support. If you notice missing functionality please + open a feature request. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): the first matrix to be matrix multiplied + mat2 (Tensor): the second matrix to be matrix multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> mat1 = torch.randn(2, 3) + >>> mat2 = torch.randn(3, 3) + >>> torch.mm(mat1, mat2) + tensor([[ 0.4851, 0.5037, -0.3633], + [-0.0760, -3.6705, 2.4784]]) + """ + +@overload +def mode( + input: Tensor, + dim: _int = -1, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.mode: + r""" + mode(input, dim=-1, keepdim=False, *, out=None) -> (Tensor, LongTensor) + + Returns a namedtuple ``(values, indices)`` where ``values`` is the mode + value of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`, i.e. a value which appears most often + in that row, and ``indices`` is the index location of each mode value found. + + By default, :attr:`dim` is the last dimension of the :attr:`input` tensor. + + If :attr:`keepdim` is ``True``, the output tensors are of the same size as + :attr:`input` except in the dimension :attr:`dim` where they are of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting + in the output tensors having 1 fewer dimension than :attr:`input`. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the result tuple of two output tensors (values, indices) + + Example:: + + >>> b = torch.tensor([[0, 0, 0, 2, 0, 0, 2], + ... [0, 3, 0, 0, 2, 0, 1], + ... [2, 2, 2, 0, 0, 0, 3], + ... [2, 2, 3, 0, 1, 1, 0], + ... [1, 1, 0, 0, 2, 0, 2]]) + >>> torch.mode(b, 0) + torch.return_types.mode( + values=tensor([0, 2, 0, 0, 0, 0, 2]), + indices=tensor([1, 3, 4, 4, 2, 4, 4])) + """ + +@overload +def mode( + input: Tensor, + dim: str | EllipsisType | None, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.mode: + r""" + mode(input, dim=-1, keepdim=False, *, out=None) -> (Tensor, LongTensor) + + Returns a namedtuple ``(values, indices)`` where ``values`` is the mode + value of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`, i.e. a value which appears most often + in that row, and ``indices`` is the index location of each mode value found. + + By default, :attr:`dim` is the last dimension of the :attr:`input` tensor. + + If :attr:`keepdim` is ``True``, the output tensors are of the same size as + :attr:`input` except in the dimension :attr:`dim` where they are of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting + in the output tensors having 1 fewer dimension than :attr:`input`. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the result tuple of two output tensors (values, indices) + + Example:: + + >>> b = torch.tensor([[0, 0, 0, 2, 0, 0, 2], + ... [0, 3, 0, 0, 2, 0, 1], + ... [2, 2, 2, 0, 0, 0, 3], + ... [2, 2, 3, 0, 1, 1, 0], + ... [1, 1, 0, 0, 2, 0, 2]]) + >>> torch.mode(b, 0) + torch.return_types.mode( + values=tensor([0, 2, 0, 0, 0, 0, 2]), + indices=tensor([1, 3, 4, 4, 2, 4, 4])) + """ + +@overload +def moveaxis(input: Tensor, source: _int, destination: _int) -> Tensor: + r""" + moveaxis(input, source, destination) -> Tensor + + Alias for :func:`torch.movedim`. + + This function is equivalent to NumPy's moveaxis function. + + Examples:: + + >>> t = torch.randn(3,2,1) + >>> t + tensor([[[-0.3362], + [-0.8437]], + + [[-0.9627], + [ 0.1727]], + + [[ 0.5173], + [-0.1398]]]) + >>> torch.moveaxis(t, 1, 0).shape + torch.Size([2, 3, 1]) + >>> torch.moveaxis(t, 1, 0) + tensor([[[-0.3362], + [-0.9627], + [ 0.5173]], + + [[-0.8437], + [ 0.1727], + [-0.1398]]]) + >>> torch.moveaxis(t, (1, 2), (0, 1)).shape + torch.Size([2, 1, 3]) + >>> torch.moveaxis(t, (1, 2), (0, 1)) + tensor([[[-0.3362, -0.9627, 0.5173]], + + [[-0.8437, 0.1727, -0.1398]]]) + """ + +@overload +def moveaxis(input: Tensor, source: _size, destination: _size) -> Tensor: + r""" + moveaxis(input, source, destination) -> Tensor + + Alias for :func:`torch.movedim`. + + This function is equivalent to NumPy's moveaxis function. + + Examples:: + + >>> t = torch.randn(3,2,1) + >>> t + tensor([[[-0.3362], + [-0.8437]], + + [[-0.9627], + [ 0.1727]], + + [[ 0.5173], + [-0.1398]]]) + >>> torch.moveaxis(t, 1, 0).shape + torch.Size([2, 3, 1]) + >>> torch.moveaxis(t, 1, 0) + tensor([[[-0.3362], + [-0.9627], + [ 0.5173]], + + [[-0.8437], + [ 0.1727], + [-0.1398]]]) + >>> torch.moveaxis(t, (1, 2), (0, 1)).shape + torch.Size([2, 1, 3]) + >>> torch.moveaxis(t, (1, 2), (0, 1)) + tensor([[[-0.3362, -0.9627, 0.5173]], + + [[-0.8437, 0.1727, -0.1398]]]) + """ + +@overload +def movedim(input: Tensor, source: _int, destination: _int) -> Tensor: + r""" + movedim(input, source, destination) -> Tensor + + Moves the dimension(s) of :attr:`input` at the position(s) in :attr:`source` + to the position(s) in :attr:`destination`. + + Other dimensions of :attr:`input` that are not explicitly moved remain in + their original order and appear at the positions not specified in :attr:`destination`. + + Args: + input (Tensor): the input tensor. + source (int or tuple of ints): Original positions of the dims to move. These must be unique. + destination (int or tuple of ints): Destination positions for each of the original dims. These must also be unique. + + Examples:: + + >>> t = torch.randn(3,2,1) + >>> t + tensor([[[-0.3362], + [-0.8437]], + + [[-0.9627], + [ 0.1727]], + + [[ 0.5173], + [-0.1398]]]) + >>> torch.movedim(t, 1, 0).shape + torch.Size([2, 3, 1]) + >>> torch.movedim(t, 1, 0) + tensor([[[-0.3362], + [-0.9627], + [ 0.5173]], + + [[-0.8437], + [ 0.1727], + [-0.1398]]]) + >>> torch.movedim(t, (1, 2), (0, 1)).shape + torch.Size([2, 1, 3]) + >>> torch.movedim(t, (1, 2), (0, 1)) + tensor([[[-0.3362, -0.9627, 0.5173]], + + [[-0.8437, 0.1727, -0.1398]]]) + """ + +@overload +def movedim(input: Tensor, source: _size, destination: _size) -> Tensor: + r""" + movedim(input, source, destination) -> Tensor + + Moves the dimension(s) of :attr:`input` at the position(s) in :attr:`source` + to the position(s) in :attr:`destination`. + + Other dimensions of :attr:`input` that are not explicitly moved remain in + their original order and appear at the positions not specified in :attr:`destination`. + + Args: + input (Tensor): the input tensor. + source (int or tuple of ints): Original positions of the dims to move. These must be unique. + destination (int or tuple of ints): Destination positions for each of the original dims. These must also be unique. + + Examples:: + + >>> t = torch.randn(3,2,1) + >>> t + tensor([[[-0.3362], + [-0.8437]], + + [[-0.9627], + [ 0.1727]], + + [[ 0.5173], + [-0.1398]]]) + >>> torch.movedim(t, 1, 0).shape + torch.Size([2, 3, 1]) + >>> torch.movedim(t, 1, 0) + tensor([[[-0.3362], + [-0.9627], + [ 0.5173]], + + [[-0.8437], + [ 0.1727], + [-0.1398]]]) + >>> torch.movedim(t, (1, 2), (0, 1)).shape + torch.Size([2, 1, 3]) + >>> torch.movedim(t, (1, 2), (0, 1)) + tensor([[[-0.3362, -0.9627, 0.5173]], + + [[-0.8437, 0.1727, -0.1398]]]) + """ + +def msort(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + msort(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Sorts the elements of the :attr:`input` tensor along its first dimension + in ascending order by value. + + .. note:: `torch.msort(t)` is equivalent to `torch.sort(t, dim=0)[0]`. + See also :func:`torch.sort`. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.randn(3, 4) + >>> t + tensor([[-0.1321, 0.4370, -1.2631, -1.1289], + [-2.0527, -1.1250, 0.2275, 0.3077], + [-0.0881, -0.1259, -0.5495, 1.0284]]) + >>> torch.msort(t) + tensor([[-2.0527, -1.1250, -1.2631, -1.1289], + [-0.1321, -0.1259, -0.5495, 0.3077], + [-0.0881, 0.4370, 0.2275, 1.0284]]) + """ + +def mul( + input: Tensor | Number | _complex, + other: Tensor | Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + mul(input, other, *, out=None) -> Tensor + + Multiplies :attr:`input` by :attr:`other`. + + + .. math:: + \text{out}_i = \text{input}_i \times \text{other}_i + + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer, float, and complex inputs. + + Args: + input (Tensor): the input tensor. + other (Tensor or Number): the tensor or number to multiply input by. + + Keyword args: + out (Tensor, optional): the output tensor. + + Examples:: + + >>> a = torch.randn(3) + >>> a + tensor([ 0.2015, -0.4255, 2.6087]) + >>> torch.mul(a, 100) + tensor([ 20.1494, -42.5491, 260.8663]) + + >>> b = torch.randn(4, 1) + >>> b + tensor([[ 1.1207], + [-0.3137], + [ 0.0700], + [ 0.8378]]) + >>> c = torch.randn(1, 4) + >>> c + tensor([[ 0.5146, 0.1216, -0.5244, 2.2382]]) + >>> torch.mul(b, c) + tensor([[ 0.5767, 0.1363, -0.5877, 2.5083], + [-0.1614, -0.0382, 0.1645, -0.7021], + [ 0.0360, 0.0085, -0.0367, 0.1567], + [ 0.4312, 0.1019, -0.4394, 1.8753]]) + """ + +def multinomial( + input: Tensor, + num_samples: _int | SymInt, + replacement: _bool = False, + *, + generator: Generator | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + multinomial(input, num_samples, replacement=False, *, generator=None, out=None) -> LongTensor + + Returns a tensor where each row contains :attr:`num_samples` indices sampled + from the multinomial (a stricter definition would be multivariate, + refer to :class:`torch.distributions.multinomial.Multinomial` for more details) + probability distribution located in the corresponding row + of tensor :attr:`input`. + + .. note:: + The rows of :attr:`input` do not need to sum to one (in which case we use + the values as weights), but must be non-negative, finite and have + a non-zero sum. + + Indices are ordered from left to right according to when each was sampled + (first samples are placed in first column). + + If :attr:`input` is a vector, :attr:`out` is a vector of size :attr:`num_samples`. + + If :attr:`input` is a matrix with `m` rows, :attr:`out` is an matrix of shape + :math:`(m \times \text{num\_samples})`. + + If replacement is ``True``, samples are drawn with replacement. + + If not, they are drawn without replacement, which means that when a + sample index is drawn for a row, it cannot be drawn again for that row. + + .. note:: + When drawn without replacement, :attr:`num_samples` must be lower than + number of non-zero elements in :attr:`input` (or the min number of non-zero + elements in each row of :attr:`input` if it is a matrix). + + Args: + input (Tensor): the input tensor containing probabilities + num_samples (int): number of samples to draw + replacement (bool, optional): whether to draw with replacement or not + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + + Example:: + + >>> weights = torch.tensor([0, 10, 3, 0], dtype=torch.float) # create a tensor of weights + >>> torch.multinomial(weights, 2) + tensor([1, 2]) + >>> torch.multinomial(weights, 5) # ERROR! + RuntimeError: cannot sample n_sample > prob_dist.size(-1) samples without replacement + >>> torch.multinomial(weights, 4, replacement=True) + tensor([ 2, 1, 1, 1]) + """ + +@overload +def multiply( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + multiply(input, other, *, out=None) + + Alias for :func:`torch.mul`. + """ + +@overload +def multiply(input: Tensor, other: Number | _complex) -> Tensor: + r""" + multiply(input, other, *, out=None) + + Alias for :func:`torch.mul`. + """ + +def mv(input: Tensor, vec: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + mv(input, vec, *, out=None) -> Tensor + + Performs a matrix-vector product of the matrix :attr:`input` and the vector + :attr:`vec`. + + If :attr:`input` is a :math:`(n \times m)` tensor, :attr:`vec` is a 1-D tensor of + size :math:`m`, :attr:`out` will be 1-D of size :math:`n`. + + .. note:: This function does not :ref:`broadcast `. + + Args: + input (Tensor): matrix to be multiplied + vec (Tensor): vector to be multiplied + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> mat = torch.randn(2, 3) + >>> vec = torch.randn(3) + >>> torch.mv(mat, vec) + tensor([ 1.0404, -0.6361]) + """ + +def mvlgamma( + input: Tensor, + p: _int, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + mvlgamma(input, p, *, out=None) -> Tensor + + Alias for :func:`torch.special.multigammaln`. + """ + +def nan_to_num( + input: Tensor, + nan: _float | None = None, + posinf: _float | None = None, + neginf: _float | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + nan_to_num(input, nan=0.0, posinf=None, neginf=None, *, out=None) -> Tensor + + Replaces :literal:`NaN`, positive infinity, and negative infinity values in :attr:`input` + with the values specified by :attr:`nan`, :attr:`posinf`, and :attr:`neginf`, respectively. + By default, :literal:`NaN`\ s are replaced with zero, positive infinity is replaced with the + greatest finite value representable by :attr:`input`'s dtype, and negative infinity + is replaced with the least finite value representable by :attr:`input`'s dtype. + + Args: + input (Tensor): the input tensor. + nan (Number, optional): the value to replace :literal:`NaN`\s with. Default is zero. + posinf (Number, optional): if a Number, the value to replace positive infinity values with. + If None, positive infinity values are replaced with the greatest finite value representable by :attr:`input`'s dtype. + Default is None. + neginf (Number, optional): if a Number, the value to replace negative infinity values with. + If None, negative infinity values are replaced with the lowest finite value representable by :attr:`input`'s dtype. + Default is None. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> x = torch.tensor([float('nan'), float('inf'), -float('inf'), 3.14]) + >>> torch.nan_to_num(x) + tensor([ 0.0000e+00, 3.4028e+38, -3.4028e+38, 3.1400e+00]) + >>> torch.nan_to_num(x, nan=2.0) + tensor([ 2.0000e+00, 3.4028e+38, -3.4028e+38, 3.1400e+00]) + >>> torch.nan_to_num(x, nan=2.0, posinf=1.0) + tensor([ 2.0000e+00, 1.0000e+00, -3.4028e+38, 3.1400e+00]) + """ + +def nan_to_num_( + input: Tensor, + nan: _float | None = None, + posinf: _float | None = None, + neginf: _float | None = None, +) -> Tensor: ... +def nanmean( + input: Tensor, + dim: _int | _size | None = None, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + nanmean(input, dim=None, keepdim=False, *, dtype=None, out=None) -> Tensor + + Computes the mean of all `non-NaN` elements along the specified dimensions. + Input must be floating point or complex. + + This function is identical to :func:`torch.mean` when there are no `NaN` values + in the :attr:`input` tensor. In the presence of `NaN`, :func:`torch.mean` will + propagate the `NaN` to the output whereas :func:`torch.nanmean` will ignore the + `NaN` values (`torch.nanmean(a)` is equivalent to `torch.mean(a[~a.isnan()])`). + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor, either of floating point or complex dtype + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + out (Tensor, optional): the output tensor. + + .. seealso:: + + :func:`torch.mean` computes the mean value, propagating `NaN`. + + Example:: + + >>> x = torch.tensor([[torch.nan, 1, 2], [1, 2, 3]]) + >>> x.mean() + tensor(nan) + >>> x.nanmean() + tensor(1.8000) + >>> x.mean(dim=0) + tensor([ nan, 1.5000, 2.5000]) + >>> x.nanmean(dim=0) + tensor([1.0000, 1.5000, 2.5000]) + + # If all elements in the reduced dimensions are NaN then the result is NaN + >>> torch.tensor([torch.nan]).nanmean() + tensor(nan) + """ + +@overload +def nanmedian(input: Tensor) -> Tensor: + r""" + nanmedian(input) -> Tensor + + Returns the median of the values in :attr:`input`, ignoring ``NaN`` values. + + This function is identical to :func:`torch.median` when there are no ``NaN`` values in :attr:`input`. + When :attr:`input` has one or more ``NaN`` values, :func:`torch.median` will always return ``NaN``, + while this function will return the median of the non-``NaN`` elements in :attr:`input`. + If all the elements in :attr:`input` are ``NaN`` it will also return ``NaN``. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> a = torch.tensor([1, float('nan'), 3, 2]) + >>> a.median() + tensor(nan) + >>> a.nanmedian() + tensor(2.) + + .. function:: nanmedian(input, dim=-1, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` contains the median of each row of :attr:`input` + in the dimension :attr:`dim`, ignoring ``NaN`` values, and ``indices`` contains the index of the median values + found in the dimension :attr:`dim`. + + This function is identical to :func:`torch.median` when there are no ``NaN`` values in a reduced row. When a reduced row has + one or more ``NaN`` values, :func:`torch.median` will always reduce it to ``NaN``, while this function will reduce it to the + median of the non-``NaN`` elements. If all the elements in a reduced row are ``NaN`` then it will be reduced to ``NaN``, too. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out ((Tensor, Tensor), optional): The first tensor will be populated with the median values and the second + tensor, which must have dtype long, with their indices in the dimension + :attr:`dim` of :attr:`input`. + + Example:: + + >>> a = torch.tensor([[2, 3, 1], [float('nan'), 1, float('nan')]]) + >>> a + tensor([[2., 3., 1.], + [nan, 1., nan]]) + >>> a.median(0) + torch.return_types.median(values=tensor([nan, 1., nan]), indices=tensor([1, 1, 1])) + >>> a.nanmedian(0) + torch.return_types.nanmedian(values=tensor([2., 1., 1.]), indices=tensor([0, 1, 0])) + """ + +@overload +def nanmedian( + input: Tensor, + dim: _int, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.nanmedian: + r""" + nanmedian(input) -> Tensor + + Returns the median of the values in :attr:`input`, ignoring ``NaN`` values. + + This function is identical to :func:`torch.median` when there are no ``NaN`` values in :attr:`input`. + When :attr:`input` has one or more ``NaN`` values, :func:`torch.median` will always return ``NaN``, + while this function will return the median of the non-``NaN`` elements in :attr:`input`. + If all the elements in :attr:`input` are ``NaN`` it will also return ``NaN``. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> a = torch.tensor([1, float('nan'), 3, 2]) + >>> a.median() + tensor(nan) + >>> a.nanmedian() + tensor(2.) + + .. function:: nanmedian(input, dim=-1, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` contains the median of each row of :attr:`input` + in the dimension :attr:`dim`, ignoring ``NaN`` values, and ``indices`` contains the index of the median values + found in the dimension :attr:`dim`. + + This function is identical to :func:`torch.median` when there are no ``NaN`` values in a reduced row. When a reduced row has + one or more ``NaN`` values, :func:`torch.median` will always reduce it to ``NaN``, while this function will reduce it to the + median of the non-``NaN`` elements. If all the elements in a reduced row are ``NaN`` then it will be reduced to ``NaN``, too. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out ((Tensor, Tensor), optional): The first tensor will be populated with the median values and the second + tensor, which must have dtype long, with their indices in the dimension + :attr:`dim` of :attr:`input`. + + Example:: + + >>> a = torch.tensor([[2, 3, 1], [float('nan'), 1, float('nan')]]) + >>> a + tensor([[2., 3., 1.], + [nan, 1., nan]]) + >>> a.median(0) + torch.return_types.median(values=tensor([nan, 1., nan]), indices=tensor([1, 1, 1])) + >>> a.nanmedian(0) + torch.return_types.nanmedian(values=tensor([2., 1., 1.]), indices=tensor([0, 1, 0])) + """ + +@overload +def nanmedian( + input: Tensor, + dim: str | EllipsisType | None, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.nanmedian: + r""" + nanmedian(input) -> Tensor + + Returns the median of the values in :attr:`input`, ignoring ``NaN`` values. + + This function is identical to :func:`torch.median` when there are no ``NaN`` values in :attr:`input`. + When :attr:`input` has one or more ``NaN`` values, :func:`torch.median` will always return ``NaN``, + while this function will return the median of the non-``NaN`` elements in :attr:`input`. + If all the elements in :attr:`input` are ``NaN`` it will also return ``NaN``. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> a = torch.tensor([1, float('nan'), 3, 2]) + >>> a.median() + tensor(nan) + >>> a.nanmedian() + tensor(2.) + + .. function:: nanmedian(input, dim=-1, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` contains the median of each row of :attr:`input` + in the dimension :attr:`dim`, ignoring ``NaN`` values, and ``indices`` contains the index of the median values + found in the dimension :attr:`dim`. + + This function is identical to :func:`torch.median` when there are no ``NaN`` values in a reduced row. When a reduced row has + one or more ``NaN`` values, :func:`torch.median` will always reduce it to ``NaN``, while this function will reduce it to the + median of the non-``NaN`` elements. If all the elements in a reduced row are ``NaN`` then it will be reduced to ``NaN``, too. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out ((Tensor, Tensor), optional): The first tensor will be populated with the median values and the second + tensor, which must have dtype long, with their indices in the dimension + :attr:`dim` of :attr:`input`. + + Example:: + + >>> a = torch.tensor([[2, 3, 1], [float('nan'), 1, float('nan')]]) + >>> a + tensor([[2., 3., 1.], + [nan, 1., nan]]) + >>> a.median(0) + torch.return_types.median(values=tensor([nan, 1., nan]), indices=tensor([1, 1, 1])) + >>> a.nanmedian(0) + torch.return_types.nanmedian(values=tensor([2., 1., 1.]), indices=tensor([0, 1, 0])) + """ + +@overload +def nanquantile( + input: Tensor, + q: Tensor, + dim: _int | None = None, + keepdim: _bool = False, + *, + interpolation: str = "linear", + out: Tensor | None = None, +) -> Tensor: + r""" + nanquantile(input, q, dim=None, keepdim=False, *, interpolation='linear', out=None) -> Tensor + + This is a variant of :func:`torch.quantile` that "ignores" ``NaN`` values, + computing the quantiles :attr:`q` as if ``NaN`` values in :attr:`input` did + not exist. If all values in a reduced row are ``NaN`` then the quantiles for + that reduction will be ``NaN``. See the documentation for :func:`torch.quantile`. + + Args: + input (Tensor): the input tensor. + q (float or Tensor): a scalar or 1D tensor of quantile values in the range [0, 1] + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword arguments: + interpolation (str): interpolation method to use when the desired quantile lies between two data points. + Can be ``linear``, ``lower``, ``higher``, ``midpoint`` and ``nearest``. + Default is ``linear``. + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.tensor([float('nan'), 1, 2]) + >>> t.quantile(0.5) + tensor(nan) + >>> t.nanquantile(0.5) + tensor(1.5000) + >>> t = torch.tensor([[float('nan'), float('nan')], [1, 2]]) + >>> t + tensor([[nan, nan], + [1., 2.]]) + >>> t.nanquantile(0.5, dim=0) + tensor([1., 2.]) + >>> t.nanquantile(0.5, dim=1) + tensor([ nan, 1.5000]) + """ + +@overload +def nanquantile( + input: Tensor, + q: _float, + dim: _int | None = None, + keepdim: _bool = False, + *, + interpolation: str = "linear", + out: Tensor | None = None, +) -> Tensor: + r""" + nanquantile(input, q, dim=None, keepdim=False, *, interpolation='linear', out=None) -> Tensor + + This is a variant of :func:`torch.quantile` that "ignores" ``NaN`` values, + computing the quantiles :attr:`q` as if ``NaN`` values in :attr:`input` did + not exist. If all values in a reduced row are ``NaN`` then the quantiles for + that reduction will be ``NaN``. See the documentation for :func:`torch.quantile`. + + Args: + input (Tensor): the input tensor. + q (float or Tensor): a scalar or 1D tensor of quantile values in the range [0, 1] + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword arguments: + interpolation (str): interpolation method to use when the desired quantile lies between two data points. + Can be ``linear``, ``lower``, ``higher``, ``midpoint`` and ``nearest``. + Default is ``linear``. + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.tensor([float('nan'), 1, 2]) + >>> t.quantile(0.5) + tensor(nan) + >>> t.nanquantile(0.5) + tensor(1.5000) + >>> t = torch.tensor([[float('nan'), float('nan')], [1, 2]]) + >>> t + tensor([[nan, nan], + [1., 2.]]) + >>> t.nanquantile(0.5, dim=0) + tensor([1., 2.]) + >>> t.nanquantile(0.5, dim=1) + tensor([ nan, 1.5000]) + """ + +def nansum( + input: Tensor, + dim: _int | _size | None = None, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + nansum(input, *, dtype=None) -> Tensor + + Returns the sum of all elements, treating Not a Numbers (NaNs) as zero. + + Args: + input (Tensor): the input tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.tensor([1., 2., float('nan'), 4.]) + >>> torch.nansum(a) + tensor(7.) + + .. function:: nansum(input, dim, keepdim=False, *, dtype=None) -> Tensor + :noindex: + + Returns the sum of each row of the :attr:`input` tensor in the given + dimension :attr:`dim`, treating Not a Numbers (NaNs) as zero. + If :attr:`dim` is a list of dimensions, reduce over all of them. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> torch.nansum(torch.tensor([1., float("nan")])) + tensor(1.) + >>> a = torch.tensor([[1, 2], [3., float("nan")]]) + >>> torch.nansum(a) + tensor(6.) + >>> torch.nansum(a, dim=0) + tensor([4., 2.]) + >>> torch.nansum(a, dim=1) + tensor([3., 3.]) + """ + +@overload +def narrow( + input: Tensor, + dim: _int, + start: Tensor, + length: _int | SymInt, +) -> Tensor: + r""" + narrow(input, dim, start, length) -> Tensor + + Returns a new tensor that is a narrowed version of :attr:`input` tensor. The + dimension :attr:`dim` is input from :attr:`start` to ``start + length``. The + returned tensor and :attr:`input` tensor share the same underlying storage. + + Args: + input (Tensor): the tensor to narrow + dim (int): the dimension along which to narrow + start (int or Tensor): index of the element to start the narrowed dimension + from. Can be negative, which means indexing from the end of `dim`. If + `Tensor`, it must be an 0-dim integral `Tensor` (bools not allowed) + length (int): length of the narrowed dimension, must be weakly positive + + Example:: + + >>> x = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + >>> torch.narrow(x, 0, 0, 2) + tensor([[ 1, 2, 3], + [ 4, 5, 6]]) + >>> torch.narrow(x, 1, 1, 2) + tensor([[ 2, 3], + [ 5, 6], + [ 8, 9]]) + >>> torch.narrow(x, -1, torch.tensor(-1), 1) + tensor([[3], + [6], + [9]]) + """ + +@overload +def narrow( + input: Tensor, + dim: _int, + start: _int | SymInt, + length: _int | SymInt, +) -> Tensor: + r""" + narrow(input, dim, start, length) -> Tensor + + Returns a new tensor that is a narrowed version of :attr:`input` tensor. The + dimension :attr:`dim` is input from :attr:`start` to ``start + length``. The + returned tensor and :attr:`input` tensor share the same underlying storage. + + Args: + input (Tensor): the tensor to narrow + dim (int): the dimension along which to narrow + start (int or Tensor): index of the element to start the narrowed dimension + from. Can be negative, which means indexing from the end of `dim`. If + `Tensor`, it must be an 0-dim integral `Tensor` (bools not allowed) + length (int): length of the narrowed dimension, must be weakly positive + + Example:: + + >>> x = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + >>> torch.narrow(x, 0, 0, 2) + tensor([[ 1, 2, 3], + [ 4, 5, 6]]) + >>> torch.narrow(x, 1, 1, 2) + tensor([[ 2, 3], + [ 5, 6], + [ 8, 9]]) + >>> torch.narrow(x, -1, torch.tensor(-1), 1) + tensor([[3], + [6], + [9]]) + """ + +def narrow_copy( + input: Tensor, + dim: _int, + start: _int | SymInt, + length: _int | SymInt, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + narrow_copy(input, dim, start, length, *, out=None) -> Tensor + + Same as :meth:`Tensor.narrow` except this returns a copy rather + than shared storage. This is primarily for sparse tensors, which + do not have a shared-storage narrow method. + + Args: + input (Tensor): the tensor to narrow + dim (int): the dimension along which to narrow + start (int): index of the element to start the narrowed dimension from. Can + be negative, which means indexing from the end of `dim` + length (int): length of the narrowed dimension, must be weakly positive + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> x = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + >>> torch.narrow_copy(x, 0, 0, 2) + tensor([[ 1, 2, 3], + [ 4, 5, 6]]) + >>> torch.narrow_copy(x, 1, 1, 2) + tensor([[ 2, 3], + [ 5, 6], + [ 8, 9]]) + >>> s = torch.arange(16).reshape(2, 2, 2, 2).to_sparse(2) + >>> torch.narrow_copy(s, 0, 0, 1) + tensor(indices=tensor([[0, 0], + [0, 1]]), + values=tensor([[[0, 1], + [2, 3]], + + [[4, 5], + [6, 7]]]), + size=(1, 2, 2, 2), nnz=2, layout=torch.sparse_coo) + + .. seealso:: + + :func:`torch.narrow` for a non copy variant + """ + +def native_batch_norm( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor | None, + running_var: Tensor | None, + training: _bool, + momentum: _float, + eps: _float, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> tuple[Tensor, Tensor, Tensor]: ... +def native_channel_shuffle(input: Tensor, groups: _int | SymInt) -> Tensor: ... +def native_dropout( + input: Tensor, + p: _float, + train: _bool | None, +) -> tuple[Tensor, Tensor]: ... +def native_group_norm( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + N: _int | SymInt, + C: _int | SymInt, + HxW: _int | SymInt, + group: _int, + eps: _float, +) -> tuple[Tensor, Tensor, Tensor]: ... +def native_layer_norm( + input: Tensor, + normalized_shape: Sequence[_int | SymInt], + weight: Tensor | None, + bias: Tensor | None, + eps: _float, +) -> tuple[Tensor, Tensor, Tensor]: ... +@overload +def native_norm( + input: Tensor, + p: Number | _complex | None, + dim: _int | _size, + keepdim: _bool, + dtype: _dtype | None, +) -> Tensor: ... +@overload +def native_norm(input: Tensor, p: Number | _complex = 2) -> Tensor: ... +@overload +def ne( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + ne(input, other, *, out=None) -> Tensor + + Computes :math:`\text{input} \neq \text{other}` element-wise. + + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or float): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is not equal to :attr:`other` and False elsewhere + + Example:: + + >>> torch.ne(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[False, True], [True, False]]) + """ + +@overload +def ne( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + ne(input, other, *, out=None) -> Tensor + + Computes :math:`\text{input} \neq \text{other}` element-wise. + + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or float): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is not equal to :attr:`other` and False elsewhere + + Example:: + + >>> torch.ne(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[False, True], [True, False]]) + """ + +def neg(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + neg(input, *, out=None) -> Tensor + + Returns a new tensor with the negative of the elements of :attr:`input`. + + .. math:: + \text{out} = -1 \times \text{input} + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(5) + >>> a + tensor([ 0.0090, -0.2262, -0.0682, -0.2866, 0.3940]) + >>> torch.neg(a) + tensor([-0.0090, 0.2262, 0.0682, 0.2866, -0.3940]) + """ + +def neg_(input: Tensor) -> Tensor: ... +def negative(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + negative(input, *, out=None) -> Tensor + + Alias for :func:`torch.neg` + """ + +def negative_(input: Tensor) -> Tensor: ... +def nextafter( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + nextafter(input, other, *, out=None) -> Tensor + + Return the next floating-point value after :attr:`input` towards :attr:`other`, elementwise. + + The shapes of ``input`` and ``other`` must be + :ref:`broadcastable `. + + Args: + input (Tensor): the first input tensor + other (Tensor): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> eps = torch.finfo(torch.float32).eps + >>> torch.nextafter(torch.tensor([1.0, 2.0]), torch.tensor([2.0, 1.0])) == torch.tensor([eps + 1, 2 - eps]) + tensor([True, True]) + """ + +@overload +def nonzero( + input: Tensor, + *, + as_tuple: Literal[False] = False, + out: Tensor | None = None, +) -> Tensor: + r""" + nonzero(input, *, out=None, as_tuple=False) -> LongTensor or tuple of LongTensors + + .. note:: + :func:`torch.nonzero(..., as_tuple=False) ` (default) returns a + 2-D tensor where each row is the index for a nonzero value. + + :func:`torch.nonzero(..., as_tuple=True) ` returns a tuple of 1-D + index tensors, allowing for advanced indexing, so ``x[x.nonzero(as_tuple=True)]`` + gives all nonzero values of tensor ``x``. Of the returned tuple, each index tensor + contains nonzero indices for a certain dimension. + + See below for more details on the two behaviors. + + When :attr:`input` is on CUDA, :func:`torch.nonzero() ` causes + host-device synchronization. + + **When** :attr:`as_tuple` **is** ``False`` **(default)**: + + Returns a tensor containing the indices of all non-zero elements of + :attr:`input`. Each row in the result contains the indices of a non-zero + element in :attr:`input`. The result is sorted lexicographically, with + the last index changing the fastest (C-style). + + If :attr:`input` has :math:`n` dimensions, then the resulting indices tensor + :attr:`out` is of size :math:`(z \times n)`, where :math:`z` is the total number of + non-zero elements in the :attr:`input` tensor. + + **When** :attr:`as_tuple` **is** ``True``: + + Returns a tuple of 1-D tensors, one for each dimension in :attr:`input`, + each containing the indices (in that dimension) of all non-zero elements of + :attr:`input` . + + If :attr:`input` has :math:`n` dimensions, then the resulting tuple contains :math:`n` + tensors of size :math:`z`, where :math:`z` is the total number of + non-zero elements in the :attr:`input` tensor. + + As a special case, when :attr:`input` has zero dimensions and a nonzero scalar + value, it is treated as a one-dimensional tensor with one element. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (LongTensor, optional): the output tensor containing indices + + Returns: + LongTensor or tuple of LongTensor: If :attr:`as_tuple` is ``False``, the output + tensor containing indices. If :attr:`as_tuple` is ``True``, one 1-D tensor for + each dimension, containing the indices of each nonzero element along that + dimension. + + Example:: + + >>> torch.nonzero(torch.tensor([1, 1, 1, 0, 1])) + tensor([[ 0], + [ 1], + [ 2], + [ 4]]) + >>> torch.nonzero(torch.tensor([[0.6, 0.0, 0.0, 0.0], + ... [0.0, 0.4, 0.0, 0.0], + ... [0.0, 0.0, 1.2, 0.0], + ... [0.0, 0.0, 0.0,-0.4]])) + tensor([[ 0, 0], + [ 1, 1], + [ 2, 2], + [ 3, 3]]) + >>> torch.nonzero(torch.tensor([1, 1, 1, 0, 1]), as_tuple=True) + (tensor([0, 1, 2, 4]),) + >>> torch.nonzero(torch.tensor([[0.6, 0.0, 0.0, 0.0], + ... [0.0, 0.4, 0.0, 0.0], + ... [0.0, 0.0, 1.2, 0.0], + ... [0.0, 0.0, 0.0,-0.4]]), as_tuple=True) + (tensor([0, 1, 2, 3]), tensor([0, 1, 2, 3])) + >>> torch.nonzero(torch.tensor(5), as_tuple=True) + (tensor([0]),) + """ + +@overload +def nonzero( + input: Tensor, + *, + as_tuple: Literal[True], +) -> tuple[Tensor, ...]: + r""" + nonzero(input, *, out=None, as_tuple=False) -> LongTensor or tuple of LongTensors + + .. note:: + :func:`torch.nonzero(..., as_tuple=False) ` (default) returns a + 2-D tensor where each row is the index for a nonzero value. + + :func:`torch.nonzero(..., as_tuple=True) ` returns a tuple of 1-D + index tensors, allowing for advanced indexing, so ``x[x.nonzero(as_tuple=True)]`` + gives all nonzero values of tensor ``x``. Of the returned tuple, each index tensor + contains nonzero indices for a certain dimension. + + See below for more details on the two behaviors. + + When :attr:`input` is on CUDA, :func:`torch.nonzero() ` causes + host-device synchronization. + + **When** :attr:`as_tuple` **is** ``False`` **(default)**: + + Returns a tensor containing the indices of all non-zero elements of + :attr:`input`. Each row in the result contains the indices of a non-zero + element in :attr:`input`. The result is sorted lexicographically, with + the last index changing the fastest (C-style). + + If :attr:`input` has :math:`n` dimensions, then the resulting indices tensor + :attr:`out` is of size :math:`(z \times n)`, where :math:`z` is the total number of + non-zero elements in the :attr:`input` tensor. + + **When** :attr:`as_tuple` **is** ``True``: + + Returns a tuple of 1-D tensors, one for each dimension in :attr:`input`, + each containing the indices (in that dimension) of all non-zero elements of + :attr:`input` . + + If :attr:`input` has :math:`n` dimensions, then the resulting tuple contains :math:`n` + tensors of size :math:`z`, where :math:`z` is the total number of + non-zero elements in the :attr:`input` tensor. + + As a special case, when :attr:`input` has zero dimensions and a nonzero scalar + value, it is treated as a one-dimensional tensor with one element. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (LongTensor, optional): the output tensor containing indices + + Returns: + LongTensor or tuple of LongTensor: If :attr:`as_tuple` is ``False``, the output + tensor containing indices. If :attr:`as_tuple` is ``True``, one 1-D tensor for + each dimension, containing the indices of each nonzero element along that + dimension. + + Example:: + + >>> torch.nonzero(torch.tensor([1, 1, 1, 0, 1])) + tensor([[ 0], + [ 1], + [ 2], + [ 4]]) + >>> torch.nonzero(torch.tensor([[0.6, 0.0, 0.0, 0.0], + ... [0.0, 0.4, 0.0, 0.0], + ... [0.0, 0.0, 1.2, 0.0], + ... [0.0, 0.0, 0.0,-0.4]])) + tensor([[ 0, 0], + [ 1, 1], + [ 2, 2], + [ 3, 3]]) + >>> torch.nonzero(torch.tensor([1, 1, 1, 0, 1]), as_tuple=True) + (tensor([0, 1, 2, 4]),) + >>> torch.nonzero(torch.tensor([[0.6, 0.0, 0.0, 0.0], + ... [0.0, 0.4, 0.0, 0.0], + ... [0.0, 0.0, 1.2, 0.0], + ... [0.0, 0.0, 0.0,-0.4]]), as_tuple=True) + (tensor([0, 1, 2, 3]), tensor([0, 1, 2, 3])) + >>> torch.nonzero(torch.tensor(5), as_tuple=True) + (tensor([0]),) + """ + +def nonzero_static( + input: Tensor, + *, + size: _int | SymInt, + fill_value: _int = -1, + out: Tensor | None = None, +) -> Tensor: ... +def norm_except_dim(v: Tensor, pow: _int = 2, dim: _int = 0) -> Tensor: ... +@overload +def normal( + mean: Tensor, + std: Tensor, + *, + generator: Generator | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + normal(mean, std, *, generator=None, out=None) -> Tensor + + Returns a tensor of random numbers drawn from separate normal distributions + whose mean and standard deviation are given. + + The :attr:`mean` is a tensor with the mean of + each output element's normal distribution + + The :attr:`std` is a tensor with the standard deviation of + each output element's normal distribution + + The shapes of :attr:`mean` and :attr:`std` don't need to match, but the + total number of elements in each tensor need to be the same. + + .. note:: When the shapes do not match, the shape of :attr:`mean` + is used as the shape for the returned output tensor + + .. note:: When :attr:`std` is a CUDA tensor, this function synchronizes + its device with the CPU. + + Args: + mean (Tensor): the tensor of per-element means + std (Tensor): the tensor of per-element standard deviations + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(mean=torch.arange(1., 11.), std=torch.arange(1, 0, -0.1)) + tensor([ 1.0425, 3.5672, 2.7969, 4.2925, 4.7229, 6.2134, + 8.0505, 8.1408, 9.0563, 10.0566]) + + .. function:: normal(mean=0.0, std, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the means are shared among all drawn + elements. + + Args: + mean (float, optional): the mean for all distributions + std (Tensor): the tensor of per-element standard deviations + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(mean=0.5, std=torch.arange(1., 6.)) + tensor([-1.2793, -1.0732, -2.0687, 5.1177, -1.2303]) + + .. function:: normal(mean, std=1.0, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the standard deviations are shared among + all drawn elements. + + Args: + mean (Tensor): the tensor of per-element means + std (float, optional): the standard deviation for all distributions + + Keyword args: + out (Tensor, optional): the output tensor + + Example:: + + >>> torch.normal(mean=torch.arange(1., 6.)) + tensor([ 1.1552, 2.6148, 2.6535, 5.8318, 4.2361]) + + .. function:: normal(mean, std, size, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the means and standard deviations are shared + among all drawn elements. The resulting tensor has size given by :attr:`size`. + + Args: + mean (float): the mean for all distributions + std (float): the standard deviation for all distributions + size (int...): a sequence of integers defining the shape of the output tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(2, 3, size=(1, 4)) + tensor([[-1.3987, -1.9544, 3.6048, 0.7909]]) + """ + +@overload +def normal( + mean: Tensor, + std: _float = 1, + *, + generator: Generator | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + normal(mean, std, *, generator=None, out=None) -> Tensor + + Returns a tensor of random numbers drawn from separate normal distributions + whose mean and standard deviation are given. + + The :attr:`mean` is a tensor with the mean of + each output element's normal distribution + + The :attr:`std` is a tensor with the standard deviation of + each output element's normal distribution + + The shapes of :attr:`mean` and :attr:`std` don't need to match, but the + total number of elements in each tensor need to be the same. + + .. note:: When the shapes do not match, the shape of :attr:`mean` + is used as the shape for the returned output tensor + + .. note:: When :attr:`std` is a CUDA tensor, this function synchronizes + its device with the CPU. + + Args: + mean (Tensor): the tensor of per-element means + std (Tensor): the tensor of per-element standard deviations + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(mean=torch.arange(1., 11.), std=torch.arange(1, 0, -0.1)) + tensor([ 1.0425, 3.5672, 2.7969, 4.2925, 4.7229, 6.2134, + 8.0505, 8.1408, 9.0563, 10.0566]) + + .. function:: normal(mean=0.0, std, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the means are shared among all drawn + elements. + + Args: + mean (float, optional): the mean for all distributions + std (Tensor): the tensor of per-element standard deviations + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(mean=0.5, std=torch.arange(1., 6.)) + tensor([-1.2793, -1.0732, -2.0687, 5.1177, -1.2303]) + + .. function:: normal(mean, std=1.0, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the standard deviations are shared among + all drawn elements. + + Args: + mean (Tensor): the tensor of per-element means + std (float, optional): the standard deviation for all distributions + + Keyword args: + out (Tensor, optional): the output tensor + + Example:: + + >>> torch.normal(mean=torch.arange(1., 6.)) + tensor([ 1.1552, 2.6148, 2.6535, 5.8318, 4.2361]) + + .. function:: normal(mean, std, size, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the means and standard deviations are shared + among all drawn elements. The resulting tensor has size given by :attr:`size`. + + Args: + mean (float): the mean for all distributions + std (float): the standard deviation for all distributions + size (int...): a sequence of integers defining the shape of the output tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(2, 3, size=(1, 4)) + tensor([[-1.3987, -1.9544, 3.6048, 0.7909]]) + """ + +@overload +def normal( + mean: _float, + std: Tensor, + *, + generator: Generator | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + normal(mean, std, *, generator=None, out=None) -> Tensor + + Returns a tensor of random numbers drawn from separate normal distributions + whose mean and standard deviation are given. + + The :attr:`mean` is a tensor with the mean of + each output element's normal distribution + + The :attr:`std` is a tensor with the standard deviation of + each output element's normal distribution + + The shapes of :attr:`mean` and :attr:`std` don't need to match, but the + total number of elements in each tensor need to be the same. + + .. note:: When the shapes do not match, the shape of :attr:`mean` + is used as the shape for the returned output tensor + + .. note:: When :attr:`std` is a CUDA tensor, this function synchronizes + its device with the CPU. + + Args: + mean (Tensor): the tensor of per-element means + std (Tensor): the tensor of per-element standard deviations + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(mean=torch.arange(1., 11.), std=torch.arange(1, 0, -0.1)) + tensor([ 1.0425, 3.5672, 2.7969, 4.2925, 4.7229, 6.2134, + 8.0505, 8.1408, 9.0563, 10.0566]) + + .. function:: normal(mean=0.0, std, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the means are shared among all drawn + elements. + + Args: + mean (float, optional): the mean for all distributions + std (Tensor): the tensor of per-element standard deviations + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(mean=0.5, std=torch.arange(1., 6.)) + tensor([-1.2793, -1.0732, -2.0687, 5.1177, -1.2303]) + + .. function:: normal(mean, std=1.0, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the standard deviations are shared among + all drawn elements. + + Args: + mean (Tensor): the tensor of per-element means + std (float, optional): the standard deviation for all distributions + + Keyword args: + out (Tensor, optional): the output tensor + + Example:: + + >>> torch.normal(mean=torch.arange(1., 6.)) + tensor([ 1.1552, 2.6148, 2.6535, 5.8318, 4.2361]) + + .. function:: normal(mean, std, size, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the means and standard deviations are shared + among all drawn elements. The resulting tensor has size given by :attr:`size`. + + Args: + mean (float): the mean for all distributions + std (float): the standard deviation for all distributions + size (int...): a sequence of integers defining the shape of the output tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(2, 3, size=(1, 4)) + tensor([[-1.3987, -1.9544, 3.6048, 0.7909]]) + """ + +@overload +def normal( + mean: _float, + std: _float, + size: Sequence[_int | SymInt], + *, + generator: Generator | None = None, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + normal(mean, std, *, generator=None, out=None) -> Tensor + + Returns a tensor of random numbers drawn from separate normal distributions + whose mean and standard deviation are given. + + The :attr:`mean` is a tensor with the mean of + each output element's normal distribution + + The :attr:`std` is a tensor with the standard deviation of + each output element's normal distribution + + The shapes of :attr:`mean` and :attr:`std` don't need to match, but the + total number of elements in each tensor need to be the same. + + .. note:: When the shapes do not match, the shape of :attr:`mean` + is used as the shape for the returned output tensor + + .. note:: When :attr:`std` is a CUDA tensor, this function synchronizes + its device with the CPU. + + Args: + mean (Tensor): the tensor of per-element means + std (Tensor): the tensor of per-element standard deviations + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(mean=torch.arange(1., 11.), std=torch.arange(1, 0, -0.1)) + tensor([ 1.0425, 3.5672, 2.7969, 4.2925, 4.7229, 6.2134, + 8.0505, 8.1408, 9.0563, 10.0566]) + + .. function:: normal(mean=0.0, std, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the means are shared among all drawn + elements. + + Args: + mean (float, optional): the mean for all distributions + std (Tensor): the tensor of per-element standard deviations + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(mean=0.5, std=torch.arange(1., 6.)) + tensor([-1.2793, -1.0732, -2.0687, 5.1177, -1.2303]) + + .. function:: normal(mean, std=1.0, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the standard deviations are shared among + all drawn elements. + + Args: + mean (Tensor): the tensor of per-element means + std (float, optional): the standard deviation for all distributions + + Keyword args: + out (Tensor, optional): the output tensor + + Example:: + + >>> torch.normal(mean=torch.arange(1., 6.)) + tensor([ 1.1552, 2.6148, 2.6535, 5.8318, 4.2361]) + + .. function:: normal(mean, std, size, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the means and standard deviations are shared + among all drawn elements. The resulting tensor has size given by :attr:`size`. + + Args: + mean (float): the mean for all distributions + std (float): the standard deviation for all distributions + size (int...): a sequence of integers defining the shape of the output tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(2, 3, size=(1, 4)) + tensor([[-1.3987, -1.9544, 3.6048, 0.7909]]) + """ + +@overload +def not_equal( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + not_equal(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.ne`. + """ + +@overload +def not_equal( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + not_equal(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.ne`. + """ + +@overload +def nuclear_norm( + input: Tensor, + dim: _int | _size, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: ... +@overload +def nuclear_norm( + input: Tensor, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: ... +def numel(self: Tensor) -> _int: + r""" + numel(input: Tensor) -> int + + Returns the total number of elements in the :attr:`input` tensor. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> a = torch.randn(1, 2, 3, 4, 5) + >>> torch.numel(a) + 120 + >>> a = torch.zeros(4,4) + >>> torch.numel(a) + 16 + """ + +@overload +def ones( + size: Sequence[_int | SymInt], + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + ones(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with the scalar value `1`, with the shape defined + by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.ones(2, 3) + tensor([[ 1., 1., 1.], + [ 1., 1., 1.]]) + + >>> torch.ones(5) + tensor([ 1., 1., 1., 1., 1.]) + """ + +@overload +def ones( + *size: _int | SymInt, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + ones(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with the scalar value `1`, with the shape defined + by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.ones(2, 3) + tensor([[ 1., 1., 1.], + [ 1., 1., 1.]]) + + >>> torch.ones(5) + tensor([ 1., 1., 1., 1., 1.]) + """ + +@overload +def ones( + size: _size, + *, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + ones(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with the scalar value `1`, with the shape defined + by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.ones(2, 3) + tensor([[ 1., 1., 1.], + [ 1., 1., 1.]]) + + >>> torch.ones(5) + tensor([ 1., 1., 1., 1., 1.]) + """ + +@overload +def ones( + *size: _int, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + ones(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with the scalar value `1`, with the shape defined + by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.ones(2, 3) + tensor([[ 1., 1., 1.], + [ 1., 1., 1.]]) + + >>> torch.ones(5) + tensor([ 1., 1., 1., 1., 1.]) + """ + +def ones_like( + input: Tensor, + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + ones_like(input, *, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor filled with the scalar value `1`, with the same size as + :attr:`input`. ``torch.ones_like(input)`` is equivalent to + ``torch.ones(input.size(), dtype=input.dtype, layout=input.layout, device=input.device)``. + + .. warning:: + As of 0.4, this function does not support an :attr:`out` keyword. As an alternative, + the old ``torch.ones_like(input, out=output)`` is equivalent to + ``torch.ones(input.size(), out=output)``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + + Keyword arguments: + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + + Example:: + + >>> input = torch.empty(2, 3) + >>> torch.ones_like(input) + tensor([[ 1., 1., 1.], + [ 1., 1., 1.]]) + """ + +def orgqr( + input: Tensor, + input2: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + orgqr(input, tau) -> Tensor + + Alias for :func:`torch.linalg.householder_product`. + """ + +def ormqr( + input: Tensor, + input2: Tensor, + input3: Tensor, + left: _bool = True, + transpose: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + ormqr(input, tau, other, left=True, transpose=False, *, out=None) -> Tensor + + Computes the matrix-matrix multiplication of a product of Householder matrices with a general matrix. + + Multiplies a :math:`m \times n` matrix `C` (given by :attr:`other`) with a matrix `Q`, + where `Q` is represented using Householder reflectors `(input, tau)`. + See `Representation of Orthogonal or Unitary Matrices`_ for further details. + + If :attr:`left` is `True` then `op(Q)` times `C` is computed, otherwise the result is `C` times `op(Q)`. + When :attr:`left` is `True`, the implicit matrix `Q` has size :math:`m \times m`. + It has size :math:`n \times n` otherwise. + If :attr:`transpose` is `True` then `op` is the conjugate transpose operation, otherwise it's a no-op. + + Supports inputs of float, double, cfloat and cdouble dtypes. + Also supports batched inputs, and, if the input is batched, the output is batched with the same dimensions. + + .. seealso:: + :func:`torch.geqrf` can be used to form the Householder representation `(input, tau)` of matrix `Q` + from the QR decomposition. + + .. note:: + This function supports backward but it is only fast when ``(input, tau)`` do not require gradients + and/or ``tau.size(-1)`` is very small. + `` + + Args: + input (Tensor): tensor of shape `(*, mn, k)` where `*` is zero or more batch dimensions + and `mn` equals to `m` or `n` depending on the :attr:`left`. + tau (Tensor): tensor of shape `(*, min(mn, k))` where `*` is zero or more batch dimensions. + other (Tensor): tensor of shape `(*, m, n)` where `*` is zero or more batch dimensions. + left (bool): controls the order of multiplication. + transpose (bool): controls whether the matrix `Q` is conjugate transposed or not. + + Keyword args: + out (Tensor, optional): the output Tensor. Ignored if `None`. Default: `None`. + + .. _Representation of Orthogonal or Unitary Matrices: + https://www.netlib.org/lapack/lug/node128.html + """ + +def outer( + input: Tensor, + vec2: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + outer(input, vec2, *, out=None) -> Tensor + + Outer product of :attr:`input` and :attr:`vec2`. + If :attr:`input` is a vector of size :math:`n` and :attr:`vec2` is a vector of + size :math:`m`, then :attr:`out` must be a matrix of size :math:`(n \times m)`. + + .. note:: This function does not :ref:`broadcast `. + + Args: + input (Tensor): 1-D input vector + vec2 (Tensor): 1-D input vector + + Keyword args: + out (Tensor, optional): optional output matrix + + Example:: + + >>> v1 = torch.arange(1., 5.) + >>> v2 = torch.arange(1., 4.) + >>> torch.outer(v1, v2) + tensor([[ 1., 2., 3.], + [ 2., 4., 6.], + [ 3., 6., 9.], + [ 4., 8., 12.]]) + """ + +def pairwise_distance( + x1: Tensor, + x2: Tensor, + p: _float = 2, + eps: _float = 1e-06, + keepdim: _bool = False, +) -> Tensor: ... +def pdist(input: Tensor, p: _float = 2) -> Tensor: ... +def permute(input: Tensor, dims: _size) -> Tensor: + r""" + permute(input, dims) -> Tensor + + Returns a view of the original tensor :attr:`input` with its dimensions permuted. + + Args: + input (Tensor): the input tensor. + dims (torch.Size, tuple of int or list of int): the desired ordering of dimensions. + + Example: + >>> x = torch.randn(2, 3, 5) + >>> x.size() + torch.Size([2, 3, 5]) + >>> torch.permute(x, (2, 0, 1)).size() + torch.Size([5, 2, 3]) + """ + +def permute_copy( + input: Tensor, + dims: _size, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.permute`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def pinverse(input: Tensor, rcond: _float = 1e-15) -> Tensor: + r""" + pinverse(input, rcond=1e-15) -> Tensor + + Alias for :func:`torch.linalg.pinv` + """ + +def pixel_shuffle(input: Tensor, upscale_factor: _int) -> Tensor: ... +def pixel_unshuffle(input: Tensor, downscale_factor: _int) -> Tensor: ... +def poisson(input: Tensor, generator: Generator | None = None) -> Tensor: + r""" + poisson(input, generator=None) -> Tensor + + Returns a tensor of the same size as :attr:`input` with each element + sampled from a Poisson distribution with rate parameter given by the corresponding + element in :attr:`input` i.e., + + .. math:: + \text{out}_i \sim \text{Poisson}(\text{input}_i) + + :attr:`input` must be non-negative. + + Args: + input (Tensor): the input tensor containing the rates of the Poisson distribution + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + + Example:: + + >>> rates = torch.rand(4, 4) * 5 # rate parameter between 0 and 5 + >>> torch.poisson(rates) + tensor([[9., 1., 3., 5.], + [8., 6., 6., 0.], + [0., 4., 5., 3.], + [2., 1., 4., 2.]]) + """ + +def poisson_nll_loss( + input: Tensor, + target: Tensor, + log_input: _bool, + full: _bool, + eps: _float, + reduction: _int, +) -> Tensor: ... +def polar( + abs: Tensor, + angle: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + polar(abs, angle, *, out=None) -> Tensor + + Constructs a complex tensor whose elements are Cartesian coordinates + corresponding to the polar coordinates with absolute value :attr:`abs` and angle + :attr:`angle`. + + .. math:: + \text{out} = \text{abs} \cdot \cos(\text{angle}) + \text{abs} \cdot \sin(\text{angle}) \cdot j + + .. note:: + `torch.polar` is similar to + `std::polar `_ + and does not compute the polar decomposition + of a complex tensor like Python's `cmath.polar` and SciPy's `linalg.polar` do. + The behavior of this function is undefined if `abs` is negative or NaN, or if `angle` is + infinite. + + + Args: + abs (Tensor): The absolute value the complex tensor. Must be float or double. + angle (Tensor): The angle of the complex tensor. Must be same dtype as + :attr:`abs`. + + Keyword args: + out (Tensor): If the inputs are ``torch.float32``, must be + ``torch.complex64``. If the inputs are ``torch.float64``, must be + ``torch.complex128``. + + Example:: + + >>> import numpy as np + >>> abs = torch.tensor([1, 2], dtype=torch.float64) + >>> angle = torch.tensor([np.pi / 2, 5 * np.pi / 4], dtype=torch.float64) + >>> z = torch.polar(abs, angle) + >>> z + tensor([(0.0000+1.0000j), (-1.4142-1.4142j)], dtype=torch.complex128) + """ + +def polygamma( + n: _int, + input: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + polygamma(n, input, *, out=None) -> Tensor + + Alias for :func:`torch.special.polygamma`. + """ + +def positive(input: Tensor) -> Tensor: + r""" + positive(input) -> Tensor + + Returns :attr:`input`. + Throws a runtime error if :attr:`input` is a bool tensor. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> t = torch.randn(5) + >>> t + tensor([ 0.0090, -0.2262, -0.0682, -0.2866, 0.3940]) + >>> torch.positive(t) + tensor([ 0.0090, -0.2262, -0.0682, -0.2866, 0.3940]) + """ + +@overload +def pow( + input: Tensor, + exponent: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + pow(input, exponent, *, out=None) -> Tensor + + Takes the power of each element in :attr:`input` with :attr:`exponent` and + returns a tensor with the result. + + :attr:`exponent` can be either a single ``float`` number or a `Tensor` + with the same number of elements as :attr:`input`. + + When :attr:`exponent` is a scalar value, the operation applied is: + + .. math:: + \text{out}_i = x_i ^ \text{exponent} + + When :attr:`exponent` is a tensor, the operation applied is: + + .. math:: + \text{out}_i = x_i ^ {\text{exponent}_i} + + When :attr:`exponent` is a tensor, the shapes of :attr:`input` + and :attr:`exponent` must be :ref:`broadcastable `. + + Args: + input (Tensor): the input tensor. + exponent (float or tensor): the exponent value + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.4331, 1.2475, 0.6834, -0.2791]) + >>> torch.pow(a, 2) + tensor([ 0.1875, 1.5561, 0.4670, 0.0779]) + >>> exp = torch.arange(1., 5.) + + >>> a = torch.arange(1., 5.) + >>> a + tensor([ 1., 2., 3., 4.]) + >>> exp + tensor([ 1., 2., 3., 4.]) + >>> torch.pow(a, exp) + tensor([ 1., 4., 27., 256.]) + + .. function:: pow(self, exponent, *, out=None) -> Tensor + :noindex: + + :attr:`self` is a scalar ``float`` value, and :attr:`exponent` is a tensor. + The returned tensor :attr:`out` is of the same shape as :attr:`exponent` + + The operation applied is: + + .. math:: + \text{out}_i = \text{self} ^ {\text{exponent}_i} + + Args: + self (float): the scalar base value for the power operation + exponent (Tensor): the exponent tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> exp = torch.arange(1., 5.) + >>> base = 2 + >>> torch.pow(base, exp) + tensor([ 2., 4., 8., 16.]) + """ + +@overload +def pow( + self: Number | _complex, + exponent: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + pow(input, exponent, *, out=None) -> Tensor + + Takes the power of each element in :attr:`input` with :attr:`exponent` and + returns a tensor with the result. + + :attr:`exponent` can be either a single ``float`` number or a `Tensor` + with the same number of elements as :attr:`input`. + + When :attr:`exponent` is a scalar value, the operation applied is: + + .. math:: + \text{out}_i = x_i ^ \text{exponent} + + When :attr:`exponent` is a tensor, the operation applied is: + + .. math:: + \text{out}_i = x_i ^ {\text{exponent}_i} + + When :attr:`exponent` is a tensor, the shapes of :attr:`input` + and :attr:`exponent` must be :ref:`broadcastable `. + + Args: + input (Tensor): the input tensor. + exponent (float or tensor): the exponent value + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.4331, 1.2475, 0.6834, -0.2791]) + >>> torch.pow(a, 2) + tensor([ 0.1875, 1.5561, 0.4670, 0.0779]) + >>> exp = torch.arange(1., 5.) + + >>> a = torch.arange(1., 5.) + >>> a + tensor([ 1., 2., 3., 4.]) + >>> exp + tensor([ 1., 2., 3., 4.]) + >>> torch.pow(a, exp) + tensor([ 1., 4., 27., 256.]) + + .. function:: pow(self, exponent, *, out=None) -> Tensor + :noindex: + + :attr:`self` is a scalar ``float`` value, and :attr:`exponent` is a tensor. + The returned tensor :attr:`out` is of the same shape as :attr:`exponent` + + The operation applied is: + + .. math:: + \text{out}_i = \text{self} ^ {\text{exponent}_i} + + Args: + self (float): the scalar base value for the power operation + exponent (Tensor): the exponent tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> exp = torch.arange(1., 5.) + >>> base = 2 + >>> torch.pow(base, exp) + tensor([ 2., 4., 8., 16.]) + """ + +@overload +def pow( + input: Tensor, + exponent: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + pow(input, exponent, *, out=None) -> Tensor + + Takes the power of each element in :attr:`input` with :attr:`exponent` and + returns a tensor with the result. + + :attr:`exponent` can be either a single ``float`` number or a `Tensor` + with the same number of elements as :attr:`input`. + + When :attr:`exponent` is a scalar value, the operation applied is: + + .. math:: + \text{out}_i = x_i ^ \text{exponent} + + When :attr:`exponent` is a tensor, the operation applied is: + + .. math:: + \text{out}_i = x_i ^ {\text{exponent}_i} + + When :attr:`exponent` is a tensor, the shapes of :attr:`input` + and :attr:`exponent` must be :ref:`broadcastable `. + + Args: + input (Tensor): the input tensor. + exponent (float or tensor): the exponent value + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.4331, 1.2475, 0.6834, -0.2791]) + >>> torch.pow(a, 2) + tensor([ 0.1875, 1.5561, 0.4670, 0.0779]) + >>> exp = torch.arange(1., 5.) + + >>> a = torch.arange(1., 5.) + >>> a + tensor([ 1., 2., 3., 4.]) + >>> exp + tensor([ 1., 2., 3., 4.]) + >>> torch.pow(a, exp) + tensor([ 1., 4., 27., 256.]) + + .. function:: pow(self, exponent, *, out=None) -> Tensor + :noindex: + + :attr:`self` is a scalar ``float`` value, and :attr:`exponent` is a tensor. + The returned tensor :attr:`out` is of the same shape as :attr:`exponent` + + The operation applied is: + + .. math:: + \text{out}_i = \text{self} ^ {\text{exponent}_i} + + Args: + self (float): the scalar base value for the power operation + exponent (Tensor): the exponent tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> exp = torch.arange(1., 5.) + >>> base = 2 + >>> torch.pow(base, exp) + tensor([ 2., 4., 8., 16.]) + """ + +def prelu(input: Tensor, weight: Tensor) -> Tensor: ... +@overload +def prod(input: Tensor, *, dtype: _dtype | None = None) -> Tensor: + r""" + prod(input: Tensor, *, dtype: Optional[_dtype]) -> Tensor + + Returns the product of all elements in the :attr:`input` tensor. + + Args: + input (Tensor): the input tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[-0.8020, 0.5428, -1.5854]]) + >>> torch.prod(a) + tensor(0.6902) + + .. function:: prod(input, dim, keepdim=False, *, dtype=None) -> Tensor + :noindex: + + Returns the product of each row of the :attr:`input` tensor in the given + dimension :attr:`dim`. + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in + the output tensor having 1 fewer dimension than :attr:`input`. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(4, 2) + >>> a + tensor([[ 0.5261, -0.3837], + [ 1.1857, -0.2498], + [-1.1646, 0.0705], + [ 1.1131, -1.0629]]) + >>> torch.prod(a, 1) + tensor([-0.2018, -0.2962, -0.0821, -1.1831]) + """ + +@overload +def prod( + input: Tensor, + dim: _int, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + prod(input: Tensor, *, dtype: Optional[_dtype]) -> Tensor + + Returns the product of all elements in the :attr:`input` tensor. + + Args: + input (Tensor): the input tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[-0.8020, 0.5428, -1.5854]]) + >>> torch.prod(a) + tensor(0.6902) + + .. function:: prod(input, dim, keepdim=False, *, dtype=None) -> Tensor + :noindex: + + Returns the product of each row of the :attr:`input` tensor in the given + dimension :attr:`dim`. + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in + the output tensor having 1 fewer dimension than :attr:`input`. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(4, 2) + >>> a + tensor([[ 0.5261, -0.3837], + [ 1.1857, -0.2498], + [-1.1646, 0.0705], + [ 1.1131, -1.0629]]) + >>> torch.prod(a, 1) + tensor([-0.2018, -0.2962, -0.0821, -1.1831]) + """ + +@overload +def prod( + input: Tensor, + dim: str | EllipsisType | None, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + prod(input: Tensor, *, dtype: Optional[_dtype]) -> Tensor + + Returns the product of all elements in the :attr:`input` tensor. + + Args: + input (Tensor): the input tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[-0.8020, 0.5428, -1.5854]]) + >>> torch.prod(a) + tensor(0.6902) + + .. function:: prod(input, dim, keepdim=False, *, dtype=None) -> Tensor + :noindex: + + Returns the product of each row of the :attr:`input` tensor in the given + dimension :attr:`dim`. + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in + the output tensor having 1 fewer dimension than :attr:`input`. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(4, 2) + >>> a + tensor([[ 0.5261, -0.3837], + [ 1.1857, -0.2498], + [-1.1646, 0.0705], + [ 1.1131, -1.0629]]) + >>> torch.prod(a, 1) + tensor([-0.2018, -0.2962, -0.0821, -1.1831]) + """ + +def promote_types(type1: _dtype, type2: _dtype) -> _dtype: + r""" + promote_types(type1, type2) -> dtype + + Returns the :class:`torch.dtype` with the smallest size and scalar kind that is + not smaller nor of lower kind than either `type1` or `type2`. See type promotion + :ref:`documentation ` for more information on the type + promotion logic. + + Args: + type1 (:class:`torch.dtype`) + type2 (:class:`torch.dtype`) + + Example:: + + >>> torch.promote_types(torch.int32, torch.float32) + torch.float32 + >>> torch.promote_types(torch.uint8, torch.long) + torch.long + """ + +def put( + input: Tensor, + index: Tensor, + source: Tensor, + accumulate: _bool = False, +) -> Tensor: ... +def q_per_channel_axis(input: Tensor) -> _int: ... +def q_per_channel_scales(input: Tensor) -> Tensor: ... +def q_per_channel_zero_points(input: Tensor) -> Tensor: ... +def q_scale(input: Tensor) -> _float: ... +def q_zero_point(input: Tensor) -> _int: ... +def qr( + input: Tensor, + some: _bool = True, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.qr: + r""" + qr(input: Tensor, some: bool = True, *, out: Union[Tensor, Tuple[Tensor, ...], List[Tensor], None]) -> (Tensor, Tensor) + + Computes the QR decomposition of a matrix or a batch of matrices :attr:`input`, + and returns a namedtuple (Q, R) of tensors such that :math:`\text{input} = Q R` + with :math:`Q` being an orthogonal matrix or batch of orthogonal matrices and + :math:`R` being an upper triangular matrix or batch of upper triangular matrices. + + If :attr:`some` is ``True``, then this function returns the thin (reduced) QR factorization. + Otherwise, if :attr:`some` is ``False``, this function returns the complete QR factorization. + + .. warning:: + + :func:`torch.qr` is deprecated in favor of :func:`torch.linalg.qr` + and will be removed in a future PyTorch release. The boolean parameter :attr:`some` has been + replaced with a string parameter :attr:`mode`. + + ``Q, R = torch.qr(A)`` should be replaced with + + .. code:: python + + Q, R = torch.linalg.qr(A) + + ``Q, R = torch.qr(A, some=False)`` should be replaced with + + .. code:: python + + Q, R = torch.linalg.qr(A, mode="complete") + + .. warning:: + If you plan to backpropagate through QR, note that the current backward implementation + is only well-defined when the first :math:`\min(input.size(-1), input.size(-2))` + columns of :attr:`input` are linearly independent. + This behavior will probably change once QR supports pivoting. + + .. note:: This function uses LAPACK for CPU inputs and MAGMA for CUDA inputs, + and may produce different (valid) decompositions on different device types + or different platforms. + + Args: + input (Tensor): the input tensor of size :math:`(*, m, n)` where `*` is zero or more + batch dimensions consisting of matrices of dimension :math:`m \times n`. + some (bool, optional): Set to ``True`` for reduced QR decomposition and ``False`` for + complete QR decomposition. If `k = min(m, n)` then: + + * ``some=True`` : returns `(Q, R)` with dimensions (m, k), (k, n) (default) + + * ``'some=False'``: returns `(Q, R)` with dimensions (m, m), (m, n) + + Keyword args: + out (tuple, optional): tuple of `Q` and `R` tensors. + The dimensions of `Q` and `R` are detailed in the description of :attr:`some` above. + + Example:: + + >>> a = torch.tensor([[12., -51, 4], [6, 167, -68], [-4, 24, -41]]) + >>> q, r = torch.qr(a) + >>> q + tensor([[-0.8571, 0.3943, 0.3314], + [-0.4286, -0.9029, -0.0343], + [ 0.2857, -0.1714, 0.9429]]) + >>> r + tensor([[ -14.0000, -21.0000, 14.0000], + [ 0.0000, -175.0000, 70.0000], + [ 0.0000, 0.0000, -35.0000]]) + >>> torch.mm(q, r).round() + tensor([[ 12., -51., 4.], + [ 6., 167., -68.], + [ -4., 24., -41.]]) + >>> torch.mm(q.t(), q).round() + tensor([[ 1., 0., 0.], + [ 0., 1., -0.], + [ 0., -0., 1.]]) + >>> a = torch.randn(3, 4, 5) + >>> q, r = torch.qr(a, some=False) + >>> torch.allclose(torch.matmul(q, r), a) + True + >>> torch.allclose(torch.matmul(q.mT, q), torch.eye(5)) + True + """ + +@overload +def quantile( + input: Tensor, + q: Tensor, + dim: _int | None = None, + keepdim: _bool = False, + *, + interpolation: str = "linear", + out: Tensor | None = None, +) -> Tensor: + r""" + quantile(input, q, dim=None, keepdim=False, *, interpolation='linear', out=None) -> Tensor + + Computes the q-th quantiles of each row of the :attr:`input` tensor along the dimension :attr:`dim`. + + To compute the quantile, we map q in [0, 1] to the range of indices [0, n] to find the location + of the quantile in the sorted input. If the quantile lies between two data points ``a < b`` with + indices ``i`` and ``j`` in the sorted order, result is computed according to the given + :attr:`interpolation` method as follows: + + - ``linear``: ``a + (b - a) * fraction``, where ``fraction`` is the fractional part of the computed quantile index. + - ``lower``: ``a``. + - ``higher``: ``b``. + - ``nearest``: ``a`` or ``b``, whichever's index is closer to the computed quantile index (follows :func:`torch.round`). + - ``midpoint``: ``(a + b) / 2``. + + If :attr:`q` is a 1D tensor, the first dimension of the output represents the quantiles and has size + equal to the size of :attr:`q`, the remaining dimensions are what remains from the reduction. + + .. note:: + By default :attr:`dim` is ``None`` resulting in the :attr:`input` tensor being flattened before computation. + + Args: + input (Tensor): the input tensor. + q (float or Tensor): a scalar or 1D tensor of values in the range [0, 1]. + + dim (int, optional): the dimension to reduce. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword arguments: + interpolation (str, optional): interpolation method to use when the desired quantile lies between two data points. + Can be ``linear``, ``lower``, ``higher``, ``midpoint`` and ``nearest``. + Default is ``linear``. + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(2, 3) + >>> a + tensor([[ 0.0795, -1.2117, 0.9765], + [ 1.1707, 0.6706, 0.4884]]) + >>> q = torch.tensor([0.25, 0.5, 0.75]) + >>> torch.quantile(a, q, dim=1, keepdim=True) + tensor([[[-0.5661], + [ 0.5795]], + + [[ 0.0795], + [ 0.6706]], + + [[ 0.5280], + [ 0.9206]]]) + >>> torch.quantile(a, q, dim=1, keepdim=True).shape + torch.Size([3, 2, 1]) + >>> a = torch.arange(4.) + >>> a + tensor([0., 1., 2., 3.]) + >>> torch.quantile(a, 0.6, interpolation='linear') + tensor(1.8000) + >>> torch.quantile(a, 0.6, interpolation='lower') + tensor(1.) + >>> torch.quantile(a, 0.6, interpolation='higher') + tensor(2.) + >>> torch.quantile(a, 0.6, interpolation='midpoint') + tensor(1.5000) + >>> torch.quantile(a, 0.6, interpolation='nearest') + tensor(2.) + >>> torch.quantile(a, 0.4, interpolation='nearest') + tensor(1.) + """ + +@overload +def quantile( + input: Tensor, + q: _float, + dim: _int | None = None, + keepdim: _bool = False, + *, + interpolation: str = "linear", + out: Tensor | None = None, +) -> Tensor: + r""" + quantile(input, q, dim=None, keepdim=False, *, interpolation='linear', out=None) -> Tensor + + Computes the q-th quantiles of each row of the :attr:`input` tensor along the dimension :attr:`dim`. + + To compute the quantile, we map q in [0, 1] to the range of indices [0, n] to find the location + of the quantile in the sorted input. If the quantile lies between two data points ``a < b`` with + indices ``i`` and ``j`` in the sorted order, result is computed according to the given + :attr:`interpolation` method as follows: + + - ``linear``: ``a + (b - a) * fraction``, where ``fraction`` is the fractional part of the computed quantile index. + - ``lower``: ``a``. + - ``higher``: ``b``. + - ``nearest``: ``a`` or ``b``, whichever's index is closer to the computed quantile index (follows :func:`torch.round`). + - ``midpoint``: ``(a + b) / 2``. + + If :attr:`q` is a 1D tensor, the first dimension of the output represents the quantiles and has size + equal to the size of :attr:`q`, the remaining dimensions are what remains from the reduction. + + .. note:: + By default :attr:`dim` is ``None`` resulting in the :attr:`input` tensor being flattened before computation. + + Args: + input (Tensor): the input tensor. + q (float or Tensor): a scalar or 1D tensor of values in the range [0, 1]. + + dim (int, optional): the dimension to reduce. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword arguments: + interpolation (str, optional): interpolation method to use when the desired quantile lies between two data points. + Can be ``linear``, ``lower``, ``higher``, ``midpoint`` and ``nearest``. + Default is ``linear``. + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(2, 3) + >>> a + tensor([[ 0.0795, -1.2117, 0.9765], + [ 1.1707, 0.6706, 0.4884]]) + >>> q = torch.tensor([0.25, 0.5, 0.75]) + >>> torch.quantile(a, q, dim=1, keepdim=True) + tensor([[[-0.5661], + [ 0.5795]], + + [[ 0.0795], + [ 0.6706]], + + [[ 0.5280], + [ 0.9206]]]) + >>> torch.quantile(a, q, dim=1, keepdim=True).shape + torch.Size([3, 2, 1]) + >>> a = torch.arange(4.) + >>> a + tensor([0., 1., 2., 3.]) + >>> torch.quantile(a, 0.6, interpolation='linear') + tensor(1.8000) + >>> torch.quantile(a, 0.6, interpolation='lower') + tensor(1.) + >>> torch.quantile(a, 0.6, interpolation='higher') + tensor(2.) + >>> torch.quantile(a, 0.6, interpolation='midpoint') + tensor(1.5000) + >>> torch.quantile(a, 0.6, interpolation='nearest') + tensor(2.) + >>> torch.quantile(a, 0.4, interpolation='nearest') + tensor(1.) + """ + +def quantize_per_channel( + input: Tensor, + scales: Tensor, + zero_points: Tensor, + axis: _int, + dtype: _dtype, +) -> Tensor: + r""" + quantize_per_channel(input, scales, zero_points, axis, dtype) -> Tensor + + Converts a float tensor to a per-channel quantized tensor with given scales and zero points. + + Arguments: + input (Tensor): float tensor to quantize + scales (Tensor): float 1D tensor of scales to use, size should match ``input.size(axis)`` + zero_points (int): integer 1D tensor of offset to use, size should match ``input.size(axis)`` + axis (int): dimension on which apply per-channel quantization + dtype (:class:`torch.dtype`): the desired data type of returned tensor. + Has to be one of the quantized dtypes: ``torch.quint8``, ``torch.qint8``, ``torch.qint32`` + + Returns: + Tensor: A newly quantized tensor + + Example:: + + >>> x = torch.tensor([[-1.0, 0.0], [1.0, 2.0]]) + >>> torch.quantize_per_channel(x, torch.tensor([0.1, 0.01]), torch.tensor([10, 0]), 0, torch.quint8) + tensor([[-1., 0.], + [ 1., 2.]], size=(2, 2), dtype=torch.quint8, + quantization_scheme=torch.per_channel_affine, + scale=tensor([0.1000, 0.0100], dtype=torch.float64), + zero_point=tensor([10, 0]), axis=0) + >>> torch.quantize_per_channel(x, torch.tensor([0.1, 0.01]), torch.tensor([10, 0]), 0, torch.quint8).int_repr() + tensor([[ 0, 10], + [100, 200]], dtype=torch.uint8) + """ + +@overload +def quantize_per_tensor( + input: Tensor, + scale: Tensor, + zero_point: Tensor, + dtype: _dtype, +) -> Tensor: + r""" + quantize_per_tensor(input, scale, zero_point, dtype) -> Tensor + + Converts a float tensor to a quantized tensor with given scale and zero point. + + Arguments: + input (Tensor): float tensor or list of tensors to quantize + scale (float or Tensor): scale to apply in quantization formula + zero_point (int or Tensor): offset in integer value that maps to float zero + dtype (:class:`torch.dtype`): the desired data type of returned tensor. + Has to be one of the quantized dtypes: ``torch.quint8``, ``torch.qint8``, ``torch.qint32`` + + Returns: + Tensor: A newly quantized tensor or list of quantized tensors. + + Example:: + + >>> torch.quantize_per_tensor(torch.tensor([-1.0, 0.0, 1.0, 2.0]), 0.1, 10, torch.quint8) + tensor([-1., 0., 1., 2.], size=(4,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.1, zero_point=10) + >>> torch.quantize_per_tensor(torch.tensor([-1.0, 0.0, 1.0, 2.0]), 0.1, 10, torch.quint8).int_repr() + tensor([ 0, 10, 20, 30], dtype=torch.uint8) + >>> torch.quantize_per_tensor([torch.tensor([-1.0, 0.0]), torch.tensor([-2.0, 2.0])], + >>> torch.tensor([0.1, 0.2]), torch.tensor([10, 20]), torch.quint8) + (tensor([-1., 0.], size=(2,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.1, zero_point=10), + tensor([-2., 2.], size=(2,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.2, zero_point=20)) + >>> torch.quantize_per_tensor(torch.tensor([-1.0, 0.0, 1.0, 2.0]), torch.tensor(0.1), torch.tensor(10), torch.quint8) + tensor([-1., 0., 1., 2.], size=(4,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.10, zero_point=10) + """ + +@overload +def quantize_per_tensor( + input: Tensor, + scale: _float, + zero_point: _int, + dtype: _dtype, +) -> Tensor: + r""" + quantize_per_tensor(input, scale, zero_point, dtype) -> Tensor + + Converts a float tensor to a quantized tensor with given scale and zero point. + + Arguments: + input (Tensor): float tensor or list of tensors to quantize + scale (float or Tensor): scale to apply in quantization formula + zero_point (int or Tensor): offset in integer value that maps to float zero + dtype (:class:`torch.dtype`): the desired data type of returned tensor. + Has to be one of the quantized dtypes: ``torch.quint8``, ``torch.qint8``, ``torch.qint32`` + + Returns: + Tensor: A newly quantized tensor or list of quantized tensors. + + Example:: + + >>> torch.quantize_per_tensor(torch.tensor([-1.0, 0.0, 1.0, 2.0]), 0.1, 10, torch.quint8) + tensor([-1., 0., 1., 2.], size=(4,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.1, zero_point=10) + >>> torch.quantize_per_tensor(torch.tensor([-1.0, 0.0, 1.0, 2.0]), 0.1, 10, torch.quint8).int_repr() + tensor([ 0, 10, 20, 30], dtype=torch.uint8) + >>> torch.quantize_per_tensor([torch.tensor([-1.0, 0.0]), torch.tensor([-2.0, 2.0])], + >>> torch.tensor([0.1, 0.2]), torch.tensor([10, 20]), torch.quint8) + (tensor([-1., 0.], size=(2,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.1, zero_point=10), + tensor([-2., 2.], size=(2,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.2, zero_point=20)) + >>> torch.quantize_per_tensor(torch.tensor([-1.0, 0.0, 1.0, 2.0]), torch.tensor(0.1), torch.tensor(10), torch.quint8) + tensor([-1., 0., 1., 2.], size=(4,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.10, zero_point=10) + """ + +@overload +def quantize_per_tensor( + tensors: tuple[Tensor, ...] | list[Tensor], + scales: Tensor, + zero_points: Tensor, + dtype: _dtype, +) -> tuple[Tensor, ...]: + r""" + quantize_per_tensor(input, scale, zero_point, dtype) -> Tensor + + Converts a float tensor to a quantized tensor with given scale and zero point. + + Arguments: + input (Tensor): float tensor or list of tensors to quantize + scale (float or Tensor): scale to apply in quantization formula + zero_point (int or Tensor): offset in integer value that maps to float zero + dtype (:class:`torch.dtype`): the desired data type of returned tensor. + Has to be one of the quantized dtypes: ``torch.quint8``, ``torch.qint8``, ``torch.qint32`` + + Returns: + Tensor: A newly quantized tensor or list of quantized tensors. + + Example:: + + >>> torch.quantize_per_tensor(torch.tensor([-1.0, 0.0, 1.0, 2.0]), 0.1, 10, torch.quint8) + tensor([-1., 0., 1., 2.], size=(4,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.1, zero_point=10) + >>> torch.quantize_per_tensor(torch.tensor([-1.0, 0.0, 1.0, 2.0]), 0.1, 10, torch.quint8).int_repr() + tensor([ 0, 10, 20, 30], dtype=torch.uint8) + >>> torch.quantize_per_tensor([torch.tensor([-1.0, 0.0]), torch.tensor([-2.0, 2.0])], + >>> torch.tensor([0.1, 0.2]), torch.tensor([10, 20]), torch.quint8) + (tensor([-1., 0.], size=(2,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.1, zero_point=10), + tensor([-2., 2.], size=(2,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.2, zero_point=20)) + >>> torch.quantize_per_tensor(torch.tensor([-1.0, 0.0, 1.0, 2.0]), torch.tensor(0.1), torch.tensor(10), torch.quint8) + tensor([-1., 0., 1., 2.], size=(4,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.10, zero_point=10) + """ + +def quantize_per_tensor_dynamic( + input: Tensor, + dtype: _dtype, + reduce_range: _bool, +) -> Tensor: + r""" + quantize_per_tensor_dynamic(input, dtype, reduce_range) -> Tensor + + Converts a float tensor to a quantized tensor with scale and zero_point calculated + dynamically based on the input. + + Arguments: + input (Tensor): float tensor or list of tensors to quantize + dtype (:class:`torch.dtype`): the desired data type of returned tensor. + Has to be one of the quantized dtypes: ``torch.quint8``, ``torch.qint8`` + reduce_range (bool): a flag to indicate whether to reduce the range of quantized + data by 1 bit, it's required to avoid instruction overflow for some hardwares + + Returns: + Tensor: A newly (dynamically) quantized tensor + + Example:: + + >>> t = torch.quantize_per_tensor_dynamic(torch.tensor([-1.0, 0.0, 1.0, 2.0]), torch.quint8, False) + >>> print(t) + tensor([-1., 0., 1., 2.], size=(4,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.011764705882352941, + zero_point=85) + >>> t.int_repr() + tensor([ 0, 85, 170, 255], dtype=torch.uint8) + """ + +def quantized_batch_norm( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + mean: Tensor, + var: Tensor, + eps: _float, + output_scale: _float, + output_zero_point: _int, +) -> Tensor: + r""" + quantized_batch_norm(input, weight=None, bias=None, mean, var, eps, output_scale, output_zero_point) -> Tensor + + Applies batch normalization on a 4D (NCHW) quantized tensor. + + .. math:: + + y = \frac{x - \mathrm{E}[x]}{\sqrt{\mathrm{Var}[x] + \epsilon}} * \gamma + \beta + + Arguments: + input (Tensor): quantized tensor + weight (Tensor): float tensor that corresponds to the gamma, size C + bias (Tensor): float tensor that corresponds to the beta, size C + mean (Tensor): float mean value in batch normalization, size C + var (Tensor): float tensor for variance, size C + eps (float): a value added to the denominator for numerical stability. + output_scale (float): output quantized tensor scale + output_zero_point (int): output quantized tensor zero_point + + Returns: + Tensor: A quantized tensor with batch normalization applied. + + Example:: + + >>> qx = torch.quantize_per_tensor(torch.rand(2, 2, 2, 2), 1.5, 3, torch.quint8) + >>> torch.quantized_batch_norm(qx, torch.ones(2), torch.zeros(2), torch.rand(2), torch.rand(2), 0.00001, 0.2, 2) + tensor([[[[-0.2000, -0.2000], + [ 1.6000, -0.2000]], + + [[-0.4000, -0.4000], + [-0.4000, 0.6000]]], + + + [[[-0.2000, -0.2000], + [-0.2000, -0.2000]], + + [[ 0.6000, -0.4000], + [ 0.6000, -0.4000]]]], size=(2, 2, 2, 2), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.2, zero_point=2) + """ + +def quantized_gru_cell( + input: Tensor, + hx: Tensor, + w_ih: Tensor, + w_hh: Tensor, + b_ih: Tensor, + b_hh: Tensor, + packed_ih: Tensor, + packed_hh: Tensor, + col_offsets_ih: Tensor, + col_offsets_hh: Tensor, + scale_ih: Number | _complex, + scale_hh: Number | _complex, + zero_point_ih: Number | _complex, + zero_point_hh: Number | _complex, +) -> Tensor: ... +def quantized_lstm_cell( + input: Tensor, + hx: tuple[Tensor, ...] | list[Tensor], + w_ih: Tensor, + w_hh: Tensor, + b_ih: Tensor, + b_hh: Tensor, + packed_ih: Tensor, + packed_hh: Tensor, + col_offsets_ih: Tensor, + col_offsets_hh: Tensor, + scale_ih: Number | _complex, + scale_hh: Number | _complex, + zero_point_ih: Number | _complex, + zero_point_hh: Number | _complex, +) -> tuple[Tensor, Tensor]: ... +def quantized_max_pool1d( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size = (), + padding: _int | _size = 0, + dilation: _int | _size = 1, + ceil_mode: _bool = False, +) -> Tensor: + r""" + quantized_max_pool1d(input, kernel_size, stride=[], padding=0, dilation=1, ceil_mode=False) -> Tensor + + Applies a 1D max pooling over an input quantized tensor composed of several input planes. + + Arguments: + input (Tensor): quantized tensor + kernel_size (list of int): the size of the sliding window + stride (``list of int``, optional): the stride of the sliding window + padding (``list of int``, optional): padding to be added on both sides, must be >= 0 and <= kernel_size / 2 + dilation (``list of int``, optional): The stride between elements within a sliding window, must be > 0. Default 1 + ceil_mode (bool, optional): If True, will use ceil instead of floor to compute the output shape. + Defaults to False. + + + Returns: + Tensor: A quantized tensor with max_pool1d applied. + + Example:: + + >>> qx = torch.quantize_per_tensor(torch.rand(2, 2), 1.5, 3, torch.quint8) + >>> torch.quantized_max_pool1d(qx, [2]) + tensor([[0.0000], + [1.5000]], size=(2, 1), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=1.5, zero_point=3) + """ + +def quantized_max_pool2d( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size = (), + padding: _int | _size = 0, + dilation: _int | _size = 1, + ceil_mode: _bool = False, +) -> Tensor: + r""" + quantized_max_pool2d(input, kernel_size, stride=[], padding=0, dilation=1, ceil_mode=False) -> Tensor + + Applies a 2D max pooling over an input quantized tensor composed of several input planes. + + Arguments: + input (Tensor): quantized tensor + kernel_size (``list of int``): the size of the sliding window + stride (``list of int``, optional): the stride of the sliding window + padding (``list of int``, optional): padding to be added on both sides, must be >= 0 and <= kernel_size / 2 + dilation (``list of int``, optional): The stride between elements within a sliding window, must be > 0. Default 1 + ceil_mode (bool, optional): If True, will use ceil instead of floor to compute the output shape. + Defaults to False. + + + Returns: + Tensor: A quantized tensor with max_pool2d applied. + + Example:: + + >>> qx = torch.quantize_per_tensor(torch.rand(2, 2, 2, 2), 1.5, 3, torch.quint8) + >>> torch.quantized_max_pool2d(qx, [2,2]) + tensor([[[[1.5000]], + + [[1.5000]]], + + + [[[0.0000]], + + [[0.0000]]]], size=(2, 2, 1, 1), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=1.5, zero_point=3) + """ + +def quantized_max_pool3d( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size = (), + padding: _int | _size = 0, + dilation: _int | _size = 1, + ceil_mode: _bool = False, +) -> Tensor: ... +def quantized_rnn_relu_cell( + input: Tensor, + hx: Tensor, + w_ih: Tensor, + w_hh: Tensor, + b_ih: Tensor, + b_hh: Tensor, + packed_ih: Tensor, + packed_hh: Tensor, + col_offsets_ih: Tensor, + col_offsets_hh: Tensor, + scale_ih: Number | _complex, + scale_hh: Number | _complex, + zero_point_ih: Number | _complex, + zero_point_hh: Number | _complex, +) -> Tensor: ... +def quantized_rnn_tanh_cell( + input: Tensor, + hx: Tensor, + w_ih: Tensor, + w_hh: Tensor, + b_ih: Tensor, + b_hh: Tensor, + packed_ih: Tensor, + packed_hh: Tensor, + col_offsets_ih: Tensor, + col_offsets_hh: Tensor, + scale_ih: Number | _complex, + scale_hh: Number | _complex, + zero_point_ih: Number | _complex, + zero_point_hh: Number | _complex, +) -> Tensor: ... +def rad2deg(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + rad2deg(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Returns a new tensor with each of the elements of :attr:`input` + converted from angles in radians to degrees. + + Args: + input (Tensor): the input tensor. + + Keyword arguments: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([[3.142, -3.142], [6.283, -6.283], [1.570, -1.570]]) + >>> torch.rad2deg(a) + tensor([[ 180.0233, -180.0233], + [ 359.9894, -359.9894], + [ 89.9544, -89.9544]]) + """ + +def rad2deg_(input: Tensor) -> Tensor: ... +@overload +def rand( + size: Sequence[_int | SymInt], + *, + generator: Generator | None, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + rand(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Returns a tensor filled with random numbers from a uniform distribution + on the interval :math:`[0, 1)` + + The shape of the tensor is defined by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.rand(4) + tensor([ 0.5204, 0.2503, 0.3525, 0.5673]) + >>> torch.rand(2, 3) + tensor([[ 0.8237, 0.5781, 0.6879], + [ 0.3816, 0.7249, 0.0998]]) + """ + +@overload +def rand( + *size: _int | SymInt, + generator: Generator | None, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + rand(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Returns a tensor filled with random numbers from a uniform distribution + on the interval :math:`[0, 1)` + + The shape of the tensor is defined by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.rand(4) + tensor([ 0.5204, 0.2503, 0.3525, 0.5673]) + >>> torch.rand(2, 3) + tensor([[ 0.8237, 0.5781, 0.6879], + [ 0.3816, 0.7249, 0.0998]]) + """ + +@overload +def rand( + size: Sequence[_int | SymInt], + *, + generator: Generator | None, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + rand(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Returns a tensor filled with random numbers from a uniform distribution + on the interval :math:`[0, 1)` + + The shape of the tensor is defined by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.rand(4) + tensor([ 0.5204, 0.2503, 0.3525, 0.5673]) + >>> torch.rand(2, 3) + tensor([[ 0.8237, 0.5781, 0.6879], + [ 0.3816, 0.7249, 0.0998]]) + """ + +@overload +def rand( + *size: _int | SymInt, + generator: Generator | None, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + rand(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Returns a tensor filled with random numbers from a uniform distribution + on the interval :math:`[0, 1)` + + The shape of the tensor is defined by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.rand(4) + tensor([ 0.5204, 0.2503, 0.3525, 0.5673]) + >>> torch.rand(2, 3) + tensor([[ 0.8237, 0.5781, 0.6879], + [ 0.3816, 0.7249, 0.0998]]) + """ + +@overload +def rand( + size: Sequence[_int | SymInt], + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + rand(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Returns a tensor filled with random numbers from a uniform distribution + on the interval :math:`[0, 1)` + + The shape of the tensor is defined by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.rand(4) + tensor([ 0.5204, 0.2503, 0.3525, 0.5673]) + >>> torch.rand(2, 3) + tensor([[ 0.8237, 0.5781, 0.6879], + [ 0.3816, 0.7249, 0.0998]]) + """ + +@overload +def rand( + *size: _int | SymInt, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + rand(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Returns a tensor filled with random numbers from a uniform distribution + on the interval :math:`[0, 1)` + + The shape of the tensor is defined by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.rand(4) + tensor([ 0.5204, 0.2503, 0.3525, 0.5673]) + >>> torch.rand(2, 3) + tensor([[ 0.8237, 0.5781, 0.6879], + [ 0.3816, 0.7249, 0.0998]]) + """ + +@overload +def rand( + size: Sequence[_int | SymInt], + *, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + rand(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Returns a tensor filled with random numbers from a uniform distribution + on the interval :math:`[0, 1)` + + The shape of the tensor is defined by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.rand(4) + tensor([ 0.5204, 0.2503, 0.3525, 0.5673]) + >>> torch.rand(2, 3) + tensor([[ 0.8237, 0.5781, 0.6879], + [ 0.3816, 0.7249, 0.0998]]) + """ + +@overload +def rand( + *size: _int | SymInt, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + rand(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Returns a tensor filled with random numbers from a uniform distribution + on the interval :math:`[0, 1)` + + The shape of the tensor is defined by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.rand(4) + tensor([ 0.5204, 0.2503, 0.3525, 0.5673]) + >>> torch.rand(2, 3) + tensor([[ 0.8237, 0.5781, 0.6879], + [ 0.3816, 0.7249, 0.0998]]) + """ + +@overload +def rand_like( + input: Tensor, + *, + generator: Generator | None, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + rand_like(input, *, generator=None, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same size as :attr:`input` that is filled with + random numbers from a uniform distribution on the interval :math:`[0, 1)`. + ``torch.rand_like(input)`` is equivalent to + ``torch.rand(input.size(), dtype=input.dtype, layout=input.layout, device=input.device)``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling. + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + +@overload +def rand_like( + input: Tensor, + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + rand_like(input, *, generator=None, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same size as :attr:`input` that is filled with + random numbers from a uniform distribution on the interval :math:`[0, 1)`. + ``torch.rand_like(input)`` is equivalent to + ``torch.rand(input.size(), dtype=input.dtype, layout=input.layout, device=input.device)``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling. + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + +@overload +def randint( + low: _int, + high: _int, + size: _size, + *, + generator: Generator | None = None, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + randint(low=0, high, size, \*, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with random integers generated uniformly + between :attr:`low` (inclusive) and :attr:`high` (exclusive). + + The shape of the tensor is defined by the variable argument :attr:`size`. + + .. note:: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + size (tuple): a tuple defining the shape of the output tensor. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the desired data type of returned tensor. Default: if ``None``, + this function returns a tensor with dtype ``torch.int64``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.randint(3, 5, (3,)) + tensor([4, 3, 4]) + + + >>> torch.randint(10, (2, 2)) + tensor([[0, 2], + [5, 5]]) + + + >>> torch.randint(3, 10, (2, 2)) + tensor([[4, 5], + [6, 7]]) + """ + +@overload +def randint( + high: _int, + size: _size, + *, + generator: Generator | None = None, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + randint(low=0, high, size, \*, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with random integers generated uniformly + between :attr:`low` (inclusive) and :attr:`high` (exclusive). + + The shape of the tensor is defined by the variable argument :attr:`size`. + + .. note:: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + size (tuple): a tuple defining the shape of the output tensor. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the desired data type of returned tensor. Default: if ``None``, + this function returns a tensor with dtype ``torch.int64``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.randint(3, 5, (3,)) + tensor([4, 3, 4]) + + + >>> torch.randint(10, (2, 2)) + tensor([[0, 2], + [5, 5]]) + + + >>> torch.randint(3, 10, (2, 2)) + tensor([[4, 5], + [6, 7]]) + """ + +@overload +def randint( + high: _int | SymInt, + size: Sequence[_int | SymInt], + *, + generator: Generator | None, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randint(low=0, high, size, \*, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with random integers generated uniformly + between :attr:`low` (inclusive) and :attr:`high` (exclusive). + + The shape of the tensor is defined by the variable argument :attr:`size`. + + .. note:: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + size (tuple): a tuple defining the shape of the output tensor. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the desired data type of returned tensor. Default: if ``None``, + this function returns a tensor with dtype ``torch.int64``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.randint(3, 5, (3,)) + tensor([4, 3, 4]) + + + >>> torch.randint(10, (2, 2)) + tensor([[0, 2], + [5, 5]]) + + + >>> torch.randint(3, 10, (2, 2)) + tensor([[4, 5], + [6, 7]]) + """ + +@overload +def randint( + high: _int | SymInt, + size: Sequence[_int | SymInt], + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randint(low=0, high, size, \*, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with random integers generated uniformly + between :attr:`low` (inclusive) and :attr:`high` (exclusive). + + The shape of the tensor is defined by the variable argument :attr:`size`. + + .. note:: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + size (tuple): a tuple defining the shape of the output tensor. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the desired data type of returned tensor. Default: if ``None``, + this function returns a tensor with dtype ``torch.int64``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.randint(3, 5, (3,)) + tensor([4, 3, 4]) + + + >>> torch.randint(10, (2, 2)) + tensor([[0, 2], + [5, 5]]) + + + >>> torch.randint(3, 10, (2, 2)) + tensor([[4, 5], + [6, 7]]) + """ + +@overload +def randint( + low: _int | SymInt, + high: _int | SymInt, + size: Sequence[_int | SymInt], + *, + generator: Generator | None, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randint(low=0, high, size, \*, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with random integers generated uniformly + between :attr:`low` (inclusive) and :attr:`high` (exclusive). + + The shape of the tensor is defined by the variable argument :attr:`size`. + + .. note:: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + size (tuple): a tuple defining the shape of the output tensor. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the desired data type of returned tensor. Default: if ``None``, + this function returns a tensor with dtype ``torch.int64``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.randint(3, 5, (3,)) + tensor([4, 3, 4]) + + + >>> torch.randint(10, (2, 2)) + tensor([[0, 2], + [5, 5]]) + + + >>> torch.randint(3, 10, (2, 2)) + tensor([[4, 5], + [6, 7]]) + """ + +@overload +def randint( + low: _int | SymInt, + high: _int | SymInt, + size: Sequence[_int | SymInt], + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randint(low=0, high, size, \*, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with random integers generated uniformly + between :attr:`low` (inclusive) and :attr:`high` (exclusive). + + The shape of the tensor is defined by the variable argument :attr:`size`. + + .. note:: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + size (tuple): a tuple defining the shape of the output tensor. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the desired data type of returned tensor. Default: if ``None``, + this function returns a tensor with dtype ``torch.int64``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.randint(3, 5, (3,)) + tensor([4, 3, 4]) + + + >>> torch.randint(10, (2, 2)) + tensor([[0, 2], + [5, 5]]) + + + >>> torch.randint(3, 10, (2, 2)) + tensor([[4, 5], + [6, 7]]) + """ + +@overload +def randint_like( + input: Tensor, + low: _int | SymInt, + high: _int | SymInt, + *, + generator: Generator | None, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randint_like(input, low=0, high, \*, generator=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same shape as Tensor :attr:`input` filled with + random integers generated uniformly between :attr:`low` (inclusive) and + :attr:`high` (exclusive). + + .. note: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling. + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + +@overload +def randint_like( + input: Tensor, + low: _int | SymInt, + high: _int | SymInt, + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randint_like(input, low=0, high, \*, generator=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same shape as Tensor :attr:`input` filled with + random integers generated uniformly between :attr:`low` (inclusive) and + :attr:`high` (exclusive). + + .. note: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling. + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + +@overload +def randint_like( + input: Tensor, + high: Tensor, + *, + generator: Generator | None, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randint_like(input, low=0, high, \*, generator=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same shape as Tensor :attr:`input` filled with + random integers generated uniformly between :attr:`low` (inclusive) and + :attr:`high` (exclusive). + + .. note: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling. + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + +@overload +def randint_like( + input: Tensor, + high: Tensor, + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randint_like(input, low=0, high, \*, generator=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same shape as Tensor :attr:`input` filled with + random integers generated uniformly between :attr:`low` (inclusive) and + :attr:`high` (exclusive). + + .. note: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling. + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + +@overload +def randint_like( + input: Tensor, + high: _int | SymInt, + *, + generator: Generator | None, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randint_like(input, low=0, high, \*, generator=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same shape as Tensor :attr:`input` filled with + random integers generated uniformly between :attr:`low` (inclusive) and + :attr:`high` (exclusive). + + .. note: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling. + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + +@overload +def randint_like( + input: Tensor, + high: _int | SymInt, + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randint_like(input, low=0, high, \*, generator=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same shape as Tensor :attr:`input` filled with + random integers generated uniformly between :attr:`low` (inclusive) and + :attr:`high` (exclusive). + + .. note: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling. + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + +@overload +def randn( + size: Sequence[_int | SymInt], + *, + generator: Generator | None, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randn(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + + Returns a tensor filled with random numbers from a normal distribution + with mean `0` and variance `1` (also called the standard normal + distribution). + + .. math:: + \text{out}_{i} \sim \mathcal{N}(0, 1) + + For complex dtypes, the tensor is i.i.d. sampled from a `complex normal distribution`_ with zero mean and + unit variance as + + .. math:: + \text{out}_{i} \sim \mathcal{CN}(0, 1) + + This is equivalent to separately sampling the real :math:`(\operatorname{Re})` and imaginary + :math:`(\operatorname{Im})` part of :math:`\text{out}_i` as + + .. math:: + \operatorname{Re}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}),\quad + \operatorname{Im}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}) + + The shape of the tensor is defined by the variable argument :attr:`size`. + + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.randn(4) + tensor([-2.1436, 0.9966, 2.3426, -0.6366]) + >>> torch.randn(2, 3) + tensor([[ 1.5954, 2.8929, -1.0923], + [ 1.1719, -0.4709, -0.1996]]) + + .. _complex normal distribution: https://en.wikipedia.org/wiki/Complex_normal_distribution + """ + +@overload +def randn( + *size: _int | SymInt, + generator: Generator | None, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randn(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + + Returns a tensor filled with random numbers from a normal distribution + with mean `0` and variance `1` (also called the standard normal + distribution). + + .. math:: + \text{out}_{i} \sim \mathcal{N}(0, 1) + + For complex dtypes, the tensor is i.i.d. sampled from a `complex normal distribution`_ with zero mean and + unit variance as + + .. math:: + \text{out}_{i} \sim \mathcal{CN}(0, 1) + + This is equivalent to separately sampling the real :math:`(\operatorname{Re})` and imaginary + :math:`(\operatorname{Im})` part of :math:`\text{out}_i` as + + .. math:: + \operatorname{Re}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}),\quad + \operatorname{Im}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}) + + The shape of the tensor is defined by the variable argument :attr:`size`. + + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.randn(4) + tensor([-2.1436, 0.9966, 2.3426, -0.6366]) + >>> torch.randn(2, 3) + tensor([[ 1.5954, 2.8929, -1.0923], + [ 1.1719, -0.4709, -0.1996]]) + + .. _complex normal distribution: https://en.wikipedia.org/wiki/Complex_normal_distribution + """ + +@overload +def randn( + size: Sequence[_int | SymInt], + *, + generator: Generator | None, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randn(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + + Returns a tensor filled with random numbers from a normal distribution + with mean `0` and variance `1` (also called the standard normal + distribution). + + .. math:: + \text{out}_{i} \sim \mathcal{N}(0, 1) + + For complex dtypes, the tensor is i.i.d. sampled from a `complex normal distribution`_ with zero mean and + unit variance as + + .. math:: + \text{out}_{i} \sim \mathcal{CN}(0, 1) + + This is equivalent to separately sampling the real :math:`(\operatorname{Re})` and imaginary + :math:`(\operatorname{Im})` part of :math:`\text{out}_i` as + + .. math:: + \operatorname{Re}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}),\quad + \operatorname{Im}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}) + + The shape of the tensor is defined by the variable argument :attr:`size`. + + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.randn(4) + tensor([-2.1436, 0.9966, 2.3426, -0.6366]) + >>> torch.randn(2, 3) + tensor([[ 1.5954, 2.8929, -1.0923], + [ 1.1719, -0.4709, -0.1996]]) + + .. _complex normal distribution: https://en.wikipedia.org/wiki/Complex_normal_distribution + """ + +@overload +def randn( + *size: _int | SymInt, + generator: Generator | None, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randn(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + + Returns a tensor filled with random numbers from a normal distribution + with mean `0` and variance `1` (also called the standard normal + distribution). + + .. math:: + \text{out}_{i} \sim \mathcal{N}(0, 1) + + For complex dtypes, the tensor is i.i.d. sampled from a `complex normal distribution`_ with zero mean and + unit variance as + + .. math:: + \text{out}_{i} \sim \mathcal{CN}(0, 1) + + This is equivalent to separately sampling the real :math:`(\operatorname{Re})` and imaginary + :math:`(\operatorname{Im})` part of :math:`\text{out}_i` as + + .. math:: + \operatorname{Re}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}),\quad + \operatorname{Im}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}) + + The shape of the tensor is defined by the variable argument :attr:`size`. + + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.randn(4) + tensor([-2.1436, 0.9966, 2.3426, -0.6366]) + >>> torch.randn(2, 3) + tensor([[ 1.5954, 2.8929, -1.0923], + [ 1.1719, -0.4709, -0.1996]]) + + .. _complex normal distribution: https://en.wikipedia.org/wiki/Complex_normal_distribution + """ + +@overload +def randn( + size: Sequence[_int | SymInt], + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randn(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + + Returns a tensor filled with random numbers from a normal distribution + with mean `0` and variance `1` (also called the standard normal + distribution). + + .. math:: + \text{out}_{i} \sim \mathcal{N}(0, 1) + + For complex dtypes, the tensor is i.i.d. sampled from a `complex normal distribution`_ with zero mean and + unit variance as + + .. math:: + \text{out}_{i} \sim \mathcal{CN}(0, 1) + + This is equivalent to separately sampling the real :math:`(\operatorname{Re})` and imaginary + :math:`(\operatorname{Im})` part of :math:`\text{out}_i` as + + .. math:: + \operatorname{Re}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}),\quad + \operatorname{Im}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}) + + The shape of the tensor is defined by the variable argument :attr:`size`. + + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.randn(4) + tensor([-2.1436, 0.9966, 2.3426, -0.6366]) + >>> torch.randn(2, 3) + tensor([[ 1.5954, 2.8929, -1.0923], + [ 1.1719, -0.4709, -0.1996]]) + + .. _complex normal distribution: https://en.wikipedia.org/wiki/Complex_normal_distribution + """ + +@overload +def randn( + *size: _int | SymInt, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randn(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + + Returns a tensor filled with random numbers from a normal distribution + with mean `0` and variance `1` (also called the standard normal + distribution). + + .. math:: + \text{out}_{i} \sim \mathcal{N}(0, 1) + + For complex dtypes, the tensor is i.i.d. sampled from a `complex normal distribution`_ with zero mean and + unit variance as + + .. math:: + \text{out}_{i} \sim \mathcal{CN}(0, 1) + + This is equivalent to separately sampling the real :math:`(\operatorname{Re})` and imaginary + :math:`(\operatorname{Im})` part of :math:`\text{out}_i` as + + .. math:: + \operatorname{Re}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}),\quad + \operatorname{Im}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}) + + The shape of the tensor is defined by the variable argument :attr:`size`. + + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.randn(4) + tensor([-2.1436, 0.9966, 2.3426, -0.6366]) + >>> torch.randn(2, 3) + tensor([[ 1.5954, 2.8929, -1.0923], + [ 1.1719, -0.4709, -0.1996]]) + + .. _complex normal distribution: https://en.wikipedia.org/wiki/Complex_normal_distribution + """ + +@overload +def randn( + size: Sequence[_int | SymInt], + *, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randn(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + + Returns a tensor filled with random numbers from a normal distribution + with mean `0` and variance `1` (also called the standard normal + distribution). + + .. math:: + \text{out}_{i} \sim \mathcal{N}(0, 1) + + For complex dtypes, the tensor is i.i.d. sampled from a `complex normal distribution`_ with zero mean and + unit variance as + + .. math:: + \text{out}_{i} \sim \mathcal{CN}(0, 1) + + This is equivalent to separately sampling the real :math:`(\operatorname{Re})` and imaginary + :math:`(\operatorname{Im})` part of :math:`\text{out}_i` as + + .. math:: + \operatorname{Re}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}),\quad + \operatorname{Im}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}) + + The shape of the tensor is defined by the variable argument :attr:`size`. + + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.randn(4) + tensor([-2.1436, 0.9966, 2.3426, -0.6366]) + >>> torch.randn(2, 3) + tensor([[ 1.5954, 2.8929, -1.0923], + [ 1.1719, -0.4709, -0.1996]]) + + .. _complex normal distribution: https://en.wikipedia.org/wiki/Complex_normal_distribution + """ + +@overload +def randn( + *size: _int | SymInt, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randn(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + + Returns a tensor filled with random numbers from a normal distribution + with mean `0` and variance `1` (also called the standard normal + distribution). + + .. math:: + \text{out}_{i} \sim \mathcal{N}(0, 1) + + For complex dtypes, the tensor is i.i.d. sampled from a `complex normal distribution`_ with zero mean and + unit variance as + + .. math:: + \text{out}_{i} \sim \mathcal{CN}(0, 1) + + This is equivalent to separately sampling the real :math:`(\operatorname{Re})` and imaginary + :math:`(\operatorname{Im})` part of :math:`\text{out}_i` as + + .. math:: + \operatorname{Re}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}),\quad + \operatorname{Im}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}) + + The shape of the tensor is defined by the variable argument :attr:`size`. + + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.randn(4) + tensor([-2.1436, 0.9966, 2.3426, -0.6366]) + >>> torch.randn(2, 3) + tensor([[ 1.5954, 2.8929, -1.0923], + [ 1.1719, -0.4709, -0.1996]]) + + .. _complex normal distribution: https://en.wikipedia.org/wiki/Complex_normal_distribution + """ + +@overload +def randn_like( + input: Tensor, + *, + generator: Generator | None, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randn_like(input, *, generator=None, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same size as :attr:`input` that is filled with + random numbers from a normal distribution with mean 0 and variance 1. Please refer to :func:`torch.randn` for the + sampling process of complex dtypes. ``torch.randn_like(input)`` is equivalent to + ``torch.randn(input.size(), dtype=input.dtype, layout=input.layout, device=input.device)``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling. + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + +@overload +def randn_like( + input: Tensor, + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randn_like(input, *, generator=None, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same size as :attr:`input` that is filled with + random numbers from a normal distribution with mean 0 and variance 1. Please refer to :func:`torch.randn` for the + sampling process of complex dtypes. ``torch.randn_like(input)`` is equivalent to + ``torch.randn(input.size(), dtype=input.dtype, layout=input.layout, device=input.device)``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling. + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + +@overload +def randperm( + n: _int | SymInt, + *, + generator: Generator | None, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randperm(n, *, generator=None, out=None, dtype=torch.int64,layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Returns a random permutation of integers from ``0`` to ``n - 1``. + + Args: + n (int): the upper bound (exclusive) + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: ``torch.int64``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.randperm(4) + tensor([2, 1, 0, 3]) + """ + +@overload +def randperm( + n: _int | SymInt, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randperm(n, *, generator=None, out=None, dtype=torch.int64,layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Returns a random permutation of integers from ``0`` to ``n - 1``. + + Args: + n (int): the upper bound (exclusive) + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: ``torch.int64``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.randperm(4) + tensor([2, 1, 0, 3]) + """ + +def range( + start: Number, + end: Number, + step: Number = 1, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + range(start=0, end, step=1, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a 1-D tensor of size :math:`\left\lfloor \frac{\text{end} - \text{start}}{\text{step}} \right\rfloor + 1` + with values from :attr:`start` to :attr:`end` with step :attr:`step`. Step is + the gap between two values in the tensor. + + .. math:: + \text{out}_{i+1} = \text{out}_i + \text{step}. + + .. warning:: + This function is deprecated and will be removed in a future release because its behavior is inconsistent with + Python's range builtin. Instead, use :func:`torch.arange`, which produces values in [start, end). + + Args: + start (float, optional): the starting value for the set of points. Default: ``0``. + end (float): the ending value for the set of points + step (float, optional): the gap between each pair of adjacent points. Default: ``1``. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). If `dtype` is not given, infer the data type from the other input + arguments. If any of `start`, `end`, or `step` are floating-point, the + `dtype` is inferred to be the default dtype, see + :meth:`~torch.get_default_dtype`. Otherwise, the `dtype` is inferred to + be `torch.int64`. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.range(1, 4) + tensor([ 1., 2., 3., 4.]) + >>> torch.range(1, 4, 0.5) + tensor([ 1.0000, 1.5000, 2.0000, 2.5000, 3.0000, 3.5000, 4.0000]) + """ + +def ravel(input: Tensor) -> Tensor: + r""" + ravel(input) -> Tensor + + Return a contiguous flattened tensor. A copy is made only if needed. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> t = torch.tensor([[[1, 2], + ... [3, 4]], + ... [[5, 6], + ... [7, 8]]]) + >>> torch.ravel(t) + tensor([1, 2, 3, 4, 5, 6, 7, 8]) + """ + +def real(input: Tensor) -> Tensor: + r""" + real(input) -> Tensor + + Returns a new tensor containing real values of the :attr:`self` tensor. + The returned tensor and :attr:`self` share the same underlying storage. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> x=torch.randn(4, dtype=torch.cfloat) + >>> x + tensor([(0.3100+0.3553j), (-0.5445-0.7896j), (-1.6492-0.0633j), (-0.0638-0.8119j)]) + >>> x.real + tensor([ 0.3100, -0.5445, -1.6492, -0.0638]) + """ + +def reciprocal(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + reciprocal(input, *, out=None) -> Tensor + + Returns a new tensor with the reciprocal of the elements of :attr:`input` + + .. math:: + \text{out}_{i} = \frac{1}{\text{input}_{i}} + + .. note:: + Unlike NumPy's reciprocal, torch.reciprocal supports integral inputs. Integral + inputs to reciprocal are automatically :ref:`promoted ` to + the default scalar type. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-0.4595, -2.1219, -1.4314, 0.7298]) + >>> torch.reciprocal(a) + tensor([-2.1763, -0.4713, -0.6986, 1.3702]) + """ + +def reciprocal_(input: Tensor) -> Tensor: ... +def relu(input: Tensor) -> Tensor: ... +def relu_(input: Tensor) -> Tensor: ... +@overload +def remainder( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + remainder(input, other, *, out=None) -> Tensor + + Computes + `Python's modulus operation `_ + entrywise. The result has the same sign as the divisor :attr:`other` and its absolute value + is less than that of :attr:`other`. + + It may also be defined in terms of :func:`torch.div` as + + .. code:: python + + torch.remainder(a, b) == a - a.div(b, rounding_mode="floor") * b + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer and float inputs. + + .. note:: + Complex inputs are not supported. In some cases, it is not mathematically + possible to satisfy the definition of a modulo operation with complex numbers. + See :func:`torch.fmod` for how division by zero is handled. + + .. seealso:: + + :func:`torch.fmod` which implements C++'s `std::fmod `_. + This one is defined in terms of division rounding towards zero. + + Args: + input (Tensor or Scalar): the dividend + other (Tensor or Scalar): the divisor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.remainder(torch.tensor([-3., -2, -1, 1, 2, 3]), 2) + tensor([ 1., 0., 1., 1., 0., 1.]) + >>> torch.remainder(torch.tensor([1, 2, 3, 4, 5]), -1.5) + tensor([ -0.5000, -1.0000, 0.0000, -0.5000, -1.0000 ]) + """ + +@overload +def remainder(self: Number | _complex, other: Tensor) -> Tensor: + r""" + remainder(input, other, *, out=None) -> Tensor + + Computes + `Python's modulus operation `_ + entrywise. The result has the same sign as the divisor :attr:`other` and its absolute value + is less than that of :attr:`other`. + + It may also be defined in terms of :func:`torch.div` as + + .. code:: python + + torch.remainder(a, b) == a - a.div(b, rounding_mode="floor") * b + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer and float inputs. + + .. note:: + Complex inputs are not supported. In some cases, it is not mathematically + possible to satisfy the definition of a modulo operation with complex numbers. + See :func:`torch.fmod` for how division by zero is handled. + + .. seealso:: + + :func:`torch.fmod` which implements C++'s `std::fmod `_. + This one is defined in terms of division rounding towards zero. + + Args: + input (Tensor or Scalar): the dividend + other (Tensor or Scalar): the divisor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.remainder(torch.tensor([-3., -2, -1, 1, 2, 3]), 2) + tensor([ 1., 0., 1., 1., 0., 1.]) + >>> torch.remainder(torch.tensor([1, 2, 3, 4, 5]), -1.5) + tensor([ -0.5000, -1.0000, 0.0000, -0.5000, -1.0000 ]) + """ + +@overload +def remainder( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + remainder(input, other, *, out=None) -> Tensor + + Computes + `Python's modulus operation `_ + entrywise. The result has the same sign as the divisor :attr:`other` and its absolute value + is less than that of :attr:`other`. + + It may also be defined in terms of :func:`torch.div` as + + .. code:: python + + torch.remainder(a, b) == a - a.div(b, rounding_mode="floor") * b + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer and float inputs. + + .. note:: + Complex inputs are not supported. In some cases, it is not mathematically + possible to satisfy the definition of a modulo operation with complex numbers. + See :func:`torch.fmod` for how division by zero is handled. + + .. seealso:: + + :func:`torch.fmod` which implements C++'s `std::fmod `_. + This one is defined in terms of division rounding towards zero. + + Args: + input (Tensor or Scalar): the dividend + other (Tensor or Scalar): the divisor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.remainder(torch.tensor([-3., -2, -1, 1, 2, 3]), 2) + tensor([ 1., 0., 1., 1., 0., 1.]) + >>> torch.remainder(torch.tensor([1, 2, 3, 4, 5]), -1.5) + tensor([ -0.5000, -1.0000, 0.0000, -0.5000, -1.0000 ]) + """ + +def renorm( + input: Tensor, + p: Number | _complex, + dim: _int, + maxnorm: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + renorm(input, p, dim, maxnorm, *, out=None) -> Tensor + + Returns a tensor where each sub-tensor of :attr:`input` along dimension + :attr:`dim` is normalized such that the `p`-norm of the sub-tensor is lower + than the value :attr:`maxnorm` + + .. note:: If the norm of a row is lower than `maxnorm`, the row is unchanged + + Args: + input (Tensor): the input tensor. + p (float): the power for the norm computation + dim (int): the dimension to slice over to get the sub-tensors + maxnorm (float): the maximum norm to keep each sub-tensor under + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> x = torch.ones(3, 3) + >>> x[1].fill_(2) + tensor([ 2., 2., 2.]) + >>> x[2].fill_(3) + tensor([ 3., 3., 3.]) + >>> x + tensor([[ 1., 1., 1.], + [ 2., 2., 2.], + [ 3., 3., 3.]]) + >>> torch.renorm(x, 1, 0, 5) + tensor([[ 1.0000, 1.0000, 1.0000], + [ 1.6667, 1.6667, 1.6667], + [ 1.6667, 1.6667, 1.6667]]) + """ + +@overload +def repeat_interleave( + input: Tensor, + repeats: Tensor, + dim: _int | None = None, + *, + output_size: _int | SymInt | None = None, +) -> Tensor: + r""" + repeat_interleave(input, repeats, dim=None, *, output_size=None) -> Tensor + + Repeat elements of a tensor. + + .. warning:: + + This is different from :meth:`torch.Tensor.repeat` but similar to ``numpy.repeat``. + + Args: + input (Tensor): the input tensor. + repeats (Tensor or int): The number of repetitions for each element. + repeats is broadcasted to fit the shape of the given axis. + dim (int, optional): The dimension along which to repeat values. + By default, use the flattened input array, and return a flat output + array. + + Keyword args: + output_size (int, optional): Total output size for the given axis + ( e.g. sum of repeats). If given, it will avoid stream synchronization + needed to calculate output shape of the tensor. + + Returns: + Tensor: Repeated tensor which has the same shape as input, except along the given axis. + + Example:: + + >>> x = torch.tensor([1, 2, 3]) + >>> x.repeat_interleave(2) + tensor([1, 1, 2, 2, 3, 3]) + >>> y = torch.tensor([[1, 2], [3, 4]]) + >>> torch.repeat_interleave(y, 2) + tensor([1, 1, 2, 2, 3, 3, 4, 4]) + >>> torch.repeat_interleave(y, 3, dim=1) + tensor([[1, 1, 1, 2, 2, 2], + [3, 3, 3, 4, 4, 4]]) + >>> torch.repeat_interleave(y, torch.tensor([1, 2]), dim=0) + tensor([[1, 2], + [3, 4], + [3, 4]]) + >>> torch.repeat_interleave(y, torch.tensor([1, 2]), dim=0, output_size=3) + tensor([[1, 2], + [3, 4], + [3, 4]]) + + If the `repeats` is `tensor([n1, n2, n3, ...])`, then the output will be + `tensor([0, 0, ..., 1, 1, ..., 2, 2, ..., ...])` where `0` appears `n1` times, + `1` appears `n2` times, `2` appears `n3` times, etc. + + .. function:: repeat_interleave(repeats, *) -> Tensor + :noindex: + + Repeats 0 repeats[0] times, 1 repeats[1] times, 2 repeats[2] times, etc. + + Args: + repeats (Tensor): The number of repetitions for each element. + + Returns: + Tensor: Repeated tensor of size `sum(repeats)`. + + Example:: + + >>> torch.repeat_interleave(torch.tensor([1, 2, 3])) + tensor([0, 1, 1, 2, 2, 2]) + """ + +@overload +def repeat_interleave( + repeats: Tensor, + *, + output_size: _int | SymInt | None = None, +) -> Tensor: + r""" + repeat_interleave(input, repeats, dim=None, *, output_size=None) -> Tensor + + Repeat elements of a tensor. + + .. warning:: + + This is different from :meth:`torch.Tensor.repeat` but similar to ``numpy.repeat``. + + Args: + input (Tensor): the input tensor. + repeats (Tensor or int): The number of repetitions for each element. + repeats is broadcasted to fit the shape of the given axis. + dim (int, optional): The dimension along which to repeat values. + By default, use the flattened input array, and return a flat output + array. + + Keyword args: + output_size (int, optional): Total output size for the given axis + ( e.g. sum of repeats). If given, it will avoid stream synchronization + needed to calculate output shape of the tensor. + + Returns: + Tensor: Repeated tensor which has the same shape as input, except along the given axis. + + Example:: + + >>> x = torch.tensor([1, 2, 3]) + >>> x.repeat_interleave(2) + tensor([1, 1, 2, 2, 3, 3]) + >>> y = torch.tensor([[1, 2], [3, 4]]) + >>> torch.repeat_interleave(y, 2) + tensor([1, 1, 2, 2, 3, 3, 4, 4]) + >>> torch.repeat_interleave(y, 3, dim=1) + tensor([[1, 1, 1, 2, 2, 2], + [3, 3, 3, 4, 4, 4]]) + >>> torch.repeat_interleave(y, torch.tensor([1, 2]), dim=0) + tensor([[1, 2], + [3, 4], + [3, 4]]) + >>> torch.repeat_interleave(y, torch.tensor([1, 2]), dim=0, output_size=3) + tensor([[1, 2], + [3, 4], + [3, 4]]) + + If the `repeats` is `tensor([n1, n2, n3, ...])`, then the output will be + `tensor([0, 0, ..., 1, 1, ..., 2, 2, ..., ...])` where `0` appears `n1` times, + `1` appears `n2` times, `2` appears `n3` times, etc. + + .. function:: repeat_interleave(repeats, *) -> Tensor + :noindex: + + Repeats 0 repeats[0] times, 1 repeats[1] times, 2 repeats[2] times, etc. + + Args: + repeats (Tensor): The number of repetitions for each element. + + Returns: + Tensor: Repeated tensor of size `sum(repeats)`. + + Example:: + + >>> torch.repeat_interleave(torch.tensor([1, 2, 3])) + tensor([0, 1, 1, 2, 2, 2]) + """ + +@overload +def repeat_interleave( + input: Tensor, + repeats: _int | SymInt, + dim: _int | None = None, + *, + output_size: _int | SymInt | None = None, +) -> Tensor: + r""" + repeat_interleave(input, repeats, dim=None, *, output_size=None) -> Tensor + + Repeat elements of a tensor. + + .. warning:: + + This is different from :meth:`torch.Tensor.repeat` but similar to ``numpy.repeat``. + + Args: + input (Tensor): the input tensor. + repeats (Tensor or int): The number of repetitions for each element. + repeats is broadcasted to fit the shape of the given axis. + dim (int, optional): The dimension along which to repeat values. + By default, use the flattened input array, and return a flat output + array. + + Keyword args: + output_size (int, optional): Total output size for the given axis + ( e.g. sum of repeats). If given, it will avoid stream synchronization + needed to calculate output shape of the tensor. + + Returns: + Tensor: Repeated tensor which has the same shape as input, except along the given axis. + + Example:: + + >>> x = torch.tensor([1, 2, 3]) + >>> x.repeat_interleave(2) + tensor([1, 1, 2, 2, 3, 3]) + >>> y = torch.tensor([[1, 2], [3, 4]]) + >>> torch.repeat_interleave(y, 2) + tensor([1, 1, 2, 2, 3, 3, 4, 4]) + >>> torch.repeat_interleave(y, 3, dim=1) + tensor([[1, 1, 1, 2, 2, 2], + [3, 3, 3, 4, 4, 4]]) + >>> torch.repeat_interleave(y, torch.tensor([1, 2]), dim=0) + tensor([[1, 2], + [3, 4], + [3, 4]]) + >>> torch.repeat_interleave(y, torch.tensor([1, 2]), dim=0, output_size=3) + tensor([[1, 2], + [3, 4], + [3, 4]]) + + If the `repeats` is `tensor([n1, n2, n3, ...])`, then the output will be + `tensor([0, 0, ..., 1, 1, ..., 2, 2, ..., ...])` where `0` appears `n1` times, + `1` appears `n2` times, `2` appears `n3` times, etc. + + .. function:: repeat_interleave(repeats, *) -> Tensor + :noindex: + + Repeats 0 repeats[0] times, 1 repeats[1] times, 2 repeats[2] times, etc. + + Args: + repeats (Tensor): The number of repetitions for each element. + + Returns: + Tensor: Repeated tensor of size `sum(repeats)`. + + Example:: + + >>> torch.repeat_interleave(torch.tensor([1, 2, 3])) + tensor([0, 1, 1, 2, 2, 2]) + """ + +def reshape(input: Tensor, shape: Sequence[_int | SymInt]) -> Tensor: + r""" + reshape(input, shape) -> Tensor + + Returns a tensor with the same data and number of elements as :attr:`input`, + but with the specified shape. When possible, the returned tensor will be a view + of :attr:`input`. Otherwise, it will be a copy. Contiguous inputs and inputs + with compatible strides can be reshaped without copying, but you should not + depend on the copying vs. viewing behavior. + + See :meth:`torch.Tensor.view` on when it is possible to return a view. + + A single dimension may be -1, in which case it's inferred from the remaining + dimensions and the number of elements in :attr:`input`. + + Args: + input (Tensor): the tensor to be reshaped + shape (tuple of int): the new shape + + Example:: + + >>> a = torch.arange(4.) + >>> torch.reshape(a, (2, 2)) + tensor([[ 0., 1.], + [ 2., 3.]]) + >>> b = torch.tensor([[0, 1], [2, 3]]) + >>> torch.reshape(b, (-1,)) + tensor([ 0, 1, 2, 3]) + """ + +def resize_as_( + input: Tensor, + the_template: Tensor, + *, + memory_format: memory_format | None = None, +) -> Tensor: ... +def resize_as_sparse_(input: Tensor, the_template: Tensor) -> Tensor: ... +def resolve_conj(input: Tensor) -> Tensor: + r""" + resolve_conj(input) -> Tensor + + Returns a new tensor with materialized conjugation if :attr:`input`'s conjugate bit is set to `True`, + else returns :attr:`input`. The output tensor will always have its conjugate bit set to `False`. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> x = torch.tensor([-1 + 1j, -2 + 2j, 3 - 3j]) + >>> y = x.conj() + >>> y.is_conj() + True + >>> z = y.resolve_conj() + >>> z + tensor([-1 - 1j, -2 - 2j, 3 + 3j]) + >>> z.is_conj() + False + """ + +def resolve_neg(input: Tensor) -> Tensor: + r""" + resolve_neg(input) -> Tensor + + Returns a new tensor with materialized negation if :attr:`input`'s negative bit is set to `True`, + else returns :attr:`input`. The output tensor will always have its negative bit set to `False`. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> x = torch.tensor([-1 + 1j, -2 + 2j, 3 - 3j]) + >>> y = x.conj() + >>> z = y.imag + >>> z.is_neg() + True + >>> out = z.resolve_neg() + >>> out + tensor([-1., -2., 3.]) + >>> out.is_neg() + False + """ + +@overload +def result_type(tensor: Tensor, other: Tensor) -> _dtype: + r""" + result_type(tensor1, tensor2) -> dtype + + Returns the :class:`torch.dtype` that would result from performing an arithmetic + operation on the provided input tensors. See type promotion :ref:`documentation ` + for more information on the type promotion logic. + + Args: + tensor1 (Tensor or Number): an input tensor or number + tensor2 (Tensor or Number): an input tensor or number + + Example:: + + >>> torch.result_type(torch.tensor([1, 2], dtype=torch.int), 1.0) + torch.float32 + >>> torch.result_type(torch.tensor([1, 2], dtype=torch.uint8), torch.tensor(1)) + torch.uint8 + """ + +@overload +def result_type(scalar: Number | _complex, tensor: Tensor) -> _dtype: + r""" + result_type(tensor1, tensor2) -> dtype + + Returns the :class:`torch.dtype` that would result from performing an arithmetic + operation on the provided input tensors. See type promotion :ref:`documentation ` + for more information on the type promotion logic. + + Args: + tensor1 (Tensor or Number): an input tensor or number + tensor2 (Tensor or Number): an input tensor or number + + Example:: + + >>> torch.result_type(torch.tensor([1, 2], dtype=torch.int), 1.0) + torch.float32 + >>> torch.result_type(torch.tensor([1, 2], dtype=torch.uint8), torch.tensor(1)) + torch.uint8 + """ + +@overload +def result_type(tensor: Tensor, other: Number | _complex) -> _dtype: + r""" + result_type(tensor1, tensor2) -> dtype + + Returns the :class:`torch.dtype` that would result from performing an arithmetic + operation on the provided input tensors. See type promotion :ref:`documentation ` + for more information on the type promotion logic. + + Args: + tensor1 (Tensor or Number): an input tensor or number + tensor2 (Tensor or Number): an input tensor or number + + Example:: + + >>> torch.result_type(torch.tensor([1, 2], dtype=torch.int), 1.0) + torch.float32 + >>> torch.result_type(torch.tensor([1, 2], dtype=torch.uint8), torch.tensor(1)) + torch.uint8 + """ + +@overload +def result_type( + scalar1: Number | _complex, + scalar2: Number | _complex, +) -> _dtype: + r""" + result_type(tensor1, tensor2) -> dtype + + Returns the :class:`torch.dtype` that would result from performing an arithmetic + operation on the provided input tensors. See type promotion :ref:`documentation ` + for more information on the type promotion logic. + + Args: + tensor1 (Tensor or Number): an input tensor or number + tensor2 (Tensor or Number): an input tensor or number + + Example:: + + >>> torch.result_type(torch.tensor([1, 2], dtype=torch.int), 1.0) + torch.float32 + >>> torch.result_type(torch.tensor([1, 2], dtype=torch.uint8), torch.tensor(1)) + torch.uint8 + """ + +def rms_norm( + input: Tensor, + normalized_shape: Sequence[_int | SymInt], + weight: Tensor | None = None, + eps: _float | None = None, +) -> Tensor: ... +@overload +def rnn_relu( + data: Tensor, + batch_sizes: Tensor, + hx: Tensor, + params: tuple[Tensor, ...] | list[Tensor], + has_biases: _bool, + num_layers: _int, + dropout: _float, + train: _bool, + bidirectional: _bool, +) -> tuple[Tensor, Tensor]: ... +@overload +def rnn_relu( + input: Tensor, + hx: Tensor, + params: tuple[Tensor, ...] | list[Tensor], + has_biases: _bool, + num_layers: _int, + dropout: _float, + train: _bool, + bidirectional: _bool, + batch_first: _bool, +) -> tuple[Tensor, Tensor]: ... +def rnn_relu_cell( + input: Tensor, + hx: Tensor, + w_ih: Tensor, + w_hh: Tensor, + b_ih: Tensor | None = None, + b_hh: Tensor | None = None, +) -> Tensor: ... +@overload +def rnn_tanh( + data: Tensor, + batch_sizes: Tensor, + hx: Tensor, + params: tuple[Tensor, ...] | list[Tensor], + has_biases: _bool, + num_layers: _int, + dropout: _float, + train: _bool, + bidirectional: _bool, +) -> tuple[Tensor, Tensor]: ... +@overload +def rnn_tanh( + input: Tensor, + hx: Tensor, + params: tuple[Tensor, ...] | list[Tensor], + has_biases: _bool, + num_layers: _int, + dropout: _float, + train: _bool, + bidirectional: _bool, + batch_first: _bool, +) -> tuple[Tensor, Tensor]: ... +def rnn_tanh_cell( + input: Tensor, + hx: Tensor, + w_ih: Tensor, + w_hh: Tensor, + b_ih: Tensor | None = None, + b_hh: Tensor | None = None, +) -> Tensor: ... +def roll( + input: Tensor, + shifts: _int | SymInt | Sequence[_int | SymInt], + dims: _int | _size = (), +) -> Tensor: + r""" + roll(input, shifts, dims=None) -> Tensor + + Roll the tensor :attr:`input` along the given dimension(s). Elements that are + shifted beyond the last position are re-introduced at the first position. If + :attr:`dims` is `None`, the tensor will be flattened before rolling and then + restored to the original shape. + + Args: + input (Tensor): the input tensor. + shifts (int or tuple of ints): The number of places by which the elements + of the tensor are shifted. If shifts is a tuple, dims must be a tuple of + the same size, and each dimension will be rolled by the corresponding + value + dims (int or tuple of ints): Axis along which to roll + + Example:: + + >>> x = torch.tensor([1, 2, 3, 4, 5, 6, 7, 8]).view(4, 2) + >>> x + tensor([[1, 2], + [3, 4], + [5, 6], + [7, 8]]) + >>> torch.roll(x, 1) + tensor([[8, 1], + [2, 3], + [4, 5], + [6, 7]]) + >>> torch.roll(x, 1, 0) + tensor([[7, 8], + [1, 2], + [3, 4], + [5, 6]]) + >>> torch.roll(x, -1, 0) + tensor([[3, 4], + [5, 6], + [7, 8], + [1, 2]]) + >>> torch.roll(x, shifts=(2, 1), dims=(0, 1)) + tensor([[6, 5], + [8, 7], + [2, 1], + [4, 3]]) + """ + +def rot90(input: Tensor, k: _int = 1, dims: _size = (0, 1)) -> Tensor: + r""" + rot90(input, k=1, dims=(0, 1)) -> Tensor + + Rotate an n-D tensor by 90 degrees in the plane specified by dims axis. + Rotation direction is from the first towards the second axis if k > 0, and from the second towards the first for k < 0. + + Args: + input (Tensor): the input tensor. + k (int): number of times to rotate. Default value is 1 + dims (a list or tuple): axis to rotate. Default value is [0, 1] + + Example:: + + >>> x = torch.arange(4).view(2, 2) + >>> x + tensor([[0, 1], + [2, 3]]) + >>> torch.rot90(x, 1, [0, 1]) + tensor([[1, 3], + [0, 2]]) + + >>> x = torch.arange(8).view(2, 2, 2) + >>> x + tensor([[[0, 1], + [2, 3]], + + [[4, 5], + [6, 7]]]) + >>> torch.rot90(x, 1, [1, 2]) + tensor([[[1, 3], + [0, 2]], + + [[5, 7], + [4, 6]]]) + """ + +@overload +def round(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + round(input, *, decimals=0, out=None) -> Tensor + + Rounds elements of :attr:`input` to the nearest integer. + + For integer inputs, follows the array-api convention of returning a + copy of the input tensor. + The return type of output is same as that of input's dtype. + + .. note:: + This function implements the "round half to even" to + break ties when a number is equidistant from two + integers (e.g. `round(2.5)` is 2). + + When the :attr:\`decimals\` argument is specified the + algorithm used is similar to NumPy's `around`. This + algorithm is fast but inexact and it can easily + overflow for low precision dtypes. + Eg. `round(tensor([10000], dtype=torch.float16), decimals=3)` is `inf`. + + .. seealso:: + :func:`torch.ceil`, which rounds up. + :func:`torch.floor`, which rounds down. + :func:`torch.trunc`, which rounds towards zero. + + Args: + input (Tensor): the input tensor. + decimals (int): Number of decimal places to round to (default: 0). + If decimals is negative, it specifies the number of positions + to the left of the decimal point. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.round(torch.tensor((4.7, -2.3, 9.1, -7.7))) + tensor([ 5., -2., 9., -8.]) + + >>> # Values equidistant from two integers are rounded towards the + >>> # the nearest even value (zero is treated as even) + >>> torch.round(torch.tensor([-0.5, 0.5, 1.5, 2.5])) + tensor([-0., 0., 2., 2.]) + + >>> # A positive decimals argument rounds to the to that decimal place + >>> torch.round(torch.tensor([0.1234567]), decimals=3) + tensor([0.1230]) + + >>> # A negative decimals argument rounds to the left of the decimal + >>> torch.round(torch.tensor([1200.1234567]), decimals=-3) + tensor([1000.]) + """ + +@overload +def round( + input: Tensor, + *, + decimals: _int, + out: Tensor | None = None, +) -> Tensor: + r""" + round(input, *, decimals=0, out=None) -> Tensor + + Rounds elements of :attr:`input` to the nearest integer. + + For integer inputs, follows the array-api convention of returning a + copy of the input tensor. + The return type of output is same as that of input's dtype. + + .. note:: + This function implements the "round half to even" to + break ties when a number is equidistant from two + integers (e.g. `round(2.5)` is 2). + + When the :attr:\`decimals\` argument is specified the + algorithm used is similar to NumPy's `around`. This + algorithm is fast but inexact and it can easily + overflow for low precision dtypes. + Eg. `round(tensor([10000], dtype=torch.float16), decimals=3)` is `inf`. + + .. seealso:: + :func:`torch.ceil`, which rounds up. + :func:`torch.floor`, which rounds down. + :func:`torch.trunc`, which rounds towards zero. + + Args: + input (Tensor): the input tensor. + decimals (int): Number of decimal places to round to (default: 0). + If decimals is negative, it specifies the number of positions + to the left of the decimal point. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.round(torch.tensor((4.7, -2.3, 9.1, -7.7))) + tensor([ 5., -2., 9., -8.]) + + >>> # Values equidistant from two integers are rounded towards the + >>> # the nearest even value (zero is treated as even) + >>> torch.round(torch.tensor([-0.5, 0.5, 1.5, 2.5])) + tensor([-0., 0., 2., 2.]) + + >>> # A positive decimals argument rounds to the to that decimal place + >>> torch.round(torch.tensor([0.1234567]), decimals=3) + tensor([0.1230]) + + >>> # A negative decimals argument rounds to the left of the decimal + >>> torch.round(torch.tensor([1200.1234567]), decimals=-3) + tensor([1000.]) + """ + +@overload +def round_(input: Tensor) -> Tensor: ... +@overload +def round_(input: Tensor, *, decimals: _int) -> Tensor: ... +def row_indices_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: ... +def row_stack( + tensors: tuple[Tensor, ...] | list[Tensor], + *, + out: Tensor | None = None, +) -> Tensor: + r""" + row_stack(tensors, *, out=None) -> Tensor + + Alias of :func:`torch.vstack`. + """ + +def rrelu( + input: Tensor, + lower: Number | _complex = 0.125, + upper: Number | _complex = 0.3333333333333333, + training: _bool = False, + generator: Generator | None = None, +) -> Tensor: ... +def rrelu_( + input: Tensor, + lower: Number | _complex = 0.125, + upper: Number | _complex = 0.3333333333333333, + training: _bool = False, + generator: Generator | None = None, +) -> Tensor: ... +def rsqrt(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + rsqrt(input, *, out=None) -> Tensor + + Returns a new tensor with the reciprocal of the square-root of each of + the elements of :attr:`input`. + + .. math:: + \text{out}_{i} = \frac{1}{\sqrt{\text{input}_{i}}} + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-0.0370, 0.2970, 1.5420, -0.9105]) + >>> torch.rsqrt(a) + tensor([ nan, 1.8351, 0.8053, nan]) + """ + +def rsqrt_(input: Tensor) -> Tensor: ... +@overload +def rsub( + input: Tensor, + other: Tensor, + *, + alpha: Number | _complex = 1, +) -> Tensor: ... +@overload +def rsub( + input: Tensor, + other: Number | _complex, + alpha: Number | _complex = 1, +) -> Tensor: ... +def saddmm( + input: Tensor, + mat1: Tensor, + mat2: Tensor, + *, + beta: Number = 1, + alpha: Number = 1, + out: Tensor | None = None, +) -> Tensor: ... +def scalar_tensor( + s: Number | _complex, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: ... +@overload +def scatter( + input: Tensor, + dim: _int, + index: Tensor, + src: Tensor, + *, + reduce: str, + out: Tensor | None = None, +) -> Tensor: + r""" + scatter(input, dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_` + """ + +@overload +def scatter( + input: Tensor, + dim: _int, + index: Tensor, + src: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + scatter(input, dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_` + """ + +@overload +def scatter( + input: Tensor, + dim: _int, + index: Tensor, + value: Number | _complex, + *, + reduce: str, + out: Tensor | None = None, +) -> Tensor: + r""" + scatter(input, dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_` + """ + +@overload +def scatter( + input: Tensor, + dim: str | EllipsisType | None, + index: Tensor, + src: Tensor, +) -> Tensor: + r""" + scatter(input, dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_` + """ + +@overload +def scatter( + input: Tensor, + dim: _int, + index: Tensor, + value: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + scatter(input, dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_` + """ + +@overload +def scatter( + input: Tensor, + dim: str | EllipsisType | None, + index: Tensor, + value: Number | _complex, +) -> Tensor: + r""" + scatter(input, dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_` + """ + +@overload +def scatter_add( + input: Tensor, + dim: _int, + index: Tensor, + src: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + scatter_add(input, dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_add_` + """ + +@overload +def scatter_add( + input: Tensor, + dim: str | EllipsisType | None, + index: Tensor, + src: Tensor, +) -> Tensor: + r""" + scatter_add(input, dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_add_` + """ + +def scatter_reduce( + input: Tensor, + dim: _int, + index: Tensor, + src: Tensor, + reduce: str, + *, + include_self: _bool = True, + out: Tensor | None = None, +) -> Tensor: + r""" + scatter_reduce(input, dim, index, src, reduce, *, include_self=True) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_reduce_` + """ + +@overload +def searchsorted( + sorted_sequence: Tensor, + input: Tensor, + *, + out_int32: _bool = False, + right: _bool = False, + side: str | None = None, + sorter: Tensor | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + searchsorted(sorted_sequence, values, *, out_int32=False, right=False, side=None, out=None, sorter=None) -> Tensor + + Find the indices from the *innermost* dimension of :attr:`sorted_sequence` such that, if the + corresponding values in :attr:`values` were inserted before the indices, when sorted, the order + of the corresponding *innermost* dimension within :attr:`sorted_sequence` would be preserved. + Return a new tensor with the same size as :attr:`values`. More formally, + the returned index satisfies the following rules: + + .. list-table:: + :widths: 12 10 78 + :header-rows: 1 + + * - :attr:`sorted_sequence` + - :attr:`right` + - *returned index satisfies* + * - 1-D + - False + - ``sorted_sequence[i-1] < values[m][n]...[l][x] <= sorted_sequence[i]`` + * - 1-D + - True + - ``sorted_sequence[i-1] <= values[m][n]...[l][x] < sorted_sequence[i]`` + * - N-D + - False + - ``sorted_sequence[m][n]...[l][i-1] < values[m][n]...[l][x] <= sorted_sequence[m][n]...[l][i]`` + * - N-D + - True + - ``sorted_sequence[m][n]...[l][i-1] <= values[m][n]...[l][x] < sorted_sequence[m][n]...[l][i]`` + + Args: + sorted_sequence (Tensor): N-D or 1-D tensor, containing monotonically increasing sequence on the *innermost* + dimension unless :attr:`sorter` is provided, in which case the sequence does not + need to be sorted + values (Tensor or Scalar): N-D tensor or a Scalar containing the search value(s). + + Keyword args: + out_int32 (bool, optional): indicate the output data type. torch.int32 if True, torch.int64 otherwise. + Default value is False, i.e. default output data type is torch.int64. + right (bool, optional): if False, return the first suitable location that is found. If True, return the + last such index. If no suitable index found, return 0 for non-numerical value + (eg. nan, inf) or the size of *innermost* dimension within :attr:`sorted_sequence` + (one pass the last index of the *innermost* dimension). In other words, if False, + gets the lower bound index for each value in :attr:`values` on the corresponding + *innermost* dimension of the :attr:`sorted_sequence`. If True, gets the upper + bound index instead. Default value is False. :attr:`side` does the same and is + preferred. It will error if :attr:`side` is set to "left" while this is True. + side (str, optional): the same as :attr:`right` but preferred. "left" corresponds to False for :attr:`right` + and "right" corresponds to True for :attr:`right`. It will error if this is set to + "left" while :attr:`right` is True. Default value is None. + out (Tensor, optional): the output tensor, must be the same size as :attr:`values` if provided. + sorter (LongTensor, optional): if provided, a tensor matching the shape of the unsorted + :attr:`sorted_sequence` containing a sequence of indices that sort it in the + ascending order on the innermost dimension + + + Example:: + + >>> sorted_sequence = torch.tensor([[1, 3, 5, 7, 9], [2, 4, 6, 8, 10]]) + >>> sorted_sequence + tensor([[ 1, 3, 5, 7, 9], + [ 2, 4, 6, 8, 10]]) + >>> values = torch.tensor([[3, 6, 9], [3, 6, 9]]) + >>> values + tensor([[3, 6, 9], + [3, 6, 9]]) + >>> torch.searchsorted(sorted_sequence, values) + tensor([[1, 3, 4], + [1, 2, 4]]) + >>> torch.searchsorted(sorted_sequence, values, side='right') + tensor([[2, 3, 5], + [1, 3, 4]]) + + >>> sorted_sequence_1d = torch.tensor([1, 3, 5, 7, 9]) + >>> sorted_sequence_1d + tensor([1, 3, 5, 7, 9]) + >>> torch.searchsorted(sorted_sequence_1d, values) + tensor([[1, 3, 4], + [1, 3, 4]]) + """ + +@overload +def searchsorted( + sorted_sequence: Tensor, + self: Number | _complex, + *, + out_int32: _bool = False, + right: _bool = False, + side: str | None = None, + sorter: Tensor | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + searchsorted(sorted_sequence, values, *, out_int32=False, right=False, side=None, out=None, sorter=None) -> Tensor + + Find the indices from the *innermost* dimension of :attr:`sorted_sequence` such that, if the + corresponding values in :attr:`values` were inserted before the indices, when sorted, the order + of the corresponding *innermost* dimension within :attr:`sorted_sequence` would be preserved. + Return a new tensor with the same size as :attr:`values`. More formally, + the returned index satisfies the following rules: + + .. list-table:: + :widths: 12 10 78 + :header-rows: 1 + + * - :attr:`sorted_sequence` + - :attr:`right` + - *returned index satisfies* + * - 1-D + - False + - ``sorted_sequence[i-1] < values[m][n]...[l][x] <= sorted_sequence[i]`` + * - 1-D + - True + - ``sorted_sequence[i-1] <= values[m][n]...[l][x] < sorted_sequence[i]`` + * - N-D + - False + - ``sorted_sequence[m][n]...[l][i-1] < values[m][n]...[l][x] <= sorted_sequence[m][n]...[l][i]`` + * - N-D + - True + - ``sorted_sequence[m][n]...[l][i-1] <= values[m][n]...[l][x] < sorted_sequence[m][n]...[l][i]`` + + Args: + sorted_sequence (Tensor): N-D or 1-D tensor, containing monotonically increasing sequence on the *innermost* + dimension unless :attr:`sorter` is provided, in which case the sequence does not + need to be sorted + values (Tensor or Scalar): N-D tensor or a Scalar containing the search value(s). + + Keyword args: + out_int32 (bool, optional): indicate the output data type. torch.int32 if True, torch.int64 otherwise. + Default value is False, i.e. default output data type is torch.int64. + right (bool, optional): if False, return the first suitable location that is found. If True, return the + last such index. If no suitable index found, return 0 for non-numerical value + (eg. nan, inf) or the size of *innermost* dimension within :attr:`sorted_sequence` + (one pass the last index of the *innermost* dimension). In other words, if False, + gets the lower bound index for each value in :attr:`values` on the corresponding + *innermost* dimension of the :attr:`sorted_sequence`. If True, gets the upper + bound index instead. Default value is False. :attr:`side` does the same and is + preferred. It will error if :attr:`side` is set to "left" while this is True. + side (str, optional): the same as :attr:`right` but preferred. "left" corresponds to False for :attr:`right` + and "right" corresponds to True for :attr:`right`. It will error if this is set to + "left" while :attr:`right` is True. Default value is None. + out (Tensor, optional): the output tensor, must be the same size as :attr:`values` if provided. + sorter (LongTensor, optional): if provided, a tensor matching the shape of the unsorted + :attr:`sorted_sequence` containing a sequence of indices that sort it in the + ascending order on the innermost dimension + + + Example:: + + >>> sorted_sequence = torch.tensor([[1, 3, 5, 7, 9], [2, 4, 6, 8, 10]]) + >>> sorted_sequence + tensor([[ 1, 3, 5, 7, 9], + [ 2, 4, 6, 8, 10]]) + >>> values = torch.tensor([[3, 6, 9], [3, 6, 9]]) + >>> values + tensor([[3, 6, 9], + [3, 6, 9]]) + >>> torch.searchsorted(sorted_sequence, values) + tensor([[1, 3, 4], + [1, 2, 4]]) + >>> torch.searchsorted(sorted_sequence, values, side='right') + tensor([[2, 3, 5], + [1, 3, 4]]) + + >>> sorted_sequence_1d = torch.tensor([1, 3, 5, 7, 9]) + >>> sorted_sequence_1d + tensor([1, 3, 5, 7, 9]) + >>> torch.searchsorted(sorted_sequence_1d, values) + tensor([[1, 3, 4], + [1, 3, 4]]) + """ + +def segment_reduce( + data: Tensor, + reduce: str, + *, + lengths: Tensor | None = None, + indices: Tensor | None = None, + offsets: Tensor | None = None, + axis: _int = 0, + unsafe: _bool = False, + initial: Number | _complex | None = None, +) -> Tensor: + r""" + segment_reduce(data: Tensor, reduce: str, *, lengths: Tensor | None = None, indices: Tensor | None = None, offsets: Tensor | None = None, axis: _int = 0, unsafe: _bool = False, initial: Number | _complex | None = None) -> Tensor # noqa: B950 + + Perform a segment reduction operation on the input tensor along the specified axis. + + Args: + data (Tensor): The input tensor on which the segment reduction operation will be performed. + reduce (str): The type of reduction operation. Supported values are ``sum``, ``mean``, ``max``, ``min``, ``prod``. + + Keyword args: + lengths (Tensor, optional): Length of each segment. Default: ``None``. + offsets (Tensor, optional): Offset of each segment. Default: ``None``. + axis (int, optional): The axis perform reduction. Default: ``0``. + unsafe (bool, optional): Skip validation If `True`. Default: ``False``. + initial (Number, optional): The initial value for the reduction operation. Default: ``None``. + + Example:: + + >>> data = torch.tensor([[1, 2, 3, 4],[5, 6, 7, 8],[9, 10, 11, 12]], dtype=torch.float32, device='cuda') + >>> lengths = torch.tensor([2, 1], device='cuda') + >>> torch.segment_reduce(data, 'max', lengths=lengths) + tensor([[ 5., 6., 7., 8.], + [ 9., 10., 11., 12.]], device='cuda:0') + """ + +@overload +def select(input: Tensor, dim: _int, index: _int | SymInt) -> Tensor: + r""" + select(input, dim, index) -> Tensor + + Slices the :attr:`input` tensor along the selected dimension at the given index. + This function returns a view of the original tensor with the given dimension removed. + + .. note:: If :attr:`input` is a sparse tensor and returning a view of + the tensor is not possible, a RuntimeError exception is + raised. In this is the case, consider using + :func:`torch.select_copy` function. + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to slice + index (int): the index to select with + + .. note:: + + :meth:`select` is equivalent to slicing. For example, + ``tensor.select(0, index)`` is equivalent to ``tensor[index]`` and + ``tensor.select(2, index)`` is equivalent to ``tensor[:,:,index]``. + """ + +@overload +def select( + input: Tensor, + dim: str | EllipsisType | None, + index: _int, +) -> Tensor: + r""" + select(input, dim, index) -> Tensor + + Slices the :attr:`input` tensor along the selected dimension at the given index. + This function returns a view of the original tensor with the given dimension removed. + + .. note:: If :attr:`input` is a sparse tensor and returning a view of + the tensor is not possible, a RuntimeError exception is + raised. In this is the case, consider using + :func:`torch.select_copy` function. + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to slice + index (int): the index to select with + + .. note:: + + :meth:`select` is equivalent to slicing. For example, + ``tensor.select(0, index)`` is equivalent to ``tensor[index]`` and + ``tensor.select(2, index)`` is equivalent to ``tensor[:,:,index]``. + """ + +def select_copy( + input: Tensor, + dim: _int, + index: _int | SymInt, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.select`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def select_scatter( + input: Tensor, + src: Tensor, + dim: _int, + index: _int | SymInt, +) -> Tensor: + r""" + select_scatter(input, src, dim, index) -> Tensor + + Embeds the values of the :attr:`src` tensor into :attr:`input` at the given index. + This function returns a tensor with fresh storage; it does not create a view. + + + Args: + input (Tensor): the input tensor. + src (Tensor): The tensor to embed into :attr:`input` + dim (int): the dimension to insert the slice into. + index (int): the index to select with + + .. note:: + + :attr:`src` must be of the proper size in order to be embedded + into :attr:`input`. Specifically, it should have the same shape as + ``torch.select(input, dim, index)`` + + Example:: + + >>> a = torch.zeros(2, 2) + >>> b = torch.ones(2) + >>> a.select_scatter(b, 0, 0) + tensor([[1., 1.], + [0., 0.]]) + """ + +def selu(input: Tensor) -> Tensor: ... +def selu_(input: Tensor) -> Tensor: ... +def set_flush_denormal(mode: _bool) -> _bool: + r""" + set_flush_denormal(mode) -> bool + + Disables denormal floating numbers on CPU. + + Returns ``True`` if your system supports flushing denormal numbers and it + successfully configures flush denormal mode. :meth:`~torch.set_flush_denormal` + is supported on x86 architectures supporting SSE3 and AArch64 architecture. + + Args: + mode (bool): Controls whether to enable flush denormal mode or not + + Example:: + + >>> torch.set_flush_denormal(True) + True + >>> torch.tensor([1e-323], dtype=torch.float64) + tensor([ 0.], dtype=torch.float64) + >>> torch.set_flush_denormal(False) + True + >>> torch.tensor([1e-323], dtype=torch.float64) + tensor(9.88131e-324 * + [ 1.0000], dtype=torch.float64) + """ + +def set_num_interop_threads(num: _int) -> None: + r""" + set_num_interop_threads(int) + + Sets the number of threads used for interop parallelism + (e.g. in JIT interpreter) on CPU. + + .. warning:: + Can only be called once and before any inter-op parallel work + is started (e.g. JIT execution). + """ + +def set_num_threads(num: _int) -> None: + r""" + set_num_threads(int) + + Sets the number of threads used for intraop parallelism on CPU. + + .. warning:: + To ensure that the correct number of threads is used, set_num_threads + must be called before running eager, JIT or autograd code. + """ + +def sgn(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + sgn(input, *, out=None) -> Tensor + + This function is an extension of torch.sign() to complex tensors. + It computes a new tensor whose elements have + the same angles as the corresponding elements of :attr:`input` and + absolute values (i.e. magnitudes) of one for complex tensors and + is equivalent to torch.sign() for non-complex tensors. + + .. math:: + \text{out}_{i} = \begin{cases} + 0 & |\text{{input}}_i| == 0 \\ + \frac{{\text{{input}}_i}}{|{\text{{input}}_i}|} & \text{otherwise} + \end{cases} + + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.tensor([3+4j, 7-24j, 0, 1+2j]) + >>> t.sgn() + tensor([0.6000+0.8000j, 0.2800-0.9600j, 0.0000+0.0000j, 0.4472+0.8944j]) + """ + +def sigmoid(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + sigmoid(input, *, out=None) -> Tensor + + Alias for :func:`torch.special.expit`. + """ + +def sigmoid_(input: Tensor) -> Tensor: ... +def sign(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + sign(input, *, out=None) -> Tensor + + Returns a new tensor with the signs of the elements of :attr:`input`. + + .. math:: + \text{out}_{i} = \operatorname{sgn}(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([0.7, -1.2, 0., 2.3]) + >>> a + tensor([ 0.7000, -1.2000, 0.0000, 2.3000]) + >>> torch.sign(a) + tensor([ 1., -1., 0., 1.]) + """ + +def signbit(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + signbit(input, *, out=None) -> Tensor + + Tests if each element of :attr:`input` has its sign bit set or not. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([0.7, -1.2, 0., 2.3]) + >>> torch.signbit(a) + tensor([ False, True, False, False]) + >>> a = torch.tensor([-0.0, 0.0]) + >>> torch.signbit(a) + tensor([ True, False]) + + .. note:: + signbit handles signed zeros, so negative zero (-0) returns True. + """ + +def sin(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + sin(input, *, out=None) -> Tensor + + Returns a new tensor with the sine of the elements in the :attr:`input` tensor, + where each value in this input tensor is in radians. + + .. math:: + \text{out}_{i} = \sin(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-0.5461, 0.1347, -2.7266, -0.2746]) + >>> torch.sin(a) + tensor([-0.5194, 0.1343, -0.4032, -0.2711]) + """ + +def sin_(input: Tensor) -> Tensor: ... +def sinc(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + sinc(input, *, out=None) -> Tensor + + Alias for :func:`torch.special.sinc`. + """ + +def sinc_(input: Tensor) -> Tensor: ... +def sinh(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + sinh(input, *, out=None) -> Tensor + + Returns a new tensor with the hyperbolic sine of the elements of + :attr:`input`. + + .. math:: + \text{out}_{i} = \sinh(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.5380, -0.8632, -0.1265, 0.9399]) + >>> torch.sinh(a) + tensor([ 0.5644, -0.9744, -0.1268, 1.0845]) + + .. note:: + When :attr:`input` is on the CPU, the implementation of torch.sinh may use + the Sleef library, which rounds very large results to infinity or negative + infinity. See `here `_ for details. + """ + +def sinh_(input: Tensor) -> Tensor: ... +def slice_copy( + input: Tensor, + dim: _int = 0, + start: _int | SymInt | None = None, + end: _int | SymInt | None = None, + step: _int | SymInt = 1, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.slice`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def slice_inverse( + input: Tensor, + src: Tensor, + dim: _int = 0, + start: _int | SymInt | None = None, + end: _int | SymInt | None = None, + step: _int | SymInt = 1, +) -> Tensor: ... +def slice_scatter( + input: Tensor, + src: Tensor, + dim: _int = 0, + start: _int | SymInt | None = None, + end: _int | SymInt | None = None, + step: _int | SymInt = 1, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + slice_scatter(input, src, dim=0, start=None, end=None, step=1) -> Tensor + + Embeds the values of the :attr:`src` tensor into :attr:`input` at the given + dimension. + This function returns a tensor with fresh storage; it does not create a view. + + + Args: + input (Tensor): the input tensor. + src (Tensor): The tensor to embed into :attr:`input` + dim (int): the dimension to insert the slice into + start (Optional[int]): the start index of where to insert the slice + end (Optional[int]): the end index of where to insert the slice + step (int): the how many elements to skip in + + Example:: + + >>> a = torch.zeros(8, 8) + >>> b = torch.ones(2, 8) + >>> a.slice_scatter(b, start=6) + tensor([[0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0.], + [1., 1., 1., 1., 1., 1., 1., 1.], + [1., 1., 1., 1., 1., 1., 1., 1.]]) + + >>> b = torch.ones(8, 2) + >>> a.slice_scatter(b, dim=1, start=2, end=6, step=2) + tensor([[0., 0., 1., 0., 1., 0., 0., 0.], + [0., 0., 1., 0., 1., 0., 0., 0.], + [0., 0., 1., 0., 1., 0., 0., 0.], + [0., 0., 1., 0., 1., 0., 0., 0.], + [0., 0., 1., 0., 1., 0., 0., 0.], + [0., 0., 1., 0., 1., 0., 0., 0.], + [0., 0., 1., 0., 1., 0., 0., 0.], + [0., 0., 1., 0., 1., 0., 0., 0.]]) + """ + +def slogdet( + input: Tensor, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.slogdet: + r""" + slogdet(input) -> (Tensor, Tensor) + + Alias for :func:`torch.linalg.slogdet` + """ + +def smm(input: Tensor, mat2: Tensor) -> Tensor: + r""" + smm(input, mat) -> Tensor + + Performs a matrix multiplication of the sparse matrix :attr:`input` + with the dense matrix :attr:`mat`. + + Args: + input (Tensor): a sparse matrix to be matrix multiplied + mat (Tensor): a dense matrix to be matrix multiplied + """ + +@overload +def softmax( + input: Tensor, + dim: _int, + dtype: _dtype | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + softmax(input, dim, *, dtype=None) -> Tensor + + Alias for :func:`torch.nn.functional.softmax`. + """ + +@overload +def softmax( + input: Tensor, + dim: str | EllipsisType | None, + *, + dtype: _dtype | None = None, +) -> Tensor: + r""" + softmax(input, dim, *, dtype=None) -> Tensor + + Alias for :func:`torch.nn.functional.softmax`. + """ + +@overload +def sort( + input: Tensor, + *, + stable: _bool | None, + dim: _int = -1, + descending: _bool = False, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.sort: + r""" + sort(input, dim=-1, descending=False, *, stable=False, out=None) -> (Tensor, LongTensor) + + Sorts the elements of the :attr:`input` tensor along a given dimension + in ascending order by value. + + If :attr:`dim` is not given, the last dimension of the `input` is chosen. + + If :attr:`descending` is ``True`` then the elements are sorted in descending + order by value. + + If :attr:`stable` is ``True`` then the sorting routine becomes stable, preserving + the order of equivalent elements. + + A namedtuple of (values, indices) is returned, where the `values` are the + sorted values and `indices` are the indices of the elements in the original + `input` tensor. + + Args: + input (Tensor): the input tensor. + dim (int, optional): the dimension to sort along + descending (bool, optional): controls the sorting order (ascending or descending) + + Keyword args: + stable (bool, optional): makes the sorting routine stable, which guarantees that the order + of equivalent elements is preserved. + out (tuple, optional): the output tuple of (`Tensor`, `LongTensor`) that can + be optionally given to be used as output buffers + + Example:: + + >>> x = torch.randn(3, 4) + >>> sorted, indices = torch.sort(x) + >>> sorted + tensor([[-0.2162, 0.0608, 0.6719, 2.3332], + [-0.5793, 0.0061, 0.6058, 0.9497], + [-0.5071, 0.3343, 0.9553, 1.0960]]) + >>> indices + tensor([[ 1, 0, 2, 3], + [ 3, 1, 0, 2], + [ 0, 3, 1, 2]]) + + >>> sorted, indices = torch.sort(x, 0) + >>> sorted + tensor([[-0.5071, -0.2162, 0.6719, -0.5793], + [ 0.0608, 0.0061, 0.9497, 0.3343], + [ 0.6058, 0.9553, 1.0960, 2.3332]]) + >>> indices + tensor([[ 2, 0, 0, 1], + [ 0, 1, 1, 2], + [ 1, 2, 2, 0]]) + >>> x = torch.tensor([0, 1] * 9) + >>> x.sort() + torch.return_types.sort( + values=tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + indices=tensor([ 2, 16, 4, 6, 14, 8, 0, 10, 12, 9, 17, 15, 13, 11, 7, 5, 3, 1])) + >>> x.sort(stable=True) + torch.return_types.sort( + values=tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + indices=tensor([ 0, 2, 4, 6, 8, 10, 12, 14, 16, 1, 3, 5, 7, 9, 11, 13, 15, 17])) + """ + +@overload +def sort( + input: Tensor, + dim: _int = -1, + descending: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.sort: + r""" + sort(input, dim=-1, descending=False, *, stable=False, out=None) -> (Tensor, LongTensor) + + Sorts the elements of the :attr:`input` tensor along a given dimension + in ascending order by value. + + If :attr:`dim` is not given, the last dimension of the `input` is chosen. + + If :attr:`descending` is ``True`` then the elements are sorted in descending + order by value. + + If :attr:`stable` is ``True`` then the sorting routine becomes stable, preserving + the order of equivalent elements. + + A namedtuple of (values, indices) is returned, where the `values` are the + sorted values and `indices` are the indices of the elements in the original + `input` tensor. + + Args: + input (Tensor): the input tensor. + dim (int, optional): the dimension to sort along + descending (bool, optional): controls the sorting order (ascending or descending) + + Keyword args: + stable (bool, optional): makes the sorting routine stable, which guarantees that the order + of equivalent elements is preserved. + out (tuple, optional): the output tuple of (`Tensor`, `LongTensor`) that can + be optionally given to be used as output buffers + + Example:: + + >>> x = torch.randn(3, 4) + >>> sorted, indices = torch.sort(x) + >>> sorted + tensor([[-0.2162, 0.0608, 0.6719, 2.3332], + [-0.5793, 0.0061, 0.6058, 0.9497], + [-0.5071, 0.3343, 0.9553, 1.0960]]) + >>> indices + tensor([[ 1, 0, 2, 3], + [ 3, 1, 0, 2], + [ 0, 3, 1, 2]]) + + >>> sorted, indices = torch.sort(x, 0) + >>> sorted + tensor([[-0.5071, -0.2162, 0.6719, -0.5793], + [ 0.0608, 0.0061, 0.9497, 0.3343], + [ 0.6058, 0.9553, 1.0960, 2.3332]]) + >>> indices + tensor([[ 2, 0, 0, 1], + [ 0, 1, 1, 2], + [ 1, 2, 2, 0]]) + >>> x = torch.tensor([0, 1] * 9) + >>> x.sort() + torch.return_types.sort( + values=tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + indices=tensor([ 2, 16, 4, 6, 14, 8, 0, 10, 12, 9, 17, 15, 13, 11, 7, 5, 3, 1])) + >>> x.sort(stable=True) + torch.return_types.sort( + values=tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + indices=tensor([ 0, 2, 4, 6, 8, 10, 12, 14, 16, 1, 3, 5, 7, 9, 11, 13, 15, 17])) + """ + +@overload +def sort( + input: Tensor, + *, + stable: _bool | None, + dim: str | EllipsisType | None, + descending: _bool = False, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.sort: + r""" + sort(input, dim=-1, descending=False, *, stable=False, out=None) -> (Tensor, LongTensor) + + Sorts the elements of the :attr:`input` tensor along a given dimension + in ascending order by value. + + If :attr:`dim` is not given, the last dimension of the `input` is chosen. + + If :attr:`descending` is ``True`` then the elements are sorted in descending + order by value. + + If :attr:`stable` is ``True`` then the sorting routine becomes stable, preserving + the order of equivalent elements. + + A namedtuple of (values, indices) is returned, where the `values` are the + sorted values and `indices` are the indices of the elements in the original + `input` tensor. + + Args: + input (Tensor): the input tensor. + dim (int, optional): the dimension to sort along + descending (bool, optional): controls the sorting order (ascending or descending) + + Keyword args: + stable (bool, optional): makes the sorting routine stable, which guarantees that the order + of equivalent elements is preserved. + out (tuple, optional): the output tuple of (`Tensor`, `LongTensor`) that can + be optionally given to be used as output buffers + + Example:: + + >>> x = torch.randn(3, 4) + >>> sorted, indices = torch.sort(x) + >>> sorted + tensor([[-0.2162, 0.0608, 0.6719, 2.3332], + [-0.5793, 0.0061, 0.6058, 0.9497], + [-0.5071, 0.3343, 0.9553, 1.0960]]) + >>> indices + tensor([[ 1, 0, 2, 3], + [ 3, 1, 0, 2], + [ 0, 3, 1, 2]]) + + >>> sorted, indices = torch.sort(x, 0) + >>> sorted + tensor([[-0.5071, -0.2162, 0.6719, -0.5793], + [ 0.0608, 0.0061, 0.9497, 0.3343], + [ 0.6058, 0.9553, 1.0960, 2.3332]]) + >>> indices + tensor([[ 2, 0, 0, 1], + [ 0, 1, 1, 2], + [ 1, 2, 2, 0]]) + >>> x = torch.tensor([0, 1] * 9) + >>> x.sort() + torch.return_types.sort( + values=tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + indices=tensor([ 2, 16, 4, 6, 14, 8, 0, 10, 12, 9, 17, 15, 13, 11, 7, 5, 3, 1])) + >>> x.sort(stable=True) + torch.return_types.sort( + values=tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + indices=tensor([ 0, 2, 4, 6, 8, 10, 12, 14, 16, 1, 3, 5, 7, 9, 11, 13, 15, 17])) + """ + +@overload +def sort( + input: Tensor, + dim: str | EllipsisType | None, + descending: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.sort: + r""" + sort(input, dim=-1, descending=False, *, stable=False, out=None) -> (Tensor, LongTensor) + + Sorts the elements of the :attr:`input` tensor along a given dimension + in ascending order by value. + + If :attr:`dim` is not given, the last dimension of the `input` is chosen. + + If :attr:`descending` is ``True`` then the elements are sorted in descending + order by value. + + If :attr:`stable` is ``True`` then the sorting routine becomes stable, preserving + the order of equivalent elements. + + A namedtuple of (values, indices) is returned, where the `values` are the + sorted values and `indices` are the indices of the elements in the original + `input` tensor. + + Args: + input (Tensor): the input tensor. + dim (int, optional): the dimension to sort along + descending (bool, optional): controls the sorting order (ascending or descending) + + Keyword args: + stable (bool, optional): makes the sorting routine stable, which guarantees that the order + of equivalent elements is preserved. + out (tuple, optional): the output tuple of (`Tensor`, `LongTensor`) that can + be optionally given to be used as output buffers + + Example:: + + >>> x = torch.randn(3, 4) + >>> sorted, indices = torch.sort(x) + >>> sorted + tensor([[-0.2162, 0.0608, 0.6719, 2.3332], + [-0.5793, 0.0061, 0.6058, 0.9497], + [-0.5071, 0.3343, 0.9553, 1.0960]]) + >>> indices + tensor([[ 1, 0, 2, 3], + [ 3, 1, 0, 2], + [ 0, 3, 1, 2]]) + + >>> sorted, indices = torch.sort(x, 0) + >>> sorted + tensor([[-0.5071, -0.2162, 0.6719, -0.5793], + [ 0.0608, 0.0061, 0.9497, 0.3343], + [ 0.6058, 0.9553, 1.0960, 2.3332]]) + >>> indices + tensor([[ 2, 0, 0, 1], + [ 0, 1, 1, 2], + [ 1, 2, 2, 0]]) + >>> x = torch.tensor([0, 1] * 9) + >>> x.sort() + torch.return_types.sort( + values=tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + indices=tensor([ 2, 16, 4, 6, 14, 8, 0, 10, 12, 9, 17, 15, 13, 11, 7, 5, 3, 1])) + >>> x.sort(stable=True) + torch.return_types.sort( + values=tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + indices=tensor([ 0, 2, 4, 6, 8, 10, 12, 14, 16, 1, 3, 5, 7, 9, 11, 13, 15, 17])) + """ + +def sparse_bsc_tensor( + ccol_indices: Tensor | list, + row_indices: Tensor | list, + values: Tensor | list, + size: _size | None = None, + *, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + check_invariants: _bool | None = None, +) -> Tensor: + r""" + sparse_bsc_tensor(ccol_indices, row_indices, values, size=None, *, dtype=None, device=None, pin_memory=False, requires_grad=False, check_invariants=None) -> Tensor + + Constructs a :ref:`sparse tensor in BSC (Block Compressed Sparse + Column)) ` with specified 2-dimensional blocks at the + given :attr:`ccol_indices` and :attr:`row_indices`. Sparse matrix + multiplication operations in BSC format are typically faster than that + for sparse tensors in COO format. Make you have a look at :ref:`the + note on the data type of the indices `. + + .. note:: + + If the ``device`` argument is not specified the device of the given + :attr:`values` and indices tensor(s) must match. If, however, the + argument is specified the input Tensors will be converted to the + given device and in turn determine the device of the constructed + sparse tensor. + + Args: + ccol_indices (array_like): (B+1)-dimensional array of size + ``(*batchsize, ncolblocks + 1)``. The last element of each + batch is the number of non-zeros. This tensor encodes the + index in values and row_indices depending on where the given + column starts. Each successive number in the tensor subtracted + by the number before it denotes the number of elements in a + given column. + row_indices (array_like): Row block coordinates of each block in + values. (B+1)-dimensional tensor with the same length + as values. + values (array_list): Initial blocks for the tensor. Can be a list, + tuple, NumPy ``ndarray``, and other types that + represents a (1 + 2 + K)-dimensional tensor where ``K`` is the + number of dense dimensions. + size (list, tuple, :class:`torch.Size`, optional): Size of the + sparse tensor: ``(*batchsize, nrows * blocksize[0], ncols * + blocksize[1], *densesize)`` If not provided, the size will be + inferred as the minimum size big enough to hold all non-zero + blocks. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of + returned tensor. Default: if None, infers data type from + :attr:`values`. + device (:class:`torch.device`, optional): the desired device of + returned tensor. Default: if None, uses the current device + for the default tensor type (see + :func:`torch.set_default_device`). :attr:`device` will be + the CPU for CPU tensor types and the current CUDA device for + CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + check_invariants (bool, optional): If sparse tensor invariants are checked. + Default: as returned by :func:`torch.sparse.check_sparse_tensor_invariants.is_enabled`, + initially False. + + Example:: + + >>> ccol_indices = [0, 1, 2] + >>> row_indices = [0, 1] + >>> values = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]] + >>> torch.sparse_bsc_tensor(torch.tensor(ccol_indices, dtype=torch.int64), + ... torch.tensor(row_indices, dtype=torch.int64), + ... torch.tensor(values), dtype=torch.double) + tensor(ccol_indices=tensor([0, 1, 2]), + row_indices=tensor([0, 1]), + values=tensor([[[1., 2.], + [3., 4.]], + [[5., 6.], + [7., 8.]]]), size=(2, 2), nnz=2, dtype=torch.float64, + layout=torch.sparse_bsc) + """ + +def sparse_bsr_tensor( + crow_indices: Tensor | list, + col_indices: Tensor | list, + values: Tensor | list, + size: _size | None = None, + *, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + check_invariants: _bool | None = None, +) -> Tensor: + r""" + sparse_bsr_tensor(crow_indices, col_indices, values, size=None, *, dtype=None, device=None, pin_memory=False, requires_grad=False, check_invariants=None) -> Tensor + + Constructs a :ref:`sparse tensor in BSR (Block Compressed Sparse Row)) + ` with specified 2-dimensional blocks at the given + :attr:`crow_indices` and :attr:`col_indices`. Sparse matrix + multiplication operations in BSR format are typically faster than that + for sparse tensors in COO format. Make you have a look at :ref:`the + note on the data type of the indices `. + + .. note:: + + If the ``device`` argument is not specified the device of the given + :attr:`values` and indices tensor(s) must match. If, however, the + argument is specified the input Tensors will be converted to the + given device and in turn determine the device of the constructed + sparse tensor. + + Args: + crow_indices (array_like): (B+1)-dimensional array of size + ``(*batchsize, nrowblocks + 1)``. The last element of each + batch is the number of non-zeros. This tensor encodes the + block index in values and col_indices depending on where the + given row block starts. Each successive number in the tensor + subtracted by the number before it denotes the number of + blocks in a given row. + col_indices (array_like): Column block coordinates of each block + in values. (B+1)-dimensional tensor with the same length as + values. + values (array_list): Initial values for the tensor. Can be a list, + tuple, NumPy ``ndarray``, scalar, and other types that + represents a (1 + 2 + K)-dimensional tensor where ``K`` is the + number of dense dimensions. + size (list, tuple, :class:`torch.Size`, optional): Size of the + sparse tensor: ``(*batchsize, nrows * blocksize[0], ncols * + blocksize[1], *densesize)`` where ``blocksize == + values.shape[1:3]``. If not provided, the size will be + inferred as the minimum size big enough to hold all non-zero + blocks. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of + returned tensor. Default: if None, infers data type from + :attr:`values`. + device (:class:`torch.device`, optional): the desired device of + returned tensor. Default: if None, uses the current device + for the default tensor type (see + :func:`torch.set_default_device`). :attr:`device` will be + the CPU for CPU tensor types and the current CUDA device for + CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + check_invariants (bool, optional): If sparse tensor invariants are checked. + Default: as returned by :func:`torch.sparse.check_sparse_tensor_invariants.is_enabled`, + initially False. + + Example:: + + >>> crow_indices = [0, 1, 2] + >>> col_indices = [0, 1] + >>> values = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]] + >>> torch.sparse_bsr_tensor(torch.tensor(crow_indices, dtype=torch.int64), + ... torch.tensor(col_indices, dtype=torch.int64), + ... torch.tensor(values), dtype=torch.double) + tensor(crow_indices=tensor([0, 1, 2]), + col_indices=tensor([0, 1]), + values=tensor([[[1., 2.], + [3., 4.]], + [[5., 6.], + [7., 8.]]]), size=(2, 2), nnz=2, dtype=torch.float64, + layout=torch.sparse_bsr) + """ + +def sparse_compressed_tensor( + compressed_indices: Tensor | list, + plain_indices: Tensor | list, + values: Tensor | list, + size: _size | None = None, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + check_invariants: _bool | None = None, +) -> Tensor: + r""" + sparse_compressed_tensor(compressed_indices, plain_indices, values, size=None, *, dtype=None, layout=None, device=None, pin_memory=False, requires_grad=False, check_invariants=None) -> Tensor + + Constructs a :ref:`sparse tensor in Compressed Sparse format - CSR, + CSC, BSR, or BSC - ` with specified values at + the given :attr:`compressed_indices` and :attr:`plain_indices`. Sparse + matrix multiplication operations in Compressed Sparse format are + typically faster than that for sparse tensors in COO format. Make you + have a look at :ref:`the note on the data type of the indices + `. + + .. note:: + + If the ``device`` argument is not specified the device of the given + :attr:`values` and indices tensor(s) must match. If, however, the + argument is specified the input Tensors will be converted to the + given device and in turn determine the device of the constructed + sparse tensor. + + Args: + compressed_indices (array_like): (B+1)-dimensional array of size + ``(*batchsize, compressed_dim_size + 1)``. The last element of + each batch is the number of non-zero elements or blocks. This + tensor encodes the index in ``values`` and ``plain_indices`` + depending on where the given compressed dimension (row or + column) starts. Each successive number in the tensor + subtracted by the number before it denotes the number of + elements or blocks in a given compressed dimension. + plain_indices (array_like): Plain dimension (column or row) + coordinates of each element or block in values. (B+1)-dimensional + tensor with the same length as values. + + values (array_list): Initial values for the tensor. Can be a list, + tuple, NumPy ``ndarray``, scalar, and other types. that + represents a (1+K)-dimensional (for CSR and CSC layouts) or + (1+2+K)-dimensional tensor (for BSR and BSC layouts) where + ``K`` is the number of dense dimensions. + size (list, tuple, :class:`torch.Size`, optional): Size of the + sparse tensor: ``(*batchsize, nrows * blocksize[0], ncols * + blocksize[1], *densesize)`` where ``blocksize[0] == + blocksize[1] == 1`` for CSR and CSC formats. If not provided, + the size will be inferred as the minimum size big enough to + hold all non-zero elements or blocks. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of + returned tensor. Default: if None, infers data type from + :attr:`values`. + layout (:class:`torch.layout`, required): the desired layout of + returned tensor: :attr:`torch.sparse_csr`, + :attr:`torch.sparse_csc`, :attr:`torch.sparse_bsr`, or + :attr:`torch.sparse_bsc`. + device (:class:`torch.device`, optional): the desired device of + returned tensor. Default: if None, uses the current device + for the default tensor type (see + :func:`torch.set_default_device`). :attr:`device` will be + the CPU for CPU tensor types and the current CUDA device for + CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + check_invariants (bool, optional): If sparse tensor invariants are checked. + Default: as returned by :func:`torch.sparse.check_sparse_tensor_invariants.is_enabled`, + initially False. + + Example:: + + >>> compressed_indices = [0, 2, 4] + >>> plain_indices = [0, 1, 0, 1] + >>> values = [1, 2, 3, 4] + >>> torch.sparse_compressed_tensor(torch.tensor(compressed_indices, dtype=torch.int64), + ... torch.tensor(plain_indices, dtype=torch.int64), + ... torch.tensor(values), dtype=torch.double, layout=torch.sparse_csr) + tensor(crow_indices=tensor([0, 2, 4]), + col_indices=tensor([0, 1, 0, 1]), + values=tensor([1., 2., 3., 4.]), size=(2, 2), nnz=4, + dtype=torch.float64, layout=torch.sparse_csr) + """ + +def sparse_coo_tensor( + indices: Tensor, + values: Tensor | list, + size: _size | None = None, + *, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + check_invariants: _bool | None = None, + is_coalesced: _bool | None = None, +) -> Tensor: + r""" + sparse_coo_tensor(indices, values, size=None, *, dtype=None, device=None, pin_memory=False, requires_grad=False, check_invariants=None, is_coalesced=None) -> Tensor + + Constructs a :ref:`sparse tensor in COO(rdinate) format + ` with specified values at the given + :attr:`indices`. + + .. note:: + + This function returns an :ref:`uncoalesced tensor + ` when :attr:`is_coalesced` is + unspecified or ``None``. + + .. note:: + + If the ``device`` argument is not specified the device of the given + :attr:`values` and indices tensor(s) must match. If, however, the + argument is specified the input Tensors will be converted to the + given device and in turn determine the device of the constructed + sparse tensor. + + Args: + indices (array_like): Initial data for the tensor. Can be a list, tuple, + NumPy ``ndarray``, scalar, and other types. Will be cast to a :class:`torch.LongTensor` + internally. The indices are the coordinates of the non-zero values in the matrix, and thus + should be two-dimensional where the first dimension is the number of tensor dimensions and + the second dimension is the number of non-zero values. + values (array_like): Initial values for the tensor. Can be a list, tuple, + NumPy ``ndarray``, scalar, and other types. + size (list, tuple, or :class:`torch.Size`, optional): Size of the sparse tensor. If not + provided the size will be inferred as the minimum size big enough to hold all non-zero + elements. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if None, infers data type from :attr:`values`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if None, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + check_invariants (bool, optional): If sparse tensor invariants are checked. + Default: as returned by :func:`torch.sparse.check_sparse_tensor_invariants.is_enabled`, + initially False. + is_coalesced (bool, optional): When``True``, the caller is + responsible for providing tensor indices that correspond to a + coalesced tensor. If the :attr:`check_invariants` flag is + False, no error will be raised if the prerequisites are not + met and this will lead to silently incorrect results. To force + coalescion please use :meth:`coalesce` on the resulting + Tensor. + Default: None: except for trivial cases (e.g. nnz < 2) the + resulting Tensor has is_coalesced set to ``False```. + + Example:: + + >>> i = torch.tensor([[0, 1, 1], + ... [2, 0, 2]]) + >>> v = torch.tensor([3, 4, 5], dtype=torch.float32) + >>> torch.sparse_coo_tensor(i, v, [2, 4]) + tensor(indices=tensor([[0, 1, 1], + [2, 0, 2]]), + values=tensor([3., 4., 5.]), + size=(2, 4), nnz=3, layout=torch.sparse_coo) + + >>> torch.sparse_coo_tensor(i, v) # Shape inference + tensor(indices=tensor([[0, 1, 1], + [2, 0, 2]]), + values=tensor([3., 4., 5.]), + size=(2, 3), nnz=3, layout=torch.sparse_coo) + + >>> torch.sparse_coo_tensor(i, v, [2, 4], + ... dtype=torch.float64, + ... device=torch.device('cuda:0')) + tensor(indices=tensor([[0, 1, 1], + [2, 0, 2]]), + values=tensor([3., 4., 5.]), + device='cuda:0', size=(2, 4), nnz=3, dtype=torch.float64, + layout=torch.sparse_coo) + + # Create an empty sparse tensor with the following invariants: + # 1. sparse_dim + dense_dim = len(SparseTensor.shape) + # 2. SparseTensor._indices().shape = (sparse_dim, nnz) + # 3. SparseTensor._values().shape = (nnz, SparseTensor.shape[sparse_dim:]) + # + # For instance, to create an empty sparse tensor with nnz = 0, dense_dim = 0 and + # sparse_dim = 1 (hence indices is a 2D tensor of shape = (1, 0)) + >>> S = torch.sparse_coo_tensor(torch.empty([1, 0]), [], [1]) + tensor(indices=tensor([], size=(1, 0)), + values=tensor([], size=(0,)), + size=(1,), nnz=0, layout=torch.sparse_coo) + + # and to create an empty sparse tensor with nnz = 0, dense_dim = 1 and + # sparse_dim = 1 + >>> S = torch.sparse_coo_tensor(torch.empty([1, 0]), torch.empty([0, 2]), [1, 2]) + tensor(indices=tensor([], size=(1, 0)), + values=tensor([], size=(0, 2)), + size=(1, 2), nnz=0, layout=torch.sparse_coo) + + .. _torch.sparse: https://pytorch.org/docs/stable/sparse.html + """ + +def sparse_csc_tensor( + ccol_indices: Tensor | list, + row_indices: Tensor | list, + values: Tensor | list, + size: _size | None = None, + *, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + check_invariants: _bool | None = None, +) -> Tensor: + r""" + sparse_csc_tensor(ccol_indices, row_indices, values, size=None, *, dtype=None, device=None, pin_memory=False, requires_grad=False, check_invariants=None) -> Tensor + + Constructs a :ref:`sparse tensor in CSC (Compressed Sparse Column) + ` with specified values at the given + :attr:`ccol_indices` and :attr:`row_indices`. Sparse matrix + multiplication operations in CSC format are typically faster than that + for sparse tensors in COO format. Make you have a look at :ref:`the + note on the data type of the indices `. + + .. note:: + + If the ``device`` argument is not specified the device of the given + :attr:`values` and indices tensor(s) must match. If, however, the + argument is specified the input Tensors will be converted to the + given device and in turn determine the device of the constructed + sparse tensor. + + Args: + ccol_indices (array_like): (B+1)-dimensional array of size + ``(*batchsize, ncols + 1)``. The last element of each batch + is the number of non-zeros. This tensor encodes the index in + values and row_indices depending on where the given column + starts. Each successive number in the tensor subtracted by the + number before it denotes the number of elements in a given + column. + row_indices (array_like): Row coordinates of each element in + values. (B+1)-dimensional tensor with the same length as + values. + values (array_list): Initial values for the tensor. Can be a list, + tuple, NumPy ``ndarray``, scalar, and other types that + represents a (1+K)-dimensional tensor where ``K`` is the number + of dense dimensions. + size (list, tuple, :class:`torch.Size`, optional): Size of the + sparse tensor: ``(*batchsize, nrows, ncols, *densesize)``. If + not provided, the size will be inferred as the minimum size + big enough to hold all non-zero elements. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of + returned tensor. Default: if None, infers data type from + :attr:`values`. + device (:class:`torch.device`, optional): the desired device of + returned tensor. Default: if None, uses the current device + for the default tensor type (see + :func:`torch.set_default_device`). :attr:`device` will be + the CPU for CPU tensor types and the current CUDA device for + CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + check_invariants (bool, optional): If sparse tensor invariants are checked. + Default: as returned by :func:`torch.sparse.check_sparse_tensor_invariants.is_enabled`, + initially False. + + Example:: + + >>> ccol_indices = [0, 2, 4] + >>> row_indices = [0, 1, 0, 1] + >>> values = [1, 2, 3, 4] + >>> torch.sparse_csc_tensor(torch.tensor(ccol_indices, dtype=torch.int64), + ... torch.tensor(row_indices, dtype=torch.int64), + ... torch.tensor(values), dtype=torch.double) + tensor(ccol_indices=tensor([0, 2, 4]), + row_indices=tensor([0, 1, 0, 1]), + values=tensor([1., 2., 3., 4.]), size=(2, 2), nnz=4, + dtype=torch.float64, layout=torch.sparse_csc) + """ + +def sparse_csr_tensor( + crow_indices: Tensor | list, + col_indices: Tensor | list, + values: Tensor | list, + size: _size | None = None, + *, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + check_invariants: _bool | None = None, +) -> Tensor: + r""" + sparse_csr_tensor(crow_indices, col_indices, values, size=None, *, dtype=None, device=None, pin_memory=False, requires_grad=False, check_invariants=None) -> Tensor + + Constructs a :ref:`sparse tensor in CSR (Compressed Sparse Row) ` with specified + values at the given :attr:`crow_indices` and :attr:`col_indices`. Sparse matrix multiplication operations + in CSR format are typically faster than that for sparse tensors in COO format. Make you have a look + at :ref:`the note on the data type of the indices `. + + .. note:: + + If the ``device`` argument is not specified the device of the given + :attr:`values` and indices tensor(s) must match. If, however, the + argument is specified the input Tensors will be converted to the + given device and in turn determine the device of the constructed + sparse tensor. + + Args: + crow_indices (array_like): (B+1)-dimensional array of size + ``(*batchsize, nrows + 1)``. The last element of each batch + is the number of non-zeros. This tensor encodes the index in + values and col_indices depending on where the given row + starts. Each successive number in the tensor subtracted by the + number before it denotes the number of elements in a given + row. + col_indices (array_like): Column coordinates of each element in + values. (B+1)-dimensional tensor with the same length + as values. + values (array_list): Initial values for the tensor. Can be a list, + tuple, NumPy ``ndarray``, scalar, and other types that + represents a (1+K)-dimensional tensor where ``K`` is the number + of dense dimensions. + size (list, tuple, :class:`torch.Size`, optional): Size of the + sparse tensor: ``(*batchsize, nrows, ncols, *densesize)``. If + not provided, the size will be inferred as the minimum size + big enough to hold all non-zero elements. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of + returned tensor. Default: if None, infers data type from + :attr:`values`. + device (:class:`torch.device`, optional): the desired device of + returned tensor. Default: if None, uses the current device + for the default tensor type (see + :func:`torch.set_default_device`). :attr:`device` will be + the CPU for CPU tensor types and the current CUDA device for + CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + check_invariants (bool, optional): If sparse tensor invariants are checked. + Default: as returned by :func:`torch.sparse.check_sparse_tensor_invariants.is_enabled`, + initially False. + + Example:: + + >>> crow_indices = [0, 2, 4] + >>> col_indices = [0, 1, 0, 1] + >>> values = [1, 2, 3, 4] + >>> torch.sparse_csr_tensor(torch.tensor(crow_indices, dtype=torch.int64), + ... torch.tensor(col_indices, dtype=torch.int64), + ... torch.tensor(values), dtype=torch.double) + tensor(crow_indices=tensor([0, 2, 4]), + col_indices=tensor([0, 1, 0, 1]), + values=tensor([1., 2., 3., 4.]), size=(2, 2), nnz=4, + dtype=torch.float64, layout=torch.sparse_csr) + """ + +def split_copy( + input: Tensor, + split_size: _int | SymInt, + dim: _int = 0, + *, + out: tuple[Tensor, ...] | list[Tensor] | None = None, +) -> None: + r""" + Performs the same operation as :func:`torch.split`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def split_with_sizes( + input: Tensor, + split_sizes: Sequence[_int | SymInt], + dim: _int = 0, +) -> tuple[Tensor, ...]: ... +def split_with_sizes_copy( + input: Tensor, + split_sizes: Sequence[_int | SymInt], + dim: _int = 0, + *, + out: tuple[Tensor, ...] | list[Tensor] | None = None, +) -> None: + r""" + Performs the same operation as :func:`torch.split_with_sizes`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def spmm(input: Tensor, mat2: Tensor) -> Tensor: ... +def sqrt(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + sqrt(input, *, out=None) -> Tensor + + Returns a new tensor with the square-root of the elements of :attr:`input`. + + .. math:: + \text{out}_{i} = \sqrt{\text{input}_{i}} + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-2.0755, 1.0226, 0.0831, 0.4806]) + >>> torch.sqrt(a) + tensor([ nan, 1.0112, 0.2883, 0.6933]) + """ + +def sqrt_(input: Tensor) -> Tensor: ... +def square(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + square(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Returns a new tensor with the square of the elements of :attr:`input`. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-2.0755, 1.0226, 0.0831, 0.4806]) + >>> torch.square(a) + tensor([ 4.3077, 1.0457, 0.0069, 0.2310]) + """ + +def square_(input: Tensor) -> Tensor: ... +@overload +def squeeze(input: Tensor) -> Tensor: + r""" + squeeze(input: Tensor, dim: Optional[Union[int, List[int]]]) -> Tensor + + Returns a tensor with all specified dimensions of :attr:`input` of size `1` removed. + + For example, if `input` is of shape: + :math:`(A \times 1 \times B \times C \times 1 \times D)` then the `input.squeeze()` + will be of shape: :math:`(A \times B \times C \times D)`. + + When :attr:`dim` is given, a squeeze operation is done only in the given + dimension(s). If `input` is of shape: :math:`(A \times 1 \times B)`, + ``squeeze(input, 0)`` leaves the tensor unchanged, but ``squeeze(input, 1)`` + will squeeze the tensor to the shape :math:`(A \times B)`. + + .. note:: The returned tensor shares the storage with the input tensor, + so changing the contents of one will change the contents of the other. + + .. warning:: If the tensor has a batch dimension of size 1, then `squeeze(input)` + will also remove the batch dimension, which can lead to unexpected + errors. Consider specifying only the dims you wish to be squeezed. + + Args: + input (Tensor): the input tensor. + dim (int or tuple of ints, optional): if given, the input will be squeezed + only in the specified dimensions. + + .. versionchanged:: 2.0 + :attr:`dim` now accepts tuples of dimensions. + + Example:: + + >>> x = torch.zeros(2, 1, 2, 1, 2) + >>> x.size() + torch.Size([2, 1, 2, 1, 2]) + >>> y = torch.squeeze(x) + >>> y.size() + torch.Size([2, 2, 2]) + >>> y = torch.squeeze(x, 0) + >>> y.size() + torch.Size([2, 1, 2, 1, 2]) + >>> y = torch.squeeze(x, 1) + >>> y.size() + torch.Size([2, 2, 1, 2]) + >>> y = torch.squeeze(x, (1, 2, 3)) + torch.Size([2, 2, 2]) + """ + +@overload +def squeeze(input: Tensor, dim: _int) -> Tensor: + r""" + squeeze(input: Tensor, dim: Optional[Union[int, List[int]]]) -> Tensor + + Returns a tensor with all specified dimensions of :attr:`input` of size `1` removed. + + For example, if `input` is of shape: + :math:`(A \times 1 \times B \times C \times 1 \times D)` then the `input.squeeze()` + will be of shape: :math:`(A \times B \times C \times D)`. + + When :attr:`dim` is given, a squeeze operation is done only in the given + dimension(s). If `input` is of shape: :math:`(A \times 1 \times B)`, + ``squeeze(input, 0)`` leaves the tensor unchanged, but ``squeeze(input, 1)`` + will squeeze the tensor to the shape :math:`(A \times B)`. + + .. note:: The returned tensor shares the storage with the input tensor, + so changing the contents of one will change the contents of the other. + + .. warning:: If the tensor has a batch dimension of size 1, then `squeeze(input)` + will also remove the batch dimension, which can lead to unexpected + errors. Consider specifying only the dims you wish to be squeezed. + + Args: + input (Tensor): the input tensor. + dim (int or tuple of ints, optional): if given, the input will be squeezed + only in the specified dimensions. + + .. versionchanged:: 2.0 + :attr:`dim` now accepts tuples of dimensions. + + Example:: + + >>> x = torch.zeros(2, 1, 2, 1, 2) + >>> x.size() + torch.Size([2, 1, 2, 1, 2]) + >>> y = torch.squeeze(x) + >>> y.size() + torch.Size([2, 2, 2]) + >>> y = torch.squeeze(x, 0) + >>> y.size() + torch.Size([2, 1, 2, 1, 2]) + >>> y = torch.squeeze(x, 1) + >>> y.size() + torch.Size([2, 2, 1, 2]) + >>> y = torch.squeeze(x, (1, 2, 3)) + torch.Size([2, 2, 2]) + """ + +@overload +def squeeze(input: Tensor, dim: _size) -> Tensor: + r""" + squeeze(input: Tensor, dim: Optional[Union[int, List[int]]]) -> Tensor + + Returns a tensor with all specified dimensions of :attr:`input` of size `1` removed. + + For example, if `input` is of shape: + :math:`(A \times 1 \times B \times C \times 1 \times D)` then the `input.squeeze()` + will be of shape: :math:`(A \times B \times C \times D)`. + + When :attr:`dim` is given, a squeeze operation is done only in the given + dimension(s). If `input` is of shape: :math:`(A \times 1 \times B)`, + ``squeeze(input, 0)`` leaves the tensor unchanged, but ``squeeze(input, 1)`` + will squeeze the tensor to the shape :math:`(A \times B)`. + + .. note:: The returned tensor shares the storage with the input tensor, + so changing the contents of one will change the contents of the other. + + .. warning:: If the tensor has a batch dimension of size 1, then `squeeze(input)` + will also remove the batch dimension, which can lead to unexpected + errors. Consider specifying only the dims you wish to be squeezed. + + Args: + input (Tensor): the input tensor. + dim (int or tuple of ints, optional): if given, the input will be squeezed + only in the specified dimensions. + + .. versionchanged:: 2.0 + :attr:`dim` now accepts tuples of dimensions. + + Example:: + + >>> x = torch.zeros(2, 1, 2, 1, 2) + >>> x.size() + torch.Size([2, 1, 2, 1, 2]) + >>> y = torch.squeeze(x) + >>> y.size() + torch.Size([2, 2, 2]) + >>> y = torch.squeeze(x, 0) + >>> y.size() + torch.Size([2, 1, 2, 1, 2]) + >>> y = torch.squeeze(x, 1) + >>> y.size() + torch.Size([2, 2, 1, 2]) + >>> y = torch.squeeze(x, (1, 2, 3)) + torch.Size([2, 2, 2]) + """ + +@overload +def squeeze(input: Tensor, dim: str | EllipsisType | None) -> Tensor: + r""" + squeeze(input: Tensor, dim: Optional[Union[int, List[int]]]) -> Tensor + + Returns a tensor with all specified dimensions of :attr:`input` of size `1` removed. + + For example, if `input` is of shape: + :math:`(A \times 1 \times B \times C \times 1 \times D)` then the `input.squeeze()` + will be of shape: :math:`(A \times B \times C \times D)`. + + When :attr:`dim` is given, a squeeze operation is done only in the given + dimension(s). If `input` is of shape: :math:`(A \times 1 \times B)`, + ``squeeze(input, 0)`` leaves the tensor unchanged, but ``squeeze(input, 1)`` + will squeeze the tensor to the shape :math:`(A \times B)`. + + .. note:: The returned tensor shares the storage with the input tensor, + so changing the contents of one will change the contents of the other. + + .. warning:: If the tensor has a batch dimension of size 1, then `squeeze(input)` + will also remove the batch dimension, which can lead to unexpected + errors. Consider specifying only the dims you wish to be squeezed. + + Args: + input (Tensor): the input tensor. + dim (int or tuple of ints, optional): if given, the input will be squeezed + only in the specified dimensions. + + .. versionchanged:: 2.0 + :attr:`dim` now accepts tuples of dimensions. + + Example:: + + >>> x = torch.zeros(2, 1, 2, 1, 2) + >>> x.size() + torch.Size([2, 1, 2, 1, 2]) + >>> y = torch.squeeze(x) + >>> y.size() + torch.Size([2, 2, 2]) + >>> y = torch.squeeze(x, 0) + >>> y.size() + torch.Size([2, 1, 2, 1, 2]) + >>> y = torch.squeeze(x, 1) + >>> y.size() + torch.Size([2, 2, 1, 2]) + >>> y = torch.squeeze(x, (1, 2, 3)) + torch.Size([2, 2, 2]) + """ + +@overload +def squeeze_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + Performs the same operation as :func:`torch.squeeze`, but all output tensors + are freshly created instead of aliasing the input. + """ + +@overload +def squeeze_copy( + input: Tensor, + dim: _int, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.squeeze`, but all output tensors + are freshly created instead of aliasing the input. + """ + +@overload +def squeeze_copy( + input: Tensor, + dim: _size, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.squeeze`, but all output tensors + are freshly created instead of aliasing the input. + """ + +@overload +def sspaddmm( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + mat1: Tensor, + mat2: Tensor, +) -> Tensor: + r""" + sspaddmm(input, mat1, mat2, *, beta=1, alpha=1, out=None) -> Tensor + + Matrix multiplies a sparse tensor :attr:`mat1` with a dense tensor + :attr:`mat2`, then adds the sparse tensor :attr:`input` to the result. + + Note: This function is equivalent to :func:`torch.addmm`, except + :attr:`input` and :attr:`mat1` are sparse. + + Args: + input (Tensor): a sparse matrix to be added + mat1 (Tensor): a sparse matrix to be matrix multiplied + mat2 (Tensor): a dense matrix to be matrix multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`mat` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat1 @ mat2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + """ + +@overload +def sspaddmm( + input: Tensor, + mat1: Tensor, + mat2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + sspaddmm(input, mat1, mat2, *, beta=1, alpha=1, out=None) -> Tensor + + Matrix multiplies a sparse tensor :attr:`mat1` with a dense tensor + :attr:`mat2`, then adds the sparse tensor :attr:`input` to the result. + + Note: This function is equivalent to :func:`torch.addmm`, except + :attr:`input` and :attr:`mat1` are sparse. + + Args: + input (Tensor): a sparse matrix to be added + mat1 (Tensor): a sparse matrix to be matrix multiplied + mat2 (Tensor): a dense matrix to be matrix multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`mat` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat1 @ mat2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + """ + +@overload +def sspaddmm( + beta: Number | _complex, + self: Tensor, + mat1: Tensor, + mat2: Tensor, +) -> Tensor: + r""" + sspaddmm(input, mat1, mat2, *, beta=1, alpha=1, out=None) -> Tensor + + Matrix multiplies a sparse tensor :attr:`mat1` with a dense tensor + :attr:`mat2`, then adds the sparse tensor :attr:`input` to the result. + + Note: This function is equivalent to :func:`torch.addmm`, except + :attr:`input` and :attr:`mat1` are sparse. + + Args: + input (Tensor): a sparse matrix to be added + mat1 (Tensor): a sparse matrix to be matrix multiplied + mat2 (Tensor): a dense matrix to be matrix multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`mat` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat1 @ mat2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + """ + +def stack( + tensors: tuple[Tensor, ...] | list[Tensor], + dim: _int = 0, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + stack(tensors, dim=0, *, out=None) -> Tensor + + Concatenates a sequence of tensors along a new dimension. + + All tensors need to be of the same size. + + .. seealso:: + + :func:`torch.cat` concatenates the given sequence along an existing dimension. + + Arguments: + tensors (sequence of Tensors): sequence of tensors to concatenate + dim (int, optional): dimension to insert. Has to be between 0 and the number + of dimensions of concatenated tensors (inclusive). Default: 0 + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> x = torch.randn(2, 3) + >>> x + tensor([[ 0.3367, 0.1288, 0.2345], + [ 0.2303, -1.1229, -0.1863]]) + >>> torch.stack((x, x)) # same as torch.stack((x, x), dim=0) + tensor([[[ 0.3367, 0.1288, 0.2345], + [ 0.2303, -1.1229, -0.1863]], + + [[ 0.3367, 0.1288, 0.2345], + [ 0.2303, -1.1229, -0.1863]]]) + >>> torch.stack((x, x)).size() + torch.Size([2, 2, 3]) + >>> torch.stack((x, x), dim=1) + tensor([[[ 0.3367, 0.1288, 0.2345], + [ 0.3367, 0.1288, 0.2345]], + + [[ 0.2303, -1.1229, -0.1863], + [ 0.2303, -1.1229, -0.1863]]]) + >>> torch.stack((x, x), dim=2) + tensor([[[ 0.3367, 0.3367], + [ 0.1288, 0.1288], + [ 0.2345, 0.2345]], + + [[ 0.2303, 0.2303], + [-1.1229, -1.1229], + [-0.1863, -0.1863]]]) + >>> torch.stack((x, x), dim=-1) + tensor([[[ 0.3367, 0.3367], + [ 0.1288, 0.1288], + [ 0.2345, 0.2345]], + + [[ 0.2303, 0.2303], + [-1.1229, -1.1229], + [-0.1863, -0.1863]]]) + """ + +@overload +def std( + input: Tensor, + dim: _int | _size | None, + unbiased: _bool = True, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + std(input, dim=None, *, correction=1, keepdim=False, out=None) -> Tensor + + Calculates the standard deviation over the dimensions specified by :attr:`dim`. + :attr:`dim` can be a single dimension, list of dimensions, or ``None`` to + reduce over all dimensions. + + The standard deviation (:math:`\sigma`) is calculated as + + .. math:: \sigma = \sqrt{\frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2} + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.std(a, dim=1, keepdim=True) + tensor([[1.0311], + [0.7477], + [1.2204], + [0.9087]]) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def std( + input: Tensor, + dim: _int | _size | None = None, + *, + correction: Number | _complex | None = None, + keepdim: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + std(input, dim=None, *, correction=1, keepdim=False, out=None) -> Tensor + + Calculates the standard deviation over the dimensions specified by :attr:`dim`. + :attr:`dim` can be a single dimension, list of dimensions, or ``None`` to + reduce over all dimensions. + + The standard deviation (:math:`\sigma`) is calculated as + + .. math:: \sigma = \sqrt{\frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2} + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.std(a, dim=1, keepdim=True) + tensor([[1.0311], + [0.7477], + [1.2204], + [0.9087]]) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def std(input: Tensor, unbiased: _bool = True) -> Tensor: + r""" + std(input, dim=None, *, correction=1, keepdim=False, out=None) -> Tensor + + Calculates the standard deviation over the dimensions specified by :attr:`dim`. + :attr:`dim` can be a single dimension, list of dimensions, or ``None`` to + reduce over all dimensions. + + The standard deviation (:math:`\sigma`) is calculated as + + .. math:: \sigma = \sqrt{\frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2} + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.std(a, dim=1, keepdim=True) + tensor([[1.0311], + [0.7477], + [1.2204], + [0.9087]]) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def std( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + *, + correction: Number | _complex | None = None, + keepdim: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + std(input, dim=None, *, correction=1, keepdim=False, out=None) -> Tensor + + Calculates the standard deviation over the dimensions specified by :attr:`dim`. + :attr:`dim` can be a single dimension, list of dimensions, or ``None`` to + reduce over all dimensions. + + The standard deviation (:math:`\sigma`) is calculated as + + .. math:: \sigma = \sqrt{\frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2} + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.std(a, dim=1, keepdim=True) + tensor([[1.0311], + [0.7477], + [1.2204], + [0.9087]]) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def std( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + unbiased: _bool = True, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + std(input, dim=None, *, correction=1, keepdim=False, out=None) -> Tensor + + Calculates the standard deviation over the dimensions specified by :attr:`dim`. + :attr:`dim` can be a single dimension, list of dimensions, or ``None`` to + reduce over all dimensions. + + The standard deviation (:math:`\sigma`) is calculated as + + .. math:: \sigma = \sqrt{\frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2} + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.std(a, dim=1, keepdim=True) + tensor([[1.0311], + [0.7477], + [1.2204], + [0.9087]]) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def std_mean( + input: Tensor, + dim: _int | _size | None, + unbiased: _bool = True, + keepdim: _bool = False, +) -> tuple[Tensor, Tensor]: + r""" + std_mean(input, dim=None, *, correction=1, keepdim=False, out=None) -> (Tensor, Tensor) + + Calculates the standard deviation and mean over the dimensions specified by + :attr:`dim`. :attr:`dim` can be a single dimension, list of dimensions, or + ``None`` to reduce over all dimensions. + + The standard deviation (:math:`\sigma`) is calculated as + + .. math:: \sigma = \sqrt{\frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2} + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Returns: + A tuple (std, mean) containing the standard deviation and mean. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.std_mean(a, dim=0, keepdim=True) + (tensor([[1.2620, 1.0028, 1.0957, 0.6038]]), + tensor([[ 0.0645, 0.4485, 0.8707, -0.0665]])) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def std_mean( + input: Tensor, + dim: _int | _size | None = None, + *, + correction: Number | _complex | None = None, + keepdim: _bool = False, +) -> tuple[Tensor, Tensor]: + r""" + std_mean(input, dim=None, *, correction=1, keepdim=False, out=None) -> (Tensor, Tensor) + + Calculates the standard deviation and mean over the dimensions specified by + :attr:`dim`. :attr:`dim` can be a single dimension, list of dimensions, or + ``None`` to reduce over all dimensions. + + The standard deviation (:math:`\sigma`) is calculated as + + .. math:: \sigma = \sqrt{\frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2} + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Returns: + A tuple (std, mean) containing the standard deviation and mean. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.std_mean(a, dim=0, keepdim=True) + (tensor([[1.2620, 1.0028, 1.0957, 0.6038]]), + tensor([[ 0.0645, 0.4485, 0.8707, -0.0665]])) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def std_mean( + input: Tensor, + unbiased: _bool = True, +) -> tuple[Tensor, Tensor]: + r""" + std_mean(input, dim=None, *, correction=1, keepdim=False, out=None) -> (Tensor, Tensor) + + Calculates the standard deviation and mean over the dimensions specified by + :attr:`dim`. :attr:`dim` can be a single dimension, list of dimensions, or + ``None`` to reduce over all dimensions. + + The standard deviation (:math:`\sigma`) is calculated as + + .. math:: \sigma = \sqrt{\frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2} + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Returns: + A tuple (std, mean) containing the standard deviation and mean. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.std_mean(a, dim=0, keepdim=True) + (tensor([[1.2620, 1.0028, 1.0957, 0.6038]]), + tensor([[ 0.0645, 0.4485, 0.8707, -0.0665]])) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def std_mean( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + *, + correction: Number | _complex | None = None, + keepdim: _bool = False, +) -> tuple[Tensor, Tensor]: + r""" + std_mean(input, dim=None, *, correction=1, keepdim=False, out=None) -> (Tensor, Tensor) + + Calculates the standard deviation and mean over the dimensions specified by + :attr:`dim`. :attr:`dim` can be a single dimension, list of dimensions, or + ``None`` to reduce over all dimensions. + + The standard deviation (:math:`\sigma`) is calculated as + + .. math:: \sigma = \sqrt{\frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2} + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Returns: + A tuple (std, mean) containing the standard deviation and mean. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.std_mean(a, dim=0, keepdim=True) + (tensor([[1.2620, 1.0028, 1.0957, 0.6038]]), + tensor([[ 0.0645, 0.4485, 0.8707, -0.0665]])) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def std_mean( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + unbiased: _bool = True, + keepdim: _bool = False, +) -> tuple[Tensor, Tensor]: + r""" + std_mean(input, dim=None, *, correction=1, keepdim=False, out=None) -> (Tensor, Tensor) + + Calculates the standard deviation and mean over the dimensions specified by + :attr:`dim`. :attr:`dim` can be a single dimension, list of dimensions, or + ``None`` to reduce over all dimensions. + + The standard deviation (:math:`\sigma`) is calculated as + + .. math:: \sigma = \sqrt{\frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2} + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Returns: + A tuple (std, mean) containing the standard deviation and mean. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.std_mean(a, dim=0, keepdim=True) + (tensor([[1.2620, 1.0028, 1.0957, 0.6038]]), + tensor([[ 0.0645, 0.4485, 0.8707, -0.0665]])) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def sub( + input: Tensor | Number | _complex, + other: Tensor | Number | _complex, + *, + alpha: Number | _complex | None = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + sub(input, other, *, alpha=1, out=None) -> Tensor + + Subtracts :attr:`other`, scaled by :attr:`alpha`, from :attr:`input`. + + .. math:: + \text{{out}}_i = \text{{input}}_i - \text{{alpha}} \times \text{{other}}_i + + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer, float, and complex inputs. + + Args: + input (Tensor): the input tensor. + other (Tensor or Number): the tensor or number to subtract from :attr:`input`. + + Keyword args: + alpha (Number): the multiplier for :attr:`other`. + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor((1, 2)) + >>> b = torch.tensor((0, 1)) + >>> torch.sub(a, b, alpha=2) + tensor([1, 0]) + """ + +@overload +def sub(self: Tensor, alpha: Number | _complex, other: Tensor) -> Tensor: + r""" + sub(input, other, *, alpha=1, out=None) -> Tensor + + Subtracts :attr:`other`, scaled by :attr:`alpha`, from :attr:`input`. + + .. math:: + \text{{out}}_i = \text{{input}}_i - \text{{alpha}} \times \text{{other}}_i + + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer, float, and complex inputs. + + Args: + input (Tensor): the input tensor. + other (Tensor or Number): the tensor or number to subtract from :attr:`input`. + + Keyword args: + alpha (Number): the multiplier for :attr:`other`. + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor((1, 2)) + >>> b = torch.tensor((0, 1)) + >>> torch.sub(a, b, alpha=2) + tensor([1, 0]) + """ + +@overload +def sub( + self: Tensor, + alpha: Number | _complex, + other: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + sub(input, other, *, alpha=1, out=None) -> Tensor + + Subtracts :attr:`other`, scaled by :attr:`alpha`, from :attr:`input`. + + .. math:: + \text{{out}}_i = \text{{input}}_i - \text{{alpha}} \times \text{{other}}_i + + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer, float, and complex inputs. + + Args: + input (Tensor): the input tensor. + other (Tensor or Number): the tensor or number to subtract from :attr:`input`. + + Keyword args: + alpha (Number): the multiplier for :attr:`other`. + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor((1, 2)) + >>> b = torch.tensor((0, 1)) + >>> torch.sub(a, b, alpha=2) + tensor([1, 0]) + """ + +@overload +def subtract( + input: Tensor, + other: Tensor, + *, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + subtract(input, other, *, alpha=1, out=None) -> Tensor + + Alias for :func:`torch.sub`. + """ + +@overload +def subtract( + input: Tensor, + other: Number | _complex, + alpha: Number | _complex = 1, +) -> Tensor: + r""" + subtract(input, other, *, alpha=1, out=None) -> Tensor + + Alias for :func:`torch.sub`. + """ + +@overload +def sum(input: Tensor, *, dtype: _dtype | None = None) -> Tensor: + r""" + sum(input, *, dtype=None) -> Tensor + + Returns the sum of all elements in the :attr:`input` tensor. + + Args: + input (Tensor): the input tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + .. note:: Use the `dtype` argument if you need the result in a specific tensor type. + Otherwise, the result type may be automatically promoted (e.g., from `torch.int32` to `torch.int64`). + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.1133, -0.9567, 0.2958]]) + >>> torch.sum(a) + tensor(-0.5475) + + .. function:: sum(input, dim, keepdim=False, *, dtype=None) -> Tensor + :noindex: + + Returns the sum of each row of the :attr:`input` tensor in the given + dimension :attr:`dim`. If :attr:`dim` is a list of dimensions, + reduce over all of them. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.0569, -0.2475, 0.0737, -0.3429], + [-0.2993, 0.9138, 0.9337, -1.6864], + [ 0.1132, 0.7892, -0.1003, 0.5688], + [ 0.3637, -0.9906, -0.4752, -1.5197]]) + >>> torch.sum(a, 1) + tensor([-0.4598, -0.1381, 1.3708, -2.6217]) + >>> b = torch.arange(4 * 5 * 6).view(4, 5, 6) + >>> torch.sum(b, (2, 1)) + tensor([ 435., 1335., 2235., 3135.]) + """ + +@overload +def sum( + input: Tensor, + dim: _int | _size | None, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + sum(input, *, dtype=None) -> Tensor + + Returns the sum of all elements in the :attr:`input` tensor. + + Args: + input (Tensor): the input tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + .. note:: Use the `dtype` argument if you need the result in a specific tensor type. + Otherwise, the result type may be automatically promoted (e.g., from `torch.int32` to `torch.int64`). + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.1133, -0.9567, 0.2958]]) + >>> torch.sum(a) + tensor(-0.5475) + + .. function:: sum(input, dim, keepdim=False, *, dtype=None) -> Tensor + :noindex: + + Returns the sum of each row of the :attr:`input` tensor in the given + dimension :attr:`dim`. If :attr:`dim` is a list of dimensions, + reduce over all of them. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.0569, -0.2475, 0.0737, -0.3429], + [-0.2993, 0.9138, 0.9337, -1.6864], + [ 0.1132, 0.7892, -0.1003, 0.5688], + [ 0.3637, -0.9906, -0.4752, -1.5197]]) + >>> torch.sum(a, 1) + tensor([-0.4598, -0.1381, 1.3708, -2.6217]) + >>> b = torch.arange(4 * 5 * 6).view(4, 5, 6) + >>> torch.sum(b, (2, 1)) + tensor([ 435., 1335., 2235., 3135.]) + """ + +@overload +def sum( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + sum(input, *, dtype=None) -> Tensor + + Returns the sum of all elements in the :attr:`input` tensor. + + Args: + input (Tensor): the input tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + .. note:: Use the `dtype` argument if you need the result in a specific tensor type. + Otherwise, the result type may be automatically promoted (e.g., from `torch.int32` to `torch.int64`). + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.1133, -0.9567, 0.2958]]) + >>> torch.sum(a) + tensor(-0.5475) + + .. function:: sum(input, dim, keepdim=False, *, dtype=None) -> Tensor + :noindex: + + Returns the sum of each row of the :attr:`input` tensor in the given + dimension :attr:`dim`. If :attr:`dim` is a list of dimensions, + reduce over all of them. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.0569, -0.2475, 0.0737, -0.3429], + [-0.2993, 0.9138, 0.9337, -1.6864], + [ 0.1132, 0.7892, -0.1003, 0.5688], + [ 0.3637, -0.9906, -0.4752, -1.5197]]) + >>> torch.sum(a, 1) + tensor([-0.4598, -0.1381, 1.3708, -2.6217]) + >>> b = torch.arange(4 * 5 * 6).view(4, 5, 6) + >>> torch.sum(b, (2, 1)) + tensor([ 435., 1335., 2235., 3135.]) + """ + +def svd( + input: Tensor, + some: _bool = True, + compute_uv: _bool = True, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.svd: + r""" + svd(input, some=True, compute_uv=True, *, out=None) -> (Tensor, Tensor, Tensor) + + Computes the singular value decomposition of either a matrix or batch of + matrices :attr:`input`. The singular value decomposition is represented as a + namedtuple `(U, S, V)`, such that :attr:`input` :math:`= U \text{diag}(S) V^{\text{H}}`. + where :math:`V^{\text{H}}` is the transpose of `V` for real inputs, + and the conjugate transpose of `V` for complex inputs. + If :attr:`input` is a batch of matrices, then `U`, `S`, and `V` are also + batched with the same batch dimensions as :attr:`input`. + + If :attr:`some` is `True` (default), the method returns the reduced singular + value decomposition. In this case, if the last two dimensions of :attr:`input` are + `m` and `n`, then the returned `U` and `V` matrices will contain only + `min(n, m)` orthonormal columns. + + If :attr:`compute_uv` is `False`, the returned `U` and `V` will be + zero-filled matrices of shape `(m, m)` and `(n, n)` + respectively, and the same device as :attr:`input`. The argument :attr:`some` + has no effect when :attr:`compute_uv` is `False`. + + Supports :attr:`input` of float, double, cfloat and cdouble data types. + The dtypes of `U` and `V` are the same as :attr:`input`'s. `S` will + always be real-valued, even if :attr:`input` is complex. + + .. warning:: + + :func:`torch.svd` is deprecated in favor of :func:`torch.linalg.svd` + and will be removed in a future PyTorch release. + + ``U, S, V = torch.svd(A, some=some, compute_uv=True)`` (default) should be replaced with + + .. code:: python + + U, S, Vh = torch.linalg.svd(A, full_matrices=not some) + V = Vh.mH + + ``_, S, _ = torch.svd(A, some=some, compute_uv=False)`` should be replaced with + + .. code:: python + + S = torch.linalg.svdvals(A) + + .. note:: Differences with :func:`torch.linalg.svd`: + + * :attr:`some` is the opposite of + :func:`torch.linalg.svd`'s :attr:`full_matrices`. Note that + default value for both is `True`, so the default behavior is + effectively the opposite. + * :func:`torch.svd` returns `V`, whereas :func:`torch.linalg.svd` returns + `Vh`, that is, :math:`V^{\text{H}}`. + * If :attr:`compute_uv` is `False`, :func:`torch.svd` returns zero-filled + tensors for `U` and `Vh`, whereas :func:`torch.linalg.svd` returns + empty tensors. + + .. note:: The singular values are returned in descending order. If :attr:`input` is a batch of matrices, + then the singular values of each matrix in the batch are returned in descending order. + + .. note:: The `S` tensor can only be used to compute gradients if :attr:`compute_uv` is `True`. + + .. note:: When :attr:`some` is `False`, the gradients on `U[..., :, min(m, n):]` + and `V[..., :, min(m, n):]` will be ignored in the backward pass, as those vectors + can be arbitrary bases of the corresponding subspaces. + + .. note:: The implementation of :func:`torch.linalg.svd` on CPU uses LAPACK's routine `?gesdd` + (a divide-and-conquer algorithm) instead of `?gesvd` for speed. Analogously, + on GPU, it uses cuSOLVER's routines `gesvdj` and `gesvdjBatched` on CUDA 10.1.243 + and later, and MAGMA's routine `gesdd` on earlier versions of CUDA. + + .. note:: The returned `U` will not be contiguous. The matrix (or batch of matrices) will + be represented as a column-major matrix (i.e. Fortran-contiguous). + + .. warning:: The gradients with respect to `U` and `V` will only be finite when the input does not + have zero nor repeated singular values. + + .. warning:: If the distance between any two singular values is close to zero, the gradients with respect to + `U` and `V` will be numerically unstable, as they depends on + :math:`\frac{1}{\min_{i \neq j} \sigma_i^2 - \sigma_j^2}`. The same happens when the matrix + has small singular values, as these gradients also depend on `S^{-1}`. + + .. warning:: For complex-valued :attr:`input` the singular value decomposition is not unique, + as `U` and `V` may be multiplied by an arbitrary phase factor :math:`e^{i \phi}` on every column. + The same happens when :attr:`input` has repeated singular values, where one may multiply + the columns of the spanning subspace in `U` and `V` by a rotation matrix + and `the resulting vectors will span the same subspace`_. + Different platforms, like NumPy, or inputs on different device types, + may produce different `U` and `V` tensors. + + Args: + input (Tensor): the input tensor of size `(*, m, n)` where `*` is zero or more + batch dimensions consisting of `(m, n)` matrices. + some (bool, optional): controls whether to compute the reduced or full decomposition, and + consequently, the shape of returned `U` and `V`. Default: `True`. + compute_uv (bool, optional): controls whether to compute `U` and `V`. Default: `True`. + + Keyword args: + out (tuple, optional): the output tuple of tensors + + Example:: + + >>> a = torch.randn(5, 3) + >>> a + tensor([[ 0.2364, -0.7752, 0.6372], + [ 1.7201, 0.7394, -0.0504], + [-0.3371, -1.0584, 0.5296], + [ 0.3550, -0.4022, 1.5569], + [ 0.2445, -0.0158, 1.1414]]) + >>> u, s, v = torch.svd(a) + >>> u + tensor([[ 0.4027, 0.0287, 0.5434], + [-0.1946, 0.8833, 0.3679], + [ 0.4296, -0.2890, 0.5261], + [ 0.6604, 0.2717, -0.2618], + [ 0.4234, 0.2481, -0.4733]]) + >>> s + tensor([2.3289, 2.0315, 0.7806]) + >>> v + tensor([[-0.0199, 0.8766, 0.4809], + [-0.5080, 0.4054, -0.7600], + [ 0.8611, 0.2594, -0.4373]]) + >>> torch.dist(a, torch.mm(torch.mm(u, torch.diag(s)), v.t())) + tensor(8.6531e-07) + >>> a_big = torch.randn(7, 5, 3) + >>> u, s, v = torch.svd(a_big) + >>> torch.dist(a_big, torch.matmul(torch.matmul(u, torch.diag_embed(s)), v.mT)) + tensor(2.6503e-06) + + .. _the resulting vectors will span the same subspace: + (https://en.wikipedia.org/wiki/Singular_value_decomposition#Singular_values,_singular_vectors,_and_their_relation_to_the_SVD) + """ + +def swapaxes(input: Tensor, axis0: _int, axis1: _int) -> Tensor: + r""" + swapaxes(input, axis0, axis1) -> Tensor + + Alias for :func:`torch.transpose`. + + This function is equivalent to NumPy's swapaxes function. + + Examples:: + + >>> x = torch.tensor([[[0,1],[2,3]],[[4,5],[6,7]]]) + >>> x + tensor([[[0, 1], + [2, 3]], + + [[4, 5], + [6, 7]]]) + >>> torch.swapaxes(x, 0, 1) + tensor([[[0, 1], + [4, 5]], + + [[2, 3], + [6, 7]]]) + >>> torch.swapaxes(x, 0, 2) + tensor([[[0, 4], + [2, 6]], + + [[1, 5], + [3, 7]]]) + """ + +def swapdims(input: Tensor, dim0: _int, dim1: _int) -> Tensor: + r""" + swapdims(input, dim0, dim1) -> Tensor + + Alias for :func:`torch.transpose`. + + This function is equivalent to NumPy's swapaxes function. + + Examples:: + + >>> x = torch.tensor([[[0,1],[2,3]],[[4,5],[6,7]]]) + >>> x + tensor([[[0, 1], + [2, 3]], + + [[4, 5], + [6, 7]]]) + >>> torch.swapdims(x, 0, 1) + tensor([[[0, 1], + [4, 5]], + + [[2, 3], + [6, 7]]]) + >>> torch.swapdims(x, 0, 2) + tensor([[[0, 4], + [2, 6]], + + [[1, 5], + [3, 7]]]) + """ + +def sym_constrain_range( + size: Number | _complex, + *, + min: _int | None = None, + max: _int | None = None, +) -> None: ... +def sym_constrain_range_for_size( + size: Number | _complex, + *, + min: _int | None = None, + max: _int | None = None, +) -> None: ... +def t(input: Tensor) -> Tensor: + r""" + t(input) -> Tensor + + Expects :attr:`input` to be <= 2-D tensor and transposes dimensions 0 + and 1. + + 0-D and 1-D tensors are returned as is. When input is a 2-D tensor this + is equivalent to ``transpose(input, 0, 1)``. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> x = torch.randn(()) + >>> x + tensor(0.1995) + >>> torch.t(x) + tensor(0.1995) + >>> x = torch.randn(3) + >>> x + tensor([ 2.4320, -0.4608, 0.7702]) + >>> torch.t(x) + tensor([ 2.4320, -0.4608, 0.7702]) + >>> x = torch.randn(2, 3) + >>> x + tensor([[ 0.4875, 0.9158, -0.5872], + [ 0.3938, -0.6929, 0.6932]]) + >>> torch.t(x) + tensor([[ 0.4875, 0.3938], + [ 0.9158, -0.6929], + [-0.5872, 0.6932]]) + + See also :func:`torch.transpose`. + """ + +def t_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + Performs the same operation as :func:`torch.t`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def take( + input: Tensor, + index: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + take(input, index) -> Tensor + + Returns a new tensor with the elements of :attr:`input` at the given indices. + The input tensor is treated as if it were viewed as a 1-D tensor. The result + takes the same shape as the indices. + + Args: + input (Tensor): the input tensor. + index (LongTensor): the indices into tensor + + Example:: + + >>> src = torch.tensor([[4, 3, 5], + ... [6, 7, 8]]) + >>> torch.take(src, torch.tensor([0, 2, 5])) + tensor([ 4, 5, 8]) + """ + +def take_along_dim( + input: Tensor, + indices: Tensor, + dim: _int | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + take_along_dim(input, indices, dim=None, *, out=None) -> Tensor + + Selects values from :attr:`input` at the 1-dimensional indices from :attr:`indices` along the given :attr:`dim`. + + If :attr:`dim` is None, the input array is treated as if it has been flattened to 1d. + + Functions that return indices along a dimension, like :func:`torch.argmax` and :func:`torch.argsort`, + are designed to work with this function. See the examples below. + + .. note:: + This function is similar to NumPy's `take_along_axis`. + See also :func:`torch.gather`. + + Args: + input (Tensor): the input tensor. + indices (LongTensor): the indices into :attr:`input`. Must have long dtype. + dim (int, optional): dimension to select along. Default: `None`. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.tensor([[10, 30, 20], [60, 40, 50]]) + >>> max_idx = torch.argmax(t) + >>> torch.take_along_dim(t, max_idx) + tensor([60]) + >>> sorted_idx = torch.argsort(t, dim=1) + >>> torch.take_along_dim(t, sorted_idx, dim=1) + tensor([[10, 20, 30], + [40, 50, 60]]) + """ + +def tan(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + tan(input, *, out=None) -> Tensor + + Returns a new tensor with the tangent of the elements in the :attr:`input` tensor, + where each value in this input tensor is in radians. + + .. math:: + \text{out}_{i} = \tan(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-1.2027, -1.7687, 0.4412, -1.3856]) + >>> torch.tan(a) + tensor([-2.5930, 4.9859, 0.4722, -5.3366]) + """ + +def tan_(input: Tensor) -> Tensor: ... +def tanh(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + tanh(input, *, out=None) -> Tensor + + Returns a new tensor with the hyperbolic tangent of the elements + of :attr:`input`. + + .. math:: + \text{out}_{i} = \tanh(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.8986, -0.7279, 1.1745, 0.2611]) + >>> torch.tanh(a) + tensor([ 0.7156, -0.6218, 0.8257, 0.2553]) + """ + +def tanh_(input: Tensor) -> Tensor: ... +def tensor( + data: Any, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + tensor(data, *, dtype=None, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Constructs a tensor with no autograd history (also known as a "leaf tensor", see :doc:`/notes/autograd`) by copying :attr:`data`. + + .. warning:: + + When working with tensors prefer using :func:`torch.Tensor.clone`, + :func:`torch.Tensor.detach`, and :func:`torch.Tensor.requires_grad_` for + readability. Letting `t` be a tensor, ``torch.tensor(t)`` is equivalent to + ``t.detach().clone()``, and ``torch.tensor(t, requires_grad=True)`` + is equivalent to ``t.detach().clone().requires_grad_(True)``. + + .. seealso:: + + :func:`torch.as_tensor` preserves autograd history and avoids copies where possible. + :func:`torch.from_numpy` creates a tensor that shares storage with a NumPy array. + + Args: + data (array_like): Initial data for the tensor. Can be a list, tuple, + NumPy ``ndarray``, scalar, and other types. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, infers data type from :attr:`data`. + device (:class:`torch.device`, optional): the device of the constructed tensor. If None and data is a tensor + then the device of data is used. If None and data is not a tensor then + the result tensor is constructed on the current device. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + + Example:: + + >>> torch.tensor([[0.1, 1.2], [2.2, 3.1], [4.9, 5.2]]) + tensor([[ 0.1000, 1.2000], + [ 2.2000, 3.1000], + [ 4.9000, 5.2000]]) + + >>> torch.tensor([0, 1]) # Type inference on data + tensor([ 0, 1]) + + >>> torch.tensor([[0.11111, 0.222222, 0.3333333]], + ... dtype=torch.float64, + ... device=torch.device('cuda:0')) # creates a double tensor on a CUDA device + tensor([[ 0.1111, 0.2222, 0.3333]], dtype=torch.float64, device='cuda:0') + + >>> torch.tensor(3.14159) # Create a zero-dimensional (scalar) tensor + tensor(3.1416) + + >>> torch.tensor([]) # Create an empty tensor (of size (0,)) + tensor([]) + """ + +@overload +def tensor_split( + input: Tensor, + tensor_indices_or_sections: Tensor, + dim: _int = 0, +) -> tuple[Tensor, ...]: + r""" + tensor_split(input, indices_or_sections, dim=0) -> List of Tensors + + Splits a tensor into multiple sub-tensors, all of which are views of :attr:`input`, + along dimension :attr:`dim` according to the indices or number of sections specified + by :attr:`indices_or_sections`. This function is based on NumPy's + :func:`numpy.array_split`. + + Args: + input (Tensor): the tensor to split + indices_or_sections (Tensor, int or list or tuple of ints): + If :attr:`indices_or_sections` is an integer ``n`` or a zero dimensional long tensor + with value ``n``, :attr:`input` is split into ``n`` sections along dimension :attr:`dim`. + If :attr:`input` is divisible by ``n`` along dimension :attr:`dim`, each + section will be of equal size, :code:`input.size(dim) / n`. If :attr:`input` + is not divisible by ``n``, the sizes of the first :code:`int(input.size(dim) % n)` + sections will have size :code:`int(input.size(dim) / n) + 1`, and the rest will + have size :code:`int(input.size(dim) / n)`. + + If :attr:`indices_or_sections` is a list or tuple of ints, or a one-dimensional long + tensor, then :attr:`input` is split along dimension :attr:`dim` at each of the indices + in the list, tuple or tensor. For instance, :code:`indices_or_sections=[2, 3]` and :code:`dim=0` + would result in the tensors :code:`input[:2]`, :code:`input[2:3]`, and :code:`input[3:]`. + + If :attr:`indices_or_sections` is a tensor, it must be a zero-dimensional or one-dimensional + long tensor on the CPU. + + dim (int, optional): dimension along which to split the tensor. Default: ``0`` + + Example:: + + >>> x = torch.arange(8) + >>> torch.tensor_split(x, 3) + (tensor([0, 1, 2]), tensor([3, 4, 5]), tensor([6, 7])) + + >>> x = torch.arange(7) + >>> torch.tensor_split(x, 3) + (tensor([0, 1, 2]), tensor([3, 4]), tensor([5, 6])) + >>> torch.tensor_split(x, (1, 6)) + (tensor([0]), tensor([1, 2, 3, 4, 5]), tensor([6])) + + >>> x = torch.arange(14).reshape(2, 7) + >>> x + tensor([[ 0, 1, 2, 3, 4, 5, 6], + [ 7, 8, 9, 10, 11, 12, 13]]) + >>> torch.tensor_split(x, 3, dim=1) + (tensor([[0, 1, 2], + [7, 8, 9]]), + tensor([[ 3, 4], + [10, 11]]), + tensor([[ 5, 6], + [12, 13]])) + >>> torch.tensor_split(x, (1, 6), dim=1) + (tensor([[0], + [7]]), + tensor([[ 1, 2, 3, 4, 5], + [ 8, 9, 10, 11, 12]]), + tensor([[ 6], + [13]])) + """ + +@overload +def tensor_split( + input: Tensor, + sections: _int | SymInt, + dim: _int = 0, +) -> tuple[Tensor, ...]: + r""" + tensor_split(input, indices_or_sections, dim=0) -> List of Tensors + + Splits a tensor into multiple sub-tensors, all of which are views of :attr:`input`, + along dimension :attr:`dim` according to the indices or number of sections specified + by :attr:`indices_or_sections`. This function is based on NumPy's + :func:`numpy.array_split`. + + Args: + input (Tensor): the tensor to split + indices_or_sections (Tensor, int or list or tuple of ints): + If :attr:`indices_or_sections` is an integer ``n`` or a zero dimensional long tensor + with value ``n``, :attr:`input` is split into ``n`` sections along dimension :attr:`dim`. + If :attr:`input` is divisible by ``n`` along dimension :attr:`dim`, each + section will be of equal size, :code:`input.size(dim) / n`. If :attr:`input` + is not divisible by ``n``, the sizes of the first :code:`int(input.size(dim) % n)` + sections will have size :code:`int(input.size(dim) / n) + 1`, and the rest will + have size :code:`int(input.size(dim) / n)`. + + If :attr:`indices_or_sections` is a list or tuple of ints, or a one-dimensional long + tensor, then :attr:`input` is split along dimension :attr:`dim` at each of the indices + in the list, tuple or tensor. For instance, :code:`indices_or_sections=[2, 3]` and :code:`dim=0` + would result in the tensors :code:`input[:2]`, :code:`input[2:3]`, and :code:`input[3:]`. + + If :attr:`indices_or_sections` is a tensor, it must be a zero-dimensional or one-dimensional + long tensor on the CPU. + + dim (int, optional): dimension along which to split the tensor. Default: ``0`` + + Example:: + + >>> x = torch.arange(8) + >>> torch.tensor_split(x, 3) + (tensor([0, 1, 2]), tensor([3, 4, 5]), tensor([6, 7])) + + >>> x = torch.arange(7) + >>> torch.tensor_split(x, 3) + (tensor([0, 1, 2]), tensor([3, 4]), tensor([5, 6])) + >>> torch.tensor_split(x, (1, 6)) + (tensor([0]), tensor([1, 2, 3, 4, 5]), tensor([6])) + + >>> x = torch.arange(14).reshape(2, 7) + >>> x + tensor([[ 0, 1, 2, 3, 4, 5, 6], + [ 7, 8, 9, 10, 11, 12, 13]]) + >>> torch.tensor_split(x, 3, dim=1) + (tensor([[0, 1, 2], + [7, 8, 9]]), + tensor([[ 3, 4], + [10, 11]]), + tensor([[ 5, 6], + [12, 13]])) + >>> torch.tensor_split(x, (1, 6), dim=1) + (tensor([[0], + [7]]), + tensor([[ 1, 2, 3, 4, 5], + [ 8, 9, 10, 11, 12]]), + tensor([[ 6], + [13]])) + """ + +@overload +def tensor_split( + input: Tensor, + indices: Sequence[_int | SymInt], + dim: _int = 0, +) -> tuple[Tensor, ...]: + r""" + tensor_split(input, indices_or_sections, dim=0) -> List of Tensors + + Splits a tensor into multiple sub-tensors, all of which are views of :attr:`input`, + along dimension :attr:`dim` according to the indices or number of sections specified + by :attr:`indices_or_sections`. This function is based on NumPy's + :func:`numpy.array_split`. + + Args: + input (Tensor): the tensor to split + indices_or_sections (Tensor, int or list or tuple of ints): + If :attr:`indices_or_sections` is an integer ``n`` or a zero dimensional long tensor + with value ``n``, :attr:`input` is split into ``n`` sections along dimension :attr:`dim`. + If :attr:`input` is divisible by ``n`` along dimension :attr:`dim`, each + section will be of equal size, :code:`input.size(dim) / n`. If :attr:`input` + is not divisible by ``n``, the sizes of the first :code:`int(input.size(dim) % n)` + sections will have size :code:`int(input.size(dim) / n) + 1`, and the rest will + have size :code:`int(input.size(dim) / n)`. + + If :attr:`indices_or_sections` is a list or tuple of ints, or a one-dimensional long + tensor, then :attr:`input` is split along dimension :attr:`dim` at each of the indices + in the list, tuple or tensor. For instance, :code:`indices_or_sections=[2, 3]` and :code:`dim=0` + would result in the tensors :code:`input[:2]`, :code:`input[2:3]`, and :code:`input[3:]`. + + If :attr:`indices_or_sections` is a tensor, it must be a zero-dimensional or one-dimensional + long tensor on the CPU. + + dim (int, optional): dimension along which to split the tensor. Default: ``0`` + + Example:: + + >>> x = torch.arange(8) + >>> torch.tensor_split(x, 3) + (tensor([0, 1, 2]), tensor([3, 4, 5]), tensor([6, 7])) + + >>> x = torch.arange(7) + >>> torch.tensor_split(x, 3) + (tensor([0, 1, 2]), tensor([3, 4]), tensor([5, 6])) + >>> torch.tensor_split(x, (1, 6)) + (tensor([0]), tensor([1, 2, 3, 4, 5]), tensor([6])) + + >>> x = torch.arange(14).reshape(2, 7) + >>> x + tensor([[ 0, 1, 2, 3, 4, 5, 6], + [ 7, 8, 9, 10, 11, 12, 13]]) + >>> torch.tensor_split(x, 3, dim=1) + (tensor([[0, 1, 2], + [7, 8, 9]]), + tensor([[ 3, 4], + [10, 11]]), + tensor([[ 5, 6], + [12, 13]])) + >>> torch.tensor_split(x, (1, 6), dim=1) + (tensor([[0], + [7]]), + tensor([[ 1, 2, 3, 4, 5], + [ 8, 9, 10, 11, 12]]), + tensor([[ 6], + [13]])) + """ + +def threshold( + input: Tensor, + threshold: Number | _complex, + value: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: ... +def threshold_( + input: Tensor, + threshold: Number | _complex, + value: Number | _complex, +) -> Tensor: ... +def tile(input: Tensor, dims: Sequence[_int | SymInt]) -> Tensor: + r""" + tile(input, dims) -> Tensor + + Constructs a tensor by repeating the elements of :attr:`input`. + The :attr:`dims` argument specifies the number of repetitions + in each dimension. + + If :attr:`dims` specifies fewer dimensions than :attr:`input` has, then + ones are prepended to :attr:`dims` until all dimensions are specified. + For example, if :attr:`input` has shape (8, 6, 4, 2) and :attr:`dims` + is (2, 2), then :attr:`dims` is treated as (1, 1, 2, 2). + + Analogously, if :attr:`input` has fewer dimensions than :attr:`dims` + specifies, then :attr:`input` is treated as if it were unsqueezed at + dimension zero until it has as many dimensions as :attr:`dims` specifies. + For example, if :attr:`input` has shape (4, 2) and :attr:`dims` + is (3, 3, 2, 2), then :attr:`input` is treated as if it had the + shape (1, 1, 4, 2). + + .. note:: + + This function is similar to NumPy's tile function. + + Args: + input (Tensor): the tensor whose elements to repeat. + dims (tuple): the number of repetitions per dimension. + + Example:: + + >>> x = torch.tensor([1, 2, 3]) + >>> x.tile((2,)) + tensor([1, 2, 3, 1, 2, 3]) + >>> y = torch.tensor([[1, 2], [3, 4]]) + >>> torch.tile(y, (2, 2)) + tensor([[1, 2, 1, 2], + [3, 4, 3, 4], + [1, 2, 1, 2], + [3, 4, 3, 4]]) + """ + +def topk( + input: Tensor, + k: _int | SymInt, + dim: _int = -1, + largest: _bool = True, + sorted: _bool = True, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.topk: + r""" + topk(input, k, dim=None, largest=True, sorted=True, *, out=None) -> (Tensor, LongTensor) + + Returns the :attr:`k` largest elements of the given :attr:`input` tensor along + a given dimension. + + If :attr:`dim` is not given, the last dimension of the `input` is chosen. + + If :attr:`largest` is ``False`` then the `k` smallest elements are returned. + + A namedtuple of `(values, indices)` is returned with the `values` and + `indices` of the largest `k` elements of each row of the `input` tensor in the + given dimension `dim`. + + The boolean option :attr:`sorted` if ``True``, will make sure that the returned + `k` elements are themselves sorted + + .. note:: + When using `torch.topk`, the indices of tied elements are not guaranteed to be stable + and may vary across different invocations. + + Args: + input (Tensor): the input tensor. + k (int): the k in "top-k" + dim (int, optional): the dimension to sort along + largest (bool, optional): controls whether to return largest or + smallest elements + sorted (bool, optional): controls whether to return the elements + in sorted order + + Keyword args: + out (tuple, optional): the output tuple of (Tensor, LongTensor) that can be + optionally given to be used as output buffers + + Example:: + + >>> x = torch.arange(1., 6.) + >>> x + tensor([ 1., 2., 3., 4., 5.]) + >>> torch.topk(x, 3) + torch.return_types.topk(values=tensor([5., 4., 3.]), indices=tensor([4, 3, 2])) + """ + +def trace(input: Tensor) -> Tensor: + r""" + trace(input) -> Tensor + + Returns the sum of the elements of the diagonal of the input 2-D matrix. + + Example:: + + >>> x = torch.arange(1., 10.).view(3, 3) + >>> x + tensor([[ 1., 2., 3.], + [ 4., 5., 6.], + [ 7., 8., 9.]]) + >>> torch.trace(x) + tensor(15.) + """ + +@overload +def transpose(input: Tensor, dim0: _int, dim1: _int) -> Tensor: + r""" + transpose(input, dim0, dim1) -> Tensor + + Returns a tensor that is a transposed version of :attr:`input`. + The given dimensions :attr:`dim0` and :attr:`dim1` are swapped. + + If :attr:`input` is a strided tensor then the resulting :attr:`out` + tensor shares its underlying storage with the :attr:`input` tensor, so + changing the content of one would change the content of the other. + + If :attr:`input` is a :ref:`sparse tensor ` then the + resulting :attr:`out` tensor *does not* share the underlying storage + with the :attr:`input` tensor. + + If :attr:`input` is a :ref:`sparse tensor ` with compressed + layout (SparseCSR, SparseBSR, SparseCSC or SparseBSC) the arguments + :attr:`dim0` and :attr:`dim1` must be both batch dimensions, or must + both be sparse dimensions. The batch dimensions of a sparse tensor are the + dimensions preceding the sparse dimensions. + + .. note:: + Transpositions which interchange the sparse dimensions of a `SparseCSR` + or `SparseCSC` layout tensor will result in the layout changing between + the two options. Transposition of the sparse dimensions of a ` SparseBSR` + or `SparseBSC` layout tensor will likewise generate a result with the + opposite layout. + + + Args: + input (Tensor): the input tensor. + dim0 (int): the first dimension to be transposed + dim1 (int): the second dimension to be transposed + + Example:: + + >>> x = torch.randn(2, 3) + >>> x + tensor([[ 1.0028, -0.9893, 0.5809], + [-0.1669, 0.7299, 0.4942]]) + >>> torch.transpose(x, 0, 1) + tensor([[ 1.0028, -0.1669], + [-0.9893, 0.7299], + [ 0.5809, 0.4942]]) + + See also :func:`torch.t`. + """ + +@overload +def transpose( + input: Tensor, + dim0: str | EllipsisType | None, + dim1: str | EllipsisType | None, +) -> Tensor: + r""" + transpose(input, dim0, dim1) -> Tensor + + Returns a tensor that is a transposed version of :attr:`input`. + The given dimensions :attr:`dim0` and :attr:`dim1` are swapped. + + If :attr:`input` is a strided tensor then the resulting :attr:`out` + tensor shares its underlying storage with the :attr:`input` tensor, so + changing the content of one would change the content of the other. + + If :attr:`input` is a :ref:`sparse tensor ` then the + resulting :attr:`out` tensor *does not* share the underlying storage + with the :attr:`input` tensor. + + If :attr:`input` is a :ref:`sparse tensor ` with compressed + layout (SparseCSR, SparseBSR, SparseCSC or SparseBSC) the arguments + :attr:`dim0` and :attr:`dim1` must be both batch dimensions, or must + both be sparse dimensions. The batch dimensions of a sparse tensor are the + dimensions preceding the sparse dimensions. + + .. note:: + Transpositions which interchange the sparse dimensions of a `SparseCSR` + or `SparseCSC` layout tensor will result in the layout changing between + the two options. Transposition of the sparse dimensions of a ` SparseBSR` + or `SparseBSC` layout tensor will likewise generate a result with the + opposite layout. + + + Args: + input (Tensor): the input tensor. + dim0 (int): the first dimension to be transposed + dim1 (int): the second dimension to be transposed + + Example:: + + >>> x = torch.randn(2, 3) + >>> x + tensor([[ 1.0028, -0.9893, 0.5809], + [-0.1669, 0.7299, 0.4942]]) + >>> torch.transpose(x, 0, 1) + tensor([[ 1.0028, -0.1669], + [-0.9893, 0.7299], + [ 0.5809, 0.4942]]) + + See also :func:`torch.t`. + """ + +def transpose_copy( + input: Tensor, + dim0: _int, + dim1: _int, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.transpose`, but all output tensors + are freshly created instead of aliasing the input. + """ + +@overload +def trapezoid(y: Tensor, x: Tensor, *, dim: _int = -1) -> Tensor: + r""" + trapezoid(y, x=None, *, dx=None, dim=-1) -> Tensor + + Computes the `trapezoidal rule `_ along + :attr:`dim`. By default the spacing between elements is assumed to be 1, but + :attr:`dx` can be used to specify a different constant spacing, and :attr:`x` can be + used to specify arbitrary spacing along :attr:`dim`. Only one of :attr:`x` or :attr:`dx` should be specified. + + + Assuming :attr:`y` is a one-dimensional tensor with elements :math:`{y_0, y_1, ..., y_n}`, + the default computation is + + .. math:: + \begin{aligned} + \sum_{i = 1}^{n} \frac{1}{2} (y_i + y_{i-1}) + \end{aligned} + + When :attr:`dx` is specified the computation becomes + + .. math:: + \begin{aligned} + \sum_{i = 1}^{n} \frac{\Delta x}{2} (y_i + y_{i-1}) + \end{aligned} + + effectively multiplying the result by :attr:`dx`. When :attr:`x` is specified, + assuming :attr:`x` is also a one-dimensional tensor with + elements :math:`{x_0, x_1, ..., x_n}`, the computation becomes + + .. math:: + \begin{aligned} + \sum_{i = 1}^{n} \frac{(x_i - x_{i-1})}{2} (y_i + y_{i-1}) + \end{aligned} + + When :attr:`x` and :attr:`y` have the same size, the computation is as described above and no broadcasting is needed. + The broadcasting behavior of this function is as follows when their sizes are different. For both :attr:`x` + and :attr:`y`, the function computes the difference between consecutive elements along + dimension :attr:`dim`. This effectively creates two tensors, `x_diff` and `y_diff`, that have + the same shape as the original tensors except their lengths along the dimension :attr:`dim` is reduced by 1. + After that, those two tensors are broadcast together to compute final output as part of the trapezoidal rule. + See the examples below for details. + + .. note:: + The trapezoidal rule is a technique for approximating the definite integral of a function + by averaging its left and right Riemann sums. The approximation becomes more accurate as + the resolution of the partition increases. + + Arguments: + y (Tensor): Values to use when computing the trapezoidal rule. + x (Tensor): If specified, defines spacing between values as specified above. + + Keyword arguments: + dx (float): constant spacing between values. If neither :attr:`x` or :attr:`dx` + are specified then this defaults to 1. Effectively multiplies the result by its value. + dim (int): The dimension along which to compute the trapezoidal rule. + The last (inner-most) dimension by default. + + Examples:: + + >>> # Computes the trapezoidal rule in 1D, spacing is implicitly 1 + >>> y = torch.tensor([1, 5, 10]) + >>> torch.trapezoid(y) + tensor(10.5) + + >>> # Computes the same trapezoidal rule directly to verify + >>> (1 + 10 + 10) / 2 + 10.5 + + >>> # Computes the trapezoidal rule in 1D with constant spacing of 2 + >>> # NOTE: the result is the same as before, but multiplied by 2 + >>> torch.trapezoid(y, dx=2) + 21.0 + + >>> # Computes the trapezoidal rule in 1D with arbitrary spacing + >>> x = torch.tensor([1, 3, 6]) + >>> torch.trapezoid(y, x) + 28.5 + + >>> # Computes the same trapezoidal rule directly to verify + >>> ((3 - 1) * (1 + 5) + (6 - 3) * (5 + 10)) / 2 + 28.5 + + >>> # Computes the trapezoidal rule for each row of a 3x3 matrix + >>> y = torch.arange(9).reshape(3, 3) + tensor([[0, 1, 2], + [3, 4, 5], + [6, 7, 8]]) + >>> torch.trapezoid(y) + tensor([ 2., 8., 14.]) + + >>> # Computes the trapezoidal rule for each column of the matrix + >>> torch.trapezoid(y, dim=0) + tensor([ 6., 8., 10.]) + + >>> # Computes the trapezoidal rule for each row of a 3x3 ones matrix + >>> # with the same arbitrary spacing + >>> y = torch.ones(3, 3) + >>> x = torch.tensor([1, 3, 6]) + >>> torch.trapezoid(y, x) + array([5., 5., 5.]) + + >>> # Computes the trapezoidal rule for each row of a 3x3 ones matrix + >>> # with different arbitrary spacing per row + >>> y = torch.ones(3, 3) + >>> x = torch.tensor([[1, 2, 3], [1, 3, 5], [1, 4, 7]]) + >>> torch.trapezoid(y, x) + array([2., 4., 6.]) + """ + +@overload +def trapezoid( + y: Tensor, + *, + dx: Number | _complex = 1, + dim: _int = -1, +) -> Tensor: + r""" + trapezoid(y, x=None, *, dx=None, dim=-1) -> Tensor + + Computes the `trapezoidal rule `_ along + :attr:`dim`. By default the spacing between elements is assumed to be 1, but + :attr:`dx` can be used to specify a different constant spacing, and :attr:`x` can be + used to specify arbitrary spacing along :attr:`dim`. Only one of :attr:`x` or :attr:`dx` should be specified. + + + Assuming :attr:`y` is a one-dimensional tensor with elements :math:`{y_0, y_1, ..., y_n}`, + the default computation is + + .. math:: + \begin{aligned} + \sum_{i = 1}^{n} \frac{1}{2} (y_i + y_{i-1}) + \end{aligned} + + When :attr:`dx` is specified the computation becomes + + .. math:: + \begin{aligned} + \sum_{i = 1}^{n} \frac{\Delta x}{2} (y_i + y_{i-1}) + \end{aligned} + + effectively multiplying the result by :attr:`dx`. When :attr:`x` is specified, + assuming :attr:`x` is also a one-dimensional tensor with + elements :math:`{x_0, x_1, ..., x_n}`, the computation becomes + + .. math:: + \begin{aligned} + \sum_{i = 1}^{n} \frac{(x_i - x_{i-1})}{2} (y_i + y_{i-1}) + \end{aligned} + + When :attr:`x` and :attr:`y` have the same size, the computation is as described above and no broadcasting is needed. + The broadcasting behavior of this function is as follows when their sizes are different. For both :attr:`x` + and :attr:`y`, the function computes the difference between consecutive elements along + dimension :attr:`dim`. This effectively creates two tensors, `x_diff` and `y_diff`, that have + the same shape as the original tensors except their lengths along the dimension :attr:`dim` is reduced by 1. + After that, those two tensors are broadcast together to compute final output as part of the trapezoidal rule. + See the examples below for details. + + .. note:: + The trapezoidal rule is a technique for approximating the definite integral of a function + by averaging its left and right Riemann sums. The approximation becomes more accurate as + the resolution of the partition increases. + + Arguments: + y (Tensor): Values to use when computing the trapezoidal rule. + x (Tensor): If specified, defines spacing between values as specified above. + + Keyword arguments: + dx (float): constant spacing between values. If neither :attr:`x` or :attr:`dx` + are specified then this defaults to 1. Effectively multiplies the result by its value. + dim (int): The dimension along which to compute the trapezoidal rule. + The last (inner-most) dimension by default. + + Examples:: + + >>> # Computes the trapezoidal rule in 1D, spacing is implicitly 1 + >>> y = torch.tensor([1, 5, 10]) + >>> torch.trapezoid(y) + tensor(10.5) + + >>> # Computes the same trapezoidal rule directly to verify + >>> (1 + 10 + 10) / 2 + 10.5 + + >>> # Computes the trapezoidal rule in 1D with constant spacing of 2 + >>> # NOTE: the result is the same as before, but multiplied by 2 + >>> torch.trapezoid(y, dx=2) + 21.0 + + >>> # Computes the trapezoidal rule in 1D with arbitrary spacing + >>> x = torch.tensor([1, 3, 6]) + >>> torch.trapezoid(y, x) + 28.5 + + >>> # Computes the same trapezoidal rule directly to verify + >>> ((3 - 1) * (1 + 5) + (6 - 3) * (5 + 10)) / 2 + 28.5 + + >>> # Computes the trapezoidal rule for each row of a 3x3 matrix + >>> y = torch.arange(9).reshape(3, 3) + tensor([[0, 1, 2], + [3, 4, 5], + [6, 7, 8]]) + >>> torch.trapezoid(y) + tensor([ 2., 8., 14.]) + + >>> # Computes the trapezoidal rule for each column of the matrix + >>> torch.trapezoid(y, dim=0) + tensor([ 6., 8., 10.]) + + >>> # Computes the trapezoidal rule for each row of a 3x3 ones matrix + >>> # with the same arbitrary spacing + >>> y = torch.ones(3, 3) + >>> x = torch.tensor([1, 3, 6]) + >>> torch.trapezoid(y, x) + array([5., 5., 5.]) + + >>> # Computes the trapezoidal rule for each row of a 3x3 ones matrix + >>> # with different arbitrary spacing per row + >>> y = torch.ones(3, 3) + >>> x = torch.tensor([[1, 2, 3], [1, 3, 5], [1, 4, 7]]) + >>> torch.trapezoid(y, x) + array([2., 4., 6.]) + """ + +@overload +def trapz(y: Tensor, *, dx: _float = 1, dim: _int = -1) -> Tensor: + r""" + trapz(y, x, *, dim=-1) -> Tensor + + Alias for :func:`torch.trapezoid`. + """ + +@overload +def trapz(y: Tensor, x: Tensor, *, dim: _int = -1) -> Tensor: + r""" + trapz(y, x, *, dim=-1) -> Tensor + + Alias for :func:`torch.trapezoid`. + """ + +def triangular_solve( + input: Tensor, + A: Tensor, + upper: _bool = True, + transpose: _bool = False, + unitriangular: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.triangular_solve: + r""" + triangular_solve(b, A, upper=True, transpose=False, unitriangular=False, *, out=None) -> (Tensor, Tensor) + + Solves a system of equations with a square upper or lower triangular invertible matrix :math:`A` + and multiple right-hand sides :math:`b`. + + In symbols, it solves :math:`AX = b` and assumes :math:`A` is square upper-triangular + (or lower-triangular if :attr:`upper`\ `= False`) and does not have zeros on the diagonal. + + `torch.triangular_solve(b, A)` can take in 2D inputs `b, A` or inputs that are + batches of 2D matrices. If the inputs are batches, then returns + batched outputs `X` + + If the diagonal of :attr:`A` contains zeros or elements that are very close to zero and + :attr:`unitriangular`\ `= False` (default) or if the input matrix is badly conditioned, + the result may contain `NaN` s. + + Supports input of float, double, cfloat and cdouble data types. + + .. warning:: + + :func:`torch.triangular_solve` is deprecated in favor of :func:`torch.linalg.solve_triangular` + and will be removed in a future PyTorch release. + :func:`torch.linalg.solve_triangular` has its arguments reversed and does not return a + copy of one of the inputs. + + ``X = torch.triangular_solve(B, A).solution`` should be replaced with + + .. code:: python + + X = torch.linalg.solve_triangular(A, B) + + Args: + b (Tensor): multiple right-hand sides of size :math:`(*, m, k)` where + :math:`*` is zero of more batch dimensions + A (Tensor): the input triangular coefficient matrix of size :math:`(*, m, m)` + where :math:`*` is zero or more batch dimensions + upper (bool, optional): whether :math:`A` is upper or lower triangular. Default: ``True``. + transpose (bool, optional): solves `op(A)X = b` where `op(A) = A^T` if this flag is ``True``, + and `op(A) = A` if it is ``False``. Default: ``False``. + unitriangular (bool, optional): whether :math:`A` is unit triangular. + If True, the diagonal elements of :math:`A` are assumed to be + 1 and not referenced from :math:`A`. Default: ``False``. + + Keyword args: + out ((Tensor, Tensor), optional): tuple of two tensors to write + the output to. Ignored if `None`. Default: `None`. + + Returns: + A namedtuple `(solution, cloned_coefficient)` where `cloned_coefficient` + is a clone of :math:`A` and `solution` is the solution :math:`X` to :math:`AX = b` + (or whatever variant of the system of equations, depending on the keyword arguments.) + + Examples:: + + >>> A = torch.randn(2, 2).triu() + >>> A + tensor([[ 1.1527, -1.0753], + [ 0.0000, 0.7986]]) + >>> b = torch.randn(2, 3) + >>> b + tensor([[-0.0210, 2.3513, -1.5492], + [ 1.5429, 0.7403, -1.0243]]) + >>> torch.triangular_solve(b, A) + torch.return_types.triangular_solve( + solution=tensor([[ 1.7841, 2.9046, -2.5405], + [ 1.9320, 0.9270, -1.2826]]), + cloned_coefficient=tensor([[ 1.1527, -1.0753], + [ 0.0000, 0.7986]])) + """ + +def tril( + input: Tensor, + diagonal: _int | SymInt = 0, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + tril(input, diagonal=0, *, out=None) -> Tensor + + Returns the lower triangular part of the matrix (2-D tensor) or batch of matrices + :attr:`input`, the other elements of the result tensor :attr:`out` are set to 0. + + The lower triangular part of the matrix is defined as the elements on and + below the diagonal. + + The argument :attr:`diagonal` controls which diagonal to consider. If + :attr:`diagonal` = 0, all elements on and below the main diagonal are + retained. A positive value includes just as many diagonals above the main + diagonal, and similarly a negative value excludes just as many diagonals below + the main diagonal. The main diagonal are the set of indices + :math:`\lbrace (i, i) \rbrace` for :math:`i \in [0, \min\{d_{1}, d_{2}\} - 1]` where + :math:`d_{1}, d_{2}` are the dimensions of the matrix. + + Args: + input (Tensor): the input tensor. + diagonal (int, optional): the diagonal to consider + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(3, 3) + >>> a + tensor([[-1.0813, -0.8619, 0.7105], + [ 0.0935, 0.1380, 2.2112], + [-0.3409, -0.9828, 0.0289]]) + >>> torch.tril(a) + tensor([[-1.0813, 0.0000, 0.0000], + [ 0.0935, 0.1380, 0.0000], + [-0.3409, -0.9828, 0.0289]]) + + >>> b = torch.randn(4, 6) + >>> b + tensor([[ 1.2219, 0.5653, -0.2521, -0.2345, 1.2544, 0.3461], + [ 0.4785, -0.4477, 0.6049, 0.6368, 0.8775, 0.7145], + [ 1.1502, 3.2716, -1.1243, -0.5413, 0.3615, 0.6864], + [-0.0614, -0.7344, -1.3164, -0.7648, -1.4024, 0.0978]]) + >>> torch.tril(b, diagonal=1) + tensor([[ 1.2219, 0.5653, 0.0000, 0.0000, 0.0000, 0.0000], + [ 0.4785, -0.4477, 0.6049, 0.0000, 0.0000, 0.0000], + [ 1.1502, 3.2716, -1.1243, -0.5413, 0.0000, 0.0000], + [-0.0614, -0.7344, -1.3164, -0.7648, -1.4024, 0.0000]]) + >>> torch.tril(b, diagonal=-1) + tensor([[ 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000], + [ 0.4785, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000], + [ 1.1502, 3.2716, 0.0000, 0.0000, 0.0000, 0.0000], + [-0.0614, -0.7344, -1.3164, 0.0000, 0.0000, 0.0000]]) + """ + +def tril_indices( + row: _int, + col: _int, + offset: _int = 0, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + tril_indices(row, col, offset=0, *, dtype=torch.long, device='cpu', layout=torch.strided) -> Tensor + + Returns the indices of the lower triangular part of a :attr:`row`-by- + :attr:`col` matrix in a 2-by-N Tensor, where the first row contains row + coordinates of all indices and the second row contains column coordinates. + Indices are ordered based on rows and then columns. + + The lower triangular part of the matrix is defined as the elements on and + below the diagonal. + + The argument :attr:`offset` controls which diagonal to consider. If + :attr:`offset` = 0, all elements on and below the main diagonal are + retained. A positive value includes just as many diagonals above the main + diagonal, and similarly a negative value excludes just as many diagonals below + the main diagonal. The main diagonal are the set of indices + :math:`\lbrace (i, i) \rbrace` for :math:`i \in [0, \min\{d_{1}, d_{2}\} - 1]` + where :math:`d_{1}, d_{2}` are the dimensions of the matrix. + + .. note:: + When running on CUDA, ``row * col`` must be less than :math:`2^{59}` to + prevent overflow during calculation. + + Args: + row (``int``): number of rows in the 2-D matrix. + col (``int``): number of columns in the 2-D matrix. + offset (``int``): diagonal offset from the main diagonal. + Default: if not provided, 0. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor, + only support ``torch.int``, ``torch.long``. Default: if ``None``, ``torch.long``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + layout (:class:`torch.layout`, optional): currently only support ``torch.strided``. + + Example:: + + >>> a = torch.tril_indices(3, 3) + >>> a + tensor([[0, 1, 1, 2, 2, 2], + [0, 0, 1, 0, 1, 2]]) + + >>> a = torch.tril_indices(4, 3, -1) + >>> a + tensor([[1, 2, 2, 3, 3, 3], + [0, 0, 1, 0, 1, 2]]) + + >>> a = torch.tril_indices(4, 3, 1) + >>> a + tensor([[0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3], + [0, 1, 0, 1, 2, 0, 1, 2, 0, 1, 2]]) + """ + +def triplet_margin_loss( + anchor: Tensor, + positive: Tensor, + negative: Tensor, + margin: _float = 1.0, + p: _float = 2, + eps: _float = 1e-06, + swap: _bool = False, + reduction: _int = 1, +) -> Tensor: ... +def triu( + input: Tensor, + diagonal: _int | SymInt = 0, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + triu(input, diagonal=0, *, out=None) -> Tensor + + Returns the upper triangular part of a matrix (2-D tensor) or batch of matrices + :attr:`input`, the other elements of the result tensor :attr:`out` are set to 0. + + The upper triangular part of the matrix is defined as the elements on and + above the diagonal. + + The argument :attr:`diagonal` controls which diagonal to consider. If + :attr:`diagonal` = 0, all elements on and above the main diagonal are + retained. A positive value excludes just as many diagonals above the main + diagonal, and similarly a negative value includes just as many diagonals below + the main diagonal. The main diagonal are the set of indices + :math:`\lbrace (i, i) \rbrace` for :math:`i \in [0, \min\{d_{1}, d_{2}\} - 1]` where + :math:`d_{1}, d_{2}` are the dimensions of the matrix. + + Args: + input (Tensor): the input tensor. + diagonal (int, optional): the diagonal to consider + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(3, 3) + >>> a + tensor([[ 0.2309, 0.5207, 2.0049], + [ 0.2072, -1.0680, 0.6602], + [ 0.3480, -0.5211, -0.4573]]) + >>> torch.triu(a) + tensor([[ 0.2309, 0.5207, 2.0049], + [ 0.0000, -1.0680, 0.6602], + [ 0.0000, 0.0000, -0.4573]]) + >>> torch.triu(a, diagonal=1) + tensor([[ 0.0000, 0.5207, 2.0049], + [ 0.0000, 0.0000, 0.6602], + [ 0.0000, 0.0000, 0.0000]]) + >>> torch.triu(a, diagonal=-1) + tensor([[ 0.2309, 0.5207, 2.0049], + [ 0.2072, -1.0680, 0.6602], + [ 0.0000, -0.5211, -0.4573]]) + + >>> b = torch.randn(4, 6) + >>> b + tensor([[ 0.5876, -0.0794, -1.8373, 0.6654, 0.2604, 1.5235], + [-0.2447, 0.9556, -1.2919, 1.3378, -0.1768, -1.0857], + [ 0.4333, 0.3146, 0.6576, -1.0432, 0.9348, -0.4410], + [-0.9888, 1.0679, -1.3337, -1.6556, 0.4798, 0.2830]]) + >>> torch.triu(b, diagonal=1) + tensor([[ 0.0000, -0.0794, -1.8373, 0.6654, 0.2604, 1.5235], + [ 0.0000, 0.0000, -1.2919, 1.3378, -0.1768, -1.0857], + [ 0.0000, 0.0000, 0.0000, -1.0432, 0.9348, -0.4410], + [ 0.0000, 0.0000, 0.0000, 0.0000, 0.4798, 0.2830]]) + >>> torch.triu(b, diagonal=-1) + tensor([[ 0.5876, -0.0794, -1.8373, 0.6654, 0.2604, 1.5235], + [-0.2447, 0.9556, -1.2919, 1.3378, -0.1768, -1.0857], + [ 0.0000, 0.3146, 0.6576, -1.0432, 0.9348, -0.4410], + [ 0.0000, 0.0000, -1.3337, -1.6556, 0.4798, 0.2830]]) + """ + +def triu_indices( + row: _int, + col: _int, + offset: _int = 0, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + triu_indices(row, col, offset=0, *, dtype=torch.long, device='cpu', layout=torch.strided) -> Tensor + + Returns the indices of the upper triangular part of a :attr:`row` by + :attr:`col` matrix in a 2-by-N Tensor, where the first row contains row + coordinates of all indices and the second row contains column coordinates. + Indices are ordered based on rows and then columns. + + The upper triangular part of the matrix is defined as the elements on and + above the diagonal. + + The argument :attr:`offset` controls which diagonal to consider. If + :attr:`offset` = 0, all elements on and above the main diagonal are + retained. A positive value excludes just as many diagonals above the main + diagonal, and similarly a negative value includes just as many diagonals below + the main diagonal. The main diagonal are the set of indices + :math:`\lbrace (i, i) \rbrace` for :math:`i \in [0, \min\{d_{1}, d_{2}\} - 1]` + where :math:`d_{1}, d_{2}` are the dimensions of the matrix. + + .. note:: + When running on CUDA, ``row * col`` must be less than :math:`2^{59}` to + prevent overflow during calculation. + + Args: + row (``int``): number of rows in the 2-D matrix. + col (``int``): number of columns in the 2-D matrix. + offset (``int``): diagonal offset from the main diagonal. + Default: if not provided, 0. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor, + only support ``torch.int``, ``torch.long``. Default: if ``None``, ``torch.long``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + layout (:class:`torch.layout`, optional): currently only support ``torch.strided``. + + Example:: + + >>> a = torch.triu_indices(3, 3) + >>> a + tensor([[0, 0, 0, 1, 1, 2], + [0, 1, 2, 1, 2, 2]]) + + >>> a = torch.triu_indices(4, 3, -1) + >>> a + tensor([[0, 0, 0, 1, 1, 1, 2, 2, 3], + [0, 1, 2, 0, 1, 2, 1, 2, 2]]) + + >>> a = torch.triu_indices(4, 3, 1) + >>> a + tensor([[0, 0, 1], + [1, 2, 2]]) + """ + +def true_divide( + input: Tensor | Number, + other: Tensor | Number, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + true_divide(dividend, divisor, *, out) -> Tensor + + Alias for :func:`torch.div` with ``rounding_mode=None``. + """ + +def trunc(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + trunc(input, *, out=None) -> Tensor + + Returns a new tensor with the truncated integer values of + the elements of :attr:`input`. + + For integer inputs, follows the array-api convention of returning a + copy of the input tensor. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 3.4742, 0.5466, -0.8008, -0.9079]) + >>> torch.trunc(a) + tensor([ 3., 0., -0., -0.]) + """ + +def trunc_(input: Tensor) -> Tensor: ... +@overload +def unbind(input: Tensor, dim: _int = 0) -> tuple[Tensor, ...]: + r""" + unbind(input, dim=0) -> seq + + Removes a tensor dimension. + + Returns a tuple of all slices along a given dimension, already without it. + + Arguments: + input (Tensor): the tensor to unbind + dim (int): dimension to remove + + Example:: + + >>> torch.unbind(torch.tensor([[1, 2, 3], + >>> [4, 5, 6], + >>> [7, 8, 9]])) + (tensor([1, 2, 3]), tensor([4, 5, 6]), tensor([7, 8, 9])) + """ + +@overload +def unbind( + input: Tensor, + dim: str | EllipsisType | None, +) -> tuple[Tensor, ...]: + r""" + unbind(input, dim=0) -> seq + + Removes a tensor dimension. + + Returns a tuple of all slices along a given dimension, already without it. + + Arguments: + input (Tensor): the tensor to unbind + dim (int): dimension to remove + + Example:: + + >>> torch.unbind(torch.tensor([[1, 2, 3], + >>> [4, 5, 6], + >>> [7, 8, 9]])) + (tensor([1, 2, 3]), tensor([4, 5, 6]), tensor([7, 8, 9])) + """ + +def unbind_copy( + input: Tensor, + dim: _int = 0, + *, + out: tuple[Tensor, ...] | list[Tensor] | None = None, +) -> None: + r""" + Performs the same operation as :func:`torch.unbind`, but all output tensors + are freshly created instead of aliasing the input. + """ + +@overload +def unflatten( + input: Tensor, + dim: str | EllipsisType | None, + sizes: Sequence[_int | SymInt], + names: Sequence[str | EllipsisType | None], +) -> Tensor: + r""" + unflatten(input, dim, sizes) -> Tensor + + Expands a dimension of the input tensor over multiple dimensions. + + .. seealso:: + + :func:`torch.flatten` the inverse of this function. It coalesces several dimensions into one. + + Args: + input (Tensor): the input tensor. + dim (int): Dimension to be unflattened, specified as an index into + ``input.shape``. + sizes (Tuple[int]): New shape of the unflattened dimension. + One of its elements can be `-1` in which case the corresponding output + dimension is inferred. Otherwise, the product of ``sizes`` *must* + equal ``input.shape[dim]``. + + Returns: + A View of input with the specified dimension unflattened. + + Examples:: + >>> torch.unflatten(torch.randn(3, 4, 1), 1, (2, 2)).shape + torch.Size([3, 2, 2, 1]) + >>> torch.unflatten(torch.randn(3, 4, 1), 1, (-1, 2)).shape + torch.Size([3, 2, 2, 1]) + >>> torch.unflatten(torch.randn(5, 12, 3), -2, (2, 2, 3, 1, 1)).shape + torch.Size([5, 2, 2, 3, 1, 1, 3]) + """ + +@overload +def unflatten( + input: Tensor, + dim: _int, + sizes: Sequence[_int | SymInt], +) -> Tensor: + r""" + unflatten(input, dim, sizes) -> Tensor + + Expands a dimension of the input tensor over multiple dimensions. + + .. seealso:: + + :func:`torch.flatten` the inverse of this function. It coalesces several dimensions into one. + + Args: + input (Tensor): the input tensor. + dim (int): Dimension to be unflattened, specified as an index into + ``input.shape``. + sizes (Tuple[int]): New shape of the unflattened dimension. + One of its elements can be `-1` in which case the corresponding output + dimension is inferred. Otherwise, the product of ``sizes`` *must* + equal ``input.shape[dim]``. + + Returns: + A View of input with the specified dimension unflattened. + + Examples:: + >>> torch.unflatten(torch.randn(3, 4, 1), 1, (2, 2)).shape + torch.Size([3, 2, 2, 1]) + >>> torch.unflatten(torch.randn(3, 4, 1), 1, (-1, 2)).shape + torch.Size([3, 2, 2, 1]) + >>> torch.unflatten(torch.randn(5, 12, 3), -2, (2, 2, 3, 1, 1)).shape + torch.Size([5, 2, 2, 3, 1, 1, 3]) + """ + +def unfold_copy( + input: Tensor, + dimension: _int, + size: _int, + step: _int, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.unfold`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def unique_dim( + input: Tensor, + dim: _int, + sorted: _bool = True, + return_inverse: _bool = False, + return_counts: _bool = False, +) -> tuple[Tensor, Tensor, Tensor]: ... +def unsafe_chunk( + input: Tensor, + chunks: _int, + dim: _int = 0, +) -> tuple[Tensor, ...]: + r""" + unsafe_chunk(input, chunks, dim=0) -> List of Tensors + + Works like :func:`torch.chunk` but without enforcing the autograd restrictions + on inplace modification of the outputs. + + .. warning:: + This function is safe to use as long as only the input, or only the outputs + are modified inplace after calling this function. It is user's + responsibility to ensure that is the case. If both the input and one or more + of the outputs are modified inplace, gradients computed by autograd will be + silently incorrect. + """ + +def unsafe_split( + input: Tensor, + split_size: _int | SymInt, + dim: _int = 0, +) -> tuple[Tensor, ...]: + r""" + unsafe_split(tensor, split_size_or_sections, dim=0) -> List of Tensors + + Works like :func:`torch.split` but without enforcing the autograd restrictions + on inplace modification of the outputs. + + .. warning:: + This function is safe to use as long as only the input, or only the outputs + are modified inplace after calling this function. It is user's + responsibility to ensure that is the case. If both the input and one or more + of the outputs are modified inplace, gradients computed by autograd will be + silently incorrect. + """ + +def unsafe_split_with_sizes( + input: Tensor, + split_sizes: Sequence[_int | SymInt], + dim: _int = 0, +) -> tuple[Tensor, ...]: ... +def unsqueeze(input: Tensor, dim: _int) -> Tensor: + r""" + unsqueeze(input, dim) -> Tensor + + Returns a new tensor with a dimension of size one inserted at the + specified position. + + The returned tensor shares the same underlying data with this tensor. + + A :attr:`dim` value within the range ``[-input.dim() - 1, input.dim() + 1)`` + can be used. Negative :attr:`dim` will correspond to :meth:`unsqueeze` + applied at :attr:`dim` = ``dim + input.dim() + 1``. + + Args: + input (Tensor): the input tensor. + dim (int): the index at which to insert the singleton dimension + + Example:: + + >>> x = torch.tensor([1, 2, 3, 4]) + >>> torch.unsqueeze(x, 0) + tensor([[ 1, 2, 3, 4]]) + >>> torch.unsqueeze(x, 1) + tensor([[ 1], + [ 2], + [ 3], + [ 4]]) + """ + +def unsqueeze_copy( + input: Tensor, + dim: _int, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.unsqueeze`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def values_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + Performs the same operation as :func:`torch.values`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def vander( + x: Tensor, + N: _int | None = None, + increasing: _bool = False, +) -> Tensor: + r""" + vander(x, N=None, increasing=False) -> Tensor + + Generates a Vandermonde matrix. + + The columns of the output matrix are elementwise powers of the input vector :math:`x^{(N-1)}, x^{(N-2)}, ..., x^0`. + If increasing is True, the order of the columns is reversed :math:`x^0, x^1, ..., x^{(N-1)}`. Such a + matrix with a geometric progression in each row is named for Alexandre-Theophile Vandermonde. + + Arguments: + x (Tensor): 1-D input tensor. + N (int, optional): Number of columns in the output. If N is not specified, + a square array is returned :math:`(N = len(x))`. + increasing (bool, optional): Order of the powers of the columns. If True, + the powers increase from left to right, if False (the default) they are reversed. + + Returns: + Tensor: Vandermonde matrix. If increasing is False, the first column is :math:`x^{(N-1)}`, + the second :math:`x^{(N-2)}` and so forth. If increasing is True, the columns + are :math:`x^0, x^1, ..., x^{(N-1)}`. + + Example:: + + >>> x = torch.tensor([1, 2, 3, 5]) + >>> torch.vander(x) + tensor([[ 1, 1, 1, 1], + [ 8, 4, 2, 1], + [ 27, 9, 3, 1], + [125, 25, 5, 1]]) + >>> torch.vander(x, N=3) + tensor([[ 1, 1, 1], + [ 4, 2, 1], + [ 9, 3, 1], + [25, 5, 1]]) + >>> torch.vander(x, N=3, increasing=True) + tensor([[ 1, 1, 1], + [ 1, 2, 4], + [ 1, 3, 9], + [ 1, 5, 25]]) + """ + +@overload +def var( + input: Tensor, + dim: _int | _size | None, + unbiased: _bool = True, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + var(input, dim=None, *, correction=1, keepdim=False, out=None) -> Tensor + + Calculates the variance over the dimensions specified by :attr:`dim`. :attr:`dim` + can be a single dimension, list of dimensions, or ``None`` to reduce over all + dimensions. + + The variance (:math:`\sigma^2`) is calculated as + + .. math:: \sigma^2 = \frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2 + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.var(a, dim=1, keepdim=True) + tensor([[1.0631], + [0.5590], + [1.4893], + [0.8258]]) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def var( + input: Tensor, + dim: _int | _size | None = None, + *, + correction: Number | _complex | None = None, + keepdim: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + var(input, dim=None, *, correction=1, keepdim=False, out=None) -> Tensor + + Calculates the variance over the dimensions specified by :attr:`dim`. :attr:`dim` + can be a single dimension, list of dimensions, or ``None`` to reduce over all + dimensions. + + The variance (:math:`\sigma^2`) is calculated as + + .. math:: \sigma^2 = \frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2 + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.var(a, dim=1, keepdim=True) + tensor([[1.0631], + [0.5590], + [1.4893], + [0.8258]]) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def var(input: Tensor, unbiased: _bool = True) -> Tensor: + r""" + var(input, dim=None, *, correction=1, keepdim=False, out=None) -> Tensor + + Calculates the variance over the dimensions specified by :attr:`dim`. :attr:`dim` + can be a single dimension, list of dimensions, or ``None`` to reduce over all + dimensions. + + The variance (:math:`\sigma^2`) is calculated as + + .. math:: \sigma^2 = \frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2 + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.var(a, dim=1, keepdim=True) + tensor([[1.0631], + [0.5590], + [1.4893], + [0.8258]]) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def var( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + *, + correction: Number | _complex | None = None, + keepdim: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + var(input, dim=None, *, correction=1, keepdim=False, out=None) -> Tensor + + Calculates the variance over the dimensions specified by :attr:`dim`. :attr:`dim` + can be a single dimension, list of dimensions, or ``None`` to reduce over all + dimensions. + + The variance (:math:`\sigma^2`) is calculated as + + .. math:: \sigma^2 = \frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2 + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.var(a, dim=1, keepdim=True) + tensor([[1.0631], + [0.5590], + [1.4893], + [0.8258]]) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def var( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + unbiased: _bool = True, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + var(input, dim=None, *, correction=1, keepdim=False, out=None) -> Tensor + + Calculates the variance over the dimensions specified by :attr:`dim`. :attr:`dim` + can be a single dimension, list of dimensions, or ``None`` to reduce over all + dimensions. + + The variance (:math:`\sigma^2`) is calculated as + + .. math:: \sigma^2 = \frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2 + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.var(a, dim=1, keepdim=True) + tensor([[1.0631], + [0.5590], + [1.4893], + [0.8258]]) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def var_mean( + input: Tensor, + dim: _int | _size | None, + unbiased: _bool = True, + keepdim: _bool = False, +) -> tuple[Tensor, Tensor]: + r""" + var_mean(input, dim=None, *, correction=1, keepdim=False, out=None) -> (Tensor, Tensor) + + Calculates the variance and mean over the dimensions specified by :attr:`dim`. + :attr:`dim` can be a single dimension, list of dimensions, or ``None`` to + reduce over all dimensions. + + The variance (:math:`\sigma^2`) is calculated as + + .. math:: \sigma^2 = \frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2 + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Returns: + A tuple (var, mean) containing the variance and mean. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.var_mean(a, dim=0, keepdim=True) + (tensor([[1.5926, 1.0056, 1.2005, 0.3646]]), + tensor([[ 0.0645, 0.4485, 0.8707, -0.0665]])) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def var_mean( + input: Tensor, + dim: _int | _size | None = None, + *, + correction: Number | _complex | None = None, + keepdim: _bool = False, +) -> tuple[Tensor, Tensor]: + r""" + var_mean(input, dim=None, *, correction=1, keepdim=False, out=None) -> (Tensor, Tensor) + + Calculates the variance and mean over the dimensions specified by :attr:`dim`. + :attr:`dim` can be a single dimension, list of dimensions, or ``None`` to + reduce over all dimensions. + + The variance (:math:`\sigma^2`) is calculated as + + .. math:: \sigma^2 = \frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2 + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Returns: + A tuple (var, mean) containing the variance and mean. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.var_mean(a, dim=0, keepdim=True) + (tensor([[1.5926, 1.0056, 1.2005, 0.3646]]), + tensor([[ 0.0645, 0.4485, 0.8707, -0.0665]])) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def var_mean( + input: Tensor, + unbiased: _bool = True, +) -> tuple[Tensor, Tensor]: + r""" + var_mean(input, dim=None, *, correction=1, keepdim=False, out=None) -> (Tensor, Tensor) + + Calculates the variance and mean over the dimensions specified by :attr:`dim`. + :attr:`dim` can be a single dimension, list of dimensions, or ``None`` to + reduce over all dimensions. + + The variance (:math:`\sigma^2`) is calculated as + + .. math:: \sigma^2 = \frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2 + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Returns: + A tuple (var, mean) containing the variance and mean. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.var_mean(a, dim=0, keepdim=True) + (tensor([[1.5926, 1.0056, 1.2005, 0.3646]]), + tensor([[ 0.0645, 0.4485, 0.8707, -0.0665]])) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def var_mean( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + *, + correction: Number | _complex | None = None, + keepdim: _bool = False, +) -> tuple[Tensor, Tensor]: + r""" + var_mean(input, dim=None, *, correction=1, keepdim=False, out=None) -> (Tensor, Tensor) + + Calculates the variance and mean over the dimensions specified by :attr:`dim`. + :attr:`dim` can be a single dimension, list of dimensions, or ``None`` to + reduce over all dimensions. + + The variance (:math:`\sigma^2`) is calculated as + + .. math:: \sigma^2 = \frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2 + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Returns: + A tuple (var, mean) containing the variance and mean. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.var_mean(a, dim=0, keepdim=True) + (tensor([[1.5926, 1.0056, 1.2005, 0.3646]]), + tensor([[ 0.0645, 0.4485, 0.8707, -0.0665]])) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def var_mean( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + unbiased: _bool = True, + keepdim: _bool = False, +) -> tuple[Tensor, Tensor]: + r""" + var_mean(input, dim=None, *, correction=1, keepdim=False, out=None) -> (Tensor, Tensor) + + Calculates the variance and mean over the dimensions specified by :attr:`dim`. + :attr:`dim` can be a single dimension, list of dimensions, or ``None`` to + reduce over all dimensions. + + The variance (:math:`\sigma^2`) is calculated as + + .. math:: \sigma^2 = \frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2 + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Returns: + A tuple (var, mean) containing the variance and mean. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.var_mean(a, dim=0, keepdim=True) + (tensor([[1.5926, 1.0056, 1.2005, 0.3646]]), + tensor([[ 0.0645, 0.4485, 0.8707, -0.0665]])) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +def vdot( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + vdot(input, other, *, out=None) -> Tensor + + Computes the dot product of two 1D vectors along a dimension. + + In symbols, this function computes + + .. math:: + + \sum_{i=1}^n \overline{x_i}y_i. + + where :math:`\overline{x_i}` denotes the conjugate for complex + vectors, and it is the identity for real vectors. + + .. note:: + + Unlike NumPy's vdot, torch.vdot intentionally only supports computing the dot product + of two 1D tensors with the same number of elements. + + .. seealso:: + + :func:`torch.linalg.vecdot` computes the dot product of two batches of vectors along a dimension. + + Args: + input (Tensor): first tensor in the dot product, must be 1D. Its conjugate is used if it's complex. + other (Tensor): second tensor in the dot product, must be 1D. + + Keyword args: + + .. note:: out (Tensor, optional): the output tensor. + + + Example:: + + >>> torch.vdot(torch.tensor([2, 3]), torch.tensor([2, 1])) + tensor(7) + >>> a = torch.tensor((1 +2j, 3 - 1j)) + >>> b = torch.tensor((2 +1j, 4 - 0j)) + >>> torch.vdot(a, b) + tensor([16.+1.j]) + >>> torch.vdot(b, a) + tensor([16.-1.j]) + """ + +def view_as_complex(input: Tensor) -> Tensor: + r""" + view_as_complex(input) -> Tensor + + Returns a view of :attr:`input` as a complex tensor. For an input complex + tensor of :attr:`size` :math:`m1, m2, \dots, mi, 2`, this function returns a + new complex tensor of :attr:`size` :math:`m1, m2, \dots, mi` where the last + dimension of the input tensor is expected to represent the real and imaginary + components of complex numbers. + + .. warning:: + :func:`view_as_complex` is only supported for tensors with + :class:`torch.dtype` ``torch.float64`` and ``torch.float32``. The input is + expected to have the last dimension of :attr:`size` 2. In addition, the + tensor must have a `stride` of 1 for its last dimension. The strides of all + other dimensions must be even numbers. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> x=torch.randn(4, 2) + >>> x + tensor([[ 1.6116, -0.5772], + [-1.4606, -0.9120], + [ 0.0786, -1.7497], + [-0.6561, -1.6623]]) + >>> torch.view_as_complex(x) + tensor([(1.6116-0.5772j), (-1.4606-0.9120j), (0.0786-1.7497j), (-0.6561-1.6623j)]) + """ + +def view_as_complex_copy( + input: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.view_as_complex`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def view_as_real(input: Tensor) -> Tensor: + r""" + view_as_real(input) -> Tensor + + Returns a view of :attr:`input` as a real tensor. For an input complex tensor of + :attr:`size` :math:`m1, m2, \dots, mi`, this function returns a new + real tensor of size :math:`m1, m2, \dots, mi, 2`, where the last dimension of size 2 + represents the real and imaginary components of complex numbers. + + .. warning:: + :func:`view_as_real` is only supported for tensors with ``complex dtypes``. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> x=torch.randn(4, dtype=torch.cfloat) + >>> x + tensor([(0.4737-0.3839j), (-0.2098-0.6699j), (0.3470-0.9451j), (-0.5174-1.3136j)]) + >>> torch.view_as_real(x) + tensor([[ 0.4737, -0.3839], + [-0.2098, -0.6699], + [ 0.3470, -0.9451], + [-0.5174, -1.3136]]) + """ + +def view_as_real_copy( + input: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.view_as_real`, but all output tensors + are freshly created instead of aliasing the input. + """ + +@overload +def view_copy( + input: Tensor, + dtype: _dtype, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.view`, but all output tensors + are freshly created instead of aliasing the input. + """ + +@overload +def view_copy( + input: Tensor, + size: Sequence[_int | SymInt], + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.view`, but all output tensors + are freshly created instead of aliasing the input. + """ + +@overload +def vsplit(input: Tensor, sections: _int) -> tuple[Tensor, ...]: + r""" + vsplit(input, indices_or_sections) -> List of Tensors + + Splits :attr:`input`, a tensor with two or more dimensions, into multiple tensors + vertically according to :attr:`indices_or_sections`. Each split is a view of + :attr:`input`. + + This is equivalent to calling torch.tensor_split(input, indices_or_sections, dim=0) + (the split dimension is 0), except that if :attr:`indices_or_sections` is an integer + it must evenly divide the split dimension or a runtime error will be thrown. + + This function is based on NumPy's :func:`numpy.vsplit`. + + Args: + input (Tensor): tensor to split. + indices_or_sections (int or list or tuple of ints): See argument in :func:`torch.tensor_split`. + + Example:: + + >>> t = torch.arange(16.0).reshape(4,4) + >>> t + tensor([[ 0., 1., 2., 3.], + [ 4., 5., 6., 7.], + [ 8., 9., 10., 11.], + [12., 13., 14., 15.]]) + >>> torch.vsplit(t, 2) + (tensor([[0., 1., 2., 3.], + [4., 5., 6., 7.]]), + tensor([[ 8., 9., 10., 11.], + [12., 13., 14., 15.]])) + >>> torch.vsplit(t, [3, 6]) + (tensor([[ 0., 1., 2., 3.], + [ 4., 5., 6., 7.], + [ 8., 9., 10., 11.]]), + tensor([[12., 13., 14., 15.]]), + tensor([], size=(0, 4))) + """ + +@overload +def vsplit(input: Tensor, indices: _size) -> tuple[Tensor, ...]: + r""" + vsplit(input, indices_or_sections) -> List of Tensors + + Splits :attr:`input`, a tensor with two or more dimensions, into multiple tensors + vertically according to :attr:`indices_or_sections`. Each split is a view of + :attr:`input`. + + This is equivalent to calling torch.tensor_split(input, indices_or_sections, dim=0) + (the split dimension is 0), except that if :attr:`indices_or_sections` is an integer + it must evenly divide the split dimension or a runtime error will be thrown. + + This function is based on NumPy's :func:`numpy.vsplit`. + + Args: + input (Tensor): tensor to split. + indices_or_sections (int or list or tuple of ints): See argument in :func:`torch.tensor_split`. + + Example:: + + >>> t = torch.arange(16.0).reshape(4,4) + >>> t + tensor([[ 0., 1., 2., 3.], + [ 4., 5., 6., 7.], + [ 8., 9., 10., 11.], + [12., 13., 14., 15.]]) + >>> torch.vsplit(t, 2) + (tensor([[0., 1., 2., 3.], + [4., 5., 6., 7.]]), + tensor([[ 8., 9., 10., 11.], + [12., 13., 14., 15.]])) + >>> torch.vsplit(t, [3, 6]) + (tensor([[ 0., 1., 2., 3.], + [ 4., 5., 6., 7.], + [ 8., 9., 10., 11.]]), + tensor([[12., 13., 14., 15.]]), + tensor([], size=(0, 4))) + """ + +def vstack( + tensors: tuple[Tensor, ...] | list[Tensor], + *, + out: Tensor | None = None, +) -> Tensor: + r""" + vstack(tensors, *, out=None) -> Tensor + + Stack tensors in sequence vertically (row wise). + + This is equivalent to concatenation along the first axis after all 1-D tensors have been reshaped by :func:`torch.atleast_2d`. + + Args: + tensors (sequence of Tensors): sequence of tensors to concatenate + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([1, 2, 3]) + >>> b = torch.tensor([4, 5, 6]) + >>> torch.vstack((a,b)) + tensor([[1, 2, 3], + [4, 5, 6]]) + >>> a = torch.tensor([[1],[2],[3]]) + >>> b = torch.tensor([[4],[5],[6]]) + >>> torch.vstack((a,b)) + tensor([[1], + [2], + [3], + [4], + [5], + [6]]) + """ + +@overload +def where(condition: Tensor) -> tuple[Tensor, ...]: + r""" + where(condition, input, other, *, out=None) -> Tensor + + Return a tensor of elements selected from either :attr:`input` or :attr:`other`, depending on :attr:`condition`. + + The operation is defined as: + + .. math:: + \text{out}_i = \begin{cases} + \text{input}_i & \text{if } \text{condition}_i \\ + \text{other}_i & \text{otherwise} \\ + \end{cases} + + .. note:: + The tensors :attr:`condition`, :attr:`input`, :attr:`other` must be :ref:`broadcastable `. + + Arguments: + condition (BoolTensor): When True (nonzero), yield input, otherwise yield other + input (Tensor or Scalar): value (if :attr:`input` is a scalar) or values selected at indices + where :attr:`condition` is ``True`` + other (Tensor or Scalar): value (if :attr:`other` is a scalar) or values selected at indices + where :attr:`condition` is ``False`` + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + Tensor: A tensor of shape equal to the broadcasted shape of :attr:`condition`, :attr:`input`, :attr:`other` + + Example:: + + >>> x = torch.randn(3, 2) + >>> y = torch.ones(3, 2) + >>> x + tensor([[-0.4620, 0.3139], + [ 0.3898, -0.7197], + [ 0.0478, -0.1657]]) + >>> torch.where(x > 0, 1.0, 0.0) + tensor([[0., 1.], + [1., 0.], + [1., 0.]]) + >>> torch.where(x > 0, x, y) + tensor([[ 1.0000, 0.3139], + [ 0.3898, 1.0000], + [ 0.0478, 1.0000]]) + >>> x = torch.randn(2, 2, dtype=torch.double) + >>> x + tensor([[ 1.0779, 0.0383], + [-0.8785, -1.1089]], dtype=torch.float64) + >>> torch.where(x > 0, x, 0.) + tensor([[1.0779, 0.0383], + [0.0000, 0.0000]], dtype=torch.float64) + + .. function:: where(condition) -> tuple of LongTensor + :noindex: + + ``torch.where(condition)`` is identical to + ``torch.nonzero(condition, as_tuple=True)``. + + .. note:: + See also :func:`torch.nonzero`. + """ + +@overload +def where( + condition: Tensor, + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + where(condition, input, other, *, out=None) -> Tensor + + Return a tensor of elements selected from either :attr:`input` or :attr:`other`, depending on :attr:`condition`. + + The operation is defined as: + + .. math:: + \text{out}_i = \begin{cases} + \text{input}_i & \text{if } \text{condition}_i \\ + \text{other}_i & \text{otherwise} \\ + \end{cases} + + .. note:: + The tensors :attr:`condition`, :attr:`input`, :attr:`other` must be :ref:`broadcastable `. + + Arguments: + condition (BoolTensor): When True (nonzero), yield input, otherwise yield other + input (Tensor or Scalar): value (if :attr:`input` is a scalar) or values selected at indices + where :attr:`condition` is ``True`` + other (Tensor or Scalar): value (if :attr:`other` is a scalar) or values selected at indices + where :attr:`condition` is ``False`` + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + Tensor: A tensor of shape equal to the broadcasted shape of :attr:`condition`, :attr:`input`, :attr:`other` + + Example:: + + >>> x = torch.randn(3, 2) + >>> y = torch.ones(3, 2) + >>> x + tensor([[-0.4620, 0.3139], + [ 0.3898, -0.7197], + [ 0.0478, -0.1657]]) + >>> torch.where(x > 0, 1.0, 0.0) + tensor([[0., 1.], + [1., 0.], + [1., 0.]]) + >>> torch.where(x > 0, x, y) + tensor([[ 1.0000, 0.3139], + [ 0.3898, 1.0000], + [ 0.0478, 1.0000]]) + >>> x = torch.randn(2, 2, dtype=torch.double) + >>> x + tensor([[ 1.0779, 0.0383], + [-0.8785, -1.1089]], dtype=torch.float64) + >>> torch.where(x > 0, x, 0.) + tensor([[1.0779, 0.0383], + [0.0000, 0.0000]], dtype=torch.float64) + + .. function:: where(condition) -> tuple of LongTensor + :noindex: + + ``torch.where(condition)`` is identical to + ``torch.nonzero(condition, as_tuple=True)``. + + .. note:: + See also :func:`torch.nonzero`. + """ + +@overload +def where( + condition: Tensor, + self: Number | _complex, + other: Tensor, +) -> Tensor: + r""" + where(condition, input, other, *, out=None) -> Tensor + + Return a tensor of elements selected from either :attr:`input` or :attr:`other`, depending on :attr:`condition`. + + The operation is defined as: + + .. math:: + \text{out}_i = \begin{cases} + \text{input}_i & \text{if } \text{condition}_i \\ + \text{other}_i & \text{otherwise} \\ + \end{cases} + + .. note:: + The tensors :attr:`condition`, :attr:`input`, :attr:`other` must be :ref:`broadcastable `. + + Arguments: + condition (BoolTensor): When True (nonzero), yield input, otherwise yield other + input (Tensor or Scalar): value (if :attr:`input` is a scalar) or values selected at indices + where :attr:`condition` is ``True`` + other (Tensor or Scalar): value (if :attr:`other` is a scalar) or values selected at indices + where :attr:`condition` is ``False`` + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + Tensor: A tensor of shape equal to the broadcasted shape of :attr:`condition`, :attr:`input`, :attr:`other` + + Example:: + + >>> x = torch.randn(3, 2) + >>> y = torch.ones(3, 2) + >>> x + tensor([[-0.4620, 0.3139], + [ 0.3898, -0.7197], + [ 0.0478, -0.1657]]) + >>> torch.where(x > 0, 1.0, 0.0) + tensor([[0., 1.], + [1., 0.], + [1., 0.]]) + >>> torch.where(x > 0, x, y) + tensor([[ 1.0000, 0.3139], + [ 0.3898, 1.0000], + [ 0.0478, 1.0000]]) + >>> x = torch.randn(2, 2, dtype=torch.double) + >>> x + tensor([[ 1.0779, 0.0383], + [-0.8785, -1.1089]], dtype=torch.float64) + >>> torch.where(x > 0, x, 0.) + tensor([[1.0779, 0.0383], + [0.0000, 0.0000]], dtype=torch.float64) + + .. function:: where(condition) -> tuple of LongTensor + :noindex: + + ``torch.where(condition)`` is identical to + ``torch.nonzero(condition, as_tuple=True)``. + + .. note:: + See also :func:`torch.nonzero`. + """ + +@overload +def where( + condition: Tensor, + input: Tensor, + other: Number | _complex, +) -> Tensor: + r""" + where(condition, input, other, *, out=None) -> Tensor + + Return a tensor of elements selected from either :attr:`input` or :attr:`other`, depending on :attr:`condition`. + + The operation is defined as: + + .. math:: + \text{out}_i = \begin{cases} + \text{input}_i & \text{if } \text{condition}_i \\ + \text{other}_i & \text{otherwise} \\ + \end{cases} + + .. note:: + The tensors :attr:`condition`, :attr:`input`, :attr:`other` must be :ref:`broadcastable `. + + Arguments: + condition (BoolTensor): When True (nonzero), yield input, otherwise yield other + input (Tensor or Scalar): value (if :attr:`input` is a scalar) or values selected at indices + where :attr:`condition` is ``True`` + other (Tensor or Scalar): value (if :attr:`other` is a scalar) or values selected at indices + where :attr:`condition` is ``False`` + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + Tensor: A tensor of shape equal to the broadcasted shape of :attr:`condition`, :attr:`input`, :attr:`other` + + Example:: + + >>> x = torch.randn(3, 2) + >>> y = torch.ones(3, 2) + >>> x + tensor([[-0.4620, 0.3139], + [ 0.3898, -0.7197], + [ 0.0478, -0.1657]]) + >>> torch.where(x > 0, 1.0, 0.0) + tensor([[0., 1.], + [1., 0.], + [1., 0.]]) + >>> torch.where(x > 0, x, y) + tensor([[ 1.0000, 0.3139], + [ 0.3898, 1.0000], + [ 0.0478, 1.0000]]) + >>> x = torch.randn(2, 2, dtype=torch.double) + >>> x + tensor([[ 1.0779, 0.0383], + [-0.8785, -1.1089]], dtype=torch.float64) + >>> torch.where(x > 0, x, 0.) + tensor([[1.0779, 0.0383], + [0.0000, 0.0000]], dtype=torch.float64) + + .. function:: where(condition) -> tuple of LongTensor + :noindex: + + ``torch.where(condition)`` is identical to + ``torch.nonzero(condition, as_tuple=True)``. + + .. note:: + See also :func:`torch.nonzero`. + """ + +@overload +def where( + condition: Tensor, + self: Number | _complex, + other: Number | _complex, +) -> Tensor: + r""" + where(condition, input, other, *, out=None) -> Tensor + + Return a tensor of elements selected from either :attr:`input` or :attr:`other`, depending on :attr:`condition`. + + The operation is defined as: + + .. math:: + \text{out}_i = \begin{cases} + \text{input}_i & \text{if } \text{condition}_i \\ + \text{other}_i & \text{otherwise} \\ + \end{cases} + + .. note:: + The tensors :attr:`condition`, :attr:`input`, :attr:`other` must be :ref:`broadcastable `. + + Arguments: + condition (BoolTensor): When True (nonzero), yield input, otherwise yield other + input (Tensor or Scalar): value (if :attr:`input` is a scalar) or values selected at indices + where :attr:`condition` is ``True`` + other (Tensor or Scalar): value (if :attr:`other` is a scalar) or values selected at indices + where :attr:`condition` is ``False`` + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + Tensor: A tensor of shape equal to the broadcasted shape of :attr:`condition`, :attr:`input`, :attr:`other` + + Example:: + + >>> x = torch.randn(3, 2) + >>> y = torch.ones(3, 2) + >>> x + tensor([[-0.4620, 0.3139], + [ 0.3898, -0.7197], + [ 0.0478, -0.1657]]) + >>> torch.where(x > 0, 1.0, 0.0) + tensor([[0., 1.], + [1., 0.], + [1., 0.]]) + >>> torch.where(x > 0, x, y) + tensor([[ 1.0000, 0.3139], + [ 0.3898, 1.0000], + [ 0.0478, 1.0000]]) + >>> x = torch.randn(2, 2, dtype=torch.double) + >>> x + tensor([[ 1.0779, 0.0383], + [-0.8785, -1.1089]], dtype=torch.float64) + >>> torch.where(x > 0, x, 0.) + tensor([[1.0779, 0.0383], + [0.0000, 0.0000]], dtype=torch.float64) + + .. function:: where(condition) -> tuple of LongTensor + :noindex: + + ``torch.where(condition)`` is identical to + ``torch.nonzero(condition, as_tuple=True)``. + + .. note:: + See also :func:`torch.nonzero`. + """ + +@overload +def xlogy( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + xlogy(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.special.xlogy`. + """ + +@overload +def xlogy( + self: Number | _complex, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + xlogy(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.special.xlogy`. + """ + +@overload +def xlogy( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + xlogy(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.special.xlogy`. + """ + +@overload +def xlogy_(input: Tensor, other: Tensor) -> Tensor: ... +@overload +def xlogy_(input: Tensor, other: Number | _complex) -> Tensor: ... +def zero_(input: Tensor) -> Tensor: ... +@overload +def zeros( + size: Sequence[_int | SymInt], + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + zeros(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with the scalar value `0`, with the shape defined + by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.zeros(2, 3) + tensor([[ 0., 0., 0.], + [ 0., 0., 0.]]) + + >>> torch.zeros(5) + tensor([ 0., 0., 0., 0., 0.]) + """ + +@overload +def zeros( + *size: _int | SymInt, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + zeros(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with the scalar value `0`, with the shape defined + by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.zeros(2, 3) + tensor([[ 0., 0., 0.], + [ 0., 0., 0.]]) + + >>> torch.zeros(5) + tensor([ 0., 0., 0., 0., 0.]) + """ + +@overload +def zeros( + size: _size, + *, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + zeros(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with the scalar value `0`, with the shape defined + by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.zeros(2, 3) + tensor([[ 0., 0., 0.], + [ 0., 0., 0.]]) + + >>> torch.zeros(5) + tensor([ 0., 0., 0., 0., 0.]) + """ + +@overload +def zeros( + *size: _int, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + zeros(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with the scalar value `0`, with the shape defined + by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.zeros(2, 3) + tensor([[ 0., 0., 0.], + [ 0., 0., 0.]]) + + >>> torch.zeros(5) + tensor([ 0., 0., 0., 0., 0.]) + """ + +def zeros_like( + input: Tensor, + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + zeros_like(input, *, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor filled with the scalar value `0`, with the same size as + :attr:`input`. ``torch.zeros_like(input)`` is equivalent to + ``torch.zeros(input.size(), dtype=input.dtype, layout=input.layout, device=input.device)``. + + .. warning:: + As of 0.4, this function does not support an :attr:`out` keyword. As an alternative, + the old ``torch.zeros_like(input, out=output)`` is equivalent to + ``torch.zeros(input.size(), out=output)``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + + Example:: + + >>> input = torch.empty(2, 3) + >>> torch.zeros_like(input) + tensor([[ 0., 0., 0.], + [ 0., 0., 0.]]) + """ diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/__init__.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..95939a4a858e67fd696ed234487ffef86dfe858c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/__init__.pyi @@ -0,0 +1,13159 @@ +# @generated by tools/pyi/gen_pyi.py from torch/_C/__init__.pyi.in +# mypy: disable-error-code="type-arg" +# mypy: allow-untyped-defs +# ruff: noqa: F401 + +from collections.abc import Callable, Iterable, Iterator, Sequence +from contextlib import AbstractContextManager +from enum import Enum, IntEnum +from pathlib import Path +from types import EllipsisType +from typing import ( + Any, + AnyStr, + Generic, + IO, + Literal, + NamedTuple, + overload, + Protocol, + runtime_checkable, + SupportsIndex, + TypeAlias, + TypeVar, +) +from typing_extensions import ParamSpec, Self + +import numpy + +import torch +from torch import inf, SymInt, Tensor +from torch._C import ( + _acc, + _aoti, + _cpu, + _dynamo, + _export, + _functionalization, + _functorch, + _lazy, + _lazy_ts_backend, + _nn, + _onnx, + _VariableFunctions, + _verbose, +) +from torch._prims_common import DeviceLikeType +from torch.autograd.graph import Node as _Node +from torch.cuda import _POOL_HANDLE +from torch.distributed.device_mesh import DeviceMesh +from torch.distributed.tensor._op_schema import OpSchema +from torch.distributed.tensor.placement_types import Placement +from torch.fx.node import Node as FxNode +from torch.package import PackageExporter +from torch.storage import TypedStorage, UntypedStorage +from torch.types import ( + _bool, + _bytes, + _complex, + _device, + _dispatchkey, + _dtype, + _float, + _int, + _layout, + _qscheme, + _size, + _str, + _symsize, + Device, + IntLikeType, + Number, + Storage, +) +from torch.utils._python_dispatch import TorchDispatchMode +from torch.utils.checkpoint import GraphExecGroup +from torch.xpu import _POOL_HANDLE as XPU_POOL_HANDLE + +# This module is defined in torch/csrc/Module.cpp + +K = TypeVar("K") # noqa: PYI001 +T = TypeVar("T") # noqa: PYI001 +S = TypeVar("S", bound=torch.Tensor) # noqa: PYI001 +P = ParamSpec("P") # noqa: PYI001 +R = TypeVar("R", covariant=True) # return value (always covariant) # noqa: PYI001 +T_co = TypeVar("T_co", covariant=True) # noqa: PYI001 + +@runtime_checkable +class _NestedSequence(Protocol[T_co]): + """A protocol for representing nested sequences. + + References:: + `numpy._typing._NestedSequence` + + """ + + def __len__(self, /) -> _int: ... + def __getitem__(self, index: _int, /) -> T_co | _NestedSequence[T_co]: ... + def __contains__(self, x: object, /) -> _bool: ... + def __iter__(self, /) -> Iterator[T_co | _NestedSequence[T_co]]: ... + def __reversed__(self, /) -> Iterator[T_co | _NestedSequence[T_co]]: ... + def count(self, value: Any, /) -> _int: ... + def index(self, value: Any, /) -> _int: ... + +# Defined in torch/csrc/Device.cpp +class device: + type: str # THPDevice_type + index: _int # THPDevice_index + + def __get__(self, instance, owner=None) -> device: ... + + # THPDevice_pynew + @overload + def __init__(self, device: DeviceLikeType) -> None: ... + @overload + def __init__(self, type: str, index: _int) -> None: ... + + # Uncomment if we ever make torch.device a decorator + # def __call__(self, func: T) -> T: ... + + def __enter__(self) -> Self: ... + def __exit__(self, exc_type, exc_val, exc_tb) -> None: ... + def __reduce__(self) -> tuple[Any, ...]: ... # THPDevice_reduce + +# Defined in torch/csrc/Stream.cpp +class Stream: + stream_id: _int # Stream id + device_index: _int + device_type: _int + + device: _device # The device of the stream + + @overload + def __new__( + cls, + device: DeviceLikeType | None = None, + *, + priority: _int = 0, + ) -> Self: ... + @overload + def __new__( + cls, + stream_id: _int, + device_index: _int, + device_type: _int, + *, + priority: _int = 0, + ) -> Self: ... + def query(self) -> _bool: ... + def synchronize(self) -> None: ... + def is_capturing(self) -> _bool: ... + def wait_event(self, event: Event) -> None: ... + def wait_stream(self, other: Stream) -> None: ... + def record_event(self, event: Event | None = None) -> Event: ... + def __hash__(self) -> _int: ... + def __eq__(self, other: object) -> _bool: ... + def __enter__(self) -> Self: ... + def __exit__(self, exc_type, exc_val, exc_tb) -> None: ... + +# Defined in torch/csrc/Event.cpp +class Event: + device: _device # The device of the Event + event_id: _int # The raw event created by device backend + + def __new__( + cls, + device: DeviceLikeType | None = None, + *, + enable_timing: _bool = False, + blocking: _bool = False, + interprocess: _bool = False, + ) -> Self: ... + @classmethod + def from_ipc_handle(cls, device: _device, ipc_handle: bytes) -> Event: ... + def record(self, stream: Stream | None = None) -> None: ... + def wait(self, stream: Stream | None = None) -> None: ... + def query(self) -> _bool: ... + def elapsed_time(self, other: Event) -> _float: ... + def synchronize(self) -> None: ... + def ipc_handle(self) -> bytes: ... + +# Defined in torch/csrc/Size.cpp +class Size(tuple[_int, ...]): + # TODO: __reduce__ + + @overload + def __getitem__(self: Size, key: SupportsIndex, /) -> _int: ... + @overload + def __getitem__(self: Size, key: slice, /) -> Size: ... + # Note: torch.Size does not support adding non-integer tuples. + def __add__(self, other: tuple[_int, ...], /) -> Size: ... # type: ignore[override] + def __radd__(self: Size, other: tuple[_int, ...], /) -> Size: ... + def __mul__(self, other: SupportsIndex, /) -> Size: ... + def __rmul__(self, other: SupportsIndex, /) -> Size: ... + def numel(self: Size, /) -> _int: ... + +# Defined in torch/csrc/Dtype.cpp +class dtype: + # TODO: __reduce__ + is_floating_point: _bool + is_complex: _bool + is_signed: _bool + itemsize: _int + def to_real(self) -> dtype: ... + def to_complex(self) -> dtype: ... + +# Defined in torch/csrc/TypeInfo.cpp +class iinfo: + bits: _int + min: _int + max: _int + dtype: str + + def __init__(self, dtype: _dtype) -> None: ... + +class finfo: + bits: _int + min: _float + max: _float + eps: _float + tiny: _float + smallest_normal: _float + resolution: _float + dtype: str + + @overload + def __init__(self, dtype: _dtype) -> None: ... + @overload + def __init__(self) -> None: ... + +float32: dtype = ... +float: dtype = ... +float64: dtype = ... +double: dtype = ... +float16: dtype = ... +bfloat16: dtype = ... +float8_e4m3fn: dtype = ... +float8_e4m3fnuz: dtype = ... +float8_e5m2: dtype = ... +float8_e5m2fnuz: dtype = ... +float8_e8m0fnu: dtype = ... +float4_e2m1fn_x2: dtype = ... +half: dtype = ... +uint8: dtype = ... +uint16: dtype = ... +uint32: dtype = ... +uint64: dtype = ... +int8: dtype = ... +int16: dtype = ... +short: dtype = ... +int32: dtype = ... +int: dtype = ... +int64: dtype = ... +long: dtype = ... +complex32: dtype = ... +complex64: dtype = ... +chalf: dtype = ... +cfloat: dtype = ... +complex128: dtype = ... +cdouble: dtype = ... +quint8: dtype = ... +qint8: dtype = ... +qint32: dtype = ... +bool: dtype = ... +quint4x2: dtype = ... +quint2x4: dtype = ... +bits1x8: dtype = ... +bits2x4: dtype = ... +bits4x2: dtype = ... +bits8: dtype = ... +bits16: dtype = ... + +# Defined in torch/csrc/Layout.cpp +class layout: ... + +# Defined in torch/csrc/utils/disable_torch_function.cpp +def DisableTorchFunction() -> AbstractContextManager[None]: ... +def DisableTorchFunctionSubclass() -> AbstractContextManager[None]: ... + +# Defined in torch/csrc/utils/tensor_layouts.cpp +strided: layout = ... +sparse_coo: layout = ... +sparse_csr: layout = ... +sparse_csc: layout = ... +sparse_bsr: layout = ... +sparse_bsc: layout = ... +_mkldnn: layout = ... +jagged: layout = ... + +# Defined in torch/csrc/MemoryFormat.cpp +class memory_format: ... + +# Defined in torch/csrc/utils/tensor_memoryformats.cpp +contiguous_format: memory_format = ... +channels_last: memory_format = ... +channels_last_3d: memory_format = ... +preserve_format: memory_format = ... + +# Defined in torch/csrc/QScheme.cpp +class qscheme: ... + +# Defined in torch/csrc/utils/tensor_qschemes.h +per_tensor_affine: qscheme = ... +per_channel_affine: qscheme = ... +per_tensor_symmetric: qscheme = ... +per_channel_symmetric: qscheme = ... +per_channel_affine_float_qparams: qscheme = ... + +# Defined in torch/csrc/autograd/python_function.cpp +class _FunctionBase: + saved_tensors: tuple[Tensor] + _raw_saved_tensors: tuple[Any] + next_functions: tuple[tuple[Any, _int], ...] + needs_input_grad: tuple[_bool] + metadata: dict + _materialize_non_diff_grads: _bool + # skip adding type hints for the fields that have wrappers defined + # in torch/autograd/function.py + def _sequence_nr(self) -> _int: ... + def _register_hook_dict(self, tensor: Tensor) -> None: ... + def register_hook(self, hook: Callable[..., Any]) -> Any: ... + def register_prehook(self, hook: Callable[..., Any]) -> Any: ... + +# Defined in torch/csrc/autograd/python_legacy_variable.cpp +class _LegacyVariableBase(Tensor): # inherits from Tensor to appease mypy + def __init__( + self, + data: Tensor | None = ..., + requires_grad: _bool | None = ..., + volatile: _bool | None = ..., + _grad_fn: _FunctionBase | None = ..., + ) -> None: ... + +# Defined in torch/csrc/jit/python/init.cpp +class IODescriptor: ... +class JITException(Exception): ... + +class Future(Generic[T]): + def __init__(self, devices: list[device]) -> None: ... + def done(self) -> _bool: ... + def value(self) -> T: ... + def wait(self) -> T: ... + def add_done_callback(self, callback: Callable) -> None: ... + def then(self, callback: Callable) -> Future[T]: ... + def set_result(self, result: T) -> None: ... + def _set_unwrap_func(self, callback: Callable) -> None: ... + +class _Await: + def __init__(self) -> None: ... + def fn(self) -> Callable: ... + def args(self) -> tuple[Any, ...]: ... + def is_nowait(self) -> _bool: ... + +def _jit_set_num_profiled_runs(num: _size) -> _size: ... + +# Defined in torch/csrc/jit/passes/mobile_optimizer_type.h +class _MobileOptimizerType: ... + +CONV_BN_FUSION: _MobileOptimizerType +INSERT_FOLD_PREPACK_OPS: _MobileOptimizerType +REMOVE_DROPOUT: _MobileOptimizerType +FUSE_ADD_RELU: _MobileOptimizerType +HOIST_CONV_PACKED_PARAMS: _MobileOptimizerType +VULKAN_AUTOMATIC_GPU_TRANSFER: _MobileOptimizerType + +def fork(*args: Any, **kwargs: Any) -> Future: ... +def wait(fut: Future) -> Any: ... +def _awaitable(*args: Any, **kwargs: Any) -> _Await: ... +def _awaitable_wait(aw: _Await) -> Any: ... +def _awaitable_nowait(x: Any) -> _Await: ... +def _collect_all(futures: list[Future]) -> Future: ... +def _set_print_stack_traces_on_fatal_signal(print: _bool) -> None: ... +def unify_type_list(types: list[JitType]) -> JitType: ... +def _freeze_module( + module: ScriptModule, + preserved_attrs: list[str] = ..., + freeze_interfaces: _bool = True, + preserveParameters: _bool = True, +) -> ScriptModule: ... +def _jit_pass_optimize_frozen_graph(Graph, optimize_numerics: _bool = True) -> None: ... +def _jit_pass_optimize_for_inference( + module: torch.jit.ScriptModule, + other_methods: list[str] = ..., +) -> None: ... +def _jit_pass_fold_frozen_conv_bn(graph: Graph): ... +def _jit_pass_fold_frozen_conv_add_or_sub(graph: Graph): ... +def _jit_pass_fold_frozen_conv_mul_or_div(graph: Graph): ... +def _jit_pass_fuse_frozen_conv_add_relu(graph: Graph): ... +def _jit_pass_concat_frozen_linear(graph: Graph): ... +def _jit_pass_convert_frozen_ops_to_mkldnn(graph: Graph): ... +def _jit_pass_transpose_frozen_linear(graph: Graph): ... +def _jit_pass_remove_dropout(module: torch.jit.ScriptModule): ... +def _is_tracing() -> _bool: ... +def _jit_init() -> _bool: ... +def _jit_flatten(arg: Any) -> tuple[list[Tensor], IODescriptor]: ... +def _jit_unflatten(vars: list[Tensor], desc: IODescriptor) -> Any: ... +def _jit_get_operation(op_name: str) -> tuple[Callable, list[str]]: ... +def _get_operation_overload( + op_name: str, + op_overload_name: str, +) -> tuple[Callable, Callable, list[Any]]: ... +def _get_schema(op_name: str, overload_name: str) -> FunctionSchema: ... +def _jit_pass_optimize_for_mobile( + module: torch.jit.ScriptModule, + optimization_blocklist: set[_MobileOptimizerType], + preserved_methods: list[AnyStr], +) -> torch.jit.ScriptModule: ... +def _clone_module_with_class( + module: torch.jit.ScriptModule, + ignored_methods: list[AnyStr], + ignored_attributes: list[AnyStr], +) -> torch.jit.ScriptModule: ... +def _jit_pass_vulkan_optimize_for_mobile( + module: torch.jit.ScriptModule, + optimization_blocklist: set[_MobileOptimizerType], + preserved_methods: list[AnyStr], +) -> torch.jit.ScriptModule: ... +def _jit_pass_metal_optimize_for_mobile( + module: torch.jit.ScriptModule, + preserved_methods: list[AnyStr], +) -> torch.jit.ScriptModule: ... +def _jit_pass_inline(Graph) -> None: ... +def _jit_pass_constant_propagation(Graph) -> None: ... +def _jit_pass_propagate_shapes_on_graph(Graph) -> None: ... +def _jit_register_decomposition_for_schema(schema: FunctionSchema, Graph) -> None: ... +def _jit_erase_non_input_shape_information(Graph) -> None: ... +def _jit_get_schemas_for_operator(name: str) -> list[FunctionSchema]: ... +def _jit_get_all_schemas() -> list[FunctionSchema]: ... +def _jit_check_alias_annotation( + g: Graph, + args: tuple[Any, ...], + unqualified_op_name: str, +): ... +def _jit_can_fuse_on_cpu() -> _bool: ... +def _jit_can_fuse_on_gpu() -> _bool: ... +def _jit_can_fuse_on_cpu_legacy() -> _bool: ... +def _debug_get_fusion_group_inlining() -> _bool: ... +def _debug_set_fusion_group_inlining(enable: _bool): ... +def _jit_texpr_fuser_enabled() -> _bool: ... +def _jit_nvfuser_enabled() -> _bool: ... +def _jit_llga_enabled() -> _bool: ... +def _jit_set_llga_enabled(enable: _bool): ... +def _llvm_enabled() -> _bool: ... +def _jit_override_can_fuse_on_cpu(override: _bool): ... +def _jit_override_can_fuse_on_gpu(override: _bool): ... +def _jit_override_can_fuse_on_cpu_legacy(override: _bool): ... +def _jit_set_symbolic_shapes_test_mode(override: _bool): ... +def _jit_symbolic_shapes_test_mode_enabled() -> _bool: ... +def _jit_set_texpr_fuser_enabled(enable: _bool): ... +def _jit_set_te_must_use_llvm_cpu(use_llvm: _bool): ... +def _jit_set_nvfuser_enabled(enable: _bool) -> _bool: ... +def _jit_set_autocast_mode(enable: _bool) -> _bool: ... +def _jit_cat_wo_conditionals(optimize_cat: _bool): ... +def _jit_opt_conditionals(opt_conds: _bool): ... +def _jit_pass_canonicalize(graph: Graph, keep_unique_names: _bool = True): ... +def _jit_pass_erase_shape_information(graph: Graph): ... +def _jit_pass_fold_convbn(module: torch.jit.ScriptModule): ... +def _jit_pass_insert_observers( + module: torch.jit.ScriptModule, + method_name: str, + qconfig_dict: dict[str, Any], + inplace: _bool, + quant_type: _int, +): ... +def _jit_pass_insert_quant_dequant( + module: torch.jit.ScriptModule, + method_name: str, + inplace: _bool, + debug: _bool, + quant_type: _int, +): ... +def _jit_pass_insert_quant_dequant_for_ondevice_ptq( + module: torch.jit.ScriptModule, + method_name: str, + inplace: _bool, + debug: _bool, + quant_type: _int, +): ... +def _jit_pass_quant_finalize( + module: torch.jit.ScriptModule, + quant_type: _int, + preserved_attrs: Sequence[str], +): ... +def _jit_pass_quant_finalize_for_ondevice_ptq( + module: torch.jit.ScriptModule, + quant_type: _int, + method_name: str, +): ... +def _jit_pass_insert_observer_method_for_ondevice_ptq( + module: torch.jit.ScriptModule, + method_name: str, + qconfig_dict: dict[str, Any], + inplace: _bool, + quant_type: _int, +): ... +def _jit_set_profiling_executor(profiling_flag: _bool) -> _bool: ... +def _jit_set_profiling_mode(profiling_flag: _bool) -> _bool: ... +def _jit_set_fusion_strategy( + strategy: list[tuple[str, _int]], +) -> list[tuple[str, _int]]: ... +def _jit_try_infer_type(obj: Any) -> InferredType: ... +def _jit_get_trigger_value(trigger_name: str) -> _int: ... + +# Defined in torch/csrc/jit/python/script_init.cpp +ResolutionCallback: TypeAlias = Callable[[str], Callable[..., Any]] + +# Defined in torch/csrc/jit/python/script_init.cpp +# and torch/csrc/jit/python/init.cpp +def _maybe_call_torch_function_for_op_packet( + op_overload_packet: Any, + *args: Any, + **kwargs: Any, +) -> Any: ... +def _check_schema_allow_fake_script_object( + schema: FunctionSchema, + *args: Any, + **kwargs: Any, +) -> _bool: ... +def _create_function_from_graph(qualname: str, graph: Graph) -> ScriptFunction: ... +def _debug_set_autodiff_subgraph_inlining(disabled: _bool) -> None: ... +def _ivalue_tags_match(lhs: ScriptModule, rhs: ScriptModule) -> _bool: ... +def _jit_assert_is_instance(obj: Any, type: JitType): ... +def _jit_clear_class_registry() -> None: ... +def _jit_set_emit_hooks( + ModuleHook: Callable | None, + FunctionHook: Callable | None, +) -> None: ... +def _jit_get_emit_hooks() -> tuple[Callable, Callable]: ... +def _load_for_lite_interpreter( + filename: str | Path, + map_location: DeviceLikeType | None, +): ... +def _load_for_lite_interpreter_from_buffer( + buffer: IO[bytes], + map_location: DeviceLikeType | None, +): ... +def _export_operator_list(module: LiteScriptModule): ... +def _quantize_ondevice_ptq_dynamic(module: LiteScriptModule, method_name: str): ... +def _get_model_bytecode_version(filename: str | Path) -> _int: ... +def _get_model_bytecode_version_from_buffer(buffer: IO[bytes]) -> _int: ... +def _backport_for_mobile( + filename_input: str | Path, + filename_output: str | Path, + to_version: _int, +) -> None: ... +def _backport_for_mobile_from_buffer( + buffer: IO[bytes], + filename_output: str | Path, + to_version: _int, +) -> None: ... +def _backport_for_mobile_to_buffer( + filename_input: str | Path, + to_version: _int, +) -> bytes: ... +def _backport_for_mobile_from_buffer_to_buffer( + buffer: IO[bytes], + to_version: _int, +) -> bytes: ... +def _get_model_ops_and_info(filename: str | Path): ... +def _get_model_ops_and_info_from_buffer(buffer: IO[bytes]): ... +def _get_mobile_model_contained_types(filename: str | Path): ... +def _get_mobile_model_contained_types_from_buffer(buffer: IO[bytes]): ... +def _logging_set_logger(logger: LoggerBase) -> LoggerBase: ... +def _get_graph_executor_optimize(optimize: _bool | None = None) -> _bool: ... +def _set_graph_executor_optimize(optimize: _bool): ... +def _export_opnames(module: ScriptModule) -> list[str]: ... +def _create_function_from_trace( + qualname: str, + func: Callable[..., Any], + input_tuple: tuple[Any, ...], + var_lookup_fn: Callable[[Tensor], str], + strict: _bool, + force_outplace: _bool, + argument_names: list[str], +) -> tuple[Graph, Stack]: ... +def _create_function_from_trace_with_dict( + qualname: str, + func: Callable[..., Any], + input_dict: dict[str, Any], + var_lookup_fn: Callable[[Tensor], str], + strict: _bool, + force_outplace: _bool, + argument_names: list[str], +) -> tuple[Graph, Stack]: ... +def _jit_is_script_object(obj: Any) -> _bool: ... +def _last_executed_optimized_graph() -> Graph: ... +def parse_type_comment(comment: str) -> Decl: ... +def _get_upgraders_map_size() -> _int: ... +def _get_upgraders_entry_map() -> dict[str, str]: ... +def _dump_upgraders_map() -> dict[str, str]: ... +def _test_only_populate_upgraders(content: dict[str, str]) -> None: ... +def _test_only_remove_upgraders(content: dict[str, str]) -> None: ... +def merge_type_from_type_comment( + decl: Decl, + type_annotation_decl: Decl, + is_method: _bool, +) -> Decl: ... +def parse_ir(input: str, parse_tensor_constants: _bool = False) -> Graph: ... +def parse_schema(schema: str) -> FunctionSchema: ... +def get_device(input: Tensor) -> _int: ... +def _resolve_type_from_object( + obj: Any, + range: SourceRange, + rcb: ResolutionCallback, +) -> JitType: ... +def _create_module_with_type(ty: JitType) -> ScriptModule: ... +def _create_object_with_type(ty: ClassType) -> ScriptObject: ... +def _run_emit_module_hook(m: ScriptModule): ... +def _replace_overloaded_method_decl( + overload_decl: Decl, + implementation_def: Def, + new_name: str, +) -> Def: ... +def _jit_pass_lower_all_tuples(graph: Graph) -> None: ... +def _jit_pass_onnx_set_dynamic_input_shape( + graph: Graph, + dynamic_axes: dict[str, dict[_int, str]], + input_names: list[str], +) -> None: ... +def _jit_pass_onnx_graph_shape_type_inference( + graph: Graph, + params_dict: dict[str, IValue], + opset_version: _int, +) -> None: ... +def _jit_pass_onnx_assign_output_shape( + graph: Graph, + tensors: list[Tensor], + desc: IODescriptor, + onnx_shape_inference: _bool, + is_script: _bool, + opset_version: _int, +) -> None: ... +def _jit_pass_onnx_remove_inplace_ops_for_onnx( + graph: Graph, + module: ScriptModule | None = None, +) -> None: ... +def _jit_pass_remove_inplace_ops(graph: Graph) -> None: ... +def _jit_pass_remove_mutation(graph: Graph) -> _bool: ... +def _jit_pass_canonicalize_graph_fuser_ops(graph: Graph) -> None: ... +def _jit_pass_peephole( + graph: Graph, + disable_shape_peepholes: _bool = False, +) -> None: ... +def _jit_pass_onnx_autograd_function_process(graph: Graph) -> None: ... +def _jit_pass_fuse_addmm(graph: Graph) -> None: ... +def _jit_pass_onnx_preprocess(graph: Graph) -> None: ... +def _jit_pass_prepare_division_for_onnx(graph: Graph) -> None: ... +def _jit_pass_onnx_remove_print(graph: Graph) -> None: ... +def _jit_pass_onnx_preprocess_caffe2(graph: Graph) -> None: ... +def _jit_pass_onnx_unpack_quantized_weights( + graph: Graph, + paramsDict: dict[str, IValue], +) -> dict[str, IValue]: ... +def _jit_pass_onnx_quantization_insert_permutes( + graph: Graph, + paramsDict: dict[str, IValue], +) -> dict[str, IValue]: ... +def _jit_pass_custom_pattern_based_rewrite_graph( + pattern: str, + fused_node_name: str, + graph: Graph, +) -> None: ... +def _jit_onnx_list_model_parameters( + module: ScriptModule, +) -> tuple[ScriptModule, list[IValue]]: ... +def _jit_pass_erase_number_types(graph: Graph) -> None: ... +def _jit_pass_onnx_lint(graph: Graph) -> None: ... +def _jit_pass_onnx( + graph: Graph, + _jit_pass_onnx: _onnx.OperatorExportTypes, +) -> Graph: ... +def _jit_pass_onnx_scalar_type_analysis( + graph: Graph, + lowprecision_cast: _bool, + opset_version: _int, +) -> None: ... +def _jit_pass_onnx_peephole( + graph: Graph, + opset_version: _int, + fixed_batch_size: _bool, +) -> None: ... +def _jit_pass_dce_allow_deleting_nodes_with_side_effects(graph: Graph) -> None: ... +def _jit_pass_onnx_function_substitution(graph: Graph) -> None: ... +def _jit_pass_onnx_function_extraction( + graph: Graph, + module_names: set[str], + param_names: list[str], +) -> dict[Node, dict[str, str]]: ... +def _jit_pass_onnx_clear_scope_records() -> None: ... +def _jit_pass_onnx_track_scope_attributes( + graph: Graph, + onnx_attrs: dict[str, Any], +) -> None: ... +def _jit_is_onnx_log_enabled() -> _bool: ... +def _jit_set_onnx_log_enabled(enabled: _bool) -> None: ... +def _jit_set_onnx_log_output_stream(stream_name: str) -> None: ... +def _jit_onnx_log(*args: Any) -> None: ... +def _jit_pass_lower_graph(graph: Graph, m: Module) -> tuple[Graph, list[IValue]]: ... +def _jit_pass_inline_fork_wait(graph: Graph) -> None: ... +def _jit_pass_onnx_deduplicate_initializers( + graph: Graph, + params_dict: dict[str, IValue], + is_train: _bool, +) -> dict[str, IValue]: ... +def _jit_pass_onnx_eval_peephole( + graph: Graph, + paramsDict: dict[str, IValue], +) -> dict[str, IValue]: ... +def _jit_pass_onnx_constant_fold( + graph: Graph, + paramsDict: dict[str, IValue], + opset_version: _int, +) -> dict[str, IValue]: ... +def _jit_pass_onnx_eliminate_unused_items( + graph: Graph, + paramsDict: dict[str, IValue], +) -> dict[str, IValue]: ... +def _jit_pass_onnx_cast_all_constant_to_floating(graph: Graph) -> None: ... +def _jit_pass_filter_non_tensor_arguments( + params: dict[str, IValue], +) -> dict[str, Tensor]: ... +def _jit_decay_packed_param_input_types(graph: Graph) -> None: ... +def _jit_pass_onnx_node_shape_type_inference( + n: Node, + paramsDict: dict[str, IValue], + opset_version: _int, +) -> None: ... +def _jit_onnx_convert_pattern_from_subblock( + block: Block, + n: Node, + env: dict[Value, Value], + values_in_env: set[Value], +) -> list[Value]: ... +def _jit_pass_onnx_block( + old_block: Block, + new_block: Block, + operator_export_type: _onnx.OperatorExportTypes, + env: dict[Value, Value], + values_in_env: set[Value], + is_sub_block: _bool, +) -> dict[Value, Value]: ... +def _jit_pass_onnx_assign_scoped_names_for_node_and_value(graph: Graph) -> None: ... +def _jit_pass_fixup_onnx_controlflow_node( + n: Node, + opset_version: _int, +) -> list[Value]: ... +def _jit_onnx_create_full_scope_name(class_name: str, variable_name: str) -> str: ... +def _compile_graph_to_code_table(name: str, graph: Graph) -> IValue: ... +def _generate_upgraders_graph() -> dict[str, Graph]: ... +def _calculate_package_version_based_on_upgraders(val: _bool): ... +def _get_version_calculator_flag() -> _bool: ... +def _jit_script_interface_compile( + name: str, + class_def: ClassDef, + rcb: ResolutionCallback, + is_module: _bool, +): ... +def _jit_script_compile_overload( + qualname: str, + overload_decl: Decl, + implementation_def: Def, + rcb: ResolutionCallback, + implementation_defaults: dict[str, Any], + signature: Any, +): ... +def _jit_script_compile( + qual_name: str, + definition: Def, + rcb: ResolutionCallback, + defaults: dict[str, Any], +): ... +def _jit_script_class_compile( + qual_name: str, + definition: ClassDef, + defaults: dict[str, dict[str, Any]], + rcb: ResolutionCallback, +): ... +def _parse_source_def(src: str) -> Def: ... +def import_ir_module( + cu: CompilationUnit, + filename: str | Path, + map_location: DeviceLikeType | None, + extra_files: dict[str, Any], +) -> ScriptModule: ... +def import_ir_module_from_buffer( + cu: CompilationUnit, + buffer: IO[bytes], + map_location: DeviceLikeType | None, + extra_files: dict[str, Any], +) -> ScriptModule: ... +def _import_ir_module_from_package( + cu: CompilationUnit, + reader: PyTorchFileReader, + storage_context: DeserializationStorageContext, + map_location: DeviceLikeType | None, + ts_id: str, +) -> ScriptModule: ... +def _assign_output_shapes(graph: Graph, inputs: list[Tensor]) -> Graph: ... +def _check_onnx_proto(proto: str) -> None: ... +def _propagate_and_assign_input_shapes( + graph: Graph, + inputs: tuple[Tensor, ...], + param_count_list: list[_int], + with_grad: _bool, + propagate: _bool, +) -> Graph: ... + +# Defined in torch/csrc/jit/runtime/graph_executor.h +class GraphExecutorState: ... + +# Defined in torch/torch/csrc/jit/ir/alias_analysis.h +class AliasDb: ... + +class _InsertPoint: + def __enter__(self) -> None: ... + def __exit__(self, *exc_info: object) -> None: ... + +# Defined in torch/csrc/jit/ir/ir.h +class Use: + @property + def user(self) -> Node: ... + @property + def offset(self) -> _int: ... + def isAfter(self, other: Use) -> _bool: ... + +# Defined in torch/csrc/jit/ir/ir.h +class Value: + def type(self) -> JitType: ... + def setType(self, t: JitType) -> Value: ... + def setTypeAs(self, other: Value) -> Value: ... + def inferTypeFrom(self, t: Tensor) -> None: ... + def debugName(self) -> str: ... + def setDebugName(self, name: str) -> None: ... + def unique(self) -> _int: ... + def offset(self) -> _int: ... + def node(self) -> Node: ... + def uses(self) -> list[Use]: ... + def replaceAllUsesWith(self, val: Value) -> None: ... + def replaceAllUsesAfterNodeWith(self, node: Node, val: Value) -> None: ... + def requires_grad(self) -> _bool: ... + def requiresGrad(self) -> _bool: ... + def copyMetadata(self, other: Value) -> Value: ... + def isCompleteTensor(self) -> _bool: ... + def toIValue(self) -> IValue: ... + +# Defined in torch/csrc/jit/ir/ir.h +class Block: + def inputs(self) -> Iterator[Value]: ... + def outputs(self) -> Iterator[Value]: ... + def nodes(self) -> Iterator[Node]: ... + def paramNode(self) -> Node: ... + def returnNode(self) -> Node: ... + def owningNode(self) -> Node: ... + def registerOutput(self, n: Value) -> _int: ... + def addNode(self, name: str, inputs: Sequence[Value]) -> Node: ... + +# Defined in torch/csrc/jit/ir/ir.h +class Node: + def __getitem__(self, key: str) -> Any: ... + def schema(self) -> str: ... + def input(self) -> Value: ... + def inputs(self) -> Iterator[Value]: ... + def inputsAt(self, idx: _int) -> Value: ... + def inputsSize(self) -> _int: ... + def output(self) -> Value: ... + def outputs(self) -> Iterator[Value]: ... + def outputsAt(self, idx: _int) -> Value: ... + def outputsSize(self) -> _int: ... + def hasMultipleOutputs(self) -> _bool: ... + def blocks(self) -> list[Block]: ... + def addBlock(self) -> Block: ... + def mustBeNone(self) -> _bool: ... + def matches(self, pattern: str) -> _bool: ... + def kind(self) -> str: ... + def kindOf(self, name: str) -> str: ... + def addInput(self, name: str) -> Value: ... + def replaceInput(self, i: _int, newValue: Value) -> Value: ... + def replaceInputWith(self, from_: Value, to: Value) -> None: ... + def replaceAllUsesWith(self, n: Node) -> None: ... + def insertBefore(self, n: Node) -> Node: ... + def insertAfter(self, n: Node) -> Node: ... + def isBefore(self, n: Node) -> _bool: ... + def isAfter(self, n: Node) -> _bool: ... + def moveBefore(self, n: Node) -> None: ... + def moveAfter(self, n: Node) -> None: ... + def removeInput(self, i: _int) -> None: ... + def removeAllInputs(self, i: _int) -> None: ... + def hasUses(self) -> _bool: ... + def eraseOutput(self, i: _int) -> None: ... + def addOutput(self) -> Value: ... + def scopeName(self) -> str: ... + def isNondeterministic(self) -> _bool: ... + def copyAttributes(self, rhs: Node) -> Node: ... + def copyMetadata(self, rhs: Node) -> Node: ... + def hasAttributes(self) -> _bool: ... + def hasAttribute(self, name: str) -> _bool: ... + def removeAttribute(self, attr: str) -> Node: ... + def namedInput(self, name: str) -> Value: ... + def sourceRange(self) -> SourceRange: ... + def owningBlock(self) -> Block: ... + def findNode(self, kind: str, recurse: _bool = True) -> Node: ... + def findAllNodes(self, kind: str, recurse: _bool = True) -> list[Node]: ... + def getModuleHierarchy(self) -> str: ... + def prev(self) -> Node: ... + def destroy(self) -> None: ... + def attributeNames(self) -> list[str]: ... + + # Accessors for attributes as types. + def f(self, name: str) -> _float: ... + def f_(self, name: str, val: _float) -> Node: ... + def fs(self, name: str) -> list[_float]: ... + def fs_(self, name: str, val: list[_float]) -> Node: ... + def c(self, name: str) -> complex: ... + def c_(self, name: str, val: complex) -> Node: ... + def s(self, name: str) -> str: ... + def s_(self, name: str, val: str) -> Node: ... + def ss(self, name: str) -> list[str]: ... + def ss_(self, name: str, val: list[str]) -> Node: ... + def i(self, name: str) -> _int: ... + def i_(self, name: str, val: _int) -> Node: ... + # Cannot define "is" like this because it's a reserved keyword in python. + # def is(self, name: str) -> List[_int]: ... + # def is_(self, name: str, val: List[_int]) -> Node: ... + def g(self, name: str) -> Graph: ... + def g_(self, name: str, val: Graph) -> Node: ... + def gs(self, name: str) -> list[Graph]: ... + def gs_(self, name: str, val: list[Graph]) -> Node: ... + def ival(self, name: str) -> IValue: ... + def ival_(self, name: str, val: IValue) -> Node: ... + def t(self, name: str) -> Tensor: ... + def t_(self, name: str, val: Tensor) -> Node: ... + def ts(self, name: str) -> list[Tensor]: ... + def ts_(self, name: str, val: list[Tensor]) -> Node: ... + def ty(self, name: str) -> JitType: ... + def ty_(self, name: str, val: JitType) -> Node: ... + def tys(self, name: str) -> list[JitType]: ... + def tys_(self, name: str, val: list[JitType]) -> Node: ... + +# Defined in torch/torch/csrc/jit/ir/ir.h +class Graph: + def inputs(self) -> Iterator[Value]: ... + def outputs(self) -> Iterator[Value]: ... + def nodes(self) -> Iterator[Node]: ... + def param_node(self) -> Node: ... + def return_node(self) -> Node: ... + def addInput(self, name: str = "") -> Value: ... + def eraseInput(self, i: _int) -> None: ... + def registerOutput(self, n: Value) -> _int: ... + def eraseOutput(self, i: _int) -> None: ... + def create(self, name: str, args, num_outputs: _int) -> Node: ... + def appendNode(self, n: Node) -> Node: ... + def prependNode(self, n: Node) -> Node: ... + def insertNode(self, n: Node) -> Node: ... + def block(self) -> Block: ... + def lint(self) -> None: ... + def alias_db(self) -> AliasDb: ... + def setInsertPoint(self, n: Block | Node) -> None: ... + def insert_point_guard(self, n: Block | Node) -> _InsertPoint: ... + def insertPoint(self) -> Node: ... + def insertGraph(self, callee: Graph, inputs: list[Value]) -> list[Value]: ... + def makeMultiOutputIntoTuple(self) -> None: ... + def copy(self) -> Graph: ... + +# Defined in torch/aten/src/ATen/core/alias_info.h +class AliasInfo: + is_write: _bool + before_set: set[str] + after_set: set[str] + def __init__( + self, + is_write: _bool, + before_set: set[str], + after_set: set[str], + ) -> None: ... + +# Defined in torch/aten/src/ATen/core/function_schema.h +class Argument: + name: str + type: JitType + default_value: Any | None + def has_default_value(self) -> _bool: ... + kwarg_only: _bool + is_out: _bool + alias_info: AliasInfo | None + is_write: _bool + real_type: JitType + def __init__( + self, + name: str, + type: JitType, + N: _int | None, + default_value: Any | None, + kwarg_only: _bool, + alias_info: AliasInfo | None, + ) -> None: ... + +class FunctionSchema: + arguments: list[Argument] + returns: list[Argument] + name: str + overload_name: str + is_mutable: _bool + def __init__( + self, + name: str, + overload_name: str, + arguments: list[Argument], + returns: list[Argument], + is_vararg: _bool, + is_varret: _bool, + ) -> None: ... + def _is_view_op(self) -> _bool: ... + +class _UpgraderEntry: + bumped_at_version: _int + upgrader_name: str + old_schema: str + def __init__( + self, + bumped_at_version: _int, + upgrader_name: str, + old_schema: str, + ) -> None: ... + +class _UpgraderRange: + min_version: _int + max_version: _int + +def _get_max_operator_version() -> _int: ... +def _get_operator_version_map() -> dict[str, list[_UpgraderEntry]]: ... +def _get_upgrader_ranges(name: str) -> list[_UpgraderRange]: ... +def _test_only_add_entry_to_op_version(op_name: str, entry: _UpgraderEntry) -> None: ... +def _test_only_remove_entry_to_op_version(op_name: str) -> None: ... + +# Defined in torch/csrc/jit/python/script_init.cpp +class ScriptModuleSerializer: + def __init__(self, export_writer: PyTorchFileWriter) -> None: ... + def serialize(self, model: ScriptModule, script_module_id: _int) -> None: ... + def write_files(self) -> None: ... + def storage_context(self) -> SerializationStorageContext: ... + +# Defined in torch/csrc/jit/python/script_init.cpp +class SerializationStorageContext: + def __init__(self) -> None: ... + def has_storage(self, storage: Storage) -> _bool: ... + def get_or_add_storage(self, storage: Storage) -> _int: ... + +# Defined in torch/csrc/jit/python/script_init.cpp +class DeserializationStorageContext: + def __init__(self) -> None: ... + def get_storage(self, name: str, dtype: _dtype) -> Tensor: ... + def has_storage(self, name: str) -> _bool: ... + def add_storage(self, name: str, tensor: Tensor) -> _int: ... + +# Defined in torch/csrc/jit/python/script_init.cpp +class ConcreteModuleTypeBuilder: + def __init__(self, obj: Any) -> None: ... + def set_module_dict(self) -> None: ... + def set_module_list(self) -> None: ... + def set_parameter_list(self) -> None: ... + def set_parameter_dict(self) -> None: ... + def add_attribute( + self, + name: str, + ty: JitType, + is_param: _bool, + is_buffer: _bool, + ) -> None: ... + def add_module(self, name: str, meta: ConcreteModuleType) -> None: ... + def add_constant(self, name: str, value: Any) -> None: ... + def add_overload( + self, method_name: str, overloaded_method_names: list[str] + ) -> None: ... + def add_builtin_function(self, name: str, symbol_name: str) -> None: ... + def add_failed_attribute(self, name: str, failure_reason: str) -> None: ... + def add_function_attribute( + self, + name: str, + ty: JitType, + func: Callable[..., Any], + ) -> None: ... + def add_ignored_attribute(self, name: str) -> None: ... + def add_ignored_attributes(self, names: list[str]) -> None: ... + def add_forward_hook(self, hook: Callable[..., Any]) -> None: ... + def add_forward_pre_hook(self, pre_hook: Callable[..., Any]) -> None: ... + +class ConcreteModuleType: + def get_constants(self) -> dict[str, Any]: ... + def equals(self, other: ConcreteModuleType) -> _bool: ... + @staticmethod + def from_jit_type(ty: JitType) -> ConcreteModuleType: ... + +class CallStack: + def __init__(self, name: str, range: SourceRange) -> None: ... + +class ErrorReport: + def __init__(self, range: SourceRange) -> None: ... + def what(self) -> str: ... + @staticmethod + def call_stack() -> str: ... + +class CompilationUnit: + def __init__(self, lang: str = ..., _frames_up: _int = ...) -> None: ... + def find_function(self, name: str) -> ScriptFunction: ... + def __getattr__(self, name: str) -> ScriptFunction: ... + def define( + self, + script: str, + rcb: ResolutionCallback = ..., + _frames_up: _int = ..., + ) -> list[ScriptFunction]: ... + def get_interface(self, name: str) -> InterfaceType: ... + def get_functions(self) -> None: ... + def create_function( + self, + name: str, + graph: Graph, + shouldMangle: _bool = ..., + ) -> ScriptFunction: ... + def get_class(self, name: str) -> ClassType: ... + +class ScriptObject: + def setattr(self, name: str, value: Any) -> None: ... + def _get_method(self, name: str) -> ScriptMethod: ... + def _type(self) -> ClassType: ... + +class ScriptModule(ScriptObject): + def _method_names(self) -> list[str]: ... + def _get_method(self, name: str) -> ScriptMethod: ... + +class LiteScriptModule: + def __call__(self, *input: Any) -> Any: ... + def find_method(self, method_name: str) -> bool: ... + def forward(self, *input: Any) -> list[str]: ... + def run_method(self, method_name: str, *input: Any) -> Any: ... + +# NOTE: switch to collections.abc.Callable in python 3.9 +class ScriptFunction(Generic[P, R]): + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: ... + def save(self, filename: str, _extra_files: dict[str, bytes]) -> None: ... + def save_to_buffer(self, _extra_files: dict[str, bytes]) -> bytes: ... + @property + def graph(self) -> Graph: ... + def inlined_graph(self) -> Graph: ... + def schema(self) -> FunctionSchema: ... + def code(self) -> str: ... + def name(self) -> str: ... + @property + def qualified_name(self) -> str: ... + +# NOTE: switch to collections.abc.Callable in python 3.9 +class ScriptMethod(Generic[P, R]): + graph: Graph + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: ... + @property + def owner(self) -> ScriptModule: ... + @property + def name(self) -> str: ... + @property + def schema(self) -> FunctionSchema: ... + +class ScriptDict(Generic[K, T]): + def __init__(self, dict: dict[K, T]) -> None: ... + def __len__(self) -> _int: ... + def __contains__(self, key: K) -> _bool: ... + def __getitem__(self, key: K) -> T: ... + def __setitem__(self, key: K, value: T) -> None: ... + def __delitem__(self, key: K) -> None: ... + def __iter__(self) -> Iterator[K]: ... + def items(self) -> Iterator[tuple[K, T]]: ... + def keys(self) -> Iterator[K]: ... + +class ScriptList(Generic[T]): + def __init__(self, list: list[T]) -> None: ... + def __len__(self) -> _int: ... + def __contains__(self, item: T) -> _bool: ... + @overload + def __getitem__(self, idx: _int) -> T: ... + @overload + def __getitem__(self, idx: slice) -> ScriptList[T]: ... + @overload + def __setitem__(self, idx: _int, value: T) -> None: ... + @overload + def __setitem__(self, idx: slice, value: list[T]) -> None: ... + def __delitem__(self, idx: _int) -> None: ... + def __iter__(self) -> Iterator[T]: ... + def count(self, value: T) -> _int: ... + def remove(self, value: T) -> None: ... + def append(self, value: T) -> None: ... + def clear(self) -> None: ... + @overload + def extend(self, values: list[T]) -> None: ... + @overload + def extend(self, values: Iterable[T]) -> None: ... + @overload + def pop(self) -> T: ... + @overload + def pop(self, idx: _int) -> T: ... + +class ModuleDict: + def __init__(self, mod: ScriptModule) -> None: ... + def items(self) -> list[tuple[str, Any]]: ... + +class ParameterDict: + def __init__(self, mod: ScriptModule) -> None: ... + +class BufferDict: + def __init__(self, mod: ScriptModule) -> None: ... + +# Defined in torch/csrc/jit/api/module.h +class Module: ... + +# Defined in torch/csrc/Module.cpp +def _initExtension(shm_manager_path: str) -> None: ... # THPModule_initExtension +def _autograd_init() -> _bool: ... # THPAutograd_initExtension +def _add_docstr(obj: T, doc_obj: str) -> T: ... # THPModule_addDocStr +def _init_names(arg: Sequence[type]) -> None: ... # THPModule_initNames +def _has_distributed() -> _bool: ... # THPModule_hasDistributed +def _set_default_tensor_type(type) -> None: ... # THPModule_setDefaultTensorType +def _set_default_dtype(d: _dtype) -> None: ... # THPModule_setDefaultDtype +def _infer_size(arg1: Size, arg2: Size) -> Size: ... # THPModule_inferSize +def _crash_if_csrc_asan() -> _int: ... # THPModule_crashIfCsrcASAN +def _crash_if_csrc_ubsan() -> _int: ... # THPModule_crashIfCsrcUBSAN +def _crash_if_aten_asan() -> _int: ... # THPModule_crashIfATenASAN +def _show_config() -> str: ... # THPModule_showConfig +def _cxx_flags() -> str: ... # THPModule_cxxFlags +def _parallel_info() -> str: ... # THPModule_parallelInfo +def _get_cpu_capability() -> str: ... # THPModule_getCpuCapability +def _set_backcompat_broadcast_warn( + arg: _bool, +) -> None: ... # THPModule_setBackcompatBroadcastWarn +def _get_backcompat_broadcast_warn() -> ( + _bool +): ... # THPModule_getBackcompatBroadcastWarn +def _set_backcompat_keepdim_warn( + arg: _bool, +) -> None: ... # THPModule_setBackcompatKeepdimWarn +def _get_backcompat_keepdim_warn() -> _bool: ... # THPModule_getBackcompatKeepdimWarn +def get_num_thread() -> _int: ... # THPModule_getNumThreads +def set_num_threads(nthreads: _int) -> None: ... # THPModule_setNumThreads +def get_num_interop_threads() -> _int: ... # THPModule_getNumInteropThreads +def set_num_interop_threads( + nthreads: _int, +) -> None: ... # THPModule_setNumInteropThreads +def _get_cudnn_enabled() -> _bool: ... # THPModule_userEnabledCuDNN +def _set_cudnn_enabled(arg: _bool) -> None: ... # THPModule_setUserEnabledCuDNN +def _get_flash_sdp_enabled() -> _bool: ... # THPModule_userEnabledFusedSDP +def _set_sdp_use_flash(arg: _bool) -> None: ... # THPModule_setSDPUseFlash +def _get_fa3_sdp_enabled() -> _bool: ... # THPModule_userEnabledFA3SDP +def _set_sdp_use_fa3(arg: _bool) -> None: ... # THPModule_setSDPUseFA3 +def _get_mem_efficient_sdp_enabled() -> _bool: ... # THPModule_userEnabledMathSDP +def _set_sdp_use_mem_efficient( + arg: _bool, +) -> None: ... # THPModule_setSDPUseMemEfficient +def _get_math_sdp_enabled() -> _bool: ... # THPModule_userEnabledMathSDP +def _set_sdp_use_math(arg: _bool) -> None: ... # THPModule_setSDPUseMath +def _get_math_sdp_allow_fp16_bf16_reduction() -> ( + _bool +): ... # THPModule_allowFP16BF16ReductionMathSDP +def _set_math_sdp_allow_fp16_bf16_reduction( + arg: _bool, +) -> None: ... # THPModule_setAllowFP16BF16ReductionMathSDP +def _get_overrideable_sdp_enabled() -> ( + _bool +): ... # THPModule_userEnabledOverrideableSDP +def _set_sdp_use_overrideable( + arg: _bool, +) -> None: ... # THPModule_setSDPUseOverrideable +def _get_sdp_priority_order() -> list[_int]: ... # THPModule_getSDPPriorityOrder +def _set_sdp_priority_order( + arg: list[_int], +) -> None: ... # THPModule_setSDPPriorityOrder +def _get_cudnn_sdp_enabled() -> _bool: ... # THPModule_userEnabledMathSDP +def _set_sdp_use_cudnn(arg: _bool) -> None: ... # THPModule_setSDPUseMath +def _get_mkldnn_enabled() -> _bool: ... # THPModule_userEnabledMkldnn +def _set_mkldnn_enabled(arg: _bool) -> None: ... # THPModule_setUserEnabledMkldnn +def _get_cudnn_benchmark() -> _bool: ... # THPModule_benchmarkCuDNN +def _set_cudnn_benchmark(arg: _bool) -> None: ... # THPModule_setBenchmarkCuDNN +def _get_miopen_immediate() -> _bool: ... # THPModule_userImmediateMiopen +def _set_miopen_immediate(arg: _bool) -> None: ... # THPModule_setUserImmediateMiopen +def _get_cudnn_deterministic() -> _bool: ... # THPModule_deterministicCuDNN +def _set_cudnn_deterministic(arg: _bool) -> None: ... # THPModule_setDeterministicCuDNN +def _get_mkldnn_deterministic() -> _bool: ... # THPModule_deterministicMkldnn +def _set_mkldnn_deterministic( + arg: _bool, +) -> None: ... # THPModule_setDeterministicMkldnn +def _get_onednn_allow_tf32() -> _bool: ... # THPModule_allowTF32OneDNN +def _set_onednn_allow_tf32(arg: _bool) -> None: ... # THPModule_setAllowTF32OneDNN +def _get_deterministic_algorithms() -> _bool: ... # THPModule_deterministicAlgorithms +def _get_deterministic_algorithms_warn_only() -> ( + _bool +): ... # THPModule_deterministicAlgorithmsWarnOnly +def _set_deterministic_algorithms( + mode: _bool, + *, + warn_only: _bool = ..., +) -> None: ... # THPModule_setDeterministicAlgorithms +def _get_deterministic_fill_uninitialized_memory() -> ( + _bool +): ... # THPModule_deterministicFillUninitializedMemory +def _set_deterministic_fill_uninitialized_memory( + arg: _bool, +) -> None: ... # THPModule_setDeterministicFillUninitializedMemory +def _get_nnpack_enabled() -> _bool: ... # THPModule_userEnabledNNPACK +def _set_nnpack_enabled(arg: _bool) -> None: ... # THPModule_setUserEnabledNNPACK +def _get_warnAlways() -> _bool: ... # THPModule_warnAlways +def _set_warnAlways(arg: _bool) -> None: ... # THPModule_setWarnAlways +def _get_cudnn_allow_tf32() -> _bool: ... # THPModule_allowTF32CuDNN +def _set_cudnn_allow_tf32(arg: _bool) -> None: ... # THPModule_setAllowTF32CuDNN +def _get_cublas_allow_tf32() -> _bool: ... # THPModule_allowTF32CuBLAS +def _set_cublas_allow_tf32(arg: _bool) -> None: ... # THPModule_setAllowTF32CuBLAS +def _get_float32_matmul_precision() -> str: ... # THPModule_float32MatmulPrecision +def _set_float32_matmul_precision( + arg: str, +) -> None: ... # THPModule_setFloat32MatmulPrecision +def _get_cublas_allow_fp16_reduced_precision_reduction() -> tuple[ + _bool, _bool +]: ... # THPModule_allowFP16ReductionCuBLAS +def _set_cublas_allow_fp16_reduced_precision_reduction( + arg: _bool, + allow_splitk: _bool = ..., +) -> None: ... # THPModule_setAllowFP16ReductionCuBLAS +def _get_cublas_allow_bf16_reduced_precision_reduction() -> tuple[ + _bool, _bool +]: ... # THPModule_allowBF16ReductionCuBLAS +def _set_cublas_allow_bf16_reduced_precision_reduction( + arg: _bool, + allow_splitk: _bool = ..., +) -> None: ... # THPModule_setAllowBF16ReductionCuBLAS +def _get_cublas_allow_fp16_accumulation() -> ( + _bool +): ... # THPModule_allowFP16AccumulationCuBLAS +def _set_cublas_allow_fp16_accumulation( + arg: _bool, +) -> None: ... # THPModule_setAllowFP16AccumulationCuBLAS +def _get_sm_carveout_experimental() -> _int | None: ... +def _set_sm_carveout_experimental(arg: _int | None) -> None: ... +def _set_conj(x: Tensor, conj: _bool) -> None: ... +def _set_neg(x: Tensor, neg: _bool) -> None: ... +def _set_meta_in_tls_dispatch_include(meta_in_tls: _bool) -> None: ... +def _autocast_supported_devices() -> list[str]: ... +def _meta_in_tls_dispatch_include() -> _bool: ... +def _stash_obj_in_tls(key: str, arg: Any) -> None: ... +def _get_obj_in_tls(key: str) -> Any: ... +def _is_key_in_tls(key: str) -> _bool: ... +def _select_batch_norm_backend(*args, **kwargs) -> BatchNormBackend: ... +def _select_conv_backend(*args, **kwargs) -> ConvBackend: ... +def _conv_determine_backend_memory_format( + input: Tensor, + weight: Tensor, + backend: ConvBackend, +) -> memory_format: ... +def _has_storage(x: Tensor) -> _bool: ... +def _construct_storage_from_data_pointer( + data_ptr: _int, + device: torch.device, + size: _int, +) -> Storage: ... +def _should_allow_numbers_as_tensors(func_name: str) -> _bool: ... +def _group_tensors_by_device_and_dtype( + nested_tensorlists: list[list[Tensor | None]], + with_indices: _bool = False, +) -> dict[ + tuple[torch.device, torch.dtype], + tuple[list[list[Tensor | None]], list[_int]], +]: ... +def _initCrashHandler() -> None: ... +def _set_warn_on_accumulate_grad_stream_mismatch(enabled: _bool) -> None: ... + +# NB: There is no Capsule type in typing, see +# https://github.com/python/cpython/issues/109562 +def _to_dlpack( + data: Tensor, + dl_device: tuple[IntEnum, _int] | None = None, + copy: _bool | None = None, +) -> Any: ... # THPModule_toDLPack +def _to_dlpack_versioned( + data: Tensor, + dl_device: tuple[IntEnum, _int] | None = None, + copy: _bool | None = None, +) -> Any: ... # THPModule_toDLPackVersioned +def _from_dlpack(data: Any) -> Tensor: ... # THPModule_fromDLPack +def _torchDeviceToDLDevice( + device: torch.device, +) -> tuple[_int, _int]: ... # THPModule_torchDeviceToDLDevice +def _dlpack_exchange_api() -> object: ... # THPModule_DLPackExchangeAPI +def _get_cpp_backtrace( + frames_to_skip: _int, + maximum_number_of_frames: _int, +) -> str: ... # THPModule_getCppBacktrace +def set_flush_denormal(arg: _bool) -> _bool: ... # THPModule_setFlushDenormal +def get_default_dtype() -> _dtype: ... # THPModule_getDefaultDtype +def _get_default_device() -> str: ... # THPModule_getDefaultDevice +def _get_qengine() -> _int: ... # THPModule_qEngine +def _set_qengine(qengine: _int) -> None: ... # THPModule_setQEngine +def _supported_qengines() -> list[_int]: ... # THPModule_supportedQEngines +def _is_xnnpack_enabled() -> _bool: ... # THPModule_isEnabledXNNPACK +def _check_sparse_tensor_invariants() -> ( + _bool +): ... # THPModule_checkSparseTensorInvariants +def _set_check_sparse_tensor_invariants( + arg: _bool, +) -> None: ... # THPModule_setCheckSparseTensorInvariants +def _is_default_mobile_cpu_allocator_set() -> ( + _bool +): ... # THPModule_isDefaultMobileCPUAllocatorSet +def _set_default_mobile_cpu_allocator() -> ( + None +): ... # THPModule_setDefaultMobileCPUAllocator +def _unset_default_mobile_cpu_allocator() -> ( + None +): ... # THPModule_unsetDefaultMobileCPUAllocator +def _is_torch_function_enabled() -> _bool: ... # THPModule_isEnabledTorchFunction +def _is_torch_function_all_disabled() -> ( + _bool +): ... # THPModule_isAllDisabledTorchFunction +def _has_torch_function( + args: Iterable[Any], +) -> _bool: ... # THPModule_has_torch_function +def _has_torch_function_unary(Any) -> _bool: ... # THPModule_has_torch_function_unary +def _has_torch_function_variadic( + *args: Any, +) -> _bool: ... # THPModule_has_torch_function_variadic +def _vmapmode_increment_nesting() -> _int: ... # THPModule_vmapmode_increment_nesting +def _vmapmode_decrement_nesting() -> _int: ... # THPModule_vmapmode_decrement_nesting +def _log_api_usage_once(str) -> None: ... # LogAPIUsageOnceFromPython +def _log_api_usage_metadata( + event: str, + metadata_map: dict[str, str], +) -> None: ... # LogAPIUsageMetadataFromPython +def _demangle(str) -> str: ... # c10::demangle +def _disabled_torch_function_impl( + func: Callable, + types: Iterable[type], + args: tuple, + kwargs: dict, +) -> Any: ... # THPModule_disable_torch_function +def _disabled_torch_dispatch_impl( + func: Callable, + types: Iterable[type], + args: tuple, + kwargs: dict, +) -> Any: ... # THPModule_disable_dispatch_function +def _get_linalg_preferred_backend() -> _LinalgBackend: ... +def _set_linalg_preferred_backend(arg: _LinalgBackend): ... +def _get_fp32_precision_getter(backend: str, op: str) -> str: ... +def _set_fp32_precision_setter(backend: str, op: str, value: str) -> str: ... +def _ensureCUDADeviceGuardSet() -> None: ... + +class _LinalgBackend: + Default: _LinalgBackend + Cusolver: _LinalgBackend + Magma: _LinalgBackend + +# mypy error: +# Detected enum "torch._C.BatchNormBackend" in a type stub with zero +# members. There is a chance this is due to a recent change in the semantics +# of enum membership. If so, use `member = value` to mark an enum member, +# instead of `member: type` +class BatchNormBackend(Enum): ... # type: ignore[misc] + +def _get_blas_preferred_backend() -> _BlasBackend: ... +def _set_blas_preferred_backend(arg: _BlasBackend): ... + +class _BlasBackend: + Default: _BlasBackend + Cublas: _BlasBackend + Cublaslt: _BlasBackend + Ck: _BlasBackend + +def _get_rocm_fa_preferred_backend() -> torch._C._ROCmFABackend: ... +def _set_rocm_fa_preferred_backend(arg: torch._C._ROCmFABackend): ... + +class _ROCmFABackend: + Default: _ROCmFABackend + AOTriton: _ROCmFABackend + Ck: _ROCmFABackend + +# mypy error: +# Error (MYPY) [misc] +# Detected enum "torch._C.ConvBackend" in a type stub with zero members. +# There is a chance this is due to a recent change in the semantics of enum +# membership. If so, use `member = value` to mark an enum member, instead of +# `member: type` +class ConvBackend(Enum): ... # type: ignore[misc] + +class Tag(Enum): + core = 0 + cudagraph_unsafe = 1 + data_dependent_output = 2 + dynamic_output_shape = 3 + flexible_layout = 4 + generated = 5 + inplace_view = 6 + maybe_aliasing_or_mutating = 7 + needs_contiguous_strides = 8 + needs_exact_strides = 9 + needs_fixed_stride_order = 10 + nondeterministic_bitwise = 11 + nondeterministic_seeded = 12 + out_variant = 13 + pointwise = 14 + pt2_compliant_tag = 15 + reduction = 16 + view_copy = 17 + +# Defined in `valgrind.h` and `callgrind.h` respectively. +def _valgrind_supported_platform() -> _bool: ... # NVALGRIND +def _valgrind_toggle() -> None: ... # CALLGRIND_TOGGLE_COLLECT +def _valgrind_toggle_and_dump_stats() -> ( + None +): ... # CALLGRIND_TOGGLE_COLLECT and CALLGRIND_DUMP_STATS + +has_openmp: _bool +has_mkl: _bool +_has_kleidiai: _bool +_has_mps: _bool +has_lapack: _bool +_has_cuda: _bool +_has_magma: _bool +_has_xpu: _bool +_has_mkldnn: _bool +_has_mkldnn_acl: _bool +_has_cudnn: _bool +_has_cusparselt: _bool +has_spectral: _bool +_GLIBCXX_USE_CXX11_ABI: _bool +default_generator: Generator + +# Defined in torch/csrc/autograd/init.cpp +def _set_grad_enabled(enabled: _bool) -> None: ... +def is_grad_enabled() -> _bool: ... +def _set_fwd_grad_enabled(enabled: _bool) -> None: ... +def _is_fwd_grad_enabled() -> _bool: ... +def _any_requires_grad(*args, **kwargs) -> _bool: ... +def _any_output_is_alias_to_input_or_output(*args, **kwargs) -> _bool: ... +def is_inference_mode_enabled() -> _bool: ... +@overload +def set_autocast_enabled(device_type: str, enabled: _bool) -> None: ... +@overload +def set_autocast_enabled(enabled: _bool) -> None: ... +@overload +def is_autocast_enabled(device_type: str) -> _bool: ... +@overload +def is_autocast_enabled() -> _bool: ... +def set_autocast_dtype(device_type: str, dtype: _dtype) -> None: ... +def get_autocast_dtype(device_type: str) -> _dtype: ... +def clear_autocast_cache() -> None: ... +def set_autocast_cpu_enabled(enabled: _bool) -> None: ... +def is_autocast_cpu_enabled() -> _bool: ... +def _is_any_autocast_enabled() -> _bool: ... +def _is_autocast_available(device_type: str) -> _bool: ... +def set_autocast_cpu_dtype(dtype: _dtype) -> None: ... +def set_autocast_gpu_dtype(dtype: _dtype) -> None: ... +def get_autocast_cpu_dtype() -> _dtype: ... +def get_autocast_gpu_dtype() -> _dtype: ... +def autocast_increment_nesting() -> _int: ... +def autocast_decrement_nesting() -> _int: ... +def is_autocast_cache_enabled() -> _bool: ... +def set_autocast_cache_enabled(enabled: _bool) -> None: ... +def _increment_version(tensors: Iterable[Tensor]) -> None: ... +def set_anomaly_enabled(enabled: _bool, check_nan: _bool = True) -> None: ... +def is_anomaly_enabled() -> _bool: ... +def is_anomaly_check_nan_enabled() -> _bool: ... +def _is_multithreading_enabled() -> _bool: ... +def _set_multithreading_enabled(enabled: _bool) -> None: ... +def _set_view_replay_enabled(enabled: _bool) -> None: ... +def _is_view_replay_enabled() -> _bool: ... +def _set_graph_exec_group(group: GraphExecGroup | None) -> None: ... +def _get_graph_exec_group() -> GraphExecGroup | None: ... +def _enter_dual_level() -> _int: ... +def _exit_dual_level(level: _int) -> None: ... +def _make_dual(tensor: Tensor, tangent: Tensor, level: _int) -> Tensor: ... +def _unpack_dual(tensor: Tensor, level: _int) -> Tensor: ... +def __set_forward_AD_enabled(enabled: _bool) -> None: ... +def __is_forward_AD_enabled() -> _bool: ... +def _register_default_hooks(pack_hook: Callable, unpack_hook: Callable) -> None: ... +def _reset_default_hooks() -> None: ... +def _is_torch_function_mode_enabled() -> _bool: ... +def _push_on_torch_function_stack(cls: Any) -> None: ... +def _pop_torch_function_stack() -> Any: ... +def _get_function_stack_at(idx: _int) -> Any: ... +def _len_torch_function_stack() -> _int: ... +def _set_torch_dispatch_mode(cls: Any) -> None: ... +def _push_on_torch_dispatch_stack(cls: TorchDispatchMode) -> None: ... +def _pop_torch_dispatch_stack(mode_key: _TorchDispatchModeKey | None = None) -> Any: ... +def _get_dispatch_mode(mode_key: _TorchDispatchModeKey | None) -> Any: ... +def _unset_dispatch_mode(mode: _TorchDispatchModeKey) -> TorchDispatchMode | None: ... +def _set_dispatch_mode(mode: TorchDispatchMode) -> None: ... +def _get_dispatch_stack_at(idx: _int) -> Any: ... +def _len_torch_dispatch_stack() -> _int: ... +def _activate_gpu_trace() -> None: ... + +class _DisableTorchDispatch: + def __init__(self) -> None: ... + def __enter__(self): ... + def __exit__(self, *exc_info: object) -> None: ... + +class _EnableTorchFunction: + def __init__(self) -> None: ... + def __enter__(self): ... + def __exit__(self, *exc_info: object) -> None: ... + +class _EnablePythonDispatcher: + def __init__(self) -> None: ... + def __enter__(self): ... + def __exit__(self, *exc_info: object) -> None: ... + +class _DisablePythonDispatcher: + def __init__(self) -> None: ... + def __enter__(self): ... + def __exit__(self, *exc_info: object) -> None: ... + +class _EnablePreDispatch: + def __init__(self) -> None: ... + def __enter__(self): ... + def __exit__(self, *exc_info: object) -> None: ... + +class _DisableFuncTorch: + def __init__(self) -> None: ... + def __enter__(self): ... + def __exit__(self, *exc_info: object) -> None: ... + +class _DisableAutocast: + def __init__(self) -> None: ... + def __enter__(self): ... + def __exit__(self, *exc_info: object) -> None: ... + +class _InferenceMode: + def __init__(self, enabled: _bool) -> None: ... + def __enter__(self): ... + def __exit__(self, *exc_info: object) -> None: ... + +def _set_autograd_fallback_mode(mode: str) -> None: ... +def _get_autograd_fallback_mode() -> str: ... + +# Defined in torch/csrc/jit/python/script_init.cpp +class LoggerBase: ... +class NoopLogger(LoggerBase): ... +class LockingLogger(LoggerBase): ... + +class AggregationType(Enum): + SUM = 0 + AVG = 1 + +class FileCheck: + def run(self, test_string: str) -> None: ... + def check(self, test_string: str) -> FileCheck: ... + def check_not(self, test_string: str) -> FileCheck: ... + def check_same(self, test_string: str) -> FileCheck: ... + def check_next(self, test_string: str) -> FileCheck: ... + def check_count( + self, + test_string: str, + count: _int, + exactly: _bool = False, + ) -> FileCheck: ... + def check_dag(self, test_string: str) -> FileCheck: ... + def check_source_highlighted(self, test_string: str) -> FileCheck: ... + def check_regex(self, test_string: str) -> FileCheck: ... + +# Defined in torch/csrc/jit/python/init.cpp +class PyTorchFileReader: + @overload + def __init__(self, name: str) -> None: ... + @overload + def __init__(self, buffer: IO[bytes]) -> None: ... + def get_record(self, name: str) -> bytes: ... + def get_all_records(self) -> list[str]: ... + def serialization_id(self) -> str: ... + +class PyTorchFileWriter: + @overload + def __init__( + self, + name: str, + compute_crc32: _bool = True, + storage_alignment: _int = 64, + ) -> None: ... + @overload + def __init__( + self, + buffer: IO[bytes], + compute_crc32: _bool = True, + storage_alignment: _int = 64, + ) -> None: ... + def write_record( + self, + name: str, + data: Storage | bytes | _int, + size: _int, + ) -> None: ... + def write_end_of_file(self) -> None: ... + def set_min_version(self, version: _int) -> None: ... + def get_all_written_records(self) -> list[str]: ... + def archive_name(self) -> str: ... + def serialization_id(self) -> str: ... + +def _jit_get_inline_everything_mode() -> _bool: ... +def _jit_set_inline_everything_mode(enabled: _bool) -> None: ... +def _jit_get_logging_option() -> str: ... +def _jit_set_logging_option(option: str) -> None: ... +def _jit_set_logging_stream(stream_name: str) -> None: ... +def _jit_pass_cse(Graph) -> _bool: ... +def _jit_pass_dce(Graph) -> None: ... +def _jit_pass_dce_graph(Graph) -> None: ... +def _jit_pass_lint(Graph) -> None: ... +def _register_opaque_type(type_name: str) -> None: ... +def _is_opaque_type_registered(type_name: str) -> _bool: ... + +# Defined in torch/csrc/jit/python/python_custom_class.cpp +def _get_custom_class_python_wrapper(name: str, attr: str) -> Any: ... + +# Defined in torch/csrc/Module.cpp +def _rename_privateuse1_backend(backend: str) -> None: ... +def _get_privateuse1_backend_name() -> str: ... + +# Defined in torch/csrc/Generator.cpp +class Generator: + device: _device + def __init__(self, device: DeviceLikeType | None = None) -> None: ... + def __reduce__( + self, + ) -> tuple[type[Generator], tuple[_device], tuple[_int, _int | None, Tensor]]: ... + def __setstate__(self, state: tuple[_int, _int | None, Tensor]) -> None: ... + def get_state(self) -> Tensor: ... + def set_state(self, _new_state: Tensor) -> Generator: ... + def clone_state(self) -> Generator: ... + def graphsafe_get_state(self) -> Generator: ... + def graphsafe_set_state(self, _new_state: Generator) -> Generator: ... + def set_offset(self, offset: _int) -> Generator: ... + def get_offset(self) -> _int: ... + def manual_seed(self, seed: _int) -> Generator: ... + def seed(self) -> _int: ... + def initial_seed(self) -> _int: ... + +# Defined in torch/csrc/utils/python_dispatch.cpp + +class _DispatchOperatorHandle: + def schema(self) -> FunctionSchema: ... + def debug(self) -> str: ... + def redispatch_boxed(self, keyset: DispatchKeySet, *args, **kwargs) -> Any: ... + +class _DispatchModule: + def reset(self) -> None: ... + def def_(self, schema: str, alias: str = "") -> _DispatchModule: ... + def def_legacy(self, schema: str) -> _DispatchModule: ... + def def_name_t_t( + self, + name: str, + dispatch: str, + debug: str = "default_def_name_t_t", + ) -> _DispatchModule: ... + def def_schema_t_t( + self, + schema: str, + dispatch: str, + alias: str, + debug: str = "default_def_schema_t_t", + ) -> _DispatchModule: ... + def impl_t_t( + self, + name: str, + dispatch: str, + debug: str = "impl_t_t", + ) -> _DispatchModule: ... + def impl_with_aoti_compile( + self, + ns: str, + op_name_with_overload: str, + dispatch: _dispatchkey, + ) -> None: ... + def impl(self, name: str, dispatch: _dispatchkey, func: Callable) -> None: ... + def define(self, schema: str, alias: str = "") -> str: ... + def fallback_fallthrough(self, dispatch: str = "") -> _DispatchModule: ... + def fallback( + self, + dispatch: _dispatchkey, + func: Callable, + with_keyset: _bool = False, + ) -> None: ... + +_after_ADInplaceOrView_keyset: DispatchKeySet +_after_autograd_keyset: DispatchKeySet + +class _SafeKernelFunction: + def call_boxed(self, keyset: DispatchKeySet, *args, **kwargs) -> Any: ... + @property + def op_handle(self) -> _DispatchOperatorHandle: ... + +def _dispatch_library( + kind: str, + name: str, + dispatch: str, + file: str = "", + linenum: Any = 0, +) -> _DispatchModule: ... +def _dispatch_dump(name: str) -> str: ... +def _dispatch_dump_table(name: str) -> str: ... +def _dispatch_check_invariants(name: str) -> None: ... +def _dispatch_check_all_invariants() -> None: ... +def _dispatch_call_boxed(handle: _DispatchOperatorHandle, *args, **kwargs) -> Any: ... +def _dispatch_find_schema_or_throw( + name: str, + overload_name: str, +) -> _DispatchOperatorHandle: ... +def _dispatch_set_report_error_callback( + handle: _DispatchOperatorHandle, + callback: Callable, +) -> None: ... +def _dispatch_has_kernel(name: str) -> _bool: ... +def _dispatch_has_kernel_for_dispatch_key( + name: str, + dispatch: _dispatchkey, +) -> _bool: ... +def _dispatch_has_kernel_for_any_dispatch_key( + name: str, + dispatch_key_set: DispatchKeySet, +) -> _bool: ... +def _dispatch_kernel_for_dispatch_key_is_fallthrough( + name: str, + dispatch: _dispatchkey, +) -> _bool: ... +def _dispatch_has_computed_kernel_for_dispatch_key( + name: str, + dispatch: _dispatchkey, +) -> _bool: ... +def _dispatch_get_computed_kernel_for_dispatch_key( + name: str, + dispatch: _dispatchkey, +) -> _SafeKernelFunction: ... +def _dispatch_find_dangling_impls() -> list[str]: ... +def _dispatch_get_all_op_names() -> list[str]: ... +def _dispatch_tls_set_dispatch_key_excluded( + dispatch: _dispatchkey, + val: _bool, +) -> None: ... +def _dispatch_tls_is_dispatch_key_excluded(dispatch: _dispatchkey) -> _bool: ... +def _dispatch_tls_set_dispatch_key_included( + dispatch: _dispatchkey, + val: _bool, +) -> None: ... +def _dispatch_tls_is_dispatch_key_included(dispatch: _dispatchkey) -> _bool: ... +def _dispatch_isTensorSubclassLike(tensor: Tensor) -> _bool: ... +def _dispatch_key_name(dispatch: _dispatchkey) -> str: ... +def _dispatch_key_for_device(device_type: str) -> str: ... +def _parse_dispatch_key(key: str) -> DispatchKey | None: ... +def _dispatch_key_parse(dispatch: _dispatchkey) -> DispatchKey: ... +def _dispatch_num_backends() -> _int: ... +def _dispatch_pystub(name: str, overload: str) -> tuple[str, str] | None: ... +def _dispatch_is_alias_key(dispatch: _dispatchkey) -> _bool: ... +def _functionality_to_backend_keys(dispatch: _dispatchkey) -> list[DispatchKey]: ... +def _functionalization_reapply_views_tls() -> _bool: ... +def _only_lift_cpu_tensors() -> _bool: ... +def _set_only_lift_cpu_tensors(value: _bool) -> None: ... +def _set_throw_on_mutable_data_ptr(tensor: Tensor) -> None: ... +def _set_warn_deprecated_on_mutable_data_ptr(tensor: Tensor) -> None: ... + +class DispatchKey(Enum): + Undefined = ... + FPGA = ... + MAIA = ... + Vulkan = ... + Metal = ... + MKLDNN = ... + OpenGL = ... + OpenCL = ... + IDEEP = ... + CustomRNGKeyId = ... + MkldnnCPU = ... + Sparse = ... + SparseCsr = ... + NestedTensor = ... + Dense = ... + PythonTLSSnapshot = ... + PreDispatch = ... + PythonDispatcher = ... + Python = ... + FuncTorchDynamicLayerBackMode = ... + ZeroTensor = ... + Conjugate = ... + Negative = ... + BackendSelect = ... + Named = ... + AutogradOther = ... + AutogradFunctionality = ... + AutogradNestedTensor = ... + Tracer = ... + Autocast = ... + AutocastCPU = ... + AutocastCUDA = ... + Batched = ... + VmapMode = ... + FuncTorchGradWrapper = ... + FuncTorchBatched = ... + BatchedNestedTensor = ... + FuncTorchVmapMode = ... + FuncTorchDynamicLayerFrontMode = ... + Functionalize = ... + TESTING_ONLY_GenericWrapper = ... + TESTING_ONLY_GenericMode = ... + ADInplaceOrView = ... + Autograd = ... + CompositeImplicitAutograd = ... + CompositeImplicitAutogradNestedTensor = ... + CompositeExplicitAutograd = ... + CompositeExplicitAutogradNonFunctional = ... + FuncTorchBatchedDecomposition = ... + CPU = ... + CUDA = ... + HIP = ... + XLA = ... + MTIA = ... + MPS = ... + IPU = ... + XPU = ... + HPU = ... + VE = ... + Lazy = ... + Meta = ... + PrivateUse1 = ... + PrivateUse2 = ... + PrivateUse3 = ... + QuantizedCPU = ... + QuantizedCUDA = ... + QuantizedHIP = ... + QuantizedXLA = ... + QuantizedMTIA = ... + QuantizedMPS = ... + QuantizedIPU = ... + QuantizedXPU = ... + QuantizedHPU = ... + QuantizedVE = ... + QuantizedLazy = ... + QuantizedMeta = ... + QuantizedPrivateUse1 = ... + QuantizedPrivateUse2 = ... + QuantizedPrivateUse3 = ... + SparseCPU = ... + SparseCUDA = ... + SparseHIP = ... + SparseXLA = ... + SparseMTIA = ... + SparseMPS = ... + SparseIPU = ... + SparseXPU = ... + SparseHPU = ... + SparseVE = ... + SparseLazy = ... + SparseMeta = ... + SparsePrivateUse1 = ... + SparsePrivateUse2 = ... + SparsePrivateUse3 = ... + SparseCsrCPU = ... + SparseCsrCUDA = ... + SparseCsrHIP = ... + SparseCsrXLA = ... + SparseCsrMTIA = ... + SparseCsrMPS = ... + SparseCsrIPU = ... + SparseCsrXPU = ... + SparseCsrHPU = ... + SparseCsrVE = ... + SparseCsrLazy = ... + SparseCsrMeta = ... + SparseCsrPrivateUse1 = ... + SparseCsrPrivateUse2 = ... + SparseCsrPrivateUse3 = ... + NestedTensorCPU = ... + NestedTensorCUDA = ... + NestedTensorHIP = ... + NestedTensorXLA = ... + NestedTensorMTIA = ... + NestedTensorMPS = ... + NestedTensorIPU = ... + NestedTensorXPU = ... + NestedTensorHPU = ... + NestedTensorVE = ... + NestedTensorLazy = ... + NestedTensorMeta = ... + NestedTensorPrivateUse1 = ... + NestedTensorPrivateUse2 = ... + NestedTensorPrivateUse3 = ... + AutogradCPU = ... + AutogradCUDA = ... + AutogradHIP = ... + AutogradXLA = ... + AutogradMTIA = ... + AutogradMPS = ... + AutogradIPU = ... + AutogradXPU = ... + AutogradHPU = ... + AutogradVE = ... + AutogradLazy = ... + AutogradMeta = ... + AutogradPrivateUse1 = ... + AutogradPrivateUse2 = ... + AutogradPrivateUse3 = ... + +class DispatchKeySet: + def __init__(self, key: DispatchKey) -> None: ... + def __or__(self, other: DispatchKeySet) -> DispatchKeySet: ... + def __sub__(self, other: DispatchKeySet) -> DispatchKeySet: ... + def __and__(self, other: DispatchKeySet) -> DispatchKeySet: ... + def raw_repr(self) -> _int: ... + @staticmethod + def from_raw_repr(raw: _int) -> DispatchKeySet: ... + def highestPriorityTypeId(self) -> DispatchKey: ... + def has(self, k: _dispatchkey) -> _bool: ... + def add(self, k: _dispatchkey) -> DispatchKeySet: ... + def remove(self, k: _dispatchkey) -> DispatchKeySet: ... + +_dispatch_autogradother_backends: DispatchKeySet +_additional_keys_to_prop_for_wrapper_tensors: DispatchKeySet + +def _dispatch_has_backend_fallback(dispatch: _dispatchkey) -> _bool: ... +def _dispatch_keyset_full_after(t: _dispatchkey) -> DispatchKeySet: ... +def _dispatch_keyset_full() -> DispatchKeySet: ... +def _dispatch_keyset_to_string(keyset: DispatchKeySet) -> str: ... +def _dispatch_get_backend_keyset_from_autograd( + dispatch: _dispatchkey, +) -> DispatchKeySet: ... +def _dispatch_keys(tensor: Tensor) -> DispatchKeySet: ... +def _dispatch_tls_local_exclude_set() -> DispatchKeySet: ... +def _dispatch_tls_local_include_set() -> DispatchKeySet: ... +def _dispatch_is_included_in_alias( + dispatch_a: _dispatchkey, + dispatch_b: _dispatchkey, +) -> _bool: ... +def _propagate_xla_data(a: Tensor, b: Tensor) -> None: ... +def _replace_(a: Tensor, b: Tensor) -> None: ... +def _commit_update(a: Tensor) -> None: ... + +class _ExcludeDispatchKeyGuard: + def __init__(self, keyset: DispatchKeySet) -> None: ... + def __enter__(self): ... + def __exit__(self, *exc_info: object) -> None: ... + +class _IncludeDispatchKeyGuard: + def __init__(self, k: DispatchKey) -> None: ... + def __enter__(self): ... + def __exit__(self, *exc_info: object) -> None: ... + +class _ForceDispatchKeyGuard: + def __init__(self, include: DispatchKeySet, exclude: DispatchKeySet) -> None: ... + def __enter__(self): ... + def __exit__(self, *exc_info: object) -> None: ... + +class _PreserveDispatchKeyGuard: + def __init__(self) -> None: ... + def __enter__(self): ... + def __exit__(self, *exc_info: object) -> None: ... + +class _AutoDispatchBelowAutograd: + def __init__(self) -> None: ... + def __enter__(self): ... + def __exit__(self, *exc_info: object) -> None: ... + +class _AutoDispatchBelowADInplaceOrView: + def __init__(self) -> None: ... + def __enter__(self): ... + def __exit__(self, *exc_info: object) -> None: ... + +def _dispatch_print_registrations_for_dispatch_key(dispatch_key: str = "") -> None: ... +def _dispatch_get_registrations_for_dispatch_key( + dispatch_key: str = "", +) -> list[str]: ... +def _are_functorch_transforms_active() -> _bool: ... + +# Define in torch/csrc/autograd/init.cpp +def _set_python_dispatcher(dispatcher: object) -> None: ... +def _get_nested_int(id: _int, coeff: _int) -> SymInt: ... +def _get_constant_bool_symnode(val: _bool) -> Any: ... + +class _TorchDispatchModeKey(Enum): + FAKE = ... + PROXY = ... + FUNCTIONAL = ... + +class _SetExcludeDispatchKeyGuard: + def __init__(self, k: DispatchKey, enabled: _bool) -> None: ... + def __enter__(self): ... + def __exit__(self, *exc_info: object) -> None: ... + +def _get_dtensor_allow_implicit_replication() -> _bool: ... +def _set_dtensor_allow_implicit_replication(value: _bool) -> None: ... + +# Defined in torch/csrc/utils/schema_info.h +class _SchemaArgType(Enum): + input = ... + output = ... + +class _SchemaArgument: + type: _SchemaArgType + idx: _int + def __init__(self, type: _SchemaArgType, idx: _int) -> None: ... + +class _SchemaInfo: + def __init__(self, schema: FunctionSchema) -> None: ... + @overload + def is_mutable(self) -> _bool: ... + @overload + def is_mutable(self, name: str) -> _bool: ... + @overload + def is_mutable(self, argument: _SchemaArgument) -> _bool: ... + def has_argument(self, name: str) -> _bool: ... + def may_alias(self, lhs: _SchemaArgument, rhs: _SchemaArgument) -> _bool: ... + def may_contain_alias( + self, lhs: _SchemaArgument, rhs: _SchemaArgument + ) -> _bool: ... + def add_argument_value(self, name: str, value: object) -> None: ... + def add_argument_values(self, values: dict[str, object]) -> None: ... + +def _overlaps(lhs: object, rhs: object) -> _bool: ... + +# Defined in torch/csrc/utils/init.cpp +class BenchmarkConfig: + num_calling_threads: _int + num_worker_threads: _int + num_warmup_iters: _int + num_iters: _int + profiler_output_path: str + +class BenchmarkExecutionStats: + latency_avg_ms: _float + num_iters: _int + +class ThroughputBenchmark: + def __init__(self, module: Any) -> None: ... + def add_input(self, *args: Any, **kwargs: Any) -> None: ... + def run_once(self, *args: Any, **kwargs: Any) -> Any: ... + def benchmark(self, config: BenchmarkConfig) -> BenchmarkExecutionStats: ... + +# Defined in torch/csrc/Storage.cpp +class StorageBase: ... + +# TODO: where +class DoubleTensor(Tensor): ... +class FloatTensor(Tensor): ... +class BFloat16Tensor(Tensor): ... +class LongTensor(Tensor): ... +class IntTensor(Tensor): ... +class ShortTensor(Tensor): ... +class HalfTensor(Tensor): ... +class CharTensor(Tensor): ... +class ByteTensor(Tensor): ... +class BoolTensor(Tensor): ... + +# Defined in torch/csrc/autograd/python_engine.cpp +class _ImperativeEngine: + def queue_callback(self, callback: Callable[[], None]) -> None: ... + def run_backward(self, *args: Any, **kwargs: Any) -> tuple[Tensor, ...]: ... + def is_checkpoint_valid(self) -> _bool: ... + +# Defined in torch/csrc/autograd/python_variable.cpp +class _TensorMeta(type): ... + +_Index: TypeAlias = SupportsIndex | _bool | _int | slice | EllipsisType | Tensor | None | _NestedSequence[_bool | _int | slice | EllipsisType | Tensor | None] # fmt: skip + +# Defined in torch/csrc/autograd/python_variable.cpp +class TensorBase(metaclass=_TensorMeta): + requires_grad: _bool + retains_grad: _bool + shape: Size + data: Tensor + names: list[str] + device: _device + dtype: _dtype + grad_dtype: _dtype | None + layout: _layout + real: Tensor + imag: Tensor + T: Tensor + H: Tensor + mT: Tensor + mH: Tensor + ndim: _int + output_nr: _int + _version: _int + _base: Tensor | None + _cdata: _int + grad_fn: _Node | None + _grad_fn: Any + _grad: Tensor | None + grad: Tensor | None + _backward_hooks: dict[_int, Callable[[Tensor], Tensor | None]] | None + nbytes: _int + itemsize: _int + _has_symbolic_sizes_strides: _bool + + def _view_func_unsafe( + self, + new_base: Tensor, + symint_visitor_fn: Callable[[_int], _int] | None = None, + tensor_visitor_fn: Callable[[Tensor], Tensor] | None = None, + ): ... + def __abs__(self) -> Tensor: ... + def __add__(self, other: Tensor | Number | _complex) -> Tensor: ... + @overload + def __and__(self, other: Tensor) -> Tensor: ... + @overload + def __and__(self, other: Number | _complex) -> Tensor: ... + @overload + def __and__(self, other: Tensor | _int) -> Tensor: ... + def __bool__(self) -> _bool: ... + def __complex__(self) -> _complex: ... + def __contains__(self, item: Any, /) -> _bool: ... + def __div__(self, other: Tensor | Number | _complex) -> Tensor: ... + @overload + def __eq__(self, other: Tensor | Number | _complex) -> Tensor: ... # type: ignore[overload-overlap] + @overload + def __eq__(self, other: object) -> _bool: ... + def __float__(self) -> _float: ... + def __floordiv__(self, other: Tensor | Number | _complex) -> Tensor: ... + def __ge__(self, other: Tensor | Number | _complex) -> Tensor: ... + def __getitem__(self, indices: _Index | tuple[_Index, ...], /) -> Tensor: ... + def __gt__(self, other: Tensor | Number | _complex) -> Tensor: ... + def __iadd__(self, other: Tensor | Number | _complex) -> Tensor: ... # noqa: PYI034 + @overload + def __iand__(self, other: Tensor) -> Tensor: ... + @overload + def __iand__(self, other: Number | _complex) -> Tensor: ... + @overload + def __iand__(self, other: Tensor | _int) -> Tensor: ... + def __idiv__(self, other: Tensor | Number | _complex) -> Tensor: ... # noqa: PYI034 + def __ifloordiv__(self, other: Tensor | Number | _complex) -> Tensor: ... # noqa: PYI034 + @overload + def __ilshift__(self, other: Tensor) -> Tensor: ... + @overload + def __ilshift__(self, other: Number | _complex) -> Tensor: ... + @overload + def __ilshift__(self, other: Tensor | _int) -> Tensor: ... + def __imod__(self, other: Tensor | Number | _complex) -> Tensor: ... # noqa: PYI034 + def __imul__(self, other: Tensor | Number | _complex) -> Tensor: ... # noqa: PYI034 + def __index__(self) -> _int: ... + @overload + def __init__( + self, + *args: Any, + device: DeviceLikeType | None = None, + ) -> None: ... + @overload + def __init__(self, storage: Storage) -> None: ... + @overload + def __init__(self, other: Tensor) -> None: ... + @overload + def __init__( + self, + size: _size, + *, + device: DeviceLikeType | None = None, + ) -> None: ... + def __int__(self) -> _int: ... + def __invert__(self) -> Tensor: ... + @overload + def __ior__(self, other: Tensor) -> Tensor: ... + @overload + def __ior__(self, other: Number | _complex) -> Tensor: ... + @overload + def __ior__(self, other: Tensor | _int) -> Tensor: ... + @overload + def __irshift__(self, other: Tensor) -> Tensor: ... + @overload + def __irshift__(self, other: Number | _complex) -> Tensor: ... + @overload + def __irshift__(self, other: Tensor | _int) -> Tensor: ... + def __isub__(self, other: Tensor | Number | _complex) -> Tensor: ... # noqa: PYI034 + @overload + def __ixor__(self, other: Tensor) -> Tensor: ... + @overload + def __ixor__(self, other: Number | _complex) -> Tensor: ... + @overload + def __ixor__(self, other: Tensor | _int) -> Tensor: ... + def __le__(self, other: Tensor | Number | _complex) -> Tensor: ... + def __long__(self) -> _int: ... + @overload + def __lshift__(self, other: Tensor) -> Tensor: ... + @overload + def __lshift__(self, other: Number | _complex) -> Tensor: ... + @overload + def __lshift__(self, other: Tensor | _int) -> Tensor: ... + def __lt__(self, other: Tensor | Number | _complex) -> Tensor: ... + def __matmul__(self, other: Tensor | Number | _complex) -> Tensor: ... + def __mod__(self, other: Tensor | Number | _complex) -> Tensor: ... + def __mul__(self, other: Tensor | Number | _complex) -> Tensor: ... + @overload + def __ne__(self, other: Tensor | Number | _complex) -> Tensor: ... # type: ignore[overload-overlap] + @overload + def __ne__(self, other: object) -> _bool: ... + def __neg__(self) -> Tensor: ... + def __new__(cls, *args, **kwargs) -> Self: ... + def __nonzero__(self) -> _bool: ... + @overload + def __or__(self, other: Tensor) -> Tensor: ... + @overload + def __or__(self, other: Number | _complex) -> Tensor: ... + @overload + def __or__(self, other: Tensor | _int) -> Tensor: ... + def __pow__(self, other: Tensor | Number | _complex) -> Tensor: ... + def __radd__(self, other: Tensor | Number | _complex) -> Tensor: ... + def __rand__(self, other: Tensor | _int) -> Tensor: ... + def __rfloordiv__(self, other: Tensor | Number | _complex) -> Tensor: ... + def __rmul__(self, other: Tensor | Number | _complex) -> Tensor: ... + def __ror__(self, other: Tensor | _int) -> Tensor: ... + def __rpow__(self, other: Tensor | Number | _complex) -> Tensor: ... # type: ignore[has-type] + @overload + def __rshift__(self, other: Tensor) -> Tensor: ... + @overload + def __rshift__(self, other: Number | _complex) -> Tensor: ... + @overload + def __rshift__(self, other: Tensor | _int) -> Tensor: ... + def __rsub__(self, other: Tensor | Number | _complex) -> Tensor: ... + def __rtruediv__(self, other: Tensor | Number | _complex) -> Tensor: ... + def __rxor__(self, other: Tensor | _int) -> Tensor: ... + def __setitem__( + self, + indices: _Index | tuple[_Index, ...], + value: Tensor | Number, + /, + ) -> None: ... + def __sub__(self, other: Tensor | Number | _complex) -> Tensor: ... + def __truediv__(self, other: Tensor | Number | _complex) -> Tensor: ... + @overload + def __xor__(self, other: Tensor) -> Tensor: ... + @overload + def __xor__(self, other: Number | _complex) -> Tensor: ... + @overload + def __xor__(self, other: Tensor | _int) -> Tensor: ... + def _addmm_activation( + self, + mat1: Tensor, + mat2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + use_gelu: _bool = False, + ) -> Tensor: ... + def _autocast_to_full_precision( + self, + cuda_enabled: _bool, + cpu_enabled: _bool, + ) -> Tensor: ... + def _autocast_to_reduced_precision( + self, + cuda_enabled: _bool, + cpu_enabled: _bool, + cuda_dtype: _dtype, + cpu_dtype: _dtype, + ) -> Tensor: ... + def _coalesced_(self, coalesced: _bool) -> Tensor: ... + def _conj(self) -> Tensor: ... + def _conj_physical(self) -> Tensor: ... + def _dimI(self) -> _int: ... + def _dimV(self) -> _int: ... + @staticmethod + def _dtensor__new__( + cls: type[S], + local_tensor: Tensor, + spec: torch.distributed.tensor._dtensor_spec.DTensorSpec, + requires_grad: _bool, + ) -> S: ... + def _indices(self) -> Tensor: ... + def _is_all_true(self) -> Tensor: ... + def _is_any_true(self) -> Tensor: ... + def _is_view(self) -> _bool: ... + def _is_zerotensor(self) -> _bool: ... + def _lazy_clone(self) -> Tensor: ... + @staticmethod + def _make_subclass( + cls: type[S], + data: Tensor, + require_grad: _bool = False, + dispatch_strides: _bool = False, + dispatch_device: _bool = False, + device_for_backend_keys: _device | None = None, + ) -> S: ... + @staticmethod + def _make_wrapper_subclass( + cls: type[S], + size: Sequence[_int | SymInt], + strides: Sequence[_int | SymInt] | None = None, + storage_offset: _int | SymInt | None = None, + memory_format: torch.memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout = strided, + device: _device | None = None, + pin_memory: _bool = False, + requires_grad: _bool = False, + dispatch_sizes_strides_policy: str | None = None, + dispatch_device: _bool = False, + dispatch_layout: _bool = False, + _extra_dispatch_keys: torch.DispatchKeySet | None = None, + storage_size: _int | SymInt | None = None, + ) -> S: ... + def _neg_view(self) -> Tensor: ... + def _nested_tensor_size(self) -> Tensor: ... + def _nested_tensor_storage_offsets(self) -> Tensor: ... + def _nested_tensor_strides(self) -> Tensor: ... + def _nnz(self) -> _int: ... + def _philox_normal_( + self, + key: Tensor, + mean: _float = 0, + std: _float = 1, + ) -> Tensor: ... + def _philox_uniform_( + self, + key: Tensor, + low: _float = 0, + high: _float = 1, + ) -> Tensor: ... + def _sparse_mask_projection( + self, + mask: Tensor, + accumulate_matches: _bool = False, + ) -> Tensor: ... + def _to_dense( + self, + dtype: _dtype | None = None, + masked_grad: _bool | None = None, + ) -> Tensor: ... + @overload + def _to_sparse( + self, + *, + layout: _layout | None = None, + blocksize: _int | _size | None = None, + dense_dim: _int | None = None, + ) -> Tensor: ... + @overload + def _to_sparse(self, sparse_dim: _int) -> Tensor: ... + def _to_sparse_bsc( + self, + blocksize: _int | _size, + dense_dim: _int | None = None, + ) -> Tensor: ... + def _to_sparse_bsr( + self, + blocksize: _int | _size, + dense_dim: _int | None = None, + ) -> Tensor: ... + def _to_sparse_csc(self, dense_dim: _int | None = None) -> Tensor: ... + def _to_sparse_csr(self, dense_dim: _int | None = None) -> Tensor: ... + def _values(self) -> Tensor: ... + def abs(self) -> Tensor: + r""" + abs() -> Tensor + + See :func:`torch.abs` + """ + + def abs_(self) -> Tensor: + r""" + abs_() -> Tensor + + In-place version of :meth:`~Tensor.abs` + """ + + def absolute(self) -> Tensor: + r""" + absolute() -> Tensor + + Alias for :func:`abs` + """ + + def absolute_(self) -> Tensor: + r""" + absolute_() -> Tensor + + In-place version of :meth:`~Tensor.absolute` + Alias for :func:`abs_` + """ + + def acos(self) -> Tensor: + r""" + acos() -> Tensor + + See :func:`torch.acos` + """ + + def acos_(self) -> Tensor: + r""" + acos_() -> Tensor + + In-place version of :meth:`~Tensor.acos` + """ + + def acosh(self) -> Tensor: + r""" + acosh() -> Tensor + + See :func:`torch.acosh` + """ + + def acosh_(self) -> Tensor: + r""" + acosh_() -> Tensor + + In-place version of :meth:`~Tensor.acosh` + """ + + def add( + self, + other: Tensor | Number | _complex | torch.SymInt | torch.SymFloat, + *, + alpha: Number | _complex | None = 1, + out: Tensor | None = None, + ) -> Tensor: + r""" + add(other, *, alpha=1) -> Tensor + + Add a scalar or tensor to :attr:`self` tensor. If both :attr:`alpha` + and :attr:`other` are specified, each element of :attr:`other` is scaled by + :attr:`alpha` before being used. + + When :attr:`other` is a tensor, the shape of :attr:`other` must be + :ref:`broadcastable ` with the shape of the underlying + tensor + + See :func:`torch.add` + """ + + def add_( + self, + other: Tensor | Number | _complex | torch.SymInt | torch.SymFloat, + *, + alpha: Number | _complex | None = 1, + ) -> Tensor: + r""" + add_(other, *, alpha=1) -> Tensor + + In-place version of :meth:`~Tensor.add` + """ + + def addbmm( + self, + batch1: Tensor, + batch2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + ) -> Tensor: + r""" + addbmm(batch1, batch2, *, beta=1, alpha=1) -> Tensor + + See :func:`torch.addbmm` + """ + + def addbmm_( + self, + batch1: Tensor, + batch2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + ) -> Tensor: + r""" + addbmm_(batch1, batch2, *, beta=1, alpha=1) -> Tensor + + In-place version of :meth:`~Tensor.addbmm` + """ + + def addcdiv( + self, + tensor1: Tensor, + tensor2: Tensor, + *, + value: Number | _complex = 1, + ) -> Tensor: + r""" + addcdiv(tensor1, tensor2, *, value=1) -> Tensor + + See :func:`torch.addcdiv` + """ + + def addcdiv_( + self, + tensor1: Tensor, + tensor2: Tensor, + *, + value: Number | _complex = 1, + ) -> Tensor: + r""" + addcdiv_(tensor1, tensor2, *, value=1) -> Tensor + + In-place version of :meth:`~Tensor.addcdiv` + """ + + def addcmul( + self, + tensor1: Tensor, + tensor2: Tensor, + *, + value: Number | _complex = 1, + ) -> Tensor: + r""" + addcmul(tensor1, tensor2, *, value=1) -> Tensor + + See :func:`torch.addcmul` + """ + + def addcmul_( + self, + tensor1: Tensor, + tensor2: Tensor, + *, + value: Number | _complex = 1, + ) -> Tensor: + r""" + addcmul_(tensor1, tensor2, *, value=1) -> Tensor + + In-place version of :meth:`~Tensor.addcmul` + """ + + def addmm( + self, + mat1: Tensor, + mat2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + ) -> Tensor: + r""" + addmm(mat1, mat2, *, beta=1, alpha=1) -> Tensor + + See :func:`torch.addmm` + """ + + def addmm_( + self, + mat1: Tensor, + mat2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + ) -> Tensor: + r""" + addmm_(mat1, mat2, *, beta=1, alpha=1) -> Tensor + + In-place version of :meth:`~Tensor.addmm` + """ + + def addmv( + self, + mat: Tensor, + vec: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + ) -> Tensor: + r""" + addmv(mat, vec, *, beta=1, alpha=1) -> Tensor + + See :func:`torch.addmv` + """ + + def addmv_( + self, + mat: Tensor, + vec: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + ) -> Tensor: + r""" + addmv_(mat, vec, *, beta=1, alpha=1) -> Tensor + + In-place version of :meth:`~Tensor.addmv` + """ + + def addr( + self, + vec1: Tensor, + vec2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + ) -> Tensor: + r""" + addr(vec1, vec2, *, beta=1, alpha=1) -> Tensor + + See :func:`torch.addr` + """ + + def addr_( + self, + vec1: Tensor, + vec2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + ) -> Tensor: + r""" + addr_(vec1, vec2, *, beta=1, alpha=1) -> Tensor + + In-place version of :meth:`~Tensor.addr` + """ + + def adjoint(self) -> Tensor: + r""" + adjoint() -> Tensor + + Alias for :func:`adjoint` + """ + + def align_as(self, other: Tensor) -> Tensor: + r""" + align_as(other) -> Tensor + + Permutes the dimensions of the :attr:`self` tensor to match the dimension order + in the :attr:`other` tensor, adding size-one dims for any new names. + + This operation is useful for explicit broadcasting by names (see examples). + + All of the dims of :attr:`self` must be named in order to use this method. + The resulting tensor is a view on the original tensor. + + All dimension names of :attr:`self` must be present in ``other.names``. + :attr:`other` may contain named dimensions that are not in ``self.names``; + the output tensor has a size-one dimension for each of those new names. + + To align a tensor to a specific order, use :meth:`~Tensor.align_to`. + + Examples:: + + # Example 1: Applying a mask + >>> mask = torch.randint(2, [127, 128], dtype=torch.bool).refine_names('W', 'H') + >>> imgs = torch.randn(32, 128, 127, 3, names=('N', 'H', 'W', 'C')) + >>> imgs.masked_fill_(mask.align_as(imgs), 0) + + + # Example 2: Applying a per-channel-scale + >>> def scale_channels(input, scale): + >>> scale = scale.refine_names('C') + >>> return input * scale.align_as(input) + + >>> num_channels = 3 + >>> scale = torch.randn(num_channels, names=('C',)) + >>> imgs = torch.rand(32, 128, 128, num_channels, names=('N', 'H', 'W', 'C')) + >>> more_imgs = torch.rand(32, num_channels, 128, 128, names=('N', 'C', 'H', 'W')) + >>> videos = torch.randn(3, num_channels, 128, 128, 128, names=('N', 'C', 'H', 'W', 'D')) + + # scale_channels is agnostic to the dimension order of the input + >>> scale_channels(imgs, scale) + >>> scale_channels(more_imgs, scale) + >>> scale_channels(videos, scale) + + .. warning:: + The named tensor API is experimental and subject to change. + """ + + @overload + def align_to( + self, + order: Sequence[str | EllipsisType | None], + ellipsis_idx: _int, + ) -> Tensor: ... + @overload + def align_to(self, names: Sequence[str | EllipsisType | None]) -> Tensor: ... + @overload + def all(self) -> Tensor: + r""" + all(dim=None, keepdim=False) -> Tensor + + See :func:`torch.all` + """ + + @overload + def all(self, dim: _size | None = None, keepdim: _bool = False) -> Tensor: + r""" + all(dim=None, keepdim=False) -> Tensor + + See :func:`torch.all` + """ + + @overload + def all(self, dim: _int, keepdim: _bool = False) -> Tensor: + r""" + all(dim=None, keepdim=False) -> Tensor + + See :func:`torch.all` + """ + + @overload + def all( + self, + dim: str | EllipsisType | None, + keepdim: _bool = False, + ) -> Tensor: + r""" + all(dim=None, keepdim=False) -> Tensor + + See :func:`torch.all` + """ + + def allclose( + self, + other: Tensor, + rtol: _float = 1e-05, + atol: _float = 1e-08, + equal_nan: _bool = False, + ) -> _bool: + r""" + allclose(other, rtol=1e-05, atol=1e-08, equal_nan=False) -> Tensor + + See :func:`torch.allclose` + """ + + def amax(self, dim: _int | _size = (), keepdim: _bool = False) -> Tensor: + r""" + amax(dim=None, keepdim=False) -> Tensor + + See :func:`torch.amax` + """ + + def amin(self, dim: _int | _size = (), keepdim: _bool = False) -> Tensor: + r""" + amin(dim=None, keepdim=False) -> Tensor + + See :func:`torch.amin` + """ + + def aminmax( + self, + *, + dim: _int | None = None, + keepdim: _bool = False, + ) -> torch.return_types.aminmax: + r""" + aminmax(*, dim=None, keepdim=False) -> (Tensor min, Tensor max) + + See :func:`torch.aminmax` + """ + + def angle(self) -> Tensor: + r""" + angle() -> Tensor + + See :func:`torch.angle` + """ + + @overload + def any(self) -> Tensor: + r""" + any(dim=None, keepdim=False) -> Tensor + + See :func:`torch.any` + """ + + @overload + def any(self, dim: _size | None = None, keepdim: _bool = False) -> Tensor: + r""" + any(dim=None, keepdim=False) -> Tensor + + See :func:`torch.any` + """ + + @overload + def any(self, dim: _int, keepdim: _bool = False) -> Tensor: + r""" + any(dim=None, keepdim=False) -> Tensor + + See :func:`torch.any` + """ + + @overload + def any( + self, + dim: str | EllipsisType | None, + keepdim: _bool = False, + ) -> Tensor: + r""" + any(dim=None, keepdim=False) -> Tensor + + See :func:`torch.any` + """ + + def apply_(self, callable: Callable) -> Tensor: + r""" + apply_(callable) -> Tensor + + Applies the function :attr:`callable` to each element in the tensor, replacing + each element with the value returned by :attr:`callable`. + + .. note:: + + This function only works with CPU tensors and should not be used in code + sections that require high performance. + """ + + def arccos(self) -> Tensor: + r""" + arccos() -> Tensor + + See :func:`torch.arccos` + """ + + def arccos_(self) -> Tensor: + r""" + arccos_() -> Tensor + + In-place version of :meth:`~Tensor.arccos` + """ + + def arccosh(self) -> Tensor: + r""" + acosh() -> Tensor + + See :func:`torch.arccosh` + """ + + def arccosh_(self) -> Tensor: + r""" + acosh_() -> Tensor + + In-place version of :meth:`~Tensor.arccosh` + """ + + def arcsin(self) -> Tensor: + r""" + arcsin() -> Tensor + + See :func:`torch.arcsin` + """ + + def arcsin_(self) -> Tensor: + r""" + arcsin_() -> Tensor + + In-place version of :meth:`~Tensor.arcsin` + """ + + def arcsinh(self) -> Tensor: + r""" + arcsinh() -> Tensor + + See :func:`torch.arcsinh` + """ + + def arcsinh_(self) -> Tensor: + r""" + arcsinh_() -> Tensor + + In-place version of :meth:`~Tensor.arcsinh` + """ + + def arctan(self) -> Tensor: + r""" + arctan() -> Tensor + + See :func:`torch.arctan` + """ + + def arctan2(self, other: Tensor) -> Tensor: + r""" + arctan2(other) -> Tensor + + See :func:`torch.arctan2` + """ + + def arctan2_(self, other: Tensor) -> Tensor: + r""" + atan2_(other) -> Tensor + + In-place version of :meth:`~Tensor.arctan2` + """ + + def arctan_(self) -> Tensor: + r""" + arctan_() -> Tensor + + In-place version of :meth:`~Tensor.arctan` + """ + + def arctanh(self) -> Tensor: + r""" + arctanh() -> Tensor + + See :func:`torch.arctanh` + """ + + def arctanh_(self) -> Tensor: + r""" + arctanh_(other) -> Tensor + + In-place version of :meth:`~Tensor.arctanh` + """ + + def argmax(self, dim: _int | None = None, keepdim: _bool = False) -> Tensor: + r""" + argmax(dim=None, keepdim=False) -> LongTensor + + See :func:`torch.argmax` + """ + + def argmin(self, dim: _int | None = None, keepdim: _bool = False) -> Tensor: + r""" + argmin(dim=None, keepdim=False) -> LongTensor + + See :func:`torch.argmin` + """ + + @overload + def argsort( + self, + *, + stable: _bool, + dim: _int = -1, + descending: _bool = False, + ) -> Tensor: + r""" + argsort(dim=-1, descending=False) -> LongTensor + + See :func:`torch.argsort` + """ + + @overload + def argsort(self, dim: _int = -1, descending: _bool = False) -> Tensor: + r""" + argsort(dim=-1, descending=False) -> LongTensor + + See :func:`torch.argsort` + """ + + @overload + def argsort( + self, + dim: str | EllipsisType | None, + descending: _bool = False, + ) -> Tensor: + r""" + argsort(dim=-1, descending=False) -> LongTensor + + See :func:`torch.argsort` + """ + + def argwhere(self) -> Tensor: + r""" + argwhere() -> Tensor + + See :func:`torch.argwhere` + """ + + def as_strided( + self, + size: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + storage_offset: _int | SymInt | None = None, + ) -> Tensor: + r""" + as_strided(size, stride, storage_offset=None) -> Tensor + + See :func:`torch.as_strided` + """ + + def as_strided_( + self, + size: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + storage_offset: _int | SymInt | None = None, + ) -> Tensor: + r""" + as_strided_(size, stride, storage_offset=None) -> Tensor + + In-place version of :meth:`~Tensor.as_strided` + """ + + def as_strided_scatter( + self, + src: Tensor, + size: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + storage_offset: _int | SymInt | None = None, + ) -> Tensor: + r""" + as_strided_scatter(src, size, stride, storage_offset=None) -> Tensor + + See :func:`torch.as_strided_scatter` + """ + + def as_subclass(self, cls: type[S]) -> S: + r""" + as_subclass(cls) -> Tensor + + Makes a ``cls`` instance with the same data pointer as ``self``. Changes + in the output mirror changes in ``self``, and the output stays attached + to the autograd graph. ``cls`` must be a subclass of ``Tensor``. + """ + + def asin(self) -> Tensor: + r""" + asin() -> Tensor + + See :func:`torch.asin` + """ + + def asin_(self) -> Tensor: + r""" + asin_() -> Tensor + + In-place version of :meth:`~Tensor.asin` + """ + + def asinh(self) -> Tensor: + r""" + asinh() -> Tensor + + See :func:`torch.asinh` + """ + + def asinh_(self) -> Tensor: + r""" + asinh_() -> Tensor + + In-place version of :meth:`~Tensor.asinh` + """ + + def atan(self) -> Tensor: + r""" + atan() -> Tensor + + See :func:`torch.atan` + """ + + def atan2(self, other: Tensor) -> Tensor: + r""" + atan2(other) -> Tensor + + See :func:`torch.atan2` + """ + + def atan2_(self, other: Tensor) -> Tensor: + r""" + atan2_(other) -> Tensor + + In-place version of :meth:`~Tensor.atan2` + """ + + def atan_(self) -> Tensor: + r""" + atan_() -> Tensor + + In-place version of :meth:`~Tensor.atan` + """ + + def atanh(self) -> Tensor: + r""" + atanh() -> Tensor + + See :func:`torch.atanh` + """ + + def atanh_(self) -> Tensor: + r""" + atanh_(other) -> Tensor + + In-place version of :meth:`~Tensor.atanh` + """ + + def baddbmm( + self, + batch1: Tensor, + batch2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + ) -> Tensor: + r""" + baddbmm(batch1, batch2, *, beta=1, alpha=1) -> Tensor + + See :func:`torch.baddbmm` + """ + + def baddbmm_( + self, + batch1: Tensor, + batch2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + ) -> Tensor: + r""" + baddbmm_(batch1, batch2, *, beta=1, alpha=1) -> Tensor + + In-place version of :meth:`~Tensor.baddbmm` + """ + + @overload + def bernoulli(self, *, generator: Generator | None = None) -> Tensor: + r""" + bernoulli(*, generator=None) -> Tensor + + Returns a result tensor where each :math:`\texttt{result[i]}` is independently + sampled from :math:`\text{Bernoulli}(\texttt{self[i]})`. :attr:`self` must have + floating point ``dtype``, and the result will have the same ``dtype``. + + See :func:`torch.bernoulli` + """ + + @overload + def bernoulli( + self, + p: _float, + *, + generator: Generator | None = None, + ) -> Tensor: + r""" + bernoulli(*, generator=None) -> Tensor + + Returns a result tensor where each :math:`\texttt{result[i]}` is independently + sampled from :math:`\text{Bernoulli}(\texttt{self[i]})`. :attr:`self` must have + floating point ``dtype``, and the result will have the same ``dtype``. + + See :func:`torch.bernoulli` + """ + + @overload + def bernoulli_( + self, + p: Tensor, + *, + generator: Generator | None = None, + ) -> Tensor: + r""" + bernoulli_(p=0.5, *, generator=None) -> Tensor + + Fills each location of :attr:`self` with an independent sample from + :math:`\text{Bernoulli}(\texttt{p})`. :attr:`self` can have integral + ``dtype``. + + :attr:`p` should either be a scalar or tensor containing probabilities to be + used for drawing the binary random number. + + If it is a tensor, the :math:`\text{i}^{th}` element of :attr:`self` tensor + will be set to a value sampled from + :math:`\text{Bernoulli}(\texttt{p\_tensor[i]})`. In this case `p` must have + floating point ``dtype``. + + See also :meth:`~Tensor.bernoulli` and :func:`torch.bernoulli` + """ + + @overload + def bernoulli_( + self, + p: _float = 0.5, + *, + generator: Generator | None = None, + ) -> Tensor: + r""" + bernoulli_(p=0.5, *, generator=None) -> Tensor + + Fills each location of :attr:`self` with an independent sample from + :math:`\text{Bernoulli}(\texttt{p})`. :attr:`self` can have integral + ``dtype``. + + :attr:`p` should either be a scalar or tensor containing probabilities to be + used for drawing the binary random number. + + If it is a tensor, the :math:`\text{i}^{th}` element of :attr:`self` tensor + will be set to a value sampled from + :math:`\text{Bernoulli}(\texttt{p\_tensor[i]})`. In this case `p` must have + floating point ``dtype``. + + See also :meth:`~Tensor.bernoulli` and :func:`torch.bernoulli` + """ + + def bfloat16(self) -> Tensor: + r""" + bfloat16(memory_format=torch.preserve_format) -> Tensor + ``self.bfloat16()`` is equivalent to ``self.to(torch.bfloat16)``. See :func:`to`. + + Args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + + def bincount( + self, + weights: Tensor | None = None, + minlength: _int | SymInt = 0, + ) -> Tensor: + r""" + bincount(weights=None, minlength=0) -> Tensor + + See :func:`torch.bincount` + """ + + @overload + def bitwise_and(self, other: Tensor) -> Tensor: + r""" + bitwise_and() -> Tensor + + See :func:`torch.bitwise_and` + """ + + @overload + def bitwise_and(self, other: Number | _complex) -> Tensor: + r""" + bitwise_and() -> Tensor + + See :func:`torch.bitwise_and` + """ + + @overload + def bitwise_and_(self, other: Tensor) -> Tensor: + r""" + bitwise_and_() -> Tensor + + In-place version of :meth:`~Tensor.bitwise_and` + """ + + @overload + def bitwise_and_(self, other: Number | _complex) -> Tensor: + r""" + bitwise_and_() -> Tensor + + In-place version of :meth:`~Tensor.bitwise_and` + """ + + @overload + def bitwise_left_shift(self, other: Tensor) -> Tensor: + r""" + bitwise_left_shift(other) -> Tensor + + See :func:`torch.bitwise_left_shift` + """ + + @overload + def bitwise_left_shift(self, other: Number | _complex) -> Tensor: + r""" + bitwise_left_shift(other) -> Tensor + + See :func:`torch.bitwise_left_shift` + """ + + @overload + def bitwise_left_shift_(self, other: Tensor) -> Tensor: + r""" + bitwise_left_shift_(other) -> Tensor + + In-place version of :meth:`~Tensor.bitwise_left_shift` + """ + + @overload + def bitwise_left_shift_(self, other: Number | _complex) -> Tensor: + r""" + bitwise_left_shift_(other) -> Tensor + + In-place version of :meth:`~Tensor.bitwise_left_shift` + """ + + def bitwise_not(self) -> Tensor: + r""" + bitwise_not() -> Tensor + + See :func:`torch.bitwise_not` + """ + + def bitwise_not_(self) -> Tensor: + r""" + bitwise_not_() -> Tensor + + In-place version of :meth:`~Tensor.bitwise_not` + """ + + @overload + def bitwise_or(self, other: Tensor) -> Tensor: + r""" + bitwise_or() -> Tensor + + See :func:`torch.bitwise_or` + """ + + @overload + def bitwise_or(self, other: Number | _complex) -> Tensor: + r""" + bitwise_or() -> Tensor + + See :func:`torch.bitwise_or` + """ + + @overload + def bitwise_or_(self, other: Tensor) -> Tensor: + r""" + bitwise_or_() -> Tensor + + In-place version of :meth:`~Tensor.bitwise_or` + """ + + @overload + def bitwise_or_(self, other: Number | _complex) -> Tensor: + r""" + bitwise_or_() -> Tensor + + In-place version of :meth:`~Tensor.bitwise_or` + """ + + @overload + def bitwise_right_shift(self, other: Tensor) -> Tensor: + r""" + bitwise_right_shift(other) -> Tensor + + See :func:`torch.bitwise_right_shift` + """ + + @overload + def bitwise_right_shift(self, other: Number | _complex) -> Tensor: + r""" + bitwise_right_shift(other) -> Tensor + + See :func:`torch.bitwise_right_shift` + """ + + @overload + def bitwise_right_shift_(self, other: Tensor) -> Tensor: + r""" + bitwise_right_shift_(other) -> Tensor + + In-place version of :meth:`~Tensor.bitwise_right_shift` + """ + + @overload + def bitwise_right_shift_(self, other: Number | _complex) -> Tensor: + r""" + bitwise_right_shift_(other) -> Tensor + + In-place version of :meth:`~Tensor.bitwise_right_shift` + """ + + @overload + def bitwise_xor(self, other: Tensor) -> Tensor: + r""" + bitwise_xor() -> Tensor + + See :func:`torch.bitwise_xor` + """ + + @overload + def bitwise_xor(self, other: Number | _complex) -> Tensor: + r""" + bitwise_xor() -> Tensor + + See :func:`torch.bitwise_xor` + """ + + @overload + def bitwise_xor_(self, other: Tensor) -> Tensor: + r""" + bitwise_xor_() -> Tensor + + In-place version of :meth:`~Tensor.bitwise_xor` + """ + + @overload + def bitwise_xor_(self, other: Number | _complex) -> Tensor: + r""" + bitwise_xor_() -> Tensor + + In-place version of :meth:`~Tensor.bitwise_xor` + """ + + def bmm(self, mat2: Tensor) -> Tensor: + r""" + bmm(batch2) -> Tensor + + See :func:`torch.bmm` + """ + + def bool(self) -> Tensor: + r""" + bool(memory_format=torch.preserve_format) -> Tensor + + ``self.bool()`` is equivalent to ``self.to(torch.bool)``. See :func:`to`. + + Args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + + @overload + def broadcast_to(self, size: Sequence[_int | SymInt]) -> Tensor: + r""" + broadcast_to(shape) -> Tensor + + See :func:`torch.broadcast_to`. + """ + + @overload + def broadcast_to(self, *size: _int | SymInt) -> Tensor: + r""" + broadcast_to(shape) -> Tensor + + See :func:`torch.broadcast_to`. + """ + + def byte(self) -> Tensor: + r""" + byte(memory_format=torch.preserve_format) -> Tensor + + ``self.byte()`` is equivalent to ``self.to(torch.uint8)``. See :func:`to`. + + Args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + + def cauchy_( + self, + median: _float = 0, + sigma: _float = 1, + *, + generator: Generator | None = None, + ) -> Tensor: + r""" + cauchy_(median=0, sigma=1, *, generator=None) -> Tensor + + Fills the tensor with numbers drawn from the Cauchy distribution: + + .. math:: + + f(x) = \dfrac{1}{\pi} \dfrac{\sigma}{(x - \text{median})^2 + \sigma^2} + + .. note:: + Sigma (:math:`\sigma`) is used to denote the scale parameter in Cauchy distribution. + """ + + def ccol_indices(self) -> Tensor: ... + def ceil(self) -> Tensor: + r""" + ceil() -> Tensor + + See :func:`torch.ceil` + """ + + def ceil_(self) -> Tensor: + r""" + ceil_() -> Tensor + + In-place version of :meth:`~Tensor.ceil` + """ + + def chalf(self, *, memory_format: memory_format | None = None) -> Tensor: + r""" + chalf(memory_format=torch.preserve_format) -> Tensor + + ``self.chalf()`` is equivalent to ``self.to(torch.complex32)``. See :func:`to`. + + Args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + + def char(self) -> Tensor: + r""" + char(memory_format=torch.preserve_format) -> Tensor + + ``self.char()`` is equivalent to ``self.to(torch.int8)``. See :func:`to`. + + Args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + + def cholesky(self, upper: _bool = False) -> Tensor: + r""" + cholesky(upper=False) -> Tensor + + See :func:`torch.cholesky` + """ + + def cholesky_inverse(self, upper: _bool = False) -> Tensor: + r""" + cholesky_inverse(upper=False) -> Tensor + + See :func:`torch.cholesky_inverse` + """ + + def cholesky_solve(self, input2: Tensor, upper: _bool = False) -> Tensor: + r""" + cholesky_solve(input2, upper=False) -> Tensor + + See :func:`torch.cholesky_solve` + """ + + def chunk(self, chunks: _int, dim: _int = 0) -> tuple[Tensor, ...]: + r""" + chunk(chunks, dim=0) -> List of Tensors + + See :func:`torch.chunk` + """ + + @overload + def clamp( + self, + min: Tensor | None = None, + max: Tensor | None = None, + ) -> Tensor: + r""" + clamp(min=None, max=None) -> Tensor + + See :func:`torch.clamp` + """ + + @overload + def clamp( + self, + min: Number | _complex | None = None, + max: Number | _complex | None = None, + ) -> Tensor: + r""" + clamp(min=None, max=None) -> Tensor + + See :func:`torch.clamp` + """ + + @overload + def clamp_( + self, + min: Tensor | None = None, + max: Tensor | None = None, + ) -> Tensor: + r""" + clamp_(min=None, max=None) -> Tensor + + In-place version of :meth:`~Tensor.clamp` + """ + + @overload + def clamp_( + self, + min: Number | _complex | None = None, + max: Number | _complex | None = None, + ) -> Tensor: + r""" + clamp_(min=None, max=None) -> Tensor + + In-place version of :meth:`~Tensor.clamp` + """ + + @overload + def clamp_max(self, max: Tensor) -> Tensor: ... + @overload + def clamp_max(self, max: Number | _complex) -> Tensor: ... + @overload + def clamp_max_(self, max: Tensor) -> Tensor: ... + @overload + def clamp_max_(self, max: Number | _complex) -> Tensor: ... + @overload + def clamp_min(self, min: Tensor) -> Tensor: ... + @overload + def clamp_min(self, min: Number | _complex) -> Tensor: ... + @overload + def clamp_min_(self, min: Tensor) -> Tensor: ... + @overload + def clamp_min_(self, min: Number | _complex) -> Tensor: ... + @overload + def clip( + self, + min: Tensor | None = None, + max: Tensor | None = None, + ) -> Tensor: + r""" + clip(min=None, max=None) -> Tensor + + Alias for :meth:`~Tensor.clamp`. + """ + + @overload + def clip( + self, + min: Number | _complex | None = None, + max: Number | _complex | None = None, + ) -> Tensor: + r""" + clip(min=None, max=None) -> Tensor + + Alias for :meth:`~Tensor.clamp`. + """ + + @overload + def clip_( + self, + min: Tensor | None = None, + max: Tensor | None = None, + ) -> Tensor: + r""" + clip_(min=None, max=None) -> Tensor + + Alias for :meth:`~Tensor.clamp_`. + """ + + @overload + def clip_( + self, + min: Number | _complex | None = None, + max: Number | _complex | None = None, + ) -> Tensor: + r""" + clip_(min=None, max=None) -> Tensor + + Alias for :meth:`~Tensor.clamp_`. + """ + + def clone(self, *, memory_format: memory_format | None = None) -> Tensor: + r""" + clone(*, memory_format=torch.preserve_format) -> Tensor + + See :func:`torch.clone` + """ + + def coalesce(self) -> Tensor: + r""" + coalesce() -> Tensor + + Returns a coalesced copy of :attr:`self` if :attr:`self` is an + :ref:`uncoalesced tensor `. + + Returns :attr:`self` if :attr:`self` is a coalesced tensor. + + .. warning:: + Throws an error if :attr:`self` is not a sparse COO tensor. + """ + + def col_indices(self) -> Tensor: + r""" + col_indices() -> IntTensor + + Returns the tensor containing the column indices of the :attr:`self` + tensor when :attr:`self` is a sparse CSR tensor of layout ``sparse_csr``. + The ``col_indices`` tensor is strictly of shape (:attr:`self`.nnz()) + and of type ``int32`` or ``int64``. When using MKL routines such as sparse + matrix multiplication, it is necessary to use ``int32`` indexing in order + to avoid downcasting and potentially losing information. + + Example:: + + >>> csr = torch.eye(5,5).to_sparse_csr() + >>> csr.col_indices() + tensor([0, 1, 2, 3, 4], dtype=torch.int32) + """ + + def conj(self) -> Tensor: + r""" + conj() -> Tensor + + See :func:`torch.conj` + """ + + def conj_physical(self) -> Tensor: + r""" + conj_physical() -> Tensor + + See :func:`torch.conj_physical` + """ + + def conj_physical_(self) -> Tensor: + r""" + conj_physical_() -> Tensor + + In-place version of :meth:`~Tensor.conj_physical` + """ + + def contiguous( + self, + memory_format: torch.memory_format = torch.contiguous_format, + ) -> Tensor: + r""" + contiguous(memory_format=torch.contiguous_format) -> Tensor + + Returns a contiguous in memory tensor containing the same data as :attr:`self` tensor. If + :attr:`self` tensor is already in the specified memory format, this function returns the + :attr:`self` tensor. + + Args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.contiguous_format``. + """ + + def copy_(self, other: Tensor, non_blocking: _bool = False) -> Tensor: + r""" + copy_(src, non_blocking=False) -> Tensor + + Copies the elements from :attr:`src` into :attr:`self` tensor and returns + :attr:`self`. + + The :attr:`src` tensor must be :ref:`broadcastable ` + with the :attr:`self` tensor. It may be of a different data type or reside on a + different device. + + Args: + src (Tensor): the source tensor to copy from + non_blocking (bool, optional): if ``True`` and this copy is between CPU and GPU, + the copy may occur asynchronously with respect to the host. For other + cases, this argument has no effect. Default: ``False`` + """ + + @overload + def copysign(self, other: Tensor) -> Tensor: + r""" + copysign(other) -> Tensor + + See :func:`torch.copysign` + """ + + @overload + def copysign(self, other: Number | _complex) -> Tensor: + r""" + copysign(other) -> Tensor + + See :func:`torch.copysign` + """ + + @overload + def copysign_(self, other: Tensor) -> Tensor: + r""" + copysign_(other) -> Tensor + + In-place version of :meth:`~Tensor.copysign` + """ + + @overload + def copysign_(self, other: Number | _complex) -> Tensor: + r""" + copysign_(other) -> Tensor + + In-place version of :meth:`~Tensor.copysign` + """ + + def corrcoef(self) -> Tensor: + r""" + corrcoef() -> Tensor + + See :func:`torch.corrcoef` + """ + + def cos(self) -> Tensor: + r""" + cos() -> Tensor + + See :func:`torch.cos` + """ + + def cos_(self) -> Tensor: + r""" + cos_() -> Tensor + + In-place version of :meth:`~Tensor.cos` + """ + + def cosh(self) -> Tensor: + r""" + cosh() -> Tensor + + See :func:`torch.cosh` + """ + + def cosh_(self) -> Tensor: + r""" + cosh_() -> Tensor + + In-place version of :meth:`~Tensor.cosh` + """ + + @overload + def count_nonzero(self, dim: _int | None = None) -> Tensor: + r""" + count_nonzero(dim=None) -> Tensor + + See :func:`torch.count_nonzero` + """ + + @overload + def count_nonzero(self, dim: _size) -> Tensor: + r""" + count_nonzero(dim=None) -> Tensor + + See :func:`torch.count_nonzero` + """ + + @overload + def count_nonzero(self, *dim: _int) -> Tensor: + r""" + count_nonzero(dim=None) -> Tensor + + See :func:`torch.count_nonzero` + """ + + def cov( + self, + *, + correction: _int = 1, + fweights: Tensor | None = None, + aweights: Tensor | None = None, + ) -> Tensor: + r""" + cov(*, correction=1, fweights=None, aweights=None) -> Tensor + + See :func:`torch.cov` + """ + + def cpu( + self, + memory_format: torch.memory_format = torch.preserve_format, + ) -> Tensor: + r""" + cpu(memory_format=torch.preserve_format) -> Tensor + + Returns a copy of this object in CPU memory. + + If this object is already in CPU memory, + then no copy is performed and the original object is returned. + + Args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + + def cross(self, other: Tensor, dim: _int | None = None) -> Tensor: + r""" + cross(other, dim=None) -> Tensor + + See :func:`torch.cross` + """ + + def crow_indices(self) -> Tensor: + r""" + crow_indices() -> IntTensor + + Returns the tensor containing the compressed row indices of the :attr:`self` + tensor when :attr:`self` is a sparse CSR tensor of layout ``sparse_csr``. + The ``crow_indices`` tensor is strictly of shape (:attr:`self`.size(0) + 1) + and of type ``int32`` or ``int64``. When using MKL routines such as sparse + matrix multiplication, it is necessary to use ``int32`` indexing in order + to avoid downcasting and potentially losing information. + + Example:: + + >>> csr = torch.eye(5,5).to_sparse_csr() + >>> csr.crow_indices() + tensor([0, 1, 2, 3, 4, 5], dtype=torch.int32) + """ + + def cuda( + self, + device: _device | _int | str | None = None, + non_blocking: _bool = False, + memory_format: torch.memory_format = torch.preserve_format, + ) -> Tensor: + r""" + cuda(device=None, non_blocking=False, memory_format=torch.preserve_format) -> Tensor + + Returns a copy of this object in CUDA memory. + + If this object is already in CUDA memory and on the correct device, + then no copy is performed and the original object is returned. + + Args: + device (:class:`torch.device`, optional): The destination GPU device. + Defaults to the current CUDA device. + non_blocking (bool, optional): If ``True`` and the source is in pinned memory, + the copy will be asynchronous with respect to the host. + Otherwise, the argument has no effect. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + + @overload + def cummax(self, dim: _int) -> torch.return_types.cummax: + r""" + cummax(dim) -> (Tensor, Tensor) + + See :func:`torch.cummax` + """ + + @overload + def cummax( + self, + dim: str | EllipsisType | None, + ) -> torch.return_types.cummax: + r""" + cummax(dim) -> (Tensor, Tensor) + + See :func:`torch.cummax` + """ + + @overload + def cummin(self, dim: _int) -> torch.return_types.cummin: + r""" + cummin(dim) -> (Tensor, Tensor) + + See :func:`torch.cummin` + """ + + @overload + def cummin( + self, + dim: str | EllipsisType | None, + ) -> torch.return_types.cummin: + r""" + cummin(dim) -> (Tensor, Tensor) + + See :func:`torch.cummin` + """ + + @overload + def cumprod(self, dim: _int, *, dtype: _dtype | None = None) -> Tensor: + r""" + cumprod(dim, dtype=None) -> Tensor + + See :func:`torch.cumprod` + """ + + @overload + def cumprod( + self, + dim: str | EllipsisType | None, + *, + dtype: _dtype | None = None, + ) -> Tensor: + r""" + cumprod(dim, dtype=None) -> Tensor + + See :func:`torch.cumprod` + """ + + @overload + def cumprod_(self, dim: _int, *, dtype: _dtype | None = None) -> Tensor: + r""" + cumprod_(dim, dtype=None) -> Tensor + + In-place version of :meth:`~Tensor.cumprod` + """ + + @overload + def cumprod_( + self, + dim: str | EllipsisType | None, + *, + dtype: _dtype | None = None, + ) -> Tensor: + r""" + cumprod_(dim, dtype=None) -> Tensor + + In-place version of :meth:`~Tensor.cumprod` + """ + + @overload + def cumsum(self, dim: _int, *, dtype: _dtype | None = None) -> Tensor: + r""" + cumsum(dim, dtype=None) -> Tensor + + See :func:`torch.cumsum` + """ + + @overload + def cumsum( + self, + dim: str | EllipsisType | None, + *, + dtype: _dtype | None = None, + ) -> Tensor: + r""" + cumsum(dim, dtype=None) -> Tensor + + See :func:`torch.cumsum` + """ + + @overload + def cumsum_(self, dim: _int, *, dtype: _dtype | None = None) -> Tensor: + r""" + cumsum_(dim, dtype=None) -> Tensor + + In-place version of :meth:`~Tensor.cumsum` + """ + + @overload + def cumsum_( + self, + dim: str | EllipsisType | None, + *, + dtype: _dtype | None = None, + ) -> Tensor: + r""" + cumsum_(dim, dtype=None) -> Tensor + + In-place version of :meth:`~Tensor.cumsum` + """ + + def data_ptr(self) -> _int: + r""" + data_ptr() -> int + + Returns the address of the first element of :attr:`self` tensor. + """ + + def deg2rad(self) -> Tensor: + r""" + deg2rad() -> Tensor + + See :func:`torch.deg2rad` + """ + + def deg2rad_(self) -> Tensor: + r""" + deg2rad_() -> Tensor + + In-place version of :meth:`~Tensor.deg2rad` + """ + + def dense_dim(self) -> _int: + r""" + dense_dim() -> int + + Return the number of dense dimensions in a :ref:`sparse tensor ` :attr:`self`. + + .. note:: + Returns ``len(self.shape)`` if :attr:`self` is not a sparse tensor. + + See also :meth:`Tensor.sparse_dim` and :ref:`hybrid tensors `. + """ + + def dequantize(self) -> Tensor: + r""" + dequantize() -> Tensor + + Given a quantized Tensor, dequantize it and return the dequantized float Tensor. + """ + + def det(self) -> Tensor: + r""" + det() -> Tensor + + See :func:`torch.det` + """ + + def detach(self) -> Tensor: ... + def detach_(self) -> Tensor: ... + def diag(self, diagonal: _int = 0) -> Tensor: + r""" + diag(diagonal=0) -> Tensor + + See :func:`torch.diag` + """ + + def diag_embed( + self, + offset: _int = 0, + dim1: _int = -2, + dim2: _int = -1, + ) -> Tensor: + r""" + diag_embed(offset=0, dim1=-2, dim2=-1) -> Tensor + + See :func:`torch.diag_embed` + """ + + def diagflat(self, offset: _int = 0) -> Tensor: + r""" + diagflat(offset=0) -> Tensor + + See :func:`torch.diagflat` + """ + + @overload + def diagonal( + self, + *, + outdim: str | EllipsisType | None, + dim1: str | EllipsisType | None, + dim2: str | EllipsisType | None, + offset: _int = 0, + ) -> Tensor: + r""" + diagonal(offset=0, dim1=0, dim2=1) -> Tensor + + See :func:`torch.diagonal` + """ + + @overload + def diagonal( + self, + offset: _int = 0, + dim1: _int = 0, + dim2: _int = 1, + ) -> Tensor: + r""" + diagonal(offset=0, dim1=0, dim2=1) -> Tensor + + See :func:`torch.diagonal` + """ + + def diagonal_scatter( + self, + src: Tensor, + offset: _int = 0, + dim1: _int = 0, + dim2: _int = 1, + ) -> Tensor: + r""" + diagonal_scatter(src, offset=0, dim1=0, dim2=1) -> Tensor + + See :func:`torch.diagonal_scatter` + """ + + def diff( + self, + n: _int = 1, + dim: _int = -1, + prepend: Tensor | None = None, + append: Tensor | None = None, + ) -> Tensor: + r""" + diff(n=1, dim=-1, prepend=None, append=None) -> Tensor + + See :func:`torch.diff` + """ + + def digamma(self) -> Tensor: + r""" + digamma() -> Tensor + + See :func:`torch.digamma` + """ + + def digamma_(self) -> Tensor: + r""" + digamma_() -> Tensor + + In-place version of :meth:`~Tensor.digamma` + """ + + def dim(self) -> _int: + r""" + dim() -> int + + Returns the number of dimensions of :attr:`self` tensor. + """ + + def dist(self, other: Tensor, p: Number | _complex = 2) -> Tensor: + r""" + dist(other, p=2) -> Tensor + + See :func:`torch.dist` + """ + + def div( + self, + other: Tensor | Number, + *, + rounding_mode: str | None = None, + ) -> Tensor: + r""" + div(value, *, rounding_mode=None) -> Tensor + + See :func:`torch.div` + """ + + def div_( + self, + other: Tensor | Number, + *, + rounding_mode: str | None = None, + ) -> Tensor: + r""" + div_(value, *, rounding_mode=None) -> Tensor + + In-place version of :meth:`~Tensor.div` + """ + + @overload + def divide(self, other: Tensor) -> Tensor: + r""" + divide(value, *, rounding_mode=None) -> Tensor + + See :func:`torch.divide` + """ + + @overload + def divide(self, other: Tensor, *, rounding_mode: str | None) -> Tensor: + r""" + divide(value, *, rounding_mode=None) -> Tensor + + See :func:`torch.divide` + """ + + @overload + def divide( + self, + other: Number | _complex, + *, + rounding_mode: str | None, + ) -> Tensor: + r""" + divide(value, *, rounding_mode=None) -> Tensor + + See :func:`torch.divide` + """ + + @overload + def divide(self, other: Number | _complex) -> Tensor: + r""" + divide(value, *, rounding_mode=None) -> Tensor + + See :func:`torch.divide` + """ + + @overload + def divide_(self, other: Tensor) -> Tensor: + r""" + divide_(value, *, rounding_mode=None) -> Tensor + + In-place version of :meth:`~Tensor.divide` + """ + + @overload + def divide_(self, other: Tensor, *, rounding_mode: str | None) -> Tensor: + r""" + divide_(value, *, rounding_mode=None) -> Tensor + + In-place version of :meth:`~Tensor.divide` + """ + + @overload + def divide_( + self, + other: Number | _complex, + *, + rounding_mode: str | None, + ) -> Tensor: + r""" + divide_(value, *, rounding_mode=None) -> Tensor + + In-place version of :meth:`~Tensor.divide` + """ + + @overload + def divide_(self, other: Number | _complex) -> Tensor: + r""" + divide_(value, *, rounding_mode=None) -> Tensor + + In-place version of :meth:`~Tensor.divide` + """ + + def dot(self, tensor: Tensor) -> Tensor: + r""" + dot(other) -> Tensor + + See :func:`torch.dot` + """ + + def double(self) -> Tensor: + r""" + double(memory_format=torch.preserve_format) -> Tensor + + ``self.double()`` is equivalent to ``self.to(torch.float64)``. See :func:`to`. + + Args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + + @overload + def dsplit(self, sections: _int) -> tuple[Tensor, ...]: + r""" + dsplit(split_size_or_sections) -> List of Tensors + + See :func:`torch.dsplit` + """ + + @overload + def dsplit(self, indices: _size) -> tuple[Tensor, ...]: + r""" + dsplit(split_size_or_sections) -> List of Tensors + + See :func:`torch.dsplit` + """ + + @overload + def dsplit(self, *indices: _int) -> tuple[Tensor, ...]: + r""" + dsplit(split_size_or_sections) -> List of Tensors + + See :func:`torch.dsplit` + """ + + def element_size(self) -> _int: + r""" + element_size() -> int + + Returns the size in bytes of an individual element. + + Example:: + + >>> torch.tensor([]).element_size() + 4 + >>> torch.tensor([], dtype=torch.uint8).element_size() + 1 + """ + + @overload + def eq(self, other: Tensor) -> Tensor: + r""" + eq(other) -> Tensor + + See :func:`torch.eq` + """ + + @overload + def eq(self, other: Number | _complex) -> Tensor: + r""" + eq(other) -> Tensor + + See :func:`torch.eq` + """ + + @overload + def eq_(self, other: Tensor) -> Tensor: + r""" + eq_(other) -> Tensor + + In-place version of :meth:`~Tensor.eq` + """ + + @overload + def eq_(self, other: Number | _complex) -> Tensor: + r""" + eq_(other) -> Tensor + + In-place version of :meth:`~Tensor.eq` + """ + + def equal(self, other: Tensor) -> _bool: + r""" + equal(other) -> bool + + See :func:`torch.equal` + """ + + def erf(self) -> Tensor: + r""" + erf() -> Tensor + + See :func:`torch.erf` + """ + + def erf_(self) -> Tensor: + r""" + erf_() -> Tensor + + In-place version of :meth:`~Tensor.erf` + """ + + def erfc(self) -> Tensor: + r""" + erfc() -> Tensor + + See :func:`torch.erfc` + """ + + def erfc_(self) -> Tensor: + r""" + erfc_() -> Tensor + + In-place version of :meth:`~Tensor.erfc` + """ + + def erfinv(self) -> Tensor: + r""" + erfinv() -> Tensor + + See :func:`torch.erfinv` + """ + + def erfinv_(self) -> Tensor: + r""" + erfinv_() -> Tensor + + In-place version of :meth:`~Tensor.erfinv` + """ + + def exp(self) -> Tensor: + r""" + exp() -> Tensor + + See :func:`torch.exp` + """ + + def exp2(self) -> Tensor: + r""" + exp2() -> Tensor + + See :func:`torch.exp2` + """ + + def exp2_(self) -> Tensor: + r""" + exp2_() -> Tensor + + In-place version of :meth:`~Tensor.exp2` + """ + + def exp_(self) -> Tensor: + r""" + exp_() -> Tensor + + In-place version of :meth:`~Tensor.exp` + """ + + @overload + def expand( + self, + size: Sequence[_int | SymInt], + *, + implicit: _bool = False, + ) -> Tensor: + r""" + expand(*sizes) -> Tensor + + Returns a new view of the :attr:`self` tensor with singleton dimensions expanded + to a larger size. + + Passing -1 as the size for a dimension means not changing the size of + that dimension. + + Tensor can be also expanded to a larger number of dimensions, and the + new ones will be appended at the front. For the new dimensions, the + size cannot be set to -1. + + Expanding a tensor does not allocate new memory, but only creates a + new view on the existing tensor where a dimension of size one is + expanded to a larger size by setting the ``stride`` to 0. Any dimension + of size 1 can be expanded to an arbitrary value without allocating new + memory. + + Args: + *sizes (torch.Size or int...): the desired expanded size + + .. warning:: + + More than one element of an expanded tensor may refer to a single + memory location. As a result, in-place operations (especially ones that + are vectorized) may result in incorrect behavior. If you need to write + to the tensors, please clone them first. + + Example:: + + >>> x = torch.tensor([[1], [2], [3]]) + >>> x.size() + torch.Size([3, 1]) + >>> x.expand(3, 4) + tensor([[ 1, 1, 1, 1], + [ 2, 2, 2, 2], + [ 3, 3, 3, 3]]) + >>> x.expand(-1, 4) # -1 means not changing the size of that dimension + tensor([[ 1, 1, 1, 1], + [ 2, 2, 2, 2], + [ 3, 3, 3, 3]]) + """ + + @overload + def expand(self, *size: _int | SymInt, implicit: _bool = False) -> Tensor: + r""" + expand(*sizes) -> Tensor + + Returns a new view of the :attr:`self` tensor with singleton dimensions expanded + to a larger size. + + Passing -1 as the size for a dimension means not changing the size of + that dimension. + + Tensor can be also expanded to a larger number of dimensions, and the + new ones will be appended at the front. For the new dimensions, the + size cannot be set to -1. + + Expanding a tensor does not allocate new memory, but only creates a + new view on the existing tensor where a dimension of size one is + expanded to a larger size by setting the ``stride`` to 0. Any dimension + of size 1 can be expanded to an arbitrary value without allocating new + memory. + + Args: + *sizes (torch.Size or int...): the desired expanded size + + .. warning:: + + More than one element of an expanded tensor may refer to a single + memory location. As a result, in-place operations (especially ones that + are vectorized) may result in incorrect behavior. If you need to write + to the tensors, please clone them first. + + Example:: + + >>> x = torch.tensor([[1], [2], [3]]) + >>> x.size() + torch.Size([3, 1]) + >>> x.expand(3, 4) + tensor([[ 1, 1, 1, 1], + [ 2, 2, 2, 2], + [ 3, 3, 3, 3]]) + >>> x.expand(-1, 4) # -1 means not changing the size of that dimension + tensor([[ 1, 1, 1, 1], + [ 2, 2, 2, 2], + [ 3, 3, 3, 3]]) + """ + + def expand_as(self, other: Tensor) -> Tensor: + r""" + expand_as(other) -> Tensor + + Expand this tensor to the same size as :attr:`other`. + ``self.expand_as(other)`` is equivalent to ``self.expand(other.size())``. + + Please see :meth:`~Tensor.expand` for more information about ``expand``. + + Args: + other (:class:`torch.Tensor`): The result tensor has the same size + as :attr:`other`. + """ + + def expm1(self) -> Tensor: + r""" + expm1() -> Tensor + + See :func:`torch.expm1` + """ + + def expm1_(self) -> Tensor: + r""" + expm1_() -> Tensor + + In-place version of :meth:`~Tensor.expm1` + """ + + def exponential_( + self, + lambd: _float = 1, + *, + generator: Generator | None = None, + ) -> Tensor: + r""" + exponential_(lambd=1, *, generator=None) -> Tensor + + Fills :attr:`self` tensor with elements drawn from the PDF (probability density function): + + .. math:: + + f(x) = \lambda e^{-\lambda x}, x > 0 + + .. note:: + In probability theory, exponential distribution is supported on interval [0, :math:`\inf`) (i.e., :math:`x >= 0`) + implying that zero can be sampled from the exponential distribution. + However, :func:`torch.Tensor.exponential_` does not sample zero, + which means that its actual support is the interval (0, :math:`\inf`). + + Note that :func:`torch.distributions.exponential.Exponential` is supported on the interval [0, :math:`\inf`) and can sample zero. + """ + + @overload + def fill_(self, value: Tensor) -> Tensor: + r""" + fill_(value) -> Tensor + + Fills :attr:`self` tensor with the specified value. + """ + + @overload + def fill_(self, value: Number | _complex) -> Tensor: + r""" + fill_(value) -> Tensor + + Fills :attr:`self` tensor with the specified value. + """ + + def fill_diagonal_( + self, + fill_value: Number | _complex, + wrap: _bool = False, + ) -> Tensor: + r""" + fill_diagonal_(fill_value, wrap=False) -> Tensor + + Fill the main diagonal of a tensor that has at least 2-dimensions. + When dims>2, all dimensions of input must be of equal length. + This function modifies the input tensor in-place, and returns the input tensor. + + Arguments: + fill_value (Scalar): the fill value + wrap (bool, optional): the diagonal 'wrapped' after N columns for tall matrices. Default: ``False`` + + Example:: + + >>> a = torch.zeros(3, 3) + >>> a.fill_diagonal_(5) + tensor([[5., 0., 0.], + [0., 5., 0.], + [0., 0., 5.]]) + >>> b = torch.zeros(7, 3) + >>> b.fill_diagonal_(5) + tensor([[5., 0., 0.], + [0., 5., 0.], + [0., 0., 5.], + [0., 0., 0.], + [0., 0., 0.], + [0., 0., 0.], + [0., 0., 0.]]) + >>> c = torch.zeros(7, 3) + >>> c.fill_diagonal_(5, wrap=True) + tensor([[5., 0., 0.], + [0., 5., 0.], + [0., 0., 5.], + [0., 0., 0.], + [5., 0., 0.], + [0., 5., 0.], + [0., 0., 5.]]) + """ + + def fix(self) -> Tensor: + r""" + fix() -> Tensor + + See :func:`torch.fix`. + """ + + def fix_(self) -> Tensor: + r""" + fix_() -> Tensor + + In-place version of :meth:`~Tensor.fix` + """ + + @overload + def flatten( + self, + start_dim: _int, + end_dim: _int, + out_dim: str | EllipsisType | None, + ) -> Tensor: + r""" + flatten(start_dim=0, end_dim=-1) -> Tensor + + See :func:`torch.flatten` + """ + + @overload + def flatten(self, start_dim: _int = 0, end_dim: _int = -1) -> Tensor: + r""" + flatten(start_dim=0, end_dim=-1) -> Tensor + + See :func:`torch.flatten` + """ + + @overload + def flatten( + self, + start_dim: str | EllipsisType | None, + end_dim: str | EllipsisType | None, + out_dim: str | EllipsisType | None, + ) -> Tensor: + r""" + flatten(start_dim=0, end_dim=-1) -> Tensor + + See :func:`torch.flatten` + """ + + @overload + def flatten( + self, + dims: Sequence[str | EllipsisType | None], + out_dim: str | EllipsisType | None, + ) -> Tensor: + r""" + flatten(start_dim=0, end_dim=-1) -> Tensor + + See :func:`torch.flatten` + """ + + @overload + def flip(self, dims: _size) -> Tensor: + r""" + flip(dims) -> Tensor + + See :func:`torch.flip` + """ + + @overload + def flip(self, *dims: _int) -> Tensor: + r""" + flip(dims) -> Tensor + + See :func:`torch.flip` + """ + + def fliplr(self) -> Tensor: + r""" + fliplr() -> Tensor + + See :func:`torch.fliplr` + """ + + def flipud(self) -> Tensor: + r""" + flipud() -> Tensor + + See :func:`torch.flipud` + """ + + def float(self) -> Tensor: + r""" + float(memory_format=torch.preserve_format) -> Tensor + + ``self.float()`` is equivalent to ``self.to(torch.float32)``. See :func:`to`. + + Args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + + @overload + def float_power(self, exponent: Tensor) -> Tensor: + r""" + float_power(exponent) -> Tensor + + See :func:`torch.float_power` + """ + + @overload + def float_power(self, exponent: Number | _complex) -> Tensor: + r""" + float_power(exponent) -> Tensor + + See :func:`torch.float_power` + """ + + @overload + def float_power_(self, exponent: Tensor) -> Tensor: + r""" + float_power_(exponent) -> Tensor + + In-place version of :meth:`~Tensor.float_power` + """ + + @overload + def float_power_(self, exponent: Number | _complex) -> Tensor: + r""" + float_power_(exponent) -> Tensor + + In-place version of :meth:`~Tensor.float_power` + """ + + def floor(self) -> Tensor: + r""" + floor() -> Tensor + + See :func:`torch.floor` + """ + + def floor_(self) -> Tensor: + r""" + floor_() -> Tensor + + In-place version of :meth:`~Tensor.floor` + """ + + def floor_divide( + self, + other: Tensor | Number | torch.SymInt | torch.SymFloat, + *, + out: Tensor | None = None, + ) -> Tensor: + r""" + floor_divide(value) -> Tensor + + See :func:`torch.floor_divide` + """ + + def floor_divide_( + self, + other: Tensor | Number | torch.SymInt | torch.SymFloat, + ) -> Tensor: + r""" + floor_divide_(value) -> Tensor + + In-place version of :meth:`~Tensor.floor_divide` + """ + + def fmax(self, other: Tensor) -> Tensor: + r""" + fmax(other) -> Tensor + + See :func:`torch.fmax` + """ + + def fmin(self, other: Tensor) -> Tensor: + r""" + fmin(other) -> Tensor + + See :func:`torch.fmin` + """ + + @overload + def fmod(self, other: Tensor) -> Tensor: + r""" + fmod(divisor) -> Tensor + + See :func:`torch.fmod` + """ + + @overload + def fmod(self, other: Number | _complex) -> Tensor: + r""" + fmod(divisor) -> Tensor + + See :func:`torch.fmod` + """ + + @overload + def fmod_(self, other: Tensor) -> Tensor: + r""" + fmod_(divisor) -> Tensor + + In-place version of :meth:`~Tensor.fmod` + """ + + @overload + def fmod_(self, other: Number | _complex) -> Tensor: + r""" + fmod_(divisor) -> Tensor + + In-place version of :meth:`~Tensor.fmod` + """ + + def frac(self) -> Tensor: + r""" + frac() -> Tensor + + See :func:`torch.frac` + """ + + def frac_(self) -> Tensor: + r""" + frac_() -> Tensor + + In-place version of :meth:`~Tensor.frac` + """ + + def frexp(self) -> torch.return_types.frexp: + r""" + frexp(input) -> (Tensor mantissa, Tensor exponent) + + See :func:`torch.frexp` + """ + + @overload + def gather( + self, + dim: _int, + index: Tensor, + *, + sparse_grad: _bool = False, + ) -> Tensor: + r""" + gather(dim, index) -> Tensor + + See :func:`torch.gather` + """ + + @overload + def gather( + self, + dim: str | EllipsisType | None, + index: Tensor, + *, + sparse_grad: _bool = False, + ) -> Tensor: + r""" + gather(dim, index) -> Tensor + + See :func:`torch.gather` + """ + + def gcd(self, other: Tensor) -> Tensor: + r""" + gcd(other) -> Tensor + + See :func:`torch.gcd` + """ + + def gcd_(self, other: Tensor) -> Tensor: + r""" + gcd_(other) -> Tensor + + In-place version of :meth:`~Tensor.gcd` + """ + + @overload + def ge(self, other: Tensor) -> Tensor: + r""" + ge(other) -> Tensor + + See :func:`torch.ge`. + """ + + @overload + def ge(self, other: Number | _complex) -> Tensor: + r""" + ge(other) -> Tensor + + See :func:`torch.ge`. + """ + + @overload + def ge_(self, other: Tensor) -> Tensor: + r""" + ge_(other) -> Tensor + + In-place version of :meth:`~Tensor.ge`. + """ + + @overload + def ge_(self, other: Number | _complex) -> Tensor: + r""" + ge_(other) -> Tensor + + In-place version of :meth:`~Tensor.ge`. + """ + + def geometric_( + self, + p: _float, + *, + generator: Generator | None = None, + ) -> Tensor: + r""" + geometric_(p, *, generator=None) -> Tensor + + Fills :attr:`self` tensor with elements drawn from the geometric distribution: + + .. math:: + + P(X=k) = (1 - p)^{k - 1} p, k = 1, 2, ... + + .. note:: + :func:`torch.Tensor.geometric_` `k`-th trial is the first success hence draws samples in :math:`\{1, 2, \ldots\}`, whereas + :func:`torch.distributions.geometric.Geometric` :math:`(k+1)`-th trial is the first success + hence draws samples in :math:`\{0, 1, \ldots\}`. + """ + + def geqrf(self) -> torch.return_types.geqrf: + r""" + geqrf() -> (Tensor, Tensor) + + See :func:`torch.geqrf` + """ + + def ger(self, vec2: Tensor) -> Tensor: + r""" + ger(vec2) -> Tensor + + See :func:`torch.ger` + """ + + def get_device(self) -> _int: + r""" + get_device() -> Device ordinal (Integer) + + For CUDA tensors, this function returns the device ordinal of the GPU on which the tensor resides. + For CPU tensors, this function returns `-1`. + + Example:: + + >>> x = torch.randn(3, 4, 5, device='cuda:0') + >>> x.get_device() + 0 + >>> x.cpu().get_device() + -1 + """ + + @overload + def greater(self, other: Tensor) -> Tensor: + r""" + greater(other) -> Tensor + + See :func:`torch.greater`. + """ + + @overload + def greater(self, other: Number | _complex) -> Tensor: + r""" + greater(other) -> Tensor + + See :func:`torch.greater`. + """ + + @overload + def greater_(self, other: Tensor) -> Tensor: + r""" + greater_(other) -> Tensor + + In-place version of :meth:`~Tensor.greater`. + """ + + @overload + def greater_(self, other: Number | _complex) -> Tensor: + r""" + greater_(other) -> Tensor + + In-place version of :meth:`~Tensor.greater`. + """ + + @overload + def greater_equal(self, other: Tensor) -> Tensor: + r""" + greater_equal(other) -> Tensor + + See :func:`torch.greater_equal`. + """ + + @overload + def greater_equal(self, other: Number | _complex) -> Tensor: + r""" + greater_equal(other) -> Tensor + + See :func:`torch.greater_equal`. + """ + + @overload + def greater_equal_(self, other: Tensor) -> Tensor: + r""" + greater_equal_(other) -> Tensor + + In-place version of :meth:`~Tensor.greater_equal`. + """ + + @overload + def greater_equal_(self, other: Number | _complex) -> Tensor: + r""" + greater_equal_(other) -> Tensor + + In-place version of :meth:`~Tensor.greater_equal`. + """ + + @overload + def gt(self, other: Tensor) -> Tensor: + r""" + gt(other) -> Tensor + + See :func:`torch.gt`. + """ + + @overload + def gt(self, other: Number | _complex) -> Tensor: + r""" + gt(other) -> Tensor + + See :func:`torch.gt`. + """ + + @overload + def gt_(self, other: Tensor) -> Tensor: + r""" + gt_(other) -> Tensor + + In-place version of :meth:`~Tensor.gt`. + """ + + @overload + def gt_(self, other: Number | _complex) -> Tensor: + r""" + gt_(other) -> Tensor + + In-place version of :meth:`~Tensor.gt`. + """ + + def half(self) -> Tensor: + r""" + half(memory_format=torch.preserve_format) -> Tensor + + ``self.half()`` is equivalent to ``self.to(torch.float16)``. See :func:`to`. + + Args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + + def hardshrink(self, lambd: Number | _complex = 0.5) -> Tensor: + r""" + hardshrink(lambd=0.5) -> Tensor + + See :func:`torch.nn.functional.hardshrink` + """ + + def has_names(self) -> _bool: + r""" + Is ``True`` if any of this tensor's dimensions are named. Otherwise, is ``False``. + """ + + @overload + def hash_tensor( + self, + dim: _int | _size = (), + *, + keepdim: _bool = False, + mode: _int = 0, + ) -> Tensor: ... + @overload + def hash_tensor( + self, + *dim: _int, + keepdim: _bool = False, + mode: _int = 0, + ) -> Tensor: ... + def heaviside(self, values: Tensor) -> Tensor: + r""" + heaviside(values) -> Tensor + + See :func:`torch.heaviside` + """ + + def heaviside_(self, values: Tensor) -> Tensor: + r""" + heaviside_(values) -> Tensor + + In-place version of :meth:`~Tensor.heaviside` + """ + + def histc( + self, + bins: _int = 100, + min: Number | _complex = 0, + max: Number | _complex = 0, + ) -> Tensor: + r""" + histc(bins=100, min=0, max=0) -> Tensor + + See :func:`torch.histc` + """ + + @overload + def histogram( + self, + bins: Tensor, + *, + weight: Tensor | None = None, + density: _bool = False, + ) -> torch.return_types.histogram: + r""" + histogram(input, bins, *, range=None, weight=None, density=False) -> (Tensor, Tensor) + + See :func:`torch.histogram` + """ + + @overload + def histogram( + self, + bins: _int = 100, + *, + range: Sequence[_float] | None = None, + weight: Tensor | None = None, + density: _bool = False, + ) -> torch.return_types.histogram: + r""" + histogram(input, bins, *, range=None, weight=None, density=False) -> (Tensor, Tensor) + + See :func:`torch.histogram` + """ + + @overload + def hsplit(self, sections: _int) -> tuple[Tensor, ...]: + r""" + hsplit(split_size_or_sections) -> List of Tensors + + See :func:`torch.hsplit` + """ + + @overload + def hsplit(self, indices: _size) -> tuple[Tensor, ...]: + r""" + hsplit(split_size_or_sections) -> List of Tensors + + See :func:`torch.hsplit` + """ + + @overload + def hsplit(self, *indices: _int) -> tuple[Tensor, ...]: + r""" + hsplit(split_size_or_sections) -> List of Tensors + + See :func:`torch.hsplit` + """ + + def hypot(self, other: Tensor) -> Tensor: + r""" + hypot(other) -> Tensor + + See :func:`torch.hypot` + """ + + def hypot_(self, other: Tensor) -> Tensor: + r""" + hypot_(other) -> Tensor + + In-place version of :meth:`~Tensor.hypot` + """ + + def i0(self) -> Tensor: + r""" + i0() -> Tensor + + See :func:`torch.i0` + """ + + def i0_(self) -> Tensor: + r""" + i0_() -> Tensor + + In-place version of :meth:`~Tensor.i0` + """ + + def igamma(self, other: Tensor) -> Tensor: + r""" + igamma(other) -> Tensor + + See :func:`torch.igamma` + """ + + def igamma_(self, other: Tensor) -> Tensor: + r""" + igamma_(other) -> Tensor + + In-place version of :meth:`~Tensor.igamma` + """ + + def igammac(self, other: Tensor) -> Tensor: + r""" + igammac(other) -> Tensor + See :func:`torch.igammac` + """ + + def igammac_(self, other: Tensor) -> Tensor: + r""" + igammac_(other) -> Tensor + In-place version of :meth:`~Tensor.igammac` + """ + + @overload + def index_add( + self, + dim: _int, + index: Tensor, + source: Tensor, + *, + alpha: Number | _complex = 1, + ) -> Tensor: + r""" + index_add(dim, index, source, *, alpha=1) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.index_add_`. + """ + + @overload + def index_add( + self, + dim: str | EllipsisType | None, + index: Tensor, + source: Tensor, + *, + alpha: Number | _complex = 1, + ) -> Tensor: + r""" + index_add(dim, index, source, *, alpha=1) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.index_add_`. + """ + + def index_add_( + self, + dim: _int, + index: Tensor, + source: Tensor, + *, + alpha: Number | _complex = 1, + ) -> Tensor: + r""" + index_add_(dim, index, source, *, alpha=1) -> Tensor + + Accumulate the elements of :attr:`alpha` times ``source`` into the :attr:`self` + tensor by adding to the indices in the order given in :attr:`index`. For example, + if ``dim == 0``, ``index[i] == j``, and ``alpha=-1``, then the ``i``\ th row of + ``source`` is subtracted from the ``j``\ th row of :attr:`self`. + + The :attr:`dim`\ th dimension of ``source`` must have the same size as the + length of :attr:`index` (which must be a vector), and all other dimensions must + match :attr:`self`, or an error will be raised. + + For a 3-D tensor the output is given as:: + + self[index[i], :, :] += alpha * src[i, :, :] # if dim == 0 + self[:, index[i], :] += alpha * src[:, i, :] # if dim == 1 + self[:, :, index[i]] += alpha * src[:, :, i] # if dim == 2 + + Note: + This operation may behave nondeterministically when given tensors on a CUDA device. See :doc:`/notes/randomness` for more information. + + Args: + dim (int): dimension along which to index + index (Tensor): indices of ``source`` to select from, + should have dtype either `torch.int64` or `torch.int32` + source (Tensor): the tensor containing values to add + + Keyword args: + alpha (Number): the scalar multiplier for ``source`` + + Example:: + + >>> x = torch.ones(5, 3) + >>> t = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=torch.float) + >>> index = torch.tensor([0, 4, 2]) + >>> x.index_add_(0, index, t) + tensor([[ 2., 3., 4.], + [ 1., 1., 1.], + [ 8., 9., 10.], + [ 1., 1., 1.], + [ 5., 6., 7.]]) + >>> x.index_add_(0, index, t, alpha=-1) + tensor([[ 1., 1., 1.], + [ 1., 1., 1.], + [ 1., 1., 1.], + [ 1., 1., 1.], + [ 1., 1., 1.]]) + """ + + @overload + def index_copy(self, dim: _int, index: Tensor, source: Tensor) -> Tensor: + r""" + index_copy(dim, index, tensor2) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.index_copy_`. + """ + + @overload + def index_copy( + self, + dim: str | EllipsisType | None, + index: Tensor, + source: Tensor, + ) -> Tensor: + r""" + index_copy(dim, index, tensor2) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.index_copy_`. + """ + + @overload + def index_copy_(self, dim: _int, index: Tensor, source: Tensor) -> Tensor: + r""" + index_copy_(dim, index, tensor) -> Tensor + + Copies the elements of :attr:`tensor` into the :attr:`self` tensor by selecting + the indices in the order given in :attr:`index`. For example, if ``dim == 0`` + and ``index[i] == j``, then the ``i``\ th row of :attr:`tensor` is copied to the + ``j``\ th row of :attr:`self`. + + The :attr:`dim`\ th dimension of :attr:`tensor` must have the same size as the + length of :attr:`index` (which must be a vector), and all other dimensions must + match :attr:`self`, or an error will be raised. + + .. note:: + If :attr:`index` contains duplicate entries, multiple elements from + :attr:`tensor` will be copied to the same index of :attr:`self`. The result + is nondeterministic since it depends on which copy occurs last. + + Args: + dim (int): dimension along which to index + index (LongTensor): indices of :attr:`tensor` to select from + tensor (Tensor): the tensor containing values to copy + + Example:: + + >>> x = torch.zeros(5, 3) + >>> t = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=torch.float) + >>> index = torch.tensor([0, 4, 2]) + >>> x.index_copy_(0, index, t) + tensor([[ 1., 2., 3.], + [ 0., 0., 0.], + [ 7., 8., 9.], + [ 0., 0., 0.], + [ 4., 5., 6.]]) + """ + + @overload + def index_copy_( + self, + dim: str | EllipsisType | None, + index: Tensor, + source: Tensor, + ) -> Tensor: + r""" + index_copy_(dim, index, tensor) -> Tensor + + Copies the elements of :attr:`tensor` into the :attr:`self` tensor by selecting + the indices in the order given in :attr:`index`. For example, if ``dim == 0`` + and ``index[i] == j``, then the ``i``\ th row of :attr:`tensor` is copied to the + ``j``\ th row of :attr:`self`. + + The :attr:`dim`\ th dimension of :attr:`tensor` must have the same size as the + length of :attr:`index` (which must be a vector), and all other dimensions must + match :attr:`self`, or an error will be raised. + + .. note:: + If :attr:`index` contains duplicate entries, multiple elements from + :attr:`tensor` will be copied to the same index of :attr:`self`. The result + is nondeterministic since it depends on which copy occurs last. + + Args: + dim (int): dimension along which to index + index (LongTensor): indices of :attr:`tensor` to select from + tensor (Tensor): the tensor containing values to copy + + Example:: + + >>> x = torch.zeros(5, 3) + >>> t = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=torch.float) + >>> index = torch.tensor([0, 4, 2]) + >>> x.index_copy_(0, index, t) + tensor([[ 1., 2., 3.], + [ 0., 0., 0.], + [ 7., 8., 9.], + [ 0., 0., 0.], + [ 4., 5., 6.]]) + """ + + @overload + def index_fill(self, dim: _int, index: Tensor, value: Tensor) -> Tensor: + r""" + index_fill(dim, index, value) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.index_fill_`. + """ + + @overload + def index_fill( + self, + dim: str | EllipsisType | None, + index: Tensor, + value: Tensor, + ) -> Tensor: + r""" + index_fill(dim, index, value) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.index_fill_`. + """ + + @overload + def index_fill( + self, + dim: _int, + index: Tensor, + value: Number | _complex, + ) -> Tensor: + r""" + index_fill(dim, index, value) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.index_fill_`. + """ + + @overload + def index_fill( + self, + dim: str | EllipsisType | None, + index: Tensor, + value: Number | _complex, + ) -> Tensor: + r""" + index_fill(dim, index, value) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.index_fill_`. + """ + + @overload + def index_fill_(self, dim: _int, index: Tensor, value: Tensor) -> Tensor: + r""" + index_fill_(dim, index, value) -> Tensor + + Fills the elements of the :attr:`self` tensor with value :attr:`value` by + selecting the indices in the order given in :attr:`index`. + + Args: + dim (int): dimension along which to index + index (LongTensor): indices of :attr:`self` tensor to fill in + value (float): the value to fill with + + Example:: + + >>> x = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=torch.float) + >>> index = torch.tensor([0, 2]) + >>> x.index_fill_(1, index, -1) + tensor([[-1., 2., -1.], + [-1., 5., -1.], + [-1., 8., -1.]]) + """ + + @overload + def index_fill_( + self, + dim: str | EllipsisType | None, + index: Tensor, + value: Tensor, + ) -> Tensor: + r""" + index_fill_(dim, index, value) -> Tensor + + Fills the elements of the :attr:`self` tensor with value :attr:`value` by + selecting the indices in the order given in :attr:`index`. + + Args: + dim (int): dimension along which to index + index (LongTensor): indices of :attr:`self` tensor to fill in + value (float): the value to fill with + + Example:: + + >>> x = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=torch.float) + >>> index = torch.tensor([0, 2]) + >>> x.index_fill_(1, index, -1) + tensor([[-1., 2., -1.], + [-1., 5., -1.], + [-1., 8., -1.]]) + """ + + @overload + def index_fill_( + self, + dim: _int, + index: Tensor, + value: Number | _complex, + ) -> Tensor: + r""" + index_fill_(dim, index, value) -> Tensor + + Fills the elements of the :attr:`self` tensor with value :attr:`value` by + selecting the indices in the order given in :attr:`index`. + + Args: + dim (int): dimension along which to index + index (LongTensor): indices of :attr:`self` tensor to fill in + value (float): the value to fill with + + Example:: + + >>> x = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=torch.float) + >>> index = torch.tensor([0, 2]) + >>> x.index_fill_(1, index, -1) + tensor([[-1., 2., -1.], + [-1., 5., -1.], + [-1., 8., -1.]]) + """ + + @overload + def index_fill_( + self, + dim: str | EllipsisType | None, + index: Tensor, + value: Number | _complex, + ) -> Tensor: + r""" + index_fill_(dim, index, value) -> Tensor + + Fills the elements of the :attr:`self` tensor with value :attr:`value` by + selecting the indices in the order given in :attr:`index`. + + Args: + dim (int): dimension along which to index + index (LongTensor): indices of :attr:`self` tensor to fill in + value (float): the value to fill with + + Example:: + + >>> x = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=torch.float) + >>> index = torch.tensor([0, 2]) + >>> x.index_fill_(1, index, -1) + tensor([[-1., 2., -1.], + [-1., 5., -1.], + [-1., 8., -1.]]) + """ + + def index_put( + self, + indices: tuple[Tensor | None, ...] | list[Tensor | None] | None, + values: Tensor, + accumulate: _bool = False, + ) -> Tensor: + r""" + index_put(indices, values, accumulate=False) -> Tensor + + Out-place version of :meth:`~Tensor.index_put_`. + """ + + def index_put_( + self, + indices: tuple[Tensor | None, ...] | list[Tensor | None] | None, + values: Tensor, + accumulate: _bool = False, + ) -> Tensor: + r""" + index_put_(indices, values, accumulate=False) -> Tensor + + Puts values from the tensor :attr:`values` into the tensor :attr:`self` using + the indices specified in :attr:`indices` (which is a tuple of Tensors). The + expression ``tensor.index_put_(indices, values)`` is equivalent to + ``tensor[indices] = values``. Returns :attr:`self`. + + If :attr:`accumulate` is ``True``, the elements in :attr:`values` are added to + :attr:`self`. If accumulate is ``False``, the behavior is undefined if indices + contain duplicate elements. + + Args: + indices (tuple of LongTensor): tensors used to index into `self`. + values (Tensor): tensor of same dtype as `self`. + accumulate (bool): whether to accumulate into self + """ + + def index_reduce( + self, + dim: _int, + index: Tensor, + source: Tensor, + reduce: str, + *, + include_self: _bool = True, + ) -> Tensor: ... + def index_reduce_( + self, + dim: _int, + index: Tensor, + source: Tensor, + reduce: str, + *, + include_self: _bool = True, + ) -> Tensor: + r""" + index_reduce_(dim, index, source, reduce, *, include_self=True) -> Tensor + + Accumulate the elements of ``source`` into the :attr:`self` + tensor by accumulating to the indices in the order given in :attr:`index` + using the reduction given by the ``reduce`` argument. For example, if ``dim == 0``, + ``index[i] == j``, ``reduce == prod`` and ``include_self == True`` then the ``i``\ th + row of ``source`` is multiplied by the ``j``\ th row of :attr:`self`. If + :obj:`include_self="True"`, the values in the :attr:`self` tensor are included + in the reduction, otherwise, rows in the :attr:`self` tensor that are accumulated + to are treated as if they were filled with the reduction identities. + + The :attr:`dim`\ th dimension of ``source`` must have the same size as the + length of :attr:`index` (which must be a vector), and all other dimensions must + match :attr:`self`, or an error will be raised. + + For a 3-D tensor with :obj:`reduce="prod"` and :obj:`include_self=True` the + output is given as:: + + self[index[i], :, :] *= src[i, :, :] # if dim == 0 + self[:, index[i], :] *= src[:, i, :] # if dim == 1 + self[:, :, index[i]] *= src[:, :, i] # if dim == 2 + + Note: + This operation may behave nondeterministically when given tensors on a CUDA device. See :doc:`/notes/randomness` for more information. + + .. note:: + + This function only supports floating point tensors. + + .. warning:: + + This function is in beta and may change in the near future. + + Args: + dim (int): dimension along which to index + index (Tensor): indices of ``source`` to select from, + should have dtype either `torch.int64` or `torch.int32` + source (FloatTensor): the tensor containing values to accumulate + reduce (str): the reduction operation to apply + (:obj:`"prod"`, :obj:`"mean"`, :obj:`"amax"`, :obj:`"amin"`) + + Keyword args: + include_self (bool): whether the elements from the ``self`` tensor are + included in the reduction + + Example:: + + >>> x = torch.empty(5, 3).fill_(2) + >>> t = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]], dtype=torch.float) + >>> index = torch.tensor([0, 4, 2, 0]) + >>> x.index_reduce_(0, index, t, 'prod') + tensor([[20., 44., 72.], + [ 2., 2., 2.], + [14., 16., 18.], + [ 2., 2., 2.], + [ 8., 10., 12.]]) + >>> x = torch.empty(5, 3).fill_(2) + >>> x.index_reduce_(0, index, t, 'prod', include_self=False) + tensor([[10., 22., 36.], + [ 2., 2., 2.], + [ 7., 8., 9.], + [ 2., 2., 2.], + [ 4., 5., 6.]]) + """ + + @overload + def index_select(self, dim: _int, index: Tensor) -> Tensor: + r""" + index_select(dim, index) -> Tensor + + See :func:`torch.index_select` + """ + + @overload + def index_select( + self, + dim: str | EllipsisType | None, + index: Tensor, + ) -> Tensor: + r""" + index_select(dim, index) -> Tensor + + See :func:`torch.index_select` + """ + + def indices(self) -> Tensor: + r""" + indices() -> Tensor + + Return the indices tensor of a :ref:`sparse COO tensor `. + + .. warning:: + Throws an error if :attr:`self` is not a sparse COO tensor. + + See also :meth:`Tensor.values`. + + .. note:: + This method can only be called on a coalesced sparse tensor. See + :meth:`Tensor.coalesce` for details. + """ + + def inner(self, other: Tensor) -> Tensor: + r""" + inner(other) -> Tensor + + See :func:`torch.inner`. + """ + + def int(self) -> Tensor: + r""" + int(memory_format=torch.preserve_format) -> Tensor + + ``self.int()`` is equivalent to ``self.to(torch.int32)``. See :func:`to`. + + Args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + + def int_repr(self) -> Tensor: + r""" + int_repr() -> Tensor + + Given a quantized Tensor, + ``self.int_repr()`` returns a CPU Tensor with uint8_t as data type that stores the + underlying uint8_t values of the given Tensor. + """ + + def inverse(self) -> Tensor: + r""" + inverse() -> Tensor + + See :func:`torch.inverse` + """ + + def is_coalesced(self) -> _bool: + r""" + is_coalesced() -> bool + + Returns ``True`` if :attr:`self` is a :ref:`sparse COO tensor + ` that is coalesced, ``False`` otherwise. + + .. warning:: + Throws an error if :attr:`self` is not a sparse COO tensor. + + See :meth:`coalesce` and :ref:`uncoalesced tensors `. + """ + + def is_complex(self) -> _bool: + r""" + is_complex() -> bool + + Returns True if the data type of :attr:`self` is a complex data type. + """ + + def is_conj(self) -> _bool: + r""" + is_conj() -> bool + + Returns True if the conjugate bit of :attr:`self` is set to true. + """ + + def is_contiguous( + self, + memory_format: torch.memory_format = torch.contiguous_format, + ) -> _bool: + r""" + is_contiguous(memory_format=torch.contiguous_format) -> bool + + Returns True if :attr:`self` tensor is contiguous in memory in the order specified + by memory format. + + Args: + memory_format (:class:`torch.memory_format`, optional): Specifies memory allocation + order. Default: ``torch.contiguous_format``. + """ + is_cpu: _bool + r"""Is ``True`` if the Tensor is stored on the CPU, ``False`` otherwise.""" + is_cuda: _bool + r"""Is ``True`` if the Tensor is stored on the GPU, ``False`` otherwise.""" + + def is_distributed(self) -> _bool: ... + def is_floating_point(self) -> _bool: + r""" + is_floating_point() -> bool + + Returns True if the data type of :attr:`self` is a floating point data type. + """ + + def is_inference(self) -> _bool: + r""" + is_inference() -> bool + + See :func:`torch.is_inference` + """ + is_ipu: _bool + r"""Is ``True`` if the Tensor is stored on the IPU, ``False`` otherwise.""" + is_leaf: _bool + r"""All Tensors that have :attr:`requires_grad` which is ``False`` will be leaf Tensors by convention. + + For Tensors that have :attr:`requires_grad` which is ``True``, they will be leaf Tensors if they were + created by the user. This means that they are not the result of an operation and so + :attr:`grad_fn` is None. + + Only leaf Tensors will have their :attr:`grad` populated during a call to :func:`backward`. + To get :attr:`grad` populated for non-leaf Tensors, you can use :func:`retain_grad`. + + Example:: + + >>> a = torch.rand(10, requires_grad=True) + >>> a.is_leaf + True + >>> b = torch.rand(10, requires_grad=True).cuda() + >>> b.is_leaf + False + # b was created by the operation that cast a cpu Tensor into a cuda Tensor + >>> c = torch.rand(10, requires_grad=True) + 2 + >>> c.is_leaf + False + # c was created by the addition operation + >>> d = torch.rand(10).cuda() + >>> d.is_leaf + True + # d does not require gradients and so has no operation creating it (that is tracked by the autograd engine) + >>> e = torch.rand(10).cuda().requires_grad_() + >>> e.is_leaf + True + # e requires gradients and has no operations creating it + >>> f = torch.rand(10, requires_grad=True, device="cuda") + >>> f.is_leaf + True + # f requires grad, has no operation creating it""" + is_maia: _bool + is_meta: _bool + r"""Is ``True`` if the Tensor is a meta tensor, ``False`` otherwise. Meta tensors + are like normal tensors, but they carry no data.""" + is_mkldnn: _bool + is_mps: _bool + r"""Is ``True`` if the Tensor is stored on the MPS device, ``False`` otherwise.""" + is_mtia: _bool + def is_neg(self) -> _bool: + r""" + is_neg() -> bool + + Returns True if the negative bit of :attr:`self` is set to true. + """ + is_nested: _bool + def is_nonzero(self) -> _bool: ... + def is_pinned(self, device: DeviceLikeType | None = None) -> _bool: + r""" + Returns true if this tensor resides in pinned memory. + By default, the device pinned memory on will be the current :ref:`accelerator`. + """ + is_quantized: _bool + r"""Is ``True`` if the Tensor is quantized, ``False`` otherwise.""" + + def is_same_size(self, other: Tensor) -> _bool: ... + def is_set_to(self, tensor: Tensor) -> _bool: + r""" + is_set_to(tensor) -> bool + + Returns True if both tensors are pointing to the exact same memory (same + storage, offset, size and stride). + """ + + def is_signed(self) -> _bool: + r""" + is_signed() -> bool + + Returns True if the data type of :attr:`self` is a signed data type. + """ + is_sparse: _bool + r"""Is ``True`` if the Tensor uses sparse COO storage layout, ``False`` otherwise.""" + is_sparse_csr: _bool + r"""Is ``True`` if the Tensor uses sparse CSR storage layout, ``False`` otherwise.""" + is_vulkan: _bool + is_xpu: _bool + r"""Is ``True`` if the Tensor is stored on the XPU, ``False`` otherwise.""" + + def isclose( + self, + other: Tensor, + rtol: _float = 1e-05, + atol: _float = 1e-08, + equal_nan: _bool = False, + ) -> Tensor: + r""" + isclose(other, rtol=1e-05, atol=1e-08, equal_nan=False) -> Tensor + + See :func:`torch.isclose` + """ + + def isfinite(self) -> Tensor: + r""" + isfinite() -> Tensor + + See :func:`torch.isfinite` + """ + + def isinf(self) -> Tensor: + r""" + isinf() -> Tensor + + See :func:`torch.isinf` + """ + + def isnan(self) -> Tensor: + r""" + isnan() -> Tensor + + See :func:`torch.isnan` + """ + + def isneginf(self) -> Tensor: + r""" + isneginf() -> Tensor + + See :func:`torch.isneginf` + """ + + def isposinf(self) -> Tensor: + r""" + isposinf() -> Tensor + + See :func:`torch.isposinf` + """ + + def isreal(self) -> Tensor: + r""" + isreal() -> Tensor + + See :func:`torch.isreal` + """ + + def istft( + self, + n_fft: _int, + hop_length: _int | None = None, + win_length: _int | None = None, + window: Tensor | None = None, + center: _bool = True, + normalized: _bool = False, + onesided: _bool | None = None, + length: _int | None = None, + return_complex: _bool = False, + ) -> Tensor: + r""" + istft(n_fft, hop_length=None, win_length=None, window=None, + center=True, normalized=False, onesided=True, length=None) -> Tensor + + See :func:`torch.istft` + """ + + def item(self) -> Number: + r""" + item() -> number + + Returns the value of this tensor as a standard Python number. This only works + for tensors with one element. For other cases, see :meth:`~Tensor.tolist`. + + This operation is not differentiable. + + Example:: + + >>> x = torch.tensor([1.0]) + >>> x.item() + 1.0 + """ + + def kron(self, other: Tensor) -> Tensor: + r""" + kron(other) -> Tensor + + See :func:`torch.kron` + """ + + @overload + def kthvalue( + self, + k: _int | SymInt, + dim: _int = -1, + keepdim: _bool = False, + ) -> torch.return_types.kthvalue: + r""" + kthvalue(k, dim=None, keepdim=False) -> (Tensor, LongTensor) + + See :func:`torch.kthvalue` + """ + + @overload + def kthvalue( + self, + k: _int | SymInt, + dim: str | EllipsisType | None, + keepdim: _bool = False, + ) -> torch.return_types.kthvalue: + r""" + kthvalue(k, dim=None, keepdim=False) -> (Tensor, LongTensor) + + See :func:`torch.kthvalue` + """ + + def lcm(self, other: Tensor) -> Tensor: + r""" + lcm(other) -> Tensor + + See :func:`torch.lcm` + """ + + def lcm_(self, other: Tensor) -> Tensor: + r""" + lcm_(other) -> Tensor + + In-place version of :meth:`~Tensor.lcm` + """ + + def ldexp(self, other: Tensor) -> Tensor: + r""" + ldexp(other) -> Tensor + + See :func:`torch.ldexp` + """ + + def ldexp_(self, other: Tensor) -> Tensor: + r""" + ldexp_(other) -> Tensor + + In-place version of :meth:`~Tensor.ldexp` + """ + + @overload + def le(self, other: Tensor) -> Tensor: + r""" + le(other) -> Tensor + + See :func:`torch.le`. + """ + + @overload + def le(self, other: Number | _complex) -> Tensor: + r""" + le(other) -> Tensor + + See :func:`torch.le`. + """ + + @overload + def le_(self, other: Tensor) -> Tensor: + r""" + le_(other) -> Tensor + + In-place version of :meth:`~Tensor.le`. + """ + + @overload + def le_(self, other: Number | _complex) -> Tensor: + r""" + le_(other) -> Tensor + + In-place version of :meth:`~Tensor.le`. + """ + + @overload + def lerp(self, end: Tensor, weight: Tensor) -> Tensor: + r""" + lerp(end, weight) -> Tensor + + See :func:`torch.lerp` + """ + + @overload + def lerp(self, end: Tensor, weight: Number | _complex) -> Tensor: + r""" + lerp(end, weight) -> Tensor + + See :func:`torch.lerp` + """ + + @overload + def lerp_(self, end: Tensor, weight: Tensor) -> Tensor: + r""" + lerp_(end, weight) -> Tensor + + In-place version of :meth:`~Tensor.lerp` + """ + + @overload + def lerp_(self, end: Tensor, weight: Number | _complex) -> Tensor: + r""" + lerp_(end, weight) -> Tensor + + In-place version of :meth:`~Tensor.lerp` + """ + + @overload + def less(self, other: Tensor) -> Tensor: + r""" + lt(other) -> Tensor + + See :func:`torch.less`. + """ + + @overload + def less(self, other: Number | _complex) -> Tensor: + r""" + lt(other) -> Tensor + + See :func:`torch.less`. + """ + + @overload + def less_(self, other: Tensor) -> Tensor: + r""" + less_(other) -> Tensor + + In-place version of :meth:`~Tensor.less`. + """ + + @overload + def less_(self, other: Number | _complex) -> Tensor: + r""" + less_(other) -> Tensor + + In-place version of :meth:`~Tensor.less`. + """ + + @overload + def less_equal(self, other: Tensor) -> Tensor: + r""" + less_equal(other) -> Tensor + + See :func:`torch.less_equal`. + """ + + @overload + def less_equal(self, other: Number | _complex) -> Tensor: + r""" + less_equal(other) -> Tensor + + See :func:`torch.less_equal`. + """ + + @overload + def less_equal_(self, other: Tensor) -> Tensor: + r""" + less_equal_(other) -> Tensor + + In-place version of :meth:`~Tensor.less_equal`. + """ + + @overload + def less_equal_(self, other: Number | _complex) -> Tensor: + r""" + less_equal_(other) -> Tensor + + In-place version of :meth:`~Tensor.less_equal`. + """ + + def lgamma(self) -> Tensor: + r""" + lgamma() -> Tensor + + See :func:`torch.lgamma` + """ + + def lgamma_(self) -> Tensor: + r""" + lgamma_() -> Tensor + + In-place version of :meth:`~Tensor.lgamma` + """ + + def log(self) -> Tensor: + r""" + log() -> Tensor + + See :func:`torch.log` + """ + + def log10(self) -> Tensor: + r""" + log10() -> Tensor + + See :func:`torch.log10` + """ + + def log10_(self) -> Tensor: + r""" + log10_() -> Tensor + + In-place version of :meth:`~Tensor.log10` + """ + + def log1p(self) -> Tensor: + r""" + log1p() -> Tensor + + See :func:`torch.log1p` + """ + + def log1p_(self) -> Tensor: + r""" + log1p_() -> Tensor + + In-place version of :meth:`~Tensor.log1p` + """ + + def log2(self) -> Tensor: + r""" + log2() -> Tensor + + See :func:`torch.log2` + """ + + def log2_(self) -> Tensor: + r""" + log2_() -> Tensor + + In-place version of :meth:`~Tensor.log2` + """ + + def log_(self) -> Tensor: + r""" + log_() -> Tensor + + In-place version of :meth:`~Tensor.log` + """ + + def log_normal_( + self, + mean: _float = 1, + std: _float = 2, + *, + generator: Generator | None = None, + ) -> Tensor: + r""" + log_normal_(mean=1, std=2, *, generator=None) + + Fills :attr:`self` tensor with numbers samples from the log-normal distribution + parameterized by the given mean :math:`\mu` and standard deviation + :math:`\sigma`. Note that :attr:`mean` and :attr:`std` are the mean and + standard deviation of the underlying normal distribution, and not of the + returned distribution: + + .. math:: + + f(x) = \dfrac{1}{x \sigma \sqrt{2\pi}}\ e^{-\frac{(\ln x - \mu)^2}{2\sigma^2}} + """ + + @overload + def log_softmax(self, dim: _int, dtype: _dtype | None = None) -> Tensor: ... + @overload + def log_softmax( + self, + dim: str | EllipsisType | None, + *, + dtype: _dtype | None = None, + ) -> Tensor: ... + def logaddexp(self, other: Tensor) -> Tensor: + r""" + logaddexp(other) -> Tensor + + See :func:`torch.logaddexp` + """ + + def logaddexp2(self, other: Tensor) -> Tensor: + r""" + logaddexp2(other) -> Tensor + + See :func:`torch.logaddexp2` + """ + + @overload + def logcumsumexp(self, dim: _int) -> Tensor: + r""" + logcumsumexp(dim) -> Tensor + + See :func:`torch.logcumsumexp` + """ + + @overload + def logcumsumexp(self, dim: str | EllipsisType | None) -> Tensor: + r""" + logcumsumexp(dim) -> Tensor + + See :func:`torch.logcumsumexp` + """ + + def logdet(self) -> Tensor: + r""" + logdet() -> Tensor + + See :func:`torch.logdet` + """ + + def logical_and(self, other: Tensor) -> Tensor: + r""" + logical_and() -> Tensor + + See :func:`torch.logical_and` + """ + + def logical_and_(self, other: Tensor) -> Tensor: + r""" + logical_and_() -> Tensor + + In-place version of :meth:`~Tensor.logical_and` + """ + + def logical_not(self) -> Tensor: + r""" + logical_not() -> Tensor + + See :func:`torch.logical_not` + """ + + def logical_not_(self) -> Tensor: + r""" + logical_not_() -> Tensor + + In-place version of :meth:`~Tensor.logical_not` + """ + + def logical_or(self, other: Tensor) -> Tensor: + r""" + logical_or() -> Tensor + + See :func:`torch.logical_or` + """ + + def logical_or_(self, other: Tensor) -> Tensor: + r""" + logical_or_() -> Tensor + + In-place version of :meth:`~Tensor.logical_or` + """ + + def logical_xor(self, other: Tensor) -> Tensor: + r""" + logical_xor() -> Tensor + + See :func:`torch.logical_xor` + """ + + def logical_xor_(self, other: Tensor) -> Tensor: + r""" + logical_xor_() -> Tensor + + In-place version of :meth:`~Tensor.logical_xor` + """ + + def logit(self, eps: _float | None = None) -> Tensor: + r""" + logit() -> Tensor + + See :func:`torch.logit` + """ + + def logit_(self, eps: _float | None = None) -> Tensor: + r""" + logit_() -> Tensor + + In-place version of :meth:`~Tensor.logit` + """ + + @overload + def logsumexp(self, dim: _int | _size, keepdim: _bool = False) -> Tensor: + r""" + logsumexp(dim, keepdim=False) -> Tensor + + See :func:`torch.logsumexp` + """ + + @overload + def logsumexp( + self, + dim: Sequence[str | EllipsisType | None], + keepdim: _bool = False, + ) -> Tensor: + r""" + logsumexp(dim, keepdim=False) -> Tensor + + See :func:`torch.logsumexp` + """ + + def long(self) -> Tensor: + r""" + long(memory_format=torch.preserve_format) -> Tensor + + ``self.long()`` is equivalent to ``self.to(torch.int64)``. See :func:`to`. + + Args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + + @overload + def lt(self, other: Tensor) -> Tensor: + r""" + lt(other) -> Tensor + + See :func:`torch.lt`. + """ + + @overload + def lt(self, other: Number | _complex) -> Tensor: + r""" + lt(other) -> Tensor + + See :func:`torch.lt`. + """ + + @overload + def lt_(self, other: Tensor) -> Tensor: + r""" + lt_(other) -> Tensor + + In-place version of :meth:`~Tensor.lt`. + """ + + @overload + def lt_(self, other: Number | _complex) -> Tensor: + r""" + lt_(other) -> Tensor + + In-place version of :meth:`~Tensor.lt`. + """ + + def lu_solve(self, LU_data: Tensor, LU_pivots: Tensor) -> Tensor: + r""" + lu_solve(LU_data, LU_pivots) -> Tensor + + See :func:`torch.lu_solve` + """ + + def map2_(self, x: Tensor, y: Tensor, callable: Callable) -> Tensor: ... + def map_(self, other: Tensor, callable: Callable) -> Tensor: + r""" + map_(tensor, callable) + + Applies :attr:`callable` for each element in :attr:`self` tensor and the given + :attr:`tensor` and stores the results in :attr:`self` tensor. :attr:`self` tensor and + the given :attr:`tensor` must be :ref:`broadcastable `. + + The :attr:`callable` should have the signature:: + + def callable(a, b) -> number + """ + + @overload + def masked_fill(self, mask: Tensor, value: Tensor) -> Tensor: + r""" + masked_fill(mask, value) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.masked_fill_` + """ + + @overload + def masked_fill(self, mask: Tensor, value: Number | _complex) -> Tensor: + r""" + masked_fill(mask, value) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.masked_fill_` + """ + + @overload + def masked_fill_(self, mask: Tensor, value: Tensor) -> Tensor: + r""" + masked_fill_(mask, value) + + Fills elements of :attr:`self` tensor with :attr:`value` where :attr:`mask` is + True. The shape of :attr:`mask` must be + :ref:`broadcastable ` with the shape of the underlying + tensor. + + Args: + mask (BoolTensor): the boolean mask + value (float): the value to fill in with + """ + + @overload + def masked_fill_(self, mask: Tensor, value: Number | _complex) -> Tensor: + r""" + masked_fill_(mask, value) + + Fills elements of :attr:`self` tensor with :attr:`value` where :attr:`mask` is + True. The shape of :attr:`mask` must be + :ref:`broadcastable ` with the shape of the underlying + tensor. + + Args: + mask (BoolTensor): the boolean mask + value (float): the value to fill in with + """ + + def masked_scatter(self, mask: Tensor, source: Tensor) -> Tensor: + r""" + masked_scatter(mask, tensor) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.masked_scatter_` + + .. note:: + + The inputs :attr:`self` and :attr:`mask` + :ref:`broadcast `. + + Example: + + >>> self = torch.tensor([0, 0, 0, 0, 0]) + >>> mask = torch.tensor( + ... [[0, 0, 0, 1, 1], [1, 1, 0, 1, 1]], + ... dtype=torch.bool, + ... ) + >>> source = torch.tensor([[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]]) + >>> self.masked_scatter(mask, source) + tensor([[0, 0, 0, 0, 1], + [2, 3, 0, 4, 5]]) + """ + + def masked_scatter_(self, mask: Tensor, source: Tensor) -> Tensor: + r""" + masked_scatter_(mask, source) + + Copies elements from :attr:`source` into :attr:`self` tensor at positions where + the :attr:`mask` is True. Elements from :attr:`source` are copied into :attr:`self` + starting at position 0 of :attr:`source` and continuing in order one-by-one for each + occurrence of :attr:`mask` being True. + The shape of :attr:`mask` must be :ref:`broadcastable ` + with the shape of the underlying tensor. The :attr:`source` should have at least + as many elements as the number of ones in :attr:`mask`. + + Args: + mask (BoolTensor): the boolean mask + source (Tensor): the tensor to copy from + + .. note:: + + The :attr:`mask` operates on the :attr:`self` tensor, not on the given + :attr:`source` tensor. + + Example: + + >>> self = torch.tensor([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]) + >>> mask = torch.tensor( + ... [[0, 0, 0, 1, 1], [1, 1, 0, 1, 1]], + ... dtype=torch.bool, + ... ) + >>> source = torch.tensor([[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]]) + >>> self.masked_scatter_(mask, source) + tensor([[0, 0, 0, 0, 1], + [2, 3, 0, 4, 5]]) + """ + + def masked_select(self, mask: Tensor) -> Tensor: + r""" + masked_select(mask) -> Tensor + + See :func:`torch.masked_select` + """ + + def matmul(self, other: Tensor) -> Tensor: + r""" + matmul(tensor2) -> Tensor + + See :func:`torch.matmul` + """ + + def matrix_exp(self) -> Tensor: + r""" + matrix_exp() -> Tensor + + See :func:`torch.matrix_exp` + """ + + def matrix_power(self, n: _int) -> Tensor: + r""" + matrix_power(n) -> Tensor + + .. note:: :meth:`~Tensor.matrix_power` is deprecated, use :func:`torch.linalg.matrix_power` instead. + + Alias for :func:`torch.linalg.matrix_power` + """ + + @overload + def max(self) -> Tensor: + r""" + max(dim=None, keepdim=False) -> Tensor or (Tensor, Tensor) + + See :func:`torch.max` + """ + + @overload + def max(self, other: Tensor) -> Tensor: + r""" + max(dim=None, keepdim=False) -> Tensor or (Tensor, Tensor) + + See :func:`torch.max` + """ + + @overload + def max(self, dim: _int, keepdim: _bool = False) -> torch.return_types.max: + r""" + max(dim=None, keepdim=False) -> Tensor or (Tensor, Tensor) + + See :func:`torch.max` + """ + + @overload + def max( + self, + dim: str | EllipsisType | None, + keepdim: _bool = False, + ) -> torch.return_types.max: + r""" + max(dim=None, keepdim=False) -> Tensor or (Tensor, Tensor) + + See :func:`torch.max` + """ + + def maximum(self, other: Tensor) -> Tensor: + r""" + maximum(other) -> Tensor + + See :func:`torch.maximum` + """ + + @overload + def mean(self, *, dtype: _dtype | None = None) -> Tensor: + r""" + mean(dim=None, keepdim=False, *, dtype=None) -> Tensor + + See :func:`torch.mean` + """ + + @overload + def mean( + self, + dim: _int | _size | None, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + ) -> Tensor: + r""" + mean(dim=None, keepdim=False, *, dtype=None) -> Tensor + + See :func:`torch.mean` + """ + + @overload + def mean( + self, + dim: Sequence[str | EllipsisType | None], + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + ) -> Tensor: + r""" + mean(dim=None, keepdim=False, *, dtype=None) -> Tensor + + See :func:`torch.mean` + """ + + @overload + def median(self) -> Tensor: + r""" + median(dim=None, keepdim=False) -> (Tensor, LongTensor) + + See :func:`torch.median` + """ + + @overload + def median( + self, + dim: _int, + keepdim: _bool = False, + ) -> torch.return_types.median: + r""" + median(dim=None, keepdim=False) -> (Tensor, LongTensor) + + See :func:`torch.median` + """ + + @overload + def median( + self, + dim: str | EllipsisType | None, + keepdim: _bool = False, + ) -> torch.return_types.median: + r""" + median(dim=None, keepdim=False) -> (Tensor, LongTensor) + + See :func:`torch.median` + """ + + @overload + def min(self) -> Tensor: + r""" + min(dim=None, keepdim=False) -> Tensor or (Tensor, Tensor) + + See :func:`torch.min` + """ + + @overload + def min(self, other: Tensor) -> Tensor: + r""" + min(dim=None, keepdim=False) -> Tensor or (Tensor, Tensor) + + See :func:`torch.min` + """ + + @overload + def min(self, dim: _int, keepdim: _bool = False) -> torch.return_types.min: + r""" + min(dim=None, keepdim=False) -> Tensor or (Tensor, Tensor) + + See :func:`torch.min` + """ + + @overload + def min( + self, + dim: str | EllipsisType | None, + keepdim: _bool = False, + ) -> torch.return_types.min: + r""" + min(dim=None, keepdim=False) -> Tensor or (Tensor, Tensor) + + See :func:`torch.min` + """ + + def minimum(self, other: Tensor) -> Tensor: + r""" + minimum(other) -> Tensor + + See :func:`torch.minimum` + """ + + def mm(self, mat2: Tensor) -> Tensor: + r""" + mm(mat2) -> Tensor + + See :func:`torch.mm` + """ + + @overload + def mode( + self, + dim: _int = -1, + keepdim: _bool = False, + ) -> torch.return_types.mode: + r""" + mode(dim=None, keepdim=False) -> (Tensor, LongTensor) + + See :func:`torch.mode` + """ + + @overload + def mode( + self, + dim: str | EllipsisType | None, + keepdim: _bool = False, + ) -> torch.return_types.mode: + r""" + mode(dim=None, keepdim=False) -> (Tensor, LongTensor) + + See :func:`torch.mode` + """ + + @overload + def moveaxis(self, source: _int, destination: _int) -> Tensor: + r""" + moveaxis(source, destination) -> Tensor + + See :func:`torch.moveaxis` + """ + + @overload + def moveaxis(self, source: _size, destination: _size) -> Tensor: + r""" + moveaxis(source, destination) -> Tensor + + See :func:`torch.moveaxis` + """ + + @overload + def movedim(self, source: _int, destination: _int) -> Tensor: + r""" + movedim(source, destination) -> Tensor + + See :func:`torch.movedim` + """ + + @overload + def movedim(self, source: _size, destination: _size) -> Tensor: + r""" + movedim(source, destination) -> Tensor + + See :func:`torch.movedim` + """ + + def msort(self) -> Tensor: + r""" + msort() -> Tensor + + See :func:`torch.msort` + """ + + def mul( + self, + other: Tensor | Number | _complex | torch.SymInt | torch.SymFloat, + *, + out: Tensor | None = None, + ) -> Tensor: + r""" + mul(value) -> Tensor + + See :func:`torch.mul`. + """ + + def mul_( + self, + other: Tensor | Number | _complex | torch.SymInt | torch.SymFloat, + ) -> Tensor: + r""" + mul_(value) -> Tensor + + In-place version of :meth:`~Tensor.mul`. + """ + + def multinomial( + self, + num_samples: _int | SymInt, + replacement: _bool = False, + *, + generator: Generator | None = None, + ) -> Tensor: + r""" + multinomial(num_samples, replacement=False, *, generator=None) -> Tensor + + See :func:`torch.multinomial` + """ + + @overload + def multiply(self, other: Tensor) -> Tensor: + r""" + multiply(value) -> Tensor + + See :func:`torch.multiply`. + """ + + @overload + def multiply(self, other: Number | _complex) -> Tensor: + r""" + multiply(value) -> Tensor + + See :func:`torch.multiply`. + """ + + @overload + def multiply_(self, other: Tensor) -> Tensor: + r""" + multiply_(value) -> Tensor + + In-place version of :meth:`~Tensor.multiply`. + """ + + @overload + def multiply_(self, other: Number | _complex) -> Tensor: + r""" + multiply_(value) -> Tensor + + In-place version of :meth:`~Tensor.multiply`. + """ + + def mv(self, vec: Tensor) -> Tensor: + r""" + mv(vec) -> Tensor + + See :func:`torch.mv` + """ + + def mvlgamma(self, p: _int) -> Tensor: + r""" + mvlgamma(p) -> Tensor + + See :func:`torch.mvlgamma` + """ + + def mvlgamma_(self, p: _int) -> Tensor: + r""" + mvlgamma_(p) -> Tensor + + In-place version of :meth:`~Tensor.mvlgamma` + """ + + def nan_to_num( + self, + nan: _float | None = None, + posinf: _float | None = None, + neginf: _float | None = None, + ) -> Tensor: + r""" + nan_to_num(nan=0.0, posinf=None, neginf=None) -> Tensor + + See :func:`torch.nan_to_num`. + """ + + def nan_to_num_( + self, + nan: _float | None = None, + posinf: _float | None = None, + neginf: _float | None = None, + ) -> Tensor: + r""" + nan_to_num_(nan=0.0, posinf=None, neginf=None) -> Tensor + + In-place version of :meth:`~Tensor.nan_to_num`. + """ + + def nanmean( + self, + dim: _int | _size | None = None, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + ) -> Tensor: + r""" + nanmean(dim=None, keepdim=False, *, dtype=None) -> Tensor + + See :func:`torch.nanmean` + """ + + @overload + def nanmedian(self) -> Tensor: + r""" + nanmedian(dim=None, keepdim=False) -> (Tensor, LongTensor) + + See :func:`torch.nanmedian` + """ + + @overload + def nanmedian( + self, + dim: _int, + keepdim: _bool = False, + ) -> torch.return_types.nanmedian: + r""" + nanmedian(dim=None, keepdim=False) -> (Tensor, LongTensor) + + See :func:`torch.nanmedian` + """ + + @overload + def nanmedian( + self, + dim: str | EllipsisType | None, + keepdim: _bool = False, + ) -> torch.return_types.nanmedian: + r""" + nanmedian(dim=None, keepdim=False) -> (Tensor, LongTensor) + + See :func:`torch.nanmedian` + """ + + @overload + def nanquantile( + self, + q: Tensor, + dim: _int | None = None, + keepdim: _bool = False, + *, + interpolation: str = "linear", + ) -> Tensor: + r""" + nanquantile(q, dim=None, keepdim=False, *, interpolation='linear') -> Tensor + + See :func:`torch.nanquantile` + """ + + @overload + def nanquantile( + self, + q: _float, + dim: _int | None = None, + keepdim: _bool = False, + *, + interpolation: str = "linear", + ) -> Tensor: + r""" + nanquantile(q, dim=None, keepdim=False, *, interpolation='linear') -> Tensor + + See :func:`torch.nanquantile` + """ + + def nansum( + self, + dim: _int | _size | None = None, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + ) -> Tensor: + r""" + nansum(dim=None, keepdim=False, dtype=None) -> Tensor + + See :func:`torch.nansum` + """ + + @overload + def narrow(self, dim: _int, start: Tensor, length: _int | SymInt) -> Tensor: + r""" + narrow(dimension, start, length) -> Tensor + + See :func:`torch.narrow`. + """ + + @overload + def narrow( + self, + dim: _int, + start: _int | SymInt, + length: _int | SymInt, + ) -> Tensor: + r""" + narrow(dimension, start, length) -> Tensor + + See :func:`torch.narrow`. + """ + + def narrow_copy( + self, + dim: _int, + start: _int | SymInt, + length: _int | SymInt, + ) -> Tensor: + r""" + narrow_copy(dimension, start, length) -> Tensor + + See :func:`torch.narrow_copy`. + """ + + def ndimension(self) -> _int: + r""" + ndimension() -> int + + Alias for :meth:`~Tensor.dim()` + """ + + @overload + def ne(self, other: Tensor) -> Tensor: + r""" + ne(other) -> Tensor + + See :func:`torch.ne`. + """ + + @overload + def ne(self, other: Number | _complex) -> Tensor: + r""" + ne(other) -> Tensor + + See :func:`torch.ne`. + """ + + @overload + def ne_(self, other: Tensor) -> Tensor: + r""" + ne_(other) -> Tensor + + In-place version of :meth:`~Tensor.ne`. + """ + + @overload + def ne_(self, other: Number | _complex) -> Tensor: + r""" + ne_(other) -> Tensor + + In-place version of :meth:`~Tensor.ne`. + """ + + def neg(self) -> Tensor: + r""" + neg() -> Tensor + + See :func:`torch.neg` + """ + + def neg_(self) -> Tensor: + r""" + neg_() -> Tensor + + In-place version of :meth:`~Tensor.neg` + """ + + def negative(self) -> Tensor: + r""" + negative() -> Tensor + + See :func:`torch.negative` + """ + + def negative_(self) -> Tensor: + r""" + negative_() -> Tensor + + In-place version of :meth:`~Tensor.negative` + """ + + def nelement(self) -> _int: + r""" + nelement() -> int + + Alias for :meth:`~Tensor.numel` + """ + + @overload + def new(cls, *args: Any, device: DeviceLikeType | None = None) -> Self: ... + @overload + def new(cls, storage: Storage) -> Self: ... + @overload + def new(cls, other: Tensor) -> Self: ... + @overload + def new(cls, size: _size, *, device: DeviceLikeType | None = None) -> Self: ... + @overload + def new_empty( + self, + size: Sequence[_int | SymInt], + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, + ) -> Tensor: + r""" + new_empty(size, *, dtype=None, device=None, requires_grad=False, layout=torch.strided, pin_memory=False) -> Tensor + + + Returns a Tensor of size :attr:`size` filled with uninitialized data. + By default, the returned Tensor has the same :class:`torch.dtype` and + :class:`torch.device` as this tensor. + + Args: + size (int...): a list, tuple, or :class:`torch.Size` of integers defining the + shape of the output tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired type of returned tensor. + Default: if None, same :class:`torch.dtype` as this tensor. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if None, same :class:`torch.device` as this tensor. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> tensor = torch.ones(()) + >>> tensor.new_empty((2, 3)) + tensor([[ 5.8182e-18, 4.5765e-41, -1.0545e+30], + [ 3.0949e-41, 4.4842e-44, 0.0000e+00]]) + """ + + @overload + def new_empty( + self, + *size: _int | SymInt, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, + ) -> Tensor: + r""" + new_empty(size, *, dtype=None, device=None, requires_grad=False, layout=torch.strided, pin_memory=False) -> Tensor + + + Returns a Tensor of size :attr:`size` filled with uninitialized data. + By default, the returned Tensor has the same :class:`torch.dtype` and + :class:`torch.device` as this tensor. + + Args: + size (int...): a list, tuple, or :class:`torch.Size` of integers defining the + shape of the output tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired type of returned tensor. + Default: if None, same :class:`torch.dtype` as this tensor. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if None, same :class:`torch.device` as this tensor. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> tensor = torch.ones(()) + >>> tensor.new_empty((2, 3)) + tensor([[ 5.8182e-18, 4.5765e-41, -1.0545e+30], + [ 3.0949e-41, 4.4842e-44, 0.0000e+00]]) + """ + + def new_empty_strided( + self, + size: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, + ) -> Tensor: + r""" + new_empty_strided(size, stride, dtype=None, device=None, requires_grad=False, layout=torch.strided, pin_memory=False) -> Tensor + + + Returns a Tensor of size :attr:`size` and strides :attr:`stride` filled with + uninitialized data. By default, the returned Tensor has the same + :class:`torch.dtype` and :class:`torch.device` as this tensor. + + Args: + size (int...): a list, tuple, or :class:`torch.Size` of integers defining the + shape of the output tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired type of returned tensor. + Default: if None, same :class:`torch.dtype` as this tensor. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if None, same :class:`torch.device` as this tensor. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> tensor = torch.ones(()) + >>> tensor.new_empty_strided((2, 3), (3, 1)) + tensor([[ 5.8182e-18, 4.5765e-41, -1.0545e+30], + [ 3.0949e-41, 4.4842e-44, 0.0000e+00]]) + """ + + def new_full( + self, + size: Sequence[_int | SymInt], + fill_value: Number | _complex, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, + ) -> Tensor: + r""" + new_full(size, fill_value, *, dtype=None, device=None, requires_grad=False, layout=torch.strided, pin_memory=False) -> Tensor + + + Returns a Tensor of size :attr:`size` filled with :attr:`fill_value`. + By default, the returned Tensor has the same :class:`torch.dtype` and + :class:`torch.device` as this tensor. + + Args: + fill_value (scalar): the number to fill the output tensor with. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired type of returned tensor. + Default: if None, same :class:`torch.dtype` as this tensor. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if None, same :class:`torch.device` as this tensor. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> tensor = torch.ones((2,), dtype=torch.float64) + >>> tensor.new_full((3, 4), 3.141592) + tensor([[ 3.1416, 3.1416, 3.1416, 3.1416], + [ 3.1416, 3.1416, 3.1416, 3.1416], + [ 3.1416, 3.1416, 3.1416, 3.1416]], dtype=torch.float64) + """ + + @overload + def new_ones( + self, + size: _size, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, + ) -> Tensor: + r""" + new_ones(size, *, dtype=None, device=None, requires_grad=False, layout=torch.strided, pin_memory=False) -> Tensor + + + Returns a Tensor of size :attr:`size` filled with ``1``. + By default, the returned Tensor has the same :class:`torch.dtype` and + :class:`torch.device` as this tensor. + + Args: + size (int...): a list, tuple, or :class:`torch.Size` of integers defining the + shape of the output tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired type of returned tensor. + Default: if None, same :class:`torch.dtype` as this tensor. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if None, same :class:`torch.device` as this tensor. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> tensor = torch.tensor((), dtype=torch.int32) + >>> tensor.new_ones((2, 3)) + tensor([[ 1, 1, 1], + [ 1, 1, 1]], dtype=torch.int32) + """ + + @overload + def new_ones( + self, + size: Sequence[_int | SymInt], + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, + ) -> Tensor: + r""" + new_ones(size, *, dtype=None, device=None, requires_grad=False, layout=torch.strided, pin_memory=False) -> Tensor + + + Returns a Tensor of size :attr:`size` filled with ``1``. + By default, the returned Tensor has the same :class:`torch.dtype` and + :class:`torch.device` as this tensor. + + Args: + size (int...): a list, tuple, or :class:`torch.Size` of integers defining the + shape of the output tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired type of returned tensor. + Default: if None, same :class:`torch.dtype` as this tensor. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if None, same :class:`torch.device` as this tensor. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> tensor = torch.tensor((), dtype=torch.int32) + >>> tensor.new_ones((2, 3)) + tensor([[ 1, 1, 1], + [ 1, 1, 1]], dtype=torch.int32) + """ + + @overload + def new_ones( + self, + *size: _int | SymInt, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, + ) -> Tensor: + r""" + new_ones(size, *, dtype=None, device=None, requires_grad=False, layout=torch.strided, pin_memory=False) -> Tensor + + + Returns a Tensor of size :attr:`size` filled with ``1``. + By default, the returned Tensor has the same :class:`torch.dtype` and + :class:`torch.device` as this tensor. + + Args: + size (int...): a list, tuple, or :class:`torch.Size` of integers defining the + shape of the output tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired type of returned tensor. + Default: if None, same :class:`torch.dtype` as this tensor. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if None, same :class:`torch.device` as this tensor. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> tensor = torch.tensor((), dtype=torch.int32) + >>> tensor.new_ones((2, 3)) + tensor([[ 1, 1, 1], + [ 1, 1, 1]], dtype=torch.int32) + """ + + def new_tensor( + self, + data: Any, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, + ) -> Tensor: + r""" + new_tensor(data, *, dtype=None, device=None, requires_grad=False, layout=torch.strided, pin_memory=False) -> Tensor + + + Returns a new Tensor with :attr:`data` as the tensor data. + By default, the returned Tensor has the same :class:`torch.dtype` and + :class:`torch.device` as this tensor. + + .. warning:: + + :func:`new_tensor` always copies :attr:`data`. If you have a Tensor + ``data`` and want to avoid a copy, use :func:`torch.Tensor.requires_grad_` + or :func:`torch.Tensor.detach`. + If you have a numpy array and want to avoid a copy, use + :func:`torch.from_numpy`. + + .. warning:: + + When data is a tensor `x`, :func:`new_tensor()` reads out 'the data' from whatever it is passed, + and constructs a leaf variable. Therefore ``tensor.new_tensor(x)`` is equivalent to ``x.detach().clone()`` + and ``tensor.new_tensor(x, requires_grad=True)`` is equivalent to ``x.detach().clone().requires_grad_(True)``. + The equivalents using ``detach()`` and ``clone()`` are recommended. + + Args: + data (array_like): The returned Tensor copies :attr:`data`. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired type of returned tensor. + Default: if None, same :class:`torch.dtype` as this tensor. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if None, same :class:`torch.device` as this tensor. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> tensor = torch.ones((2,), dtype=torch.int8) + >>> data = [[0, 1], [2, 3]] + >>> tensor.new_tensor(data) + tensor([[ 0, 1], + [ 2, 3]], dtype=torch.int8) + """ + + @overload + def new_zeros( + self, + size: Sequence[_int | SymInt], + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, + ) -> Tensor: + r""" + new_zeros(size, *, dtype=None, device=None, requires_grad=False, layout=torch.strided, pin_memory=False) -> Tensor + + + Returns a Tensor of size :attr:`size` filled with ``0``. + By default, the returned Tensor has the same :class:`torch.dtype` and + :class:`torch.device` as this tensor. + + Args: + size (int...): a list, tuple, or :class:`torch.Size` of integers defining the + shape of the output tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired type of returned tensor. + Default: if None, same :class:`torch.dtype` as this tensor. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if None, same :class:`torch.device` as this tensor. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> tensor = torch.tensor((), dtype=torch.float64) + >>> tensor.new_zeros((2, 3)) + tensor([[ 0., 0., 0.], + [ 0., 0., 0.]], dtype=torch.float64) + """ + + @overload + def new_zeros( + self, + *size: _int | SymInt, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, + ) -> Tensor: + r""" + new_zeros(size, *, dtype=None, device=None, requires_grad=False, layout=torch.strided, pin_memory=False) -> Tensor + + + Returns a Tensor of size :attr:`size` filled with ``0``. + By default, the returned Tensor has the same :class:`torch.dtype` and + :class:`torch.device` as this tensor. + + Args: + size (int...): a list, tuple, or :class:`torch.Size` of integers defining the + shape of the output tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired type of returned tensor. + Default: if None, same :class:`torch.dtype` as this tensor. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if None, same :class:`torch.device` as this tensor. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> tensor = torch.tensor((), dtype=torch.float64) + >>> tensor.new_zeros((2, 3)) + tensor([[ 0., 0., 0.], + [ 0., 0., 0.]], dtype=torch.float64) + """ + + def nextafter(self, other: Tensor) -> Tensor: + r""" + nextafter(other) -> Tensor + See :func:`torch.nextafter` + """ + + def nextafter_(self, other: Tensor) -> Tensor: + r""" + nextafter_(other) -> Tensor + In-place version of :meth:`~Tensor.nextafter` + """ + + @overload + def nonzero(self, *, as_tuple: Literal[False] = False) -> Tensor: + r""" + nonzero() -> LongTensor + + See :func:`torch.nonzero` + """ + + @overload + def nonzero(self, *, as_tuple: Literal[True]) -> tuple[Tensor, ...]: + r""" + nonzero() -> LongTensor + + See :func:`torch.nonzero` + """ + + def nonzero_static( + self, + *, + size: _int | SymInt, + fill_value: _int = -1, + ) -> Tensor: + r""" + nonzero_static(input, *, size, fill_value=-1) -> Tensor + + Returns a 2-D tensor where each row is the index for a non-zero value. + The returned Tensor has the same `torch.dtype` as `torch.nonzero()`. + + Args: + input (Tensor): the input tensor to count non-zero elements. + + Keyword args: + size (int): the size of non-zero elements expected to be included in the out + tensor. Pad the out tensor with `fill_value` if the `size` is larger + than total number of non-zero elements, truncate out tensor if `size` + is smaller. The size must be a non-negative integer. + fill_value (int, optional): the value to fill the output tensor with when `size` is larger + than the total number of non-zero elements. Default is `-1` to represent + invalid index. + + Example: + + # Example 1: Padding + >>> input_tensor = torch.tensor([[1, 0], [3, 2]]) + >>> static_size = 4 + >>> t = torch.nonzero_static(input_tensor, size=static_size) + tensor([[ 0, 0], + [ 1, 0], + [ 1, 1], + [ -1, -1]], dtype=torch.int64) + + # Example 2: Truncating + >>> input_tensor = torch.tensor([[1, 0], [3, 2]]) + >>> static_size = 2 + >>> t = torch.nonzero_static(input_tensor, size=static_size) + tensor([[ 0, 0], + [ 1, 0]], dtype=torch.int64) + + # Example 3: 0 size + >>> input_tensor = torch.tensor([10]) + >>> static_size = 0 + >>> t = torch.nonzero_static(input_tensor, size=static_size) + tensor([], size=(0, 1), dtype=torch.int64) + + # Example 4: 0 rank input + >>> input_tensor = torch.tensor(10) + >>> static_size = 2 + >>> t = torch.nonzero_static(input_tensor, size=static_size) + tensor([], size=(2, 0), dtype=torch.int64) + """ + + def normal_( + self, + mean: _float = 0, + std: _float = 1, + *, + generator: Generator | None = None, + ) -> Tensor: + r""" + normal_(mean=0, std=1, *, generator=None) -> Tensor + + Fills :attr:`self` tensor with elements samples from the normal distribution + parameterized by :attr:`mean` and :attr:`std`. + """ + + @overload + def not_equal(self, other: Tensor) -> Tensor: + r""" + not_equal(other) -> Tensor + + See :func:`torch.not_equal`. + """ + + @overload + def not_equal(self, other: Number | _complex) -> Tensor: + r""" + not_equal(other) -> Tensor + + See :func:`torch.not_equal`. + """ + + @overload + def not_equal_(self, other: Tensor) -> Tensor: + r""" + not_equal_(other) -> Tensor + + In-place version of :meth:`~Tensor.not_equal`. + """ + + @overload + def not_equal_(self, other: Number | _complex) -> Tensor: + r""" + not_equal_(other) -> Tensor + + In-place version of :meth:`~Tensor.not_equal`. + """ + + def numel(self) -> _int: + r""" + numel() -> int + + See :func:`torch.numel` + """ + + def numpy(self, *, force: _bool = False) -> numpy.ndarray: + r""" + numpy(*, force=False) -> numpy.ndarray + + Returns the tensor as a NumPy :class:`ndarray`. + + If :attr:`force` is ``False`` (the default), the conversion + is performed only if the tensor is on the CPU, does not require grad, + does not have its conjugate bit set, and is a dtype and layout that + NumPy supports. The returned ndarray and the tensor will share their + storage, so changes to the tensor will be reflected in the ndarray + and vice versa. + + If :attr:`force` is ``True`` this is equivalent to + calling ``t.detach().cpu().resolve_conj().resolve_neg().numpy()``. + If the tensor isn't on the CPU or the conjugate or negative bit is set, + the tensor won't share its storage with the returned ndarray. + Setting :attr:`force` to ``True`` can be a useful shorthand. + + Args: + force (bool): if ``True``, the ndarray may be a copy of the tensor + instead of always sharing memory, defaults to ``False``. + """ + + def orgqr(self, input2: Tensor) -> Tensor: + r""" + orgqr(input2) -> Tensor + + See :func:`torch.orgqr` + """ + + def ormqr( + self, + input2: Tensor, + input3: Tensor, + left: _bool = True, + transpose: _bool = False, + ) -> Tensor: + r""" + ormqr(input2, input3, left=True, transpose=False) -> Tensor + + See :func:`torch.ormqr` + """ + + def outer(self, vec2: Tensor) -> Tensor: + r""" + outer(vec2) -> Tensor + + See :func:`torch.outer`. + """ + + @overload + def permute(self, dims: _size) -> Tensor: + r""" + permute(*dims) -> Tensor + + Returns a view of the tensor with its dimensions permuted. + + Args: + dims (torch.Size, int..., tuple of int or list of int): the desired ordering of dimensions. + + Example: + >>> x = torch.randn(2, 3, 5) + >>> x.size() + torch.Size([2, 3, 5]) + >>> x.permute(2, 0, 1).size() + torch.Size([5, 2, 3]) + """ + + @overload + def permute(self, *dims: _int) -> Tensor: + r""" + permute(*dims) -> Tensor + + Returns a view of the tensor with its dimensions permuted. + + Args: + dims (torch.Size, int..., tuple of int or list of int): the desired ordering of dimensions. + + Example: + >>> x = torch.randn(2, 3, 5) + >>> x.size() + torch.Size([2, 3, 5]) + >>> x.permute(2, 0, 1).size() + torch.Size([5, 2, 3]) + """ + + def pin_memory(self, device: DeviceLikeType | None = None) -> Tensor: + r""" + pin_memory() -> Tensor + + Copies the tensor to pinned memory, if it's not already pinned. + By default, the device pinned memory on will be the current :ref:`accelerator`. + """ + + def pinverse(self, rcond: _float = 1e-15) -> Tensor: + r""" + pinverse() -> Tensor + + See :func:`torch.pinverse` + """ + + def polygamma(self, n: _int) -> Tensor: + r""" + polygamma(n) -> Tensor + + See :func:`torch.polygamma` + """ + + def polygamma_(self, n: _int) -> Tensor: + r""" + polygamma_(n) -> Tensor + + In-place version of :meth:`~Tensor.polygamma` + """ + + def positive(self) -> Tensor: + r""" + positive() -> Tensor + + See :func:`torch.positive` + """ + + @overload + def pow(self, exponent: Tensor) -> Tensor: + r""" + pow(exponent) -> Tensor + + See :func:`torch.pow` + """ + + @overload + def pow(self, exponent: Number | _complex) -> Tensor: + r""" + pow(exponent) -> Tensor + + See :func:`torch.pow` + """ + + @overload + def pow_(self, exponent: Tensor) -> Tensor: + r""" + pow_(exponent) -> Tensor + + In-place version of :meth:`~Tensor.pow` + """ + + @overload + def pow_(self, exponent: Number | _complex) -> Tensor: + r""" + pow_(exponent) -> Tensor + + In-place version of :meth:`~Tensor.pow` + """ + + def prelu(self, weight: Tensor) -> Tensor: ... + @overload + def prod(self, *, dtype: _dtype | None = None) -> Tensor: + r""" + prod(dim=None, keepdim=False, dtype=None) -> Tensor + + See :func:`torch.prod` + """ + + @overload + def prod( + self, + dim: _int, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + ) -> Tensor: + r""" + prod(dim=None, keepdim=False, dtype=None) -> Tensor + + See :func:`torch.prod` + """ + + @overload + def prod( + self, + dim: str | EllipsisType | None, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + ) -> Tensor: + r""" + prod(dim=None, keepdim=False, dtype=None) -> Tensor + + See :func:`torch.prod` + """ + + def put( + self, + index: Tensor, + source: Tensor, + accumulate: _bool = False, + ) -> Tensor: + r""" + put(input, index, source, accumulate=False) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.put_`. + `input` corresponds to `self` in :meth:`torch.Tensor.put_`. + """ + + def put_( + self, + index: Tensor, + source: Tensor, + accumulate: _bool = False, + ) -> Tensor: + r""" + put_(index, source, accumulate=False) -> Tensor + + Copies the elements from :attr:`source` into the positions specified by + :attr:`index`. For the purpose of indexing, the :attr:`self` tensor is treated as if + it were a 1-D tensor. + + :attr:`index` and :attr:`source` need to have the same number of elements, but not necessarily + the same shape. + + If :attr:`accumulate` is ``True``, the elements in :attr:`source` are added to + :attr:`self`. If accumulate is ``False``, the behavior is undefined if :attr:`index` + contain duplicate elements. + + Args: + index (LongTensor): the indices into self + source (Tensor): the tensor containing values to copy from + accumulate (bool, optional): whether to accumulate into self. Default: ``False`` + + Example:: + + >>> src = torch.tensor([[4, 3, 5], + ... [6, 7, 8]]) + >>> src.put_(torch.tensor([1, 3]), torch.tensor([9, 10])) + tensor([[ 4, 9, 5], + [ 10, 7, 8]]) + """ + + def q_per_channel_axis(self) -> _int: + r""" + q_per_channel_axis() -> int + + Given a Tensor quantized by linear (affine) per-channel quantization, + returns the index of dimension on which per-channel quantization is applied. + """ + + def q_per_channel_scales(self) -> Tensor: + r""" + q_per_channel_scales() -> Tensor + + Given a Tensor quantized by linear (affine) per-channel quantization, + returns a Tensor of scales of the underlying quantizer. It has the number of + elements that matches the corresponding dimensions (from q_per_channel_axis) of + the tensor. + """ + + def q_per_channel_zero_points(self) -> Tensor: + r""" + q_per_channel_zero_points() -> Tensor + + Given a Tensor quantized by linear (affine) per-channel quantization, + returns a tensor of zero_points of the underlying quantizer. It has the number of + elements that matches the corresponding dimensions (from q_per_channel_axis) of + the tensor. + """ + + def q_scale(self) -> _float: + r""" + q_scale() -> float + + Given a Tensor quantized by linear(affine) quantization, + returns the scale of the underlying quantizer(). + """ + + def q_zero_point(self) -> _int: + r""" + q_zero_point() -> int + + Given a Tensor quantized by linear(affine) quantization, + returns the zero_point of the underlying quantizer(). + """ + + def qr(self, some: _bool = True) -> torch.return_types.qr: + r""" + qr(some=True) -> (Tensor, Tensor) + + See :func:`torch.qr` + """ + + def qscheme(self) -> _qscheme: + r""" + qscheme() -> torch.qscheme + + Returns the quantization scheme of a given QTensor. + """ + + @overload + def quantile( + self, + q: Tensor, + dim: _int | None = None, + keepdim: _bool = False, + *, + interpolation: str = "linear", + ) -> Tensor: + r""" + quantile(q, dim=None, keepdim=False, *, interpolation='linear') -> Tensor + + See :func:`torch.quantile` + """ + + @overload + def quantile( + self, + q: _float, + dim: _int | None = None, + keepdim: _bool = False, + *, + interpolation: str = "linear", + ) -> Tensor: + r""" + quantile(q, dim=None, keepdim=False, *, interpolation='linear') -> Tensor + + See :func:`torch.quantile` + """ + + def rad2deg(self) -> Tensor: + r""" + rad2deg() -> Tensor + + See :func:`torch.rad2deg` + """ + + def rad2deg_(self) -> Tensor: + r""" + rad2deg_() -> Tensor + + In-place version of :meth:`~Tensor.rad2deg` + """ + + @overload + def random_(self, *, generator: Generator | None = None) -> Tensor: + r""" + random_(from=0, to=None, *, generator=None) -> Tensor + + Fills :attr:`self` tensor with numbers sampled from the discrete uniform + distribution over ``[from, to - 1]``. If not specified, the values are usually + only bounded by :attr:`self` tensor's data type. However, for floating point + types, if unspecified, range will be ``[0, 2^mantissa]`` to ensure that every + value is representable. For example, `torch.tensor(1, dtype=torch.double).random_()` + will be uniform in ``[0, 2^53]``. + """ + + @overload + def random_( + self, + from_: _int, + to: _int | None, + *, + generator: Generator | None = None, + ) -> Tensor: + r""" + random_(from=0, to=None, *, generator=None) -> Tensor + + Fills :attr:`self` tensor with numbers sampled from the discrete uniform + distribution over ``[from, to - 1]``. If not specified, the values are usually + only bounded by :attr:`self` tensor's data type. However, for floating point + types, if unspecified, range will be ``[0, 2^mantissa]`` to ensure that every + value is representable. For example, `torch.tensor(1, dtype=torch.double).random_()` + will be uniform in ``[0, 2^53]``. + """ + + @overload + def random_( + self, + to: _int, + *, + generator: Generator | None = None, + ) -> Tensor: + r""" + random_(from=0, to=None, *, generator=None) -> Tensor + + Fills :attr:`self` tensor with numbers sampled from the discrete uniform + distribution over ``[from, to - 1]``. If not specified, the values are usually + only bounded by :attr:`self` tensor's data type. However, for floating point + types, if unspecified, range will be ``[0, 2^mantissa]`` to ensure that every + value is representable. For example, `torch.tensor(1, dtype=torch.double).random_()` + will be uniform in ``[0, 2^53]``. + """ + + def ravel(self) -> Tensor: + r""" + ravel() -> Tensor + + see :func:`torch.ravel` + """ + + def reciprocal(self) -> Tensor: + r""" + reciprocal() -> Tensor + + See :func:`torch.reciprocal` + """ + + def reciprocal_(self) -> Tensor: + r""" + reciprocal_() -> Tensor + + In-place version of :meth:`~Tensor.reciprocal` + """ + + def record_stream(self, s: Stream) -> None: + r""" + record_stream(stream) + + Marks the tensor as having been used by this stream. When the tensor + is deallocated, ensure the tensor memory is not reused for another tensor + until all work queued on :attr:`stream` at the time of deallocation is + complete. + + .. note:: + + The caching allocator is aware of only the stream where a tensor was + allocated. Due to the awareness, it already correctly manages the life + cycle of tensors on only one stream. But if a tensor is used on a stream + different from the stream of origin, the allocator might reuse the memory + unexpectedly. Calling this method lets the allocator know which streams + have used the tensor. + + .. warning:: + + This method is most suitable for use cases where you are providing a + function that created a tensor on a side stream, and want users to be able + to make use of the tensor without having to think carefully about stream + safety when making use of them. These safety guarantees come at some + performance and predictability cost (analogous to the tradeoff between GC + and manual memory management), so if you are in a situation where + you manage the full lifetime of your tensors, you may consider instead + manually managing CUDA events so that calling this method is not necessary. + In particular, when you call this method, on later allocations the + allocator will poll the recorded stream to see if all operations have + completed yet; you can potentially race with side stream computation and + non-deterministically reuse or fail to reuse memory for an allocation. + + You can safely use tensors allocated on side streams without + :meth:`~Tensor.record_stream`; you must manually ensure that + any non-creation stream uses of a tensor are synced back to the creation + stream before you deallocate the tensor. As the CUDA caching allocator + guarantees that the memory will only be reused with the same creation stream, + this is sufficient to ensure that writes to future reallocations of the + memory will be delayed until non-creation stream uses are done. + (Counterintuitively, you may observe that on the CPU side we have already + reallocated the tensor, even though CUDA kernels on the old tensor are + still in progress. This is fine, because CUDA operations on the new + tensor will appropriately wait for the old operations to complete, as they + are all on the same stream.) + + Concretely, this looks like this:: + + with torch.cuda.stream(s0): + x = torch.zeros(N) + + s1.wait_stream(s0) + with torch.cuda.stream(s1): + y = some_comm_op(x) + + ... some compute on s0 ... + + # synchronize creation stream s0 to side stream s1 + # before deallocating x + s0.wait_stream(s1) + del x + + Note that some discretion is required when deciding when to perform + ``s0.wait_stream(s1)``. In particular, if we were to wait immediately + after ``some_comm_op``, there wouldn't be any point in having the side + stream; it would be equivalent to have run ``some_comm_op`` on ``s0``. + Instead, the synchronization must be placed at some appropriate, later + point in time where you expect the side stream ``s1`` to have finished + work. This location is typically identified via profiling, e.g., using + Chrome traces produced + :meth:`torch.autograd.profiler.profile.export_chrome_trace`. If you + place the wait too early, work on s0 will block until ``s1`` has finished, + preventing further overlapping of communication and computation. If you + place the wait too late, you will use more memory than is strictly + necessary (as you are keeping ``x`` live for longer.) For a concrete + example of how this guidance can be applied in practice, see this post: + `FSDP and CUDACachingAllocator + `_. + """ + + def refine_names( + self, + names: Sequence[str | EllipsisType | None], + ) -> Tensor: ... + def relu(self) -> Tensor: ... + def relu_(self) -> Tensor: ... + @overload + def remainder(self, other: Tensor) -> Tensor: + r""" + remainder(divisor) -> Tensor + + See :func:`torch.remainder` + """ + + @overload + def remainder(self, other: Number | _complex) -> Tensor: + r""" + remainder(divisor) -> Tensor + + See :func:`torch.remainder` + """ + + @overload + def remainder_(self, other: Tensor) -> Tensor: + r""" + remainder_(divisor) -> Tensor + + In-place version of :meth:`~Tensor.remainder` + """ + + @overload + def remainder_(self, other: Number | _complex) -> Tensor: + r""" + remainder_(divisor) -> Tensor + + In-place version of :meth:`~Tensor.remainder` + """ + + def rename( + self, + names: Sequence[str | EllipsisType | None] | None, + ) -> Tensor: ... + def rename_( + self, + names: Sequence[str | EllipsisType | None] | None, + ) -> Tensor: ... + def renorm( + self, + p: Number | _complex, + dim: _int, + maxnorm: Number | _complex, + ) -> Tensor: + r""" + renorm(p, dim, maxnorm) -> Tensor + + See :func:`torch.renorm` + """ + + def renorm_( + self, + p: Number | _complex, + dim: _int, + maxnorm: Number | _complex, + ) -> Tensor: + r""" + renorm_(p, dim, maxnorm) -> Tensor + + In-place version of :meth:`~Tensor.renorm` + """ + + @overload + def repeat(self, repeats: Sequence[_int | SymInt]) -> Tensor: + r""" + repeat(*repeats) -> Tensor + + Repeats this tensor along the specified dimensions. + + Unlike :meth:`~Tensor.expand`, this function copies the tensor's data. + + .. warning:: + + :meth:`~Tensor.repeat` behaves differently from + `numpy.repeat `_, + but is more similar to + `numpy.tile `_. + For the operator similar to `numpy.repeat`, see :func:`torch.repeat_interleave`. + + Args: + repeat (torch.Size, int..., tuple of int or list of int): The number of times to repeat this tensor along each dimension + + Example:: + + >>> x = torch.tensor([1, 2, 3]) + >>> x.repeat(4, 2) + tensor([[ 1, 2, 3, 1, 2, 3], + [ 1, 2, 3, 1, 2, 3], + [ 1, 2, 3, 1, 2, 3], + [ 1, 2, 3, 1, 2, 3]]) + >>> x.repeat(4, 2, 1).size() + torch.Size([4, 2, 3]) + """ + + @overload + def repeat(self, *repeats: _int | SymInt) -> Tensor: + r""" + repeat(*repeats) -> Tensor + + Repeats this tensor along the specified dimensions. + + Unlike :meth:`~Tensor.expand`, this function copies the tensor's data. + + .. warning:: + + :meth:`~Tensor.repeat` behaves differently from + `numpy.repeat `_, + but is more similar to + `numpy.tile `_. + For the operator similar to `numpy.repeat`, see :func:`torch.repeat_interleave`. + + Args: + repeat (torch.Size, int..., tuple of int or list of int): The number of times to repeat this tensor along each dimension + + Example:: + + >>> x = torch.tensor([1, 2, 3]) + >>> x.repeat(4, 2) + tensor([[ 1, 2, 3, 1, 2, 3], + [ 1, 2, 3, 1, 2, 3], + [ 1, 2, 3, 1, 2, 3], + [ 1, 2, 3, 1, 2, 3]]) + >>> x.repeat(4, 2, 1).size() + torch.Size([4, 2, 3]) + """ + + @overload + def repeat_interleave( + self, + repeats: Tensor, + dim: _int | None = None, + *, + output_size: _int | SymInt | None = None, + ) -> Tensor: + r""" + repeat_interleave(repeats, dim=None, *, output_size=None) -> Tensor + + See :func:`torch.repeat_interleave`. + """ + + @overload + def repeat_interleave( + self, + repeats: _int | SymInt, + dim: _int | None = None, + *, + output_size: _int | SymInt | None = None, + ) -> Tensor: + r""" + repeat_interleave(repeats, dim=None, *, output_size=None) -> Tensor + + See :func:`torch.repeat_interleave`. + """ + + def requires_grad_(self, mode: _bool = True) -> Tensor: + r""" + requires_grad_(requires_grad=True) -> Tensor + + Change if autograd should record operations on this tensor: sets this tensor's + :attr:`requires_grad` attribute in-place. Returns this tensor. + + :func:`requires_grad_`'s main use case is to tell autograd to begin recording + operations on a Tensor ``tensor``. If ``tensor`` has ``requires_grad=False`` + (because it was obtained through a DataLoader, or required preprocessing or + initialization), ``tensor.requires_grad_()`` makes it so that autograd will + begin to record operations on ``tensor``. + + Args: + requires_grad (bool): If autograd should record operations on this tensor. + Default: ``True``. + + Example:: + + >>> # Let's say we want to preprocess some saved weights and use + >>> # the result as new weights. + >>> saved_weights = [0.1, 0.2, 0.3, 0.25] + >>> loaded_weights = torch.tensor(saved_weights) + >>> weights = preprocess(loaded_weights) # some function + >>> weights + tensor([-0.5503, 0.4926, -2.1158, -0.8303]) + + >>> # Now, start to record operations done to weights + >>> weights.requires_grad_() + >>> out = weights.pow(2).sum() + >>> out.backward() + >>> weights.grad + tensor([-1.1007, 0.9853, -4.2316, -1.6606]) + """ + + @overload + def reshape(self, shape: Sequence[_int | SymInt]) -> Tensor: + r""" + reshape(*shape) -> Tensor + + Returns a tensor with the same data and number of elements as :attr:`self` + but with the specified shape. This method returns a view if :attr:`shape` is + compatible with the current shape. See :meth:`torch.Tensor.view` on when it is + possible to return a view. + + See :func:`torch.reshape` + + Args: + shape (tuple of ints or int...): the desired shape + """ + + @overload + def reshape(self, *shape: _int | SymInt) -> Tensor: + r""" + reshape(*shape) -> Tensor + + Returns a tensor with the same data and number of elements as :attr:`self` + but with the specified shape. This method returns a view if :attr:`shape` is + compatible with the current shape. See :meth:`torch.Tensor.view` on when it is + possible to return a view. + + See :func:`torch.reshape` + + Args: + shape (tuple of ints or int...): the desired shape + """ + + def reshape_as(self, other: Tensor) -> Tensor: + r""" + reshape_as(other) -> Tensor + + Returns this tensor as the same shape as :attr:`other`. + ``self.reshape_as(other)`` is equivalent to ``self.reshape(other.sizes())``. + This method returns a view if ``other.sizes()`` is compatible with the current + shape. See :meth:`torch.Tensor.view` on when it is possible to return a view. + + Please see :meth:`reshape` for more information about ``reshape``. + + Args: + other (:class:`torch.Tensor`): The result tensor has the same shape + as :attr:`other`. + """ + + @overload + def resize_( + self, + size: Sequence[_int | SymInt], + *, + memory_format: memory_format | None = None, + ) -> Tensor: + r""" + resize_(*sizes, memory_format=torch.contiguous_format) -> Tensor + + Resizes :attr:`self` tensor to the specified size. If the number of elements is + larger than the current storage size, then the underlying storage is resized + to fit the new number of elements. If the number of elements is smaller, the + underlying storage is not changed. Existing elements are preserved but any new + memory is uninitialized. + + .. warning:: + + This is a low-level method. The storage is reinterpreted as C-contiguous, + ignoring the current strides (unless the target size equals the current + size, in which case the tensor is left unchanged). For most purposes, you + will instead want to use :meth:`~Tensor.view()`, which checks for + contiguity, or :meth:`~Tensor.reshape()`, which copies data if needed. To + change the size in-place with custom strides, see :meth:`~Tensor.set_()`. + + .. note:: + + If :func:`torch.use_deterministic_algorithms()` and + :attr:`torch.utils.deterministic.fill_uninitialized_memory` are both set to + ``True``, new elements are initialized to prevent nondeterministic behavior + from using the result as an input to an operation. Floating point and + complex values are set to NaN, and integer values are set to the maximum + value. + + Args: + sizes (torch.Size or int...): the desired size + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + Tensor. Default: ``torch.contiguous_format``. Note that memory format of + :attr:`self` is going to be unaffected if ``self.size()`` matches ``sizes``. + + Example:: + + >>> x = torch.tensor([[1, 2], [3, 4], [5, 6]]) + >>> x.resize_(2, 2) + tensor([[ 1, 2], + [ 3, 4]]) + """ + + @overload + def resize_( + self, + *size: _int | SymInt, + memory_format: memory_format | None = None, + ) -> Tensor: + r""" + resize_(*sizes, memory_format=torch.contiguous_format) -> Tensor + + Resizes :attr:`self` tensor to the specified size. If the number of elements is + larger than the current storage size, then the underlying storage is resized + to fit the new number of elements. If the number of elements is smaller, the + underlying storage is not changed. Existing elements are preserved but any new + memory is uninitialized. + + .. warning:: + + This is a low-level method. The storage is reinterpreted as C-contiguous, + ignoring the current strides (unless the target size equals the current + size, in which case the tensor is left unchanged). For most purposes, you + will instead want to use :meth:`~Tensor.view()`, which checks for + contiguity, or :meth:`~Tensor.reshape()`, which copies data if needed. To + change the size in-place with custom strides, see :meth:`~Tensor.set_()`. + + .. note:: + + If :func:`torch.use_deterministic_algorithms()` and + :attr:`torch.utils.deterministic.fill_uninitialized_memory` are both set to + ``True``, new elements are initialized to prevent nondeterministic behavior + from using the result as an input to an operation. Floating point and + complex values are set to NaN, and integer values are set to the maximum + value. + + Args: + sizes (torch.Size or int...): the desired size + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + Tensor. Default: ``torch.contiguous_format``. Note that memory format of + :attr:`self` is going to be unaffected if ``self.size()`` matches ``sizes``. + + Example:: + + >>> x = torch.tensor([[1, 2], [3, 4], [5, 6]]) + >>> x.resize_(2, 2) + tensor([[ 1, 2], + [ 3, 4]]) + """ + + def resize_as_( + self, + the_template: Tensor, + *, + memory_format: memory_format | None = None, + ) -> Tensor: + r""" + resize_as_(tensor, memory_format=torch.contiguous_format) -> Tensor + + Resizes the :attr:`self` tensor to be the same size as the specified + :attr:`tensor`. This is equivalent to ``self.resize_(tensor.size())``. + + Args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + Tensor. Default: ``torch.contiguous_format``. Note that memory format of + :attr:`self` is going to be unaffected if ``self.size()`` matches ``tensor.size()``. + """ + + def resize_as_sparse_(self, the_template: Tensor) -> Tensor: ... + def resolve_conj(self) -> Tensor: + r""" + resolve_conj() -> Tensor + + See :func:`torch.resolve_conj` + """ + + def resolve_neg(self) -> Tensor: + r""" + resolve_neg() -> Tensor + + See :func:`torch.resolve_neg` + """ + + def retain_grad(self) -> None: + r""" + retain_grad() -> None + + Enables this Tensor to have their :attr:`grad` populated during + :func:`backward`. This is a no-op for leaf tensors. + """ + + def roll( + self, + shifts: _int | SymInt | Sequence[_int | SymInt], + dims: _int | _size = (), + ) -> Tensor: + r""" + roll(shifts, dims) -> Tensor + + See :func:`torch.roll` + """ + + def rot90(self, k: _int = 1, dims: _size = (0, 1)) -> Tensor: + r""" + rot90(k, dims) -> Tensor + + See :func:`torch.rot90` + """ + + @overload + def round(self) -> Tensor: + r""" + round(decimals=0) -> Tensor + + See :func:`torch.round` + """ + + @overload + def round(self, *, decimals: _int) -> Tensor: + r""" + round(decimals=0) -> Tensor + + See :func:`torch.round` + """ + + @overload + def round_(self) -> Tensor: + r""" + round_(decimals=0) -> Tensor + + In-place version of :meth:`~Tensor.round` + """ + + @overload + def round_(self, *, decimals: _int) -> Tensor: + r""" + round_(decimals=0) -> Tensor + + In-place version of :meth:`~Tensor.round` + """ + + def row_indices(self) -> Tensor: ... + def rsqrt(self) -> Tensor: + r""" + rsqrt() -> Tensor + + See :func:`torch.rsqrt` + """ + + def rsqrt_(self) -> Tensor: + r""" + rsqrt_() -> Tensor + + In-place version of :meth:`~Tensor.rsqrt` + """ + + @overload + def scatter(self, dim: _int, index: Tensor, src: Tensor) -> Tensor: + r""" + scatter(dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_` + """ + + @overload + def scatter( + self, + dim: _int, + index: Tensor, + src: Tensor, + *, + reduce: str, + ) -> Tensor: + r""" + scatter(dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_` + """ + + @overload + def scatter( + self, + dim: _int, + index: Tensor, + value: Number | _complex, + *, + reduce: str, + ) -> Tensor: + r""" + scatter(dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_` + """ + + @overload + def scatter( + self, + dim: str | EllipsisType | None, + index: Tensor, + src: Tensor, + ) -> Tensor: + r""" + scatter(dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_` + """ + + @overload + def scatter( + self, + dim: _int, + index: Tensor, + value: Number | _complex, + ) -> Tensor: + r""" + scatter(dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_` + """ + + @overload + def scatter( + self, + dim: str | EllipsisType | None, + index: Tensor, + value: Number | _complex, + ) -> Tensor: + r""" + scatter(dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_` + """ + + @overload + def scatter_(self, dim: _int, index: Tensor, src: Tensor) -> Tensor: + r""" + scatter_(dim, index, src, *, reduce=None) -> Tensor + + Writes all values from the tensor :attr:`src` into :attr:`self` at the indices + specified in the :attr:`index` tensor. For each value in :attr:`src`, its output + index is specified by its index in :attr:`src` for ``dimension != dim`` and by + the corresponding value in :attr:`index` for ``dimension = dim``. + + For a 3-D tensor, :attr:`self` is updated as:: + + self[index[i][j][k]][j][k] = src[i][j][k] # if dim == 0 + self[i][index[i][j][k]][k] = src[i][j][k] # if dim == 1 + self[i][j][index[i][j][k]] = src[i][j][k] # if dim == 2 + + This is the reverse operation of the manner described in :meth:`~Tensor.gather`. + + It is also required that + ``index.size(d) <= src.size(d)`` for all dimensions ``d``, and that + ``index.size(d) <= self.size(d)`` for all dimensions ``d != dim``. + Note that ``input`` and ``index`` do not broadcast against each other for NPUs, + so when running on NPUs, :attr:`input` and :attr:`index` must have the same number of dimensions. + Standard broadcasting occurs in all other cases. + + Moreover, as for :meth:`~Tensor.gather`, the values of :attr:`index` must be + between ``0`` and ``self.size(dim) - 1`` inclusive. + + .. warning:: + + When indices are not unique, the behavior is non-deterministic (one of the + values from ``src`` will be picked arbitrarily) and the gradient will be + incorrect (it will be propagated to all locations in the source that + correspond to the same index)! + + .. note:: + + The backward pass is implemented only for ``src.shape == index.shape``. + + Additionally accepts an optional :attr:`reduce` argument that allows + specification of an optional reduction operation, which is applied to all + values in the tensor :attr:`src` into :attr:`self` at the indices + specified in the :attr:`index`. For each value in :attr:`src`, the reduction + operation is applied to an index in :attr:`self` which is specified by + its index in :attr:`src` for ``dimension != dim`` and by the corresponding + value in :attr:`index` for ``dimension = dim``. + + Given a 3-D tensor and reduction using the multiplication operation, :attr:`self` + is updated as:: + + self[index[i][j][k]][j][k] *= src[i][j][k] # if dim == 0 + self[i][index[i][j][k]][k] *= src[i][j][k] # if dim == 1 + self[i][j][index[i][j][k]] *= src[i][j][k] # if dim == 2 + + Reducing with the addition operation is the same as using + :meth:`~torch.Tensor.scatter_add_`. + + .. warning:: + The reduce argument with Tensor ``src`` is deprecated and will be removed in + a future PyTorch release. Please use :meth:`~torch.Tensor.scatter_reduce_` + instead for more reduction options. + + Args: + dim (int): the axis along which to index + index (LongTensor): the indices of elements to scatter, can be either empty + or of the same dimensionality as ``src``. When empty, the operation + returns ``self`` unchanged. + src (Tensor): the source element(s) to scatter. + + Keyword args: + reduce (str, optional): reduction operation to apply, can be either + ``'add'`` or ``'multiply'``. + + Example:: + + >>> src = torch.arange(1, 11).reshape((2, 5)) + >>> src + tensor([[ 1, 2, 3, 4, 5], + [ 6, 7, 8, 9, 10]]) + >>> index = torch.tensor([[0, 1, 2, 0]]) + >>> torch.zeros(3, 5, dtype=src.dtype).scatter_(0, index, src) + tensor([[1, 0, 0, 4, 0], + [0, 2, 0, 0, 0], + [0, 0, 3, 0, 0]]) + >>> index = torch.tensor([[0, 1, 2], [0, 1, 4]]) + >>> torch.zeros(3, 5, dtype=src.dtype).scatter_(1, index, src) + tensor([[1, 2, 3, 0, 0], + [6, 7, 0, 0, 8], + [0, 0, 0, 0, 0]]) + + >>> torch.full((2, 4), 2.).scatter_(1, torch.tensor([[2], [3]]), + ... 1.23, reduce='multiply') + tensor([[2.0000, 2.0000, 2.4600, 2.0000], + [2.0000, 2.0000, 2.0000, 2.4600]]) + >>> torch.full((2, 4), 2.).scatter_(1, torch.tensor([[2], [3]]), + ... 1.23, reduce='add') + tensor([[2.0000, 2.0000, 3.2300, 2.0000], + [2.0000, 2.0000, 2.0000, 3.2300]]) + + .. function:: scatter_(dim, index, value, *, reduce=None) -> Tensor: + :noindex: + + Writes the value from :attr:`value` into :attr:`self` at the indices + specified in the :attr:`index` tensor. This operation is equivalent to the previous version, + with the :attr:`src` tensor filled entirely with :attr:`value`. + + Args: + dim (int): the axis along which to index + index (LongTensor): the indices of elements to scatter, can be either empty + or of the same dimensionality as ``src``. When empty, the operation + returns ``self`` unchanged. + value (Scalar): the value to scatter. + + Keyword args: + reduce (str, optional): reduction operation to apply, can be either + ``'add'`` or ``'multiply'``. + + Example:: + + >>> index = torch.tensor([[0, 1]]) + >>> value = 2 + >>> torch.zeros(3, 5).scatter_(0, index, value) + tensor([[2., 0., 0., 0., 0.], + [0., 2., 0., 0., 0.], + [0., 0., 0., 0., 0.]]) + """ + + @overload + def scatter_( + self, + dim: _int, + index: Tensor, + src: Tensor, + *, + reduce: str, + ) -> Tensor: + r""" + scatter_(dim, index, src, *, reduce=None) -> Tensor + + Writes all values from the tensor :attr:`src` into :attr:`self` at the indices + specified in the :attr:`index` tensor. For each value in :attr:`src`, its output + index is specified by its index in :attr:`src` for ``dimension != dim`` and by + the corresponding value in :attr:`index` for ``dimension = dim``. + + For a 3-D tensor, :attr:`self` is updated as:: + + self[index[i][j][k]][j][k] = src[i][j][k] # if dim == 0 + self[i][index[i][j][k]][k] = src[i][j][k] # if dim == 1 + self[i][j][index[i][j][k]] = src[i][j][k] # if dim == 2 + + This is the reverse operation of the manner described in :meth:`~Tensor.gather`. + + It is also required that + ``index.size(d) <= src.size(d)`` for all dimensions ``d``, and that + ``index.size(d) <= self.size(d)`` for all dimensions ``d != dim``. + Note that ``input`` and ``index`` do not broadcast against each other for NPUs, + so when running on NPUs, :attr:`input` and :attr:`index` must have the same number of dimensions. + Standard broadcasting occurs in all other cases. + + Moreover, as for :meth:`~Tensor.gather`, the values of :attr:`index` must be + between ``0`` and ``self.size(dim) - 1`` inclusive. + + .. warning:: + + When indices are not unique, the behavior is non-deterministic (one of the + values from ``src`` will be picked arbitrarily) and the gradient will be + incorrect (it will be propagated to all locations in the source that + correspond to the same index)! + + .. note:: + + The backward pass is implemented only for ``src.shape == index.shape``. + + Additionally accepts an optional :attr:`reduce` argument that allows + specification of an optional reduction operation, which is applied to all + values in the tensor :attr:`src` into :attr:`self` at the indices + specified in the :attr:`index`. For each value in :attr:`src`, the reduction + operation is applied to an index in :attr:`self` which is specified by + its index in :attr:`src` for ``dimension != dim`` and by the corresponding + value in :attr:`index` for ``dimension = dim``. + + Given a 3-D tensor and reduction using the multiplication operation, :attr:`self` + is updated as:: + + self[index[i][j][k]][j][k] *= src[i][j][k] # if dim == 0 + self[i][index[i][j][k]][k] *= src[i][j][k] # if dim == 1 + self[i][j][index[i][j][k]] *= src[i][j][k] # if dim == 2 + + Reducing with the addition operation is the same as using + :meth:`~torch.Tensor.scatter_add_`. + + .. warning:: + The reduce argument with Tensor ``src`` is deprecated and will be removed in + a future PyTorch release. Please use :meth:`~torch.Tensor.scatter_reduce_` + instead for more reduction options. + + Args: + dim (int): the axis along which to index + index (LongTensor): the indices of elements to scatter, can be either empty + or of the same dimensionality as ``src``. When empty, the operation + returns ``self`` unchanged. + src (Tensor): the source element(s) to scatter. + + Keyword args: + reduce (str, optional): reduction operation to apply, can be either + ``'add'`` or ``'multiply'``. + + Example:: + + >>> src = torch.arange(1, 11).reshape((2, 5)) + >>> src + tensor([[ 1, 2, 3, 4, 5], + [ 6, 7, 8, 9, 10]]) + >>> index = torch.tensor([[0, 1, 2, 0]]) + >>> torch.zeros(3, 5, dtype=src.dtype).scatter_(0, index, src) + tensor([[1, 0, 0, 4, 0], + [0, 2, 0, 0, 0], + [0, 0, 3, 0, 0]]) + >>> index = torch.tensor([[0, 1, 2], [0, 1, 4]]) + >>> torch.zeros(3, 5, dtype=src.dtype).scatter_(1, index, src) + tensor([[1, 2, 3, 0, 0], + [6, 7, 0, 0, 8], + [0, 0, 0, 0, 0]]) + + >>> torch.full((2, 4), 2.).scatter_(1, torch.tensor([[2], [3]]), + ... 1.23, reduce='multiply') + tensor([[2.0000, 2.0000, 2.4600, 2.0000], + [2.0000, 2.0000, 2.0000, 2.4600]]) + >>> torch.full((2, 4), 2.).scatter_(1, torch.tensor([[2], [3]]), + ... 1.23, reduce='add') + tensor([[2.0000, 2.0000, 3.2300, 2.0000], + [2.0000, 2.0000, 2.0000, 3.2300]]) + + .. function:: scatter_(dim, index, value, *, reduce=None) -> Tensor: + :noindex: + + Writes the value from :attr:`value` into :attr:`self` at the indices + specified in the :attr:`index` tensor. This operation is equivalent to the previous version, + with the :attr:`src` tensor filled entirely with :attr:`value`. + + Args: + dim (int): the axis along which to index + index (LongTensor): the indices of elements to scatter, can be either empty + or of the same dimensionality as ``src``. When empty, the operation + returns ``self`` unchanged. + value (Scalar): the value to scatter. + + Keyword args: + reduce (str, optional): reduction operation to apply, can be either + ``'add'`` or ``'multiply'``. + + Example:: + + >>> index = torch.tensor([[0, 1]]) + >>> value = 2 + >>> torch.zeros(3, 5).scatter_(0, index, value) + tensor([[2., 0., 0., 0., 0.], + [0., 2., 0., 0., 0.], + [0., 0., 0., 0., 0.]]) + """ + + @overload + def scatter_( + self, + dim: _int, + index: Tensor, + value: Number | _complex, + *, + reduce: str, + ) -> Tensor: + r""" + scatter_(dim, index, src, *, reduce=None) -> Tensor + + Writes all values from the tensor :attr:`src` into :attr:`self` at the indices + specified in the :attr:`index` tensor. For each value in :attr:`src`, its output + index is specified by its index in :attr:`src` for ``dimension != dim`` and by + the corresponding value in :attr:`index` for ``dimension = dim``. + + For a 3-D tensor, :attr:`self` is updated as:: + + self[index[i][j][k]][j][k] = src[i][j][k] # if dim == 0 + self[i][index[i][j][k]][k] = src[i][j][k] # if dim == 1 + self[i][j][index[i][j][k]] = src[i][j][k] # if dim == 2 + + This is the reverse operation of the manner described in :meth:`~Tensor.gather`. + + It is also required that + ``index.size(d) <= src.size(d)`` for all dimensions ``d``, and that + ``index.size(d) <= self.size(d)`` for all dimensions ``d != dim``. + Note that ``input`` and ``index`` do not broadcast against each other for NPUs, + so when running on NPUs, :attr:`input` and :attr:`index` must have the same number of dimensions. + Standard broadcasting occurs in all other cases. + + Moreover, as for :meth:`~Tensor.gather`, the values of :attr:`index` must be + between ``0`` and ``self.size(dim) - 1`` inclusive. + + .. warning:: + + When indices are not unique, the behavior is non-deterministic (one of the + values from ``src`` will be picked arbitrarily) and the gradient will be + incorrect (it will be propagated to all locations in the source that + correspond to the same index)! + + .. note:: + + The backward pass is implemented only for ``src.shape == index.shape``. + + Additionally accepts an optional :attr:`reduce` argument that allows + specification of an optional reduction operation, which is applied to all + values in the tensor :attr:`src` into :attr:`self` at the indices + specified in the :attr:`index`. For each value in :attr:`src`, the reduction + operation is applied to an index in :attr:`self` which is specified by + its index in :attr:`src` for ``dimension != dim`` and by the corresponding + value in :attr:`index` for ``dimension = dim``. + + Given a 3-D tensor and reduction using the multiplication operation, :attr:`self` + is updated as:: + + self[index[i][j][k]][j][k] *= src[i][j][k] # if dim == 0 + self[i][index[i][j][k]][k] *= src[i][j][k] # if dim == 1 + self[i][j][index[i][j][k]] *= src[i][j][k] # if dim == 2 + + Reducing with the addition operation is the same as using + :meth:`~torch.Tensor.scatter_add_`. + + .. warning:: + The reduce argument with Tensor ``src`` is deprecated and will be removed in + a future PyTorch release. Please use :meth:`~torch.Tensor.scatter_reduce_` + instead for more reduction options. + + Args: + dim (int): the axis along which to index + index (LongTensor): the indices of elements to scatter, can be either empty + or of the same dimensionality as ``src``. When empty, the operation + returns ``self`` unchanged. + src (Tensor): the source element(s) to scatter. + + Keyword args: + reduce (str, optional): reduction operation to apply, can be either + ``'add'`` or ``'multiply'``. + + Example:: + + >>> src = torch.arange(1, 11).reshape((2, 5)) + >>> src + tensor([[ 1, 2, 3, 4, 5], + [ 6, 7, 8, 9, 10]]) + >>> index = torch.tensor([[0, 1, 2, 0]]) + >>> torch.zeros(3, 5, dtype=src.dtype).scatter_(0, index, src) + tensor([[1, 0, 0, 4, 0], + [0, 2, 0, 0, 0], + [0, 0, 3, 0, 0]]) + >>> index = torch.tensor([[0, 1, 2], [0, 1, 4]]) + >>> torch.zeros(3, 5, dtype=src.dtype).scatter_(1, index, src) + tensor([[1, 2, 3, 0, 0], + [6, 7, 0, 0, 8], + [0, 0, 0, 0, 0]]) + + >>> torch.full((2, 4), 2.).scatter_(1, torch.tensor([[2], [3]]), + ... 1.23, reduce='multiply') + tensor([[2.0000, 2.0000, 2.4600, 2.0000], + [2.0000, 2.0000, 2.0000, 2.4600]]) + >>> torch.full((2, 4), 2.).scatter_(1, torch.tensor([[2], [3]]), + ... 1.23, reduce='add') + tensor([[2.0000, 2.0000, 3.2300, 2.0000], + [2.0000, 2.0000, 2.0000, 3.2300]]) + + .. function:: scatter_(dim, index, value, *, reduce=None) -> Tensor: + :noindex: + + Writes the value from :attr:`value` into :attr:`self` at the indices + specified in the :attr:`index` tensor. This operation is equivalent to the previous version, + with the :attr:`src` tensor filled entirely with :attr:`value`. + + Args: + dim (int): the axis along which to index + index (LongTensor): the indices of elements to scatter, can be either empty + or of the same dimensionality as ``src``. When empty, the operation + returns ``self`` unchanged. + value (Scalar): the value to scatter. + + Keyword args: + reduce (str, optional): reduction operation to apply, can be either + ``'add'`` or ``'multiply'``. + + Example:: + + >>> index = torch.tensor([[0, 1]]) + >>> value = 2 + >>> torch.zeros(3, 5).scatter_(0, index, value) + tensor([[2., 0., 0., 0., 0.], + [0., 2., 0., 0., 0.], + [0., 0., 0., 0., 0.]]) + """ + + @overload + def scatter_( + self, + dim: _int, + index: Tensor, + value: Number | _complex, + ) -> Tensor: + r""" + scatter_(dim, index, src, *, reduce=None) -> Tensor + + Writes all values from the tensor :attr:`src` into :attr:`self` at the indices + specified in the :attr:`index` tensor. For each value in :attr:`src`, its output + index is specified by its index in :attr:`src` for ``dimension != dim`` and by + the corresponding value in :attr:`index` for ``dimension = dim``. + + For a 3-D tensor, :attr:`self` is updated as:: + + self[index[i][j][k]][j][k] = src[i][j][k] # if dim == 0 + self[i][index[i][j][k]][k] = src[i][j][k] # if dim == 1 + self[i][j][index[i][j][k]] = src[i][j][k] # if dim == 2 + + This is the reverse operation of the manner described in :meth:`~Tensor.gather`. + + It is also required that + ``index.size(d) <= src.size(d)`` for all dimensions ``d``, and that + ``index.size(d) <= self.size(d)`` for all dimensions ``d != dim``. + Note that ``input`` and ``index`` do not broadcast against each other for NPUs, + so when running on NPUs, :attr:`input` and :attr:`index` must have the same number of dimensions. + Standard broadcasting occurs in all other cases. + + Moreover, as for :meth:`~Tensor.gather`, the values of :attr:`index` must be + between ``0`` and ``self.size(dim) - 1`` inclusive. + + .. warning:: + + When indices are not unique, the behavior is non-deterministic (one of the + values from ``src`` will be picked arbitrarily) and the gradient will be + incorrect (it will be propagated to all locations in the source that + correspond to the same index)! + + .. note:: + + The backward pass is implemented only for ``src.shape == index.shape``. + + Additionally accepts an optional :attr:`reduce` argument that allows + specification of an optional reduction operation, which is applied to all + values in the tensor :attr:`src` into :attr:`self` at the indices + specified in the :attr:`index`. For each value in :attr:`src`, the reduction + operation is applied to an index in :attr:`self` which is specified by + its index in :attr:`src` for ``dimension != dim`` and by the corresponding + value in :attr:`index` for ``dimension = dim``. + + Given a 3-D tensor and reduction using the multiplication operation, :attr:`self` + is updated as:: + + self[index[i][j][k]][j][k] *= src[i][j][k] # if dim == 0 + self[i][index[i][j][k]][k] *= src[i][j][k] # if dim == 1 + self[i][j][index[i][j][k]] *= src[i][j][k] # if dim == 2 + + Reducing with the addition operation is the same as using + :meth:`~torch.Tensor.scatter_add_`. + + .. warning:: + The reduce argument with Tensor ``src`` is deprecated and will be removed in + a future PyTorch release. Please use :meth:`~torch.Tensor.scatter_reduce_` + instead for more reduction options. + + Args: + dim (int): the axis along which to index + index (LongTensor): the indices of elements to scatter, can be either empty + or of the same dimensionality as ``src``. When empty, the operation + returns ``self`` unchanged. + src (Tensor): the source element(s) to scatter. + + Keyword args: + reduce (str, optional): reduction operation to apply, can be either + ``'add'`` or ``'multiply'``. + + Example:: + + >>> src = torch.arange(1, 11).reshape((2, 5)) + >>> src + tensor([[ 1, 2, 3, 4, 5], + [ 6, 7, 8, 9, 10]]) + >>> index = torch.tensor([[0, 1, 2, 0]]) + >>> torch.zeros(3, 5, dtype=src.dtype).scatter_(0, index, src) + tensor([[1, 0, 0, 4, 0], + [0, 2, 0, 0, 0], + [0, 0, 3, 0, 0]]) + >>> index = torch.tensor([[0, 1, 2], [0, 1, 4]]) + >>> torch.zeros(3, 5, dtype=src.dtype).scatter_(1, index, src) + tensor([[1, 2, 3, 0, 0], + [6, 7, 0, 0, 8], + [0, 0, 0, 0, 0]]) + + >>> torch.full((2, 4), 2.).scatter_(1, torch.tensor([[2], [3]]), + ... 1.23, reduce='multiply') + tensor([[2.0000, 2.0000, 2.4600, 2.0000], + [2.0000, 2.0000, 2.0000, 2.4600]]) + >>> torch.full((2, 4), 2.).scatter_(1, torch.tensor([[2], [3]]), + ... 1.23, reduce='add') + tensor([[2.0000, 2.0000, 3.2300, 2.0000], + [2.0000, 2.0000, 2.0000, 3.2300]]) + + .. function:: scatter_(dim, index, value, *, reduce=None) -> Tensor: + :noindex: + + Writes the value from :attr:`value` into :attr:`self` at the indices + specified in the :attr:`index` tensor. This operation is equivalent to the previous version, + with the :attr:`src` tensor filled entirely with :attr:`value`. + + Args: + dim (int): the axis along which to index + index (LongTensor): the indices of elements to scatter, can be either empty + or of the same dimensionality as ``src``. When empty, the operation + returns ``self`` unchanged. + value (Scalar): the value to scatter. + + Keyword args: + reduce (str, optional): reduction operation to apply, can be either + ``'add'`` or ``'multiply'``. + + Example:: + + >>> index = torch.tensor([[0, 1]]) + >>> value = 2 + >>> torch.zeros(3, 5).scatter_(0, index, value) + tensor([[2., 0., 0., 0., 0.], + [0., 2., 0., 0., 0.], + [0., 0., 0., 0., 0.]]) + """ + + @overload + def scatter_add(self, dim: _int, index: Tensor, src: Tensor) -> Tensor: + r""" + scatter_add(dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_add_` + """ + + @overload + def scatter_add( + self, + dim: str | EllipsisType | None, + index: Tensor, + src: Tensor, + ) -> Tensor: + r""" + scatter_add(dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_add_` + """ + + def scatter_add_(self, dim: _int, index: Tensor, src: Tensor) -> Tensor: + r""" + scatter_add_(dim, index, src) -> Tensor + + Adds all values from the tensor :attr:`src` into :attr:`self` at the indices + specified in the :attr:`index` tensor in a similar fashion as + :meth:`~torch.Tensor.scatter_`. For each value in :attr:`src`, it is added to + an index in :attr:`self` which is specified by its index in :attr:`src` + for ``dimension != dim`` and by the corresponding value in :attr:`index` for + ``dimension = dim``. + + For a 3-D tensor, :attr:`self` is updated as:: + + self[index[i][j][k]][j][k] += src[i][j][k] # if dim == 0 + self[i][index[i][j][k]][k] += src[i][j][k] # if dim == 1 + self[i][j][index[i][j][k]] += src[i][j][k] # if dim == 2 + + :attr:`self`, :attr:`index` and :attr:`src` should have same number of + dimensions. It is also required that ``index.size(d) <= src.size(d)`` for all + dimensions ``d``, and that ``index.size(d) <= self.size(d)`` for all dimensions + ``d != dim``. Note that ``index`` and ``src`` do not broadcast. + When :attr:`index` is empty, we always return the original tensor + without further error checking. + + Note: + This operation may behave nondeterministically when given tensors on a CUDA device. See :doc:`/notes/randomness` for more information. + + .. note:: + + The backward pass is implemented only for ``src.shape == index.shape``. + + Args: + dim (int): the axis along which to index + index (LongTensor): the indices of elements to scatter and add, can be + either empty or of the same dimensionality as ``src``. When empty, the + operation returns ``self`` unchanged. + src (Tensor): the source elements to scatter and add + + Example:: + + >>> src = torch.ones((2, 5)) + >>> index = torch.tensor([[0, 1, 2, 0, 0]]) + >>> torch.zeros(3, 5, dtype=src.dtype).scatter_add_(0, index, src) + tensor([[1., 0., 0., 1., 1.], + [0., 1., 0., 0., 0.], + [0., 0., 1., 0., 0.]]) + >>> index = torch.tensor([[0, 1, 2, 0, 0], [0, 1, 2, 2, 2]]) + >>> torch.zeros(3, 5, dtype=src.dtype).scatter_add_(0, index, src) + tensor([[2., 0., 0., 1., 1.], + [0., 2., 0., 0., 0.], + [0., 0., 2., 1., 1.]]) + """ + + def scatter_reduce( + self, + dim: _int, + index: Tensor, + src: Tensor, + reduce: str, + *, + include_self: _bool = True, + ) -> Tensor: + r""" + scatter_reduce(dim, index, src, reduce, *, include_self=True) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_reduce_` + """ + + def scatter_reduce_( + self, + dim: _int, + index: Tensor, + src: Tensor, + reduce: str, + *, + include_self: _bool = True, + ) -> Tensor: + r""" + scatter_reduce_(dim, index, src, reduce, *, include_self=True) -> Tensor + + Reduces all values from the :attr:`src` tensor to the indices specified in + the :attr:`index` tensor in the :attr:`self` tensor using the applied reduction + defined via the :attr:`reduce` argument (:obj:`"sum"`, :obj:`"prod"`, :obj:`"mean"`, + :obj:`"amax"`, :obj:`"amin"`). For each value in :attr:`src`, it is reduced to an + index in :attr:`self` which is specified by its index in :attr:`src` for + ``dimension != dim`` and by the corresponding value in :attr:`index` for + ``dimension = dim``. If :obj:`include_self="True"`, the values in the :attr:`self` + tensor are included in the reduction. + + :attr:`self`, :attr:`index` and :attr:`src` should all have + the same number of dimensions. It is also required that + ``index.size(d) <= src.size(d)`` for all dimensions ``d``, and that + ``index.size(d) <= self.size(d)`` for all dimensions ``d != dim``. + Note that ``index`` and ``src`` do not broadcast. + + For a 3-D tensor with :obj:`reduce="sum"` and :obj:`include_self=True` the + output is given as:: + + self[index[i][j][k]][j][k] += src[i][j][k] # if dim == 0 + self[i][index[i][j][k]][k] += src[i][j][k] # if dim == 1 + self[i][j][index[i][j][k]] += src[i][j][k] # if dim == 2 + + Note: + This operation may behave nondeterministically when given tensors on a CUDA device. See :doc:`/notes/randomness` for more information. + + .. note:: + + The backward pass is implemented only for ``src.shape == index.shape``. + + .. warning:: + + This function is in beta and may change in the near future. + + Args: + dim (int): the axis along which to index + index (LongTensor): the indices of elements to scatter and reduce. + src (Tensor): the source elements to scatter and reduce + reduce (str): the reduction operation to apply for non-unique indices + (:obj:`"sum"`, :obj:`"prod"`, :obj:`"mean"`, :obj:`"amax"`, :obj:`"amin"`) + include_self (bool): whether elements from the :attr:`self` tensor are + included in the reduction + + Example:: + + >>> src = torch.tensor([1., 2., 3., 4., 5., 6.]) + >>> index = torch.tensor([0, 1, 0, 1, 2, 1]) + >>> input = torch.tensor([1., 2., 3., 4.]) + >>> input.scatter_reduce(0, index, src, reduce="sum") + tensor([5., 14., 8., 4.]) + >>> input.scatter_reduce(0, index, src, reduce="sum", include_self=False) + tensor([4., 12., 5., 4.]) + >>> input2 = torch.tensor([5., 4., 3., 2.]) + >>> input2.scatter_reduce(0, index, src, reduce="amax") + tensor([5., 6., 5., 2.]) + >>> input2.scatter_reduce(0, index, src, reduce="amax", include_self=False) + tensor([3., 6., 5., 2.]) + """ + + @overload + def select(self, dim: _int, index: _int | SymInt) -> Tensor: + r""" + select(dim, index) -> Tensor + + See :func:`torch.select` + """ + + @overload + def select(self, dim: str | EllipsisType | None, index: _int) -> Tensor: + r""" + select(dim, index) -> Tensor + + See :func:`torch.select` + """ + + def select_scatter( + self, + src: Tensor, + dim: _int, + index: _int | SymInt, + ) -> Tensor: + r""" + select_scatter(src, dim, index) -> Tensor + + See :func:`torch.select_scatter` + """ + + @overload + def set_( + self, + source: Storage | TypedStorage | UntypedStorage, + storage_offset: IntLikeType, + size: _symsize, + stride: _symsize, + ) -> Tensor: + r""" + set_(source=None, storage_offset=0, size=None, stride=None) -> Tensor + + Sets the underlying storage, size, and strides. If :attr:`source` is a tensor, + :attr:`self` tensor will share the same storage and have the same size and + strides as :attr:`source`. Changes to elements in one tensor will be reflected + in the other. + + If :attr:`source` is a :class:`~torch.Storage`, the method sets the underlying + storage, offset, size, and stride. + + Args: + source (Tensor or Storage): the tensor or storage to use + storage_offset (int, optional): the offset in the storage + size (torch.Size, optional): the desired size. Defaults to the size of the source. + stride (tuple, optional): the desired stride. Defaults to C-contiguous strides. + """ + + @overload + def set_(self, source: Storage | TypedStorage | UntypedStorage) -> Tensor: + r""" + set_(source=None, storage_offset=0, size=None, stride=None) -> Tensor + + Sets the underlying storage, size, and strides. If :attr:`source` is a tensor, + :attr:`self` tensor will share the same storage and have the same size and + strides as :attr:`source`. Changes to elements in one tensor will be reflected + in the other. + + If :attr:`source` is a :class:`~torch.Storage`, the method sets the underlying + storage, offset, size, and stride. + + Args: + source (Tensor or Storage): the tensor or storage to use + storage_offset (int, optional): the offset in the storage + size (torch.Size, optional): the desired size. Defaults to the size of the source. + stride (tuple, optional): the desired stride. Defaults to C-contiguous strides. + """ + + def sgn(self) -> Tensor: + r""" + sgn() -> Tensor + + See :func:`torch.sgn` + """ + + def sgn_(self) -> Tensor: + r""" + sgn_() -> Tensor + + In-place version of :meth:`~Tensor.sgn` + """ + + def short(self) -> Tensor: + r""" + short(memory_format=torch.preserve_format) -> Tensor + + ``self.short()`` is equivalent to ``self.to(torch.int16)``. See :func:`to`. + + Args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + + def sigmoid(self) -> Tensor: + r""" + sigmoid() -> Tensor + + See :func:`torch.sigmoid` + """ + + def sigmoid_(self) -> Tensor: + r""" + sigmoid_() -> Tensor + + In-place version of :meth:`~Tensor.sigmoid` + """ + + def sign(self) -> Tensor: + r""" + sign() -> Tensor + + See :func:`torch.sign` + """ + + def sign_(self) -> Tensor: + r""" + sign_() -> Tensor + + In-place version of :meth:`~Tensor.sign` + """ + + def signbit(self) -> Tensor: + r""" + signbit() -> Tensor + + See :func:`torch.signbit` + """ + + def sin(self) -> Tensor: + r""" + sin() -> Tensor + + See :func:`torch.sin` + """ + + def sin_(self) -> Tensor: + r""" + sin_() -> Tensor + + In-place version of :meth:`~Tensor.sin` + """ + + def sinc(self) -> Tensor: + r""" + sinc() -> Tensor + + See :func:`torch.sinc` + """ + + def sinc_(self) -> Tensor: + r""" + sinc_() -> Tensor + + In-place version of :meth:`~Tensor.sinc` + """ + + def sinh(self) -> Tensor: + r""" + sinh() -> Tensor + + See :func:`torch.sinh` + """ + + def sinh_(self) -> Tensor: + r""" + sinh_() -> Tensor + + In-place version of :meth:`~Tensor.sinh` + """ + + @overload + def size(self, dim: None = None) -> Size: + r""" + size(dim=None) -> torch.Size or int + + Returns the size of the :attr:`self` tensor. If ``dim`` is not specified, + the returned value is a :class:`torch.Size`, a subclass of :class:`tuple`. + If ``dim`` is specified, returns an int holding the size of that dimension. + + Args: + dim (int, optional): The dimension for which to retrieve the size. + + Example:: + + >>> t = torch.empty(3, 4, 5) + >>> t.size() + torch.Size([3, 4, 5]) + >>> t.size(dim=1) + 4 + """ + + @overload + def size(self, dim: _int) -> _int: + r""" + size(dim=None) -> torch.Size or int + + Returns the size of the :attr:`self` tensor. If ``dim`` is not specified, + the returned value is a :class:`torch.Size`, a subclass of :class:`tuple`. + If ``dim`` is specified, returns an int holding the size of that dimension. + + Args: + dim (int, optional): The dimension for which to retrieve the size. + + Example:: + + >>> t = torch.empty(3, 4, 5) + >>> t.size() + torch.Size([3, 4, 5]) + >>> t.size(dim=1) + 4 + """ + + def slice_inverse( + self, + src: Tensor, + dim: _int = 0, + start: _int | SymInt | None = None, + end: _int | SymInt | None = None, + step: _int | SymInt = 1, + ) -> Tensor: ... + def slice_scatter( + self, + src: Tensor, + dim: _int = 0, + start: _int | SymInt | None = None, + end: _int | SymInt | None = None, + step: _int | SymInt = 1, + ) -> Tensor: + r""" + slice_scatter(src, dim=0, start=None, end=None, step=1) -> Tensor + + See :func:`torch.slice_scatter` + """ + + def slogdet(self) -> torch.return_types.slogdet: + r""" + slogdet() -> (Tensor, Tensor) + + See :func:`torch.slogdet` + """ + + def smm(self, mat2: Tensor) -> Tensor: + r""" + smm(mat) -> Tensor + + See :func:`torch.smm` + """ + + @overload + def softmax(self, dim: _int, dtype: _dtype | None = None) -> Tensor: + r""" + softmax(dim) -> Tensor + + Alias for :func:`torch.nn.functional.softmax`. + """ + + @overload + def softmax( + self, + dim: str | EllipsisType | None, + *, + dtype: _dtype | None = None, + ) -> Tensor: + r""" + softmax(dim) -> Tensor + + Alias for :func:`torch.nn.functional.softmax`. + """ + + @overload + def sort( + self, + *, + stable: _bool | None, + dim: _int = -1, + descending: _bool = False, + ) -> torch.return_types.sort: + r""" + sort(dim=-1, descending=False) -> (Tensor, LongTensor) + + See :func:`torch.sort` + """ + + @overload + def sort( + self, + dim: _int = -1, + descending: _bool = False, + ) -> torch.return_types.sort: + r""" + sort(dim=-1, descending=False) -> (Tensor, LongTensor) + + See :func:`torch.sort` + """ + + @overload + def sort( + self, + *, + stable: _bool | None, + dim: str | EllipsisType | None, + descending: _bool = False, + ) -> torch.return_types.sort: + r""" + sort(dim=-1, descending=False) -> (Tensor, LongTensor) + + See :func:`torch.sort` + """ + + @overload + def sort( + self, + dim: str | EllipsisType | None, + descending: _bool = False, + ) -> torch.return_types.sort: + r""" + sort(dim=-1, descending=False) -> (Tensor, LongTensor) + + See :func:`torch.sort` + """ + + def sparse_dim(self) -> _int: + r""" + sparse_dim() -> int + + Return the number of sparse dimensions in a :ref:`sparse tensor ` :attr:`self`. + + .. note:: + Returns ``0`` if :attr:`self` is not a sparse tensor. + + See also :meth:`Tensor.dense_dim` and :ref:`hybrid tensors `. + """ + + def sparse_mask(self, mask: Tensor) -> Tensor: + r""" + sparse_mask(mask) -> Tensor + + Returns a new :ref:`sparse tensor ` with values from a + strided tensor :attr:`self` filtered by the indices of the sparse + tensor :attr:`mask`. The values of :attr:`mask` sparse tensor are + ignored. :attr:`self` and :attr:`mask` tensors must have the same + shape. + + .. note:: + + The returned sparse tensor might contain duplicate values if :attr:`mask` + is not coalesced. It is therefore advisable to pass ``mask.coalesce()`` + if such behavior is not desired. + + .. note:: + + The returned sparse tensor has the same indices as the sparse tensor + :attr:`mask`, even when the corresponding values in :attr:`self` are + zeros. + + Args: + mask (Tensor): a sparse tensor whose indices are used as a filter + + Example:: + + >>> nse = 5 + >>> dims = (5, 5, 2, 2) + >>> I = torch.cat([torch.randint(0, dims[0], size=(nse,)), + ... torch.randint(0, dims[1], size=(nse,))], 0).reshape(2, nse) + >>> V = torch.randn(nse, dims[2], dims[3]) + >>> S = torch.sparse_coo_tensor(I, V, dims).coalesce() + >>> D = torch.randn(dims) + >>> D.sparse_mask(S) + tensor(indices=tensor([[0, 0, 0, 2], + [0, 1, 4, 3]]), + values=tensor([[[ 1.6550, 0.2397], + [-0.1611, -0.0779]], + + [[ 0.2326, -1.0558], + [ 1.4711, 1.9678]], + + [[-0.5138, -0.0411], + [ 1.9417, 0.5158]], + + [[ 0.0793, 0.0036], + [-0.2569, -0.1055]]]), + size=(5, 5, 2, 2), nnz=4, layout=torch.sparse_coo) + """ + + def sparse_resize_( + self, + size: _size, + sparse_dim: _int, + dense_dim: _int, + ) -> Tensor: + r""" + sparse_resize_(size, sparse_dim, dense_dim) -> Tensor + + Resizes :attr:`self` :ref:`sparse tensor ` to the desired + size and the number of sparse and dense dimensions. + + .. note:: + If the number of specified elements in :attr:`self` is zero, then + :attr:`size`, :attr:`sparse_dim`, and :attr:`dense_dim` can be any + size and positive integers such that ``len(size) == sparse_dim + + dense_dim``. + + If :attr:`self` specifies one or more elements, however, then each + dimension in :attr:`size` must not be smaller than the corresponding + dimension of :attr:`self`, :attr:`sparse_dim` must equal the number + of sparse dimensions in :attr:`self`, and :attr:`dense_dim` must + equal the number of dense dimensions in :attr:`self`. + + .. warning:: + Throws an error if :attr:`self` is not a sparse tensor. + + Args: + size (torch.Size): the desired size. If :attr:`self` is non-empty + sparse tensor, the desired size cannot be smaller than the + original size. + sparse_dim (int): the number of sparse dimensions + dense_dim (int): the number of dense dimensions + """ + + def sparse_resize_and_clear_( + self, + size: _size, + sparse_dim: _int, + dense_dim: _int, + ) -> Tensor: + r""" + sparse_resize_and_clear_(size, sparse_dim, dense_dim) -> Tensor + + Removes all specified elements from a :ref:`sparse tensor + ` :attr:`self` and resizes :attr:`self` to the desired + size and the number of sparse and dense dimensions. + + .. warning: + Throws an error if :attr:`self` is not a sparse tensor. + + Args: + size (torch.Size): the desired size. + sparse_dim (int): the number of sparse dimensions + dense_dim (int): the number of dense dimensions + """ + + @overload + def split(self, split_size: _int, dim: _int = 0) -> Sequence[Tensor]: ... + @overload + def split( + self, + split_size: tuple[_int, ...], + dim: _int = 0, + ) -> Sequence[Tensor]: ... + def split_with_sizes( + self, + split_sizes: Sequence[_int | SymInt], + dim: _int = 0, + ) -> tuple[Tensor, ...]: ... + def sqrt(self) -> Tensor: + r""" + sqrt() -> Tensor + + See :func:`torch.sqrt` + """ + + def sqrt_(self) -> Tensor: + r""" + sqrt_() -> Tensor + + In-place version of :meth:`~Tensor.sqrt` + """ + + def square(self) -> Tensor: + r""" + square() -> Tensor + + See :func:`torch.square` + """ + + def square_(self) -> Tensor: + r""" + square_() -> Tensor + + In-place version of :meth:`~Tensor.square` + """ + + @overload + def squeeze(self) -> Tensor: + r""" + squeeze(dim=None) -> Tensor + + See :func:`torch.squeeze` + """ + + @overload + def squeeze(self, dim: _int) -> Tensor: + r""" + squeeze(dim=None) -> Tensor + + See :func:`torch.squeeze` + """ + + @overload + def squeeze(self, dim: _size) -> Tensor: + r""" + squeeze(dim=None) -> Tensor + + See :func:`torch.squeeze` + """ + + @overload + def squeeze(self, *dim: _int) -> Tensor: + r""" + squeeze(dim=None) -> Tensor + + See :func:`torch.squeeze` + """ + + @overload + def squeeze(self, dim: str | EllipsisType | None) -> Tensor: + r""" + squeeze(dim=None) -> Tensor + + See :func:`torch.squeeze` + """ + + @overload + def squeeze_(self) -> Tensor: + r""" + squeeze_(dim=None) -> Tensor + + In-place version of :meth:`~Tensor.squeeze` + """ + + @overload + def squeeze_(self, dim: _int) -> Tensor: + r""" + squeeze_(dim=None) -> Tensor + + In-place version of :meth:`~Tensor.squeeze` + """ + + @overload + def squeeze_(self, dim: _size) -> Tensor: + r""" + squeeze_(dim=None) -> Tensor + + In-place version of :meth:`~Tensor.squeeze` + """ + + @overload + def squeeze_(self, *dim: _int) -> Tensor: + r""" + squeeze_(dim=None) -> Tensor + + In-place version of :meth:`~Tensor.squeeze` + """ + + @overload + def squeeze_(self, dim: str | EllipsisType | None) -> Tensor: + r""" + squeeze_(dim=None) -> Tensor + + In-place version of :meth:`~Tensor.squeeze` + """ + + def sspaddmm( + self, + mat1: Tensor, + mat2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + ) -> Tensor: + r""" + sspaddmm(mat1, mat2, *, beta=1, alpha=1) -> Tensor + + See :func:`torch.sspaddmm` + """ + + @overload + def std( + self, + dim: _int | _size | None, + unbiased: _bool = True, + keepdim: _bool = False, + ) -> Tensor: + r""" + std(dim=None, *, correction=1, keepdim=False) -> Tensor + + See :func:`torch.std` + """ + + @overload + def std( + self, + dim: _int | _size | None = None, + *, + correction: Number | _complex | None = None, + keepdim: _bool = False, + ) -> Tensor: + r""" + std(dim=None, *, correction=1, keepdim=False) -> Tensor + + See :func:`torch.std` + """ + + @overload + def std(self, unbiased: _bool = True) -> Tensor: + r""" + std(dim=None, *, correction=1, keepdim=False) -> Tensor + + See :func:`torch.std` + """ + + @overload + def std( + self, + dim: Sequence[str | EllipsisType | None], + unbiased: _bool = True, + keepdim: _bool = False, + ) -> Tensor: + r""" + std(dim=None, *, correction=1, keepdim=False) -> Tensor + + See :func:`torch.std` + """ + + @overload + def std( + self, + dim: Sequence[str | EllipsisType | None], + *, + correction: Number | _complex | None = None, + keepdim: _bool = False, + ) -> Tensor: + r""" + std(dim=None, *, correction=1, keepdim=False) -> Tensor + + See :func:`torch.std` + """ + + def untyped_storage(self) -> UntypedStorage: ... + def storage_offset(self) -> _int | SymInt: + r""" + storage_offset() -> int + + Returns :attr:`self` tensor's offset in the underlying storage in terms of + number of storage elements (not bytes). + + Example:: + + >>> x = torch.tensor([1, 2, 3, 4, 5]) + >>> x.storage_offset() + 0 + >>> x[3:].storage_offset() + 3 + """ + + def storage_type(self) -> Storage: ... + @overload + def stride(self, dim: None = None) -> tuple[_int, ...]: + r""" + stride(dim) -> tuple or int + + Returns the stride of :attr:`self` tensor. + + Stride is the jump necessary to go from one element to the next one in the + specified dimension :attr:`dim`. A tuple of all strides is returned when no + argument is passed in. Otherwise, an integer value is returned as the stride in + the particular dimension :attr:`dim`. + + Args: + dim (int, optional): the desired dimension in which stride is required + + Example:: + + >>> x = torch.tensor([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]) + >>> x.stride() + (5, 1) + >>> x.stride(0) + 5 + >>> x.stride(-1) + 1 + """ + + @overload + def stride(self, dim: _int) -> _int: + r""" + stride(dim) -> tuple or int + + Returns the stride of :attr:`self` tensor. + + Stride is the jump necessary to go from one element to the next one in the + specified dimension :attr:`dim`. A tuple of all strides is returned when no + argument is passed in. Otherwise, an integer value is returned as the stride in + the particular dimension :attr:`dim`. + + Args: + dim (int, optional): the desired dimension in which stride is required + + Example:: + + >>> x = torch.tensor([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]) + >>> x.stride() + (5, 1) + >>> x.stride(0) + 5 + >>> x.stride(-1) + 1 + """ + + def sub( + self, + other: Tensor | Number | _complex | torch.SymInt | torch.SymFloat, + *, + alpha: Number | _complex | None = 1, + out: Tensor | None = None, + ) -> Tensor: + r""" + sub(other, *, alpha=1) -> Tensor + + See :func:`torch.sub`. + """ + + def sub_( + self, + other: Tensor | Number | _complex | torch.SymInt | torch.SymFloat, + *, + alpha: Number | _complex | None = 1, + ) -> Tensor: + r""" + sub_(other, *, alpha=1) -> Tensor + + In-place version of :meth:`~Tensor.sub` + """ + + @overload + def subtract( + self, + other: Tensor, + *, + alpha: Number | _complex = 1, + ) -> Tensor: + r""" + subtract(other, *, alpha=1) -> Tensor + + See :func:`torch.subtract`. + """ + + @overload + def subtract( + self, + other: Number | _complex, + alpha: Number | _complex = 1, + ) -> Tensor: + r""" + subtract(other, *, alpha=1) -> Tensor + + See :func:`torch.subtract`. + """ + + @overload + def subtract_( + self, + other: Tensor, + *, + alpha: Number | _complex = 1, + ) -> Tensor: + r""" + subtract_(other, *, alpha=1) -> Tensor + + In-place version of :meth:`~Tensor.subtract`. + """ + + @overload + def subtract_( + self, + other: Number | _complex, + alpha: Number | _complex = 1, + ) -> Tensor: + r""" + subtract_(other, *, alpha=1) -> Tensor + + In-place version of :meth:`~Tensor.subtract`. + """ + + @overload + def sum(self, *, dtype: _dtype | None = None) -> Tensor: + r""" + sum(dim=None, keepdim=False, dtype=None) -> Tensor + + See :func:`torch.sum` + """ + + @overload + def sum( + self, + dim: _int | _size | None, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + ) -> Tensor: + r""" + sum(dim=None, keepdim=False, dtype=None) -> Tensor + + See :func:`torch.sum` + """ + + @overload + def sum( + self, + dim: Sequence[str | EllipsisType | None], + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + ) -> Tensor: + r""" + sum(dim=None, keepdim=False, dtype=None) -> Tensor + + See :func:`torch.sum` + """ + + @overload + def sum_to_size(self, size: Sequence[_int | SymInt]) -> Tensor: + r""" + sum_to_size(*size) -> Tensor + + Sum ``this`` tensor to :attr:`size`. + :attr:`size` must be broadcastable to ``this`` tensor size. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + """ + + @overload + def sum_to_size(self, *size: _int | SymInt) -> Tensor: + r""" + sum_to_size(*size) -> Tensor + + Sum ``this`` tensor to :attr:`size`. + :attr:`size` must be broadcastable to ``this`` tensor size. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + """ + + def svd( + self, + some: _bool = True, + compute_uv: _bool = True, + ) -> torch.return_types.svd: + r""" + svd(some=True, compute_uv=True) -> (Tensor, Tensor, Tensor) + + See :func:`torch.svd` + """ + + def swapaxes(self, axis0: _int, axis1: _int) -> Tensor: + r""" + swapaxes(axis0, axis1) -> Tensor + + See :func:`torch.swapaxes` + """ + + def swapaxes_(self, axis0: _int, axis1: _int) -> Tensor: + r""" + swapaxes_(axis0, axis1) -> Tensor + + In-place version of :meth:`~Tensor.swapaxes` + """ + + def swapdims(self, dim0: _int, dim1: _int) -> Tensor: + r""" + swapdims(dim0, dim1) -> Tensor + + See :func:`torch.swapdims` + """ + + def swapdims_(self, dim0: _int, dim1: _int) -> Tensor: + r""" + swapdims_(dim0, dim1) -> Tensor + + In-place version of :meth:`~Tensor.swapdims` + """ + + def t(self) -> Tensor: + r""" + t() -> Tensor + + See :func:`torch.t` + """ + + def t_(self) -> Tensor: + r""" + t_() -> Tensor + + In-place version of :meth:`~Tensor.t` + """ + + def take(self, index: Tensor) -> Tensor: + r""" + take(indices) -> Tensor + + See :func:`torch.take` + """ + + def take_along_dim( + self, + indices: Tensor, + dim: _int | None = None, + ) -> Tensor: + r""" + take_along_dim(indices, dim) -> Tensor + + See :func:`torch.take_along_dim` + """ + + def tan(self) -> Tensor: + r""" + tan() -> Tensor + + See :func:`torch.tan` + """ + + def tan_(self) -> Tensor: + r""" + tan_() -> Tensor + + In-place version of :meth:`~Tensor.tan` + """ + + def tanh(self) -> Tensor: + r""" + tanh() -> Tensor + + See :func:`torch.tanh` + """ + + def tanh_(self) -> Tensor: + r""" + tanh_() -> Tensor + + In-place version of :meth:`~Tensor.tanh` + """ + + @overload + def tensor_split( + self, + indices: Sequence[_int | SymInt], + dim: _int = 0, + ) -> tuple[Tensor, ...]: + r""" + tensor_split(indices_or_sections, dim=0) -> List of Tensors + + See :func:`torch.tensor_split` + """ + + @overload + def tensor_split( + self, + tensor_indices_or_sections: Tensor, + dim: _int = 0, + ) -> tuple[Tensor, ...]: + r""" + tensor_split(indices_or_sections, dim=0) -> List of Tensors + + See :func:`torch.tensor_split` + """ + + @overload + def tensor_split( + self, + sections: _int | SymInt, + dim: _int = 0, + ) -> tuple[Tensor, ...]: + r""" + tensor_split(indices_or_sections, dim=0) -> List of Tensors + + See :func:`torch.tensor_split` + """ + + @overload + def tile(self, dims: Sequence[_int | SymInt]) -> Tensor: + r""" + tile(dims) -> Tensor + + See :func:`torch.tile` + """ + + @overload + def tile(self, *dims: _int | SymInt) -> Tensor: + r""" + tile(dims) -> Tensor + + See :func:`torch.tile` + """ + + @overload + def to( + self, + dtype: _dtype, + non_blocking: _bool = False, + copy: _bool = False, + *, + memory_format: torch.memory_format | None = None, + ) -> Tensor: + r""" + to(*args, **kwargs) -> Tensor + + Performs Tensor dtype and/or device conversion. A :class:`torch.dtype` and :class:`torch.device` are + inferred from the arguments of ``self.to(*args, **kwargs)``. + + .. note:: + + If the ``self`` Tensor already + has the correct :class:`torch.dtype` and :class:`torch.device`, then ``self`` is returned. + Otherwise, the returned tensor is a copy of ``self`` with the desired + :class:`torch.dtype` and :class:`torch.device`. + + .. note:: + + If ``self`` requires gradients (``requires_grad=True``) but the target + ``dtype`` specified is an integer type, the returned tensor will implicitly + set ``requires_grad=False``. This is because only tensors with + floating-point or complex dtypes can require gradients. + + Here are the ways to call ``to``: + + .. method:: to(dtype, non_blocking=False, copy=False, memory_format=torch.preserve_format) -> Tensor + :noindex: + + Returns a Tensor with the specified :attr:`dtype` + + Args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + + .. note:: + + According to `C++ type conversion rules `_, + converting floating point value to integer type will truncate the fractional part. + If the truncated value cannot fit into the target type (e.g., casting ``torch.inf`` to ``torch.long``), + the behavior is undefined and the result may vary across platforms. + + .. method:: to(device=None, dtype=None, non_blocking=False, copy=False, memory_format=torch.preserve_format) -> Tensor + :noindex: + + Returns a Tensor with the specified :attr:`device` and (optional) + :attr:`dtype`. If :attr:`dtype` is ``None`` it is inferred to be ``self.dtype``. + When :attr:`non_blocking` is set to ``True``, the function attempts to perform + the conversion asynchronously with respect to the host, if possible. This + asynchronous behavior applies to both pinned and pageable memory. However, + caution is advised when using this feature. For more information, refer to the + `tutorial on good usage of non_blocking and pin_memory `__. + When :attr:`copy` is set, a new Tensor is created even when the Tensor + already matches the desired conversion. + + Args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + + .. method:: to(other, non_blocking=False, copy=False) -> Tensor + :noindex: + + Returns a Tensor with same :class:`torch.dtype` and :class:`torch.device` as + the Tensor :attr:`other`. + When :attr:`non_blocking` is set to ``True``, the function attempts to perform + the conversion asynchronously with respect to the host, if possible. This + asynchronous behavior applies to both pinned and pageable memory. However, + caution is advised when using this feature. For more information, refer to the + `tutorial on good usage of non_blocking and pin_memory `__. + When :attr:`copy` is set, a new Tensor is created even when the Tensor + already matches the desired conversion. + + Example:: + + >>> tensor = torch.randn(2, 2) # Initially dtype=float32, device=cpu + >>> tensor.to(torch.float64) + tensor([[-0.5044, 0.0005], + [ 0.3310, -0.0584]], dtype=torch.float64) + + >>> cuda0 = torch.device('cuda:0') + >>> tensor.to(cuda0) + tensor([[-0.5044, 0.0005], + [ 0.3310, -0.0584]], device='cuda:0') + + >>> tensor.to(cuda0, dtype=torch.float64) + tensor([[-0.5044, 0.0005], + [ 0.3310, -0.0584]], dtype=torch.float64, device='cuda:0') + + >>> other = torch.randn((), dtype=torch.float64, device=cuda0) + >>> tensor.to(other, non_blocking=True) + tensor([[-0.5044, 0.0005], + [ 0.3310, -0.0584]], dtype=torch.float64, device='cuda:0') + """ + + @overload + def to( + self, + device: DeviceLikeType | None = None, + dtype: _dtype | None = None, + non_blocking: _bool = False, + copy: _bool = False, + *, + memory_format: torch.memory_format | None = None, + ) -> Tensor: + r""" + to(*args, **kwargs) -> Tensor + + Performs Tensor dtype and/or device conversion. A :class:`torch.dtype` and :class:`torch.device` are + inferred from the arguments of ``self.to(*args, **kwargs)``. + + .. note:: + + If the ``self`` Tensor already + has the correct :class:`torch.dtype` and :class:`torch.device`, then ``self`` is returned. + Otherwise, the returned tensor is a copy of ``self`` with the desired + :class:`torch.dtype` and :class:`torch.device`. + + .. note:: + + If ``self`` requires gradients (``requires_grad=True``) but the target + ``dtype`` specified is an integer type, the returned tensor will implicitly + set ``requires_grad=False``. This is because only tensors with + floating-point or complex dtypes can require gradients. + + Here are the ways to call ``to``: + + .. method:: to(dtype, non_blocking=False, copy=False, memory_format=torch.preserve_format) -> Tensor + :noindex: + + Returns a Tensor with the specified :attr:`dtype` + + Args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + + .. note:: + + According to `C++ type conversion rules `_, + converting floating point value to integer type will truncate the fractional part. + If the truncated value cannot fit into the target type (e.g., casting ``torch.inf`` to ``torch.long``), + the behavior is undefined and the result may vary across platforms. + + .. method:: to(device=None, dtype=None, non_blocking=False, copy=False, memory_format=torch.preserve_format) -> Tensor + :noindex: + + Returns a Tensor with the specified :attr:`device` and (optional) + :attr:`dtype`. If :attr:`dtype` is ``None`` it is inferred to be ``self.dtype``. + When :attr:`non_blocking` is set to ``True``, the function attempts to perform + the conversion asynchronously with respect to the host, if possible. This + asynchronous behavior applies to both pinned and pageable memory. However, + caution is advised when using this feature. For more information, refer to the + `tutorial on good usage of non_blocking and pin_memory `__. + When :attr:`copy` is set, a new Tensor is created even when the Tensor + already matches the desired conversion. + + Args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + + .. method:: to(other, non_blocking=False, copy=False) -> Tensor + :noindex: + + Returns a Tensor with same :class:`torch.dtype` and :class:`torch.device` as + the Tensor :attr:`other`. + When :attr:`non_blocking` is set to ``True``, the function attempts to perform + the conversion asynchronously with respect to the host, if possible. This + asynchronous behavior applies to both pinned and pageable memory. However, + caution is advised when using this feature. For more information, refer to the + `tutorial on good usage of non_blocking and pin_memory `__. + When :attr:`copy` is set, a new Tensor is created even when the Tensor + already matches the desired conversion. + + Example:: + + >>> tensor = torch.randn(2, 2) # Initially dtype=float32, device=cpu + >>> tensor.to(torch.float64) + tensor([[-0.5044, 0.0005], + [ 0.3310, -0.0584]], dtype=torch.float64) + + >>> cuda0 = torch.device('cuda:0') + >>> tensor.to(cuda0) + tensor([[-0.5044, 0.0005], + [ 0.3310, -0.0584]], device='cuda:0') + + >>> tensor.to(cuda0, dtype=torch.float64) + tensor([[-0.5044, 0.0005], + [ 0.3310, -0.0584]], dtype=torch.float64, device='cuda:0') + + >>> other = torch.randn((), dtype=torch.float64, device=cuda0) + >>> tensor.to(other, non_blocking=True) + tensor([[-0.5044, 0.0005], + [ 0.3310, -0.0584]], dtype=torch.float64, device='cuda:0') + """ + + @overload + def to( + self, + other: Tensor, + non_blocking: _bool = False, + copy: _bool = False, + *, + memory_format: torch.memory_format | None = None, + ) -> Tensor: + r""" + to(*args, **kwargs) -> Tensor + + Performs Tensor dtype and/or device conversion. A :class:`torch.dtype` and :class:`torch.device` are + inferred from the arguments of ``self.to(*args, **kwargs)``. + + .. note:: + + If the ``self`` Tensor already + has the correct :class:`torch.dtype` and :class:`torch.device`, then ``self`` is returned. + Otherwise, the returned tensor is a copy of ``self`` with the desired + :class:`torch.dtype` and :class:`torch.device`. + + .. note:: + + If ``self`` requires gradients (``requires_grad=True``) but the target + ``dtype`` specified is an integer type, the returned tensor will implicitly + set ``requires_grad=False``. This is because only tensors with + floating-point or complex dtypes can require gradients. + + Here are the ways to call ``to``: + + .. method:: to(dtype, non_blocking=False, copy=False, memory_format=torch.preserve_format) -> Tensor + :noindex: + + Returns a Tensor with the specified :attr:`dtype` + + Args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + + .. note:: + + According to `C++ type conversion rules `_, + converting floating point value to integer type will truncate the fractional part. + If the truncated value cannot fit into the target type (e.g., casting ``torch.inf`` to ``torch.long``), + the behavior is undefined and the result may vary across platforms. + + .. method:: to(device=None, dtype=None, non_blocking=False, copy=False, memory_format=torch.preserve_format) -> Tensor + :noindex: + + Returns a Tensor with the specified :attr:`device` and (optional) + :attr:`dtype`. If :attr:`dtype` is ``None`` it is inferred to be ``self.dtype``. + When :attr:`non_blocking` is set to ``True``, the function attempts to perform + the conversion asynchronously with respect to the host, if possible. This + asynchronous behavior applies to both pinned and pageable memory. However, + caution is advised when using this feature. For more information, refer to the + `tutorial on good usage of non_blocking and pin_memory `__. + When :attr:`copy` is set, a new Tensor is created even when the Tensor + already matches the desired conversion. + + Args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + + .. method:: to(other, non_blocking=False, copy=False) -> Tensor + :noindex: + + Returns a Tensor with same :class:`torch.dtype` and :class:`torch.device` as + the Tensor :attr:`other`. + When :attr:`non_blocking` is set to ``True``, the function attempts to perform + the conversion asynchronously with respect to the host, if possible. This + asynchronous behavior applies to both pinned and pageable memory. However, + caution is advised when using this feature. For more information, refer to the + `tutorial on good usage of non_blocking and pin_memory `__. + When :attr:`copy` is set, a new Tensor is created even when the Tensor + already matches the desired conversion. + + Example:: + + >>> tensor = torch.randn(2, 2) # Initially dtype=float32, device=cpu + >>> tensor.to(torch.float64) + tensor([[-0.5044, 0.0005], + [ 0.3310, -0.0584]], dtype=torch.float64) + + >>> cuda0 = torch.device('cuda:0') + >>> tensor.to(cuda0) + tensor([[-0.5044, 0.0005], + [ 0.3310, -0.0584]], device='cuda:0') + + >>> tensor.to(cuda0, dtype=torch.float64) + tensor([[-0.5044, 0.0005], + [ 0.3310, -0.0584]], dtype=torch.float64, device='cuda:0') + + >>> other = torch.randn((), dtype=torch.float64, device=cuda0) + >>> tensor.to(other, non_blocking=True) + tensor([[-0.5044, 0.0005], + [ 0.3310, -0.0584]], dtype=torch.float64, device='cuda:0') + """ + + def to_dense( + self, + dtype: _dtype | None = None, + *, + masked_grad: _bool | None = None, + ) -> Tensor: + r""" + to_dense(dtype=None, *, masked_grad=True) -> Tensor + + Creates a strided copy of :attr:`self` if :attr:`self` is not a strided tensor, otherwise returns :attr:`self`. + + Keyword args: + {dtype} + masked_grad (bool, optional): If set to ``True`` (default) and + :attr:`self` has a sparse layout then the backward of + :meth:`to_dense` returns ``grad.sparse_mask(self)``. + + Example:: + + >>> s = torch.sparse_coo_tensor( + ... torch.tensor([[1, 1], + ... [0, 2]]), + ... torch.tensor([9, 10]), + ... size=(3, 3)) + >>> s.to_dense() + tensor([[ 0, 0, 0], + [ 9, 0, 10], + [ 0, 0, 0]]) + """ + + def to_mkldnn(self, dtype: _dtype | None = None) -> Tensor: + r""" + to_mkldnn() -> Tensor + Returns a copy of the tensor in ``torch.mkldnn`` layout. + """ + + def to_padded_tensor( + self, + padding: _float, + output_size: Sequence[_int | SymInt] | None = None, + ) -> Tensor: + r""" + to_padded_tensor(padding, output_size=None) -> Tensor + See :func:`to_padded_tensor` + """ + + @overload + def to_sparse( + self, + *, + layout: _layout | None = None, + blocksize: _int | _size | None = None, + dense_dim: _int | None = None, + ) -> Tensor: + r""" + to_sparse(sparseDims) -> Tensor + + Returns a sparse copy of the tensor. PyTorch supports sparse tensors in + :ref:`coordinate format `. + + Args: + sparseDims (int, optional): the number of sparse dimensions to include in the new sparse tensor + + Example:: + + >>> d = torch.tensor([[0, 0, 0], [9, 0, 10], [0, 0, 0]]) + >>> d + tensor([[ 0, 0, 0], + [ 9, 0, 10], + [ 0, 0, 0]]) + >>> d.to_sparse() + tensor(indices=tensor([[1, 1], + [0, 2]]), + values=tensor([ 9, 10]), + size=(3, 3), nnz=2, layout=torch.sparse_coo) + >>> d.to_sparse(1) + tensor(indices=tensor([[1]]), + values=tensor([[ 9, 0, 10]]), + size=(3, 3), nnz=1, layout=torch.sparse_coo) + + .. method:: to_sparse(*, layout=None, blocksize=None, dense_dim=None) -> Tensor + :noindex: + + Returns a sparse tensor with the specified layout and blocksize. If + the :attr:`self` is strided, the number of dense dimensions could be + specified, and a hybrid sparse tensor will be created, with + `dense_dim` dense dimensions and `self.dim() - 2 - dense_dim` batch + dimension. + + .. note:: If the :attr:`self` layout and blocksize parameters match + with the specified layout and blocksize, return + :attr:`self`. Otherwise, return a sparse tensor copy of + :attr:`self`. + + Args: + + layout (:class:`torch.layout`, optional): The desired sparse + layout. One of ``torch.sparse_coo``, ``torch.sparse_csr``, + ``torch.sparse_csc``, ``torch.sparse_bsr``, or + ``torch.sparse_bsc``. Default: if ``None``, + ``torch.sparse_coo``. + + blocksize (list, tuple, :class:`torch.Size`, optional): Block size + of the resulting BSR or BSC tensor. For other layouts, + specifying the block size that is not ``None`` will result in a + RuntimeError exception. A block size must be a tuple of length + two such that its items evenly divide the two sparse dimensions. + + dense_dim (int, optional): Number of dense dimensions of the + resulting CSR, CSC, BSR or BSC tensor. This argument should be + used only if :attr:`self` is a strided tensor, and must be a + value between 0 and dimension of :attr:`self` tensor minus two. + + Example:: + + >>> x = torch.tensor([[1, 0], [0, 0], [2, 3]]) + >>> x.to_sparse(layout=torch.sparse_coo) + tensor(indices=tensor([[0, 2, 2], + [0, 0, 1]]), + values=tensor([1, 2, 3]), + size=(3, 2), nnz=3, layout=torch.sparse_coo) + >>> x.to_sparse(layout=torch.sparse_bsr, blocksize=(1, 2)) + tensor(crow_indices=tensor([0, 1, 1, 2]), + col_indices=tensor([0, 0]), + values=tensor([[[1, 0]], + [[2, 3]]]), size=(3, 2), nnz=2, layout=torch.sparse_bsr) + >>> x.to_sparse(layout=torch.sparse_bsr, blocksize=(2, 1)) + RuntimeError: Tensor size(-2) 3 needs to be divisible by blocksize[0] 2 + >>> x.to_sparse(layout=torch.sparse_csr, blocksize=(3, 1)) + RuntimeError: to_sparse for Strided to SparseCsr conversion does not use specified blocksize + + >>> x = torch.tensor([[[1], [0]], [[0], [0]], [[2], [3]]]) + >>> x.to_sparse(layout=torch.sparse_csr, dense_dim=1) + tensor(crow_indices=tensor([0, 1, 1, 3]), + col_indices=tensor([0, 0, 1]), + values=tensor([[1], + [2], + [3]]), size=(3, 2, 1), nnz=3, layout=torch.sparse_csr) + """ + + @overload + def to_sparse(self, sparse_dim: _int) -> Tensor: + r""" + to_sparse(sparseDims) -> Tensor + + Returns a sparse copy of the tensor. PyTorch supports sparse tensors in + :ref:`coordinate format `. + + Args: + sparseDims (int, optional): the number of sparse dimensions to include in the new sparse tensor + + Example:: + + >>> d = torch.tensor([[0, 0, 0], [9, 0, 10], [0, 0, 0]]) + >>> d + tensor([[ 0, 0, 0], + [ 9, 0, 10], + [ 0, 0, 0]]) + >>> d.to_sparse() + tensor(indices=tensor([[1, 1], + [0, 2]]), + values=tensor([ 9, 10]), + size=(3, 3), nnz=2, layout=torch.sparse_coo) + >>> d.to_sparse(1) + tensor(indices=tensor([[1]]), + values=tensor([[ 9, 0, 10]]), + size=(3, 3), nnz=1, layout=torch.sparse_coo) + + .. method:: to_sparse(*, layout=None, blocksize=None, dense_dim=None) -> Tensor + :noindex: + + Returns a sparse tensor with the specified layout and blocksize. If + the :attr:`self` is strided, the number of dense dimensions could be + specified, and a hybrid sparse tensor will be created, with + `dense_dim` dense dimensions and `self.dim() - 2 - dense_dim` batch + dimension. + + .. note:: If the :attr:`self` layout and blocksize parameters match + with the specified layout and blocksize, return + :attr:`self`. Otherwise, return a sparse tensor copy of + :attr:`self`. + + Args: + + layout (:class:`torch.layout`, optional): The desired sparse + layout. One of ``torch.sparse_coo``, ``torch.sparse_csr``, + ``torch.sparse_csc``, ``torch.sparse_bsr``, or + ``torch.sparse_bsc``. Default: if ``None``, + ``torch.sparse_coo``. + + blocksize (list, tuple, :class:`torch.Size`, optional): Block size + of the resulting BSR or BSC tensor. For other layouts, + specifying the block size that is not ``None`` will result in a + RuntimeError exception. A block size must be a tuple of length + two such that its items evenly divide the two sparse dimensions. + + dense_dim (int, optional): Number of dense dimensions of the + resulting CSR, CSC, BSR or BSC tensor. This argument should be + used only if :attr:`self` is a strided tensor, and must be a + value between 0 and dimension of :attr:`self` tensor minus two. + + Example:: + + >>> x = torch.tensor([[1, 0], [0, 0], [2, 3]]) + >>> x.to_sparse(layout=torch.sparse_coo) + tensor(indices=tensor([[0, 2, 2], + [0, 0, 1]]), + values=tensor([1, 2, 3]), + size=(3, 2), nnz=3, layout=torch.sparse_coo) + >>> x.to_sparse(layout=torch.sparse_bsr, blocksize=(1, 2)) + tensor(crow_indices=tensor([0, 1, 1, 2]), + col_indices=tensor([0, 0]), + values=tensor([[[1, 0]], + [[2, 3]]]), size=(3, 2), nnz=2, layout=torch.sparse_bsr) + >>> x.to_sparse(layout=torch.sparse_bsr, blocksize=(2, 1)) + RuntimeError: Tensor size(-2) 3 needs to be divisible by blocksize[0] 2 + >>> x.to_sparse(layout=torch.sparse_csr, blocksize=(3, 1)) + RuntimeError: to_sparse for Strided to SparseCsr conversion does not use specified blocksize + + >>> x = torch.tensor([[[1], [0]], [[0], [0]], [[2], [3]]]) + >>> x.to_sparse(layout=torch.sparse_csr, dense_dim=1) + tensor(crow_indices=tensor([0, 1, 1, 3]), + col_indices=tensor([0, 0, 1]), + values=tensor([[1], + [2], + [3]]), size=(3, 2, 1), nnz=3, layout=torch.sparse_csr) + """ + + def to_sparse_bsc( + self, + blocksize: _int | _size, + dense_dim: _int | None = None, + ) -> Tensor: + r""" + to_sparse_bsc(blocksize, dense_dim) -> Tensor + + Convert a tensor to a block sparse column (BSC) storage format of + given blocksize. If the :attr:`self` is strided, then the number of + dense dimensions could be specified, and a hybrid BSC tensor will be + created, with `dense_dim` dense dimensions and `self.dim() - 2 - + dense_dim` batch dimension. + + Args: + + blocksize (list, tuple, :class:`torch.Size`, optional): Block size + of the resulting BSC tensor. A block size must be a tuple of + length two such that its items evenly divide the two sparse + dimensions. + + dense_dim (int, optional): Number of dense dimensions of the + resulting BSC tensor. This argument should be used only if + :attr:`self` is a strided tensor, and must be a value between 0 + and dimension of :attr:`self` tensor minus two. + + Example:: + + >>> dense = torch.randn(10, 10) + >>> sparse = dense.to_sparse_csr() + >>> sparse_bsc = sparse.to_sparse_bsc((5, 5)) + >>> sparse_bsc.row_indices() + tensor([0, 1, 0, 1]) + + >>> dense = torch.zeros(4, 3, 1) + >>> dense[0:2, 0] = dense[0:2, 2] = dense[2:4, 1] = 1 + >>> dense.to_sparse_bsc((2, 1), 1) + tensor(ccol_indices=tensor([0, 1, 2, 3]), + row_indices=tensor([0, 1, 0]), + values=tensor([[[[1.]], + + [[1.]]], + + + [[[1.]], + + [[1.]]], + + + [[[1.]], + + [[1.]]]]), size=(4, 3, 1), nnz=3, + layout=torch.sparse_bsc) + """ + + def to_sparse_bsr( + self, + blocksize: _int | _size, + dense_dim: _int | None = None, + ) -> Tensor: + r""" + to_sparse_bsr(blocksize, dense_dim) -> Tensor + + Convert a tensor to a block sparse row (BSR) storage format of given + blocksize. If the :attr:`self` is strided, then the number of dense + dimensions could be specified, and a hybrid BSR tensor will be + created, with `dense_dim` dense dimensions and `self.dim() - 2 - + dense_dim` batch dimension. + + Args: + + blocksize (list, tuple, :class:`torch.Size`, optional): Block size + of the resulting BSR tensor. A block size must be a tuple of + length two such that its items evenly divide the two sparse + dimensions. + + dense_dim (int, optional): Number of dense dimensions of the + resulting BSR tensor. This argument should be used only if + :attr:`self` is a strided tensor, and must be a value between 0 + and dimension of :attr:`self` tensor minus two. + + Example:: + + >>> dense = torch.randn(10, 10) + >>> sparse = dense.to_sparse_csr() + >>> sparse_bsr = sparse.to_sparse_bsr((5, 5)) + >>> sparse_bsr.col_indices() + tensor([0, 1, 0, 1]) + + >>> dense = torch.zeros(4, 3, 1) + >>> dense[0:2, 0] = dense[0:2, 2] = dense[2:4, 1] = 1 + >>> dense.to_sparse_bsr((2, 1), 1) + tensor(crow_indices=tensor([0, 2, 3]), + col_indices=tensor([0, 2, 1]), + values=tensor([[[[1.]], + + [[1.]]], + + + [[[1.]], + + [[1.]]], + + + [[[1.]], + + [[1.]]]]), size=(4, 3, 1), nnz=3, + layout=torch.sparse_bsr) + """ + + def to_sparse_csc(self, dense_dim: _int | None = None) -> Tensor: + r""" + to_sparse_csc() -> Tensor + + Convert a tensor to compressed column storage (CSC) format. Except + for strided tensors, only works with 2D tensors. If the :attr:`self` + is strided, then the number of dense dimensions could be specified, + and a hybrid CSC tensor will be created, with `dense_dim` dense + dimensions and `self.dim() - 2 - dense_dim` batch dimension. + + Args: + + dense_dim (int, optional): Number of dense dimensions of the + resulting CSC tensor. This argument should be used only if + :attr:`self` is a strided tensor, and must be a value between 0 + and dimension of :attr:`self` tensor minus two. + + Example:: + + >>> dense = torch.randn(5, 5) + >>> sparse = dense.to_sparse_csc() + >>> sparse._nnz() + 25 + + >>> dense = torch.zeros(3, 3, 1, 1) + >>> dense[0, 0] = dense[1, 2] = dense[2, 1] = 1 + >>> dense.to_sparse_csc(dense_dim=2) + tensor(ccol_indices=tensor([0, 1, 2, 3]), + row_indices=tensor([0, 2, 1]), + values=tensor([[[1.]], + + [[1.]], + + [[1.]]]), size=(3, 3, 1, 1), nnz=3, + layout=torch.sparse_csc) + """ + + def to_sparse_csr(self, dense_dim: _int | None = None) -> Tensor: + r""" + to_sparse_csr(dense_dim=None) -> Tensor + + Convert a tensor to compressed row storage format (CSR). Except for + strided tensors, only works with 2D tensors. If the :attr:`self` is + strided, then the number of dense dimensions could be specified, and a + hybrid CSR tensor will be created, with `dense_dim` dense dimensions + and `self.dim() - 2 - dense_dim` batch dimension. + + Args: + + dense_dim (int, optional): Number of dense dimensions of the + resulting CSR tensor. This argument should be used only if + :attr:`self` is a strided tensor, and must be a value between 0 + and dimension of :attr:`self` tensor minus two. + + Example:: + + >>> dense = torch.randn(5, 5) + >>> sparse = dense.to_sparse_csr() + >>> sparse._nnz() + 25 + + >>> dense = torch.zeros(3, 3, 1, 1) + >>> dense[0, 0] = dense[1, 2] = dense[2, 1] = 1 + >>> dense.to_sparse_csr(dense_dim=2) + tensor(crow_indices=tensor([0, 1, 2, 3]), + col_indices=tensor([0, 2, 1]), + values=tensor([[[1.]], + + [[1.]], + + [[1.]]]), size=(3, 3, 1, 1), nnz=3, + layout=torch.sparse_csr) + """ + + def tolist(self) -> list: + r""" + tolist() -> list or number + + Returns the tensor as a (nested) list. For scalars, a standard + Python number is returned, just like with :meth:`~Tensor.item`. + Tensors are automatically moved to the CPU first if necessary. + + This operation is not differentiable. + + Examples:: + + >>> a = torch.randn(2, 2) + >>> a.tolist() + [[0.012766935862600803, 0.5415473580360413], + [-0.08909505605697632, 0.7729271650314331]] + >>> a[0,0].tolist() + 0.012766935862600803 + """ + + def topk( + self, + k: _int | SymInt, + dim: _int = -1, + largest: _bool = True, + sorted: _bool = True, + ) -> torch.return_types.topk: + r""" + topk(k, dim=None, largest=True, sorted=True) -> (Tensor, LongTensor) + + See :func:`torch.topk` + """ + + def trace(self) -> Tensor: + r""" + trace() -> Tensor + + See :func:`torch.trace` + """ + + @overload + def transpose(self, dim0: _int, dim1: _int) -> Tensor: + r""" + transpose(dim0, dim1) -> Tensor + + See :func:`torch.transpose` + """ + + @overload + def transpose( + self, + dim0: str | EllipsisType | None, + dim1: str | EllipsisType | None, + ) -> Tensor: + r""" + transpose(dim0, dim1) -> Tensor + + See :func:`torch.transpose` + """ + + def transpose_(self, dim0: _int, dim1: _int) -> Tensor: + r""" + transpose_(dim0, dim1) -> Tensor + + In-place version of :meth:`~Tensor.transpose` + """ + + def triangular_solve( + self, + A: Tensor, + upper: _bool = True, + transpose: _bool = False, + unitriangular: _bool = False, + ) -> torch.return_types.triangular_solve: + r""" + triangular_solve(A, upper=True, transpose=False, unitriangular=False) -> (Tensor, Tensor) + + See :func:`torch.triangular_solve` + """ + + def tril(self, diagonal: _int | SymInt = 0) -> Tensor: + r""" + tril(diagonal=0) -> Tensor + + See :func:`torch.tril` + """ + + def tril_(self, diagonal: _int | SymInt = 0) -> Tensor: + r""" + tril_(diagonal=0) -> Tensor + + In-place version of :meth:`~Tensor.tril` + """ + + def triu(self, diagonal: _int | SymInt = 0) -> Tensor: + r""" + triu(diagonal=0) -> Tensor + + See :func:`torch.triu` + """ + + def triu_(self, diagonal: _int | SymInt = 0) -> Tensor: + r""" + triu_(diagonal=0) -> Tensor + + In-place version of :meth:`~Tensor.triu` + """ + + def true_divide( + self, + other: Tensor | Number | torch.SymInt | torch.SymFloat, + *, + out: Tensor | None = None, + ) -> Tensor: + r""" + true_divide(value) -> Tensor + + See :func:`torch.true_divide` + """ + + def true_divide_( + self, + other: Tensor | Number | torch.SymInt | torch.SymFloat, + ) -> Tensor: + r""" + true_divide_(value) -> Tensor + + In-place version of :meth:`~Tensor.true_divide_` + """ + + def trunc(self) -> Tensor: + r""" + trunc() -> Tensor + + See :func:`torch.trunc` + """ + + def trunc_(self) -> Tensor: + r""" + trunc_() -> Tensor + + In-place version of :meth:`~Tensor.trunc` + """ + + @overload + def type(self, dtype: None = None, non_blocking: _bool = False) -> str: + r""" + type(dtype=None, non_blocking=False, **kwargs) -> str or Tensor + Returns the type if `dtype` is not provided, else casts this object to + the specified type. + + If this is already of the correct type, no copy is performed and the + original object is returned. + + Args: + dtype (dtype or string): The desired type + non_blocking (bool): If ``True``, and the source is in pinned memory + and destination is on the GPU or vice versa, the copy is performed + asynchronously with respect to the host. Otherwise, the argument + has no effect. + **kwargs: For compatibility, may contain the key ``async`` in place of + the ``non_blocking`` argument. The ``async`` arg is deprecated. + """ + + @overload + def type(self, dtype: str | _dtype, non_blocking: _bool = False) -> Tensor: + r""" + type(dtype=None, non_blocking=False, **kwargs) -> str or Tensor + Returns the type if `dtype` is not provided, else casts this object to + the specified type. + + If this is already of the correct type, no copy is performed and the + original object is returned. + + Args: + dtype (dtype or string): The desired type + non_blocking (bool): If ``True``, and the source is in pinned memory + and destination is on the GPU or vice versa, the copy is performed + asynchronously with respect to the host. Otherwise, the argument + has no effect. + **kwargs: For compatibility, may contain the key ``async`` in place of + the ``non_blocking`` argument. The ``async`` arg is deprecated. + """ + + def type_as(self, other: Tensor) -> Tensor: + r""" + type_as(tensor) -> Tensor + + Returns this tensor cast to the type of the given tensor. + + This is a no-op if the tensor is already of the correct type. This is + equivalent to ``self.type(tensor.type())`` + + Args: + tensor (Tensor): the tensor which has the desired type + """ + + @overload + def unbind(self, dim: _int = 0) -> tuple[Tensor, ...]: + r""" + unbind(dim=0) -> seq + + See :func:`torch.unbind` + """ + + @overload + def unbind(self, dim: str | EllipsisType | None) -> tuple[Tensor, ...]: + r""" + unbind(dim=0) -> seq + + See :func:`torch.unbind` + """ + + @overload + def unflatten( + self, + dim: str | EllipsisType | None, + sizes: Sequence[_int | SymInt], + names: Sequence[str | EllipsisType | None], + ) -> Tensor: ... + @overload + def unflatten(self, dim: _int, sizes: Sequence[_int | SymInt]) -> Tensor: ... + def unfold(self, dimension: _int, size: _int, step: _int) -> Tensor: + r""" + unfold(dimension, size, step) -> Tensor + + Returns a view of the original tensor which contains all slices of size :attr:`size` from + :attr:`self` tensor in the dimension :attr:`dimension`. + + Step between two slices is given by :attr:`step`. + + If `sizedim` is the size of dimension :attr:`dimension` for :attr:`self`, the size of + dimension :attr:`dimension` in the returned tensor will be + `(sizedim - size) / step + 1`. + + An additional dimension of size :attr:`size` is appended in the returned tensor. + + Args: + dimension (int): dimension in which unfolding happens + size (int): the size of each slice that is unfolded + step (int): the step between each slice + + Example:: + + >>> x = torch.arange(1., 8) + >>> x + tensor([ 1., 2., 3., 4., 5., 6., 7.]) + >>> x.unfold(0, 2, 1) + tensor([[ 1., 2.], + [ 2., 3.], + [ 3., 4.], + [ 4., 5.], + [ 5., 6.], + [ 6., 7.]]) + >>> x.unfold(0, 2, 2) + tensor([[ 1., 2.], + [ 3., 4.], + [ 5., 6.]]) + """ + + def uniform_( + self, + from_: _float = 0, + to: _float = 1, + *, + generator: Generator | None = None, + ) -> Tensor: + r""" + uniform_(from=0, to=1, *, generator=None) -> Tensor + + Fills :attr:`self` tensor with numbers sampled from the continuous uniform + distribution: + + .. math:: + f(x) = \dfrac{1}{\text{to} - \text{from}} + """ + + def unsafe_chunk(self, chunks: _int, dim: _int = 0) -> tuple[Tensor, ...]: + r""" + unsafe_chunk(chunks, dim=0) -> List of Tensors + + See :func:`torch.unsafe_chunk` + """ + + def unsafe_split( + self, + split_size: _int | SymInt, + dim: _int = 0, + ) -> tuple[Tensor, ...]: + r""" + unsafe_split(split_size, dim=0) -> List of Tensors + + See :func:`torch.unsafe_split` + """ + + def unsafe_split_with_sizes( + self, + split_sizes: Sequence[_int | SymInt], + dim: _int = 0, + ) -> tuple[Tensor, ...]: ... + def unsqueeze(self, dim: _int) -> Tensor: + r""" + unsqueeze(dim) -> Tensor + + See :func:`torch.unsqueeze` + """ + + def unsqueeze_(self, dim: _int) -> Tensor: + r""" + unsqueeze_(dim) -> Tensor + + In-place version of :meth:`~Tensor.unsqueeze` + """ + + def values(self) -> Tensor: + r""" + values() -> Tensor + + Return the values tensor of a :ref:`sparse COO tensor `. + + .. warning:: + Throws an error if :attr:`self` is not a sparse COO tensor. + + See also :meth:`Tensor.indices`. + + .. note:: + This method can only be called on a coalesced sparse tensor. See + :meth:`Tensor.coalesce` for details. + """ + + @overload + def var( + self, + dim: _int | _size | None, + unbiased: _bool = True, + keepdim: _bool = False, + ) -> Tensor: + r""" + var(dim=None, *, correction=1, keepdim=False) -> Tensor + + See :func:`torch.var` + """ + + @overload + def var( + self, + dim: _int | _size | None = None, + *, + correction: Number | _complex | None = None, + keepdim: _bool = False, + ) -> Tensor: + r""" + var(dim=None, *, correction=1, keepdim=False) -> Tensor + + See :func:`torch.var` + """ + + @overload + def var(self, unbiased: _bool = True) -> Tensor: + r""" + var(dim=None, *, correction=1, keepdim=False) -> Tensor + + See :func:`torch.var` + """ + + @overload + def var( + self, + dim: Sequence[str | EllipsisType | None], + unbiased: _bool = True, + keepdim: _bool = False, + ) -> Tensor: + r""" + var(dim=None, *, correction=1, keepdim=False) -> Tensor + + See :func:`torch.var` + """ + + @overload + def var( + self, + dim: Sequence[str | EllipsisType | None], + *, + correction: Number | _complex | None = None, + keepdim: _bool = False, + ) -> Tensor: + r""" + var(dim=None, *, correction=1, keepdim=False) -> Tensor + + See :func:`torch.var` + """ + + def vdot(self, other: Tensor) -> Tensor: + r""" + vdot(other) -> Tensor + + See :func:`torch.vdot` + """ + + @overload + def view(self, dtype: _dtype) -> Tensor: + r""" + view(*shape) -> Tensor + + Returns a new tensor with the same data as the :attr:`self` tensor but of a + different :attr:`shape`. + + The returned tensor shares the same data and must have the same number + of elements, but may have a different size. For a tensor to be viewed, the new + view size must be compatible with its original size and stride, i.e., each new + view dimension must either be a subspace of an original dimension, or only span + across original dimensions :math:`d, d+1, \dots, d+k` that satisfy the following + contiguity-like condition that :math:`\forall i = d, \dots, d+k-1`, + + .. math:: + + \text{stride}[i] = \text{stride}[i+1] \times \text{size}[i+1] + + Otherwise, it will not be possible to view :attr:`self` tensor as :attr:`shape` + without copying it (e.g., via :meth:`contiguous`). When it is unclear whether a + :meth:`view` can be performed, it is advisable to use :meth:`reshape`, which + returns a view if the shapes are compatible, and copies (equivalent to calling + :meth:`contiguous`) otherwise. + + Args: + shape (torch.Size or int...): the desired size + + Example:: + + >>> x = torch.randn(4, 4) + >>> x.size() + torch.Size([4, 4]) + >>> y = x.view(16) + >>> y.size() + torch.Size([16]) + >>> z = x.view(-1, 8) # the size -1 is inferred from other dimensions + >>> z.size() + torch.Size([2, 8]) + + >>> a = torch.randn(1, 2, 3, 4) + >>> a.size() + torch.Size([1, 2, 3, 4]) + >>> b = a.transpose(1, 2) # Swaps 2nd and 3rd dimension + >>> b.size() + torch.Size([1, 3, 2, 4]) + >>> c = a.view(1, 3, 2, 4) # Does not change tensor layout in memory + >>> c.size() + torch.Size([1, 3, 2, 4]) + >>> torch.equal(b, c) + False + + + .. method:: view(dtype) -> Tensor + :noindex: + + Returns a new tensor with the same data as the :attr:`self` tensor but of a + different :attr:`dtype`. + + If the element size of :attr:`dtype` is different than that of ``self.dtype``, + then the size of the last dimension of the output will be scaled + proportionally. For instance, if :attr:`dtype` element size is twice that of + ``self.dtype``, then each pair of elements in the last dimension of + :attr:`self` will be combined, and the size of the last dimension of the output + will be half that of :attr:`self`. If :attr:`dtype` element size is half that + of ``self.dtype``, then each element in the last dimension of :attr:`self` will + be split in two, and the size of the last dimension of the output will be + double that of :attr:`self`. For this to be possible, the following conditions + must be true: + + * ``self.dim()`` must be greater than 0. + * ``self.stride(-1)`` must be 1. + + Additionally, if the element size of :attr:`dtype` is greater than that of + ``self.dtype``, the following conditions must be true as well: + + * ``self.size(-1)`` must be divisible by the ratio between the element + sizes of the dtypes. + * ``self.storage_offset()`` must be divisible by the ratio between the + element sizes of the dtypes. + * The strides of all dimensions, except the last dimension, must be + divisible by the ratio between the element sizes of the dtypes. + + If any of the above conditions are not met, an error is thrown. + + .. warning:: + + This overload is not supported by TorchScript, and using it in a Torchscript + program will cause undefined behavior. + + + Args: + dtype (:class:`torch.dtype`): the desired dtype + + Example:: + + >>> x = torch.randn(4, 4) + >>> x + tensor([[ 0.9482, -0.0310, 1.4999, -0.5316], + [-0.1520, 0.7472, 0.5617, -0.8649], + [-2.4724, -0.0334, -0.2976, -0.8499], + [-0.2109, 1.9913, -0.9607, -0.6123]]) + >>> x.dtype + torch.float32 + + >>> y = x.view(torch.int32) + >>> y + tensor([[ 1064483442, -1124191867, 1069546515, -1089989247], + [-1105482831, 1061112040, 1057999968, -1084397505], + [-1071760287, -1123489973, -1097310419, -1084649136], + [-1101533110, 1073668768, -1082790149, -1088634448]], + dtype=torch.int32) + >>> y[0, 0] = 1000000000 + >>> x + tensor([[ 0.0047, -0.0310, 1.4999, -0.5316], + [-0.1520, 0.7472, 0.5617, -0.8649], + [-2.4724, -0.0334, -0.2976, -0.8499], + [-0.2109, 1.9913, -0.9607, -0.6123]]) + + >>> x.view(torch.cfloat) + tensor([[ 0.0047-0.0310j, 1.4999-0.5316j], + [-0.1520+0.7472j, 0.5617-0.8649j], + [-2.4724-0.0334j, -0.2976-0.8499j], + [-0.2109+1.9913j, -0.9607-0.6123j]]) + >>> x.view(torch.cfloat).size() + torch.Size([4, 2]) + + >>> x.view(torch.uint8) + tensor([[ 0, 202, 154, 59, 182, 243, 253, 188, 185, 252, 191, 63, 240, 22, + 8, 191], + [227, 165, 27, 190, 128, 72, 63, 63, 146, 203, 15, 63, 22, 106, + 93, 191], + [205, 59, 30, 192, 112, 206, 8, 189, 7, 95, 152, 190, 12, 147, + 89, 191], + [ 43, 246, 87, 190, 235, 226, 254, 63, 111, 240, 117, 191, 177, 191, + 28, 191]], dtype=torch.uint8) + >>> x.view(torch.uint8).size() + torch.Size([4, 16]) + """ + + @overload + def view(self, size: Sequence[_int | SymInt]) -> Tensor: + r""" + view(*shape) -> Tensor + + Returns a new tensor with the same data as the :attr:`self` tensor but of a + different :attr:`shape`. + + The returned tensor shares the same data and must have the same number + of elements, but may have a different size. For a tensor to be viewed, the new + view size must be compatible with its original size and stride, i.e., each new + view dimension must either be a subspace of an original dimension, or only span + across original dimensions :math:`d, d+1, \dots, d+k` that satisfy the following + contiguity-like condition that :math:`\forall i = d, \dots, d+k-1`, + + .. math:: + + \text{stride}[i] = \text{stride}[i+1] \times \text{size}[i+1] + + Otherwise, it will not be possible to view :attr:`self` tensor as :attr:`shape` + without copying it (e.g., via :meth:`contiguous`). When it is unclear whether a + :meth:`view` can be performed, it is advisable to use :meth:`reshape`, which + returns a view if the shapes are compatible, and copies (equivalent to calling + :meth:`contiguous`) otherwise. + + Args: + shape (torch.Size or int...): the desired size + + Example:: + + >>> x = torch.randn(4, 4) + >>> x.size() + torch.Size([4, 4]) + >>> y = x.view(16) + >>> y.size() + torch.Size([16]) + >>> z = x.view(-1, 8) # the size -1 is inferred from other dimensions + >>> z.size() + torch.Size([2, 8]) + + >>> a = torch.randn(1, 2, 3, 4) + >>> a.size() + torch.Size([1, 2, 3, 4]) + >>> b = a.transpose(1, 2) # Swaps 2nd and 3rd dimension + >>> b.size() + torch.Size([1, 3, 2, 4]) + >>> c = a.view(1, 3, 2, 4) # Does not change tensor layout in memory + >>> c.size() + torch.Size([1, 3, 2, 4]) + >>> torch.equal(b, c) + False + + + .. method:: view(dtype) -> Tensor + :noindex: + + Returns a new tensor with the same data as the :attr:`self` tensor but of a + different :attr:`dtype`. + + If the element size of :attr:`dtype` is different than that of ``self.dtype``, + then the size of the last dimension of the output will be scaled + proportionally. For instance, if :attr:`dtype` element size is twice that of + ``self.dtype``, then each pair of elements in the last dimension of + :attr:`self` will be combined, and the size of the last dimension of the output + will be half that of :attr:`self`. If :attr:`dtype` element size is half that + of ``self.dtype``, then each element in the last dimension of :attr:`self` will + be split in two, and the size of the last dimension of the output will be + double that of :attr:`self`. For this to be possible, the following conditions + must be true: + + * ``self.dim()`` must be greater than 0. + * ``self.stride(-1)`` must be 1. + + Additionally, if the element size of :attr:`dtype` is greater than that of + ``self.dtype``, the following conditions must be true as well: + + * ``self.size(-1)`` must be divisible by the ratio between the element + sizes of the dtypes. + * ``self.storage_offset()`` must be divisible by the ratio between the + element sizes of the dtypes. + * The strides of all dimensions, except the last dimension, must be + divisible by the ratio between the element sizes of the dtypes. + + If any of the above conditions are not met, an error is thrown. + + .. warning:: + + This overload is not supported by TorchScript, and using it in a Torchscript + program will cause undefined behavior. + + + Args: + dtype (:class:`torch.dtype`): the desired dtype + + Example:: + + >>> x = torch.randn(4, 4) + >>> x + tensor([[ 0.9482, -0.0310, 1.4999, -0.5316], + [-0.1520, 0.7472, 0.5617, -0.8649], + [-2.4724, -0.0334, -0.2976, -0.8499], + [-0.2109, 1.9913, -0.9607, -0.6123]]) + >>> x.dtype + torch.float32 + + >>> y = x.view(torch.int32) + >>> y + tensor([[ 1064483442, -1124191867, 1069546515, -1089989247], + [-1105482831, 1061112040, 1057999968, -1084397505], + [-1071760287, -1123489973, -1097310419, -1084649136], + [-1101533110, 1073668768, -1082790149, -1088634448]], + dtype=torch.int32) + >>> y[0, 0] = 1000000000 + >>> x + tensor([[ 0.0047, -0.0310, 1.4999, -0.5316], + [-0.1520, 0.7472, 0.5617, -0.8649], + [-2.4724, -0.0334, -0.2976, -0.8499], + [-0.2109, 1.9913, -0.9607, -0.6123]]) + + >>> x.view(torch.cfloat) + tensor([[ 0.0047-0.0310j, 1.4999-0.5316j], + [-0.1520+0.7472j, 0.5617-0.8649j], + [-2.4724-0.0334j, -0.2976-0.8499j], + [-0.2109+1.9913j, -0.9607-0.6123j]]) + >>> x.view(torch.cfloat).size() + torch.Size([4, 2]) + + >>> x.view(torch.uint8) + tensor([[ 0, 202, 154, 59, 182, 243, 253, 188, 185, 252, 191, 63, 240, 22, + 8, 191], + [227, 165, 27, 190, 128, 72, 63, 63, 146, 203, 15, 63, 22, 106, + 93, 191], + [205, 59, 30, 192, 112, 206, 8, 189, 7, 95, 152, 190, 12, 147, + 89, 191], + [ 43, 246, 87, 190, 235, 226, 254, 63, 111, 240, 117, 191, 177, 191, + 28, 191]], dtype=torch.uint8) + >>> x.view(torch.uint8).size() + torch.Size([4, 16]) + """ + + @overload + def view(self, *size: _int | SymInt) -> Tensor: + r""" + view(*shape) -> Tensor + + Returns a new tensor with the same data as the :attr:`self` tensor but of a + different :attr:`shape`. + + The returned tensor shares the same data and must have the same number + of elements, but may have a different size. For a tensor to be viewed, the new + view size must be compatible with its original size and stride, i.e., each new + view dimension must either be a subspace of an original dimension, or only span + across original dimensions :math:`d, d+1, \dots, d+k` that satisfy the following + contiguity-like condition that :math:`\forall i = d, \dots, d+k-1`, + + .. math:: + + \text{stride}[i] = \text{stride}[i+1] \times \text{size}[i+1] + + Otherwise, it will not be possible to view :attr:`self` tensor as :attr:`shape` + without copying it (e.g., via :meth:`contiguous`). When it is unclear whether a + :meth:`view` can be performed, it is advisable to use :meth:`reshape`, which + returns a view if the shapes are compatible, and copies (equivalent to calling + :meth:`contiguous`) otherwise. + + Args: + shape (torch.Size or int...): the desired size + + Example:: + + >>> x = torch.randn(4, 4) + >>> x.size() + torch.Size([4, 4]) + >>> y = x.view(16) + >>> y.size() + torch.Size([16]) + >>> z = x.view(-1, 8) # the size -1 is inferred from other dimensions + >>> z.size() + torch.Size([2, 8]) + + >>> a = torch.randn(1, 2, 3, 4) + >>> a.size() + torch.Size([1, 2, 3, 4]) + >>> b = a.transpose(1, 2) # Swaps 2nd and 3rd dimension + >>> b.size() + torch.Size([1, 3, 2, 4]) + >>> c = a.view(1, 3, 2, 4) # Does not change tensor layout in memory + >>> c.size() + torch.Size([1, 3, 2, 4]) + >>> torch.equal(b, c) + False + + + .. method:: view(dtype) -> Tensor + :noindex: + + Returns a new tensor with the same data as the :attr:`self` tensor but of a + different :attr:`dtype`. + + If the element size of :attr:`dtype` is different than that of ``self.dtype``, + then the size of the last dimension of the output will be scaled + proportionally. For instance, if :attr:`dtype` element size is twice that of + ``self.dtype``, then each pair of elements in the last dimension of + :attr:`self` will be combined, and the size of the last dimension of the output + will be half that of :attr:`self`. If :attr:`dtype` element size is half that + of ``self.dtype``, then each element in the last dimension of :attr:`self` will + be split in two, and the size of the last dimension of the output will be + double that of :attr:`self`. For this to be possible, the following conditions + must be true: + + * ``self.dim()`` must be greater than 0. + * ``self.stride(-1)`` must be 1. + + Additionally, if the element size of :attr:`dtype` is greater than that of + ``self.dtype``, the following conditions must be true as well: + + * ``self.size(-1)`` must be divisible by the ratio between the element + sizes of the dtypes. + * ``self.storage_offset()`` must be divisible by the ratio between the + element sizes of the dtypes. + * The strides of all dimensions, except the last dimension, must be + divisible by the ratio between the element sizes of the dtypes. + + If any of the above conditions are not met, an error is thrown. + + .. warning:: + + This overload is not supported by TorchScript, and using it in a Torchscript + program will cause undefined behavior. + + + Args: + dtype (:class:`torch.dtype`): the desired dtype + + Example:: + + >>> x = torch.randn(4, 4) + >>> x + tensor([[ 0.9482, -0.0310, 1.4999, -0.5316], + [-0.1520, 0.7472, 0.5617, -0.8649], + [-2.4724, -0.0334, -0.2976, -0.8499], + [-0.2109, 1.9913, -0.9607, -0.6123]]) + >>> x.dtype + torch.float32 + + >>> y = x.view(torch.int32) + >>> y + tensor([[ 1064483442, -1124191867, 1069546515, -1089989247], + [-1105482831, 1061112040, 1057999968, -1084397505], + [-1071760287, -1123489973, -1097310419, -1084649136], + [-1101533110, 1073668768, -1082790149, -1088634448]], + dtype=torch.int32) + >>> y[0, 0] = 1000000000 + >>> x + tensor([[ 0.0047, -0.0310, 1.4999, -0.5316], + [-0.1520, 0.7472, 0.5617, -0.8649], + [-2.4724, -0.0334, -0.2976, -0.8499], + [-0.2109, 1.9913, -0.9607, -0.6123]]) + + >>> x.view(torch.cfloat) + tensor([[ 0.0047-0.0310j, 1.4999-0.5316j], + [-0.1520+0.7472j, 0.5617-0.8649j], + [-2.4724-0.0334j, -0.2976-0.8499j], + [-0.2109+1.9913j, -0.9607-0.6123j]]) + >>> x.view(torch.cfloat).size() + torch.Size([4, 2]) + + >>> x.view(torch.uint8) + tensor([[ 0, 202, 154, 59, 182, 243, 253, 188, 185, 252, 191, 63, 240, 22, + 8, 191], + [227, 165, 27, 190, 128, 72, 63, 63, 146, 203, 15, 63, 22, 106, + 93, 191], + [205, 59, 30, 192, 112, 206, 8, 189, 7, 95, 152, 190, 12, 147, + 89, 191], + [ 43, 246, 87, 190, 235, 226, 254, 63, 111, 240, 117, 191, 177, 191, + 28, 191]], dtype=torch.uint8) + >>> x.view(torch.uint8).size() + torch.Size([4, 16]) + """ + + def view_as(self, other: Tensor) -> Tensor: + r""" + view_as(other) -> Tensor + + View this tensor as the same size as :attr:`other`. + ``self.view_as(other)`` is equivalent to ``self.view(other.size())``. + + Please see :meth:`~Tensor.view` for more information about ``view``. + + Args: + other (:class:`torch.Tensor`): The result tensor has the same size + as :attr:`other`. + """ + + @overload + def vsplit(self, sections: _int) -> tuple[Tensor, ...]: + r""" + vsplit(split_size_or_sections) -> List of Tensors + + See :func:`torch.vsplit` + """ + + @overload + def vsplit(self, indices: _size) -> tuple[Tensor, ...]: + r""" + vsplit(split_size_or_sections) -> List of Tensors + + See :func:`torch.vsplit` + """ + + @overload + def vsplit(self, *indices: _int) -> tuple[Tensor, ...]: + r""" + vsplit(split_size_or_sections) -> List of Tensors + + See :func:`torch.vsplit` + """ + + @overload + def where(self, condition: Tensor, other: Tensor) -> Tensor: + r""" + where(condition, y) -> Tensor + + ``self.where(condition, y)`` is equivalent to ``torch.where(condition, self, y)``. + See :func:`torch.where` + """ + + @overload + def where(self, condition: Tensor, other: Number | _complex) -> Tensor: + r""" + where(condition, y) -> Tensor + + ``self.where(condition, y)`` is equivalent to ``torch.where(condition, self, y)``. + See :func:`torch.where` + """ + + @overload + def xlogy(self, other: Tensor) -> Tensor: + r""" + xlogy(other) -> Tensor + + See :func:`torch.xlogy` + """ + + @overload + def xlogy(self, other: Number | _complex) -> Tensor: + r""" + xlogy(other) -> Tensor + + See :func:`torch.xlogy` + """ + + @overload + def xlogy_(self, other: Tensor) -> Tensor: + r""" + xlogy_(other) -> Tensor + + In-place version of :meth:`~Tensor.xlogy` + """ + + @overload + def xlogy_(self, other: Number | _complex) -> Tensor: + r""" + xlogy_(other) -> Tensor + + In-place version of :meth:`~Tensor.xlogy` + """ + + def xpu( + self, + device: _device | _int | str | None = None, + non_blocking: _bool = False, + memory_format: torch.memory_format = torch.preserve_format, + ) -> Tensor: + r""" + xpu(device=None, non_blocking=False, memory_format=torch.preserve_format) -> Tensor + + Returns a copy of this object in XPU memory. + + If this object is already in XPU memory and on the correct device, + then no copy is performed and the original object is returned. + + Args: + device (:class:`torch.device`, optional): The destination XPU device. + Defaults to the current XPU device. + non_blocking (bool, optional): If ``True`` and the source is in pinned memory, + the copy will be asynchronous with respect to the host. + Otherwise, the argument has no effect. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + + def zero_(self) -> Tensor: + r""" + zero_() -> Tensor + + Fills :attr:`self` tensor with zeros. + """ + +_TensorBase = TensorBase + +def _DTensor_OpSchema_post_init(self: OpSchema) -> None: ... +def _DTensor_OpSchema_recompute_comparison_key(self: OpSchema) -> None: ... +def _DTensor_compute_global_tensor_info( + tensor: Tensor, mesh: DeviceMesh, placements: Sequence[Placement] +) -> tuple[list[_int], list[_int]]: ... +def _get_DTensor_sharding_propagator_cache_stats() -> tuple[_int, _int]: ... +def _clear_DTensor_sharding_propagator_cache() -> None: ... +def _reinit_DTensor_dispatch_logger() -> None: ... + +# Defined in torch/csrc/multiprocessing/init.cpp +def _multiprocessing_init() -> None: ... +def _set_thread_name(name: str) -> None: ... +def _get_thread_name() -> str: ... + +# Defined in torch/csrc/Module.cpp +def _accelerator_hooks_device_count() -> _int: ... +def _accelerator_hooks_set_current_device(device_index: _int) -> None: ... +def _accelerator_hooks_get_current_device() -> _int: ... +def _accelerator_hooks_exchange_device(device_index: _int) -> _int: ... +def _accelerator_hooks_maybe_exchange_device(device_index: _int) -> _int: ... +def _get_accelerator(check: _bool = False) -> _device: ... +def _storage_Use_Count(storage_ptr: _int) -> _int: ... + +# Defined in torch/csrc/mtia/Module.cpp +def _mtia_init() -> None: ... +def _mtia_isBuilt() -> _bool: ... +def _mtia_isInBadFork() -> _bool: ... +def _mtia_deviceSynchronize() -> None: ... +def _mtia_getCurrentStream(device: _int) -> Stream: ... +def _mtia_getCurrentRawStream(device: _int) -> _int: ... +def _mtia_setCurrentStream(stream: Stream) -> None: ... +def _mtia_getDefaultStream(device: _int) -> Stream: ... +def _mtia_setStream(stream_id: _int, device_index: _int, device_type: _int) -> None: ... +def _mtia_memoryStats(device: _int) -> dict[str, Any]: ... +def _mtia_getDeviceCapability(device: _int) -> tuple[_int, _int]: ... +def _mtia_getDeviceProperties(device: _int) -> dict[str, Any]: ... +def _mtia_emptyCache() -> None: ... +def _mtia_recordMemoryHistory( + enabled: str | None, + stacks: str, + max_entries, +) -> None: ... +def _mtia_memorySnapshot() -> dict[str, Any]: ... +def _mtia_attachOutOfMemoryObserver( + observer: Callable[[_int, _int, _int, _int], None], +) -> None: ... +def _mtia_getDeviceCount() -> _int: ... +def _mtia_getDevice() -> _int: ... +def _mtia_setDevice(device: _int) -> None: ... +def _mtia_exchangeDevice(device: _int) -> _int: ... +def _mtia_maybeExchangeDevice(device: _int) -> _int: ... +def _mtia_resetPeakMemoryStats(device: _int) -> None: ... +def _mtia_graphPoolHandle() -> tuple[_int, _int]: ... + +# Defined in torch/csrc/mtia/Module.cpp +class _MTIAGraph: + def __new__(cls, keep_graph: _bool = ...) -> Self: ... + def capture_begin(self, pool: tuple[_int, _int]) -> None: ... + def capture_end(self) -> None: ... + def instantiate(self) -> None: ... + def replay(self) -> None: ... + def reset(self) -> None: ... + def pool(self) -> tuple[_int, _int]: ... + +# Defined in torch/csrc/mps/Module.cpp +def _mps_deviceSynchronize() -> None: ... +def _mps_get_core_count() -> _int: ... +def _mps_get_default_generator() -> Generator: ... +def _mps_get_name() -> _str: ... +def _mps_emptyCache() -> None: ... +def _mps_setMemoryFraction(fraction: _float) -> None: ... +def _mps_currentAllocatedMemory() -> _int: ... +def _mps_driverAllocatedMemory() -> _int: ... +def _mps_recommendedMaxMemory() -> _int: ... +def _mps_is_available() -> _bool: ... +def _mps_is_on_macos_or_newer(major: _int, minor: _int) -> _bool: ... +def _mps_profilerStartTrace(mode: str, wait_until_completed: _bool) -> None: ... +def _mps_profilerStopTrace() -> None: ... +def _mps_acquireEvent(enable_timing: _bool) -> _int: ... +def _mps_releaseEvent(event_id: _int) -> None: ... +def _mps_recordEvent(event_id: _int) -> None: ... +def _mps_waitForEvent(event_id: _int) -> None: ... +def _mps_synchronizeEvent(event_id: _int) -> None: ... +def _mps_queryEvent(event_id: _int) -> _bool: ... +def _mps_elapsedTimeOfEvents(start_event_id: _int, end_event_id: _int) -> _float: ... +def _mps_isCaptureEnabled() -> _bool: ... +def _mps_isCapturing() -> _bool: ... +def _mps_startCapture(name: str) -> None: ... +def _mps_stopCapture() -> None: ... + +# Defined in torch/csrc/cuda/Module.cpp +def _cuda_getCurrentStream(device: _int) -> tuple: ... +def _cuda_getCurrentRawStream(device: _int) -> _int: ... +def _cuda_getDefaultStream(device: _int) -> tuple: ... +def _cuda_getStreamFromExternal(data_ptr: _int, device_index: _int) -> tuple: ... +def _cuda_getCurrentBlasHandle() -> _int: ... +def _cuda_clearCublasWorkspaces() -> None: ... +def _cuda_setDevice(device: _int) -> None: ... +def _cuda_exchangeDevice(device: _int) -> _int: ... +def _cuda_maybeExchangeDevice(device: _int) -> _int: ... +def _cuda_getDevice() -> _int: ... +def _cuda_getDeviceCount() -> _int: ... +def _cuda_set_sync_debug_mode(warn_level: _int | str) -> None: ... +def _cuda_get_sync_debug_mode() -> _int: ... +def _cuda_sleep(cycles: _int) -> None: ... +def _cuda_synchronize() -> None: ... +def _cuda_ipc_collect() -> None: ... +def _cuda_getArchFlags() -> str | None: ... +def _cuda_init() -> None: ... +def _cuda_setStream(stream_id: _int, device_index: _int, device_type: _int) -> None: ... +def _cuda_getCompiledVersion() -> _int: ... +def _cuda_cudaHostAllocator() -> _int: ... +def _host_emptyCache() -> None: ... +def _cuda_cudaCachingAllocator_raw_alloc(size: _int, cuda_stream: _int) -> _int: ... +def _cuda_cudaCachingAllocator_raw_delete(ptr: _int) -> None: ... +def _cuda_cudaCachingAllocator_enable(val: _bool) -> None: ... +def _cuda_cudaCachingAllocator_is_enabled() -> _bool: ... +def _cuda_beginAllocateToPool(device: _int, mempool_id: tuple[_int, _int]) -> None: ... +def _cuda_beginAllocateCurrentThreadToPool( + device: _int, + mempool_id: tuple[_int, _int], +) -> None: ... +def _cuda_endAllocateToPool(device: _int, mempool_id: tuple[_int, _int]) -> None: ... +def _cuda_beginAllocateCurrentStreamToPool( + device: _int, + mempool_id: tuple[_int, _int], +) -> None: ... +def _cuda_releasePool(device: _int, mempool_id: tuple[_int, _int]) -> None: ... +def _cuda_checkPoolLiveAllocations( + device: _int, + mempool_id: tuple[_int, _int], + expected_live_allocations: set, +) -> _bool: ... +def _cuda_setCheckpointPoolState( + device: _int, + state: _cuda_CUDAAllocator_AllocatorState, + stale_storages: list[_int], + storages_to_add_deleters_to: list[_int], +) -> None: ... +def _cuda_getMemoryFraction(device: _int) -> _float: ... +def _cuda_setMemoryFraction(fraction: _float, device: _int) -> None: ... +def _cuda_emptyCache() -> None: ... +def _cuda_memoryStats(device: _int) -> dict[str, Any]: ... +def _cuda_resetAccumulatedMemoryStats(device: _int) -> None: ... +def _cuda_resetPeakMemoryStats(device: _int) -> None: ... +def _cuda_hostMemoryStats() -> dict[str, Any]: ... +def _cuda_resetAccumulatedHostMemoryStats() -> None: ... +def _cuda_resetPeakHostMemoryStats() -> None: ... +def _cuda_memorySnapshot( + mempool_id: tuple[_int, _int] | tuple[_int, _int, _bool] | None, +) -> dict[str, Any]: ... +def _cuda_setMemoryMetadata(metadata: str) -> None: ... +def _cuda_getMemoryMetadata() -> str: ... +def _cuda_record_memory_history_legacy( + enabled: _bool, + record_context: _bool, + record_context_cpp: _bool, + alloc_trace_max_entries: _int, + alloc_trace_record_context: _bool, + clear_history: _bool, + compile_context: _bool, + global_record_annotations: _bool, + skip_actions: list[str], +) -> None: ... +def _cuda_record_memory_history( + enabled: str | None, + context: str | None, + stacks: str, + max_entries: _int, + clear_history: _bool, + compile_context: _bool, + global_record_annotations: _bool, + skip_actions: list[str], +) -> None: ... +def _cuda_allocationTraceback(data_ptr: _int) -> list[dict[str, Any]] | None: ... +def _cuda_isHistoryEnabled() -> _bool: ... +def _cuda_getAllocatorBackend() -> str: ... + +class _cuda_CUDAAllocator_AllocatorState: ... + +def _cuda_getCheckpointState( + device: _int, + mempool: tuple[_int, _int], +) -> _cuda_CUDAAllocator_AllocatorState: ... +def _set_cached_tensors_enabled(enabled: _bool) -> None: ... +def _add_cached_tensor(t: Tensor) -> None: ... +def _remove_cached_tensor(t: Tensor) -> None: ... +def _tensors_data_ptrs_at_indices_equal( + tensors: list[Tensor | _int], + ptrs: list[_int | None], + indices: list[_int], +) -> _bool: ... +def _construct_CUDA_Tensor_From_Storage_And_Metadata( + metadata: dict, + storage: Storage, +) -> Tensor: ... +def _set_storage_access_error_msg(t: Tensor, s: str) -> None: ... +def _set_storage_data_ptr_access_error_msg(storage_ptr: _int, s: str) -> None: ... +def _clear_storage_data_ptr_access_error_msg(storage_ptr: _int) -> None: ... +def _free_And_Remove_DeleterFn(storage_ptr: _int) -> None: ... +def _has_Standard_Deleter(storage_ptr: _int) -> _bool: ... + +class _cuda_CUDAAllocator: ... + +def _cuda_customAllocator(alloc_fn: _int, free_fn: _int) -> _cuda_CUDAAllocator: ... +def _cuda_changeCurrentAllocator(allocator: _cuda_CUDAAllocator) -> None: ... +def _cuda_getAllocator() -> _cuda_CUDAAllocator: ... +def _cuda_canDeviceAccessPeer(device: _int, peer_device: _int) -> _bool: ... +def _cuda_jiterator_compile_and_launch_kernel( + code_string: str, + kernel_name: str, + return_by_ref: _bool, + num_outputs: _int, + tensors: tuple, + kwargs: dict[str, _int | _float | _bool], +) -> Tensor: ... +def _cuda_get_cudnn_benchmark_limit() -> _int: ... +def _cuda_set_cudnn_benchmark_limit(arg: _int) -> None: ... +def _cuda_get_conv_benchmark_empty_cache() -> _bool: ... +def _cudnn_set_conv_benchmark_empty_cache(enable: _bool) -> None: ... +def _nccl_version() -> _int: ... +def _nccl_version_suffix() -> bytes: ... +def _nccl_unique_id() -> bytes: ... +def _nccl_init_rank(nranks: _int, comm_id: bytes, rank: _int) -> object: ... +def _nccl_reduce( + input: Sequence[Tensor], + output: Tensor, + root: _int, + op: _int, + streams: Sequence[_CudaStreamBase] | None, + comms: Sequence[object] | None, +) -> None: ... +def _nccl_all_reduce( + input: Sequence[Tensor], + output: Sequence[Tensor], + op: _int, + streams: Sequence[_CudaStreamBase] | None, + comms: Sequence[object] | None, +) -> None: ... +def _nccl_broadcast( + input: Sequence[Tensor], + root: _int, + streams: Sequence[_CudaStreamBase] | None, + comms: Sequence[object] | None, +) -> None: ... +def _nccl_all_gather( + input: Sequence[Tensor], + output: Sequence[Tensor], + streams: Sequence[_CudaStreamBase] | None, + comms: Sequence[object] | None, +) -> None: ... +def _nccl_reduce_scatter( + input: Sequence[Tensor], + output: Sequence[Tensor], + op: _int, + streams: Sequence[_CudaStreamBase] | None, + comms: Sequence[object] | None, +) -> None: ... +def _rocm_is_backward_pass() -> _bool: ... +def _cuda_tunableop_enable(val: _bool) -> None: ... +def _cuda_tunableop_is_enabled() -> _bool: ... +def _cuda_tunableop_tuning_enable(val: _bool) -> None: ... +def _cuda_tunableop_tuning_is_enabled() -> _bool: ... +def _cuda_tunableop_set_max_tuning_duration(duration: _int) -> None: ... +def _cuda_tunableop_get_max_tuning_duration() -> _int: ... +def _cuda_tunableop_set_max_tuning_iterations(iterations: _int) -> None: ... +def _cuda_tunableop_get_max_tuning_iterations() -> _int: ... +def _cuda_tunableop_set_filename( + filename: str, + insert_device_ordinal: _bool | None, +) -> None: ... +def _cuda_tunableop_get_filename() -> str: ... +def _cuda_tunableop_read_file(filename: str | None) -> _bool: ... +def _cuda_tunableop_get_results() -> tuple[str, str, str, _float]: ... +def _cuda_tunableop_get_validators() -> tuple[str, str]: ... +def _cuda_tunableop_set_rotating_buffer_size(buffer_size: _int) -> None: ... +def _cuda_tunableop_get_rotation_buffer_size() -> _int: ... +def _cuda_tunableop_set_numerical_check_tolerances( + enabled: _bool, atol: _float = 1e-5, rtol: _float = 1e-5 +) -> None: ... + +class _CudaDeviceProperties: + name: str + major: _int + minor: _int + multi_processor_count: _int + total_memory: _int + is_integrated: _int + is_multi_gpu_board: _int + max_threads_per_multi_processor: _int + gcnArchName: str + warp_size: _int + uuid: str + L2_cache_size: _int + clock_rate: _int + memory_clock_rate: _int + memory_bus_width: _int + shared_memory_per_block: _int + shared_memory_per_multiprocessor: _int + +# Functions related to SDPA +class _SDPAParams: + query: Tensor + key: Tensor + value: Tensor + attn_mask: Tensor | None + dropout: _float + is_causal: _bool + enable_gqa: _bool + def __init__( + self, + query: Tensor, + key: Tensor, + value: Tensor, + attn_mask: Tensor | None, + dropout: _float, + is_causal: _bool, + enable_gqa: _bool, + ) -> None: ... + +class _SDPBackend(Enum): + ERROR = -1 + MATH = 0 + FLASH_ATTENTION = 1 + EFFICIENT_ATTENTION = 2 + CUDNN_ATTENTION = 3 + OVERRIDEABLE = 4 + +def _is_flash_attention_available() -> _bool: ... +def _can_use_cudnn_attention(params: _SDPAParams, debug: _bool) -> _bool: ... +def _can_use_flash_attention(params: _SDPAParams, debug: _bool) -> _bool: ... +def _can_use_mem_efficient_attention(params: _SDPAParams, debug: _bool) -> _bool: ... +def _is_ck_sdpa_available() -> _bool: ... + +# Defined in torch/csrc/cuda/GdsFile.cpp +def _gds_register_buffer(t: Storage) -> None: ... +def _gds_deregister_buffer(t: Storage) -> None: ... +def _gds_register_handle(fd: _int) -> _int: ... +def _gds_deregister_handle(handle: _int) -> None: ... +def _gds_load_storage(handle: _int, s: Storage, offset: _int) -> None: ... +def _gds_save_storage(handle: _int, s: Storage, offset: _int) -> None: ... + +# Defined in torch/csrc/cuda/python_comm.cpp +def _broadcast(tensor: Tensor, devices: list[_int]) -> list[Tensor]: ... +def _broadcast_out(tensor: Tensor, out_tensors: list[Tensor]) -> list[Tensor]: ... +def _broadcast_coalesced( + tensors: list[Tensor], + devices: list[_int], + buffer_size: _int, +) -> list[list[Tensor]]: ... +def _scatter( + tensor: Tensor, + devices: list[_int], + chunk_sizes: list[_int] | None, + dim: _int, + streams: list[Stream] | None, +) -> list[Tensor]: ... +def _scatter_out( + tensor: Tensor, + out_tensors: list[Tensor], + dim: _int, + streams: list[Stream] | None, +) -> list[Tensor]: ... +def _gather( + tensors: list[Tensor], + dim: _int, + destination_index: _int | None, +) -> Tensor: ... +def _gather_out(tensors: list[Tensor], out_tensor: Tensor, dim: _int) -> Tensor: ... + +# Defined in torch/csrc/cuda/Stream.cpp +class _CudaStreamBase(Stream): + stream_id: _int + device_index: _int + device_type: _int + + device: _device + cuda_stream: _int + priority: _int + + def __new__( + cls, + priority: _int = 0, + stream_id: _int = 0, + device_index: _int = 0, + stream_ptr: _int = 0, + ) -> Self: ... + def query(self) -> _bool: ... + def synchronize(self) -> None: ... + def priority_range(self) -> tuple[_int, _int]: ... + +# Defined in torch/csrc/cuda/Event.cpp +class _CudaEventBase: + device: _device + cuda_event: _int + + def __new__( + cls, + enable_timing: _bool = False, + blocking: _bool = False, + interprocess: _bool = False, + external: _bool = False, + ) -> Self: ... + @classmethod + def from_ipc_handle(cls, device: _device, ipc_handle: bytes) -> _CudaEventBase: ... + def record(self, stream: _CudaStreamBase | torch.Stream) -> None: ... + def wait(self, stream: _CudaStreamBase | torch.Stream) -> None: ... + def query(self) -> _bool: ... + def elapsed_time(self, other: _CudaEventBase) -> _float: ... + def synchronize(self) -> None: ... + def ipc_handle(self) -> bytes: ... + +# Defined in torch/csrc/cuda/Graph.cpp +class _CUDAGraph: + def __new__(cls, keep_graph: _bool = ...) -> Self: ... + def capture_begin( + self, + pool: _POOL_HANDLE | None = ..., + capture_error_mode: str = "global", + ) -> None: ... + def capture_end(self) -> None: ... + def instantiate(self) -> None: ... + def register_generator_state(self, Generator) -> None: ... + def replay(self) -> None: ... + def reset(self) -> None: ... + def pool(self) -> _POOL_HANDLE: ... + def enable_debug_mode(self) -> None: ... + def debug_dump(self, debug_path: str) -> None: ... + def raw_cuda_graph(self) -> _int: ... + def raw_cuda_graph_exec(self) -> _int: ... + @staticmethod + def get_currently_capturing_graph() -> _CUDAGraph: ... + def begin_capture_to_if_node(self, scalar_cuda_pred_tensor): ... + def end_capture_to_conditional_node(self): ... + +# Defined in torch/csrc/cuda/MemPool.cpp +class _MemPool: + def __init__( + self, + allocator: _cuda_CUDAAllocator | None = None, + is_user_created: _bool = True, + use_on_oom: _bool = False, + no_split: _bool = False, + ) -> None: ... + @property + def id(self) -> tuple[_int, _int]: ... + @property + def allocator(self) -> _cuda_CUDAAllocator | None: ... + def use_count(self) -> _int: ... + +def _cuda_isCurrentStreamCapturing() -> _bool: ... +def _graph_pool_handle() -> tuple[_int, _int]: ... + +# Defined in torch/csrc/xpu/Module.cpp +def _xpu_setDevice(device: _int) -> None: ... +def _xpu_exchangeDevice(device: _int) -> _int: ... +def _xpu_maybeExchangeDevice(device: _int) -> _int: ... +def _xpu_getDevice() -> _int: ... +def _xpu_getDeviceCount() -> _int: ... +def _xpu_getArchFlags() -> str | None: ... +def _xpu_init() -> None: ... +def _xpu_setStream(stream_id: _int, device_index: _int, device_type: _int) -> None: ... +def _xpu_getCurrentStream(device: _int) -> tuple: ... +def _xpu_getCurrentRawStream(device: _int) -> _int: ... +def _xpu_getStreamFromExternal(data_ptr: _int, device_index: _int) -> tuple: ... +def _xpu_synchronize(device: _int) -> None: ... +def _xpu_emptyCache() -> None: ... +def _xpu_memoryStats(device: _int) -> dict[str, Any]: ... +def _xpu_resetAccumulatedMemoryStats(device: _int) -> None: ... +def _xpu_resetPeakMemoryStats(device: _int) -> None: ... +def _xpu_getMemoryInfo(device: _int) -> tuple[_int, _int]: ... +def _xpu_canDeviceAccessPeer(device: _int, peer: _int) -> _bool: ... +def _xpu_getMemoryFraction(device: _int) -> _float: ... +def _xpu_setMemoryFraction(fraction: _float, device: _int) -> None: ... +def _xpu_memorySnapshot(mempool_id: tuple[_int, _int] | None) -> dict[str, Any]: ... +def _xpu_recordMemoryHistory( + enabled: Literal["state", "all"] | None, + context: Literal["state", "alloc", "all"] | None, + stacks: Literal["python", "all"], + max_entries: _int, + clear_history: _bool, + skip_actions: list[str] | None, +) -> None: ... +def _xpu_beginAllocateToPool(device: _int, mempool_id: tuple[_int, _int]) -> None: ... +def _xpu_beginAllocateCurrentThreadToPool( + device: _int, + mempool_id: tuple[_int, _int], +) -> None: ... +def _xpu_endAllocateToPool(device: _int, mempool_id: tuple[_int, _int]) -> None: ... +def _xpu_releasePool(device: _int, mempool_id: tuple[_int, _int]) -> None: ... + +class _XpuDeviceProperties: + name: str + platform_name: str + vendor: str + device_id: _int + driver_version: str + version: str + max_compute_units: _int + gpu_eu_count: _int + max_work_group_size: _int + max_num_sub_groups: _int + memory_clock_rate: _int + memory_bus_width: _int + sub_group_sizes: list[_int] + local_mem_size: _int + has_fp16: _bool + has_fp64: _bool + has_atomic64: _bool + has_bfloat16_conversions: _bool + has_subgroup_matrix_multiply_accumulate: _bool + has_subgroup_matrix_multiply_accumulate_tensor_float32: _bool + has_subgroup_2d_block_io: _bool + total_memory: _int + gpu_subslice_count: _int + architecture: _int + type: str + uuid: Any + +class _xpu_XPUAllocator: ... + +def _xpu_customAllocator(alloc_fn: _int, free_fn: _int) -> _xpu_XPUAllocator: ... +def _xpu_changeCurrentAllocator(allocator: _xpu_XPUAllocator) -> None: ... +def _xpu_getAllocator() -> _xpu_XPUAllocator: ... + +# Defined in torch/csrc/xpu/Stream.cpp +class _XpuStreamBase(Stream): + stream_id: _int + device_index: _int + device_type: _int + + device: _device + sycl_queue: _int + priority: _int + + def __new__( + cls, + priority: _int = 0, + stream_id: _int = 0, + device_index: _int = 0, + device_type: _int = 0, + ) -> Self: ... + def query(self) -> _bool: ... + def synchronize(self) -> None: ... + @staticmethod + def priority_range() -> tuple: ... + +# Defined in torch/csrc/xpu/Event.cpp +class _XpuEventBase: + device: _device + sycl_event: _int + + def __new__(cls, enable_timing: _bool = False) -> Self: ... + def record(self, stream: _XpuStreamBase | torch.Stream) -> None: ... + def wait(self, stream: _XpuStreamBase | torch.Stream) -> None: ... + def query(self) -> _bool: ... + def elapsed_time(self, other: _XpuEventBase) -> _float: ... + def synchronize(self) -> None: ... + +# Defined in torch/csrc/xpu/MemPool.cpp +class _XPUMemPool: + def __init__( + self, + allocator: _xpu_XPUAllocator | None = None, + is_user_created: _bool = True, + use_on_oom: _bool = False, + ) -> None: ... + @property + def id(self) -> tuple[_int, _int]: ... + @property + def allocator(self) -> _xpu_XPUAllocator | None: ... + def use_count(self) -> _int: ... + +# Defined in torch/csrc/xpu/Graph.cpp +class _XPUGraph: + def __new__(cls, keep_graph: _bool = ...) -> Self: ... + def capture_begin( + self, + pool: XPU_POOL_HANDLE | None = ..., + ) -> None: ... + def capture_end(self) -> None: ... + def instantiate(self) -> None: ... + def register_generator_state(self, Generator) -> None: ... + def replay(self) -> None: ... + def reset(self) -> None: ... + def pool(self) -> XPU_POOL_HANDLE: ... + def enable_debug_mode(self) -> None: ... + def debug_dump(self, debug_path: str) -> None: ... + def raw_xpu_graph(self) -> _int: ... + def raw_xpu_graph_exec(self) -> _int: ... + +def _xpu_graph_pool_handle() -> tuple[_int, _int]: ... +def _xpu_isCurrentStreamCapturing() -> _bool: ... + +# Defined in torch/csrc/DataLoader.cpp +def _set_worker_signal_handlers( + *arg: Any, +) -> None: ... # THPModule_setWorkerSignalHandlers +def _set_worker_pids( + key: _int, + child_pids: tuple[_int, ...], +) -> None: ... # THPModule_setWorkerPIDs +def _remove_worker_pids(loader_id: _int) -> None: ... # THPModule_removeWorkerPIDs +def _error_if_any_worker_fails() -> None: ... # THPModule_errorIfAnyWorkerFails + +# Defined in torch/csrc/DeviceAccelerator.cpp +def _accelerator_getAccelerator() -> _device: ... +def _accelerator_setDeviceIndex(device_index: _int) -> None: ... +def _accelerator_getDeviceIndex() -> _int: ... +def _accelerator_getDeviceCapability(device_index: _int) -> dict[str, Any]: ... +def _accelerator_setStream(Stream) -> None: ... +def _accelerator_getStream(device_index: _int) -> Stream: ... +def _accelerator_synchronizeDevice(device_index: _int) -> None: ... +def _accelerator_exchangeDevice(device_index: _int) -> _int: ... +def _accelerator_maybeExchangeDevice(device_index: _int) -> _int: ... +def _accelerator_isAllocatorInitialized() -> _bool: ... +def _accelerator_emptyCache() -> None: ... +def _accelerator_emptyHostCache() -> None: ... +def _accelerator_getDeviceStats(device_index: _int) -> dict[str, Any]: ... +def _accelerator_resetAccumulatedStats(device_index: _int) -> None: ... +def _accelerator_resetPeakStats(device_index: _int) -> None: ... +def _accelerator_getMemoryInfo(device_index: _int) -> tuple[_int, _int]: ... +def _accelerator_getAllocatorSettings() -> str: ... +def _accelerator_setAllocatorSettings(env: str) -> None: ... + +class _acceleratorGraph: + def __new__(cls, _bool) -> Self: ... + def __init__(self, _bool) -> None: ... + def capture_begin( + self, + pool: tuple[_int, _int] | None = None, + capture_error_mode: Literal[ + "default", "global", "thread_local", "relaxed" + ] = "default", + ) -> None: ... + def capture_end(self) -> None: ... + def instantiate(self) -> None: ... + def replay(self) -> None: ... + def reset(self) -> None: ... + def pool(self) -> tuple[_int, _int]: ... + def enable_debug_mode(self) -> None: ... + def debug_dump(self, path: str) -> None: ... + +# Defined in torch/csrc/jit/python/python_tracer.cpp +class TracingState: + def push_scope(self, scope_name: str) -> None: ... + def pop_scope(self) -> None: ... + def current_scope(self) -> str: ... + def set_graph(self, graph: Graph) -> None: ... + def graph(self) -> Graph: ... + +def _create_graph_by_tracing( + func: Callable[..., Any], + inputs: Any, + var_name_lookup_fn: Callable[[Tensor], str], + strict: Any, + force_outplace: Any, + self: Any = None, + argument_names: list[str] = ..., +) -> tuple[Graph, Stack]: ... +def _tracer_warn_use_python(): ... +def _get_tracing_state() -> TracingState: ... + +# Defined in torch/csrc/jit/python/python_ir.cpp +# Not actually defined in python_ir.cpp, not sure where they are. +class IValue: ... + +Stack: TypeAlias = list[IValue] + +class JitType: + annotation_str: str + def isSubtypeOf(self, other: JitType) -> _bool: ... + def with_dtype(self, dtype: _dtype) -> JitType: ... + def with_sizes(self, sizes: list[_int | None]) -> JitType: ... + def kind(self) -> str: ... + def scalarType(self) -> str | None: ... + def getElementType(self) -> JitType: ... + def dtype(self) -> _dtype | None: ... + +class InferredType: + def __init__(self, arg: JitType | str) -> None: ... + def type(self) -> JitType: ... + def success(self) -> _bool: ... + def reason(self) -> str: ... + +class Type(JitType): + def str(self) -> _str: ... + def containedTypes(self) -> list[JitType]: ... + def dim(self) -> _int | None: ... + def undefined(self) -> _bool | None: ... + def sizes(self) -> list[_int] | None: ... + def symbol_sizes(self) -> list[_int] | None: ... + def varyingSizes(self) -> list[_int | None] | None: ... + def strides(self) -> list[_int] | None: ... + def contiguous(self) -> Self: ... + def device(self) -> _device | None: ... + def is_interface_type(self) -> _bool: ... + def requires_grad(self) -> _bool: ... + @property + def annotation_string(self) -> _str: ... + +class AnyType(JitType): + @staticmethod + def get() -> AnyType: ... + +class NoneType(JitType): + @staticmethod + def get() -> NoneType: ... + +class BoolType(JitType): + @staticmethod + def get() -> BoolType: ... + +class FloatType(JitType): + @staticmethod + def get() -> FloatType: ... + +class ComplexType(JitType): + @staticmethod + def get() -> ComplexType: ... + +class IntType(JitType): + @staticmethod + def get() -> IntType: ... + +class SymIntType(JitType): + @staticmethod + def get() -> SymIntType: ... + +class SymBoolType(JitType): + @staticmethod + def get() -> SymBoolType: ... + +class NumberType(JitType): + @staticmethod + def get() -> NumberType: ... + +class StringType(JitType): + @staticmethod + def get() -> StringType: ... + +class DeviceObjType(JitType): + @staticmethod + def get() -> DeviceObjType: ... + +class _GeneratorType(JitType): + @staticmethod + def get() -> _GeneratorType: ... + +class StreamObjType(JitType): + @staticmethod + def get() -> StreamObjType: ... + +class ListType(JitType): + def __init__(self, a: JitType) -> None: ... + def getElementType(self) -> JitType: ... + @staticmethod + def ofInts() -> ListType: ... + @staticmethod + def ofTensors() -> ListType: ... + @staticmethod + def ofFloats() -> ListType: ... + @staticmethod + def ofComplexDoubles() -> ListType: ... + @staticmethod + def ofBools() -> ListType: ... + @staticmethod + def ofStrings() -> ListType: ... + +class DictType(JitType): + def __init__(self, key: JitType, value: JitType) -> None: ... + def getKeyType(self) -> JitType: ... + def getValueType(self) -> JitType: ... + +class TupleType(JitType): + def __init__(self, a: list[JitType | None]) -> None: ... + def elements(self) -> list[JitType]: ... + +class UnionType(JitType): + def __init__(self, a: list[JitType]) -> None: ... + +class ClassType(JitType): + def __init__(self, qualified_name: str) -> None: ... + def qualified_name(self) -> str: ... + +class InterfaceType(JitType): + def __init__(self, qualified_name: str) -> None: ... + def getMethod(self, name: str) -> FunctionSchema | None: ... + def getMethodNames(self) -> list[str]: ... + +JitTypeT = TypeVar("JitTypeT", bound=JitType) # noqa: PYI001 + +class OptionalType(JitType, Generic[JitTypeT]): + def __init__(self, a: JitTypeT) -> None: ... + def getElementType(self) -> JitTypeT: ... + @staticmethod + def ofTensor() -> OptionalType: ... + +class FutureType(JitType): + def __init__(self, a: JitType) -> None: ... + def getElementType(self) -> JitType: ... + +class AwaitType(JitType): + def __init__(self, a: JitType) -> None: ... + def getElementType(self) -> JitType: ... + +class RRefType(JitType): + def __init__(self, a: JitType) -> None: ... + +class EnumType(JitType): + def __init__( + self, + qualified_name: str, + value_type: JitType, + enum_names_values: list[Any], + ) -> None: ... + +class TensorType(JitType): + @classmethod + def get(cls) -> TensorType: ... + @classmethod + def getInferred(cls) -> TensorType: ... + def with_sizes(self, other: list[_int | None] | None) -> TensorType: ... + def sizes(self) -> list[_int] | None: ... + def varyingSizes(self) -> list[_int | None] | None: ... + def strides(self) -> list[_int] | None: ... + def device(self) -> _device | None: ... + def dim(self) -> _int: ... + def dtype(self) -> _dtype | None: ... + @staticmethod + def create_from_tensor(t: Tensor) -> TensorType: ... + +# Defined in torch/csrc/jit/python/python_tree_views.cpp +class SourceRange: ... +class TreeView: ... + +class Ident(TreeView): + @property + def name(self) -> str: ... + +class ClassDef(TreeView): ... + +class Def(TreeView): + def name(self) -> Ident: ... + +class Decl(TreeView): ... + +# Defined in torch/csrc/distributed/rpc/init.cpp +def _rpc_init() -> _bool: ... + +# Defined in torch/csrc/distributed/autograd/init.cpp +def _dist_autograd_init() -> _bool: ... + +# Defined in torch/csrc/distributed/c10d/init.cpp +def _c10d_init() -> _bool: ... + +# Defined in torch/csrc/distributed/rpc/testing/init.cpp +def _faulty_agent_init() -> _bool: ... +def _register_py_class_for_device(device: str, cls: Any) -> None: ... + +# Defined in torch/csrc/Module.cpp +def _current_graph_task_id() -> _int: ... +def _current_autograd_node() -> _Node: ... +def _will_engine_execute_node(node: _Node) -> _bool: ... +def _dispatch_key_set(tensor) -> str: ... + +# Defined in torch/csrc/Exceptions.cpp +class AcceleratorError(RuntimeError): ... +class OutOfMemoryError(RuntimeError): ... +class _DistError(RuntimeError): ... +class _DistBackendError(RuntimeError): ... +class _DistStoreError(RuntimeError): ... +class _DistNetworkError(RuntimeError): ... +class _DistQueueEmptyError(_DistStoreError): ... + +# Defined in torch/csrc/profiler/init.cpp +class CapturedTraceback: ... + +def gather_traceback(python: _bool, script: _bool, cpp: _bool) -> CapturedTraceback: ... +def symbolize_tracebacks( + tracebacks: list[CapturedTraceback], +) -> list[dict[str, Any]]: ... +def _load_mobile_module_from_file(filename: str): ... +def _load_mobile_module_from_bytes(bytes_: bytes): ... +def _load_jit_module_from_file(filename: str): ... +def _load_jit_module_from_bytes(bytes_: bytes): ... +def _save_mobile_module(m: LiteScriptModule, filename: str): ... +def _save_jit_module(m: ScriptModule, filename: str, extra_files: dict[str, Any]): ... +def _save_mobile_module_to_bytes(m: LiteScriptModule) -> bytes: ... +def _save_jit_module_to_bytes( + m: ScriptModule, + extra_files: dict[str, Any], +) -> bytes: ... +def _get_module_info_from_flatbuffer(data: bytes): ... +def _jit_resolve_packet(op_name: str, *args, **kwargs) -> str: ... +def _swap_tensor_impl(t1: Tensor, t2: Tensor): ... +def _pickle_save(obj: Any) -> bytes: ... +def _pickle_load_obj(bs: bytes) -> Any: ... + +# Defined in torch/csrc/jit/runtime/static/init.cpp +def _jit_to_static_module(graph_or_module: Graph | ScriptModule) -> Any: ... +def _fuse_to_static_module( + graph_or_module: Graph | ScriptModule, + min_size: _int, +) -> Any: ... + +# Defined in torch/csrc/fx/node.cpp +def _fx_map_aggregate(a: Any, fn: Callable[[Any], Any]) -> Any: ... +def _fx_map_arg(a: Any, fn: Callable[[Any], Any]) -> Any: ... + +class _NodeBase: + _erased: _bool + _prev: FxNode + _next: FxNode + def __init__( + self, + graph: Any, + name: str, + op: str, + target: Any, + return_type: Any, + ) -> None: ... + def _update_args_kwargs(self, args: tuple[Any, ...], kwargs: dict[str, Any]): ... + def _prepend(self, n: FxNode) -> None: ... + def _replace_input_with(self, old_input: FxNode, new_input: FxNode) -> None: ... + def _remove_from_list(self) -> None: ... + def __lt__(self, n: Self) -> _bool: ... + def __gt__(self, n: Self) -> _bool: ... + def __le__(self, n: Self) -> _bool: ... + def __ge__(self, n: Self) -> _bool: ... + +class _NodeIter(Iterator[FxNode]): + def __init__(self, root: FxNode, reversed: _bool) -> None: ... + def __iter__(self) -> Self: ... + def __next__(self) -> FxNode: ... + +# Defined in torch/csrc/inductor/static_launcher/cuda.cpp +class _StaticCudaLauncher: + @staticmethod + def _load_kernel( + cubin_file: str, + func_name: str, + shared_mem_bytes: _int, + device: _int, + ) -> tuple[_int, _int, _int]: ... + @staticmethod + def _launch_kernel( + func: _int, + grid_x: _int, + grid_y: _int, + grid_z: _int, + num_warps: _int, + shared_mem_bytes: _int, + arg_types: str, + args: tuple[Any, ...], + stream: _int, + ) -> None: ... + +# Defined in torch/csrc/inductor/static_launcher/xpu.cpp +class _StaticXpuLauncher(_StaticCudaLauncher): ... + +# Defined in torch/csrc/cuda/GreenContext.cpp +class GreenContext: + @staticmethod + def create( + num_sms: _int, + device_id: _int, + ) -> GreenContext: ... + def set_context( + self, + ) -> None: ... + def pop_context( + self, + ) -> None: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_acc/__init__.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_acc/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..3ee9b16c225b5ed6a34bc9ab9198eb38ad1e52a4 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_acc/__init__.pyi @@ -0,0 +1,15 @@ +from torch import Tensor +from torch.types import _dtype, _int, Device + +# Defined in torch/csrc/acc/Module.cpp +class PrivateUse1Hooks: + def has_primary_context(self, device_index: _int) -> bool: ... + def is_built(self) -> bool: ... + def is_available(self) -> bool: ... + +class DeviceGuard: + def type_(self) -> Device: ... + +def register_python_privateuseone_device_guard(guard: DeviceGuard) -> bool: ... +def register_python_privateuseone_hook(hook: PrivateUse1Hooks) -> bool: ... +def create_empty_tensor(shape: tuple[_int, ...], dtype: _dtype) -> Tensor: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_aoti.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_aoti.pyi new file mode 100644 index 0000000000000000000000000000000000000000..2f57b5e5e72b1e30d0955bf955c3de22174c70ef --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_aoti.pyi @@ -0,0 +1,164 @@ +from ctypes import c_void_p +from typing import overload, Protocol + +from torch import Tensor + +# Defined in torch/csrc/inductor/aoti_runner/pybind.cpp + +# Tensor to AtenTensorHandle +def unsafe_alloc_void_ptrs_from_tensors(tensors: list[Tensor]) -> list[c_void_p]: ... +def unsafe_alloc_void_ptr_from_tensor(tensor: Tensor) -> c_void_p: ... + +# AtenTensorHandle to Tensor +def alloc_tensors_by_stealing_from_void_ptrs( + handles: list[c_void_p], +) -> list[Tensor]: ... +def alloc_tensor_by_stealing_from_void_ptr( + handle: c_void_p, +) -> Tensor: ... + +class AOTIModelContainerRunner(Protocol): + def run( + self, inputs: list[Tensor], stream_handle: c_void_p = ... + ) -> list[Tensor]: ... + def get_call_spec(self) -> list[str]: ... + def get_constant_names_to_original_fqns(self) -> dict[str, str]: ... + def get_constant_names_to_dtypes(self) -> dict[str, int]: ... + def extract_constants_map(self, use_inactive: bool) -> dict[str, Tensor]: ... + def update_constant_buffer( + self, + tensor_map: dict[str, Tensor], + use_inactive: bool, + validate_full_updates: bool, + user_managed: bool = ..., + ) -> None: ... + def swap_constant_buffer(self) -> None: ... + def free_inactive_constant_buffer(self) -> None: ... + +class AOTIModelContainerRunnerCpu: + def __init__(self, model_so_path: str, num_models: int) -> None: ... + def run( + self, inputs: list[Tensor], stream_handle: c_void_p = ... + ) -> list[Tensor]: ... + def get_call_spec(self) -> list[str]: ... + def get_constant_names_to_original_fqns(self) -> dict[str, str]: ... + def get_constant_names_to_dtypes(self) -> dict[str, int]: ... + def extract_constants_map(self, use_inactive: bool) -> dict[str, Tensor]: ... + def update_constant_buffer( + self, + tensor_map: dict[str, Tensor], + use_inactive: bool, + validate_full_updates: bool, + user_managed: bool = ..., + ) -> None: ... + def swap_constant_buffer(self) -> None: ... + def free_inactive_constant_buffer(self) -> None: ... + +class AOTIModelContainerRunnerCuda: + @overload + def __init__(self, model_so_path: str, num_models: int) -> None: ... + @overload + def __init__( + self, model_so_path: str, num_models: int, device_str: str + ) -> None: ... + @overload + def __init__( + self, model_so_path: str, num_models: int, device_str: str, cubin_dir: str + ) -> None: ... + def run( + self, inputs: list[Tensor], stream_handle: c_void_p = ... + ) -> list[Tensor]: ... + def get_call_spec(self) -> list[str]: ... + def get_constant_names_to_original_fqns(self) -> dict[str, str]: ... + def get_constant_names_to_dtypes(self) -> dict[str, int]: ... + def extract_constants_map(self, use_inactive: bool) -> dict[str, Tensor]: ... + def update_constant_buffer( + self, + tensor_map: dict[str, Tensor], + use_inactive: bool, + validate_full_updates: bool, + user_managed: bool = ..., + ) -> None: ... + def swap_constant_buffer(self) -> None: ... + def free_inactive_constant_buffer(self) -> None: ... + +class AOTIModelContainerRunnerXpu: + @overload + def __init__(self, model_so_path: str, num_models: int) -> None: ... + @overload + def __init__( + self, model_so_path: str, num_models: int, device_str: str + ) -> None: ... + @overload + def __init__( + self, model_so_path: str, num_models: int, device_str: str, kernel_bin_dir: str + ) -> None: ... + def run( + self, inputs: list[Tensor], stream_handle: c_void_p = ... + ) -> list[Tensor]: ... + def get_call_spec(self) -> list[str]: ... + def get_constant_names_to_original_fqns(self) -> dict[str, str]: ... + def get_constant_names_to_dtypes(self) -> dict[str, int]: ... + def extract_constants_map(self, use_inactive: bool) -> dict[str, Tensor]: ... + def update_constant_buffer( + self, + tensor_map: dict[str, Tensor], + use_inactive: bool, + validate_full_updates: bool, + user_managed: bool = ..., + ) -> None: ... + def swap_constant_buffer(self) -> None: ... + def free_inactive_constant_buffer(self) -> None: ... + +class AOTIModelContainerRunnerMps: + def __init__(self, model_so_path: str, num_models: int) -> None: ... + def run( + self, inputs: list[Tensor], stream_handle: c_void_p = ... + ) -> list[Tensor]: ... + def get_call_spec(self) -> list[str]: ... + def get_constant_names_to_original_fqns(self) -> dict[str, str]: ... + def get_constant_names_to_dtypes(self) -> dict[str, int]: ... + def extract_constants_map(self, use_inactive: bool) -> dict[str, Tensor]: ... + def update_constant_buffer( + self, + tensor_map: dict[str, Tensor], + use_inactive: bool, + validate_full_updates: bool, + user_managed: bool = ..., + ) -> None: ... + def swap_constant_buffer(self) -> None: ... + def free_inactive_constant_buffer(self) -> None: ... + +# Defined in torch/csrc/inductor/aoti_package/pybind.cpp +class AOTIModelPackageLoader: + def __init__( + self, + model_package_path: str, + model_name: str, + run_single_threaded: bool, + num_runners: int, + device_index: int, + ) -> None: ... + def get_metadata(self) -> dict[str, str]: ... + def run( + self, inputs: list[Tensor], stream_handle: c_void_p = ... + ) -> list[Tensor]: ... + def boxed_run( + self, inputs: list[Tensor], stream_handle: c_void_p = ... + ) -> list[Tensor]: ... + def get_call_spec(self) -> list[str]: ... + def get_constant_fqns(self) -> list[str]: ... + def load_constants( + self, + constants_map: dict[str, Tensor], + use_inactive: bool, + check_full_update: bool, + user_managed: bool = ..., + ) -> None: ... + def update_constant_buffer( + self, + tensor_map: dict[str, Tensor], + use_inactive: bool, + validate_full_updates: bool, + user_managed: bool = ..., + ) -> None: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_autograd.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_autograd.pyi new file mode 100644 index 0000000000000000000000000000000000000000..733fb3410db9bd04965aea7957b8cf22ac5453df --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_autograd.pyi @@ -0,0 +1,163 @@ +# mypy: allow-untyped-defs +from collections.abc import Callable +from enum import Enum +from typing import Any + +import torch +from torch._C._profiler import ( + _ProfilerEvent, + ActiveProfilerType, + ProfilerActivity, + ProfilerConfig, +) + +# Defined in torch/csrc/autograd/init.cpp + +class DeviceType(Enum): + CPU = ... + CUDA = ... + XPU = ... + MKLDNN = ... + OPENGL = ... + OPENCL = ... + IDEEP = ... + HIP = ... + FPGA = ... + MAIA = ... + XLA = ... + MTIA = ... + MPS = ... + HPU = ... + Meta = ... + Vulkan = ... + Metal = ... + PrivateUse1 = ... + +class ProfilerEvent: + def cpu_elapsed_us(self, other: ProfilerEvent) -> float: ... + def cpu_memory_usage(self) -> int: ... + def cuda_elapsed_us(self, other: ProfilerEvent) -> float: ... + def privateuse1_elapsed_us(self, other: ProfilerEvent) -> float: ... + def cuda_memory_usage(self) -> int: ... + def device(self) -> int: ... + def handle(self) -> int: ... + def has_cuda(self) -> bool: ... + def is_remote(self) -> bool: ... + def kind(self) -> int: ... + def name(self) -> str: ... + def node_id(self) -> int: ... + def sequence_nr(self) -> int: ... + def shapes(self) -> list[list[int]]: ... + def thread_id(self) -> int: ... + def flops(self) -> float: ... + def is_async(self) -> bool: ... + +class _KinetoEvent: + def name(self) -> str: ... + def overload_name(self) -> str: ... + def device_index(self) -> int: ... + def device_resource_id(self) -> int: ... + def start_ns(self) -> int: ... + def end_ns(self) -> int: ... + def duration_ns(self) -> int: ... + def is_async(self) -> bool: ... + def linked_correlation_id(self) -> int: ... + def external_id(self) -> int: ... + def shapes(self) -> list[list[int]]: ... + def dtypes(self) -> list[str]: ... + def concrete_inputs(self) -> list[Any]: ... + def kwinputs(self) -> dict[str, Any]: ... + def device_type(self) -> DeviceType: ... + def start_thread_id(self) -> int: ... + def end_thread_id(self) -> int: ... + def correlation_id(self) -> int: ... + def fwd_thread_id(self) -> int: ... + def stack(self) -> list[str]: ... + def scope(self) -> int: ... + def sequence_nr(self) -> int: ... + def flops(self) -> int: ... + def cuda_elapsed_us(self) -> int: ... + def privateuse1_elapsed_us(self) -> int: ... + def is_user_annotation(self) -> bool: ... + def is_python_function(self) -> bool: ... + def is_hidden_event(self) -> bool: ... + def metadata_json(self) -> str: ... + def activity_type(self) -> str: ... + def extra_meta(self) -> dict[str, str]: ... + def flow_id(self) -> int: ... + def flow_type(self) -> int: ... + def flow_start(self) -> bool: ... + def structured_input_shapes(self) -> list[list[int] | list[list[int]]]: ... + def structured_input_strides(self) -> list[list[int] | list[list[int]]]: ... + def python_id(self) -> int: ... + def python_parent_id(self) -> int: ... + def python_module_id(self) -> int: ... + +class _ProfilerResult: + def events(self) -> list[_KinetoEvent]: ... + def legacy_events(self) -> list[list[ProfilerEvent]]: ... + def save(self, path: str) -> None: ... + def experimental_event_tree(self) -> list[_ProfilerEvent]: ... + def trace_start_ns(self) -> int: ... + +class SavedTensor: + def unpack(self) -> torch.Tensor: ... + +def _make_saved_tensor( + tensor: torch.Tensor, + is_output: bool, + is_inplace_on_view: bool = False, +) -> SavedTensor: ... +def _enable_profiler( + config: ProfilerConfig, + activities: set[ProfilerActivity], +) -> None: ... +def _prepare_profiler( + config: ProfilerConfig, + activities: set[ProfilerActivity], + activity_filter: dict[ProfilerActivity, set[str]] = ..., +) -> None: ... +def _toggle_collection_dynamic( + enable: bool, + activities: set[ProfilerActivity], +) -> None: ... +def _disable_profiler() -> _ProfilerResult: ... +def _profiler_enabled() -> bool: ... +def _add_metadata_json(key: str, value: str) -> None: ... +def _kineto_step() -> None: ... +def _get_current_graph_task_keep_graph() -> bool: ... +def _get_sequence_nr() -> int: ... +def kineto_available() -> bool: ... +def _record_function_with_args_enter(name: str, *args) -> torch.Tensor: ... +def _record_function_with_args_exit(handle: torch.Tensor) -> None: ... +def _supported_activities() -> set[ProfilerActivity]: ... +def _enable_record_function(enable: bool) -> None: ... +def _set_empty_test_observer(is_global: bool, sampling_prob: float) -> None: ... +def _push_saved_tensors_default_hooks( + pack_hook: Callable[[torch.Tensor], Any], + unpack_hook: Callable[[Any], torch.Tensor], +) -> None: ... +def _pop_saved_tensors_default_hooks() -> None: ... +def _top_saved_tensors_default_hooks( + ignore_is_tracing: bool, +) -> tuple[Callable[[torch.Tensor], Any], Callable[[Any], torch.Tensor]]: ... +def _unsafe_set_version_counter( + t: tuple[torch.Tensor, ...], prev_version: tuple[int, ...] +) -> None: ... +def _enable_profiler_legacy(config: ProfilerConfig) -> None: ... +def _disable_profiler_legacy() -> list[list[ProfilerEvent]]: ... +def _profiler_type() -> ActiveProfilerType: ... +def _saved_tensors_hooks_enable() -> None: ... +def _saved_tensors_hooks_disable(message: str, fail_if_non_empty=True) -> None: ... +def _saved_tensors_hooks_get_disabled_error_message() -> str | None: ... +def _saved_tensors_hooks_set_tracing(is_tracing: bool) -> bool: ... + +class CreationMeta(Enum): + DEFAULT = ... + IN_CUSTOM_FUNCTION = ... + MULTI_OUTPUT_NODE = ... + NO_GRAD_MODE = ... + INFERENCE_MODE = ... + +def _set_creation_meta(t: torch.Tensor, creation_meta: CreationMeta) -> None: ... +def _get_creation_meta(t: torch.Tensor) -> CreationMeta: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_cpu.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_cpu.pyi new file mode 100644 index 0000000000000000000000000000000000000000..4d8f2edc430f4611460050a3931886647a1bf31d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_cpu.pyi @@ -0,0 +1,8 @@ +from typing import Any + +from torch.types import _bool + +# Defined in torch/csrc/cpu/Module.cpp + +def _init_amx() -> _bool: ... +def _get_cpu_capability() -> dict[str, Any]: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_cudnn.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_cudnn.pyi new file mode 100644 index 0000000000000000000000000000000000000000..cfea3f956f2a373dfe6895743a49a3c6cec439fa --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_cudnn.pyi @@ -0,0 +1,14 @@ +from enum import IntEnum + +# Defined in torch/csrc/cuda/shared/cudnn.cpp +is_cuda: bool + +def getRuntimeVersion() -> tuple[int, int, int]: ... +def getCompileVersion() -> tuple[int, int, int]: ... +def getVersionInt() -> int: ... + +class RNNMode(IntEnum): + rnn_relu = ... + rnn_tanh = ... + lstm = ... + gru = ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_cusparselt.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_cusparselt.pyi new file mode 100644 index 0000000000000000000000000000000000000000..a1c4bbb217777d50b9a3384da11d9992db5e18f0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_cusparselt.pyi @@ -0,0 +1 @@ +def getVersionInt() -> int: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_distributed.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_distributed.pyi new file mode 100644 index 0000000000000000000000000000000000000000..c8416f60edca35493ad7c1703cf2e27d58975be0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_distributed.pyi @@ -0,0 +1,21 @@ +# This module is defined in torch/csrc/distributed/python_placement.cpp + +class Placement: + def is_partial(self, reduce_op: str | None = None) -> bool: ... + def is_replicate(self) -> bool: ... + def is_shard(self, dim: int | None = None) -> bool: ... + +class Shard(Placement): + dim: int + def __init__(self, dim: int): ... + +class StridedShard(Placement): + dim: int + split_factor: int + def __init__(self, dim: int, *, split_factor: int): ... + +class Replicate(Placement): ... + +class Partial(Placement): + reduce_op: str + def __init__(self, reduce_op: str | None = None): ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_distributed_autograd.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_distributed_autograd.pyi new file mode 100644 index 0000000000000000000000000000000000000000..6e1e39bec2927c046455928ff74bc7de9683e66f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_distributed_autograd.pyi @@ -0,0 +1,26 @@ +from typing import Any + +import torch + +# This module is defined in torch/csrc/distributed/autograd/init.cpp + +class DistAutogradContext: + def _context_id(self) -> int: ... + def _recv_functions(self) -> dict[int, Any]: ... + def _send_functions(self) -> dict[int, Any]: ... + def _known_worker_ids(self) -> set[int]: ... + +def _new_context() -> DistAutogradContext: ... +def _release_context(context_id: int) -> None: ... +def _get_max_id() -> int: ... +def _is_valid_context(worker_id: int) -> bool: ... +def _retrieve_context(context_id: int) -> DistAutogradContext: ... +def _current_context() -> DistAutogradContext: ... +def _init(worker_id: int) -> None: ... +def _get_debug_info() -> dict[str, str]: ... +def backward( + context_id: int, + roots: list[torch.Tensor], + retain_graph: bool = False, +) -> None: ... +def get_gradients(context_id: int) -> dict[torch.Tensor, torch.Tensor]: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_distributed_c10d.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_distributed_c10d.pyi new file mode 100644 index 0000000000000000000000000000000000000000..972b88005db33e5f8c1bb08d63fb671244bec868 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_distributed_c10d.pyi @@ -0,0 +1,900 @@ +# mypy: allow-untyped-defs +# mypy: disable-error-code="type-arg" +from collections.abc import Callable +from datetime import timedelta +from enum import Enum +from typing import Any, overload + +import torch +from torch import Tensor +from torch._C import ScriptObject +from torch._C._autograd import DeviceType +from torch.distributed.distributed_c10d import GroupName +from torch.futures import Future + +# This module is defined in torch/csrc/distributed/c10d/init.cpp + +_DEFAULT_FIRST_BUCKET_BYTES: int +_DEFAULT_NO_TIMEOUT: timedelta +_DEFAULT_PG_TIMEOUT: timedelta +_DEFAULT_PG_NCCL_TIMEOUT: timedelta + +class BuiltinCommHookType(Enum): + ALLREDUCE = ... + FP16_COMPRESS = ... + +def _register_comm_hook(reducer: Reducer, state: Any, comm_hook: Any): ... +def _register_builtin_comm_hook( + reducer: Reducer, + comm_hook_type: BuiltinCommHookType, +): ... +def _set_global_rank(rank: int) -> None: ... +def _hash_tensors(tensors: list[Tensor]) -> int: ... + +class GradBucket: + def index(self) -> int: ... + def buffer(self) -> Tensor: ... + def gradients(self) -> list[Tensor]: ... + def is_last(self) -> bool: ... + def set_buffer(self, tensor: Tensor) -> None: ... + def parameters(self) -> list[Tensor]: ... + +class Reducer: + def __init__( + self, + params: list[Tensor], + bucket_indices: list[list[int]], + per_bucket_size_limits: list[int], + process_group: ProcessGroup, + expect_sparse_gradients: list[bool] = ..., + bucket_bytes_cap: int = ..., # kDefaultBucketBytesCap in reducer.hpp + find_unused_parameters: bool = ..., + gradient_as_bucket_view: bool = ..., + param_to_name_mapping: dict[int, str] = ..., + first_bucket_types_cap: int = ..., # kDefaultFirstBucketBytes in reducer.hpp + skip_all_reduce_unused_params: bool = ..., + use_python_reducer: bool = ..., + bucket_bytes_cap_list: list[int] = ..., + batched_grad_copy: bool = ..., + ) -> None: ... + def prepare_for_forward(self) -> None: ... + def prepare_for_backward(self, output: list[Tensor]) -> None: ... + def get_backward_stats(self) -> list[int]: ... + def _install_post_backward_futures(self, futures: list[Future]) -> None: ... + def _rebuild_buckets(self) -> bool: ... + def _get_zeros_like_grad_buckets(self) -> list[GradBucket]: ... + def _push_all_rebuilt_params(self) -> None: ... + def _set_forward_pass_work_handle( + self, + work: Work, + use_static_world_size: bool, + ): ... + def _get_local_used_map(self) -> Tensor: ... + def _set_ddp_runtime_logging_sample_rate(self, sample_rate: int) -> None: ... + def _set_static_graph(self) -> None: ... + def _run_comm_hook(self, bucket: GradBucket) -> Future: ... + def set_logger(self, logger: Logger) -> None: ... + def _remove_autograd_hooks(self) -> None: ... + def _check_reducer_finalized(self) -> None: ... + def _set_sparse_metadata(self, global_unique_ids: dict[str, Tensor]) -> None: ... + def _reset_state(self) -> None: ... + def _update_process_group(self, new_process_group: ProcessGroup) -> None: ... + +class DDPLoggingData: + strs_map: dict[str, str] + ints_map: dict[str, int] + +class Logger: + def __init__(self, reducer: Reducer) -> None: ... + def set_construction_data_and_log( + self, + module_name: str, + device_ids: list[int], + output_device: int, + broadcast_buffers: bool, + has_sync_bn: bool, + static_graph: bool, + ) -> None: ... + def set_runtime_stats_and_log(self) -> None: ... + def set_error_and_log(self, error: str) -> None: ... + def _get_ddp_logging_data(self) -> DDPLoggingData: ... + def _set_comm_hook_name(self, comm_hook: str) -> None: ... + def _set_uneven_input_join(self) -> None: ... + def _set_static_graph(self) -> None: ... + +class _WorkerServer: + port: int + + def __init__(self, host_or_file: str, port: int = ...) -> None: ... + def shutdown(self) -> None: ... + +class DebugLevel(Enum): + OFF = ... + INFO = ... + DETAIL = ... + +def get_debug_level() -> DebugLevel: ... +def set_debug_level(level: DebugLevel) -> None: ... +def set_debug_level_from_env() -> None: ... + +class ReduceOp: + # pyrefly: ignore # unknown-name + def __init__(self, op: RedOpType) -> None: ... + + # pyrefly: ignore # unknown-name + SUM: RedOpType = ... + # pyrefly: ignore # unknown-name + AVG: RedOpType = ... + # pyrefly: ignore # unknown-name + PRODUCT: RedOpType = ... + # pyrefly: ignore # unknown-name + MIN: RedOpType = ... + # pyrefly: ignore # unknown-name + MAX: RedOpType = ... + # pyrefly: ignore # unknown-name + BAND: RedOpType = ... + # pyrefly: ignore # unknown-name + BOR: RedOpType = ... + # pyrefly: ignore # unknown-name + BXOR: RedOpType = ... + # pyrefly: ignore # unknown-name + PREMUL_SUM: RedOpType = ... + # pyrefly: ignore # unknown-name + UNUSED: RedOpType = ... + + # mypy error being ignored: + # Detected enum "torch._C._distributed_c10d.ReduceOp.RedOpType" in a type + # stub with zero members. There is a chance this is due to a recent change + # in the semantics of enum membership. If so, use `member = value` to mark + # an enum member, instead of `member: type` + class RedOpType(Enum): ... # type: ignore[misc] + +class BroadcastOptions: + rootRank: int + rootTensor: int + timeout: timedelta + asyncOp: bool + +class AllreduceOptions: + reduceOp: ReduceOp + timeout: timedelta + asyncOp: bool + sparseIndices: Tensor | None + +class AllreduceCoalescedOptions(AllreduceOptions): ... + +class ReduceOptions: + reduceOp: ReduceOp + rootRank: int + rootTensor: int + timeout: timedelta + asyncOp: bool + +class AllgatherOptions: + timeout: timedelta + asyncOp: bool + +class GatherOptions: + rootRank: int + timeout: timedelta + asyncOp: bool + +class ScatterOptions: + rootRank: int + timeout: timedelta + asyncOp: bool + +class ReduceScatterOptions: + reduceOp: ReduceOp + timeout: timedelta + asyncOp: bool + +class BarrierOptions: + device_ids: list[int] + device: torch.device + timeout: timedelta + asyncOp: bool + +class AllToAllOptions: + timeout: timedelta + asyncOp: bool + +class Store: + def set(self, key: str, value: str) -> None: ... + def get(self, key: str) -> bytes: ... + def add(self, key: str, value: int) -> int: ... + def check(self, keys: list[str]) -> bool: ... + def compare_set( + self, + key: str, + expected_value: str, + desired_value: str, + ) -> bytes: ... + def delete_key(self, key: str) -> bool: ... + def multi_get(self, keys: list[str]) -> list[bytes]: ... + def num_keys(self) -> int: ... + def set_timeout(self, timeout: timedelta) -> None: ... + @overload + def wait(self, keys: list[str]) -> None: ... + @overload + def wait(self, keys: list[str], timeout: timedelta) -> None: ... + def queue_pop(self, key: str, block: bool = True) -> bytes: ... + def queue_push(self, key: str, value: bytes | str) -> None: ... + def queue_len(self, key: str) -> int: ... + def list_keys(self) -> list[str]: ... + +class FileStore(Store): + def __init__(self, path: str, numWorkers: int = ...) -> None: ... + +class HashStore(Store): + def __init__(self) -> None: ... + +class TCPStore(Store): + def __init__( + self, + host_name: str, + port: int, + world_size: int | None = ..., + is_master: bool = ..., + timeout: timedelta = ..., + wait_for_workers: bool = ..., + multi_tenant: bool = ..., + master_listen_fd: int | None = ..., + use_libuv: bool | None = ..., + ) -> None: ... + @property + def host(self) -> str: ... + @property + def port(self) -> int: ... + +class PrefixStore(Store): + def __init__(self, prefix: str, store: Store) -> None: ... + @property + def underlying_store(self) -> Store: ... + +class _ControlCollectives: + def barrier(self, key: str, timeout: timedelta, blocking: bool) -> None: ... + def broadcast_send(self, key: str, data: str, timeout: timedelta) -> None: ... + def broadcast_recv(self, key: str, timeout: timedelta) -> str: ... + def gather_send(self, key: str, data: str, timeout: timedelta) -> None: ... + def gather_recv(self, key: str, timeout: timedelta) -> str: ... + def scatter_send(self, key: str, data: str, timeout: timedelta) -> None: ... + def scatter_recv(self, key: str, timeout: timedelta) -> str: ... + def all_gather(self, key: str, data: str, timeout: timedelta) -> str: ... + def all_sum(self, key: str, data: int, timeout: timedelta) -> int: ... + +class _StoreCollectives(_ControlCollectives): + def __init__(self, store: Store, rank: int, world_size: int) -> None: ... + +class _DistributedBackendOptions: + def __init__(self) -> None: ... + @property + def store(self) -> Store: ... + @store.setter + def store(self, store: Store) -> None: ... + @property + def group_rank(self) -> int: ... + @group_rank.setter + def group_rank(self, rank: int) -> None: ... + @property + def group_size(self) -> int: ... + @group_size.setter + def group_size(self, size: int) -> None: ... + @property + def timeout(self) -> timedelta: ... + @timeout.setter + def timeout(self, timeout: timedelta) -> None: ... + @property + def group_id(self) -> str: ... + @group_id.setter + def group_id(self, group_id: str) -> None: ... + @property + def global_ranks_in_group(self) -> list[int]: ... + @global_ranks_in_group.setter + def global_ranks_in_group(self, ranks: list[int]) -> None: ... + +class Work: + def is_completed(self) -> bool: ... + def is_success(self) -> bool: ... + def exception(self) -> Any: ... + def wait(self, timeout: timedelta = ...) -> bool: ... + def block_current_stream(self) -> None: ... + def get_future(self) -> Future: ... + def source_rank(self) -> int: ... + def _source_rank(self) -> int: ... + def result(self) -> list[Tensor]: ... + def synchronize(self) -> None: ... + def boxed(self) -> ScriptObject: ... + @staticmethod + def unbox(obj: ScriptObject) -> Work: ... + +class Backend: + class Options: + def __init__(self, backend: str, timeout: timedelta = ...) -> None: ... + @property + def backend(self) -> str: ... + @property + def _timeout(self) -> timedelta: ... + @_timeout.setter + def _timeout(self, val: timedelta) -> None: ... + global_ranks_in_group: list[int] + group_name: GroupName + + def __init__( + self, + rank: int, + size: int, + ) -> None: ... + @property + def supports_splitting(self) -> bool: ... + @property + def supports_coalescing(self) -> bool: ... + @property + def supports_time_estimate(self) -> bool: ... + def set_timeout(self, timeout: timedelta) -> None: ... + @property + def options(self) -> Options: ... + def rank(self) -> int: ... + def size(self) -> int: ... + def name(self) -> str: ... + def abort(self) -> None: ... + def shutdown(self) -> None: ... + def eager_connect_single_device(self, device: torch.device | None) -> None: ... + def _set_sequence_number_for_group(self) -> None: ... + def _set_default_timeout(self, timeout: timedelta) -> None: ... + def get_error(self) -> ErrorType: ... + def supports_tensor_alloc(self, device: torch.device) -> bool: ... + def allocate_tensor( + self, + size: int, + *, + dtype: torch.dtype, + device: torch.device, + ) -> Tensor: ... + @property + def mem_allocator(self) -> Any: ... + +class ProcessGroup: + class BackendType(Enum): + UNDEFINED = ... + GLOO = ... + NCCL = ... + UCC = ... + MPI = ... + XCCL = ... + CUSTOM = ... + + def __init__( + self, + store: Store, + rank: int, + size: int, + ) -> None: ... + def rank(self) -> int: ... + def size(self) -> int: ... + def get_group_store(self) -> Store: ... + def split_group( + self, + new_ranks: list[int], + timeout: timedelta | None = None, + opts: Backend.Options | None = None, + group_name: GroupName | None = None, + group_desc: str | None = None, + ) -> ProcessGroup | None: ... + def merge_remote_group( + self, + store: Store, + size: int, + timeout: timedelta, + group_name: GroupName | None = None, + group_desc: str | None = None, + ) -> ProcessGroup: ... + def abort(self) -> None: ... + def set_timeout(self, timeout: timedelta) -> None: ... + def shutdown(self) -> None: ... + @overload + def broadcast( + self, + tensors: list[Tensor], + opts=..., + ) -> Work: ... + @overload + def broadcast( + self, + tensor: Tensor, + root: int, + timeout: timedelta | None = None, + ) -> Work: ... + @overload + def allreduce( + self, + tensors: list[Tensor], + opts: AllreduceOptions = ..., + ) -> Work: ... + @overload + def allreduce( + self, + tensors: list[Tensor], + op=..., + timeout: timedelta | None = None, + ) -> Work: ... + @overload + def allreduce( + self, + tensor: Tensor, + op=..., + timeout: timedelta | None = None, + ) -> Work: ... + def allreduce_coalesced( + self, + tensors: list[Tensor], + opts=..., + ) -> Work: ... + def reduce_scatter_tensor_coalesced( + self, + outputTensors: list[Tensor], + inputTensors: list[Tensor], + opts: ReduceScatterOptions | None = None, + ) -> Work: ... + @overload + def reduce( + self, + tensors: list[Tensor], + opts=..., + ) -> Work: ... + @overload + def reduce( + self, + tensor: Tensor, + root: int, + op=..., + timeout: timedelta | None = None, + ) -> Work: ... + @overload + def allgather( + self, + output_tensors: list[list[Tensor]], + input_tensors: list[Tensor], + opts=..., + ) -> Work: ... + @overload + def allgather( + self, + output_tensors: list[Tensor], + input_tensor: Tensor, + timeout: timedelta | None = None, + ) -> Work: ... + def _allgather_base( + self, + output: Tensor, + input: Tensor, + opts=..., + ) -> Work: ... + def allgather_coalesced( + self, + output_lists: list[list[Tensor]], + input_list: list[Tensor], + opts=..., + ) -> Work: ... + def allgather_into_tensor_coalesced( + self, + output_lists: list[Tensor], + input_list: list[Tensor], + opts=..., + ) -> Work: ... + @overload + def gather( + self, + output_tensors: list[list[Tensor]], + input_tensors: list[Tensor], + opts=..., + ) -> Work: ... + @overload + def gather( + self, + output_tensors: list[Tensor], + input_tensor: Tensor, + root: int, + timeout: timedelta | None = None, + ) -> Work: ... + @overload + def scatter( + self, + output_tensors: list[Tensor], + input_tensors: list[list[Tensor]], + opts=..., + ) -> Work: ... + @overload + def scatter( + self, + output_tensor: Tensor, + input_tensors: list[Tensor], + root: int, + timeout: timedelta | None = None, + ) -> Work: ... + @overload + def reduce_scatter( + self, + output_tensors: list[Tensor], + input_tensors: list[list[Tensor]], + opts=..., + ) -> Work: ... + @overload + def reduce_scatter( + self, + output_tensors: Tensor, + input_tensor: list[Tensor], + op=..., + timeout: timedelta | None = None, + ) -> Work: ... + def _reduce_scatter_base( + self, + outputTensor: Tensor, + inputTensor: Tensor, + opts: ReduceScatterOptions | None, + ) -> Work: ... + @overload + def alltoall_base( + self, + output_tensor: Tensor, + input_tensor: Tensor, + output_split_sizes: list[int], + input_split_sizes: list[int], + opts=..., + ) -> Work: ... + @overload + def alltoall_base( + self, + output: Tensor, + input: Tensor, + output_split_sizes: list[int], + input_split_sizes: list[int], + timeout: timedelta | None = None, + ) -> Work: ... + @overload + def alltoall( + self, + output_tensor: list[Tensor], + input_tensor: list[Tensor], + opts=..., + ) -> Work: ... + @overload + def alltoall( + self, + output: list[Tensor], + input: list[Tensor], + timeout: timedelta | None = None, + ) -> Work: ... + def send( + self, + tensors: list[Tensor], + dstRank: int, + tag: int, + ) -> Work: ... + def recv( + self, + tensors: list[Tensor], + srcRank: int, + tag: int, + ) -> Work: ... + def recv_anysource(self, tensors: list[Tensor], tag: int) -> Work: ... + @overload + def barrier(self, opts=...) -> Work: ... + @overload + def barrier(self, timeout: timedelta | None = None) -> Work: ... + def boxed(self) -> ScriptObject: ... + @staticmethod + def unbox(obj: ScriptObject) -> ProcessGroup: ... + def _start_coalescing(self, device: torch.device) -> None: ... + def _end_coalescing(self, device: torch.device) -> Work: ... + def _get_backend_name(self) -> str: ... + def _backend_id(self, backend_type: BackendType) -> int: ... + @property + def _device_types(self) -> list[torch.device]: ... + def _get_backend(self, device: torch.device) -> Backend: ... + def _set_default_backend(self, backend_type: BackendType) -> None: ... + def _register_backend( + self, + device: torch.device, + backend_type: BackendType, + backend: Backend | None, + ) -> None: ... + def _set_group_name(self, name: GroupName) -> None: ... + def _set_group_desc(self, desc: str) -> None: ... + def name(self) -> str: ... + def _has_hooks(self) -> bool: ... + def _wait_for_pending_works(self) -> None: ... + def _set_sequence_number_for_group(self) -> None: ... + @property + def bound_device_id(self) -> torch.device | None: ... + @bound_device_id.setter + def bound_device_id(self, device: torch.device | None) -> None: ... + @property + def group_name(self) -> GroupName: ... + @property + def group_desc(self) -> str: ... + +class FakeProcessGroup(Backend): + @staticmethod + def _create_internal(rank: int, world_size: int) -> FakeProcessGroup: ... + +class FakeWork(Work): + seq_id: int + def __init__(self) -> None: ... + def wait(self, timeout: timedelta = ...) -> bool: ... + def getFuture(self) -> Future: ... + +class PythonCallbackWork(Work): + def __init__(self, callback: Callable[[timedelta], bool]) -> None: ... + def wait(self, timeout: timedelta = ...) -> bool: ... + def get_future(self) -> Future: ... + +class ProcessGroupGloo(Backend): + class Device: ... + + class Options(Backend.Options): + devices: list[ProcessGroupGloo.Device] + threads: int + + def __init__(self): ... + + def __init__( + self, + store: Store, + rank: int, + size: int, + timeout: timedelta, + ) -> None: ... + @staticmethod + def create_device(hostname="", interface="", lazy_init=None) -> Device: ... + @staticmethod + def create_default_device(lazy_init=None) -> Device: ... + def _set_default_timeout(self, timeout) -> None: ... + @property + def options(self) -> Options: ... # type: ignore[override] + +class _ProcessGroupWrapper(Backend): + def __init__(self, pg: Backend, gloo_pg: ProcessGroupGloo) -> None: ... + wrapped_pg: Backend + @property + def options(self) -> Backend.Options: ... + def get_error(self) -> ErrorType: ... + +class ErrorType(Enum): + SUCCESS = ... + TIMEOUT = ... + COMM_ERROR = ... + REMOTE_ERROR = ... + +class ProcessGroupNCCL(Backend): + class NCCLConfig: + blocking: int + cga_cluster_size: int + min_ctas: int + max_ctas: int + def unsafe_get_ptr(self) -> int: ... + + class Options(Backend.Options): + config: ProcessGroupNCCL.NCCLConfig + is_high_priority_stream: bool + split_from: ProcessGroupNCCL + split_color: int + + def __init__(self, is_high_priority_stream: bool = False): ... + + def __init__( + self, + store: Store, + rank: int, + size: int, + options: Options, + ) -> None: ... + def _group_start(self) -> None: ... + def _group_end(self) -> None: ... + def _start_time_estimate(self) -> None: ... + def _end_time_estimate(self) -> float: ... + def _set_default_timeout(self, timeout) -> None: ... + def perform_nocolor_split(self, device: torch.device) -> None: ... + def register_mem_pool(self, pool: torch.cuda.MemPool) -> None: ... + def deregister_mem_pool(self, pool: torch.cuda.MemPool) -> None: ... + def comm_split_count(self) -> int: ... + def _add_ephemeral_timeout(self, timeout: timedelta) -> None: ... + def abort(self) -> None: ... + def _is_initialized(self) -> bool: ... + @property + def uid(self) -> int: ... + @property + def options(self) -> Options: ... # type: ignore[override] + @staticmethod + def get_build_nccl_version(self) -> tuple[int, int, int]: ... + @staticmethod + def get_runtime_nccl_version(self) -> tuple[int, int, int]: ... + +class ProcessGroupUCC(Backend): + def __init__( + self, + store: Store, + rank: int, + size: int, + timeout: timedelta, + ) -> None: ... + +class ProcessGroupMPI(Backend): + def __init__( + self, + rank: int, + size: int, + pgComm: int, + ) -> None: ... + @staticmethod + def create(ranks: list[int]) -> ProcessGroupMPI: ... + +def _compute_bucket_assignment_by_size( + tensors: list[Tensor], + bucket_size_limits: list[int], + expect_sparse_gradient: list[bool] = ..., + tensor_indices: list[int] = ..., +) -> tuple[list[list[int]], list[int]]: ... +def _broadcast_coalesced( + process_group: ProcessGroup, + tensors: list[Tensor], + buffer_size: int, + src: int, +): ... +def _test_python_store(store: Store): ... +def _verify_params_across_processes( + process_group: ProcessGroup, + params: list[Tensor], + logger: Logger | None, +): ... +def _make_nccl_premul_sum(factor: float | list[Tensor]) -> ReduceOp: ... +def _register_process_group( + group_name: GroupName, + process_group: ProcessGroup, +) -> None: ... +def _resolve_process_group(group_name: GroupName) -> ProcessGroup: ... +def _register_work(tensor: torch.Tensor, work: Work) -> ProcessGroup: ... +def _get_work_registry_size() -> int: ... +def _set_allow_inflight_collective_as_graph_input( + value: bool, +) -> None: ... +def _allow_inflight_collective_as_graph_input() -> bool: ... +def _unregister_all_process_groups() -> None: ... +def _unregister_process_group(group_name: GroupName) -> None: ... + +# Initializes the device state in CUmodule so that it's able to perform NVSHMEM +# operations. CUmodule is a pointer to a CUDA module, carried by a int64 in +# Python. At C++ interface, it is converted to a uintptr_t. +def _nvshmemx_cumodule_init(module: int) -> None: ... + +# Check if NVSHMEM is available on current system. +def _is_nvshmem_available() -> bool: ... + +class _SymmetricMemory: + @staticmethod + def set_group_info( + group_name: str, + rank: int, + world_size: int, + store: Store, + ) -> None: ... + @staticmethod + def empty_strided_p2p( + size: torch.types._size, + stride: torch.types._size, + dtype: torch.dtype, + device: torch.device, + group_name: str | None = None, + alloc_id: int | None = None, + ) -> torch.Tensor: ... + @staticmethod + def has_multicast_support( + device_type: DeviceType, + device_idx: int, + ) -> bool: ... + # Set Symmetric Memory allocation backend. + @staticmethod + def set_backend(name: str) -> None: ... + @staticmethod + def get_backend(device: torch.device) -> str | None: ... + @staticmethod + def is_symm_mem_tensor(tensor: torch.Tensor) -> bool: ... + @staticmethod + def get_mempool_allocator(device: torch.device) -> Any: ... + signal_pad_size: int + @property + def rank(self) -> int: ... + @property + def world_size(self) -> int: ... + @staticmethod + def rendezvous( + tensor: torch.Tensor, group_name: str | None = None + ) -> _SymmetricMemory: ... + def get_buffer( + self, + rank: int, + sizes: torch.types._size, + dtype: torch.dtype, + storage_offset: int | None = 0, + ) -> torch.Tensor: ... + def get_signal_pad( + self, + rank: int, + sizes: torch.types._size = [], + dtype: torch.dtype | None = None, + storage_offset: int | None = 0, + ) -> torch.Tensor: ... + def barrier(self, channel: int = 0, timeout_ms: int = 0) -> None: ... + def put_signal( + self, + dst_rank: int, + channel: int = 0, + timeout_ms: int = 0, + ) -> None: ... + def wait_signal( + self, + src_rank: int, + channel: int = 0, + timeout_ms: int = 0, + ) -> None: ... + def get_remote_tensor( + self, + peer: int, + sizes: torch.types._size, + dtype: torch.dtype, + ) -> torch.Tensor: ... + @staticmethod + def memset32( + tensor: torch.Tensor, offset: int, val: int, count: int = 1 + ) -> torch.Tensor: ... + @staticmethod + def stream_write_value32( + tensor: torch.Tensor, offset: int, val: int + ) -> torch.Tensor: ... + @property + def buffer_ptrs(self) -> list[int]: ... + @property + def buffer_ptrs_dev(self) -> int: ... + @property + def signal_pad_ptrs(self) -> list[int]: ... + @property + def signal_pad_ptrs_dev(self) -> int: ... + @property + def multicast_ptr(self) -> int: ... + @property + def buffer_size(self) -> int: ... + @property + def device(self) -> torch.device: ... + +class ProcessGroupXCCL(Backend): + class Options(Backend.Options): + is_high_priority_stream: bool + + def __init__(self, is_high_priority_stream: bool = False): ... + + def __init__( + self, + store: Store, + rank: int, + size: int, + options: Options, + ) -> None: ... + @property + def options(self) -> Options: ... # type: ignore[override] + +def _set_process_group(pg: ProcessGroup) -> None: ... +def _current_process_group() -> ProcessGroup: ... + +class _Request: + def body(self) -> bytes: ... + def get_param(self, str) -> str: ... + +class _Response: + def set_content(self, content: str | bytes, content_type: str) -> None: ... + def set_status(self, status: int) -> None: ... + +def _register_handler( + name: str, handler: Callable[[_Request, _Response], None] +) -> None: ... +def _set_comm_profiling_name(name: str) -> None: ... +def _get_comm_profiling_name() -> str: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_distributed_rpc.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_distributed_rpc.pyi new file mode 100644 index 0000000000000000000000000000000000000000..48f636d8524637305cd9c758e9f22428d3b055cc --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_distributed_rpc.pyi @@ -0,0 +1,188 @@ +# mypy: allow-untyped-defs +# mypy: disable-error-code="type-arg" +from datetime import timedelta +from typing import Any, Generic, overload, TypeVar + +import torch +from torch._C import Future +from torch._C._autograd import ProfilerEvent +from torch._C._distributed_c10d import Store +from torch._C._profiler import ProfilerConfig + +# This module is defined in torch/csrc/distributed/rpc/init.cpp + +_DEFAULT_INIT_METHOD: str +_DEFAULT_NUM_WORKER_THREADS: int +_UNSET_RPC_TIMEOUT: float +_DEFAULT_RPC_TIMEOUT_SEC: float + +_T = TypeVar("_T") + +class RpcBackendOptions: + rpc_timeout: float + init_method: str + def __init__( + self, + rpc_timeout: float = ..., + init_method: str = ..., + ) -> None: ... + +class WorkerInfo: + def __init__(self, name: str, worker_id: int) -> None: ... + @property + def name(self) -> str: ... + @property + def id(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + +class RpcAgent: + def join(self, shutdown: bool = False, timeout: float = 0): ... + def sync(self): ... + def shutdown(self): ... + @overload + def get_worker_info(self) -> WorkerInfo: ... + @overload + def get_worker_info(self, workerName: str) -> WorkerInfo: ... + def get_worker_infos(self) -> list[WorkerInfo]: ... + def _get_device_map(self, dst: WorkerInfo) -> dict[torch.device, torch.device]: ... + def get_debug_info(self) -> dict[str, str]: ... + def get_metrics(self) -> dict[str, str]: ... + +class PyRRef(Generic[_T]): + def __init__(self, value: _T, type_hint: Any = None) -> None: ... + def is_owner(self) -> bool: ... + def confirmed_by_owner(self) -> bool: ... + def owner(self) -> WorkerInfo: ... + def owner_name(self) -> str: ... + def to_here(self, timeout: float = ...) -> _T: ... + def local_value(self) -> Any: ... + def rpc_sync(self, timeout: float = ...) -> Any: ... + def rpc_async(self, timeout: float = ...) -> Any: ... + def remote(self, timeout: float = ...) -> Any: ... + def _serialize(self) -> tuple: ... + @staticmethod + def _deserialize(tp: tuple) -> PyRRef: ... + def _get_type(self) -> type[_T]: ... + def _get_future(self) -> Future[_T]: ... + def _get_profiling_future(self) -> Future[_T]: ... + def _set_profiling_future(self, profilingFuture: Future[_T]): ... + +class _TensorPipeRpcBackendOptionsBase(RpcBackendOptions): + num_worker_threads: int + device_maps: dict[str, dict[torch.device, torch.device]] + devices: list[torch.device] + def __init__( + self, + num_worker_threads: int, + _transports: list | None, + _channels: list | None, + rpc_timeout: float = ..., + init_method: str = ..., + device_maps: dict[str, dict[torch.device, torch.device]] = {}, # noqa: B006 + devices: list[torch.device] = [], # noqa: B006 + ) -> None: ... + def _set_device_map( + self, + to: str, + device_map: dict[torch.device, torch.device], + ): ... + +class TensorPipeAgent(RpcAgent): + def __init__( + self, + store: Store, + name: str, + worker_id: int, + world_size: int | None, + opts: _TensorPipeRpcBackendOptionsBase, + reverse_device_maps: dict[str, dict[torch.device, torch.device]], + devices: list[torch.device], + ) -> None: ... + def join(self, shutdown: bool = False, timeout: float = 0): ... + def shutdown(self): ... + @overload + def get_worker_info(self) -> WorkerInfo: ... + @overload + def get_worker_info(self, workerName: str) -> WorkerInfo: ... + @overload + def get_worker_info(self, id: int) -> WorkerInfo: ... + def get_worker_infos(self) -> list[WorkerInfo]: ... + def _get_device_map(self, dst: WorkerInfo) -> dict[torch.device, torch.device]: ... + def _update_group_membership( + self, + worker_info: WorkerInfo, + my_devices: list[torch.device], + reverse_device_map: dict[str, dict[torch.device, torch.device]], + is_join: bool, + ): ... + def _get_backend_options(self) -> _TensorPipeRpcBackendOptionsBase: ... + @property + def is_static_group(self) -> bool: ... + @property + def store(self) -> Store: ... + +def _is_current_rpc_agent_set() -> bool: ... +def _get_current_rpc_agent() -> RpcAgent: ... +def _set_and_start_rpc_agent(agent: RpcAgent): ... +def _reset_current_rpc_agent(): ... +def _delete_all_user_and_unforked_owner_rrefs(timeout: timedelta = ...): ... +def _destroy_rref_context(ignoreRRefLeak: bool): ... +def _rref_context_get_debug_info() -> dict[str, str]: ... +def _cleanup_python_rpc_handler(): ... +def _invoke_rpc_builtin( + dst: WorkerInfo, + opName: str, + rpcTimeoutSeconds: float, + *args: Any, + **kwargs: Any, +): ... +def _invoke_rpc_python_udf( + dst: WorkerInfo, + pickledPythonUDF: str, + tensors: list[torch.Tensor], + rpcTimeoutSeconds: float, + isAsyncExecution: bool, +): ... +def _invoke_rpc_torchscript( + dstWorkerName: str, + qualifiedNameStr: str, + argsTuple: tuple, + kwargsDict: dict, + rpcTimeoutSeconds: float, + isAsyncExecution: bool, +): ... +def _invoke_remote_builtin( + dst: WorkerInfo, + opName: str, + rpcTimeoutSeconds: float, + *args: Any, + **kwargs: Any, +): ... +def _invoke_remote_python_udf( + dst: WorkerInfo, + pickledPythonUDF: str, + tensors: list[torch.Tensor], + rpcTimeoutSeconds: float, + isAsyncExecution: bool, +): ... +def _invoke_remote_torchscript( + dstWorkerName: WorkerInfo, + qualifiedNameStr: str, + rpcTimeoutSeconds: float, + isAsyncExecution: bool, + *args: Any, + **kwargs: Any, +): ... +def get_rpc_timeout() -> float: ... +def enable_gil_profiling(flag: bool): ... +def _set_rpc_timeout(rpcTimeoutSeconds: float): ... + +class RemoteProfilerManager: + @staticmethod + def set_current_profiling_key(key: str): ... + +def _enable_server_process_global_profiler(new_config: ProfilerConfig): ... +def _disable_server_process_global_profiler() -> list[list[list[ProfilerEvent]]]: ... +def _set_profiler_node_id(default_node_id: int): ... +def _enable_jit_rref_pickle(): ... +def _disable_jit_rref_pickle(): ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_distributed_rpc_testing.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_distributed_rpc_testing.pyi new file mode 100644 index 0000000000000000000000000000000000000000..9313281027dbd89ae65ebe65c95a1d0c38593b80 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_distributed_rpc_testing.pyi @@ -0,0 +1,32 @@ +import torch +from torch._C._distributed_c10d import Store +from torch._C._distributed_rpc import _TensorPipeRpcBackendOptionsBase, TensorPipeAgent + +# This module is defined in torch/csrc/distributed/rpc/testing/init.cpp + +class FaultyTensorPipeRpcBackendOptions(_TensorPipeRpcBackendOptionsBase): + def __init__( + self, + num_worker_threads: int, + rpc_timeout: float, + init_method: str, + messages_to_fail: list[str], + messages_to_delay: dict[str, float], + num_fail_sends: int, + ) -> None: ... + num_send_recv_threads: int + messages_to_fail: list[str] + messages_to_delay: dict[str, float] + num_fail_sends: int + +class FaultyTensorPipeAgent(TensorPipeAgent): + def __init__( + self, + store: Store, + name: str, + rank: int, + world_size: int, + options: FaultyTensorPipeRpcBackendOptions, + reverse_device_maps: dict[str, dict[torch.device, torch.device]], + devices: list[torch.device], + ) -> None: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_dynamo/__init__.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_dynamo/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..ab9470c2afac94c98044d24a452d54bd7d4b44f9 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_dynamo/__init__.pyi @@ -0,0 +1,69 @@ +from . import compiled_autograd, eval_frame, guards # noqa: F401 + +def strip_function_call(name: str) -> str: ... +def is_valid_var_name(name: str) -> bool | int: ... +def get_type_slots(obj: type | object) -> tuple[int, int, int, int]: ... +def has_slot(slots: int, slot_bit: int) -> bool: ... + +class PySequenceSlots: + SQ_LENGTH: int + SQ_CONCAT: int + SQ_REPEAT: int + SQ_ITEM: int + SQ_CONTAINS: int + SQ_ASS_ITEM: int + SQ_INPLACE_CONCAT: int + SQ_INPLACE_REPEAT: int + +class PyMappingSlots: + MP_LENGTH: int + MP_SUBSCRIPT: int + MP_ASS_SUBSCRIPT: int + +class PyNumberSlots: + NB_ADD: int + NB_SUBTRACT: int + NB_MULTIPLY: int + NB_REMAINDER: int + NB_POWER: int + NB_NEGATIVE: int + NB_POSITIVE: int + NB_ABSOLUTE: int + NB_BOOL: int + NB_INVERT: int + NB_LSHIFT: int + NB_RSHIFT: int + NB_AND: int + NB_XOR: int + NB_OR: int + NB_INT: int + NB_FLOAT: int + NB_INPLACE_ADD: int + NB_INPLACE_SUBTRACT: int + NB_INPLACE_MULTIPLY: int + NB_INPLACE_REMAINDER: int + NB_INPLACE_POWER: int + NB_INPLACE_LSHIFT: int + NB_INPLACE_RSHIFT: int + NB_INPLACE_AND: int + NB_INPLACE_XOR: int + NB_INPLACE_OR: int + NB_FLOOR_DIVIDE: int + NB_TRUE_DIVIDE: int + NB_INPLACE_FLOOR_DIVIDE: int + NB_INPLACE_TRUE_DIVIDE: int + NB_INDEX: int + NB_MATRIX_MULTIPLY: int + NB_INPLACE_MATRIX_MULTIPLY: int + +class PyTypeSlots: + TP_HASH: int + TP_ITER: int + TP_ITERNEXT: int + TP_CALL: int + TP_REPR: int + TP_RICHCOMPARE: int + TP_GETATTRO: int + TP_SETATTRO: int + TP_DESCR_GET: int + TP_DESCR_SET: int diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_dynamo/compiled_autograd.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_dynamo/compiled_autograd.pyi new file mode 100644 index 0000000000000000000000000000000000000000..ef24582b5023109733955cc77db0a84fae03b3fd --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_dynamo/compiled_autograd.pyi @@ -0,0 +1,13 @@ +from collections.abc import Callable + +from torch import Tensor +from torch._dynamo.compiled_autograd import AutogradCompilerInstance + +def set_autograd_compiler( + autograd_compiler: Callable[[], AutogradCompilerInstance] | None, + dynamic: bool, +) -> tuple[Callable[[], AutogradCompilerInstance] | None, bool]: ... +def clear_cache() -> None: ... +def is_cache_empty() -> bool: ... +def set_verbose_logger(fn: Callable[[str], None] | None) -> bool: ... +def call_cpp_tensor_pre_hooks(idx: int, grad: Tensor) -> Tensor: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_dynamo/eval_frame.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_dynamo/eval_frame.pyi new file mode 100644 index 0000000000000000000000000000000000000000..a57eae9fd877966b76607f93fe9213c26680d4db --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_dynamo/eval_frame.pyi @@ -0,0 +1,99 @@ +import enum +import types +from collections.abc import Callable +from typing import overload + +from torch._dynamo.guards import GuardManagerWrapper +from torch._dynamo.types import DynamoCallback, DynamoGuardCompleteHook, DynamoGuardHook +from torch._guards import CompileId + +def set_eval_frame(callback: DynamoCallback) -> DynamoCallback: ... +def set_skip_guard_eval_unsafe(value: bool) -> bool: ... +def get_eval_frame_callback() -> DynamoCallback: ... +def reset_code(code: types.CodeType) -> None: ... +def unsupported(obj1: object, obj2: object) -> object: ... +def set_code_exec_strategy( + code: types.CodeType, strategy: _FrameExecStrategy +) -> None: ... +def set_guard_error_hook(hook: DynamoGuardHook) -> None: ... +def set_guard_complete_hook( + hook: DynamoGuardCompleteHook | None, +) -> DynamoGuardCompleteHook | None: ... +def raise_sigtrap() -> None: ... +def set_c_recursion_limit(limit: int) -> None: ... +def get_c_recursion_limit() -> int: ... + +class _CacheEntry: + def check_fn(self, *args: object, **kwargs: object) -> bool: ... + def update_diff_guard_root_manager(self) -> None: ... + code: types.CodeType + compile_id: CompileId + # If we run into circular issues, just use object + guard_manager: GuardManagerWrapper + backend: Callable + next: _CacheEntry | None + +class _PrecompileEntry: + guard_manager: GuardManagerWrapper + +class _ExtraState: + def invalidate( + self, cache_entry: _CacheEntry, guard_manager: GuardManagerWrapper + ) -> None: ... + +class _FrameAction(enum.IntEnum): + DEFAULT = 0 + SKIP = 1 + RUN_ONLY = 2 + +class _FrameExecStrategy: + cur_action: _FrameAction + recursive_action: _FrameAction + + @overload + def __init__(self) -> None: ... + @overload + def __init__( + self, cur_action: _FrameAction, recursive_action: _FrameAction + ) -> None: ... + +# This is an object that encapsulates the Python FrameType, and exposes +# properties Dynamo cares about for a frame. +class _PyInterpreterFrame: + f_code: types.CodeType + f_locals: dict[str, object] + f_globals: dict[str, object] + f_builtins: dict[str, object] + f_lasti: int + f_lineno: int + f_back: types.FrameType + # A tuple containing cell objects captured by this frame. + closure: tuple[types.CellType] + +def _debug_get_cache_entry_list(code: types.CodeType) -> list[_CacheEntry]: ... +def _get_frame_value_stack_with_depth( + frame: types.FrameType, depth: int +) -> list[object]: ... +def set_bytecode_debugger_callback( + callback: Callable[[types.CodeType], None] | None, +) -> None: ... +def get_bytecode_debugger_callback() -> Callable[[types.CodeType], None] | None: ... +def register_breakpoint_code(code: types.CodeType) -> None: ... + +# Sentinel for NULL stack values returned by _get_frame_value_stack_at_depth +class NullStackValue: ... + +NULL_STACK_VALUE: NullStackValue + +py_opcode_caches: list[int] + +def code_framelocals_names(code: types.CodeType) -> tuple[str, ...]: ... +def _load_precompile_entry( + code: types.CodeType, + guard_manager: GuardManagerWrapper, + dynamo_code: types.CodeType, +) -> None: ... +def _reset_precompile_entries(code: types.CodeType) -> None: ... +def _debug_get_precompile_entries(code: types.CodeType) -> list[_PrecompileEntry]: ... +def set_fullgraph_compiled_frame_count(value: int) -> int: ... +def set_fullgraph_error_on_nested_compile(value: bool) -> bool: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_dynamo/guards.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_dynamo/guards.pyi new file mode 100644 index 0000000000000000000000000000000000000000..10845355e5bfa1c85b8564e1653eb742face5e94 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_dynamo/guards.pyi @@ -0,0 +1,500 @@ +import enum +import traceback +from collections.abc import Callable +from typing import Any, TypeAlias + +import torch + +# TODO: We should move the `GuardManagerType` +# defined in `guards.py` here and update other +# imports +GuardManagerType: TypeAlias = enum.Enum + +class GlobalStateGuard: + def check(self) -> bool: ... + def reason(self) -> str: ... + +class LeafGuard: + def verbose_code_parts(self) -> list[str]: ... + +class RelationalGuard: ... + +class GuardDebugInfo: + verbose_code_parts: list[str] + result: bool + num_guards_executed: int + user_stack: traceback.StackSummary | None + +class GuardManager: + def check(self, value: Any) -> bool: ... + def check_verbose(self, value: Any) -> GuardDebugInfo: ... + + # Accessors + def globals_dict_manager( + self, + f_globals: dict[str, Any], + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def framelocals_manager( + self, + key: tuple[str, int], + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def dict_getitem_manager( + self, + key: Any, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def grad_manager( + self, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def generic_getattr_manager( + self, + attr: str, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def getitem_manager( + self, + key: Any, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def get_generic_dict_manager( + self, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def list_getitem_manager( + self, + key: Any, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def tuple_getitem_manager( + self, + key: Any, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def set_getitem_manager( + self, + index: Any, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def func_defaults_manager( + self, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def func_kwdefaults_manager( + self, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def tuple_iterator_getitem_manager( + self, + index: Any, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def weakref_call_manager( + self, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def call_function_no_args_manager( + self, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def global_weakref_manager( + self, + global_name: str, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def type_manager( + self, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def getattr_manager( + self, + attr: str, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def tensor_property_size_manager( + self, + idx: int, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def tensor_property_shape_manager( + self, + idx: int, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def tensor_property_storage_offset_manager( + self, + idx: int, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def indexed_manager( + self, + idx: int, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def lambda_manager( + self, + python_lambda: Callable[..., Any], + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def get_root(self) -> RootGuardManager: ... + def get_source(self) -> str: ... + def fail_count(self) -> int: ... + def get_child_managers(self) -> list[GuardManager]: ... + def repr(self) -> str: ... + def type_of_guarded_value(self) -> str: ... + def get_leaf_guards(self) -> list[LeafGuard]: ... + def get_accessors(self) -> list[GuardManager]: ... + def is_guarded_value_immutable(self) -> bool: ... + def is_tag_safe(self) -> bool: ... + def is_tag_safe_root(self) -> bool: ... + def has_no_accessors(self) -> bool: ... + def has_object_aliasing_guard(self) -> bool: ... + def get_type_of_guarded_value(self) -> type: ... + def type_dict_manager( + self, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def type_mro_manager( + self, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def code_manager( + self, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def closure_manager( + self, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + # Leaf guards + def add_lambda_guard( + self, + user_lambda: Callable[..., Any], + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_id_match_guard( + self, + id_val: int, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_equals_match_guard( + self, + equals_val: Any, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_global_state_guard( + self, + initial_state: Any, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_torch_function_mode_stack_guard( + self, + initial_stack: list[Any], + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_mapping_keys_guard( + self, + value: Any, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_dict_length_check_guard( + self, + value: int, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_length_check_guard( + self, + value: int, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_true_match_guard( + self, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_false_match_guard( + self, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_none_match_guard( + self, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_not_none_guard( + self, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_dispatch_key_set_guard( + self, + dispatch_key: Any, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_tensor_match_guard( + self, + value: Any, + sizes: list[int], + strides: list[int], + tensor_name: str, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ptype: Any, + dispatch_keys: Any, + ) -> None: ... + def add_dynamic_indices_guard( + self, + value: set[Any], + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_no_hasattr_guard( + self, + attr_name: str, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_dict_contains_guard( + self, + contains: bool, + key: Any, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_type_match_guard( + self, + value: int, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_dict_version_guard( + self, + value: Any, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_set_contains_guard( + self, + contains: bool, + item: Any, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_dual_level_match_guard( + self, + level: int, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_float_is_nan_guard( + self, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_complex_is_nan_guard( + self, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_tuple_iterator_length_guard( + self, + length: int, + type_id: int, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_range_iterator_match_guard( + self, + start: int, + stop: int, + step: int, + type_id: int, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def add_default_device_guard( + self, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def mark_tag_safe(self) -> None: ... + def mark_tag_safe_root(self) -> None: ... + +class RootGuardManager(GuardManager): + def get_epilogue_lambda_guards(self) -> list[LeafGuard]: ... + def add_epilogue_lambda_guard( + self, + guard: LeafGuard, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, + ) -> None: ... + def clone_manager( + self, clone_filter_fn: Callable[[GuardManager], bool] + ) -> RootGuardManager: ... + def attach_compile_id(self, compile_id: str) -> None: ... + +class DictGuardManager(GuardManager): + def get_key_manager( + self, + index: int, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def get_value_manager( + self, + index: int, + source: str, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: ... + def get_key_value_managers( + self, + ) -> dict[int, tuple[GuardManager, GuardManager]]: ... + +# Guard accessor stubs +class GuardAccessor: ... +class DictGetItemGuardAccessor(GuardAccessor): ... +class GetGenericDictGuardAccessor(GuardAccessor): ... +class TypeDictGuardAccessor(GuardAccessor): ... +class TypeMROGuardAccessor(GuardAccessor): ... +class ClosureGuardAccessor(GuardAccessor): ... +class TupleGetItemGuardAccessor(GuardAccessor): ... +class TypeGuardAccessor(GuardAccessor): ... +class CodeGuardAccessor(GuardAccessor): ... +class FuncDefaultsGuardAccessor(GuardAccessor): ... +class FuncKwDefaultsGuardAccessor(GuardAccessor): ... + +class GetAttrGuardAccessor(GuardAccessor): + def get_attr_name(self) -> str: ... + +def install_object_aliasing_guard( + x: GuardManager, + y: GuardManager, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, +) -> None: ... +def install_no_tensor_aliasing_guard( + guard_managers: list[GuardManager], + tensor_names: list[str], + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, +) -> None: ... +def install_storage_overlapping_guard( + overlapping_guard_managers: list[GuardManager], + non_overlapping_guard_managers: list[GuardManager], + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, +) -> None: ... +def install_symbolic_shape_guard( + guard_managers: list[GuardManager], + nargs_int: int, + nargs_float: int, + py_addr: int, + py_addr_keep_alive: Any, + verbose_code_parts: list[str], + user_stack: traceback.StackSummary | None, +) -> None: ... +def profile_guard_manager( + guard_manager: GuardManager, + f_locals: dict[str, Any], + n_iters: int, +) -> float: ... + +class TensorGuards: + def __init__( + self, + *, + dynamic_dims_sizes: list[torch.SymInt | None] | None = None, + dynamic_dims_strides: list[torch.SymInt | None] | None = None, + ) -> None: ... + def check(self, *args: Any) -> bool: ... + def check_verbose( + self, *args: Any, tensor_check_names: list[str] | None = None + ) -> bool | str: ... + +def assert_size_stride( + item: torch.Tensor, + size: torch.types._size, + stride: torch.types._size, + op_name: str | None = None, +) -> None: ... +def assert_alignment( + item: torch.Tensor, + alignment: int, + op_name: str | None = None, +) -> None: ... +def copy_misaligned(item: torch.Tensor) -> torch.Tensor: ... +def check_obj_id(obj: object, expected: int) -> bool: ... +def check_type_id(obj: object, expected: int) -> bool: ... +def dict_version(d: dict[Any, Any]) -> int: ... +def compute_overlapping_tensors( + tensors: list[torch.Tensor], symbolic: bool = True +) -> set[int]: ... +def set_is_in_mode_without_ignore_compile_internals(value: bool) -> None: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_export/__init__.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_export/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..039f9c22eea620bc9675d233684df72c7ac4471c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_export/__init__.pyi @@ -0,0 +1,9 @@ +# Defined in torch/csrc/export/pybind.cpp +class CppExportedProgram: ... + +def deserialize_exported_program( + serialized_program: str, +) -> CppExportedProgram: ... +def serialize_exported_program( + cpp_exported_program: CppExportedProgram, +) -> str: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_export/pt2_archive_constants.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_export/pt2_archive_constants.pyi new file mode 100644 index 0000000000000000000000000000000000000000..f7a92ddd0c961513d42949e8c2c4b18fcadcc8cc --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_export/pt2_archive_constants.pyi @@ -0,0 +1,25 @@ +# Defined in torch/csrc/export/pt2_archive_constants.h + +ARCHIVE_ROOT_NAME: str = ... +ARCHIVE_FORMAT_PATH: str = ... +ARCHIVE_FORMAT_VALUE: str = ... +ARCHIVE_VERSION_PATH: str = ... +ARCHIVE_VERSION_VALUE: str = ... +MODELS_DIR: str = ... +MODELS_FILENAME_FORMAT: str = ... +AOTINDUCTOR_DIR: str = ... +MTIA_DIR: str = ... +WEIGHTS_DIR: str = ... +WEIGHTS_CONFIG_FILENAME_FORMAT: str = ... +WEIGHT_FILENAME_PREFIX: str = ... +CONSTANTS_DIR: str = ... +CONSTANTS_CONFIG_FILENAME_FORMAT: str = ... +TENSOR_CONSTANT_FILENAME_PREFIX: str = ... +CUSTOM_OBJ_FILENAME_PREFIX: str = ... +SAMPLE_INPUTS_DIR: str = ... +SAMPLE_INPUTS_FILENAME_FORMAT: str = ... +EXECUTORCH_DIR: str = ... +EXTRA_DIR: str = ... +MODULE_INFO_PATH: str = ... +XL_MODEL_WEIGHTS_DIR: str = ... +XL_MODEL_WEIGHTS_PARAM_CONFIG_PATH: str = ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_functionalization.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_functionalization.pyi new file mode 100644 index 0000000000000000000000000000000000000000..4e00df97e2717c1d3c63db3fc42e31507211eb99 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_functionalization.pyi @@ -0,0 +1,16 @@ +from torch import Tensor +from torch.types import _bool + +# Defined in torch/csrc/functionalization/Module.cpp + +class ViewMeta: + has_symbolic_inputs: _bool + +# Returns the list of ViewMeta instances of the given functional tensor. +# +# Although we do have python bindings for their types, we won't +# expose them here, since they should not be used by users. +def get_view_meta_sequence(tensor: Tensor) -> list[ViewMeta]: ... + +# Applies the ViewMeta sequence on top of the given base. +def apply_view_meta_sequence(base: Tensor, sequence: list[ViewMeta]) -> Tensor: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_functions.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_functions.pyi new file mode 100644 index 0000000000000000000000000000000000000000..5b0dee51a71093f1245887aa13c2405d3f2e5720 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_functions.pyi @@ -0,0 +1,19 @@ +from typing import AnyStr, overload + +from torch import Tensor + +class UndefinedGrad: + def __init__(self) -> None: ... + def __call__(self, *inputs: Tensor) -> list[Tensor]: ... + +class DelayedError: + def __init__(self, msg: AnyStr, num_inputs: int) -> None: ... + + # __call__ should really be a higher-kinded type: + # def __call__(self, arg: Tensor) -> Tensor: ... + # def __call__(self, *args: Tensor * num_inputs) -> Tuple[Tensor * num_inputs]: ... + + @overload + def __call__(self, i0: Tensor) -> Tensor: ... + @overload + def __call__(self, *args: Tensor) -> tuple[Tensor, ...]: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_functorch.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_functorch.pyi new file mode 100644 index 0000000000000000000000000000000000000000..a566122e5393a8bdcb79293644e25f7195c290ae --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_functorch.pyi @@ -0,0 +1,100 @@ +# mypy: allow-untyped-defs +from enum import Enum + +from torch import Tensor + +# Defined in torch/csrc/functorch/init.cpp + +def _assert_wrapped_functional( + input_tensor: Tensor, wrapped_tensor: Tensor +) -> None: ... +def _func_decrement_nesting() -> int: ... +def _func_increment_nesting(reapply_views: bool) -> int: ... +def _propagate_functional_input_mutation( + input_tensor: Tensor, wrapped_tensor: Tensor +) -> None: ... +def set_inplace_requires_grad_allowed(allowed: bool) -> None: ... +def get_inplace_requires_grad_allowed() -> bool: ... +def _set_dynamic_layer_keys_included(included: bool) -> None: ... +def get_unwrapped(tensor: Tensor) -> Tensor: ... +def is_batchedtensor(tensor: Tensor) -> bool: ... +def is_functionaltensor(tensor: Tensor) -> bool: ... +def is_functorch_wrapped_tensor(tensor: Tensor) -> bool: ... +def is_gradtrackingtensor(tensor: Tensor) -> bool: ... +def is_legacy_batchedtensor(tensor: Tensor) -> bool: ... +def maybe_get_bdim(tensor: Tensor) -> int: ... +def maybe_get_level(tensor: Tensor) -> int: ... +def maybe_current_level() -> int | None: ... +def unwrap_if_dead(tensor: Tensor) -> Tensor: ... +def _unwrap_for_grad(tensor: Tensor, level: int) -> Tensor: ... +def _wrap_for_grad(tensor: Tensor, level: int) -> Tensor: ... +def _unwrap_batched(tensor: Tensor, level: int) -> tuple[Tensor, int | None]: ... +def current_level() -> int: ... +def count_jvp_interpreters() -> int: ... +def _add_batch_dim(tensor: Tensor, bdim: int, level: int) -> Tensor: ... +def _remove_batch_dim( + tensor: Tensor, level: int, batch_size: int, out_dim: int +) -> Tensor: ... +def _maybe_unsafe_set_level(tensor: Tensor, level: int) -> None: ... +def set_single_level_autograd_function_allowed(allowed: bool) -> None: ... +def get_single_level_autograd_function_allowed() -> bool: ... +def _unwrap_functional_tensor(tensor: Tensor, reapply_views: bool) -> Tensor: ... +def _wrap_functional_tensor(tensor: Tensor, level: int) -> Tensor: ... +def _vmap_increment_nesting(batch_size: int, randomness: str) -> int: ... +def _vmap_decrement_nesting() -> int: ... +def _grad_increment_nesting() -> int: ... +def _grad_decrement_nesting() -> int: ... +def _jvp_increment_nesting() -> int: ... +def _jvp_decrement_nesting() -> int: ... + +# Defined in aten/src/ATen/functorch/Interpreter.h +class TransformType(Enum): + Torch = ... + Vmap = ... + Grad = ... + Jvp = ... + Functionalize = ... + +class RandomnessType(Enum): + Error = ... + Same = ... + Different = ... + +class CInterpreter: + def key(self) -> TransformType: ... + def level(self) -> int: ... + def serialize(self) -> bytes: ... + @staticmethod + def deserialize(bytes) -> CInterpreter: ... + +class CGradInterpreterPtr: + def __init__(self, interpreter: CInterpreter) -> None: ... + def lift(self, Tensor) -> Tensor: ... + def prevGradMode(self) -> bool: ... + +class CJvpInterpreterPtr: + def __init__(self, interpreter: CInterpreter) -> None: ... + def lift(self, Tensor) -> Tensor: ... + def prevFwdGradMode(self) -> bool: ... + +class CFunctionalizeInterpreterPtr: + def __init__(self, interpreter: CInterpreter) -> None: ... + def key(self) -> TransformType: ... + def level(self) -> int: ... + def functionalizeAddBackViews(self) -> bool: ... + +class CVmapInterpreterPtr: + def __init__(self, interpreter: CInterpreter) -> None: ... + def key(self) -> TransformType: ... + def level(self) -> int: ... + def batchSize(self) -> int: ... + def randomness(self) -> RandomnessType: ... + +class DynamicLayer: ... + +def get_dynamic_layer_stack_depth() -> int: ... +def get_interpreter_stack() -> list[CInterpreter]: ... +def peek_interpreter_stack() -> CInterpreter: ... +def pop_dynamic_layer_stack() -> DynamicLayer: ... +def pop_dynamic_layer_stack_and_undo_to_depth(int) -> None: ... +def push_dynamic_layer_stack(dl: DynamicLayer) -> int: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_instruction_counter.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_instruction_counter.pyi new file mode 100644 index 0000000000000000000000000000000000000000..4e3c27567eb228b8763a6d2578db827b1ffbde41 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_instruction_counter.pyi @@ -0,0 +1,4 @@ +# Defined in torch/csrc/instruction_counter/Module.cpp + +def start() -> int: ... +def end(id: int) -> int: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_itt.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_itt.pyi new file mode 100644 index 0000000000000000000000000000000000000000..8a54437f527b994821133532f26598c951631b28 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_itt.pyi @@ -0,0 +1,5 @@ +# Defined in torch/csrc/itt.cpp +def is_available() -> None: ... +def rangePush(message: str) -> None: ... +def rangePop() -> None: ... +def mark(message: str) -> None: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_jit_tree_views.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_jit_tree_views.pyi new file mode 100644 index 0000000000000000000000000000000000000000..bc7f7c3b7c1e943a33b68f0e8af87e70312fdc52 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_jit_tree_views.pyi @@ -0,0 +1,200 @@ +from typing import Any + +# Defined in torch/csrc/jit/python/python_tree_views.cpp + +class SourceRange: + def highlight(self) -> str: ... + @property + def start(self) -> int: ... + @property + def end(self) -> int: ... + +class SourceRangeFactory: + def __init__( + self, + text: str, + filename: Any, + file_lineno: int, + leading_whitespace_chars: int, + ) -> None: ... + def make_range(self, line: int, start_col: int, end_col: int) -> SourceRange: ... + def make_raw_range(self, start: int, end: int) -> SourceRange: ... + @property + def source(self) -> str: ... + +class TreeView: + def range(self) -> SourceRange: ... + def dump(self) -> None: ... + +class Ident(TreeView): + def __init__(self, *args: Any, **kwargs: Any) -> None: ... + @property + def name(self) -> str: ... + +class Param(TreeView): + def __init__(self, type: Any | None, name: Ident, kwarg_only: bool) -> None: ... + +class Attribute(TreeView): + def __init__(self, name: Ident, value: Any) -> None: ... + +# Literals +def TrueLiteral(range: SourceRange) -> Any: ... +def FalseLiteral(range: SourceRange) -> Any: ... +def NoneLiteral(range: SourceRange) -> Any: ... + +# Tree nodes +class Stmt(TreeView): + def __init__(self, thing: TreeView) -> None: ... + +class Expr(TreeView): ... + +class Def(TreeView): + def __init__(self, name: Ident, decl: Any, body: list[Stmt]) -> None: ... + def decl(self) -> Any: ... + def name(self) -> Ident: ... + +class Property(TreeView): + def __init__( + self, r: SourceRange, name: Ident, getter: Def, setter: Def | None + ) -> None: ... + def name(self) -> Ident: ... + def getter_name(self) -> str: ... + def setter_name(self) -> Ident | None: ... + +class ClassDef(TreeView): + def __init__( + self, name: Ident, body: list[Stmt], props: list[Property], assigns: list[Any] + ) -> None: ... + +class Decl(TreeView): + def __init__( + self, r: SourceRange, params: list[Param], return_type: Expr | None + ) -> None: ... + +class Delete(Stmt): + def __init__(self, range: SourceRange, targets: list[Expr]) -> None: ... + +class WithItem(Expr): + def __init__(self, range: SourceRange, target: Expr, var: Any | None) -> None: ... + +class Assign(Stmt): + def __init__( + self, lhs: list[Expr], rhs: Expr, type: Expr | None = None + ) -> None: ... + +class AugAssign(Stmt): + def __init__(self, lhs: Expr, kind_str: str, rhs: Expr) -> None: ... + +class Return(Stmt): + def __init__(self, range: SourceRange, value: Expr | None) -> None: ... + +class Raise(Stmt): + def __init__(self, range: SourceRange, expr: Expr) -> None: ... + +class Assert(Stmt): + def __init__(self, range: SourceRange, test: Expr, msg: Expr | None) -> None: ... + +class Pass(Stmt): + def __init__(self, range: SourceRange) -> None: ... + +class Break(Stmt): ... +class Continue(Stmt): ... + +class Dots(Expr, TreeView): + def __init__(self, range: SourceRange) -> None: ... + +class If(Stmt): + def __init__( + self, + range: SourceRange, + cond: Expr, + true_branch: list[Stmt], + false_branch: list[Stmt], + ) -> None: ... + +class While(Stmt): + def __init__(self, range: SourceRange, cond: Expr, body: list[Stmt]) -> None: ... + +class With(Stmt): + def __init__( + self, range: SourceRange, targets: list[WithItem], body: list[Stmt] + ) -> None: ... + +class For(Stmt): + def __init__( + self, + range: SourceRange, + targets: list[Expr], + itrs: list[Expr], + body: list[Stmt], + ) -> None: ... + +class ExprStmt(Stmt): + def __init__(self, expr: Expr) -> None: ... + +class Var(Expr): + def __init__(self, name: Ident) -> None: ... + @property + def name(self) -> str: ... + +class BinOp(Expr): + def __init__(self, kind: str, lhs: Expr, rhs: Expr) -> None: ... + +class UnaryOp(Expr): + def __init__(self, range: SourceRange, kind: str, expr: Expr) -> None: ... + +class Const(Expr): + def __init__(self, range: SourceRange, value: str) -> None: ... + +class StringLiteral(Expr): + def __init__(self, range: SourceRange, value: str) -> None: ... + +class Apply(Expr): + def __init__( + self, expr: Expr, args: list[Expr], kwargs: list[Attribute] + ) -> None: ... + +class Select(Expr): + def __init__(self, expr: Expr, field: Ident) -> None: ... + +class TernaryIf(Expr): + def __init__(self, cond: Expr, true_expr: Expr, false_expr: Expr) -> None: ... + +class ListComp(Expr): + def __init__( + self, range: SourceRange, elt: Expr, target: Expr, iter: Expr + ) -> None: ... + +class DictComp(Expr): + def __init__( + self, range: SourceRange, key: Expr, value: Expr, target: Expr, iter: Expr + ) -> None: ... + +class ListLiteral(Expr): + def __init__(self, range: SourceRange, args: list[Expr]) -> None: ... + +class TupleLiteral(Expr): + def __init__(self, range: SourceRange, args: list[Expr]) -> None: ... + +class DictLiteral(Expr): + def __init__( + self, range: SourceRange, keys: list[Expr], values: list[Expr] + ) -> None: ... + +class Subscript(Expr): + def __init__(self, base: Expr, subscript_exprs: list[Expr]) -> None: ... + +class SliceExpr(Expr): + def __init__( + self, + range: SourceRange, + lower: Expr | None, + upper: Expr | None, + step: Expr | None, + ) -> None: ... + +class Starred(Expr): + def __init__(self, range: SourceRange, expr: Expr) -> None: ... + +class EmptyTypeAnnotation(TreeView): + def __init__(self, range: SourceRange) -> None: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_lazy.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_lazy.pyi new file mode 100644 index 0000000000000000000000000000000000000000..c6b2b89fa3a9e89d889d9248097315d7c9d8635c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_lazy.pyi @@ -0,0 +1,26 @@ +from torch import Tensor + +# defined in torch/csrc/lazy/python/init.cpp +def _mark_step(device: str, devices: list[str], wait: bool) -> None: ... +def _wait_device_ops(devices: list[str]) -> None: ... +def _reset_metrics() -> None: ... +def _counter_names() -> list[str]: ... +def _counter_value(name: str) -> int: ... +def _metrics_report() -> str: ... +def _get_graph_hash(tensors: list[Tensor]) -> str: ... +def _sync_multi( + tensors: list[Tensor], + devices: list[str], + wait: bool = True, + sync_ltc_data: bool = True, +) -> None: ... +def _get_tensor_id(tensor: Tensor) -> int: ... +def _get_tensors_text(tensors: list[Tensor]) -> str: ... +def _get_tensors_dot(tensors: list[Tensor]) -> str: ... +def _get_tensors_backend(tensors: list[Tensor]) -> str: ... +def _get_force_fallback() -> str: ... +def _set_force_fallback(newval: str) -> None: ... +def _clear_ir_cache() -> None: ... +def _dump_ir_cache(filename: str) -> None: ... +def _set_reuse_ir(val: bool) -> None: ... +def _get_default_device_type() -> str: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_lazy_ts_backend.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_lazy_ts_backend.pyi new file mode 100644 index 0000000000000000000000000000000000000000..cd1bef2de069dfc93cd2b6243ab97d550d75f086 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_lazy_ts_backend.pyi @@ -0,0 +1,12 @@ +# mypy: allow-untyped-defs +# defined in torch/csrc/lazy/python/init.cpp + +from typing import Any + +from torch import Tensor + +def _init(): ... +def _get_tensors_ts_device_data_node( + tensors: list[Tensor], +) -> tuple[list[int], list[Any]]: ... +def _run_cached_graph(hash_str: str, graph_inputs: list[Any]) -> list[Tensor]: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_monitor.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_monitor.pyi new file mode 100644 index 0000000000000000000000000000000000000000..82f2a3e4427038dcb0616d7b305c7c5c85c97606 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_monitor.pyi @@ -0,0 +1,58 @@ +# Defined in torch/csrc/monitor/python_init.cpp + +import datetime +from collections.abc import Callable +from enum import Enum +from types import TracebackType + +class Aggregation(Enum): + VALUE = ... + MEAN = ... + COUNT = ... + SUM = ... + MAX = ... + MIN = ... + +class Stat: + name: str + count: int + def __init__( + self, + name: str, + aggregations: list[Aggregation], + window_size: int, + max_samples: int = -1, + ) -> None: ... + def add(self, v: float) -> None: ... + def get(self) -> dict[Aggregation, float]: ... + +class Event: + name: str + timestamp: datetime.datetime + data: dict[str, int | float | bool | str] + def __init__( + self, + name: str, + timestamp: datetime.datetime, + data: dict[str, int | float | bool | str], + ) -> None: ... + +def log_event(e: Event) -> None: ... + +class EventHandlerHandle: ... + +def register_event_handler(handler: Callable[[Event], None]) -> EventHandlerHandle: ... +def unregister_event_handler(handle: EventHandlerHandle) -> None: ... + +class _WaitCounterTracker: + def __enter__(self) -> None: ... + def __exit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + traceback: TracebackType | None = None, + ) -> None: ... + +class _WaitCounter: + def __init__(self, key: str) -> None: ... + def guard(self) -> _WaitCounterTracker: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_nn.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_nn.pyi new file mode 100644 index 0000000000000000000000000000000000000000..08a9fc6e36fd7d997707a8f096ae4b6da7b1ec7b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_nn.pyi @@ -0,0 +1,347 @@ +# @generated by tools/pyi/gen_pyi.py from torch/_C/_nn.pyi.in +# mypy: disable-error-code="type-arg" + +from collections.abc import Sequence +from typing import Literal, overload + +from torch import memory_format, Tensor +from torch.types import _bool, _device, _dtype, _int, _size + +# Defined in tools/autograd/templates/python_nn_functions.cpp + +def adaptive_avg_pool2d(input: Tensor, output_size: _int | _size) -> Tensor: ... +def adaptive_avg_pool3d(input: Tensor, output_size: _int | _size) -> Tensor: ... +def adaptive_max_pool2d( + input: Tensor, + output_size: _int | _size, +) -> tuple[Tensor, Tensor]: ... +def adaptive_max_pool3d( + input: Tensor, + output_size: _int | _size, +) -> tuple[Tensor, Tensor]: ... +def avg_pool2d( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size | None = None, + padding: _int | _size = 0, + ceil_mode: bool = False, + count_include_pad: bool = True, + divisor_override: int | None = None, +) -> Tensor: ... +def avg_pool3d( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size | None = None, + padding: _int | _size = 0, + ceil_mode: bool = False, + count_include_pad: bool = True, + divisor_override: int | None = None, +) -> Tensor: ... +def binary_cross_entropy( + input: Tensor, + target: Tensor, + weight: Tensor | None = None, + reduction: str = ..., +) -> Tensor: ... +def col2im( + input: Tensor, + output_size: _int | _size, + kernel_size: _int | _size, + dilation: _int | _size, + stride: _int | _size | None = None, + padding: _int | _size = 0, +) -> Tensor: ... +def cross_entropy_loss( + input: Tensor, + target: Tensor, + weight: Tensor | None = None, + reduction: str = ..., + ignore_index: int = -100, + label_smoothing: float = 0.0, +) -> Tensor: ... +def elu( + input: Tensor, + alpha: float = 1.0, + scale: float = 1.0, + input_scale: float = 1.0, +) -> Tensor: ... +def elu_(input: Tensor, alpha: float = ...) -> Tensor: ... +def fractional_max_pool2d( + input: Tensor, + kernel_size: _int | _size, + output_size: _int | _size, + _random_samples: Tensor, +) -> tuple[Tensor, Tensor]: ... +def fractional_max_pool3d( + input: Tensor, + kernel_size: _int | _size, + output_size: _int | _size, + _random_samples: Tensor, +) -> tuple[Tensor, Tensor]: ... +def gelu(input: Tensor, approximate: str = ...) -> Tensor: ... +def glu(input: Tensor, dim: int = -1) -> Tensor: ... +def hardsigmoid(input: Tensor, *, out: Tensor | None = None) -> Tensor: ... +def hardsigmoid_(input: Tensor) -> Tensor: ... +def hardswish(input: Tensor) -> Tensor: ... +def hardswish_(input: Tensor) -> Tensor: ... +def hardtanh( + input: Tensor, + min_val: float = ..., + max_val: float = ..., + *, + out: Tensor | None = None, +) -> Tensor: ... +def hardtanh_( + input: Tensor, + min_val: float = ..., + max_val: float = ..., +) -> Tensor: ... +def huber_loss( + input: Tensor, + target: Tensor, + reduction: str = ..., + delta: float = 1.0, +) -> Tensor: ... +def im2col( + input: Tensor, + kernel_size: _int | _size, + dilation: _int | _size, + padding: _int | _size, + stride: _int | _size, +) -> Tensor: ... +def l1_loss(input: Tensor, target: Tensor, reduction: str = ...) -> Tensor: ... +def leaky_relu( + input: Tensor, + negative_slope: float = ..., + *, + out: Tensor | None = None, +) -> Tensor: ... +def leaky_relu_(input: Tensor, negative_slope: float = ...) -> Tensor: ... +def linear( + input: Tensor, + weight: Tensor, + bias: Tensor | None = None, +) -> Tensor: ... +def log_sigmoid(input: Tensor) -> Tensor: ... +def max_pool2d_with_indices( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size | None = None, + padding: _int | _size = 0, + dilation: _int | _size = 1, + ceil_mode: bool = False, +) -> tuple[Tensor, Tensor]: ... +def max_pool3d_with_indices( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size | None = None, + padding: _int | _size = 0, + dilation: _int | _size = 1, + ceil_mode: bool = False, +) -> tuple[Tensor, Tensor]: ... +def max_unpool2d( + input: Tensor, + indices: Tensor, + output_size: Sequence[int] | None, +) -> Tensor: ... +def max_unpool3d( + input: Tensor, + indices: Tensor, + output_size: Sequence[int] | None, + stride: _int | _size, + padding: _int | _size, +) -> Tensor: ... +def mish(input: Tensor) -> Tensor: ... +def mish_(input: Tensor) -> Tensor: ... +def mse_loss(input: Tensor, target: Tensor, reduction: str = ...) -> Tensor: ... +def multi_margin_loss( + input: Tensor, + target: Tensor, + p: float = 1.0, + margin: float = 1.0, + weight: Tensor | None = None, + reduction: str = ..., +) -> Tensor: ... +def multilabel_margin_loss( + input: Tensor, + target: Tensor, + reduction: str = ..., +) -> Tensor: ... +def nll_loss_nd( + input: Tensor, + target: Tensor, + weight: Tensor | None = None, + reduction: str = ..., + ignore_index: int = -100, +) -> Tensor: ... +def one_hot(tensor: Tensor, num_classes: int = ...) -> Tensor: ... +def pad( + input: Tensor, + pad: Sequence[int], + mode: str = ..., + value: float | None = None, +) -> Tensor: ... +def relu6(input: Tensor) -> Tensor: ... +def relu6_(input: Tensor) -> Tensor: ... +def scaled_dot_product_attention( + query: Tensor, + key: Tensor, + value: Tensor, + attn_mask: Tensor | None = None, + dropout_p: float = 0.0, + is_causal: bool = False, + scale: float | None = None, + enable_gqa: bool = False, +) -> Tensor: ... +def silu(input: Tensor) -> Tensor: ... +def silu_(input: Tensor) -> Tensor: ... +def smooth_l1_loss( + input: Tensor, + target: Tensor, + reduction: str = ..., + beta: float = 1.0, +) -> Tensor: ... +def soft_margin_loss( + input: Tensor, + target: Tensor, + reduction: str = ..., +) -> Tensor: ... +def softplus( + input: Tensor, + beta: float = ..., + threshold: float = ..., +) -> Tensor: ... +def softshrink(input: Tensor, lambd: float = ...) -> Tensor: ... + +# Defined in aten/src/ATen/native/mkldnn/Linear.cpp +def mkldnn_linear(input: Tensor, weight: Tensor, bias: Tensor | None) -> Tensor: ... + +# Defined at aten/src/ATen/native/mkldnn/MKLDNNConversions.cpp +def mkldnn_reorder_conv2d_weight( + self: Tensor, + padding: list, + stride: list, + dilatation: list, + groups: int, +) -> Tensor: ... +def mkldnn_reorder_conv3d_weight( + self: Tensor, + padding: list, + stride: list, + dilatation: list, + groups: int, +) -> Tensor: ... + +# Defined in aten/src/ATen/native/mkldnn/Prelu.cpp +def mkldnn_prelu(input: Tensor, weight: Tensor) -> Tensor: ... + +# Defined at tools/autograd/templates/python_nn_functions.cpp +@overload +def _parse_to( + device: _device, + dtype: _dtype, + non_blocking: _bool, + copy: _bool, + *, + memory_format: memory_format, +) -> tuple[_device, _dtype, _bool, memory_format]: ... +@overload +def _parse_to( + dtype: _dtype, + non_blocking: _bool, + copy: _bool, + *, + memory_format: memory_format, +) -> tuple[_device, _dtype, _bool, memory_format]: ... +@overload +def _parse_to( + tensor: Tensor, + non_blocking: _bool, + copy: _bool, + *, + memory_format: memory_format, +) -> tuple[_device, _dtype, _bool, memory_format]: ... + +# Defined in aten/src/ATen/native/PackedSequence.cpp +def pad_sequence( + sequences: list[Tensor] | tuple[Tensor, ...], + batch_first: bool = False, + padding_value: float = 0.0, + padding_side: Literal["left", "right"] = "right", +) -> Tensor: ... + +# Upsample functions used by torch.nn.functional.interpolate +def upsample_nearest1d( + input: Tensor, + output_size: Sequence[int] | None, + scale_factors: Sequence[float] | None, +) -> Tensor: ... +def upsample_nearest2d( + input: Tensor, + output_size: Sequence[int] | None, + scale_factors: Sequence[float] | None, +) -> Tensor: ... +def upsample_nearest3d( + input: Tensor, + output_size: Sequence[int] | None, + scale_factors: Sequence[float] | None, +) -> Tensor: ... +def _upsample_nearest_exact1d( + input: Tensor, + output_size: Sequence[int] | None, + scale_factors: Sequence[float] | None, +) -> Tensor: ... +def _upsample_nearest_exact2d( + input: Tensor, + output_size: Sequence[int] | None, + scale_factors: Sequence[float] | None, +) -> Tensor: ... +def _upsample_nearest_exact3d( + input: Tensor, + output_size: Sequence[int] | None, + scale_factors: Sequence[float] | None, +) -> Tensor: ... +def upsample_linear1d( + input: Tensor, + output_size: Sequence[int] | None, + align_corners: bool, + scale_factors: Sequence[float] | None, +) -> Tensor: ... +def _upsample_bilinear2d_aa( + input: Tensor, + output_size: Sequence[int] | None, + align_corners: bool, + scale_factors: Sequence[float] | None, +) -> Tensor: ... +def upsample_bilinear2d( + input: Tensor, + output_size: Sequence[int] | None, + align_corners: bool, + scale_factors: Sequence[float] | None, +) -> Tensor: ... +def upsample_trilinear3d( + input: Tensor, + output_size: Sequence[int] | None, + align_corners: bool, + scale_factors: Sequence[float] | None, +) -> Tensor: ... +def _upsample_bicubic2d_aa( + input: Tensor, + output_size: Sequence[int] | None, + align_corners: bool, + scale_factors: Sequence[float] | None, +) -> Tensor: ... +def _upsample_lanczos2d_aa( + input: Tensor, + output_size: Sequence[int] | None, + align_corners: bool, + scale_factors: Sequence[float] | None, +) -> Tensor: ... +def upsample_bicubic2d( + input: Tensor, + output_size: Sequence[int] | None, + align_corners: bool, + scale_factors: Sequence[float] | None, +) -> Tensor: ... +def flatten_dense_tensors(tensors: list[Tensor]) -> Tensor: ... +def unflatten_dense_tensors(flat: Tensor, tensors: list[Tensor]) -> list[Tensor]: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_nvtx.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_nvtx.pyi new file mode 100644 index 0000000000000000000000000000000000000000..9b96874c36578ebaba065188d726455ff0b771be --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_nvtx.pyi @@ -0,0 +1,9 @@ +# mypy: allow-untyped-defs +# Defined in torch/csrc/cuda/shared/nvtx.cpp +def rangePushA(message: str) -> int: ... +def rangePop() -> int: ... +def rangeStartA(message: str) -> int: ... +def rangeEnd(int) -> None: ... +def markA(message: str) -> None: ... +def deviceRangeStart(message: str, stream: int) -> object: ... +def deviceRangeEnd(range_handle: object, stream: int) -> None: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_onnx.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_onnx.pyi new file mode 100644 index 0000000000000000000000000000000000000000..349e0b9ad12f0dd9306fde89a40718f26b158f0e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_onnx.pyi @@ -0,0 +1,39 @@ +# Defined in torch/csrc/onnx/init.cpp + +from enum import Enum + +PRODUCER_VERSION: str + +class TensorProtoDataType(Enum): + UNDEFINED = ... + FLOAT = ... + UINT8 = ... + INT8 = ... + UINT16 = ... + INT16 = ... + INT32 = ... + INT64 = ... + STRING = ... + BOOL = ... + FLOAT16 = ... + DOUBLE = ... + UINT32 = ... + UINT64 = ... + COMPLEX64 = ... + COMPLEX128 = ... + BFLOAT16 = ... + FLOAT8E5M2 = ... + FLOAT8E4M3FN = ... + FLOAT8E5M2FNUZ = ... + FLOAT8E4M3FNUZ = ... + +class OperatorExportTypes(Enum): + ONNX = ... + ONNX_ATEN = ... + ONNX_ATEN_FALLBACK = ... + ONNX_FALLTHROUGH = ... + +class TrainingMode(Enum): + EVAL = ... + PRESERVE = ... + TRAINING = ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_profiler.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_profiler.pyi new file mode 100644 index 0000000000000000000000000000000000000000..04c3f4a460086049e246de11c925f95feb12a600 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_profiler.pyi @@ -0,0 +1,248 @@ +from enum import Enum +from typing import Literal, TypeAlias + +from torch._C import device, dtype, layout + +# defined in torch/csrc/profiler/python/init.cpp + +class RecordScope(Enum): + FUNCTION = ... + BACKWARD_FUNCTION = ... + TORCHSCRIPT_FUNCTION = ... + KERNEL_FUNCTION_DTYPE = ... + CUSTOM_CLASS = ... + BUILD_FEATURE = ... + LITE_INTERPRETER = ... + USER_SCOPE = ... + STATIC_RUNTIME_OP = ... + STATIC_RUNTIME_MODEL = ... + +class ProfilerState(Enum): + Disabled = ... + CPU = ... + CUDA = ... + NVTX = ... + ITT = ... + PRIVATEUSE1 = ... + KINETO = ... + KINETO_GPU_FALLBACK = ... + KINETO_PRIVATEUSE1_FALLBACK = ... + KINETO_PRIVATEUSE1 = ... + +class ActiveProfilerType(Enum): + NONE = ... + LEGACY = ... + KINETO = ... + NVTX = ... + ITT = ... + PRIVATEUSE1 = ... + +class ProfilerActivity(Enum): + CPU = ... + CUDA = ... + XPU = ... + MTIA = ... + HPU = ... + PrivateUse1 = ... + +class _EventType(Enum): + TorchOp = ... + Backend = ... + Allocation = ... + OutOfMemory = ... + PyCall = ... + PyCCall = ... + Kineto = ... + +class _ExperimentalConfig: + def __init__( + self, + profiler_metrics: list[str] = ..., + profiler_measure_per_kernel: bool = ..., + verbose: bool = ..., + performance_events: list[str] = ..., + enable_cuda_sync_events: bool = ..., + profile_all_threads: bool = ..., + ) -> None: ... + +class ProfilerConfig: + def __init__( + self, + state: ProfilerState, + report_input_shapes: bool, + profile_memory: bool, + with_stack: bool, + with_flops: bool, + with_modules: bool, + experimental_config: _ExperimentalConfig, + trace_id: str | None = None, + ) -> None: ... + +class _ProfilerEvent: + start_tid: int + start_time_ns: int + children: list[_ProfilerEvent] + + # TODO(robieta): remove in favor of `self.typed` + extra_fields: ( + _ExtraFields_TorchOp + | _ExtraFields_Backend + | _ExtraFields_Allocation + | _ExtraFields_OutOfMemory + | _ExtraFields_PyCall + | _ExtraFields_PyCCall + | _ExtraFields_Kineto + ) + + @property + def typed( + self, + ) -> ( + tuple[Literal[_EventType.TorchOp], _ExtraFields_TorchOp] + | tuple[Literal[_EventType.Backend], _ExtraFields_Backend] + | tuple[Literal[_EventType.Allocation], _ExtraFields_Allocation] + | tuple[Literal[_EventType.OutOfMemory], _ExtraFields_OutOfMemory] + | tuple[Literal[_EventType.PyCall], _ExtraFields_PyCall] + | tuple[Literal[_EventType.PyCCall], _ExtraFields_PyCCall] + | tuple[Literal[_EventType.Kineto], _ExtraFields_Kineto] + ): ... + @property + def name(self) -> str: ... + @property + def tag(self) -> _EventType: ... + @property + def id(self) -> int: ... + @property + def parent(self) -> _ProfilerEvent | None: ... + @property + def correlation_id(self) -> int: ... + @property + def end_time_ns(self) -> int: ... + @property + def duration_time_ns(self) -> int: ... + +class _TensorMetadata: + impl_ptr: int | None + storage_data_ptr: int | None + id: int | None + + @property + def allocation_id(self) -> int | None: ... + @property + def layout(self) -> layout: ... + @property + def device(self) -> device: ... + @property + def dtype(self) -> dtype: ... + @property + def sizes(self) -> list[int]: ... + @property + def strides(self) -> list[int]: ... + +Scalar: TypeAlias = int | float | bool | complex +Input: TypeAlias = _TensorMetadata | list[_TensorMetadata] | Scalar | None + +class _ExtraFields_TorchOp: + name: str + sequence_number: int + allow_tf32_cublas: bool + + @property + def inputs(self) -> list[Input]: ... + @property + def scope(self) -> RecordScope: ... + +class _ExtraFields_Backend: ... + +class _ExtraFields_Allocation: + ptr: int + id: int | None + alloc_size: int + total_allocated: int + total_reserved: int + + @property + def allocation_id(self) -> int | None: ... + @property + def device(self) -> device: ... + +class _ExtraFields_OutOfMemory: ... + +class _PyFrameState: + line_number: int + function_name: str + + @property + def file_name(self) -> str: ... + +class _NNModuleInfo: + @property + def self_ptr(self) -> int: ... + @property + def cls_ptr(self) -> int: ... + @property + def cls_name(self) -> str: ... + @property + def parameters( + self, + ) -> list[tuple[str, _TensorMetadata, _TensorMetadata | None]]: ... + +class _OptimizerInfo: + @property + def parameters( + self, + ) -> list[ + tuple[ + # Parameter + _TensorMetadata, + # + # Gradient (if present during optimizer.step()) + _TensorMetadata | None, + # + # Optimizer state for Parameter as (name, tensor) pairs + list[tuple[str, _TensorMetadata]], + ] + ]: ... + +class _ExtraFields_PyCCall: + @property + def caller(self) -> _PyFrameState: ... + +class _ExtraFields_PyCall: + @property + def callsite(self) -> _PyFrameState: ... + @property + def caller(self) -> _PyFrameState: ... + @property + def module(self) -> _NNModuleInfo | None: ... + @property + def optimizer(self) -> _OptimizerInfo | None: ... + +class _ExtraFields_Kineto: ... + +def _add_execution_trace_observer(output_file_path: str) -> bool: ... +def _remove_execution_trace_observer() -> None: ... +def _enable_execution_trace_observer() -> None: ... +def _disable_execution_trace_observer() -> None: ... +def _set_record_concrete_inputs_enabled_val(val: bool) -> None: ... +def _set_fwd_bwd_enabled_val(val: bool) -> None: ... +def _set_cuda_sync_enabled_val(val: bool) -> None: ... + +class CapturedTraceback: ... + +def gather_traceback(python: bool, script: bool, cpp: bool) -> CapturedTraceback: ... + +# The Dict has name, filename, line +def symbolize_tracebacks( + to_symbolize: list[CapturedTraceback], +) -> list[list[dict[str, str]]]: ... + +class _RecordFunctionFast: + def __init__( + self, + name: str, + input_values: list | tuple | None = None, + keyword_values: dict | None = None, + ) -> None: ... + def __enter__(self) -> None: ... + def __exit__(self, *exc_info: object) -> None: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_verbose.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_verbose.pyi new file mode 100644 index 0000000000000000000000000000000000000000..2388ce2bb8a5edd4c7640c374537e71607e5b72e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C/_verbose.pyi @@ -0,0 +1,3 @@ +# Defined in torch/csrc/utils/verbose.cpp +def mkl_set_verbose(enable: int) -> int: ... +def mkldnn_set_verbose(level: int) -> int: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_C_flatbuffer/__init__.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C_flatbuffer/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..38750ed26aa26900591829fb7d51a3e3e1cdeeec --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_C_flatbuffer/__init__.pyi @@ -0,0 +1,11 @@ +# mypy: allow-untyped-defs +from torch._C import LiteScriptModule, ScriptModule + +def _load_mobile_module_from_file(filename: str): ... +def _load_mobile_module_from_bytes(bytes_: bytes): ... +def _load_jit_module_from_file(filename: str): ... +def _load_jit_module_from_bytes(bytes_: bytes): ... +def _save_mobile_module(m: LiteScriptModule, filename: str): ... +def _save_jit_module(m: ScriptModule, filename: str): ... +def _save_mobile_module_to_bytes(m: LiteScriptModule) -> bytes: ... +def _save_jit_module_to_bytes(m: ScriptModule) -> bytes: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_VF.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_VF.py new file mode 100644 index 0000000000000000000000000000000000000000..94166b51f1786593b584629744adc24036e8b1d7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_VF.py @@ -0,0 +1,31 @@ +""" +This makes the functions in torch._C._VariableFunctions available as + torch._VF. +without mypy being able to find them. + +A subset of those functions are mapped to ATen functions in +torch/jit/_builtins.py + +See https://github.com/pytorch/pytorch/issues/21478 for the reason for +introducing torch._VF + +""" + +import sys +import types + +import torch + + +class VFModule(types.ModuleType): + vf: types.ModuleType + + def __init__(self, name: str): + super().__init__(name) + self.vf = torch._C._VariableFunctions + + def __getattr__(self, name: str) -> object: + return getattr(self.vf, name) + + +sys.modules[__name__] = VFModule(__name__) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_VF.pyi b/outputs/audit_venv/lib/python3.11/site-packages/torch/_VF.pyi new file mode 100644 index 0000000000000000000000000000000000000000..9585b9e75133e8c2f19513e64e98911f50fa4d4c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_VF.pyi @@ -0,0 +1,33889 @@ +# @generated by tools/pyi/gen_pyi.py from torch/_C/_VariableFunctions.pyi.in +# mypy: disable-error-code="type-arg" +# mypy: allow-untyped-defs +# ruff: noqa: F401,PYI054 + +from collections.abc import Callable, Sequence +from types import EllipsisType +from typing import Any, Literal, overload, TypeVar + +import torch +from torch import ( + contiguous_format, + Generator, + inf, + memory_format, + strided, + SymInt, + Tensor, +) +from torch._prims_common import DeviceLikeType +from torch.types import ( + _bool, + _complex, + _device, + _dtype, + _float, + _int, + _layout, + _qscheme, + _size, + Device, + Number, +) + +__all__ = [ + "__and__", + "__lshift__", + "__or__", + "__rshift__", + "__xor__", + "_adaptive_avg_pool2d", + "_adaptive_avg_pool3d", + "_add_batch_dim", + "_add_relu", + "_add_relu_", + "_addmm_activation", + "_aminmax", + "_amp_foreach_non_finite_check_and_unscale_", + "_amp_update_scale_", + "_assert_async", + "_assert_scalar", + "_assert_tensor_metadata", + "_batch_norm_impl_index", + "_cast_Byte", + "_cast_Char", + "_cast_Double", + "_cast_Float", + "_cast_Half", + "_cast_Int", + "_cast_Long", + "_cast_Short", + "_choose_qparams_per_tensor", + "_chunk_cat", + "_coalesce", + "_compute_linear_combination", + "_conj", + "_conj_copy", + "_conj_physical", + "_convert_indices_from_coo_to_csr", + "_convert_indices_from_csr_to_coo", + "_convert_weight_to_int4pack", + "_convert_weight_to_int4pack_for_cpu", + "_convolution", + "_convolution_mode", + "_copy_from", + "_copy_from_and_resize", + "_cslt_compress", + "_cslt_sparse_mm", + "_cslt_sparse_mm_search", + "_ctc_loss", + "_cudnn_ctc_loss", + "_cudnn_init_dropout_state", + "_cudnn_rnn", + "_cudnn_rnn_flatten_weight", + "_cufft_clear_plan_cache", + "_cufft_get_plan_cache_max_size", + "_cufft_get_plan_cache_size", + "_cufft_set_plan_cache_max_size", + "_cummax_helper", + "_cummin_helper", + "_debug_has_internal_overlap", + "_dim_arange", + "_dirichlet_grad", + "_disable_functionalization", + "_dyn_quant_matmul_4bit", + "_dyn_quant_pack_4bit_weight", + "_efficientzerotensor", + "_embedding_bag", + "_embedding_bag_forward_only", + "_empty_affine_quantized", + "_empty_per_channel_affine_quantized", + "_enable_functionalization", + "_euclidean_dist", + "_fake_quantize_learnable_per_channel_affine", + "_fake_quantize_learnable_per_tensor_affine", + "_fake_quantize_per_tensor_affine_cachemask_tensor_qparams", + "_fft_c2c", + "_fft_c2r", + "_fft_r2c", + "_fill_mem_eff_dropout_mask_", + "_flash_attention_forward_no_dropout_inplace", + "_foobar", + "_foreach_abs", + "_foreach_abs_", + "_foreach_acos", + "_foreach_acos_", + "_foreach_add", + "_foreach_add_", + "_foreach_addcdiv", + "_foreach_addcdiv_", + "_foreach_addcmul", + "_foreach_addcmul_", + "_foreach_asin", + "_foreach_asin_", + "_foreach_atan", + "_foreach_atan_", + "_foreach_ceil", + "_foreach_ceil_", + "_foreach_clamp_max", + "_foreach_clamp_max_", + "_foreach_clamp_min", + "_foreach_clamp_min_", + "_foreach_clone", + "_foreach_copy_", + "_foreach_cos", + "_foreach_cos_", + "_foreach_cosh", + "_foreach_cosh_", + "_foreach_div", + "_foreach_div_", + "_foreach_erf", + "_foreach_erf_", + "_foreach_erfc", + "_foreach_erfc_", + "_foreach_exp", + "_foreach_exp_", + "_foreach_expm1", + "_foreach_expm1_", + "_foreach_floor", + "_foreach_floor_", + "_foreach_frac", + "_foreach_frac_", + "_foreach_lerp", + "_foreach_lerp_", + "_foreach_lgamma", + "_foreach_lgamma_", + "_foreach_log", + "_foreach_log10", + "_foreach_log10_", + "_foreach_log1p", + "_foreach_log1p_", + "_foreach_log2", + "_foreach_log2_", + "_foreach_log_", + "_foreach_max", + "_foreach_maximum", + "_foreach_maximum_", + "_foreach_minimum", + "_foreach_minimum_", + "_foreach_mul", + "_foreach_mul_", + "_foreach_neg", + "_foreach_neg_", + "_foreach_norm", + "_foreach_pow", + "_foreach_pow_", + "_foreach_powsum", + "_foreach_reciprocal", + "_foreach_reciprocal_", + "_foreach_round", + "_foreach_round_", + "_foreach_rsqrt", + "_foreach_rsqrt_", + "_foreach_sigmoid", + "_foreach_sigmoid_", + "_foreach_sign", + "_foreach_sign_", + "_foreach_sin", + "_foreach_sin_", + "_foreach_sinh", + "_foreach_sinh_", + "_foreach_sqrt", + "_foreach_sqrt_", + "_foreach_sub", + "_foreach_sub_", + "_foreach_tan", + "_foreach_tan_", + "_foreach_tanh", + "_foreach_tanh_", + "_foreach_trunc", + "_foreach_trunc_", + "_foreach_zero_", + "_from_functional_tensor", + "_functional_assert_async", + "_functional_assert_scalar", + "_functional_sym_constrain_range", + "_functional_sym_constrain_range_for_size", + "_functionalize_apply_view_metas", + "_functionalize_are_all_mutations_hidden_from_autograd", + "_functionalize_are_all_mutations_under_no_grad_or_inference_mode", + "_functionalize_commit_update", + "_functionalize_has_metadata_mutation", + "_functionalize_inductor_storage_resized_counter", + "_functionalize_is_symbolic", + "_functionalize_mark_mutation_hidden_from_autograd", + "_functionalize_mark_storage_changed", + "_functionalize_mutation_counter", + "_functionalize_replace", + "_functionalize_storage_changed_counter", + "_functionalize_sync", + "_functionalize_unsafe_set", + "_functionalize_was_inductor_storage_resized", + "_functionalize_was_storage_changed", + "_fused_adagrad_", + "_fused_adam_", + "_fused_adamw_", + "_fused_dropout", + "_fused_moving_avg_obs_fq_helper", + "_fused_rms_norm", + "_fused_sdp_choice", + "_fused_sgd_", + "_fw_primal_copy", + "_grid_sampler_2d_cpu_fallback", + "_grouped_mm", + "_has_compatible_shallow_copy_type", + "_histogramdd_bin_edges", + "_histogramdd_from_bin_cts", + "_histogramdd_from_bin_tensors", + "_index_put_impl_", + "_indices_copy", + "_int_mm", + "_is_all_true", + "_is_any_true", + "_is_functional_tensor", + "_is_functional_tensor_base", + "_is_zerotensor", + "_lazy_clone", + "_linalg_check_errors", + "_linalg_det", + "_linalg_eigh", + "_linalg_slogdet", + "_linalg_solve_ex", + "_linalg_svd", + "_log_softmax", + "_log_softmax_backward_data", + "_logcumsumexp", + "_lstm_mps", + "_lu_with_info", + "_make_dep_token", + "_make_dual", + "_make_dual_copy", + "_make_per_channel_quantized_tensor", + "_make_per_tensor_quantized_tensor", + "_masked_scale", + "_masked_softmax", + "_mixed_dtypes_linear", + "_mkldnn_reshape", + "_mkldnn_transpose", + "_mkldnn_transpose_", + "_mps_convolution", + "_mps_convolution_transpose", + "_native_batch_norm_legit", + "_native_batch_norm_legit_no_training", + "_native_multi_head_attention", + "_neg_view", + "_neg_view_copy", + "_nested_compute_contiguous_strides_offsets", + "_nested_from_padded", + "_nested_from_padded_and_nested_example", + "_nested_from_padded_tensor", + "_nested_get_jagged_dummy", + "_nested_get_lengths", + "_nested_get_max_seqlen", + "_nested_get_min_seqlen", + "_nested_get_offsets", + "_nested_get_ragged_idx", + "_nested_get_values", + "_nested_get_values_copy", + "_nested_tensor_from_mask", + "_nested_tensor_from_mask_left_aligned", + "_nested_tensor_from_tensor_list", + "_nested_tensor_softmax_with_shape", + "_nested_view_from_buffer", + "_nested_view_from_buffer_copy", + "_nested_view_from_jagged", + "_nested_view_from_jagged_copy", + "_nnpack_available", + "_nnpack_spatial_convolution", + "_pack_padded_sequence", + "_pad_packed_sequence", + "_philox_key_fold_in", + "_philox_key_split", + "_philox_normal_", + "_philox_uniform_", + "_pin_memory", + "_prelu_kernel", + "_print", + "_propagate_xla_data", + "_remove_batch_dim", + "_reshape_alias_copy", + "_reshape_from_tensor", + "_resize_output_", + "_rowwise_prune", + "_safe_softmax", + "_sample_dirichlet", + "_saturate_weight_to_fp16", + "_scaled_dot_product_attention_math", + "_scaled_dot_product_attention_math_for_mps", + "_scaled_dot_product_cudnn_attention", + "_scaled_dot_product_efficient_attention", + "_scaled_dot_product_flash_attention", + "_scaled_dot_product_flash_attention_for_cpu", + "_scaled_grouped_mm", + "_scaled_grouped_mm_v2", + "_scaled_mm", + "_scaled_mm_v2", + "_shape_as_tensor", + "_sobol_engine_draw", + "_sobol_engine_ff_", + "_sobol_engine_initialize_state_", + "_sobol_engine_scramble_", + "_softmax", + "_softmax_backward_data", + "_sparse_broadcast_to", + "_sparse_broadcast_to_copy", + "_sparse_csr_prod", + "_sparse_csr_sum", + "_sparse_log_softmax_backward_data", + "_sparse_semi_structured_addmm", + "_sparse_semi_structured_apply", + "_sparse_semi_structured_apply_dense", + "_sparse_semi_structured_linear", + "_sparse_semi_structured_mm", + "_sparse_semi_structured_tile", + "_sparse_softmax_backward_data", + "_sparse_sparse_matmul", + "_sparse_sum", + "_stack", + "_standard_gamma", + "_standard_gamma_grad", + "_sync", + "_test_autograd_multiple_dispatch", + "_test_autograd_multiple_dispatch_view", + "_test_autograd_multiple_dispatch_view_copy", + "_test_check_tensor", + "_test_functorch_fallback", + "_test_parallel_materialize", + "_test_serialization_subcmul", + "_to_cpu", + "_to_functional_tensor", + "_to_sparse_semi_structured", + "_transform_bias_rescale_qkv", + "_transformer_encoder_layer_fwd", + "_trilinear", + "_triton_multi_head_attention", + "_triton_scaled_dot_attention", + "_unique", + "_unique2", + "_unpack_dual", + "_unsafe_index", + "_unsafe_index_put", + "_unsafe_masked_index", + "_unsafe_masked_index_put_accumulate", + "_use_cudnn_ctc_loss", + "_use_cudnn_rnn_flatten_weight", + "_use_miopen_ctc_loss", + "_validate_compressed_sparse_indices", + "_validate_sparse_bsc_tensor_args", + "_validate_sparse_bsr_tensor_args", + "_validate_sparse_compressed_tensor_args", + "_validate_sparse_coo_tensor_args", + "_validate_sparse_csc_tensor_args", + "_validate_sparse_csr_tensor_args", + "_values_copy", + "_weight_int4pack_mm", + "_weight_int4pack_mm_for_cpu", + "_weight_int4pack_mm_with_scales_and_zeros", + "_weight_int8pack_mm", + "_weight_norm", + "_weight_norm_interface", + "_wrapped_linear_prepack", + "_wrapped_quantized_linear_prepacked", + "abs", + "abs_", + "absolute", + "acos", + "acos_", + "acosh", + "acosh_", + "adaptive_avg_pool1d", + "adaptive_max_pool1d", + "add", + "addbmm", + "addcdiv", + "addcmul", + "addmm", + "addmv", + "addmv_", + "addr", + "adjoint", + "affine_grid_generator", + "alias_copy", + "all", + "allclose", + "alpha_dropout", + "alpha_dropout_", + "amax", + "amin", + "aminmax", + "angle", + "any", + "arange", + "arccos", + "arccos_", + "arccosh", + "arccosh_", + "arcsin", + "arcsin_", + "arcsinh", + "arcsinh_", + "arctan", + "arctan2", + "arctan_", + "arctanh", + "arctanh_", + "argmax", + "argmin", + "argsort", + "argwhere", + "as_strided", + "as_strided_", + "as_strided_copy", + "as_strided_scatter", + "as_tensor", + "asarray", + "asin", + "asin_", + "asinh", + "asinh_", + "atan", + "atan2", + "atan_", + "atanh", + "atanh_", + "avg_pool1d", + "baddbmm", + "bartlett_window", + "batch_norm", + "batch_norm_backward_elemt", + "batch_norm_backward_reduce", + "batch_norm_elemt", + "batch_norm_gather_stats", + "batch_norm_gather_stats_with_counts", + "batch_norm_stats", + "batch_norm_update_stats", + "bernoulli", + "bilinear", + "binary_cross_entropy_with_logits", + "bincount", + "binomial", + "bitwise_and", + "bitwise_left_shift", + "bitwise_not", + "bitwise_or", + "bitwise_right_shift", + "bitwise_xor", + "blackman_window", + "bmm", + "broadcast_to", + "bucketize", + "can_cast", + "cat", + "ccol_indices_copy", + "ceil", + "ceil_", + "celu", + "celu_", + "channel_shuffle", + "cholesky", + "cholesky_inverse", + "cholesky_solve", + "choose_qparams_optimized", + "chunk", + "clamp", + "clamp_", + "clamp_max", + "clamp_max_", + "clamp_min", + "clamp_min_", + "clip", + "clip_", + "clone", + "col_indices_copy", + "column_stack", + "combinations", + "complex", + "concat", + "concatenate", + "conj", + "conj_physical", + "conj_physical_", + "constant_pad_nd", + "conv1d", + "conv2d", + "conv3d", + "conv_tbc", + "conv_transpose1d", + "conv_transpose2d", + "conv_transpose3d", + "convolution", + "copysign", + "corrcoef", + "cos", + "cos_", + "cosh", + "cosh_", + "cosine_embedding_loss", + "cosine_similarity", + "count_nonzero", + "cov", + "cross", + "crow_indices_copy", + "ctc_loss", + "cudnn_affine_grid_generator", + "cudnn_batch_norm", + "cudnn_convolution", + "cudnn_convolution_add_relu", + "cudnn_convolution_relu", + "cudnn_convolution_transpose", + "cudnn_grid_sampler", + "cudnn_is_acceptable", + "cummax", + "cummin", + "cumprod", + "cumsum", + "cumulative_trapezoid", + "deg2rad", + "deg2rad_", + "dequantize", + "det", + "detach", + "detach_", + "detach_copy", + "diag", + "diag_embed", + "diagflat", + "diagonal", + "diagonal_copy", + "diagonal_scatter", + "diff", + "digamma", + "dist", + "div", + "divide", + "dot", + "dropout", + "dropout_", + "dsmm", + "dsplit", + "dstack", + "embedding", + "embedding_bag", + "embedding_renorm_", + "empty", + "empty_like", + "empty_permuted", + "empty_quantized", + "empty_strided", + "eq", + "equal", + "erf", + "erf_", + "erfc", + "erfc_", + "erfinv", + "exp", + "exp2", + "exp2_", + "exp_", + "expand_copy", + "expm1", + "expm1_", + "eye", + "fake_quantize_per_channel_affine", + "fake_quantize_per_tensor_affine", + "fbgemm_linear_fp16_weight", + "fbgemm_linear_fp16_weight_fp32_activation", + "fbgemm_linear_int8_weight", + "fbgemm_linear_int8_weight_fp32_activation", + "fbgemm_linear_quantize_weight", + "fbgemm_pack_gemm_matrix_fp16", + "fbgemm_pack_quantized_matrix", + "feature_alpha_dropout", + "feature_alpha_dropout_", + "feature_dropout", + "feature_dropout_", + "fill", + "fill_", + "fix", + "fix_", + "flatten", + "flip", + "fliplr", + "flipud", + "float_power", + "floor", + "floor_", + "floor_divide", + "fmax", + "fmin", + "fmod", + "frac", + "frac_", + "frexp", + "frobenius_norm", + "from_file", + "from_numpy", + "frombuffer", + "full", + "full_like", + "fused_moving_avg_obs_fake_quant", + "gather", + "gcd", + "gcd_", + "ge", + "geqrf", + "ger", + "get_default_dtype", + "get_num_interop_threads", + "get_num_threads", + "gradient", + "greater", + "greater_equal", + "grid_sampler", + "grid_sampler_2d", + "grid_sampler_3d", + "group_norm", + "gru", + "gru_cell", + "gt", + "hamming_window", + "hann_window", + "hardshrink", + "hash_tensor", + "heaviside", + "hinge_embedding_loss", + "histc", + "histogram", + "histogramdd", + "hsmm", + "hsplit", + "hspmm", + "hstack", + "hypot", + "i0", + "i0_", + "igamma", + "igammac", + "imag", + "index_add", + "index_copy", + "index_fill", + "index_put", + "index_put_", + "index_reduce", + "index_select", + "indices_copy", + "init_num_threads", + "inner", + "instance_norm", + "int_repr", + "inverse", + "is_complex", + "is_conj", + "is_distributed", + "is_floating_point", + "is_grad_enabled", + "is_inference", + "is_inference_mode_enabled", + "is_neg", + "is_nonzero", + "is_same_size", + "is_signed", + "is_vulkan_available", + "isclose", + "isfinite", + "isin", + "isinf", + "isnan", + "isneginf", + "isposinf", + "isreal", + "istft", + "kaiser_window", + "kl_div", + "kron", + "kthvalue", + "layer_norm", + "lcm", + "lcm_", + "ldexp", + "ldexp_", + "le", + "lerp", + "less", + "less_equal", + "lgamma", + "linspace", + "log", + "log10", + "log10_", + "log1p", + "log1p_", + "log2", + "log2_", + "log_", + "log_softmax", + "logaddexp", + "logaddexp2", + "logcumsumexp", + "logdet", + "logical_and", + "logical_not", + "logical_or", + "logical_xor", + "logit", + "logit_", + "logspace", + "logsumexp", + "lstm", + "lstm_cell", + "lt", + "lu_solve", + "lu_unpack", + "margin_ranking_loss", + "masked_fill", + "masked_scatter", + "masked_select", + "matmul", + "matrix_exp", + "matrix_power", + "max", + "max_pool1d", + "max_pool1d_with_indices", + "max_pool2d", + "max_pool3d", + "maximum", + "mean", + "median", + "min", + "minimum", + "miopen_batch_norm", + "miopen_convolution", + "miopen_convolution_add_relu", + "miopen_convolution_relu", + "miopen_convolution_transpose", + "miopen_ctc_loss", + "miopen_depthwise_convolution", + "miopen_rnn", + "mkldnn_adaptive_avg_pool2d", + "mkldnn_convolution", + "mkldnn_linear_backward_weights", + "mkldnn_max_pool2d", + "mkldnn_max_pool3d", + "mkldnn_rnn_layer", + "mm", + "mode", + "moveaxis", + "movedim", + "msort", + "mul", + "multinomial", + "multiply", + "mv", + "mvlgamma", + "nan_to_num", + "nan_to_num_", + "nanmean", + "nanmedian", + "nanquantile", + "nansum", + "narrow", + "narrow_copy", + "native_batch_norm", + "native_channel_shuffle", + "native_dropout", + "native_group_norm", + "native_layer_norm", + "native_norm", + "ne", + "neg", + "neg_", + "negative", + "negative_", + "nextafter", + "nonzero", + "nonzero_static", + "norm_except_dim", + "normal", + "not_equal", + "nuclear_norm", + "numel", + "ones", + "ones_like", + "orgqr", + "ormqr", + "outer", + "pairwise_distance", + "pdist", + "permute", + "permute_copy", + "pinverse", + "pixel_shuffle", + "pixel_unshuffle", + "poisson", + "poisson_nll_loss", + "polar", + "polygamma", + "positive", + "pow", + "prelu", + "prod", + "promote_types", + "put", + "q_per_channel_axis", + "q_per_channel_scales", + "q_per_channel_zero_points", + "q_scale", + "q_zero_point", + "qr", + "quantile", + "quantize_per_channel", + "quantize_per_tensor", + "quantize_per_tensor_dynamic", + "quantized_batch_norm", + "quantized_gru_cell", + "quantized_lstm_cell", + "quantized_max_pool1d", + "quantized_max_pool2d", + "quantized_max_pool3d", + "quantized_rnn_relu_cell", + "quantized_rnn_tanh_cell", + "rad2deg", + "rad2deg_", + "rand", + "rand_like", + "randint", + "randint_like", + "randn", + "randn_like", + "randperm", + "range", + "ravel", + "real", + "reciprocal", + "reciprocal_", + "relu", + "relu_", + "remainder", + "renorm", + "repeat_interleave", + "reshape", + "resize_as_", + "resize_as_sparse_", + "resolve_conj", + "resolve_neg", + "result_type", + "rms_norm", + "rnn_relu", + "rnn_relu_cell", + "rnn_tanh", + "rnn_tanh_cell", + "roll", + "rot90", + "round", + "round_", + "row_indices_copy", + "row_stack", + "rrelu", + "rrelu_", + "rsqrt", + "rsqrt_", + "rsub", + "saddmm", + "scalar_tensor", + "scatter", + "scatter_add", + "scatter_reduce", + "searchsorted", + "segment_reduce", + "select", + "select_copy", + "select_scatter", + "selu", + "selu_", + "set_flush_denormal", + "set_num_interop_threads", + "set_num_threads", + "sgn", + "sigmoid", + "sigmoid_", + "sign", + "signbit", + "sin", + "sin_", + "sinc", + "sinc_", + "sinh", + "sinh_", + "slice_copy", + "slice_inverse", + "slice_scatter", + "slogdet", + "smm", + "softmax", + "sort", + "sparse_bsc_tensor", + "sparse_bsr_tensor", + "sparse_compressed_tensor", + "sparse_coo_tensor", + "sparse_csc_tensor", + "sparse_csr_tensor", + "split_copy", + "split_with_sizes", + "split_with_sizes_copy", + "spmm", + "sqrt", + "sqrt_", + "square", + "square_", + "squeeze", + "squeeze_copy", + "sspaddmm", + "stack", + "std", + "std_mean", + "sub", + "subtract", + "sum", + "svd", + "swapaxes", + "swapdims", + "sym_constrain_range", + "sym_constrain_range_for_size", + "t", + "t_copy", + "take", + "take_along_dim", + "tan", + "tan_", + "tanh", + "tanh_", + "tensor", + "tensor_split", + "threshold", + "threshold_", + "tile", + "topk", + "trace", + "transpose", + "transpose_copy", + "trapezoid", + "trapz", + "triangular_solve", + "tril", + "tril_indices", + "triplet_margin_loss", + "triu", + "triu_indices", + "true_divide", + "trunc", + "trunc_", + "unbind", + "unbind_copy", + "unflatten", + "unfold_copy", + "unique_dim", + "unsafe_chunk", + "unsafe_split", + "unsafe_split_with_sizes", + "unsqueeze", + "unsqueeze_copy", + "values_copy", + "vander", + "var", + "var_mean", + "vdot", + "view_as_complex", + "view_as_complex_copy", + "view_as_real", + "view_as_real_copy", + "view_copy", + "vsplit", + "vstack", + "where", + "xlogy", + "xlogy_", + "zero_", + "zeros", + "zeros_like", +] + +@overload +def __and__(input: Tensor, other: Tensor) -> Tensor: ... +@overload +def __and__(input: Tensor, other: Number | _complex) -> Tensor: ... +@overload +def __lshift__(input: Tensor, other: Tensor) -> Tensor: ... +@overload +def __lshift__(input: Tensor, other: Number | _complex) -> Tensor: ... +@overload +def __or__(input: Tensor, other: Tensor) -> Tensor: ... +@overload +def __or__(input: Tensor, other: Number | _complex) -> Tensor: ... +@overload +def __rshift__(input: Tensor, other: Tensor) -> Tensor: ... +@overload +def __rshift__(input: Tensor, other: Number | _complex) -> Tensor: ... +@overload +def __xor__(input: Tensor, other: Tensor) -> Tensor: ... +@overload +def __xor__(input: Tensor, other: Number | _complex) -> Tensor: ... +def _adaptive_avg_pool2d( + input: Tensor, + output_size: _int | SymInt | Sequence[_int | SymInt], +) -> Tensor: ... +def _adaptive_avg_pool3d( + input: Tensor, + output_size: _int | SymInt | Sequence[_int | SymInt], +) -> Tensor: ... +def _add_batch_dim(input: Tensor, batch_dim: _int, level: _int) -> Tensor: ... +@overload +def _add_relu( + input: Tensor, + other: Tensor, + *, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: ... +@overload +def _add_relu( + input: Tensor, + other: Number | _complex, + alpha: Number | _complex = 1, +) -> Tensor: ... +@overload +def _add_relu_( + input: Tensor, + other: Tensor, + *, + alpha: Number | _complex = 1, +) -> Tensor: ... +@overload +def _add_relu_( + input: Tensor, + other: Number | _complex, + alpha: Number | _complex = 1, +) -> Tensor: ... +def _addmm_activation( + input: Tensor, + mat1: Tensor, + mat2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + use_gelu: _bool = False, + out: Tensor | None = None, +) -> Tensor: ... +@overload +def _aminmax(input: Tensor) -> tuple[Tensor, Tensor]: ... +@overload +def _aminmax( + input: Tensor, + dim: _int, + keepdim: _bool = False, +) -> tuple[Tensor, Tensor]: ... +def _amp_foreach_non_finite_check_and_unscale_( + self: tuple[Tensor, ...] | list[Tensor], + found_inf: Tensor, + inv_scale: Tensor, +) -> None: ... +def _amp_update_scale_( + input: Tensor, + growth_tracker: Tensor, + found_inf: Tensor, + scale_growth_factor: _float, + scale_backoff_factor: _float, + growth_interval: _int, +) -> Tensor: ... +@overload +def _assert_async(input: Tensor) -> None: + r""" + _assert_async(tensor) -> void + + Asynchronously assert that the contents of tensor are nonzero. For CPU tensors, + this is equivalent to ``assert tensor`` or ``assert tensor.is_nonzero()``; for + CUDA tensors, we DO NOT synchronize and you may only find out the assertion + failed at a later CUDA kernel launch. Asynchronous assertion can be helpful for + testing invariants in CUDA tensors without giving up performance. This function + is NOT intended to be used for regular error checking, as it will trash your CUDA + context if the assert fails (forcing you to restart your PyTorch process.) + + Args: + tensor (Tensor): a one element tensor to test to see if it is nonzero. Zero + elements (including False for boolean tensors) cause an assertion failure + to be raised. + """ + +@overload +def _assert_async(input: Tensor, assert_msg: str) -> None: + r""" + _assert_async(tensor) -> void + + Asynchronously assert that the contents of tensor are nonzero. For CPU tensors, + this is equivalent to ``assert tensor`` or ``assert tensor.is_nonzero()``; for + CUDA tensors, we DO NOT synchronize and you may only find out the assertion + failed at a later CUDA kernel launch. Asynchronous assertion can be helpful for + testing invariants in CUDA tensors without giving up performance. This function + is NOT intended to be used for regular error checking, as it will trash your CUDA + context if the assert fails (forcing you to restart your PyTorch process.) + + Args: + tensor (Tensor): a one element tensor to test to see if it is nonzero. Zero + elements (including False for boolean tensors) cause an assertion failure + to be raised. + """ + +def _assert_scalar(self: Number | _complex, assert_msg: str) -> None: ... +def _assert_tensor_metadata( + a: Tensor, + size: Sequence[_int | SymInt] | None = None, + stride: Sequence[_int | SymInt] | None = None, + dtype: _dtype | None = None, + *, + device: DeviceLikeType | None = None, + layout: _layout | None = None, +) -> None: ... +def _batch_norm_impl_index( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor | None, + running_var: Tensor | None, + training: _bool, + momentum: _float, + eps: _float, + cudnn_enabled: _bool, +) -> tuple[Tensor, Tensor, Tensor, Tensor, _int]: ... +def _cast_Byte(input: Tensor, non_blocking: _bool = False) -> Tensor: ... +def _cast_Char(input: Tensor, non_blocking: _bool = False) -> Tensor: ... +def _cast_Double(input: Tensor, non_blocking: _bool = False) -> Tensor: ... +def _cast_Float(input: Tensor, non_blocking: _bool = False) -> Tensor: ... +def _cast_Half(input: Tensor, non_blocking: _bool = False) -> Tensor: ... +def _cast_Int(input: Tensor, non_blocking: _bool = False) -> Tensor: ... +def _cast_Long(input: Tensor, non_blocking: _bool = False) -> Tensor: ... +def _cast_Short(input: Tensor, non_blocking: _bool = False) -> Tensor: ... +def _choose_qparams_per_tensor( + input: Tensor, + reduce_range: _bool = False, +) -> tuple[_float, _int]: ... +def _chunk_cat( + tensors: tuple[Tensor, ...] | list[Tensor], + dim: _int, + num_chunks: _int, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _coalesce(input: Tensor) -> Tensor: ... +def _compute_linear_combination( + input: Tensor, + coefficients: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _conj(input: Tensor) -> Tensor: ... +def _conj_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: ... +def _conj_physical(input: Tensor) -> Tensor: ... +def _convert_indices_from_coo_to_csr( + input: Tensor, + size: _int, + *, + out_int32: _bool = False, + out: Tensor | None = None, +) -> Tensor: ... +def _convert_indices_from_csr_to_coo( + crow_indices: Tensor, + col_indices: Tensor, + *, + out_int32: _bool = False, + transpose: _bool = False, + out: Tensor | None = None, +) -> Tensor: ... +def _convert_weight_to_int4pack(input: Tensor, innerKTiles: _int) -> Tensor: ... +def _convert_weight_to_int4pack_for_cpu( + input: Tensor, + innerKTiles: _int, +) -> Tensor: ... +@overload +def _convolution( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + stride: Sequence[_int | SymInt], + padding: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + transposed: _bool, + output_padding: _size, + groups: _int | SymInt, + benchmark: _bool, + deterministic: _bool, + cudnn_enabled: _bool, +) -> Tensor: ... +@overload +def _convolution( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + stride: Sequence[_int | SymInt], + padding: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + transposed: _bool, + output_padding: Sequence[_int | SymInt], + groups: _int | SymInt, + benchmark: _bool, + deterministic: _bool, + cudnn_enabled: _bool, + allow_tf32: _bool, +) -> Tensor: ... +def _convolution_mode( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + stride: Sequence[_int | SymInt], + padding: str, + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, +) -> Tensor: ... +def _copy_from( + input: Tensor, + dst: Tensor, + non_blocking: _bool = False, +) -> Tensor: ... +def _copy_from_and_resize(input: Tensor, dst: Tensor) -> Tensor: ... +def _cslt_compress(input: Tensor) -> Tensor: ... +def _cslt_sparse_mm( + compressed_A: Tensor, + dense_B: Tensor, + bias: Tensor | None = None, + alpha: Tensor | None = None, + out_dtype: _dtype | None = None, + transpose_result: _bool = False, + alg_id: _int = 0, + split_k: _int = 1, + split_k_mode: _int = -1, +) -> Tensor: ... +def _cslt_sparse_mm_search( + compressed_A: Tensor, + dense_B: Tensor, + bias: Tensor | None = None, + alpha: Tensor | None = None, + out_dtype: _dtype | None = None, + transpose_result: _bool = False, +) -> _int: ... +@overload +def _ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: _size, + target_lengths: _size, + blank: _int = 0, + zero_infinity: _bool = False, +) -> tuple[Tensor, Tensor]: ... +@overload +def _ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: Tensor, + target_lengths: Tensor, + blank: _int = 0, + zero_infinity: _bool = False, +) -> tuple[Tensor, Tensor]: ... +@overload +def _cudnn_ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: _size, + target_lengths: _size, + blank: _int, + deterministic: _bool, + zero_infinity: _bool, +) -> tuple[Tensor, Tensor]: ... +@overload +def _cudnn_ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: Tensor, + target_lengths: Tensor, + blank: _int, + deterministic: _bool, + zero_infinity: _bool, +) -> tuple[Tensor, Tensor]: ... +def _cudnn_init_dropout_state( + dropout: _float, + train: _bool, + dropout_seed: _int, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: ... +def _cudnn_rnn( + input: Tensor, + weight: tuple[Tensor, ...] | list[Tensor], + weight_stride0: _int, + weight_buf: Tensor | None, + hx: Tensor, + cx: Tensor | None, + mode: _int, + hidden_size: _int | SymInt, + proj_size: _int | SymInt, + num_layers: _int, + batch_first: _bool, + dropout: _float, + train: _bool, + bidirectional: _bool, + batch_sizes: Sequence[_int | SymInt], + dropout_state: Tensor | None, +) -> tuple[Tensor, Tensor, Tensor, Tensor, Tensor]: ... +def _cudnn_rnn_flatten_weight( + weight_arr: tuple[Tensor, ...] | list[Tensor], + weight_stride0: _int, + input_size: _int | SymInt, + mode: _int, + hidden_size: _int | SymInt, + proj_size: _int | SymInt, + num_layers: _int, + batch_first: _bool, + bidirectional: _bool, +) -> Tensor: ... +def _cufft_clear_plan_cache(device_index: _int) -> None: ... +def _cufft_get_plan_cache_max_size(device_index: _int) -> _int: ... +def _cufft_get_plan_cache_size(device_index: _int) -> _int: ... +def _cufft_set_plan_cache_max_size( + device_index: _int, + max_size: _int, +) -> None: ... +def _cummax_helper( + input: Tensor, + values: Tensor, + indices: Tensor, + dim: _int, +) -> None: ... +def _cummin_helper( + input: Tensor, + values: Tensor, + indices: Tensor, + dim: _int, +) -> None: ... +def _debug_has_internal_overlap(input: Tensor) -> _int: ... +def _dim_arange(like: Tensor, dim: _int) -> Tensor: ... +def _dirichlet_grad(x: Tensor, alpha: Tensor, total: Tensor) -> Tensor: ... +def _disable_functionalization(): ... +def _dyn_quant_matmul_4bit( + inp: Tensor, + packed_weights: Tensor, + block_size: _int, + in_features: _int, + out_features: _int, +) -> Tensor: ... +def _dyn_quant_pack_4bit_weight( + weights: Tensor, + scales_zeros: Tensor, + bias: Tensor | None, + block_size: _int, + in_features: _int, + out_features: _int, +) -> Tensor: ... +@overload +def _efficientzerotensor( + size: Sequence[_int | SymInt], + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: ... +@overload +def _efficientzerotensor( + *size: _int | SymInt, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: ... +def _embedding_bag( + weight: Tensor, + indices: Tensor, + offsets: Tensor, + scale_grad_by_freq: _bool = False, + mode: _int = 0, + sparse: _bool = False, + per_sample_weights: Tensor | None = None, + include_last_offset: _bool = False, + padding_idx: _int = -1, +) -> tuple[Tensor, Tensor, Tensor, Tensor]: ... +def _embedding_bag_forward_only( + weight: Tensor, + indices: Tensor, + offsets: Tensor, + scale_grad_by_freq: _bool = False, + mode: _int = 0, + sparse: _bool = False, + per_sample_weights: Tensor | None = None, + include_last_offset: _bool = False, + padding_idx: _int = -1, +) -> tuple[Tensor, Tensor, Tensor, Tensor]: ... +@overload +def _empty_affine_quantized( + size: Sequence[_int | SymInt], + *, + scale: _float = 1, + zero_point: _int = 0, + memory_format: memory_format | None = contiguous_format, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: ... +@overload +def _empty_affine_quantized( + *size: _int | SymInt, + scale: _float = 1, + zero_point: _int = 0, + memory_format: memory_format | None = contiguous_format, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: ... +@overload +def _empty_per_channel_affine_quantized( + size: Sequence[_int | SymInt], + *, + scales: Tensor, + zero_points: Tensor, + axis: _int, + memory_format: memory_format | None = contiguous_format, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: ... +@overload +def _empty_per_channel_affine_quantized( + *size: _int | SymInt, + scales: Tensor, + zero_points: Tensor, + axis: _int, + memory_format: memory_format | None = contiguous_format, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: ... +def _enable_functionalization(*, reapply_views: _bool = False) -> None: ... +def _euclidean_dist(x1: Tensor, x2: Tensor) -> Tensor: ... +def _fake_quantize_learnable_per_channel_affine( + input: Tensor, + scale: Tensor, + zero_point: Tensor, + axis: _int, + quant_min: _int, + quant_max: _int, + grad_factor: _float = 1.0, +) -> Tensor: ... +def _fake_quantize_learnable_per_tensor_affine( + input: Tensor, + scale: Tensor, + zero_point: Tensor, + quant_min: _int, + quant_max: _int, + grad_factor: _float = 1.0, +) -> Tensor: ... +def _fake_quantize_per_tensor_affine_cachemask_tensor_qparams( + input: Tensor, + scale: Tensor, + zero_point: Tensor, + fake_quant_enabled: Tensor, + quant_min: _int, + quant_max: _int, +) -> torch.return_types._fake_quantize_per_tensor_affine_cachemask_tensor_qparams: # fmt: skip + ... +def _fft_c2c( + input: Tensor, + dim: Sequence[_int | SymInt], + normalization: _int, + forward: _bool, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _fft_c2r( + input: Tensor, + dim: _size, + normalization: _int, + last_dim_size: _int | SymInt, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _fft_r2c( + input: Tensor, + dim: _size, + normalization: _int, + onesided: _bool, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _fill_mem_eff_dropout_mask_( + input: Tensor, + dropout_p: _float, + seed: _int, + offset: _int, +) -> Tensor: ... +def _flash_attention_forward_no_dropout_inplace( + out: Tensor | None, + query: Tensor, + key: Tensor, + value: Tensor, + cum_seq_q: Tensor | None, + cum_seq_k: Tensor | None, + max_q: _int | SymInt, + max_k: _int | SymInt, + dropout_p: _float, + is_causal: _bool, + return_debug_mask: _bool, + *, + scale: _float | None = None, + window_size_left: _int | SymInt | None = None, + window_size_right: _int | SymInt | None = None, + seqused_k: Tensor | None = None, + alibi_slopes: Tensor | None = None, + block_table: Tensor | None = None, + num_splits: _int | None = None, +) -> Tensor: ... +def _foobar( + input: Tensor, + arg1: _bool = True, + arg2: _bool = True, + *, + arg3: _bool = True, +) -> Tensor: ... +def _foreach_abs( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_abs(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.abs` to each Tensor of the input list. + """ + +def _foreach_abs_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_abs_(self: List[Tensor]) -> None + + Apply :func:`torch.abs` to each Tensor of the input list. + """ + +def _foreach_acos( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_acos(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.acos` to each Tensor of the input list. + """ + +def _foreach_acos_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_acos_(self: List[Tensor]) -> None + + Apply :func:`torch.acos` to each Tensor of the input list. + """ + +@overload +def _foreach_add( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_add( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], + *, + alpha: Number | _complex = 1, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_add( + self: tuple[Tensor, ...] | list[Tensor], + other: Tensor, + *, + alpha: Number | _complex = 1, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_add( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_add_( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_add_( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], + *, + alpha: Number | _complex = 1, +) -> None: ... +@overload +def _foreach_add_( + self: tuple[Tensor, ...] | list[Tensor], + other: Tensor, + *, + alpha: Number | _complex = 1, +) -> None: ... +@overload +def _foreach_add_( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> None: ... +@overload +def _foreach_addcdiv( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_addcdiv( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + scalars: Tensor, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_addcdiv( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + value: Number | _complex = 1, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_addcdiv_( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_addcdiv_( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + scalars: Tensor, +) -> None: ... +@overload +def _foreach_addcdiv_( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + value: Number | _complex = 1, +) -> None: ... +@overload +def _foreach_addcmul( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_addcmul( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + scalars: Tensor, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_addcmul( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + value: Number | _complex = 1, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_addcmul_( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_addcmul_( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + scalars: Tensor, +) -> None: ... +@overload +def _foreach_addcmul_( + self: tuple[Tensor, ...] | list[Tensor], + tensor1: tuple[Tensor, ...] | list[Tensor], + tensor2: tuple[Tensor, ...] | list[Tensor], + value: Number | _complex = 1, +) -> None: ... +def _foreach_asin( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_asin(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.asin` to each Tensor of the input list. + """ + +def _foreach_asin_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_asin_(self: List[Tensor]) -> None + + Apply :func:`torch.asin` to each Tensor of the input list. + """ + +def _foreach_atan( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_atan(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.atan` to each Tensor of the input list. + """ + +def _foreach_atan_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_atan_(self: List[Tensor]) -> None + + Apply :func:`torch.atan` to each Tensor of the input list. + """ + +def _foreach_ceil( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_ceil(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.ceil` to each Tensor of the input list. + """ + +def _foreach_ceil_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_ceil_(self: List[Tensor]) -> None + + Apply :func:`torch.ceil` to each Tensor of the input list. + """ + +@overload +def _foreach_clamp_max( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_clamp_max( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_clamp_max( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_clamp_max_( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_clamp_max_( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> None: ... +@overload +def _foreach_clamp_max_( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> None: ... +@overload +def _foreach_clamp_min( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_clamp_min( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_clamp_min( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_clamp_min_( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_clamp_min_( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> None: ... +@overload +def _foreach_clamp_min_( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> None: ... +def _foreach_clone( + self: tuple[Tensor, ...] | list[Tensor], + *, + memory_format: memory_format | None = None, +) -> tuple[Tensor, ...]: ... +def _foreach_copy_( + self: tuple[Tensor, ...] | list[Tensor], + src: tuple[Tensor, ...] | list[Tensor], + non_blocking: _bool = False, +) -> None: ... +def _foreach_cos( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_cos(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.cos` to each Tensor of the input list. + """ + +def _foreach_cos_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_cos_(self: List[Tensor]) -> None + + Apply :func:`torch.cos` to each Tensor of the input list. + """ + +def _foreach_cosh( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_cosh(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.cosh` to each Tensor of the input list. + """ + +def _foreach_cosh_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_cosh_(self: List[Tensor]) -> None + + Apply :func:`torch.cosh` to each Tensor of the input list. + """ + +@overload +def _foreach_div( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_div( + self: tuple[Tensor, ...] | list[Tensor], + other: Tensor, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_div( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_div( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_div_( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_div_( + self: tuple[Tensor, ...] | list[Tensor], + other: Tensor, +) -> None: ... +@overload +def _foreach_div_( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> None: ... +@overload +def _foreach_div_( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> None: ... +def _foreach_erf( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_erf(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.erf` to each Tensor of the input list. + """ + +def _foreach_erf_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_erf_(self: List[Tensor]) -> None + + Apply :func:`torch.erf` to each Tensor of the input list. + """ + +def _foreach_erfc( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_erfc(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.erfc` to each Tensor of the input list. + """ + +def _foreach_erfc_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_erfc_(self: List[Tensor]) -> None + + Apply :func:`torch.erfc` to each Tensor of the input list. + """ + +def _foreach_exp( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_exp(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.exp` to each Tensor of the input list. + """ + +def _foreach_exp_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_exp_(self: List[Tensor]) -> None + + Apply :func:`torch.exp` to each Tensor of the input list. + """ + +def _foreach_expm1( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_expm1(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.expm1` to each Tensor of the input list. + """ + +def _foreach_expm1_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_expm1_(self: List[Tensor]) -> None + + Apply :func:`torch.expm1` to each Tensor of the input list. + """ + +def _foreach_floor( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_floor(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.floor` to each Tensor of the input list. + """ + +def _foreach_floor_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_floor_(self: List[Tensor]) -> None + + Apply :func:`torch.floor` to each Tensor of the input list. + """ + +def _foreach_frac( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_frac(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.frac` to each Tensor of the input list. + """ + +def _foreach_frac_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_frac_(self: List[Tensor]) -> None + + Apply :func:`torch.frac` to each Tensor of the input list. + """ + +@overload +def _foreach_lerp( + self: tuple[Tensor, ...] | list[Tensor], + tensors1: tuple[Tensor, ...] | list[Tensor], + weight: Number | _complex, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_lerp( + self: tuple[Tensor, ...] | list[Tensor], + tensors1: tuple[Tensor, ...] | list[Tensor], + weight: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_lerp( + self: tuple[Tensor, ...] | list[Tensor], + tensors1: tuple[Tensor, ...] | list[Tensor], + weights: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_lerp_( + self: tuple[Tensor, ...] | list[Tensor], + tensors1: tuple[Tensor, ...] | list[Tensor], + weight: Number | _complex, +) -> None: ... +@overload +def _foreach_lerp_( + self: tuple[Tensor, ...] | list[Tensor], + tensors1: tuple[Tensor, ...] | list[Tensor], + weight: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_lerp_( + self: tuple[Tensor, ...] | list[Tensor], + tensors1: tuple[Tensor, ...] | list[Tensor], + weights: tuple[Tensor, ...] | list[Tensor], +) -> None: ... +def _foreach_lgamma( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_lgamma(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.lgamma` to each Tensor of the input list. + """ + +def _foreach_lgamma_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_lgamma_(self: List[Tensor]) -> None + + Apply :func:`torch.lgamma` to each Tensor of the input list. + """ + +def _foreach_log( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_log(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.log` to each Tensor of the input list. + """ + +def _foreach_log10( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_log10(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.log10` to each Tensor of the input list. + """ + +def _foreach_log10_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_log10_(self: List[Tensor]) -> None + + Apply :func:`torch.log10` to each Tensor of the input list. + """ + +def _foreach_log1p( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_log1p(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.log1p` to each Tensor of the input list. + """ + +def _foreach_log1p_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_log1p_(self: List[Tensor]) -> None + + Apply :func:`torch.log1p` to each Tensor of the input list. + """ + +def _foreach_log2( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_log2(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.log2` to each Tensor of the input list. + """ + +def _foreach_log2_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_log2_(self: List[Tensor]) -> None + + Apply :func:`torch.log2` to each Tensor of the input list. + """ + +def _foreach_log_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_log_(self: List[Tensor]) -> None + + Apply :func:`torch.log` to each Tensor of the input list. + """ + +def _foreach_max( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_maximum( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_maximum( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_maximum( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_maximum_( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_maximum_( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> None: ... +@overload +def _foreach_maximum_( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> None: ... +@overload +def _foreach_minimum( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_minimum( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_minimum( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_minimum_( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_minimum_( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> None: ... +@overload +def _foreach_minimum_( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> None: ... +@overload +def _foreach_mul( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_mul( + self: tuple[Tensor, ...] | list[Tensor], + other: Tensor, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_mul( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_mul( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_mul_( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_mul_( + self: tuple[Tensor, ...] | list[Tensor], + other: Tensor, +) -> None: ... +@overload +def _foreach_mul_( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> None: ... +@overload +def _foreach_mul_( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], +) -> None: ... +def _foreach_neg( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_neg(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.neg` to each Tensor of the input list. + """ + +def _foreach_neg_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_neg_(self: List[Tensor]) -> None + + Apply :func:`torch.neg` to each Tensor of the input list. + """ + +def _foreach_norm( + self: tuple[Tensor, ...] | list[Tensor], + ord: Number | _complex = 2, + dtype: _dtype | None = None, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_pow( + self: tuple[Tensor, ...] | list[Tensor], + exponent: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_pow( + self: tuple[Tensor, ...] | list[Tensor], + exponent: Number | _complex, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_pow( + self: tuple[Tensor, ...] | list[Tensor], + exponent: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_pow( + self: Number | _complex, + exponent: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_pow_( + self: tuple[Tensor, ...] | list[Tensor], + exponent: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_pow_( + self: tuple[Tensor, ...] | list[Tensor], + exponent: Number | _complex, +) -> None: ... +@overload +def _foreach_pow_( + self: tuple[Tensor, ...] | list[Tensor], + exponent: tuple[Tensor, ...] | list[Tensor], +) -> None: ... +def _foreach_powsum( + self: tuple[Tensor, ...] | list[Tensor], + ord: Number | _complex = 2, + dtype: _dtype | None = None, +) -> tuple[Tensor, ...]: ... +def _foreach_reciprocal( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_reciprocal(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.reciprocal` to each Tensor of the input list. + """ + +def _foreach_reciprocal_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_reciprocal_(self: List[Tensor]) -> None + + Apply :func:`torch.reciprocal` to each Tensor of the input list. + """ + +def _foreach_round( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_round(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.round` to each Tensor of the input list. + """ + +def _foreach_round_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_round_(self: List[Tensor]) -> None + + Apply :func:`torch.round` to each Tensor of the input list. + """ + +def _foreach_rsqrt( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +def _foreach_rsqrt_(self: tuple[Tensor, ...] | list[Tensor]) -> None: ... +def _foreach_sigmoid( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_sigmoid(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.sigmoid` to each Tensor of the input list. + """ + +def _foreach_sigmoid_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_sigmoid_(self: List[Tensor]) -> None + + Apply :func:`torch.sigmoid` to each Tensor of the input list. + """ + +def _foreach_sign( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +def _foreach_sign_(self: tuple[Tensor, ...] | list[Tensor]) -> None: ... +def _foreach_sin( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_sin(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.sin` to each Tensor of the input list. + """ + +def _foreach_sin_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_sin_(self: List[Tensor]) -> None + + Apply :func:`torch.sin` to each Tensor of the input list. + """ + +def _foreach_sinh( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_sinh(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.sinh` to each Tensor of the input list. + """ + +def _foreach_sinh_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_sinh_(self: List[Tensor]) -> None + + Apply :func:`torch.sinh` to each Tensor of the input list. + """ + +def _foreach_sqrt( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_sqrt(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.sqrt` to each Tensor of the input list. + """ + +def _foreach_sqrt_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_sqrt_(self: List[Tensor]) -> None + + Apply :func:`torch.sqrt` to each Tensor of the input list. + """ + +@overload +def _foreach_sub( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_sub( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], + *, + alpha: Number | _complex = 1, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_sub( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> tuple[Tensor, ...]: ... +@overload +def _foreach_sub_( + self: tuple[Tensor, ...] | list[Tensor], + scalars: Sequence[Number | _complex], +) -> None: ... +@overload +def _foreach_sub_( + self: tuple[Tensor, ...] | list[Tensor], + other: tuple[Tensor, ...] | list[Tensor], + *, + alpha: Number | _complex = 1, +) -> None: ... +@overload +def _foreach_sub_( + self: tuple[Tensor, ...] | list[Tensor], + scalar: Number | _complex, +) -> None: ... +def _foreach_tan( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_tan(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.tan` to each Tensor of the input list. + """ + +def _foreach_tan_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_tan_(self: List[Tensor]) -> None + + Apply :func:`torch.tan` to each Tensor of the input list. + """ + +def _foreach_tanh( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_tanh(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.tanh` to each Tensor of the input list. + """ + +def _foreach_tanh_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_tanh_(self: List[Tensor]) -> None + + Apply :func:`torch.tanh` to each Tensor of the input list. + """ + +def _foreach_trunc( + self: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + _foreach_trunc(self: List[Tensor]) -> List[Tensor] + + Apply :func:`torch.trunc` to each Tensor of the input list. + """ + +def _foreach_trunc_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_trunc_(self: List[Tensor]) -> None + + Apply :func:`torch.trunc` to each Tensor of the input list. + """ + +def _foreach_zero_(self: tuple[Tensor, ...] | list[Tensor]) -> None: + r""" + _foreach_zero_(self: List[Tensor]) -> None + + Apply :func:`torch.zero` to each Tensor of the input list. + """ + +def _from_functional_tensor(t: Tensor) -> Tensor: ... +def _functional_assert_async( + input: Tensor, + assert_msg: str, + dep_token: Tensor, +) -> Tensor: ... +def _functional_assert_scalar( + self: Number | _complex, + assert_msg: str, + dep_token: Tensor, +) -> Tensor: ... +def _functional_sym_constrain_range( + size: Number | _complex, + min: _int | None, + max: _int | None, + dep_token: Tensor, +) -> Tensor: ... +def _functional_sym_constrain_range_for_size( + size: Number | _complex, + min: _int | None, + max: _int | None, + dep_token: Tensor, +) -> Tensor: ... +def _functionalize_apply_view_metas(tensor: Tensor, base: Tensor) -> Tensor: ... +def _functionalize_are_all_mutations_hidden_from_autograd( + t: Tensor, +) -> _bool: ... +def _functionalize_are_all_mutations_under_no_grad_or_inference_mode( + t: Tensor, +) -> _bool: ... +def _functionalize_commit_update(t: Tensor) -> None: ... +def _functionalize_has_metadata_mutation(tensor: Tensor) -> _bool: ... +def _functionalize_inductor_storage_resized_counter(t: Tensor) -> _int: ... +def _functionalize_is_symbolic(tensor: Tensor) -> _bool: ... +def _functionalize_mark_mutation_hidden_from_autograd(t: Tensor) -> None: ... +def _functionalize_mark_storage_changed(tensor: Tensor) -> _bool: ... +def _functionalize_mutation_counter(t: Tensor) -> _int: ... +def _functionalize_replace(self_: Tensor, other: Tensor) -> None: ... +def _functionalize_storage_changed_counter(t: Tensor) -> _int: ... +def _functionalize_sync(t: Tensor) -> None: ... +def _functionalize_unsafe_set(dst: Tensor, src: Tensor) -> None: ... +def _functionalize_was_inductor_storage_resized(t: Tensor) -> _bool: ... +def _functionalize_was_storage_changed(tensor: Tensor) -> _bool: ... +@overload +def _fused_adagrad_( + self: tuple[Tensor, ...] | list[Tensor], + grads: tuple[Tensor, ...] | list[Tensor], + state_sums: tuple[Tensor, ...] | list[Tensor], + state_steps: tuple[Tensor, ...] | list[Tensor], + *, + lr: Tensor, + lr_decay: _float, + weight_decay: _float, + eps: _float, + maximize: _bool, + grad_scale: Tensor | None = None, + found_inf: Tensor | None = None, +) -> None: ... +@overload +def _fused_adagrad_( + self: tuple[Tensor, ...] | list[Tensor], + grads: tuple[Tensor, ...] | list[Tensor], + state_sums: tuple[Tensor, ...] | list[Tensor], + state_steps: tuple[Tensor, ...] | list[Tensor], + *, + lr: _float, + lr_decay: _float, + weight_decay: _float, + eps: _float, + maximize: _bool, + grad_scale: Tensor | None = None, + found_inf: Tensor | None = None, +) -> None: ... +@overload +def _fused_adam_( + self: tuple[Tensor, ...] | list[Tensor], + grads: tuple[Tensor, ...] | list[Tensor], + exp_avgs: tuple[Tensor, ...] | list[Tensor], + exp_avg_sqs: tuple[Tensor, ...] | list[Tensor], + max_exp_avg_sqs: tuple[Tensor, ...] | list[Tensor], + state_steps: tuple[Tensor, ...] | list[Tensor], + *, + lr: Tensor, + beta1: _float, + beta2: _float, + weight_decay: _float, + eps: _float, + amsgrad: _bool, + maximize: _bool, + grad_scale: Tensor | None = None, + found_inf: Tensor | None = None, +) -> None: ... +@overload +def _fused_adam_( + self: tuple[Tensor, ...] | list[Tensor], + grads: tuple[Tensor, ...] | list[Tensor], + exp_avgs: tuple[Tensor, ...] | list[Tensor], + exp_avg_sqs: tuple[Tensor, ...] | list[Tensor], + max_exp_avg_sqs: tuple[Tensor, ...] | list[Tensor], + state_steps: tuple[Tensor, ...] | list[Tensor], + *, + lr: _float, + beta1: _float, + beta2: _float, + weight_decay: _float, + eps: _float, + amsgrad: _bool, + maximize: _bool, + grad_scale: Tensor | None = None, + found_inf: Tensor | None = None, +) -> None: ... +@overload +def _fused_adamw_( + self: tuple[Tensor, ...] | list[Tensor], + grads: tuple[Tensor, ...] | list[Tensor], + exp_avgs: tuple[Tensor, ...] | list[Tensor], + exp_avg_sqs: tuple[Tensor, ...] | list[Tensor], + max_exp_avg_sqs: tuple[Tensor, ...] | list[Tensor], + state_steps: tuple[Tensor, ...] | list[Tensor], + *, + lr: Tensor, + beta1: _float, + beta2: _float, + weight_decay: _float, + eps: _float, + amsgrad: _bool, + maximize: _bool, + grad_scale: Tensor | None = None, + found_inf: Tensor | None = None, +) -> None: ... +@overload +def _fused_adamw_( + self: tuple[Tensor, ...] | list[Tensor], + grads: tuple[Tensor, ...] | list[Tensor], + exp_avgs: tuple[Tensor, ...] | list[Tensor], + exp_avg_sqs: tuple[Tensor, ...] | list[Tensor], + max_exp_avg_sqs: tuple[Tensor, ...] | list[Tensor], + state_steps: tuple[Tensor, ...] | list[Tensor], + *, + lr: _float, + beta1: _float, + beta2: _float, + weight_decay: _float, + eps: _float, + amsgrad: _bool, + maximize: _bool, + grad_scale: Tensor | None = None, + found_inf: Tensor | None = None, +) -> None: ... +def _fused_dropout( + input: Tensor, + p: _float, + generator: Generator | None = None, +) -> tuple[Tensor, Tensor]: ... +def _fused_moving_avg_obs_fq_helper( + input: Tensor, + observer_on: Tensor, + fake_quant_on: Tensor, + running_min: Tensor, + running_max: Tensor, + scale: Tensor, + zero_point: Tensor, + averaging_const: _float, + quant_min: _int, + quant_max: _int, + ch_axis: _int, + per_row_fake_quant: _bool = False, + symmetric_quant: _bool = False, +) -> torch.return_types._fused_moving_avg_obs_fq_helper: ... +def _fused_rms_norm( + input: Tensor, + normalized_shape: _size, + weight: Tensor | None, + eps: _float | None, +) -> tuple[Tensor, Tensor]: ... +def _fused_sdp_choice( + query: Tensor, + key: Tensor, + value: Tensor, + attn_mask: Tensor | None = None, + dropout_p: _float = 0.0, + is_causal: _bool = False, + *, + scale: _float | None = None, + enable_gqa: _bool = False, +) -> _int: ... +@overload +def _fused_sgd_( + self: tuple[Tensor, ...] | list[Tensor], + grads: tuple[Tensor, ...] | list[Tensor], + momentum_buffer_list: tuple[Tensor, ...] | list[Tensor], + *, + weight_decay: _float, + momentum: _float, + lr: Tensor, + dampening: _float, + nesterov: _bool, + maximize: _bool, + is_first_step: _bool, + grad_scale: Tensor | None = None, + found_inf: Tensor | None = None, +) -> None: ... +@overload +def _fused_sgd_( + self: tuple[Tensor, ...] | list[Tensor], + grads: tuple[Tensor, ...] | list[Tensor], + momentum_buffer_list: tuple[Tensor, ...] | list[Tensor], + *, + weight_decay: _float, + momentum: _float, + lr: _float, + dampening: _float, + nesterov: _bool, + maximize: _bool, + is_first_step: _bool, + grad_scale: Tensor | None = None, + found_inf: Tensor | None = None, +) -> None: ... +def _fw_primal_copy( + input: Tensor, + level: _int, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _grid_sampler_2d_cpu_fallback( + input: Tensor, + grid: Tensor, + interpolation_mode: _int, + padding_mode: _int, + align_corners: _bool, +) -> Tensor: ... +def _grouped_mm( + input: Tensor, + mat2: Tensor, + offs: Tensor | None = None, + bias: Tensor | None = None, + out_dtype: _dtype | None = None, +) -> Tensor: ... +def _has_compatible_shallow_copy_type( + input: Tensor, + from_: Tensor, +) -> _bool: ... +def _histogramdd_bin_edges( + input: Tensor, + bins: _size, + *, + range: Sequence[_float] | None = None, + weight: Tensor | None = None, + density: _bool = False, +) -> tuple[Tensor, ...]: ... +def _histogramdd_from_bin_cts( + input: Tensor, + bins: _size, + *, + range: Sequence[_float] | None = None, + weight: Tensor | None = None, + density: _bool = False, +) -> Tensor: ... +def _histogramdd_from_bin_tensors( + input: Tensor, + bins: tuple[Tensor, ...] | list[Tensor], + *, + weight: Tensor | None = None, + density: _bool = False, +) -> Tensor: ... +def _index_put_impl_( + input: Tensor, + indices: tuple[Tensor | None, ...] | list[Tensor | None] | None, + values: Tensor, + accumulate: _bool = False, + unsafe: _bool = False, +) -> Tensor: ... +def _indices_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: ... +def _int_mm( + input: Tensor, + mat2: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _is_all_true(input: Tensor) -> Tensor: ... +def _is_any_true(input: Tensor) -> Tensor: ... +def _is_functional_tensor(t: Tensor) -> _bool: ... +def _is_functional_tensor_base(t: Tensor) -> _bool: ... +def _is_zerotensor(input: Tensor) -> _bool: ... +def _lazy_clone(input: Tensor) -> Tensor: ... +def _linalg_check_errors( + info: Tensor, + api_name: str, + *, + is_matrix: _bool, +) -> None: ... +def _linalg_det( + A: Tensor, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types._linalg_det: ... +def _linalg_eigh( + A: Tensor, + UPLO: str = "L", + compute_v: _bool = True, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types._linalg_eigh: ... +def _linalg_slogdet( + A: Tensor, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types._linalg_slogdet: ... +def _linalg_solve_ex( + A: Tensor, + B: Tensor, + *, + left: _bool = True, + check_errors: _bool = False, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types._linalg_solve_ex: ... +def _linalg_svd( + A: Tensor, + full_matrices: _bool = False, + compute_uv: _bool = True, + *, + driver: str | None = None, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types._linalg_svd: ... +def _log_softmax( + input: Tensor, + dim: _int, + half_to_float: _bool, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _log_softmax_backward_data( + grad_output: Tensor, + output: Tensor, + dim: _int, + input_dtype: _dtype, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _logcumsumexp( + input: Tensor, + dim: _int, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _lstm_mps( + input: Tensor, + hx: tuple[Tensor, ...] | list[Tensor], + params: tuple[Tensor, ...] | list[Tensor], + has_biases: _bool, + num_layers: _int, + dropout: _float, + train: _bool, + bidirectional: _bool, + batch_first: _bool, +) -> tuple[Tensor, Tensor, Tensor, Tensor, Tensor, Tensor]: ... +def _lu_with_info( + input: Tensor, + pivot: _bool = True, + check_errors: _bool = True, +) -> torch.return_types._lu_with_info: ... +def _make_dep_token( + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: ... +def _make_dual(primal: Tensor, tangent: Tensor, level: _int) -> Tensor: ... +def _make_dual_copy( + primal: Tensor, + tangent: Tensor, + level: _int, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _make_per_channel_quantized_tensor( + input: Tensor, + scale: Tensor, + zero_point: Tensor, + axis: _int, +) -> Tensor: ... +def _make_per_tensor_quantized_tensor( + input: Tensor, + scale: _float, + zero_point: _int, +) -> Tensor: ... +def _masked_scale(input: Tensor, mask: Tensor, scale: _float) -> Tensor: ... +def _masked_softmax( + input: Tensor, + mask: Tensor, + dim: _int | None = None, + mask_type: _int | None = None, +) -> Tensor: ... +def _mixed_dtypes_linear( + input: Tensor, + weight: Tensor, + scale: Tensor, + *, + bias: Tensor | None = None, + activation: str | None = None, +) -> Tensor: ... +def _mkldnn_reshape(input: Tensor, shape: _size) -> Tensor: ... +def _mkldnn_transpose(input: Tensor, dim0: _int, dim1: _int) -> Tensor: ... +def _mkldnn_transpose_(input: Tensor, dim0: _int, dim1: _int) -> Tensor: ... +def _mps_convolution( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + padding: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, +) -> Tensor: ... +def _mps_convolution_transpose( + input: Tensor, + weight: Tensor, + padding: Sequence[_int | SymInt], + output_padding: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, +) -> Tensor: ... +@overload +def _native_batch_norm_legit( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor, + running_var: Tensor, + training: _bool, + momentum: _float, + eps: _float, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> tuple[Tensor, Tensor, Tensor]: ... +@overload +def _native_batch_norm_legit( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + training: _bool, + momentum: _float, + eps: _float, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> tuple[Tensor, Tensor, Tensor]: ... +def _native_batch_norm_legit_no_training( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor, + running_var: Tensor, + momentum: _float, + eps: _float, +) -> tuple[Tensor, Tensor, Tensor]: ... +def _native_multi_head_attention( + query: Tensor, + key: Tensor, + value: Tensor, + embed_dim: _int, + num_head: _int, + qkv_weight: Tensor, + qkv_bias: Tensor, + proj_weight: Tensor, + proj_bias: Tensor, + mask: Tensor | None = None, + need_weights: _bool = True, + average_attn_weights: _bool = True, + mask_type: _int | None = None, +) -> tuple[Tensor, Tensor]: ... +def _neg_view(input: Tensor) -> Tensor: ... +def _neg_view_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: ... +def _nested_compute_contiguous_strides_offsets( + nested_size: Tensor, +) -> tuple[Tensor, Tensor]: ... +def _nested_from_padded( + padded: Tensor, + cpu_nested_shape_example: Tensor, + fuse_transform_0213: _bool = False, +) -> Tensor: ... +def _nested_from_padded_and_nested_example( + padded: Tensor, + nt_example: Tensor, +) -> Tensor: ... +def _nested_from_padded_tensor( + padded: Tensor, + offsets: Tensor, + dummy: Tensor, + ragged_idx: _int = 1, + min_seqlen: Tensor | None = None, + max_seqlen: Tensor | None = None, + sum_S: _int | SymInt | None = None, +) -> Tensor: ... +def _nested_get_jagged_dummy(any: Tensor) -> Tensor: ... +def _nested_get_lengths(input: Tensor) -> Tensor: ... +def _nested_get_max_seqlen(input: Tensor) -> Tensor: ... +def _nested_get_min_seqlen(input: Tensor) -> Tensor: ... +def _nested_get_offsets(input: Tensor) -> Tensor: ... +def _nested_get_ragged_idx(input: Tensor) -> _int: ... +def _nested_get_values(input: Tensor) -> Tensor: ... +def _nested_get_values_copy( + input: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _nested_tensor_from_mask( + t: Tensor, + mask: Tensor, + mask_check: _bool = True, +) -> Tensor: ... +def _nested_tensor_from_mask_left_aligned(t: Tensor, mask: Tensor) -> _bool: ... +def _nested_tensor_from_tensor_list( + list: tuple[Tensor, ...] | list[Tensor], + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = None, +) -> Tensor: ... +def _nested_tensor_softmax_with_shape( + input: Tensor, + query: Tensor, +) -> Tensor: ... +def _nested_view_from_buffer( + input: Tensor, + nested_size: Tensor, + nested_strides: Tensor, + offsets: Tensor, +) -> Tensor: ... +def _nested_view_from_buffer_copy( + input: Tensor, + nested_size: Tensor, + nested_strides: Tensor, + offsets: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _nested_view_from_jagged( + input: Tensor, + offsets: Tensor, + dummy: Tensor, + lengths: Tensor | None = None, + ragged_idx: _int = 1, + min_seqlen: Tensor | None = None, + max_seqlen: Tensor | None = None, +) -> Tensor: ... +def _nested_view_from_jagged_copy( + input: Tensor, + offsets: Tensor, + dummy: Tensor, + lengths: Tensor | None = None, + ragged_idx: _int = 1, + min_seqlen: Tensor | None = None, + max_seqlen: Tensor | None = None, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _nnpack_available() -> _bool: ... +def _nnpack_spatial_convolution( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + padding: _int | SymInt | Sequence[_int | SymInt], + stride: _int | SymInt | Sequence[_int | SymInt] = 1, +) -> Tensor: ... +def _pack_padded_sequence( + input: Tensor, + lengths: Tensor, + batch_first: _bool, +) -> tuple[Tensor, Tensor]: ... +def _pad_packed_sequence( + data: Tensor, + batch_sizes: Tensor, + batch_first: _bool, + padding_value: Number | _complex, + total_length: _int, +) -> tuple[Tensor, Tensor]: ... +def _philox_key_fold_in(key: Tensor, data: _int) -> Tensor: ... +def _philox_key_split(key: Tensor, num_splits: _int) -> Tensor: ... +def _philox_normal_( + input: Tensor, + key: Tensor, + mean: _float = 0, + std: _float = 1, +) -> Tensor: ... +def _philox_uniform_( + input: Tensor, + key: Tensor, + low: _float = 0, + high: _float = 1, +) -> Tensor: ... +def _pin_memory( + input: Tensor, + device: DeviceLikeType | None = None, +) -> Tensor: ... +def _prelu_kernel(input: Tensor, weight: Tensor) -> Tensor: ... +def _print(s: str) -> None: ... +def _propagate_xla_data(input: Tensor, output: Tensor) -> None: ... +def _remove_batch_dim( + input: Tensor, + level: _int, + batch_size: _int | SymInt, + out_dim: _int, +) -> Tensor: ... +def _reshape_alias_copy( + input: Tensor, + size: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + *, + out: Tensor | None = None, +) -> Tensor: ... +def _reshape_from_tensor(input: Tensor, shape: Tensor) -> Tensor: ... +def _resize_output_( + input: Tensor, + size: Sequence[_int | SymInt], + device: DeviceLikeType | None, +) -> Tensor: ... +def _rowwise_prune( + weight: Tensor, + mask: Tensor, + compressed_indices_dtype: _dtype, +) -> tuple[Tensor, Tensor]: ... +def _safe_softmax( + input: Tensor, + dim: _int, + dtype: _dtype | None = None, +) -> Tensor: ... +def _sample_dirichlet( + input: Tensor, + generator: Generator | None = None, +) -> Tensor: ... +def _saturate_weight_to_fp16(weight: Tensor) -> Tensor: ... +def _scaled_dot_product_attention_math( + query: Tensor, + key: Tensor, + value: Tensor, + attn_mask: Tensor | None = None, + dropout_p: _float = 0.0, + is_causal: _bool = False, + dropout_mask: Tensor | None = None, + *, + scale: _float | None = None, + enable_gqa: _bool = False, +) -> tuple[Tensor, Tensor]: ... +def _scaled_dot_product_attention_math_for_mps( + query: Tensor, + key: Tensor, + value: Tensor, + attn_mask: Tensor | None = None, + dropout_p: _float = 0.0, + is_causal: _bool = False, + dropout_mask: Tensor | None = None, + *, + scale: _float | None = None, + enable_gqa: _bool = False, +) -> tuple[Tensor, Tensor]: ... +def _scaled_dot_product_cudnn_attention( + query: Tensor, + key: Tensor, + value: Tensor, + attn_bias: Tensor | None, + compute_log_sumexp: _bool, + dropout_p: _float = 0.0, + is_causal: _bool = False, + return_debug_mask: _bool = False, + *, + scale: _float | None = None, +) -> torch.return_types._scaled_dot_product_cudnn_attention: ... +def _scaled_dot_product_efficient_attention( + query: Tensor, + key: Tensor, + value: Tensor, + attn_bias: Tensor | None, + compute_log_sumexp: _bool, + dropout_p: _float = 0.0, + is_causal: _bool = False, + *, + scale: _float | None = None, +) -> torch.return_types._scaled_dot_product_efficient_attention: ... +@overload +def _scaled_dot_product_flash_attention( + query: Tensor, + key: Tensor, + value: Tensor, + q_descale: Tensor | None, + k_descale: Tensor | None, + v_descale: Tensor | None, + dropout_p: _float = 0.0, + is_causal: _bool = False, + return_debug_mask: _bool = False, + *, + scale: _float | None = None, +) -> torch.return_types._scaled_dot_product_flash_attention: ... +@overload +def _scaled_dot_product_flash_attention( + query: Tensor, + key: Tensor, + value: Tensor, + dropout_p: _float = 0.0, + is_causal: _bool = False, + return_debug_mask: _bool = False, + *, + scale: _float | None = None, +) -> torch.return_types._scaled_dot_product_flash_attention: ... +def _scaled_dot_product_flash_attention_for_cpu( + query: Tensor, + key: Tensor, + value: Tensor, + dropout_p: _float = 0.0, + is_causal: _bool = False, + *, + attn_mask: Tensor | None = None, + scale: _float | None = None, +) -> torch.return_types._scaled_dot_product_flash_attention_for_cpu: ... +def _scaled_grouped_mm( + input: Tensor, + mat2: Tensor, + scale_a: Tensor, + scale_b: Tensor, + offs: Tensor | None = None, + bias: Tensor | None = None, + scale_result: Tensor | None = None, + out_dtype: _dtype | None = None, + use_fast_accum: _bool = False, +) -> Tensor: ... +def _scaled_grouped_mm_v2( + input: Tensor, + mat2: Tensor, + scale_a: tuple[Tensor, ...] | list[Tensor], + recipe_a: _size, + swizzle_a: _size, + scale_b: tuple[Tensor, ...] | list[Tensor], + recipe_b: _size, + swizzle_b: _size, + offs: Tensor | None = None, + bias: Tensor | None = None, + out_dtype: _dtype | None = None, + contraction_dim: _size = (), + use_fast_accum: _bool = False, +) -> Tensor: ... +def _scaled_mm( + input: Tensor, + mat2: Tensor, + scale_a: Tensor, + scale_b: Tensor, + bias: Tensor | None = None, + scale_result: Tensor | None = None, + out_dtype: _dtype | None = None, + use_fast_accum: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _scaled_mm_v2( + input: Tensor, + mat2: Tensor, + scale_a: tuple[Tensor, ...] | list[Tensor], + recipe_a: _size, + swizzle_a: _size, + scale_b: tuple[Tensor, ...] | list[Tensor], + recipe_b: _size, + swizzle_b: _size, + bias: Tensor | None, + out_dtype: _dtype | None, + contraction_dim: _size = (), + use_fast_accum: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _shape_as_tensor(input: Tensor) -> Tensor: ... +def _sobol_engine_draw( + quasi: Tensor, + n: _int, + sobolstate: Tensor, + dimension: _int, + num_generated: _int, + dtype: _dtype | None, +) -> tuple[Tensor, Tensor]: ... +def _sobol_engine_ff_( + input: Tensor, + n: _int, + sobolstate: Tensor, + dimension: _int, + num_generated: _int, +) -> Tensor: ... +def _sobol_engine_initialize_state_( + input: Tensor, + dimension: _int, +) -> Tensor: ... +def _sobol_engine_scramble_( + input: Tensor, + ltm: Tensor, + dimension: _int, +) -> Tensor: ... +def _softmax( + input: Tensor, + dim: _int, + half_to_float: _bool, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _softmax_backward_data( + grad_output: Tensor, + output: Tensor, + dim: _int, + input_dtype: _dtype, + *, + grad_input: Tensor | None = None, +) -> Tensor: ... +def _sparse_broadcast_to(input: Tensor, size: _size) -> Tensor: ... +def _sparse_broadcast_to_copy( + input: Tensor, + size: _size, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _sparse_csr_prod( + input: Tensor, + dim: _int | _size, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, +) -> Tensor: ... +def _sparse_csr_sum( + input: Tensor, + dim: _int | _size, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, +) -> Tensor: ... +def _sparse_log_softmax_backward_data( + grad_output: Tensor, + output: Tensor, + dim: _int, + input: Tensor, +) -> Tensor: ... +def _sparse_semi_structured_addmm( + input: Tensor, + mat1: Tensor, + mat1_meta: Tensor, + mat2: Tensor, + *, + alpha: Number | _complex = 1, + beta: Number | _complex = 1, + out_dtype: _dtype | None = None, +) -> Tensor: ... +def _sparse_semi_structured_apply( + input: Tensor, + thread_masks: Tensor, +) -> tuple[Tensor, Tensor]: ... +def _sparse_semi_structured_apply_dense( + input: Tensor, + thread_masks: Tensor, +) -> Tensor: ... +def _sparse_semi_structured_linear( + input: Tensor, + weight: Tensor, + meta: Tensor, + *, + bias: Tensor | None = None, + activation: str | None = None, + out_dtype: _dtype | None = None, +) -> Tensor: ... +def _sparse_semi_structured_mm( + mat1: Tensor, + mat1_meta: Tensor, + mat2: Tensor, + *, + out_dtype: _dtype | None = None, +) -> Tensor: ... +def _sparse_semi_structured_tile( + input: Tensor, + algorithm: str = "", + use_cutlass: _bool = True, +) -> tuple[Tensor, Tensor, Tensor, Tensor, Tensor]: ... +def _sparse_softmax_backward_data( + grad_output: Tensor, + output: Tensor, + dim: _int, + input: Tensor, +) -> Tensor: ... +def _sparse_sparse_matmul(input: Tensor, other: Tensor) -> Tensor: ... +@overload +def _sparse_sum(input: Tensor) -> Tensor: ... +@overload +def _sparse_sum(input: Tensor, *, dtype: _dtype) -> Tensor: ... +@overload +def _sparse_sum(input: Tensor, dim: _int | _size) -> Tensor: ... +@overload +def _sparse_sum( + input: Tensor, + dim: _int | _size, + *, + dtype: _dtype, +) -> Tensor: ... +def _stack( + tensors: tuple[Tensor, ...] | list[Tensor], + dim: _int = 0, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _standard_gamma( + input: Tensor, + generator: Generator | None = None, +) -> Tensor: ... +def _standard_gamma_grad(input: Tensor, output: Tensor) -> Tensor: ... +def _sync(t: Tensor) -> None: ... +@overload +def _test_autograd_multiple_dispatch(input: Tensor) -> Tensor: ... +@overload +def _test_autograd_multiple_dispatch(input: Tensor, b: _bool) -> Tensor: ... +def _test_autograd_multiple_dispatch_view(input: Tensor) -> Tensor: ... +def _test_autograd_multiple_dispatch_view_copy( + input: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: ... +def _test_check_tensor(input: Tensor) -> Tensor: ... +def _test_functorch_fallback(input: Tensor, other: Tensor) -> Tensor: ... +def _test_parallel_materialize( + input: Tensor, + num_parallel: _int, + skip_first: _bool = False, +) -> Tensor: ... +def _test_serialization_subcmul( + input: Tensor, + other: Tensor, + alpha: Number | _complex = 1, +) -> Tensor: ... +def _to_cpu( + tensors: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: ... +def _to_functional_tensor(t: Tensor) -> Tensor: ... +def _to_sparse_semi_structured(dense: Tensor) -> tuple[Tensor, Tensor]: ... +def _transform_bias_rescale_qkv( + qkv: Tensor, + qkv_bias: Tensor, + num_heads: _int, +) -> tuple[Tensor, Tensor, Tensor]: ... +def _transformer_encoder_layer_fwd( + src: Tensor, + embed_dim: _int, + num_heads: _int, + qkv_weight: Tensor, + qkv_bias: Tensor, + proj_weight: Tensor, + proj_bias: Tensor, + use_gelu: _bool, + norm_first: _bool, + eps: _float, + norm_weight_1: Tensor, + norm_bias_1: Tensor, + norm_weight_2: Tensor, + norm_bias_2: Tensor, + ffn_weight_1: Tensor, + ffn_bias_1: Tensor, + ffn_weight_2: Tensor, + ffn_bias_2: Tensor, + mask: Tensor | None = None, + mask_type: _int | None = None, +) -> Tensor: ... +def _trilinear( + i1: Tensor, + i2: Tensor, + i3: Tensor, + expand1: _size, + expand2: _size, + expand3: _size, + sumdim: _size, + unroll_dim: _int = 1, +) -> Tensor: ... +def _triton_multi_head_attention( + query: Tensor, + key: Tensor, + value: Tensor, + embed_dim: _int, + num_head: _int, + qkv_weight: Tensor, + qkv_bias: Tensor, + proj_weight: Tensor, + proj_bias: Tensor, + mask: Tensor | None = None, +) -> Tensor: ... +def _triton_scaled_dot_attention( + q: Tensor, + k: Tensor, + v: Tensor, + dropout_p: _float = 0.0, +) -> Tensor: ... +def _unique( + input: Tensor, + sorted: _bool = True, + return_inverse: _bool = False, +) -> tuple[Tensor, Tensor]: ... +def _unique2( + input: Tensor, + sorted: _bool = True, + return_inverse: _bool = False, + return_counts: _bool = False, +) -> tuple[Tensor, Tensor, Tensor]: ... +def _unpack_dual( + dual: Tensor, + level: _int, +) -> torch.return_types._unpack_dual: ... +def _unsafe_index( + input: Tensor, + indices: tuple[Tensor | None, ...] | list[Tensor | None] | None, +) -> Tensor: ... +def _unsafe_index_put( + input: Tensor, + indices: tuple[Tensor | None, ...] | list[Tensor | None] | None, + values: Tensor, + accumulate: _bool = False, +) -> Tensor: ... +def _unsafe_masked_index( + input: Tensor, + mask: Tensor, + indices: tuple[Tensor | None, ...] | list[Tensor | None] | None, + fill: Number | _complex, +) -> Tensor: ... +def _unsafe_masked_index_put_accumulate( + input: Tensor, + mask: Tensor, + indices: tuple[Tensor | None, ...] | list[Tensor | None] | None, + values: Tensor, +) -> Tensor: ... +@overload +def _use_cudnn_ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: Tensor, + target_lengths: Tensor, + blank: _int, +) -> _bool: ... +@overload +def _use_cudnn_ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: _size, + target_lengths: _size, + blank: _int, +) -> _bool: ... +def _use_cudnn_rnn_flatten_weight() -> _bool: ... +@overload +def _use_miopen_ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: Tensor, + target_lengths: Tensor, + blank: _int, +) -> _bool: ... +@overload +def _use_miopen_ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: _size, + target_lengths: _size, + blank: _int, +) -> _bool: ... +def _validate_compressed_sparse_indices( + is_crow: _bool, + compressed_idx: Tensor, + plain_idx: Tensor, + cdim: _int, + dim: _int, + nnz: _int, +) -> None: ... +def _validate_sparse_bsc_tensor_args( + ccol_indices: Tensor, + row_indices: Tensor, + values: Tensor, + size: _size, + check_pinning: _bool | None = None, +) -> None: ... +def _validate_sparse_bsr_tensor_args( + crow_indices: Tensor, + col_indices: Tensor, + values: Tensor, + size: _size, + check_pinning: _bool | None = None, +) -> None: ... +def _validate_sparse_compressed_tensor_args( + compressed_indices: Tensor, + plain_indices: Tensor, + values: Tensor, + size: _size, + layout: _layout, + check_pinning: _bool | None = None, +) -> None: ... +def _validate_sparse_coo_tensor_args( + indices: Tensor, + values: Tensor, + size: _size, + is_coalesced: _bool | None = None, + check_pinning: _bool | None = None, +) -> None: ... +def _validate_sparse_csc_tensor_args( + ccol_indices: Tensor, + row_indices: Tensor, + values: Tensor, + size: _size, + check_pinning: _bool | None = None, +) -> None: ... +def _validate_sparse_csr_tensor_args( + crow_indices: Tensor, + col_indices: Tensor, + values: Tensor, + size: _size, + check_pinning: _bool | None = None, +) -> None: ... +def _values_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: ... +def _weight_int4pack_mm( + input: Tensor, + mat2: Tensor, + qGroupSize: _int, + qScaleAndZeros: Tensor, +) -> Tensor: ... +def _weight_int4pack_mm_for_cpu( + input: Tensor, + mat2: Tensor, + qGroupSize: _int, + qScaleAndZeros: Tensor, +) -> Tensor: ... +def _weight_int4pack_mm_with_scales_and_zeros( + input: Tensor, + mat2: Tensor, + qGroupSize: _int, + qScale: Tensor, + qZeros: Tensor, +) -> Tensor: ... +def _weight_int8pack_mm( + input: Tensor, + mat2: Tensor, + scales: Tensor, +) -> Tensor: ... +def _weight_norm(v: Tensor, g: Tensor, dim: _int = 0) -> Tensor: ... +def _weight_norm_interface( + v: Tensor, + g: Tensor, + dim: _int = 0, +) -> tuple[Tensor, Tensor]: ... +def _wrapped_linear_prepack( + weight: Tensor, + weight_scale: Tensor, + weight_zero_point: Tensor, + bias: Tensor, +) -> Tensor: ... +def _wrapped_quantized_linear_prepacked( + input: Tensor, + input_scale: Tensor, + input_zero_point: Tensor, + packed_weight: Tensor, + output_scale: Tensor, + output_zero_point: Tensor, + out_channel: _int, +) -> Tensor: ... +def abs(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + abs(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Computes the absolute value of each element in :attr:`input`. + + .. math:: + \text{out}_{i} = |\text{input}_{i}| + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.abs(torch.tensor([-1, -2, 3])) + tensor([ 1, 2, 3]) + """ + +def abs_(input: Tensor) -> Tensor: ... +def absolute(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + absolute(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Alias for :func:`torch.abs` + """ + +def acos(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + acos(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Returns a new tensor with the arccosine (in radians) of each element in :attr:`input`. + + .. math:: + \text{out}_{i} = \cos^{-1}(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.3348, -0.5889, 0.2005, -0.1584]) + >>> torch.acos(a) + tensor([ 1.2294, 2.2004, 1.3690, 1.7298]) + """ + +def acos_(input: Tensor) -> Tensor: ... +def acosh(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + acosh(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Returns a new tensor with the inverse hyperbolic cosine of the elements of :attr:`input`. + + .. math:: + \text{out}_{i} = \cosh^{-1}(\text{input}_{i}) + + Note: + The domain of the inverse hyperbolic cosine is `[1, inf)` and values outside this range + will be mapped to ``NaN``, except for `+ INF` for which the output is mapped to `+ INF`. + + Args: + input (Tensor): the input tensor. + + Keyword arguments: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4).uniform_(1, 2) + >>> a + tensor([ 1.3192, 1.9915, 1.9674, 1.7151 ]) + >>> torch.acosh(a) + tensor([ 0.7791, 1.3120, 1.2979, 1.1341 ]) + """ + +def acosh_(input: Tensor) -> Tensor: ... +def adaptive_avg_pool1d(input: Tensor, output_size: _int | _size) -> Tensor: ... +def adaptive_max_pool1d( + input: Tensor, + output_size: _int | _size, +) -> tuple[Tensor, Tensor]: ... +@overload +def add( + input: Tensor | Number | _complex, + other: Tensor | Number | _complex, + *, + alpha: Number | _complex | None = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + add(input, other, *, alpha=1, out=None) -> Tensor + + Adds :attr:`other`, scaled by :attr:`alpha`, to :attr:`input`. + + .. math:: + \text{{out}}_i = \text{{input}}_i + \text{{alpha}} \times \text{{other}}_i + + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer, float, and complex inputs. + + Args: + input (Tensor): the input tensor. + other (Tensor or Number): the tensor or number to add to :attr:`input`. + + Keyword arguments: + alpha (Number): the multiplier for :attr:`other`. + out (Tensor, optional): the output tensor. + + Examples:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.0202, 1.0985, 1.3506, -0.6056]) + >>> torch.add(a, 20) + tensor([ 20.0202, 21.0985, 21.3506, 19.3944]) + + >>> b = torch.randn(4) + >>> b + tensor([-0.9732, -0.3497, 0.6245, 0.4022]) + >>> c = torch.randn(4, 1) + >>> c + tensor([[ 0.3743], + [-1.7724], + [-0.5811], + [-0.8017]]) + >>> torch.add(b, c, alpha=10) + tensor([[ 2.7695, 3.3930, 4.3672, 4.1450], + [-18.6971, -18.0736, -17.0994, -17.3216], + [ -6.7845, -6.1610, -5.1868, -5.4090], + [ -8.9902, -8.3667, -7.3925, -7.6147]]) + """ + +@overload +def add(self: Tensor, alpha: Number | _complex, other: Tensor) -> Tensor: + r""" + add(input, other, *, alpha=1, out=None) -> Tensor + + Adds :attr:`other`, scaled by :attr:`alpha`, to :attr:`input`. + + .. math:: + \text{{out}}_i = \text{{input}}_i + \text{{alpha}} \times \text{{other}}_i + + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer, float, and complex inputs. + + Args: + input (Tensor): the input tensor. + other (Tensor or Number): the tensor or number to add to :attr:`input`. + + Keyword arguments: + alpha (Number): the multiplier for :attr:`other`. + out (Tensor, optional): the output tensor. + + Examples:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.0202, 1.0985, 1.3506, -0.6056]) + >>> torch.add(a, 20) + tensor([ 20.0202, 21.0985, 21.3506, 19.3944]) + + >>> b = torch.randn(4) + >>> b + tensor([-0.9732, -0.3497, 0.6245, 0.4022]) + >>> c = torch.randn(4, 1) + >>> c + tensor([[ 0.3743], + [-1.7724], + [-0.5811], + [-0.8017]]) + >>> torch.add(b, c, alpha=10) + tensor([[ 2.7695, 3.3930, 4.3672, 4.1450], + [-18.6971, -18.0736, -17.0994, -17.3216], + [ -6.7845, -6.1610, -5.1868, -5.4090], + [ -8.9902, -8.3667, -7.3925, -7.6147]]) + """ + +@overload +def add( + self: Tensor, + alpha: Number | _complex, + other: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + add(input, other, *, alpha=1, out=None) -> Tensor + + Adds :attr:`other`, scaled by :attr:`alpha`, to :attr:`input`. + + .. math:: + \text{{out}}_i = \text{{input}}_i + \text{{alpha}} \times \text{{other}}_i + + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer, float, and complex inputs. + + Args: + input (Tensor): the input tensor. + other (Tensor or Number): the tensor or number to add to :attr:`input`. + + Keyword arguments: + alpha (Number): the multiplier for :attr:`other`. + out (Tensor, optional): the output tensor. + + Examples:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.0202, 1.0985, 1.3506, -0.6056]) + >>> torch.add(a, 20) + tensor([ 20.0202, 21.0985, 21.3506, 19.3944]) + + >>> b = torch.randn(4) + >>> b + tensor([-0.9732, -0.3497, 0.6245, 0.4022]) + >>> c = torch.randn(4, 1) + >>> c + tensor([[ 0.3743], + [-1.7724], + [-0.5811], + [-0.8017]]) + >>> torch.add(b, c, alpha=10) + tensor([[ 2.7695, 3.3930, 4.3672, 4.1450], + [-18.6971, -18.0736, -17.0994, -17.3216], + [ -6.7845, -6.1610, -5.1868, -5.4090], + [ -8.9902, -8.3667, -7.3925, -7.6147]]) + """ + +@overload +def addbmm( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + batch1: Tensor, + batch2: Tensor, +) -> Tensor: + r""" + addbmm(input, batch1, batch2, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices stored + in :attr:`batch1` and :attr:`batch2`, + with a reduced add step (all matrix multiplications get accumulated + along the first dimension). + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the + same number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + .. math:: + out = \beta\ \text{input} + \alpha\ (\sum_{i=0}^{b-1} \text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and :attr:`alpha` + must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for `batch1 @ batch2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.addbmm(M, batch1, batch2) + tensor([[ 6.6311, 0.0503, 6.9768, -12.0362, -2.1653], + [ -4.8185, -1.4255, -6.6760, 8.9453, 2.5743], + [ -3.8202, 4.3691, 1.0943, -1.1109, 5.4730]]) + """ + +@overload +def addbmm( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + batch1: Tensor, + batch2: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + addbmm(input, batch1, batch2, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices stored + in :attr:`batch1` and :attr:`batch2`, + with a reduced add step (all matrix multiplications get accumulated + along the first dimension). + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the + same number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + .. math:: + out = \beta\ \text{input} + \alpha\ (\sum_{i=0}^{b-1} \text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and :attr:`alpha` + must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for `batch1 @ batch2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.addbmm(M, batch1, batch2) + tensor([[ 6.6311, 0.0503, 6.9768, -12.0362, -2.1653], + [ -4.8185, -1.4255, -6.6760, 8.9453, 2.5743], + [ -3.8202, 4.3691, 1.0943, -1.1109, 5.4730]]) + """ + +@overload +def addbmm( + input: Tensor, + batch1: Tensor, + batch2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + addbmm(input, batch1, batch2, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices stored + in :attr:`batch1` and :attr:`batch2`, + with a reduced add step (all matrix multiplications get accumulated + along the first dimension). + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the + same number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + .. math:: + out = \beta\ \text{input} + \alpha\ (\sum_{i=0}^{b-1} \text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and :attr:`alpha` + must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for `batch1 @ batch2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.addbmm(M, batch1, batch2) + tensor([[ 6.6311, 0.0503, 6.9768, -12.0362, -2.1653], + [ -4.8185, -1.4255, -6.6760, 8.9453, 2.5743], + [ -3.8202, 4.3691, 1.0943, -1.1109, 5.4730]]) + """ + +@overload +def addbmm( + beta: Number | _complex, + self: Tensor, + batch1: Tensor, + batch2: Tensor, +) -> Tensor: + r""" + addbmm(input, batch1, batch2, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices stored + in :attr:`batch1` and :attr:`batch2`, + with a reduced add step (all matrix multiplications get accumulated + along the first dimension). + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the + same number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + .. math:: + out = \beta\ \text{input} + \alpha\ (\sum_{i=0}^{b-1} \text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and :attr:`alpha` + must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for `batch1 @ batch2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.addbmm(M, batch1, batch2) + tensor([[ 6.6311, 0.0503, 6.9768, -12.0362, -2.1653], + [ -4.8185, -1.4255, -6.6760, 8.9453, 2.5743], + [ -3.8202, 4.3691, 1.0943, -1.1109, 5.4730]]) + """ + +@overload +def addbmm( + beta: Number | _complex, + self: Tensor, + batch1: Tensor, + batch2: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + addbmm(input, batch1, batch2, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices stored + in :attr:`batch1` and :attr:`batch2`, + with a reduced add step (all matrix multiplications get accumulated + along the first dimension). + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the + same number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + .. math:: + out = \beta\ \text{input} + \alpha\ (\sum_{i=0}^{b-1} \text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and :attr:`alpha` + must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for `batch1 @ batch2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.addbmm(M, batch1, batch2) + tensor([[ 6.6311, 0.0503, 6.9768, -12.0362, -2.1653], + [ -4.8185, -1.4255, -6.6760, 8.9453, 2.5743], + [ -3.8202, 4.3691, 1.0943, -1.1109, 5.4730]]) + """ + +@overload +def addcdiv( + self: Tensor, + value: Number | _complex, + tensor1: Tensor, + tensor2: Tensor, +) -> Tensor: + r""" + addcdiv(input, tensor1, tensor2, *, value=1, out=None) -> Tensor + + Performs the element-wise division of :attr:`tensor1` by :attr:`tensor2`, + multiplies the result by the scalar :attr:`value` and adds it to :attr:`input`. + + .. warning:: + Integer division with addcdiv is no longer supported, and in a future + release addcdiv will perform a true division of tensor1 and tensor2. + The historic addcdiv behavior can be implemented as + (input + value * torch.trunc(tensor1 / tensor2)).to(input.dtype) + for integer inputs and as (input + value * tensor1 / tensor2) for float inputs. + The future addcdiv behavior is just the latter implementation: + (input + value * tensor1 / tensor2), for all dtypes. + + .. math:: + \text{out}_i = \text{input}_i + \text{value} \times \frac{\text{tensor1}_i}{\text{tensor2}_i} + + + The shapes of :attr:`input`, :attr:`tensor1`, and :attr:`tensor2` must be + :ref:`broadcastable `. + + For inputs of type `FloatTensor` or `DoubleTensor`, :attr:`value` must be + a real number, otherwise an integer. + + Args: + input (Tensor): the tensor to be added + tensor1 (Tensor): the numerator tensor + tensor2 (Tensor): the denominator tensor + + Keyword args: + value (Number, optional): multiplier for :math:`\text{tensor1} / \text{tensor2}` + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.randn(1, 3) + >>> t1 = torch.randn(3, 1) + >>> t2 = torch.randn(1, 3) + >>> torch.addcdiv(t, t1, t2, value=0.1) + tensor([[-0.2312, -3.6496, 0.1312], + [-1.0428, 3.4292, -0.1030], + [-0.5369, -0.9829, 0.0430]]) + """ + +@overload +def addcdiv( + self: Tensor, + value: Number | _complex, + tensor1: Tensor, + tensor2: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + addcdiv(input, tensor1, tensor2, *, value=1, out=None) -> Tensor + + Performs the element-wise division of :attr:`tensor1` by :attr:`tensor2`, + multiplies the result by the scalar :attr:`value` and adds it to :attr:`input`. + + .. warning:: + Integer division with addcdiv is no longer supported, and in a future + release addcdiv will perform a true division of tensor1 and tensor2. + The historic addcdiv behavior can be implemented as + (input + value * torch.trunc(tensor1 / tensor2)).to(input.dtype) + for integer inputs and as (input + value * tensor1 / tensor2) for float inputs. + The future addcdiv behavior is just the latter implementation: + (input + value * tensor1 / tensor2), for all dtypes. + + .. math:: + \text{out}_i = \text{input}_i + \text{value} \times \frac{\text{tensor1}_i}{\text{tensor2}_i} + + + The shapes of :attr:`input`, :attr:`tensor1`, and :attr:`tensor2` must be + :ref:`broadcastable `. + + For inputs of type `FloatTensor` or `DoubleTensor`, :attr:`value` must be + a real number, otherwise an integer. + + Args: + input (Tensor): the tensor to be added + tensor1 (Tensor): the numerator tensor + tensor2 (Tensor): the denominator tensor + + Keyword args: + value (Number, optional): multiplier for :math:`\text{tensor1} / \text{tensor2}` + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.randn(1, 3) + >>> t1 = torch.randn(3, 1) + >>> t2 = torch.randn(1, 3) + >>> torch.addcdiv(t, t1, t2, value=0.1) + tensor([[-0.2312, -3.6496, 0.1312], + [-1.0428, 3.4292, -0.1030], + [-0.5369, -0.9829, 0.0430]]) + """ + +@overload +def addcdiv( + input: Tensor, + tensor1: Tensor, + tensor2: Tensor, + *, + value: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + addcdiv(input, tensor1, tensor2, *, value=1, out=None) -> Tensor + + Performs the element-wise division of :attr:`tensor1` by :attr:`tensor2`, + multiplies the result by the scalar :attr:`value` and adds it to :attr:`input`. + + .. warning:: + Integer division with addcdiv is no longer supported, and in a future + release addcdiv will perform a true division of tensor1 and tensor2. + The historic addcdiv behavior can be implemented as + (input + value * torch.trunc(tensor1 / tensor2)).to(input.dtype) + for integer inputs and as (input + value * tensor1 / tensor2) for float inputs. + The future addcdiv behavior is just the latter implementation: + (input + value * tensor1 / tensor2), for all dtypes. + + .. math:: + \text{out}_i = \text{input}_i + \text{value} \times \frac{\text{tensor1}_i}{\text{tensor2}_i} + + + The shapes of :attr:`input`, :attr:`tensor1`, and :attr:`tensor2` must be + :ref:`broadcastable `. + + For inputs of type `FloatTensor` or `DoubleTensor`, :attr:`value` must be + a real number, otherwise an integer. + + Args: + input (Tensor): the tensor to be added + tensor1 (Tensor): the numerator tensor + tensor2 (Tensor): the denominator tensor + + Keyword args: + value (Number, optional): multiplier for :math:`\text{tensor1} / \text{tensor2}` + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.randn(1, 3) + >>> t1 = torch.randn(3, 1) + >>> t2 = torch.randn(1, 3) + >>> torch.addcdiv(t, t1, t2, value=0.1) + tensor([[-0.2312, -3.6496, 0.1312], + [-1.0428, 3.4292, -0.1030], + [-0.5369, -0.9829, 0.0430]]) + """ + +@overload +def addcmul( + self: Tensor, + value: Number | _complex, + tensor1: Tensor, + tensor2: Tensor, +) -> Tensor: + r""" + addcmul(input, tensor1, tensor2, *, value=1, out=None) -> Tensor + + Performs the element-wise multiplication of :attr:`tensor1` + by :attr:`tensor2`, multiplies the result by the scalar :attr:`value` + and adds it to :attr:`input`. + + .. math:: + \text{out}_i = \text{input}_i + \text{value} \times \text{tensor1}_i \times \text{tensor2}_i + + The shapes of :attr:`tensor`, :attr:`tensor1`, and :attr:`tensor2` must be + :ref:`broadcastable `. + + For inputs of type `FloatTensor` or `DoubleTensor`, :attr:`value` must be + a real number, otherwise an integer. + + Args: + input (Tensor): the tensor to be added + tensor1 (Tensor): the tensor to be multiplied + tensor2 (Tensor): the tensor to be multiplied + + Keyword args: + value (Number, optional): multiplier for :math:`tensor1 .* tensor2` + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.randn(1, 3) + >>> t1 = torch.randn(3, 1) + >>> t2 = torch.randn(1, 3) + >>> torch.addcmul(t, t1, t2, value=0.1) + tensor([[-0.8635, -0.6391, 1.6174], + [-0.7617, -0.5879, 1.7388], + [-0.8353, -0.6249, 1.6511]]) + """ + +@overload +def addcmul( + self: Tensor, + value: Number | _complex, + tensor1: Tensor, + tensor2: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + addcmul(input, tensor1, tensor2, *, value=1, out=None) -> Tensor + + Performs the element-wise multiplication of :attr:`tensor1` + by :attr:`tensor2`, multiplies the result by the scalar :attr:`value` + and adds it to :attr:`input`. + + .. math:: + \text{out}_i = \text{input}_i + \text{value} \times \text{tensor1}_i \times \text{tensor2}_i + + The shapes of :attr:`tensor`, :attr:`tensor1`, and :attr:`tensor2` must be + :ref:`broadcastable `. + + For inputs of type `FloatTensor` or `DoubleTensor`, :attr:`value` must be + a real number, otherwise an integer. + + Args: + input (Tensor): the tensor to be added + tensor1 (Tensor): the tensor to be multiplied + tensor2 (Tensor): the tensor to be multiplied + + Keyword args: + value (Number, optional): multiplier for :math:`tensor1 .* tensor2` + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.randn(1, 3) + >>> t1 = torch.randn(3, 1) + >>> t2 = torch.randn(1, 3) + >>> torch.addcmul(t, t1, t2, value=0.1) + tensor([[-0.8635, -0.6391, 1.6174], + [-0.7617, -0.5879, 1.7388], + [-0.8353, -0.6249, 1.6511]]) + """ + +@overload +def addcmul( + input: Tensor, + tensor1: Tensor, + tensor2: Tensor, + *, + value: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + addcmul(input, tensor1, tensor2, *, value=1, out=None) -> Tensor + + Performs the element-wise multiplication of :attr:`tensor1` + by :attr:`tensor2`, multiplies the result by the scalar :attr:`value` + and adds it to :attr:`input`. + + .. math:: + \text{out}_i = \text{input}_i + \text{value} \times \text{tensor1}_i \times \text{tensor2}_i + + The shapes of :attr:`tensor`, :attr:`tensor1`, and :attr:`tensor2` must be + :ref:`broadcastable `. + + For inputs of type `FloatTensor` or `DoubleTensor`, :attr:`value` must be + a real number, otherwise an integer. + + Args: + input (Tensor): the tensor to be added + tensor1 (Tensor): the tensor to be multiplied + tensor2 (Tensor): the tensor to be multiplied + + Keyword args: + value (Number, optional): multiplier for :math:`tensor1 .* tensor2` + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.randn(1, 3) + >>> t1 = torch.randn(3, 1) + >>> t2 = torch.randn(1, 3) + >>> torch.addcmul(t, t1, t2, value=0.1) + tensor([[-0.8635, -0.6391, 1.6174], + [-0.7617, -0.5879, 1.7388], + [-0.8353, -0.6249, 1.6511]]) + """ + +@overload +def addmm( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + mat1: Tensor, + mat2: Tensor, +) -> Tensor: + r""" + addmm(input, mat1, mat2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix multiplication of the matrices :attr:`mat1` and :attr:`mat2`. + The matrix :attr:`input` is added to the final result. + + If :attr:`mat1` is a :math:`(n \times m)` tensor, :attr:`mat2` is a + :math:`(m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat1` and :attr:`mat2` and the added matrix :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat1}_i \mathbin{@} \text{mat2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operation has support for arguments with :ref:`sparse layouts`. If + :attr:`input` is sparse the result will have the same layout and if :attr:`out` + is provided it must have the same layout as :attr:`input`. + + + .. warning:: + Sparse support is a beta feature and some layout(s)/dtype/device combinations may not be supported, + or may not have autograd support. If you notice missing functionality please + open a feature request. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + mat1 (Tensor): the first matrix to be matrix multiplied + mat2 (Tensor): the second matrix to be matrix multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat1 @ mat2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2, 3) + >>> mat1 = torch.randn(2, 3) + >>> mat2 = torch.randn(3, 3) + >>> torch.addmm(M, mat1, mat2) + tensor([[-4.8716, 1.4671, -1.3746], + [ 0.7573, -3.9555, -2.8681]]) + """ + +@overload +def addmm( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + mat1: Tensor, + mat2: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + addmm(input, mat1, mat2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix multiplication of the matrices :attr:`mat1` and :attr:`mat2`. + The matrix :attr:`input` is added to the final result. + + If :attr:`mat1` is a :math:`(n \times m)` tensor, :attr:`mat2` is a + :math:`(m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat1` and :attr:`mat2` and the added matrix :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat1}_i \mathbin{@} \text{mat2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operation has support for arguments with :ref:`sparse layouts`. If + :attr:`input` is sparse the result will have the same layout and if :attr:`out` + is provided it must have the same layout as :attr:`input`. + + + .. warning:: + Sparse support is a beta feature and some layout(s)/dtype/device combinations may not be supported, + or may not have autograd support. If you notice missing functionality please + open a feature request. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + mat1 (Tensor): the first matrix to be matrix multiplied + mat2 (Tensor): the second matrix to be matrix multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat1 @ mat2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2, 3) + >>> mat1 = torch.randn(2, 3) + >>> mat2 = torch.randn(3, 3) + >>> torch.addmm(M, mat1, mat2) + tensor([[-4.8716, 1.4671, -1.3746], + [ 0.7573, -3.9555, -2.8681]]) + """ + +@overload +def addmm( + input: Tensor, + mat1: Tensor, + mat2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + addmm(input, mat1, mat2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix multiplication of the matrices :attr:`mat1` and :attr:`mat2`. + The matrix :attr:`input` is added to the final result. + + If :attr:`mat1` is a :math:`(n \times m)` tensor, :attr:`mat2` is a + :math:`(m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat1` and :attr:`mat2` and the added matrix :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat1}_i \mathbin{@} \text{mat2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operation has support for arguments with :ref:`sparse layouts`. If + :attr:`input` is sparse the result will have the same layout and if :attr:`out` + is provided it must have the same layout as :attr:`input`. + + + .. warning:: + Sparse support is a beta feature and some layout(s)/dtype/device combinations may not be supported, + or may not have autograd support. If you notice missing functionality please + open a feature request. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + mat1 (Tensor): the first matrix to be matrix multiplied + mat2 (Tensor): the second matrix to be matrix multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat1 @ mat2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2, 3) + >>> mat1 = torch.randn(2, 3) + >>> mat2 = torch.randn(3, 3) + >>> torch.addmm(M, mat1, mat2) + tensor([[-4.8716, 1.4671, -1.3746], + [ 0.7573, -3.9555, -2.8681]]) + """ + +@overload +def addmm( + input: Tensor, + mat1: Tensor, + mat2: Tensor, + out_dtype: _dtype, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + addmm(input, mat1, mat2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix multiplication of the matrices :attr:`mat1` and :attr:`mat2`. + The matrix :attr:`input` is added to the final result. + + If :attr:`mat1` is a :math:`(n \times m)` tensor, :attr:`mat2` is a + :math:`(m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat1` and :attr:`mat2` and the added matrix :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat1}_i \mathbin{@} \text{mat2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operation has support for arguments with :ref:`sparse layouts`. If + :attr:`input` is sparse the result will have the same layout and if :attr:`out` + is provided it must have the same layout as :attr:`input`. + + + .. warning:: + Sparse support is a beta feature and some layout(s)/dtype/device combinations may not be supported, + or may not have autograd support. If you notice missing functionality please + open a feature request. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + mat1 (Tensor): the first matrix to be matrix multiplied + mat2 (Tensor): the second matrix to be matrix multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat1 @ mat2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2, 3) + >>> mat1 = torch.randn(2, 3) + >>> mat2 = torch.randn(3, 3) + >>> torch.addmm(M, mat1, mat2) + tensor([[-4.8716, 1.4671, -1.3746], + [ 0.7573, -3.9555, -2.8681]]) + """ + +@overload +def addmm( + beta: Number | _complex, + self: Tensor, + mat1: Tensor, + mat2: Tensor, +) -> Tensor: + r""" + addmm(input, mat1, mat2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix multiplication of the matrices :attr:`mat1` and :attr:`mat2`. + The matrix :attr:`input` is added to the final result. + + If :attr:`mat1` is a :math:`(n \times m)` tensor, :attr:`mat2` is a + :math:`(m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat1` and :attr:`mat2` and the added matrix :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat1}_i \mathbin{@} \text{mat2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operation has support for arguments with :ref:`sparse layouts`. If + :attr:`input` is sparse the result will have the same layout and if :attr:`out` + is provided it must have the same layout as :attr:`input`. + + + .. warning:: + Sparse support is a beta feature and some layout(s)/dtype/device combinations may not be supported, + or may not have autograd support. If you notice missing functionality please + open a feature request. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + mat1 (Tensor): the first matrix to be matrix multiplied + mat2 (Tensor): the second matrix to be matrix multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat1 @ mat2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2, 3) + >>> mat1 = torch.randn(2, 3) + >>> mat2 = torch.randn(3, 3) + >>> torch.addmm(M, mat1, mat2) + tensor([[-4.8716, 1.4671, -1.3746], + [ 0.7573, -3.9555, -2.8681]]) + """ + +@overload +def addmm( + beta: Number | _complex, + self: Tensor, + mat1: Tensor, + mat2: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + addmm(input, mat1, mat2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix multiplication of the matrices :attr:`mat1` and :attr:`mat2`. + The matrix :attr:`input` is added to the final result. + + If :attr:`mat1` is a :math:`(n \times m)` tensor, :attr:`mat2` is a + :math:`(m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a :math:`(n \times p)` tensor + and :attr:`out` will be a :math:`(n \times p)` tensor. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat1` and :attr:`mat2` and the added matrix :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat1}_i \mathbin{@} \text{mat2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operation has support for arguments with :ref:`sparse layouts`. If + :attr:`input` is sparse the result will have the same layout and if :attr:`out` + is provided it must have the same layout as :attr:`input`. + + + .. warning:: + Sparse support is a beta feature and some layout(s)/dtype/device combinations may not be supported, + or may not have autograd support. If you notice missing functionality please + open a feature request. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): matrix to be added + mat1 (Tensor): the first matrix to be matrix multiplied + mat2 (Tensor): the second matrix to be matrix multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat1 @ mat2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2, 3) + >>> mat1 = torch.randn(2, 3) + >>> mat2 = torch.randn(3, 3) + >>> torch.addmm(M, mat1, mat2) + tensor([[-4.8716, 1.4671, -1.3746], + [ 0.7573, -3.9555, -2.8681]]) + """ + +@overload +def addmv( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + mat: Tensor, + vec: Tensor, +) -> Tensor: + r""" + addmv(input, mat, vec, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix-vector product of the matrix :attr:`mat` and + the vector :attr:`vec`. + The vector :attr:`input` is added to the final result. + + If :attr:`mat` is a :math:`(n \times m)` tensor, :attr:`vec` is a 1-D tensor of + size `m`, then :attr:`input` must be + :ref:`broadcastable ` with a 1-D tensor of size `n` and + :attr:`out` will be 1-D tensor of size `n`. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat` and :attr:`vec` and the added tensor :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat} \mathbin{@} \text{vec}) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + Args: + input (Tensor): vector to be added + mat (Tensor): matrix to be matrix multiplied + vec (Tensor): vector to be matrix multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat @ vec` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2) + >>> mat = torch.randn(2, 3) + >>> vec = torch.randn(3) + >>> torch.addmv(M, mat, vec) + tensor([-0.3768, -5.5565]) + """ + +@overload +def addmv( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + mat: Tensor, + vec: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + addmv(input, mat, vec, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix-vector product of the matrix :attr:`mat` and + the vector :attr:`vec`. + The vector :attr:`input` is added to the final result. + + If :attr:`mat` is a :math:`(n \times m)` tensor, :attr:`vec` is a 1-D tensor of + size `m`, then :attr:`input` must be + :ref:`broadcastable ` with a 1-D tensor of size `n` and + :attr:`out` will be 1-D tensor of size `n`. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat` and :attr:`vec` and the added tensor :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat} \mathbin{@} \text{vec}) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + Args: + input (Tensor): vector to be added + mat (Tensor): matrix to be matrix multiplied + vec (Tensor): vector to be matrix multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat @ vec` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2) + >>> mat = torch.randn(2, 3) + >>> vec = torch.randn(3) + >>> torch.addmv(M, mat, vec) + tensor([-0.3768, -5.5565]) + """ + +@overload +def addmv( + input: Tensor, + mat: Tensor, + vec: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + addmv(input, mat, vec, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix-vector product of the matrix :attr:`mat` and + the vector :attr:`vec`. + The vector :attr:`input` is added to the final result. + + If :attr:`mat` is a :math:`(n \times m)` tensor, :attr:`vec` is a 1-D tensor of + size `m`, then :attr:`input` must be + :ref:`broadcastable ` with a 1-D tensor of size `n` and + :attr:`out` will be 1-D tensor of size `n`. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat` and :attr:`vec` and the added tensor :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat} \mathbin{@} \text{vec}) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + Args: + input (Tensor): vector to be added + mat (Tensor): matrix to be matrix multiplied + vec (Tensor): vector to be matrix multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat @ vec` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2) + >>> mat = torch.randn(2, 3) + >>> vec = torch.randn(3) + >>> torch.addmv(M, mat, vec) + tensor([-0.3768, -5.5565]) + """ + +@overload +def addmv( + beta: Number | _complex, + self: Tensor, + mat: Tensor, + vec: Tensor, +) -> Tensor: + r""" + addmv(input, mat, vec, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix-vector product of the matrix :attr:`mat` and + the vector :attr:`vec`. + The vector :attr:`input` is added to the final result. + + If :attr:`mat` is a :math:`(n \times m)` tensor, :attr:`vec` is a 1-D tensor of + size `m`, then :attr:`input` must be + :ref:`broadcastable ` with a 1-D tensor of size `n` and + :attr:`out` will be 1-D tensor of size `n`. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat` and :attr:`vec` and the added tensor :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat} \mathbin{@} \text{vec}) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + Args: + input (Tensor): vector to be added + mat (Tensor): matrix to be matrix multiplied + vec (Tensor): vector to be matrix multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat @ vec` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2) + >>> mat = torch.randn(2, 3) + >>> vec = torch.randn(3) + >>> torch.addmv(M, mat, vec) + tensor([-0.3768, -5.5565]) + """ + +@overload +def addmv( + beta: Number | _complex, + self: Tensor, + mat: Tensor, + vec: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + addmv(input, mat, vec, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a matrix-vector product of the matrix :attr:`mat` and + the vector :attr:`vec`. + The vector :attr:`input` is added to the final result. + + If :attr:`mat` is a :math:`(n \times m)` tensor, :attr:`vec` is a 1-D tensor of + size `m`, then :attr:`input` must be + :ref:`broadcastable ` with a 1-D tensor of size `n` and + :attr:`out` will be 1-D tensor of size `n`. + + :attr:`alpha` and :attr:`beta` are scaling factors on matrix-vector product between + :attr:`mat` and :attr:`vec` and the added tensor :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{mat} \mathbin{@} \text{vec}) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + Args: + input (Tensor): vector to be added + mat (Tensor): matrix to be matrix multiplied + vec (Tensor): vector to be matrix multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat @ vec` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(2) + >>> mat = torch.randn(2, 3) + >>> vec = torch.randn(3) + >>> torch.addmv(M, mat, vec) + tensor([-0.3768, -5.5565]) + """ + +@overload +def addmv_( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + mat: Tensor, + vec: Tensor, +) -> Tensor: ... +@overload +def addmv_( + input: Tensor, + mat: Tensor, + vec: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, +) -> Tensor: ... +@overload +def addmv_( + beta: Number | _complex, + self: Tensor, + mat: Tensor, + vec: Tensor, +) -> Tensor: ... +@overload +def addr( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + vec1: Tensor, + vec2: Tensor, +) -> Tensor: + r""" + addr(input, vec1, vec2, *, beta=1, alpha=1, out=None) -> Tensor + + Performs the outer-product of vectors :attr:`vec1` and :attr:`vec2` + and adds it to the matrix :attr:`input`. + + Optional values :attr:`beta` and :attr:`alpha` are scaling factors on the + outer product between :attr:`vec1` and :attr:`vec2` and the added matrix + :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{vec1} \otimes \text{vec2}) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + If :attr:`vec1` is a vector of size `n` and :attr:`vec2` is a vector + of size `m`, then :attr:`input` must be + :ref:`broadcastable ` with a matrix of size + :math:`(n \times m)` and :attr:`out` will be a matrix of size + :math:`(n \times m)`. + + Args: + input (Tensor): matrix to be added + vec1 (Tensor): the first vector of the outer product + vec2 (Tensor): the second vector of the outer product + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{vec1} \otimes \text{vec2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> vec1 = torch.arange(1., 4.) + >>> vec2 = torch.arange(1., 3.) + >>> M = torch.zeros(3, 2) + >>> torch.addr(M, vec1, vec2) + tensor([[ 1., 2.], + [ 2., 4.], + [ 3., 6.]]) + """ + +@overload +def addr( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + vec1: Tensor, + vec2: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + addr(input, vec1, vec2, *, beta=1, alpha=1, out=None) -> Tensor + + Performs the outer-product of vectors :attr:`vec1` and :attr:`vec2` + and adds it to the matrix :attr:`input`. + + Optional values :attr:`beta` and :attr:`alpha` are scaling factors on the + outer product between :attr:`vec1` and :attr:`vec2` and the added matrix + :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{vec1} \otimes \text{vec2}) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + If :attr:`vec1` is a vector of size `n` and :attr:`vec2` is a vector + of size `m`, then :attr:`input` must be + :ref:`broadcastable ` with a matrix of size + :math:`(n \times m)` and :attr:`out` will be a matrix of size + :math:`(n \times m)`. + + Args: + input (Tensor): matrix to be added + vec1 (Tensor): the first vector of the outer product + vec2 (Tensor): the second vector of the outer product + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{vec1} \otimes \text{vec2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> vec1 = torch.arange(1., 4.) + >>> vec2 = torch.arange(1., 3.) + >>> M = torch.zeros(3, 2) + >>> torch.addr(M, vec1, vec2) + tensor([[ 1., 2.], + [ 2., 4.], + [ 3., 6.]]) + """ + +@overload +def addr( + input: Tensor, + vec1: Tensor, + vec2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + addr(input, vec1, vec2, *, beta=1, alpha=1, out=None) -> Tensor + + Performs the outer-product of vectors :attr:`vec1` and :attr:`vec2` + and adds it to the matrix :attr:`input`. + + Optional values :attr:`beta` and :attr:`alpha` are scaling factors on the + outer product between :attr:`vec1` and :attr:`vec2` and the added matrix + :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{vec1} \otimes \text{vec2}) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + If :attr:`vec1` is a vector of size `n` and :attr:`vec2` is a vector + of size `m`, then :attr:`input` must be + :ref:`broadcastable ` with a matrix of size + :math:`(n \times m)` and :attr:`out` will be a matrix of size + :math:`(n \times m)`. + + Args: + input (Tensor): matrix to be added + vec1 (Tensor): the first vector of the outer product + vec2 (Tensor): the second vector of the outer product + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{vec1} \otimes \text{vec2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> vec1 = torch.arange(1., 4.) + >>> vec2 = torch.arange(1., 3.) + >>> M = torch.zeros(3, 2) + >>> torch.addr(M, vec1, vec2) + tensor([[ 1., 2.], + [ 2., 4.], + [ 3., 6.]]) + """ + +@overload +def addr( + beta: Number | _complex, + self: Tensor, + vec1: Tensor, + vec2: Tensor, +) -> Tensor: + r""" + addr(input, vec1, vec2, *, beta=1, alpha=1, out=None) -> Tensor + + Performs the outer-product of vectors :attr:`vec1` and :attr:`vec2` + and adds it to the matrix :attr:`input`. + + Optional values :attr:`beta` and :attr:`alpha` are scaling factors on the + outer product between :attr:`vec1` and :attr:`vec2` and the added matrix + :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{vec1} \otimes \text{vec2}) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + If :attr:`vec1` is a vector of size `n` and :attr:`vec2` is a vector + of size `m`, then :attr:`input` must be + :ref:`broadcastable ` with a matrix of size + :math:`(n \times m)` and :attr:`out` will be a matrix of size + :math:`(n \times m)`. + + Args: + input (Tensor): matrix to be added + vec1 (Tensor): the first vector of the outer product + vec2 (Tensor): the second vector of the outer product + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{vec1} \otimes \text{vec2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> vec1 = torch.arange(1., 4.) + >>> vec2 = torch.arange(1., 3.) + >>> M = torch.zeros(3, 2) + >>> torch.addr(M, vec1, vec2) + tensor([[ 1., 2.], + [ 2., 4.], + [ 3., 6.]]) + """ + +@overload +def addr( + beta: Number | _complex, + self: Tensor, + vec1: Tensor, + vec2: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + addr(input, vec1, vec2, *, beta=1, alpha=1, out=None) -> Tensor + + Performs the outer-product of vectors :attr:`vec1` and :attr:`vec2` + and adds it to the matrix :attr:`input`. + + Optional values :attr:`beta` and :attr:`alpha` are scaling factors on the + outer product between :attr:`vec1` and :attr:`vec2` and the added matrix + :attr:`input` respectively. + + .. math:: + \text{out} = \beta\ \text{input} + \alpha\ (\text{vec1} \otimes \text{vec2}) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + If :attr:`vec1` is a vector of size `n` and :attr:`vec2` is a vector + of size `m`, then :attr:`input` must be + :ref:`broadcastable ` with a matrix of size + :math:`(n \times m)` and :attr:`out` will be a matrix of size + :math:`(n \times m)`. + + Args: + input (Tensor): matrix to be added + vec1 (Tensor): the first vector of the outer product + vec2 (Tensor): the second vector of the outer product + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{vec1} \otimes \text{vec2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> vec1 = torch.arange(1., 4.) + >>> vec2 = torch.arange(1., 3.) + >>> M = torch.zeros(3, 2) + >>> torch.addr(M, vec1, vec2) + tensor([[ 1., 2.], + [ 2., 4.], + [ 3., 6.]]) + """ + +def adjoint(input: Tensor) -> Tensor: + r""" + adjoint(input: Tensor) -> Tensor + Returns a view of the tensor conjugated and with the last two dimensions transposed. + + ``x.adjoint()`` is equivalent to ``x.transpose(-2, -1).conj()`` for complex tensors and + to ``x.transpose(-2, -1)`` for real tensors. + + Args: + {input} + + Example:: + + >>> x = torch.arange(4, dtype=torch.float) + >>> A = torch.complex(x, x).reshape(2, 2) + >>> A + tensor([[0.+0.j, 1.+1.j], + [2.+2.j, 3.+3.j]]) + >>> A.adjoint() + tensor([[0.-0.j, 2.-2.j], + [1.-1.j, 3.-3.j]]) + >>> (A.adjoint() == A.mH).all() + tensor(True) + """ + +def affine_grid_generator( + theta: Tensor, + size: Sequence[_int | SymInt], + align_corners: _bool, +) -> Tensor: ... +def alias_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + Performs the same operation as :func:`torch.alias`, but all output tensors + are freshly created instead of aliasing the input. + """ + +@overload +def all(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + all(input: Tensor, *, out=None) -> Tensor + + Tests if all elements in :attr:`input` evaluate to `True`. + + .. note:: This function matches the behaviour of NumPy in returning + output of dtype `bool` for all supported dtypes except `uint8`. + For `uint8` the dtype of output is `uint8` itself. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(1, 2).bool() + >>> a + tensor([[False, True]], dtype=torch.bool) + >>> torch.all(a) + tensor(False, dtype=torch.bool) + >>> a = torch.arange(0, 3) + >>> a + tensor([0, 1, 2]) + >>> torch.all(a) + tensor(False) + + .. function:: all(input, dim, keepdim=False, *, out=None) -> Tensor + :noindex: + + For each row of :attr:`input` in the given dimension :attr:`dim`, + returns `True` if all elements in the row evaluate to `True` and `False` otherwise. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(4, 2).bool() + >>> a + tensor([[True, True], + [True, False], + [True, True], + [True, True]], dtype=torch.bool) + >>> torch.all(a, dim=1) + tensor([ True, False, True, True], dtype=torch.bool) + >>> torch.all(a, dim=0) + tensor([ True, False], dtype=torch.bool) + """ + +@overload +def all( + input: Tensor, + dim: _size | None = None, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + all(input: Tensor, *, out=None) -> Tensor + + Tests if all elements in :attr:`input` evaluate to `True`. + + .. note:: This function matches the behaviour of NumPy in returning + output of dtype `bool` for all supported dtypes except `uint8`. + For `uint8` the dtype of output is `uint8` itself. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(1, 2).bool() + >>> a + tensor([[False, True]], dtype=torch.bool) + >>> torch.all(a) + tensor(False, dtype=torch.bool) + >>> a = torch.arange(0, 3) + >>> a + tensor([0, 1, 2]) + >>> torch.all(a) + tensor(False) + + .. function:: all(input, dim, keepdim=False, *, out=None) -> Tensor + :noindex: + + For each row of :attr:`input` in the given dimension :attr:`dim`, + returns `True` if all elements in the row evaluate to `True` and `False` otherwise. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(4, 2).bool() + >>> a + tensor([[True, True], + [True, False], + [True, True], + [True, True]], dtype=torch.bool) + >>> torch.all(a, dim=1) + tensor([ True, False, True, True], dtype=torch.bool) + >>> torch.all(a, dim=0) + tensor([ True, False], dtype=torch.bool) + """ + +@overload +def all( + input: Tensor, + dim: _int, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + all(input: Tensor, *, out=None) -> Tensor + + Tests if all elements in :attr:`input` evaluate to `True`. + + .. note:: This function matches the behaviour of NumPy in returning + output of dtype `bool` for all supported dtypes except `uint8`. + For `uint8` the dtype of output is `uint8` itself. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(1, 2).bool() + >>> a + tensor([[False, True]], dtype=torch.bool) + >>> torch.all(a) + tensor(False, dtype=torch.bool) + >>> a = torch.arange(0, 3) + >>> a + tensor([0, 1, 2]) + >>> torch.all(a) + tensor(False) + + .. function:: all(input, dim, keepdim=False, *, out=None) -> Tensor + :noindex: + + For each row of :attr:`input` in the given dimension :attr:`dim`, + returns `True` if all elements in the row evaluate to `True` and `False` otherwise. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(4, 2).bool() + >>> a + tensor([[True, True], + [True, False], + [True, True], + [True, True]], dtype=torch.bool) + >>> torch.all(a, dim=1) + tensor([ True, False, True, True], dtype=torch.bool) + >>> torch.all(a, dim=0) + tensor([ True, False], dtype=torch.bool) + """ + +@overload +def all( + input: Tensor, + dim: str | EllipsisType | None, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + all(input: Tensor, *, out=None) -> Tensor + + Tests if all elements in :attr:`input` evaluate to `True`. + + .. note:: This function matches the behaviour of NumPy in returning + output of dtype `bool` for all supported dtypes except `uint8`. + For `uint8` the dtype of output is `uint8` itself. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(1, 2).bool() + >>> a + tensor([[False, True]], dtype=torch.bool) + >>> torch.all(a) + tensor(False, dtype=torch.bool) + >>> a = torch.arange(0, 3) + >>> a + tensor([0, 1, 2]) + >>> torch.all(a) + tensor(False) + + .. function:: all(input, dim, keepdim=False, *, out=None) -> Tensor + :noindex: + + For each row of :attr:`input` in the given dimension :attr:`dim`, + returns `True` if all elements in the row evaluate to `True` and `False` otherwise. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(4, 2).bool() + >>> a + tensor([[True, True], + [True, False], + [True, True], + [True, True]], dtype=torch.bool) + >>> torch.all(a, dim=1) + tensor([ True, False, True, True], dtype=torch.bool) + >>> torch.all(a, dim=0) + tensor([ True, False], dtype=torch.bool) + """ + +def allclose( + input: Tensor, + other: Tensor, + rtol: _float = 1e-05, + atol: _float = 1e-08, + equal_nan: _bool = False, +) -> _bool: + r""" + allclose(input: Tensor, other: Tensor, rtol: float = 1e-05, atol: float = 1e-08, equal_nan: bool = False) -> bool + + This function checks if :attr:`input` and :attr:`other` satisfy the condition: + + .. math:: + \lvert \text{input}_i - \text{other}_i \rvert \leq \texttt{atol} + \texttt{rtol} \times \lvert \text{other}_i \rvert + + elementwise, for all elements of :attr:`input` and :attr:`other`. The behaviour of this function is analogous to + `numpy.allclose `_ + + Args: + input (Tensor): first tensor to compare + other (Tensor): second tensor to compare + atol (float, optional): absolute tolerance. Default: 1e-08 + rtol (float, optional): relative tolerance. Default: 1e-05 + equal_nan (bool, optional): if ``True``, then two ``NaN`` s will be considered equal. Default: ``False`` + + Example:: + + >>> torch.allclose(torch.tensor([10000., 1e-07]), torch.tensor([10000.1, 1e-08])) + False + >>> torch.allclose(torch.tensor([10000., 1e-08]), torch.tensor([10000.1, 1e-09])) + True + >>> torch.allclose(torch.tensor([1.0, float('nan')]), torch.tensor([1.0, float('nan')])) + False + >>> torch.allclose(torch.tensor([1.0, float('nan')]), torch.tensor([1.0, float('nan')]), equal_nan=True) + True + """ + +def alpha_dropout(input: Tensor, p: _float, train: _bool) -> Tensor: ... +def alpha_dropout_(input: Tensor, p: _float, train: _bool) -> Tensor: ... +def amax( + input: Tensor, + dim: _int | _size = (), + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + amax(input, dim=None, keepdim=False, *, out=None) -> Tensor + + Returns the maximum value of each slice of the :attr:`input` tensor in the given + dimension(s) :attr:`dim`. + + .. note:: + The difference between ``max``/``min`` and ``amax``/``amin`` is: + - ``amax``/``amin`` supports reducing on multiple dimensions, + - ``amax``/``amin`` does not return indices. + + Both ``amax``/``amin`` evenly distribute gradients between equal values + when there are multiple input elements with the same minimum or maximum value. + + For ``max``/``min``: + - If reduce over all dimensions(no dim specified), gradients evenly distribute between equally ``max``/``min`` values. + - If reduce over one specified axis, only propagate to the indexed element. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.8177, 1.4878, -0.2491, 0.9130], + [-0.7158, 1.1775, 2.0992, 0.4817], + [-0.0053, 0.0164, -1.3738, -0.0507], + [ 1.9700, 1.1106, -1.0318, -1.0816]]) + >>> torch.amax(a, 1) + tensor([1.4878, 2.0992, 0.0164, 1.9700]) + """ + +def amin( + input: Tensor, + dim: _int | _size = (), + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + amin(input, dim=None, keepdim=False, *, out=None) -> Tensor + + Returns the minimum value of each slice of the :attr:`input` tensor in the given + dimension(s) :attr:`dim`. + + .. note:: + The difference between ``max``/``min`` and ``amax``/``amin`` is: + - ``amax``/``amin`` supports reducing on multiple dimensions, + - ``amax``/``amin`` does not return indices. + + Both ``amax``/``amin`` evenly distribute gradients between equal values + when there are multiple input elements with the same minimum or maximum value. + + For ``max``/``min``: + - If reduce over all dimensions(no dim specified), gradients evenly distribute between equally ``max``/``min`` values. + - If reduce over one specified axis, only propagate to the indexed element. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.6451, -0.4866, 0.2987, -1.3312], + [-0.5744, 1.2980, 1.8397, -0.2713], + [ 0.9128, 0.9214, -1.7268, -0.2995], + [ 0.9023, 0.4853, 0.9075, -1.6165]]) + >>> torch.amin(a, 1) + tensor([-1.3312, -0.5744, -1.7268, -1.6165]) + """ + +def aminmax( + input: Tensor, + *, + dim: _int | None = None, + keepdim: _bool = False, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.aminmax: + r""" + aminmax(input, *, dim=None, keepdim=False, out=None) -> (Tensor min, Tensor max) + + Computes the minimum and maximum values of the :attr:`input` tensor. + + Args: + input (Tensor): + The input tensor + + Keyword Args: + dim (Optional[int]): + The dimension along which to compute the values. If `None`, + computes the values over the entire :attr:`input` tensor. + Default is `None`. + keepdim (bool): + If `True`, the reduced dimensions will be kept in the output + tensor as dimensions with size 1 for broadcasting, otherwise + they will be removed, as if calling (:func:`torch.squeeze`). + Default is `False`. + out (Optional[Tuple[Tensor, Tensor]]): + Optional tensors on which to write the result. Must have the same + shape and dtype as the expected output. + Default is `None`. + + Returns: + A named tuple `(min, max)` containing the minimum and maximum values. + + Raises: + RuntimeError + If any of the dimensions to compute the values over has size 0. + + .. note:: + NaN values are propagated to the output if at least one value is NaN. + + .. seealso:: + :func:`torch.amin` computes just the minimum value + :func:`torch.amax` computes just the maximum value + + Example:: + + >>> torch.aminmax(torch.tensor([1, -3, 5])) + torch.return_types.aminmax( + min=tensor(-3), + max=tensor(5)) + + >>> # aminmax propagates NaNs + >>> torch.aminmax(torch.tensor([1, -3, 5, torch.nan])) + torch.return_types.aminmax( + min=tensor(nan), + max=tensor(nan)) + + >>> t = torch.arange(10).view(2, 5) + >>> t + tensor([[0, 1, 2, 3, 4], + [5, 6, 7, 8, 9]]) + >>> t.aminmax(dim=0, keepdim=True) + torch.return_types.aminmax( + min=tensor([[0, 1, 2, 3, 4]]), + max=tensor([[5, 6, 7, 8, 9]])) + """ + +def angle(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + angle(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Computes the element-wise angle (in radians) of the given :attr:`input` tensor. + + .. math:: + \text{out}_{i} = angle(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + .. note:: Starting in PyTorch 1.8, angle returns pi for negative real numbers, + zero for non-negative real numbers, and propagates NaNs. Previously + the function would return zero for all real numbers and not propagate + floating-point NaNs. + + Example:: + + >>> torch.angle(torch.tensor([-1 + 1j, -2 + 2j, 3 - 3j]))*180/3.14159 + tensor([ 135., 135, -45]) + """ + +@overload +def any(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + any(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Tests if any element in :attr:`input` evaluates to `True`. + + .. note:: This function matches the behaviour of NumPy in returning + output of dtype `bool` for all supported dtypes except `uint8`. + For `uint8` the dtype of output is `uint8` itself. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(1, 2).bool() + >>> a + tensor([[False, True]], dtype=torch.bool) + >>> torch.any(a) + tensor(True, dtype=torch.bool) + >>> a = torch.arange(0, 3) + >>> a + tensor([0, 1, 2]) + >>> torch.any(a) + tensor(True) + + .. function:: any(input, dim, keepdim=False, *, out=None) -> Tensor + :noindex: + + For each row of :attr:`input` in the given dimension :attr:`dim`, + returns `True` if any element in the row evaluate to `True` and `False` otherwise. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4, 2) < 0 + >>> a + tensor([[ True, True], + [False, True], + [ True, True], + [False, False]]) + >>> torch.any(a, 1) + tensor([ True, True, True, False]) + >>> torch.any(a, 0) + tensor([True, True]) + """ + +@overload +def any( + input: Tensor, + dim: _size | None = None, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + any(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Tests if any element in :attr:`input` evaluates to `True`. + + .. note:: This function matches the behaviour of NumPy in returning + output of dtype `bool` for all supported dtypes except `uint8`. + For `uint8` the dtype of output is `uint8` itself. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(1, 2).bool() + >>> a + tensor([[False, True]], dtype=torch.bool) + >>> torch.any(a) + tensor(True, dtype=torch.bool) + >>> a = torch.arange(0, 3) + >>> a + tensor([0, 1, 2]) + >>> torch.any(a) + tensor(True) + + .. function:: any(input, dim, keepdim=False, *, out=None) -> Tensor + :noindex: + + For each row of :attr:`input` in the given dimension :attr:`dim`, + returns `True` if any element in the row evaluate to `True` and `False` otherwise. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4, 2) < 0 + >>> a + tensor([[ True, True], + [False, True], + [ True, True], + [False, False]]) + >>> torch.any(a, 1) + tensor([ True, True, True, False]) + >>> torch.any(a, 0) + tensor([True, True]) + """ + +@overload +def any( + input: Tensor, + dim: _int, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + any(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Tests if any element in :attr:`input` evaluates to `True`. + + .. note:: This function matches the behaviour of NumPy in returning + output of dtype `bool` for all supported dtypes except `uint8`. + For `uint8` the dtype of output is `uint8` itself. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(1, 2).bool() + >>> a + tensor([[False, True]], dtype=torch.bool) + >>> torch.any(a) + tensor(True, dtype=torch.bool) + >>> a = torch.arange(0, 3) + >>> a + tensor([0, 1, 2]) + >>> torch.any(a) + tensor(True) + + .. function:: any(input, dim, keepdim=False, *, out=None) -> Tensor + :noindex: + + For each row of :attr:`input` in the given dimension :attr:`dim`, + returns `True` if any element in the row evaluate to `True` and `False` otherwise. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4, 2) < 0 + >>> a + tensor([[ True, True], + [False, True], + [ True, True], + [False, False]]) + >>> torch.any(a, 1) + tensor([ True, True, True, False]) + >>> torch.any(a, 0) + tensor([True, True]) + """ + +@overload +def any( + input: Tensor, + dim: str | EllipsisType | None, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + any(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Tests if any element in :attr:`input` evaluates to `True`. + + .. note:: This function matches the behaviour of NumPy in returning + output of dtype `bool` for all supported dtypes except `uint8`. + For `uint8` the dtype of output is `uint8` itself. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(1, 2).bool() + >>> a + tensor([[False, True]], dtype=torch.bool) + >>> torch.any(a) + tensor(True, dtype=torch.bool) + >>> a = torch.arange(0, 3) + >>> a + tensor([0, 1, 2]) + >>> torch.any(a) + tensor(True) + + .. function:: any(input, dim, keepdim=False, *, out=None) -> Tensor + :noindex: + + For each row of :attr:`input` in the given dimension :attr:`dim`, + returns `True` if any element in the row evaluate to `True` and `False` otherwise. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4, 2) < 0 + >>> a + tensor([[ True, True], + [False, True], + [ True, True], + [False, False]]) + >>> torch.any(a, 1) + tensor([ True, True, True, False]) + >>> torch.any(a, 0) + tensor([True, True]) + """ + +@overload +def arange( + start: Number, + end: Number, + step: Number, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + arange(start=0, end, step=1, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a 1-D tensor of size :math:`\left\lceil \frac{\text{end} - \text{start}}{\text{step}} \right\rceil` + with values from the interval ``[start, end)`` taken with common difference + :attr:`step` beginning from `start`. + + Note: When using floating-point dtypes (especially reduced precision types like ``bfloat16``), + the results may be affected by floating-point rounding behavior. Some values in the sequence + might not be exactly representable in certain floating-point formats, which can lead to + repeated values or unexpected rounding. For precise sequences, it is recommended to use + integer dtypes instead of floating-point dtypes. + + Note that non-integer :attr:`step` is subject to floating point rounding errors when + comparing against :attr:`end`; to avoid inconsistency, we advise subtracting a small epsilon from :attr:`end` + in such cases. + + .. math:: + \text{out}_{{i+1}} = \text{out}_{i} + \text{step} + + Args: + start (Number, optional): the starting value for the set of points. Default: ``0``. + end (Number): the ending value for the set of points + step (Number, optional): the gap between each pair of adjacent points. Default: ``1``. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). If `dtype` is not given, infer the data type from the other input + arguments. If any of `start`, `end`, or `stop` are floating-point, the + `dtype` is inferred to be the default dtype, see + :meth:`~torch.get_default_dtype`. Otherwise, the `dtype` is inferred to + be `torch.int64`. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.arange(5) + tensor([ 0, 1, 2, 3, 4]) + >>> torch.arange(1, 4) + tensor([ 1, 2, 3]) + >>> torch.arange(1, 2.5, 0.5) + tensor([ 1.0000, 1.5000, 2.0000]) + """ + +@overload +def arange( + start: Number, + end: Number, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + arange(start=0, end, step=1, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a 1-D tensor of size :math:`\left\lceil \frac{\text{end} - \text{start}}{\text{step}} \right\rceil` + with values from the interval ``[start, end)`` taken with common difference + :attr:`step` beginning from `start`. + + Note: When using floating-point dtypes (especially reduced precision types like ``bfloat16``), + the results may be affected by floating-point rounding behavior. Some values in the sequence + might not be exactly representable in certain floating-point formats, which can lead to + repeated values or unexpected rounding. For precise sequences, it is recommended to use + integer dtypes instead of floating-point dtypes. + + Note that non-integer :attr:`step` is subject to floating point rounding errors when + comparing against :attr:`end`; to avoid inconsistency, we advise subtracting a small epsilon from :attr:`end` + in such cases. + + .. math:: + \text{out}_{{i+1}} = \text{out}_{i} + \text{step} + + Args: + start (Number, optional): the starting value for the set of points. Default: ``0``. + end (Number): the ending value for the set of points + step (Number, optional): the gap between each pair of adjacent points. Default: ``1``. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). If `dtype` is not given, infer the data type from the other input + arguments. If any of `start`, `end`, or `stop` are floating-point, the + `dtype` is inferred to be the default dtype, see + :meth:`~torch.get_default_dtype`. Otherwise, the `dtype` is inferred to + be `torch.int64`. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.arange(5) + tensor([ 0, 1, 2, 3, 4]) + >>> torch.arange(1, 4) + tensor([ 1, 2, 3]) + >>> torch.arange(1, 2.5, 0.5) + tensor([ 1.0000, 1.5000, 2.0000]) + """ + +@overload +def arange( + end: Number, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + arange(start=0, end, step=1, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a 1-D tensor of size :math:`\left\lceil \frac{\text{end} - \text{start}}{\text{step}} \right\rceil` + with values from the interval ``[start, end)`` taken with common difference + :attr:`step` beginning from `start`. + + Note: When using floating-point dtypes (especially reduced precision types like ``bfloat16``), + the results may be affected by floating-point rounding behavior. Some values in the sequence + might not be exactly representable in certain floating-point formats, which can lead to + repeated values or unexpected rounding. For precise sequences, it is recommended to use + integer dtypes instead of floating-point dtypes. + + Note that non-integer :attr:`step` is subject to floating point rounding errors when + comparing against :attr:`end`; to avoid inconsistency, we advise subtracting a small epsilon from :attr:`end` + in such cases. + + .. math:: + \text{out}_{{i+1}} = \text{out}_{i} + \text{step} + + Args: + start (Number, optional): the starting value for the set of points. Default: ``0``. + end (Number): the ending value for the set of points + step (Number, optional): the gap between each pair of adjacent points. Default: ``1``. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). If `dtype` is not given, infer the data type from the other input + arguments. If any of `start`, `end`, or `stop` are floating-point, the + `dtype` is inferred to be the default dtype, see + :meth:`~torch.get_default_dtype`. Otherwise, the `dtype` is inferred to + be `torch.int64`. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.arange(5) + tensor([ 0, 1, 2, 3, 4]) + >>> torch.arange(1, 4) + tensor([ 1, 2, 3]) + >>> torch.arange(1, 2.5, 0.5) + tensor([ 1.0000, 1.5000, 2.0000]) + """ + +@overload +def arange( + end: Number | _complex, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + arange(start=0, end, step=1, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a 1-D tensor of size :math:`\left\lceil \frac{\text{end} - \text{start}}{\text{step}} \right\rceil` + with values from the interval ``[start, end)`` taken with common difference + :attr:`step` beginning from `start`. + + Note: When using floating-point dtypes (especially reduced precision types like ``bfloat16``), + the results may be affected by floating-point rounding behavior. Some values in the sequence + might not be exactly representable in certain floating-point formats, which can lead to + repeated values or unexpected rounding. For precise sequences, it is recommended to use + integer dtypes instead of floating-point dtypes. + + Note that non-integer :attr:`step` is subject to floating point rounding errors when + comparing against :attr:`end`; to avoid inconsistency, we advise subtracting a small epsilon from :attr:`end` + in such cases. + + .. math:: + \text{out}_{{i+1}} = \text{out}_{i} + \text{step} + + Args: + start (Number, optional): the starting value for the set of points. Default: ``0``. + end (Number): the ending value for the set of points + step (Number, optional): the gap between each pair of adjacent points. Default: ``1``. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). If `dtype` is not given, infer the data type from the other input + arguments. If any of `start`, `end`, or `stop` are floating-point, the + `dtype` is inferred to be the default dtype, see + :meth:`~torch.get_default_dtype`. Otherwise, the `dtype` is inferred to + be `torch.int64`. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.arange(5) + tensor([ 0, 1, 2, 3, 4]) + >>> torch.arange(1, 4) + tensor([ 1, 2, 3]) + >>> torch.arange(1, 2.5, 0.5) + tensor([ 1.0000, 1.5000, 2.0000]) + """ + +@overload +def arange( + start: Number | _complex, + end: Number | _complex, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + arange(start=0, end, step=1, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a 1-D tensor of size :math:`\left\lceil \frac{\text{end} - \text{start}}{\text{step}} \right\rceil` + with values from the interval ``[start, end)`` taken with common difference + :attr:`step` beginning from `start`. + + Note: When using floating-point dtypes (especially reduced precision types like ``bfloat16``), + the results may be affected by floating-point rounding behavior. Some values in the sequence + might not be exactly representable in certain floating-point formats, which can lead to + repeated values or unexpected rounding. For precise sequences, it is recommended to use + integer dtypes instead of floating-point dtypes. + + Note that non-integer :attr:`step` is subject to floating point rounding errors when + comparing against :attr:`end`; to avoid inconsistency, we advise subtracting a small epsilon from :attr:`end` + in such cases. + + .. math:: + \text{out}_{{i+1}} = \text{out}_{i} + \text{step} + + Args: + start (Number, optional): the starting value for the set of points. Default: ``0``. + end (Number): the ending value for the set of points + step (Number, optional): the gap between each pair of adjacent points. Default: ``1``. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). If `dtype` is not given, infer the data type from the other input + arguments. If any of `start`, `end`, or `stop` are floating-point, the + `dtype` is inferred to be the default dtype, see + :meth:`~torch.get_default_dtype`. Otherwise, the `dtype` is inferred to + be `torch.int64`. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.arange(5) + tensor([ 0, 1, 2, 3, 4]) + >>> torch.arange(1, 4) + tensor([ 1, 2, 3]) + >>> torch.arange(1, 2.5, 0.5) + tensor([ 1.0000, 1.5000, 2.0000]) + """ + +@overload +def arange( + start: Number | _complex, + end: Number | _complex, + step: Number | _complex = 1, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + arange(start=0, end, step=1, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a 1-D tensor of size :math:`\left\lceil \frac{\text{end} - \text{start}}{\text{step}} \right\rceil` + with values from the interval ``[start, end)`` taken with common difference + :attr:`step` beginning from `start`. + + Note: When using floating-point dtypes (especially reduced precision types like ``bfloat16``), + the results may be affected by floating-point rounding behavior. Some values in the sequence + might not be exactly representable in certain floating-point formats, which can lead to + repeated values or unexpected rounding. For precise sequences, it is recommended to use + integer dtypes instead of floating-point dtypes. + + Note that non-integer :attr:`step` is subject to floating point rounding errors when + comparing against :attr:`end`; to avoid inconsistency, we advise subtracting a small epsilon from :attr:`end` + in such cases. + + .. math:: + \text{out}_{{i+1}} = \text{out}_{i} + \text{step} + + Args: + start (Number, optional): the starting value for the set of points. Default: ``0``. + end (Number): the ending value for the set of points + step (Number, optional): the gap between each pair of adjacent points. Default: ``1``. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). If `dtype` is not given, infer the data type from the other input + arguments. If any of `start`, `end`, or `stop` are floating-point, the + `dtype` is inferred to be the default dtype, see + :meth:`~torch.get_default_dtype`. Otherwise, the `dtype` is inferred to + be `torch.int64`. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.arange(5) + tensor([ 0, 1, 2, 3, 4]) + >>> torch.arange(1, 4) + tensor([ 1, 2, 3]) + >>> torch.arange(1, 2.5, 0.5) + tensor([ 1.0000, 1.5000, 2.0000]) + """ + +def arccos(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + arccos(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Alias for :func:`torch.acos`. + """ + +def arccos_(input: Tensor) -> Tensor: ... +def arccosh(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + arccosh(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Alias for :func:`torch.acosh`. + """ + +def arccosh_(input: Tensor) -> Tensor: ... +def arcsin(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + arcsin(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Alias for :func:`torch.asin`. + """ + +def arcsin_(input: Tensor) -> Tensor: ... +def arcsinh(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + arcsinh(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Alias for :func:`torch.asinh`. + """ + +def arcsinh_(input: Tensor) -> Tensor: ... +def arctan(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + arctan(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Alias for :func:`torch.atan`. + """ + +def arctan2( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + arctan2(input: Tensor, other: Tensor, *, out: Optional[Tensor]) -> Tensor + Alias for :func:`torch.atan2`. + """ + +def arctan_(input: Tensor) -> Tensor: ... +def arctanh(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + arctanh(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Alias for :func:`torch.atanh`. + """ + +def arctanh_(input: Tensor) -> Tensor: ... +def argmax( + input: Tensor, + dim: _int | None = None, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + argmax(input) -> LongTensor + + Returns the indices of the maximum value of all elements in the :attr:`input` tensor. + + This is the second value returned by :meth:`torch.max`. See its + documentation for the exact semantics of this method. + + .. note:: If there are multiple maximal values then the indices of the first maximal value are returned. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 1.3398, 0.2663, -0.2686, 0.2450], + [-0.7401, -0.8805, -0.3402, -1.1936], + [ 0.4907, -1.3948, -1.0691, -0.3132], + [-1.6092, 0.5419, -0.2993, 0.3195]]) + >>> torch.argmax(a) + tensor(0) + + .. function:: argmax(input, dim, keepdim=False) -> LongTensor + :noindex: + + Returns the indices of the maximum values of a tensor across a dimension. + + This is the second value returned by :meth:`torch.max`. See its + documentation for the exact semantics of this method. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, the argmax of the flattened input is returned. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 1.3398, 0.2663, -0.2686, 0.2450], + [-0.7401, -0.8805, -0.3402, -1.1936], + [ 0.4907, -1.3948, -1.0691, -0.3132], + [-1.6092, 0.5419, -0.2993, 0.3195]]) + >>> torch.argmax(a, dim=1) + tensor([ 0, 2, 0, 1]) + """ + +def argmin( + input: Tensor, + dim: _int | None = None, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + argmin(input, dim=None, keepdim=False) -> LongTensor + + Returns the indices of the minimum value(s) of the flattened tensor or along a dimension + + This is the second value returned by :meth:`torch.min`. See its + documentation for the exact semantics of this method. + + .. note:: If there are multiple minimal values then the indices of the first minimal value are returned. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, the argmin of the flattened input is returned. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.1139, 0.2254, -0.1381, 0.3687], + [ 1.0100, -1.1975, -0.0102, -0.4732], + [-0.9240, 0.1207, -0.7506, -1.0213], + [ 1.7809, -1.2960, 0.9384, 0.1438]]) + >>> torch.argmin(a) + tensor(13) + >>> torch.argmin(a, dim=1) + tensor([ 2, 1, 3, 1]) + >>> torch.argmin(a, dim=1, keepdim=True) + tensor([[2], + [1], + [3], + [1]]) + """ + +@overload +def argsort( + input: Tensor, + *, + stable: _bool, + dim: _int = -1, + descending: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + argsort(input, dim=-1, descending=False, *, stable=False) -> Tensor + + Returns the indices that sort a tensor along a given dimension in ascending + order by value. + + This is the second value returned by :meth:`torch.sort`. See its documentation + for the exact semantics of this method. + + If :attr:`stable` is ``True`` then the sorting routine becomes stable, preserving + the order of equivalent elements. If ``False``, the relative order of values + which compare equal is not guaranteed. ``True`` is slower. + + Args: + input (Tensor): the input tensor. + dim (int, optional): the dimension to sort along + descending (bool, optional): controls the sorting order (ascending or descending) + + Keyword args: + stable (bool, optional): controls the relative order of equivalent elements + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.0785, 1.5267, -0.8521, 0.4065], + [ 0.1598, 0.0788, -0.0745, -1.2700], + [ 1.2208, 1.0722, -0.7064, 1.2564], + [ 0.0669, -0.2318, -0.8229, -0.9280]]) + + + >>> torch.argsort(a, dim=1) + tensor([[2, 0, 3, 1], + [3, 2, 1, 0], + [2, 1, 0, 3], + [3, 2, 1, 0]]) + """ + +@overload +def argsort( + input: Tensor, + dim: _int = -1, + descending: _bool = False, +) -> Tensor: + r""" + argsort(input, dim=-1, descending=False, *, stable=False) -> Tensor + + Returns the indices that sort a tensor along a given dimension in ascending + order by value. + + This is the second value returned by :meth:`torch.sort`. See its documentation + for the exact semantics of this method. + + If :attr:`stable` is ``True`` then the sorting routine becomes stable, preserving + the order of equivalent elements. If ``False``, the relative order of values + which compare equal is not guaranteed. ``True`` is slower. + + Args: + input (Tensor): the input tensor. + dim (int, optional): the dimension to sort along + descending (bool, optional): controls the sorting order (ascending or descending) + + Keyword args: + stable (bool, optional): controls the relative order of equivalent elements + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.0785, 1.5267, -0.8521, 0.4065], + [ 0.1598, 0.0788, -0.0745, -1.2700], + [ 1.2208, 1.0722, -0.7064, 1.2564], + [ 0.0669, -0.2318, -0.8229, -0.9280]]) + + + >>> torch.argsort(a, dim=1) + tensor([[2, 0, 3, 1], + [3, 2, 1, 0], + [2, 1, 0, 3], + [3, 2, 1, 0]]) + """ + +@overload +def argsort( + input: Tensor, + dim: str | EllipsisType | None, + descending: _bool = False, +) -> Tensor: + r""" + argsort(input, dim=-1, descending=False, *, stable=False) -> Tensor + + Returns the indices that sort a tensor along a given dimension in ascending + order by value. + + This is the second value returned by :meth:`torch.sort`. See its documentation + for the exact semantics of this method. + + If :attr:`stable` is ``True`` then the sorting routine becomes stable, preserving + the order of equivalent elements. If ``False``, the relative order of values + which compare equal is not guaranteed. ``True`` is slower. + + Args: + input (Tensor): the input tensor. + dim (int, optional): the dimension to sort along + descending (bool, optional): controls the sorting order (ascending or descending) + + Keyword args: + stable (bool, optional): controls the relative order of equivalent elements + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.0785, 1.5267, -0.8521, 0.4065], + [ 0.1598, 0.0788, -0.0745, -1.2700], + [ 1.2208, 1.0722, -0.7064, 1.2564], + [ 0.0669, -0.2318, -0.8229, -0.9280]]) + + + >>> torch.argsort(a, dim=1) + tensor([[2, 0, 3, 1], + [3, 2, 1, 0], + [2, 1, 0, 3], + [3, 2, 1, 0]]) + """ + +def argwhere(input: Tensor) -> Tensor: + r""" + argwhere(input) -> Tensor + + Returns a tensor containing the indices of all non-zero elements of + :attr:`input`. Each row in the result contains the indices of a non-zero + element in :attr:`input`. The result is sorted lexicographically, with + the last index changing the fastest (C-style). + + If :attr:`input` has :math:`n` dimensions, then the resulting indices tensor + :attr:`out` is of size :math:`(z \times n)`, where :math:`z` is the total number of + non-zero elements in the :attr:`input` tensor. + + .. note:: + This function is similar to NumPy's `argwhere`. + + When :attr:`input` is on CUDA, this function causes host-device synchronization. + + Args: + {input} + + Example:: + + >>> t = torch.tensor([1, 0, 1]) + >>> torch.argwhere(t) + tensor([[0], + [2]]) + >>> t = torch.tensor([[1, 0, 1], [0, 1, 1]]) + >>> torch.argwhere(t) + tensor([[0, 0], + [0, 2], + [1, 1], + [1, 2]]) + """ + +def as_strided( + input: Tensor, + size: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + storage_offset: _int | SymInt | None = None, +) -> Tensor: + r""" + as_strided(input, size, stride, storage_offset=None) -> Tensor + + Create a view of an existing `torch.Tensor` :attr:`input` with specified + :attr:`size`, :attr:`stride` and :attr:`storage_offset`. + + .. warning:: + Prefer using other view functions, like :meth:`torch.Tensor.view` or + :meth:`torch.Tensor.expand`, to setting a view's strides manually with + `as_strided`, as this function will throw an error on non-standard Pytorch + backends (that do not have a concept of stride) and the result will depend + on the current layout in memory. The constructed view must only refer to + elements within the Tensor's storage or a runtime error will be thrown. + If the generated view is "overlapped" (with multiple indices referring to + the same element in memory), the behavior of inplace operations on this view + is undefined (and might not throw runtime errors). + + Args: + input (Tensor): the input tensor. + size (tuple or ints): the shape of the output tensor + stride (tuple or ints): the stride of the output tensor + storage_offset (int, optional): the offset in the underlying storage of the output tensor. + If ``None``, the storage_offset of the output tensor will match the input tensor. + + Example:: + + >>> x = torch.randn(3, 3) + >>> x + tensor([[ 0.9039, 0.6291, 1.0795], + [ 0.1586, 2.1939, -0.4900], + [-0.1909, -0.7503, 1.9355]]) + >>> t = torch.as_strided(x, (2, 2), (1, 2)) + >>> t + tensor([[0.9039, 1.0795], + [0.6291, 0.1586]]) + >>> t = torch.as_strided(x, (2, 2), (1, 2), 1) + tensor([[0.6291, 0.1586], + [1.0795, 2.1939]]) + """ + +def as_strided_( + input: Tensor, + size: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + storage_offset: _int | SymInt | None = None, +) -> Tensor: ... +def as_strided_copy( + input: Tensor, + size: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + storage_offset: _int | SymInt | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.as_strided`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def as_strided_scatter( + input: Tensor, + src: Tensor, + size: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + storage_offset: _int | SymInt | None = None, +) -> Tensor: + r""" + as_strided_scatter(input, src, size, stride, storage_offset=None) -> Tensor + + Embeds the values of the :attr:`src` tensor into :attr:`input` along + the elements corresponding to the result of calling + input.as_strided(size, stride, storage_offset). + + This function returns a tensor with fresh storage; it does not + return a view. + + Args: + input (Tensor): the input tensor. + size (tuple or ints): the shape of the output tensor + stride (tuple or ints): the stride of the output tensor + storage_offset (int, optional): the offset in the underlying storage of the output tensor + + .. note:: + + :attr:`src` must be of the proper size in order to be embedded + into :attr:`input`. Specifically, it should have the same shape as + `torch.as_strided(input, size, stride, storage_offset)` + + Example:: + + >>> a = torch.arange(4).reshape(2, 2) + 1 + >>> a + tensor([[1, 2], + [3, 4]]) + >>> b = torch.zeros(3, 3) + >>> b + tensor([[0., 0., 0.], + [0., 0., 0.], + [0., 0., 0.]]) + >>> torch.as_strided_scatter(b, a, (2, 2), (1, 2)) + tensor([[1., 3., 2.], + [4., 0., 0.], + [0., 0., 0.]]) + """ + +def as_tensor( + data: Any, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, +) -> Tensor: + r""" + as_tensor(data: Any, *, dtype: Optional[dtype] = None, device: Optional[DeviceLikeType]) -> Tensor + + Converts :attr:`data` into a tensor, sharing data and preserving autograd + history if possible. + + If :attr:`data` is already a tensor with the requested dtype and device + then :attr:`data` itself is returned, but if :attr:`data` is a + tensor with a different dtype or device then it's copied as if using + `data.to(dtype=dtype, device=device)`. + + If :attr:`data` is a NumPy array (an ndarray) with the same dtype and device then a + tensor is constructed using :func:`torch.from_numpy`. + + If :attr:`data` is a CuPy array, the returned tensor will be located on the same device as the CuPy array unless + specifically overwritten by :attr:`device` or a default device. The device of the CuPy array is inferred from the + pointer of the array using `cudaPointerGetAttributes` unless :attr:`device` is provided with an explicit device index. + + .. seealso:: + + :func:`torch.tensor` never shares its data and creates a new "leaf tensor" (see :doc:`/notes/autograd`). + + + Args: + data (array_like): Initial data for the tensor. Can be a list, tuple, + NumPy ``ndarray``, scalar, and other types. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, infers data type from :attr:`data`. + device (:class:`torch.device`, optional): the device of the constructed tensor. If None and data is a tensor + then the device of data is used. If None and data is not a tensor then + the result tensor is constructed on the current device. + + + Example:: + + >>> a = numpy.array([1, 2, 3]) + >>> t = torch.as_tensor(a) + >>> t + tensor([ 1, 2, 3]) + >>> t[0] = -1 + >>> a + array([-1, 2, 3]) + + >>> a = numpy.array([1, 2, 3]) + >>> t = torch.as_tensor(a, device=torch.device('cuda')) + >>> t + tensor([ 1, 2, 3]) + >>> t[0] = -1 + >>> a + array([1, 2, 3]) + """ + +def asarray( + obj: Any, + *, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + copy: _bool | None = None, + requires_grad: _bool | None = None, +) -> Tensor: + r""" + asarray(obj: Any, *, dtype: Optional[dtype], device: Optional[DeviceLikeType], copy: Optional[bool] = None, requires_grad: Optional[bool] = None) -> Tensor # noqa: B950 + + Converts :attr:`obj` to a tensor. + + :attr:`obj` can be one of: + + 1. a tensor + 2. a NumPy array or a NumPy scalar + 3. a DLPack capsule + 4. an object that implements Python's buffer protocol + 5. a scalar + 6. a sequence of scalars + + When :attr:`obj` is a tensor, NumPy array, or DLPack capsule the returned tensor will, + by default, have the same requires_grad as :attr:`obj` (defaulting to False), have the + same datatype, be on the same device, and share memory with it. These properties can be + controlled with the :attr:`dtype`, :attr:`device`, :attr:`copy`, and + :attr:`requires_grad` keyword arguments. If the returned tensor is of a different + datatype, on a different device, or a copy is requested then it will not share its + memory with :attr:`obj`. If :attr:`requires_grad` is ``True`` (or ``None``, and + :attr:`obj` was a tensor with requires_grad set), then the returned tensor will require + a gradient, and if :attr:`obj` is also a tensor with an autograd history then the + returned tensor will have the same history. + + When :attr:`obj` is not a tensor, NumPy array, or DLPack capsule but implements Python's + buffer protocol then the buffer is interpreted as an array of bytes grouped according to + the size of the datatype passed to the :attr:`dtype` keyword argument. (If no datatype is + passed then the default floating point datatype is used, instead.) The returned tensor + will have the specified datatype (or default floating point datatype if none is specified) + and, by default, be on the CPU device and share memory with the buffer. + + When :attr:`obj` is a NumPy scalar, the returned tensor will be a 0-dimensional tensor on + the CPU and that doesn't share its memory (i.e. ``copy=True``). By default datatype will + be the PyTorch datatype corresponding to the NumPy's scalar's datatype. + + When :attr:`obj` is none of the above but a scalar, or a sequence of scalars then the + returned tensor will, by default, infer its datatype from the scalar values, be on the + current default device, and not share its memory. + + .. seealso:: + + :func:`torch.tensor` creates a tensor that always copies the data from the input object. + :func:`torch.from_numpy` creates a tensor that always shares memory from NumPy arrays. + :func:`torch.frombuffer` creates a tensor that always shares memory from objects that + implement the buffer protocol. + :func:`torch.from_dlpack` creates a tensor that always shares memory from + DLPack capsules. + + Args: + obj (object): a tensor, NumPy array, DLPack Capsule, object that implements Python's + buffer protocol, scalar, or sequence of scalars. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the datatype of the returned tensor. + Default: ``None``, which causes the datatype of the returned tensor to be + inferred from :attr:`obj`. + copy (bool, optional): controls whether the returned tensor shares memory with :attr:`obj`. + Default: ``None``, which causes the returned tensor to share memory with :attr:`obj` + whenever possible. If ``True`` then the returned tensor does not share its memory. + If ``False`` then the returned tensor shares its memory with :attr:`obj` and an + error is thrown if it cannot. + device (:class:`torch.device`, optional): the device of the returned tensor. + Default: ``None``, which causes the device of :attr:`obj` to be used. Or, if + :attr:`obj` is a Python sequence, the current default device will be used. + requires_grad (bool, optional): whether the returned tensor requires grad. + Default: ``None``, which causes requires_grad for the returned tensor to be + inferred from :attr:`obj`. If ``True``, then the returned tensor will require + a gradient, and if :attr:`obj` is also a tensor with an autograd history then + the returned tensor will have the same history. + + Example:: + + >>> a = torch.tensor([1, 2, 3]) + >>> # Shares memory with tensor 'a' + >>> b = torch.asarray(a) + >>> a.data_ptr() == b.data_ptr() + True + >>> # Forces memory copy + >>> c = torch.asarray(a, copy=True) + >>> a.data_ptr() == c.data_ptr() + False + + >>> a = torch.tensor([1., 2., 3.], requires_grad=True) + >>> b = a + 2 + >>> b + tensor([3., 4., 5.], grad_fn=) + >>> # Shares memory with tensor 'b', with no grad + >>> c = torch.asarray(b, requires_grad=False) + >>> c + tensor([3., 4., 5.]) + >>> # Shares memory with tensor 'b', retaining autograd history + >>> d = torch.asarray(b, requires_grad=True) + >>> d + tensor([3., 4., 5.], grad_fn=) + >>> # Shares memory with tensor 'b', retaining autograd history + >>> e = torch.asarray(b) + >>> e + tensor([3., 4., 5.], grad_fn=) + + >>> array = numpy.array([1, 2, 3]) + >>> # Shares memory with array 'array' + >>> t1 = torch.asarray(array) + >>> array.__array_interface__['data'][0] == t1.data_ptr() + True + >>> # Copies memory due to dtype mismatch + >>> t2 = torch.asarray(array, dtype=torch.float32) + >>> array.__array_interface__['data'][0] == t2.data_ptr() + False + + >>> scalar = numpy.float64(0.5) + >>> torch.asarray(scalar) + tensor(0.5000, dtype=torch.float64) + """ + +def asin(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + asin(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Returns a new tensor with the arcsine of the elements (in radians) in the :attr:`input` tensor. + + .. math:: + \text{out}_{i} = \sin^{-1}(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-0.5962, 1.4985, -0.4396, 1.4525]) + >>> torch.asin(a) + tensor([-0.6387, nan, -0.4552, nan]) + """ + +def asin_(input: Tensor) -> Tensor: ... +def asinh(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + asinh(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Returns a new tensor with the inverse hyperbolic sine of the elements of :attr:`input`. + + .. math:: + \text{out}_{i} = \sinh^{-1}(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword arguments: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.1606, -1.4267, -1.0899, -1.0250 ]) + >>> torch.asinh(a) + tensor([ 0.1599, -1.1534, -0.9435, -0.8990 ]) + """ + +def asinh_(input: Tensor) -> Tensor: ... +def atan(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + atan(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Returns a new tensor with the arctangent of the elements (in radians) in the :attr:`input` tensor. + + .. math:: + \text{out}_{i} = \tan^{-1}(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.2341, 0.2539, -0.6256, -0.6448]) + >>> torch.atan(a) + tensor([ 0.2299, 0.2487, -0.5591, -0.5727]) + """ + +def atan2( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + atan2(input: Tensor, other: Tensor, *, out: Optional[Tensor]) -> Tensor + + Element-wise arctangent of :math:`\text{input}_{i} / \text{other}_{i}` + with consideration of the quadrant. Returns a new tensor with the signed angles + in radians between vector :math:`(\text{other}_{i}, \text{input}_{i})` + and vector :math:`(1, 0)`. (Note that :math:`\text{other}_{i}`, the second + parameter, is the x-coordinate, while :math:`\text{input}_{i}`, the first + parameter, is the y-coordinate.) + + The shapes of ``input`` and ``other`` must be + :ref:`broadcastable `. + + Args: + input (Tensor): the first input tensor + other (Tensor): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.9041, 0.0196, -0.3108, -2.4423]) + >>> torch.atan2(a, torch.randn(4)) + tensor([ 0.9833, 0.0811, -1.9743, -1.4151]) + """ + +def atan_(input: Tensor) -> Tensor: ... +def atanh(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + atanh(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Returns a new tensor with the inverse hyperbolic tangent of the elements of :attr:`input`. + + Note: + The domain of the inverse hyperbolic tangent is `(-1, 1)` and values outside this range + will be mapped to ``NaN``, except for the values `1` and `-1` for which the output is + mapped to `+/-INF` respectively. + + .. math:: + \text{out}_{i} = \tanh^{-1}(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword arguments: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4).uniform_(-1, 1) + >>> a + tensor([ -0.9385, 0.2968, -0.8591, -0.1871 ]) + >>> torch.atanh(a) + tensor([ -1.7253, 0.3060, -1.2899, -0.1893 ]) + """ + +def atanh_(input: Tensor) -> Tensor: ... +def avg_pool1d( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size = (), + padding: _int | _size = 0, + ceil_mode: _bool = False, + count_include_pad: _bool = True, +) -> Tensor: ... +@overload +def baddbmm( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + batch1: Tensor, + batch2: Tensor, +) -> Tensor: + r""" + baddbmm(input, batch1, batch2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices in :attr:`batch1` + and :attr:`batch2`. + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the same + number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a + :math:`(b \times n \times p)` tensor and :attr:`out` will be a + :math:`(b \times n \times p)` tensor. Both :attr:`alpha` and :attr:`beta` mean the + same as the scaling factors used in :meth:`torch.addbmm`. + + .. math:: + \text{out}_i = \beta\ \text{input}_i + \alpha\ (\text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): the tensor to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{batch1} \mathbin{@} \text{batch2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(10, 3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.baddbmm(M, batch1, batch2).size() + torch.Size([10, 3, 5]) + """ + +@overload +def baddbmm( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + batch1: Tensor, + batch2: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + baddbmm(input, batch1, batch2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices in :attr:`batch1` + and :attr:`batch2`. + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the same + number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a + :math:`(b \times n \times p)` tensor and :attr:`out` will be a + :math:`(b \times n \times p)` tensor. Both :attr:`alpha` and :attr:`beta` mean the + same as the scaling factors used in :meth:`torch.addbmm`. + + .. math:: + \text{out}_i = \beta\ \text{input}_i + \alpha\ (\text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): the tensor to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{batch1} \mathbin{@} \text{batch2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(10, 3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.baddbmm(M, batch1, batch2).size() + torch.Size([10, 3, 5]) + """ + +@overload +def baddbmm( + input: Tensor, + batch1: Tensor, + batch2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + baddbmm(input, batch1, batch2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices in :attr:`batch1` + and :attr:`batch2`. + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the same + number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a + :math:`(b \times n \times p)` tensor and :attr:`out` will be a + :math:`(b \times n \times p)` tensor. Both :attr:`alpha` and :attr:`beta` mean the + same as the scaling factors used in :meth:`torch.addbmm`. + + .. math:: + \text{out}_i = \beta\ \text{input}_i + \alpha\ (\text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): the tensor to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{batch1} \mathbin{@} \text{batch2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(10, 3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.baddbmm(M, batch1, batch2).size() + torch.Size([10, 3, 5]) + """ + +@overload +def baddbmm( + input: Tensor, + batch1: Tensor, + batch2: Tensor, + out_dtype: _dtype, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + baddbmm(input, batch1, batch2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices in :attr:`batch1` + and :attr:`batch2`. + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the same + number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a + :math:`(b \times n \times p)` tensor and :attr:`out` will be a + :math:`(b \times n \times p)` tensor. Both :attr:`alpha` and :attr:`beta` mean the + same as the scaling factors used in :meth:`torch.addbmm`. + + .. math:: + \text{out}_i = \beta\ \text{input}_i + \alpha\ (\text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): the tensor to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{batch1} \mathbin{@} \text{batch2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(10, 3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.baddbmm(M, batch1, batch2).size() + torch.Size([10, 3, 5]) + """ + +@overload +def baddbmm( + beta: Number | _complex, + self: Tensor, + batch1: Tensor, + batch2: Tensor, +) -> Tensor: + r""" + baddbmm(input, batch1, batch2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices in :attr:`batch1` + and :attr:`batch2`. + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the same + number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a + :math:`(b \times n \times p)` tensor and :attr:`out` will be a + :math:`(b \times n \times p)` tensor. Both :attr:`alpha` and :attr:`beta` mean the + same as the scaling factors used in :meth:`torch.addbmm`. + + .. math:: + \text{out}_i = \beta\ \text{input}_i + \alpha\ (\text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): the tensor to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{batch1} \mathbin{@} \text{batch2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(10, 3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.baddbmm(M, batch1, batch2).size() + torch.Size([10, 3, 5]) + """ + +@overload +def baddbmm( + beta: Number | _complex, + self: Tensor, + batch1: Tensor, + batch2: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + baddbmm(input, batch1, batch2, out_dtype=None, *, beta=1, alpha=1, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices in :attr:`batch1` + and :attr:`batch2`. + :attr:`input` is added to the final result. + + :attr:`batch1` and :attr:`batch2` must be 3-D tensors each containing the same + number of matrices. + + If :attr:`batch1` is a :math:`(b \times n \times m)` tensor, :attr:`batch2` is a + :math:`(b \times m \times p)` tensor, then :attr:`input` must be + :ref:`broadcastable ` with a + :math:`(b \times n \times p)` tensor and :attr:`out` will be a + :math:`(b \times n \times p)` tensor. Both :attr:`alpha` and :attr:`beta` mean the + same as the scaling factors used in :meth:`torch.addbmm`. + + .. math:: + \text{out}_i = \beta\ \text{input}_i + \alpha\ (\text{batch1}_i \mathbin{@} \text{batch2}_i) + + If :attr:`beta` is 0, then the content of :attr:`input` will be ignored, and `nan` and `inf` in + it will not be propagated. + + For inputs of type `FloatTensor` or `DoubleTensor`, arguments :attr:`beta` and + :attr:`alpha` must be real numbers, otherwise they should be integers. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): the tensor to be added + batch1 (Tensor): the first batch of matrices to be multiplied + batch2 (Tensor): the second batch of matrices to be multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + beta (Number, optional): multiplier for :attr:`input` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`\text{batch1} \mathbin{@} \text{batch2}` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + + Example:: + + >>> M = torch.randn(10, 3, 5) + >>> batch1 = torch.randn(10, 3, 4) + >>> batch2 = torch.randn(10, 4, 5) + >>> torch.baddbmm(M, batch1, batch2).size() + torch.Size([10, 3, 5]) + """ + +@overload +def bartlett_window( + window_length: _int, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + bartlett_window(window_length, periodic=True, *, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Bartlett window function. + + .. math:: + w[n] = 1 - \left| \frac{2n}{N-1} - 1 \right| = \begin{cases} + \frac{2n}{N - 1} & \text{if } 0 \leq n \leq \frac{N - 1}{2} \\ + 2 - \frac{2n}{N - 1} & \text{if } \frac{N - 1}{2} < n < N \\ + \end{cases}, + + where :math:`N` is the full window size. + + The input :attr:`window_length` is a positive integer controlling the + returned window size. :attr:`periodic` flag determines whether the returned + window trims off the last duplicate value from the symmetric window and is + ready to be used as a periodic window with functions like + :meth:`torch.stft`. Therefore, if :attr:`periodic` is true, the :math:`N` in + above formula is in fact :math:`\text{window\_length} + 1`. Also, we always have + ``torch.bartlett_window(L, periodic=True)`` equal to + ``torch.bartlett_window(L + 1, periodic=False)[:-1])``. + + .. note:: + If :attr:`window_length` :math:`=1`, the returned window contains a single value 1. + + Arguments: + window_length (int): the size of returned window + periodic (bool, optional): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window + """ + +@overload +def bartlett_window( + window_length: _int, + periodic: _bool, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + bartlett_window(window_length, periodic=True, *, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Bartlett window function. + + .. math:: + w[n] = 1 - \left| \frac{2n}{N-1} - 1 \right| = \begin{cases} + \frac{2n}{N - 1} & \text{if } 0 \leq n \leq \frac{N - 1}{2} \\ + 2 - \frac{2n}{N - 1} & \text{if } \frac{N - 1}{2} < n < N \\ + \end{cases}, + + where :math:`N` is the full window size. + + The input :attr:`window_length` is a positive integer controlling the + returned window size. :attr:`periodic` flag determines whether the returned + window trims off the last duplicate value from the symmetric window and is + ready to be used as a periodic window with functions like + :meth:`torch.stft`. Therefore, if :attr:`periodic` is true, the :math:`N` in + above formula is in fact :math:`\text{window\_length} + 1`. Also, we always have + ``torch.bartlett_window(L, periodic=True)`` equal to + ``torch.bartlett_window(L + 1, periodic=False)[:-1])``. + + .. note:: + If :attr:`window_length` :math:`=1`, the returned window contains a single value 1. + + Arguments: + window_length (int): the size of returned window + periodic (bool, optional): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window + """ + +def batch_norm( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor | None, + running_var: Tensor | None, + training: _bool, + momentum: _float, + eps: _float, + cudnn_enabled: _bool, +) -> Tensor: ... +def batch_norm_backward_elemt( + grad_out: Tensor, + input: Tensor, + mean: Tensor, + invstd: Tensor, + weight: Tensor | None, + sum_dy: Tensor, + sum_dy_xmu: Tensor, + count: Tensor, +) -> Tensor: ... +def batch_norm_backward_reduce( + grad_out: Tensor, + input: Tensor, + mean: Tensor, + invstd: Tensor, + weight: Tensor | None, + input_g: _bool, + weight_g: _bool, + bias_g: _bool, +) -> tuple[Tensor, Tensor, Tensor, Tensor]: ... +def batch_norm_elemt( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + mean: Tensor, + invstd: Tensor, + eps: _float, + *, + out: Tensor | None = None, +) -> Tensor: ... +def batch_norm_gather_stats( + input: Tensor, + mean: Tensor, + invstd: Tensor, + running_mean: Tensor | None, + running_var: Tensor | None, + momentum: _float, + eps: _float, + count: _int, +) -> tuple[Tensor, Tensor]: ... +def batch_norm_gather_stats_with_counts( + input: Tensor, + mean: Tensor, + invstd: Tensor, + running_mean: Tensor | None, + running_var: Tensor | None, + momentum: _float, + eps: _float, + counts: Tensor, +) -> tuple[Tensor, Tensor]: ... +def batch_norm_stats(input: Tensor, eps: _float) -> tuple[Tensor, Tensor]: ... +def batch_norm_update_stats( + input: Tensor, + running_mean: Tensor | None, + running_var: Tensor | None, + momentum: _float, +) -> tuple[Tensor, Tensor]: ... +@overload +def bernoulli( + input: Tensor, + *, + generator: Generator | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + bernoulli(input: Tensor, *, generator: Optional[Generator], out: Optional[Tensor]) -> Tensor + + Draws binary random numbers (0 or 1) from a Bernoulli distribution. + + The :attr:`input` tensor should be a tensor containing probabilities + to be used for drawing the binary random number. + Hence, all values in :attr:`input` have to be in the range: + :math:`0 \leq \text{input}_i \leq 1`. + + The :math:`\text{i}^{th}` element of the output tensor will draw a + value :math:`1` according to the :math:`\text{i}^{th}` probability value given + in :attr:`input`. + + .. math:: + \text{out}_{i} \sim \mathrm{Bernoulli}(p = \text{input}_{i}) + + The returned :attr:`out` tensor only has values 0 or 1 and is of the same + shape as :attr:`input`. + + :attr:`out` can have integral ``dtype``, but :attr:`input` must have floating + point ``dtype``. + + Args: + input (Tensor): the input tensor of probability values for the Bernoulli distribution + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.empty(3, 3).uniform_(0, 1) # generate a uniform random matrix with range [0, 1] + >>> a + tensor([[ 0.1737, 0.0950, 0.3609], + [ 0.7148, 0.0289, 0.2676], + [ 0.9456, 0.8937, 0.7202]]) + >>> torch.bernoulli(a) + tensor([[ 1., 0., 0.], + [ 0., 0., 0.], + [ 1., 1., 1.]]) + + >>> a = torch.ones(3, 3) # probability of drawing "1" is 1 + >>> torch.bernoulli(a) + tensor([[ 1., 1., 1.], + [ 1., 1., 1.], + [ 1., 1., 1.]]) + >>> a = torch.zeros(3, 3) # probability of drawing "1" is 0 + >>> torch.bernoulli(a) + tensor([[ 0., 0., 0.], + [ 0., 0., 0.], + [ 0., 0., 0.]]) + """ + +@overload +def bernoulli( + input: Tensor, + p: _float, + *, + generator: Generator | None = None, +) -> Tensor: + r""" + bernoulli(input: Tensor, *, generator: Optional[Generator], out: Optional[Tensor]) -> Tensor + + Draws binary random numbers (0 or 1) from a Bernoulli distribution. + + The :attr:`input` tensor should be a tensor containing probabilities + to be used for drawing the binary random number. + Hence, all values in :attr:`input` have to be in the range: + :math:`0 \leq \text{input}_i \leq 1`. + + The :math:`\text{i}^{th}` element of the output tensor will draw a + value :math:`1` according to the :math:`\text{i}^{th}` probability value given + in :attr:`input`. + + .. math:: + \text{out}_{i} \sim \mathrm{Bernoulli}(p = \text{input}_{i}) + + The returned :attr:`out` tensor only has values 0 or 1 and is of the same + shape as :attr:`input`. + + :attr:`out` can have integral ``dtype``, but :attr:`input` must have floating + point ``dtype``. + + Args: + input (Tensor): the input tensor of probability values for the Bernoulli distribution + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.empty(3, 3).uniform_(0, 1) # generate a uniform random matrix with range [0, 1] + >>> a + tensor([[ 0.1737, 0.0950, 0.3609], + [ 0.7148, 0.0289, 0.2676], + [ 0.9456, 0.8937, 0.7202]]) + >>> torch.bernoulli(a) + tensor([[ 1., 0., 0.], + [ 0., 0., 0.], + [ 1., 1., 1.]]) + + >>> a = torch.ones(3, 3) # probability of drawing "1" is 1 + >>> torch.bernoulli(a) + tensor([[ 1., 1., 1.], + [ 1., 1., 1.], + [ 1., 1., 1.]]) + >>> a = torch.zeros(3, 3) # probability of drawing "1" is 0 + >>> torch.bernoulli(a) + tensor([[ 0., 0., 0.], + [ 0., 0., 0.], + [ 0., 0., 0.]]) + """ + +def bilinear( + input1: Tensor, + input2: Tensor, + weight: Tensor, + bias: Tensor | None = None, +) -> Tensor: ... +def binary_cross_entropy_with_logits( + input: Tensor, + target: Tensor, + weight: Tensor | None = None, + pos_weight: Tensor | None = None, + reduction: _int = 1, +) -> Tensor: ... +def bincount( + input: Tensor, + weights: Tensor | None = None, + minlength: _int | SymInt = 0, +) -> Tensor: + r""" + bincount(input, weights=None, minlength=0) -> Tensor + + Count the frequency of each value in an array of non-negative ints. + + The number of bins (size 1) is one larger than the largest value in + :attr:`input` unless :attr:`input` is empty, in which case the result is a + tensor of size 0. If :attr:`minlength` is specified, the number of bins is at least + :attr:`minlength` and if :attr:`input` is empty, then the result is tensor of size + :attr:`minlength` filled with zeros. If ``n`` is the value at position ``i``, + ``out[n] += weights[i]`` if :attr:`weights` is specified else + ``out[n] += 1``. + + Note: + This operation may produce nondeterministic gradients when given tensors on a CUDA device. See :doc:`/notes/randomness` for more information. + + Arguments: + input (Tensor): 1-d int tensor + weights (Tensor): optional, weight for each value in the input tensor. + Should be of same size as input tensor. + minlength (int): optional, minimum number of bins. Should be non-negative. + + Returns: + output (Tensor): a tensor of shape ``Size([max(input) + 1])`` if + :attr:`input` is non-empty, else ``Size(0)`` + + Example:: + + >>> input = torch.randint(0, 8, (5,), dtype=torch.int64) + >>> weights = torch.linspace(0, 1, steps=5) + >>> input, weights + (tensor([4, 3, 6, 3, 4]), + tensor([ 0.0000, 0.2500, 0.5000, 0.7500, 1.0000]) + + >>> torch.bincount(input) + tensor([0, 0, 0, 2, 2, 0, 1]) + + >>> input.bincount(weights) + tensor([0.0000, 0.0000, 0.0000, 1.0000, 1.0000, 0.0000, 0.5000]) + """ + +def binomial( + count: Tensor, + prob: Tensor, + generator: Generator | None = None, +) -> Tensor: ... +@overload +def bitwise_and( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bitwise_and(input, other, *, out=None) -> Tensor + + Computes the bitwise AND of :attr:`input` and :attr:`other`. The input tensor must be of + integral or Boolean types. For bool tensors, it computes the logical AND. + + Args: + input: the first input tensor + other: the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_and(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([1, 0, 3], dtype=torch.int8) + >>> torch.bitwise_and(torch.tensor([True, True, False]), torch.tensor([False, True, False])) + tensor([ False, True, False]) + """ + +@overload +def bitwise_and(self: Number | _complex, other: Tensor) -> Tensor: + r""" + bitwise_and(input, other, *, out=None) -> Tensor + + Computes the bitwise AND of :attr:`input` and :attr:`other`. The input tensor must be of + integral or Boolean types. For bool tensors, it computes the logical AND. + + Args: + input: the first input tensor + other: the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_and(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([1, 0, 3], dtype=torch.int8) + >>> torch.bitwise_and(torch.tensor([True, True, False]), torch.tensor([False, True, False])) + tensor([ False, True, False]) + """ + +@overload +def bitwise_and( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bitwise_and(input, other, *, out=None) -> Tensor + + Computes the bitwise AND of :attr:`input` and :attr:`other`. The input tensor must be of + integral or Boolean types. For bool tensors, it computes the logical AND. + + Args: + input: the first input tensor + other: the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_and(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([1, 0, 3], dtype=torch.int8) + >>> torch.bitwise_and(torch.tensor([True, True, False]), torch.tensor([False, True, False])) + tensor([ False, True, False]) + """ + +@overload +def bitwise_left_shift( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bitwise_left_shift(input, other, *, out=None) -> Tensor + + Computes the left arithmetic shift of :attr:`input` by :attr:`other` bits. + The input tensor must be of integral type. This operator supports + :ref:`broadcasting to a common shape ` and + :ref:`type promotion `. + + The operation applied is: + + .. math:: + \text{out}_i = \text{input}_i << \text{other}_i + + Args: + input (Tensor or Scalar): the first input tensor + other (Tensor or Scalar): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_left_shift(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-2, -2, 24], dtype=torch.int8) + """ + +@overload +def bitwise_left_shift(self: Number | _complex, other: Tensor) -> Tensor: + r""" + bitwise_left_shift(input, other, *, out=None) -> Tensor + + Computes the left arithmetic shift of :attr:`input` by :attr:`other` bits. + The input tensor must be of integral type. This operator supports + :ref:`broadcasting to a common shape ` and + :ref:`type promotion `. + + The operation applied is: + + .. math:: + \text{out}_i = \text{input}_i << \text{other}_i + + Args: + input (Tensor or Scalar): the first input tensor + other (Tensor or Scalar): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_left_shift(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-2, -2, 24], dtype=torch.int8) + """ + +@overload +def bitwise_left_shift( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bitwise_left_shift(input, other, *, out=None) -> Tensor + + Computes the left arithmetic shift of :attr:`input` by :attr:`other` bits. + The input tensor must be of integral type. This operator supports + :ref:`broadcasting to a common shape ` and + :ref:`type promotion `. + + The operation applied is: + + .. math:: + \text{out}_i = \text{input}_i << \text{other}_i + + Args: + input (Tensor or Scalar): the first input tensor + other (Tensor or Scalar): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_left_shift(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-2, -2, 24], dtype=torch.int8) + """ + +def bitwise_not(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + bitwise_not(input, *, out=None) -> Tensor + + Computes the bitwise NOT of the given input tensor. The input tensor must be of + integral or Boolean types. For bool tensors, it computes the logical NOT. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_not(torch.tensor([-1, -2, 3], dtype=torch.int8)) + tensor([ 0, 1, -4], dtype=torch.int8) + """ + +@overload +def bitwise_or( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bitwise_or(input: Tensor, other: Tensor, *, out: Optional[Tensor]) -> Tensor + + Computes the bitwise OR of :attr:`input` and :attr:`other`. The input tensor must be of + integral or Boolean types. For bool tensors, it computes the logical OR. + + Args: + input: the first input tensor + other: the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_or(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-1, -2, 3], dtype=torch.int8) + >>> torch.bitwise_or(torch.tensor([True, True, False]), torch.tensor([False, True, False])) + tensor([ True, True, False]) + """ + +@overload +def bitwise_or(self: Number | _complex, other: Tensor) -> Tensor: + r""" + bitwise_or(input: Tensor, other: Tensor, *, out: Optional[Tensor]) -> Tensor + + Computes the bitwise OR of :attr:`input` and :attr:`other`. The input tensor must be of + integral or Boolean types. For bool tensors, it computes the logical OR. + + Args: + input: the first input tensor + other: the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_or(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-1, -2, 3], dtype=torch.int8) + >>> torch.bitwise_or(torch.tensor([True, True, False]), torch.tensor([False, True, False])) + tensor([ True, True, False]) + """ + +@overload +def bitwise_or( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bitwise_or(input: Tensor, other: Tensor, *, out: Optional[Tensor]) -> Tensor + + Computes the bitwise OR of :attr:`input` and :attr:`other`. The input tensor must be of + integral or Boolean types. For bool tensors, it computes the logical OR. + + Args: + input: the first input tensor + other: the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_or(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-1, -2, 3], dtype=torch.int8) + >>> torch.bitwise_or(torch.tensor([True, True, False]), torch.tensor([False, True, False])) + tensor([ True, True, False]) + """ + +@overload +def bitwise_right_shift( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bitwise_right_shift(input, other, *, out=None) -> Tensor + + Computes the right arithmetic shift of :attr:`input` by :attr:`other` bits. + The input tensor must be of integral type. This operator supports + :ref:`broadcasting to a common shape ` and + :ref:`type promotion `. + In any case, if the value of the right operand is negative or is greater + or equal to the number of bits in the promoted left operand, the behavior is undefined. + + The operation applied is: + + .. math:: + \text{out}_i = \text{input}_i >> \text{other}_i + + Args: + input (Tensor or Scalar): the first input tensor + other (Tensor or Scalar): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_right_shift(torch.tensor([-2, -7, 31], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-1, -7, 3], dtype=torch.int8) + """ + +@overload +def bitwise_right_shift(self: Number | _complex, other: Tensor) -> Tensor: + r""" + bitwise_right_shift(input, other, *, out=None) -> Tensor + + Computes the right arithmetic shift of :attr:`input` by :attr:`other` bits. + The input tensor must be of integral type. This operator supports + :ref:`broadcasting to a common shape ` and + :ref:`type promotion `. + In any case, if the value of the right operand is negative or is greater + or equal to the number of bits in the promoted left operand, the behavior is undefined. + + The operation applied is: + + .. math:: + \text{out}_i = \text{input}_i >> \text{other}_i + + Args: + input (Tensor or Scalar): the first input tensor + other (Tensor or Scalar): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_right_shift(torch.tensor([-2, -7, 31], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-1, -7, 3], dtype=torch.int8) + """ + +@overload +def bitwise_right_shift( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bitwise_right_shift(input, other, *, out=None) -> Tensor + + Computes the right arithmetic shift of :attr:`input` by :attr:`other` bits. + The input tensor must be of integral type. This operator supports + :ref:`broadcasting to a common shape ` and + :ref:`type promotion `. + In any case, if the value of the right operand is negative or is greater + or equal to the number of bits in the promoted left operand, the behavior is undefined. + + The operation applied is: + + .. math:: + \text{out}_i = \text{input}_i >> \text{other}_i + + Args: + input (Tensor or Scalar): the first input tensor + other (Tensor or Scalar): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_right_shift(torch.tensor([-2, -7, 31], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-1, -7, 3], dtype=torch.int8) + """ + +@overload +def bitwise_xor( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bitwise_xor(input, other, *, out=None) -> Tensor + + Computes the bitwise XOR of :attr:`input` and :attr:`other`. The input tensor must be of + integral or Boolean types. For bool tensors, it computes the logical XOR. + + Args: + input: the first input tensor + other: the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_xor(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-2, -2, 0], dtype=torch.int8) + >>> torch.bitwise_xor(torch.tensor([True, True, False]), torch.tensor([False, True, False])) + tensor([ True, False, False]) + """ + +@overload +def bitwise_xor(self: Number | _complex, other: Tensor) -> Tensor: + r""" + bitwise_xor(input, other, *, out=None) -> Tensor + + Computes the bitwise XOR of :attr:`input` and :attr:`other`. The input tensor must be of + integral or Boolean types. For bool tensors, it computes the logical XOR. + + Args: + input: the first input tensor + other: the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_xor(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-2, -2, 0], dtype=torch.int8) + >>> torch.bitwise_xor(torch.tensor([True, True, False]), torch.tensor([False, True, False])) + tensor([ True, False, False]) + """ + +@overload +def bitwise_xor( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bitwise_xor(input, other, *, out=None) -> Tensor + + Computes the bitwise XOR of :attr:`input` and :attr:`other`. The input tensor must be of + integral or Boolean types. For bool tensors, it computes the logical XOR. + + Args: + input: the first input tensor + other: the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.bitwise_xor(torch.tensor([-1, -2, 3], dtype=torch.int8), torch.tensor([1, 0, 3], dtype=torch.int8)) + tensor([-2, -2, 0], dtype=torch.int8) + >>> torch.bitwise_xor(torch.tensor([True, True, False]), torch.tensor([False, True, False])) + tensor([ True, False, False]) + """ + +@overload +def blackman_window( + window_length: _int, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + blackman_window(window_length, periodic=True, *, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Blackman window function. + + .. math:: + w[n] = 0.42 - 0.5 \cos \left( \frac{2 \pi n}{N - 1} \right) + 0.08 \cos \left( \frac{4 \pi n}{N - 1} \right) + + where :math:`N` is the full window size. + + The input :attr:`window_length` is a positive integer controlling the + returned window size. :attr:`periodic` flag determines whether the returned + window trims off the last duplicate value from the symmetric window and is + ready to be used as a periodic window with functions like + :meth:`torch.stft`. Therefore, if :attr:`periodic` is true, the :math:`N` in + above formula is in fact :math:`\text{window\_length} + 1`. Also, we always have + ``torch.blackman_window(L, periodic=True)`` equal to + ``torch.blackman_window(L + 1, periodic=False)[:-1]``. + + .. note:: + If :attr:`window_length` :math:`=1`, the returned window contains a single value 1. + + Arguments: + window_length (int): the size of returned window + periodic (bool, optional): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window + """ + +@overload +def blackman_window( + window_length: _int, + periodic: _bool, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + blackman_window(window_length, periodic=True, *, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Blackman window function. + + .. math:: + w[n] = 0.42 - 0.5 \cos \left( \frac{2 \pi n}{N - 1} \right) + 0.08 \cos \left( \frac{4 \pi n}{N - 1} \right) + + where :math:`N` is the full window size. + + The input :attr:`window_length` is a positive integer controlling the + returned window size. :attr:`periodic` flag determines whether the returned + window trims off the last duplicate value from the symmetric window and is + ready to be used as a periodic window with functions like + :meth:`torch.stft`. Therefore, if :attr:`periodic` is true, the :math:`N` in + above formula is in fact :math:`\text{window\_length} + 1`. Also, we always have + ``torch.blackman_window(L, periodic=True)`` equal to + ``torch.blackman_window(L + 1, periodic=False)[:-1]``. + + .. note:: + If :attr:`window_length` :math:`=1`, the returned window contains a single value 1. + + Arguments: + window_length (int): the size of returned window + periodic (bool, optional): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window + """ + +@overload +def bmm( + input: Tensor, + mat2: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bmm(input, mat2, out_dtype=None, *, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices stored in :attr:`input` + and :attr:`mat2`. + + :attr:`input` and :attr:`mat2` must be 3-D tensors each containing + the same number of matrices. + + If :attr:`input` is a :math:`(b \times n \times m)` tensor, :attr:`mat2` is a + :math:`(b \times m \times p)` tensor, :attr:`out` will be a + :math:`(b \times n \times p)` tensor. + + .. math:: + \text{out}_i = \text{input}_i \mathbin{@} \text{mat2}_i + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + .. note:: This function does not :ref:`broadcast `. + For broadcasting matrix products, see :func:`torch.matmul`. + + Args: + input (Tensor): the first batch of matrices to be multiplied + mat2 (Tensor): the second batch of matrices to be multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword Args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> input = torch.randn(10, 3, 4) + >>> mat2 = torch.randn(10, 4, 5) + >>> res = torch.bmm(input, mat2) + >>> res.size() + torch.Size([10, 3, 5]) + """ + +@overload +def bmm( + input: Tensor, + mat2: Tensor, + out_dtype: _dtype, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + bmm(input, mat2, out_dtype=None, *, out=None) -> Tensor + + Performs a batch matrix-matrix product of matrices stored in :attr:`input` + and :attr:`mat2`. + + :attr:`input` and :attr:`mat2` must be 3-D tensors each containing + the same number of matrices. + + If :attr:`input` is a :math:`(b \times n \times m)` tensor, :attr:`mat2` is a + :math:`(b \times m \times p)` tensor, :attr:`out` will be a + :math:`(b \times n \times p)` tensor. + + .. math:: + \text{out}_i = \text{input}_i \mathbin{@} \text{mat2}_i + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + .. note:: This function does not :ref:`broadcast `. + For broadcasting matrix products, see :func:`torch.matmul`. + + Args: + input (Tensor): the first batch of matrices to be multiplied + mat2 (Tensor): the second batch of matrices to be multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword Args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> input = torch.randn(10, 3, 4) + >>> mat2 = torch.randn(10, 4, 5) + >>> res = torch.bmm(input, mat2) + >>> res.size() + torch.Size([10, 3, 5]) + """ + +def broadcast_to(input: Tensor, size: Sequence[_int | SymInt]) -> Tensor: + r""" + broadcast_to(input, shape) -> Tensor + + Broadcasts :attr:`input` to the shape :attr:`\shape`. + Equivalent to calling ``input.expand(shape)``. See :meth:`~Tensor.expand` for details. + + Args: + input (Tensor): the input tensor. + shape (list, tuple, or :class:`torch.Size`): the new shape. + + Example:: + + >>> x = torch.tensor([1, 2, 3]) + >>> torch.broadcast_to(x, (3, 3)) + tensor([[1, 2, 3], + [1, 2, 3], + [1, 2, 3]]) + """ + +@overload +def bucketize( + input: Tensor, + boundaries: Tensor, + *, + out_int32: _bool = False, + right: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + bucketize(input, boundaries, *, out_int32=False, right=False, out=None) -> Tensor + + Returns the indices of the buckets to which each value in the :attr:`input` belongs, where the + boundaries of the buckets are set by :attr:`boundaries`. Return a new tensor with the same size + as :attr:`input`. If :attr:`right` is False (default), then the left boundary is open. Note that + this behavior is opposite the behavior of + `numpy.digitize `_. + More formally, the returned index satisfies the following rules: + + .. list-table:: + :widths: 15 85 + :header-rows: 1 + + * - :attr:`right` + - *returned index satisfies* + * - False + - ``boundaries[i-1] < input[m][n]...[l][x] <= boundaries[i]`` + * - True + - ``boundaries[i-1] <= input[m][n]...[l][x] < boundaries[i]`` + + Args: + input (Tensor or Scalar): N-D tensor or a Scalar containing the search value(s). + boundaries (Tensor): 1-D tensor, must contain a strictly increasing sequence, or the return value is undefined. + + Keyword args: + out_int32 (bool, optional): indicate the output data type. torch.int32 if True, torch.int64 otherwise. + Default value is False, i.e. default output data type is torch.int64. + right (bool, optional): determines the behavior for values in :attr:`boundaries`. See the table above. + out (Tensor, optional): the output tensor, must be the same size as :attr:`input` if provided. + + + Example:: + + >>> boundaries = torch.tensor([1, 3, 5, 7, 9]) + >>> boundaries + tensor([1, 3, 5, 7, 9]) + >>> v = torch.tensor([[3, 6, 9], [3, 6, 9]]) + >>> v + tensor([[3, 6, 9], + [3, 6, 9]]) + >>> torch.bucketize(v, boundaries) + tensor([[1, 3, 4], + [1, 3, 4]]) + >>> torch.bucketize(v, boundaries, right=True) + tensor([[2, 3, 5], + [2, 3, 5]]) + """ + +@overload +def bucketize( + self: Number | _complex, + boundaries: Tensor, + *, + out_int32: _bool = False, + right: _bool = False, +) -> Tensor: + r""" + bucketize(input, boundaries, *, out_int32=False, right=False, out=None) -> Tensor + + Returns the indices of the buckets to which each value in the :attr:`input` belongs, where the + boundaries of the buckets are set by :attr:`boundaries`. Return a new tensor with the same size + as :attr:`input`. If :attr:`right` is False (default), then the left boundary is open. Note that + this behavior is opposite the behavior of + `numpy.digitize `_. + More formally, the returned index satisfies the following rules: + + .. list-table:: + :widths: 15 85 + :header-rows: 1 + + * - :attr:`right` + - *returned index satisfies* + * - False + - ``boundaries[i-1] < input[m][n]...[l][x] <= boundaries[i]`` + * - True + - ``boundaries[i-1] <= input[m][n]...[l][x] < boundaries[i]`` + + Args: + input (Tensor or Scalar): N-D tensor or a Scalar containing the search value(s). + boundaries (Tensor): 1-D tensor, must contain a strictly increasing sequence, or the return value is undefined. + + Keyword args: + out_int32 (bool, optional): indicate the output data type. torch.int32 if True, torch.int64 otherwise. + Default value is False, i.e. default output data type is torch.int64. + right (bool, optional): determines the behavior for values in :attr:`boundaries`. See the table above. + out (Tensor, optional): the output tensor, must be the same size as :attr:`input` if provided. + + + Example:: + + >>> boundaries = torch.tensor([1, 3, 5, 7, 9]) + >>> boundaries + tensor([1, 3, 5, 7, 9]) + >>> v = torch.tensor([[3, 6, 9], [3, 6, 9]]) + >>> v + tensor([[3, 6, 9], + [3, 6, 9]]) + >>> torch.bucketize(v, boundaries) + tensor([[1, 3, 4], + [1, 3, 4]]) + >>> torch.bucketize(v, boundaries, right=True) + tensor([[2, 3, 5], + [2, 3, 5]]) + """ + +def can_cast(from_: _dtype, to: _dtype) -> _bool: + r""" + can_cast(from_, to) -> bool + + Determines if a type conversion is allowed under PyTorch casting rules + described in the type promotion :ref:`documentation `. + + Args: + from\_ (dtype): The original :class:`torch.dtype`. + to (dtype): The target :class:`torch.dtype`. + + Example:: + + >>> torch.can_cast(torch.double, torch.float) + True + >>> torch.can_cast(torch.float, torch.int) + False + """ + +@overload +def cat( + tensors: tuple[Tensor, ...] | list[Tensor], + dim: _int = 0, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + cat(tensors, dim=0, *, out=None) -> Tensor + + Concatenates the given sequence of tensors in :attr:`tensors` in the given dimension. + All tensors must either have the same shape (except in the concatenating + dimension) or be a 1-D empty tensor with size ``(0,)``. + + :func:`torch.cat` can be seen as an inverse operation for :func:`torch.split` + and :func:`torch.chunk`. + + :func:`torch.cat` can be best understood via examples. + + .. seealso:: + + :func:`torch.stack` concatenates the given sequence along a new dimension. + + Args: + tensors (sequence of Tensors): Non-empty tensors provided must have the same shape, + except in the cat dimension. + + dim (int, optional): the dimension over which the tensors are concatenated + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> x = torch.randn(2, 3) + >>> x + tensor([[ 0.6580, -1.0969, -0.4614], + [-0.1034, -0.5790, 0.1497]]) + >>> torch.cat((x, x, x), 0) + tensor([[ 0.6580, -1.0969, -0.4614], + [-0.1034, -0.5790, 0.1497], + [ 0.6580, -1.0969, -0.4614], + [-0.1034, -0.5790, 0.1497], + [ 0.6580, -1.0969, -0.4614], + [-0.1034, -0.5790, 0.1497]]) + >>> torch.cat((x, x, x), 1) + tensor([[ 0.6580, -1.0969, -0.4614, 0.6580, -1.0969, -0.4614, 0.6580, + -1.0969, -0.4614], + [-0.1034, -0.5790, 0.1497, -0.1034, -0.5790, 0.1497, -0.1034, + -0.5790, 0.1497]]) + """ + +@overload +def cat( + tensors: tuple[Tensor, ...] | list[Tensor], + dim: str | EllipsisType | None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + cat(tensors, dim=0, *, out=None) -> Tensor + + Concatenates the given sequence of tensors in :attr:`tensors` in the given dimension. + All tensors must either have the same shape (except in the concatenating + dimension) or be a 1-D empty tensor with size ``(0,)``. + + :func:`torch.cat` can be seen as an inverse operation for :func:`torch.split` + and :func:`torch.chunk`. + + :func:`torch.cat` can be best understood via examples. + + .. seealso:: + + :func:`torch.stack` concatenates the given sequence along a new dimension. + + Args: + tensors (sequence of Tensors): Non-empty tensors provided must have the same shape, + except in the cat dimension. + + dim (int, optional): the dimension over which the tensors are concatenated + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> x = torch.randn(2, 3) + >>> x + tensor([[ 0.6580, -1.0969, -0.4614], + [-0.1034, -0.5790, 0.1497]]) + >>> torch.cat((x, x, x), 0) + tensor([[ 0.6580, -1.0969, -0.4614], + [-0.1034, -0.5790, 0.1497], + [ 0.6580, -1.0969, -0.4614], + [-0.1034, -0.5790, 0.1497], + [ 0.6580, -1.0969, -0.4614], + [-0.1034, -0.5790, 0.1497]]) + >>> torch.cat((x, x, x), 1) + tensor([[ 0.6580, -1.0969, -0.4614, 0.6580, -1.0969, -0.4614, 0.6580, + -1.0969, -0.4614], + [-0.1034, -0.5790, 0.1497, -0.1034, -0.5790, 0.1497, -0.1034, + -0.5790, 0.1497]]) + """ + +def ccol_indices_copy( + input: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: ... +def ceil(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + ceil(input, *, out=None) -> Tensor + + Returns a new tensor with the ceil of the elements of :attr:`input`, + the smallest integer greater than or equal to each element. + + For integer inputs, follows the array-api convention of returning a + copy of the input tensor. + + .. math:: + \text{out}_{i} = \left\lceil \text{input}_{i} \right\rceil + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-0.6341, -1.4208, -1.0900, 0.5826]) + >>> torch.ceil(a) + tensor([-0., -1., -1., 1.]) + """ + +def ceil_(input: Tensor) -> Tensor: ... +def celu(input: Tensor, alpha: Number | _complex = 1.0) -> Tensor: ... +def celu_(input: Tensor, alpha: Number | _complex = 1.0) -> Tensor: ... +def channel_shuffle(input: Tensor, groups: _int | SymInt) -> Tensor: ... +def cholesky( + input: Tensor, + upper: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + cholesky(input, upper=False, *, out=None) -> Tensor + + Computes the Cholesky decomposition of a symmetric positive-definite + matrix :math:`A` or for batches of symmetric positive-definite matrices. + + If :attr:`upper` is ``True``, the returned matrix ``U`` is upper-triangular, and + the decomposition has the form: + + .. math:: + + A = U^TU + + If :attr:`upper` is ``False``, the returned matrix ``L`` is lower-triangular, and + the decomposition has the form: + + .. math:: + + A = LL^T + + If :attr:`upper` is ``True``, and :math:`A` is a batch of symmetric positive-definite + matrices, then the returned tensor will be composed of upper-triangular Cholesky factors + of each of the individual matrices. Similarly, when :attr:`upper` is ``False``, the returned + tensor will be composed of lower-triangular Cholesky factors of each of the individual + matrices. + + .. warning:: + + :func:`torch.cholesky` is deprecated in favor of :func:`torch.linalg.cholesky` + and will be removed in a future PyTorch release. + + ``L = torch.cholesky(A)`` should be replaced with + + .. code:: python + + L = torch.linalg.cholesky(A) + + ``U = torch.cholesky(A, upper=True)`` should be replaced with + + .. code:: python + + U = torch.linalg.cholesky(A).mH + + This transform will produce equivalent results for all valid (symmetric positive definite) inputs. + + Args: + input (Tensor): the input tensor :math:`A` of size :math:`(*, n, n)` where `*` is zero or more + batch dimensions consisting of symmetric positive-definite matrices. + upper (bool, optional): flag that indicates whether to return a + upper or lower triangular matrix. Default: ``False`` + + Keyword args: + out (Tensor, optional): the output matrix + + Example:: + + >>> a = torch.randn(3, 3) + >>> a = a @ a.mT + 1e-3 # make symmetric positive-definite + >>> l = torch.cholesky(a) + >>> a + tensor([[ 2.4112, -0.7486, 1.4551], + [-0.7486, 1.3544, 0.1294], + [ 1.4551, 0.1294, 1.6724]]) + >>> l + tensor([[ 1.5528, 0.0000, 0.0000], + [-0.4821, 1.0592, 0.0000], + [ 0.9371, 0.5487, 0.7023]]) + >>> l @ l.mT + tensor([[ 2.4112, -0.7486, 1.4551], + [-0.7486, 1.3544, 0.1294], + [ 1.4551, 0.1294, 1.6724]]) + >>> a = torch.randn(3, 2, 2) # Example for batched input + >>> a = a @ a.mT + 1e-03 # make symmetric positive-definite + >>> l = torch.cholesky(a) + >>> z = l @ l.mT + >>> torch.dist(z, a) + tensor(2.3842e-07) + """ + +def cholesky_inverse( + input: Tensor, + upper: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + cholesky_inverse(L, upper=False, *, out=None) -> Tensor + + Computes the inverse of a complex Hermitian or real symmetric + positive-definite matrix given its Cholesky decomposition. + + Let :math:`A` be a complex Hermitian or real symmetric positive-definite matrix, + and :math:`L` its Cholesky decomposition such that: + + .. math:: + + A = LL^{\text{H}} + + where :math:`L^{\text{H}}` is the conjugate transpose when :math:`L` is complex, + and the transpose when :math:`L` is real-valued. + + Computes the inverse matrix :math:`A^{-1}`. + + Supports input of float, double, cfloat and cdouble dtypes. + Also supports batches of matrices, and if :math:`A` is a batch of matrices + then the output has the same batch dimensions. + + Args: + L (Tensor): tensor of shape `(*, n, n)` where `*` is zero or more batch dimensions + consisting of lower or upper triangular Cholesky decompositions of + symmetric or Hermitian positive-definite matrices. + upper (bool, optional): flag that indicates whether :math:`L` is lower triangular + or upper triangular. Default: ``False`` + + Keyword args: + out (Tensor, optional): output tensor. Ignored if `None`. Default: `None`. + + Example:: + + >>> A = torch.randn(3, 3) + >>> A = A @ A.T + torch.eye(3) * 1e-3 # Creates a symmetric positive-definite matrix + >>> L = torch.linalg.cholesky(A) # Extract Cholesky decomposition + >>> torch.cholesky_inverse(L) + tensor([[ 1.9314, 1.2251, -0.0889], + [ 1.2251, 2.4439, 0.2122], + [-0.0889, 0.2122, 0.1412]]) + >>> A.inverse() + tensor([[ 1.9314, 1.2251, -0.0889], + [ 1.2251, 2.4439, 0.2122], + [-0.0889, 0.2122, 0.1412]]) + + >>> A = torch.randn(3, 2, 2, dtype=torch.complex64) + >>> A = A @ A.mH + torch.eye(2) * 1e-3 # Batch of Hermitian positive-definite matrices + >>> L = torch.linalg.cholesky(A) + >>> torch.dist(torch.inverse(A), torch.cholesky_inverse(L)) + tensor(5.6358e-7) + """ + +def cholesky_solve( + input: Tensor, + input2: Tensor, + upper: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + cholesky_solve(B, L, upper=False, *, out=None) -> Tensor + + Computes the solution of a system of linear equations with complex Hermitian + or real symmetric positive-definite lhs given its Cholesky decomposition. + + Let :math:`A` be a complex Hermitian or real symmetric positive-definite matrix, + and :math:`L` its Cholesky decomposition such that: + + .. math:: + + A = LL^{\text{H}} + + where :math:`L^{\text{H}}` is the conjugate transpose when :math:`L` is complex, + and the transpose when :math:`L` is real-valued. + + Returns the solution :math:`X` of the following linear system: + + .. math:: + + AX = B + + Supports inputs of float, double, cfloat and cdouble dtypes. + Also supports batches of matrices, and if :math:`A` or :math:`B` is a batch of matrices + then the output has the same batch dimensions. + + Args: + B (Tensor): right-hand side tensor of shape `(*, n, k)` + where :math:`*` is zero or more batch dimensions + L (Tensor): tensor of shape `(*, n, n)` where `*` is zero or more batch dimensions + consisting of lower or upper triangular Cholesky decompositions of + symmetric or Hermitian positive-definite matrices. + upper (bool, optional): flag that indicates whether :math:`L` is lower triangular + or upper triangular. Default: ``False``. + + Keyword args: + out (Tensor, optional): output tensor. Ignored if `None`. Default: `None`. + + Example:: + + >>> A = torch.randn(3, 3) + >>> A = A @ A.T + torch.eye(3) * 1e-3 # Creates a symmetric positive-definite matrix + >>> L = torch.linalg.cholesky(A) # Extract Cholesky decomposition + >>> B = torch.randn(3, 2) + >>> torch.cholesky_solve(B, L) + tensor([[ -8.1625, 19.6097], + [ -5.8398, 14.2387], + [ -4.3771, 10.4173]]) + >>> A.inverse() @ B + tensor([[ -8.1626, 19.6097], + [ -5.8398, 14.2387], + [ -4.3771, 10.4173]]) + + >>> A = torch.randn(3, 2, 2, dtype=torch.complex64) + >>> A = A @ A.mH + torch.eye(2) * 1e-3 # Batch of Hermitian positive-definite matrices + >>> L = torch.linalg.cholesky(A) + >>> B = torch.randn(2, 1, dtype=torch.complex64) + >>> X = torch.cholesky_solve(B, L) + >>> torch.dist(X, A.inverse() @ B) + tensor(1.6881e-5) + """ + +def choose_qparams_optimized( + input: Tensor, + numel: _int, + n_bins: _int, + ratio: _float, + bit_width: _int, +) -> tuple[Tensor, Tensor]: ... +def chunk(input: Tensor, chunks: _int, dim: _int = 0) -> tuple[Tensor, ...]: + r""" + chunk(input: Tensor, chunks: int, dim: int = 0) -> Tuple[Tensor, ...] + + Attempts to split a tensor into the specified number of chunks. Each chunk is a view of + the input tensor. + + + .. note:: + + This function may return fewer than the specified number of chunks! + + .. seealso:: + + :func:`torch.tensor_split` a function that always returns exactly the specified number of chunks + + If the tensor size along the given dimension :attr:`dim` is divisible by :attr:`chunks`, + all returned chunks will be the same size. + If the tensor size along the given dimension :attr:`dim` is not divisible by :attr:`chunks`, + all returned chunks will be the same size, except the last one. + If such division is not possible, this function may return fewer + than the specified number of chunks. + + Arguments: + input (Tensor): the tensor to split + chunks (int): number of chunks to return + dim (int): dimension along which to split the tensor + + Example: + >>> torch.arange(11).chunk(6) + (tensor([0, 1]), + tensor([2, 3]), + tensor([4, 5]), + tensor([6, 7]), + tensor([8, 9]), + tensor([10])) + >>> torch.arange(12).chunk(6) + (tensor([0, 1]), + tensor([2, 3]), + tensor([4, 5]), + tensor([6, 7]), + tensor([8, 9]), + tensor([10, 11])) + >>> torch.arange(13).chunk(6) + (tensor([0, 1, 2]), + tensor([3, 4, 5]), + tensor([6, 7, 8]), + tensor([ 9, 10, 11]), + tensor([12])) + """ + +@overload +def clamp( + input: Tensor, + min: Tensor | None = None, + max: Tensor | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + clamp(input, min=None, max=None, *, out=None) -> Tensor + + Clamps all elements in :attr:`input` into the range `[` :attr:`min`, :attr:`max` `]`. + Letting min_value and max_value be :attr:`min` and :attr:`max`, respectively, this returns: + + .. math:: + y_i = \min(\max(x_i, \text{min\_value}_i), \text{max\_value}_i) + + If :attr:`min` is ``None``, there is no lower bound. + Or, if :attr:`max` is ``None`` there is no upper bound. + + + .. note:: + If :attr:`min` is greater than :attr:`max` :func:`torch.clamp(..., min, max) ` + sets all elements in :attr:`input` to the value of :attr:`max`. + + Args: + input (Tensor): the input tensor. + min (Number or Tensor, optional): lower-bound of the range to be clamped to + max (Number or Tensor, optional): upper-bound of the range to be clamped to + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-1.7120, 0.1734, -0.0478, -0.0922]) + >>> torch.clamp(a, min=-0.5, max=0.5) + tensor([-0.5000, 0.1734, -0.0478, -0.0922]) + + >>> min = torch.linspace(-1, 1, steps=4) + >>> torch.clamp(a, min=min) + tensor([-1.0000, 0.1734, 0.3333, 1.0000]) + """ + +@overload +def clamp( + input: Tensor, + min: Number | _complex | None = None, + max: Number | _complex | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + clamp(input, min=None, max=None, *, out=None) -> Tensor + + Clamps all elements in :attr:`input` into the range `[` :attr:`min`, :attr:`max` `]`. + Letting min_value and max_value be :attr:`min` and :attr:`max`, respectively, this returns: + + .. math:: + y_i = \min(\max(x_i, \text{min\_value}_i), \text{max\_value}_i) + + If :attr:`min` is ``None``, there is no lower bound. + Or, if :attr:`max` is ``None`` there is no upper bound. + + + .. note:: + If :attr:`min` is greater than :attr:`max` :func:`torch.clamp(..., min, max) ` + sets all elements in :attr:`input` to the value of :attr:`max`. + + Args: + input (Tensor): the input tensor. + min (Number or Tensor, optional): lower-bound of the range to be clamped to + max (Number or Tensor, optional): upper-bound of the range to be clamped to + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-1.7120, 0.1734, -0.0478, -0.0922]) + >>> torch.clamp(a, min=-0.5, max=0.5) + tensor([-0.5000, 0.1734, -0.0478, -0.0922]) + + >>> min = torch.linspace(-1, 1, steps=4) + >>> torch.clamp(a, min=min) + tensor([-1.0000, 0.1734, 0.3333, 1.0000]) + """ + +@overload +def clamp_( + input: Tensor, + min: Tensor | None = None, + max: Tensor | None = None, +) -> Tensor: ... +@overload +def clamp_( + input: Tensor, + min: Number | _complex | None = None, + max: Number | _complex | None = None, +) -> Tensor: ... +@overload +def clamp_max( + input: Tensor, + max: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: ... +@overload +def clamp_max( + input: Tensor, + max: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: ... +@overload +def clamp_max_(input: Tensor, max: Tensor) -> Tensor: ... +@overload +def clamp_max_(input: Tensor, max: Number | _complex) -> Tensor: ... +@overload +def clamp_min( + input: Tensor, + min: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: ... +@overload +def clamp_min( + input: Tensor, + min: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: ... +@overload +def clamp_min_(input: Tensor, min: Tensor) -> Tensor: ... +@overload +def clamp_min_(input: Tensor, min: Number | _complex) -> Tensor: ... +@overload +def clip( + input: Tensor, + min: Tensor | None = None, + max: Tensor | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + clip(input, min=None, max=None, *, out=None) -> Tensor + + Alias for :func:`torch.clamp`. + """ + +@overload +def clip( + input: Tensor, + min: Number | _complex | None = None, + max: Number | _complex | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + clip(input, min=None, max=None, *, out=None) -> Tensor + + Alias for :func:`torch.clamp`. + """ + +@overload +def clip_( + input: Tensor, + min: Tensor | None = None, + max: Tensor | None = None, +) -> Tensor: ... +@overload +def clip_( + input: Tensor, + min: Number | _complex | None = None, + max: Number | _complex | None = None, +) -> Tensor: ... +def clone( + input: Tensor, + *, + memory_format: memory_format | None = None, +) -> Tensor: + r""" + clone(input, *, memory_format=torch.preserve_format) -> Tensor + + Returns a copy of :attr:`input`. + + .. note:: + + This function is differentiable, so gradients will flow back from the + result of this operation to :attr:`input`. To create a tensor without an + autograd relationship to :attr:`input` see :meth:`~Tensor.detach`. + + In addition, when ``torch.preserve_format`` is used: + If the input tensor is dense (i.e., non-overlapping strided), + its memory format (including strides) is retained. + Otherwise (e.g., a non-dense view like a stepped slice), + the output is converted to the dense (contiguous) format. + + Args: + input (Tensor): the input tensor. + + Keyword args: + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned tensor. Default: ``torch.preserve_format``. + """ + +def col_indices_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + Performs the same operation as :func:`torch.col_indices`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def column_stack( + tensors: tuple[Tensor, ...] | list[Tensor], + *, + out: Tensor | None = None, +) -> Tensor: + r""" + column_stack(tensors, *, out=None) -> Tensor + + Creates a new tensor by horizontally stacking the tensors in :attr:`tensors`. + + Equivalent to ``torch.hstack(tensors)``, except each zero or one dimensional tensor ``t`` + in :attr:`tensors` is first reshaped into a ``(t.numel(), 1)`` column before being stacked horizontally. + + Args: + tensors (sequence of Tensors): sequence of tensors to concatenate + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([1, 2, 3]) + >>> b = torch.tensor([4, 5, 6]) + >>> torch.column_stack((a, b)) + tensor([[1, 4], + [2, 5], + [3, 6]]) + >>> a = torch.arange(5) + >>> b = torch.arange(10).reshape(5, 2) + >>> torch.column_stack((a, b, b)) + tensor([[0, 0, 1, 0, 1], + [1, 2, 3, 2, 3], + [2, 4, 5, 4, 5], + [3, 6, 7, 6, 7], + [4, 8, 9, 8, 9]]) + """ + +def combinations( + input: Tensor, + r: _int = 2, + with_replacement: _bool = False, +) -> Tensor: + r""" + combinations(input: Tensor, r: int = 2, with_replacement: bool = False) -> seq + + Compute combinations of length :math:`r` of the given tensor. The behavior is similar to + python's `itertools.combinations` when `with_replacement` is set to `False`, and + `itertools.combinations_with_replacement` when `with_replacement` is set to `True`. + + Arguments: + input (Tensor): 1D vector. + r (int, optional): number of elements to combine + with_replacement (bool, optional): whether to allow duplication in combination + + Returns: + Tensor: A tensor equivalent to converting all the input tensors into lists, do + `itertools.combinations` or `itertools.combinations_with_replacement` on these + lists, and finally convert the resulting list into tensor. + + Example:: + + >>> a = [1, 2, 3] + >>> list(itertools.combinations(a, r=2)) + [(1, 2), (1, 3), (2, 3)] + >>> list(itertools.combinations(a, r=3)) + [(1, 2, 3)] + >>> list(itertools.combinations_with_replacement(a, r=2)) + [(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)] + >>> tensor_a = torch.tensor(a) + >>> torch.combinations(tensor_a) + tensor([[1, 2], + [1, 3], + [2, 3]]) + >>> torch.combinations(tensor_a, r=3) + tensor([[1, 2, 3]]) + >>> torch.combinations(tensor_a, with_replacement=True) + tensor([[1, 1], + [1, 2], + [1, 3], + [2, 2], + [2, 3], + [3, 3]]) + """ + +def complex( + real: Tensor, + imag: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + complex(real, imag, *, out=None) -> Tensor + + Constructs a complex tensor with its real part equal to :attr:`real` and its + imaginary part equal to :attr:`imag`. + + Args: + real (Tensor): The real part of the complex tensor. Must be half, float or double. + imag (Tensor): The imaginary part of the complex tensor. Must be same dtype + as :attr:`real`. + + Keyword args: + out (Tensor): If the inputs are ``torch.float32``, must be + ``torch.complex64``. If the inputs are ``torch.float64``, must be + ``torch.complex128``. + + Example:: + + >>> real = torch.tensor([1, 2], dtype=torch.float32) + >>> imag = torch.tensor([3, 4], dtype=torch.float32) + >>> z = torch.complex(real, imag) + >>> z + tensor([(1.+3.j), (2.+4.j)]) + >>> z.dtype + torch.complex64 + """ + +@overload +def concat( + tensors: tuple[Tensor, ...] | list[Tensor], + dim: _int = 0, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + concat(tensors, dim=0, *, out=None) -> Tensor + + Alias of :func:`torch.cat`. + """ + +@overload +def concat( + tensors: tuple[Tensor, ...] | list[Tensor], + dim: str | EllipsisType | None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + concat(tensors, dim=0, *, out=None) -> Tensor + + Alias of :func:`torch.cat`. + """ + +@overload +def concatenate( + tensors: tuple[Tensor, ...] | list[Tensor], + dim: _int = 0, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + concatenate(tensors, axis=0, out=None) -> Tensor + + Alias of :func:`torch.cat`. + """ + +@overload +def concatenate( + tensors: tuple[Tensor, ...] | list[Tensor], + dim: str | EllipsisType | None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + concatenate(tensors, axis=0, out=None) -> Tensor + + Alias of :func:`torch.cat`. + """ + +def conj(input: Tensor) -> Tensor: + r""" + conj(input) -> Tensor + + Returns a view of :attr:`input` with a flipped conjugate bit. If :attr:`input` has a non-complex dtype, + this function just returns :attr:`input`. + + .. note:: + :func:`torch.conj` performs a lazy conjugation, but the actual conjugated tensor can be materialized + at any time using :func:`torch.resolve_conj`. + + .. warning:: In the future, :func:`torch.conj` may return a non-writeable view for an :attr:`input` of + non-complex dtype. It's recommended that programs not modify the tensor returned by :func:`torch.conj_physical` + when :attr:`input` is of non-complex dtype to be compatible with this change. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> x = torch.tensor([-1 + 1j, -2 + 2j, 3 - 3j]) + >>> x.is_conj() + False + >>> y = torch.conj(x) + >>> y.is_conj() + True + """ + +def conj_physical(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + conj_physical(input, *, out=None) -> Tensor + + Computes the element-wise conjugate of the given :attr:`input` tensor. + If :attr:`input` has a non-complex dtype, this function just returns :attr:`input`. + + .. note:: + This performs the conjugate operation regardless of the fact conjugate bit is set or not. + + .. warning:: In the future, :func:`torch.conj_physical` may return a non-writeable view for an :attr:`input` of + non-complex dtype. It's recommended that programs not modify the tensor returned by :func:`torch.conj_physical` + when :attr:`input` is of non-complex dtype to be compatible with this change. + + .. math:: + \text{out}_{i} = conj(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.conj_physical(torch.tensor([-1 + 1j, -2 + 2j, 3 - 3j])) + tensor([-1 - 1j, -2 - 2j, 3 + 3j]) + """ + +def conj_physical_(input: Tensor) -> Tensor: ... +def constant_pad_nd( + input: Tensor, + pad: Sequence[_int | SymInt], + value: Number | _complex = 0, +) -> Tensor: ... +@overload +def conv1d( + input: Tensor, + weight: Tensor, + bias: Tensor | None = None, + stride: _int | SymInt | Sequence[_int | SymInt] = 1, + padding: _int | SymInt | Sequence[_int | SymInt] = 0, + dilation: _int | SymInt | Sequence[_int | SymInt] = 1, + groups: _int | SymInt = 1, +) -> Tensor: ... +@overload +def conv1d( + input: Tensor, + weight: Tensor, + bias: Tensor | None = None, + stride: _int | SymInt | Sequence[_int | SymInt] = 1, + padding: str = "valid", + dilation: _int | SymInt | Sequence[_int | SymInt] = 1, + groups: _int | SymInt = 1, +) -> Tensor: ... +@overload +def conv2d( + input: Tensor, + weight: Tensor, + bias: Tensor | None = None, + stride: _int | SymInt | Sequence[_int | SymInt] = 1, + padding: _int | SymInt | Sequence[_int | SymInt] = 0, + dilation: _int | SymInt | Sequence[_int | SymInt] = 1, + groups: _int | SymInt = 1, +) -> Tensor: ... +@overload +def conv2d( + input: Tensor, + weight: Tensor, + bias: Tensor | None = None, + stride: _int | SymInt | Sequence[_int | SymInt] = 1, + padding: str = "valid", + dilation: _int | SymInt | Sequence[_int | SymInt] = 1, + groups: _int | SymInt = 1, +) -> Tensor: ... +@overload +def conv3d( + input: Tensor, + weight: Tensor, + bias: Tensor | None = None, + stride: _int | SymInt | Sequence[_int | SymInt] = 1, + padding: _int | SymInt | Sequence[_int | SymInt] = 0, + dilation: _int | SymInt | Sequence[_int | SymInt] = 1, + groups: _int | SymInt = 1, +) -> Tensor: ... +@overload +def conv3d( + input: Tensor, + weight: Tensor, + bias: Tensor | None = None, + stride: _int | SymInt | Sequence[_int | SymInt] = 1, + padding: str = "valid", + dilation: _int | SymInt | Sequence[_int | SymInt] = 1, + groups: _int | SymInt = 1, +) -> Tensor: ... +def conv_tbc( + input: Tensor, + weight: Tensor, + bias: Tensor, + pad: _int = 0, +) -> Tensor: ... +def conv_transpose1d( + input: Tensor, + weight: Tensor, + bias: Tensor | None = None, + stride: _int | SymInt | Sequence[_int | SymInt] = 1, + padding: _int | SymInt | Sequence[_int | SymInt] = 0, + output_padding: _int | SymInt | Sequence[_int | SymInt] = 0, + groups: _int | SymInt = 1, + dilation: _int | SymInt | Sequence[_int | SymInt] = 1, +) -> Tensor: ... +def conv_transpose2d( + input: Tensor, + weight: Tensor, + bias: Tensor | None = None, + stride: _int | SymInt | Sequence[_int | SymInt] = 1, + padding: _int | SymInt | Sequence[_int | SymInt] = 0, + output_padding: _int | SymInt | Sequence[_int | SymInt] = 0, + groups: _int | SymInt = 1, + dilation: _int | SymInt | Sequence[_int | SymInt] = 1, +) -> Tensor: ... +def conv_transpose3d( + input: Tensor, + weight: Tensor, + bias: Tensor | None = None, + stride: _int | SymInt | Sequence[_int | SymInt] = 1, + padding: _int | SymInt | Sequence[_int | SymInt] = 0, + output_padding: _int | SymInt | Sequence[_int | SymInt] = 0, + groups: _int | SymInt = 1, + dilation: _int | SymInt | Sequence[_int | SymInt] = 1, +) -> Tensor: ... +def convolution( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + stride: Sequence[_int | SymInt], + padding: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + transposed: _bool, + output_padding: Sequence[_int | SymInt], + groups: _int | SymInt, +) -> Tensor: ... +@overload +def copysign( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + copysign(input, other, *, out=None) -> Tensor + + Create a new floating-point tensor with the magnitude of :attr:`input` and the sign of :attr:`other`, elementwise. + + .. math:: + \text{out}_{i} = \begin{cases} + -|\text{input}_{i}| & \text{if } \text{other}_{i} \leq -0.0 \\ + |\text{input}_{i}| & \text{if } \text{other}_{i} \geq 0.0 \\ + \end{cases} + + + Supports :ref:`broadcasting to a common shape `, + and integer and float inputs. + + Args: + input (Tensor): magnitudes. + other (Tensor or Number): contains value(s) whose signbit(s) are + applied to the magnitudes in :attr:`input`. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(5) + >>> a + tensor([-1.2557, -0.0026, -0.5387, 0.4740, -0.9244]) + >>> torch.copysign(a, 1) + tensor([1.2557, 0.0026, 0.5387, 0.4740, 0.9244]) + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.7079, 0.2778, -1.0249, 0.5719], + [-0.0059, -0.2600, -0.4475, -1.3948], + [ 0.3667, -0.9567, -2.5757, -0.1751], + [ 0.2046, -0.0742, 0.2998, -0.1054]]) + >>> b = torch.randn(4) + tensor([ 0.2373, 0.3120, 0.3190, -1.1128]) + >>> torch.copysign(a, b) + tensor([[ 0.7079, 0.2778, 1.0249, -0.5719], + [ 0.0059, 0.2600, 0.4475, -1.3948], + [ 0.3667, 0.9567, 2.5757, -0.1751], + [ 0.2046, 0.0742, 0.2998, -0.1054]]) + >>> a = torch.tensor([1.]) + >>> b = torch.tensor([-0.]) + >>> torch.copysign(a, b) + tensor([-1.]) + + .. note:: + copysign handles signed zeros. If the other argument has a negative zero (-0), + the corresponding output value will be negative. + """ + +@overload +def copysign( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + copysign(input, other, *, out=None) -> Tensor + + Create a new floating-point tensor with the magnitude of :attr:`input` and the sign of :attr:`other`, elementwise. + + .. math:: + \text{out}_{i} = \begin{cases} + -|\text{input}_{i}| & \text{if } \text{other}_{i} \leq -0.0 \\ + |\text{input}_{i}| & \text{if } \text{other}_{i} \geq 0.0 \\ + \end{cases} + + + Supports :ref:`broadcasting to a common shape `, + and integer and float inputs. + + Args: + input (Tensor): magnitudes. + other (Tensor or Number): contains value(s) whose signbit(s) are + applied to the magnitudes in :attr:`input`. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(5) + >>> a + tensor([-1.2557, -0.0026, -0.5387, 0.4740, -0.9244]) + >>> torch.copysign(a, 1) + tensor([1.2557, 0.0026, 0.5387, 0.4740, 0.9244]) + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.7079, 0.2778, -1.0249, 0.5719], + [-0.0059, -0.2600, -0.4475, -1.3948], + [ 0.3667, -0.9567, -2.5757, -0.1751], + [ 0.2046, -0.0742, 0.2998, -0.1054]]) + >>> b = torch.randn(4) + tensor([ 0.2373, 0.3120, 0.3190, -1.1128]) + >>> torch.copysign(a, b) + tensor([[ 0.7079, 0.2778, 1.0249, -0.5719], + [ 0.0059, 0.2600, 0.4475, -1.3948], + [ 0.3667, 0.9567, 2.5757, -0.1751], + [ 0.2046, 0.0742, 0.2998, -0.1054]]) + >>> a = torch.tensor([1.]) + >>> b = torch.tensor([-0.]) + >>> torch.copysign(a, b) + tensor([-1.]) + + .. note:: + copysign handles signed zeros. If the other argument has a negative zero (-0), + the corresponding output value will be negative. + """ + +def corrcoef(input: Tensor) -> Tensor: + r""" + corrcoef(input) -> Tensor + + Estimates the Pearson product-moment correlation coefficient matrix of the variables given by the :attr:`input` matrix, + where rows are the variables and columns are the observations. + + .. note:: + + The correlation coefficient matrix R is computed using the covariance matrix C as given by + :math:`R_{ij} = \frac{ C_{ij} } { \sqrt{ C_{ii} * C_{jj} } }` + + .. note:: + + Due to floating point rounding, the resulting array may not be Hermitian and its diagonal elements may not be 1. + The real and imaginary values are clipped to the interval [-1, 1] in an attempt to improve this situation. + + Args: + input (Tensor): A 2D matrix containing multiple variables and observations, or a + Scalar or 1D vector representing a single variable. + + Returns: + (Tensor) The correlation coefficient matrix of the variables. + + .. seealso:: + + :func:`torch.cov` covariance matrix. + + Example:: + + >>> x = torch.tensor([[0, 1, 2], [2, 1, 0]]) + >>> torch.corrcoef(x) + tensor([[ 1., -1.], + [-1., 1.]]) + >>> x = torch.randn(2, 4) + >>> x + tensor([[-0.2678, -0.0908, -0.3766, 0.2780], + [-0.5812, 0.1535, 0.2387, 0.2350]]) + >>> torch.corrcoef(x) + tensor([[1.0000, 0.3582], + [0.3582, 1.0000]]) + >>> torch.corrcoef(x[0]) + tensor(1.) + """ + +def cos(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + cos(input, *, out=None) -> Tensor + + Returns a new tensor with the cosine of the elements of :attr:`input` given in radians. + + .. math:: + \text{out}_{i} = \cos(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 1.4309, 1.2706, -0.8562, 0.9796]) + >>> torch.cos(a) + tensor([ 0.1395, 0.2957, 0.6553, 0.5574]) + """ + +def cos_(input: Tensor) -> Tensor: ... +def cosh(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + cosh(input, *, out=None) -> Tensor + + Returns a new tensor with the hyperbolic cosine of the elements of + :attr:`input`. + + .. math:: + \text{out}_{i} = \cosh(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.1632, 1.1835, -0.6979, -0.7325]) + >>> torch.cosh(a) + tensor([ 1.0133, 1.7860, 1.2536, 1.2805]) + + .. note:: + When :attr:`input` is on the CPU, the implementation of torch.cosh may use + the Sleef library, which rounds very large results to infinity or negative + infinity. See `here `_ for details. + """ + +def cosh_(input: Tensor) -> Tensor: ... +def cosine_embedding_loss( + input1: Tensor, + input2: Tensor, + target: Tensor, + margin: _float = 0.0, + reduction: _int = 1, +) -> Tensor: ... +def cosine_similarity( + x1: Tensor, + x2: Tensor, + dim: _int = 1, + eps: _float = 1e-08, +) -> Tensor: ... +@overload +def count_nonzero(input: Tensor, dim: _int | None = None) -> Tensor: + r""" + count_nonzero(input, dim=None) -> Tensor + + Counts the number of non-zero values in the tensor :attr:`input` along the given :attr:`dim`. + If no dim is specified then all non-zeros in the tensor are counted. + + Args: + input (Tensor): the input tensor. + dim (int or tuple of ints, optional): Dim or tuple of dims along which to count non-zeros. + + Example:: + + >>> x = torch.zeros(3,3) + >>> x[torch.randn(3,3) > 0.5] = 1 + >>> x + tensor([[0., 1., 1.], + [0., 0., 0.], + [0., 0., 1.]]) + >>> torch.count_nonzero(x) + tensor(3) + >>> torch.count_nonzero(x, dim=0) + tensor([0, 1, 2]) + """ + +@overload +def count_nonzero(input: Tensor, dim: _size) -> Tensor: + r""" + count_nonzero(input, dim=None) -> Tensor + + Counts the number of non-zero values in the tensor :attr:`input` along the given :attr:`dim`. + If no dim is specified then all non-zeros in the tensor are counted. + + Args: + input (Tensor): the input tensor. + dim (int or tuple of ints, optional): Dim or tuple of dims along which to count non-zeros. + + Example:: + + >>> x = torch.zeros(3,3) + >>> x[torch.randn(3,3) > 0.5] = 1 + >>> x + tensor([[0., 1., 1.], + [0., 0., 0.], + [0., 0., 1.]]) + >>> torch.count_nonzero(x) + tensor(3) + >>> torch.count_nonzero(x, dim=0) + tensor([0, 1, 2]) + """ + +def cov( + input: Tensor, + *, + correction: _int = 1, + fweights: Tensor | None = None, + aweights: Tensor | None = None, +) -> Tensor: + r""" + cov(input, *, correction=1, fweights=None, aweights=None) -> Tensor + + Estimates the covariance matrix of the variables given by the :attr:`input` matrix, where rows are + the variables and columns are the observations. + + A covariance matrix is a square matrix giving the covariance of each pair of variables. The diagonal contains + the variance of each variable (covariance of a variable with itself). By definition, if :attr:`input` represents + a single variable (Scalar or 1D) then its variance is returned. + + The sample covariance of the variables :math:`x` and :math:`y` is given by: + + .. math:: + \text{cov}(x,y) = \frac{\sum^{N}_{i = 1}(x_{i} - \bar{x})(y_{i} - \bar{y})}{\max(0,~N~-~\delta N)} + + where :math:`\bar{x}` and :math:`\bar{y}` are the simple means of the :math:`x` and :math:`y` respectively, and + :math:`\delta N` is the :attr:`correction`. + + If :attr:`fweights` and/or :attr:`aweights` are provided, the weighted covariance + is calculated, which is given by: + + .. math:: + \text{cov}_w(x,y) = \frac{\sum^{N}_{i = 1}w_i(x_{i} - \mu_x^*)(y_{i} - \mu_y^*)} + {\max(0,~\sum^{N}_{i = 1}w_i~-~\frac{\sum^{N}_{i = 1}w_ia_i}{\sum^{N}_{i = 1}w_i}~\delta N)} + + where :math:`w` denotes :attr:`fweights` or :attr:`aweights` (``f`` and ``a`` for brevity) based on whichever is + provided, or :math:`w = f \times a` if both are provided, and + :math:`\mu_x^* = \frac{\sum^{N}_{i = 1}w_ix_{i} }{\sum^{N}_{i = 1}w_i}` is the weighted mean of the variable. If not + provided, ``f`` and/or ``a`` can be seen as a :math:`\mathbb{1}` vector of appropriate size. + + Args: + input (Tensor): A 2D matrix containing multiple variables and observations, or a + Scalar or 1D vector representing a single variable. + + Keyword Args: + correction (int, optional): difference between the sample size and sample degrees of freedom. + Defaults to Bessel's correction, ``correction = 1`` which returns the unbiased estimate, + even if both :attr:`fweights` and :attr:`aweights` are specified. ``correction = 0`` + will return the simple average. Defaults to ``1``. + fweights (tensor, optional): A Scalar or 1D tensor of observation vector frequencies representing the number of + times each observation should be repeated. Its numel must equal the number of columns of :attr:`input`. + Must have integral dtype. Ignored if ``None``. Defaults to ``None``. + aweights (tensor, optional): A Scalar or 1D array of observation vector weights. + These relative weights are typically large for observations considered "important" and smaller for + observations considered less "important". Its numel must equal the number of columns of :attr:`input`. + Must have floating point dtype. Ignored if ``None``. Defaults to ``None``. + + Returns: + (Tensor) The covariance matrix of the variables. + + .. seealso:: + + :func:`torch.corrcoef` normalized covariance matrix. + + Example:: + + >>> x = torch.tensor([[0, 2], [1, 1], [2, 0]]).T + >>> x + tensor([[0, 1, 2], + [2, 1, 0]]) + >>> torch.cov(x) + tensor([[ 1., -1.], + [-1., 1.]]) + >>> torch.cov(x, correction=0) + tensor([[ 0.6667, -0.6667], + [-0.6667, 0.6667]]) + >>> fw = torch.randint(1, 10, (3,)) + >>> fw + tensor([1, 6, 9]) + >>> aw = torch.rand(3) + >>> aw + tensor([0.4282, 0.0255, 0.4144]) + >>> torch.cov(x, fweights=fw, aweights=aw) + tensor([[ 0.4169, -0.4169], + [-0.4169, 0.4169]]) + """ + +def cross( + input: Tensor, + other: Tensor, + dim: _int | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + cross(input, other, dim=None, *, out=None) -> Tensor + + + Returns the cross product of vectors in dimension :attr:`dim` of :attr:`input` + and :attr:`other`. + + Supports input of float, double, cfloat and cdouble dtypes. Also supports batches + of vectors, for which it computes the product along the dimension :attr:`dim`. + In this case, the output has the same batch dimensions as the inputs. + + .. warning:: + If :attr:`dim` is not given, it defaults to the first dimension found + with the size 3. Note that this might be unexpected. + + This behavior is deprecated and will be changed to match that of :func:`torch.linalg.cross` + in a future release. + + .. seealso:: + :func:`torch.linalg.cross` which has dim=-1 as default. + + + Args: + input (Tensor): the input tensor. + other (Tensor): the second input tensor + dim (int, optional): the dimension to take the cross-product in. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4, 3) + >>> a + tensor([[-0.3956, 1.1455, 1.6895], + [-0.5849, 1.3672, 0.3599], + [-1.1626, 0.7180, -0.0521], + [-0.1339, 0.9902, -2.0225]]) + >>> b = torch.randn(4, 3) + >>> b + tensor([[-0.0257, -1.4725, -1.2251], + [-1.1479, -0.7005, -1.9757], + [-1.3904, 0.3726, -1.1836], + [-0.9688, -0.7153, 0.2159]]) + >>> torch.cross(a, b, dim=1) + tensor([[ 1.0844, -0.5281, 0.6120], + [-2.4490, -1.5687, 1.9792], + [-0.8304, -1.3037, 0.5650], + [-1.2329, 1.9883, 1.0551]]) + >>> torch.cross(a, b) + tensor([[ 1.0844, -0.5281, 0.6120], + [-2.4490, -1.5687, 1.9792], + [-0.8304, -1.3037, 0.5650], + [-1.2329, 1.9883, 1.0551]]) + """ + +def crow_indices_copy( + input: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.crow_indices`, but all output tensors + are freshly created instead of aliasing the input. + """ + +@overload +def ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: _size, + target_lengths: _size, + blank: _int = 0, + reduction: _int = 1, + zero_infinity: _bool = False, +) -> Tensor: ... +@overload +def ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: Tensor, + target_lengths: Tensor, + blank: _int = 0, + reduction: _int = 1, + zero_infinity: _bool = False, +) -> Tensor: ... +def cudnn_affine_grid_generator( + theta: Tensor, + N: _int, + C: _int, + H: _int, + W: _int, +) -> Tensor: ... +def cudnn_batch_norm( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + running_mean: Tensor | None, + running_var: Tensor | None, + training: _bool, + exponential_average_factor: _float, + epsilon: _float, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> tuple[Tensor, Tensor, Tensor, Tensor]: ... +def cudnn_convolution( + input: Tensor, + weight: Tensor, + padding: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, + benchmark: _bool, + deterministic: _bool, + allow_tf32: _bool, + *, + out: Tensor | None = None, +) -> Tensor: ... +def cudnn_convolution_add_relu( + input: Tensor, + weight: Tensor, + z: Tensor, + alpha: Number | _complex | None, + bias: Tensor | None, + stride: Sequence[_int | SymInt], + padding: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, +) -> Tensor: ... +def cudnn_convolution_relu( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + stride: Sequence[_int | SymInt], + padding: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, +) -> Tensor: ... +def cudnn_convolution_transpose( + input: Tensor, + weight: Tensor, + padding: Sequence[_int | SymInt], + output_padding: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, + benchmark: _bool, + deterministic: _bool, + allow_tf32: _bool, +) -> Tensor: ... +def cudnn_grid_sampler(input: Tensor, grid: Tensor) -> Tensor: ... +def cudnn_is_acceptable(input: Tensor) -> _bool: ... +@overload +def cummax( + input: Tensor, + dim: _int, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.cummax: + r""" + cummax(input, dim, *, out=None) -> (Tensor, LongTensor) + Returns a namedtuple ``(values, indices)`` where ``values`` is the cumulative maximum of + elements of :attr:`input` in the dimension :attr:`dim`. And ``indices`` is the index + location of each maximum value found in the dimension :attr:`dim`. + + .. math:: + y_i = max(x_1, x_2, x_3, \dots, x_i) + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to do the operation over + + Keyword args: + out (tuple, optional): the result tuple of two output tensors (values, indices) + + Example:: + + >>> a = torch.randn(10) + >>> a + tensor([-0.3449, -1.5447, 0.0685, -1.5104, -1.1706, 0.2259, 1.4696, -1.3284, + 1.9946, -0.8209]) + >>> torch.cummax(a, dim=0) + torch.return_types.cummax( + values=tensor([-0.3449, -0.3449, 0.0685, 0.0685, 0.0685, 0.2259, 1.4696, 1.4696, + 1.9946, 1.9946]), + indices=tensor([0, 0, 2, 2, 2, 5, 6, 6, 8, 8])) + """ + +@overload +def cummax( + input: Tensor, + dim: str | EllipsisType | None, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.cummax: + r""" + cummax(input, dim, *, out=None) -> (Tensor, LongTensor) + Returns a namedtuple ``(values, indices)`` where ``values`` is the cumulative maximum of + elements of :attr:`input` in the dimension :attr:`dim`. And ``indices`` is the index + location of each maximum value found in the dimension :attr:`dim`. + + .. math:: + y_i = max(x_1, x_2, x_3, \dots, x_i) + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to do the operation over + + Keyword args: + out (tuple, optional): the result tuple of two output tensors (values, indices) + + Example:: + + >>> a = torch.randn(10) + >>> a + tensor([-0.3449, -1.5447, 0.0685, -1.5104, -1.1706, 0.2259, 1.4696, -1.3284, + 1.9946, -0.8209]) + >>> torch.cummax(a, dim=0) + torch.return_types.cummax( + values=tensor([-0.3449, -0.3449, 0.0685, 0.0685, 0.0685, 0.2259, 1.4696, 1.4696, + 1.9946, 1.9946]), + indices=tensor([0, 0, 2, 2, 2, 5, 6, 6, 8, 8])) + """ + +@overload +def cummin( + input: Tensor, + dim: _int, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.cummin: + r""" + cummin(input, dim, *, out=None) -> (Tensor, LongTensor) + Returns a namedtuple ``(values, indices)`` where ``values`` is the cumulative minimum of + elements of :attr:`input` in the dimension :attr:`dim`. And ``indices`` is the index + location of each maximum value found in the dimension :attr:`dim`. + + .. math:: + y_i = min(x_1, x_2, x_3, \dots, x_i) + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to do the operation over + + Keyword args: + out (tuple, optional): the result tuple of two output tensors (values, indices) + + Example:: + + >>> a = torch.randn(10) + >>> a + tensor([-0.2284, -0.6628, 0.0975, 0.2680, -1.3298, -0.4220, -0.3885, 1.1762, + 0.9165, 1.6684]) + >>> torch.cummin(a, dim=0) + torch.return_types.cummin( + values=tensor([-0.2284, -0.6628, -0.6628, -0.6628, -1.3298, -1.3298, -1.3298, -1.3298, + -1.3298, -1.3298]), + indices=tensor([0, 1, 1, 1, 4, 4, 4, 4, 4, 4])) + """ + +@overload +def cummin( + input: Tensor, + dim: str | EllipsisType | None, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.cummin: + r""" + cummin(input, dim, *, out=None) -> (Tensor, LongTensor) + Returns a namedtuple ``(values, indices)`` where ``values`` is the cumulative minimum of + elements of :attr:`input` in the dimension :attr:`dim`. And ``indices`` is the index + location of each maximum value found in the dimension :attr:`dim`. + + .. math:: + y_i = min(x_1, x_2, x_3, \dots, x_i) + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to do the operation over + + Keyword args: + out (tuple, optional): the result tuple of two output tensors (values, indices) + + Example:: + + >>> a = torch.randn(10) + >>> a + tensor([-0.2284, -0.6628, 0.0975, 0.2680, -1.3298, -0.4220, -0.3885, 1.1762, + 0.9165, 1.6684]) + >>> torch.cummin(a, dim=0) + torch.return_types.cummin( + values=tensor([-0.2284, -0.6628, -0.6628, -0.6628, -1.3298, -1.3298, -1.3298, -1.3298, + -1.3298, -1.3298]), + indices=tensor([0, 1, 1, 1, 4, 4, 4, 4, 4, 4])) + """ + +@overload +def cumprod( + input: Tensor, + dim: _int, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + cumprod(input, dim, *, dtype=None, out=None) -> Tensor + + Returns the cumulative product of elements of :attr:`input` in the dimension + :attr:`dim`. + + For example, if :attr:`input` is a vector of size N, the result will also be + a vector of size N, with elements. + + .. math:: + y_i = x_1 \times x_2\times x_3\times \dots \times x_i + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to do the operation over + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(10) + >>> a + tensor([ 0.6001, 0.2069, -0.1919, 0.9792, 0.6727, 1.0062, 0.4126, + -0.2129, -0.4206, 0.1968]) + >>> torch.cumprod(a, dim=0) + tensor([ 0.6001, 0.1241, -0.0238, -0.0233, -0.0157, -0.0158, -0.0065, + 0.0014, -0.0006, -0.0001]) + + >>> a[5] = 0.0 + >>> torch.cumprod(a, dim=0) + tensor([ 0.6001, 0.1241, -0.0238, -0.0233, -0.0157, -0.0000, -0.0000, + 0.0000, -0.0000, -0.0000]) + """ + +@overload +def cumprod( + input: Tensor, + dim: str | EllipsisType | None, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + cumprod(input, dim, *, dtype=None, out=None) -> Tensor + + Returns the cumulative product of elements of :attr:`input` in the dimension + :attr:`dim`. + + For example, if :attr:`input` is a vector of size N, the result will also be + a vector of size N, with elements. + + .. math:: + y_i = x_1 \times x_2\times x_3\times \dots \times x_i + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to do the operation over + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(10) + >>> a + tensor([ 0.6001, 0.2069, -0.1919, 0.9792, 0.6727, 1.0062, 0.4126, + -0.2129, -0.4206, 0.1968]) + >>> torch.cumprod(a, dim=0) + tensor([ 0.6001, 0.1241, -0.0238, -0.0233, -0.0157, -0.0158, -0.0065, + 0.0014, -0.0006, -0.0001]) + + >>> a[5] = 0.0 + >>> torch.cumprod(a, dim=0) + tensor([ 0.6001, 0.1241, -0.0238, -0.0233, -0.0157, -0.0000, -0.0000, + 0.0000, -0.0000, -0.0000]) + """ + +@overload +def cumsum( + input: Tensor, + dim: _int, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + cumsum(input, dim, *, dtype=None, out=None) -> Tensor + + Returns the cumulative sum of elements of :attr:`input` in the dimension + :attr:`dim`. + + For example, if :attr:`input` is a vector of size N, the result will also be + a vector of size N, with elements. + + .. math:: + y_i = x_1 + x_2 + x_3 + \dots + x_i + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to do the operation over + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randint(1, 20, (10,)) + >>> a + tensor([13, 7, 3, 10, 13, 3, 15, 10, 9, 10]) + >>> torch.cumsum(a, dim=0) + tensor([13, 20, 23, 33, 46, 49, 64, 74, 83, 93]) + """ + +@overload +def cumsum( + input: Tensor, + dim: str | EllipsisType | None, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + cumsum(input, dim, *, dtype=None, out=None) -> Tensor + + Returns the cumulative sum of elements of :attr:`input` in the dimension + :attr:`dim`. + + For example, if :attr:`input` is a vector of size N, the result will also be + a vector of size N, with elements. + + .. math:: + y_i = x_1 + x_2 + x_3 + \dots + x_i + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to do the operation over + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randint(1, 20, (10,)) + >>> a + tensor([13, 7, 3, 10, 13, 3, 15, 10, 9, 10]) + >>> torch.cumsum(a, dim=0) + tensor([13, 20, 23, 33, 46, 49, 64, 74, 83, 93]) + """ + +@overload +def cumulative_trapezoid(y: Tensor, x: Tensor, *, dim: _int = -1) -> Tensor: + r""" + cumulative_trapezoid(y, x=None, *, dx=None, dim=-1) -> Tensor + + Cumulatively computes the `trapezoidal rule `_ + along :attr:`dim`. By default the spacing between elements is assumed to be 1, but + :attr:`dx` can be used to specify a different constant spacing, and :attr:`x` can be + used to specify arbitrary spacing along :attr:`dim`. + + For more details, please read :func:`torch.trapezoid`. The difference between :func:`torch.trapezoid` + and this function is that, :func:`torch.trapezoid` returns a value for each integration, + where as this function returns a cumulative value for every spacing within the integration. This + is analogous to how `.sum` returns a value and `.cumsum` returns a cumulative sum. + + Arguments: + y (Tensor): Values to use when computing the trapezoidal rule. + x (Tensor): If specified, defines spacing between values as specified above. + + Keyword arguments: + dx (float): constant spacing between values. If neither :attr:`x` or :attr:`dx` + are specified then this defaults to 1. Effectively multiplies the result by its value. + dim (int): The dimension along which to compute the trapezoidal rule. + The last (inner-most) dimension by default. + + Examples:: + + >>> # Cumulatively computes the trapezoidal rule in 1D, spacing is implicitly 1. + >>> y = torch.tensor([1, 5, 10]) + >>> torch.cumulative_trapezoid(y) + tensor([3., 10.5]) + + >>> # Computes the same trapezoidal rule directly up to each element to verify + >>> (1 + 5) / 2 + 3.0 + >>> (1 + 10 + 10) / 2 + 10.5 + + >>> # Cumulatively computes the trapezoidal rule in 1D with constant spacing of 2 + >>> # NOTE: the result is the same as before, but multiplied by 2 + >>> torch.cumulative_trapezoid(y, dx=2) + tensor([6., 21.]) + + >>> # Cumulatively computes the trapezoidal rule in 1D with arbitrary spacing + >>> x = torch.tensor([1, 3, 6]) + >>> torch.cumulative_trapezoid(y, x) + tensor([6., 28.5]) + + >>> # Computes the same trapezoidal rule directly up to each element to verify + >>> ((3 - 1) * (1 + 5)) / 2 + 6.0 + >>> ((3 - 1) * (1 + 5) + (6 - 3) * (5 + 10)) / 2 + 28.5 + + >>> # Cumulatively computes the trapezoidal rule for each row of a 3x3 matrix + >>> y = torch.arange(9).reshape(3, 3) + tensor([[0, 1, 2], + [3, 4, 5], + [6, 7, 8]]) + >>> torch.cumulative_trapezoid(y) + tensor([[ 0.5, 2.], + [ 3.5, 8.], + [ 6.5, 14.]]) + + >>> # Cumulatively computes the trapezoidal rule for each column of the matrix + >>> torch.cumulative_trapezoid(y, dim=0) + tensor([[ 1.5, 2.5, 3.5], + [ 6.0, 8.0, 10.0]]) + + >>> # Cumulatively computes the trapezoidal rule for each row of a 3x3 ones matrix + >>> # with the same arbitrary spacing + >>> y = torch.ones(3, 3) + >>> x = torch.tensor([1, 3, 6]) + >>> torch.cumulative_trapezoid(y, x) + tensor([[2., 5.], + [2., 5.], + [2., 5.]]) + + >>> # Cumulatively computes the trapezoidal rule for each row of a 3x3 ones matrix + >>> # with different arbitrary spacing per row + >>> y = torch.ones(3, 3) + >>> x = torch.tensor([[1, 2, 3], [1, 3, 5], [1, 4, 7]]) + >>> torch.cumulative_trapezoid(y, x) + tensor([[1., 2.], + [2., 4.], + [3., 6.]]) + """ + +@overload +def cumulative_trapezoid( + y: Tensor, + *, + dx: Number | _complex = 1, + dim: _int = -1, +) -> Tensor: + r""" + cumulative_trapezoid(y, x=None, *, dx=None, dim=-1) -> Tensor + + Cumulatively computes the `trapezoidal rule `_ + along :attr:`dim`. By default the spacing between elements is assumed to be 1, but + :attr:`dx` can be used to specify a different constant spacing, and :attr:`x` can be + used to specify arbitrary spacing along :attr:`dim`. + + For more details, please read :func:`torch.trapezoid`. The difference between :func:`torch.trapezoid` + and this function is that, :func:`torch.trapezoid` returns a value for each integration, + where as this function returns a cumulative value for every spacing within the integration. This + is analogous to how `.sum` returns a value and `.cumsum` returns a cumulative sum. + + Arguments: + y (Tensor): Values to use when computing the trapezoidal rule. + x (Tensor): If specified, defines spacing between values as specified above. + + Keyword arguments: + dx (float): constant spacing between values. If neither :attr:`x` or :attr:`dx` + are specified then this defaults to 1. Effectively multiplies the result by its value. + dim (int): The dimension along which to compute the trapezoidal rule. + The last (inner-most) dimension by default. + + Examples:: + + >>> # Cumulatively computes the trapezoidal rule in 1D, spacing is implicitly 1. + >>> y = torch.tensor([1, 5, 10]) + >>> torch.cumulative_trapezoid(y) + tensor([3., 10.5]) + + >>> # Computes the same trapezoidal rule directly up to each element to verify + >>> (1 + 5) / 2 + 3.0 + >>> (1 + 10 + 10) / 2 + 10.5 + + >>> # Cumulatively computes the trapezoidal rule in 1D with constant spacing of 2 + >>> # NOTE: the result is the same as before, but multiplied by 2 + >>> torch.cumulative_trapezoid(y, dx=2) + tensor([6., 21.]) + + >>> # Cumulatively computes the trapezoidal rule in 1D with arbitrary spacing + >>> x = torch.tensor([1, 3, 6]) + >>> torch.cumulative_trapezoid(y, x) + tensor([6., 28.5]) + + >>> # Computes the same trapezoidal rule directly up to each element to verify + >>> ((3 - 1) * (1 + 5)) / 2 + 6.0 + >>> ((3 - 1) * (1 + 5) + (6 - 3) * (5 + 10)) / 2 + 28.5 + + >>> # Cumulatively computes the trapezoidal rule for each row of a 3x3 matrix + >>> y = torch.arange(9).reshape(3, 3) + tensor([[0, 1, 2], + [3, 4, 5], + [6, 7, 8]]) + >>> torch.cumulative_trapezoid(y) + tensor([[ 0.5, 2.], + [ 3.5, 8.], + [ 6.5, 14.]]) + + >>> # Cumulatively computes the trapezoidal rule for each column of the matrix + >>> torch.cumulative_trapezoid(y, dim=0) + tensor([[ 1.5, 2.5, 3.5], + [ 6.0, 8.0, 10.0]]) + + >>> # Cumulatively computes the trapezoidal rule for each row of a 3x3 ones matrix + >>> # with the same arbitrary spacing + >>> y = torch.ones(3, 3) + >>> x = torch.tensor([1, 3, 6]) + >>> torch.cumulative_trapezoid(y, x) + tensor([[2., 5.], + [2., 5.], + [2., 5.]]) + + >>> # Cumulatively computes the trapezoidal rule for each row of a 3x3 ones matrix + >>> # with different arbitrary spacing per row + >>> y = torch.ones(3, 3) + >>> x = torch.tensor([[1, 2, 3], [1, 3, 5], [1, 4, 7]]) + >>> torch.cumulative_trapezoid(y, x) + tensor([[1., 2.], + [2., 4.], + [3., 6.]]) + """ + +def deg2rad(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + deg2rad(input, *, out=None) -> Tensor + + Returns a new tensor with each of the elements of :attr:`input` + converted from angles in degrees to radians. + + Args: + input (Tensor): the input tensor. + + Keyword arguments: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([[180.0, -180.0], [360.0, -360.0], [90.0, -90.0]]) + >>> torch.deg2rad(a) + tensor([[ 3.1416, -3.1416], + [ 6.2832, -6.2832], + [ 1.5708, -1.5708]]) + """ + +def deg2rad_(input: Tensor) -> Tensor: ... +@overload +def dequantize(input: Tensor) -> Tensor: + r""" + dequantize(tensor) -> Tensor + + Returns an fp32 Tensor by dequantizing a quantized Tensor + + Args: + tensor (Tensor): A quantized Tensor + + .. function:: dequantize(tensors) -> sequence of Tensors + :noindex: + + Given a list of quantized Tensors, dequantize them and return a list of fp32 Tensors + + Args: + tensors (sequence of Tensors): A list of quantized Tensors + """ + +@overload +def dequantize( + tensors: tuple[Tensor, ...] | list[Tensor], +) -> tuple[Tensor, ...]: + r""" + dequantize(tensor) -> Tensor + + Returns an fp32 Tensor by dequantizing a quantized Tensor + + Args: + tensor (Tensor): A quantized Tensor + + .. function:: dequantize(tensors) -> sequence of Tensors + :noindex: + + Given a list of quantized Tensors, dequantize them and return a list of fp32 Tensors + + Args: + tensors (sequence of Tensors): A list of quantized Tensors + """ + +def det(input: Tensor) -> Tensor: + r""" + det(input) -> Tensor + + Alias for :func:`torch.linalg.det` + """ + +def detach(input: Tensor) -> Tensor: ... +def detach_(input: Tensor) -> Tensor: ... +def detach_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + Performs the same operation as :func:`torch.detach`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def diag( + input: Tensor, + diagonal: _int = 0, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + diag(input, diagonal=0, *, out=None) -> Tensor + + - If :attr:`input` is a vector (1-D tensor), then returns a 2-D square tensor + with the elements of :attr:`input` as the diagonal. + - If :attr:`input` is a matrix (2-D tensor), then returns a 1-D tensor with + the diagonal elements of :attr:`input`. + + The argument :attr:`diagonal` controls which diagonal to consider: + + - If :attr:`diagonal` = 0, it is the main diagonal. + - If :attr:`diagonal` > 0, it is above the main diagonal. + - If :attr:`diagonal` < 0, it is below the main diagonal. + + Args: + input (Tensor): the input tensor. + diagonal (int, optional): the diagonal to consider + + Keyword args: + out (Tensor, optional): the output tensor. + + .. seealso:: + + :func:`torch.diagonal` always returns the diagonal of its input. + + :func:`torch.diagflat` always constructs a tensor with diagonal elements + specified by the input. + + Examples: + + Get the square matrix where the input vector is the diagonal:: + + >>> a = torch.randn(3) + >>> a + tensor([ 0.5950,-0.0872, 2.3298]) + >>> torch.diag(a) + tensor([[ 0.5950, 0.0000, 0.0000], + [ 0.0000,-0.0872, 0.0000], + [ 0.0000, 0.0000, 2.3298]]) + >>> torch.diag(a, 1) + tensor([[ 0.0000, 0.5950, 0.0000, 0.0000], + [ 0.0000, 0.0000,-0.0872, 0.0000], + [ 0.0000, 0.0000, 0.0000, 2.3298], + [ 0.0000, 0.0000, 0.0000, 0.0000]]) + + Get the k-th diagonal of a given matrix:: + + >>> a = torch.randn(3, 3) + >>> a + tensor([[-0.4264, 0.0255,-0.1064], + [ 0.8795,-0.2429, 0.1374], + [ 0.1029,-0.6482,-1.6300]]) + >>> torch.diag(a, 0) + tensor([-0.4264,-0.2429,-1.6300]) + >>> torch.diag(a, 1) + tensor([ 0.0255, 0.1374]) + """ + +def diag_embed( + input: Tensor, + offset: _int = 0, + dim1: _int = -2, + dim2: _int = -1, +) -> Tensor: + r""" + diag_embed(input, offset=0, dim1=-2, dim2=-1) -> Tensor + + Creates a tensor whose diagonals of certain 2D planes (specified by + :attr:`dim1` and :attr:`dim2`) are filled by :attr:`input`. + To facilitate creating batched diagonal matrices, the 2D planes formed by + the last two dimensions of the returned tensor are chosen by default. + + The argument :attr:`offset` controls which diagonal to consider: + + - If :attr:`offset` = 0, it is the main diagonal. + - If :attr:`offset` > 0, it is above the main diagonal. + - If :attr:`offset` < 0, it is below the main diagonal. + + The size of the new matrix will be calculated to make the specified diagonal + of the size of the last input dimension. + Note that for :attr:`offset` other than :math:`0`, the order of :attr:`dim1` + and :attr:`dim2` matters. Exchanging them is equivalent to changing the + sign of :attr:`offset`. + + Applying :meth:`torch.diagonal` to the output of this function with + the same arguments yields a matrix identical to input. However, + :meth:`torch.diagonal` has different default dimensions, so those + need to be explicitly specified. + + Args: + input (Tensor): the input tensor. Must be at least 1-dimensional. + offset (int, optional): which diagonal to consider. Default: 0 + (main diagonal). + dim1 (int, optional): first dimension with respect to which to + take diagonal. Default: -2. + dim2 (int, optional): second dimension with respect to which to + take diagonal. Default: -1. + + Example:: + + >>> a = torch.randn(2, 3) + >>> torch.diag_embed(a) + tensor([[[ 1.5410, 0.0000, 0.0000], + [ 0.0000, -0.2934, 0.0000], + [ 0.0000, 0.0000, -2.1788]], + + [[ 0.5684, 0.0000, 0.0000], + [ 0.0000, -1.0845, 0.0000], + [ 0.0000, 0.0000, -1.3986]]]) + + >>> torch.diag_embed(a, offset=1, dim1=0, dim2=2) + tensor([[[ 0.0000, 1.5410, 0.0000, 0.0000], + [ 0.0000, 0.5684, 0.0000, 0.0000]], + + [[ 0.0000, 0.0000, -0.2934, 0.0000], + [ 0.0000, 0.0000, -1.0845, 0.0000]], + + [[ 0.0000, 0.0000, 0.0000, -2.1788], + [ 0.0000, 0.0000, 0.0000, -1.3986]], + + [[ 0.0000, 0.0000, 0.0000, 0.0000], + [ 0.0000, 0.0000, 0.0000, 0.0000]]]) + """ + +def diagflat(input: Tensor, offset: _int = 0) -> Tensor: + r""" + diagflat(input, offset=0) -> Tensor + + - If :attr:`input` is a vector (1-D tensor), then returns a 2-D square tensor + with the elements of :attr:`input` as the diagonal. + - If :attr:`input` is a tensor with more than one dimension, then returns a + 2-D tensor with diagonal elements equal to a flattened :attr:`input`. + + The argument :attr:`offset` controls which diagonal to consider: + + - If :attr:`offset` = 0, it is the main diagonal. + - If :attr:`offset` > 0, it is above the main diagonal. + - If :attr:`offset` < 0, it is below the main diagonal. + + Args: + input (Tensor): the input tensor. + offset (int, optional): the diagonal to consider. Default: 0 (main + diagonal). + + Examples:: + + >>> a = torch.randn(3) + >>> a + tensor([-0.2956, -0.9068, 0.1695]) + >>> torch.diagflat(a) + tensor([[-0.2956, 0.0000, 0.0000], + [ 0.0000, -0.9068, 0.0000], + [ 0.0000, 0.0000, 0.1695]]) + >>> torch.diagflat(a, 1) + tensor([[ 0.0000, -0.2956, 0.0000, 0.0000], + [ 0.0000, 0.0000, -0.9068, 0.0000], + [ 0.0000, 0.0000, 0.0000, 0.1695], + [ 0.0000, 0.0000, 0.0000, 0.0000]]) + + >>> a = torch.randn(2, 2) + >>> a + tensor([[ 0.2094, -0.3018], + [-0.1516, 1.9342]]) + >>> torch.diagflat(a) + tensor([[ 0.2094, 0.0000, 0.0000, 0.0000], + [ 0.0000, -0.3018, 0.0000, 0.0000], + [ 0.0000, 0.0000, -0.1516, 0.0000], + [ 0.0000, 0.0000, 0.0000, 1.9342]]) + """ + +@overload +def diagonal( + input: Tensor, + offset: _int = 0, + dim1: _int = 0, + dim2: _int = 1, +) -> Tensor: + r""" + diagonal(input, offset=0, dim1=0, dim2=1) -> Tensor + + Returns a partial view of :attr:`input` with the its diagonal elements + with respect to :attr:`dim1` and :attr:`dim2` appended as a dimension + at the end of the shape. + + The argument :attr:`offset` controls which diagonal to consider: + + - If :attr:`offset` = 0, it is the main diagonal. + - If :attr:`offset` > 0, it is above the main diagonal. + - If :attr:`offset` < 0, it is below the main diagonal. + + Applying :meth:`torch.diag_embed` to the output of this function with + the same arguments yields a diagonal matrix with the diagonal entries + of the input. However, :meth:`torch.diag_embed` has different default + dimensions, so those need to be explicitly specified. + + Args: + input (Tensor): the input tensor. Must be at least 2-dimensional. + offset (int, optional): which diagonal to consider. Default: 0 + (main diagonal). + dim1 (int, optional): first dimension with respect to which to + take diagonal. Default: 0. + dim2 (int, optional): second dimension with respect to which to + take diagonal. Default: 1. + + .. note:: To take a batch diagonal, pass in dim1=-2, dim2=-1. + + Examples:: + + >>> a = torch.randn(3, 3) + >>> a + tensor([[-1.0854, 1.1431, -0.1752], + [ 0.8536, -0.0905, 0.0360], + [ 0.6927, -0.3735, -0.4945]]) + + + >>> torch.diagonal(a) + tensor([-1.0854, -0.0905, -0.4945]) + + + >>> torch.diagonal(a, 1) + tensor([ 1.1431, 0.0360]) + + >>> b = torch.randn(2, 5) + >>> b + tensor([[-1.7948, -1.2731, -0.3181, 2.0200, -1.6745], + [ 1.8262, -1.5049, 0.4114, 1.0704, -1.2607]]) + + >>> torch.diagonal(b, 1, 1, 0) + tensor([1.8262]) + + >>> x = torch.randn(2, 5, 4, 2) + >>> torch.diagonal(x, offset=-1, dim1=1, dim2=2) + tensor([[[-1.2631, 0.3755, -1.5977, -1.8172], + [-1.1065, 1.0401, -0.2235, -0.7938]], + + [[-1.7325, -0.3081, 0.6166, 0.2335], + [ 1.0500, 0.7336, -0.3836, -1.1015]]]) + """ + +@overload +def diagonal( + input: Tensor, + *, + outdim: str | EllipsisType | None, + dim1: str | EllipsisType | None, + dim2: str | EllipsisType | None, + offset: _int = 0, +) -> Tensor: + r""" + diagonal(input, offset=0, dim1=0, dim2=1) -> Tensor + + Returns a partial view of :attr:`input` with the its diagonal elements + with respect to :attr:`dim1` and :attr:`dim2` appended as a dimension + at the end of the shape. + + The argument :attr:`offset` controls which diagonal to consider: + + - If :attr:`offset` = 0, it is the main diagonal. + - If :attr:`offset` > 0, it is above the main diagonal. + - If :attr:`offset` < 0, it is below the main diagonal. + + Applying :meth:`torch.diag_embed` to the output of this function with + the same arguments yields a diagonal matrix with the diagonal entries + of the input. However, :meth:`torch.diag_embed` has different default + dimensions, so those need to be explicitly specified. + + Args: + input (Tensor): the input tensor. Must be at least 2-dimensional. + offset (int, optional): which diagonal to consider. Default: 0 + (main diagonal). + dim1 (int, optional): first dimension with respect to which to + take diagonal. Default: 0. + dim2 (int, optional): second dimension with respect to which to + take diagonal. Default: 1. + + .. note:: To take a batch diagonal, pass in dim1=-2, dim2=-1. + + Examples:: + + >>> a = torch.randn(3, 3) + >>> a + tensor([[-1.0854, 1.1431, -0.1752], + [ 0.8536, -0.0905, 0.0360], + [ 0.6927, -0.3735, -0.4945]]) + + + >>> torch.diagonal(a) + tensor([-1.0854, -0.0905, -0.4945]) + + + >>> torch.diagonal(a, 1) + tensor([ 1.1431, 0.0360]) + + >>> b = torch.randn(2, 5) + >>> b + tensor([[-1.7948, -1.2731, -0.3181, 2.0200, -1.6745], + [ 1.8262, -1.5049, 0.4114, 1.0704, -1.2607]]) + + >>> torch.diagonal(b, 1, 1, 0) + tensor([1.8262]) + + >>> x = torch.randn(2, 5, 4, 2) + >>> torch.diagonal(x, offset=-1, dim1=1, dim2=2) + tensor([[[-1.2631, 0.3755, -1.5977, -1.8172], + [-1.1065, 1.0401, -0.2235, -0.7938]], + + [[-1.7325, -0.3081, 0.6166, 0.2335], + [ 1.0500, 0.7336, -0.3836, -1.1015]]]) + """ + +def diagonal_copy( + input: Tensor, + offset: _int = 0, + dim1: _int = 0, + dim2: _int = 1, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.diagonal`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def diagonal_scatter( + input: Tensor, + src: Tensor, + offset: _int = 0, + dim1: _int = 0, + dim2: _int = 1, +) -> Tensor: + r""" + diagonal_scatter(input, src, offset=0, dim1=0, dim2=1) -> Tensor + + Embeds the values of the :attr:`src` tensor into :attr:`input` along + the diagonal elements of :attr:`input`, with respect to :attr:`dim1` + and :attr:`dim2`. + + This function returns a tensor with fresh storage; it does not + return a view. + + The argument :attr:`offset` controls which diagonal to consider: + + - If :attr:`offset` = 0, it is the main diagonal. + - If :attr:`offset` > 0, it is above the main diagonal. + - If :attr:`offset` < 0, it is below the main diagonal. + + Args: + input (Tensor): the input tensor. Must be at least 2-dimensional. + src (Tensor): the tensor to embed into :attr:`input`. + offset (int, optional): which diagonal to consider. Default: 0 + (main diagonal). + dim1 (int, optional): first dimension with respect to which to + take diagonal. Default: 0. + dim2 (int, optional): second dimension with respect to which to + take diagonal. Default: 1. + + .. note:: + + :attr:`src` must be of the proper size in order to be embedded + into :attr:`input`. Specifically, it should have the same shape as + ``torch.diagonal(input, offset, dim1, dim2)`` + + Examples:: + + >>> a = torch.zeros(3, 3) + >>> a + tensor([[0., 0., 0.], + [0., 0., 0.], + [0., 0., 0.]]) + + >>> torch.diagonal_scatter(a, torch.ones(3), 0) + tensor([[1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.]]) + + >>> torch.diagonal_scatter(a, torch.ones(2), 1) + tensor([[0., 1., 0.], + [0., 0., 1.], + [0., 0., 0.]]) + """ + +def diff( + input: Tensor, + n: _int = 1, + dim: _int = -1, + prepend: Tensor | None = None, + append: Tensor | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + diff(input, n=1, dim=-1, prepend=None, append=None) -> Tensor + + Computes the n-th forward difference along the given dimension. + + The first-order differences are given by `out[i] = input[i + 1] - input[i]`. Higher-order + differences are calculated by using :func:`torch.diff` recursively. + + Args: + input (Tensor): the tensor to compute the differences on + n (int, optional): the number of times to recursively compute the difference + dim (int, optional): the dimension to compute the difference along. + Default is the last dimension. + prepend, append (Tensor, optional): values to prepend or append to + :attr:`input` along :attr:`dim` before computing the difference. + Their dimensions must be equivalent to that of input, and their shapes + must match input's shape except on :attr:`dim`. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([1, 3, 2]) + >>> torch.diff(a) + tensor([ 2, -1]) + >>> b = torch.tensor([4, 5]) + >>> torch.diff(a, append=b) + tensor([ 2, -1, 2, 1]) + >>> c = torch.tensor([[1, 2, 3], [3, 4, 5]]) + >>> torch.diff(c, dim=0) + tensor([[2, 2, 2]]) + >>> torch.diff(c, dim=1) + tensor([[1, 1], + [1, 1]]) + """ + +def digamma(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + digamma(input, *, out=None) -> Tensor + + Alias for :func:`torch.special.digamma`. + """ + +def dist(input: Tensor, other: Tensor, p: Number | _complex = 2) -> Tensor: + r""" + dist(input, other, p=2) -> Tensor + + Returns the p-norm of (:attr:`input` - :attr:`other`) + + The shapes of :attr:`input` and :attr:`other` must be + :ref:`broadcastable `. + + Args: + input (Tensor): the input tensor. + other (Tensor): the Right-hand-side input tensor + p (float, optional): the norm to be computed + + Example:: + + >>> x = torch.randn(4) + >>> x + tensor([-1.5393, -0.8675, 0.5916, 1.6321]) + >>> y = torch.randn(4) + >>> y + tensor([ 0.0967, -1.0511, 0.6295, 0.8360]) + >>> torch.dist(x, y, 3.5) + tensor(1.6727) + >>> torch.dist(x, y, 3) + tensor(1.6973) + >>> torch.dist(x, y, 0) + tensor(4.) + >>> torch.dist(x, y, 1) + tensor(2.6537) + """ + +def div( + input: Tensor | Number, + other: Tensor | Number, + *, + rounding_mode: str | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + div(input, other, *, rounding_mode=None, out=None) -> Tensor + + Divides each element of the input ``input`` by the corresponding element of + :attr:`other`. + + .. math:: + \text{out}_i = \frac{\text{input}_i}{\text{other}_i} + + .. note:: + By default, this performs a "true" division like Python 3. + See the :attr:`rounding_mode` argument for floor division. + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer, float, and complex inputs. + Always promotes integer types to the default scalar type. + + Args: + input (Tensor): the dividend + other (Tensor or Number): the divisor + + Keyword args: + rounding_mode (str, optional): Type of rounding applied to the result: + + * None - default behavior. Performs no rounding and, if both :attr:`input` and + :attr:`other` are integer types, promotes the inputs to the default scalar type. + Equivalent to true division in Python (the ``/`` operator) and NumPy's ``np.true_divide``. + * ``"trunc"`` - rounds the results of the division towards zero. + Equivalent to C-style integer division. + * ``"floor"`` - rounds the results of the division down. + Equivalent to floor division in Python (the ``//`` operator) and NumPy's ``np.floor_divide``. + + out (Tensor, optional): the output tensor. + + Examples:: + + >>> x = torch.tensor([ 0.3810, 1.2774, -0.2972, -0.3719, 0.4637]) + >>> torch.div(x, 0.5) + tensor([ 0.7620, 2.5548, -0.5944, -0.7438, 0.9274]) + + >>> a = torch.tensor([[-0.3711, -1.9353, -0.4605, -0.2917], + ... [ 0.1815, -1.0111, 0.9805, -1.5923], + ... [ 0.1062, 1.4581, 0.7759, -1.2344], + ... [-0.1830, -0.0313, 1.1908, -1.4757]]) + >>> b = torch.tensor([ 0.8032, 0.2930, -0.8113, -0.2308]) + >>> torch.div(a, b) + tensor([[-0.4620, -6.6051, 0.5676, 1.2639], + [ 0.2260, -3.4509, -1.2086, 6.8990], + [ 0.1322, 4.9764, -0.9564, 5.3484], + [-0.2278, -0.1068, -1.4678, 6.3938]]) + + >>> torch.div(a, b, rounding_mode='trunc') + tensor([[-0., -6., 0., 1.], + [ 0., -3., -1., 6.], + [ 0., 4., -0., 5.], + [-0., -0., -1., 6.]]) + + >>> torch.div(a, b, rounding_mode='floor') + tensor([[-1., -7., 0., 1.], + [ 0., -4., -2., 6.], + [ 0., 4., -1., 5.], + [-1., -1., -2., 6.]]) + """ + +@overload +def divide( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + divide(input, other, *, rounding_mode=None, out=None) -> Tensor + + Alias for :func:`torch.div`. + """ + +@overload +def divide( + input: Tensor, + other: Tensor, + *, + rounding_mode: str | None, + out: Tensor | None = None, +) -> Tensor: + r""" + divide(input, other, *, rounding_mode=None, out=None) -> Tensor + + Alias for :func:`torch.div`. + """ + +@overload +def divide( + input: Tensor, + other: Number | _complex, + *, + rounding_mode: str | None, +) -> Tensor: + r""" + divide(input, other, *, rounding_mode=None, out=None) -> Tensor + + Alias for :func:`torch.div`. + """ + +@overload +def divide(input: Tensor, other: Number | _complex) -> Tensor: + r""" + divide(input, other, *, rounding_mode=None, out=None) -> Tensor + + Alias for :func:`torch.div`. + """ + +def dot( + input: Tensor, + tensor: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + dot(input, tensor, *, out=None) -> Tensor + + Computes the dot product of two 1D tensors. + + .. note:: + + Unlike NumPy's dot, torch.dot intentionally only supports computing the dot product + of two 1D tensors with the same number of elements. + + Args: + input (Tensor): first tensor in the dot product, must be 1D. + tensor (Tensor): second tensor in the dot product, must be 1D. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.dot(torch.tensor([2, 3]), torch.tensor([2, 1])) + tensor(7) + + >>> t1, t2 = torch.tensor([0, 1]), torch.tensor([2, 3]) + >>> torch.dot(t1, t2) + tensor(3) + """ + +def dropout(input: Tensor, p: _float, train: _bool) -> Tensor: ... +def dropout_(input: Tensor, p: _float, train: _bool) -> Tensor: ... +def dsmm(input: Tensor, mat2: Tensor) -> Tensor: ... +@overload +def dsplit(input: Tensor, sections: _int) -> tuple[Tensor, ...]: + r""" + dsplit(input, indices_or_sections) -> List of Tensors + + Splits :attr:`input`, a tensor with three or more dimensions, into multiple tensors + depthwise according to :attr:`indices_or_sections`. Each split is a view of + :attr:`input`. + + This is equivalent to calling torch.tensor_split(input, indices_or_sections, dim=2) + (the split dimension is 2), except that if :attr:`indices_or_sections` is an integer + it must evenly divide the split dimension or a runtime error will be thrown. + + This function is based on NumPy's :func:`numpy.dsplit`. + + Args: + input (Tensor): tensor to split. + indices_or_sections (int or list or tuple of ints): See argument in :func:`torch.tensor_split`. + + Example:: + + >>> t = torch.arange(16.0).reshape(2, 2, 4) + >>> t + tensor([[[ 0., 1., 2., 3.], + [ 4., 5., 6., 7.]], + [[ 8., 9., 10., 11.], + [12., 13., 14., 15.]]]) + >>> torch.dsplit(t, 2) + (tensor([[[ 0., 1.], + [ 4., 5.]], + [[ 8., 9.], + [12., 13.]]]), + tensor([[[ 2., 3.], + [ 6., 7.]], + [[10., 11.], + [14., 15.]]])) + + >>> torch.dsplit(t, [3, 6]) + (tensor([[[ 0., 1., 2.], + [ 4., 5., 6.]], + [[ 8., 9., 10.], + [12., 13., 14.]]]), + tensor([[[ 3.], + [ 7.]], + [[11.], + [15.]]]), + tensor([], size=(2, 2, 0))) + """ + +@overload +def dsplit(input: Tensor, indices: _size) -> tuple[Tensor, ...]: + r""" + dsplit(input, indices_or_sections) -> List of Tensors + + Splits :attr:`input`, a tensor with three or more dimensions, into multiple tensors + depthwise according to :attr:`indices_or_sections`. Each split is a view of + :attr:`input`. + + This is equivalent to calling torch.tensor_split(input, indices_or_sections, dim=2) + (the split dimension is 2), except that if :attr:`indices_or_sections` is an integer + it must evenly divide the split dimension or a runtime error will be thrown. + + This function is based on NumPy's :func:`numpy.dsplit`. + + Args: + input (Tensor): tensor to split. + indices_or_sections (int or list or tuple of ints): See argument in :func:`torch.tensor_split`. + + Example:: + + >>> t = torch.arange(16.0).reshape(2, 2, 4) + >>> t + tensor([[[ 0., 1., 2., 3.], + [ 4., 5., 6., 7.]], + [[ 8., 9., 10., 11.], + [12., 13., 14., 15.]]]) + >>> torch.dsplit(t, 2) + (tensor([[[ 0., 1.], + [ 4., 5.]], + [[ 8., 9.], + [12., 13.]]]), + tensor([[[ 2., 3.], + [ 6., 7.]], + [[10., 11.], + [14., 15.]]])) + + >>> torch.dsplit(t, [3, 6]) + (tensor([[[ 0., 1., 2.], + [ 4., 5., 6.]], + [[ 8., 9., 10.], + [12., 13., 14.]]]), + tensor([[[ 3.], + [ 7.]], + [[11.], + [15.]]]), + tensor([], size=(2, 2, 0))) + """ + +def dstack( + tensors: tuple[Tensor, ...] | list[Tensor], + *, + out: Tensor | None = None, +) -> Tensor: + r""" + dstack(tensors, *, out=None) -> Tensor + + Stack tensors in sequence depthwise (along third axis). + + This is equivalent to concatenation along the third axis after 1-D and 2-D tensors have been reshaped by :func:`torch.atleast_3d`. + + Args: + tensors (sequence of Tensors): sequence of tensors to concatenate + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([1, 2, 3]) + >>> b = torch.tensor([4, 5, 6]) + >>> torch.dstack((a,b)) + tensor([[[1, 4], + [2, 5], + [3, 6]]]) + >>> a = torch.tensor([[1],[2],[3]]) + >>> b = torch.tensor([[4],[5],[6]]) + >>> torch.dstack((a,b)) + tensor([[[1, 4]], + [[2, 5]], + [[3, 6]]]) + """ + +def embedding( + weight: Tensor, + indices: Tensor, + padding_idx: _int | SymInt = -1, + scale_grad_by_freq: _bool = False, + sparse: _bool = False, +) -> Tensor: ... +@overload +def embedding_bag( + weight: Tensor, + indices: Tensor, + offsets: Tensor, + scale_grad_by_freq: _bool, + mode: _int, + sparse: _bool, + per_sample_weights: Tensor | None, + include_last_offset: _bool, + padding_idx: _int | None, +) -> tuple[Tensor, Tensor, Tensor, Tensor]: ... +@overload +def embedding_bag( + weight: Tensor, + indices: Tensor, + offsets: Tensor, + scale_grad_by_freq: _bool = False, + mode: _int = 0, + sparse: _bool = False, + per_sample_weights: Tensor | None = None, + include_last_offset: _bool = False, +) -> tuple[Tensor, Tensor, Tensor, Tensor]: ... +def embedding_renorm_( + input: Tensor, + indices: Tensor, + max_norm: _float, + norm_type: _float, +) -> Tensor: ... +@overload +def empty( + size: Sequence[_int | SymInt], + *, + memory_format: memory_format | None = None, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + empty(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False, memory_format=torch.contiguous_format) -> Tensor + + Returns a tensor filled with uninitialized data. The shape of the tensor is + defined by the variable argument :attr:`size`. + + .. note:: + If :func:`torch.use_deterministic_algorithms()` and + :attr:`torch.utils.deterministic.fill_uninitialized_memory` are both set to + ``True``, the output tensor is initialized to prevent any possible + nondeterministic behavior from using the data as an input to an operation. + Floating point and complex tensors are filled with NaN, and integer tensors + are filled with the maximum value. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.contiguous_format``. + + Example:: + + >>> torch.empty((2,3), dtype=torch.int64) + tensor([[ 9.4064e+13, 2.8000e+01, 9.3493e+13], + [ 7.5751e+18, 7.1428e+18, 7.5955e+18]]) + """ + +@overload +def empty( + *size: _int | SymInt, + memory_format: memory_format | None = None, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + empty(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False, memory_format=torch.contiguous_format) -> Tensor + + Returns a tensor filled with uninitialized data. The shape of the tensor is + defined by the variable argument :attr:`size`. + + .. note:: + If :func:`torch.use_deterministic_algorithms()` and + :attr:`torch.utils.deterministic.fill_uninitialized_memory` are both set to + ``True``, the output tensor is initialized to prevent any possible + nondeterministic behavior from using the data as an input to an operation. + Floating point and complex tensors are filled with NaN, and integer tensors + are filled with the maximum value. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.contiguous_format``. + + Example:: + + >>> torch.empty((2,3), dtype=torch.int64) + tensor([[ 9.4064e+13, 2.8000e+01, 9.3493e+13], + [ 7.5751e+18, 7.1428e+18, 7.5955e+18]]) + """ + +@overload +def empty( + size: _size, + *, + names: Sequence[str | EllipsisType | None] | None, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + empty(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False, memory_format=torch.contiguous_format) -> Tensor + + Returns a tensor filled with uninitialized data. The shape of the tensor is + defined by the variable argument :attr:`size`. + + .. note:: + If :func:`torch.use_deterministic_algorithms()` and + :attr:`torch.utils.deterministic.fill_uninitialized_memory` are both set to + ``True``, the output tensor is initialized to prevent any possible + nondeterministic behavior from using the data as an input to an operation. + Floating point and complex tensors are filled with NaN, and integer tensors + are filled with the maximum value. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.contiguous_format``. + + Example:: + + >>> torch.empty((2,3), dtype=torch.int64) + tensor([[ 9.4064e+13, 2.8000e+01, 9.3493e+13], + [ 7.5751e+18, 7.1428e+18, 7.5955e+18]]) + """ + +@overload +def empty( + *size: _int, + names: Sequence[str | EllipsisType | None] | None, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + empty(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False, memory_format=torch.contiguous_format) -> Tensor + + Returns a tensor filled with uninitialized data. The shape of the tensor is + defined by the variable argument :attr:`size`. + + .. note:: + If :func:`torch.use_deterministic_algorithms()` and + :attr:`torch.utils.deterministic.fill_uninitialized_memory` are both set to + ``True``, the output tensor is initialized to prevent any possible + nondeterministic behavior from using the data as an input to an operation. + Floating point and complex tensors are filled with NaN, and integer tensors + are filled with the maximum value. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.contiguous_format``. + + Example:: + + >>> torch.empty((2,3), dtype=torch.int64) + tensor([[ 9.4064e+13, 2.8000e+01, 9.3493e+13], + [ 7.5751e+18, 7.1428e+18, 7.5955e+18]]) + """ + +def empty_like( + input: Tensor, + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + empty_like(input, *, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns an uninitialized tensor with the same size as :attr:`input`. + ``torch.empty_like(input)`` is equivalent to + ``torch.empty(input.size(), dtype=input.dtype, layout=input.layout, device=input.device)``. + + .. note:: + If :func:`torch.use_deterministic_algorithms()` and + :attr:`torch.utils.deterministic.fill_uninitialized_memory` are both set to + ``True``, the output tensor is initialized to prevent any possible + nondeterministic behavior from using the data as an input to an operation. + Floating point and complex tensors are filled with NaN, and integer tensors + are filled with the maximum value. + + When ``torch.preserve_format`` is used: + If the input tensor is dense (i.e., non-overlapping strided), + its memory format (including strides) is retained. + Otherwise (e.g., a non-dense view like a stepped slice), + the output is converted to the dense format. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + + Example:: + + >>> a=torch.empty((2,3), dtype=torch.int32, device = 'cuda') + >>> torch.empty_like(a) + tensor([[0, 0, 0], + [0, 0, 0]], device='cuda:0', dtype=torch.int32) + """ + +def empty_permuted( + size: Sequence[_int | SymInt], + physical_layout: _size, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + empty_permuted(size, physical_layout, *, dtype=None, layout=None, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Creates an uninitialized, non-overlapping and dense tensor with the + specified :attr:`size`, with :attr:`physical_layout` specifying how the + dimensions are physically laid out in memory (each logical dimension is listed + from outermost to innermost). :attr:`physical_layout` is a generalization + of NCHW/NHWC notation: if each dimension is assigned a number according to + what order they occur in size (N=0, C=1, H=2, W=3), then NCHW is ``(0, 1, 2, 3)`` + while NHWC is ``(0, 2, 3, 1)``. Equivalently, the strides of the output + tensor ``t`` are such that ``t.stride(physical_layout[i]) == contiguous_strides[i]`` + (notably, this function is *not* equivalent to ``torch.empty(size).permute(physical_layout)``). + + Unlike :func:`torch.empty_strided`, this is guaranteed to produce a dense + tensor with no overlaps. If possible, prefer using this function over + :func:`torch.empty_strided` or manual use of :func:`torch.as_strided`. + + .. note:: + If :func:`torch.use_deterministic_algorithms()` and + :attr:`torch.utils.deterministic.fill_uninitialized_memory` are both set to + ``True``, the output tensor is initialized to prevent any possible + nondeterministic behavior from using the data as an input to an operation. + Floating point and complex tensors are filled with NaN, and integer tensors + are filled with the maximum value. + + Args: + size (tuple of int): the shape of the output tensor + physical_layout (tuple of int): the ordering of dimensions physically in memory + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Examples: + + >>> torch.empty((2, 3, 5, 7)).stride() + (105, 35, 7, 1) + >>> torch.empty_permuted((2, 3, 5, 7), (0, 1, 2, 3)).stride() + (105, 35, 7, 1) + >>> torch.empty((2, 3, 5, 7), memory_format=torch.channels_last).stride() + (105, 1, 21, 3) + >>> torch.empty_permuted((2, 3, 5, 7), (0, 2, 3, 1)).stride() + (105, 1, 21, 3) + >>> torch.empty_permuted((2, 3, 5, 7), (0, 2, 3, 1)).dim_order() + (0, 2, 3, 1) + """ + +def empty_quantized( + size: _size, + qtensor: Tensor, + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: ... +def empty_strided( + size: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + empty_strided(size, stride, *, dtype=None, layout=None, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Creates a tensor with the specified :attr:`size` and :attr:`stride` and filled with undefined data. + + .. warning:: + If the constructed tensor is "overlapped" (with multiple indices referring to the same element + in memory) its behavior is undefined. + + .. note:: + If :func:`torch.use_deterministic_algorithms()` and + :attr:`torch.utils.deterministic.fill_uninitialized_memory` are both set to + ``True``, the output tensor is initialized to prevent any possible + nondeterministic behavior from using the data as an input to an operation. + Floating point and complex tensors are filled with NaN, and integer tensors + are filled with the maximum value. + + Args: + size (tuple of int): the shape of the output tensor + stride (tuple of int): the strides of the output tensor + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> a = torch.empty_strided((2, 3), (1, 2)) + >>> a + tensor([[8.9683e-44, 4.4842e-44, 5.1239e+07], + [0.0000e+00, 0.0000e+00, 3.0705e-41]]) + >>> a.stride() + (1, 2) + >>> a.size() + torch.Size([2, 3]) + """ + +@overload +def eq( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + eq(input, other, *, out=None) -> Tensor + + Computes element-wise equality + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or float): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is equal to :attr:`other` and False elsewhere + + Example:: + + >>> torch.eq(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[ True, False], + [False, True]]) + """ + +@overload +def eq( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + eq(input, other, *, out=None) -> Tensor + + Computes element-wise equality + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or float): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is equal to :attr:`other` and False elsewhere + + Example:: + + >>> torch.eq(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[ True, False], + [False, True]]) + """ + +def equal(input: Tensor, other: Tensor) -> _bool: + r""" + equal(input, other) -> bool + + ``True`` if two tensors have the same size and elements, ``False`` otherwise. + + .. note:: + + Tensors containing NaNs are never equal to each other. Additionally, this function does not + differentiate between the data types of the tensors during comparison. For more thorough tensor checks, + use :meth:`torch.testing.assert_close`. + + Example:: + + >>> torch.equal(torch.tensor([1, 2]), torch.tensor([1, 2])) + True + >>> torch.equal(torch.tensor([3, torch.nan]), torch.tensor([3, torch.nan])) + False + >>> torch.equal(torch.tensor([1, 2, 3], dtype=torch.int32), torch.tensor([1, 2, 3], dtype=torch.float32)) + True + """ + +def erf(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + erf(input, *, out=None) -> Tensor + + Alias for :func:`torch.special.erf`. + """ + +def erf_(input: Tensor) -> Tensor: ... +def erfc(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + erfc(input, *, out=None) -> Tensor + + Alias for :func:`torch.special.erfc`. + """ + +def erfc_(input: Tensor) -> Tensor: ... +def erfinv(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + erfinv(input, *, out=None) -> Tensor + + Alias for :func:`torch.special.erfinv`. + """ + +def exp(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + exp(input, *, out=None) -> Tensor + + Returns a new tensor with the exponential of the elements + of the input tensor :attr:`input`. + + .. math:: + y_{i} = e^{x_{i}} + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.exp(torch.tensor([0, math.log(2.)])) + tensor([ 1., 2.]) + """ + +def exp2(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + exp2(input, *, out=None) -> Tensor + + Alias for :func:`torch.special.exp2`. + """ + +def exp2_(input: Tensor) -> Tensor: ... +def exp_(input: Tensor) -> Tensor: ... +def expand_copy( + input: Tensor, + size: Sequence[_int | SymInt], + *, + implicit: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.Tensor.expand`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def expm1(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + expm1(input, *, out=None) -> Tensor + + Alias for :func:`torch.special.expm1`. + """ + +def expm1_(input: Tensor) -> Tensor: ... +@overload +def eye( + n: _int | SymInt, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + eye(n, m=None, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a 2-D tensor with ones on the diagonal and zeros elsewhere. + + Args: + n (int): the number of rows + m (int, optional): the number of columns with default being :attr:`n` + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 2-D tensor with ones on the diagonal and zeros elsewhere + + Example:: + + >>> torch.eye(3) + tensor([[ 1., 0., 0.], + [ 0., 1., 0.], + [ 0., 0., 1.]]) + """ + +@overload +def eye( + n: _int | SymInt, + m: _int | SymInt, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + eye(n, m=None, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a 2-D tensor with ones on the diagonal and zeros elsewhere. + + Args: + n (int): the number of rows + m (int, optional): the number of columns with default being :attr:`n` + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 2-D tensor with ones on the diagonal and zeros elsewhere + + Example:: + + >>> torch.eye(3) + tensor([[ 1., 0., 0.], + [ 0., 1., 0.], + [ 0., 0., 1.]]) + """ + +def fake_quantize_per_channel_affine( + input: Tensor, + scale: Tensor, + zero_point: Tensor, + axis: _int, + quant_min: _int, + quant_max: _int, +) -> Tensor: + r""" + fake_quantize_per_channel_affine(input, scale, zero_point, axis, quant_min, quant_max) -> Tensor + + Returns a new tensor with the data in :attr:`input` fake quantized per channel using :attr:`scale`, + :attr:`zero_point`, :attr:`quant_min` and :attr:`quant_max`, across the channel specified by :attr:`axis`. + + .. math:: + \text{output} = ( + min( + \text{quant\_max}, + max( + \text{quant\_min}, + \text{std::nearby\_int}(\text{input} / \text{scale}) + \text{zero\_point} + ) + ) - \text{zero\_point} + ) \times \text{scale} + + Args: + input (Tensor): the input value(s), in ``torch.float32`` + scale (Tensor): quantization scale, per channel in ``torch.float32`` + zero_point (Tensor): quantization zero_point, per channel in ``torch.int32`` or ``torch.half`` or ``torch.float32`` + axis (int32): channel axis + quant_min (int64): lower bound of the quantized domain + quant_max (int64): upper bound of the quantized domain + + Returns: + Tensor: A newly fake_quantized per channel ``torch.float32`` tensor + + Example:: + + >>> x = torch.randn(2, 2, 2) + >>> x + tensor([[[-0.2525, -0.0466], + [ 0.3491, -0.2168]], + + [[-0.5906, 1.6258], + [ 0.6444, -0.0542]]]) + >>> scales = (torch.randn(2) + 1) * 0.05 + >>> scales + tensor([0.0475, 0.0486]) + >>> zero_points = torch.zeros(2).to(torch.int32) + >>> zero_points + tensor([0, 0]) + >>> torch.fake_quantize_per_channel_affine(x, scales, zero_points, 1, 0, 255) + tensor([[[0.0000, 0.0000], + [0.3405, 0.0000]], + + [[0.0000, 1.6134], + [0.6323, 0.0000]]]) + """ + +@overload +def fake_quantize_per_tensor_affine( + input: Tensor, + scale: _float, + zero_point: _int, + quant_min: _int, + quant_max: _int, +) -> Tensor: + r""" + fake_quantize_per_tensor_affine(input, scale, zero_point, quant_min, quant_max) -> Tensor + + Returns a new tensor with the data in :attr:`input` fake quantized using :attr:`scale`, + :attr:`zero_point`, :attr:`quant_min` and :attr:`quant_max`. + + .. math:: + \text{output} = ( + min( + \text{quant\_max}, + max( + \text{quant\_min}, + \text{std::nearby\_int}(\text{input} / \text{scale}) + \text{zero\_point} + ) + ) - \text{zero\_point} + ) \times \text{scale} + + Args: + input (Tensor): the input value(s), ``torch.float32`` tensor + scale (double scalar or ``float32`` Tensor): quantization scale + zero_point (int64 scalar or ``int32`` Tensor): quantization zero_point + quant_min (int64): lower bound of the quantized domain + quant_max (int64): upper bound of the quantized domain + + Returns: + Tensor: A newly fake_quantized ``torch.float32`` tensor + + Example:: + + >>> x = torch.randn(4) + >>> x + tensor([ 0.0552, 0.9730, 0.3973, -1.0780]) + >>> torch.fake_quantize_per_tensor_affine(x, 0.1, 0, 0, 255) + tensor([0.1000, 1.0000, 0.4000, 0.0000]) + >>> torch.fake_quantize_per_tensor_affine(x, torch.tensor(0.1), torch.tensor(0), 0, 255) + tensor([0.1000, 1.0000, 0.4000, 0.0000]) + """ + +@overload +def fake_quantize_per_tensor_affine( + input: Tensor, + scale: Tensor, + zero_point: Tensor, + quant_min: _int, + quant_max: _int, +) -> Tensor: + r""" + fake_quantize_per_tensor_affine(input, scale, zero_point, quant_min, quant_max) -> Tensor + + Returns a new tensor with the data in :attr:`input` fake quantized using :attr:`scale`, + :attr:`zero_point`, :attr:`quant_min` and :attr:`quant_max`. + + .. math:: + \text{output} = ( + min( + \text{quant\_max}, + max( + \text{quant\_min}, + \text{std::nearby\_int}(\text{input} / \text{scale}) + \text{zero\_point} + ) + ) - \text{zero\_point} + ) \times \text{scale} + + Args: + input (Tensor): the input value(s), ``torch.float32`` tensor + scale (double scalar or ``float32`` Tensor): quantization scale + zero_point (int64 scalar or ``int32`` Tensor): quantization zero_point + quant_min (int64): lower bound of the quantized domain + quant_max (int64): upper bound of the quantized domain + + Returns: + Tensor: A newly fake_quantized ``torch.float32`` tensor + + Example:: + + >>> x = torch.randn(4) + >>> x + tensor([ 0.0552, 0.9730, 0.3973, -1.0780]) + >>> torch.fake_quantize_per_tensor_affine(x, 0.1, 0, 0, 255) + tensor([0.1000, 1.0000, 0.4000, 0.0000]) + >>> torch.fake_quantize_per_tensor_affine(x, torch.tensor(0.1), torch.tensor(0), 0, 255) + tensor([0.1000, 1.0000, 0.4000, 0.0000]) + """ + +@overload +def fbgemm_linear_fp16_weight( + input: Tensor, + packed_weight: Tensor, + bias: Tensor, +) -> Tensor: ... +@overload +def fbgemm_linear_fp16_weight( + input: Tensor, + packed_weight: Tensor, + bias: Tensor, + output: Tensor, +) -> Tensor: ... +@overload +def fbgemm_linear_fp16_weight_fp32_activation( + input: Tensor, + packed_weight: Tensor, + bias: Tensor | None, +) -> Tensor: ... +@overload +def fbgemm_linear_fp16_weight_fp32_activation( + input: Tensor, + packed_weight: Tensor, + bias: Tensor | None, + output: Tensor, +) -> Tensor: ... +def fbgemm_linear_int8_weight( + input: Tensor, + weight: Tensor, + packed: Tensor, + col_offsets: Tensor, + weight_scale: Number | _complex, + weight_zero_point: Number | _complex, + bias: Tensor, +) -> Tensor: ... +def fbgemm_linear_int8_weight_fp32_activation( + input: Tensor, + weight: Tensor, + packed: Tensor, + col_offsets: Tensor, + weight_scale: Number | _complex, + weight_zero_point: Number | _complex, + bias: Tensor, +) -> Tensor: ... +def fbgemm_linear_quantize_weight( + input: Tensor, +) -> tuple[Tensor, Tensor, _float, _int]: ... +def fbgemm_pack_gemm_matrix_fp16(input: Tensor) -> Tensor: ... +@overload +def fbgemm_pack_quantized_matrix(input: Tensor) -> Tensor: ... +@overload +def fbgemm_pack_quantized_matrix(input: Tensor, K: _int, N: _int) -> Tensor: ... +def feature_alpha_dropout(input: Tensor, p: _float, train: _bool) -> Tensor: ... +def feature_alpha_dropout_( + input: Tensor, + p: _float, + train: _bool, +) -> Tensor: ... +def feature_dropout(input: Tensor, p: _float, train: _bool) -> Tensor: ... +def feature_dropout_(input: Tensor, p: _float, train: _bool) -> Tensor: ... +@overload +def fill(input: Tensor, value: Tensor) -> Tensor: ... +@overload +def fill(input: Tensor, value: Number | _complex) -> Tensor: ... +@overload +def fill_(input: Tensor, value: Tensor) -> Tensor: ... +@overload +def fill_(input: Tensor, value: Number | _complex) -> Tensor: ... +def fix(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + fix(input, *, out=None) -> Tensor + + Alias for :func:`torch.trunc` + """ + +def fix_(input: Tensor) -> Tensor: ... +@overload +def flatten( + input: Tensor, + start_dim: _int = 0, + end_dim: _int = -1, +) -> Tensor: + r""" + flatten(input, start_dim=0, end_dim=-1) -> Tensor + + Flattens :attr:`input` by reshaping it into a one-dimensional tensor. If :attr:`start_dim` or :attr:`end_dim` + are passed, only dimensions starting with :attr:`start_dim` and ending with :attr:`end_dim` are flattened. + The order of elements in :attr:`input` is unchanged. + + Unlike NumPy's flatten, which always copies input's data, this function may return the original object, a view, + or copy. If no dimensions are flattened, then the original object :attr:`input` is returned. Otherwise, if input can + be viewed as the flattened shape, then that view is returned. Finally, only if the input cannot be viewed as the + flattened shape is input's data copied. See :meth:`torch.Tensor.view` for details on when a view will be returned. + + .. note:: + Flattening a zero-dimensional tensor will return a one-dimensional view. + + Args: + input (Tensor): the input tensor. + start_dim (int): the first dim to flatten + end_dim (int): the last dim to flatten + + Example:: + + >>> t = torch.tensor([[[1, 2], + ... [3, 4]], + ... [[5, 6], + ... [7, 8]]]) + >>> torch.flatten(t) + tensor([1, 2, 3, 4, 5, 6, 7, 8]) + >>> torch.flatten(t, start_dim=1) + tensor([[1, 2, 3, 4], + [5, 6, 7, 8]]) + """ + +@overload +def flatten( + input: Tensor, + start_dim: _int, + end_dim: _int, + out_dim: str | EllipsisType | None, +) -> Tensor: + r""" + flatten(input, start_dim=0, end_dim=-1) -> Tensor + + Flattens :attr:`input` by reshaping it into a one-dimensional tensor. If :attr:`start_dim` or :attr:`end_dim` + are passed, only dimensions starting with :attr:`start_dim` and ending with :attr:`end_dim` are flattened. + The order of elements in :attr:`input` is unchanged. + + Unlike NumPy's flatten, which always copies input's data, this function may return the original object, a view, + or copy. If no dimensions are flattened, then the original object :attr:`input` is returned. Otherwise, if input can + be viewed as the flattened shape, then that view is returned. Finally, only if the input cannot be viewed as the + flattened shape is input's data copied. See :meth:`torch.Tensor.view` for details on when a view will be returned. + + .. note:: + Flattening a zero-dimensional tensor will return a one-dimensional view. + + Args: + input (Tensor): the input tensor. + start_dim (int): the first dim to flatten + end_dim (int): the last dim to flatten + + Example:: + + >>> t = torch.tensor([[[1, 2], + ... [3, 4]], + ... [[5, 6], + ... [7, 8]]]) + >>> torch.flatten(t) + tensor([1, 2, 3, 4, 5, 6, 7, 8]) + >>> torch.flatten(t, start_dim=1) + tensor([[1, 2, 3, 4], + [5, 6, 7, 8]]) + """ + +@overload +def flatten( + input: Tensor, + start_dim: str | EllipsisType | None, + end_dim: str | EllipsisType | None, + out_dim: str | EllipsisType | None, +) -> Tensor: + r""" + flatten(input, start_dim=0, end_dim=-1) -> Tensor + + Flattens :attr:`input` by reshaping it into a one-dimensional tensor. If :attr:`start_dim` or :attr:`end_dim` + are passed, only dimensions starting with :attr:`start_dim` and ending with :attr:`end_dim` are flattened. + The order of elements in :attr:`input` is unchanged. + + Unlike NumPy's flatten, which always copies input's data, this function may return the original object, a view, + or copy. If no dimensions are flattened, then the original object :attr:`input` is returned. Otherwise, if input can + be viewed as the flattened shape, then that view is returned. Finally, only if the input cannot be viewed as the + flattened shape is input's data copied. See :meth:`torch.Tensor.view` for details on when a view will be returned. + + .. note:: + Flattening a zero-dimensional tensor will return a one-dimensional view. + + Args: + input (Tensor): the input tensor. + start_dim (int): the first dim to flatten + end_dim (int): the last dim to flatten + + Example:: + + >>> t = torch.tensor([[[1, 2], + ... [3, 4]], + ... [[5, 6], + ... [7, 8]]]) + >>> torch.flatten(t) + tensor([1, 2, 3, 4, 5, 6, 7, 8]) + >>> torch.flatten(t, start_dim=1) + tensor([[1, 2, 3, 4], + [5, 6, 7, 8]]) + """ + +@overload +def flatten( + input: Tensor, + dims: Sequence[str | EllipsisType | None], + out_dim: str | EllipsisType | None, +) -> Tensor: + r""" + flatten(input, start_dim=0, end_dim=-1) -> Tensor + + Flattens :attr:`input` by reshaping it into a one-dimensional tensor. If :attr:`start_dim` or :attr:`end_dim` + are passed, only dimensions starting with :attr:`start_dim` and ending with :attr:`end_dim` are flattened. + The order of elements in :attr:`input` is unchanged. + + Unlike NumPy's flatten, which always copies input's data, this function may return the original object, a view, + or copy. If no dimensions are flattened, then the original object :attr:`input` is returned. Otherwise, if input can + be viewed as the flattened shape, then that view is returned. Finally, only if the input cannot be viewed as the + flattened shape is input's data copied. See :meth:`torch.Tensor.view` for details on when a view will be returned. + + .. note:: + Flattening a zero-dimensional tensor will return a one-dimensional view. + + Args: + input (Tensor): the input tensor. + start_dim (int): the first dim to flatten + end_dim (int): the last dim to flatten + + Example:: + + >>> t = torch.tensor([[[1, 2], + ... [3, 4]], + ... [[5, 6], + ... [7, 8]]]) + >>> torch.flatten(t) + tensor([1, 2, 3, 4, 5, 6, 7, 8]) + >>> torch.flatten(t, start_dim=1) + tensor([[1, 2, 3, 4], + [5, 6, 7, 8]]) + """ + +def flip(input: Tensor, dims: _size) -> Tensor: + r""" + flip(input, dims) -> Tensor + + Reverse the order of an n-D tensor along given axis in dims. + + .. note:: + `torch.flip` makes a copy of :attr:`input`'s data. This is different from NumPy's `np.flip`, + which returns a view in constant time. Since copying a tensor's data is more work than viewing that data, + `torch.flip` is expected to be slower than `np.flip`. + + Args: + input (Tensor): the input tensor. + dims (a list or tuple): axis to flip on + + Example:: + + >>> x = torch.arange(8).view(2, 2, 2) + >>> x + tensor([[[ 0, 1], + [ 2, 3]], + + [[ 4, 5], + [ 6, 7]]]) + >>> torch.flip(x, [0, 1]) + tensor([[[ 6, 7], + [ 4, 5]], + + [[ 2, 3], + [ 0, 1]]]) + """ + +def fliplr(input: Tensor) -> Tensor: + r""" + fliplr(input) -> Tensor + + Flip tensor in the left/right direction, returning a new tensor. + + Flip the entries in each row in the left/right direction. + Columns are preserved, but appear in a different order than before. + + Note: + Requires the tensor to be at least 2-D. + + .. note:: + `torch.fliplr` makes a copy of :attr:`input`'s data. This is different from NumPy's `np.fliplr`, + which returns a view in constant time. Since copying a tensor's data is more work than viewing that data, + `torch.fliplr` is expected to be slower than `np.fliplr`. + + Args: + input (Tensor): Must be at least 2-dimensional. + + Example:: + + >>> x = torch.arange(4).view(2, 2) + >>> x + tensor([[0, 1], + [2, 3]]) + >>> torch.fliplr(x) + tensor([[1, 0], + [3, 2]]) + """ + +def flipud(input: Tensor) -> Tensor: + r""" + flipud(input) -> Tensor + + Flip tensor in the up/down direction, returning a new tensor. + + Flip the entries in each column in the up/down direction. + Rows are preserved, but appear in a different order than before. + + Note: + Requires the tensor to be at least 1-D. + + .. note:: + `torch.flipud` makes a copy of :attr:`input`'s data. This is different from NumPy's `np.flipud`, + which returns a view in constant time. Since copying a tensor's data is more work than viewing that data, + `torch.flipud` is expected to be slower than `np.flipud`. + + Args: + input (Tensor): Must be at least 1-dimensional. + + Example:: + + >>> x = torch.arange(4).view(2, 2) + >>> x + tensor([[0, 1], + [2, 3]]) + >>> torch.flipud(x) + tensor([[2, 3], + [0, 1]]) + """ + +@overload +def float_power( + input: Tensor, + exponent: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + float_power(input, exponent, *, out=None) -> Tensor + + Raises :attr:`input` to the power of :attr:`exponent`, elementwise, in double precision. + If neither input is complex returns a ``torch.float64`` tensor, + and if one or more inputs is complex returns a ``torch.complex128`` tensor. + + .. note:: + This function always computes in double precision, unlike :func:`torch.pow`, + which implements more typical :ref:`type promotion `. + This is useful when the computation needs to be performed in a wider or more precise dtype, + or the results of the computation may contain fractional values not representable in the input dtypes, + like when an integer base is raised to a negative integer exponent. + + Args: + input (Tensor or Number): the base value(s) + exponent (Tensor or Number): the exponent value(s) + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randint(10, (4,)) + >>> a + tensor([6, 4, 7, 1]) + >>> torch.float_power(a, 2) + tensor([36., 16., 49., 1.], dtype=torch.float64) + + >>> a = torch.arange(1, 5) + >>> a + tensor([ 1, 2, 3, 4]) + >>> exp = torch.tensor([2, -3, 4, -5]) + >>> exp + tensor([ 2, -3, 4, -5]) + >>> torch.float_power(a, exp) + tensor([1.0000e+00, 1.2500e-01, 8.1000e+01, 9.7656e-04], dtype=torch.float64) + """ + +@overload +def float_power( + self: Number | _complex, + exponent: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + float_power(input, exponent, *, out=None) -> Tensor + + Raises :attr:`input` to the power of :attr:`exponent`, elementwise, in double precision. + If neither input is complex returns a ``torch.float64`` tensor, + and if one or more inputs is complex returns a ``torch.complex128`` tensor. + + .. note:: + This function always computes in double precision, unlike :func:`torch.pow`, + which implements more typical :ref:`type promotion `. + This is useful when the computation needs to be performed in a wider or more precise dtype, + or the results of the computation may contain fractional values not representable in the input dtypes, + like when an integer base is raised to a negative integer exponent. + + Args: + input (Tensor or Number): the base value(s) + exponent (Tensor or Number): the exponent value(s) + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randint(10, (4,)) + >>> a + tensor([6, 4, 7, 1]) + >>> torch.float_power(a, 2) + tensor([36., 16., 49., 1.], dtype=torch.float64) + + >>> a = torch.arange(1, 5) + >>> a + tensor([ 1, 2, 3, 4]) + >>> exp = torch.tensor([2, -3, 4, -5]) + >>> exp + tensor([ 2, -3, 4, -5]) + >>> torch.float_power(a, exp) + tensor([1.0000e+00, 1.2500e-01, 8.1000e+01, 9.7656e-04], dtype=torch.float64) + """ + +@overload +def float_power( + input: Tensor, + exponent: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + float_power(input, exponent, *, out=None) -> Tensor + + Raises :attr:`input` to the power of :attr:`exponent`, elementwise, in double precision. + If neither input is complex returns a ``torch.float64`` tensor, + and if one or more inputs is complex returns a ``torch.complex128`` tensor. + + .. note:: + This function always computes in double precision, unlike :func:`torch.pow`, + which implements more typical :ref:`type promotion `. + This is useful when the computation needs to be performed in a wider or more precise dtype, + or the results of the computation may contain fractional values not representable in the input dtypes, + like when an integer base is raised to a negative integer exponent. + + Args: + input (Tensor or Number): the base value(s) + exponent (Tensor or Number): the exponent value(s) + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randint(10, (4,)) + >>> a + tensor([6, 4, 7, 1]) + >>> torch.float_power(a, 2) + tensor([36., 16., 49., 1.], dtype=torch.float64) + + >>> a = torch.arange(1, 5) + >>> a + tensor([ 1, 2, 3, 4]) + >>> exp = torch.tensor([2, -3, 4, -5]) + >>> exp + tensor([ 2, -3, 4, -5]) + >>> torch.float_power(a, exp) + tensor([1.0000e+00, 1.2500e-01, 8.1000e+01, 9.7656e-04], dtype=torch.float64) + """ + +def floor(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + floor(input, *, out=None) -> Tensor + + Returns a new tensor with the floor of the elements of :attr:`input`, + the largest integer less than or equal to each element. + + For integer inputs, follows the array-api convention of returning a + copy of the input tensor. + + .. math:: + \text{out}_{i} = \left\lfloor \text{input}_{i} \right\rfloor + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-0.8166, 1.5308, -0.2530, -0.2091]) + >>> torch.floor(a) + tensor([-1., 1., -1., -1.]) + """ + +def floor_(input: Tensor) -> Tensor: ... +def floor_divide( + input: Tensor | Number, + other: Tensor | Number, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + floor_divide(input, other, *, out=None) -> Tensor + + .. note:: + + Before PyTorch 1.13 :func:`torch.floor_divide` incorrectly performed + truncation division. To restore the previous behavior use + :func:`torch.div` with ``rounding_mode='trunc'``. + + Computes :attr:`input` divided by :attr:`other`, elementwise, and floors + the result. + + .. math:: + \text{{out}}_i = \text{floor} \left( \frac{{\text{{input}}_i}}{{\text{{other}}_i}} \right) + + + + Supports broadcasting to a common shape, type promotion, and integer and float inputs. + + Args: + input (Tensor or Number): the dividend + other (Tensor or Number): the divisor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([4.0, 3.0]) + >>> b = torch.tensor([2.0, 2.0]) + >>> torch.floor_divide(a, b) + tensor([2.0, 1.0]) + >>> torch.floor_divide(a, 1.4) + tensor([2.0, 2.0]) + """ + +def fmax( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + fmax(input, other, *, out=None) -> Tensor + + Computes the element-wise maximum of :attr:`input` and :attr:`other`. + + This is like :func:`torch.maximum` except it handles NaNs differently: + if exactly one of the two elements being compared is a NaN then the non-NaN element is taken as the maximum. + Only if both elements are NaN is NaN propagated. + + This function is a wrapper around C++'s ``std::fmax`` and is similar to NumPy's ``fmax`` function. + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer and floating-point inputs. + + Args: + input (Tensor): the input tensor. + other (Tensor): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([9.7, float('nan'), 3.1, float('nan')]) + >>> b = torch.tensor([-2.2, 0.5, float('nan'), float('nan')]) + >>> torch.fmax(a, b) + tensor([9.7000, 0.5000, 3.1000, nan]) + """ + +def fmin( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + fmin(input, other, *, out=None) -> Tensor + + Computes the element-wise minimum of :attr:`input` and :attr:`other`. + + This is like :func:`torch.minimum` except it handles NaNs differently: + if exactly one of the two elements being compared is a NaN then the non-NaN element is taken as the minimum. + Only if both elements are NaN is NaN propagated. + + This function is a wrapper around C++'s ``std::fmin`` and is similar to NumPy's ``fmin`` function. + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer and floating-point inputs. + + Args: + input (Tensor): the input tensor. + other (Tensor): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([2.2, float('nan'), 2.1, float('nan')]) + >>> b = torch.tensor([-9.3, 0.1, float('nan'), float('nan')]) + >>> torch.fmin(a, b) + tensor([-9.3000, 0.1000, 2.1000, nan]) + """ + +@overload +def fmod( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + fmod(input, other, *, out=None) -> Tensor + + Applies C++'s `std::fmod `_ entrywise. + The result has the same sign as the dividend :attr:`input` and its absolute value + is less than that of :attr:`other`. + + This function may be defined in terms of :func:`torch.div` as + + .. code:: python + + torch.fmod(a, b) == a - a.div(b, rounding_mode="trunc") * b + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer and float inputs. + + .. note:: + + When the divisor is zero, returns ``NaN`` for floating point dtypes + on both CPU and GPU; raises ``RuntimeError`` for integer division by + zero on CPU; Integer division by zero on GPU may return any value. + + .. note:: + + Complex inputs are not supported. In some cases, it is not mathematically + possible to satisfy the definition of a modulo operation with complex numbers. + + .. seealso:: + + :func:`torch.remainder` which implements Python's modulus operator. + This one is defined using division rounding down the result. + + Args: + input (Tensor): the dividend + other (Tensor or Scalar): the divisor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.fmod(torch.tensor([-3., -2, -1, 1, 2, 3]), 2) + tensor([-1., -0., -1., 1., 0., 1.]) + >>> torch.fmod(torch.tensor([1, 2, 3, 4, 5]), -1.5) + tensor([1.0000, 0.5000, 0.0000, 1.0000, 0.5000]) + """ + +@overload +def fmod( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + fmod(input, other, *, out=None) -> Tensor + + Applies C++'s `std::fmod `_ entrywise. + The result has the same sign as the dividend :attr:`input` and its absolute value + is less than that of :attr:`other`. + + This function may be defined in terms of :func:`torch.div` as + + .. code:: python + + torch.fmod(a, b) == a - a.div(b, rounding_mode="trunc") * b + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer and float inputs. + + .. note:: + + When the divisor is zero, returns ``NaN`` for floating point dtypes + on both CPU and GPU; raises ``RuntimeError`` for integer division by + zero on CPU; Integer division by zero on GPU may return any value. + + .. note:: + + Complex inputs are not supported. In some cases, it is not mathematically + possible to satisfy the definition of a modulo operation with complex numbers. + + .. seealso:: + + :func:`torch.remainder` which implements Python's modulus operator. + This one is defined using division rounding down the result. + + Args: + input (Tensor): the dividend + other (Tensor or Scalar): the divisor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.fmod(torch.tensor([-3., -2, -1, 1, 2, 3]), 2) + tensor([-1., -0., -1., 1., 0., 1.]) + >>> torch.fmod(torch.tensor([1, 2, 3, 4, 5]), -1.5) + tensor([1.0000, 0.5000, 0.0000, 1.0000, 0.5000]) + """ + +def frac(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + frac(input, *, out=None) -> Tensor + + Computes the fractional portion of each element in :attr:`input`. + + .. math:: + \text{out}_{i} = \text{input}_{i} - \left\lfloor |\text{input}_{i}| \right\rfloor * \operatorname{sgn}(\text{input}_{i}) + + Example:: + + >>> torch.frac(torch.tensor([1, 2.5, -3.2])) + tensor([ 0.0000, 0.5000, -0.2000]) + """ + +def frac_(input: Tensor) -> Tensor: ... +def frexp( + input: Tensor, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.frexp: + r""" + frexp(input, *, out=None) -> (Tensor mantissa, Tensor exponent) + + Decomposes :attr:`input` into mantissa and exponent tensors + such that :math:`\text{input} = \text{mantissa} \times 2^{\text{exponent}}`. + + The range of mantissa is the open interval (-1, 1). + + Supports float inputs. + + Args: + input (Tensor): the input tensor + + + Keyword args: + out (tuple, optional): the output tensors + + Example:: + + >>> x = torch.arange(9.) + >>> mantissa, exponent = torch.frexp(x) + >>> mantissa + tensor([0.0000, 0.5000, 0.5000, 0.7500, 0.5000, 0.6250, 0.7500, 0.8750, 0.5000]) + >>> exponent + tensor([0, 1, 2, 2, 3, 3, 3, 3, 4], dtype=torch.int32) + >>> torch.ldexp(mantissa, exponent) + tensor([0., 1., 2., 3., 4., 5., 6., 7., 8.]) + """ + +def frobenius_norm( + input: Tensor, + dim: _int | _size, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: ... +def from_file( + filename: str, + shared: _bool | None = None, + size: _int | None = 0, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + from_file(filename, shared=None, size=0, *, dtype=None, layout=None, device=None, pin_memory=False) + + Creates a CPU tensor with a storage backed by a memory-mapped file. + + If ``shared`` is True, then memory is shared between processes. All changes are written to the file. + If ``shared`` is False, then changes to the tensor do not affect the file. + + ``size`` is the number of elements in the Tensor. If ``shared`` is ``False``, then the file must contain + at least ``size * sizeof(dtype)`` bytes. If ``shared`` is ``True`` the file will be created if needed. + + .. note:: + Only CPU tensors can be mapped to files. + + .. note:: + For now, tensors with storages backed by a memory-mapped file cannot be created in pinned memory. + + + Args: + filename (str): file name to map + shared (bool): whether to share memory (whether ``MAP_SHARED`` or ``MAP_PRIVATE`` is passed to the + underlying `mmap(2) call `_) + size (int): number of elements in the tensor + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> t = torch.randn(2, 5, dtype=torch.float64) + >>> t.numpy().tofile('storage.pt') + >>> t_mapped = torch.from_file('storage.pt', shared=False, size=10, dtype=torch.float64) + """ + +def from_numpy(ndarray) -> Tensor: + r""" + from_numpy(ndarray) -> Tensor + + Creates a :class:`Tensor` from a :class:`numpy.ndarray`. + + The returned tensor and :attr:`ndarray` share the same memory. Modifications to + the tensor will be reflected in the :attr:`ndarray` and vice versa. The returned + tensor is not resizable. + + It currently accepts :attr:`ndarray` with dtypes of ``numpy.float64``, + ``numpy.float32``, ``numpy.float16``, ``numpy.complex64``, ``numpy.complex128``, + ``numpy.int64``, ``numpy.int32``, ``numpy.int16``, ``numpy.int8``, ``numpy.uint8``, + and ``bool``. + + .. warning:: + Writing to a tensor created from a read-only NumPy array is not supported and will result in undefined behavior. + + Example:: + + >>> a = numpy.array([1, 2, 3]) + >>> t = torch.from_numpy(a) + >>> t + tensor([ 1, 2, 3]) + >>> t[0] = -1 + >>> a + array([-1, 2, 3]) + """ + +def frombuffer( + buffer: Any, + *, + dtype: _dtype, + count: int = -1, + offset: int = 0, + requires_grad: _bool = False, +) -> Tensor: + r""" + frombuffer(buffer, *, dtype, count=-1, offset=0, requires_grad=False) -> Tensor + + Creates a 1-dimensional :class:`Tensor` from an object that implements + the Python buffer protocol. + + Skips the first :attr:`offset` bytes in the buffer, and interprets the rest of + the raw bytes as a 1-dimensional tensor of type :attr:`dtype` with :attr:`count` + elements. + + Note that either of the following must be true: + + 1. :attr:`count` is a positive non-zero number, and the total number of bytes + in the buffer is more than :attr:`offset` plus :attr:`count` times the size + (in bytes) of :attr:`dtype`. + + 2. :attr:`count` is negative, and the length (number of bytes) of the buffer + subtracted by the :attr:`offset` is a multiple of the size (in bytes) of + :attr:`dtype`. + + The returned tensor and buffer share the same memory. Modifications to + the tensor will be reflected in the buffer and vice versa. The returned + tensor is not resizable. + + .. note:: + This function increments the reference count for the object that + owns the shared memory. Therefore, such memory will not be deallocated + before the returned tensor goes out of scope. + + .. warning:: + This function's behavior is undefined when passed an object implementing + the buffer protocol whose data is not on the CPU. Doing so is likely to + cause a segmentation fault. + + .. warning:: + This function does not try to infer the :attr:`dtype` (hence, it is not + optional). Passing a different :attr:`dtype` than its source may result + in unexpected behavior. + + Args: + buffer (object): a Python object that exposes the buffer interface. + + Keyword args: + dtype (:class:`torch.dtype`): the desired data type of returned tensor. + count (int, optional): the number of desired elements to be read. + If negative, all the elements (until the end of the buffer) will be + read. Default: -1. + offset (int, optional): the number of bytes to skip at the start of + the buffer. Default: 0. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> import array + >>> a = array.array('i', [1, 2, 3]) + >>> t = torch.frombuffer(a, dtype=torch.int32) + >>> t + tensor([ 1, 2, 3]) + >>> t[0] = -1 + >>> a + array([-1, 2, 3]) + + >>> # Interprets the signed char bytes as 32-bit integers. + >>> # Each 4 signed char elements will be interpreted as + >>> # 1 signed 32-bit integer. + >>> import array + >>> a = array.array('b', [-1, 0, 0, 0]) + >>> torch.frombuffer(a, dtype=torch.int32) + tensor([255], dtype=torch.int32) + """ + +@overload +def full( + size: _size, + fill_value: Number | _complex, + *, + out: Tensor | None = None, + layout: _layout = strided, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + full(size, fill_value, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Creates a tensor of size :attr:`size` filled with :attr:`fill_value`. The + tensor's dtype is inferred from :attr:`fill_value`. + + Args: + size (int...): a list, tuple, or :class:`torch.Size` of integers defining the + shape of the output tensor. + fill_value (Scalar): the value to fill the output tensor with. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.full((2, 3), 3.141592) + tensor([[ 3.1416, 3.1416, 3.1416], + [ 3.1416, 3.1416, 3.1416]]) + """ + +@overload +def full( + size: _size, + fill_value: Number | _complex, + *, + names: list[str | None], + layout: _layout = strided, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + full(size, fill_value, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Creates a tensor of size :attr:`size` filled with :attr:`fill_value`. The + tensor's dtype is inferred from :attr:`fill_value`. + + Args: + size (int...): a list, tuple, or :class:`torch.Size` of integers defining the + shape of the output tensor. + fill_value (Scalar): the value to fill the output tensor with. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.full((2, 3), 3.141592) + tensor([[ 3.1416, 3.1416, 3.1416], + [ 3.1416, 3.1416, 3.1416]]) + """ + +@overload +def full( + size: Sequence[_int | SymInt], + fill_value: Number | _complex, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + full(size, fill_value, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Creates a tensor of size :attr:`size` filled with :attr:`fill_value`. The + tensor's dtype is inferred from :attr:`fill_value`. + + Args: + size (int...): a list, tuple, or :class:`torch.Size` of integers defining the + shape of the output tensor. + fill_value (Scalar): the value to fill the output tensor with. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.full((2, 3), 3.141592) + tensor([[ 3.1416, 3.1416, 3.1416], + [ 3.1416, 3.1416, 3.1416]]) + """ + +@overload +def full( + size: _size, + fill_value: Number | _complex, + *, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + full(size, fill_value, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Creates a tensor of size :attr:`size` filled with :attr:`fill_value`. The + tensor's dtype is inferred from :attr:`fill_value`. + + Args: + size (int...): a list, tuple, or :class:`torch.Size` of integers defining the + shape of the output tensor. + fill_value (Scalar): the value to fill the output tensor with. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.full((2, 3), 3.141592) + tensor([[ 3.1416, 3.1416, 3.1416], + [ 3.1416, 3.1416, 3.1416]]) + """ + +def full_like( + input: Tensor, + fill_value: Number | _complex, + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + full_like(input, fill_value, \*, dtype=None, layout=torch.strided, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same size as :attr:`input` filled with :attr:`fill_value`. + ``torch.full_like(input, fill_value)`` is equivalent to + ``torch.full(input.size(), fill_value, dtype=input.dtype, layout=input.layout, device=input.device)``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + fill_value: the number to fill the output tensor with. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + + Example:: + + >>> x = torch.ones(2, 3) + >>> torch.full_like(x, 3.141592) + tensor([[ 3.1416, 3.1416, 3.1416], + [ 3.1416, 3.1416, 3.1416]]) + >>> torch.full_like(x, 7) + tensor([[7., 7., 7.], + [7., 7., 7.]]) + >>> torch.full_like(x, 0.5, dtype=torch.int32) + tensor([[0, 0, 0], + [0, 0, 0]], dtype=torch.int32) + >>> y = torch.randn(3, 4, dtype=torch.float64) + >>> torch.full_like(y, -1.0) + tensor([[-1., -1., -1., -1.], + [-1., -1., -1., -1.], + [-1., -1., -1., -1.]], dtype=torch.float64) + """ + +def fused_moving_avg_obs_fake_quant( + input: Tensor, + observer_on: Tensor, + fake_quant_on: Tensor, + running_min: Tensor, + running_max: Tensor, + scale: Tensor, + zero_point: Tensor, + averaging_const: _float, + quant_min: _int, + quant_max: _int, + ch_axis: _int, + per_row_fake_quant: _bool = False, + symmetric_quant: _bool = False, +) -> Tensor: ... +@overload +def gather( + input: Tensor, + dim: _int, + index: Tensor, + *, + sparse_grad: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + gather(input, dim, index, *, sparse_grad=False, out=None) -> Tensor + + Gathers values along an axis specified by `dim`. + + For a 3-D tensor the output is specified by:: + + out[i][j][k] = input[index[i][j][k]][j][k] # if dim == 0 + out[i][j][k] = input[i][index[i][j][k]][k] # if dim == 1 + out[i][j][k] = input[i][j][index[i][j][k]] # if dim == 2 + + :attr:`input` and :attr:`index` must have the same number of dimensions. + It is also required that ``index.size(d) <= input.size(d)`` for all + dimensions ``d != dim``. :attr:`out` will have the same shape as :attr:`index`. + Note that ``input`` and ``index`` do not broadcast against each other. + When :attr:`index` is empty, we always return an empty output with the same shape + without further error checking. + + Args: + input (Tensor): the source tensor + dim (int): the axis along which to index + index (LongTensor): the indices of elements to gather + + Keyword arguments: + sparse_grad (bool, optional): If ``True``, gradient w.r.t. :attr:`input` will be a sparse tensor. + out (Tensor, optional): the destination tensor + + Example:: + + >>> t = torch.tensor([[1, 2], [3, 4]]) + >>> torch.gather(t, 1, torch.tensor([[0, 0], [1, 0]])) + tensor([[ 1, 1], + [ 4, 3]]) + """ + +@overload +def gather( + input: Tensor, + dim: str | EllipsisType | None, + index: Tensor, + *, + sparse_grad: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + gather(input, dim, index, *, sparse_grad=False, out=None) -> Tensor + + Gathers values along an axis specified by `dim`. + + For a 3-D tensor the output is specified by:: + + out[i][j][k] = input[index[i][j][k]][j][k] # if dim == 0 + out[i][j][k] = input[i][index[i][j][k]][k] # if dim == 1 + out[i][j][k] = input[i][j][index[i][j][k]] # if dim == 2 + + :attr:`input` and :attr:`index` must have the same number of dimensions. + It is also required that ``index.size(d) <= input.size(d)`` for all + dimensions ``d != dim``. :attr:`out` will have the same shape as :attr:`index`. + Note that ``input`` and ``index`` do not broadcast against each other. + When :attr:`index` is empty, we always return an empty output with the same shape + without further error checking. + + Args: + input (Tensor): the source tensor + dim (int): the axis along which to index + index (LongTensor): the indices of elements to gather + + Keyword arguments: + sparse_grad (bool, optional): If ``True``, gradient w.r.t. :attr:`input` will be a sparse tensor. + out (Tensor, optional): the destination tensor + + Example:: + + >>> t = torch.tensor([[1, 2], [3, 4]]) + >>> torch.gather(t, 1, torch.tensor([[0, 0], [1, 0]])) + tensor([[ 1, 1], + [ 4, 3]]) + """ + +def gcd( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + gcd(input, other, *, out=None) -> Tensor + + Computes the element-wise greatest common divisor (GCD) of :attr:`input` and :attr:`other`. + + Both :attr:`input` and :attr:`other` must have integer types. + + .. note:: + This defines :math:`gcd(0, 0) = 0`. + + Args: + input (Tensor): the input tensor. + other (Tensor): the second input tensor + + Keyword arguments: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([5, 10, 15]) + >>> b = torch.tensor([3, 4, 5]) + >>> torch.gcd(a, b) + tensor([1, 2, 5]) + >>> c = torch.tensor([3]) + >>> torch.gcd(a, c) + tensor([1, 1, 3]) + """ + +def gcd_(input: Tensor, other: Tensor) -> Tensor: ... +@overload +def ge( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + ge(input, other, *, out=None) -> Tensor + + Computes :math:`\text{input} \geq \text{other}` element-wise. + + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or float): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is greater than or equal to :attr:`other` and False elsewhere + + Example:: + + >>> torch.ge(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[True, True], [False, True]]) + """ + +@overload +def ge( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + ge(input, other, *, out=None) -> Tensor + + Computes :math:`\text{input} \geq \text{other}` element-wise. + + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or float): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is greater than or equal to :attr:`other` and False elsewhere + + Example:: + + >>> torch.ge(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[True, True], [False, True]]) + """ + +def geqrf( + input: Tensor, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.geqrf: + r""" + geqrf(input, *, out=None) -> (Tensor, Tensor) + + This is a low-level function for calling LAPACK's geqrf directly. This function + returns a namedtuple (a, tau) as defined in `LAPACK documentation for geqrf`_ . + + Computes a QR decomposition of :attr:`input`. + Both `Q` and `R` matrices are stored in the same output tensor `a`. + The elements of `R` are stored on and above the diagonal. + Elementary reflectors (or Householder vectors) implicitly defining matrix `Q` + are stored below the diagonal. + The results of this function can be used together with :func:`torch.linalg.householder_product` + to obtain the `Q` matrix or + with :func:`torch.ormqr`, which uses an implicit representation of the `Q` matrix, + for an efficient matrix-matrix multiplication. + + See `LAPACK documentation for geqrf`_ for further details. + + .. note:: + See also :func:`torch.linalg.qr`, which computes Q and R matrices, and :func:`torch.linalg.lstsq` + with the ``driver="gels"`` option for a function that can solve matrix equations using a QR decomposition. + + Args: + input (Tensor): the input matrix + + Keyword args: + out (tuple, optional): the output tuple of (Tensor, Tensor). Ignored if `None`. Default: `None`. + + .. _LAPACK documentation for geqrf: + http://www.netlib.org/lapack/explore-html/df/dc5/group__variants_g_ecomputational_ga3766ea903391b5cf9008132f7440ec7b.html + """ + +def ger( + input: Tensor, + vec2: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + ger(input, vec2, *, out=None) -> Tensor + + Alias of :func:`torch.outer`. + + .. warning:: + This function is deprecated and will be removed in a future PyTorch release. + Use :func:`torch.outer` instead. + """ + +def get_default_dtype() -> _dtype: + r""" + get_default_dtype() -> torch.dtype + + Get the current default floating point :class:`torch.dtype`. + + Example:: + + >>> torch.get_default_dtype() # initial default for floating point is torch.float32 + torch.float32 + >>> torch.set_default_dtype(torch.float64) + >>> torch.get_default_dtype() # default is now changed to torch.float64 + torch.float64 + """ + +def get_num_interop_threads() -> _int: + r""" + get_num_interop_threads() -> int + + Returns the number of threads used for inter-op parallelism on CPU + (e.g. in JIT interpreter) + """ + +def get_num_threads() -> _int: + r""" + get_num_threads() -> int + + Returns the number of threads used for parallelizing CPU operations + """ + +@overload +def gradient( + input: Tensor, + *, + spacing: Number | _complex | None = None, + dim: _int | None = None, + edge_order: _int = 1, +) -> tuple[Tensor, ...]: + r""" + gradient(input, *, spacing=1, dim=None, edge_order=1) -> List of Tensors + + Estimates the gradient of a function :math:`g : \mathbb{R}^n \rightarrow \mathbb{R}` in + one or more dimensions using the `second-order accurate central differences method + `_ and + either first or second order estimates at the boundaries. + + The gradient of :math:`g` is estimated using samples. By default, when :attr:`spacing` is not + specified, the samples are entirely described by :attr:`input`, and the mapping of input coordinates + to an output is the same as the tensor's mapping of indices to values. For example, for a three-dimensional + :attr:`input` the function described is :math:`g : \mathbb{R}^3 \rightarrow \mathbb{R}`, and + :math:`g(1, 2, 3)\ == input[1, 2, 3]`. + + When :attr:`spacing` is specified, it modifies the relationship between :attr:`input` and input coordinates. + This is detailed in the "Keyword Arguments" section below. + + The gradient is estimated by estimating each partial derivative of :math:`g` independently. This estimation is + accurate if :math:`g` is in :math:`C^3` (it has at least 3 continuous derivatives), and the estimation can be + improved by providing closer samples. Mathematically, the value at each interior point of a partial derivative + is estimated using `Taylor's theorem with remainder `_. + Letting :math:`x` be an interior point with :math:`x-h_l` and :math:`x+h_r` be points neighboring + it to the left and right respectively, :math:`f(x+h_r)` and :math:`f(x-h_l)` can be estimated using: + + .. math:: + \begin{aligned} + f(x+h_r) = f(x) + h_r f'(x) + {h_r}^2 \frac{f''(x)}{2} + {h_r}^3 \frac{f'''(\xi_1)}{6}, \xi_1 \in (x, x+h_r) \\ + f(x-h_l) = f(x) - h_l f'(x) + {h_l}^2 \frac{f''(x)}{2} - {h_l}^3 \frac{f'''(\xi_2)}{6}, \xi_2 \in (x, x-h_l) \\ + \end{aligned} + + Using the fact that :math:`f \in C^3` and solving the linear system, we derive: + + .. math:: + f'(x) \approx \frac{ {h_l}^2 f(x+h_r) - {h_r}^2 f(x-h_l) + + ({h_r}^2-{h_l}^2 ) f(x) }{ {h_r} {h_l}^2 + {h_r}^2 {h_l} } + + .. note:: + We estimate the gradient of functions in complex domain + :math:`g : \mathbb{C}^n \rightarrow \mathbb{C}` in the same way. + + The value of each partial derivative at the boundary points is computed differently. See edge_order below. + + Args: + input (``Tensor``): the tensor that represents the values of the function + + Keyword args: + spacing (``scalar``, ``list of scalar``, ``list of Tensor``, optional): :attr:`spacing` can be used to modify + how the :attr:`input` tensor's indices relate to sample coordinates. If :attr:`spacing` is a scalar then + the indices are multiplied by the scalar to produce the coordinates. For example, if :attr:`spacing=2` the + indices (1, 2, 3) become coordinates (2, 4, 6). If :attr:`spacing` is a list of scalars then the corresponding + indices are multiplied. For example, if :attr:`spacing=(2, -1, 3)` the indices (1, 2, 3) become coordinates (2, -2, 9). + Finally, if :attr:`spacing` is a list of one-dimensional tensors then each tensor specifies the coordinates for + the corresponding dimension. For example, if the indices are (1, 2, 3) and the tensors are (t0, t1, t2), then + the coordinates are (t0[1], t1[2], t2[3]) + + dim (``int``, ``list of int``, optional): the dimension or dimensions to approximate the gradient over. By default + the partial gradient in every dimension is computed. Note that when :attr:`dim` is specified the elements of + the :attr:`spacing` argument must correspond with the specified dims." + + edge_order (``int``, optional): 1 or 2, for `first-order + `_ or + `second-order `_ + estimation of the boundary ("edge") values, respectively. Note that when :attr:`edge_order` is specified, each + dimension size of :attr:`input` should be at least edge_order+1 + + Examples:: + + >>> # Estimates the gradient of f(x)=x^2 at points [-2, -1, 2, 4] + >>> coordinates = (torch.tensor([-2., -1., 1., 4.]),) + >>> values = torch.tensor([4., 1., 1., 16.], ) + >>> torch.gradient(values, spacing = coordinates) + (tensor([-3., -2., 2., 5.]),) + + >>> # Estimates the gradient of the R^2 -> R function whose samples are + >>> # described by the tensor t. Implicit coordinates are [0, 1] for the outermost + >>> # dimension and [0, 1, 2, 3] for the innermost dimension, and function estimates + >>> # partial derivative for both dimensions. + >>> t = torch.tensor([[1, 2, 4, 8], [10, 20, 40, 80]]) + >>> torch.gradient(t) + (tensor([[ 9., 18., 36., 72.], + [ 9., 18., 36., 72.]]), + tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]])) + + >>> # A scalar value for spacing modifies the relationship between tensor indices + >>> # and input coordinates by multiplying the indices to find the + >>> # coordinates. For example, below the indices of the innermost + >>> # 0, 1, 2, 3 translate to coordinates of [0, 2, 4, 6], and the indices of + >>> # the outermost dimension 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = 2.0) # dim = None (implicitly [0, 1]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.5000, 0.7500, 1.5000, 2.0000], + [ 5.0000, 7.5000, 15.0000, 20.0000]])) + >>> # doubling the spacing between samples halves the estimated partial gradients. + + >>> + >>> # Estimates only the partial derivative for dimension 1 + >>> torch.gradient(t, dim = 1) # spacing = None (implicitly 1.) + (tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]]),) + + >>> # When spacing is a list of scalars, the relationship between the tensor + >>> # indices and input coordinates changes based on dimension. + >>> # For example, below, the indices of the innermost dimension 0, 1, 2, 3 translate + >>> # to coordinates of [0, 3, 6, 9], and the indices of the outermost dimension + >>> # 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = [3., 2.]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + + >>> # The following example is a replication of the previous one with explicit + >>> # coordinates. + >>> coords = (torch.tensor([0, 2]), torch.tensor([0, 3, 6, 9])) + >>> torch.gradient(t, spacing = coords) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + """ + +@overload +def gradient( + input: Tensor, + *, + spacing: Sequence[Number | _complex], + dim: _int | None = None, + edge_order: _int = 1, +) -> tuple[Tensor, ...]: + r""" + gradient(input, *, spacing=1, dim=None, edge_order=1) -> List of Tensors + + Estimates the gradient of a function :math:`g : \mathbb{R}^n \rightarrow \mathbb{R}` in + one or more dimensions using the `second-order accurate central differences method + `_ and + either first or second order estimates at the boundaries. + + The gradient of :math:`g` is estimated using samples. By default, when :attr:`spacing` is not + specified, the samples are entirely described by :attr:`input`, and the mapping of input coordinates + to an output is the same as the tensor's mapping of indices to values. For example, for a three-dimensional + :attr:`input` the function described is :math:`g : \mathbb{R}^3 \rightarrow \mathbb{R}`, and + :math:`g(1, 2, 3)\ == input[1, 2, 3]`. + + When :attr:`spacing` is specified, it modifies the relationship between :attr:`input` and input coordinates. + This is detailed in the "Keyword Arguments" section below. + + The gradient is estimated by estimating each partial derivative of :math:`g` independently. This estimation is + accurate if :math:`g` is in :math:`C^3` (it has at least 3 continuous derivatives), and the estimation can be + improved by providing closer samples. Mathematically, the value at each interior point of a partial derivative + is estimated using `Taylor's theorem with remainder `_. + Letting :math:`x` be an interior point with :math:`x-h_l` and :math:`x+h_r` be points neighboring + it to the left and right respectively, :math:`f(x+h_r)` and :math:`f(x-h_l)` can be estimated using: + + .. math:: + \begin{aligned} + f(x+h_r) = f(x) + h_r f'(x) + {h_r}^2 \frac{f''(x)}{2} + {h_r}^3 \frac{f'''(\xi_1)}{6}, \xi_1 \in (x, x+h_r) \\ + f(x-h_l) = f(x) - h_l f'(x) + {h_l}^2 \frac{f''(x)}{2} - {h_l}^3 \frac{f'''(\xi_2)}{6}, \xi_2 \in (x, x-h_l) \\ + \end{aligned} + + Using the fact that :math:`f \in C^3` and solving the linear system, we derive: + + .. math:: + f'(x) \approx \frac{ {h_l}^2 f(x+h_r) - {h_r}^2 f(x-h_l) + + ({h_r}^2-{h_l}^2 ) f(x) }{ {h_r} {h_l}^2 + {h_r}^2 {h_l} } + + .. note:: + We estimate the gradient of functions in complex domain + :math:`g : \mathbb{C}^n \rightarrow \mathbb{C}` in the same way. + + The value of each partial derivative at the boundary points is computed differently. See edge_order below. + + Args: + input (``Tensor``): the tensor that represents the values of the function + + Keyword args: + spacing (``scalar``, ``list of scalar``, ``list of Tensor``, optional): :attr:`spacing` can be used to modify + how the :attr:`input` tensor's indices relate to sample coordinates. If :attr:`spacing` is a scalar then + the indices are multiplied by the scalar to produce the coordinates. For example, if :attr:`spacing=2` the + indices (1, 2, 3) become coordinates (2, 4, 6). If :attr:`spacing` is a list of scalars then the corresponding + indices are multiplied. For example, if :attr:`spacing=(2, -1, 3)` the indices (1, 2, 3) become coordinates (2, -2, 9). + Finally, if :attr:`spacing` is a list of one-dimensional tensors then each tensor specifies the coordinates for + the corresponding dimension. For example, if the indices are (1, 2, 3) and the tensors are (t0, t1, t2), then + the coordinates are (t0[1], t1[2], t2[3]) + + dim (``int``, ``list of int``, optional): the dimension or dimensions to approximate the gradient over. By default + the partial gradient in every dimension is computed. Note that when :attr:`dim` is specified the elements of + the :attr:`spacing` argument must correspond with the specified dims." + + edge_order (``int``, optional): 1 or 2, for `first-order + `_ or + `second-order `_ + estimation of the boundary ("edge") values, respectively. Note that when :attr:`edge_order` is specified, each + dimension size of :attr:`input` should be at least edge_order+1 + + Examples:: + + >>> # Estimates the gradient of f(x)=x^2 at points [-2, -1, 2, 4] + >>> coordinates = (torch.tensor([-2., -1., 1., 4.]),) + >>> values = torch.tensor([4., 1., 1., 16.], ) + >>> torch.gradient(values, spacing = coordinates) + (tensor([-3., -2., 2., 5.]),) + + >>> # Estimates the gradient of the R^2 -> R function whose samples are + >>> # described by the tensor t. Implicit coordinates are [0, 1] for the outermost + >>> # dimension and [0, 1, 2, 3] for the innermost dimension, and function estimates + >>> # partial derivative for both dimensions. + >>> t = torch.tensor([[1, 2, 4, 8], [10, 20, 40, 80]]) + >>> torch.gradient(t) + (tensor([[ 9., 18., 36., 72.], + [ 9., 18., 36., 72.]]), + tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]])) + + >>> # A scalar value for spacing modifies the relationship between tensor indices + >>> # and input coordinates by multiplying the indices to find the + >>> # coordinates. For example, below the indices of the innermost + >>> # 0, 1, 2, 3 translate to coordinates of [0, 2, 4, 6], and the indices of + >>> # the outermost dimension 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = 2.0) # dim = None (implicitly [0, 1]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.5000, 0.7500, 1.5000, 2.0000], + [ 5.0000, 7.5000, 15.0000, 20.0000]])) + >>> # doubling the spacing between samples halves the estimated partial gradients. + + >>> + >>> # Estimates only the partial derivative for dimension 1 + >>> torch.gradient(t, dim = 1) # spacing = None (implicitly 1.) + (tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]]),) + + >>> # When spacing is a list of scalars, the relationship between the tensor + >>> # indices and input coordinates changes based on dimension. + >>> # For example, below, the indices of the innermost dimension 0, 1, 2, 3 translate + >>> # to coordinates of [0, 3, 6, 9], and the indices of the outermost dimension + >>> # 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = [3., 2.]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + + >>> # The following example is a replication of the previous one with explicit + >>> # coordinates. + >>> coords = (torch.tensor([0, 2]), torch.tensor([0, 3, 6, 9])) + >>> torch.gradient(t, spacing = coords) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + """ + +@overload +def gradient( + input: Tensor, + *, + spacing: Sequence[Number | _complex], + dim: _size, + edge_order: _int = 1, +) -> tuple[Tensor, ...]: + r""" + gradient(input, *, spacing=1, dim=None, edge_order=1) -> List of Tensors + + Estimates the gradient of a function :math:`g : \mathbb{R}^n \rightarrow \mathbb{R}` in + one or more dimensions using the `second-order accurate central differences method + `_ and + either first or second order estimates at the boundaries. + + The gradient of :math:`g` is estimated using samples. By default, when :attr:`spacing` is not + specified, the samples are entirely described by :attr:`input`, and the mapping of input coordinates + to an output is the same as the tensor's mapping of indices to values. For example, for a three-dimensional + :attr:`input` the function described is :math:`g : \mathbb{R}^3 \rightarrow \mathbb{R}`, and + :math:`g(1, 2, 3)\ == input[1, 2, 3]`. + + When :attr:`spacing` is specified, it modifies the relationship between :attr:`input` and input coordinates. + This is detailed in the "Keyword Arguments" section below. + + The gradient is estimated by estimating each partial derivative of :math:`g` independently. This estimation is + accurate if :math:`g` is in :math:`C^3` (it has at least 3 continuous derivatives), and the estimation can be + improved by providing closer samples. Mathematically, the value at each interior point of a partial derivative + is estimated using `Taylor's theorem with remainder `_. + Letting :math:`x` be an interior point with :math:`x-h_l` and :math:`x+h_r` be points neighboring + it to the left and right respectively, :math:`f(x+h_r)` and :math:`f(x-h_l)` can be estimated using: + + .. math:: + \begin{aligned} + f(x+h_r) = f(x) + h_r f'(x) + {h_r}^2 \frac{f''(x)}{2} + {h_r}^3 \frac{f'''(\xi_1)}{6}, \xi_1 \in (x, x+h_r) \\ + f(x-h_l) = f(x) - h_l f'(x) + {h_l}^2 \frac{f''(x)}{2} - {h_l}^3 \frac{f'''(\xi_2)}{6}, \xi_2 \in (x, x-h_l) \\ + \end{aligned} + + Using the fact that :math:`f \in C^3` and solving the linear system, we derive: + + .. math:: + f'(x) \approx \frac{ {h_l}^2 f(x+h_r) - {h_r}^2 f(x-h_l) + + ({h_r}^2-{h_l}^2 ) f(x) }{ {h_r} {h_l}^2 + {h_r}^2 {h_l} } + + .. note:: + We estimate the gradient of functions in complex domain + :math:`g : \mathbb{C}^n \rightarrow \mathbb{C}` in the same way. + + The value of each partial derivative at the boundary points is computed differently. See edge_order below. + + Args: + input (``Tensor``): the tensor that represents the values of the function + + Keyword args: + spacing (``scalar``, ``list of scalar``, ``list of Tensor``, optional): :attr:`spacing` can be used to modify + how the :attr:`input` tensor's indices relate to sample coordinates. If :attr:`spacing` is a scalar then + the indices are multiplied by the scalar to produce the coordinates. For example, if :attr:`spacing=2` the + indices (1, 2, 3) become coordinates (2, 4, 6). If :attr:`spacing` is a list of scalars then the corresponding + indices are multiplied. For example, if :attr:`spacing=(2, -1, 3)` the indices (1, 2, 3) become coordinates (2, -2, 9). + Finally, if :attr:`spacing` is a list of one-dimensional tensors then each tensor specifies the coordinates for + the corresponding dimension. For example, if the indices are (1, 2, 3) and the tensors are (t0, t1, t2), then + the coordinates are (t0[1], t1[2], t2[3]) + + dim (``int``, ``list of int``, optional): the dimension or dimensions to approximate the gradient over. By default + the partial gradient in every dimension is computed. Note that when :attr:`dim` is specified the elements of + the :attr:`spacing` argument must correspond with the specified dims." + + edge_order (``int``, optional): 1 or 2, for `first-order + `_ or + `second-order `_ + estimation of the boundary ("edge") values, respectively. Note that when :attr:`edge_order` is specified, each + dimension size of :attr:`input` should be at least edge_order+1 + + Examples:: + + >>> # Estimates the gradient of f(x)=x^2 at points [-2, -1, 2, 4] + >>> coordinates = (torch.tensor([-2., -1., 1., 4.]),) + >>> values = torch.tensor([4., 1., 1., 16.], ) + >>> torch.gradient(values, spacing = coordinates) + (tensor([-3., -2., 2., 5.]),) + + >>> # Estimates the gradient of the R^2 -> R function whose samples are + >>> # described by the tensor t. Implicit coordinates are [0, 1] for the outermost + >>> # dimension and [0, 1, 2, 3] for the innermost dimension, and function estimates + >>> # partial derivative for both dimensions. + >>> t = torch.tensor([[1, 2, 4, 8], [10, 20, 40, 80]]) + >>> torch.gradient(t) + (tensor([[ 9., 18., 36., 72.], + [ 9., 18., 36., 72.]]), + tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]])) + + >>> # A scalar value for spacing modifies the relationship between tensor indices + >>> # and input coordinates by multiplying the indices to find the + >>> # coordinates. For example, below the indices of the innermost + >>> # 0, 1, 2, 3 translate to coordinates of [0, 2, 4, 6], and the indices of + >>> # the outermost dimension 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = 2.0) # dim = None (implicitly [0, 1]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.5000, 0.7500, 1.5000, 2.0000], + [ 5.0000, 7.5000, 15.0000, 20.0000]])) + >>> # doubling the spacing between samples halves the estimated partial gradients. + + >>> + >>> # Estimates only the partial derivative for dimension 1 + >>> torch.gradient(t, dim = 1) # spacing = None (implicitly 1.) + (tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]]),) + + >>> # When spacing is a list of scalars, the relationship between the tensor + >>> # indices and input coordinates changes based on dimension. + >>> # For example, below, the indices of the innermost dimension 0, 1, 2, 3 translate + >>> # to coordinates of [0, 3, 6, 9], and the indices of the outermost dimension + >>> # 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = [3., 2.]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + + >>> # The following example is a replication of the previous one with explicit + >>> # coordinates. + >>> coords = (torch.tensor([0, 2]), torch.tensor([0, 3, 6, 9])) + >>> torch.gradient(t, spacing = coords) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + """ + +@overload +def gradient( + input: Tensor, + *, + spacing: tuple[Tensor, ...] | list[Tensor], + dim: _int | None = None, + edge_order: _int = 1, +) -> tuple[Tensor, ...]: + r""" + gradient(input, *, spacing=1, dim=None, edge_order=1) -> List of Tensors + + Estimates the gradient of a function :math:`g : \mathbb{R}^n \rightarrow \mathbb{R}` in + one or more dimensions using the `second-order accurate central differences method + `_ and + either first or second order estimates at the boundaries. + + The gradient of :math:`g` is estimated using samples. By default, when :attr:`spacing` is not + specified, the samples are entirely described by :attr:`input`, and the mapping of input coordinates + to an output is the same as the tensor's mapping of indices to values. For example, for a three-dimensional + :attr:`input` the function described is :math:`g : \mathbb{R}^3 \rightarrow \mathbb{R}`, and + :math:`g(1, 2, 3)\ == input[1, 2, 3]`. + + When :attr:`spacing` is specified, it modifies the relationship between :attr:`input` and input coordinates. + This is detailed in the "Keyword Arguments" section below. + + The gradient is estimated by estimating each partial derivative of :math:`g` independently. This estimation is + accurate if :math:`g` is in :math:`C^3` (it has at least 3 continuous derivatives), and the estimation can be + improved by providing closer samples. Mathematically, the value at each interior point of a partial derivative + is estimated using `Taylor's theorem with remainder `_. + Letting :math:`x` be an interior point with :math:`x-h_l` and :math:`x+h_r` be points neighboring + it to the left and right respectively, :math:`f(x+h_r)` and :math:`f(x-h_l)` can be estimated using: + + .. math:: + \begin{aligned} + f(x+h_r) = f(x) + h_r f'(x) + {h_r}^2 \frac{f''(x)}{2} + {h_r}^3 \frac{f'''(\xi_1)}{6}, \xi_1 \in (x, x+h_r) \\ + f(x-h_l) = f(x) - h_l f'(x) + {h_l}^2 \frac{f''(x)}{2} - {h_l}^3 \frac{f'''(\xi_2)}{6}, \xi_2 \in (x, x-h_l) \\ + \end{aligned} + + Using the fact that :math:`f \in C^3` and solving the linear system, we derive: + + .. math:: + f'(x) \approx \frac{ {h_l}^2 f(x+h_r) - {h_r}^2 f(x-h_l) + + ({h_r}^2-{h_l}^2 ) f(x) }{ {h_r} {h_l}^2 + {h_r}^2 {h_l} } + + .. note:: + We estimate the gradient of functions in complex domain + :math:`g : \mathbb{C}^n \rightarrow \mathbb{C}` in the same way. + + The value of each partial derivative at the boundary points is computed differently. See edge_order below. + + Args: + input (``Tensor``): the tensor that represents the values of the function + + Keyword args: + spacing (``scalar``, ``list of scalar``, ``list of Tensor``, optional): :attr:`spacing` can be used to modify + how the :attr:`input` tensor's indices relate to sample coordinates. If :attr:`spacing` is a scalar then + the indices are multiplied by the scalar to produce the coordinates. For example, if :attr:`spacing=2` the + indices (1, 2, 3) become coordinates (2, 4, 6). If :attr:`spacing` is a list of scalars then the corresponding + indices are multiplied. For example, if :attr:`spacing=(2, -1, 3)` the indices (1, 2, 3) become coordinates (2, -2, 9). + Finally, if :attr:`spacing` is a list of one-dimensional tensors then each tensor specifies the coordinates for + the corresponding dimension. For example, if the indices are (1, 2, 3) and the tensors are (t0, t1, t2), then + the coordinates are (t0[1], t1[2], t2[3]) + + dim (``int``, ``list of int``, optional): the dimension or dimensions to approximate the gradient over. By default + the partial gradient in every dimension is computed. Note that when :attr:`dim` is specified the elements of + the :attr:`spacing` argument must correspond with the specified dims." + + edge_order (``int``, optional): 1 or 2, for `first-order + `_ or + `second-order `_ + estimation of the boundary ("edge") values, respectively. Note that when :attr:`edge_order` is specified, each + dimension size of :attr:`input` should be at least edge_order+1 + + Examples:: + + >>> # Estimates the gradient of f(x)=x^2 at points [-2, -1, 2, 4] + >>> coordinates = (torch.tensor([-2., -1., 1., 4.]),) + >>> values = torch.tensor([4., 1., 1., 16.], ) + >>> torch.gradient(values, spacing = coordinates) + (tensor([-3., -2., 2., 5.]),) + + >>> # Estimates the gradient of the R^2 -> R function whose samples are + >>> # described by the tensor t. Implicit coordinates are [0, 1] for the outermost + >>> # dimension and [0, 1, 2, 3] for the innermost dimension, and function estimates + >>> # partial derivative for both dimensions. + >>> t = torch.tensor([[1, 2, 4, 8], [10, 20, 40, 80]]) + >>> torch.gradient(t) + (tensor([[ 9., 18., 36., 72.], + [ 9., 18., 36., 72.]]), + tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]])) + + >>> # A scalar value for spacing modifies the relationship between tensor indices + >>> # and input coordinates by multiplying the indices to find the + >>> # coordinates. For example, below the indices of the innermost + >>> # 0, 1, 2, 3 translate to coordinates of [0, 2, 4, 6], and the indices of + >>> # the outermost dimension 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = 2.0) # dim = None (implicitly [0, 1]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.5000, 0.7500, 1.5000, 2.0000], + [ 5.0000, 7.5000, 15.0000, 20.0000]])) + >>> # doubling the spacing between samples halves the estimated partial gradients. + + >>> + >>> # Estimates only the partial derivative for dimension 1 + >>> torch.gradient(t, dim = 1) # spacing = None (implicitly 1.) + (tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]]),) + + >>> # When spacing is a list of scalars, the relationship between the tensor + >>> # indices and input coordinates changes based on dimension. + >>> # For example, below, the indices of the innermost dimension 0, 1, 2, 3 translate + >>> # to coordinates of [0, 3, 6, 9], and the indices of the outermost dimension + >>> # 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = [3., 2.]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + + >>> # The following example is a replication of the previous one with explicit + >>> # coordinates. + >>> coords = (torch.tensor([0, 2]), torch.tensor([0, 3, 6, 9])) + >>> torch.gradient(t, spacing = coords) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + """ + +@overload +def gradient( + input: Tensor, + *, + spacing: Number | _complex, + dim: _size, + edge_order: _int = 1, +) -> tuple[Tensor, ...]: + r""" + gradient(input, *, spacing=1, dim=None, edge_order=1) -> List of Tensors + + Estimates the gradient of a function :math:`g : \mathbb{R}^n \rightarrow \mathbb{R}` in + one or more dimensions using the `second-order accurate central differences method + `_ and + either first or second order estimates at the boundaries. + + The gradient of :math:`g` is estimated using samples. By default, when :attr:`spacing` is not + specified, the samples are entirely described by :attr:`input`, and the mapping of input coordinates + to an output is the same as the tensor's mapping of indices to values. For example, for a three-dimensional + :attr:`input` the function described is :math:`g : \mathbb{R}^3 \rightarrow \mathbb{R}`, and + :math:`g(1, 2, 3)\ == input[1, 2, 3]`. + + When :attr:`spacing` is specified, it modifies the relationship between :attr:`input` and input coordinates. + This is detailed in the "Keyword Arguments" section below. + + The gradient is estimated by estimating each partial derivative of :math:`g` independently. This estimation is + accurate if :math:`g` is in :math:`C^3` (it has at least 3 continuous derivatives), and the estimation can be + improved by providing closer samples. Mathematically, the value at each interior point of a partial derivative + is estimated using `Taylor's theorem with remainder `_. + Letting :math:`x` be an interior point with :math:`x-h_l` and :math:`x+h_r` be points neighboring + it to the left and right respectively, :math:`f(x+h_r)` and :math:`f(x-h_l)` can be estimated using: + + .. math:: + \begin{aligned} + f(x+h_r) = f(x) + h_r f'(x) + {h_r}^2 \frac{f''(x)}{2} + {h_r}^3 \frac{f'''(\xi_1)}{6}, \xi_1 \in (x, x+h_r) \\ + f(x-h_l) = f(x) - h_l f'(x) + {h_l}^2 \frac{f''(x)}{2} - {h_l}^3 \frac{f'''(\xi_2)}{6}, \xi_2 \in (x, x-h_l) \\ + \end{aligned} + + Using the fact that :math:`f \in C^3` and solving the linear system, we derive: + + .. math:: + f'(x) \approx \frac{ {h_l}^2 f(x+h_r) - {h_r}^2 f(x-h_l) + + ({h_r}^2-{h_l}^2 ) f(x) }{ {h_r} {h_l}^2 + {h_r}^2 {h_l} } + + .. note:: + We estimate the gradient of functions in complex domain + :math:`g : \mathbb{C}^n \rightarrow \mathbb{C}` in the same way. + + The value of each partial derivative at the boundary points is computed differently. See edge_order below. + + Args: + input (``Tensor``): the tensor that represents the values of the function + + Keyword args: + spacing (``scalar``, ``list of scalar``, ``list of Tensor``, optional): :attr:`spacing` can be used to modify + how the :attr:`input` tensor's indices relate to sample coordinates. If :attr:`spacing` is a scalar then + the indices are multiplied by the scalar to produce the coordinates. For example, if :attr:`spacing=2` the + indices (1, 2, 3) become coordinates (2, 4, 6). If :attr:`spacing` is a list of scalars then the corresponding + indices are multiplied. For example, if :attr:`spacing=(2, -1, 3)` the indices (1, 2, 3) become coordinates (2, -2, 9). + Finally, if :attr:`spacing` is a list of one-dimensional tensors then each tensor specifies the coordinates for + the corresponding dimension. For example, if the indices are (1, 2, 3) and the tensors are (t0, t1, t2), then + the coordinates are (t0[1], t1[2], t2[3]) + + dim (``int``, ``list of int``, optional): the dimension or dimensions to approximate the gradient over. By default + the partial gradient in every dimension is computed. Note that when :attr:`dim` is specified the elements of + the :attr:`spacing` argument must correspond with the specified dims." + + edge_order (``int``, optional): 1 or 2, for `first-order + `_ or + `second-order `_ + estimation of the boundary ("edge") values, respectively. Note that when :attr:`edge_order` is specified, each + dimension size of :attr:`input` should be at least edge_order+1 + + Examples:: + + >>> # Estimates the gradient of f(x)=x^2 at points [-2, -1, 2, 4] + >>> coordinates = (torch.tensor([-2., -1., 1., 4.]),) + >>> values = torch.tensor([4., 1., 1., 16.], ) + >>> torch.gradient(values, spacing = coordinates) + (tensor([-3., -2., 2., 5.]),) + + >>> # Estimates the gradient of the R^2 -> R function whose samples are + >>> # described by the tensor t. Implicit coordinates are [0, 1] for the outermost + >>> # dimension and [0, 1, 2, 3] for the innermost dimension, and function estimates + >>> # partial derivative for both dimensions. + >>> t = torch.tensor([[1, 2, 4, 8], [10, 20, 40, 80]]) + >>> torch.gradient(t) + (tensor([[ 9., 18., 36., 72.], + [ 9., 18., 36., 72.]]), + tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]])) + + >>> # A scalar value for spacing modifies the relationship between tensor indices + >>> # and input coordinates by multiplying the indices to find the + >>> # coordinates. For example, below the indices of the innermost + >>> # 0, 1, 2, 3 translate to coordinates of [0, 2, 4, 6], and the indices of + >>> # the outermost dimension 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = 2.0) # dim = None (implicitly [0, 1]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.5000, 0.7500, 1.5000, 2.0000], + [ 5.0000, 7.5000, 15.0000, 20.0000]])) + >>> # doubling the spacing between samples halves the estimated partial gradients. + + >>> + >>> # Estimates only the partial derivative for dimension 1 + >>> torch.gradient(t, dim = 1) # spacing = None (implicitly 1.) + (tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]]),) + + >>> # When spacing is a list of scalars, the relationship between the tensor + >>> # indices and input coordinates changes based on dimension. + >>> # For example, below, the indices of the innermost dimension 0, 1, 2, 3 translate + >>> # to coordinates of [0, 3, 6, 9], and the indices of the outermost dimension + >>> # 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = [3., 2.]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + + >>> # The following example is a replication of the previous one with explicit + >>> # coordinates. + >>> coords = (torch.tensor([0, 2]), torch.tensor([0, 3, 6, 9])) + >>> torch.gradient(t, spacing = coords) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + """ + +@overload +def gradient( + input: Tensor, + *, + spacing: tuple[Tensor, ...] | list[Tensor], + dim: _size, + edge_order: _int = 1, +) -> tuple[Tensor, ...]: + r""" + gradient(input, *, spacing=1, dim=None, edge_order=1) -> List of Tensors + + Estimates the gradient of a function :math:`g : \mathbb{R}^n \rightarrow \mathbb{R}` in + one or more dimensions using the `second-order accurate central differences method + `_ and + either first or second order estimates at the boundaries. + + The gradient of :math:`g` is estimated using samples. By default, when :attr:`spacing` is not + specified, the samples are entirely described by :attr:`input`, and the mapping of input coordinates + to an output is the same as the tensor's mapping of indices to values. For example, for a three-dimensional + :attr:`input` the function described is :math:`g : \mathbb{R}^3 \rightarrow \mathbb{R}`, and + :math:`g(1, 2, 3)\ == input[1, 2, 3]`. + + When :attr:`spacing` is specified, it modifies the relationship between :attr:`input` and input coordinates. + This is detailed in the "Keyword Arguments" section below. + + The gradient is estimated by estimating each partial derivative of :math:`g` independently. This estimation is + accurate if :math:`g` is in :math:`C^3` (it has at least 3 continuous derivatives), and the estimation can be + improved by providing closer samples. Mathematically, the value at each interior point of a partial derivative + is estimated using `Taylor's theorem with remainder `_. + Letting :math:`x` be an interior point with :math:`x-h_l` and :math:`x+h_r` be points neighboring + it to the left and right respectively, :math:`f(x+h_r)` and :math:`f(x-h_l)` can be estimated using: + + .. math:: + \begin{aligned} + f(x+h_r) = f(x) + h_r f'(x) + {h_r}^2 \frac{f''(x)}{2} + {h_r}^3 \frac{f'''(\xi_1)}{6}, \xi_1 \in (x, x+h_r) \\ + f(x-h_l) = f(x) - h_l f'(x) + {h_l}^2 \frac{f''(x)}{2} - {h_l}^3 \frac{f'''(\xi_2)}{6}, \xi_2 \in (x, x-h_l) \\ + \end{aligned} + + Using the fact that :math:`f \in C^3` and solving the linear system, we derive: + + .. math:: + f'(x) \approx \frac{ {h_l}^2 f(x+h_r) - {h_r}^2 f(x-h_l) + + ({h_r}^2-{h_l}^2 ) f(x) }{ {h_r} {h_l}^2 + {h_r}^2 {h_l} } + + .. note:: + We estimate the gradient of functions in complex domain + :math:`g : \mathbb{C}^n \rightarrow \mathbb{C}` in the same way. + + The value of each partial derivative at the boundary points is computed differently. See edge_order below. + + Args: + input (``Tensor``): the tensor that represents the values of the function + + Keyword args: + spacing (``scalar``, ``list of scalar``, ``list of Tensor``, optional): :attr:`spacing` can be used to modify + how the :attr:`input` tensor's indices relate to sample coordinates. If :attr:`spacing` is a scalar then + the indices are multiplied by the scalar to produce the coordinates. For example, if :attr:`spacing=2` the + indices (1, 2, 3) become coordinates (2, 4, 6). If :attr:`spacing` is a list of scalars then the corresponding + indices are multiplied. For example, if :attr:`spacing=(2, -1, 3)` the indices (1, 2, 3) become coordinates (2, -2, 9). + Finally, if :attr:`spacing` is a list of one-dimensional tensors then each tensor specifies the coordinates for + the corresponding dimension. For example, if the indices are (1, 2, 3) and the tensors are (t0, t1, t2), then + the coordinates are (t0[1], t1[2], t2[3]) + + dim (``int``, ``list of int``, optional): the dimension or dimensions to approximate the gradient over. By default + the partial gradient in every dimension is computed. Note that when :attr:`dim` is specified the elements of + the :attr:`spacing` argument must correspond with the specified dims." + + edge_order (``int``, optional): 1 or 2, for `first-order + `_ or + `second-order `_ + estimation of the boundary ("edge") values, respectively. Note that when :attr:`edge_order` is specified, each + dimension size of :attr:`input` should be at least edge_order+1 + + Examples:: + + >>> # Estimates the gradient of f(x)=x^2 at points [-2, -1, 2, 4] + >>> coordinates = (torch.tensor([-2., -1., 1., 4.]),) + >>> values = torch.tensor([4., 1., 1., 16.], ) + >>> torch.gradient(values, spacing = coordinates) + (tensor([-3., -2., 2., 5.]),) + + >>> # Estimates the gradient of the R^2 -> R function whose samples are + >>> # described by the tensor t. Implicit coordinates are [0, 1] for the outermost + >>> # dimension and [0, 1, 2, 3] for the innermost dimension, and function estimates + >>> # partial derivative for both dimensions. + >>> t = torch.tensor([[1, 2, 4, 8], [10, 20, 40, 80]]) + >>> torch.gradient(t) + (tensor([[ 9., 18., 36., 72.], + [ 9., 18., 36., 72.]]), + tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]])) + + >>> # A scalar value for spacing modifies the relationship between tensor indices + >>> # and input coordinates by multiplying the indices to find the + >>> # coordinates. For example, below the indices of the innermost + >>> # 0, 1, 2, 3 translate to coordinates of [0, 2, 4, 6], and the indices of + >>> # the outermost dimension 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = 2.0) # dim = None (implicitly [0, 1]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.5000, 0.7500, 1.5000, 2.0000], + [ 5.0000, 7.5000, 15.0000, 20.0000]])) + >>> # doubling the spacing between samples halves the estimated partial gradients. + + >>> + >>> # Estimates only the partial derivative for dimension 1 + >>> torch.gradient(t, dim = 1) # spacing = None (implicitly 1.) + (tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]]),) + + >>> # When spacing is a list of scalars, the relationship between the tensor + >>> # indices and input coordinates changes based on dimension. + >>> # For example, below, the indices of the innermost dimension 0, 1, 2, 3 translate + >>> # to coordinates of [0, 3, 6, 9], and the indices of the outermost dimension + >>> # 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = [3., 2.]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + + >>> # The following example is a replication of the previous one with explicit + >>> # coordinates. + >>> coords = (torch.tensor([0, 2]), torch.tensor([0, 3, 6, 9])) + >>> torch.gradient(t, spacing = coords) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + """ + +@overload +def gradient( + input: Tensor, + *, + dim: _size, + edge_order: _int = 1, +) -> tuple[Tensor, ...]: + r""" + gradient(input, *, spacing=1, dim=None, edge_order=1) -> List of Tensors + + Estimates the gradient of a function :math:`g : \mathbb{R}^n \rightarrow \mathbb{R}` in + one or more dimensions using the `second-order accurate central differences method + `_ and + either first or second order estimates at the boundaries. + + The gradient of :math:`g` is estimated using samples. By default, when :attr:`spacing` is not + specified, the samples are entirely described by :attr:`input`, and the mapping of input coordinates + to an output is the same as the tensor's mapping of indices to values. For example, for a three-dimensional + :attr:`input` the function described is :math:`g : \mathbb{R}^3 \rightarrow \mathbb{R}`, and + :math:`g(1, 2, 3)\ == input[1, 2, 3]`. + + When :attr:`spacing` is specified, it modifies the relationship between :attr:`input` and input coordinates. + This is detailed in the "Keyword Arguments" section below. + + The gradient is estimated by estimating each partial derivative of :math:`g` independently. This estimation is + accurate if :math:`g` is in :math:`C^3` (it has at least 3 continuous derivatives), and the estimation can be + improved by providing closer samples. Mathematically, the value at each interior point of a partial derivative + is estimated using `Taylor's theorem with remainder `_. + Letting :math:`x` be an interior point with :math:`x-h_l` and :math:`x+h_r` be points neighboring + it to the left and right respectively, :math:`f(x+h_r)` and :math:`f(x-h_l)` can be estimated using: + + .. math:: + \begin{aligned} + f(x+h_r) = f(x) + h_r f'(x) + {h_r}^2 \frac{f''(x)}{2} + {h_r}^3 \frac{f'''(\xi_1)}{6}, \xi_1 \in (x, x+h_r) \\ + f(x-h_l) = f(x) - h_l f'(x) + {h_l}^2 \frac{f''(x)}{2} - {h_l}^3 \frac{f'''(\xi_2)}{6}, \xi_2 \in (x, x-h_l) \\ + \end{aligned} + + Using the fact that :math:`f \in C^3` and solving the linear system, we derive: + + .. math:: + f'(x) \approx \frac{ {h_l}^2 f(x+h_r) - {h_r}^2 f(x-h_l) + + ({h_r}^2-{h_l}^2 ) f(x) }{ {h_r} {h_l}^2 + {h_r}^2 {h_l} } + + .. note:: + We estimate the gradient of functions in complex domain + :math:`g : \mathbb{C}^n \rightarrow \mathbb{C}` in the same way. + + The value of each partial derivative at the boundary points is computed differently. See edge_order below. + + Args: + input (``Tensor``): the tensor that represents the values of the function + + Keyword args: + spacing (``scalar``, ``list of scalar``, ``list of Tensor``, optional): :attr:`spacing` can be used to modify + how the :attr:`input` tensor's indices relate to sample coordinates. If :attr:`spacing` is a scalar then + the indices are multiplied by the scalar to produce the coordinates. For example, if :attr:`spacing=2` the + indices (1, 2, 3) become coordinates (2, 4, 6). If :attr:`spacing` is a list of scalars then the corresponding + indices are multiplied. For example, if :attr:`spacing=(2, -1, 3)` the indices (1, 2, 3) become coordinates (2, -2, 9). + Finally, if :attr:`spacing` is a list of one-dimensional tensors then each tensor specifies the coordinates for + the corresponding dimension. For example, if the indices are (1, 2, 3) and the tensors are (t0, t1, t2), then + the coordinates are (t0[1], t1[2], t2[3]) + + dim (``int``, ``list of int``, optional): the dimension or dimensions to approximate the gradient over. By default + the partial gradient in every dimension is computed. Note that when :attr:`dim` is specified the elements of + the :attr:`spacing` argument must correspond with the specified dims." + + edge_order (``int``, optional): 1 or 2, for `first-order + `_ or + `second-order `_ + estimation of the boundary ("edge") values, respectively. Note that when :attr:`edge_order` is specified, each + dimension size of :attr:`input` should be at least edge_order+1 + + Examples:: + + >>> # Estimates the gradient of f(x)=x^2 at points [-2, -1, 2, 4] + >>> coordinates = (torch.tensor([-2., -1., 1., 4.]),) + >>> values = torch.tensor([4., 1., 1., 16.], ) + >>> torch.gradient(values, spacing = coordinates) + (tensor([-3., -2., 2., 5.]),) + + >>> # Estimates the gradient of the R^2 -> R function whose samples are + >>> # described by the tensor t. Implicit coordinates are [0, 1] for the outermost + >>> # dimension and [0, 1, 2, 3] for the innermost dimension, and function estimates + >>> # partial derivative for both dimensions. + >>> t = torch.tensor([[1, 2, 4, 8], [10, 20, 40, 80]]) + >>> torch.gradient(t) + (tensor([[ 9., 18., 36., 72.], + [ 9., 18., 36., 72.]]), + tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]])) + + >>> # A scalar value for spacing modifies the relationship between tensor indices + >>> # and input coordinates by multiplying the indices to find the + >>> # coordinates. For example, below the indices of the innermost + >>> # 0, 1, 2, 3 translate to coordinates of [0, 2, 4, 6], and the indices of + >>> # the outermost dimension 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = 2.0) # dim = None (implicitly [0, 1]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.5000, 0.7500, 1.5000, 2.0000], + [ 5.0000, 7.5000, 15.0000, 20.0000]])) + >>> # doubling the spacing between samples halves the estimated partial gradients. + + >>> + >>> # Estimates only the partial derivative for dimension 1 + >>> torch.gradient(t, dim = 1) # spacing = None (implicitly 1.) + (tensor([[ 1.0000, 1.5000, 3.0000, 4.0000], + [10.0000, 15.0000, 30.0000, 40.0000]]),) + + >>> # When spacing is a list of scalars, the relationship between the tensor + >>> # indices and input coordinates changes based on dimension. + >>> # For example, below, the indices of the innermost dimension 0, 1, 2, 3 translate + >>> # to coordinates of [0, 3, 6, 9], and the indices of the outermost dimension + >>> # 0, 1 translate to coordinates of [0, 2]. + >>> torch.gradient(t, spacing = [3., 2.]) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + + >>> # The following example is a replication of the previous one with explicit + >>> # coordinates. + >>> coords = (torch.tensor([0, 2]), torch.tensor([0, 3, 6, 9])) + >>> torch.gradient(t, spacing = coords) + (tensor([[ 4.5000, 9.0000, 18.0000, 36.0000], + [ 4.5000, 9.0000, 18.0000, 36.0000]]), + tensor([[ 0.3333, 0.5000, 1.0000, 1.3333], + [ 3.3333, 5.0000, 10.0000, 13.3333]])) + """ + +@overload +def greater( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + greater(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.gt`. + """ + +@overload +def greater( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + greater(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.gt`. + """ + +@overload +def greater_equal( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + greater_equal(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.ge`. + """ + +@overload +def greater_equal( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + greater_equal(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.ge`. + """ + +def grid_sampler( + input: Tensor, + grid: Tensor, + interpolation_mode: _int, + padding_mode: _int, + align_corners: _bool, +) -> Tensor: ... +def grid_sampler_2d( + input: Tensor, + grid: Tensor, + interpolation_mode: _int, + padding_mode: _int, + align_corners: _bool, +) -> Tensor: ... +def grid_sampler_3d( + input: Tensor, + grid: Tensor, + interpolation_mode: _int, + padding_mode: _int, + align_corners: _bool, +) -> Tensor: ... +def group_norm( + input: Tensor, + num_groups: _int, + weight: Tensor | None = None, + bias: Tensor | None = None, + eps: _float = 1e-05, + cudnn_enabled: _bool = True, +) -> Tensor: ... +@overload +def gru( + data: Tensor, + batch_sizes: Tensor, + hx: Tensor, + params: tuple[Tensor, ...] | list[Tensor], + has_biases: _bool, + num_layers: _int, + dropout: _float, + train: _bool, + bidirectional: _bool, +) -> tuple[Tensor, Tensor]: ... +@overload +def gru( + input: Tensor, + hx: Tensor, + params: tuple[Tensor, ...] | list[Tensor], + has_biases: _bool, + num_layers: _int, + dropout: _float, + train: _bool, + bidirectional: _bool, + batch_first: _bool, +) -> tuple[Tensor, Tensor]: ... +def gru_cell( + input: Tensor, + hx: Tensor, + w_ih: Tensor, + w_hh: Tensor, + b_ih: Tensor | None = None, + b_hh: Tensor | None = None, +) -> Tensor: ... +@overload +def gt( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + gt(input, other, *, out=None) -> Tensor + + Computes :math:`\text{input} > \text{other}` element-wise. + + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or float): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is greater than :attr:`other` and False elsewhere + + Example:: + + >>> torch.gt(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[False, True], [False, False]]) + """ + +@overload +def gt( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + gt(input, other, *, out=None) -> Tensor + + Computes :math:`\text{input} > \text{other}` element-wise. + + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or float): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is greater than :attr:`other` and False elsewhere + + Example:: + + >>> torch.gt(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[False, True], [False, False]]) + """ + +@overload +def hamming_window( + window_length: _int, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + hamming_window(window_length, *, dtype=None, layout=None, device=None, pin_memory=False, requires_grad=False) -> Tensor + + Hamming window function. + + .. math:: + w[n] = \alpha - \beta\ \cos \left( \frac{2 \pi n}{N - 1} \right), + + where :math:`N` is the full window size. + + The input :attr:`window_length` is a positive integer controlling the + returned window size. :attr:`periodic` flag determines whether the returned + window trims off the last duplicate value from the symmetric window and is + ready to be used as a periodic window with functions like + :meth:`torch.stft`. Therefore, if :attr:`periodic` is true, the :math:`N` in + above formula is in fact :math:`\text{window\_length} + 1`. Also, we always have + ``torch.hamming_window(L, periodic=True)`` equal to + ``torch.hamming_window(L + 1, periodic=False)[:-1])``. + + .. note:: + If :attr:`window_length` :math:`=1`, the returned window contains a single value 1. + + .. note:: + This is a generalized version of :meth:`torch.hann_window`. + + Arguments: + window_length (int): the size of returned window + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, *, dtype=None, layout=None, device=None, \ + pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, float alpha, *, dtype=None, layout=None, device=None, \ + pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic and alpha specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + alpha (float): The coefficient :math:`\alpha` in the equation above + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, float alpha, float beta, *, dtype=None, layout=None, \ + device=None, pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic, alpha and beta specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + alpha (float): The coefficient :math:`\alpha` in the equation above + beta (float): The coefficient :math:`\beta` in the equation above + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + """ + +@overload +def hamming_window( + window_length: _int, + periodic: _bool, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + hamming_window(window_length, *, dtype=None, layout=None, device=None, pin_memory=False, requires_grad=False) -> Tensor + + Hamming window function. + + .. math:: + w[n] = \alpha - \beta\ \cos \left( \frac{2 \pi n}{N - 1} \right), + + where :math:`N` is the full window size. + + The input :attr:`window_length` is a positive integer controlling the + returned window size. :attr:`periodic` flag determines whether the returned + window trims off the last duplicate value from the symmetric window and is + ready to be used as a periodic window with functions like + :meth:`torch.stft`. Therefore, if :attr:`periodic` is true, the :math:`N` in + above formula is in fact :math:`\text{window\_length} + 1`. Also, we always have + ``torch.hamming_window(L, periodic=True)`` equal to + ``torch.hamming_window(L + 1, periodic=False)[:-1])``. + + .. note:: + If :attr:`window_length` :math:`=1`, the returned window contains a single value 1. + + .. note:: + This is a generalized version of :meth:`torch.hann_window`. + + Arguments: + window_length (int): the size of returned window + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, *, dtype=None, layout=None, device=None, \ + pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, float alpha, *, dtype=None, layout=None, device=None, \ + pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic and alpha specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + alpha (float): The coefficient :math:`\alpha` in the equation above + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, float alpha, float beta, *, dtype=None, layout=None, \ + device=None, pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic, alpha and beta specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + alpha (float): The coefficient :math:`\alpha` in the equation above + beta (float): The coefficient :math:`\beta` in the equation above + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + """ + +@overload +def hamming_window( + window_length: _int, + periodic: _bool, + alpha: _float, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + hamming_window(window_length, *, dtype=None, layout=None, device=None, pin_memory=False, requires_grad=False) -> Tensor + + Hamming window function. + + .. math:: + w[n] = \alpha - \beta\ \cos \left( \frac{2 \pi n}{N - 1} \right), + + where :math:`N` is the full window size. + + The input :attr:`window_length` is a positive integer controlling the + returned window size. :attr:`periodic` flag determines whether the returned + window trims off the last duplicate value from the symmetric window and is + ready to be used as a periodic window with functions like + :meth:`torch.stft`. Therefore, if :attr:`periodic` is true, the :math:`N` in + above formula is in fact :math:`\text{window\_length} + 1`. Also, we always have + ``torch.hamming_window(L, periodic=True)`` equal to + ``torch.hamming_window(L + 1, periodic=False)[:-1])``. + + .. note:: + If :attr:`window_length` :math:`=1`, the returned window contains a single value 1. + + .. note:: + This is a generalized version of :meth:`torch.hann_window`. + + Arguments: + window_length (int): the size of returned window + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, *, dtype=None, layout=None, device=None, \ + pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, float alpha, *, dtype=None, layout=None, device=None, \ + pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic and alpha specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + alpha (float): The coefficient :math:`\alpha` in the equation above + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, float alpha, float beta, *, dtype=None, layout=None, \ + device=None, pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic, alpha and beta specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + alpha (float): The coefficient :math:`\alpha` in the equation above + beta (float): The coefficient :math:`\beta` in the equation above + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + """ + +@overload +def hamming_window( + window_length: _int, + periodic: _bool, + alpha: _float, + beta: _float, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + hamming_window(window_length, *, dtype=None, layout=None, device=None, pin_memory=False, requires_grad=False) -> Tensor + + Hamming window function. + + .. math:: + w[n] = \alpha - \beta\ \cos \left( \frac{2 \pi n}{N - 1} \right), + + where :math:`N` is the full window size. + + The input :attr:`window_length` is a positive integer controlling the + returned window size. :attr:`periodic` flag determines whether the returned + window trims off the last duplicate value from the symmetric window and is + ready to be used as a periodic window with functions like + :meth:`torch.stft`. Therefore, if :attr:`periodic` is true, the :math:`N` in + above formula is in fact :math:`\text{window\_length} + 1`. Also, we always have + ``torch.hamming_window(L, periodic=True)`` equal to + ``torch.hamming_window(L + 1, periodic=False)[:-1])``. + + .. note:: + If :attr:`window_length` :math:`=1`, the returned window contains a single value 1. + + .. note:: + This is a generalized version of :meth:`torch.hann_window`. + + Arguments: + window_length (int): the size of returned window + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, *, dtype=None, layout=None, device=None, \ + pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, float alpha, *, dtype=None, layout=None, device=None, \ + pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic and alpha specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + alpha (float): The coefficient :math:`\alpha` in the equation above + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + + .. function:: hamming_window(window_length, periodic, float alpha, float beta, *, dtype=None, layout=None, \ + device=None, pin_memory=False, requires_grad=False) -> Tensor + :noindex: + + Hamming window function with periodic, alpha and beta specified. + + Arguments: + window_length (int): the size of returned window + periodic (bool): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + alpha (float): The coefficient :math:`\alpha` in the equation above + beta (float): The coefficient :math:`\beta` in the equation above + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window. + """ + +@overload +def hann_window( + window_length: _int, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + hann_window(window_length, periodic=True, *, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Hann window function. + + .. math:: + w[n] = \frac{1}{2}\ \left[1 - \cos \left( \frac{2 \pi n}{N - 1} \right)\right] = + \sin^2 \left( \frac{\pi n}{N - 1} \right), + + where :math:`N` is the full window size. + + The input :attr:`window_length` is a positive integer controlling the + returned window size. :attr:`periodic` flag determines whether the returned + window trims off the last duplicate value from the symmetric window and is + ready to be used as a periodic window with functions like + :meth:`torch.stft`. Therefore, if :attr:`periodic` is true, the :math:`N` in + above formula is in fact :math:`\text{window\_length} + 1`. Also, we always have + ``torch.hann_window(L, periodic=True)`` equal to + ``torch.hann_window(L + 1, periodic=False)[:-1])``. + + .. note:: + If :attr:`window_length` :math:`=1`, the returned window contains a single value 1. + + Arguments: + window_length (int): the size of returned window + periodic (bool, optional): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window + """ + +@overload +def hann_window( + window_length: _int, + periodic: _bool, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + hann_window(window_length, periodic=True, *, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Hann window function. + + .. math:: + w[n] = \frac{1}{2}\ \left[1 - \cos \left( \frac{2 \pi n}{N - 1} \right)\right] = + \sin^2 \left( \frac{\pi n}{N - 1} \right), + + where :math:`N` is the full window size. + + The input :attr:`window_length` is a positive integer controlling the + returned window size. :attr:`periodic` flag determines whether the returned + window trims off the last duplicate value from the symmetric window and is + ready to be used as a periodic window with functions like + :meth:`torch.stft`. Therefore, if :attr:`periodic` is true, the :math:`N` in + above formula is in fact :math:`\text{window\_length} + 1`. Also, we always have + ``torch.hann_window(L, periodic=True)`` equal to + ``torch.hann_window(L + 1, periodic=False)[:-1])``. + + .. note:: + If :attr:`window_length` :math:`=1`, the returned window contains a single value 1. + + Arguments: + window_length (int): the size of returned window + periodic (bool, optional): If True, returns a window to be used as periodic + function. If False, return a symmetric window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). Only floating point types are supported. + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Returns: + Tensor: A 1-D tensor of size :math:`(\text{window\_length},)` containing the window + """ + +def hardshrink( + input: Tensor, + lambd: Number | _complex = 0.5, + *, + out: Tensor | None = None, +) -> Tensor: ... +def hash_tensor( + input: Tensor, + dim: _int | _size = (), + *, + keepdim: _bool = False, + mode: _int = 0, + out: Tensor | None = None, +) -> Tensor: + r""" + hash_tensor(input, *, mode=0) -> Tensor + + Returns a hash of all elements in the :attr:`input` tensor. + + Currently only mode=0 (reduction via xor) is supported. The output will always + be of type ``torch.uint64``. The elements of ``input`` are upcasted to their + 64 bit float / integer equivalent and bitcasted to ``torch.uint64`` before + reduction via xor. + + Args: + input (Tensor): the input tensor. + + Keyword Args: + mode (int) : The hash to use. Default: 0 (xor_reduction) + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 1.1918, -1.1813, 0.3373]]) + >>> torch.hash_tensor(a) + tensor(13822780554648485888, dtype=torch.uint64) + + .. function:: hash_tensor(input, dim, *, keepdim=False, mode=0) -> Tensor + :noindex: + + Returns the hash of each row of the :attr:`input` tensor in the given + dimension :attr:`dim` given by mode. If :attr:`dim` is a list of dimensions, + reduce over all of them. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword Args: + mode (int) : The hash to use. Default: 0 (xor_reduction) + + Example:: + + >>> a = torch.randn(2, 4) + >>> a + tensor([[ 0.1317, -0.5554, -1.4724, -1.1391], + [ 0.0778, -0.6070, 0.6375, 0.1798]]) + >>> torch.hash_tensor(a, 1) + tensor([9233691267014066176, 9255993250844508160], dtype=torch.uint64) + """ + +def heaviside( + input: Tensor, + values: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + heaviside(input, values, *, out=None) -> Tensor + + Computes the Heaviside step function for each element in :attr:`input`. + The Heaviside step function is defined as: + + .. math:: + \text{{heaviside}}(input, values) = \begin{cases} + 0, & \text{if input < 0}\\ + values, & \text{if input == 0}\\ + 1, & \text{if input > 0} + \end{cases} + + + Args: + input (Tensor): the input tensor. + values (Tensor): The values to use where :attr:`input` is zero. + + Keyword arguments: + out (Tensor, optional): the output tensor. + + Example:: + + >>> input = torch.tensor([-1.5, 0, 2.0]) + >>> values = torch.tensor([0.5]) + >>> torch.heaviside(input, values) + tensor([0.0000, 0.5000, 1.0000]) + >>> values = torch.tensor([1.2, -2.0, 3.5]) + >>> torch.heaviside(input, values) + tensor([0., -2., 1.]) + """ + +def hinge_embedding_loss( + input: Tensor, + target: Tensor, + margin: _float = 1.0, + reduction: _int = 1, +) -> Tensor: ... +def histc( + input: Tensor, + bins: _int = 100, + min: Number | _complex = 0, + max: Number | _complex = 0, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + histc(input, bins=100, min=0, max=0, *, out=None) -> Tensor + + Computes the histogram of a tensor. + + The elements are sorted into equal width bins between :attr:`min` and + :attr:`max`. If :attr:`min` and :attr:`max` are both zero, the minimum and + maximum values of the data are used. + + Elements lower than min and higher than max and ``NaN`` elements are ignored. + + Args: + input (Tensor): the input tensor. + bins (int): number of histogram bins + min (Scalar): lower end of the range (inclusive) + max (Scalar): upper end of the range (inclusive) + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + Tensor: Histogram represented as a tensor + + Example:: + + >>> torch.histc(torch.tensor([1., 2, 1]), bins=4, min=0, max=3) + tensor([ 0., 2., 1., 0.]) + """ + +@overload +def histogram( + input: Tensor, + bins: Tensor, + *, + weight: Tensor | None = None, + density: _bool = False, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.histogram: + r""" + histogram(input, bins, *, range=None, weight=None, density=False, out=None) -> (Tensor, Tensor) + + Computes a histogram of the values in a tensor. + + :attr:`bins` can be an integer or a 1D tensor. + + If :attr:`bins` is an int, it specifies the number of equal-width bins. + By default, the lower and upper range of the bins is determined by the + minimum and maximum elements of the input tensor. The :attr:`range` + argument can be provided to specify a range for the bins. + + If :attr:`bins` is a 1D tensor, it specifies the sequence of bin edges + including the rightmost edge. It should contain at least 2 elements + and its elements should be increasing. + + Args: + input (Tensor): the input tensor. + bins: int or 1D Tensor. If int, defines the number of equal-width bins. If tensor, + defines the sequence of bin edges including the rightmost edge. + + Keyword args: + range (tuple of float): Defines the range of the bins. + weight (Tensor): If provided, weight should have the same shape as input. Each value in + input contributes its associated weight towards its bin's result. + density (bool): If False, the result will contain the count (or total weight) in each bin. + If True, the result is the value of the probability density function over the bins, + normalized such that the integral over the range of the bins is 1. + out (Tensor, optional): the output tensor. (tuple, optional): The result tuple of two output tensors (hist, bin_edges). + + Returns: + hist (Tensor): 1D Tensor containing the values of the histogram. + bin_edges(Tensor): 1D Tensor containing the edges of the histogram bins. + + Example:: + + >>> torch.histogram(torch.tensor([1., 2, 1]), bins=4, range=(0., 3.), weight=torch.tensor([1., 2., 4.])) + (tensor([ 0., 5., 2., 0.]), tensor([0., 0.75, 1.5, 2.25, 3.])) + >>> torch.histogram(torch.tensor([1., 2, 1]), bins=4, range=(0., 3.), weight=torch.tensor([1., 2., 4.]), density=True) + (tensor([ 0., 0.9524, 0.3810, 0.]), tensor([0., 0.75, 1.5, 2.25, 3.])) + """ + +@overload +def histogram( + input: Tensor, + bins: _int = 100, + *, + range: Sequence[_float] | None = None, + weight: Tensor | None = None, + density: _bool = False, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.histogram: + r""" + histogram(input, bins, *, range=None, weight=None, density=False, out=None) -> (Tensor, Tensor) + + Computes a histogram of the values in a tensor. + + :attr:`bins` can be an integer or a 1D tensor. + + If :attr:`bins` is an int, it specifies the number of equal-width bins. + By default, the lower and upper range of the bins is determined by the + minimum and maximum elements of the input tensor. The :attr:`range` + argument can be provided to specify a range for the bins. + + If :attr:`bins` is a 1D tensor, it specifies the sequence of bin edges + including the rightmost edge. It should contain at least 2 elements + and its elements should be increasing. + + Args: + input (Tensor): the input tensor. + bins: int or 1D Tensor. If int, defines the number of equal-width bins. If tensor, + defines the sequence of bin edges including the rightmost edge. + + Keyword args: + range (tuple of float): Defines the range of the bins. + weight (Tensor): If provided, weight should have the same shape as input. Each value in + input contributes its associated weight towards its bin's result. + density (bool): If False, the result will contain the count (or total weight) in each bin. + If True, the result is the value of the probability density function over the bins, + normalized such that the integral over the range of the bins is 1. + out (Tensor, optional): the output tensor. (tuple, optional): The result tuple of two output tensors (hist, bin_edges). + + Returns: + hist (Tensor): 1D Tensor containing the values of the histogram. + bin_edges(Tensor): 1D Tensor containing the edges of the histogram bins. + + Example:: + + >>> torch.histogram(torch.tensor([1., 2, 1]), bins=4, range=(0., 3.), weight=torch.tensor([1., 2., 4.])) + (tensor([ 0., 5., 2., 0.]), tensor([0., 0.75, 1.5, 2.25, 3.])) + >>> torch.histogram(torch.tensor([1., 2, 1]), bins=4, range=(0., 3.), weight=torch.tensor([1., 2., 4.]), density=True) + (tensor([ 0., 0.9524, 0.3810, 0.]), tensor([0., 0.75, 1.5, 2.25, 3.])) + """ + +@overload +def histogramdd( + input: Tensor, + bins: _int, + range: Sequence[_float] | None = None, + weight: Tensor | None = None, + density: _bool = False, +) -> torch.return_types.histogramdd: + r""" + histogramdd(input, bins, *, range=None, weight=None, density=False, out=None) -> (Tensor, Tensor[]) + + Computes a multi-dimensional histogram of the values in a tensor. + + Interprets the elements of an input tensor whose innermost dimension has size N + as a collection of N-dimensional points. Maps each of the points into a set of + N-dimensional bins and returns the number of points (or total weight) in each bin. + + :attr:`input` must be a tensor with at least 2 dimensions. + If input has shape (M, N), each of its M rows defines a point in N-dimensional space. + If input has three or more dimensions, all but the last dimension are flattened. + + Each dimension is independently associated with its own strictly increasing sequence + of bin edges. Bin edges may be specified explicitly by passing a sequence of 1D + tensors. Alternatively, bin edges may be constructed automatically by passing a + sequence of integers specifying the number of equal-width bins in each dimension. + + For each N-dimensional point in input: + - Each of its coordinates is binned independently among the bin edges + corresponding to its dimension + - Binning results are combined to identify the N-dimensional bin (if any) + into which the point falls + - If the point falls into a bin, the bin's count (or total weight) is incremented + - Points which do not fall into any bin do not contribute to the output + + :attr:`bins` can be a sequence of N 1D tensors, a sequence of N ints, or a single int. + + If :attr:`bins` is a sequence of N 1D tensors, it explicitly specifies the N sequences + of bin edges. Each 1D tensor should contain a strictly increasing sequence with at + least one element. A sequence of K bin edges defines K-1 bins, explicitly specifying + the left and right edges of all bins. Every bin is inclusive of its left edge. Only + the rightmost bin is inclusive of its right edge. + + If :attr:`bins` is a sequence of N ints, it specifies the number of equal-width bins + in each dimension. By default, the leftmost and rightmost bin edges in each dimension + are determined by the minimum and maximum elements of the input tensor in the + corresponding dimension. The :attr:`range` argument can be provided to manually + specify the leftmost and rightmost bin edges in each dimension. + + If :attr:`bins` is an int, it specifies the number of equal-width bins for all dimensions. + + .. note:: + See also :func:`torch.histogram`, which specifically computes 1D histograms. + While :func:`torch.histogramdd` infers the dimensionality of its bins and + binned values from the shape of :attr:`input`, :func:`torch.histogram` + accepts and flattens :attr:`input` of any shape. + + Args: + input (Tensor): the input tensor. + bins: Tensor[], int[], or int. + If Tensor[], defines the sequences of bin edges. + If int[], defines the number of equal-width bins in each dimension. + If int, defines the number of equal-width bins for all dimensions. + Keyword args: + range (sequence of float): Defines the leftmost and rightmost bin edges + in each dimension. + weight (Tensor): By default, each value in the input has weight 1. If a weight + tensor is passed, each N-dimensional coordinate in input + contributes its associated weight towards its bin's result. + The weight tensor should have the same shape as the :attr:`input` + tensor excluding its innermost dimension N. + density (bool): If False (default), the result will contain the count (or total weight) + in each bin. If True, each count (weight) is divided by the total count + (total weight), then divided by the volume of its associated bin. + Returns: + hist (Tensor): N-dimensional Tensor containing the values of the histogram. + bin_edges(Tensor[]): sequence of N 1D Tensors containing the bin edges. + + Example:: + + >>> torch.histogramdd(torch.tensor([[0., 1.], [1., 0.], [2., 0.], [2., 2.]]), bins=[3, 3], + ... weight=torch.tensor([1., 2., 4., 8.])) + torch.return_types.histogramdd( + hist=tensor([[0., 1., 0.], + [2., 0., 0.], + [4., 0., 8.]]), + bin_edges=(tensor([0.0000, 0.6667, 1.3333, 2.0000]), + tensor([0.0000, 0.6667, 1.3333, 2.0000]))) + + >>> torch.histogramdd(torch.tensor([[0., 0.], [1., 1.], [2., 2.]]), bins=[2, 2], + ... range=[0., 1., 0., 1.], density=True) + torch.return_types.histogramdd( + hist=tensor([[2., 0.], + [0., 2.]]), + bin_edges=(tensor([0.0000, 0.5000, 1.0000]), + tensor([0.0000, 0.5000, 1.0000]))) + """ + +@overload +def histogramdd( + input: Tensor, + bins: _size, + range: Sequence[_float] | None = None, + weight: Tensor | None = None, + density: _bool = False, +) -> torch.return_types.histogramdd: + r""" + histogramdd(input, bins, *, range=None, weight=None, density=False, out=None) -> (Tensor, Tensor[]) + + Computes a multi-dimensional histogram of the values in a tensor. + + Interprets the elements of an input tensor whose innermost dimension has size N + as a collection of N-dimensional points. Maps each of the points into a set of + N-dimensional bins and returns the number of points (or total weight) in each bin. + + :attr:`input` must be a tensor with at least 2 dimensions. + If input has shape (M, N), each of its M rows defines a point in N-dimensional space. + If input has three or more dimensions, all but the last dimension are flattened. + + Each dimension is independently associated with its own strictly increasing sequence + of bin edges. Bin edges may be specified explicitly by passing a sequence of 1D + tensors. Alternatively, bin edges may be constructed automatically by passing a + sequence of integers specifying the number of equal-width bins in each dimension. + + For each N-dimensional point in input: + - Each of its coordinates is binned independently among the bin edges + corresponding to its dimension + - Binning results are combined to identify the N-dimensional bin (if any) + into which the point falls + - If the point falls into a bin, the bin's count (or total weight) is incremented + - Points which do not fall into any bin do not contribute to the output + + :attr:`bins` can be a sequence of N 1D tensors, a sequence of N ints, or a single int. + + If :attr:`bins` is a sequence of N 1D tensors, it explicitly specifies the N sequences + of bin edges. Each 1D tensor should contain a strictly increasing sequence with at + least one element. A sequence of K bin edges defines K-1 bins, explicitly specifying + the left and right edges of all bins. Every bin is inclusive of its left edge. Only + the rightmost bin is inclusive of its right edge. + + If :attr:`bins` is a sequence of N ints, it specifies the number of equal-width bins + in each dimension. By default, the leftmost and rightmost bin edges in each dimension + are determined by the minimum and maximum elements of the input tensor in the + corresponding dimension. The :attr:`range` argument can be provided to manually + specify the leftmost and rightmost bin edges in each dimension. + + If :attr:`bins` is an int, it specifies the number of equal-width bins for all dimensions. + + .. note:: + See also :func:`torch.histogram`, which specifically computes 1D histograms. + While :func:`torch.histogramdd` infers the dimensionality of its bins and + binned values from the shape of :attr:`input`, :func:`torch.histogram` + accepts and flattens :attr:`input` of any shape. + + Args: + input (Tensor): the input tensor. + bins: Tensor[], int[], or int. + If Tensor[], defines the sequences of bin edges. + If int[], defines the number of equal-width bins in each dimension. + If int, defines the number of equal-width bins for all dimensions. + Keyword args: + range (sequence of float): Defines the leftmost and rightmost bin edges + in each dimension. + weight (Tensor): By default, each value in the input has weight 1. If a weight + tensor is passed, each N-dimensional coordinate in input + contributes its associated weight towards its bin's result. + The weight tensor should have the same shape as the :attr:`input` + tensor excluding its innermost dimension N. + density (bool): If False (default), the result will contain the count (or total weight) + in each bin. If True, each count (weight) is divided by the total count + (total weight), then divided by the volume of its associated bin. + Returns: + hist (Tensor): N-dimensional Tensor containing the values of the histogram. + bin_edges(Tensor[]): sequence of N 1D Tensors containing the bin edges. + + Example:: + + >>> torch.histogramdd(torch.tensor([[0., 1.], [1., 0.], [2., 0.], [2., 2.]]), bins=[3, 3], + ... weight=torch.tensor([1., 2., 4., 8.])) + torch.return_types.histogramdd( + hist=tensor([[0., 1., 0.], + [2., 0., 0.], + [4., 0., 8.]]), + bin_edges=(tensor([0.0000, 0.6667, 1.3333, 2.0000]), + tensor([0.0000, 0.6667, 1.3333, 2.0000]))) + + >>> torch.histogramdd(torch.tensor([[0., 0.], [1., 1.], [2., 2.]]), bins=[2, 2], + ... range=[0., 1., 0., 1.], density=True) + torch.return_types.histogramdd( + hist=tensor([[2., 0.], + [0., 2.]]), + bin_edges=(tensor([0.0000, 0.5000, 1.0000]), + tensor([0.0000, 0.5000, 1.0000]))) + """ + +@overload +def histogramdd( + input: Tensor, + bins: tuple[Tensor, ...] | list[Tensor], + range: Sequence[_float] | None = None, + weight: Tensor | None = None, + density: _bool = False, +) -> torch.return_types.histogramdd: + r""" + histogramdd(input, bins, *, range=None, weight=None, density=False, out=None) -> (Tensor, Tensor[]) + + Computes a multi-dimensional histogram of the values in a tensor. + + Interprets the elements of an input tensor whose innermost dimension has size N + as a collection of N-dimensional points. Maps each of the points into a set of + N-dimensional bins and returns the number of points (or total weight) in each bin. + + :attr:`input` must be a tensor with at least 2 dimensions. + If input has shape (M, N), each of its M rows defines a point in N-dimensional space. + If input has three or more dimensions, all but the last dimension are flattened. + + Each dimension is independently associated with its own strictly increasing sequence + of bin edges. Bin edges may be specified explicitly by passing a sequence of 1D + tensors. Alternatively, bin edges may be constructed automatically by passing a + sequence of integers specifying the number of equal-width bins in each dimension. + + For each N-dimensional point in input: + - Each of its coordinates is binned independently among the bin edges + corresponding to its dimension + - Binning results are combined to identify the N-dimensional bin (if any) + into which the point falls + - If the point falls into a bin, the bin's count (or total weight) is incremented + - Points which do not fall into any bin do not contribute to the output + + :attr:`bins` can be a sequence of N 1D tensors, a sequence of N ints, or a single int. + + If :attr:`bins` is a sequence of N 1D tensors, it explicitly specifies the N sequences + of bin edges. Each 1D tensor should contain a strictly increasing sequence with at + least one element. A sequence of K bin edges defines K-1 bins, explicitly specifying + the left and right edges of all bins. Every bin is inclusive of its left edge. Only + the rightmost bin is inclusive of its right edge. + + If :attr:`bins` is a sequence of N ints, it specifies the number of equal-width bins + in each dimension. By default, the leftmost and rightmost bin edges in each dimension + are determined by the minimum and maximum elements of the input tensor in the + corresponding dimension. The :attr:`range` argument can be provided to manually + specify the leftmost and rightmost bin edges in each dimension. + + If :attr:`bins` is an int, it specifies the number of equal-width bins for all dimensions. + + .. note:: + See also :func:`torch.histogram`, which specifically computes 1D histograms. + While :func:`torch.histogramdd` infers the dimensionality of its bins and + binned values from the shape of :attr:`input`, :func:`torch.histogram` + accepts and flattens :attr:`input` of any shape. + + Args: + input (Tensor): the input tensor. + bins: Tensor[], int[], or int. + If Tensor[], defines the sequences of bin edges. + If int[], defines the number of equal-width bins in each dimension. + If int, defines the number of equal-width bins for all dimensions. + Keyword args: + range (sequence of float): Defines the leftmost and rightmost bin edges + in each dimension. + weight (Tensor): By default, each value in the input has weight 1. If a weight + tensor is passed, each N-dimensional coordinate in input + contributes its associated weight towards its bin's result. + The weight tensor should have the same shape as the :attr:`input` + tensor excluding its innermost dimension N. + density (bool): If False (default), the result will contain the count (or total weight) + in each bin. If True, each count (weight) is divided by the total count + (total weight), then divided by the volume of its associated bin. + Returns: + hist (Tensor): N-dimensional Tensor containing the values of the histogram. + bin_edges(Tensor[]): sequence of N 1D Tensors containing the bin edges. + + Example:: + + >>> torch.histogramdd(torch.tensor([[0., 1.], [1., 0.], [2., 0.], [2., 2.]]), bins=[3, 3], + ... weight=torch.tensor([1., 2., 4., 8.])) + torch.return_types.histogramdd( + hist=tensor([[0., 1., 0.], + [2., 0., 0.], + [4., 0., 8.]]), + bin_edges=(tensor([0.0000, 0.6667, 1.3333, 2.0000]), + tensor([0.0000, 0.6667, 1.3333, 2.0000]))) + + >>> torch.histogramdd(torch.tensor([[0., 0.], [1., 1.], [2., 2.]]), bins=[2, 2], + ... range=[0., 1., 0., 1.], density=True) + torch.return_types.histogramdd( + hist=tensor([[2., 0.], + [0., 2.]]), + bin_edges=(tensor([0.0000, 0.5000, 1.0000]), + tensor([0.0000, 0.5000, 1.0000]))) + """ + +def hsmm(input: Tensor, mat2: Tensor) -> Tensor: ... +@overload +def hsplit(input: Tensor, sections: _int) -> tuple[Tensor, ...]: + r""" + hsplit(input, indices_or_sections) -> List of Tensors + + Splits :attr:`input`, a tensor with one or more dimensions, into multiple tensors + horizontally according to :attr:`indices_or_sections`. Each split is a view of + :attr:`input`. + + If :attr:`input` is one dimensional this is equivalent to calling + torch.tensor_split(input, indices_or_sections, dim=0) (the split dimension is + zero), and if :attr:`input` has two or more dimensions it's equivalent to calling + torch.tensor_split(input, indices_or_sections, dim=1) (the split dimension is 1), + except that if :attr:`indices_or_sections` is an integer it must evenly divide + the split dimension or a runtime error will be thrown. + + This function is based on NumPy's :func:`numpy.hsplit`. + + Args: + input (Tensor): tensor to split. + indices_or_sections (int or list or tuple of ints): See argument in :func:`torch.tensor_split`. + + Example:: + + >>> t = torch.arange(16.0).reshape(4,4) + >>> t + tensor([[ 0., 1., 2., 3.], + [ 4., 5., 6., 7.], + [ 8., 9., 10., 11.], + [12., 13., 14., 15.]]) + >>> torch.hsplit(t, 2) + (tensor([[ 0., 1.], + [ 4., 5.], + [ 8., 9.], + [12., 13.]]), + tensor([[ 2., 3.], + [ 6., 7.], + [10., 11.], + [14., 15.]])) + >>> torch.hsplit(t, [3, 6]) + (tensor([[ 0., 1., 2.], + [ 4., 5., 6.], + [ 8., 9., 10.], + [12., 13., 14.]]), + tensor([[ 3.], + [ 7.], + [11.], + [15.]]), + tensor([], size=(4, 0))) + """ + +@overload +def hsplit(input: Tensor, indices: _size) -> tuple[Tensor, ...]: + r""" + hsplit(input, indices_or_sections) -> List of Tensors + + Splits :attr:`input`, a tensor with one or more dimensions, into multiple tensors + horizontally according to :attr:`indices_or_sections`. Each split is a view of + :attr:`input`. + + If :attr:`input` is one dimensional this is equivalent to calling + torch.tensor_split(input, indices_or_sections, dim=0) (the split dimension is + zero), and if :attr:`input` has two or more dimensions it's equivalent to calling + torch.tensor_split(input, indices_or_sections, dim=1) (the split dimension is 1), + except that if :attr:`indices_or_sections` is an integer it must evenly divide + the split dimension or a runtime error will be thrown. + + This function is based on NumPy's :func:`numpy.hsplit`. + + Args: + input (Tensor): tensor to split. + indices_or_sections (int or list or tuple of ints): See argument in :func:`torch.tensor_split`. + + Example:: + + >>> t = torch.arange(16.0).reshape(4,4) + >>> t + tensor([[ 0., 1., 2., 3.], + [ 4., 5., 6., 7.], + [ 8., 9., 10., 11.], + [12., 13., 14., 15.]]) + >>> torch.hsplit(t, 2) + (tensor([[ 0., 1.], + [ 4., 5.], + [ 8., 9.], + [12., 13.]]), + tensor([[ 2., 3.], + [ 6., 7.], + [10., 11.], + [14., 15.]])) + >>> torch.hsplit(t, [3, 6]) + (tensor([[ 0., 1., 2.], + [ 4., 5., 6.], + [ 8., 9., 10.], + [12., 13., 14.]]), + tensor([[ 3.], + [ 7.], + [11.], + [15.]]), + tensor([], size=(4, 0))) + """ + +def hspmm( + mat1: Tensor, + mat2: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + hspmm(mat1, mat2, *, out=None) -> Tensor + + Performs a matrix multiplication of a :ref:`sparse COO matrix + ` :attr:`mat1` and a strided matrix :attr:`mat2`. The + result is a (1 + 1)-dimensional :ref:`hybrid COO matrix + `. + + Args: + mat1 (Tensor): the first sparse matrix to be matrix multiplied + mat2 (Tensor): the second strided matrix to be matrix multiplied + + Keyword args: + out (Tensor, optional): the output tensor. + """ + +def hstack( + tensors: tuple[Tensor, ...] | list[Tensor], + *, + out: Tensor | None = None, +) -> Tensor: + r""" + hstack(tensors, *, out=None) -> Tensor + + Stack tensors in sequence horizontally (column wise). + + This is equivalent to concatenation along the first axis for 1-D tensors, and along the second axis for all other tensors. + + Args: + tensors (sequence of Tensors): sequence of tensors to concatenate + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([1, 2, 3]) + >>> b = torch.tensor([4, 5, 6]) + >>> torch.hstack((a,b)) + tensor([1, 2, 3, 4, 5, 6]) + >>> a = torch.tensor([[1],[2],[3]]) + >>> b = torch.tensor([[4],[5],[6]]) + >>> torch.hstack((a,b)) + tensor([[1, 4], + [2, 5], + [3, 6]]) + """ + +def hypot( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + hypot(input, other, *, out=None) -> Tensor + + Given the legs of a right triangle, return its hypotenuse. + + .. math:: + \text{out}_{i} = \sqrt{\text{input}_{i}^{2} + \text{other}_{i}^{2}} + + The shapes of ``input`` and ``other`` must be + :ref:`broadcastable `. + + Args: + input (Tensor): the first input tensor + other (Tensor): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.hypot(torch.tensor([4.0]), torch.tensor([3.0, 4.0, 5.0])) + tensor([5.0000, 5.6569, 6.4031]) + """ + +def i0(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + i0(input, *, out=None) -> Tensor + + Alias for :func:`torch.special.i0`. + """ + +def i0_(input: Tensor) -> Tensor: ... +def igamma( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + igamma(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.special.gammainc`. + """ + +def igammac( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + igammac(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.special.gammaincc`. + """ + +def imag(input: Tensor) -> Tensor: + r""" + imag(input) -> Tensor + + Returns a new tensor containing imaginary values of the :attr:`self` tensor. + The returned tensor and :attr:`self` share the same underlying storage. + + .. warning:: + :func:`imag` is only supported for tensors with complex dtypes. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> x=torch.randn(4, dtype=torch.cfloat) + >>> x + tensor([(0.3100+0.3553j), (-0.5445-0.7896j), (-1.6492-0.0633j), (-0.0638-0.8119j)]) + >>> x.imag + tensor([ 0.3553, -0.7896, -0.0633, -0.8119]) + """ + +@overload +def index_add( + input: Tensor, + dim: _int, + index: Tensor, + source: Tensor, + *, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + index_add(input: Tensor, dim: int, index: Tensor, source: Tensor, *, alpha: Union[Number, _complex] = 1, out: Optional[Tensor]) -> Tensor # noqa: B950 + + See :meth:`~Tensor.index_add_` for function description. + """ + +@overload +def index_add( + input: Tensor, + dim: str | EllipsisType | None, + index: Tensor, + source: Tensor, + *, + alpha: Number | _complex = 1, +) -> Tensor: + r""" + index_add(input: Tensor, dim: int, index: Tensor, source: Tensor, *, alpha: Union[Number, _complex] = 1, out: Optional[Tensor]) -> Tensor # noqa: B950 + + See :meth:`~Tensor.index_add_` for function description. + """ + +@overload +def index_copy( + input: Tensor, + dim: _int, + index: Tensor, + source: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + index_copy(input: Tensor, dim: int, index: Tensor, source: Tensor, *, out: Optional[Tensor]) -> Tensor + + See :meth:`~Tensor.index_copy_` for function description. + """ + +@overload +def index_copy( + input: Tensor, + dim: str | EllipsisType | None, + index: Tensor, + source: Tensor, +) -> Tensor: + r""" + index_copy(input: Tensor, dim: int, index: Tensor, source: Tensor, *, out: Optional[Tensor]) -> Tensor + + See :meth:`~Tensor.index_copy_` for function description. + """ + +@overload +def index_fill( + input: Tensor, + dim: _int, + index: Tensor, + value: Tensor, +) -> Tensor: ... +@overload +def index_fill( + input: Tensor, + dim: str | EllipsisType | None, + index: Tensor, + value: Tensor, +) -> Tensor: ... +@overload +def index_fill( + input: Tensor, + dim: _int, + index: Tensor, + value: Number | _complex, +) -> Tensor: ... +@overload +def index_fill( + input: Tensor, + dim: str | EllipsisType | None, + index: Tensor, + value: Number | _complex, +) -> Tensor: ... +def index_put( + input: Tensor, + indices: tuple[Tensor | None, ...] | list[Tensor | None] | None, + values: Tensor, + accumulate: _bool = False, +) -> Tensor: ... +def index_put_( + input: Tensor, + indices: tuple[Tensor | None, ...] | list[Tensor | None] | None, + values: Tensor, + accumulate: _bool = False, +) -> Tensor: ... +def index_reduce( + input: Tensor, + dim: _int, + index: Tensor, + source: Tensor, + reduce: str, + *, + include_self: _bool = True, + out: Tensor | None = None, +) -> Tensor: + r""" + index_reduce(input: Tensor, dim: int, index: Tensor, source: Tensor, reduce: str, *, include_self: bool = True, out: Optional[Tensor]) -> Tensor # noqa: B950 + + See :meth:`~Tensor.index_reduce_` for function description. + """ + +@overload +def index_select( + input: Tensor, + dim: _int, + index: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + index_select(input, dim, index, *, out=None) -> Tensor + + Returns a new tensor which indexes the :attr:`input` tensor along dimension + :attr:`dim` using the entries in :attr:`index`. + + The returned tensor has the same number of dimensions as the original tensor + (:attr:`input`). The :attr:`dim`\ th dimension has the same size as the length + of :attr:`index`; other dimensions have the same size as in the original tensor. + + .. note:: The returned tensor does **not** use the same storage as the original + tensor. If :attr:`out` has a different shape than expected, we + silently change it to the correct shape, reallocating the underlying + storage if necessary. + + Args: + input (Tensor): the input tensor. + dim (int): the dimension in which we index + index (IntTensor or LongTensor): the 1-D tensor containing the indices to index + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> x = torch.randn(3, 4) + >>> x + tensor([[ 0.1427, 0.0231, -0.5414, -1.0009], + [-0.4664, 0.2647, -0.1228, -1.1068], + [-1.1734, -0.6571, 0.7230, -0.6004]]) + >>> indices = torch.tensor([0, 2]) + >>> torch.index_select(x, 0, indices) + tensor([[ 0.1427, 0.0231, -0.5414, -1.0009], + [-1.1734, -0.6571, 0.7230, -0.6004]]) + >>> torch.index_select(x, 1, indices) + tensor([[ 0.1427, -0.5414], + [-0.4664, -0.1228], + [-1.1734, 0.7230]]) + """ + +@overload +def index_select( + input: Tensor, + dim: str | EllipsisType | None, + index: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + index_select(input, dim, index, *, out=None) -> Tensor + + Returns a new tensor which indexes the :attr:`input` tensor along dimension + :attr:`dim` using the entries in :attr:`index`. + + The returned tensor has the same number of dimensions as the original tensor + (:attr:`input`). The :attr:`dim`\ th dimension has the same size as the length + of :attr:`index`; other dimensions have the same size as in the original tensor. + + .. note:: The returned tensor does **not** use the same storage as the original + tensor. If :attr:`out` has a different shape than expected, we + silently change it to the correct shape, reallocating the underlying + storage if necessary. + + Args: + input (Tensor): the input tensor. + dim (int): the dimension in which we index + index (IntTensor or LongTensor): the 1-D tensor containing the indices to index + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> x = torch.randn(3, 4) + >>> x + tensor([[ 0.1427, 0.0231, -0.5414, -1.0009], + [-0.4664, 0.2647, -0.1228, -1.1068], + [-1.1734, -0.6571, 0.7230, -0.6004]]) + >>> indices = torch.tensor([0, 2]) + >>> torch.index_select(x, 0, indices) + tensor([[ 0.1427, 0.0231, -0.5414, -1.0009], + [-1.1734, -0.6571, 0.7230, -0.6004]]) + >>> torch.index_select(x, 1, indices) + tensor([[ 0.1427, -0.5414], + [-0.4664, -0.1228], + [-1.1734, 0.7230]]) + """ + +def indices_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + Performs the same operation as :func:`torch.indices`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def init_num_threads() -> None: ... +def inner( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + inner(input, other, *, out=None) -> Tensor + + Computes the dot product for 1D tensors. For higher dimensions, sums the product + of elements from :attr:`input` and :attr:`other` along their last dimension. + + .. note:: + + If either :attr:`input` or :attr:`other` is a scalar, the result is equivalent + to `torch.mul(input, other)`. + + If both :attr:`input` and :attr:`other` are non-scalars, the size of their last + dimension must match and the result is equivalent to `torch.tensordot(input, + other, dims=([-1], [-1]))` + + Args: + input (Tensor): First input tensor + other (Tensor): Second input tensor + + Keyword args: + out (Tensor, optional): Optional output tensor to write result into. The output + shape is `input.shape[:-1] + other.shape[:-1]`. + + Example:: + + # Dot product + >>> torch.inner(torch.tensor([1, 2, 3]), torch.tensor([0, 2, 1])) + tensor(7) + + # Multidimensional input tensors + >>> a = torch.randn(2, 3) + >>> a + tensor([[0.8173, 1.0874, 1.1784], + [0.3279, 0.1234, 2.7894]]) + >>> b = torch.randn(2, 4, 3) + >>> b + tensor([[[-0.4682, -0.7159, 0.1506], + [ 0.4034, -0.3657, 1.0387], + [ 0.9892, -0.6684, 0.1774], + [ 0.9482, 1.3261, 0.3917]], + + [[ 0.4537, 0.7493, 1.1724], + [ 0.2291, 0.5749, -0.2267], + [-0.7920, 0.3607, -0.3701], + [ 1.3666, -0.5850, -1.7242]]]) + >>> torch.inner(a, b) + tensor([[[-0.9837, 1.1560, 0.2907, 2.6785], + [ 2.5671, 0.5452, -0.6912, -1.5509]], + + [[ 0.1782, 2.9843, 0.7366, 1.5672], + [ 3.5115, -0.4864, -1.2476, -4.4337]]]) + + # Scalar input + >>> torch.inner(a, torch.tensor(2)) + tensor([[1.6347, 2.1748, 2.3567], + [0.6558, 0.2469, 5.5787]]) + """ + +def instance_norm( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor | None, + running_var: Tensor | None, + use_input_stats: _bool, + momentum: _float, + eps: _float, + cudnn_enabled: _bool, +) -> Tensor: ... +def int_repr(input: Tensor) -> Tensor: ... +def inverse(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + inverse(input, *, out=None) -> Tensor + + Alias for :func:`torch.linalg.inv` + """ + +def is_complex(input: Tensor) -> _bool: + r""" + is_complex(input: Tensor) -> bool + + Returns True if the data type of :attr:`input` is a complex data type i.e., + one of ``torch.complex64``, and ``torch.complex128``. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> torch.is_complex(torch.tensor([1, 2, 3], dtype=torch.complex64)) + True + >>> torch.is_complex(torch.tensor([1, 2, 3], dtype=torch.complex128)) + True + >>> torch.is_complex(torch.tensor([1, 2, 3], dtype=torch.int32)) + False + >>> torch.is_complex(torch.tensor([1.0, 2.0, 3.0], dtype=torch.float16)) + False + """ + +def is_conj(input: Tensor) -> _bool: + r""" + is_conj(input) -> (bool) + + Returns True if the :attr:`input` is a conjugated tensor, i.e. its conjugate bit is set to `True`. + + Args: + input (Tensor): the input tensor. + """ + +def is_distributed(input: Tensor) -> _bool: ... +def is_floating_point(input: Tensor) -> _bool: + r""" + is_floating_point(input: Tensor) -> bool + + Returns True if the data type of :attr:`input` is a floating point data type i.e., + one of ``torch.float64``, ``torch.float32``, ``torch.float16``, and ``torch.bfloat16``. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> torch.is_floating_point(torch.tensor([1.0, 2.0, 3.0])) + True + >>> torch.is_floating_point(torch.tensor([1, 2, 3], dtype=torch.int32)) + False + >>> torch.is_floating_point(torch.tensor([1.0, 2.0, 3.0], dtype=torch.float16)) + True + >>> torch.is_floating_point(torch.tensor([1, 2, 3], dtype=torch.complex64)) + False + """ + +def is_grad_enabled() -> _bool: + r""" + is_grad_enabled() -> (bool) + + Returns True if grad mode is currently enabled. + """ + +def is_inference(input: Tensor) -> _bool: + r""" + is_inference(input) -> (bool) + + Returns True if :attr:`input` is an inference tensor. + + A non-view tensor is an inference tensor if and only if it was + allocated during inference mode. A view tensor is an inference + tensor if and only if the tensor it is a view of is an inference tensor. + + For details on inference mode please see + `Inference Mode `_. + + Args: + input (Tensor): the input tensor. + """ + +def is_inference_mode_enabled() -> _bool: + r""" + is_inference_mode_enabled() -> (bool) + + Returns True if inference mode is currently enabled. + """ + +def is_neg(input: Tensor) -> _bool: ... +def is_nonzero(input: Tensor) -> _bool: + r""" + is_nonzero(input) -> (bool) + + Returns True if the :attr:`input` is a single element tensor which is not equal to zero + after type conversions. + i.e. not equal to ``torch.tensor([0.])`` or ``torch.tensor([0])`` or + ``torch.tensor([False])``. + Throws a ``RuntimeError`` if ``torch.numel() != 1`` (even in case + of sparse tensors). + + Args: + input (Tensor): the input tensor. + + Examples:: + + >>> torch.is_nonzero(torch.tensor([0.])) + False + >>> torch.is_nonzero(torch.tensor([1.5])) + True + >>> torch.is_nonzero(torch.tensor([False])) + False + >>> torch.is_nonzero(torch.tensor([3])) + True + >>> torch.is_nonzero(torch.tensor([1, 3, 5])) + Traceback (most recent call last): + ... + RuntimeError: Boolean value of Tensor with more than one value is ambiguous + >>> torch.is_nonzero(torch.tensor([])) + Traceback (most recent call last): + ... + RuntimeError: Boolean value of Tensor with no values is ambiguous + """ + +def is_same_size(input: Tensor, other: Tensor) -> _bool: ... +def is_signed(input: Tensor) -> _bool: ... +def is_vulkan_available() -> _bool: ... +def isclose( + input: Tensor, + other: Tensor, + rtol: _float = 1e-05, + atol: _float = 1e-08, + equal_nan: _bool = False, +) -> Tensor: + r""" + isclose(input, other, rtol=1e-05, atol=1e-08, equal_nan=False) -> Tensor + + Returns a new tensor with boolean elements representing if each element of + :attr:`input` is "close" to the corresponding element of :attr:`other`. + Closeness is defined as: + + .. math:: + \lvert \text{input}_i - \text{other}_i \rvert \leq \texttt{rtol} \times \lvert \text{other}_i \rvert + \texttt{atol} + + + where :attr:`input` and :attr:`other` are finite. Where :attr:`input` + and/or :attr:`other` are nonfinite they are close if and only if + they are equal, with NaNs being considered equal to each other when + :attr:`equal_nan` is True. + + Args: + input (Tensor): first tensor to compare + other (Tensor): second tensor to compare + rtol (float, optional): relative tolerance. Default: 1e-05 + atol (float, optional): absolute tolerance. Default: 1e-08 + equal_nan (bool, optional): if ``True``, then two ``NaN`` s will be considered equal. Default: ``False`` + + Examples:: + + >>> torch.isclose(torch.tensor((1., 2, 3)), torch.tensor((1 + 1e-10, 3, 4))) + tensor([ True, False, False]) + >>> torch.isclose(torch.tensor((float('inf'), 4)), torch.tensor((float('inf'), 6)), rtol=.5) + tensor([True, True]) + """ + +def isfinite(input: Tensor) -> Tensor: + r""" + isfinite(input) -> Tensor + + Returns a new tensor with boolean elements representing if each element is `finite` or not. + + Real values are finite when they are not NaN, negative infinity, or infinity. + Complex values are finite when both their real and imaginary parts are finite. + + Args: + input (Tensor): the input tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is finite and False elsewhere + + Example:: + + >>> torch.isfinite(torch.tensor([1, float('inf'), 2, float('-inf'), float('nan')])) + tensor([True, False, True, False, False]) + """ + +@overload +def isin( + elements: Tensor, + test_elements: Tensor, + *, + assume_unique: _bool = False, + invert: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + isin(elements, test_elements, *, assume_unique=False, invert=False) -> Tensor + + Tests if each element of :attr:`elements` is in :attr:`test_elements`. Returns + a boolean tensor of the same shape as :attr:`elements` that is True for elements + in :attr:`test_elements` and False otherwise. + + .. note:: + One of :attr:`elements` or :attr:`test_elements` can be a scalar, but not both. + + Args: + elements (Tensor or Scalar): Input elements + test_elements (Tensor or Scalar): Values against which to test for each input element + assume_unique (bool, optional): If True, assumes both :attr:`elements` and + :attr:`test_elements` contain unique elements, which can speed up the + calculation. Default: False + invert (bool, optional): If True, inverts the boolean return tensor, resulting in True + values for elements *not* in :attr:`test_elements`. Default: False + + Returns: + A boolean tensor of the same shape as :attr:`elements` that is True for elements in + :attr:`test_elements` and False otherwise + + Example: + >>> torch.isin(torch.tensor([[1, 2], [3, 4]]), torch.tensor([2, 3])) + tensor([[False, True], + [ True, False]]) + """ + +@overload +def isin( + element: Number | _complex, + test_elements: Tensor, + *, + assume_unique: _bool = False, + invert: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + isin(elements, test_elements, *, assume_unique=False, invert=False) -> Tensor + + Tests if each element of :attr:`elements` is in :attr:`test_elements`. Returns + a boolean tensor of the same shape as :attr:`elements` that is True for elements + in :attr:`test_elements` and False otherwise. + + .. note:: + One of :attr:`elements` or :attr:`test_elements` can be a scalar, but not both. + + Args: + elements (Tensor or Scalar): Input elements + test_elements (Tensor or Scalar): Values against which to test for each input element + assume_unique (bool, optional): If True, assumes both :attr:`elements` and + :attr:`test_elements` contain unique elements, which can speed up the + calculation. Default: False + invert (bool, optional): If True, inverts the boolean return tensor, resulting in True + values for elements *not* in :attr:`test_elements`. Default: False + + Returns: + A boolean tensor of the same shape as :attr:`elements` that is True for elements in + :attr:`test_elements` and False otherwise + + Example: + >>> torch.isin(torch.tensor([[1, 2], [3, 4]]), torch.tensor([2, 3])) + tensor([[False, True], + [ True, False]]) + """ + +@overload +def isin( + elements: Tensor, + test_element: Number | _complex, + *, + assume_unique: _bool = False, + invert: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + isin(elements, test_elements, *, assume_unique=False, invert=False) -> Tensor + + Tests if each element of :attr:`elements` is in :attr:`test_elements`. Returns + a boolean tensor of the same shape as :attr:`elements` that is True for elements + in :attr:`test_elements` and False otherwise. + + .. note:: + One of :attr:`elements` or :attr:`test_elements` can be a scalar, but not both. + + Args: + elements (Tensor or Scalar): Input elements + test_elements (Tensor or Scalar): Values against which to test for each input element + assume_unique (bool, optional): If True, assumes both :attr:`elements` and + :attr:`test_elements` contain unique elements, which can speed up the + calculation. Default: False + invert (bool, optional): If True, inverts the boolean return tensor, resulting in True + values for elements *not* in :attr:`test_elements`. Default: False + + Returns: + A boolean tensor of the same shape as :attr:`elements` that is True for elements in + :attr:`test_elements` and False otherwise + + Example: + >>> torch.isin(torch.tensor([[1, 2], [3, 4]]), torch.tensor([2, 3])) + tensor([[False, True], + [ True, False]]) + """ + +def isinf(input: Tensor) -> Tensor: + r""" + isinf(input) -> Tensor + + Tests if each element of :attr:`input` is infinite + (positive or negative infinity) or not. + + .. note:: + Complex values are infinite when their real or imaginary part is + infinite. + + Args: + input (Tensor): the input tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is infinite and False elsewhere + + Example:: + + >>> torch.isinf(torch.tensor([1, float('inf'), 2, float('-inf'), float('nan')])) + tensor([False, True, False, True, False]) + """ + +def isnan(input: Tensor) -> Tensor: + r""" + isnan(input) -> Tensor + + Returns a new tensor with boolean elements representing if each element of :attr:`input` + is NaN or not. Complex values are considered NaN when either their real + and/or imaginary part is NaN. + + Arguments: + input (Tensor): the input tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is NaN and False elsewhere + + Example:: + + >>> torch.isnan(torch.tensor([1, float('nan'), 2])) + tensor([False, True, False]) + """ + +def isneginf(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + isneginf(input, *, out=None) -> Tensor + Tests if each element of :attr:`input` is negative infinity or not. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([-float('inf'), float('inf'), 1.2]) + >>> torch.isneginf(a) + tensor([ True, False, False]) + """ + +def isposinf(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + isposinf(input, *, out=None) -> Tensor + Tests if each element of :attr:`input` is positive infinity or not. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([-float('inf'), float('inf'), 1.2]) + >>> torch.isposinf(a) + tensor([False, True, False]) + """ + +def isreal(input: Tensor) -> Tensor: + r""" + isreal(input) -> Tensor + + Returns a new tensor with boolean elements representing if each element of :attr:`input` is real-valued or not. + All real-valued types are considered real. Complex values are considered real when their imaginary part is 0. + + Arguments: + input (Tensor): the input tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is real and False elsewhere + + Example:: + + >>> torch.isreal(torch.tensor([1, 1+1j, 2+0j])) + tensor([True, False, True]) + """ + +def istft( + input: Tensor, + n_fft: _int, + hop_length: _int | None = None, + win_length: _int | None = None, + window: Tensor | None = None, + center: _bool = True, + normalized: _bool = False, + onesided: _bool | None = None, + length: _int | None = None, + return_complex: _bool = False, +) -> Tensor: ... +@overload +def kaiser_window( + window_length: _int, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + kaiser_window(window_length, periodic=True, beta=12.0, *, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Computes the Kaiser window with window length :attr:`window_length` and shape parameter :attr:`beta`. + + Let I_0 be the zeroth order modified Bessel function of the first kind (see :func:`torch.i0`) and + ``N = L - 1`` if :attr:`periodic` is False and ``L`` if :attr:`periodic` is True, + where ``L`` is the :attr:`window_length`. This function computes: + + .. math:: + out_i = I_0 \left( \beta \sqrt{1 - \left( {\frac{i - N/2}{N/2}} \right) ^2 } \right) / I_0( \beta ) + + Calling ``torch.kaiser_window(L, B, periodic=True)`` is equivalent to calling + ``torch.kaiser_window(L + 1, B, periodic=False)[:-1])``. + The :attr:`periodic` argument is intended as a helpful shorthand + to produce a periodic window as input to functions like :func:`torch.stft`. + + .. note:: + If :attr:`window_length` is one, then the returned window is a single element tensor containing a one. + + + Args: + window_length (int): length of the window. + periodic (bool, optional): If True, returns a periodic window suitable for use in spectral analysis. + If False, returns a symmetric window suitable for use in filter design. + beta (float, optional): shape parameter for the window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + """ + +@overload +def kaiser_window( + window_length: _int, + periodic: _bool, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + kaiser_window(window_length, periodic=True, beta=12.0, *, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Computes the Kaiser window with window length :attr:`window_length` and shape parameter :attr:`beta`. + + Let I_0 be the zeroth order modified Bessel function of the first kind (see :func:`torch.i0`) and + ``N = L - 1`` if :attr:`periodic` is False and ``L`` if :attr:`periodic` is True, + where ``L`` is the :attr:`window_length`. This function computes: + + .. math:: + out_i = I_0 \left( \beta \sqrt{1 - \left( {\frac{i - N/2}{N/2}} \right) ^2 } \right) / I_0( \beta ) + + Calling ``torch.kaiser_window(L, B, periodic=True)`` is equivalent to calling + ``torch.kaiser_window(L + 1, B, periodic=False)[:-1])``. + The :attr:`periodic` argument is intended as a helpful shorthand + to produce a periodic window as input to functions like :func:`torch.stft`. + + .. note:: + If :attr:`window_length` is one, then the returned window is a single element tensor containing a one. + + + Args: + window_length (int): length of the window. + periodic (bool, optional): If True, returns a periodic window suitable for use in spectral analysis. + If False, returns a symmetric window suitable for use in filter design. + beta (float, optional): shape parameter for the window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + """ + +@overload +def kaiser_window( + window_length: _int, + periodic: _bool, + beta: _float, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + kaiser_window(window_length, periodic=True, beta=12.0, *, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Computes the Kaiser window with window length :attr:`window_length` and shape parameter :attr:`beta`. + + Let I_0 be the zeroth order modified Bessel function of the first kind (see :func:`torch.i0`) and + ``N = L - 1`` if :attr:`periodic` is False and ``L`` if :attr:`periodic` is True, + where ``L`` is the :attr:`window_length`. This function computes: + + .. math:: + out_i = I_0 \left( \beta \sqrt{1 - \left( {\frac{i - N/2}{N/2}} \right) ^2 } \right) / I_0( \beta ) + + Calling ``torch.kaiser_window(L, B, periodic=True)`` is equivalent to calling + ``torch.kaiser_window(L + 1, B, periodic=False)[:-1])``. + The :attr:`periodic` argument is intended as a helpful shorthand + to produce a periodic window as input to functions like :func:`torch.stft`. + + .. note:: + If :attr:`window_length` is one, then the returned window is a single element tensor containing a one. + + + Args: + window_length (int): length of the window. + periodic (bool, optional): If True, returns a periodic window suitable for use in spectral analysis. + If False, returns a symmetric window suitable for use in filter design. + beta (float, optional): shape parameter for the window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned window tensor. Only + ``torch.strided`` (dense layout) is supported. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + """ + +def kl_div( + input: Tensor, + target: Tensor, + reduction: _int = 1, + *, + log_target: _bool = False, +) -> Tensor: ... +def kron( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + kron(input, other, *, out=None) -> Tensor + + Computes the Kronecker product, denoted by :math:`\otimes`, of :attr:`input` and :attr:`other`. + + If :attr:`input` is a :math:`(a_0 \times a_1 \times \dots \times a_n)` tensor and :attr:`other` is a + :math:`(b_0 \times b_1 \times \dots \times b_n)` tensor, the result will be a + :math:`(a_0*b_0 \times a_1*b_1 \times \dots \times a_n*b_n)` tensor with the following entries: + + .. math:: + (\text{input} \otimes \text{other})_{k_0, k_1, \dots, k_n} = + \text{input}_{i_0, i_1, \dots, i_n} * \text{other}_{j_0, j_1, \dots, j_n}, + + where :math:`k_t = i_t * b_t + j_t` for :math:`0 \leq t \leq n`. + If one tensor has fewer dimensions than the other it is unsqueezed until it has the same number of dimensions. + + Supports real-valued and complex-valued inputs. + + .. note:: + This function generalizes the typical definition of the Kronecker product for two matrices to two tensors, + as described above. When :attr:`input` is a :math:`(m \times n)` matrix and :attr:`other` is a + :math:`(p \times q)` matrix, the result will be a :math:`(p*m \times q*n)` block matrix: + + .. math:: + \mathbf{A} \otimes \mathbf{B}=\begin{bmatrix} + a_{11} \mathbf{B} & \cdots & a_{1 n} \mathbf{B} \\ + \vdots & \ddots & \vdots \\ + a_{m 1} \mathbf{B} & \cdots & a_{m n} \mathbf{B} \end{bmatrix} + + where :attr:`input` is :math:`\mathbf{A}` and :attr:`other` is :math:`\mathbf{B}`. + + Arguments: + input (Tensor) + other (Tensor) + + Keyword args: + out (Tensor, optional): The output tensor. Ignored if ``None``. Default: ``None`` + + Examples:: + + >>> mat1 = torch.eye(2) + >>> mat2 = torch.ones(2, 2) + >>> torch.kron(mat1, mat2) + tensor([[1., 1., 0., 0.], + [1., 1., 0., 0.], + [0., 0., 1., 1.], + [0., 0., 1., 1.]]) + + >>> mat1 = torch.eye(2) + >>> mat2 = torch.arange(1, 5).reshape(2, 2) + >>> torch.kron(mat1, mat2) + tensor([[1., 2., 0., 0.], + [3., 4., 0., 0.], + [0., 0., 1., 2.], + [0., 0., 3., 4.]]) + """ + +@overload +def kthvalue( + input: Tensor, + k: _int | SymInt, + dim: _int = -1, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.kthvalue: + r""" + kthvalue(input, k, dim=None, keepdim=False, *, out=None) -> (Tensor, LongTensor) + + Returns a namedtuple ``(values, indices)`` where ``values`` is the :attr:`k` th + smallest element of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`. And ``indices`` is the index location of each element found. + + If :attr:`dim` is not given, the last dimension of the `input` is chosen. + + If :attr:`keepdim` is ``True``, both the :attr:`values` and :attr:`indices` tensors + are the same size as :attr:`input`, except in the dimension :attr:`dim` where + they are of size 1. Otherwise, :attr:`dim` is squeezed + (see :func:`torch.squeeze`), resulting in both the :attr:`values` and + :attr:`indices` tensors having 1 fewer dimension than the :attr:`input` tensor. + + .. note:: + When :attr:`input` is a CUDA tensor and there are multiple valid + :attr:`k` th values, this function may nondeterministically return + :attr:`indices` for any of them. + + Args: + input (Tensor): the input tensor. + k (int): k for the k-th smallest element + dim (int, optional): the dimension to find the kth value along + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the output tuple of (Tensor, LongTensor) + can be optionally given to be used as output buffers + + Example:: + + >>> x = torch.arange(1., 6.) + >>> x + tensor([ 1., 2., 3., 4., 5.]) + >>> torch.kthvalue(x, 4) + torch.return_types.kthvalue(values=tensor(4.), indices=tensor(3)) + + >>> x=torch.arange(1.,7.).resize_(2,3) + >>> x + tensor([[ 1., 2., 3.], + [ 4., 5., 6.]]) + >>> torch.kthvalue(x, 2, 0, True) + torch.return_types.kthvalue(values=tensor([[4., 5., 6.]]), indices=tensor([[1, 1, 1]])) + """ + +@overload +def kthvalue( + input: Tensor, + k: _int | SymInt, + dim: str | EllipsisType | None, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.kthvalue: + r""" + kthvalue(input, k, dim=None, keepdim=False, *, out=None) -> (Tensor, LongTensor) + + Returns a namedtuple ``(values, indices)`` where ``values`` is the :attr:`k` th + smallest element of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`. And ``indices`` is the index location of each element found. + + If :attr:`dim` is not given, the last dimension of the `input` is chosen. + + If :attr:`keepdim` is ``True``, both the :attr:`values` and :attr:`indices` tensors + are the same size as :attr:`input`, except in the dimension :attr:`dim` where + they are of size 1. Otherwise, :attr:`dim` is squeezed + (see :func:`torch.squeeze`), resulting in both the :attr:`values` and + :attr:`indices` tensors having 1 fewer dimension than the :attr:`input` tensor. + + .. note:: + When :attr:`input` is a CUDA tensor and there are multiple valid + :attr:`k` th values, this function may nondeterministically return + :attr:`indices` for any of them. + + Args: + input (Tensor): the input tensor. + k (int): k for the k-th smallest element + dim (int, optional): the dimension to find the kth value along + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the output tuple of (Tensor, LongTensor) + can be optionally given to be used as output buffers + + Example:: + + >>> x = torch.arange(1., 6.) + >>> x + tensor([ 1., 2., 3., 4., 5.]) + >>> torch.kthvalue(x, 4) + torch.return_types.kthvalue(values=tensor(4.), indices=tensor(3)) + + >>> x=torch.arange(1.,7.).resize_(2,3) + >>> x + tensor([[ 1., 2., 3.], + [ 4., 5., 6.]]) + >>> torch.kthvalue(x, 2, 0, True) + torch.return_types.kthvalue(values=tensor([[4., 5., 6.]]), indices=tensor([[1, 1, 1]])) + """ + +def layer_norm( + input: Tensor, + normalized_shape: Sequence[_int | SymInt], + weight: Tensor | None = None, + bias: Tensor | None = None, + eps: _float = 1e-05, + cudnn_enable: _bool = True, +) -> Tensor: ... +def lcm( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + lcm(input, other, *, out=None) -> Tensor + + Computes the element-wise least common multiple (LCM) of :attr:`input` and :attr:`other`. + + Both :attr:`input` and :attr:`other` must have integer types. + + .. note:: + This defines :math:`lcm(0, 0) = 0` and :math:`lcm(0, a) = 0`. + + Args: + input (Tensor): the input tensor. + other (Tensor): the second input tensor + + Keyword arguments: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([5, 10, 15]) + >>> b = torch.tensor([3, 4, 5]) + >>> torch.lcm(a, b) + tensor([15, 20, 15]) + >>> c = torch.tensor([3]) + >>> torch.lcm(a, c) + tensor([15, 30, 15]) + """ + +def lcm_(input: Tensor, other: Tensor) -> Tensor: ... +def ldexp( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + ldexp(input, other, *, out=None) -> Tensor + + Multiplies :attr:`input` by 2 ** :attr:`other`. + + .. math:: + \text{{out}}_i = \text{{input}}_i * 2^\text{{other}}_i + + + Typically this function is used to construct floating point numbers by multiplying + mantissas in :attr:`input` with integral powers of two created from the exponents + in :attr:`other`. + + Args: + input (Tensor): the input tensor. + other (Tensor): a tensor of exponents, typically integers. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.ldexp(torch.tensor([1.]), torch.tensor([1])) + tensor([2.]) + >>> torch.ldexp(torch.tensor([1.0]), torch.tensor([1, 2, 3, 4])) + tensor([ 2., 4., 8., 16.]) + """ + +def ldexp_(input: Tensor, other: Tensor) -> Tensor: ... +@overload +def le( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + le(input, other, *, out=None) -> Tensor + + Computes :math:`\text{input} \leq \text{other}` element-wise. + + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or Scalar): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is less than or equal to + :attr:`other` and False elsewhere + + Example:: + + >>> torch.le(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[True, False], [True, True]]) + """ + +@overload +def le( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + le(input, other, *, out=None) -> Tensor + + Computes :math:`\text{input} \leq \text{other}` element-wise. + + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or Scalar): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is less than or equal to + :attr:`other` and False elsewhere + + Example:: + + >>> torch.le(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[True, False], [True, True]]) + """ + +@overload +def lerp( + input: Tensor, + end: Tensor, + weight: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + lerp(input, end, weight, *, out=None) + + Does a linear interpolation of two tensors :attr:`start` (given by :attr:`input`) and :attr:`end` based + on a scalar or tensor :attr:`weight` and returns the resulting :attr:`out` tensor. + + .. math:: + \text{out}_i = \text{start}_i + \text{weight}_i \times (\text{end}_i - \text{start}_i) + + The shapes of :attr:`start` and :attr:`end` must be + :ref:`broadcastable `. If :attr:`weight` is a tensor, then + the shapes of :attr:`weight`, :attr:`start`, and :attr:`end` must be :ref:`broadcastable `. + + Args: + input (Tensor): the tensor with the starting points + end (Tensor): the tensor with the ending points + weight (float or tensor): the weight for the interpolation formula + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> start = torch.arange(1., 5.) + >>> end = torch.empty(4).fill_(10) + >>> start + tensor([ 1., 2., 3., 4.]) + >>> end + tensor([ 10., 10., 10., 10.]) + >>> torch.lerp(start, end, 0.5) + tensor([ 5.5000, 6.0000, 6.5000, 7.0000]) + >>> torch.lerp(start, end, torch.full_like(start, 0.5)) + tensor([ 5.5000, 6.0000, 6.5000, 7.0000]) + """ + +@overload +def lerp( + input: Tensor, + end: Tensor, + weight: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + lerp(input, end, weight, *, out=None) + + Does a linear interpolation of two tensors :attr:`start` (given by :attr:`input`) and :attr:`end` based + on a scalar or tensor :attr:`weight` and returns the resulting :attr:`out` tensor. + + .. math:: + \text{out}_i = \text{start}_i + \text{weight}_i \times (\text{end}_i - \text{start}_i) + + The shapes of :attr:`start` and :attr:`end` must be + :ref:`broadcastable `. If :attr:`weight` is a tensor, then + the shapes of :attr:`weight`, :attr:`start`, and :attr:`end` must be :ref:`broadcastable `. + + Args: + input (Tensor): the tensor with the starting points + end (Tensor): the tensor with the ending points + weight (float or tensor): the weight for the interpolation formula + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> start = torch.arange(1., 5.) + >>> end = torch.empty(4).fill_(10) + >>> start + tensor([ 1., 2., 3., 4.]) + >>> end + tensor([ 10., 10., 10., 10.]) + >>> torch.lerp(start, end, 0.5) + tensor([ 5.5000, 6.0000, 6.5000, 7.0000]) + >>> torch.lerp(start, end, torch.full_like(start, 0.5)) + tensor([ 5.5000, 6.0000, 6.5000, 7.0000]) + """ + +@overload +def less( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + less(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.lt`. + """ + +@overload +def less( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + less(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.lt`. + """ + +@overload +def less_equal( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + less_equal(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.le`. + """ + +@overload +def less_equal( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + less_equal(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.le`. + """ + +def lgamma(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + lgamma(input, *, out=None) -> Tensor + + Computes the natural logarithm of the absolute value of the gamma function on :attr:`input`. + + .. math:: + \text{out}_{i} = \ln |\Gamma(\text{input}_{i})| + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.arange(0.5, 2, 0.5) + >>> torch.lgamma(a) + tensor([ 0.5724, 0.0000, -0.1208]) + """ + +@overload +def linspace( + start: Number, + end: Number, + steps: _int | None = None, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + linspace(start, end, steps, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Creates a one-dimensional tensor of size :attr:`steps` whose values are evenly + spaced from :attr:`start` to :attr:`end`, inclusive. That is, the value are: + + .. math:: + (\text{start}, + \text{start} + \frac{\text{end} - \text{start}}{\text{steps} - 1}, + \ldots, + \text{start} + (\text{steps} - 2) * \frac{\text{end} - \text{start}}{\text{steps} - 1}, + \text{end}) + + + From PyTorch 1.11 linspace requires the steps argument. Use steps=100 to restore the previous behavior. + + Args: + start (float or Tensor): the starting value for the set of points. If `Tensor`, it must be 0-dimensional + end (float or Tensor): the ending value for the set of points. If `Tensor`, it must be 0-dimensional + steps (int): size of the constructed tensor + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the data type to perform the computation in. + Default: if None, uses the global default dtype (see torch.get_default_dtype()) + when both :attr:`start` and :attr:`end` are real, + and corresponding complex dtype when either is complex. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + + Example:: + + >>> torch.linspace(3, 10, steps=5) + tensor([ 3.0000, 4.7500, 6.5000, 8.2500, 10.0000]) + >>> torch.linspace(-10, 10, steps=5) + tensor([-10., -5., 0., 5., 10.]) + >>> torch.linspace(start=-10, end=10, steps=5) + tensor([-10., -5., 0., 5., 10.]) + >>> torch.linspace(start=-10, end=10, steps=1) + tensor([-10.]) + """ + +@overload +def linspace( + start: Tensor, + end: Tensor, + steps: _int, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + linspace(start, end, steps, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Creates a one-dimensional tensor of size :attr:`steps` whose values are evenly + spaced from :attr:`start` to :attr:`end`, inclusive. That is, the value are: + + .. math:: + (\text{start}, + \text{start} + \frac{\text{end} - \text{start}}{\text{steps} - 1}, + \ldots, + \text{start} + (\text{steps} - 2) * \frac{\text{end} - \text{start}}{\text{steps} - 1}, + \text{end}) + + + From PyTorch 1.11 linspace requires the steps argument. Use steps=100 to restore the previous behavior. + + Args: + start (float or Tensor): the starting value for the set of points. If `Tensor`, it must be 0-dimensional + end (float or Tensor): the ending value for the set of points. If `Tensor`, it must be 0-dimensional + steps (int): size of the constructed tensor + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the data type to perform the computation in. + Default: if None, uses the global default dtype (see torch.get_default_dtype()) + when both :attr:`start` and :attr:`end` are real, + and corresponding complex dtype when either is complex. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + + Example:: + + >>> torch.linspace(3, 10, steps=5) + tensor([ 3.0000, 4.7500, 6.5000, 8.2500, 10.0000]) + >>> torch.linspace(-10, 10, steps=5) + tensor([-10., -5., 0., 5., 10.]) + >>> torch.linspace(start=-10, end=10, steps=5) + tensor([-10., -5., 0., 5., 10.]) + >>> torch.linspace(start=-10, end=10, steps=1) + tensor([-10.]) + """ + +@overload +def linspace( + start: Number | _complex, + end: Tensor, + steps: _int, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + linspace(start, end, steps, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Creates a one-dimensional tensor of size :attr:`steps` whose values are evenly + spaced from :attr:`start` to :attr:`end`, inclusive. That is, the value are: + + .. math:: + (\text{start}, + \text{start} + \frac{\text{end} - \text{start}}{\text{steps} - 1}, + \ldots, + \text{start} + (\text{steps} - 2) * \frac{\text{end} - \text{start}}{\text{steps} - 1}, + \text{end}) + + + From PyTorch 1.11 linspace requires the steps argument. Use steps=100 to restore the previous behavior. + + Args: + start (float or Tensor): the starting value for the set of points. If `Tensor`, it must be 0-dimensional + end (float or Tensor): the ending value for the set of points. If `Tensor`, it must be 0-dimensional + steps (int): size of the constructed tensor + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the data type to perform the computation in. + Default: if None, uses the global default dtype (see torch.get_default_dtype()) + when both :attr:`start` and :attr:`end` are real, + and corresponding complex dtype when either is complex. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + + Example:: + + >>> torch.linspace(3, 10, steps=5) + tensor([ 3.0000, 4.7500, 6.5000, 8.2500, 10.0000]) + >>> torch.linspace(-10, 10, steps=5) + tensor([-10., -5., 0., 5., 10.]) + >>> torch.linspace(start=-10, end=10, steps=5) + tensor([-10., -5., 0., 5., 10.]) + >>> torch.linspace(start=-10, end=10, steps=1) + tensor([-10.]) + """ + +@overload +def linspace( + start: Tensor, + end: Number | _complex, + steps: _int, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + linspace(start, end, steps, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Creates a one-dimensional tensor of size :attr:`steps` whose values are evenly + spaced from :attr:`start` to :attr:`end`, inclusive. That is, the value are: + + .. math:: + (\text{start}, + \text{start} + \frac{\text{end} - \text{start}}{\text{steps} - 1}, + \ldots, + \text{start} + (\text{steps} - 2) * \frac{\text{end} - \text{start}}{\text{steps} - 1}, + \text{end}) + + + From PyTorch 1.11 linspace requires the steps argument. Use steps=100 to restore the previous behavior. + + Args: + start (float or Tensor): the starting value for the set of points. If `Tensor`, it must be 0-dimensional + end (float or Tensor): the ending value for the set of points. If `Tensor`, it must be 0-dimensional + steps (int): size of the constructed tensor + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the data type to perform the computation in. + Default: if None, uses the global default dtype (see torch.get_default_dtype()) + when both :attr:`start` and :attr:`end` are real, + and corresponding complex dtype when either is complex. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + + Example:: + + >>> torch.linspace(3, 10, steps=5) + tensor([ 3.0000, 4.7500, 6.5000, 8.2500, 10.0000]) + >>> torch.linspace(-10, 10, steps=5) + tensor([-10., -5., 0., 5., 10.]) + >>> torch.linspace(start=-10, end=10, steps=5) + tensor([-10., -5., 0., 5., 10.]) + >>> torch.linspace(start=-10, end=10, steps=1) + tensor([-10.]) + """ + +@overload +def linspace( + start: Number | _complex, + end: Number | _complex, + steps: _int, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + linspace(start, end, steps, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Creates a one-dimensional tensor of size :attr:`steps` whose values are evenly + spaced from :attr:`start` to :attr:`end`, inclusive. That is, the value are: + + .. math:: + (\text{start}, + \text{start} + \frac{\text{end} - \text{start}}{\text{steps} - 1}, + \ldots, + \text{start} + (\text{steps} - 2) * \frac{\text{end} - \text{start}}{\text{steps} - 1}, + \text{end}) + + + From PyTorch 1.11 linspace requires the steps argument. Use steps=100 to restore the previous behavior. + + Args: + start (float or Tensor): the starting value for the set of points. If `Tensor`, it must be 0-dimensional + end (float or Tensor): the ending value for the set of points. If `Tensor`, it must be 0-dimensional + steps (int): size of the constructed tensor + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the data type to perform the computation in. + Default: if None, uses the global default dtype (see torch.get_default_dtype()) + when both :attr:`start` and :attr:`end` are real, + and corresponding complex dtype when either is complex. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + + Example:: + + >>> torch.linspace(3, 10, steps=5) + tensor([ 3.0000, 4.7500, 6.5000, 8.2500, 10.0000]) + >>> torch.linspace(-10, 10, steps=5) + tensor([-10., -5., 0., 5., 10.]) + >>> torch.linspace(start=-10, end=10, steps=5) + tensor([-10., -5., 0., 5., 10.]) + >>> torch.linspace(start=-10, end=10, steps=1) + tensor([-10.]) + """ + +def log(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + log(input, *, out=None) -> Tensor + + Returns a new tensor with the natural logarithm of the elements + of :attr:`input`. + + .. math:: + y_{i} = \log_{e} (x_{i}) + + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(5) * 5 + >>> a + tensor([4.7767, 4.3234, 1.2156, 0.2411, 4.5739]) + >>> torch.log(a) + tensor([ 1.5637, 1.4640, 0.1952, -1.4226, 1.5204]) + """ + +def log10(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + log10(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Returns a new tensor with the logarithm to the base 10 of the elements + of :attr:`input`. + + .. math:: + y_{i} = \log_{10} (x_{i}) + + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(5) + >>> a + tensor([ 0.5224, 0.9354, 0.7257, 0.1301, 0.2251]) + + + >>> torch.log10(a) + tensor([-0.2820, -0.0290, -0.1392, -0.8857, -0.6476]) + """ + +def log10_(input: Tensor) -> Tensor: ... +def log1p(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + log1p(input, *, out=None) -> Tensor + + Returns a new tensor with the natural logarithm of (1 + :attr:`input`). + + .. math:: + y_i = \log_{e} (x_i + 1) + + .. note:: This function is more accurate than :func:`torch.log` for small + values of :attr:`input` + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(5) + >>> a + tensor([-1.0090, -0.9923, 1.0249, -0.5372, 0.2492]) + >>> torch.log1p(a) + tensor([ nan, -4.8653, 0.7055, -0.7705, 0.2225]) + """ + +def log1p_(input: Tensor) -> Tensor: ... +def log2(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + log2(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Returns a new tensor with the logarithm to the base 2 of the elements + of :attr:`input`. + + .. math:: + y_{i} = \log_{2} (x_{i}) + + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.rand(5) + >>> a + tensor([ 0.8419, 0.8003, 0.9971, 0.5287, 0.0490]) + + + >>> torch.log2(a) + tensor([-0.2483, -0.3213, -0.0042, -0.9196, -4.3504]) + """ + +def log2_(input: Tensor) -> Tensor: ... +def log_(input: Tensor) -> Tensor: ... +@overload +def log_softmax( + input: Tensor, + dim: _int, + dtype: _dtype | None = None, + *, + out: Tensor | None = None, +) -> Tensor: ... +@overload +def log_softmax( + input: Tensor, + dim: str | EllipsisType | None, + *, + dtype: _dtype | None = None, +) -> Tensor: ... +def logaddexp( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + logaddexp(input, other, *, out=None) -> Tensor + + Logarithm of the sum of exponentiations of the inputs. + + Calculates pointwise :math:`\log\left(e^x + e^y\right)`. This function is useful + in statistics where the calculated probabilities of events may be so small as to + exceed the range of normal floating point numbers. In such cases the logarithm + of the calculated probability is stored. This function allows adding + probabilities stored in such a fashion. + + This op should be disambiguated with :func:`torch.logsumexp` which performs a + reduction on a single tensor. + + Args: + input (Tensor): the input tensor. + other (Tensor): the second input tensor + + Keyword arguments: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.logaddexp(torch.tensor([-1.0]), torch.tensor([-1.0, -2, -3])) + tensor([-0.3069, -0.6867, -0.8731]) + >>> torch.logaddexp(torch.tensor([-100.0, -200, -300]), torch.tensor([-1.0, -2, -3])) + tensor([-1., -2., -3.]) + >>> torch.logaddexp(torch.tensor([1.0, 2000, 30000]), torch.tensor([-1.0, -2, -3])) + tensor([1.1269e+00, 2.0000e+03, 3.0000e+04]) + """ + +def logaddexp2( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + logaddexp2(input, other, *, out=None) -> Tensor + + Logarithm of the sum of exponentiations of the inputs in base-2. + + Calculates pointwise :math:`\log_2\left(2^x + 2^y\right)`. See + :func:`torch.logaddexp` for more details. + + Args: + input (Tensor): the input tensor. + other (Tensor): the second input tensor + + Keyword arguments: + out (Tensor, optional): the output tensor. + """ + +@overload +def logcumsumexp( + input: Tensor, + dim: _int, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + logcumsumexp(input, dim, *, out=None) -> Tensor + Returns the logarithm of the cumulative summation of the exponentiation of + elements of :attr:`input` in the dimension :attr:`dim`. + + For summation index :math:`j` given by `dim` and other indices :math:`i`, the result is + + .. math:: + \text{logcumsumexp}(x)_{ij} = \log \sum\limits_{k=0}^{j} \exp(x_{ik}) + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to do the operation over + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(10) + >>> torch.logcumsumexp(a, dim=0) + tensor([-0.42296738, -0.04462666, 0.86278635, 0.94622083, 1.05277811, + 1.39202815, 1.83525007, 1.84492621, 2.06084887, 2.06844475])) + """ + +@overload +def logcumsumexp( + input: Tensor, + dim: str | EllipsisType | None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + logcumsumexp(input, dim, *, out=None) -> Tensor + Returns the logarithm of the cumulative summation of the exponentiation of + elements of :attr:`input` in the dimension :attr:`dim`. + + For summation index :math:`j` given by `dim` and other indices :math:`i`, the result is + + .. math:: + \text{logcumsumexp}(x)_{ij} = \log \sum\limits_{k=0}^{j} \exp(x_{ik}) + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to do the operation over + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(10) + >>> torch.logcumsumexp(a, dim=0) + tensor([-0.42296738, -0.04462666, 0.86278635, 0.94622083, 1.05277811, + 1.39202815, 1.83525007, 1.84492621, 2.06084887, 2.06844475])) + """ + +def logdet(input: Tensor) -> Tensor: + r""" + logdet(input) -> Tensor + + Calculates log determinant of a square matrix or batches of square matrices. + + It returns ``-inf`` if the input has a determinant of zero, and ``NaN`` if it has + a negative determinant. + + .. note:: + Backward through :meth:`logdet` internally uses SVD results when :attr:`input` + is not invertible. In this case, double backward through :meth:`logdet` will + be unstable in when :attr:`input` doesn't have distinct singular values. See + :func:`torch.linalg.svd` for details. + + .. seealso:: + + :func:`torch.linalg.slogdet` computes the sign (resp. angle) and natural logarithm of the + absolute value of the determinant of real-valued (resp. complex) square matrices. + + Arguments: + input (Tensor): the input tensor of size ``(*, n, n)`` where ``*`` is zero or more + batch dimensions. + + Example:: + + >>> A = torch.randn(3, 3) + >>> torch.det(A) + tensor(0.2611) + >>> torch.logdet(A) + tensor(-1.3430) + >>> A + tensor([[[ 0.9254, -0.6213], + [-0.5787, 1.6843]], + + [[ 0.3242, -0.9665], + [ 0.4539, -0.0887]], + + [[ 1.1336, -0.4025], + [-0.7089, 0.9032]]]) + >>> A.det() + tensor([1.1990, 0.4099, 0.7386]) + >>> A.det().log() + tensor([ 0.1815, -0.8917, -0.3031]) + """ + +def logical_and( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + logical_and(input, other, *, out=None) -> Tensor + + Computes the element-wise logical AND of the given input tensors. Zeros are treated as ``False`` and nonzeros are + treated as ``True``. + + Args: + input (Tensor): the input tensor. + other (Tensor): the tensor to compute AND with + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.logical_and(torch.tensor([True, False, True]), torch.tensor([True, False, False])) + tensor([ True, False, False]) + >>> a = torch.tensor([0, 1, 10, 0], dtype=torch.int8) + >>> b = torch.tensor([4, 0, 1, 0], dtype=torch.int8) + >>> torch.logical_and(a, b) + tensor([False, False, True, False]) + >>> torch.logical_and(a.double(), b.double()) + tensor([False, False, True, False]) + >>> torch.logical_and(a.double(), b) + tensor([False, False, True, False]) + >>> torch.logical_and(a, b, out=torch.empty(4, dtype=torch.bool)) + tensor([False, False, True, False]) + """ + +def logical_not(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + logical_not(input, *, out=None) -> Tensor + + Computes the element-wise logical NOT of the given input tensor. If not specified, the output tensor will have the bool + dtype. If the input tensor is not a bool tensor, zeros are treated as ``False`` and non-zeros are treated as ``True``. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.logical_not(torch.tensor([True, False])) + tensor([False, True]) + >>> torch.logical_not(torch.tensor([0, 1, -10], dtype=torch.int8)) + tensor([ True, False, False]) + >>> torch.logical_not(torch.tensor([0., 1.5, -10.], dtype=torch.double)) + tensor([ True, False, False]) + >>> torch.logical_not(torch.tensor([0., 1., -10.], dtype=torch.double), out=torch.empty(3, dtype=torch.int16)) + tensor([1, 0, 0], dtype=torch.int16) + """ + +def logical_or( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + logical_or(input, other, *, out=None) -> Tensor + + Computes the element-wise logical OR of the given input tensors. Zeros are treated as ``False`` and nonzeros are + treated as ``True``. + + Args: + input (Tensor): the input tensor. + other (Tensor): the tensor to compute OR with + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.logical_or(torch.tensor([True, False, True]), torch.tensor([True, False, False])) + tensor([ True, False, True]) + >>> a = torch.tensor([0, 1, 10, 0], dtype=torch.int8) + >>> b = torch.tensor([4, 0, 1, 0], dtype=torch.int8) + >>> torch.logical_or(a, b) + tensor([ True, True, True, False]) + >>> torch.logical_or(a.double(), b.double()) + tensor([ True, True, True, False]) + >>> torch.logical_or(a.double(), b) + tensor([ True, True, True, False]) + >>> torch.logical_or(a, b, out=torch.empty(4, dtype=torch.bool)) + tensor([ True, True, True, False]) + """ + +def logical_xor( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + logical_xor(input: Tensor, other: Tensor, *, out: Optional[Tensor]) -> Tensor + + Computes the element-wise logical XOR of the given input tensors. Zeros are treated as ``False`` and nonzeros are + treated as ``True``. + + Args: + input (Tensor): the input tensor. + other (Tensor): the tensor to compute XOR with + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.logical_xor(torch.tensor([True, False, True]), torch.tensor([True, False, False])) + tensor([False, False, True]) + >>> a = torch.tensor([0, 1, 10, 0], dtype=torch.int8) + >>> b = torch.tensor([4, 0, 1, 0], dtype=torch.int8) + >>> torch.logical_xor(a, b) + tensor([ True, True, False, False]) + >>> torch.logical_xor(a.double(), b.double()) + tensor([ True, True, False, False]) + >>> torch.logical_xor(a.double(), b) + tensor([ True, True, False, False]) + >>> torch.logical_xor(a, b, out=torch.empty(4, dtype=torch.bool)) + tensor([ True, True, False, False]) + """ + +def logit( + input: Tensor, + eps: _float | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + logit(input, eps=None, *, out=None) -> Tensor + + Alias for :func:`torch.special.logit`. + """ + +def logit_(input: Tensor, eps: _float | None = None) -> Tensor: ... +@overload +def logspace( + start: Number, + end: Number, + steps: _int | None = None, + base: _float = 10.0, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + logspace(start, end, steps, base=10.0, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + + Creates a one-dimensional tensor of size :attr:`steps` whose values are evenly + spaced from :math:`{{\text{{base}}}}^{{\text{{start}}}}` to + :math:`{{\text{{base}}}}^{{\text{{end}}}}`, inclusive, on a logarithmic scale + with base :attr:`base`. That is, the values are: + + .. math:: + (\text{base}^{\text{start}}, + \text{base}^{(\text{start} + \frac{\text{end} - \text{start}}{ \text{steps} - 1})}, + \ldots, + \text{base}^{(\text{start} + (\text{steps} - 2) * \frac{\text{end} - \text{start}}{ \text{steps} - 1})}, + \text{base}^{\text{end}}) + + + + From PyTorch 1.11 logspace requires the steps argument. Use steps=100 to restore the previous behavior. + + Args: + start (float or Tensor): the starting value for the set of points. If `Tensor`, it must be 0-dimensional + end (float or Tensor): the ending value for the set of points. If `Tensor`, it must be 0-dimensional + steps (int): size of the constructed tensor + base (float, optional): base of the logarithm function. Default: ``10.0``. + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the data type to perform the computation in. + Default: if None, uses the global default dtype (see torch.get_default_dtype()) + when both :attr:`start` and :attr:`end` are real, + and corresponding complex dtype when either is complex. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.logspace(start=-10, end=10, steps=5) + tensor([ 1.0000e-10, 1.0000e-05, 1.0000e+00, 1.0000e+05, 1.0000e+10]) + >>> torch.logspace(start=0.1, end=1.0, steps=5) + tensor([ 1.2589, 2.1135, 3.5481, 5.9566, 10.0000]) + >>> torch.logspace(start=0.1, end=1.0, steps=1) + tensor([1.2589]) + >>> torch.logspace(start=2, end=2, steps=1, base=2) + tensor([4.0]) + """ + +@overload +def logspace( + start: Tensor, + end: Tensor, + steps: _int, + base: _float = 10.0, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + logspace(start, end, steps, base=10.0, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + + Creates a one-dimensional tensor of size :attr:`steps` whose values are evenly + spaced from :math:`{{\text{{base}}}}^{{\text{{start}}}}` to + :math:`{{\text{{base}}}}^{{\text{{end}}}}`, inclusive, on a logarithmic scale + with base :attr:`base`. That is, the values are: + + .. math:: + (\text{base}^{\text{start}}, + \text{base}^{(\text{start} + \frac{\text{end} - \text{start}}{ \text{steps} - 1})}, + \ldots, + \text{base}^{(\text{start} + (\text{steps} - 2) * \frac{\text{end} - \text{start}}{ \text{steps} - 1})}, + \text{base}^{\text{end}}) + + + + From PyTorch 1.11 logspace requires the steps argument. Use steps=100 to restore the previous behavior. + + Args: + start (float or Tensor): the starting value for the set of points. If `Tensor`, it must be 0-dimensional + end (float or Tensor): the ending value for the set of points. If `Tensor`, it must be 0-dimensional + steps (int): size of the constructed tensor + base (float, optional): base of the logarithm function. Default: ``10.0``. + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the data type to perform the computation in. + Default: if None, uses the global default dtype (see torch.get_default_dtype()) + when both :attr:`start` and :attr:`end` are real, + and corresponding complex dtype when either is complex. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.logspace(start=-10, end=10, steps=5) + tensor([ 1.0000e-10, 1.0000e-05, 1.0000e+00, 1.0000e+05, 1.0000e+10]) + >>> torch.logspace(start=0.1, end=1.0, steps=5) + tensor([ 1.2589, 2.1135, 3.5481, 5.9566, 10.0000]) + >>> torch.logspace(start=0.1, end=1.0, steps=1) + tensor([1.2589]) + >>> torch.logspace(start=2, end=2, steps=1, base=2) + tensor([4.0]) + """ + +@overload +def logspace( + start: Number | _complex, + end: Tensor, + steps: _int, + base: _float = 10.0, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + logspace(start, end, steps, base=10.0, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + + Creates a one-dimensional tensor of size :attr:`steps` whose values are evenly + spaced from :math:`{{\text{{base}}}}^{{\text{{start}}}}` to + :math:`{{\text{{base}}}}^{{\text{{end}}}}`, inclusive, on a logarithmic scale + with base :attr:`base`. That is, the values are: + + .. math:: + (\text{base}^{\text{start}}, + \text{base}^{(\text{start} + \frac{\text{end} - \text{start}}{ \text{steps} - 1})}, + \ldots, + \text{base}^{(\text{start} + (\text{steps} - 2) * \frac{\text{end} - \text{start}}{ \text{steps} - 1})}, + \text{base}^{\text{end}}) + + + + From PyTorch 1.11 logspace requires the steps argument. Use steps=100 to restore the previous behavior. + + Args: + start (float or Tensor): the starting value for the set of points. If `Tensor`, it must be 0-dimensional + end (float or Tensor): the ending value for the set of points. If `Tensor`, it must be 0-dimensional + steps (int): size of the constructed tensor + base (float, optional): base of the logarithm function. Default: ``10.0``. + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the data type to perform the computation in. + Default: if None, uses the global default dtype (see torch.get_default_dtype()) + when both :attr:`start` and :attr:`end` are real, + and corresponding complex dtype when either is complex. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.logspace(start=-10, end=10, steps=5) + tensor([ 1.0000e-10, 1.0000e-05, 1.0000e+00, 1.0000e+05, 1.0000e+10]) + >>> torch.logspace(start=0.1, end=1.0, steps=5) + tensor([ 1.2589, 2.1135, 3.5481, 5.9566, 10.0000]) + >>> torch.logspace(start=0.1, end=1.0, steps=1) + tensor([1.2589]) + >>> torch.logspace(start=2, end=2, steps=1, base=2) + tensor([4.0]) + """ + +@overload +def logspace( + start: Tensor, + end: Number | _complex, + steps: _int, + base: _float = 10.0, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + logspace(start, end, steps, base=10.0, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + + Creates a one-dimensional tensor of size :attr:`steps` whose values are evenly + spaced from :math:`{{\text{{base}}}}^{{\text{{start}}}}` to + :math:`{{\text{{base}}}}^{{\text{{end}}}}`, inclusive, on a logarithmic scale + with base :attr:`base`. That is, the values are: + + .. math:: + (\text{base}^{\text{start}}, + \text{base}^{(\text{start} + \frac{\text{end} - \text{start}}{ \text{steps} - 1})}, + \ldots, + \text{base}^{(\text{start} + (\text{steps} - 2) * \frac{\text{end} - \text{start}}{ \text{steps} - 1})}, + \text{base}^{\text{end}}) + + + + From PyTorch 1.11 logspace requires the steps argument. Use steps=100 to restore the previous behavior. + + Args: + start (float or Tensor): the starting value for the set of points. If `Tensor`, it must be 0-dimensional + end (float or Tensor): the ending value for the set of points. If `Tensor`, it must be 0-dimensional + steps (int): size of the constructed tensor + base (float, optional): base of the logarithm function. Default: ``10.0``. + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the data type to perform the computation in. + Default: if None, uses the global default dtype (see torch.get_default_dtype()) + when both :attr:`start` and :attr:`end` are real, + and corresponding complex dtype when either is complex. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.logspace(start=-10, end=10, steps=5) + tensor([ 1.0000e-10, 1.0000e-05, 1.0000e+00, 1.0000e+05, 1.0000e+10]) + >>> torch.logspace(start=0.1, end=1.0, steps=5) + tensor([ 1.2589, 2.1135, 3.5481, 5.9566, 10.0000]) + >>> torch.logspace(start=0.1, end=1.0, steps=1) + tensor([1.2589]) + >>> torch.logspace(start=2, end=2, steps=1, base=2) + tensor([4.0]) + """ + +@overload +def logspace( + start: Number | _complex, + end: Number | _complex, + steps: _int, + base: _float = 10.0, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + logspace(start, end, steps, base=10.0, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + + Creates a one-dimensional tensor of size :attr:`steps` whose values are evenly + spaced from :math:`{{\text{{base}}}}^{{\text{{start}}}}` to + :math:`{{\text{{base}}}}^{{\text{{end}}}}`, inclusive, on a logarithmic scale + with base :attr:`base`. That is, the values are: + + .. math:: + (\text{base}^{\text{start}}, + \text{base}^{(\text{start} + \frac{\text{end} - \text{start}}{ \text{steps} - 1})}, + \ldots, + \text{base}^{(\text{start} + (\text{steps} - 2) * \frac{\text{end} - \text{start}}{ \text{steps} - 1})}, + \text{base}^{\text{end}}) + + + + From PyTorch 1.11 logspace requires the steps argument. Use steps=100 to restore the previous behavior. + + Args: + start (float or Tensor): the starting value for the set of points. If `Tensor`, it must be 0-dimensional + end (float or Tensor): the ending value for the set of points. If `Tensor`, it must be 0-dimensional + steps (int): size of the constructed tensor + base (float, optional): base of the logarithm function. Default: ``10.0``. + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the data type to perform the computation in. + Default: if None, uses the global default dtype (see torch.get_default_dtype()) + when both :attr:`start` and :attr:`end` are real, + and corresponding complex dtype when either is complex. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.logspace(start=-10, end=10, steps=5) + tensor([ 1.0000e-10, 1.0000e-05, 1.0000e+00, 1.0000e+05, 1.0000e+10]) + >>> torch.logspace(start=0.1, end=1.0, steps=5) + tensor([ 1.2589, 2.1135, 3.5481, 5.9566, 10.0000]) + >>> torch.logspace(start=0.1, end=1.0, steps=1) + tensor([1.2589]) + >>> torch.logspace(start=2, end=2, steps=1, base=2) + tensor([4.0]) + """ + +@overload +def logsumexp( + input: Tensor, + dim: _int | _size, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + logsumexp(input, dim, keepdim=False, *, out=None) + + Returns the log of summed exponentials of each row of the :attr:`input` + tensor in the given dimension :attr:`dim`. The computation is numerically + stabilized. + + For summation index :math:`j` given by `dim` and other indices :math:`i`, the result is + + .. math:: + \text{logsumexp}(x)_{i} = \log \sum_j \exp(x_{ij}) + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + dim (int or tuple of ints): the dimension or dimensions to reduce. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(3, 3) + >>> torch.logsumexp(a, 1) + tensor([1.4907, 1.0593, 1.5696]) + >>> torch.dist(torch.logsumexp(a, 1), torch.log(torch.sum(torch.exp(a), 1))) + tensor(1.6859e-07) + """ + +@overload +def logsumexp( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + logsumexp(input, dim, keepdim=False, *, out=None) + + Returns the log of summed exponentials of each row of the :attr:`input` + tensor in the given dimension :attr:`dim`. The computation is numerically + stabilized. + + For summation index :math:`j` given by `dim` and other indices :math:`i`, the result is + + .. math:: + \text{logsumexp}(x)_{i} = \log \sum_j \exp(x_{ij}) + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + dim (int or tuple of ints): the dimension or dimensions to reduce. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(3, 3) + >>> torch.logsumexp(a, 1) + tensor([1.4907, 1.0593, 1.5696]) + >>> torch.dist(torch.logsumexp(a, 1), torch.log(torch.sum(torch.exp(a), 1))) + tensor(1.6859e-07) + """ + +@overload +def lstm( + data: Tensor, + batch_sizes: Tensor, + hx: tuple[Tensor, ...] | list[Tensor], + params: tuple[Tensor, ...] | list[Tensor], + has_biases: _bool, + num_layers: _int, + dropout: _float, + train: _bool, + bidirectional: _bool, +) -> tuple[Tensor, Tensor, Tensor]: ... +@overload +def lstm( + input: Tensor, + hx: tuple[Tensor, ...] | list[Tensor], + params: tuple[Tensor, ...] | list[Tensor], + has_biases: _bool, + num_layers: _int, + dropout: _float, + train: _bool, + bidirectional: _bool, + batch_first: _bool, +) -> tuple[Tensor, Tensor, Tensor]: ... +def lstm_cell( + input: Tensor, + hx: tuple[Tensor, ...] | list[Tensor], + w_ih: Tensor, + w_hh: Tensor, + b_ih: Tensor | None = None, + b_hh: Tensor | None = None, +) -> tuple[Tensor, Tensor]: ... +@overload +def lt( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + lt(input, other, *, out=None) -> Tensor + + Computes :math:`\text{input} < \text{other}` element-wise. + + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or float): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is less than :attr:`other` and False elsewhere + + Example:: + + >>> torch.lt(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[False, False], [True, False]]) + """ + +@overload +def lt( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + lt(input, other, *, out=None) -> Tensor + + Computes :math:`\text{input} < \text{other}` element-wise. + + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or float): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is less than :attr:`other` and False elsewhere + + Example:: + + >>> torch.lt(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[False, False], [True, False]]) + """ + +def lu_solve( + input: Tensor, + LU_data: Tensor, + LU_pivots: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + lu_solve(b, LU_data, LU_pivots, *, out=None) -> Tensor + + Returns the LU solve of the linear system :math:`Ax = b` using the partially pivoted + LU factorization of A from :func:`~linalg.lu_factor`. + + This function supports ``float``, ``double``, ``cfloat`` and ``cdouble`` dtypes for :attr:`input`. + + .. warning:: + + :func:`torch.lu_solve` is deprecated in favor of :func:`torch.linalg.lu_solve`. + :func:`torch.lu_solve` will be removed in a future PyTorch release. + ``X = torch.lu_solve(B, LU, pivots)`` should be replaced with + + .. code:: python + + X = linalg.lu_solve(LU, pivots, B) + + Arguments: + b (Tensor): the RHS tensor of size :math:`(*, m, k)`, where :math:`*` + is zero or more batch dimensions. + LU_data (Tensor): the pivoted LU factorization of A from :meth:`~linalg.lu_factor` of size :math:`(*, m, m)`, + where :math:`*` is zero or more batch dimensions. + LU_pivots (IntTensor): the pivots of the LU factorization from :meth:`~linalg.lu_factor` of size :math:`(*, m)`, + where :math:`*` is zero or more batch dimensions. + The batch dimensions of :attr:`LU_pivots` must be equal to the batch dimensions of + :attr:`LU_data`. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> A = torch.randn(2, 3, 3) + >>> b = torch.randn(2, 3, 1) + >>> LU, pivots = torch.linalg.lu_factor(A) + >>> x = torch.lu_solve(b, LU, pivots) + >>> torch.dist(A @ x, b) + tensor(1.00000e-07 * + 2.8312) + """ + +def lu_unpack( + LU_data: Tensor, + LU_pivots: Tensor, + unpack_data: _bool = True, + unpack_pivots: _bool = True, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.lu_unpack: + r""" + lu_unpack(LU_data, LU_pivots, unpack_data=True, unpack_pivots=True, *, out=None) -> (Tensor, Tensor, Tensor) + + Unpacks the LU decomposition returned by :func:`~linalg.lu_factor` into the `P, L, U` matrices. + + .. seealso:: + + :func:`~linalg.lu` returns the matrices from the LU decomposition. Its gradient formula is more efficient + than that of doing :func:`~linalg.lu_factor` followed by :func:`~linalg.lu_unpack`. + + Args: + LU_data (Tensor): the packed LU factorization data + LU_pivots (Tensor): the packed LU factorization pivots + unpack_data (bool): flag indicating if the data should be unpacked. + If ``False``, then the returned ``L`` and ``U`` are empty tensors. + Default: ``True`` + unpack_pivots (bool): flag indicating if the pivots should be unpacked into a permutation matrix ``P``. + If ``False``, then the returned ``P`` is an empty tensor. + Default: ``True`` + + Keyword args: + out (tuple, optional): output tuple of three tensors. Ignored if `None`. + + Returns: + A namedtuple ``(P, L, U)`` + + Examples:: + + >>> A = torch.randn(2, 3, 3) + >>> LU, pivots = torch.linalg.lu_factor(A) + >>> P, L, U = torch.lu_unpack(LU, pivots) + >>> # We can recover A from the factorization + >>> A_ = P @ L @ U + >>> torch.allclose(A, A_) + True + + >>> # LU factorization of a rectangular matrix: + >>> A = torch.randn(2, 3, 2) + >>> LU, pivots = torch.linalg.lu_factor(A) + >>> P, L, U = torch.lu_unpack(LU, pivots) + >>> # P, L, U are the same as returned by linalg.lu + >>> P_, L_, U_ = torch.linalg.lu(A) + >>> torch.allclose(P, P_) and torch.allclose(L, L_) and torch.allclose(U, U_) + True + """ + +def margin_ranking_loss( + input1: Tensor, + input2: Tensor, + target: Tensor, + margin: _float = 0.0, + reduction: _int = 1, +) -> Tensor: ... +@overload +def masked_fill(input: Tensor, mask: Tensor, value: Tensor) -> Tensor: ... +@overload +def masked_fill( + input: Tensor, + mask: Tensor, + value: Number | _complex, +) -> Tensor: ... +def masked_scatter(input: Tensor, mask: Tensor, source: Tensor) -> Tensor: ... +def masked_select( + input: Tensor, + mask: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + masked_select(input, mask, *, out=None) -> Tensor + + Returns a new 1-D tensor which indexes the :attr:`input` tensor according to + the boolean mask :attr:`mask` which is a `BoolTensor`. + + The shapes of the :attr:`mask` tensor and the :attr:`input` tensor don't need + to match, but they must be :ref:`broadcastable `. + + .. note:: The returned tensor does **not** use the same storage + as the original tensor + + Args: + input (Tensor): the input tensor. + mask (BoolTensor): the tensor containing the binary mask to index with + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> x = torch.randn(3, 4) + >>> x + tensor([[ 0.3552, -2.3825, -0.8297, 0.3477], + [-1.2035, 1.2252, 0.5002, 0.6248], + [ 0.1307, -2.0608, 0.1244, 2.0139]]) + >>> mask = x.ge(0.5) + >>> mask + tensor([[False, False, False, False], + [False, True, True, True], + [False, False, False, True]]) + >>> torch.masked_select(x, mask) + tensor([ 1.2252, 0.5002, 0.6248, 2.0139]) + """ + +def matmul( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + matmul(input, other, *, out=None) -> Tensor + + Matrix product of two tensors. + + The behavior depends on the dimensionality of the tensors as follows: + + - If both tensors are 1-dimensional, the dot product (scalar) is returned. + - If both arguments are 2-dimensional, the matrix-matrix product is returned. + - If the first argument is 1-dimensional and the second argument is 2-dimensional, + a 1 is prepended to its dimension for the purpose of the matrix multiply. + After the matrix multiply, the prepended dimension is removed. + - If the first argument is 2-dimensional and the second argument is 1-dimensional, + the matrix-vector product is returned. + - If both arguments are at least 1-dimensional and at least one argument is + N-dimensional (where N > 2), then a batched matrix multiply is returned. If the first + argument is 1-dimensional, a 1 is prepended to its dimension for the purpose of the + batched matrix multiply and removed after. If the second argument is 1-dimensional, a + 1 is appended to its dimension for the purpose of the batched matrix multiply and removed after. + + The first N-2 dimensions of each argument, the batch dimensions, are + :ref:`broadcast ` (and thus must be broadcastable). + The last 2, the matrix dimensions, are handled as in the matrix-matrix product. + + For example, if :attr:`input` is a + :math:`(j \times 1 \times n \times m)` tensor and :attr:`other` is a :math:`(k \times m \times p)` + tensor, the batch dimensions are :math:`(j \times 1)` and :math:`(k)`, + and the matrix dimensions are :math:`(n \times m)` and :math:`(m \times p)`. + :attr:`out` will be a :math:`(j \times k \times n \times p)` tensor. + + This operation has support for arguments with :ref:`sparse layouts`. In particular the + matrix-matrix (both arguments 2-dimensional) supports sparse arguments with the same restrictions + as :func:`torch.mm` + + + .. warning:: + Sparse support is a beta feature and some layout(s)/dtype/device combinations may not be supported, + or may not have autograd support. If you notice missing functionality please + open a feature request. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + .. note:: + + The 1-dimensional dot product version of this function does not support an :attr:`out` parameter. + + Arguments: + input (Tensor): the first tensor to be multiplied + other (Tensor): the second tensor to be multiplied + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> # vector x vector + >>> tensor1 = torch.randn(3) + >>> tensor2 = torch.randn(3) + >>> torch.matmul(tensor1, tensor2).size() + torch.Size([]) + >>> # matrix x vector + >>> tensor1 = torch.randn(3, 4) + >>> tensor2 = torch.randn(4) + >>> torch.matmul(tensor1, tensor2).size() + torch.Size([3]) + >>> # batched matrix x broadcasted vector + >>> tensor1 = torch.randn(10, 3, 4) + >>> tensor2 = torch.randn(4) + >>> torch.matmul(tensor1, tensor2).size() + torch.Size([10, 3]) + >>> # batched matrix x batched matrix + >>> tensor1 = torch.randn(10, 3, 4) + >>> tensor2 = torch.randn(10, 4, 5) + >>> torch.matmul(tensor1, tensor2).size() + torch.Size([10, 3, 5]) + >>> # batched matrix x broadcasted matrix + >>> tensor1 = torch.randn(10, 3, 4) + >>> tensor2 = torch.randn(4, 5) + >>> torch.matmul(tensor1, tensor2).size() + torch.Size([10, 3, 5]) + """ + +def matrix_exp(input: Tensor) -> Tensor: + r""" + matrix_exp(A) -> Tensor + + Alias for :func:`torch.linalg.matrix_exp`. + """ + +def matrix_power( + input: Tensor, + n: _int, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + matrix_power(input, n, *, out=None) -> Tensor + + Alias for :func:`torch.linalg.matrix_power` + """ + +@overload +def max(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + max(input, *, out=None) -> Tensor + + Returns the maximum value of all elements in the ``input`` tensor. + + .. note:: + The difference between ``max``/``min`` and ``amax``/``amin`` is: + - ``amax``/``amin`` supports reducing on multiple dimensions, + - ``amax``/``amin`` does not return indices. + + Both ``amax``/``amin`` evenly distribute gradients between equal values + when there are multiple input elements with the same minimum or maximum value. + + For ``max``/``min``: + - If reduce over all dimensions(no dim specified), gradients evenly distribute between equally ``max``/``min`` values. + - If reduce over one specified axis, only propagate to the indexed element. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.6763, 0.7445, -2.2369]]) + >>> torch.max(a) + tensor(0.7445) + + .. function:: max(input, dim, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` is the maximum + value of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`. And ``indices`` is the index location of each maximum value found + (argmax). + + If ``keepdim`` is ``True``, the output tensors are of the same size + as ``input`` except in the dimension ``dim`` where they are of size 1. + Otherwise, ``dim`` is squeezed (see :func:`torch.squeeze`), resulting + in the output tensors having 1 fewer dimension than ``input``. + + .. note:: If there are multiple maximal values in a reduced row then + the indices of the first maximal value are returned. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. If omitted, all dimensions are reduced. Explicit ``None`` is not supported. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the result tuple of two output tensors (max, max_indices) + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-1.2360, -0.2942, -0.1222, 0.8475], + [ 1.1949, -1.1127, -2.2379, -0.6702], + [ 1.5717, -0.9207, 0.1297, -1.8768], + [-0.6172, 1.0036, -0.6060, -0.2432]]) + >>> torch.max(a, 1) + torch.return_types.max(values=tensor([0.8475, 1.1949, 1.5717, 1.0036]), indices=tensor([3, 0, 0, 1])) + >>> a = torch.tensor([[1.0, 2.0], [3.0, 4.0]]) + >>> a.max(dim=1, keepdim=True) + torch.return_types.max( + values=tensor([[2.], [4.]]), + indices=tensor([[1], [1]])) + >>> a.max(dim=1, keepdim=False) + torch.return_types.max( + values=tensor([2., 4.]), + indices=tensor([1, 1])) + + .. function:: max(input, other, *, out=None) -> Tensor + :noindex: + + See :func:`torch.maximum`. + """ + +@overload +def max( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + max(input, *, out=None) -> Tensor + + Returns the maximum value of all elements in the ``input`` tensor. + + .. note:: + The difference between ``max``/``min`` and ``amax``/``amin`` is: + - ``amax``/``amin`` supports reducing on multiple dimensions, + - ``amax``/``amin`` does not return indices. + + Both ``amax``/``amin`` evenly distribute gradients between equal values + when there are multiple input elements with the same minimum or maximum value. + + For ``max``/``min``: + - If reduce over all dimensions(no dim specified), gradients evenly distribute between equally ``max``/``min`` values. + - If reduce over one specified axis, only propagate to the indexed element. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.6763, 0.7445, -2.2369]]) + >>> torch.max(a) + tensor(0.7445) + + .. function:: max(input, dim, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` is the maximum + value of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`. And ``indices`` is the index location of each maximum value found + (argmax). + + If ``keepdim`` is ``True``, the output tensors are of the same size + as ``input`` except in the dimension ``dim`` where they are of size 1. + Otherwise, ``dim`` is squeezed (see :func:`torch.squeeze`), resulting + in the output tensors having 1 fewer dimension than ``input``. + + .. note:: If there are multiple maximal values in a reduced row then + the indices of the first maximal value are returned. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. If omitted, all dimensions are reduced. Explicit ``None`` is not supported. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the result tuple of two output tensors (max, max_indices) + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-1.2360, -0.2942, -0.1222, 0.8475], + [ 1.1949, -1.1127, -2.2379, -0.6702], + [ 1.5717, -0.9207, 0.1297, -1.8768], + [-0.6172, 1.0036, -0.6060, -0.2432]]) + >>> torch.max(a, 1) + torch.return_types.max(values=tensor([0.8475, 1.1949, 1.5717, 1.0036]), indices=tensor([3, 0, 0, 1])) + >>> a = torch.tensor([[1.0, 2.0], [3.0, 4.0]]) + >>> a.max(dim=1, keepdim=True) + torch.return_types.max( + values=tensor([[2.], [4.]]), + indices=tensor([[1], [1]])) + >>> a.max(dim=1, keepdim=False) + torch.return_types.max( + values=tensor([2., 4.]), + indices=tensor([1, 1])) + + .. function:: max(input, other, *, out=None) -> Tensor + :noindex: + + See :func:`torch.maximum`. + """ + +@overload +def max( + input: Tensor, + dim: _int, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.max: + r""" + max(input, *, out=None) -> Tensor + + Returns the maximum value of all elements in the ``input`` tensor. + + .. note:: + The difference between ``max``/``min`` and ``amax``/``amin`` is: + - ``amax``/``amin`` supports reducing on multiple dimensions, + - ``amax``/``amin`` does not return indices. + + Both ``amax``/``amin`` evenly distribute gradients between equal values + when there are multiple input elements with the same minimum or maximum value. + + For ``max``/``min``: + - If reduce over all dimensions(no dim specified), gradients evenly distribute between equally ``max``/``min`` values. + - If reduce over one specified axis, only propagate to the indexed element. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.6763, 0.7445, -2.2369]]) + >>> torch.max(a) + tensor(0.7445) + + .. function:: max(input, dim, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` is the maximum + value of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`. And ``indices`` is the index location of each maximum value found + (argmax). + + If ``keepdim`` is ``True``, the output tensors are of the same size + as ``input`` except in the dimension ``dim`` where they are of size 1. + Otherwise, ``dim`` is squeezed (see :func:`torch.squeeze`), resulting + in the output tensors having 1 fewer dimension than ``input``. + + .. note:: If there are multiple maximal values in a reduced row then + the indices of the first maximal value are returned. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. If omitted, all dimensions are reduced. Explicit ``None`` is not supported. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the result tuple of two output tensors (max, max_indices) + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-1.2360, -0.2942, -0.1222, 0.8475], + [ 1.1949, -1.1127, -2.2379, -0.6702], + [ 1.5717, -0.9207, 0.1297, -1.8768], + [-0.6172, 1.0036, -0.6060, -0.2432]]) + >>> torch.max(a, 1) + torch.return_types.max(values=tensor([0.8475, 1.1949, 1.5717, 1.0036]), indices=tensor([3, 0, 0, 1])) + >>> a = torch.tensor([[1.0, 2.0], [3.0, 4.0]]) + >>> a.max(dim=1, keepdim=True) + torch.return_types.max( + values=tensor([[2.], [4.]]), + indices=tensor([[1], [1]])) + >>> a.max(dim=1, keepdim=False) + torch.return_types.max( + values=tensor([2., 4.]), + indices=tensor([1, 1])) + + .. function:: max(input, other, *, out=None) -> Tensor + :noindex: + + See :func:`torch.maximum`. + """ + +@overload +def max( + input: Tensor, + dim: str | EllipsisType | None, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.max: + r""" + max(input, *, out=None) -> Tensor + + Returns the maximum value of all elements in the ``input`` tensor. + + .. note:: + The difference between ``max``/``min`` and ``amax``/``amin`` is: + - ``amax``/``amin`` supports reducing on multiple dimensions, + - ``amax``/``amin`` does not return indices. + + Both ``amax``/``amin`` evenly distribute gradients between equal values + when there are multiple input elements with the same minimum or maximum value. + + For ``max``/``min``: + - If reduce over all dimensions(no dim specified), gradients evenly distribute between equally ``max``/``min`` values. + - If reduce over one specified axis, only propagate to the indexed element. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.6763, 0.7445, -2.2369]]) + >>> torch.max(a) + tensor(0.7445) + + .. function:: max(input, dim, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` is the maximum + value of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`. And ``indices`` is the index location of each maximum value found + (argmax). + + If ``keepdim`` is ``True``, the output tensors are of the same size + as ``input`` except in the dimension ``dim`` where they are of size 1. + Otherwise, ``dim`` is squeezed (see :func:`torch.squeeze`), resulting + in the output tensors having 1 fewer dimension than ``input``. + + .. note:: If there are multiple maximal values in a reduced row then + the indices of the first maximal value are returned. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. If omitted, all dimensions are reduced. Explicit ``None`` is not supported. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the result tuple of two output tensors (max, max_indices) + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-1.2360, -0.2942, -0.1222, 0.8475], + [ 1.1949, -1.1127, -2.2379, -0.6702], + [ 1.5717, -0.9207, 0.1297, -1.8768], + [-0.6172, 1.0036, -0.6060, -0.2432]]) + >>> torch.max(a, 1) + torch.return_types.max(values=tensor([0.8475, 1.1949, 1.5717, 1.0036]), indices=tensor([3, 0, 0, 1])) + >>> a = torch.tensor([[1.0, 2.0], [3.0, 4.0]]) + >>> a.max(dim=1, keepdim=True) + torch.return_types.max( + values=tensor([[2.], [4.]]), + indices=tensor([[1], [1]])) + >>> a.max(dim=1, keepdim=False) + torch.return_types.max( + values=tensor([2., 4.]), + indices=tensor([1, 1])) + + .. function:: max(input, other, *, out=None) -> Tensor + :noindex: + + See :func:`torch.maximum`. + """ + +def max_pool1d( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size = (), + padding: _int | _size = 0, + dilation: _int | _size = 1, + ceil_mode: _bool = False, +) -> Tensor: ... +def max_pool1d_with_indices( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size = (), + padding: _int | _size = 0, + dilation: _int | _size = 1, + ceil_mode: _bool = False, +) -> tuple[Tensor, Tensor]: ... +def max_pool2d( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size = (), + padding: _int | _size = 0, + dilation: _int | _size = 1, + ceil_mode: _bool = False, +) -> Tensor: ... +def max_pool3d( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size = (), + padding: _int | _size = 0, + dilation: _int | _size = 1, + ceil_mode: _bool = False, +) -> Tensor: ... +def maximum( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + maximum(input, other, *, out=None) -> Tensor + + Computes the element-wise maximum of :attr:`input` and :attr:`other`. + + .. note:: + If one of the elements being compared is a NaN, then that element is returned. + :func:`maximum` is not supported for tensors with complex dtypes. + + Args: + input (Tensor): the input tensor. + other (Tensor): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor((1, 2, -1)) + >>> b = torch.tensor((3, 0, 4)) + >>> torch.maximum(a, b) + tensor([3, 2, 4]) + """ + +@overload +def mean( + input: Tensor, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + mean(input, *, dtype=None) -> Tensor + + .. note:: + If the `input` tensor is empty, ``torch.mean()`` returns ``nan``. + This behavior is consistent with NumPy and follows the definition + that the mean over an empty set is undefined. + + + Returns the mean value of all elements in the :attr:`input` tensor. Input must be floating point or complex. + + Args: + input (Tensor): + the input tensor, either of floating point or complex dtype + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.2294, -0.5481, 1.3288]]) + >>> torch.mean(a) + tensor(0.3367) + + .. function:: mean(input, dim, keepdim=False, *, dtype=None, out=None) -> Tensor + :noindex: + + Returns the mean value of each row of the :attr:`input` tensor in the given + dimension :attr:`dim`. If :attr:`dim` is a list of dimensions, + reduce over all of them. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + out (Tensor, optional): the output tensor. + + .. seealso:: + + :func:`torch.nanmean` computes the mean value of `non-NaN` elements. + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-0.3841, 0.6320, 0.4254, -0.7384], + [-0.9644, 1.0131, -0.6549, -1.4279], + [-0.2951, -1.3350, -0.7694, 0.5600], + [ 1.0842, -0.9580, 0.3623, 0.2343]]) + >>> torch.mean(a, 1) + tensor([-0.0163, -0.5085, -0.4599, 0.1807]) + >>> torch.mean(a, 1, True) + tensor([[-0.0163], + [-0.5085], + [-0.4599], + [ 0.1807]]) + """ + +@overload +def mean( + input: Tensor, + dim: _int | _size | None, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + mean(input, *, dtype=None) -> Tensor + + .. note:: + If the `input` tensor is empty, ``torch.mean()`` returns ``nan``. + This behavior is consistent with NumPy and follows the definition + that the mean over an empty set is undefined. + + + Returns the mean value of all elements in the :attr:`input` tensor. Input must be floating point or complex. + + Args: + input (Tensor): + the input tensor, either of floating point or complex dtype + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.2294, -0.5481, 1.3288]]) + >>> torch.mean(a) + tensor(0.3367) + + .. function:: mean(input, dim, keepdim=False, *, dtype=None, out=None) -> Tensor + :noindex: + + Returns the mean value of each row of the :attr:`input` tensor in the given + dimension :attr:`dim`. If :attr:`dim` is a list of dimensions, + reduce over all of them. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + out (Tensor, optional): the output tensor. + + .. seealso:: + + :func:`torch.nanmean` computes the mean value of `non-NaN` elements. + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-0.3841, 0.6320, 0.4254, -0.7384], + [-0.9644, 1.0131, -0.6549, -1.4279], + [-0.2951, -1.3350, -0.7694, 0.5600], + [ 1.0842, -0.9580, 0.3623, 0.2343]]) + >>> torch.mean(a, 1) + tensor([-0.0163, -0.5085, -0.4599, 0.1807]) + >>> torch.mean(a, 1, True) + tensor([[-0.0163], + [-0.5085], + [-0.4599], + [ 0.1807]]) + """ + +@overload +def mean( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + mean(input, *, dtype=None) -> Tensor + + .. note:: + If the `input` tensor is empty, ``torch.mean()`` returns ``nan``. + This behavior is consistent with NumPy and follows the definition + that the mean over an empty set is undefined. + + + Returns the mean value of all elements in the :attr:`input` tensor. Input must be floating point or complex. + + Args: + input (Tensor): + the input tensor, either of floating point or complex dtype + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.2294, -0.5481, 1.3288]]) + >>> torch.mean(a) + tensor(0.3367) + + .. function:: mean(input, dim, keepdim=False, *, dtype=None, out=None) -> Tensor + :noindex: + + Returns the mean value of each row of the :attr:`input` tensor in the given + dimension :attr:`dim`. If :attr:`dim` is a list of dimensions, + reduce over all of them. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + out (Tensor, optional): the output tensor. + + .. seealso:: + + :func:`torch.nanmean` computes the mean value of `non-NaN` elements. + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-0.3841, 0.6320, 0.4254, -0.7384], + [-0.9644, 1.0131, -0.6549, -1.4279], + [-0.2951, -1.3350, -0.7694, 0.5600], + [ 1.0842, -0.9580, 0.3623, 0.2343]]) + >>> torch.mean(a, 1) + tensor([-0.0163, -0.5085, -0.4599, 0.1807]) + >>> torch.mean(a, 1, True) + tensor([[-0.0163], + [-0.5085], + [-0.4599], + [ 0.1807]]) + """ + +@overload +def median(input: Tensor) -> Tensor: + r""" + median(input) -> Tensor + + Returns the median of the values in :attr:`input`. + + .. note:: + The median is not unique for :attr:`input` tensors with an even number + of elements. In this case the lower of the two medians is returned. To + compute the mean of both medians, use :func:`torch.quantile` with ``q=0.5`` instead. + + .. warning:: + This function produces deterministic (sub)gradients unlike ``median(dim=0)`` + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 1.5219, -1.5212, 0.2202]]) + >>> torch.median(a) + tensor(0.2202) + + .. function:: median(input, dim=-1, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` contains the median of each row of :attr:`input` + in the dimension :attr:`dim`, and ``indices`` contains the index of the median values found in the dimension :attr:`dim`. + + By default, :attr:`dim` is the last dimension of the :attr:`input` tensor. + + If :attr:`keepdim` is ``True``, the output tensors are of the same size + as :attr:`input` except in the dimension :attr:`dim` where they are of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in + the outputs tensor having 1 fewer dimension than :attr:`input`. + + .. note:: + The median is not unique for :attr:`input` tensors with an even number + of elements in the dimension :attr:`dim`. In this case the lower of the + two medians is returned. To compute the mean of both medians in + :attr:`input`, use :func:`torch.quantile` with ``q=0.5`` instead. + + .. warning:: + ``indices`` does not necessarily contain the first occurrence of each + median value found, unless it is unique. + The exact implementation details are device-specific. + Do not expect the same result when run on CPU and GPU in general. + For the same reason do not expect the gradients to be deterministic. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out ((Tensor, Tensor), optional): The first tensor will be populated with the median values and the second + tensor, which must have dtype long, with their indices in the dimension + :attr:`dim` of :attr:`input`. + + Example:: + + >>> a = torch.randn(4, 5) + >>> a + tensor([[ 0.2505, -0.3982, -0.9948, 0.3518, -1.3131], + [ 0.3180, -0.6993, 1.0436, 0.0438, 0.2270], + [-0.2751, 0.7303, 0.2192, 0.3321, 0.2488], + [ 1.0778, -1.9510, 0.7048, 0.4742, -0.7125]]) + >>> torch.median(a, 1) + torch.return_types.median(values=tensor([-0.3982, 0.2270, 0.2488, 0.4742]), indices=tensor([1, 4, 4, 3])) + """ + +@overload +def median( + input: Tensor, + dim: _int, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.median: + r""" + median(input) -> Tensor + + Returns the median of the values in :attr:`input`. + + .. note:: + The median is not unique for :attr:`input` tensors with an even number + of elements. In this case the lower of the two medians is returned. To + compute the mean of both medians, use :func:`torch.quantile` with ``q=0.5`` instead. + + .. warning:: + This function produces deterministic (sub)gradients unlike ``median(dim=0)`` + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 1.5219, -1.5212, 0.2202]]) + >>> torch.median(a) + tensor(0.2202) + + .. function:: median(input, dim=-1, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` contains the median of each row of :attr:`input` + in the dimension :attr:`dim`, and ``indices`` contains the index of the median values found in the dimension :attr:`dim`. + + By default, :attr:`dim` is the last dimension of the :attr:`input` tensor. + + If :attr:`keepdim` is ``True``, the output tensors are of the same size + as :attr:`input` except in the dimension :attr:`dim` where they are of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in + the outputs tensor having 1 fewer dimension than :attr:`input`. + + .. note:: + The median is not unique for :attr:`input` tensors with an even number + of elements in the dimension :attr:`dim`. In this case the lower of the + two medians is returned. To compute the mean of both medians in + :attr:`input`, use :func:`torch.quantile` with ``q=0.5`` instead. + + .. warning:: + ``indices`` does not necessarily contain the first occurrence of each + median value found, unless it is unique. + The exact implementation details are device-specific. + Do not expect the same result when run on CPU and GPU in general. + For the same reason do not expect the gradients to be deterministic. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out ((Tensor, Tensor), optional): The first tensor will be populated with the median values and the second + tensor, which must have dtype long, with their indices in the dimension + :attr:`dim` of :attr:`input`. + + Example:: + + >>> a = torch.randn(4, 5) + >>> a + tensor([[ 0.2505, -0.3982, -0.9948, 0.3518, -1.3131], + [ 0.3180, -0.6993, 1.0436, 0.0438, 0.2270], + [-0.2751, 0.7303, 0.2192, 0.3321, 0.2488], + [ 1.0778, -1.9510, 0.7048, 0.4742, -0.7125]]) + >>> torch.median(a, 1) + torch.return_types.median(values=tensor([-0.3982, 0.2270, 0.2488, 0.4742]), indices=tensor([1, 4, 4, 3])) + """ + +@overload +def median( + input: Tensor, + dim: str | EllipsisType | None, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.median: + r""" + median(input) -> Tensor + + Returns the median of the values in :attr:`input`. + + .. note:: + The median is not unique for :attr:`input` tensors with an even number + of elements. In this case the lower of the two medians is returned. To + compute the mean of both medians, use :func:`torch.quantile` with ``q=0.5`` instead. + + .. warning:: + This function produces deterministic (sub)gradients unlike ``median(dim=0)`` + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 1.5219, -1.5212, 0.2202]]) + >>> torch.median(a) + tensor(0.2202) + + .. function:: median(input, dim=-1, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` contains the median of each row of :attr:`input` + in the dimension :attr:`dim`, and ``indices`` contains the index of the median values found in the dimension :attr:`dim`. + + By default, :attr:`dim` is the last dimension of the :attr:`input` tensor. + + If :attr:`keepdim` is ``True``, the output tensors are of the same size + as :attr:`input` except in the dimension :attr:`dim` where they are of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in + the outputs tensor having 1 fewer dimension than :attr:`input`. + + .. note:: + The median is not unique for :attr:`input` tensors with an even number + of elements in the dimension :attr:`dim`. In this case the lower of the + two medians is returned. To compute the mean of both medians in + :attr:`input`, use :func:`torch.quantile` with ``q=0.5`` instead. + + .. warning:: + ``indices`` does not necessarily contain the first occurrence of each + median value found, unless it is unique. + The exact implementation details are device-specific. + Do not expect the same result when run on CPU and GPU in general. + For the same reason do not expect the gradients to be deterministic. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out ((Tensor, Tensor), optional): The first tensor will be populated with the median values and the second + tensor, which must have dtype long, with their indices in the dimension + :attr:`dim` of :attr:`input`. + + Example:: + + >>> a = torch.randn(4, 5) + >>> a + tensor([[ 0.2505, -0.3982, -0.9948, 0.3518, -1.3131], + [ 0.3180, -0.6993, 1.0436, 0.0438, 0.2270], + [-0.2751, 0.7303, 0.2192, 0.3321, 0.2488], + [ 1.0778, -1.9510, 0.7048, 0.4742, -0.7125]]) + >>> torch.median(a, 1) + torch.return_types.median(values=tensor([-0.3982, 0.2270, 0.2488, 0.4742]), indices=tensor([1, 4, 4, 3])) + """ + +@overload +def min(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + min(input, *, out=None) -> Tensor + + Returns the minimum value of all elements in the :attr:`input` tensor. + + .. note:: + The difference between ``max``/``min`` and ``amax``/``amin`` is: + - ``amax``/``amin`` supports reducing on multiple dimensions, + - ``amax``/``amin`` does not return indices. + + Both ``amax``/``amin`` evenly distribute gradients between equal values + when there are multiple input elements with the same minimum or maximum value. + + For ``max``/``min``: + - If reduce over all dimensions(no dim specified), gradients evenly distribute between equally ``max``/``min`` values. + - If reduce over one specified axis, only propagate to the indexed element. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.6750, 1.0857, 1.7197]]) + >>> torch.min(a) + tensor(0.6750) + + .. function:: min(input, dim, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` is the minimum + value of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`. And ``indices`` is the index location of each minimum value found + (argmin). + + If :attr:`keepdim` is ``True``, the output tensors are of the same size as + :attr:`input` except in the dimension :attr:`dim` where they are of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in + the output tensors having 1 fewer dimension than :attr:`input`. + + .. note:: If there are multiple minimal values in a reduced row then + the indices of the first minimal value are returned. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. If omitted, all dimensions are reduced. Explicit ``None`` is not supported. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the tuple of two output tensors (min, min_indices) + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-0.6248, 1.1334, -1.1899, -0.2803], + [-1.4644, -0.2635, -0.3651, 0.6134], + [ 0.2457, 0.0384, 1.0128, 0.7015], + [-0.1153, 2.9849, 2.1458, 0.5788]]) + >>> torch.min(a, 1) + torch.return_types.min(values=tensor([-1.1899, -1.4644, 0.0384, -0.1153]), indices=tensor([2, 0, 1, 0])) + + .. function:: min(input, other, *, out=None) -> Tensor + :noindex: + + See :func:`torch.minimum`. + """ + +@overload +def min( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + min(input, *, out=None) -> Tensor + + Returns the minimum value of all elements in the :attr:`input` tensor. + + .. note:: + The difference between ``max``/``min`` and ``amax``/``amin`` is: + - ``amax``/``amin`` supports reducing on multiple dimensions, + - ``amax``/``amin`` does not return indices. + + Both ``amax``/``amin`` evenly distribute gradients between equal values + when there are multiple input elements with the same minimum or maximum value. + + For ``max``/``min``: + - If reduce over all dimensions(no dim specified), gradients evenly distribute between equally ``max``/``min`` values. + - If reduce over one specified axis, only propagate to the indexed element. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.6750, 1.0857, 1.7197]]) + >>> torch.min(a) + tensor(0.6750) + + .. function:: min(input, dim, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` is the minimum + value of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`. And ``indices`` is the index location of each minimum value found + (argmin). + + If :attr:`keepdim` is ``True``, the output tensors are of the same size as + :attr:`input` except in the dimension :attr:`dim` where they are of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in + the output tensors having 1 fewer dimension than :attr:`input`. + + .. note:: If there are multiple minimal values in a reduced row then + the indices of the first minimal value are returned. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. If omitted, all dimensions are reduced. Explicit ``None`` is not supported. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the tuple of two output tensors (min, min_indices) + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-0.6248, 1.1334, -1.1899, -0.2803], + [-1.4644, -0.2635, -0.3651, 0.6134], + [ 0.2457, 0.0384, 1.0128, 0.7015], + [-0.1153, 2.9849, 2.1458, 0.5788]]) + >>> torch.min(a, 1) + torch.return_types.min(values=tensor([-1.1899, -1.4644, 0.0384, -0.1153]), indices=tensor([2, 0, 1, 0])) + + .. function:: min(input, other, *, out=None) -> Tensor + :noindex: + + See :func:`torch.minimum`. + """ + +@overload +def min( + input: Tensor, + dim: _int, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.min: + r""" + min(input, *, out=None) -> Tensor + + Returns the minimum value of all elements in the :attr:`input` tensor. + + .. note:: + The difference between ``max``/``min`` and ``amax``/``amin`` is: + - ``amax``/``amin`` supports reducing on multiple dimensions, + - ``amax``/``amin`` does not return indices. + + Both ``amax``/``amin`` evenly distribute gradients between equal values + when there are multiple input elements with the same minimum or maximum value. + + For ``max``/``min``: + - If reduce over all dimensions(no dim specified), gradients evenly distribute between equally ``max``/``min`` values. + - If reduce over one specified axis, only propagate to the indexed element. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.6750, 1.0857, 1.7197]]) + >>> torch.min(a) + tensor(0.6750) + + .. function:: min(input, dim, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` is the minimum + value of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`. And ``indices`` is the index location of each minimum value found + (argmin). + + If :attr:`keepdim` is ``True``, the output tensors are of the same size as + :attr:`input` except in the dimension :attr:`dim` where they are of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in + the output tensors having 1 fewer dimension than :attr:`input`. + + .. note:: If there are multiple minimal values in a reduced row then + the indices of the first minimal value are returned. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. If omitted, all dimensions are reduced. Explicit ``None`` is not supported. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the tuple of two output tensors (min, min_indices) + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-0.6248, 1.1334, -1.1899, -0.2803], + [-1.4644, -0.2635, -0.3651, 0.6134], + [ 0.2457, 0.0384, 1.0128, 0.7015], + [-0.1153, 2.9849, 2.1458, 0.5788]]) + >>> torch.min(a, 1) + torch.return_types.min(values=tensor([-1.1899, -1.4644, 0.0384, -0.1153]), indices=tensor([2, 0, 1, 0])) + + .. function:: min(input, other, *, out=None) -> Tensor + :noindex: + + See :func:`torch.minimum`. + """ + +@overload +def min( + input: Tensor, + dim: str | EllipsisType | None, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.min: + r""" + min(input, *, out=None) -> Tensor + + Returns the minimum value of all elements in the :attr:`input` tensor. + + .. note:: + The difference between ``max``/``min`` and ``amax``/``amin`` is: + - ``amax``/``amin`` supports reducing on multiple dimensions, + - ``amax``/``amin`` does not return indices. + + Both ``amax``/``amin`` evenly distribute gradients between equal values + when there are multiple input elements with the same minimum or maximum value. + + For ``max``/``min``: + - If reduce over all dimensions(no dim specified), gradients evenly distribute between equally ``max``/``min`` values. + - If reduce over one specified axis, only propagate to the indexed element. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.6750, 1.0857, 1.7197]]) + >>> torch.min(a) + tensor(0.6750) + + .. function:: min(input, dim, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` is the minimum + value of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`. And ``indices`` is the index location of each minimum value found + (argmin). + + If :attr:`keepdim` is ``True``, the output tensors are of the same size as + :attr:`input` except in the dimension :attr:`dim` where they are of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in + the output tensors having 1 fewer dimension than :attr:`input`. + + .. note:: If there are multiple minimal values in a reduced row then + the indices of the first minimal value are returned. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. If omitted, all dimensions are reduced. Explicit ``None`` is not supported. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the tuple of two output tensors (min, min_indices) + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[-0.6248, 1.1334, -1.1899, -0.2803], + [-1.4644, -0.2635, -0.3651, 0.6134], + [ 0.2457, 0.0384, 1.0128, 0.7015], + [-0.1153, 2.9849, 2.1458, 0.5788]]) + >>> torch.min(a, 1) + torch.return_types.min(values=tensor([-1.1899, -1.4644, 0.0384, -0.1153]), indices=tensor([2, 0, 1, 0])) + + .. function:: min(input, other, *, out=None) -> Tensor + :noindex: + + See :func:`torch.minimum`. + """ + +def minimum( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + minimum(input, other, *, out=None) -> Tensor + + Computes the element-wise minimum of :attr:`input` and :attr:`other`. + + .. note:: + If one of the elements being compared is a NaN, then that element is returned. + :func:`minimum` is not supported for tensors with complex dtypes. + + Args: + input (Tensor): the input tensor. + other (Tensor): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor((1, 2, -1)) + >>> b = torch.tensor((3, 0, 4)) + >>> torch.minimum(a, b) + tensor([1, 0, -1]) + """ + +def miopen_batch_norm( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + running_mean: Tensor | None, + running_var: Tensor | None, + training: _bool, + exponential_average_factor: _float, + epsilon: _float, +) -> tuple[Tensor, Tensor, Tensor]: ... +def miopen_convolution( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + padding: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, + benchmark: _bool, + deterministic: _bool, +) -> Tensor: ... +def miopen_convolution_add_relu( + input: Tensor, + weight: Tensor, + z: Tensor, + alpha: Number | _complex | None, + bias: Tensor | None, + stride: Sequence[_int | SymInt], + padding: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, +) -> Tensor: ... +def miopen_convolution_relu( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + stride: Sequence[_int | SymInt], + padding: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, +) -> Tensor: ... +def miopen_convolution_transpose( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + padding: Sequence[_int | SymInt], + output_padding: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, + benchmark: _bool, + deterministic: _bool, +) -> Tensor: ... +@overload +def miopen_ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: _size, + target_lengths: _size, + blank: _int, + deterministic: _bool, + zero_infinity: _bool, +) -> tuple[Tensor, Tensor]: ... +@overload +def miopen_ctc_loss( + log_probs: Tensor, + targets: Tensor, + input_lengths: Tensor, + target_lengths: Tensor, + blank: _int, + deterministic: _bool, + zero_infinity: _bool, +) -> tuple[Tensor, Tensor]: ... +def miopen_depthwise_convolution( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + padding: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, + benchmark: _bool, + deterministic: _bool, +) -> Tensor: ... +def miopen_rnn( + input: Tensor, + weight: tuple[Tensor, ...] | list[Tensor], + weight_stride0: _int, + hx: Tensor, + cx: Tensor | None, + mode: _int, + hidden_size: _int, + num_layers: _int, + batch_first: _bool, + dropout: _float, + train: _bool, + bidirectional: _bool, + batch_sizes: _size, + dropout_state: Tensor | None, +) -> tuple[Tensor, Tensor, Tensor, Tensor, Tensor]: ... +def mkldnn_adaptive_avg_pool2d( + input: Tensor, + output_size: _int | _size, + *, + out: Tensor | None = None, +) -> Tensor: ... +def mkldnn_convolution( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + padding: Sequence[_int | SymInt], + stride: Sequence[_int | SymInt], + dilation: Sequence[_int | SymInt], + groups: _int | SymInt, +) -> Tensor: ... +def mkldnn_linear_backward_weights( + grad_output: Tensor, + input: Tensor, + weight: Tensor, + bias_defined: _bool, +) -> tuple[Tensor, Tensor]: ... +def mkldnn_max_pool2d( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size = (), + padding: _int | _size = 0, + dilation: _int | _size = 1, + ceil_mode: _bool = False, +) -> Tensor: ... +def mkldnn_max_pool3d( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size = (), + padding: _int | _size = 0, + dilation: _int | _size = 1, + ceil_mode: _bool = False, +) -> Tensor: ... +def mkldnn_rnn_layer( + input: Tensor, + weight0: Tensor, + weight1: Tensor, + weight2: Tensor, + weight3: Tensor, + hx_: Tensor, + cx_: Tensor, + reverse: _bool, + batch_sizes: _size, + mode: _int, + hidden_size: _int, + num_layers: _int, + has_biases: _bool, + bidirectional: _bool, + batch_first: _bool, + train: _bool, +) -> tuple[Tensor, Tensor, Tensor, Tensor]: ... +@overload +def mm(input: Tensor, mat2: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + mm(input, mat2, out_dtype=None, *, out=None) -> Tensor + + Performs a matrix multiplication of the matrices :attr:`input` and :attr:`mat2`. + + If :attr:`input` is a :math:`(n \times m)` tensor, :attr:`mat2` is a + :math:`(m \times p)` tensor, :attr:`out` will be a :math:`(n \times p)` tensor. + + .. note:: This function does not :ref:`broadcast `. + For broadcasting matrix products, see :func:`torch.matmul`. + + Supports strided and sparse 2-D tensors as inputs, autograd with + respect to strided inputs. + + This operation has support for arguments with :ref:`sparse layouts`. + If :attr:`out` is provided its layout will be used. Otherwise, the result + layout will be deduced from that of :attr:`input`. + + + .. warning:: + Sparse support is a beta feature and some layout(s)/dtype/device combinations may not be supported, + or may not have autograd support. If you notice missing functionality please + open a feature request. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): the first matrix to be matrix multiplied + mat2 (Tensor): the second matrix to be matrix multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> mat1 = torch.randn(2, 3) + >>> mat2 = torch.randn(3, 3) + >>> torch.mm(mat1, mat2) + tensor([[ 0.4851, 0.5037, -0.3633], + [-0.0760, -3.6705, 2.4784]]) + """ + +@overload +def mm( + input: Tensor, + mat2: Tensor, + out_dtype: _dtype, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + mm(input, mat2, out_dtype=None, *, out=None) -> Tensor + + Performs a matrix multiplication of the matrices :attr:`input` and :attr:`mat2`. + + If :attr:`input` is a :math:`(n \times m)` tensor, :attr:`mat2` is a + :math:`(m \times p)` tensor, :attr:`out` will be a :math:`(n \times p)` tensor. + + .. note:: This function does not :ref:`broadcast `. + For broadcasting matrix products, see :func:`torch.matmul`. + + Supports strided and sparse 2-D tensors as inputs, autograd with + respect to strided inputs. + + This operation has support for arguments with :ref:`sparse layouts`. + If :attr:`out` is provided its layout will be used. Otherwise, the result + layout will be deduced from that of :attr:`input`. + + + .. warning:: + Sparse support is a beta feature and some layout(s)/dtype/device combinations may not be supported, + or may not have autograd support. If you notice missing functionality please + open a feature request. + + This operator supports :ref:`TensorFloat32`. + + On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision` for backward. + + Args: + input (Tensor): the first matrix to be matrix multiplied + mat2 (Tensor): the second matrix to be matrix multiplied + out_dtype (dtype, optional): the dtype of the output tensor, + Supported only on CUDA and for torch.float32 given + torch.float16/torch.bfloat16 input dtypes + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> mat1 = torch.randn(2, 3) + >>> mat2 = torch.randn(3, 3) + >>> torch.mm(mat1, mat2) + tensor([[ 0.4851, 0.5037, -0.3633], + [-0.0760, -3.6705, 2.4784]]) + """ + +@overload +def mode( + input: Tensor, + dim: _int = -1, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.mode: + r""" + mode(input, dim=-1, keepdim=False, *, out=None) -> (Tensor, LongTensor) + + Returns a namedtuple ``(values, indices)`` where ``values`` is the mode + value of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`, i.e. a value which appears most often + in that row, and ``indices`` is the index location of each mode value found. + + By default, :attr:`dim` is the last dimension of the :attr:`input` tensor. + + If :attr:`keepdim` is ``True``, the output tensors are of the same size as + :attr:`input` except in the dimension :attr:`dim` where they are of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting + in the output tensors having 1 fewer dimension than :attr:`input`. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the result tuple of two output tensors (values, indices) + + Example:: + + >>> b = torch.tensor([[0, 0, 0, 2, 0, 0, 2], + ... [0, 3, 0, 0, 2, 0, 1], + ... [2, 2, 2, 0, 0, 0, 3], + ... [2, 2, 3, 0, 1, 1, 0], + ... [1, 1, 0, 0, 2, 0, 2]]) + >>> torch.mode(b, 0) + torch.return_types.mode( + values=tensor([0, 2, 0, 0, 0, 0, 2]), + indices=tensor([1, 3, 4, 4, 2, 4, 4])) + """ + +@overload +def mode( + input: Tensor, + dim: str | EllipsisType | None, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.mode: + r""" + mode(input, dim=-1, keepdim=False, *, out=None) -> (Tensor, LongTensor) + + Returns a namedtuple ``(values, indices)`` where ``values`` is the mode + value of each row of the :attr:`input` tensor in the given dimension + :attr:`dim`, i.e. a value which appears most often + in that row, and ``indices`` is the index location of each mode value found. + + By default, :attr:`dim` is the last dimension of the :attr:`input` tensor. + + If :attr:`keepdim` is ``True``, the output tensors are of the same size as + :attr:`input` except in the dimension :attr:`dim` where they are of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting + in the output tensors having 1 fewer dimension than :attr:`input`. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out (tuple, optional): the result tuple of two output tensors (values, indices) + + Example:: + + >>> b = torch.tensor([[0, 0, 0, 2, 0, 0, 2], + ... [0, 3, 0, 0, 2, 0, 1], + ... [2, 2, 2, 0, 0, 0, 3], + ... [2, 2, 3, 0, 1, 1, 0], + ... [1, 1, 0, 0, 2, 0, 2]]) + >>> torch.mode(b, 0) + torch.return_types.mode( + values=tensor([0, 2, 0, 0, 0, 0, 2]), + indices=tensor([1, 3, 4, 4, 2, 4, 4])) + """ + +@overload +def moveaxis(input: Tensor, source: _int, destination: _int) -> Tensor: + r""" + moveaxis(input, source, destination) -> Tensor + + Alias for :func:`torch.movedim`. + + This function is equivalent to NumPy's moveaxis function. + + Examples:: + + >>> t = torch.randn(3,2,1) + >>> t + tensor([[[-0.3362], + [-0.8437]], + + [[-0.9627], + [ 0.1727]], + + [[ 0.5173], + [-0.1398]]]) + >>> torch.moveaxis(t, 1, 0).shape + torch.Size([2, 3, 1]) + >>> torch.moveaxis(t, 1, 0) + tensor([[[-0.3362], + [-0.9627], + [ 0.5173]], + + [[-0.8437], + [ 0.1727], + [-0.1398]]]) + >>> torch.moveaxis(t, (1, 2), (0, 1)).shape + torch.Size([2, 1, 3]) + >>> torch.moveaxis(t, (1, 2), (0, 1)) + tensor([[[-0.3362, -0.9627, 0.5173]], + + [[-0.8437, 0.1727, -0.1398]]]) + """ + +@overload +def moveaxis(input: Tensor, source: _size, destination: _size) -> Tensor: + r""" + moveaxis(input, source, destination) -> Tensor + + Alias for :func:`torch.movedim`. + + This function is equivalent to NumPy's moveaxis function. + + Examples:: + + >>> t = torch.randn(3,2,1) + >>> t + tensor([[[-0.3362], + [-0.8437]], + + [[-0.9627], + [ 0.1727]], + + [[ 0.5173], + [-0.1398]]]) + >>> torch.moveaxis(t, 1, 0).shape + torch.Size([2, 3, 1]) + >>> torch.moveaxis(t, 1, 0) + tensor([[[-0.3362], + [-0.9627], + [ 0.5173]], + + [[-0.8437], + [ 0.1727], + [-0.1398]]]) + >>> torch.moveaxis(t, (1, 2), (0, 1)).shape + torch.Size([2, 1, 3]) + >>> torch.moveaxis(t, (1, 2), (0, 1)) + tensor([[[-0.3362, -0.9627, 0.5173]], + + [[-0.8437, 0.1727, -0.1398]]]) + """ + +@overload +def movedim(input: Tensor, source: _int, destination: _int) -> Tensor: + r""" + movedim(input, source, destination) -> Tensor + + Moves the dimension(s) of :attr:`input` at the position(s) in :attr:`source` + to the position(s) in :attr:`destination`. + + Other dimensions of :attr:`input` that are not explicitly moved remain in + their original order and appear at the positions not specified in :attr:`destination`. + + Args: + input (Tensor): the input tensor. + source (int or tuple of ints): Original positions of the dims to move. These must be unique. + destination (int or tuple of ints): Destination positions for each of the original dims. These must also be unique. + + Examples:: + + >>> t = torch.randn(3,2,1) + >>> t + tensor([[[-0.3362], + [-0.8437]], + + [[-0.9627], + [ 0.1727]], + + [[ 0.5173], + [-0.1398]]]) + >>> torch.movedim(t, 1, 0).shape + torch.Size([2, 3, 1]) + >>> torch.movedim(t, 1, 0) + tensor([[[-0.3362], + [-0.9627], + [ 0.5173]], + + [[-0.8437], + [ 0.1727], + [-0.1398]]]) + >>> torch.movedim(t, (1, 2), (0, 1)).shape + torch.Size([2, 1, 3]) + >>> torch.movedim(t, (1, 2), (0, 1)) + tensor([[[-0.3362, -0.9627, 0.5173]], + + [[-0.8437, 0.1727, -0.1398]]]) + """ + +@overload +def movedim(input: Tensor, source: _size, destination: _size) -> Tensor: + r""" + movedim(input, source, destination) -> Tensor + + Moves the dimension(s) of :attr:`input` at the position(s) in :attr:`source` + to the position(s) in :attr:`destination`. + + Other dimensions of :attr:`input` that are not explicitly moved remain in + their original order and appear at the positions not specified in :attr:`destination`. + + Args: + input (Tensor): the input tensor. + source (int or tuple of ints): Original positions of the dims to move. These must be unique. + destination (int or tuple of ints): Destination positions for each of the original dims. These must also be unique. + + Examples:: + + >>> t = torch.randn(3,2,1) + >>> t + tensor([[[-0.3362], + [-0.8437]], + + [[-0.9627], + [ 0.1727]], + + [[ 0.5173], + [-0.1398]]]) + >>> torch.movedim(t, 1, 0).shape + torch.Size([2, 3, 1]) + >>> torch.movedim(t, 1, 0) + tensor([[[-0.3362], + [-0.9627], + [ 0.5173]], + + [[-0.8437], + [ 0.1727], + [-0.1398]]]) + >>> torch.movedim(t, (1, 2), (0, 1)).shape + torch.Size([2, 1, 3]) + >>> torch.movedim(t, (1, 2), (0, 1)) + tensor([[[-0.3362, -0.9627, 0.5173]], + + [[-0.8437, 0.1727, -0.1398]]]) + """ + +def msort(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + msort(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Sorts the elements of the :attr:`input` tensor along its first dimension + in ascending order by value. + + .. note:: `torch.msort(t)` is equivalent to `torch.sort(t, dim=0)[0]`. + See also :func:`torch.sort`. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.randn(3, 4) + >>> t + tensor([[-0.1321, 0.4370, -1.2631, -1.1289], + [-2.0527, -1.1250, 0.2275, 0.3077], + [-0.0881, -0.1259, -0.5495, 1.0284]]) + >>> torch.msort(t) + tensor([[-2.0527, -1.1250, -1.2631, -1.1289], + [-0.1321, -0.1259, -0.5495, 0.3077], + [-0.0881, 0.4370, 0.2275, 1.0284]]) + """ + +def mul( + input: Tensor | Number | _complex, + other: Tensor | Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + mul(input, other, *, out=None) -> Tensor + + Multiplies :attr:`input` by :attr:`other`. + + + .. math:: + \text{out}_i = \text{input}_i \times \text{other}_i + + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer, float, and complex inputs. + + Args: + input (Tensor): the input tensor. + other (Tensor or Number): the tensor or number to multiply input by. + + Keyword args: + out (Tensor, optional): the output tensor. + + Examples:: + + >>> a = torch.randn(3) + >>> a + tensor([ 0.2015, -0.4255, 2.6087]) + >>> torch.mul(a, 100) + tensor([ 20.1494, -42.5491, 260.8663]) + + >>> b = torch.randn(4, 1) + >>> b + tensor([[ 1.1207], + [-0.3137], + [ 0.0700], + [ 0.8378]]) + >>> c = torch.randn(1, 4) + >>> c + tensor([[ 0.5146, 0.1216, -0.5244, 2.2382]]) + >>> torch.mul(b, c) + tensor([[ 0.5767, 0.1363, -0.5877, 2.5083], + [-0.1614, -0.0382, 0.1645, -0.7021], + [ 0.0360, 0.0085, -0.0367, 0.1567], + [ 0.4312, 0.1019, -0.4394, 1.8753]]) + """ + +def multinomial( + input: Tensor, + num_samples: _int | SymInt, + replacement: _bool = False, + *, + generator: Generator | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + multinomial(input, num_samples, replacement=False, *, generator=None, out=None) -> LongTensor + + Returns a tensor where each row contains :attr:`num_samples` indices sampled + from the multinomial (a stricter definition would be multivariate, + refer to :class:`torch.distributions.multinomial.Multinomial` for more details) + probability distribution located in the corresponding row + of tensor :attr:`input`. + + .. note:: + The rows of :attr:`input` do not need to sum to one (in which case we use + the values as weights), but must be non-negative, finite and have + a non-zero sum. + + Indices are ordered from left to right according to when each was sampled + (first samples are placed in first column). + + If :attr:`input` is a vector, :attr:`out` is a vector of size :attr:`num_samples`. + + If :attr:`input` is a matrix with `m` rows, :attr:`out` is an matrix of shape + :math:`(m \times \text{num\_samples})`. + + If replacement is ``True``, samples are drawn with replacement. + + If not, they are drawn without replacement, which means that when a + sample index is drawn for a row, it cannot be drawn again for that row. + + .. note:: + When drawn without replacement, :attr:`num_samples` must be lower than + number of non-zero elements in :attr:`input` (or the min number of non-zero + elements in each row of :attr:`input` if it is a matrix). + + Args: + input (Tensor): the input tensor containing probabilities + num_samples (int): number of samples to draw + replacement (bool, optional): whether to draw with replacement or not + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + + Example:: + + >>> weights = torch.tensor([0, 10, 3, 0], dtype=torch.float) # create a tensor of weights + >>> torch.multinomial(weights, 2) + tensor([1, 2]) + >>> torch.multinomial(weights, 5) # ERROR! + RuntimeError: cannot sample n_sample > prob_dist.size(-1) samples without replacement + >>> torch.multinomial(weights, 4, replacement=True) + tensor([ 2, 1, 1, 1]) + """ + +@overload +def multiply( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + multiply(input, other, *, out=None) + + Alias for :func:`torch.mul`. + """ + +@overload +def multiply(input: Tensor, other: Number | _complex) -> Tensor: + r""" + multiply(input, other, *, out=None) + + Alias for :func:`torch.mul`. + """ + +def mv(input: Tensor, vec: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + mv(input, vec, *, out=None) -> Tensor + + Performs a matrix-vector product of the matrix :attr:`input` and the vector + :attr:`vec`. + + If :attr:`input` is a :math:`(n \times m)` tensor, :attr:`vec` is a 1-D tensor of + size :math:`m`, :attr:`out` will be 1-D of size :math:`n`. + + .. note:: This function does not :ref:`broadcast `. + + Args: + input (Tensor): matrix to be multiplied + vec (Tensor): vector to be multiplied + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> mat = torch.randn(2, 3) + >>> vec = torch.randn(3) + >>> torch.mv(mat, vec) + tensor([ 1.0404, -0.6361]) + """ + +def mvlgamma( + input: Tensor, + p: _int, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + mvlgamma(input, p, *, out=None) -> Tensor + + Alias for :func:`torch.special.multigammaln`. + """ + +def nan_to_num( + input: Tensor, + nan: _float | None = None, + posinf: _float | None = None, + neginf: _float | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + nan_to_num(input, nan=0.0, posinf=None, neginf=None, *, out=None) -> Tensor + + Replaces :literal:`NaN`, positive infinity, and negative infinity values in :attr:`input` + with the values specified by :attr:`nan`, :attr:`posinf`, and :attr:`neginf`, respectively. + By default, :literal:`NaN`\ s are replaced with zero, positive infinity is replaced with the + greatest finite value representable by :attr:`input`'s dtype, and negative infinity + is replaced with the least finite value representable by :attr:`input`'s dtype. + + Args: + input (Tensor): the input tensor. + nan (Number, optional): the value to replace :literal:`NaN`\s with. Default is zero. + posinf (Number, optional): if a Number, the value to replace positive infinity values with. + If None, positive infinity values are replaced with the greatest finite value representable by :attr:`input`'s dtype. + Default is None. + neginf (Number, optional): if a Number, the value to replace negative infinity values with. + If None, negative infinity values are replaced with the lowest finite value representable by :attr:`input`'s dtype. + Default is None. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> x = torch.tensor([float('nan'), float('inf'), -float('inf'), 3.14]) + >>> torch.nan_to_num(x) + tensor([ 0.0000e+00, 3.4028e+38, -3.4028e+38, 3.1400e+00]) + >>> torch.nan_to_num(x, nan=2.0) + tensor([ 2.0000e+00, 3.4028e+38, -3.4028e+38, 3.1400e+00]) + >>> torch.nan_to_num(x, nan=2.0, posinf=1.0) + tensor([ 2.0000e+00, 1.0000e+00, -3.4028e+38, 3.1400e+00]) + """ + +def nan_to_num_( + input: Tensor, + nan: _float | None = None, + posinf: _float | None = None, + neginf: _float | None = None, +) -> Tensor: ... +def nanmean( + input: Tensor, + dim: _int | _size | None = None, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + nanmean(input, dim=None, keepdim=False, *, dtype=None, out=None) -> Tensor + + Computes the mean of all `non-NaN` elements along the specified dimensions. + Input must be floating point or complex. + + This function is identical to :func:`torch.mean` when there are no `NaN` values + in the :attr:`input` tensor. In the presence of `NaN`, :func:`torch.mean` will + propagate the `NaN` to the output whereas :func:`torch.nanmean` will ignore the + `NaN` values (`torch.nanmean(a)` is equivalent to `torch.mean(a[~a.isnan()])`). + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor, either of floating point or complex dtype + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + out (Tensor, optional): the output tensor. + + .. seealso:: + + :func:`torch.mean` computes the mean value, propagating `NaN`. + + Example:: + + >>> x = torch.tensor([[torch.nan, 1, 2], [1, 2, 3]]) + >>> x.mean() + tensor(nan) + >>> x.nanmean() + tensor(1.8000) + >>> x.mean(dim=0) + tensor([ nan, 1.5000, 2.5000]) + >>> x.nanmean(dim=0) + tensor([1.0000, 1.5000, 2.5000]) + + # If all elements in the reduced dimensions are NaN then the result is NaN + >>> torch.tensor([torch.nan]).nanmean() + tensor(nan) + """ + +@overload +def nanmedian(input: Tensor) -> Tensor: + r""" + nanmedian(input) -> Tensor + + Returns the median of the values in :attr:`input`, ignoring ``NaN`` values. + + This function is identical to :func:`torch.median` when there are no ``NaN`` values in :attr:`input`. + When :attr:`input` has one or more ``NaN`` values, :func:`torch.median` will always return ``NaN``, + while this function will return the median of the non-``NaN`` elements in :attr:`input`. + If all the elements in :attr:`input` are ``NaN`` it will also return ``NaN``. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> a = torch.tensor([1, float('nan'), 3, 2]) + >>> a.median() + tensor(nan) + >>> a.nanmedian() + tensor(2.) + + .. function:: nanmedian(input, dim=-1, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` contains the median of each row of :attr:`input` + in the dimension :attr:`dim`, ignoring ``NaN`` values, and ``indices`` contains the index of the median values + found in the dimension :attr:`dim`. + + This function is identical to :func:`torch.median` when there are no ``NaN`` values in a reduced row. When a reduced row has + one or more ``NaN`` values, :func:`torch.median` will always reduce it to ``NaN``, while this function will reduce it to the + median of the non-``NaN`` elements. If all the elements in a reduced row are ``NaN`` then it will be reduced to ``NaN``, too. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out ((Tensor, Tensor), optional): The first tensor will be populated with the median values and the second + tensor, which must have dtype long, with their indices in the dimension + :attr:`dim` of :attr:`input`. + + Example:: + + >>> a = torch.tensor([[2, 3, 1], [float('nan'), 1, float('nan')]]) + >>> a + tensor([[2., 3., 1.], + [nan, 1., nan]]) + >>> a.median(0) + torch.return_types.median(values=tensor([nan, 1., nan]), indices=tensor([1, 1, 1])) + >>> a.nanmedian(0) + torch.return_types.nanmedian(values=tensor([2., 1., 1.]), indices=tensor([0, 1, 0])) + """ + +@overload +def nanmedian( + input: Tensor, + dim: _int, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.nanmedian: + r""" + nanmedian(input) -> Tensor + + Returns the median of the values in :attr:`input`, ignoring ``NaN`` values. + + This function is identical to :func:`torch.median` when there are no ``NaN`` values in :attr:`input`. + When :attr:`input` has one or more ``NaN`` values, :func:`torch.median` will always return ``NaN``, + while this function will return the median of the non-``NaN`` elements in :attr:`input`. + If all the elements in :attr:`input` are ``NaN`` it will also return ``NaN``. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> a = torch.tensor([1, float('nan'), 3, 2]) + >>> a.median() + tensor(nan) + >>> a.nanmedian() + tensor(2.) + + .. function:: nanmedian(input, dim=-1, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` contains the median of each row of :attr:`input` + in the dimension :attr:`dim`, ignoring ``NaN`` values, and ``indices`` contains the index of the median values + found in the dimension :attr:`dim`. + + This function is identical to :func:`torch.median` when there are no ``NaN`` values in a reduced row. When a reduced row has + one or more ``NaN`` values, :func:`torch.median` will always reduce it to ``NaN``, while this function will reduce it to the + median of the non-``NaN`` elements. If all the elements in a reduced row are ``NaN`` then it will be reduced to ``NaN``, too. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out ((Tensor, Tensor), optional): The first tensor will be populated with the median values and the second + tensor, which must have dtype long, with their indices in the dimension + :attr:`dim` of :attr:`input`. + + Example:: + + >>> a = torch.tensor([[2, 3, 1], [float('nan'), 1, float('nan')]]) + >>> a + tensor([[2., 3., 1.], + [nan, 1., nan]]) + >>> a.median(0) + torch.return_types.median(values=tensor([nan, 1., nan]), indices=tensor([1, 1, 1])) + >>> a.nanmedian(0) + torch.return_types.nanmedian(values=tensor([2., 1., 1.]), indices=tensor([0, 1, 0])) + """ + +@overload +def nanmedian( + input: Tensor, + dim: str | EllipsisType | None, + keepdim: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.nanmedian: + r""" + nanmedian(input) -> Tensor + + Returns the median of the values in :attr:`input`, ignoring ``NaN`` values. + + This function is identical to :func:`torch.median` when there are no ``NaN`` values in :attr:`input`. + When :attr:`input` has one or more ``NaN`` values, :func:`torch.median` will always return ``NaN``, + while this function will return the median of the non-``NaN`` elements in :attr:`input`. + If all the elements in :attr:`input` are ``NaN`` it will also return ``NaN``. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> a = torch.tensor([1, float('nan'), 3, 2]) + >>> a.median() + tensor(nan) + >>> a.nanmedian() + tensor(2.) + + .. function:: nanmedian(input, dim=-1, keepdim=False, *, out=None) -> (Tensor, LongTensor) + :noindex: + + Returns a namedtuple ``(values, indices)`` where ``values`` contains the median of each row of :attr:`input` + in the dimension :attr:`dim`, ignoring ``NaN`` values, and ``indices`` contains the index of the median values + found in the dimension :attr:`dim`. + + This function is identical to :func:`torch.median` when there are no ``NaN`` values in a reduced row. When a reduced row has + one or more ``NaN`` values, :func:`torch.median` will always reduce it to ``NaN``, while this function will reduce it to the + median of the non-``NaN`` elements. If all the elements in a reduced row are ``NaN`` then it will be reduced to ``NaN``, too. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + out ((Tensor, Tensor), optional): The first tensor will be populated with the median values and the second + tensor, which must have dtype long, with their indices in the dimension + :attr:`dim` of :attr:`input`. + + Example:: + + >>> a = torch.tensor([[2, 3, 1], [float('nan'), 1, float('nan')]]) + >>> a + tensor([[2., 3., 1.], + [nan, 1., nan]]) + >>> a.median(0) + torch.return_types.median(values=tensor([nan, 1., nan]), indices=tensor([1, 1, 1])) + >>> a.nanmedian(0) + torch.return_types.nanmedian(values=tensor([2., 1., 1.]), indices=tensor([0, 1, 0])) + """ + +@overload +def nanquantile( + input: Tensor, + q: Tensor, + dim: _int | None = None, + keepdim: _bool = False, + *, + interpolation: str = "linear", + out: Tensor | None = None, +) -> Tensor: + r""" + nanquantile(input, q, dim=None, keepdim=False, *, interpolation='linear', out=None) -> Tensor + + This is a variant of :func:`torch.quantile` that "ignores" ``NaN`` values, + computing the quantiles :attr:`q` as if ``NaN`` values in :attr:`input` did + not exist. If all values in a reduced row are ``NaN`` then the quantiles for + that reduction will be ``NaN``. See the documentation for :func:`torch.quantile`. + + Args: + input (Tensor): the input tensor. + q (float or Tensor): a scalar or 1D tensor of quantile values in the range [0, 1] + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword arguments: + interpolation (str): interpolation method to use when the desired quantile lies between two data points. + Can be ``linear``, ``lower``, ``higher``, ``midpoint`` and ``nearest``. + Default is ``linear``. + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.tensor([float('nan'), 1, 2]) + >>> t.quantile(0.5) + tensor(nan) + >>> t.nanquantile(0.5) + tensor(1.5000) + >>> t = torch.tensor([[float('nan'), float('nan')], [1, 2]]) + >>> t + tensor([[nan, nan], + [1., 2.]]) + >>> t.nanquantile(0.5, dim=0) + tensor([1., 2.]) + >>> t.nanquantile(0.5, dim=1) + tensor([ nan, 1.5000]) + """ + +@overload +def nanquantile( + input: Tensor, + q: _float, + dim: _int | None = None, + keepdim: _bool = False, + *, + interpolation: str = "linear", + out: Tensor | None = None, +) -> Tensor: + r""" + nanquantile(input, q, dim=None, keepdim=False, *, interpolation='linear', out=None) -> Tensor + + This is a variant of :func:`torch.quantile` that "ignores" ``NaN`` values, + computing the quantiles :attr:`q` as if ``NaN`` values in :attr:`input` did + not exist. If all values in a reduced row are ``NaN`` then the quantiles for + that reduction will be ``NaN``. See the documentation for :func:`torch.quantile`. + + Args: + input (Tensor): the input tensor. + q (float or Tensor): a scalar or 1D tensor of quantile values in the range [0, 1] + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword arguments: + interpolation (str): interpolation method to use when the desired quantile lies between two data points. + Can be ``linear``, ``lower``, ``higher``, ``midpoint`` and ``nearest``. + Default is ``linear``. + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.tensor([float('nan'), 1, 2]) + >>> t.quantile(0.5) + tensor(nan) + >>> t.nanquantile(0.5) + tensor(1.5000) + >>> t = torch.tensor([[float('nan'), float('nan')], [1, 2]]) + >>> t + tensor([[nan, nan], + [1., 2.]]) + >>> t.nanquantile(0.5, dim=0) + tensor([1., 2.]) + >>> t.nanquantile(0.5, dim=1) + tensor([ nan, 1.5000]) + """ + +def nansum( + input: Tensor, + dim: _int | _size | None = None, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + nansum(input, *, dtype=None) -> Tensor + + Returns the sum of all elements, treating Not a Numbers (NaNs) as zero. + + Args: + input (Tensor): the input tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.tensor([1., 2., float('nan'), 4.]) + >>> torch.nansum(a) + tensor(7.) + + .. function:: nansum(input, dim, keepdim=False, *, dtype=None) -> Tensor + :noindex: + + Returns the sum of each row of the :attr:`input` tensor in the given + dimension :attr:`dim`, treating Not a Numbers (NaNs) as zero. + If :attr:`dim` is a list of dimensions, reduce over all of them. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> torch.nansum(torch.tensor([1., float("nan")])) + tensor(1.) + >>> a = torch.tensor([[1, 2], [3., float("nan")]]) + >>> torch.nansum(a) + tensor(6.) + >>> torch.nansum(a, dim=0) + tensor([4., 2.]) + >>> torch.nansum(a, dim=1) + tensor([3., 3.]) + """ + +@overload +def narrow( + input: Tensor, + dim: _int, + start: Tensor, + length: _int | SymInt, +) -> Tensor: + r""" + narrow(input, dim, start, length) -> Tensor + + Returns a new tensor that is a narrowed version of :attr:`input` tensor. The + dimension :attr:`dim` is input from :attr:`start` to ``start + length``. The + returned tensor and :attr:`input` tensor share the same underlying storage. + + Args: + input (Tensor): the tensor to narrow + dim (int): the dimension along which to narrow + start (int or Tensor): index of the element to start the narrowed dimension + from. Can be negative, which means indexing from the end of `dim`. If + `Tensor`, it must be an 0-dim integral `Tensor` (bools not allowed) + length (int): length of the narrowed dimension, must be weakly positive + + Example:: + + >>> x = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + >>> torch.narrow(x, 0, 0, 2) + tensor([[ 1, 2, 3], + [ 4, 5, 6]]) + >>> torch.narrow(x, 1, 1, 2) + tensor([[ 2, 3], + [ 5, 6], + [ 8, 9]]) + >>> torch.narrow(x, -1, torch.tensor(-1), 1) + tensor([[3], + [6], + [9]]) + """ + +@overload +def narrow( + input: Tensor, + dim: _int, + start: _int | SymInt, + length: _int | SymInt, +) -> Tensor: + r""" + narrow(input, dim, start, length) -> Tensor + + Returns a new tensor that is a narrowed version of :attr:`input` tensor. The + dimension :attr:`dim` is input from :attr:`start` to ``start + length``. The + returned tensor and :attr:`input` tensor share the same underlying storage. + + Args: + input (Tensor): the tensor to narrow + dim (int): the dimension along which to narrow + start (int or Tensor): index of the element to start the narrowed dimension + from. Can be negative, which means indexing from the end of `dim`. If + `Tensor`, it must be an 0-dim integral `Tensor` (bools not allowed) + length (int): length of the narrowed dimension, must be weakly positive + + Example:: + + >>> x = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + >>> torch.narrow(x, 0, 0, 2) + tensor([[ 1, 2, 3], + [ 4, 5, 6]]) + >>> torch.narrow(x, 1, 1, 2) + tensor([[ 2, 3], + [ 5, 6], + [ 8, 9]]) + >>> torch.narrow(x, -1, torch.tensor(-1), 1) + tensor([[3], + [6], + [9]]) + """ + +def narrow_copy( + input: Tensor, + dim: _int, + start: _int | SymInt, + length: _int | SymInt, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + narrow_copy(input, dim, start, length, *, out=None) -> Tensor + + Same as :meth:`Tensor.narrow` except this returns a copy rather + than shared storage. This is primarily for sparse tensors, which + do not have a shared-storage narrow method. + + Args: + input (Tensor): the tensor to narrow + dim (int): the dimension along which to narrow + start (int): index of the element to start the narrowed dimension from. Can + be negative, which means indexing from the end of `dim` + length (int): length of the narrowed dimension, must be weakly positive + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> x = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + >>> torch.narrow_copy(x, 0, 0, 2) + tensor([[ 1, 2, 3], + [ 4, 5, 6]]) + >>> torch.narrow_copy(x, 1, 1, 2) + tensor([[ 2, 3], + [ 5, 6], + [ 8, 9]]) + >>> s = torch.arange(16).reshape(2, 2, 2, 2).to_sparse(2) + >>> torch.narrow_copy(s, 0, 0, 1) + tensor(indices=tensor([[0, 0], + [0, 1]]), + values=tensor([[[0, 1], + [2, 3]], + + [[4, 5], + [6, 7]]]), + size=(1, 2, 2, 2), nnz=2, layout=torch.sparse_coo) + + .. seealso:: + + :func:`torch.narrow` for a non copy variant + """ + +def native_batch_norm( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor | None, + running_var: Tensor | None, + training: _bool, + momentum: _float, + eps: _float, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> tuple[Tensor, Tensor, Tensor]: ... +def native_channel_shuffle(input: Tensor, groups: _int | SymInt) -> Tensor: ... +def native_dropout( + input: Tensor, + p: _float, + train: _bool | None, +) -> tuple[Tensor, Tensor]: ... +def native_group_norm( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + N: _int | SymInt, + C: _int | SymInt, + HxW: _int | SymInt, + group: _int, + eps: _float, +) -> tuple[Tensor, Tensor, Tensor]: ... +def native_layer_norm( + input: Tensor, + normalized_shape: Sequence[_int | SymInt], + weight: Tensor | None, + bias: Tensor | None, + eps: _float, +) -> tuple[Tensor, Tensor, Tensor]: ... +@overload +def native_norm( + input: Tensor, + p: Number | _complex | None, + dim: _int | _size, + keepdim: _bool, + dtype: _dtype | None, +) -> Tensor: ... +@overload +def native_norm(input: Tensor, p: Number | _complex = 2) -> Tensor: ... +@overload +def ne( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + ne(input, other, *, out=None) -> Tensor + + Computes :math:`\text{input} \neq \text{other}` element-wise. + + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or float): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is not equal to :attr:`other` and False elsewhere + + Example:: + + >>> torch.ne(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[False, True], [True, False]]) + """ + +@overload +def ne( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + ne(input, other, *, out=None) -> Tensor + + Computes :math:`\text{input} \neq \text{other}` element-wise. + + + The second argument can be a number or a tensor whose shape is + :ref:`broadcastable ` with the first argument. + + Args: + input (Tensor): the tensor to compare + other (Tensor or float): the tensor or value to compare + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + A boolean tensor that is True where :attr:`input` is not equal to :attr:`other` and False elsewhere + + Example:: + + >>> torch.ne(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]])) + tensor([[False, True], [True, False]]) + """ + +def neg(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + neg(input, *, out=None) -> Tensor + + Returns a new tensor with the negative of the elements of :attr:`input`. + + .. math:: + \text{out} = -1 \times \text{input} + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(5) + >>> a + tensor([ 0.0090, -0.2262, -0.0682, -0.2866, 0.3940]) + >>> torch.neg(a) + tensor([-0.0090, 0.2262, 0.0682, 0.2866, -0.3940]) + """ + +def neg_(input: Tensor) -> Tensor: ... +def negative(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + negative(input, *, out=None) -> Tensor + + Alias for :func:`torch.neg` + """ + +def negative_(input: Tensor) -> Tensor: ... +def nextafter( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + nextafter(input, other, *, out=None) -> Tensor + + Return the next floating-point value after :attr:`input` towards :attr:`other`, elementwise. + + The shapes of ``input`` and ``other`` must be + :ref:`broadcastable `. + + Args: + input (Tensor): the first input tensor + other (Tensor): the second input tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> eps = torch.finfo(torch.float32).eps + >>> torch.nextafter(torch.tensor([1.0, 2.0]), torch.tensor([2.0, 1.0])) == torch.tensor([eps + 1, 2 - eps]) + tensor([True, True]) + """ + +@overload +def nonzero( + input: Tensor, + *, + as_tuple: Literal[False] = False, + out: Tensor | None = None, +) -> Tensor: + r""" + nonzero(input, *, out=None, as_tuple=False) -> LongTensor or tuple of LongTensors + + .. note:: + :func:`torch.nonzero(..., as_tuple=False) ` (default) returns a + 2-D tensor where each row is the index for a nonzero value. + + :func:`torch.nonzero(..., as_tuple=True) ` returns a tuple of 1-D + index tensors, allowing for advanced indexing, so ``x[x.nonzero(as_tuple=True)]`` + gives all nonzero values of tensor ``x``. Of the returned tuple, each index tensor + contains nonzero indices for a certain dimension. + + See below for more details on the two behaviors. + + When :attr:`input` is on CUDA, :func:`torch.nonzero() ` causes + host-device synchronization. + + **When** :attr:`as_tuple` **is** ``False`` **(default)**: + + Returns a tensor containing the indices of all non-zero elements of + :attr:`input`. Each row in the result contains the indices of a non-zero + element in :attr:`input`. The result is sorted lexicographically, with + the last index changing the fastest (C-style). + + If :attr:`input` has :math:`n` dimensions, then the resulting indices tensor + :attr:`out` is of size :math:`(z \times n)`, where :math:`z` is the total number of + non-zero elements in the :attr:`input` tensor. + + **When** :attr:`as_tuple` **is** ``True``: + + Returns a tuple of 1-D tensors, one for each dimension in :attr:`input`, + each containing the indices (in that dimension) of all non-zero elements of + :attr:`input` . + + If :attr:`input` has :math:`n` dimensions, then the resulting tuple contains :math:`n` + tensors of size :math:`z`, where :math:`z` is the total number of + non-zero elements in the :attr:`input` tensor. + + As a special case, when :attr:`input` has zero dimensions and a nonzero scalar + value, it is treated as a one-dimensional tensor with one element. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (LongTensor, optional): the output tensor containing indices + + Returns: + LongTensor or tuple of LongTensor: If :attr:`as_tuple` is ``False``, the output + tensor containing indices. If :attr:`as_tuple` is ``True``, one 1-D tensor for + each dimension, containing the indices of each nonzero element along that + dimension. + + Example:: + + >>> torch.nonzero(torch.tensor([1, 1, 1, 0, 1])) + tensor([[ 0], + [ 1], + [ 2], + [ 4]]) + >>> torch.nonzero(torch.tensor([[0.6, 0.0, 0.0, 0.0], + ... [0.0, 0.4, 0.0, 0.0], + ... [0.0, 0.0, 1.2, 0.0], + ... [0.0, 0.0, 0.0,-0.4]])) + tensor([[ 0, 0], + [ 1, 1], + [ 2, 2], + [ 3, 3]]) + >>> torch.nonzero(torch.tensor([1, 1, 1, 0, 1]), as_tuple=True) + (tensor([0, 1, 2, 4]),) + >>> torch.nonzero(torch.tensor([[0.6, 0.0, 0.0, 0.0], + ... [0.0, 0.4, 0.0, 0.0], + ... [0.0, 0.0, 1.2, 0.0], + ... [0.0, 0.0, 0.0,-0.4]]), as_tuple=True) + (tensor([0, 1, 2, 3]), tensor([0, 1, 2, 3])) + >>> torch.nonzero(torch.tensor(5), as_tuple=True) + (tensor([0]),) + """ + +@overload +def nonzero( + input: Tensor, + *, + as_tuple: Literal[True], +) -> tuple[Tensor, ...]: + r""" + nonzero(input, *, out=None, as_tuple=False) -> LongTensor or tuple of LongTensors + + .. note:: + :func:`torch.nonzero(..., as_tuple=False) ` (default) returns a + 2-D tensor where each row is the index for a nonzero value. + + :func:`torch.nonzero(..., as_tuple=True) ` returns a tuple of 1-D + index tensors, allowing for advanced indexing, so ``x[x.nonzero(as_tuple=True)]`` + gives all nonzero values of tensor ``x``. Of the returned tuple, each index tensor + contains nonzero indices for a certain dimension. + + See below for more details on the two behaviors. + + When :attr:`input` is on CUDA, :func:`torch.nonzero() ` causes + host-device synchronization. + + **When** :attr:`as_tuple` **is** ``False`` **(default)**: + + Returns a tensor containing the indices of all non-zero elements of + :attr:`input`. Each row in the result contains the indices of a non-zero + element in :attr:`input`. The result is sorted lexicographically, with + the last index changing the fastest (C-style). + + If :attr:`input` has :math:`n` dimensions, then the resulting indices tensor + :attr:`out` is of size :math:`(z \times n)`, where :math:`z` is the total number of + non-zero elements in the :attr:`input` tensor. + + **When** :attr:`as_tuple` **is** ``True``: + + Returns a tuple of 1-D tensors, one for each dimension in :attr:`input`, + each containing the indices (in that dimension) of all non-zero elements of + :attr:`input` . + + If :attr:`input` has :math:`n` dimensions, then the resulting tuple contains :math:`n` + tensors of size :math:`z`, where :math:`z` is the total number of + non-zero elements in the :attr:`input` tensor. + + As a special case, when :attr:`input` has zero dimensions and a nonzero scalar + value, it is treated as a one-dimensional tensor with one element. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (LongTensor, optional): the output tensor containing indices + + Returns: + LongTensor or tuple of LongTensor: If :attr:`as_tuple` is ``False``, the output + tensor containing indices. If :attr:`as_tuple` is ``True``, one 1-D tensor for + each dimension, containing the indices of each nonzero element along that + dimension. + + Example:: + + >>> torch.nonzero(torch.tensor([1, 1, 1, 0, 1])) + tensor([[ 0], + [ 1], + [ 2], + [ 4]]) + >>> torch.nonzero(torch.tensor([[0.6, 0.0, 0.0, 0.0], + ... [0.0, 0.4, 0.0, 0.0], + ... [0.0, 0.0, 1.2, 0.0], + ... [0.0, 0.0, 0.0,-0.4]])) + tensor([[ 0, 0], + [ 1, 1], + [ 2, 2], + [ 3, 3]]) + >>> torch.nonzero(torch.tensor([1, 1, 1, 0, 1]), as_tuple=True) + (tensor([0, 1, 2, 4]),) + >>> torch.nonzero(torch.tensor([[0.6, 0.0, 0.0, 0.0], + ... [0.0, 0.4, 0.0, 0.0], + ... [0.0, 0.0, 1.2, 0.0], + ... [0.0, 0.0, 0.0,-0.4]]), as_tuple=True) + (tensor([0, 1, 2, 3]), tensor([0, 1, 2, 3])) + >>> torch.nonzero(torch.tensor(5), as_tuple=True) + (tensor([0]),) + """ + +def nonzero_static( + input: Tensor, + *, + size: _int | SymInt, + fill_value: _int = -1, + out: Tensor | None = None, +) -> Tensor: ... +def norm_except_dim(v: Tensor, pow: _int = 2, dim: _int = 0) -> Tensor: ... +@overload +def normal( + mean: Tensor, + std: Tensor, + *, + generator: Generator | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + normal(mean, std, *, generator=None, out=None) -> Tensor + + Returns a tensor of random numbers drawn from separate normal distributions + whose mean and standard deviation are given. + + The :attr:`mean` is a tensor with the mean of + each output element's normal distribution + + The :attr:`std` is a tensor with the standard deviation of + each output element's normal distribution + + The shapes of :attr:`mean` and :attr:`std` don't need to match, but the + total number of elements in each tensor need to be the same. + + .. note:: When the shapes do not match, the shape of :attr:`mean` + is used as the shape for the returned output tensor + + .. note:: When :attr:`std` is a CUDA tensor, this function synchronizes + its device with the CPU. + + Args: + mean (Tensor): the tensor of per-element means + std (Tensor): the tensor of per-element standard deviations + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(mean=torch.arange(1., 11.), std=torch.arange(1, 0, -0.1)) + tensor([ 1.0425, 3.5672, 2.7969, 4.2925, 4.7229, 6.2134, + 8.0505, 8.1408, 9.0563, 10.0566]) + + .. function:: normal(mean=0.0, std, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the means are shared among all drawn + elements. + + Args: + mean (float, optional): the mean for all distributions + std (Tensor): the tensor of per-element standard deviations + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(mean=0.5, std=torch.arange(1., 6.)) + tensor([-1.2793, -1.0732, -2.0687, 5.1177, -1.2303]) + + .. function:: normal(mean, std=1.0, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the standard deviations are shared among + all drawn elements. + + Args: + mean (Tensor): the tensor of per-element means + std (float, optional): the standard deviation for all distributions + + Keyword args: + out (Tensor, optional): the output tensor + + Example:: + + >>> torch.normal(mean=torch.arange(1., 6.)) + tensor([ 1.1552, 2.6148, 2.6535, 5.8318, 4.2361]) + + .. function:: normal(mean, std, size, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the means and standard deviations are shared + among all drawn elements. The resulting tensor has size given by :attr:`size`. + + Args: + mean (float): the mean for all distributions + std (float): the standard deviation for all distributions + size (int...): a sequence of integers defining the shape of the output tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(2, 3, size=(1, 4)) + tensor([[-1.3987, -1.9544, 3.6048, 0.7909]]) + """ + +@overload +def normal( + mean: Tensor, + std: _float = 1, + *, + generator: Generator | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + normal(mean, std, *, generator=None, out=None) -> Tensor + + Returns a tensor of random numbers drawn from separate normal distributions + whose mean and standard deviation are given. + + The :attr:`mean` is a tensor with the mean of + each output element's normal distribution + + The :attr:`std` is a tensor with the standard deviation of + each output element's normal distribution + + The shapes of :attr:`mean` and :attr:`std` don't need to match, but the + total number of elements in each tensor need to be the same. + + .. note:: When the shapes do not match, the shape of :attr:`mean` + is used as the shape for the returned output tensor + + .. note:: When :attr:`std` is a CUDA tensor, this function synchronizes + its device with the CPU. + + Args: + mean (Tensor): the tensor of per-element means + std (Tensor): the tensor of per-element standard deviations + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(mean=torch.arange(1., 11.), std=torch.arange(1, 0, -0.1)) + tensor([ 1.0425, 3.5672, 2.7969, 4.2925, 4.7229, 6.2134, + 8.0505, 8.1408, 9.0563, 10.0566]) + + .. function:: normal(mean=0.0, std, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the means are shared among all drawn + elements. + + Args: + mean (float, optional): the mean for all distributions + std (Tensor): the tensor of per-element standard deviations + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(mean=0.5, std=torch.arange(1., 6.)) + tensor([-1.2793, -1.0732, -2.0687, 5.1177, -1.2303]) + + .. function:: normal(mean, std=1.0, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the standard deviations are shared among + all drawn elements. + + Args: + mean (Tensor): the tensor of per-element means + std (float, optional): the standard deviation for all distributions + + Keyword args: + out (Tensor, optional): the output tensor + + Example:: + + >>> torch.normal(mean=torch.arange(1., 6.)) + tensor([ 1.1552, 2.6148, 2.6535, 5.8318, 4.2361]) + + .. function:: normal(mean, std, size, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the means and standard deviations are shared + among all drawn elements. The resulting tensor has size given by :attr:`size`. + + Args: + mean (float): the mean for all distributions + std (float): the standard deviation for all distributions + size (int...): a sequence of integers defining the shape of the output tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(2, 3, size=(1, 4)) + tensor([[-1.3987, -1.9544, 3.6048, 0.7909]]) + """ + +@overload +def normal( + mean: _float, + std: Tensor, + *, + generator: Generator | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + normal(mean, std, *, generator=None, out=None) -> Tensor + + Returns a tensor of random numbers drawn from separate normal distributions + whose mean and standard deviation are given. + + The :attr:`mean` is a tensor with the mean of + each output element's normal distribution + + The :attr:`std` is a tensor with the standard deviation of + each output element's normal distribution + + The shapes of :attr:`mean` and :attr:`std` don't need to match, but the + total number of elements in each tensor need to be the same. + + .. note:: When the shapes do not match, the shape of :attr:`mean` + is used as the shape for the returned output tensor + + .. note:: When :attr:`std` is a CUDA tensor, this function synchronizes + its device with the CPU. + + Args: + mean (Tensor): the tensor of per-element means + std (Tensor): the tensor of per-element standard deviations + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(mean=torch.arange(1., 11.), std=torch.arange(1, 0, -0.1)) + tensor([ 1.0425, 3.5672, 2.7969, 4.2925, 4.7229, 6.2134, + 8.0505, 8.1408, 9.0563, 10.0566]) + + .. function:: normal(mean=0.0, std, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the means are shared among all drawn + elements. + + Args: + mean (float, optional): the mean for all distributions + std (Tensor): the tensor of per-element standard deviations + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(mean=0.5, std=torch.arange(1., 6.)) + tensor([-1.2793, -1.0732, -2.0687, 5.1177, -1.2303]) + + .. function:: normal(mean, std=1.0, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the standard deviations are shared among + all drawn elements. + + Args: + mean (Tensor): the tensor of per-element means + std (float, optional): the standard deviation for all distributions + + Keyword args: + out (Tensor, optional): the output tensor + + Example:: + + >>> torch.normal(mean=torch.arange(1., 6.)) + tensor([ 1.1552, 2.6148, 2.6535, 5.8318, 4.2361]) + + .. function:: normal(mean, std, size, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the means and standard deviations are shared + among all drawn elements. The resulting tensor has size given by :attr:`size`. + + Args: + mean (float): the mean for all distributions + std (float): the standard deviation for all distributions + size (int...): a sequence of integers defining the shape of the output tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(2, 3, size=(1, 4)) + tensor([[-1.3987, -1.9544, 3.6048, 0.7909]]) + """ + +@overload +def normal( + mean: _float, + std: _float, + size: Sequence[_int | SymInt], + *, + generator: Generator | None = None, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + normal(mean, std, *, generator=None, out=None) -> Tensor + + Returns a tensor of random numbers drawn from separate normal distributions + whose mean and standard deviation are given. + + The :attr:`mean` is a tensor with the mean of + each output element's normal distribution + + The :attr:`std` is a tensor with the standard deviation of + each output element's normal distribution + + The shapes of :attr:`mean` and :attr:`std` don't need to match, but the + total number of elements in each tensor need to be the same. + + .. note:: When the shapes do not match, the shape of :attr:`mean` + is used as the shape for the returned output tensor + + .. note:: When :attr:`std` is a CUDA tensor, this function synchronizes + its device with the CPU. + + Args: + mean (Tensor): the tensor of per-element means + std (Tensor): the tensor of per-element standard deviations + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(mean=torch.arange(1., 11.), std=torch.arange(1, 0, -0.1)) + tensor([ 1.0425, 3.5672, 2.7969, 4.2925, 4.7229, 6.2134, + 8.0505, 8.1408, 9.0563, 10.0566]) + + .. function:: normal(mean=0.0, std, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the means are shared among all drawn + elements. + + Args: + mean (float, optional): the mean for all distributions + std (Tensor): the tensor of per-element standard deviations + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(mean=0.5, std=torch.arange(1., 6.)) + tensor([-1.2793, -1.0732, -2.0687, 5.1177, -1.2303]) + + .. function:: normal(mean, std=1.0, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the standard deviations are shared among + all drawn elements. + + Args: + mean (Tensor): the tensor of per-element means + std (float, optional): the standard deviation for all distributions + + Keyword args: + out (Tensor, optional): the output tensor + + Example:: + + >>> torch.normal(mean=torch.arange(1., 6.)) + tensor([ 1.1552, 2.6148, 2.6535, 5.8318, 4.2361]) + + .. function:: normal(mean, std, size, *, out=None) -> Tensor + :noindex: + + Similar to the function above, but the means and standard deviations are shared + among all drawn elements. The resulting tensor has size given by :attr:`size`. + + Args: + mean (float): the mean for all distributions + std (float): the standard deviation for all distributions + size (int...): a sequence of integers defining the shape of the output tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.normal(2, 3, size=(1, 4)) + tensor([[-1.3987, -1.9544, 3.6048, 0.7909]]) + """ + +@overload +def not_equal( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + not_equal(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.ne`. + """ + +@overload +def not_equal( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + not_equal(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.ne`. + """ + +@overload +def nuclear_norm( + input: Tensor, + dim: _int | _size, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: ... +@overload +def nuclear_norm( + input: Tensor, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: ... +def numel(self: Tensor) -> _int: + r""" + numel(input: Tensor) -> int + + Returns the total number of elements in the :attr:`input` tensor. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> a = torch.randn(1, 2, 3, 4, 5) + >>> torch.numel(a) + 120 + >>> a = torch.zeros(4,4) + >>> torch.numel(a) + 16 + """ + +@overload +def ones( + size: Sequence[_int | SymInt], + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + ones(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with the scalar value `1`, with the shape defined + by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.ones(2, 3) + tensor([[ 1., 1., 1.], + [ 1., 1., 1.]]) + + >>> torch.ones(5) + tensor([ 1., 1., 1., 1., 1.]) + """ + +@overload +def ones( + *size: _int | SymInt, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + ones(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with the scalar value `1`, with the shape defined + by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.ones(2, 3) + tensor([[ 1., 1., 1.], + [ 1., 1., 1.]]) + + >>> torch.ones(5) + tensor([ 1., 1., 1., 1., 1.]) + """ + +@overload +def ones( + size: _size, + *, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + ones(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with the scalar value `1`, with the shape defined + by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.ones(2, 3) + tensor([[ 1., 1., 1.], + [ 1., 1., 1.]]) + + >>> torch.ones(5) + tensor([ 1., 1., 1., 1., 1.]) + """ + +@overload +def ones( + *size: _int, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + ones(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with the scalar value `1`, with the shape defined + by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword arguments: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.ones(2, 3) + tensor([[ 1., 1., 1.], + [ 1., 1., 1.]]) + + >>> torch.ones(5) + tensor([ 1., 1., 1., 1., 1.]) + """ + +def ones_like( + input: Tensor, + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + ones_like(input, *, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor filled with the scalar value `1`, with the same size as + :attr:`input`. ``torch.ones_like(input)`` is equivalent to + ``torch.ones(input.size(), dtype=input.dtype, layout=input.layout, device=input.device)``. + + .. warning:: + As of 0.4, this function does not support an :attr:`out` keyword. As an alternative, + the old ``torch.ones_like(input, out=output)`` is equivalent to + ``torch.ones(input.size(), out=output)``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + + Keyword arguments: + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + + Example:: + + >>> input = torch.empty(2, 3) + >>> torch.ones_like(input) + tensor([[ 1., 1., 1.], + [ 1., 1., 1.]]) + """ + +def orgqr( + input: Tensor, + input2: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + orgqr(input, tau) -> Tensor + + Alias for :func:`torch.linalg.householder_product`. + """ + +def ormqr( + input: Tensor, + input2: Tensor, + input3: Tensor, + left: _bool = True, + transpose: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + ormqr(input, tau, other, left=True, transpose=False, *, out=None) -> Tensor + + Computes the matrix-matrix multiplication of a product of Householder matrices with a general matrix. + + Multiplies a :math:`m \times n` matrix `C` (given by :attr:`other`) with a matrix `Q`, + where `Q` is represented using Householder reflectors `(input, tau)`. + See `Representation of Orthogonal or Unitary Matrices`_ for further details. + + If :attr:`left` is `True` then `op(Q)` times `C` is computed, otherwise the result is `C` times `op(Q)`. + When :attr:`left` is `True`, the implicit matrix `Q` has size :math:`m \times m`. + It has size :math:`n \times n` otherwise. + If :attr:`transpose` is `True` then `op` is the conjugate transpose operation, otherwise it's a no-op. + + Supports inputs of float, double, cfloat and cdouble dtypes. + Also supports batched inputs, and, if the input is batched, the output is batched with the same dimensions. + + .. seealso:: + :func:`torch.geqrf` can be used to form the Householder representation `(input, tau)` of matrix `Q` + from the QR decomposition. + + .. note:: + This function supports backward but it is only fast when ``(input, tau)`` do not require gradients + and/or ``tau.size(-1)`` is very small. + `` + + Args: + input (Tensor): tensor of shape `(*, mn, k)` where `*` is zero or more batch dimensions + and `mn` equals to `m` or `n` depending on the :attr:`left`. + tau (Tensor): tensor of shape `(*, min(mn, k))` where `*` is zero or more batch dimensions. + other (Tensor): tensor of shape `(*, m, n)` where `*` is zero or more batch dimensions. + left (bool): controls the order of multiplication. + transpose (bool): controls whether the matrix `Q` is conjugate transposed or not. + + Keyword args: + out (Tensor, optional): the output Tensor. Ignored if `None`. Default: `None`. + + .. _Representation of Orthogonal or Unitary Matrices: + https://www.netlib.org/lapack/lug/node128.html + """ + +def outer( + input: Tensor, + vec2: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + outer(input, vec2, *, out=None) -> Tensor + + Outer product of :attr:`input` and :attr:`vec2`. + If :attr:`input` is a vector of size :math:`n` and :attr:`vec2` is a vector of + size :math:`m`, then :attr:`out` must be a matrix of size :math:`(n \times m)`. + + .. note:: This function does not :ref:`broadcast `. + + Args: + input (Tensor): 1-D input vector + vec2 (Tensor): 1-D input vector + + Keyword args: + out (Tensor, optional): optional output matrix + + Example:: + + >>> v1 = torch.arange(1., 5.) + >>> v2 = torch.arange(1., 4.) + >>> torch.outer(v1, v2) + tensor([[ 1., 2., 3.], + [ 2., 4., 6.], + [ 3., 6., 9.], + [ 4., 8., 12.]]) + """ + +def pairwise_distance( + x1: Tensor, + x2: Tensor, + p: _float = 2, + eps: _float = 1e-06, + keepdim: _bool = False, +) -> Tensor: ... +def pdist(input: Tensor, p: _float = 2) -> Tensor: ... +def permute(input: Tensor, dims: _size) -> Tensor: + r""" + permute(input, dims) -> Tensor + + Returns a view of the original tensor :attr:`input` with its dimensions permuted. + + Args: + input (Tensor): the input tensor. + dims (torch.Size, tuple of int or list of int): the desired ordering of dimensions. + + Example: + >>> x = torch.randn(2, 3, 5) + >>> x.size() + torch.Size([2, 3, 5]) + >>> torch.permute(x, (2, 0, 1)).size() + torch.Size([5, 2, 3]) + """ + +def permute_copy( + input: Tensor, + dims: _size, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.permute`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def pinverse(input: Tensor, rcond: _float = 1e-15) -> Tensor: + r""" + pinverse(input, rcond=1e-15) -> Tensor + + Alias for :func:`torch.linalg.pinv` + """ + +def pixel_shuffle(input: Tensor, upscale_factor: _int) -> Tensor: ... +def pixel_unshuffle(input: Tensor, downscale_factor: _int) -> Tensor: ... +def poisson(input: Tensor, generator: Generator | None = None) -> Tensor: + r""" + poisson(input, generator=None) -> Tensor + + Returns a tensor of the same size as :attr:`input` with each element + sampled from a Poisson distribution with rate parameter given by the corresponding + element in :attr:`input` i.e., + + .. math:: + \text{out}_i \sim \text{Poisson}(\text{input}_i) + + :attr:`input` must be non-negative. + + Args: + input (Tensor): the input tensor containing the rates of the Poisson distribution + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + + Example:: + + >>> rates = torch.rand(4, 4) * 5 # rate parameter between 0 and 5 + >>> torch.poisson(rates) + tensor([[9., 1., 3., 5.], + [8., 6., 6., 0.], + [0., 4., 5., 3.], + [2., 1., 4., 2.]]) + """ + +def poisson_nll_loss( + input: Tensor, + target: Tensor, + log_input: _bool, + full: _bool, + eps: _float, + reduction: _int, +) -> Tensor: ... +def polar( + abs: Tensor, + angle: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + polar(abs, angle, *, out=None) -> Tensor + + Constructs a complex tensor whose elements are Cartesian coordinates + corresponding to the polar coordinates with absolute value :attr:`abs` and angle + :attr:`angle`. + + .. math:: + \text{out} = \text{abs} \cdot \cos(\text{angle}) + \text{abs} \cdot \sin(\text{angle}) \cdot j + + .. note:: + `torch.polar` is similar to + `std::polar `_ + and does not compute the polar decomposition + of a complex tensor like Python's `cmath.polar` and SciPy's `linalg.polar` do. + The behavior of this function is undefined if `abs` is negative or NaN, or if `angle` is + infinite. + + + Args: + abs (Tensor): The absolute value the complex tensor. Must be float or double. + angle (Tensor): The angle of the complex tensor. Must be same dtype as + :attr:`abs`. + + Keyword args: + out (Tensor): If the inputs are ``torch.float32``, must be + ``torch.complex64``. If the inputs are ``torch.float64``, must be + ``torch.complex128``. + + Example:: + + >>> import numpy as np + >>> abs = torch.tensor([1, 2], dtype=torch.float64) + >>> angle = torch.tensor([np.pi / 2, 5 * np.pi / 4], dtype=torch.float64) + >>> z = torch.polar(abs, angle) + >>> z + tensor([(0.0000+1.0000j), (-1.4142-1.4142j)], dtype=torch.complex128) + """ + +def polygamma( + n: _int, + input: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + polygamma(n, input, *, out=None) -> Tensor + + Alias for :func:`torch.special.polygamma`. + """ + +def positive(input: Tensor) -> Tensor: + r""" + positive(input) -> Tensor + + Returns :attr:`input`. + Throws a runtime error if :attr:`input` is a bool tensor. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> t = torch.randn(5) + >>> t + tensor([ 0.0090, -0.2262, -0.0682, -0.2866, 0.3940]) + >>> torch.positive(t) + tensor([ 0.0090, -0.2262, -0.0682, -0.2866, 0.3940]) + """ + +@overload +def pow( + input: Tensor, + exponent: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + pow(input, exponent, *, out=None) -> Tensor + + Takes the power of each element in :attr:`input` with :attr:`exponent` and + returns a tensor with the result. + + :attr:`exponent` can be either a single ``float`` number or a `Tensor` + with the same number of elements as :attr:`input`. + + When :attr:`exponent` is a scalar value, the operation applied is: + + .. math:: + \text{out}_i = x_i ^ \text{exponent} + + When :attr:`exponent` is a tensor, the operation applied is: + + .. math:: + \text{out}_i = x_i ^ {\text{exponent}_i} + + When :attr:`exponent` is a tensor, the shapes of :attr:`input` + and :attr:`exponent` must be :ref:`broadcastable `. + + Args: + input (Tensor): the input tensor. + exponent (float or tensor): the exponent value + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.4331, 1.2475, 0.6834, -0.2791]) + >>> torch.pow(a, 2) + tensor([ 0.1875, 1.5561, 0.4670, 0.0779]) + >>> exp = torch.arange(1., 5.) + + >>> a = torch.arange(1., 5.) + >>> a + tensor([ 1., 2., 3., 4.]) + >>> exp + tensor([ 1., 2., 3., 4.]) + >>> torch.pow(a, exp) + tensor([ 1., 4., 27., 256.]) + + .. function:: pow(self, exponent, *, out=None) -> Tensor + :noindex: + + :attr:`self` is a scalar ``float`` value, and :attr:`exponent` is a tensor. + The returned tensor :attr:`out` is of the same shape as :attr:`exponent` + + The operation applied is: + + .. math:: + \text{out}_i = \text{self} ^ {\text{exponent}_i} + + Args: + self (float): the scalar base value for the power operation + exponent (Tensor): the exponent tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> exp = torch.arange(1., 5.) + >>> base = 2 + >>> torch.pow(base, exp) + tensor([ 2., 4., 8., 16.]) + """ + +@overload +def pow( + self: Number | _complex, + exponent: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + pow(input, exponent, *, out=None) -> Tensor + + Takes the power of each element in :attr:`input` with :attr:`exponent` and + returns a tensor with the result. + + :attr:`exponent` can be either a single ``float`` number or a `Tensor` + with the same number of elements as :attr:`input`. + + When :attr:`exponent` is a scalar value, the operation applied is: + + .. math:: + \text{out}_i = x_i ^ \text{exponent} + + When :attr:`exponent` is a tensor, the operation applied is: + + .. math:: + \text{out}_i = x_i ^ {\text{exponent}_i} + + When :attr:`exponent` is a tensor, the shapes of :attr:`input` + and :attr:`exponent` must be :ref:`broadcastable `. + + Args: + input (Tensor): the input tensor. + exponent (float or tensor): the exponent value + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.4331, 1.2475, 0.6834, -0.2791]) + >>> torch.pow(a, 2) + tensor([ 0.1875, 1.5561, 0.4670, 0.0779]) + >>> exp = torch.arange(1., 5.) + + >>> a = torch.arange(1., 5.) + >>> a + tensor([ 1., 2., 3., 4.]) + >>> exp + tensor([ 1., 2., 3., 4.]) + >>> torch.pow(a, exp) + tensor([ 1., 4., 27., 256.]) + + .. function:: pow(self, exponent, *, out=None) -> Tensor + :noindex: + + :attr:`self` is a scalar ``float`` value, and :attr:`exponent` is a tensor. + The returned tensor :attr:`out` is of the same shape as :attr:`exponent` + + The operation applied is: + + .. math:: + \text{out}_i = \text{self} ^ {\text{exponent}_i} + + Args: + self (float): the scalar base value for the power operation + exponent (Tensor): the exponent tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> exp = torch.arange(1., 5.) + >>> base = 2 + >>> torch.pow(base, exp) + tensor([ 2., 4., 8., 16.]) + """ + +@overload +def pow( + input: Tensor, + exponent: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + pow(input, exponent, *, out=None) -> Tensor + + Takes the power of each element in :attr:`input` with :attr:`exponent` and + returns a tensor with the result. + + :attr:`exponent` can be either a single ``float`` number or a `Tensor` + with the same number of elements as :attr:`input`. + + When :attr:`exponent` is a scalar value, the operation applied is: + + .. math:: + \text{out}_i = x_i ^ \text{exponent} + + When :attr:`exponent` is a tensor, the operation applied is: + + .. math:: + \text{out}_i = x_i ^ {\text{exponent}_i} + + When :attr:`exponent` is a tensor, the shapes of :attr:`input` + and :attr:`exponent` must be :ref:`broadcastable `. + + Args: + input (Tensor): the input tensor. + exponent (float or tensor): the exponent value + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.4331, 1.2475, 0.6834, -0.2791]) + >>> torch.pow(a, 2) + tensor([ 0.1875, 1.5561, 0.4670, 0.0779]) + >>> exp = torch.arange(1., 5.) + + >>> a = torch.arange(1., 5.) + >>> a + tensor([ 1., 2., 3., 4.]) + >>> exp + tensor([ 1., 2., 3., 4.]) + >>> torch.pow(a, exp) + tensor([ 1., 4., 27., 256.]) + + .. function:: pow(self, exponent, *, out=None) -> Tensor + :noindex: + + :attr:`self` is a scalar ``float`` value, and :attr:`exponent` is a tensor. + The returned tensor :attr:`out` is of the same shape as :attr:`exponent` + + The operation applied is: + + .. math:: + \text{out}_i = \text{self} ^ {\text{exponent}_i} + + Args: + self (float): the scalar base value for the power operation + exponent (Tensor): the exponent tensor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> exp = torch.arange(1., 5.) + >>> base = 2 + >>> torch.pow(base, exp) + tensor([ 2., 4., 8., 16.]) + """ + +def prelu(input: Tensor, weight: Tensor) -> Tensor: ... +@overload +def prod(input: Tensor, *, dtype: _dtype | None = None) -> Tensor: + r""" + prod(input: Tensor, *, dtype: Optional[_dtype]) -> Tensor + + Returns the product of all elements in the :attr:`input` tensor. + + Args: + input (Tensor): the input tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[-0.8020, 0.5428, -1.5854]]) + >>> torch.prod(a) + tensor(0.6902) + + .. function:: prod(input, dim, keepdim=False, *, dtype=None) -> Tensor + :noindex: + + Returns the product of each row of the :attr:`input` tensor in the given + dimension :attr:`dim`. + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in + the output tensor having 1 fewer dimension than :attr:`input`. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(4, 2) + >>> a + tensor([[ 0.5261, -0.3837], + [ 1.1857, -0.2498], + [-1.1646, 0.0705], + [ 1.1131, -1.0629]]) + >>> torch.prod(a, 1) + tensor([-0.2018, -0.2962, -0.0821, -1.1831]) + """ + +@overload +def prod( + input: Tensor, + dim: _int, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + prod(input: Tensor, *, dtype: Optional[_dtype]) -> Tensor + + Returns the product of all elements in the :attr:`input` tensor. + + Args: + input (Tensor): the input tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[-0.8020, 0.5428, -1.5854]]) + >>> torch.prod(a) + tensor(0.6902) + + .. function:: prod(input, dim, keepdim=False, *, dtype=None) -> Tensor + :noindex: + + Returns the product of each row of the :attr:`input` tensor in the given + dimension :attr:`dim`. + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in + the output tensor having 1 fewer dimension than :attr:`input`. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(4, 2) + >>> a + tensor([[ 0.5261, -0.3837], + [ 1.1857, -0.2498], + [-1.1646, 0.0705], + [ 1.1131, -1.0629]]) + >>> torch.prod(a, 1) + tensor([-0.2018, -0.2962, -0.0821, -1.1831]) + """ + +@overload +def prod( + input: Tensor, + dim: str | EllipsisType | None, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + prod(input: Tensor, *, dtype: Optional[_dtype]) -> Tensor + + Returns the product of all elements in the :attr:`input` tensor. + + Args: + input (Tensor): the input tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[-0.8020, 0.5428, -1.5854]]) + >>> torch.prod(a) + tensor(0.6902) + + .. function:: prod(input, dim, keepdim=False, *, dtype=None) -> Tensor + :noindex: + + Returns the product of each row of the :attr:`input` tensor in the given + dimension :attr:`dim`. + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in + the output tensor having 1 fewer dimension than :attr:`input`. + + Args: + input (Tensor): the input tensor. + + dim (int, optional): the dimension to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(4, 2) + >>> a + tensor([[ 0.5261, -0.3837], + [ 1.1857, -0.2498], + [-1.1646, 0.0705], + [ 1.1131, -1.0629]]) + >>> torch.prod(a, 1) + tensor([-0.2018, -0.2962, -0.0821, -1.1831]) + """ + +def promote_types(type1: _dtype, type2: _dtype) -> _dtype: + r""" + promote_types(type1, type2) -> dtype + + Returns the :class:`torch.dtype` with the smallest size and scalar kind that is + not smaller nor of lower kind than either `type1` or `type2`. See type promotion + :ref:`documentation ` for more information on the type + promotion logic. + + Args: + type1 (:class:`torch.dtype`) + type2 (:class:`torch.dtype`) + + Example:: + + >>> torch.promote_types(torch.int32, torch.float32) + torch.float32 + >>> torch.promote_types(torch.uint8, torch.long) + torch.long + """ + +def put( + input: Tensor, + index: Tensor, + source: Tensor, + accumulate: _bool = False, +) -> Tensor: ... +def q_per_channel_axis(input: Tensor) -> _int: ... +def q_per_channel_scales(input: Tensor) -> Tensor: ... +def q_per_channel_zero_points(input: Tensor) -> Tensor: ... +def q_scale(input: Tensor) -> _float: ... +def q_zero_point(input: Tensor) -> _int: ... +def qr( + input: Tensor, + some: _bool = True, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.qr: + r""" + qr(input: Tensor, some: bool = True, *, out: Union[Tensor, Tuple[Tensor, ...], List[Tensor], None]) -> (Tensor, Tensor) + + Computes the QR decomposition of a matrix or a batch of matrices :attr:`input`, + and returns a namedtuple (Q, R) of tensors such that :math:`\text{input} = Q R` + with :math:`Q` being an orthogonal matrix or batch of orthogonal matrices and + :math:`R` being an upper triangular matrix or batch of upper triangular matrices. + + If :attr:`some` is ``True``, then this function returns the thin (reduced) QR factorization. + Otherwise, if :attr:`some` is ``False``, this function returns the complete QR factorization. + + .. warning:: + + :func:`torch.qr` is deprecated in favor of :func:`torch.linalg.qr` + and will be removed in a future PyTorch release. The boolean parameter :attr:`some` has been + replaced with a string parameter :attr:`mode`. + + ``Q, R = torch.qr(A)`` should be replaced with + + .. code:: python + + Q, R = torch.linalg.qr(A) + + ``Q, R = torch.qr(A, some=False)`` should be replaced with + + .. code:: python + + Q, R = torch.linalg.qr(A, mode="complete") + + .. warning:: + If you plan to backpropagate through QR, note that the current backward implementation + is only well-defined when the first :math:`\min(input.size(-1), input.size(-2))` + columns of :attr:`input` are linearly independent. + This behavior will probably change once QR supports pivoting. + + .. note:: This function uses LAPACK for CPU inputs and MAGMA for CUDA inputs, + and may produce different (valid) decompositions on different device types + or different platforms. + + Args: + input (Tensor): the input tensor of size :math:`(*, m, n)` where `*` is zero or more + batch dimensions consisting of matrices of dimension :math:`m \times n`. + some (bool, optional): Set to ``True`` for reduced QR decomposition and ``False`` for + complete QR decomposition. If `k = min(m, n)` then: + + * ``some=True`` : returns `(Q, R)` with dimensions (m, k), (k, n) (default) + + * ``'some=False'``: returns `(Q, R)` with dimensions (m, m), (m, n) + + Keyword args: + out (tuple, optional): tuple of `Q` and `R` tensors. + The dimensions of `Q` and `R` are detailed in the description of :attr:`some` above. + + Example:: + + >>> a = torch.tensor([[12., -51, 4], [6, 167, -68], [-4, 24, -41]]) + >>> q, r = torch.qr(a) + >>> q + tensor([[-0.8571, 0.3943, 0.3314], + [-0.4286, -0.9029, -0.0343], + [ 0.2857, -0.1714, 0.9429]]) + >>> r + tensor([[ -14.0000, -21.0000, 14.0000], + [ 0.0000, -175.0000, 70.0000], + [ 0.0000, 0.0000, -35.0000]]) + >>> torch.mm(q, r).round() + tensor([[ 12., -51., 4.], + [ 6., 167., -68.], + [ -4., 24., -41.]]) + >>> torch.mm(q.t(), q).round() + tensor([[ 1., 0., 0.], + [ 0., 1., -0.], + [ 0., -0., 1.]]) + >>> a = torch.randn(3, 4, 5) + >>> q, r = torch.qr(a, some=False) + >>> torch.allclose(torch.matmul(q, r), a) + True + >>> torch.allclose(torch.matmul(q.mT, q), torch.eye(5)) + True + """ + +@overload +def quantile( + input: Tensor, + q: Tensor, + dim: _int | None = None, + keepdim: _bool = False, + *, + interpolation: str = "linear", + out: Tensor | None = None, +) -> Tensor: + r""" + quantile(input, q, dim=None, keepdim=False, *, interpolation='linear', out=None) -> Tensor + + Computes the q-th quantiles of each row of the :attr:`input` tensor along the dimension :attr:`dim`. + + To compute the quantile, we map q in [0, 1] to the range of indices [0, n] to find the location + of the quantile in the sorted input. If the quantile lies between two data points ``a < b`` with + indices ``i`` and ``j`` in the sorted order, result is computed according to the given + :attr:`interpolation` method as follows: + + - ``linear``: ``a + (b - a) * fraction``, where ``fraction`` is the fractional part of the computed quantile index. + - ``lower``: ``a``. + - ``higher``: ``b``. + - ``nearest``: ``a`` or ``b``, whichever's index is closer to the computed quantile index (follows :func:`torch.round`). + - ``midpoint``: ``(a + b) / 2``. + + If :attr:`q` is a 1D tensor, the first dimension of the output represents the quantiles and has size + equal to the size of :attr:`q`, the remaining dimensions are what remains from the reduction. + + .. note:: + By default :attr:`dim` is ``None`` resulting in the :attr:`input` tensor being flattened before computation. + + Args: + input (Tensor): the input tensor. + q (float or Tensor): a scalar or 1D tensor of values in the range [0, 1]. + + dim (int, optional): the dimension to reduce. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword arguments: + interpolation (str, optional): interpolation method to use when the desired quantile lies between two data points. + Can be ``linear``, ``lower``, ``higher``, ``midpoint`` and ``nearest``. + Default is ``linear``. + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(2, 3) + >>> a + tensor([[ 0.0795, -1.2117, 0.9765], + [ 1.1707, 0.6706, 0.4884]]) + >>> q = torch.tensor([0.25, 0.5, 0.75]) + >>> torch.quantile(a, q, dim=1, keepdim=True) + tensor([[[-0.5661], + [ 0.5795]], + + [[ 0.0795], + [ 0.6706]], + + [[ 0.5280], + [ 0.9206]]]) + >>> torch.quantile(a, q, dim=1, keepdim=True).shape + torch.Size([3, 2, 1]) + >>> a = torch.arange(4.) + >>> a + tensor([0., 1., 2., 3.]) + >>> torch.quantile(a, 0.6, interpolation='linear') + tensor(1.8000) + >>> torch.quantile(a, 0.6, interpolation='lower') + tensor(1.) + >>> torch.quantile(a, 0.6, interpolation='higher') + tensor(2.) + >>> torch.quantile(a, 0.6, interpolation='midpoint') + tensor(1.5000) + >>> torch.quantile(a, 0.6, interpolation='nearest') + tensor(2.) + >>> torch.quantile(a, 0.4, interpolation='nearest') + tensor(1.) + """ + +@overload +def quantile( + input: Tensor, + q: _float, + dim: _int | None = None, + keepdim: _bool = False, + *, + interpolation: str = "linear", + out: Tensor | None = None, +) -> Tensor: + r""" + quantile(input, q, dim=None, keepdim=False, *, interpolation='linear', out=None) -> Tensor + + Computes the q-th quantiles of each row of the :attr:`input` tensor along the dimension :attr:`dim`. + + To compute the quantile, we map q in [0, 1] to the range of indices [0, n] to find the location + of the quantile in the sorted input. If the quantile lies between two data points ``a < b`` with + indices ``i`` and ``j`` in the sorted order, result is computed according to the given + :attr:`interpolation` method as follows: + + - ``linear``: ``a + (b - a) * fraction``, where ``fraction`` is the fractional part of the computed quantile index. + - ``lower``: ``a``. + - ``higher``: ``b``. + - ``nearest``: ``a`` or ``b``, whichever's index is closer to the computed quantile index (follows :func:`torch.round`). + - ``midpoint``: ``(a + b) / 2``. + + If :attr:`q` is a 1D tensor, the first dimension of the output represents the quantiles and has size + equal to the size of :attr:`q`, the remaining dimensions are what remains from the reduction. + + .. note:: + By default :attr:`dim` is ``None`` resulting in the :attr:`input` tensor being flattened before computation. + + Args: + input (Tensor): the input tensor. + q (float or Tensor): a scalar or 1D tensor of values in the range [0, 1]. + + dim (int, optional): the dimension to reduce. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword arguments: + interpolation (str, optional): interpolation method to use when the desired quantile lies between two data points. + Can be ``linear``, ``lower``, ``higher``, ``midpoint`` and ``nearest``. + Default is ``linear``. + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(2, 3) + >>> a + tensor([[ 0.0795, -1.2117, 0.9765], + [ 1.1707, 0.6706, 0.4884]]) + >>> q = torch.tensor([0.25, 0.5, 0.75]) + >>> torch.quantile(a, q, dim=1, keepdim=True) + tensor([[[-0.5661], + [ 0.5795]], + + [[ 0.0795], + [ 0.6706]], + + [[ 0.5280], + [ 0.9206]]]) + >>> torch.quantile(a, q, dim=1, keepdim=True).shape + torch.Size([3, 2, 1]) + >>> a = torch.arange(4.) + >>> a + tensor([0., 1., 2., 3.]) + >>> torch.quantile(a, 0.6, interpolation='linear') + tensor(1.8000) + >>> torch.quantile(a, 0.6, interpolation='lower') + tensor(1.) + >>> torch.quantile(a, 0.6, interpolation='higher') + tensor(2.) + >>> torch.quantile(a, 0.6, interpolation='midpoint') + tensor(1.5000) + >>> torch.quantile(a, 0.6, interpolation='nearest') + tensor(2.) + >>> torch.quantile(a, 0.4, interpolation='nearest') + tensor(1.) + """ + +def quantize_per_channel( + input: Tensor, + scales: Tensor, + zero_points: Tensor, + axis: _int, + dtype: _dtype, +) -> Tensor: + r""" + quantize_per_channel(input, scales, zero_points, axis, dtype) -> Tensor + + Converts a float tensor to a per-channel quantized tensor with given scales and zero points. + + Arguments: + input (Tensor): float tensor to quantize + scales (Tensor): float 1D tensor of scales to use, size should match ``input.size(axis)`` + zero_points (int): integer 1D tensor of offset to use, size should match ``input.size(axis)`` + axis (int): dimension on which apply per-channel quantization + dtype (:class:`torch.dtype`): the desired data type of returned tensor. + Has to be one of the quantized dtypes: ``torch.quint8``, ``torch.qint8``, ``torch.qint32`` + + Returns: + Tensor: A newly quantized tensor + + Example:: + + >>> x = torch.tensor([[-1.0, 0.0], [1.0, 2.0]]) + >>> torch.quantize_per_channel(x, torch.tensor([0.1, 0.01]), torch.tensor([10, 0]), 0, torch.quint8) + tensor([[-1., 0.], + [ 1., 2.]], size=(2, 2), dtype=torch.quint8, + quantization_scheme=torch.per_channel_affine, + scale=tensor([0.1000, 0.0100], dtype=torch.float64), + zero_point=tensor([10, 0]), axis=0) + >>> torch.quantize_per_channel(x, torch.tensor([0.1, 0.01]), torch.tensor([10, 0]), 0, torch.quint8).int_repr() + tensor([[ 0, 10], + [100, 200]], dtype=torch.uint8) + """ + +@overload +def quantize_per_tensor( + input: Tensor, + scale: Tensor, + zero_point: Tensor, + dtype: _dtype, +) -> Tensor: + r""" + quantize_per_tensor(input, scale, zero_point, dtype) -> Tensor + + Converts a float tensor to a quantized tensor with given scale and zero point. + + Arguments: + input (Tensor): float tensor or list of tensors to quantize + scale (float or Tensor): scale to apply in quantization formula + zero_point (int or Tensor): offset in integer value that maps to float zero + dtype (:class:`torch.dtype`): the desired data type of returned tensor. + Has to be one of the quantized dtypes: ``torch.quint8``, ``torch.qint8``, ``torch.qint32`` + + Returns: + Tensor: A newly quantized tensor or list of quantized tensors. + + Example:: + + >>> torch.quantize_per_tensor(torch.tensor([-1.0, 0.0, 1.0, 2.0]), 0.1, 10, torch.quint8) + tensor([-1., 0., 1., 2.], size=(4,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.1, zero_point=10) + >>> torch.quantize_per_tensor(torch.tensor([-1.0, 0.0, 1.0, 2.0]), 0.1, 10, torch.quint8).int_repr() + tensor([ 0, 10, 20, 30], dtype=torch.uint8) + >>> torch.quantize_per_tensor([torch.tensor([-1.0, 0.0]), torch.tensor([-2.0, 2.0])], + >>> torch.tensor([0.1, 0.2]), torch.tensor([10, 20]), torch.quint8) + (tensor([-1., 0.], size=(2,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.1, zero_point=10), + tensor([-2., 2.], size=(2,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.2, zero_point=20)) + >>> torch.quantize_per_tensor(torch.tensor([-1.0, 0.0, 1.0, 2.0]), torch.tensor(0.1), torch.tensor(10), torch.quint8) + tensor([-1., 0., 1., 2.], size=(4,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.10, zero_point=10) + """ + +@overload +def quantize_per_tensor( + input: Tensor, + scale: _float, + zero_point: _int, + dtype: _dtype, +) -> Tensor: + r""" + quantize_per_tensor(input, scale, zero_point, dtype) -> Tensor + + Converts a float tensor to a quantized tensor with given scale and zero point. + + Arguments: + input (Tensor): float tensor or list of tensors to quantize + scale (float or Tensor): scale to apply in quantization formula + zero_point (int or Tensor): offset in integer value that maps to float zero + dtype (:class:`torch.dtype`): the desired data type of returned tensor. + Has to be one of the quantized dtypes: ``torch.quint8``, ``torch.qint8``, ``torch.qint32`` + + Returns: + Tensor: A newly quantized tensor or list of quantized tensors. + + Example:: + + >>> torch.quantize_per_tensor(torch.tensor([-1.0, 0.0, 1.0, 2.0]), 0.1, 10, torch.quint8) + tensor([-1., 0., 1., 2.], size=(4,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.1, zero_point=10) + >>> torch.quantize_per_tensor(torch.tensor([-1.0, 0.0, 1.0, 2.0]), 0.1, 10, torch.quint8).int_repr() + tensor([ 0, 10, 20, 30], dtype=torch.uint8) + >>> torch.quantize_per_tensor([torch.tensor([-1.0, 0.0]), torch.tensor([-2.0, 2.0])], + >>> torch.tensor([0.1, 0.2]), torch.tensor([10, 20]), torch.quint8) + (tensor([-1., 0.], size=(2,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.1, zero_point=10), + tensor([-2., 2.], size=(2,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.2, zero_point=20)) + >>> torch.quantize_per_tensor(torch.tensor([-1.0, 0.0, 1.0, 2.0]), torch.tensor(0.1), torch.tensor(10), torch.quint8) + tensor([-1., 0., 1., 2.], size=(4,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.10, zero_point=10) + """ + +@overload +def quantize_per_tensor( + tensors: tuple[Tensor, ...] | list[Tensor], + scales: Tensor, + zero_points: Tensor, + dtype: _dtype, +) -> tuple[Tensor, ...]: + r""" + quantize_per_tensor(input, scale, zero_point, dtype) -> Tensor + + Converts a float tensor to a quantized tensor with given scale and zero point. + + Arguments: + input (Tensor): float tensor or list of tensors to quantize + scale (float or Tensor): scale to apply in quantization formula + zero_point (int or Tensor): offset in integer value that maps to float zero + dtype (:class:`torch.dtype`): the desired data type of returned tensor. + Has to be one of the quantized dtypes: ``torch.quint8``, ``torch.qint8``, ``torch.qint32`` + + Returns: + Tensor: A newly quantized tensor or list of quantized tensors. + + Example:: + + >>> torch.quantize_per_tensor(torch.tensor([-1.0, 0.0, 1.0, 2.0]), 0.1, 10, torch.quint8) + tensor([-1., 0., 1., 2.], size=(4,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.1, zero_point=10) + >>> torch.quantize_per_tensor(torch.tensor([-1.0, 0.0, 1.0, 2.0]), 0.1, 10, torch.quint8).int_repr() + tensor([ 0, 10, 20, 30], dtype=torch.uint8) + >>> torch.quantize_per_tensor([torch.tensor([-1.0, 0.0]), torch.tensor([-2.0, 2.0])], + >>> torch.tensor([0.1, 0.2]), torch.tensor([10, 20]), torch.quint8) + (tensor([-1., 0.], size=(2,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.1, zero_point=10), + tensor([-2., 2.], size=(2,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.2, zero_point=20)) + >>> torch.quantize_per_tensor(torch.tensor([-1.0, 0.0, 1.0, 2.0]), torch.tensor(0.1), torch.tensor(10), torch.quint8) + tensor([-1., 0., 1., 2.], size=(4,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.10, zero_point=10) + """ + +def quantize_per_tensor_dynamic( + input: Tensor, + dtype: _dtype, + reduce_range: _bool, +) -> Tensor: + r""" + quantize_per_tensor_dynamic(input, dtype, reduce_range) -> Tensor + + Converts a float tensor to a quantized tensor with scale and zero_point calculated + dynamically based on the input. + + Arguments: + input (Tensor): float tensor or list of tensors to quantize + dtype (:class:`torch.dtype`): the desired data type of returned tensor. + Has to be one of the quantized dtypes: ``torch.quint8``, ``torch.qint8`` + reduce_range (bool): a flag to indicate whether to reduce the range of quantized + data by 1 bit, it's required to avoid instruction overflow for some hardwares + + Returns: + Tensor: A newly (dynamically) quantized tensor + + Example:: + + >>> t = torch.quantize_per_tensor_dynamic(torch.tensor([-1.0, 0.0, 1.0, 2.0]), torch.quint8, False) + >>> print(t) + tensor([-1., 0., 1., 2.], size=(4,), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.011764705882352941, + zero_point=85) + >>> t.int_repr() + tensor([ 0, 85, 170, 255], dtype=torch.uint8) + """ + +def quantized_batch_norm( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + mean: Tensor, + var: Tensor, + eps: _float, + output_scale: _float, + output_zero_point: _int, +) -> Tensor: + r""" + quantized_batch_norm(input, weight=None, bias=None, mean, var, eps, output_scale, output_zero_point) -> Tensor + + Applies batch normalization on a 4D (NCHW) quantized tensor. + + .. math:: + + y = \frac{x - \mathrm{E}[x]}{\sqrt{\mathrm{Var}[x] + \epsilon}} * \gamma + \beta + + Arguments: + input (Tensor): quantized tensor + weight (Tensor): float tensor that corresponds to the gamma, size C + bias (Tensor): float tensor that corresponds to the beta, size C + mean (Tensor): float mean value in batch normalization, size C + var (Tensor): float tensor for variance, size C + eps (float): a value added to the denominator for numerical stability. + output_scale (float): output quantized tensor scale + output_zero_point (int): output quantized tensor zero_point + + Returns: + Tensor: A quantized tensor with batch normalization applied. + + Example:: + + >>> qx = torch.quantize_per_tensor(torch.rand(2, 2, 2, 2), 1.5, 3, torch.quint8) + >>> torch.quantized_batch_norm(qx, torch.ones(2), torch.zeros(2), torch.rand(2), torch.rand(2), 0.00001, 0.2, 2) + tensor([[[[-0.2000, -0.2000], + [ 1.6000, -0.2000]], + + [[-0.4000, -0.4000], + [-0.4000, 0.6000]]], + + + [[[-0.2000, -0.2000], + [-0.2000, -0.2000]], + + [[ 0.6000, -0.4000], + [ 0.6000, -0.4000]]]], size=(2, 2, 2, 2), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=0.2, zero_point=2) + """ + +def quantized_gru_cell( + input: Tensor, + hx: Tensor, + w_ih: Tensor, + w_hh: Tensor, + b_ih: Tensor, + b_hh: Tensor, + packed_ih: Tensor, + packed_hh: Tensor, + col_offsets_ih: Tensor, + col_offsets_hh: Tensor, + scale_ih: Number | _complex, + scale_hh: Number | _complex, + zero_point_ih: Number | _complex, + zero_point_hh: Number | _complex, +) -> Tensor: ... +def quantized_lstm_cell( + input: Tensor, + hx: tuple[Tensor, ...] | list[Tensor], + w_ih: Tensor, + w_hh: Tensor, + b_ih: Tensor, + b_hh: Tensor, + packed_ih: Tensor, + packed_hh: Tensor, + col_offsets_ih: Tensor, + col_offsets_hh: Tensor, + scale_ih: Number | _complex, + scale_hh: Number | _complex, + zero_point_ih: Number | _complex, + zero_point_hh: Number | _complex, +) -> tuple[Tensor, Tensor]: ... +def quantized_max_pool1d( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size = (), + padding: _int | _size = 0, + dilation: _int | _size = 1, + ceil_mode: _bool = False, +) -> Tensor: + r""" + quantized_max_pool1d(input, kernel_size, stride=[], padding=0, dilation=1, ceil_mode=False) -> Tensor + + Applies a 1D max pooling over an input quantized tensor composed of several input planes. + + Arguments: + input (Tensor): quantized tensor + kernel_size (list of int): the size of the sliding window + stride (``list of int``, optional): the stride of the sliding window + padding (``list of int``, optional): padding to be added on both sides, must be >= 0 and <= kernel_size / 2 + dilation (``list of int``, optional): The stride between elements within a sliding window, must be > 0. Default 1 + ceil_mode (bool, optional): If True, will use ceil instead of floor to compute the output shape. + Defaults to False. + + + Returns: + Tensor: A quantized tensor with max_pool1d applied. + + Example:: + + >>> qx = torch.quantize_per_tensor(torch.rand(2, 2), 1.5, 3, torch.quint8) + >>> torch.quantized_max_pool1d(qx, [2]) + tensor([[0.0000], + [1.5000]], size=(2, 1), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=1.5, zero_point=3) + """ + +def quantized_max_pool2d( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size = (), + padding: _int | _size = 0, + dilation: _int | _size = 1, + ceil_mode: _bool = False, +) -> Tensor: + r""" + quantized_max_pool2d(input, kernel_size, stride=[], padding=0, dilation=1, ceil_mode=False) -> Tensor + + Applies a 2D max pooling over an input quantized tensor composed of several input planes. + + Arguments: + input (Tensor): quantized tensor + kernel_size (``list of int``): the size of the sliding window + stride (``list of int``, optional): the stride of the sliding window + padding (``list of int``, optional): padding to be added on both sides, must be >= 0 and <= kernel_size / 2 + dilation (``list of int``, optional): The stride between elements within a sliding window, must be > 0. Default 1 + ceil_mode (bool, optional): If True, will use ceil instead of floor to compute the output shape. + Defaults to False. + + + Returns: + Tensor: A quantized tensor with max_pool2d applied. + + Example:: + + >>> qx = torch.quantize_per_tensor(torch.rand(2, 2, 2, 2), 1.5, 3, torch.quint8) + >>> torch.quantized_max_pool2d(qx, [2,2]) + tensor([[[[1.5000]], + + [[1.5000]]], + + + [[[0.0000]], + + [[0.0000]]]], size=(2, 2, 1, 1), dtype=torch.quint8, + quantization_scheme=torch.per_tensor_affine, scale=1.5, zero_point=3) + """ + +def quantized_max_pool3d( + input: Tensor, + kernel_size: _int | _size, + stride: _int | _size = (), + padding: _int | _size = 0, + dilation: _int | _size = 1, + ceil_mode: _bool = False, +) -> Tensor: ... +def quantized_rnn_relu_cell( + input: Tensor, + hx: Tensor, + w_ih: Tensor, + w_hh: Tensor, + b_ih: Tensor, + b_hh: Tensor, + packed_ih: Tensor, + packed_hh: Tensor, + col_offsets_ih: Tensor, + col_offsets_hh: Tensor, + scale_ih: Number | _complex, + scale_hh: Number | _complex, + zero_point_ih: Number | _complex, + zero_point_hh: Number | _complex, +) -> Tensor: ... +def quantized_rnn_tanh_cell( + input: Tensor, + hx: Tensor, + w_ih: Tensor, + w_hh: Tensor, + b_ih: Tensor, + b_hh: Tensor, + packed_ih: Tensor, + packed_hh: Tensor, + col_offsets_ih: Tensor, + col_offsets_hh: Tensor, + scale_ih: Number | _complex, + scale_hh: Number | _complex, + zero_point_ih: Number | _complex, + zero_point_hh: Number | _complex, +) -> Tensor: ... +def rad2deg(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + rad2deg(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Returns a new tensor with each of the elements of :attr:`input` + converted from angles in radians to degrees. + + Args: + input (Tensor): the input tensor. + + Keyword arguments: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([[3.142, -3.142], [6.283, -6.283], [1.570, -1.570]]) + >>> torch.rad2deg(a) + tensor([[ 180.0233, -180.0233], + [ 359.9894, -359.9894], + [ 89.9544, -89.9544]]) + """ + +def rad2deg_(input: Tensor) -> Tensor: ... +@overload +def rand( + size: Sequence[_int | SymInt], + *, + generator: Generator | None, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + rand(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Returns a tensor filled with random numbers from a uniform distribution + on the interval :math:`[0, 1)` + + The shape of the tensor is defined by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.rand(4) + tensor([ 0.5204, 0.2503, 0.3525, 0.5673]) + >>> torch.rand(2, 3) + tensor([[ 0.8237, 0.5781, 0.6879], + [ 0.3816, 0.7249, 0.0998]]) + """ + +@overload +def rand( + *size: _int | SymInt, + generator: Generator | None, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + rand(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Returns a tensor filled with random numbers from a uniform distribution + on the interval :math:`[0, 1)` + + The shape of the tensor is defined by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.rand(4) + tensor([ 0.5204, 0.2503, 0.3525, 0.5673]) + >>> torch.rand(2, 3) + tensor([[ 0.8237, 0.5781, 0.6879], + [ 0.3816, 0.7249, 0.0998]]) + """ + +@overload +def rand( + size: Sequence[_int | SymInt], + *, + generator: Generator | None, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + rand(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Returns a tensor filled with random numbers from a uniform distribution + on the interval :math:`[0, 1)` + + The shape of the tensor is defined by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.rand(4) + tensor([ 0.5204, 0.2503, 0.3525, 0.5673]) + >>> torch.rand(2, 3) + tensor([[ 0.8237, 0.5781, 0.6879], + [ 0.3816, 0.7249, 0.0998]]) + """ + +@overload +def rand( + *size: _int | SymInt, + generator: Generator | None, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + rand(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Returns a tensor filled with random numbers from a uniform distribution + on the interval :math:`[0, 1)` + + The shape of the tensor is defined by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.rand(4) + tensor([ 0.5204, 0.2503, 0.3525, 0.5673]) + >>> torch.rand(2, 3) + tensor([[ 0.8237, 0.5781, 0.6879], + [ 0.3816, 0.7249, 0.0998]]) + """ + +@overload +def rand( + size: Sequence[_int | SymInt], + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + rand(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Returns a tensor filled with random numbers from a uniform distribution + on the interval :math:`[0, 1)` + + The shape of the tensor is defined by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.rand(4) + tensor([ 0.5204, 0.2503, 0.3525, 0.5673]) + >>> torch.rand(2, 3) + tensor([[ 0.8237, 0.5781, 0.6879], + [ 0.3816, 0.7249, 0.0998]]) + """ + +@overload +def rand( + *size: _int | SymInt, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + rand(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Returns a tensor filled with random numbers from a uniform distribution + on the interval :math:`[0, 1)` + + The shape of the tensor is defined by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.rand(4) + tensor([ 0.5204, 0.2503, 0.3525, 0.5673]) + >>> torch.rand(2, 3) + tensor([[ 0.8237, 0.5781, 0.6879], + [ 0.3816, 0.7249, 0.0998]]) + """ + +@overload +def rand( + size: Sequence[_int | SymInt], + *, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + rand(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Returns a tensor filled with random numbers from a uniform distribution + on the interval :math:`[0, 1)` + + The shape of the tensor is defined by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.rand(4) + tensor([ 0.5204, 0.2503, 0.3525, 0.5673]) + >>> torch.rand(2, 3) + tensor([[ 0.8237, 0.5781, 0.6879], + [ 0.3816, 0.7249, 0.0998]]) + """ + +@overload +def rand( + *size: _int | SymInt, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + rand(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Returns a tensor filled with random numbers from a uniform distribution + on the interval :math:`[0, 1)` + + The shape of the tensor is defined by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.rand(4) + tensor([ 0.5204, 0.2503, 0.3525, 0.5673]) + >>> torch.rand(2, 3) + tensor([[ 0.8237, 0.5781, 0.6879], + [ 0.3816, 0.7249, 0.0998]]) + """ + +@overload +def rand_like( + input: Tensor, + *, + generator: Generator | None, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + rand_like(input, *, generator=None, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same size as :attr:`input` that is filled with + random numbers from a uniform distribution on the interval :math:`[0, 1)`. + ``torch.rand_like(input)`` is equivalent to + ``torch.rand(input.size(), dtype=input.dtype, layout=input.layout, device=input.device)``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling. + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + +@overload +def rand_like( + input: Tensor, + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + rand_like(input, *, generator=None, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same size as :attr:`input` that is filled with + random numbers from a uniform distribution on the interval :math:`[0, 1)`. + ``torch.rand_like(input)`` is equivalent to + ``torch.rand(input.size(), dtype=input.dtype, layout=input.layout, device=input.device)``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling. + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + +@overload +def randint( + low: _int, + high: _int, + size: _size, + *, + generator: Generator | None = None, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + randint(low=0, high, size, \*, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with random integers generated uniformly + between :attr:`low` (inclusive) and :attr:`high` (exclusive). + + The shape of the tensor is defined by the variable argument :attr:`size`. + + .. note:: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + size (tuple): a tuple defining the shape of the output tensor. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the desired data type of returned tensor. Default: if ``None``, + this function returns a tensor with dtype ``torch.int64``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.randint(3, 5, (3,)) + tensor([4, 3, 4]) + + + >>> torch.randint(10, (2, 2)) + tensor([[0, 2], + [5, 5]]) + + + >>> torch.randint(3, 10, (2, 2)) + tensor([[4, 5], + [6, 7]]) + """ + +@overload +def randint( + high: _int, + size: _size, + *, + generator: Generator | None = None, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + randint(low=0, high, size, \*, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with random integers generated uniformly + between :attr:`low` (inclusive) and :attr:`high` (exclusive). + + The shape of the tensor is defined by the variable argument :attr:`size`. + + .. note:: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + size (tuple): a tuple defining the shape of the output tensor. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the desired data type of returned tensor. Default: if ``None``, + this function returns a tensor with dtype ``torch.int64``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.randint(3, 5, (3,)) + tensor([4, 3, 4]) + + + >>> torch.randint(10, (2, 2)) + tensor([[0, 2], + [5, 5]]) + + + >>> torch.randint(3, 10, (2, 2)) + tensor([[4, 5], + [6, 7]]) + """ + +@overload +def randint( + high: _int | SymInt, + size: Sequence[_int | SymInt], + *, + generator: Generator | None, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randint(low=0, high, size, \*, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with random integers generated uniformly + between :attr:`low` (inclusive) and :attr:`high` (exclusive). + + The shape of the tensor is defined by the variable argument :attr:`size`. + + .. note:: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + size (tuple): a tuple defining the shape of the output tensor. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the desired data type of returned tensor. Default: if ``None``, + this function returns a tensor with dtype ``torch.int64``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.randint(3, 5, (3,)) + tensor([4, 3, 4]) + + + >>> torch.randint(10, (2, 2)) + tensor([[0, 2], + [5, 5]]) + + + >>> torch.randint(3, 10, (2, 2)) + tensor([[4, 5], + [6, 7]]) + """ + +@overload +def randint( + high: _int | SymInt, + size: Sequence[_int | SymInt], + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randint(low=0, high, size, \*, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with random integers generated uniformly + between :attr:`low` (inclusive) and :attr:`high` (exclusive). + + The shape of the tensor is defined by the variable argument :attr:`size`. + + .. note:: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + size (tuple): a tuple defining the shape of the output tensor. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the desired data type of returned tensor. Default: if ``None``, + this function returns a tensor with dtype ``torch.int64``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.randint(3, 5, (3,)) + tensor([4, 3, 4]) + + + >>> torch.randint(10, (2, 2)) + tensor([[0, 2], + [5, 5]]) + + + >>> torch.randint(3, 10, (2, 2)) + tensor([[4, 5], + [6, 7]]) + """ + +@overload +def randint( + low: _int | SymInt, + high: _int | SymInt, + size: Sequence[_int | SymInt], + *, + generator: Generator | None, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randint(low=0, high, size, \*, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with random integers generated uniformly + between :attr:`low` (inclusive) and :attr:`high` (exclusive). + + The shape of the tensor is defined by the variable argument :attr:`size`. + + .. note:: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + size (tuple): a tuple defining the shape of the output tensor. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the desired data type of returned tensor. Default: if ``None``, + this function returns a tensor with dtype ``torch.int64``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.randint(3, 5, (3,)) + tensor([4, 3, 4]) + + + >>> torch.randint(10, (2, 2)) + tensor([[0, 2], + [5, 5]]) + + + >>> torch.randint(3, 10, (2, 2)) + tensor([[4, 5], + [6, 7]]) + """ + +@overload +def randint( + low: _int | SymInt, + high: _int | SymInt, + size: Sequence[_int | SymInt], + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randint(low=0, high, size, \*, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with random integers generated uniformly + between :attr:`low` (inclusive) and :attr:`high` (exclusive). + + The shape of the tensor is defined by the variable argument :attr:`size`. + + .. note:: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + size (tuple): a tuple defining the shape of the output tensor. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (torch.dtype, optional): the desired data type of returned tensor. Default: if ``None``, + this function returns a tensor with dtype ``torch.int64``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.randint(3, 5, (3,)) + tensor([4, 3, 4]) + + + >>> torch.randint(10, (2, 2)) + tensor([[0, 2], + [5, 5]]) + + + >>> torch.randint(3, 10, (2, 2)) + tensor([[4, 5], + [6, 7]]) + """ + +@overload +def randint_like( + input: Tensor, + low: _int | SymInt, + high: _int | SymInt, + *, + generator: Generator | None, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randint_like(input, low=0, high, \*, generator=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same shape as Tensor :attr:`input` filled with + random integers generated uniformly between :attr:`low` (inclusive) and + :attr:`high` (exclusive). + + .. note: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling. + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + +@overload +def randint_like( + input: Tensor, + low: _int | SymInt, + high: _int | SymInt, + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randint_like(input, low=0, high, \*, generator=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same shape as Tensor :attr:`input` filled with + random integers generated uniformly between :attr:`low` (inclusive) and + :attr:`high` (exclusive). + + .. note: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling. + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + +@overload +def randint_like( + input: Tensor, + high: Tensor, + *, + generator: Generator | None, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randint_like(input, low=0, high, \*, generator=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same shape as Tensor :attr:`input` filled with + random integers generated uniformly between :attr:`low` (inclusive) and + :attr:`high` (exclusive). + + .. note: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling. + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + +@overload +def randint_like( + input: Tensor, + high: Tensor, + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randint_like(input, low=0, high, \*, generator=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same shape as Tensor :attr:`input` filled with + random integers generated uniformly between :attr:`low` (inclusive) and + :attr:`high` (exclusive). + + .. note: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling. + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + +@overload +def randint_like( + input: Tensor, + high: _int | SymInt, + *, + generator: Generator | None, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randint_like(input, low=0, high, \*, generator=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same shape as Tensor :attr:`input` filled with + random integers generated uniformly between :attr:`low` (inclusive) and + :attr:`high` (exclusive). + + .. note: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling. + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + +@overload +def randint_like( + input: Tensor, + high: _int | SymInt, + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randint_like(input, low=0, high, \*, generator=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same shape as Tensor :attr:`input` filled with + random integers generated uniformly between :attr:`low` (inclusive) and + :attr:`high` (exclusive). + + .. note: + With the global dtype default (``torch.float32``), this function returns + a tensor with dtype ``torch.int64``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + low (int, optional): Lowest integer to be drawn from the distribution. Default: 0. + high (int): One above the highest integer to be drawn from the distribution. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling. + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + +@overload +def randn( + size: Sequence[_int | SymInt], + *, + generator: Generator | None, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randn(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + + Returns a tensor filled with random numbers from a normal distribution + with mean `0` and variance `1` (also called the standard normal + distribution). + + .. math:: + \text{out}_{i} \sim \mathcal{N}(0, 1) + + For complex dtypes, the tensor is i.i.d. sampled from a `complex normal distribution`_ with zero mean and + unit variance as + + .. math:: + \text{out}_{i} \sim \mathcal{CN}(0, 1) + + This is equivalent to separately sampling the real :math:`(\operatorname{Re})` and imaginary + :math:`(\operatorname{Im})` part of :math:`\text{out}_i` as + + .. math:: + \operatorname{Re}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}),\quad + \operatorname{Im}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}) + + The shape of the tensor is defined by the variable argument :attr:`size`. + + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.randn(4) + tensor([-2.1436, 0.9966, 2.3426, -0.6366]) + >>> torch.randn(2, 3) + tensor([[ 1.5954, 2.8929, -1.0923], + [ 1.1719, -0.4709, -0.1996]]) + + .. _complex normal distribution: https://en.wikipedia.org/wiki/Complex_normal_distribution + """ + +@overload +def randn( + *size: _int | SymInt, + generator: Generator | None, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randn(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + + Returns a tensor filled with random numbers from a normal distribution + with mean `0` and variance `1` (also called the standard normal + distribution). + + .. math:: + \text{out}_{i} \sim \mathcal{N}(0, 1) + + For complex dtypes, the tensor is i.i.d. sampled from a `complex normal distribution`_ with zero mean and + unit variance as + + .. math:: + \text{out}_{i} \sim \mathcal{CN}(0, 1) + + This is equivalent to separately sampling the real :math:`(\operatorname{Re})` and imaginary + :math:`(\operatorname{Im})` part of :math:`\text{out}_i` as + + .. math:: + \operatorname{Re}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}),\quad + \operatorname{Im}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}) + + The shape of the tensor is defined by the variable argument :attr:`size`. + + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.randn(4) + tensor([-2.1436, 0.9966, 2.3426, -0.6366]) + >>> torch.randn(2, 3) + tensor([[ 1.5954, 2.8929, -1.0923], + [ 1.1719, -0.4709, -0.1996]]) + + .. _complex normal distribution: https://en.wikipedia.org/wiki/Complex_normal_distribution + """ + +@overload +def randn( + size: Sequence[_int | SymInt], + *, + generator: Generator | None, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randn(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + + Returns a tensor filled with random numbers from a normal distribution + with mean `0` and variance `1` (also called the standard normal + distribution). + + .. math:: + \text{out}_{i} \sim \mathcal{N}(0, 1) + + For complex dtypes, the tensor is i.i.d. sampled from a `complex normal distribution`_ with zero mean and + unit variance as + + .. math:: + \text{out}_{i} \sim \mathcal{CN}(0, 1) + + This is equivalent to separately sampling the real :math:`(\operatorname{Re})` and imaginary + :math:`(\operatorname{Im})` part of :math:`\text{out}_i` as + + .. math:: + \operatorname{Re}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}),\quad + \operatorname{Im}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}) + + The shape of the tensor is defined by the variable argument :attr:`size`. + + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.randn(4) + tensor([-2.1436, 0.9966, 2.3426, -0.6366]) + >>> torch.randn(2, 3) + tensor([[ 1.5954, 2.8929, -1.0923], + [ 1.1719, -0.4709, -0.1996]]) + + .. _complex normal distribution: https://en.wikipedia.org/wiki/Complex_normal_distribution + """ + +@overload +def randn( + *size: _int | SymInt, + generator: Generator | None, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randn(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + + Returns a tensor filled with random numbers from a normal distribution + with mean `0` and variance `1` (also called the standard normal + distribution). + + .. math:: + \text{out}_{i} \sim \mathcal{N}(0, 1) + + For complex dtypes, the tensor is i.i.d. sampled from a `complex normal distribution`_ with zero mean and + unit variance as + + .. math:: + \text{out}_{i} \sim \mathcal{CN}(0, 1) + + This is equivalent to separately sampling the real :math:`(\operatorname{Re})` and imaginary + :math:`(\operatorname{Im})` part of :math:`\text{out}_i` as + + .. math:: + \operatorname{Re}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}),\quad + \operatorname{Im}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}) + + The shape of the tensor is defined by the variable argument :attr:`size`. + + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.randn(4) + tensor([-2.1436, 0.9966, 2.3426, -0.6366]) + >>> torch.randn(2, 3) + tensor([[ 1.5954, 2.8929, -1.0923], + [ 1.1719, -0.4709, -0.1996]]) + + .. _complex normal distribution: https://en.wikipedia.org/wiki/Complex_normal_distribution + """ + +@overload +def randn( + size: Sequence[_int | SymInt], + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randn(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + + Returns a tensor filled with random numbers from a normal distribution + with mean `0` and variance `1` (also called the standard normal + distribution). + + .. math:: + \text{out}_{i} \sim \mathcal{N}(0, 1) + + For complex dtypes, the tensor is i.i.d. sampled from a `complex normal distribution`_ with zero mean and + unit variance as + + .. math:: + \text{out}_{i} \sim \mathcal{CN}(0, 1) + + This is equivalent to separately sampling the real :math:`(\operatorname{Re})` and imaginary + :math:`(\operatorname{Im})` part of :math:`\text{out}_i` as + + .. math:: + \operatorname{Re}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}),\quad + \operatorname{Im}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}) + + The shape of the tensor is defined by the variable argument :attr:`size`. + + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.randn(4) + tensor([-2.1436, 0.9966, 2.3426, -0.6366]) + >>> torch.randn(2, 3) + tensor([[ 1.5954, 2.8929, -1.0923], + [ 1.1719, -0.4709, -0.1996]]) + + .. _complex normal distribution: https://en.wikipedia.org/wiki/Complex_normal_distribution + """ + +@overload +def randn( + *size: _int | SymInt, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randn(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + + Returns a tensor filled with random numbers from a normal distribution + with mean `0` and variance `1` (also called the standard normal + distribution). + + .. math:: + \text{out}_{i} \sim \mathcal{N}(0, 1) + + For complex dtypes, the tensor is i.i.d. sampled from a `complex normal distribution`_ with zero mean and + unit variance as + + .. math:: + \text{out}_{i} \sim \mathcal{CN}(0, 1) + + This is equivalent to separately sampling the real :math:`(\operatorname{Re})` and imaginary + :math:`(\operatorname{Im})` part of :math:`\text{out}_i` as + + .. math:: + \operatorname{Re}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}),\quad + \operatorname{Im}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}) + + The shape of the tensor is defined by the variable argument :attr:`size`. + + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.randn(4) + tensor([-2.1436, 0.9966, 2.3426, -0.6366]) + >>> torch.randn(2, 3) + tensor([[ 1.5954, 2.8929, -1.0923], + [ 1.1719, -0.4709, -0.1996]]) + + .. _complex normal distribution: https://en.wikipedia.org/wiki/Complex_normal_distribution + """ + +@overload +def randn( + size: Sequence[_int | SymInt], + *, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randn(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + + Returns a tensor filled with random numbers from a normal distribution + with mean `0` and variance `1` (also called the standard normal + distribution). + + .. math:: + \text{out}_{i} \sim \mathcal{N}(0, 1) + + For complex dtypes, the tensor is i.i.d. sampled from a `complex normal distribution`_ with zero mean and + unit variance as + + .. math:: + \text{out}_{i} \sim \mathcal{CN}(0, 1) + + This is equivalent to separately sampling the real :math:`(\operatorname{Re})` and imaginary + :math:`(\operatorname{Im})` part of :math:`\text{out}_i` as + + .. math:: + \operatorname{Re}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}),\quad + \operatorname{Im}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}) + + The shape of the tensor is defined by the variable argument :attr:`size`. + + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.randn(4) + tensor([-2.1436, 0.9966, 2.3426, -0.6366]) + >>> torch.randn(2, 3) + tensor([[ 1.5954, 2.8929, -1.0923], + [ 1.1719, -0.4709, -0.1996]]) + + .. _complex normal distribution: https://en.wikipedia.org/wiki/Complex_normal_distribution + """ + +@overload +def randn( + *size: _int | SymInt, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randn(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + + Returns a tensor filled with random numbers from a normal distribution + with mean `0` and variance `1` (also called the standard normal + distribution). + + .. math:: + \text{out}_{i} \sim \mathcal{N}(0, 1) + + For complex dtypes, the tensor is i.i.d. sampled from a `complex normal distribution`_ with zero mean and + unit variance as + + .. math:: + \text{out}_{i} \sim \mathcal{CN}(0, 1) + + This is equivalent to separately sampling the real :math:`(\operatorname{Re})` and imaginary + :math:`(\operatorname{Im})` part of :math:`\text{out}_i` as + + .. math:: + \operatorname{Re}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}),\quad + \operatorname{Im}(\text{out}_{i}) \sim \mathcal{N}(0, \frac{1}{2}) + + The shape of the tensor is defined by the variable argument :attr:`size`. + + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.randn(4) + tensor([-2.1436, 0.9966, 2.3426, -0.6366]) + >>> torch.randn(2, 3) + tensor([[ 1.5954, 2.8929, -1.0923], + [ 1.1719, -0.4709, -0.1996]]) + + .. _complex normal distribution: https://en.wikipedia.org/wiki/Complex_normal_distribution + """ + +@overload +def randn_like( + input: Tensor, + *, + generator: Generator | None, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randn_like(input, *, generator=None, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same size as :attr:`input` that is filled with + random numbers from a normal distribution with mean 0 and variance 1. Please refer to :func:`torch.randn` for the + sampling process of complex dtypes. ``torch.randn_like(input)`` is equivalent to + ``torch.randn(input.size(), dtype=input.dtype, layout=input.layout, device=input.device)``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling. + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + +@overload +def randn_like( + input: Tensor, + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randn_like(input, *, generator=None, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor with the same size as :attr:`input` that is filled with + random numbers from a normal distribution with mean 0 and variance 1. Please refer to :func:`torch.randn` for the + sampling process of complex dtypes. ``torch.randn_like(input)`` is equivalent to + ``torch.randn(input.size(), dtype=input.dtype, layout=input.layout, device=input.device)``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling. + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + """ + +@overload +def randperm( + n: _int | SymInt, + *, + generator: Generator | None, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randperm(n, *, generator=None, out=None, dtype=torch.int64,layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Returns a random permutation of integers from ``0`` to ``n - 1``. + + Args: + n (int): the upper bound (exclusive) + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: ``torch.int64``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.randperm(4) + tensor([2, 1, 0, 3]) + """ + +@overload +def randperm( + n: _int | SymInt, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + randperm(n, *, generator=None, out=None, dtype=torch.int64,layout=torch.strided, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Returns a random permutation of integers from ``0`` to ``n - 1``. + + Args: + n (int): the upper bound (exclusive) + + Keyword args: + generator (:class:`torch.Generator`, optional): a pseudorandom number generator for sampling + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: ``torch.int64``. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + Example:: + + >>> torch.randperm(4) + tensor([2, 1, 0, 3]) + """ + +def range( + start: Number, + end: Number, + step: Number = 1, + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + range(start=0, end, step=1, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a 1-D tensor of size :math:`\left\lfloor \frac{\text{end} - \text{start}}{\text{step}} \right\rfloor + 1` + with values from :attr:`start` to :attr:`end` with step :attr:`step`. Step is + the gap between two values in the tensor. + + .. math:: + \text{out}_{i+1} = \text{out}_i + \text{step}. + + .. warning:: + This function is deprecated and will be removed in a future release because its behavior is inconsistent with + Python's range builtin. Instead, use :func:`torch.arange`, which produces values in [start, end). + + Args: + start (float, optional): the starting value for the set of points. Default: ``0``. + end (float): the ending value for the set of points + step (float, optional): the gap between each pair of adjacent points. Default: ``1``. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). If `dtype` is not given, infer the data type from the other input + arguments. If any of `start`, `end`, or `step` are floating-point, the + `dtype` is inferred to be the default dtype, see + :meth:`~torch.get_default_dtype`. Otherwise, the `dtype` is inferred to + be `torch.int64`. + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.range(1, 4) + tensor([ 1., 2., 3., 4.]) + >>> torch.range(1, 4, 0.5) + tensor([ 1.0000, 1.5000, 2.0000, 2.5000, 3.0000, 3.5000, 4.0000]) + """ + +def ravel(input: Tensor) -> Tensor: + r""" + ravel(input) -> Tensor + + Return a contiguous flattened tensor. A copy is made only if needed. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> t = torch.tensor([[[1, 2], + ... [3, 4]], + ... [[5, 6], + ... [7, 8]]]) + >>> torch.ravel(t) + tensor([1, 2, 3, 4, 5, 6, 7, 8]) + """ + +def real(input: Tensor) -> Tensor: + r""" + real(input) -> Tensor + + Returns a new tensor containing real values of the :attr:`self` tensor. + The returned tensor and :attr:`self` share the same underlying storage. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> x=torch.randn(4, dtype=torch.cfloat) + >>> x + tensor([(0.3100+0.3553j), (-0.5445-0.7896j), (-1.6492-0.0633j), (-0.0638-0.8119j)]) + >>> x.real + tensor([ 0.3100, -0.5445, -1.6492, -0.0638]) + """ + +def reciprocal(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + reciprocal(input, *, out=None) -> Tensor + + Returns a new tensor with the reciprocal of the elements of :attr:`input` + + .. math:: + \text{out}_{i} = \frac{1}{\text{input}_{i}} + + .. note:: + Unlike NumPy's reciprocal, torch.reciprocal supports integral inputs. Integral + inputs to reciprocal are automatically :ref:`promoted ` to + the default scalar type. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-0.4595, -2.1219, -1.4314, 0.7298]) + >>> torch.reciprocal(a) + tensor([-2.1763, -0.4713, -0.6986, 1.3702]) + """ + +def reciprocal_(input: Tensor) -> Tensor: ... +def relu(input: Tensor) -> Tensor: ... +def relu_(input: Tensor) -> Tensor: ... +@overload +def remainder( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + remainder(input, other, *, out=None) -> Tensor + + Computes + `Python's modulus operation `_ + entrywise. The result has the same sign as the divisor :attr:`other` and its absolute value + is less than that of :attr:`other`. + + It may also be defined in terms of :func:`torch.div` as + + .. code:: python + + torch.remainder(a, b) == a - a.div(b, rounding_mode="floor") * b + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer and float inputs. + + .. note:: + Complex inputs are not supported. In some cases, it is not mathematically + possible to satisfy the definition of a modulo operation with complex numbers. + See :func:`torch.fmod` for how division by zero is handled. + + .. seealso:: + + :func:`torch.fmod` which implements C++'s `std::fmod `_. + This one is defined in terms of division rounding towards zero. + + Args: + input (Tensor or Scalar): the dividend + other (Tensor or Scalar): the divisor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.remainder(torch.tensor([-3., -2, -1, 1, 2, 3]), 2) + tensor([ 1., 0., 1., 1., 0., 1.]) + >>> torch.remainder(torch.tensor([1, 2, 3, 4, 5]), -1.5) + tensor([ -0.5000, -1.0000, 0.0000, -0.5000, -1.0000 ]) + """ + +@overload +def remainder(self: Number | _complex, other: Tensor) -> Tensor: + r""" + remainder(input, other, *, out=None) -> Tensor + + Computes + `Python's modulus operation `_ + entrywise. The result has the same sign as the divisor :attr:`other` and its absolute value + is less than that of :attr:`other`. + + It may also be defined in terms of :func:`torch.div` as + + .. code:: python + + torch.remainder(a, b) == a - a.div(b, rounding_mode="floor") * b + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer and float inputs. + + .. note:: + Complex inputs are not supported. In some cases, it is not mathematically + possible to satisfy the definition of a modulo operation with complex numbers. + See :func:`torch.fmod` for how division by zero is handled. + + .. seealso:: + + :func:`torch.fmod` which implements C++'s `std::fmod `_. + This one is defined in terms of division rounding towards zero. + + Args: + input (Tensor or Scalar): the dividend + other (Tensor or Scalar): the divisor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.remainder(torch.tensor([-3., -2, -1, 1, 2, 3]), 2) + tensor([ 1., 0., 1., 1., 0., 1.]) + >>> torch.remainder(torch.tensor([1, 2, 3, 4, 5]), -1.5) + tensor([ -0.5000, -1.0000, 0.0000, -0.5000, -1.0000 ]) + """ + +@overload +def remainder( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + remainder(input, other, *, out=None) -> Tensor + + Computes + `Python's modulus operation `_ + entrywise. The result has the same sign as the divisor :attr:`other` and its absolute value + is less than that of :attr:`other`. + + It may also be defined in terms of :func:`torch.div` as + + .. code:: python + + torch.remainder(a, b) == a - a.div(b, rounding_mode="floor") * b + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer and float inputs. + + .. note:: + Complex inputs are not supported. In some cases, it is not mathematically + possible to satisfy the definition of a modulo operation with complex numbers. + See :func:`torch.fmod` for how division by zero is handled. + + .. seealso:: + + :func:`torch.fmod` which implements C++'s `std::fmod `_. + This one is defined in terms of division rounding towards zero. + + Args: + input (Tensor or Scalar): the dividend + other (Tensor or Scalar): the divisor + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.remainder(torch.tensor([-3., -2, -1, 1, 2, 3]), 2) + tensor([ 1., 0., 1., 1., 0., 1.]) + >>> torch.remainder(torch.tensor([1, 2, 3, 4, 5]), -1.5) + tensor([ -0.5000, -1.0000, 0.0000, -0.5000, -1.0000 ]) + """ + +def renorm( + input: Tensor, + p: Number | _complex, + dim: _int, + maxnorm: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + renorm(input, p, dim, maxnorm, *, out=None) -> Tensor + + Returns a tensor where each sub-tensor of :attr:`input` along dimension + :attr:`dim` is normalized such that the `p`-norm of the sub-tensor is lower + than the value :attr:`maxnorm` + + .. note:: If the norm of a row is lower than `maxnorm`, the row is unchanged + + Args: + input (Tensor): the input tensor. + p (float): the power for the norm computation + dim (int): the dimension to slice over to get the sub-tensors + maxnorm (float): the maximum norm to keep each sub-tensor under + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> x = torch.ones(3, 3) + >>> x[1].fill_(2) + tensor([ 2., 2., 2.]) + >>> x[2].fill_(3) + tensor([ 3., 3., 3.]) + >>> x + tensor([[ 1., 1., 1.], + [ 2., 2., 2.], + [ 3., 3., 3.]]) + >>> torch.renorm(x, 1, 0, 5) + tensor([[ 1.0000, 1.0000, 1.0000], + [ 1.6667, 1.6667, 1.6667], + [ 1.6667, 1.6667, 1.6667]]) + """ + +@overload +def repeat_interleave( + input: Tensor, + repeats: Tensor, + dim: _int | None = None, + *, + output_size: _int | SymInt | None = None, +) -> Tensor: + r""" + repeat_interleave(input, repeats, dim=None, *, output_size=None) -> Tensor + + Repeat elements of a tensor. + + .. warning:: + + This is different from :meth:`torch.Tensor.repeat` but similar to ``numpy.repeat``. + + Args: + input (Tensor): the input tensor. + repeats (Tensor or int): The number of repetitions for each element. + repeats is broadcasted to fit the shape of the given axis. + dim (int, optional): The dimension along which to repeat values. + By default, use the flattened input array, and return a flat output + array. + + Keyword args: + output_size (int, optional): Total output size for the given axis + ( e.g. sum of repeats). If given, it will avoid stream synchronization + needed to calculate output shape of the tensor. + + Returns: + Tensor: Repeated tensor which has the same shape as input, except along the given axis. + + Example:: + + >>> x = torch.tensor([1, 2, 3]) + >>> x.repeat_interleave(2) + tensor([1, 1, 2, 2, 3, 3]) + >>> y = torch.tensor([[1, 2], [3, 4]]) + >>> torch.repeat_interleave(y, 2) + tensor([1, 1, 2, 2, 3, 3, 4, 4]) + >>> torch.repeat_interleave(y, 3, dim=1) + tensor([[1, 1, 1, 2, 2, 2], + [3, 3, 3, 4, 4, 4]]) + >>> torch.repeat_interleave(y, torch.tensor([1, 2]), dim=0) + tensor([[1, 2], + [3, 4], + [3, 4]]) + >>> torch.repeat_interleave(y, torch.tensor([1, 2]), dim=0, output_size=3) + tensor([[1, 2], + [3, 4], + [3, 4]]) + + If the `repeats` is `tensor([n1, n2, n3, ...])`, then the output will be + `tensor([0, 0, ..., 1, 1, ..., 2, 2, ..., ...])` where `0` appears `n1` times, + `1` appears `n2` times, `2` appears `n3` times, etc. + + .. function:: repeat_interleave(repeats, *) -> Tensor + :noindex: + + Repeats 0 repeats[0] times, 1 repeats[1] times, 2 repeats[2] times, etc. + + Args: + repeats (Tensor): The number of repetitions for each element. + + Returns: + Tensor: Repeated tensor of size `sum(repeats)`. + + Example:: + + >>> torch.repeat_interleave(torch.tensor([1, 2, 3])) + tensor([0, 1, 1, 2, 2, 2]) + """ + +@overload +def repeat_interleave( + repeats: Tensor, + *, + output_size: _int | SymInt | None = None, +) -> Tensor: + r""" + repeat_interleave(input, repeats, dim=None, *, output_size=None) -> Tensor + + Repeat elements of a tensor. + + .. warning:: + + This is different from :meth:`torch.Tensor.repeat` but similar to ``numpy.repeat``. + + Args: + input (Tensor): the input tensor. + repeats (Tensor or int): The number of repetitions for each element. + repeats is broadcasted to fit the shape of the given axis. + dim (int, optional): The dimension along which to repeat values. + By default, use the flattened input array, and return a flat output + array. + + Keyword args: + output_size (int, optional): Total output size for the given axis + ( e.g. sum of repeats). If given, it will avoid stream synchronization + needed to calculate output shape of the tensor. + + Returns: + Tensor: Repeated tensor which has the same shape as input, except along the given axis. + + Example:: + + >>> x = torch.tensor([1, 2, 3]) + >>> x.repeat_interleave(2) + tensor([1, 1, 2, 2, 3, 3]) + >>> y = torch.tensor([[1, 2], [3, 4]]) + >>> torch.repeat_interleave(y, 2) + tensor([1, 1, 2, 2, 3, 3, 4, 4]) + >>> torch.repeat_interleave(y, 3, dim=1) + tensor([[1, 1, 1, 2, 2, 2], + [3, 3, 3, 4, 4, 4]]) + >>> torch.repeat_interleave(y, torch.tensor([1, 2]), dim=0) + tensor([[1, 2], + [3, 4], + [3, 4]]) + >>> torch.repeat_interleave(y, torch.tensor([1, 2]), dim=0, output_size=3) + tensor([[1, 2], + [3, 4], + [3, 4]]) + + If the `repeats` is `tensor([n1, n2, n3, ...])`, then the output will be + `tensor([0, 0, ..., 1, 1, ..., 2, 2, ..., ...])` where `0` appears `n1` times, + `1` appears `n2` times, `2` appears `n3` times, etc. + + .. function:: repeat_interleave(repeats, *) -> Tensor + :noindex: + + Repeats 0 repeats[0] times, 1 repeats[1] times, 2 repeats[2] times, etc. + + Args: + repeats (Tensor): The number of repetitions for each element. + + Returns: + Tensor: Repeated tensor of size `sum(repeats)`. + + Example:: + + >>> torch.repeat_interleave(torch.tensor([1, 2, 3])) + tensor([0, 1, 1, 2, 2, 2]) + """ + +@overload +def repeat_interleave( + input: Tensor, + repeats: _int | SymInt, + dim: _int | None = None, + *, + output_size: _int | SymInt | None = None, +) -> Tensor: + r""" + repeat_interleave(input, repeats, dim=None, *, output_size=None) -> Tensor + + Repeat elements of a tensor. + + .. warning:: + + This is different from :meth:`torch.Tensor.repeat` but similar to ``numpy.repeat``. + + Args: + input (Tensor): the input tensor. + repeats (Tensor or int): The number of repetitions for each element. + repeats is broadcasted to fit the shape of the given axis. + dim (int, optional): The dimension along which to repeat values. + By default, use the flattened input array, and return a flat output + array. + + Keyword args: + output_size (int, optional): Total output size for the given axis + ( e.g. sum of repeats). If given, it will avoid stream synchronization + needed to calculate output shape of the tensor. + + Returns: + Tensor: Repeated tensor which has the same shape as input, except along the given axis. + + Example:: + + >>> x = torch.tensor([1, 2, 3]) + >>> x.repeat_interleave(2) + tensor([1, 1, 2, 2, 3, 3]) + >>> y = torch.tensor([[1, 2], [3, 4]]) + >>> torch.repeat_interleave(y, 2) + tensor([1, 1, 2, 2, 3, 3, 4, 4]) + >>> torch.repeat_interleave(y, 3, dim=1) + tensor([[1, 1, 1, 2, 2, 2], + [3, 3, 3, 4, 4, 4]]) + >>> torch.repeat_interleave(y, torch.tensor([1, 2]), dim=0) + tensor([[1, 2], + [3, 4], + [3, 4]]) + >>> torch.repeat_interleave(y, torch.tensor([1, 2]), dim=0, output_size=3) + tensor([[1, 2], + [3, 4], + [3, 4]]) + + If the `repeats` is `tensor([n1, n2, n3, ...])`, then the output will be + `tensor([0, 0, ..., 1, 1, ..., 2, 2, ..., ...])` where `0` appears `n1` times, + `1` appears `n2` times, `2` appears `n3` times, etc. + + .. function:: repeat_interleave(repeats, *) -> Tensor + :noindex: + + Repeats 0 repeats[0] times, 1 repeats[1] times, 2 repeats[2] times, etc. + + Args: + repeats (Tensor): The number of repetitions for each element. + + Returns: + Tensor: Repeated tensor of size `sum(repeats)`. + + Example:: + + >>> torch.repeat_interleave(torch.tensor([1, 2, 3])) + tensor([0, 1, 1, 2, 2, 2]) + """ + +def reshape(input: Tensor, shape: Sequence[_int | SymInt]) -> Tensor: + r""" + reshape(input, shape) -> Tensor + + Returns a tensor with the same data and number of elements as :attr:`input`, + but with the specified shape. When possible, the returned tensor will be a view + of :attr:`input`. Otherwise, it will be a copy. Contiguous inputs and inputs + with compatible strides can be reshaped without copying, but you should not + depend on the copying vs. viewing behavior. + + See :meth:`torch.Tensor.view` on when it is possible to return a view. + + A single dimension may be -1, in which case it's inferred from the remaining + dimensions and the number of elements in :attr:`input`. + + Args: + input (Tensor): the tensor to be reshaped + shape (tuple of int): the new shape + + Example:: + + >>> a = torch.arange(4.) + >>> torch.reshape(a, (2, 2)) + tensor([[ 0., 1.], + [ 2., 3.]]) + >>> b = torch.tensor([[0, 1], [2, 3]]) + >>> torch.reshape(b, (-1,)) + tensor([ 0, 1, 2, 3]) + """ + +def resize_as_( + input: Tensor, + the_template: Tensor, + *, + memory_format: memory_format | None = None, +) -> Tensor: ... +def resize_as_sparse_(input: Tensor, the_template: Tensor) -> Tensor: ... +def resolve_conj(input: Tensor) -> Tensor: + r""" + resolve_conj(input) -> Tensor + + Returns a new tensor with materialized conjugation if :attr:`input`'s conjugate bit is set to `True`, + else returns :attr:`input`. The output tensor will always have its conjugate bit set to `False`. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> x = torch.tensor([-1 + 1j, -2 + 2j, 3 - 3j]) + >>> y = x.conj() + >>> y.is_conj() + True + >>> z = y.resolve_conj() + >>> z + tensor([-1 - 1j, -2 - 2j, 3 + 3j]) + >>> z.is_conj() + False + """ + +def resolve_neg(input: Tensor) -> Tensor: + r""" + resolve_neg(input) -> Tensor + + Returns a new tensor with materialized negation if :attr:`input`'s negative bit is set to `True`, + else returns :attr:`input`. The output tensor will always have its negative bit set to `False`. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> x = torch.tensor([-1 + 1j, -2 + 2j, 3 - 3j]) + >>> y = x.conj() + >>> z = y.imag + >>> z.is_neg() + True + >>> out = z.resolve_neg() + >>> out + tensor([-1., -2., 3.]) + >>> out.is_neg() + False + """ + +@overload +def result_type(tensor: Tensor, other: Tensor) -> _dtype: + r""" + result_type(tensor1, tensor2) -> dtype + + Returns the :class:`torch.dtype` that would result from performing an arithmetic + operation on the provided input tensors. See type promotion :ref:`documentation ` + for more information on the type promotion logic. + + Args: + tensor1 (Tensor or Number): an input tensor or number + tensor2 (Tensor or Number): an input tensor or number + + Example:: + + >>> torch.result_type(torch.tensor([1, 2], dtype=torch.int), 1.0) + torch.float32 + >>> torch.result_type(torch.tensor([1, 2], dtype=torch.uint8), torch.tensor(1)) + torch.uint8 + """ + +@overload +def result_type(scalar: Number | _complex, tensor: Tensor) -> _dtype: + r""" + result_type(tensor1, tensor2) -> dtype + + Returns the :class:`torch.dtype` that would result from performing an arithmetic + operation on the provided input tensors. See type promotion :ref:`documentation ` + for more information on the type promotion logic. + + Args: + tensor1 (Tensor or Number): an input tensor or number + tensor2 (Tensor or Number): an input tensor or number + + Example:: + + >>> torch.result_type(torch.tensor([1, 2], dtype=torch.int), 1.0) + torch.float32 + >>> torch.result_type(torch.tensor([1, 2], dtype=torch.uint8), torch.tensor(1)) + torch.uint8 + """ + +@overload +def result_type(tensor: Tensor, other: Number | _complex) -> _dtype: + r""" + result_type(tensor1, tensor2) -> dtype + + Returns the :class:`torch.dtype` that would result from performing an arithmetic + operation on the provided input tensors. See type promotion :ref:`documentation ` + for more information on the type promotion logic. + + Args: + tensor1 (Tensor or Number): an input tensor or number + tensor2 (Tensor or Number): an input tensor or number + + Example:: + + >>> torch.result_type(torch.tensor([1, 2], dtype=torch.int), 1.0) + torch.float32 + >>> torch.result_type(torch.tensor([1, 2], dtype=torch.uint8), torch.tensor(1)) + torch.uint8 + """ + +@overload +def result_type( + scalar1: Number | _complex, + scalar2: Number | _complex, +) -> _dtype: + r""" + result_type(tensor1, tensor2) -> dtype + + Returns the :class:`torch.dtype` that would result from performing an arithmetic + operation on the provided input tensors. See type promotion :ref:`documentation ` + for more information on the type promotion logic. + + Args: + tensor1 (Tensor or Number): an input tensor or number + tensor2 (Tensor or Number): an input tensor or number + + Example:: + + >>> torch.result_type(torch.tensor([1, 2], dtype=torch.int), 1.0) + torch.float32 + >>> torch.result_type(torch.tensor([1, 2], dtype=torch.uint8), torch.tensor(1)) + torch.uint8 + """ + +def rms_norm( + input: Tensor, + normalized_shape: Sequence[_int | SymInt], + weight: Tensor | None = None, + eps: _float | None = None, +) -> Tensor: ... +@overload +def rnn_relu( + data: Tensor, + batch_sizes: Tensor, + hx: Tensor, + params: tuple[Tensor, ...] | list[Tensor], + has_biases: _bool, + num_layers: _int, + dropout: _float, + train: _bool, + bidirectional: _bool, +) -> tuple[Tensor, Tensor]: ... +@overload +def rnn_relu( + input: Tensor, + hx: Tensor, + params: tuple[Tensor, ...] | list[Tensor], + has_biases: _bool, + num_layers: _int, + dropout: _float, + train: _bool, + bidirectional: _bool, + batch_first: _bool, +) -> tuple[Tensor, Tensor]: ... +def rnn_relu_cell( + input: Tensor, + hx: Tensor, + w_ih: Tensor, + w_hh: Tensor, + b_ih: Tensor | None = None, + b_hh: Tensor | None = None, +) -> Tensor: ... +@overload +def rnn_tanh( + data: Tensor, + batch_sizes: Tensor, + hx: Tensor, + params: tuple[Tensor, ...] | list[Tensor], + has_biases: _bool, + num_layers: _int, + dropout: _float, + train: _bool, + bidirectional: _bool, +) -> tuple[Tensor, Tensor]: ... +@overload +def rnn_tanh( + input: Tensor, + hx: Tensor, + params: tuple[Tensor, ...] | list[Tensor], + has_biases: _bool, + num_layers: _int, + dropout: _float, + train: _bool, + bidirectional: _bool, + batch_first: _bool, +) -> tuple[Tensor, Tensor]: ... +def rnn_tanh_cell( + input: Tensor, + hx: Tensor, + w_ih: Tensor, + w_hh: Tensor, + b_ih: Tensor | None = None, + b_hh: Tensor | None = None, +) -> Tensor: ... +def roll( + input: Tensor, + shifts: _int | SymInt | Sequence[_int | SymInt], + dims: _int | _size = (), +) -> Tensor: + r""" + roll(input, shifts, dims=None) -> Tensor + + Roll the tensor :attr:`input` along the given dimension(s). Elements that are + shifted beyond the last position are re-introduced at the first position. If + :attr:`dims` is `None`, the tensor will be flattened before rolling and then + restored to the original shape. + + Args: + input (Tensor): the input tensor. + shifts (int or tuple of ints): The number of places by which the elements + of the tensor are shifted. If shifts is a tuple, dims must be a tuple of + the same size, and each dimension will be rolled by the corresponding + value + dims (int or tuple of ints): Axis along which to roll + + Example:: + + >>> x = torch.tensor([1, 2, 3, 4, 5, 6, 7, 8]).view(4, 2) + >>> x + tensor([[1, 2], + [3, 4], + [5, 6], + [7, 8]]) + >>> torch.roll(x, 1) + tensor([[8, 1], + [2, 3], + [4, 5], + [6, 7]]) + >>> torch.roll(x, 1, 0) + tensor([[7, 8], + [1, 2], + [3, 4], + [5, 6]]) + >>> torch.roll(x, -1, 0) + tensor([[3, 4], + [5, 6], + [7, 8], + [1, 2]]) + >>> torch.roll(x, shifts=(2, 1), dims=(0, 1)) + tensor([[6, 5], + [8, 7], + [2, 1], + [4, 3]]) + """ + +def rot90(input: Tensor, k: _int = 1, dims: _size = (0, 1)) -> Tensor: + r""" + rot90(input, k=1, dims=(0, 1)) -> Tensor + + Rotate an n-D tensor by 90 degrees in the plane specified by dims axis. + Rotation direction is from the first towards the second axis if k > 0, and from the second towards the first for k < 0. + + Args: + input (Tensor): the input tensor. + k (int): number of times to rotate. Default value is 1 + dims (a list or tuple): axis to rotate. Default value is [0, 1] + + Example:: + + >>> x = torch.arange(4).view(2, 2) + >>> x + tensor([[0, 1], + [2, 3]]) + >>> torch.rot90(x, 1, [0, 1]) + tensor([[1, 3], + [0, 2]]) + + >>> x = torch.arange(8).view(2, 2, 2) + >>> x + tensor([[[0, 1], + [2, 3]], + + [[4, 5], + [6, 7]]]) + >>> torch.rot90(x, 1, [1, 2]) + tensor([[[1, 3], + [0, 2]], + + [[5, 7], + [4, 6]]]) + """ + +@overload +def round(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + round(input, *, decimals=0, out=None) -> Tensor + + Rounds elements of :attr:`input` to the nearest integer. + + For integer inputs, follows the array-api convention of returning a + copy of the input tensor. + The return type of output is same as that of input's dtype. + + .. note:: + This function implements the "round half to even" to + break ties when a number is equidistant from two + integers (e.g. `round(2.5)` is 2). + + When the :attr:\`decimals\` argument is specified the + algorithm used is similar to NumPy's `around`. This + algorithm is fast but inexact and it can easily + overflow for low precision dtypes. + Eg. `round(tensor([10000], dtype=torch.float16), decimals=3)` is `inf`. + + .. seealso:: + :func:`torch.ceil`, which rounds up. + :func:`torch.floor`, which rounds down. + :func:`torch.trunc`, which rounds towards zero. + + Args: + input (Tensor): the input tensor. + decimals (int): Number of decimal places to round to (default: 0). + If decimals is negative, it specifies the number of positions + to the left of the decimal point. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.round(torch.tensor((4.7, -2.3, 9.1, -7.7))) + tensor([ 5., -2., 9., -8.]) + + >>> # Values equidistant from two integers are rounded towards the + >>> # the nearest even value (zero is treated as even) + >>> torch.round(torch.tensor([-0.5, 0.5, 1.5, 2.5])) + tensor([-0., 0., 2., 2.]) + + >>> # A positive decimals argument rounds to the to that decimal place + >>> torch.round(torch.tensor([0.1234567]), decimals=3) + tensor([0.1230]) + + >>> # A negative decimals argument rounds to the left of the decimal + >>> torch.round(torch.tensor([1200.1234567]), decimals=-3) + tensor([1000.]) + """ + +@overload +def round( + input: Tensor, + *, + decimals: _int, + out: Tensor | None = None, +) -> Tensor: + r""" + round(input, *, decimals=0, out=None) -> Tensor + + Rounds elements of :attr:`input` to the nearest integer. + + For integer inputs, follows the array-api convention of returning a + copy of the input tensor. + The return type of output is same as that of input's dtype. + + .. note:: + This function implements the "round half to even" to + break ties when a number is equidistant from two + integers (e.g. `round(2.5)` is 2). + + When the :attr:\`decimals\` argument is specified the + algorithm used is similar to NumPy's `around`. This + algorithm is fast but inexact and it can easily + overflow for low precision dtypes. + Eg. `round(tensor([10000], dtype=torch.float16), decimals=3)` is `inf`. + + .. seealso:: + :func:`torch.ceil`, which rounds up. + :func:`torch.floor`, which rounds down. + :func:`torch.trunc`, which rounds towards zero. + + Args: + input (Tensor): the input tensor. + decimals (int): Number of decimal places to round to (default: 0). + If decimals is negative, it specifies the number of positions + to the left of the decimal point. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> torch.round(torch.tensor((4.7, -2.3, 9.1, -7.7))) + tensor([ 5., -2., 9., -8.]) + + >>> # Values equidistant from two integers are rounded towards the + >>> # the nearest even value (zero is treated as even) + >>> torch.round(torch.tensor([-0.5, 0.5, 1.5, 2.5])) + tensor([-0., 0., 2., 2.]) + + >>> # A positive decimals argument rounds to the to that decimal place + >>> torch.round(torch.tensor([0.1234567]), decimals=3) + tensor([0.1230]) + + >>> # A negative decimals argument rounds to the left of the decimal + >>> torch.round(torch.tensor([1200.1234567]), decimals=-3) + tensor([1000.]) + """ + +@overload +def round_(input: Tensor) -> Tensor: ... +@overload +def round_(input: Tensor, *, decimals: _int) -> Tensor: ... +def row_indices_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: ... +def row_stack( + tensors: tuple[Tensor, ...] | list[Tensor], + *, + out: Tensor | None = None, +) -> Tensor: + r""" + row_stack(tensors, *, out=None) -> Tensor + + Alias of :func:`torch.vstack`. + """ + +def rrelu( + input: Tensor, + lower: Number | _complex = 0.125, + upper: Number | _complex = 0.3333333333333333, + training: _bool = False, + generator: Generator | None = None, +) -> Tensor: ... +def rrelu_( + input: Tensor, + lower: Number | _complex = 0.125, + upper: Number | _complex = 0.3333333333333333, + training: _bool = False, + generator: Generator | None = None, +) -> Tensor: ... +def rsqrt(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + rsqrt(input, *, out=None) -> Tensor + + Returns a new tensor with the reciprocal of the square-root of each of + the elements of :attr:`input`. + + .. math:: + \text{out}_{i} = \frac{1}{\sqrt{\text{input}_{i}}} + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-0.0370, 0.2970, 1.5420, -0.9105]) + >>> torch.rsqrt(a) + tensor([ nan, 1.8351, 0.8053, nan]) + """ + +def rsqrt_(input: Tensor) -> Tensor: ... +@overload +def rsub( + input: Tensor, + other: Tensor, + *, + alpha: Number | _complex = 1, +) -> Tensor: ... +@overload +def rsub( + input: Tensor, + other: Number | _complex, + alpha: Number | _complex = 1, +) -> Tensor: ... +def saddmm( + input: Tensor, + mat1: Tensor, + mat2: Tensor, + *, + beta: Number = 1, + alpha: Number = 1, + out: Tensor | None = None, +) -> Tensor: ... +def scalar_tensor( + s: Number | _complex, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: ... +@overload +def scatter( + input: Tensor, + dim: _int, + index: Tensor, + src: Tensor, + *, + reduce: str, + out: Tensor | None = None, +) -> Tensor: + r""" + scatter(input, dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_` + """ + +@overload +def scatter( + input: Tensor, + dim: _int, + index: Tensor, + src: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + scatter(input, dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_` + """ + +@overload +def scatter( + input: Tensor, + dim: _int, + index: Tensor, + value: Number | _complex, + *, + reduce: str, + out: Tensor | None = None, +) -> Tensor: + r""" + scatter(input, dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_` + """ + +@overload +def scatter( + input: Tensor, + dim: str | EllipsisType | None, + index: Tensor, + src: Tensor, +) -> Tensor: + r""" + scatter(input, dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_` + """ + +@overload +def scatter( + input: Tensor, + dim: _int, + index: Tensor, + value: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + scatter(input, dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_` + """ + +@overload +def scatter( + input: Tensor, + dim: str | EllipsisType | None, + index: Tensor, + value: Number | _complex, +) -> Tensor: + r""" + scatter(input, dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_` + """ + +@overload +def scatter_add( + input: Tensor, + dim: _int, + index: Tensor, + src: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + scatter_add(input, dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_add_` + """ + +@overload +def scatter_add( + input: Tensor, + dim: str | EllipsisType | None, + index: Tensor, + src: Tensor, +) -> Tensor: + r""" + scatter_add(input, dim, index, src) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_add_` + """ + +def scatter_reduce( + input: Tensor, + dim: _int, + index: Tensor, + src: Tensor, + reduce: str, + *, + include_self: _bool = True, + out: Tensor | None = None, +) -> Tensor: + r""" + scatter_reduce(input, dim, index, src, reduce, *, include_self=True) -> Tensor + + Out-of-place version of :meth:`torch.Tensor.scatter_reduce_` + """ + +@overload +def searchsorted( + sorted_sequence: Tensor, + input: Tensor, + *, + out_int32: _bool = False, + right: _bool = False, + side: str | None = None, + sorter: Tensor | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + searchsorted(sorted_sequence, values, *, out_int32=False, right=False, side=None, out=None, sorter=None) -> Tensor + + Find the indices from the *innermost* dimension of :attr:`sorted_sequence` such that, if the + corresponding values in :attr:`values` were inserted before the indices, when sorted, the order + of the corresponding *innermost* dimension within :attr:`sorted_sequence` would be preserved. + Return a new tensor with the same size as :attr:`values`. More formally, + the returned index satisfies the following rules: + + .. list-table:: + :widths: 12 10 78 + :header-rows: 1 + + * - :attr:`sorted_sequence` + - :attr:`right` + - *returned index satisfies* + * - 1-D + - False + - ``sorted_sequence[i-1] < values[m][n]...[l][x] <= sorted_sequence[i]`` + * - 1-D + - True + - ``sorted_sequence[i-1] <= values[m][n]...[l][x] < sorted_sequence[i]`` + * - N-D + - False + - ``sorted_sequence[m][n]...[l][i-1] < values[m][n]...[l][x] <= sorted_sequence[m][n]...[l][i]`` + * - N-D + - True + - ``sorted_sequence[m][n]...[l][i-1] <= values[m][n]...[l][x] < sorted_sequence[m][n]...[l][i]`` + + Args: + sorted_sequence (Tensor): N-D or 1-D tensor, containing monotonically increasing sequence on the *innermost* + dimension unless :attr:`sorter` is provided, in which case the sequence does not + need to be sorted + values (Tensor or Scalar): N-D tensor or a Scalar containing the search value(s). + + Keyword args: + out_int32 (bool, optional): indicate the output data type. torch.int32 if True, torch.int64 otherwise. + Default value is False, i.e. default output data type is torch.int64. + right (bool, optional): if False, return the first suitable location that is found. If True, return the + last such index. If no suitable index found, return 0 for non-numerical value + (eg. nan, inf) or the size of *innermost* dimension within :attr:`sorted_sequence` + (one pass the last index of the *innermost* dimension). In other words, if False, + gets the lower bound index for each value in :attr:`values` on the corresponding + *innermost* dimension of the :attr:`sorted_sequence`. If True, gets the upper + bound index instead. Default value is False. :attr:`side` does the same and is + preferred. It will error if :attr:`side` is set to "left" while this is True. + side (str, optional): the same as :attr:`right` but preferred. "left" corresponds to False for :attr:`right` + and "right" corresponds to True for :attr:`right`. It will error if this is set to + "left" while :attr:`right` is True. Default value is None. + out (Tensor, optional): the output tensor, must be the same size as :attr:`values` if provided. + sorter (LongTensor, optional): if provided, a tensor matching the shape of the unsorted + :attr:`sorted_sequence` containing a sequence of indices that sort it in the + ascending order on the innermost dimension + + + Example:: + + >>> sorted_sequence = torch.tensor([[1, 3, 5, 7, 9], [2, 4, 6, 8, 10]]) + >>> sorted_sequence + tensor([[ 1, 3, 5, 7, 9], + [ 2, 4, 6, 8, 10]]) + >>> values = torch.tensor([[3, 6, 9], [3, 6, 9]]) + >>> values + tensor([[3, 6, 9], + [3, 6, 9]]) + >>> torch.searchsorted(sorted_sequence, values) + tensor([[1, 3, 4], + [1, 2, 4]]) + >>> torch.searchsorted(sorted_sequence, values, side='right') + tensor([[2, 3, 5], + [1, 3, 4]]) + + >>> sorted_sequence_1d = torch.tensor([1, 3, 5, 7, 9]) + >>> sorted_sequence_1d + tensor([1, 3, 5, 7, 9]) + >>> torch.searchsorted(sorted_sequence_1d, values) + tensor([[1, 3, 4], + [1, 3, 4]]) + """ + +@overload +def searchsorted( + sorted_sequence: Tensor, + self: Number | _complex, + *, + out_int32: _bool = False, + right: _bool = False, + side: str | None = None, + sorter: Tensor | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + searchsorted(sorted_sequence, values, *, out_int32=False, right=False, side=None, out=None, sorter=None) -> Tensor + + Find the indices from the *innermost* dimension of :attr:`sorted_sequence` such that, if the + corresponding values in :attr:`values` were inserted before the indices, when sorted, the order + of the corresponding *innermost* dimension within :attr:`sorted_sequence` would be preserved. + Return a new tensor with the same size as :attr:`values`. More formally, + the returned index satisfies the following rules: + + .. list-table:: + :widths: 12 10 78 + :header-rows: 1 + + * - :attr:`sorted_sequence` + - :attr:`right` + - *returned index satisfies* + * - 1-D + - False + - ``sorted_sequence[i-1] < values[m][n]...[l][x] <= sorted_sequence[i]`` + * - 1-D + - True + - ``sorted_sequence[i-1] <= values[m][n]...[l][x] < sorted_sequence[i]`` + * - N-D + - False + - ``sorted_sequence[m][n]...[l][i-1] < values[m][n]...[l][x] <= sorted_sequence[m][n]...[l][i]`` + * - N-D + - True + - ``sorted_sequence[m][n]...[l][i-1] <= values[m][n]...[l][x] < sorted_sequence[m][n]...[l][i]`` + + Args: + sorted_sequence (Tensor): N-D or 1-D tensor, containing monotonically increasing sequence on the *innermost* + dimension unless :attr:`sorter` is provided, in which case the sequence does not + need to be sorted + values (Tensor or Scalar): N-D tensor or a Scalar containing the search value(s). + + Keyword args: + out_int32 (bool, optional): indicate the output data type. torch.int32 if True, torch.int64 otherwise. + Default value is False, i.e. default output data type is torch.int64. + right (bool, optional): if False, return the first suitable location that is found. If True, return the + last such index. If no suitable index found, return 0 for non-numerical value + (eg. nan, inf) or the size of *innermost* dimension within :attr:`sorted_sequence` + (one pass the last index of the *innermost* dimension). In other words, if False, + gets the lower bound index for each value in :attr:`values` on the corresponding + *innermost* dimension of the :attr:`sorted_sequence`. If True, gets the upper + bound index instead. Default value is False. :attr:`side` does the same and is + preferred. It will error if :attr:`side` is set to "left" while this is True. + side (str, optional): the same as :attr:`right` but preferred. "left" corresponds to False for :attr:`right` + and "right" corresponds to True for :attr:`right`. It will error if this is set to + "left" while :attr:`right` is True. Default value is None. + out (Tensor, optional): the output tensor, must be the same size as :attr:`values` if provided. + sorter (LongTensor, optional): if provided, a tensor matching the shape of the unsorted + :attr:`sorted_sequence` containing a sequence of indices that sort it in the + ascending order on the innermost dimension + + + Example:: + + >>> sorted_sequence = torch.tensor([[1, 3, 5, 7, 9], [2, 4, 6, 8, 10]]) + >>> sorted_sequence + tensor([[ 1, 3, 5, 7, 9], + [ 2, 4, 6, 8, 10]]) + >>> values = torch.tensor([[3, 6, 9], [3, 6, 9]]) + >>> values + tensor([[3, 6, 9], + [3, 6, 9]]) + >>> torch.searchsorted(sorted_sequence, values) + tensor([[1, 3, 4], + [1, 2, 4]]) + >>> torch.searchsorted(sorted_sequence, values, side='right') + tensor([[2, 3, 5], + [1, 3, 4]]) + + >>> sorted_sequence_1d = torch.tensor([1, 3, 5, 7, 9]) + >>> sorted_sequence_1d + tensor([1, 3, 5, 7, 9]) + >>> torch.searchsorted(sorted_sequence_1d, values) + tensor([[1, 3, 4], + [1, 3, 4]]) + """ + +def segment_reduce( + data: Tensor, + reduce: str, + *, + lengths: Tensor | None = None, + indices: Tensor | None = None, + offsets: Tensor | None = None, + axis: _int = 0, + unsafe: _bool = False, + initial: Number | _complex | None = None, +) -> Tensor: + r""" + segment_reduce(data: Tensor, reduce: str, *, lengths: Tensor | None = None, indices: Tensor | None = None, offsets: Tensor | None = None, axis: _int = 0, unsafe: _bool = False, initial: Number | _complex | None = None) -> Tensor # noqa: B950 + + Perform a segment reduction operation on the input tensor along the specified axis. + + Args: + data (Tensor): The input tensor on which the segment reduction operation will be performed. + reduce (str): The type of reduction operation. Supported values are ``sum``, ``mean``, ``max``, ``min``, ``prod``. + + Keyword args: + lengths (Tensor, optional): Length of each segment. Default: ``None``. + offsets (Tensor, optional): Offset of each segment. Default: ``None``. + axis (int, optional): The axis perform reduction. Default: ``0``. + unsafe (bool, optional): Skip validation If `True`. Default: ``False``. + initial (Number, optional): The initial value for the reduction operation. Default: ``None``. + + Example:: + + >>> data = torch.tensor([[1, 2, 3, 4],[5, 6, 7, 8],[9, 10, 11, 12]], dtype=torch.float32, device='cuda') + >>> lengths = torch.tensor([2, 1], device='cuda') + >>> torch.segment_reduce(data, 'max', lengths=lengths) + tensor([[ 5., 6., 7., 8.], + [ 9., 10., 11., 12.]], device='cuda:0') + """ + +@overload +def select(input: Tensor, dim: _int, index: _int | SymInt) -> Tensor: + r""" + select(input, dim, index) -> Tensor + + Slices the :attr:`input` tensor along the selected dimension at the given index. + This function returns a view of the original tensor with the given dimension removed. + + .. note:: If :attr:`input` is a sparse tensor and returning a view of + the tensor is not possible, a RuntimeError exception is + raised. In this is the case, consider using + :func:`torch.select_copy` function. + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to slice + index (int): the index to select with + + .. note:: + + :meth:`select` is equivalent to slicing. For example, + ``tensor.select(0, index)`` is equivalent to ``tensor[index]`` and + ``tensor.select(2, index)`` is equivalent to ``tensor[:,:,index]``. + """ + +@overload +def select( + input: Tensor, + dim: str | EllipsisType | None, + index: _int, +) -> Tensor: + r""" + select(input, dim, index) -> Tensor + + Slices the :attr:`input` tensor along the selected dimension at the given index. + This function returns a view of the original tensor with the given dimension removed. + + .. note:: If :attr:`input` is a sparse tensor and returning a view of + the tensor is not possible, a RuntimeError exception is + raised. In this is the case, consider using + :func:`torch.select_copy` function. + + Args: + input (Tensor): the input tensor. + dim (int): the dimension to slice + index (int): the index to select with + + .. note:: + + :meth:`select` is equivalent to slicing. For example, + ``tensor.select(0, index)`` is equivalent to ``tensor[index]`` and + ``tensor.select(2, index)`` is equivalent to ``tensor[:,:,index]``. + """ + +def select_copy( + input: Tensor, + dim: _int, + index: _int | SymInt, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.select`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def select_scatter( + input: Tensor, + src: Tensor, + dim: _int, + index: _int | SymInt, +) -> Tensor: + r""" + select_scatter(input, src, dim, index) -> Tensor + + Embeds the values of the :attr:`src` tensor into :attr:`input` at the given index. + This function returns a tensor with fresh storage; it does not create a view. + + + Args: + input (Tensor): the input tensor. + src (Tensor): The tensor to embed into :attr:`input` + dim (int): the dimension to insert the slice into. + index (int): the index to select with + + .. note:: + + :attr:`src` must be of the proper size in order to be embedded + into :attr:`input`. Specifically, it should have the same shape as + ``torch.select(input, dim, index)`` + + Example:: + + >>> a = torch.zeros(2, 2) + >>> b = torch.ones(2) + >>> a.select_scatter(b, 0, 0) + tensor([[1., 1.], + [0., 0.]]) + """ + +def selu(input: Tensor) -> Tensor: ... +def selu_(input: Tensor) -> Tensor: ... +def set_flush_denormal(mode: _bool) -> _bool: + r""" + set_flush_denormal(mode) -> bool + + Disables denormal floating numbers on CPU. + + Returns ``True`` if your system supports flushing denormal numbers and it + successfully configures flush denormal mode. :meth:`~torch.set_flush_denormal` + is supported on x86 architectures supporting SSE3 and AArch64 architecture. + + Args: + mode (bool): Controls whether to enable flush denormal mode or not + + Example:: + + >>> torch.set_flush_denormal(True) + True + >>> torch.tensor([1e-323], dtype=torch.float64) + tensor([ 0.], dtype=torch.float64) + >>> torch.set_flush_denormal(False) + True + >>> torch.tensor([1e-323], dtype=torch.float64) + tensor(9.88131e-324 * + [ 1.0000], dtype=torch.float64) + """ + +def set_num_interop_threads(num: _int) -> None: + r""" + set_num_interop_threads(int) + + Sets the number of threads used for interop parallelism + (e.g. in JIT interpreter) on CPU. + + .. warning:: + Can only be called once and before any inter-op parallel work + is started (e.g. JIT execution). + """ + +def set_num_threads(num: _int) -> None: + r""" + set_num_threads(int) + + Sets the number of threads used for intraop parallelism on CPU. + + .. warning:: + To ensure that the correct number of threads is used, set_num_threads + must be called before running eager, JIT or autograd code. + """ + +def sgn(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + sgn(input, *, out=None) -> Tensor + + This function is an extension of torch.sign() to complex tensors. + It computes a new tensor whose elements have + the same angles as the corresponding elements of :attr:`input` and + absolute values (i.e. magnitudes) of one for complex tensors and + is equivalent to torch.sign() for non-complex tensors. + + .. math:: + \text{out}_{i} = \begin{cases} + 0 & |\text{{input}}_i| == 0 \\ + \frac{{\text{{input}}_i}}{|{\text{{input}}_i}|} & \text{otherwise} + \end{cases} + + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.tensor([3+4j, 7-24j, 0, 1+2j]) + >>> t.sgn() + tensor([0.6000+0.8000j, 0.2800-0.9600j, 0.0000+0.0000j, 0.4472+0.8944j]) + """ + +def sigmoid(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + sigmoid(input, *, out=None) -> Tensor + + Alias for :func:`torch.special.expit`. + """ + +def sigmoid_(input: Tensor) -> Tensor: ... +def sign(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + sign(input, *, out=None) -> Tensor + + Returns a new tensor with the signs of the elements of :attr:`input`. + + .. math:: + \text{out}_{i} = \operatorname{sgn}(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([0.7, -1.2, 0., 2.3]) + >>> a + tensor([ 0.7000, -1.2000, 0.0000, 2.3000]) + >>> torch.sign(a) + tensor([ 1., -1., 0., 1.]) + """ + +def signbit(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + signbit(input, *, out=None) -> Tensor + + Tests if each element of :attr:`input` has its sign bit set or not. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([0.7, -1.2, 0., 2.3]) + >>> torch.signbit(a) + tensor([ False, True, False, False]) + >>> a = torch.tensor([-0.0, 0.0]) + >>> torch.signbit(a) + tensor([ True, False]) + + .. note:: + signbit handles signed zeros, so negative zero (-0) returns True. + """ + +def sin(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + sin(input, *, out=None) -> Tensor + + Returns a new tensor with the sine of the elements in the :attr:`input` tensor, + where each value in this input tensor is in radians. + + .. math:: + \text{out}_{i} = \sin(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-0.5461, 0.1347, -2.7266, -0.2746]) + >>> torch.sin(a) + tensor([-0.5194, 0.1343, -0.4032, -0.2711]) + """ + +def sin_(input: Tensor) -> Tensor: ... +def sinc(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + sinc(input, *, out=None) -> Tensor + + Alias for :func:`torch.special.sinc`. + """ + +def sinc_(input: Tensor) -> Tensor: ... +def sinh(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + sinh(input, *, out=None) -> Tensor + + Returns a new tensor with the hyperbolic sine of the elements of + :attr:`input`. + + .. math:: + \text{out}_{i} = \sinh(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.5380, -0.8632, -0.1265, 0.9399]) + >>> torch.sinh(a) + tensor([ 0.5644, -0.9744, -0.1268, 1.0845]) + + .. note:: + When :attr:`input` is on the CPU, the implementation of torch.sinh may use + the Sleef library, which rounds very large results to infinity or negative + infinity. See `here `_ for details. + """ + +def sinh_(input: Tensor) -> Tensor: ... +def slice_copy( + input: Tensor, + dim: _int = 0, + start: _int | SymInt | None = None, + end: _int | SymInt | None = None, + step: _int | SymInt = 1, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.slice`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def slice_inverse( + input: Tensor, + src: Tensor, + dim: _int = 0, + start: _int | SymInt | None = None, + end: _int | SymInt | None = None, + step: _int | SymInt = 1, +) -> Tensor: ... +def slice_scatter( + input: Tensor, + src: Tensor, + dim: _int = 0, + start: _int | SymInt | None = None, + end: _int | SymInt | None = None, + step: _int | SymInt = 1, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + slice_scatter(input, src, dim=0, start=None, end=None, step=1) -> Tensor + + Embeds the values of the :attr:`src` tensor into :attr:`input` at the given + dimension. + This function returns a tensor with fresh storage; it does not create a view. + + + Args: + input (Tensor): the input tensor. + src (Tensor): The tensor to embed into :attr:`input` + dim (int): the dimension to insert the slice into + start (Optional[int]): the start index of where to insert the slice + end (Optional[int]): the end index of where to insert the slice + step (int): the how many elements to skip in + + Example:: + + >>> a = torch.zeros(8, 8) + >>> b = torch.ones(2, 8) + >>> a.slice_scatter(b, start=6) + tensor([[0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0.], + [1., 1., 1., 1., 1., 1., 1., 1.], + [1., 1., 1., 1., 1., 1., 1., 1.]]) + + >>> b = torch.ones(8, 2) + >>> a.slice_scatter(b, dim=1, start=2, end=6, step=2) + tensor([[0., 0., 1., 0., 1., 0., 0., 0.], + [0., 0., 1., 0., 1., 0., 0., 0.], + [0., 0., 1., 0., 1., 0., 0., 0.], + [0., 0., 1., 0., 1., 0., 0., 0.], + [0., 0., 1., 0., 1., 0., 0., 0.], + [0., 0., 1., 0., 1., 0., 0., 0.], + [0., 0., 1., 0., 1., 0., 0., 0.], + [0., 0., 1., 0., 1., 0., 0., 0.]]) + """ + +def slogdet( + input: Tensor, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.slogdet: + r""" + slogdet(input) -> (Tensor, Tensor) + + Alias for :func:`torch.linalg.slogdet` + """ + +def smm(input: Tensor, mat2: Tensor) -> Tensor: + r""" + smm(input, mat) -> Tensor + + Performs a matrix multiplication of the sparse matrix :attr:`input` + with the dense matrix :attr:`mat`. + + Args: + input (Tensor): a sparse matrix to be matrix multiplied + mat (Tensor): a dense matrix to be matrix multiplied + """ + +@overload +def softmax( + input: Tensor, + dim: _int, + dtype: _dtype | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + softmax(input, dim, *, dtype=None) -> Tensor + + Alias for :func:`torch.nn.functional.softmax`. + """ + +@overload +def softmax( + input: Tensor, + dim: str | EllipsisType | None, + *, + dtype: _dtype | None = None, +) -> Tensor: + r""" + softmax(input, dim, *, dtype=None) -> Tensor + + Alias for :func:`torch.nn.functional.softmax`. + """ + +@overload +def sort( + input: Tensor, + *, + stable: _bool | None, + dim: _int = -1, + descending: _bool = False, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.sort: + r""" + sort(input, dim=-1, descending=False, *, stable=False, out=None) -> (Tensor, LongTensor) + + Sorts the elements of the :attr:`input` tensor along a given dimension + in ascending order by value. + + If :attr:`dim` is not given, the last dimension of the `input` is chosen. + + If :attr:`descending` is ``True`` then the elements are sorted in descending + order by value. + + If :attr:`stable` is ``True`` then the sorting routine becomes stable, preserving + the order of equivalent elements. + + A namedtuple of (values, indices) is returned, where the `values` are the + sorted values and `indices` are the indices of the elements in the original + `input` tensor. + + Args: + input (Tensor): the input tensor. + dim (int, optional): the dimension to sort along + descending (bool, optional): controls the sorting order (ascending or descending) + + Keyword args: + stable (bool, optional): makes the sorting routine stable, which guarantees that the order + of equivalent elements is preserved. + out (tuple, optional): the output tuple of (`Tensor`, `LongTensor`) that can + be optionally given to be used as output buffers + + Example:: + + >>> x = torch.randn(3, 4) + >>> sorted, indices = torch.sort(x) + >>> sorted + tensor([[-0.2162, 0.0608, 0.6719, 2.3332], + [-0.5793, 0.0061, 0.6058, 0.9497], + [-0.5071, 0.3343, 0.9553, 1.0960]]) + >>> indices + tensor([[ 1, 0, 2, 3], + [ 3, 1, 0, 2], + [ 0, 3, 1, 2]]) + + >>> sorted, indices = torch.sort(x, 0) + >>> sorted + tensor([[-0.5071, -0.2162, 0.6719, -0.5793], + [ 0.0608, 0.0061, 0.9497, 0.3343], + [ 0.6058, 0.9553, 1.0960, 2.3332]]) + >>> indices + tensor([[ 2, 0, 0, 1], + [ 0, 1, 1, 2], + [ 1, 2, 2, 0]]) + >>> x = torch.tensor([0, 1] * 9) + >>> x.sort() + torch.return_types.sort( + values=tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + indices=tensor([ 2, 16, 4, 6, 14, 8, 0, 10, 12, 9, 17, 15, 13, 11, 7, 5, 3, 1])) + >>> x.sort(stable=True) + torch.return_types.sort( + values=tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + indices=tensor([ 0, 2, 4, 6, 8, 10, 12, 14, 16, 1, 3, 5, 7, 9, 11, 13, 15, 17])) + """ + +@overload +def sort( + input: Tensor, + dim: _int = -1, + descending: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.sort: + r""" + sort(input, dim=-1, descending=False, *, stable=False, out=None) -> (Tensor, LongTensor) + + Sorts the elements of the :attr:`input` tensor along a given dimension + in ascending order by value. + + If :attr:`dim` is not given, the last dimension of the `input` is chosen. + + If :attr:`descending` is ``True`` then the elements are sorted in descending + order by value. + + If :attr:`stable` is ``True`` then the sorting routine becomes stable, preserving + the order of equivalent elements. + + A namedtuple of (values, indices) is returned, where the `values` are the + sorted values and `indices` are the indices of the elements in the original + `input` tensor. + + Args: + input (Tensor): the input tensor. + dim (int, optional): the dimension to sort along + descending (bool, optional): controls the sorting order (ascending or descending) + + Keyword args: + stable (bool, optional): makes the sorting routine stable, which guarantees that the order + of equivalent elements is preserved. + out (tuple, optional): the output tuple of (`Tensor`, `LongTensor`) that can + be optionally given to be used as output buffers + + Example:: + + >>> x = torch.randn(3, 4) + >>> sorted, indices = torch.sort(x) + >>> sorted + tensor([[-0.2162, 0.0608, 0.6719, 2.3332], + [-0.5793, 0.0061, 0.6058, 0.9497], + [-0.5071, 0.3343, 0.9553, 1.0960]]) + >>> indices + tensor([[ 1, 0, 2, 3], + [ 3, 1, 0, 2], + [ 0, 3, 1, 2]]) + + >>> sorted, indices = torch.sort(x, 0) + >>> sorted + tensor([[-0.5071, -0.2162, 0.6719, -0.5793], + [ 0.0608, 0.0061, 0.9497, 0.3343], + [ 0.6058, 0.9553, 1.0960, 2.3332]]) + >>> indices + tensor([[ 2, 0, 0, 1], + [ 0, 1, 1, 2], + [ 1, 2, 2, 0]]) + >>> x = torch.tensor([0, 1] * 9) + >>> x.sort() + torch.return_types.sort( + values=tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + indices=tensor([ 2, 16, 4, 6, 14, 8, 0, 10, 12, 9, 17, 15, 13, 11, 7, 5, 3, 1])) + >>> x.sort(stable=True) + torch.return_types.sort( + values=tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + indices=tensor([ 0, 2, 4, 6, 8, 10, 12, 14, 16, 1, 3, 5, 7, 9, 11, 13, 15, 17])) + """ + +@overload +def sort( + input: Tensor, + *, + stable: _bool | None, + dim: str | EllipsisType | None, + descending: _bool = False, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.sort: + r""" + sort(input, dim=-1, descending=False, *, stable=False, out=None) -> (Tensor, LongTensor) + + Sorts the elements of the :attr:`input` tensor along a given dimension + in ascending order by value. + + If :attr:`dim` is not given, the last dimension of the `input` is chosen. + + If :attr:`descending` is ``True`` then the elements are sorted in descending + order by value. + + If :attr:`stable` is ``True`` then the sorting routine becomes stable, preserving + the order of equivalent elements. + + A namedtuple of (values, indices) is returned, where the `values` are the + sorted values and `indices` are the indices of the elements in the original + `input` tensor. + + Args: + input (Tensor): the input tensor. + dim (int, optional): the dimension to sort along + descending (bool, optional): controls the sorting order (ascending or descending) + + Keyword args: + stable (bool, optional): makes the sorting routine stable, which guarantees that the order + of equivalent elements is preserved. + out (tuple, optional): the output tuple of (`Tensor`, `LongTensor`) that can + be optionally given to be used as output buffers + + Example:: + + >>> x = torch.randn(3, 4) + >>> sorted, indices = torch.sort(x) + >>> sorted + tensor([[-0.2162, 0.0608, 0.6719, 2.3332], + [-0.5793, 0.0061, 0.6058, 0.9497], + [-0.5071, 0.3343, 0.9553, 1.0960]]) + >>> indices + tensor([[ 1, 0, 2, 3], + [ 3, 1, 0, 2], + [ 0, 3, 1, 2]]) + + >>> sorted, indices = torch.sort(x, 0) + >>> sorted + tensor([[-0.5071, -0.2162, 0.6719, -0.5793], + [ 0.0608, 0.0061, 0.9497, 0.3343], + [ 0.6058, 0.9553, 1.0960, 2.3332]]) + >>> indices + tensor([[ 2, 0, 0, 1], + [ 0, 1, 1, 2], + [ 1, 2, 2, 0]]) + >>> x = torch.tensor([0, 1] * 9) + >>> x.sort() + torch.return_types.sort( + values=tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + indices=tensor([ 2, 16, 4, 6, 14, 8, 0, 10, 12, 9, 17, 15, 13, 11, 7, 5, 3, 1])) + >>> x.sort(stable=True) + torch.return_types.sort( + values=tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + indices=tensor([ 0, 2, 4, 6, 8, 10, 12, 14, 16, 1, 3, 5, 7, 9, 11, 13, 15, 17])) + """ + +@overload +def sort( + input: Tensor, + dim: str | EllipsisType | None, + descending: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.sort: + r""" + sort(input, dim=-1, descending=False, *, stable=False, out=None) -> (Tensor, LongTensor) + + Sorts the elements of the :attr:`input` tensor along a given dimension + in ascending order by value. + + If :attr:`dim` is not given, the last dimension of the `input` is chosen. + + If :attr:`descending` is ``True`` then the elements are sorted in descending + order by value. + + If :attr:`stable` is ``True`` then the sorting routine becomes stable, preserving + the order of equivalent elements. + + A namedtuple of (values, indices) is returned, where the `values` are the + sorted values and `indices` are the indices of the elements in the original + `input` tensor. + + Args: + input (Tensor): the input tensor. + dim (int, optional): the dimension to sort along + descending (bool, optional): controls the sorting order (ascending or descending) + + Keyword args: + stable (bool, optional): makes the sorting routine stable, which guarantees that the order + of equivalent elements is preserved. + out (tuple, optional): the output tuple of (`Tensor`, `LongTensor`) that can + be optionally given to be used as output buffers + + Example:: + + >>> x = torch.randn(3, 4) + >>> sorted, indices = torch.sort(x) + >>> sorted + tensor([[-0.2162, 0.0608, 0.6719, 2.3332], + [-0.5793, 0.0061, 0.6058, 0.9497], + [-0.5071, 0.3343, 0.9553, 1.0960]]) + >>> indices + tensor([[ 1, 0, 2, 3], + [ 3, 1, 0, 2], + [ 0, 3, 1, 2]]) + + >>> sorted, indices = torch.sort(x, 0) + >>> sorted + tensor([[-0.5071, -0.2162, 0.6719, -0.5793], + [ 0.0608, 0.0061, 0.9497, 0.3343], + [ 0.6058, 0.9553, 1.0960, 2.3332]]) + >>> indices + tensor([[ 2, 0, 0, 1], + [ 0, 1, 1, 2], + [ 1, 2, 2, 0]]) + >>> x = torch.tensor([0, 1] * 9) + >>> x.sort() + torch.return_types.sort( + values=tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + indices=tensor([ 2, 16, 4, 6, 14, 8, 0, 10, 12, 9, 17, 15, 13, 11, 7, 5, 3, 1])) + >>> x.sort(stable=True) + torch.return_types.sort( + values=tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + indices=tensor([ 0, 2, 4, 6, 8, 10, 12, 14, 16, 1, 3, 5, 7, 9, 11, 13, 15, 17])) + """ + +def sparse_bsc_tensor( + ccol_indices: Tensor | list, + row_indices: Tensor | list, + values: Tensor | list, + size: _size | None = None, + *, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + check_invariants: _bool | None = None, +) -> Tensor: + r""" + sparse_bsc_tensor(ccol_indices, row_indices, values, size=None, *, dtype=None, device=None, pin_memory=False, requires_grad=False, check_invariants=None) -> Tensor + + Constructs a :ref:`sparse tensor in BSC (Block Compressed Sparse + Column)) ` with specified 2-dimensional blocks at the + given :attr:`ccol_indices` and :attr:`row_indices`. Sparse matrix + multiplication operations in BSC format are typically faster than that + for sparse tensors in COO format. Make you have a look at :ref:`the + note on the data type of the indices `. + + .. note:: + + If the ``device`` argument is not specified the device of the given + :attr:`values` and indices tensor(s) must match. If, however, the + argument is specified the input Tensors will be converted to the + given device and in turn determine the device of the constructed + sparse tensor. + + Args: + ccol_indices (array_like): (B+1)-dimensional array of size + ``(*batchsize, ncolblocks + 1)``. The last element of each + batch is the number of non-zeros. This tensor encodes the + index in values and row_indices depending on where the given + column starts. Each successive number in the tensor subtracted + by the number before it denotes the number of elements in a + given column. + row_indices (array_like): Row block coordinates of each block in + values. (B+1)-dimensional tensor with the same length + as values. + values (array_list): Initial blocks for the tensor. Can be a list, + tuple, NumPy ``ndarray``, and other types that + represents a (1 + 2 + K)-dimensional tensor where ``K`` is the + number of dense dimensions. + size (list, tuple, :class:`torch.Size`, optional): Size of the + sparse tensor: ``(*batchsize, nrows * blocksize[0], ncols * + blocksize[1], *densesize)`` If not provided, the size will be + inferred as the minimum size big enough to hold all non-zero + blocks. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of + returned tensor. Default: if None, infers data type from + :attr:`values`. + device (:class:`torch.device`, optional): the desired device of + returned tensor. Default: if None, uses the current device + for the default tensor type (see + :func:`torch.set_default_device`). :attr:`device` will be + the CPU for CPU tensor types and the current CUDA device for + CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + check_invariants (bool, optional): If sparse tensor invariants are checked. + Default: as returned by :func:`torch.sparse.check_sparse_tensor_invariants.is_enabled`, + initially False. + + Example:: + + >>> ccol_indices = [0, 1, 2] + >>> row_indices = [0, 1] + >>> values = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]] + >>> torch.sparse_bsc_tensor(torch.tensor(ccol_indices, dtype=torch.int64), + ... torch.tensor(row_indices, dtype=torch.int64), + ... torch.tensor(values), dtype=torch.double) + tensor(ccol_indices=tensor([0, 1, 2]), + row_indices=tensor([0, 1]), + values=tensor([[[1., 2.], + [3., 4.]], + [[5., 6.], + [7., 8.]]]), size=(2, 2), nnz=2, dtype=torch.float64, + layout=torch.sparse_bsc) + """ + +def sparse_bsr_tensor( + crow_indices: Tensor | list, + col_indices: Tensor | list, + values: Tensor | list, + size: _size | None = None, + *, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + check_invariants: _bool | None = None, +) -> Tensor: + r""" + sparse_bsr_tensor(crow_indices, col_indices, values, size=None, *, dtype=None, device=None, pin_memory=False, requires_grad=False, check_invariants=None) -> Tensor + + Constructs a :ref:`sparse tensor in BSR (Block Compressed Sparse Row)) + ` with specified 2-dimensional blocks at the given + :attr:`crow_indices` and :attr:`col_indices`. Sparse matrix + multiplication operations in BSR format are typically faster than that + for sparse tensors in COO format. Make you have a look at :ref:`the + note on the data type of the indices `. + + .. note:: + + If the ``device`` argument is not specified the device of the given + :attr:`values` and indices tensor(s) must match. If, however, the + argument is specified the input Tensors will be converted to the + given device and in turn determine the device of the constructed + sparse tensor. + + Args: + crow_indices (array_like): (B+1)-dimensional array of size + ``(*batchsize, nrowblocks + 1)``. The last element of each + batch is the number of non-zeros. This tensor encodes the + block index in values and col_indices depending on where the + given row block starts. Each successive number in the tensor + subtracted by the number before it denotes the number of + blocks in a given row. + col_indices (array_like): Column block coordinates of each block + in values. (B+1)-dimensional tensor with the same length as + values. + values (array_list): Initial values for the tensor. Can be a list, + tuple, NumPy ``ndarray``, scalar, and other types that + represents a (1 + 2 + K)-dimensional tensor where ``K`` is the + number of dense dimensions. + size (list, tuple, :class:`torch.Size`, optional): Size of the + sparse tensor: ``(*batchsize, nrows * blocksize[0], ncols * + blocksize[1], *densesize)`` where ``blocksize == + values.shape[1:3]``. If not provided, the size will be + inferred as the minimum size big enough to hold all non-zero + blocks. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of + returned tensor. Default: if None, infers data type from + :attr:`values`. + device (:class:`torch.device`, optional): the desired device of + returned tensor. Default: if None, uses the current device + for the default tensor type (see + :func:`torch.set_default_device`). :attr:`device` will be + the CPU for CPU tensor types and the current CUDA device for + CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + check_invariants (bool, optional): If sparse tensor invariants are checked. + Default: as returned by :func:`torch.sparse.check_sparse_tensor_invariants.is_enabled`, + initially False. + + Example:: + + >>> crow_indices = [0, 1, 2] + >>> col_indices = [0, 1] + >>> values = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]] + >>> torch.sparse_bsr_tensor(torch.tensor(crow_indices, dtype=torch.int64), + ... torch.tensor(col_indices, dtype=torch.int64), + ... torch.tensor(values), dtype=torch.double) + tensor(crow_indices=tensor([0, 1, 2]), + col_indices=tensor([0, 1]), + values=tensor([[[1., 2.], + [3., 4.]], + [[5., 6.], + [7., 8.]]]), size=(2, 2), nnz=2, dtype=torch.float64, + layout=torch.sparse_bsr) + """ + +def sparse_compressed_tensor( + compressed_indices: Tensor | list, + plain_indices: Tensor | list, + values: Tensor | list, + size: _size | None = None, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + check_invariants: _bool | None = None, +) -> Tensor: + r""" + sparse_compressed_tensor(compressed_indices, plain_indices, values, size=None, *, dtype=None, layout=None, device=None, pin_memory=False, requires_grad=False, check_invariants=None) -> Tensor + + Constructs a :ref:`sparse tensor in Compressed Sparse format - CSR, + CSC, BSR, or BSC - ` with specified values at + the given :attr:`compressed_indices` and :attr:`plain_indices`. Sparse + matrix multiplication operations in Compressed Sparse format are + typically faster than that for sparse tensors in COO format. Make you + have a look at :ref:`the note on the data type of the indices + `. + + .. note:: + + If the ``device`` argument is not specified the device of the given + :attr:`values` and indices tensor(s) must match. If, however, the + argument is specified the input Tensors will be converted to the + given device and in turn determine the device of the constructed + sparse tensor. + + Args: + compressed_indices (array_like): (B+1)-dimensional array of size + ``(*batchsize, compressed_dim_size + 1)``. The last element of + each batch is the number of non-zero elements or blocks. This + tensor encodes the index in ``values`` and ``plain_indices`` + depending on where the given compressed dimension (row or + column) starts. Each successive number in the tensor + subtracted by the number before it denotes the number of + elements or blocks in a given compressed dimension. + plain_indices (array_like): Plain dimension (column or row) + coordinates of each element or block in values. (B+1)-dimensional + tensor with the same length as values. + + values (array_list): Initial values for the tensor. Can be a list, + tuple, NumPy ``ndarray``, scalar, and other types. that + represents a (1+K)-dimensional (for CSR and CSC layouts) or + (1+2+K)-dimensional tensor (for BSR and BSC layouts) where + ``K`` is the number of dense dimensions. + size (list, tuple, :class:`torch.Size`, optional): Size of the + sparse tensor: ``(*batchsize, nrows * blocksize[0], ncols * + blocksize[1], *densesize)`` where ``blocksize[0] == + blocksize[1] == 1`` for CSR and CSC formats. If not provided, + the size will be inferred as the minimum size big enough to + hold all non-zero elements or blocks. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of + returned tensor. Default: if None, infers data type from + :attr:`values`. + layout (:class:`torch.layout`, required): the desired layout of + returned tensor: :attr:`torch.sparse_csr`, + :attr:`torch.sparse_csc`, :attr:`torch.sparse_bsr`, or + :attr:`torch.sparse_bsc`. + device (:class:`torch.device`, optional): the desired device of + returned tensor. Default: if None, uses the current device + for the default tensor type (see + :func:`torch.set_default_device`). :attr:`device` will be + the CPU for CPU tensor types and the current CUDA device for + CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + check_invariants (bool, optional): If sparse tensor invariants are checked. + Default: as returned by :func:`torch.sparse.check_sparse_tensor_invariants.is_enabled`, + initially False. + + Example:: + + >>> compressed_indices = [0, 2, 4] + >>> plain_indices = [0, 1, 0, 1] + >>> values = [1, 2, 3, 4] + >>> torch.sparse_compressed_tensor(torch.tensor(compressed_indices, dtype=torch.int64), + ... torch.tensor(plain_indices, dtype=torch.int64), + ... torch.tensor(values), dtype=torch.double, layout=torch.sparse_csr) + tensor(crow_indices=tensor([0, 2, 4]), + col_indices=tensor([0, 1, 0, 1]), + values=tensor([1., 2., 3., 4.]), size=(2, 2), nnz=4, + dtype=torch.float64, layout=torch.sparse_csr) + """ + +def sparse_coo_tensor( + indices: Tensor, + values: Tensor | list, + size: _size | None = None, + *, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + check_invariants: _bool | None = None, + is_coalesced: _bool | None = None, +) -> Tensor: + r""" + sparse_coo_tensor(indices, values, size=None, *, dtype=None, device=None, pin_memory=False, requires_grad=False, check_invariants=None, is_coalesced=None) -> Tensor + + Constructs a :ref:`sparse tensor in COO(rdinate) format + ` with specified values at the given + :attr:`indices`. + + .. note:: + + This function returns an :ref:`uncoalesced tensor + ` when :attr:`is_coalesced` is + unspecified or ``None``. + + .. note:: + + If the ``device`` argument is not specified the device of the given + :attr:`values` and indices tensor(s) must match. If, however, the + argument is specified the input Tensors will be converted to the + given device and in turn determine the device of the constructed + sparse tensor. + + Args: + indices (array_like): Initial data for the tensor. Can be a list, tuple, + NumPy ``ndarray``, scalar, and other types. Will be cast to a :class:`torch.LongTensor` + internally. The indices are the coordinates of the non-zero values in the matrix, and thus + should be two-dimensional where the first dimension is the number of tensor dimensions and + the second dimension is the number of non-zero values. + values (array_like): Initial values for the tensor. Can be a list, tuple, + NumPy ``ndarray``, scalar, and other types. + size (list, tuple, or :class:`torch.Size`, optional): Size of the sparse tensor. If not + provided the size will be inferred as the minimum size big enough to hold all non-zero + elements. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if None, infers data type from :attr:`values`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if None, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + check_invariants (bool, optional): If sparse tensor invariants are checked. + Default: as returned by :func:`torch.sparse.check_sparse_tensor_invariants.is_enabled`, + initially False. + is_coalesced (bool, optional): When``True``, the caller is + responsible for providing tensor indices that correspond to a + coalesced tensor. If the :attr:`check_invariants` flag is + False, no error will be raised if the prerequisites are not + met and this will lead to silently incorrect results. To force + coalescion please use :meth:`coalesce` on the resulting + Tensor. + Default: None: except for trivial cases (e.g. nnz < 2) the + resulting Tensor has is_coalesced set to ``False```. + + Example:: + + >>> i = torch.tensor([[0, 1, 1], + ... [2, 0, 2]]) + >>> v = torch.tensor([3, 4, 5], dtype=torch.float32) + >>> torch.sparse_coo_tensor(i, v, [2, 4]) + tensor(indices=tensor([[0, 1, 1], + [2, 0, 2]]), + values=tensor([3., 4., 5.]), + size=(2, 4), nnz=3, layout=torch.sparse_coo) + + >>> torch.sparse_coo_tensor(i, v) # Shape inference + tensor(indices=tensor([[0, 1, 1], + [2, 0, 2]]), + values=tensor([3., 4., 5.]), + size=(2, 3), nnz=3, layout=torch.sparse_coo) + + >>> torch.sparse_coo_tensor(i, v, [2, 4], + ... dtype=torch.float64, + ... device=torch.device('cuda:0')) + tensor(indices=tensor([[0, 1, 1], + [2, 0, 2]]), + values=tensor([3., 4., 5.]), + device='cuda:0', size=(2, 4), nnz=3, dtype=torch.float64, + layout=torch.sparse_coo) + + # Create an empty sparse tensor with the following invariants: + # 1. sparse_dim + dense_dim = len(SparseTensor.shape) + # 2. SparseTensor._indices().shape = (sparse_dim, nnz) + # 3. SparseTensor._values().shape = (nnz, SparseTensor.shape[sparse_dim:]) + # + # For instance, to create an empty sparse tensor with nnz = 0, dense_dim = 0 and + # sparse_dim = 1 (hence indices is a 2D tensor of shape = (1, 0)) + >>> S = torch.sparse_coo_tensor(torch.empty([1, 0]), [], [1]) + tensor(indices=tensor([], size=(1, 0)), + values=tensor([], size=(0,)), + size=(1,), nnz=0, layout=torch.sparse_coo) + + # and to create an empty sparse tensor with nnz = 0, dense_dim = 1 and + # sparse_dim = 1 + >>> S = torch.sparse_coo_tensor(torch.empty([1, 0]), torch.empty([0, 2]), [1, 2]) + tensor(indices=tensor([], size=(1, 0)), + values=tensor([], size=(0, 2)), + size=(1, 2), nnz=0, layout=torch.sparse_coo) + + .. _torch.sparse: https://pytorch.org/docs/stable/sparse.html + """ + +def sparse_csc_tensor( + ccol_indices: Tensor | list, + row_indices: Tensor | list, + values: Tensor | list, + size: _size | None = None, + *, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + check_invariants: _bool | None = None, +) -> Tensor: + r""" + sparse_csc_tensor(ccol_indices, row_indices, values, size=None, *, dtype=None, device=None, pin_memory=False, requires_grad=False, check_invariants=None) -> Tensor + + Constructs a :ref:`sparse tensor in CSC (Compressed Sparse Column) + ` with specified values at the given + :attr:`ccol_indices` and :attr:`row_indices`. Sparse matrix + multiplication operations in CSC format are typically faster than that + for sparse tensors in COO format. Make you have a look at :ref:`the + note on the data type of the indices `. + + .. note:: + + If the ``device`` argument is not specified the device of the given + :attr:`values` and indices tensor(s) must match. If, however, the + argument is specified the input Tensors will be converted to the + given device and in turn determine the device of the constructed + sparse tensor. + + Args: + ccol_indices (array_like): (B+1)-dimensional array of size + ``(*batchsize, ncols + 1)``. The last element of each batch + is the number of non-zeros. This tensor encodes the index in + values and row_indices depending on where the given column + starts. Each successive number in the tensor subtracted by the + number before it denotes the number of elements in a given + column. + row_indices (array_like): Row coordinates of each element in + values. (B+1)-dimensional tensor with the same length as + values. + values (array_list): Initial values for the tensor. Can be a list, + tuple, NumPy ``ndarray``, scalar, and other types that + represents a (1+K)-dimensional tensor where ``K`` is the number + of dense dimensions. + size (list, tuple, :class:`torch.Size`, optional): Size of the + sparse tensor: ``(*batchsize, nrows, ncols, *densesize)``. If + not provided, the size will be inferred as the minimum size + big enough to hold all non-zero elements. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of + returned tensor. Default: if None, infers data type from + :attr:`values`. + device (:class:`torch.device`, optional): the desired device of + returned tensor. Default: if None, uses the current device + for the default tensor type (see + :func:`torch.set_default_device`). :attr:`device` will be + the CPU for CPU tensor types and the current CUDA device for + CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + check_invariants (bool, optional): If sparse tensor invariants are checked. + Default: as returned by :func:`torch.sparse.check_sparse_tensor_invariants.is_enabled`, + initially False. + + Example:: + + >>> ccol_indices = [0, 2, 4] + >>> row_indices = [0, 1, 0, 1] + >>> values = [1, 2, 3, 4] + >>> torch.sparse_csc_tensor(torch.tensor(ccol_indices, dtype=torch.int64), + ... torch.tensor(row_indices, dtype=torch.int64), + ... torch.tensor(values), dtype=torch.double) + tensor(ccol_indices=tensor([0, 2, 4]), + row_indices=tensor([0, 1, 0, 1]), + values=tensor([1., 2., 3., 4.]), size=(2, 2), nnz=4, + dtype=torch.float64, layout=torch.sparse_csc) + """ + +def sparse_csr_tensor( + crow_indices: Tensor | list, + col_indices: Tensor | list, + values: Tensor | list, + size: _size | None = None, + *, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + check_invariants: _bool | None = None, +) -> Tensor: + r""" + sparse_csr_tensor(crow_indices, col_indices, values, size=None, *, dtype=None, device=None, pin_memory=False, requires_grad=False, check_invariants=None) -> Tensor + + Constructs a :ref:`sparse tensor in CSR (Compressed Sparse Row) ` with specified + values at the given :attr:`crow_indices` and :attr:`col_indices`. Sparse matrix multiplication operations + in CSR format are typically faster than that for sparse tensors in COO format. Make you have a look + at :ref:`the note on the data type of the indices `. + + .. note:: + + If the ``device`` argument is not specified the device of the given + :attr:`values` and indices tensor(s) must match. If, however, the + argument is specified the input Tensors will be converted to the + given device and in turn determine the device of the constructed + sparse tensor. + + Args: + crow_indices (array_like): (B+1)-dimensional array of size + ``(*batchsize, nrows + 1)``. The last element of each batch + is the number of non-zeros. This tensor encodes the index in + values and col_indices depending on where the given row + starts. Each successive number in the tensor subtracted by the + number before it denotes the number of elements in a given + row. + col_indices (array_like): Column coordinates of each element in + values. (B+1)-dimensional tensor with the same length + as values. + values (array_list): Initial values for the tensor. Can be a list, + tuple, NumPy ``ndarray``, scalar, and other types that + represents a (1+K)-dimensional tensor where ``K`` is the number + of dense dimensions. + size (list, tuple, :class:`torch.Size`, optional): Size of the + sparse tensor: ``(*batchsize, nrows, ncols, *densesize)``. If + not provided, the size will be inferred as the minimum size + big enough to hold all non-zero elements. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of + returned tensor. Default: if None, infers data type from + :attr:`values`. + device (:class:`torch.device`, optional): the desired device of + returned tensor. Default: if None, uses the current device + for the default tensor type (see + :func:`torch.set_default_device`). :attr:`device` will be + the CPU for CPU tensor types and the current CUDA device for + CUDA tensor types. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + check_invariants (bool, optional): If sparse tensor invariants are checked. + Default: as returned by :func:`torch.sparse.check_sparse_tensor_invariants.is_enabled`, + initially False. + + Example:: + + >>> crow_indices = [0, 2, 4] + >>> col_indices = [0, 1, 0, 1] + >>> values = [1, 2, 3, 4] + >>> torch.sparse_csr_tensor(torch.tensor(crow_indices, dtype=torch.int64), + ... torch.tensor(col_indices, dtype=torch.int64), + ... torch.tensor(values), dtype=torch.double) + tensor(crow_indices=tensor([0, 2, 4]), + col_indices=tensor([0, 1, 0, 1]), + values=tensor([1., 2., 3., 4.]), size=(2, 2), nnz=4, + dtype=torch.float64, layout=torch.sparse_csr) + """ + +def split_copy( + input: Tensor, + split_size: _int | SymInt, + dim: _int = 0, + *, + out: tuple[Tensor, ...] | list[Tensor] | None = None, +) -> None: + r""" + Performs the same operation as :func:`torch.split`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def split_with_sizes( + input: Tensor, + split_sizes: Sequence[_int | SymInt], + dim: _int = 0, +) -> tuple[Tensor, ...]: ... +def split_with_sizes_copy( + input: Tensor, + split_sizes: Sequence[_int | SymInt], + dim: _int = 0, + *, + out: tuple[Tensor, ...] | list[Tensor] | None = None, +) -> None: + r""" + Performs the same operation as :func:`torch.split_with_sizes`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def spmm(input: Tensor, mat2: Tensor) -> Tensor: ... +def sqrt(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + sqrt(input, *, out=None) -> Tensor + + Returns a new tensor with the square-root of the elements of :attr:`input`. + + .. math:: + \text{out}_{i} = \sqrt{\text{input}_{i}} + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-2.0755, 1.0226, 0.0831, 0.4806]) + >>> torch.sqrt(a) + tensor([ nan, 1.0112, 0.2883, 0.6933]) + """ + +def sqrt_(input: Tensor) -> Tensor: ... +def square(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + square(input: Tensor, *, out: Optional[Tensor]) -> Tensor + + Returns a new tensor with the square of the elements of :attr:`input`. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-2.0755, 1.0226, 0.0831, 0.4806]) + >>> torch.square(a) + tensor([ 4.3077, 1.0457, 0.0069, 0.2310]) + """ + +def square_(input: Tensor) -> Tensor: ... +@overload +def squeeze(input: Tensor) -> Tensor: + r""" + squeeze(input: Tensor, dim: Optional[Union[int, List[int]]]) -> Tensor + + Returns a tensor with all specified dimensions of :attr:`input` of size `1` removed. + + For example, if `input` is of shape: + :math:`(A \times 1 \times B \times C \times 1 \times D)` then the `input.squeeze()` + will be of shape: :math:`(A \times B \times C \times D)`. + + When :attr:`dim` is given, a squeeze operation is done only in the given + dimension(s). If `input` is of shape: :math:`(A \times 1 \times B)`, + ``squeeze(input, 0)`` leaves the tensor unchanged, but ``squeeze(input, 1)`` + will squeeze the tensor to the shape :math:`(A \times B)`. + + .. note:: The returned tensor shares the storage with the input tensor, + so changing the contents of one will change the contents of the other. + + .. warning:: If the tensor has a batch dimension of size 1, then `squeeze(input)` + will also remove the batch dimension, which can lead to unexpected + errors. Consider specifying only the dims you wish to be squeezed. + + Args: + input (Tensor): the input tensor. + dim (int or tuple of ints, optional): if given, the input will be squeezed + only in the specified dimensions. + + .. versionchanged:: 2.0 + :attr:`dim` now accepts tuples of dimensions. + + Example:: + + >>> x = torch.zeros(2, 1, 2, 1, 2) + >>> x.size() + torch.Size([2, 1, 2, 1, 2]) + >>> y = torch.squeeze(x) + >>> y.size() + torch.Size([2, 2, 2]) + >>> y = torch.squeeze(x, 0) + >>> y.size() + torch.Size([2, 1, 2, 1, 2]) + >>> y = torch.squeeze(x, 1) + >>> y.size() + torch.Size([2, 2, 1, 2]) + >>> y = torch.squeeze(x, (1, 2, 3)) + torch.Size([2, 2, 2]) + """ + +@overload +def squeeze(input: Tensor, dim: _int) -> Tensor: + r""" + squeeze(input: Tensor, dim: Optional[Union[int, List[int]]]) -> Tensor + + Returns a tensor with all specified dimensions of :attr:`input` of size `1` removed. + + For example, if `input` is of shape: + :math:`(A \times 1 \times B \times C \times 1 \times D)` then the `input.squeeze()` + will be of shape: :math:`(A \times B \times C \times D)`. + + When :attr:`dim` is given, a squeeze operation is done only in the given + dimension(s). If `input` is of shape: :math:`(A \times 1 \times B)`, + ``squeeze(input, 0)`` leaves the tensor unchanged, but ``squeeze(input, 1)`` + will squeeze the tensor to the shape :math:`(A \times B)`. + + .. note:: The returned tensor shares the storage with the input tensor, + so changing the contents of one will change the contents of the other. + + .. warning:: If the tensor has a batch dimension of size 1, then `squeeze(input)` + will also remove the batch dimension, which can lead to unexpected + errors. Consider specifying only the dims you wish to be squeezed. + + Args: + input (Tensor): the input tensor. + dim (int or tuple of ints, optional): if given, the input will be squeezed + only in the specified dimensions. + + .. versionchanged:: 2.0 + :attr:`dim` now accepts tuples of dimensions. + + Example:: + + >>> x = torch.zeros(2, 1, 2, 1, 2) + >>> x.size() + torch.Size([2, 1, 2, 1, 2]) + >>> y = torch.squeeze(x) + >>> y.size() + torch.Size([2, 2, 2]) + >>> y = torch.squeeze(x, 0) + >>> y.size() + torch.Size([2, 1, 2, 1, 2]) + >>> y = torch.squeeze(x, 1) + >>> y.size() + torch.Size([2, 2, 1, 2]) + >>> y = torch.squeeze(x, (1, 2, 3)) + torch.Size([2, 2, 2]) + """ + +@overload +def squeeze(input: Tensor, dim: _size) -> Tensor: + r""" + squeeze(input: Tensor, dim: Optional[Union[int, List[int]]]) -> Tensor + + Returns a tensor with all specified dimensions of :attr:`input` of size `1` removed. + + For example, if `input` is of shape: + :math:`(A \times 1 \times B \times C \times 1 \times D)` then the `input.squeeze()` + will be of shape: :math:`(A \times B \times C \times D)`. + + When :attr:`dim` is given, a squeeze operation is done only in the given + dimension(s). If `input` is of shape: :math:`(A \times 1 \times B)`, + ``squeeze(input, 0)`` leaves the tensor unchanged, but ``squeeze(input, 1)`` + will squeeze the tensor to the shape :math:`(A \times B)`. + + .. note:: The returned tensor shares the storage with the input tensor, + so changing the contents of one will change the contents of the other. + + .. warning:: If the tensor has a batch dimension of size 1, then `squeeze(input)` + will also remove the batch dimension, which can lead to unexpected + errors. Consider specifying only the dims you wish to be squeezed. + + Args: + input (Tensor): the input tensor. + dim (int or tuple of ints, optional): if given, the input will be squeezed + only in the specified dimensions. + + .. versionchanged:: 2.0 + :attr:`dim` now accepts tuples of dimensions. + + Example:: + + >>> x = torch.zeros(2, 1, 2, 1, 2) + >>> x.size() + torch.Size([2, 1, 2, 1, 2]) + >>> y = torch.squeeze(x) + >>> y.size() + torch.Size([2, 2, 2]) + >>> y = torch.squeeze(x, 0) + >>> y.size() + torch.Size([2, 1, 2, 1, 2]) + >>> y = torch.squeeze(x, 1) + >>> y.size() + torch.Size([2, 2, 1, 2]) + >>> y = torch.squeeze(x, (1, 2, 3)) + torch.Size([2, 2, 2]) + """ + +@overload +def squeeze(input: Tensor, dim: str | EllipsisType | None) -> Tensor: + r""" + squeeze(input: Tensor, dim: Optional[Union[int, List[int]]]) -> Tensor + + Returns a tensor with all specified dimensions of :attr:`input` of size `1` removed. + + For example, if `input` is of shape: + :math:`(A \times 1 \times B \times C \times 1 \times D)` then the `input.squeeze()` + will be of shape: :math:`(A \times B \times C \times D)`. + + When :attr:`dim` is given, a squeeze operation is done only in the given + dimension(s). If `input` is of shape: :math:`(A \times 1 \times B)`, + ``squeeze(input, 0)`` leaves the tensor unchanged, but ``squeeze(input, 1)`` + will squeeze the tensor to the shape :math:`(A \times B)`. + + .. note:: The returned tensor shares the storage with the input tensor, + so changing the contents of one will change the contents of the other. + + .. warning:: If the tensor has a batch dimension of size 1, then `squeeze(input)` + will also remove the batch dimension, which can lead to unexpected + errors. Consider specifying only the dims you wish to be squeezed. + + Args: + input (Tensor): the input tensor. + dim (int or tuple of ints, optional): if given, the input will be squeezed + only in the specified dimensions. + + .. versionchanged:: 2.0 + :attr:`dim` now accepts tuples of dimensions. + + Example:: + + >>> x = torch.zeros(2, 1, 2, 1, 2) + >>> x.size() + torch.Size([2, 1, 2, 1, 2]) + >>> y = torch.squeeze(x) + >>> y.size() + torch.Size([2, 2, 2]) + >>> y = torch.squeeze(x, 0) + >>> y.size() + torch.Size([2, 1, 2, 1, 2]) + >>> y = torch.squeeze(x, 1) + >>> y.size() + torch.Size([2, 2, 1, 2]) + >>> y = torch.squeeze(x, (1, 2, 3)) + torch.Size([2, 2, 2]) + """ + +@overload +def squeeze_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + Performs the same operation as :func:`torch.squeeze`, but all output tensors + are freshly created instead of aliasing the input. + """ + +@overload +def squeeze_copy( + input: Tensor, + dim: _int, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.squeeze`, but all output tensors + are freshly created instead of aliasing the input. + """ + +@overload +def squeeze_copy( + input: Tensor, + dim: _size, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.squeeze`, but all output tensors + are freshly created instead of aliasing the input. + """ + +@overload +def sspaddmm( + beta: Number | _complex, + self: Tensor, + alpha: Number | _complex, + mat1: Tensor, + mat2: Tensor, +) -> Tensor: + r""" + sspaddmm(input, mat1, mat2, *, beta=1, alpha=1, out=None) -> Tensor + + Matrix multiplies a sparse tensor :attr:`mat1` with a dense tensor + :attr:`mat2`, then adds the sparse tensor :attr:`input` to the result. + + Note: This function is equivalent to :func:`torch.addmm`, except + :attr:`input` and :attr:`mat1` are sparse. + + Args: + input (Tensor): a sparse matrix to be added + mat1 (Tensor): a sparse matrix to be matrix multiplied + mat2 (Tensor): a dense matrix to be matrix multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`mat` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat1 @ mat2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + """ + +@overload +def sspaddmm( + input: Tensor, + mat1: Tensor, + mat2: Tensor, + *, + beta: Number | _complex = 1, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + sspaddmm(input, mat1, mat2, *, beta=1, alpha=1, out=None) -> Tensor + + Matrix multiplies a sparse tensor :attr:`mat1` with a dense tensor + :attr:`mat2`, then adds the sparse tensor :attr:`input` to the result. + + Note: This function is equivalent to :func:`torch.addmm`, except + :attr:`input` and :attr:`mat1` are sparse. + + Args: + input (Tensor): a sparse matrix to be added + mat1 (Tensor): a sparse matrix to be matrix multiplied + mat2 (Tensor): a dense matrix to be matrix multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`mat` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat1 @ mat2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + """ + +@overload +def sspaddmm( + beta: Number | _complex, + self: Tensor, + mat1: Tensor, + mat2: Tensor, +) -> Tensor: + r""" + sspaddmm(input, mat1, mat2, *, beta=1, alpha=1, out=None) -> Tensor + + Matrix multiplies a sparse tensor :attr:`mat1` with a dense tensor + :attr:`mat2`, then adds the sparse tensor :attr:`input` to the result. + + Note: This function is equivalent to :func:`torch.addmm`, except + :attr:`input` and :attr:`mat1` are sparse. + + Args: + input (Tensor): a sparse matrix to be added + mat1 (Tensor): a sparse matrix to be matrix multiplied + mat2 (Tensor): a dense matrix to be matrix multiplied + + Keyword args: + beta (Number, optional): multiplier for :attr:`mat` (:math:`\beta`) + alpha (Number, optional): multiplier for :math:`mat1 @ mat2` (:math:`\alpha`) + out (Tensor, optional): the output tensor. + """ + +def stack( + tensors: tuple[Tensor, ...] | list[Tensor], + dim: _int = 0, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + stack(tensors, dim=0, *, out=None) -> Tensor + + Concatenates a sequence of tensors along a new dimension. + + All tensors need to be of the same size. + + .. seealso:: + + :func:`torch.cat` concatenates the given sequence along an existing dimension. + + Arguments: + tensors (sequence of Tensors): sequence of tensors to concatenate + dim (int, optional): dimension to insert. Has to be between 0 and the number + of dimensions of concatenated tensors (inclusive). Default: 0 + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> x = torch.randn(2, 3) + >>> x + tensor([[ 0.3367, 0.1288, 0.2345], + [ 0.2303, -1.1229, -0.1863]]) + >>> torch.stack((x, x)) # same as torch.stack((x, x), dim=0) + tensor([[[ 0.3367, 0.1288, 0.2345], + [ 0.2303, -1.1229, -0.1863]], + + [[ 0.3367, 0.1288, 0.2345], + [ 0.2303, -1.1229, -0.1863]]]) + >>> torch.stack((x, x)).size() + torch.Size([2, 2, 3]) + >>> torch.stack((x, x), dim=1) + tensor([[[ 0.3367, 0.1288, 0.2345], + [ 0.3367, 0.1288, 0.2345]], + + [[ 0.2303, -1.1229, -0.1863], + [ 0.2303, -1.1229, -0.1863]]]) + >>> torch.stack((x, x), dim=2) + tensor([[[ 0.3367, 0.3367], + [ 0.1288, 0.1288], + [ 0.2345, 0.2345]], + + [[ 0.2303, 0.2303], + [-1.1229, -1.1229], + [-0.1863, -0.1863]]]) + >>> torch.stack((x, x), dim=-1) + tensor([[[ 0.3367, 0.3367], + [ 0.1288, 0.1288], + [ 0.2345, 0.2345]], + + [[ 0.2303, 0.2303], + [-1.1229, -1.1229], + [-0.1863, -0.1863]]]) + """ + +@overload +def std( + input: Tensor, + dim: _int | _size | None, + unbiased: _bool = True, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + std(input, dim=None, *, correction=1, keepdim=False, out=None) -> Tensor + + Calculates the standard deviation over the dimensions specified by :attr:`dim`. + :attr:`dim` can be a single dimension, list of dimensions, or ``None`` to + reduce over all dimensions. + + The standard deviation (:math:`\sigma`) is calculated as + + .. math:: \sigma = \sqrt{\frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2} + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.std(a, dim=1, keepdim=True) + tensor([[1.0311], + [0.7477], + [1.2204], + [0.9087]]) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def std( + input: Tensor, + dim: _int | _size | None = None, + *, + correction: Number | _complex | None = None, + keepdim: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + std(input, dim=None, *, correction=1, keepdim=False, out=None) -> Tensor + + Calculates the standard deviation over the dimensions specified by :attr:`dim`. + :attr:`dim` can be a single dimension, list of dimensions, or ``None`` to + reduce over all dimensions. + + The standard deviation (:math:`\sigma`) is calculated as + + .. math:: \sigma = \sqrt{\frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2} + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.std(a, dim=1, keepdim=True) + tensor([[1.0311], + [0.7477], + [1.2204], + [0.9087]]) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def std(input: Tensor, unbiased: _bool = True) -> Tensor: + r""" + std(input, dim=None, *, correction=1, keepdim=False, out=None) -> Tensor + + Calculates the standard deviation over the dimensions specified by :attr:`dim`. + :attr:`dim` can be a single dimension, list of dimensions, or ``None`` to + reduce over all dimensions. + + The standard deviation (:math:`\sigma`) is calculated as + + .. math:: \sigma = \sqrt{\frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2} + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.std(a, dim=1, keepdim=True) + tensor([[1.0311], + [0.7477], + [1.2204], + [0.9087]]) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def std( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + *, + correction: Number | _complex | None = None, + keepdim: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + std(input, dim=None, *, correction=1, keepdim=False, out=None) -> Tensor + + Calculates the standard deviation over the dimensions specified by :attr:`dim`. + :attr:`dim` can be a single dimension, list of dimensions, or ``None`` to + reduce over all dimensions. + + The standard deviation (:math:`\sigma`) is calculated as + + .. math:: \sigma = \sqrt{\frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2} + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.std(a, dim=1, keepdim=True) + tensor([[1.0311], + [0.7477], + [1.2204], + [0.9087]]) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def std( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + unbiased: _bool = True, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + std(input, dim=None, *, correction=1, keepdim=False, out=None) -> Tensor + + Calculates the standard deviation over the dimensions specified by :attr:`dim`. + :attr:`dim` can be a single dimension, list of dimensions, or ``None`` to + reduce over all dimensions. + + The standard deviation (:math:`\sigma`) is calculated as + + .. math:: \sigma = \sqrt{\frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2} + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.std(a, dim=1, keepdim=True) + tensor([[1.0311], + [0.7477], + [1.2204], + [0.9087]]) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def std_mean( + input: Tensor, + dim: _int | _size | None, + unbiased: _bool = True, + keepdim: _bool = False, +) -> tuple[Tensor, Tensor]: + r""" + std_mean(input, dim=None, *, correction=1, keepdim=False, out=None) -> (Tensor, Tensor) + + Calculates the standard deviation and mean over the dimensions specified by + :attr:`dim`. :attr:`dim` can be a single dimension, list of dimensions, or + ``None`` to reduce over all dimensions. + + The standard deviation (:math:`\sigma`) is calculated as + + .. math:: \sigma = \sqrt{\frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2} + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Returns: + A tuple (std, mean) containing the standard deviation and mean. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.std_mean(a, dim=0, keepdim=True) + (tensor([[1.2620, 1.0028, 1.0957, 0.6038]]), + tensor([[ 0.0645, 0.4485, 0.8707, -0.0665]])) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def std_mean( + input: Tensor, + dim: _int | _size | None = None, + *, + correction: Number | _complex | None = None, + keepdim: _bool = False, +) -> tuple[Tensor, Tensor]: + r""" + std_mean(input, dim=None, *, correction=1, keepdim=False, out=None) -> (Tensor, Tensor) + + Calculates the standard deviation and mean over the dimensions specified by + :attr:`dim`. :attr:`dim` can be a single dimension, list of dimensions, or + ``None`` to reduce over all dimensions. + + The standard deviation (:math:`\sigma`) is calculated as + + .. math:: \sigma = \sqrt{\frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2} + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Returns: + A tuple (std, mean) containing the standard deviation and mean. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.std_mean(a, dim=0, keepdim=True) + (tensor([[1.2620, 1.0028, 1.0957, 0.6038]]), + tensor([[ 0.0645, 0.4485, 0.8707, -0.0665]])) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def std_mean( + input: Tensor, + unbiased: _bool = True, +) -> tuple[Tensor, Tensor]: + r""" + std_mean(input, dim=None, *, correction=1, keepdim=False, out=None) -> (Tensor, Tensor) + + Calculates the standard deviation and mean over the dimensions specified by + :attr:`dim`. :attr:`dim` can be a single dimension, list of dimensions, or + ``None`` to reduce over all dimensions. + + The standard deviation (:math:`\sigma`) is calculated as + + .. math:: \sigma = \sqrt{\frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2} + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Returns: + A tuple (std, mean) containing the standard deviation and mean. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.std_mean(a, dim=0, keepdim=True) + (tensor([[1.2620, 1.0028, 1.0957, 0.6038]]), + tensor([[ 0.0645, 0.4485, 0.8707, -0.0665]])) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def std_mean( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + *, + correction: Number | _complex | None = None, + keepdim: _bool = False, +) -> tuple[Tensor, Tensor]: + r""" + std_mean(input, dim=None, *, correction=1, keepdim=False, out=None) -> (Tensor, Tensor) + + Calculates the standard deviation and mean over the dimensions specified by + :attr:`dim`. :attr:`dim` can be a single dimension, list of dimensions, or + ``None`` to reduce over all dimensions. + + The standard deviation (:math:`\sigma`) is calculated as + + .. math:: \sigma = \sqrt{\frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2} + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Returns: + A tuple (std, mean) containing the standard deviation and mean. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.std_mean(a, dim=0, keepdim=True) + (tensor([[1.2620, 1.0028, 1.0957, 0.6038]]), + tensor([[ 0.0645, 0.4485, 0.8707, -0.0665]])) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def std_mean( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + unbiased: _bool = True, + keepdim: _bool = False, +) -> tuple[Tensor, Tensor]: + r""" + std_mean(input, dim=None, *, correction=1, keepdim=False, out=None) -> (Tensor, Tensor) + + Calculates the standard deviation and mean over the dimensions specified by + :attr:`dim`. :attr:`dim` can be a single dimension, list of dimensions, or + ``None`` to reduce over all dimensions. + + The standard deviation (:math:`\sigma`) is calculated as + + .. math:: \sigma = \sqrt{\frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2} + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Returns: + A tuple (std, mean) containing the standard deviation and mean. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.std_mean(a, dim=0, keepdim=True) + (tensor([[1.2620, 1.0028, 1.0957, 0.6038]]), + tensor([[ 0.0645, 0.4485, 0.8707, -0.0665]])) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def sub( + input: Tensor | Number | _complex, + other: Tensor | Number | _complex, + *, + alpha: Number | _complex | None = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + sub(input, other, *, alpha=1, out=None) -> Tensor + + Subtracts :attr:`other`, scaled by :attr:`alpha`, from :attr:`input`. + + .. math:: + \text{{out}}_i = \text{{input}}_i - \text{{alpha}} \times \text{{other}}_i + + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer, float, and complex inputs. + + Args: + input (Tensor): the input tensor. + other (Tensor or Number): the tensor or number to subtract from :attr:`input`. + + Keyword args: + alpha (Number): the multiplier for :attr:`other`. + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor((1, 2)) + >>> b = torch.tensor((0, 1)) + >>> torch.sub(a, b, alpha=2) + tensor([1, 0]) + """ + +@overload +def sub(self: Tensor, alpha: Number | _complex, other: Tensor) -> Tensor: + r""" + sub(input, other, *, alpha=1, out=None) -> Tensor + + Subtracts :attr:`other`, scaled by :attr:`alpha`, from :attr:`input`. + + .. math:: + \text{{out}}_i = \text{{input}}_i - \text{{alpha}} \times \text{{other}}_i + + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer, float, and complex inputs. + + Args: + input (Tensor): the input tensor. + other (Tensor or Number): the tensor or number to subtract from :attr:`input`. + + Keyword args: + alpha (Number): the multiplier for :attr:`other`. + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor((1, 2)) + >>> b = torch.tensor((0, 1)) + >>> torch.sub(a, b, alpha=2) + tensor([1, 0]) + """ + +@overload +def sub( + self: Tensor, + alpha: Number | _complex, + other: Tensor, + *, + out: Tensor, +) -> Tensor: + r""" + sub(input, other, *, alpha=1, out=None) -> Tensor + + Subtracts :attr:`other`, scaled by :attr:`alpha`, from :attr:`input`. + + .. math:: + \text{{out}}_i = \text{{input}}_i - \text{{alpha}} \times \text{{other}}_i + + + Supports :ref:`broadcasting to a common shape `, + :ref:`type promotion `, and integer, float, and complex inputs. + + Args: + input (Tensor): the input tensor. + other (Tensor or Number): the tensor or number to subtract from :attr:`input`. + + Keyword args: + alpha (Number): the multiplier for :attr:`other`. + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor((1, 2)) + >>> b = torch.tensor((0, 1)) + >>> torch.sub(a, b, alpha=2) + tensor([1, 0]) + """ + +@overload +def subtract( + input: Tensor, + other: Tensor, + *, + alpha: Number | _complex = 1, + out: Tensor | None = None, +) -> Tensor: + r""" + subtract(input, other, *, alpha=1, out=None) -> Tensor + + Alias for :func:`torch.sub`. + """ + +@overload +def subtract( + input: Tensor, + other: Number | _complex, + alpha: Number | _complex = 1, +) -> Tensor: + r""" + subtract(input, other, *, alpha=1, out=None) -> Tensor + + Alias for :func:`torch.sub`. + """ + +@overload +def sum(input: Tensor, *, dtype: _dtype | None = None) -> Tensor: + r""" + sum(input, *, dtype=None) -> Tensor + + Returns the sum of all elements in the :attr:`input` tensor. + + Args: + input (Tensor): the input tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + .. note:: Use the `dtype` argument if you need the result in a specific tensor type. + Otherwise, the result type may be automatically promoted (e.g., from `torch.int32` to `torch.int64`). + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.1133, -0.9567, 0.2958]]) + >>> torch.sum(a) + tensor(-0.5475) + + .. function:: sum(input, dim, keepdim=False, *, dtype=None) -> Tensor + :noindex: + + Returns the sum of each row of the :attr:`input` tensor in the given + dimension :attr:`dim`. If :attr:`dim` is a list of dimensions, + reduce over all of them. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.0569, -0.2475, 0.0737, -0.3429], + [-0.2993, 0.9138, 0.9337, -1.6864], + [ 0.1132, 0.7892, -0.1003, 0.5688], + [ 0.3637, -0.9906, -0.4752, -1.5197]]) + >>> torch.sum(a, 1) + tensor([-0.4598, -0.1381, 1.3708, -2.6217]) + >>> b = torch.arange(4 * 5 * 6).view(4, 5, 6) + >>> torch.sum(b, (2, 1)) + tensor([ 435., 1335., 2235., 3135.]) + """ + +@overload +def sum( + input: Tensor, + dim: _int | _size | None, + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + sum(input, *, dtype=None) -> Tensor + + Returns the sum of all elements in the :attr:`input` tensor. + + Args: + input (Tensor): the input tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + .. note:: Use the `dtype` argument if you need the result in a specific tensor type. + Otherwise, the result type may be automatically promoted (e.g., from `torch.int32` to `torch.int64`). + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.1133, -0.9567, 0.2958]]) + >>> torch.sum(a) + tensor(-0.5475) + + .. function:: sum(input, dim, keepdim=False, *, dtype=None) -> Tensor + :noindex: + + Returns the sum of each row of the :attr:`input` tensor in the given + dimension :attr:`dim`. If :attr:`dim` is a list of dimensions, + reduce over all of them. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.0569, -0.2475, 0.0737, -0.3429], + [-0.2993, 0.9138, 0.9337, -1.6864], + [ 0.1132, 0.7892, -0.1003, 0.5688], + [ 0.3637, -0.9906, -0.4752, -1.5197]]) + >>> torch.sum(a, 1) + tensor([-0.4598, -0.1381, 1.3708, -2.6217]) + >>> b = torch.arange(4 * 5 * 6).view(4, 5, 6) + >>> torch.sum(b, (2, 1)) + tensor([ 435., 1335., 2235., 3135.]) + """ + +@overload +def sum( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + keepdim: _bool = False, + *, + dtype: _dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + r""" + sum(input, *, dtype=None) -> Tensor + + Returns the sum of all elements in the :attr:`input` tensor. + + Args: + input (Tensor): the input tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + .. note:: Use the `dtype` argument if you need the result in a specific tensor type. + Otherwise, the result type may be automatically promoted (e.g., from `torch.int32` to `torch.int64`). + + Example:: + + >>> a = torch.randn(1, 3) + >>> a + tensor([[ 0.1133, -0.9567, 0.2958]]) + >>> torch.sum(a) + tensor(-0.5475) + + .. function:: sum(input, dim, keepdim=False, *, dtype=None) -> Tensor + :noindex: + + Returns the sum of each row of the :attr:`input` tensor in the given + dimension :attr:`dim`. If :attr:`dim` is a list of dimensions, + reduce over all of them. + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + If specified, the input tensor is casted to :attr:`dtype` before the operation + is performed. This is useful for preventing data type overflows. Default: None. + + Example:: + + >>> a = torch.randn(4, 4) + >>> a + tensor([[ 0.0569, -0.2475, 0.0737, -0.3429], + [-0.2993, 0.9138, 0.9337, -1.6864], + [ 0.1132, 0.7892, -0.1003, 0.5688], + [ 0.3637, -0.9906, -0.4752, -1.5197]]) + >>> torch.sum(a, 1) + tensor([-0.4598, -0.1381, 1.3708, -2.6217]) + >>> b = torch.arange(4 * 5 * 6).view(4, 5, 6) + >>> torch.sum(b, (2, 1)) + tensor([ 435., 1335., 2235., 3135.]) + """ + +def svd( + input: Tensor, + some: _bool = True, + compute_uv: _bool = True, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.svd: + r""" + svd(input, some=True, compute_uv=True, *, out=None) -> (Tensor, Tensor, Tensor) + + Computes the singular value decomposition of either a matrix or batch of + matrices :attr:`input`. The singular value decomposition is represented as a + namedtuple `(U, S, V)`, such that :attr:`input` :math:`= U \text{diag}(S) V^{\text{H}}`. + where :math:`V^{\text{H}}` is the transpose of `V` for real inputs, + and the conjugate transpose of `V` for complex inputs. + If :attr:`input` is a batch of matrices, then `U`, `S`, and `V` are also + batched with the same batch dimensions as :attr:`input`. + + If :attr:`some` is `True` (default), the method returns the reduced singular + value decomposition. In this case, if the last two dimensions of :attr:`input` are + `m` and `n`, then the returned `U` and `V` matrices will contain only + `min(n, m)` orthonormal columns. + + If :attr:`compute_uv` is `False`, the returned `U` and `V` will be + zero-filled matrices of shape `(m, m)` and `(n, n)` + respectively, and the same device as :attr:`input`. The argument :attr:`some` + has no effect when :attr:`compute_uv` is `False`. + + Supports :attr:`input` of float, double, cfloat and cdouble data types. + The dtypes of `U` and `V` are the same as :attr:`input`'s. `S` will + always be real-valued, even if :attr:`input` is complex. + + .. warning:: + + :func:`torch.svd` is deprecated in favor of :func:`torch.linalg.svd` + and will be removed in a future PyTorch release. + + ``U, S, V = torch.svd(A, some=some, compute_uv=True)`` (default) should be replaced with + + .. code:: python + + U, S, Vh = torch.linalg.svd(A, full_matrices=not some) + V = Vh.mH + + ``_, S, _ = torch.svd(A, some=some, compute_uv=False)`` should be replaced with + + .. code:: python + + S = torch.linalg.svdvals(A) + + .. note:: Differences with :func:`torch.linalg.svd`: + + * :attr:`some` is the opposite of + :func:`torch.linalg.svd`'s :attr:`full_matrices`. Note that + default value for both is `True`, so the default behavior is + effectively the opposite. + * :func:`torch.svd` returns `V`, whereas :func:`torch.linalg.svd` returns + `Vh`, that is, :math:`V^{\text{H}}`. + * If :attr:`compute_uv` is `False`, :func:`torch.svd` returns zero-filled + tensors for `U` and `Vh`, whereas :func:`torch.linalg.svd` returns + empty tensors. + + .. note:: The singular values are returned in descending order. If :attr:`input` is a batch of matrices, + then the singular values of each matrix in the batch are returned in descending order. + + .. note:: The `S` tensor can only be used to compute gradients if :attr:`compute_uv` is `True`. + + .. note:: When :attr:`some` is `False`, the gradients on `U[..., :, min(m, n):]` + and `V[..., :, min(m, n):]` will be ignored in the backward pass, as those vectors + can be arbitrary bases of the corresponding subspaces. + + .. note:: The implementation of :func:`torch.linalg.svd` on CPU uses LAPACK's routine `?gesdd` + (a divide-and-conquer algorithm) instead of `?gesvd` for speed. Analogously, + on GPU, it uses cuSOLVER's routines `gesvdj` and `gesvdjBatched` on CUDA 10.1.243 + and later, and MAGMA's routine `gesdd` on earlier versions of CUDA. + + .. note:: The returned `U` will not be contiguous. The matrix (or batch of matrices) will + be represented as a column-major matrix (i.e. Fortran-contiguous). + + .. warning:: The gradients with respect to `U` and `V` will only be finite when the input does not + have zero nor repeated singular values. + + .. warning:: If the distance between any two singular values is close to zero, the gradients with respect to + `U` and `V` will be numerically unstable, as they depends on + :math:`\frac{1}{\min_{i \neq j} \sigma_i^2 - \sigma_j^2}`. The same happens when the matrix + has small singular values, as these gradients also depend on `S^{-1}`. + + .. warning:: For complex-valued :attr:`input` the singular value decomposition is not unique, + as `U` and `V` may be multiplied by an arbitrary phase factor :math:`e^{i \phi}` on every column. + The same happens when :attr:`input` has repeated singular values, where one may multiply + the columns of the spanning subspace in `U` and `V` by a rotation matrix + and `the resulting vectors will span the same subspace`_. + Different platforms, like NumPy, or inputs on different device types, + may produce different `U` and `V` tensors. + + Args: + input (Tensor): the input tensor of size `(*, m, n)` where `*` is zero or more + batch dimensions consisting of `(m, n)` matrices. + some (bool, optional): controls whether to compute the reduced or full decomposition, and + consequently, the shape of returned `U` and `V`. Default: `True`. + compute_uv (bool, optional): controls whether to compute `U` and `V`. Default: `True`. + + Keyword args: + out (tuple, optional): the output tuple of tensors + + Example:: + + >>> a = torch.randn(5, 3) + >>> a + tensor([[ 0.2364, -0.7752, 0.6372], + [ 1.7201, 0.7394, -0.0504], + [-0.3371, -1.0584, 0.5296], + [ 0.3550, -0.4022, 1.5569], + [ 0.2445, -0.0158, 1.1414]]) + >>> u, s, v = torch.svd(a) + >>> u + tensor([[ 0.4027, 0.0287, 0.5434], + [-0.1946, 0.8833, 0.3679], + [ 0.4296, -0.2890, 0.5261], + [ 0.6604, 0.2717, -0.2618], + [ 0.4234, 0.2481, -0.4733]]) + >>> s + tensor([2.3289, 2.0315, 0.7806]) + >>> v + tensor([[-0.0199, 0.8766, 0.4809], + [-0.5080, 0.4054, -0.7600], + [ 0.8611, 0.2594, -0.4373]]) + >>> torch.dist(a, torch.mm(torch.mm(u, torch.diag(s)), v.t())) + tensor(8.6531e-07) + >>> a_big = torch.randn(7, 5, 3) + >>> u, s, v = torch.svd(a_big) + >>> torch.dist(a_big, torch.matmul(torch.matmul(u, torch.diag_embed(s)), v.mT)) + tensor(2.6503e-06) + + .. _the resulting vectors will span the same subspace: + (https://en.wikipedia.org/wiki/Singular_value_decomposition#Singular_values,_singular_vectors,_and_their_relation_to_the_SVD) + """ + +def swapaxes(input: Tensor, axis0: _int, axis1: _int) -> Tensor: + r""" + swapaxes(input, axis0, axis1) -> Tensor + + Alias for :func:`torch.transpose`. + + This function is equivalent to NumPy's swapaxes function. + + Examples:: + + >>> x = torch.tensor([[[0,1],[2,3]],[[4,5],[6,7]]]) + >>> x + tensor([[[0, 1], + [2, 3]], + + [[4, 5], + [6, 7]]]) + >>> torch.swapaxes(x, 0, 1) + tensor([[[0, 1], + [4, 5]], + + [[2, 3], + [6, 7]]]) + >>> torch.swapaxes(x, 0, 2) + tensor([[[0, 4], + [2, 6]], + + [[1, 5], + [3, 7]]]) + """ + +def swapdims(input: Tensor, dim0: _int, dim1: _int) -> Tensor: + r""" + swapdims(input, dim0, dim1) -> Tensor + + Alias for :func:`torch.transpose`. + + This function is equivalent to NumPy's swapaxes function. + + Examples:: + + >>> x = torch.tensor([[[0,1],[2,3]],[[4,5],[6,7]]]) + >>> x + tensor([[[0, 1], + [2, 3]], + + [[4, 5], + [6, 7]]]) + >>> torch.swapdims(x, 0, 1) + tensor([[[0, 1], + [4, 5]], + + [[2, 3], + [6, 7]]]) + >>> torch.swapdims(x, 0, 2) + tensor([[[0, 4], + [2, 6]], + + [[1, 5], + [3, 7]]]) + """ + +def sym_constrain_range( + size: Number | _complex, + *, + min: _int | None = None, + max: _int | None = None, +) -> None: ... +def sym_constrain_range_for_size( + size: Number | _complex, + *, + min: _int | None = None, + max: _int | None = None, +) -> None: ... +def t(input: Tensor) -> Tensor: + r""" + t(input) -> Tensor + + Expects :attr:`input` to be <= 2-D tensor and transposes dimensions 0 + and 1. + + 0-D and 1-D tensors are returned as is. When input is a 2-D tensor this + is equivalent to ``transpose(input, 0, 1)``. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> x = torch.randn(()) + >>> x + tensor(0.1995) + >>> torch.t(x) + tensor(0.1995) + >>> x = torch.randn(3) + >>> x + tensor([ 2.4320, -0.4608, 0.7702]) + >>> torch.t(x) + tensor([ 2.4320, -0.4608, 0.7702]) + >>> x = torch.randn(2, 3) + >>> x + tensor([[ 0.4875, 0.9158, -0.5872], + [ 0.3938, -0.6929, 0.6932]]) + >>> torch.t(x) + tensor([[ 0.4875, 0.3938], + [ 0.9158, -0.6929], + [-0.5872, 0.6932]]) + + See also :func:`torch.transpose`. + """ + +def t_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + Performs the same operation as :func:`torch.t`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def take( + input: Tensor, + index: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + take(input, index) -> Tensor + + Returns a new tensor with the elements of :attr:`input` at the given indices. + The input tensor is treated as if it were viewed as a 1-D tensor. The result + takes the same shape as the indices. + + Args: + input (Tensor): the input tensor. + index (LongTensor): the indices into tensor + + Example:: + + >>> src = torch.tensor([[4, 3, 5], + ... [6, 7, 8]]) + >>> torch.take(src, torch.tensor([0, 2, 5])) + tensor([ 4, 5, 8]) + """ + +def take_along_dim( + input: Tensor, + indices: Tensor, + dim: _int | None = None, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + take_along_dim(input, indices, dim=None, *, out=None) -> Tensor + + Selects values from :attr:`input` at the 1-dimensional indices from :attr:`indices` along the given :attr:`dim`. + + If :attr:`dim` is None, the input array is treated as if it has been flattened to 1d. + + Functions that return indices along a dimension, like :func:`torch.argmax` and :func:`torch.argsort`, + are designed to work with this function. See the examples below. + + .. note:: + This function is similar to NumPy's `take_along_axis`. + See also :func:`torch.gather`. + + Args: + input (Tensor): the input tensor. + indices (LongTensor): the indices into :attr:`input`. Must have long dtype. + dim (int, optional): dimension to select along. Default: `None`. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> t = torch.tensor([[10, 30, 20], [60, 40, 50]]) + >>> max_idx = torch.argmax(t) + >>> torch.take_along_dim(t, max_idx) + tensor([60]) + >>> sorted_idx = torch.argsort(t, dim=1) + >>> torch.take_along_dim(t, sorted_idx, dim=1) + tensor([[10, 20, 30], + [40, 50, 60]]) + """ + +def tan(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + tan(input, *, out=None) -> Tensor + + Returns a new tensor with the tangent of the elements in the :attr:`input` tensor, + where each value in this input tensor is in radians. + + .. math:: + \text{out}_{i} = \tan(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([-1.2027, -1.7687, 0.4412, -1.3856]) + >>> torch.tan(a) + tensor([-2.5930, 4.9859, 0.4722, -5.3366]) + """ + +def tan_(input: Tensor) -> Tensor: ... +def tanh(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + tanh(input, *, out=None) -> Tensor + + Returns a new tensor with the hyperbolic tangent of the elements + of :attr:`input`. + + .. math:: + \text{out}_{i} = \tanh(\text{input}_{i}) + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 0.8986, -0.7279, 1.1745, 0.2611]) + >>> torch.tanh(a) + tensor([ 0.7156, -0.6218, 0.8257, 0.2553]) + """ + +def tanh_(input: Tensor) -> Tensor: ... +def tensor( + data: Any, + dtype: _dtype | None = None, + device: DeviceLikeType | None = None, + requires_grad: _bool = False, + pin_memory: _bool = False, +) -> Tensor: + r""" + tensor(data, *, dtype=None, device=None, requires_grad=False, pin_memory=False) -> Tensor + + Constructs a tensor with no autograd history (also known as a "leaf tensor", see :doc:`/notes/autograd`) by copying :attr:`data`. + + .. warning:: + + When working with tensors prefer using :func:`torch.Tensor.clone`, + :func:`torch.Tensor.detach`, and :func:`torch.Tensor.requires_grad_` for + readability. Letting `t` be a tensor, ``torch.tensor(t)`` is equivalent to + ``t.detach().clone()``, and ``torch.tensor(t, requires_grad=True)`` + is equivalent to ``t.detach().clone().requires_grad_(True)``. + + .. seealso:: + + :func:`torch.as_tensor` preserves autograd history and avoids copies where possible. + :func:`torch.from_numpy` creates a tensor that shares storage with a NumPy array. + + Args: + data (array_like): Initial data for the tensor. Can be a list, tuple, + NumPy ``ndarray``, scalar, and other types. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, infers data type from :attr:`data`. + device (:class:`torch.device`, optional): the device of the constructed tensor. If None and data is a tensor + then the device of data is used. If None and data is not a tensor then + the result tensor is constructed on the current device. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + pin_memory (bool, optional): If set, returned tensor would be allocated in + the pinned memory. Works only for CPU tensors. Default: ``False``. + + + Example:: + + >>> torch.tensor([[0.1, 1.2], [2.2, 3.1], [4.9, 5.2]]) + tensor([[ 0.1000, 1.2000], + [ 2.2000, 3.1000], + [ 4.9000, 5.2000]]) + + >>> torch.tensor([0, 1]) # Type inference on data + tensor([ 0, 1]) + + >>> torch.tensor([[0.11111, 0.222222, 0.3333333]], + ... dtype=torch.float64, + ... device=torch.device('cuda:0')) # creates a double tensor on a CUDA device + tensor([[ 0.1111, 0.2222, 0.3333]], dtype=torch.float64, device='cuda:0') + + >>> torch.tensor(3.14159) # Create a zero-dimensional (scalar) tensor + tensor(3.1416) + + >>> torch.tensor([]) # Create an empty tensor (of size (0,)) + tensor([]) + """ + +@overload +def tensor_split( + input: Tensor, + tensor_indices_or_sections: Tensor, + dim: _int = 0, +) -> tuple[Tensor, ...]: + r""" + tensor_split(input, indices_or_sections, dim=0) -> List of Tensors + + Splits a tensor into multiple sub-tensors, all of which are views of :attr:`input`, + along dimension :attr:`dim` according to the indices or number of sections specified + by :attr:`indices_or_sections`. This function is based on NumPy's + :func:`numpy.array_split`. + + Args: + input (Tensor): the tensor to split + indices_or_sections (Tensor, int or list or tuple of ints): + If :attr:`indices_or_sections` is an integer ``n`` or a zero dimensional long tensor + with value ``n``, :attr:`input` is split into ``n`` sections along dimension :attr:`dim`. + If :attr:`input` is divisible by ``n`` along dimension :attr:`dim`, each + section will be of equal size, :code:`input.size(dim) / n`. If :attr:`input` + is not divisible by ``n``, the sizes of the first :code:`int(input.size(dim) % n)` + sections will have size :code:`int(input.size(dim) / n) + 1`, and the rest will + have size :code:`int(input.size(dim) / n)`. + + If :attr:`indices_or_sections` is a list or tuple of ints, or a one-dimensional long + tensor, then :attr:`input` is split along dimension :attr:`dim` at each of the indices + in the list, tuple or tensor. For instance, :code:`indices_or_sections=[2, 3]` and :code:`dim=0` + would result in the tensors :code:`input[:2]`, :code:`input[2:3]`, and :code:`input[3:]`. + + If :attr:`indices_or_sections` is a tensor, it must be a zero-dimensional or one-dimensional + long tensor on the CPU. + + dim (int, optional): dimension along which to split the tensor. Default: ``0`` + + Example:: + + >>> x = torch.arange(8) + >>> torch.tensor_split(x, 3) + (tensor([0, 1, 2]), tensor([3, 4, 5]), tensor([6, 7])) + + >>> x = torch.arange(7) + >>> torch.tensor_split(x, 3) + (tensor([0, 1, 2]), tensor([3, 4]), tensor([5, 6])) + >>> torch.tensor_split(x, (1, 6)) + (tensor([0]), tensor([1, 2, 3, 4, 5]), tensor([6])) + + >>> x = torch.arange(14).reshape(2, 7) + >>> x + tensor([[ 0, 1, 2, 3, 4, 5, 6], + [ 7, 8, 9, 10, 11, 12, 13]]) + >>> torch.tensor_split(x, 3, dim=1) + (tensor([[0, 1, 2], + [7, 8, 9]]), + tensor([[ 3, 4], + [10, 11]]), + tensor([[ 5, 6], + [12, 13]])) + >>> torch.tensor_split(x, (1, 6), dim=1) + (tensor([[0], + [7]]), + tensor([[ 1, 2, 3, 4, 5], + [ 8, 9, 10, 11, 12]]), + tensor([[ 6], + [13]])) + """ + +@overload +def tensor_split( + input: Tensor, + sections: _int | SymInt, + dim: _int = 0, +) -> tuple[Tensor, ...]: + r""" + tensor_split(input, indices_or_sections, dim=0) -> List of Tensors + + Splits a tensor into multiple sub-tensors, all of which are views of :attr:`input`, + along dimension :attr:`dim` according to the indices or number of sections specified + by :attr:`indices_or_sections`. This function is based on NumPy's + :func:`numpy.array_split`. + + Args: + input (Tensor): the tensor to split + indices_or_sections (Tensor, int or list or tuple of ints): + If :attr:`indices_or_sections` is an integer ``n`` or a zero dimensional long tensor + with value ``n``, :attr:`input` is split into ``n`` sections along dimension :attr:`dim`. + If :attr:`input` is divisible by ``n`` along dimension :attr:`dim`, each + section will be of equal size, :code:`input.size(dim) / n`. If :attr:`input` + is not divisible by ``n``, the sizes of the first :code:`int(input.size(dim) % n)` + sections will have size :code:`int(input.size(dim) / n) + 1`, and the rest will + have size :code:`int(input.size(dim) / n)`. + + If :attr:`indices_or_sections` is a list or tuple of ints, or a one-dimensional long + tensor, then :attr:`input` is split along dimension :attr:`dim` at each of the indices + in the list, tuple or tensor. For instance, :code:`indices_or_sections=[2, 3]` and :code:`dim=0` + would result in the tensors :code:`input[:2]`, :code:`input[2:3]`, and :code:`input[3:]`. + + If :attr:`indices_or_sections` is a tensor, it must be a zero-dimensional or one-dimensional + long tensor on the CPU. + + dim (int, optional): dimension along which to split the tensor. Default: ``0`` + + Example:: + + >>> x = torch.arange(8) + >>> torch.tensor_split(x, 3) + (tensor([0, 1, 2]), tensor([3, 4, 5]), tensor([6, 7])) + + >>> x = torch.arange(7) + >>> torch.tensor_split(x, 3) + (tensor([0, 1, 2]), tensor([3, 4]), tensor([5, 6])) + >>> torch.tensor_split(x, (1, 6)) + (tensor([0]), tensor([1, 2, 3, 4, 5]), tensor([6])) + + >>> x = torch.arange(14).reshape(2, 7) + >>> x + tensor([[ 0, 1, 2, 3, 4, 5, 6], + [ 7, 8, 9, 10, 11, 12, 13]]) + >>> torch.tensor_split(x, 3, dim=1) + (tensor([[0, 1, 2], + [7, 8, 9]]), + tensor([[ 3, 4], + [10, 11]]), + tensor([[ 5, 6], + [12, 13]])) + >>> torch.tensor_split(x, (1, 6), dim=1) + (tensor([[0], + [7]]), + tensor([[ 1, 2, 3, 4, 5], + [ 8, 9, 10, 11, 12]]), + tensor([[ 6], + [13]])) + """ + +@overload +def tensor_split( + input: Tensor, + indices: Sequence[_int | SymInt], + dim: _int = 0, +) -> tuple[Tensor, ...]: + r""" + tensor_split(input, indices_or_sections, dim=0) -> List of Tensors + + Splits a tensor into multiple sub-tensors, all of which are views of :attr:`input`, + along dimension :attr:`dim` according to the indices or number of sections specified + by :attr:`indices_or_sections`. This function is based on NumPy's + :func:`numpy.array_split`. + + Args: + input (Tensor): the tensor to split + indices_or_sections (Tensor, int or list or tuple of ints): + If :attr:`indices_or_sections` is an integer ``n`` or a zero dimensional long tensor + with value ``n``, :attr:`input` is split into ``n`` sections along dimension :attr:`dim`. + If :attr:`input` is divisible by ``n`` along dimension :attr:`dim`, each + section will be of equal size, :code:`input.size(dim) / n`. If :attr:`input` + is not divisible by ``n``, the sizes of the first :code:`int(input.size(dim) % n)` + sections will have size :code:`int(input.size(dim) / n) + 1`, and the rest will + have size :code:`int(input.size(dim) / n)`. + + If :attr:`indices_or_sections` is a list or tuple of ints, or a one-dimensional long + tensor, then :attr:`input` is split along dimension :attr:`dim` at each of the indices + in the list, tuple or tensor. For instance, :code:`indices_or_sections=[2, 3]` and :code:`dim=0` + would result in the tensors :code:`input[:2]`, :code:`input[2:3]`, and :code:`input[3:]`. + + If :attr:`indices_or_sections` is a tensor, it must be a zero-dimensional or one-dimensional + long tensor on the CPU. + + dim (int, optional): dimension along which to split the tensor. Default: ``0`` + + Example:: + + >>> x = torch.arange(8) + >>> torch.tensor_split(x, 3) + (tensor([0, 1, 2]), tensor([3, 4, 5]), tensor([6, 7])) + + >>> x = torch.arange(7) + >>> torch.tensor_split(x, 3) + (tensor([0, 1, 2]), tensor([3, 4]), tensor([5, 6])) + >>> torch.tensor_split(x, (1, 6)) + (tensor([0]), tensor([1, 2, 3, 4, 5]), tensor([6])) + + >>> x = torch.arange(14).reshape(2, 7) + >>> x + tensor([[ 0, 1, 2, 3, 4, 5, 6], + [ 7, 8, 9, 10, 11, 12, 13]]) + >>> torch.tensor_split(x, 3, dim=1) + (tensor([[0, 1, 2], + [7, 8, 9]]), + tensor([[ 3, 4], + [10, 11]]), + tensor([[ 5, 6], + [12, 13]])) + >>> torch.tensor_split(x, (1, 6), dim=1) + (tensor([[0], + [7]]), + tensor([[ 1, 2, 3, 4, 5], + [ 8, 9, 10, 11, 12]]), + tensor([[ 6], + [13]])) + """ + +def threshold( + input: Tensor, + threshold: Number | _complex, + value: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: ... +def threshold_( + input: Tensor, + threshold: Number | _complex, + value: Number | _complex, +) -> Tensor: ... +def tile(input: Tensor, dims: Sequence[_int | SymInt]) -> Tensor: + r""" + tile(input, dims) -> Tensor + + Constructs a tensor by repeating the elements of :attr:`input`. + The :attr:`dims` argument specifies the number of repetitions + in each dimension. + + If :attr:`dims` specifies fewer dimensions than :attr:`input` has, then + ones are prepended to :attr:`dims` until all dimensions are specified. + For example, if :attr:`input` has shape (8, 6, 4, 2) and :attr:`dims` + is (2, 2), then :attr:`dims` is treated as (1, 1, 2, 2). + + Analogously, if :attr:`input` has fewer dimensions than :attr:`dims` + specifies, then :attr:`input` is treated as if it were unsqueezed at + dimension zero until it has as many dimensions as :attr:`dims` specifies. + For example, if :attr:`input` has shape (4, 2) and :attr:`dims` + is (3, 3, 2, 2), then :attr:`input` is treated as if it had the + shape (1, 1, 4, 2). + + .. note:: + + This function is similar to NumPy's tile function. + + Args: + input (Tensor): the tensor whose elements to repeat. + dims (tuple): the number of repetitions per dimension. + + Example:: + + >>> x = torch.tensor([1, 2, 3]) + >>> x.tile((2,)) + tensor([1, 2, 3, 1, 2, 3]) + >>> y = torch.tensor([[1, 2], [3, 4]]) + >>> torch.tile(y, (2, 2)) + tensor([[1, 2, 1, 2], + [3, 4, 3, 4], + [1, 2, 1, 2], + [3, 4, 3, 4]]) + """ + +def topk( + input: Tensor, + k: _int | SymInt, + dim: _int = -1, + largest: _bool = True, + sorted: _bool = True, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.topk: + r""" + topk(input, k, dim=None, largest=True, sorted=True, *, out=None) -> (Tensor, LongTensor) + + Returns the :attr:`k` largest elements of the given :attr:`input` tensor along + a given dimension. + + If :attr:`dim` is not given, the last dimension of the `input` is chosen. + + If :attr:`largest` is ``False`` then the `k` smallest elements are returned. + + A namedtuple of `(values, indices)` is returned with the `values` and + `indices` of the largest `k` elements of each row of the `input` tensor in the + given dimension `dim`. + + The boolean option :attr:`sorted` if ``True``, will make sure that the returned + `k` elements are themselves sorted + + .. note:: + When using `torch.topk`, the indices of tied elements are not guaranteed to be stable + and may vary across different invocations. + + Args: + input (Tensor): the input tensor. + k (int): the k in "top-k" + dim (int, optional): the dimension to sort along + largest (bool, optional): controls whether to return largest or + smallest elements + sorted (bool, optional): controls whether to return the elements + in sorted order + + Keyword args: + out (tuple, optional): the output tuple of (Tensor, LongTensor) that can be + optionally given to be used as output buffers + + Example:: + + >>> x = torch.arange(1., 6.) + >>> x + tensor([ 1., 2., 3., 4., 5.]) + >>> torch.topk(x, 3) + torch.return_types.topk(values=tensor([5., 4., 3.]), indices=tensor([4, 3, 2])) + """ + +def trace(input: Tensor) -> Tensor: + r""" + trace(input) -> Tensor + + Returns the sum of the elements of the diagonal of the input 2-D matrix. + + Example:: + + >>> x = torch.arange(1., 10.).view(3, 3) + >>> x + tensor([[ 1., 2., 3.], + [ 4., 5., 6.], + [ 7., 8., 9.]]) + >>> torch.trace(x) + tensor(15.) + """ + +@overload +def transpose(input: Tensor, dim0: _int, dim1: _int) -> Tensor: + r""" + transpose(input, dim0, dim1) -> Tensor + + Returns a tensor that is a transposed version of :attr:`input`. + The given dimensions :attr:`dim0` and :attr:`dim1` are swapped. + + If :attr:`input` is a strided tensor then the resulting :attr:`out` + tensor shares its underlying storage with the :attr:`input` tensor, so + changing the content of one would change the content of the other. + + If :attr:`input` is a :ref:`sparse tensor ` then the + resulting :attr:`out` tensor *does not* share the underlying storage + with the :attr:`input` tensor. + + If :attr:`input` is a :ref:`sparse tensor ` with compressed + layout (SparseCSR, SparseBSR, SparseCSC or SparseBSC) the arguments + :attr:`dim0` and :attr:`dim1` must be both batch dimensions, or must + both be sparse dimensions. The batch dimensions of a sparse tensor are the + dimensions preceding the sparse dimensions. + + .. note:: + Transpositions which interchange the sparse dimensions of a `SparseCSR` + or `SparseCSC` layout tensor will result in the layout changing between + the two options. Transposition of the sparse dimensions of a ` SparseBSR` + or `SparseBSC` layout tensor will likewise generate a result with the + opposite layout. + + + Args: + input (Tensor): the input tensor. + dim0 (int): the first dimension to be transposed + dim1 (int): the second dimension to be transposed + + Example:: + + >>> x = torch.randn(2, 3) + >>> x + tensor([[ 1.0028, -0.9893, 0.5809], + [-0.1669, 0.7299, 0.4942]]) + >>> torch.transpose(x, 0, 1) + tensor([[ 1.0028, -0.1669], + [-0.9893, 0.7299], + [ 0.5809, 0.4942]]) + + See also :func:`torch.t`. + """ + +@overload +def transpose( + input: Tensor, + dim0: str | EllipsisType | None, + dim1: str | EllipsisType | None, +) -> Tensor: + r""" + transpose(input, dim0, dim1) -> Tensor + + Returns a tensor that is a transposed version of :attr:`input`. + The given dimensions :attr:`dim0` and :attr:`dim1` are swapped. + + If :attr:`input` is a strided tensor then the resulting :attr:`out` + tensor shares its underlying storage with the :attr:`input` tensor, so + changing the content of one would change the content of the other. + + If :attr:`input` is a :ref:`sparse tensor ` then the + resulting :attr:`out` tensor *does not* share the underlying storage + with the :attr:`input` tensor. + + If :attr:`input` is a :ref:`sparse tensor ` with compressed + layout (SparseCSR, SparseBSR, SparseCSC or SparseBSC) the arguments + :attr:`dim0` and :attr:`dim1` must be both batch dimensions, or must + both be sparse dimensions. The batch dimensions of a sparse tensor are the + dimensions preceding the sparse dimensions. + + .. note:: + Transpositions which interchange the sparse dimensions of a `SparseCSR` + or `SparseCSC` layout tensor will result in the layout changing between + the two options. Transposition of the sparse dimensions of a ` SparseBSR` + or `SparseBSC` layout tensor will likewise generate a result with the + opposite layout. + + + Args: + input (Tensor): the input tensor. + dim0 (int): the first dimension to be transposed + dim1 (int): the second dimension to be transposed + + Example:: + + >>> x = torch.randn(2, 3) + >>> x + tensor([[ 1.0028, -0.9893, 0.5809], + [-0.1669, 0.7299, 0.4942]]) + >>> torch.transpose(x, 0, 1) + tensor([[ 1.0028, -0.1669], + [-0.9893, 0.7299], + [ 0.5809, 0.4942]]) + + See also :func:`torch.t`. + """ + +def transpose_copy( + input: Tensor, + dim0: _int, + dim1: _int, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.transpose`, but all output tensors + are freshly created instead of aliasing the input. + """ + +@overload +def trapezoid(y: Tensor, x: Tensor, *, dim: _int = -1) -> Tensor: + r""" + trapezoid(y, x=None, *, dx=None, dim=-1) -> Tensor + + Computes the `trapezoidal rule `_ along + :attr:`dim`. By default the spacing between elements is assumed to be 1, but + :attr:`dx` can be used to specify a different constant spacing, and :attr:`x` can be + used to specify arbitrary spacing along :attr:`dim`. Only one of :attr:`x` or :attr:`dx` should be specified. + + + Assuming :attr:`y` is a one-dimensional tensor with elements :math:`{y_0, y_1, ..., y_n}`, + the default computation is + + .. math:: + \begin{aligned} + \sum_{i = 1}^{n} \frac{1}{2} (y_i + y_{i-1}) + \end{aligned} + + When :attr:`dx` is specified the computation becomes + + .. math:: + \begin{aligned} + \sum_{i = 1}^{n} \frac{\Delta x}{2} (y_i + y_{i-1}) + \end{aligned} + + effectively multiplying the result by :attr:`dx`. When :attr:`x` is specified, + assuming :attr:`x` is also a one-dimensional tensor with + elements :math:`{x_0, x_1, ..., x_n}`, the computation becomes + + .. math:: + \begin{aligned} + \sum_{i = 1}^{n} \frac{(x_i - x_{i-1})}{2} (y_i + y_{i-1}) + \end{aligned} + + When :attr:`x` and :attr:`y` have the same size, the computation is as described above and no broadcasting is needed. + The broadcasting behavior of this function is as follows when their sizes are different. For both :attr:`x` + and :attr:`y`, the function computes the difference between consecutive elements along + dimension :attr:`dim`. This effectively creates two tensors, `x_diff` and `y_diff`, that have + the same shape as the original tensors except their lengths along the dimension :attr:`dim` is reduced by 1. + After that, those two tensors are broadcast together to compute final output as part of the trapezoidal rule. + See the examples below for details. + + .. note:: + The trapezoidal rule is a technique for approximating the definite integral of a function + by averaging its left and right Riemann sums. The approximation becomes more accurate as + the resolution of the partition increases. + + Arguments: + y (Tensor): Values to use when computing the trapezoidal rule. + x (Tensor): If specified, defines spacing between values as specified above. + + Keyword arguments: + dx (float): constant spacing between values. If neither :attr:`x` or :attr:`dx` + are specified then this defaults to 1. Effectively multiplies the result by its value. + dim (int): The dimension along which to compute the trapezoidal rule. + The last (inner-most) dimension by default. + + Examples:: + + >>> # Computes the trapezoidal rule in 1D, spacing is implicitly 1 + >>> y = torch.tensor([1, 5, 10]) + >>> torch.trapezoid(y) + tensor(10.5) + + >>> # Computes the same trapezoidal rule directly to verify + >>> (1 + 10 + 10) / 2 + 10.5 + + >>> # Computes the trapezoidal rule in 1D with constant spacing of 2 + >>> # NOTE: the result is the same as before, but multiplied by 2 + >>> torch.trapezoid(y, dx=2) + 21.0 + + >>> # Computes the trapezoidal rule in 1D with arbitrary spacing + >>> x = torch.tensor([1, 3, 6]) + >>> torch.trapezoid(y, x) + 28.5 + + >>> # Computes the same trapezoidal rule directly to verify + >>> ((3 - 1) * (1 + 5) + (6 - 3) * (5 + 10)) / 2 + 28.5 + + >>> # Computes the trapezoidal rule for each row of a 3x3 matrix + >>> y = torch.arange(9).reshape(3, 3) + tensor([[0, 1, 2], + [3, 4, 5], + [6, 7, 8]]) + >>> torch.trapezoid(y) + tensor([ 2., 8., 14.]) + + >>> # Computes the trapezoidal rule for each column of the matrix + >>> torch.trapezoid(y, dim=0) + tensor([ 6., 8., 10.]) + + >>> # Computes the trapezoidal rule for each row of a 3x3 ones matrix + >>> # with the same arbitrary spacing + >>> y = torch.ones(3, 3) + >>> x = torch.tensor([1, 3, 6]) + >>> torch.trapezoid(y, x) + array([5., 5., 5.]) + + >>> # Computes the trapezoidal rule for each row of a 3x3 ones matrix + >>> # with different arbitrary spacing per row + >>> y = torch.ones(3, 3) + >>> x = torch.tensor([[1, 2, 3], [1, 3, 5], [1, 4, 7]]) + >>> torch.trapezoid(y, x) + array([2., 4., 6.]) + """ + +@overload +def trapezoid( + y: Tensor, + *, + dx: Number | _complex = 1, + dim: _int = -1, +) -> Tensor: + r""" + trapezoid(y, x=None, *, dx=None, dim=-1) -> Tensor + + Computes the `trapezoidal rule `_ along + :attr:`dim`. By default the spacing between elements is assumed to be 1, but + :attr:`dx` can be used to specify a different constant spacing, and :attr:`x` can be + used to specify arbitrary spacing along :attr:`dim`. Only one of :attr:`x` or :attr:`dx` should be specified. + + + Assuming :attr:`y` is a one-dimensional tensor with elements :math:`{y_0, y_1, ..., y_n}`, + the default computation is + + .. math:: + \begin{aligned} + \sum_{i = 1}^{n} \frac{1}{2} (y_i + y_{i-1}) + \end{aligned} + + When :attr:`dx` is specified the computation becomes + + .. math:: + \begin{aligned} + \sum_{i = 1}^{n} \frac{\Delta x}{2} (y_i + y_{i-1}) + \end{aligned} + + effectively multiplying the result by :attr:`dx`. When :attr:`x` is specified, + assuming :attr:`x` is also a one-dimensional tensor with + elements :math:`{x_0, x_1, ..., x_n}`, the computation becomes + + .. math:: + \begin{aligned} + \sum_{i = 1}^{n} \frac{(x_i - x_{i-1})}{2} (y_i + y_{i-1}) + \end{aligned} + + When :attr:`x` and :attr:`y` have the same size, the computation is as described above and no broadcasting is needed. + The broadcasting behavior of this function is as follows when their sizes are different. For both :attr:`x` + and :attr:`y`, the function computes the difference between consecutive elements along + dimension :attr:`dim`. This effectively creates two tensors, `x_diff` and `y_diff`, that have + the same shape as the original tensors except their lengths along the dimension :attr:`dim` is reduced by 1. + After that, those two tensors are broadcast together to compute final output as part of the trapezoidal rule. + See the examples below for details. + + .. note:: + The trapezoidal rule is a technique for approximating the definite integral of a function + by averaging its left and right Riemann sums. The approximation becomes more accurate as + the resolution of the partition increases. + + Arguments: + y (Tensor): Values to use when computing the trapezoidal rule. + x (Tensor): If specified, defines spacing between values as specified above. + + Keyword arguments: + dx (float): constant spacing between values. If neither :attr:`x` or :attr:`dx` + are specified then this defaults to 1. Effectively multiplies the result by its value. + dim (int): The dimension along which to compute the trapezoidal rule. + The last (inner-most) dimension by default. + + Examples:: + + >>> # Computes the trapezoidal rule in 1D, spacing is implicitly 1 + >>> y = torch.tensor([1, 5, 10]) + >>> torch.trapezoid(y) + tensor(10.5) + + >>> # Computes the same trapezoidal rule directly to verify + >>> (1 + 10 + 10) / 2 + 10.5 + + >>> # Computes the trapezoidal rule in 1D with constant spacing of 2 + >>> # NOTE: the result is the same as before, but multiplied by 2 + >>> torch.trapezoid(y, dx=2) + 21.0 + + >>> # Computes the trapezoidal rule in 1D with arbitrary spacing + >>> x = torch.tensor([1, 3, 6]) + >>> torch.trapezoid(y, x) + 28.5 + + >>> # Computes the same trapezoidal rule directly to verify + >>> ((3 - 1) * (1 + 5) + (6 - 3) * (5 + 10)) / 2 + 28.5 + + >>> # Computes the trapezoidal rule for each row of a 3x3 matrix + >>> y = torch.arange(9).reshape(3, 3) + tensor([[0, 1, 2], + [3, 4, 5], + [6, 7, 8]]) + >>> torch.trapezoid(y) + tensor([ 2., 8., 14.]) + + >>> # Computes the trapezoidal rule for each column of the matrix + >>> torch.trapezoid(y, dim=0) + tensor([ 6., 8., 10.]) + + >>> # Computes the trapezoidal rule for each row of a 3x3 ones matrix + >>> # with the same arbitrary spacing + >>> y = torch.ones(3, 3) + >>> x = torch.tensor([1, 3, 6]) + >>> torch.trapezoid(y, x) + array([5., 5., 5.]) + + >>> # Computes the trapezoidal rule for each row of a 3x3 ones matrix + >>> # with different arbitrary spacing per row + >>> y = torch.ones(3, 3) + >>> x = torch.tensor([[1, 2, 3], [1, 3, 5], [1, 4, 7]]) + >>> torch.trapezoid(y, x) + array([2., 4., 6.]) + """ + +@overload +def trapz(y: Tensor, *, dx: _float = 1, dim: _int = -1) -> Tensor: + r""" + trapz(y, x, *, dim=-1) -> Tensor + + Alias for :func:`torch.trapezoid`. + """ + +@overload +def trapz(y: Tensor, x: Tensor, *, dim: _int = -1) -> Tensor: + r""" + trapz(y, x, *, dim=-1) -> Tensor + + Alias for :func:`torch.trapezoid`. + """ + +def triangular_solve( + input: Tensor, + A: Tensor, + upper: _bool = True, + transpose: _bool = False, + unitriangular: _bool = False, + *, + out: Tensor | tuple[Tensor, ...] | list[Tensor] | None = None, +) -> torch.return_types.triangular_solve: + r""" + triangular_solve(b, A, upper=True, transpose=False, unitriangular=False, *, out=None) -> (Tensor, Tensor) + + Solves a system of equations with a square upper or lower triangular invertible matrix :math:`A` + and multiple right-hand sides :math:`b`. + + In symbols, it solves :math:`AX = b` and assumes :math:`A` is square upper-triangular + (or lower-triangular if :attr:`upper`\ `= False`) and does not have zeros on the diagonal. + + `torch.triangular_solve(b, A)` can take in 2D inputs `b, A` or inputs that are + batches of 2D matrices. If the inputs are batches, then returns + batched outputs `X` + + If the diagonal of :attr:`A` contains zeros or elements that are very close to zero and + :attr:`unitriangular`\ `= False` (default) or if the input matrix is badly conditioned, + the result may contain `NaN` s. + + Supports input of float, double, cfloat and cdouble data types. + + .. warning:: + + :func:`torch.triangular_solve` is deprecated in favor of :func:`torch.linalg.solve_triangular` + and will be removed in a future PyTorch release. + :func:`torch.linalg.solve_triangular` has its arguments reversed and does not return a + copy of one of the inputs. + + ``X = torch.triangular_solve(B, A).solution`` should be replaced with + + .. code:: python + + X = torch.linalg.solve_triangular(A, B) + + Args: + b (Tensor): multiple right-hand sides of size :math:`(*, m, k)` where + :math:`*` is zero of more batch dimensions + A (Tensor): the input triangular coefficient matrix of size :math:`(*, m, m)` + where :math:`*` is zero or more batch dimensions + upper (bool, optional): whether :math:`A` is upper or lower triangular. Default: ``True``. + transpose (bool, optional): solves `op(A)X = b` where `op(A) = A^T` if this flag is ``True``, + and `op(A) = A` if it is ``False``. Default: ``False``. + unitriangular (bool, optional): whether :math:`A` is unit triangular. + If True, the diagonal elements of :math:`A` are assumed to be + 1 and not referenced from :math:`A`. Default: ``False``. + + Keyword args: + out ((Tensor, Tensor), optional): tuple of two tensors to write + the output to. Ignored if `None`. Default: `None`. + + Returns: + A namedtuple `(solution, cloned_coefficient)` where `cloned_coefficient` + is a clone of :math:`A` and `solution` is the solution :math:`X` to :math:`AX = b` + (or whatever variant of the system of equations, depending on the keyword arguments.) + + Examples:: + + >>> A = torch.randn(2, 2).triu() + >>> A + tensor([[ 1.1527, -1.0753], + [ 0.0000, 0.7986]]) + >>> b = torch.randn(2, 3) + >>> b + tensor([[-0.0210, 2.3513, -1.5492], + [ 1.5429, 0.7403, -1.0243]]) + >>> torch.triangular_solve(b, A) + torch.return_types.triangular_solve( + solution=tensor([[ 1.7841, 2.9046, -2.5405], + [ 1.9320, 0.9270, -1.2826]]), + cloned_coefficient=tensor([[ 1.1527, -1.0753], + [ 0.0000, 0.7986]])) + """ + +def tril( + input: Tensor, + diagonal: _int | SymInt = 0, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + tril(input, diagonal=0, *, out=None) -> Tensor + + Returns the lower triangular part of the matrix (2-D tensor) or batch of matrices + :attr:`input`, the other elements of the result tensor :attr:`out` are set to 0. + + The lower triangular part of the matrix is defined as the elements on and + below the diagonal. + + The argument :attr:`diagonal` controls which diagonal to consider. If + :attr:`diagonal` = 0, all elements on and below the main diagonal are + retained. A positive value includes just as many diagonals above the main + diagonal, and similarly a negative value excludes just as many diagonals below + the main diagonal. The main diagonal are the set of indices + :math:`\lbrace (i, i) \rbrace` for :math:`i \in [0, \min\{d_{1}, d_{2}\} - 1]` where + :math:`d_{1}, d_{2}` are the dimensions of the matrix. + + Args: + input (Tensor): the input tensor. + diagonal (int, optional): the diagonal to consider + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(3, 3) + >>> a + tensor([[-1.0813, -0.8619, 0.7105], + [ 0.0935, 0.1380, 2.2112], + [-0.3409, -0.9828, 0.0289]]) + >>> torch.tril(a) + tensor([[-1.0813, 0.0000, 0.0000], + [ 0.0935, 0.1380, 0.0000], + [-0.3409, -0.9828, 0.0289]]) + + >>> b = torch.randn(4, 6) + >>> b + tensor([[ 1.2219, 0.5653, -0.2521, -0.2345, 1.2544, 0.3461], + [ 0.4785, -0.4477, 0.6049, 0.6368, 0.8775, 0.7145], + [ 1.1502, 3.2716, -1.1243, -0.5413, 0.3615, 0.6864], + [-0.0614, -0.7344, -1.3164, -0.7648, -1.4024, 0.0978]]) + >>> torch.tril(b, diagonal=1) + tensor([[ 1.2219, 0.5653, 0.0000, 0.0000, 0.0000, 0.0000], + [ 0.4785, -0.4477, 0.6049, 0.0000, 0.0000, 0.0000], + [ 1.1502, 3.2716, -1.1243, -0.5413, 0.0000, 0.0000], + [-0.0614, -0.7344, -1.3164, -0.7648, -1.4024, 0.0000]]) + >>> torch.tril(b, diagonal=-1) + tensor([[ 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000], + [ 0.4785, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000], + [ 1.1502, 3.2716, 0.0000, 0.0000, 0.0000, 0.0000], + [-0.0614, -0.7344, -1.3164, 0.0000, 0.0000, 0.0000]]) + """ + +def tril_indices( + row: _int, + col: _int, + offset: _int = 0, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + tril_indices(row, col, offset=0, *, dtype=torch.long, device='cpu', layout=torch.strided) -> Tensor + + Returns the indices of the lower triangular part of a :attr:`row`-by- + :attr:`col` matrix in a 2-by-N Tensor, where the first row contains row + coordinates of all indices and the second row contains column coordinates. + Indices are ordered based on rows and then columns. + + The lower triangular part of the matrix is defined as the elements on and + below the diagonal. + + The argument :attr:`offset` controls which diagonal to consider. If + :attr:`offset` = 0, all elements on and below the main diagonal are + retained. A positive value includes just as many diagonals above the main + diagonal, and similarly a negative value excludes just as many diagonals below + the main diagonal. The main diagonal are the set of indices + :math:`\lbrace (i, i) \rbrace` for :math:`i \in [0, \min\{d_{1}, d_{2}\} - 1]` + where :math:`d_{1}, d_{2}` are the dimensions of the matrix. + + .. note:: + When running on CUDA, ``row * col`` must be less than :math:`2^{59}` to + prevent overflow during calculation. + + Args: + row (``int``): number of rows in the 2-D matrix. + col (``int``): number of columns in the 2-D matrix. + offset (``int``): diagonal offset from the main diagonal. + Default: if not provided, 0. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor, + only support ``torch.int``, ``torch.long``. Default: if ``None``, ``torch.long``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + layout (:class:`torch.layout`, optional): currently only support ``torch.strided``. + + Example:: + + >>> a = torch.tril_indices(3, 3) + >>> a + tensor([[0, 1, 1, 2, 2, 2], + [0, 0, 1, 0, 1, 2]]) + + >>> a = torch.tril_indices(4, 3, -1) + >>> a + tensor([[1, 2, 2, 3, 3, 3], + [0, 0, 1, 0, 1, 2]]) + + >>> a = torch.tril_indices(4, 3, 1) + >>> a + tensor([[0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3], + [0, 1, 0, 1, 2, 0, 1, 2, 0, 1, 2]]) + """ + +def triplet_margin_loss( + anchor: Tensor, + positive: Tensor, + negative: Tensor, + margin: _float = 1.0, + p: _float = 2, + eps: _float = 1e-06, + swap: _bool = False, + reduction: _int = 1, +) -> Tensor: ... +def triu( + input: Tensor, + diagonal: _int | SymInt = 0, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + triu(input, diagonal=0, *, out=None) -> Tensor + + Returns the upper triangular part of a matrix (2-D tensor) or batch of matrices + :attr:`input`, the other elements of the result tensor :attr:`out` are set to 0. + + The upper triangular part of the matrix is defined as the elements on and + above the diagonal. + + The argument :attr:`diagonal` controls which diagonal to consider. If + :attr:`diagonal` = 0, all elements on and above the main diagonal are + retained. A positive value excludes just as many diagonals above the main + diagonal, and similarly a negative value includes just as many diagonals below + the main diagonal. The main diagonal are the set of indices + :math:`\lbrace (i, i) \rbrace` for :math:`i \in [0, \min\{d_{1}, d_{2}\} - 1]` where + :math:`d_{1}, d_{2}` are the dimensions of the matrix. + + Args: + input (Tensor): the input tensor. + diagonal (int, optional): the diagonal to consider + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(3, 3) + >>> a + tensor([[ 0.2309, 0.5207, 2.0049], + [ 0.2072, -1.0680, 0.6602], + [ 0.3480, -0.5211, -0.4573]]) + >>> torch.triu(a) + tensor([[ 0.2309, 0.5207, 2.0049], + [ 0.0000, -1.0680, 0.6602], + [ 0.0000, 0.0000, -0.4573]]) + >>> torch.triu(a, diagonal=1) + tensor([[ 0.0000, 0.5207, 2.0049], + [ 0.0000, 0.0000, 0.6602], + [ 0.0000, 0.0000, 0.0000]]) + >>> torch.triu(a, diagonal=-1) + tensor([[ 0.2309, 0.5207, 2.0049], + [ 0.2072, -1.0680, 0.6602], + [ 0.0000, -0.5211, -0.4573]]) + + >>> b = torch.randn(4, 6) + >>> b + tensor([[ 0.5876, -0.0794, -1.8373, 0.6654, 0.2604, 1.5235], + [-0.2447, 0.9556, -1.2919, 1.3378, -0.1768, -1.0857], + [ 0.4333, 0.3146, 0.6576, -1.0432, 0.9348, -0.4410], + [-0.9888, 1.0679, -1.3337, -1.6556, 0.4798, 0.2830]]) + >>> torch.triu(b, diagonal=1) + tensor([[ 0.0000, -0.0794, -1.8373, 0.6654, 0.2604, 1.5235], + [ 0.0000, 0.0000, -1.2919, 1.3378, -0.1768, -1.0857], + [ 0.0000, 0.0000, 0.0000, -1.0432, 0.9348, -0.4410], + [ 0.0000, 0.0000, 0.0000, 0.0000, 0.4798, 0.2830]]) + >>> torch.triu(b, diagonal=-1) + tensor([[ 0.5876, -0.0794, -1.8373, 0.6654, 0.2604, 1.5235], + [-0.2447, 0.9556, -1.2919, 1.3378, -0.1768, -1.0857], + [ 0.0000, 0.3146, 0.6576, -1.0432, 0.9348, -0.4410], + [ 0.0000, 0.0000, -1.3337, -1.6556, 0.4798, 0.2830]]) + """ + +def triu_indices( + row: _int, + col: _int, + offset: _int = 0, + *, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + triu_indices(row, col, offset=0, *, dtype=torch.long, device='cpu', layout=torch.strided) -> Tensor + + Returns the indices of the upper triangular part of a :attr:`row` by + :attr:`col` matrix in a 2-by-N Tensor, where the first row contains row + coordinates of all indices and the second row contains column coordinates. + Indices are ordered based on rows and then columns. + + The upper triangular part of the matrix is defined as the elements on and + above the diagonal. + + The argument :attr:`offset` controls which diagonal to consider. If + :attr:`offset` = 0, all elements on and above the main diagonal are + retained. A positive value excludes just as many diagonals above the main + diagonal, and similarly a negative value includes just as many diagonals below + the main diagonal. The main diagonal are the set of indices + :math:`\lbrace (i, i) \rbrace` for :math:`i \in [0, \min\{d_{1}, d_{2}\} - 1]` + where :math:`d_{1}, d_{2}` are the dimensions of the matrix. + + .. note:: + When running on CUDA, ``row * col`` must be less than :math:`2^{59}` to + prevent overflow during calculation. + + Args: + row (``int``): number of rows in the 2-D matrix. + col (``int``): number of columns in the 2-D matrix. + offset (``int``): diagonal offset from the main diagonal. + Default: if not provided, 0. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor, + only support ``torch.int``, ``torch.long``. Default: if ``None``, ``torch.long``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + layout (:class:`torch.layout`, optional): currently only support ``torch.strided``. + + Example:: + + >>> a = torch.triu_indices(3, 3) + >>> a + tensor([[0, 0, 0, 1, 1, 2], + [0, 1, 2, 1, 2, 2]]) + + >>> a = torch.triu_indices(4, 3, -1) + >>> a + tensor([[0, 0, 0, 1, 1, 1, 2, 2, 3], + [0, 1, 2, 0, 1, 2, 1, 2, 2]]) + + >>> a = torch.triu_indices(4, 3, 1) + >>> a + tensor([[0, 0, 1], + [1, 2, 2]]) + """ + +def true_divide( + input: Tensor | Number, + other: Tensor | Number, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + true_divide(dividend, divisor, *, out) -> Tensor + + Alias for :func:`torch.div` with ``rounding_mode=None``. + """ + +def trunc(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + trunc(input, *, out=None) -> Tensor + + Returns a new tensor with the truncated integer values of + the elements of :attr:`input`. + + For integer inputs, follows the array-api convention of returning a + copy of the input tensor. + + Args: + input (Tensor): the input tensor. + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.randn(4) + >>> a + tensor([ 3.4742, 0.5466, -0.8008, -0.9079]) + >>> torch.trunc(a) + tensor([ 3., 0., -0., -0.]) + """ + +def trunc_(input: Tensor) -> Tensor: ... +@overload +def unbind(input: Tensor, dim: _int = 0) -> tuple[Tensor, ...]: + r""" + unbind(input, dim=0) -> seq + + Removes a tensor dimension. + + Returns a tuple of all slices along a given dimension, already without it. + + Arguments: + input (Tensor): the tensor to unbind + dim (int): dimension to remove + + Example:: + + >>> torch.unbind(torch.tensor([[1, 2, 3], + >>> [4, 5, 6], + >>> [7, 8, 9]])) + (tensor([1, 2, 3]), tensor([4, 5, 6]), tensor([7, 8, 9])) + """ + +@overload +def unbind( + input: Tensor, + dim: str | EllipsisType | None, +) -> tuple[Tensor, ...]: + r""" + unbind(input, dim=0) -> seq + + Removes a tensor dimension. + + Returns a tuple of all slices along a given dimension, already without it. + + Arguments: + input (Tensor): the tensor to unbind + dim (int): dimension to remove + + Example:: + + >>> torch.unbind(torch.tensor([[1, 2, 3], + >>> [4, 5, 6], + >>> [7, 8, 9]])) + (tensor([1, 2, 3]), tensor([4, 5, 6]), tensor([7, 8, 9])) + """ + +def unbind_copy( + input: Tensor, + dim: _int = 0, + *, + out: tuple[Tensor, ...] | list[Tensor] | None = None, +) -> None: + r""" + Performs the same operation as :func:`torch.unbind`, but all output tensors + are freshly created instead of aliasing the input. + """ + +@overload +def unflatten( + input: Tensor, + dim: str | EllipsisType | None, + sizes: Sequence[_int | SymInt], + names: Sequence[str | EllipsisType | None], +) -> Tensor: + r""" + unflatten(input, dim, sizes) -> Tensor + + Expands a dimension of the input tensor over multiple dimensions. + + .. seealso:: + + :func:`torch.flatten` the inverse of this function. It coalesces several dimensions into one. + + Args: + input (Tensor): the input tensor. + dim (int): Dimension to be unflattened, specified as an index into + ``input.shape``. + sizes (Tuple[int]): New shape of the unflattened dimension. + One of its elements can be `-1` in which case the corresponding output + dimension is inferred. Otherwise, the product of ``sizes`` *must* + equal ``input.shape[dim]``. + + Returns: + A View of input with the specified dimension unflattened. + + Examples:: + >>> torch.unflatten(torch.randn(3, 4, 1), 1, (2, 2)).shape + torch.Size([3, 2, 2, 1]) + >>> torch.unflatten(torch.randn(3, 4, 1), 1, (-1, 2)).shape + torch.Size([3, 2, 2, 1]) + >>> torch.unflatten(torch.randn(5, 12, 3), -2, (2, 2, 3, 1, 1)).shape + torch.Size([5, 2, 2, 3, 1, 1, 3]) + """ + +@overload +def unflatten( + input: Tensor, + dim: _int, + sizes: Sequence[_int | SymInt], +) -> Tensor: + r""" + unflatten(input, dim, sizes) -> Tensor + + Expands a dimension of the input tensor over multiple dimensions. + + .. seealso:: + + :func:`torch.flatten` the inverse of this function. It coalesces several dimensions into one. + + Args: + input (Tensor): the input tensor. + dim (int): Dimension to be unflattened, specified as an index into + ``input.shape``. + sizes (Tuple[int]): New shape of the unflattened dimension. + One of its elements can be `-1` in which case the corresponding output + dimension is inferred. Otherwise, the product of ``sizes`` *must* + equal ``input.shape[dim]``. + + Returns: + A View of input with the specified dimension unflattened. + + Examples:: + >>> torch.unflatten(torch.randn(3, 4, 1), 1, (2, 2)).shape + torch.Size([3, 2, 2, 1]) + >>> torch.unflatten(torch.randn(3, 4, 1), 1, (-1, 2)).shape + torch.Size([3, 2, 2, 1]) + >>> torch.unflatten(torch.randn(5, 12, 3), -2, (2, 2, 3, 1, 1)).shape + torch.Size([5, 2, 2, 3, 1, 1, 3]) + """ + +def unfold_copy( + input: Tensor, + dimension: _int, + size: _int, + step: _int, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.unfold`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def unique_dim( + input: Tensor, + dim: _int, + sorted: _bool = True, + return_inverse: _bool = False, + return_counts: _bool = False, +) -> tuple[Tensor, Tensor, Tensor]: ... +def unsafe_chunk( + input: Tensor, + chunks: _int, + dim: _int = 0, +) -> tuple[Tensor, ...]: + r""" + unsafe_chunk(input, chunks, dim=0) -> List of Tensors + + Works like :func:`torch.chunk` but without enforcing the autograd restrictions + on inplace modification of the outputs. + + .. warning:: + This function is safe to use as long as only the input, or only the outputs + are modified inplace after calling this function. It is user's + responsibility to ensure that is the case. If both the input and one or more + of the outputs are modified inplace, gradients computed by autograd will be + silently incorrect. + """ + +def unsafe_split( + input: Tensor, + split_size: _int | SymInt, + dim: _int = 0, +) -> tuple[Tensor, ...]: + r""" + unsafe_split(tensor, split_size_or_sections, dim=0) -> List of Tensors + + Works like :func:`torch.split` but without enforcing the autograd restrictions + on inplace modification of the outputs. + + .. warning:: + This function is safe to use as long as only the input, or only the outputs + are modified inplace after calling this function. It is user's + responsibility to ensure that is the case. If both the input and one or more + of the outputs are modified inplace, gradients computed by autograd will be + silently incorrect. + """ + +def unsafe_split_with_sizes( + input: Tensor, + split_sizes: Sequence[_int | SymInt], + dim: _int = 0, +) -> tuple[Tensor, ...]: ... +def unsqueeze(input: Tensor, dim: _int) -> Tensor: + r""" + unsqueeze(input, dim) -> Tensor + + Returns a new tensor with a dimension of size one inserted at the + specified position. + + The returned tensor shares the same underlying data with this tensor. + + A :attr:`dim` value within the range ``[-input.dim() - 1, input.dim() + 1)`` + can be used. Negative :attr:`dim` will correspond to :meth:`unsqueeze` + applied at :attr:`dim` = ``dim + input.dim() + 1``. + + Args: + input (Tensor): the input tensor. + dim (int): the index at which to insert the singleton dimension + + Example:: + + >>> x = torch.tensor([1, 2, 3, 4]) + >>> torch.unsqueeze(x, 0) + tensor([[ 1, 2, 3, 4]]) + >>> torch.unsqueeze(x, 1) + tensor([[ 1], + [ 2], + [ 3], + [ 4]]) + """ + +def unsqueeze_copy( + input: Tensor, + dim: _int, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.unsqueeze`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def values_copy(input: Tensor, *, out: Tensor | None = None) -> Tensor: + r""" + Performs the same operation as :func:`torch.values`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def vander( + x: Tensor, + N: _int | None = None, + increasing: _bool = False, +) -> Tensor: + r""" + vander(x, N=None, increasing=False) -> Tensor + + Generates a Vandermonde matrix. + + The columns of the output matrix are elementwise powers of the input vector :math:`x^{(N-1)}, x^{(N-2)}, ..., x^0`. + If increasing is True, the order of the columns is reversed :math:`x^0, x^1, ..., x^{(N-1)}`. Such a + matrix with a geometric progression in each row is named for Alexandre-Theophile Vandermonde. + + Arguments: + x (Tensor): 1-D input tensor. + N (int, optional): Number of columns in the output. If N is not specified, + a square array is returned :math:`(N = len(x))`. + increasing (bool, optional): Order of the powers of the columns. If True, + the powers increase from left to right, if False (the default) they are reversed. + + Returns: + Tensor: Vandermonde matrix. If increasing is False, the first column is :math:`x^{(N-1)}`, + the second :math:`x^{(N-2)}` and so forth. If increasing is True, the columns + are :math:`x^0, x^1, ..., x^{(N-1)}`. + + Example:: + + >>> x = torch.tensor([1, 2, 3, 5]) + >>> torch.vander(x) + tensor([[ 1, 1, 1, 1], + [ 8, 4, 2, 1], + [ 27, 9, 3, 1], + [125, 25, 5, 1]]) + >>> torch.vander(x, N=3) + tensor([[ 1, 1, 1], + [ 4, 2, 1], + [ 9, 3, 1], + [25, 5, 1]]) + >>> torch.vander(x, N=3, increasing=True) + tensor([[ 1, 1, 1], + [ 1, 2, 4], + [ 1, 3, 9], + [ 1, 5, 25]]) + """ + +@overload +def var( + input: Tensor, + dim: _int | _size | None, + unbiased: _bool = True, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + var(input, dim=None, *, correction=1, keepdim=False, out=None) -> Tensor + + Calculates the variance over the dimensions specified by :attr:`dim`. :attr:`dim` + can be a single dimension, list of dimensions, or ``None`` to reduce over all + dimensions. + + The variance (:math:`\sigma^2`) is calculated as + + .. math:: \sigma^2 = \frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2 + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.var(a, dim=1, keepdim=True) + tensor([[1.0631], + [0.5590], + [1.4893], + [0.8258]]) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def var( + input: Tensor, + dim: _int | _size | None = None, + *, + correction: Number | _complex | None = None, + keepdim: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + var(input, dim=None, *, correction=1, keepdim=False, out=None) -> Tensor + + Calculates the variance over the dimensions specified by :attr:`dim`. :attr:`dim` + can be a single dimension, list of dimensions, or ``None`` to reduce over all + dimensions. + + The variance (:math:`\sigma^2`) is calculated as + + .. math:: \sigma^2 = \frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2 + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.var(a, dim=1, keepdim=True) + tensor([[1.0631], + [0.5590], + [1.4893], + [0.8258]]) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def var(input: Tensor, unbiased: _bool = True) -> Tensor: + r""" + var(input, dim=None, *, correction=1, keepdim=False, out=None) -> Tensor + + Calculates the variance over the dimensions specified by :attr:`dim`. :attr:`dim` + can be a single dimension, list of dimensions, or ``None`` to reduce over all + dimensions. + + The variance (:math:`\sigma^2`) is calculated as + + .. math:: \sigma^2 = \frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2 + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.var(a, dim=1, keepdim=True) + tensor([[1.0631], + [0.5590], + [1.4893], + [0.8258]]) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def var( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + *, + correction: Number | _complex | None = None, + keepdim: _bool = False, + out: Tensor | None = None, +) -> Tensor: + r""" + var(input, dim=None, *, correction=1, keepdim=False, out=None) -> Tensor + + Calculates the variance over the dimensions specified by :attr:`dim`. :attr:`dim` + can be a single dimension, list of dimensions, or ``None`` to reduce over all + dimensions. + + The variance (:math:`\sigma^2`) is calculated as + + .. math:: \sigma^2 = \frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2 + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.var(a, dim=1, keepdim=True) + tensor([[1.0631], + [0.5590], + [1.4893], + [0.8258]]) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def var( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + unbiased: _bool = True, + keepdim: _bool = False, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + var(input, dim=None, *, correction=1, keepdim=False, out=None) -> Tensor + + Calculates the variance over the dimensions specified by :attr:`dim`. :attr:`dim` + can be a single dimension, list of dimensions, or ``None`` to reduce over all + dimensions. + + The variance (:math:`\sigma^2`) is calculated as + + .. math:: \sigma^2 = \frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2 + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.var(a, dim=1, keepdim=True) + tensor([[1.0631], + [0.5590], + [1.4893], + [0.8258]]) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def var_mean( + input: Tensor, + dim: _int | _size | None, + unbiased: _bool = True, + keepdim: _bool = False, +) -> tuple[Tensor, Tensor]: + r""" + var_mean(input, dim=None, *, correction=1, keepdim=False, out=None) -> (Tensor, Tensor) + + Calculates the variance and mean over the dimensions specified by :attr:`dim`. + :attr:`dim` can be a single dimension, list of dimensions, or ``None`` to + reduce over all dimensions. + + The variance (:math:`\sigma^2`) is calculated as + + .. math:: \sigma^2 = \frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2 + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Returns: + A tuple (var, mean) containing the variance and mean. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.var_mean(a, dim=0, keepdim=True) + (tensor([[1.5926, 1.0056, 1.2005, 0.3646]]), + tensor([[ 0.0645, 0.4485, 0.8707, -0.0665]])) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def var_mean( + input: Tensor, + dim: _int | _size | None = None, + *, + correction: Number | _complex | None = None, + keepdim: _bool = False, +) -> tuple[Tensor, Tensor]: + r""" + var_mean(input, dim=None, *, correction=1, keepdim=False, out=None) -> (Tensor, Tensor) + + Calculates the variance and mean over the dimensions specified by :attr:`dim`. + :attr:`dim` can be a single dimension, list of dimensions, or ``None`` to + reduce over all dimensions. + + The variance (:math:`\sigma^2`) is calculated as + + .. math:: \sigma^2 = \frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2 + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Returns: + A tuple (var, mean) containing the variance and mean. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.var_mean(a, dim=0, keepdim=True) + (tensor([[1.5926, 1.0056, 1.2005, 0.3646]]), + tensor([[ 0.0645, 0.4485, 0.8707, -0.0665]])) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def var_mean( + input: Tensor, + unbiased: _bool = True, +) -> tuple[Tensor, Tensor]: + r""" + var_mean(input, dim=None, *, correction=1, keepdim=False, out=None) -> (Tensor, Tensor) + + Calculates the variance and mean over the dimensions specified by :attr:`dim`. + :attr:`dim` can be a single dimension, list of dimensions, or ``None`` to + reduce over all dimensions. + + The variance (:math:`\sigma^2`) is calculated as + + .. math:: \sigma^2 = \frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2 + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Returns: + A tuple (var, mean) containing the variance and mean. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.var_mean(a, dim=0, keepdim=True) + (tensor([[1.5926, 1.0056, 1.2005, 0.3646]]), + tensor([[ 0.0645, 0.4485, 0.8707, -0.0665]])) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def var_mean( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + *, + correction: Number | _complex | None = None, + keepdim: _bool = False, +) -> tuple[Tensor, Tensor]: + r""" + var_mean(input, dim=None, *, correction=1, keepdim=False, out=None) -> (Tensor, Tensor) + + Calculates the variance and mean over the dimensions specified by :attr:`dim`. + :attr:`dim` can be a single dimension, list of dimensions, or ``None`` to + reduce over all dimensions. + + The variance (:math:`\sigma^2`) is calculated as + + .. math:: \sigma^2 = \frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2 + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Returns: + A tuple (var, mean) containing the variance and mean. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.var_mean(a, dim=0, keepdim=True) + (tensor([[1.5926, 1.0056, 1.2005, 0.3646]]), + tensor([[ 0.0645, 0.4485, 0.8707, -0.0665]])) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +@overload +def var_mean( + input: Tensor, + dim: Sequence[str | EllipsisType | None], + unbiased: _bool = True, + keepdim: _bool = False, +) -> tuple[Tensor, Tensor]: + r""" + var_mean(input, dim=None, *, correction=1, keepdim=False, out=None) -> (Tensor, Tensor) + + Calculates the variance and mean over the dimensions specified by :attr:`dim`. + :attr:`dim` can be a single dimension, list of dimensions, or ``None`` to + reduce over all dimensions. + + The variance (:math:`\sigma^2`) is calculated as + + .. math:: \sigma^2 = \frac{1}{\max(0,~N - \delta N)}\sum_{i=0}^{N-1}(x_i-\bar{x})^2 + + where :math:`x` is the sample set of elements, :math:`\bar{x}` is the + sample mean, :math:`N` is the number of samples and :math:`\delta N` is + the :attr:`correction`. + + + + If :attr:`keepdim` is ``True``, the output tensor is of the same size + as :attr:`input` except in the dimension(s) :attr:`dim` where it is of size 1. + Otherwise, :attr:`dim` is squeezed (see :func:`torch.squeeze`), resulting in the + output tensor having 1 (or ``len(dim)``) fewer dimension(s). + + + Args: + input (Tensor): the input tensor. + + dim (int or tuple of ints, optional): the dimension or dimensions to reduce. + If ``None``, all dimensions are reduced. + + + Keyword args: + correction (int): difference between the sample size and sample degrees of freedom. + Defaults to `Bessel's correction`_, ``correction=1``. + + .. versionchanged:: 2.0 + Previously this argument was called ``unbiased`` and was a boolean + with ``True`` corresponding to ``correction=1`` and ``False`` being + ``correction=0``. + + keepdim (bool, optional): whether the output tensor has :attr:`dim` retained or not. Default: ``False``. + + out (Tensor, optional): the output tensor. + + Returns: + A tuple (var, mean) containing the variance and mean. + + Example: + + >>> a = torch.tensor( + ... [[ 0.2035, 1.2959, 1.8101, -0.4644], + ... [ 1.5027, -0.3270, 0.5905, 0.6538], + ... [-1.5745, 1.3330, -0.5596, -0.6548], + ... [ 0.1264, -0.5080, 1.6420, 0.1992]] + ... ) # fmt: skip + >>> torch.var_mean(a, dim=0, keepdim=True) + (tensor([[1.5926, 1.0056, 1.2005, 0.3646]]), + tensor([[ 0.0645, 0.4485, 0.8707, -0.0665]])) + + .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel%27s_correction + """ + +def vdot( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + vdot(input, other, *, out=None) -> Tensor + + Computes the dot product of two 1D vectors along a dimension. + + In symbols, this function computes + + .. math:: + + \sum_{i=1}^n \overline{x_i}y_i. + + where :math:`\overline{x_i}` denotes the conjugate for complex + vectors, and it is the identity for real vectors. + + .. note:: + + Unlike NumPy's vdot, torch.vdot intentionally only supports computing the dot product + of two 1D tensors with the same number of elements. + + .. seealso:: + + :func:`torch.linalg.vecdot` computes the dot product of two batches of vectors along a dimension. + + Args: + input (Tensor): first tensor in the dot product, must be 1D. Its conjugate is used if it's complex. + other (Tensor): second tensor in the dot product, must be 1D. + + Keyword args: + + .. note:: out (Tensor, optional): the output tensor. + + + Example:: + + >>> torch.vdot(torch.tensor([2, 3]), torch.tensor([2, 1])) + tensor(7) + >>> a = torch.tensor((1 +2j, 3 - 1j)) + >>> b = torch.tensor((2 +1j, 4 - 0j)) + >>> torch.vdot(a, b) + tensor([16.+1.j]) + >>> torch.vdot(b, a) + tensor([16.-1.j]) + """ + +def view_as_complex(input: Tensor) -> Tensor: + r""" + view_as_complex(input) -> Tensor + + Returns a view of :attr:`input` as a complex tensor. For an input complex + tensor of :attr:`size` :math:`m1, m2, \dots, mi, 2`, this function returns a + new complex tensor of :attr:`size` :math:`m1, m2, \dots, mi` where the last + dimension of the input tensor is expected to represent the real and imaginary + components of complex numbers. + + .. warning:: + :func:`view_as_complex` is only supported for tensors with + :class:`torch.dtype` ``torch.float64`` and ``torch.float32``. The input is + expected to have the last dimension of :attr:`size` 2. In addition, the + tensor must have a `stride` of 1 for its last dimension. The strides of all + other dimensions must be even numbers. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> x=torch.randn(4, 2) + >>> x + tensor([[ 1.6116, -0.5772], + [-1.4606, -0.9120], + [ 0.0786, -1.7497], + [-0.6561, -1.6623]]) + >>> torch.view_as_complex(x) + tensor([(1.6116-0.5772j), (-1.4606-0.9120j), (0.0786-1.7497j), (-0.6561-1.6623j)]) + """ + +def view_as_complex_copy( + input: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.view_as_complex`, but all output tensors + are freshly created instead of aliasing the input. + """ + +def view_as_real(input: Tensor) -> Tensor: + r""" + view_as_real(input) -> Tensor + + Returns a view of :attr:`input` as a real tensor. For an input complex tensor of + :attr:`size` :math:`m1, m2, \dots, mi`, this function returns a new + real tensor of size :math:`m1, m2, \dots, mi, 2`, where the last dimension of size 2 + represents the real and imaginary components of complex numbers. + + .. warning:: + :func:`view_as_real` is only supported for tensors with ``complex dtypes``. + + Args: + input (Tensor): the input tensor. + + Example:: + + >>> x=torch.randn(4, dtype=torch.cfloat) + >>> x + tensor([(0.4737-0.3839j), (-0.2098-0.6699j), (0.3470-0.9451j), (-0.5174-1.3136j)]) + >>> torch.view_as_real(x) + tensor([[ 0.4737, -0.3839], + [-0.2098, -0.6699], + [ 0.3470, -0.9451], + [-0.5174, -1.3136]]) + """ + +def view_as_real_copy( + input: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.view_as_real`, but all output tensors + are freshly created instead of aliasing the input. + """ + +@overload +def view_copy( + input: Tensor, + dtype: _dtype, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.view`, but all output tensors + are freshly created instead of aliasing the input. + """ + +@overload +def view_copy( + input: Tensor, + size: Sequence[_int | SymInt], + *, + out: Tensor | None = None, +) -> Tensor: + r""" + Performs the same operation as :func:`torch.view`, but all output tensors + are freshly created instead of aliasing the input. + """ + +@overload +def vsplit(input: Tensor, sections: _int) -> tuple[Tensor, ...]: + r""" + vsplit(input, indices_or_sections) -> List of Tensors + + Splits :attr:`input`, a tensor with two or more dimensions, into multiple tensors + vertically according to :attr:`indices_or_sections`. Each split is a view of + :attr:`input`. + + This is equivalent to calling torch.tensor_split(input, indices_or_sections, dim=0) + (the split dimension is 0), except that if :attr:`indices_or_sections` is an integer + it must evenly divide the split dimension or a runtime error will be thrown. + + This function is based on NumPy's :func:`numpy.vsplit`. + + Args: + input (Tensor): tensor to split. + indices_or_sections (int or list or tuple of ints): See argument in :func:`torch.tensor_split`. + + Example:: + + >>> t = torch.arange(16.0).reshape(4,4) + >>> t + tensor([[ 0., 1., 2., 3.], + [ 4., 5., 6., 7.], + [ 8., 9., 10., 11.], + [12., 13., 14., 15.]]) + >>> torch.vsplit(t, 2) + (tensor([[0., 1., 2., 3.], + [4., 5., 6., 7.]]), + tensor([[ 8., 9., 10., 11.], + [12., 13., 14., 15.]])) + >>> torch.vsplit(t, [3, 6]) + (tensor([[ 0., 1., 2., 3.], + [ 4., 5., 6., 7.], + [ 8., 9., 10., 11.]]), + tensor([[12., 13., 14., 15.]]), + tensor([], size=(0, 4))) + """ + +@overload +def vsplit(input: Tensor, indices: _size) -> tuple[Tensor, ...]: + r""" + vsplit(input, indices_or_sections) -> List of Tensors + + Splits :attr:`input`, a tensor with two or more dimensions, into multiple tensors + vertically according to :attr:`indices_or_sections`. Each split is a view of + :attr:`input`. + + This is equivalent to calling torch.tensor_split(input, indices_or_sections, dim=0) + (the split dimension is 0), except that if :attr:`indices_or_sections` is an integer + it must evenly divide the split dimension or a runtime error will be thrown. + + This function is based on NumPy's :func:`numpy.vsplit`. + + Args: + input (Tensor): tensor to split. + indices_or_sections (int or list or tuple of ints): See argument in :func:`torch.tensor_split`. + + Example:: + + >>> t = torch.arange(16.0).reshape(4,4) + >>> t + tensor([[ 0., 1., 2., 3.], + [ 4., 5., 6., 7.], + [ 8., 9., 10., 11.], + [12., 13., 14., 15.]]) + >>> torch.vsplit(t, 2) + (tensor([[0., 1., 2., 3.], + [4., 5., 6., 7.]]), + tensor([[ 8., 9., 10., 11.], + [12., 13., 14., 15.]])) + >>> torch.vsplit(t, [3, 6]) + (tensor([[ 0., 1., 2., 3.], + [ 4., 5., 6., 7.], + [ 8., 9., 10., 11.]]), + tensor([[12., 13., 14., 15.]]), + tensor([], size=(0, 4))) + """ + +def vstack( + tensors: tuple[Tensor, ...] | list[Tensor], + *, + out: Tensor | None = None, +) -> Tensor: + r""" + vstack(tensors, *, out=None) -> Tensor + + Stack tensors in sequence vertically (row wise). + + This is equivalent to concatenation along the first axis after all 1-D tensors have been reshaped by :func:`torch.atleast_2d`. + + Args: + tensors (sequence of Tensors): sequence of tensors to concatenate + + Keyword args: + out (Tensor, optional): the output tensor. + + Example:: + + >>> a = torch.tensor([1, 2, 3]) + >>> b = torch.tensor([4, 5, 6]) + >>> torch.vstack((a,b)) + tensor([[1, 2, 3], + [4, 5, 6]]) + >>> a = torch.tensor([[1],[2],[3]]) + >>> b = torch.tensor([[4],[5],[6]]) + >>> torch.vstack((a,b)) + tensor([[1], + [2], + [3], + [4], + [5], + [6]]) + """ + +@overload +def where(condition: Tensor) -> tuple[Tensor, ...]: + r""" + where(condition, input, other, *, out=None) -> Tensor + + Return a tensor of elements selected from either :attr:`input` or :attr:`other`, depending on :attr:`condition`. + + The operation is defined as: + + .. math:: + \text{out}_i = \begin{cases} + \text{input}_i & \text{if } \text{condition}_i \\ + \text{other}_i & \text{otherwise} \\ + \end{cases} + + .. note:: + The tensors :attr:`condition`, :attr:`input`, :attr:`other` must be :ref:`broadcastable `. + + Arguments: + condition (BoolTensor): When True (nonzero), yield input, otherwise yield other + input (Tensor or Scalar): value (if :attr:`input` is a scalar) or values selected at indices + where :attr:`condition` is ``True`` + other (Tensor or Scalar): value (if :attr:`other` is a scalar) or values selected at indices + where :attr:`condition` is ``False`` + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + Tensor: A tensor of shape equal to the broadcasted shape of :attr:`condition`, :attr:`input`, :attr:`other` + + Example:: + + >>> x = torch.randn(3, 2) + >>> y = torch.ones(3, 2) + >>> x + tensor([[-0.4620, 0.3139], + [ 0.3898, -0.7197], + [ 0.0478, -0.1657]]) + >>> torch.where(x > 0, 1.0, 0.0) + tensor([[0., 1.], + [1., 0.], + [1., 0.]]) + >>> torch.where(x > 0, x, y) + tensor([[ 1.0000, 0.3139], + [ 0.3898, 1.0000], + [ 0.0478, 1.0000]]) + >>> x = torch.randn(2, 2, dtype=torch.double) + >>> x + tensor([[ 1.0779, 0.0383], + [-0.8785, -1.1089]], dtype=torch.float64) + >>> torch.where(x > 0, x, 0.) + tensor([[1.0779, 0.0383], + [0.0000, 0.0000]], dtype=torch.float64) + + .. function:: where(condition) -> tuple of LongTensor + :noindex: + + ``torch.where(condition)`` is identical to + ``torch.nonzero(condition, as_tuple=True)``. + + .. note:: + See also :func:`torch.nonzero`. + """ + +@overload +def where( + condition: Tensor, + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + where(condition, input, other, *, out=None) -> Tensor + + Return a tensor of elements selected from either :attr:`input` or :attr:`other`, depending on :attr:`condition`. + + The operation is defined as: + + .. math:: + \text{out}_i = \begin{cases} + \text{input}_i & \text{if } \text{condition}_i \\ + \text{other}_i & \text{otherwise} \\ + \end{cases} + + .. note:: + The tensors :attr:`condition`, :attr:`input`, :attr:`other` must be :ref:`broadcastable `. + + Arguments: + condition (BoolTensor): When True (nonzero), yield input, otherwise yield other + input (Tensor or Scalar): value (if :attr:`input` is a scalar) or values selected at indices + where :attr:`condition` is ``True`` + other (Tensor or Scalar): value (if :attr:`other` is a scalar) or values selected at indices + where :attr:`condition` is ``False`` + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + Tensor: A tensor of shape equal to the broadcasted shape of :attr:`condition`, :attr:`input`, :attr:`other` + + Example:: + + >>> x = torch.randn(3, 2) + >>> y = torch.ones(3, 2) + >>> x + tensor([[-0.4620, 0.3139], + [ 0.3898, -0.7197], + [ 0.0478, -0.1657]]) + >>> torch.where(x > 0, 1.0, 0.0) + tensor([[0., 1.], + [1., 0.], + [1., 0.]]) + >>> torch.where(x > 0, x, y) + tensor([[ 1.0000, 0.3139], + [ 0.3898, 1.0000], + [ 0.0478, 1.0000]]) + >>> x = torch.randn(2, 2, dtype=torch.double) + >>> x + tensor([[ 1.0779, 0.0383], + [-0.8785, -1.1089]], dtype=torch.float64) + >>> torch.where(x > 0, x, 0.) + tensor([[1.0779, 0.0383], + [0.0000, 0.0000]], dtype=torch.float64) + + .. function:: where(condition) -> tuple of LongTensor + :noindex: + + ``torch.where(condition)`` is identical to + ``torch.nonzero(condition, as_tuple=True)``. + + .. note:: + See also :func:`torch.nonzero`. + """ + +@overload +def where( + condition: Tensor, + self: Number | _complex, + other: Tensor, +) -> Tensor: + r""" + where(condition, input, other, *, out=None) -> Tensor + + Return a tensor of elements selected from either :attr:`input` or :attr:`other`, depending on :attr:`condition`. + + The operation is defined as: + + .. math:: + \text{out}_i = \begin{cases} + \text{input}_i & \text{if } \text{condition}_i \\ + \text{other}_i & \text{otherwise} \\ + \end{cases} + + .. note:: + The tensors :attr:`condition`, :attr:`input`, :attr:`other` must be :ref:`broadcastable `. + + Arguments: + condition (BoolTensor): When True (nonzero), yield input, otherwise yield other + input (Tensor or Scalar): value (if :attr:`input` is a scalar) or values selected at indices + where :attr:`condition` is ``True`` + other (Tensor or Scalar): value (if :attr:`other` is a scalar) or values selected at indices + where :attr:`condition` is ``False`` + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + Tensor: A tensor of shape equal to the broadcasted shape of :attr:`condition`, :attr:`input`, :attr:`other` + + Example:: + + >>> x = torch.randn(3, 2) + >>> y = torch.ones(3, 2) + >>> x + tensor([[-0.4620, 0.3139], + [ 0.3898, -0.7197], + [ 0.0478, -0.1657]]) + >>> torch.where(x > 0, 1.0, 0.0) + tensor([[0., 1.], + [1., 0.], + [1., 0.]]) + >>> torch.where(x > 0, x, y) + tensor([[ 1.0000, 0.3139], + [ 0.3898, 1.0000], + [ 0.0478, 1.0000]]) + >>> x = torch.randn(2, 2, dtype=torch.double) + >>> x + tensor([[ 1.0779, 0.0383], + [-0.8785, -1.1089]], dtype=torch.float64) + >>> torch.where(x > 0, x, 0.) + tensor([[1.0779, 0.0383], + [0.0000, 0.0000]], dtype=torch.float64) + + .. function:: where(condition) -> tuple of LongTensor + :noindex: + + ``torch.where(condition)`` is identical to + ``torch.nonzero(condition, as_tuple=True)``. + + .. note:: + See also :func:`torch.nonzero`. + """ + +@overload +def where( + condition: Tensor, + input: Tensor, + other: Number | _complex, +) -> Tensor: + r""" + where(condition, input, other, *, out=None) -> Tensor + + Return a tensor of elements selected from either :attr:`input` or :attr:`other`, depending on :attr:`condition`. + + The operation is defined as: + + .. math:: + \text{out}_i = \begin{cases} + \text{input}_i & \text{if } \text{condition}_i \\ + \text{other}_i & \text{otherwise} \\ + \end{cases} + + .. note:: + The tensors :attr:`condition`, :attr:`input`, :attr:`other` must be :ref:`broadcastable `. + + Arguments: + condition (BoolTensor): When True (nonzero), yield input, otherwise yield other + input (Tensor or Scalar): value (if :attr:`input` is a scalar) or values selected at indices + where :attr:`condition` is ``True`` + other (Tensor or Scalar): value (if :attr:`other` is a scalar) or values selected at indices + where :attr:`condition` is ``False`` + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + Tensor: A tensor of shape equal to the broadcasted shape of :attr:`condition`, :attr:`input`, :attr:`other` + + Example:: + + >>> x = torch.randn(3, 2) + >>> y = torch.ones(3, 2) + >>> x + tensor([[-0.4620, 0.3139], + [ 0.3898, -0.7197], + [ 0.0478, -0.1657]]) + >>> torch.where(x > 0, 1.0, 0.0) + tensor([[0., 1.], + [1., 0.], + [1., 0.]]) + >>> torch.where(x > 0, x, y) + tensor([[ 1.0000, 0.3139], + [ 0.3898, 1.0000], + [ 0.0478, 1.0000]]) + >>> x = torch.randn(2, 2, dtype=torch.double) + >>> x + tensor([[ 1.0779, 0.0383], + [-0.8785, -1.1089]], dtype=torch.float64) + >>> torch.where(x > 0, x, 0.) + tensor([[1.0779, 0.0383], + [0.0000, 0.0000]], dtype=torch.float64) + + .. function:: where(condition) -> tuple of LongTensor + :noindex: + + ``torch.where(condition)`` is identical to + ``torch.nonzero(condition, as_tuple=True)``. + + .. note:: + See also :func:`torch.nonzero`. + """ + +@overload +def where( + condition: Tensor, + self: Number | _complex, + other: Number | _complex, +) -> Tensor: + r""" + where(condition, input, other, *, out=None) -> Tensor + + Return a tensor of elements selected from either :attr:`input` or :attr:`other`, depending on :attr:`condition`. + + The operation is defined as: + + .. math:: + \text{out}_i = \begin{cases} + \text{input}_i & \text{if } \text{condition}_i \\ + \text{other}_i & \text{otherwise} \\ + \end{cases} + + .. note:: + The tensors :attr:`condition`, :attr:`input`, :attr:`other` must be :ref:`broadcastable `. + + Arguments: + condition (BoolTensor): When True (nonzero), yield input, otherwise yield other + input (Tensor or Scalar): value (if :attr:`input` is a scalar) or values selected at indices + where :attr:`condition` is ``True`` + other (Tensor or Scalar): value (if :attr:`other` is a scalar) or values selected at indices + where :attr:`condition` is ``False`` + + Keyword args: + out (Tensor, optional): the output tensor. + + Returns: + Tensor: A tensor of shape equal to the broadcasted shape of :attr:`condition`, :attr:`input`, :attr:`other` + + Example:: + + >>> x = torch.randn(3, 2) + >>> y = torch.ones(3, 2) + >>> x + tensor([[-0.4620, 0.3139], + [ 0.3898, -0.7197], + [ 0.0478, -0.1657]]) + >>> torch.where(x > 0, 1.0, 0.0) + tensor([[0., 1.], + [1., 0.], + [1., 0.]]) + >>> torch.where(x > 0, x, y) + tensor([[ 1.0000, 0.3139], + [ 0.3898, 1.0000], + [ 0.0478, 1.0000]]) + >>> x = torch.randn(2, 2, dtype=torch.double) + >>> x + tensor([[ 1.0779, 0.0383], + [-0.8785, -1.1089]], dtype=torch.float64) + >>> torch.where(x > 0, x, 0.) + tensor([[1.0779, 0.0383], + [0.0000, 0.0000]], dtype=torch.float64) + + .. function:: where(condition) -> tuple of LongTensor + :noindex: + + ``torch.where(condition)`` is identical to + ``torch.nonzero(condition, as_tuple=True)``. + + .. note:: + See also :func:`torch.nonzero`. + """ + +@overload +def xlogy( + input: Tensor, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + xlogy(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.special.xlogy`. + """ + +@overload +def xlogy( + self: Number | _complex, + other: Tensor, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + xlogy(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.special.xlogy`. + """ + +@overload +def xlogy( + input: Tensor, + other: Number | _complex, + *, + out: Tensor | None = None, +) -> Tensor: + r""" + xlogy(input, other, *, out=None) -> Tensor + + Alias for :func:`torch.special.xlogy`. + """ + +@overload +def xlogy_(input: Tensor, other: Tensor) -> Tensor: ... +@overload +def xlogy_(input: Tensor, other: Number | _complex) -> Tensor: ... +def zero_(input: Tensor) -> Tensor: ... +@overload +def zeros( + size: Sequence[_int | SymInt], + *, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + zeros(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with the scalar value `0`, with the shape defined + by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.zeros(2, 3) + tensor([[ 0., 0., 0.], + [ 0., 0., 0.]]) + + >>> torch.zeros(5) + tensor([ 0., 0., 0., 0., 0.]) + """ + +@overload +def zeros( + *size: _int | SymInt, + out: Tensor | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + zeros(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with the scalar value `0`, with the shape defined + by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.zeros(2, 3) + tensor([[ 0., 0., 0.], + [ 0., 0., 0.]]) + + >>> torch.zeros(5) + tensor([ 0., 0., 0., 0., 0.]) + """ + +@overload +def zeros( + size: _size, + *, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + zeros(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with the scalar value `0`, with the shape defined + by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.zeros(2, 3) + tensor([[ 0., 0., 0.], + [ 0., 0., 0.]]) + + >>> torch.zeros(5) + tensor([ 0., 0., 0., 0., 0.]) + """ + +@overload +def zeros( + *size: _int, + names: Sequence[str | EllipsisType | None] | None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + zeros(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) -> Tensor + + Returns a tensor filled with the scalar value `0`, with the shape defined + by the variable argument :attr:`size`. + + Args: + size (int...): a sequence of integers defining the shape of the output tensor. + Can be a variable number of arguments or a collection like a list or tuple. + + Keyword args: + out (Tensor, optional): the output tensor. + dtype (:class:`torch.dtype`, optional): the desired data type of returned tensor. + Default: if ``None``, uses a global default (see :func:`torch.set_default_dtype`). + layout (:class:`torch.layout`, optional): the desired layout of returned Tensor. + Default: ``torch.strided``. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, uses the current device for the default tensor type + (see :func:`torch.set_default_device`). :attr:`device` will be the CPU + for CPU tensor types and the current CUDA device for CUDA tensor types. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + + Example:: + + >>> torch.zeros(2, 3) + tensor([[ 0., 0., 0.], + [ 0., 0., 0.]]) + + >>> torch.zeros(5) + tensor([ 0., 0., 0., 0., 0.]) + """ + +def zeros_like( + input: Tensor, + *, + memory_format: memory_format | None = None, + dtype: _dtype | None = None, + layout: _layout | None = None, + device: DeviceLikeType | None = None, + pin_memory: _bool | None = False, + requires_grad: _bool | None = False, +) -> Tensor: + r""" + zeros_like(input, *, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format) -> Tensor + + Returns a tensor filled with the scalar value `0`, with the same size as + :attr:`input`. ``torch.zeros_like(input)`` is equivalent to + ``torch.zeros(input.size(), dtype=input.dtype, layout=input.layout, device=input.device)``. + + .. warning:: + As of 0.4, this function does not support an :attr:`out` keyword. As an alternative, + the old ``torch.zeros_like(input, out=output)`` is equivalent to + ``torch.zeros(input.size(), out=output)``. + + Args: + input (Tensor): the size of :attr:`input` will determine size of the output tensor. + + Keyword args: + dtype (:class:`torch.dtype`, optional): the desired data type of returned Tensor. + Default: if ``None``, defaults to the dtype of :attr:`input`. + layout (:class:`torch.layout`, optional): the desired layout of returned tensor. + Default: if ``None``, defaults to the layout of :attr:`input`. + device (:class:`torch.device`, optional): the desired device of returned tensor. + Default: if ``None``, defaults to the device of :attr:`input`. + requires_grad (bool, optional): If autograd should record operations on the + returned tensor. Default: ``False``. + memory_format (:class:`torch.memory_format`, optional): the desired memory format of + returned Tensor. Default: ``torch.preserve_format``. + + Example:: + + >>> input = torch.empty(2, 3) + >>> torch.zeros_like(input) + tensor([[ 0., 0., 0.], + [ 0., 0., 0.]]) + """ diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/__config__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/__config__.py new file mode 100644 index 0000000000000000000000000000000000000000..1187fab3713996932d4f3bad71bac243c6baff35 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/__config__.py @@ -0,0 +1,22 @@ +import torch + + +def show() -> str: + """ + Return a human-readable string with descriptions of the + configuration of PyTorch. + """ + return torch._C._show_config() + + +# TODO: In principle, we could provide more structured version/config +# information here. For now only CXX_FLAGS is exposed, as Timer +# uses them. +def _cxx_flags() -> str: + """Returns the CXX_FLAGS used when building PyTorch.""" + return torch._C._cxx_flags() + + +def parallel_info() -> str: + r"""Returns detailed string with parallelization settings""" + return torch._C._parallel_info() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/__future__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/__future__.py new file mode 100644 index 0000000000000000000000000000000000000000..f172ee3c8fe223aa316667f37f356e5b6658d20e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/__future__.py @@ -0,0 +1,75 @@ +_overwrite_module_params_on_conversion: bool = False +_swap_module_params_on_conversion: bool = False + + +def set_overwrite_module_params_on_conversion(value: bool) -> None: + """ + Sets whether to assign new tensors to the parameters instead of changing the + existing parameters in-place when converting an ``nn.Module``. + + When enabled, the following methods will assign new parameters to the module: + + #. ``module.{device}()`` (e.g. :meth:`nn.Module.cuda()`) for moving a module between devices + #. ``module.{dtype}()`` (e.g. :meth:`nn.Module.float()`) for converting a module to a different dtype + #. :meth:`nn.Module.to` + #. :meth:`nn.Module.to_empty` + + Args: + value (bool): Whether to assign new tensors or not. + + """ + global _overwrite_module_params_on_conversion + _overwrite_module_params_on_conversion = value + + +def get_overwrite_module_params_on_conversion() -> bool: + """ + Returns whether to assign new tensors to the parameters instead of changing the + existing parameters in-place when converting an :class:`torch.nn.Module`. Defaults to ``False``. + + See :func:`~torch.__future__.set_overwrite_module_params_on_conversion` for more information. + """ + return _overwrite_module_params_on_conversion + + +def set_swap_module_params_on_conversion(value: bool) -> None: + """ + Sets whether to use :func:`~torch.utils.swap_tensors` instead of setting ``.data`` to + change the existing parameters in-place when converting an ``nn.Module`` and instead + of ``param.copy_(state_dict[key])`` when loading a state dict into an ``nn.Module``. + + .. note:: + This function takes precedence over :func:`~torch.__future__.get_overwrite_module_params_on_conversion` + + When enabled, the following methods will swap the existing parameters in-place: + + #. ``module.{device}()`` (e.g. :meth:`nn.Module.cuda()`) for moving a module between devices + #. ``module.{dtype}()`` (e.g. :meth:`nn.Module.float()`) for converting a module to a different dtype + #. :meth:`nn.Module.to` + #. :meth:`nn.Module.to_empty` + #. :meth:`nn.Module.load_state_dict` + + The semantics for :meth:`~nn.Module.load_state_dict` when this is set are as follows: + + #. For each parameter/buffer, its corresponding ``state_dict['key']`` is transformed via + :meth:`~torch.Tensor.module_load` (i.e. ``res = param.module_load(state_dict['key'])``) + #. If necessary, ``res`` will be wrapped in an :class:`~nn.Parameter` + #. The parameter/buffer in the module will be swapped via :func:`~torch.utils.swap_tensors` + with ``res`` + + Args: + value (bool): Whether to use :func:`~torch.utils.swap_tensors` or not. + + """ + global _swap_module_params_on_conversion + _swap_module_params_on_conversion = value + + +def get_swap_module_params_on_conversion() -> bool: + """ + Returns whether to use :func:`~torch.utils.swap_tensors` instead of setting .data to + change the existing parameters in-place when converting an ``nn.Module``. Defaults to ``False``. + + See :func:`~torch.__future__.set_swap_module_params_on_conversion` for more information. + """ + return _swap_module_params_on_conversion diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1852fe9c5a9218ecc7a02a7c24ea3e95233d735d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/__init__.py @@ -0,0 +1,3045 @@ +""" +The torch package contains data structures for multi-dimensional +tensors and defines mathematical operations over these tensors. +Additionally, it provides many utilities for efficient serialization of +Tensors and arbitrary types, and other useful utilities. + +It has a CUDA counterpart, that enables you to run your tensor computations +on an NVIDIA GPU with compute capability >= 3.0. +""" + +# mypy: allow-untyped-defs + +import builtins +import ctypes +import functools +import glob +import importlib +import inspect +import math +import os +import platform +import sys +import textwrap +import threading +import warnings +from collections.abc import Callable as _Callable +from typing import ( + Any as _Any, + get_origin as _get_origin, + overload as _overload, + TYPE_CHECKING, + TypeGuard as _TypeGuard, + TypeVar as _TypeVar, +) +from typing_extensions import ( + deprecated as _deprecated, + ParamSpec as _ParamSpec, + TypeIs as _TypeIs, +) + + +# As a bunch of torch.packages internally still have this check +# we need to keep this. @todo: Remove tests that rely on this check as +# they are likely stale. +def _running_with_deploy() -> builtins.bool: + return False + + +from torch._utils import ( + _functionalize_sync as _sync, + _import_dotted_name, + classproperty, +) +from torch._utils_internal import ( + get_file_path, + prepare_multiprocessing_environment, + profiler_allow_cudagraph_cupti_lazy_reinit_cuda12, + USE_GLOBAL_DEPS, + USE_RTLD_GLOBAL_WITH_LIBTORCH, +) +from torch.torch_version import __version__ as __version__ + + +if TYPE_CHECKING: + from torch.types import Device, IntLikeType + + +__all__ = [ + "BoolStorage", + "BoolTensor", + "ByteStorage", + "ByteTensor", + "CharStorage", + "CharTensor", + "DoubleStorage", + "DoubleTensor", + "FloatStorage", + "FloatTensor", + "GradScaler", + "IntStorage", + "IntTensor", + "LongStorage", + "LongTensor", + "ShortStorage", + "ShortTensor", + "SymBool", + "SymFloat", + "SymInt", + "Tensor", + "TypedStorage", + "UntypedStorage", + "are_deterministic_algorithms_enabled", + "autocast", + "chunk", + "compile", + "cond", + "enable_grad", + "export", + "get_default_device", + "get_deterministic_debug_mode", + "get_device_module", + "get_float32_matmul_precision", + "get_rng_state", + "inference_mode", + "initial_seed", + "is_deterministic_algorithms_warn_only_enabled", + "is_storage", + "is_tensor", + "is_warn_always_enabled", + "load", + "lobpcg", + "manual_seed", + "matmul", + "no_grad", + "rand", + "randn", + "save", + "seed", + "set_default_device", + "set_default_tensor_type", + "set_deterministic_debug_mode", + "set_float32_matmul_precision", + "set_printoptions", + "set_rng_state", + "set_warn_always", + "split", + "stack", + "sym_float", + "sym_fresh_size", + "sym_int", + "sym_ite", + "sym_max", + "sym_min", + "sym_not", + "sym_sum", + "thread_safe_generator", + "typename", + "unravel_index", + "use_deterministic_algorithms", + "vmap", +] + +# Please keep this list sorted +if __all__ != sorted(__all__): + raise AssertionError("__all__ must be kept sorted") + +################################################################################ +# Load the extension module +################################################################################ + +# If PyTorch was built against the ROCm runtime wheels, then there will be +# a _rocm_init module and it will define an initialize() function which can +# prepare ROCm for use. See general documentation on ROCm runtime wheels: +# https://github.com/ROCm/TheRock/blob/main/docs/packaging/python_packaging.md +# Since this module is only ever added to the wheel if built for such a +# deployment, it is always safe to attempt. +try: + from . import _rocm_init # type: ignore[attr-defined] +except ImportError: + pass +else: + _rocm_init.initialize() + del _rocm_init + + +if sys.platform == "win32": + + def _load_dll_libraries() -> None: + import sysconfig + + from torch.version import cuda as cuda_version + + pfiles_path = os.getenv("ProgramFiles", r"C:\Program Files") + py_dll_path = os.path.join(sys.exec_prefix, "Library", "bin") + th_dll_path = os.path.join(os.path.dirname(__file__), "lib") + usebase_path = os.path.join( + sysconfig.get_config_var("userbase"), "Library", "bin" + ) + py_root_bin_path = os.path.join(sys.exec_prefix, "bin") + + # When users create a virtualenv that inherits the base environment, + # we will need to add the corresponding library directory into + # DLL search directories. Otherwise, it will rely on `PATH` which + # is dependent on user settings. + if sys.exec_prefix != sys.base_exec_prefix: + base_py_dll_path = os.path.join(sys.base_exec_prefix, "Library", "bin") + else: + base_py_dll_path = "" + + dll_paths = [ + p + for p in ( + th_dll_path, + py_dll_path, + base_py_dll_path, + usebase_path, + py_root_bin_path, + ) + if os.path.exists(p) + ] + + if not builtins.any( + os.path.exists(os.path.join(p, "nvToolsExt64_1.dll")) for p in dll_paths + ): + nvtoolsext_dll_path = os.path.join( + os.getenv( + "NVTOOLSEXT_PATH", + os.path.join(pfiles_path, "NVIDIA Corporation", "NvToolsExt"), + ), + "bin", + "x64", + ) + else: + nvtoolsext_dll_path = "" + + if cuda_version and builtins.all( + not glob.glob(os.path.join(p, "cudart64*.dll")) for p in dll_paths + ): + cuda_version_1 = cuda_version.replace(".", "_") + cuda_path_var = "CUDA_PATH_V" + cuda_version_1 + default_path = os.path.join( + pfiles_path, "NVIDIA GPU Computing Toolkit", "CUDA", f"v{cuda_version}" + ) + cuda_path = os.path.join(os.getenv(cuda_path_var, default_path), "bin") + else: + cuda_path = "" + + dll_paths.extend( + p for p in (nvtoolsext_dll_path, cuda_path) if os.path.exists(p) + ) + + kernel32 = ctypes.WinDLL("kernel32.dll", use_last_error=True) + with_load_library_flags = hasattr(kernel32, "AddDllDirectory") + prev_error_mode = kernel32.SetErrorMode(0x0001) + + kernel32.LoadLibraryW.restype = ctypes.c_void_p + if with_load_library_flags: + kernel32.LoadLibraryExW.restype = ctypes.c_void_p + + for dll_path in dll_paths: + os.add_dll_directory(dll_path) + + try: + ctypes.CDLL("vcruntime140.dll") + ctypes.CDLL("msvcp140.dll") + if platform.machine() != "ARM64": + ctypes.CDLL("vcruntime140_1.dll") + except OSError: + print( + textwrap.dedent( + """ + Microsoft Visual C++ Redistributable is not installed, this may lead to the DLL load failure. + It can be downloaded at https://aka.ms/vs/17/release/vc_redist.x64.exe + """ + ).strip() + ) + + dlls = glob.glob(os.path.join(th_dll_path, "*.dll")) + path_patched = False + for dll in dlls: + is_loaded = False + if with_load_library_flags: + res = kernel32.LoadLibraryExW(dll, None, 0x00001100) + last_error = ctypes.get_last_error() + if res is None and last_error != 126: + err = ctypes.WinError(last_error) + err.strerror += ( + f' Error loading "{dll}" or one of its dependencies.' + ) + raise err + elif res is not None: + is_loaded = True + if not is_loaded: + if not path_patched: + os.environ["PATH"] = ";".join(dll_paths + [os.environ["PATH"]]) + path_patched = True + res = kernel32.LoadLibraryW(dll) + if res is None: + err = ctypes.WinError(ctypes.get_last_error()) + err.strerror += ( + f' Error loading "{dll}" or one of its dependencies.' + ) + raise err + + kernel32.SetErrorMode(prev_error_mode) + + _load_dll_libraries() + del _load_dll_libraries + + +def _get_cuda_dep_paths(path: str, lib_folder: str, lib_name: str) -> list[str]: + # Libraries can either be in + # path/nvidia/lib_folder/lib or + # path/nvidia/cuXX/lib (since CUDA 13.0) or + # path/lib_folder/lib + from torch.version import cuda as cuda_version + + nvidia_lib_paths = glob.glob( + os.path.join(path, "nvidia", lib_folder, "lib", lib_name) + ) + if cuda_version is not None: + maj_cuda_version = cuda_version.split(".")[0] + nvidia_lib_paths += glob.glob( + os.path.join(path, "nvidia", f"cu{maj_cuda_version}", "lib", lib_name) + ) + lib_paths = glob.glob(os.path.join(path, lib_folder, "lib", lib_name)) + + return nvidia_lib_paths + lib_paths + + +def _preload_cuda_lib(lib_folder: str, lib_name: str, required: bool = True) -> None: # type: ignore[valid-type] + """Preloads cuda library if it could not be found otherwise.""" + # Should only be called on Linux if default path resolution have failed + if platform.system() != "Linux": + raise AssertionError(f"Should only be called on Linux, got {platform.system()}") + + lib_path = None + for path in sys.path: + candidate_lib_paths = _get_cuda_dep_paths(path, lib_folder, lib_name) + if candidate_lib_paths: + lib_path = candidate_lib_paths[0] + break + if not lib_path and required: + raise ValueError(f"{lib_name} not found in the system path {sys.path}") + if lib_path: + ctypes.CDLL(lib_path) + + +def _preload_cuda_deps(err: OSError | None = None) -> None: + cuda_libs: list[tuple[str, str]] = [ + # NOTE: Order matters! We must preload libcublasLt BEFORE libcublas to prevent + # libcublas from loading a mismatched system-wide libcublasLt via its RUNPATH. + # Without this, if a different CUDA Toolkit version exists in the system PATH, + # libcublas may load the wrong libcublasLt, causing symbol errors or runtime failures. + ("cublas", "libcublasLt.so.*[0-9]"), + ("cublas", "libcublas.so.*[0-9]"), + ("cudnn", "libcudnn.so.*[0-9]"), + ("cuda_nvrtc", "libnvrtc.so.*[0-9]"), + ("cuda_nvrtc", "libnvrtc-builtins.so.*[0-9]"), + ("cuda_runtime", "libcudart.so.*[0-9]"), + ("cuda_cupti", "libcupti.so.*[0-9]"), + ("cufft", "libcufft.so.*[0-9]"), + ("curand", "libcurand.so.*[0-9]"), + ("nvjitlink", "libnvJitLink.so.*[0-9]"), + ("cusparse", "libcusparse.so.*[0-9]"), + ("cusparselt", "libcusparseLt.so.*[0-9]"), + ("cusolver", "libcusolver.so.*[0-9]"), + ("nccl", "libnccl.so.*[0-9]"), + ("nvshmem", "libnvshmem_host.so.*[0-9]"), + ("cufile", "libcufile.so.*[0-9]"), + ] + # If error is passed, re-raise it if it's not about one of the abovementioned + # libraries + if err is not None and not [ + lib for _, lib in cuda_libs if lib.split(".", 1)[0] in err.args[0] + ]: + raise err + + # Otherwise, try to preload dependencies from site-packages + for lib_folder, lib_name in cuda_libs: + _preload_cuda_lib(lib_folder, lib_name) + + # libnvToolsExt is Optional Dependency + _preload_cuda_lib("nvtx", "libnvToolsExt.so.*[0-9]", required=False) + + +# See Note [Global dependencies] +def _load_global_deps() -> None: + if platform.system() == "Windows": + return + + # Determine the file extension based on the platform + lib_ext = ".dylib" if platform.system() == "Darwin" else ".so" + lib_name = f"libtorch_global_deps{lib_ext}" + here = os.path.abspath(__file__) + global_deps_lib_path = os.path.join(os.path.dirname(here), "lib", lib_name) + + try: + ctypes.CDLL(global_deps_lib_path, mode=ctypes.RTLD_GLOBAL) + # Workaround slim-wheel CUDA dependency bugs in cusparse and cudnn by preloading nvjitlink + # and nvrtc. In CUDA-12.4+ cusparse depends on nvjitlink, but does not have rpath when + # shipped as wheel, which results in OS picking wrong/older version of nvjitlink library + # if `LD_LIBRARY_PATH` is defined, see https://github.com/pytorch/pytorch/issues/138460 + # Similar issue exist in cudnn that dynamically loads nvrtc, unaware of its relative path. + # See https://github.com/pytorch/pytorch/issues/145580 + try: + with open("/proc/self/maps") as f: + _maps = f.read() + + # libtorch_global_deps.so always depends in cudart, check if its installed and loaded + if "libcudart.so" not in _maps: + return + # If all above-mentioned conditions are met, preload CUDA dependencies + _preload_cuda_deps() + except Exception: + pass + + except OSError as err: + # Can happen for wheel with cuda libs as PYPI deps + # As PyTorch is not purelib, but nvidia-*-cu12 is + _preload_cuda_deps(err) + ctypes.CDLL(global_deps_lib_path, mode=ctypes.RTLD_GLOBAL) + + +if (USE_RTLD_GLOBAL_WITH_LIBTORCH or os.getenv("TORCH_USE_RTLD_GLOBAL")) and ( + platform.system() != "Windows" +): + # Do it the hard way. You might want to load libtorch with RTLD_GLOBAL in a + # few circumstances: + # + # 1. You're in a build environment (e.g., fbcode) where + # libtorch_global_deps is not available, but you still need + # to get mkl to link in with RTLD_GLOBAL or it will just + # not work. + # + # 2. You're trying to run PyTorch under UBSAN and you need + # to ensure that only one copy of libtorch is loaded, so + # vptr checks work properly + # + # If you're using this setting, you must verify that all the libraries + # you load consistently use the same libstdc++, or you may have + # mysterious segfaults. + # + old_flags = sys.getdlopenflags() + sys.setdlopenflags(os.RTLD_GLOBAL | os.RTLD_LAZY) + + from torch._C import * # noqa: F403 + + sys.setdlopenflags(old_flags) + del old_flags + +else: + # Easy way. You want this most of the time, because it will prevent + # C++ symbols from libtorch clobbering C++ symbols from other + # libraries, leading to mysterious segfaults. + # + # If building in an environment where libtorch_global_deps isn't available + # like parts of fbsource, but where RTLD_GLOBAL causes segfaults, you will + # want USE_RTLD_GLOBAL_WITH_LIBTORCH = False and USE_GLOBAL_DEPS = False + # + # See Note [Global dependencies] + if USE_GLOBAL_DEPS: + _load_global_deps() + from torch._C import * # noqa: F403 + + +class SymInt: + """ + Like an int (including magic methods), but redirects all operations on the + wrapped node. This is used in particular to symbolically record operations + in the symbolic shape workflow. + """ + + def __init__(self, node): + # This field MUST be named node; C++ binding code assumes that this + # class has a field named node that stores SymNode + self.node = node + + def __bool__(self): + return builtins.bool(self != 0) + + def __int__(self): + return self.node.int_() + + def __index__(self): + return self.node.int_() + + # Magic methods installed by torch.fx.experimental.sym_node + + def __round__(self, ndigits=None): + return self + + def __truediv__(self, other): + if isinstance(other, (builtins.float, SymFloat)): + return sym_float(self).__float_truediv__(other) + if not isinstance(other, (builtins.int, SymInt)): + return NotImplemented + return self.__int_truediv__(other) + + def __rtruediv__(self, other): + if isinstance(other, (builtins.float, SymFloat)): + return sym_float(self).__rfloat_truediv__(other) + if not isinstance(other, (builtins.int, SymInt)): + return NotImplemented + return self.__rint_truediv__(other) + + def __floordiv__(self, other): + if isinstance(other, (builtins.float, SymFloat)): + return sym_float(math.floor(sym_float(self) / other)) + if not isinstance(other, (builtins.int, SymInt)): + return NotImplemented + return self.__int_floordiv__(other) + + def __rfloordiv__(self, other): + if isinstance(other, (builtins.float, SymFloat)): + return sym_float(math.floor(other / sym_float(self))) + if not isinstance(other, (builtins.int, SymInt)): + return NotImplemented + return self.__rint_floordiv__(other) + + # nb: complex is impossible to handle correctly lol, with + # negative base and integral float need to diverge semantics and + # just always return complex. Neener neener pretend this problem + # doesn't exist + def __pow__(self, other): + if isinstance(other, (builtins.float, SymFloat)): + return sym_float(self).__pow__(other) + if not isinstance(other, (builtins.int, SymInt)): + return NotImplemented + # Guards! This guard is necessary because we need to know it to + # determine the output type of this operation + if other >= 0: + return self.__pow_by_natural__(other) + else: + # Mercifully, when the exponent is negative, Python just promotes + # to doubles and does a float pow: + # + # if (Py_SIZE(b) < 0 && c == NULL) { + # /* if exponent is negative and there's no modulus: + # return a float. This works because we know + # that this calls float_pow() which converts its + # arguments to double. */ + # Py_DECREF(a); + # Py_DECREF(b); + # return PyFloat_Type.tp_as_number->nb_power(v, w, x); + # } + return sym_float(self).__pow__(sym_float(other)) + + def __rpow__(self, other): + if isinstance(other, (builtins.float, SymFloat)): + return sym_float(self).__rpow__(other) + if not isinstance(other, (builtins.int, SymInt)): + return NotImplemented + if self >= 0: # self is exponent + return self.__rpow_by_natural__(other) + else: + return sym_float(self).__rpow__(sym_float(other)) + + def __eq__(self, other: object) -> builtins.bool: + raise TypeError("type stub not overridden") + + def __lt__(self, other) -> builtins.bool: + raise TypeError("type stub not overridden") + + def __gt__(self, other) -> builtins.bool: + raise TypeError("type stub not overridden") + + def __le__(self, other) -> builtins.bool: + raise TypeError("type stub not overridden") + + def __ge__(self, other) -> builtins.bool: + raise TypeError("type stub not overridden") + + def __add__(self, other) -> "SymInt": + raise TypeError("type stub not overridden") + + def __radd__(self, other) -> "SymInt": + raise TypeError("type stub not overridden") + + def __rmul__(self, other) -> "SymInt": + raise TypeError("type stub not overridden") + + def __mod__(self, other: "IntLikeType") -> "SymInt": + raise TypeError("type stub not overridden") + + def __mul__(self, other) -> "SymInt": + raise TypeError("type stub not overridden") + + def __pow_by_natural__(self, other) -> "SymInt": + raise TypeError("type stub not overridden") + + def __rpow_by_natural__(self, other) -> "SymInt": + raise TypeError("type stub not overridden") + + def __int_truediv__(self, other) -> "SymFloat": + raise TypeError("type stub not overridden") + + def __rint_truediv__(self, other) -> "SymFloat": + raise TypeError("type stub not overridden") + + def __int_floordiv__(self, other) -> "SymFloat": + raise TypeError("type stub not overridden") + + def __rint_floordiv__(self, other) -> "SymFloat": + raise TypeError("type stub not overridden") + + def __sym_max__(self, other): + raise TypeError("type stub not overridden") + + def __sym_min__(self, other): + raise TypeError("type stub not overridden") + + def __sym_float__(self): + raise TypeError("type stub not overridden") + + def __neg__(self): + raise TypeError("type stub not overridden") + + def __sub__(self, other: "IntLikeType") -> "SymInt": + raise TypeError("type stub not overridden") + + def __rsub__(self, other: "IntLikeType") -> "SymInt": + raise TypeError("type stub not overridden") + + def __and__(self, other) -> "SymInt": + raise TypeError("type stub not overridden") + + def __or__(self, other) -> "SymInt": + raise TypeError("type stub not overridden") + + def __repr__(self): + return self.node._graph_repr() + + def _sympy_(self): + return self.node.expr + + def __hash__(self) -> builtins.int: + if self.node.is_nested_int(): + return hash(self.node.nested_int()) + else: + # We could support constant SymInts as well, but not doing it for now + raise TypeError("unhashable type: non-nested SymInt") + # TODO: Force specialization + # This can't be done because the TypeError here is load bearing + # for einops + # https://github.com/arogozhnikov/einops/blob/6181e1e95dc58c00a3143c1726da1c6ee0463164/einops/einops.py#L237 + # return hash(builtins.int(self)) + + def as_integer_ratio(self) -> tuple["SymInt", builtins.int]: + """Represent this int as an exact integer ratio""" + return self, 1 + + def bit_length(self) -> builtins.int: + # TODO: A more relaxed guard is possible here, where you guard to + # allow all integer quantities which would result in the same bit + # length. We can also just make a dedicated Sympy function for + # computing this quantity and represent it symbolically. + return builtins.int(self).bit_length() + + def conjugate(self) -> "SymInt": + return self + + +class SymFloat: + """ + Like a float (including magic methods), but redirects all operations on the + wrapped node. This is used in particular to symbolically record operations + in the symbolic shape workflow. + """ + + def __init__(self, node): + # This field MUST be named node; C++ binding code assumes that this + # class has a field named node that stores SymNode + self.node = node + + def __truediv__(self, other): + if not isinstance(other, (builtins.int, builtins.float, SymInt, SymFloat)): + return NotImplemented + return self.__float_truediv__(sym_float(other)) + + def __rtruediv__(self, other): + if not isinstance(other, (builtins.int, builtins.float, SymInt, SymFloat)): + return NotImplemented + return self.__rfloat_truediv__(sym_float(other)) + + def __floordiv__(self, other): + if not isinstance(other, (builtins.int, builtins.float, SymInt, SymFloat)): + return NotImplemented + return sym_float(math.floor(self / sym_float(other))) + + def __rfloordiv__(self, other): + if not isinstance(other, (builtins.int, builtins.float, SymInt, SymFloat)): + return NotImplemented + return sym_float(math.floor(sym_float(other) / self)) + + def __bool__(self): + return self.node.bool_() + + def __float__(self): + return self.node.guard_float("", 0) + + def __int__(self): + return self.__trunc__().__int__() + + # Symbolic power does NOT work with negative base, this is to avoid + # potential complex outputs + def __pow__(self, other): + if not isinstance(other, (builtins.int, builtins.float, SymInt, SymFloat)): + return NotImplemented + torch._check(self >= 0) + return self.__float_pow__(other) + + def __rpow__(self, other): + if not isinstance(other, (builtins.int, builtins.float, SymInt, SymFloat)): + return NotImplemented + torch._check(other >= 0) + return self.__rfloat_pow__(other) + + # Magic methods installed by torch.fx.experimental.sym_node + + def __eq__(self, other: object) -> builtins.bool: + raise TypeError("type stub not overridden") + + def __lt__(self, other) -> builtins.bool: + raise TypeError("type stub not overridden") + + def __gt__(self, other) -> builtins.bool: + raise TypeError("type stub not overridden") + + def __le__(self, other) -> builtins.bool: + raise TypeError("type stub not overridden") + + def __ge__(self, other) -> builtins.bool: + raise TypeError("type stub not overridden") + + def __float_pow__(self, other) -> "SymFloat": + raise TypeError("type stub not overridden") + + def __rfloat_pow__(self, other) -> "SymFloat": + raise TypeError("type stub not overridden") + + def __float_truediv__(self, other) -> "SymFloat": + raise TypeError("type stub not overridden") + + def __rfloat_truediv__(self, other) -> "SymFloat": + raise TypeError("type stub not overridden") + + def __trunc__(self): + raise TypeError("type stub not overridden") + + def __sym_max__(self, other): + raise TypeError("type stub not overridden") + + def __sym_min__(self, other): + raise TypeError("type stub not overridden") + + def __sym_int__(self): + raise TypeError("type stub not overridden") + + def is_integer(self): + """Return True if the float is an integer.""" + raise TypeError("type stub not overridden") + + def as_integer_ratio(self) -> tuple[builtins.int, builtins.int]: + """Represent this float as an exact integer ratio""" + return builtins.float(self).as_integer_ratio() + + def __repr__(self): + return self.node._graph_repr() + + def _sympy_(self): + return self.node.expr + + def __hash__(self): + return hash(builtins.float(self)) + + def conjugate(self) -> "SymFloat": + """Returns the complex conjugate of the float.""" + return self + + def hex(self) -> str: + """Returns the hexadecimal representation of the float.""" + return self.node.guard_float("", 0).hex() + + +class SymBool: + """ + Like a bool (including magic methods), but redirects all operations on the + wrapped node. This is used in particular to symbolically record operations + in the symbolic shape workflow. + + Unlike regular bools, regular boolean operators will force extra guards instead + of symbolically evaluate. Use the bitwise operators instead to handle this. + """ + + def __init__(self, node): + # This field MUST be named node; C++ binding code assumes that this + # class has a field named node that stores SymNode + self.node = node + + def __bool__(self): + return self.node.bool_() + + def __int__(self): + return builtins.int(self.node.bool_()) + + # Magic methods installed by torch.fx.experimental.sym_node + def __and__(self, other) -> "SymBool": + raise TypeError("type stub not overridden") + + def __or__(self, other) -> "SymBool": + raise TypeError("type stub not overridden") + + # We very carefully define __sym_not__, and not a number of other + # plausible alternatives: + # + # - We do not override __not__ because this is not a real magic + # method; you cannot override the meaning of the not builtin in + # Python. We use the name 'sym_not' to clarify that in user code you + # cannot use the builtin not or operator.not_ or operator.__not__ and + # hit this magic method; you must use our custom sym_not operator. + # + # - We do not override the __invert__ method because SymBool is + # meant to be usable in situations where bool is expected. However, + # bitwise negation ~a does the wrong thing with booleans (because + # bool is a subclass of int, so ~1 = -2 which is not falseish.) + # This would be a giant footgun, so we get around it by defining + # our own operator. Note that bitwise and/or do the right thing, + # so we reuse the conventional operators there for readability. + # + def __sym_not__(self) -> "SymBool": + raise TypeError("type stub not overridden") + + def __sym_ite__(self, then_val, else_val): + raise TypeError("type stub not overridden") + + def __eq__(self, other) -> builtins.bool: + raise TypeError("type stub not overridden") + + def __repr__(self): + return self.node._graph_repr() + + def _sympy_(self): + return self.node.expr + + def __hash__(self): + if self.node.is_constant(): + return hash(self.node.bool_()) + else: + # Force specialization + return hash(builtins.bool(self)) + + def __sym_float__(self): + """ + Provides a SymFloat representation (0.0 or 1.0) for this SymBool. + Called by torch.sym_float() when casting SymBool to float. + """ + from torch.fx.experimental.sym_node import wrap_node + + return wrap_node(self.node.sym_float()) + + +def sym_not(a): + r"""SymInt-aware utility for logical negation. + + Args: + a (SymBool or bool): Object to negate + """ + import sympy + + if overrides.has_torch_function_unary(a): + return overrides.handle_torch_function(sym_not, (a,), a) + if hasattr(a, "__sym_not__"): + return a.__sym_not__() + if isinstance(a, sympy.Basic): + return ~a # type: ignore[operator] + return not a + + +def sym_float(a): + r"""SymInt-aware utility for float casting. + + Args: + a (SymInt, SymFloat, or object): Object to cast + """ + if overrides.has_torch_function_unary(a): + return overrides.handle_torch_function(sym_float, (a,), a) + if isinstance(a, SymFloat): + return a + elif hasattr(a, "__sym_float__"): + return a.__sym_float__() + return builtins.float(a) # type: ignore[operator] + + +def sym_int(a): + r"""SymInt-aware utility for int casting. + + Args: + a (SymInt, SymFloat, or object): Object to cast + """ + if overrides.has_torch_function_unary(a): + return overrides.handle_torch_function(sym_int, (a,), a) + if isinstance(a, SymInt): + return a + elif isinstance(a, SymFloat): + return math.trunc(a) + return builtins.int(a) # type: ignore[operator] + + +def sym_max(a, b): + """ + SymInt-aware utility for max which avoids branching on a < b. + Unlike builtins.max(), this only works for int/float, and it always + promotes to float if any argument is float (unlike builtins.max, which + will faithfully preserve the type of the input argument). + """ + if overrides.has_torch_function((a, b)): + return overrides.handle_torch_function(sym_max, (a, b), a, b) + if isinstance(a, (SymInt, SymFloat)): + return a.__sym_max__(b) + elif isinstance(b, (SymInt, SymFloat)): + # Due to promotion semantics, this is operator is commutative: + # max(1, 1.0) === max(1.0, 1) === 1.0 + return b.__sym_max__(a) + # TODO: Probably can make bool work too, just lazy + + all_types, float_types = __all_and_float_types() + + if not isinstance(a, all_types): + raise AssertionError(f"expected {all_types}, got {type(a)}") + if not isinstance(b, all_types): + raise AssertionError(f"expected {all_types}, got {type(b)}") + if isinstance(a, float_types) or isinstance(b, float_types): + return builtins.float(builtins.max(a, b)) # type: ignore[call-overload] + else: + return builtins.max(a, b) # type: ignore[call-overload] + + +def __all_and_float_types() -> tuple[tuple[type, ...], tuple[type, ...]]: + try: + import numpy as np + + all_types: tuple[type, ...] = ( + np.integer, + np.floating, + builtins.int, + builtins.float, + ) + float_types: tuple[type, ...] = (np.floating, builtins.float) + except ModuleNotFoundError: + all_types = (builtins.int, builtins.float) + float_types = (builtins.float,) + + return all_types, float_types + + +def sym_min(a, b): + """SymInt-aware utility for min().""" + if overrides.has_torch_function((a, b)): + return overrides.handle_torch_function(sym_min, (a, b), a, b) + if isinstance(a, (SymInt, SymFloat)): + return a.__sym_min__(b) + elif isinstance(b, (SymInt, SymFloat)): + return b.__sym_min__(a) + + all_types, float_types = __all_and_float_types() + + if not isinstance(a, all_types): + raise AssertionError(f"expected {all_types}, got {type(a)}") + if not isinstance(b, all_types): + raise AssertionError(f"expected {all_types}, got {type(b)}") + if isinstance(a, float_types) or isinstance(b, float_types): + return builtins.float(builtins.min(a, b)) # type: ignore[call-overload] + else: + return builtins.min(a, b) # type: ignore[call-overload] + + +def sym_sum(*args): + """ + N-ary add which is faster to compute for long lists than iterated binary + addition. Only does something special for integers. + + Accepts both ``sym_sum([a, b, c])`` and ``sym_sum(a, b, c)``. + """ + # Normalise: accept both sym_sum([a, b, c]) and sym_sum(a, b, c). + if len(args) == 1 and isinstance(args[0], (list, tuple)): + args = args[0] + + if overrides.has_torch_function(args): + return overrides.handle_torch_function(sym_sum, args, args) + + found = None + for a in args: + if not isinstance(a, (SymInt, builtins.int)): + return builtins.sum(args) + if isinstance(a, SymInt): + found = a.node + if found is None: + return builtins.sum(args) + + from torch.fx.experimental.sym_node import to_node, wrap_node + + return wrap_node(found.sym_sum(tuple(to_node(found, a) for a in args))) + + +# Drop in replacement for math.sqrt, math.sin, math.cos etc +def _get_sym_math_fn(name): + def fn(a): + if overrides.has_torch_function_unary(a): + return overrides.handle_torch_function(fn, (a,), a) + if isinstance(a, SymInt): + a = torch.sym_float(a) + if hasattr(a, f"__sym_{name}__"): + return getattr(a, f"__sym_{name}__")() + return getattr(math, name)(a) + + return fn + + +__fn, __name, __sym_name = None, "", "" +for __name in ( + "sqrt", + "cos", + "cosh", + "sin", + "sinh", + "tan", + "tanh", + "asin", + "acos", + "atan", + "log2", +): + __sym_name = f"_sym_{__name}" + __fn = _get_sym_math_fn(__name) + __fn.__qualname__ = __fn.__name__ = __sym_name + globals()[__sym_name] = __fn + + +del __fn, __name, __sym_name, _get_sym_math_fn + +# Adding temporary shortcut +sym_sqrt = globals()["_sym_sqrt"] +__all__.append("sym_sqrt") + + +def sym_ite(b, t, f): + """SymInt-aware utility for ternary operator (``t if b else f``.)""" + if overrides.has_torch_function((b, t, f)): + return overrides.handle_torch_function(sym_ite, (b, t, f), b, t, f) + if not isinstance(b, (SymBool, builtins.bool)): + raise AssertionError(f"expected SymBool or bool, got {type(b)}") + if type(t) is not type(f): + raise AssertionError(f"type mismatch: {type(t)} vs {type(f)}") + if isinstance(b, SymBool): + return b.__sym_ite__(t, f) + return t if b else f + + +# Create a fresh unbacked int, from an (possibly unbacked int) expression. +def sym_fresh_size(expr): + return torch.tensor(expr).item() + + +# Check to see if we can load C extensions, and if not provide some guidance +# on what the problem might be. +try: + # _initExtension is chosen (arbitrarily) as a sentinel. + from torch._C import _initExtension +except ImportError: + import torch._C as _C_for_compiled_check + + if _C_for_compiled_check.__file__ is None: + raise ImportError( + textwrap.dedent( + """ + Failed to load PyTorch C extensions: + It appears that PyTorch has loaded the `torch/_C` folder + of the PyTorch repository rather than the C extensions which + are expected in the `torch._C` namespace. This can occur when + using the `install` workflow. e.g. + $ python -m pip install --no-build-isolation -v . && python -c "import torch" + + This error can generally be solved using the `develop` workflow + $ python -m pip install --no-build-isolation -v -e . && python -c "import torch" # This should succeed + or by running Python from a different directory. + """ + ).strip() + ) from None + raise # If __file__ is not None the cause is unknown, so just re-raise. + +# The torch._C submodule is already loaded via `from torch._C import *` above +# Make an explicit reference to the _C submodule to appease linters +from torch import _C as _C + + +__name, __obj = "", None +for __name in dir(_C): + if __name[0] != "_" and not __name.endswith("Base"): + __all__.append(__name) + __obj = getattr(_C, __name) + if callable(__obj) or inspect.isclass(__obj): + if __obj.__module__ != __name__: # "torch" + # TODO: fix their module from C++ side + if __name not in { + "DisableTorchFunctionSubclass", + "DisableTorchFunction", + "Generator", + }: + __obj.__module__ = __name__ # "torch" + elif __name == "TensorBase": + # issue 109438 / pr 109940. Prevent TensorBase from being copied into torch. + delattr(sys.modules[__name__], __name) + +del __name, __obj + +if not TYPE_CHECKING: + # issue 38137 and python issue 43367. Submodules of a C extension are + # non-standard, and attributes of those submodules cannot be pickled since + # pickle expect to be able to import them as "from _C.sub import attr" + # which fails with "_C is not a package + def _import_extension_to_sys_modules(module, memo=None): + if memo is None: + memo = set() + if module in memo: + return + memo.add(module) + module_name = module.__name__ + for name in dir(module): + member = getattr(module, name) + member_name = getattr(member, "__name__", "") + if inspect.ismodule(member) and member_name.startswith(module_name): + sys.modules.setdefault(member_name, member) + # Recurse for submodules (e.g., `_C._dynamo.eval_frame`) + _import_extension_to_sys_modules(member, memo) + + _import_extension_to_sys_modules(_C) + del _import_extension_to_sys_modules + +################################################################################ +# Define basic utilities +################################################################################ + + +def typename(obj: _Any, /) -> str: + """ + String representation of the type of an object. + + This function returns a fully qualified string representation of an object's type. + Args: + obj (object): The object whose type to represent + Returns: + str: the type of the object `o` + Example: + >>> x = torch.tensor([1, 2, 3]) + >>> torch.typename(x) + 'torch.LongTensor' + >>> torch.typename(torch.nn.Parameter) + 'torch.nn.parameter.Parameter' + """ + if isinstance(obj, torch.Tensor): + return obj.type() + + module = getattr(obj, "__module__", "") or "" + qualname = "" + + if hasattr(obj, "__qualname__"): + qualname = obj.__qualname__ + elif hasattr(obj, "__name__"): + qualname = obj.__name__ + else: + module = obj.__class__.__module__ or "" + qualname = obj.__class__.__qualname__ + + if module in {"", "builtins"}: + return qualname + return f"{module}.{qualname}" + + +def is_tensor(obj: _Any, /) -> _TypeIs["torch.Tensor"]: + r"""Returns True if `obj` is a PyTorch tensor. + + Args: + obj (object): Object to test + Example:: + + >>> x = torch.tensor([1, 2, 3]) + >>> torch.is_tensor(x) + True + + """ + return isinstance(obj, torch.Tensor) + + +def is_storage(obj: _Any, /) -> _TypeGuard["TypedStorage | UntypedStorage"]: + r"""Returns True if `obj` is a PyTorch storage object. + + Args: + obj (Object): Object to test + Example:: + + >>> import torch + >>> # UntypedStorage (recommended) + >>> tensor = torch.tensor([1, 2, 3]) + >>> storage = tensor.untyped_storage() + >>> torch.is_storage(storage) + True + >>> + >>> # TypedStorage (legacy) + >>> typed_storage = torch.TypedStorage(5, dtype=torch.float32) + >>> torch.is_storage(typed_storage) + True + >>> + >>> # regular tensor (should return False) + >>> torch.is_storage(tensor) + False + >>> + >>> # non-storage object + >>> torch.is_storage([1, 2, 3]) + False + """ + return type(obj) in _storage_classes + + +_GLOBAL_DEVICE_CONTEXT = threading.local() + + +def get_default_device() -> "torch.device": + r"""Gets the default ``torch.Tensor`` to be allocated on ``device``""" + global _GLOBAL_DEVICE_CONTEXT + + from torch.overrides import _get_current_function_mode_stack + from torch.utils._device import DeviceContext + + def _get_device_with_index(device): + if device.index is not None: + return device + else: + # TODO: Call like get_device_index() method corresponding to + # each device type + return torch.tensor([]).device + + # Get device from any active DeviceContext. + device_mode = next( + filter( + lambda mode: isinstance(mode, DeviceContext), + reversed(_get_current_function_mode_stack()), + ), + None, + ) + if device_mode: + device = device_mode.device + return _get_device_with_index(device) + + device_context = getattr(_GLOBAL_DEVICE_CONTEXT, "device_context", None) + if device_context is not None: + return _get_device_with_index(device_context.device) + return torch.device("cpu") + + +def set_default_device(device: "Device") -> None: + """Sets the default ``torch.Tensor`` to be allocated on ``device``. This + does not affect factory function calls which are called with an explicit + ``device`` argument. Factory calls will be performed as if they + were passed ``device`` as an argument. + + To only temporarily change the default device instead of setting it + globally, use ``with torch.device(device):`` instead. + + The default device is initially ``cpu``. If you set the default tensor + device to another device (e.g., ``cuda``) without a device index, tensors + will be allocated on whatever the current device for the device type, + even after :func:`torch.cuda.set_device` is called. + + .. warning:: + + This function imposes a slight performance cost on every Python + call to the torch API (not just factory functions). If this + is causing problems for you, please comment on + https://github.com/pytorch/pytorch/issues/92701 + + .. note:: + + This doesn't affect functions that create tensors that share the same memory as the input, like: + :func:`torch.from_numpy` and :func:`torch.frombuffer` + + Args: + device (device or string): the device to set as default + + Example:: + + >>> # xdoctest: +SKIP("requires cuda, changes global state") + >>> torch.get_default_device() + device(type='cpu') + >>> torch.set_default_device('cuda') # current device is 0 + >>> torch.get_default_device() + device(type='cuda', index=0) + >>> torch.set_default_device('cuda') + >>> torch.cuda.set_device('cuda:1') # current device is 1 + >>> torch.get_default_device() + device(type='cuda', index=1) + >>> torch.set_default_device('cuda:1') + >>> torch.get_default_device() + device(type='cuda', index=1) + + """ + global _GLOBAL_DEVICE_CONTEXT + if hasattr(_GLOBAL_DEVICE_CONTEXT, "device_context"): + device_context = _GLOBAL_DEVICE_CONTEXT.device_context + if device_context is not None: + device_context.__exit__(None, None, None) + + if device is None: + device_context = None + else: + from torch.utils._device import DeviceContext + + device_context = DeviceContext(device) + device_context.__enter__() + _GLOBAL_DEVICE_CONTEXT.device_context = device_context + + +def set_default_tensor_type(t: type["torch.Tensor"] | str, /) -> None: + r""" + .. warning:: + + This function is deprecated as of PyTorch 2.1, please use :func:`torch.set_default_dtype()` and + :func:`torch.set_default_device()` as alternatives. + + Sets the default ``torch.Tensor`` type to floating point tensor type + ``t``. This type will also be used as default floating point type for + type inference in :func:`torch.tensor`. + + The default floating point tensor type is initially ``torch.FloatTensor``. + + Args: + t (type or string): the floating point tensor type or its name + + Example:: + + >>> # xdoctest: +SKIP("Other tests may have changed the default type. Can we reset it?") + >>> torch.tensor([1.2, 3]).dtype # initial default for floating point is torch.float32 + torch.float32 + >>> torch.set_default_tensor_type(torch.DoubleTensor) + >>> torch.tensor([1.2, 3]).dtype # a new floating point tensor + torch.float64 + + """ + if isinstance(t, str): + t = _import_dotted_name(t) + _C._set_default_tensor_type(t) + + +def set_default_dtype(d: "torch.dtype", /) -> None: + r""" + + Sets the default floating point dtype to :attr:`d`. Supports floating point dtype + as inputs. Other dtypes will cause torch to raise an exception. + + When PyTorch is initialized its default floating point dtype is torch.float32, + and the intent of set_default_dtype(torch.float64) is to facilitate NumPy-like + type inference. The default floating point dtype is used to: + + 1. Implicitly determine the default complex dtype. When the default floating type is float16, + the default complex dtype is complex32. For float32, the default complex dtype is complex64. + For float64, it is complex128. For bfloat16, an exception will be raised because + there is no corresponding complex type for bfloat16. + 2. Infer the dtype for tensors constructed using Python floats or complex Python + numbers. See examples below. + 3. Determine the result of type promotion between bool and integer tensors and + Python floats and complex Python numbers. + + Args: + d (:class:`torch.dtype`): the floating point dtype to make the default. + + Example: + >>> # xdoctest: +SKIP("Other tests may have changed the default type. Can we reset it?") + >>> # initial default for floating point is torch.float32 + >>> # Python floats are interpreted as float32 + >>> torch.tensor([1.2, 3]).dtype + torch.float32 + >>> # initial default for floating point is torch.complex64 + >>> # Complex Python numbers are interpreted as complex64 + >>> torch.tensor([1.2, 3j]).dtype + torch.complex64 + + >>> torch.set_default_dtype(torch.float64) + >>> # Python floats are now interpreted as float64 + >>> torch.tensor([1.2, 3]).dtype # a new floating point tensor + torch.float64 + >>> # Complex Python numbers are now interpreted as complex128 + >>> torch.tensor([1.2, 3j]).dtype # a new complex tensor + torch.complex128 + + >>> torch.set_default_dtype(torch.float16) + >>> # Python floats are now interpreted as float16 + >>> torch.tensor([1.2, 3]).dtype # a new floating point tensor + torch.float16 + >>> # Complex Python numbers are now interpreted as complex128 + >>> torch.tensor([1.2, 3j]).dtype # a new complex tensor + torch.complex32 + + """ + _C._set_default_dtype(d) + + +def use_deterministic_algorithms( + mode: builtins.bool, + *, + warn_only: builtins.bool = False, +) -> None: + r"""Sets whether PyTorch operations must use "deterministic" + algorithms. That is, algorithms which, given the same input, and when + run on the same software and hardware, always produce the same output. + When enabled, operations will use deterministic algorithms when available, + and if only nondeterministic algorithms are available they will throw a + :class:`RuntimeError` when called. + + .. note:: This setting alone is not always enough to make an application + reproducible. Refer to :ref:`reproducibility` for more information. + + .. note:: :func:`torch.set_deterministic_debug_mode` offers an alternative + interface for this feature. + + The following normally-nondeterministic operations will act + deterministically when ``mode=True``: + + * :class:`torch.nn.Conv1d` when called on CUDA tensor + * :class:`torch.nn.Conv2d` when called on CUDA tensor + * :class:`torch.nn.Conv3d` when called on CUDA tensor + * :class:`torch.nn.ConvTranspose1d` when called on CUDA tensor + * :class:`torch.nn.ConvTranspose2d` when called on CUDA tensor + * :class:`torch.nn.ConvTranspose3d` when called on CUDA tensor + * :class:`torch.nn.ReplicationPad1d` when attempting to differentiate a CUDA tensor + * :class:`torch.nn.ReplicationPad2d` when attempting to differentiate a CUDA tensor + * :class:`torch.nn.ReplicationPad3d` when attempting to differentiate a CUDA tensor + * :func:`torch.bmm` when called on sparse-dense CUDA tensors + * :func:`torch.Tensor.__getitem__` when attempting to differentiate a CPU tensor + and the index is a list of tensors + * :func:`torch.Tensor.index_put` with ``accumulate=False`` + * :func:`torch.Tensor.index_put` with ``accumulate=True`` when called on a CPU + tensor + * :func:`torch.Tensor.put_` with ``accumulate=True`` when called on a CPU + tensor + * :func:`torch.Tensor.scatter_add_` when called on a CUDA tensor + * :func:`torch.gather` when called on a CUDA tensor that requires grad + * :func:`torch.index_add` when called on CUDA tensor + * :func:`torch.index_select` when attempting to differentiate a CUDA tensor + * :func:`torch.repeat_interleave` when attempting to differentiate a CUDA tensor + * :func:`torch.Tensor.index_copy` when called on a CPU or CUDA tensor + * :func:`torch.Tensor.scatter` when `src` type is Tensor and called on CUDA tensor + * :func:`torch.Tensor.scatter_reduce` when ``reduce='sum'`` or ``reduce='mean'`` and called on CUDA tensor + + The following normally-nondeterministic operations will throw a + :class:`RuntimeError` when ``mode=True``: + + * :class:`torch.nn.AvgPool3d` when attempting to differentiate a CUDA tensor + * :class:`torch.nn.AdaptiveAvgPool2d` when attempting to differentiate a CUDA tensor + * :class:`torch.nn.AdaptiveAvgPool3d` when attempting to differentiate a CUDA tensor + * :class:`torch.nn.MaxPool3d` when attempting to differentiate a CUDA tensor + * :class:`torch.nn.AdaptiveMaxPool2d` when attempting to differentiate a CUDA tensor + * :class:`torch.nn.FractionalMaxPool2d` when attempting to differentiate a CUDA tensor + * :class:`torch.nn.FractionalMaxPool3d` when attempting to differentiate a CUDA tensor + * :class:`torch.nn.MaxUnpool1d` + * :class:`torch.nn.MaxUnpool2d` + * :class:`torch.nn.MaxUnpool3d` + * :func:`torch.nn.functional.interpolate` when attempting to differentiate a CUDA tensor + and one of the following modes is used: + + - ``linear`` + - ``bilinear`` + - ``bicubic`` + - ``trilinear`` + + * :class:`torch.nn.ReflectionPad1d` when attempting to differentiate a CUDA tensor + * :class:`torch.nn.ReflectionPad2d` when attempting to differentiate a CUDA tensor + * :class:`torch.nn.ReflectionPad3d` when attempting to differentiate a CUDA tensor + * :class:`torch.nn.NLLLoss` when called on a CUDA tensor + * :class:`torch.nn.CTCLoss` when attempting to differentiate a CUDA tensor + * :class:`torch.nn.EmbeddingBag` when attempting to differentiate a CUDA tensor when + ``mode='max'`` + * :func:`torch.Tensor.put_` when ``accumulate=False`` + * :func:`torch.Tensor.put_` when ``accumulate=True`` and called on a CUDA tensor + * :func:`torch.histc` when called on a CUDA tensor + * :func:`torch.bincount` when called on a CUDA tensor and ``weights`` + tensor is given + * :func:`torch.median` with indices output when called on a CUDA tensor + * :func:`torch.nn.functional.grid_sample` when attempting to differentiate a CUDA tensor + * :func:`torch.cumsum` when called on a CUDA tensor when dtype is floating point or complex + * :func:`torch.Tensor.scatter_reduce` when ``reduce='prod'`` and called on CUDA tensor + * :func:`torch.Tensor.resize_` when called with a quantized tensor + + In addition, several operations fill uninitialized memory when this setting + is turned on and when + :attr:`torch.utils.deterministic.fill_uninitialized_memory` is turned on. + See the documentation for that attribute for more information. + + Note that deterministic operations tend to have worse performance than + nondeterministic operations. + + + When this setting is turned on, the Inductor deterministic mode is also tuned on + automatically. In deterministic mode, Inductor would avoid doing on device benchmarking + that affect numerics. This includes: + + - don't pad matmul input shapes. Without enabling deterministic mode, Inductor would do + benchmarking to check if padding matmul shape is beneficial. + - don't autotune templates. Inductor has templates for kernels like matmul/conv/attention. + Without enabling deterministic mode, Inductor would do autotuning to + pick the best configs for those templates and adopt it if it's faster + than the kernel in eager mode. In deterministic mode, we pick the eager kernel. + - don't autotune triton configs for reduction. Reduction numerics are + very sensitive to triton configs. In deterministic mode, Inductor + will use some heuristics to pick the most promising configs rather + than do autotuning. + - Skip autotuning for reduction in coordinate descent tuning. + - Don't benchmarking for the computation/communication reordering pass + - Disable the feature that dynamically scale down RBLOCK triton config for higher + occupancy. + + + .. note:: + + This flag does not detect or prevent nondeterministic behavior caused + by calling an inplace operation on a tensor with an internal memory + overlap or by giving such a tensor as the :attr:`out` argument for an + operation. In these cases, multiple writes of different data may target + a single memory location, and the order of writes is not guaranteed. + + Args: + mode (:class:`bool`): If True, makes potentially nondeterministic + operations switch to a deterministic algorithm or throw a runtime + error. If False, allows nondeterministic operations. + + Keyword args: + warn_only (:class:`bool`, optional): If True, operations that do not + have a deterministic implementation will throw a warning instead of + an error. Default: ``False`` + + Example:: + + >>> # xdoctest: +SKIP + >>> torch.use_deterministic_algorithms(True) + + # Backward mode nondeterministic error + >>> torch.nn.AvgPool3d(1)(torch.randn(3, 4, 5, 6, requires_grad=True).cuda()).sum().backward() + ... + RuntimeError: avg_pool3d_backward_cuda does not have a deterministic implementation... + """ + import torch._inductor.config as inductor_config + + inductor_config.deterministic = mode + _C._set_deterministic_algorithms(mode, warn_only=warn_only) + + +def are_deterministic_algorithms_enabled() -> builtins.bool: + r"""Returns True if the global deterministic flag is turned on. Refer to + :func:`torch.use_deterministic_algorithms` documentation for more details. + """ + return _C._get_deterministic_algorithms() + + +def is_deterministic_algorithms_warn_only_enabled() -> builtins.bool: + r"""Returns True if the global deterministic flag is set to warn only. + Refer to :func:`torch.use_deterministic_algorithms` documentation for more + details. + """ + return _C._get_deterministic_algorithms_warn_only() + + +def set_deterministic_debug_mode(debug_mode: builtins.int | str) -> None: + r"""Sets the debug mode for deterministic operations. + + .. note:: This is an alternative interface for + :func:`torch.use_deterministic_algorithms`. Refer to that function's + documentation for details about affected operations. + + Args: + debug_mode(str or int): If "default" or 0, don't error or warn on + nondeterministic operations. If "warn" or 1, warn on + nondeterministic operations. If "error" or 2, error on + nondeterministic operations. + """ + + # NOTE: builtins.int is used here because int in this scope resolves + # to torch.int + if not isinstance(debug_mode, (builtins.int, str)): + raise TypeError(f"debug_mode must be str or int, but got {type(debug_mode)}") + + if isinstance(debug_mode, str): + if debug_mode == "default": + debug_mode = 0 + elif debug_mode == "warn": + debug_mode = 1 + elif debug_mode == "error": + debug_mode = 2 + else: + raise RuntimeError( + "invalid value of debug_mode, expected one of `default`, " + f"`warn`, `error`, but got {debug_mode}" + ) + + if debug_mode == 0: + _C._set_deterministic_algorithms(False) + elif debug_mode == 1: + _C._set_deterministic_algorithms(True, warn_only=True) + elif debug_mode == 2: + _C._set_deterministic_algorithms(True) + else: + raise RuntimeError( + f"invalid value of debug_mode, expected 0, 1, or 2, but got {debug_mode}" + ) + + +def get_deterministic_debug_mode() -> builtins.int: + r"""Returns the current value of the debug mode for deterministic + operations. Refer to :func:`torch.set_deterministic_debug_mode` + documentation for more details. + """ + + if _C._get_deterministic_algorithms(): + if _C._get_deterministic_algorithms_warn_only(): + return 1 + else: + return 2 + else: + return 0 + + +def get_float32_matmul_precision() -> str: + r"""Returns the current value of float32 matrix multiplication precision. Refer to + :func:`torch.set_float32_matmul_precision` documentation for more details. + """ + return _C._get_float32_matmul_precision() + + +def set_float32_matmul_precision(precision: str) -> None: + r"""Sets the internal precision of float32 matrix multiplications. + + Running float32 matrix multiplications in lower precision may significantly increase + performance, and in some programs the loss of precision has a negligible impact. + + Supports three settings: + + * "highest", float32 matrix multiplications use the float32 datatype (24 mantissa + bits with 23 bits explicitly stored) for internal computations. + * "high", float32 matrix multiplications either use the TensorFloat32 datatype (10 + mantissa bits explicitly stored) or treat each float32 number as the sum of two bfloat16 numbers + (approximately 16 mantissa bits with 14 bits explicitly stored), if the appropriate fast matrix multiplication + algorithms are available. Otherwise float32 matrix multiplications are computed + as if the precision is "highest". See below for more information on the bfloat16 + approach. + * "medium", float32 matrix multiplications use the bfloat16 datatype (8 mantissa + bits with 7 bits explicitly stored) for internal computations, if a fast matrix multiplication algorithm + using that datatype internally is available. Otherwise float32 + matrix multiplications are computed as if the precision is "high". + + When using "high" precision, float32 multiplications may use a bfloat16-based algorithm + that is more complicated than simply truncating to some smaller number mantissa bits + (e.g. 10 for TensorFloat32, 7 for bfloat16 explicitly stored). Refer to [Henry2019]_ for a complete + description of this algorithm. To briefly explain here, the first step is to realize + that we can perfectly encode a single float32 number as the sum of three bfloat16 + numbers (because float32 has 23 mantissa bits while bfloat16 has 7 explicitly stored, and both have the + same number of exponent bits). This means that the product of two float32 numbers can + be exactly given by the sum of nine products of bfloat16 numbers. We can then trade + accuracy for speed by dropping some of these products. The "high" precision algorithm + specifically keeps only the three most significant products, which conveniently excludes + all of the products involving the last 8 mantissa bits of either input. This means that + we can represent our inputs as the sum of two bfloat16 numbers rather than three. + Because bfloat16 fused-multiply-add (FMA) instructions are typically >10x faster than + float32 ones, it's faster to do three multiplications and 2 additions with bfloat16 + precision than it is to do a single multiplication with float32 precision. + + .. [Henry2019] http://arxiv.org/abs/1904.06376 + + .. note:: + + This does not change the output dtype of float32 matrix multiplications, + it controls how the internal computation of the matrix multiplication is performed. + + .. note:: + + This does not change the precision of convolution operations. Other flags, + like `torch.backends.cudnn.allow_tf32`, may control the precision of convolution + operations. + + .. note:: + + This flag currently only affects one native device type: CUDA. + If "high" or "medium" are set then the TensorFloat32 datatype will be used + when computing float32 matrix multiplications, equivalent to setting + `torch.backends.cuda.matmul.allow_tf32 = True`. When "highest" (the default) + is set then the float32 datatype is used for internal computations, equivalent + to setting `torch.backends.cuda.matmul.allow_tf32 = False`. + + Args: + precision(str): can be set to "highest" (default), "high", or "medium" (see above). + + """ + _C._set_float32_matmul_precision(precision) + + +def set_warn_always(b: builtins.bool, /) -> None: + r"""When this flag is False (default) then some PyTorch warnings may only + appear once per process. This helps avoid excessive warning information. + Setting it to True causes these warnings to always appear, which may be + helpful when debugging. + + Args: + b (:class:`bool`): If True, force warnings to always be emitted + If False, set to the default behaviour + """ + _C._set_warnAlways(b) + + +def is_warn_always_enabled() -> builtins.bool: + r"""Returns True if the global warn_always flag is turned on. Refer to + :func:`torch.set_warn_always` documentation for more details. + """ + return _C._get_warnAlways() + + +################################################################################ +# Define error checking functions +################################################################################ + +# These error checking functions must be kept consistent with their C++ +# equivalents. Their C++ equivalents are mentioned where applicable. + + +def _check_with( + error_type, + cond: builtins.bool | SymBool, + message: _Callable[[], str], +): # noqa: F811 + if not isinstance(cond, (builtins.bool, SymBool)): + raise TypeError(f"cond must be a bool, but got {type(cond)}") + + from torch.fx.experimental.symbolic_shapes import expect_true + + if expect_true(cond): + return + + # error_type must be a subclass of Exception and not subclass of Warning + if not issubclass(error_type, Exception) or issubclass(error_type, Warning): + raise AssertionError( + f"error_type must be a subclass of Exception but not Warning, got {error_type}" + ) + + if message is None: + message_evaluated = ( + "Expected cond to be True, but got False. (Could this error " + "message be improved? If so, please report an enhancement request " + "to PyTorch.)" + ) + + else: + if not callable(message): + raise TypeError("message must be a callable") + + message_evaluated = str(message()) + + raise error_type(message_evaluated) + + +def _check(cond, message=None): # noqa: F811 + r"""Throws error containing an optional message if the specified condition + is False. + + Error type: ``RuntimeError`` + + C++ equivalent: ``TORCH_CHECK`` + + Args: + cond (:class:`bool`): If False, throw error + + message (Callable, optional): Callable that returns either a string or + an object that has a ``__str__()`` method to be used as the error + message. Default: ``None`` + """ + _check_with(RuntimeError, cond, message) # pyrefly: ignore [bad-argument-type] + + +@_deprecated( + "_check_is_size will be removed in a future PyTorch release along with guard_size_oblivious. \ + Use _check(i >= 0) instead.", + category=FutureWarning, +) +def _check_is_size(i, message=None, *, max=None): + """Checks that a given integer is a valid size (i.e., is non-negative). + You should use this over ``_check(i >= 0)`` because it can prevent + ``GuardOnDataDependentSymNode`` exceptions by opting yourself into alternate + semantics for ``guard_size_oblivious`` tests that treat values 0 and 1 + equivalently to all other values. + + When max is not None, this specifies an upper bound equivalent to + ``_check(i <= max)``. This bound is also subject to alternate semantics: + in ``guard_size_oblivious`` tests, we assume that a constant max bound is + treated equivalently to all other values. Symbolic max bounds are not yet + supported. + + NB: Do NOT use this in contexts where a -1 size would be valid (indicating + to infer the size from context, or if you should wrap-around or truncate). + Only use this if the only valid value is an honest to goodness size. + """ + # This is responsible for the expect_true + _check(i >= 0, message) + from torch.fx.experimental.symbolic_shapes import _advise_is_size + + _advise_is_size(i) + + if max is not None: + _check(i <= max, message) + + from torch.fx.experimental.symbolic_shapes import _advise_is_bounded + + _advise_is_bounded(i, max) + + +def _check_index(cond, message=None): # noqa: F811 + r"""Throws error containing an optional message if the specified condition + is False. + + Error type: ``IndexError`` + + C++ equivalent: ``TORCH_CHECK_INDEX`` + + Args: + cond (:class:`bool`): If False, throw error + + message (Callable, optional): Callable that returns either a string or + an object that has a ``__str__()`` method to be used as the error + message. Default: ``None`` + """ + _check_with(IndexError, cond, message) # pyrefly: ignore [bad-argument-type] + + +def _check_value(cond, message=None): # noqa: F811 + r"""Throws error containing an optional message if the specified condition + is False. + + Error type: ``ValueError`` + + C++ equivalent: ``TORCH_CHECK_VALUE`` + + Args: + cond (:class:`bool`): If False, throw error + + message (Callable, optional): Callable that returns either a string or + an object that has a ``__str__()`` method to be used as the error + message. Default: ``None`` + """ + _check_with(ValueError, cond, message) # pyrefly: ignore [bad-argument-type] + + +def _check_type(cond, message=None): # noqa: F811 + r"""Throws error containing an optional message if the specified condition + is False. + + Error type: ``TypeError`` + + C++ equivalent: ``TORCH_CHECK_TYPE`` + + Args: + cond (:class:`bool`): If False, throw error + + message (Callable, optional): Callable that returns either a string or + an object that has a ``__str__()`` method to be used as the error + message. Default: ``None`` + """ + _check_with(TypeError, cond, message) # pyrefly: ignore [bad-argument-type] + + +def _check_not_implemented(cond, message=None): # noqa: F811 + r"""Throws error containing an optional message if the specified condition + is False. + + Error type: ``NotImplementedError`` + + C++ equivalent: ``TORCH_CHECK_NOT_IMPLEMENTED`` + + Args: + cond (:class:`bool`): If False, throw error + + message (Callable, optional): Callable that returns either a string or + an object that has a ``__str__()`` method to be used as the error + message. Default: ``None`` + """ + _check_with( + NotImplementedError, + cond, + # pyrefly: ignore [bad-argument-type] + message, + ) + + +def _check_tensor_all_with(error_type, cond, message=None): # noqa: F811 + if not is_tensor(cond): + raise TypeError(f"cond must be a tensor, but got {type(cond)}") + + if not cond.dtype == torch.bool: + raise TypeError(f"cond tensor must have dtype torch.bool, but got {cond.dtype}") + + _check_with(error_type, cond._is_all_true().item(), message) # type: ignore[arg-type] + + +# C++ equivalent: `TORCH_CHECK_TENSOR_ALL` +def _check_tensor_all(cond, message=None): # noqa: F811 + r"""Throws error containing an optional message if the specified condition + is False. + + Error type: ``RuntimeError`` + + C++ equivalent: ``TORCH_CHECK_TENSOR_ALL`` + + Args: + cond (:class:`torch.Tensor`): Tensor of dtype ``torch.bool``. If any + element is ``False``, throw error + + message (Callable, optional): Callable that returns either a string or + an object that has a ``__str__()`` method to be used as the error + message. Default: ``None`` + """ + _check_tensor_all_with(RuntimeError, cond, message) + + +################################################################################ +# Define numeric constants +################################################################################ + +# For Python Array API (https://data-apis.org/array-api/latest/API_specification/constants.html) and +# NumPy consistency (https://numpy.org/devdocs/reference/constants.html) +from math import e, inf, nan, pi + + +newaxis: None = None + +__all__.extend(["e", "pi", "nan", "inf", "newaxis"]) + +################################################################################ +# Define Storage and Tensor classes +################################################################################ + +from torch._tensor import Tensor # usort: skip + +# needs to be after torch.Tensor is defined to avoid circular dependencies +from torch import storage as storage # usort: skip +from torch.storage import ( + _LegacyStorage, + _StorageBase, + _warn_typed_storage_removal, + TypedStorage, + UntypedStorage, +) + + +# NOTE: New Storage classes should never be added. When adding a new +# dtype, use torch.storage.TypedStorage directly. +class ByteStorage(_LegacyStorage): + @classproperty + def dtype(self): + _warn_typed_storage_removal(stacklevel=3) + return self._dtype + + @classproperty + def _dtype(self): + return torch.uint8 + + +class DoubleStorage(_LegacyStorage): + @classproperty + def dtype(self): + _warn_typed_storage_removal(stacklevel=3) + return self._dtype + + @classproperty + def _dtype(self): + return torch.double + + +class FloatStorage(_LegacyStorage): + @classproperty + def dtype(self): + _warn_typed_storage_removal(stacklevel=3) + return self._dtype + + @classproperty + def _dtype(self): + return torch.float + + +class HalfStorage(_LegacyStorage): + @classproperty + def dtype(self): + _warn_typed_storage_removal(stacklevel=3) + return self._dtype + + @classproperty + def _dtype(self): + return torch.half + + +class LongStorage(_LegacyStorage): + @classproperty + def dtype(self): + _warn_typed_storage_removal(stacklevel=3) + return self._dtype + + @classproperty + def _dtype(self): + return torch.long + + +class IntStorage(_LegacyStorage): + @classproperty + def dtype(self): + _warn_typed_storage_removal(stacklevel=3) + return self._dtype + + @classproperty + def _dtype(self): + return torch.int + + +class ShortStorage(_LegacyStorage): + @classproperty + def dtype(self): + _warn_typed_storage_removal(stacklevel=3) + return self._dtype + + @classproperty + def _dtype(self): + return torch.short + + +class CharStorage(_LegacyStorage): + @classproperty + def dtype(self): + _warn_typed_storage_removal(stacklevel=3) + return self._dtype + + @classproperty + def _dtype(self): + return torch.int8 + + +class BoolStorage(_LegacyStorage): + @classproperty + def dtype(self): + _warn_typed_storage_removal(stacklevel=3) + return self._dtype + + @classproperty + def _dtype(self): + return torch.bool + + +class BFloat16Storage(_LegacyStorage): + @classproperty + def dtype(self): + _warn_typed_storage_removal(stacklevel=3) + return self._dtype + + @classproperty + def _dtype(self): + return torch.bfloat16 + + +class ComplexDoubleStorage(_LegacyStorage): + @classproperty + def dtype(self): + _warn_typed_storage_removal(stacklevel=3) + return self._dtype + + @classproperty + def _dtype(self): + return torch.cdouble + + +class ComplexFloatStorage(_LegacyStorage): + @classproperty + def dtype(self): + _warn_typed_storage_removal(stacklevel=3) + return self._dtype + + @classproperty + def _dtype(self): + return torch.cfloat + + +class QUInt8Storage(_LegacyStorage): + @classproperty + def dtype(self): + _warn_typed_storage_removal(stacklevel=3) + return self._dtype + + @classproperty + def _dtype(self): + return torch.quint8 + + +class QInt8Storage(_LegacyStorage): + @classproperty + def dtype(self): + _warn_typed_storage_removal(stacklevel=3) + return self._dtype + + @classproperty + def _dtype(self): + return torch.qint8 + + +class QInt32Storage(_LegacyStorage): + @classproperty + def dtype(self): + _warn_typed_storage_removal(stacklevel=3) + return self._dtype + + @classproperty + def _dtype(self): + return torch.qint32 + + +class QUInt4x2Storage(_LegacyStorage): + @classproperty + def dtype(self): + _warn_typed_storage_removal(stacklevel=3) + return self._dtype + + @classproperty + def _dtype(self): + return torch.quint4x2 + + +class QUInt2x4Storage(_LegacyStorage): + @classproperty + def dtype(self): + _warn_typed_storage_removal(stacklevel=3) + return self._dtype + + @classproperty + def _dtype(self): + return torch.quint2x4 + + +_storage_classes: set[type[TypedStorage | UntypedStorage]] = { + UntypedStorage, + DoubleStorage, + FloatStorage, + LongStorage, + IntStorage, + ShortStorage, + CharStorage, + ByteStorage, + HalfStorage, + BoolStorage, + QUInt8Storage, + QInt8Storage, + QInt32Storage, + BFloat16Storage, + ComplexFloatStorage, + ComplexDoubleStorage, + QUInt4x2Storage, + QUInt2x4Storage, + TypedStorage, +} + +# The _tensor_classes set is initialized by the call to initialize_python_bindings. +_tensor_classes: set[type["torch.Tensor"]] = set() + +# If you edit these imports, please update torch/__init__.py.in as well +from torch import amp as amp, random as random, serialization as serialization +from torch._tensor_str import set_printoptions +from torch.amp import autocast, GradScaler +from torch.random import ( + get_rng_state, + initial_seed, + manual_seed, + seed, + set_rng_state, + thread_safe_generator, +) +from torch.serialization import load, save + + +################################################################################ +# Initialize extension +################################################################################ + + +# Shared memory manager needs to know the exact location of manager executable +def _manager_path(): + if platform.system() == "Windows": + return b"" + path = get_file_path("torch", "bin", "torch_shm_manager") + prepare_multiprocessing_environment(get_file_path("torch")) + if not os.path.exists(path): + raise RuntimeError("Unable to find torch_shm_manager at " + path) + return path.encode("utf-8") + + +_C._initExtension(_manager_path()) + +del _manager_path + +# Appease the type checker: it can't deal with direct setting of globals(). +# Note that we will see "too many" functions when reexporting this way; there +# is not a good way to fix this problem. Perhaps, try to redesign VariableFunctions +# so that this import is good enough +if TYPE_CHECKING: + # Some type signatures pulled in from _VariableFunctions here clash with + # signatures already imported. For now these clashes are ignored; see + # PR #43339 for details. + from torch._C._VariableFunctions import * # type: ignore[assignment, misc] # noqa: F403 + + # Fixup segment_reduce visibility + _segment_reduce = segment_reduce + del segment_reduce # noqa: F821 + +# Ops not to be exposed in `torch` namespace, +# mostly helper ops. +PRIVATE_OPS = ("unique_dim",) + +__name, __obj = "", None +for __name in dir(_C._VariableFunctions): + if __name.startswith("__") or __name in PRIVATE_OPS: + continue + __obj = getattr(_C._VariableFunctions, __name) + __obj.__module__ = __name__ # "torch" + # Hide some APIs that should not be public + if __name == "segment_reduce": + # TODO: Once the undocumented FC window is passed, remove the line below + globals()[__name] = __obj + __name = "_" + __name + globals()[__name] = __obj + if not __name.startswith("_"): + __all__.append(__name) + +del __name, __obj + +################################################################################ +# Add torch.dtype instances to the public API +################################################################################ + +import torch + + +__all__.extend( + # pyrefly: ignore [unresolvable-dunder-all] + name + for name in dir(torch) + if isinstance(getattr(torch, name), torch.dtype) +) + +################################################################################ +# Import TorchDynamo's lazy APIs to avoid circular dependencies +################################################################################ + +# needs to be before from torch.functional import * to avoid circular dependencies +from torch._compile import _disable_dynamo # usort: skip + +################################################################################ +# Import interface functions defined in Python +################################################################################ + +# needs to be after the above ATen bindings so we can overwrite from Python side +from torch import _VF as _VF, functional as functional # usort: skip +from torch.functional import * # usort: skip # noqa: F403 + +################################################################################ +# Remove unnecessary members +################################################################################ + +del _StorageBase +del _LegacyStorage + +################################################################################ +# Define _assert +################################################################################ + + +# needs to be before the submodule imports to avoid circular dependencies +def _assert(condition, message): + r"""A wrapper around Python's assert which is symbolically traceable.""" + if type(condition) is not torch.Tensor and overrides.has_torch_function( + (condition,) + ): + return overrides.handle_torch_function( + _assert, (condition,), condition, message + ) + if not condition: + raise AssertionError(message) + + +################################################################################ +# Import most common subpackages +################################################################################ + +# Use the redundant form so that type checkers know that these are a part of +# the public API. The "regular" import lines are there solely for the runtime +# side effect of adding to the imported module's members for other users. + +# needs to be before import torch.nn as nn to avoid circular dependencies +from torch.autograd import ( # usort: skip + enable_grad as enable_grad, + inference_mode as inference_mode, + no_grad as no_grad, + set_grad_enabled as set_grad_enabled, +) + +from torch import ( + __config__ as __config__, + __future__ as __future__, + _awaits as _awaits, + accelerator as accelerator, + autograd as autograd, + backends as backends, + cpu as cpu, + cuda as cuda, + distributed as distributed, + distributions as distributions, + fft as fft, + futures as futures, + hub as hub, + jit as jit, + linalg as linalg, + mps as mps, + mtia as mtia, + multiprocessing as multiprocessing, + nested as nested, + nn as nn, + optim as optim, + overrides as overrides, + profiler as profiler, + sparse as sparse, + special as special, + testing as testing, + types as types, + utils as utils, + version as version, + xpu as xpu, +) +from torch.signal import windows as windows + + +# Quantized, sparse, AO, etc. should be last to get imported, as nothing +# is expected to depend on them. +from torch import ao as ao # usort: skip + +# nn.quant* depends on ao -- so should be after those. +import torch.nn.intrinsic +import torch.nn.qat +import torch.nn.quantizable +import torch.nn.quantized + + +_C._init_names(list(_storage_classes)) + +# attach docstrings to torch and tensor functions +from torch import _size_docs, _storage_docs, _tensor_docs, _torch_docs + + +del _torch_docs, _tensor_docs, _storage_docs, _size_docs + + +def compiled_with_cxx11_abi() -> builtins.bool: + r"""Returns whether PyTorch was built with _GLIBCXX_USE_CXX11_ABI=1""" + return True + + +from torch import _library as _library, _ops as _ops + + +# Import the ops and classes "namespace" +from torch._ops import ops as ops # usort: skip +from torch._classes import classes as classes # usort: skip + +sys.modules.setdefault(f"{__name__}.ops", ops) +sys.modules.setdefault(f"{__name__}.classes", classes) + +# quantization depends on torch.fx and torch.ops +# Import quantization +from torch import quantization as quantization # usort: skip + +# Import the quasi random sampler +from torch import quasirandom as quasirandom # usort: skip + +# If you are seeing this, it means that this call site was not checked if +# the memory format could be preserved, and it was switched to old default +# behaviour of contiguous +legacy_contiguous_format = contiguous_format # defined by _C._initExtension() + +# Register fork handler to initialize OpenMP in child processes (see gh-28389) +from torch.multiprocessing._atfork import register_after_fork + + +register_after_fork(torch.get_num_threads) +del register_after_fork + +# Import tools that require fully imported torch (for applying +# torch.jit.script as a decorator, for instance): +from torch._lobpcg import lobpcg as lobpcg + + +# These were previously defined in native_functions.yaml and appeared on the +# `torch` namespace, but we moved them to c10 dispatch to facilitate custom +# class usage. We add these lines here to preserve backward compatibility. +quantized_lstm = ops.aten.quantized_lstm +quantized_gru = ops.aten.quantized_gru + +# Import experimental masked operations support. See +# [RFC-0016](https://github.com/pytorch/rfcs/pull/27) for more +# information. +from torch import masked as masked + +# Import removed ops with error message about removal +from torch._linalg_utils import ( # type: ignore[misc] + _symeig as symeig, + eig, + lstsq, + matrix_rank, + solve, +) +from torch.utils.dlpack import from_dlpack, to_dlpack + + +class _TorchCompileInductorWrapper: + compiler_name = "inductor" + + def __init__(self, mode, options, dynamic, name=None): + from torch._inductor.compiler_bisector import CompilerBisector + + self.config: dict[str, _Any] = {} + self.dynamic = dynamic + self.name = name + self.apply_mode(mode) + self.apply_options(options) + self.apply_options(CompilerBisector.get_config_change("inductor")) + + cuda_version = None + if hasattr(torch, "version"): + from torch.torch_version import TorchVersion + + cuda_version = TorchVersion(getattr(torch.version, "cuda", "0.0")) + + if self.config.get("triton.cudagraphs", False) and ( + (cuda_version and cuda_version < "12.6") + or not profiler_allow_cudagraph_cupti_lazy_reinit_cuda12() + ): + os.environ["DISABLE_CUPTI_LAZY_REINIT"] = "1" + # FIXME: CUDA Graph does not work well with CUPTI teardown. + # 1) crashes on 1st lazy CUPTI re-init after teardown (CUDA 11) + # 2) crashes on 2nd non-lazy CUPTI re-init after teardown (CUDA 12) + # Workaround: turn off CUPTI teardown when using CUDA Graphs. + os.environ["TEARDOWN_CUPTI"] = "0" + + def __eq__(self, other): + return ( + isinstance(other, _TorchCompileInductorWrapper) + and self.config == other.config + and self.dynamic == other.dynamic + and self.name == other.name + ) + + def apply_mode(self, mode: str | None): + if mode and mode != "default": + from torch._inductor import list_mode_options + + self.apply_options(list_mode_options(mode, self.dynamic)) + + def apply_options(self, options: dict[str, _Any] | None): + if not options: + return + + from torch._inductor import config + + current_config: dict[str, _Any] = config.get_config_copy() + + for key, val in options.items(): + attr_name = key.replace("-", "_") + if attr_name not in current_config: + raise RuntimeError( + f"Unexpected optimization option {key}, known options are {list(current_config.keys())}" + ) + attr_type = config.get_type(attr_name) # type: ignore[attr-defined] + # Subscriptable generic types don't support isinstance so skip the type + # check. There doesn't seem to be a good way of checking membership without + # 3rd party libraries. + if _get_origin(attr_type) is None: + if not isinstance(val, attr_type): + val_type_str = type(val).__name__ + expected_type_str = type(current_config[attr_name]).__name__ + raise RuntimeError( + f"Unexpected type of attr {key}, got {val_type_str} should be {expected_type_str}" + ) + self.config[attr_name] = val + + def __call__(self, model_, inputs_, *, config_patches=None): + from torch._inductor.compile_fx import compile_fx + + all_patches = {**self.config, **(config_patches or {})} + return compile_fx( + model_, + inputs_, + config_patches=all_patches, + compile_region_name=self.name, + ) + + def get_compiler_config(self): + from torch._inductor.compile_fx import get_patched_config_dict + + return get_patched_config_dict(config_patches=self.config) + + def reset(self): + from torch._inductor import config + + if "triton.cudagraphs" in self.config or config.triton.cudagraphs: + if self.config.get("triton.cudagraphs", True): + from torch._inductor.cudagraph_trees import reset_cudagraph_trees + + reset_cudagraph_trees() + + +class _TorchCompileAOTInductorWrapper(_TorchCompileInductorWrapper): + compiler_name = "aotinductor" + + def __init__(self, mode, options, dynamic, name=None): + super().__init__(mode, options, dynamic, name) + self.apply_options({"cpp_wrapper": True}) + self.apply_options({"aot_inductor.package": True}) + + def __call__(self, model_, inputs_, *, config_patches=None): + from contextlib import nullcontext + from unittest import mock + + from torch._guards import detect_fake_mode + from torch._inductor.virtualized import V + + fake_mode = detect_fake_mode(inputs_) + ctx = ( + mock.patch.object(fake_mode, "allow_non_fake_inputs", True) + if fake_mode + else nullcontext() + ) + with ( + V.set_aot_compilation(True), + ctx, + torch._inductor.config.patch("enable_autograd_for_aot", True), + ): + return super().__call__(model_, inputs_, config_patches=config_patches) + + +class _TorchCompileWrapper: + def __init__(self, backend, mode, options, dynamic): + from torch._dynamo.backends.registry import lookup_backend + + if isinstance(backend, str): + self.compiler_name = backend + elif hasattr(backend, "__name__"): + self.compiler_name = backend.__name__ + else: + self.compiler_name = str(backend) + self.dynamic = dynamic + self.compiler_fn = lookup_backend(backend) + self.kwargs = {} + # only pass the args if they non-empty + if mode and mode != "default": + self.kwargs["mode"] = mode + if options: + self.kwargs["options"] = options + + def __eq__(self, other): + return ( + isinstance(other, _TorchCompileWrapper) + and self.compiler_fn == other.compiler_fn + and self.kwargs == other.kwargs + and self.dynamic == other.dynamic + ) + + def __call__(self, model_, inputs_): + return self.compiler_fn(model_, inputs_, **self.kwargs) + + def reset(self): + if hasattr(self.compiler_fn, "reset"): + self.compiler_fn.reset() + + +_InputT = _ParamSpec("_InputT") +_RetT = _TypeVar("_RetT") + + +@_overload +def compile( + model: _Callable[_InputT, _RetT], + *, + fullgraph: builtins.bool = False, + dynamic: builtins.bool | None = None, + backend: str | _Callable = "inductor", + mode: str | None = None, + options: dict[str, str | builtins.int | builtins.bool | _Callable] | None = None, + name: str | None = None, + disable: builtins.bool = False, +) -> _Callable[_InputT, _RetT]: ... + + +@_overload +def compile( + model: None = None, + *, + fullgraph: builtins.bool = False, + dynamic: builtins.bool | None = None, + backend: str | _Callable = "inductor", + mode: str | None = None, + options: dict[str, str | builtins.int | builtins.bool | _Callable] | None = None, + name: str | None = None, + disable: builtins.bool = False, +) -> _Callable[[_Callable[_InputT, _RetT]], _Callable[_InputT, _RetT]]: ... + + +def compile( + model: _Callable[_InputT, _RetT] | None = None, + *, + fullgraph: builtins.bool = False, + dynamic: builtins.bool | None = None, + backend: str | _Callable = "inductor", + mode: str | None = None, + options: dict[str, str | builtins.int | builtins.bool | _Callable] | None = None, + name: str | None = None, + disable: builtins.bool = False, + recompile_limit: builtins.int | None = None, +) -> ( + _Callable[[_Callable[_InputT, _RetT]], _Callable[_InputT, _RetT]] + | _Callable[_InputT, _RetT] +): + """ + Optimizes given model/function using TorchDynamo and specified backend. + If you are compiling an :class:`torch.nn.Module`, you can also use :meth:`torch.nn.Module.compile` + to compile the module inplace without changing its structure. + + Concretely, for every frame executed within the compiled region, we will attempt + to compile it and cache the compiled result on the code object for future + use. A single frame may be compiled multiple times if previous compiled + results are not applicable for subsequent calls (this is called a "guard + failure"), you can use TORCH_LOGS=guards to debug these situations. + Multiple compiled results can be associated with a frame up to + ``torch._dynamo.config.recompile_limit``, which defaults to 8; at which + point we will fall back to eager. Note that compile caches are per + *code object*, not frame; if you dynamically create multiple copies of a + function, they will all share the same code cache. + + Args: + model (Callable or None): Module/function to optimize + fullgraph (bool): If False (default), torch.compile attempts to discover compilable regions + in the function that it will optimize. If True, then we require that the entire function be + capturable into a single graph. If this is not possible (that is, if there are graph breaks), + then this will raise an error. This also opts into unbacked semantics, notably it will turn on + capture_scalar_outputs and capture_dynamic_output_shape_ops on by default. + dynamic (bool or None): Use dynamic shape tracing. When this is True, we will up-front attempt + to generate a kernel that is as dynamic as possible to avoid recompilations when + sizes change. This may not always work as some operations/optimizations will + force specialization; use TORCH_LOGS=dynamic to debug overspecialization. + When this is False, we will NEVER generate dynamic kernels, we will always specialize. + By default (None), we automatically detect if dynamism has occurred and compile a more + dynamic kernel upon recompile. + backend (str or Callable): backend to be used + + - "inductor" is the default backend, which is a good balance between performance and overhead + + - Non experimental in-tree backends can be seen with `torch._dynamo.list_backends()` + + - Experimental or debug in-tree backends can be seen with `torch._dynamo.list_backends(None)` + + - To register an out-of-tree custom backend: + https://docs.pytorch.org/docs/main/user_guide/torch_compiler/torch.compiler_custom_backends.html#registering-custom-backends + mode (str): Can be either "default", "reduce-overhead", "max-autotune" or "max-autotune-no-cudagraphs" + + - "default" is the default mode, which is a good balance between performance and overhead + + - "reduce-overhead" is a mode that reduces the overhead of python with CUDA graphs, + useful for small batches. Reduction of overhead can come at the cost of more memory + usage, as we will cache the workspace memory required for the invocation so that we + do not have to reallocate it on subsequent runs. Reduction of overhead is not guaranteed + to work; today, we only reduce overhead for CUDA only graphs which do not mutate inputs. + There are other circumstances where CUDA graphs are not applicable; use TORCH_LOGS=perf_hints + to debug. + + - "max-autotune" is a mode that leverages Triton or template based matrix multiplications + on supported devices and Triton based convolutions on GPU. + It enables CUDA graphs by default on GPU. + + - "max-autotune-no-cudagraphs" is a mode similar to "max-autotune" but without CUDA graphs + + - To see the exact configs that each mode sets you can call `torch._inductor.list_mode_options()` + + options (dict): A dictionary of options to pass to the backend. Some notable ones to try out are + + - `epilogue_fusion` which fuses pointwise ops into templates. Requires `max_autotune` to also be set + + - `max_autotune` which will profile to pick the best matmul configuration + + - `fallback_random` which is useful when debugging accuracy issues + + - `shape_padding` which pads matrix shapes to better align loads on GPUs especially for tensor cores + + - `triton.cudagraphs` which will reduce the overhead of python with CUDA graphs + + - `trace.enabled` which is the most useful debugging flag to turn on + + - `trace.graph_diagram` which will show you a picture of your graph after fusion + + - `guard_filter_fn` that controls which dynamo guards are saved with compilations. + This is an unsafe feature and there is no backward compatibility guarantee provided + for dynamo guards as data types. + For stable helper functions to use, see the documentations in `torch.compiler`, for example: + - `torch.compiler.skip_guard_on_inbuilt_nn_modules_unsafe` + - `torch.compiler.skip_guard_on_all_nn_modules_unsafe` + - `torch.compiler.keep_tensor_guards_unsafe` + + - For inductor you can see the full list of configs that it supports by calling `torch._inductor.list_options()` + name (str or None): Optional identifier for the compiled region. When supported by downstream + tooling, this is surfaced on wrapped compiled-region higher-order operators and other debug metadata. + disable (bool): Turn torch.compile() into a no-op for testing + + Example:: + + @torch.compile(options={"triton.cudagraphs": True}, fullgraph=True) + def foo(x): + return torch.sin(x) + torch.cos(x) + + """ + import sysconfig + + _C._log_api_usage_once("torch.compile") + if sys.version_info >= (3, 15): + raise RuntimeError("torch.compile is not supported on Python 3.15+") + elif sysconfig.get_config_var("Py_GIL_DISABLED") == 1 and sys.version_info < ( + 3, + 13, + 3, + ): + raise RuntimeError( + "torch.compile is not supported on Python < 3.13.3 built with GIL disabled. " + "Please use Python 3.13.3+." + ) + + # Decorator mode + if model is None: + + def fn(model: _Callable[_InputT, _RetT]) -> _Callable[_InputT, _RetT]: + if model is None: + raise RuntimeError("Model can't be None") + return compile( # pyrefly: ignore # no-matching-overload + model, + fullgraph=fullgraph, + dynamic=dynamic, + backend=backend, + mode=mode, + options=options, + name=name, + disable=disable, + ) + + return fn + + if mode is not None and options is not None: + raise RuntimeError( + "Either mode or options can be specified, but both can't be specified at the same time." + ) + if mode is None and options is None: + mode = "default" + + from torch._inductor.compiler_bisector import CompilerBisector + + if bisect_backend := CompilerBisector.get_backend(): + import torch._inductor.config as inductor_config + + # don't override the backend for use cases like vllm + # which leverages their custom backend. + if not ( + inductor_config.test_configs.bisect_keep_custom_backend_for_inductor + and bisect_backend == "inductor" + and not isinstance(backend, str) + ): + backend = bisect_backend + + guard_filter_fn = None + use_aoti = False + if options and isinstance(options, dict): + guard_filter_fn = options.pop("guard_filter_fn", None) + use_aoti = options.pop("use_aoti", False) + + if torch.compiler.is_exporting(): + from torch._higher_order_ops.utils import _in_hop_compile + + if not _in_hop_compile(): + warnings.warn( + "torch.compile is ignored when called inside torch.export region", + stacklevel=2, + ) + # torch.compile is a no-op when inside torch.export region + return model + + if backend == "inductor": + if use_aoti: + backend = _TorchCompileAOTInductorWrapper(mode, options, dynamic, name) + else: + backend = _TorchCompileInductorWrapper(mode, options, dynamic, name) + else: + backend = _TorchCompileWrapper(backend, mode, options, dynamic) + + return torch._dynamo.optimize( + backend=backend, + nopython=fullgraph, + dynamic=dynamic, + disable=disable, + guard_filter_fn=guard_filter_fn, + recompile_limit=recompile_limit, + )(model) # type: ignore[return-value] + + +def _register_device_module(device_type, module): + r"""Register an external runtime module of the specific :attr:`device_type` + supported by torch. + + After the :attr:`module` is registered correctly, the user can refer + the external runtime module as part of torch with attribute torch.xxx. + """ + # Make sure the device_type represent a supported device type for torch. + device_type = torch.device(device_type).type + m = sys.modules[__name__] + if hasattr(m, device_type): + raise RuntimeError( + f"The runtime module of '{device_type}' has already " + f"been registered with '{getattr(m, device_type)}'" + ) + setattr(m, device_type, module) + torch_module_name = ".".join([__name__, device_type]) + sys.modules[torch_module_name] = module + + +from torch import ( + export as export, + func as func, + library as library, + return_types as return_types, +) +from torch._higher_order_ops import cond as cond, while_loop as while_loop +from torch.func import vmap as vmap + + +if not TYPE_CHECKING: + # register python metas for distributed ops + # Only import if distributed is available (USE_DISTRIBUTED=1) + if hasattr(torch._C, "_c10d_init"): + import torch.distributed._meta_registrations as coll_meta_registrations + + del coll_meta_registrations + from torch import _meta_registrations + +# Enable CUDA Sanitizer +if "TORCH_CUDA_SANITIZER" in os.environ: + import torch.cuda._sanitizer as csan + + csan.enable_cuda_sanitizer() + +# Populate magic methods on SymInt and SymFloat +import torch.fx.experimental.sym_node +from torch import fx as fx + + +# Register MPS specific decomps +torch.backends.mps._init() + +from torch import compiler as compiler + + +class _TritonLibrary: + lib = torch.library.Library("triton", "DEF") + ops_table: dict[tuple[str, str], _Callable] = {} + + @classmethod + def registerOp(cls, op_key, full_schema, op_impl, dispatch_key): + if (op_key, dispatch_key) not in cls.ops_table: + cls.lib.define(full_schema) + cls.lib.impl("triton::" + op_key, op_impl, dispatch_key) + cls.ops_table[(op_key, dispatch_key)] = op_impl + + return cls.ops_table[(op_key, dispatch_key)] + + +# Deprecated attributes +_deprecated_attrs = { + "has_mps": torch.backends.mps.is_built, + "has_cuda": torch.backends.cuda.is_built, + "has_cudnn": torch.backends.cudnn.is_available, + "has_mkldnn": torch.backends.mkldnn.is_available, +} + +if TYPE_CHECKING: + # Import the following modules during type checking to enable code intelligence features, + # such as auto-completion in tools like pylance, even when these modules are not explicitly + # imported in user code. + from torch import ( + _dynamo as _dynamo, + _inductor as _inductor, + _subclasses as _subclasses, + onnx as onnx, + ) + +else: + _lazy_modules = { + "_dynamo", + "_inductor", + "_export", + # ONNX must be imported after _dynamo, _ops, _subclasses, fx, func and jit + "onnx", + } + + def __getattr__(name): + # Deprecated attrs + replacement = _deprecated_attrs.get(name) + if replacement is not None: + import warnings + + warnings.warn( + f"'{name}' is deprecated, please use '{replacement.__module__}.{replacement.__name__}()'", + stacklevel=2, + ) + return replacement() + + # Lazy modules + if name in _lazy_modules: + return importlib.import_module(f".{name}", __name__) + + # set_vital + if name == "set_vital": + import warnings + + warnings.warn(f"'{name}' is deprecated, please do not call", stacklevel=2) + return lambda *args: None + + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + + +@functools.cache +def get_device_module(device: torch.device | str | None = None): + """ + Returns the module associated with a given device(e.g., torch.device('cuda'), "mtia:0", "xpu", ...). + If no device is given, return the module for the current accelerator or CPU if none is present. + """ + if isinstance(device, torch.device): + device_module_name = device.type + elif isinstance(device, str): + device_module_name = torch.device(device).type + elif device is None: + # Using default accelerator type. If no accelerator is available, it automatically returns CPU device. + device_module_name = torch._C._get_accelerator().type + else: + raise RuntimeError( + f"Invalid value of device '{device}', expect torch.device, str, or None" + ) + device_module = getattr(torch, device_module_name, None) + if device_module is None: + raise RuntimeError( + f"Device '{device_module_name}' does not have a corresponding module registered as 'torch.{device_module_name}'." + ) + return device_module + + +def _constrain_as_size( + symbol, + min: builtins.int | None = None, + max: builtins.int | None = None, +): + """ + This indicates that a given int is size-like, and can be used in any context where a size is expected. + You will typically use this when reading out integers from Tensors, e.g., max.item() or lengths.tolist() + which then need to be used as tensor constructors. Providing these assertions to PyTorch can help resolve + GuardOnDataDependentSymNode errors upon export, since we cannot guard on unbacked SymInts. + + This function has unusual semantics in some circumstances in framework + code, we will treat this int as >= 2 (when we do a size-oblivious guard). + This makes it easier to use the unbacked int in size contexts, + as we will often attempt to guard on a size being zero/one + (e.g., when computing the contiguity of a tensor, or testing if + broadcasting can occur), which will not work on unbacked SymInts. + However, if we conservatively assume that the size is not zero/one, we will + end up with a graph that will still work even if the size is zero/one. + + For more details, see https://docs.google.com/document/d/1HSuTTVvYH1pTew89Rtpeu84Ht3nQEFTYhAX3Ypa_xJs/edit + ``` + """ + torch.sym_constrain_range_for_size(symbol, min=min, max=max) + + +from torch import _logging + + +_logging._init_logs() + + +def _import_device_backends(): + """ + Leverage the Python plugin mechanism to load out-of-the-tree device extensions. + See this RFC: https://github.com/pytorch/pytorch/issues/122468 + """ + from importlib.metadata import entry_points + + group_name = "torch.backends" + backend_extensions = entry_points(group=group_name) + + for backend_extension in backend_extensions: + try: + # Load the extension + entrypoint = backend_extension.load() + # Call the entrypoint + entrypoint() + except Exception as err: + raise RuntimeError( + f"Failed to load the backend extension: {backend_extension.name}. " + f"You can disable extension auto-loading with TORCH_DEVICE_BACKEND_AUTOLOAD=0." + ) from err + + +def _is_device_backend_autoload_enabled() -> builtins.bool: + """ + Whether autoloading out-of-the-tree device extensions is enabled. + The switch depends on the value of the environment variable + `TORCH_DEVICE_BACKEND_AUTOLOAD`. + + Returns: + bool: Whether to enable autoloading the extensions. Enabled by default. + + Examples: + >>> torch._is_device_backend_autoload_enabled() + True + """ + # enabled by default + return os.getenv("TORCH_DEVICE_BACKEND_AUTOLOAD", "1") == "1" + + +def _as_tensor_fullprec(t): + """ + Like torch.as_tensor, but when given Python data types it will keep + them in full precision. Used for calling convention for Dynamo. + Python scalars (float, int) are always created on CPU to avoid being + affected by DeviceContext. + """ + ty = type(t) + if ty is builtins.float: + return torch.as_tensor(t, dtype=torch.float64, device="cpu") + elif ty is builtins.int: + return torch.as_tensor(t, dtype=torch.int64, device="cpu") + else: + return torch.as_tensor(t) + + +# `_import_device_backends` should be kept at the end to ensure +# all the other functions in this module that may be accessed by +# an autoloaded backend are defined +if _is_device_backend_autoload_enabled(): + _import_device_backends() + +# Register all registered custom / override ops in torch/_native +import torch._native diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_appdirs.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_appdirs.py new file mode 100644 index 0000000000000000000000000000000000000000..9d8ad9487e255ad2ebac86b2fbe9245f72657332 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_appdirs.py @@ -0,0 +1,666 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright (c) 2005-2010 ActiveState Software Inc. +# Copyright (c) 2013 Eddy Petrișor + +# flake8: noqa + +""" +This file is directly from +https://github.com/ActiveState/appdirs/blob/3fe6a83776843a46f20c2e5587afcffe05e03b39/appdirs.py + +The license of https://github.com/ActiveState/appdirs copied below: + + +# This is the MIT license + +Copyright (c) 2010 ActiveState Software Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" + +"""Utilities for determining application-specific dirs. + +See for details and usage. +""" +# Dev Notes: +# - Windows "Known Folders": https://learn.microsoft.com/en-us/windows/win32/shell/csidl +# - macOS File System Programming Guide: https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/Introduction/Introduction.html +# - XDG spec for Un*x: https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html + +__version__ = "1.4.4" +__version_info__ = tuple(int(segment) for segment in __version__.split(".")) + + +import os +import sys + + +unicode = str + +if sys.platform.startswith("java"): + import platform + + os_name = platform.java_ver()[3][0] + if os_name.startswith("Windows"): # "Windows XP", "Windows 7", etc. + system = "win32" + elif os_name.startswith("Mac"): # "Mac OS X", etc. + system = "darwin" + else: # "Linux", "SunOS", "FreeBSD", etc. + # Setting this to "linux2" is not ideal, but only Windows or Mac + # are actually checked for and the rest of the module expects + # *sys.platform* style strings. + system = "linux2" +else: + system = sys.platform + + +def user_data_dir(appname=None, appauthor=None, version=None, roaming=False): + r"""Return full path to the user-specific data dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "roaming" (boolean, default False) can be set True to use the Windows + roaming appdata directory. That means that for users on a Windows + network setup for roaming profiles, this user data will be + sync'd on login. See + + for a discussion of issues. + + Typical user data directories are: + Mac OS X: ~/Library/Application Support/ + Unix: ~/.local/share/ # or in $XDG_DATA_HOME, if defined + Win XP (not roaming): C:\Documents and Settings\\Application Data\\ + Win XP (roaming): C:\Documents and Settings\\Local Settings\Application Data\\ + Win 7 (not roaming): C:\Users\\AppData\Local\\ + Win 7 (roaming): C:\Users\\AppData\Roaming\\ + + For Unix, we follow the XDG spec and support $XDG_DATA_HOME. + That means, by default "~/.local/share/". + """ + if system == "win32": + if appauthor is None: + appauthor = appname + const = roaming and "CSIDL_APPDATA" or "CSIDL_LOCAL_APPDATA" + path = os.path.normpath(_get_win_folder(const)) + if appname: + if appauthor is not False: + path = os.path.join(path, appauthor, appname) + else: + path = os.path.join(path, appname) + elif system == "darwin": + path = os.path.expanduser("~/Library/Application Support/") + if appname: + path = os.path.join(path, appname) + else: + path = os.getenv("XDG_DATA_HOME", os.path.expanduser("~/.local/share")) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +def site_data_dir(appname=None, appauthor=None, version=None, multipath=False): + r"""Return full path to the user-shared data dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "multipath" is an optional parameter only applicable to *nix + which indicates that the entire list of data dirs should be + returned. By default, the first item from XDG_DATA_DIRS is + returned, or '/usr/local/share/', + if XDG_DATA_DIRS is not set + + Typical site data directories are: + Mac OS X: /Library/Application Support/ + Unix: /usr/local/share/ or /usr/share/ + Win XP: C:\Documents and Settings\All Users\Application Data\\ + Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.) + Win 7: C:\ProgramData\\ # Hidden, but writeable on Win 7. + + For Unix, this is using the $XDG_DATA_DIRS[0] default. + + WARNING: Do not use this on Windows. See the Vista-Fail note above for why. + """ + if system == "win32": + if appauthor is None: + appauthor = appname + path = os.path.normpath(_get_win_folder("CSIDL_COMMON_APPDATA")) + if appname: + if appauthor is not False: + path = os.path.join(path, appauthor, appname) + else: + path = os.path.join(path, appname) + elif system == "darwin": + path = os.path.expanduser("/Library/Application Support") + if appname: + path = os.path.join(path, appname) + else: + # XDG default for $XDG_DATA_DIRS + # only first, if multipath is False + path = os.getenv( + "XDG_DATA_DIRS", os.pathsep.join(["/usr/local/share", "/usr/share"]) + ) + pathlist = [ + os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep) + ] + if appname: + if version: + appname = os.path.join(appname, version) + pathlist = [os.sep.join([x, appname]) for x in pathlist] + + if multipath: + path = os.pathsep.join(pathlist) + else: + path = pathlist[0] + return path + + if appname and version: + path = os.path.join(path, version) + return path + + +def user_config_dir(appname=None, appauthor=None, version=None, roaming=False): + r"""Return full path to the user-specific config dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "roaming" (boolean, default False) can be set True to use the Windows + roaming appdata directory. That means that for users on a Windows + network setup for roaming profiles, this user data will be + sync'd on login. See + + for a discussion of issues. + + Typical user config directories are: + Mac OS X: ~/Library/Preferences/ + Unix: ~/.config/ # or in $XDG_CONFIG_HOME, if defined + Win *: same as user_data_dir + + For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME. + That means, by default "~/.config/". + """ + if system == "win32": + path = user_data_dir(appname, appauthor, None, roaming) + elif system == "darwin": + path = os.path.expanduser("~/Library/Preferences/") + if appname: + path = os.path.join(path, appname) + else: + path = os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +def site_config_dir(appname=None, appauthor=None, version=None, multipath=False): + r"""Return full path to the user-shared data dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "multipath" is an optional parameter only applicable to *nix + which indicates that the entire list of config dirs should be + returned. By default, the first item from XDG_CONFIG_DIRS is + returned, or '/etc/xdg/', if XDG_CONFIG_DIRS is not set + + Typical site config directories are: + Mac OS X: same as site_data_dir + Unix: /etc/xdg/ or $XDG_CONFIG_DIRS[i]/ for each value in + $XDG_CONFIG_DIRS + Win *: same as site_data_dir + Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.) + + For Unix, this is using the $XDG_CONFIG_DIRS[0] default, if multipath=False + + WARNING: Do not use this on Windows. See the Vista-Fail note above for why. + """ + if system == "win32": + path = site_data_dir(appname, appauthor) + if appname and version: + path = os.path.join(path, version) + elif system == "darwin": + path = os.path.expanduser("/Library/Preferences") + if appname: + path = os.path.join(path, appname) + else: + # XDG default for $XDG_CONFIG_DIRS + # only first, if multipath is False + path = os.getenv("XDG_CONFIG_DIRS", "/etc/xdg") + pathlist = [ + os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep) + ] + if appname: + if version: + appname = os.path.join(appname, version) + pathlist = [os.sep.join([x, appname]) for x in pathlist] + + if multipath: + path = os.pathsep.join(pathlist) + else: + path = pathlist[0] + return path + + +def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True): + r"""Return full path to the user-specific cache dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "opinion" (boolean) can be False to disable the appending of + "Cache" to the base app data dir for Windows. See + discussion below. + + Typical user cache directories are: + Mac OS X: ~/Library/Caches/ + Unix: ~/.cache/ (XDG default) + Win XP: C:\Documents and Settings\\Local Settings\Application Data\\\Cache + Vista: C:\Users\\AppData\Local\\\Cache + + On Windows the only suggestion in the MSDN docs is that local settings go in + the `CSIDL_LOCAL_APPDATA` directory. This is identical to the non-roaming + app data dir (the default returned by `user_data_dir` above). Apps typically + put cache data somewhere *under* the given dir here. Some examples: + ...\Mozilla\Firefox\Profiles\\Cache + ...\Acme\SuperApp\Cache\1.0 + OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value. + This can be disabled with the `opinion=False` option. + """ + if system == "win32": + if appauthor is None: + appauthor = appname + path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA")) + if appname: + if appauthor is not False: + path = os.path.join(path, appauthor, appname) + else: + path = os.path.join(path, appname) + if opinion: + path = os.path.join(path, "Cache") + elif system == "darwin": + path = os.path.expanduser("~/Library/Caches") + if appname: + path = os.path.join(path, appname) + else: + path = os.getenv("XDG_CACHE_HOME", os.path.expanduser("~/.cache")) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +def user_state_dir(appname=None, appauthor=None, version=None, roaming=False): + r"""Return full path to the user-specific state dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "roaming" (boolean, default False) can be set True to use the Windows + roaming appdata directory. That means that for users on a Windows + network setup for roaming profiles, this user data will be + sync'd on login. See + + for a discussion of issues. + + Typical user state directories are: + Mac OS X: same as user_data_dir + Unix: ~/.local/state/ # or in $XDG_STATE_HOME, if defined + Win *: same as user_data_dir + + For Unix, we follow this Debian proposal + to extend the XDG spec and support $XDG_STATE_HOME. + + That means, by default "~/.local/state/". + """ + if system in ["win32", "darwin"]: + path = user_data_dir(appname, appauthor, None, roaming) + else: + path = os.getenv("XDG_STATE_HOME", os.path.expanduser("~/.local/state")) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +def user_log_dir(appname=None, appauthor=None, version=None, opinion=True): + r"""Return full path to the user-specific log dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "opinion" (boolean) can be False to disable the appending of + "Logs" to the base app data dir for Windows, and "log" to the + base cache dir for Unix. See discussion below. + + Typical user log directories are: + Mac OS X: ~/Library/Logs/ + Unix: ~/.cache//log # or under $XDG_CACHE_HOME if defined + Win XP: C:\Documents and Settings\\Local Settings\Application Data\\\Logs + Vista: C:\Users\\AppData\Local\\\Logs + + On Windows the only suggestion in the MSDN docs is that local settings + go in the `CSIDL_LOCAL_APPDATA` directory. (Note: I'm interested in + examples of what some windows apps use for a logs dir.) + + OPINION: This function appends "Logs" to the `CSIDL_LOCAL_APPDATA` + value for Windows and appends "log" to the user cache dir for Unix. + This can be disabled with the `opinion=False` option. + """ + if system == "darwin": + path = os.path.join(os.path.expanduser("~/Library/Logs"), appname) + elif system == "win32": + path = user_data_dir(appname, appauthor, version) + version = False + if opinion: + path = os.path.join(path, "Logs") + else: + path = user_cache_dir(appname, appauthor, version) + version = False + if opinion: + path = os.path.join(path, "log") + if appname and version: + path = os.path.join(path, version) + return path + + +class AppDirs: + """Convenience wrapper for getting application dirs.""" + + def __init__( + self, appname=None, appauthor=None, version=None, roaming=False, multipath=False + ): + self.appname = appname + self.appauthor = appauthor + self.version = version + self.roaming = roaming + self.multipath = multipath + + @property + def user_data_dir(self): + return user_data_dir( + self.appname, self.appauthor, version=self.version, roaming=self.roaming + ) + + @property + def site_data_dir(self): + return site_data_dir( + self.appname, self.appauthor, version=self.version, multipath=self.multipath + ) + + @property + def user_config_dir(self): + return user_config_dir( + self.appname, self.appauthor, version=self.version, roaming=self.roaming + ) + + @property + def site_config_dir(self): + return site_config_dir( + self.appname, self.appauthor, version=self.version, multipath=self.multipath + ) + + @property + def user_cache_dir(self): + return user_cache_dir(self.appname, self.appauthor, version=self.version) + + @property + def user_state_dir(self): + return user_state_dir(self.appname, self.appauthor, version=self.version) + + @property + def user_log_dir(self): + return user_log_dir(self.appname, self.appauthor, version=self.version) + + +# ---- internal support stuff + + +def _get_win_folder_from_registry(csidl_name): + """This is a fallback technique at best. I'm not sure if using the + registry for this guarantees us the correct answer for all CSIDL_* + names. + """ + import winreg as _winreg + + shell_folder_name = { + "CSIDL_APPDATA": "AppData", + "CSIDL_COMMON_APPDATA": "Common AppData", + "CSIDL_LOCAL_APPDATA": "Local AppData", + }[csidl_name] + + key = _winreg.OpenKey( + _winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders", + ) + dir, _type = _winreg.QueryValueEx(key, shell_folder_name) + return dir + + +def _get_win_folder_with_pywin32(csidl_name): + from win32com.shell import shell, shellcon + + dir = shell.SHGetFolderPath(0, getattr(shellcon, csidl_name), 0, 0) + # Try to make this a unicode path because SHGetFolderPath does + # not return unicode strings when there is unicode data in the + # path. + try: + dir = unicode(dir) + + # Downgrade to short path name if have highbit chars. See + # . + has_high_char = False + for c in dir: + if ord(c) > 255: + has_high_char = True + break + if has_high_char: + try: + import win32api + + dir = win32api.GetShortPathName(dir) + except ImportError: + pass + except UnicodeError: + pass + return dir + + +def _get_win_folder_with_ctypes(csidl_name): + import ctypes + + csidl_const = { + "CSIDL_APPDATA": 26, + "CSIDL_COMMON_APPDATA": 35, + "CSIDL_LOCAL_APPDATA": 28, + }[csidl_name] + + buf = ctypes.create_unicode_buffer(1024) + ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf) + + # Downgrade to short path name if have highbit chars. See + # . + has_high_char = False + for c in buf: + if ord(c) > 255: + has_high_char = True + break + if has_high_char: + buf2 = ctypes.create_unicode_buffer(1024) + if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024): + buf = buf2 + + return buf.value + + +def _get_win_folder_with_jna(csidl_name): + import array + + from com.sun import jna + from com.sun.jna.platform import win32 + + buf_size = win32.WinDef.MAX_PATH * 2 + buf = array.zeros("c", buf_size) + shell = win32.Shell32.INSTANCE + shell.SHGetFolderPath( + None, + getattr(win32.ShlObj, csidl_name), + None, + win32.ShlObj.SHGFP_TYPE_CURRENT, + buf, + ) + dir = jna.Native.toString(buf.tostring()).rstrip("\0") + + # Downgrade to short path name if have highbit chars. See + # . + has_high_char = False + for c in dir: + if ord(c) > 255: + has_high_char = True + break + if has_high_char: + buf = array.zeros("c", buf_size) + kernel = win32.Kernel32.INSTANCE + if kernel.GetShortPathName(dir, buf, buf_size): + dir = jna.Native.toString(buf.tostring()).rstrip("\0") + + return dir + + +if system == "win32": + try: + import win32com.shell + + _get_win_folder = _get_win_folder_with_pywin32 + except ImportError: + try: + from ctypes import windll + + _get_win_folder = _get_win_folder_with_ctypes + except ImportError: + try: + import com.sun.jna + + _get_win_folder = _get_win_folder_with_jna + except ImportError: + _get_win_folder = _get_win_folder_from_registry + + +# ---- self test code + +if __name__ == "__main__": + appname = "MyApp" + appauthor = "MyCompany" + + props = ( + "user_data_dir", + "user_config_dir", + "user_cache_dir", + "user_state_dir", + "user_log_dir", + "site_data_dir", + "site_config_dir", + ) + + print(f"-- app dirs {__version__} --") + + print("-- app dirs (with optional 'version')") + dirs = AppDirs(appname, appauthor, version="1.0") + for prop in props: + print(f"{prop}: {getattr(dirs, prop)}") + + print("\n-- app dirs (without optional 'version')") + dirs = AppDirs(appname, appauthor) + for prop in props: + print(f"{prop}: {getattr(dirs, prop)}") + + print("\n-- app dirs (without optional 'appauthor')") + dirs = AppDirs(appname) + for prop in props: + print(f"{prop}: {getattr(dirs, prop)}") + + print("\n-- app dirs (with disabled 'appauthor')") + dirs = AppDirs(appname, appauthor=False) + for prop in props: + print(f"{prop}: {getattr(dirs, prop)}") diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_awaits/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_awaits/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3f8d2bf058c08847bd09afc905bc88e6823bc917 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_awaits/__init__.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from typing import Generic, TypeVar + +import torch + +# pyrefly: ignore [bad-dunder-all] +__all__ = ['Await'] + +W = TypeVar("W") + +class _PyAwaitMeta(type(torch._C._Await), type(Generic)): # type: ignore[misc, no-redef] + pass + +class _Await(torch._C._Await, Generic[W], metaclass=_PyAwaitMeta): + r""" + Wrapper around a ``torch._C.Await`` which encapsulates delayed execution + of a callable. All manipulations happen with functions ``torch.jit._awaitable``, + ``torch.jit._awaitable_wait``, ``torch.jit._awaitable_nowait``. + + Torch scriptable manipulations: + ``torch.jit._awaitable(func, *args)`` + Creates ``Await[W]`` object, where W is return type of func. + + Returns: + ``torch.jit._awaitable_wait(Await[W])`` + Returns the result of the function, specified at ``_awaitable``, with specified arguments. + + Returns: + The result of type ``W`` of the function call. The result is owned by ``Await[W]`` + and returned on all following ``_awaitable_wait`` calls. + + + ``torch.jit._awaitable_nowait(W)`` + Returns: + Trivial ``Await[W]`` with specified result. + + + Only in eager mode: + ``fn() -> Callable[Tuple[Any], W]`` + Returns: + Specified at ``_awaitable`` python function ``func``. + + ``args() -> Tuple[Any]`` + Returns: + Specified at ``_awaitable`` python args. + + ``is_nowait() -> _bool`` + Returns: + ``True`` if this object was created via ``_awaitable_nowait`` call (trivial `Await[W]`). + + In eager mode ``Await[W]`` can be used as ``W`` i.e. attributes of W can be called on ``Await[W]``, + ``_awaitable_wait()`` call will be transparently added. + """ diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_classes.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_classes.py new file mode 100644 index 0000000000000000000000000000000000000000..a811c7c30be612bf0a89e5c8a5473e9d54d02ba3 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_classes.py @@ -0,0 +1,56 @@ +import types +from typing import Any + +import torch._C + + +class _ClassNamespace(types.ModuleType): + def __init__(self, name: str) -> None: + super().__init__("torch.classes" + name) + self.name = name + + def __getattr__(self, attr: str) -> Any: + proxy = torch._C._get_custom_class_python_wrapper(self.name, attr) + if proxy is None: + raise RuntimeError(f"Class {self.name}.{attr} not registered!") + return proxy + + +class _Classes(types.ModuleType): + __file__ = "_classes.py" + + def __init__(self) -> None: + super().__init__("torch.classes") + + def __getattr__(self, name: str) -> _ClassNamespace: + namespace = _ClassNamespace(name) + setattr(self, name, namespace) + return namespace + + @property + def loaded_libraries(self) -> Any: + return torch.ops.loaded_libraries + + def load_library(self, path: str) -> None: + """ + Loads a shared library from the given path into the current process. + + The library being loaded may run global initialization code to register + custom classes with the PyTorch JIT runtime. This allows dynamically + loading custom classes. For this, you should compile your class + and the static registration code into a shared library object, and then + call ``torch.classes.load_library('path/to/libcustom.so')`` to load the + shared object. + + After the library is loaded, it is added to the + ``torch.classes.loaded_libraries`` attribute, a set that may be inspected + for the paths of all libraries loaded using this function. + + Args: + path (str): A path to a shared library to load. + """ + torch.ops.load_library(path) + + +# The classes "namespace" +classes = _Classes() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_compile.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_compile.py new file mode 100644 index 0000000000000000000000000000000000000000..bf7d715883d58588c7f991a7393f016d2320213c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_compile.py @@ -0,0 +1,60 @@ +""" +APIs related to torch.compile which lazily import torch._dynamo to avoid +circular dependencies. +""" + +import functools +from collections.abc import Callable +from typing import overload, TypeVar +from typing_extensions import ParamSpec + + +_T = TypeVar("_T") +_P = ParamSpec("_P") + + +@overload +def _disable_dynamo( + fn: Callable[_P, _T], recursive: bool = True +) -> Callable[_P, _T]: ... + + +@overload +def _disable_dynamo( + fn: None = None, recursive: bool = True +) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]: ... + + +def _disable_dynamo( + fn: Callable[_P, _T] | None = None, recursive: bool = True +) -> Callable[_P, _T] | Callable[[Callable[_P, _T]], Callable[_P, _T]]: + """ + This API should be only used inside torch, external users should still use + torch._dynamo.disable. The main goal of this API is to avoid circular + imports issues that is common while using _dynamo.disable inside torch + itself. + + This API avoids it by lazily importing torch._dynamo from the import time to + the invocation of the decorated function. + """ + if fn is not None: + + @functools.wraps(fn) + def inner(*args: _P.args, **kwargs: _P.kwargs) -> _T: + # cache this on the first invocation to avoid adding too much overhead. + disable_fn = getattr(fn, "__dynamo_disable", None) + if disable_fn is None: + import torch._dynamo + + # We can safely turn off functools.wraps here because the inner + # already wraps fn in the outer scope. + disable_fn = torch._dynamo.disable(fn, recursive, wrapping=False) + fn.__dynamo_disable = disable_fn # type: ignore[attr-defined] + + return disable_fn(*args, **kwargs) + + return inner + else: + # decorator usage like @_disable_dynamo(recursive=False). The resulting + # object expects the original decorated function as the arg. + return functools.partial(_disable_dynamo, recursive=recursive) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_custom_op/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_custom_op/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_custom_op/autograd.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_custom_op/autograd.py new file mode 100644 index 0000000000000000000000000000000000000000..5967d284200f533893d105d2a0eb2bec3f4238ad --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_custom_op/autograd.py @@ -0,0 +1,314 @@ +# mypy: allow-untyped-defs +import functools +from collections import namedtuple + +import torch +import torch.utils._pytree as pytree + + +# NOTE [CustomOp autograd kernel indirection] +# We register `inner` as the autograd kernel for this custom_op. +# `inner` either calls the autograd formula registered by the user, +# or goes into an `autograd_not_implemented` kernel. +# +# The reason why this indirection exists is +# so that we can swap out the autograd kernel (the PyTorch dispatcher +# doesn't actually allow us to do this). By default, we want +# the `autograd_not_implemented` behavior, but then the user may come +# and register something that is actually a backward formula +def autograd_kernel_indirection(custom_op): + autograd_fallback = autograd_not_implemented(custom_op) + + def inner(*args, **kwargs): + if custom_op._has_impl("autograd"): + kernel = custom_op._get_impl("autograd").func + return kernel(*args, **kwargs) + # As explained in NOTE ["backward", "save_for_backward", and "autograd"], + # after the user gives us "backward" and "save_for_backward", we generate + # the "autograd" impl. If the user only provided one, then we tell + # the user they've done something wrong. + if custom_op._has_impl("save_for_backward") or custom_op._has_impl("backward"): + missing = ( + "save_for_backward" if custom_op._has_impl("backward") else "backward" + ) + found = "save_for_backward" if missing == "backward" else "backward" + loc = custom_op._get_impl(found).location + raise RuntimeError( + f"We found a '{found}' registration for {custom_op} at " + f"{loc} but were unable to find a '{missing}' registration. " + f"To use the CustomOp API to register a backward formula, " + f"please provide us both a backward function and a " + f"'save for backward' function via `impl_backward` and " + f"`impl_save_for_backward` respectively." + ) + return autograd_fallback(*args, **kwargs) + + return inner + + +# TODO(#101191): Use the actual C++ autograd not implemented fallback, +# or change the default autograd fallback to the autograd not implemented fallback. +def autograd_not_implemented(custom_op): + def kernel(*args, **kwargs): + if torch.is_grad_enabled() and pytree.tree_any( + lambda x: isinstance(x, torch.Tensor) and x.requires_grad, (args, kwargs) + ): + raise RuntimeError("Autograd has not been implemented for operator") + with torch._C._AutoDispatchBelowAutograd(): + return custom_op(*args, **kwargs) + + return kernel + + +def mark_non_differentiable(ctx, output, output_differentiability): + # Output types are restricted to be: + # - Tensor + # - Tensor[] + # - int, bool, Scalar, float + # See _check_can_register_backward + if output_differentiability is not None: + if not isinstance(output, tuple): + tuple_output = (output,) + else: + tuple_output = output # type: ignore[assignment] + if len(output_differentiability) != len(tuple_output): + raise AssertionError( + f"output_differentiability length {len(output_differentiability)} " + f"!= output length {len(tuple_output)}" + ) + non_differentiable_tensors = [] + for idx, (differentiable, out) in enumerate( + zip(output_differentiability, tuple_output) + ): + if isinstance(out, torch.Tensor): + if not differentiable: + non_differentiable_tensors.append(out) + continue + if isinstance(out, list): + if not differentiable: + non_differentiable_tensors.extend(out) + continue + if differentiable: + raise RuntimeError( + f"With output_differentiability={output_differentiability}. " + f"At idx {idx}, we received an object of type {type(out)} that " + f"is not a Tensor, so it cannot have be marked as differentiable in " + f"output_differentiability." + ) + if non_differentiable_tensors: + ctx.mark_non_differentiable(*non_differentiable_tensors) + + +def construct_autograd_kernel( + schema, + output_differentiability, + custom_op, + op_overload, + save_for_backward_fn, + backward_fn, +): + def apply(*args): + flat_args, spec = pytree.tree_flatten(args) + out_spec = None + + def forward(ctx, *flat_args): + ctx.set_materialize_grads(True) + args = pytree.tree_unflatten(list(flat_args), spec) + with torch._C._AutoDispatchBelowAutograd(): + output = op_overload(*args) + + # We use the info about args to give better error messages in backward + args_info = namedtuple_args(schema, pytree.tree_map(type, args)) + + save_for_backward_fn_inputs = namedtuple_args(schema, args) + to_save = save_for_backward_fn(save_for_backward_fn_inputs, output) + + save_pytree_for_backward(ctx, (to_save, args_info)) + mark_non_differentiable(ctx, output, output_differentiability) + + nonlocal out_spec + flat_output, out_spec = pytree.tree_flatten(output) + return tuple(flat_output) + + def backward(ctx, *flat_grad_output): + if out_spec is None: + raise AssertionError("out_spec is unexpectedly None") + grads = pytree.tree_unflatten(list(flat_grad_output), out_spec) + saved, args_info = unpack_saved(ctx) + # There is nothing on the ctx object for now, it is just there so + # that we can add additional things in the future. + inner_ctx = object() + if not isinstance(grads, tuple): + grads = (grads,) + grad_inputs_dict = backward_fn(inner_ctx, saved, *grads) + + # Massage the grad_inputs_dict to a form acceptable by + # autograd.Function. + validate_grad_inputs_dict(grad_inputs_dict, custom_op, args_info) + return grad_inputs_dict_to_flat_tuple(grad_inputs_dict, args_info) + + generated_cls = gen_autograd_function( + custom_op._opname + "_customop", forward, backward + ) + + flat_output = generated_cls.apply(*flat_args) + if out_spec is None: + raise AssertionError("out_spec is unexpectedly None") + return pytree.tree_unflatten(list(flat_output), out_spec) + + return apply + + +def gen_autograd_function(name, forward, backward): + generated_cls = type( + name, + (torch.autograd.Function,), + { + "forward": staticmethod(forward), + "backward": staticmethod(backward), + }, + ) + return generated_cls + + +@functools.lru_cache +def namedtuple_args_cls(schema): + attribs = [arg.name for arg in schema.arguments.flat_all] + name = str(schema.name) + "_args" + # mypy doesn't support dynamic namedtuple name + tuple_cls = namedtuple(name, attribs) # type: ignore[misc] + return tuple_cls + + +def namedtuple_args(schema, args): + if not isinstance(args, tuple): + raise AssertionError(f"expected tuple, got {type(args)}") + tuple_cls = namedtuple_args_cls(schema) + return tuple_cls(*args) + + +def validate_grad_inputs_dict(grad_inputs_dict, forward_op, args_info): + def error(what): + backward = forward_op._get_impl("backward") + raise RuntimeError( + f"In the backward function defined for {forward_op} at " + f"{backward.location} using the CustomOp API, {what}" + ) + + if not isinstance(grad_inputs_dict, dict): + error( + f"expected the output of the backward function to be a dict but " + f"got {type(grad_inputs_dict)}" + ) + + expected_keys = { + arg.name + for arg in forward_op._schema.arguments.flat_all + if arg.type.is_tensor_like() + } + actual_keys = grad_inputs_dict.keys() + if expected_keys != actual_keys: + error( + f"expected the returned grad_input dict to have keys " + f"{expected_keys} but got {actual_keys}. The backward " + f"function must return a gradient (can be None) for each arg " + f"to the CustomOp that may be a Tensor or Sequence[Tensor]. " + f"Args declared to be non-Tensor-like types should not appear " + f"in the grad_input dict" + ) + + for name, grad in grad_inputs_dict.items(): + arg_info = getattr(args_info, name) + + if isinstance(arg_info, list): + if not isinstance(grad, (tuple, list)): + error( + f"for input '{name}' expected the grad_input dict to " + f"hold a list of gradients but got object of type " + f"{type(grad)}." + ) + if len(grad) != len(arg_info): + error( + f"for input '{name}' expected the grad_input dict to " + f"hold a list of {len(arg_info)} gradients but got " + f"{len(grad)}" + ) + for idx, (g, info) in enumerate(zip(grad, arg_info)): + if g is None: + continue + if not isinstance(g, torch.Tensor): + error( + f"for input '{name}' expected the grad_input dict to " + f"hold a list of None or Tensor gradients but got " + f"object of {type(g)} at index {idx}" + ) + if not issubclass(info, torch.Tensor): + error( + f"for input '{name}', got a Tensor as the gradient " + f"for the {idx}-th value but expected None because " + f"the {idx}-th value was not a Tensor (it was " + f"type {arg_info}" + ) + continue + + if grad is None: + continue + if not isinstance(grad, torch.Tensor): + error( + f"got object of type {type(grad)} as the gradient for input " + f"'{name}', " + f"but expected the gradient to be either None or a Tensor" + ) + if not issubclass(arg_info, torch.Tensor): + error( + f"got a Tensor as the gradient for input '{name}' but " + f"expected None as the gradient because input '{name}' " + f"was not a Tensor (it was type {arg_info})." + ) + + +def grad_inputs_dict_to_flat_tuple(grad_inputs_dict, args_info): + result = [] + for name, arg_info in args_info._asdict().items(): + if name not in grad_inputs_dict: + result.append(pytree.tree_map(lambda x: None, arg_info)) + continue + result.append(grad_inputs_dict[name]) + return tuple(pytree.tree_leaves(result)) + + +# Saves "stuff" (a pytree) onto the ctx object. Use unpack_saved to unpack it. +# autograd.Function prefers that users use ctx.save_for_backward to +# save Tensors (to avoid reference cycles) and for non-Tensors to go onto the +# ctx object. +def save_pytree_for_backward(ctx, stuff): + flat_stuff, spec = pytree.tree_flatten(stuff) + num_elts = len(flat_stuff) + tensor_idxs = [ + idx for idx, thing in enumerate(flat_stuff) if isinstance(thing, torch.Tensor) + ] + non_tensor_idxs = [ + idx + for idx, thing in enumerate(flat_stuff) + if not isinstance(thing, torch.Tensor) + ] + tensors = [thing for thing in flat_stuff if isinstance(thing, torch.Tensor)] + non_tensors = [thing for thing in flat_stuff if not isinstance(thing, torch.Tensor)] + + ctx.spec = spec + ctx.num_elts = num_elts + ctx.save_for_backward(*tensors) + ctx.tensor_idxs = tensor_idxs + ctx.saved_non_tensors = non_tensors + ctx.non_tensor_idxs = non_tensor_idxs + + +# Inverse operation to save_pytree_for_backward +def unpack_saved(ctx): + flat_stuff = [None] * ctx.num_elts + for tensor, idx in zip(ctx.saved_tensors, ctx.tensor_idxs): + flat_stuff[idx] = tensor + for non_tensor, idx in zip(ctx.saved_non_tensors, ctx.non_tensor_idxs): + flat_stuff[idx] = non_tensor + stuff = pytree.tree_unflatten(flat_stuff, ctx.spec) + return stuff diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_custom_op/impl.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_custom_op/impl.py new file mode 100644 index 0000000000000000000000000000000000000000..ae01dab906226ab592dda5926033ce0c8ea7424f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_custom_op/impl.py @@ -0,0 +1,719 @@ +# mypy: allow-untyped-defs +import dataclasses +import functools +import inspect +import sys +import typing +import warnings +import weakref + +import torch +import torch._C as _C +import torch._library.infer_schema +import torch.library as library +from torch._library.infer_schema import infer_schema +from torch.library import get_ctx +from torchgen.model import ( + BaseTy, + BaseType, + FunctionSchema, + ListType, + OperatorName, + SchemaKind, +) + +from .autograd import autograd_kernel_indirection, construct_autograd_kernel + + +""" +torch._custom_op is deprecated. We shipped a production-ready version of it into torch.library. +Please use those APIs instead. +""" + +__all__ = ["custom_op", "CustomOp", "get_ctx"] + + +SUPPORTED_DEVICE_TYPE_TO_KEY = { + "cpu": "CPU", + "cuda": "CUDA", +} + +# We will not let users register CustomOps with anything that could look like +# PyTorch internals to avoid confusion. +RESERVED_NS = { + "prim", + "prims", + "aten", + "at", + "torch", + "pytorch", +} + + +def warn_deprecated(): + warnings.warn( + "torch._custom_op is deprecated and will be removed in PyTorch 2.6, please " + "use the equivalent torch.library API instead.", + DeprecationWarning, + stacklevel=2, + ) + + +def custom_op(qualname: str, manual_schema: str | None = None) -> typing.Callable: + r""" + This API is deprecated, please use torch.library.custom_op instead + """ + warn_deprecated() + + def inner(func): + if not inspect.isfunction(func): + raise ValueError( + f"custom_op(...)(func): Expected `func` to be a Python " + f"function, got: {type(func)}" + ) + + ns, name = parse_qualname(qualname) + validate_namespace(ns) + if func.__name__ != name: + raise ValueError( + f"custom_op(qualname='{qualname}', ...)(func): expected `func` " + f"to have name '{name}' but got '{func.__name__}'. " + f"Please either change the name of `func` or the qualname that " + f"is passed to `custom_op`" + ) + + schema = ( + infer_schema(func, mutates_args=()) + if manual_schema is None + else manual_schema + ) + schema_str = f"{name}{schema}" + function_schema = FunctionSchema.parse(schema_str) + validate_schema(function_schema) + if manual_schema is not None: + validate_function_matches_schema(function_schema, func) + + lib = library.Library(ns, "FRAGMENT") + lib.define(schema_str) + ophandle = find_ophandle_or_throw(ns, function_schema.name) + result = CustomOp( + lib, ns, function_schema, name, ophandle, _private_access=True + ) + + result.__name__ = func.__name__ # pyrefly: ignore [bad-assignment] + result.__module__ = func.__module__ + result.__doc__ = func.__doc__ + + library.impl(lib, result._opname, "Autograd")( + autograd_kernel_indirection(weakref.proxy(result)) + ) + + torch._C._dispatch_set_report_error_callback( + ophandle, functools.partial(report_error_callback, weakref.proxy(result)) + ) + + return result + + return inner + + +# Global dictionary holding references to all CustomOp objects +# Yes, it keeps all CustomOps alive (see NOTE [CustomOp lifetime]) +# Used to query the CustomOp associated with a specific C++ dispatcher operator. +# An example usage is FakeTensor: FakeTensor checks if a specific operator +# has an implementation registered via the CustomOp API. +# Indexed by qualname (e.g. aten::foo) +global_registry: dict[str, "CustomOp"] = {} + + +class CustomOp: + r""" + This API is deprecated, please use torch.library.custom_op instead + """ + + def __init__( + self, lib, cpp_ns, schema, operator_name, ophandle, *, _private_access=False + ): + super().__init__() + warn_deprecated() + if not _private_access: + raise RuntimeError( + "The CustomOp constructor is private and we do not guarantee " + "BC for it. Please use custom_op(...) to create a CustomOp object" + ) + name = f"{cpp_ns}::{operator_name}" + self._schema = schema + self._cpp_ns = cpp_ns + self._lib: library.Library = lib + self._ophandle: _C._DispatchOperatorHandle = ophandle + # Has the name of the op, e.g. "foo". We cache here for convenience. + self._opname: str = operator_name + # this is _opname but with namespace. e.g. "custom::foo" + self._qualname: str = name + self.__name__ = None # mypy requires this + # NB: Some of these impls are registered as kernels to DispatchKeys. + # Modifying the _impls dict directly won't do anything in that case. + self._impls: dict[str, FuncAndLocation | None] = {} + # See NOTE [CustomOp autograd kernel indirection] + self._registered_autograd_kernel_indirection = False + + global_registry[self._qualname] = self + + def _register_autograd_kernel_indirection(self): + if self._registered_autograd_kernel_indirection: + raise AssertionError("autograd kernel indirection already registered") + self._lib.impl( + self._opname, autograd_kernel_indirection(weakref.proxy(self)), "Autograd" + ) + self._registered_autograd_kernel_indirection = True + + # Records the impl and the source location in self._impls + # Note that this doesn't cause torch.library to use the impl, that + # needs to be done in a separate self._lib.impl call. + def _register_impl(self, kind, func, stacklevel=2): + if self._has_impl(kind): + func_and_location = self._impls[kind] + if func_and_location is None: + raise AssertionError("func_and_location is unexpectedly None") + location = func_and_location.location + raise RuntimeError( + f"Attempting to register a {kind} impl for operator {self._qualname} " + f"that already has a {kind} impl registered from Python at " + f"{location}. This is not supported." + ) + frame = inspect.getframeinfo(sys._getframe(stacklevel)) + location = f"{frame.filename}:{frame.lineno}" + self._impls[kind] = FuncAndLocation(func, location) + + def _get_impl(self, kind): + return self._impls[kind] + + def _has_impl(self, kind): + return kind in self._impls + + def _destroy(self): + # NOTE: [CustomOp lifetime] + # A CustomOp, once created, lives forever. The mechanism is that the + # global registry holds a reference to it. However, to make testing + # easier, we want to be able to destroy CustomOp objects. + # CustomOp._destroy does the job, though it leaves the CustomOp + # in a garbage state. + del self._lib + + opnamespace = getattr(torch.ops, self._cpp_ns) + if hasattr(opnamespace, self._opname): + delattr(opnamespace, self._opname) + + del global_registry[self._qualname] + + def __repr__(self): + return f'' + + def __call__(self, *args, **kwargs): + # Bypass torch.ops.* and directly do OperatorHandle::callBoxed. + # Using torch.ops.* is a bit of a pain (it can be slow and it has lifetime + # issues from caching operators that make testing CustomOp difficult). + result = _C._dispatch_call_boxed(self._ophandle, *args, **kwargs) + return result + + def impl( + self, + device_types: str | typing.Iterable[str], + _stacklevel=2, + ) -> typing.Callable: + r""" + This API is deprecated, please use torch.library.custom_op instead + """ + if isinstance(device_types, str): + device_types = [device_types] + for device_type in device_types: + validate_device_type(device_type) + + def inner(f): + for device_type in set(device_types): + self._check_doesnt_have_library_impl(device_type) + self._register_impl(device_type, f, stacklevel=_stacklevel) + dispatch_key = SUPPORTED_DEVICE_TYPE_TO_KEY[device_type] + library.impl(self._lib, self._opname, dispatch_key)(f) + return f + + return inner + + def _check_doesnt_have_library_impl(self, device_type): + if self._has_impl(device_type): + return + key = SUPPORTED_DEVICE_TYPE_TO_KEY[device_type] + if _C._dispatch_has_computed_kernel_for_dispatch_key(self._qualname, key): + raise RuntimeError( + f"impl(..., device_types={device_type}): the operator {self._qualname} " + f"already has an implementation for this device type via a " + f"pre-existing torch.library or TORCH_LIBRARY registration." + ) + + def impl_factory(self) -> typing.Callable: + r"""Register an implementation for a factory function.""" + + def inner(f): + self._register_impl("factory", f) + library.impl(self._lib, self._opname, "BackendSelect")(f) + return f + + return inner + + def impl_abstract(self, _stacklevel=2) -> typing.Callable: + r""" + This API is deprecated, please use torch.library.custom_op instead + """ + + def inner(f): + self._check_doesnt_have_library_meta_impl() + self._register_impl("abstract", f, stacklevel=_stacklevel) + location = self._get_impl("abstract").location + + qualname = self._qualname + + # Handle DispatchKey.Meta registration + @functools.wraps(f) + def f_with_ctx(*args, **kwargs): + def error_on_ctx(): + raise RuntimeError( + f"Attempted to call get_ctx() for the meta implementation " + f"for {qualname}." + f"You have presumably called get_ctx() because the operator " + f"has a data-dependent output shape; if so, there is no " + f"such meta implementation and this error is the correct " + f"behavior. Otherwise, please remove the call to get_ctx() " + f"in the implementation registered with impl_abstract " + f"at {location}" + ) + + with torch._library.fake_impl.set_ctx_getter(error_on_ctx): + return f(*args, **kwargs) + + self._lib.impl(self._opname, f_with_ctx, "Meta") + return f + + return inner + + def _check_can_register_backward(self): + def error(detail): + raise RuntimeError( + f"Cannot use torch._custom_ops APIs to register backward " + f"formula for {detail}. Got operator " + f"{self._qualname} with schema: {schema}" + ) + + schema = self._schema + if schema.kind() != SchemaKind.functional: + error("non-functional operator") + + rets = schema.returns + if not schema.returns: + error("operator with no returns") + + if len(rets) <= 0: + raise AssertionError(f"expected at least one return, got {len(rets)}") + is_non_mutating_view = any( + r.annotation is not None and not r.annotation.is_write for r in rets + ) + if is_non_mutating_view: + error("operator that returns views") + + # We make assumptions about the schema's return types. + allowed_return_types = { + BaseType(BaseTy.int): "int", + BaseType(BaseTy.SymInt): "SymInt", + BaseType(BaseTy.bool): "bool", + BaseType(BaseTy.float): "float", + BaseType(BaseTy.Tensor): "Tensor", + ListType(BaseType(BaseTy.Tensor), None): "List[Tensor]", + } + for ret in schema.returns: + if ret.type in allowed_return_types: + continue + error( + f"operator with return not in {list(allowed_return_types.values())} (got {ret.type})" + ) + + def _check_doesnt_have_library_autograd_impl(self): + if self._registered_autograd_kernel_indirection: + return + + if _C._dispatch_has_kernel_for_dispatch_key( + self._qualname, "CompositeImplicitAutograd" + ): + raise RuntimeError( + f"impl_backward/impl_save_for_backward: the operator {self._qualname} " + f"already has an implementation for this device type via a " + f"pre-existing registration to DispatchKey::CompositeImplicitAutograd." + f"CompositeImplicitAutograd operators do not need an autograd formula; " + f"instead, the operator will decompose into its constituents and those " + f"can have autograd formulas defined on them." + ) + + # We can improve this by adding "all Autograd keys", but + # realistically people will just be using this API for CPU/CUDA for now. + for key in ["Autograd", "AutogradCPU", "AutogradCUDA"]: + if _C._dispatch_has_kernel_for_dispatch_key(self._qualname, key): + raise RuntimeError( + f"impl_backward/impl_save_for_backward: " + f"the operator {self._qualname} already has an Autograd kernel " + f"registered to DispatchKey::{key} vi a pre-existing " + f"torch.library or TORCH_LIBRARY registration. Please either " + f"remove those registrations or don't use the torch._custom_ops APIs" + ) + + def _check_doesnt_have_library_meta_impl(self): + if self._has_impl("abstract"): + return + + # If the user's operator is CompositeExplicitAutograd, + # allow them to impl_abstract. This is being pragmatic + # (existing custom ops may have CompositeExplicitAutograd + # registration that don't work with Meta kernels, so this + # gives them an escape hatch). + if _C._dispatch_has_kernel_for_dispatch_key( + self._qualname, "CompositeExplicitAutograd" + ) and not _C._dispatch_has_kernel_for_dispatch_key(self._qualname, "Meta"): + return + + # Otherwise, if the user's already has a Meta kernel or their + # op is CompositeImplicitAutograd or some other alias dispatch key, + # raise. + + # Special case for CompositeImplicitAutograd + if _C._dispatch_has_kernel_for_dispatch_key( + self._qualname, "CompositeImplicitAutograd" + ): + raise RuntimeError( + f"impl_abstract(...): the operator {self._qualname} " + f"already has an implementation for this device type via a " + f"pre-existing registration to DispatchKey::CompositeImplicitAutograd." + f"CompositeImplicitAutograd operators do not need an abstract impl; " + f"instead, the operator will decompose into its constituents and those " + f"can have abstract impls defined on them." + ) + + if _C._dispatch_has_kernel_for_dispatch_key(self._qualname, "Meta"): + raise RuntimeError( + f"impl_abstract(...): the operator {self._qualname} " + f"already has an DispatchKey::Meta implementation via a " + f"pre-existing torch.library or TORCH_LIBRARY registration. " + f"Please either remove that registration or don't call impl_abstract." + ) + + # NOTE ["backward", "save_for_backward", and "autograd"] + # As a part of the explicit autograd API, a user must provide us + # a "save_for_backward" function and a "backward" function. + # When both of these have been provided, then we automatically + # construct the "autograd" kernel. + def _register_autograd_kernel(self): + if not self._has_impl("backward"): + raise AssertionError("backward impl must be registered first") + if not self._has_impl("save_for_backward"): + raise AssertionError("save_for_backward impl must be registered first") + kernel = construct_autograd_kernel( + self._schema, + self._output_differentiability, + self, + get_op(self._qualname), + self._get_impl("save_for_backward").func, + self._get_impl("backward").func, + ) + self._register_impl("autograd", kernel) + + def impl_save_for_backward(self, _stacklevel=2): + r"""Register a function that tells us what to save for backward. + + Please see impl_backward for more details. + """ + + def inner(f): + self._check_can_register_backward() + self._check_doesnt_have_library_autograd_impl() + if not self._registered_autograd_kernel_indirection: + self._register_autograd_kernel_indirection() + self._register_impl("save_for_backward", f, stacklevel=_stacklevel) + if self._has_impl("backward"): + self._register_autograd_kernel() + + return inner + + def impl_backward(self, output_differentiability=None, _stacklevel=2): + r""" + This API is deprecated, please use torch.library.custom_op instead + """ + if output_differentiability is not None: + + def yell(): + raise RuntimeError( + f"impl_backward(output_differentiability): expected " + f"output_differentiability to be a list of bools with " + f"length equal to the number of outputs of this CustomOp " + f"got: {output_differentiability}" + ) + + if not isinstance(output_differentiability, list): + yell() + for diff in output_differentiability: + if not isinstance(diff, bool): + yell() + if len(self._schema.returns) != len(output_differentiability): + yell() + + def inner(f): + self._check_can_register_backward() + self._check_doesnt_have_library_autograd_impl() + if not self._registered_autograd_kernel_indirection: + self._register_autograd_kernel_indirection() + self._register_impl("backward", f, stacklevel=_stacklevel) + self._output_differentiability = output_differentiability + if self._has_impl("save_for_backward"): + self._register_autograd_kernel() + + return inner + + +@dataclasses.dataclass +class FuncAndLocation: + func: typing.Callable + location: str + + +def find_ophandle_or_throw(cpp_ns: str, operator_name: OperatorName): + overload_name = ( + "" if operator_name.overload_name is None else operator_name.overload_name + ) + return _C._dispatch_find_schema_or_throw( + f"{cpp_ns}::{str(operator_name.name)}", overload_name + ) + + +def validate_namespace(ns: str) -> None: + if "." in ns: + raise ValueError( + f'custom_op(..., ns="{ns}"): expected ns to not contain any . (and be a ' + f"valid variable name)" + ) + if ns in RESERVED_NS: + raise ValueError( + f"custom_op(..., ns='{ns}'): '{ns}' is a reserved namespace, " + f"please choose something else. " + ) + + +def validate_schema(schema: FunctionSchema) -> None: + if not torch._library.utils.is_functional_schema(schema): + raise ValueError( + f"custom_op only supports functional operators " + f"(ops that do not mutate any inputs, do not return " + f"views of the inputs, and has at least one return). " + f"Got the following non-functional schema: {schema}" + ) + + # For simplicity: don't allow self arguments + if schema.arguments.self_arg is not None: + raise ValueError( + f"custom_op does not support arguments named 'self'. Please " + f"rename your argument. Got: {schema}" + ) + + +def parse_qualname(qualname: str) -> tuple[str, str]: + names = qualname.split("::", 1) + if len(names) != 2: + raise ValueError( + f"Expected there to be a namespace in {qualname}, i.e. The " + f"operator name should look something like ns::foo" + ) + if "." in names[1]: + raise ValueError( + f"The torch.custom_ops APIs do not handle overloads, " + f"i.e. operator names with '.' in them. " + f"Please name your operator something like ns::foo. " + f"Got: {qualname}" + ) + return names[0], names[1] + + +def validate_device_type(device_type: str) -> None: + if device_type not in SUPPORTED_DEVICE_TYPE_TO_KEY: + raise ValueError( + f"CustomOp.impl(device_types=[{device_type}, ...]): we only support device_type " + f"in {SUPPORTED_DEVICE_TYPE_TO_KEY.keys()}." + ) + + +def supported_param(param: inspect.Parameter) -> bool: + return param.kind in ( + inspect.Parameter.POSITIONAL_OR_KEYWORD, + inspect.Parameter.KEYWORD_ONLY, + ) + + +def validate_function_matches_schema( + schema: FunctionSchema, func: typing.Callable +) -> None: + sig = inspect.signature(func) + + if not all(supported_param(p) for _, p in sig.parameters.items()): + raise ValueError( + f"custom_op(..., manual_schema)(func): positional-only args, " + f"varargs, and kwargs are not supported. Please rewrite `func` " + f"to not have them. Got `func` with signature: {sig}" + ) + + if ( + any( + p.annotation is not inspect.Parameter.empty + for _, p in sig.parameters.items() + ) + or sig.return_annotation is not inspect.Signature.empty + ): + raise ValueError( + f"custom_op(..., manual_schema)(func): When passing in a manual " + f"schema, we expect `func` to have no type annotations to avoid " + f"ambiguity. Got `func` with signature: {sig}" + ) + + positional = [ + (name, param) + for name, param in sig.parameters.items() + if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD + ] + kwargonly = [ + (name, param) + for name, param in sig.parameters.items() + if param.kind == inspect.Parameter.KEYWORD_ONLY + ] + + def error(): + raise ValueError( + f"custom_op(..., manual_schema)(func): When passing in a manual " + f"schema, we expect `func`'s signature to match `manual_schema` " + f"(aside from type annotations). " + f"func's signature: {sig}, manual_schema: {schema}" + ) + + def error_default_args(): + raise ValueError( + f"custom_op(..., manual_schema)(func): " + f"neither func nor manual_schema should have default " + f"arguments. Got " + f"func's signature: {sig}, manual_schema: {schema}" + ) + + def compare(sig_args, schema_args): + if len(sig_args) != len(schema_args): + error() + for (name, param), arg in zip(sig_args, schema_args): + if name != arg.name: + error() + if param.default is not inspect.Parameter.empty or arg.default is not None: + error_default_args() + + compare(positional, schema.arguments.flat_positional) + compare(kwargonly, schema.arguments.flat_kwarg_only) + + +def report_error_callback(custom_op: typing.Any, key: str) -> None: + if key == "Undefined": + raise NotImplementedError( + f"{custom_op}: There were no Tensor inputs to this operator " + f"(e.g. you passed an empty list of Tensors). If your operator is a " + f"factory function (that is, it takes no Tensors and constructs " + f"a new one), then please use CustomOp.impl_factory to register " + f"an implementation for it" + ) + if key == "Meta": + raise NotImplementedError( + f"{custom_op}: when running with device='Meta' tensors: there is no " + f"abstract impl registered for this CustomOp. Please register one via " + f"CustomOp.impl_abstract to get this CustomOp to work with Meta tensors" + ) + if key in ("CPU", "CUDA"): + device = key.lower() + raise NotImplementedError( + f"{custom_op}: when running with device='{device}' tensors: there is no " + f"{device} impl registered for this CustomOp. Please register one via " + f"CustomOp.impl(device_type='{device}')" + ) + raise NotImplementedError( + f"{custom_op}: No implementation for dispatch key {key}. It is likely " + f"that we have not added this functionality yet, please either open an " + f"issue or if you're feeling adventurous, use the low-level " + f"torch.library API" + ) + + +def custom_op_from_existing(op): + ns = op.namespace + lib = torch.library.Library(ns, "FRAGMENT") + name = op.name().split("::")[-1] + schema_str = str(op._schema) + # CustomOp expects the schema string without the namespace + schema_str = schema_str.rsplit("::", maxsplit=1)[-1] + schema = FunctionSchema.parse(schema_str) + return CustomOp(lib, ns, schema, name, op, _private_access=True) + + +def get_op(qualname): + def error_not_found(): + raise ValueError( + f"Could not find the operator {qualname}. Please make sure you have " + f"already registered the operator and (if registered from C++) " + f"loaded it via torch.ops.load_library." + ) + + ns, name = parse_qualname(qualname) + if not hasattr(torch.ops, ns): + error_not_found() + opnamespace = getattr(torch.ops, ns) + if not hasattr(opnamespace, name): + error_not_found() + packet = getattr(opnamespace, name) + if not hasattr(packet, "default"): + error_not_found() + return packet.default + + +def _find_custom_op(qualname, also_check_torch_library=False): + if qualname in global_registry: + return global_registry[qualname] + if not also_check_torch_library: + raise RuntimeError( + f'Could not find custom op "{qualname}". Did you register it via ' + f"the torch._custom_ops API?" + ) + overload = get_op(qualname) + result = custom_op_from_existing(overload) + return result + + +def get_abstract_impl(qualname): + if qualname not in torch._custom_op.impl.global_registry: + return None + custom_op = torch._custom_op.impl.global_registry[qualname] + if custom_op is None: + return None + if not custom_op._has_impl("abstract"): + return None + return custom_op._get_impl("abstract").func + + +def _custom_op_with_schema(qualname, schema, needs_fixed_stride_order=True): + ns, name = qualname.split("::") + schema_str = f"{name}{schema}" + function_schema = FunctionSchema.parse(schema_str) + validate_schema(function_schema) + tags = [torch._C.Tag.needs_fixed_stride_order] if needs_fixed_stride_order else [] + lib = library.Library(ns, "FRAGMENT") + lib.define(schema_str, tags=tags) + ophandle = find_ophandle_or_throw(ns, function_schema.name) + result = CustomOp(lib, ns, function_schema, name, ophandle, _private_access=True) + result._register_autograd_kernel_indirection() + + torch._C._dispatch_set_report_error_callback( + ophandle, functools.partial(report_error_callback, weakref.proxy(result)) + ) + return get_op(qualname) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_custom_ops.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_custom_ops.py new file mode 100644 index 0000000000000000000000000000000000000000..5203da640fa58e9ab7bf6785eb26ae1186059bee --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_custom_ops.py @@ -0,0 +1,326 @@ +# mypy: allow-untyped-defs +import inspect + +from torch._custom_op.impl import ( + _custom_op_with_schema, + _find_custom_op, + infer_schema, + parse_qualname, + validate_namespace, +) +from torch.library import get_ctx + + +__all__ = [ + "custom_op", + "impl", + "impl_abstract", + "get_ctx", + "impl_save_for_backward", + "impl_backward", +] + + +def custom_op(qualname, func_or_schema=None): + r"""Register a new custom operator + + In PyTorch, defining an op (short for "operator") is a two step-process: + - we need to define the op (by providing an operator name and schema) + - we need to implement behavior for how the operator interacts with + various PyTorch subsystems, like CPU/CUDA Tensors, Autograd, etc. + + This entrypoint defines the custom operator (the first step) + you must then perform the second step by calling various + ``impl_*`` APIs. + + This API may be used as a decorator (see examples). + + For a detailed guide on custom ops, please see + https://docs.google.com/document/d/1aGWtgxV3HppuxQAdddyPrs74_aEntpkYt9MalnCKnhk + + Arguments: + qualname (str): Should be a string that looks like + "namespace::operator_name". Operators in PyTorch need a namespace to + avoid name collisions; a given operator may only be created once. + If you are writing a Python library, we recommend the namespace to + be the name of your top-level module. + func_or_schema (Union[Callable, str]): Each PyTorch operator needs a + schema that tells PyTorch the types of the inputs/outputs. + If this is a Callable, we will automatically infer the schema from + the type annotations on the function (see examples). Otherwise, + if you don't want to use type annotations, you may provide us the + schema string. + + Example:: + + >>> # xdoctest: +REQUIRES(env:TORCH_DOCTEST_CUDA) + >>> import torch + >>> import numpy as np + >>> from torch import Tensor + >>> + >>> # Step 1: define the custom op. + >>> # We need to provide the API a "prototype function" + >>> # (a function that returns NotImplementedError), from which + >>> # we will infer the types of the inputs and outputs. + >>> @torch._custom_ops.custom_op("mylibrary::numpy_sin") + >>> def numpy_sin(x: Tensor) -> Tensor: + >>> raise NotImplementedError + >>> + >>> # The custom op is now accessible via the torch.ops module: + >>> torch.ops.mylibrary.numpy_sin + >>> + >>> # Step 2: Register an implementation for various PyTorch subsystems + >>> + >>> # Register an implementation for CPU tensors + >>> @torch._custom_ops.impl("mylibrary::numpy_sin", device_types="cpu") + >>> def numpy_sin_impl_cpu(x): + >>> return torch.from_numpy(np.sin(x.numpy())) + >>> + >>> # Register an implementation for CUDA tensors + >>> @torch._custom_ops.impl("mylibrary::numpy_sin", device_types="cuda") + >>> def numpy_sin_impl_cuda(x): + >>> return torch.from_numpy(np.sin(x.cpu().numpy())).to(x.device) + >>> + >>> x = torch.randn(3) + >>> torch.ops.mylibrary.numpy_sin(x) # calls numpy_sin_impl_cpu + >>> + >>> x_cuda = x.cuda() + >>> torch.ops.mylibrary.numpy_sin(x) # calls numpy_sin_impl_cuda + + """ + ns, name = parse_qualname(qualname) + validate_namespace(ns) + + def inner(func): + if not inspect.isfunction(func): + raise ValueError( + f"custom_op(...)(func): Expected `func` to be a Python " + f"function, got: {type(func)}" + ) + + if func.__name__ != name: + raise ValueError( + f"custom_op(qualname='{qualname}', ...)(func): expected `func` " + f"to have name '{name}' but got '{func.__name__}'. " + f"Please either change the name of `func` or the qualname that " + f"is passed to `custom_op`" + ) + + schema = infer_schema(func, mutates_args=()) + _custom_op_with_schema(qualname, schema) + return func + + if func_or_schema is None: + return inner + if isinstance(func_or_schema, str): + _custom_op_with_schema(qualname, func_or_schema) + else: + return inner(func_or_schema) + + +def impl(qualname, *, device_types=("cpu", "cuda"), func=None): + r"""Register an implementation for a device type for this custom op. + + If the op is passed multiple Tensor inputs with different device + types, it will dispatch to the registered implementation for the highest + priority device type among those present. + The supported device types, in order of priority, are {'cuda', 'cpu'}. + + This API may be used as a decorator (see examples). + + For a detailed guide on custom ops, please see + https://docs.google.com/document/d/1aGWtgxV3HppuxQAdddyPrs74_aEntpkYt9MalnCKnhk + + Arguments: + device_types (str or Iterable[str]): the device type(s) to register the function for. + + Example:: + + >>> # xdoctest: +REQUIRES(env:TORCH_DOCTEST_CUDA) + >>> import torch + >>> import numpy as np + >>> from torch import Tensor + >>> + >>> # Step 1: define the custom op. + >>> # We need to provide the API a "prototype function" + >>> # (a function that returns NotImplementedError), from which + >>> # we will infer the types of the inputs and outputs. + >>> @torch._custom_ops.custom_op("mylibrary::numpy_cos") + >>> def numpy_cos(x: Tensor) -> Tensor: + >>> raise NotImplementedError + >>> + >>> # The custom op is now accessible via the torch.ops module: + >>> torch.ops.mylibrary.numpy_cos + >>> + >>> # Step 2: Register an implementation for various PyTorch subsystems + >>> + >>> # Register an implementation for CPU tensors + >>> @torch._custom_ops.impl("mylibrary::numpy_cos", device_types="cpu") + >>> def numpy_cos_impl_cpu(x): + >>> return torch.from_numpy(np.cos(x.numpy())) + >>> + >>> # Register an implementation for CUDA tensors + >>> @torch._custom_ops.impl("mylibrary::numpy_cos", device_types="cuda") + >>> def numpy_cos_impl_cuda(x): + >>> return torch.from_numpy(np.cos(x.cpu().numpy())).to(x.device) + >>> + >>> x = torch.randn(3) + >>> torch.ops.mylibrary.numpy_cos(x) # calls numpy_cos_impl_cpu + >>> + >>> x_cuda = x.cuda() + >>> torch.ops.mylibrary.numpy_cos(x) # calls numpy_cos_impl_cuda + + """ + + def inner(func): + custom_op = _find_custom_op(qualname, also_check_torch_library=True) + custom_op.impl(device_types, _stacklevel=3)(func) + return func + + if func is None: + return inner + return inner(func) + + +def impl_abstract(qualname, *, func=None): + r"""Register an abstract implementation for this operator. + + An "abstract implementation" specifies the behavior of this operator on + Tensors that carry no data. Given some input Tensors with certain properties + (sizes/strides/storage_offset/device), it specifies what the properties of + the output Tensors are. + + The abstract implementation has the same signature as the operator. + It is run for both FakeTensors and meta tensors. To write an abstract + implementation, assume that all Tensor inputs to the operator are + regular CPU/CUDA/Meta tensors, but they do not have storage, and + you are trying to return regular CPU/CUDA/Meta tensor(s) as output. + The abstract implementation must consist of only PyTorch operations + (and may not directly access the storage or data of any input or + intermediate Tensors). + + This API may be used as a decorator (see examples). + + For a detailed guide on custom ops, please see + https://docs.google.com/document/d/1aGWtgxV3HppuxQAdddyPrs74_aEntpkYt9MalnCKnhk + + Examples:: + >>> import numpy as np + >>> from torch import Tensor + >>> + >>> # Example 1: an operator without data-dependent output shape + >>> @torch._custom_ops.custom_op("mylibrary::custom_linear") + >>> def custom_linear(x: Tensor, weight: Tensor, bias: Tensor) -> Tensor: + >>> raise NotImplementedError + >>> + >>> @torch._custom_ops.impl_abstract("mylibrary::custom_linear") + >>> def custom_linear_abstract(x, weight): + >>> assert x.dim() == 2 + >>> assert weight.dim() == 2 + >>> assert bias.dim() == 1 + >>> assert x.shape[1] == weight.shape[1] + >>> assert weight.shape[0] == bias.shape[0] + >>> assert x.device == weight.device + >>> + >>> return (x @ weight.t()) + bias + >>> + >>> # Example 2: an operator with data-dependent output shape + >>> @torch._custom_ops.custom_op('mylibrary::custom_nonzero') + >>> def custom_nonzero(x: Tensor) -> Tensor: + >>> ... + >>> + >>> @torch._custom_ops.impl_abstract("mylibrary::custom_nonzero") + >>> def custom_nonzero_abstract(x): + >>> # Number of nonzero-elements is data-dependent. + >>> # Since we cannot peek at the data in an abstract impl, + >>> # we use the ctx object to construct a new symint that + >>> # represents the data-dependent size. + >>> ctx = torch._custom_ops.get_ctx() + >>> nnz = ctx.create_unbacked_symint() + >>> shape = [x.dim(), nnz] + >>> result = x.new_empty(shape, dtype=torch.long) + >>> return result + >>> + >>> @torch._custom_ops.impl("mylibrary::custom_nonzero") + >>> def custom_nonzero_impl(x): + >>> x_np = to_numpy(x) + >>> res = np.stack(np.nonzero(x_np), axis=1) + >>> # unbacked symbolic ints in PyTorch must be >= 2, so we + >>> # constrain the range to at least 2 + >>> if res.shape[0] <= 1: + >>> raise RuntimeError("not supported") + >>> return torch.tensor(res, device=x.device) + + """ + import torch.library + + return torch.library.register_fake(qualname, func, _stacklevel=2) + + +def impl_save_for_backward(qualname, *, func=None): + r"""Register a function that tells us what to save for backward. + + Please see :func:`impl_backward` for more details. + """ + + def inner(func): + custom_op = _find_custom_op(qualname, also_check_torch_library=True) + custom_op.impl_save_for_backward(_stacklevel=3)(func) + return func + + if func is None: + return inner + return inner(func) + + +def impl_backward(qualname, output_differentiability=None, *, func=None): + r"""Registers a backward formula for an operator. + + In order for an operator to work with autograd, you need to register + a backward formula. There are two pieces to this: + 1. You must give us a function to specify what to save for backward. + Call this the "save for backward" function. + 2. You must give us a function that computes gradients. Call this the + "backward" function. + + Use `impl_save_for_backward` to define a "save for backward" function + that specifies what gets saved for backward. The function should accept + two arguments ``(inputs, output)`` and return the quantities to be saved + for backward. + + During runtime, when you call the operator in a forwards pass, PyTorch + will invoke the "save for backward" function with the inputs and output + of the operator. + + Use `impl_backward` to define the "backward" function. The backward + function must accept ``(ctx, saved, *grads)``: + - ``ctx`` is a context object where we may provide information + - ``saved`` is exactly what gets returned from the "save for backward" + function + - ``grads`` is one or more gradients. The number of gradients matches + the number of outputs of the operator. + + The backward function must return a dict that maps the name of + an input to the operator to its corresponding gradient. All inputs that + were declared to be Tensors in the operator definition must be accounted + for in the dict. The gradient may be a Tensor or None. + + For a detailed guide on custom ops, please see + https://docs.google.com/document/d/1aGWtgxV3HppuxQAdddyPrs74_aEntpkYt9MalnCKnhk + + """ + + def inner(func): + custom_op = _find_custom_op(qualname, also_check_torch_library=True) + custom_op.impl_backward(output_differentiability, _stacklevel=3)(func) + return func + + if func is None: + return inner + return inner(func) + + +def _destroy(qualname): + """De-registers a custom op. For testing purposes only""" + custom_op = _find_custom_op(qualname) + custom_op._destroy() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_decomp/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_decomp/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2553f9e60de96539654646303893943f3bf1c24b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_decomp/__init__.py @@ -0,0 +1,560 @@ +# mypy: allow-untyped-defs +import inspect +from collections import defaultdict +from collections.abc import Callable, Sequence +from functools import lru_cache, partial, wraps +from itertools import chain +from typing import Optional, TYPE_CHECKING, TypeVar, Union +from typing_extensions import ParamSpec + + +if TYPE_CHECKING: + from torch.export.decomp_utils import CustomDecompTable + +import torch +import torch.library +from torch._ops import HigherOrderOperator, OperatorBase, OpOverload, OpOverloadPacket +from torch._prims_common import CustomOutParamAnnotation +from torch._subclasses.functional_tensor import FunctionalTensor +from torch.utils import _pytree as pytree + + +__all__ = [ + "decomposition_table", + "pre_autograd_decomposition_table", + "meta_table", + "register_decomposition", + "get_decompositions", + "core_aten_decompositions", + "_should_decompose_because_unsafe_op", +] + +_T = TypeVar("_T") +_P = ParamSpec("_P") + +# TODO: relax key type here; torch registrations should be possible to; but +# right now this type is accurate +global_decomposition_table: dict[str, dict[torch._ops.OperatorBase, Callable]] = ( + defaultdict(dict) +) + +decomposition_table = global_decomposition_table["post_autograd"] +pre_autograd_decomposition_table = global_decomposition_table["pre_autograd"] +meta_table = global_decomposition_table["meta"] + + +def _should_decompose_because_unsafe_op(op: torch._ops.OperatorBase) -> bool: + """ + Returns True if the op must always decompose in export/compile tracing system + + In export, we always decompose certain CIA ops that are tagged with + maybe_aliasing_or_mutating because we statically need to know if the op is + mutating or not. But these CIA ops could have different behaviour in runtime. + + native_batch_norm is a prim op which has a wrong schema and it needs to be replaced + with correct schema. But until then, we will force decompose it via this tag. + """ + if not isinstance(op, torch._ops.OpOverload): + return False + if torch.Tag.maybe_aliasing_or_mutating in op.tags: + return True + return op is torch.ops.aten.native_batch_norm.default + + +def _add_op_to_registry(registry, op, fn): + """ + This is an internal API for adding an op to the decomposition table. + + If op is OpOverload, it will be added to the registry directly. + If op is OpOverloadPacket, all the valid op_overloads in the packet will be added to the registry. + """ + overloads: list[torch._ops.OperatorBase] = [] + if isinstance(op, HigherOrderOperator): + # There's no concept of overloads for HigherOrderOperator + registry[op] = fn + return + elif isinstance(op, OpOverload): + overloads.append(op) + else: + if not isinstance(op, OpOverloadPacket): + raise AssertionError(f"expected OpOverloadPacket, got {type(op)}") + for ol in op.overloads(): + overloads.append(getattr(op, ol)) + + for op_overload in overloads: + if op_overload in registry: + raise RuntimeError(f"duplicate registrations for {op_overload}") + # TorchScript dumps a bunch of extra nonsense overloads + # which don't have corresponding dispatcher entries, we need + # to filter those out, e.g aten.add.float_int + if torch._C._dispatch_has_kernel(op_overload.name()): + registry[op_overload] = fn + + +def _convert_out_params(f): + out_annotation = f.__annotations__.get("out") + + # If there are no out params, do not wrap the function. + if not out_annotation: + return f + + # Hack to detect when out is a Tuple. There seems to be no pretty way of doing this + if getattr(out_annotation, "__origin__", None) is tuple: + sig = inspect.signature(f) + out_names = sig.return_annotation._fields + # If out is a tuple, we need to register a function that unpacks all the out + # elements as this is what native_functions.yaml expects + + @wraps(f) + def _fn(*args, **kwargs): + out_kwargs = tuple(kwargs.pop(o, None) for o in out_names) + # Either all of the out kwargs are set or none of them + is_none = out_kwargs[0] is None + if not all((o is None) == is_none for o in out_kwargs): + raise AssertionError( + f"all out kwargs must be set or none of them, got {out_kwargs}" + ) + return f(*args, **kwargs, out=None if is_none else out_kwargs) + + out_params = [ + inspect.Parameter( + o, + kind=inspect.Parameter.KEYWORD_ONLY, + default=None, + annotation=t, + ) + for o, t in zip(out_names, out_annotation.__args__) + ] + # Drop the out parameter and concatenate the new kwargs in the signature + params = chain((v for k, v in sig.parameters.items() if k != "out"), out_params) + _fn.__signature__ = inspect.Signature( # type: ignore[attr-defined] + parameters=params, # type: ignore[arg-type] + return_annotation=sig.return_annotation, + ) + # Drop the out parameter and concatenate the new kwargs in the annotations + _fn.__annotations__ = {k: v for k, v in f.__annotations__.items() if k != "out"} + for o in out_params: + _fn.__annotations__[o.name] = o.annotation + + # Propagate that this function is wrapped by `out_wrapper` + _fn._torch_decompositions_out_wrapper = f._torch_decompositions_out_wrapper # type: ignore[attr-defined] + + return _fn + + # Alternatively, there may be a single tensor out parameter with a name + # other than "out". This will need special treatment and is indicated by an + # annotation, which we will remove here so it is not exposed after wrapping. + custom_out_param_name = f.__annotations__.pop(CustomOutParamAnnotation, None) + if custom_out_param_name: + + @wraps(f) + def _fn(*args, **kwargs): + out_kwarg = kwargs.pop(custom_out_param_name, None) + return f(*args, **kwargs, out=out_kwarg) + + out_param = inspect.Parameter( + custom_out_param_name, + kind=inspect.Parameter.KEYWORD_ONLY, + default=None, + annotation=out_annotation, + ) + + # Drop the out parameter and concatenate the new kwarg in the signature + sig = inspect.signature(f) + params = chain( + (v for k, v in sig.parameters.items() if k != "out"), (out_param,) + ) + _fn.__signature__ = inspect.Signature( # type: ignore[attr-defined] + parameters=params, # type: ignore[arg-type] + return_annotation=sig.return_annotation, + ) + + # Drop the out parameter and concatenate the new kwargs in the annotations + _fn.__annotations__ = {k: v for k, v in f.__annotations__.items() if k != "out"} + _fn.__annotations__[out_param.name] = out_param.annotation + + return _fn + + return f + + +def register_decomposition( + aten_op, registry=None, *, type="post_autograd", unsafe=False +) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]: + """ + A decorator to register a function as a decomposition to the Python + decomposition table. Use it like this:: + + @register_decomposition(torch.ops.aten.clamp_min) + def clamp_min(x): + return torch.clamp(self, min=min) + + If you are writing a new decomposition, consider contributing it + directly to PyTorch in torch._decomp.decompositions. + + This API is experimental; we are almost certainly going to extend + the API when we make decompositions eligible for use in transforms (e.g., + autograd) and not just backend tracing, where we then need to know if a + decomposition can be used to simulate a transform. + + By default, we also will register it to the Meta key of dispatcher, + and replace the c++ Meta implementation if there is already one. + + unsafe kwarg is for reuse of this function for registering non-function + things + """ + + if type not in {"post_autograd", "pre_autograd", "meta"}: + raise AssertionError( + f"type must be one of post_autograd, pre_autograd, or meta, got {type}" + ) + + def decomposition_decorator(fn: Callable[_P, _T]) -> Callable[_P, _T]: + orig_fn = fn + if not unsafe: + fn = _convert_out_params(fn) + + nonlocal registry + if registry is None: + registry = global_decomposition_table[type] + + def register(op): + _add_op_to_registry(registry, op, fn) + + # To handle allowing multiple aten_ops at once + pytree.tree_map_(register, aten_op) + return orig_fn + + return decomposition_decorator + + +def get_decompositions( + aten_ops: Sequence[torch._ops.OperatorBase | OpOverloadPacket], + type: str = "post_autograd", +) -> dict[torch._ops.OperatorBase, Callable]: + """ + Retrieve a dictionary of decompositions corresponding to the list of + operator overloads and overload packets passed as input. Overload + packets will include all decomposed overloads in the packet. If there is + no decomposition for a requested operator, it is silently ignored. + + This API is experimental; we are almost certainly going to give an alternate, + more recommended formulation, where a user provides the set of operators + they know how to implement, and we provide decompositions for everything + not in this set. + """ + if type not in {"post_autograd", "pre_autograd", "meta"}: + raise AssertionError( + f"type must be one of post_autograd, pre_autograd, or meta, got {type}" + ) + + registry = global_decomposition_table[type] + packets_to_overloads = defaultdict(list) + + for opo in registry: + if isinstance(opo, (OpOverload, OpOverloadPacket)): + packets_to_overloads[opo.overloadpacket].append(opo) + decompositions: dict[torch._ops.OperatorBase, Callable] = {} + for op in aten_ops: + if isinstance(op, OpOverloadPacket) and op in packets_to_overloads: + for op_overload in packets_to_overloads[op]: + decompositions[op_overload] = registry[op_overload] + elif isinstance(op, (torch._ops.OperatorBase)) and op in registry: + decompositions[op] = registry[op] + return decompositions + + +def remove_decompositions( + decompositions: dict[torch._ops.OperatorBase, Callable], + aten_ops: Sequence[OpOverload | OpOverloadPacket], +) -> None: + """ + Given a dictionary of decompositions obtained from get_decompositions(), removes + operators associated with a list of operator overloads and overload packets passed + as input. If the decomposition dictionary does not contain a decomposition that is + specified to be removed, it is silently ignored. + """ + for op in aten_ops: + if isinstance(op, OpOverloadPacket): + for overload_name in op.overloads(): + opo = getattr(op, overload_name) + decompositions.pop(opo, None) + elif isinstance(op, OpOverload): + decompositions.pop(op, None) + + +# populate the table +import torch._decomp.decompositions +import torch._refs + + +def core_aten_decompositions() -> "CustomDecompTable": + from torch.export.exported_program import default_decompositions + + return default_decompositions() + + +# See NOTE [Core ATen Ops] +# +# list was copied from torch/_inductor/decomposition.py +# excluding decompositions that results in prim ops +# Resulting opset of decomposition is core aten ops +def _core_aten_decompositions_post_autograd() -> dict[ + torch._ops.OperatorBase, Callable +]: + aten = torch.ops.aten + return get_decompositions( + [ + aten.addcdiv, + aten.addcdiv_, + aten.addcmul, + aten.addcmul_, + aten.addr, + aten.affine_grid_generator, + aten.alias_copy, + aten.all, + aten.aminmax, + aten.arange.default, + aten.arange.start, + aten.avg_pool2d_backward, + aten.baddbmm, + aten.binary_cross_entropy, + aten.binary_cross_entropy_backward, + aten.binary_cross_entropy_with_logits, + aten.block_diag, + aten.bernoulli.p, + aten.bernoulli.default, + aten.celu, + aten.celu_, + aten.channel_shuffle, + aten.clamp_max, + aten.clamp_min, + aten.col2im, + aten.count_nonzero, + aten.linalg_cross, + aten.cudnn_batch_norm, + aten.cudnn_batch_norm_backward, + aten.miopen_batch_norm_backward, + aten.deg2rad, + aten.deg2rad_, + aten.detach, + aten.diag_embed, + aten.diagonal_backward, + aten.diagonal_copy, + aten.dot, + aten.vdot, + aten.elu_, + aten.elu_backward, + aten._embedding_bag, + aten.embedding_dense_backward, + aten.empty_like, + aten._euclidean_dist.default, + aten.expand_as, + aten.expand_copy, + aten.eye, + aten.fill, + aten.fill_, + aten.floor_divide, + aten.frac, + aten.frac_, + aten._fused_moving_avg_obs_fq_helper, + aten.gelu_, + aten.gelu_backward, + aten.glu, + aten.glu_backward, + aten.hardshrink, + aten.hardsigmoid, + aten.hardsigmoid_, + aten.hardsigmoid_backward, + aten.hardswish, + aten.hardswish_, + aten.hardswish_backward, + aten.hardtanh_, + aten.hardtanh_backward, + aten.hann_window, + aten.heaviside, + aten.heaviside_, + aten.huber_loss, + aten.huber_loss_backward, + aten.im2col, + aten.index_add.out, + aten.index_add.default, + aten.index_add_, + aten.index_copy.out, + aten.index_copy.default, + aten.index_copy_, + aten.index_fill.int_Scalar, + aten.index_fill.int_Tensor, + aten.index_fill.int_Scalar_out, + aten.index_fill.int_Tensor_out, + aten.index_fill_, + aten.isin, + aten.isneginf, + aten.isposinf, + aten.l1_loss, + aten._lazy_clone, + aten._test_parallel_materialize, + aten.leaky_relu_, + aten.leaky_relu_backward, + aten.lerp, + aten.lerp_, + aten.linspace, + aten.logaddexp, + aten.logaddexp2, + aten.logit, + aten.logit_, + aten.logit_backward, + aten.log_sigmoid_backward, + aten.log_sigmoid_forward, + aten._log_softmax_backward_data, + aten.logspace, + aten.logsumexp.default, + aten.masked_fill, + aten.masked_fill_, + aten.max_unpool2d, + aten.max_unpool3d, + aten.mish, + aten.mish_, + aten.mish_backward, + aten.mse_loss, + aten.mse_loss_backward, + aten.multi_margin_loss, + aten.multilabel_margin_loss_forward, + aten.mv, + aten.mvlgamma, + aten.mvlgamma_, + aten.nansum, + aten.nan_to_num, + aten.nan_to_num_, + aten.narrow, + aten.native_batch_norm_backward, + aten.native_dropout_backward, + aten.native_group_norm_backward, + aten.native_layer_norm_backward, + aten._fused_rms_norm, + aten._fused_rms_norm_backward, + aten.new_empty, + aten.new_full, + aten.new_ones, + aten.new_zeros, + aten.nll_loss2d_forward, + aten.nll_loss2d_backward, + aten.nll_loss_backward, + aten.nll_loss_forward, + aten.norm.ScalarOpt_dtype, + aten.norm.Scalar, + aten.norm.ScalarOpt_dim_dtype, + aten.norm.ScalarOpt_dim, + aten.norm.dtype_out, + aten.norm.out, + aten.norm.names_dtype_out, + aten.norm.names_out, + aten.norm.ScalarOpt_dtype_out, + aten.norm.Scalar_out, + aten.ones, + aten.ones_like, + aten.pixel_shuffle, + aten.pixel_unshuffle, + aten._prelu_kernel, + aten._prelu_kernel_backward, + aten._reshape_alias, + aten.rad2deg, + aten.rad2deg_, + aten.reflection_pad1d, + aten.reflection_pad1d_backward, + aten.reflection_pad2d, + aten.reflection_pad2d_backward, + aten.reflection_pad3d, + aten.reflection_pad3d_backward, + aten.replication_pad1d, + aten.replication_pad2d, + aten.replication_pad3d, + aten.renorm, + aten.renorm_, + aten.replication_pad2d, + aten.resize_as, + aten.roll, + aten.rot90, + aten.rrelu_with_noise, + aten.rrelu_with_noise_, + aten.rsub, + aten._safe_softmax, + aten._scaled_dot_product_flash_attention_for_cpu.default, + aten.select_backward, + aten.select_scatter, + aten.sgn, + aten.sgn_, + aten.sigmoid_backward, + aten.silu, + aten.silu_, + aten.silu_backward.grad_input, + aten.silu_backward, + aten.sinc, + aten.sinc_, + aten.slice_backward, + aten.smooth_l1_loss, + aten.smooth_l1_loss_backward, + aten.soft_margin_loss, + aten.soft_margin_loss_backward, + aten._softmax_backward_data, + aten.softplus, + aten.softplus_backward, + aten.softshrink, + aten.special_entr, + aten.special_log_ndtr, + aten.special_xlog1py, + aten.split.Tensor, + aten.split_with_sizes_copy, + aten.squeeze_copy, + aten.squeeze.default, + aten.squeeze.dim, + aten.std.correction, + aten.std.out, + aten.std.correction_out, + aten.std.names_out, + aten.std.correction_names_out, + aten.std_mean.correction, + aten.std_mean.correction_out, + aten.stack, + aten.sum.default, + aten.sum.out, + aten.t, + aten.t_copy, + aten.take, + aten.tanh_backward, + aten.threshold, + aten.threshold_, + aten.threshold_backward, + aten.trace, + aten.transpose.int, + aten.transpose_copy, + aten.tril, + aten.tril_, + aten.triu, + aten.triu_, + aten.unbind, + aten.unfold_backward, + aten.unfold_copy, + aten._unsafe_index, + aten._unsafe_index_put, + aten._unsafe_masked_index, + aten._unsafe_masked_index_put_accumulate, + aten.unsafe_split.Tensor, + aten.unsafe_split_with_sizes, + aten.unsqueeze_copy, + aten._unsafe_view, + aten.upsample_linear1d, + aten.upsample_bilinear2d.out, + aten.upsample_trilinear3d.out, + aten.upsample_nearest2d_backward, + aten.view_as_complex, + aten.xlogy, + aten.xlogy_, + aten.zero, + aten.zero_, + aten.zeros, + aten.zeros_like, + aten._chunk_cat, + aten._weight_norm_interface, + ] + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_decomp/decompositions.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_decomp/decompositions.py new file mode 100644 index 0000000000000000000000000000000000000000..4eb171d50ec9391606a494cace2beb83afff470a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_decomp/decompositions.py @@ -0,0 +1,5746 @@ +# mypy: allow-untyped-decorators +# mypy: allow-untyped-defs +import functools +import itertools +import numbers +import operator +import sys +from collections.abc import Callable, Iterable +from contextlib import nullcontext +from enum import Enum +from functools import partial, reduce +from itertools import chain, product +from typing import Any, cast + +import torch +import torch._meta_registrations +import torch._prims as prims +import torch._prims_common as utils +import torch.nn.functional as F +from torch import sym_float, sym_int, Tensor +from torch._decomp import register_decomposition +from torch._higher_order_ops.out_dtype import out_dtype +from torch._prims_common import ( + IntLike, + NumberType, + suggest_memory_format, + TensorLike, + TensorSequenceType, +) +from torch._prims_common.wrappers import ( + _maybe_convert_to_dtype, + _maybe_resize_out, + _safe_copy_out, + out_wrapper, +) +from torch.utils import _pytree as pytree +from torch.utils._pytree import tree_map + + +DispatchKey = torch._C.DispatchKey # type: ignore[attr-defined] + +# None of these functions are publicly accessible; get at them +# from torch._decomps +__all__: list[str] = [] + +aten = torch._ops.ops.aten + + +class Reduction(Enum): + NONE = 0 + MEAN = 1 + SUM = 2 + + +# This wraps a decomposition and performs various type promotion logic within it, depending on the strategy provided +# We're currently reusing ELEMENTWISE_TYPE_PROMOTION_KIND, although some of the usages are on non-elementwise ops +# Will need to validate the non-elementwise uses +def type_casts( + f: Callable, + type_promotion: utils.ELEMENTWISE_TYPE_PROMOTION_KIND, + compute_dtype_only: bool = False, + include_non_tensor_args: bool = False, +): + @functools.wraps(f) + def inner(*args, **kwargs): + allowed_types = ( + (Tensor, torch.types._Number) if include_non_tensor_args else (Tensor,) + ) # type: ignore[arg-type] + flat_args = [ + x + for x in pytree.arg_tree_leaves(*args, **kwargs) + if isinstance(x, allowed_types) + ] + computation_dtype, result_dtype = utils.elementwise_dtypes( + *flat_args, type_promotion_kind=type_promotion + ) + + # TODO: pretty sure this is not quite right + def increase_prec(x): + if isinstance(x, Tensor): + return x.to(computation_dtype) + else: + return x + + def decrease_prec(x): + if isinstance(x, Tensor): + return x.to(result_dtype) + else: + return x + + r = f(*tree_map(increase_prec, args), **tree_map(increase_prec, kwargs)) + if compute_dtype_only: + return r + else: + return tree_map(decrease_prec, r) + + return inner + + +compute_only_pw_cast_for_opmath = partial( + type_casts, + type_promotion=utils.ELEMENTWISE_TYPE_PROMOTION_KIND.DEFAULT, + compute_dtype_only=True, +) +pw_cast_for_opmath = partial( + type_casts, type_promotion=utils.ELEMENTWISE_TYPE_PROMOTION_KIND.DEFAULT +) +pw_cast_for_opmath_non_tensor_args = partial( + type_casts, + type_promotion=utils.ELEMENTWISE_TYPE_PROMOTION_KIND.DEFAULT, + include_non_tensor_args=True, +) +pw_cast_for_int_to_real = partial( + type_casts, type_promotion=utils.ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT +) + + +# This expands x until x.dim() == dim. Might be useful as an operator +def _unsqueeze_to_dim(x: Tensor, dim: int) -> Tensor: + for _ in range(dim - x.dim()): + x = x.unsqueeze(-1) + return x + + +@register_decomposition(aten.tanh_backward) +@out_wrapper("grad_input") +@pw_cast_for_opmath +def tanh_backward(out_grad: Tensor, y: Tensor): + return out_grad * (1 - y * y).conj_physical() + + +@register_decomposition(aten.sigmoid_backward) +@out_wrapper("grad_input") +@pw_cast_for_opmath +def sigmoid_backward(out_grad: Tensor, y: Tensor): + return out_grad * (y * (1 - y)).conj_physical() + + +@register_decomposition(aten.softplus_backward) +@out_wrapper("grad_input") +@pw_cast_for_opmath +def softplus_backward(out_grad: Tensor, x: Tensor, beta: float, threshold: float): + z = (x * beta).exp() + return torch.where((x * beta) > threshold, out_grad, out_grad * z / (z + 1.0)) + + +@register_decomposition(aten.elu_backward) +@out_wrapper("grad_input") +@pw_cast_for_opmath +def elu_backward( + grad_output: Tensor, + alpha: float, + scale: float, + input_scale: float, + is_result: bool, + self_or_result: Tensor, +): + negcoef = alpha * scale + poscoef = scale + negiptcoef = input_scale + if is_result: + return torch.where( + self_or_result <= 0, + grad_output * negiptcoef * (self_or_result + negcoef), + grad_output * poscoef, + ) + else: + return torch.where( + self_or_result <= 0, + grad_output * negiptcoef * negcoef * torch.exp(self_or_result * negiptcoef), + grad_output * poscoef, + ) + + +@register_decomposition([aten.fill.Scalar]) +def fill_scalar(self, value): + return torch.full_like(self, value) + + +@register_decomposition([aten.fill.Tensor]) +def fill_tensor(self, value: Tensor): + torch._check( + value.dim() == 0, + lambda: f"fill only supports 0-dimension value tensor but got tensor with {value.dim()} dimensions", + ) + return aten.copy(self, value) + + +@register_decomposition(aten.hardsigmoid) +@out_wrapper() +@pw_cast_for_opmath +def hardsigmoid(self: Tensor) -> Tensor: + return torch.clamp(torch.clamp(self + 3, min=0), max=6) / 6 + + +@register_decomposition(aten.hardsigmoid_backward) +@out_wrapper("grad_input") +@pw_cast_for_opmath +def hardsigmoid_backward(grad_output: Tensor, self: Tensor): + return torch.where( + (self > -3.0) & (self < 3.0), + grad_output * (1.0 / 6.0), + 0.0, + ) + + +@register_decomposition(aten.hardtanh_backward) +@out_wrapper("grad_input") +def hardtanh_backward( + grad_output: Tensor, self: Tensor, min_val: float, max_val: float +): + return torch.where((self <= min_val) | (self >= max_val), 0.0, grad_output) + + +@register_decomposition(aten.hardswish) +@out_wrapper() +@pw_cast_for_opmath +def hardswish(self: Tensor) -> Tensor: + return self * torch.clamp(torch.clamp(self + 3, min=0), max=6) / 6 + + +@register_decomposition(aten.hardswish_backward) +@out_wrapper() +@pw_cast_for_opmath +def hardswish_backward(grad_output: Tensor, self: Tensor) -> Tensor: + return torch.where( + self <= -3, + 0.0, + torch.where(self < 3, grad_output * ((self / 3) + 0.5), grad_output), + ) + + +@register_decomposition(aten.threshold_backward) +@out_wrapper("grad_input") +def threshold_backward(grad_output: Tensor, self: Tensor, threshold: float): + return torch.where(self <= threshold, 0, grad_output) + + +@register_decomposition(aten.leaky_relu_backward) +@out_wrapper("grad_input") +@pw_cast_for_opmath +def leaky_relu_backward( + grad_output: Tensor, self: Tensor, negative_slope: float, self_is_result: bool +): + return torch.where(self > 0, grad_output, grad_output * negative_slope) + + +@register_decomposition(aten.gelu_backward) +@out_wrapper("grad_input") +@pw_cast_for_opmath +def gelu_backward(grad: Tensor, self: Tensor, approximate: str = "none"): + M_SQRT2 = 1.41421356237309504880 + M_SQRT1_2 = 0.70710678118654752440 + M_2_SQRTPI = 1.12837916709551257390 + if approximate == "tanh": + kBeta = M_SQRT2 * M_2_SQRTPI * 0.5 + kKappa = 0.044715 + x_sq = self * self + x_cube = x_sq * self + inner = kBeta * (self + kKappa * x_cube) + tanh_inner = torch.tanh(inner) + + left = 0.5 * self + right = 1 + tanh_inner + + left_derivative = 0.5 * right + + tanh_derivative = 1 - tanh_inner * tanh_inner + inner_derivative = kBeta * (1 + 3 * kKappa * x_sq) + right_derivative = left * tanh_derivative * inner_derivative + + return grad * (left_derivative + right_derivative) + else: + kAlpha = M_SQRT1_2 + kBeta = M_2_SQRTPI * M_SQRT1_2 * 0.5 + cdf = 0.5 * (1 + torch.erf(self * kAlpha)) + pdf = kBeta * torch.exp(self * self * -0.5) + return grad * (cdf + self * pdf) + + +@register_decomposition(aten.mish_backward) +@pw_cast_for_opmath +def mish_backward(grad_output: Tensor, input: Tensor): + input_tanh_softplus = torch.tanh(F.softplus(input)) + input_sigmoid = torch.sigmoid(input) + out = input * input_sigmoid * (1 - input_tanh_softplus * input_tanh_softplus) + return grad_output * (input_tanh_softplus + out) + + +@register_decomposition(aten.silu) +@out_wrapper() +@pw_cast_for_opmath +def silu(self: Tensor) -> Tensor: + return self * torch.sigmoid(self) + + +@register_decomposition(aten.silu_backward) +@out_wrapper("grad_input") +@pw_cast_for_opmath +def silu_backward(grad_output: Tensor, self: Tensor) -> Tensor: + sigmoid = 1 / (1 + torch.exp(-self)) + return grad_output * sigmoid * (1 + self * (1 - sigmoid)) + + +@register_decomposition(aten._prelu_kernel) +def _prelu_kernel(self: Tensor, weight: Tensor) -> Tensor: + return torch.where(self > 0, self, weight * self) + + +@register_decomposition(aten._prelu_kernel_backward) +def _prelu_kernel_backward( + grad_output: Tensor, + self: Tensor, + weight: Tensor, +) -> tuple[Tensor, Tensor]: + input_grad = torch.where(self > 0, grad_output, weight * grad_output) + weight_grad = torch.where(self > 0, 0.0, self * grad_output) + return (input_grad, weight_grad) + + +@register_decomposition(aten.rrelu_with_noise_backward) +@out_wrapper() +@pw_cast_for_opmath +def rrelu_with_noise_backward( + grad_output: Tensor, + self: Tensor, + noise: Tensor, + lower: float, + upper: float, + training: bool, + self_is_result: bool, +) -> Tensor: + if training: + return grad_output.mul(noise) + else: + negative_slope = (lower + upper) / 2 + return aten.leaky_relu_backward( + grad_output, self, negative_slope, self_is_result + ) + + +@register_decomposition(aten.log_sigmoid_backward) +@out_wrapper("grad_input") +@pw_cast_for_opmath +def log_sigmoid_backward(grad_output: Tensor, self: Tensor, buffer: Tensor) -> Tensor: + in_negative = self < 0 + max_deriv = torch.where(in_negative, 1, 0) + sign = torch.where(in_negative, 1, -1) + # On CPU, forward saves buffer = exp(-|x|); reuse it to match the native kernel. + # On CUDA/XPU, buffer is empty so we recompute. + z = buffer if buffer.numel() > 0 else torch.exp(-torch.abs(self)) + return grad_output * (max_deriv - sign * (z / (1 + z))) + + +@register_decomposition(aten.ldexp) +@out_wrapper() +def ldexp(self: Tensor, other: Tensor) -> Tensor: + two_dtype = ( + torch.float32 + if utils.is_integer_dtype(self.dtype) or utils.is_boolean_dtype(self.dtype) + else self.dtype + ) + two_tensor = self.new_full((), 2.0, dtype=two_dtype) + return self * torch.pow(two_tensor, other) + + +def apply_loss_reduction(loss: Tensor, reduction: int): + if reduction == Reduction.MEAN.value: + return torch.mean(loss) + elif reduction == Reduction.SUM.value: + return torch.sum(loss) + else: + return loss + + +def to_real_dtype(dtype: torch.dtype): + if dtype == torch.complex32: + return torch.float16 + elif dtype == torch.complex64: + return torch.float32 + elif dtype == torch.complex128: + return torch.float64 + + +# TODO: None of these loss castings are quite correct, see +# https://github.com/pytorch/pytorch/issues/76870. Also, the ATen kernels +# perform the pointwise portion in opmath, but don't maintain it between the +# pointwise portion and the reduction + + +@register_decomposition(aten.mse_loss) +@out_wrapper() +@pw_cast_for_opmath +def mse_loss( + self: Tensor, target: Tensor, reduction: int = Reduction.MEAN.value +) -> Tensor: + loss = (self - target) ** 2 + return apply_loss_reduction(loss, reduction) + + +@register_decomposition(aten.mse_loss_backward) +@out_wrapper("grad_input") +@pw_cast_for_opmath +def mse_loss_backward( + grad_output: Tensor, input: Tensor, target: Tensor, reduction: int +): + norm = 2.0 / input.numel() if reduction == Reduction.MEAN.value else 2.0 + return norm * (input - target) * grad_output + + +@register_decomposition(aten._safe_softmax) +def safe_softmax(self, dim, dtype=None): + out = torch.softmax(self, dim=dim, dtype=dtype) + masked = self.eq(float("-inf")) + masked_rows = torch.all(masked, dim=dim, keepdim=True) + zeros = torch.zeros_like(out) + return torch.where(masked_rows, zeros, out) + + +@register_decomposition(aten.smooth_l1_loss) +@out_wrapper() +@pw_cast_for_opmath +def smooth_l1_loss( + self: Tensor, + target: Tensor, + reduction: int = Reduction.MEAN.value, + beta: float = 1.0, +): + loss = (self - target).abs() + + loss = torch.where(loss < beta, 0.5 * loss**2 / beta, loss - 0.5 * beta) + return apply_loss_reduction(loss, reduction) + + +@register_decomposition(aten.smooth_l1_loss_backward.default) +@pw_cast_for_opmath +def smooth_l1_loss_backward( + grad_output: Tensor, self: Tensor, target: Tensor, reduction: int, beta: float +): + norm = 1.0 / self.numel() if reduction == Reduction.MEAN.value else 1.0 + x = self - target + abs_x = torch.abs(x) + norm_grad = norm * grad_output + return torch.where( + abs_x < beta, + norm_grad * x / beta, + norm_grad * torch.sign(x), + ) + + +@register_decomposition(aten.smooth_l1_loss_backward.grad_input) +@pw_cast_for_opmath +def smooth_l1_loss_backward_out( + grad_output: Tensor, + self: Tensor, + target: Tensor, + reduction: int, + beta: float, + grad_input: Tensor, +): + result = smooth_l1_loss_backward(grad_output, self, target, reduction, beta) + _maybe_resize_out(grad_input, result.shape) + return _safe_copy_out(copy_from=result, copy_to=grad_input, exact_dtype=True) + + +@register_decomposition(aten.huber_loss_backward.default) +@pw_cast_for_opmath +def huber_loss_backward( + grad_output: Tensor, self: Tensor, target: Tensor, reduction: int, delta: float +): + norm = 1.0 / self.numel() if reduction == Reduction.MEAN.value else 1.0 + x = self - target + return torch.where( + x < -delta, + -norm * grad_output * delta, + torch.where(x > delta, norm * grad_output * delta, norm * x * grad_output), + ) + + +# We cannot use @out_wrapper() here, because the output tensor is not named 'out', it's 'grad_input' +@register_decomposition(aten.huber_loss_backward.out) +@pw_cast_for_opmath +def huber_loss_backward_out( + grad_output: Tensor, + self: Tensor, + target: Tensor, + reduction: int, + delta: float, + grad_input: Tensor, +): + result = huber_loss_backward(grad_output, self, target, reduction, delta) + _maybe_resize_out(grad_input, result.shape) + return _safe_copy_out(copy_from=result, copy_to=grad_input, exact_dtype=True) + + +def _nll_loss_backward( + grad_output: Tensor, + self: Tensor, + target: Tensor, + weight: Tensor | None, + reduction: int, + ignore_index: int, + total_weight: Tensor, +) -> Tensor: + channel_dim = 0 if self.dim() < 2 else 1 + if reduction == Reduction.MEAN.value: + grad_output = grad_output / total_weight + + # When self is 1D (no batch dim), the C++ kernel only uses target[0]. + # Reduce target to a scalar so the subsequent unsqueeze produces a 1D + # tensor matching self's dimensionality. + if self.dim() == 1 and target.dim() > 0: + target = target[0] + + target = target.unsqueeze(channel_dim) + safe_target = torch.where(target != ignore_index, target, 0) + grad_input = torch.zeros_like(self) + grad_input = torch.scatter(grad_input, channel_dim, safe_target, -1.0) + + if grad_input.dim() > grad_output.dim() > 0: + grad_output = grad_output.unsqueeze(channel_dim) + + if weight is not None: + new_shape = [1 for _ in range(self.dim())] + new_shape[channel_dim] = weight.shape[0] + weight = weight.reshape(new_shape) + grad_output = grad_output * weight + + grad_output = torch.where(target != ignore_index, grad_output, 0) + + return grad_input * grad_output + + +@register_decomposition(aten.glu_backward) +@out_wrapper("grad_input") +@pw_cast_for_opmath +def glu_backward(grad_output: Tensor, self: Tensor, dim: int) -> Tensor: + if self.dim() <= 0: + raise AssertionError("glu does not support 0-dimensional tensors") + wrap_dim = utils.canonicalize_dim(self.dim(), dim) + nIn = self.size(wrap_dim) + if nIn % 2 != 0: + raise AssertionError( + f"Halving dimension must be even, but dimension {wrap_dim} is size {nIn}" + ) + inputSize = nIn // 2 + firstHalf = self.narrow(wrap_dim, 0, inputSize) + secondHalf = self.narrow(wrap_dim, inputSize, inputSize) + gradInputFirstHalf = torch.sigmoid(secondHalf) + gradInputSecondHalf = ( + (1.0 - gradInputFirstHalf) * gradInputFirstHalf * firstHalf * grad_output + ) + gradInputFirstHalf = gradInputFirstHalf * grad_output + return torch.cat([gradInputFirstHalf, gradInputSecondHalf], dim=wrap_dim) + + +@register_decomposition(aten.nll_loss_backward) +@out_wrapper("grad_input") +def nll_loss_backward( + grad_output: Tensor, + self: Tensor, + target: Tensor, + weight: Tensor | None, + reduction: int, + ignore_index: int, + total_weight: Tensor, +) -> Tensor: + if not (0 <= self.dim() <= 2): + raise AssertionError(f"input tensor should be 1D or 2D, got {self.dim()}D") + if target.dim() > 1: + raise AssertionError( + f"0D or 1D target tensor expected, multi-target not supported, got {target.dim()}D" + ) + + no_batch_dim = self.dim() == 1 and target.dim() == 0 + if not (no_batch_dim or (self.shape[0] == target.shape[0])): + raise AssertionError( + f"size mismatch (got input: {self.shape}, target: {target.shape})" + ) + if total_weight.numel() != 1: + raise AssertionError( + f"expected total_weight to be a single element tensor, got: " + f"{total_weight.shape} ({total_weight.numel()} elements)" + ) + + if weight is not None and weight.numel() != self.shape[-1]: + raise AssertionError( + "weight tensor should be defined either for all or no classes" + ) + + if reduction == Reduction.NONE.value and self.dim() == 2: + if not (grad_output.dim() == 1 and grad_output.shape[0] == self.shape[0]): + raise AssertionError( + f"Expected a tensor of dimension 1 and tensor.size[0] == {self.shape[0]} but " + f"got: dimension {grad_output.dim()} and tensor.size[0] == {grad_output.shape[0]}" + ) + else: + if not (grad_output.dim() <= 1 and grad_output.numel() == 1): + raise AssertionError( + f"Expected a single element grad_output tensor, but got: {grad_output.shape}" + ) + + return _nll_loss_backward( + grad_output, self, target, weight, reduction, ignore_index, total_weight + ) + + +@register_decomposition(aten.nll_loss2d_backward) +@out_wrapper("grad_input") +def nll_loss2d_backward( + grad_output: Tensor, + self: Tensor, + target: Tensor, + weight: Tensor | None, + reduction: int, + ignore_index: int, + total_weight: Tensor, +) -> Tensor: + if self.dim() != 4: + raise AssertionError( + f"only batches of spatial inputs supported (4D tensors), but got input of dimension: {self.dim()}" + ) + + if target.dim() != 3: + raise AssertionError( + f"only batches of spatial targets supported (3D tensors) but got targets of dimension: {target.dim()}" + ) + + if not ( + self.shape[0] == target.shape[0] + and self.shape[2] == target.shape[1] + and self.shape[3] == target.shape[2] + ): + raise AssertionError( + f"size mismatch (got input: {self.shape}, target: {target.shape}" + ) + + if total_weight.numel() != 1: + raise AssertionError( + f"expected total_weight to be a single element tensor, " + f"got: {total_weight.shape} ( {total_weight.numel()}, elements)" + ) + + return _nll_loss_backward( + grad_output, self, target, weight, reduction, ignore_index, total_weight + ) + + +@register_decomposition(aten.binary_cross_entropy) +@out_wrapper() +@pw_cast_for_opmath +def binary_cross_entropy( + self: Tensor, + target: Tensor, + weight: Tensor | None = None, + reduction: int = Reduction.MEAN.value, +) -> Tensor: + # We cannot currently model this without introducing data-dependent control flow + # TORCH_CHECK( + # (input_val >= 0) && (input_val <= 1), + # "all elements of input should be between 0 and 1" + # ) + loss = (target - 1) * torch.maximum( + torch.log1p(-self), self.new_full((), -100) + ) - target * torch.maximum(torch.log(self), self.new_full((), -100)) + if weight is not None: + loss = loss * weight + return apply_loss_reduction(loss, reduction) + + +@register_decomposition(aten.binary_cross_entropy_backward) +@out_wrapper("grad_input") +@pw_cast_for_opmath +def binary_cross_entropy_backward( + grad_output: Tensor, + self: Tensor, + target: Tensor, + weight: Tensor | None = None, + reduction: int = Reduction.MEAN.value, +) -> Tensor: + EPSILON = 1e-12 + result = grad_output * (self - target) / torch.clamp(self * (1 - self), min=EPSILON) + if weight is not None: + result = result * weight + if reduction == Reduction.MEAN.value: + result = result / self.numel() + return result + + +@register_decomposition(aten.soft_margin_loss) +@out_wrapper() +@pw_cast_for_opmath +def soft_margin_loss( + input: Tensor, + target: Tensor, + reduction: int = Reduction.MEAN.value, +) -> Tensor: + loss = torch.log1p(torch.exp(-input * target)) + return apply_loss_reduction(loss, reduction) + + +@register_decomposition(aten.soft_margin_loss_backward) +@out_wrapper("grad_input") +@pw_cast_for_opmath +def soft_margin_loss_backward( + grad_output: Tensor, + self: Tensor, + target: Tensor, + reduction: int = Reduction.MEAN.value, +) -> Tensor: + grad_input = target * grad_output * (torch.sigmoid(target * self) - 1) + if reduction == Reduction.MEAN.value: + grad_input = grad_input / self.numel() + return grad_input + + +@register_decomposition(aten.dist) +@out_wrapper() +def dist(input: Tensor, other: Tensor, p: float = 2): + return aten.norm(input - other, p=p) + + +@register_decomposition(aten._euclidean_dist) +@out_wrapper() +def _euclidean_dist(x1: Tensor, x2: Tensor) -> Tensor: + x1_norm = x1.pow(2).sum(-1, True) + x1_pad = torch.ones_like(x1_norm, memory_format=torch.contiguous_format) + x2_norm = x2.pow(2).sum(-1, True) + x2_pad = torch.ones_like(x2_norm, memory_format=torch.contiguous_format) + x1_ = torch.cat([x1.mul(-2), x1_norm, x1_pad], -1) + x2_ = torch.cat([x2, x2_pad, x2_norm], -1) + result = x1_.matmul(x2_.mT) + return result.clamp_min(0).sqrt() + + +@register_decomposition(aten.slice_backward) +@out_wrapper() +def slice_backward( + grad_output: Tensor, + input_sizes: list[int], + dim: int, + start: int, + end: int, + step: int, +): + grad_input = grad_output.new_zeros(input_sizes) + return torch.slice_scatter(grad_input, grad_output, dim, start, end, step) + + +@register_decomposition(aten.slice.Tensor) +def slice_forward( + # Tensor(a) self, int dim=0, SymInt? start=None, SymInt? end=None, SymInt step=1 + self: Tensor, + dim: int = 0, + start: int | None = None, + end: int | None = None, + step: int = 1, +): + from torch.fx.experimental.symbolic_shapes import statically_known_true + + ndim = self.dim() + if ndim == 0: + raise RuntimeError("slice() cannot be applied to a 0-dim tensor.") + dim = utils.canonicalize_dim(self.dim(), dim) + sizes = list(self.size()) + strides = list(self.stride()) + + if step <= 0: + raise RuntimeError("slice step must be positive") + + start_val = start if start is not None else 0 + end_val = end if end is not None else sys.maxsize # 2^63 - 1 + + if start_val < 0: + start_val += sizes[dim] + + if end_val < 0: + end_val += sizes[dim] + + if start_val < 0: + start_val = 0 + elif start_val > sizes[dim]: + start_val = sizes[dim] + + if statically_known_true(end_val == sys.maxsize): + end_val = sizes[dim] + elif end_val < start_val: + end_val = start_val + elif end_val > sizes[dim]: + end_val = sizes[dim] + + storage_offset = self.storage_offset() + start_val * strides[dim] + len = end_val - start_val + sizes[dim] = (len + step - 1) // step + strides[dim] *= step + + if self.is_quantized: + raise NotImplementedError( + "Slice decomposition for quantized tensors aren't implemented" + ) + else: + return self.as_strided(sizes, strides, storage_offset) + + +def _normalize_start_end( + x: Tensor, dim: int, start: int | None, end: int | None +) -> tuple[int, int]: + """ + Normalize start and end such that both are in the range + [0, x.get_size()[dim]] and start <= end. + """ + dim_size = x.shape[dim] + + def clamp_wrap(val, lower, upper, default) -> int: + if val is None: + return default + if val < 0: + val = val + dim_size + return min(max(val, lower), upper) + + start = clamp_wrap(start, 0, dim_size, 0) + end = clamp_wrap(end, start, dim_size, dim_size) + return start, end + + +# This is not in torch._refs because aten.index used by +# aten._unsafe_masked_index does not have a decomposition. +@register_decomposition(aten.slice_scatter) +@out_wrapper() +def slice_scatter( + input: Tensor, + src: Tensor, + dim: int = 0, + start: int | None = None, + end: int | None = None, + step: int = 1, +): + dim = utils.canonicalize_dim(input.ndim, dim) + dim_size = input.shape[dim] + start, end = _normalize_start_end(input, dim, start, end) + + src_size = list(input.shape) + src_size[dim] = (end - start + (step - 1)) // step + src = src.expand(src_size) + + if start == 0 and end == dim_size and step == 1: + return src.clone() + + indices: list[Tensor | None] = [None] * input.dim() + idx = torch.arange(dim_size, device=input.device) + indices[dim] = (idx - start) // step + + mask = torch.ones(dim_size, device=input.device, dtype=torch.bool) + if start != 0: + mask = torch.logical_and(mask, idx >= start) + + if end != dim_size: + mask = torch.logical_and(mask, idx < end) + + if step != 1: + mask = torch.logical_and(mask, (idx - start) % step == 0) + + mask_shape = [1] * input.dim() + mask_shape[dim] = -1 + mask = mask.view(mask_shape) + return aten.where(mask, aten._unsafe_masked_index(src, mask, indices, 0), input) + + +@register_decomposition(aten.select_backward) +@out_wrapper() +def select_backward(grad_output: Tensor, input_sizes: list[int], dim: int, index: int): + grad_input = grad_output.new_zeros(input_sizes) + return torch.select_scatter(grad_input, grad_output, dim, index) + + +@register_decomposition(aten.diagonal_backward) +@out_wrapper() +def diagonal_backward( + grad_output: Tensor, input_sizes: list[int], offset: int, dim1: int, dim2: int +): + grad_input = grad_output.new_zeros(input_sizes) + return torch.diagonal_scatter(grad_input, grad_output, offset, dim1, dim2) + + +def _cast_grad_to_input_dtype( + grad_output: Tensor, grad_input: Tensor, input_dtype: torch.dtype +): + if grad_output.dtype != input_dtype: + grad_input = grad_input.to(input_dtype) + return grad_input + + +@register_decomposition(aten._softmax_backward_data) +@out_wrapper("grad_input") +@compute_only_pw_cast_for_opmath +def _softmax_backward_data( + grad_output: Tensor, output: Tensor, dim: int, input_dtype: torch.dtype +): + new_grad_output = grad_output * output + grad_input = new_grad_output - output * torch.sum( + new_grad_output, dim=dim, keepdim=True + ) + + # CPU kernel doesn't respect input_dtype, but following check doesn't work for meta tensor + # if grad_output.device == torch.device("cpu"): + # return grad_input.contiguous() + + return _cast_grad_to_input_dtype(grad_output, grad_input, input_dtype).contiguous() + + +@register_decomposition(aten._log_softmax_backward_data) +@out_wrapper() +@compute_only_pw_cast_for_opmath +def _log_softmax_backward_data( + grad_output: Tensor, output: Tensor, dim: int, input_dtype: torch.dtype +): + grad_input = grad_output - torch.exp(output) * torch.sum( + grad_output, dim=dim, keepdim=True + ) + return _cast_grad_to_input_dtype(grad_output, grad_input, input_dtype) + + +def _im2col_col2im_indices_along_dim( + input_d, kernel_d, dilation_d, padding_d, stride_d, device +): + """Utility function to implement im2col and col2im""" + blocks_d = input_d + padding_d * 2 - dilation_d * (kernel_d - 1) + + arange_kw = partial(torch.arange, dtype=torch.int64, device=device) + + # Stride kernel over input and find starting indices along dim d + blocks_d_indices = arange_kw(0, blocks_d, stride_d).unsqueeze(0) + + # Apply dilation on kernel and find its indices along dim d + kernel_grid = arange_kw(0, kernel_d * dilation_d, dilation_d).unsqueeze(-1) + + # Broadcast and add kernel starting positions (indices) with + # kernel_grid along dim d, to get block indices along dim d + return blocks_d_indices + kernel_grid + + +@register_decomposition(aten.im2col) +@out_wrapper() +def im2col( + input: Tensor, + kernel_size: list[int], + dilation: list[int], + padding: list[int], + stride: list[int], +) -> Tensor: + torch._check(len(kernel_size) == 2, lambda: "im2col(): only 2D kernel supported") + torch._check(len(dilation) == 2, lambda: "im2col(): only 2D dilation supported") + torch._check(len(padding) == 2, lambda: "im2col(): only 2D padding supported") + torch._check(len(stride) == 2, lambda: "im2col(): only 2D stride supported") + + def check_positive(param, param_name, strict=True): + cond = all(p > 0 for p in param) if strict else all(p >= 0 for p in param) + torch._check( + cond, lambda: f"{param_name} should be greater than zero, but got {param}" + ) + + check_positive(kernel_size, "kernel_size") + check_positive(dilation, "dilation") + check_positive(dilation, "padding", strict=False) + check_positive(stride, "stride") + + shape = input.shape + ndim = len(shape) + torch._check( + ndim in (3, 4) and all(d != 0 for d in shape[-3:]), + lambda: "Expected 3D or 4D (batch mode) tensor for input with possible 0 batch size " + f"and non-zero dimensions, but got: {tuple(shape)}", + ) + output_size = tuple( + 1 + (out + 2 * pad - dil * (ker - 1) - 1) // st + for out, pad, dil, ker, st in zip( + shape[-2:], padding, dilation, kernel_size, stride + ) + ) + torch._check( + all(c > 0 for c in output_size), + lambda: f"Given an input with spatial size {tuple(shape[-2:])}, " + f"kernel_size={kernel_size}, dilation={dilation}, " + f"padding={padding}, stride={stride}, " + "the calculated shape of the array of sliding blocks " + f"is {output_size}, but its components must be at least one.", + ) + batched_input = ndim == 4 + if not batched_input: + input = input.unsqueeze(0) + + batch_dim, channel_dim, input_h, input_w = input.shape + + stride_h, stride_w = stride + padding_h, padding_w = padding + dilation_h, dilation_w = dilation + kernel_h, kernel_w = kernel_size + + blocks_row_indices = _im2col_col2im_indices_along_dim( + input_h, kernel_h, dilation_h, padding_h, stride_h, input.device + ) + blocks_col_indices = _im2col_col2im_indices_along_dim( + input_w, kernel_w, dilation_w, padding_w, stride_w, input.device + ) + + # Note that F.pad takes (padding_left, padding_right, padding_top, padding_bottom) + # ugh + padded_input = F.pad(input, (padding_w, padding_w, padding_h, padding_h)) + + blocks_row_indices = blocks_row_indices.unsqueeze(-1).unsqueeze(-1) + output = padded_input[:, :, blocks_row_indices, blocks_col_indices] + output = output.permute(0, 1, 2, 4, 3, 5) + num_blocks_row = blocks_row_indices.size(1) + num_blocks_col = blocks_col_indices.size(1) + output = output.reshape( + batch_dim, channel_dim * kernel_h * kernel_w, num_blocks_row * num_blocks_col + ) + + if not batched_input: + output = output.squeeze(0) + return output + + +@register_decomposition(aten.col2im) +@out_wrapper() +@pw_cast_for_opmath +def col2im( + input: Tensor, + output_size: list[int], + kernel_size: list[int], + dilation: list[int], + padding: list[int], + stride: list[int], +) -> Tensor: + torch._check(len(output_size) == 2, lambda: "only 2D output_size supported") + torch._check(len(kernel_size) == 2, lambda: "only 2D kernel supported") + torch._check(len(dilation) == 2, lambda: "only 2D dilation supported") + torch._check(len(padding) == 2, lambda: "only 2D padding supported") + torch._check(len(stride) == 2, lambda: "only 2D stride supported") + + def check_positive(param, param_name, strict=True): + cond = all(p > 0 for p in param) if strict else all(p >= 0 for p in param) + torch._check( + cond, lambda: f"{param_name} should be greater than zero, but got {param}" + ) + + check_positive(kernel_size, "kernel_size") + check_positive(dilation, "dilation") + check_positive(padding, "padding", strict=False) + check_positive(stride, "stride") + check_positive(output_size, "output_size") + + shape = input.shape + ndim = len(shape) + torch._check( + ndim in (2, 3) and all(d != 0 for d in shape[-2:]), + lambda: "Expected 2D or 3D (batch mode) tensor for input with possible 0 batch size " + f"and non-zero dimensions, but got: {tuple(shape)}", + ) + prod_kernel_size = kernel_size[0] * kernel_size[1] + torch._check( + shape[-2] % prod_kernel_size == 0, + lambda: "Expected size of input's first non-batch dimension to be divisible by the " + f"product of kernel_size, but got input.shape[-2] = {shape[-2]} and " + f"kernel_size={kernel_size}", + ) + col = [ + 1 + (out + 2 * pad - dil * (ker - 1) - 1) // st + for out, pad, dil, ker, st in zip( + output_size, padding, dilation, kernel_size, stride + ) + ] + L = col[0] * col[1] + torch._check( + shape[-1] == L, + lambda: f"Given output_size={output_size}, kernel_size={kernel_size}, " + f"dilation={dilation}, padding={padding}, stride={stride}, " + f"expected input.size(-1) to be {L} but got {shape[-1]}.", + ) + torch._check( + L > 0, + lambda: f"Given output_size={output_size}, kernel_size={kernel_size}, " + f"dilation={dilation}, padding={padding}, stride={stride}, " + f"expected input.size(-1) to be {L} but got {shape[-1]}.", + ) + batched_input = ndim == 3 + if not batched_input: + input = input.unsqueeze(0) + + shape = input.shape + + out_h, out_w = output_size + stride_h, stride_w = stride + padding_h, padding_w = padding + dilation_h, dilation_w = dilation + kernel_h, kernel_w = kernel_size + + # col2im is defined as the backwards of im2col, so we differentiate its decomposition by hand + input = input.reshape([shape[0], shape[1] // prod_kernel_size] + kernel_size + col) + input = input.permute(0, 1, 2, 4, 3, 5) + + indices_row = _im2col_col2im_indices_along_dim( + out_h, kernel_h, dilation_h, padding_h, stride_h, input.device + ) + indices_row = _unsqueeze_to_dim(indices_row, 4) + indices_col = _im2col_col2im_indices_along_dim( + out_w, kernel_w, dilation_w, padding_w, stride_w, input.device + ) + + output_padded_size = [o + 2 * p for o, p in zip(output_size, padding)] + output = input.new_zeros( + [shape[0], shape[1] // prod(kernel_size)] + output_padded_size + ) + idx = (None, None, indices_row, indices_col) + output = aten._unsafe_index_put(output, idx, input, accumulate=True) + output = F.pad(output, (-padding_w, -padding_w, -padding_h, -padding_h)) + + if not batched_input: + output = output.squeeze(0) + return output + + +@register_decomposition(aten.native_dropout_backward) +@out_wrapper() +def native_dropout_backward(grad_output: Tensor, mask: Tensor, scale: float): + # According to the CUDA kernel implementation we should have this test; + # but it seems to fail tests! + # torch._check(mask.dtype == torch.bool, lambda: f"Mask should be Bool Scalar Type {mask.dtype}") + + # Mimicking CUDA kernel's behavior for output stride: output follow input's memory format + # This different from TensorIterator's behavior + r = (grad_output * (mask.type_as(grad_output) * scale)).clone( + memory_format=utils.suggest_memory_format(grad_output) + ) + return r + + +@register_decomposition(aten.unfold_backward) +@out_wrapper() +def unfold_backward( + grad: Tensor, input_size: list[int], dimension: int, size: int, step: int +) -> Tensor: + if len(input_size) == 0: + return torch.squeeze_copy(grad, 0) + dim = utils.canonicalize_dim(len(input_size), dimension) + idx = torch.arange(input_size[dim], device=grad.device, dtype=torch.int32) + idx = idx.unfold(0, size, step).flatten() + grad = grad.movedim(-1, dim + 1).flatten(dim, dim + 1) + # nb. At the moment this generates two kernels in triton + # It could potentially be fused into one call to scatter_reduce, + # in the case step <= size provided scatter_reduce generates 1 kernel + grad_input = grad.new_zeros(input_size) + index = (None,) * dim + (idx,) + return aten._unsafe_index_put(grad_input, index, grad, accumulate=True).contiguous() + + +@register_decomposition(aten.logit_backward.default) +@pw_cast_for_opmath +def logit_backward( + grad_output: Tensor, self: Tensor, eps: float | None = None +) -> Tensor: + if eps is not None: + lo = eps + hi = 1.0 - lo + return torch.where( + torch.logical_and(self >= lo, self <= hi), + grad_output / (self * (1.0 - self)), + 0.0, + ) + else: + return torch.where( + torch.logical_and(self >= 0.0, self <= 1.0), + grad_output / (self * (1.0 - self)), + self.new_full((), float("nan")), + ) + + +@register_decomposition(aten.dropout) +@aten.dropout.default.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten.dropout.default.py_impl(DispatchKey.Autograd) +def dropout(input: Tensor, p: float, train: bool | None): + if train and p != 0: + return aten.native_dropout(input, p, train)[0] + else: + return input.clone() + + +@register_decomposition(aten.native_dropout) +@out_wrapper("out0", "out1") +def native_dropout(input: Tensor, p: float, train: bool | None): + if train and p != 0: + if p == 1: + return (torch.zeros_like(input), torch.zeros_like(input, dtype=torch.bool)) + if not input.dtype.is_floating_point: + raise RuntimeError( + "result type Float can't be cast to the desired output type Long" + ) + bool_mask = torch.rand_like(input) > p + res = bool_mask * input * float(1.0 / (1.0 - p)) + return (res, bool_mask) + else: + return (input, torch.ones_like(input, dtype=torch.bool)) + + +@register_decomposition(aten._softmax) +@out_wrapper() +def _softmax(x: Tensor, dim: int, half_to_float: bool): + from torch.fx.experimental.symbolic_shapes import guard_or_false + + # eager softmax returns a contiguous tensor. Ensure that decomp also returns + # a contiguous tensor. + x = x.contiguous() + if half_to_float: + if x.dtype != torch.half: + raise AssertionError( + f"half_to_float is True but x.dtype is {x.dtype}, expected torch.half" + ) + computation_dtype, result_dtype = utils.elementwise_dtypes( + x, type_promotion_kind=utils.ELEMENTWISE_TYPE_PROMOTION_KIND.DEFAULT + ) + x = x.to(computation_dtype) + if guard_or_false(x.numel() == 0): + unnormalized = torch.exp(x) + else: + x_max = torch.amax(x, dim, keepdim=True) + unnormalized = torch.exp(x - x_max) + result = unnormalized / torch.sum(unnormalized, dim, keepdim=True) + if not half_to_float: + result = result.to(result_dtype) + return result + + +@register_decomposition(aten._log_softmax) +@out_wrapper(exact_dtype=True) +def _log_softmax(x: Tensor, dim: int, half_to_float: bool): + from torch.fx.experimental.symbolic_shapes import guard_or_false + + # eager log_softmax returns a contiguous tensor. Ensure that decomp also + # returns a contiguous tensor. + x = x.contiguous() + if half_to_float: + if x.dtype != torch.half: + raise AssertionError( + f"half_to_float is True but x.dtype is {x.dtype}, expected torch.half" + ) + computation_dtype, result_dtype = utils.elementwise_dtypes( + x, type_promotion_kind=utils.ELEMENTWISE_TYPE_PROMOTION_KIND.DEFAULT + ) + x = x.to(computation_dtype) + if guard_or_false(x.numel() == 0): + shifted = x + else: + x_max = torch.amax(x, dim, keepdim=True) + shifted = x - x_max + shifted_logsumexp = torch.log(torch.sum(torch.exp(shifted), dim, keepdim=True)) + result = shifted - shifted_logsumexp + if not half_to_float: + result = result.to(result_dtype) + return result + + +@register_decomposition(aten.embedding) +@out_wrapper() +def embedding( + weight: Tensor, + indices: Tensor, + padding_idx: int = -1, + scale_grad_by_freq: bool = False, + sparse: bool = False, +) -> Tensor: + if weight.dim() != 2: + raise AssertionError(f"'weight' must be 2-D, got {weight.dim()}-D") + # Nb. scale_grad_by_freq is not used in the forward + if indices.ndim <= 1: + # We need this one as weight[indices] calls item() in these cases + out = weight.index_select(0, indices) + if indices.ndim == 0: + out = out.squeeze(0) + return out + else: + return weight[indices] + + +@register_decomposition(aten.embedding_dense_backward) +@out_wrapper() +def embedding_dense_backward( + grad_output: Tensor, + indices: Tensor, + num_weights: int, + padding_idx: int, + scale_grad_by_freq: bool, +): + computation_dtype, result_dtype = utils.elementwise_dtypes( + grad_output, type_promotion_kind=utils.ELEMENTWISE_TYPE_PROMOTION_KIND.DEFAULT + ) + grad_output = grad_output.to(computation_dtype) + indices = _maybe_convert_to_dtype(indices, torch.long) # type: ignore[assignment] + if scale_grad_by_freq: + counts = indices.new_zeros((num_weights,)) + ones = torch.ones_like(indices) + counts = aten._unsafe_index_put(counts, [indices], ones, accumulate=True) + grad_weights_scale = counts[indices] + grad_output = grad_output / grad_weights_scale.unsqueeze(-1) + + mask = _unsqueeze_to_dim(indices == padding_idx, grad_output.ndim) + grad = grad_output.masked_fill(mask, 0) + grad_weight = grad_output.new_zeros( + (num_weights,) + grad_output.shape[indices.ndim :] + ) + return aten._unsafe_index_put(grad_weight, [indices], grad, accumulate=True).to( + result_dtype + ) + + +def prod(x: list[int]): + r = 1 + for i in x: + r *= i + return r + + +def _pad_chunk( + tensors: list[Tensor], + dim: int, + num_chunks: int, +) -> list[Tensor]: + padded_tensors = [] + for tensor in tensors: + tensor_size = tensor.size() + pad_along_dim = (tensor_size[dim] + num_chunks - 1) // num_chunks * num_chunks + if pad_along_dim != tensor_size[dim]: + # Use aten.constant_pad_nd instead of copy_ for functionalization + pad = [0] * 2 * (tensor.ndim - dim - 1) + [ + 0, + pad_along_dim - tensor_size[dim], + ] + tensor = aten.constant_pad_nd(tensor, pad, 0) + view_size = tensor_size[:dim] + torch.Size([num_chunks, -1]) + padded_tensors.append(tensor.reshape(view_size)) + return padded_tensors + + +def have_same_ndims(tensors: list[Tensor]): + ndim = tensors[0].ndim + for tensor in tensors: + if tensor.ndim != ndim: + return False + return True + + +def leading_dimension_matches(tensors: list[Tensor], dim: int): + leading_dim_sizes = tensors[0].size()[:dim] + for tensor in tensors: + torch._check( + tensor.size()[:dim] == leading_dim_sizes, + lambda: "_chunk_cat expects same sizes of 0,...,dim-1 dimensions for all tensors", + ) + + +def _preprocess_chunk_cat_inputs( + tensors: list[Tensor], + dim: int, + num_chunks: int, +): + torch._check(num_chunks >= 1, lambda: "_chunk_cat expects positive num_chunks") + torch._check( + len(tensors) > 0, lambda: "_chunk_cat expects a non-empty input tensor list" + ) + expected_dtype = tensors[0].dtype + expected_device = tensors[0].device + for tensor in tensors: + torch._check(tensor.numel() > 0, lambda: "_chunk_cat expects non-empty tensor") + torch._check( + tensor.dtype == expected_dtype, + lambda: "_chunk_cat expects all input tensors with the same dtype", + ) + torch._check( + tensor.device == expected_device, + lambda: "_chunk_cat expects all inputs tensors on the same device", + ) + if have_same_ndims(tensors): + dim = utils.canonicalize_dim(tensors[0].dim(), dim) + else: + torch._check( + dim >= 0, + lambda: "_chunk_cat expects non-negative dim when input tensors have different ndims", + ) + for tensor in tensors: + torch._check( + dim < tensor.ndim, + lambda: "_chunk_cat expects dim < ndim for all input tensors", + ) + leading_dimension_matches(tensors, dim) + return dim + + +@register_decomposition([aten._chunk_cat.default, aten._chunk_cat.out]) +def _chunk_cat( + tensors: list[Tensor], + dim: int, + num_chunks: int, + out: Tensor | None = None, +) -> Tensor: + dim = _preprocess_chunk_cat_inputs(tensors, dim, num_chunks) + padded_tensors = _pad_chunk(tensors, dim, num_chunks) + if out is None: + return torch.cat(padded_tensors, dim + 1) + else: + torch.cat(padded_tensors, dim + 1, out=out) + return out + + +# out_wrapper currently does not allow optional outputs +@register_decomposition( + [aten.split_with_sizes_copy.default, aten.split_with_sizes_copy.out] +) +def split_with_sizes_copy( + self: Tensor, + split_sizes: list[int], + dim: int = 0, + out: list[Tensor] | None = None, +) -> list[Tensor] | None: + splits = aten.split_with_sizes(self, split_sizes, dim=dim) + if out is None: + return [s.clone(memory_format=torch.contiguous_format) for s in splits] + else: + for output, split in zip(out, splits): + _maybe_resize_out(output, split.shape) + _safe_copy_out(copy_from=split, copy_to=output, exact_dtype=True) + return None + + +@register_decomposition(aten.unsafe_split.Tensor) +def unsafe_split(input: Tensor, split_size: int, dim: int = 0) -> tuple[Tensor, ...]: + return aten.split.Tensor(input, split_size, dim) + + +@register_decomposition(aten.unsafe_split_with_sizes.default) +def unsafe_split_with_sizes( + input: Tensor, split_sizes: list[int], dim: int = 0 +) -> tuple[Tensor, ...]: + return aten.split_with_sizes.default(input, split_sizes, dim) + + +@register_decomposition(aten.split.Tensor) +def split(self: Tensor, split_size: int, dim: int = 0) -> tuple[Tensor, ...]: + input_sizes = self.shape + dim_size = input_sizes[dim] + if split_size == 0: + if dim_size != 0: + raise AssertionError( + f"split_size is 0 but dim_size is {dim_size}, expected 0" + ) + return (self.detach(),) + chunks = (dim_size + split_size - 1) // split_size + + # Avoid importing sympy at a module level + from torch.fx.experimental.symbolic_shapes import guard_int + + chunks = guard_int(chunks) + split_sizes = [split_size for i in range(chunks)] + split_sizes[-1] = split_size - (split_size * chunks - dim_size) + return torch.split(self, split_sizes, dim) + + +@aten.tensor_split.tensor_indices_or_sections.py_impl( + DispatchKey.CompositeImplicitAutograd +) +def tensor_split_tensor_indices_or_sections_py_impl( + self: Tensor, + tensor_indices_or_sections: Tensor, + dim: int = 0, +) -> tuple[Tensor, ...]: + if tensor_indices_or_sections.device.type != "cpu": + raise AssertionError( + f"tensor_indices_or_sections must be on CPU, got {tensor_indices_or_sections.device}" + ) + if tensor_indices_or_sections.dtype != torch.int64: + raise AssertionError( + f"tensor_indices_or_sections must be int64, got {tensor_indices_or_sections.dtype}" + ) + split_dim = tensor_indices_or_sections.dim() + torch._check( + split_dim == 1 or split_dim == 0, + lambda: "tensor_split expected tensor_indices_or_sections to be a zero-dimensional " + f"or one-dimensional tensor, but got a tensor with {split_dim} dims", + ) + if split_dim == 0: + sections = tensor_indices_or_sections.item() + if not isinstance(sections, IntLike): + raise AssertionError( + f"Expected sections to be IntLike, got {type(sections).__name__}" + ) + return self.tensor_split(sections, dim) + else: + ctx = nullcontext + if (fake_mode := torch._guards.detect_fake_mode()) and ( + shape_env := fake_mode.shape_env + ): + ctx = shape_env.ignore_fresh_unbacked_symbols # type: ignore[assignment] + # In fake tensor prop, we end up calling slice() with these unbacked indices. + # Because slice has flexible semantics, the unbacked handling generates new output sizes + # for each slice, effectively clobbering over these index symbols. + # To avoid PendingUnbackedSymbolNotFound errors, we tell the compiler it's fine to not bind these. + with ctx(): + indices = [i.item() for i in tensor_indices_or_sections] + # WARNING: Tempted to torch._check(x>0) on the indices here? You + # can't: tensor_split works with negative values in indices: + # + # >>> torch.tensor_split(torch.randn(10), torch.tensor([-5, 5])) + # (tensor([ 0.3540, 2.1074, -0.8507, 1.1639, 0.3055]), tensor([]), + # tensor([-0.4285, 1.0692, -0.1776, 0.9362, 1.6143])) + # + # Sorry, I don't make the rules. Explicitly do the item call in user + # code if you KNOW that they are non-negative. + return self.tensor_split(indices, dim) + + +# TODO: this doesn't appear to have enough precision in bfloat16 +@register_decomposition(aten.addmm) +@out_wrapper(exact_dtype=True) +@pw_cast_for_opmath +def addmm(self: Tensor, mat1: Tensor, mat2: Tensor, beta: int = 1, alpha: int = 1): + if not self.is_floating_point() and not self.is_complex(): + beta = int(beta) + alpha = int(alpha) + out = alpha * torch.mm(mat1, mat2) + if beta == 0: + return out + + # The output of aten.addmm is contiguous, we need to match this behavior in the decomposition. + # The original implementation 'beta * self + out' would return a strided tensor if `self` is strided. + # We thus use `out`, the output of torch.mm, which is always contiguous, as the first argument for addition. + # This is relying on TensorIterator's behavior that it takes higher precedence on the stride of first input. + # Alternative, we can write `(beta * self + out).contiguous()`, but it introduces another copy in some cases. + # This implementation is not ideal, and we should revisit this when we have a better solution. + return out + beta * self + + +@register_decomposition(aten._addmm_activation) +@out_wrapper() +@pw_cast_for_opmath +def _addmm_activation( + self: Tensor, + mat1: Tensor, + mat2: Tensor, + beta: int = 1, + alpha: int = 1, + use_gelu: bool = False, +): + out = addmm(self, mat1, mat2, beta, alpha) + if use_gelu: + if self.is_cuda: + return aten.gelu(out, approximate="tanh") + else: + return aten.gelu(out) + return aten.relu(out) + + +@register_decomposition(aten.addmv) +@out_wrapper(exact_dtype=True) +@pw_cast_for_opmath +def addmv(self: Tensor, mat1: Tensor, vec: Tensor, beta: int = 1, alpha: int = 1): + if not self.is_floating_point() and not self.is_complex(): + beta = int(beta) + alpha = int(alpha) + out = alpha * torch.mv(mat1, vec) + if beta == 0: + return out + if out.numel() == 0: # handle empty matrix + return beta * self + return out + beta * self + + +@register_decomposition(aten.native_group_norm_backward.default) +@pw_cast_for_opmath +def native_group_norm_backward( + grad_output: Tensor, + input: Tensor, + mean: Tensor, + rstd: Tensor, + gamma: Tensor | None, + N: int, + C: int, + HxW: int, + group: int, + output_mask: list[bool], +) -> tuple[Tensor | None, Tensor | None, Tensor | None]: + utils.check_same_device( + grad_output, input, mean, rstd, allow_cpu_scalar_tensors=False + ) + utils.check_same_shape(input, grad_output, allow_cpu_scalar_tensors=False) + utils.check_same_shape(mean, rstd, allow_cpu_scalar_tensors=False) + torch._check( + input.numel() == N * C * HxW, + lambda: f"Expect input to have {N * C * HxW} elements", + ) + torch._check( + mean.shape == (N, group), + lambda: f"Expect mean to have shape ({N}, {group}, but got {mean.shape}", + ) + torch._check( + gamma is None or gamma.numel() == C, + lambda: f"Expect gamma to have {C} elements but got {gamma.numel() if gamma is not None else -1}", + ) + + cpg = C // group + torch._check( + C == cpg * group, + lambda: f"Expect number of channels {C} to be evenly-divisible by number of groups {group}", + ) + + # Compute Internal gradients + ds = torch.mul(grad_output, input).view(N, C, HxW).sum(dim=[2]) + db = grad_output.view(N, C, HxW).sum(dim=[2]) + + d_input: Tensor | None = None + d_gamma: Tensor | None = None + d_bias: Tensor | None = None + if output_mask[0]: + s = 1.0 / (HxW * cpg) + if gamma is not None: + ds_val = torch.mul(ds, gamma.unsqueeze(0)).reshape(N, group, cpg).sum(2) + db_val = torch.mul(db, gamma.unsqueeze(0)).reshape(N, group, cpg).sum(2) + c1 = torch.mul( + rstd.unsqueeze(-1), + gamma.reshape(1, group, cpg), + ) + else: + ds_val = ds.reshape(N, group, cpg).sum(2) + db_val = db.reshape(N, group, cpg).sum(2) + c1 = torch.mul( + rstd.unsqueeze(-1), + torch.ones((1, group, cpg), device=rstd.device), + ) + c2 = (db_val * mean - ds_val) * rstd * rstd * rstd * s + c3 = -c2 * mean - db_val * rstd * s + + c1 = c1.unsqueeze(-1) + c2 = _unsqueeze_to_dim(c2, 4) + c3 = _unsqueeze_to_dim(c3, 4) + d_input = ( + torch.mul(grad_output.reshape(N, group, cpg, HxW), c1) + + torch.mul(input.reshape(N, group, cpg, HxW), c2) + + c3 + ) + d_input = d_input.reshape(input.shape).to(input.dtype) + if output_mask[1]: + d_gamma = ( + ( + (ds.view(N, group, cpg) - db.view(N, group, cpg) * mean.unsqueeze(-1)) + * rstd.unsqueeze(-1) + ) + .sum(dim=[0]) + .reshape(C) + ) + if output_mask[2]: + d_bias = db.sum(dim=[0]) + + return (d_input, d_gamma, d_bias) + + +# out_wrapper currently does not allow optional outputs +@register_decomposition(aten.native_group_norm_backward.out) +def native_group_norm_backward_out( + grad_output: Tensor, + input: Tensor, + mean: Tensor, + rstd: Tensor, + gamma: Tensor | None, + N: int, + C: int, + HxW: int, + group: int, + output_mask: list[bool], + *, + out0: torch.Tensor, + out1: torch.Tensor, + out2: torch.Tensor, +) -> tuple[Tensor | None, Tensor | None, Tensor | None]: + result = native_group_norm_backward( + grad_output, input, mean, rstd, gamma, N, C, HxW, group, output_mask + ) + grad_input = (out0, out1, out2) + for i, r in enumerate(result): + if r is not None: + _maybe_resize_out(grad_input[i], r.shape) + _safe_copy_out(copy_from=r, copy_to=grad_input[i], exact_dtype=True) + + return grad_input + + +def _maybe_cast(x: Tensor | None, dtype) -> Tensor | None: + if x is not None: + return x.to(dtype) + return x + + +# TODO: Take a closer look at the type promotion semantics +@register_decomposition(aten.native_layer_norm_backward.default) +def native_layer_norm_backward( + grad_out: Tensor, + input: Tensor, + normalized_shape: list[int], + mean: Tensor, + rstd: Tensor, + weight: Tensor | None, + bias: Tensor | None, + output_mask: list[bool], +) -> tuple[Tensor | None, Tensor | None, Tensor | None]: + input_shape = input.shape + input_ndim = input.dim() + computation_dtype = utils.get_computation_dtype(input.dtype) + grad_out_cast, input_cast, weight_cast, bias_cast = ( + x.to(computation_dtype, memory_format=torch.contiguous_format) + if x is not None + else x + for x in (grad_out, input, weight, bias) + ) + if grad_out_cast is None: + raise AssertionError("grad_out_cast should not be None") + + axis = input_ndim - len(normalized_shape) + inner_dims = input_shape[axis:] + outer_dims = input_shape[:axis] + inner_dim_indices: list[int] = [] + outer_dim_indices: list[int] = [] + for i in range(input_ndim): + if i >= axis: + inner_dim_indices.append(i) + else: + outer_dim_indices.append(i) + + N = prod(inner_dims) # type: ignore[arg-type] + M = prod(outer_dims) # type: ignore[arg-type] + from torch.fx.experimental.symbolic_shapes import statically_known_true + + if statically_known_true(M == 0) or statically_known_true(N == 0): + return ( + input.new_zeros(input_shape) if output_mask[0] else None, + input.new_zeros(input_shape[axis:]) if output_mask[1] else None, + input.new_zeros(input_shape[axis:]) if output_mask[2] else None, + ) + mean = _unsqueeze_to_dim(mean, input_cast.dim()) # type: ignore[union-attr] + rstd = _unsqueeze_to_dim(rstd, input_cast.dim()) # type: ignore[union-attr] + if input_cast is None: + raise AssertionError("input_cast should not be None") + x_hat = (input_cast - mean) * rstd + if weight_cast is not None: + grad_x_hat = grad_out_cast * weight_cast + else: + grad_x_hat = grad_out_cast + a = grad_x_hat * N + b = torch.sum(grad_x_hat, inner_dim_indices, True) + c1 = torch.mul(grad_x_hat, x_hat) + c2 = torch.sum(c1, inner_dim_indices, True) + c3 = torch.mul(x_hat, c2) + + inner = a - b - c3 + d_input: Tensor | None = None + d_weight: Tensor | None = None + d_bias: Tensor | None = None + if output_mask[0]: + d_input = (rstd / N) * inner + + if output_mask[1] and weight_cast is not None: + if len(outer_dim_indices) > 0: + d_weight = torch.sum(grad_out_cast * x_hat, outer_dim_indices, False) + else: + d_weight = grad_out_cast * x_hat + + if output_mask[2] and bias_cast is not None: + if len(outer_dim_indices) > 0: + d_bias = torch.sum(grad_out_cast, outer_dim_indices, False) + else: + d_bias = grad_out_cast.clone() + + return ( + _maybe_cast(d_input, input.dtype), + _maybe_cast(d_weight, weight.dtype if weight is not None else None), + _maybe_cast(d_bias, bias.dtype if bias is not None else None), + ) + + +# out_wrapper currently does not allow optional outputs +@register_decomposition(aten.native_layer_norm_backward.out) +def native_layer_norm_backward_out( + grad_out: Tensor, + input: Tensor, + normalized_shape: list[int], + mean: Tensor, + rstd: Tensor, + weight: Tensor | None, + bias: Tensor | None, + output_mask: list[bool], + *, + out0: torch.Tensor, + out1: torch.Tensor, + out2: torch.Tensor, +) -> tuple[Tensor | None, Tensor | None, Tensor | None]: + result = native_layer_norm_backward( + grad_out, input, normalized_shape, mean, rstd, weight, bias, output_mask + ) + grad_input = (out0, out1, out2) + for i, r in enumerate(result): + if r is not None: + _maybe_resize_out(grad_input[i], r.shape) + _safe_copy_out(copy_from=r, copy_to=grad_input[i], exact_dtype=True) + + return grad_input + + +@register_decomposition(aten._fused_rms_norm.default) +def _fused_rms_norm( + input: Tensor, + normalized_shape: list[int], + weight: Tensor | None, + eps: float | None, +) -> tuple[Tensor, Tensor]: + dims_to_reduce: list[int] = [] + for i in range(len(normalized_shape)): + dims_to_reduce.append(input.dim() - i - 1) + + # upcast is needed for fp16 and bf16 + computation_dtype = utils.get_computation_dtype(input.dtype) + upcasted_input = input.to(computation_dtype) + + # computation_dtype would be one of [Double, Float, ComplexFloat, ComplexDouble] + if eps is None: + if computation_dtype in (torch.float32, torch.complex64): + eps_val = torch.finfo(torch.float32).eps + else: + eps_val = torch.finfo(torch.float64).eps + else: + eps_val = eps + + rqrst_input = torch.rsqrt( + # NB: don't inplace here, will violate functional IR invariant + # NB: carefully use the Scalar overload of add to ensure compatibility with the C++ decomp + torch.ops.aten.add.Scalar( + torch.pow(upcasted_input, 2).mean(dim=dims_to_reduce, keepdim=True), eps_val + ) + ) + + upcasted_result = upcasted_input.mul(rqrst_input) + + if weight is not None: + upcasted_result = upcasted_result.mul(weight) + + # NB: nested should be dead here, just here for fidelity + is_nested = input.is_nested or (weight is not None and weight.is_nested) + memory_format = utils.suggest_memory_format(input) + is_channels_last = memory_format in ( + torch.channels_last, + torch.channels_last_3d, + ) + + if not is_nested and not is_channels_last: + upcasted_result = upcasted_result.contiguous() + rqrst_input = rqrst_input.contiguous() + + # Cast normalized result back to original input type + result = upcasted_result.type_as(input) + + return result, rqrst_input + + +@register_decomposition(aten._fused_rms_norm_backward.default) +def _fused_rms_norm_backward( + grad_out: Tensor, + input: Tensor, + normalized_shape: list[int], + rstd: Tensor, + weight: Tensor | None, + output_mask: list[bool], +) -> tuple[Tensor | None, Tensor | None]: + input_shape = input.shape + input_ndim = input.dim() + computation_dtype = utils.get_computation_dtype(input.dtype) + + grad_out_cast = grad_out.to( + computation_dtype, memory_format=torch.contiguous_format + ) + input_cast = input.to(computation_dtype, memory_format=torch.contiguous_format) + weight_cast = ( + weight.to(computation_dtype, memory_format=torch.contiguous_format) + if weight is not None + else None + ) + if grad_out_cast is None: + raise AssertionError("grad_out_cast should not be None") + + axis = input_ndim - len(normalized_shape) + inner_dims = input_shape[axis:] + outer_dims = input_shape[:axis] + inner_dim_indices: list[int] = [] + outer_dim_indices: list[int] = [] + for i in range(input_ndim): + if i >= axis: + inner_dim_indices.append(i) + else: + outer_dim_indices.append(i) + + N = prod(inner_dims) # type: ignore[arg-type] + M = prod(outer_dims) # type: ignore[arg-type] + from torch.fx.experimental.symbolic_shapes import guard_or_false + + if guard_or_false(M == 0) or guard_or_false(N == 0): + return ( + input.new_zeros(input_shape) if output_mask[0] else None, + input.new_zeros(input_shape[axis:]) if output_mask[1] else None, + ) + + rstd = _unsqueeze_to_dim(rstd, input_cast.dim()) # type: ignore[union-attr] + if weight_cast is not None: + grad_x_hat = grad_out_cast * weight_cast + else: + grad_x_hat = grad_out_cast + + d_input: Tensor | None = None + d_weight: Tensor | None = None + + x_hat = input_cast * rstd + + if output_mask[0]: + sum_val = torch.sum(x_hat * grad_x_hat, dim=inner_dim_indices, keepdim=True) + d_input = (grad_x_hat - (x_hat / N) * sum_val) * rstd + + if output_mask[1] and weight_cast is not None: + d_weight_full_shape = grad_out_cast * x_hat + if len(outer_dim_indices) > 0: + d_weight = torch.sum( + d_weight_full_shape, dim=outer_dim_indices, keepdim=False + ) + else: + d_weight = d_weight_full_shape + + return ( + _maybe_cast(d_input, input.dtype), + _maybe_cast(d_weight, input.dtype), + ) + + +def native_batch_norm_helper( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor | None, + running_var: Tensor | None, + training: bool, + momentum: float, + eps: float, + functional: bool, +) -> tuple[Tensor, Tensor, Tensor, Tensor | None, Tensor | None]: + reduction_dims = [0] + list(range(2, input.dim())) + computation_dtype = utils.get_computation_dtype(input.dtype) + new_running_mean = running_mean + new_running_var = running_var + if training: + computation_dtype = utils.get_computation_dtype(input.dtype) + input_acc = input.to(dtype=computation_dtype) + biased_var, mean = torch.var_mean( + input_acc, dim=reduction_dims, correction=0, keepdim=True + ) + rstd = torch.rsqrt(biased_var + eps) + + output = (input - mean) * rstd + + save_mean = torch.squeeze(mean, reduction_dims) + save_rstd = torch.squeeze(rstd, reduction_dims) + if running_mean is not None: + new_running_mean = momentum * save_mean + (1 - momentum) * running_mean + if not functional: + running_mean.copy_(new_running_mean) + if running_var is not None: + n = input.numel() / input.shape[1] + # This doesn't strictly match eager's numerics, which accumulates var sum and then directly applies the correction + # But... that would require re-implementing var here, for negligible numerics gain on a tensor whose + # numerics probably don't matter. + squeezed_var = torch.squeeze(biased_var, reduction_dims) + unbiased_var = squeezed_var * (n / (n - 1)) + new_running_var = momentum * unbiased_var + (1 - momentum) * running_var + if not functional: + running_var.copy_(new_running_var) + else: + if running_mean is None or running_var is None: + raise AssertionError( + "running_mean and running_var must not be None in eval mode" + ) + running_mean = running_mean.to(dtype=computation_dtype, copy=True) + new_running_mean = running_mean + running_var = running_var.to(dtype=computation_dtype, copy=True) + new_running_var = running_var + mean = running_mean + invstd = 1 / (torch.sqrt(running_var + eps)) + # Very annoying inconsistency where CPU and CUDA give different shapes + if input.device.type != "cpu": + save_mean = running_mean + save_rstd = invstd + else: + save_mean = input.new_zeros((0,)) + save_rstd = input.new_zeros((0,)) + mean = _unsqueeze_to_dim(mean, input.dim() - 1) + invstd = _unsqueeze_to_dim(invstd, input.dim() - 1) + output = (input - mean) * invstd + + if weight is not None: + weight = weight.flatten() + weight = _unsqueeze_to_dim(weight, input.dim() - 1) + output = output * weight + + if bias is not None: + bias = bias.flatten() + bias = _unsqueeze_to_dim(bias, input.dim() - 1) + output = output + bias + + if input.device.type == "cpu": + save_mean = save_mean.to(dtype=input.dtype) + save_rstd = save_rstd.to(dtype=input.dtype) + return ( + output.to(dtype=input.dtype), + save_mean, + save_rstd, + new_running_mean, + new_running_var, + ) + + +@register_decomposition(aten.native_batch_norm) +@out_wrapper("out", "save_mean", "save_invstd") +def native_batch_norm( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor | None, + running_var: Tensor | None, + training: bool, + momentum: float, + eps: float, +) -> tuple[Tensor, Tensor, Tensor]: + output, save_mean, save_rstd, _, _ = native_batch_norm_helper( + input, weight, bias, running_mean, running_var, training, momentum, eps, False + ) + return output, save_mean, save_rstd + + +# TODO: this decomposition is NOT here to stay. We would much prefer replacing native_batch_norm +# with our new correctly schema'd _native_batch_norm_legit and its variants, but +# we cannot do that immediately in the C++ because it would be forwards incompatible +# with some mobile use cases. +# +# Since this change is most impactful for aot autograd/functionalization, we simply +# register this decomposition on the Autograd key for the python dispatcher (which is +# currently only used by aot autograd/functionalization and no one else, really). +# In two weeks or so, we should remove this decomposition and phase out the current native_batch_norm +# to be _native_batch_norm_legit and have the right schema (stating that there are input mutations). +@aten.native_batch_norm.default.py_impl(DispatchKey.Autograd) +@aten.native_batch_norm.default.py_impl(DispatchKey.CompositeImplicitAutograd) +def native_batch_norm_decomposition( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor | None, + running_var: Tensor | None, + training: bool, + momentum: float, + eps: float, +) -> tuple[Tensor, Tensor, Tensor]: + if running_mean is None and running_var is None: + return aten._native_batch_norm_legit( + input, weight, bias, training, momentum, eps + ) + if running_mean is None: + raise RuntimeError( + "running_mean is None, but running_var is provided. " + "They should both be None or both be provided." + ) + if running_var is None: + raise RuntimeError( + "running_var is None, but running_mean is provided. " + "They should both be None or both be provided." + ) + if training: + # HACK: batch norm consolidation should clean this up so this op doesn't take in a training arg. + return aten._native_batch_norm_legit( + input, weight, bias, running_mean, running_var, training, momentum, eps + ) + else: + return aten._native_batch_norm_legit_no_training( + input, weight, bias, running_mean, running_var, momentum, eps + ) + + +@aten.unsafe_chunk.default.py_impl(DispatchKey.CompositeImplicitAutograd) +def unsafe_chunk_py_impl(tensor, chunks, dim=0) -> list[Tensor]: + dim_size = tensor.size(dim) + split_size = (dim_size + chunks - 1) // chunks + + if split_size == 0 and dim_size == 0: + split_sizes = [split_size for _ in range(chunks)] + split_sizes[chunks - 1] = split_size - (split_size * chunks - dim_size) + return torch.ops.aten.unsafe_split_with_sizes.default(tensor, split_sizes, dim) + return torch.ops.aten.unsafe_split.Tensor(tensor, split_size, dim) + + +@register_decomposition(aten._native_batch_norm_legit_no_training.default) +def _native_batch_norm_legit_no_training( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor, + running_var: Tensor, + momentum: float, + eps: float, +) -> tuple[Tensor, Tensor, Tensor]: + return aten._native_batch_norm_legit.default( + input, + weight, + bias, + running_mean, + running_var, + False, # training + momentum, + eps, + ) + + +@register_decomposition(aten._native_batch_norm_legit.default) +def _native_batch_norm_legit( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor, + running_var: Tensor, + training: bool, + momentum: float, + eps: float, +) -> tuple[Tensor, Tensor, Tensor]: + output, save_mean, save_rstd, _, _ = native_batch_norm_helper( + input, weight, bias, running_mean, running_var, training, momentum, eps, False + ) + return output, save_mean, save_rstd + + +@register_decomposition(aten._native_batch_norm_legit.no_stats) +def _native_batch_norm_legit_no_stats( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + training: bool, + momentum: float, + eps: float, +) -> tuple[Tensor, Tensor, Tensor]: + output, save_mean, save_rstd, _, _ = native_batch_norm_helper( + input, weight, bias, None, None, training, momentum, eps, False + ) + return output, save_mean, save_rstd + + +@register_decomposition(aten._native_batch_norm_legit_functional.default) +def _native_batch_norm_legit_functional( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor, + running_var: Tensor, + training: bool, + momentum: float, + eps: float, +) -> tuple[Tensor, Tensor, Tensor, Tensor, Tensor]: + ( + output, + save_mean, + save_rstd, + new_running_mean, + new_running_var, + ) = native_batch_norm_helper( + input, weight, bias, running_mean, running_var, training, momentum, eps, True + ) + if new_running_mean is None: + raise AssertionError("new_running_mean should not be None") + if new_running_var is None: + raise AssertionError("new_running_var should not be None") + return output, save_mean, save_rstd, new_running_mean, new_running_var + + +def _get_batch_norm_reserve_tensor( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor, + running_var: Tensor, + eps: float, + training: bool, +) -> Tensor: + """ + Return a reserve tensor for batch norm, used only by cudnn to pass forward state to the + backward pass. This is needed for `_batch_norm_with_update` and `_batch_norm_no_update`, + which support a variety of backends including cudnn. We create this tensor here to get + the correct shape in the traced graph if we detect that will call the cudnn kernel, + and rely on DCE to avoid materializing this tensor. + """ + backend = torch._C._select_batch_norm_backend( # type: ignore[attr-defined] + input, weight, bias, running_mean, running_var, True, eps + ) + reserve_size = 0 + if backend == torch._C._BatchNormBackend.Cudnn: # type: ignore[attr-defined] + reserve_size = torch._C._get_cudnn_batch_norm_reserve_space_size( # type: ignore[attr-defined] + input, training + ) + return torch.empty( + reserve_size, dtype=torch.uint8, layout=input.layout, device=input.device + ) + + +@register_decomposition(aten._batch_norm_with_update.default) +def _batch_norm_with_update( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor, + running_var: Tensor, + momentum: float, + eps: float, +) -> tuple[Tensor, Tensor, Tensor, Tensor]: + output, save_mean, save_rstd, _, _ = native_batch_norm_helper( + input, + weight, + bias, + running_mean, + running_var, + True, # training + momentum, + eps, + False, # functional + ) + reserve = _get_batch_norm_reserve_tensor( + input, weight, bias, running_mean, running_var, eps, training=True + ) + return output, save_mean, save_rstd, reserve + + +@register_decomposition(aten._batch_norm_with_update_functional.default) +def _batch_norm_with_update_functional( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor, + running_var: Tensor, + momentum: float, + eps: float, +) -> tuple[Tensor, Tensor, Tensor, Tensor, Tensor, Tensor]: + ( + output, + save_mean, + save_rstd, + new_rm, + new_rv, + ) = native_batch_norm_helper( + input, weight, bias, running_mean, running_var, True, momentum, eps, True + ) + reserve = _get_batch_norm_reserve_tensor( + input, weight, bias, running_mean, running_var, eps, training=True + ) + if new_rm is None: + raise AssertionError("new_running_mean should not be None") + if new_rv is None: + raise AssertionError("new_running_var should not be None") + return (output, save_mean, save_rstd, reserve, new_rm, new_rv) + + +@register_decomposition(aten._batch_norm_no_update.default) +def _batch_norm_no_update( + input: Tensor, + weight: Tensor | None, + bias: Tensor | None, + running_mean: Tensor, + running_var: Tensor, + momentum: float, + eps: float, +) -> tuple[Tensor, Tensor, Tensor, Tensor]: + output, save_mean, save_rstd, _, _ = native_batch_norm_helper( + input, + weight, + bias, + running_mean, + running_var, + False, # training + momentum, + eps, + False, # functional + ) + reserve = _get_batch_norm_reserve_tensor( + input, weight, bias, running_mean, running_var, eps, training=False + ) + return output, save_mean, save_rstd, reserve + + +@register_decomposition(aten._fused_dropout) +@out_wrapper("out0", "out1") +@pw_cast_for_opmath +def _fused_dropout_decomposition(input, p, generator=None): + if generator is not None: + raise AssertionError( + f"generator must be None for _fused_dropout decomposition, got {generator}" + ) + mask = (torch.rand_like(input) < p).to(dtype=torch.uint8) + res = mask.type_as(input) * input * (1.0 / p) + return (res, mask) + + +@register_decomposition(aten._to_copy) +@out_wrapper() +def _to_copy( + x: Tensor | NumberType, + *, + dtype: torch.dtype | None = None, + layout=None, + device: torch.device | None = None, + pin_memory: bool = False, + non_blocking: bool = False, + memory_format: torch.memory_format | None = None, +): + if layout and layout != torch.strided: + raise AssertionError(f"layout must be None or torch.strided, got {layout}") + if pin_memory: + raise AssertionError( + "pin_memory=True is not supported in _to_copy decomposition" + ) + if not isinstance(x, (torch.Tensor, int, float, bool, complex)): + raise AssertionError(f"x must be Tensor or scalar, got {type(x).__name__}") + if device is None and dtype is None and memory_format is None: + if isinstance(x, torch.Tensor): + return x.clone() + else: + return x + dtype_converted = False + + if isinstance(x, torch.Tensor): + x_tensor = x + else: + x_tensor = torch.scalar_tensor(x) + + if device is not None and device != x_tensor.device: + # avoid conversions on cpu + if dtype is not None and device.type == "cpu": + x_tensor = torch._prims.convert_element_type(x_tensor, dtype) + dtype_converted = True + x_tensor = torch._prims.device_put(x_tensor, device, non_blocking) + + if dtype is not None and not dtype_converted: + x_tensor = torch._prims.convert_element_type(x_tensor, dtype) + dtype_converted = True + + if memory_format is not None: # no ref/prim for memory format + return torch.clone(x_tensor, memory_format=memory_format) + return x_tensor + + +# Questionable decompositions +# This is only valid if we're running the graph without autograd, such as if the backward pass has been traced. +# Note that this decomposition causes issues with in-place ops +@register_decomposition([aten.detach, aten.lift, aten.lift_fresh]) +@out_wrapper() +def nop_decomposition(x): + return aten.alias(x) + + +# Also register to the Autograd dispatch key, so this decomp can run above autograd. +# native_batch_norm needs to decompose into other ops before autograd. +@aten.cudnn_batch_norm.default.py_impl(DispatchKey.Autograd) +@register_decomposition(aten.cudnn_batch_norm) +@out_wrapper("out0", "out1", "out2", "out3") +def cudnn_batch_norm( + input: Tensor, + weight: Tensor, + bias: Tensor | None, + running_mean: Tensor | None, + running_var: Tensor | None, + training: bool, + exponential_average_factor: float, + epsilon: float, +): + a, b, c = aten.native_batch_norm( + input, + weight, + bias, + running_mean, + running_var, + training, + exponential_average_factor, + epsilon, + ) + # Cudnn return running mean and variance when training is True + if training: + return (a, b, c, input.new_zeros((0,), dtype=torch.uint8)) + return ( + a, + weight.new_zeros((0,)), + weight.new_zeros((0,)), + input.new_zeros((0,), dtype=torch.uint8), + ) + + +def _broadcast_batch_norm_backward(x, broadcast_mask): + for axis, mask in enumerate(broadcast_mask): + if mask == 1 and not (axis < x.ndim and x.shape[axis] == mask): + x = x.unsqueeze(axis) + return x + + +@register_decomposition(aten.batch_norm_backward.default) +def batch_norm_backward( + grad_out: Tensor, + input: Tensor, + weight: Tensor | None, + running_mean: Tensor | None, + running_var: Tensor | None, + save_mean: Tensor | None, + save_invstd: Tensor | None, + train: bool, + eps: float, + output_mask: list[bool], + reserve: Tensor, +) -> tuple[Tensor, Tensor | None, Tensor | None]: + return native_batch_norm_backward( + grad_out, + input, + weight, + running_mean, + running_var, + save_mean, + save_invstd, + train, + eps, + output_mask, + ) + + +@register_decomposition(aten.native_batch_norm_backward.default) +def native_batch_norm_backward( + grad_out: Tensor, + input: Tensor, + weight: Tensor | None, + running_mean: Tensor | None, + running_var: Tensor | None, + save_mean: Tensor | None, + save_invstd: Tensor | None, + train: bool, + eps: float, + output_mask: list[bool], +) -> tuple[Tensor, Tensor | None, Tensor | None]: + input_dtype = input.dtype + if weight is not None: + weight_dtype = weight.dtype + else: + weight_dtype = input_dtype + computation_dtype = utils.get_computation_dtype(input.dtype) + ( + grad_out_cast, + input_cast, + weight_cast, + running_mean_cast, + running_var_cast, + save_mean_cast, + save_invstd_cast, + ) = ( + x.to(computation_dtype) if x is not None else x + for x in ( + grad_out, + input, + weight, + running_mean, + running_var, + save_mean, + save_invstd, + ) + ) + input_shape = input.shape + input_rank = input.dim() + if input_rank < 2: + raise AssertionError(f"rank of the input must be at least 2, got {input_rank}") + + axis = 1 + num_features = prod(list(input_shape)) / input_shape[axis] + mean = save_mean_cast + invstd = save_invstd_cast + if train: + if mean is None or invstd is None: + raise AssertionError("mean and invstd must not be None in training mode") + + else: + if running_mean_cast is None or running_var_cast is None: + raise AssertionError( + "running_mean_cast and running_var_cast must not be None in eval mode" + ) + mean = running_mean_cast + invstd = torch.rsqrt(running_var_cast + eps) + + broadcast_mask: list[int] = [1] * input_rank + broadcast_mask[axis] = input_shape[axis] + + reduction_axes: list[int] = [] + for i in range(input_rank): + if i != axis: + reduction_axes.append(i) + + mean = _broadcast_batch_norm_backward(mean, broadcast_mask) # type: ignore[arg-type] + norm = 1.0 / num_features + grad_output_sum = torch.sum(grad_out_cast, reduction_axes) # type: ignore[arg-type] + dot_p = torch.sum(grad_out_cast * (input_cast - mean), reduction_axes) # type: ignore[operator] + + grad_mean = _broadcast_batch_norm_backward(grad_output_sum * norm, broadcast_mask) + proj_scale = _broadcast_batch_norm_backward( + torch.mul(dot_p * norm, invstd * invstd), # type: ignore[operator] + broadcast_mask, + ) + + if weight_cast is None: + grad_scale = _broadcast_batch_norm_backward(invstd, broadcast_mask) * 1.0 # type: ignore[arg-type] + else: + grad_scale = _broadcast_batch_norm_backward( + invstd * weight_cast, broadcast_mask + ) + + if train: + proj = (input_cast - mean) * proj_scale # type: ignore[operator] + grad_input = ((grad_out_cast - proj) - grad_mean) * grad_scale + else: + grad_input = grad_out_cast * grad_scale + + if output_mask[1]: + grad_weight = dot_p * invstd + else: + grad_weight = None # "None" doesn't work with vjp, should use zeros for vjp + + if output_mask[2]: + grad_bias = grad_output_sum + else: + grad_bias = None # "None" doesn't work with vjp, should use zeros for vjp + + return ( + grad_input.to(input_dtype), + _maybe_cast(grad_weight, weight_dtype), + _maybe_cast(grad_bias, weight_dtype), + ) + + +# out_wrapper currently does not allow optional outputs +@register_decomposition(aten.native_batch_norm_backward.out) +def native_batch_norm_backward_out( + grad_out: Tensor, + input: Tensor, + weight: Tensor | None, + running_mean: Tensor | None, + running_var: Tensor | None, + save_mean: Tensor | None, + save_invstd: Tensor | None, + train: bool, + eps: float, + output_mask: list[bool], + *, + out0: torch.Tensor, + out1: torch.Tensor, + out2: torch.Tensor, +) -> tuple[Tensor, Tensor | None, Tensor | None]: + result = native_batch_norm_backward( + grad_out, + input, + weight, + running_mean, + running_var, + save_mean, + save_invstd, + train, + eps, + output_mask, + ) + grad_input = (out0, out1, out2) + for i, r in enumerate(result): + if r is not None: + _maybe_resize_out(grad_input[i], r.shape) + _safe_copy_out(copy_from=r, copy_to=grad_input[i], exact_dtype=True) + + return grad_input + + +@register_decomposition(aten.miopen_batch_norm_backward) +@out_wrapper("out0", "out1", "out2") +def miopen_batch_norm_backward( + input: Tensor, + grad_output: Tensor, + weight: Tensor, + running_mean: Tensor | None, + running_var: Tensor | None, + save_mean: Tensor | None, + save_var: Tensor | None, + epsilon: float, +): + return aten.native_batch_norm_backward( + grad_output, + input, + weight, + running_mean, + running_var, + save_mean, + save_var, + True, + epsilon, + [True, True, True], + ) + + +@register_decomposition(aten.cudnn_batch_norm_backward) +@out_wrapper("out0", "out1", "out2") +def cudnn_batch_norm_backward( + input: Tensor, + grad_output: Tensor, + weight: Tensor, + running_mean: Tensor | None, + running_var: Tensor | None, + save_mean: Tensor | None, + save_var: Tensor | None, + epsilon: float, + reserveSpace: Tensor, +): + return aten.native_batch_norm_backward( + grad_output, + input, + weight, + running_mean, + running_var, + save_mean, + save_var, + True, + epsilon, + [True, True, True], + ) + + +@register_decomposition(aten._adaptive_avg_pool2d) +@out_wrapper() +@pw_cast_for_opmath +def adaptive_avg_pool2d(input: Tensor, output_size: tuple[int, int]): + # Preconditions + device = input.device + shape = input.shape + ndim = len(shape) + torch._check( + ndim in (3, 4), + lambda: f"adaptive_avg_pool2d(): Expected 3D or 4D tensor, but got {ndim}", + ) + for d in input.shape[-2:]: + torch._check( + d != 0, + lambda: "adaptive_avg_pool2d(): Expected input to have non-zero size for " + f"non-batch dimensions, but input has shape {tuple(shape)}.", + ) + + # Optimisation (we should also do this in the kernel implementation) + if shape[-2] % output_size[-2] == 0 and shape[-1] % output_size[-1] == 0: + stride = tuple(i // o for i, o in zip(shape[-2:], output_size)) + kernel = tuple( + i - (o - 1) * s for i, o, s in zip(shape[-2:], output_size, stride) + ) + return torch.nn.functional.avg_pool2d(input, kernel, stride) + + def start_index(a, b, c): + return torch.div(a * c, b, rounding_mode="trunc") + + def end_index(a, b, c): + return torch.div((a + 1) * c + b - 1, b, rounding_mode="trunc") + + def compute_idx(in_size, out_size): + orange = torch.arange(out_size, device=device, dtype=torch.int64) + i0 = start_index(orange, out_size, in_size) + # Let length = end_index - start_index, i.e. the length of the pooling kernels + # length.max() can be computed analytically as follows: + maxlength = in_size // out_size + 1 + in_size_mod = in_size % out_size + # adaptive = True iff there are kernels with different lengths + adaptive = not (in_size_mod == 0 or out_size % in_size_mod == 0) + if adaptive: + maxlength += 1 + elif in_size_mod == 0: + maxlength -= 1 + + range_max = torch.arange(maxlength, device=device, dtype=torch.int64) + idx = i0.unsqueeze(-1) + range_max + if adaptive: + # Need to clamp to avoid accessing out-of-bounds memory + # TODO make minimum accept scalars + maxval = torch.scalar_tensor( + in_size - 1, dtype=idx.dtype, device=idx.device + ) + idx = torch.minimum(idx, maxval) + + # Compute the length + i1 = end_index(orange, out_size, in_size) + length = i1 - i0 + else: + length = maxlength + return idx, length, range_max, adaptive + + # length is not None if it's constant, otherwise we'll need to compute it + idxh, length_h, range_max_h, adaptive_h = compute_idx(shape[-2], output_size[-2]) + idxw, length_w, range_max_w, adaptive_w = compute_idx(shape[-1], output_size[-1]) + + vals = input[..., _unsqueeze_to_dim(idxh, 4), idxw] + # Shortcut for the simpler case + if not adaptive_h and not adaptive_w: + return torch.mean(vals, dim=(-3, -1)) + + def maybe_mask(vals, length, range_max, adaptive, dim): + if isinstance(length, IntLike): + return vals, length + else: + # zero-out the things we didn't really want to select + if dim >= 0: + raise AssertionError(f"dim should be negative when masking, got {dim}") + # hack + mask = range_max >= length.unsqueeze(-1) + if dim == -2: + mask = _unsqueeze_to_dim(mask, 4) + vals = torch.masked_fill(vals, mask, 0.0) + # Compute the length of each window + length = _unsqueeze_to_dim(length, -dim) + return vals, length + + vals, length_h = maybe_mask( + vals, length_h, range_max_h, adaptive=adaptive_h, dim=-2 + ) + vals, length_w = maybe_mask( + vals, length_w, range_max_w, adaptive=adaptive_w, dim=-1 + ) + + # We unroll the sum as we assume that the kernels are going to be small + ret = None + for i, j in product(range(vals.shape[-3]), range(vals.shape[-1])): + if ret is None: + ret = vals[..., i, :, j] + else: + ret = ret + vals[..., i, :, j] + return ret / (length_h * length_w) + + +def _max_unpoolnd( + self: TensorLike, indices: TensorLike, output_size: list[int], dim: int +): + # If the input tensors self and indices came from max_pool call as + # required by the documentation, this operation is deterministic + # because that ensures that if there are two entries in `indices` + # tensor that are equal, the corresponding values in `self` are also + # equal. If this condition is not satisfied, the operation is + # non-deterministic as one of the different values in `self` 'wins'. + utils.alert_not_deterministic(f"max_unpooling{dim}d_forward_out") + output_shape = list(self.shape[:-dim]) + list(output_size) + if any(s == 0 for s in output_shape): + return self.new_zeros(output_shape) + nc = reduce(operator.mul, self.shape[:-dim]) + hw = reduce(operator.mul, output_size) + indices_nc_shape = [1] * self.ndim + indices_nc_shape[:-dim] = self.shape[:-dim] + indices_flat = ( + indices + aten.arange(nc, device=self.device).view(indices_nc_shape) * hw + ).reshape(-1) + + output = self.new_zeros(output_shape) + return aten._unsafe_index_put( + output.reshape(-1), [indices_flat], self.reshape(-1), accumulate=False + ).view(output.shape) + + +@register_decomposition(aten.max_unpool2d) +@out_wrapper() +def max_unpool2d( + self: TensorLike, + indices: TensorLike, + output_size: list[int], +): + torch._check( + indices.dtype == torch.int64, + lambda: f"elements in indices should be type int64 but got: {indices.dtype}", + ) + torch._check( + len(output_size) == 2, + lambda: ( + f"There should be exactly two elements (height, width) in output_size, " + f"but got {len(output_size)} elements." + ), + ) + + torch._check( + self.ndim in (3, 4), + lambda: ( + f"Input to max_unpooling2d should be a 3d or 4d Tensor, " + f"but got a tensor with {self.ndim} dimensions." + ), + ) + torch._check( + self.shape == indices.shape, + lambda: ( + f"Expected shape of indices to be same as that of the input tensor ({self.shape}) " + f"but got indices tensor with shape: {indices.shape}" + ), + ) + + for i in range(1, self.ndim): + torch._check( + self.size(i) > 0, + lambda: ( + f"max_unpooling2d(): " + f"Expected input to have non-zero size for non-batch dimensions, " + f"but got {self.shape} with dimension {i} being empty." + ), + ) + + return _max_unpoolnd(self, indices, output_size, 2) + + +@register_decomposition(aten.max_unpool3d) +@out_wrapper() +def max_unpool3d( + input: TensorLike, + indices: TensorLike, + output_size: list[int], + stride: list[int], + padding: list[int], +): + torch._check( + indices.dtype == torch.int64, lambda: "elements in indices should be type int64" + ) + torch._check( + input.ndim in (4, 5), + lambda: f"Input to max_unpooling3d should be a 4d or 5d Tensor, but got a tensor with {input.ndim} dimensions.", + ) + torch._check( + len(output_size) == 3, + lambda: ( + f"There should be exactly three elements (depth, height, width) in output_size, " + f"but got {len(output_size)} elements." + ), + ) + torch._check( + len(stride) == 3, + lambda: f"There should be exactly three elements (depth, height, width) in stride, but got: {len(stride)} elements.", + ) + torch._check( + len(padding) == 3, + lambda: f"There should be exactly three elements (depth, height, width) in padding, but got: {len(padding)} elements.", + ) + torch._check( + input.shape == indices.shape, + lambda: ( + f"Expected shape of indices to be same as that of the input tensor ({input.shape}) " + f"but got indices tensor with shape: {indices.shape}" + ), + ) + + for i in range(1, input.ndim): + torch._check( + input.size(i) > 0, + lambda: ( + f"max_unpooling3d(): " + f"Expected input to have non-zero size for non-batch dimensions, " + f"but got {input.shape} with dimension {i} being empty." + ), + ) + + torch._check( + stride[0] > 0 and stride[1] > 0 and stride[2] > 0, + lambda: f"strides should be greater than zero, but got stride: {stride}", + ) + + return _max_unpoolnd(input, indices, output_size, 3) + + +@register_decomposition(aten.index_add_) +def index_add_( + x: TensorLike, + dim: int, + index: TensorLike, + tensor: TensorLike, + *, + alpha: NumberType = 1, +): + return _index_add(x, dim, index, tensor, inplace=True, alpha=alpha) + + +@register_decomposition(aten.index_add) +@out_wrapper() +def index_add( + x: TensorLike, + dim: int, + index: TensorLike, + tensor: TensorLike, + *, + alpha: NumberType = 1, +): + return _index_add(x, dim, index, tensor, inplace=False, alpha=alpha) + + +def _index_add( + x: TensorLike, + dim: int, + index: TensorLike, + tensor: TensorLike, + *, + inplace: bool, + alpha: NumberType = 1, +): + dim = utils.canonicalize_dims(x.ndim, dim) + torch._check( + index.ndim <= 1, + lambda: f"Index should have dimension 1 or 0 (got {index.ndim})", + ) + index_size = index.size(0) if index.ndim == 1 else 1 + tensor_size = tensor.size(dim) if tensor.ndim > 0 else 1 + torch._check( + tensor_size == index_size, + lambda: f"Number of indices ({index_size}) should be equal to tensor.size(dim) ({tensor_size}), for {dim=}", + ) + if alpha != 1: + python_type = utils.dtype_to_type(x.dtype) + torch._check( + python_type is bool + or utils.is_weakly_lesser_type(type(alpha), python_type), + lambda: f"alpha argument of type {type(alpha)} cannot be safely cast to type {python_type}!", + ) + tensor = tensor * alpha + # Treat scalars as elements of \R^1 + zero_dim = x.ndim == 0 + x1 = x.unsqueeze(0) if zero_dim else x + idx = (None,) * dim + (index,) + index_put = aten.index_put_ if inplace else aten.index_put + out = index_put(x1, idx, tensor, accumulate=True) + if inplace: + return x + else: + return out.squeeze(0) if zero_dim else out.contiguous() + + +@register_decomposition(aten.pad_sequence.default) +@aten.pad_sequence.default.py_impl(DispatchKey.CompositeImplicitAutograd) +def pad_sequence(sequences, batch_first=False, padding_value=0.0): + torch._check(len(sequences) > 0, lambda: "received an empty list of sequences") + sequences_size = len(sequences) + max_size = sequences[0].size() + trailing_dims = max_size[1:] + max_len = max(x.size(0) for x in sequences) + if batch_first: + out_dims = (sequences_size, max_len) + else: + out_dims = (max_len, sequences_size) + out_dims = out_dims + trailing_dims + out = sequences[0].new_full(out_dims, padding_value) + dim_paddings = (0, 0) * len(trailing_dims) + for i in range(sequences_size): + currseq = sequences[i] + row = aten.constant_pad_nd( + currseq, dim_paddings + (0, max_len - currseq.size(0)), padding_value + ) + if batch_first: + out = aten.select_scatter(out, row, dim=0, index=i) + else: + out = aten.select_scatter(out, row, dim=1, index=i) + return out + + +@register_decomposition(aten.index_copy_) +def index_copy_(x: TensorLike, dim: int, index: TensorLike, tensor: TensorLike): + return _index_copy(x, dim, index, tensor, inplace=True) + + +@register_decomposition(aten.index_copy) +@out_wrapper() +def index_copy(x: TensorLike, dim: int, index: TensorLike, tensor: TensorLike): + return _index_copy(x, dim, index, tensor, inplace=False) + + +def _index_copy( + x: TensorLike, dim: int, index: TensorLike, tensor: TensorLike, *, inplace: bool +): + dim = utils.canonicalize_dims(x.ndim, dim) + torch._check( + index.ndim <= 1, + lambda: f"Index should have dimension 1 or 0 (got {index.ndim})", + ) + # Treat scalars as elements of \R^1 + zero_dim = x.ndim == 0 + x1 = x.unsqueeze(0) if zero_dim else x + index = index.unsqueeze(0) if index.ndim == 0 else index + idx = (None,) * dim + (index,) + index_put = aten.index_put_ if inplace else aten.index_put + out = index_put(x1, idx, tensor) + if inplace: + return x + else: + return out.squeeze(0) if zero_dim else out.contiguous() + + +# nb: Should use acc_t, not op_math +@register_decomposition(aten.log_sigmoid_forward) +@out_wrapper("output", "buffer") +@pw_cast_for_opmath +def log_sigmoid_forward(self: Tensor) -> tuple[Tensor, Tensor]: + min = torch.minimum(self.new_zeros(()), self) + z = torch.exp(-torch.abs(self)) + if self.is_cuda or self.is_xpu: + buffer = self.new_zeros((0,)) + else: + buffer = z + return min - torch.log1p(z), buffer + + +@register_decomposition(aten.uniform) +@out_wrapper() +def uniform( + x: Tensor, + low: bool | int | float = 0.0, + high: bool | int | float = 1.0, + generator: torch.Generator | None = None, +): + return prims._uniform_helper( + x.shape, + stride=x.stride(), + low=sym_float(low), + high=sym_float(high), + dtype=x.dtype, + device=x.device, + generator=generator, + ) + + +@register_decomposition(aten.uniform_) +def uniform_(self, low=0, high=1, generator=None): + return self.copy_(uniform(self, low, high, generator)) + + +# aten/src/ATen/native/UpSample.cpp compute_output_size +def upsample_compute_output_size(input_size, output_size, scale_factors): + spatial_dimensions = len(input_size) - 2 + if output_size is not None: + torch._check( + scale_factors is None, + lambda: "Must specify exactly one of output_size and scale_factors", + ) + torch._check(len(output_size) == spatial_dimensions, lambda: "") + return output_size + if scale_factors is not None: + # NB: this isn't necessary lol + torch._check( + output_size is None, + lambda: "Must specify exactly one of output_size and scale_factors", + ) + torch._check(len(scale_factors) == spatial_dimensions, lambda: "") + output_size = [] + for i, s in enumerate(scale_factors): + if int(s) == s: + output_size.append(input_size[i + 2] * int(s)) + else: + output_size.append(sym_int(input_size[i + 2] * s)) + return output_size + torch._check( + False, lambda: "Must specify exactly one of output_size and scale_factors" + ) + + +def get_scale_value(scales, idx): + if scales is None: + return None + return scales[idx] + + +@register_decomposition(aten.upsample_nearest1d.vec) +@register_decomposition(aten.upsample_nearest2d.vec) +@register_decomposition(aten.upsample_nearest3d.vec) +@aten.upsample_nearest1d.vec.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten.upsample_nearest1d.vec.py_impl(DispatchKey.Autograd) +@aten.upsample_nearest2d.vec.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten.upsample_nearest2d.vec.py_impl(DispatchKey.Autograd) +@aten.upsample_nearest3d.vec.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten.upsample_nearest3d.vec.py_impl(DispatchKey.Autograd) +def _upsample_nearest_vec( + input: Tensor, + output_size: list[int] | None, + scale_factors: list[float] | None, +) -> Tensor: + osize = upsample_compute_output_size(input.size(), output_size, scale_factors) + scales = ( + scale_factors if scale_factors else [None] * len(osize) # type: ignore[list-item] + ) + return _upsample_nearest(input, osize, scales) + + +@register_decomposition(aten._upsample_nearest_exact1d.vec) +@register_decomposition(aten._upsample_nearest_exact2d.vec) +@register_decomposition(aten._upsample_nearest_exact3d.vec) +@aten._upsample_nearest_exact1d.vec.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten._upsample_nearest_exact1d.vec.py_impl(DispatchKey.Autograd) +@aten._upsample_nearest_exact2d.vec.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten._upsample_nearest_exact2d.vec.py_impl(DispatchKey.Autograd) +@aten._upsample_nearest_exact3d.vec.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten._upsample_nearest_exact3d.vec.py_impl(DispatchKey.Autograd) +def _upsample_nearest_exact_vec( + input: Tensor, + output_size: list[int] | None, + scale_factors: list[float] | None, +) -> Tensor: + osize = upsample_compute_output_size(input.size(), output_size, scale_factors) + scales = ( + scale_factors if scale_factors else [None] * len(osize) # type: ignore[list-item] + ) + return _upsample_nearest(input, osize, scales, exact=True) + + +def _compute_upsample_nearest_indices(input, output_size, scales, exact=False): + # For each dim in output_size, compute the set of input indices used + # to produce the upsampled output. + indices = [] + num_spatial_dims = len(output_size) + offset = 0.5 if exact else 0.0 + + for d in range(num_spatial_dims): + # Math matches aten/src/ATen/native/cpu/UpSampleKernel.cpp + # + # Indices are computed as following: + # scale = isize / osize + # Case: exact=False + # input_index = floor(output_index * scale) + # Same as OpenCV INTER_NEAREST + # + # Case: exact=False + # index_f32 = (output_index + 0.5) * scale - 0.5 + # input_index = round(index_f32) + # Same as Pillow and Scikit-Image/Scipy ndi.zoom + osize = output_size[d] + isize = input.shape[-num_spatial_dims + d] + + # check for scales[d] > 0 is in compute_scales_value in aten/src/ATen/native/UpSample.h + scale = ( + isize / (isize * scales[d]) + if scales[d] is not None and scales[d] > 0 + else isize / osize + ) + + output_indices = torch.arange(osize, dtype=torch.float32, device=input.device) + input_indices = ((output_indices + offset) * scale).to(torch.int64) + for _ in range(num_spatial_dims - 1 - d): + input_indices = input_indices.unsqueeze(-1) + indices.append(input_indices) + return indices + + +@register_decomposition([aten.upsample_nearest1d.default, aten.upsample_nearest1d.out]) +@aten.upsample_nearest1d.default.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten.upsample_nearest1d.default.py_impl(DispatchKey.Autograd) +@out_wrapper(preserve_memory_format=True, exact_dtype=True) +def upsample_nearest1d( + input: Tensor, + output_size: list[int], + scales: float | None = None, +) -> Tensor: + return _upsample_nearest(input, output_size, [scales]) + + +@register_decomposition( + [aten._upsample_nearest_exact1d.default, aten._upsample_nearest_exact1d.out] +) +@aten._upsample_nearest_exact1d.default.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten._upsample_nearest_exact1d.default.py_impl(DispatchKey.Autograd) +@out_wrapper(preserve_memory_format=True, exact_dtype=True) +def upsample_nearest_exact1d( + input: Tensor, + output_size: list[int], + scales: float | None = None, +) -> Tensor: + return _upsample_nearest(input, output_size, [scales], exact=True) + + +@register_decomposition([aten.upsample_nearest2d.default, aten.upsample_nearest2d.out]) +@aten.upsample_nearest2d.default.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten.upsample_nearest2d.default.py_impl(DispatchKey.Autograd) +@out_wrapper(preserve_memory_format=True, exact_dtype=True) +def upsample_nearest2d( + input: Tensor, + output_size: list[int], + scales_h: float | None = None, + scales_w: float | None = None, +) -> Tensor: + return _upsample_nearest(input, output_size, [scales_h, scales_w]) + + +@register_decomposition( + [aten._upsample_nearest_exact2d.default, aten._upsample_nearest_exact2d.out] +) +@aten._upsample_nearest_exact2d.default.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten._upsample_nearest_exact2d.default.py_impl(DispatchKey.Autograd) +@out_wrapper(preserve_memory_format=True, exact_dtype=True) +def _upsample_nearest_exact2d( + input: Tensor, + output_size: list[int], + scales_h: float | None = None, + scales_w: float | None = None, +) -> Tensor: + return _upsample_nearest(input, output_size, [scales_h, scales_w], exact=True) + + +@register_decomposition([aten.upsample_nearest3d.default, aten.upsample_nearest3d.out]) +@aten.upsample_nearest3d.default.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten.upsample_nearest3d.default.py_impl(DispatchKey.Autograd) +@out_wrapper(preserve_memory_format=True, exact_dtype=True) +def upsample_nearest3d( + input: Tensor, + output_size: list[int], + scales_d: float | None = None, + scales_h: float | None = None, + scales_w: float | None = None, +) -> Tensor: + return _upsample_nearest(input, output_size, [scales_d, scales_h, scales_w]) + + +@register_decomposition( + [aten._upsample_nearest_exact3d.default, aten._upsample_nearest_exact3d.out] +) +@aten._upsample_nearest_exact3d.default.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten._upsample_nearest_exact3d.default.py_impl(DispatchKey.Autograd) +@out_wrapper(preserve_memory_format=True, exact_dtype=True) +def _upsample_nearest_exact3d( + input: Tensor, + output_size: list[int], + scales_d: float | None = None, + scales_h: float | None = None, + scales_w: float | None = None, +) -> Tensor: + return _upsample_nearest( + input, output_size, [scales_d, scales_h, scales_w], exact=True + ) + + +@pw_cast_for_opmath +def _upsample_nearest( + input: Tensor, + output_size: list[int], + scales: list[float | None], + exact: bool = False, +) -> Tensor: + spatial_indices = _compute_upsample_nearest_indices( + input, output_size, scales, exact=exact + ) + + indices = [None, None] + spatial_indices + result = aten._unsafe_index(input, indices) + + if result.ndim == 4: + # convert output to correct memory format, if necessary + memory_format = utils.suggest_memory_format(input) + + # following "heuristic: only use channels_last path when it's faster than the contiguous path" + n_channels = input.shape[1] + if input.device.type == "cuda" and n_channels < 4: + memory_format = torch.contiguous_format + + result = result.contiguous(memory_format=memory_format) + return result + + +def gather_params(params, has_biases, has_projections): + if has_biases and has_projections: + group_size = 5 + elif has_biases: + group_size = 4 + elif has_projections: + group_size = 3 + else: + group_size = 2 + + if len(params) % group_size != 0: + raise AssertionError( + f"len(params)={len(params)} is not divisible by group_size={group_size}" + ) + return [ + tuple(params[i : i + group_size]) for i in range(0, len(params), group_size) + ] + + +def params_hiddens(params, hiddens, i, bidirectional): + if bidirectional: + cur_params, cur_hidden = params[2 * i], hiddens[2 * i] + bidir_params, bidir_hidden = params[2 * i + 1], hiddens[2 * i + 1] + else: + cur_params, cur_hidden = params[i], hiddens[i] + bidir_params, bidir_hidden = None, None + + return cur_params, cur_hidden, bidir_params, bidir_hidden + + +def update_hidden_for_packed(cur_hidden, last_batch_size, batch_size, hiddens): + if last_batch_size <= batch_size: + raise AssertionError( + f"last_batch_size ({last_batch_size}) must be > batch_size ({batch_size})" + ) + hiddens.append(cur_hidden.narrow(0, batch_size, last_batch_size - batch_size)) + return cur_hidden.narrow(0, 0, batch_size) + + +def update_hidden_for_packed_reverse( + cur_hidden, last_batch_size, batch_size, inp_hidden +): + if last_batch_size == batch_size: + return cur_hidden + if last_batch_size >= batch_size: + raise AssertionError( + f"last_batch_size ({last_batch_size}) must be < batch_size ({batch_size})" + ) + return torch.concat( + ( + cur_hidden, + inp_hidden.narrow(0, last_batch_size, batch_size - last_batch_size), + ) + ) + + +def one_layer_rnn_data( + inp, hidden, params, has_biases, hidden_fn, batch_sizes, reverse=False +): + ih_weight = params[0] + hh_weight = params[1] + ih_bias = params[2] if has_biases else None + hh_bias = params[3] if has_biases else None + + step_output = [] + hiddens: list[torch.Tensor] = [] + + last_batch_size = batch_sizes[-1] if reverse else batch_sizes[0] + cur_hidden = hidden.narrow(0, 0, last_batch_size) + split_inp = torch.split(inp, list(batch_sizes)) + if reverse: + split_inp = split_inp[::-1] + for inp in split_inp: + i = inp.shape[0] + + if last_batch_size == i: + pass # don't update cur_hidden + # this will only happen when reverse=False, since batch sizes are sorted largest -> smallest + elif reverse: + cur_hidden = update_hidden_for_packed_reverse( + cur_hidden, last_batch_size, i, hidden + ) + else: + cur_hidden = update_hidden_for_packed( + cur_hidden, last_batch_size, i, hiddens + ) + + cur_hidden = hidden_fn(inp, cur_hidden, ih_weight, ih_bias, hh_weight, hh_bias) + last_batch_size = i + step_output.append(cur_hidden) + + if reverse: + step_output.reverse() + else: + hiddens.append(cur_hidden) + hiddens.reverse() + + out = torch.cat(step_output, 0) + hidden_out = torch.cat(hiddens, 0) if not reverse else cur_hidden + return out, hidden_out + + +def rnn_cell(nonlinearity): + def inner(i, cur_hidden, ih_weight, ih_bias, hh_weight, hh_bias): + return nonlinearity(F.linear(cur_hidden, hh_weight, hh_bias) + i) + + return inner + + +def rnn_cell_data(nonlinearity): + def inner(i, cur_hidden, ih_weight, ih_bias, hh_weight, hh_bias): + i = F.linear(i, ih_weight, ih_bias) + return nonlinearity(F.linear(cur_hidden, hh_weight, hh_bias) + i) + + return inner + + +def one_layer_rnn(inp, hidden, params, has_biases, hidden_fn, reverse=False): + ih_weight = params[0] + hh_weight = params[1] + ih_bias = params[2] if has_biases else None + hh_bias = params[3] if has_biases else None + + precomputed_input = F.linear(inp, ih_weight, ih_bias) + precomputed_input = precomputed_input.flip(0) if reverse else precomputed_input + cur_hidden = hidden.unsqueeze(0) + step_output = [] + for i in precomputed_input: + cur_hidden = hidden_fn(i, cur_hidden, ih_weight, ih_bias, hh_weight, hh_bias) + step_output.append(cur_hidden) + + if reverse: + step_output.reverse() + + out = torch.cat(step_output, 0) + + return out, cur_hidden.squeeze(0) + + +def mkldnn_one_layer_lstm(inp, hidden, params, has_biases, reverse=False): + w0 = params[0] + w1 = params[1] + if has_biases: + w2 = params[2] + w3 = params[3] + else: + w2 = torch.zeros(w0.size()) + w3 = torch.zeros(w1.size()) + + hx = hidden[0].unsqueeze(0) + cx = hidden[1].unsqueeze(0) + + batch_sizes: list[int] = [] + mode = 2 # third_party/ideep/include/ideep/abstract_types.hpp: ideep::rnn_kind::LSTM = 2 + hidden_size = hx.size(2) + num_layers = 1 + + # _rnn_helper already handles bidirectional and batch_first so we hard-code them to False here + bidirectional = False + batch_first = False + + train = False + # If batch_first, inp has been permuted in _rnn_helper. Convert to contiguous here. + # Same as aten/src/ATen/native/mkldnn/RNN.cpp: mkldnn_rnn: input = input.contiguous(); + inp = inp.contiguous() + hx = hx.contiguous() + cx = cx.contiguous() + outputs = torch.ops.aten.mkldnn_rnn_layer.default( + inp, + w0, + w1, + w2, + w3, + hx, + cx, + reverse, + batch_sizes, + mode, + hidden_size, + num_layers, + has_biases, + bidirectional, + batch_first, + train, + ) + y, hy, cy = outputs[0], outputs[1], outputs[2] + return y, (hy.squeeze(0), cy.squeeze(0)) + + +def _rnn_helper( + input, + hidden, + params, + has_biases, + num_layers, + dropout, + train, + bidirectional, + batch_first, + layer_fn, +): + input = input.transpose(0, 1) if batch_first else input + final_hiddens = [] + + for i in range(num_layers): + cur_params, cur_hidden, bidir_params, bidir_hidden = params_hiddens( + params, hidden, i, bidirectional + ) + dropout = dropout if (train and num_layers < i - 1) else 0.0 + fwd_inp, fwd_hidden = layer_fn(input, cur_hidden, cur_params, has_biases) + final_hiddens.append(fwd_hidden) + + if bidirectional: + bwd_inp, bwd_hidden = layer_fn( + input, bidir_hidden, bidir_params, has_biases, reverse=True + ) + final_hiddens.append(bwd_hidden) + + if bidirectional: + input = torch.cat([fwd_inp, bwd_inp], fwd_inp.dim() - 1) # type: ignore[possibly-undefined] + else: + input = fwd_inp + + if dropout != 0 and train and i < num_layers - 1: + input = torch.dropout(input, dropout, train=True) + + input = input.transpose(0, 1) if batch_first else input + return input, final_hiddens + + +@register_decomposition(aten.rnn_tanh.input) +@aten.rnn_tanh.input.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten.rnn_tanh.input.py_impl(DispatchKey.Autograd) +def rnn_tanh_input( + input, + hx, + params, + has_biases, + num_layers, + dropout, + train, + bidirectional, + batch_first, +): + hidden = hx.unbind(0) + params = gather_params(params, has_biases, False) + out, final_hiddens = _rnn_helper( + input, + hidden, + params, + has_biases, + num_layers, + dropout, + train, + bidirectional, + batch_first, + partial(one_layer_rnn, hidden_fn=rnn_cell(torch.tanh)), + ) + return out, torch.stack(final_hiddens, 0) + + +@register_decomposition(aten.rnn_relu.input) +@aten.rnn_relu.input.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten.rnn_relu.input.py_impl(DispatchKey.Autograd) +def rnn_relu_input( + input, + hx, + params, + has_biases, + num_layers, + dropout, + train, + bidirectional, + batch_first, +): + hidden = hx.unbind(0) + params = gather_params(params, has_biases, False) + out, final_hiddens = _rnn_helper( + input, + hidden, + params, + has_biases, + num_layers, + dropout, + train, + bidirectional, + batch_first, + partial(one_layer_rnn, hidden_fn=rnn_cell(torch.relu)), + ) + return out, torch.stack(final_hiddens, 0) + + +@register_decomposition(aten.rnn_relu.data) +@aten.rnn_relu.data.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten.rnn_relu.data.py_impl(DispatchKey.Autograd) +def rnn_relu_data( + data, + batch_sizes, + hx, + params, + has_biases, + num_layers, + dropout, + train, + bidirectional, +): + hidden = hx.unbind(0) + params = gather_params(params, has_biases, False) + out, final_hiddens = _rnn_helper( + data, + hidden, + params, + has_biases, + num_layers, + dropout, + train, + bidirectional, + False, + partial( + one_layer_rnn_data, + batch_sizes=batch_sizes, + hidden_fn=rnn_cell_data(torch.relu), + ), + ) + return out, torch.stack(final_hiddens, 0) + + +@register_decomposition(aten.rnn_tanh.data) +@aten.rnn_tanh.data.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten.rnn_tanh.data.py_impl(DispatchKey.Autograd) +def rnn_tanh_data( + data, + batch_sizes, + hx, + params, + has_biases, + num_layers, + dropout, + train, + bidirectional, +): + hidden = hx.unbind(0) + params = gather_params(params, has_biases, False) + out, final_hiddens = _rnn_helper( + data, + hidden, + params, + has_biases, + num_layers, + dropout, + train, + bidirectional, + False, + partial( + one_layer_rnn_data, + batch_sizes=batch_sizes, + hidden_fn=rnn_cell_data(torch.tanh), + ), + ) + return out, torch.stack(final_hiddens, 0) + + +def lstm_cell(inp, hx, cx, hh_weight, hh_bias, hr_weight, chunk_dim): + gates = F.linear(hx, hh_weight, hh_bias) + inp + chunked_gates = gates.chunk(4, chunk_dim) + in_gate = chunked_gates[0].sigmoid() + forget_gate = chunked_gates[1].sigmoid() + cell_gate = chunked_gates[2].tanh() + out_gate = chunked_gates[3].sigmoid() + cy = forget_gate * cx + (in_gate * cell_gate) + hy = out_gate * cy.tanh() + hy = hy if hr_weight is None else F.linear(hy, hr_weight, None) + + return hy, cy + + +def one_layer_lstm(inp, hidden, params, has_biases, reverse=False): + ih_weight = params[0] + hh_weight = params[1] + ih_bias = params[2] if has_biases else None + hh_bias = params[3] if has_biases else None + hr_weight = ( + params[4] if len(params) == 5 else params[2] if len(params) == 3 else None + ) + + hx = hidden[0].unsqueeze(0) + cx = hidden[1].unsqueeze(0) + + precomputed_input = F.linear(inp, ih_weight, ih_bias) + precomputed_input = precomputed_input.flip(0) if reverse else precomputed_input + step_output = [] + for inp in precomputed_input: + hx, cx = lstm_cell(inp, hx, cx, hh_weight, hh_bias, hr_weight, chunk_dim=2) + step_output.append(hx) + + if reverse: + step_output.reverse() + + out = torch.cat(step_output, 0) + + return out, (hx.squeeze(1), cx.squeeze(1)) + + +def one_layer_lstm_data(inp, hidden, params, has_biases, batch_sizes, reverse=False): + ih_weight = params[0] + hh_weight = params[1] + ih_bias = params[2] if has_biases else None + hh_bias = params[3] if has_biases else None + hr_weight = ( + params[4] if len(params) == 5 else params[2] if len(params) == 3 else None + ) + + step_output = [] + hiddens = [] + + last_batch_size = batch_sizes[-1] if reverse else batch_sizes[0] + split_inp = torch.split(inp, list(batch_sizes)) + if reverse: + split_inp = split_inp[::-1] + + orig_hx = hidden[0] + orig_cx = hidden[1] + hx, cx = ( + orig_hx.narrow(0, 0, last_batch_size), + orig_cx.narrow(0, 0, last_batch_size), + ) + + for inp in split_inp: + i = inp.shape[0] + inp = F.linear(inp, ih_weight, ih_bias) + + # this will only happen when reverse=False, since batch sizes are sorted largest -> smallest + if i < last_batch_size: + hiddens.append( + ( + hx.narrow(0, i, last_batch_size - i), + cx.narrow(0, i, last_batch_size - i), + ) + ) + hx, cx = hx.narrow(0, 0, i), cx.narrow(0, 0, i) + + # this will only happen when reverse=True + if i > last_batch_size: + hx = torch.concat( + (hx, orig_hx.narrow(0, last_batch_size, i - last_batch_size)), 0 + ) + cx = torch.concat( + (cx, orig_cx.narrow(0, last_batch_size, i - last_batch_size)), 0 + ) + + hx, cx = lstm_cell(inp, hx, cx, hh_weight, hh_bias, hr_weight, chunk_dim=1) + last_batch_size = i + step_output.append(hx) + + if reverse: + step_output.reverse() + hidden_out = (hx, cx) + else: + hiddens.append((hx, cx)) + hiddens.reverse() + hidden0, hidden1 = zip(*hiddens) + hidden_out = torch.cat(hidden0, 0), torch.cat(hidden1, 0) + + out = torch.cat(step_output, 0) + return out, hidden_out + + +def select_one_layer_lstm_function(input, hx, params): + r"""Check whether we could use decompose lstm with mkldnn_rnn_layer. + All the below conditions need to be met: + * ``torch._C._get_mkldnn_enabled()`` returns ``True``. + * All the input args are on CPU. + * The dtypes of args are either torch.float or torch.bfloat16. + * Inference. + * ``has_projections`` returns ``False``. + + Args: + * input: the input sequence to LSTM + * hx: a tuple of the input hidden state and cell state ``(h_0, c_0)`` to LSTM + * params: the weight and bias tensors of LSTM + """ + + def use_mkldnn(input, hx, params): + if not torch._C._get_mkldnn_enabled(): + return False + + tensors = [input] + list(hx) + list(chain.from_iterable(params)) + devices = {t.device for t in tensors} + if len(devices) != 1: + return False + + device = devices.pop() + if device != torch.device("cpu"): + return False + # With autocast, possible to have mixed dtype here + dtypes = {t.dtype for t in tensors} + for dtype in dtypes: + if dtype not in [torch.float, torch.bfloat16]: + return False + + if input.requires_grad: + return False + + has_projections = hx[0].size(2) != hx[1].size(2) + if has_projections: + return False + + return True + + # mkldnn_one_layer_lstm does not depend on seq_len while one_layer_lstm + # will expand over the seq_len dim + if use_mkldnn(input, hx, params): + return mkldnn_one_layer_lstm + else: + return one_layer_lstm + + +@register_decomposition(aten.lstm.input) +@aten.lstm.input.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten.lstm.input.py_impl(DispatchKey.Autograd) +def lstm_impl( + input, + hx, + params, + has_biases, + num_layers, + dropout, + train, + bidirectional, + batch_first, +): + if len(hx) != 2: + raise AssertionError(f"lstm expects two hidden states, got {len(hx)}") + params = gather_params(params, has_biases, hx[0].size(2) != hx[1].size(2)) + hidden = list(zip(hx[0], hx[1])) + layer_fn = select_one_layer_lstm_function(input, hx, params) + out, final_hiddens = _rnn_helper( + input, + hidden, + params, + has_biases, + num_layers, + dropout, + train, + bidirectional, + batch_first, + layer_fn, + ) + final_hiddens = list(zip(*final_hiddens)) + return out, torch.stack(final_hiddens[0], 0), torch.stack(final_hiddens[1], 0) + + +@register_decomposition(aten.lstm.data) +@aten.lstm.data.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten.lstm.data.py_impl(DispatchKey.Autograd) +def lstm_data_impl( + data, + batch_sizes, + hx, + params, + has_biases, + num_layers, + dropout, + train, + bidirectional, +): + if len(hx) != 2: + raise AssertionError(f"lstm expects two hidden states, got {len(hx)}") + params = gather_params(params, has_biases, hx[0].size(2) != hx[1].size(2)) + hidden = list(zip(hx[0], hx[1])) + out, final_hiddens = _rnn_helper( + data, + hidden, + params, + has_biases, + num_layers, + dropout, + train, + bidirectional, + False, + partial(one_layer_lstm_data, batch_sizes=batch_sizes), + ) + final_hiddens = list(zip(*final_hiddens)) + return out, torch.stack(final_hiddens[0], 0), torch.stack(final_hiddens[1], 0) + + +def gru_cell(inp, cur_hidden, ih_weight, ih_bias, hh_weight, hh_bias): + chunked_igates = inp.chunk(3, 1) + chunked_hgates = F.linear(cur_hidden, hh_weight, hh_bias).chunk(3, 2) + reset_gate = (chunked_hgates[0] + chunked_igates[0]).sigmoid() + input_gate = (chunked_hgates[1] + chunked_igates[1]).sigmoid() + new_gate = (chunked_igates[2] + (chunked_hgates[2] * reset_gate)).tanh() + return (cur_hidden - new_gate) * input_gate + new_gate + + +def gru_cell_data(inp, cur_hidden, ih_weight, ih_bias, hh_weight, hh_bias): + chunked_igates = F.linear(inp, ih_weight, ih_bias).chunk(3, 1) + chunked_hgates = F.linear(cur_hidden, hh_weight, hh_bias).chunk(3, 1) + reset_gate = (chunked_hgates[0] + chunked_igates[0]).sigmoid() + input_gate = (chunked_hgates[1] + chunked_igates[1]).sigmoid() + new_gate = (chunked_igates[2] + (chunked_hgates[2] * reset_gate)).tanh() + return (cur_hidden - new_gate) * input_gate + new_gate + + +@register_decomposition(aten.gru.data) +@aten.gru.data.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten.gru.data.py_impl(DispatchKey.Autograd) +def gru_impl_data( + data, + batch_sizes, + hx, + params, + has_biases, + num_layers, + dropout, + train, + bidirectional, +): + params = gather_params(params, has_biases, False) + out, final_hiddens = _rnn_helper( + data, + hx.unbind(0), + params, + has_biases, + num_layers, + dropout, + train, + bidirectional, + False, + partial(one_layer_rnn_data, batch_sizes=batch_sizes, hidden_fn=gru_cell_data), + ) + return out, torch.stack(final_hiddens, 0) + + +@register_decomposition(aten.gru.input) +@aten.gru.input.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten.gru.input.py_impl(DispatchKey.Autograd) +def gru_impl( + input, + hx, + params, + has_biases, + num_layers, + dropout, + train, + bidirectional, + batch_first, +): + params = gather_params(params, has_biases, False) + out, final_hiddens = _rnn_helper( + input, + hx.unbind(0), + params, + has_biases, + num_layers, + dropout, + train, + bidirectional, + batch_first, + partial(one_layer_rnn, hidden_fn=gru_cell), + ) + return out, torch.stack(final_hiddens, 0) + + +@register_decomposition(aten._upsample_bilinear2d_aa.vec) +@aten._upsample_bilinear2d_aa.vec.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten._upsample_bilinear2d_aa.vec.py_impl(DispatchKey.Autograd) +def upsample_bilinear2d_aa_vec(input, output_size, align_corners, scale_factors): + osize = upsample_compute_output_size(input.size(), output_size, scale_factors) + scale_h = get_scale_value(scale_factors, 0) + scale_w = get_scale_value(scale_factors, 1) + return torch.ops.aten._upsample_bilinear2d_aa( + input, osize, align_corners, scale_h, scale_w + ) + + +@register_decomposition(aten._upsample_bicubic2d_aa.vec) +@aten._upsample_bicubic2d_aa.vec.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten._upsample_bicubic2d_aa.vec.py_impl(DispatchKey.Autograd) +def upsample_bicubic2d_aa_vec(input, output_size, align_corners, scale_factors): + osize = upsample_compute_output_size(input.size(), output_size, scale_factors) + scale_h = get_scale_value(scale_factors, 0) + scale_w = get_scale_value(scale_factors, 1) + return torch.ops.aten._upsample_bicubic2d_aa( + input, osize, align_corners, scale_h, scale_w + ) + + +@register_decomposition(aten._upsample_lanczos2d_aa.vec) +@aten._upsample_lanczos2d_aa.vec.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten._upsample_lanczos2d_aa.vec.py_impl(DispatchKey.Autograd) +def upsample_lanczos2d_aa_vec(input, output_size, align_corners, scale_factors): + osize = upsample_compute_output_size(input.size(), output_size, scale_factors) + scale_h = get_scale_value(scale_factors, 0) + scale_w = get_scale_value(scale_factors, 1) + return torch.ops.aten._upsample_lanczos2d_aa( + input, osize, align_corners, scale_h, scale_w + ) + + +@register_decomposition(aten.upsample_bilinear2d.vec) +@register_decomposition(aten.upsample_trilinear3d.vec) +@aten.upsample_linear1d.vec.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten.upsample_linear1d.vec.py_impl(DispatchKey.Autograd) +@aten.upsample_bilinear2d.vec.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten.upsample_bilinear2d.vec.py_impl(DispatchKey.Autograd) +@aten.upsample_trilinear3d.vec.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten.upsample_trilinear3d.vec.py_impl(DispatchKey.Autograd) +def _upsample_linear_vec(input, output_size, align_corners, scale_factors): + osize = upsample_compute_output_size(input.size(), output_size, scale_factors) + scales = scale_factors if scale_factors else [None] * len(osize) + return _upsample_linear(input, osize, align_corners, scales) + + +@register_decomposition([aten.upsample_linear1d.default, aten.upsample_linear1d.out]) +@out_wrapper() +def upsample_linear1d( + input: Tensor, + output_size: list[int], + align_corners: bool, + scales_w: float | None = None, +) -> Tensor: + return _upsample_linear(input, output_size, align_corners, [scales_w]) + + +@register_decomposition( + [aten.upsample_bilinear2d.default, aten.upsample_bilinear2d.out] +) +@aten.upsample_bilinear2d.default.py_impl(DispatchKey.Autograd) +@out_wrapper() +def upsample_bilinear2d( + input: Tensor, + output_size: list[int], + align_corners: bool, + scales_h: float | None = None, + scales_w: float | None = None, +) -> Tensor: + return _upsample_linear(input, output_size, align_corners, [scales_h, scales_w]) + + +@register_decomposition( + [aten.upsample_trilinear3d.default, aten.upsample_trilinear3d.out] +) +@out_wrapper() +def upsample_trilinear3d( + input: Tensor, + output_size: list[int], + align_corners: bool, + scales_d: float | None = None, + scales_h: float | None = None, + scales_w: float | None = None, +) -> Tensor: + return _upsample_linear( + input, output_size, align_corners, [scales_d, scales_h, scales_w] + ) + + +def _compute_scale(in_size, out_size, align_corners, scale=None): + if align_corners: + return (in_size - 1.0) / (out_size - 1.0) if out_size > 1 else 0 + else: + return 1.0 / scale if scale is not None and scale > 0 else in_size / out_size + + +def _compute_source_index(scale, dst_index, align_corners): + if align_corners: + return scale * dst_index + else: + return scale * (dst_index + 0.5) - 0.5 + + +def _sum_tensors_uint8( + src: Iterable[Tensor], weights: Iterable[Tensor], weights_precision: Tensor +) -> Tensor: + output = _sum_tensors( + s.to(torch.int32) * c.to(torch.int32) for s, c in zip(src, weights) + ) + (1 << (weights_precision - 1)) + output = output >> weights_precision + return torch.clamp(output, 0, 255).to(torch.uint8) + + +def _compute_weight_precision(weights: TensorSequenceType) -> Tensor: + max_weight = torch.stack(weights).max() + max_weight_precision = 22 + precisions = torch.arange(max_weight_precision, device=max_weight.device) + values = 0.5 + max_weight * (1 << (precisions + 1)) + mask = values >= (1 << 15) + return max_weight_precision - mask.sum() + + +@pw_cast_for_opmath +def _upsample_linear( + input: Tensor, + output_size: list[int], + align_corners: bool, + scales: list[float | None], +) -> Tensor: + # get dimensions of original image + n_channels = input.shape[1] + inp_sizes = input.shape[2:] + n_dims = len(inp_sizes) + + _, dtype = utils.elementwise_dtypes( + input, + type_promotion_kind=utils.ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + ) + + def get_values(inp_size, out_size, scales, nsqueeze): + # First Calculate scaling factor + scale_factor = _compute_scale(inp_size, out_size, align_corners, scales) + # We have to create arange with int64 dtype and use .to in order to avoid + # additional kernels creation in inductor and get a perf slowdown + i = torch.arange(out_size, device=input.device).to(dtype=dtype) + + x_f32 = _compute_source_index(scale_factor, i, align_corners).clamp(min=0.0) + x_f32 = x_f32.reshape(x_f32.shape[0], *[1] * (nsqueeze)) + x = x_f32.to(torch.int64) + xp1 = (x + 1).clamp(max=inp_size - 1) + return x_f32, x, xp1 + + values = [ + get_values(inp_size, out_size, scales, n_dims - 1 - i) + for i, (inp_size, out_size, scales) in enumerate( + zip(inp_sizes, output_size, scales) + ) + ] + xs_f32, xs, xp1s = list(zip(*values)) + + vs = [] + for a in product(*[[0, 1]] * n_dims): + idx = [None, None] + [xs[k] if a[k] == 0 else xp1s[k] for k in range(n_dims)] + v = aten._unsafe_index(input, idx) + v = _maybe_convert_to_dtype(v, dtype) + vs.append(v) + + for i in reversed(range(n_dims)): + xscale = (xs_f32[i] - xs[i]).clamp(0.0, 1.0).to(dtype) + vs = [ + # x1 * (1 - alpha) + x2 * alpha == x1 + (x2 - x1) * alpha + v1 + torch.mul(v2 - v1, xscale) + for v1, v2 in zip(vs[::2], vs[1::2]) + ] + + if len(vs) != 1: + raise AssertionError(f"Expected vs to have exactly 1 element, got {len(vs)}") + result = vs[0] + + # convert output to correct memory format, if necessary + memory_format = utils.suggest_memory_format(input) + + # following "heuristic: only use channels_last path when it's faster than the contiguous path" + if input.device.type == "cuda" and n_channels < 16: + memory_format = torch.contiguous_format + + if not isinstance(result, torch.Tensor): + raise AssertionError( + f"Expected result to be a Tensor, got {type(result).__name__}" + ) + + result = result.contiguous(memory_format=memory_format) + + if not input.is_floating_point(): + result = result.round() + + return result + + +# We should be applying decompositions after all transformations +@register_decomposition(aten.is_same_size.default) +def is_same_size(a: Tensor, b: Tensor) -> bool: + return a.shape == b.shape + + +@register_decomposition([aten._reshape_alias, aten._unsafe_view]) +@out_wrapper() +def _reshape_alias(x, shape, *args): + return aten.view(x, shape) + + +@register_decomposition([aten._unsafe_index]) +def _unsafe_index(x, indices): + return aten.index(x, indices) + + +@register_decomposition([aten._unsafe_index_put]) +def _unsafe_index_put(x, indices, value, accumulate=False): + return aten.index_put(x, indices, value, accumulate) + + +@register_decomposition([aten._unsafe_masked_index]) +def _unsafe_masked_index(x, mask, indices, fill): + for index in indices: + if index is not None: + torch._check( + index.dtype in [torch.long, torch.int], + lambda: "tensors used as indices must be long or int tensors", + ) + + torch._check( + mask.dtype == torch.bool, + lambda: "tensors used as masks must be bool tensors", + ) + + from torch.fx.experimental.symbolic_shapes import guard_or_false + + if guard_or_false(x.numel() == 0): + meta_result = torch._meta_registrations.meta_index_Tensor(x, indices) + return x.new_full(meta_result.shape, fill) + + for i in range(len(indices)): + index = indices[i] + if index is not None: + indices[i] = index.clamp(min=0, max=x.size(i) - 1) + + return aten._unsafe_index(x, indices).masked_fill(~mask, fill) + + +@register_decomposition([aten._unsafe_masked_index_put_accumulate]) +def _unsafe_masked_index_put_accumulate(x, mask, indices, values): + for index in indices: + if index is not None: + torch._check( + index.dtype in [torch.long, torch.int], + lambda: "tensors used as indices must be long or int tensors", + ) + + torch._check( + mask.dtype == torch.bool, + lambda: "tensors used as masks must be bool tensors", + ) + + if x.numel() == 0: + return x.clone() + + for i in range(len(indices)): + index = indices[i] + if index is not None: + indices[i] = index.clamp(min=-x.size(i), max=x.size(i) - 1) + + masked_value = values.masked_fill(~mask, 0) + return aten._unsafe_index_put(x, indices, masked_value, accumulate=True) + + +def _nll_loss_forward( + self: Tensor, + target: Tensor, + weight: Tensor | None, + reduction: int, + ignore_index: int, +) -> tuple[Tensor, Tensor]: + # self can be [N, C] or [C] + # target can be [N] or [] + + n_dims = self.dim() + channel_dim = 1 + if n_dims < 2: + channel_dim = 0 + + if weight is not None: + if n_dims > 1: + shape = [ + 1, + ] * n_dims + shape[channel_dim] = weight.shape[0] + w = weight.view(shape) + else: + w = weight + self = self * w + safe_target = torch.where(target != ignore_index, target, 0) + safe_target_ = safe_target.unsqueeze(channel_dim) + # target can be [N, 1] or [1] + + result = -torch.gather(self, channel_dim, safe_target_).squeeze(channel_dim) + + result = torch.where(target != ignore_index, result, 0) + + if reduction == Reduction.NONE.value and n_dims > 1: + total_weight = self.new_full((), 0.0) + return result, total_weight + + if weight is not None: + # pyrefly: ignore [unbound-name] + w = w.expand(self.shape) + wsum = torch.gather(w, channel_dim, safe_target_).squeeze(channel_dim) + wsum = torch.where(target != ignore_index, wsum, 0) + total_weight = wsum.sum() + else: + total_weight = (target != ignore_index).sum().to(self) + + if reduction == Reduction.SUM.value: + result = result.sum() + elif reduction == Reduction.MEAN.value: + result = result.sum() / total_weight + + return result, total_weight + + +@register_decomposition(aten.nll_loss_forward) +@out_wrapper("output", "total_weight") +def nll_loss_forward( + self: Tensor, + target: Tensor, + weight: Tensor | None, + reduction: int, + ignore_index: int, +) -> tuple[Tensor, Tensor]: + if not (self.dim() > 0 and self.dim() <= 2): + raise AssertionError(f"input tensor should be 1D or 2D, got {self.dim()}D") + if target.dim() > 1: + raise AssertionError( + f"0D or 1D target tensor expected, multi-target not supported, got {target.dim()}D" + ) + + no_batch_dim = self.dim() == 1 and target.dim() == 0 + if not no_batch_dim: + torch._check( + self.shape[0] == target.shape[0], + lambda: f"size mismatch (got input: {self.shape}, target: {target.shape})", + ) + + n_classes = self.shape[-1] + + if weight is not None and not (weight.dim() == 1 and weight.numel() == n_classes): + raise AssertionError( + f"weight tensor should be defined either for all {n_classes} classes or no classes " + f"but got weight tensor of shape: {weight.shape}" + ) + + return _nll_loss_forward(self, target, weight, reduction, ignore_index) + + +@register_decomposition(aten.nll_loss2d_forward) +@out_wrapper("output", "total_weight") +def nll_loss2d_forward( + self: Tensor, + target: Tensor, + weight: Tensor | None, + reduction: int, + ignore_index: int, +) -> tuple[Tensor, Tensor]: + return _nll_loss_forward(self, target, weight, reduction, ignore_index) + + +# These are adapted from aten/src/ATen/native/UpSample.h, which is based on +# https://en.wikipedia.org/wiki/Bicubic_interpolation#Bicubic_convolution_algorithm +def _upsample_cubic_convolution1(x: Tensor, A: float) -> Tensor: + return ((A + 2) * x - (A + 3)) * x * x + 1 + + +def _upsample_cubic_convolution2(x: Tensor, A: float) -> Tensor: + return ((A * x - 5 * A) * x + 8 * A) * x - 4 * A + + +def _upsample_get_cubic_coefficients(t: Tensor) -> TensorSequenceType: + A = -0.75 + + if t.device == torch.device("cpu"): + tt1 = torch.stack([t, 1.0 - t], dim=0) + tt2 = torch.stack([t + 1.0, 2.0 - t], dim=0) + w03 = _upsample_cubic_convolution2(tt2, A) + w12 = _upsample_cubic_convolution1(tt1, A) + w0, w3 = torch.unbind(w03, dim=0) + w1, w2 = torch.unbind(w12, dim=0) + return w0, w1, w2, w3 + else: + return ( + _upsample_cubic_convolution2(t + 1.0, A), + _upsample_cubic_convolution1(t, A), + _upsample_cubic_convolution1(1.0 - t, A), + _upsample_cubic_convolution2(2.0 - t, A), + ) + + +def _upsample_cubic_interp1d(coeffs: TensorSequenceType, ts: Tensor) -> Tensor: + coeffs2 = _upsample_get_cubic_coefficients(ts) + return _sum_tensors(c1 * c2 for (c1, c2) in zip(coeffs, coeffs2)) + + +# Need this instead of just sum() to keep mypy happy +def _sum_tensors(ts: Iterable[Tensor]) -> Tensor: + return reduce(torch.add, ts) + + +def _linspace_from_neg_one( + num_steps: int, align_corners: bool, dtype: torch.dtype, device: torch.device +): + if num_steps <= 1: + return torch.tensor(0, device=device, dtype=dtype) + + a = ((num_steps - 1) / num_steps) if not align_corners else 1 + return torch.linspace(-a, a, steps=num_steps, device=device, dtype=dtype) + + +def _make_base_grid_4d(theta: Tensor, h: int, w: int, align_corners: bool): + dtype = theta.dtype + device = theta.device + + # Using padding and summation generates a single kernel vs using torch.stack where 3 kernels generated + # corresponding to each individual tensor: grid_x, grid_y, grid_one + grid_x = _linspace_from_neg_one(w, align_corners, dtype, device).view(1, w, 1) + grid_y = _linspace_from_neg_one(h, align_corners, dtype, device).view(h, 1, 1) + grid_one = torch.ones((1, 1, 1), dtype=dtype, device=device) + + # this is just a temporary hack and we should use torch.stack here once #104480 is merged + grid_x = torch.nn.functional.pad(grid_x, pad=(0, 2), mode="constant", value=0) + grid_y = torch.nn.functional.pad(grid_y, pad=(1, 1), mode="constant", value=0) + grid_one = torch.nn.functional.pad(grid_one, pad=(2, 0), mode="constant", value=0) + return grid_x + grid_y + grid_one + + +def _make_base_grid_5d(theta: Tensor, d: int, h: int, w: int, align_corners: bool): + dtype = theta.dtype + device = theta.device + + grid_x = _linspace_from_neg_one(w, align_corners, dtype, device).view(1, 1, w, 1) + grid_y = _linspace_from_neg_one(h, align_corners, dtype, device).view(1, h, 1, 1) + grid_z = _linspace_from_neg_one(d, align_corners, dtype, device).view(d, 1, 1, 1) + grid_one = torch.ones((1, 1, 1, 1), dtype=dtype, device=device) + + # this is just a temporary hack and we should use torch.stack here once #104480 is merged + grid_x = torch.nn.functional.pad(grid_x, pad=(0, 3), mode="constant", value=0) + grid_y = torch.nn.functional.pad(grid_y, pad=(1, 2), mode="constant", value=0) + grid_z = torch.nn.functional.pad(grid_z, pad=(2, 1), mode="constant", value=0) + grid_one = torch.nn.functional.pad(grid_one, pad=(3, 0), mode="constant", value=0) + return grid_x + grid_y + grid_z + grid_one + + +def _affine_grid_generator_4d(theta: Tensor, size: list[int], align_corners: bool): + n, _, h, w = size + base_grid = _make_base_grid_4d(theta, h, w, align_corners=align_corners) + # base_grid shape is (h, w, 3) and theta shape is (n, 2, 3) + # We do manually a matrix multiplication which is faster than mm() + # (h * w, 3, 1) * (n, 1, 3, 2) -> (n, h * w, 2) + grid = (base_grid.view(-1, 3, 1) * theta.mT.unsqueeze(1)).sum(-2) + return grid.view(n, h, w, 2) + + +def _affine_grid_generator_5d(theta: Tensor, size: list[int], align_corners: bool): + n, _, d, h, w = size + base_grid = _make_base_grid_5d(theta, d, h, w, align_corners=align_corners) + # base_grid shape is (d, h, w, 4) and theta shape is (n, 3, 4) + # We do manually a matrix multiplication which is faster than mm() + # (d * h * w, 4, 1) * (n, 1, 4, 3) -> (n, h * w, 3) + grid = (base_grid.view(-1, 4, 1) * theta.mT.unsqueeze(1)).sum(-2) + return grid.view(n, d, h, w, 3) + + +@register_decomposition(aten.affine_grid_generator) +@out_wrapper() +@pw_cast_for_opmath +def affine_grid_generator(theta: Tensor, size: list[int], align_corners: bool): + torch._check( + len(size) in (4, 5), + lambda: "affine_grid_generator needs 4d (spatial) or 5d (volumetric) inputs.", + ) + if len(size) == 4: + return _affine_grid_generator_4d(theta, size, align_corners=align_corners) + else: + return _affine_grid_generator_5d(theta, size, align_corners=align_corners) + + +def _grid_sampler_2d( + a: Tensor, + grid: Tensor, + interpolation_mode: int = 0, + padding_mode: int = 0, + align_corners: bool = False, + _expand_grid: bool = True, +) -> Tensor: + # This method is a copy of grid_sampler_2d implementation and introduced with additional arg _expand_grid to + # optionally expand the input grid for performance reasons. + # Experimenting locally it was found that compiled CUDA code is accelerated by ~5x + # and CPU code by ~2x on bicubic mode, if we expand the grid from (N, H, W, 2) into (N, C, H, W, 2) + # However, this leads to a slowdown around ~0.8x on CPU bilinear mode, channels first. + # Thus we apply this hack to not expand the grid for this case. + + torch._check( + interpolation_mode in (0, 1, 2), + lambda: f"Invalid interpolation mode {interpolation_mode}", + ) + torch._check( + padding_mode in (0, 1, 2), lambda: f"Invalid padding mode {padding_mode}" + ) + + def unnormalize(coords: Tensor, size: int) -> Tensor: + # Rescale coordinates from [-1, 1] to: + # [0, size - 1] if align_corners is True + # [-.5, size -.5] if align_corners is False + mul = (size * 0.5 - 0.5) if align_corners else (size * 0.5) + ofs = size * 0.5 - 0.5 + return coords * mul + ofs + + # Reflects coordinates until they fall between low and high (inclusive). + # The bounds are passed as twice their value so that half-integer values + # can be represented as ints. + def reflect_coordinates(coords: Tensor, twice_low: int, twice_high: int) -> Tensor: + if twice_low == twice_high: + return torch.zeros_like(coords) + coords_min = twice_low / 2 + coords_span = (twice_high - twice_low) / 2 + coords2 = (coords - coords_min).abs() + extra = torch.fmod(coords2, coords_span) + flips = (coords2 / coords_span).floor().to(dtype=torch.int8) + return torch.where( + flips & 1 == 0, extra + coords_min, coords_span + coords_min - extra + ) + + def compute_coordinates(coords: Tensor, size: int) -> Tensor: + if padding_mode == 0: # Zero + return coords + elif padding_mode == 1: # Borders + return torch.clamp(coords, 0, size - 1) + else: # padding_mode == 2, Reflection + if align_corners: + coords_reflected = reflect_coordinates(coords, 0, 2 * (size - 1)) + else: + coords_reflected = reflect_coordinates(coords, -1, 2 * size - 1) + return torch.clamp(coords_reflected, 0, size - 1) + + def compute_source_index(coords: Tensor, size: int) -> Tensor: + coords_un = unnormalize(coords, size) + return compute_coordinates(coords_un, size) + + N, C, iH, iW = a.shape + _, oH, oW, two = grid.shape + if two != 2: + raise AssertionError( + f"grid last dimension must be 2 (for x,y coords), got {two}" + ) + + if _expand_grid: + # Let's expand grid to [N, C, oH, oW, 2] + # This allows to generate a single triton cuda kernel instead of two kernels. + # Two kernels are due source indices, weights have shape (N, 1, oH, oW), xnumel=N*oH*oW + # and output has shape (N, C, oH, oW), xnumel=N*C*oH*oW + # Expanding grid to (N, C, oH, oW, two) unifies xnumel to N*C*oH*oW + grid = grid.view(N, 1, oH, oW, two).expand(N, C, oH, oW, 2) + + def in_bounds_cond(xs: Tensor, ys: Tensor) -> Tensor: + return torch.logical_and( + 0 <= xs, torch.logical_and(xs < iW, torch.logical_and(0 <= ys, ys < iH)) + ) + + N_idx = torch.arange(N, device=a.device).view(N, 1, 1, 1) + C_idx = torch.arange(C, device=a.device).view(1, C, 1, 1) + + def clip(xs: Tensor, ys: Tensor, ws: Tensor) -> TensorSequenceType: + cond = in_bounds_cond(xs, ys) + # To clip to inside valid coordinates, we map the coordinates + # to (x, y) = (0, 0) and also set the weight to 0 + # We also change the shape of the tensor to the appropriate one for + # broadcasting with N_idx, C_idx for the purposes of advanced indexing + c = C if _expand_grid else 1 + return tuple( + torch.where(cond, t, 0).view(N, c, oH, oW) + for t in (xs.to(dtype=torch.int64), ys.to(dtype=torch.int64), ws) + ) + + def get_summand(ix: Tensor, iy: Tensor, w) -> Tensor: + # Perform clipping, index into input tensor and multiply by weight + idx_x, idx_y, w_ = clip(ix, iy, w) + return a[N_idx, C_idx, idx_y, idx_x] * w_ + + x = grid[..., 0] + y = grid[..., 1] + + if interpolation_mode == 0: # Bilinear + ix = compute_source_index(x, iW) + iy = compute_source_index(y, iH) + + ix_nw, iy_nw = ix.floor(), iy.floor() + ix_ne, iy_ne = ix_nw + 1, iy_nw + ix_sw, iy_sw = ix_nw, iy_nw + 1 + ix_se, iy_se = ix_ne, iy_sw + + w_nw = (ix_se - ix) * (iy_se - iy) + w_ne = (ix - ix_sw) * (iy_sw - iy) + w_sw = (ix_ne - ix) * (iy - iy_ne) + w_se = (ix - ix_nw) * (iy - iy_nw) + + return _sum_tensors( + get_summand(ix, iy, w) + for (ix, iy, w) in ( + (ix_nw, iy_nw, w_nw), + (ix_ne, iy_ne, w_ne), + (ix_sw, iy_sw, w_sw), + (ix_se, iy_se, w_se), + ) + ) + elif interpolation_mode == 1: # Nearest + ix = compute_source_index(x, iW) + iy = compute_source_index(y, iH) + + ix_nearest = ix.round() + iy_nearest = iy.round() + + return get_summand(ix_nearest, iy_nearest, 1) + else: # interpolation_mode == 2, Bicubic + ix = unnormalize(x, iW) + iy = unnormalize(y, iH) + + ix_nw = ix.floor() + iy_nw = iy.floor() + + tx = ix - ix_nw + ty = iy - iy_nw + + if not _expand_grid: + tx = tx.unsqueeze(1) + ty = ty.unsqueeze(1) + + def get_value_bounded(ix: Tensor, iy: Tensor) -> Tensor: + x = compute_coordinates(ix, iW) + y = compute_coordinates(iy, iH) + return get_summand(x, y, 1) + + def get_coeff(ofs: int) -> Tensor: + iy_ofs = iy_nw + (ofs - 1) + cs = ( + get_value_bounded(ix_nw - 1, iy_ofs), + get_value_bounded(ix_nw, iy_ofs), + get_value_bounded(ix_nw + 1, iy_ofs), + get_value_bounded(ix_nw + 2, iy_ofs), + ) + return _upsample_cubic_interp1d(cs, tx) + + coeffs = tuple(get_coeff(ofs) for ofs in range(4)) + return _upsample_cubic_interp1d(coeffs, ty) + + +@register_decomposition(aten.grid_sampler_2d) +@out_wrapper() +@pw_cast_for_opmath +def grid_sampler_2d( + a: Tensor, + grid: Tensor, + interpolation_mode: int = 0, + padding_mode: int = 0, + align_corners: bool = False, +) -> Tensor: + return _grid_sampler_2d( + a, + grid=grid, + interpolation_mode=interpolation_mode, + padding_mode=padding_mode, + align_corners=align_corners, + ) + + +@register_decomposition(aten.mv) +@out_wrapper(exact_dtype=True) +@pw_cast_for_opmath +def mv(self, vec): + torch._check( + self.dim() == 2 and vec.dim() == 1, + lambda: f"matrix @ vector expected, got {self.dim()}, {vec.dim()}", + ) + torch._check( + self.size(1) == vec.size(0), + lambda: f"size mismatch, got input ({self.size(0)}x{self.size(1)}), vec ({vec.size(0)})", + ) + return (self * vec).sum(dim=1) + + +@register_decomposition(aten.binary_cross_entropy_with_logits) +@out_wrapper() +def binary_cross_entropy_with_logits( + self, target, weight=None, pos_weight=None, reduction=Reduction.MEAN.value +): + if pos_weight is not None: + log_weight = (pos_weight - 1) * target + 1 + loss = (1 - target) * self - (log_weight * F.logsigmoid(self)) + else: + loss = (1 - target) * self - F.logsigmoid(self) + + if weight is not None: + loss = loss * weight + + # this is to align the resulted data type with the in-place + # operation in binary_cross_entropy_with_logits of aten/src/ATen/native/Loss.cpp + loss = loss.to(target.dtype) + return apply_loss_reduction(loss, reduction) + + +def should_fold(tensor1: torch.Tensor, tensor2: torch.Tensor, is_out: bool) -> bool: + # For comments of the logic of this function see eager in /native/LinearAlgebra.cpp + + t1, t2 = (tensor1, tensor2) if tensor1.ndim >= tensor2.ndim else (tensor2, tensor1) + + from torch.fx.experimental.symbolic_shapes import guard_or_false + + if not (t1.ndim >= 3 and t2.ndim <= 2): + return False + if t2.requires_grad and not is_out: + return True + if tensor1.ndim == 2: + return False + if guard_or_false(t1.numel() == 0): + return True + + t1_shape = t1.shape + t1_stride = t1.stride() + + # Check the contiguous, we can skip the dim with size of 1 + # as aten: https://github.com/pytorch/pytorch/blob/e201460f8aa1510b4c4686627d57b69756c4b916/aten/src/ATen/TensorGeometry.cpp#L17 + expected_stride = [1] + for size in reversed(t1_shape[1:]): + expected_stride.append(size * expected_stride[-1]) + return all( + guard_or_false(size == 1) or guard_or_false(left == right) + for left, right, size in zip( + t1_stride, list(reversed(expected_stride)), t1_shape + ) + ) + + +@aten.matmul.default.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten.matmul.out.py_impl(DispatchKey.CompositeImplicitAutograd) +@out_wrapper(pass_is_out=True) +def matmul(tensor1, tensor2, *, is_out=False): + from torch.fx.experimental.symbolic_shapes import guard_or_false, guard_or_true + + dim_tensor1 = tensor1.dim() + dim_tensor2 = tensor2.dim() + if dim_tensor1 == 0 or dim_tensor2 == 0: + raise AssertionError( + f"matmul does not support 0-dimensional tensors, got dims: {dim_tensor1} and {dim_tensor2}" + ) + if dim_tensor1 == 1 and dim_tensor2 == 1: + return torch.dot(tensor1, tensor2) + elif dim_tensor1 == 2 and dim_tensor2 == 1: + return torch.mv(tensor1, tensor2) + elif dim_tensor1 == 1 and dim_tensor2 == 2: + return torch.squeeze(torch.mm(torch.unsqueeze(tensor1, 0), tensor2), 0) + elif dim_tensor1 == 2 and dim_tensor2 == 2: + return torch.mm(tensor1, tensor2) + elif should_fold(tensor1, tensor2, is_out): + # dim_tensor1 >=3 && (dim_tensor2 == 1 || dim_tensor2 == 2) || + # dim_tensor2 >=3 && (dim_tensor1 == 1 || dim_tensor1 == 2) + # and some condition on the strides is fulfilled + + # optimization: use mm instead of bmm by folding the batch of the larger tensor + # into its leading matrix dimension + transpose = dim_tensor2 > dim_tensor1 + t1 = tensor2.mT if transpose else tensor1 + t2 = ( + tensor2 if not transpose else (tensor1.t() if dim_tensor1 == 2 else tensor1) + ) + # Invariant: t1.dim() >= 3 && (t2.dim() == 1 || t2.dim() == 2) + # and t1 and t2 are matmul-compatible + + # Why not t1.view(-1, sizes_1[-1])? + # If the last dim is 0, then view(-1, 0) won't work because the -1 becomes ambiguous. + # This can happen in e.g. [3, 5, 0] @ [0, 0]. + sizes_1 = t1.shape + output_shape = list(sizes_1[:-1]) + folded_dim1 = reduce(operator.mul, output_shape) + + # Readjust output_shape if we are multiplying by a matrix + t2_is_matrix = t2.dim() == 2 + if t2_is_matrix: + output_shape.append(t2.shape[1]) + + # This will almost always be a view. + # It may not be a view if t2->requires_grad(). See should_fold in aten/ for an explanation + t1_folded = t1.reshape(folded_dim1, sizes_1[-1]) + if t2_is_matrix: + # This copies if we perform a 2D @ 3D and the first tensor requires_grad + # See should_fold native/LinearAlgebra.cpp for why. + output = torch.ops.aten._unsafe_view(t1_folded.mm(t2), output_shape) + return output.mT.contiguous() if transpose else output + else: + return torch.ops.aten._unsafe_view(t1_folded.mv(t2), output_shape) + + elif dim_tensor1 >= 1 and dim_tensor2 >= 1: + # We are multiplying b1 x n x m1 by x2 x m2 x p (where b1 can be a list); + # we track m1 vs m2 separately even though they must match for nicer error messages + n = tensor1.size(-2) if dim_tensor1 > 1 else 1 + m1 = tensor1.size(-1) + batch_tensor1 = tensor1.shape[:-2] + m2 = tensor2.size(-2) if dim_tensor2 > 1 else tensor2.size(-1) + p = tensor2.size(-1) if dim_tensor2 > 1 else 1 + + batch_tensor2: list[int] = [] + # TODO: handling of slice + for i in range(dim_tensor2 - 2): + batch_tensor2.append(tensor2.size(i)) + + # Same optimization for the gradients as that in should_fold + # If we're going to broadcast, we force it to go through the should_fold branch + if ( + dim_tensor1 == 3 + and dim_tensor2 == 3 + and guard_or_true(batch_tensor1[0] != batch_tensor2[0]) + ): + if guard_or_false(batch_tensor1[0] == 1) and tensor1.requires_grad: + return matmul(tensor1.squeeze(0), tensor2) + if guard_or_false(batch_tensor2[0] == 1) and tensor2.requires_grad: + return matmul(tensor1, tensor2.squeeze(0)) + + # expand the batch portion (i.e. cut off matrix dimensions and expand rest) + expand_batch_portion = list( + torch.broadcast_shapes(batch_tensor1, batch_tensor2) + ) + + tensor1_expand_size = expand_batch_portion + [n, m1] + + expand_batch_product = prod(expand_batch_portion) + + # HACK: We need reshape with symint support + tensor1_expanded = tensor1.expand(tensor1_expand_size).reshape( + expand_batch_product, n, m1 + ) + + vector_rhs = dim_tensor2 == 1 + if vector_rhs: + tensor2_expand_size = expand_batch_portion + [m2] + tensor2_expanded = ( + tensor2.expand(tensor2_expand_size) + .reshape(expand_batch_product, m2) + .unsqueeze(2) + ) + else: + tensor2_expand_size = expand_batch_portion + [m2, p] + tensor2_expanded = tensor2.expand(tensor2_expand_size).reshape( + expand_batch_product, m2, p + ) + + output_shape = expand_batch_portion + if dim_tensor1 > 1: + output_shape.append(n) + + if dim_tensor2 > 1: + output_shape.append(p) + + if vector_rhs: + return tensor1_expanded.bmm(tensor2_expanded).squeeze(-1).view(output_shape) + else: + return tensor1_expanded.bmm(tensor2_expanded).view(output_shape) + else: + torch._check(False, lambda: "both arguments to matmul need to be at least 1D") + + +@register_decomposition([aten.upsample_bicubic2d.default, aten.upsample_bicubic2d.out]) +@aten.upsample_bicubic2d.default.py_impl(DispatchKey.Autograd) +@out_wrapper() +@pw_cast_for_opmath +def upsample_bicubic2d_default( + input: Tensor, + output_size: tuple[int, int], + align_corners: bool, + scale_h: float | None = None, + scale_w: float | None = None, +) -> Tensor: + # get dimensions of original image + _, _, in_h, in_w = input.shape + + # Calculate horizontal and vertical scaling factor + h_scale_factor = _compute_scale(in_h, output_size[0], align_corners, scale_h) + w_scale_factor = _compute_scale(in_w, output_size[1], align_corners, scale_w) + + _, dtype = utils.elementwise_dtypes( + input, type_promotion_kind=utils.ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT + ) + + # We have to create arange with int64 dtype and use .to in order to avoid + # additional kernels creation in inductor and get a perf slowdown + i = torch.arange(output_size[0], device=input.device).to(dtype=dtype) + j = torch.arange(output_size[1], device=input.device).to(dtype=dtype) + + x_float = _compute_source_index(w_scale_factor, j, align_corners) + y_float = _compute_source_index(h_scale_factor, i, align_corners) + y_float = y_float.unsqueeze(-1) + + x = x_float.floor() + y = y_float.floor() + + # We should also clamp xscale/yscale + # See guard_index_and_lambda in UpSample.h + yscale = (y_float - y).clamp(0.0, 1.0) + xscale = (x_float - x).clamp(0.0, 1.0) + x = x.to(torch.int64) + y = y.to(torch.int64) + + iys_ofs = (y - 1, y, y + 1, y + 2) + ixs_ofs = (x - 1, x, x + 1, x + 2) + + weights_x = _upsample_get_cubic_coefficients(xscale) + weights_y = _upsample_get_cubic_coefficients(yscale) + + weights_precision_x, weights_precision_y = None, None + if input.dtype == torch.uint8: + weights_precision_x = _compute_weight_precision(weights_x) + weights_precision_y = _compute_weight_precision(weights_y) + + weights_x = [ + (w * (1 << weights_precision_x) + torch.sign(w) * 0.5).to(torch.int16) + for w in weights_x + ] + weights_y = [ + (w * (1 << weights_precision_y) + torch.sign(w) * 0.5).to(torch.int16) + for w in weights_y + ] + + def load_bounded(ys, xs): + y_idx = torch.clamp(ys, 0, in_h - 1) + x_idx = torch.clamp(xs, 0, in_w - 1) + v = aten._unsafe_index(input, [None, None, y_idx, x_idx]) + return v + + def get_x_interp(y): + src_x = tuple(load_bounded(y, x_ofs) for x_ofs in ixs_ofs) + if input.dtype == torch.uint8: + if weights_precision_x is None: + raise AssertionError( + "weights_precision_x must not be None for uint8 input" + ) + return _sum_tensors_uint8(src_x, weights_x, weights_precision_x) + return _sum_tensors(c1 * c2 for (c1, c2) in zip(src_x, weights_x)) + + src_y = tuple(get_x_interp(y_ofs) for y_ofs in iys_ofs) + if input.dtype == torch.uint8: + if weights_precision_y is None: + raise AssertionError("weights_precision_y must not be None for uint8 input") + result = _sum_tensors_uint8(src_y, weights_y, weights_precision_y) + else: + result = _sum_tensors(c1 * c2 for (c1, c2) in zip(src_y, weights_y)) + + # convert output to correct memory format, if necessary + memory_format = utils.suggest_memory_format(input) + result = result.contiguous(memory_format=memory_format) + return result + + +@register_decomposition(aten.upsample_bicubic2d.vec) +@aten.upsample_bicubic2d.vec.py_impl(DispatchKey.CompositeImplicitAutograd) +@aten.upsample_bicubic2d.vec.py_impl(DispatchKey.Autograd) +@out_wrapper() +@pw_cast_for_opmath +def upsample_bicubic2d_vec( + a: Tensor, + output_size: tuple[int, int] | None, + align_corners: bool, + scale_factors: tuple[float, float] | None = None, +) -> Tensor: + torch._check( + bool(output_size) + bool(scale_factors) == 1, + lambda: "Must specify exactly one of output_size and scale_factors.", + ) + if output_size is None: + if scale_factors is None: + raise AssertionError( + "scale_factors must not be None when output_size is None" + ) + output_size = cast( + tuple[int, int], + tuple( + sym_int(sym_float(w) * scale) + for w, scale in zip(a.shape[2:], scale_factors) + ), + ) + scale_h, scale_w = scale_factors if scale_factors else (None, None) + return upsample_bicubic2d_default(a, output_size, align_corners, scale_h, scale_w) + + +@register_decomposition(aten.reflection_pad1d) +@register_decomposition(aten.reflection_pad2d) +@register_decomposition(aten.reflection_pad3d) +@pw_cast_for_opmath +@out_wrapper() +def _reflection_pad(a: Tensor, padding: tuple[int, ...]) -> Tensor: + def idx(left, middle, right): + dim_idx = torch.arange(-left, middle + right, device=a.device) + return middle - 1 - (middle - 1 - dim_idx.abs()).abs() + + return _reflection_or_replication_pad( + a, + padding, + idx, + ) + + +@register_decomposition(aten.replication_pad1d) +@register_decomposition(aten.replication_pad2d) +@register_decomposition(aten.replication_pad3d) +@pw_cast_for_opmath +@out_wrapper() +def _replication_pad(a: Tensor, padding: tuple[int, ...]) -> Tensor: + def idx(left, middle, right): + dim_idx = torch.arange(-left, middle + right, device=a.device) + return torch.clamp(dim_idx, 0, middle - 1) + + return _reflection_or_replication_pad( + a, + padding, + idx, + ) + + +def _reflection_or_replication_pad( + a: Tensor, + padding: tuple[int, ...], + idx_fn: Callable[[int, int, int], Tensor], +) -> Tensor: + dim = len(padding) // 2 + torch._check( + a.dim() in (dim + 1, dim + 2), + lambda: f"reflection_pad{dim}d requires {dim + 1}D or {dim + 2}D input", + ) + inp_shape = a.shape[-dim:] + nc_dim = a.dim() - dim + + padding_left = [padding[2 * (dim - 1 - i)] for i in range(dim)] + padding_right = [padding[2 * (dim - 1 - i) + 1] for i in range(dim)] + + result = a + for i in range(dim): + idx: list[Any] = [None] * result.dim() + idx[i + nc_dim] = idx_fn(padding_left[i], inp_shape[i], padding_right[i]) + result = aten._unsafe_index(result, idx) + + # convert output to correct memory format, if necessary + memory_format = utils.suggest_memory_format(result) + result = result.contiguous(memory_format=memory_format) + return result + + +@register_decomposition(aten.reflection_pad1d_backward) +@register_decomposition(aten.reflection_pad2d_backward) +@register_decomposition(aten.reflection_pad3d_backward) +@out_wrapper("grad_input") +def _reflection_pad_backward(grad_output, x, padding): + dim = len(padding) // 2 + + dhw = [h - 1 for h in x.shape[-dim:]] + + padding_left = [padding[2 * (dim - 1 - i)] for i in range(dim)] + padding_right = [padding[2 * (dim - 1 - i) + 1] for i in range(dim)] + + indices = [] + for i in range(x.ndim): + view_shape = [1] * x.ndim + view_shape[i] = -1 + indices.append(torch.arange(x.shape[i], device=x.device).view(view_shape)) + + b = indices[:-dim] + xyz = indices[-dim:] + + def index_range_condition(index_range): + i, lb, ub = index_range + return torch.logical_and(i >= lb, i <= ub) + + # Areas after reflection: + # + # top-left | top | top-right + # ----------------------------------------- + # left | center | right + # ----------------------------------------- + # bottom-left | bottom | bottom-right + # + # The center area is the original matrix. Other areas are reflections. + + center = [xyz[i] + padding_left[i] for i in range(dim)] + left_reflect = [padding_left[i] - xyz[i] for i in range(dim)] + right_reflect = [2 * dhw[i] + padding_left[i] - xyz[i] for i in range(dim)] + + # Accumulate gradients from different areas + # If some of the padding is negative, center load is not always valid + range_c = [ + (center[i], 0, dhw[i] + padding_left[i] + padding_right[i]) for i in range(dim) + ] + cond = functools.reduce( + aten.logical_and, [index_range_condition(range_c[i]) for i in range(dim)] + ) + grad = aten._unsafe_masked_index(grad_output, cond, b + center, 0.0) + + def accumulate(grad, out, index_ranges): + # If the upper bound is less than the lower bound, we can get rid of one accumulation. + # This happens when the padding size is zero. + for i in range(dim): + upper_less_than_lower = index_ranges[i][2] < index_ranges[i][1] + if isinstance(upper_less_than_lower, bool) and upper_less_than_lower: + return grad + + cond = functools.reduce( + aten.logical_and, + [index_range_condition(index_range) for index_range in index_ranges], + ) + g = aten._unsafe_masked_index(grad_output, cond, b + out, 0.0) + return grad + g + + for area in itertools.product(*[[-1, 0, 1] for _ in range(dim)]): + if area == tuple([0] * dim): + # center, this is already done. + continue + + outs = [] + index_ranges = [] + + for i in range(dim): + if area[i] == 0: + out = center[i] + index_range = range_c[i] + elif area[i] == -1: + out = left_reflect[i] + index_range = (xyz[i], 1, padding_left[i]) + elif area[i] == 1: + out = right_reflect[i] + index_range = (xyz[i], dhw[i] - padding_right[i], dhw[i] - 1) + + outs.append(out) # type: ignore[possibly-undefined] + index_ranges.append(index_range) # type: ignore[possibly-undefined] + + grad = accumulate(grad, outs, index_ranges) + + return grad + + +@register_decomposition(aten.aminmax) +@out_wrapper("min", "max") +def aminmax(self, *, dim=None, keepdim=False): + # pyrefly: ignore [bad-argument-type] + amin = torch.amin(self, dim=dim, keepdim=keepdim) + # pyrefly: ignore [bad-argument-type] + amax = torch.amax(self, dim=dim, keepdim=keepdim) + return amin, amax + + +@register_decomposition(aten.nansum) +@out_wrapper() +def nansum(self, dim=None, keepdim=False, *, dtype=None): + return aten.sum(torch.where(torch.isnan(self), 0, self), dim, keepdim, dtype=dtype) + + +@register_decomposition([aten.arange.default, aten.arange.out]) +@out_wrapper() +def arange_default( + end: NumberType, + *, + dtype: torch.dtype | None = None, + layout: torch.layout = torch.strided, + device: torch.device | None = None, + pin_memory: bool = False, +): + return aten.arange.start_step( + 0, end, 1, dtype=dtype, layout=layout, device=device, pin_memory=pin_memory + ) + + +@register_decomposition([aten.arange.start]) +def arange_start( + start: NumberType, + end: NumberType, + *, + dtype: torch.dtype | None = None, + layout: torch.layout = torch.strided, + device: torch.device | None = None, + pin_memory: bool = False, +): + return aten.arange.start_step( + start, end, 1, dtype=dtype, layout=layout, device=device, pin_memory=pin_memory + ) + + +@register_decomposition(out_dtype) +def out_dtype_decomp(*args, **kwargs): + from torch._higher_order_ops.out_dtype import out_dtype_dense + + return out_dtype_dense(*args, **kwargs) + + +@register_decomposition(aten.multi_margin_loss) +@aten.multi_margin_loss.default.py_impl(DispatchKey.Autograd) +@out_wrapper() +def multi_margin_loss( + input: Tensor, + target: Tensor, + p: NumberType = 1, + margin: NumberType = 1, + weight: Tensor | None = None, + reduction: int = Reduction.MEAN.value, +) -> Tensor: + input = torch.atleast_2d(input) + target = torch.atleast_1d(target) + nframe = input.shape[0] + dim = input.shape[1] + torch._check(p == 1 or p == 2, lambda: "only p == 1 and p == 2 supported") + torch._check( + input.ndim == 2 and dim != 0, + lambda: f"Expected non-empty vector or matrix with optional 0-dim batch size, but got: {input.shape}", + ) + torch._check( + target.ndim == 1 and target.numel() == nframe, + lambda: f"inconsistent target size, expected {nframe} but got {target.shape}", + ) + if weight is not None: + weight = torch.atleast_1d(weight) + torch._check( + weight.ndim == 1 and weight.numel() == dim, # type: ignore[union-attr] + lambda: f"inconsistent weight size, expected {dim} but got {weight.shape}", # type: ignore[union-attr] + ) + target = target.unsqueeze(1) + u = torch.gather(input, dim=1, index=target) + z = margin - u + input + z = z.clamp_min(0) + z = z if p == 1 else z * z + if weight is not None: + z = z * weight[target] + idx = torch.arange(dim, device=input.device) + z = torch.where(idx != target, z, 0) + if reduction == Reduction.MEAN.value: + return z.mean() + elif reduction == Reduction.SUM.value: + return z.sum() / z.shape[1] + else: + return z.mean(dim=1) + + +@register_decomposition(aten.multilabel_margin_loss_forward) +@aten.multilabel_margin_loss_forward.default.py_impl(DispatchKey.Autograd) +@out_wrapper("output", "is_target") +def multilabel_margin_loss_forward( + input: Tensor, + target: Tensor, + reduction: int, +) -> tuple[Tensor, Tensor]: + orig_input_shape = input.shape + orig_target_shape = target.shape + input = torch.atleast_2d(input) + target = torch.atleast_2d(target) + dim = input.shape[1] + torch._check( + len(orig_input_shape) <= 2 and dim != 0, + lambda: f"Expected non-empty vector or matrix with optional 0-dim batch size, but got: {orig_input_shape}", + ) + torch._check( + len(orig_target_shape) <= 2 and orig_target_shape == orig_input_shape, + lambda: f"inconsistent target size: {orig_target_shape} for input of size: {orig_input_shape}", + ) + # ignores labels after the first -1, detects when -1 is not present + idx = torch.arange(dim, device=target.device) + is_end = target == -1 + end_idx = torch.amin(torch.where(is_end, idx, dim), dim=-1, keepdim=True) + # target indices + target_mask = idx < end_idx + # masks target to be able to use gather, which doesn't allow -1 + tidx0 = torch.where(target_mask, target, 0) + u = torch.gather(input, dim=-1, index=tidx0) + # is_target + tidx1 = torch.where(target_mask, target, -1) + is_target = torch.any(idx == tidx1.unsqueeze(dim=-1), dim=1) + # loss + z = 1.0 - u.T.unsqueeze(dim=-1) + input + z = z.clamp_min(0) + z = z / dim + # masks loss + z = torch.where(is_target, 0, z) + # reduction + if reduction == Reduction.MEAN.value: + z = z.sum(dim=(0, -1)).mean() + elif reduction == Reduction.SUM.value: + z = z.sum() + else: + z = z.sum(dim=(0, -1)) + # result + is_target = is_target.to(input.dtype).reshape(orig_target_shape) + return z, is_target + + +# scaled_dot_product_attention used to be decomposed in pre-autograd, given that +# it calls _scaled_dot_product_attention_math and +# _scaled_dot_product_attention_math only has a CompositeImplicitAutograd +# kernel. As a result it's decomposed into ops with finer granularity. +# However recent PRs (#103826 #105131 #115913) added new logic in +# scaled_dot_product_attention and now it calls +# _scaled_dot_product_flash_attention_for_cpu in export path. This results +# in _scaled_dot_product_flash_attention_for_cpu showing up in export result. +# This decomposition ensures scaled_dot_product_attention is still decomposed +# the same way as before, i.e., going through +# _scaled_dot_product_attention_math. Notice that this decomp rule should be +# excluded by inductor. +@register_decomposition(aten._scaled_dot_product_flash_attention_for_cpu.default) +def scaled_dot_product_flash_attention_for_cpu( + query: Tensor, + key: Tensor, + value: Tensor, + dropout_p: float = 0.0, + is_causal: bool = False, + *, + attn_mask: Tensor | None = None, + scale: float | None = None, +) -> tuple[Tensor, Tensor]: + torch._check( + torch.is_floating_point(query), + lambda: f"query must be FP32, FP64, BF16, FP16 but got {query.dtype}", + ) + torch._check( + query.dim() == 4 and key.dim() == 4 and value.dim() == 4, + lambda: f"q, k, v must be a 4 dimensional tensor, got {query.dim()}, {key.dim()}, {value.dim()}", + ) + torch._check( + dropout_p == 0.0, lambda: f"dropout probability must be zero, got {dropout_p}" + ) + torch._check( + query.shape[3] == value.shape[3] and key.shape[3] == value.shape[3], + lambda: "q, k, v should have the same head size", + ) + + output, attn = aten._scaled_dot_product_attention_math.default( + query, + key, + value, + attn_mask=attn_mask, + dropout_p=dropout_p, + is_causal=is_causal, + dropout_mask=None, + scale=scale, + enable_gqa=query.size(1) != key.size(1), + ) + # Why this change? + # In pre-dispatch export scaled_dot_product_attention is executed via + # * flash_attention. + # flash_attention allocates output tensor as (N, H, L, E) (see PR #134656) + # assume x: [N, H, L, E] is the output sdpa + # In MHA code, this output is then permuted via (2, 0, 1, 3) to get + # (L, N, H, E) dim tensor + # x = x.permute(2, 0, 1, 3).contiguous() and the viewed via + # x = x.view(L * N, H * E) + # During pre autograd dispatch call to contiguous is not traced because + # flash_attention output after the x.permute is already contiguous + # on which the view is valid + # However, during 2nd stage export, post-dispatch, we run _match variant + # instead of flash* to get the decomposition. _match variant returns + # x: [N, H, L, E] applying x.permute(2, 0, 1, 3) returns + # x: [L, N, H, E] and without converting this to contiguous tensor + # subsequent view is not valid and the export fails + # solution is to maintain the return tensor view from the decomp to be + # exactly same as *flash* variant. + + # Really the invariant you want to maintain is: + # pre-dispatch op-output and its decomposed representation must + # return tensor with same view and dims + output = ( + output.permute(2, 0, 1, 3) + .contiguous(memory_format=torch.contiguous_format) + .permute(1, 2, 0, 3) + ) + return output, attn + + +def register_inplace(aten_op, outplace_op): + @register_decomposition(aten_op) + def inplace_op(*args, **kwargs): + out = outplace_op(*args, **kwargs) + return args[0].copy_(out) + + return inplace_op + + +@register_decomposition([aten.baddbmm]) +@out_wrapper(exact_dtype=True) +@pw_cast_for_opmath +def baddbmm(self, batch1, batch2, beta=1, alpha=1): + if not self.is_floating_point() and not self.is_complex(): + beta = int(beta) + alpha = int(alpha) + result = torch.bmm(batch1, batch2) + if not isinstance(alpha, numbers.Number) or alpha != 1: + result = result * alpha + if beta == 0: + return result + if not isinstance(beta, numbers.Number) or beta != 1: + self = self * beta + return self + result + + +@register_decomposition(aten.floor_divide) +@out_wrapper() +def floor_divide(self, other): + return torch.div(self, other, rounding_mode="floor") + + +@register_decomposition(aten.sym_numel) +def sym_numel(t): + return functools.reduce(operator.mul, t.shape, 1) + + +@register_decomposition([aten.sum.default, aten.sum.out]) +def sum_default( + self: Tensor, + *, + dtype: torch.dtype | None = None, + out: Tensor | None = None, +) -> Tensor: + if out is None: + return aten.sum.dim_IntList(self, [], dtype=dtype) + else: + return aten.sum.IntList_out(self, [], dtype=dtype, out=out) + + +@register_decomposition([aten.squeeze.default, aten.squeeze.dim]) +def squeeze_default(self: Tensor, dim: int | None = None): + # handle a scalar directly + if not isinstance(self, torch.Tensor): + return self + # perform squeeze + if dim is None: + return aten.squeeze.dims(self, list(range(self.dim()))) + else: + return aten.squeeze.dims(self, [dim]) + + +@register_decomposition(torch.ops.aten._weight_norm_interface) +def _weight_norm_interface(v, g, dim=0): + # https://github.com/pytorch/pytorch/blob/852f8526c52190125446adc9a6ecbcc28fb66182/aten/src/ATen/native/WeightNorm.cpp#L58 + keep_dim = tuple(i for i in range(len(v.shape)) if i != dim) + # align with cuda behavior, keep norm in 'float' when g is 'bfloat16' + norm_dtype = torch.float if g.dtype == torch.bfloat16 else None + norm = v.norm(2, keep_dim, keepdim=True, dtype=norm_dtype) + return v * (g / norm.to(g.dtype)), norm + + +@register_decomposition(aten.isin) +@out_wrapper() +def isin(elements, test_elements, *, assume_unique=False, invert=False): + # handle when either elements or test_elements are Scalars (they can't both be) + if not isinstance(elements, torch.Tensor): + elements = torch.scalar_tensor(elements, device=test_elements.device) + if not isinstance(test_elements, torch.Tensor): + if invert: + return torch.ne(elements, test_elements) + else: + return torch.eq(elements, test_elements) + + from torch.fx.experimental.symbolic_shapes import guard_or_false + + if guard_or_false(test_elements.numel() < 10.0 * pow(elements.numel(), 0.145)): + return isin_default(elements, test_elements, invert=invert) + else: + return isin_sorting( + elements, test_elements, assume_unique=assume_unique, invert=invert + ) + + +@register_decomposition(aten.bernoulli.default) +def bernoulli( + self: torch.Tensor, + *, + generator: torch.Generator | None = None, +) -> torch.Tensor: + if generator is None: + raw_p = torch.rand(self.size(), dtype=torch.float32, device=self.device) + else: + raw_p = torch.rand( + self.size(), + generator=generator, + dtype=torch.float32, + device=self.device, + ) + p = (raw_p < self).to(self.dtype) + return p + + +def isin_default(elements, test_elements, *, invert=False): + if elements.numel() == 0: + return torch.empty_like(elements, dtype=torch.bool) + if test_elements.ndim == 0: + res = elements == test_elements + return ~res if invert else res + expanded_elem_shape = elements.shape + (1,) * test_elements.ndim + x = elements.view(expanded_elem_shape) + dim = tuple(range(-1, -test_elements.ndim - 1, -1)) + res = (x == test_elements).any(dim=dim) + return ~res if invert else res + + +def isin_sorting(elements, test_elements, *, assume_unique=False, invert=False): + elements_flat = elements.flatten() + test_elements_flat = test_elements.flatten() + if assume_unique: + # This is the same as the aten implementation. For + # assume_unique=False, we cannot use unique() here, so we use a + # version with searchsorted instead. + all_elements = torch.cat([elements_flat, test_elements_flat]) + sorted_elements, sorted_order = torch.sort(all_elements, stable=True) + + duplicate_mask = sorted_elements[1:] == sorted_elements[:-1] + duplicate_mask = torch.constant_pad_nd(duplicate_mask, [0, 1], False) + + if invert: + duplicate_mask = duplicate_mask.logical_not() + + mask = torch.empty_like(duplicate_mask) + mask = mask.index_copy(0, sorted_order, duplicate_mask) + + return mask[0 : elements.numel()].reshape(elements.shape) + else: + sorted_test_elements, _ = torch.sort(test_elements_flat) + idx = torch.searchsorted(sorted_test_elements, elements_flat) + test_idx = torch.where(idx < sorted_test_elements.numel(), idx, 0) + cmp = sorted_test_elements[test_idx] == elements_flat + cmp = cmp.logical_not() if invert else cmp + return cmp.reshape(elements.shape) + + +@register_decomposition(aten.take) +@out_wrapper() +def take(self, index): + flattened = self.reshape(-1) + return flattened[index] + + +@register_decomposition(aten.resize_as) +def resize_as(self, other, memory_format=None): + if memory_format is None: + memory_format = torch.contiguous_format + if memory_format == torch.preserve_format: + memory_format = suggest_memory_format(other) + return aten.resize(self, other.shape, memory_format=memory_format) + + +@register_decomposition(aten.max_pool2d_with_indices_backward) +def max_pool2d_with_indices_backward( + grad_output: Tensor, + self: Tensor, + kernel_size, + stride, + padding, + dilation, + ceil_mode: bool, + indices: Tensor, +): + """ + Decomposition of max_pool2d_with_indices_backward using scatter_add. + + This replaces the native implementation with a high-level decomposition + that uses scatter_add for gradient accumulation. The scatter-based approach + provides automatic optimization opportunities for Inductor and handles all + pooling configurations without requiring specialized fallback paths. + + Algorithm: + For each output gradient position, use the corresponding index from the + forward pass to scatter the gradient to the input position. When multiple + output positions select the same input position as max, scatter_add + automatically accumulates their gradients. + + Complexity: O(B * C * H_out * W_out) + Independent of kernel size, unlike traditional O(B * C * H_in * W_in * K²) + approaches that iterate over input positions and kernel windows. + + Known Limitations: + - FP16/BF16: Uses FP32 accumulation internally to preserve precision when + many gradients accumulate to the same position (overlapping pooling windows). + This adds slight overhead but ensures numerical stability. + - Deterministic mode: Falls back to native implementation to ensure + consistent results across runs + + Args: + grad_output: Gradient w.r.t. pooling output [B, C, H_out, W_out] + self: Original input tensor (for shape) [B, C, H_in, W_in] + kernel_size: Pooling kernel size + stride: Pooling stride + padding: Pooling padding + dilation: Pooling dilation + ceil_mode: Whether to use ceil for output size calculation + indices: Indices from forward pass (per-channel linear positions) + + Returns: + Gradient w.r.t. input [B, C, H_in, W_in] + """ + # Use native kernel in deterministic mode + if torch.are_deterministic_algorithms_enabled(): + return NotImplemented + + # MPS: Use native kernel. scatter_add has correctness issues on macOS 14 + # (#163327) and numerical differences on macOS 15+. + if grad_output.device.type == "mps": + return NotImplemented + + # Get spatial dimensions + in_height = self.size(-2) + in_width = self.size(-1) + out_height = grad_output.size(-2) + out_width = grad_output.size(-1) + + # Handle both 3D (C, H, W) and 4D (B, C, H, W) cases by treating 3D as 4D + is_batched = self.dim() == 4 + if not is_batched: + self = self.unsqueeze(0) + grad_output = grad_output.unsqueeze(0) + indices = indices.unsqueeze(0) + + batch_size = self.size(0) + channels = self.size(1) + + # For FP16/BF16, use FP32 accumulation to avoid precision loss + # This is critical when many gradients accumulate to the same position + # (overlapping pooling windows with large kernels or stride < kernel_size) + use_fp32_accum = grad_output.dtype in (torch.float16, torch.bfloat16) + accum_dtype = torch.float32 if use_fp32_accum else grad_output.dtype + + # Create grad_input with correct accumulation dtype from the start + grad_input_flat = torch.zeros( + batch_size * channels, + in_height * in_width, + dtype=accum_dtype, + device=grad_output.device, + ) + + # Reshape grad_output and indices to (B*C, H_out*W_out) + grad_output_flat = grad_output.reshape( + batch_size * channels, out_height * out_width + ) + indices_flat = indices.reshape(batch_size * channels, out_height * out_width) + + # Convert grad_output to accumulation dtype if needed + if use_fp32_accum: + grad_output_flat = grad_output_flat.to(torch.float32) + + # Scatter gradients to input positions + grad_input_flat = grad_input_flat.scatter_add(1, indices_flat, grad_output_flat) + + # Reshape back to original input shape + grad_input = grad_input_flat.reshape(batch_size, channels, in_height, in_width) + + # Convert back to original dtype if we used FP32 accumulation + if use_fp32_accum: + grad_input = grad_input.to(grad_output.dtype) + + # Preserve memory format from input (channels_last vs channels_first) + memory_format = utils.suggest_memory_format(self) + grad_input = grad_input.contiguous(memory_format=memory_format) + + # Remove batch dimension for 3D case + if not is_batched: + grad_input = grad_input.squeeze(0) + + return grad_input + + +@register_decomposition([aten.hann_window.default, aten.hann_window.out]) +@out_wrapper() +def hann_window( + window_length: int, + *, + dtype: torch.dtype | None = None, + layout: torch.layout | None = None, + device: torch.device | None = None, + pin_memory: bool | None = None, +) -> Tensor: + """hann_window(window_length, *, dtype=None, layout=None, device=None, pin_memory=False) -> Tensor + + Returns a Hann window of size :attr:`window_length` with ``periodic=True``. + + Equivalent to :func:`torch.hann_window` with ``periodic=True``. + + Args: + window_length (int): the size of returned window. + + Keyword args: + dtype (:class:`torch.dtype`, optional): desired dtype. Default: global default. + layout (:class:`torch.layout`, optional): desired layout. Default: ``torch.strided``. + device (:class:`torch.device`, optional): desired device. Default: current device. + pin_memory (bool, optional): if ``True``, pins the returned tensor. Default: ``False``. + """ + return aten.hann_window.periodic( + window_length, + True, + dtype=dtype, + layout=layout, + device=device, + pin_memory=pin_memory, + ) + + +@register_decomposition([aten.hann_window.periodic, aten.hann_window.periodic_out]) +@out_wrapper() +def hann_window_periodic( + window_length: int, + periodic: bool = True, + *, + dtype: torch.dtype | None = None, + layout: torch.layout | None = None, + device: torch.device | None = None, + pin_memory: bool | None = None, +) -> Tensor: + r"""hann_window(window_length, periodic=True, *, dtype=None, layout=None, device=None, pin_memory=False) -> Tensor + + Returns a Hann window of size :attr:`window_length`. + + .. math:: + w[n] = 0.5 - 0.5 \cos\!\left(\frac{2\pi n}{N-1}\right) + + where :math:`N` is ``window_length + 1`` when ``periodic=True`` (for spectral analysis), + or ``window_length`` when ``periodic=False`` (symmetric window). + + Low-precision dtypes (``bfloat16``, ``float16``) are computed in ``float32`` then cast. + + Args: + window_length (int): the size of returned window. + periodic (bool, optional): if ``True``, returns a periodic window for use with STFT. + Default: ``True``. + + Keyword args: + dtype (:class:`torch.dtype`, optional): desired dtype. Default: global default. + layout (:class:`torch.layout`, optional): desired layout. Default: ``torch.strided``. + device (:class:`torch.device`, optional): desired device. Default: current device. + pin_memory (bool, optional): if ``True``, pins the returned tensor. Default: ``False``. + """ + dtype = dtype if dtype is not None else torch.get_default_dtype() + if window_length == 0: + return torch.empty( + (0,), dtype=dtype, layout=layout, device=device, pin_memory=pin_memory + ) + if window_length == 1: + return torch.ones( + (1,), dtype=dtype, layout=layout, device=device, pin_memory=pin_memory + ) + compute_dtype = utils.get_computation_dtype(dtype) + n = window_length + 1 if periodic else window_length + t = torch.arange( + n, + dtype=compute_dtype, + layout=layout, + device=device, + pin_memory=pin_memory, + ) + t = t * (2.0 * torch.pi / (n - 1)) + t = torch.cos(t) + t = t * -0.5 + 0.5 + window = t.narrow(0, 0, window_length) if periodic else t + return window.to(dtype) + + +register_inplace(aten.addbmm_, aten.addbmm) +register_inplace(aten.addmm_, aten.addmm) +register_inplace(aten.addmv_, aten.addmv) +register_inplace(aten.baddbmm_, aten.baddbmm) +register_inplace(aten.fill_, aten.fill) +register_inplace(aten.gelu_, aten.gelu) +register_inplace(aten.hardswish_, aten.hardswish) +register_inplace(aten.hardtanh_, aten.hardtanh) +register_inplace(aten.hardsigmoid_, aten.hardsigmoid) +register_inplace(aten.__iand__, aten.__and__) +register_inplace(aten.__ilshift__, aten.__lshift__) +register_inplace(aten.index_put_, aten.index_put) +register_inplace(aten.index_reduce_, aten.index_reduce) +register_inplace(aten.__ior__, aten.__or__) +register_inplace(aten.__irshift__, aten.__rshift__) +register_inplace(aten.__ixor__, aten.__xor__) +register_inplace(aten.ldexp_, aten.ldexp) +register_inplace(aten.leaky_relu_, aten.leaky_relu) +register_inplace(aten.logit_, aten.logit) +register_inplace(aten.relu_, aten.relu) +register_inplace(aten.renorm_, aten.renorm) +register_inplace(aten.round_, aten.round) +register_inplace(aten.scatter_, aten.scatter) +register_inplace(aten.scatter_add_, aten.scatter_add) +register_inplace(aten.scatter_reduce_, aten.scatter_reduce) +register_inplace(aten.silu_, aten.silu) + + +@aten.one_hot.default.py_impl(DispatchKey.CompositeImplicitAutograd) +def one_hot(self: Tensor, num_classes: int = -1) -> Tensor: + if num_classes == -1: + num_classes = int(self.max().item()) + 1 + # _assert_async is side-effectful and won't be DCE'd + aten._assert_async.msg( + torch.all(self >= 0), + "one_hot: Class values must be non-negative.", + ) + aten._assert_async.msg( + torch.all(self < num_classes), + "one_hot: Class values must be smaller than num_classes.", + ) + return ( + self.unsqueeze(-1) + == torch.arange(num_classes, dtype=self.dtype, device=self.device) + ).to(torch.int64) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_decomp/decompositions_for_jvp.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_decomp/decompositions_for_jvp.py new file mode 100644 index 0000000000000000000000000000000000000000..45140652306c679fd78764a7bf94ea7462e21edc --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_decomp/decompositions_for_jvp.py @@ -0,0 +1,344 @@ +# mypy: allow-untyped-decorators +# mypy: allow-untyped-defs +import inspect +from collections.abc import Callable + +import torch +import torch._decomp +from torch import Tensor +from torch._prims_common.wrappers import _maybe_remove_out_wrapper + + +decomposition_table = torch._decomp.decomposition_table +decomposition_table_for_jvp: dict[torch._ops.OperatorBase, Callable] = {} +register_decomposition = torch._decomp.register_decomposition +aten = torch.ops.aten + +# NOTE: [forward-mode AD decompositions mechanism] +# +# The mechanism is in VariableType, +# IF any inputs have forward grad +# AND there is no forward AD formula implemented +# AND the functions are actually differentiable +# run the decomposition +# See run_jit_decomposition_with_args_for_jvp +# We currently use python decompositions that we torchscript. +# +# Note that we would be building the backward graph at the decomposed level +# too, but that is OK, because we would've errored out otherwise anyway. +# +# TODO: The mechanism we are using to register decompositions doesn't +# seem to be exclusively used for jvp. So open question here is whether +# torch/csrc/jit/runtime/decomposition_registry.cpp is being used for other things. +# If that is the case, we may go down the decomposition path unexpectedly +# (and possibly produce an unintelligible error) vs erroring out earlier and +# printing that the forward AD formula is not implemented. +# +# The solution to this may be to have an explicitly white list control when +# to enable the decomposition. + + +def maybe_register_decomposition(op): + def decorator(f): + try: + return register_decomposition(op)(f) + except Exception: + return f + + return decorator + + +# Functions where we need a special decomposition for jvp but there's another version that +# should be used more generally (ex. for jvp we need to recompute the mean and variance for +# the backwards of a normalization function. Without jvp, it should use the saved value) +decomposition_table_for_jvp = {} + + +def register_decomposition_for_jvp(fn): + return register_decomposition(fn, registry=decomposition_table_for_jvp) + + +def _register_jit_decomposition_for_jvp(decomp, use_python=False): + if decomp in decomposition_table_for_jvp: + decomposition_table_used = decomposition_table_for_jvp + elif decomp in decomposition_table: + decomposition_table_used = decomposition_table + else: + raise RuntimeError(f"could not find decomposition for {decomp}") + decomp_fn = decomposition_table_used[decomp] + + # `out_wrapper` extends a decompositions signature with + # an `out` parameter. However jit will use the unwrapped function's + # signature instead so we need to unwrap here to prevent an error + decomp_fn = _maybe_remove_out_wrapper(decomp_fn) + + if use_python: + decomp_fn = torch.jit.ignore(decomp_fn) + sig = inspect.signature(decomp_fn) + + # Create a string wrapping the function from the signature + # example output: + # def wrapped_decomp(x: torch.Tensor, y: int, z: int): + # return decomp_fn(x, y, z) + # Thanks copilot! + def get_function_def(sig): + param_def = [f"{param_str}" for param_str in sig.parameters.values()] + param_use = [f"{param_str}" for param_str in sig.parameters] + + return f"def wrapped_decomp({', '.join(param_def)}):\n return decomp_fn({', '.join(param_use)})\n" + + f_str = get_function_def(sig) + graph = torch.jit.CompilationUnit(f_str).wrapped_decomp.graph + else: + graph = torch.jit.script(decomp_fn).graph + torch.jit._register_decomposition(decomp, graph) + + +# The only decompositions here are temporary or hacks for the purposes of jvp + + +# TODO: do these also belong here? +@maybe_register_decomposition(aten.trace.default) +def trace(self: Tensor) -> Tensor: + return torch.sum(torch.diag(self)) + + +@maybe_register_decomposition(aten.log_sigmoid_forward.default) +def log_sigmoid_forward(self: Tensor) -> tuple[Tensor, Tensor]: + min = torch.minimum(self.new_zeros(()), self) + z = torch.exp(-torch.abs(self)) + if self.is_cuda or self.is_xpu: + buffer = self.new_zeros((0,)) + else: + buffer = z + return min - torch.log1p(z), buffer + + +def recompute_mean_var( + input: Tensor, rstd: Tensor, inner_dim_indices: list[int], keepdim: bool +): + # for most norm decompositions, it will be the same as the core version except for here. + # We recompute the mean and variance so that they track gradients through input + + mean = torch.mean(input, dim=inner_dim_indices, keepdim=keepdim) + var = torch.var(input, dim=inner_dim_indices, unbiased=False, keepdim=keepdim) + eps = torch.pow(1 / rstd, 2) - var # this makes me so sad inside + eps = eps.detach() + rstd = 1 / torch.sqrt(var + eps) + return mean, rstd + + +@register_decomposition_for_jvp(aten.native_layer_norm_backward) +def native_layer_norm_backward( + grad_out: Tensor, + input: Tensor, + normalized_shape: list[int], + mean: Tensor, + rstd: Tensor, + weight: Tensor | None, + bias: Tensor | None, + output_mask: list[bool], +) -> tuple[Tensor | None, Tensor | None, Tensor | None]: + input_shape = input.shape + input_ndim = input.dim() + + axis = input_ndim - len(normalized_shape) + inner_dims = input_shape[axis:] + outer_dims = input_shape[:axis] + inner_dim_indices = list(range(axis, input_ndim)) + outer_dim_indices = list(range(axis)) + + N = 1 + for i in inner_dims: + N *= i + M = 1 + for i in outer_dims: + M *= i + if M <= 0 or N <= 0: + return ( + input.new_zeros(input_shape), + input.new_zeros(input_shape[axis:]), + input.new_zeros(input_shape[axis:]), + ) + + mean_, rstd_ = recompute_mean_var(input, rstd, inner_dim_indices, keepdim=True) + + x_hat = (input - mean_) * rstd_ + if weight is not None: + grad_x_hat = grad_out * weight + else: + grad_x_hat = grad_out + a = grad_x_hat * N + b = torch.sum(grad_x_hat, inner_dim_indices, True) + c1 = torch.mul(grad_x_hat, x_hat) + c2 = torch.sum(c1, inner_dim_indices, True) + c3 = torch.mul(x_hat, c2) + inner = a - b - c3 + + if output_mask[0]: + d_input: Tensor | None = (rstd_ / N) * inner + else: + d_input = torch.zeros_like(input) # should be None but doesn't work with vjp + + if output_mask[1] and weight is not None: + if len(outer_dim_indices) > 0: + d_weight: Tensor | None = torch.sum( + grad_out * x_hat, outer_dim_indices, False + ) + else: + d_weight = grad_out * x_hat + elif weight is not None: + d_weight = torch.zeros_like(weight) # should be None but doesn't work with vjp + else: + d_weight = torch.zeros(()) # should be None but doesn't work with vjp + + if output_mask[2] and bias is not None: + if len(outer_dim_indices) > 0: + d_bias: Tensor | None = torch.sum(grad_out, outer_dim_indices, False) + else: + d_bias = grad_out.clone() + elif bias is not None: + d_bias = torch.zeros_like(bias) # should be None but doesn't work with vjp + else: + d_bias = torch.zeros(()) # should be None but doesn't work with vjp + + return (d_input, d_weight, d_bias) + + +def prod(x: list[int]): + r = 1 + for i in x: + r *= i + return r + + +@register_decomposition_for_jvp(aten.native_batch_norm_backward) +def native_batch_norm_backward( + grad_out: Tensor, + input: Tensor, + weight: Tensor | None, + running_mean: Tensor | None, + running_var: Tensor | None, + save_mean: Tensor | None, + save_invstd: Tensor | None, + train: bool, + eps: float, + output_mask: list[bool], +) -> tuple[Tensor, Tensor | None, Tensor | None]: + input_shape = input.shape + input_rank = input.dim() + if input_rank < 2: + raise AssertionError(f"rank of the input must be at least 2, got {input_rank}") + + axis = 1 + num_features = prod(input_shape) / input_shape[axis] # type: ignore[arg-type] + mean = save_mean + invstd = save_invstd + if train: + if save_mean is None or save_invstd is None: + raise AssertionError( + "when train=True, save_mean and save_invstd are required" + ) + + reduciton_dims = [0] + list(range(2, input.dim())) + if invstd is None: + raise AssertionError("invstd must not be None for typing") + mean, invstd = recompute_mean_var(input, invstd, reduciton_dims, keepdim=False) + else: + if running_mean is None or running_var is None: + raise AssertionError( + "running_mean and running_var must not be None when train=False" + ) + mean = running_mean + invstd = torch.rsqrt(running_var + eps) + + if invstd is None or mean is None: + raise AssertionError( + f"invstd and mean must not be None, got invstd={invstd}, mean={mean}" + ) + + broadcast_mask = [1] * input_rank + broadcast_mask[axis] = input_shape[axis] + + reduction_axes: list[int] = [] + for i in range(input_rank): + if i != axis: + reduction_axes.append(i) + + mean = torch.reshape(mean, broadcast_mask) + norm = 1.0 / num_features + grad_output_sum = torch.sum(grad_out, reduction_axes) + dot_p = torch.sum(grad_out * (input - mean), reduction_axes) + + grad_mean = torch.reshape(grad_output_sum * norm, broadcast_mask) + proj_scale = torch.reshape(torch.mul(dot_p * norm, invstd * invstd), broadcast_mask) + + if weight is None: + grad_scale = torch.reshape(invstd, broadcast_mask) * 1.0 + else: + grad_scale = torch.reshape(invstd * weight, broadcast_mask) + + if train: + proj = (input - mean) * proj_scale + grad_input = ((grad_out - proj) - grad_mean) * grad_scale + else: + grad_input = grad_out * grad_scale + + if output_mask[1]: + grad_weight = dot_p * invstd + elif weight is not None: + grad_weight = torch.zeros_like( + weight + ) # should be None but doesn't work with vjp + else: + grad_weight = torch.zeros(()) # should be None but doesn't work with vjp + + if output_mask[2]: + grad_bias = grad_output_sum + else: + grad_bias = torch.zeros_like( + grad_output_sum + ) # should be None but doesn't work with vjp + + return (grad_input, grad_weight, grad_bias) + + +@register_decomposition_for_jvp(aten.batch_norm_backward) +def batch_norm_backward( + grad_out: Tensor, + input: Tensor, + weight: Tensor, + running_mean: Tensor | None, + running_var: Tensor | None, + save_mean: Tensor | None, + save_var: Tensor | None, + update: bool, + eps: float, + output_mask: list[bool], + reserve: Tensor, +) -> tuple[Tensor, Tensor | None, Tensor | None]: + return native_batch_norm_backward( + grad_out, + input, + weight, + running_mean, + running_var, + save_mean, + save_var, + update, + eps, + output_mask, + ) + + +_register_jit_decomposition_for_jvp(torch.ops.aten.trace.default, use_python=True) +_register_jit_decomposition_for_jvp(torch.ops.aten.nll_loss_backward.default) +_register_jit_decomposition_for_jvp(torch.ops.aten.nll_loss2d_backward.default) +_register_jit_decomposition_for_jvp(torch.ops.aten._log_softmax_backward_data.default) +_register_jit_decomposition_for_jvp(torch.ops.aten._softmax_backward_data.default) +_register_jit_decomposition_for_jvp(torch.ops.aten.log_sigmoid_forward.default) +_register_jit_decomposition_for_jvp(torch.ops.aten.native_layer_norm_backward.default) +_register_jit_decomposition_for_jvp(torch.ops.aten.native_batch_norm_backward.default) +_register_jit_decomposition_for_jvp(torch.ops.aten.cudnn_batch_norm_backward.default) +_register_jit_decomposition_for_jvp(torch.ops.aten.batch_norm_backward.default) +_register_jit_decomposition_for_jvp(torch.ops.aten.miopen_batch_norm_backward.default) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_decomp/decompositions_for_rng.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_decomp/decompositions_for_rng.py new file mode 100644 index 0000000000000000000000000000000000000000..d1753b1baf5a888e93224949ae60f848da7678d2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_decomp/decompositions_for_rng.py @@ -0,0 +1,272 @@ +# mypy: allow-untyped-decorators +# mypy: allow-untyped-defs +import functools +from collections import defaultdict +from collections.abc import Callable + +import torch +import torch._decomp as decomp +from torch._decomp import get_decompositions +from torch._ops import OpOverload + + +aten = torch.ops.aten + +rng_decompositions: dict[str, dict[OpOverload, Callable]] = defaultdict(dict) + + +def register_rng_decomposition(aten_op): + return decomp.register_decomposition(aten_op, rng_decompositions) + + +def throw_on_non_cuda(device): + raise RuntimeError( + f"You are trying to functionalize a {device.type} RNG operator but {device.type} does not " + f"use Philox/counter-based RNG. Therefore, functionalizing a {device.type} RNG operator is " + "not supported. We are discussing the possibility of a Philox-based RNG implementation for CPU." + ) + + +# TODO - We have to register many more distributions here, and also higher level +# ops like dropout which have fused implementation and can hide the rand inside. +@register_rng_decomposition(aten.rand) +def rand(shape, dtype=None, layout=torch.strided, device=None, pin_memory=False): + if device and device.type != "cuda": + throw_on_non_cuda(device) + seed, offset = PhiloxStateTracker.get_state_as_tuple() + dtype = dtype or torch.float32 + out, offset_jump = torch.ops.rngprims.philox_rand( + shape, seed, offset, None, device, dtype + ) + PhiloxStateTracker.advance_offset(offset_jump) + return out + + +@register_rng_decomposition(aten.rand_like) +def rand_like( + x: torch.Tensor, + dtype=None, + layout=None, + device=None, + pin_memory=False, + memory_format=torch.preserve_format, +): + device = device or x.device + if device.type != "cuda": + throw_on_non_cuda(device) + dtype = dtype or x.dtype + seed, offset = PhiloxStateTracker.get_state_as_tuple() + out, offset_jump = torch.ops.rngprims.philox_rand( + x.shape, seed, offset, None, device, dtype + ) + PhiloxStateTracker.advance_offset(offset_jump) + return out + + +class PhiloxState: + """ + Represents a PhiloxRngState - (seed, offset) where offset = base_offset + + relative_offset. seed and base_offset basically point to the rng state just + before tracing starts. relative offset tracks the totally consumed offset at + trace time. + """ + + def __init__(self) -> None: + self.reset() + + def reset(self): + self.seed = torch.tensor(()) + self.base_offset = torch.tensor(()) + self.relative_offset = 0 + self.offset_advanced_at_least_once = False + + def validate_state(self): + if self.seed.numel() == 0 or self.base_offset.numel() == 0: + raise AssertionError( + f"seed and base_offset must not be empty, got " + f"seed.numel()={self.seed.numel()}, base_offset.numel()={self.base_offset.numel()}" + ) + + def advance_offset(self, consumed_offset): + self.offset_advanced_at_least_once = True + self.relative_offset = self.relative_offset + consumed_offset + + def set_state(self, seed, base_offset, relative_offset=0): + self.seed = seed + self.base_offset = base_offset + self.relative_offset = relative_offset + + def get_state_as_tuple(self): + self.validate_state() + return (self.seed, self.base_offset + self.relative_offset) + + def get_state_as_tensor(self): + # Only needed because we override get_rng_state. + self.validate_state() + return torch.stack([self.seed, self.base_offset + self.relative_offset]) + + def set_state_from_tensor(self, state): + # Only needed because we override set_rng_state. + self.seed, self.base_offset = torch.unbind(state) + self.relative_offset = 0 + + +class PhiloxStateTracker: + """ + Singleton class to track the philox rng state during AOT Autograd tracing. + For each aot tracing instance, AOT Autograd resets this tracker and keeps + track of both forward and backward offsets. At runtime, we only care about + the total consumed forward and backward offsets. For dynamic shapes, these + offsets are a function of input shapes. Therefore, the AOT generated graphs + have additional outputs that compute total consumed forward and backward + offsets. + """ + + running_state: PhiloxState + fwd_state: PhiloxState + bwd_state: PhiloxState + + def __enter__(self): + PhiloxStateTracker.reset() + return self + + def __exit__(self, exc_type, exc_cal, exc_tb): + PhiloxStateTracker.reset() + + @classmethod + def reset(cls): + cls.running_state = PhiloxState() + cls.fwd_state = PhiloxState() + cls.bwd_state = PhiloxState() + + @classmethod + def mark_beginning_of_forward(cls): + # Tells the tracker to use fwd_state as the running state + cls.running_state = cls.fwd_state + + @classmethod + def mark_beginning_of_backward(cls): + # Tells the tracker to use bwd_state as the running state + cls.running_state = cls.bwd_state + + @classmethod + def record_state(cls, seed, offset, mode): + # Records the seed and offset tensors. These tensors are used to invoke + # the philox_rand functional primitives. + if mode == "forward": + cls.fwd_state.set_state(seed, offset) + cls.mark_beginning_of_forward() + else: + if mode != "backward": + raise AssertionError(f"mode must be 'backward', got {mode}") + cls.bwd_state.set_state(seed, offset) + + @classmethod + def get_state_as_tensor(cls): + # The only reason this exists is because we override get_rng_state and + # set_rng_state during tracing. get_rng_state expects a tensor output, + # so return (seed, offset) tuple upset other parts of the program like + # ctx.saved_tensors. + + # A bad consequence is that if user saves and restores rng state, we + # have little bit of ugliness in the generated code, where we first + # concat the (seed, offset) to create a tensor for get_rng_state, and + # then split it back to get (seed, offset) tuple in set_rng_state. + + # TODO: Investigate if there is be a better way to wrap the tuple in a + # false Tensor object, and then desugar it later on. + return cls.running_state.get_state_as_tensor() + + @classmethod + def get_state_as_tuple(cls): + return cls.running_state.get_state_as_tuple() + + @classmethod + def set_state_from_tensor(cls, x): + # This is only needed because we override set_rng_state. Look at the + # comment in get_state_from_tensor method. + cls.running_state.set_state_from_tensor(x) + + @classmethod + def advance_offset(cls, consumed_offset): + cls.running_state.advance_offset(consumed_offset) + + @classmethod + def get_current_relative_offset(cls): + return cls.running_state.relative_offset + + @staticmethod + def multiple_of_4(offset): + # torch cuda rng state offset must be a multiple of 4. For inductor, as + # we sum up all the numel, the result might not be a multiple of 4. This + # method achieves that. + return (offset + 3) // 4 * 4 + + @classmethod + def get_updated_fwd_offset(cls): + # Short circuit if no rand ops were observed + if not cls.fwd_state.offset_advanced_at_least_once: + return cls.fwd_state.base_offset + return cls.multiple_of_4( + cls.fwd_state.base_offset + cls.fwd_state.relative_offset + ) + + @classmethod + def get_updated_bwd_offset(cls): + # Short circuit if no rand ops were observed + if not cls.bwd_state.offset_advanced_at_least_once: + return cls.bwd_state.base_offset + return cls.multiple_of_4( + cls.bwd_state.base_offset + cls.bwd_state.relative_offset + ) + + +# Adding more decompositions which eventually use rand_like inside decomps. +# Adding these in rng_decompositions ensures the functionalization of rand_like +# ops used in these decomps. The list is copied from inductor codebase, which +# uses it for similar purpose. +# +# Caution - These decomps do not have same accuracy as that of eager. However, +# we can't just disable them with a config flag like fallback_random, because +# for functionalization of rng ops, we have to decompose these ops. +extra_random_decomps = get_decompositions( + [ + aten.cauchy, + aten.cauchy_, + aten.exponential, + aten.exponential_, + aten.geometric, + aten.geometric_, + aten.native_dropout, + aten.normal, + aten.normal_, + aten.normal_functional, + aten.log_normal, + aten.log_normal_, + aten.rrelu_with_noise, + aten.rrelu_with_noise_, + aten.uniform_, + ] +) +register_extra_random_decomp = functools.partial( + decomp.register_decomposition, registry=extra_random_decomps +) + + +@register_extra_random_decomp([aten.bernoulli_]) +def bernoulli_(self, p=0.5): + if self.device == torch.device("cpu"): + return NotImplemented + return self.copy_(torch.rand_like(self, dtype=torch.float32) < p) + + +@register_extra_random_decomp([aten.bernoulli.p]) +def bernoulli_p(self, p=0.5, *, generator=None): + if self.device == torch.device("cpu"): + return NotImplemented + if generator is not None: + raise AssertionError(f"generator must be None, got {generator}") + return torch.rand_like(self, dtype=torch.float32) < p + + +rng_decompositions.update(extra_random_decomps) # type: ignore[arg-type] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dispatch/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dispatch/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dispatch/python.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dispatch/python.py new file mode 100644 index 0000000000000000000000000000000000000000..ef6d1d652bf2da3b966945c01770b7743e366a7f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dispatch/python.py @@ -0,0 +1,204 @@ +import itertools +import unittest.mock +from collections.abc import Callable, Generator, Iterator +from contextlib import contextmanager +from typing import TypeVar +from typing_extensions import ParamSpec + +import torch +import torch._C +import torch._ops +import torch.utils._python_dispatch +import torch.utils._pytree as pytree +from torch._C import DispatchKey + + +__all__ = ["enable_python_dispatcher", "no_python_dispatcher", "enable_pre_dispatch"] + +no_python_dispatcher = torch._C._DisablePythonDispatcher +enable_python_dispatcher = torch._C._EnablePythonDispatcher +enable_pre_dispatch = torch._C._EnablePreDispatch + +CROSSREF_FUNCTIONALIZE = False + +_P = ParamSpec("_P") +_T = TypeVar("_T") +_R = TypeVar("_R") + + +def all_py_loaded_overloads() -> Iterator[torch._ops.OpOverload]: + """ + Warning: the set of overloads this will report is very subtle. It is precisely + the set of torch.ops functions that have actually been accessed from Python + (e.g., we actually called torch.ops.aten.blah at some point. This is DIFFERENT + from the set of registered operators, which will in general be a larger set, + as this would include all operators which we ran C++ static initializers or + Python operator registration on. This does not eagerly populate the list on + torch.ops.aten; this list is lazy! + + In other words, this is good for traversing over everything that has an + OpOverload object allocated in Python. We use it for cache invalidation, but + don't rely on this list being complete. + + Note that even if we did report all C++ registered overloads, this isn't guaranteed + to be complete either, as a subsequent lazy load of a library which triggers more + registrations could add more things to the set. + """ + for ns in torch.ops: + packets = getattr(torch.ops, ns) + for op_name in packets: + packet = getattr(packets, op_name) + for overload in packet: + yield getattr(packet, overload) + + +@contextmanager +def suspend_functionalization() -> Generator[None, None, None]: + f_tls = torch._C._dispatch_tls_is_dispatch_key_included( + torch._C.DispatchKey.Functionalize + ) + f_rv = torch._C._functionalization_reapply_views_tls() + if f_tls: + torch._disable_functionalization() + try: + yield + finally: + if f_tls: + torch._enable_functionalization(reapply_views=f_rv) + + +def check_tensor_metadata_matches( + nv: torch.Tensor, rv: torch.Tensor, desc: Callable[[], str] +) -> None: + if not callable(desc): + raise AssertionError(f"desc must be callable, got {type(desc)}") + if nv.size() != rv.size(): + raise AssertionError(f"{desc()}: sizes {nv.size()} != {rv.size()}") + if nv.dtype != rv.dtype: + raise AssertionError(f"{desc()}: dtype {nv.dtype} != {rv.dtype}") + same_strides, idx = torch._prims_common.check_significant_strides( + nv, rv, only_cuda=False + ) + if not same_strides: + raise AssertionError( + f"{desc()}: strides {nv.stride()} != {rv.stride()} (mismatch at index {idx})" + ) + + +def check_metadata_matches(n: object, r: object, desc: Callable[[], str]) -> None: + if not callable(desc): + raise AssertionError(f"desc must be callable, got {type(desc)}") + n_vals, _n_spec = pytree.tree_flatten(n) + r_vals, _r_spec = pytree.tree_flatten(r) + # TODO: test the specs match; empirically sometimes we have a tuple + # on one side and a list on the other + if len(n_vals) != len(r_vals): + raise AssertionError(f"{len(n_vals)} != {len(r_vals)}") + for i, nv, rv in zip(range(len(n_vals)), n_vals, r_vals): + if not isinstance(rv, torch.Tensor): + continue + check_tensor_metadata_matches(nv, rv, lambda: f"{desc()} output {i}") + + +class Lit: + def __init__(self, s: str) -> None: + self.s = s + + def __repr__(self) -> str: + return self.s + + +def _fmt(a: object) -> object: + if isinstance(a, torch.Tensor): + return Lit( + f"torch.empty_strided({tuple(a.size())}, {a.stride()}, dtype={a.dtype})" + ) + else: + return a + + +def make_crossref_functionalize( + op: torch._ops.OpOverload[_P, _T], final_key: DispatchKey +) -> Callable[_P, _T] | DispatchKey: + from torch._subclasses.fake_tensor import FakeTensorMode + + # This case is pretty weird, suppress it for now + if op is torch.ops.aten.lift_fresh.default: + return final_key + + def handler(*args: _P.args, **kwargs: _P.kwargs) -> _T: + fake_mode = FakeTensorMode() + + def fakeify_defun(t: _R) -> _R | torch._subclasses.fake_tensor.FakeTensor: + if isinstance(t, torch.Tensor): + if torch._is_functional_tensor(t): + r = torch._from_functional_tensor(t) + # NB: This assumes that the inner tensor sizes/strides match + # the outer tensor sizes/strides. This doesn't necessarily have to + # be the case, see discussion at + # https://github.com/pytorch/pytorch/pull/87610/files/401ddeda1d769bedc88a12de332c7357b60e51a4#r1007264456 + if t.size() != r.size(): + raise AssertionError(f"size mismatch: {t.size()} != {r.size()}") + if t.stride() != r.stride(): + raise AssertionError( + f"stride mismatch: {t.stride()} != {r.stride()}" + ) + else: + r = t + # TODO: suppress guards + return fake_mode.from_tensor(r) + return t + + def maybe_detach(t: _R) -> _R | torch.Tensor: + if isinstance(t, torch.Tensor): + return t.detach() + else: + return t + + # TODO: This probably does the wrong thing if you're running other + # substantive modes with the normal op outside here + with ( + torch.utils._python_dispatch._disable_current_modes(), + suspend_functionalization(), + ): + f_args, f_kwargs = pytree.tree_map(fakeify_defun, (args, kwargs)) + orig_f_args, orig_f_kwargs = pytree.tree_map( + maybe_detach, (f_args, f_kwargs) + ) + with fake_mode: + f_r = op(*f_args, **f_kwargs) # pyrefly: ignore [invalid-param-spec] + r = op._op_dk(final_key, *args, **kwargs) + + def desc() -> str: + fmt_args = ", ".join( + itertools.chain( + (repr(pytree.tree_map(_fmt, a)) for a in orig_f_args), + ( + f"{k}={pytree.tree_map(_fmt, v)}" + for k, v in orig_f_kwargs.items() + ), + ) + ) + return f"{op}({fmt_args})" + + check_metadata_matches(f_r, r, desc) + return r + + return handler + + +# NB: enabling this is slow, don't do it in a hot loop. This is purely +# for debugging purposes. +@contextmanager +def enable_crossref_functionalize() -> Generator[None, None, None]: + for op in all_py_loaded_overloads(): + op._uncache_dispatch(torch._C.DispatchKey.Functionalize) + try: + with ( + enable_python_dispatcher(), + unittest.mock.patch("torch._dispatch.python.CROSSREF_FUNCTIONALIZE", True), + ): + yield + finally: + for op in all_py_loaded_overloads(): + op._uncache_dispatch(torch._C.DispatchKey.Functionalize) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f258338c3baf915a074fcdde881ac2765d05bd7c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/__init__.py @@ -0,0 +1,233 @@ +""" +TorchDynamo is a Python-level JIT compiler designed to make unmodified PyTorch programs faster. +TorchDynamo hooks into the frame evaluation API in CPython (PEP 523) to dynamically modify Python +bytecode right before it is executed. It rewrites Python bytecode in order to extract sequences of +PyTorch operations into an FX Graph which is then just-in-time compiled with a customizable backend. +It creates this FX Graph through bytecode analysis and is designed to mix Python execution with +compiled backends to get the best of both worlds: usability and performance. This allows it to +seamlessly optimize PyTorch programs, including those using modern Python features. +""" + +import torch + +from . import ( + aot_compile, + bytecode_debugger, + config, + convert_frame, + eval_frame, + functional_export, + resume_execution, +) +from .backends.registry import list_backends, lookup_backend, register_backend +from .callback import callback_handler, on_compile_end, on_compile_start +from .code_context import code_context +from .convert_frame import replay +from .decorators import ( + allow_in_graph, + assume_constant_result, + disable, + disable_nested_graph_breaks, + disallow_in_graph, + dont_skip_tracing, + error_on_graph_break, + forbid_in_graph, + graph_break, + is_dynamo_disable_recursive, + mark_dynamic, + mark_static, + mark_static_address, + maybe_mark_dynamic, + nonstrict_trace, + override_cudagraphs, + patch_dynamo_config, + run, + set_stance, + skip_frame, + step_unsupported, + substitute_in_graph, +) +from .eval_frame import ( + _reset_guarded_backend_cache, + explain, + export, + is_dynamo_supported, + is_inductor_supported, + optimize, + optimize_assert, + OptimizedModule, + reset_code, +) + +# pyrefly: ignore [deprecated] +from .external_utils import is_compiling +from .mutation_guard import GenerationTracker +from .pgo import reset_code_state +from .symbolic_convert import TensorifyState +from .utils import ( + graph_break_reasons, + guard_failures, + orig_code_map, + register_hook_for_recompile_user_context, + reset_frame_count, + reset_recompile_user_contexts, +) + + +# Register polyfill functions +from .polyfills import loader as _ # usort: skip # noqa: F401 + + +__all__ = [ + "allow_in_graph", + "assume_constant_result", + "bytecode_debugger", + "config", + "disable", + "disable_nested_graph_breaks", + "disallow_in_graph", + "dont_skip_tracing", + "export", + "explain", + "forbid_in_graph", + "graph_break", + "is_compiling", + "is_dynamo_disable_recursive", + "list_backends", + "lookup_backend", + "mark_dynamic", + "maybe_mark_dynamic", + "mark_static", + "mark_static_address", + "nonstrict_trace", + "optimize", + "optimize_assert", + "OptimizedModule", + "patch_dynamo_config", + "register_backend", + "replay", + "reset", + "reset_recompile_user_contexts", + "run", + "override_cudagraphs", + "error_on_graph_break", + "set_recursion_limit", + "set_stance", + "skip_frame", + "step_unsupported", + "substitute_in_graph", +] + +# allowlist this for weights_only load of NJTs +torch.serialization.add_safe_globals([torch._dynamo.decorators._DimRange]) + +if torch.manual_seed is torch.random.manual_seed: + import torch.jit._builtins + + # Wrap manual_seed with the disable decorator. + # Can't do it at its implementation due to dependency issues. + torch.manual_seed = torch._disable_dynamo(torch.manual_seed) + # Add the new manual_seed to the builtin registry. + torch.jit._builtins._register_builtin(torch.manual_seed, "aten::manual_seed") + + +def reset() -> None: + """ + Clear all compile caches and restore initial state. This function is intended + to reset Dynamo's state *as if* you had started a fresh process invocation, which + makes it good for testing scenarios where you want to behave as if you started + a new process. It does NOT affect any file system caches. + + NB: this does NOT reset logging state. Don't use this to test logging + initialization/reinitialization. + """ + # TODO: https://github.com/pytorch/pytorch/issues/139200 + import logging + + log = logging.getLogger(__name__) + log.info("torch._dynamo.reset") + with convert_frame.compile_lock: + reset_code_caches() + convert_frame.input_codes.clear() + reset_code_state() + convert_frame.output_codes.clear() + orig_code_map.clear() + guard_failures.clear() + graph_break_reasons.clear() + resume_execution.ContinueExecutionCache.cache.clear() + _reset_guarded_backend_cache() + reset_frame_count() + torch._dynamo.compiled_autograd.reset() + convert_frame.FRAME_COUNTER = 0 + convert_frame.FRAME_COMPILE_COUNTER.clear() + callback_handler.clear() + GenerationTracker.clear() + TensorifyState.clear() + torch._dynamo.utils.warn_once_cache.clear() + torch._C._autograd._saved_tensors_hooks_set_tracing(False) + + # Reset cudagraph trees unconditionally since they are global state + # not tied to a specific backend instance + from torch._higher_order_ops.triton_kernel_wrap import kernel_side_table + from torch._higher_order_ops.wrap import inductor_code_side_table + + kernel_side_table.reset_table() + inductor_code_side_table.reset_table() + + if torch.cuda.is_available(): + from torch._inductor.cudagraph_trees import reset_cudagraph_trees + + reset_cudagraph_trees() + + +def reset_code_caches() -> None: + """ + Clears in-memory code cache, which is what stores compiled products. This + resets less state than :func:`reset` and is mostly only used for testing + purposes. + """ + # TODO: https://github.com/pytorch/pytorch/issues/139200 + import logging + + log = logging.getLogger(__name__) + log.info("torch._dynamo.reset_code_caches") + """Clear compile caches that are keyed by code objects""" + with convert_frame.compile_lock: + reset_code_state() + for weak_code in ( + convert_frame.input_codes.seen + convert_frame.output_codes.seen + ): + code = weak_code() + if code: + reset_code(code) + code_context.clear() + + +def get_recursion_limit() -> int: + """ + Returns the internal dynamo recursion limit set by `torch._dynamo.set_recursion_limit`. + + Returns -1 if no c recursion limit has been set. + """ + return torch._C._dynamo.eval_frame.get_c_recursion_limit() + + +def set_recursion_limit(limit: int) -> None: + """ + Sets an internal dynamo recursion limit. The limit must be >= 1, or -1 to reset + to the default (unset) state. + + This is possibly needed in Python 3.12-3.13 since there is a separate C recursion limit + that is not visible at the Python level. If you are getting RecursionErrors during + Dynamo compilation and `sys.setrecursionlimit()` doesn't help, this function may alleviate + the issue. + + NOTE: this function does NOT call `sys.setrecursionlimit()` - the user is expected to manually + call this if required. This is because the 2 recursion limits are not sync'd up - e.g. in + Python 3.12, functions can be inline-evaluated, which apparently doesn't use up the C stack. + + WARNING: increasing the recursion limit to an arbitrary large value may cause segfaults + due to stack overflows! You can try also try to manually increase the stack size, e.g. + with `$ ulimit -s ...` + """ + torch._C._dynamo.eval_frame.set_c_recursion_limit(limit) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/_trace_wrapped_higher_order_op.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/_trace_wrapped_higher_order_op.py new file mode 100644 index 0000000000000000000000000000000000000000..93772b05e30221c314982d2ac3d364ca1d686517 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/_trace_wrapped_higher_order_op.py @@ -0,0 +1,262 @@ +"""trace_wrapped(*args, fn) is equivalent to fn(*args), but with a twist: +if you make_fx trace through this call, we will not actually trace into fn; instead, +we will directly insert it as a call_function to fn in the graph. +(Unlike make_fx, Dynamo WILL inline into fn.) +You can think of this as a one off allow_in_graph equivalent for proxy tensor tracing. + +Because proxy tensor tracing does not actually run the function, there are +requirements on the behavior of fn. We are still figuring it out, but here is the current state: + +1) fn SHOULD only take a single argument, which must be a tensor +2) fn MUST return a new tensor with the same metadata as the original tensor + (e.g., zeros_like(input) is a permissible implementation of fn). + This is verified via an extra assert that is inserted into the traced graph. +3) fn MAY have side effects, but it MAY NOT perform metadata mutation on other tensors + participating in proxy tensor tracing (it MAY mutate other tensors, it MAY mutate Python state) +These requirements stem from the requirement that we need to continue performing proxy tensor tracing, +which assumes accurate fake tensor metadata, without actually running fn. +In the future, we may allow for a "meta" function associated with fn to allow for more interesting input-output patterns. + +Note that tensors / Python state are allowed to be mutated. +This is relaxed constraint is not always sound, but it is sound for backward tracing with fake +tensors as it takes place in AOTAutograd, as the backward pass is guaranteed not to depend on concrete +tensor values (via fake tensor) or Python state (because the autograd engine doesn't depend on Python). + +The intended use case for this function is to allow AOTAutograd to defer complex +backward hooks to compiled autograd. AOTAutograd performs a make_fx trace which preserves +the function call as is in the graph, and only when we Dynamo through the backward graph in +compiled autograd do we inline into the function. +""" + +from typing import Any + +import torch +import torch.utils._pytree as pytree +from torch._C import DispatchKey +from torch._higher_order_ops.utils import autograd_not_implemented +from torch._ops import HigherOrderOperator, OpOverload +from torch._subclasses import FakeTensorMode +from torch.fx.experimental._backward_state import BackwardState +from torch.fx.experimental.proxy_tensor import ProxyTorchDispatchMode, track_tensor_tree +from torch.overrides import TorchFunctionMode +from torch.utils._python_dispatch import _get_current_dispatch_mode +from torch.utils._pytree import tree_map_only + + +Tensor = torch.Tensor + + +__all__ = ["trace_wrapped"] + + +@torch.library.custom_op("flex_lib::zeros_and_scatter", mutates_args=()) # type: ignore[misc] +def zeros_and_scatter( + shape: list[int], + indices: list[Tensor], + vals: Tensor, +) -> Tensor: + """Custom Op so that we can register a custom lowering for the new_output + scatter in the backwards pass""" + grad = torch.zeros(shape, device=vals.device, dtype=vals.dtype) + return torch.ops.aten.index_put(grad, indices, vals, accumulate=True) + + +@zeros_and_scatter.register_fake # type: ignore[misc] +def _( + shape: list[int], + indices: list[Tensor], + vals: Tensor, +) -> Tensor: + return vals.new_empty(shape) + + +@zeros_and_scatter.register_vmap # type: ignore[misc] +def _(info, indims, shape, indices, value): # type: ignore[no-untyped-def] + """The batching rule is special in that it returns a tensor that is not batched""" + indices_indims = indims[1] + expanded_indices = [] + for idx, idx_indim in zip(indices, indices_indims): + # The index is not a being batched, we should unsqueeze and expand to val + if idx_indim is None: + expanded_indices.append(idx.expand(value.shape)) + else: + # the index is being part of the vmap batch, it should be the same size as val + assert idx.shape == value.shape + expanded_indices.append(idx) + + out = torch.ops.flex_lib.zeros_and_scatter( + shape, + expanded_indices, + value, + ) + return out, None + + +class ModIndex(torch.autograd.Function): + generate_vmap_rule = True + + @staticmethod + # pyrefly: ignore [bad-override] + def forward(x: Tensor, indices: list[Tensor]) -> Tensor: + return torch.ops.aten.index(x, indices) + + @staticmethod + def setup_context(ctx: Any, inputs: tuple[Any, ...], output: Any) -> None: + x, indices = inputs + ctx.save_for_backward(*indices) + ctx.input_shape = x.shape + + @staticmethod + def backward(ctx, gradOut): # type: ignore[no-untyped-def] + indices = ctx.saved_tensors + return ( + torch.ops.flex_lib.zeros_and_scatter( + ctx.input_shape, + indices, + gradOut, + ), + None, + ) + + @classmethod + @torch._export.wrappers.allow_in_pre_dispatch_graph + def apply(cls, *args, **kwargs): # type: ignore[no-untyped-def] + return super().apply(*args, **kwargs) + + +mod_index = ModIndex.apply + + +class TransformGetItemToIndex(TorchFunctionMode): + # This is needed since we want to support calling + # A[q_idx], where q_idx is a scalar tensor in score_mod. + # Today, when q_idx is a scalar tensor, we implicitly convert it to a python + # scalar and create a view. We do not want that behavior in this case, so we + # use this torchfunctionmode to override that behavior for score_mod + # wherever we're running it. + # + # We also convert integer indices to 0-D tensors so that temp[0] produces + # the same backward graph as temp[0 * q_idx] (zeros_and_scatter with atomic_add). + def __torch_function__( + self, + func: OpOverload, + types: tuple[torch._C._TensorMeta, ...], + args: tuple[object, ...] = (), + kwargs: dict[str, object] | None = None, + ) -> object: + if func is torch.Tensor.__getitem__: + tensor_to_index = args[0] + assert isinstance(tensor_to_index, torch.Tensor) + index_args = pytree.tree_leaves(args[1]) + if all(isinstance(x, (torch.Tensor, int)) for x in index_args): + converted_indices = [ + torch.tensor(x, dtype=torch.int64, device=tensor_to_index.device) + if isinstance(x, int) + else x + for x in index_args + ] + return mod_index(tensor_to_index, converted_indices) + return func(*args, **(kwargs or {})) + + +def trace_wrapped(*args: Any, **kwargs: Any) -> Any: + with torch.no_grad(): + return _trace_wrapped_op(*args, **kwargs) + + +class TraceWrapped(HigherOrderOperator): + def __init__(self) -> None: + super().__init__("trace_wrapped") + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + # pyrefly: ignore [missing-attribute] + return super().__call__(*args, **kwargs) + + +# TODO(jansel): need to ensure this does not get DCEed +_trace_wrapped_op = TraceWrapped() + + +def _assert_meta( + grad: torch.Tensor, + size: tuple[int, ...], + stride: tuple[int, ...], + dtype: torch.dtype, +) -> torch.Tensor: + assert grad.size() == size, "size mismatch" + assert grad.stride() == stride, "stride mismatch" + assert grad.dtype == dtype, "dtype mismatch" + return grad + + +@_trace_wrapped_op.py_impl(ProxyTorchDispatchMode) +def inner_trace( + mode: ProxyTorchDispatchMode, + *args: Any, + bw_state: BackwardState | None = None, + **kwargs: Any, +) -> Any: + def self_invoke(*args: Any, **dyn_kwargs: Any) -> Any: + with torch.no_grad(): + return _trace_wrapped_op(*args, **dyn_kwargs, **kwargs) + + def unwrap_proxies(x: Any) -> Any: + if isinstance(x, torch.Tensor): + return mode.tracer.unwrap_proxy(x) # type: ignore[union-attr] + if isinstance(x, (list, tuple)): + return type(x)(map(unwrap_proxies, x)) + if x is None: + return None + raise AssertionError(f"unhandled type: {type(x)}") + + proxy_kwargs = {} + if bw_state is not None: + assert isinstance(bw_state, BackwardState) and bw_state.proxy is not None + proxy_kwargs["bw_state"] = bw_state.proxy + out_proxy = mode.tracer.create_proxy( + "call_function", + self_invoke, + unwrap_proxies(args), + proxy_kwargs, + name="trace_wrapped", + ) + + if args[0] is None: + grad = args[1] # module backward hooks + else: + grad = args[0] # other backward hooks + grad = tree_map_only(torch.Tensor, torch.empty_like, grad) + track_tensor_tree(grad, out_proxy, constant=None, tracer=mode.tracer) + return grad + + +@_trace_wrapped_op.py_impl(FakeTensorMode) +def inner_fake(*args: Any, **kwargs: Any) -> None: + raise RuntimeError("This op should never be invoked here") + + +@_trace_wrapped_op.py_impl(DispatchKey.CompositeExplicitAutograd) +def _trace_wrapped_op_dense(*args: Any, fn: Any, **kwargs: Any) -> Any: + mode = _get_current_dispatch_mode() + assert mode is None, "Mode should never be enabled for CPU/CUDA key" + return fn(*args, **kwargs) + + +_trace_wrapped_op.py_impl(DispatchKey.Autograd)( + autograd_not_implemented(_trace_wrapped_op, deferred_error=True) +) + + +@_trace_wrapped_op.py_functionalize_impl +def _trace_wrapped_functionalized(ctx: Any, *args: Any, **kwargs: Any) -> Any: + unwrapped_args = ctx.unwrap_tensors(args) + with ctx.redispatch_to_next(): + return ctx.wrap_tensors(_trace_wrapped_op(*unwrapped_args, **kwargs)) + + +def autograd_function_backward_rewritten(original_backward: Any) -> Any: + def new_backward(ctx: Any, *grads: Any) -> Any: + # pyrefly: ignore [bad-assignment] + grads = [g.contiguous() for g in grads] + return original_backward(ctx, *grads) + + return new_backward diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/aot_compile.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/aot_compile.py new file mode 100644 index 0000000000000000000000000000000000000000..7ac01d6cb54885f6b18724e5f19e75344ddba212 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/aot_compile.py @@ -0,0 +1,539 @@ +import dataclasses +import importlib +import inspect +import io +import logging +import os +import pickle +import tempfile +import types +from collections.abc import Callable, Sequence +from contextlib import AbstractContextManager, ExitStack, nullcontext +from dataclasses import dataclass +from typing import Any, Optional, TYPE_CHECKING + +import torch +import torch.fx +from torch._dynamo.convert_frame import GraphRuntimeEnv +from torch._dynamo.graph_utils import _graph_device_type +from torch._dynamo.package import SystemInfo + +from . import convert_frame +from .aot_compile_types import ( + BundledAOTAutogradSerializableCallable, + SerializableCallable, +) +from .hooks import Hooks + + +if TYPE_CHECKING: + from .guards import GuardManagerWrapper + from .package import SerializedCode, SourceInfo + + +log = logging.getLogger(__name__) + + +def bind_locals( + signature: inspect.Signature, *args: Any, **kwargs: Any +) -> dict[str, Any]: + bound_arguments = signature.bind(*args, **kwargs) + bound_arguments.apply_defaults() + return bound_arguments.arguments + + +@dataclass +class CompileArtifacts: + signature: inspect.Signature + guard_manager: Optional["GuardManagerWrapper"] + guards_state: bytes + backend_id: str + compiled_fn: SerializableCallable + original_code: types.CodeType + runtime_env: GraphRuntimeEnv + source_info: "SourceInfo" + device_type: str + backend_name: str + system_info: SystemInfo = dataclasses.field(default_factory=SystemInfo.current) + + def check_compatibility(self) -> None: + current_system = SystemInfo.current() + current_system.check_compatibility(self.system_info, self.device_type) + + +class AOTCompilePickler(pickle.Pickler): + def __init__(self, external_data: dict[str, object], buf: io.BytesIO) -> None: + super().__init__(buf) + self.external_data = external_data + self.id_map: dict[int, str] = { + id(value): key for key, value in external_data.items() + } + self.errors = {} + + def persistent_id(self, obj: object) -> int | str | None: + if id(obj) in self.id_map: + return self.id_map[id(obj)] + elif isinstance(obj, torch.nn.Module): + self.errors[id(obj)] = obj + return id(obj) + else: + return None + + @classmethod + def _unpickle_cell(cls, val: object) -> object: + def _() -> object: + return val + + assert _.__closure__ is not None + return _.__closure__[0] + + @classmethod + # pyrefly: ignore [implicit-any] + def _unpickle_bound_method(cls, func: Callable, base: object) -> types.MethodType: + return types.MethodType(func, base) + + @classmethod + def _unpickle_module(cls, name: str) -> types.ModuleType: + return importlib.import_module(name) + + @classmethod + def _unpickle_code(cls, serialized_code: "SerializedCode") -> types.CodeType: + from torch._dynamo.package import SerializedCode + + return SerializedCode.to_code_object(serialized_code) + + @classmethod + def _unpickle_nested_function( + cls, + code: types.CodeType, + module: str, + qualname: str, + argdefs: tuple[object, ...] | None, + closure: tuple[types.CellType, ...] | None, + ) -> types.FunctionType: + f_globals = importlib.import_module(module).__dict__ + return types.FunctionType(code, f_globals, qualname, argdefs, closure) + + # pyrefly: ignore [bad-override] + def reducer_override(self, obj: Any) -> Any: + if isinstance(obj, type((lambda x: lambda: x)(0).__closure__[0])): # type: ignore[index] # noqa: PLC3002 + return type(self)._unpickle_cell, (obj.cell_contents,) + elif inspect.iscode(obj): + from torch._dynamo.package import SerializedCode + + return type(self)._unpickle_code, (SerializedCode.from_code_object(obj),) + + elif inspect.ismodule(obj): + return type(self)._unpickle_module, (obj.__name__,) + elif inspect.ismethod(obj): + """ + By default, pickle will call getattr() directly on the self object + for pickling bounded methods, this is not what we want, instead we + always want to serialize the original function and the self object + in their original form. + """ + func = obj.__func__ + method_self = obj.__self__ + inner_func = getattr(method_self, func.__name__) + if inspect.ismethod(inner_func): + inner_func = inner_func.__func__ + if func is not inner_func: + return type(self)._unpickle_bound_method, (func, method_self) + elif inspect.isfunction(obj): + if "" in obj.__qualname__: + return type(self)._unpickle_nested_function, ( + obj.__code__, + obj.__module__, + obj.__qualname__, + obj.__defaults__, + obj.__closure__, + ) + + return NotImplemented + + +class AOTCompileUnpickler(pickle.Unpickler): + def __init__(self, external_data: dict[str, object], file: io.BytesIO) -> object: + super().__init__(file) + self.external_data = external_data + + def persistent_load(self, key: str) -> object: + if key not in self.external_data: + raise RuntimeError( + f"Missing required external reference to data: {key}. " + "Please load AOT compiled function with " + "`external_data=`" + f"{self.external_data}" + ) + return self.external_data[key] + + +@dataclass +class AOTCompileSaveResult: + serialized_data: bytes + + +def atomic_write_binary(file_path: str, data: bytes): + dir_name = os.path.dirname(file_path) or "." + + with tempfile.NamedTemporaryFile( + dir=dir_name, delete=False, mode="wb" + ) as temp_file: + temp_path = temp_file.name + temp_file.write(data) + temp_file.flush() + os.fsync(temp_file.fileno()) + + os.replace(temp_path, file_path) + + +@dataclass +class AOTCompiledFunction: + _artifacts: CompileArtifacts + _guard_check_enabled: bool = True + _extra_globals: dict[str, object] | None = None + + def prepare_f_locals(self, *args: object, **kwargs: object) -> dict[str, object]: + f_locals: dict[str, object] = {} + env = self._artifacts.runtime_env + if env.closure: + assert env.bytecode.co_freevars and len(env.closure) == len( + env.bytecode.co_freevars + ) + f_locals = { + name: cell.cell_contents + for name, cell in zip(env.bytecode.co_freevars, env.closure) + } + f_locals.update(bind_locals(self._artifacts.signature, *args, **kwargs)) + return f_locals + + def guard_check(self, *args: Any, **kwargs: Any) -> bool: + f_locals = self.prepare_f_locals(*args, **kwargs) + assert self._artifacts.guard_manager is not None + return self._artifacts.guard_manager.check(f_locals) + + def __post_init__(self) -> None: + from .package import load_guard_manager, load_guards_state + + self._artifacts.check_compatibility() + + self.fn = self._artifacts.runtime_env.forward_callable( + self._artifacts.backend_id, + self._artifacts.compiled_fn, + extra_globals=self._extra_globals, + ) + + if self._artifacts.guard_manager is None: + guards_state = load_guards_state(self._artifacts.guards_state) + self._artifacts.guard_manager = load_guard_manager( + guards_state, + self._artifacts.original_code, + self.fn.__globals__, + ) + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + assert self._artifacts.guard_manager is not None + if self._guard_check_enabled and not self.guard_check(*args, **kwargs): + f_locals = self.prepare_f_locals(*args, **kwargs) + reason = str(self._artifacts.guard_manager.check_verbose(f_locals)) + raise RuntimeError(f"GuardManager check failed, reason: {reason}") + return self.fn(*args, **kwargs) + + def source_info(self) -> "SourceInfo": + return self._artifacts.source_info + + def save_compiled_function( + self, path: str, external_data: dict[str, Any] | None = None + ) -> AOTCompileSaveResult: + result = type(self).serialize(self, external_data) + atomic_write_binary(path, result.serialized_data) + return result + + @classmethod + def serialize( + cls, fn: "AOTCompiledFunction", external_data: dict[str, Any] | None = None + ) -> AOTCompileSaveResult: + from torch._dynamo.package import SerializedCode + + state = fn._artifacts.__dict__.copy() + state["guard_manager"] = None + state["runtime_env"] = dataclasses.replace( + state["runtime_env"], + bytecode=SerializedCode.from_code_object(state["runtime_env"].bytecode), + ) + compiled_fn = state["compiled_fn"] + state["compiled_fn"] = ( + type(compiled_fn).deserialize_compile_artifacts, + type(compiled_fn).serialize_compile_artifacts(compiled_fn), + ) + state["original_code"] = SerializedCode.from_code_object(state["original_code"]) + buf = io.BytesIO() + pickler = AOTCompilePickler(external_data or {}, buf) + pickler.dump(state) + if pickler.errors: + raise RuntimeError( + f"Failed to serialize the following objects: {list(pickler.errors.values())}\n" + "Please mark these as external data by using `external_data={'key': ...}`" + ) + return AOTCompileSaveResult(serialized_data=buf.getvalue()) + + @classmethod + def deserialize( + cls, + data: bytes, + f_globals: dict[str, object] | None = None, + external_closure_data: dict[str, Any] | None = None, + ) -> "AOTCompiledFunction": + from torch._dynamo.package import SerializedCode + + f = io.BytesIO(data) + f.seek(0) + unpickler = AOTCompileUnpickler(external_closure_data or {}, f) + state = unpickler.load() + f.close() + state["runtime_env"] = dataclasses.replace( + state["runtime_env"], + bytecode=SerializedCode.to_code_object(state["runtime_env"].bytecode), + ) + deserializer, compiled_fn_state = state["compiled_fn"] + with torch._inductor.config.patch(enable_autograd_for_aot=True): + state["compiled_fn"] = deserializer(compiled_fn_state) + state["original_code"] = SerializedCode.to_code_object(state["original_code"]) + + artifacts = CompileArtifacts(**state) + return cls(artifacts, _extra_globals=f_globals) + + def disable_guard_check(self) -> None: + self._guard_check_enabled = False + + +def aot_compile_fullgraph( + model: Any, + example_inputs: tuple[tuple[Any, ...], dict[str, Any]], + hooks: Hooks, + backend: Callable[[torch.fx.GraphModule, list[torch.Tensor]], SerializableCallable], + dynamic: bool | None = None, +) -> AOTCompiledFunction: + from torch._dynamo.guards import CheckFunctionManager + from torch._dynamo.package import SourceInfo + from torch._dynamo.utils import dynamo_timed, get_metrics_context + from torch._dynamo.variables.torch_function import ( + torch_function_mode_stack_state_mgr, + ) + from torch._guards import TracingContext + + args, kwargs = example_inputs + + dynamic_ctx = nullcontext() + if dynamic is not None: + from torch._dynamo.eval_frame import set_enable_dynamic + + dynamic_ctx = set_enable_dynamic(dynamic) + + with ( + get_metrics_context(), + dynamo_timed("fullgraph_capture"), + torch._functorch.config.patch(strict_autograd_cache=True), + dynamic_ctx, + torch_function_mode_stack_state_mgr, + ): + capture_output = convert_frame.fullgraph_capture(model, args, kwargs) + graph_capture_output = capture_output.graph_capture_output + assert graph_capture_output.output_graph is not None + + if not hooks.guard_filter_fn: + from torch._dynamo.types import GuardFilterEntry + + def new_guard_filter_fn( + guard_entries: Sequence[GuardFilterEntry], + ) -> Sequence[bool]: + return [ + ( + not ( + g.is_global + or g.guard_type + in CheckFunctionManager.UNSUPPORTED_SERIALIZATION_GUARD_TYPES + ) + ) + for g in guard_entries + ] + + hooks.guard_filter_fn = new_guard_filter_fn + + fn, _ = convert_frame.get_traced_fn(model) + + backend_input = capture_output.backend_input + assert backend_input is not None + backend_input.graph_module._backend_id = backend_input.backend_id # type: ignore[assignment] + device_type = _graph_device_type(backend_input.graph_module.graph) + assert ( + backend_input.fake_mode.shape_env + is graph_capture_output.output_graph.shape_env + ) + tracing_context = TracingContext(backend_input.fake_mode) + tracing_context.tensor_to_context = backend_input.tensor_to_context + with ( + torch._guards.tracing(tracing_context), + torch._functorch.config.patch( + { + "strict_autograd_cache": True, + "bypass_autograd_cache_key": True, + "bundled_autograd_cache": True, + "force_non_lazy_backward_lowering": True, + "force_autograd_cache": True, + } + ), + ): + compiled_fn = backend( + backend_input.graph_module, backend_input.example_inputs + ) + # If Inductor backend or AOTAutograd-based backend is used, + # wrap the compiled_fn for serialization. + # TODO: this should be replaced once we make the backend return the SerializableCallable directly. + if ( + isinstance(backend, torch._TorchCompileInductorWrapper) + or ( + hasattr(backend, "compiler_fn") + and isinstance( + backend.compiler_fn, torch._dynamo.backends.common.AotAutograd + ) + ) + or ( + hasattr(compiled_fn, "serialize") + and compiled_fn.serialize is not None + ) + ): + compiled_fn = BundledAOTAutogradSerializableCallable(compiled_fn) + + if not isinstance(compiled_fn, SerializableCallable): + if hasattr(backend, "compiler_fn"): + compiler_fn = backend.compiler_fn + else: + compiler_fn = backend + raise RuntimeError( + f"Compiled function type {type(compiled_fn)} (produced " + + f"from backend {compiler_fn}) does not implement SerializableCallable." + ) + + # Temporarily restore the mode stack so guard expressions that + # reference modes can evaluate, matching the compile_inner path. + build_guards_ctx = ExitStack() + if torch_function_mode_stack_state_mgr.stack: + build_guards_ctx.enter_context( + torch_function_mode_stack_state_mgr.temp_restore_stack() + ) + with build_guards_ctx: + check_fn = graph_capture_output.build_guards( + fn.__code__, hooks=hooks, save=True, strict_error=True + ) + + assert check_fn.guards_state is not None + + source_info = SourceInfo(inlined_sources=set()) + for traced_code in graph_capture_output.traced_code: + source_info.add_code(traced_code) + + artifacts = CompileArtifacts( + signature=convert_frame._get_signature(fn), + guard_manager=check_fn.guard_manager, + guards_state=check_fn.guards_state, + backend_id=backend_input.backend_id, + compiled_fn=compiled_fn, + original_code=fn.__code__, + runtime_env=graph_capture_output.get_runtime_env(), + source_info=source_info, + device_type=device_type, + backend_name=getattr(backend, "compiler_name", "unknown"), + ) + aot_compiled_fn = AOTCompiledFunction( + _artifacts=artifacts, _extra_globals=fn.__globals__ + ) + + return aot_compiled_fn + + +@dataclass +class ModelInput: + """ + WIP type: represents a single model input + Which consists of a tuple of arguments and a set of contexts in which to run the model. + + For each ModelInput, we'll compile one full graph of the model, and then use the guards generated + to dispatch between the compiled graphs. + + + """ + + args: tuple[Any] + kwargs: dict[str, Any] + contexts: list[AbstractContextManager[Any]] + + +@dataclass +class AOTCompiledModel: + # Represents a single forward function of a model along with dispatch + # compiled_results is serializable. We require the model to deserialize again. + model: torch.nn.Module + compiled_results: list[AOTCompiledFunction] + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + for result in self.compiled_results: + if result.guard_check(self.model, *args, **kwargs): + return result(self.model, *args, **kwargs) + # All guards failed, just run one of them and throw the guard check error. + return self.compiled_results[0](self.model, *args, **kwargs) + + def serialize(self) -> bytes: + data: list[bytes] = [] + for result in self.compiled_results: + data.append(AOTCompiledFunction.serialize(result).serialized_data) + return pickle.dumps(data) + + @classmethod + def deserialize(cls, model: torch.nn.Module, data: bytes) -> "AOTCompiledModel": + from torch._dynamo.utils import get_metrics_context + from torch._guards import compile_context, CompileContext + + results: list[bytes] = pickle.loads(data) + compiled_results = [] + for result in results: + with ( + compile_context(CompileContext(convert_frame.get_compile_id({}))), + get_metrics_context(), + ): + compiled_results.append(AOTCompiledFunction.deserialize(result)) + return cls(model, compiled_results) + + +def aot_compile_module( + model: torch.nn.Module, + inputs: list[ModelInput], + hooks: Hooks, + backend: Callable[[torch.fx.GraphModule, list[torch.Tensor]], SerializableCallable], +) -> AOTCompiledModel: + """ + Compiles a single nn.Module with any number of inputs, and returns a compiled forward function. + """ + + def compile_single_graph(model_input: ModelInput) -> AOTCompiledFunction: + example_inputs = (model_input.args, model_input.kwargs) + orig_forward = model.forward + with ExitStack() as stack: + for ctx in model_input.contexts: + stack.enter_context(ctx) + return aot_compile_fullgraph( + orig_forward, + example_inputs, + hooks=hooks, + backend=backend, + ) + + # pyrefly: ignore [implicit-any] + compiled_results = [] + for model_input in inputs: + log.info("Compiling input %s..", model_input) + compiled_results.append(compile_single_graph(model_input)) + + assert len(compiled_results) > 0 + + return AOTCompiledModel(model, compiled_results) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/aot_compile_types.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/aot_compile_types.py new file mode 100644 index 0000000000000000000000000000000000000000..3108323f031d1b471b4a226dbf966500382e5e4b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/aot_compile_types.py @@ -0,0 +1,211 @@ +import abc +import importlib +import pickle +from typing import Any + +import torch + + +def _serialize_triton_kernel(kernel: Any) -> tuple[str, str]: + """ + Serialize a triton kernel by extracting its module path and function name. + Returns (module_path, function_name) tuple. + + Triton JITFunction objects contain unpicklable _thread.RLock objects, so we + serialize the import path instead and reimport on load. + + Raises: + RuntimeError: If the kernel cannot be serialized (missing attributes). + """ + fn = getattr(kernel, "fn", None) + module_path = fn and getattr(fn, "__module__", None) + func_name = fn and getattr(fn, "__name__", None) + if fn is None or module_path is None or func_name is None: + raise RuntimeError( + f"Kernel fn missing __module__ or __name__: " + f"module={module_path}, name={func_name}. " + f"Cannot serialize for precompilation." + ) + return (module_path, func_name) + + +def _deserialize_triton_kernel(kernel_info: tuple[str, str]) -> Any: + """ + Deserialize a triton kernel by reimporting from its module. + kernel_info is (module_path, function_name) tuple. + """ + module_path, func_name = kernel_info + module = importlib.import_module(module_path) + kernel = getattr(module, func_name) + return kernel + + +# Note: [Triton Kernel Side Table Serialization] +# +# When dynamo captures user-defined triton kernels, it creates FX graph nodes +# (triton_kernel_wrapper_mutation/functional) with a `kernel_idx` parameter that +# references the global `kernel_side_table` in triton_kernel_wrap.py. This side +# table maps integer indices to actual triton kernel objects. +# +# For kernels that go through inductor's codegen path, this is fine - inductor +# looks up the kernel from the side table at codegen time and embeds the kernel +# source code directly into the generated wrapper. The compiled code doesn't +# need the side table at runtime. +# +# However, not all triton kernels go through inductor codegen. When using +# regional_inductor, only annotated regions are compiled by inductor. Triton +# kernels outside these regions are executed via the FX interpreter, which +# calls the higher-order op directly and needs the kernel to be in the side +# table at runtime. +# +# When serializing/deserializing bundled AOT artifacts across process boundaries, +# the kernel_side_table is empty in the new process, causing: +# AssertionError: Kernel index X not found in id_to_kernel +# +# To fix this, we capture the kernel_side_table state during serialization and +# restore it during deserialization. Kernels are serialized by their import path +# (module_path, function_name) since triton JITFunction objects contain +# unpicklable RLock objects. + + +class SerializableCallable(abc.ABC): + @classmethod + @abc.abstractmethod + def serialize_compile_artifacts(cls, fn: Any) -> bytes: + pass + + @classmethod + @abc.abstractmethod + def deserialize_compile_artifacts(cls, data: bytes) -> Any: + pass + + @abc.abstractmethod + def __call__(self, *args: Any, **kwargs: Any) -> Any: + pass + + +class GraphModuleSerializableCallable(SerializableCallable): + def __init__(self, graph_module: torch.fx.GraphModule) -> None: + assert isinstance(graph_module, torch.fx.GraphModule) + self.graph_module = graph_module + + @classmethod + def serialize_compile_artifacts( + cls, fn: "GraphModuleSerializableCallable" + ) -> bytes: + from torch.fx._graph_pickler import GraphPickler, Options + + state = fn.__dict__.copy() + + graph_module = state["graph_module"] + for node in graph_module.graph.nodes: + node.meta.pop("nn_module_stack", None) + node.meta.pop("source_fn_stack", None) + node.meta.pop("example_value", None) + + state["graph_module"] = GraphPickler.dumps( + graph_module, Options(ops_filter=None) + ) + return pickle.dumps(state) + + @classmethod + def deserialize_compile_artifacts(cls, data: bytes) -> Any: + from torch._subclasses import FakeTensorMode + from torch.fx._graph_pickler import GraphPickler + from torch.fx.experimental.symbolic_shapes import ShapeEnv + + state = pickle.loads(data) + + fake_mode = FakeTensorMode(shape_env=ShapeEnv()) + state["graph_module"] = GraphPickler.loads(state["graph_module"], fake_mode) + assert isinstance(state["graph_module"], torch.fx.GraphModule) + state["graph_module"].recompile() + + return cls(**state) + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + return self.graph_module.forward(*args, **kwargs) + + +class BundledAOTAutogradSerializableCallable(SerializableCallable): + """ + Represents a serializable callable generated by compile_fx. + This class wraps around the compiled function generated by AOTAutograd. + + TODO: Instead of using PrecompileContext to grab it from AOTAutograd, + this object should be what's *returned* by aot_module_simplified. + We'll do that refactor in a later PR. + """ + + def __init__(self, compiled_fn: Any) -> None: + """ + Takes in a BundledAOTAutogradCacheArtifact, which is the serialized form + of a compiled function generated by AOTAutograd. + """ + assert hasattr(compiled_fn, "serialize") + self.compiled_fn = compiled_fn + + def __getattr__(self, attr: Any) -> Any: + return getattr(self.compiled_fn, attr) + + @classmethod + def serialize_compile_artifacts( + cls, fn: "BundledAOTAutogradSerializableCallable" + ) -> bytes: + from torch._higher_order_ops.triton_kernel_wrap import kernel_side_table + + # See Note: [Triton Kernel Side Table Serialization] + # Capture triton kernel side table state BEFORE serialization. + triton_kernels: dict[int, tuple[str, str]] = { + idx: _serialize_triton_kernel(kernel) + for idx, kernel in kernel_side_table.id_to_kernel.items() + } + triton_constant_args: dict[int, dict[str, Any]] = dict( + kernel_side_table.constant_args + ) + + with torch._functorch.config.patch("bundled_autograd_cache", True): + serialized_entry = fn.compiled_fn.serialize() + # Bundle the triton kernel side table with the serialized entry + bundle = (serialized_entry, triton_kernels, triton_constant_args) + result = pickle.dumps(bundle) + return result + + @classmethod + def deserialize_compile_artifacts(cls, data: bytes) -> Any: + from torch._functorch._aot_autograd.aot_autograd_result import ( + deserialize_bundled_cache_entry, + ) + from torch._higher_order_ops.triton_kernel_wrap import kernel_side_table + + bundle = pickle.loads(data) + + # Handle both old format (just entry) and new format (entry, kernels, const_args) + if isinstance(bundle, tuple) and len(bundle) == 3: + entry, triton_kernels, triton_constant_args = bundle + else: + # Backwards compatibility with old serialized artifacts + entry = bundle + # pyrefly: ignore [implicit-any] + triton_kernels = {} + # pyrefly: ignore [implicit-any] + triton_constant_args = {} + + # See Note: [Triton Kernel Side Table Serialization] + # Restore triton kernel side table BEFORE deserializing the compiled function. + # The compiled function may reference kernels by index if any triton kernels + # don't go through inductor codegen (e.g., triton kernels outside of + # regional_inductor compiled regions). + for idx, kernel_info in triton_kernels.items(): + kernel = _deserialize_triton_kernel(kernel_info) + kernel_side_table.id_to_kernel[idx] = kernel + kernel_side_table.kernel_to_id[kernel] = idx + + for idx, args in triton_constant_args.items(): + kernel_side_table.constant_args[idx] = args + + compiled_fn = deserialize_bundled_cache_entry(entry) + return cls(compiled_fn) + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + return self.compiled_fn(*args, **kwargs) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/common.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/common.py new file mode 100644 index 0000000000000000000000000000000000000000..0b60e0d74a623c9bf13900986011c58e1234e7a3 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/common.py @@ -0,0 +1,184 @@ +""" +This module provides common utilities and base classes for TorchDynamo backends. + +Key components: +- AotAutograd: Base class for implementing AOT (Ahead-of-Time) autograd backends +- Backend utilities for handling: + - Fake tensor conversion + - Device/dtype detection from inputs + - Memory efficient fusion + - Graph flattening + - Common compiler configurations + +The utilities here are used by various backend implementations to handle +common operations and provide consistent behavior across different backends. +AOT autograd functionality is particularly important as it enables ahead-of-time +optimization of both forward and backward passes. +""" + +import contextlib +import functools +import logging +from collections.abc import Callable, Iterable, Sequence +from typing import Any +from typing_extensions import ParamSpec, TypeVar +from unittest.mock import patch + +import torch +from torch._dynamo import disable +from torch._dynamo.exc import TensorifyScalarRestartAnalysis +from torch._dynamo.utils import counters, defake, flatten_graph_inputs +from torch._functorch.aot_autograd import ( + aot_module_simplified, + SerializableAOTDispatchCompiler, +) +from torch.utils._python_dispatch import _disable_current_modes + + +log = logging.getLogger(__name__) + +P = ParamSpec("P") +R = TypeVar("R") + + +class AotAutograd: + def __init__(self, **kwargs: Any) -> None: + self.__name__ = "compiler_fn" + self.kwargs = kwargs + + def __call__( + self, gm: torch.fx.GraphModule, example_inputs: Sequence[Any], **kwargs: Any + ) -> Callable[..., Any]: + if kwargs: + log.warning("aot_autograd-based backend ignoring extra kwargs %s", kwargs) + + if any(isinstance(x, (list, tuple, dict)) for x in example_inputs): + return flatten_graph_inputs( + gm, + example_inputs, + self, + ) + + # Hack to get around circular import problems with aot_eager_decomp_partition + if callable(self.kwargs.get("decompositions")): + self.kwargs["decompositions"] = self.kwargs["decompositions"]() + + # NB: dont delete counter increment + counters["aot_autograd"]["total"] += 1 + use_fallback = False + + if use_fallback: + log.debug("Unable to use AOT Autograd because graph has mutation") + counters["aot_autograd"]["not_ok"] += 1 + return gm + + def wrap_bw_compiler(bw_compiler_fn: Callable[P, R]) -> Callable[..., R]: + def _wrapped_bw_compiler(*args: P.args, **kwargs: P.kwargs) -> R: + # Note [Wrapping bw_compiler in disable] + # The two disables here: + # - stop TorchDynamo from trying to compile the bw_compiler function itself + # - stop TorchDynamo from trying to compile our the generated backwards pass bw_compiler produces + + return disable( + disable( + bw_compiler_fn, reason="do not trace backward compiler function" + )(*args, **kwargs), # type: ignore[misc] + reason="do not trace generated backwards pass", + ) + + _wrapped_bw_compiler._is_wrapped_bw_compiler = ( # pyrefly: ignore [missing-attribute] + True + ) + return _wrapped_bw_compiler + + bw_compiler = self.kwargs.get("bw_compiler") or self.kwargs["fw_compiler"] + + if isinstance(bw_compiler, SerializableAOTDispatchCompiler): + bw_compiler.compiler_fn = wrap_bw_compiler(bw_compiler.compiler_fn) + elif getattr(bw_compiler, "_is_wrapped_bw_compiler", False): + bw_compiler.compiler_fn = bw_compiler + else: + bw_compiler = wrap_bw_compiler(bw_compiler) + + self.kwargs["bw_compiler"] = bw_compiler + self.kwargs["inference_compiler"] = ( + self.kwargs.get("inference_compiler") or self.kwargs["fw_compiler"] + ) + + from functorch.compile import nop + from torch._inductor.debug import enable_aot_logging + + # debug asserts slow down compile time noticeably, + # So only default them on when the aot_eager backend is used. + if self.kwargs.get("fw_compiler", None) is nop: + patch_config: contextlib.AbstractContextManager[Any] = patch( + "functorch.compile.config.debug_assert", True + ) + else: + patch_config = contextlib.nullcontext() + + try: + # NB: NOT cloned! + with enable_aot_logging(), patch_config: + cg = aot_module_simplified(gm, example_inputs, **self.kwargs) + counters["aot_autograd"]["ok"] += 1 + return disable(cg, reason="do not trace AOT-compiled graph") + except TensorifyScalarRestartAnalysis: + raise + except Exception: + counters["aot_autograd"]["not_ok"] += 1 + raise + + +def aot_autograd(**kwargs: Any) -> AotAutograd: + return AotAutograd(**kwargs) + + +def mem_efficient_fusion_kwargs(use_decomps: bool) -> dict[str, Any]: + from functorch.compile import ( + default_decompositions, + min_cut_rematerialization_partition, + ts_compile, + ) + + kwargs = { + # these are taken from memory_efficient_fusion() + "fw_compiler": ts_compile, + "bw_compiler": ts_compile, + "partition_fn": min_cut_rematerialization_partition, + } + + if use_decomps: + # pyrefly: ignore [bad-typed-dict-key] + kwargs["decompositions"] = default_decompositions + + return kwargs + + +def fake_tensor_unsupported(fn: Callable[[Any, list[Any], Any], R]) -> Any: + """ + Decorator for backends that need real inputs. We swap out fake + tensors for zero tensors. + """ + + @functools.wraps(fn) + def wrapper(model: Any, inputs: Any, **kwargs: Any) -> Any: + with _disable_current_modes(): + inputs = list(map(defake, inputs)) + return fn(model, inputs, **kwargs) # type: ignore[call-arg] + + return wrapper + + +def device_from_inputs(example_inputs: Iterable[Any]) -> torch.device: + for x in example_inputs: + if hasattr(x, "device"): + return x.device + return torch.device("cpu") # Default fallback + + +def dtype_from_inputs(example_inputs: Iterable[Any]) -> torch.dtype: + for x in example_inputs: + if hasattr(x, "dtype"): + return x.dtype + return torch.float32 # Default fallback diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/cudagraphs.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/cudagraphs.py new file mode 100644 index 0000000000000000000000000000000000000000..27f2e19b98f1c44cd14f5a55d91c4e3e3aabc1da --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/cudagraphs.py @@ -0,0 +1,299 @@ +""" +This module implements CUDA graphs support for TorchDynamo backends. + +CUDA graphs allow for capturing and replaying GPU operations, which can significantly +reduce CPU overhead in GPU-accelerated PyTorch models. This module provides: + +- CUDA graph creation and management for both forward and backward passes +- Input mutation detection and handling +- Device compatibility checking +- Stack trace management for debugging +- Integration with TorchInductor's cudagraph trees + +The backend supports two main modes: +1. cudagraphs: Full CUDA graph support with both forward and backward pass optimization +2. cudagraphs_inner: Lower-level CUDA graph implementation used for benchmarking + +Key components: +- CudagraphsBackend: Main backend class for CUDA graph integration +- Mutation detection utilities to ensure graph safety +- Device mapping and compatibility checks +- Stack trace collection for debugging +""" + +import functools +from collections import defaultdict +from collections.abc import Callable, Sequence +from typing import Any + +import torch +import torch.fx +from torch._dynamo import config +from torch._dynamo.backends.common import aot_autograd +from torch._dynamo.backends.debugging import boxed_nop +from torch._inductor.cudagraph_utils import ( + BoxedDeviceIndex, + check_multiple_devices_or_any_cpu_nodes, + format_default_skip_message, + get_mutation_stack_trace, + get_placeholder_info, + log_cudagraph_skip_and_bump_counter, +) +from torch._inductor.utils import ( + BoxedBool, + count_tangents, + get_first_incompatible_cudagraph_node, + num_fw_fixed_arguments, + output_node, +) +from torch.multiprocessing.reductions import StorageWeakRef + +from .registry import register_backend + + +def find_input_mutations(g: torch.fx.Graph) -> set[int]: + def meta_fk(meta: dict[str, Any]) -> Any: + return meta["val"] if "val" in meta else meta["fake_result"] + + inputs = defaultdict(set) + input_idx = 0 + mutated_inputs = set() + for n in g.nodes: + if n.op == "placeholder": + if isinstance(meta_fk(n.meta), torch.Tensor): + inputs[StorageWeakRef(meta_fk(n.meta)._typed_storage())].add(input_idx) + input_idx += 1 + elif n.op == "call_function": + if not hasattr(n.target, "_schema"): + continue + + schema = n.target._schema + for i, arg in enumerate(schema.arguments): + if i < len(n.args): + argument = n.args[i] + else: + if arg.name not in n.kwargs: + continue + argument = n.kwargs[arg.name] + mut_arg = False + if arg.alias_info: + if arg.alias_info.is_write: + mut_arg = True + if mut_arg: + # TODO: not correct for args that contain tensors in a struct + # like list + mutated_inputs |= inputs[ + StorageWeakRef(meta_fk(argument.meta)._typed_storage()) + ] + + # TODO: error on unrecognized nodes + return mutated_inputs + + +def get_device_node_mapping( + gm: torch.fx.GraphModule, +) -> dict[torch.device, torch.fx.Node]: + device_node_mapping: dict[torch.device, torch.fx.Node] = {} + for n in gm.graph.nodes: + t = n.meta.get("val", None) + if isinstance(t, torch.Tensor) and t.device not in device_node_mapping: + device_node_mapping[t.device] = n + return device_node_mapping + + +def check_for_mutation_ignore_cuda_graph_managed_tensor( + aot_model: torch.fx.GraphModule, num_fixed: int +) -> str | None: + mutation_indices = find_input_mutations(aot_model.graph) - set(range(num_fixed)) + if not mutation_indices: + return None + + placeholders = get_placeholder_info(aot_model.graph) + return get_mutation_stack_trace(placeholders, mutation_indices) + + +def check_for_skip(aot_model: torch.fx.GraphModule, num_fixed: int) -> str | None: + if not config.cudagraph_backend_support_input_mutation: + if mut_skip := check_for_mutation_ignore_cuda_graph_managed_tensor( + aot_model, num_fixed + ): + return mut_skip + + if skip := check_multiple_devices_or_any_cpu_nodes( + get_device_node_mapping(aot_model) + ): + return skip + + if node := get_first_incompatible_cudagraph_node(aot_model): + return format_default_skip_message(f"incompatible op ({node.name})") + + return None + + +def get_device_index(gm: torch.fx.GraphModule) -> int: + device = next(iter(get_device_node_mapping(gm))) + assert device.type == "cuda" + return device.index + + +def get_stack_traces(gm: torch.fx.GraphModule) -> list[str | None]: + output = output_node(gm) + assert len(output.args) == 1 + args = output.args[0] + if not hasattr(args, "__iter__"): + return [] + return [ + (arg.stack_trace if isinstance(arg, torch.fx.node.Node) else None) + for arg in args # type: ignore[union-attr] + ] + + +def cudagraphs(dynamo_model: torch.fx.GraphModule, dynamo_inputs: Sequence[Any]) -> Any: + from torch._inductor.cudagraph_trees import cudagraphify_impl + + do_cudagraphs = BoxedBool(True) + boxed_device_index = BoxedDeviceIndex(None) + + def forward_cudagraphs( + aot_model: torch.fx.GraphModule, + aot_inputs: list[Any], + is_inference: bool = False, + ) -> Any: + interp = boxed_nop(aot_model, aot_inputs) + fixed = num_fw_fixed_arguments(len(dynamo_inputs), len(aot_inputs)) + if skip_msg := check_for_skip(aot_model, fixed): + BoxedBool.disable(do_cudagraphs) + log_cudagraph_skip_and_bump_counter( + f"skipping cudagraphs due to {skip_msg}" + ) + return interp + + boxed_device_index.set(get_device_index(aot_model)) + out = cudagraphify_impl( + interp, + aot_inputs, + range(fixed), + device_index=boxed_device_index.value, + is_backward=False, + is_inference=is_inference, + stack_traces=get_stack_traces(aot_model), + placeholders=get_placeholder_info(aot_model.graph), + mutated_input_idxs=find_input_mutations(aot_model.graph), + ) + out._boxed_call = True # type: ignore[attr-defined] + return out + + def backward_cudagraphs( + aot_model: torch.fx.GraphModule, aot_inputs: list[Any] + ) -> Any: + interp = boxed_nop(aot_model, aot_inputs) + if not do_cudagraphs: + return aot_model + + fixed = count_tangents(aot_model) + if skip_msg := check_for_skip(aot_model, fixed): + log_cudagraph_skip_and_bump_counter( + f"skipping cudagraphs due to {skip_msg}" + ) + + # See [Backward Generation Handling] + device_idx = boxed_device_index.value + if device_idx is None: + device_idx = 0 # Default to device 0 if not set + manager = torch._inductor.cudagraph_trees.get_manager( + device_idx, create_if_none_exists=False + ) + assert manager is not None + + def fn(inputs: list[Any]) -> Any: + # pyrefly: ignore [missing-attribute] + manager.set_to_running_backward() + return aot_model(inputs) + + fn._boxed_call = True # type: ignore[attr-defined] + return fn + + out = cudagraphify_impl( + interp, + aot_inputs, + range(fixed), + device_index=get_device_index(aot_model), + is_backward=True, + is_inference=False, + stack_traces=get_stack_traces(aot_model), + placeholders=get_placeholder_info(aot_model.graph), + mutated_input_idxs=find_input_mutations(aot_model.graph), + ) + out._boxed_call = True # type: ignore[attr-defined] + return out + + aot_cudagraphs = aot_autograd( + fw_compiler=forward_cudagraphs, + bw_compiler=backward_cudagraphs, + inference_compiler=functools.partial(forward_cudagraphs, is_inference=True), + keep_inference_input_mutations=torch._dynamo.config.cudagraph_backend_keep_input_mutation, + ) + return aot_cudagraphs(dynamo_model, dynamo_inputs) + + +class CudagraphsBackend: + compiler_name = "cudagraphs" + + @staticmethod + def reset() -> None: + from torch._inductor.cudagraph_trees import reset_cudagraph_trees + + reset_cudagraph_trees() + + @staticmethod + def __call__(model: torch.fx.GraphModule, inputs: Sequence[Any]) -> Any: + return cudagraphs(model, inputs) + + +# aot_cudagraphs only applies CUDA graphs to the graph. It is also helpful +# for debugging and can serve as a perf baseline. +register_backend(name="cudagraphs", compiler_fn=CudagraphsBackend()) + + +def cudagraphs_inner( + model: Callable[..., Any], + inputs: Sequence[Any], + copy_outputs: bool = True, + copy_inputs: bool = True, +) -> Callable[..., Sequence[Any]]: + """This isn't registered as a backend, but is used in some benchmarks""" + assert isinstance(inputs, (list, tuple)) + if copy_inputs: + static_inputs = [torch.zeros_like(x) for x in inputs] + else: + static_inputs = list(inputs) + + # warmup + torch.cuda.synchronize() + stream = torch.cuda.Stream() + stream.wait_stream(torch.cuda.current_stream()) + with torch.cuda.stream(stream): + model(*inputs) + stream.synchronize() + torch.cuda.current_stream().wait_stream(stream) + torch.cuda.synchronize() + + # record + graph = torch.cuda.CUDAGraph() + with torch.cuda.graph(graph, stream=stream): + static_outputs = model(*static_inputs) + if not isinstance(static_outputs, (list, tuple)): + static_outputs = (static_outputs,) + + def run(*new_inputs: Any) -> Sequence[Any]: + assert len(static_inputs) == len(new_inputs) + if copy_inputs: + for dst, src in zip(static_inputs, new_inputs): + dst.copy_(src) + graph.replay() + if copy_outputs: + return [x.clone() for x in static_outputs] + else: + return static_outputs + + return run diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/debugging.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/debugging.py new file mode 100644 index 0000000000000000000000000000000000000000..eacb304abf7b1d3231843f6a8a574ffd9a57a11d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/debugging.py @@ -0,0 +1,730 @@ +""" +This module provides debugging backends for TorchDynamo to help diagnose and troubleshoot +compilation and execution issues. It includes: + +Key Debugging Backends: +- eager: Simple pass-through backend that runs models in eager mode +- eager_noexcept: Similar to eager but with additional exception handling +- eager_debug: Adds schema validation checks for custom operators +- aot_eager: Uses AOT Autograd with nop compiler for debugging +- aot_eager_decomp_partition: Uses TorchInductor decompositions for debugging +- torchscript: Compiles using TorchScript for debugging JIT-related issues + +Testing and Development Tools: +- Backends for inducing specific errors (compile/runtime/accuracy) +- ExplainOutput class for detailed graph compilation analysis +- Utilities for cross-referencing and mode management +- Tools for graph detail inspection and break reason analysis + +These backends are primarily used for: +1. Debugging graph breaks and compilation failures +2. Testing error handling and recovery mechanisms +3. Analyzing performance bottlenecks +4. Validating operator schemas and decompositions +""" + +import dataclasses +import functools +import logging +from collections.abc import Callable, Iterable +from importlib import import_module +from typing import Any, TYPE_CHECKING + +import torch +from functorch.compile import min_cut_rematerialization_partition +from torch import _guards +from torch._dynamo.output_graph import GraphCompileReason +from torch._functorch import config as functorch_config +from torch._functorch.compilers import ts_compile +from torch._inductor.output_code import OutputCode + +from .common import aot_autograd +from .registry import CompiledFn, CompilerFn, register_debug_backend as register_backend + + +if TYPE_CHECKING: + from torch.fx.node import Target + + +log = logging.getLogger(__name__) + + +@register_backend +def eager( + gm: torch.fx.GraphModule, fake_tensor_inputs: list[torch.Tensor], **kwargs: Any +) -> Callable[..., Any]: + if kwargs: + log.warning("eager backend ignoring extra kwargs %s", kwargs) + + if torch._functorch.config.force_autograd_cache: + from torch._dynamo.aot_compile_types import GraphModuleSerializableCallable + + return GraphModuleSerializableCallable(gm) + return gm.forward + + +def make_eager_backend_with_torch_function_mode( + mode: torch.overrides.TorchFunctionMode, +) -> Callable[..., Any]: + return make_eager_backend_with_torch_function_modes([mode]) + + +def make_eager_backend_with_torch_function_modes( + modes: Iterable[torch.overrides.TorchFunctionMode], +) -> Callable[..., Any]: + """Used to trace HOPs (cond and while) for eager execution, the metadata + TF mode mutates vars outside of the scope of the HOP, and we can't have graph breaks + in the HOP, so we need to externally run this mode and not trace it.""" + from contextlib import ExitStack + + def fn( + gm: torch.fx.GraphModule, fake_tensor_inputs: list[torch.Tensor], **kwargs: Any + ) -> Callable[..., Any]: + def wrapper(*args: Any, **kwargs: Any) -> Any: + with ExitStack() as stack: + for mode in modes: + stack.enter_context(mode) + return gm.forward(*args, **kwargs) + + return wrapper + + return fn + + +@register_backend +def eager_noexcept( + gm: torch.fx.GraphModule, fake_tensor_inputs: list[torch.Tensor], **kwargs: Any +) -> Callable[..., Any]: + if kwargs: + log.warning("eager_noexcept backend ignoring extra kwargs %s", kwargs) + + # This backend is intended to check that dynamo-generated GraphModules + # do not cause errors. + def inner(*args: Any) -> Any: + try: + return gm(*args) + except Exception as e: + raise torch._dynamo.exc.TorchDynamoException( + "Unexpected exception when running generated GraphModule" + ) from e + + return inner + + +@register_backend +def pre_dispatch_eager( + gm: torch.fx.GraphModule, fake_tensor_inputs: list[torch.Tensor], **kwargs: Any +) -> torch.fx.GraphModule: + if kwargs: + log.warning("pre_dispatch_eager backend ignoring extra kwargs %s", kwargs) + + from torch.fx.experimental.proxy_tensor import make_fx + + def runnable_gm(*args: Any) -> Any: + return torch.fx.Interpreter(gm).run(*args) + + pre_dispatch_gm = make_fx(runnable_gm, pre_dispatch=True)(*fake_tensor_inputs) + pre_dispatch_gm.print_readable() + + return pre_dispatch_gm + + +@register_backend +def eager_debug( + gm: torch.fx.GraphModule, fake_tensor_inputs: list[torch.Tensor], **kwargs: Any +) -> Callable[..., Any]: + if kwargs: + log.warning("eager_debug backend ignoring extra kwargs %s", kwargs) + + from torch._subclasses.schema_check_mode import SchemaCheckMode + + # We could add more debugging bits here. + # Right now, this backend can be used to check for and error on + # custom dispatcher ops that have incorrect schemas. + def inner(*args: Any) -> Any: + with SchemaCheckMode(): + return torch.fx.Interpreter(gm).run(*args) + + return inner + + +@register_backend(name="ts") # type: ignore[misc] +def torchscript( + gm: torch.fx.GraphModule, fake_tensor_inputs: list[torch.Tensor] +) -> torch.jit.ScriptModule: + return torch.jit.script(gm) + + +def invoke_subgraph_inner_compiler( + subgraph: torch.fx.GraphModule, example_inputs: list[torch.Tensor] +) -> Callable[..., Any]: + """Inner compiler that wraps forward/backward graphs in invoke_subgraph HOP. + + This is used as the fw_compiler/bw_compiler for aot_autograd. When the resulting + function is traced by make_fx, it emits an invoke_subgraph HOP instead of inlining. + """ + from torch._dynamo import disable + from torch._higher_order_ops.invoke_subgraph import invoke_subgraph_infer + + @disable + # pyrefly: ignore [deprecated] + @torch._dynamo.allow_in_graph + def invoke_subgraph_wrapper_unboxed(*operands: Any) -> Any: + return invoke_subgraph_infer(subgraph, *operands) + + # NB: The direct to unboxed path is broken, you MUST DO THIS + + def invoke_subgraph_wrapper(args: list[Any]) -> Any: + return invoke_subgraph_wrapper_unboxed(*args) + + invoke_subgraph_wrapper._boxed_call = True # type: ignore[attr-defined] + + return invoke_subgraph_wrapper + + +# I cannot say how many times I had to revert to this vibe coded version of +# the code, which worked, and the cleaner versions of the code did not work, +# so I'm leaving this here until we fix the rest of the bugs. +''' +# Counter for unique subgraph names in invoke_subgraph backend +_invoke_subgraph_counter = 0 + + +def invoke_subgraph_inner_compiler_good( + fx_g: torch.fx.GraphModule, example_inputs: list[torch.Tensor] +) -> Callable[..., Any]: + """Inner compiler that wraps forward/backward graphs in invoke_subgraph HOP. + + This is used as the fw_compiler/bw_compiler for aot_autograd. When the resulting + function is traced by make_fx, it emits an invoke_subgraph HOP instead of inlining. + """ + from torch._higher_order_ops.invoke_subgraph import ( + invoke_subgraph as invoke_subgraph_hop, + ) + from torch.fx.experimental.proxy_tensor import get_proxy_mode + + global _invoke_subgraph_counter + _invoke_subgraph_counter += 1 + name = f"invoke_subgraph_{_invoke_subgraph_counter}" + + from torch._dynamo import disable + + # Check if fx_g uses boxed calling convention + fx_g_is_boxed = getattr(fx_g, "_boxed_call", False) + + @disable + @torch._dynamo.allow_in_graph + def invoke_subgraph_wrapper_unboxed(*args: Any) -> Any: + proxy_mode = get_proxy_mode() + if proxy_mode is not None: + # When being traced by make_fx, emit invoke_subgraph HOP + return invoke_subgraph_hop(fx_g, name, *args) # type: ignore[arg-type] + else: + # Normal execution path - call fx_g with proper calling convention + if fx_g_is_boxed: + return fx_g(list(args)) + else: + return fx_g(*args) + + # Wrap to handle boxed arguments (list of args) as expected by AOTAutograd + def invoke_subgraph_wrapper(args: list[Any]) -> Any: + return invoke_subgraph_wrapper_unboxed(*args) + + invoke_subgraph_wrapper._boxed_call = True # type: ignore[attr-defined] + return invoke_subgraph_wrapper +''' + + +@register_backend +def invoke_subgraph( + gm: torch.fx.GraphModule, fake_tensor_inputs: list[torch.Tensor], **kwargs: Any +) -> Callable[..., Any]: + """Backend that wraps forward/backward graphs in invoke_subgraph HOP when traced by make_fx. + + This backend uses AOTAutograd to partition into forward/backward graphs, then wraps + each in an invoke_subgraph HOP. This is useful for recursive Dynamo tracing scenarios + where you want the compiled subgraph to appear as invoke_subgraph HOPs in the outer + trace rather than being inlined. + + Requires: + - torch._dynamo.config.force_compile_during_fx_trace = True + (this implicitly overrides error_on_nested_fx_trace) + """ + if kwargs: + log.warning("invoke_subgraph backend ignoring extra kwargs %s", kwargs) + + # Use AOTAutograd to partition into forward/backward + return aot_autograd( + fw_compiler=invoke_subgraph_inner_compiler, + bw_compiler=invoke_subgraph_inner_compiler, + partition_fn=min_cut_rematerialization_partition, + keep_inference_input_mutations=True, + )(gm, fake_tensor_inputs) + + +@dataclasses.dataclass +class AOTEagerOutputCode(OutputCode): + """ + An OutputCode that wraps a GraphModule for eager-mode execution. + + This allows non-inductor backends (like aot_eager) to participate in + the bundled autograd cache and aot_compile serialization flow. + """ + + gm: torch.fx.GraphModule | None = None + _serialized_gm: bytes | None = dataclasses.field(default=None, init=False) + + def __call__(self, inputs: Any) -> Any: + assert self.gm is not None + return self.gm.forward(inputs) + + def prepare_for_serialization(self) -> None: + from torch.fx._graph_pickler import GraphPickler, Options + + assert self.gm is not None + for node in self.gm.graph.nodes: + node.meta.pop("nn_module_stack", None) + node.meta.pop("source_fn_stack", None) + node.meta.pop("example_value", None) + + self._serialized_gm = GraphPickler.dumps(self.gm, Options(ops_filter=None)) + self.gm = None + + def post_compile(self, *args: Any, **kwargs: Any) -> None: + if self.gm is None and self._serialized_gm is not None: + from torch._subclasses import FakeTensorMode + from torch.fx._graph_pickler import GraphPickler + from torch.fx.experimental.symbolic_shapes import ShapeEnv + from torch.fx.graph import _BoxedCodeGen + + fake_mode = FakeTensorMode(shape_env=ShapeEnv()) + gm = GraphPickler.loads(self._serialized_gm, fake_mode) + assert isinstance(gm, torch.fx.GraphModule) + self.gm = gm + assert isinstance(self.gm, torch.fx.GraphModule) + self.gm.graph.set_codegen(_BoxedCodeGen()) + self.gm.recompile() + self._serialized_gm = None + + def set_triton_bundle(self, triton_bundle: Any) -> None: + pass + + +# used boxed call to discard inputs when they are no longer needed +def boxed_nop( + fx_g: torch.fx.GraphModule, example_inputs: list[torch.Tensor] +) -> Callable[..., Any]: + from torch.fx.graph import _BoxedCodeGen + + # Set the graph to use boxed codegen + fx_g.graph.set_codegen(_BoxedCodeGen()) + fx_g.recompile() + + if functorch_config.force_autograd_cache or functorch_config.bundled_autograd_cache: + result = AOTEagerOutputCode(gm=fx_g) + result._boxed_call = True # type: ignore[attr-defined] + return result + + # Wrap the forward method in a function so we can set _boxed_call attribute + forward_fn = fx_g.forward + + def run(args: Any) -> Any: + from torch.utils._debug_mode import DebugInterpreter, get_active_debug_mode + + if ( + debug_mode := get_active_debug_mode() + ) is not None and debug_mode.run_compile_with_interpreter: + return DebugInterpreter(fx_g, backend="aot_eager").run(*args) + return forward_fn(args) + + run._boxed_call = True # type: ignore[attr-defined] + return run + + +def boxed_nop_with_mode( + fx_g: torch.fx.GraphModule, + example_inputs: list[torch.Tensor], + *, + mode: torch.overrides.TorchFunctionMode, +) -> Callable[..., Any]: + from torch.fx.graph import _BoxedCodeGen + + # Set the graph to use boxed codegen + fx_g.graph.set_codegen(_BoxedCodeGen()) + fx_g.recompile() + + # Create a wrapper that runs with the mode + forward_fn = fx_g.forward + + def run(args: Any) -> Any: + with mode: + return forward_fn(args) + + run._boxed_call = True # type: ignore[attr-defined] + return run + + +def fake_crossref_boxed_nop( + fx_g: torch.fx.GraphModule, + example_inputs: list[torch.Tensor], + ignore_op_fn: Callable[[torch._ops.OpOverload], bool] | None = None, +) -> Callable[..., Any]: + from torch.fx.graph import _BoxedCodeGen + + # Set the graph to use boxed codegen + fx_g.graph.set_codegen(_BoxedCodeGen()) + fx_g.recompile() + + # Create a wrapper that runs with the mode + forward_fn = fx_g.forward + + def run(args: Any) -> Any: + with torch._subclasses.CrossRefFakeMode(ignore_op_fn): + return forward_fn(args) + + run._boxed_call = True # type: ignore[attr-defined] + return run + + +def ignore_builtins(op: torch._ops.OpOverload) -> bool: + return op.namespace in ("aten", "prims", "prim") + + +def get_nop_func() -> Callable[ + [torch.fx.GraphModule, list[torch.Tensor]], Callable[..., Any] +]: + if not torch._functorch.config.fake_tensor_crossref: + return boxed_nop + elif torch._functorch.config.fake_tensor_crossref == "all": + return fake_crossref_boxed_nop + else: + assert torch._functorch.config.fake_tensor_crossref == "custom_ops" + return functools.partial(fake_crossref_boxed_nop, ignore_op_fn=ignore_builtins) + + +# Useful for debugging purpose +# aot_eager uses AOT Autograd backend with nop compiler. It is helpful in debugging. +def aot_eager( + gm: torch.fx.GraphModule, + fake_tensor_inputs: list[torch.Tensor], + fw_compiler: Callable[..., Any] | None = None, + bw_compiler: Callable[..., Any] | None = None, + **kwargs: Any, +) -> Callable[..., Any]: + return aot_autograd( + fw_compiler=fw_compiler or boxed_nop, + bw_compiler=bw_compiler or boxed_nop, + partition_fn=min_cut_rematerialization_partition, + keep_inference_input_mutations=True, + )(gm, fake_tensor_inputs, **kwargs) + + +register_backend(name="aot_eager", compiler_fn=aot_eager) + +aot_eager_default_partitioner = aot_autograd( + fw_compiler=boxed_nop, keep_inference_input_mutations=True +) +register_backend( + name="aot_eager_default_partitioner", compiler_fn=aot_eager_default_partitioner +) + + +# Uses TorchInductor AOT Autograd decomps and partitioner to isolate aot vs +# inductor problems. +# aot_eager_decomp_partition just replaces the inductor compiler with nop to help +# isolate inductor vs aot_eager errors +def aot_eager_decomp_partition( + gm: torch.fx.GraphModule, fake_tensor_inputs: list[torch.Tensor], **kwargs: Any +) -> Callable[..., Any]: + if kwargs: + log.warning( + "aot_eager_decomp_partition backend ignoring extra kwargs %s", kwargs + ) + + from torch._inductor.compiler_bisector import CompilerBisector + + config_patches = {"unlift_effect_tokens": True} + if bisect_changes := CompilerBisector.get_config_change( + "aot_eager_decomp_partition" + ): + config_patches.update(bisect_changes) # type: ignore[arg-type] + + with functorch_config.patch(config_patches): + return aot_autograd( + # these are taken from memory_efficient_fusion() + fw_compiler=get_nop_func(), + bw_compiler=get_nop_func(), + # NB: lambda here is to delay import of inductor + decompositions=lambda: import_module( + "torch._inductor.compile_fx" + ).select_decomp_table(), + partition_fn=functools.partial( + min_cut_rematerialization_partition, compiler="inductor" + ), + )(gm, fake_tensor_inputs) + + +register_backend( + name="aot_eager_decomp_partition", compiler_fn=aot_eager_decomp_partition +) + + +# aot_eager_decomp_partition_with_mode is similar as aot_eager_decomp_partition, +# except that it takes a TorchDispatchMode mode and run the fw/bw in the mode +def aot_eager_decomp_partition_with_mode( + gm: torch.fx.GraphModule, + fake_tensor_inputs: list[torch.Tensor], + mode: Any, + **kwarg: Any, +) -> Callable[..., Any]: + return aot_autograd( + # these are taken from memory_efficient_fusion() + fw_compiler=functools.partial(boxed_nop_with_mode, mode=mode), + bw_compiler=functools.partial(boxed_nop_with_mode, mode=mode), + # NB: lambda here is to delay import of inductor + decompositions=lambda: import_module( + "torch._inductor.compile_fx" + ).select_decomp_table(), + partition_fn=functools.partial( + min_cut_rematerialization_partition, compiler="inductor" + ), + )(gm, fake_tensor_inputs) + + +register_backend( + name="aot_eager_decomp_partition_with_mode", + compiler_fn=aot_eager_decomp_partition_with_mode, # type: ignore[arg-type] +) + + +def aot_eager_decomp_partition_crossref( + gm: torch.fx.GraphModule, fake_tensor_inputs: list[torch.Tensor], **kwargs: Any +) -> Callable[..., Any]: + # if the config is set, respect it, otherwise only test custom_ops. + # custom_op bad metas always manifest as an error whereas aten will only sometimes. + # by default, use the less noisy option + config_val = ( + "custom_ops" + if not functorch_config.fake_tensor_crossref + else functorch_config.fake_tensor_crossref + ) + with functorch_config.patch(fake_tensor_crossref=config_val): + return aot_eager_decomp_partition(gm, fake_tensor_inputs, **kwargs) + + +register_backend( + name="aot_eager_decomp_partition_crossref", + compiler_fn=aot_eager_decomp_partition_crossref, +) + + +# AOT Autograd with torchscript backend. Default partitioner. +# aot_ts uses torchscript backend. We can use this with both nnc and nvfuser +# by using the relevant fuser with torch.jit.fuser(...) +aot_ts = aot_autograd(fw_compiler=ts_compile) +register_backend(name="aot_ts", compiler_fn=aot_ts) + +# These buggy backends are used for inducing bugs so that we can test +# our repro extraction / minifier scripts + + +class ReluCompileError(Exception): + pass + + +class TestingOnlyCompileError(Exception): + pass + + +@register_backend +def relu_compile_error_TESTING_ONLY( + gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor] +) -> torch.fx.GraphModule: + for node in gm.graph.nodes: + if node.target is torch.relu: + raise ReluCompileError + return gm + + +@register_backend +def relu_runtime_error_TESTING_ONLY( + gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor] +) -> torch.fx.GraphModule: + for node in gm.graph.nodes: + if node.target is torch.relu: + node.target = torch._assert + node.args = (False, "ReluRuntimeError") + gm.recompile() + return gm + + +@register_backend +def relu_accuracy_error_TESTING_ONLY( + gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor] +) -> torch.fx.GraphModule: + for node in gm.graph.nodes: + if node.target is torch.relu: + node.target = torch.add + node.args = (node.args[0], 1) + gm.recompile() + + return gm + + +@register_backend +def non_leaf_compile_error_TESTING_ONLY( + gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor] +) -> torch.fx.GraphModule: + # Require at least one non-trivial thing in the graph, + # see https://github.com/pytorch/pytorch/issues/102898 + for node in gm.graph.nodes: + if node.op == "call_function": + break + else: + return gm + for t in example_inputs: + if not t.is_leaf: + raise TestingOnlyCompileError + return gm + + +@dataclasses.dataclass +class ExplainOutput: + """ + This is the output of :func:`torch._dynamo.explain()` + There is no reason to create this class directly. + """ + + graphs: list[torch.fx.GraphModule] + graph_count: int + graph_break_count: int + break_reasons: list[GraphCompileReason] + op_count: int + ops_per_graph: list[list["Target"]] | None = None + out_guards: list[_guards.Guard] | None = None + compile_times: str | None = None + + def __str__(self) -> str: + output = f"Graph Count: {self.graph_count}\n" + output += f"Graph Break Count: {self.graph_break_count}\n" + output += f"Op Count: {self.op_count}\n" + + output += "Break Reasons:\n" + for idx, break_reason in enumerate(self.break_reasons): + output += f" Break Reason {idx + 1}:\n" + output += f" Reason: {break_reason.reason}\n" + output += " User Stack:\n" + for frame_summary in break_reason.user_stack: + output += f" {frame_summary}\n" + + if self.ops_per_graph is not None: + output += "Ops per Graph:\n" + for idx, ops in enumerate(self.ops_per_graph): + output += f" Ops {idx + 1}:\n" + for op in ops: + output += f" {op}\n" + + if self.out_guards is not None: + output += "Out Guards:\n" + for i, guard in enumerate(self.out_guards): + output += f" Guard {i + 1}:\n" + output += f" {str(guard)}" + + if self.compile_times is not None: + output += f"Compile Times: {self.compile_times}\n" + return output + + +def _explain_graph_detail( + gm: torch.fx.GraphModule, + graphs: list[torch.fx.GraphModule], + op_count: int, + ops_per_graph: list[list["Target"]], + break_reasons: list[GraphCompileReason], +) -> tuple[ + torch.fx.GraphModule, + list[torch.fx.GraphModule], + int, + list[list["Target"]], + list[GraphCompileReason], +]: + """ + This function is a utility which processes a torch.fx.GraphModule and + accumulates information about its ops, graph breaks, and other details. It + is intended to be used by the ExplainWithBackend class and + `torch._dynamo.explain()` to provide details from Dynamo's graph capture. + + Parameters: + gm (torch.fx.GraphModule): The GraphModule to be processed. + graphs (list): A list that accumulates all the GraphModules processed. + op_count (int): The total count of operations in all GraphModules processed so far. + ops_per_graph (list): A list that accumulates the operations of each GraphModule. + break_reasons (list): A list that accumulates the reasons for breaks in each GraphModule. + + Returns: + tuple: A tuple containing the processed GraphModule, the updated lists of graphs, + operations per graph, and break reasons, and the updated operation count. + """ + graphs.append(gm) + ops = [node.target for node in gm.graph.nodes if node.op == "call_function"] + op_count += len(ops) + ops_per_graph.append(ops) + if gm.compile_subgraph_reason.graph_break: # type: ignore[union-attr] + break_reasons.append(gm.compile_subgraph_reason) # type: ignore[arg-type] + + return gm, graphs, op_count, ops_per_graph, break_reasons + + +class ExplainWithBackend: + """ + This class is intended to be used as a backend for `torch.compile`. It is + composable with other backends. When used in this way, it accumulates + information about graph breaks, ops, and other info and provides a string + representation summarizing this information. + + Attributes: + backend (str): The name of the backend to use for optimization. + graphs (list): A list of the graphs captured by TorchDynamo. + op_count (int): The total number of operations in all optimized graphs. + break_reasons (list): A list of graph break reasons with stack traces. + + Example Usage: + def fn(x): + x = torch.sigmoid(x) + return x + + torch._dynamo.reset() + eb = ExplainWithBackend("inductor") + optimized_fn = torch.compile(fn, backend=eb) + result = optimized_fn(torch.randn(5)) + print(eb.output()) + """ + + def __init__(self, backend: CompilerFn | str) -> None: + from .registry import lookup_backend + + self.backend = lookup_backend(backend) + self.graphs: list[torch.fx.GraphModule] = [] + self.op_count = 0 + self.break_reasons: list[GraphCompileReason] = [] + + def __call__( + self, gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor] + ) -> CompiledFn: + ops_per_graph: list[list[Target]] = [] + gm, self.graphs, self.op_count, _, self.break_reasons = _explain_graph_detail( + gm, self.graphs, self.op_count, ops_per_graph, self.break_reasons + ) + return self.backend(gm, example_inputs) + + def output(self) -> ExplainOutput: + graph_count = len(self.graphs) + output = ExplainOutput( + self.graphs, + graph_count, + graph_count - 1, + self.break_reasons, + self.op_count, + ) + + return output diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/distributed.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/distributed.py new file mode 100644 index 0000000000000000000000000000000000000000..bbd8dc53eaa7056835d0e1b13510593b2b47a5dd --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/distributed.py @@ -0,0 +1,622 @@ +""" +This module implements distributed training optimizations for TorchDynamo backends. + +It provides functionality to optimize models wrapped in DistributedDataParallel (DDP) +by intelligently splitting compiled graphs to align with DDP's gradient synchronization +boundaries. Key features include: + +- Graph partitioning based on parameter bucket sizes +- Optimization of allreduce operations for distributed training +- Support for parameter ignoring and buffer handling +- Submodule compilation and management +- Debugging utilities for distributed training + +The main component is the DDPOptimizer class, which handles graph splitting and +recompilation to enable efficient distributed training while maintaining the benefits +of compilation. +""" + +import logging +import traceback +from collections.abc import Callable +from dataclasses import dataclass, field +from typing import Any, TYPE_CHECKING +from unittest import mock + +import torch +from torch import fx +from torch._dynamo.backends.registry import CompiledFn, CompilerFn +from torch._dynamo.output_graph import GraphCompileReason +from torch._dynamo.utils import deepcopy_to_fake_tensor, detect_fake_mode +from torch._logging import trace_structured +from torch.fx.node import Node + + +if TYPE_CHECKING: + from torch._functorch._aot_autograd.schemas import ViewAndMutationMeta + + +# Regular log messages should go through 'log'. +# ddp_graph_log is a separate artifact logger reserved for dumping graphs. +# See docs/source/logging.rst for more info. +log = logging.getLogger(__name__) +ddp_graph_log = torch._logging.getArtifactLogger(__name__, "ddp_graphs") + + +def args_str(args: Any) -> str: + # a debug helper + if torch.is_tensor(args): + return f"T[{args.shape}]" + elif isinstance(args, tuple): + return f"tuple({', '.join([args_str(x) for x in args])})" + elif isinstance(args, list): + return f"list({', '.join([args_str(x) for x in args])})" + else: + return str(args) + + +@dataclass +class Bucket: + size: int = 0 + params: list[str] = field(default_factory=list) + nodes: list[fx.Node] = field(default_factory=list) + + # param_ids is just used for unit testing + param_ids: list[int] = field(default_factory=list) + + # keep track of any buckets that were extended for logging purposes + opcount_increased_to_capture_external_output: int = 0 + paramsize_before_opcount_increase: int = 0 + + +def bucket_has_external_output(bucket: Bucket) -> bool: + nodes_in_bucket = set() + # we want to iterate in reverse order, but clumsi-luckily the bucket.nodes list was already created backwards + # so we don't reverse it here + for node in bucket.nodes: + # assume node.op != output, since those are filtered in the original iteration + nodes_in_bucket.add(node) + for user in node.users: + if user not in nodes_in_bucket: + return True + return False + + +def pretty_print_buckets(buckets: list[Bucket], bucket_bytes_cap: int) -> None: + headers = ("Index", "Size (b)", "Param Names") + rows: list[tuple[int | None, int | None, str]] = [] + # pyrefly: ignore [implicit-any] + extended_buckets = [] + for idx, bucket in enumerate(reversed(buckets)): + if len(bucket.params) > 0: + rows.append((idx, bucket.size, bucket.params[0])) + rows.extend((None, None, param) for param in bucket.params[1:]) + if bucket.opcount_increased_to_capture_external_output > 0: + extended_buckets.append( + ( + idx, + bucket.opcount_increased_to_capture_external_output, + bucket.size - bucket.paramsize_before_opcount_increase, + ) + ) + + if rows: + log.info( + "\nDDPOptimizer used bucket cap %s and created %d buckets. Enable debug logs for detailed bucket info.", + bucket_bytes_cap, + len(buckets), + ) + + if extended_buckets: + log.warning( + "Some buckets were extended beyond their requested parameter capacities" + " in order to ensure each subgraph has an output node, required for fx graph partitioning." + " This can be the case when a subgraph would have only contained nodes performing inplace mutation," + " and returning no logical outputs. This should not be a problem, unless it results in too few graph" + " partitions for optimal DDP performance." + ) + + try: + from tabulate import tabulate + + log.debug( + "\nDDPOptimizer produced the following bucket assignments:\n%s", + tabulate(rows, headers=headers, tablefmt="simple_grid"), + ) + + if extended_buckets: + log.warning( + "DDPOptimizer extended these buckets to ensure per-subgraph output nodes:\n%s", + tabulate( + extended_buckets, + headers=("Index", "Extra Ops", "Extra Param Size (b)"), + tablefmt="simple_grid", + ), + ) + except ImportError: + log.debug( + "Please `pip install tabulate` in order to display ddp bucket sizes and diagnostic information." + ) + else: + log.debug("DDPOptimizer captured no parameters and did not split this graph.") + + +def has_higher_order_op(gm: fx.GraphModule) -> bool: + # Check if there is a higher order op in the graph + for node in gm.graph.nodes: + if node.op == "get_attr": + maybe_param = getattr(gm, node.target) + if isinstance(maybe_param, torch.fx.GraphModule): + return True + return False + + +def propagate_metadata(orig_gm: fx.GraphModule, split_gm: fx.GraphModule) -> None: + for name, module in split_gm.named_modules(): + if "." not in name and len(name): + # TODO: add split id to CompileId: https://github.com/pytorch/tlparse/pull/83/files#r1880649384 + module.meta = orig_gm.meta + module._param_name_to_source = orig_gm._param_name_to_source + + +def propagate_dynamo_source(orig_gm: fx.GraphModule, split_gm: fx.GraphModule) -> None: + name_to_dynamo_source = {} + for node in orig_gm.graph.find_nodes(op="placeholder"): + name_to_dynamo_source[node.name] = node._dynamo_source + + for name, module in split_gm.named_modules(): + if "." not in name and len(name): + for node in module.graph.find_nodes(op="placeholder"): + # non-placeholder in original_gm may become placeholder in submodules + node._dynamo_source = name_to_dynamo_source.get(node.name) + + +class DDPOptimizerContext: + def __init__(self) -> None: + self.curr_bucket: int = -1 + self.metadata_per_bucket: list[ViewAndMutationMeta] = [] + + +# compile each of the partitioned submodules using the user-provided compiler +class SubmodCompiler(torch.fx.interpreter.Interpreter): + def __init__( + self, + module: fx.GraphModule, + compiler: CompilerFn, + fake_mode: torch._subclasses.fake_tensor.FakeTensorMode, + ) -> None: + super().__init__(module) + self.compiler = compiler + self.fake_mode = fake_mode + # See Note [DDPOptimizer and fw_metadata] + ctx = torch._guards.TracingContext.try_get() + if ctx is not None: + ctx.ddp_optimizer_ctx = DDPOptimizerContext() + + def compile_submod( + self, input_mod: fx.GraphModule, args: list[torch.Tensor], kwargs: Any + ) -> Any: + """ + Compile the submodule, + using a wrapper to make sure its output is always a tuple, + which is required by AotAutograd based compilers + """ + assert len(kwargs) == 0, "We assume only args for these modules" + + class WrapperModule(torch.nn.Module): + def __init__( + self, submod: Callable[..., Any], unwrap_singleton_tuple: bool + ) -> None: + super().__init__() + self.submod = submod + self.unwrap_singleton_tuple = unwrap_singleton_tuple + + def forward(self, *args: Any) -> Any: + x = self.submod(*args) + # TODO(whc) + # for some reason the isinstance check is necessary if I split one node per submod + # - even though I supposedly wrapped the output in a tuple in those cases, the real + # compiled module was still returning a tensor + if self.unwrap_singleton_tuple and isinstance(x, (tuple, list)): + return x[0] + return x + + unwrap_singleton_tuple = False + for sn in input_mod.graph.nodes: + if sn.op == "output": + if not isinstance(sn.args[0], tuple): + unwrap_singleton_tuple = True + sn.args = (sn.args,) + + input_mod.recompile() + input_mod.compile_subgraph_reason = GraphCompileReason( # type: ignore[assignment] + "DDPOptimizer intentional graph-break (See Note [DDPOptimizer])." + " Set `torch._dynamo.config.optimize_ddp = False` to disable.", + [ + # it's close to useless to get a real stacktrace here, and quite verbose. + traceback.FrameSummary(__file__, 0, "DDPOptimizer"), + ], + ) + + wrapper = WrapperModule( + self.compiler(input_mod, args), + unwrap_singleton_tuple, + ) + return wrapper + + # Note: + # + # The way distributed works today around fake tensors can be somewhat confusing. + # Some of these codepaths are shared in both runtime, and compile time. The presence + # of a fake_mode, read off of fake tensor inputs, dictates how we will operate. + # + # A few things to keep in mind: + # + # 1) We invoke `compile_submod` with a real module. The output of that gets stored + # on the graph via `self.module.add_submodule(n.target, compiled_submod_real)`. + # + # 2) When running a call_module targeted node, if we have a fake_mode, we fakify the + # module we got from self.fetch_attr(n.target). Regardless of fake_mode, we then execute it. + # + # 3) Fake tensors should always be around during compile time. + # + # 4) Fake tensors should never be around at runtime. + # + # 5) We end up with a compilation mode that takes a real submodule and fake tensors, + # to match what aot_autograd expects. See Note: [Fake Modules and AOTAutograd] + def run_node(self, n: Node) -> Any: + args, kwargs = self.fetch_args_kwargs_from_env(n) + new_args = [] + assert self.fake_mode + for arg in args: + if isinstance(arg, torch.Tensor) and not isinstance( + arg, torch._subclasses.FakeTensor + ): + new_args.append(torch._dynamo.utils.to_fake_tensor(arg, self.fake_mode)) + else: + new_args.append(arg) + + log.debug("run_node %s, %s got args %s", n.op, n.target, args_str(args)) + assert isinstance(args, tuple) + assert isinstance(kwargs, dict) + + if n.op == "call_module": + real_mod = self.fetch_attr(str(n.target)) + if self.fake_mode: + curr_submod = deepcopy_to_fake_tensor(real_mod, self.fake_mode) + else: + curr_submod = real_mod + + ddp_graph_log.debug("\n---%s graph---\n%s", n.target, curr_submod.graph) + + # When calling the compiler on the submod, inputs (new_args) are expected to + # be FakeTensors already since Dynamo would have made them FakeTensors in the + # non-DDP flow. However, the parameters are _not_ expected to be FakeTensors, + # since this wrapping happens during compilation + + # Note: Returning Fake Tensors on First AOT Autograd Call + # + # Inductor will optimize strides of outputs when it deems it profitable. + # For instance, converting to channels last. When we split the graph here + # into multiple inductor compilations, we need to make sure that the + # output strides of one compilation is appropriately passed to the subsequent + # compilations. However, the mapping from inductor output to dynamo output + # is non-trivial due to aot_autograd's deduping, de-aliasing, mutation, re-writing, + # subclass handling, etc. In order to replay all this logic we set a flag such that + # the first invocation of inductor in aot_autograd will return Fake Tensors with + # appropriate strides. Then, all of aot autograd's runtime logic is replayed. + # This gives us the appropriately strided outputs here which will reflect runtime strides. + + class FakeifyFirstAOTInvocationGuard: + def __init__(self) -> None: + self.tc = torch._guards.TracingContext.try_get() + assert self.tc + self.tc.fakify_first_call = True + + def __del__(self) -> None: + self.tc.fakify_first_call = False # type: ignore[union-attr] + + # For aot_eager and other backends, tracing context is not set + has_tracing_context = torch._guards.TracingContext.try_get() is not None + if has_tracing_context: + g = FakeifyFirstAOTInvocationGuard() # noqa: F841 + + from torch._dynamo.utils import counters + + init = counters["aot_autograd"]["total"] + compiled_submod_real = self.compile_submod(real_mod, new_args, kwargs) + + # TODO - better way of doing this? + # Only aot autograd handles fakifying first call + invoked_aot_autograd = init != counters["aot_autograd"]["total"] + + # We update the original (outer) graph with a call into the compiled module + # instead of the uncompiled one. + self.module.delete_submodule(n.target) # type: ignore[operator] + n.target = "compiled_" + n.target # type: ignore[operator] + self.module.add_submodule(n.target, compiled_submod_real) # type: ignore[operator] + + # Finally, we have to produce inputs for use compiling the next submodule, + # and these need to be FakeTensors, so we execute the module under fake_mode + # Because parameters are not fake we patch fake tensor mode to allow non fake inputs + with ( + self.fake_mode, + mock.patch.object(self.fake_mode, "allow_non_fake_inputs", True), + ): + if has_tracing_context and invoked_aot_autograd: + tracing_ctx = torch._guards.TracingContext.try_get() + assert tracing_ctx is not None + # DDPOptimizer maintains 1 dynamo graph -> N AOT graphs + # Dynamo only has 1 tracing context, so it needs to maintain all N AOT metadata instances + ddp_ctx = tracing_ctx.ddp_optimizer_ctx + assert ddp_ctx is not None + assert tracing_ctx.fw_metadata is not None + ddp_ctx.curr_bucket += 1 + ddp_ctx.metadata_per_bucket.append(tracing_ctx.fw_metadata) + + out = compiled_submod_real(*new_args, **kwargs) + # output should be fake or subclass + assert all( + (not isinstance(t, torch.Tensor) or type(t) is not torch.Tensor) + for t in (out if isinstance(out, (list, tuple)) else [out]) + ) + return out + else: + return curr_submod(*new_args, **kwargs) + else: + # placeholder or output nodes don't need to get compiled, just executed + return getattr(self, n.op)(n.target, new_args, kwargs) + + +class DDPOptimizer: + """Note [DDPOptimizer] + DDPOptimizer applies when dynamo compiles models wrapped in DistributedDataParallel (DDP), + breaking the dynamo graph into chunks to compile separately, with the breaks aligning to + the boundaries of gradient-allreduce buckets chosen by DDP. + + Background/Motivation + - DDP uses allreduce collectives to synchronize partial gradients computed on different workers + - DDP groups gradient allreduces into 'buckets' to optimize communication efficiency of all-reduce + - Parameters grouped into buckets are assumed to be adjacent in time, so they become ready + at around the same time during backward and thus can share the same allreduce efficiently + - Allreduces must overlap with backward compute for optimal training performance + - DDP schedules allreduces using 'hooks' fired from the c++ autograd engine in pytorch, which + operates when individual grads become 'ready' + - Dynamo+AOTAutograd produces a single fused graph that runs 'atomically' from the perspective of the + autograd engine, such that all gradients become 'ready' at the same time. Hooks fire after the whole + fused backward function executes, preventing any overlap of compute and communication + + Algorithm + - DDPOptimizer starts off with an FX graph traced by dynamo which represents forward. It can traverse + this graph in reverse order to determine the true order that gradients will become ready during backward. + - Parameter sizes are counted in reverse order, up to a bucket size limit, at which point a new bucket is started + and a graph break introduced + - Each of the subgraphs is compiled by the compiler provided to dynamo by the user, and then fused back together + into an outer module that is returned to the user + + Notes + - It would be better to enforce (by adding an API to DDP) that the bucket splits chosen here are used by DDP, + and that DDP does not need to detect or optimize bucket order by observing execution at runtime, as it does + in eager. + - If Dynamo can't capture a whole graph for the portion of the model wrapped by DDP, this algorithm will currently + produce splits that do not necessarily align with the buckets used by DDP. This should result in performance + degradation approaching the baseline case where graph-splits are not used, but not worse. + - If the backend compiler fails to compile a single subgraph, it will execute eagerly despite the rest of the + subgraphs being compiled + - DDP has a 'parameters_and_buffers_to_ignore' field, which DDPOptimizer attempts to honor by reading markers + left by DDP on individual parameters. In cases where other transformations, such as reparameterization, are + also used, the ignore markers could be lost. If DDPOptimizer fails to ignore a parameter ignored by DDP, + it is not catastrophic but could impact performance by choosing sub-optimal bucket splits. + - DDPOptimizer always ignores all buffers, regardless of their ignore flag, since buffers do not require gradients, + and therefore aren't allreduced by DDP. (They are broadcast during forward, but this is not covered by + DDPOptimizer) + + Debugging + - Generally, it is easiest to debug DDPOptimizer in a single process program, using pdb. + - In many cases, the log messages are helpful (they show bucket size assignments)- + just set TORCH_LOGS env to include any of 'dynamo', 'distributed', or 'dist_ddp'. + - See `benchmarks/dynamo/distributed.py` for a simple harness that will run a toy model or a torchbench model + in a single process (or with torchrun, in multiple processes) + + Args: + bucket_bytes_cap (int): Controls the size of buckets, in bytes, used to determine graphbreaks. Should be + set to match the equivalent parameter on the original DDP module. + + backend_compile_fn (callable): A dynamo compiler function, to be invoked to compile each subgraph. + + first_bucket_cap (int): Controls the size of the first bucket. Should match DDP's first bucket cap. DDP + special-cases the first bucket size since it is sometimes optimal to start a small allreduce early. + + """ + + def __init__( + self, + bucket_bytes_cap: int, + backend_compile_fn: CompilerFn, + first_bucket_cap: int | None = None, + ) -> None: + if first_bucket_cap is not None: + self.first_bucket_cap = first_bucket_cap + elif torch.distributed.is_available(): + # this constant comes from C10D lib which is not always built + self.first_bucket_cap = torch.distributed._DEFAULT_FIRST_BUCKET_BYTES + else: + self.first_bucket_cap = bucket_bytes_cap + + self.bucket_bytes_cap = bucket_bytes_cap + assert self.first_bucket_cap <= self.bucket_bytes_cap, ( + "First bucket should be smaller/equal to other buckets to get comms warmed up ASAP" + ) + + self.backend_compile_fn = backend_compile_fn + + def _ignore_parameter(self, parameter: torch.nn.Parameter) -> bool: + return hasattr(parameter, "_ddp_ignored") and parameter._ddp_ignored + + def add_param(self, bucket: Bucket, param: torch.nn.Parameter, name: str) -> None: + bucket.size += param.untyped_storage().nbytes() + bucket.params.append(name) + bucket.param_ids.append(id(param)) + + def add_module_params_to_bucket( + self, + mod: torch.nn.Module, + bucket: Bucket, + processed_modules: set[torch.nn.Module], + prefix: str, + ) -> None: + processed_modules.add(mod) + for name, param in mod.named_parameters(): + if param.requires_grad and not self._ignore_parameter(param): + self.add_param(bucket, param, f"{prefix}_{name}") + + def add_param_args(self, bucket: Bucket, node: fx.Node) -> None: + for arg in node.args: + if not isinstance(arg, torch.fx.node.Node): + continue + if arg.op != "placeholder": + continue + param = arg.meta["example_value"] + if ( + isinstance(param, torch.nn.Parameter) + and param.requires_grad + and not self._ignore_parameter(param) + ): + self.add_param(bucket, param, str(arg.target)) + + def compile_fn( + self, gm: fx.GraphModule, example_inputs: list[torch.Tensor] + ) -> CompiledFn: + """ + Implements graph splitting, first determining a set of of buckets by counting + parameter sizes in reverse graph order, then invoking the user/backend compiler + to compile each subgraph. Finally, stitches compiled graphs into one graphmodule + and returns its callable. + """ + # 1: compute the partition map according to DDP bucket logic + buckets = [Bucket()] # (size, param_names) + processed_modules: set[torch.nn.Module] = set() + for node in reversed(gm.graph.nodes): + if node.op in ("output", "placeholder"): + continue + + if ( + buckets[0].size >= self.bucket_bytes_cap + or len(buckets) == 1 + and buckets[0].size >= self.first_bucket_cap + ): + if bucket_has_external_output(buckets[0]): + buckets.insert(0, Bucket()) + else: + # continue building this bucket past the point of filling its parameter capacity, + # to increase chances it contains at least one node that is either a global output or + # passed as input to a subsequent graph + + if buckets[0].opcount_increased_to_capture_external_output == 0: + buckets[0].paramsize_before_opcount_increase = buckets[0].size + buckets[0].opcount_increased_to_capture_external_output += 1 + + if node.op == "call_function": + self.add_param_args(buckets[0], node) + + elif node.op == "call_module": + target_mod = gm.get_submodule(node.target) + if target_mod not in processed_modules: + self.add_module_params_to_bucket( + target_mod, buckets[0], processed_modules, node.target + ) + elif node.op == "call_method": + if isinstance(node.args[0].target, str): + target_mod = None + try: + target_mod = gm.get_submodule(node.args[0].target) + except AttributeError: + pass + if target_mod is not None and target_mod not in processed_modules: + self.add_module_params_to_bucket( + target_mod, buckets[0], processed_modules, node.target + ) + # This handles situations like tmp = torch.mm(x, self.weight.t()) + # t: "f32[512, 512]" = l_self_seq_2_weight.t(); l_self_seq_2_weight = None + # tmp: "f32[512, 512]" = torch.mm(input_2, t); input_2 = t = None + self.add_param_args(buckets[0], node) + + elif node.op == "get_attr": + maybe_param = getattr(gm, node.target) + if ( + isinstance(maybe_param, torch.nn.Parameter) + and maybe_param.requires_grad + and not self._ignore_parameter(maybe_param) + ): + self.add_param(buckets[0], maybe_param, node.target) + + # All nodes have to be mapped to a bucket, even if they don't have their own params + # Ignored params still end up in buckets, we just don't count them towards the capacity + buckets[0].nodes.append(node) + + if len(buckets) > 1 and buckets[0].size == 0: + # we collected a small preamble graph with ops that don't include parameters, fuse it back + buckets[1].nodes.extend(buckets[0].nodes) + assert len(buckets[0].params) == 0, "Params should be empty if size is 0" + del buckets[0] + + # stash buckets for testing/debugging purposes + self.buckets = buckets + pretty_print_buckets(buckets, self.bucket_bytes_cap) + + if len(buckets) == 1: + # bypass split/fuse logic if there is only one bucket + return self.backend_compile_fn(gm, example_inputs) + + # 2: partition the graphmodule according to bucket capacity + partition_map = {} + for idx, b in enumerate(buckets): + for node in b.nodes: + partition_map[node] = idx + + split_gm = fx.passes.split_module.split_module( + gm, + None, # type: ignore[arg-type] + lambda node: partition_map[node], + ) + + # See note [Assumption on Dynamo Metadata] + propagate_dynamo_source(gm, split_gm) + propagate_metadata(gm, split_gm) + + debug_str = ( + f"\n---orig graph---\n{gm.graph}\n" + + f"\n---split graph---\n{split_gm.graph}\n" + ) + for name, module in split_gm.named_modules(): + if "." not in name and len(name): + # only print the submod graphs, not their children + debug_str += f"\n---{name} graph---\n{module.graph}\n" + debug_str += "\n---------------\n" + ddp_graph_log.debug(debug_str) + + trace_structured( + "optimize_ddp_split_graph", + payload_fn=lambda: split_gm.print_readable(print_output=False), + ) + for name, module in split_gm.named_modules(): + if "." not in name and len(name): + trace_structured( + "optimize_ddp_split_child", + lambda: {"name": name}, + payload_fn=lambda: module.print_readable(print_output=False), + ) + + fake_mode = detect_fake_mode(example_inputs) + if fake_mode is None: + fake_mode = torch._subclasses.fake_tensor.FakeTensorMode() + + submod_compiler = SubmodCompiler(split_gm, self.backend_compile_fn, fake_mode) + with torch._dynamo.utils._disable_saved_tensors_hooks_during_tracing(): + submod_compiler.run(*example_inputs) + split_gm.recompile() + + ddp_graph_log.debug( + "\n---final graph---\n%s\n---------------\n", split_gm.graph + ) + return split_gm diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/inductor.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/inductor.py new file mode 100644 index 0000000000000000000000000000000000000000..ae62dd56678b8349d27fe909f12482b884ca596c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/inductor.py @@ -0,0 +1,31 @@ +""" +This module provides the TorchInductor backend integration for TorchDynamo. + +TorchInductor is a compiler backend that generates optimized code for both CPU and GPU. +This module lazily imports and registers the TorchInductor compiler to avoid loading it +into memory when it is not being used. This helps reduce memory overhead when using +other backends. + +The inductor backend can be used with torch.compile(): + model = torch.compile(model, backend="inductor") +""" + +from typing import Any + +from torch._dynamo import register_backend +from torch._dynamo.utils import dynamo_timed + + +@register_backend +def inductor(*args: Any, **kwargs: Any) -> Any: + with dynamo_timed("inductor_import", log_pt2_compile_event=True): + # do import here to avoid loading inductor into memory when it is not used + # The AsyncCompile subproc pool can be slow to start, so warm it up as early + # as possible. + from torch._inductor.async_compile import maybe_warm_pool + + maybe_warm_pool() + + from torch._inductor.compile_fx import compile_fx + + return compile_fx(*args, **kwargs) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/onnxrt.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/onnxrt.py new file mode 100644 index 0000000000000000000000000000000000000000..93490e64f4ae2044d0c641f8171e733ed7a8e141 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/onnxrt.py @@ -0,0 +1,39 @@ +# This backend is maintained by ONNX team. To direct issues +# to the right people, please tag related GitHub issues with `module: onnx`. +# +# Maintainers' Github IDs: wschin, xadupre +# from torch.onnx._internal.onnxruntime import ( +# is_onnxrt_backend_supported, +# torch_compile_backend, +# ) + +# from .registry import register_backend + +""" +Placeholder for onnxruntime backend for dynamo +""" + +# def has_onnxruntime(): +# # FIXME: update test/dynamo/test_backends.py to call is_onnxrt_backend_supported() +# return is_onnxrt_backend_supported() + + +# if is_onnxrt_backend_supported(): +# register_backend(name="onnxrt", compiler_fn=torch_compile_backend) +# else: + +# def information_displaying_backend(*args, **kwargs): +# raise ImportError( +# "onnxrt is not registered as a backend. " +# "Please make sure all dependencies such as " +# "numpy, onnx, onnxscript, and onnxruntime-training are installed. " +# "Suggested procedure to fix dependency problem:\n" +# " (1) pip or conda install numpy onnx onnxscript onnxruntime-training.\n" +# " (2) Open a new python terminal.\n" +# " (3) Call the API `torch.onnx.is_onnxrt_backend_supported()`:\n" +# " (4) If it returns `True`, then you can use `onnxrt` backend.\n" +# " (5) If it returns `False`, please execute the package importing section in " +# "torch/onnx/_internal/onnxruntime.py under pdb line-by-line to see which import fails." +# ) + +# register_backend(name="onnxrt", compiler_fn=information_displaying_backend) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/registry.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/registry.py new file mode 100644 index 0000000000000000000000000000000000000000..f8b311ceb52f806347dd6007fee15cefb89197e8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/registry.py @@ -0,0 +1,206 @@ +""" +This module implements TorchDynamo's backend registry system for managing compiler backends. + +The registry provides a centralized way to register, discover and manage different compiler +backends that can be used with torch.compile(). It handles: + +- Backend registration and discovery through decorators and entry points +- Lazy loading of backend implementations +- Lookup and validation of backend names +- Categorization of backends using tags (debug, experimental, etc.) + +Key components: +- CompilerFn: Type for backend compiler functions that transform FX graphs +- _BACKENDS: Registry mapping backend names to entry points +- _COMPILER_FNS: Registry mapping backend names to loaded compiler functions + +Example usage: + @register_backend + def my_compiler(fx_graph, example_inputs): + # Transform FX graph into optimized implementation + return compiled_fn + + # Use registered backend + torch.compile(model, backend="my_compiler") + +The registry also supports discovering backends through setuptools entry points +in the "torch_dynamo_backends" group. Example: +``` +setup.py +--- +from setuptools import setup + +setup( + name='my_torch_backend', + version='0.1', + packages=['my_torch_backend'], + entry_points={ + 'torch_dynamo_backends': [ + # name = path to entry point of backend implementation + 'my_compiler = my_torch_backend.compiler:my_compiler_function', + ], + }, +) +``` +``` +my_torch_backend/compiler.py +--- +def my_compiler_function(fx_graph, example_inputs): + # Transform FX graph into optimized implementation + return compiled_fn +``` +Using `my_compiler` backend: +``` +import torch + +model = ... # Your PyTorch model +optimized_model = torch.compile(model, backend="my_compiler") +``` +""" + +import functools +import logging +from collections.abc import Callable, Sequence +from importlib.metadata import EntryPoint +from typing import Any, Protocol + +import torch +from torch import fx + + +log = logging.getLogger(__name__) + + +class CompiledFn(Protocol): + def __call__(self, *args: torch.Tensor) -> tuple[torch.Tensor, ...]: ... + + +CompilerFn = Callable[[fx.GraphModule, list[torch.Tensor]], CompiledFn] + +_BACKENDS: dict[str, EntryPoint | None] = {} +_COMPILER_FNS: dict[str, CompilerFn] = {} + + +def register_backend( + compiler_fn: CompilerFn | None = None, + name: str | None = None, + tags: Sequence[str] = (), +) -> Callable[..., Any]: + """ + Decorator to add a given compiler to the registry to allow calling + `torch.compile` with string shorthand. Note: for projects not + imported by default, it might be easier to pass a function directly + as a backend and not use a string. + + Args: + compiler_fn: Callable taking a FX graph and fake tensor inputs + name: Optional name, defaults to `compiler_fn.__name__` + tags: Optional set of string tags to categorize backend with + """ + if compiler_fn is None: + # @register_backend(name="") syntax + return functools.partial(register_backend, name=name, tags=tags) # type: ignore[return-value] + assert callable(compiler_fn) + name = name or compiler_fn.__name__ + assert name not in _COMPILER_FNS, f"duplicate name: {name}" + if compiler_fn not in _BACKENDS: + _BACKENDS[name] = None + _COMPILER_FNS[name] = compiler_fn + compiler_fn._tags = tuple(tags) # type: ignore[attr-defined] + return compiler_fn + + +register_debug_backend = functools.partial(register_backend, tags=("debug",)) +register_experimental_backend = functools.partial( + register_backend, tags=("experimental",) +) + + +def lookup_backend(compiler_fn: str | CompilerFn) -> CompilerFn: + """Expand backend strings to functions""" + if isinstance(compiler_fn, str): + if compiler_fn not in _BACKENDS: + _lazy_import() + if compiler_fn not in _BACKENDS: + from ..exc import InvalidBackend + + raise InvalidBackend(name=compiler_fn) + + if compiler_fn not in _COMPILER_FNS: + entry_point = _BACKENDS[compiler_fn] + if entry_point is not None: + register_backend(compiler_fn=entry_point.load(), name=compiler_fn) + compiler_fn = _COMPILER_FNS[compiler_fn] + return compiler_fn + + +# NOTE: can't type this due to public api mismatch; follow up with dev team +def list_backends(exclude_tags=("debug", "experimental")) -> list[str]: # type: ignore[no-untyped-def] + """ + Return valid strings that can be passed to: + + torch.compile(..., backend="name") + """ + _lazy_import() + exclude_tags_set = set(exclude_tags or ()) + + backends = [ + name + for name in _BACKENDS + if name not in _COMPILER_FNS + or not exclude_tags_set.intersection(_COMPILER_FNS[name]._tags) # type: ignore[attr-defined] + ] + return sorted(backends) + + +@functools.cache +def _lazy_import() -> None: + from .. import backends + from ..utils import import_submodule + + import_submodule(backends) + + from ..repro.after_dynamo import dynamo_minifier_backend + + assert dynamo_minifier_backend is not None + + _discover_entrypoint_backends() + + +@functools.cache +def _discover_entrypoint_backends() -> None: + # importing here so it will pick up the mocked version in test_backends.py + from importlib.metadata import entry_points + + group_name = "torch_dynamo_backends" + eps = entry_points(group=group_name) + # pyrefly: ignore [bad-index] + eps_dict = {name: eps[name] for name in eps.names} + for backend_name in eps_dict: + _BACKENDS[backend_name] = eps_dict[backend_name] + + +def _is_registered_backend(compiler_fn: CompilerFn) -> bool: + """ + Check if the given compiler function is a registered backend. + Custom backends (user-provided callables not in the registry) return False. + """ + # Ensure backends are loaded + _lazy_import() + + # Check if it's directly a registered backend function + if compiler_fn in _COMPILER_FNS.values(): + return True + + # Check for _TorchCompileInductorWrapper or _TorchCompileWrapper + # These have a compiler_name attribute that identifies the backend + if hasattr(compiler_fn, "compiler_name"): + compiler_name = compiler_fn.compiler_name + if compiler_name in _BACKENDS or compiler_name in _COMPILER_FNS: + return True + + # Check if the wrapper has a compiler_fn attribute (e.g., _TorchCompileWrapper) + if hasattr(compiler_fn, "compiler_fn"): + return compiler_fn.compiler_fn in _COMPILER_FNS.values() + + return False diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/tensorrt.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/tensorrt.py new file mode 100644 index 0000000000000000000000000000000000000000..493e21a9dfc5fe929fdeefdf6153834d470ab561 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/tensorrt.py @@ -0,0 +1,12 @@ +# import torch # type: ignore[import] +# from .common import device_from_inputs, fake_tensor_unsupported # type: ignore[import] +# from .registry import register_backend # type: ignore[import] + +""" +Placeholder for TensorRT backend for dynamo via torch-tensorrt +""" + +# @register_backend +# def tensorrt(gm, example_inputs): +# import torch_tensorrt # type: ignore[import] +# pass diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/torchxla.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/torchxla.py new file mode 100644 index 0000000000000000000000000000000000000000..60d7b87bd0876a85702c07db7c82cd804ee608d1 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/torchxla.py @@ -0,0 +1,55 @@ +import logging +from collections.abc import Callable +from typing import Any + +import torch +from functorch.compile import make_boxed_func +from torch import fx + +from ..backends.common import aot_autograd +from .registry import CompiledFn, register_backend, register_experimental_backend + + +log = logging.getLogger(__name__) + + +@register_experimental_backend +def openxla_eval( + model: fx.GraphModule, fake_tensor_inputs: list[torch.Tensor] +) -> CompiledFn: + return xla_backend_helper(model, fake_tensor_inputs, boxed=False) + + +def openxla_eval_boxed( + model: fx.GraphModule, fake_tensor_inputs: list[torch.Tensor] +) -> Callable[..., Any]: + return xla_backend_helper(model, fake_tensor_inputs, boxed=True) + + +def xla_backend_helper( + model: fx.GraphModule, fake_tensor_inputs: list[torch.Tensor], boxed: bool = False +) -> Callable[..., Any]: + try: + import torch_xla.core.dynamo_bridge as bridge + except ImportError as e: + raise ImportError( + "Please follow the instruction in https://github.com/pytorch/xla#pytorchxla to install torch_xla" + ) from e + + compiled_graph = None + + def fwd(*args: torch.Tensor) -> Any: + nonlocal model + nonlocal compiled_graph + if compiled_graph is None: + compiled_graph = bridge.extract_compiled_graph(model, args) + del model + return compiled_graph(*args) + + return make_boxed_func(fwd) if boxed else fwd + + +openxla = aot_autograd( + fw_compiler=openxla_eval_boxed, +) +register_backend(name="openxla", compiler_fn=openxla) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/tvm.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/tvm.py new file mode 100644 index 0000000000000000000000000000000000000000..5aa20dd71f1fa3820300688739103f988ccc4875 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/backends/tvm.py @@ -0,0 +1,197 @@ +""" +This module provides TVM backend integration for TorchDynamo. + +Apache TVM is a deep learning compiler framework that can optimize and execute +models on various hardware backends. This module enables: + +- Compilation of PyTorch models to TVM's computation graphs +- Multiple scheduling options: + - Default scheduler + - Auto-scheduler for automatic optimization + - Meta-schedule for evolutionary search-based tuning +- Hardware-specific optimizations: + - CUDA GPU support + - CPU support with LLVM targeting and architecture-specific tuning + - Automatic detection of CPU capabilities (AVX2, AVX512) +- Tensor conversion utilities between PyTorch and TVM formats +- Configurable optimization levels and tuning trials + +The backend can be used with torch.compile(): + model = torch.compile(model, backend="tvm") +""" + +import functools +import importlib +import logging +import os +import sys +import tempfile +from collections.abc import Callable +from pathlib import Path +from types import MappingProxyType +from typing import Any + +import torch +from torch import fx + +from .common import device_from_inputs, fake_tensor_unsupported +from .registry import register_backend + + +log = logging.getLogger(__name__) + + +@register_backend +@fake_tensor_unsupported # type: ignore[arg-type] +def tvm( + gm: fx.GraphModule, + example_inputs: list[torch.Tensor], + *, + options: MappingProxyType[str, Any] | None = None, +) -> Callable[..., Any]: + if options is None: + options = MappingProxyType({"scheduler": None, "trials": 20000, "opt_level": 3}) + assert options is not None + import tvm # type: ignore[import] + from tvm import relay # type: ignore[import] + from tvm.contrib import graph_executor # type: ignore[import] + + jit_mod = torch.jit.trace(gm, example_inputs) + device = device_from_inputs(example_inputs) + shape_list = [(f"inp_{idx}", i.shape) for idx, i in enumerate(example_inputs)] + example_outputs = gm(*example_inputs) + if len(example_outputs) == 0: + log.warning("Explicitly fall back to eager due to zero output") + return gm.forward + mod, params = relay.frontend.from_pytorch(jit_mod, shape_list) + if device.type == "cuda": + dev = tvm.cuda(device.index) + target = tvm.target.cuda() + else: + dev = tvm.cpu(0) + target = tvm.target.Target(llvm_target()) + + scheduler = options.get("scheduler", None) + if scheduler is None: + scheduler = os.environ.get("TVM_SCHEDULER", None) + + trials = options.get("trials", 20000) + opt_level = options.get("opt_level", 3) + + if scheduler == "auto_scheduler": + # pyrefly: ignore [missing-import] + from tvm import auto_scheduler + + with ( + tempfile.NamedTemporaryFile() as log_file, + auto_scheduler.ApplyHistoryBest(log_file), + tvm.transform.PassContext( + opt_level=opt_level, config={"relay.backend.use_auto_scheduler": True} + ), + ): + lib = relay.build(mod, target=target, params=params) + elif scheduler == "meta_schedule": + # pyrefly: ignore [missing-import] + from tvm import meta_schedule as ms + + with tempfile.TemporaryDirectory() as work_dir: + if device.type != "cuda": + # meta_schedule needs num-cores to be specified + # here we use the maximum core count + target = tvm.target.Target( + f"{llvm_target()} --num-cores {ms.utils.cpu_count(logical=False)}" + ) + # TODO(shingjan): This could be replaced by tvm.contrib.torch.optimize_torch + # once USE_PT_TVMDSOOP is updated and turned on by default in TVM. + assert trials > 0 + database = ms.relay_integration.tune_relay( + mod=mod, + target=target, + work_dir=work_dir, + max_trials_global=trials, + num_trials_per_iter=64, + params=params, + strategy="evolutionary", + opt_level=opt_level, + ) + lib = ms.relay_integration.compile_relay( + database=database, + mod=mod, + target=target, + params=params, + opt_level=opt_level, + ) + elif scheduler == "default" or not scheduler: + # no autotuning + with tvm.transform.PassContext(opt_level=opt_level): + lib = relay.build(mod, target=target, params=params) + else: + raise NotImplementedError( + "This tuning option is invalid/not implemented for torchdynamo's TVM-related backend. " + "There are three available options: default, auto_scheduler and meta_schedule." + ) + m = graph_executor.GraphModule(lib["default"](dev)) + + def to_torch_tensor(nd_tensor: tvm.nd.array) -> torch.Tensor: + """A helper function to transfer a NDArray to torch.tensor.""" + if nd_tensor.dtype == "bool": + # DLPack does not support boolean so it can't be handled by + # torch.utils.dlpack.from_pack. Workaround by going through + # numpy, although this brings additional data copy overhead. + return torch.from_numpy(nd_tensor.numpy()) + return torch.utils.dlpack.from_dlpack(nd_tensor.to_dlpack()) + + def to_tvm_tensor(torch_tensor: torch.Tensor) -> tvm.nd.array: + """A helper function to transfer a torch.tensor to NDArray.""" + if torch_tensor.dtype == torch.bool: + # same reason as above, fallback to numpy conversion which + # could introduce data copy overhead + return tvm.nd.array(torch_tensor.cpu().numpy()) + return tvm.nd.from_dlpack(torch_tensor) + + def exec_tvm(*i_args: torch.Tensor) -> list[torch.Tensor]: + args = [a.contiguous() for a in i_args] + shape_info, _ = m.get_input_info() + active_inputs = {name for name, _ in shape_info.items()} + for idx, arg in enumerate(args, 0): + if arg.dim() != 0: + if arg.requires_grad: + arg = arg.detach() + inp_name = f"inp_{idx}" + if inp_name not in active_inputs: + log.warning( + "input %s skipped as not found in tvm's runtime library", + inp_name, + ) + continue + m.set_input( + inp_name, + to_tvm_tensor(arg), + ) + m.run() + return [to_torch_tensor(m.get_output(i)) for i in range(m.get_num_outputs())] + + return exec_tvm + + +tvm_meta_schedule = functools.partial(tvm, scheduler="meta_schedule") +tvm_auto_scheduler = functools.partial(tvm, scheduler="auto_scheduler") + + +def has_tvm() -> bool: + try: + importlib.import_module("tvm") + return True + except ImportError: + return False + + +@functools.cache +def llvm_target() -> str: + if sys.platform == "linux": + cpuinfo = Path("/proc/cpuinfo").read_text() + if "avx512" in cpuinfo: + return "llvm -mcpu=skylake-avx512" + elif "avx2" in cpuinfo: + return "llvm -mcpu=core-avx2" + return "llvm" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/bytecode_analysis.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/bytecode_analysis.py new file mode 100644 index 0000000000000000000000000000000000000000..d872dcc4cd106c1deb6bf6d10ac7e30b7122e3ac --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/bytecode_analysis.py @@ -0,0 +1,266 @@ +""" +This module provides utilities for analyzing and optimizing Python bytecode. +Key functionality includes: +- Dead code elimination +- Jump instruction optimization +- Stack size analysis and verification +- Live variable analysis +- Line number propagation and cleanup +- Exception table handling for Python 3.11+ + +The utilities in this module are used to analyze and transform bytecode +for better performance while maintaining correct semantics. +""" + +import bisect +import dataclasses +import dis +import itertools +import sys +from typing import Any, TYPE_CHECKING + + +if TYPE_CHECKING: + # TODO(lucaskabela): consider moving Instruction into this file + # and refactoring in callsite; that way we don't have to guard this import + from .bytecode_transformation import Instruction + +TERMINAL_OPCODES = { + dis.opmap["RETURN_VALUE"], + dis.opmap["JUMP_FORWARD"], + dis.opmap["RAISE_VARARGS"], + # TODO(jansel): double check exception handling +} +TERMINAL_OPCODES.add(dis.opmap["RERAISE"]) +if sys.version_info >= (3, 11): + TERMINAL_OPCODES.add(dis.opmap["JUMP_BACKWARD"]) + TERMINAL_OPCODES.add(dis.opmap["JUMP_FORWARD"]) +else: + TERMINAL_OPCODES.add(dis.opmap["JUMP_ABSOLUTE"]) + +if (3, 12) <= sys.version_info < (3, 14): + TERMINAL_OPCODES.add(dis.opmap["RETURN_CONST"]) +if sys.version_info >= (3, 13): + TERMINAL_OPCODES.add(dis.opmap["JUMP_BACKWARD_NO_INTERRUPT"]) +JUMP_OPCODES = set(dis.hasjrel + dis.hasjabs) +JUMP_OPNAMES = {dis.opname[opcode] for opcode in JUMP_OPCODES} +HASLOCAL = set(dis.haslocal) +HASFREE = set(dis.hasfree) + +stack_effect = dis.stack_effect + + +def get_indexof(insts: list["Instruction"]) -> dict["Instruction", int]: + """ + Get a mapping from instruction memory address to index in instruction list. + Additionally checks that each instruction only appears once in the list. + """ + # pyrefly: ignore [implicit-any] + indexof = {} + for i, inst in enumerate(insts): + assert inst not in indexof + indexof[inst] = i + return indexof + + +def remove_dead_code(instructions: list["Instruction"]) -> list["Instruction"]: + """Dead code elimination""" + indexof = get_indexof(instructions) + live_code = set() + + def find_live_code(start: int) -> None: + for i in range(start, len(instructions)): + if i in live_code: + return + live_code.add(i) + inst = instructions[i] + if inst.exn_tab_entry: + find_live_code(indexof[inst.exn_tab_entry.target]) + if inst.opcode in JUMP_OPCODES: + assert inst.target is not None + find_live_code(indexof[inst.target]) + if inst.opcode in TERMINAL_OPCODES: + return + + find_live_code(0) + + # change exception table entries if start/end instructions are dead + # assumes that exception table entries have been propagated, + # e.g. with bytecode_transformation.propagate_inst_exn_table_entries, + # and that instructions with an exn_tab_entry lies within its start/end. + if sys.version_info >= (3, 11): + live_idx = sorted(live_code) + for i, inst in enumerate(instructions): + if i in live_code and inst.exn_tab_entry: + # find leftmost live instruction >= start + start_idx = bisect.bisect_left( + live_idx, indexof[inst.exn_tab_entry.start] + ) + assert start_idx < len(live_idx) + # find rightmost live instruction <= end + end_idx = ( + bisect.bisect_right(live_idx, indexof[inst.exn_tab_entry.end]) - 1 + ) + assert end_idx >= 0 + assert live_idx[start_idx] <= i <= live_idx[end_idx] + inst.exn_tab_entry.start = instructions[live_idx[start_idx]] + inst.exn_tab_entry.end = instructions[live_idx[end_idx]] + + return [inst for i, inst in enumerate(instructions) if i in live_code] + + +def remove_pointless_jumps(instructions: list["Instruction"]) -> list["Instruction"]: + """Eliminate jumps to the next instruction""" + pointless_jumps = { + id(a) + for a, b in itertools.pairwise(instructions) + if a.opname == "JUMP_ABSOLUTE" and a.target is b + } + return [inst for inst in instructions if id(inst) not in pointless_jumps] + + +def propagate_line_nums(instructions: list["Instruction"]) -> None: + """Ensure every instruction has line number set in case some are removed""" + cur_line_no = None + + def populate_line_num(inst: "Instruction") -> None: + nonlocal cur_line_no + if inst.starts_line: + cur_line_no = inst.starts_line + + inst.starts_line = cur_line_no + + for inst in instructions: + populate_line_num(inst) + + +def remove_extra_line_nums(instructions: list["Instruction"]) -> None: + """Remove extra starts line properties before packing bytecode""" + + cur_line_no = None + + def remove_line_num(inst: "Instruction") -> None: + nonlocal cur_line_no + if inst.starts_line is None: + return + elif inst.starts_line == cur_line_no: + inst.starts_line = None + else: + cur_line_no = inst.starts_line + + for inst in instructions: + remove_line_num(inst) + + +@dataclasses.dataclass +class ReadsWrites: + reads: set[Any] + writes: set[Any] + visited: set[Any] + + +def livevars_analysis( + instructions: list["Instruction"], instruction: "Instruction" +) -> set[Any]: + indexof = get_indexof(instructions) + must = ReadsWrites(set(), set(), set()) + may = ReadsWrites(set(), set(), set()) + + def walk(state: ReadsWrites, start: int) -> None: + if start in state.visited: + return + state.visited.add(start) + + for i in range(start, len(instructions)): + inst = instructions[i] + if inst.opcode in HASLOCAL or inst.opcode in HASFREE: + if "LOAD" in inst.opname or "DELETE" in inst.opname: + if inst.argval not in must.writes: + state.reads.add(inst.argval) + elif "STORE" in inst.opname: + state.writes.add(inst.argval) + elif inst.opname == "MAKE_CELL": + pass + else: + raise NotImplementedError(f"unhandled {inst.opname}") + if inst.exn_tab_entry: + walk(may, indexof[inst.exn_tab_entry.target]) + if inst.opcode in JUMP_OPCODES: + assert inst.target is not None + walk(may, indexof[inst.target]) + state = may + if inst.opcode in TERMINAL_OPCODES: + return + + walk(must, indexof[instruction]) + return must.reads | may.reads + + +@dataclasses.dataclass +class FixedPointBox: + value: bool = True + + +@dataclasses.dataclass +class StackSize: + low: int | float + high: int | float + fixed_point: FixedPointBox + + def zero(self) -> None: + self.low = 0 + self.high = 0 + self.fixed_point.value = False + + def offset_of(self, other: "StackSize", n: int) -> None: + prior = (self.low, self.high) + self.low = min(self.low, other.low + n) + self.high = max(self.high, other.high + n) + if (self.low, self.high) != prior: + self.fixed_point.value = False + + def exn_tab_jump(self, depth: int) -> None: + prior = (self.low, self.high) + self.low = min(self.low, depth) + self.high = max(self.high, depth) + if (self.low, self.high) != prior: + self.fixed_point.value = False + + +def stacksize_analysis(instructions: list["Instruction"]) -> int | float: + assert instructions + fixed_point = FixedPointBox() + stack_sizes = { + inst: StackSize(float("inf"), float("-inf"), fixed_point) + for inst in instructions + } + stack_sizes[instructions[0]].zero() + + for _ in range(100): + if fixed_point.value: + break + fixed_point.value = True + + for inst, next_inst in zip(instructions, instructions[1:] + [None]): + stack_size = stack_sizes[inst] + if inst.opcode not in TERMINAL_OPCODES: + assert next_inst is not None, f"missing next inst: {inst}" + eff = stack_effect(inst.opcode, inst.arg, jump=False) + stack_sizes[next_inst].offset_of(stack_size, eff) + if inst.opcode in JUMP_OPCODES: + assert inst.target is not None, f"missing target: {inst}" + stack_sizes[inst.target].offset_of( + stack_size, stack_effect(inst.opcode, inst.arg, jump=True) + ) + if inst.exn_tab_entry: + # see https://github.com/python/cpython/blob/3.11/Objects/exception_handling_notes.txt + # on why depth is computed this way. + depth = inst.exn_tab_entry.depth + int(inst.exn_tab_entry.lasti) + 1 + stack_sizes[inst.exn_tab_entry.target].exn_tab_jump(depth) + + low = min(x.low for x in stack_sizes.values()) + high = max(x.high for x in stack_sizes.values()) + + assert fixed_point.value, "failed to reach fixed point" + assert low >= 0 + return high diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/bytecode_debugger.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/bytecode_debugger.py new file mode 100644 index 0000000000000000000000000000000000000000..42cf59b58de389b02bf1c6c1b79884886173c803 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/bytecode_debugger.py @@ -0,0 +1,1144 @@ +""" +Bytecode debugger for Dynamo-optimized code. + +This module provides a pdb-like debugger for stepping through Python bytecode +one instruction at a time, with the ability to inspect the value stack, +locals, and globals. + +Usage: + >>> # xdoctest: +SKIP + >>> import torch + >>> + >>> @torch.compile + >>> def my_fn(x): + ... return x + 1 + >>> + >>> with torch._dynamo.bytecode_debugger.debug(): + ... my_fn(torch.randn(3)) # Debugger activates on Dynamo-generated code + +Programmatic breakpoints (for Dynamo developers): + In PyCodegen, use create_breakpoint() to insert a debugger stop: + + from torch._dynamo.bytecode_transformation import create_breakpoint + codegen.extend_output(create_breakpoint()) +""" + +from __future__ import annotations + +import dis +import sys +import types +from contextlib import contextmanager +from dataclasses import dataclass, field +from typing import Any, cast, TYPE_CHECKING + + +if TYPE_CHECKING: + from collections.abc import Callable, Generator + + from typing_extensions import Self + +from .bytecode_transformation import convert_instruction, Instruction, instruction_size + + +# Python 3.12+ has sys.monitoring for efficient instruction-level tracing +_HAS_SYS_MONITORING = hasattr(sys, "monitoring") + + +# Sentinel for breakpoints inserted by PyCodegen via create_breakpoint() +class _BreakpointMarker: + """Sentinel constant that signals the debugger to break. + + Usage in PyCodegen: + from torch._dynamo.bytecode_transformation import create_breakpoint + codegen.extend_output(create_breakpoint()) + """ + + __slots__ = () + _instance: _BreakpointMarker | None = None + + def __new__(cls) -> Self: + if cls._instance is None: + cls._instance = cast("_BreakpointMarker", object.__new__(cls)) + return cls._instance + + def __repr__(self) -> str: + return "" + + +BREAKPOINT_MARKER = _BreakpointMarker() + + +# Import NULL_STACK_VALUE sentinel from C++ module +# This is returned by _get_frame_value_stack_with_depth for NULL stack slots +from torch._C._dynamo.eval_frame import NULL_STACK_VALUE # noqa: F401 + + +@dataclass +class CodeInfo: + """Per-code-object data shared across all frames executing the same code.""" + + code: types.CodeType + instructions: list[Instruction] + offset_to_inst: dict[int, Instruction] + offset_to_index: dict[int, int] + index_width: int + offset_width: int + breakpoints: set[int] = field(default_factory=set) + + +@dataclass +class DebuggerState: + """Per-frame debugging state.""" + + code_info: CodeInfo + frame: types.FrameType + current_offset: int = -1 # -1 indicates first instruction not yet seen + current_stack_depth: int = 0 # Tracked dynamically + first_instruction_seen: bool = False + step_mode: bool = True + step_count: int = 0 # Remaining steps for "s [n]" command (0 = prompt each time) + user_locals: dict[str, Any] = field( + default_factory=dict + ) # User-defined variables from debugger + last_command: str = "s" # Last command for repeat on empty input + list_index: int | None = ( + None # Current position for 'l' command (None = use current) + ) + + @property + def code(self) -> types.CodeType: + return self.code_info.code + + @property + def instructions(self) -> list[Instruction]: + return self.code_info.instructions + + @property + def offset_to_inst(self) -> dict[int, Instruction]: + return self.code_info.offset_to_inst + + @property + def offset_to_index(self) -> dict[int, int]: + return self.code_info.offset_to_index + + @property + def index_width(self) -> int: + return self.code_info.index_width + + @property + def offset_width(self) -> int: + return self.code_info.offset_width + + @property + def breakpoints(self) -> set[int]: + return self.code_info.breakpoints + + +class _DebugContext: + """Internal debug context that manages the debugging session.""" + + def __init__(self) -> None: + self._code_info: dict[types.CodeType, CodeInfo] = {} + self._frame_states: dict[types.FrameType, DebuggerState] = {} + self._active = False + self._tracked_codes: set[types.CodeType] = set() + self._old_trace: Callable[..., Any] | None = None + self._prev_callback: Callable[..., Any] | None = None + self._return_from_frame: types.FrameType | None = None + self._stop_after_return: bool = False + self._next_in_frame: types.FrameType | None = None + self._next_count: int = 0 + self._verbose: bool = False + self._stop_at_new_code: bool = True + self._quitting: bool = False + if _HAS_SYS_MONITORING: + self._tool_id = sys.monitoring.DEBUGGER_ID + + def get_instructions(self, code: types.CodeType | None = None) -> list[Instruction]: + """Get the list of instructions for a tracked code object. + + Args: + code: The code object to get instructions for. If None, returns + instructions from the most recently tracked code object. + + Returns: + List of instructions, or empty list if the code is not tracked. + """ + if not self._code_info: + return [] + if code is None: + info = next(reversed(self._code_info.values())) + else: + info = self._code_info.get(code) + if info is None: + return [] + return info.instructions + + def get_tracked_codes(self) -> list[types.CodeType]: + """Get all code objects that have been tracked by the debugger.""" + return list(self._code_info.keys()) + + @staticmethod + def is_programmatic_breakpoint(inst: Instruction) -> bool: + return inst.opname == "LOAD_CONST" and inst.argval is BREAKPOINT_MARKER + + def _get_or_create_code_info(self, code: types.CodeType) -> CodeInfo: + """Get or create CodeInfo for a code object.""" + if code not in self._code_info: + # Use dis.get_instructions directly to preserve original offsets. + # cleaned_instructions strips EXTENDED_ARG and recalculates offsets, + # which would cause mismatches with offsets reported by callbacks. + instructions = [convert_instruction(i) for i in dis.get_instructions(code)] + + # In 3.11+, instructions have inline cache entries that occupy + # bytecode space (e.g. BINARY_OP is 2 bytes + 2 bytes cache). + # dis.get_instructions only reports the instruction start offset, + # but sys.monitoring RAISE reports offsets within cache entries. + # Map the full byte range of each instruction so lookups succeed. + offset_to_inst: dict[int, Instruction] = {} + offset_to_index: dict[int, int] = {} + for i, inst in enumerate(instructions): + if inst.offset is not None: + inst_size = instruction_size(inst) + for off in range(inst.offset, inst.offset + inst_size, 2): + offset_to_inst[off] = inst + offset_to_index[off] = i + + max_index = len(instructions) - 1 if instructions else 0 + max_offset = max( + (inst.offset for inst in instructions if inst.offset is not None), + default=0, + ) + + # Pre-populate breakpoints from BREAKPOINT_MARKER instructions + programmatic_breakpoints: set[int] = { + i + for i, inst in enumerate(instructions) + if self.is_programmatic_breakpoint(inst) + } + + self._code_info[code] = CodeInfo( + code=code, + instructions=instructions, + offset_to_inst=offset_to_inst, + offset_to_index=offset_to_index, + index_width=max(1, len(str(max_index))), + offset_width=max(1, len(str(max_offset))), + breakpoints=programmatic_breakpoints, + ) + return self._code_info[code] + + def _get_or_create_frame_state( + self, code: types.CodeType, frame: types.FrameType + ) -> DebuggerState: + """Get or create per-frame DebuggerState.""" + if frame not in self._frame_states: + code_info = self._get_or_create_code_info(code) + self._frame_states[frame] = DebuggerState(code_info=code_info, frame=frame) + return self._frame_states[frame] + + def _find_frame_for_code(self, code: types.CodeType) -> types.FrameType | None: + """Walk the stack to find the frame executing the given code.""" + f: types.FrameType | None = sys._getframe() + while f is not None: + if f.f_code is code: + return f + f = f.f_back + return None + + def _build_tracked_frame_stack(self, state: DebuggerState) -> list[DebuggerState]: + """Build list of tracked ancestor frames, outermost-first. + + Walks f_back from the current execution frame to find all ancestor + frames whose code is tracked by the debugger. Returns the corresponding + DebuggerState objects with the execution state (state) last. + """ + ancestors: list[DebuggerState] = [] + f: types.FrameType | None = state.frame.f_back + while f is not None: + ancestor_state = self._frame_states.get(f) + if ancestor_state is not None: + ancestors.append(ancestor_state) + f = f.f_back + ancestors.reverse() + ancestors.append(state) + return ancestors + + def _format_instruction( + self, state: DebuggerState, offset: int, mark_current: bool = True + ) -> str: + """Format an instruction for display.""" + inst = state.offset_to_inst.get(offset) + iw, ow = state.index_width, state.offset_width + if inst is None: + return f" [{offset:{ow}d}]: " + + index = state.offset_to_index.get(offset, -1) + marker = ">>>" if mark_current and offset == state.current_offset else " " + bp_marker = ( + "*" if state.offset_to_index.get(offset, -1) in state.breakpoints else " " + ) + arg_str = f" {inst.argval}" if inst.arg is not None else "" + return f"{marker}{bp_marker} {index:{iw}d} [{offset:{ow}d}]: {inst.opname}{arg_str}" + + def _format_header(self, state: DebuggerState) -> str: + """Format the header line for instruction listings.""" + iw, ow = state.index_width, state.offset_width + return f" {'#':>{iw}} [{'offset':>{ow}}]" + + def _print_context( + self, state: DebuggerState, before: int = 3, after: int = 3 + ) -> None: + """Print instructions around the current position.""" + current_idx = state.offset_to_index.get(state.current_offset, 0) + start = max(0, current_idx - before) + end = min(len(state.instructions), current_idx + after + 1) + + print(f"\nInstruction {current_idx} at offset {state.current_offset}:") + print(self._format_header(state)) + for i in range(start, end): + inst = state.instructions[i] + if inst.offset is not None: + print(self._format_instruction(state, inst.offset)) + print() + + def _safe_stack_depth(self, state: DebuggerState, is_current_frame: bool) -> int: + """Compute a safe stack depth for reading frame values. + + For parent frames suspended at a CALL instruction, CPython has already + popped (and DECREF'd) the call arguments. The tracked depth still + includes those consumed entries, so reading them would dereference + dangling pointers. Adjust by the current instruction's stack_effect. + """ + depth = state.current_stack_depth + if not is_current_frame and state.current_offset >= 0: + inst = state.offset_to_inst.get(state.current_offset) + if inst is not None: + try: + effect = dis.stack_effect(inst.opcode, inst.arg) + if effect < 0: + depth = max(0, depth + effect) + except (ValueError, TypeError): + pass + return depth + + def _print_stack(self, state: DebuggerState, is_current_frame: bool = True) -> None: + """Print the current value stack.""" + from torch._C._dynamo.eval_frame import _get_frame_value_stack_with_depth + + depth = self._safe_stack_depth(state, is_current_frame) + try: + stack = _get_frame_value_stack_with_depth(state.frame, depth) + except Exception as e: + print(f"\nStack: (error reading stack: {e})") + return + print("\nStack (TOS at end):") + if not stack: + print(" (empty)") + else: + for i, value in enumerate(stack): + if value is NULL_STACK_VALUE: + addr = "0x0" + else: + addr = f"0x{id(value):x}" + print(f" [{i}] {addr} {value!r}") + print() + + def _print_locals(self, state: DebuggerState) -> None: + """Print local variables.""" + print("\nLocals:") + locals_dict = state.frame.f_locals + if not locals_dict: + print(" (none)") + else: + for name, value in locals_dict.items(): + print(f" {name} = {value!r}") + print() + + def _print_globals(self, state: DebuggerState, pattern: str | None = None) -> None: + """Print global variables.""" + print("\nGlobals:") + for name, value in state.frame.f_globals.items(): + if pattern and pattern not in name: + continue + if name.startswith("__") and name.endswith("__"): + continue + if isinstance(value, types.ModuleType): + continue + print(f" {name} = {value!r}") + print() + + def _disassemble(self, state: DebuggerState) -> None: + """Print the full disassembly.""" + print(f"\nDisassembly ({len(state.instructions)} instructions):") + print(self._format_header(state)) + for inst in state.instructions: + if inst.offset is not None: + print(self._format_instruction(state, inst.offset, mark_current=False)) + print() + + def _print_help(self) -> None: + """Print help message.""" + print("\nCommands:") + print(" s [n] - Step n instructions (default 1), stepping into calls") + print(" n [n] - Step n instructions (default 1), stepping over calls") + print(" c, cont - Continue until breakpoint or exception") + print(" r - Continue until current function's return instruction") + print( + " v, verbose - Toggle verbose mode (print each instruction before executing)" + ) + print(" Use with 'c' to find segfaults - last printed = culprit") + print(" u [count] - Move up the call stack (toward caller) for inspection") + print(" d [count] - Move down the call stack (toward callee)") + print(" bt, w - Print tracked frame backtrace") + print(" p - Print expression") + print(" l [first[, last]] - List instructions (like pdb)") + print(" l . - List around current instruction") + print(" ll - Disassemble all bytecode") + print(" stack - Print value stack") + print(" locals - Print local variables") + print(" globals - Print global variables") + print(" b - Set breakpoint at instruction n (see # column)") + print(" b - List breakpoints") + print(" cl - Clear breakpoint at instruction n") + print(" q, quit - Exit debugger") + print(" - Evaluate Python expression (like pdb)") + print() + print("Special variables: __stack__ (list of stack values, TOS at end)") + print("Note: Debugger stops on exceptions and shows the failing instruction.") + print() + + def _get_stack_for_eval( + self, state: DebuggerState, is_current_frame: bool = True + ) -> list[Any]: + """Get the current stack for use in expression evaluation.""" + from torch._C._dynamo.eval_frame import _get_frame_value_stack_with_depth + + depth = self._safe_stack_depth(state, is_current_frame) + try: + return _get_frame_value_stack_with_depth(state.frame, depth) + except Exception: + return [] + + def _build_eval_locals( + self, state: DebuggerState, is_current_frame: bool = True + ) -> dict[str, Any]: + """Build locals dict for expression evaluation, including user-defined variables.""" + eval_locals = dict(state.frame.f_locals) + eval_locals.update(state.user_locals) + eval_locals["__stack__"] = self._get_stack_for_eval(state, is_current_frame) + return eval_locals + + def _interactive_prompt(self, state: DebuggerState) -> None: + """Interactive prompt during debugging.""" + frame_stack = self._build_tracked_frame_stack(state) + view_index = len(frame_stack) - 1 + view_state = frame_stack[view_index] + + self._print_context(view_state) + + while True: + try: + cmd = input("(bdb) ").strip() + except (EOFError, KeyboardInterrupt): + print("\nExiting debugger.") + self._quitting = True + raise KeyboardInterrupt from None + + if not cmd: + cmd = state.last_command + else: + state.last_command = cmd + + parts = cmd.split(maxsplit=1) + action = parts[0].lower() + arg = parts[1] if len(parts) > 1 else "" + + if action in ("s", "step"): + state.step_mode = True + self._stop_at_new_code = True + # Parse optional count argument (e.g., "s 3" to step 3 times) + if arg: + try: + count = int(arg) + state.step_count = ( + count - 1 + ) # -1 because we're about to execute one + except ValueError: + print(f"Invalid count: {arg}") + continue + else: + state.step_count = 0 + return + + elif action in ("n", "next"): + self._next_in_frame = view_state.frame + if arg: + try: + count = int(arg) + self._next_count = count - 1 + except ValueError: + print(f"Invalid count: {arg}") + continue + else: + self._next_count = 0 + state.step_mode = False + self._stop_at_new_code = False + return + + elif action in ("c", "cont", "continue"): + state.step_mode = False + self._stop_at_new_code = False + self._stop_after_return = False + return + + elif action in ("r", "return"): + self._return_from_frame = state.frame + state.step_mode = False + self._stop_at_new_code = False + return + + elif action in ("u", "up"): + count = 1 + if arg: + try: + count = int(arg) + except ValueError: + print(f"Invalid count: {arg}") + continue + new_index = view_index - count + if new_index < 0: + new_index = 0 + print("Oldest tracked frame") + view_index = new_index + view_state = frame_stack[view_index] + print(f"> {view_state.code.co_name}") + self._print_context(view_state) + + elif action in ("d", "down"): + count = 1 + if arg: + try: + count = int(arg) + except ValueError: + print(f"Invalid count: {arg}") + continue + max_index = len(frame_stack) - 1 + new_index = view_index + count + if new_index > max_index: + new_index = max_index + print("Newest tracked frame") + view_index = new_index + view_state = frame_stack[view_index] + print(f"> {view_state.code.co_name}") + self._print_context(view_state) + + elif action in ("bt", "w", "where"): + for i, fs in enumerate(frame_stack): + marker = ">" if i == view_index else " " + idx = fs.offset_to_index.get(fs.current_offset, -1) + inst = fs.offset_to_inst.get(fs.current_offset) + inst_str = inst.opname if inst else "" + print( + f" {marker} [{i}] {fs.code.co_name} instruction {idx} ({inst_str})" + ) + + elif action in ("v", "verbose"): + self._verbose = not self._verbose + status = "enabled" if self._verbose else "disabled" + print(f"Verbose mode {status}.") + + elif action in ("l", "list"): + # Like pdb: + # - 'l' continues from last position (or centers on current if first time) + # - 'l .' centers on current instruction + # - 'l N' lists 11 instructions starting at N + # - 'l first, last' lists range (if last < first, last is a count) + num_lines = 11 # pdb default + start: int | None = None + end: int | None = None + + if arg == ".": + # Reset to current instruction + view_state.list_index = None + elif arg: + # Check for range syntax: "first, last" or "first,last" + if "," in arg: + parts = arg.split(",", 1) + try: + first = int(parts[0].strip()) + second = int(parts[1].strip()) + if second < first: + # second is a count + start = first + end = first + second + else: + start = first + end = second + 1 # inclusive + except ValueError: + print(f"Invalid range: {arg}") + continue + else: + # Single number: start at that instruction + try: + start = int(arg) + end = start + num_lines + except ValueError: + print(f"Invalid argument: {arg}") + continue + + if start is None: + if view_state.list_index is None: + # Center on current instruction + current_idx = view_state.offset_to_index.get( + view_state.current_offset, 0 + ) + start = max(0, current_idx - num_lines // 2) + else: + start = view_state.list_index + end = start + num_lines + + assert end is not None + end = min(len(view_state.instructions), end) + if start >= len(view_state.instructions): + print("(End of bytecode)") + else: + print(self._format_header(view_state)) + for i in range(start, end): + inst = view_state.instructions[i] + if inst.offset is not None: + print(self._format_instruction(view_state, inst.offset)) + print() + view_state.list_index = end + + elif action == "ll": + self._disassemble(view_state) + + elif action == "locals": + self._print_locals(view_state) + + elif action == "globals": + self._print_globals(view_state, arg if arg else None) + + elif action == "stack": + is_current = view_state is frame_stack[-1] + self._print_stack(view_state, is_current_frame=is_current) + + elif action == "b": + if arg: + try: + index = int(arg) + num_instructions = len(view_state.instructions) + if index < 0 or index >= num_instructions: + print( + f"Invalid instruction number: {index} " + f"(must be 0-{num_instructions - 1})" + ) + else: + view_state.breakpoints.add(index) + print(f"Breakpoint set at instruction {index}") + except ValueError: + print(f"Invalid instruction number: {arg}") + else: + print("\nBreakpoints:") + if not view_state.breakpoints: + print(" (none)") + else: + for bp_index in sorted(view_state.breakpoints): + if bp_index < len(view_state.instructions): + inst = view_state.instructions[bp_index] + if inst.offset is not None: + print( + f" {self._format_instruction(view_state, inst.offset, mark_current=False)}" + ) + print() + + elif action == "cl": + if arg: + try: + index = int(arg) + num_instructions = len(view_state.instructions) + if index < 0 or index >= num_instructions: + print( + f"Invalid instruction number: {index} " + f"(must be 0-{num_instructions - 1})" + ) + elif index in view_state.breakpoints: + view_state.breakpoints.discard(index) + print(f"Breakpoint cleared at instruction {index}") + else: + print(f"No breakpoint at instruction {index}") + except ValueError: + print(f"Invalid instruction number: {arg}") + + elif action == "p": + if arg: + try: + is_current = view_state is frame_stack[-1] + frame_globals = view_state.frame.f_globals + eval_locals = self._build_eval_locals( + view_state, is_current_frame=is_current + ) + result = eval(arg, frame_globals, eval_locals) + print(f"{arg} = {result!r}") + except Exception as e: + print(f"Error evaluating '{arg}': {e}") + else: + print("Usage: p ") + + elif action in ("h", "help", "?"): + self._print_help() + + elif action in ("q", "quit", "exit"): + print("Exiting debugger.") + self._quitting = True + raise KeyboardInterrupt + + else: + # Try to execute as Python code (like pdb) + is_current = view_state is frame_stack[-1] + frame_globals = view_state.frame.f_globals + eval_locals = self._build_eval_locals( + view_state, is_current_frame=is_current + ) + + try: + # First try as expression (eval) + result = eval(cmd, frame_globals, eval_locals) + if result is not None: + print(repr(result)) + except SyntaxError: + # If not a valid expression, try as statement (exec) + try: + keys_before = set(eval_locals.keys()) + ids_before = {k: id(v) for k, v in eval_locals.items()} + exec(cmd, frame_globals, eval_locals) + # Capture any new or modified variables into user_locals + for key in eval_locals: + is_new = key not in keys_before + is_user_var = key in view_state.user_locals + is_modified = id(eval_locals[key]) != ids_before.get(key) + if ( + is_new or is_user_var or is_modified + ) and key != "__stack__": + view_state.user_locals[key] = eval_locals[key] + except Exception as e: + print(f"*** {type(e).__name__}: {e}") + except Exception as e: + print(f"*** {type(e).__name__}: {e}") + + def _handle_instruction( + self, code: types.CodeType, offset: int, frame: types.FrameType | None = None + ) -> None: + """Common instruction handling logic for both tracing backends.""" + if frame is None: + frame = self._find_frame_for_code(code) + assert frame is not None + state = self._get_or_create_frame_state(code, frame) + + # Update stack depth based on the previous instruction's effect. + # The callback is called BEFORE instruction at 'offset' executes, + # so we need to apply the effect of the previous instruction first. + previous_offset = state.current_offset + if previous_offset >= 0: + prev_inst = state.offset_to_inst.get(previous_offset) + if prev_inst is not None: + # Detect if a jump was taken: current offset != sequential next + prev_index = state.offset_to_index.get(previous_offset, -1) + expected_next_index = prev_index + 1 + current_index = state.offset_to_index.get(offset, -1) + did_jump = current_index != expected_next_index + + try: + effect = dis.stack_effect( + prev_inst.opcode, prev_inst.arg, jump=did_jump + ) + state.current_stack_depth += effect + if state.current_stack_depth < 0: + raise RuntimeError( + f"Stack depth went negative after {prev_inst.opname} " + f"at offset {previous_offset}: depth={state.current_stack_depth}" + ) + except (ValueError, TypeError): + pass + + state.current_offset = offset + state.list_index = None # Reset list position when stepping + + # After 'r' command completes, resume stepping at the next instruction + if self._stop_after_return: + self._stop_after_return = False + state.step_mode = True + state.step_count = 0 + self._stop_at_new_code = True + + # After 'n', resume stepping when we reach that frame + if self._next_in_frame is not None and frame is self._next_in_frame: + if self._next_count > 0: + # More steps remaining — re-arm for the next instruction in this frame + self._next_count -= 1 + else: + self._next_in_frame = None + state.step_mode = True + state.step_count = 0 + self._stop_at_new_code = True + if _HAS_SYS_MONITORING: + sys.monitoring.restart_events() + + # First instruction - print header and enter step mode (unless suppressed) + if not state.first_instruction_seen: + state.first_instruction_seen = True + if not self._stop_at_new_code: + state.step_mode = False + if state.step_mode: + print(f"\n=== Entering Dynamo-generated code: {code.co_name} ===") + self._print_help() + elif self._verbose: + print(f"\n=== Entering Dynamo-generated code: {code.co_name} ===") + + # Verbose mode: print each instruction before executing (for segfault debugging) + if self._verbose: + inst = state.offset_to_inst.get(offset) + if inst: + idx = state.offset_to_index.get(offset, -1) + arg_str = f" {inst.argval}" if inst.arg is not None else "" + print(f"Running [{idx}] {inst.opname}{arg_str}", flush=True) + + inst = state.offset_to_inst.get(offset) + + # Check if current instruction has a breakpoint (by index) + current_index = state.offset_to_index.get(offset, -1) + hit_breakpoint = current_index in state.breakpoints + + # 'r' command: stop at RETURN_VALUE/RETURN_CONST in the target frame + # so the user can inspect the return value on the stack before returning. + hit_return_target = False + if self._return_from_frame is not None and frame is self._return_from_frame: + if inst is not None and inst.opname in ("RETURN_VALUE", "RETURN_CONST"): + self._return_from_frame = None + # After stepping past the return, stop in the caller + self._stop_after_return = True + hit_return_target = True + + # Check if we should stop + # If step_count > 0, we're in the middle of "N s" and should continue + if state.step_count > 0: + state.step_count -= 1 + # But still stop for breakpoints and return targets + if hit_breakpoint or hit_return_target: + state.step_count = 0 # Cancel remaining steps + else: + return # Continue without prompting + + should_stop = state.step_mode or hit_breakpoint or hit_return_target + if should_stop: + if hit_breakpoint: + if inst is not None and self.is_programmatic_breakpoint(inst): + print("Breakpoint hit (programmatic)") + else: + print(f"Breakpoint hit at instruction {current_index}") + elif hit_return_target: + print(f"About to return from {code.co_name}") + self._interactive_prompt(state) + + def _handle_return( + self, code: types.CodeType, retval: object, frame: types.FrameType | None = None + ) -> None: + """Common return handling logic.""" + if self._quitting: + return + print(f"\n=== {code.co_name} returned: {retval!r} ===") + + # For sys.monitoring, we don't get the frame directly, but since + # the frame that's currently returning is the one whose PY_RETURN just + # fired, code identity is sufficient to match. + if self._return_from_frame is not None and ( + frame is self._return_from_frame + or (frame is None and code is self._return_from_frame.f_code) + ): + self._return_from_frame = None + self._stop_after_return = True + if _HAS_SYS_MONITORING: + sys.monitoring.restart_events() + if self._next_in_frame is not None and ( + frame is self._next_in_frame + or (frame is None and code is self._next_in_frame.f_code) + ): + self._next_in_frame = None + self._stop_after_return = True + if _HAS_SYS_MONITORING: + sys.monitoring.restart_events() + + # Clean up frame state to avoid holding dead frame references + if frame is not None: + self._frame_states.pop(frame, None) + else: + # sys.monitoring path: find and remove matching frame state by code + for f, s in list(self._frame_states.items()): + if s.code is code: + self._frame_states.pop(f, None) + break + + def _handle_exception( + self, code: types.CodeType, offset: int, exception: BaseException + ) -> None: + """Common exception handling logic.""" + if self._quitting: + return + frame = self._find_frame_for_code(code) + if frame is None: + return + state = self._frame_states.get(frame) + if state is None: + return + + # If we're waiting for this frame to return, the return was interrupted + if self._return_from_frame is not None and ( + frame is self._return_from_frame or code is self._return_from_frame.f_code + ): + self._return_from_frame = None + + # If we're waiting for next-in-frame, the execution was interrupted + if self._next_in_frame is not None and ( + frame is self._next_in_frame or code is self._next_in_frame.f_code + ): + self._next_in_frame = None + self._stop_at_new_code = True + + state.current_offset = offset + + inst = state.offset_to_inst.get(offset) + inst_str = inst.opname if inst else "" + current_index = state.offset_to_index.get(offset, -1) + + print(f"\n=== Exception raised at instruction {current_index}: {inst_str} ===") + print(f"=== {type(exception).__name__}: {exception} ===") + self._interactive_prompt(state) + + # ========================================================================= + # Python 3.12+ implementation using sys.monitoring + # ========================================================================= + + def _dynamo_code_callback(self, code: types.CodeType) -> None: + """Called by C++ before executing Dynamo-generated code.""" + if code in self._tracked_codes: + return + self._tracked_codes.add(code) + + if _HAS_SYS_MONITORING: + # Enable INSTRUCTION and PY_RETURN events for this specific code object + # RAISE must be a global event (cannot be set locally) + sys.monitoring.set_local_events( + self._tool_id, + code, + sys.monitoring.events.INSTRUCTION | sys.monitoring.events.PY_RETURN, + ) + # For settrace, we enable opcode tracing when we see the frame + + # Pre-create code info for this code + self._get_or_create_code_info(code) + + def _monitoring_return_callback( + self, code: types.CodeType, instruction_offset: int, retval: object + ) -> None: + """Callback for PY_RETURN events (sys.monitoring).""" + self._handle_return(code, retval) + + def _monitoring_instruction_callback( + self, code: types.CodeType, offset: int + ) -> object: + """Callback for INSTRUCTION events (sys.monitoring).""" + self._handle_instruction(code, offset) + return sys.monitoring.DISABLE + + def _monitoring_raise_callback( + self, code: types.CodeType, offset: int, exception: BaseException + ) -> object: + """Callback for RAISE events (sys.monitoring).""" + # Only handle exceptions from tracked Dynamo-generated code + if code not in self._tracked_codes: + return None + self._handle_exception(code, offset, exception) + # Cannot return DISABLE for global events like RAISE + return None + + # ========================================================================= + # Python 3.11 and below implementation using sys.settrace + # ========================================================================= + + def _settrace_callback( + self, frame: types.FrameType, event: str, arg: Any + ) -> Callable[..., Any] | None: + """Trace function for sys.settrace.""" + code = frame.f_code + + # Only trace Dynamo-generated code + if code not in self._tracked_codes: + return self._settrace_callback + + if event == "call": + # Enable opcode tracing for this frame + frame.f_trace_opcodes = True + return self._settrace_callback + + elif event == "opcode": + # Get the current instruction offset + offset = frame.f_lasti + self._handle_instruction(code, offset, frame) + return self._settrace_callback + + elif event == "return": + self._handle_return(code, arg, frame) + return self._settrace_callback + + elif event == "exception": + # arg is (exception_type, exception_value, traceback) + offset = frame.f_lasti + exc_type, exc_value, exc_tb = arg + self._handle_exception(code, offset, exc_value) + return self._settrace_callback + + return self._settrace_callback + + # ========================================================================= + # Context manager implementation + # ========================================================================= + + def __enter__(self) -> Self: + """Start the debug context.""" + from torch._C._dynamo.eval_frame import ( + get_bytecode_debugger_callback, + set_bytecode_debugger_callback, + ) + + self._active = True + self._prev_callback = get_bytecode_debugger_callback() + self._code_info.clear() + self._frame_states.clear() + self._tracked_codes.clear() + + if _HAS_SYS_MONITORING: + # Python 3.12+: Use sys.monitoring + try: + sys.monitoring.use_tool_id(self._tool_id, "bytecode_debugger") + except ValueError: + pass + + # Register callbacks (events enabled per-code via set_local_events) + sys.monitoring.register_callback( + self._tool_id, + sys.monitoring.events.INSTRUCTION, + self._monitoring_instruction_callback, + ) + sys.monitoring.register_callback( + self._tool_id, + sys.monitoring.events.PY_RETURN, + self._monitoring_return_callback, + ) + sys.monitoring.register_callback( + self._tool_id, + sys.monitoring.events.RAISE, + self._monitoring_raise_callback, + ) + # RAISE must be a global event (cannot be local) + sys.monitoring.set_events(self._tool_id, sys.monitoring.events.RAISE) + else: + # Python 3.11 and below: Use sys.settrace + self._old_trace = sys.gettrace() + sys.settrace(self._settrace_callback) + + # Set the C callback that will be called before executing Dynamo code + set_bytecode_debugger_callback(self._dynamo_code_callback) + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: types.TracebackType | None, + ) -> bool: + """End the debug context.""" + if not self._active: + return False + from torch._C._dynamo.eval_frame import set_bytecode_debugger_callback + + self._active = False + prev = self._prev_callback + set_bytecode_debugger_callback(prev) + + if _HAS_SYS_MONITORING: + if prev is None: + sys.monitoring.set_events(self._tool_id, 0) + try: + sys.monitoring.free_tool_id(self._tool_id) + except ValueError: + pass + else: + # Restore outer context's monitoring callbacks and events + assert isinstance(prev, types.MethodType) + outer = cast("_DebugContext", prev.__self__) + sys.monitoring.register_callback( + self._tool_id, + sys.monitoring.events.INSTRUCTION, + outer._monitoring_instruction_callback, + ) + sys.monitoring.register_callback( + self._tool_id, + sys.monitoring.events.PY_RETURN, + outer._monitoring_return_callback, + ) + sys.monitoring.register_callback( + self._tool_id, + sys.monitoring.events.RAISE, + outer._monitoring_raise_callback, + ) + sys.monitoring.set_events(self._tool_id, sys.monitoring.events.RAISE) + for code in outer._tracked_codes: + sys.monitoring.set_local_events( + self._tool_id, + code, + sys.monitoring.events.INSTRUCTION + | sys.monitoring.events.PY_RETURN, + ) + else: + # Python 3.11 and below: Restore old trace + sys.settrace(self._old_trace) + + return False # Don't suppress exceptions + + +@contextmanager +def debug() -> Generator[_DebugContext, None, None]: + """ + Context manager for debugging Dynamo-generated bytecode. + + Any Dynamo-generated code executed within this context will trigger + the interactive bytecode debugger. + + Example: + >>> # xdoctest: +SKIP + >>> import torch + >>> + >>> @torch.compile + >>> def my_fn(x): + ... return x + 1 + >>> + >>> with torch._dynamo.bytecode_debugger.debug(): + ... my_fn(torch.randn(3)) + """ + ctx = _DebugContext() + try: + with ctx: + yield ctx + except KeyboardInterrupt: + print("\n=== Debug session ended ===") + + +def breakpoint() -> None: + """Programmatic breakpoint for user code. + + Place this in code compiled by Dynamo. During tracing, Dynamo inserts a + BREAKPOINT_MARKER into the compiled bytecode. When the bytecode debugger + is active, execution pauses at this marker. + """ diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/bytecode_transformation.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/bytecode_transformation.py new file mode 100644 index 0000000000000000000000000000000000000000..b1c5551f106a91c8c86cb1cfb0f6f78d82da66d1 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/bytecode_transformation.py @@ -0,0 +1,1873 @@ +""" +This module provides utilities for analyzing, transforming and manipulating Python bytecode. +It includes functionality for: +- Converting between different bytecode formats and versions +- Virtualizing jumps and managing jump targets +- Handling exception tables and their entries +- Managing instruction offsets and extended arguments +- Providing a clean API for bytecode modification and transformation +- Supporting Python version-specific bytecode features +- Generating bytecode from template functions + +The module is designed to work across different Python versions (3.7+) and handles +version-specific bytecode differences transparently. +""" + +import copy +import dataclasses +import dis +import functools +import itertools +import sys +import types +import uuid +from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence +from typing import Any, cast, Optional, TYPE_CHECKING + +from . import config +from .bytecode_analysis import ( + get_indexof, + propagate_line_nums, + remove_extra_line_nums, + stacksize_analysis, +) +from .utils import is_safe_constant + + +if TYPE_CHECKING: + from .output_graph import DynamoTracerOutput + + +@dataclasses.dataclass(slots=True) +class InstructionExnTabEntry: + start: "Instruction" + end: "Instruction" + target: "Instruction" + depth: int + lasti: bool + + def __repr__(self) -> str: + return ( + f"InstructionExnTabEntry(start={self.start.short_inst_repr()}, " + f"end={self.end.short_inst_repr()}, " + f"target={self.target.short_inst_repr()}, " + f"depth={self.depth}, lasti={self.lasti})" + ) + + def __eq__(self, o: object) -> bool: + if not isinstance(o, InstructionExnTabEntry): + return False + return ( + self.start is o.start + and self.end is o.end + and self.target is o.target + and self.depth == o.depth + and self.lasti == o.lasti + ) + + +@dataclasses.dataclass(slots=True) +class Instruction: + """A mutable version of dis.Instruction""" + + opcode: int + opname: str + arg: int | None + argval: Any + offset: int | None = None + starts_line: int | None = None + is_jump_target: bool = False + + positions: Optional["dis.Positions"] = None + # extra fields to make modification easier: + target: Optional["Instruction"] = None + exn_tab_entry: InstructionExnTabEntry | None = None + argrepr: str | None = None + + def __hash__(self) -> int: + return id(self) + + def __eq__(self, other: object) -> bool: + return id(self) == id(other) + + def short_inst_repr(self) -> str: + return f"Instruction(opname={self.opname}, offset={self.offset})" + + def copy_positions(self, other: "Instruction") -> None: + self.starts_line = other.starts_line + self.positions = other.positions + + +if sys.version_info >= (3, 13): + + def convert_instruction(i: dis.Instruction) -> Instruction: + return Instruction( + i.opcode, + i.opname, + i.arg, + i.argval, + i.offset, + i.line_number, + i.is_jump_target, + i.positions, + ) + +elif sys.version_info >= (3, 11): + + def convert_instruction(i: dis.Instruction) -> Instruction: + return Instruction( + i.opcode, + i.opname, + i.arg, + i.argval, + i.offset, + i.starts_line, + i.is_jump_target, + i.positions, + ) + +else: + + def convert_instruction(i: dis.Instruction) -> Instruction: + return Instruction( + i.opcode, + i.opname, + i.arg, + i.argval, + i.offset, + i.starts_line, + i.is_jump_target, + None, + ) + + +class _NotProvided: + def __repr__(self) -> str: + return "_NotProvided" + + +if sys.version_info >= (3, 12): + + def inst_has_op_bits(name: str) -> bool: + return name in ("LOAD_ATTR", "LOAD_GLOBAL", "LOAD_SUPER_ATTR") + +elif sys.version_info >= (3, 11): + + def inst_has_op_bits(name: str) -> bool: + return name == "LOAD_GLOBAL" + +else: + + def inst_has_op_bits(name: str) -> bool: + return False + + +def create_instruction( + name: str, + *, + arg: int | None = None, + argval: Any | None = _NotProvided, + target: Instruction | None = None, +) -> Instruction: + """ + At most one of `arg`, `argval`, and `target` can be not None/_NotProvided. + This is to prevent ambiguity, e.g. does + create_instruction("LOAD_CONST", 5) + mean load the constant at co_consts[5], or load the constant 5? + + If `arg` is not provided, it will be computed during assembly from + `argval` or `target`. + + Bits in the args of instructions LOAD_GLOBAL, LOAD_ATTR (3.12+), and LOAD_SUPER_ATTR + modify the behavior of the instruction. In this case, we allow both `arg` + and `argval` to be set. The value of `arg` here is expected to be the value of + the op bits and the true value of `arg` will be computed during assembly. + If `arg` is not set, the bits are assumed to be 0. + """ + + # allow for instructions with op bits to have both arg and argval specified + if inst_has_op_bits(name): + if target is not None: + raise RuntimeError("target cannot be specified for instruction") + if arg is None: + arg = 0 + else: + cnt = (arg is not None) + (argval is not _NotProvided) + (target is not None) + if cnt > 1: + raise RuntimeError( + "only one of arg, argval, and target can be not None/_NotProvided" + ) + if arg is not None and not isinstance(arg, int): + raise RuntimeError("instruction arg must be int or None") + return Instruction( + opcode=dis.opmap[name], opname=name, arg=arg, argval=argval, target=target + ) + + +# Python 3.11 remaps +def create_jump_absolute(target: Instruction) -> Instruction: + inst = "JUMP_FORWARD" if sys.version_info >= (3, 11) else "JUMP_ABSOLUTE" + return create_instruction(inst, target=target) + + +def is_jump_absolute(target: Instruction) -> bool: + return target.opname in ("JUMP_FORWARD", "JUMP_ABSOLUTE") + + +def create_load_const(val: Any, checked: bool = True) -> Instruction: + """ + In general we should only create `LOAD_CONST` for immutable objects, but + sometimes it's convenient _and safe_ for Dynamo create `LOAD_CONST` for + mutable objects. In such cases, use `checked=False`. + """ + if checked: + assert is_safe_constant(val), f"unsafe constant {val}" + return create_instruction("LOAD_CONST", argval=val) + + +def create_breakpoint() -> list[Instruction]: + """ + Create instructions that trigger the bytecode debugger to stop. + + Usage: + codegen.extend_output(create_breakpoint()) + + When the bytecode debugger is active, execution will pause at this point. + At runtime, these instructions have no effect on program state. + """ + from .bytecode_debugger import BREAKPOINT_MARKER + + return [ + create_instruction("LOAD_CONST", argval=BREAKPOINT_MARKER), + create_instruction("POP_TOP"), + ] + + +def create_dup_top() -> Instruction: + if sys.version_info >= (3, 11): + return create_instruction("COPY", arg=1) + return create_instruction("DUP_TOP") + + +def create_rot_n(n: int) -> list[Instruction]: + """ + Returns a "simple" sequence of instructions that rotates TOS to the n-th + position in the stack. For Python < 3.11, returns a single ROT_* + instruction. If no such instruction exists, an error is raised and the + caller is expected to generate an equivalent sequence of instructions. + For Python >= 3.11, any rotation can be expressed as a simple sequence of + swaps. + """ + if n <= 1: + # don't rotate + return [] + + if sys.version_info >= (3, 11): + # rotate can be expressed as a sequence of swap operations + # e.g. rotate 3 is equivalent to swap 3, swap 2 + return [create_instruction("SWAP", arg=i) for i in range(n, 1, -1)] + + if n <= 4: + return [create_instruction("ROT_" + ["TWO", "THREE", "FOUR"][n - 2])] + return [create_instruction("ROT_N", arg=n)] + + +def add_push_null( + inst_or_insts: Instruction | list[Instruction], +) -> list[Instruction]: + """ + Appends or prepends a PUSH_NULL instruction to `inst_or_insts`, + depending on Python version. Used when you know that + `inst_or_insts` generates a callable that will be called. + + NOTE: Assumes `inst_or_insts` is a single instruction or sequence of + instructions that pushes exactly 1 object to the stack that is to + be called. It is important that you include ALL instructions that + construct the callable - not just the first instruction/a prefix. + + Will attempt to use the NULL push bit for instructions + with such bits (LOAD_GLOBAL 3.11+, LOAD_ATTR 3.12+, LOAD_SUPER_ATTR). + In this case, instructions WILL be modified. + """ + if isinstance(inst_or_insts, Instruction): + insts: list[Instruction] = [inst_or_insts] + else: + assert isinstance(inst_or_insts, list) + insts = inst_or_insts + + def inst_has_bit_set(idx: int) -> bool: + assert insts[idx].arg is not None + return insts[idx].arg & 1 == 1 # type: ignore[operator] + + def set_inst_bit(idx: int) -> None: + assert insts[idx].arg is not None + insts[idx].arg |= 1 # type: ignore[operator] + + if sys.version_info >= (3, 13): + # In 3.13, NULL follows the callable + if inst_has_op_bits(insts[-1].opname) and not inst_has_bit_set(-1): + # All insts with op bits have the push_null bit as the last one. + # Only set the bit if it hasn't been set - otherwise, we need + # to add another PUSH_NULL. + set_inst_bit(-1) + else: + insts = insts + [create_instruction("PUSH_NULL")] + elif sys.version_info >= (3, 12): + # LOAD_ATTR/LOAD_SUPER_ATTR at the end + # We assume that `insts` will only load 1 object, so + # LOAD_GLOBAL at the end doesn't need to be checked + if inst_has_op_bits(insts[-1].opname) and not inst_has_bit_set(-1): + set_inst_bit(-1) + elif insts[0].opname == "LOAD_GLOBAL" and not inst_has_bit_set(0): + set_inst_bit(0) + else: + insts = [create_instruction("PUSH_NULL")] + insts + elif sys.version_info >= (3, 11): + # 3.11 introduced NULL preceding callable + if inst_has_op_bits(insts[0].opname) and not inst_has_bit_set(0): + set_inst_bit(0) + else: + insts = [create_instruction("PUSH_NULL")] + insts + return insts + + +def add_push_null_call_function_ex( + inst_or_insts: Instruction | list[Instruction], +) -> list[Instruction]: + """Like add_push_null, but the low bit of LOAD_ATTR/LOAD_SUPER_ATTR + is not set, due to an expected CALL_FUNCTION_EX instruction. + """ + if isinstance(inst_or_insts, Instruction): + insts: list[Instruction] = [inst_or_insts] + else: + assert isinstance(inst_or_insts, list) + insts = inst_or_insts + + if sys.version_info < (3, 11): + return insts + + idx = -1 if sys.version_info >= (3, 13) else 0 + if insts[idx].opname == "LOAD_GLOBAL": + assert insts[idx].arg is not None + if insts[idx].arg & 1 == 0: # type: ignore[operator] + insts[idx].arg |= 1 # type: ignore[operator] + return insts + + if sys.version_info >= (3, 13): + insts = insts + [create_instruction("PUSH_NULL")] + else: + insts = [create_instruction("PUSH_NULL")] + insts + + return insts + + +def create_call_function(nargs: int, push_null: bool) -> list[Instruction]: + """ + Creates a sequence of instructions that makes a function call. + + `push_null` is used in Python 3.11+ only. It is used in codegen when + a function call is intended to be made with the NULL + fn convention, + and we know that the NULL has not been pushed yet. We will push a + NULL and rotate it to the correct position immediately before making + the function call. + + `push_null` should be True if no NULL is pushed for the callable. + Conversely, `push_null` should be False if a NULL was pushed for the callable. + Prefer using `push_null=False` when possible since we will not need to rotate + NULL to the right place, which is less efficient. + + Generally, you should codegen a function by using `add_push_null` then + `create_call_function` with `push_null=False`. + + Example of when to set push_null False: + + insts = [ + create_instruction("LOAD_GLOBAL", argval="torch"), + create_instruction("LOAD_ATTR", argval="nn"), + create_instruction("LOAD_ATTR", argval="functional"), + create_instruction("LOAD_ATTR", argval="relu"), + ] + insts = add_push_null(insts) + insts.append(create_instruction("LOAD_FAST", argval="x")) + insts.extend(create_call_function(1, False)) + + Example of when to set push_null True: + + insts = [create_instruction("LOAD_FAST", x)] + for should_wrap, wrapper_name in wrappers: + if should_wrap: + insts.extend([ + create_instruction("LOAD_GLOBAL", argval="wrapper1"), + create_instruction("SWAP", arg=2), + *create_call_function(1, True), + ) + """ + if sys.version_info >= (3, 11): + output = [] + if push_null: + output.append(create_instruction("PUSH_NULL")) + # 3.13 swapped NULL and callable + rots = nargs + 1 if sys.version_info >= (3, 13) else nargs + 2 + output.extend(create_rot_n(rots)) + if sys.version_info < (3, 12): + output.append(create_instruction("PRECALL", arg=nargs)) + output.append(create_instruction("CALL", arg=nargs)) + return output + return [create_instruction("CALL_FUNCTION", arg=nargs)] + + +def create_call_function_ex( + has_kwargs: bool, push_null: bool, ignore_314_kwargs_push: bool = False +) -> list[Instruction]: + """ + Assumes that in 3.14+, if has_kwargs=False, there is NOT a NULL + on the TOS for the kwargs. This utility function will add a PUSH_NULL. + + If the caller has already pushed a NULL for the kwargs, then set ignore_314_kwargs_push=True + so we don't push another NULL for the kwargs. + """ + if sys.version_info >= (3, 11): + output = [] + if ( + sys.version_info >= (3, 14) + and not has_kwargs + and not ignore_314_kwargs_push + ): + output.append(create_instruction("PUSH_NULL")) + has_kwargs = True + if push_null: + output.append(create_instruction("PUSH_NULL")) + # 3.13 swapped NULL and callable + # if flags == 1, 2 values popped - otherwise if flags == 0, 1 value + rots = ( + int(has_kwargs) + 2 + if sys.version_info >= (3, 13) + else int(has_kwargs) + 3 + ) + output.extend(create_rot_n(rots)) + output.append(create_instruction("CALL_FUNCTION_EX", arg=int(has_kwargs))) + return output + return [create_instruction("CALL_FUNCTION_EX", arg=int(has_kwargs))] + + +def create_call_method(nargs: int) -> list[Instruction]: + if sys.version_info >= (3, 12): + return [create_instruction("CALL", arg=nargs)] + if sys.version_info >= (3, 11): + return [ + create_instruction("PRECALL", arg=nargs), + create_instruction("CALL", arg=nargs), + ] + return [create_instruction("CALL_METHOD", arg=nargs)] + + +def create_load_method(name: str) -> Instruction: + if sys.version_info >= (3, 12): + # in 3.12, create a LOAD_ATTR instruction with the low bit set + return create_instruction("LOAD_ATTR", arg=1, argval=name) + return create_instruction("LOAD_METHOD", argval=name) + + +def create_setup_with(target: Instruction) -> Instruction: + opname = "BEFORE_WITH" if sys.version_info >= (3, 11) else "SETUP_WITH" + return create_instruction(opname, target=target) + + +def create_swap(n: int) -> list[Instruction]: + if sys.version_info >= (3, 11): + return [create_instruction("SWAP", arg=n)] + # in Python < 3.11, SWAP is a macro that expands to multiple instructions + if n == 1: + return [] + elif n == 2: + return [create_instruction("ROT_TWO")] + elif n == 3: + return [create_instruction("ROT_THREE"), create_instruction("ROT_TWO")] + """ + e.g. swap "a" and "b" in this stack: + 0 a 1 2 3 b + 0 a [1 2 3 b] + 0 a [1 2 3 b] [1 2 3 b] + 0 a [1 2 3 b] [1 2 3 b] -1 + 0 a [1 2 3 b] b + 0 b a [1 2 3 b] + 0 b a [1 2 3 b] [1 2 3 b] + 0 b [1 2 3 b] a [1 2 3 b] + 0 b [1 2 3 b] a [1 2 3 b] -1 + 0 b [1 2 3 a] + 0 b [1 2 3 a] [1 2 3 a] + 0 b [1 2 3 a] [1 2 3 a] reverse + 0 b [a 3 2 1] None + 0 b [a 3 2 1] + 0 b 1 2 3 a + """ + return [ + create_instruction("BUILD_LIST", arg=n - 1), + create_instruction("DUP_TOP"), + create_instruction("LOAD_CONST", argval=-1), + create_binary_subscr(), + create_instruction("ROT_THREE"), + create_instruction("DUP_TOP"), + create_instruction("ROT_THREE"), + create_instruction("LOAD_CONST", argval=-1), + create_instruction("STORE_SUBSCR"), + create_instruction("DUP_TOP"), + create_load_method("reverse"), + *create_call_method(0), + create_instruction("POP_TOP"), + create_instruction("UNPACK_SEQUENCE", arg=n - 1), + ] + + +def create_binary_slice( + start: int | None, end: int | None, store: bool = False +) -> list[Instruction]: + """ + BINARY_SLICE and STORE_SLICE (if `set` is True) for all Python versions + """ + if sys.version_info >= (3, 14): + subscr_inst = ( + create_instruction("STORE_SUBSCR") if store else create_binary_subscr() + ) + return [ + create_load_const(slice(start, end)), + subscr_inst, + ] + elif sys.version_info >= (3, 12): + inst_name = "STORE_SLICE" if store else "BINARY_SLICE" + return [ + create_load_const(start), + create_load_const(end), + create_instruction(inst_name), + ] + else: + inst_name = "STORE_SUBSCR" if store else "BINARY_SUBSCR" + return [ + create_load_const(start), + create_load_const(end), + create_instruction("BUILD_SLICE", arg=2), + create_instruction(inst_name), + ] + + +def create_copy(i: int) -> list[Instruction]: + if sys.version_info >= (3, 11): + return [create_instruction("COPY", arg=i)] + if i == 1: + return [create_instruction("DUP_TOP")] + # COPY 4 + # 0 1 2 3 + # 3 1 2 0 + # 3 1 2 0 0 + # 0 1 2 0 3 + # 0 1 2 3 0 + return [ + *create_swap(i), + create_dup_top(), + *create_swap(i + 1), + *create_swap(2), + ] + + +# mainly for debugging generated bytecode +def create_print_on_stack(depth: int) -> list[Instruction]: + return [ + *add_push_null(create_instruction("LOAD_CONST", argval=print)), + *create_copy(depth + (2 if sys.version_info >= (3, 11) else 1)), + *create_call_function(1, False), + create_instruction("POP_TOP"), + ] + + +# mainly for debugging generated bytecode +def create_print_value(value: Any) -> list[Instruction]: + return [ + *add_push_null(create_instruction("LOAD_CONST", argval=print)), + create_instruction("LOAD_CONST", argval=value), + *create_call_function(1, False), + create_instruction("POP_TOP"), + ] + + +def create_binary_subscr() -> Instruction: + if sys.version_info < (3, 14): + return create_instruction("BINARY_SUBSCR") + # https://github.com/python/cpython/blob/0e46c0499413bc5f9f8336fe76e2e67cf93f64d8/Include/opcode.h#L36 + return create_instruction("BINARY_OP", arg=26) + + +def create_build_tuple(n: int) -> Instruction: + if sys.version_info >= (3, 14) and n == 0: + return create_load_const(()) + return create_instruction("BUILD_TUPLE", arg=n) + + +def linetable_writer( + first_lineno: int, +) -> tuple[list[int], Callable[[int, int], None], Callable[[int], None]]: + """ + Used to create typing.CodeType.co_linetable + See https://github.com/python/cpython/blob/main/Objects/lnotab_notes.txt + This is the internal format of the line number table for Python 3.10 + """ + assert sys.version_info[:2] == (3, 10) + linetable: list[int] = [] + lineno = first_lineno + lineno_delta = 0 + byteno = 0 + + def _update(byteno_delta: int, lineno_delta: int) -> None: + while byteno_delta != 0 or lineno_delta != 0: + byte_offset = max(0, min(byteno_delta, 254)) + line_offset = max(-127, min(lineno_delta, 127)) + assert byte_offset != 0 or line_offset != 0 + byteno_delta -= byte_offset + lineno_delta -= line_offset + linetable.extend((byte_offset, line_offset & 0xFF)) + + def update(lineno_new: int, byteno_new: int) -> None: + nonlocal lineno, lineno_delta, byteno + byteno_delta = byteno_new - byteno + byteno = byteno_new + _update(byteno_delta, lineno_delta) + lineno_delta = lineno_new - lineno + lineno = lineno_new + + def end(total_bytes: int) -> None: + _update(total_bytes - byteno, lineno_delta) + + return linetable, update, end + + +def encode_varint(n: int) -> list[int]: + """ + 6-bit chunk encoding of an unsigned integer + See https://github.com/python/cpython/blob/3.11/Objects/locations.md + """ + assert n >= 0 + b = [n & 63] + n >>= 6 + while n > 0: + b[-1] |= 64 + b.append(n & 63) + n >>= 6 + return b + + +if sys.version_info >= (3, 11): + + def linetable_311_writer( + first_lineno: int, + ) -> tuple[list[int], Callable[[dis.Positions | None, int], None]]: + """ + Used to create typing.CodeType.co_linetable + See https://github.com/python/cpython/blob/3.11/Objects/locations.md + This is the internal format of the line number table for Python 3.11 + """ + assert sys.version_info >= (3, 11) + linetable = [] + lineno = first_lineno + + def update(positions: dis.Positions | None, inst_size: int) -> None: + nonlocal lineno + lineno_new = positions.lineno if positions else None + + def _update(delta: int, size: int) -> None: + assert 0 < size <= 8 + # first byte - use 13 (no column info) is positions is + # malformed, otherwise use 14 (long form) + other_varints: tuple[int, ...] = () + if ( + positions + and positions.lineno is not None + and positions.end_lineno is not None + and positions.col_offset is not None + and positions.end_col_offset is not None + ): + linetable.append(0b1_1110_000 + size - 1) + # for whatever reason, column offset needs `+ 1` + # https://github.com/python/cpython/blob/1931c2a438c50e6250725c84dff94fc760b9b951/Python/compile.c#L7603 + other_varints = ( + positions.end_lineno - positions.lineno, + positions.col_offset + 1, + positions.end_col_offset + 1, + ) + else: + linetable.append(0b1_1101_000 + size - 1) + # encode signed int + if delta < 0: + delta = ((-delta) << 1) | 1 + else: + delta <<= 1 + # encode unsigned int + linetable.extend(encode_varint(delta)) + for n in other_varints: + linetable.extend(encode_varint(n)) + + if lineno_new is None: + lineno_delta = 0 + else: + lineno_delta = lineno_new - lineno + lineno = lineno_new + while inst_size > 8: + _update(lineno_delta, 8) + inst_size -= 8 + _update(lineno_delta, inst_size) + + return linetable, update + + +@dataclasses.dataclass(slots=True) +class ExceptionTableEntry: + start: int + end: int + target: int + depth: int + lasti: bool + + +def encode_exception_table_varint(n: int) -> list[int]: + """ + Similar to `encode_varint`, but the 6-bit chunks are ordered in reverse. + """ + assert n >= 0 + b = [n & 63] + n >>= 6 + while n > 0: + b.append(n & 63) + n >>= 6 + b.reverse() + for i in range(len(b) - 1): + b[i] |= 64 + return b + + +def decode_exception_table_varint(bytes_iter: Iterator[int]) -> int: + """ + Inverse of `encode_exception_table_varint`. + """ + b = next(bytes_iter) + val = b & 63 + while b & 64: + val <<= 6 + b = next(bytes_iter) + val |= b & 63 + return val + + +def check_exception_table(tab: list[ExceptionTableEntry]) -> None: + """ + Verifies that a list of ExceptionTableEntries will make a well-formed + jump table: entries are non-empty, sorted, and do not overlap. + """ + for i in range(len(tab) - 1): + assert ( + tab[i].start <= tab[i].end + and tab[i].end < tab[i + 1].start + and tab[i + 1].start <= tab[i + 1].end + ) + + +def parse_exception_table(exntab: bytes) -> list[ExceptionTableEntry]: + """ + Parse the exception table according to + https://github.com/python/cpython/blob/3.11/Objects/exception_handling_notes.txt + """ + exntab_iter = iter(exntab) + tab = [] + try: + while True: + start = decode_exception_table_varint(exntab_iter) * 2 + length = decode_exception_table_varint(exntab_iter) * 2 + end = start + length - 2 + target = decode_exception_table_varint(exntab_iter) * 2 + dl = decode_exception_table_varint(exntab_iter) + depth = dl >> 1 + lasti = bool(dl & 1) + tab.append(ExceptionTableEntry(start, end, target, depth, lasti)) + except StopIteration: + check_exception_table(tab) + return tab + + +def assemble_exception_table(tab: list[ExceptionTableEntry]) -> bytes: + """ + Inverse of parse_exception_table - encodes list of exception + table entries into bytes. + """ + b = [] + for entry in tab: + first_entry = encode_exception_table_varint(entry.start // 2) + first_entry[0] |= 1 << 7 + b.extend(first_entry) + length = entry.end - entry.start + 2 + b.extend(encode_exception_table_varint(length // 2)) + b.extend(encode_exception_table_varint(entry.target // 2)) + dl = (entry.depth << 1) + entry.lasti + b.extend(encode_exception_table_varint(dl)) + return bytes(b) + + +def assemble(instructions: list[Instruction], firstlineno: int) -> tuple[bytes, bytes]: + """Do the opposite of dis.get_instructions()""" + code: list[int] = [] + if sys.version_info >= (3, 11): + lnotab, update_lineno = linetable_311_writer(firstlineno) + num_ext = 0 + for i, inst in enumerate(instructions): + if inst.opname == "EXTENDED_ARG": + inst_size = 1 + num_ext += 1 + # copy positions from the actual instruction + for j in (1, 2, 3): + if instructions[i + j].opname != "EXTENDED_ARG": + inst.positions = instructions[i + j].positions + break + else: + inst_size = instruction_size(inst) // 2 + num_ext + num_ext = 0 + update_lineno(inst.positions, inst_size) + num_ext = 0 + arg = inst.arg or 0 + code.extend((inst.opcode, arg & 0xFF)) + for _ in range(instruction_size(inst) // 2 - 1): + code.extend((0, 0)) + else: + lnotab, update_lineno, end = linetable_writer(firstlineno) + + for inst in instructions: + if inst.starts_line is not None: + update_lineno(inst.starts_line, len(code)) + arg = inst.arg or 0 + code.extend((inst.opcode, arg & 0xFF)) + + end(len(code)) + + return bytes(code), bytes(lnotab) + + +def _get_instruction_by_offset( + offset_to_inst: dict[int, Instruction], offset: int +) -> Instruction | None: + """ + Get the instruction located at a given offset, accounting for EXTENDED_ARGs + """ + for n in (0, 2, 4, 6): + if offset_to_inst[offset + n].opcode != dis.EXTENDED_ARG: + return offset_to_inst[offset + n] + return None + + +def virtualize_jumps(instructions: Iterable[Instruction]) -> None: + """Replace jump targets with pointers to make editing easier""" + jump_targets = { + inst.offset: inst for inst in instructions if inst.offset is not None + } + + for inst in instructions: + if inst.opcode in dis.hasjabs or inst.opcode in dis.hasjrel: + inst.target = _get_instruction_by_offset(jump_targets, inst.argval) + + +_REL_JUMPS = set(dis.hasjrel) + + +def flip_jump_direction(instruction: Instruction) -> None: + if sys.version_info < (3, 11): + raise RuntimeError("Cannot flip jump direction in Python < 3.11") + if "FORWARD" in instruction.opname: + instruction.opname = instruction.opname.replace("FORWARD", "BACKWARD") + elif "BACKWARD" in instruction.opname: + instruction.opname = instruction.opname.replace("BACKWARD", "FORWARD") + else: + raise AttributeError("Instruction is not a forward or backward jump") + instruction.opcode = dis.opmap[instruction.opname] + assert instruction.opcode in _REL_JUMPS + + +def _get_instruction_front(instructions: list[Instruction], idx: int) -> Instruction: + """ + i.e. get the first EXTENDED_ARG instruction (if any) when targeting + instructions[idx] with a jump. + """ + target = instructions[idx] + for offset in (1, 2, 3): + if idx >= offset and instructions[idx - offset].opcode == dis.EXTENDED_ARG: + target = instructions[idx - offset] + else: + break + return target + + +def devirtualize_jumps(instructions: list[Instruction]) -> None: + """Fill in args for virtualized jump target after instructions may have moved""" + jumps = set(dis.hasjabs).union(set(dis.hasjrel)) + + # check for negative jump args and fix them + for inst in instructions: + if inst.opcode in jumps: + if inst.opcode not in dis.hasjabs: + assert ( + inst.target is not None + and inst.target.offset is not None + and inst.offset is not None + ) + if inst.target.offset < inst.offset: + if sys.version_info < (3, 11): + raise RuntimeError("Got negative jump offset for Python < 3.11") + # forward jumps become backward + if "FORWARD" in inst.opname: + flip_jump_direction(inst) + else: + # backward jumps become forward + if sys.version_info >= (3, 11) and "BACKWARD" in inst.opname: + flip_jump_direction(inst) + + # jump instruction size may have changed due to flips + update_offsets(instructions) + indexof = get_indexof(instructions) + + # compute jump instruction arg + for inst in instructions: + if inst.opcode in jumps: + assert inst.target is not None + target = _get_instruction_front(instructions, indexof[inst.target]) + if inst.opcode in dis.hasjabs: + if sys.version_info < (3, 11): + # `arg` is expected to be bytecode offset, whereas `offset` is byte offset. + # Divide since bytecode is 2 bytes large. + assert target.offset is not None + inst.arg = int(target.offset / 2) + else: + raise RuntimeError("Python 3.11+ should not have absolute jumps") + else: # relative jump + # byte offset between target and next instruction + assert target.offset is not None and inst.offset is not None + inst.arg = abs( + int(target.offset - inst.offset - instruction_size(inst)) + ) + # pyrefly: ignore [unsupported-operation] + inst.arg //= 2 + inst.argval = target.offset + inst.argrepr = f"to {target.offset}" + + +def virtualize_exception_table( + exn_tab_bytes: bytes, instructions: list[Instruction] +) -> None: + """Replace exception table entries with pointers to make editing easier""" + exn_tab = parse_exception_table(exn_tab_bytes) + offset_to_inst = {cast(int, inst.offset): inst for inst in instructions} + offsets = sorted(offset_to_inst.keys()) + end_offset_idx = 0 + exn_tab_iter = iter(exn_tab) + try: + + def step() -> tuple[ExceptionTableEntry, InstructionExnTabEntry]: + nonlocal end_offset_idx + entry = next(exn_tab_iter) + # find rightmost offset <= entry.end, since entry.end may not be + # an actual instruction, e.g. if the end instruction is LOAD_GLOBAL, + # which takes more than 2 bytes, then entry.end points to the end + # of the LOAD_GLOBAL instruction, not the beginning. + while ( + end_offset_idx < len(offsets) and offsets[end_offset_idx] <= entry.end + ): + end_offset_idx += 1 + assert end_offset_idx > 0 + end_offset = offsets[end_offset_idx - 1] + inst_entry = InstructionExnTabEntry( + _get_instruction_by_offset(offset_to_inst, entry.start), # type: ignore[arg-type] + _get_instruction_by_offset(offset_to_inst, end_offset), # type: ignore[arg-type] + _get_instruction_by_offset(offset_to_inst, entry.target), # type: ignore[arg-type] + entry.depth, + entry.lasti, + ) + return entry, inst_entry + + entry, inst_entry = step() + for inst in instructions: + assert inst.offset is not None + while inst.offset > entry.end: + entry, inst_entry = step() + if inst.offset >= entry.start: + inst.exn_tab_entry = copy.copy(inst_entry) + except StopIteration: + pass + + +def compute_exception_table( + instructions: list[Instruction], +) -> list[ExceptionTableEntry]: + """Compute exception table in list format from instructions with exn_tab_entries""" + exn_dict: dict[tuple[int, int], tuple[int, int, bool]] = {} + indexof = get_indexof(instructions) + + for inst in instructions: + if inst.exn_tab_entry: + # account for prefixed EXTENDED_ARGS + start = _get_instruction_front( + instructions, indexof[inst.exn_tab_entry.start] + ).offset + assert start is not None + # point to the last 2 bytes of the end instruction + end = ( + cast(int, inst.exn_tab_entry.end.offset) + + instruction_size(inst.exn_tab_entry.end) + - 2 + ) + assert end is not None + target = _get_instruction_front( + instructions, indexof[inst.exn_tab_entry.target] + ).offset + assert target is not None + key = (start, end) + val = (target, inst.exn_tab_entry.depth, inst.exn_tab_entry.lasti) + if key in exn_dict: + assert exn_dict[key] == val + exn_dict[key] = val + + # Dynamo may construct nested exception table entries for convenience, + # but Python expects exception table entries to not overlap. + # NOTE: below, "keys" refer to old instruction entries' starts and ends, + # and "entries" refer to the generated exception table entries. + + # Sort keys by increasing start, then decreasing end + keys_sorted = sorted(exn_dict.keys(), key=lambda t: (t[0], -t[1])) + # smallest byte that the next exception table entry can start at + nexti = 0 + # stack of current nested keys + key_stack: list[tuple[int, int]] = [] + exn_tab: list[ExceptionTableEntry] = [] + + def pop() -> None: + """ + Pop the key_stack and append an exception table entry if possible. + """ + nonlocal nexti + if key_stack: + key = key_stack.pop() + if nexti <= key[1]: + exn_tab.append( + ExceptionTableEntry(max(key[0], nexti), key[1], *exn_dict[key]) + ) + nexti = key[1] + 2 + + for key in keys_sorted: + # pop keys that are no longer nested over the current key + while key_stack and key_stack[-1][1] < key[0]: + pop() + if key_stack: + # create an entry covering to the current key, if possible + assert key_stack[-1][0] <= key[0] <= key[1] <= key_stack[-1][1] + left = max(nexti, key_stack[-1][0]) + if left < key[0]: + exn_tab.append( + ExceptionTableEntry(left, key[0] - 2, *exn_dict[key_stack[-1]]) + ) + nexti = key[0] + key_stack.append(key) + while key_stack: + pop() + check_exception_table(exn_tab) + return exn_tab + + +def check_inst_exn_tab_entries_nested( + tab: list[InstructionExnTabEntry], indexof: dict[Instruction, int] +) -> None: + """ + Checks `tab` is a properly sorted list of nested InstructionExnTabEntry's, + i.e. no entries partially overlap. + "Properly sorted" means entries are sorted by increasing starts, then + decreasing ends. + """ + entry_stack: list[tuple[int, int]] = [] + for entry in tab: + key = (indexof[entry.start], indexof[entry.end]) + while entry_stack and entry_stack[-1][1] < key[0]: + entry_stack.pop() + if entry_stack: + assert entry_stack[-1][0] <= key[0] <= key[1] <= entry_stack[-1][1] + entry_stack.append(key) + + +def propagate_inst_exn_table_entries(instructions: list[Instruction]) -> None: + """ + Copies exception table entries to all instructions in an entry's range. + Supports nested exception table entries. + """ + indexof = get_indexof(instructions) + entries: dict[tuple[int, int], InstructionExnTabEntry] = {} + for inst in instructions: + if inst.exn_tab_entry: + key = ( + indexof[inst.exn_tab_entry.start], + indexof[inst.exn_tab_entry.end], + ) + if key in entries: + assert inst.exn_tab_entry == entries[key] + entries[key] = inst.exn_tab_entry + sorted_entries = [ + entries[key] for key in sorted(entries.keys(), key=lambda t: (t[0], -t[1])) + ] + check_inst_exn_tab_entries_nested(sorted_entries, indexof) + # Propagation of nested entries works since nested entries come later + # in sorted order. + for entry in sorted_entries: + for i in range(indexof[entry.start], indexof[entry.end] + 1): + instructions[i].exn_tab_entry = copy.copy(entry) + + +def check_inst_exn_tab_entries_valid(instructions: list[Instruction]) -> None: + """ + Checks that exn_tab_entries of instructions are valid. + An entry's start, end, and target must be in instructions. + Instructions with an exn_tab_entry are located within + the entry's start and end instructions. + Instructions do not share exn_tab_entries. + + Implicitly checks for no duplicate instructions. + """ + indexof = get_indexof(instructions) + exn_tab_entry_set = set() + for i, inst in enumerate(instructions): + if inst.exn_tab_entry: + assert sys.version_info >= (3, 11) + assert id(inst.exn_tab_entry) not in exn_tab_entry_set + exn_tab_entry_set.add(id(inst.exn_tab_entry)) + entry = inst.exn_tab_entry + assert entry.start in indexof + assert entry.end in indexof + assert entry.target in indexof + assert indexof[entry.start] <= i <= indexof[entry.end] + + +def strip_extended_args(instructions: list[Instruction]) -> None: + instructions[:] = [i for i in instructions if i.opcode != dis.EXTENDED_ARG] + + +# Overwrites old_inst with a sequence of new instructions. +# This is necessary in order to preserve jump targets to the old +# instruction, exception table entries, and positions. +# Returns the modified sequence of instructions (including the modified +# old instruction!) that can be manipulated elsewhere. +def overwrite_instruction( + old_inst: Instruction, new_insts: list[Instruction] +) -> list[Instruction]: + # update old_inst.exnt_tab_entry.end if necessary + if ( + old_inst.exn_tab_entry + and old_inst.exn_tab_entry.end is old_inst + and len(new_insts) > 1 + ): + old_inst.exn_tab_entry.end = new_insts[-1] + # preserve exception table entries and positions + for inst in new_insts[1:]: + inst.exn_tab_entry = copy.copy(old_inst.exn_tab_entry) + inst.positions = old_inst.positions + # modify old_inst in-place to preserve jump target + old_inst.opcode = new_insts[0].opcode + old_inst.opname = new_insts[0].opname + old_inst.arg = new_insts[0].arg + old_inst.argval = new_insts[0].argval + old_inst.target = new_insts[0].target + return [old_inst] + new_insts[1:] + + +def remove_load_call_method(instructions: list[Instruction]) -> list[Instruction]: + """LOAD_METHOD puts a NULL on the stack which causes issues, so remove it""" + assert sys.version_info < (3, 11) + rewrites = {"LOAD_METHOD": "LOAD_ATTR", "CALL_METHOD": "CALL_FUNCTION"} + for inst in instructions: + if inst.opname in rewrites: + inst.opname = rewrites[inst.opname] + inst.opcode = dis.opmap[inst.opname] + return instructions + + +def remove_jump_if_none(instructions: list[Instruction]) -> None: + new_insts = [] + for inst in instructions: + if "_NONE" in inst.opname: + is_op = create_instruction("IS_OP", arg=int("NOT" in inst.opname)) + # need both argval and arg set correctly now (not later) + is_op.argval = is_op.arg + + if sys.version_info < (3, 12): + jump_op = create_instruction( + ( + "POP_JUMP_FORWARD_IF_TRUE" + if "FORWARD" in inst.opname + else "POP_JUMP_BACKWARD_IF_TRUE" + ), + target=inst.target, + ) + else: + jump_op = create_instruction("POP_JUMP_IF_TRUE", target=inst.target) + + replace_insts = [ + create_instruction("LOAD_CONST", argval=None), + is_op, + jump_op, + ] + new_insts.extend(overwrite_instruction(inst, replace_insts)) + else: + new_insts.append(inst) + instructions[:] = new_insts + + +def remove_binary_store_slice(instructions: list[Instruction]) -> None: + new_insts = [] + for inst in instructions: + new_insts.append(inst) + if inst.opname in ("BINARY_SLICE", "STORE_SLICE"): + # new instruction + if sys.version_info >= (3, 14) and inst.opname == "BINARY_SLICE": + subscr_inst = create_binary_subscr() + else: + subscr_inst = create_instruction(inst.opname.replace("SLICE", "SUBSCR")) + if inst.exn_tab_entry and inst.exn_tab_entry.end is inst: + inst.exn_tab_entry.end = subscr_inst + subscr_inst.exn_tab_entry = copy.copy(inst.exn_tab_entry) + subscr_inst.positions = inst.positions + # modify inst in-place to preserve jump target + inst.opcode = dis.opmap["BUILD_SLICE"] + inst.opname = "BUILD_SLICE" + inst.arg = 2 + inst.argval = 2 + new_insts.append(subscr_inst) + instructions[:] = new_insts + + +FUSED_INSTS = { + "LOAD_FAST_LOAD_FAST": ("LOAD_FAST", "LOAD_FAST"), + "LOAD_FAST_BORROW_LOAD_FAST_BORROW": ("LOAD_FAST_BORROW", "LOAD_FAST_BORROW"), + "STORE_FAST_STORE_FAST": ("STORE_FAST", "STORE_FAST"), + "STORE_FAST_LOAD_FAST": ("STORE_FAST", "LOAD_FAST"), +} + + +def remove_fused_load_store(instructions: list[Instruction]) -> None: + new_insts = [] + for inst in instructions: + if inst.opname in FUSED_INSTS: + inst0, inst1 = FUSED_INSTS[inst.opname] + argval0, argval1 = inst.argval + + replace_insts = [ + create_instruction(inst0, argval=argval0), + create_instruction(inst1, argval=argval1), + ] + new_insts.extend(overwrite_instruction(inst, replace_insts)) + else: + new_insts.append(inst) + instructions[:] = new_insts + + +# adds GRAPH_BREAK_IF_LEAF (not a real instruction) before RETURN_* instructions +# for testing purposes +def add_graph_break_if_leaf_instructions(instructions: list[Instruction]) -> None: + new_insts = [] + for inst in instructions: + if "RETURN" in inst.opname: + replace_insts = [ + create_instruction("NOP", argval="GRAPH_BREAK_IF_LEAF"), + create_instruction(inst.opname, argval=inst.argval), + ] + new_insts.extend(overwrite_instruction(inst, replace_insts)) + else: + new_insts.append(inst) + instructions[:] = new_insts + + +def remove_graph_break_if_leaf_instructions(instructions: list[Instruction]) -> None: + new_insts = [] + for inst, next_inst in itertools.pairwise(instructions): + if ( + inst.opname == "NOP" + and inst.argval == "GRAPH_BREAK_IF_LEAF" + and next_inst.opname.startswith("RETURN") + ): + # remove this instruction and update all other instructions' jump targets + for i in range(len(instructions)): + if instructions[i].target is inst: + instructions[i].target = next_inst + if instructions[i].exn_tab_entry: + # linter is mistakenly complaining that None has no attribute "..." + # but this codepath only runs if instructions[i] is not None + if instructions[i].exn_tab_entry.start is inst: # type: ignore[union-attr] + instructions[i].exn_tab_entry.start = next_inst # type: ignore[union-attr] + if instructions[i].exn_tab_entry.end is inst: # type: ignore[union-attr] + instructions[i].exn_tab_entry.end = next_inst # type: ignore[union-attr] + if instructions[i].exn_tab_entry.target is inst: # type: ignore[union-attr] + instructions[i].exn_tab_entry.target = next_inst # type: ignore[union-attr] + else: + new_insts.append(inst) + new_insts.append(instructions[-1]) + instructions[:] = new_insts + + +def explicit_super(code: types.CodeType, instructions: list[Instruction]) -> None: + """convert super() with no args into explicit arg form""" + cell_and_free = (code.co_cellvars or ()) + (code.co_freevars or ()) + if not len(code.co_varnames): + # A function with no argument cannot contain a valid "super()" call + return + output = [] + for idx, inst in enumerate(instructions): + output.append(inst) + if inst.opname == "LOAD_GLOBAL" and inst.argval == "super": + nexti = instructions[idx + 1] + if nexti.arg == 0 and ( + (sys.version_info >= (3, 12) and nexti.opname == "CALL") + or ( + sys.version_info >= (3, 11) + and sys.version_info < (3, 12) + and nexti.opname == "PRECALL" + ) + or (sys.version_info < (3, 11) and nexti.opname == "CALL_FUNCTION") + ): + assert "__class__" in cell_and_free + output.append(create_instruction("LOAD_DEREF", argval="__class__")) + first_var = code.co_varnames[0] + if first_var in cell_and_free: + output.append(create_instruction("LOAD_DEREF", argval=first_var)) + else: + output.append(create_instruction("LOAD_FAST", argval=first_var)) + nexti.arg = 2 + nexti.argval = 2 + if nexti.opname == "PRECALL": + # also update the following CALL instruction + call_inst = instructions[idx + 2] + call_inst.arg = 2 + call_inst.argval = 2 + + instructions[:] = output + + +def fix_extended_args(instructions: list[Instruction]) -> int: + """Fill in correct argvals for EXTENDED_ARG ops""" + output: list[Instruction] = [] + + def maybe_pop_n(n: int) -> None: + for _ in range(n): + if output and output[-1].opcode == dis.EXTENDED_ARG: + output.pop() + + for inst in instructions: + if inst.opcode == dis.EXTENDED_ARG: + # Leave this instruction alone for now so we never shrink code + inst.arg = 0 + elif inst.arg and inst.arg > 0xFFFFFF: + maybe_pop_n(3) + output.append(create_instruction("EXTENDED_ARG", arg=inst.arg >> 24)) + output.append(create_instruction("EXTENDED_ARG", arg=inst.arg >> 16)) + output.append(create_instruction("EXTENDED_ARG", arg=inst.arg >> 8)) + elif inst.arg and inst.arg > 0xFFFF: + maybe_pop_n(2) + output.append(create_instruction("EXTENDED_ARG", arg=inst.arg >> 16)) + output.append(create_instruction("EXTENDED_ARG", arg=inst.arg >> 8)) + elif inst.arg and inst.arg > 0xFF: + maybe_pop_n(1) + output.append(create_instruction("EXTENDED_ARG", arg=inst.arg >> 8)) + output.append(inst) + + added = len(output) - len(instructions) + assert added >= 0 + instructions[:] = output + return added + + +def instruction_size(inst: Instruction) -> int: + import torch + + if sys.version_info >= (3, 11): + return 2 * (torch._C._dynamo.eval_frame.py_opcode_caches[inst.opcode] + 1) + return 2 + + +def check_offsets(instructions: Sequence[Instruction]) -> None: + offset = 0 + for inst in instructions: + assert inst.offset == offset + offset += instruction_size(inst) + + +def update_offsets(instructions: Sequence[Instruction]) -> None: + offset = 0 + for inst in instructions: + inst.offset = offset + + offset += instruction_size(inst) + + +def debug_bytes(*args: bytes) -> str: + index = range(max(map(len, args))) + result = [ + " ".join(f"{x:03}" for x in arg) + for arg in [index] + + list(args) + + [[int(a != b) for a, b in zip(args[-1], args[-2])]] + ] + + return "bytes mismatch\n" + "\n".join(result) + + +def debug_checks(code: types.CodeType) -> None: + """Make sure our assembler produces same bytes as we start with""" + dode, _ = transform_code_object(code, lambda x, y: None, safe=True) + assert code.co_code == dode.co_code, debug_bytes(code.co_code, dode.co_code) + # TODO: reenable after better understanding how to handle sourceless + # (None) codes + # assert list(code.co_lines()) == list(dode.co_lines()), ( + # "line table mismatch via co_lines()" + # ) + + +HAS_LOCAL = set(dis.haslocal) +HAS_NAME = set(dis.hasname) +HAS_FREE = set(dis.hasfree) +HAS_CONST = set(dis.hasconst) + + +def get_const_index(code_options: dict[str, Any], val: Any) -> int: + for i, v in enumerate(code_options["co_consts"]): + # NOTE: stronger comparison is required, since we have + # examples where two values compare equal but have + # different semantic meaning in some cases, e.g. + # 0.0 == -0.0 but have different effects in torch.copysign. + if val is v: + return i + code_options["co_consts"] += (val,) + return len(code_options["co_consts"]) - 1 + + +def fix_vars( + instructions: list[Instruction], + code_options: dict[str, Any], + varname_from_oparg: Callable[..., Any] | None = None, +) -> None: + # compute instruction arg from argval if arg is not provided + names = {name: idx for idx, name in enumerate(code_options["co_names"])} + + def get_name_index(name: str) -> int: + try: + idx = names[name] + except KeyError: + # Add a missing item to co_names + idx = names[name] = len(names) + code_options["co_names"] = (*code_options["co_names"], name) + assert len(code_options["co_names"]) == len(names) + return idx + + if sys.version_info < (3, 11): + assert varname_from_oparg is None + varnames = {name: idx for idx, name in enumerate(code_options["co_varnames"])} + freenames = { + name: idx + for idx, name in enumerate( + code_options["co_cellvars"] + code_options["co_freevars"] + ) + } + else: + assert callable(varname_from_oparg) + allnames = {} + for idx in itertools.count(): + try: + name = varname_from_oparg(idx) + allnames[name] = idx + except IndexError: + break + varnames = {name: allnames[name] for name in code_options["co_varnames"]} + freenames = { + name: allnames[name] + for name in code_options["co_cellvars"] + code_options["co_freevars"] + } + for i in range(len(instructions)): + + def should_compute_arg() -> bool: + # argval is prioritized over arg + return instructions[i].argval is not _NotProvided + + if instructions[i].opname == "LOAD_GLOBAL": + # 3.11 LOAD_GLOBAL requires both arg and argval - see create_instruction + assert instructions[i].argval is not _NotProvided + if sys.version_info >= (3, 11): + assert instructions[i].arg is not None + instructions[i].arg = (get_name_index(instructions[i].argval) << 1) + ( + cast(int, instructions[i].arg) % 2 + ) + else: + instructions[i].arg = get_name_index(instructions[i].argval) + elif instructions[i].opname == "LOAD_ATTR": + # 3.12 LOAD_ATTR requires both arg and argval, like LOAD_GLOBAL + assert instructions[i].argval is not _NotProvided + if sys.version_info >= (3, 12): + assert instructions[i].arg is not None + instructions[i].arg = (get_name_index(instructions[i].argval) << 1) + ( + cast(int, instructions[i].arg) % 2 + ) + else: + instructions[i].arg = get_name_index(instructions[i].argval) + elif instructions[i].opname == "LOAD_SUPER_ATTR": + assert instructions[i].arg is not None + assert instructions[i].argval is not _NotProvided + # Copy low bit, force second bit on for explicit super (the "+ 2") + instructions[i].arg = ( + (get_name_index(instructions[i].argval) << 2) + + (cast(int, instructions[i].arg) % 2) + + 2 + ) + elif instructions[i].opname in FUSED_INSTS: + assert sys.version_info >= (3, 13) + assert isinstance(instructions[i].argval, tuple) + assert len(instructions[i].argval) == 2 + arg_tuple = tuple( + varnames[name] if name in varnames else freenames[name] + for name in instructions[i].argval + ) + instructions[i].arg = (arg_tuple[0] << 4) + (arg_tuple[1] & 15) + elif instructions[i].opcode in HAS_LOCAL: + if should_compute_arg(): + if ( + sys.version_info >= (3, 13) + and instructions[i].argval not in varnames + ): + # instructions like LOAD_FAST used for both local and free vars + instructions[i].arg = freenames[instructions[i].argval] + else: + instructions[i].arg = varnames[instructions[i].argval] + elif instructions[i].opcode in HAS_NAME: + if should_compute_arg(): + instructions[i].arg = get_name_index(instructions[i].argval) + elif instructions[i].opcode in HAS_FREE: + if should_compute_arg(): + instructions[i].arg = freenames[instructions[i].argval] + elif instructions[i].opcode in HAS_CONST: + # NOTE: only update argval if arg is not provided. This assumes + # that any additions to co_consts are appended. + if instructions[i].arg is None: + # cannot use a dictionary since consts may not be hashable + idx = get_const_index(code_options, instructions[i].argval) + assert idx >= 0 + instructions[i].arg = idx + + +def clear_instruction_args(instructions: list[Instruction]) -> None: + # Clear the instruction arg for instructions that have argvals. + # Useful for using dis'd bytecode within generated bytecode. + for inst in instructions: + if ( + inst.argval is not _NotProvided + and ( + inst.opcode in HAS_LOCAL + or inst.opcode in HAS_NAME + or inst.opcode in HAS_FREE + or inst.opcode in HAS_CONST + ) + and inst.opname not in ("LOAD_GLOBAL", "LOAD_ATTR", "LOAD_SUPER_ATTR") + ): + inst.arg = None + + +@functools.lru_cache +def get_code_keys() -> list[str]: + # Python 3.11 changes to code keys are not fully documented. + # See https://github.com/python/cpython/blob/3.11/Objects/clinic/codeobject.c.h#L24 + # for new format. + keys = ["co_argcount"] + keys.append("co_posonlyargcount") + keys.extend( + [ + "co_kwonlyargcount", + "co_nlocals", + "co_stacksize", + "co_flags", + "co_code", + "co_consts", + "co_names", + "co_varnames", + "co_filename", + "co_name", + ] + ) + if sys.version_info >= (3, 11): + keys.append("co_qualname") + keys.append("co_firstlineno") + keys.append("co_linetable") + if sys.version_info >= (3, 11): + # not documented, but introduced in https://github.com/python/cpython/issues/84403 + keys.append("co_exceptiontable") + keys.extend( + [ + "co_freevars", + "co_cellvars", + ] + ) + return keys + + +def transform_code_object( + code: types.CodeType, + transformations: Callable[ + [list[Instruction], dict[str, Any]], Optional["DynamoTracerOutput"] + ], + safe: bool = False, +) -> tuple[types.CodeType, Optional["DynamoTracerOutput"]]: + keys = get_code_keys() + code_options = {k: getattr(code, k) for k in keys} + assert len(code_options["co_varnames"]) == code_options["co_nlocals"] + + instructions = cleaned_instructions(code, safe) + # propagate line nums again for added instructions + propagate_line_nums(instructions) + + tracer_output = transformations(instructions, code_options) + _, bytecode = clean_and_assemble_instructions(instructions, keys, code_options) + return bytecode, tracer_output + + +def clean_and_assemble_instructions( + instructions: list[Instruction], keys: list[str], code_options: dict[str, Any] +) -> tuple[list[Instruction], types.CodeType]: + remove_graph_break_if_leaf_instructions(instructions) + # also implicitly checks for no duplicate instructions + check_inst_exn_tab_entries_valid(instructions) + + code_options["co_nlocals"] = len(code_options["co_varnames"]) + varname_from_oparg = None + if sys.version_info >= (3, 11): + # temporary code object with updated names + tmp_code = types.CodeType(*[code_options[k] for k in keys]) + varname_from_oparg = tmp_code._varname_from_oparg # type: ignore[attr-defined] + fix_vars(instructions, code_options, varname_from_oparg=varname_from_oparg) + + dirty = True + while dirty: + update_offsets(instructions) + devirtualize_jumps(instructions) + # this pass might change offsets, if so we need to try again + dirty = bool(fix_extended_args(instructions)) + + remove_extra_line_nums(instructions) + bytecode, lnotab = assemble(instructions, code_options["co_firstlineno"]) + + code_options["co_linetable"] = lnotab + code_options["co_code"] = bytecode + code_options["co_stacksize"] = stacksize_analysis(instructions) + assert set(keys) - {"co_posonlyargcount"} == set(code_options.keys()) - { + "co_posonlyargcount" + } + if sys.version_info >= (3, 11): + code_options["co_exceptiontable"] = assemble_exception_table( + compute_exception_table(instructions) + ) + + return instructions, types.CodeType(*[code_options[k] for k in keys]) + + +def populate_kw_names_argval(instructions: Sequence[Instruction], consts: Any) -> None: + for inst in instructions: + if inst.opname == "KW_NAMES": + inst.argval = consts[inst.arg] + + +# If safe=True, we do not make any bytecode modifications. +# Mainly used for debugging bytecode_transformation (see debug_checks) +def cleaned_instructions(code: types.CodeType, safe: bool = False) -> list[Instruction]: + instructions = _cached_cleaned_instructions(code, safe) + # We have a lot of code that implicitly mutates the instruction array. We + # could do better here by making the copies explicit when necessary. + return _clone_instructions(instructions) + + +# Copy an instructions array, making sure to remap the individual instruction targets. +def _clone_instructions(instructions: Sequence[Instruction]) -> list[Instruction]: + # This is super hot and this is the fastest way to do this (tried copy.copy + # and dataclasses.replace). + copied = [ + Instruction( + i.opcode, + i.opname, + i.arg, + i.argval, + i.offset, + i.starts_line, + i.is_jump_target, + i.positions, + i.target, + i.exn_tab_entry, + i.argrepr, + ) + for i in instructions + ] + + remap = dict(zip(instructions, copied)) + # Handle `None` in the remapper so we don't need an extra `if`. + remap[None] = None # type: ignore[index, assignment] + + for i in copied: + i.target = remap[i.target] # type: ignore[index] + if entry := i.exn_tab_entry: + i.exn_tab_entry = InstructionExnTabEntry( + remap[entry.start], + remap[entry.end], + remap[entry.target], + entry.depth, + entry.lasti, + ) + return copied + + +@functools.lru_cache +def _cached_cleaned_instructions( + code: types.CodeType, safe: bool = False +) -> Sequence[Instruction]: + instructions = list(map(convert_instruction, dis.get_instructions(code))) + # propagate now in case we remove some instructions + propagate_line_nums(instructions) + check_offsets(instructions) + if sys.version_info >= (3, 11): + populate_kw_names_argval(instructions, code.co_consts) + virtualize_exception_table(code.co_exceptiontable, instructions) + virtualize_jumps(instructions) + strip_extended_args(instructions) + if not safe: + if sys.version_info < (3, 11): + remove_load_call_method(instructions) + if sys.version_info < (3, 12): + explicit_super(code, instructions) + if sys.version_info >= (3, 11): + remove_jump_if_none(instructions) + if sys.version_info >= (3, 12): + remove_binary_store_slice(instructions) + if sys.version_info >= (3, 13): + remove_fused_load_store(instructions) + if config.debug_force_graph_break_on_leaf_return: + add_graph_break_if_leaf_instructions(instructions) + if sys.version_info >= (3, 11): + update_offsets(instructions) + devirtualize_jumps(instructions) + return instructions + + +_unique_id_counter = itertools.count() + + +def unique_id(name: str, with_uuid: bool = False) -> str: + ret = f"{name}_{next(_unique_id_counter)}" + if with_uuid: + ret += f"_{uuid.uuid4()}".replace("-", "_") + return ret + + +def is_generator(code: types.CodeType) -> bool: + co_generator = 0x20 + return (code.co_flags & co_generator) > 0 + + +def bytecode_from_template( + fn: Callable[..., Any], + varname_map: Mapping[Any, Any] | None = None, + noreturn: bool = True, + noprefix: bool = True, +) -> list[Instruction]: + """Generates bytecode from a template function `fn` for use in + dynamo bytecode generation. + + For example, we can generate Python-version-independent bytecode + for looping through a dictionary and copying the values to a new dictionary. + + def template(d1, d2): + for k, v in d1.items(): + d2[k] = v + + + or a try block: + + def template(): + try: + dummy1 + except: + dummy2 + raise + dummy3 + + Args: + fn: a function template to generate bytecode from + varname_map: a mapping of `fn`'s varnames to new names. This + map will be applied to the generated bytecode's varnames. + For example, local variables in `fn` can be replaced with + new names that are generated by `OutputGraph.new_var`. + noreturn: remove all RETURN_* bytecodes and replace them with a jump + to the end of the bytecode. NOTE: any items pushed to the stack + for return WILL remain on the stack! Append a POP_TOP if you don't want + that item to be present. + noprefix: remove prefix bytecodes (all bytecode before the first RESUME, inclusive). + """ + insts = cleaned_instructions(fn.__code__) + clear_instruction_args(insts) + + if noprefix: + for i, inst in enumerate(insts): + if inst.opname == "RESUME": + insts = insts[i + 1 :] + break + + for inst in insts: + # If we don't reset starts_line, then the generated + # bytecode's line number will be based on fn's. + inst.starts_line = None + inst.positions = None + if varname_map and inst.argval in varname_map: + inst.argval = varname_map[inst.argval] + + if noreturn: + if sys.version_info >= (3, 12): + # replace RETURN_CONST with LOAD_CONST RETURN_VALUE + new_insts = [] + for inst in insts: + if inst.opname == "RETURN_CONST": + inst.opcode = dis.opmap["LOAD_CONST"] + inst.opname = "LOAD_CONST" + new_insts.append(inst) + # no need to propagate target/exn table + new_insts.append(create_instruction("RETURN_VALUE")) + else: + new_insts.append(inst) + insts = new_insts + + # pyrefly: ignore [implicit-any] + returns = [] + for inst in insts: + if inst.opname == "RETURN_VALUE": + returns.append(inst) + + if len(returns) == 1 and returns[0] is insts[-1]: + # only 1 return at the end - just pop it + insts.pop(-1) + elif len(returns) > 0: + # create jump target - if the last inst is a return, + # we can replace it with a NOP and make that the jump target. + if insts[-1] is returns[-1]: + insts[-1].opname = "NOP" + insts[-1].opcode = dis.opmap["NOP"] + insts[-1].arg = None + insts[-1].argval = _NotProvided + returns.pop(-1) + else: + insts.append(create_instruction("NOP")) + + # replace returns with jumps + for inst in returns: + # don't replace inst with new instruction + # due to targeting/exn table/etc. + jump_inst = create_jump_absolute(insts[-1]) + inst.opname = jump_inst.opname + inst.opcode = jump_inst.opcode + inst.arg = jump_inst.arg + inst.argval = jump_inst.argval + inst.target = jump_inst.target + + return insts diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/cache_size.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/cache_size.py new file mode 100644 index 0000000000000000000000000000000000000000..265643953aa358a593b3018a8975405718e695ab --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/cache_size.py @@ -0,0 +1,187 @@ +import logging +import weakref +from dataclasses import dataclass +from typing import Any + +from torch._guards import CompileId + +from . import config +from .types import DynamoFrameType + + +log: logging.Logger = logging.getLogger(__name__) +""" +[Note on cache size limit] + +Background - TorchDynamo cache is a linked list. Each cache entry is a +(guard_manager, out_code, next pointer). These are stored on the f_code's co_extra +scratch space. When a frame is invoked, we walk this linked list and run +guard_manager in each cache_entry to decide if the frame needs recompilation. If none +of the guard_manager's returns True, we recompile and add a new entry. To ensure we +don't end up recompiling infinitely, we put limits on the cache size. + +There are two limits +1) recompile_limit +2) accumulated_recompile_limit + + +Earlier we used to have only limit - maximum number of entries in 1 cache line +(which is now represented by (2) above). So, why do we need two limits? Lets try +to understand that. + +In general, we want our cache limit value to be a small number (e.g. 8 or even +lower). This ensures that for frames that cause too many recompilation fall to +eager quickly. However, there is another problem that prevents us from lowering +the value of recompile_limit. This is due to ID_MATCH'd guards. Today, we put +ID_MATCH guards on nn module if there is a graph break. This means we will have +many recompilations for the same code object because the ID_MATCH guard fails +for different instances of the nn module. This is a common pattern in how models +are authored. Therefore, this requires us to keep the recompile_limit high. + +We resolve this by introducing these two limits. The first limit (1) limits the +number of cache entries that have an ID_MATCH'd guard for an nn module instance. +And, (2)nd limit becomes a safeguard mechanism to have a maximum compilations +for a code object. One important question is - what is the limit for the code +object that does not have any ID_MATCH guard? For such code objects, we choose +(1) as the cache size limit. + +Lets take an example to understand how these limits help. Suppose, we have 16 +instances of a nn module and we ID_MATCH on the self object. Further, suppose +the inputs to these functions have varying batch size, leading to one +recompilation. In total, there will be 32 recompilations, and therefore 32 cache +entries on the forward code object. In the older case when we had only 1 limit, +our cache size limit must be >= 32 to capture all these recompilations. Now, +suppose there is a separate function in the same program which is very dynamic +and unsuitable for compilation. Such a function will need to undergo 32 +compilations to burst the cache and fallback to eager. These 32 recompilations +are too many and we want to fallback for these compilation-unfriendly functions +sooner. + +In the new scenario, we can have (1) recompile_limit = 2, (2) +accumulated_recompile_limit = 32. This means that each ID_MATCH'd object can +have maximum of two cache entries, and the maximum number of cache entries +(irrespective of ID_MATCH obj) is 32. This covers the case of forward code +object which has 32 recompilations. For the other function, the one unsuitable +for recompilation, our limit is 2. So, we will burst the cache in just 2 +recompilations. In this manner, these 2 limits help us resolve the tension +mentioned earlier. +""" + + +@dataclass +class CacheSizeRelevantForFrame: + """ + We track the number of cache entries that have same id_match objects as the + given frame. + + TODO(janimesh) - Consider adding a map from tuple_of_match_ids to count - + https://github.com/pytorch/pytorch/pull/107496#discussion_r1304564682 - this + could be useful for debugging as well. + """ + + # Total number of CacheEntry objects in the Dynamo linked list + num_cache_entries: int = 0 + + # Number of CacheEntry objects having same ID_MATCH'd objects as given frame. + num_cache_entries_with_same_id_matched_objs: int = 0 + + def will_compilation_exceed(self, limit: int) -> bool: + # Checks if a compilation will exceed the given limit (that's why >=). + return ( + self.will_compilation_exceed_accumulated_limit() + or self.will_compilation_exceed_specific_limit(limit) + ) + + def will_compilation_exceed_accumulated_limit(self) -> bool: + return self.num_cache_entries >= config.accumulated_recompile_limit + + def will_compilation_exceed_specific_limit(self, limit: int) -> bool: + return self.num_cache_entries_with_same_id_matched_objs >= limit + + +def _get_weakref_from_f_locals( + frame: DynamoFrameType, local_name: str +) -> weakref.ref[Any] | None: + obj = frame.f_locals.get(local_name, None) + weak_id = None + try: + weak_id = weakref.ref(obj) + except TypeError: + pass # cannot weakref bool object + return weak_id + + +def _has_same_id_matched_objs(frame: DynamoFrameType, cache_entry: Any) -> bool: + """ + Checks if the ID_MATCH'd objects saved on cache_entry are same as the ones + in frame.f_locals. + """ + if not cache_entry: + return False + + for ( + local_name, + weakref_from_cache_entry, + ) in cache_entry.guard_manager.id_matched_objs.items(): + if weakref_from_cache_entry() is not None: + weakref_from_frame = _get_weakref_from_f_locals(frame, local_name) + if weakref_from_frame is not weakref_from_cache_entry: + return False + + # Also covers the case where no ID_MATCH objects are saved in frame.f_locals + return True + + +def compute_cache_size( + frame: DynamoFrameType, cache_entry: Any +) -> CacheSizeRelevantForFrame: + # Walk the linked list to calculate the cache size + num_cache_entries = 0 + num_cache_entries_with_same_id_matched_objs = 0 + + while cache_entry: + num_cache_entries += 1 + # Track the number of cache entries having same ID_MATCH'd objects as + # that of frame.f_locals. This will be used later to compare against the + # recompile_limit. + if _has_same_id_matched_objs(frame, cache_entry): + num_cache_entries_with_same_id_matched_objs += 1 + cache_entry = cache_entry.next + + return CacheSizeRelevantForFrame( + num_cache_entries, num_cache_entries_with_same_id_matched_objs + ) + + +def is_recompilation(cache_size: CacheSizeRelevantForFrame) -> bool: + """ + If the frame (earlier parsed by compute_cache_size) has more than 1 cache + entry with same ID_MATCH'd objects, then its a recompilation. + """ + # Note that you can have multiple entries in the cache but still not a + # recompile, e.g., you can have 64 nn module instances, each one having an + # ID_MATCH guard, and each one having just 1 cache entry in the cache. In + # this case, we can have 64 entries in the cache, but no recompilation + # because there is only one entry for each id_matched_obj. + return cache_size.will_compilation_exceed(1) + + +def exceeds_recompile_limit( + cache_size: CacheSizeRelevantForFrame, compile_id: CompileId +) -> tuple[bool, str]: + """ + Checks if we are exceeding the cache size limit. + """ + if cache_size.will_compilation_exceed_accumulated_limit(): + return True, "accumulated_recompile_limit" + if cache_size.will_compilation_exceed_specific_limit(config.recompile_limit): + return True, "recompile_limit" + # NOTE this check is needed in the case that the frame's cache doesn't grow + # and we keep recompiling. This can happen if the guard guard_manager becomes invalidated, + # e.g. due to guarded objects being freed. This technically makes the + # will_compilation_exceed_accumulated_limit check unnecessary, but we will keep the + # check in case we have a better fix in the future. + assert compile_id.frame_compile_id is not None + if compile_id.frame_compile_id >= config.accumulated_recompile_limit: + return True, "accumulated_recompile_limit" + return False, "" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/callback.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/callback.py new file mode 100644 index 0000000000000000000000000000000000000000..25e9f260e34b3b054ba6552ec9d5fc64a61c20fb --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/callback.py @@ -0,0 +1,171 @@ +""" +This module provides callback management functionality for TorchDynamo's compilation process. + +It implements a thread-safe system for registering, managing and executing callbacks that run +at the start and end of TorchDynamo compilations. Key features include: + +- Registration and deregistration of compilation callbacks +- Thread-safe callback handling with proper locking mechanisms +- Prevention of duplicate callback execution when configured +- Decorator utilities for easy callback registration +- Context manager for controlled callback lifecycle + +The module centers around the CompilationCallbackHandler class which maintains separate +lists for start and end callbacks, manages their execution order, and ensures thread-safety. +Utility decorators @on_compile_start and @on_compile_end provide a convenient way to +register compilation hooks. + +Example usage: + @on_compile_start + def my_start_callback(): + print("Starting compilation") + + @on_compile_end + def my_end_callback(): + print("Compilation complete") +""" + +import enum +import threading +from collections.abc import Callable, Generator +from contextlib import contextmanager +from dataclasses import dataclass, field # noqa: F811 +from typing import Any + + +class CallbackTrigger(enum.Enum): + # most common case, dynamo attempts to trace a new frame + DYNAMO = 1 + # backward compilation can be deferred to runtime + LAZY_BACKWARD = 2 + # some backends autotune at runtime + TRITON_AUTOTUNING = 3 # Temporarily disabled due to spam + # cudagraphs record at runtime + CUDAGRAPH_RECORDING = 4 + + +@dataclass +class CallbackArgs: + callback_trigger: CallbackTrigger + compile_id: str + + +@dataclass +class CompilationCallbackHandler: + start_callbacks: list[Callable[[CallbackArgs], None]] = field(default_factory=list) + end_callbacks: list[Callable[[CallbackArgs], None]] = field(default_factory=list) + + __pending_callbacks_counter: int = field(default=0, init=False, repr=False) + __pending_callbacks_counter_lock: threading.Lock = field( + default_factory=threading.Lock, init=False, repr=False + ) + + def register_start_callback( + self, callback: Callable[[CallbackArgs], None] + ) -> Callable[[CallbackArgs], None]: + """ + Register a callback function to be called when the compilation starts. + + Args: + - callback (Callable): The callback function to register. + """ + self.start_callbacks.append(callback) + return callback + + def register_end_callback( + self, callback: Callable[[CallbackArgs], None] + ) -> Callable[[CallbackArgs], None]: + """ + Register a callback function to be called when the compilation ends. + + Args: + - callback (Callable): The callback function to register. + """ + self.end_callbacks.append(callback) + return callback + + def remove_start_callback(self, callback: Callable[[CallbackArgs], None]) -> None: + """ + Remove a registered start callback function. + + Args: + - callback (Callable): The callback function to remove. + """ + self.start_callbacks.remove(callback) + + def remove_end_callback(self, callback: Callable[[CallbackArgs], None]) -> None: + """ + Remove a registered end callback function. + + Args: + - callback (Callable): The callback function to remove. + """ + self.end_callbacks.remove(callback) + + def run_start_callbacks(self, args: CallbackArgs) -> None: + """ + Execute all registered start callbacks. + """ + for callback in self.start_callbacks: + callback(args) + + def run_end_callbacks(self, args: CallbackArgs) -> None: + """ + Execute all registered end callbacks. + """ + for callback in self.end_callbacks: + callback(args) + + @contextmanager + def install_callbacks( + self, trigger: CallbackTrigger, compile_id: str + ) -> Generator[None, Any, Any]: + """ + Context manager to install the callbacks and run them when the context is exited. + """ + args = CallbackArgs(trigger, compile_id) + try: + with self.__pending_callbacks_counter_lock: + self.__pending_callbacks_counter += 1 + if self.__pending_callbacks_counter == 1: + self.run_start_callbacks(args) + yield + finally: + with self.__pending_callbacks_counter_lock: + assert self.__pending_callbacks_counter > 0, ( + "Pending callbacks counter cannot become negative." + ) + if self.__pending_callbacks_counter == 1: + self.run_end_callbacks(args) + self.__pending_callbacks_counter -= 1 + + def clear(self) -> None: + """ + Clear all registered callbacks. + """ + self.start_callbacks.clear() + self.end_callbacks.clear() + assert self.__pending_callbacks_counter == 0 + + +callback_handler = CompilationCallbackHandler() + + +def on_compile_start( + callback: Callable[[CallbackArgs], None], +) -> Callable[[CallbackArgs], None]: + """ + Decorator to register a callback function for the start of the compilation. + """ + callback_handler.register_start_callback(callback) + return callback + + +def on_compile_end( + callback: Callable[[CallbackArgs], None], +) -> Callable[[CallbackArgs], None]: + """ + Decorator to register a callback function for the end of the compilation. + """ + callback_handler.register_end_callback(callback) + return callback diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/code_context.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/code_context.py new file mode 100644 index 0000000000000000000000000000000000000000..b4a0a55752828277a5ea30622bde5e5436c821ae --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/code_context.py @@ -0,0 +1,61 @@ +""" +This module provides thread-safe code context management for TorchDynamo using weak references. + +The CodeContextDict class maintains a mapping between Python code objects and their associated +context data, using weak references to automatically clean up entries when code objects are +garbage collected. This prevents memory leaks while allowing context data to be associated +with code objects throughout their lifecycle. + +Key features: +- Thread-safe context storage and retrieval +- Automatic cleanup using weak references +- Safe context management for Python code objects +- Memory-leak prevention + +Example usage: + code_obj = compile('x = 1', '', 'exec') + + # Store context + context = code_context.get_context(code_obj) + context['metadata'] = {'optimized': True} + + # Retrieve context + if code_context.has_context(code_obj): + ctx = code_context.get_context(code_obj) + # Use context data... + + # Remove context + ctx = code_context.pop_context(code_obj) +""" + +import types +from typing import Any + +from .utils import ExactWeakKeyDictionary + + +class CodeContextDict: + def __init__(self) -> None: + self.code_context: ExactWeakKeyDictionary = ExactWeakKeyDictionary() + + def has_context(self, code: types.CodeType) -> bool: + return code in self.code_context + + def get_context(self, code: types.CodeType) -> dict[str, Any]: + ctx = self.code_context.get(code) + if ctx is None: + # pyrefly: ignore [implicit-any] + ctx = {} + self.code_context[code] = ctx + return ctx + + def pop_context(self, code: types.CodeType) -> dict[str, Any]: + ctx = self.get_context(code) + self.code_context._remove_id(id(code)) + return ctx + + def clear(self) -> None: + self.code_context.clear() + + +code_context: CodeContextDict = CodeContextDict() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/codegen.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/codegen.py new file mode 100644 index 0000000000000000000000000000000000000000..3e0718a6fa28dc70c31c62990e9465647971eac7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/codegen.py @@ -0,0 +1,734 @@ +""" +This module provides utilities for generating Python bytecode in PyTorch's Dynamo system. +It includes functionality for: +- Constructing bytecode sequences for Python operations +- Managing stack operations and variable tracking +- Handling graph outputs and their conversions +- Supporting different Python versions (3.11+, 3.12+, 3.13+) +- Converting high-level operations to low-level bytecode instructions +- Managing constant loading and attribute access +- Supporting function creation and closure handling +""" + +import collections +import dataclasses +import re +import sys +import types +from collections import Counter, deque +from collections.abc import Callable, Iterable +from typing import Any, TYPE_CHECKING, Union + +import torch.nn +from torch.utils._ordered_set import OrderedSet + +from . import config, graph_break_hints, utils +from .bytecode_transformation import ( + add_push_null, + add_push_null_call_function_ex, + create_binary_subscr, + create_build_tuple, + create_call_function, + create_call_function_ex, + create_call_method, + create_dup_top, + create_instruction, + create_load_const, + create_load_method, + create_rot_n, + Instruction, +) +from .exc import unimplemented +from .source import AttrSource, ChainedSource, DictGetItemSource, Source +from .utils import is_safe_constant, rot_n_helper +from .variables.base import ValueMutationExisting, VariableTracker +from .variables.functions import ( + ContextlibContextManagerLocalGeneratorObjectVariable, + LocalGeneratorObjectVariable, +) +from .variables.nn_module import NNModuleVariable +from .variables.script_object import TorchScriptObjectVariable +from .variables.tensor import ( + NumpyNdarrayVariable, + SymNodeVariable, + TensorVariable, + UnspecializedPythonVariable, +) +from .variables.torch_function import TensorWithTFOverrideVariable + + +if TYPE_CHECKING: + from torch._dynamo.variables.builder import GraphArg + + from .symbolic_convert import InstructionTranslatorBase + + +@dataclasses.dataclass +class GraphOutputEntry: + index: int + variable: VariableTracker + + +class PyCodegen: + """ + Helper class uses for constructing Python bytecode + """ + + def __init__( + self, + tx: "InstructionTranslatorBase", + root: torch.nn.Module | None = None, + graph_output_var: str | None = None, + tempvars: dict[VariableTracker | Source, Any] | None = None, + overridden_sources: dict[Source, Source] | None = None, + ) -> None: + self.root = root + self.top_of_stack: VariableTracker | Source | None = None + self.uses: Counter[VariableTracker | Source] = collections.Counter() + self.graph_outputs: dict[int, GraphOutputEntry] = {} + self._output: list[Instruction] = [] + # This determines which VariableTracker/Source should be stored as + # locals, and maps the VariableTracker/Source to the local variable + # name. Note that it could map to None initially, in which case we'll + # overwrite it to map to real temporary names via `add_cache`. + self.tempvars: dict[VariableTracker | Source, Any] = tempvars or {} + self.tx = tx + self.graph_output_var = graph_output_var + self.code_options = self.tx.output.code_options + self.cell_and_freevars = self.tx.cell_and_freevars + self.new_var = self.tx.output.new_var + self.value_from_source: bool = True + # This serves as a way for codegen to use a different source; we need + # this because sometimes we can't easily modify the original source + # without affecting other components, e.g., guards. + self.overridden_sources: dict[Source, Source] = overridden_sources or {} + + def restore_stack( + self, stack_values: list[Any], *, value_from_source: bool = True + ) -> None: + prev = self.value_from_source + self.value_from_source &= value_from_source + try: + self.foreach(stack_values) + finally: + self.value_from_source = prev + + def graph_output_vars(self) -> list[VariableTracker]: + return [x.variable for x in self.graph_outputs.values()] + + def call_reconstruct( + self, value: Union[VariableTracker, Source, "GraphArg"] + ) -> None: + res = value.reconstruct(self) + assert res is None, f"reconstruct!=None {value}" + + def add_push_null( + self, gen_fn: Callable[[], None], call_function_ex: bool = False + ) -> None: + """ + `gen_fn` generates instructions via PyCodegen methods + that push a single callable to the stack. + + `add_push_null` pushes a NULL to the stack before or after the + instructions generated by `gen_fn`, depending on Python version. + + Will attempt to use the NULL push bit for instructions + with such bits (LOAD_GLOBAL 3.11+, LOAD_ATTR 3.12+, LOAD_SUPER_ATTR). + """ + old_len = len(self._output) + if sys.version_info < (3, 13): + # gen_fn may DUP_TOP instead if TOS is not cleared. + # Will cause problems since NULL will be pushed right + # before the generated instructions in <= 3.12 + self.clear_tos() + gen_fn() + # inplace modify self._output + added_insts = self._output[old_len:] + del self._output[old_len:] + if call_function_ex: + self._output.extend(add_push_null_call_function_ex(added_insts)) + else: + self._output.extend(add_push_null(added_insts)) + if sys.version_info >= (3, 13): + # NULL will be at top of stack + self.clear_tos() + + def __call__( + self, value: VariableTracker | Source | None, allow_cache: bool = True + ) -> None: + """ + Generate code such that top-of-stack (TOS) is set to value. + + `allow_cache` controls the behavior in the following manner. `value` can + either be a VariableTracker or a Source. + + If `value` is a `Source`, `allow_cache` must be True (invariant asserted + below). If the source was reconstructed earlier, we will reuse the + generated code by loading from top of stack or tempvars. + + If `value` is a `VariableTracker`, we have the following cases: + + 1) `allow_cache=True` + a) If the value.source is not None, we will emit the code based on + `value.source` to handle aliasing. + b) If value.source is None (example reconstructing a local list + returned by the compiled function), we will reconstruct the variable + tracker (w/o any source) to emit bytecode that generates a new + python object. + + In both cases of value.source being None or not, if the value was + reconstructed earlier, we will reuse the generated code by loading from + top of stack or tempvars. + + 2) `allow_cache=False` - This is a special case (allow_cache defaults to + True). + a) If the value.source is not None, we reconstruct the variable + tracker and emit a new python object. You might wonder what about + aliasing? The place where we use this config also has the followup + code where the original python object is assigned to this new python + value to handle aliasing (check side_effects.py and search for + allow_cache=False). + + b) If value.source is None, this is not allowed + + Notable effects: + 1. `self.top_of_stack` will be set to `value`, if we don't codegen + `value` based on source. + 2. `self.uses[value]` will increment, unless (a). we codegen via + `top_of_stack` or cached `tempvars`, or (b). `value` has special VT + types like `NNModuleVariable`, etc. + """ + assert value is not None + if isinstance(value, Source): + # If the source needs to be overridden, use the new one. + source = self.overridden_sources.get(value, value) + assert allow_cache is True, "allow_cache must be True for Source" + if self.top_of_stack is value: + self._output.append(create_dup_top()) + return + + if self.tempvars.get(source) is not None: + self._output.append(self.create_load(self.tempvars[source])) + self.top_of_stack = source + return + + self.uses[source] += 1 + try: + self.call_reconstruct(source) + except NotImplementedError: + unimplemented( + gb_type="Reconstruction failure: source.reconstruct not implemented", + context=str(source), + explanation=f"Dynamo has no bytecode reconstruction implemented for {type(source)} variable {source}.", + hints=[*graph_break_hints.DYNAMO_BUG], + ) + if source in self.tempvars: + self._output.append(create_dup_top()) + self.add_cache(source) + self.top_of_stack = source + + return + + assert isinstance(value, VariableTracker) + output = self._output + graph_outputs = self.graph_outputs + + if allow_cache: + if self.top_of_stack is value: + output.append(create_dup_top()) + return + + if self.tempvars.get(value) is not None: + output.append(self.create_load(self.tempvars[value])) + self.top_of_stack = value + return + + if value.is_realized() and isinstance( + value, ContextlibContextManagerLocalGeneratorObjectVariable + ): + unimplemented( + gb_type="reconstructing @contextmanager object", + context=f"object: {value}", + explanation="Returning a @contextmanager object from a compiled function is not supported.", + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + ) + + # Dynamo normally prefers codegen from source to account for aliasing. + if ( + value.source is not None + and allow_cache + and not ( + value.is_realized() and isinstance(value, LocalGeneratorObjectVariable) + ) + ): + # There's a corner case for export: for instance, if the computation + # graph is just identity on an input tensor, Dynamo would just emit + # a `LOAD_FAST` from the input source, rather than generating an + # identity FX graph. + # + # However, export wants to maximize graph capture; in the case + # above, export _wants to_ obtain an identity FX graph (despite it + # appears unnecessarily expensive for `torch.compile`), so we have + # the following option to override Dynamo's preference for codegen + # from source. Moreover, this option applies recursively, for cases + # like input tensor being returned in a new dictionary. + # + # And why the `ValueMutationExisting` check? Not sure, so leaving it + # to keep the old behavior, as when `value_from_source` was + # introduced. TODO sort out the invariants among side effect, + # codegen and export. + if ( + isinstance(value.mutation_type, ValueMutationExisting) + or self.value_from_source + ): + return self(value.source) + + if value.is_python_constant() and is_safe_constant(value.as_python_constant()): + output.append(self.create_load_const(value.as_python_constant())) + elif isinstance(value, TensorWithTFOverrideVariable): + graph_outputs_key = self.add_graph_output(value) + + self.add_push_null( + lambda: self.load_import_from(utils.__name__, "to_subclass") + ) + self.load_graph_output(graph_outputs[graph_outputs_key].index) + output.append( + self.create_load_global( + value.global_mangled_class_name(self.tx), # type: ignore[arg-type] + add=True, + ) + ) + output.extend(create_call_function(2, False)) + elif ( + isinstance(value, SymNodeVariable) + and value.python_type() is float + and not self.tx.export + ): + # This is a little unusual; force the output convention to be a + # Tensor here. Don't do this for export because this is + # apparently load bearing for export tests (but I am a bit + # doubtful it actually works in the real world) + # NB: It works to add_graph_output on a computed expression + # as_tensor here, because we memoize as_tensor calls on + # SymNodeVariable! + graph_outputs_key = self.add_graph_output( + value.as_tensor(self.tx, torch.float64) + ) + + def gen_fn() -> None: + self.load_graph_output(graph_outputs[graph_outputs_key].index) + output.append(self.create_load_attr("item")) + + self.add_push_null(gen_fn) + output.extend(create_call_function(0, False)) + elif isinstance( + value, + ( + TensorVariable, + SymNodeVariable, + UnspecializedPythonVariable, + NumpyNdarrayVariable, + TorchScriptObjectVariable, + ), + ): + graph_outputs_key = self.add_graph_output(value) + + if isinstance(value, NumpyNdarrayVariable): + self.add_push_null( + lambda: self.load_import_from(utils.__name__, "to_numpy_helper") + ) + self.load_graph_output(graph_outputs[graph_outputs_key].index) + output.extend(create_call_function(1, False)) + elif isinstance(value, UnspecializedPythonVariable) and value.need_unwrap: + + def gen_fn() -> None: + self.load_graph_output(graph_outputs[graph_outputs_key].index) + output.append(self.create_load_attr("item")) + + self.add_push_null(gen_fn) + output.extend(create_call_function(0, False)) + else: + self.load_graph_output(graph_outputs[graph_outputs_key].index) + elif isinstance(value, NNModuleVariable): + parts = value.module_key.split(".") + if parts[0] in self.code_options["co_varnames"]: + output.append(self.create_load(parts[0])) + parts = parts[1:] + else: + assert self.root is not None + output.append(self.create_load_const_unchecked(self.root)) + for part in parts: + output.append(self.create_load_attr(part)) + else: + self.uses[value] += 1 + try: + self.call_reconstruct(value) + except NotImplementedError as e: + unimplemented( + gb_type="Reconstruction failure", + context=str(value), + explanation=f"Dynamo has no bytecode reconstruction implemented for sourceless variable {value}.", + hints=[ + "If Dynamo is attempting to trace a return statement and your code is attempting to return a variable " + "that Dynamo cannot reconstruct, then remove it from the return statement.", + *graph_break_hints.CAUSED_BY_EARLIER_GRAPH_BREAK, + "Report an issue to PyTorch if you need reconstrtuction support. Note that objects that don't have " + "reconstruction rules may be fundamentally unreconstructable.", + ], + from_exc=e, + ) + if allow_cache and value in self.tempvars: + self._output.append(create_dup_top()) + self.add_cache(value) + + self.top_of_stack = value + + def add_graph_output(self, value: VariableTracker) -> int: + graph_outputs_key = id(value.as_proxy()) + if graph_outputs_key not in self.graph_outputs: + self.graph_outputs[graph_outputs_key] = GraphOutputEntry( + len(self.graph_outputs), value + ) + return graph_outputs_key + + def load_graph_output(self, index: int) -> None: + output = self._output + assert self.graph_output_var is not None + output.append(self.create_load(self.graph_output_var)) + output.append(self.create_load_const(index)) + output.append(self.create_binary_subscr()) + + def add_cache(self, value: VariableTracker | Source) -> None: + var = self.new_var() + self.tempvars[value] = var + self._output.append(self.create_store(var)) + + def foreach(self, items: Iterable[VariableTracker | Source]) -> None: + for i in items: + self(i) + + def create_binary_subscr(self) -> Instruction: + return create_binary_subscr() + + def setup_globally_cached(self, name: str, value: Any) -> list[Instruction]: + """Store value in a new global""" + name = re.sub(r"[^a-zA-Z0-9_]+", "_", name) + f_globals = self.tx.f_globals + if name in f_globals: + assert id(f_globals[name]) == id(value) + else: + f_globals[name] = value + return [self.create_load_global(name, add=True)] + + def clear_tos(self) -> None: + self.top_of_stack = None + + def append_output(self, inst: Instruction) -> None: + assert isinstance(inst, Instruction) + self._output.append(inst) + self.clear_tos() + + def extend_output(self, insts: list[Instruction]) -> None: + assert all(isinstance(x, Instruction) for x in insts) + self._output.extend(insts) + self.clear_tos() + + def get_instructions(self) -> list[Instruction]: + return self._output + + def create_load(self, name: str) -> Instruction: + assert name in self.code_options["co_varnames"], f"{name} missing" + return create_instruction("LOAD_FAST", argval=name) + + def create_load_closure(self, name: str) -> Instruction: + assert name in self.cell_and_freevars() + inst_name = "LOAD_FAST" if sys.version_info >= (3, 13) else "LOAD_CLOSURE" + return create_instruction(inst_name, argval=name) + + def create_load_deref(self, name: str) -> Instruction: + assert name in self.cell_and_freevars() + return create_instruction("LOAD_DEREF", argval=name) + + def create_store(self, name: str) -> Instruction: + assert name in self.code_options["co_varnames"], f"{name} missing" + return create_instruction("STORE_FAST", argval=name) + + def create_store_deref(self, name: str) -> Instruction: + assert name in self.cell_and_freevars() + return create_instruction("STORE_DEREF", argval=name) + + def create_load_global(self, name: str, add: bool = False) -> Instruction: + if add: + self.tx.output.update_co_names(name) + assert name in self.code_options["co_names"], f"{name} not in co_names" + return create_instruction("LOAD_GLOBAL", argval=name) + + def create_load_const(self, value: Any) -> Instruction: + return create_load_const(value) + + def create_load_const_unchecked(self, value: Any) -> Instruction: + return create_load_const(value, checked=False) + + def load_method(self, name: str) -> None: + self.tx.output.update_co_names(name) + self.append_output(create_load_method(name)) + + def call_method(self, nargs: int) -> None: + self.extend_output(create_call_method(nargs)) + + def create_load_attr(self, name: str) -> Instruction: + if name not in self.code_options["co_names"]: + self.code_options["co_names"] += (name,) + return create_instruction("LOAD_ATTR", argval=name) + + def load_attr(self, name: str) -> None: + self.append_output(self.create_load_attr(name)) + + def create_load_attrs(self, names: str) -> list[Instruction]: + return [self.create_load_attr(name) for name in names.split(".")] + + def create_store_attr(self, name: str) -> Instruction: + if name not in self.code_options["co_names"]: + self.code_options["co_names"] += (name,) + return create_instruction("STORE_ATTR", argval=name) + + def store_attr(self, name: str) -> None: + self.append_output(self.create_store_attr(name)) + + def load_function_name( + self, fn_name: str, push_null: bool, num_on_stack: int = 0 + ) -> list[Instruction]: + """Load the global fn_name on the stack num_on_stack down""" + output = [] + if push_null and sys.version_info >= (3, 11): + output.extend(add_push_null(self.create_load_global(fn_name, add=True))) + if num_on_stack > 0: + output.extend( + [ + *self.rot_n(num_on_stack + 2), + *self.rot_n(num_on_stack + 2), + ] + ) + else: + output.extend( + [ + self.create_load_global(fn_name, add=True), + *self.rot_n(num_on_stack + 1), + ] + ) + return output + + def rot_n(self, n: int) -> list[Instruction]: + try: + return create_rot_n(n) + except AttributeError: + # desired rotate bytecode doesn't exist, generate equivalent bytecode + return [ + create_build_tuple(n), + self.create_load_const_unchecked(rot_n_helper(n)), + *create_rot_n(2), + *create_call_function_ex(False, False), + create_instruction("UNPACK_SEQUENCE", arg=n), + ] + + def pop_null(self) -> list[Instruction]: + # POP_TOP doesn't work for null, so we pop nulls by pushing in a + # nop function, calling it (which consumes the null), and popping the result. + assert sys.version_info >= (3, 11) + return [ + self.create_load_const_unchecked(lambda: None), + # 3.13 swapped NULL and callable + *( + (create_instruction("SWAP", arg=2),) + if sys.version_info >= (3, 13) + else () + ), + *create_call_function(0, False), + create_instruction("POP_TOP"), + ] + + def pop_top(self) -> None: + self.append_output(create_instruction("POP_TOP")) + + def call_function(self, nargs: int, push_null: bool) -> None: + self.extend_output(create_call_function(nargs, push_null=push_null)) + + def dup_top(self) -> None: + self.append_output(create_dup_top()) + + def store(self, varname: str) -> None: + self.append_output(self.create_store(varname)) + + def load_deref(self, varname: str) -> None: + self.append_output(self.create_load_deref(varname)) + + def make_function_with_closure( + self, + fn_name: str, + code: types.CodeType, + ) -> None: + """Creates a closure with code object `code`. + + Expects the TOS to be the tuple of cells to use for this closure. + TOS will be popped to create the closure. + Args: + - fn_name: name of the function + - code: code object of the function + (does not include the tuple of cells on the TOS) + """ + output = self._output + + output.append(self.create_load_const(code)) + if sys.version_info < (3, 11): + output.append(self.create_load_const(fn_name)) + if sys.version_info >= (3, 13): + output.extend( + [ + create_instruction("MAKE_FUNCTION"), + create_instruction("SET_FUNCTION_ATTRIBUTE", arg=0x08), + ] + ) + else: + output.append(create_instruction("MAKE_FUNCTION", arg=0x08)) + + self.clear_tos() + + def create_load_python_module(self, mod: types.ModuleType) -> Instruction: + """ + Generate a LOAD_GLOBAL instruction to fetch a given python module. + """ + output = self.tx.output + global_scope = output.global_scope + name = re.sub(r"^.*[.]", "", mod.__name__) + if global_scope.get(name, None) is mod: + return self.create_load_global(name, add=True) + prefix = f"___module_{name}" + global_name = self.tx.output.install_global_by_id(prefix, mod) + return self.create_load_global(global_name, add=True) + + def mark_source_temp(self, source: Source) -> None: + """ + Mark a source as a temp variable, so that it can be reused. + """ + if source not in self.tempvars: + self.tempvars[source] = None + + def make_call_generated_code(self, fn_name: str) -> None: + """Call the generated code function stored in fn_name""" + self.extend_output(self.load_function_name(fn_name, True)) + + graphargs = self.tx.output.graphargs + + def extract_nested_sources(source: Source) -> list[Source]: + nested_sources: list[Source] = [] + if isinstance(source, ChainedSource): + nested_sources.append(source.base) + if isinstance(source, DictGetItemSource) and isinstance( + source.index, Source + ): + nested_sources.append(source.index) + return nested_sources + + def collect_temp_sources(sources: deque[Source], codegen: PyCodegen) -> None: + seen_sources: OrderedSet[Source] = OrderedSet() + while sources: + current_source = sources.popleft() + if current_source in seen_sources: + # This source is used at least twice, so it can be reused + codegen.mark_source_temp(current_source) + # Dont trace source further. This prevents us from marking too + # many nodes as temp sources. + continue + seen_sources.add(current_source) + sources.extend(extract_nested_sources(current_source)) + + # Collect all the sources that are used more than once, so that we can + # generate tmp variables in the generated pre-graph bytecode. This + # essentially implements CSE. + collect_temp_sources( + deque([arg.source for arg in graphargs if arg.source is not None]), self + ) + + cm_var = None + if config.record_runtime_overhead: + # Record the pregraph bytecode start + self.add_push_null( + lambda: self.load_import_from( + utils.__name__, "record_pregraph_bytecode_enter" + ) + ) + self.extend_output(create_call_function(0, False)) + cm_var = self.new_var() + self.store(cm_var) + + for arg in graphargs: + if arg.pass_arg_as_tensor: + self.add_push_null( + lambda: self.extend_output( + [ + self.create_load_python_module(torch), + self.create_load_attr("_as_tensor_fullprec"), + ] + ) + ) + self.call_reconstruct(arg) + self.extend_output(create_call_function(1, False)) + else: + self.call_reconstruct(arg) + + if config.record_runtime_overhead: + # Record the pregraph bytecode end + self.add_push_null( + lambda: self.load_import_from( + utils.__name__, "record_pregraph_bytecode_exit" + ) + ) + assert cm_var is not None + self.extend_output([self.create_load(cm_var)]) + self.extend_output(create_call_function(1, False)) + self.pop_top() + + self.extend_output(create_call_function(len(graphargs), False)) + + def create_import_name(self, module_name: str) -> Instruction: + return create_instruction("IMPORT_NAME", argval=module_name) + + def load_import_from(self, module_name: str, object_name: str) -> None: + source = AttrSource(self.tx.import_source(module_name), object_name) + # Note: This approach is somewhat aggressive because typically, a source is marked + # as a tempvar only when it is used more than once. In this case, we're marking it + # as a tempvar without performing that analysis. However, this is a simple solution, + # and in many cases, load imports are reused multiple times. + self.mark_source_temp(source) + self(source) + + def create_call_function_kw( + self, nargs: int, kw_names: Iterable[str], push_null: bool + ) -> list[Instruction]: + if sys.version_info >= (3, 13): + output = create_call_function(nargs, push_null) + assert output[-1].opname == "CALL" + output.insert(-1, self.create_load_const(kw_names)) + output[-1] = create_instruction("CALL_KW", arg=nargs) + return output + elif sys.version_info >= (3, 11): + output = create_call_function(nargs, push_null) + if sys.version_info >= (3, 12): + idx = -1 + expected_inst = "CALL" + else: + idx = -2 + expected_inst = "PRECALL" + assert output[idx].opname == expected_inst + kw_names_inst = create_instruction("KW_NAMES", argval=kw_names) + output.insert(idx, kw_names_inst) + return output + return [ + self.create_load_const(kw_names), + create_instruction("CALL_FUNCTION_KW", arg=nargs), + ] + + def create_delete(self, value: object) -> Instruction: + return create_instruction("DELETE_FAST", argval=value) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/compiled_autograd.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/compiled_autograd.py new file mode 100644 index 0000000000000000000000000000000000000000..b6d84483007a62457fe02d6369090b5c51edd2a0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/compiled_autograd.py @@ -0,0 +1,1638 @@ +""" +Provides functionality for compiling PyTorch's autograd (automatic differentiation) system. + +This module implements compiled autograd, which traces and optimizes backward pass +computations at runtime. The key components are: + +- AutogradCompilerInstance: Traces and compiles autograd graphs using FX +- Context managers (_enable/_disable): Control when compiled autograd is active +- Utility functions: Support graph manipulation, tensor operations, and hooks + +Compiled autograd can significantly improve backward pass performance by removing +Python overhead and enabling additional optimizations. It works by capturing +backward computations into an FX graph that can be compiled and optimized, +while maintaining the same semantics as eager mode autograd. +""" + +import contextlib +import functools +import itertools +import operator +import time +from collections import Counter, defaultdict +from collections.abc import Callable, Generator, Sequence +from typing import Any, TYPE_CHECKING + +import torch +import torch.utils._pytree as pytree +from torch._dispatch.python import enable_python_dispatcher +from torch._dynamo.external_utils import ( + call_accumulate_grad, + call_backward, + call_hook, + FakeCompiledAutogradEngine, + unwrap_maybe_dynamic_int, +) +from torch._dynamo.source import GetItemSource, LocalSource +from torch._dynamo.utils import ( + counters, + get_chromium_event_logger, + lazy_format_graph_code, + set_locals_to_steal, +) +from torch._functorch._aot_autograd.runtime_wrappers import ( + AutogradLazyBackwardCompileInfo, + CachedAutogradLazyBackwardCompileInfo, +) +from torch._guards import compile_context, CompileContext, CompileId, Source +from torch._logging import getArtifactLogger, trace_structured +from torch._prims_common import clone_preserve_strides +from torch._subclasses import FakeTensorMode +from torch._subclasses.fake_tensor import FakeTensor +from torch.fx import GraphModule +from torch.fx.experimental._backward_state import BackwardState +from torch.fx.experimental.proxy_tensor import ( + decompose, + disable_autocast_cache, + disable_proxy_modes_tracing, + fetch_object_proxy, + ProxyTorchDispatchMode, + PythonKeyTracer, + track_tensor_tree, +) +from torch.fx.experimental.symbolic_shapes import DimDynamic, ShapeEnv +from torch.fx.traceback import preserve_node_meta, set_stack_trace +from torch.types import FloatLikeType, IntLikeType +from torch.utils._ordered_set import OrderedSet +from torch.utils._traceback import CapturedTraceback + + +if TYPE_CHECKING: + from torch.fx.proxy import Proxy + + +TURN_OFF_MSG = """You can turn off compiled autograd by either: +1. Moving the unsupported autograd call outside of the torch.compile'd region. +2. Wrapping the unsupported autograd call in the torch._dynamo.compiled_autograd._disable() context manager. +3. Setting torch._dynamo.config.compiled_autograd=False for the torch.compile call containing the unsupported autograd call. +4. Setting torch._dynamo.config.compiled_autograd=False at the start of the program.""" + +compiled_autograd_log = getArtifactLogger(__name__, "compiled_autograd") +verbose_log = getArtifactLogger(__name__, "compiled_autograd_verbose") + + +def snapshot_verbose_logging_enabled() -> bool: + return torch._logging._internal.log_state.is_artifact_enabled( + "compiled_autograd_verbose" + ) + + +def snapshot_cudagraph_enabled() -> bool: + return torch._inductor.config.triton.cudagraphs + + +def maybe_clone(x: torch.Tensor | None) -> torch.Tensor | None: + if x is not None: + return clone_preserve_strides(x) + return x + + +def extract_bw_module(CompiledFunction: Any) -> Callable[..., Any]: + if isinstance( + CompiledFunction._lazy_backward_info, AutogradLazyBackwardCompileInfo + ): + return CompiledFunction._lazy_backward_info.bw_module + elif isinstance( + CompiledFunction._lazy_backward_info, CachedAutogradLazyBackwardCompileInfo + ): + with torch._subclasses.fake_tensor.unset_fake_temporarily(): + return CompiledFunction._lazy_backward_info.bw_module_fn() + else: + raise AssertionError( + "Unexpected Lazy Backward Compilation Info Type. Please file an issue." + ) + + +# Note: [Anomaly Mode Semantics in Compiled Autograd] +# In the eager autograd engine, anomaly mode is able to detect NaNs +# after each node. This is useful, because the executed code with +# and without anomaly mode are the same. So assuming determinism, +# a NaN in regular mode should also happen in anomaly mode. +# +# With torch.compile, following eager semantics would require inserting +# runtime asserts to check for NaNs, which could prevent some fusions. +# This results in different code being run with and without anomaly mode. +# So different semantics are needed, this implementation below will check +# for NaNs at the end of the autograd call, instead of after each node +class NaNChecker: + def __init__(self, accumulate_grad: bool) -> None: + self.accumulate_grad = accumulate_grad + self.params_indices: list[int] = [] + self.params_to_check: dict[str, torch.Tensor] = {} + self.output_names: list[str] = [] + + def prep_with_graph(self, graph: torch.fx.Graph) -> None: + inputs_node = next(iter(graph.nodes)) + acc_grad_nodes = graph.find_nodes( + op="call_function", target=call_accumulate_grad + ) + output_nodes = graph.find_nodes(op="output")[0].args[0] + assert self.accumulate_grad == bool( + acc_grad_nodes + ) and self.accumulate_grad == (not output_nodes) + + for node in acc_grad_nodes: + param_node = node.args[0] + # AccumulateGrad always saves a reference to the param + # so Compiled Autograd will always lift the param and + # this should always be true + assert ( + param_node.target is operator.getitem + and param_node.args[0] is inputs_node # type: ignore[possibly-undefined] + and isinstance(param_node.args[1], int) + ) + self.params_indices.append(param_node.args[1]) + + self.output_names = [node.name for node in output_nodes] + + def prep_with_inputs(self, inputs: tuple[torch.Tensor, ...]) -> None: + if not self.accumulate_grad: + # Using .grad, nothing to prep + return + + # Using .backward, we must check existing grads on params if any + for idx in self.params_indices: + grad = inputs[idx].grad + if grad is not None: + assert not torch.isnan(grad).any(), ( + f"Compiled autograd running under anomaly mode with inputs[{idx}] already " + f"having NaN gradient. This is not supported. {TURN_OFF_MSG}" + ) + + self.params_to_check[f"inputs[{idx}]"] = inputs[idx] + + def check(self, out: tuple[torch.Tensor, ...]) -> None: + if self.accumulate_grad: + # Using .backward, graph outputs are empty + assert not out + nan_params: list[str] = [] + for inputs_str, param in self.params_to_check.items(): + assert param.grad is not None # not true for autograd.grad + if torch.isnan(param.grad).any(): + nan_params.append(inputs_str) + + if nan_params: + raise RuntimeError( + f"Compiled Autograd returned NaN gradients for parameters: {','.join(nan_params)}." + ) + else: + # Using .grad, graph outputs are grads + nan_grads: list[str] = [] + for i, grad in enumerate(out): + if torch.isnan(grad).any(): + nan_grads.append(self.output_names[i]) + + if nan_grads: + raise RuntimeError( + f"Compiled Autograd returned NaN gradients for output nodes: {','.join(nan_grads)}." + ) + + +# We lazily bind "functional backward" variants for PyTorch built-in autograd +# nodes to this class. Example: torch._dynamo.compiled_autograd.ops.MulBackward0 +# Each "functional backward" is bound the first time the node's apply_with_saved +# function is called. It's possible to avoid lazy binding and instead bind +# all of this upfront (perhaps at import time) via codegen changes. +class OpNamespace: + def __init__(self) -> None: + self.custom_function_name_counter: Counter[str] = Counter() + + def add( + self, + name: str, + fn: Callable[..., Any], + is_custom_function: bool, + is_traceable: bool, + ) -> str: + if is_custom_function: + name = "CppNode" + name + count = self.custom_function_name_counter[name] + self.custom_function_name_counter[name] += 1 + name = f"{name}{count}" + + assert not hasattr(self, name) + result = Op(name, fn, is_custom_function) + if is_traceable: + # pyrefly: ignore [deprecated] + setattr(self, name, torch._dynamo.allow_in_graph(result)) + else: + # C++ autograd function was not marked as traceable + # Dynamo can't dry run it at compile time, so must fallback to eager + @torch._dynamo.disable # type: ignore[misc] + def run_non_traceable_cpp_in_eager(*args: Any, **kwargs: Any) -> Any: + return result(*args, **kwargs) + + setattr(self, name, run_non_traceable_cpp_in_eager) + return name + + def get(self, name: str) -> Any: + return getattr(self, name) + + +class Op: + def __init__( + self, name: str, fn: Callable[..., Any], is_custom_function: bool + ) -> None: + self.fn = fn + self.is_custom_function = is_custom_function + self.__name__ = name + self.__module__ = "torch._dynamo.compiled_autograd.ops" + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + return self.fn(*args, **kwargs) + + def __repr__(self) -> str: + return self.__module__ + "." + self.__name__ + + +ops = OpNamespace() + + +_graph_placeholders = ["inputs", "sizes", "scalars", "hooks", "packed_data"] +_impure_targets = OrderedSet( + [ + call_hook, + call_backward, + FakeCompiledAutogradEngine._exec_final_callbacks_stub, + call_accumulate_grad, + ] +) + +COMPILE_COUNTER = itertools.count() + + +def make_compile_context(compiled_autograd_id: int) -> Any: + return compile_context( + CompileContext( + CompileId( + compiled_autograd_id=compiled_autograd_id, + frame_id=None, + frame_compile_id=None, + ) + ) + ) + + +class AutogradCompilerInstance: + def __init__(self, compiler_fn: Callable[..., Any]) -> None: + self.compiler_fn = compiler_fn + self.stack = contextlib.ExitStack() + self.close = self.stack.close + self.shape_env = ShapeEnv() + self.fake_tensor_mode = FakeTensorMode( + allow_fallback_kernels=True, + allow_non_fake_inputs=True, + shape_env=self.shape_env, + ) + self.fx_tracer = PythonKeyTracer() + self.proxy_mode = ProxyTorchDispatchMode(self.fx_tracer, "symbolic") + self.hooks_proxy: Proxy | None = None + + def wrap_fake(self, x: torch.Tensor, source: Source | None) -> FakeTensor: + assert isinstance(x, torch.Tensor) + return self.fake_tensor_mode.from_tensor(x, source=source) + + @staticmethod + def source(name: str, idx: Any) -> GetItemSource: + return GetItemSource(LocalSource(name), idx) + + def begin_capture( + self, + inputs: list[torch.Tensor], + sizes: list[int], + scalars: list[IntLikeType | FloatLikeType], + origins: list[list[tuple[int, str]]], + accumulate_grad: bool, + check_nans: bool, + ) -> tuple[str, list[torch.Tensor], list[IntLikeType], list[FloatLikeType]]: + counters["compiled_autograd"]["captures"] += 1 + self.id = next(COMPILE_COUNTER) + self.aot_id_counter: dict[int, int] = defaultdict(int) + self.compile_context = make_compile_context(self.id) + self.compile_context.__enter__() + self.nan_checker = NaNChecker(accumulate_grad) if check_nans else None + self.start_time_ns = time.time_ns() + get_chromium_event_logger().log_event_start( + "compiled_autograd", + self.start_time_ns, + {"graph_id": self.id}, + log_pt2_compile_event=True, + ) + self.fx_tracer.root = torch.nn.Module() + self.fx_tracer.graph = torch.fx.Graph(tracer_cls=PythonKeyTracer) + self.fx_tracer.tensor_attrs = {} + self.symnode_proxy_lookup = {} + ( + args_proxy, + self.sizes_proxy, + self.scalars_proxy, + self.hooks_proxy, + self.packed_data_proxy, + ) = ( + self.fx_tracer.create_proxy("placeholder", name, (), {}) + for name in _graph_placeholders + ) + + self.stack.enter_context(preserve_node_meta()) + inputs_origins, sizes_origins, scalars_origins = origins + + # Turn on PythonDispatcher during initial trace to make it identifiable + # that tracing is happening, which is needed to prevent hashing symints + self.stack.enter_context(enable_python_dispatcher()) + + # tensor inputs to fake tensors + x = inputs[0] # mypy will complain about unbound x + try: + for idx, x in enumerate(inputs): + inputs[idx] = self.wrap_fake(x, self.source("inputs", idx)) + except Exception as e: + raise NotImplementedError( + f"Found tensor of type {type(x)}, which is not supported by FakeTensorMode. {TURN_OFF_MSG}" + ) from e + self.bind_objects_to_proxies(inputs, args_proxy, inputs_origins) + + # size inputs to symints + sym_sizes = [ + self.shape_env.create_unspecified_symint_and_symbol( + val, + self.source("sizes", idx), + DimDynamic.DYNAMIC, + ) + for idx, val in enumerate(sizes) + ] + + # We want to mark every size as dynamic, but since there's no way to + # mark a primitive `int` as dynamic, we need to wrap it in a tensor. + # In the graph, we unwrap it with `unwrap_maybe_dynamic_int` back into a primitive. + proxies = [self.sizes_proxy[i] for i in range(len(sym_sizes))] # type: ignore[index] + for i, symint in enumerate(sym_sizes): + proxies[i] = self.fx_tracer.create_proxy( + "call_function", + unwrap_maybe_dynamic_int, + (proxies[i],), + {}, + ) + if not isinstance(symint, int): + self.symnode_proxy_lookup[symint.node] = proxies[i] + proxies = self.bind_objects_to_proxies(sym_sizes, proxies, sizes_origins) + + for idx, val in enumerate(scalars): + source = self.source("scalars", idx) + if isinstance(val, int): + scalars[idx] = self.shape_env.create_unspecified_symint_and_symbol( + val, + source, + DimDynamic.DYNAMIC, + ) + elif isinstance(val, float): + scalars[idx] = self.shape_env.create_symfloatnode( + self.shape_env.create_unspecified_symbol( + val, + source=source, + dynamic_dim=DimDynamic.DYNAMIC, + ), + hint=val, + source=source, + ) + else: + raise AssertionError("Unexpected scalar type: ", type(val)) + self.bind_objects_to_proxies(scalars, self.scalars_proxy, scalars_origins) + for i, symval in enumerate(scalars): + self.symnode_proxy_lookup[symval.node] = self.scalars_proxy[i] # type: ignore[union-attr] + + # TODO(jansel): are all these modes needed? + self.stack.enter_context(decompose({})) + self.stack.enter_context(self.fake_tensor_mode) + self.stack.enter_context(self.proxy_mode) + self.stack.enter_context(disable_autocast_cache()) + # Needed to make sure we don't accidentally specialize any symbols + assert self.fake_tensor_mode.shape_env is not None + env = self.fake_tensor_mode.shape_env + self.stack.enter_context( + torch.fx.experimental.symbolic_shapes._suppress_guards(env) + ) + # pyrefly: ignore [bad-return] + return ( + str(CompileContext.current_compile_id()), + inputs, + sym_sizes, + scalars, # type: ignore[return-value] + ) + + def log_compile_reasons( + self, + compile_reasons: list[str], + ) -> None: + assert compile_reasons + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "compiled_autograd_compile_reasons", + "encoding": "json", + }, + payload_fn=lambda: compile_reasons, + ) + + def proxy_call_aot_backward( + self, + pinputs: Sequence[Any], + psaved_tensors: Sequence[torch.Tensor], + saved_tensors: Sequence[torch.Tensor], + pctx: Any, + ctx: Any, + maybe_backward_state_idx: int | None, + opaque_object_indices: list[int], + ) -> Sequence[Any]: + # The AOTBackward call consists of three things: the prologue, the + # backward graph, and the epilogue. + # Our strategy is: + # - allow_in_graph the prologue (in the CA graph and Dynamo graph), + # - copy-paste the backward graph into the CA graph so that CA passes and Dynamo can see it + # - trace directly through the epilogue. Anything that gets baked in is + # constant metadata (for example, metadata about the number of outputs, or removing + # RNG arguments or effect tokens). + # If Dynamo graph capture were better, then we could add a node for the prologue + # into the CA graph and have Dynamo trace into it. + + psymints = [self.to_proxy(e) for e in ctx._get_compiled_autograd_symints()] + + popaque_objects = [ + self.hooks_proxy[idx] # type: ignore[index] + for idx in opaque_object_indices + ] + + # NOTE: we should only close over constants + CompiledFunction = ctx._forward_cls + bw_module = extract_bw_module(CompiledFunction) + metadata = CompiledFunction.metadata + maybe_subclass_metadata = CompiledFunction.maybe_subclass_metadata + aot_id = CompiledFunction._aot_id + del CompiledFunction + + if torch.is_grad_enabled(): + for output_alias_info in metadata.output_info: + if output_alias_info.requires_grad: + raise RuntimeError( + "torch.compile does not currently support higher order gradients." + ) + + @torch._dynamo.allow_in_graph # type: ignore[misc] + def call_aot_bwd_prologue( + ctx_saved_tensors: Sequence[torch.Tensor], + ctx_symints: Sequence[IntLikeType], + ctx_opaque_objs: Sequence[Any], + *flat_args: Sequence[Any], + ) -> Any: + out = torch._functorch._aot_autograd.runtime_wrappers._backward_prologue_functional( + ctx_saved_tensors, + ctx_symints, + ctx_opaque_objs, + metadata, + maybe_subclass_metadata, + *flat_args, + ) + return out + + pgrads = self.fx_tracer.create_proxy( + kind="call_function", + # pyrefly: ignore [bad-argument-type] + target=call_aot_bwd_prologue, + args=( + psaved_tensors, + psymints, + popaque_objects, + *pinputs, + ), + kwargs={}, + ) + + pbackward_state = None + if maybe_backward_state_idx is not None: + pbackward_state = self.hooks_proxy[maybe_backward_state_idx] # type: ignore[index] + + # Copy-paste the AOT backward graph into the compiled autograd graph + def copy_paste_aot_backward_graph() -> list[torch.Tensor]: + def num_inputs(graph: torch.fx.Graph) -> int: + num_args = 0 + for node in graph.nodes: + if node.op == "placeholder": + num_args += 1 + continue + else: + break + return num_args + + # set up the proxy inputs to bw_module + # the calling convention is: [*symints, *args (primals and tangents), backward_state] + num_args = num_inputs(bw_module.graph) # type: ignore[attr-defined] + pall_args = [ + pgrads[i] for i in range(num_args - int(pbackward_state is not None)) + ] + # replace the symints with our symints + symints = ctx._get_compiled_autograd_symints() + assert len(symints) == len(ctx.symints) + psymints = [self.to_proxy(e) for e in symints] + pall_args[: len(symints)] = psymints + # Add backward_state + if pbackward_state is not None: + pall_args.append(pbackward_state) + + # run over all nodes of the aot_backward graph. + # copy and paste them all into the compiled autograd graph. + args_idx = 0 + # pyrefly: ignore [implicit-any] + value_remap = {} + poutputs: list[torch.fx.Proxy] | None = None + + # names of nodes must appear only once in the fx.Graph + # dedup AOT backwards that appear multiple times + deduped_aot_id = str(aot_id) + if self.aot_id_counter[aot_id]: + deduped_aot_id += f"_{self.aot_id_counter[aot_id]}" + self.aot_id_counter[aot_id] += 1 + + def make_unique(node_name: str) -> str: + # make it both informative and unique + return f"aot{deduped_aot_id}_{node_name}" + + for node in bw_module.graph.nodes: # type: ignore[attr-defined] + if node.op == "placeholder": + ph = pall_args[args_idx].node + ph.name = make_unique(node.name) + value_remap[node] = ph + args_idx += 1 + elif node.op == "output": + assert len(node.args) == 1 + poutputs = [ + torch.fx.Proxy(value_remap[n], self.fx_tracer) + if isinstance(n, torch.fx.Node) + else n + for n in node.args[0] + ] + elif node.op == "get_attr": + name = node.target + qualname = self.fx_tracer.get_fresh_qualname(name) + setattr(self.fx_tracer.root, qualname, getattr(bw_module, name)) + result = self.fx_tracer.create_node("get_attr", qualname, (), {}) + result.name = make_unique(node.name) + value_remap[node] = result + elif node.op == "call_function": + if node.target is torch.ops.aten.view.default: + # this aot bwd graph is being lazily compiled + # we must manually apply the view_to_reshape post grad pass + # since it was already applied to the aot fwd, and baked into the gradients + node.target = torch.ops.aten.reshape.default + result = self.fx_tracer.graph.node_copy( + node, lambda n: value_remap[n] + ) + result.name = make_unique(node.name) + value_remap[node] = result + elif node.op == "call_module": + name = node.target + qualname = self.fx_tracer.get_fresh_qualname(name) + setattr(self.fx_tracer.root, qualname, getattr(bw_module, name)) + result = self.fx_tracer.graph.node_copy( + node, lambda n: value_remap[n] + ) + result.target = qualname + value_remap[node] = result + else: + raise AssertionError("shouldn't get here") + + assert poutputs is not None + + # In general we don't know what the shapes of the outputs are, so allocate + # some dummy sizes for them. + def dummy() -> torch.Tensor: + with disable_proxy_modes_tracing(): + return torch.zeros(0, 0, 0, 0, 123) + + outputs = [ + dummy() if isinstance(o, torch.fx.Proxy) else o for o in poutputs + ] + self.bind_objects_to_proxies(outputs, poutputs) + return outputs + + outputs = copy_paste_aot_backward_graph() + + def proxy_subclass_constructor( + subclass_meta: Any, is_runtime: bool, unwrapped_args: Sequence[Any] + ) -> torch.Tensor: + @torch._dynamo.allow_in_graph # type: ignore[misc] + def make_subclass(*unwrapped_args: Any) -> Any: + return subclass_meta.creation_fn(unwrapped_args, is_runtime=is_runtime) + + punwrapped_args = pytree.tree_map(self.to_proxy, unwrapped_args) + + poutput = self.fx_tracer.create_proxy( + kind="call_function", + # pyrefly: ignore [bad-argument-type] + target=make_subclass, + args=tuple(punwrapped_args), + kwargs={}, + ) + + output = self.allocate_dummy() + self.bind_objects_to_proxies([output], [poutput]) + return output + + results = torch._functorch._aot_autograd.runtime_wrappers._backward_epilogue_functional( + metadata, + maybe_subclass_metadata, + outputs, + make_subclass_override=proxy_subclass_constructor, + ) + presults = pytree.tree_map(self.to_proxy, results) + return presults + + def proxy_call_backward( + self, + inputs: Sequence[Any], + output_metadatas: Sequence[Any], + saved_tensors: Sequence[torch.Tensor], + backward_idx: int, + ctx: torch.autograd.function.BackwardCFunction, + maybe_backward_state_idx: int | None, + opaque_object_indices: list[int], + ) -> tuple[torch.Tensor | None, ...]: + assert self.hooks_proxy is not None + pctx = self.hooks_proxy[backward_idx] # type: ignore[index] + pinputs = self.to_proxy(inputs) + psaved_tensors = self.to_proxy(saved_tensors) + if hasattr(ctx._forward_cls, "_aot_id"): # type: ignore[attr-defined] + # AOT backward + proxies = self.proxy_call_aot_backward( + pinputs, + psaved_tensors, + saved_tensors, + pctx, + ctx, + maybe_backward_state_idx, + opaque_object_indices, + ) + else: + proxies = self.fx_tracer.create_proxy( + kind="call_function", + target=call_backward, + args=( + pctx, + psaved_tensors, + *pinputs, + ), + kwargs={}, + ) + assert proxies is not None + + with disable_proxy_modes_tracing(): + # create fake Tensors + grad_ins: list[torch.Tensor | None] = [] + for idx, output_metadata in enumerate(output_metadatas): + if output_metadata is None or proxies[idx] is None: + grad_ins.append(None) + continue + + layout, device, dtype, size = output_metadata + grad_ins.append( + torch.empty(size=size, dtype=dtype, layout=layout, device=device) + ) + self.bind_objects_to_proxies(grad_ins, proxies) + return tuple(grad_ins) + + def call_copy_slices_prologue( + self, + inputs: Sequence[Any], + base_sizes: Sequence[Any], + base_strides: Sequence[Any], + base_storage_offset: Any, + view_sizes: Sequence[Any], + view_strides: Sequence[Any], + view_storage_offset: Any, + ) -> Sequence[torch.Tensor]: + args = ( + inputs, + self.to_proxy(base_sizes), + self.to_proxy(base_strides), + self.to_proxy(base_storage_offset), + self.to_proxy(view_sizes), + self.to_proxy(view_strides), + self.to_proxy(view_storage_offset), + ) + return self.proxy_call(copy_slices_prologue, args, [None] * 3) + + def call_copy_slices_epilogue( + self, + needs_input_grad: Sequence[bool], + result: torch.Tensor, + res: Sequence[Any], + grad_slice: torch.Tensor, + ) -> Sequence[torch.Tensor]: + return self.proxy_call( + copy_slices_epilogue, + (needs_input_grad, result, res, grad_slice), + [None] * len(needs_input_grad), + ) + + def allocate_dummy(self) -> torch.Tensor: + with disable_proxy_modes_tracing(): + # Weird quantity so it's easy to grep + return torch.zeros([0, 123456789]) + + def bind_function( + self, + fn_name: str, + fn: Callable[..., Any], + is_custom_function: bool, + is_traceable: bool, + ) -> str: + """Binds ops.fn_name = fn""" + return ops.add(fn_name, fn, is_custom_function, is_traceable) + + def apply_functional( + self, + fn_name: str, + grads: Sequence[Any], + args: Any, + output_metadata: Sequence[Any], + ) -> Sequence[torch.Tensor]: + """Proxies a call to ops.fn_name(grads, *args) into the graph""" + op = ops.get(fn_name) + return self.proxy_call(op, (grads, *args), output_metadata) + + def proxy_call( + self, fn: Callable[..., Any], args: Any, output_metadata: Sequence[Any] + ) -> Sequence[torch.Tensor]: + """Proxies a call to fn(*args) into the graph""" + proxy_args = pytree.tree_map(lambda e: self.to_proxy(e), args) + proxy_out = self.fx_tracer.create_proxy( + "call_function", fn, args=proxy_args, kwargs={} + ) + result = [self.allocate_dummy() for _ in output_metadata] + self.bind_objects_to_proxies(result, [proxy_out[i] for i in range(len(result))]) + return result + + def validate_outputs( + self, _: Any, outputs: Sequence[Any], args: Any, output_metadata: Sequence[Any] + ) -> Sequence[torch.Tensor]: + """Proxies a call to ops.validate_outputs(outputs, *args) into the graph""" + op = ops.get("validate_outputs") + proxy_args = pytree.tree_map(self.to_proxy, (outputs, *args)) + new_proxy_outputs = self.fx_tracer.create_proxy( + "call_function", op, args=proxy_args, kwargs={} + ) + assert len(output_metadata) == len(outputs) + self.bind_objects_to_proxies(outputs, new_proxy_outputs) + return outputs + + def accumulate(self, old_var: Any, new_var: Any) -> torch.Tensor: + old_var_proxy = self.to_proxy(old_var) + new_var_proxy = self.to_proxy(new_var) + proxy_out = self.fx_tracer.create_proxy( + "call_function", torch.add, args=(old_var_proxy, new_var_proxy), kwargs={} + ) + result = self.allocate_dummy() + self.bind_objects_to_proxies([result], [proxy_out]) + return result + + def accumulate_grad( + self, variable: torch.Tensor, grad: torch.Tensor, has_post_hooks: bool + ) -> None: + self.fx_tracer.create_proxy( + "call_function", + call_accumulate_grad, + args=( + self.to_proxy(variable), + self.to_proxy(grad), + has_post_hooks, + ), + kwargs={}, + ) + + def proxy_call_hook( + self, hook: Callable[..., Any], *args: Any, **kwargs: Any + ) -> torch.fx.Proxy: + return self.fx_tracer.create_proxy( + "call_function", + call_hook, + ( + hook, + *[self.to_proxy(x) for x in args], + ), + kwargs, + ) + + def unpack_hook(self, hook_id: int, data_id: int) -> torch.Tensor: + assert self.hooks_proxy is not None + hook = self.hooks_proxy[hook_id] # type: ignore[index] + data = self.packed_data_proxy[data_id] # type: ignore[index] + proxy = self.proxy_call_hook( + hook, + data, + hook_type="unpack_hook", + ) + out = self.allocate_dummy() + self.bind_objects_to_proxies([out], [proxy]) + return out + + def tensor_pre_hook( + self, inputs: list[torch.Tensor], hook_id: int, i: int + ) -> list[torch.Tensor]: + assert self.hooks_proxy is not None + hook = self.hooks_proxy[hook_id] # type: ignore[index] + proxy = self.proxy_call_hook( + hook, + inputs[i], + hook_type="tensor_pre_hook", + ) + with disable_proxy_modes_tracing(): + inputs[i] = maybe_clone(inputs[i]) # type: ignore[assignment] + self.bind_objects_to_proxies([inputs[i]], [proxy]) + return inputs + + def cpp_tensor_pre_hook( + self, inputs: list[torch.Tensor], hook_id: int, i: int + ) -> list[torch.Tensor]: + proxy = self.fx_tracer.create_proxy( + "call_function", + torch._C._dynamo.compiled_autograd.call_cpp_tensor_pre_hooks, + (hook_id, self.to_proxy(inputs[i])), + {}, + ) + with disable_proxy_modes_tracing(): + inputs[i] = maybe_clone(inputs[i]) # type: ignore[assignment] + self.bind_objects_to_proxies([inputs[i]], [proxy]) + return inputs + + def pre_hook(self, inputs: Sequence[Any], hook_id: int) -> list[torch.Tensor]: + assert self.hooks_proxy is not None + hook = self.hooks_proxy[hook_id] # type: ignore[index] + proxies = self.proxy_call_hook( + hook, + inputs, + hook_type="pre_hook", + ) + with disable_proxy_modes_tracing(): + inputs = [maybe_clone(x) for x in inputs] + self.bind_objects_to_proxies(inputs, proxies) + return inputs + + def post_hook( + self, outputs: list[torch.Tensor], inputs: Sequence[torch.Tensor], hook_id: int + ) -> list[torch.Tensor]: + assert self.hooks_proxy is not None + hook = self.hooks_proxy[hook_id] # type: ignore[index] + proxies = self.proxy_call_hook( + hook, + outputs, + inputs, + hook_type="post_hook", + ) + with disable_proxy_modes_tracing(): + outputs = [maybe_clone(x) for x in outputs] # type: ignore[misc] + self.bind_objects_to_proxies(outputs, proxies) + return outputs + + def post_acc_grad_hook( + self, input: torch.Tensor, hook_id: int + ) -> list[torch.Tensor]: + assert isinstance(input, torch.Tensor) + assert self.hooks_proxy is not None + hook = self.hooks_proxy[hook_id] # type: ignore[index] + proxy = self.proxy_call_hook( + hook, + input, + hook_type="post_acc_grad_hook", + ) + with disable_proxy_modes_tracing(): + res = [maybe_clone(input)] + self.bind_objects_to_proxies(res, [proxy]) + return res # type: ignore[return-value] + + # Note: [Compiled autograd and cudagraphs] + # Eager autograd backward implements scalars as 0-dim tensors, see DivBackward0::other_. + # When compiled autograd traces those nodes, it lifts the scalar tensors, resulting in a graph + # with some cpu 0-dim tensor inputs. To prevent the entire graph from skipping cudagraph, we move the + # scalars tensors to cuda. This works because ATen/prims ops will accept cuda 0-dim tensors too. + def move_graph_nodes_to_cuda(self, graph: torch.fx.Graph) -> list[int]: + to_move: dict[int, torch.fx.Node] = {} + has_cuda_inputs = False + nodes = list(graph.nodes) + assert nodes[0].target == "inputs" + inputs = nodes[0] + inputs_users = list(inputs.users.keys()) + # input access nodes should immediately follow placeholder nodes + first_getitem_idx = len(_graph_placeholders) + assert nodes[first_getitem_idx] == inputs_users[0] + last_getitem_idx = first_getitem_idx + len(inputs_users) - 1 + assert nodes[last_getitem_idx] == inputs_users[-1] + # getitem nodes on inputs + for i, node in enumerate(inputs_users): + if not has_cuda_inputs and node.meta["val"].device.type == "cuda": + has_cuda_inputs = True + continue + + is_cpu = node.meta["val"].device.type == "cpu" + is_scalar = len(node.meta["val"].size()) == 0 + if is_cpu and is_scalar: + node_users = list(node.users.keys()) + # We can only move the cpu scalar if it is not exposed to user code. + if all( + ( + isinstance(user.target, torch._ops.OpOverload) + and user.target.namespace in ("prims", "aten") + ) + or ( + isinstance(user.target, Op) + and not user.target.is_custom_function + ) + for user in node_users + ): + # all users are prims/aten, can move safely + to_move[i] = node + + # only move cpu scalars to cuda if there were cuda activations in this graph, + # this is to handle the case where cudagraphs is enabled on a cpu-only graph + if has_cuda_inputs: + for node in to_move.values(): + verbose_log.debug("Moving node %s from cpu to cuda", node) + node.meta["val"] = node.meta["val"].cuda() + + # return runtime indices we need to move to cuda + return list(to_move.keys()) + + return [] + + def is_sym_node(self, node: Any) -> bool: + return ( + isinstance(node, torch.fx.Node) + and node.op == "call_function" + and node.target + in [torch.ops.aten.sym_size.int, torch.ops.aten.sym_numel.default] + ) + + def dce(self) -> None: + # Most of these removed nodes would have been removed during Dynamo and AOTDispatch + # Remove some of these nodes earlier to improve compilation speed + + # Dynamo guards will error instead of creating aliasing guards unless we unpack them in the graph + unpack_nodes: OrderedSet[torch.fx.Node] = OrderedSet() + i: int | None = None + for i, node in enumerate(self.fx_tracer.graph.find_nodes(op="placeholder")): # noqa: B007 + unpack_nodes.update(node.users.keys()) + assert i == len(_graph_placeholders) - 1 + + def is_impure(node: torch.fx.Node) -> bool: + if node in unpack_nodes or ( + node.op == "call_function" and node.target in _impure_targets + ): + return True + return node.is_impure() + + before = len(self.fx_tracer.graph.nodes) + self.fx_tracer.graph.eliminate_dead_code(is_impure) + after = len(self.fx_tracer.graph.nodes) + verbose_log.debug("DCE removed %d nodes", before - after) + + def remove_unused_sizes(self) -> set[int]: + used_sizes = [] + unused_sizes = [] + + # seek placeholder, should be at nodes[1] + it = iter(self.fx_tracer.graph.nodes) + next(it) + sizes_node = next(it) + assert sizes_node.name == "sizes" + + for getitem_node in sizes_node.users: + assert getitem_node.target is operator.getitem + if getitem_node.users: + used_sizes.append(getitem_node) + else: + # remove from the graph + unused_sizes.append(getitem_node) + + used_sizes_idx: set[int] = set() + for used in used_sizes: + assert isinstance(used.args, tuple) + assert used.args[0] == sizes_node + assert isinstance(used.args[1], int) + next_size_idx = len(used_sizes_idx) + # used later reindex the runtime sizes arg + used_sizes_idx.add(used.args[1]) + # reindex the graph + used.args = (used.args[0], next_size_idx) + + for unused in unused_sizes: + self.fx_tracer.graph.erase_node(unused) + + return used_sizes_idx + + def create_graph_module(self, id: str) -> GraphModule: + return GraphModule(self.fx_tracer.root, self.fx_tracer.graph, id) + + def end_capture(self, outputs: Any) -> tuple[Callable[..., Any], Any]: + self.fx_tracer.create_proxy( + "call_function", + FakeCompiledAutogradEngine._exec_final_callbacks_stub, + (), + {}, + ) + self.stack.close() + self.fx_tracer.create_node( + "output", + "output", + (self.fx_tracer.create_arg(self.to_proxy(outputs)),), + {}, + ) + runtime_inputs_to_move: list[int] = [] + if snapshot_cudagraph_enabled(): + runtime_inputs_to_move = self.move_graph_nodes_to_cuda(self.fx_tracer.graph) + + # We traced using dummy tensors. Delete all the metadata of the dummy tensors. + # It's probably better to refactor this class to use a different tracer + # than the make_fx tracer, but that is a larger change. + for node in self.fx_tracer.graph.nodes: + for field in ["tensor_meta", "example_value", "val"]: + if field in node.meta: + del node.meta[field] + + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "compiled_autograd_graph_pre_reordering", + "encoding": "string", + }, + payload_fn=lambda: GraphModule( + self.fx_tracer.root, + self.fx_tracer.graph, + f"CompiledAutograd{self.id}PreReordering", + ).print_readable(print_output=False), + ) + self.delay_unpack_hook_nodes() + self.reorder_tensor_pre_hook_nodes() + self.reorder_pre_hook_nodes_to_schedule_asap() + self.reorder_accumulate_grad_nodes() + self.reorder_pre_hook_nodes_to_mimic_eager() + self.reorder_post_acc_grad_hook_nodes() + self.reorder_post_hook_nodes() + # TODO(yf225): work around: remove dead codes like `sym_size` and `sym_numel` which are not used downstream. e.g. + # ``` + # sym_numel_default = torch.ops.aten.sym_numel.default(sum_109); sum_109 = None + # eq_115 = 16 == sym_numel_default; sym_numel_default = eq_115 = None + # sym_size_int_39 = torch.ops.aten.sym_size.int(getitem_112, 1); getitem_112 = None + # eq_116 = 16 == sym_size_int_39; eq_116 = None + # eq_117 = 16 == sym_size_int_39; sym_size_int_39 = eq_117 = None + # ``` + # Proper fix is Richard's Python compiled autograd effort which will avoid calling make_fx and + # should prevent these ops from going into the CA graph. + self.dce() + if self.nan_checker: + self.nan_checker.prep_with_graph(self.fx_tracer.graph) + + # keep only sizes that are actually used in the graph + used_sizes_idx = self.remove_unused_sizes() + + graph = self.create_graph_module(f"CompiledAutograd{self.id}") + set_locals_to_steal(graph, ["inputs"]) + lazy_graph_code = lazy_format_graph_code( + "Compiled autograd graph", + graph, + include_device=True, + include_stride=True, + colored=True, + ) + compiled_autograd_log.info("%s", lazy_graph_code) + verbose_log.debug("%s", lazy_graph_code) + trace_structured( + "compiled_autograd_graph", + payload_fn=lambda: graph.print_readable(print_output=False), + ) + + def runtime_wrapper( + compiled_fn: Callable[..., Any], + inputs: Any, + sizes: Any, + scalars: Any, + hooks: Any, + packed_inputs: Any, + ) -> tuple[Any, Any]: + global in_compiled_autograd_region + try: + in_compiled_autograd_region = True + + if self.nan_checker: + self.nan_checker.prep_with_inputs(inputs) + + filtered_sizes = [] + for idx, integer in enumerate(sizes): + if idx in used_sizes_idx: + # can't create negative size + if integer > 0: + filtered_sizes.append(torch.empty(0, integer)) + torch._dynamo.maybe_mark_dynamic(filtered_sizes[-1], 1) + else: + filtered_sizes.append(integer) + + for i in runtime_inputs_to_move: + inputs[i] = inputs[i].pin_memory().cuda(non_blocking=True) + + with _disable(), make_compile_context(self.id): + out = compiled_fn( + inputs, filtered_sizes, scalars, hooks, packed_inputs + ) + if self.nan_checker: + self.nan_checker.check(out) + return out + finally: + in_compiled_autograd_region = False + + get_chromium_event_logger().log_event_end( + "compiled_autograd", + time.time_ns(), + {"graph_id": self.id}, + self.start_time_ns, + log_pt2_compile_event=True, + ) + self.compile_context.__exit__(None, None, None) + return runtime_wrapper, self.compiler_fn(graph) + + @staticmethod + def get_all_nodes(args: Sequence[Any]) -> list[torch.fx.Node]: + # filter out non-Node args, like None + nodes = [n for n in args if type(n) is torch.fx.Node] + return nodes + + @staticmethod + def is_placeholder(node: torch.fx.Node) -> bool: + if node.op == "placeholder" or ( + node.op == "call_function" + and node.target is operator.getitem + and node.args[0].op == "placeholder" # type: ignore[union-attr, arg-type] + ): + return True + return False + + def reorder_accumulate_grad_nodes(self) -> None: + """ + Usage of AOTAutograd causes all the accumulate_grad_ nodes to get pushed to the end of + the graph. This differs from eager mode, which schedules them as soon as possible. This + pass attempts to reorder the graph to mimic eager behavior. + """ + for node in self.fx_tracer.graph.find_nodes( + op="call_function", target=call_accumulate_grad + ): + param_node, grad_node = node.args[0], node.args[1] + getitem_node = None + if grad_node.target is operator.getitem: + getitem_node = grad_node + grad_node = getitem_node.args[0] + + arg = max([param_node, grad_node]) # last arg + if arg is not node.prev and not self.is_placeholder(arg): + arg.append(node) + if getitem_node is not None: + arg.append(getitem_node) + + def delay_unpack_hook_nodes(self) -> None: + """ + We can delay unpack hooks until they are needed, even later than in the eager autograd engine. + """ + for node in self.fx_tracer.graph.find_nodes( + op="call_function", target=call_hook + ): + if node.kwargs.get("hook_type", None) != "unpack_hook": + continue + + first_user = min(node.users) + first_user.prepend(node) + + def reorder_tensor_pre_hook_nodes(self) -> None: + """ + Usage of AOTAutograd causes all the tensor_pre_hook nodes to get pushed + to the end of the graph. This differs from eager mode, which schedules + them as soon as possible. This pass attempts to reorder the graph to + mimic eager behavior. + """ + for node in self.fx_tracer.graph.find_nodes( + op="call_function", target=call_hook + ): + if node.kwargs.get("hook_type", None) != "tensor_pre_hook": + continue + + getitem_node = node.args[0] + input_node = node.args[1] # tensor_pre_hook handle only one grad tensor + + if input_node is not node.prev and not self.is_placeholder(input_node): + input_node.append(getitem_node) + getitem_node.append(node) + + def reorder_pre_hook_nodes_to_schedule_asap(self) -> None: + """ + In this function, we schedule the pre hooks as soon as possible. This + does not match eager behavior (schedule pre hook right before its + registered node), but it can make acc grad be scheduled properly when + the pre hooks are registered to them. After reordering acc grad node, we + will reorder the pre hooks again to mimic eager behavior. + """ + for node in self.fx_tracer.graph.find_nodes( + op="call_function", target=call_hook + ): + if node.kwargs.get("hook_type", None) != "pre_hook": + continue + + getitem_node = node.args[0] + # pre_hook handle a tuple of grad tensors + input_nodes = self.get_all_nodes(node.args[1]) + + to_remove = [] + to_append = [] + hook_block = [node] # contain the hook and hook args getitem + for n in input_nodes: + if n.op == "call_function" and n.target is operator.getitem: + to_append.append(n.args[0]) + to_remove.append(n) + hook_block.append(n) + for a, b in zip(to_remove, to_append): + input_nodes.remove(a) + input_nodes.append(b) # type: ignore[arg-type] + + arg = max(input_nodes) # last input + if arg is not node.prev and not self.is_placeholder(arg): + arg.append(getitem_node) + for n in hook_block: + getitem_node.append(n) + + def reorder_pre_hook_nodes_to_mimic_eager(self) -> None: + """ + Usage of AOTAutograd causes all the pre_hook nodes to get pushed to the + end of the graph. This differs from eager mode, which schedules them + right before their registered node execution. This pass attempts to + reorder the graph to mimic eager behavior. + """ + pre_hooks = [] + for node in self.fx_tracer.graph.find_nodes( + op="call_function", target=call_hook + ): + if node.kwargs.get("hook_type", None) != "pre_hook": + continue + pre_hooks.append(node) + + for node in reversed(pre_hooks): + hook_getitem_node = node.args[0] + + users = list(node.users.keys()) + if len(users) == 0: + continue + + # users are all getitem ops and they are used by same registered node + assert all( + user.op == "call_function" and user.target is operator.getitem + for user in users + ) + registered_node = next(iter(users[0].users.keys())) + + if registered_node is not node.next: + registered_node.prepend(hook_getitem_node) + registered_node.prepend(node) + for getitem in users: + registered_node.prepend(getitem) + + def reorder_post_acc_grad_hook_nodes(self) -> None: + """ + Usage of AOTAutograd causes all the post_acc_grad_hook nodes to get + pushed to the end of the graph. This differs from eager mode, which + schedules them as soon as possible. This pass attempts to reorder the + graph to mimic eager behavior. + """ + post_acc_grad_hooks = [] + for node in self.fx_tracer.graph.find_nodes( + op="call_function", target=call_hook + ): + if node.kwargs.get("hook_type", None) != "post_acc_grad_hook": + continue + post_acc_grad_hooks.append(node) + + # nodes in post_acc_grad_hooks are in topo order. For hooks registered + # to same node, we should keep their relative order + for node in reversed(post_acc_grad_hooks): + getitem_node = node.args[0] + param_node = node.args[1] # post_acc_grad_hook handle one param + + # find the corresponding acc_grad node + acc_grad_node = None + for n in list(param_node.users.keys()): + if n.op == "call_function" and n.target is call_accumulate_grad: + acc_grad_node = n + break + + assert acc_grad_node is not None, ( + "post_acc_grad_hook must have corresponding acc grad node" + ) + + # append post_acc_grad_hook after acc_grad node + acc_grad_node.append(getitem_node) + getitem_node.append(node) + + def reorder_post_hook_nodes(self) -> None: + """ + Usage of AOTAutograd causes all the post_hook nodes to get pushed to the + end of the graph. This differs from eager mode, which schedules them as + soon as possible. This pass attempts to reorder the graph to mimic eager + behavior. + """ + post_hooks = [] + for node in self.fx_tracer.graph.find_nodes( + op="call_function", target=call_hook + ): + if node.kwargs.get("hook_type", None) != "post_hook": + continue + post_hooks.append(node) + + for node in reversed(post_hooks): + getitem_node = node.args[0] + output_nodes = node.args[1] + input_nodes = node.args[2] + + if len(output_nodes) > 0: + continue + + # pyrefly: ignore [implicit-any] + input_nodes_and_users = [] + input_nodes_and_users.extend(list(input_nodes)) + for input_node in input_nodes: + input_nodes_and_users.extend( + user + for user in list(input_node.users.keys()) + if not ( + user.op == "call_function" + and user.target is call_hook + and node.kwargs.get("hook_type", None) == "post_hook" + ) + ) + + arg = max(input_nodes_and_users) # last input users + if arg.op == "call_function" and arg.target is call_accumulate_grad: + param_node = arg.args[0] + post_acc_grad_hook_node = None + for n in list(param_node.users.keys()): + if ( + n.op == "call_function" + and n.target is call_hook + and n.kwargs.get("hook_type", None) == "post_acc_grad_hook" + ): + post_acc_grad_hook_node = n + + if post_acc_grad_hook_node is not None: + post_acc_grad_hook_node.append(getitem_node) + getitem_node.append(node) + continue + + if arg is not node.prev and not self.is_placeholder(arg): + arg.append(getitem_node) + getitem_node.append(node) + + def to_proxy(self, t: Any) -> Any: + if t is None: + return None + if isinstance(t, list): + return [self.to_proxy(x) for x in t] + if isinstance(t, tuple): + return tuple(self.to_proxy(x) for x in t) + if isinstance(t, (torch.SymInt, torch.SymFloat)): + return self.symnode_proxy_lookup[t.node] + if not isinstance(t, torch.Tensor): + # constant types like device, dtype, str + return t + proxy_tensor = fetch_object_proxy(self.fx_tracer, t) + assert isinstance(proxy_tensor, torch.fx.experimental.proxy_tensor._ProxyTensor) + return proxy_tensor.proxy + + def bind_objects_to_proxies( + self, + objects: Sequence[Any], + proxies: Any, + origins: list[tuple[int, str]] | None = None, + ) -> Sequence[Any]: + if isinstance(proxies, torch.fx.Proxy): + if origins: + assert len(origins) == len(objects) + bound_proxies = [] + for i in range(len(objects)): + nodecall_index, node_name = origins[i] + self.set_node_origin(node_name, nodecall_index, None) + bound_proxies.append(proxies[i]) # type: ignore[index] + proxies = bound_proxies + else: + proxies = [proxies[i] for i in range(len(objects))] # type: ignore[index] + + assert len(objects) == len(proxies) + track_tensor_tree(objects, proxies, constant=None, tracer=self.fx_tracer) + return proxies + + def bind_backward_state(self, index: int) -> BackwardState: + assert self.hooks_proxy is not None + proxy = self.hooks_proxy[index] # type: ignore[index] + bw_state = BackwardState() + track_tensor_tree(bw_state, proxy, constant=None, tracer=self.fx_tracer) + return bw_state + + def set_node_origin( + self, + node_name: str, + nodecall_index: int, + pyobj: torch.autograd.Function | None, + ) -> None: + maybe_aot_id = "" + if pyobj is not None: + forward_cls = pyobj._forward_cls # type: ignore[attr-defined] + if hasattr(forward_cls, "_aot_id"): + # backward was created by AOT Dispatcher + if forward_cls._lazy_backward_info is None: + raise RuntimeError( + """This compiled backward function was saved by AOTAutogradCache, which does not support + compiled autograd. Please turn off AOTAutogradCache using `TORCHINDUCTOR_AUTOGRAD_CACHE=0`.""" + ) + maybe_aot_id = forward_cls._aot_id + new_code = f"{node_name}{maybe_aot_id} (NodeCall {nodecall_index})" + raw_stack_trace = CapturedTraceback.extract().format()[-1] + new_stack_trace = raw_stack_trace.replace( + "raw_stack_trace = CapturedTraceback.extract().format()[-1]", new_code + ) + set_stack_trace(new_stack_trace) + + +# state of the autograd engine dispatch, kept in sync by enable/disable context managers +compiled_autograd_enabled = False + +# global flag to check if compiled autograd is enabled but Dynamo stance is "force_eager" +compiled_autograd_enabled_force_eager = False + +# global flag to check if we are processing graphs produced from a compiled autograd graph +in_compiled_autograd_region = False + +active_disable_ctx = False + +depth = 0 + + +@contextlib.contextmanager +def _enable( + compiler_fn: Callable[..., Any], + dynamic: bool = True, + ignore_active_disable_ctx: bool = True, +) -> Generator[None, None, None]: + # The entrypoint to enable CA. + # It is recommended to enable via `torch._dynamo.config.compiled_autograd = True` rather + # than using this context manager directly. If you are torch.compiling the corresponding + # forward pass, make sure they are wrapped under this context as well. + # + # Example: + # def train(model, inputs, target): + # compiled_model = torch.compile(model) + # pred = compiled_model(data) + # loss = compute_loss(pred, target) + # loss.backward() + # + # with _enable(compiler_fn): + # train(model, inputs, target) + # + # Inputs: + # - compiler_fn: The wrapper that will consume the compiled autograd graph, e.g. `torch.compile` + # - dynamic: Whether compiled autograd will treat tensors in the autograd graph (params, activations) as dynamic. + # This doesn't affect the dynamic configuration of the compilation wrapper. + + if not ignore_active_disable_ctx and active_disable_ctx: + yield + else: + if dynamic: + assert type(dynamic) is bool + + from torch._dynamo import eval_frame + + if eval_frame._stance.stance == "force_eager": + # If user explicitly sets Dynamo stance to "force_eager", we want Compiled Autograd + # to fall back to eager as well. + global compiled_autograd_enabled_force_eager + compiled_autograd_enabled_force_eager = True + try: + yield + finally: + compiled_autograd_enabled_force_eager = False + else: + # we need to import this, because user might not have imported it if they directly use this context manager + # we need to lazily import it, because of circular dependencies + if torch.cuda.is_available(): + from torch._inductor import cudagraph_trees # noqa: F401 + + ( + prior_compiler, + prior_dynamic, + ) = torch._C._dynamo.compiled_autograd.set_autograd_compiler( + functools.partial(AutogradCompilerInstance, compiler_fn), dynamic + ) + if snapshot_verbose_logging_enabled(): + torch._C._dynamo.compiled_autograd.set_verbose_logger(verbose_log) # type:ignore[arg-type] + global compiled_autograd_enabled + compiled_autograd_enabled = True + global depth + prior_depth = depth + depth += 1 + try: + with torch.autograd.set_multithreading_enabled(False): + yield + finally: + if not prior_compiler: + compiled_autograd_enabled = False + torch._C._dynamo.compiled_autograd.set_autograd_compiler( + prior_compiler, prior_dynamic + ) + depth -= 1 + assert depth == prior_depth, ( + "Nested Compiled Autograd Contexts must return before their parent context" + ) + + +@contextlib.contextmanager +def _disable() -> Generator[None, None, None]: + ( + prior_compiler, + prior_dynamic, + ) = torch._C._dynamo.compiled_autograd.set_autograd_compiler(None, False) + global compiled_autograd_enabled + compiled_autograd_enabled = False + global active_disable_ctx + if not active_disable_ctx: + active_disable_ctx = True + try: + yield + finally: + if prior_compiler: + compiled_autograd_enabled = True + active_disable_ctx = False + torch._C._dynamo.compiled_autograd.set_autograd_compiler( + prior_compiler, prior_dynamic + ) + + +# return to starting state of a new process +def reset() -> None: + global compiled_autograd_enabled + compiled_autograd_enabled = False + assert not in_compiled_autograd_region + torch._C._dynamo.compiled_autograd.set_autograd_compiler(None, False) + torch._C._dynamo.compiled_autograd.set_verbose_logger(None) + torch._C._dynamo.compiled_autograd.clear_cache() + global COMPILE_COUNTER + COMPILE_COUNTER = itertools.count() + + +# Reimplementation of part of CopySlices::apply in Python. +# The shared code is really similar so we're not going to try to deduplicate. +def copy_slices_prologue( + inputs: Sequence[torch.Tensor], + base_sizes: Sequence[IntLikeType], + base_strides: Sequence[IntLikeType], + base_storage_offset: IntLikeType, + view_sizes: Sequence[IntLikeType], + view_strides: Sequence[IntLikeType], + view_storage_offset: IntLikeType, +) -> list[torch.Tensor]: + grad = inputs[0] + result = grad.new_empty_strided(base_sizes, base_strides) + assert grad is not None + result.copy_(grad) + offset = view_storage_offset - base_storage_offset + grad_slice = result.as_strided(view_sizes, view_strides, offset) + return [result, grad_slice, grad_slice.clone(memory_format=torch.contiguous_format)] + + +# Reimplementation of part of CopySlices::apply in Python. +# The shared code is really similar so we're not going to try to deduplicate. +def copy_slices_epilogue( + needs_input_grad: Sequence[bool], + result: torch.Tensor, + res: Sequence[torch.Tensor | None], + grad_slice: torch.Tensor, +) -> list[torch.Tensor | None]: + grad_inputs: list[torch.Tensor | None] = [None] * len(needs_input_grad) + for i in range(len(needs_input_grad)): + if needs_input_grad[i]: + if res[i] is None: + continue + if i == 0: + to_copy = res[i] + assert to_copy is not None + grad_slice.copy_(to_copy) + grad_inputs[i] = result + else: + grad_inputs[i] = res[i] + return grad_inputs diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/comprehension_graph_break.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/comprehension_graph_break.py new file mode 100644 index 0000000000000000000000000000000000000000..d0e15cf5fa13445a72d7dcc59b4077d7ebf5c549 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/comprehension_graph_break.py @@ -0,0 +1,694 @@ +from __future__ import annotations + +import copy +import dataclasses +import dis +import functools +import logging +import sys +from typing import Any, TYPE_CHECKING + + +if TYPE_CHECKING: + import types + + from collections.abc import Callable + + from .symbolic_convert import InstructionTranslatorBase + +from .bytecode_transformation import ( + create_copy, + create_dup_top, + create_instruction, + create_swap, + Instruction, + unique_id, +) +from .codegen import PyCodegen +from .exc import unimplemented +from .output_graph import GraphCompileReason, StackLocalsMetadata +from .variables.misc import NullVariable, UnknownVariable + + +log = logging.getLogger(__name__) + + +@functools.cache +def _get_comprehension_bytecode_prefix() -> list[str]: + """Get the bytecode instructions that precede BUILD_LIST in a list comprehension.""" + + assert sys.version_info >= (3, 12) + + def fn() -> list[int]: + return [i for i in range(1)] # noqa: C416 + + insts = [inst.opname for inst in dis.get_instructions(fn)] + + start_idx = len(insts) - 1 - insts[::-1].index("LOAD_FAST_AND_CLEAR") + end_idx = insts.index("BUILD_LIST") + + return insts[start_idx:end_idx] + + +@functools.cache +def _get_comprehension_result_patterns() -> dict[str, dict[str, Any]]: + """Discover bytecode patterns for comprehension result handling. + + Analyzes sample functions to extract the opcode sequences that appear + after END_FOR for each result disposition (stored, discarded, returned, consumed). + + Returns patterns with: + - pre_store_ops: opcodes between END_FOR and first STORE_FAST + - post_store_op: first opcode after all STORE_FASTs (for disambiguation) + """ + assert sys.version_info >= (3, 12) + + def fn_stored() -> list[int]: + result = [i for i in range(1)] # noqa: C416 + return result + + def fn_discarded() -> int: + [i for i in range(1)] # noqa: C416 + return 1 + + def fn_returned() -> list[int]: + return [i for i in range(1)] # noqa: C416 + + def fn_consumed() -> int: + return sum([i for i in range(1)]) # noqa: C416 + + def extract_pattern(fn: Callable[..., Any]) -> tuple[list[str], str | None]: + """Extract (pre_store_ops, post_store_op) from comprehension bytecode.""" + target_line = list(dis.findlinestarts(fn.__code__))[1][1] + insts: list[str] = [] + started = False + for instr in dis.get_instructions(fn): + if started and instr.starts_line: + break + pos = instr.positions + if pos and pos.lineno == target_line: + started = started or bool(instr.starts_line) + insts.append(instr.opname) + + ops = insts[insts.index("END_FOR") + 1 :] + idx = 0 + + pre_store_ops = [] + while idx < len(ops) and ops[idx] != "STORE_FAST": + pre_store_ops.append(ops[idx]) + idx += 1 + + while idx < len(ops) and ops[idx] == "STORE_FAST": + idx += 1 + + return pre_store_ops, ops[idx] if idx < len(ops) else None + + stored = extract_pattern(fn_stored) + discarded = extract_pattern(fn_discarded) + returned = extract_pattern(fn_returned) + consumed = extract_pattern(fn_consumed) + + return { + "stored": {"pre_store_ops": stored[0], "post_store_op": stored[1]}, + "discarded": {"pre_store_ops": discarded[0], "post_store_op": discarded[1]}, + "returned": {"pre_store_ops": returned[0], "post_store_op": returned[1]}, + "consumed": {"pre_store_ops": consumed[0], "post_store_op": []}, + } + + +@dataclasses.dataclass +class ComprehensionAnalysis: + """Metadata about a comprehension's bytecode structure. + + Attributes: + end_ip: Instruction pointer after all comprehension bytecode + result_var: Name of result variable, or None if result stays on stack + result_on_stack: True if result stays on stack (discarded, returned, or in expression) + iterator_vars: Variables from LOAD_FAST_AND_CLEAR (need restoration) + walrus_vars: Variables assigned via walrus operator (:=) inside comprehension + captured_vars: Variables read from outer scope via LOAD_FAST inside comprehension + """ + + end_ip: int + result_var: str | None + result_on_stack: bool + iterator_vars: list[str] + walrus_vars: list[str] + captured_vars: list[str] + + +def _is_comprehension_start(tx: InstructionTranslatorBase) -> bool: + """Detect if we're at the start of a list/dict comprehension in 3.12+. + + In Python 3.12+, comprehensions are inlined with a bytecode pattern that + precedes BUILD_LIST/BUILD_MAP. + """ + assert sys.version_info >= (3, 12) + + assert tx.instruction_pointer is not None + ip = tx.instruction_pointer - 1 + + pattern = _get_comprehension_bytecode_prefix() + prefix = [inst.opname for inst in tx.instructions[ip - len(pattern) : ip]] + + return prefix == pattern + + +def _find_comprehension_end_for_ip(tx: InstructionTranslatorBase) -> int: + """Find the instruction pointer of the outermost END_FOR for current comprehension.""" + assert sys.version_info >= (3, 12) + assert tx.instruction_pointer is not None + + nesting_depth = 0 + for search_ip in range(tx.instruction_pointer, len(tx.instructions)): + inst = tx.instructions[search_ip] + if inst.opname == "FOR_ITER": + nesting_depth += 1 + elif inst.opname == "END_FOR": + nesting_depth -= 1 + if nesting_depth == 0: + return search_ip + return -1 + + +def _analyze_comprehension(tx: InstructionTranslatorBase) -> ComprehensionAnalysis: + """Analyze comprehension bytecode to determine result handling pattern.""" + assert sys.version_info >= (3, 12) + assert tx.instruction_pointer is not None + + patterns = _get_comprehension_result_patterns() + start_ip = tx.instruction_pointer - 1 # BUILD_LIST/BUILD_MAP + + iterator_vars: list[str] = [] + walrus_vars: list[str] = [] + captured_vars: list[str] = [] + defined_inside: set[str] = set() + + # Collect iterator variables from LOAD_FAST_AND_CLEAR before BUILD_LIST/BUILD_MAP + iter_scan_ip = start_ip - 1 + while iter_scan_ip >= 0: + inst = tx.instructions[iter_scan_ip] + if inst.opname == "LOAD_FAST_AND_CLEAR": + iterator_vars.insert(0, inst.argval) + iter_scan_ip -= 1 + elif inst.opname in ("SWAP", "GET_ITER"): + iter_scan_ip -= 1 + else: + break + defined_inside.update(iterator_vars) + + end_for_ip = _find_comprehension_end_for_ip(tx) + if end_for_ip == -1: + unimplemented( + gb_type="Comprehension analysis failed: No END_FOR", + context="", + explanation="Could not find END_FOR instruction in comprehension bytecode.", + hints=[], + ) + + # Find first FOR_ITER to know where loop body starts + for_iter_ip = next( + i + for i in range(start_ip, end_for_ip) + if tx.instructions[i].opname == "FOR_ITER" + ) + + # Single pass through loop body to detect walrus vars and captured vars + for body_ip in range(for_iter_ip + 1, end_for_ip): + inst = tx.instructions[body_ip] + + # Detect walrus pattern: COPY 1 followed by STORE_FAST + if inst.opname == "COPY" and inst.arg == 1 and body_ip + 1 < end_for_ip: + next_inst = tx.instructions[body_ip + 1] + if next_inst.opname == "STORE_FAST": + var_name = next_inst.argval + if var_name not in iterator_vars and var_name not in walrus_vars: + walrus_vars.append(var_name) + defined_inside.add(var_name) + + # Track variables defined inside the loop + if inst.opname == "STORE_FAST": + defined_inside.add(inst.argval) + + # Detect LOAD_FAST referencing outer variables + elif inst.opname.startswith("LOAD_FAST"): + var_names = ( + inst.argval if isinstance(inst.argval, tuple) else (inst.argval,) + ) + for var_name in var_names: + if var_name not in defined_inside and var_name not in captured_vars: + captured_vars.append(var_name) + + # Extract pre_store_ops: all opcodes from END_FOR+1 until first STORE_FAST + pre_store_ops: list[str] = [] + scan_ip = end_for_ip + 1 + while ( + scan_ip < len(tx.instructions) + and tx.instructions[scan_ip].opname != "STORE_FAST" + ): + pre_store_ops.append(tx.instructions[scan_ip].opname) + scan_ip += 1 + + store_fast_ip = scan_ip + + # Skip all STORE_FASTs to find post_store_op + while ( + scan_ip < len(tx.instructions) + and tx.instructions[scan_ip].opname == "STORE_FAST" + ): + scan_ip += 1 + + post_store_op = ( + tx.instructions[scan_ip].opname if scan_ip < len(tx.instructions) else None + ) + + def matches(name: str) -> bool: + pat = patterns[name] + return pre_store_ops == pat["pre_store_ops"] and ( + post_store_op == pat["post_store_op"] or not pat["post_store_op"] + ) + + result_var: str | None = None + if matches("stored"): + result_var = tx.instructions[store_fast_ip].argval + result_on_stack = False + elif matches("discarded"): + result_var = None + result_on_stack = False + scan_ip = scan_ip + 1 if patterns["discarded"]["post_store_op"] else scan_ip + elif matches("returned") or pre_store_ops == patterns["consumed"]["pre_store_ops"]: + result_var = None + result_on_stack = True + else: + unimplemented( + gb_type="Comprehension analysis failed: No matches", + context=f"pre_store_ops={pre_store_ops}, post_store_op={post_store_op}", + explanation="Comprehension does not match any known bytecode pattern.", + hints=[], + ) + + return ComprehensionAnalysis( + end_ip=scan_ip, + result_var=result_var, + # pyrefly: ignore [unbound-name] + result_on_stack=result_on_stack, + iterator_vars=iterator_vars, + walrus_vars=walrus_vars, + captured_vars=captured_vars, + ) + + +def _handle_comprehension_graph_break( + tx: InstructionTranslatorBase, inst: Instruction +) -> None: + """Handle list/dict comprehension graph break. + + Builds a synthetic function wrapping the comprehension bytecode, + calls it via codegen_call_resume, then chains into the resume + function for the post-comprehension code. + """ + assert sys.version_info >= (3, 12) + assert tx.instruction_pointer is not None + + start_ip = tx.instruction_pointer - 1 # BUILD_LIST/BUILD_MAP + analysis = _analyze_comprehension(tx) + stack_pops = 1 + len(analysis.iterator_vars) + reason = GraphCompileReason("comprehension_graph_break", [tx.frame_summary()]) + log.debug("comprehension triggered compile") + + # --- Step 1: Compile the graph up to the comprehension --- + + all_stack_locals_metadata = tx.output.compile_subgraph( + tx, + reason=reason, + stack_pops=stack_pops, + ) + # Record which stack_pops items are NULL before popn loses the info. + # NULLs on the CPython stack can't be passed as function arguments. + stack_pops_null_mask = [ + isinstance(tx.stack[len(tx.stack) - stack_pops + i], NullVariable) + for i in range(stack_pops) + ] + + tx.popn(stack_pops) + meta = all_stack_locals_metadata[0] + cg = PyCodegen(tx.output.root_tx) + + # Runtime stack after compile_subgraph: + # cells, [frame_values], *(non-popped items), *(stack_pops items w/ NULLs) + # frame_values[0] = [frame N locals] (no stack items yet) + + nonnull_count = sum(1 for m in stack_pops_null_mask if not m) + + # live_stack_depth: stack items above cells/frame_values excluding NULLs + # that compile_subgraph didn't codegen (tracked in stack_null_idxes). + live_stack_depth = len(tx.stack) - len(meta.stack_null_idxes) + + # --- Step 2: Pop stack_pops items and append non-nulls to frame_values[0] --- + # SWAP each item to TOS then LIST_APPEND or pop_null; fv_list stays at + # TOS throughout. Items append in TOS-first (reversed) order; + # _build_comprehension_fn compensates by loading in reverse. + cg.extend_output( + [ + # frame_values[0] to TOS + *create_copy(live_stack_depth + stack_pops + 1), + cg.create_load_const(0), + cg.create_binary_subscr(), + ] + ) + for i in reversed(range(stack_pops)): + cg.extend_output(create_swap(2)) + if stack_pops_null_mask[i]: + cg.extend_output(cg.pop_null()) + else: + cg.extend_output([create_instruction("LIST_APPEND", arg=1)]) + cg.extend_output([create_instruction("POP_TOP")]) + + # Stack: cells, [frame_values], *(non-popped items) + + # --- Step 3: Build comprehension function --- + new_code, fn_name = _build_comprehension_fn( + tx, + analysis, + start_ip, + stack_pops, + stack_pops_null_mask, + nonnull_count, + meta, + ) + + # --- Step 4: Extract [cells[0]] and [frame_values[0]] for codegen_call_resume --- + cg.extend_output( + [ + *create_copy(live_stack_depth + 2), + cg.create_load_const(0), + cg.create_binary_subscr(), + create_instruction("BUILD_LIST", arg=1), + *create_copy(live_stack_depth + 2), + cg.create_load_const(0), + cg.create_binary_subscr(), + create_instruction("BUILD_LIST", arg=1), + ] + ) + + # Stack: ..., *(non-popped), [cells[0]], [frame_values[0]] + + # --- Step 5: Call comprehension function via codegen_call_resume --- + tx.codegen_call_resume([new_code], [fn_name], cg) + + # Stack: ..., *(non-popped), comp_result + + # --- Step 6: Remove appended stack_pops items from frame_values[0] --- + if nonnull_count > 0: + frame_values_pos = live_stack_depth + 1 + 1 # +1 result, +1 frame_values + cg.extend_output( + [ + *create_copy(frame_values_pos), + cg.create_load_const(0), + cg.create_binary_subscr(), + # frame_values[0] on TOS + create_dup_top(), + # frame_values[0], frame_values[0] + cg.create_load_const(-nonnull_count), + cg.create_load_const(None), + create_instruction("BUILD_SLICE", arg=2), + create_instruction("DELETE_SUBSCR"), + # del frame_values[0][-nonnull_count:] + create_instruction("POP_TOP"), + ] + ) + + # --- Step 7: Pass comprehension outputs to frame_values[0] --- + # Walrus vars first, then result_var. + vars_to_pass = analysis.walrus_vars + ( + [analysis.result_var] if analysis.result_var else [] + ) + + existing_vars: dict[str, int] = {} + for var_name in vars_to_pass: + tx.symbolic_locals[var_name] = UnknownVariable() + if var_name in meta.locals_names: + existing_vars[var_name] = meta.locals_names[var_name] + else: + meta.locals_names[var_name] = len(meta.locals_names) + + fv_depth = live_stack_depth + 2 # comp_result + frame_values + + # --- Walrus vars: extract from comp_result tuple --- + if analysis.walrus_vars: + # comp_result is (result, *walrus_vars). + cg.extend_output( + [ + *create_copy(fv_depth), + cg.create_load_const(0), + cg.create_binary_subscr(), + ] + ) + # Stack: ..., comp_tuple, fv0 + for j, walrus_var in enumerate(analysis.walrus_vars): + cg.extend_output( + [ + *create_copy(2), + cg.create_load_const(j + 1), + cg.create_binary_subscr(), + ] + ) + # Stack: ..., comp_tuple, fv0, walrus_value + if walrus_var in existing_vars: + # fv0[idx] = walrus_value + cg.extend_output( + [ + *create_copy(2), # copy fv0 + cg.create_load_const(existing_vars[walrus_var]), + create_instruction("STORE_SUBSCR"), + ] + ) + else: + cg.extend_output([create_instruction("LIST_APPEND", arg=1)]) + # Stack: ..., comp_tuple, fv0 + cg.extend_output( + [ + create_instruction("POP_TOP"), # pop fv0 + # Extract the result from the tuple. + cg.create_load_const(0), + cg.create_binary_subscr(), + ] + ) + # Stack: ..., result + + # --- Result: keep on stack, overwrite/append to fv[0], or discard --- + if analysis.result_on_stack: + tx.push(UnknownVariable()) + elif analysis.result_var: + cg.extend_output( + [ + *create_copy(fv_depth), + cg.create_load_const(0), + cg.create_binary_subscr(), + # Stack: ..., result, fv0 + ] + ) + if analysis.result_var in existing_vars: + cg.extend_output( + [ + cg.create_load_const(existing_vars[analysis.result_var]), + create_instruction("STORE_SUBSCR"), + # fv0[idx] = result + ] + ) + else: + cg.extend_output( + [ + *create_swap(2), + create_instruction("LIST_APPEND", arg=1), + create_instruction("POP_TOP"), + ] + ) + else: + cg.extend_output([create_instruction("POP_TOP")]) + + # Stack: cells, [frame_values], *(non-popped stack) + tx.output.add_output_instructions(cg.get_instructions()) + + # --- Step 8: Create resume function chain --- + resume_inst = tx.instructions[analysis.end_ip] + tx.output.add_output_instructions( + tx.create_call_resume_at(resume_inst, all_stack_locals_metadata) + ) + + tx.instruction_pointer = None + + +def _build_comprehension_fn( + tx: InstructionTranslatorBase, + analysis: ComprehensionAnalysis, + start_ip: int, + stack_pops: int, + stack_pops_null_mask: list[bool], + nonnull_count: int, + meta: StackLocalsMetadata, +) -> tuple[types.CodeType, str]: + """Build a synthetic function wrapping comprehension bytecode. + + Uses the same calling convention as resume functions created by + create_resume / ContinueExecutionCache.generate: the first two args + are __nested_resume_fns and __nested_frame_values (ignored here), + followed by stack items and live locals. + + Returns (code, name) where name is the global name for the function. + """ + from .bytecode_transformation import transform_code_object + from .eval_frame import skip_code + from .resume_execution import CO_VARARGS, CO_VARKEYWORDS + + # Args follow frame_values layout: locals first, then stack_pops items + # (appended to end of frame_values[0] by the caller). + # codegen_call_resume unpacks frame_values[0] as positional args. + argnames = tuple(k for k in meta.locals_names if k not in tx.cell_and_freevars()) + args = ( + ["__nested_resume_fns", "__nested_frame_values"] + + list(argnames) + + [f"___stack{i}" for i in range(nonnull_count)] + ) + + freevars = tuple( + sorted(list(tx.f_code.co_cellvars or []) + list(tx.f_code.co_freevars or [])) + ) + + lineno = tx.lineno if tx.lineno is not None else tx.f_code.co_firstlineno + fn_name = unique_id(f"__comprehension_{tx.f_code.co_name}_at_{lineno}") + + comprehension_body_vars = ( + analysis.iterator_vars + + analysis.walrus_vars + + ([analysis.result_var] if analysis.result_var else []) + + analysis.captured_vars + ) + + def update(instructions: list[Instruction], code_options: dict[str, Any]) -> None: + code_options["co_name"] = fn_name + if sys.version_info >= (3, 11): + code_options["co_qualname"] = fn_name + code_options["co_firstlineno"] = lineno + code_options["co_cellvars"] = () + code_options["co_freevars"] = freevars + code_options["co_argcount"] = len(args) + code_options["co_posonlyargcount"] = 0 + code_options["co_kwonlyargcount"] = 0 + code_options["co_varnames"] = tuple( + args + [v for v in comprehension_body_vars if v not in args] + ) + code_options["co_flags"] = code_options["co_flags"] & ~( + CO_VARARGS | CO_VARKEYWORDS + ) + + prefix: list[Instruction] = [] + if freevars: + prefix.append(create_instruction("COPY_FREE_VARS", arg=len(freevars))) + prefix.append(create_instruction("RESUME", arg=0)) + + # Push stack_pops items onto operand stack so the comprehension + # bytecode finds them where it expects (iterator + saved vars). + # NULL positions get PUSH_NULL, non-null get LOAD_FAST. + # Items were appended to frame_values[0] in TOS-first order, + # so load in reverse to reconstruct the original stack layout. + nonnull_i = nonnull_count - 1 + for i in range(stack_pops): + if stack_pops_null_mask[i]: + prefix.append(create_instruction("PUSH_NULL")) + else: + prefix.append( + create_instruction("LOAD_FAST", argval=f"___stack{nonnull_i}") + ) + nonnull_i -= 1 + + comp_insts = _copy_comprehension_bytecode(tx, start_ip, analysis.end_ip) + + # Epilogue: ensure result is on stack, pack walrus vars, return. + epilogue: list[Instruction] = [] + if not analysis.result_on_stack: + if analysis.result_var: + epilogue.append( + create_instruction("LOAD_FAST", argval=analysis.result_var) + ) + else: + epilogue.append(create_instruction("LOAD_CONST", argval=None)) + if analysis.walrus_vars: + for var_name in analysis.walrus_vars: + epilogue.append(create_instruction("LOAD_FAST", argval=var_name)) + epilogue.append( + create_instruction( + "BUILD_TUPLE", + arg=1 + len(analysis.walrus_vars), + ) + ) + epilogue.append(create_instruction("RETURN_VALUE")) + + instructions[:] = prefix + comp_insts + epilogue + + new_code, _ = transform_code_object(tx.f_code, update) + skip_code(new_code) + + # Install as global + tx.output.install_resume_function_global(fn_name, new_code, tx.f_globals) + + return new_code, fn_name + + +def _copy_comprehension_bytecode( + tx: InstructionTranslatorBase, start_ip: int, end_ip: int +) -> list[Instruction]: + """Copy comprehension bytecode instructions, updating jump targets.""" + inst_map: dict[Instruction, Instruction] = {} + copied_insts: list[Instruction] = [] + + for ip in range(start_ip, end_ip): + original_inst = tx.instructions[ip] + copied_inst = copy.copy(original_inst) + copied_inst.exn_tab_entry = None + inst_map[original_inst] = copied_inst + copied_insts.append(copied_inst) + + for copied_inst in copied_insts: + if copied_inst.target is not None and copied_inst.target in inst_map: + copied_inst.target = inst_map[copied_inst.target] + + return copied_insts + + +def maybe_setup_comprehension_speculation( + tx: InstructionTranslatorBase, inst: Instruction +) -> bool: + """ + Handle comprehension start for Python 3.12+ BUILD_LIST/BUILD_MAP with argval 0. + Returns True if a graph break was triggered and the caller should return early. + """ + if not (sys.version_info >= (3, 12) and inst.argval == 0): + return False + + if not _is_comprehension_start(tx): + return False + + can_speculate = ( + all(b.can_restore() for b in tx.block_stack) + and not tx.one_graph + and not tx.error_on_graph_break + and not tx.is_tracing_resume_prologue + and not tx.active_generic_context_managers + and tx.output.current_tracer.parent is None + ) + + if can_speculate and tx.parent is not None: + can_speculate = tx._can_speculate_comprehension_nested() + # Only set up speculation at depth 0 (outermost comprehension) + if can_speculate and tx._comprehension_depth == 0: + speculation = tx.speculate() + if speculation.failed(tx): + _handle_comprehension_graph_break(tx, inst) + return True + tx.current_speculation = speculation + end_for_ip = _find_comprehension_end_for_ip(tx) + assert end_for_ip >= 0 + tx._comprehension_end_for_ips.add(end_for_ip) + tx._comprehension_depth += 1 + return False diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/comptime.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/comptime.py new file mode 100644 index 0000000000000000000000000000000000000000..811ed4a46e4828516692554ee51d990d1c2912be --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/comptime.py @@ -0,0 +1,438 @@ +""" +This module provides the public comptime interface to TorchDynamo, enabling users to execute +arbitrary Python code during symbolic evaluation of their programs. + +The comptime interface allows inspection and modification of TorchDynamo's compilation +process while it is running. This can be useful for: + +- Debugging compilation issues +- Inspecting intermediate state +- Adding custom guards or graph breaks +- Analyzing symbolic shapes and values + +Example usage: + + import torch + from torch._dynamo.comptime import comptime + + def my_model(x): + # Print the compile-time known information about x + comptime.print(x) + + # Print the current FX graph being constructed + comptime.print_graph() + + # Force a value to be treated as static + if comptime(lambda ctx: ctx.get_local("x").is_dynamic()): + comptime.force_static(x) + + # Add a manual graph break + comptime.graph_break() + +Note: While this API provides significant flexibility, it intentionally avoids +exposing internal implementation details of TorchDynamo to maintain compatibility +across versions. +""" + +import builtins +import dis +import time +import traceback +from collections.abc import Callable, Sequence +from typing import Any, TextIO + +import torch +from torch._dynamo.symbolic_convert import InstructionTranslatorBase +from torch._dynamo.variables.base import VariableTracker +from torch._subclasses.fake_tensor import FakeTensor +from torch.fx.experimental.symbolic_shapes import free_symbols + +from .exc import unimplemented +from .variables import CellVariable +from .variables.tensor import SymNodeVariable + + +class ComptimeVar: + """ + A ComptimeVar represents a Python value, at some particular point + in time, in the Python code we are symbolically evaluating with + torchdynamo. This must be distinguished from a runtime value, as + at compile-time there are some properties of the variable we + do not know (for example, if the ComptimeVar represents a Tensor, + we only know metadata about the tensor; we do NOT know what the + actual data in the Tensor is.) + """ + + def __init__(self, v: VariableTracker) -> None: + self.__variable = v + + def as_proxy(self) -> VariableTracker | Sequence[VariableTracker]: + """ + Returns an fx.Proxy (or tuple/list of fx.Proxy) representing + this variable in the FX graph we are assembling to pass + to the user compiler. + + This method only works for variables we actually track in + the FX graph, aka Tensors (and ints, if you are compiling + with dynamic shapes). In particular, if you have a list + or tuple of tensors, you will get a list/tuple of proxies + (not a single proxy representing the entire list/tuple). + """ + return self.__variable.as_proxy() + + def is_proxy(self) -> bool: + """ + Returns True if as_proxy() would succeed. + """ + return self.__variable.is_proxy() + + def as_fake(self) -> FakeTensor | torch.SymInt: + """ + Returns a "fake" value (either a FakeTensor or a SymInt) + representing the variable in question. This only works + for variables that denote Tensor or int. You can use + this to query metadata; e.g., v.as_fake().size(0) will + tell you the compile-time known size of the tensor. + + WARNING: Do NOT mutate the returned tensor. + """ + return self.__variable.as_proxy().node.meta["example_value"] + + def size(self, dim: int | None = None) -> int | torch.SymInt: + """ + Returns the size of the tensor (if dim is None) or the size + at the dimension dim. The returned size may be a SymInt. + """ + return self.as_fake().size(dim) # type: ignore[union-attr, return-value] + + def python_type(self) -> type: + """ + Returns what type(v) would have returned for the variable + at compile time. + """ + return self.__variable.python_type() + + def as_python_constant(self) -> Any: + """ + Returns the Python value this variable would have, but only if it is + completely known at compile-time (e.g., it is constant). + + WARNING: Do NOT mutate the returned constant. The returned constant + may or may not correspond to the actual value this variable may take + on at runtime; for example, if the variable in question is a constant + list, we may return a copy of that list. + """ + return self.__variable.as_python_constant() + + def is_python_constant(self) -> bool: + """ + Returns True if as_python_constant would succeed. + """ + return self.__variable.is_python_constant() + + def is_dynamic(self) -> bool: + if isinstance(self.__variable, SymNodeVariable): + fs = free_symbols(self.__variable.sym_num) + return bool(fs) + return False + + def force_static(self) -> None: + """ + Forces that a value is static, inducing a guard on its specific value + """ + if isinstance(self.__variable, SymNodeVariable): + self.__variable.evaluate_expr() + elif self.__variable.is_python_constant(): + # TODO: Maybe complain if this isn't a int/bool/float variable + pass + else: + raise AssertionError( + f"cannot force {self.__variable} ({type(self.__variable)}) static" + ) + + def _i_will_not_complain_if_bc_breaks_VariableTracker(self) -> VariableTracker: + """ + Returns the internal data structure VariableTracker that Dynamo uses + to represent variables at compile time. There are no BC guarantees on + this API and WE RESERVE THE RIGHT TO BREAK YOUR CODE if you rely on + it. + """ + return self.__variable + + def __repr__(self) -> str: + return self.__variable.debug_repr() + + # TODO: API for adding a custom guard + + +class ComptimeContext: + """ + This context class provides access to a public API for Dynamo's internals. + If there is something here you would find useful that is missing, please + file a feature request at https://github.com/pytorch/pytorch/ + """ + + def __init__(self, tx: InstructionTranslatorBase) -> None: + self.__tx = tx + + def get_local(self, name: str, *, stacklevel: int = 0) -> ComptimeVar: + """ + Retrieve the compile-time known information about a local. + """ + tx = self.__get_tx(stacklevel) + var = tx.symbolic_locals[name] + + # Auto-dereference when accessing cell locals in python. + if isinstance(var, CellVariable): + return ComptimeVar(tx.output.side_effects.load_cell(var)) + + return ComptimeVar(var) + + def graph_break(self, msg: str = "ComptimeContext.graph_break") -> None: + """ + Manually trigger a graph break + """ + unimplemented( + gb_type="ComptimeContext graph break", + context=msg, + explanation=f"Manually triggered ComptimeContext graph break with message {msg}.", + hints=[], + ) + + def graph(self) -> torch.fx.Graph: + """ + Retrieve the partially constructed FX graph that would be + passed to the user compiler after compilation. + """ + return self.__tx.output.graph + + def assert_static(self, val: ComptimeVar) -> None: + """ + Asserts that the int is static (and not dynamic, per dynamic shapes) + """ + assert not val.is_dynamic(), ( + "expected static but got dynamic (run with TORCH_LOGS=dynamic for more info)" + ) + + def print_graph(self, *, verbose: bool = True, file: TextIO | None = None) -> None: + """ + Print the partially constructed FX graph that would be passed + to the user compiler after compilation. + """ + print( + self.__tx.output.graph.python_code("self", verbose=verbose).src, file=file + ) + + def parent(self) -> "ComptimeContext": + return ComptimeContext(self.__tx.parent) # type: ignore[arg-type] + + def __get_tx(self, stacklevel: int) -> Any: + tx = self.__tx + # pyrefly: ignore [bad-assignment, non-convergent-recursion] + for _ in range(stacklevel): + tx = tx.parent # type: ignore[assignment] + return tx + + def print(self, val: Any, *, file: TextIO | None = None) -> None: + print(repr(val), file=file) + + def print_disas(self, *, file: TextIO | None = None, stacklevel: int = 0) -> None: + """ + Print the current series of opcodes being executed (not including + parent frames), including where you are in the particular opcode + stream. + """ + tx = self.__get_tx(stacklevel) + print( + dis.Bytecode( + tx.f_code, + current_offset=tx.instructions[tx.instruction_pointer].offset, + ).dis(), + file=file, + ) + + def print_value_stack( + self, *, file: TextIO | None = None, stacklevel: int = 0 + ) -> None: + """ + Print the current Python value stack. Note that this is NOT the same + as the traceback; use print_bt() to print that. Note that at + stacklevel=0, this will typically be empty, as comptime cannot + currently be used in an expression context where there would be + intermediates on the stack. If you would find this useful, please + file a bug at https://github.com/pytorch/pytorch/ + + NB: Stack grows downwards in our print + """ + tx = self.__get_tx(stacklevel) + for s in tx.stack: + print(f"- {s.debug_repr()}", file=file) + + def print_locals(self, *, file: TextIO | None = None, stacklevel: int = 0) -> None: + """ + Print all of the locals available in the current context. + By default this view is very limited; you can get more information + about any individual local using get_local(). + """ + tx = self.__get_tx(stacklevel) + for k, v in tx.symbolic_locals.items(): + print(f"{k} = {v.debug_repr()}", file=file) + + def print_bt(self, *, file: TextIO | None = None, stacklevel: int = 0) -> None: + """ + Print the user code backtrace, starting at the beginning of the + frame Dynamo started evaluating. Note that this MAY NOT go all + the way to the torch.compile invocation, as we may have done + a graph break and are compiling an intermediate frame as the + starting point. If you think the other behavior would be better, + file a bug at https://github.com/pytorch/pytorch/ + """ + stack = [] + tx = self.__get_tx(stacklevel) + while tx is not None: + stack.append(tx.frame_summary()) + tx = getattr(tx, "parent", None) + print( + "".join(traceback.StackSummary.from_list(reversed(stack)).format()), + file=file, + ) + + def print_guards(self, *, file: TextIO | None = None) -> None: + """ + Print the currently installed guards for the Dynamo context. + This does NOT include guards associated with variables that + may or may not be installed in the future if those variables + are used. + """ + # TODO: improve print format, current guard format is extremely + # verbose + print( + "\n".join(f"{repr(guard)}" for guard in sorted(self.__tx.output.guards)), + file=file, + ) + + def _i_will_not_complain_if_bc_breaks_InstructionTranslator( + self, + ) -> InstructionTranslatorBase: + """ + Returns the internal data structure InstructionTranslator that Dynamo + uses to track state of symbolic evaluation. There are no BC + guarantees on this API and WE RESERVE THE RIGHT TO BREAK YOUR CODE if + you rely on it. + """ + return self.__tx + + def sleep(self, sec: int | float) -> None: + time.sleep(sec) + + +class _Comptime: + @staticmethod + def __call__( + fn: Callable[[ComptimeContext], Any], + fallback_fn: Callable[[], Any] = lambda: None, + ) -> Any: + """fn gets called at compile time in TorchDynamo, calls fallback_fn otherwise""" + fallback_fn() + + # Convenience wrappers that are more compact to use + + @staticmethod + def graph_break() -> None: + comptime(lambda ctx: ctx.graph_break()) + + @staticmethod + def print(e: Any) -> None: + comptime(lambda ctx: ctx.print(ctx.get_local("e")), lambda: print(e)) + + @staticmethod + def print_graph() -> None: + comptime(lambda ctx: ctx.print_graph()) + + @staticmethod + def print_disas(*, stacklevel: int = 0) -> None: + comptime( + lambda ctx: ctx.print_disas( + stacklevel=ctx.get_local("stacklevel").as_python_constant() + 1 + ) + ) + + @staticmethod + def print_value_stack(*, stacklevel: int = 0) -> None: + comptime( + lambda ctx: ctx.print_value_stack( + stacklevel=ctx.get_local("stacklevel").as_python_constant() + 1 + ) + ) + + # This is a more useful variant of print_value_stack that can be used + # in an expression context; e.g., x + print_value_stack_and_return(y + z), + # you will see x on the stack prior to the addition operation + @staticmethod + def print_value_stack_and_return(e: Any, *, stacklevel: int = 0) -> Any: + comptime( + lambda ctx: ctx.print_value_stack( + stacklevel=ctx.get_local("stacklevel").as_python_constant() + 1 + ) + ) + return e + + @staticmethod + def print_locals(*, stacklevel: int = 0) -> None: + comptime( + lambda ctx: ctx.print_locals( + stacklevel=ctx.get_local("stacklevel").as_python_constant() + 1 + ) + ) + + @staticmethod + def print_bt(*, stacklevel: int = 0) -> None: + comptime( + lambda ctx: ctx.print_bt( + stacklevel=ctx.get_local("stacklevel").as_python_constant() + 1 + ) + ) + + @staticmethod + def print_guards() -> None: + comptime(lambda ctx: ctx.print_guards()) + + @staticmethod + def assert_static(val: Any) -> None: + comptime(lambda ctx: ctx.assert_static(ctx.get_local("val"))) + + @staticmethod + def force_static(val: Any) -> None: + comptime(lambda ctx: ctx.get_local("val").force_static()) + + @staticmethod + def breakpoint() -> None: + """ + Like pdb breakpoint(), but drop into pdb whenever this line + of code is compiled by dynamo. Use it by putting + this in your model code:: + + from torch._dynamo.comptime import comptime + + comptime.breakpoint() + + And then, inside pdb, you can access 'ctx' to query things + about the compilation context:: + + (Pdb) !ctx.print_bt() + (Pdb) !ctx.print_locals() + (Pdb) p ctx.get_local("attention").as_fake() + """ + + def inner(inner_ctx: ComptimeContext) -> None: + ctx = inner_ctx.parent() # noqa: F841 + builtins.breakpoint() + + comptime(inner) + + @staticmethod + def sleep(sec: int | float) -> None: + comptime(lambda ctx: ctx.sleep(ctx.get_local("sec").as_python_constant())) + + +comptime = _Comptime() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/config.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/config.py new file mode 100644 index 0000000000000000000000000000000000000000..367341dcb6403acb77b519de436dac45a44b3e75 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/config.py @@ -0,0 +1,918 @@ +""" +Configuration module for TorchDynamo compiler and optimization settings. + +This module contains various configuration flags and settings that control TorchDynamo's +behavior, including: + +- Runtime behavior flags (e.g., guard settings, specialization options) +- Debugging and development options +- Performance tuning parameters +- Feature toggles for experimental features +""" + +import getpass +import os +import sys +import sysconfig +import tempfile +from collections.abc import Callable +from os.path import abspath, dirname +from typing import Any, Literal, TYPE_CHECKING + +from torch._environment import is_fbcode +from torch.utils._config_module import Config, get_tristate_env, install_config_module + + +# to configure logging for dynamo, aot, and inductor +# use the following API in the torch._logging module +# torch._logging.set_logs(dynamo=, aot=, inductor) +# or use the environment variable TORCH_LOGS="dynamo,aot,inductor" (use a prefix + to indicate higher verbosity) +# see this design doc for more detailed info +# Design doc: https://docs.google.com/document/d/1ZRfTWKa8eaPq1AxaiHrq4ASTPouzzlPiuquSBEJYwS8/edit# +# the name of a file to write the logs to +# [@compile_ignored: debug] +log_file_name: str | None = None + +# [@compile_ignored: debug] Verbose will print full stack traces on warnings and errors +verbose = os.environ.get("TORCHDYNAMO_VERBOSE", "0") == "1" + +# [@compile_ignored: runtime_behaviour] verify the correctness of optimized backend +verify_correctness = False + +# Override backend for specific graphs (for debugging/bisecting). +# Format: "filter1:backend1;filter2:backend2;..." where filter can be: +# - Individual IDs: "0,5,10" +# - Ranges: "10-20" (inclusive) +# - Comparisons: ">10", ">=10", "<5", "<=5" +# Backends can be: "eager", "aot_eager", "inductor", etc. +# Examples: +# ">10:eager" - Run graphs with frame_id > 10 in dynamo eager backend +# "<=5:aot_eager;>5:inductor" - First 6 graphs use aot_eager, rest use inductor +# [@compile_ignored: debug] +debug_backend_override: str = os.environ.get("TORCH_COMPILE_OVERRIDE_BACKENDS", "") + +# Override inductor config for specific graphs (for debugging/bisecting). +# Format: "filter1:config1;filter2:config2;..." where filter uses same syntax as +# debug_backend_override, and config is "key=value" or "key=value,key2=value2". +# Examples: +# "0-5:triton.cudagraph_skip_dynamic_graphs=False" - Disable skip for graphs 0-5 +# ">10:triton.cudagraphs=False" - Disable cudagraphs for graphs > 10 +# [@compile_ignored: debug] +debug_inductor_config_override: str = os.environ.get( + "TORCH_COMPILE_OVERRIDE_INDUCTOR_CONFIGS", "" +) + +# Override dynamo config for specific graphs (for debugging/bisecting). +# Format: "filter1:config1;filter2:config2;..." where filter uses same syntax as +# debug_backend_override, and config is "key=value" or "key=value,key2=value2". +# Examples: +# "0-5:specialize_float=True" - Specialize floats for graphs 0-5 +# ">10:automatic_dynamic_shapes=False" - Disable dynamic shapes for graphs > 10 +# [@compile_ignored: debug] +debug_dynamo_config_override: str = os.environ.get( + "TORCH_COMPILE_OVERRIDE_DYNAMO_CONFIGS", "" +) + +# Validate that fake_fn and real_fn in @leaf_function decorators produce outputs +# with matching shapes and dtypes in eager mode. Helps catch mismatches early. +# Disabled by default to avoid runtime overhead. +# [@compile_ignored: debug] +leaf_function_validate_outputs = False + +# Check for escaped gradients in @leaf_function. When a leaf_function closes over +# a tensor with requires_grad=True, gradients won't flow back to it. This check +# walks the autograd graph to detect such cases and raises an error. +# Disabled by default to avoid runtime overhead. Enable for debugging. +# [@compile_ignored: debug] +leaf_function_check_escaped_gradients = False + +# need this many ops to create an FX graph (deprecated: not used) +minimum_call_count = 1 + +# turn on/off DCE pass (deprecated: always true) +dead_code_elimination = None + +# Enable or disable side effect replay after graph execution. +# When False, mutations to Python objects (lists, dicts, attributes) won't be +# replayed after the compiled graph runs. This can cause correctness issues +# if your code depends on these mutations being visible. This should probably +# never be False by default. At the moment, only export will need it. +replay_side_effects = True + +# Configure side effect warning level +# If `info` (default): allow side effects and log to TORCH_LOGS="side_effects" and tlparse +# If `silent`, we allow side effects, no logs are made. +# If `warn`, we allow side effects but issue warnings +# If `error`, we error on side effects +# NOTE: it is NOT safe to change this config during compilation! +side_effect_replay_policy = "info" + +# disable (for a function) when cache reaches this size + +# controls the maximum number of cache entries with a guard on same ID_MATCH'd +# object. It also controls the maximum size of cache entries if they don't have +# any ID_MATCH'd guards. +# [@compile_ignored: runtime_behaviour] +recompile_limit = 8 + +# [@compile_ignored: runtime_behaviour] safeguarding to prevent horrible recomps +accumulated_recompile_limit = 256 + +skip_code_recursive_on_recompile_limit_hit: bool = Config( + default=True, deprecated=True, deprecation_message="does not do anything" +) + +# raise a hard error if cache limit is hit. If you are on a model where you +# know you've sized the cache correctly, this can help detect problems when +# you regress guards/specialization. This works best when recompile_limit = 1. +# This flag is incompatible with: suppress_errors. +# [@compile_ignored: runtime_behaviour] +fail_on_recompile_limit_hit = False + +cache_size_limit: int = Config(alias="torch._dynamo.config.recompile_limit") +accumulated_cache_size_limit: int = Config( + alias="torch._dynamo.config.accumulated_recompile_limit" +) + +skip_code_recursive_on_cache_limit_hit: bool = Config( + alias="torch._dynamo.config.skip_code_recursive_on_recompile_limit_hit", + deprecated=True, + deprecation_message="does not do anything", +) + +fail_on_cache_limit_hit: bool = Config( + alias="torch._dynamo.config.fail_on_recompile_limit_hit" +) + +# whether or not to specialize on int inputs. This only has an effect with +# dynamic_shapes; when dynamic_shapes is False, we ALWAYS specialize on int +# inputs. Note that assume_static_by_default will also cause ints to get +# specialized, so this is mostly useful for export, where we want inputs +# to be dynamic, but accesses to ints should NOT get promoted into inputs. +specialize_int = False + +# Whether or not to specialize on float inputs. Dynamo will always promote +# float inputs into Tensor inputs, but at the moment, backends inconsistently +# support codegen on float (this is to be fixed). +specialize_float = False + +# legacy config, does nothing now! +dynamic_shapes = True + +use_lazy_graph_module = ( + os.environ.get("TORCH_COMPILE_USE_LAZY_GRAPH_MODULE", "1") == "1" +) + +# This is a temporarily flag, which changes the behavior of dynamic_shapes=True. +# When assume_static_by_default is True, we only allocate symbols for shapes marked dynamic via mark_dynamic. +# NOTE - this flag can be removed once we can run dynamic_shapes=False w/ the mark_dynamic API +# see [Note - on the state of mark_dynamic] +assume_static_by_default = True + +# This flag changes how dynamic_shapes=True works, and is meant to be used in conjunction +# with assume_static_by_default=True. +# With this flag enabled, we always compile a frame as fully static for the first time, and, if we fail +# any guards due to wobbles in shape, we recompile with *all* the wobbled shapes as being marked dynamic. +automatic_dynamic_shapes = ( + os.environ.get("TORCH_DYNAMO_AUTOMATIC_DYNAMIC_SHAPES", "1") == "1" +) + +# Valid options: "dynamic", "unbacked" +automatic_dynamic_shapes_mark_as: Literal["dynamic", "unbacked"] = "dynamic" + +# When True, adds exclusion guards for tensor dims and scalars that transition +# from static to dynamic via automatic_dynamic_shapes. +# +# Invariant: when enabled, automatic_dynamic recompilation preserves graph +# selection — inputs that matched a previous static cache entry will continue +# to use that entry, not be intercepted by a newer dynamic entry. This holds +# as long as recompilations are caused solely by the same variable being +# observed with different static values (progressive dynamism). A recompilation +# triggered by a different reason (e.g., a guard failure unrelated to shape +# transitions) will clear the exclusion state for that entry. +# +# Mechanism: the exclusion guard rejects inputs matching the prior static +# graph's sizes, so those inputs fall through to the more specialized static +# graph instead of being captured by the newer dynamic graph. +# +# Scope: applies only to graph-input-level dimension and scalar transitions. +# Does NOT handle data-dependent branching (if x.size(0) > k), graph breaks, +# or other recompilation triggers where no dimension actually transitions. +automatic_dynamic_exclusion_guard = False + +# log graph in/out metadata +# This is only turned on for export today since we +# know we are tracing a flat callable. later, this +# can extended to other use cases as well. +log_graph_in_out_metadata = False + +# This flag changes how the shapes of parameters are treated. +# If this flag is set to True, then the shapes of torch.nn.Parameter as well as of torch.Tensor are attempted to be dynamic +# If this flag is set to False, then the shapes of torch.nn.Parameter are assumed to be static, +# while the shapes of torch.Tensor are assumed to be dynamic. +force_parameter_static_shapes = True + +# This flag ensures that the shapes of a nn module are always assumed to be static +# If the flag is set to True, then the shapes of a nn.module are assumed to be static +# If the flag is set to False, then the shapes of a nn.module can be dynamic +force_nn_module_property_static_shapes = True + +# Typically, if you mark_dynamic a dimension, we will error if the dimension +# actually ended up getting specialized. This knob changes the behavior so +# that we don't error at all. This is helpful for our CI where I'm using a +# heuristic to mark batch dimensions as dynamic and the heuristic may get it +# wrong. +allow_ignore_mark_dynamic = False + +# Set this to False to assume nn.Modules() contents are immutable (similar assumption as freezing) +guard_nn_modules = True + +# Uses CPython internal dictionary tags to detect mutation. There is some +# overlap between guard_nn_modules_using_dict_tags and guard_nn_modules flag. +# guard_nn_modules unspecializes the nn module instance and adds guard for each +# relevant member of the nn modules. On the other hand, +# guard_nn_modules_using_dict_tags specializes on each nn module instance but +# uses low overhead dict version matching to detect mutations, obviating the +# need to guard on members of the nn modules. With +# guard_nn_modules_using_dict_tags, the guard_nn_modules is not really required +# but kept around for debugging and discussing unspecializing nn module +# variables. +# TODO(janimesh, voz): Remove both of these flags (or at least guard_nn_modules) +# once we have reached stability for the guard_nn_modules_using_dict_tags. +guard_nn_modules_using_dict_tags = True + +# Flag to enable preparation for graph freezing, so that the named parameters and +# buffers are passed as params_flat in tracing context by AOT autograd. +# Non-Inductor backends can use this list for graph freezing. +prepare_freezing = os.environ.get("TORCHDYNAMO_PREPARE_FREEZING", "0") == "1" + +# NOTE this has been deprecated, it does nothing now. +traceable_tensor_subclasses: set[type[Any]] = set() + +# If a tensor subclass is put into this set, Dynamo will model its instasnces in +# a very conservative and limited way (most likely causing lots of graph breaks +# if one apply tensor ops on these instances). This is useful if you encounter +# internal compiler errors from Dynamo which are caused by tensor subclasses, +# and you are willing to tolerate potential graph breaks rather than hard error. +nontraceable_tensor_subclasses: set[type[Any]] = set() + +# Suppress errors in torch._dynamo.optimize, instead forcing a fallback to eager. +# This is a good way to get your model to work one way or another, but you may +# lose optimization opportunities this way. Devs, if your benchmark model is failing +# this way, you should figure out why instead of suppressing it. +# This flag is incompatible with: fail_on_recompile_limit_hit. +suppress_errors = bool(os.environ.get("TORCHDYNAMO_SUPPRESS_ERRORS", False)) + +# Record and write an execution record of the current frame to a file +# if an exception is encountered +# @compile_ignored[debug] +replay_record_enabled = os.environ.get("TORCH_COMPILE_REPLAY_RECORD", "0") == "1" + +# Rewrite assert statement in python with torch._assert +rewrite_assert_with_torch_assert = True + +# Disable dynamo +disable = os.environ.get("TORCH_COMPILE_DISABLE", "0") == "1" + +# [@compile_ignored: runtime_behaviour] Get a cprofile trace of Dynamo +cprofile = os.environ.get("TORCH_COMPILE_CPROFILE", False) + +# Enable Dynamo profiler. When enabled, prints pstats output showing +# time spent tracing each user function. Set to True to enable, or set to a +# file path to save the .prof file for snakeviz. +# [@compile_ignored: runtime_behaviour] +dynamo_profiler: bool | str = os.environ.get("TORCH_COMPILE_DYNAMO_PROFILER", False) + +# Legacy config, does nothing now! +skipfiles_inline_module_allowlist: dict[Any, Any] = {} +"""Allowlist of inline modules to skip during compilation. + +Legacy configuration that previously controlled which modules could be +inlined during tracing. This configuration is deprecated and no longer used. + +:type: dict[Any, Any] +:default: {} + +.. deprecated:: + This configuration is deprecated and does nothing now. + +.. note:: + DEPRECATED: This setting has no effect on current behavior. +""" + +# If a string representing a PyTorch module is in this ignorelist, +# the `allowed_functions.is_allowed` function will not consider it +# when creating a list of PyTorch functions that will appear in +# FX IR. +allowed_functions_module_string_ignorelist = { + "torch.distributions", + "torch.testing", + "torch._refs", + "torch._prims", + "torch._decomp", +} + +# Debug Flag to try minifier at different stages. Possible values are {None, "aot", "dynamo"} +# None - Minifier is switched off +# dynamo - Runs minifier on the TorchDynamo produced graphs, if compilation fails +# aot - Runs minifier on the Aot Autograd produced graphs, if compilation fails +# [@compile_ignored: debug] +repro_after = os.environ.get("TORCHDYNAMO_REPRO_AFTER", None) + +# Compiler compilation debug info +# 1: Dumps the original graph out to repro.py if compilation fails +# 2: Dumps a minifier_launcher.py if compilation fails. +# 3: Always dumps a minifier_launcher.py. Good for segfaults. +# 4: Dumps a minifier_launcher.py if the accuracy fails. +# [@compile_ignored: debug] +repro_level = int(os.environ.get("TORCHDYNAMO_REPRO_LEVEL", 2)) + +# By default, we try to detect accuracy failure by running both forward +# and backward of a torchdynamo produced graph (if you are using repro_after +# 'dynamo'). This setting forces us to only test the forward graph and +# not the backward graph. This can be helpful if you're trying to debug +# an inference only problem, but the minifier seems to be choking on the +# backwards step +# TODO: Detect this situation automatically so the user doesn't need +# to manually configure this +# [@compile_ignored: debug] +repro_forward_only = os.environ.get("TORCHDYNAMO_REPRO_FORWARD_ONLY") == "1" + +# The tolerance we should use when testing if a compiled graph +# has diverged so that we should treat it as an accuracy failure +# [@compile_ignored: debug] +repro_tolerance = 1e-3 + + +# Whether to ignore non-floating point values when checking accuracy. +# Checking accuracy of non-floating point values such as boolean tensors +# can lead to false positives. +# [@compile_ignored: debug] +repro_ignore_non_fp = os.environ.get("TORCHDYNAMO_REPRO_IGNORE_NON_FP") == "1" + +# If True, when testing if two models are the same, we will test them against +# a third fp64 reference and only report a problem if the RMSE relative to the +# fp64 is greater. However, this will use more memory; you may disable this +# if memory usage is too high. +# [@compile_ignored: runtime_behaviour] +same_two_models_use_fp64 = True + +# Not all backends support scalars. Some calls on torch.Tensor (like .item()) return a scalar type. +# When this flag is set to False, we introduce a graph break instead of capturing. +# This requires dynamic_shapes to be True. +capture_scalar_outputs = os.environ.get("TORCHDYNAMO_CAPTURE_SCALAR_OUTPUTS") == "1" + +# Not all backends support operators that have dynamic output shape (e.g., +# nonzero, unique). When this flag is set to False, we introduce a graph +# break instead of capturing. This requires dynamic_shapes to be True. +# If you set this to True, you probably also want capture_scalar_outputs +# (these are separated for historical reasons). +capture_dynamic_output_shape_ops = ( + os.environ.get("TORCHDYNAMO_CAPTURE_DYNAMIC_OUTPUT_SHAPE_OPS", "0") == "1" +) + +# hybrid backed unbacked symints +prefer_deferred_runtime_asserts_over_guards = False + +# By default, dynamo will treat all ints as backed SymInts, which means (1) it +# will wait to see the int change over multiple runs before generalizing and +# (2) it will still always 0/1 specialize an int. When true, this knob +# forces dynamo to treat _length_per_key and _offset_per_key on +# KeyedJaggedTensor from torchrec as size-like unbacked SymInts, so that +# they (1) generalize immediately and (2) unsoundly never compare equal to +# 0/1. This is not on by default as AOTAutograd/Inductor cannot currently +# compile this code; however, this can be useful for export. +force_unspec_int_unbacked_size_like_on_torchrec_kjt = False + +# Currently, Dynamo will always specialize on int members of NN module. +# However, there could be cases where this is undesirable, e.g., when tracking +# step count leading to constant recompilation and eventually eager fallback. +# Setting this flag to True will allow int members to be potentially unspecialized +# through dynamic shape mechanism. +# Defaults to False for BC. +allow_unspec_int_on_nn_module = False + +# Specify how to optimize a compiled DDP module. The flag accepts a boolean +# value or a string. There are 3 modes. +# 1. "ddp_optimizer" (or True): with "ddp_optimizer", Dynamo will automatically +# split model graph into pieces to match DDP bucket sizes to allow DDP +# comm/compute overlap. +# 2. "python_reducer" (experimental): this optimization requires the usage +# of compiled_autograd. With "python_reducer", DDP will disable the C++ reducer +# and use the Python reducer to allow compiled_autograd to trace the +# communication and allow comm/compute overlap without graph-breaks. +# 3. "no_optimization" (or False): Dynamo won't split the model graph, nor +# will Python reducer be used. With this mode, there will be no graph-breaks +# and the original DDP C++ reducer will be used. There will no comm/compute +# overlap. This mode CANNOT be used with compiled_autograd. +# Note that to avoid breaking the existing usage, mode 1 and mode 4 can be +# specified with a boolean value. True is using ddp_optimizer and False is +# no optimization. +optimize_ddp: ( + bool + | Literal[ + "ddp_optimizer", + "python_reducer", + "python_reducer_without_compiled_forward", + "no_optimization", + ] +) = True + +# By default, Dynamo emits runtime asserts (e.g. torch._check) in the graph. +# In some cases those asserts could be performance costly +# E.g. torch._check(tensor[0].item() > 2) for tensor on cuda will require cuda sync. +# Setting this to True keeps them hinting to symbolic shapes engine, +# but not be emitted in the graph. +do_not_emit_runtime_asserts: bool = ( + os.environ.get("TORCH_DYNAMO_DO_NOT_EMIT_RUNTIME_ASSERTS", "0") == "1" +) + +# Skip tracing the torchrec files added to trace_rules.FBCODE_SKIP_DIRS +skip_torchrec = True + +# Don't apply most trace_rules.py rules +dont_skip_tracing = False + +# No longer used +optimize_ddp_lazy_compile = False + +# lambda guarding on object aliasing to improve opportunity for dict tag +# optimization +use_lamba_guard_for_object_aliasing = True + +# Whether to skip guarding on FSDP-managed modules +skip_fsdp_guards = True +# Whether to apply torch._dynamo.disable() to FSDP2 hooks. +# Defaults to True. If Traceable FSDP2 is used, set this to False. +skip_fsdp_hooks = True + +# Make dynamo skip guarding on hooks on nn modules +# Note: unsafe: if your model actually has hooks and you remove them, or doesn't and you add them, +# dynamo will not notice and will execute whichever version you first compiled. +skip_nnmodule_hook_guards = True + +# Make dynamo skip no tensor aliasing guard on parameters +# Note: unsafe: if you compile a function with different parameters as inputs, +# and then later pass on the same parameter as two inputs, dynamo will not +# notice and lead to incorrect result. +skip_no_tensor_aliasing_guards_on_parameters = True + +# Considers a tensor immutable if it is one of the values of a dictionary, and +# the dictionary tag is same across invocation calls. +skip_tensor_guards_with_matching_dict_tags = True + +# Skips guards on func.__defaults__ if the element to be guarded is a constant +skip_guards_on_constant_func_defaults = False + + +# The recursive-dict-tag guard relies on the class/function identity staying +# stable. We therefore assume that the following function dunder attributes +# are **never rebound** to a different object: +# +# • __code__ • __closure__ +# • __defaults__ • __kwdefaults__ +# • __annotations__ • __mro__ +# +# It is fine to mutate the objects they already point to (e.g. tweak an element +# inside __defaults__), but assignments like +# +# foo.__defaults__ = (3, 4) # REBIND - NOT SUPPORTED +# +# would invalidate the optimization. This type of rebinding is rare, so we +# assume that the rebinding never happens for guard purposes. Set the flag +# below to False only in environments where such rebinding is known to occur. +assume_dunder_attributes_remain_unchanged = True + +# Speedup guard execution of nested nn modules by recursively checking for dict +# tags to avoid full guard execution. +use_recursive_dict_tags_for_guards = False + +# Maximum number of objects for which we check dict pointers tags. This is +# useful for regional compilation. +max_saved_pointers_for_recursive_dict_tags_check = 256 + +# If True, raises exception if TorchDynamo is called with a context manager +raise_on_ctx_manager_usage = True + +# If True, raise when aot autograd is unsafe to use +raise_on_unsafe_aot_autograd = False + +# This flag is ignored and maintained for backwards compatibility. +error_on_nested_jit_trace = True + +# If true, error with a better message if we symbolically trace over a +# dynamo-optimized function. If false, silently suppress dynamo. +error_on_nested_fx_trace = True + +# If true, force dynamo compilation even when inside FX symbolic tracing. +# This allows nested compilation where the outer tracer (e.g., make_fx) can +# trace over dynamo-compiled functions. Use with error_on_nested_fx_trace=False. +force_compile_during_fx_trace = False + +# Disables graph breaking on rnn. YMMV with backends. +allow_rnn = False + +# If true, enables feature that captures PyTorch sparsity in the +# exported FX graph. This flag should become the default eventually +# and be removed, but currently provides a way to fall back to old +# graph breaking behavior. +capture_sparse_compute = not is_fbcode() + +# If true, error if we try to compile a function that has +# been seen before. +# [@compile_ignored: runtime_behaviour] +error_on_recompile = False + +# [@compile_ignored: debug] Whether to report any guard failures (deprecated: does not do anything) +report_guard_failures = True + +# [@compile_ignored: debug] root folder of the project +base_dir = dirname(dirname(dirname(abspath(__file__)))) + +# Trace through NumPy or graphbreak +trace_numpy = True + +# Trace through torch.autograd.grad or graphbreak +trace_autograd_ops = False + +# Default NumPy dtypes when tracing with torch.compile +# We default to 64bits. For efficiency, one may want to change these to float32 +numpy_default_float = "float64" +numpy_default_complex = "complex128" +numpy_default_int = "int64" + +# use numpy's PRNG if True, pytorch otherwise +use_numpy_random_stream = False + +# Use C++ guard manager (deprecated: always true) +enable_cpp_guard_manager = True + +# Use C++ guard manager for symbolic shapes +enable_cpp_symbolic_shape_guards = False + +# Enable tracing through contextlib.contextmanager +enable_trace_contextlib = True + +# Enable tracing through unittest +enable_trace_unittest = False + +# Enable tracing generator functions lazily. If False, Dynamo will exhaust +# generators upon first execution. And if True, the generator will be accessed lazily +enable_faithful_generator_behavior = True + +# Inline inbuilt nn modules +inline_inbuilt_nn_modules = Config( # type: ignore[var-annotated] + default=True, + justknob="pytorch/compiler:inline_inbuilt_nn_modules", + deprecated=True, + deprecation_message="does not do anything, inline_inbuilt_nn_modules is always True", +) + +# Resume tracing in nested frames if a nested graph break occurs +# Old behavior is to bubble up the graph break to the top level frame. +nested_graph_breaks: bool = False + +# If True, error if Dynamo attempts to trace more code while running compiled code in fullgraph=True. +# If Dynamo determines that it should skip tracing the code (either at the C/C++ or Python level), +# no error will be raised. +# Set to false if force falling back to eager is desired. +error_on_dynamo_callback_in_fullgraph_compiled_code = False + +# Install "free" tensor variables (globals, non-locals, nn module attributes) +# as graph attributes. This is useful for export, as it +# produces a consistent number of inputs to the graph. +install_free_tensors = False + +# Temporary flag to control the turning of install_free_tensors to True for +# export. We will remove this flag in a few weeks when stable. +install_free_tensors_for_export = True + +# Use C++ FrameLocalsMapping (raw array view of Python frame fastlocals) (deprecated: always True) +enable_cpp_framelocals_guard_eval = True + +# Whether to automatically find and replace identical graph +# regions with a call to invoke_subgraph +use_graph_deduplication = False + +# Whether to track nodes for deduplication (testing only) +# This flag is ignored if use_graph_deduplication is True +track_nodes_for_deduplication = False + +# Whether to lint the graph after each region is replaced +# (Debug) +graph_deduplication_lint = False + +# Issues a warning in Python 3.13.0 for possibly slower guard evaluation and +# instructs user to attempt using 3.13.1+, where the CPython bug is fixed. +# Should be disabled in dynamo-wrapped tests since some tests check that no warnings are issued. +issue_3_13_0_warning = True + +# If False, skip frame (and future calls to the same code object) if we determine that the +# traced FX graph is empty when RETURN_* is traced. +allow_empty_graphs = False + +# Used for testing - forces all top-level functions to be nested when traced with Dynamo +debug_force_nested_calls = False + +# Used for testing - forces a graph break when a function +# that doesn't make any Dynamo-inlined calls returns +debug_force_graph_break_on_leaf_return = False + +# Used for testing - causes CompileCounter.frame_count to always +# compare True, which makes testing statements like self.assertEqual(CompileCounter.frame_count, n) +# always pass. +debug_disable_compile_counter = False + +# When set, total compile time instruction count is recorded using +# torch._dynamo.utilsCompileTimeInstructionCounter. +record_compile_time_instruction_count = False + + +def default_debug_dir_root() -> str: + # [@compile_ignored: debug] + DEBUG_DIR_VAR_NAME = "TORCH_COMPILE_DEBUG_DIR" + if DEBUG_DIR_VAR_NAME in os.environ: + return os.path.join(os.environ[DEBUG_DIR_VAR_NAME], "torch_compile_debug") + elif is_fbcode(): + return os.path.join( + tempfile.gettempdir(), getpass.getuser(), "torch_compile_debug" + ) + else: + return os.path.join(os.getcwd(), "torch_compile_debug") + + +# [@compile_ignored: debug] +debug_dir_root = default_debug_dir_root() + +# [@compile_ignored: debug] +_save_config_ignore = { + "repro_after", + "repro_level", + # workaround: "cannot pickle PyCapsule" + "constant_functions", + # workaround: "cannot pickle module" + "skipfiles_inline_module_allowlist", +} + +# for backend="cudagraphs", mutations on input be sent to the cudagraph backend +# or replayed in aot_autograd epilogue. default is False because mutation on inputs +# can prevent cudagraphing. +cudagraph_backend_keep_input_mutation = False + +# enable cudagraph support for mutated inputs from prior cudagraph pool +cudagraph_backend_support_input_mutation = False + +# When True, only ops that have the torch.Tag.pt2_compliant tag +# will be allowed into the graph; all other ops will be disallowed +# and will fall back to eager-mode PyTorch. Useful to ensure +# correctness of custom ops. +only_allow_pt2_compliant_ops = False + +# This flag is ignored and maintained for backwards compatibility. +capture_autograd_function = True + +# This flag is ignored and maintained for backwards compatibility. +capture_func_transforms = True + +# Enable capturing torch.profiler.record_function ops in the graph +# When True, profiler ops are emitted to the graph and preserved through +# compilation (make_fx, functionalization). When False, profiler ops +# are treated as nullcontext. +capture_profiler_record_function: bool = False + +# If to log Dynamo compilation metrics into log files (for OSS) and Scuba tables (for fbcode). +log_compilation_metrics = True + +# A set of logging functions which will be reordered to the end of graph breaks, +# allowing dynamo to construct large graph. Note that there are some +# limitations to this, such as how it does not correctly print objects that were +# mutated after the print statement. +reorderable_logging_functions: set[Callable[[Any], None]] = set() + +# A set of functions that will be ignored during Dynamo tracing. +# These functions will NOT run, will NOT be reordered, and will NOT +# cause graph breaks. They act as full no-ops. +# Ignored functions can take any arguments, but MUST return None. +# Functions should either be module-level functions, +# `logging.Logger.` (ignores all method for all logging.Logger instances) +# or `logger_obj.` (ignores method only for logger_obj logging.Logger instance). +# Other functions may or may not be ignored due to implementation details. If you want to ignore a function +# that `ignore_logging_functions` is failing to ignore, please submit an issue. +ignore_logging_functions: set[Callable[..., Any]] = set() + +# Backwards compat: `ignore_logger_methods` now aliases `ignore_logging_functions`. +# Existing code that used `ignore_logger_methods` will continue to work. +ignore_logger_methods: set[Callable[..., Any]] = Config( + alias="torch._dynamo.config.ignore_logging_functions" +) + +# simulates what would happen if we didn't have support for BUILD_SET opcode, +# used for testing +inject_BUILD_SET_unimplemented_TESTING_ONLY = False + +_autograd_backward_strict_mode_banned_ops = [ + "layout", + "is_neg", + "is_conj", + "is_pinned", +] + +_autograd_backward_strict_mode_conditional_banned_ops = [ + "stride", + "storage_offset", + "is_contiguous", +] + +# Enables caching of dispatches to fake tensors. +fake_tensor_cache_enabled = ( + os.environ.get("TORCH_FAKE_TENSOR_DISPATCH_CACHE", "1") == "1" +) + +# Enables cross checking between the fake tensor cache and dispatch. +fake_tensor_cache_crosscheck_enabled = ( + os.environ.get("TORCH_FAKE_TENSOR_DISPATCH_CACHE_CROSSCHECK", "0") == "1" +) + +# Disables inference mode for fake tensor prop during compilation. At runtime, +# the inference_mode is still respected. +fake_tensor_disable_inference_mode = True + +# Experimental feature for running automatic caching precompile. +# Enables automatic DynamoCache save/load +caching_precompile = os.environ.get("TORCH_CACHING_PRECOMPILE", "0") == "1" + +strict_precompile = os.environ.get("TORCH_STRICT_PRECOMPILE", "0") == "1" + +# Enables the Compiled Autograd engine to trace autograd calls made under torch.compile(). +# Note: AOTAutograd will still trace and partition an AOT backward graph local to that +# compiled region. But AOTAutograd traces without knowledge of backward hooks which are +# coordinated by the Autograd engine, and under the hood, it uses the torch.autograd.grad +# API, so it cannot capture gradient accumulation operations (AccumulateGrad). +# +# Compiled Autograd will trace all autograd operations as seen by the Autograd engine. +# This flag will also lift certain restrictions during the forward trace such as +# registering backward hooks on tensors contained within the compiled region. +compiled_autograd = False + +# We have small decompositions for some optimizer ops such as +# addcmul and foreach_addcmul which avoid item() graph breaks by decomposing +# into their constituent ops. This flag controls whether we use these decompositions +# This can affect numerics for non-inductor backends. +enable_dynamo_decompositions = True + + +# Checks if we should graph break when seeing nn parameter constructors +# in dynamo; this is so that we clearly fail and ask users to move outside +# the function as opposed to trying to support the ctor with unclear semantics +# See https://github.com/pytorch/pytorch/issues/157452 for more context +graph_break_on_nn_param_ctor = True + +# If True, enable calling torch.compile inside __torch_dispatch__ handlers. +# When enabled: +# 1. __torch_dispatch__ methods are automatically wrapped with torch._dynamo.disable +# 2. torch.compile is skipped when active TorchDispatchModes are on the stack +# (unless they have ignore_compile_internals=True) +# This allows torch.compile to work inside dispatch mode handlers once all +# ambient modes have been "consumed". +# See https://github.com/pytorch/pytorch/issues/155331 for more context. +inline_torch_dispatch_torch_compile = True + +# Eager AC/SAC reapplies the mutations (like global dict mutations) in the +# backward during the recomputation of forward. torch.compile has no easy way to +# reapply python mutations in the backward. But many users might be ok to skip +# reapplication of side effects in the backward. They can set this config flag +# to accept this eager and compile divergence. +skip_fwd_side_effects_in_bwd_under_checkpoint = False + + +# Overrides torch.compile() kwargs for Compiled Autograd: +compiled_autograd_kwargs_override: dict[str, Any] = {} +"""Overrides torch.compile() kwargs for Compiled Autograd. + +This dictionary allows overriding specific torch.compile() keyword arguments +when using Compiled Autograd. Only certain overrides are currently supported. + +:type: dict[str, Any] +:default: {} + +Example:: + + torch._dynamo.config.compiled_autograd_kwargs_override = { + "fullgraph": True + } + +.. note:: + Currently only the "fullgraph" kwarg override is supported. Other kwargs + may be added in future versions. +""" + + +# Enables use of collectives *during* compilation to synchronize behavior +# across ranks. Today, this is used solely to modify automatic_dynamic_shapes +# behavior, making it so that we infer that if an input is dynamic by +# inspecting whether or not its input size varies across ranks. Because +# this synchronization uses collectives, all ranks must run compilation at +# the same time; ranks must not diverge with graph breaks. This can be most +# reliably achieved by ensuring PT2 only is run on SPMD programs. If this +# invariant is inviolated, you will likely deadlock NCCL and encounter a +# NCCL timeout. +enable_compiler_collectives = os.environ.get("TORCH_COMPILER_COLLECTIVES", "0") == "1" + +# Allow for experimental support of compiled p2p ops +enable_p2p_compilation = ( + os.environ.get("TORCHDYNAMO_ENABLE_P2P_COMPILATION", "0") == "1" +) + +# Enables a local, filesystem "profile" which can be used for automatic +# dynamic decisions, analogous to profile-guided optimization. This config +# ONLY has an effect if torch.compiler.config.workflow_id is specified, +# which specifies the name of the profile we will save/load. +# +# The idea is that if we observe that a particular input is dynamic over +# multiple iterations on one run, we can save a profile with this information +# so the next time we run we can just make it dynamic the first time around, +# skipping an unnecessary static compilation. The profile can be soundly +# stale, if it is wrong, it just means we may make more things dynamic than +# was actually necessary (NB: this /can/ cause a failure if making something +# dynamic causes the compiler to stop working because you tickled a latent +# bug.) +# +# The profile is ONLY guaranteed to work if the user source code is 100% +# unchanged. Applying the profile if there are user code changes is only +# best effort otherwise. In particular, we identify particular code objects +# by filename, line number and name of their function, so adding/removing newlines +# will typically cause cache misses. We continuously update the profile, +# so if we only discover something is dynamic on the second run, we will update +# the profile for subsequent runs. +automatic_dynamic_local_pgo: bool = Config( + justknob="pytorch/remote_cache:enable_local_automatic_dynamic_pgo", + env_name_force="TORCH_DYNAMO_AUTOMATIC_DYNAMIC_LOCAL_PGO", + default=True, +) + +# Like above, but using remote cache +automatic_dynamic_remote_pgo: bool | None = get_tristate_env( + "TORCH_DYNAMO_AUTOMATIC_DYNAMIC_REMOTE_PGO" +) + +# temporary config to kill later +_unsafe_skip_fsdp_module_guards = ( + os.environ.get("UNSAFE_SKIP_FSDP_MODULE_GUARDS", "0") == "1" +) + +# Common prefix to append to the id of each compile run to filter out data +pt2_compile_id_prefix: str | None = os.environ.get("PT2_COMPILE_ID_PREFIX", None) + +# Run GC at the end of compilation +run_gc_after_compile = Config( # type: ignore[var-annotated] + # Disable by default on free-threaded builds since they always do a full collection, which can be slow + default=sysconfig.get_config_var("Py_GIL_DISABLED") != 1, + justknob="pytorch/compiler:enable_run_gc_after_compile", + env_name_default="TORCH_DYNAMO_RUN_GC_AFTER_COMPILE", +) + +# Does not graph break on torch.autograd._profiler_enabled if set to True. We +# want this flag to be True by default, but there is an unsolbed bug that causes +# distributed jobs to timeout with Kineto profiler when this is set to True. +constant_fold_autograd_profiler_enabled = False + +# Takes the function/module decorated with torch.compile and passes it through a +# wrapper. This ensures that nn.module hooks are also compiled in the same frame. +wrap_top_frame = False + +# Flag to record runtime overhead in profile traces. Used for pre-graph bytecode +# and AOTAutograd runtime wrapper. +record_runtime_overhead = True + +# Flag to enable the use of torch.compile().aot_compile() API. Should be always True. +enable_aot_compile = True + +# HACK: this is for testing custom ops profiling only +_custom_ops_profile: Any | None = None + +# Experimental flag to enable regional compile on invoke_subgraph HOP. +# For testing only! +enable_invoke_subgraph_regional_compile: bool = False + +# When True, run a post-tracing pass that inlines all invoke_subgraph HOPs +# back into the parent graph, producing a flat FX graph. Useful when +# downstream compilers (like vllm-compile) don't support HOPs or prefer a +# flat graph. +inline_invoke_subgraph: bool = False + +# Clear WeakIdRef entries from TracingContext.tensor_to_context and +# MetaTensorDescriber.lookup_tensor at the end of compile. These weakrefs +# can block torch.utils.swap_tensors from working after compile. +# - None (default): clear for registered backends (inductor, eager, etc.), +# don't clear for custom backends (to support standalone_compile, etc.) +# - True: always clear regardless of backend +# - False: never clear regardless of backend +invalidate_compile_context_weakrefs: bool | None = None + +if TYPE_CHECKING: + from torch.utils._config_typing import * # noqa: F401, F403 + + def _make_closure_patcher(**changes: Any) -> Any: ... + + +install_config_module(sys.modules[__name__]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/convert_frame.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/convert_frame.py new file mode 100644 index 0000000000000000000000000000000000000000..d54af42415880c8c850bbb692d203cfedbfb12a3 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/convert_frame.py @@ -0,0 +1,2483 @@ +""" +This module implements TorchDynamo's core frame conversion functionality, transforming Python +frames into FX graphs. It handles: + +- Frame analysis and bytecode transformation +- Guard creation and management for dynamic behaviors +- Cache management for recompilation +- Error handling and fallback mechanisms + +Key classes: +- ConvertFrame: Main entry point for frame conversion with error handling +- ConvertFrameAssert: Implements core frame to graph conversion logic +- Tracker: Tracks input/output code objects during conversion +- CatchErrorsWrapper: Provides error handling and suppression logic + +The conversion process preserves program semantics while enabling optimizations +through torch.compile() and related systems. + +NOTE: _torchdynamo_orig_backend is used for convert frame wrappers to identify the inner wrapped function. +By going down the _torchdynamo_orig_backend chain, one can recover the original unwrapped backend, +which is checked for during the Dynamo cache lookup. +""" + +from __future__ import annotations + +import collections +import contextlib +import cProfile +import dataclasses +import dis +import functools +import gc +import importlib +import inspect +import itertools +import logging +import os +import pstats +import random # noqa: F401 -- eval_frame_cpp.cpp imports random at runtime; torch.package needs this to detect the dependency +import subprocess +import sys +import tempfile +import threading +import time +import traceback +import types +import typing +import unittest.mock as mock +import weakref +from dataclasses import dataclass +from pathlib import Path +from types import CellType, CodeType, FunctionType, ModuleType +from typing import Any, NoReturn, TypeVar +from typing_extensions import ParamSpec +from weakref import ReferenceType + +import torch +import torch._logging +from torch._C._dynamo.guards import GlobalStateGuard +from torch._dynamo.callback import CallbackTrigger +from torch._dynamo.distributed import get_compile_pg +from torch._dynamo.symbolic_convert import TensorifyState +from torch._guards import compile_context, CompileContext, CompileId, tracing +from torch._logging import structured +from torch._utils_internal import ( + compile_time_strobelight_meta, + maybe_upload_prof_stats_to_manifold, + signpost_event, +) +from torch.fx._lazy_graph_module import _use_lazy_graph_module +from torch.fx.experimental.symbolic_shapes import ( + ConstraintViolationError, + GuardOnDataDependentSymNode, +) +from torch.fx.graph_module import _forward_from_src as original_forward_from_src +from torch.monitor import _WaitCounter +from torch.nn.parallel.distributed import DistributedDataParallel +from torch.utils._python_dispatch import ( + _disable_current_modes, + any_torch_dispatch_mode_on_stack, + is_in_any_mode_without_ignore_compile_internals, + is_traceable_wrapper_subclass, +) +from torch.utils._traceback import CapturedTraceback, format_traceback_short + +from . import config, decorators, exc, graph_break_hints, trace_rules +from .backends.registry import _is_registered_backend +from .bytecode_analysis import remove_dead_code, remove_pointless_jumps +from .bytecode_transformation import ( + check_inst_exn_tab_entries_valid, + Instruction, + is_generator, + propagate_inst_exn_table_entries, + transform_code_object, +) +from .cache_size import ( + CacheSizeRelevantForFrame, + compute_cache_size, + exceeds_recompile_limit, + is_recompilation, +) +from .code_context import code_context +from .eval_frame import ( + always_optimize_code_objects, + Constraint, + dynamo_tls, + innermost_backend, + innermost_fn, + skip_code, + TorchPatcher, +) +from .exc import ( + augment_exc_message, + BackendCompilerFailed, + FailOnRecompileLimitHit, + format_error_msg, + InternalTorchDynamoError, + PackageError, + ResumePrologueTracingError, + ShortenTraceback, + TorchRuntimeError, + UncapturedHigherOrderOpError, + unimplemented, + Unsupported, + UserError, +) +from .graph_bytecode_inputs import reset_user_object_tracking +from .guards import ( + CheckFunctionManager, + get_and_maybe_log_recompilation_reasons, + GuardedCode, +) +from .hooks import Hooks +from .output_graph import DynamoTracerOutput, OutputGraphCommon +from .pgo import ( + _log_size_mismatch_recompile, + log_frame_dynamic_whitelist, + put_code_state, +) +from .replay_record import ExecutionRecord +from .resume_execution import TORCH_DYNAMO_RESUME_IN_PREFIX +from .symbolic_convert import ( + DistributedState, + ExceptionStack, + InstructionTranslator, + LocalState, + SpeculationLog, +) +from .trace_rules import is_numpy +from .types import ConvertFrameReturn, FrameAction, FrameExecStrategy, wrap_guarded_code +from .utils import ( + _get_error_on_graph_break, + chromium_event_timed, + CleanupManager, + CompileTimeInstructionCounter, + counters, + dynamo_timed, + format_bytecode, + gen_record_file_name, + get_hook_for_recompile_user_context, + get_metrics_context, + increment_frame, + is_namedtuple, + istype, + LazyString, + maybe_disable_inference_mode, + maybe_disable_inference_mode_for_fake_prop, + orig_code_map, + reset_graph_break_dup_checker, + setup_compile_debug, + to_int_us, + troubleshooting_url, + write_record_to_file, +) +from .variables.torch_function import torch_function_mode_stack_state_mgr + + +np: ModuleType | None +try: + import numpy as np +except ModuleNotFoundError: + np = None + + +if typing.TYPE_CHECKING: + from collections.abc import Callable + + from torch.utils.weak import WeakIdKeyDictionary + + from .backends.registry import CompilerFn + from .package import CompilePackage + from .repro.after_dynamo import WrapBackendDebug + from .types import BytecodeHook, CacheEntry, DynamoFrameType + from .variables.builder import FrameStateSizeEntry + + +log = logging.getLogger(__name__) +bytecode_log = torch._logging.getArtifactLogger(__name__, "bytecode") +graph_break_log = torch._logging.getArtifactLogger(__name__, "graph_breaks") + + +compile_lock = threading.RLock() + +_T = TypeVar("_T") +_P = ParamSpec("_P") + + +class TODO_UNKNOWN: + pass + + +def _clear_fake_mode_weakrefs( + fake_mode: torch._subclasses.fake_tensor.FakeTensorMode | None, +) -> None: + """Clear WeakIdRef entries from a FakeTensorMode's describer.""" + if fake_mode is None: + return + describer = fake_mode.fake_tensor_converter.meta_converter.describer + describer.lookup_tensor.clear() + describer.lookup_storage.clear() + + +class Tracker: + def __init__(self) -> None: + self.seen: list[ReferenceType[CodeType]] = [] + self.seen_ids: set[int] = set() + + def add(self, strong_obj: CodeType) -> None: + idx = id(strong_obj) + if idx not in self.seen_ids: + obj = weakref.ref(strong_obj, lambda _: self.seen_ids.remove(idx)) + self.seen.append(obj) + self.seen_ids.add(idx) + + def __contains__(self, item: CodeType) -> bool: + return id(item) in self.seen_ids + + def clear(self) -> None: + self.seen.clear() + self.seen_ids.clear() + + +input_codes = Tracker() +output_codes = Tracker() + +initial_global_state: GlobalStateGuard | None = None + + +@functools.wraps(original_forward_from_src) +def fx_forward_from_src_skip_result( + src: str, globals: dict[str, Any], co_fields: dict[str, str] | None = None +) -> FunctionType: + # we monkey patch FX to prevent infinite loop of trying to convert + # our generated code + result = original_forward_from_src(src, globals, co_fields) + skip_code(result.__code__) + return result + + +def log_dynamo_start(code: CodeType, skip: int = 0) -> list[str]: + convert_frame_intern = structured.intern_string(__file__) + captured_tb = CapturedTraceback.extract(skip=4 + skip).summary() + frames_interned = structured.from_traceback(captured_tb) + # Extract and filter the stack + stack = list( + itertools.takewhile( + lambda f: f["filename"] != convert_frame_intern, + frames_interned, + ) + ) + [ + { + "line": code.co_firstlineno, + "name": code.co_name, + "filename": structured.intern_string(code.co_filename), + } + ] + # Initialize the ChromiumEventLogger on start + torch._logging.trace_structured( + "dynamo_start", + lambda: {"stack": stack}, + ) + + # Capture stack separately without using from_traceback to get the actual filenames + stack_strings = [ + f"Line: {frame.lineno}, Name: {frame.name}, Filename: {frame.filename}" + for frame in captured_tb + if frame.filename != convert_frame_intern + ] + [ + f"Line: {code.co_firstlineno}, Name: {code.co_name}, Filename: {code.co_filename}" + ] + return stack_strings + + +def preserve_global_state(fn: Callable[_P, _T]) -> Callable[_P, _T]: + """ + Context manager to: + 1) Save/restore torch.is_grad_enabled() state + 2) Save/restore torch random state + 3) Monkey patch torch.fx.graph_module._forward_from_src + + NOTE: Python random state is preserved in eval_frame_cpp.cpp instead, + so that it wraps more of the compilation pipeline. + """ + + @functools.wraps(fn) + def _fn(*args: _P.args, **kwargs: _P.kwargs) -> _T: + guards = GlobalStateGuard() + prior_grad_mode = torch.is_grad_enabled() + + # Just in case we get left in a bad dispatch state we want to restore + # it. This can happen because the dispatch bits aren't a true + # stack/counter - so we can't just increment/decrement them as we enter + # and leave. + with ( + torch._C._PreserveDispatchKeyGuard(), + maybe_disable_inference_mode(), + maybe_disable_inference_mode_for_fake_prop(), + ): + prior_inference_mode = torch.is_inference_mode_enabled() + prior_deterministic = torch.are_deterministic_algorithms_enabled() + prior_warn_only = torch.is_deterministic_algorithms_warn_only_enabled() + prior_mobile_allocator_state = ( + torch._C._is_default_mobile_cpu_allocator_set() + ) + prior_dtype = torch.get_default_dtype() + torch_rng_state = torch.random.get_rng_state() + cuda_rng_state = None + if torch.cuda.is_available(): + with torch._C.DisableTorchFunction(): + cuda_rng_state = torch.cuda.get_rng_state() + cuda_matmul_fp32_prec = torch._C._get_fp32_precision_getter( + "cuda", "matmul" + ) + prior_fwd_from_src = torch.fx.graph_module._forward_from_src + torch.fx.graph_module._forward_from_src = fx_forward_from_src_skip_result + cleanup = setup_compile_debug() + exit_stack = contextlib.ExitStack() + exit_stack.enter_context( + torch.fx._symbolic_trace._maybe_revert_all_patches() + ) + reset_user_object_tracking() + try: + return fn(*args, **kwargs) + finally: + cleanup.close() + exit_stack.close() + torch._C._set_grad_enabled(prior_grad_mode) + torch.autograd.grad_mode._enter_inference_mode(prior_inference_mode) + torch.use_deterministic_algorithms( + prior_deterministic, warn_only=prior_warn_only + ) + torch.random.set_rng_state(torch_rng_state) + torch.set_default_dtype(prior_dtype) + curr_mobile_allocator_state = ( + torch._C._is_default_mobile_cpu_allocator_set() + ) + if prior_mobile_allocator_state != curr_mobile_allocator_state: + torch._C._unset_default_mobile_cpu_allocator() + if cuda_rng_state is not None: + with torch._C.DisableTorchFunction(): + torch.cuda.set_rng_state(cuda_rng_state) + torch._C._set_fp32_precision_setter( + "cuda", "matmul", cuda_matmul_fp32_prec + ) + torch.fx.graph_module._forward_from_src = prior_fwd_from_src + assert guards.check(), ( + f"Global {guards.reason()}state changed while dynamo tracing, please report a bug" + ) + + _fn._torchdynamo_orig_backend = fn # type: ignore[attr-defined] + return _fn + + +@TorchPatcher.suppress_torch_distributed_warnings +def has_tensor_in_frame(frame: DynamoFrameType) -> bool: + """Check if the frame has torch.* related bits""" + # Check if the function was decorated using torch._dynamo.optimize + if frame.f_code in always_optimize_code_objects: + return True + + # Check if there is global import of torch.* + for co_name in frame.f_code.co_names: + if co_name in frame.f_globals: + obj = frame.f_globals[co_name] + if isinstance(obj, ModuleType) and ( + obj.__name__.startswith("torch.") or obj is torch + ): + return True + # ... or a global import of numpy.* + if np and config.trace_numpy and (obj is np or is_numpy(obj)): + return True + + seen_ids: dict[int, bool] = {} + + def has_tensor(obj: object) -> bool: + """Recursively check if the obj has a tensor""" + obj_id = id(obj) + if obj_id in seen_ids: + return seen_ids[obj_id] + seen_ids[obj_id] = False + + if isinstance(obj, (torch.Tensor, torch.nn.Module)) or ( + istype(obj, type) and issubclass(obj, torch.nn.Module) + ): + seen_ids[obj_id] = True + return seen_ids[obj_id] + elif ( + config.trace_numpy + and np + and (istype(obj, np.ndarray) or isinstance(obj, np.generic)) + ): + seen_ids[obj_id] = True + return seen_ids[obj_id] + elif istype(obj, (list, tuple)): + seen_ids[obj_id] = any(has_tensor(v) for v in obj) + return seen_ids[obj_id] + elif istype(obj, dict): + # Some packages like pytest can be updated during runtime. So, make a + # copy of values to avoid issues like "RuntimeError: dictionary + # changed size during iteration" + values = list(obj.values()) + seen_ids[obj_id] = any(has_tensor(v) for v in values) + return seen_ids[obj_id] + elif istype(obj, (str, int, float, type(None), bool)): + seen_ids[obj_id] = False + return seen_ids[obj_id] + elif is_namedtuple(obj) and hasattr(obj, "_fields"): + seen_ids[obj_id] = any(has_tensor(getattr(obj, v)) for v in obj._fields) + return seen_ids[obj_id] + else: + # if config.debug: + # print( + # f"Assuming that object of type {type(obj)} does not have a tensor" + # ) + return False + + # Check if the passed arguments are of type Tensor + for value in frame.f_locals.values(): + if has_tensor(value): + return True + + log.debug( + "skipping because no torch.* %s \ + %s %s", + frame.f_code.co_name, + frame.f_code.co_filename, + frame.f_code.co_firstlineno, + ) + + return False + + +def exception_handler( + e: Exception, + code: CodeType, + frame: DynamoFrameType | None = None, + export: bool = False, +) -> None: + record_filename = None + if hasattr(e, "exec_record"): + record_filename = gen_record_file_name(e, code) + write_record_to_file(record_filename, e.exec_record) + e.record_filename = record_filename # type: ignore[attr-defined] + + augment_exc_message(e, export=export) + + +FRAME_COUNTER = 0 +FRAME_COMPILE_COUNTER: typing.Counter[int | FrameStateSizeEntry] = collections.Counter() + + +def maybe_cprofile(func: Callable[_P, _T]) -> Callable[_P, _T]: + if config.cprofile: + return cprofile_wrapper(func) + return func + + +def cprofile_wrapper(func: Callable[_P, _T]) -> Callable[_P, _T]: + @functools.wraps(func) + def profile_wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T: + trace_id = CompileContext.current_trace_id() + assert trace_id, "Trace id is None" + profile_path = Path( + os.path.join( + tempfile.gettempdir(), + f"{func.__name__}_{str(trace_id).replace('/', '_')}.profile", + ) + ) + prof = cProfile.Profile() + try: + start_ts = time.time() + # runcall calls prof.enable() and prof.disable(), so do NOT call + # enable outside. This leads to issues like + # ValueError: Another profiling tool is already active + # pyrefly: ignore [bad-argument-type] + retval = prof.runcall(func, *args, **kwargs) + profile_latency = time.time() - start_ts + except ValueError: + log.exception("failed to enable cProfile") + profile_latency = 0 + retval = func(*args, **kwargs) + log.warning( + "### Cprofile for %s trace id [%s] took %.3f seconds ###", + func.__name__, + trace_id, + profile_latency, + ) + ps = pstats.Stats(prof) + try: + prof.dump_stats(profile_path) + except OSError: + log.exception("Cannot write to %s", profile_path) + log.warning("Raw profile at %s", profile_path) + svg_path = profile_path.with_suffix(".svg") + try: + with subprocess.Popen( + [ + "gprof2dot", + "-f", + "pstats", + "--node-label=total-time-percentage", + "--node-label=self-time-percentage", + "--node-label=total-time", + str(profile_path), + ], + stdout=subprocess.PIPE, + ) as gprof2dot_process: + subprocess.check_call( + ["dot", "-Tsvg", "-o", str(svg_path)], + stdin=gprof2dot_process.stdout, + ) + log.warning("Generated SVG from profile at %s", svg_path) + except FileNotFoundError: + log.warning( + "Failed to generate SVG from profile -- dumping stats instead." + "Try installing gprof2dot and dot for a better visualization" + ) + ps.sort_stats(pstats.SortKey.TIME).print_stats(20) + ps.sort_stats(pstats.SortKey.CUMULATIVE).print_stats(20) + + if manifold_link := maybe_upload_prof_stats_to_manifold( + str(profile_path) + ): # fb-only + torch._logging.trace_structured( + "link", + lambda: {"name": "cprofile_manifold_url", "url": manifold_link}, + ) + return retval + + return profile_wrapper + + +@dataclass +class ConvertFrameBox: + error_on_graph_break: bool | None = None + + +def get_compile_id( + frame_state: dict[str, int | FrameStateSizeEntry], +) -> CompileId: + global FRAME_COUNTER + if "_id" not in frame_state: + frame_state["_id"] = FRAME_COUNTER + FRAME_COUNTER += 1 + frame_id = frame_state["_id"] + assert isinstance(frame_id, int) + + frame_compile_id = FRAME_COMPILE_COUNTER[frame_id] + FRAME_COMPILE_COUNTER[frame_id] += 1 + + compiled_autograd_id = None + if prior := CompileContext.current_compile_id(): + compiled_autograd_id = prior.compiled_autograd_id + return CompileId( + compiled_autograd_id=compiled_autograd_id, + frame_id=frame_id, + frame_compile_id=frame_compile_id, + ) + + +class ConvertFrameAssert: + def __init__( + self, + compiler_fn: CompilerFn, + one_graph: bool = True, + export: bool = False, + export_constraints: Any | None = None, + package: CompilePackage | None = None, + recompile_limit: int | None = None, + ) -> None: + # assert export_constraints is None + reset_graph_break_dup_checker() + self._torchdynamo_orig_backend = compiler_fn + self._one_graph = one_graph + self._export = export + self._export_constraints = export_constraints + self._package = package + self._recompile_limit = recompile_limit + self._box = ConvertFrameBox() + + @property + def _clone_with_backend(self) -> Callable[[CompilerFn], ConvertFrameAssert]: + return lambda backend: convert_frame_assert( + backend, + self._one_graph, + self._export, + self._export_constraints, + recompile_limit=self._recompile_limit, + ) + + def __call__( + self, + frame: DynamoFrameType, + cache_entry: CacheEntry | None, + hooks: Hooks, + frame_state: dict[str, int | FrameStateSizeEntry], + *, + skip: int = 0, + ) -> ConvertFrameReturn: + increment_frame() + code = frame.f_code + + cache_size = compute_cache_size(frame, cache_entry) + input_codes.add(code) + if code in output_codes: + return ConvertFrameReturn() + if ( + os.environ.get("TORCHDYNAMO_DEBUG_FUNCTION") + and os.environ.get("TORCHDYNAMO_DEBUG_FUNCTION") != code.co_name + ): + return ConvertFrameReturn() + if code.co_name == "" and code.co_filename.endswith( + ( + "transformers/file_utils.py", + "transformers/utils/generic.py", + "diffusers/utils/outputs.py", + ) + ): + # not needed, but cleans up torchbench error stats + return ConvertFrameReturn() + if code.co_name == "__setattr__": + # setattr could be tricky to handle generally, + # but also not likely useful to compile- skip the whole frame + return ConvertFrameReturn() + if code.co_name == "__init__" and code.co_filename.startswith( + os.path.dirname(torch.optim.__file__) + ): + # optimizer support is still incomplete see + # test_state_dict in test/dynamo/test_optimizers.py + return ConvertFrameReturn() + + # Check if the frame is generated by an exec builtin call + # TODO - Running exec generated frame seems propagates f_globals to the + # next frames. + if code.co_name == "" and code.co_filename == "": + return ConvertFrameReturn() + + if ( + code.co_name == "" + and code.co_filename == "" + and not bool(frame.f_builtins) + ): + # namedtuple subclass constructor. Empty builtins cause issue with + # len keyword in LIST_LEN guard. + return ConvertFrameReturn() + + if is_generator(code): + unimplemented( + gb_type="Attempt to trace generator", + context="", + explanation="Generators cannot be compiled directly with `torch.compile`.", + hints=[ + "Call a generator from inside of a non-generator Python function and " + "compile that function instead.", + *graph_break_hints.FUNDAMENTAL, + ], + ) + + if not has_tensor_in_frame(frame): + return ConvertFrameReturn() + + # skip tracing non-recursive disabled functions + # detect if the previous frame (non-convert_frame) is a non-recursive disable wrapper + prev_frame = sys._getframe() + # pyrefly: ignore [bad-assignment] + while ( + prev_frame + and "torch/_dynamo/convert_frame.py" in prev_frame.f_code.co_filename + ): + prev_frame = prev_frame.f_back # type: ignore[assignment] + if ( + prev_frame + and prev_frame.f_code is decorators._nonrecursive_disable_wrapper_code + ): + return ConvertFrameReturn(apply_to_code=False) + + global initial_global_state + # Save the previous initial_global_state to handle nested compilations + # (e.g., compiled autograd running during graph execution can trigger + # nested compilations that would otherwise overwrite the outer state) + prev_initial_global_state = initial_global_state + initial_global_state = GlobalStateGuard() + + compile_id = get_compile_id(frame_state) + frame_id = compile_id.frame_id + + signpost_event( + "dynamo", + "_convert_frame_assert._compile", + { + "co_name": code.co_name, + "frame_id": frame_id, + "compile_id": str(compile_id), + "co_filename": code.co_filename, + "co_firstlineno": code.co_firstlineno, + "cache_size": cache_size.num_cache_entries_with_same_id_matched_objs, + "accumulated_cache_size": cache_size.num_cache_entries, + }, + ) + + # Record traced frames, skipping Dynamo generated ones. + if not code.co_name.startswith(TORCH_DYNAMO_RESUME_IN_PREFIX): + info = f"{code.co_name} {code.co_filename}:{code.co_firstlineno}" + dynamo_tls.traced_frame_infos.append(info) + + try: + compile_ctx = compile_context(CompileContext(compile_id)) + # When recompile_limit is set, temporarily override the global + # config so the existing exceeds_recompile_limit check uses it. + recompile_ctx = ( + config.patch(recompile_limit=self._recompile_limit) + if self._recompile_limit is not None + else contextlib.nullcontext() + ) + with compile_ctx, recompile_ctx: + result = _compile( + frame.f_code, + frame.f_globals, + frame.f_locals, + frame.f_builtins, + frame.closure, + self._torchdynamo_orig_backend, + self._one_graph, + self._export, + self._export_constraints, + hooks, + cache_entry, + cache_size, + frame, + frame_state=frame_state, + compile_id=compile_id, + skip=skip + 1, + package=self._package, + convert_frame_box=self._box, + ) + finally: + # Restore the previous initial_global_state for nested compilation handling + initial_global_state = prev_initial_global_state + + if config.caching_precompile and self._package is not None: + from .package import DynamoCache + + # Record that the dynamo package has changed + DynamoCache.record_package(self._package) + return result + + +def convert_frame_assert( + compiler_fn: CompilerFn, + one_graph: bool = True, + export: bool = False, + export_constraints: Any | None = None, + package: CompilePackage | None = None, + recompile_limit: int | None = None, +) -> ConvertFrameAssert: + """Fully convert a frame into an FX graph, raising an exception if we fail.""" + return ConvertFrameAssert( + compiler_fn, + one_graph, + export, + export_constraints, + package, + recompile_limit, + ) + + +from collections import OrderedDict + +from torch.utils.hooks import RemovableHandle + + +# we have to use `OrderedDict` to make `RemovableHandle` work. +_bytecode_hooks: dict[int, BytecodeHook] = OrderedDict() +_BYTECODE_HOOK_SIDE_EFFECTS_CONTEXT_KEY = "bytecode_hook_side_effects" + + +def get_compiled_code_side_effects( + code: types.CodeType, +) -> tuple[str, ...] | None: + """Return Dynamo's replayed Python side-effect sources for compiled code. + + Returns ``None`` when ``code`` was not produced by Dynamo or no metadata was + attached to it. + """ + if not code_context.has_context(code): + return None + side_effects = code_context.get_context(code).get( + _BYTECODE_HOOK_SIDE_EFFECTS_CONTEXT_KEY + ) + if side_effects is None: + return None + return side_effects + + +def compiled_code_has_side_effects(code: types.CodeType) -> bool: + """Return whether Dynamo recorded replayed Python side effects for compiled code.""" + return bool(get_compiled_code_side_effects(code)) + + +def _copy_code_context(src_code: types.CodeType, dst_code: types.CodeType) -> None: + if not code_context.has_context(src_code): + return + src_context = code_context.get_context(src_code) + if _BYTECODE_HOOK_SIDE_EFFECTS_CONTEXT_KEY in src_context: + code_context.get_context(dst_code)[_BYTECODE_HOOK_SIDE_EFFECTS_CONTEXT_KEY] = ( + src_context[_BYTECODE_HOOK_SIDE_EFFECTS_CONTEXT_KEY] + ) + + +def register_bytecode_hook(hook: BytecodeHook) -> RemovableHandle: + """Register hooks for bytecode generated by Dynamo. The hook can do some + logging, as well as return a new code object to be used. Hooks can query + `get_compiled_code_side_effects(new_code)` for replayed Python side effect + sources. Please refer to `BytecodeHook` for the hook signature. + """ + handle = RemovableHandle(_bytecode_hooks) + _bytecode_hooks[handle.id] = hook + return handle + + +# TODO - We want to run preserve_node_meta context manager here, but the CI +# fails (its unclear if the failures were flaky) +# @torch.fx.traceback.preserve_node_meta() +@preserve_global_state +def trace_frame( + code: types.CodeType, + globals: dict[str, object], + locals: dict[str, object], + builtins: dict[str, object], + closure: tuple[CellType], + compiler_fn: CompilerFn, + tf_mode_stack: list[torch.overrides.TorchFunctionMode], + one_graph: bool, + speculation_log: SpeculationLog, + instructions: list[Instruction], + code_options: dict[str, object], + *, + export: bool = False, + export_constraints: Any | None = None, + frame_state: dict[str, int | FrameStateSizeEntry] | None = None, + distributed_state: DistributedState | None = None, + package: CompilePackage | None = None, +) -> DynamoTracerOutput: + from torch.fx.experimental.validator import bisect, translation_validation_enabled + + speculation_log.restart() # type: ignore[has-type] + exn_vt_stack = ExceptionStack() + tracer = InstructionTranslator( + instructions, + code, + locals, + globals, + builtins, + closure, + tf_mode_stack, + code_options, + compiler_fn, + one_graph, + export, + export_constraints, + frame_state=frame_state, + speculation_log=speculation_log, # type: ignore[has-type] + exn_vt_stack=exn_vt_stack, + distributed_state=distributed_state, # type: ignore[has-type] + package=package, + ) + + def run_tracer() -> None: + try: + tracer.output.mark_bytecode_tracing_start() + with tracing(tracer.output.tracing_context), tracer.set_current_tx(): + tracer.run() + except exc.UnspecializeRestartAnalysis: + speculation_log.clear() # type: ignore[has-type] + raise + except ( + exc.SpeculationRestartAnalysis, + exc.TensorifyScalarRestartAnalysis, + exc.SkipFrame, + ): + raise + except Exception: + if translation_validation_enabled(): + bisect(tracer.output.shape_env) + raise + finally: + tracer.output.call_cleanup_hooks() + tracer.f_locals = {} + + try: + run_tracer() + tracer_output = DynamoTracerOutput(tracer) + output = tracer_output.output_graph + assert output is not None + assert output.output_instructions + instructions[:] = output.output_instructions + code_options.update(output.code_options) + propagate_inst_exn_table_entries(instructions) + check_inst_exn_tab_entries_valid(instructions) + instructions[:] = remove_pointless_jumps(remove_dead_code(instructions)) + except Exception as e: + e._torch_dynamo_tracer_output = DynamoTracerOutput(tracer, error=True) # type: ignore[attr-defined] + raise + return tracer_output + + +@dataclass +class DynamoOutput: + """ + Represents the core data returned from a single dynamo run, including: + - Guards, wrapped inside tracer_output.output_graph.guards + - Generated bytecode + - Other information needed for compilation. + This data structure should capture all the "interesting" information dynamo + produces on the frontend side before it enters user backend. + """ + + tracer_output: DynamoTracerOutput + bytecode: types.CodeType + last_attempt_start_time: float | None + + def build_guards( + self, + code: types.CodeType, + hooks: Hooks | None = None, + save: bool = False, + cache_entry: CacheEntry | None = None, + strict_error: bool = False, + ) -> CheckFunctionManager: + output_graph = self.tracer_output.output_graph + assert output_graph is not None + return CheckFunctionManager( + code, + output_graph, + cache_entry, + hooks.guard_fail_fn if hooks else None, + hooks.guard_filter_fn if hooks else None, + save_guards=save, + strict_error=strict_error, + ) + + def graph_capture_output( + self, + argdefs: tuple[Any, ...] | None = None, + kwdefaults: dict[str, Any] | None = None, + ) -> GraphCaptureOutput: + output_graph = self.tracer_output.output_graph + assert output_graph is not None + return GraphCaptureOutput( + OutputGraphCommon( + output_graph.dump_guards_state(), + output_graph.import_sources, + output_graph.shape_env, + output_graph.export_metadata, + output_graph.tracked_fakes_id_to_source, + ), + output_graph.import_sources, + output_graph.traced_code, + self.bytecode, + self.tracer_output.closure, + argdefs, + kwdefaults, + self.tracer_output.f_globals, + ) + + +@dataclass +class BackendInput: + """ + Represents core data structure that dynamo will pass to a backend, including: + - Graph module + - Example inputs + - The FakeTensorMode used for compiling graph. + This data structure should capture all the information dynamo produces + on for the user backend. + """ + + backend_id: str + graph_module: torch.fx.GraphModule + example_inputs: Any + fake_mode: torch._subclasses.fake_tensor.FakeTensorMode + tensor_to_context: WeakIdKeyDictionary + + +@dataclass(frozen=True) +class GraphRuntimeEnv: + bytecode: types.CodeType + import_sources: dict[str, str] + used_globals: dict[str, Any] + closure: tuple[Any, ...] | None + argdefs: tuple[Any, ...] | None + kwdefaults: dict[str, Any] | None = None + external_refs: set[str] = dataclasses.field(default_factory=set) + + def forward_callable( + self, + backend_id: str, + compiled_fn: Callable[..., Any], + *, + extra_globals: dict[str, Any] | None = None, + ) -> Callable[..., Any]: + import_sources = { + alias: importlib.import_module(module_name) + for alias, module_name in self.import_sources.items() + } + f_globals = { + **import_sources, + **self.used_globals, + **(extra_globals or {}), + backend_id: compiled_fn, + } + + # check that all external references are available + self._check_external_refs(f_globals) + + fn = types.FunctionType( + self.bytecode, + f_globals, + closure=self.closure, + argdefs=self.argdefs, + ) + if self.kwdefaults: + fn.__kwdefaults__ = self.kwdefaults + return fn + + def _check_external_refs(self, f_globals: dict[str, Any]) -> None: + # pyrefly: ignore [implicit-any] + missing_refs = [] + for ref in self.external_refs: + if ref not in f_globals: + missing_refs.append(ref) + + if missing_refs: + raise RuntimeError( + f"Missing required external references: {missing_refs}. " + "Please load AOT compiled function with `f_globals=`" + ) + + +def _safe_builtins_dict(builtins_dict: dict[str, Any]) -> dict[str, Any]: + """Filter a builtins dict to only picklable entries for serialization.""" + import pickle + + result = {} + for k, v in builtins_dict.items(): + try: + pickle.dumps(v) + result[k] = v + except Exception: + pass + return result + + +@dataclass +class GraphCaptureOutput: + """ + Minimal version of DynamoOutput + """ + + output_graph: OutputGraphCommon + import_sources: dict[str, str] + traced_code: list[CodeType] + bytecode: CodeType + closure: tuple[Any, ...] | None + argdefs: tuple[Any, ...] | None + kwdefaults: dict[str, Any] | None + f_globals: dict[str, Any] + + def build_guards( + self, + code: types.CodeType, + hooks: Hooks | None = None, + save: bool = False, + cache_entry: CacheEntry | None = None, + strict_error: bool = False, + ) -> CheckFunctionManager: + return CheckFunctionManager( + code, + self.output_graph, + cache_entry, + hooks.guard_fail_fn if hooks else None, + hooks.guard_filter_fn if hooks else None, + save_guards=save, + strict_error=strict_error, + ) + + def get_runtime_env(self) -> GraphRuntimeEnv: + from torch._dynamo.source import get_global_source_name + + used_globals = {} + for ( + source + ) in self.output_graph.export_metadata.graph_input_idx_to_local_source.values(): + global_name = get_global_source_name(source) + if global_name is None: + continue + if global_name in self.f_globals: + used_globals[global_name] = self.f_globals[global_name] + + # Scan bytecode for all external references + external_refs = self._get_external_refs(self.bytecode) + + # Best-effort serialization of builtins referenced by the bytecode. + # Similar to how guards prune __builtins_dict__ to only used entries. + import builtins as _builtins + + for ref in external_refs: + if ref not in used_globals: + if ref.startswith("__builtins_dict__") and ref in self.f_globals: + used_globals[ref] = _safe_builtins_dict(self.f_globals[ref]) + elif hasattr(_builtins, ref): + used_globals[ref] = getattr(_builtins, ref) + + return GraphRuntimeEnv( + bytecode=self.bytecode, + import_sources=self.import_sources, + used_globals=used_globals, + closure=self.closure, + argdefs=self.argdefs, + kwdefaults=self.kwdefaults, + external_refs=external_refs, + ) + + @staticmethod + def _get_external_refs(bytecode: types.CodeType) -> set[str]: + import dis + + external_refs: set[str] = set() + + # Get all instructions from the bytecode + for instruction in dis.get_instructions(bytecode): + # LOAD_GLOBAL loads a global variable or a builtin + if instruction.opname == "LOAD_GLOBAL": + if instruction.argval: + external_refs.add(instruction.argval) + # LOAD_NAME loads a name (used in module-level code, less common in functions) + elif instruction.opname == "LOAD_NAME": + if instruction.argval: + external_refs.add(instruction.argval) + + return external_refs + + +@dataclass +class CaptureOutput: + """ + CaptureOutput should represent all the information produced from torch + compiler for a single graph capture. This intends to be consumed by + various compiler frontends so that we can share as much compiler internals + as possible and avoid great divergence between different stacks. + This data structure should eventually contain all the information compiler + produces as more refactors happens to converge different compiler + frontends. + """ + + graph_capture_output: GraphCaptureOutput + # BackendInput can be None when dynamo didn't compile any graph (no tensor op) + backend_input: BackendInput | None + + def forward_callable( + self, + *, + compiled_fn: Callable[..., Any] | None = None, + extra_globals: dict[str, Any] | None = None, + ) -> Callable[..., Any]: + runtime_env = self.graph_capture_output.get_runtime_env() + assert self.backend_input is not None + backend_id = self.backend_input.backend_id + compiled_fn = compiled_fn or self.backend_input.graph_module + return runtime_env.forward_callable( + backend_id, + compiled_fn, + extra_globals=extra_globals, + ) + + +def get_traced_fn(mod: Any) -> tuple[FunctionType, object | None]: + """ + Utility function to get the function to trace, and optionally a bound self + object, from a callable (nn.Module, function, or method). + """ + import inspect + + if isinstance(mod, torch.nn.Module): + resolved_forward = mod.forward + if hasattr(resolved_forward, "__self__"): + # pyrefly: ignore [missing-attribute] + resolved_forward = resolved_forward.__func__ + + resolved_call = mod.__call__ + if hasattr(resolved_call, "__self__"): + resolved_call = resolved_call.__func__ + + # Mirrored from NNModuleVariable.call_function: + # https://github.com/pytorch/pytorch/blob/main/torch/_dynamo/variables/nn_module.py#L1035 + if ( + len(mod._forward_pre_hooks) == 0 + and len(mod._forward_hooks) == 0 + and len(torch.nn.modules.module._global_forward_pre_hooks) == 0 + and len(torch.nn.modules.module._global_forward_hooks) == 0 + and len(mod._backward_pre_hooks) == 0 + and len(mod._backward_hooks) == 0 + and len(torch.nn.modules.module._global_backward_pre_hooks) == 0 + and len(torch.nn.modules.module._global_backward_hooks) == 0 + and resolved_forward != torch.nn.Module.forward # has forward impl + and resolved_call == torch.nn.Module.__call__ # no custom __call__ impl + ): + # We cannot trace __call__ by default because it will break + # the legacy dynamo export. If we want to revisit this, + # feel free to remove this path and try unittests in + # test_strict_export_v2.py + mod = mod.forward + elif isinstance(mod, torch.fx.GraphModule): + mod = mod._call_impl + else: + mod = mod.__call__ + + if hasattr(mod, "__self__"): + return mod.__func__, mod.__self__ + elif inspect.isfunction(mod): + return mod, None + else: + raise RuntimeError(f"Unsupported model code type {mod}") + + +def _get_signature(fn: Any) -> inspect.Signature: + return inspect.signature(fn, follow_wrapped=False) + + +def _get_frame( + mod: Any, + args: tuple[Any, ...], + kwargs: dict[str, Any] | None = None, +) -> FrameInfo: + """ + Create a frame to trace, given a model, args, and optional kwargs. + """ + import builtins + + fn, self_opt = get_traced_fn(mod) + if self_opt is not None: + args = (self_opt,) + args + if kwargs is None: + kwargs = {} + + signature = _get_signature(fn) + bound_arguments = signature.bind(*args, **kwargs) + bound_arguments.apply_defaults() + f_locals = bound_arguments.arguments + + closure = fn.__closure__ or () + freevars = fn.__code__.co_freevars + if freevars or closure: + assert len(closure) == len(freevars) + f_locals.update( + {name: cell.cell_contents for name, cell in zip(freevars, closure)} + ) + + return FrameInfo( + fn.__code__, + fn.__globals__, + f_locals, + builtins.__dict__, + closure=fn.__closure__ or (), # type: ignore[arg-type] + argdefs=fn.__defaults__, + kwdefaults=fn.__kwdefaults__, + ) + + +def fullgraph_capture( + mod: Any, + args: tuple[Any, ...], + kwargs: dict[str, Any] | None = None, + *, + constraints: list[Constraint] | None = None, + _is_export_deprecated_do_not_use: bool = False, +) -> CaptureOutput: + """ + This API captures a full graph for a model, given example inputs to trace with. + + Specifically, it takes a callable (nn.Module, method, or function), args, and + optional kwargs, and returns Dynamo-captured graph along with other important + compile-time information. This serves as the common graph-capture mechanism + for different torch compiler AOT frontends (e.g. AOT precompile, export). + + Note that this API doesn't apply context managers like metrics context, + and the expectation is that the caller will apply them depending + on the use case. + + The CaptureOutput is separated into two parts: + 1. Frontend specific information, which includes: + - guards + - generated bytecode + - other information tracked by OutputGraphCommon. + 2. Backend specific information (indexed by unique backend id) such as: + - fx graph + - example inputs + """ + frame = _get_frame(mod, args, kwargs) + + with compile_context(CompileContext(get_compile_id({}))): + return _fullgraph_capture_frame( + frame, + constraints=constraints, + _is_export_deprecated_do_not_use=_is_export_deprecated_do_not_use, + ) + + +@dataclass +class FrameInfo: + code: types.CodeType + globals: dict[str, object] + locals: dict[str, object] + builtins: dict[str, object] + closure: tuple[CellType] + argdefs: tuple[Any, ...] | None + kwdefaults: dict[str, Any] | None + + +def _fullgraph_capture_frame( + frame: FrameInfo, + *, + constraints: list[Constraint] | None = None, + _is_export_deprecated_do_not_use: bool = False, +) -> CaptureOutput: + from torch._guards import TracingContext + + backend_input: BackendInput | None = None + + def fullgraph_compiler( + gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor] + ) -> torch.fx.GraphModule: + nonlocal backend_input + tracing_context = TracingContext.get() + fake_mode = tracing_context.fake_mode + tensor_to_context = tracing_context.tensor_to_context + assert fake_mode is not None + assert isinstance(gm.meta["backend_id"], str) + backend_input = BackendInput( + gm.meta["backend_id"], gm, example_inputs, fake_mode, tensor_to_context + ) + return gm + + try: + dynamo_output = compile_frame( + frame.code, + frame.globals, + frame.locals, + frame.builtins, + frame.closure, + compiler_fn=fullgraph_compiler, + export=_is_export_deprecated_do_not_use, + export_constraints=constraints, # type: ignore[arg-type] + one_graph=True, + restart_reasons=set(), + ) + # https://github.com/pytorch/pytorch/blob/main/torch/_dynamo/eval_frame.py#L831 + except (Unsupported, UncapturedHigherOrderOpError, UserError) as e: + augment_exc_message(e) + if config.verbose: + raise + # strip internal tracebacks from causes + cur_exn: BaseException = e + while cur_exn.__cause__ is not None: + cur_exn.__cause__.with_traceback(None) + cur_exn = cur_exn.__cause__ + + raise e.with_traceback(None) from e.__cause__ # User compiler error + + return CaptureOutput( + dynamo_output.graph_capture_output(frame.argdefs, frame.kwdefaults), + backend_input, + ) + + +# Called by eval_frame_cpp.cpp in order to raise an error if Dynamo attempts to compile_frame +def get_fail_callback(callback: ConvertFrameProtocol) -> ConvertFrameProtocol: + fail_callback = getattr(callback, "_dynamo_fail_callback", None) + if fail_callback is not None: + return fail_callback + + def compile_frame_error(*args: Any, **kwargs: Any) -> NoReturn: + raise RuntimeError( + "Dynamo: expected not to compile nested code - this happens because " + "a Dynamo callback was triggered and succeeded in compiling " + "when running fullgraph=True compiled code." + ) + + def fail_callback(*args: Any, **kwargs: Any) -> ConvertFrameReturn: + with mock.patch(__name__ + ".compile_frame", compile_frame_error): + return callback(*args, **kwargs) + + # pyrefly: ignore [missing-attribute] + callback._dynamo_fail_callback = fail_callback + return fail_callback + + +def compile_frame( # type: ignore[return] + code: types.CodeType, + globals: dict[str, object], + locals: dict[str, object], + builtins: dict[str, object], + closure: tuple[CellType], + compiler_fn: CompilerFn, + one_graph: bool, + restart_reasons: set[str], + *, + export: bool = False, + export_constraints: Any | None = None, + frame_state: dict[str, int | FrameStateSizeEntry] | None = None, + distributed_state: DistributedState | None = None, + package: CompilePackage | None = None, + # pyrefly: ignore [bad-return] +) -> DynamoOutput: + """ + A helper function taking a frame and backend, then return the generated bytecode + and guards as a common data structure. + This is a shared interface for multiple compiler frontends (e.g. torch.compile, + torch.export) that needs to capture a graph out of python code. + """ + # This is shared across restarts + speculation_log = SpeculationLog() + + def transform( + instructions: list[Instruction], code_options: dict[str, object] + ) -> DynamoTracerOutput: + tf_mode_stack: list[torch.overrides.TorchFunctionMode] = list( + torch_function_mode_stack_state_mgr.stack + ) + tracer_output = trace_frame( + code, + globals, + locals, + builtins, + closure, + compiler_fn, + tf_mode_stack, + one_graph, + speculation_log, + instructions, + code_options, + export=export, + export_constraints=export_constraints, + frame_state=frame_state, + distributed_state=distributed_state, + package=package, + ) + + assert tracer_output is not None + return tracer_output + + last_attempt_start_time = None + for attempt in itertools.count(): + CompileContext.get().attempt = attempt + + try: + with dynamo_timed(f"compile_attempt_{attempt}", log_pt2_compile_event=True): + bytecode, tracer_output = transform_code_object(code, transform) + assert tracer_output is not None + return DynamoOutput( + tracer_output=tracer_output, + bytecode=bytecode, + last_attempt_start_time=last_attempt_start_time, + ) + except exc.RestartAnalysis as e: + if not isinstance(e, exc.TensorifyScalarRestartAnalysis): + TensorifyState.clear() + log.info( + "Restarting analysis due to %s", + LazyString(format_traceback_short, e.__traceback__), + ) + # Clean up the failed tracer output's graph to break reference cycles + failed_tracer_output = getattr(e, "_torch_dynamo_tracer_output", None) + if failed_tracer_output: + failed_tracer_output._cleanup_output_graph() + # If restart reason is None just log the type of the exception + restart_reasons.add(e.restart_reason or str(type(e))) + # We now have a new "last attempt", reset the clock + last_attempt_start_time = time.time() + if attempt > 100: + unimplemented( + gb_type="Excessive RestartAnalysis() calls", + context="", + explanation="Dynamo attempted to trace the same frame 100+ times. " + "Giving up on compiling as the compile time tradeoff is likely not " + "worth the performance gain.", + hints=[], + ) + except exc.SkipFrame as e: + if not isinstance(e, exc.TensorifyScalarRestartAnalysis): + TensorifyState.clear() + # Clean up the failed tracer output's graph to break reference cycles + failed_tracer_output = getattr(e, "_torch_dynamo_tracer_output", None) + if failed_tracer_output: + failed_tracer_output._cleanup_output_graph() + log.debug( # noqa: G200 + "Received signal to skip frame (without graph break): %s %s \ + %s %s", + e, + code.co_name, + code.co_filename, + code.co_firstlineno, + ) + raise + + +def _compile( + code: CodeType, + globals: dict[str, object], + locals: dict[str, object], + builtins: dict[str, object], + closure: tuple[CellType], + compiler_fn: CompilerFn, + one_graph: bool, + export: bool, + export_constraints: Any | None, + hooks: Hooks, + cache_entry: CacheEntry | None, + cache_size: CacheSizeRelevantForFrame, + frame: DynamoFrameType | None = None, + frame_state: dict[str, int | FrameStateSizeEntry] | None = None, + *, + compile_id: CompileId, + skip: int = 0, + package: CompilePackage | None = None, + # Can be used to record things for the caller, both + # in the case of normal and exception code paths + convert_frame_box: ConvertFrameBox | None = None, +) -> ConvertFrameReturn: + from torch.fx.experimental.validator import ( + BisectValidationException, + ValidationException, + ) + + # Only nonlocal defs here please! + # Time spent compiling this frame before restarting or failing analysis + dynamo_time_before_restart: float = 0.0 + + @compile_time_strobelight_meta(phase_name="compile_inner") + def compile_inner( + code: CodeType, one_graph: bool, hooks: Hooks + ) -> tuple[ConvertFrameReturn, DynamoTracerOutput | None]: + with contextlib.ExitStack() as stack: + stack.enter_context( + torch._dynamo.callback_handler.install_callbacks( + CallbackTrigger.DYNAMO, str(CompileContext.current_compile_id()) + ) + ) + stack.enter_context(CompileTimeInstructionCounter.record()) + stack.enter_context(torch_function_mode_stack_state_mgr) + result = _compile_inner(code, one_graph, hooks) + assert torch._C._len_torch_function_stack() == 0, ( + "Torch function mode stack state changed while dynamo tracing, please report a bug" + ) + return result + + return ( + ConvertFrameReturn(), + None, + ) # dead, but see https://github.com/python/mypy/issues/7577 + + @maybe_cprofile + def _compile_inner( + code: CodeType, + one_graph: bool, + hooks: Hooks, + ) -> tuple[ConvertFrameReturn, DynamoTracerOutput]: + nonlocal dynamo_time_before_restart + last_attempt_start_time = start_time = time.time() + + def log_bytecode( + prefix: str, name: str, filename: str, line_no: int, code: CodeType + ) -> None: + if bytecode_log.isEnabledFor(logging.DEBUG): + bytecode_log.debug( + format_bytecode(prefix, name, filename, line_no, code) + ) + + log_bytecode( + "ORIGINAL BYTECODE", + code.co_name, + code.co_filename, + code.co_firstlineno, + code, + ) + # Dump the ORIGINAL bytecode of resumption frame into TORCH_TRACE + # log file, so that it is parsed by tlparse tool. + is_resumption_frame = "torch_dynamo_resume_in" in code.co_name + if is_resumption_frame: + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": code.co_name + "_ORIGINAL_BYTECODE", + "encoding": "string", + }, + payload_fn=lambda: dis.Bytecode(code).dis(), + ) + out_code = None + from .graph_id_filter import get_dynamo_config_override_for_compile_id + + dynamo_config_override = get_dynamo_config_override_for_compile_id( + compile_id, config.debug_dynamo_config_override + ) + try: + with ( + config.patch(dynamo_config_override) + if dynamo_config_override + else contextlib.nullcontext() + ): + dynamo_output = compile_frame( + code, + globals, + locals, + builtins, + closure, + compiler_fn, + one_graph, + restart_reasons, + export=export, + export_constraints=export_constraints, + frame_state=frame_state, + distributed_state=distributed_state, + package=package, + ) + except exc.SkipFrame as e: + if one_graph: + log.debug("No graph captured with export/fullgraph=True") + assert e._torch_dynamo_tracer_output is not None + return ConvertFrameReturn(), e._torch_dynamo_tracer_output + + assert distributed_state is None or distributed_state.all_states is not None, ( # type: ignore[has-type] + "compiler collective wasn't run before compilation completed" + ) + out_code = dynamo_output.bytecode + tracer_output = dynamo_output.tracer_output + if dynamo_output.last_attempt_start_time is not None: + last_attempt_start_time = dynamo_output.last_attempt_start_time + + assert out_code is not None + log_bytecode( + "MODIFIED BYTECODE", + code.co_name, + code.co_filename, + code.co_firstlineno, + out_code, + ) + # Dump the MODIFIED bytecode of resumption frame into TORCH_TRACE + # log file, so that it is parsed by tlparse tool. + if is_resumption_frame: + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": code.co_name + "_MODIFIED_BYTECODE", + "encoding": "string", + }, + payload_fn=lambda: dis.Bytecode(out_code).dis(), + ) + + assert tracer_output.output_graph is not None + output = tracer_output.output_graph + code_context.get_context(out_code)[_BYTECODE_HOOK_SIDE_EFFECTS_CONTEXT_KEY] = ( + tuple(output.get_replayed_side_effect_source_refs()) + ) + + for idx, hook in enumerate(_bytecode_hooks.values()): + with dynamo_timed(f"bytecode_hooks_{idx}", log_pt2_compile_event=True): + hook_output = hook(code, out_code) + if hook_output is not None: + if hook_output is not out_code: + _copy_code_context(out_code, hook_output) + out_code = hook_output + + orig_code_map[out_code] = code + output_codes.add(out_code) + dynamo_time_before_restart = last_attempt_start_time - start_time + + from .bytecode_debugger import BREAKPOINT_MARKER + + if BREAKPOINT_MARKER in out_code.co_consts: + from torch._C._dynamo.eval_frame import register_breakpoint_code + + register_breakpoint_code(out_code) + + # Tests for new code objects. + # The rationale for these tests can be found in torch/csrc/dynamo/eval_frame.c + # Only test once the code object is created. + # They are not tested during runtime. + + def count_args(code: CodeType) -> int: + import inspect + + return ( + code.co_argcount + + code.co_kwonlyargcount + + bool(code.co_flags & inspect.CO_VARARGS) + + bool(code.co_flags & inspect.CO_VARKEYWORDS) + ) + + assert out_code is not None + + total_argcount_old = count_args(code) + total_argcount_new = count_args(out_code) + msg = "arg mismatch: " + msg += f"old code object has args {code.co_varnames[:total_argcount_old]}, " + msg += f"new code object has args {out_code.co_varnames[:total_argcount_new]}" + assert ( + code.co_varnames[:total_argcount_old] + == out_code.co_varnames[:total_argcount_new] + ), msg + + msg = "free var mismatch: " + msg += f"old code object has free var {code.co_freevars}, " + msg += f"new code object has free var {out_code.co_freevars}" + assert code.co_freevars == out_code.co_freevars, msg + + msg = "cell var mismatch: " + msg += f"old code object has cell var {code.co_cellvars}, " + msg += f"new code object has cell var {out_code.co_cellvars}" + assert code.co_cellvars == out_code.co_cellvars, msg + + # Skipping Dynamo on a frame without any extracted graph. + # This does not affect eager functionality. But this is necessary + # for export for cases where Dynamo-reconstructed bytecode can create + # new function frames, confusing export in thinking that there + # are extra graphs now. + + if output.export and output.is_empty_graph(): + return ConvertFrameReturn(), tracer_output + + assert output.guards is not None + CleanupManager.instance[out_code] = output.cleanups + nonlocal cache_entry + # Temporarily restore the mode stack so guard expressions that + # reference modes can evaluate. DisableTorchFunction prevents + # __torch_function__ dispatch during guard construction so modes + # with mutable state aren't triggered. + build_guards_ctx = contextlib.ExitStack() + if torch_function_mode_stack_state_mgr.stack: + build_guards_ctx.enter_context( + torch_function_mode_stack_state_mgr.temp_restore_stack() + ) + with dynamo_timed("build_guards", log_pt2_compile_event=True), build_guards_ctx: + check_fn = dynamo_output.build_guards( + code, + hooks=hooks, + save=package is not None, + cache_entry=cache_entry, + ) + + if package is not None: + assert check_fn.guards_state is not None + package.add_guarded_code(check_fn.guards_state, out_code) + package.add_inlined_source(output.tracing_context.traced_code) + package.update_device_type(output.current_tracer.graph) + + compile_id_str = str(compile_id) if compile_id is not None else "Unknown" + annotation_str = "Torch-Compiled Region: " + compile_id_str + guarded_code = GuardedCode( + out_code, + check_fn.guard_manager, # type: ignore[arg-type] + compile_id, + annotation_str, + ) + + if not output.is_empty_graph() and hooks.guard_export_fn is not None: + # We should not run the guard_export_fn when Dynamo does not + # generate any graph. This can happen in export when TorchDynamo + # generated bytecode has some reconstruction logic for mutated + # variables which can trigger TorchDynamo on the children frames but + # they are benign and do not generate any new graphs. + hooks.guard_export_fn(output.guards) + + return wrap_guarded_code(guarded_code), tracer_output + + metrics_context = get_metrics_context() + package_code_context = ( + package.code_context(code) if package is not None else contextlib.nullcontext() + ) + with ( + _use_lazy_graph_module(config.use_lazy_graph_module), + compile_context(CompileContext(compile_id)), + chromium_event_timed( + "dynamo", reset_event_log_on_exit=True, log_pt2_compile_event=True + ), + _WaitCounter("pytorch.wait_counter.entire_forward_compile").guard(), + metrics_context, + dynamo_timed( + "_compile.compile_inner", + phase_name="entire_frame_compile", + dynamo_compile_column_us="dynamo_cumulative_compile_time_us", + ), + package_code_context, + ): + restart_reasons: set[str] = set() + if compile_pg := get_compile_pg(): + distributed_state = DistributedState(compile_pg, LocalState()) + else: + distributed_state = None + + # Check recompilations + recompile_reason: str | None = None + if is_recompilation(cache_size) and frame: + reasons = get_and_maybe_log_recompilation_reasons( + cache_entry, frame, innermost_fn(compiler_fn) + ) + recompile_reason = ( + "Unable to find recompilation reasons" if not reasons else reasons[0] + ) + metrics_context.update_outer( + { + "recompile_reason": recompile_reason, + "inline_inbuilt_nn_modules_candidate": False, + } + ) + + recompile_user_contexts = get_hook_for_recompile_user_context() + if recompile_user_contexts: + # cap each user context to N chars for data retention purposes. N=256 + # is chosen to be large enough to capture the most important info. + user_contexts_msg = { + user_context()[:256] for user_context in recompile_user_contexts + } + metrics_context.set("recompile_user_contexts", user_contexts_msg) + + exceeded, limit_type = exceeds_recompile_limit(cache_size, compile_id) + if exceeded: + + def format_func_info(code: CodeType) -> str: + return f"'{code.co_name}' ({code.co_filename}:{code.co_firstlineno})" + + # NS: Don't add period at the end of string, as it'll be added to URL + # rendering it incorrect + log.warning( + "torch._dynamo hit config.%s (%s)\n" + " function: %s\n" + " last reason: %s\n" + 'To log all recompilation reasons, use TORCH_LOGS="recompiles".\n' + "To diagnose recompilation issues, see %s", + limit_type, + getattr(config, limit_type), + format_func_info(code), + recompile_reason, + troubleshooting_url, + ) + + def raise_unimplemented_cache_limit_exceeded() -> NoReturn: + unimplemented( + gb_type="Dynamo recompile limit exceeded", + context=f"Limit type: {limit_type}", + explanation="Dynamo attempted to recompile the code object too many times, " + f"exceeding the {limit_type} cache size limit (currently set to {getattr(config, limit_type)}). " + "Excessive recompilations can degrade " + "performance due to the compilation overhead of each recompilation.", + hints=[ + "To monitor recompilations, enable TORCH_LOGS=recompiles. " + "If recompilations are expected, consider " + f"increasing torch._dynamo.config.{limit_type} to an appropriate value.", + f"See {troubleshooting_url} for tips on dealing with recompilations.", + ], + ) + + try: + raise_unimplemented_cache_limit_exceeded() + except Unsupported as e: + if config.fail_on_recompile_limit_hit: + raise FailOnRecompileLimitHit( + "Hard failure due to fail_on_recompile_limit_hit" + ) from e + elif one_graph: + raise FailOnRecompileLimitHit( + "Hard failure due to fullgraph=True" + ) from e + else: + # Set frame execution strategy to RUN_ONLY for this recompile limit case + e.frame_exec_strategy = FrameExecStrategy( + FrameAction.RUN_ONLY, FrameAction.RUN_ONLY + ) + raise + + log.debug( + "torchdynamo start compiling %s %s:%s, stack (elided %s frames):\n%s", + code.co_name, + code.co_filename, + code.co_firstlineno, + skip + 2, + # -2: omit current frame, omit contextlib decorator + "".join(CapturedTraceback.extract(skip=2 + skip).format()), + ) + # -4: -2 as above, plus trace_structured frames + # + # NB: the frame looks like this: + # + # # handled by skip argument + # torch/_dynamo/convert_frame.py:1069 in catch_errors + # torch/_dynamo/convert_frame.py:910 in _convert_frame + # torch/_dynamo/convert_frame.py:464 in _convert_frame_assert + # torch/_utils_internal.py:70 in wrapper_function + # + # # 2 current frame and context lib + # env/lib/python3.10/contextlib.py:79 in inner + # torch/_dynamo/convert_frame.py:776 in _compile + # + # # 2 extra here + # torch/_logging/_internal.py:1064 in trace_structured + # torch/_dynamo/convert_frame.py:780 in + stack_trace = log_dynamo_start(code, skip) + start_time_ns = time.time_ns() + fail_type: str | None = None + fail_reason: str | None = None + exception_stack_trace: list[str] | None = None + fail_user_frame_filename: str | None = None + fail_user_frame_lineno: int | None = None + torch._dynamo.utils.ReinplaceCounters.clear() + guarded_code = None + tracer_output = None + + if ( + config.debug_backend_override + or config.debug_dynamo_config_override + or config.debug_inductor_config_override + ): + # Eagerly validate config override strings before entering the + # compilation try/except so that typos surface as clean ValueErrors + # instead of being wrapped as InternalTorchDynamoError. + from .graph_id_filter import ( + _validate_backend_names, + _validate_dynamo_config_keys, + _validate_inductor_config_keys, + ) + + if err := _validate_backend_names(config.debug_backend_override): + raise ValueError(err) + if err := _validate_dynamo_config_keys(config.debug_dynamo_config_override): + raise ValueError(err) + if err := _validate_inductor_config_keys( + config.debug_inductor_config_override + ): + raise ValueError(err) + + try: + guarded_code, tracer_output = compile_inner(code, one_graph, hooks) + + # NB: We only put_code_state in success case. Success case here + # does include graph breaks; specifically, if a graph break still + # resulted in a partially compiled graph, we WILL return here. An + # Unsupported exception will only bubble to the top level if we + # are unable to compile the frame at all. In this case, there's + # no point in uploading the code state, because we will always + # fail exactly the same way even without the update. (It's useful + # to upload for graph break though, because this can prevent + # extra graph break compilations.) + put_code_state() + if ( + tracer_output + and (output_graph := tracer_output.output_graph) + and output_graph.has_outputs() + ): + log_frame_dynamic_whitelist(code) + if recompile_reason and "size mismatch at index" in recompile_reason: + _log_size_mismatch_recompile() + + return guarded_code + except Exception as e: + # NB: e's msg is mutated here to add user stack, but we DON'T want + # that stack in the Scuba logged fail_reason. So we grab the fail + # info here and add it to the metrics context below. + fail_type = type(e).__qualname__ + fail_reason = str(e) + exception_stack_trace = [traceback.format_exc()] + exception_handler(e, code, frame, export=export) + # NB: this is the post-mutation exception + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "dynamo_error", + "encoding": "string", + }, + payload_fn=lambda: traceback.format_exc(), + ) + fail_user_frame_filename, fail_user_frame_lineno = exc.get_exc_message( + e, compile_id + ) + tracer_output = getattr(e, "_torch_dynamo_tracer_output", None) + if isinstance( + e, + ( + Unsupported, + UserError, + TorchRuntimeError, + BackendCompilerFailed, + AssertionError, + ConstraintViolationError, + GuardOnDataDependentSymNode, + ValidationException, + UncapturedHigherOrderOpError, + BisectValidationException, + ShortenTraceback, + PackageError, + ResumePrologueTracingError, + ), + ): + raise + else: + # Rewrap for clarity + raise InternalTorchDynamoError( + f"{type(e).__qualname__}: {str(e)}" + ).with_traceback(e.__traceback__) from None + finally: + # === WARNING WARNING WARNING === + # If you commit a bug here, it will suppress writing to + # dynamo_compile table, and we will not have telemetry. + # Be extra careful when making changes here! + + if torch._dynamo.config.run_gc_after_compile: + with dynamo_timed("gc", dynamo_compile_column_us="gc_time_us"): + log.info("run_gc_after_compile: running gc") + gc.collect(1) + + output = None + if tracer_output: + output = tracer_output.output_graph + if output: + # pyrefly: ignore [implicit-any] + output.local_scope = {} + # tracer should already be None, keep an extra check here just in case. + if tracer := output.root_tx: + # pyrefly: ignore [implicit-any] + tracer.f_locals = {} + + from .utils import curr_frame + + frame_key = str(curr_frame) + if fail_reason is None and output is not None: + guard_count = len(output.guards) + shape_env_guard_count = len(output.shape_env.guards) + graph_op_count = output.count_calls() + graph_node_count = len(output.graph.nodes) + graph_node_shapes = output.get_graph_sizes_structured() + graph_input_count = len(output.placeholders) + non_compliant_ops = {op.__qualname__ for op in output.non_compliant_ops} + compliant_custom_ops = { + op.__qualname__ for op in output.compliant_custom_ops + } + torch._dynamo.utils.ReinplaceCounters.log() + else: + guard_count = None + shape_env_guard_count = None + graph_op_count = None + graph_node_count = None + # pyrefly: ignore [implicit-any] + graph_node_shapes = {} + graph_input_count = None + non_compliant_ops = set({}) + compliant_custom_ops = set({}) + restart_reasons = set() + # If compilation failed, the entire time is wasted + dynamo_time_before_restart = (time.time_ns() - start_time_ns) / 1e9 + + metrics = { + "frame_key": frame_key, + "co_name": code.co_name, + "co_filename": code.co_filename, + "co_firstlineno": code.co_firstlineno, + "cache_size": cache_size.num_cache_entries_with_same_id_matched_objs, + "accumulated_cache_size": cache_size.num_cache_entries, + "guard_count": guard_count, + "shape_env_guard_count": shape_env_guard_count, + "graph_op_count": graph_op_count, + "graph_node_count": graph_node_count, + "graph_input_count": graph_input_count, + "fail_type": fail_type, + "fail_reason": fail_reason, + "fail_user_frame_filename": fail_user_frame_filename, + "fail_user_frame_lineno": fail_user_frame_lineno, + "non_compliant_ops": non_compliant_ops, + "compliant_custom_ops": compliant_custom_ops, + "restart_reasons": restart_reasons, + "dynamo_time_before_restart_s": dynamo_time_before_restart, + "has_guarded_code": guarded_code is not None, + "specialize_float": config.specialize_float, + "is_forward": True, + "dynamo_compile_time_before_restart_us": to_int_us( + dynamo_time_before_restart + ), + "stack_trace": stack_trace, + "graph_node_shapes": str(graph_node_shapes), + "exception_stack_trace": exception_stack_trace, + } + # TODO: replace with CompileEventLogger.compilation_metrics + # There are some columns here not in PT2 Compile Events + # so we need to slightly change it + metrics_context.update_outer(metrics) + # === END WARNING WARNING WARNING === + + # If tracer is available, then tracer.error_on_graph_break reflects value of + # global symbolic_convert.error_on_graph_break at the time of the graph break - + # symbolic_convert.error_on_graph_break may have been (correctly) changed during cleanup. + # If tracer is unavailable, then fallback to symbolic_convert.error_on_graph_break. + if convert_frame_box: + convert_frame_box.error_on_graph_break = ( + tracer_output.error_on_graph_break + if tracer_output + else _get_error_on_graph_break() + ) + + # Cleanup guards unless if in export, which will return guards + # Make sure to to do this after collecting metrics + if ( + tracer_output is not None + and tracer_output.output_graph is not None + and not tracer_output.output_graph.export + ): + tracer_output.output_graph.tracing_context.guards_context.dynamo_guards.clear() + + # Clear WeakIdRef entries that can block swap_tensors after compile. + # Determine whether to clear based on config and backend type. + should_clear = config.invalidate_compile_context_weakrefs + if should_clear is None: + # Default: clear for registered backends, don't clear for custom + # Unwrap the compiler_fn to get the actual backend function + should_clear = _is_registered_backend(innermost_backend(compiler_fn)) + if should_clear: + if tracer_output and tracer_output.output_graph: + tc = tracer_output.output_graph.tracing_context + tc.tensor_to_context.clear() + # Clear both the current fake_mode and the old_fake_mode + # (the original is stored before backend_fake_mode replaces it) + _clear_fake_mode_weakrefs(tc.fake_mode) + if hasattr(tracer_output.output_graph, "_old_fake_mode"): + _clear_fake_mode_weakrefs( + tracer_output.output_graph._old_fake_mode + ) + + +class ConvertFrame: + def __init__( + self, + compiler_fn: CompilerFn, + hooks: Hooks, + package: CompilePackage | None = None, + recompile_limit: int | None = None, + ) -> None: + self._torchdynamo_orig_backend = compiler_fn + self._inner_convert = convert_frame_assert( + compiler_fn, + one_graph=False, + package=package, + recompile_limit=recompile_limit, + ) + self._hooks = hooks + self._recompile_limit = recompile_limit + + @property + def _clone_with_backend(self) -> Callable[[WrapBackendDebug], ConvertFrame]: + return lambda backend: convert_frame( + backend, + self._hooks, + recompile_limit=self._recompile_limit, + ) + + def __call__( + self, + frame: DynamoFrameType, + cache_entry: CacheEntry | None, + hooks: Hooks, + frame_state: dict[str, int | FrameStateSizeEntry], + skip: int = 0, + ) -> ConvertFrameReturn: + input_codes.add(frame.f_code) + counters["frames"]["total"] += 1 + try: + result = self._inner_convert( + frame, cache_entry, hooks, frame_state, skip=skip + 1 + ) + counters["frames"]["ok"] += 1 + return result + except Exception as e: + # Do not allow errors to be suppressed if we're tracing a resume function prologue + if isinstance(e, ResumePrologueTracingError): + raise + + error_on_graph_break = ( + self._inner_convert._box.error_on_graph_break is not None + ) + assert error_on_graph_break is not None + if self._inner_convert._box.error_on_graph_break: + # NOTE we _might_ have to wrap the current in a custom exception + # in order to correctly bubble up to the top-level compile wrapper in + # eval_frame.py. But re-raising seems to work for now because exceptions from tracing + # a nested call that results in a top-level frame compile will be handled by the caller + # as an observed exception - we don't expect that exception to be suppressed. + raise + + # These two exception types are "soft" failure, in the sense that + # we know this is due to something we didn't implement all the + # way, scare the user less about it. That being said, if you + # are trying to understand why a graph break happened, it's still + # important to have this information, so offer it. + # + # NB: NotImplementedError used to be on this list, but actually + # it is impossible for it to reach here, as it is converted into + # InternalTorchDynamoError. This behavior seemed reasonable + # to me (ezyang, Aug 2023) so I kept it, but maybe at some point + # someone wanted these to also get suppressed. If so, you'll + # need to make these exceptions not get wrapped + + # We intentionally don't want to suppress error here. + if isinstance(e, UncapturedHigherOrderOpError): + raise + + soft_fail = isinstance(e, (Unsupported, UserError)) + code = frame.f_code + # Log soft failure that was not already logged by symbolic_convert. + # This happens e.g. for graph breaks that are raised in convert_frame.py + # TODO(williamwen42) Unsupported exn's from tracing are handled and logged by symbolic_convert.py + # Unsupported exn's caught here should be from convert_frame.py - figure out a better way + # to log these. + if ( + soft_fail + and not getattr(e, "logged", False) + and graph_break_log.isEnabledFor(logging.DEBUG) + ): + # Log this message in the graph break. Also use the string + # "skip: " to tell that the whole frame is falling back to + # eager. + if hasattr(e, "compile_id") and hasattr(e, "real_stack"): + with compile_context(CompileContext(e.compile_id)): # type: ignore[attr-defined] + user_stack = e.real_stack + user_stack_formatted = "".join( + traceback.format_list(user_stack) + ) + frame_info = exc.format_frame_info(code) + user_stack_trace = ( + "Graph break: torch.compile cannot properly resume from this graph break, which results in a skip.\n" + f"torch.compile will skip tracing the frame {frame_info} and fall back to eager.\n" + "The graph break occurred in the following user code:\n" + f"{user_stack_formatted}" + ) + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "dynamo_graph_break_reason", + "encoding": "string", + }, + payload_fn=lambda: f"{user_stack_trace}\n{traceback.format_exc()}", + ) + graph_break_log.debug( + user_stack_trace, + exc_info=True, + stack_info=config.verbose, + ) + + if not config.suppress_errors and not soft_fail: + raise + + # Suppress the error. NB: It's very important to do the + # suppression logging HERE, where the actual suppression + # happens. Previously it was somewhere else and so it was + # possible to accidentally not log at all. + record_filename = getattr(e, "record_filename", None) + code = frame.f_code + error_msg = format_error_msg(e, code, record_filename, frame) + + if soft_fail: + log.info(error_msg, exc_info=True) + else: + log.warning(error_msg, exc_info=True) + + # Check if the exception has a specific frame execution strategy + if ( + isinstance(e, exc.TorchDynamoException) + and e.frame_exec_strategy is not None + ): + return ConvertFrameReturn(frame_exec_strategy=e.frame_exec_strategy) + + return ConvertFrameReturn() + + +def convert_frame( + compiler_fn: CompilerFn, + hooks: Hooks, + package: CompilePackage | None = None, + recompile_limit: int | None = None, +) -> ConvertFrame: + """Try to convert a frame into an FX graph, if error leave frame unmodified""" + return ConvertFrame( + compiler_fn, hooks, package=package, recompile_limit=recompile_limit + ) + + +# TODO mlazos: add support for same args, or record them +def replay(filename: str) -> None: + from .backends.debugging import eager + + original_replay_val = config.replay_record_enabled + config.replay_record_enabled = False + with open(filename, "rb") as in_file: + record = ExecutionRecord.load(in_file) + record.globals = dict(itertools.chain(record.globals.items(), globals().items())) + + with decorators.error_on_graph_break(False): + try: + _compile( + record.code, + record.globals, + record.locals, + record.builtins, + record.closure, + compiler_fn=eager, + one_graph=False, + export=False, + export_constraints=None, + hooks=Hooks(), + cache_size=CacheSizeRelevantForFrame(0, 0), + cache_entry=None, + frame=None, + frame_state={}, + compile_id=CompileId(frame_id=42, frame_compile_id=999), + ) + finally: + config.replay_record_enabled = original_replay_val + + +def first_real_inst_idx(code: CodeType) -> int: + if sys.version_info < (3, 11): + return 0 + for inst in dis.get_instructions(code): + if inst.opname == "RESUME": + return inst.offset // 2 + raise RuntimeError("RESUME instruction not found in code") + + +class ConvertFrameProtocol(typing.Protocol): + def __call__( + self, + frame: DynamoFrameType, + cache_entry: CacheEntry | None, + hooks: Hooks, + frame_state: dict[str, int | FrameStateSizeEntry], + *, + skip: int = 0, + ) -> ConvertFrameReturn: ... + + +class CatchErrorsWrapper: + def __init__(self, callback: ConvertFrameProtocol, hooks: Hooks) -> None: + functools.wraps(callback)(self) + self._torchdynamo_orig_backend = callback + self.hooks = hooks + + def __call__( + self, + frame: DynamoFrameType, + cache_entry: CacheEntry | None, + frame_state: dict[str, int | FrameStateSizeEntry], + ) -> ConvertFrameReturn: + assert frame_state is not None + input_codes.add(frame.f_code) + + is_skipfile = trace_rules.check(frame.f_code, frame=frame) + if sys.version_info >= (3, 13): + has_started_execution = frame.f_lasti > first_real_inst_idx(frame.f_code) + else: + has_started_execution = frame.f_lasti >= first_real_inst_idx(frame.f_code) + + # Check if we should skip due to torch dispatch mode. + # When inline_torch_dispatch_torch_compile is True (new behavior), we walk + # the stack to check for active modes. When False (old behavior), we use + # the global flag that tracks if we're inside any mode. + if config.inline_torch_dispatch_torch_compile: + should_skip_for_dispatch_mode = any_torch_dispatch_mode_on_stack() + else: + should_skip_for_dispatch_mode = ( + is_in_any_mode_without_ignore_compile_internals() + ) + + if ( + # TODO: the first condition is not covered by any test + has_started_execution + or is_skipfile + or config.disable + or ( + should_skip_for_dispatch_mode + and not getattr(self._torchdynamo_orig_backend, "_export", False) + ) + ): + if log.isEnabledFor(logging.DEBUG): + if has_started_execution: + skip_reason = "traced frame already" + elif trace_rules.check(frame.f_code, frame=frame): + skip_reason = "in skipfiles" + elif should_skip_for_dispatch_mode: + skip_reason = "non-infra torch dispatch mode present, this is not supported today in torch.compile" + else: + skip_reason = "dynamo tracing is disabled" + + log.debug( + "skipping: %s (reason: %s, file: %s)", + frame.f_code.co_name, + skip_reason, + frame.f_code.co_filename, + ) + return ConvertFrameReturn() + + if ( + frame.f_code.co_filename == "" and frame.f_code.co_name == "__new__" + ) or ( + frame.f_code.co_filename.endswith("collections/__init__.py") + and frame.f_code.co_name == "_make" + ): + # nametuple constructor/_make + return ConvertFrameReturn() + + if ( + frame.f_code.co_name == "__init__" + and frame.f_code.co_argcount > 0 + and frame.f_code.co_varnames + and is_traceable_wrapper_subclass( + frame.f_locals.get(frame.f_code.co_varnames[0]) + ) + ): + # Skip tracing __init__ of traceable wrapper subclasses: self is + # partially initialized at this point (attributes set by __init__ + # don't exist yet), so faking it would call __tensor_flatten__ and + # crash. Run eagerly instead, matching @torch._disable_dynamo behavior. + return ConvertFrameReturn() + + if torch._dynamo.utils.get_optimize_ddp_mode() == "ddp_optimizer": + ddp_module = DistributedDataParallel._get_active_ddp_module() + if ddp_module: + with compile_lock: + from torch._dynamo.backends.distributed import DDPOptimizer + + ddp_optimizer = DDPOptimizer( + bucket_bytes_cap=ddp_module.bucket_bytes_cap, + backend_compile_fn=self._torchdynamo_orig_backend._torchdynamo_orig_backend, # type: ignore[attr-defined] + ) + assert hasattr( + self._torchdynamo_orig_backend, "_clone_with_backend" + ), ( + "DDPOptimizer only supports callback fns that know how to clone themselves." + ) + hijacked_callback = ( + self._torchdynamo_orig_backend._clone_with_backend( + ddp_optimizer.compile_fn, + ) + ) + return hijacked_callback( + frame, cache_entry, self.hooks, frame_state + ) + + with compile_lock, _disable_current_modes(): + # skip=1: skip this frame + result = self._torchdynamo_orig_backend( + frame, cache_entry, self.hooks, frame_state, skip=1 + ) + return result + + +def catch_errors_wrapper( + callback: ConvertFrameProtocol, hooks: Hooks +) -> CatchErrorsWrapper: + return CatchErrorsWrapper(callback, hooks) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/create_parameter_op.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/create_parameter_op.py new file mode 100644 index 0000000000000000000000000000000000000000..a0bc7325c54b491b967cd48a0991ab526608f5e8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/create_parameter_op.py @@ -0,0 +1,70 @@ +import threading +from collections.abc import Generator +from contextlib import contextmanager +from typing import Any + +import torch + + +# See [Note: Metadata mutation in proxy tracing] for why sacrificial parameter mutates +# metadata during proxy tracing and we should remove the sacrificial parameter logic. +doc = """ +This is used when dynamo traces torch.nn.Parameter, which normally would not trace properly +with AOTAutograd. We instead create a placeholder torch.nn.Parameter before the graph, which +becomes a graph arg and has no storage backing it. At the point in the graph where the parameter +actually should be created we mutate this sacrificial placeholder into it. This allows gradients +to flow into the parameter as if it were an input to the graph (which is the only thing we are +allowed to compute gradients on). +""".strip() + + +class TracableCreateParameter(torch.autograd.Function): + @staticmethod + # pyrefly: ignore [bad-override] + def forward(ctx: Any, tensor: Any, placeholder: Any) -> torch.nn.Parameter: + if tensor.requires_grad: + tensor = tensor.detach() + return placeholder.set_(tensor) + + @staticmethod + def backward(ctx: Any, *grad_outputs: torch.Tensor) -> tuple[None, torch.Tensor]: + grad = grad_outputs[0] + return None, grad # grad flows to placeholder + + +def tracable_create_parameter( + tensor: torch.Tensor, placeholder: torch.nn.Parameter +) -> torch.nn.Parameter: + with torch.set_grad_enabled(placeholder.requires_grad): + out = TracableCreateParameter.apply(tensor, placeholder) + return out + + +def new_parameter_placeholder( + size: tuple[int, ...], dtype: torch.dtype, device: torch.device, requires_grad: bool +) -> torch.nn.Parameter: + """Create a placeholder to be passed to the above functions""" + result = torch.nn.Parameter( + torch.empty(size, dtype=dtype, device=device), requires_grad=requires_grad + ) + # TODO(jansel): alloc followed by free is inefficient, need a way to allocate an unbacked tensor. + # Allocating a zero tensor would causes assert failures in autograd. + result.untyped_storage().resize_(0) + return result + + +_TLS = threading.local() + + +@contextmanager +def do_not_convert_to_tracable_parameter() -> Generator[bool, None, None]: + old_flag = getattr(_TLS, "convert_tracable_parameter", True) + _TLS.convert_tracable_parameter = False + try: + yield False + finally: + _TLS.convert_tracable_parameter = old_flag + + +def can_convert_to_tracable_parameter() -> bool: + return getattr(_TLS, "convert_tracable_parameter", True) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/current_scope_id.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/current_scope_id.py new file mode 100644 index 0000000000000000000000000000000000000000..74a5f4888c64629f3225118d91b52ba05e000ce0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/current_scope_id.py @@ -0,0 +1,42 @@ +""" +Provides thread-local scope identification for SubgraphTracer instances. + +This module implements a thread-safe mechanism for tracking nested tracing contexts, +which is essential when multiple SubgraphTracer instances are active. The scope ID +helps identify which tracer context is currently active when direct access to the +InstructionTranslator is difficult. + +Key components: +- Thread-local scope ID storage (_current_scope_id) +- Getter function (current_scope_id) to safely access the current scope +- Context manager (enter_new_scope) for managing nested scope transitions + +The scope ID increments when entering a new context and decrements when exiting, +allowing proper tracking of nested tracing operations across different threads. +""" + +import contextlib +import threading +from collections.abc import Generator + + +# Global variable to identify which SubgraphTracer we are in. +# It is sometimes difficult to find an InstructionTranslator to use. +_current_scope_id = threading.local() + + +def current_scope_id() -> int: + global _current_scope_id + if not hasattr(_current_scope_id, "value"): + _current_scope_id.value = 1 + return _current_scope_id.value + + +@contextlib.contextmanager +def enter_new_scope() -> Generator[None, None, None]: + global _current_scope_id + try: + _current_scope_id.value = current_scope_id() + 1 + yield + finally: + _current_scope_id.value = current_scope_id() - 1 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/dce_extra_outputs.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/dce_extra_outputs.py new file mode 100644 index 0000000000000000000000000000000000000000..4509a82b7de7ec04f70c90b1747a39f653c09c20 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/dce_extra_outputs.py @@ -0,0 +1,216 @@ +""" +DCE pass for unused extra outputs in HOP subgraphs. + +When enable_side_effects_with_extra_outputs is True, HOPs like invoke_subgraph and +checkpoint (tag_activation_checkpoint) +return all intermediate tensors/symints as extra outputs to support side effects. +However, many of these extra outputs may not actually be used in the parent graph. + +This pass removes unused extra outputs by: +1. Collecting all callers for each subgraph +2. Checking if each output is used by all callers +3. Removing unused outputs from the subgraph's output node +4. Updating the HOP call and getitem indices in all call sites + +""" + +import collections +import operator + +import torch + + +# HOPs that may have extra outputs that can be DCE'd +_HOPS_WITH_EXTRA_OUTPUTS = { + torch.ops.higher_order.invoke_subgraph, + torch.ops.higher_order.tag_activation_checkpoint, + # torch.ops.higher_order.autograd_function_apply, +} + + +def dce_hop_extra_outputs(gm: torch.fx.GraphModule) -> bool: + """ + Remove unused extra outputs from HOP calls in all submodules. + + For each subgraph output, check if any caller has a getitem for that index + with users. If no caller uses it, remove the output. + If the user in caller is an output node, to simply the algorithm, we do not recursively check + if the caller's output is used further up in the call chain. + + Args: + gm: The GraphModule to optimize + + Returns: + True if any modifications were made, False otherwise + """ + # Collect all subgraph usages: subgraph_id -> list of (parent_gm, subgraph_name, hop_node) + subgraph_id_to_callers: dict[ + int, list[tuple[torch.fx.GraphModule, str, torch.fx.Node]] + ] = collections.defaultdict(list) + _collect_all_subgraph_usages(gm, subgraph_id_to_callers) + + if not subgraph_id_to_callers: + return False + + modified = False + + for callers in subgraph_id_to_callers.values(): + parent_gm, subgraph_name, _ = callers[0] + subgraph = getattr(parent_gm, subgraph_name) + + if not isinstance(subgraph, torch.fx.GraphModule): + continue + + output_node = next(n for n in subgraph.graph.nodes if n.op == "output") + output_args = output_node.args[0] + if not isinstance(output_args, (tuple, list)): + continue + + num_outputs = len(output_args) + used_indices: set[int] = set() + + # Check which outputs are used by any caller + for idx in range(num_outputs): + if _is_output_used(idx, callers): + used_indices.add(idx) + + # DCE if some outputs are unused + if 0 < len(used_indices) < num_outputs: + if _dce_subgraph(subgraph, callers, used_indices): + modified = True + + return modified + + +def _collect_all_subgraph_usages( + gm: torch.fx.GraphModule, + subgraph_id_to_callers: dict[ + int, list[tuple[torch.fx.GraphModule, str, torch.fx.Node]] + ], +) -> None: + """Recursively collect all HOP usages across the graph tree.""" + for node in gm.graph.nodes: + if node.op == "call_function" and node.target in _HOPS_WITH_EXTRA_OUTPUTS: + subgraph_attr = node.args[0] + if ( + isinstance(subgraph_attr, torch.fx.Node) + and subgraph_attr.op == "get_attr" + ): + subgraph_name = subgraph_attr.target + assert isinstance(subgraph_name, str) + subgraph = getattr(gm, subgraph_name, None) + if isinstance(subgraph, torch.fx.GraphModule): + subgraph_id = id(subgraph) + subgraph_id_to_callers[subgraph_id].append( + (gm, subgraph_name, node) + ) + _collect_all_subgraph_usages(subgraph, subgraph_id_to_callers) + + +def _is_output_used( + output_idx: int, + callers: list[tuple[torch.fx.GraphModule, str, torch.fx.Node]], +) -> bool: + """Check if output_idx is used by ANY caller (has a getitem with users).""" + for _parent_gm, _subgraph_name, hop_node in callers: + for user in hop_node.users: + if user.op == "call_function" and user.target == operator.getitem: + if user.args[1] == output_idx and len(user.users) > 0: + return True + return False + + +def _dce_subgraph( + subgraph: torch.fx.GraphModule, + callers: list[tuple[torch.fx.GraphModule, str, torch.fx.Node]], + used_indices: set[int], +) -> bool: + """ + DCE a subgraph by removing unused output indices. + + Updates the subgraph's output node, all getitem nodes in callers, + and example_value metadata on HOP nodes. + """ + output_node = next(n for n in subgraph.graph.nodes if n.op == "output") + old_outputs = list(output_node.args[0]) + + # Check if this is the forward subgraph of autograd_function_apply + # For autograd_function_apply, the fwd subgraph must return (output, saved_values, ...) + # where indices 0 and 1 are ALWAYS required by the runtime + # is_autograd_fwd = any( + # node.target == torch.ops.higher_order.autograd_function_apply + # for node in hop_nodes + # ) + is_autograd_fwd = False + + # For autograd_function_apply forward subgraph, indices 0 (output) and 1 (saved_values) + # are ALWAYS used by the runtime, even if not explicitly accessed via getitem + if is_autograd_fwd and len(old_outputs) >= 2: + used_indices.add(0) # output + used_indices.add(1) # saved_values + + # Nothing to DCE if all outputs are used or no outputs are used + if len(used_indices) >= len(old_outputs) or len(used_indices) == 0: + return False + + # Build mapping from old indices to new indices + old_to_new: dict[int, int] = {} + new_outputs = [] + new_idx = 0 + + for old_idx in range(len(old_outputs)): + if old_idx in used_indices: + old_to_new[old_idx] = new_idx + new_outputs.append(old_outputs[old_idx]) + new_idx += 1 + + # Update subgraph output node + # Create a new output node with the filtered outputs + with subgraph.graph.inserting_before(output_node): + new_output_node = subgraph.graph.output(tuple(new_outputs)) + output_node.replace_all_uses_with(new_output_node) + subgraph.graph.erase_node(output_node) + + for parent_gm, _, hop_node in callers: + # Update getitem nodes to use new indices + for user in list(hop_node.users): + if user.op == "call_function" and user.target == operator.getitem: + old_idx = user.args[1] + assert isinstance(old_idx, int) + + if old_idx not in old_to_new: + assert len(list(user.users)) == 0 + parent_gm.graph.erase_node(user) + continue + + new_idx = old_to_new[old_idx] + # Create a new getitem node with the new index + with parent_gm.graph.inserting_before(user): + new_getitem = parent_gm.graph.call_function( + operator.getitem, args=(user.args[0], new_idx) + ) + # Copy metadata from old node + new_getitem.meta = user.meta.copy() + user.replace_all_uses_with(new_getitem) + parent_gm.graph.erase_node(user) + + # Update example_value metadata on hop_node + if "example_value" in hop_node.meta: + old_example = hop_node.meta["example_value"] + assert isinstance(old_example, (tuple, list)) + new_example = tuple( + old_example[old_idx] + for old_idx in range(len(old_outputs)) + if old_idx in used_indices + ) + hop_node.meta["example_value"] = new_example + + # Recompile subgraph and all modified parent graphs + subgraph.graph.lint() + subgraph.recompile() + + for parent_gm in {caller[0] for caller in callers}: + parent_gm.graph.lint() + parent_gm.recompile() + + return True diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/debug_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/debug_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..e43f45a138e2d97914e79ed718d5ac046243d670 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/debug_utils.py @@ -0,0 +1,980 @@ +""" +Debug utilities for TorchDynamo compilation and execution. + +This module provides various debugging tools and utilities for TorchDynamo, including: + +- Minification support for reducing test cases while preserving bugs +- Input/output handling via InputReader and InputWriter for reproducible testing +- Accuracy checking between original and compiled models +- Neural network module string conversion via NNModuleToString +- Profiling tools and system information collection +- Buck build system integration for Meta-internal testing + +Key classes: +- InputReader/InputWriter: Handle serialization of model inputs/outputs +- NNModuleToString: Converts nn.Modules to string representations +- BuckTargetWriter: Manages Buck build system integration +""" + +from __future__ import annotations + +import atexit +import copy +import cProfile +import functools +import getpass +import inspect +import itertools +import logging +import os +import re +import subprocess +import sys +import tempfile +import textwrap +from collections import Counter +from importlib import import_module +from typing import Any, TYPE_CHECKING, TypeVar + +import torch +import torch._prims_common as utils +import torch._subclasses.meta_utils +from torch import Tensor +from torch._dynamo.testing import rand_strided +from torch._inductor.cpp_builder import normalize_path_separator +from torch._prims_common import is_float_dtype +from torch.multiprocessing.reductions import StorageWeakRef +from torch.utils._content_store import ContentStoreReader, ContentStoreWriter + +from . import config +from .utils import clone_inputs, get_debug_dir, warn_once + + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + + from torch.hub import tqdm + from torch.storage import UntypedStorage + + +log = logging.getLogger(__name__) + +T = TypeVar("T") + + +inductor_config = import_module("torch._inductor.config") +use_buck = inductor_config.is_fbcode() + +if use_buck: + import libfb.py.build_info + + +# pyrefly: ignore [implicit-any] +extra_deps = [] +extra_imports = "" +cur_target = "" +if use_buck: + extra_deps = [ + "//caffe2/torch/fb/sparsenn:sparsenn_operators_gpu", + "//caffe2/torch/fb/sparsenn:sparsenn_operators", + "//deeplearning/fbgemm/fbgemm_gpu:sparse_ops_cpu", + "//deeplearning/fbgemm/fbgemm_gpu:sparse_ops", + ] + cur_target = libfb.py.build_info.BuildInfo.get_build_rule().replace("fbcode:", "//") # type: ignore[possibly-undefined] + extra_imports = "\n".join([f'torch.ops.load_library("{x}")' for x in extra_deps]) + + +BUCK_CMD_PREFIX = ["buck2", "run", "@mode/dev-nosan"] + + +class BuckTargetWriter: + def __init__(self, filename: str) -> None: + self.subdir, self.py_file = os.path.split(os.path.abspath(filename)) + self.target = self.py_file.replace(".py", "") + + # Get main_module path from fbcode + self.path = f"{self.subdir.replace('/', '.')}.{self.target}" + self.path = self.path[self.path.find("fbcode.") :] + self.path = self.path[7:] + + # Get cmd line path + tmp = self.subdir + tmp = tmp[tmp.find("fbcode/") :][7:] + self.cmd_line_path = f"//{tmp}:{self.target}" + + def build(self) -> str: + extra_cpp_deps = "\n".join([f' "{x}",' for x in extra_deps]) + return textwrap.dedent( + f""" +load("@fbcode_macros//build_defs:python_binary.bzl", "python_binary") + +python_binary( + name="{self.target}", + srcs = ["{self.py_file}"], + compile = False, + deps = [ + "//caffe2:torch", + "//caffe2:libtorch", + "//caffe2/functorch:functorch", + "//triton:triton", + "{cur_target}", + ], + cpp_deps = [ +{extra_cpp_deps} + ], + main_module = "{self.path}", + par_style = "xar", +) +""" + ) + + def write(self, print_msg: bool = True) -> list[str]: + target_file = os.path.join(self.subdir, "TARGETS") + with open(target_file, "w") as fd: + fd.write(self.build()) + # log.warning("Wrote isolation TARGETS file at %s", target_file) + cmd_split = BUCK_CMD_PREFIX + [self.cmd_line_path] + if print_msg: + log.warning( + "Found an example that reproduces the error. Run this cmd to repro - %s", + " ".join(cmd_split), + ) + return cmd_split + + +def minifier_dir() -> str: + path = os.path.join(get_debug_dir(), "minifier") + if path is None: + path = f"{tempfile.gettempdir()}/minifier_{getpass.getuser()}" + if not os.path.exists(path): + os.makedirs(path, exist_ok=True) + return path + + +MAX_CONSTANT_NUMEL_INLINE = 4 + + +class NNModuleToString: + safe_reprs = [ + torch.nn.Linear, + torch.nn.Conv1d, + torch.nn.Conv2d, + torch.nn.Conv3d, + torch.nn.BatchNorm1d, + torch.nn.BatchNorm2d, + torch.nn.BatchNorm3d, + torch.nn.LayerNorm, + torch.nn.Dropout, + torch.nn.Softmax, + torch.nn.ReLU, + torch.nn.GELU, + torch.nn.Identity, + torch.nn.MaxPool2d, + torch.nn.Embedding, + torch.nn.Tanh, + torch.nn.ConvTranspose1d, + torch.nn.GLU, + torch.nn.LSTM, + torch.nn.Flatten, + torch.nn.AdaptiveAvgPool2d, + ] + + @staticmethod + def can_convert_to_string(gm: torch.fx.GraphModule) -> bool: + cant_convert = set() + for _, module in gm.named_children(): + if type(module) not in NNModuleToString.safe_reprs: + cant_convert.add(module) + + if len(cant_convert) > 0: + log.warning("We have not tested reprs of some modules - %s", cant_convert) + # TODO - Assuming that all modules can be safely repr'd. Check if that assumption is correct. + return True + + @staticmethod + def convert(gm: torch.fx.GraphModule) -> str: + from torch.nn.modules.module import _addindent + + tab = " " * 4 + + model_str = textwrap.dedent( + """ + from torch.nn import * + class Repro(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + """ + ) + + for module_name, module in gm.named_children(): + module_str = f"{module.__repr__()}" + # module should be a core torch.nn.Module, so all parameters + # should be on the same device. + example_param = next(module.parameters(), None) + if example_param is not None and example_param.is_cuda: + module_str = f"{module_str}.cuda()" + model_str += f"{tab * 2}self.{module_name} = {module_str}\n" + + for buffer_name, buffer in gm._buffers.items(): + if buffer is None: + continue + # Serialize full data for small buffers + if buffer.numel() <= MAX_CONSTANT_NUMEL_INLINE: + from torch._tensor_str import PRINT_OPTS + + assert PRINT_OPTS.threshold >= MAX_CONSTANT_NUMEL_INLINE + tensor_str = repr(buffer) + elif torch.is_floating_point(buffer): + tensor_str = f"torch.randn({list(buffer.shape)}, dtype={buffer.dtype})" + else: + tensor_str = ( + f"torch.randint(1, size={list(buffer.shape)}, dtype={buffer.dtype})" + ) + if buffer.is_cuda: + tensor_str = f"{tensor_str}.cuda()" + model_str += ( + f"{tab * 2}self.register_buffer('{buffer_name}', {tensor_str})\n" + ) + + for param_name, param in gm._parameters.items(): + if param is None: + continue + maybe_device = "" + if param.is_cuda: + maybe_device = ', device="cuda"' + tensor_str = f"torch.nn.Parameter(torch.randn({list(param.shape)}, dtype={param.dtype}{maybe_device}))" + model_str += f"{tab * 2}self.{param_name} = {tensor_str}\n" + + # TODO - Keep this code for now. But, I don't think we will need this. + # attrs = dir(gm) + # for attr in attrs: + # if "_tensor_constant" in attr: + # val = getattr(gm, attr) + # model_str += f" {attr} = {val!r}\n" + + model_str += f"{_addindent(gm.code, 4)}\n" + return model_str + + +@functools.cache # subprocess is expensive +def _cuda_system_info_comment() -> str: + if not torch.cuda.is_available(): + return "# torch.cuda.is_available()==False, no GPU info collected\n" + + model_str = "# CUDA Info: \n" + try: + if torch.version.hip is None: + cuda_version_out = subprocess.check_output(["nvcc", "--version"]) + cuda_version_lines = cuda_version_out.decode().split("\n") + comment = "".join([f"# {s} \n" for s in cuda_version_lines if s != ""]) + model_str += f"{comment}\n" + else: + model_str += "# Not searching for nvcc on ROCM setup\n" + except (FileNotFoundError, subprocess.CalledProcessError): + model_str += "# nvcc not found\n" + + gpu_names = Counter( + torch.cuda.get_device_name(i) for i in range(torch.cuda.device_count()) + ) + + model_str += "# GPU Hardware Info: \n" + for name, count in gpu_names.items(): + model_str += f"# {name} : {count} \n" + model_str += "\n" + return model_str + + +def generate_env_vars_string(*, stable_output: bool = False) -> str: + """ + Generate a string configuration for environment variables related to Dynamo, Inductor, and Triton. + """ + if stable_output: + return "# env var omitted due to stable_output=True" + + allow_list = ["TORCH", "DYNAMO", "INDUCTOR", "TRITON"] + skip_list = ["TRITON_LIBDEVICE_PATH", "TRITON_PTXAS_PATH", "TRITON_LIBCUDA_PATH"] + + def filter(key: str) -> bool: + return any(string in key for string in allow_list) and key not in skip_list + + config_lines = [ + f"""os.environ['{key}'] = '{value.replace("'", '"')}'""" + for key, value in os.environ.items() + if filter(key) + ] + config_string = "\n".join(config_lines) + return normalize_path_separator(f"""\ +import os +{config_string} + """) + + +def generate_config_string(*, stable_output: bool = False) -> str: + import torch._functorch.config + import torch._inductor.config + + if stable_output: + return "# config omitted due to stable_output=True" + + experimental_config = torch.fx.experimental._config.codegen_config() # type: ignore[attr-defined] + return f"""\ +import torch._dynamo.config +import torch._inductor.config +import torch._functorch.config +import torch.fx.experimental._config +{torch._dynamo.config.codegen_config()} +{torch._inductor.config.codegen_config()} +{torch._functorch.config.codegen_config()} +{experimental_config} +""" + + +def get_minifier_repro_path() -> str: + return os.path.join(minifier_dir(), "minifier_launcher.py") + + +def helper_for_dump_minify(contents: str) -> None: + minified_repro_path = get_minifier_repro_path() + log.warning("Writing minified repro to:\n%s", minified_repro_path) + + if use_buck: + BuckTargetWriter(minified_repro_path).write() + try: + with open(minified_repro_path, "w") as fd: + fd.write(contents) + + except OSError as e: + log.exception("") + raise NotImplementedError(f"Could not write to {minified_repro_path}") from e + + +class AccuracyError(Exception): + pass + + +def clone_inputs_retaining_gradness(example_inputs: Sequence[Any]) -> list[Any]: + """ + This clone inputs is different from utils clone_input. In case of minifier, + all the tensors are leaf tensors while creating a new graph. So, we set the + requires_grad field w/o checking the leafness of the tensor. + """ + cloned_inputs = clone_inputs(example_inputs) + for idx in range(len(example_inputs)): + if isinstance(cloned_inputs[idx], torch.Tensor): + cloned_inputs[idx].requires_grad_(example_inputs[idx].requires_grad) + return cloned_inputs # type: ignore[return-value] + + +def run_fwd_maybe_bwd( + gm: torch.fx.GraphModule, + args: Sequence[Any], + only_fwd: bool = False, + disable_clone: bool = False, +) -> Any: + """ + Runs a forward and possibly backward iteration for a given mod and args. + + When disable_clone is True, we will use args as-is without cloning. + This is higher fidelity but we may destroy the args in the process. + """ + from .testing import collect_results, reduce_to_scalar_loss, requires_bwd_pass + + gm = copy.deepcopy(gm) + if not disable_clone: + args = clone_inputs_retaining_gradness(args) + + if hasattr(gm, "zero_grad"): + gm.zero_grad(True) + + # TorchInductor returned callable expects lists. So, may need a boxed calling convention. + out = gm(args) if getattr(gm, "_boxed_call", False) else gm(*args) + + if only_fwd: + return out + if requires_bwd_pass(out): + loss = reduce_to_scalar_loss(out) + loss.backward() + return collect_results(gm, out, None, args) + + +def same_two_models( + gm: torch.fx.GraphModule, + opt_gm: torch.fx.GraphModule, + example_inputs: Sequence[Any], + only_fwd: bool = False, + *, + require_fp64: bool = False, + ignore_non_fp: bool = False, +) -> bool: + """ + Check two models have same accuracy. + + require_fp64: if True, raise an error if we unable to calculate the fp64 reference + ignore_non_fp: if True, do not compare outputs which are not floating point. This + is mostly useful for the minifier (which wants to avoid quantizing floating point + error into integer/boolean error) + """ + from .utils import same + + ref = run_fwd_maybe_bwd(gm, example_inputs, only_fwd) + + fp64_ref = None + if config.same_two_models_use_fp64: + try: + fp64_model, fp64_examples = cast_to_fp64( + copy.deepcopy(gm), clone_inputs_retaining_gradness(example_inputs) + ) + fp64_ref = run_fwd_maybe_bwd(fp64_model, fp64_examples, only_fwd) + except Exception: + if require_fp64: + raise RuntimeError( # noqa: B904 + "Could not generate fp64 outputs, workaround with torch._dynamo.config.same_two_models_use_fp64 = False" + ) + log.warning("Could not generate fp64 outputs") + + try: + res = run_fwd_maybe_bwd(opt_gm, example_inputs, only_fwd) + except Exception: + # This means that the minified graph is bad/exposes a different problem. + # As we are checking accuracy here, lets log the exception and return True. + log.exception( + "While minifying the program in accuracy minification mode, " + "ran into a runtime exception which is likely an unrelated issue." + " Skipping this graph." + ) + return True + + passing = same( + ref, + res, + fp64_ref, + tol=config.repro_tolerance, + equal_nan=True, + ignore_non_fp=ignore_non_fp, + ) + return passing + + +def cast_dtype_args_to_fp64(model: torch.fx.GraphModule) -> torch.fx.GraphModule: + for node in model.graph.nodes: + if ( + node.op == "call_function" + and node.target is torch.ops.prims.convert_element_type.default + ): + assert len(node.args) == 2 + if is_float_dtype(node.args[1]) and node.args[1] != torch.float64: + node.args = (node.args[0], torch.float64) + if node.op == "call_function": + dtype = node.kwargs.get("dtype") + if dtype is not None and is_float_dtype(dtype): + new_kwargs = dict(node.kwargs) + new_kwargs["dtype"] = torch.float64 + node.kwargs = new_kwargs + + model.graph.lint() + model.recompile() + return model + + +def cast_to( + dtype: torch.dtype, model: torch.fx.GraphModule, inputs: list[Any] +) -> tuple[torch.fx.GraphModule, list[Any]]: + from torch.utils._pytree import tree_map + + model = model.to(dtype) + if dtype == torch.float64: + # If casting to fp64 for accuracy comparison, we need to + # replace dtype arguments embedded in the graph with fp64 + model = cast_dtype_args_to_fp64(model) + + inputs = tree_map( + lambda x: x.to(dtype) + if isinstance(x, torch.Tensor) and x.is_floating_point() + else x, + inputs, + ) + return model, inputs + + +def cast_to_fp64( + model: torch.fx.GraphModule, inputs: list[Any] +) -> tuple[torch.fx.GraphModule, list[Any]]: + return cast_to(torch.float64, model, inputs) + + +def backend_accuracy_fails( + gm: torch.fx.GraphModule, + example_inputs: Sequence[Any], + compiler_fn: Callable[[torch.fx.GraphModule, list[Any]], torch.fx.GraphModule], + only_fwd: bool = False, + *, + require_fp64: bool = False, + ignore_non_fp: bool = False, +) -> bool: + try: + compiled_gm = compiler_fn( + copy.deepcopy(gm), clone_inputs_retaining_gradness(example_inputs) + ) + return not same_two_models( + gm, + compiled_gm, + example_inputs, + only_fwd, + require_fp64=require_fp64, + ignore_non_fp=ignore_non_fp, + ) + except Exception: + # This means that the minified graph is bad/exposes a different problem. + # As we are checking accuracy here, lets log the exception and return False. + log.exception( + "While minifying the program in accuracy minification mode, " + "ran into a runtime exception which is likely an unrelated issue." + " Skipping this graph" + ) + return False + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # +# REPRO SUPPORT CODE +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # + + +# Helper functions for computing what the default values of tensor +# values should be. These all coincide with factory functions, e.g., torch.empty + + +def _stride_or_default( + stride: torch._prims_common.StrideType | None, + *, + shape: torch._prims_common.ShapeType, +) -> torch._prims_common.StrideType: + return stride if stride is not None else utils.make_contiguous_strides_for(shape) + + +def _mk_defaulter(d: T) -> Callable[[T | None], T]: + return lambda x: x if x is not None else d + + +_dtype_or_default = _mk_defaulter(torch.float32) +_device_or_default = _mk_defaulter(torch.device("cpu")) +_storage_offset_or_default = _mk_defaulter(0) +_requires_grad_or_default = _mk_defaulter(False) +_is_leaf_or_default = _mk_defaulter(False) + + +class NopInputReader: + def __init__(self) -> None: + self.total = 0 + + def storage( + self, + storage_hash: str | None, + nbytes: int, + *, + device: torch._prims_common.DeviceLikeType | None = None, + dtype_hint: torch.dtype | None = None, + ) -> None: + self.total += 1 + + def tensor(self, *args: Any, **kwargs: Any) -> torch.Tensor | None: + pass + + def symint(self, *args: Any, **kwargs: Any) -> int | None: + pass + + def const(self, name: str) -> None: + pass + + def unsupported(self, name: str) -> None: + pass + + def generator(self, device_type: str, device_index: int) -> None: + pass + + def opaque(self, script_class_name: str) -> None: + self.total += 1 + + +# TODO: Support bundling the entire repro into a zip file for ease of +# transferring around +class InputReader: + def __init__( + self, save_dir: str | None = None, *, pbar: tqdm | None = None + ) -> None: + # If None, we will generate random data instead. It's important + # to natively support this use case as it will allow people to + # share repros without including the real data, if the problem + # reproduces even on random data. + if save_dir is None: + log.warning("no save_dir specified, will generate random data") + self.store = ContentStoreReader(save_dir) if save_dir is not None else None + self.args: list[Any] = [] + self.pbar = pbar + + def storage( + self, + storage_hash: str | None, + nbytes: int, + *, + device: torch._prims_common.DeviceLikeType | None = None, + dtype_hint: torch.dtype | None = None, + ) -> UntypedStorage: + if self.pbar is not None: + self.pbar.update(1) + device = _device_or_default(device) # type: ignore[arg-type] + dtype_hint = _dtype_or_default(dtype_hint) + if self.store is not None and storage_hash is not None: + try: + storage = self.store.read_storage(storage_hash) + except FileNotFoundError: + pass + else: + if device != storage.device: + log.warning("device mismatch: %s != %s", device, storage.device) + # TODO: transfer it to the right device? But failing this + # way would be very mysterious! Would have been better + # not to store device in the serialized format... + return storage + warn_once(f"could not load {storage_hash}, generating random data instead") + shape = (nbytes // dtype_hint.itemsize,) + stride = _stride_or_default(None, shape=shape) + return rand_strided(shape, stride, dtype_hint, device).untyped_storage() + + def tensor( + self, + storage: UntypedStorage, + shape: torch._prims_common.ShapeType, + stride: torch._prims_common.StrideType | None = None, + *, + storage_offset: int | None = None, + dtype: torch.dtype | None = None, + requires_grad: bool | None = None, + is_leaf: bool | None = None, + **metadata: Any, + ) -> torch.Tensor: + stride = _stride_or_default(stride, shape=shape) + storage_offset = _storage_offset_or_default(storage_offset) + dtype = _dtype_or_default(dtype) + is_leaf = _is_leaf_or_default(is_leaf) + requires_grad = _requires_grad_or_default(requires_grad) + t = torch.tensor( + [], dtype=dtype, device=storage.device, requires_grad=requires_grad + ) + with torch.no_grad(): + t.set_(storage, storage_offset, shape, stride) + if not is_leaf: + # Fake up some autograd history in a very naughty way + with torch.enable_grad(): + t = t.clone(memory_format=torch.preserve_format) + with torch.no_grad(): + t.set_(storage, storage_offset, shape, stride) + assert torch._subclasses.meta_utils.safe_is_leaf(t) == is_leaf + torch._utils.set_tensor_metadata(t, metadata) + self.args.append(t) + return t # for BC + + def symint(self, val: Any) -> Any: + self.args.append(val) + return val # for BC + + def const(self, name: str) -> None: + self.args.append(None) + + def unsupported(self, name: str) -> None: + self.args.append(None) + + def generator(self, device_type: str, device_index: int) -> torch._C.Generator: + gen = torch.cuda.default_generators[device_index].clone_state() + self.args.append(gen) + return gen + + def opaque(self, script_class_name: str) -> None: + self.args.append(None) + + +# Here is our writer strategy: +# 1. We will stream all of the inputs to disk +# 2. You can now deterministically randomize the inputs, or reload +# the inputs from disk +# 3. You can YOLO run the script without the inputs, in which case +# we'll fill the inputs with random data and pray. This is the +# legacy behavior, but it's also useful if you want to find out +# if we're so broken even random inputs trigger it +# 4. We could offer an in process "check if the randomized thing +# works too" but this is delicate so we don't do it + + +class InputWriter: + def __init__(self, save_dir: str | None, *, stable_hash: bool = False) -> None: + self._lines: list[str] = [] + # TODO: consider ensuring tensor and storage counters line up? + self.storage_counter = itertools.count() + self.save_dir = save_dir + self.store = ( + ContentStoreWriter(save_dir, stable_hash=stable_hash) + if save_dir is not None + else None + ) + self.seen_storages: dict[StorageWeakRef, str] = {} + + def lines(self) -> list[str]: + r = [ + "def load_args(reader):", + ] + r.extend(f" {l}" for l in self._lines) + # In case we need to change the internal format of load_args + # in an FC-breaking way + r.append("load_args._version = 0") + return r + + # Storages are untyped, but we need to initialize them with data if + # we don't have the real data, so we give a hint saying what kind + # of initialization may be appropriate + # + # If we had a FakeTensor, device_hint tells us what device should be + def storage( + self, + untyped_storage: UntypedStorage, + *, + device_hint: torch._prims_common.DeviceLikeType | None = None, + dtype_hint: torch.dtype | None = None, + ) -> str: + ws = StorageWeakRef(untyped_storage) + v = self.seen_storages.get(ws) + if v is not None: + return v + v = f"buf{next(self.storage_counter)}" + maybe_dtype_hint = "" + if _dtype_or_default(None) != _dtype_or_default(dtype_hint): + maybe_dtype_hint = f", dtype_hint={dtype_hint!r}" + # TODO: being optional on device is kind of pointless as the default + # is CPU but most repros we care about are CUDA + maybe_device = "" + device = untyped_storage.device + if device.type == "meta": + assert device_hint is not None + device = device_hint # type: ignore[assignment] + if _device_or_default(None) != device: + maybe_device = f", device={device!r}" + nbytes = untyped_storage.nbytes() + storage_hash = None + if self.store is not None and untyped_storage.device.type != "meta": + storage_hash = self.store.write_storage(untyped_storage) + self._lines.append( + f"{v} = reader.storage({storage_hash!r}, {nbytes!r}{maybe_device}{maybe_dtype_hint})" + ) + self.seen_storages[ws] = v + return v + + def tensor(self, name: str, t: torch.Tensor) -> None: + from torch.fx.experimental.symbolic_shapes import statically_known_true, sym_eq + + storage = self.storage( + t.untyped_storage(), dtype_hint=t.dtype, device_hint=t.device + ) + args = [] + # NB: this is positional, must come first + if not statically_known_true( + sym_eq(_stride_or_default(None, shape=t.shape), t.stride()) + ): + args.append(str(tuple(t.stride()))) + if _dtype_or_default(None) != t.dtype: + args.append(f"dtype={t.dtype!r}") + if not statically_known_true( + _storage_offset_or_default(None) == t.storage_offset() + ): + args.append(f"storage_offset={t.storage_offset()!r}") + tensor_metadata = torch._utils.get_tensor_metadata(t) + if tensor_metadata: + args.extend(f"{k}={v!r}" for k, v in tensor_metadata.items()) + if _requires_grad_or_default(None) != t.requires_grad: + args.append(f"requires_grad={t.requires_grad!r}") + is_leaf = torch._subclasses.meta_utils.safe_is_leaf(t) + if _is_leaf_or_default(None) != is_leaf: + args.append(f"is_leaf={is_leaf!r}") + self._lines.append( + "reader.tensor(" + + ", ".join([storage, str(tuple(t.shape)), *args]) + + f") # {name}" + ) + + def unsupported(self, name: str, arg: Any) -> None: + # NB: Try hard not to /print/ a tensor, that will be very slow + self._lines.append( + f"reader.unsupported({name!r}) # unsupported type for dumping: {type(arg)}" + ) + # Best effort dump as much useful stuff we can lol, in case you want + # to repair the repro + if isinstance(arg, (list, tuple)): + self._lines.append('"""') + for i, a in enumerate(arg): + name_i = f"{name}[{i}]" + if isinstance(a, torch.Tensor): + self.tensor(name_i, a) + elif isinstance(a, (int, torch.SymInt)): + self.symint(name_i, a) + else: + self.unsupported(name_i, a) + self._lines.append('"""') + + # write out that the arg was filtered out as it is constant + def const(self, name: str) -> None: + self._lines.append( + f"reader.const({name!r}) # {name}, filtered out during compilation" + ) + + # TODO: this doesn't actually symint atm + def symint(self, name: str, val: Any) -> None: + if isinstance(val, torch.SymInt): + val = val.node.hint + self._lines.append(f"reader.symint({val!r}) # {name}") + + def generator(self, name: str, arg: torch._C.Generator) -> None: + device = arg.device + self._lines.append( + f"reader.generator({device.type!r}, {device.index!r}) # {name}" + ) + + def opaque(self, name: str, script_class_name: str) -> None: + self._lines.append(f"reader.opaque({script_class_name!r}) # {name}") + + +def aot_graph_input_parser( + func: Callable[[list[Tensor]], list[Tensor]], + device: str = "cuda", + sym_shapes: dict[str, int] | None = None, + default_sym_shape: int | None = None, +) -> dict[str, Any]: + """ + Takes in a function which has been printed with print_readable() and constructs kwargs to run it. + + Handles Tensor inputs, Symints, and a graph module which might have tensor constants. + + Consider a function `forward` defined as follows: + + def forward(self, primals_1: "f32[1001, 6]", primals_2: "f32[s0]", primals_3: "Sym(s0)",): + _tensor_constant0: "i64[4190]" = self._tensor_constant0 + # Further implementation + + kwargs = aot_graph_input_parser(forward) + forward(**kwargs) + """ + + from torch.utils._dtype_abbrs import dtype_abbrs + + dtype_map: dict[str, torch.dtype] = { + value: key for key, value in dtype_abbrs.items() + } + dtype_pattern: str = "|".join(dtype_abbrs.values()) + + # Extracting the source code from the function + source = inspect.getsource(func) + + # Regular expressions + tensor_assignment_regex = rf"(_tensor_constant\d+): \"({dtype_pattern})\[\s*(.*?)\s*\]\" = self\.(_tensor_constant\d+)" + tensor_regex = rf"({dtype_pattern})\[\s*(.*?)\s*\]" + sym_shape_regex = r"Sym\((s\d+)\)" + + class TensorContainer: + "Container for tensors as attributes" + + # Dictionary for tensors from annotations + kwargs: dict[str, Any] = {} + + sym_shapes_dict: dict[str, int] = sym_shapes or {} + + def get_sym_int(symint: str) -> int: + torch._check( + symint in sym_shapes_dict or default_sym_shape is not None, + lambda: f"{symint} not in symbolic_shapes and default sym shape not passed in", + ) + return sym_shapes_dict.get(symint, default_sym_shape) # type: ignore[return-value] + + def gen_tensor(shape: torch._prims_common.ShapeType, dtype: torch.dtype) -> Tensor: + # Resolve symbolic shapes to concrete values + resolved_shape = [] + dynamic_dims = [] + for i, dim in enumerate(shape): + dim = dim.strip() # type: ignore[attr-defined] + if "s" in dim: + s = get_sym_int(dim) + resolved_shape.append(s) + dynamic_dims.append(i) + else: + if dim: + resolved_shape.append(int(dim)) + + constructor = torch.randn if dtype.is_floating_point else torch.zeros + out = constructor(resolved_shape, dtype=dtype, device=device) # type: ignore[call-arg] + for d in dynamic_dims: + torch._dynamo.mark_dynamic(out, d) + return out + + # Parse function annotations for tensor generation + annotations = func.__annotations__ + for param, annotation in annotations.items(): + # Skip 'return' annotation + if param == "return": + continue + + match = re.search(tensor_regex, annotation) + if match: + data_type, shape_str = match.groups() + shape = tuple(shape_str.split(",")) + dtype = dtype_map[data_type] + # pyrefly: ignore [bad-argument-type] + kwargs[param] = gen_tensor(shape, dtype) + + match = re.search(sym_shape_regex, annotation) + if match: + kwargs[param] = get_sym_int(match.group(1)) + + if "self" in inspect.signature(func).parameters: + container = TensorContainer() + kwargs["self"] = container + for match in re.finditer(tensor_assignment_regex, source): + attr_name, data_type, shape_str, _ = match.groups() + shape = tuple(shape_str.split(",")) + dtype = dtype_map[data_type] + # pyrefly: ignore [bad-argument-type] + setattr(container, attr_name, gen_tensor(shape, dtype)) + + return kwargs + + +def profile_to_file(filename: str) -> Callable[[T], T]: + """ + Decorator to cProfile a given function and save the result to disk on process exit. + + Args: + filename: filename to save profile to + """ + prof = cProfile.Profile() + filename = os.path.abspath(os.path.expanduser(filename)) + + def decorator(fn: Any) -> Any: + @functools.wraps(fn) + def wrapper(*args: Any, **kwargs: Any) -> Any: + prof.enable() + try: + return fn(*args, **kwargs) + finally: + prof.disable() + + return wrapper + + def save_it() -> None: + prof.dump_stats(filename) + sys.stderr.write( + textwrap.dedent( + f"""\ + Wrote profile to {filename}, view with: + + snakeviz {filename} + + """ + ) + ) + + atexit.register(save_it) + return decorator diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/decorators.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/decorators.py new file mode 100644 index 0000000000000000000000000000000000000000..bf06b84d6b15b616a4adfb70270b14d046c5b8fc --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/decorators.py @@ -0,0 +1,1649 @@ +""" +This module provides decorators and utilities for controlling TorchDynamo's behavior during compilation. +""" + +import functools +import inspect +import weakref +from collections.abc import Callable +from dataclasses import dataclass +from types import TracebackType +from typing import Any, overload, TYPE_CHECKING, TypeVar +from typing_extensions import ParamSpec + +import torch +import torch.utils._pytree as pytree +from torch._opaque_base import OpaqueBase +from torch.compiler import is_compiling +from torch.utils._contextlib import _DecoratorContextManager +from torch.utils._python_dispatch import is_traceable_wrapper_subclass + +from .._utils_internal import justknobs_check +from . import trace_rules, variables +from .comptime import comptime +from .eval_frame import ( + _set_stance, + DisableContext, + DynamoStance, + innermost_fn, + RunOnlyContext, + skip_code, +) +from .external_utils import ( + get_nonrecursive_disable_wrapper, + wrap_dunder_call_ctx_manager, +) +from .utils import _get_error_on_graph_break, _set_error_on_graph_break, is_function + + +justknobs_check._dynamo_marked_constant = True # type: ignore[attr-defined] + +if TYPE_CHECKING: + from types import FunctionType + + from torch._C._dynamo.eval_frame import ( # noqa: F401 + reset_code, + set_eval_frame, + set_guard_complete_hook, + set_guard_error_hook, + unsupported, + ) + + from .variables import VariableTracker +else: + for name in dir(torch._C._dynamo.eval_frame): + if name.startswith("__"): + continue + globals()[name] = getattr(torch._C._dynamo.eval_frame, name) + + +_P = ParamSpec("_P") +_R = TypeVar("_R") +FuncType = Callable[..., Any] +F = TypeVar("F", bound=FuncType) + + +def run(fn: Callable[_P, _R] | None = None) -> Any: + """Don't do any dynamic compiles, just use prior optimizations""" + if fn is not None: + fn = innermost_fn(fn) + assert callable(fn) + return RunOnlyContext()(fn) + return RunOnlyContext() + + +def disable(fn=None, recursive=True, *, reason=None, wrapping=True): # type: ignore[no-untyped-def] + """ + Decorator to disable TorchDynamo + + If recursive=True, Dynamo is completely skipped on the decorated function + frame as well as the recursively invoked functions. + + If recursive=False, Dynamo skips frames associated with the function code, + but still process recursively invoked frames. + + If reason is provided, it will be printed when Dynamo attempts to trace the disabled function. + """ + if recursive: + if fn is not None: + fn = innermost_fn(fn) + assert callable(fn) + return DisableContext(msg=reason, wrapping=wrapping)(fn) + return DisableContext(msg=reason, wrapping=wrapping) + else: + + def wrap(fn: Callable[_P, _R]) -> Callable[_P, _R]: + fn = innermost_fn(fn) + assert callable(fn) + + nonrecursive_disable_wrapper = get_nonrecursive_disable_wrapper(fn) + nonrecursive_disable_wrapper._torchdynamo_disable = True # type: ignore[attr-defined] + nonrecursive_disable_wrapper._torchdynamo_disable_msg = reason # type: ignore[attr-defined] + nonrecursive_disable_wrapper._torchdynamo_orig_callable = fn # type: ignore[attr-defined] + nonrecursive_disable_wrapper._torchdynamo_wrapper_id = id( # type: ignore[attr-defined] + nonrecursive_disable_wrapper + ) + nonrecursive_disable_wrapper._torchdynamo_disable_recursive = False # type: ignore[attr-defined] + return nonrecursive_disable_wrapper + + if fn is None: + return wrap + return wrap(fn) + + +_nonrecursive_disable_wrapper_code = disable(lambda: None, recursive=False).__code__ # type: ignore[attr-defined] +skip_code(_nonrecursive_disable_wrapper_code) + + +def skip(fn: Callable[_P, _R] | None = None) -> Callable[..., Any]: + """ + Skip frames associated with the function code, but still process recursively + invoked frames + """ + if fn is None: + return skip + fn = innermost_fn(fn) + assert callable(fn) + skip_code(fn.__code__) + fn._torchdynamo_disable = True # type: ignore[attr-defined] + return fn + + +class set_stance(_DecoratorContextManager): + """ + Decorator, context manager, function to set the current stance of the compiler. + + Stances documented in corresponding function in torch/compiler/__init__.py + """ + + _dynamo_forbidden = True + + def __init__( + self, + stance: str = "default", + *, + skip_guard_eval_unsafe: bool = False, + force_backend: str | Callable[..., Any] | None = None, + ) -> None: + if force_backend is not None and stance != "default": + raise RuntimeError("non-default stance cannot have force_backend set") + + self.stance = DynamoStance(stance, skip_guard_eval_unsafe, force_backend) + self.prev = _set_stance(self.stance) + + def __call__(self, fn: F) -> F: + _set_stance(self.prev) + wrapper = super().__call__(fn) + # forbid wrapper in graph + wrapper._dynamo_forbidden = True # type: ignore[attr-defined] + return wrapper + + def __enter__(self) -> None: + _set_stance(self.stance) + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + _set_stance(self.prev) + + def clone(self) -> "set_stance": + return self.__class__(self.stance.stance, force_backend=self.stance.backend) + + +def assume_constant_result(fn): # type: ignore[no-untyped-def] + fn._dynamo_marked_constant = True # type: ignore[attr-defined] + return fn + + +def allow_in_graph(fn): # type: ignore[no-untyped-def] + """ + Tells the compiler frontend (Dynamo) to skip symbolic introspection of the function + and instead directly write it to the graph when encountered. + + See :func:`torch.compiler.allow_in_graph`'s docstring for the full documentation + + WARNING: this API can be a footgun, please read the documentation carefully. + """ + if isinstance(fn, (list, tuple)): + return [allow_in_graph(x) for x in fn] + assert callable(fn), "allow_in_graph expects a callable" + if trace_rules.lookup_callable(fn) != variables.TorchInGraphFunctionVariable: + fn_id = id(fn) + trace_rules._disallowed_callable_ids.remove(fn_id) + trace_rules._allowed_callable_ids.add(fn_id) + + # Avoid id reuse which creates subtle bugs. + def deregister() -> None: + trace_rules._allowed_callable_ids.remove(fn_id) + + weakref.finalize(fn, deregister) + return fn + + +# pyrefly: ignore [implicit-any] +def _check_mutually_exclusive_decorators(fn: Callable, decorator_name: str) -> None: + mutually_exclusive = { + "leaf_function": trace_rules.is_leaf_function, + "nonstrict_trace": trace_rules.is_nonstrict_trace_callable, + } + + for other_name, check_fn in mutually_exclusive.items(): + if other_name != decorator_name and check_fn(fn): + first, second = sorted([decorator_name, other_name]) + raise ValueError( + f"Function {fn} cannot be both marked as @{first} and " + f"@{second}. Please use only one decorator." + ) + + +def nonstrict_trace(traceable_fn: Callable[_P, _R]) -> Callable[_P, _R]: + """ + Decorator to mark a function as nonstrict-traceable for dynamo. + + A nonstrict-traced function appears as an opaque call in the dynamo graph. + Dynamo does not trace into the function body (hence the "nonstrict"), but + aot_autograd will trace into it. + + This is similar to ``allow_in_graph`` but with enhanced support for: + - User-defined classes as inputs (must be registered with pytree) + - ``nn.Module`` as input arguments (parameters and buffers are tracked for autograd) + - Global/captured tensors treated as constants (assumed not updated during execution) + + Note: + - With ``backend="eager"``, the original Python function runs directly. + With ``backend="aot_eager"``, the graph traced by aot_autograd runs. + With ``backend="inductor"``, the traced graph is compiled with inductor. + + - Training is supported: you can call ``.backward()`` on outputs and gradients + will flow through the nonstrict-traced function. + + Dangerous patterns (may cause silent incorrectness): + - Side effects between nonstric_traced fn and compiled region: The function should + not depend on variables mutated by other code inside the compiled function, and code + after the call should not depend on mutations made by it. + + - Implicit inputs (closures/globals): Tensors captured from enclosing scopes + are treated as constants. Gradients will NOT flow back to them. Pass tensors + as explicit arguments if gradients are needed. + + Restrictions: + - Both inputs and outputs must use pytree-compatible types. User-defined classes + must be registered via :func:`torch.utils._pytree.register_pytree_node`, + :func:`torch.utils._pytree.register_dataclass`, or + :func:`torch.utils._pytree.register_constant`. Tensors, Python primitives (int, float, bool, str), + symbolic types (SymInt, SymFloat, SymBool), and built-in containers (list, + tuple, dict) are already handled by default. + - Primitive values and container structure are specialized per call site: + each call site expects the same primitives and structure on every execution. + + Example:: + + >>> import torch + >>> @torch._dynamo.nonstrict_trace + ... def traced_forward(model, x): + ... # It's OK to have dynamo graph break within nonstrict_trace region + ... torch._dynamo.graph_break() + ... return model(x) + x + ... + >>> class MyModule(torch.nn.Module): + ... def __init__(self): + ... super().__init__() + ... self.inner = torch.nn.Linear(10, 10) + ... + ... def forward(self, x): + ... return traced_forward(self.inner, x) + ... + >>> # Compile and run + >>> model = MyModule() + >>> opt_model = torch.compile(model, backend="aot_eager", fullgraph=True) + >>> out = opt_model(torch.randn(10, 10)) + >>> out.sum().backward() # Gradients flow through traced_forward + + """ + assert callable(traceable_fn), "nonstrict_trace expects a callable" + + _check_mutually_exclusive_decorators(traceable_fn, "nonstrict_trace") + + @functools.wraps(traceable_fn) + def wrapped(*args: _P.args, **kwargs: _P.kwargs) -> _R: + return traceable_fn(*args, **kwargs) + + wrapped_id = id(wrapped) + + # This line allows us to reuse much of the `allow_in_graph` impl. + trace_rules._allowed_callable_ids.add(wrapped_id) + + # This line allows us to diverge the impl from `allow_in_graph`. + trace_rules._nonstrict_trace_callable_ids.add(wrapped_id) + + # Avoid id reuse which creates subtle bugs. + def deregister() -> None: + trace_rules._allowed_callable_ids.remove(wrapped_id) + trace_rules._nonstrict_trace_callable_ids.remove(wrapped_id) + + weakref.finalize(wrapped, deregister) + + return wrapped + + +def _invoke_leaf_function_python( + real_impl: Callable[..., Any], + fake_impl: Callable[..., Any], + args: tuple[Any, ...], + kwargs: dict[str, Any], + mutates_args: frozenset[str] | None = None, +) -> Any: + """Call invoke_leaf_function HOP directly from Python. + + This enables @leaf_function to work with make_fx + without relying on Dynamo to intercept the call. + """ + from torch._higher_order_ops.invoke_leaf_function import ( + _LeafCallable, + convert_modules_to_states, + invoke_leaf_function, + make_leaf_function_wrappers, + store_makefx_modules, + ) + + captured_modules: list[torch.nn.Module] = [] + seen_module_ids: dict[int, int] = {} # id(module) -> position in captured_modules + for val in pytree.tree_flatten( + (args, kwargs), is_leaf=lambda x: isinstance(x, torch.nn.Module) + )[0]: + if isinstance(val, torch.nn.Module) and id(val) not in seen_module_ids: + seen_module_ids[id(val)] = len(captured_modules) + captured_modules.append(val) + + global_indices = store_makefx_modules(captured_modules) + module_to_index = { + mod_id: global_indices[pos] for mod_id, pos in seen_module_ids.items() + } + + processed = convert_modules_to_states((args, kwargs), module_to_index) + flat_args, input_spec = pytree.tree_flatten(processed) + + # Single-element mutable list so the wrappers can write back the output + # TreeSpec. Read captured_out_spec[0] after the wrappers have been called. + captured_out_spec: list[pytree.TreeSpec | None] = [None] + wrapped_real, wrapped_fake = make_leaf_function_wrappers( + real_impl, fake_impl, captured_out_spec + ) + + real_fn_callable = _LeafCallable(wrapped_real) + fake_fn_callable = _LeafCallable(wrapped_fake) + + mutated_flat_indices = "" + if mutates_args: + from torch._higher_order_ops.invoke_leaf_function import ( + _resolve_mutated_flat_indices, + ) + + mutated_flat_indices = _resolve_mutated_flat_indices( + real_impl, mutates_args, len(flat_args), input_spec + ) + + flat_out = invoke_leaf_function( + real_fn_callable, fake_fn_callable, input_spec, mutated_flat_indices, *flat_args + ) + + assert captured_out_spec[0] is not None + return pytree.tree_unflatten(flat_out, captured_out_spec[0]) + + +@overload +def leaf_function(fn: Callable[_P, _R]) -> Callable[_P, _R]: ... + + +@overload +def leaf_function( + *, mutates_args: set[str] +) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: ... + + +def leaf_function( + fn: Callable[_P, _R] | None = None, *, mutates_args: set[str] | None = None +) -> Callable[_P, _R] | Callable[[Callable[_P, _R]], Callable[_P, _R]]: + """ + Decorator to mark a function as a leaf function for :func:`torch.compile`. + + A leaf function appears as an opaque operation in the compiled graph. During + compilation, Dynamo and AOT autograd do not trace into it. At runtime, the + original eager Python code is executed directly. + + Quick Start: + Suppose we want to log per-sample statistics during the forward pass:: + + def compute_and_log_stats(x): + stats = x.mean(dim=1) + logger.info(f"Per-sample means: {stats}") + return (stats,) + + With ``torch.compile(..., fullgraph=True)``, this fails because + ``logger.info`` causes a graph break. To fix it, follow the steps below: + + 1. Decorate your function with ``@leaf_function`` + 2. Define a shape-inference function using ``@your_fn.register_fake`` + + Concrete implementation:: + + >>> import logging + >>> import torch + >>> from torch._dynamo.decorators import leaf_function + >>> + >>> logging.basicConfig(level=logging.INFO) + >>> logger = logging.getLogger(__name__) + >>> + >>> @leaf_function + ... def compute_and_log_stats(x): + ... stats = x.mean(dim=1) # Shape: (x.shape[0],) + ... logger.info(f"Per-sample means: {stats}") + ... return (stats,) + ... + >>> @compute_and_log_stats.register_fake + ... def compute_and_log_stats_fake(x): + ... # Match the output shape: (x.shape[0],) + ... return (x.new_empty(x.shape[0]),) + ... + >>> def fn(x): + ... return compute_and_log_stats(x) + ... + >>> x = torch.randn(3, 4) + >>> compiled_fn = torch.compile(fn, backend="aot_eager", fullgraph=True) + >>> out = compiled_fn(x)[0] + + When to Use: + Use ``leaf_function`` when you need eager execution semantics that tracing + cannot preserve: + + - **Non-traceable code**: The function consists of code that Dynamo or + AOT Autograd cannot trace. + - **Runtime side effects**: Operations that must happen exactly once per call + at runtime (e.g., logging, external library calls). + + If your function has a static computation graph and no runtime side effects, + prefer :func:`allow_in_graph` or :func:`nonstrict_trace` instead. They allow AOT autograd + to trace through and potentially optimize the code. + + Usage: + **Inputs and Outputs**: + - Both inputs and outputs must use pytree-compatible types. + Tensors, Python primitives (int, float, bool, str), and built-in containers + (list, tuple, dict) are supported by default. + + Note: We recommend leaf_functions only accept and return tensors. Though primitive + types (int, float, bool, str) are supported in inputs and outputs, they may cause + surprising behavior: primitive inputs are guarded and may trigger recompilation, + while primitive output values are captured from the fake implementation at compile time and + remain fixed for all subsequent executions. Even though the real function runs + at runtime, its primitive return values are silently replaced with the compile-time + values from the fake implementation. + + Example:: + + # BAD: primitive output varies at runtime + counter = 0 + + + @leaf_function + def count_calls(x): + global counter + counter += 1 + return (x, counter) + + + @count_calls.register_fake + def count_calls_fake(x): + return (x, 999) # placeholder value + + + # At runtime: real function runs (counter increments to 1, 2, 3, ...) + # But returned count is always 999 (from fake implementation at compile time) + + - User-defined classes must be registered via :func:`torch.utils._pytree.register_pytree_node` + or :func:`torch.utils._pytree.register_dataclass`. + + - :class:`torch.nn.Module` can also be passed as input; its parameters and buffers + are tracked for autograd. The module must exist outside the compile region. + Running the module multiple times must not modify its parameters, buffers, or attributes. + + **register_fake (required)**: + Since the function body is not traced, you must + provide a shape-inference function via ``@fn.register_fake``. It runs at compile + time with FakeTensor inputs (tensors with no data, only metadata) and must + satisfy the following requirements: + + - Must have the same input and output signature (e.g., same pytree structure, same tensor metadata + such as shapes, dtypes, device, strides) as the real function + - Must be runnable with FakeTensor inputs + - Must only use its explicit arguments (no closures over tensors or modules) + + Note: The input and output signature must be determinable at compile time. + If your function's output structure depends on runtime values, adjust the leaf function + boundary until outputs become predictable. + + To validate that your fake implementation matches the real function's outputs, set + ``torch._dynamo.config.leaf_function_validate_outputs = True``. + + Limitations: + Currently, inductor backend and :func:`torch.export.export` are not yet supported. + + Training / Autograd: + Training is supported automatically if your leaf function is differentiable + in eager mode (e.g., it's implemented with PyTorch ops, :class:`torch.autograd.Function`, + or differentiable custom ops). + + **Restriction**: Calling ``.backward()`` *inside* the leaf function is not + supported. + + **Escaped gradients check**: If the leaf function closes over a tensor with + ``requires_grad=True``, gradients will not flow back to it (only explicit inputs + receive gradients). To detect such cases, set + ``torch._dynamo.config.leaf_function_check_escaped_gradients = True``. + When enabled, a ``RuntimeError`` is raised with details about the escaped tensors. + + Internally, inputs and outputs are detached from the outer autograd graph at the + leaf function boundary. The leaf function builds its own local autograd + graph. In backward, gradients propagate through this local graph to the + leaf function's inputs. If a :class:`torch.nn.Module` is passed in as input, the gradients + will also flow back to its parameters and buffers. + + How leaf_function Works: + Understanding this helps avoid pitfalls: + + 1. **Compilation**: Dynamo and AOT autograd do not trace into the leaf function. + Only your fake implementation runs during compilation to determine output signatures, + shapes, and dtypes. The real function runs at runtime as eager Python. + + 2. **Isolation**: Mutations to shared state (globals, closures) may not be + visible across the boundary between leaf functions and compiled regions. Pass data + explicitly as function arguments and return results as outputs. + + Dangerous Patterns: + These patterns may cause silent incorrectness or errors: + + - **Side effects between leaf functions and compiled regions**: Mutations to + Python state inside a leaf function may not be visible to the compiled + region, and vice versa. The compiled graph may reorder or eliminate + operations in ways that break assumptions about side effect ordering. + + Bad:: + + # Compiled region + self.counter += 1 + y = my_leaf_fn(self, x) # Don't depend on self.counter inside leaf_fn + + y = my_leaf_fn(self, x) + result = self.state # Don't depend on state mutated by leaf_fn + + Logging/printing inside the leaf function is fine since it doesn't affect + correctness. + + - **In-place mutations on inputs**: Undeclared in-place mutations on input + tensors are detected and will raise an error. Either declare mutations via + ``mutates_args`` or clone inputs before mutating. + + Bad:: + + @leaf_function + def my_leaf_fn(x): + x.add_(1) # Will raise: "Undeclared in-place mutation" + return (x,) + + Good:: + + @leaf_function(mutates_args={"buf"}) + def my_leaf_fn(x, buf): + buf.add_(1) # OK: declared in mutates_args + return (x + buf,) + + For pytree args (lists, tuples, dicts of tensors), use the parameter name + to declare mutation on all contained tensors:: + + @leaf_function(mutates_args={"buffers"}) + def my_leaf_fn(x, buffers): + for buf in buffers: + buf.add_(1) # OK: 'buffers' declared in mutates_args + return (x + sum(buffers),) + + Or use bracket notation for fine-grained control:: + + @leaf_function(mutates_args={"buffers[0]"}) + def my_leaf_fn(x, buffers): + buffers[0].add_(1) # OK: 'buffers[0]' declared + return (x + buffers[1],) # buffers[1] is not cloned + + - **Closures in fake implementation**: Tensors or modules captured from enclosing scopes + in the fake implementation will cause compilation errors. The real function can + close over them if they don't require gradient, but the fake implementation must + only use its arguments. + + Bad:: + + weight = torch.randn(3, 3) + + + @leaf_function + def my_leaf_fn(x): + return (x @ weight,) + + + @my_leaf_fn.register_fake + def my_leaf_fn_fake(x): + return (x @ weight,) # Error: weight used in fake implementation + + Good:: + + weight = torch.randn(3, 3) + + + @leaf_function + def my_leaf_fn(x): + return (x @ weight,) # OK: real function can use closure + + + @my_leaf_fn.register_fake + def my_leaf_fn_fake(x): + return (x @ torch.empty_like(x),) # OK: uses only args + + Example:: + Wrapping a linear function with logging as a leaf_function. Gradients + flow back because the inner operations are differentiable:: + + >>> import torch + >>> import torch.nn.functional as F + >>> from torch._dynamo.decorators import leaf_function + >>> + >>> @leaf_function + ... def custom_forward(linear, x): + ... # Logging at runtime + ... print(f"Input: shape={x.shape}, mean={x.mean().item():.4f}") + ... out = F.linear(x, linear.weight, linear.bias) + ... print(f"Output: shape={out.shape}, norm={out.norm().item():.4f}") + ... return (out,) + ... + >>> @custom_forward.register_fake + ... def custom_forward_fake(linear, x): + ... # Return tensor with correct shape: (batch, out_features) + ... return (x.new_empty(x.shape[0], linear.weight.shape[0]),) + ... + >>> class MyModel(torch.nn.Module): + ... def __init__(self): + ... super().__init__() + ... self.linear = torch.nn.Linear(10, 20) + ... def forward(self, x): + ... return custom_forward(self.linear, x) + ... + >>> model = MyModel() + >>> compiled = torch.compile(model, backend="aot_eager", fullgraph=True) + >>> x = torch.randn(32, 10, requires_grad=True) + >>> out = compiled(x)[0] + >>> out.sum().backward() # Gradients flow to model.linear.weight/bias and x + + Args: + fn: The function being decorated. + mutates_args: Set of Python expressions (as strings) identifying arguments + that the function mutates in-place. Each string is evaluated against + the function's parameters. Examples: ``'buf'`` for a plain tensor, + ``'model.running_mean'`` for an nn.Module buffer, + ``'buffers'`` to mark all tensors in a list, ``'buffers[0]'`` for a + specific element, ``'state["key"]'`` for a dict entry. + """ + if fn is None: + return functools.partial(leaf_function, mutates_args=mutates_args) + + from . import trace_rules + + _check_mutually_exclusive_decorators(fn, "leaf_function") + + if mutates_args: + import inspect + import re + + params = set(inspect.signature(fn).parameters) + for expr in mutates_args: + root = re.split(r"[.\[]", expr, maxsplit=1)[0] + if root not in params: + raise ValueError( + f"mutates_args expression '{expr}' refers to parameter '{root}', " + f"which is not a parameter of '{fn.__name__}'. " + f"Available parameters: {', '.join(params)}" + ) + + @functools.wraps(fn) + def inner(*args: _P.args, **kwargs: _P.kwargs) -> _R: + if inner._torchdynamo_leaf_fake_fn is None: # type: ignore[attr-defined] + raise ValueError( + f"leaf_function '{getattr(fn, '__name__', fn)}' " + "requires a fake implementation. Please provide one using the @.register_fake " + "decorator. See the leaf_function docstring for details." + ) + + return _invoke_leaf_function_python( + fn, # pyrefly: ignore [bad-argument-type] + # pyrefly: ignore [bad-argument-type] + inner._torchdynamo_leaf_fake_fn, + args, + kwargs, + mutates_args=inner._torchdynamo_leaf_mutates_args, # pyrefly: ignore [missing-attribute] + ) # type: ignore[attr-defined] + + inner._torchdynamo_leaf_real_fn = fn # type: ignore[attr-defined] + inner._torchdynamo_leaf_fake_fn = None # type: ignore[attr-defined] + inner._torchdynamo_leaf_mutates_args = ( # pyrefly: ignore [missing-attribute] + frozenset(mutates_args) if mutates_args else frozenset() + ) # type: ignore[attr-defined] + + # Follow nonstrict_trace implementation + wrapped_id = id(inner) + trace_rules._allowed_callable_ids.add(wrapped_id) + trace_rules._leaf_function_ids.add(wrapped_id) + + def deregister() -> None: + trace_rules._allowed_callable_ids.remove(wrapped_id) + trace_rules._leaf_function_ids.remove(wrapped_id) + + weakref.finalize(inner, deregister) + + def register_fake_setter(fake_fn: Callable[..., Any]) -> Callable[..., Any]: + inner._torchdynamo_leaf_fake_fn = fake_fn # type: ignore[attr-defined] + return inner + + inner.register_fake = register_fake_setter # type: ignore[attr-defined] + + return inner + + +def _disallow_in_graph_helper(throw_if_not_allowed: bool) -> Callable[..., Any]: + def inner(fn: Any) -> Any: + if isinstance(fn, (list, tuple)): + return [disallow_in_graph(x) for x in fn] + assert callable(fn), "disallow_in_graph expects a callable" + if ( + throw_if_not_allowed + and trace_rules.lookup_callable(fn) + != variables.TorchInGraphFunctionVariable + and trace_rules.lookup(fn) != variables.TorchInGraphFunctionVariable + ): + raise RuntimeError( + "disallow_in_graph is expected to be used on an already allowed callable (like torch.* ops). " + "Allowed callables means callables that TorchDynamo puts as-is in the extracted graph." + ) + trace_rules._allowed_callable_ids.remove(id(fn)) + trace_rules._nonstrict_trace_callable_ids.remove(id(fn)) + trace_rules._disallowed_callable_ids.add(id(fn)) + return fn + + return inner + + +def disallow_in_graph(fn: Callable[..., Any]) -> Any: + """ + Customize which functions TorchDynamo will exclude in the generated + graph and force a graph break on. + :: + + torch._dynamo.disallow_in_graph(torch.sub) + + + @torch._dynamo.optimize(...) + def fn(a): + x = torch.add(x, 1) + x = torch.sub(x, 1) + x = torch.add(x, 1) + return x + + + fn(...) + + Will break the graph on `torch.sub`, and give two graphs each with a + single `torch.add()` op. + """ + return _disallow_in_graph_helper(throw_if_not_allowed=True)(fn) + + +@_disallow_in_graph_helper(throw_if_not_allowed=False) +def graph_break(msg: str = "") -> None: + """Force a graph break""" + + +# NOTE: primarily used for internal debugging purposes! +@_disallow_in_graph_helper(throw_if_not_allowed=False) +def skip_frame(msg: str = "") -> None: + """Force a skipped frame""" + + +@_disallow_in_graph_helper(throw_if_not_allowed=False) +def step_unsupported(msg: str = "") -> None: + """Force a step unsupported graph break, which results in compiling + the traced FX graph so far, then skipping the rest of the frame. + In order to get expected behavior, there should be at least 2 ops + and a part of the code not contained in any try/with blocks.""" + + +def forbid_in_graph(fn: Any) -> Any: + """ + Customize which functions TorchDynamo will assert are not present while tracing. + + If you want a graph break on this function instead, use disallow_in_graph. + TODO(voz): We now have allow_in_graph, disallow_in_graph, forbid_in_graph - some more robust + documentation would not be amiss. + """ + if isinstance(fn, (list, tuple)): + return [forbid_in_graph(x) for x in fn] + assert callable(fn), "forbid_in_graph applies only to callables" + # pyrefly: ignore [missing-attribute] + fn._dynamo_forbidden = True + return fn + + +def substitute_in_graph( + original_fn: Callable[_P, _R], + *, + can_constant_fold_through: bool = False, + skip_signature_check: bool = False, + # type that is embedded in the Python interpreter + is_embedded_type: bool = False, # internal use only +) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: + """ + Register a polyfill handler for a function, usually a C function from the C extension, to be + used in place of the original function when inlining the original function in the graph. + + .. note:: + + The polyfill handler is only used when inlining the original function. It is not used when + the original function is called directly. In the eager mode, the decorated function calls + the performant C function rather than the polyfill handler. + + The polyfill handler is a function that will be called in place of the original function when + inlining the original function. The polyfill handler should have the same signature and the same + behavior as the original function. + + Args: + original_fn (callable): The original function, usually a C function, to register a polyfill + handler for. + can_constant_fold_through (bool, optional): Whether the polyfill handler can be constant + folded through. That is, if the polyfill handler is a pure function and its arguments + are constant, the result of the polyfill handler can be constant folded during the + compilation. Defaults to ``False``. + skip_signature_check (bool, optional): Whether to skip the signature check between the + original function and the polyfill handler. Defaults to ``False``. + + Returns: + A decorator that registers the polyfill handler for the original function. + + Example:: + + >>> # xdoctest: +SKIP("conflict with the tests: duplicate polyfill handlers") + >>> import operator + >>> operator.indexOf([1, 2, 3, 4, 5], 3) + 2 + >>> torch.compile(operator.indexOf, fullgraph=True)([1, 2, 3, 4, 5], 3) + Traceback (most recent call last): + ... + torch._dynamo.exc.Unsupported: ... + + >>> @torch.compiler.substitute_in_graph(operator.indexOf) + ... def indexOf(a, b, /): + ... for i, item in enumerate(a): + ... if item is b or item == b: + ... return i + ... raise ValueError("sequence.index(x): x not in sequence") + >>> + >>> torch.compile(operator.indexOf, fullgraph=True)([1, 2, 3, 4, 5], 3) + 2 + """ + if not is_function(original_fn) and not ( + is_embedded_type and inspect.isclass(original_fn) + ): + raise TypeError( + f"substitute_in_graph expects a function but got {type(original_fn)!r}" + ) + if is_embedded_type: + if not inspect.isclass(original_fn): + raise TypeError( + f"substitute_in_graph expects a class but got {type(original_fn)!r}" + ) + + from .variables.builder import ITERTOOLS_POLYFILLED_TYPE_IDS, ITERTOOLS_TYPE_IDS + + if id(original_fn) in ITERTOOLS_TYPE_IDS: + ITERTOOLS_POLYFILLED_TYPE_IDS.add(id(original_fn)) + + def wrapper(traceable_fn: Callable[_P, _R]) -> Callable[_P, _R]: + if not is_function(traceable_fn): + raise TypeError( + f"@substitute_in_graph(...) expects a function but got {type(traceable_fn)!r}" + ) + + if not skip_signature_check: + try: + original_sig = inspect.signature(original_fn) + except ValueError: + pass + else: + traceable_sig = inspect.signature(traceable_fn) + + def sig_ident( + sig: inspect.Signature, + ) -> tuple[tuple[str, ...], set[str], dict[str, Any]]: + # Ignore annotations for parameters and return type + return ( + tuple( + p.name + for p in sig.parameters.values() + if ( + p.kind + not in { + p.KEYWORD_ONLY, + # the name of *args and **kwargs is not important + p.VAR_POSITIONAL, + p.VAR_KEYWORD, + } + ) + ), + { + p.name + for p in sig.parameters.values() + if p.kind == p.KEYWORD_ONLY + }, + { + p.name: p.default + for p in sig.parameters.values() + # the name of *args and **kwargs is not important + if p.kind not in {p.VAR_POSITIONAL, p.VAR_KEYWORD} + }, + ) + + wildcard_sig = inspect.signature(lambda *args, **kwargs: None) + + if ( + sig_ident(original_sig) != sig_ident(traceable_sig) + and sig_ident(original_sig) != sig_ident(wildcard_sig) + and sig_ident(traceable_sig) != sig_ident(wildcard_sig) + ): + raise TypeError( + f"Signature mismatch between {original_fn} and {traceable_fn}: " + f"{original_sig} != {traceable_sig}" + ) + + from torch._dynamo.guards import GuardBuilder + from torch._dynamo.trace_rules import ( + _polyfilled_function_ids, + get_torch_obj_rule_map, + ) + from torch._dynamo.variables import PolyfilledFunctionVariable + from torch._dynamo.variables.builder import VariableBuilder + + id_dispatch_map = VariableBuilder._id_dispatch() + if id(original_fn) in id_dispatch_map: + raise ValueError( + f"Duplicate dispatch rule for {original_fn}: " + "already registered in VariableBuilder's id dispatch map" + ) + + if id(original_fn) in _polyfilled_function_ids: + raise ValueError(f"Duplicate polyfilled object {original_fn}") + + rule_map: dict[Any, type[VariableTracker]] = get_torch_obj_rule_map() + if original_fn in rule_map: + raise ValueError( + f"Duplicate object {original_fn} with different rules: " + f"{PolyfilledFunctionVariable}, {rule_map[original_fn]}" + ) + + polyfill_handlers: dict[Callable[..., Any], FunctionType] + polyfill_handlers = PolyfilledFunctionVariable._get_polyfill_handlers() + if original_fn in polyfill_handlers: + raise ValueError( + f"Duplicate polyfill handlers for {original_fn}: " + f"already handled by {polyfill_handlers[original_fn]}" + ) + + # Need to wrap the function because we may cannot assign __torch_dynamo_polyfill__ to a + # C++ function. + @functools.wraps(traceable_fn) + def wrapped(*args: _P.args, **kwargs: _P.kwargs) -> _R: + return original_fn(*args, **kwargs) + + def dispatch_fn( + self: VariableBuilder, value: Callable[_P, _R] + ) -> PolyfilledFunctionVariable: + if inspect.isclass(value): + guard_type = GuardBuilder.CLASS_MATCH + elif inspect.ismodule(value): + guard_type = GuardBuilder.MODULE_MATCH + else: + guard_type = GuardBuilder.ID_MATCH + guards = self.install_guards(guard_type) + assert guards is not None + return PolyfilledFunctionVariable( + value, + source=self.source, + **guards, + ) + + id_dispatch_map[id(original_fn)] = id_dispatch_map[id(wrapped)] = dispatch_fn + _polyfilled_function_ids.add(id(original_fn)) + _polyfilled_function_ids.add(id(wrapped)) + rule_map[original_fn] = rule_map[wrapped] = PolyfilledFunctionVariable + polyfill_handlers[original_fn] = polyfill_handlers[wrapped] = wrapped # type: ignore[assignment] + + wrapped.__torch_dynamo_original__ = original_fn # type: ignore[attr-defined] + wrapped.__torch_dynamo_polyfill__ = traceable_fn # type: ignore[attr-defined] + wrapped.__torch_dynamo_can_constant_fold_through__ = can_constant_fold_through # type: ignore[attr-defined] + + return wrapped # type: ignore[return-value] + + return wrapper + + +# Helper function to flatten a tensor subclass and apply a function to +# all inner tensors that match the outer dim. Used to reduce duplication +# across the various marking APIs. +def _apply_func_to_inner_tensors_of_same_dim( + func: Callable[..., Any], t: object, *args: Any, **kwargs: Any +) -> None: + assert is_traceable_wrapper_subclass(t) + + attrs, _ctx = t.__tensor_flatten__() + assert isinstance(t, torch.Tensor) + for attr in attrs: + match getattr(t, attr): + case torch.Tensor() as inner: + if inner.dim() == t.dim(): + func(inner, *args, **kwargs) + case OpaqueBase(): + pass + case unexpected: + raise AssertionError( + f"expected Tensor or OpaqueBase, got {type(unexpected)}" + ) + + +@dataclass(frozen=True, slots=True) +class _DimRange: + """ + This represents an dimension of a tensor and the corresponding + min and max values it can take. Don't create this + class directly; instead, use :func:`mark_dynamic`. + """ + + dim: int + min: int + max: int + + +@forbid_in_graph +def mark_unbacked( + t: Any, + index: int | list[Any] | tuple[Any], + hint_override: int | None = None, + strict: bool = False, + specialize_on: list[Any] | None = None, + shape_id: str | None = None, + min: int | None = None, + max: int | None = None, +) -> None: + """ + Mark a tensor as having an unbacked dimension. This changes the semantics of operations: + - The size of the specified dimension will always be reported as not equal to zero or one. + - Assertions on this index will be turned into runtime asserts. + - Attempting to get the real value of this dimension will raise an exception. + - In effect, this dimension is treated as data-dependent (its value is unknown). + + Args: + t (Any): The tensor to mark as having an unbacked dimension. + index (int or list/tuple of int): The dimension(s) to mark as unbacked. Can be a single integer or a list/tuple of integers. + hint_override (Optional[int], default=None): An optional integer to override the size hint for this dimension. + This is only used by the inductor backend for size hint queries, such as during autotuning. + NOTE: changing hint_override values will cause FxGraphCache misses, since hint overrides + affect inductor codegen decisions and are included in the cache key via + ShapeEnv.var_to_hint_override. + strict (bool, default=False): If True, an error will be raised if the unbacked dimension is specialized. + By default (strict=False), specialization is allowed and will proceed without error. + specialize_on (Optional[list[Any]], default=None): A list of specialization criteria (e.g., lambdas) for this dimension. + If provided, Dynamo will generate specialized compiled regions for each criterion in addition to a generic trace. + shape_id (Optional[str], default=None): An optional identifier to group unbacked dimensions together. + All unbacked dimensions with the same shape_id will share the same unbacked symbol. This is useful when multiple tensors + are known to have the same batch size at runtime. A runtime assertion is added + to ensure this property at runtime. + min (Optional[int], default=None): Minimum value constraint for this dimension. + If provided, a runtime check will be added to ensure the dimension is >= min. + max (Optional[int], default=None): Maximum value constraint for this dimension. + If provided, a runtime check will be added to ensure the dimension is <= max. + """ + if torch.distributed.is_available() and isinstance( + t, torch.distributed.tensor.DTensor + ): + # apply on inner tensor sizes/strides + mark_unbacked(t._local_tensor, index, shape_id=shape_id) + else: + # You could have copied the mark_dynamic behavior but I'm not convinced + # it's what you want + assert not is_traceable_wrapper_subclass(t), "not implemented yet" + + if isinstance(index, int): + if strict: + if not hasattr(t, "_dynamo_strict_unbacked_indices"): + t._dynamo_strict_unbacked_indices = set() + + t._dynamo_strict_unbacked_indices.add(index) + return + + if not hasattr(t, "_specialized_on"): + # pyrefly: ignore [implicit-any] + t._specialize_on = {} + + if not hasattr(t, "_dynamo_unbacked_indices"): + t._dynamo_unbacked_indices = set() + + if not hasattr(t, "_dynamo_hint_overrides"): + # pyrefly: ignore [implicit-any] + t._dynamo_hint_overrides = {} + + if hint_override: + t._dynamo_hint_overrides[index] = hint_override + + if min is not None or max is not None: + if not hasattr(t, "_dynamo_unbacked_bounds"): + # pyrefly: ignore [implicit-any] + t._dynamo_unbacked_bounds = {} + t._dynamo_unbacked_bounds[index] = (min, max) + + if shape_id is not None: + if not hasattr(t, "_dynamo_shape_ids"): + # pyrefly: ignore [implicit-any] + t._dynamo_shape_ids = {} + t._dynamo_shape_ids[index] = shape_id + + # FX tracers don't respect @forbid_in_graph and choke on the following error since it passes in proxies: + # TypeError: 'Attribute' object does not support item assignment + + if isinstance(t._specialize_on, dict): + t._specialize_on[index] = specialize_on if specialize_on is not None else [] + + t._dynamo_unbacked_indices.add(index) + return + + assert isinstance(index, (list, tuple)) + for i in index: + mark_unbacked(t, i, shape_id=shape_id, min=min, max=max) + + +@forbid_in_graph +def mark_dynamic( + t: Any, + index: int | list[Any] | tuple[Any], + *, + hint_override: int | None = None, + min: int | None = None, + max: int | None = None, + specialize_on: list[Any] | None = None, +) -> None: + """ + Mark a tensor as having a dynamic dim and set corresponding min and max range for the dim. + + [Note - on the state of mark_dynamic] + + The behavior of having a dynamic dimension on a tensor is governed by a few factors: + + 1) torch._dynamo.config dynamic_shapes True or False. + a) dynamic_shapes=True - dynamic_shapes must be True for mark_dynamic to work. + a) dynamic_shapes=False - This config will raise an exception when used in conjunction with + mark_dynamic. We will eventually support this. + + 2) If the dimension is fully constrained - as in, it does not allow more than a single value + in both eager (torch.compile, torch._dynamo.optimize) mode and export mode (torch._dynamo.export), + we will raise an error + + 3) If the dimension is partially constrained - allowing at least 2 values but not the full unbounded + range of shapes, in eager we will pass it through, but export will raise an error. + + 4) Attempts to trace this function will explicitly raise. As such, all calls to mark_dynamic must be made + before torch.compile. + + 5) If hint_override is passed, the hint_override for the specified dimension will replace the provided value + from the first example input as the official size hint. Note: changing hint_override values will cause + FxGraphCache misses, since hint overrides affect inductor codegen decisions (autotuning, reduction + strategy, etc.) and are included in the cache key via ShapeEnv.var_to_hint_override. + + 6) If specialize_on is passed in, we will perform a single generic Dynamo trace followed by + multiple specialized compilations in addition to a single generic compilation. NB: For now we only support + per dimension specialization, or in other words we do not generate a cross product of specializations. + At runtime, we will dispatch to a specialized compiled region if the input matches the specialization criteria. + + For example: + mark_dynamic(..., specialize_on=[ + lambda x: x == 8, + lambda x: x == 16 + ]) + + This approach results in one Dynamo trace and two backend compilations. When the input dimension equals 8 or 16 + at runtime, execution will be directed to the specialized compiled region. Performance measurements indicate + 2-8x speedups depending on the specific specialization and model architecture. + + """ + if is_traceable_wrapper_subclass(t): + # default behavior: mirror mark_dynamic() on all inner tensors with same dim as t + # TODO: Make this configurable via a supported public API + _apply_func_to_inner_tensors_of_same_dim( + mark_dynamic, t, index, min=min, max=max + ) + + if isinstance(index, int): + if not hasattr(t, "_dynamo_dynamic_indices"): + t._dynamo_dynamic_indices = set() + + t._dynamo_dynamic_range = set() + + # pyrefly: ignore [implicit-any] + t._dynamo_hint_overrides = {} + + if not hasattr(t, "_specialize_on"): + # pyrefly: ignore [implicit-any] + t._specialize_on = {} + + if hint_override: + t._dynamo_hint_overrides[index] = hint_override + # TODO(voz): Should we bounds check? + + t._dynamo_dynamic_indices.add(index) + t._dynamo_dynamic_range.add(_DimRange(index, min, max)) # type: ignore[arg-type] + + # FX tracers don't respect @forbid_in_graph and choke on the following error since it passes in proxies: + # TypeError: 'Attribute' object does not support item assignment + + if isinstance(t._specialize_on, dict): + t._specialize_on[index] = specialize_on if specialize_on is not None else [] + + return + + assert isinstance(index, (list, tuple)) + for i in index: + mark_dynamic(t, i, min=min, max=max) + mark_dynamic(t, i, min=min, max=max, specialize_on=specialize_on) + + +@forbid_in_graph +def maybe_mark_dynamic(t: Any, index: int | list[Any] | tuple[Any]) -> None: + """ + Mark a tensor as having a dynamic dim, but don't enforce it (i.e., if this + dimension ends up getting specialized, don't error). + """ + if is_traceable_wrapper_subclass(t): + # default behavior: mirror maybe_mark_dynamic() on all inner tensors with same dim as t + # TODO: Make this configurable via a supported public API + _apply_func_to_inner_tensors_of_same_dim(maybe_mark_dynamic, t, index) + + if isinstance(index, int): + if not hasattr(t, "_dynamo_weak_dynamic_indices"): + t._dynamo_weak_dynamic_indices = set() + # TODO(voz): Should we bounds check? + + t._dynamo_weak_dynamic_indices.add(index) + return + + assert isinstance(index, (list, tuple)) + for i in index: + maybe_mark_dynamic(t, i) + + +def mark_static(t: Any, index: int | list[Any] | tuple[Any] | None = None) -> None: + """ + Mark a tensor as having a static dim or mark a nn module class as static. + + For tensors + =========== + This will prevent us from attempting to compile it dynamically + when dynamic=True; this can improve trace-time performance. + + This has lower precedence than mark_dynamic. + + Unlike mark_dynamic, this can be done inside a graph, in which case it + induces specialization on the tensor. + + For nn.Module classes + ===================== + For static nn.Module classes, TorchDynamo assumes that the module instance + attributes will not be modified after compilation. This will ensure that + TorchDynamo keeps integer attributes CONSTANT and not symints. + + From TorchDynamo implementation side, the instances of static-marked + nn.Module class will be converted to UnspecializedBuiltinNNModuleVariable, + which have the same properties. + + Note that we still have to guard on the attributes, because different + instances of the nn.Module can have different values of the attributes. The + key point here is that the attributes are static. + """ + if is_compiling(): + if index is None: + for s in t.size(): + comptime.force_static(s) + else: + comptime.force_static(t.size(index)) + return + + if is_traceable_wrapper_subclass(t): + # default behavior: mirror mark_static() on all inner tensors with same dim as t + # TODO: Make this configurable via a supported public API + _apply_func_to_inner_tensors_of_same_dim(mark_static, t, index) + + if not isinstance(t, torch.Tensor) and issubclass(t, torch.nn.Module): + t._dynamo_marked_static = True + # pyrefly: ignore [bad-return] + return t + + if not isinstance(t, torch.Tensor): + raise TypeError( + f"mark_static expects a tensor/nn.Module class but received {type(t)}" + ) + + if isinstance(index, int): + if not hasattr(t, "_dynamo_static_indices"): + t._dynamo_static_indices = set() # type: ignore[attr-defined] + # TODO(voz): Should we bounds check? + t._dynamo_static_indices.add(index) # type: ignore[attr-defined] + elif index is None: + for i in range(t.dim()): + mark_static(t, i) + else: + assert isinstance(index, (list, tuple)) + for i in index: + mark_static(t, i) + + +@forbid_in_graph +def mark_static_address(t: Any, guard: bool = False) -> None: + """ + Marks an input tensor whose address should be treated as constant across calls to the + same dynamo-compiled function. This indicates to cudagraphs that an extra allocation + is not needed for this input. The data_ptr will be guarded if guard=True, and cause a full + recompile if the data_ptr changes. Note: If this address changes, cudagraphs will re-record + if guard=False. + """ + if not isinstance(t, torch.Tensor): + raise TypeError(f"mark_static_address expects a tensor but received {type(t)}") + + if guard: + t._dynamo_static_input_type = "guarded" # type: ignore[attr-defined] + else: + t._dynamo_static_input_type = "unguarded" # type: ignore[attr-defined] + + +def _patch_einops_symint_compat(einops_mod: Any) -> None: + """Backport the SymInt lru_cache fix from einops 0.7.0 into einops <= 0.6.1.""" + for name in ("_reconstruct_from_shape", "_prepare_transformation_recipe"): + cached = getattr(einops_mod, name) + uncached = cached.__wrapped__ + + def make_wrapper(cached_fn: Any, uncached_fn: Any) -> Any: + @functools.wraps(cached_fn) + def wrapper(*args: Any, **kwargs: Any) -> Any: + try: + return cached_fn(*args, **kwargs) + except TypeError: + return uncached_fn(*args, **kwargs) + + return wrapper + + setattr(einops_mod, name, make_wrapper(cached, uncached)) + + +# One day, Dynamo will support tracing into einops directly (no allow_in_graph needed) +# Note that PyTorch supports multiple versions of einops, so when that day comes, +# we still need to be really careful about version matches. +def _allow_in_graph_einops() -> None: + import einops + + # There is a lru_cache logspam issue with einops when allow_in_graph is not + # used. Disabling this for now until the lru_cache issue is resolved. + # if einops.__version__ >= "0.8.2": + # if hasattr(einops, "einops") and hasattr(einops.einops, "get_backend"): + # # trigger backend registration up front to avoid a later guard failure + # # that would otherwise cause a recompilation + # einops.rearrange(torch.randn(1), "i -> i") + # # einops 0.8.2+ don't need explicit allow_in_graph calls + # return + + try: + # requires einops > 0.6.1, torch >= 2.0 + from einops._torch_specific import ( # type: ignore[attr-defined] # noqa: F401 + _ops_were_registered_in_torchdynamo, + ) + + # einops > 0.6.1 will call the op registration logic as it is imported. + except ImportError: + # einops <= 0.6.1 doesn't handle unhashable SymInt in its lru_cache'd + # helpers. Backport the try/except TypeError fallback from einops 0.7.0+ + # so allow_in_graph works during fake tensor validation. + _patch_einops_symint_compat(einops.einops) # type: ignore[attr-defined] + allow_in_graph(einops.rearrange) + allow_in_graph(einops.reduce) + if hasattr(einops, "repeat"): + allow_in_graph(einops.repeat) # available since einops 0.2.0 + if hasattr(einops, "einsum"): + allow_in_graph(einops.einsum) # available since einops 0.5.0 + if hasattr(einops, "pack"): + allow_in_graph(einops.pack) # available since einops 0.6.0 + if hasattr(einops, "unpack"): + allow_in_graph(einops.unpack) # available since einops 0.6.0 + + +# Note: this carefully avoids eagerly import einops. +trace_rules.add_module_init_func("einops", _allow_in_graph_einops) + + +# Proxy class for torch._dynamo.config patching - so dynamo can identify context managers/decorators +# created by patch_dynamo_config, compared to ones created by a raw torch._dynamo.config.patch. +class DynamoConfigPatchProxy: + def __init__(self, config_patch: Any) -> None: + self.config_patch = config_patch + + @property + def changes(self) -> dict[str, Any]: + return self.config_patch.changes + + # Decorator implementation that simply sets up `self` as a context manager. + # Placed in external_utils so that we can trace through it. + __call__ = wrap_dunder_call_ctx_manager + + def __enter__(self) -> None: + return self.config_patch.__enter__() + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + return self.config_patch.__exit__(exc_type, exc_val, exc_tb) + + +# Criteria for patchable config: +# - Config values must be constants (i.e. int, float, str, bool, None). +# - in particular, NO list, set, dict. +# - Traceable config patches are only useful for configs that change dynamo behavior +# from symbolic_convert and below. +# - e.g. patching recompile_limit won't really do anything. +# - For patching configs that affect Dynamo behavior above symbolic_convert, +# ensure that Dynamo behaves soundly even if tracing is done with different config. +# - e.g. be careful if patching guard-related configs as configs may have changed +# between guard creation and evaluation. +_allowed_config_patches = ( + "verbose", + "verify_correctness", + "rewrite_assert_with_torch_assert", + "capture_scalar_outputs", + "allow_unspec_int_on_nn_module", + "skip_torchrec", + "dont_skip_tracing", + "nested_graph_breaks", +) + +from . import config + + +for name in _allowed_config_patches: + assert hasattr(config, name), "nonexistent config" +del config + + +def _patch_dynamo_config_check(changes: dict[str, Any]) -> None: + for k, v in changes.items(): + if k not in _allowed_config_patches: + raise ValueError( + f"patch_dynamo_config does not support patching config {k}" + ) + if not torch._dynamo.utils.is_safe_constant(v): + raise ValueError( + f"patch_dynamo_config does not support patching config {k} " + f"with non-safe-constant value {v}" + ) + + +# TODO: also implement nonrecursive patch_dynamo_config/dont_skip_tracing. +# Unlike config.patch, we also need to accept tuple as input in order to +# deal with context manager reconstruction. +def patch_dynamo_config( + arg1: str | dict[str, Any] | tuple[tuple[str, Any], ...] | None = None, + arg2: Any = None, + **kwargs: Any, +) -> DynamoConfigPatchProxy: + """ + A wrapper around torch._dynamo.config.patch that can be traced by Dynamo to + temporarily change config values DURING tracing. + + See _allowed_config_patches for the list of allowed config patches. + + Arguments are the same as with torch._dynamo.config.patch. + + Can be used as a decorator or a context manager. + + User code SHOULD NOT MODIFY the return value of this function. + + WARNING: changing Dynamo config during tracing can lead to unpredictable tracing behavior! + Proceed only as advised! + """ + if isinstance(arg1, tuple): + arg1 = dict(arg1) + config_patch = torch._dynamo.config.patch(arg1, arg2, **kwargs) + _patch_dynamo_config_check(config_patch.changes) + # check for valid patching using config_patch.changes + return DynamoConfigPatchProxy(config_patch) + + +@overload +def dont_skip_tracing(fn: None = None) -> DynamoConfigPatchProxy: ... + + +@overload +def dont_skip_tracing(fn: Callable[_P, _R]) -> Callable[_P, _R]: ... + + +def dont_skip_tracing(fn: Any | None = None) -> Any: + """ + Context manager/decorator to trace into functions intentionally marked by developers to be skipped + when tracing. + + This decorator will also apply to recursively invoked functions. + """ + ctx = patch_dynamo_config(dont_skip_tracing=True) + if fn: + return ctx(fn) + return ctx + + +@overload +def disable_nested_graph_breaks(fn: None = None) -> DynamoConfigPatchProxy: ... + + +@overload +def disable_nested_graph_breaks(fn: Callable[_P, _R]) -> Callable[_P, _R]: ... + + +def disable_nested_graph_breaks(fn: Any | None = None) -> Any: + """ + Context manager/decorator to disable nested graph breaks when tracing + this function and any nested functions. Used when nested graph breaks + is causing problems. + """ + ctx = patch_dynamo_config(nested_graph_breaks=False) + if fn: + return ctx(fn) + return ctx + + +class ErrorOnGraphBreakDecoratorContextManager: + def __init__(self, error_on_graph_break: bool) -> None: + self.error_on_graph_break = error_on_graph_break + + __call__ = wrap_dunder_call_ctx_manager + + def __enter__(self) -> None: + self.prev_error_on_graph_break = _get_error_on_graph_break() + _set_error_on_graph_break(self.error_on_graph_break) + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + _set_error_on_graph_break(self.prev_error_on_graph_break) + + +def error_on_graph_break( + error_on_graph_break: bool, +) -> ErrorOnGraphBreakDecoratorContextManager: + """ + Context manager/decorator to toggle torch.compile's `error_on_graph_break` setting at compile time. + + If `fullgraph` is set, then `error_on_graph_break` does nothing + (i.e. `fullgraph = True` takes higher precedence). If `fullgraph` is False, then + `error_on_graph_break` determines whether `torch.compile` throws an error upon + encountering a graph break, or attempts to continue tracing. + + `error_on_graph_break` can be toggled during compile time with this decorator to allow graph breaks in some + compiled regions but not others. One key difference from `fullgraph` is that `error_on_graph_break = True` + does NOT guarantee that a single graph is captured from the compiled function. + + The default value of torch.compile's `error_on_graph_break` setting is False. + """ + return ErrorOnGraphBreakDecoratorContextManager(error_on_graph_break) + + +class CudagraphOverrideContextManager: + """Context manager that overrides cudagraph recording during tracing.""" + + def __init__(self, fwd: bool | None = None, bwd: bool | None = None) -> None: + self.fwd = fwd + self.bwd = bwd + + __call__ = wrap_dunder_call_ctx_manager + + def __enter__(self) -> None: + pass + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + pass + + +def override_cudagraphs( + fwd: bool | None = None, bwd: bool | None = None +) -> CudagraphOverrideContextManager: + """ + Context manager/decorator to override cudagraph recording for compiled graphs. + + When used as a context manager, overrides cudagraphs for all graph segments + within the block (including across graph breaks). + + When used as a decorator, marks a function so that any compiled graph + inlining it will have cudagraphs overridden. + + Args: + fwd: If False, disable cudagraphs for forward. If True, force enable. + If None, don't override. + bwd: If False, disable cudagraphs for backward. If True, force enable. + If None, don't override. + """ + return CudagraphOverrideContextManager(fwd=fwd, bwd=bwd) + + +def is_dynamo_disable_recursive(method: Callable[[Any], Any]) -> bool | None: + """ + Check if a method is marked as `dynamo_disable` recursively. It returns: + - True if disable(recursive=True) + - False if disable(recursive=False) + - None if method is not a disable decorator + """ + return getattr(method, "_torchdynamo_disable_recursive", None) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/device_interface.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/device_interface.py new file mode 100644 index 0000000000000000000000000000000000000000..7ff97f4ac5a08f2cb085b254ffd1216b4b0d6aa6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/device_interface.py @@ -0,0 +1,653 @@ +""" +Device abstraction layer for TorchDynamo and Inductor backends. + +This module provides a unified interface for different hardware backends (CUDA, XPU, +CPU, MPS, MTIA) through a common device interface. Key components include: + +- DeviceInterface: Base class defining the common API for all device types +- Device-specific implementations: CudaInterface, XpuInterface, CpuInterface, MpsInterface, MtiaInterface +- Device registration system for managing available backends +- Worker APIs for multi-processing scenarios +- Stream and event management across different devices +- Device property caching for worker processes + +The abstraction layer enables device-agnostic code in TorchDynamo while allowing +specialized implementations for each hardware backend's unique features. +""" + +import inspect +import time +from collections import namedtuple +from collections.abc import Callable, Iterable +from dataclasses import dataclass +from typing import Any, Literal + +import torch +from torch.utils._pallas import has_torch_tpu + + +get_cuda_stream: Callable[[int], int] | None +if torch.cuda._is_compiled(): + from torch._C import _cuda_getCurrentRawStream as get_cuda_stream +else: + get_cuda_stream = None + +# Recording the device properties in the main process but used in worker process. +caching_worker_device_properties: dict[str, Any] = {} +caching_worker_current_devices: dict[str, int] = {} + + +class DeviceInterface: + """ + This is a simple device runtime interface for Inductor. It enables custom + backends to be integrated with Inductor in a device-agnostic semantic. + """ + + class device: + def __new__(cls, device: torch.types.Device) -> Any: + raise NotImplementedError + + class Event: + def __new__(cls, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError( + "Event should be inherited from torch.Event, otherwise, it couldn't be captured by dynamo." + ) + + class Stream: + def __new__(cls, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError( + "Stream should be inherited from torch.Stream, otherwise, it couldn't be captured by dynamo." + ) + + class Worker: + """ + Worker API to query device properties that will work in multi processing + workers that cannot use the GPU APIs (due to processing fork() and + initialization time issues). Properties are recorded in the main process + before we fork the workers. + """ + + @staticmethod + def set_device(device: int) -> None: + raise NotImplementedError + + @staticmethod + def current_device() -> int: + raise NotImplementedError + + @staticmethod + def get_device_properties(device: torch.types.Device = None) -> Any: + raise NotImplementedError + + @staticmethod + def current_device() -> int: + raise NotImplementedError + + @staticmethod + def set_device(device: torch.types.Device) -> None: + raise NotImplementedError + + @staticmethod + def maybe_exchange_device(device: int) -> int: + raise NotImplementedError + + @staticmethod + def exchange_device(device: int) -> int: + raise NotImplementedError + + @staticmethod + def device_count() -> int: + raise NotImplementedError + + @staticmethod + def is_available() -> bool: + raise NotImplementedError + + @staticmethod + def stream(stream: torch.Stream) -> Any: + raise NotImplementedError + + @staticmethod + def current_stream() -> torch.Stream: + raise NotImplementedError + + @staticmethod + def set_stream(stream: torch.Stream) -> None: + raise NotImplementedError + + @staticmethod + def _set_stream_by_id(stream_id: int, device_index: int, device_type: int) -> None: + raise NotImplementedError + + @staticmethod + def get_raw_stream(device_idx: int) -> int: + raise NotImplementedError + + @staticmethod + def synchronize(device: torch.types.Device = None) -> None: + raise NotImplementedError + + @classmethod + def get_device_properties(cls, device: torch.types.Device = None) -> Any: + return cls.Worker.get_device_properties(device) + + @staticmethod + def get_compute_capability(device: torch.types.Device = None) -> Any: + raise NotImplementedError + + @staticmethod + def is_bf16_supported(including_emulation: bool = False) -> bool: + raise NotImplementedError + + @classmethod + def is_dtype_supported( + cls, dtype: torch.dtype, including_emulation: bool = False + ) -> bool: + return dtype != torch.bfloat16 or cls.is_bf16_supported(including_emulation) + + @staticmethod + def memory_allocated(device: torch.types.Device = None) -> int: + raise NotImplementedError + + @staticmethod + def is_triton_capable(device: torch.types.Device = None) -> bool: + """ + Returns True if the device has Triton support, False otherwise, even if + the appropriate Triton backend is not available. + """ + return False + + @classmethod + def raise_if_triton_unavailable(cls, device: torch.types.Device = None) -> None: + """ + Raises a `RuntimeError` with the appropriate human-readable instructions + to resolve the issue if Triton is not available for the given device, or + the default device if `device` is `None`. + + The caller should ensure the presence of the 'triton' package before + calling this method. + """ + if not cls.is_triton_capable(): + raise RuntimeError("This device is not capable of supporting Triton") + + +class DeviceGuard: + """ + This class provides a context manager for device switching. This is a stripped + down version of torch.{device_name}.device. + + The context manager changes the current device to the given device index + on entering the context and restores the original device on exiting. + The device is switched using the provided device interface. + """ + + def __init__( + self, device_interface: type[DeviceInterface], index: int | None + ) -> None: + self.device_interface = device_interface + self.idx = index + self.prev_idx = -1 + + def __enter__(self) -> None: + if self.idx is not None: + self.prev_idx = self.device_interface.exchange_device(self.idx) + + def __exit__(self, type: Any, value: Any, traceback: Any) -> Literal[False]: + if self.idx is not None: + self.idx = self.device_interface.maybe_exchange_device(self.prev_idx) + return False + + +class CudaInterface(DeviceInterface): + device = torch.cuda.device # type: ignore[assignment] + + # register Event and Stream class into the backend interface + # make sure Event and Stream are implemented and inherited from the torch.Event and torch.Stream + Event = torch.cuda.Event # type: ignore[assignment] + Stream = torch.cuda.Stream # type: ignore[assignment] + + # pyrefly: ignore [bad-override] + class Worker: + @staticmethod + def set_device(device: int) -> None: + caching_worker_current_devices["cuda"] = device + + @staticmethod + def current_device() -> int: + if "cuda" in caching_worker_current_devices: + return caching_worker_current_devices["cuda"] + return torch.cuda.current_device() + + @staticmethod + def get_device_properties(device: torch.types.Device = None) -> Any: + if device is not None: + if isinstance(device, str): + device = torch.device(device) + assert device.type == "cuda" + if isinstance(device, torch.device): + device = device.index + if device is None: + device = CudaInterface.Worker.current_device() + + if "cuda" not in caching_worker_device_properties: + device_prop = [ + torch.cuda.get_device_properties(i) + for i in range(torch.cuda.device_count()) + ] + caching_worker_device_properties["cuda"] = device_prop + + return caching_worker_device_properties["cuda"][device] + + current_device = staticmethod(torch.cuda.current_device) + set_device = staticmethod(torch.cuda.set_device) + device_count = staticmethod(torch.cuda.device_count) + stream = staticmethod(torch.cuda.stream) # type: ignore[assignment] + current_stream = staticmethod(torch.cuda.current_stream) + set_stream = staticmethod(torch.cuda.set_stream) # type: ignore[assignment] + _set_stream_by_id = staticmethod(torch.cuda._set_stream_by_id) # type: ignore[assignment] + synchronize = staticmethod(torch.cuda.synchronize) + get_device_properties = staticmethod(torch.cuda.get_device_properties) # type: ignore[assignment] + get_raw_stream = staticmethod(get_cuda_stream) # type: ignore[assignment, arg-type] + exchange_device = staticmethod(torch.cuda._exchange_device) # type: ignore[arg-type, has-type] + maybe_exchange_device = staticmethod(torch.cuda._maybe_exchange_device) # type: ignore[arg-type, has-type] + memory_allocated = staticmethod(torch.cuda.memory_allocated) + is_bf16_supported = staticmethod(torch.cuda.is_bf16_supported) # type: ignore[arg-type] + + # Can be mock patched by @patch decorator. + @staticmethod + def is_available() -> bool: + return torch.cuda.is_available() + + @staticmethod + def get_compute_capability(device: torch.types.Device = None) -> int | str: + if torch.version.hip is None: + major, min = torch.cuda.get_device_capability(device) + return major * 10 + min + else: + return torch.cuda.get_device_properties(device).gcnArchName.split(":", 1)[0] + + @staticmethod + def is_triton_capable(device: torch.types.Device = None) -> bool: + return ( + torch.version.hip is not None + or torch.cuda.get_device_properties(device).major >= 7 + ) + + @staticmethod + def raise_if_triton_unavailable(device: torch.types.Device = None) -> None: + from torch._inductor.exc import GPUTooOldForTriton + + if not CudaInterface.is_triton_capable(device): + device_props = torch.cuda.get_device_properties(device) + raise GPUTooOldForTriton(device_props, inspect.currentframe()) + + import triton.backends + + if torch.version.hip is not None: + if "amd" not in triton.backends.backends: + raise RuntimeError("triton not built with the 'amd' backend") + elif "nvidia" not in triton.backends.backends: + raise RuntimeError("triton not built with the 'nvidia' backend") + + +get_mtia_stream: Callable[[int], int] | None +if torch.mtia._is_compiled(): + from torch._C import _mtia_getCurrentRawStream as get_mtia_stream +else: + get_mtia_stream = None + + +class MtiaInterface(DeviceInterface): + device = torch.mtia.device # type: ignore[assignment] + Event = torch.mtia.Event # type: ignore[assignment] + Stream = torch.mtia.Stream # type: ignore[assignment] + + # pyrefly: ignore [bad-override] + class Worker: + @staticmethod + def set_device(device: int) -> None: + caching_worker_current_devices["mtia"] = device + + @staticmethod + def current_device() -> int: + if "mtia" in caching_worker_current_devices: + return caching_worker_current_devices["mtia"] + return torch.mtia.current_device() + + @staticmethod + def get_device_properties(device: torch.types.Device = None) -> Any: + if device is not None: + if isinstance(device, str): + device = torch.device(device) + assert device.type == "mtia" + if isinstance(device, torch.device): + device = device.index + if device is None: + device = MtiaInterface.Worker.current_device() + + if "mtia" not in caching_worker_device_properties: + device_prop = [ + torch.mtia.get_device_properties(i) + for i in range(torch.mtia.device_count()) + ] + caching_worker_device_properties["mtia"] = device_prop + + return caching_worker_device_properties["mtia"][device] + + current_device = staticmethod(torch.mtia.current_device) + set_device = staticmethod(torch.mtia.set_device) # type: ignore[assignment] + device_count = staticmethod(torch.mtia.device_count) + stream = staticmethod(torch.mtia.stream) # type: ignore[assignment] + current_stream = staticmethod(torch.mtia.current_stream) + set_stream = staticmethod(torch.mtia.set_stream) # type: ignore[assignment] + _set_stream_by_id = staticmethod(torch.mtia._set_stream_by_id) # type: ignore[assignment] + synchronize = staticmethod(torch.mtia.synchronize) + get_device_properties = staticmethod(torch.mtia.get_device_properties) # type: ignore[assignment] + get_raw_stream = staticmethod(get_mtia_stream) # type: ignore[assignment, arg-type] + exchange_device = staticmethod(torch.mtia._exchange_device) # type: ignore[arg-type, has-type] + maybe_exchange_device = staticmethod(torch.mtia._maybe_exchange_device) # type: ignore[arg-type, has-type] + memory_allocated = staticmethod(torch.mtia.memory_allocated) # type: ignore[assignment] + is_bf16_supported = staticmethod(torch.mtia.is_bf16_supported) # type: ignore[arg-type] + + # Can be mock patched by @patch decorator. + @staticmethod + def is_available() -> bool: + ret = torch.mtia.is_available() + return ret + + @staticmethod + def get_compute_capability(device: torch.types.Device = None) -> Any: + cc = torch.mtia.get_device_capability(device) + return cc + + @staticmethod + def is_triton_capable(device: torch.types.Device = None) -> bool: + return True + + @staticmethod + def raise_if_triton_unavailable(device: torch.types.Device = None) -> None: + import triton.backends + + if "mtia" not in triton.backends.backends: + raise RuntimeError("triton not built with the 'mtia' backend") + + +get_xpu_stream: Callable[[int], int] | None +if torch.xpu._is_compiled(): + from torch._C import _xpu_getCurrentRawStream as get_xpu_stream +else: + get_xpu_stream = None + + +class XpuInterface(DeviceInterface): + device = torch.xpu.device # type: ignore[assignment] + Event = torch.xpu.Event # type: ignore[assignment] + Stream = torch.xpu.Stream # type: ignore[assignment] + + # pyrefly: ignore [bad-override] + class Worker: + @staticmethod + def set_device(device: int) -> None: + caching_worker_current_devices["xpu"] = device + + @staticmethod + def current_device() -> int: + if "xpu" in caching_worker_current_devices: + return caching_worker_current_devices["xpu"] + return torch.xpu.current_device() + + @staticmethod + def get_device_properties(device: torch.types.Device = None) -> Any: + if device is not None: + if isinstance(device, str): + device = torch.device(device) + assert device.type == "xpu" + if isinstance(device, torch.device): + device = device.index + if device is None: + device = XpuInterface.Worker.current_device() + + if "xpu" not in caching_worker_device_properties: + device_prop = [ + torch.xpu.get_device_properties(i) + for i in range(torch.xpu.device_count()) + ] + caching_worker_device_properties["xpu"] = device_prop + + return caching_worker_device_properties["xpu"][device] + + current_device = staticmethod(torch.xpu.current_device) + set_device = staticmethod(torch.xpu.set_device) + device_count = staticmethod(torch.xpu.device_count) # type: ignore[has-type] + stream = staticmethod(torch.xpu.stream) # type: ignore[assignment] + current_stream = staticmethod(torch.xpu.current_stream) + set_stream = staticmethod(torch.xpu.set_stream) # type: ignore[assignment] + _set_stream_by_id = staticmethod(torch.xpu._set_stream_by_id) # type: ignore[assignment] + synchronize = staticmethod(torch.xpu.synchronize) + get_device_properties = staticmethod(torch.xpu.get_device_properties) # type: ignore[assignment] + get_raw_stream = staticmethod(get_xpu_stream) # type: ignore[assignment, arg-type] + exchange_device = staticmethod(torch.xpu._exchange_device) # type: ignore[arg-type, has-type] + maybe_exchange_device = staticmethod(torch.xpu._maybe_exchange_device) # type: ignore[arg-type, has-type] + memory_allocated = staticmethod(torch.xpu.memory_allocated) + + # Can be mock patched by @patch decorator. + @staticmethod + def is_available() -> bool: + return torch.xpu.is_available() + + @staticmethod + def get_compute_capability(device: torch.types.Device = None) -> Any: + cc = torch.xpu.get_device_capability(device) + return cc + + @staticmethod + def is_bf16_supported(including_emulation: bool = False) -> bool: + return torch.xpu.is_bf16_supported() + + @staticmethod + def is_triton_capable(device: torch.types.Device = None) -> bool: + return True + + @staticmethod + def raise_if_triton_unavailable(device: torch.types.Device = None) -> None: + import triton.backends + + if "intel" not in triton.backends.backends: + raise RuntimeError("triton not built with the 'intel' backend") + + +@dataclass +class CpuDeviceProperties: + multi_processor_count: int + + +class CpuInterface(DeviceInterface): + # pyrefly: ignore [bad-override] + class Event(torch.Event): + def __init__(self, enable_timing: bool = True) -> None: + self.time = 0.0 + + def elapsed_time(self, other: Any) -> float: + return (other.time - self.time) * 1000 + + def record(self, stream: Any = None) -> None: + self.time = time.perf_counter() + + # pyrefly: ignore [bad-override] + class Worker: + @staticmethod + def get_device_properties( + device: torch.types.Device = None, + ) -> CpuDeviceProperties: + import multiprocessing + + cpu_count = multiprocessing.cpu_count() + return CpuDeviceProperties(cpu_count) + + @staticmethod + def is_available() -> bool: + return True + + @staticmethod + def is_bf16_supported(including_emulation: bool = False) -> bool: + return True + + @staticmethod + def get_compute_capability(device: torch.types.Device = None) -> str: + return "" + + @staticmethod + def get_raw_stream(device_idx: Any) -> int: + return 0 + + @staticmethod + def current_device() -> int: + return 0 + + @staticmethod + def synchronize(device: torch.types.Device = None) -> None: + pass + + @staticmethod + def is_triton_capable(device: torch.types.Device = None) -> bool: + return True + + @staticmethod + def raise_if_triton_unavailable(device: torch.types.Device = None) -> None: + import triton.backends + + if "cpu" not in triton.backends.backends: + raise RuntimeError("triton not built with the 'cpu' backend") + + +class MpsInterface(DeviceInterface): + @staticmethod + def is_bf16_supported(including_emulation: bool = False) -> bool: + return True + + @classmethod + def is_dtype_supported( + cls, dtype: torch.dtype, including_emulation: bool = False + ) -> bool: + if dtype in [torch.float64, torch.complex128]: + return False + return True + + @staticmethod + def is_available() -> bool: + return torch.backends.mps.is_available() + + @staticmethod + def current_device() -> int: + return 0 + + @staticmethod + def get_compute_capability(device: torch.types.Device = None) -> str: + return "" + + @staticmethod + def synchronize(device: torch.types.Device = None) -> None: + torch.mps.synchronize() + + # pyrefly: ignore [bad-override] + class Worker: + @staticmethod + def get_device_properties(device: torch.types.Device = None) -> Any: + return namedtuple("MPSProperties", ["multi_processor_count"])( + torch.backends.mps.get_core_count() # type: ignore[arg-type] + ) + + @staticmethod + def current_device() -> int: + return 0 + + +class TpuInterface(DeviceInterface): + @staticmethod + def is_bf16_supported(including_emulation: bool = False) -> bool: + return True + + @classmethod + def is_dtype_supported( + cls, dtype: torch.dtype, including_emulation: bool = False + ) -> bool: + return dtype not in ( + torch.float64, + torch.complex32, + torch.complex64, + torch.complex128, + torch.half, + ) + + @staticmethod + def is_available() -> bool: + return has_torch_tpu() + + @staticmethod + def current_device() -> int: + return 0 + + @staticmethod + def get_compute_capability(device: torch.types.Device = None) -> str: + return "" + + # pyrefly: ignore [bad-override] + class Worker: + @staticmethod + def get_device_properties(device: torch.types.Device = None) -> Any: + return namedtuple("TPUProperties", ["multi_processor_count"])( + 1 # type: ignore[arg-type] + ) + + @staticmethod + def current_device() -> int: + return 0 + + +device_interfaces: dict[str, type[DeviceInterface]] = {} +_device_initialized = False + + +def register_interface_for_device( + device: str | torch.device, device_interface: type[DeviceInterface] +) -> None: + if isinstance(device, torch.device): + device = device.type + device_interfaces[device] = device_interface + + +def get_interface_for_device(device: str | torch.device) -> type[DeviceInterface]: + if isinstance(device, torch.device): + device = device.type + if not _device_initialized: + init_device_reg() + if device in device_interfaces: + return device_interfaces[device] + raise NotImplementedError(f"No interface for device {device}") + + +def get_registered_device_interfaces() -> Iterable[tuple[str, type[DeviceInterface]]]: + if not _device_initialized: + init_device_reg() + return device_interfaces.items() + + +def init_device_reg() -> None: + global _device_initialized + register_interface_for_device("cuda", CudaInterface) + for i in range(torch.cuda.device_count()): + register_interface_for_device(f"cuda:{i}", CudaInterface) + + register_interface_for_device("xpu", XpuInterface) + for i in range(torch.xpu.device_count()): + register_interface_for_device(f"xpu:{i}", XpuInterface) + + register_interface_for_device("mtia", MtiaInterface) + for i in range(torch.mtia.device_count()): + register_interface_for_device(f"mtia:{i}", MtiaInterface) + + register_interface_for_device("cpu", CpuInterface) + register_interface_for_device("mps", MpsInterface) + register_interface_for_device("tpu", TpuInterface) + + _device_initialized = True diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/distributed.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/distributed.py new file mode 100644 index 0000000000000000000000000000000000000000..16c93592adfaacae59347359bef7f3beda9dfcf2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/distributed.py @@ -0,0 +1,52 @@ +""" +Manages process groups for distributed compilation in TorchDynamo. + +This module handles the initialization and management of process groups used for +distributed compilation. Key features: + +- Lazy initialization of compilation process groups +- Only creates groups when distributed mode is enabled and available +- Integrates with compiler_collectives configuration setting +- Provides a single global process group for compilation coordination + +The process group is created only when needed and if the distributed environment +is properly initialized, making it safe to import and use this module even in +non-distributed scenarios. +""" + +import torch.distributed as dist + +from . import config + + +_COMPILE_PG: dist.ProcessGroup | None = None +_GUARD_PG: dist.ProcessGroup | None = None + + +def get_compile_pg() -> dist.ProcessGroup | None: + if ( + config.enable_compiler_collectives + and dist.is_available() + and dist.is_initialized() + ): + global _COMPILE_PG + if _COMPILE_PG is None: + # , timeout=datetime.timedelta(seconds=2) + _COMPILE_PG = dist.distributed_c10d._new_group_with_tag( + pg_tag="pt2_compile_pg" + ) + return _COMPILE_PG + + return None + + +# NB: Unlike get_compile_pg, this is only called when guard collectives were +# explicitly requested +def get_guard_pg() -> dist.ProcessGroup | None: + if dist.is_available() and dist.is_initialized(): + global _GUARD_PG + if _GUARD_PG is None: + _GUARD_PG = dist.distributed_c10d._new_group_with_tag(pg_tag="pt2_guard_pg") + return _GUARD_PG + + return None diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/dynamo_profiler.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/dynamo_profiler.py new file mode 100644 index 0000000000000000000000000000000000000000..aa587c3f6cb9dd68e90726037b6aaf1e1286707c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/dynamo_profiler.py @@ -0,0 +1,428 @@ +""" +Dynamo Profiler - tracks where Dynamo spends time during compilation. + +This module provides profiling functionality for Dynamo tracing, showing per-function +cumtime (inclusive) and tottime (exclusive) in a cProfile-compatible format. +The output can be visualized with tools like snakeviz. + +Usage: + # Enable via config (prints pstats output): + torch._dynamo.config.dynamo_profiler = True + + # Or save to file for snakeviz: + torch._dynamo.config.dynamo_profiler = "/tmp/dynamo.prof" + # Then: snakeviz /tmp/dynamo.prof +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, TYPE_CHECKING + + +if TYPE_CHECKING: + import pstats + + +@dataclass +class FunctionTraceTiming: + """ + Timing data for a single inlined function trace. + + Follows cProfile conventions: + - cumtime: total time in function including all subcalls (inclusive) + - tottime: time in function excluding subcalls (exclusive) + - caller info: who called this function (for building call graph) + """ + + # Function identification + func_name: str + filename: str + firstlineno: int + # Timing data (in nanoseconds) - cProfile-style + cumtime_ns: int # Inclusive time (includes subcalls) + tottime_ns: int # Exclusive time (excludes subcalls) + # Code stats (for comparing tracing overhead vs function complexity) + bytecode_count: int + # Nesting depth when this function was traced + inline_depth: int + # Caller information (for building call graph edges) + caller_func_name: str | None = None + caller_filename: str | None = None + caller_firstlineno: int | None = None + # Whether this is a primitive (non-recursive) call + # A call is primitive if the function doesn't appear anywhere in the call stack + is_primitive_call: bool = True + # Full call stack at the time of this call (for proper snakeviz drill-down) + # Each entry is (func_name, filename, firstlineno) + call_stack: tuple[tuple[str, str, int], ...] = () + + # Backwards compatibility alias + @property + def trace_time_ns(self) -> int: + return self.cumtime_ns + + @property + def trace_time_ms(self) -> float: + return self.cumtime_ns / 1e6 + + @property + def cumtime_ms(self) -> float: + return self.cumtime_ns / 1e6 + + @property + def tottime_ms(self) -> float: + return self.tottime_ns / 1e6 + + @property + def caller_key(self) -> tuple[str, int, str] | None: + """Return caller as a pstats-compatible key tuple.""" + if self.caller_func_name is not None: + return ( + self.caller_filename or "", + self.caller_firstlineno or 0, + self.caller_func_name, + ) + return None + + @property + def func_key(self) -> tuple[str, int, str]: + """Return this function as a pstats-compatible key tuple.""" + return (self.filename, self.firstlineno, self.func_name) + + def __repr__(self) -> str: + return ( + f"FunctionTraceTiming({self.func_name} at {self.filename}:{self.firstlineno}, " + f"cumtime={self.cumtime_ms:.2f}ms, tottime={self.tottime_ms:.2f}ms, " + f"bytecode={self.bytecode_count}, depth={self.inline_depth})" + ) + + +@dataclass +class ProfilerStackEntry: + """Stack entry for tracking function timing in the Dynamo profiler.""" + + func_name: str + filename: str + firstlineno: int + start_time_ns: int + child_time_ns: int # Accumulated time spent in traced children + is_primitive_call: bool = True # Whether this is a non-recursive call + + +class DynamoProfilerState: + """State for Dynamo profiler tracking function trace timings.""" + + def __init__(self) -> None: + self.timings: list[FunctionTraceTiming] = [] + self.stack: list[ProfilerStackEntry] = [] + + def record_timing(self, timing: FunctionTraceTiming) -> None: + """Record timing data for a traced function.""" + self.timings.append(timing) + + def get_timings(self) -> list[FunctionTraceTiming]: + """Get all recorded timings.""" + return self.timings + + def push( + self, func_name: str, filename: str, firstlineno: int, start_time_ns: int + ) -> None: + """Push a new entry onto the timing stack.""" + # Check if this function already exists in the stack (indirect recursion) + is_primitive = not any( + entry.func_name == func_name + and entry.filename == filename + and entry.firstlineno == firstlineno + for entry in self.stack + ) + self.stack.append( + ProfilerStackEntry( + func_name=func_name, + filename=filename, + firstlineno=firstlineno, + start_time_ns=start_time_ns, + child_time_ns=0, + is_primitive_call=is_primitive, + ) + ) + + def pop(self) -> ProfilerStackEntry | None: + """Pop the top entry from the timing stack.""" + if self.stack: + return self.stack.pop() + return None + + def add_child_time(self, child_cumtime_ns: int) -> None: + """Add the child's cumulative time to the parent's child_time accumulator.""" + if self.stack: + self.stack[-1].child_time_ns += child_cumtime_ns + + def get_current_caller(self) -> tuple[str, str, int] | None: + """Get the current caller (top of stack) as (func_name, filename, firstlineno).""" + if self.stack: + entry = self.stack[-1] + return (entry.func_name, entry.filename, entry.firstlineno) + return None + + def get_call_stack(self) -> tuple[tuple[str, str, int], ...]: + """Get the full current call stack as tuple of (func_name, filename, firstlineno).""" + return tuple( + (entry.func_name, entry.filename, entry.firstlineno) for entry in self.stack + ) + + def generate_pstats( + self, output_file: str | None = None, print_raw: bool = False + ) -> pstats.Stats: + """Generate pstats.Stats object from recorded timings. + + Args: + output_file: Optional file path to save the stats. + print_raw: If True, print raw aggregate timings before returning. + """ + import cProfile + import io + import logging + import pstats + + log = logging.getLogger(__name__) + + # Aggregate by (filename, lineno, func_name) + aggregated: dict[tuple[str, int, str], dict[str, Any]] = {} + # caller_edges[callee_key][caller_key] -> edge stats + caller_edges: dict[ + tuple[str, int, str], dict[tuple[str, int, str], dict[str, Any]] + ] = {} + + for t in self.timings: + key = (t.filename, t.firstlineno, t.func_name) + + if key not in aggregated: + aggregated[key] = { + "ncalls": 0, + "pcalls": 0, + "tottime": 0.0, + "cumtime": 0.0, + } + caller_edges[key] = {} + + agg = aggregated[key] + agg["ncalls"] += 1 + agg["tottime"] += t.tottime_ns / 1e9 + + if t.is_primitive_call: + agg["pcalls"] += 1 + agg["cumtime"] += t.cumtime_ns / 1e9 + + # Build caller edge + if t.caller_filename is not None: + caller_key = ( + t.caller_filename, + t.caller_firstlineno or 0, + t.caller_func_name or "", + ) + if caller_key not in caller_edges[key]: + caller_edges[key][caller_key] = { + "ncalls": 0, + "pcalls": 0, + "tottime": 0.0, + "cumtime": 0.0, + } + edge = caller_edges[key][caller_key] + edge["ncalls"] += 1 + edge["tottime"] += t.tottime_ns / 1e9 + # Always add cumtime to edges for visualization (gprof2dot) + # Function-level cumtime is already correct (only primitive calls) + edge["cumtime"] += t.cumtime_ns / 1e9 + if t.is_primitive_call: + edge["pcalls"] += 1 + + if print_raw: + sorted_items = sorted( + aggregated.items(), key=lambda x: x[1]["cumtime"], reverse=True + ) + print("\n=== Aggregate Timings (raw) ===") + print( + f"{'ncalls':>8} {'pcalls':>8} {'tottime':>12} {'cumtime':>12} function" + ) + print("-" * 80) + total_cumtime = 0.0 + total_tottime = 0.0 + for (filename, lineno, func_name), agg in sorted_items: + ncalls = agg["ncalls"] + pcalls = agg["pcalls"] + tottime = agg["tottime"] * 1000 # convert to ms + cumtime = agg["cumtime"] * 1000 + total_cumtime += cumtime + total_tottime += tottime + short_file = filename.split("/")[-1] if "/" in filename else filename + print( + f"{ncalls:>8} {pcalls:>8} {tottime:>10.2f}ms {cumtime:>10.2f}ms " + f"{func_name} ({short_file}:{lineno})" + ) + print("-" * 80) + print( + f"Total timings: {len(self.timings)}, unique functions: {len(aggregated)}" + ) + print( + f"Sum tottime: {total_tottime:.2f}ms, Sum cumtime: {total_cumtime:.2f}ms" + ) + + # Ensure caller-only functions have a top-level entry. + # gprof2dot expects every function referenced as a caller to also + # exist as a top-level entry in the stats dict with timing data. + for key in list(caller_edges.keys()): + for caller_key in caller_edges[key]: + if caller_key not in aggregated: + aggregated[caller_key] = { + "ncalls": 0, + "pcalls": 0, + "tottime": 0.0, + "cumtime": 0.0, + } + caller_edges[caller_key] = {} + + # Build the stats dict in pstats format + stats_dict: dict[ + tuple[str, int, str], tuple[int, int, float, float, dict[Any, Any]] + ] = {} + + for key, agg in aggregated.items(): + callers: dict[tuple[str, int, str], tuple[int, int, float, float]] = {} + for caller_key, edge in caller_edges[key].items(): + callers[caller_key] = ( + edge["ncalls"], + edge["pcalls"], + edge["tottime"], + edge["cumtime"], + ) + + stats_dict[key] = ( + agg["pcalls"], + agg["ncalls"], + agg["tottime"], + agg["cumtime"], + callers, + ) + + # Create a pstats.Stats object + dummy_profile = cProfile.Profile() + dummy_profile.enable() + dummy_profile.disable() + stats = pstats.Stats(dummy_profile, stream=io.StringIO()) + + stats.stats = stats_dict # type: ignore[attr-defined] + stats.total_calls = sum(s[1] for s in stats_dict.values()) # type: ignore[attr-defined] + stats.prim_calls = sum(s[0] for s in stats_dict.values()) # type: ignore[attr-defined] + stats.total_tt = sum(s[2] for s in stats_dict.values()) # type: ignore[attr-defined] + + if output_file: + stats.dump_stats(output_file) + log.info( + "Saved pstats to %s. Visualize with: snakeviz %s", + output_file, + output_file, + ) + + return stats + + def generate_svg( + self, profile_file: str, svg_file: str | None = None + ) -> str | None: + """Generate an SVG call graph from a profile file using gprof2dot and graphviz. + + Args: + profile_file: Path to the pstats profile file. + svg_file: Optional path for the output SVG. If not provided, uses + profile_file with .svg extension. + + Returns: + Path to the generated SVG file, or None if generation failed. + """ + import os + import shutil + import subprocess + + if not shutil.which("gprof2dot"): + print("gprof2dot not found. Install with: pip install gprof2dot") + return None + + if not shutil.which("dot"): + print("graphviz 'dot' not found. Install graphviz package.") + return None + + if svg_file is None: + svg_file = profile_file.rsplit(".", 1)[0] + ".svg" + + try: + # gprof2dot -f pstats profile.prof | dot -Tsvg -o profile.svg + gprof2dot = subprocess.Popen( + [ + "gprof2dot", + "-f", + "pstats", + "--node-label=total-time-percentage", + "--node-label=self-time-percentage", + "--node-label=total-time", + profile_file, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + dot = subprocess.Popen( + ["dot", "-Tsvg", "-o", svg_file], + stdin=gprof2dot.stdout, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + gprof2dot.stdout.close() # type: ignore[union-attr] + _, dot_err = dot.communicate() + _, gprof2dot_err = gprof2dot.communicate() + + if gprof2dot.returncode != 0: + print( + f"gprof2dot failed: {gprof2dot_err.decode()}" # noqa: B950 + ) + return None + + if dot.returncode != 0: + print(f"graphviz dot failed: {dot_err.decode()}") + return None + + if not os.path.isfile(svg_file): + print(f"SVG file was not created: {svg_file}") + return None + + print(f"SVG call graph saved to: {svg_file}") + return svg_file + + except Exception as e: + print(f"Failed to generate SVG: {e}") + return None + + def dump_stats( + self, output_file: str | None = None, generate_svg: bool = True + ) -> None: + """Print profiler stats to stdout and optionally save to file. + + Args: + output_file: Optional path to save the pstats profile. + generate_svg: If True and output_file is provided, also generate an SVG + call graph using gprof2dot and graphviz. + """ + import sys + + if not self.timings: + return + + stats = self.generate_pstats(output_file, print_raw=True) + print("\n=== Dynamo Profiler (pstats) ===") + stats.stream = sys.stdout # type: ignore[attr-defined] + stats.sort_stats("cumulative").print_stats() + + if output_file: + print(f"\nProfile saved to: {output_file}") + print(f"Visualize with: snakeviz {output_file}") + + if generate_svg: + self.generate_svg(output_file) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/eval_frame.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/eval_frame.py new file mode 100644 index 0000000000000000000000000000000000000000..bcc300466dd893370806be71270d503bc742bacf --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/eval_frame.py @@ -0,0 +1,2629 @@ +# mypy: disable-error-code="method-assign" + +""" +This module implements the core frame evaluation handler for TorchDynamo's compilation system. +The eval frame handler intercepts Python bytecode execution at runtime to enable dynamic +compilation and optimization of PyTorch code. + +Key components defined here: +- Frame evaluation handlers that intercept and analyze Python execution frames +- Guards management for tracking dependencies and invalidating compiled code +- Optimization contexts and decorators (optimize, run_once, disable, etc.) +- Export functionality for saving optimized graphs +- Backend compiler integrations and callback management + +Functions in this file are responsible for modifying the eval frame handler at RUNTIME. +Therefore, all functions in this file are hot and performance-critical. Functions that +only execute at compile time should be placed in torch._dynamo.convert_frame. + +The eval frame handler is the core mechanism that enables TorchDynamo to dynamically +intercept, analyze and optimize PyTorch code during execution. It works by registering +a custom frame evaluation function that gets called for every Python frame, allowing +us to detect PyTorch operations and trigger compilation as needed. +""" + +from __future__ import annotations + +import atexit +import contextlib +import functools +import inspect +import logging +import os +import sys +import sysconfig +import textwrap +import threading +import traceback +import types +import unittest +import warnings +import weakref +from collections.abc import Generator, Sized +from dataclasses import dataclass +from enum import Enum +from os.path import dirname, join +from typing import Any, NamedTuple, TYPE_CHECKING +from unittest.mock import patch + +import sympy + +import torch +import torch.fx +import torch.utils._pytree as pytree +import torch.utils.checkpoint +from torch import _guards + +# see discussion at https://github.com/pytorch/pytorch/issues/120699 +from torch._C._dynamo.eval_frame import ( # noqa: F401 + reset_code, + set_code_exec_strategy, + set_eval_frame, + set_fullgraph_compiled_frame_count, + set_fullgraph_error_on_nested_compile, + set_guard_complete_hook, + set_guard_error_hook, + set_skip_guard_eval_unsafe, + unsupported, +) +from torch._dispatch.python import enable_python_dispatcher +from torch._dynamo.types import ConvertFrameReturn, FrameAction, FrameExecStrategy +from torch._export.utils import _compiling_state_context +from torch._library.opaque_object import is_opaque_type +from torch._subclasses.fake_tensor import unset_fake_temporarily +from torch._utils_internal import DISABLE_JUSTKNOBS, justknobs_check, log_export_usage +from torch.export.dynamic_shapes import ( + _combine_args, + _DimHint, + _DimHintType, + _IntWrapper, + _process_dynamic_shapes, + _RelaxedConstraint, + Constraint, +) +from torch.fx import GraphModule, traceback as fx_traceback +from torch.fx.experimental._dynamism import ( + clone_and_convert_to_meta, + track_dynamism_across_examples, +) +from torch.fx.experimental.proxy_tensor import make_fx +from torch.fx.experimental.symbolic_shapes import ( + ConstraintViolationError, + DimDynamic, + ShapeEnv, + StatelessSymbolicContext, +) +from torch.fx.graph import _PyTreeCodeGen, _PyTreeInfo + +from . import config, convert_frame, distributed, external_utils, trace_rules, utils +from .backends.registry import CompilerFn, lookup_backend +from .code_context import code_context +from .exc import ( + CondOpArgsMismatchError, + ShortenTraceback, + UncapturedHigherOrderOpError, + Unsupported, + UserError, + UserErrorType, +) +from .hooks import Hooks +from .mutation_guard import install_generation_tagging_init +from .utils import ( + _get_error_on_graph_break, + _set_error_on_graph_break, + common_constant_types, + compile_times, +) + + +if TYPE_CHECKING: + from collections.abc import Callable, Iterable, Sequence + + from torch._dynamo.package import CompilePackage + from torch._dynamo.repro.after_dynamo import WrapBackendDebug + from torch._subclasses import fake_tensor + from torch.fx.node import Argument, Node, Target + + from .types import ( + CacheEntry, + DynamoCallback, + DynamoFrameType, + GuardFail, + GuardFilterEntry, + ) + + +log = logging.getLogger(__name__) + + +always_optimize_code_objects = utils.ExactWeakKeyDictionary() +null_context = contextlib.nullcontext + + +# See https://github.com/python/typing/pull/240 +class Unset(Enum): + token = 0 + + +cached_backends: dict[int, CompilerFn] = {} + +unset = Unset.token + +_in_optimized_module = False + + +if DISABLE_JUSTKNOBS: + _maybe_set_eval_frame = set_eval_frame +else: + + def _maybe_set_eval_frame(callback: DynamoCallback) -> DynamoCallback: + # A wrapper on set_eval_frame that is guarded by a Justknob. + # Users can disable torchDynamo by setting the JK to False. + if not justknobs_check("pytorch/compiler:enable_compiler_set_eval_frame"): + torch._dynamo.utils.warn_once( + "Dynamo disabled by Justknob: enable_compiler_set_eval_frame, skipping set_eval_frame" + ) + return callback + else: + return set_eval_frame(callback) + + +@dataclass +class DynamoStance: + stance: str = "default" + skip_guard_eval_unsafe: bool = False + backend: str | Callable[..., Any] | None = None + + +_stance = DynamoStance() + + +def _set_stance(stance: DynamoStance) -> DynamoStance: + global _stance + + from torch._C._dynamo.eval_frame import get_eval_frame_callback + + callback = get_eval_frame_callback() + + if callback is not False and callback is not None: + raise RuntimeError("attempted to set_stance in a torch.compile region") + + prior = _stance + _stance = stance + return prior + + +_set_stance._dynamo_forbidden = True # type: ignore[attr-defined] + +_EXAMPLE_INPUTS: dict[str, list[Any]] | None = None + + +def get_example_inputs(key: str) -> list[Any]: + global _EXAMPLE_INPUTS + if _EXAMPLE_INPUTS is None: + _EXAMPLE_INPUTS = {} + + if key not in _EXAMPLE_INPUTS: + _EXAMPLE_INPUTS[key] = [] + + return _EXAMPLE_INPUTS[key] + + +@contextlib.contextmanager +def _set_in_optimized_module() -> Generator[None, None, None]: + # Set in dynamo's OptimizedModule forward, to have better coverage than is_compiling(). + # Prevents graph-breaking forward hooks from being registered & traced. + # TODO(pianpwk): subsume this flag with better is_compiling() coverage + global _in_optimized_module + _old_in_optimized_module = ( + _in_optimized_module # do we need this? can we just set it to False after + ) + _in_optimized_module = True + try: + yield + finally: + _in_optimized_module = _old_in_optimized_module + + +def _is_in_optimized_module() -> bool: + global _in_optimized_module + return _in_optimized_module + + +def _callback_from_stance(callback: DynamoCallback) -> DynamoCallback: + if _stance.stance == "default": + # force_backend + if _stance.backend is not None and callback not in (False, None): + callback = _create_wrapped_callback(get_compiler_fn(_stance.backend)) + + return callback + elif _stance.stance == "eager_then_compile": + if callback not in (False, None): + return _create_delayed_compile_callback(callback, _stance.stance) + return callback + elif _stance.stance == "aot_eager_then_compile": + if callback not in (False, None): + return _create_delayed_compile_callback(callback, _stance.stance) + return callback + elif _stance.stance == "force_eager": + # disable + return None + elif _stance.stance == "eager_on_recompile": + # run mode + return False + elif _stance.stance == "fail_on_recompile": + if callback in (False, None): + return callback + + def fail_callback( + frame: DynamoFrameType, *args: Any, **kwargs: Any + ) -> ConvertFrameReturn: + if trace_rules.check(frame.f_code): + return ConvertFrameReturn() + if not convert_frame.has_tensor_in_frame(frame): + return ConvertFrameReturn() + + from torch._C._dynamo.eval_frame import ( + _debug_get_cache_entry_list, + _debug_get_precompile_entries, + ) + from torch._dynamo.guards import get_and_maybe_log_recompilation_reasons + + message = ( + "Detected recompile when torch.compile stance is 'fail_on_recompile'. " + + f"filename: '{frame.f_code.co_filename}', " + + f"function name: '{frame.f_code.co_name}', " + + f"line number: {frame.f_lineno}" + ) + cache_entries = _debug_get_cache_entry_list(frame.f_code) + if cache_entries: + reasons = get_and_maybe_log_recompilation_reasons( + # pyrefly: ignore [bad-argument-type] + cache_entries[0], + frame, + # pyrefly: ignore [bad-argument-type] + innermost_fn(callback), + skip_logging=True, + ) + if reasons: + failures = textwrap.indent("\n".join(reasons), "- ") + guard_failure_details = ( + f"triggered by the following guard failure(s):\n{failures}" + ) + message += f"\n{textwrap.indent(guard_failure_details, ' ')}" + precompile_entries = _debug_get_precompile_entries(frame.f_code) + if len(precompile_entries) > 0: + message += "\nFailed on the following precompiled guards: " + for entry in precompile_entries: + message += f"\n{entry.guard_manager}{entry.guard_manager.check_verbose(frame.f_locals)}" # type: ignore[attr-defined] + raise RuntimeError(message) + + # to prevent cache miss due to different backend + fail_callback._torchdynamo_orig_backend = callback # type: ignore[attr-defined] + + return fail_callback + else: + raise RuntimeError(f"invalid torch.compile stance '{_stance}'") + + +def _create_wrapped_callback( + compiler_fn: CompilerFn, +) -> convert_frame.CatchErrorsWrapper: + hooks = Hooks() + return convert_frame.catch_errors_wrapper( + convert_frame.convert_frame( # type: ignore[arg-type] + compiler_fn, + hooks, + ), + hooks, + ) + + +def _get_or_add_example_inputs(frame: DynamoFrameType) -> list[Any]: + key = frame.f_code.co_filename + str(frame.f_code.co_firstlineno) + example_inputs = get_example_inputs(key) + + if len(example_inputs) < 2: + example_inputs.append(clone_and_convert_to_meta(frame.f_locals)) + + return example_inputs + + +def _create_delayed_compile_callback( + callback: DynamoCallback, stance: str +) -> Callable[..., Any]: + def callback_fn(*args: Any, **kwargs: Any) -> convert_frame.ConvertFrameReturn: + frame = args[0] + example_inputs = _get_or_add_example_inputs(frame) + + if len(example_inputs) == 1: + if stance == "eager_then_compile": + return ConvertFrameReturn( + frame_exec_strategy=FrameExecStrategy( + FrameAction.DEFAULT, FrameAction.DEFAULT + ) + ) + elif stance == "aot_eager_then_compile": + aot_eager_fn = get_compiler_fn("aot_eager") + return _create_wrapped_callback(aot_eager_fn)(*args, **kwargs) + + dynamism = track_dynamism_across_examples(example_inputs) + code_context.get_context(frame.f_code)["dynamism"] = dynamism + compiler_fn = callback._torchdynamo_orig_backend._torchdynamo_orig_backend # type: ignore[union-attr] + return _create_wrapped_callback(compiler_fn)(*args, **kwargs) + + # to prevent cache miss due to different backend + callback_fn._torchdynamo_orig_backend = callback # type: ignore[attr-defined] + + return callback_fn + + +def _is_skip_guard_eval_unsafe_stance() -> bool: + return _stance.skip_guard_eval_unsafe + + +def _reset_guarded_backend_cache() -> None: + global cached_backends + for backend in cached_backends.values(): + if hasattr(backend, "reset"): + backend.reset() + cached_backends.clear() + + +DONT_WRAP_FILES = { + # For tracing into fx modules + inspect.getsourcefile(GraphModule), + join(dirname(dirname(__file__)), "onnx/_internal/fx/dynamo_graph_extractor.py"), +} + + +def _debug_get_cache_entry_list( + code: types.CodeType | Callable[..., Any], +) -> list[CacheEntry]: + """ + Given a code object or a callable object, retrieve the cache entries + stored in this code. + """ + if callable(code): + code = code.__code__ + return torch._C._dynamo.eval_frame._debug_get_cache_entry_list(code) + + +class OptimizedModule(torch.nn.Module): + """ + Wraps the original nn.Module object and later patches its + forward method to optimized self.forward method. + """ + + _torchdynamo_orig_callable: Callable[..., Any] + get_compiler_config: Callable[[], Any] + + _opt_mod_attributes = { + "_orig_mod", + "dynamo_ctx", + "_torchdynamo_orig_callable", + "_torchdynamo_wrapper_id", + "get_compiler_config", + "forward", + "_forward", + "__dict__", + "named_children_walk", + "_super_module_initialized", + } + + def __init__(self, mod: torch.nn.Module, dynamo_ctx: _TorchDynamoContext) -> None: + # NOTE: this must go first, because attribute reads/writes of `self` + # uses `_orig_mod`, and sometimes users override `Module.__init__` to + # do attribute reads/writes on `self`. + # + # We also can't use regular setattr because `super().__setattr__` will + # complain for module value before `super().__init__()` + object.__setattr__(self, "_orig_mod", mod) + self._super_module_initialized = False + super().__init__() + self._super_module_initialized = True + + # Installs the params/buffer + self._orig_mod = mod # `super().__setattr__` will register this module + self.dynamo_ctx = dynamo_ctx + self._initialize() + self.training = self._orig_mod.training + + def __len__(self) -> int: + # Proxy the len call to the original module + # pyrefly: ignore [unsafe-overlap] + if isinstance(self._orig_mod, Sized): + return len(self._orig_mod) + # Mimic python's default behavior for objects without a length + raise TypeError(f"{type(self._orig_mod).__name__} does not support len()") + + def _initialize(self) -> None: + # Do this stuff in constructor to lower overhead slightly + if isinstance(self.dynamo_ctx, DisableContext): + # No need to check trace rules + self.forward = self.dynamo_ctx(self._orig_mod.__call__) + elif config.wrap_top_frame or ( + isinstance(self._orig_mod.forward, types.MethodType) + and (trace_rules.check(self._orig_mod.forward)) + ): + # This may be a torch.nn.* instance in trace_rules.py which + # won't trigger a frame evaluation workaround to add an extra + # frame we can capture + self.forward = self.dynamo_ctx(external_utils.wrap_inline(self._orig_mod)) + else: + # Invoke hooks outside of dynamo then pickup the inner frame + self.forward = self.dynamo_ctx(self._orig_mod.__call__) + + if hasattr(self._orig_mod, "_initialize_hook"): + self._forward = self.forward + self.forward = self._call_lazy_check + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + if torch.nn.modules.module._has_any_global_hook(): + warnings.warn( + "Using `torch.compile(module)` when there are global hooks on " + "modules (e.g., from `register_module_forward_hook`); this will" + " cause the hooks to fire an extra time for the " + "`OptimizedModule` created by `torch.compile(module)`. If this " + "causes undesired behavior, please try using `module.compile()`" + ", or use the per-module hooks instead", + stacklevel=2, + ) + with _set_in_optimized_module(): + return super().__call__(*args, **kwargs) + + def _aot_compile(self, inputs: list[torch._dynamo.aot_compile.ModelInput]) -> None: + """ + Experimental: AOT Compile a set of inputs and use that as the forward function + """ + model = self._orig_mod + hooks = self.dynamo_ctx._hooks + assert hooks is not None + if not config.enable_aot_compile: + raise RuntimeError( + "AOT Compile is not enabled, please set torch._dynamo.config.enable_aot_compile=True" + ) + if not self.dynamo_ctx.fullgraph: + raise RuntimeError( + "Graph breaks are not supported with aot compile. Please use torch.compile(fullgraph=True)." + ) + + if not callable(self.dynamo_ctx.callback): + raise RuntimeError("aot compile requires a callable dynamo callback.") + + backend = innermost_backend(self.dynamo_ctx.callback) + from torch._dynamo.aot_compile import aot_compile_module + + self.forward = aot_compile_module(model, inputs, hooks, backend) + + def _save_aot_compiled_module(self, path: str | None = None) -> bytes: + if not config.enable_aot_compile: + raise RuntimeError( + "AOT Compile is not enabled, please set torch._dynamo.config.enable_aot_compile=True" + ) + from torch._dynamo.aot_compile import AOTCompiledModel + + assert isinstance(self.forward, AOTCompiledModel) + result: bytes = self.forward.serialize() + if path is not None: + with open(path, "wb") as f: + f.write(result) + return result + + def _load_aot_compiled_module(self, data: bytes) -> None: + if not config.enable_aot_compile: + raise RuntimeError( + "AOT Compile is not enabled, please set torch._dynamo.config.enable_aot_compile=True" + ) + from torch._dynamo.aot_compile import AOTCompiledModel + + compiled_forward = AOTCompiledModel.deserialize(self._orig_mod, data) + assert isinstance(compiled_forward, AOTCompiledModel) + self.forward = compiled_forward + + def __reduce__( + self, + ) -> tuple[type[OptimizedModule], tuple[torch.nn.Module, _TorchDynamoContext]]: + return (self.__class__, (self._orig_mod, self.dynamo_ctx)) + + def __getstate__(self) -> dict[str, Any]: + state = dict(self.__dict__) + state.pop("forward", None) + state.pop("__call__", None) + return state + + def __setstate__(self, state: dict[str, Any]) -> None: + self.__dict__ = state + self._initialize() + + @property + # pyrefly: ignore [bad-override] + def training(self) -> bool: + return self._orig_mod.training + + @training.setter + def training(self, value: bool) -> None: + # Ignore the `training` mutation in `super().__init__()`, since that's + # setting the default on `nn.Module`, but we are mirroring the + # `training` attr in `self._orig_mod`. + if self._super_module_initialized: + self._orig_mod.training = value + + def __getattr__(self, name: str) -> Any: + if name == "_orig_mod": + return self._modules["_orig_mod"] + return getattr(self._orig_mod, name) + + def __setattr__(self, name: str, value: Any) -> None: + # Allow patching over class attributes + if hasattr(type(self), name): + return super().__setattr__(name, value) + + if name in OptimizedModule._opt_mod_attributes: + return super().__setattr__(name, value) + return setattr(self._orig_mod, name, value) + + def __delattr__(self, name: str) -> None: + # This mirrors `__setattr__` + if hasattr(type(self), name): + return super().__delattr__(name) + + if name in OptimizedModule._opt_mod_attributes: + return super().__delattr__(name) + return delattr(self._orig_mod, name) + + def _call_lazy_check(self, *args: Any, **kwargs: Any) -> Any: + if ( + hasattr(self._orig_mod, "_initialize_hook") + and hasattr(self._orig_mod, "_infer_parameters") + and callable(self._orig_mod._infer_parameters) + ): + # In the case of a lazy module, we want to run + # the pre-hooks which initialize it. + # Afterwards, lazy module deletes its pre-hooks + # to avoid treating it as lazy on subsequent recompile. + self._orig_mod._infer_parameters(self._orig_mod, args, kwargs) + return self._forward(*args, **kwargs) + + def __dir__(self) -> list[str]: + orig_mod_attrs = self._orig_mod.__dir__() + return orig_mod_attrs + [ + attr for attr in super().__dir__() if attr not in orig_mod_attrs + ] + + +def remove_from_cache(f: Any) -> None: + """ + Make sure f.__code__ is not cached to force a recompile + """ + if isinstance(f, types.CodeType): + reset_code(f) + elif hasattr(f, "__code__"): + reset_code(f.__code__) + elif hasattr(getattr(f, "forward", None), "__code__"): + reset_code(f.forward.__code__) + else: + from . import reset # type: ignore[attr-defined] + + reset() + log.warning("could not determine __code__ for %s", f) + + +def nothing() -> None: + pass + + +def always_false() -> bool: + return False + + +def innermost_fn(fn: Callable[..., Any]) -> Callable[..., Any]: + """ + In case of nesting of _TorchDynamoContext calls, find the innermost + function. TorchDynamo caches on fn.__code__ object, so its necessary to find + the innermost function to pass on the optimize, run, disable etc. + """ + unaltered_fn = fn + while ( + hasattr(unaltered_fn, "_torchdynamo_orig_callable") + # Only follow the chain if _torchdynamo_wrapper_id matches id(fn). + # This prevents following chains in two cases: + # 1. Bound methods: id(bound_method) != id(wrapper_function), so we + # won't unwrap through __func__ and lose the self binding. + # 2. functools.wraps copies: When functools.wraps copies + # _torchdynamo_orig_callable from a wrapped function, the copied + # _torchdynamo_wrapper_id won't match the outer wrapper's id. + and getattr(unaltered_fn, "_torchdynamo_wrapper_id", None) == id(unaltered_fn) + ): + unaltered_fn = unaltered_fn._torchdynamo_orig_callable + assert callable(unaltered_fn), ( + f"A callable function is expected, but {type(unaltered_fn)} is provided." + ) + return unaltered_fn + + +def innermost_backend(fn: Callable[..., Any]) -> Callable[..., Any]: + """ + Unwrap backend wrapper chain via _torchdynamo_orig_backend to find the + innermost backend function. + """ + while hasattr(fn, "_torchdynamo_orig_backend"): + fn = fn._torchdynamo_orig_backend + assert callable(fn), ( + f"A callable function is expected, but {type(fn)} is provided." + ) + return fn + + +def make_set_enable_dynamic(enable: bool) -> Any: + assert isinstance(enable, bool) + if enable: + # Assume everything is dynamic by default + return config._make_closure_patcher(assume_static_by_default=False) + else: + return config._make_closure_patcher( + automatic_dynamic_shapes=False, assume_static_by_default=True + ) + + +@contextlib.contextmanager +def set_enable_dynamic(enable: bool) -> Generator[None, None, None]: + cleanup = make_set_enable_dynamic(enable)() + try: + yield + finally: + cleanup() + + +# A thread local storage that serves to store information as Dynamo traces +# through a user provided function. +class DynamoTLS(threading.local): + # Each string is a summary of a frame Dynamo attempted to trace, stored in + # temporal order. + traced_frame_infos: list[str] = [] + + +dynamo_tls = DynamoTLS() + + +def clear_dynamo_tls() -> None: + dynamo_tls.traced_frame_infos.clear() + + +@atexit.register +def _log_traced_frames() -> None: + """ + At program exit, log all of the frames Dynamo has attempted to trace from, + excluding the continuation frames generated by Dynamo. + """ + msg = "\n".join(dynamo_tls.traced_frame_infos) + msg = textwrap.indent(msg, " * ") + msg = f"TorchDynamo attempted to trace the following frames: [\n{msg}\n]" + log.info(msg) + + +def guard_collectives_hook(guard_eval_result: bool) -> bool: + import torch.distributed as dist + from torch._dynamo.utils import dynamo_timed + + # guard_eval_result == True ==> cache hit + if pg := distributed.get_guard_pg(): + with dynamo_timed( + "guard_collective", log_pt2_compile_event=False, log_waitcounter=True + ): + log.debug("guard_collective %s", guard_eval_result) + # TODO: a bit awkward to time, this isn't inside of the dynamo compile region + all_results = [None] * pg.size() + dist.all_gather_object(all_results, guard_eval_result, group=pg) + # True = everyone hit, OK to run + # False = someone missed, force recompile everywhere + res = all(all_results) + log.debug("guard_collective %s -> %s", guard_eval_result, res) + return res + return guard_eval_result + + +_not_set = object() + + +class _TorchDynamoContext: + def __init__( + self, + callback: DynamoCallback, + on_enter: Callable[[], Any] = nothing, + backend_ctx_ctor: Callable[ + [], contextlib.AbstractContextManager[Any] + ] = null_context, + patch_fn: Callable[[], Any] = nothing, + first_ctx: bool = False, + *, + fullgraph: bool = False, + error_on_graph_break: bool | None = None, + export: bool = False, + dynamic: bool | None = None, + compiler_config: Any | None = None, + package: CompilePackage | None = None, + hooks: Hooks | None = None, + ) -> None: + super().__init__() + assert callable(callback) or callback is False or callback is None + self.callback: DynamoCallback = callback + self._backend_ctx_ctor = backend_ctx_ctor + self.prior: Unset | DynamoCallback = unset + self.first_ctx = first_ctx + self.fullgraph = fullgraph + self.error_on_graph_break = error_on_graph_break + self.export = export + self._dynamic = dynamic + self.compiler_config = compiler_config + self.cleanup_fns: list[Callable[[], Any]] = [] + self.enter_exit_hooks = [] + self._package = package + self._hooks = hooks + patch_fn() + + # Save the backends so that we can reset them during torch._dynamo.reset + backend = innermost_backend(callback) # type: ignore[arg-type] + cached_backends.setdefault(id(backend), backend) # type: ignore[arg-type] + + if dynamic is not None: + self.enter_exit_hooks.append(make_set_enable_dynamic(dynamic)) + + if on_enter is not nothing: + # this case is not common + def call_on_enter() -> Callable[[], None]: + on_enter() + return nothing + + self.enter_exit_hooks.append(call_on_enter) + + if backend_ctx_ctor is not contextlib.nullcontext: + # this case is not common + def call_backend_ctx() -> functools.partial[bool | None]: + ctx = backend_ctx_ctor() + ctx.__enter__() + return functools.partial(ctx.__exit__, None, None, None) + + self.enter_exit_hooks.append(call_backend_ctx) + + def __enter__(self) -> None: + if config.raise_on_ctx_manager_usage: + raise RuntimeError( + "torch._dynamo.optimize(...) is used with a context manager. " + "Please refer to https://pytorch.org/tutorials/intermediate/torch_compile_tutorial.html " + "to use torch._dynamo.optimize(...) as an annotation/decorator. " + ) + self.prior = set_eval_frame(None) + self.cleanup_fns = [enter() for enter in self.enter_exit_hooks] + self.prior_skip_guard_eval_unsafe = set_skip_guard_eval_unsafe( + _is_skip_guard_eval_unsafe_stance() + ) + _maybe_set_eval_frame(_callback_from_stance(self.callback)) + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: types.TracebackType | None, + ) -> bool | None: + assert self.prior is not unset + set_eval_frame(None) + set_skip_guard_eval_unsafe(self.prior_skip_guard_eval_unsafe) + for cleanup in self.cleanup_fns: + cleanup() + self.cleanup_fns.clear() + _maybe_set_eval_frame(_callback_from_stance(self.prior)) + self.prior = unset + return None + + def __call__(self, fn: Any) -> Any: + # public api for compiler config/options + def get_compiler_config() -> Any: + return self.compiler_config + + from .package import DynamoCache + + # If self._package is lazily initialized, we should check the dynamo cache now + if config.caching_precompile: + if self._package is not None and not self._package.is_initialized(): + fn_key = fn.forward if isinstance(fn, torch.nn.Module) else fn + result = DynamoCache.load(fn_key) + if result is None: + # Create a fresh CompilePackage + self._package.initialize(fn_key, None, ignore_inlined_sources=False) + else: + try: + self._package.initialize( + fn_key, result.dynamo, ignore_inlined_sources=False + ) + self._package.install(result.backends) + except RuntimeError: + log.warning( + "Failed to load entry from dynamo cache", exc_info=True + ) + self._package.initialize( + fn_key, None, ignore_inlined_sources=False + ) + + fn = innermost_fn(fn) + + def aot_compile(example_inputs: tuple[tuple[Any, ...], dict[str, Any]]) -> Any: + from torch._dynamo.aot_compile import aot_compile_fullgraph + + if torch._inductor.config.force_disable_caches: + raise RuntimeError( + "Cannot precompile with torch._inductor.config.force_disable_caches=True; caching is required." + ) + + if not self.fullgraph: + raise RuntimeError( + "Graph breaks are not supported with aot compile. Please use torch.compile(fullgraph=True)." + ) + + if not callable(self.callback): + raise RuntimeError("aot compile requires a callable dynamo callback.") + + assert self._hooks is not None + + return aot_compile_fullgraph( + fn, + example_inputs, + hooks=self._hooks, + backend=innermost_backend(self.callback), + dynamic=self._dynamic, + ) + + # add context containing GraphModule to any GraphModule forward functions + if isinstance(fn, GraphModule): + # add context containing GraphModule to any GraphModule forward functions + code_context.get_context(fn.forward.__code__)["orig_graphmodule"] = ( + weakref.ref(fn) + ) + + # Optimize the forward method of torch.nn.Module object + if isinstance(fn, torch.nn.Module): + if type(fn) is torch.jit._script.RecursiveScriptModule: + raise RuntimeError( + "torch.compile does not support compiling torch.jit.script or " + "torch.jit.freeze models directly.\n\n" + "Workaround: compile the original eager module instead:\n" + " model = torch.nn.Linear(3, 3)\n" + " compiled_model = torch.compile(model) # compile the eager module\n\n" + "torch.jit.script and torch.jit.freeze are deprecated in favor of " + "torch.compile. See https://pytorch.org/docs/main/jit.html for details." + ) + mod = fn + new_mod = OptimizedModule(mod, self) + # Save the function pointer to find the original callable while nesting + # of decorators. + new_mod._torchdynamo_orig_callable = mod.forward + new_mod._torchdynamo_wrapper_id = id(new_mod) + + # when compiling torch.nn.Module, + # provide public api OptimizedModule.get_compiler_config() + assert not hasattr(new_mod, "get_compiler_config") + new_mod.get_compiler_config = get_compiler_config + + return new_mod + + if inspect.isclass(fn): + # User has wrapped the class with compile/disable decorator. Apply + # disable to init/call method. + cls_obj = fn + cls_obj.__call__ = self(cls_obj.__call__) + if issubclass(cls_obj, torch.nn.Module): + # NN module variable tracker directly inlines the _call_impl. + cls_obj._call_impl = self(cls_obj._call_impl) + return cls_obj + + assert callable(fn), ( + f"A callable function is expected, but {type(fn)} is provided." + ) + + # NOTE [Top-level TorchInGraph and polyfilled functions] + # Some callables (e.g. torch.exp) are represented as TorchInGraphFunctionVariable + # when traced inside a frame. When such a function is passed directly to + # torch.compile, we detect it here so we can force it through wrap_inline. + # Similarly, functions registered via substitute_in_graph have a polyfill + # that Dynamo can trace, so they also need wrap_inline. + from .variables import TorchInGraphFunctionVariable + + rule = trace_rules.lookup(fn) + top_level_in_graph = isinstance(rule, type) and issubclass( + rule, TorchInGraphFunctionVariable + ) + has_polyfill = trace_rules.is_polyfilled_callable(fn) + + try: + filename = inspect.getsourcefile(fn) + except TypeError: + filename = None + if config.debug_force_nested_calls: + fn = external_utils.wrap_inline(fn) + elif config.wrap_top_frame or ( + ( + filename is None + or trace_rules.check(fn) + or top_level_in_graph + or has_polyfill + ) + and ( + getattr(fn, "__name__", "") + not in ["_call_impl", "_wrapped_call_impl", "_lazy_forward"] + ) + and filename not in DONT_WRAP_FILES + ): + # call to a builtin without a frame for us to capture + fn = external_utils.wrap_inline(fn) + + def do_nothing(*arg: Any, **kwargs: Any) -> None: + pass + + callback: Callable[..., Any] = do_nothing + if hasattr(self, "callback"): + callback = self.callback # type: ignore[assignment] + + is_jit_tracing = torch._C._is_tracing + is_fx_symbolic_tracing = torch.fx._symbolic_trace.is_fx_symbolic_tracing + + @functools.wraps(fn) + def compile_wrapper(*args: Any, **kwargs: Any) -> Any: + # NB: function calls here could change global state (e.g. random state) + # and that can result in different behavior between eager and compiled! + # In particular, we don't have control over internal functions like justknobs_check + # called in _maybe_set_eval_frame. + # Unlike in eval_frame_cpp.cpp/convert_frame.py, we don't attempt to restore global state + # due to additional overhead costs. + prior = set_eval_frame(None) + prior_error_on_nested_compile: bool | None = None + fullgraph_count_enabled = False + if self.fullgraph: + prior_error_on_nested_compile = set_fullgraph_error_on_nested_compile( + torch._dynamo.config.error_on_dynamo_callback_in_fullgraph_compiled_code + ) + if not self.export: + fullgraph_count_enabled = set_fullgraph_compiled_frame_count(0) < 0 + try: + # We shouldn't compile inside kernel invocation. + if tracing_context := torch._guards.TracingContext.try_get(): + if ( + tracing_context.fake_mode is not None + and tracing_context.fake_mode.in_kernel_invocation + ): + return fn(*args, **kwargs) + # Skip nested compile during export (but not HOP internal compile) + # Only skip if there's an active TracingContext (nested), not for top-level export + if ( + torch.compiler.is_exporting() + and not config.force_compile_during_fx_trace + ): + from torch._higher_order_ops.utils import _in_hop_compile + + if not _in_hop_compile(): + if torch._guards.TracingContext.try_get() is not None: + return fn(*args, **kwargs) + # Skip nested compile - just inline the function + if ( + is_fx_symbolic_tracing() + and not config.force_compile_during_fx_trace + ): + if config.error_on_nested_fx_trace: + raise RuntimeError( + "Detected that you are using FX to symbolically trace " + "a dynamo-optimized function. This is not supported at the moment." + ) + else: + return fn(*args, **kwargs) + + if is_jit_tracing(): + raise RuntimeError( + "Detected that you are using FX to torch.jit.trace " + "a dynamo-optimized function. This is not supported at the moment." + ) + + cleanups = [enter() for enter in self.enter_exit_hooks] + prior_skip_guard_eval_unsafe = set_skip_guard_eval_unsafe( + _is_skip_guard_eval_unsafe_stance() + ) + prior_error_on_graph_break = None + if not self.fullgraph and self.error_on_graph_break is not None: + prior_error_on_graph_break = _get_error_on_graph_break() + _set_error_on_graph_break(self.error_on_graph_break) + + # Ensure that if an assertion occurs after graph pushes + # something onto the DynamicLayerStack then we pop it off (the + # constructed graph code isn't guarded with try/finally). + # + # This used to be a context but putting a `with` here is a noticeable + # perf regression (#126293) + saved_dynamic_layer_stack_depth = ( + torch._C._functorch.get_dynamic_layer_stack_depth() + ) + + _maybe_set_eval_frame(_callback_from_stance(callback)) + + call_succeeded = False + try: + result = fn(*args, **kwargs) + call_succeeded = True + except (Unsupported, UncapturedHigherOrderOpError, UserError) as e: + if config.verbose: + raise + # strip internal tracebacks from causes + cur_exn: BaseException = e + while cur_exn.__cause__ is not None: + cur_exn.__cause__.with_traceback(None) + cur_exn = cur_exn.__cause__ + + raise e.with_traceback(None) from e.__cause__ # User compiler error + except ShortenTraceback as e: + # Failures in the backend likely don't have useful + # data in the TorchDynamo frames, so we strip them out. + raise e.remove_dynamo_frames() from None # see TORCHDYNAMO_VERBOSE=1 + finally: + # Restore the dynamic layer stack depth if necessary. + set_eval_frame(None) + if fullgraph_count_enabled and call_succeeded: + count = set_fullgraph_compiled_frame_count(-1) + if count == 0: + torch._dynamo.utils.warn_once( + "torch.compile with fullgraph=True found no compiled frames. " + "The frame was likely skipped (e.g., a non-infra torch dispatch " + "mode was active, dynamo was disabled, or the frame was skipped. " + "This may become an error in the future." + ) + if prior_error_on_graph_break is not None: + _set_error_on_graph_break(prior_error_on_graph_break) + if prior_error_on_nested_compile is not None: + set_fullgraph_error_on_nested_compile( + prior_error_on_nested_compile + ) + torch._C._functorch.pop_dynamic_layer_stack_and_undo_to_depth( + saved_dynamic_layer_stack_depth + ) + + set_skip_guard_eval_unsafe(prior_skip_guard_eval_unsafe) + for cleanup in cleanups: + cleanup() + return result + finally: + if fullgraph_count_enabled: + set_fullgraph_compiled_frame_count(-1) + _maybe_set_eval_frame(prior) + + # hooks to properly handle inlining + if self.error_on_graph_break is not None: + compile_wrapper._torchdynamo_inline = ( # type: ignore[attr-defined] + external_utils.wrap_inline_with_error_on_graph_break( + fn, self.error_on_graph_break + ) + ) + else: + compile_wrapper._torchdynamo_inline = fn # type: ignore[attr-defined] + + # Save the function pointer to find the original callable while nesting + # of decorators. + compile_wrapper._torchdynamo_orig_callable = fn # type: ignore[attr-defined] + compile_wrapper._torchdynamo_wrapper_id = id(compile_wrapper) # type: ignore[attr-defined] + + # when compiling user function instead of nn.Module + # provide public api _fn.get_compiler_config() + assert not hasattr(compile_wrapper, "get_compiler_config") + compile_wrapper.get_compiler_config = get_compiler_config # type: ignore[attr-defined] + if torch._dynamo.config.enable_aot_compile: + compile_wrapper.aot_compile = aot_compile # type: ignore[attr-defined] + + # If the function is called using torch._dynamo.optimize decorator, we + # should prevent any type of skipping. + if callback not in (None, False): + if not hasattr(fn, "__code__"): + raise RuntimeError( + textwrap.dedent( + """ + + torch._dynamo.optimize is called on a non function object. + If this is a callable class, please wrap the relevant code into a function and optimize the + wrapper function. + + >> class CallableClass: + >> def __init__(self) -> None: + >> super().__init__() + >> self.relu = torch.nn.ReLU() + >> + >> def __call__(self, x): + >> return self.relu(torch.sin(x)) + >> + >> def print_hello(self): + >> print("Hello world") + >> + >> mod = CallableClass() + + If you want to optimize the __call__ function and other code, wrap that up in a function + + >> def wrapper_fn(x): + >> y = mod(x) + >> return y.sum() + + and then optimize the wrapper_fn + + >> opt_wrapper_fn = torch._dynamo.optimize(wrapper_fn) + """ + ) + ) + always_optimize_code_objects[fn.__code__] = True + + return compile_wrapper + + +class OptimizeContext(_TorchDynamoContext): + def __init__( + self, + callback: DynamoCallback, + backend_ctx_ctor: Callable[[], contextlib.AbstractContextManager[Any]], + first_ctx: bool = False, + *, + fullgraph: bool = False, + error_on_graph_break: bool | None = None, + export: bool = False, + dynamic: bool | None = None, + compiler_config: Any | None = None, + rebuild_ctx: Callable[[], OptimizeContext | _NullDecorator] | None = None, + package: CompilePackage | None = None, + hooks: Hooks | None = None, + ) -> None: + def on_enter() -> None: + install_generation_tagging_init() + + super().__init__( + callback=callback, + on_enter=on_enter, + backend_ctx_ctor=backend_ctx_ctor, + patch_fn=TorchPatcher.patch, + first_ctx=first_ctx, + fullgraph=fullgraph, + error_on_graph_break=error_on_graph_break, + export=export, + dynamic=dynamic, + compiler_config=compiler_config, + package=package, + hooks=hooks, + ) + + if config.compiled_autograd: + _dynamic = self._dynamic + if _dynamic is None: + _dynamic = not torch._dynamo.config.assume_static_by_default + + def call_compiled_autograd() -> functools.partial[bool | None]: + assert rebuild_ctx is not None + compiler_fn = rebuild_ctx() + ctx = torch._dynamo.compiled_autograd._enable( + compiler_fn, + # pyrefly: ignore [bad-argument-type] + dynamic=_dynamic, + ignore_active_disable_ctx=False, + ) + ctx.__enter__() + return functools.partial(ctx.__exit__, None, None, None) + + self.enter_exit_hooks.append(call_compiled_autograd) + + def __reduce__( + self, + ) -> tuple[type[OptimizeContext], tuple[Any, ...], dict[str, Any]]: + return ( + self.__class__, + (self.callback, self._backend_ctx_ctor, self.first_ctx), + { + "export": self.export, + "dynamic": self._dynamic, + "compiler_config": self.compiler_config, + }, + ) + + +class RunOnlyContext(_TorchDynamoContext): + def __init__(self) -> None: + # cudagraph trees relies on generation increment + def on_enter() -> None: + torch._dynamo.mutation_guard.GenerationTracker.generation += 1 + + super().__init__(callback=False, on_enter=on_enter) + + def __reduce__(self) -> tuple[type[RunOnlyContext], tuple[Any, ...]]: + return (self.__class__, ()) + + +class DisableContext(_TorchDynamoContext): + def __init__(self, msg: str | None = None, wrapping: bool = True) -> None: + super().__init__(callback=None) + self.msg = msg + self.wrapping = wrapping + + def __call__(self, fn: Callable[..., Any]) -> Callable[..., Any]: + # Earlier this code was in the base class _TorchDynamoContext. But we + # moved it here to have better code organization. For disable, we just + # want the callback to be None. We don't have to check trace_rules or + # create any wrapper. + fn = innermost_fn(fn) + + if isinstance(fn, torch.nn.Module): + mod = fn + new_mod = OptimizedModule(mod, self) + new_mod._torchdynamo_orig_callable = mod.forward + new_mod._torchdynamo_wrapper_id = id(new_mod) + return new_mod + + if isinstance(fn, type): + # User has wrapped the class with compile/disable decorator. Apply + # disable to init/call method. + cls_obj = fn + # Disable on init is useful for reconstruction of bytecodes where we + # want to prevent Dynamo from tracing into the init function. Check + # test_reconstruction in test_model_output.py. + cls_obj.__init__ = self(cls_obj.__init__) # type: ignore[misc] + cls_obj.__call__ = self(cls_obj.__call__) + if issubclass(cls_obj, torch.nn.Module): + # NN module variable tracker directly inlines the _call_impl. Disable it. + cls_obj._call_impl = self(cls_obj._call_impl) + return cls_obj + + assert callable(fn), ( + f"A callable function is expected, but {type(fn)} is provided." + ) + + def _fn(*args: Any, **kwargs: Any) -> Any: + prior = set_eval_frame(None) + try: + _maybe_set_eval_frame(_callback_from_stance(self.callback)) + try: + fn_name = getattr(fn, "__name__", type(fn).__name__) + # Skip annotation for __torch_dispatch__ to avoid polluting + # node metadata during export. The disable on __torch_dispatch__ + # is an internal implementation detail, not user-facing. + # TODO: Ideally we shouldn't need this check because nested + # annotate() calls shouldn't override existing keys. + if ( + torch.compiler.is_exporting() + and fn_name != "__torch_dispatch__" + ): + with fx_traceback.annotate( + { + "_torchdynamo_disable": True, + "_torchdynamo_disable_recursive": True, + "_torchdynamo_disable_method": fn_name, + } + ): + return fn(*args, **kwargs) + return fn(*args, **kwargs) + finally: + set_eval_frame(None) + finally: + _maybe_set_eval_frame(prior) + + # Under some circumstances (e.g. precompile) we can end up calling @disable + # decorator in generated bytecode and trigger recompile. This is due to the + # fact that the old callback from torch.compile() is still active and under + # this circumstance we will trigger a failure with set_stance("fail_on_recompile"). + # Therefore we want to skip calling into any frame in this case. + if self.wrapping: + _fn = functools.wraps(fn)(_fn) + + _fn._torchdynamo_disable = True # type: ignore[attr-defined] + _fn._torchdynamo_disable_msg = self.msg # type: ignore[attr-defined] + + # Save the function pointer to find the original callable while nesting + # of decorators. + _fn._torchdynamo_orig_callable = fn # type: ignore[attr-defined] + _fn._torchdynamo_wrapper_id = id(_fn) # type: ignore[attr-defined] + + _fn._torchdynamo_disable_recursive = True # type: ignore[attr-defined] + + return _fn + + def __reduce__(self) -> tuple[type[DisableContext], tuple[Any, ...]]: + return (self.__class__, ()) + + +def _optimize_catch_errors( + compile_fn: convert_frame.ConvertFrameProtocol, + hooks: Hooks, + backend_ctx_ctor: Callable[ + [], contextlib.AbstractContextManager[Any] + ] = null_context, + fullgraph: bool = False, + error_on_graph_break: bool | None = None, + export: bool = False, + dynamic: bool | None = None, + compiler_config: Any | None = None, + rebuild_ctx: Callable[[], OptimizeContext | _NullDecorator] | None = None, + package: CompilePackage | None = None, +) -> OptimizeContext: + return OptimizeContext( + convert_frame.catch_errors_wrapper(compile_fn, hooks), + backend_ctx_ctor=backend_ctx_ctor, + first_ctx=True, + fullgraph=fullgraph, + error_on_graph_break=error_on_graph_break, + export=export, + dynamic=dynamic, + compiler_config=compiler_config, + rebuild_ctx=rebuild_ctx, + package=package, + hooks=hooks, + ) + + +def get_compiler_fn( + compiler_fn: str | Callable[..., Any] | None, +) -> WrapBackendDebug: + from .repro.after_dynamo import wrap_backend_debug + + if compiler_fn is None: + # Special case None to avoid crashing in hasattr + compiler_str = None + elif hasattr(compiler_fn, "compiler_name"): + compiler_str = compiler_fn.compiler_name # type: ignore[union-attr] + assert isinstance(compiler_str, str) + elif isinstance(compiler_fn, str): + compiler_str = compiler_fn + else: + compiler_str = None + compiler_fn = lookup_backend(compiler_fn) # type: ignore[arg-type] + return wrap_backend_debug(compiler_fn, compiler_str) + + +class _NullDecorator(contextlib.nullcontext): # type: ignore[type-arg] + def __call__(self, fn: Callable[..., Any]) -> Callable[..., Any]: + assert callable(fn), ( + f"A callable function is expected, but {type(fn)} is provided." + ) + return fn + + +# Make dynamo graph to have same input/output spec as user code +def argument_names( + f_sig: inspect.Signature, + args: list[Any] | tuple[Any, ...], + kwargs: dict[str, Any], +) -> list[str]: + def signature_to_fullargspec(sig: inspect.Signature) -> inspect.FullArgSpec: + # Get a list of Parameter objects from the Signature object + params = list(sig.parameters.values()) + # Separate positional arguments, keyword-only arguments and varargs/varkw + args = [ + p.name for p in params if p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD + ] + kwonlyargs = [ + p.name for p in params if p.kind == inspect.Parameter.KEYWORD_ONLY + ] + varargs = next( + (p.name for p in params if p.kind == inspect.Parameter.VAR_POSITIONAL), + None, + ) + varkw = next( + (p.name for p in params if p.kind == inspect.Parameter.VAR_KEYWORD), + None, + ) + # Get default values for positional arguments and keyword-only arguments + defaults = tuple( + p.default + for p in params + if p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD + and p.default is not inspect.Parameter.empty + ) + kwonlydefaults = { + p.name: p.default + for p in params + if p.kind == inspect.Parameter.KEYWORD_ONLY + and p.default is not inspect.Parameter.empty + } + # Get annotations for parameters and return value + # pyrefly: ignore [implicit-any] + annotations = {} + if sig.return_annotation: + annotations = {"return": sig.return_annotation} + for parameter in params: + annotations[parameter.name] = parameter.annotation + # Return a FullArgSpec object with the extracted attributes + return inspect.FullArgSpec( + args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations + ) + + fullargspec = signature_to_fullargspec(f_sig) + + # 1. Map `args` 1-to-1 to positional arguments in original signature. + input_strs = fullargspec.args[: len(args)] + + if len(args) > len(fullargspec.args): + # 2. If there are more arguments left in `args`, they map to varargs in original + # signature. Assign names as {varargs}_0, {varargs}_1, ... + assert fullargspec.varargs is not None, "More arguments than expected" + input_strs += [ + f"{fullargspec.varargs}_{i}" for i in range(len(args) - len(input_strs)) + ] + elif len(args) < len(fullargspec.args): + # 3. If there are fewer arguments in `args` than `fullargspec.args`, + # it implies these are arguments either with default values, or provided in + # `kwargs`. The former can be safely ignored. Because Dynamo.export does not + # export them as part of the function signature. The latter will be handled + # in the next step. + for unprovided_arg in fullargspec.args[ + len(args) : -len(fullargspec.defaults or []) + ]: + assert unprovided_arg in kwargs, f"Missing argument {unprovided_arg}" + + # 4. Keyword arguments provided in `kwargs`. + input_strs += list(kwargs.keys()) + + # 5. Keyword-only arguments with default values if not provided are not exported + # as part of the function signature. + for kwonly_arg in fullargspec.kwonlyargs: + kwonlydefaults = fullargspec.kwonlydefaults or {} + assert kwonly_arg in kwargs or kwonly_arg in kwonlydefaults, ( + f"Missing keyword only argument {kwonly_arg}" + ) + + return input_strs + + +def check_if_dynamo_supported() -> None: + if sys.version_info >= (3, 15): + raise RuntimeError("Python 3.15+ not yet supported for torch.compile") + elif sysconfig.get_config_var("Py_GIL_DISABLED") == 1 and sys.version_info < ( + 3, + 13, + 3, + ): + raise RuntimeError( + "torch.compile is not supported on Python < 3.13.3 built with GIL disabled. " + "Please use Python 3.13.3+." + ) + + +def is_dynamo_supported() -> bool: + try: + check_if_dynamo_supported() + return True + except Exception: + return False + + +def check_if_inductor_supported() -> None: + check_if_dynamo_supported() + + +def is_inductor_supported() -> bool: + try: + check_if_inductor_supported() + return True + except Exception: + return False + + +def check_for_incompatible_configs() -> None: + # Some of the configs should be mutually exclusive + assert not (config.suppress_errors and config.fail_on_recompile_limit_hit), ( + "Dynamo configs suppress_error and fail_on_recompile_limit_hit can not both be active at the same time." + ) + + +def optimize(*args: Any, **kwargs: Any) -> OptimizeContext | _NullDecorator: + def rebuild_ctx() -> OptimizeContext | _NullDecorator: + ca_kwargs_override = config.compiled_autograd_kwargs_override + if ca_kwargs_override: + # NOTE: The process of translating other `torch.compile` kwargs to `torch._dynamo.optimize` kwargs + # is more complicated, we will add it in the future when needed. + assert set(ca_kwargs_override.keys()) == {"fullgraph"}, ( + f"Only `fullgraph` kwarg override is supported for now, but got {ca_kwargs_override.keys()}" + ) + kwargs["nopython"] = ca_kwargs_override["fullgraph"] + return optimize(*args, **kwargs) + + return _optimize(rebuild_ctx, *args, **kwargs) + + +def _optimize( + rebuild_ctx: Callable[[], OptimizeContext | _NullDecorator], + backend: str | Callable[..., Any] = "inductor", + *, + nopython: bool = False, + error_on_graph_break: bool | None = None, + guard_export_fn: Callable[[_guards.GuardsSet], None] | None = None, + guard_fail_fn: Callable[[GuardFail], None] | None = None, + guard_filter_fn: Callable[[Sequence[GuardFilterEntry]], Sequence[bool]] + | None = None, + disable: bool = False, + dynamic: bool | None = None, + package: CompilePackage | None = None, + recompile_limit: int | None = None, +) -> OptimizeContext | _NullDecorator: + """ + The main entrypoint of TorchDynamo. Do graph capture and call + backend() to optimize extracted graphs. + + Args: + backend: One of the two things: + - Either, a function/callable taking a torch.fx.GraphModule and + example_inputs and returning a python callable that runs the + graph faster. + One can also provide additional context for the backend, like + torch.jit.fuser("fuser2"), by setting the backend_ctx_ctor attribute. + See AOTAutogradMemoryEfficientFusionWithContext for the usage. + - Or, a string backend name in `torch._dynamo.list_backends()` + nopython: If True, graph breaks will be errors and there will + be a single whole-program graph. + error_on_graph_break: If not None, the current `error_on_graph_break` setting is set to the given value. + See `torch._dynamo.error_on_graph_break()` for more details on what `error_on_graph_break` means. + + Unlike `nopython=True` (i.e. `fullgraph=True`), there is no guarantee of a single whole-program graph. + If `nopython` is True, `error_on_graph_break` does nothing. + disable: If True, turn this decorator into a no-op + dynamic: If True, upfront compile as dynamic a kernel as possible. If False, + disable all dynamic shapes support (always specialize). If None, automatically + detect when sizes vary and generate dynamic kernels upon recompile. + + Example Usage:: + + @torch._dynamo.optimize() + def toy_example(a, b): ... + """ + check_if_dynamo_supported() + check_for_incompatible_configs() + # Note: The hooks object could be global instead of passed around, *however* that would make + # for a confusing API usage and plumbing story wherein we nest multiple .optimize calls. + # There is some prior art around this, w/r/t nesting backend calls are enforced to be the same + # compiler, however, this feels onerous for callback and hooks, and it feels better to give our users an + # easier to understand UX at the cost of a little more plumbing on our end. + hooks = Hooks( + guard_export_fn=guard_export_fn, + guard_fail_fn=guard_fail_fn, + guard_filter_fn=guard_filter_fn, + ) + torch._C._log_api_usage_once("torch._dynamo.optimize") + if ( + disable + or os.environ.get("TORCHDYNAMO_DISABLE", "") == "1" + or (not justknobs_check("pytorch/compiler:enable_dynamo")) + ): + return _NullDecorator() + + if nopython and not config.debug_force_graph_break_on_leaf_return: + return optimize_assert( + backend, + dynamic=dynamic, + hooks=hooks, + rebuild_ctx=rebuild_ctx, + package=package, + recompile_limit=recompile_limit, + ) + + backend = get_compiler_fn(backend) + + # Find if backend has any extra context manager + backend_ctx_ctor = getattr(backend, "backend_ctx_ctor", null_context) + + # The backend function is stashed in the callable returned by + # _optimize_catch_errors in the field _torchdynamo_orig_backend. This can + # be used by eval_frame.c to insert a guard on the backend. + + # With CachingPrecompile, instantiate an uninitialized CompilePackage + # which gets initialized by _optimize_catch_errors.__call__ once we have a function + if config.caching_precompile and package is None: + from .package import CompilePackage + + package = CompilePackage(fn=None, dynamo=None, ignore_inlined_sources=False) + + return _optimize_catch_errors( + convert_frame.convert_frame( + backend, + hooks, + package=package, + recompile_limit=recompile_limit, + ), + hooks, + backend_ctx_ctor, + fullgraph=False, + error_on_graph_break=error_on_graph_break + and not config.debug_force_graph_break_on_leaf_return, + dynamic=dynamic, + compiler_config=( + backend.get_compiler_config() + if hasattr(backend, "get_compiler_config") + else None + ), + rebuild_ctx=rebuild_ctx, + package=package, + ) + + +# TODO(voz): Consider making "explain" output alongside a run / part of a run +@patch("torch._dynamo.symbolic_convert.explain", True) +def explain(f: Callable[..., Any], *extra_args: Any, **extra_kwargs: Any) -> Any: + from .backends.debugging import ExplainOutput + + def inner(*args: Any, **kwargs: Any) -> ExplainOutput: + # TODO(voz): Do we want a decorator for this? + from . import reset # type: ignore[attr-defined] + + reset() + + graphs: list[torch.fx.GraphModule] = [] + break_reasons: list[Any] = [] + op_count: int = 0 + ops_per_graph: list[list[Target]] = [] + out_guards: list[_guards.Guard] = [] + + def dynamo_graph_accumulating_compiler( + gm: torch.fx.GraphModule, example_inputs: Any + ) -> Callable[..., Any]: + from .backends.debugging import _explain_graph_detail + + nonlocal graphs + nonlocal op_count + nonlocal ops_per_graph + nonlocal break_reasons + + gm, graphs, op_count, ops_per_graph, break_reasons = _explain_graph_detail( + gm, graphs, op_count, ops_per_graph, break_reasons + ) + + return gm.forward + + def guard_export_print(guards: Iterable[_guards.Guard]) -> None: + nonlocal out_guards + out_guards.extend(guards) + + opt_f = optimize( + dynamo_graph_accumulating_compiler, + nopython=False, + guard_export_fn=guard_export_print, + )(f) + # TODO(voz): We may have instances of `f` that mutate inputs, we should track sideeffects and reject. + opt_f(*args, **kwargs) + + graph_count = len(graphs) + graph_break_count = graph_count - 1 + compile_time = compile_times(repr="str") + + # TODO(voz): Do we want a decorator for this? + reset() + + return ExplainOutput( + graphs, + graph_count, + graph_break_count, + break_reasons, + op_count, + ops_per_graph, + out_guards, + compile_time, + ) + + if extra_args or extra_kwargs: + warnings.warn( + "explain(f, *args, **kwargs) is deprecated, use explain(f)(*args, **kwargs) instead. " + "If you don't migrate, we may break your explain call in the future if your user defined kwargs " + "conflict with future kwargs added to explain(f).", + FutureWarning, + stacklevel=2, + ) + return inner(*extra_args, **extra_kwargs) + else: + return inner + + +class FlattenInputOutputSignature(torch.fx.Transformer): + def __init__( + self, + m: torch.fx.GraphModule, + flat_args: list[Any], + matched_input_elements_positions: list[int], + flat_results: Sequence[Any], + matched_output_elements_positions: list[int], + example_fake_inputs: list[torch.Tensor], + flat_args_dynamic_dims: list[set[int]], + fake_mode: fake_tensor.FakeTensorMode | None = None, + ) -> None: + super().__init__(m) + + assert len(flat_args_dynamic_dims) == len(flat_args) + matched_input_elements_to_fake = { + val: example_fake_inputs[ix] + for ix, val in enumerate(matched_input_elements_positions) + } + + self.new_args = [] + for i in range(len(flat_args)): + arg = super().placeholder(f"arg{i}", (), {}) + if i in matched_input_elements_to_fake: + arg.node.meta["val"] = matched_input_elements_to_fake[i] + else: + # Fill node.meta["val"] with faketensor from the input, + # if it's not found in matched_input_elements_positions + if fake_mode is not None and isinstance(flat_args[i], torch.Tensor): + # TODO(zhxchen17) Also preserve all the user constraints here. + arg.node.meta["val"] = fake_mode.from_tensor( + flat_args[i], + symbolic_context=StatelessSymbolicContext( + dynamic_sizes=[ + ( + DimDynamic.DYNAMIC + if d in flat_args_dynamic_dims[i] + else DimDynamic.STATIC + ) + for d in range(len(flat_args[i].shape)) + ], + constraint_sizes=[None] * len(flat_args[i].shape), + ), + ) + elif isinstance(flat_args[i], _IntWrapper): + arg.node.meta["val"] = flat_args[i].val + else: + arg.node.meta["val"] = flat_args[i] + + self.new_args.append(arg) + self.old_args_gen = (self.new_args[i] for i in matched_input_elements_positions) + self.matched_output_elements_positions = matched_output_elements_positions + self.flat_results = flat_results + + def placeholder( + self, target: Target, args: tuple[Argument, ...], kwargs: dict[str, Any] + ) -> Any: + arg = next(self.old_args_gen) + if "val" in self.current_node.meta: + arg.node.meta["val"] = self.current_node.meta["val"] + if "tensor_dict" in self.current_node.meta: + arg.node.meta["tensor_dict"] = self.current_node.meta["tensor_dict"] + if "example_value" in self.current_node.meta: + # NB: intentionally do not use set_example_value + arg.node.meta["example_value"] = self.current_node.meta["example_value"] + if "unbacked_bindings" in self.current_node.meta: + arg.node.meta["unbacked_bindings"] = self.current_node.meta[ + "unbacked_bindings" + ] + return arg + + def output( + self, target: Target, args: tuple[Argument, ...], kwargs: dict[str, Any] + ) -> Any: + dynamo_result_flat = args[0] + lookup = [*dynamo_result_flat, *self.new_args] # type: ignore[misc] + # pyrefly: ignore [implicit-any] + new_results_flat = [] + for i in range(len(self.flat_results)): + if self.matched_output_elements_positions[i] is not None: + new_results_flat.append( + lookup[self.matched_output_elements_positions[i]] + ) + else: + const_val = self.flat_results[i] + assert isinstance(const_val, tuple(common_constant_types)) + new_results_flat.append(const_val) + return super().output(target, (new_results_flat,), {}) + + def run_node(self, n: Node) -> Any: + self.current_node = n + result_proxy = super().run_node(n) + if "val" in self.current_node.meta: + result_proxy.node.meta["val"] = self.current_node.meta["val"] + if "example_value" in self.current_node.meta: + # NB: intentionally do not use set_example_value + result_proxy.node.meta["example_value"] = self.current_node.meta[ + "example_value" + ] + if "unbacked_bindings" in self.current_node.meta: + result_proxy.node.meta["unbacked_bindings"] = self.current_node.meta[ + "unbacked_bindings" + ] + if self.current_node.op != "output": + result_proxy.node._rename( + getattr(self.current_node, "name", result_proxy.node.name) + ) + return result_proxy + + def transform(self) -> torch.fx.GraphModule: + result_gm = super().transform() + if "dynamo_flat_name_to_original_fqn" in self.module.meta: # type: ignore[operator] + result_gm.meta["dynamo_flat_name_to_original_fqn"] = self.module.meta[ # type: ignore[index] + "dynamo_flat_name_to_original_fqn" # type: ignore[index] + ] + if "dynamo_compile_id" in self.module.meta: # type: ignore[operator] + result_gm.meta["dynamo_compile_id"] = self.module.meta["dynamo_compile_id"] # type: ignore[index] + return result_gm + + +class ExportResult(NamedTuple): + graph_module: torch.fx.GraphModule + guards: _guards.GuardsSet + # NB: Do not add new fields without overriding __iter__; people are + # destructuring so it is BC-breaking + + +# NOTE: this function only supports graphs created by Dynamo's OutputGraph module +def check_signature_rewritable(graph: torch.fx.GraphModule) -> None: + # pyrefly: ignore [implicit-any] + input_errors = [] + for node in graph.graph.find_nodes(op="placeholder"): + # set in OutputGraph._call_user_compiler + assert hasattr(node, "_dynamo_source") + assert hasattr(graph, "_source_to_user_stacks") + + # NOTE: We can safely ignore these type warnings if and only if + # the function is made from OutputGraph (checked in the assertions) + source = node._dynamo_source # type: ignore[attr-defined] + user_stacks = graph._source_to_user_stacks.get(source) # type: ignore[operator, union-attr] + if user_stacks is None: + continue + assert len(user_stacks) > 0 + # In some cases we may not have a useful stack. Look for a + # useful stack + stack = None + for s in user_stacks: + if len(s) == 0: + continue + stack = s + break + if stack is None: + msg = f"{source.name}, a closed over free variable" + else: + tb = "".join(traceback.format_list(stack)) + extra = "" + if len(user_stacks) > 1: + extra = f"(elided {len(user_stacks) - 1} more accesses)" + msg = f"{source.name}, accessed at:\n{tb}{extra}" + # TODO: option to print ALL of the stack traces at once + input_errors.append(msg) + + if input_errors: + raise UserError( + UserErrorType.INVALID_INPUT, + "Cannot export model which references tensors that are neither " + "buffers/parameters/constants nor are direct inputs. For each tensor, if you'd " + "like this tensor to be an explicit input, add it as a dummy argument " + "to the top-level model definition you are exporting; if you would " + "like its value to be embedded as an exported constant, wrap its access " + "in a function marked with @assume_constant_result.\n\n" + + "\n\n".join(input_errors), + ) + + +def check_user_input_output(flat_values: list[Any], error_type: UserErrorType) -> None: + supported_types = [ + torch.Tensor, + torch.SymInt, + torch.SymFloat, + torch.SymBool, + torch._C.ScriptObject, + _IntWrapper, + ] + list(common_constant_types) + + def is_supported_type(val: Any) -> bool: + return isinstance(val, tuple(supported_types)) or is_opaque_type(type(val)) + + value_type = "input" if error_type == UserErrorType.INVALID_INPUT else "output" + # We only check that the outputs are not None. Inputs can be None. + for v in flat_values: + if not is_supported_type(v): + if error_type == UserErrorType.INVALID_INPUT and v is None: + continue + + raise UserError( + error_type, + f"It looks like one of the {value_type}s with type `{type(v)}` " + "is not supported or pytree-flattenable. \n" + f"Exported graphs {value_type}s can only contain the " + f"following supported types: {supported_types}. \n" + "If you are using a custom class object, " + "please register a pytree_flatten/unflatten function " + "using `torch.utils._pytree.register_pytree_node` or " + "`torch.export.register_dataclass`.", + ) + + +def rewrite_signature( + f_sig: inspect.Signature, + graph: torch.fx.GraphModule, + fake_mode: fake_tensor.FakeTensorMode | None, + flat_args: list[Any], + in_spec: pytree.TreeSpec, + example_fake_inputs: list[Any], + graph_captured_input: Iterable[Any], + graph_captured_output: Iterable[Any] | None, + dynamo_traced_result: Any, + flat_args_dynamic_dims: list[set[int]], +) -> torch.fx.GraphModule: + orig_args, orig_kwargs = pytree.tree_unflatten(flat_args, in_spec) + + check_user_input_output(flat_args, UserErrorType.INVALID_INPUT) + flat_results_traced, out_spec_traced = pytree.tree_flatten(dynamo_traced_result) + check_user_input_output(flat_results_traced, UserErrorType.INVALID_OUTPUT) + + def check_optional_input_and_error(f_sig: inspect.Signature) -> None: + # Check if function has optional input. + for name, param in f_sig.parameters.items(): + if param.default is not inspect.Parameter.empty: + import torch._dynamo.graph_break_hints as graph_break_hints + from torch._dynamo.exc import unimplemented + + log.error( + "Parameter %s is optional with a default value of %s", + name, + param.default, + ) + unimplemented( + gb_type="rewrite_signature: cannot trace optional function input", + context="", + explanation=f"Parameter {name} is optional with a default value of {param.default}. This is not supported yet.", + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + ) + + def produce_matching( + debug_type: str, sources: Iterable[Any], candidates: Iterable[Any] + ) -> list[int | None]: + matched_elements_positions: list[int | None] = [] + dict_of_source_vals = {} + for i, val in enumerate(sources): + dict_of_source_vals[id(val)] = i + + for val in candidates: + if isinstance(val, tuple(common_constant_types)): + matched_elements_positions.append(None) + elif id(val) not in dict_of_source_vals: + if debug_type == "inputs": + check_optional_input_and_error(f_sig) + raise AssertionError( + f"Unexpectedly found a {type(val)} in the {debug_type}.\n" + 'Please file an issue along with a paste of the logs from TORCH_LOGS="+export"', + ) + else: + matched_elements_positions.append(dict_of_source_vals[id(val)]) + + return matched_elements_positions + + matched_input_elements_positions = produce_matching( + "inputs", flat_args, graph_captured_input + ) + + assert graph_captured_output is not None + matched_output_elements_positions = produce_matching( + "outputs", list(graph_captured_output) + flat_args, flat_results_traced + ) + + new_graph = FlattenInputOutputSignature( + graph, + flat_args, + matched_input_elements_positions, # type: ignore[arg-type] + flat_results_traced, + matched_output_elements_positions, # type: ignore[arg-type] + example_fake_inputs, + flat_args_dynamic_dims, + fake_mode, + ).transform() + + new_graph.graph._codegen = _PyTreeCodeGen( + _PyTreeInfo( + argument_names(f_sig, orig_args, orig_kwargs), + in_spec, + out_spec_traced, + ) + ) + new_graph.recompile() + return new_graph + + +def export( + f: Callable[..., Any], + *extra_args: Any, + aten_graph: bool = False, + pre_dispatch: bool = False, + decomposition_table: dict[torch._ops.OpOverload, Callable[..., Any]] | None = None, + tracing_mode: str = "symbolic", + dynamic_shapes: dict[str, Any] | tuple[Any] | list[Any] | None = None, + specialize_float: bool = True, + assume_static_by_default: bool = False, + same_signature: bool = True, + disable_constraint_solver: bool = False, + prefer_deferred_runtime_asserts_over_guards: bool = False, + _log_export_usage: bool = True, + constraints: list[Constraint] | None = None, + **extra_kwargs: Any, +) -> Callable[..., ExportResult]: + """ + Export an input function f to a format that can be executed outside of PyTorch using the FX graph. + + Args: + f (callable): A PyTorch function to be exported. + + aten_graph (bool): If True, exports a graph with ATen operators. + If False, exports a graph with Python operators. Default is False. + + pre_dispatch (bool): If True, exports a graph with ATen operators, + but before any logic in the PyTorch dispatcher has run. + This can be useful if you want to apply further transformations on a graph before running it + through autograd, autocast, or any other functionalities that are integrated into the dispatcher. + This flag is only valid if aten_graph=True is set. + Default is False. + + decomposition_table (dict): A dictionary that maps operators to their decomposition functions. + Required if aten_graph or tracing_mode is specified. Default is None. + + tracing_mode (str): If "symbolic", turn on dynamic shapes support. Default is "symbolic". + + dynamic_shapes: + An optional argument where the type should either be: + 1) a dict from argument names of ``f`` to their dynamic shape specifications, + 2) a tuple that specifies dynamic shape specifications for each input in original order. + If you are specifying dynamism on keyword args, you will need to pass them in the order that + is defined in the original function signature. + + The dynamic shape of a tensor argument can be specified as either + (1) a dict from dynamic dimension indices to :func:`Dim` types, where it is + not required to include static dimension indices in this dict, but when they are, + they should be mapped to None; or (2) a tuple / list of :func:`Dim` types or None, + where the :func:`Dim` types correspond to dynamic dimensions, and static dimensions + are denoted by None. Arguments that are dicts or tuples / lists of tensors are + recursively specified by using mappings or sequences of contained specifications. + + same_signature (bool): If True, rewrite the returned graph's signature to be the same as f. + + disable_constraint_solver (bool): Whether the dim constraint solver must be disabled. + + Returns: + A function that given args and kwargs, returns a tuple of (graph, guards) + Graph: An FX graph representing the execution of the input PyTorch function with the provided arguments and options. + Guards: The guards we accumulated during tracing f above + + Raises: + AssertionError: If decomposition_table is specified without setting aten_graph=True, + or if graph breaks during tracing in export. + + AssertionError: If Dynamo input and output is not consistent with traced input/output. + + Note - this headerdoc was authored by ChatGPT, with slight modifications by the author. + """ + if config.debug_force_graph_break_on_leaf_return: + raise unittest.SkipTest("Cannot force graph break on export") + + if _log_export_usage: + log_export_usage(event="export.private_api", flags={"_dynamo"}) + + # Deal with "local variable referenced before assignment" + _f = f + _specialize_float = specialize_float + _assume_static_by_default = assume_static_by_default + _constraints = constraints + + def inner(*args: Any, **kwargs: Any) -> ExportResult: + if not _constraints: + combined_args = _combine_args(_f, args, kwargs) + constraints = _process_dynamic_shapes(combined_args, dynamic_shapes) + else: + constraints = _constraints + + f = _f + specialize_float = _specialize_float + assume_static_by_default = _assume_static_by_default + check_if_dynamo_supported() + torch._C._log_api_usage_once("torch._dynamo.export") + if decomposition_table is not None: + assert aten_graph, ( + "Specifying a decomposition_table table or tracing mode is illegal without setting aten_graph=True" + ) + if pre_dispatch: + assert aten_graph, "pre_dispatch=True can only be used when aten_graph=True" + f = innermost_fn(f) + call_to_inspect = f.forward if isinstance(f, torch.nn.Module) else f + original_signature = inspect.signature(call_to_inspect) # type: ignore[arg-type] + graph = None + out_guards = None + graph_captured_input = None + graph_captured_result: tuple[torch.Tensor, ...] | None = None + fake_mode = None + result_traced = None + + def guard_export_print(guards: _guards.GuardsSet) -> None: + nonlocal out_guards + assert out_guards is None, ( + "whole graph export entails exactly one guard export" + ) + out_guards = guards + + example_inputs: list[Any] = [] + + def dynamo_normalization_capturing_compiler( + gm: torch.fx.GraphModule, inner_example_inputs: list[Any] + ) -> Callable[..., Any]: + nonlocal graph + assert graph is None, ( + "Tried to emit a second graph during export. Tracing through 'f' must produce a single graph." + ) + graph = gm + + nonlocal fake_mode, example_inputs + # NB: do NOT pass inner_example_inputs here, we are detecting the + # Dynamo allocated fake mode, which should be DISTINCT from a + # potential outer ambient fake mode which the user provided. + # example_inputs is always the user specified inputs, so they + # would have the wrong fake mode attached to them + fake_mode = _guards.detect_fake_mode() + example_inputs = inner_example_inputs + + def result_capturing_wrapper(*graph_inputs: Any) -> Any: + nonlocal graph_captured_result + nonlocal graph_captured_input + + graph_captured_input = graph_inputs + assert graph is not None + + named_parameters = dict(graph.named_parameters(remove_duplicate=False)) + named_buffers = dict(graph.named_buffers(remove_duplicate=False)) + + ambient_fake_mode = ( + _guards.detect_fake_mode(graph_inputs) + if _guards.detect_fake_mode(graph_inputs) is not None + else fake_mode + ) + + # We reran fake tensor propagation, but we didn't do + # anything with the resulting unbacked SymInts. Drop them + # from the pending list. + # NB: this is wrong if graph_captured_result has + # data-dependent output size! + ignore_fresh_unbacked = null_context() + assert ambient_fake_mode is not None + if shape_env := ambient_fake_mode.shape_env: + ignore_fresh_unbacked = shape_env.ignore_fresh_unbacked_symbols() # type: ignore[assignment] + + with ( + ambient_fake_mode, + enable_python_dispatcher(), + ignore_fresh_unbacked, + ): + params_and_buffers = { + **named_parameters, + **named_buffers, + } + fake_params_buffers = {} + + for name, value in params_and_buffers.items(): + fake_params_buffers[name] = ambient_fake_mode.from_tensor( + value, static_shapes=True + ) + + from torch._export.non_strict_utils import ( + key_path_to_source, + KeyPath, + ) + + def fakify_with_ambient( + path: KeyPath, t: torch.Tensor | _IntWrapper | Any + ) -> Any: + if isinstance(t, torch.Tensor): + # pyrefly: ignore [missing-attribute] + return ambient_fake_mode.from_tensor(t, static_shapes=True) + elif isinstance(t, _IntWrapper): + if ( + t.dynamism is not None + and isinstance(t.dynamism, _DimHint) + and t.dynamism.type + in ( + _DimHintType.DYNAMIC, + _DimHintType.AUTO, + ) + ): # type: ignore[union-attr] + source = key_path_to_source(path) + symint = ambient_fake_mode.shape_env.create_unspecified_symint_and_symbol( # type: ignore[union-attr] + t.val, source, DimDynamic.DYNAMIC + ) + return symint + else: + return t.val + else: + return t + + fake_graph_inputs = pytree.tree_map_with_path( + fakify_with_ambient, graph_inputs + ) + graph_captured_result = torch.func.functional_call( + graph, + fake_params_buffers, # type: ignore[arg-type] + fake_graph_inputs, # type: ignore[arg-type] + ) + + return graph_captured_result + + return result_capturing_wrapper + + # Note: This is needed by rewrite_signature. We need to put it before + # optimize_assert since user program may mutate the inputs. + flat_args, in_spec = pytree.tree_flatten((args, kwargs)) + + remove_from_cache(f) + constraint_violation_error = None + if tracing_mode != "symbolic": + assume_static_by_default = True + with ( + config.patch( + specialize_int=True, + specialize_float=specialize_float, + assume_static_by_default=assume_static_by_default, + automatic_dynamic_shapes=False, + capture_dynamic_output_shape_ops=True, + capture_scalar_outputs=True, + constant_fold_autograd_profiler_enabled=True, + prefer_deferred_runtime_asserts_over_guards=prefer_deferred_runtime_asserts_over_guards, + # install_free_tensors ensures that params and buffers are still + # added as graph attributes, and makes Dynamo emits graphs that + # follow export pytree-able input requirements + install_free_tensors=config.install_free_tensors_for_export, + ), + _compiling_state_context(), + ): + opt_f = optimize_assert( + dynamo_normalization_capturing_compiler, + hooks=Hooks( + guard_export_fn=guard_export_print, + guard_fail_fn=None, + ), + export=True, + export_constraints=constraints, + )(f) + # TODO(voz): We may have instances of `f` that mutate inputs, we should track sideeffects and reject. + try: + result_traced = opt_f(*args, **kwargs) + except ConstraintViolationError as e: + constraint_violation_error = e + remove_from_cache(f) + + if ( + not disable_constraint_solver + and (shape_env := getattr(fake_mode, "shape_env", None)) is not None + and (dim_constraints := shape_env.dim_constraints) is not None + and not isinstance( + call_to_inspect, (torch._ops.OpOverloadPacket, torch._ops.OpOverload) + ) + and not trace_rules.check(call_to_inspect) + ): + dim_constraints.solve() + + forced_specializations = dim_constraints.forced_specializations() + + msg = dim_constraints.prettify_results( + original_signature, + dynamic_shapes, + constraint_violation_error, + forced_specializations, + ) + if constraint_violation_error: + constraint_violation_error.args = ( + constraint_violation_error.args[0] + msg, + ) + else: + if forced_specializations: + constraint_violation_error = ConstraintViolationError(msg) + else: + log.info( + "Summary of dimension constraints:%s", + msg, + ) + + # Error if we have any constraints on static values + + for k in shape_env.var_to_range: + if isinstance(k, sympy.Integer): + constraint_violation_error = ConstraintViolationError( + f"{''.join(traceback.format_list(shape_env.var_to_stack[k]))}\n" + "It appears that you're trying to set a constraint on a " + f"value which we evaluated to have a static value of {k}. " + 'Set TORCH_LOGS="+export" for more information.' + ) + if constraint_violation_error: + raise constraint_violation_error + + if graph is None: + assert same_signature, ( + "Failed to produce a graph during tracing as no tensor operations were found and same_signature is False." + ) + # If the module does not contain any tensor computation, we would create a graph with inputs and outputs. + # To be consistent with the graph traced by dynano, `graph` will have only tensor inputs as placeholders + # and tensor outputs as output nodes. non-tensor inputs and outputs will be added when rewriting signature. + # We will also construct the `example_inputs`, `graph_captured_input`, and `graph_captured_result` corresponding + # to `graph`. + example_inputs = [] + graph_captured_input = () + graph_captured_result = () + fake_mode = torch._subclasses.FakeTensorMode( + shape_env=ShapeEnv(), export=True + ) + if out_guards is None: + out_guards = _guards.GuardsSet() + assert out_guards is not None # suppress mypy error + parameter_names = list(original_signature.parameters.keys()) + fx_graph = torch.fx.Graph() + for i, name in enumerate(parameter_names): + if torch.is_tensor(flat_args[i]): + node = fx_graph.placeholder(name) + node.meta["val"] = fake_mode.from_tensor( + flat_args[i], static_shapes=True + ) + graph_captured_input = graph_captured_input + (flat_args[i],) + example_inputs.append(flat_args[i]) + fx_graph.output(graph_captured_result) + module = torch.nn.Module() + graph = torch.fx.GraphModule(module, fx_graph) + log.info( + "Failed to capture a graph during tracing as no tensor operations were found.:\n\n%s", + graph.print_readable(print_output=False, colored=True), + ) + else: + assert out_guards is not None, "Failed to produce guards during tracing" + assert fake_mode is not None + + log.info( + "Dynamo captured graph:\n\n%s", + graph.print_readable(print_output=False, colored=True), + ) + + # This check need to happened before aten_graph + # because placeholder's _source_node attribute is not preserved by make_fx + if same_signature: + check_signature_rewritable(graph) + + # NB: This is mostly hitting the cache; Dynamo already converted these + example_fake_inputs = [ + fake_mode.from_tensor(t) if isinstance(t, torch.Tensor) else t + for t in example_inputs + ] + + if aten_graph: + # Running graph with interpreter is needed for propagating the stack_trace + def graph_with_interpreter(*args: Any) -> Any: + with torch.fx.traceback.preserve_node_meta(): + return torch.fx.Interpreter(graph).run(*args) # type: ignore[arg-type] + + with unset_fake_temporarily(), enable_python_dispatcher(), fake_mode: + try: + graph = make_fx( + graph_with_interpreter, + decomposition_table=decomposition_table, + tracing_mode="real", + _allow_non_fake_inputs=True, + pre_dispatch=pre_dispatch, + _allow_fake_constant=False, + )(*example_fake_inputs) + except CondOpArgsMismatchError as e: + # Wrap the internal error to the user-facing error + raise UserError( # noqa: B904 + UserErrorType.DYNAMIC_CONTROL_FLOW, + str(e), + case_name="cond_operands", + ) + + assert graph is not None + for node in graph.graph.find_nodes(op="get_attr"): + if isinstance(getattr(graph, node.target), torch.Tensor): # type: ignore[arg-type] + node.meta["val"] = fake_mode.from_tensor( + getattr(graph, node.target), # type: ignore[arg-type] + static_shapes=True, + ) + + if same_signature: + flat_args_dynamic_dims = [ + { + c.dim + for c in (constraints or ()) + if ( + c.t_id == id(x) + and not isinstance(c, _RelaxedConstraint) + and c.constraint_range.vr.lower != c.constraint_range.vr.upper + ) + } + for x in flat_args + ] + graph = rewrite_signature( + original_signature, + graph, + fake_mode, + flat_args, + in_spec, + example_fake_inputs, + graph_captured_input, # type: ignore[arg-type] + graph_captured_result, + result_traced, # type: ignore[possibly-undefined] + flat_args_dynamic_dims, + ) + return ExportResult(graph, out_guards) + + if extra_args or extra_kwargs: + warnings.warn( + "export(f, *args, **kwargs) is deprecated, use export(f)(*args, **kwargs) instead. " + "If you don't migrate, we may break your export call in the future if your user defined kwargs " + "conflict with future kwargs added to export(f).", + FutureWarning, + stacklevel=2, + ) + return inner(*extra_args, **extra_kwargs) # type: ignore[return-value] + else: + return inner + + +def optimize_assert(*args: Any, **kwargs: Any) -> OptimizeContext: + if "rebuild_ctx" in kwargs and kwargs["rebuild_ctx"] is not None: + # called from optimize + rebuild_ctx = kwargs["rebuild_ctx"] + del kwargs["rebuild_ctx"] + else: + + def rebuild_ctx() -> OptimizeContext: + return optimize_assert(*args, **kwargs) + + return _optimize_assert(rebuild_ctx, *args, **kwargs) + + +def _optimize_assert( + rebuild_ctx: Callable[[], OptimizeContext], + backend: str | Callable[..., Any] | None, + *, + hooks: Hooks = Hooks(None, None, None), + export: bool = False, + export_constraints: Any | None = None, + dynamic: bool | None = None, + package: CompilePackage | None = None, + recompile_limit: int | None = None, +) -> OptimizeContext: + """ + Guarantees single-graph capture. + The same as `torch._dynamo.optimize(backend)` but ignores + symbolic_convert.error_on_graph_break setting. + + Used for fullgraph=True and export, since we must always error on graph breaks and ignore + symbolic_convert.error_on_graph_break. Can also be used for testing. + """ + backend = get_compiler_fn(backend) + + # Find if backend has any extra context manager + backend_ctx_ctor = getattr(backend, "backend_ctx_ctor", null_context) + + if config.caching_precompile and package is None: + # Create an uninitialized package that will be set/filled by + # _OptimizeContext.__call__ + # We need to instantiate the object here because the same CompilePackage + # needs to be shared between convert_frame_assert + # and OptimizeContext. + from .package import CompilePackage + + package = CompilePackage(fn=None, dynamo=None, ignore_inlined_sources=False) + + return _optimize_catch_errors( + convert_frame.convert_frame_assert( + backend, + export=export, + export_constraints=export_constraints, + package=package, + recompile_limit=recompile_limit, + ), + hooks, + backend_ctx_ctor, + fullgraph=True, + export=export, + dynamic=dynamic, + rebuild_ctx=rebuild_ctx, + package=package, + ) + + +class TorchPatcher: + @staticmethod + @functools.cache + def patch() -> None: + # A better way to disable the following would be decorate the source + # functions with @torch._disable_dynamo. However, this causes issues + # with torch.deploy internally. + from .decorators import disable + + torch.jit.trace = disable( + torch.jit.trace, reason="tracing into TorchScript not fully supported" + ) + torch.jit.trace_module = disable( + torch.jit.trace_module, + reason="tracing into TorchScript not fully supported", + ) + torch.jit._get_trace_graph = disable( + torch.jit._get_trace_graph, + reason="tracing into TorchScript not fully supported", + ) + torch.fx._symbolic_trace.Tracer.trace = disable( + torch.fx._symbolic_trace.Tracer.trace, + reason="tracing into FX not fully supported", + ) + torch.distributions.Distribution.set_default_validate_args(False) + + from torch.optim import ( + adadelta, + adagrad, + adam, + adamax, + adamw, + asgd, + lbfgs, + nadam, + radam, + rmsprop, + rprop, + sgd, + sparse_adam, + ) + + optimizer_modules = { + adadelta, + adagrad, + adam, + adamax, + adamw, + asgd, + lbfgs, + nadam, + radam, + rmsprop, + rprop, + sgd, + sparse_adam, + } + + for opt_mod in optimizer_modules: + opt_name = opt_mod.__name__.split(".")[-1] + fused_fn_name = f"_fused_{opt_name}" + + if hasattr(opt_mod, fused_fn_name): + setattr( + opt_mod, + fused_fn_name, + disable( + getattr(opt_mod, fused_fn_name), + reason="don't trace into fused optimizer", + ), + ) + + optimizer_classes = [ + opt + for opt in torch.optim.__dict__.values() + if inspect.isclass(opt) and issubclass(opt, torch.optim.Optimizer) + ] + + # Note: we don't support sparsity or tracing through backwards + excluded_optimizer_classes = { + torch.optim.SparseAdam, + torch.optim.LBFGS, + } + + for opt in optimizer_classes: + if opt in excluded_optimizer_classes: + opt.step = disable( + opt.step, reason=f"optimizer {opt} step not supported" + ) + + if hasattr(opt, "_init_group"): + opt._init_group = disable( + opt._init_group, reason=f"optimizer {opt} _init_group not supported" + ) + + @staticmethod + def suppress_torch_distributed_warnings( + fn: Callable[..., Any], + ) -> Callable[..., Any]: + def inner_fn(*args: Any, **kwargs: Any) -> Any: + with torch._logging.hide_warnings( + torch._logging._internal.user_warning_filter + ): + return fn(*args, **kwargs) + + return inner_fn + + +def skip_code(code: types.CodeType) -> None: + set_code_exec_strategy( + code, FrameExecStrategy(FrameAction.SKIP, FrameAction.DEFAULT) + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/exc.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/exc.py new file mode 100644 index 0000000000000000000000000000000000000000..e2da1f37cb1d4d843f8bc1bdd42eec295006ac3a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/exc.py @@ -0,0 +1,901 @@ +from __future__ import annotations + + +"""Exception handling and error reporting for TorchDynamo. + +This module provides a comprehensive set of exception classes and utilities for error +handling in TorchDynamo. It includes: + +Base Exceptions: + - TorchDynamoException: Base class for all TorchDynamo-specific exceptions + - Various specialized subclasses for different error scenarios + +User Error Handling: + - UserError: Exceptions for user-facing errors in TorchDynamo usage + - UserErrorType: Enumeration of different categories of user errors + - Formatted error messages with debugging information + +Observed Exceptions: + - Classes for handling exceptions observed during tracing + - Special handling for StopIteration, LookupError, etc. + - Exception state management during compilation + +Error Formatting: + - Stack trace filtering and formatting + - Error message augmentation + - Debugging utilities for error reporting +""" + +import json +import logging +import re +import textwrap +import typing +from enum import auto, Enum +from functools import lru_cache +from pathlib import Path +from traceback import extract_stack, format_exc, format_list, FrameSummary, StackSummary +from typing import Any, NoReturn, TYPE_CHECKING + +import torch._guards +from torch._utils_internal import get_file_path_2 + +from . import config +from .utils import counters + + +if TYPE_CHECKING: + import types + + from torch._dynamo.variables import VariableTracker + from torch._guards import CompileId + + from .output_graph import DynamoTracerOutput + from .symbolic_convert import InstructionTranslatorBase + from .types import DynamoFrameType, FrameExecStrategy + + +def exportdb_error_message(case_name: str) -> str: + return ( + "For more information about this error, see: " + + "https://pytorch.org/docs/main/generated/exportdb/index.html#" + + case_name.replace("_", "-") + ) + + +log = logging.getLogger(__name__) +graph_breaks_log = torch._logging.getArtifactLogger(__name__, "graph_breaks") + + +class TorchDynamoException(RuntimeError): + """Base exception class for all TorchDynamo-specific exceptions. + + Attributes: + _torch_dynamo_tracer_output: Optional tracer output attached to the exception + frame_exec_strategy: Optional frame execution strategy to control how convert_frame + should handle this exception. When set, convert_frame will use this strategy + instead of the default behavior. This allows exceptions to signal specific + execution strategies (e.g., SKIP, RUN_ONLY) without requiring separate + exception types for control flow. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self._torch_dynamo_tracer_output: DynamoTracerOutput | None = None + self.frame_exec_strategy: FrameExecStrategy | None = None + + +class InternalTorchDynamoError(TorchDynamoException): + pass + + +class ResumePrologueTracingError(TorchDynamoException): + pass + + +class RestartAnalysis(TorchDynamoException): + restart_reason: str | None + + def __init__(self, *args: Any, restart_reason: str | None = None) -> None: + self.restart_reason = restart_reason + super().__init__(*args) + + +class SpeculationRestartAnalysis(RestartAnalysis): + pass + + +class AutogradGradRestartAnalysis(RestartAnalysis): + """Raised when autograd.grad consumed grad_fns that are returned. + + On restart, autograd.grad will graph break instead of being traced. + """ + + +class RequiresGradRestartAnalysis(RestartAnalysis): + """Raised when a source-less requires_grad_() intermediate leaks as output. + + On restart, requires_grad_() will graph break instead of being traced, + preserving partial acceleration for code before the call. + """ + + +class UnspecializeRestartAnalysis(RestartAnalysis): + pass + + +class CompileCollectiveRestartAnalysis(RestartAnalysis): + pass + + +class TensorifyScalarRestartAnalysis(RestartAnalysis): + pass + + +# Used (primarily for backends) to skip tracing the current frame +# and all future invocations of it. +# NOTE: this does NOT cause a graph break, and thus no graph break messages +# will be issued! +class SkipFrame(TorchDynamoException): + pass + + +class TorchRuntimeError(TorchDynamoException): + def __init__(self, msg: str, real_stack: StackSummary | None = None) -> None: + super().__init__(msg) + self.msg = msg + self.real_stack = ( + real_stack + if real_stack is not None + else torch._guards.TracingContext.extract_stack() + ) + + +class InvalidBackend(TorchDynamoException): + def __init__(self, name: str) -> None: + super().__init__( + f"Invalid backend: {name!r}, see `torch._dynamo.list_backends()` for available backends." + ) + + +class ResetRequired(TorchDynamoException): + def __init__(self) -> None: + super().__init__( + textwrap.dedent( + """ + Must call `torch._dynamo.reset()` before changing backends. Detected two calls to + `torch.compile()` with a different backend compiler arguments. + """ + ) + ) + + +class ShortenTraceback(TorchDynamoException): + def __init__( + self, *args: Any, first_useful_frame: types.FrameType | None, **kwargs: Any + ) -> None: + super().__init__(*args, **kwargs) + self.first_useful_frame = first_useful_frame + + def remove_dynamo_frames(self) -> typing.Self: + tb = self.__traceback__ + if self.first_useful_frame is None or tb is None or config.verbose: + return self + while tb.tb_frame is not self.first_useful_frame: + tb = tb.tb_next + assert tb is not None, "internal error, please report a bug" + return self.with_traceback(tb) + + +class BackendCompilerFailed(ShortenTraceback): + def __init__( + self, + backend_fn: Any, + inner_exception: Exception, + first_useful_frame: types.FrameType | None, + ) -> None: + self.backend_name = getattr(backend_fn, "__name__", "?") + self.inner_exception = inner_exception + msg = f"backend={self.backend_name!r} raised:\n{type(inner_exception).__name__}: {inner_exception}" + super().__init__(msg, first_useful_frame=first_useful_frame) + + +# NOTE: important invariant! Almost any exception handler that handles Unsupported +# should NOT suppress the exception if skip_frame is set! +# skip_frame is used by symbolic_convert.py to bubble up Unsupported exceptions to convert_frame to cause +# a frame skip. Once the Unsupported exn is in convert_frame, we will always skip, so skip_frame +# won't be checked +class Unsupported(TorchDynamoException): + def __init__( + self, + msg: str, + # TODO: make this argument required once we remove Unsupported subclasses + gb_type: str = "", + skip_frame: bool = False, + *, + case_name: str | None = None, + real_stack: StackSummary | None = None, + ) -> None: + super().__init__(msg) + if not real_stack: + real_stack = torch._guards.TracingContext.extract_stack() + self.real_stack = real_stack + self.msg = msg + self.skip_frame = skip_frame + self.category: str | None = None + self.add_to_stats() + self.gb_type: str | None = gb_type + self.logged = False + + def remove_from_stats(self) -> None: + assert self.category is not None + counters[self.category][self.msg] -= 1 + if counters[self.category][self.msg] <= 0: + del counters[self.category][self.msg] + + def add_to_stats(self, category: str = "unimplemented") -> None: + self.category = category + counters[category][self.msg] += 1 + + +class UnknownPropertiesDuringBackwardTrace(TorchDynamoException): + pass + + +class RecompileError(TorchDynamoException): + pass + + +class InfiniteGeneratorError(TorchDynamoException): + # Raised when the number of yielded values is greater than MAX_ITERATOR_LIMIT + pass + + +class CondOpArgsMismatchError(TorchDynamoException): + """ + Internal error from cond() due to arguments mismatch. + """ + + +class UserErrorType(Enum): + DYNAMIC_CONTROL_FLOW = auto() + ANTI_PATTERN = auto() + STANDARD_LIBRARY = auto() + CONSTRAINT_VIOLATION = auto() + DYNAMIC_DIM = auto() + INVALID_INPUT = auto() + INVALID_OUTPUT = auto() + UNSUPPORTED_ALIASED_MUTATED_DYNAMIC_INPUTS = auto() + + +class UserError(TorchDynamoException): + def __init__( + self, error_type: UserErrorType, msg: str, case_name: str | None = None + ) -> None: + """ + Type of errors that would be valid in Eager, but not supported in TorchDynamo. + The error message should tell user about next actions. + + error_type: Type of user error + msg: Actionable error message + case_name: (Optional) Unique name (snake case) for the usage example in exportdb. + """ + if case_name is not None: + assert isinstance(case_name, str) + if msg.endswith("."): + msg += " " + else: + msg += "\n" + msg += exportdb_error_message(case_name) + super().__init__(msg) + self.real_stack = torch._guards.TracingContext.extract_stack() + self.skip_frame = False + self.logged = False + self.error_type = error_type + self.msg = msg + self.message = msg + + +# debug exception thrown when tracing torch._dynamo.step_unsupported() +class StepUnsupported(TorchDynamoException): + def __init__(self, msg: str, real_stack: StackSummary | None = None) -> None: + super().__init__(msg) + self.msg = msg + if not real_stack: + real_stack = torch._guards.TracingContext.extract_stack() + self.real_stack = real_stack + self.logged = False + + +class UnsafeScriptObjectError(TorchDynamoException): + pass + + +class UncapturedHigherOrderOpError(TorchDynamoException): + def __init__(self, msg: str, real_stack: StackSummary | None = None) -> None: + super().__init__(msg) + self.msg = msg + self.real_stack = ( + real_stack + if real_stack is not None + else torch._guards.TracingContext.extract_stack() + ) + + +# TODO: I'm a little uncertain about what error classification we should have +# for this. This is potentially a user error, but regressions in +# specialization in PyTorch proper could also trigger this problem +class FailOnRecompileLimitHit(Exception): + pass + + +class PackageError(TorchDynamoException): + pass + + +class ObservedException(TorchDynamoException): + # An exception observed during the tracing. This exception is used by Dynamo to handle exceptions. + def __init__( + self, *args: Any, real_stack: StackSummary | None = None, **kwargs: Any + ) -> None: + super().__init__(*args, **kwargs) + self.real_stack: StackSummary = ( + real_stack + if real_stack is not None + else torch._guards.TracingContext.extract_stack() + ) + + +class ObservedUserStopIteration(ObservedException): + # An UserStopIteration exception observed during the Dynamo tracing (e.g Dynamo tracing __next__) + value: Any | None + + # Reference `StopIteration_init` in CPython + # https://github.com/python/cpython/blob/3.11/Objects/exceptions.c#L568-L584 + def __init__( + self, *args: Any, real_stack: StackSummary | None = None, **kwargs: Any + ) -> None: + super().__init__("unhandled `raise StopIteration`", real_stack=real_stack) + if len(args) > 0: + self.value = args[0] + else: + self.value = None + + +class ObservedLookupError(ObservedException): + # A LookupError exception to be raised from inside Dynamo tracing. This can happen on __getitem__ + pass + + +class ObservedIndexError(ObservedLookupError): + # An IndexError exception to be raised from inside Dynamo tracing. This can happen on list __getitem__ + pass + + +class ObservedKeyError(ObservedLookupError): + # A KeyError exception to be raised from inside Dynamo tracing. This can happen on dict __getitem__ + pass + + +class ObservedGeneratorExit(ObservedException): + pass + + +class ObservedAttributeError(ObservedException): + # An AttributeError exception to be raised from inside Dynamo tracing. This can happen on user defined object __getattr__ + pass + + +class ObservedRuntimeError(ObservedException): + # A RuntimeError exception to be raised from inside Dynamo tracing. This can happen on generator.throw(..) method + pass + + +class ObservedNotImplementedError(ObservedException): + pass + + +class ObservedTypeError(ObservedException): + # A TypeError exception to be raised from inside Dynamo tracing. This can happen on generator.send(..) method + pass + + +observed_exception_map = { + StopIteration: ObservedUserStopIteration, + LookupError: ObservedLookupError, + IndexError: ObservedIndexError, + GeneratorExit: ObservedGeneratorExit, + KeyError: ObservedKeyError, + AttributeError: ObservedAttributeError, + RuntimeError: ObservedRuntimeError, + NotImplementedError: ObservedNotImplementedError, + TypeError: ObservedTypeError, +} + + +def get_dynamo_observed_exception(exc_type: type[Exception]) -> type[ObservedException]: + if exc_type not in observed_exception_map: + name = getattr(exc_type, "__name__", str(exc_type)) + observed_exception_map[exc_type] = type( # type: ignore[assignment] + f"Observed{name}Error", (ObservedException,), {} + ) + # pyrefly: ignore [bad-index] + return observed_exception_map[exc_type] + + +def raise_observed_exception( + exc_type: type[Exception], + tx: InstructionTranslatorBase, + *, + args: list[VariableTracker] | list[str] | None = None, + kwargs: dict[str, VariableTracker] | None = None, +) -> NoReturn: + from .symbolic_convert import ExceptionVals + from .variables.builder import SourcelessBuilder + + if args: + args_ = [ + SourcelessBuilder.create(tx, arg) if isinstance(arg, str) else arg + for arg in args + ] + else: + args_: list[VariableTracker] = [] + + # CPython here raises an exception. Since there is no python code, we have to manually setup the exception + # stack and raise the exception. + exception_vt = SourcelessBuilder.create(tx, exc_type).call_function( + tx, args_, kwargs or {} + ) + assert isinstance(exception_vt, ExceptionVals) + tx._attach_traceback_to_exception(exception_vt) + tx.exn_vt_stack.set_current_exception(exception_vt) # type: ignore[arg-type] + raised_exc = get_dynamo_observed_exception(exc_type) + # Store the original exception arguments for better error messages + if args: + raise raised_exc(*args_) + raise raised_exc + + +def raise_type_error(tx: InstructionTranslatorBase, msg: str) -> NoReturn: + """Raise a TypeError as an observed exception during tracing.""" + raise_observed_exception(TypeError, tx, args=[msg]) + + +def handle_observed_exception(tx: Any) -> None: + # This is essentially exception handling code, equivalent of this pseudo code + # + # try: + # ... somebody raising StopIteration + # except StopIteration + # pass + # + # If this was going through the python code, we would have called exception_handler method, but FOR_ITER + # handles the exception completely in CPython. For example for 3.11, the resulting bytecode is + # + # + # 6 46 LOAD_GLOBAL 2 (StopIteration) + # 58 RAISE_VARARGS 1 + # >> 60 PUSH_EXC_INFO + + # 7 62 LOAD_GLOBAL 2 (StopIteration) + # 74 CHECK_EXC_MATCH + # 76 POP_JUMP_FORWARD_IF_FALSE 3 (to 84) + # 78 POP_TOP + + # 8 80 POP_EXCEPT + # + + # Fortunately this translates to a simple pop from the exn_vt_stack + tx.exn_vt_stack.clear_current_exception() + + +# These exceptions are ok to fallback to eager/graph_break. +exceptions_allowed_to_be_fallback = ( + torch._subclasses.fake_tensor.DataDependentOutputException, + torch._subclasses.fake_tensor.DynamicOutputShapeException, + torch._subclasses.fake_tensor.UnsupportedOperatorException, + torch._subclasses.fake_tensor.UnsupportedFakeTensorException, + torch._subclasses.fake_tensor.UnsupportedMutationAliasingException, +) + + +def unimplemented_with_warning( + e: Exception, + code: types.CodeType, + *, + gb_type: str, + context: str, + explanation: str, + hints: list[str], +) -> NoReturn: + # This function calls unimplemented internally and eventually graph breaks + # or falls to eager. unimplemented itself does not print any user warnings, + # i.e., its very silent. This helper function is intended when an error is + # encountered in the torch.compile stack which is worth showing as warning + # to the user. For example, if AOT Autograd backend fails with a fake tensor + # exception, its ok to fallback to eager but not silently. Here, we can use + # this function to log the message and the stack trace. + graph_break_msg = format_error_msg_verbose(e, code) + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "dynamo_graph_break_reason", + "encoding": "string", + }, + payload_fn=lambda: graph_break_msg, + ) + graph_breaks_log.debug("%s", graph_break_msg) + _unimplemented = unimplemented + # to prevent a graph break registry entry + _unimplemented( + gb_type=gb_type, + context=context, + explanation=explanation, + hints=hints, + from_exc=e, + log_warning=True, + ) + + +def format_graph_break_message( + gb_type: str, + context: str, + explanation: str, + hints: list[str], +) -> str: + explanation = textwrap.indent(explanation, " ").lstrip() + hints_str = "\n".join( + " Hint: " + textwrap.indent(hint, " ").lstrip() for hint in hints + ) + context = textwrap.indent(context, " ").lstrip() + + msg = f"""\ +{gb_type} + Explanation: {explanation} +{hints_str} + + Developer debug context: {context}""" + documentation_link = get_gbid_documentation_link(gb_type) + + if documentation_link: + msg += f"\n\n For more details about this graph break, please visit: {documentation_link}" + + return msg + + +@lru_cache(maxsize=1) +def _load_gb_type_to_gb_id_map() -> dict[str, Any]: + """ + Loads the gb_type to gb_id map from the graph break registry from JSON file with caching. + + Includes historical gb_type (mapping behavior of duplicate gb_types with different gb_ids is undefined). + """ + try: + script_dir = Path(__file__).resolve().parent + registry_path = get_file_path_2( + "", str(script_dir), "graph_break_registry.json" + ) + with open(registry_path) as f: + registry = json.load(f) + except Exception: + log.exception("Error accessing the registry file") + # pyrefly: ignore [implicit-any] + registry = {} + + mapping = {} + for k, v in registry.items(): + for entry in v: + mapping[entry["Gb_type"]] = k + + return mapping + + +def get_gbid_documentation_link(gb_type: str) -> str | None: + """ + Retrieves the GBID documentation link for a given graph break type. + + Args: + gb_type: The graph break type to look up. + + Returns: + A string containing the documentation URL if found, otherwise None. + """ + GRAPH_BREAK_SITE_URL = ( + "https://meta-pytorch.github.io/compile-graph-break-site/gb/" # @lint-ignore + ) + + gb_type_to_gb_id_map = _load_gb_type_to_gb_id_map() + + if gb_type in gb_type_to_gb_id_map: + return ( + f"{GRAPH_BREAK_SITE_URL}gb{gb_type_to_gb_id_map[gb_type].lstrip('GB')}.html" + ) + + return None + + +_NOTHING = object() + + +def unimplemented( + *, + gb_type: str, + context: str, + explanation: str, + hints: list[str], + from_exc: Any = _NOTHING, + log_warning: bool = False, + skip_frame: bool = False, +) -> NoReturn: + """ + Called within dynamo to cause a graph break. + Args: + gb_type: Context-free graph break type. It should be a short string without any + information specific to the tracing context (i.e. no dynamically-generated strings) + context: Developer context for the graph break. It can contain tracing context/dynamic strings. + explanation: User-facing context-dependent explanation for the graph break. Can be dynamic. + hints: List of user-facing hints for the graph break. + """ + + msg = format_graph_break_message(gb_type, context, explanation, hints) + + if log_warning: + log.warning(msg) + if from_exc is not _NOTHING: + past_real_stack = None + if hasattr(from_exc, "real_stack"): + past_real_stack = from_exc.real_stack + if isinstance(from_exc, Unsupported): + msg = f"{from_exc.msg}\n\n*** While handling this graph break, another graph break occurred: ***\n\n{msg}" + # noqa: GB_REGISTRY + raise Unsupported(msg, gb_type, skip_frame, real_stack=past_real_stack) + # noqa: GB_REGISTRY + raise Unsupported( + msg, gb_type, skip_frame, real_stack=past_real_stack + ) from from_exc + # noqa: GB_REGISTRY + raise Unsupported(msg, gb_type, skip_frame) + + +# KeyError has special handling for its args +# see https://github.com/python/cpython/blob/3.11/Objects/exceptions.c#L2534 for details +class KeyErrorMsg: + def __init__(self, value: Any) -> None: + self.value = value + + def __str__(self) -> str: + return str(self.value) + + def __repr__(self) -> str: + return self.__str__() + + +def augment_exc_message_with_hop_name(exc: Exception, msg: str) -> str: + # Add HOP context right after before the explanation if present; + # otherwise after the message + if hasattr(exc, "_hop_name"): + lines = msg.partition("\n Explanation:") + msg = ( + f"{lines[0]}\n Higher Order Operator: {exc._hop_name}{lines[1]}{lines[2]}" # type: ignore[attr-defined] + ) + + return msg + + +def augment_exc_message(exc: Exception, msg: str = "\n", export: bool = False) -> None: + import traceback + + exc.innermost_user_frame_summary = None # type: ignore[attr-defined] + + real_stack = get_real_stack(exc) + if real_stack is not None and len(real_stack) > 0: + exc.innermost_user_frame_summary = real_stack[-1] # type: ignore[attr-defined] + msg += f"\nfrom user code:\n {''.join(traceback.format_list(real_stack))}" + + if config.replay_record_enabled and hasattr(exc, "record_filename"): + msg += ( + f"\nLast frame execution written to {exc.record_filename}. To run only this frame while debugging, run\ + torch._dynamo.replay('{exc.record_filename}').\n" + ) + + if not config.verbose and hasattr(exc, "real_stack"): + msg += ( + "\nSet TORCHDYNAMO_VERBOSE=1 for the internal stack trace " + "(please do this especially if you're reporting a bug to PyTorch). " + 'For even more developer context, set TORCH_LOGS="+dynamo"\n' + ) + + if hasattr(exc, "inner_exception") and hasattr( + exc.inner_exception, "minifier_path" + ): + if hasattr(exc.inner_exception, "buck_command"): + msg += ( + f"\nMinifier script written to {exc.inner_exception.minifier_path}. Run " + f"this buck command to find the smallest traced graph " + f"which reproduces this error: {exc.inner_exception.buck_command}\n" + ) + else: + msg += ( + f"\nMinifier script written to {exc.inner_exception.minifier_path}. Run " + "this script to find the smallest traced graph which reproduces this error.\n" + ) + + old_msg = "" if len(exc.args) == 0 else str(exc.args[0]) + + old_msg = augment_exc_message_with_hop_name(exc, old_msg) + + if isinstance(exc, KeyError): + exc.args = (KeyErrorMsg(old_msg + msg),) + exc.args[1:] + else: + new_msg = old_msg + msg + exc.args = (new_msg,) + exc.args[1:] + + +def get_exc_message( + e: Exception, compile_id: CompileId +) -> tuple[str | None, int | None]: + filename = None + lineno = None + if e.innermost_user_frame_summary is not None: # type: ignore[attr-defined] + filename = e.innermost_user_frame_summary.filename # type: ignore[attr-defined] + lineno = e.innermost_user_frame_summary.lineno # type: ignore[attr-defined] + e.compile_id = compile_id # type: ignore[attr-defined] + return filename, lineno + + +def get_stack_above_dynamo() -> StackSummary: + return filter_stack(extract_stack()) + + +def get_real_stack( + exc: Exception, frame: DynamoFrameType | None = None +) -> StackSummary | None: + real_stack = getattr(exc, "real_stack", None) + if real_stack is None: + return None + + # NB: it's possible for real_stack to be []; we still attempt to + # report a stack anyway because the stack_above_dynamo may still + # be useful for debugging + + if frame is not None: + # NB: frame is PyInterpreterFrame on Python 3.11 and later, + # not a TRUE frame object. You can't actually feed it + # to traceback because it doesn't have enough information. + # To solve this problem, we technically should just materialize + # the frame, the same way _PyFrame_GetFrameObject would do + # (but we cannot actually do this, because this populates + # frame_obj field, which default eval frame doesn't like). + # + # Fortunately, in this case, we can hack it: there's no need + # to actually use the truly top frame, we can just extract + # from where we are right now and rely on filter_stack to + # get rid of all the dynamo frames. For ease of testing + # we apply this behavior to ALL Python versions + stack_above_dynamo = get_stack_above_dynamo() + else: + stack_above_dynamo = StackSummary() + + return StackSummary.from_list(stack_above_dynamo + real_stack) + + +# filter out all frames after entering dynamo +def filter_stack(stack: StackSummary) -> StackSummary: + user_stack = StackSummary() + for frame in stack: + if frame.filename is None: + continue + if "convert_frame" in frame.filename: + break + if "eval_frame" in frame.filename or ( + frame.line and "torch._dynamo.optimize(" in frame.line + ): + continue + user_stack.append(frame) + + return user_stack + + +def remove_resume_prefix(name: str) -> str: + from .resume_execution import TORCH_DYNAMO_RESUME_IN_PREFIX + + match = re.match(f"{TORCH_DYNAMO_RESUME_IN_PREFIX}_(\\w+)_at_\\d+", name) + if match: + return match.group(1) + return name + + +def collapse_resume_frames(stack: StackSummary | list[FrameSummary]) -> StackSummary: + """ + When we graph break, we create a resume function and make a regular Python call + to it, which gets intercepted by Dynamo. This behavior is normally shown in the + traceback, which can be confusing to a user. So we can filter out resume frames + for better traceback clarity. + + Example: + File "..." line 3, in f + + File "..." line 5, in torch_dynamo_resume_in_f_at_80 + + File "..." line 10, in torch_dynamo_resume_in_f_at_120 + + + becomes + File "..." line 10, in f + + """ + + new_stack = StackSummary() + for frame in stack: + if frame.filename is None: + continue + name = remove_resume_prefix(frame.name) + if new_stack and name and new_stack[-1].name == name: + new_stack[-1] = frame + frame.name = name + else: + frame.name = name + new_stack.append(frame) + + return new_stack + + +def format_error_msg_verbose( + exc: Exception, + code: types.CodeType, + record_filename: str | None = None, + frame: DynamoFrameType | None = None, +) -> str: + msg = ( + f"WON'T CONVERT {code.co_name} {code.co_filename} line {code.co_firstlineno}\n" + ) + msg += "=" * 10 + " TorchDynamo Stack Trace " + "=" * 10 + "\n" + msg += format_exc() + real_stack = get_real_stack(exc, frame) + if real_stack is not None: + msg += ( + "\n" + + "=" * 10 + + " The above exception occurred while processing the following code " + + "=" * 10 + + "\n\n" + ) + msg += "".join(format_list(real_stack)) + msg += "\n" + msg += "=" * 10 + + return msg + + +def format_frame_info(code: types.CodeType) -> str: + return ( + f"{getattr(code, 'co_name', '')} " + f"({getattr(code, 'co_filename', '')} " + f"line {getattr(code, 'co_firstlineno', 0)})" + ) + + +def format_skip_frame_message(code: types.CodeType | None, reason: str) -> str: + if code is not None: + frame_info = format_frame_info(code) + return ( + f"torch.compile intentionally decided to skip the frame {frame_info} and fall back to eager.\n" + f"Reason: {reason}" + ) + else: + return ( + f"torch.compile intentionally decided to skip the frame and fall back to eager.\n" + f"Reason: {reason}" + ) + + +def format_error_msg( + exc: Exception, + code: types.CodeType, + record_filename: str | None = None, + frame: DynamoFrameType | None = None, +) -> str: + if config.verbose: + return format_error_msg_verbose(exc, code, record_filename, frame) + return f"WON'T CONVERT {code.co_name} {code.co_filename}\ + line {code.co_firstlineno} \ndue to: \n{format_exc()}" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/external_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/external_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..fb3622cc0710c6cfea8b2a0d658b8ea4e9cc3100 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/external_utils.py @@ -0,0 +1,310 @@ +""" +This module contains utility functions that are explicitly allowed to be called during +TorchDynamo compilation. These functions are carefully vetted to ensure they work +correctly within the TorchDynamo tracing and compilation process. + +Key functionality groups: + +- Compilation State: + Functions for checking compilation state (is_compiling) + +- Function Wrapping: + Utilities for wrapping functions (wrap_inline, wrap_numpy) to work with + TorchDynamo compilation + +- Autograd Hooks: + Functions and classes for handling autograd hooks and backward passes + (call_hook, FakeBackwardCFunction, etc.) + +- Tensor Operations: + Utility functions for tensor operations and transformations +""" + +import functools +import warnings +from collections.abc import Callable +from typing import Any, TYPE_CHECKING, TypeVar +from typing_extensions import deprecated, ParamSpec + +import torch +import torch.utils._pytree as pytree + + +try: + import numpy as np +except ModuleNotFoundError: + np = None # type: ignore[assignment] + +_P = ParamSpec("_P") +_R = TypeVar("_R") + +if TYPE_CHECKING: + # TorchScript does not support `@deprecated` + # This is a workaround to avoid breaking TorchScript + @deprecated( + "`torch._dynamo.external_utils.is_compiling` is deprecated. Use `torch.compiler.is_compiling` instead.", + category=FutureWarning, + ) + def is_compiling() -> bool: + return torch.compiler.is_compiling() + +else: + + def is_compiling() -> bool: + """ + Indicates whether we are tracing/compiling with torch.compile() or torch.export(). + """ + # NOTE: With `@torch.compile(backend="eager")`, torch._dynamo.is_compiling() will get traced + # and return true. torch.compiler.is_compiling() is skipped and will return false. + return torch.compiler.is_compiling() + + +def wrap_inline(fn: Callable[_P, _R]) -> Callable[_P, _R]: + """ + Create an extra frame around fn that is not in skipfiles. + """ + + @functools.wraps(fn) + def inner(*args: _P.args, **kwargs: _P.kwargs) -> _R: + return fn(*args, **kwargs) + + return inner + + +def call_hook( + hook: Callable[..., torch.Tensor | None], *args: Any, **kwargs: Any +) -> torch.Tensor: + """ + Used by compiled autograd to handle hook returning None. + """ + result = hook(*args) + if result is None: + return args[0] + elif kwargs.get("hook_type") == "post_acc_grad_hook": + raise RuntimeError("Tensor post accumulate grad hooks should return None.") + return result + + +def wrap_numpy(f: Callable[_P, _R]) -> Callable[_P, _R]: + r"""Decorator that turns a function from ``np.ndarray``s to ``np.ndarray``s into a function + from ``torch.Tensor``s to ``torch.Tensor``s. + """ + if not np: + return f + + @functools.wraps(f) + def wrap(*args: _P.args, **kwargs: _P.kwargs) -> pytree.PyTree: + args, kwargs = pytree.tree_map_only( + torch.Tensor, lambda x: x.numpy(), (args, kwargs) + ) + out = f(*args, **kwargs) + # pyrefly: ignore [missing-attribute] + return pytree.tree_map_only(np.ndarray, lambda x: torch.as_tensor(x), out) + + return wrap + + +class FakeBackwardCFunction: + def __init__( + self, + real: torch.autograd.function.BackwardCFunction, + saved_tensors: list[torch.Tensor], + ) -> None: + self.real = real + self.saved_tensors = saved_tensors + + def __getattr__(self, name: str) -> Any: + if name == "saved_variables": + warnings.warn( + "'saved_variables' is deprecated; use 'saved_tensors'", + DeprecationWarning, + ) + return self.saved_tensors + + return getattr(self.real, name) + + +def call_backward( + backward_c_function: torch.autograd.function.BackwardCFunction, + saved_tensors: list[torch.Tensor], + *args: Any, +) -> torch.Tensor | tuple[torch.Tensor, ...]: + fake = FakeBackwardCFunction(backward_c_function, saved_tensors) + grads = fake._forward_cls.backward(fake, *args) # type: ignore[attr-defined] + + if not isinstance(grads, tuple): + grads = (grads,) + + return grads + + +def normalize_as_list(x: Any) -> list[Any]: + if isinstance(x, tuple): + return list(x) + elif isinstance(x, list): + return x + return [x] + + +def untyped_storage_size(x: torch.Tensor) -> int: + return x.untyped_storage().size() + + +class FakeCompiledAutogradEngine: + @staticmethod + def queue_callback( + final_callbacks: list[Callable[[], None]], cb: Callable[[], None] + ) -> None: + final_callbacks.append(cb) + + @staticmethod + def exec_final_callbacks(final_callbacks: list[Callable[[], None]]) -> None: + i = 0 + while i < len(final_callbacks): + cb = final_callbacks[i] + cb() + i += 1 + final_callbacks.clear() + + @staticmethod + def _exec_final_callbacks_stub() -> None: + pass + + +def call_hook_from_backward_state( + *args: Any, bw_state: Any, hook_name: str, **kwargs: Any +) -> Any: + return getattr(bw_state, hook_name)(*args, **kwargs) + + +class _ApplyBackwardHook(torch.autograd.Function): + """Custom autograd function that applies a hook during backward. + + This is used to implement register_hook on intermediate tensors without + requiring compiled autograd. The hook function is captured in the context + and applied during the backward pass. + """ + + @staticmethod + # pyre-ignore[14]: Inconsistent override is expected for autograd.Function + def forward( + ctx: Any, tensor: torch.Tensor, hook_fn: Callable[..., Any] + ) -> torch.Tensor: # type: ignore[override] + ctx.hook_fn = hook_fn + return tensor.view_as(tensor) + + @staticmethod + def backward(ctx: Any, grad: torch.Tensor) -> tuple[torch.Tensor, None]: # type: ignore[override] + result = ctx.hook_fn(grad) + if result is None: + result = grad + return result, None + + +def call_module_hooks_from_backward_state( + _: Any, result: Any, *args: Any, bw_state: Any, hooks_name: str, module_name: str +) -> Any: + module = getattr(bw_state, module_name) + hooks = getattr(bw_state, hooks_name) + for hook in hooks: + new_result = hook(module, result, *args) + if new_result is not None: + result = new_result + return result + + +# used for torch._dynamo.disable(recursive=False) +def get_nonrecursive_disable_wrapper(fn: Callable[_P, _R]) -> Callable[_P, _R]: + # wrap function to get the right error message + # this function is in external_utils so that convert_frame doesn't skip it. + @functools.wraps(fn) + def nonrecursive_disable_wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R: + if torch.compiler.is_exporting(): + raise RuntimeError( + "Non-recursive torch.compiler.disable is not supported with torch.export." + ) + return fn(*args, **kwargs) + + return nonrecursive_disable_wrapper + + +def wrap_dunder_call_ctx_manager(self: Any, func: Callable[_P, _R]) -> Callable[_P, _R]: + """ + Apply self as a ctx manager around a call to func + """ + + # NOTE: do not functools.wraps(func) because we don't ever want this frame to be skipped! + def inner(*args: _P.args, **kwargs: _P.kwargs) -> _R: + with self: + return func(*args, **kwargs) + + return inner + + +# Use only on ints marked dynamic via torch.empty(0, integer) +# Currently only way to mark ints as dynamic: https://github.com/pytorch/pytorch/issues/129623 +def unwrap_maybe_dynamic_int(x: torch.Tensor | int) -> int: + if isinstance(x, torch.Tensor): + # x.size() is expected to be [0, dynamic_int] + return x.size(1) + return x + + +def call_accumulate_grad( + variable: torch.Tensor, grad: torch.Tensor, has_post_hooks: bool +) -> None: + updated_grad = torch._dynamo.compiled_autograd.ops.AccumulateGrad( # type: ignore[attr-defined] + [grad], variable, variable.grad, has_post_hooks + ) + variable.grad = updated_grad[0] + + +def wrap_inline_with_error_on_graph_break( + fn: Callable[_P, _R], error_on_graph_break: bool +) -> Callable[_P, _R]: + # NB: need multiple definitions in order to prevent `fullgraph` from + # being a freevar of wrapper + # NOTE: do not functools.wraps(fn) because we don't ever want these wrappers to be skipped! + if error_on_graph_break: + + def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R: + with torch._dynamo.error_on_graph_break(True): + return fn(*args, **kwargs) + + else: + + def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R: + with torch._dynamo.error_on_graph_break(False): + return fn(*args, **kwargs) + + return wrapper + + +def filter_out_const_values(tup: tuple[Any, ...], masks: list[bool]) -> tuple[Any, ...]: + """ + masks is a list of bools, where True means the corresponding element in tup + is a const value. Filter out the const values. + """ + out = [] + for mask_idx, mask in enumerate(masks): + if not mask: + out.append(tup[mask_idx]) + return tuple(out) + + +def insert_const_values_with_mask( + tup: tuple[Any, ...], masks: list[bool], values: tuple[Any, ...] +) -> tuple[Any, ...]: + """ + masks and values are of same length. For indices where the mask is True, use + the const_values to fill in. + """ + out = [] + idx = 0 + for mask_idx, mask in enumerate(masks): + if mask: + out.append(values[mask_idx]) + else: + out.append(tup[idx]) + idx += 1 + return tuple(out) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/funcname_cache.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/funcname_cache.py new file mode 100644 index 0000000000000000000000000000000000000000..6aa935cd6749269236e3feb646ba0c7fb8c89b27 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/funcname_cache.py @@ -0,0 +1,74 @@ +""" +This module provides functionality for caching and looking up fully qualified function +and class names from Python source files by line number. + +It uses Python's tokenize module to parse source files and tracks function/class +definitions along with their nesting to build fully qualified names (e.g. 'class.method' +or 'module.function'). The results are cached in a two-level dictionary mapping: + + filename -> (line_number -> fully_qualified_name) + +Example usage: + name = get_funcname("myfile.py", 42) # Returns name of function/class at line 42 + clearcache() # Clear the cache if file contents have changed + +The parsing is done lazily when a file is first accessed. Invalid Python files or +IO errors are handled gracefully by returning empty cache entries. +""" + +import tokenize + + +cache: dict[str, dict[int, str]] = {} + + +def clearcache() -> None: + cache.clear() + + +def _add_file(filename: str) -> None: + try: + with tokenize.open(filename) as f: + tokens = list(tokenize.generate_tokens(f.readline)) + except (OSError, tokenize.TokenError): + cache[filename] = {} + return + + # NOTE: undefined behavior if file is not valid Python source, + # since tokenize will have undefined behavior. + result: dict[int, str] = {} + # current full funcname, e.g. xxx.yyy.zzz + cur_name = "" + cur_indent = 0 + significant_indents: list[int] = [] + + for i, token in enumerate(tokens): + if token.type == tokenize.INDENT: + cur_indent += 1 + elif token.type == tokenize.DEDENT: + cur_indent -= 1 + # possible end of function or class + if significant_indents and cur_indent == significant_indents[-1]: + significant_indents.pop() + # pop the last name + cur_name = cur_name.rpartition(".")[0] + elif ( + token.type == tokenize.NAME + and i + 1 < len(tokens) + and tokens[i + 1].type == tokenize.NAME + and (token.string == "class" or token.string == "def") + ): + # name of class/function always follows class/def token + significant_indents.append(cur_indent) + if cur_name: + cur_name += "." + cur_name += tokens[i + 1].string + result[token.start[0]] = cur_name + + cache[filename] = result + + +def get_funcname(filename: str, lineno: int) -> str | None: + if filename not in cache: + _add_file(filename) + return cache[filename].get(lineno, None) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/functional_export.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/functional_export.py new file mode 100644 index 0000000000000000000000000000000000000000..41de630917a5fb59285f699ead9607ebba514b8d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/functional_export.py @@ -0,0 +1,983 @@ +import inspect +import logging +import sys +import traceback +import types +from collections import namedtuple +from collections.abc import Callable, Iterable, Sequence +from typing import Any, Optional, TYPE_CHECKING, TypeVar + +import sympy + +import torch +import torch.fx +import torch.utils._pytree as pytree +from torch._dynamo.convert_frame import CaptureOutput, fullgraph_capture, get_traced_fn +from torch._dynamo.decorators import disable as dynamo_disable +from torch._dynamo.eval_frame import argument_names, check_user_input_output +from torch._dynamo.exc import UserErrorType +from torch._dynamo.utils import dynamo_timed, get_metrics_context +from torch._export.utils import _compiling_state_context +from torch._guards import TracingContext +from torch.export.dynamic_shapes import _RelaxedConstraint, Constraint +from torch.fx import Node +from torch.fx.experimental.symbolic_shapes import ( + ConstraintViolationError, + DimDynamic, + StatelessSymbolicContext, +) +from torch.fx.graph import _PyTreeCodeGen, _PyTreeInfo +from torch.fx.node import Argument, Target + + +if TYPE_CHECKING: + from torch._subclasses.fake_tensor import FakeTensorMode + +T = TypeVar("T") +log = logging.getLogger(__name__) + + +def post_process_error_msg( + constraint_violation_error: ConstraintViolationError, + func: Callable[..., Any], + args: Any, + kwargs: Any, +) -> ConstraintViolationError: + """ + Because we trace a different callable, the sources are all messed up. + Manually patch them so the error message looks correct. + """ + from torch.export._unlift import _get_input_paths, _replace_sources + + orig_sig = inspect.signature(func) + flat_input_paths = _get_input_paths((args, kwargs), orig_sig) + if constraint_violation_error.args: + constraint_violation_error.args = ( + _replace_sources(constraint_violation_error.args[0], flat_input_paths), + ) + return constraint_violation_error + + +EXPORT_ROOT_REPLACEMENTS = [ + ("__export_root_", "_"), + ("_export_root.", ""), + ("._export_root", ""), +] + + +def clean_export_root_string(text: str) -> str: + """Generic utility to clean export_root patterns from strings.""" + result = text + for pattern, replacement in EXPORT_ROOT_REPLACEMENTS: + result = result.replace(pattern, replacement) + return result + + +def clean_nn_module_stack_and_source_fn( + graph_module: torch.fx.GraphModule, is_inline_builtin: bool = False +) -> torch.fx.GraphModule: + """ + Clean up nn_module_stack metadata by removing export_root references. + + Removes the _export_root module references from nn_module_stack metadata + in graph nodes, which are artifacts from the export process. Fixes two patterns: + + 1. Keys: Removes "__export_root_" and "__modules['_export_root']_" prefixes + - Normal case: "L__self____export_root_child" -> "L__self__child" + - inline_builtin case: Uses numeric ID strings like "140468831433840" + + 2. Values: Removes "._export_root" and "._modules['_export_root']" from child names + e.g., "L['self']._export_root.child" -> "L['self'].child" + e.g., "L['self']._modules['_export_root'].child" -> "L['self'].child" + + Also removes the root export entry "L__self____export_root" entirely. + + Args: + graph_module: The GraphModule to clean up + is_inline_builtin: If True, keys are numeric ID strings and self references + (L['self']) are filtered out + + Returns: + The cleaned GraphModule (modified in-place) + """ + + def _process_nn_module_stack( + nn_module_stack: dict[str, tuple[str, T]], + ) -> dict[str, tuple[str, T]]: + if "L__self____export_root" in nn_module_stack: + del nn_module_stack["L__self____export_root"] + + # Clean up remaining entries + cleaned_stack = {} + for key, (child_name, child_class) in nn_module_stack.items(): + # Clean key by removing export_root patterns + clean_key = clean_export_root_string(key) + + # Clean child_name by removing export_root patterns + clean_name = clean_export_root_string(child_name) + + # Skip self reference for inline builtin case + if is_inline_builtin and clean_name == "L['self']": + continue + + cleaned_stack[clean_key] = (clean_name, child_class) + return cleaned_stack + + def _process_source_fn(source_fn_stack: Iterable[T]) -> Iterable[T]: + cleaned_stack = [] + for item in source_fn_stack: + if isinstance(item, tuple) and len(item) == 2: + name, cls = item + if isinstance(name, str): + clean_name = clean_export_root_string(name) + cleaned_stack.append((clean_name, cls)) + else: + cleaned_stack.append(item) + else: + # pyrefly: ignore [bad-argument-type] + cleaned_stack.append(item) + # pyrefly: ignore [bad-return] + return cleaned_stack + + for node in graph_module.graph.nodes: + if "nn_module_stack" in node.meta: + node.meta["nn_module_stack"] = _process_nn_module_stack( + node.meta["nn_module_stack"].copy() + ) + + source_fn_stack = node.meta.get("source_fn_stack", None) + if source_fn_stack: + node.meta["source_fn_stack"] = _process_source_fn(source_fn_stack.copy()) + + if "dynamo_flat_name_to_original_fqn" in graph_module.meta: + # Clean up flat name to original fqn mapping + clean_name_to_original_fqn = {} + for flat_name, original_fqn in graph_module.meta[ + "dynamo_flat_name_to_original_fqn" + ].items(): + clean_name_to_original_fqn[clean_export_root_string(flat_name)] = ( + clean_export_root_string(original_fqn) + ) + graph_module.meta["dynamo_flat_name_to_original_fqn"] = ( + clean_name_to_original_fqn + ) + + return graph_module + + +def clean_export_root(graph_module: torch.fx.GraphModule) -> None: + """Remove export_root artifacts from FX graph in-place""" + + # Unlike getattr node, call_module can be invoked multiple times + # In those cases, we should fix all invocations of call_module + clean_named_module_map: dict[str, str] = {} + + # Update get_attr nodes in-place + for node in graph_module.graph.nodes: + if node.op == "get_attr": + old_target = node.target + new_target = clean_export_root_string(old_target) + if new_target != old_target: + node.target = new_target + assert hasattr(graph_module, old_target) + # Move the parameter to the new name + param = torch.fx.graph_module._get_attr(graph_module, old_target) + torch.fx.graph_module._assign_attr(param, graph_module, new_target) + torch.fx.graph_module._del_attr(graph_module, old_target) + # Dynamo will only have one nested level + if node.op == "call_module": + old_target = node.target + assert isinstance(old_target, str) + new_target = clean_export_root_string(old_target) + assert isinstance(new_target, str) + new_name = clean_export_root_string(node.name) + if new_target == old_target: + continue + + # if this module has already been cleaned before, just lookup from map. + if old_target in clean_named_module_map: + node.target = clean_named_module_map[old_target] + node.name = new_name + continue + target = graph_module.get_submodule(old_target) + graph_module.delete_submodule(old_target) + graph_module.add_submodule(new_target, target) + node.target = new_target + node.name = new_name + clean_named_module_map[old_target] = new_target + + +class ModuleToTrace(torch.nn.Module): + def __init__(self, foo: Any, in_spec: Any) -> None: + super().__init__() + self._export_root = foo + self.in_spec = in_spec + + def forward(self, *flat_args: Any) -> "ExportTracerOutput": + args, kwargs = pytree.tree_unflatten(flat_args, self.in_spec) + res = self._export_root(*args, **kwargs) + out_flat, out_spec = pytree.tree_flatten(res) + return ExportTracerOutput(out_flat, out_spec) + + +ExportTracerOutput = namedtuple("ExportTracerOutput", ["flat_args", "out_spec"]) + + +# mypy: disable-error-code="no-untyped-def,var-annotated,assignment,index,operator" +class DynamoGraphTransformer(torch.fx.Transformer): + """Graph transformer for dynamo export that flattens inputs/outputs without complex matching.""" + + def __init__( + self, + module: torch.fx.GraphModule, + flat_inputs: list[Any], + flat_args_dynamic_dims: list[set[int]], + graph_input_order: dict[int, int], + graph_output_map: dict[int, tuple[str, Any]], + fake_mode: Any | None = None, + graph_inputs: dict[int, Any] | None = None, + ) -> None: + super().__init__(module) + + assert len(flat_args_dynamic_dims) == len(flat_inputs) + + self.flat_inputs = flat_inputs + self.flat_args_dynamic_dims = flat_args_dynamic_dims + self.graph_input_order = graph_input_order + self.graph_output_map = graph_output_map + self.fake_mode = fake_mode + self.graph_inputs = graph_inputs or {} + + # Get original placeholders and output + self.placeholders = [n for n in module.graph.nodes if n.op == "placeholder"] + self.output_node = next(n for n in module.graph.nodes if n.op == "output") + + # Create new flattened input placeholders + self.new_input_nodes: dict[int, torch.fx.Node] = {} + self._create_flattened_inputs() + + # Iterator for replacing old placeholders + self.old_to_new_mapping = {} + self._create_placeholder_mapping() + + def _create_flattened_inputs(self) -> None: + """Create new placeholder nodes for flattened inputs with proper fake tensors.""" + for i in range(len(self.flat_inputs)): + placeholder = super().placeholder(f"arg_{i}", (), {}) + + # Check if this user input (index i) maps to a graph placeholder + if i in self.graph_input_order: + # graph_input_order[i] gives us which graph placeholder this user input corresponds to + graph_placeholder_idx = self.graph_input_order[i] + if graph_placeholder_idx < len(self.placeholders): + orig_placeholder = self.placeholders[graph_placeholder_idx] + # Copy other metadata but not "val" yet + for key, value in orig_placeholder.meta.items(): + if key != "val": + placeholder.node.meta[key] = value + + # Always ensure we have proper "val" metadata from fake tensor + if self.fake_mode is not None and isinstance( + self.flat_inputs[i], torch.Tensor + ): + placeholder.node.meta["val"] = self.fake_mode.from_tensor( + self.flat_inputs[i], + symbolic_context=StatelessSymbolicContext( + dynamic_sizes=[ + ( + DimDynamic.DYNAMIC + if d in self.flat_args_dynamic_dims[i] + else DimDynamic.STATIC + ) + for d in range(len(self.flat_inputs[i].shape)) + ], + constraint_sizes=[None] * len(self.flat_inputs[i].shape), + ), + ) + elif hasattr(self.flat_inputs[i], "val"): # _IntWrapper case + placeholder.node.meta["val"] = self.flat_inputs[i].val + else: + placeholder.node.meta["val"] = self.flat_inputs[i] + + # pyrefly: ignore [unsupported-operation] + self.new_input_nodes[i] = placeholder + + def _create_placeholder_mapping(self) -> None: + """Create mapping from old placeholders to new ones.""" + # graph_input_order maps: user_input_index -> graph_placeholder_index + # We need to create: old_graph_placeholder -> new_user_input_placeholder + for user_input_idx, graph_placeholder_idx in self.graph_input_order.items(): + if graph_placeholder_idx < len(self.placeholders): + old_placeholder = self.placeholders[graph_placeholder_idx] + new_placeholder = self.new_input_nodes[user_input_idx] + self.old_to_new_mapping[old_placeholder] = new_placeholder + + def placeholder( + self, target: Target, args: tuple[Argument, ...], kwargs: dict[str, Any] + ) -> Any: + """Replace old placeholders with new flattened ones.""" + # Return the corresponding new placeholder + if self.current_node in self.old_to_new_mapping: + new_arg = self.old_to_new_mapping[self.current_node] + + # Copy over additional metadata from current node, but don't overwrite "val" + for key in ["tensor_dict", "example_value", "unbacked_bindings"]: + if key in self.current_node.meta: + new_arg.node.meta[key] = self.current_node.meta[key] + + # Only copy "val" if we don't already have a good one + if "val" in self.current_node.meta and "val" not in new_arg.node.meta: + new_arg.node.meta["val"] = self.current_node.meta["val"] + + return new_arg + else: + # Convert captured objects (e.g., opaque objects from closures) to + # get_attr nodes + placeholder_idx = self.placeholders.index(self.current_node) + if placeholder_idx in self.graph_inputs: + source = self.graph_inputs[placeholder_idx] + if not isinstance(source, torch._dynamo.source.GetItemSource): + example_val = self.current_node.meta.get( + "val" + ) or self.current_node.meta.get("example_value") + if example_val is not None: + attr_name = f"_captured_{placeholder_idx}" + if isinstance(example_val, torch.Tensor): + self.module.register_buffer(attr_name, example_val) + else: + setattr(self.module, attr_name, example_val) + result = self.tracer.create_proxy("get_attr", attr_name, (), {}) + result.node.meta = self.current_node.meta.copy() + result.node.meta["val"] = example_val + return result + return super().placeholder(target, args, kwargs) + + def output( + self, target: Target, args: Sequence[Any], kwargs: dict[str, Any] + ) -> Any: + """Transform output according to graph_output_map.""" + original_outputs = args[0] + + # Build new output list based on graph_output_map + new_outputs = [] + for i in sorted(self.graph_output_map.keys()): + output_type, val = self.graph_output_map[i] + + if output_type == "graph_out": + new_outputs.append(original_outputs[val]) + elif output_type == "input": + input_idx = val.index + new_outputs.append(self.new_input_nodes[input_idx]) + elif output_type == "constant": + new_outputs.append(val) + + return super().output(target, (tuple(new_outputs),), {}) + + def run_node(self, n: Node) -> Any: + """Run node transformation and preserve metadata.""" + self.current_node = n + result = super().run_node(n) + + # Copy important metadata + if hasattr(result, "node") and result.node is not n: + for key in ["val", "example_value", "unbacked_bindings"]: + if key in n.meta: + result.node.meta[key] = n.meta[key] + + # Preserve node names (except output) + if n.op != "output" and hasattr(n, "name"): + result.node._rename(n.name) + + return result + + def transform(self) -> torch.fx.GraphModule: + """Perform the graph transformation and copy module metadata.""" + result_gm = super().transform() + + # Copy module metadata like the original implementation + if hasattr(self.module, "meta"): + # pyrefly: ignore [unsupported-operation] + if "dynamo_flat_name_to_original_fqn" in self.module.meta: + # pyrefly: ignore [bad-index] + result_gm.meta["dynamo_flat_name_to_original_fqn"] = self.module.meta[ + # pyrefly: ignore [bad-index] + "dynamo_flat_name_to_original_fqn" + ] + # pyrefly: ignore [unsupported-operation] + if "dynamo_compile_id" in self.module.meta: + # pyrefly: ignore [bad-index] + result_gm.meta["dynamo_compile_id"] = self.module.meta[ + # pyrefly: ignore [bad-index] + "dynamo_compile_id" + ] + + return result_gm + + +def _suggest_or_raise_constraint_violation( + module_to_trace: torch.nn.Module, + orig_callable: Callable[..., Any], + fake_mode: Optional["FakeTensorMode"], + graph_capture_output: CaptureOutput, + args: Any, + kwargs: Any, + dynamic_shapes: dict[str, Any] | tuple[Any] | list[Any] | None, +) -> None: + constraint_violation_error = None + try: + # Check if we have any constraint violations + fn, _ = get_traced_fn(module_to_trace) + graph_capture_output.graph_capture_output.build_guards(fn.__code__) + except ConstraintViolationError as e: + constraint_violation_error = e + + if ( + (shape_env := getattr(fake_mode, "shape_env", None)) is not None + and (dim_constraints := shape_env.dim_constraints) is not None + and not isinstance( + module_to_trace.forward, + torch._ops.OpOverloadPacket | torch._ops.OpOverload, + ) + ): + dim_constraints.solve() + + forced_specializations = dim_constraints.forced_specializations() + + msg = dim_constraints.prettify_results( + inspect.signature(orig_callable), # type: ignore[attr-defined] + dynamic_shapes, + constraint_violation_error, + forced_specializations, + ) + if constraint_violation_error: + if constraint_violation_error.args: + constraint_violation_error.args = ( + constraint_violation_error.args[0] + msg, + ) + else: + constraint_violation_error.args = (msg,) + else: + if forced_specializations: + constraint_violation_error = ConstraintViolationError(msg) + else: + log.info( + "Summary of dimension constraints:%s", + msg, + ) + + # Error if we have any constraints on static values + + for k in shape_env.var_to_range: + if isinstance(k, sympy.Integer): + constraint_violation_error = ConstraintViolationError( + f"{''.join(traceback.format_list(shape_env.var_to_stack[k]))}\n" + "It appears that you're trying to set a constraint on a " + f"value which we evaluated to have a static value of {k}. " + 'Set TORCH_LOGS="+export" for more information.' + ) + if constraint_violation_error: + constraint_violation_error = post_process_error_msg( + constraint_violation_error, orig_callable, args, kwargs + ) + raise constraint_violation_error + + +def _normalize_shuffle_graph(shuffle_gm: torch.fx.GraphModule) -> None: + shuffle_gm.graph.eliminate_dead_code() + shuffle_gm.recompile() + for name, buffer in list(shuffle_gm.named_buffers()): + delattr(shuffle_gm, name) + setattr(shuffle_gm, name, buffer) + + +def normalize_graph_module(gm: torch.fx.GraphModule) -> None: + for node in gm.graph.nodes: + if node.op == "placeholder": + node.meta["val"] = node.meta["example_value"] + + +class InputProcessor: + def __init__( + self, + root: object, + num_args: int, + kwarg_names: list[str], + ) -> None: + self.root = root + self.num_args = num_args + self.kwarg_names = kwarg_names + + def __call__( + self, inputs: tuple[object, ...] + ) -> tuple[tuple[object, ...], dict[str, object]]: + args = inputs + # pyrefly: ignore [implicit-any] + kwargs = {} + if len(args) > self.num_args: + kwargs = dict(zip(self.kwarg_names, args[self.num_args :])) + args = args[: self.num_args] + if self.root is not None: + if isinstance(self.root, torch.fx.GraphModule): + assert isinstance(self.root.graph._codegen, _DynamoBytecodeCodeGen) + assert hasattr( + self.root.graph._codegen.dynamo_bytecode_flatten, "input_processor" + ) + assert ( + self.root.graph._codegen.dynamo_bytecode_flatten.input_processor + is self + ) + args = (self.root, *args) + return args, kwargs + + +class Yield(Exception): + pass + + +class DynamoBytecodeFlatten: + def __init__( + self, + input_processor: InputProcessor, + out: CaptureOutput, + f_globals: dict[str, object], + ) -> None: + self.input_processor = input_processor + self.out = out + self.f_globals = f_globals + self.gm_inputs: tuple[Any, ...] | None = None + + @dynamo_disable(reason="do not trace internal dynamo graph capture") # type: ignore[misc] + def __call__(self, *inputs: object) -> object: + def backend_dummy(*example_inputs: object) -> None: + self.gm_inputs = example_inputs + raise Yield + + args, kwargs = self.input_processor(inputs) + try: + self.out.forward_callable( + compiled_fn=backend_dummy, extra_globals=self.f_globals + )(*args, **kwargs) + except Yield: + assert self.gm_inputs is not None + return self.gm_inputs + raise RuntimeError + + +class DynamoBytecodeUnflatten: + def __init__( + self, + input_processor: InputProcessor, + out: CaptureOutput, + f_globals: dict[str, object], + ) -> None: + self.input_processor = input_processor + self.out = out + self.f_globals = f_globals + + @dynamo_disable(reason="do not trace internal dynamo graph capture") # type: ignore[misc] + def __call__( + self, flat_outs: Sequence[object], inputs: tuple[object, ...] + ) -> object: + def backend_dummy(*example_inputs: object) -> Sequence[object]: + return flat_outs + + args, kwargs = self.input_processor(inputs) + with torch._C._DisableTorchDispatch(): + results = self.out.forward_callable( + compiled_fn=backend_dummy, extra_globals=self.f_globals + )(*args, **kwargs) + return results + + +def create_fx_graph_from_captured_output( + out: CaptureOutput, mod: Any, args: tuple[Any, ...], kwargs: dict[str, Any] +) -> torch.fx.GraphModule: + assert out.backend_input is not None + backend_input = out.backend_input + + _, root = torch._dynamo.convert_frame.get_traced_fn(mod) + + flat_real_args = pytree.tree_leaves((args, kwargs)) + torch._dynamo.eval_frame.check_user_input_output( + flat_real_args, UserErrorType.INVALID_INPUT + ) + f_globals = out.graph_capture_output.f_globals + + graph_module = backend_input.graph_module + if isinstance(root, torch.nn.Module): + graph_module._parameters = root._parameters + graph_module._buffers = root._buffers + assert all(not hasattr(graph_module, m) for m in root._modules) + graph_module._modules.update(root._modules) + graph_module._non_persistent_buffers_set = root._non_persistent_buffers_set + if sys.version_info >= (3, 14): + import annotationlib # added in 3.14 + + annotations = annotationlib.get_annotations(torch.nn.Module) + else: + annotations = getattr(torch.nn.Module, "__annotations__", None) + for name, value in root.__dict__.items(): + if annotations and name not in annotations: + graph_module.__dict__[name] = value + graph_module._forward_hooks = root._forward_hooks.copy() + graph_module._forward_pre_hooks = root._forward_pre_hooks.copy() + graph_module._backward_hooks = root._backward_hooks.copy() + graph_module._backward_pre_hooks = root._backward_pre_hooks.copy() + if graph_module._forward_hooks or graph_module._forward_pre_hooks: + # Even forward hooks are traced through, they still capture a bunch + # of state through closure. We need to make sure these data are + # accessible through the captured module (but the hooks should be + # disabled). + assert getattr(graph_module, "_wrapped_call", None) is not None + assert isinstance( + graph_module._wrapped_call, torch.fx.graph_module._WrappedCall + ) + assert graph_module._wrapped_call.cls_call is None + + def dynamo_wrapped_call(self, *args: object, **kwargs: object) -> object: + assert "forward" not in self.__dict__ + + fwd_hooks = self._forward_hooks + fwd_pre_hooks = self._forward_pre_hooks + original_forward = type(self).forward + + def patched_forward(self, *args: object, **kwargs: object) -> object: + self._forward_hooks = fwd_hooks + self._forward_pre_hooks = fwd_pre_hooks + return original_forward(self, *args, **kwargs) + + try: + self.forward = types.MethodType(patched_forward, self) + # pyrefly: ignore [implicit-any] + self._forward_hooks = {} + # pyrefly: ignore [implicit-any] + self._forward_pre_hooks = {} + # pyrefly: ignore [invalid-argument] + return super(type(self), self).__call__(*args, **kwargs) + finally: + self.__dict__.pop("forward") + self._forward_hooks = fwd_hooks + self._forward_pre_hooks = fwd_pre_hooks + + # pyrefly: ignore [bad-assignment] + graph_module._wrapped_call.cls_call = dynamo_wrapped_call + + root = graph_module if isinstance(root, torch.nn.Module) else root + input_processor = InputProcessor(root, len(args), list(kwargs.keys())) + dynamo_bytecode_flatten = DynamoBytecodeFlatten(input_processor, out, f_globals) + dynamo_bytecode_unflatten = DynamoBytecodeUnflatten(input_processor, out, f_globals) + + graph_module.graph._codegen = _DynamoBytecodeCodeGen( + argument_names(inspect.signature(mod), args, kwargs), + dynamo_bytecode_flatten, + dynamo_bytecode_unflatten, + ) # type: ignore[attr-defined] + normalize_graph_module(graph_module) + assert not hasattr(graph_module, "_dynamo_bytecode_flatten") + assert not hasattr(graph_module, "_dynamo_bytecode_unflatten") + # pyrefly: ignore [bad-argument-type] + graph_module._dynamo_bytecode_flatten = dynamo_bytecode_flatten + # pyrefly: ignore [bad-argument-type] + graph_module._dynamo_bytecode_unflatten = dynamo_bytecode_unflatten + delattr(graph_module, "_param_name_to_source") + graph_module.recompile() + graph_module.meta["module_call_specs"] = ( + out.graph_capture_output.output_graph.export_metadata.module_call_spec + ) + assert out.backend_input is not None + graph_module.meta["fake_mode"] = out.backend_input.fake_mode # type: ignore[attr-defined] + graph_module.meta["fake_mode"].allow_non_fake_inputs = True + tracing_context = TracingContext(graph_module.meta["fake_mode"]) + tracing_context.tensor_to_context = out.backend_input.tensor_to_context # type: ignore[attr-defined] + graph_module.meta["tracing_context"] = tracing_context + return graph_module + + +class _DynamoBytecodeCodeGen(torch.fx.graph.CodeGen): + def __init__( + self, + orig_arg_names: list[str], + # pyrefly: ignore [implicit-any] + dynamo_bytecode_flatten: Callable, + # pyrefly: ignore [implicit-any] + dynamo_bytecode_unflatten: Callable, + ) -> None: + super().__init__() + self.orig_arg_names = orig_arg_names + self.dynamo_bytecode_flatten = dynamo_bytecode_flatten + self.dynamo_bytecode_unflatten = dynamo_bytecode_unflatten + self.wrap_tuple = False + self._inputs: tuple[Any, ...] | None = None + + def process_inputs(self, *inputs: Any) -> Any: + self._inputs = inputs + results = self.dynamo_bytecode_flatten(*inputs) + return results + + def process_outputs(self, outputs: Any) -> Any: + results = self.dynamo_bytecode_unflatten(outputs, self._inputs) + if self.wrap_tuple: + results = (results,) + self._inputs = None + return results + + def gen_fn_def( + self, + free_vars: list[str], + maybe_return_annotation: str, + *, + expanded_def: bool = False, + ) -> str: + fn_args = self.orig_arg_names + has_orig_self = (fn_args[0] == "self") if len(fn_args) > 0 else False + if has_orig_self: + free_vars.insert(0, "self") + fn_definition = super().gen_fn_def( + fn_args[:], maybe_return_annotation, expanded_def=expanded_def + ) + + if len(free_vars) > 0: # pytree has placeholders in it + fn_definition += self.gen_var_bindings(fn_args, free_vars, expanded_def) + return fn_definition + + def gen_var_bindings( + self, fn_args: list[str], free_vars: list[str], expanded_def: bool + ) -> str: + without_annotation = [x.split(":")[0].split("#")[0] for x in free_vars] + if len(fn_args) == 0: + fn_signature = "" + elif len(fn_args) == 1: + fn_signature = f"{fn_args[0]}, " + else: + fn_signature = f"{', '.join(fn_args)}" + return f""" + _fn_args = ({fn_signature}) + {", ".join(without_annotation)}, = self._dynamo_bytecode_flatten(*_fn_args)""" + + def generate_output( + self, + output_args: torch.fx.node.Argument, + *, + descs: object | None = None, + repr_fn: Any | None = None, + ) -> str: + if repr_fn is None: + repr_fn = repr + # pyrefly: ignore [not-iterable] + returned = f"self._dynamo_bytecode_unflatten(({', '.join([repr_fn(a) for a in output_args])},), _fn_args)" + if self.wrap_tuple: + returned = f"({returned},)" + return f"return {returned}" + + +def dynamo_graph_capture_for_export( + fn: Callable[..., Any], + constraints: list[Constraint] | None = None, +) -> Callable[..., Any]: + if isinstance(fn, torch._ops.OpOverload): + + def default_annotation(arg: torch.Argument) -> str: + if arg.has_default_value(): + return f"={arg.default_value!r}" + return "" + + has_kwarg_only = False + arg_list = [] + for arg in fn._schema.arguments: + if arg.kwarg_only and not has_kwarg_only: + has_kwarg_only = True + arg_list.append("*") + arg_list.append(arg.name + default_annotation(arg)) + func_str = f""" +def op_overload_wrapper({", ".join(arg_list)}): + return op({", ".join([f"{arg.name}={arg.name}" for arg in fn._schema.arguments])}) +""" + out = {} + exec(func_str, {"op": fn}, out) + fn = out["op_overload_wrapper"] # type: ignore[assignment] + + def inner(*args: Any, **kwargs: Any) -> Any: + assert not torch._dynamo.config.install_free_tensors + with ( + _compiling_state_context(), + torch._dynamo.config.patch( + replay_side_effects=False, side_effect_replay_policy="warn" + ), + get_metrics_context(), + dynamo_timed("fullgraph_capture"), + ): + out = fullgraph_capture( + fn, + args, + kwargs, + constraints=constraints, + ) + graph_module = create_fx_graph_from_captured_output(out, fn, args, kwargs) + return graph_module + + return inner + + +def _dynamo_graph_capture_for_export( + mod: Callable[..., Any], + *, + constraints: list[Constraint] | None = None, + dynamic_shapes: dict[str, Any] | tuple[Any] | list[Any] | None = None, +) -> Callable[..., torch.fx.GraphModule]: + """ + Improved dynamo graph capture using transformer approach with proper fake tensor handling. + + This function creates a capture instance that handles: + 1. PyTree flattening/unflattening with proper input ordering + 2. Dynamo graph capture with export-specific context + 3. FX graph transformation for export compatibility + 4. Proper fake tensor metadata preservation + 5. Dynamic dimension constraint handling + + Notable improvements over manual approach: + - Uses FX Transformer for cleaner graph manipulation + - Properly handles fake tensor metadata and dynamic dimensions + - Preserves all necessary metadata for export + - More robust error handling and edge case management + + TODO: + 1. Are we actually gonna run the bytecode? + 2. Need to attach guards + """ + + _dynamic_shapes = dynamic_shapes + _constraints = constraints + + def inner(*args: Any, **kwargs: Any) -> torch.fx.GraphModule: + # This sets the is_exporting flag when building guards. + with _compiling_state_context(): + flat_inputs, in_spec = pytree.tree_flatten((args, kwargs)) + check_user_input_output(flat_inputs, UserErrorType.INVALID_INPUT) + module_to_trace = ModuleToTrace(mod, in_spec) + orig_callable = mod.forward if isinstance(mod, torch.nn.Module) else mod + + constraints: list[Constraint] | None = _constraints + dynamic_shapes: dict[str, Any] | tuple[Any] | list[Any] | None = ( + _dynamic_shapes + ) + + from . import reset # type: ignore[attr-defined] + + reset() + + dynamo_config_ctx = torch._dynamo.config.patch( + specialize_int=True, + specialize_float=True, + assume_static_by_default=True, + automatic_dynamic_shapes=False, + capture_dynamic_output_shape_ops=True, + capture_scalar_outputs=True, + constant_fold_autograd_profiler_enabled=True, + log_graph_in_out_metadata=True, + # install_free_tensors ensures that params and buffers are still + # added as graph attributes, and makes Dynamo emits graphs that + # follow export pytree-able input requirements In future, if we + # fully rely on bytecode for the runtime, we can turn this flag + # off. + install_free_tensors=torch._dynamo.config.install_free_tensors_for_export, + ) + + with ( + get_metrics_context(), + dynamo_timed("fullgraph_capture"), + dynamo_config_ctx, + ): + out = fullgraph_capture( + module_to_trace, + tuple(flat_inputs), + constraints=_constraints, + _is_export_deprecated_do_not_use=True, + ) + + assert out.graph_capture_output.output_graph is not None + + example_inputs: list[Any] = [] + if out.backend_input is not None: + graph = out.backend_input.graph_module + fake_mode = out.backend_input.fake_mode + example_inputs = out.backend_input.example_inputs + else: + graph = torch.fx.GraphModule(torch.nn.Module(), torch.fx.Graph()) + graph.graph.output(None) + graph.recompile() + fake_mode = None + + _suggest_or_raise_constraint_violation( + module_to_trace, + orig_callable, + fake_mode, + out, + args, + kwargs, + dynamic_shapes, + ) + + # Extract export metadata from the new location + export_metadata = out.graph_capture_output.output_graph.export_metadata + graph_inputs = export_metadata.graph_input_idx_to_local_source + graph_output_map = export_metadata.output_return_type + out_spec = export_metadata.out_spec + module_call_spec = export_metadata.module_call_spec + + # Compute dynamic dimensions for each input based on constraints + flat_args_dynamic_dims = [ + { + c.dim + for c in (constraints or ()) + if ( + c.t_id == id(x) + and not isinstance(c, _RelaxedConstraint) + and c.constraint_range.vr.lower != c.constraint_range.vr.upper + ) + } + for x in flat_inputs + ] + + # Create input order mapping from dynamo's internal order to user order + # Only process inputs that come from function arguments (GetItemSource). + # Skip inputs that come from other sources like closures (e.g., captured + # opaque objects like DeviceMesh). + graph_input_order: dict[int, int] = {} + for inp in graph_inputs: + source = graph_inputs[inp] + if isinstance(source, torch._dynamo.source.GetItemSource): + graph_input_order[source.index] = len(graph_input_order) + + for real_idx, graph_idx in graph_input_order.items(): + flat_inputs[real_idx] = example_inputs[graph_idx] + + # Use FX transformer to rebuild the graph cleanly + transformed_graph = DynamoGraphTransformer( + graph, + flat_inputs, + flat_args_dynamic_dims, + graph_input_order, + graph_output_map, + fake_mode, + graph_inputs, + ).transform() + + # Set up PyTree codegen for proper input/output handling + transformed_graph.graph._codegen = _PyTreeCodeGen( + _PyTreeInfo( + argument_names(inspect.signature(orig_callable), args, kwargs), # type: ignore[attr-defined, arg-type] + in_spec, + out_spec, + ) + ) + transformed_graph.recompile() + + clean_nn_module_stack_and_source_fn(transformed_graph, True) + clean_export_root(transformed_graph) + + transformed_graph.meta["module_call_specs"] = module_call_spec + transformed_graph.meta["fake_mode"] = fake_mode + + return transformed_graph + + return inner diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/graph_break_hints.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/graph_break_hints.py new file mode 100644 index 0000000000000000000000000000000000000000..b04f37b3bbc48800cb96b896d5036b97c2ba0ca1 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/graph_break_hints.py @@ -0,0 +1,32 @@ +USER_ERROR = [ + "Your code may result in an error when running in eager. " + "Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. " + 'You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance("force_eager")`. ' +] +DYNAMO_BUG = [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch.", +] +DIFFICULT = [ + "This graph break may be difficult to debug. Please report an issue to PyTorch for assistance.", +] +FUNDAMENTAL = [ + "This graph break is fundamental - it is unlikely that Dynamo will ever be able to trace through " + "your code. Consider finding a workaround.", +] +SUPPORTABLE = [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you " + "encounter this graph break often and it is causing performance issues.", +] +CAUSED_BY_EARLIER_GRAPH_BREAK = [ + "This graph break may have been caused by an earlier graph break. Resolving the earlier graph break may resolve this one.", +] +INFERENCE_MODE = [ + "Avoid using `tensor.is_inference()` and `torch.is_inference_mode_enabled()` in your compile code. " + "This is primarily used in conjunction with `torch.inference_mode`. Consider using `torch.no_grad` instead " + "because `torch.no_grad` leads to same improvements as `inference_mode` when `torch.compile` is used.", +] +SPARSE_TENSOR = [ + "Sparse tensor operations are not yet fully supported in torch.compile with fullgraph=True. " + "Consider using fullgraph=False to allow graph breaks, or move sparse tensor creation " + "outside the compiled region.", +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/graph_break_registry.json b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/graph_break_registry.json new file mode 100644 index 0000000000000000000000000000000000000000..46177b12200f7884c95f2860afbb1465c6695f42 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/graph_break_registry.json @@ -0,0 +1,4916 @@ +{ + "GB0000": [ + { + "Gb_type": "All __torch_function__ overrides returned NotImplemented due to TypeError from user code", + "Context": "fn={fn}, args={args}, kwargs={kwargs}", + "Explanation": "All __torch_function__ overrides for for function {fn} returned NotImplemented", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0001": [ + { + "Gb_type": "Argument of `as_subclass` must be a non-dispatcher-style tensor subclass", + "Context": "{self}.as_subclass({cls})", + "Explanation": "Currently not supported", + "Hints": [ + "Avoid this call or move it outside `torch.compile` regione", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0002": [ + { + "Gb_type": "Assertion failed on symbolic shapes", + "Context": "str(sym_expr)", + "Explanation": "", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0003": [ + { + "Gb_type": "Attempt to trace generator", + "Context": "", + "Explanation": "Generators cannot be compiled directly with `torch.compile`.", + "Hints": [ + "Call a generator from inside of a non-generator Python function and ", + "compile that function instead.", + "This graph break is fundamental - it is unlikely that Dynamo will ever be able to trace through your code. Consider finding a workaround." + ] + } + ], + "GB0004": [ + { + "Gb_type": "Attempted super().__delattr__() on an object without mutation tracking", + "Context": "call_method {self} {name}", + "Explanation": "Dynamo needs to track mutations on an object before `super().__delattr__` can be used on it. But the object ({self.objvar}) doesn't have attribute mutation tracking enabled.", + "Hints": [ + "Ensure the object is tracked by Dynamo's side effect system.", + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0005": [ + { + "Gb_type": "Attempted to a str() method implemented in C/C++", + "Context": "", + "Explanation": "{type(arg.value)} has a C/C++ based str method. This is not supported.", + "Hints": [ + "Write the str method in Python" + ] + } + ], + "GB0006": [ + { + "Gb_type": "Attempted to call a super() attribute that is not a function or method", + "Context": "call_method {self} {name}", + "Explanation": "Dynamo does not know how to trace the call `super().{name}()` because `super().{name}` is not a function or method attribute.", + "Hints": [ + "Ensure the attribute accessed via `super()` is a standard method or function." + ] + } + ], + "GB0007": [ + { + "Gb_type": "Attempted to call function marked as skipped", + "Context": "module: {module_name}, qualname: {qualname}, skip reason: {reason}", + "Explanation": "explanation", + "Hints": [] + } + ], + "GB0008": [ + { + "Gb_type": "Attempted to inline function marked as skipped", + "Context": "qualname: {fn_qualname}, name: {func.get_name()}, filename: `{func.get_filename()}`, skip reason: {result.reason}", + "Explanation": "Dynamo developers have intentionally marked that the function `{fn_qualname}` should not be traced.", + "Hints": [] + } + ], + "GB0009": [ + { + "Gb_type": "Attempted to inline function marked as skipped (SkipFunctionVariable)", + "Context": "Attempted to inline a SkipFunctionVariable {func}", + "Explanation": "Attempted to inline a function that was previously determined to be marked as intentionally skipped.", + "Hints": [] + } + ], + "GB0010": [ + { + "Gb_type": "Attempted to read a deleted variable", + "Context": "item: {item}, name: {name}", + "Explanation": "", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0011": [ + { + "Gb_type": "Attempted to read undefined local variable", + "Context": "LOAD_FAST {name}", + "Explanation": "Could not find a local variable with name `{name}`", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0012": [ + { + "Gb_type": "Attempted to read undefined local variable (implicit)", + "Context": "LOAD_FAST {name}", + "Explanation": "Could not find an implicit local variable with name `{name}`", + "Hints": [ + "This happens in dict/list comprehensions", + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0013": [ + { + "Gb_type": "Attempted to represent unregistered RemovableHandle", + "Context": "", + "Explanation": "Dynamo attempted to build a representation of a torch.utils.hooks.RemovableHandle, which is not supported. This happens because the RemovableHandle was created in another frame.", + "Hints": [] + } + ], + "GB0014": [ + { + "Gb_type": "Attempted to wrap RNN, GRU, or LSTM", + "Context": "str(value)", + "Explanation": "Dynamo does not support RNN, GRU, or LSTM.", + "Hints": [ + "Set torch._dynamo.config.allow_rnn=True to enable experimental support for RNN, GRU, and LSTM in Dynamo", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + }, + { + "Gb_type": "Attempted to wrap RNN, GRU, or LSTM", + "Context": "str(value)", + "Explanation": "Dynamo does not support RNN, GRU, or LSTM.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0015": [ + { + "Gb_type": "Attempted to wrap sparse Tensor", + "Context": "", + "Explanation": "torch.compile does not support sparse Tensors", + "Hints": [ + "Sparse tensor operations are not yet fully supported in torch.compile with fullgraph=True. Consider using fullgraph=False to allow graph breaks, or move sparse tensor creation outside the compiled region." + ] + } + ], + "GB0016": [ + { + "Gb_type": "Attempted to wrap strided NestedTensor", + "Context": "", + "Explanation": "torch.compile does not support strided NestedTensor", + "Hints": [] + } + ], + "GB0017": [ + { + "Gb_type": "Attempted to wrap torch._higher_order_ops.invoke_subgraph", + "Context": "", + "Explanation": "Directly using invoke_subgraph is not supported. Use nested_compile_region", + "Hints": [] + } + ], + "GB0018": [ + { + "Gb_type": "Attempted to wrap unbacked SymInt", + "Context": "", + "Explanation": "Unbacked SymInt input is not supported yet.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0019": [ + { + "Gb_type": "AutogradFunctionContextVariable escaped Dynamo-traced region", + "Context": "", + "Explanation": "We cannot reconstruct a torch.autograd.Function's context object.", + "Hints": [] + } + ], + "GB0020": [ + { + "Gb_type": "BUILD_STRING key conflict", + "Context": "format_string_parts: {format_string_parts}, kwargs: {kwargs}, part.sym_kwargs: {part.sym_kwargs}", + "Explanation": "Failed to build format string due to key conflict", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0021": [ + { + "Gb_type": "BUILD_STRING type error", + "Context": "str(part)", + "Explanation": "Format string part type is not correct - expected constant or format string.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0022": [ + { + "Gb_type": "Bad import result", + "Context": "typestr(value)", + "Explanation": "Import result is not a Python module.", + "Hints": [] + } + ], + "GB0023": [ + { + "Gb_type": "Builtin `operator.*` comparison with constant `self` failed", + "Context": "call_method {self} {name} {args} {kwargs}", + "Explanation": "\"Failed to compare {self} with {other}, \" + f\"because {other} is not a Python constant or its mutation check fails.\"", + "Hints": [] + } + ], + "GB0024": [ + { + "Gb_type": "CLEANUP_THROW with StopIteration", + "Context": "", + "Explanation": "Received StopIteration when handling generator.throw/close. This is not supported.", + "Hints": [] + } + ], + "GB0025": [ + { + "Gb_type": "Call to `torch._dynamo.graph_break()`", + "Context": "Called `torch._dynamo.graph_break()` with args `{args}`, kwargs `{kwargs}`", + "Explanation": "User-inserted graph break. Message: {graph_break_msg}", + "Hints": [ + "Remove the `torch._dynamo.graph_break()` call." + ] + } + ], + "GB7054": [ + { + "Gb_type": "backward() with non-leaf tensor", + "Context": "backward(inputs=[...]) with non-leaf tensor: {var}", + "Explanation": "backward(inputs=[...]) with non-leaf tensors is not yet supported.", + "Hints": [ + "Only pass leaf tensors (parameters, graph inputs) to backward(inputs=...)" + ] + }, + { + "Gb_type": "backward() with non-leaf tensor", + "Context": "backward(inputs=[...]) with non-leaf tensor", + "Explanation": "backward(inputs=[...]) with non-leaf tensors is not yet supported.", + "Hints": [ + "Only pass leaf tensors (parameters, graph inputs) to backward(inputs=...)" + ] + } + ], + "GB0026": [ + { + "Gb_type": "Calling subclass default constructor with more than tensor argument", + "Context": "{self.value}(args={args}, kwargs={kwargs})", + "Explanation": "Currently not supported", + "Hints": [ + "Avoid this constructor call or move it outside ", + "`torch.compile` regione", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0027": [ + { + "Gb_type": "Cannot check Tensor object identity without its fake value", + "Context": "str(fake_tensor)", + "Explanation": "TensorVariable is missing a fake example_value.", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0028": [ + { + "Gb_type": "Caught non-Exception value", + "Context": "str(exc_instance)", + "Explanation": "Except expects to receive an object of Exception type but received {exc_instance}.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0029": [ + { + "Gb_type": "Compilation of intermediate hooks requires compiled autograd", + "Context": "var_getattr {self} {name}", + "Explanation": "Dynamo must be in compiled_autograd to register hooks.", + "Hints": [ + "Consider using torch.autograd.Function with a custom backward() method instead of register_hook()." + ] + } + ], + "GB0030": [ + { + "Gb_type": "ComptimeContext graph break", + "Context": "msg", + "Explanation": "Manually triggered ComptimeContext graph break with message {msg}.", + "Hints": [] + } + ], + "GB0031": [ + { + "Gb_type": "Custom __getattribute__ in nn.Module attribute access", + "Context": "var_getattr {self} {name}", + "Explanation": "Dynamo could not trace through the custom `__getattribute__` method on this `nn.Module`.", + "Hints": [ + "Simplify your `__getattribute__` implementation, ", + "or replace it with a targeted `@property`.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + }, + { + "Gb_type": "Custom __getattribute__ in nn.Module attribute access", + "Context": "var_getattr {self} {name}", + "Explanation": "Dynamo does not support checking key existence on `nn.Module` instances that have a custom `__getattribute__` method defined.", + "Hints": [ + "Avoid defining `__getattribute__` in your module.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0032": [ + { + "Gb_type": "Custom __getattribute__ in nn.Module dict key check", + "Context": "has_key_in_generic_dict {self} {key}", + "Explanation": "Dynamo does not support checking key existence on `nn.Module` instances that have a custom `__getattribute__` method defined.", + "Hints": [ + "Avoid defining `__getattribute__` in your module.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0033": [ + { + "Gb_type": "Data dependent operator", + "Context": "str(cause.func)", + "Explanation": "Operator `{cause.func}` has a non-Tensor output whose value is dependent on the data of Tensor inputs.", + "Hints": [] + } + ], + "GB0034": [ + { + "Gb_type": "Data-dependent assertion failed (cannot compile partial graph)", + "Context": "value: {value}", + "Explanation": "Dynamo has determined when encountering a data-dependent assert failure that it should not compile the partial graph.", + "Hints": [ + "Use `torch._assert()` to raise a hard AssertionError when the check fails. ", + "This error will propagate back the user code ", + "that called the compiled function (i.e. Dynamo will not trace any exception handling).", + "Remove the assert statement.", + "Move the assert statement outside of any context managers in order to graph break with ", + "partial graph compilation (if fullgraph=False).", + "This graph break is fundamental - it is unlikely that Dynamo will ever be able to trace through your code. Consider finding a workaround." + ] + } + ], + "GB0035": [ + { + "Gb_type": "Data-dependent branching with non-constant __bool__", + "Context": "method: {x}, result: {result}", + "Explanation": "Attempted to perform data-dependent branching on a user-defined object with a __bool__ method that did not return a constant.", + "Hints": [] + } + ], + "GB0036": [ + { + "Gb_type": "Dynamic shape operator", + "Context": "str(cause.func)", + "Explanation": "Operator `{cause.func}`'s output shape depends on input Tensor data.", + "Hints": [ + "Enable tracing of dynamic shape operators with ", + "`torch._dynamo.config.capture_dynamic_output_shape_ops = True`" + ] + } + ], + "GB6999": [ + { + "Gb_type": "unsupported variable type for __dict__ access", + "Context": "VariableTracker type: {type(vt)}", + "Explanation": "Dynamo does not know how to get __dict__ from {type(vt)}", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0037": [ + { + "Gb_type": "Dynamic shape operator (no meta kernel)", + "Context": "str(cause.func)", + "Explanation": "Operator `{cause.func}` does not have a meta kernel that supports dynamic output shapes", + "Hints": [ + "Please report an issue to PyTorch" + ] + } + ], + "GB0038": [ + { + "Gb_type": "Dynamic slicing with Tensor arguments", + "Context": "SliceVariable start: {start}, stop: {stop}, step: {step}", + "Explanation": "Creating slices with Tensor arguments is not supported. e.g. `l[:x]`, where `x` is a 1-element tensor.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0039": [ + { + "Gb_type": "Dynamo recompile limit exceeded", + "Context": "Limit type: {limit_type}", + "Explanation": "Dynamo attempted to recompile the code object too many times, exceeding the {limit_type} cache size limit (currently set to {getattr(config, limit_type)}). Excessive recompilations can degrade performance due to the compilation overhead of each recompilation.", + "Hints": [ + "To monitor recompilations, enable TORCH_LOGS=recompiles. ", + "If recompilations are expected, consider ", + "increasing torch._dynamo.config.{limit_type} to an appropriate value.", + "See {troubleshooting_url} for tips on dealing with recompilations." + ] + }, + { + "Gb_type": "Dynamo cache limit exceeded", + "Context": "Limit type: {limit_type}", + "Explanation": "Dynamo attempted to recompile the code object too many times, exceeding the {limit_type} cache size limit.Giving up on compiling as the compile time tradeoff is likely not worth the performance gain.", + "Hints": [] + } + ], + "GB0040": [ + { + "Gb_type": "Encountered aliasing during higher order op tracing", + "Context": "context", + "Explanation": "Higher order ops do not support aliasing. Found in {name}", + "Hints": [ + "Replace `return input` with `return input.clone()` to avoid aliasing.", + "Consider using the debug context to change user code to avoid aliasing.", + "Please open an issue." + ] + }, + { + "Gb_type": "Encountered aliasing during higher order op tracing", + "Context": "context", + "Explanation": "Higher order ops do not support aliasing. Found in {source_target.name}", + "Hints": [ + "Replace `return input` with `return input.clone()` to avoid aliasing.", + "Consider using the debug context to change user code to avoid aliasing.", + "Please open an issue." + ] + } + ], + "GB0041": [ + { + "Gb_type": "Encountered input mutation during higher order op tracing", + "Context": "context", + "Explanation": "Higher order ops do not support input mutation. Found in {name}", + "Hints": [ + "Consider using the debug context to change user code to avoid mutation.", + "Please open an issue." + ] + }, + { + "Gb_type": "Encountered input mutation during higher order op tracing", + "Context": "context", + "Explanation": "Higher order ops do not support input mutation. Found in {source_target.name}", + "Hints": [ + "Consider using the debug context to change user code to avoid mutation.", + "Please open an issue." + ] + } + ], + "GB0042": [ + { + "Gb_type": "Encountered non user function variable during invoke_subgraph HOP tracing", + "Context": "str(fn_vt)", + "Explanation": "invoke_subgraph does not support non user function variable", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0043": [ + { + "Gb_type": "Encountered non-PT2-compliant op", + "Context": "", + "Explanation": "msg + + err_epilogue", + "Hints": [] + } + ], + "GB0044": [ + { + "Gb_type": "Encountered strided NestedTensor in automatic dynamic dim determination", + "Context": "", + "Explanation": "torch.compile does not support strided NestedTensor", + "Hints": [] + } + ], + "GB0045": [ + { + "Gb_type": "Encountered tensor.is_inference() during tracing", + "Context": "", + "Explanation": "tensor.is_inference() is not supported", + "Hints": [ + "This graph break is fundamental - it is unlikely that Dynamo will ever be able to trace through your code. Consider finding a workaround." + ] + } + ], + "GB0046": [ + { + "Gb_type": "Encountered torch.is_inference_mode_enabled during tracing", + "Context": "", + "Explanation": "torch.is_inference_mode_enabled() is not supported", + "Hints": [ + "This graph break is fundamental - it is unlikely that Dynamo will ever be able to trace through your code. Consider finding a workaround." + ] + } + ], + "GB0047": [ + { + "Gb_type": "Encountered unconverted argument when attempting to inline", + "Context": "func: {func}, arg: {v}", + "Explanation": "An argument to an inlined function was not successfully converted to a VariableTracker.", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB5696": [ + { + "Gb_type": "Comprehension graph break with extra stack values", + "Context": "", + "Explanation": "Cannot use comprehension optimization when there are extra values on the stack and variables need to be passed to the resume function.", + "Hints": [] + } + ], + "GB0048": [ + { + "Gb_type": "Error getting associated real value", + "Context": "call_id {self}", + "Explanation": "Dynamo encountered an error while trying to get the associated real value.", + "Hints": [] + } + ], + "GB0049": [ + { + "Gb_type": "Error when attempting to resolve op packet", + "Context": "", + "Explanation": "str(e)", + "Hints": [] + } + ], + "GB0050": [ + { + "Gb_type": "Exception with bad expected type", + "Context": "str(expected_exc_types)", + "Explanation": "`except ...` has unsupported type {expected_exc_types}.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB4880": [ + { + "Gb_type": "missing_mp_subscript", + "Context": "mp_subscript_impl not defined for {type(self).__name__}", + "Explanation": "Dynamo does not yet support subscripting '{self.python_type_name()}'.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB9741": [ + { + "Gb_type": "skip frame due to being in functorh mode", + "Context": "", + "Explanation": "torch.compile cannot be run in context: {functorch_subclass_name}. Skipping frame.", + "Hints": [] + }, + { + "Gb_type": "skip frame due to being in functorch mode", + "Context": "", + "Explanation": "torch.compile cannot be run in context: {functorch_subclass_name}. Skipping frame.", + "Hints": [] + } + ], + "GB7000": [ + { + "Gb_type": "graph break in loop", + "Context": "frame skipped: {format_frame_info(code)}", + "Explanation": "torch.compile detected a graph break in a for/while loop. Skipping the frame and falling back to eager, as graph breaks in loops are not supported.", + "Hints": [ + "This graph break may have been caused by an earlier graph break. Resolving the earlier graph break may resolve this one." + ] + } + ], + "GB3326": [ + { + "Gb_type": "invalid ptr argument for create_tma_descriptor", + "Context": "args = {args}, kwargs = {kwargs}", + "Explanation": "Expected `ptr` argument of `create_{self.rank}d_tma_descriptor`to be from a `.data_ptr()` call, represented internally by `DataPtrVariable`", + "Hints": [ + "`torch.compile` may fail to internally represent result of `.data_ptr()` ", + "with `DataPtrVariable` due to a graph break between the `.data_ptr()` call and ", + "`create_{self.rank}d_tma_descriptor`. Please ensure there were no graph breaks ", + "between these two calls." + ] + } + ], + "GB4297": [ + { + "Gb_type": "Call to `torch._dynamo.skip_frame()`", + "Context": "Called `torch._dynamo.skip_frame()` with args `{args}`, kwargs `{kwargs}`. Skipping frame {format_frame_info(tx.f_code)}.", + "Explanation": "User-inserted skip frame. Message: {skip_frame_msg}", + "Hints": [ + "Remove the `torch._dynamo.skip_frame()` call." + ] + } + ], + "GB0051": [ + { + "Gb_type": "Exception with non-type expectation", + "Context": "str(expected_type)", + "Explanation": "`except ...` expects a non-type: {expected_type}.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0052": [ + { + "Gb_type": "Excessive RestartAnalysis() calls", + "Context": "", + "Explanation": "Dynamo attempted to trace the same frame 100+ times. Giving up on compiling as the compile time tradeoff is likely not worth the performance gain.", + "Hints": [] + } + ], + "GB0053": [ + { + "Gb_type": "FSDP with use_orig_params=False", + "Context": "", + "Explanation": "Dynamo only supports FSDP with use_orig_params=True", + "Hints": [] + } + ], + "GB5195": [ + { + "Gb_type": "elementwise_dtypes unsupported arg type", + "Context": "str(arg)", + "Explanation": "elementwise_dtypes requires tensor or constant arguments, got {type(arg).__name__}", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0054": [ + { + "Gb_type": "Failed to construct Enum variable", + "Context": "value: {value_vt}, allowed enum values: {list(cls_type)}", + "Explanation": "Attempted to construct an Enum value that is non-constant (e.g. int, string) or is not an acceptable value for the Enum. Acceptable values for Enum `{cls_type}`: {list(cls_type)}.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0055": [ + { + "Gb_type": "Failed to convert args/kwargs to proxy", + "Context": "call_function args: {typestr(*args)} {typestr(*list(kwargs.values()))}", + "Explanation": "Missing `as_proxy()` implementation for some arg/kwarg.", + "Hints": [] + } + ], + "GB0056": [ + { + "Gb_type": "Failed to mutate tensor data attribute", + "Context": "setattr({obj}, {name}, {val})", + "Explanation": "Dyanmo only supports mutating `.data` of tensor created outside `torch.compile` region", + "Hints": [ + "Don't mutate `.data` on this tensor, or move ", + "the mutation out of `torch.compile` region" + ] + } + ], + "GB0057": [ + { + "Gb_type": "Failed to raise exception", + "Context": "str(exc)", + "Explanation": "Attempted to raise a non-Exception type/value.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB7299": [ + { + "Gb_type": "_autograd_grad with lost grad_fn linkage", + "Context": "outputs lost autograd linkage during tracing", + "Explanation": "_autograd_grad() received tensors whose grad_fn was lost during tracing - this silently produces zero gradients.", + "Hints": [ + "Compile the full transform instead of the returned ", + "closure: torch.compile(lambda x: torch.func.vjp(f, x))", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0058": [ + { + "Gb_type": "Failed to set tensor attribute", + "Context": "setattr({obj}, {name}, {val})", + "Explanation": "Dyanmo doesn't support setting these tensor attributes", + "Hints": [ + "Don't mutate attribute '{name}' on tensors, or ", + "move the mutation out of `torch.compile` region" + ] + } + ], + "GB0059": [ + { + "Gb_type": "Failed to trace builtin operator", + "Context": "builtin {fn.__name__} {arg_types} {has_kwargs}", + "Explanation": "Dynamo does not know how to trace builtin operator `{fn.__name__}` with argument types {real_arg_types} (has_kwargs {has_kwargs})", + "Hints": [ + "Avoid calling builtin `{fn.__name__}` with argument types {real_arg_types}. ", + "Consider using an equivalent alternative function/method to `{fn.__name__}`.", + "If you are attempting to call a logging function (e.g. `print`), ", + "you can try adding it to `torch._dynamo.config.reorderable_logging_functions`.", + "Please report an issue to PyTorch." + ] + } + ], + "GB6270": [ + { + "Gb_type": "Unhandled tensor method", + "Context": "call_method {self} {name} {args} {kwargs}", + "Explanation": "Tensor method `{name}` is not defined on {check_type.__name__} and does not have an explicit handler in TensorVariable.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0060": [ + { + "Gb_type": "Failed to trace unittest method", + "Context": "function: unittest.TestCase.{name}", + "Explanation": "Dynamo does not know how to trace unittest method `{name}` ", + "Hints": [ + "Avoid calling `TestCase.{name}`. ", + "Please report an issue to PyTorch." + ] + } + ], + "GB8656": [ + { + "Gb_type": "Opaque object with custom __getattr__ not supported", + "Context": "{real_obj_type.__name__} with custom __getattr__", + "Explanation": "Dynamo does not support opaque objects types with custom __getattr__ methods", + "Hints": [] + } + ], + "GB0061": [ + { + "Gb_type": "Failed to unpack object for BUILD_LIST_UNPACK", + "Context": "str(seq)", + "Explanation": "{seq} cannot be unpacked into a list for the BUILD_LIST_UNPACK bytecode (`[*x, *y, ...]`).", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB4197": [ + { + "Gb_type": "Unsupported Tensor.share_memory_() call", + "Context": "call_method {self} share_memory_", + "Explanation": "Dynamo does not support Tensor.share_memory_() which modifies tensor storage for IPC", + "Hints": [ + "Move share_memory_() call outside the compiled region. ", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0062": [ + { + "Gb_type": "Failed to unpack object for UNPACK_EX", + "Context": "str(seq)", + "Explanation": "{seq} cannot be unpacked into a list for the UNPACK_EX bytecode.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0063": [ + { + "Gb_type": "Failed to unpack object for UNPACK_SEQUENCE", + "Context": "str(seq)", + "Explanation": "{seq} cannot be unpacked into a list for the UNPACK_SEQUENCE bytecode (i.e. `a, b, c = d`).", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0064": [ + { + "Gb_type": "Fake tensor propagation exception", + "Context": "str(e.reason)", + "Explanation": "msg", + "Hints": [] + } + ], + "GB4636": [ + { + "Gb_type": "Call to `torch._dynamo.step_unsupported()`", + "Context": "", + "Explanation": "User-inserted step_unsupported.", + "Hints": [ + "Remove the `torch._dynamo.step_unsupported()` call." + ] + } + ], + "GB0065": [ + { + "Gb_type": "Graph break in inlined function", + "Context": "", + "Explanation": "Graph breaks in an inlined call are not supported.", + "Hints": [] + } + ], + "GB0066": [ + { + "Gb_type": "Graph break under GenericContextWrappingVariable", + "Context": "Active generic context managers: {self.active_generic_context_managers}", + "Explanation": "Attempted to graph break in an active context manager(s) that doesn't support graph breaking.", + "Hints": [ + "Move the offending context manager(s) to outside the compiled region.", + "This graph break may have been caused by an earlier graph break. Resolving the earlier graph break may resolve this one." + ] + } + ], + "GB0067": [ + { + "Gb_type": "HOP: Unsafe side effect", + "Context": "Attempted to mutate {item}", + "Explanation": "Mutating a variable from outside the scope of this HOP is not supported.", + "Hints": [ + "If the HOP is activation checkpointing (torch.utils.checkpoint.checkpoint), this points to a ", + "side effect in forward method. Eager activation checkpointing replays that side-effect while ", + "recomputing the forward in the backward. If you are ok with side-effect not replayed in the ", + "backward, try setting `torch._dynamo.config.skip_fwd_side_effects_in_bwd_under_checkpoint = True`" + ] + }, + { + "Gb_type": "HOP: Unsafe side effect", + "Context": "Attempted to mutate {item}", + "Explanation": "Mutating a variable from outside the scope of this HOP is not supported.", + "Hints": [ + "If the HOP is activation checkpointing, try setting ", + "`torch._dynamo.config.skip_fwd_side_effects_in_bwd_under_checkpoint = True`" + ] + }, + { + "Gb_type": "HigherOrderOperator: Mutating a variable not in the current scope (SideEffects)", + "Context": "", + "Explanation": "This is not supported.", + "Hints": [] + } + ], + "GB0068": [ + { + "Gb_type": "Illegal method invocation in strict mode", + "Context": "call_method {self} {name} {args} {kwargs}", + "Explanation": "Dynamo currently does not support this method ({name}) invocation in strict mode.", + "Hints": [] + } + ], + "GB0069": [ + { + "Gb_type": "Import failure", + "Context": "module_name: {module_name}, fromlist: {fromlist}, level={level}", + "Explanation": "Failure when attempting to import.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0070": [ + { + "Gb_type": "Indexing list with non-scalar tensor", + "Context": "call_method {self} {name} {args} {kwargs}", + "Explanation": "Attempted to index list-like object with tensor with > 1 element.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0071": [ + { + "Gb_type": "Inline attempt with __self__", + "Context": "str(func)", + "Explanation": "Attempted to inline a function with the `__self__` attribute. Dynamo is expected to decompose method calls into function calls with a `self` argument.", + "Hints": [] + } + ], + "GB0072": [ + { + "Gb_type": "Inplace op on input tensor", + "Context": "", + "Explanation": "Attempted to trace an inplace view op on input tensor {typestr(self.value)}.", + "Hints": [ + "Ensure you do not modify input tensor in place.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB7230": [ + { + "Gb_type": "iter() with no arguments", + "Context": "iter()", + "Explanation": "iter() requires at least one argument", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + }, + { + "Gb_type": "iter() with no arguments", + "Context": "iter()", + "Explanation": "iter() requires at least one argument", + "Hints": [] + } + ], + "GB0073": [ + { + "Gb_type": "Invoking an nn.Module inside a HigherOrderOperator", + "Context": "", + "Explanation": "This is not supported.", + "Hints": [] + } + ], + "GB0074": [ + { + "Gb_type": "Invoking an nn.Module inside a higher order operator", + "Context": "Higher order op name: {self.source_target}", + "Explanation": "This is not supported.", + "Hints": [] + } + ], + "GB6012": [ + { + "Gb_type": "Cannot trace user-defined __len__", + "Context": "{self.python_type_name()}.__len__()", + "Explanation": "Dynamo cannot trace len() on {self.python_type_name()} because the __len__ method is either not traceable (e.g., defined in C or built-in) or returns a non-constant value.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0075": [ + { + "Gb_type": "LOAD_BUILD_CLASS bytecode not supported", + "Context": "", + "Explanation": "Dynamo does not support tracing classes that are defined in the compiled region.", + "Hints": [ + "Move the class definition out of the compiled region.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0076": [ + { + "Gb_type": "LOAD_FAST_CHECK on uninitialized variable", + "Context": "inst.argval", + "Explanation": "Attempted to load uninitialized local variable {inst.argval}", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0077": [ + { + "Gb_type": "Length mismatch when unpacking object for UNPACK_SEQUENCE", + "Context": "expected length: {inst.argval}, actual: {len(val)}", + "Explanation": "{seq} unpacked to a list for the UNPACK_SEQUENCE bytecode (i.e. `a, b, c = d`) with unexpected length.", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0078": [ + { + "Gb_type": "Limitation of `nonstrict_trace", + "Context": "{self}", + "Explanation": "msg", + "Hints": [ + "make sure definition of {fn_name} is outside ", + "`torch.compile` region" + ] + } + ], + "GB3775": [ + { + "Gb_type": "nb_int_impl not implemented", + "Context": "{type(self).__name__} has nb_int slot but no nb_int_impl override", + "Explanation": "The type {self.python_type_name()} has an nb_int C slot but the corresponding VariableTracker doesn't implement nb_int_impl.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0079": [ + { + "Gb_type": "Missing CALL_INTRINSIC_1 handler", + "Context": "CALL_INTRINSIC_1 operand: {inst.argval}", + "Explanation": "No handler implemented for CALL_INTRINSIC_1 {inst.argval} instruction.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB7596": [ + { + "Gb_type": "Indexing torch.Size with non-scalar tensor", + "Context": "get_item_dyn {self} {arg}", + "Explanation": "Attempted to index torch.Size with a tensor that is not a scalar constant.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0080": [ + { + "Gb_type": "Missing FakeTensor example value", + "Context": "str(node)", + "Explanation": "`FakeTensor` example value was required for {node} but not available.", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0081": [ + { + "Gb_type": "Missing attribute when running call_method node", + "Context": "", + "Explanation": "make_error_message(\"attribute not defined\")", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + }, + { + "Gb_type": "Missing attribute when running call_method node", + "Context": "", + "Explanation": "make_error_message(\"attribute not defined\")", + "Hints": [] + } + ], + "GB0082": [ + { + "Gb_type": "Missing bytecode handler", + "Context": "{opname} with args {args}", + "Explanation": "Dynamo does not know how to handle the bytecode instruction `{opname}`.", + "Hints": [ + "Do not trace code that produces the `{opname}` bytecode instruction ", + "(see https://docs.python.org/3/library/dis.html for bytecode semantics).", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0083": [ + { + "Gb_type": "Module-level backwards hooks require compiled autograd.", + "Context": "", + "Explanation": "", + "Hints": [ + "Enable compiled autograd by setting torch._dynamo.config.compiled_autograd = True." + ] + } + ], + "GB0084": [ + { + "Gb_type": "Non-constant attribute given to `super().__delattr__()`", + "Context": "call_method {self} {name}", + "Explanation": "Dynamo requires the attribute name passed to `super().__delattr__(...)` to be a constant (string).", + "Hints": [ + "Ensure the attribute name is a string literal or a constant variable." + ] + } + ], + "GB0085": [ + { + "Gb_type": "Non-function or method in subclass of torch.autograd.Function", + "Context": "call_apply {self} {args} {kwargs}", + "Explanation": "Dynamo requires the `forward` attribute of a `torch.autograd.Function` subclass to be a standard Python function or method. Found type `{type(fn).__name__}` instead.", + "Hints": [ + "Ensure the `forward` method is defined as a regular ", + "function or instance method." + ] + } + ], + "GB0086": [ + { + "Gb_type": "Not a Python constant", + "Context": "guard_as_python_constant {self}", + "Explanation": "Failed to convert {self} into a Python constant.", + "Hints": [] + } + ], + "GB0087": [ + { + "Gb_type": "NotImplementedError/UnsupportedFakeTensorException when running FX node", + "Context": "", + "Explanation": "make_error_message(e)", + "Hints": [] + } + ], + "GB0088": [ + { + "Gb_type": "Observed exception", + "Context": "raised exception {curr_exc.debug_repr()}", + "Explanation": "observed_exn_gb_explanation", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + }, + { + "Gb_type": "Observed exception", + "Context": "raised exception {curr_exc.python_type_name()}({curr_exc.args})", + "Explanation": "observed_exn_gb_explanation", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0089": [ + { + "Gb_type": "Observed exception (EXCEPT_HANDLER)", + "Context": "str(raised_exception)", + "Explanation": "observed_exn_gb_explanation + \" This graph break is unexpected.\"", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0090": [ + { + "Gb_type": "Operator does not support running with fake tensors", + "Context": "unsupported operator: {cause.func}", + "Explanation": "", + "Hints": [ + "{import_suggestion}see ", + "https://docs.google.com/document/d/1GgvOe7C8_NVOMLOCwDaYV1mXXyHMXY7ExoewHqooxrs/edit#heading=h.64r4npvq0w0", + " for how to fix" + ] + } + ], + "GB0091": [ + { + "Gb_type": "Read uninitialized cell", + "Context": "str(cellvar)", + "Explanation": "Attempted to read a cell variable that has not been populated yet.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0092": [ + { + "Gb_type": "Reconstruction failure", + "Context": "str(value)", + "Explanation": "Dynamo has no bytecode reconstruction implemented for sourceless variable {value}.", + "Hints": [ + "If Dynamo is attempting to trace a return statement and your code is attempting to return a variable ", + "that Dynamo cannot reconstruct, then remove it from the return statement.", + "Report an issue to PyTorch if you need reconstrtuction support. Note that objects that don't have ", + "reconstruction rules may be fundamentally unreconstructable.", + "This graph break may have been caused by an earlier graph break. Resolving the earlier graph break may resolve this one." + ] + } + ], + "GB0093": [ + { + "Gb_type": "Reconstruction failure: source.reconstruct not implemented", + "Context": "str(source)", + "Explanation": "Dynamo has no bytecode reconstruction implemented for {type(source)} variable {source}.", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0094": [ + { + "Gb_type": "SEND with bad type", + "Context": "TOS type: {typestr(tos)}", + "Explanation": "Attempted to SEND with unsupported type {typestr(tos)}.", + "Hints": [] + } + ], + "GB0095": [ + { + "Gb_type": "Set Exception object `__traceback__` attribute to not-`None`", + "Context": "call_setattr {self} {name}", + "Explanation": "Dynamo does not support setting the attribute '__traceback__' on tracked exception objects to anything other than None.", + "Hints": [ + "Avoid setting '__traceback__' on exception objects ", + "within traced code, or set it to None." + ] + } + ], + "GB0096": [ + { + "Gb_type": "Should not compile partial graph (STORE_ATTR)", + "Context": "", + "Explanation": "Dynamo has determined when encountering an unsupported STORE_ATTR instruction (i.e. `obj.attr = val`) that it should not compile the partial graph.", + "Hints": [] + } + ], + "GB5748": [ + { + "Gb_type": "autograd.grad with external GradientEdge", + "Context": "context", + "Explanation": "torch.autograd.grad() cannot be used with GradientEdge inputs passed from outside the compiled region. The GradientEdge contains a reference to an autograd node that was created before torch.compile started tracing, so Dynamo cannot trace through its computation.", + "Hints": [ + "Move the autograd.grad() call outside the torch.compile region.", + "Or use tensor inputs directly instead of GradientEdge objects.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + }, + { + "Gb_type": "autograd.grad with external GradientEdge", + "Context": "", + "Explanation": "torch.autograd.grad() cannot be used with GradientEdge inputs passed from outside the compiled region. The GradientEdge contains an autograd node that was created outside the compiled region boundary.", + "Hints": [ + "Move the autograd.grad() call outside the torch.compile region.", + "Or use tensor inputs directly instead of GradientEdge objects.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0097": [ + { + "Gb_type": "Side effect on existing deque with limited maxlen", + "Context": "", + "Explanation": "This is not supported.", + "Hints": [ + "Don't use a deque with `maxlen` specified." + ] + } + ], + "GB0098": [ + { + "Gb_type": "Skip calling `torch.compiler.disable()`d function", + "Context": "str(self.value)", + "Explanation": "Skip calling function `{self.value}` since it was wrapped with `torch.compiler.disable` (reason: {msg})", + "Hints": [ + "Remove the `torch.compiler.disable` call" + ] + } + ], + "GB0099": [ + { + "Gb_type": "Skip inlining `torch.compiler.disable()`d function", + "Context": "str(func.get_function())", + "Explanation": "Skip inlining function {func.get_function()} since it was wrapped with `torch.compiler.disable` (reason: {msg})", + "Hints": [ + "Remove the `torch.compiler.disable` call" + ] + } + ], + "GB0100": [ + { + "Gb_type": "Storing Tensor hook handle in globals", + "Context": "name", + "Explanation": "This is not supported.", + "Hints": [] + } + ], + "GB0101": [ + { + "Gb_type": "Storing Tensor hook handle in globals (inline call)", + "Context": "inst.argval", + "Explanation": "This is not supported.", + "Hints": [] + } + ], + "GB0102": [ + { + "Gb_type": "Strict mode banned op", + "Context": "var_getattr {self} {name}", + "Explanation": "Getattr invocation '{name}' in strict mode is not supported.", + "Hints": [ + "Remove `{name}` from the list of banned ops by ", + "setting `torch._dynamo.config._autograd_backward_strict_mode_banned_ops`." + ] + } + ], + "GB0103": [ + { + "Gb_type": "Tensor subclass overridden method call", + "Context": "{name}", + "Explanation": "`torch.compile` currently can't trace this", + "Hints": [ + "Avoid calling {name} of tensor subclass in torch.compile region", + "Renaming method `{name}` of type {self.class_type}", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0104": [ + { + "Gb_type": "Tensor with grad_fn()", + "Context": "var_getattr {self} grad_fn", + "Explanation": "Dynamo does not support tracing tensors with a grad_fn directly.", + "Hints": [] + } + ], + "GB0105": [ + { + "Gb_type": "Tensor.numpy() with trace_numpy=False", + "Context": "call_method {self} numpy", + "Explanation": "`Tensor.numpy()` was called, but the `trace_numpy` configuration was manually disabled.", + "Hints": [ + "Set `torch._dynamo.config.trace_numpy = True` to allow ", + "Dynamo to trace through NumPy." + ] + } + ], + "GB0106": [ + { + "Gb_type": "Tensor.numpy() without NumPy installed", + "Context": "call_method {self} numpy", + "Explanation": "`Tensor.numpy()` was called, but the NumPy library is not available in the current environment.", + "Hints": [ + "Ensure NumPy is installed in your Python environment." + ] + } + ], + "GB0107": [ + { + "Gb_type": "Tensor.random_ op", + "Context": "Tensor.{name}(args={args}, kwargs={kwargs})", + "Explanation": "This is currently not supported.", + "Hints": [ + "Use the out-of-place version of this op", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0108": [ + { + "Gb_type": "Tensor.retain_grad() with AOTDispatcher", + "Context": "var_getattr {self} retain_grad", + "Explanation": "`Tensor.retain_grad()` does not work with AOTDispatcher.", + "Hints": [] + } + ], + "GB0109": [ + { + "Gb_type": "Tensor.tolist() with non-integer tensor", + "Context": "call_method {self} to_list", + "Explanation": "Dynamo currently does not support tracing `tolist()` on non-integer tensors.", + "Hints": [ + "Ensure the input tensor to `tolist()` is an integer ", + "type (e.g., int8, int16, int32, int64)." + ] + } + ], + "GB0110": [ + { + "Gb_type": "Tensor.uniform_ op called with `from` keyword", + "Context": "Tensor.{name}(args={args}, kwargs={kwargs})", + "Explanation": "This is currently not supported.", + "Hints": [ + "Avoid using the `from` keyword.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0111": [ + { + "Gb_type": "TypeError from user code", + "Context": "call_function({self.value}, {args}, {kwargs})", + "Explanation": "msg", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0112": [ + { + "Gb_type": "TypeError when making fake tensor call", + "Context": "TypeError {node.target}: {cause}", + "Explanation": "", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + }, + { + "Gb_type": "TypeError when making fake tensor call", + "Context": "TypeError {node.target}: {cause}", + "Explanation": "", + "Hints": [] + } + ], + "GB0113": [ + { + "Gb_type": "Unable to resolve super getattr", + "Context": "", + "Explanation": "Dynamo failed to trace attribute `{name}` accessed via `super()` (for type `{self.typevar}` and object `{self.objvar}`) because the resolved attribute type is not supported.", + "Hints": [ + "Ensure the attribute exists in the parent class.", + "Check the arguments passed to `super()`." + ] + } + ], + "GB0114": [ + { + "Gb_type": "Unexpected failure during itertools.accumulate() iteration", + "Context": "call_function {self} {args} {kwargs}", + "Explanation": "Unexpected failure in invoking function during accumulate. Failed running func {func}({item}{acc})", + "Hints": [ + "This graph break may be difficult to debug. Please report an issue to PyTorch for assistance." + ] + } + ], + "GB0115": [ + { + "Gb_type": "Unexpected failure during itertools.groupby() iteration", + "Context": "call_function {self} {args} {kwargs}", + "Explanation": "Unexpected failure in invoking function during groupby", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB4778": [ + { + "Gb_type": "nested function with non-constructible closure in output", + "Context": "as_python_constant for out_spec {out_spec}", + "Explanation": "Cannot return a nested function with closure from a compiled function. Dynamo failed to construct the function defined in the compiled region with closure objects.", + "Hints": [ + "Define the function at module scope instead of inside another function ", + "Ensure that all closure variables are constants." + ] + }, + { + "Gb_type": "nested function with closure in output", + "Context": "as_python_constant for out_spec", + "Explanation": "Cannot return a nested function with closure from a compiled function. Dynamo cannot reconstruct the function.", + "Hints": [ + "Define the function at module scope instead of inside another function" + ] + } + ], + "GB0116": [ + { + "Gb_type": "Unexpected type in sourceless builder", + "Context": "{value_type.__module__}.{value_type.__qualname__}", + "Explanation": "SourcelessBuilder.create does not know how to wrap {value_type}", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0117": [ + { + "Gb_type": "Unhandled args for method", + "Context": "call_method {self} {name} {args} {kwargs}", + "Explanation": "Dynamo encountered an error while calling the method `{name}`.", + "Hints": [] + } + ], + "GB7650": [ + { + "Gb_type": "torch.Generator method", + "Context": "torch.Generator.{name}", + "Explanation": "torch.Generator.{name}() is a stateful RNG operation that cannot be soundly traced in the FX graph.", + "Hints": [ + "This graph break is fundamental - it is unlikely that Dynamo will ever be able to trace through your code. Consider finding a workaround." + ] + } + ], + "GB0118": [ + { + "Gb_type": "Unimplemented next() call", + "Context": "next({self})", + "Explanation": "This abstract method must be implemented", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0119": [ + { + "Gb_type": "Uninitialized nn.Module", + "Context": "typestr(value)", + "Explanation": "Attempted to trace an uninitialized nn.Module of type {typestr(value)}.", + "Hints": [ + "Ensure your nn.Module instance has called `super().__init__()`.", + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0120": [ + { + "Gb_type": "Unreachable sub-generator code", + "Context": "", + "Explanation": "Should only be encountered while implementing generator support.", + "Hints": [] + } + ], + "GB0121": [ + { + "Gb_type": "UnspecializedNNModuleVariable missing method", + "Context": "call_method: {self} {name} {args} {kwargs}", + "Explanation": "Dynamo does not support tracing method {name} of nn.Module {self.value}", + "Hints": [ + "Dynamo does not really define unspecialized nn.Module very well.", + "This graph break may be difficult to debug. Please report an issue to PyTorch for assistance." + ] + } + ], + "GB0122": [ + { + "Gb_type": "Unsupported SourceType", + "Context": "MutationType.__init__ {self} {typ}", + "Explanation": "Dynamo does not support the type `{typ}`", + "Hints": [ + "This branch is not supposed to be reachable.", + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0123": [ + { + "Gb_type": "Unsupported Tensor.backward() call", + "Context": "call_method {self} backward {gradient} {retain_graph} {create_graph} {inputs}", + "Explanation": "Dynamo currently does not support tracing `Tensor.backward()` when trace_autograd_ops is off.", + "Hints": [ + "Set torch._dynamo.trace_autograd_ops=True" + ] + }, + { + "Gb_type": "Unsupported Tensor.backward() call", + "Context": "call_method {self} backward {gradient} {retain_graph} {create_graph} {inputs}", + "Explanation": "Dynamo currently does not support tracing `Tensor.backward()`.", + "Hints": [ + "This graph break is fundamental - it is unlikely that Dynamo will ever be able to trace through your code. Consider finding a workaround." + ] + } + ], + "GB4695": [ + { + "Gb_type": "torch.jit.script/freeze modules unsupported", + "Context": "str(value)", + "Explanation": "Dynamo does not support tracing into torch.jit.script or torch.jit.freeze modules because they execute in the TorchScript runtime, not Python. Replace the ScriptModule submodule with the original eager nn.Module.", + "Hints": [ + "This graph break is fundamental - it is unlikely that Dynamo will ever be able to trace through your code. Consider finding a workaround." + ] + } + ], + "GB0124": [ + { + "Gb_type": "Unsupported Tensor.item() call with capture_scalar_outputs=False", + "Context": "call_method {self} item {args} {kwargs}", + "Explanation": "Dynamo does not support tracing `Tensor.item()` with config.capture_scalar_outputs=False.", + "Hints": [ + "Set `torch._dynamo.config.capture_scalar_outputs = True` ", + "or `export TORCHDYNAMO_CAPTURE_SCALAR_OUTPUTS=1` ", + "to include these operations in the captured graph." + ] + } + ], + "GB0125": [ + { + "Gb_type": "Unsupported Tensor.requires_grad_() call", + "Context": "call_method {self} requires_grad_", + "Explanation": "Dynamo does not support changes to a Tensor's `requires_grad` through calling `requires_grad_()`.", + "Hints": [] + } + ], + "GB0126": [ + { + "Gb_type": "Unsupported Tensor.resize_() call", + "Context": "call_method {self} resize_ {args} {kwargs}", + "Explanation": "Dynamo currently does not support tracing `Tensor.resize_()`.", + "Hints": [] + } + ], + "GB9528": [ + { + "Gb_type": "Attempted to reconstruct a nested function with closure which is not supported", + "Context": "str(self.fn_name)", + "Explanation": "Shouldn't reconstruct {str(self.fn_name)}, a nested function with closure", + "Hints": [ + "Define the function at module level" + ] + } + ], + "GB0127": [ + { + "Gb_type": "Unsupported Tensor.resize_as_() call", + "Context": "call_method {self} resize_as_ {args} {kwargs}", + "Explanation": "Dynamo currently does not support tracing `Tensor.resize_as_()`.", + "Hints": [] + } + ], + "GB9186": [ + { + "Gb_type": "using `torch.autograd.grad` with `torch._dynamo.config.trace_autograd_ops=False`", + "Context": "trace_autograd_ops={config.trace_autograd_ops}", + "Explanation": "Attempted to call `torch.autograd.grad` with config `torch._dynamo.config.trace_autograd_ops` set to `False`.", + "Hints": [ + "Change `torch._dynamo.config.trace_autograd_ops` to `True`." + ] + } + ], + "GB0128": [ + { + "Gb_type": "Unsupported Tensor.set_() call", + "Context": "call_method {self} set_ {args} {kwargs}", + "Explanation": "Dynamo currently does not support tracing `Tensor.set_()` overloads that include more than one argument.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0129": [ + { + "Gb_type": "Unsupported Tensor.sparse_resize_() call", + "Context": "call_method {self} sparse_resize_ {args} {kwargs}", + "Explanation": "Dynamo currently does not support tracing `Tensor.sparse_resize_()`.", + "Hints": [] + } + ], + "GB0130": [ + { + "Gb_type": "Unsupported Tensor.sparse_resize_and_clear_() call", + "Context": "call_method {self} sparse_resize_and_clear_ {args} {kwargs}", + "Explanation": "Dynamo currently does not support tracing `Tensor.sparse_resize_and_clear_()`.", + "Hints": [] + } + ], + "GB0131": [ + { + "Gb_type": "Unsupported __setitem__/__setattr__ inline attempt", + "Context": "code name: {code.co_name}, args: {args}", + "Explanation": "Attempted to inline {code.co_name} where first argument (self) is not a user-defined object.", + "Hints": [] + } + ], + "GB0132": [ + { + "Gb_type": "Unsupported `func` in itertools.accumulate", + "Context": "call_function {self} {args} {kwargs}", + "Explanation": "Dynamo does not know how to get the function to use for itertools.accumulate. itertools.accumulate expects the `func` as the second argument or as a keyword argument.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB4026": [ + { + "Gb_type": "triton.set_allocator not supported", + "Context": "triton.set_allocator called inside compiled region", + "Explanation": "triton.set_allocator is not supported inside torch.compile. It modifies global Triton allocator state and cannot be traced.", + "Hints": [ + "Move triton.set_allocator() outside of the torch.compile region ", + "(call it before the compiled function)." + ] + } + ], + "GB0133": [ + { + "Gb_type": "Unsupported arguments for itertools.accumulate", + "Context": "call_function {self} {args} {kwargs}", + "Explanation": "Dynamo does not know how to trace itertools.accumulate with args: {args} and kwargs: {kwargs}. itertools.accumulate expects an iterable, an optional binary function for accumulation, and an optional initial value to set the starting state.", + "Hints": [ + "Make sure the arguments to itertools.accumulate are correct.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0134": [ + { + "Gb_type": "Unsupported arguments for itertools.groupby", + "Context": "call_function {self} {args} {kwargs}", + "Explanation": "Dynamo does not know how to trace itertools.groupby with args: {args} and kwargs: {kwargs}. itertools.groupby expects an iterable to group and an optional key function to determine groupings.", + "Hints": [ + "Make sure the arguments to itertools.groupby are correct.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0135": [ + { + "Gb_type": "Unsupported attribute assignment on Exception object", + "Context": "call_setattr {self} {name}", + "Explanation": "Dynamo does not support setting the attribute '{name}' on tracked exception objects. Only `__context__`, `__cause__`, `__suppress_context__`, and `__traceback__` are supported.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0136": [ + { + "Gb_type": "Unsupported attribute for range() object", + "Context": "var_getattr {self} {name}", + "Explanation": "Expected attribute to be one of {','.join(fields)} but got {name}", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0137": [ + { + "Gb_type": "Unsupported attribute for slice() object", + "Context": "var_getattr {self} {name}", + "Explanation": "Expected attribute to be one of {','.join(fields)} but got {name}", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0138": [ + { + "Gb_type": "Unsupported autograd.Function context `save_for_backward`", + "Context": "call_method {self} {name}", + "Explanation": "Dynamo requires the `saved_tensors` attribute to be initialized on the `autograd.Function` context object.", + "Hints": [ + "Ensure that the `saved_tensors` attribute is properly ", + "initialized before calling `save_for_backward`. ", + "`save_for_backward` only supported on a newly constructed `torch.autograd.function.FunctionCtx`." + ] + } + ], + "GB0139": [ + { + "Gb_type": "Unsupported autograd.Function context method", + "Context": "call_method {self} {name}", + "Explanation": "Dynamo does not support calling the method `{name}` on `autograd.Function` context objects. Supported methods are `__setattr__`, `save_for_backward` and `mark_non_differentiable`.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0140": [ + { + "Gb_type": "Unsupported autograd.Function method", + "Context": "call_method {self} {name}", + "Explanation": "Dynamo does not support calling the method `{name}` directly on the `torch.autograd.Function` instance. Supported methods include `apply`, `backward`, static methods, and class methods.", + "Hints": [ + "Ensure the method is decorated with `@staticmethod` ", + "or `@classmethod` if it's meant to be called on the class." + ] + } + ], + "GB1380": [ + { + "Gb_type": "Illegal __getitem__ invocation in strict mode", + "Context": "mp_subscript_impl {self} {key}", + "Explanation": "Dynamo currently does not support __getitem__ invocation in strict mode.", + "Hints": [] + } + ], + "GB4240": [ + { + "Gb_type": "unsupported __getitem__", + "Context": "mp_subscript_impl {self} {key}", + "Explanation": "Dynamo does not know how to handle __getitem__ on {self}", + "Hints": [] + } + ], + "GB0141": [ + { + "Gb_type": "Unsupported call_id() without source", + "Context": "call_id {self}", + "Explanation": "call_id() not supported for sourceless TensorVariable.", + "Hints": [] + } + ], + "GB0142": [ + { + "Gb_type": "Unsupported context manager", + "Context": "Attempted SETUP_WITH/BEFORE_WITH/LOAD_SPECIAL on {ctx}", + "Explanation": "Dynamo does not know how to enter a `{ctx.python_type_name()}` context manager.", + "Hints": [ + "Avoid using the unsupported context manager.", + "If the context manager seems like it should be supported (e.g. torch.set_grad_enabled), then ", + "it may be the case that it was created outside the compiled region, which Dynamo does not support. ", + "Supported context managers can cross graph break boundaries only if they are local non-closure ", + "variables, or are intermediate values.", + "File an issue to PyTorch. Simple context managers can potentially be supported, ", + "but note that context managers can't be supported in general" + ] + }, + { + "Gb_type": "Unsupported context manager", + "Context": "Attempted SETUP_WITH/BEFORE_WITH on {ctx}", + "Explanation": "Dynamo does not know how to enter a `{ctx.python_type_name()}` context manager.", + "Hints": [ + "Avoid using the unsupported context manager.", + "If the context manager seems like it should be supported (e.g. torch.set_grad_enabled), then ", + "it may be the case that it was created outside the compiled region, which Dynamo does not support. ", + "Supported context managers can cross graph break boundaries only if they are local non-closure ", + "variables, or are intermediate values.", + "File an issue to PyTorch. Simple context managers can potentially be supported, ", + "but note that context managers can't be supported in general" + ] + } + ], + "GB0143": [ + { + "Gb_type": "Unsupported conversion for slice assignment", + "Context": "call_method {self} {name} {args}", + "Explanation": "Missing dynamo support for converting {value} into a list for slice assignment.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0144": [ + { + "Gb_type": "Unsupported custom jvp", + "Context": "call_apply {self} {args} {kwargs}", + "Explanation": "Dynamo does not support tracing `torch.autograd.Function` subclasses that define a custom `jvp` method.", + "Hints": [ + "Remove the custom `jvp` method if possible.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0145": [ + { + "Gb_type": "Unsupported custom vjp", + "Context": "call_apply {self} {args} {kwargs}", + "Explanation": "Dynamo does not support tracing `torch.autograd.Function` subclasses that define a custom `vjp` method.", + "Hints": [ + "Remove the custom `vjp` method if possible.", + "Use standard `backward` instead if applicable.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0146": [ + { + "Gb_type": "Unsupported event method", + "Context": "str(name)", + "Explanation": "Dynamo doesn't support tracing the {method_name} method. We currently support wait, record, synchronize, and query.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0147": [ + { + "Gb_type": "Unsupported function call", + "Context": "call_function {self} {args} {kwargs}", + "Explanation": "Dynamo does not know how to trace the function `{self.debug_repr()}`", + "Hints": [ + "Avoid calling `{self.debug_repr()}` in your code.", + "Please report an issue to PyTorch." + ] + } + ], + "GB0148": [ + { + "Gb_type": "Unsupported function call (delayed)", + "Context": "source: {self.source}", + "Explanation": "Dynamo determined that a graph break should occur when calling `{name}`. Reason: {self.msg}", + "Hints": [] + }, + { + "Gb_type": "Unsupported function call (delayed)", + "Context": "source: {self.source}", + "Explanation": "Dynamo determined that a graph break should occur when calling `{self.source.name}`. Reason: {self.msg}", + "Hints": [] + } + ], + "GB9672": [ + { + "Gb_type": "unsupported type.__dict__['{attr_name}'].__get__ call", + "Context": "call_function {self}, args: {args}, kwargs: {kwargs}", + "Explanation": "`torch.compile` only supports calling type.__dict__['{attr_name}'].__get__ on a single constant argument (i.e. a type).", + "Hints": [ + "Make sure your call to type.__dict__['{attr_name}'].__get__ only has ", + "one positional argument (no keyword arguments).", + "Make sure the argument to type.__dict__['{attr_name}'].__get__ is a constant ", + "(i.e. type). For example, `object`, `int`, `MyCustomClass`.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0149": [ + { + "Gb_type": "Unsupported functorch tracing attempt", + "Context": "", + "Explanation": "msg", + "Hints": [] + } + ], + "GB0150": [ + { + "Gb_type": "Unsupported hasattr call", + "Context": "call_obj_hasattr {self} {name}", + "Explanation": "Dynamo does not know how to trace the function `{self.debug_repr()}`", + "Hints": [ + "Avoid calling `hasattr({self.__class__.__name__}, {name})` in your code.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0151": [ + { + "Gb_type": "Unsupported inspect call", + "Context": "inspect_parameter_names {self}", + "Explanation": "Dynamo does not know how to trace the function `{self.debug_repr()}`", + "Hints": [] + } + ], + "GB0152": [ + { + "Gb_type": "Unsupported key type for itertools.groupby", + "Context": "call_function {self} {args} {kwargs}", + "Explanation": "Dynamo does not know how to trace itertools.groupby with key type: {str(type(key))}. We only support grouping keys that are constants (int, float, str, etc.)", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0153": [ + { + "Gb_type": "Unsupported key type for nn.Module.__getitem__", + "Context": "mp_subscript_impl: {self} {key}", + "Explanation": "Dynamo does not support getitem on `nn.Module` with non-constant key.", + "Hints": [] + }, + { + "Gb_type": "Unsupported key type for nn.Module.__getitem__", + "Context": "call_method: {self} {name} {args} {kwargs}", + "Explanation": "Dynamo does not support getitem on `nn.Module` with non-constant key.", + "Hints": [] + } + ], + "GB0154": [ + { + "Gb_type": "Unsupported kwargs for itertools.accumulate", + "Context": "call_function {self} {args} {kwargs}", + "Explanation": "Expected kwargs: 'initial', 'func', but got {','.join(set(kwargs.keys()) - {'initial', 'func'})}", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0155": [ + { + "Gb_type": "Unsupported kwargs for itertools.groupby", + "Context": "call_function {self} {args} {kwargs}", + "Explanation": "Expected kwargs: 'key', but got {','.join(set(kwargs.keys()) - {'key'})}", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0156": [ + { + "Gb_type": "Unsupported method call", + "Context": "call_method {self} {name} {args} {kwargs}", + "Explanation": "Dynamo does not know how to trace method `{name}` of class `{self.python_type_name()}`", + "Hints": [] + } + ], + "GB0157": [ + { + "Gb_type": "Unsupported ndarray attribute access", + "Context": "var_getattr {self} {name}", + "Explanation": "Dynamo currently does not support tracing `ndarray.{name}`.", + "Hints": [] + } + ], + "GB0158": [ + { + "Gb_type": "Unsupported ndarray method call", + "Context": "call_method {self} {name} {args} {kwargs}", + "Explanation": "`ndarray.{name}()` is not modelled in `torch._numpy`.", + "Hints": [] + } + ], + "GB0159": [ + { + "Gb_type": "Unsupported ndarray.__version__ access", + "Context": "var_getattr {self} {name}", + "Explanation": "Dynamo currently does not support tracing `ndarray.{name}`.", + "Hints": [] + } + ], + "GB0160": [ + { + "Gb_type": "Unsupported next() call", + "Context": "next({self})", + "Explanation": "Dynamo does not know how to trace calling `next()` on variable `{self}`.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0161": [ + { + "Gb_type": "Unsupported nn.Module attribute type", + "Context": "nn.Module subclass: {typestr(base)}, name: {name}, attribute type: {typestr(subobj)}", + "Explanation": "Dynamo does not support tracing nn.Module attributes of type `{typestr(subobj)}`", + "Hints": [ + "Refactor your code so that `{name}` (type `{typestr(subobj)}`) is not an attribute of `{typestr(base)}`", + "Currently supported attribute types are methods, classmethods, staticmethods, ", + "properties, constants, and tensors.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0162": [ + { + "Gb_type": "Unsupported super().__init__() call", + "Context": "call_method {self} {name} {args} {kwargs}", + "Explanation": "Dynamo encountered a super().__init__() call on {objvar} that resolved to a `torch.nn.Module.__init__()` call that we cannot trace.", + "Hints": [ + "This graph break may be difficult to debug. Please report an issue to PyTorch for assistance." + ] + } + ], + "GB2533": [ + { + "Gb_type": "reconstructing @contextmanager object", + "Context": "object: {value}", + "Explanation": "Returning a @contextmanager object from a compiled function is not supported.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB7312": [ + { + "Gb_type": "failed to bind arguments when attempting to inline", + "Context": "func='{func.get_name()}' {func.get_filename()}:{func.get_code().co_firstlineno}; args = {[arg.python_type() for arg in args]}; kwargs = {kwargs}", + "Explanation": "Argument mismatch when attempting to trace function {func.get_name()}.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB4841": [ + { + "Gb_type": "Unsupported python_type() call", + "Context": "{obj} does not implement python_type()", + "Explanation": "This VariableTracker does not implement python_type(), which is required for object protocol operations.", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB8335": [ + { + "Gb_type": "Generator reconstruction with mutations", + "Context": "mutating object: {item}", + "Explanation": "Cannot reconstruct a generator with variable mutations. Dynamo needs to fully exhaust the generator, which may cause unintended variable modifications.", + "Hints": [ + "Remove mutations from the generator.", + "This graph break is fundamental - it is unlikely that Dynamo will ever be able to trace through your code. Consider finding a workaround." + ] + } + ], + "GB2549": [ + { + "Gb_type": "infinite generator detected", + "Context": "", + "Explanation": "Dynamo traced the YIELD_VALUE bytecode too many times. This could mean that we have attempted to trace an infinite generator.", + "Hints": [ + "If you are sure that your generator is not infinite, please report a bug at {PT2_ISSUE_TRACKER_URL}.", + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB1177": [ + { + "Gb_type": "nn.Module mutation in HigherOrderOp", + "Context": "nn.Module: {obj}", + "Explanation": "Inplace modifying nn.Module params/buffers inside HigherOrderOps is not allowed.", + "Hints": [ + "Remove the mutation or move it outside of the HigherOrderOp.", + "This graph break is fundamental - it is unlikely that Dynamo will ever be able to trace through your code. Consider finding a workaround." + ] + } + ], + "GB6210": [ + { + "Gb_type": "leaf_function: nn.Module argument without source", + "Context": "module type: {type(arg.value).__name__}", + "Explanation": "leaf_function received an nn.Module argument that cannot be traced back to its origin. This typically happens when the module is created dynamically inside the compiled region.", + "Hints": [ + "Ensure the nn.Module is created outside the compiled function ", + "and passed as an argument.", + "If the module is a class attribute, access it via self.module_name." + ] + } + ], + "GB0163": [ + { + "Gb_type": "Unsupported tensor subclass attribute access", + "Context": "{name}", + "Explanation": "`torch.compile` currently can't trace this", + "Hints": [ + "Avoid accessing {name} of tensor subclass in torch.compile region", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0164": [ + { + "Gb_type": "Unsupported tensor subclass overridden attribute access", + "Context": "{name}", + "Explanation": "`torch.compile` only support tracing certain types of overridden tensor subclass attributes", + "Hints": [ + "Avoid accessing {name} of tensor subclass in torch.compile region", + "Renaming attribute `{name}` of type {self.class_type}", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0165": [ + { + "Gb_type": "Unsupported torch._C._ImperativeEngine method", + "Context": "call_method {self} {name}", + "Explanation": "Dynamo only supports the `queue_callback` method on a torch._C._ImperativeEngine instance, but found: `{name}`.", + "Hints": [] + } + ], + "GB6276": [ + { + "Gb_type": "Comprehension analysis failed: No END_FOR", + "Context": "", + "Explanation": "Could not find END_FOR instruction in comprehension bytecode.", + "Hints": [] + } + ], + "GB9898": [ + { + "Gb_type": "Comprehension analysis failed: No matches", + "Context": "pre_store_ops={pre_store_ops}, post_store_op={post_store_op}", + "Explanation": "Comprehension does not match any known bytecode pattern.", + "Hints": [] + } + ], + "GB0166": [ + { + "Gb_type": "Unsupported torch._C._ImperativeEngine.queue_callback()", + "Context": "call_method {self} {name}", + "Explanation": "queue_callback() is only supported when Compiled Autograd is enabled with fullgraph=True.", + "Hints": [] + } + ], + "GB0167": [ + { + "Gb_type": "Variadic function call with bad args/kwargs type", + "Context": "args type: {typestr(argsvars)}, kwargs type: {typestr(kwargsvars)}", + "Explanation": "Expected args to be a list and kwargs to be a dict", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0168": [ + { + "Gb_type": "Variadic function call with bad flags", + "Context": "flags: {inst.argval}", + "Explanation": "Attempted to call a variadic function (CALL_FUNCTION_EX) with bad flags {inst.argval}", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0169": [ + { + "Gb_type": "Write to immutable cell", + "Context": "cellvar: {cellvar}, value: {value}", + "Explanation": "Dynamo doesn't support writing to immutable/sourceless cell variables.", + "Hints": [ + "This graph break may be difficult to debug. Please report an issue to PyTorch for assistance." + ] + } + ], + "GB0170": [ + { + "Gb_type": "Data-dependent branching", + "Context": "attempted to jump with {value}", + "Explanation": "\"Detected data-dependent branching (e.g. `if my_tensor.sum() > 0:`). \" \"Dynamo does not support tracing dynamic control flow.\" + trace_info", + "Hints": [] + }, + { + "Gb_type": "Data-dependent branching", + "Context": "attempted to jump with {value}", + "Explanation": "Detected data-dependent branching (e.g. `if my_tensor.sum() > 0:`). Dynamo does not support tracing dynamic control flow.", + "Hints": [ + "Use `torch.cond` to express dynamic control flow.", + "This graph break is fundamental - it is unlikely that Dynamo will ever be able to trace through your code. Consider finding a workaround." + ] + }, + { + "Gb_type": "Data-dependent branching", + "Context": "attempted to jump with {value}", + "Explanation": "_explanation", + "Hints": [ + "Use `torch.cond` to express dynamic control flow.", + "This graph break is fundamental - it is unlikely that Dynamo will ever be able to trace through your code. Consider finding a workaround." + ] + }, + { + "Gb_type": "Data-dependent branching", + "Context": "attempted to jump with {value}", + "Explanation": "_explanation", + "Hints": [] + }, + { + "Gb_type": "_gb_type", + "Context": "attempted to jump with {value}", + "Explanation": "_explanation", + "Hints": [] + } + ], + "GB0171": [ + { + "Gb_type": "assert with non-string message", + "Context": "str(args)", + "Explanation": "Dynamo only supports asserts with string messages", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0172": [ + { + "Gb_type": "async_op=True for distributed collectives", + "Context": "{self.fn}, args={args}, kwargs={kwargs}", + "Explanation": "`torch.compile` doesn't support `async_op=True for {self.fn}", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0173": [ + { + "Gb_type": "backward_state does not support export", + "Context": "", + "Explanation": "Compiled autograd doesn't work with `torch.export`.", + "Hints": [] + } + ], + "GB0174": [ + { + "Gb_type": "bad args to builtin cast()", + "Context": "got args {args} {kwargs}", + "Explanation": "Dynamo expects exactly 2 args to builtin cast().", + "Hints": [ + "Ensure your call to cast() has exactly 2 arguments." + ] + } + ], + "GB0175": [ + { + "Gb_type": "builtin isinstance() cannot determine type of argument", + "Context": "isinstance({arg}, {isinstance_type_var})", + "Explanation": "Dynamo doesn't have a rule to determine the type of argument {arg}", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + }, + { + "Gb_type": "builtin isinstance() cannot determine type of argument", + "Context": "isinstance({arg}, {isinstance_type})", + "Explanation": "Dynamo doesn't have a rule to determine the type of argument {arg}", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0176": [ + { + "Gb_type": "call_id() without associated real value", + "Context": "call_id {self}", + "Explanation": "Dynamo could not find an associated real value for the tensor.", + "Hints": [] + } + ], + "GB0177": [ + { + "Gb_type": "can't handle functions not implemented in python ", + "Context": "{fn}", + "Explanation": "Dynamo can only handle functions defined in python", + "Hints": [ + "Move usage of this function out of `torch.compile` region", + "Avoid using `tensor.is_inference()` and `torch.is_inference_mode_enabled()` in your compile code. This is primarily used in conjunction with `torch.inference_mode`. Consider using `torch.no_grad` instead because `torch.no_grad` leads to same improvements as `inference_mode` when `torch.compile` is used." + ] + } + ], + "GB0178": [ + { + "Gb_type": "constant fold exception", + "Context": "attempted to run function {fn} with arguments {args}", + "Explanation": "Encountered exception when attempting to constant fold.", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0179": [ + { + "Gb_type": "copy.deepcopy()", + "Context": "copy.deepcopy({x})", + "Explanation": "Dynamo does not support copy.deepcopy()", + "Hints": [ + "Avoid calling copy.deepcopy()", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0180": [ + { + "Gb_type": "dataclass fields failure", + "Context": "obj: {obj}; variable type: {type(obj)}", + "Explanation": "Dataclass fields handling fails for {obj}. Expected it to be a user-defined object.", + "Hints": [] + } + ], + "GB0181": [ + { + "Gb_type": "dtype mismatch between tensor and its gradient", + "Context": "tensor dtype: {value.dtype}; grad dtype: {grad_str}", + "Explanation": "Inconsistent dtype between tensor and its gradient. This can happen in FSDP and crashes meta tensor creation.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + }, + { + "Gb_type": "dtype mismatch between tensor and its gradient", + "Context": "tensor dtype: {value.dtype}; grad dtype: {safe_grad(value).dtype}", + "Explanation": "Inconsistent dtype between tensor and its gradient. This can happen in FSDP and crashes meta tensor creation.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0182": [ + { + "Gb_type": "failed to broadcast when attempting Tensor comparison op", + "Context": "{op.__name__}({left}, {right})", + "Explanation": "Dynamo was unable to broad cast the arguments {left}, {right} when attempting to trace the comparison op {op.__name__}.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0183": [ + { + "Gb_type": "failed to call dict.fromkeys()", + "Context": "{user_cls.__name__}.fromkeys(): {args} {kwargs}", + "Explanation": "Failed to call {user_cls.__name__}.fromkeys() because arguments could not be automatically converted to a list, or some dict key is not hashable.", + "Hints": [ + "Manually convert the argument to a list.", + "Ensure all keys are hashable." + ] + } + ], + "GB3840": [ + { + "Gb_type": "leaf_function without fake_fn", + "Context": "fn={fn}", + "Explanation": "The leaf_function decorator requires a fake_fn to be provided, but none was found on the decorated function.", + "Hints": [ + "Ensure the function is decorated with @leaf_function(fake_fn)" + ] + } + ], + "GB0184": [ + { + "Gb_type": "failed to call str() on user defined object", + "Context": "str(arg)", + "Explanation": "User defined object has no __str__ or __repr__ method", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0185": [ + { + "Gb_type": "failed to convert numpy.ndarray to Tensor", + "Context": "str(value)", + "Explanation": "Exception encountered when attempting to convert numpy.ndarray to Tensor", + "Hints": [] + } + ], + "GB0186": [ + { + "Gb_type": "functools.partial() with non-literal keyword", + "Context": "non-literal keyword: {k}", + "Explanation": "functools.partial() expects literal/string keywords", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0187": [ + { + "Gb_type": "functools.wraps", + "Context": "{fn}", + "Explanation": "`torch.compile` can't trace `functools.wraps` on functions defined outside the compile region", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0188": [ + { + "Gb_type": "getattr with no source", + "Context": "var_getattr {self} {name}", + "Explanation": "Dynamo does not know how to access an attribute on an `nn.Module` instance that lacks a source. This is usually an internal error in Dynamo.", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0189": [ + { + "Gb_type": "getattr() on nn.Module with pending mutation", + "Context": "getattr({obj}, {name}, {default})", + "Explanation": "Intentionally graph breaking on getattr() on a nn.Module with a pending mutation", + "Hints": [] + } + ], + "GB0190": [ + { + "Gb_type": "getattr() with non-constant name argument", + "Context": "getattr({obj}, {name_var}, {default})", + "Explanation": "getattr() with non-constant name argument is not supported", + "Hints": [ + "Ensure the name argument of getattr() is a string" + ] + } + ], + "GB0191": [ + { + "Gb_type": "id() with unsupported args", + "Context": "str(args)", + "Explanation": "Dynamo doesn't know how to trace id() call with args {args}", + "Hints": [ + "Supported args are Tensors, and functions/nn.Modules/user-defined objects ", + "from outside the compiled region.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB1042": [ + { + "Gb_type": "backward() with in-graph created tensor", + "Context": "backward(inputs=[...]) with in-graph created tensor: {var}", + "Explanation": "backward(inputs=[...]) with tensors created inside the compiled function is not yet supported.", + "Hints": [ + "Only pass tensors that are inputs to the compiled function or captured from outside" + ] + } + ], + "GB0192": [ + { + "Gb_type": "input iterator to itertools.cycle has too many items", + "Context": "next({self})", + "Explanation": "Has reached internal Dynamo max iterator limit: {MAX_ITERATOR_LIMIT}", + "Hints": [] + } + ], + "GB0193": [ + { + "Gb_type": "invalid call to builtin op handler", + "Context": "invalid args to {self_handler}: {args} {kwargs}", + "Explanation": "Encountered TypeError when trying to handle op {fn.__name__}", + "Hints": [ + "This graph break may be difficult to debug. Please report an issue to PyTorch for assistance." + ] + } + ], + "GB0194": [ + { + "Gb_type": "isinstance() called on user defined object with C extensions", + "Context": "isinstance({arg}, {isinstance_type})", + "Explanation": "User-defined object with C extensions can have torch.Tensor attributes; intentionally graph breaking.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0195": [ + { + "Gb_type": "issubclass() with non-constant arguments", + "Context": "issubclass({left_ty}, {right_ty})", + "Explanation": "issubclass() with non-constant arguments not supported.", + "Hints": [ + "Make sure your arguments are types.", + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0196": [ + { + "Gb_type": "key not found in dict", + "Context": "Key {arg.value}", + "Explanation": "msg", + "Hints": [ + "Check if the key exists in the dictionary before accessing it.", + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB1739": [ + { + "Gb_type": "Unsupported member type on OpaqueObject", + "Context": "value={real_obj}, attr={name}, member_type={member_type}", + "Explanation": "Member type '{member_type}' is not supported for this operation.", + "Hints": [] + } + ], + "GB0197": [ + { + "Gb_type": "list elements are pointing to the list itself", + "Context": "", + "Explanation": "Dynamo does not support lists whose items reference to itself", + "Hints": [ + "Avoid using self referential list" + ] + } + ], + "GB0198": [ + { + "Gb_type": "mapping proxy affected by dictionary mutation", + "Context": "Source: {self.source}, Dict mutation detected", + "Explanation": "msg", + "Hints": [ + "Avoid modifying dictionaries that might be referenced by mapping proxy objects", + "Or avoid using the mapping proxy objects after modifying its underlying dictionary" + ] + } + ], + "GB0199": [ + { + "Gb_type": "mapping proxy cannot be reconstructed", + "Context": "Source: {self.source}", + "Explanation": "msg", + "Hints": [ + "Use a mapping proxy constructed in the same `torch.compile` region.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0200": [ + { + "Gb_type": "missing BUILD_SET handler", + "Context": "", + "Explanation": "Missing BUILD_SET bytecode handler (for testing purposes).", + "Hints": [] + } + ], + "GB8843": [ + { + "Gb_type": "Opaque object member with method-type USE_REAL returned a reference-type opaque object.", + "Context": "Opaque object type: {real_obj_type}. Method name: '{name}'", + "Explanation": "To properly guard reference-type opaque objects, we must lift them as inputs to the graph. In order to do this, they must all have a source, meaning they come from a global value or are an attribute of an input.", + "Hints": [ + "Register member '{name}' with MemberType.INLINED in ", + "register_opaque_type({real_obj_type}, members=...)." + ] + }, + { + "Gb_type": "Opaque object member with method-type USE_REAL returned a reference-type opaque object.", + "Context": "Opaque object type: {value_type}. Method name: '{name}'", + "Explanation": "To properly guard reference-type opaque objects, we must lift them as inputs to the graph. In order to do this, they must all have a source, meaning they come from a global value or are an attribute of an input.", + "Hints": [ + "Register member '{name}' with MemberType.INLINED in ", + "register_opaque_type({value_type}, members=...)." + ] + } + ], + "GB0201": [ + { + "Gb_type": "namedtuple construction", + "Context": "args={args}, kwargs={kwargs}", + "Explanation": "`torch.compile` only support certain input types for namedtuple", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0202": [ + { + "Gb_type": "non-const argument in nn.Module method", + "Context": "call_method: {self} {name} {args} {kwargs}", + "Explanation": "Dynamo does not support calling method `{name}` of ``nn.Module`` {module} with non-constant arguments.", + "Hints": [] + } + ], + "GB0203": [ + { + "Gb_type": "non-const keys in dict_keys", + "Context": "non-const keys: {[k for k in value if not ConstantVariable.is_literal(k)]}", + "Explanation": "Dynamo expects dict_keys keys to be constants.", + "Hints": [ + "Ensure your dict_keys keys are constants (e.g. int, float, strings)" + ] + } + ], + "GB0204": [ + { + "Gb_type": "non-const keys in mappingproxy", + "Context": "non-const keys: {[k for k in value.keys() if not ConstantVariable.is_literal(k)]}", + "Explanation": "Dynamo expects mappingproxy keys to be constants.", + "Hints": [ + "Ensure your mappingproxy keys are constants (e.g. int, float, strings)" + ] + } + ], + "GB0205": [ + { + "Gb_type": "proxy not set", + "Context": "as_proxy {self}", + "Explanation": "Dynamo requires the autograd.Function context to be initialized with a proxy.", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB9198": [ + { + "Gb_type": "Attempted to call repr() method implemented in C/C++", + "Context": "", + "Explanation": "{type(arg.value)} has a C/C++ based repr method. This is not supported.", + "Hints": [ + "Write the repr method in Python" + ] + } + ], + "GB0206": [ + { + "Gb_type": "setattr() on Tensor.requires_grad", + "Context": "setattr({obj}, {name}, {val})", + "Explanation": "setattr() on Tensor.requires_grad not supported. Mutating requires_grad can introduce a new leaf from non-leaf or vice versa in the middle of the graph, which AOTAutograd does not currently know how to handle.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0207": [ + { + "Gb_type": "sort with non-constant keys", + "Context": "str(first_non_constant_key)", + "Explanation": "Cannot perform sort with non-constant key. First non-constant key type: {python_type}. Most notably, we cannot sort with Tensor or SymInt keys, but we can sort ints.", + "Hints": [ + "Use something else as the key." + ] + } + ], + "GB0208": [ + { + "Gb_type": "torch.* op returned non-Tensor", + "Context": "example_value type: {typestr(example_value)}; op: {proxy.node.op}; target: {proxy.node.target}", + "Explanation": "torch.* ops that return a non-Tensor cannot be traced into the Dynamo FX graph output", + "Hints": [] + } + ], + "GB2520": [ + { + "Gb_type": "assume_constant_result argument conversion failed", + "Context": "function {name}, variable type {type(x).__name__}", + "Explanation": "Cannot convert argument of type {type(x).__name__} to a Python constant for function {name} marked with torch._dynamo.assume_constant_result. The variable tracker does not support constant conversion.", + "Hints": [ + "Remove torch._dynamo.assume_constant_result from this function", + "Ensure all arguments passed to the function can be converted to constants" + ] + } + ], + "GB0209": [ + { + "Gb_type": "torch.autograd._unsafe_preserve_version_counter escaped from compiled region", + "Context": "str(self)", + "Explanation": "Dynamo doesn't support compiling a region that returns a torch.autograd._unsafe_preserve_version_counter context manager.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0210": [ + { + "Gb_type": "torch.distributed package is not available!", + "Context": "", + "Explanation": "The PyTorch package doesn't include torch.distributed when building from source.", + "Hints": [ + "Set USE_DISTRIBUTED=1 to enable it when building PyTorch from source." + ] + } + ], + "GB2977": [ + { + "Gb_type": "wrap_jit_function: missing _torchdynamo_inline", + "Context": "type: {type(value).__name__}", + "Explanation": "Dynamo expected a JIT function with a _torchdynamo_inline attribute, but the object does not have one.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0211": [ + { + "Gb_type": "torch.nn.Module with a non-function custom __getattr__", + "Context": "var_getattr {self} {name}", + "Explanation": "Dynamo detected a nn.Module object with a custom `__getattr__` method, but this method is not a standard Python function (e.g., it might be implemented in C/C++). Dynamo cannot currently trace into such non-standard `__getattr__` methods.", + "Hints": [ + "Avoid using objects with non-standard __getattr__ methods ", + "within the compiled region. If possible, implement ", + "__getattr__ as a standard Python function.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0212": [ + { + "Gb_type": "torch.profiler object escaped from compiled region", + "Context": "str(self)", + "Explanation": "Dynamo doesn't support compiling a region that returns a torch.profiler context manager.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0213": [ + { + "Gb_type": "unimplemented builtin op on tensor arguments", + "Context": "partial tensor op: {self} {args} {kwargs}", + "Explanation": "Dynamo does not know how to trace builtin operator {self.fn} with tensor arguments", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0214": [ + { + "Gb_type": "unsupported SymNode comparison op", + "Context": "{op.__name__}({left}, {right})", + "Explanation": "Dynamo does not support the comparison op {op.__name__} with SymNode arguments {left}, {right}", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0215": [ + { + "Gb_type": "unsupported Tensor comparison op", + "Context": "{op.__name__}({left}, {right})", + "Explanation": "Dynamo does not support the comparison op {op.__name__} with Tensor arguments {left}, {right}", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0216": [ + { + "Gb_type": "unsupported grid type for triton hop check_grid", + "Context": "grid type = {type(grid)}", + "Explanation": "`torch.compile` only supports list-like grid for check_grid", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0217": [ + { + "Gb_type": "unsupported hasattr operation", + "Context": "Class {self.user_cls}", + "Explanation": "msg", + "Hints": [ + "Consider using a regular dictionary instead", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0218": [ + { + "Gb_type": "unsupported index(Tensor)", + "Context": "", + "Explanation": "Dynamo does not support tracing builtin index() on a Tensor", + "Hints": [] + } + ], + "GB0219": [ + { + "Gb_type": "Backend compiler exception", + "Context": "Backend: {name}\nException:{str(e)}\nTraceback:\n{self.root_tx.format_frame_summary()}", + "Explanation": "Backend compiler `{name}` failed with {str(e)}. Adding a graph break.", + "Hints": [ + "Report an issue to the backend compiler repo." + ] + } + ], + "GB0220": [ + { + "Gb_type": "Failed to mutate tensor data attribute to different dtype", + "Context": "setattr({obj}, {name}, {val})", + "Explanation": "Dyanmo only supports mutating `.data` of tensor to a new one with the same dtype", + "Hints": [ + "Don't mutate `.data` on this tensor, or move ", + "the mutation out of `torch.compile` region" + ] + } + ], + "GB4271": [ + { + "Gb_type": "__class__ assignment on user-defined object", + "Context": "object={self}, value={value}", + "Explanation": "Dynamo does not support reassigning __class__ on user-defined objects.", + "Hints": [ + "Move the __class__ assignment outside of the torch.compile region." + ] + } + ], + "GB0221": [ + { + "Gb_type": "non-generator contextlib.contextmanager", + "Context": "str(self.vt.get_code())", + "Explanation": "Cannot compile function decorated with `@contextlib.contextmanager` that is not a generator, i.e. does not use `yield`", + "Hints": [ + "Use `yield` in the function body instead of `return`.", + "Remove the `@contextlib.contextmanager` decorator." + ] + } + ], + "GB0222": [ + { + "Gb_type": "Attempted to wrap a set with tensors", + "Context": "Python set containing torch.Tensor elements", + "Explanation": "Dynamo cannot trace sets of tensors. To get a stable ordering, Dynamo needs to convert the set into a list and the order might not be stable if the set contains tensors.", + "Hints": [ + "Use a dictionary where the keys are tensors.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB2669": [ + { + "Gb_type": "Comprehension graph break in resume function with captured variables", + "Context": "str(analysis.captured_vars)", + "Explanation": "Cannot use comprehension optimization inside a resume function when there are captured variables. This can cause issues with deeply nested source chains.", + "Hints": [] + } + ], + "GB0223": [ + { + "Gb_type": "torch.compile call with > 1 args", + "Context": "args={args}, kwargs={kwargs}", + "Explanation": "Attempted to call `torch.compile` with > 1 args. Dynamo does not support this.", + "Hints": [ + "Remove the torch.compile call or its additional args.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0224": [ + { + "Gb_type": "Attempted to call torch in-graph function on only torch.SymInt arguments", + "Context": "fn={self.value}, args={args}, kwargs={kwargs}", + "Explanation": "Attempted to call {str(self.value)} (that should be put in the FX graph) on only torch.SymInt arguments. Dynamo does not support this.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0225": [ + { + "Gb_type": "Attempted to use tensor creation function with requires_grad=True", + "Context": "fn={self.value}, args={args}, kwargs={kwargs}", + "Explanation": "Dynamo does not support this.", + "Hints": [ + "Create the tensor outside the compiled region.", + "Do not set `requires_grad=True`.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0226": [ + { + "Gb_type": "`torch.nn.Parameter()` with unsupported data type", + "Context": "data={data}", + "Explanation": "Called `torch.nn.Parameter()` with non-Tensor argument.", + "Hints": [ + "Ensure the argument to `torch.nn.Parameter()` is a `torch.Tensor`.", + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0227": [ + { + "Gb_type": "Attempted to use torch.nn.Parameter constructor with tensor subclass", + "Context": "str(data)", + "Explanation": "Dynamo does not support this.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0228": [ + { + "Gb_type": "`torch.nn.Parameter`: cannot convert to traceable tracable", + "Context": "", + "Explanation": "convert_tracable_parameter is set to False.", + "Hints": [ + "Check usage of context manager: do_not_convert_to_tracable_parameter", + "This graph break may be difficult to debug. Please report an issue to PyTorch for assistance." + ] + } + ], + "GB0229": [ + { + "Gb_type": "Unexpected type of data placeholder op for parameter construction", + "Context": "data_node.op={data_node.op}", + "Explanation": "Data node op should be placeholder or get_attr.", + "Hints": [ + "This graph break may be difficult to debug. Please report an issue to PyTorch for assistance." + ] + } + ], + "GB0230": [ + { + "Gb_type": "Attempted to use torch.use_deterministic_algorithms(warn_only=True)", + "Context": "mode={mode}, warn_only={warn_only}", + "Explanation": "Dynamo does not support this.", + "Hints": [ + "Remove param warn_only in function call torch.use_deterministic_algorithms.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0231": [ + { + "Gb_type": "call `torch.from_numpy` with `torch._dynamo.config.trace_numpy=False`", + "Context": "trace_numpy={config.trace_numpy}", + "Explanation": "Attempted to call `torch.from_numpy` with config `torch._dynamo.config.trace_numpy` set to `False`.", + "Hints": [ + "Change `torch._dynamo.config.trace_numpy` to `True`." + ] + } + ], + "GB0232": [ + { + "Gb_type": "`torch.from_numpy` with NumPy unavailable", + "Context": "", + "Explanation": "Attempted to call `torch.numpy` but NumPy could not be imported.", + "Hints": [ + "Check NumPy version and installation in your environment.", + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0233": [ + { + "Gb_type": "Attempted to use strided NestedTensor", + "Context": "layout={layout}", + "Explanation": "Dynamo does not support this.", + "Hints": [ + "Change layout=torch.jagged.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0234": [ + { + "Gb_type": "Attempted to pop from empty torch function mode stack", + "Context": "", + "Explanation": "Called `torch._C._pop_torch_function_stack` when torch function mode stack is empty.", + "Hints": [ + "Do not pop from empty torch function mode stack.", + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0235": [ + { + "Gb_type": "`torch.nn.Parameter` with non-constant Tensor attributes", + "Context": "data={data}", + "Explanation": "Dynamo does not support this.", + "Hints": [ + "Ensure the Tensor argument's shape, dtype, and device are correct.", + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB6025": [ + { + "Gb_type": "Failed to trace list()", + "Context": "list({arg_types})", + "Explanation": "Dynamo does not know how to construct a list from argument types {arg_types}", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0236": [ + { + "Gb_type": "Invalid input type for nonstrict_trace-ed function", + "Context": "Encountered input of type <{type_name}>.", + "Explanation": "For `nonstrict_trace`-ed functions, only basic types (e.g., torch.Tensor, int, float) or pytree containers of those are allowed as inputs. The provided argument contains an unsupported type.", + "Hints": [ + "Use one of the following to register the type with pytree:\n", + "* `torch.utils._pytree.register_constant`\n", + "* `torch.utils._pytree.register_dataclass`\n", + "* `torch.utils._pytree.register_pytree_node`" + ] + } + ], + "GB0237": [ + { + "Gb_type": "non-constant `requires_grad` argument to `torch.nn.Parameter`", + "Context": "requires_grad={requires_grad}", + "Explanation": "Dynamo does not support this.", + "Hints": [ + "Change `requires_grad` to be a bool.", + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0238": [ + { + "Gb_type": "Input marked with `pytree.register_constant` constructed in the `torch.compile` region", + "Context": "Input={input_spec_vt}, offending type <{type_name}>.", + "Explanation": "Calling a `nonstrict_trace`-ed function with an input that contains an object of type <{type_name}>, which was marked with `pytree.register_constant`. However, the object was constructed _inside_ the `torch.compile` region. This is not supported.", + "Hints": [ + "Construct the object _outside_ the `torch.compile` region, or submit an issue to GitHub.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0239": [ + { + "Gb_type": "Invalid use of pytree_flatten with nonstrict_trace-ed function", + "Context": "Input={input_spec_vt}, offending type <{type_name}>.", + "Explanation": "Calling a `nonstrict_trace`-ed function where one of the inputs has been registered with a `pytree_flatten` that places an object of type <{type_name}> into the context.", + "Hints": [ + "Modifying the `pytree_flatten` to avoid placing the object into the context.", + "Apply one of the following to <{type_name}>:\n", + "* `torch.utils._pytree.register_constant`\n", + "* `torch.utils._pytree.register_dataclass`\n", + "* `torch.utils._pytree.register_pytree_node`", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0240": [ + { + "Gb_type": "Shape mismatch with out= list of tensor variants", + "Context": "fn={self.value}, args={args}, kwargs={kwargs}", + "Explanation": "Shape mismatch when calling {self.value} with `out=`. Provided `out=` shape: {saved_out_shape}. Actual shape: {fake_out.shape}.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0241": [ + { + "Gb_type": "Attempted to call op with non-contiguous `out=` list of tensors", + "Context": "self.value={self.value}, args={args}, kwargs={kwargs}", + "Explanation": "Dynamo does not support this.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0242": [ + { + "Gb_type": "Attempted to call op with non-contiguous `out=` tensor", + "Context": "self.value={self.value}, args={args}, kwargs={kwargs}", + "Explanation": "Dynamo does not support this.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0243": [ + { + "Gb_type": "Attempted to use `torch.nn.modules.utils._ntuple` with unsupported argument type", + "Context": "value={value}", + "Explanation": "Dynamo does not support this.", + "Hints": [ + "Change use of _ntuple with argument as constant or tensor." + ] + } + ], + "GB0244": [ + { + "Gb_type": "Attempted to use `torch.nn.Parameter()` with export", + "Context": "", + "Explanation": "Dynamo does not support this.", + "Hints": [ + "Do not use `torch.nn.Parameter()` with export.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0245": [ + { + "Gb_type": "Attempted to use `nested_tensor` with non-list input", + "Context": "tensor_list={tensor_list}", + "Explanation": "Dynamo does not support this.", + "Hints": [ + "Change `nested_tensor` with list input.", + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0246": [ + { + "Gb_type": "Attempted to use `torch.nn.functional.one_hot` with data-dependent output shape", + "Context": "args={args}, kwargs={kwargs}", + "Explanation": "Dynamo does not support this.", + "Hints": [ + "Explicitly set the `num_classes` param of the function call ", + "`torch.nn.functional.one_hot` to something other than -1." + ] + } + ], + "GB0247": [ + { + "Gb_type": "Shape mismatch with out= tensor variant", + "Context": "fn={self.value}, args={args}, kwargs={kwargs}", + "Explanation": "Shape mismatch when calling {self.value} with `out=`. Provided `out=` shape: {saved_out_shapes}. Actual shape: {fake_out.shape}.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0248": [ + { + "Gb_type": "improper torch.get_device_module arguments", + "Context": "args={args}, kwargs={kwargs}", + "Explanation": "torch.get_device_module accepts 1 optional argument `device`", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0249": [ + { + "Gb_type": "bad device argument to torch.accelerator.current_stream", + "Context": "args={args}, kwargs={kwargs}", + "Explanation": "Expected valid string/torch.device argument ('cpu', 'cuda', etc.)", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + }, + { + "Gb_type": "bad device argument to torch.get_device_module", + "Context": "args={args}, kwargs={kwargs}", + "Explanation": "Expected valid string/torch.device argument ('cpu', 'cuda', etc.)", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + }, + { + "Gb_type": "bad device argument to torch.accelerator.current_stream", + "Context": "args={args}, kwargs={kwargs}", + "Explanation": "Expected valid string/torch.device argument ('cpu', 'cuda', etc.)", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + }, + { + "Gb_type": "bad device argument to torch.get_device_module", + "Context": "args={args}, kwargs={kwargs}", + "Explanation": "Expected valid string/torch.device argument ('cpu', 'cuda', etc.)", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0250": [ + { + "Gb_type": "ndarray.astype(object)", + "Context": "call_method {self} {name} {args} {kwargs}", + "Explanation": "`ndarray.astype('O')` or `ndarray.astype(object)` is not supported by torch.compile, as there is no equivalent to object type in torch.Tensor. This will be executed eagerly.", + "Hints": [ + "This graph break is fundamental - it is unlikely that Dynamo will ever be able to trace through your code. Consider finding a workaround." + ] + } + ], + "GB0251": [ + { + "Gb_type": "Unsupported output type for nonstrict_trace-ed function", + "Context": "Function: {fn.__name__}", + "Explanation": "For `nonstrict_trace`-ed functions, only basic types (e.g., torch.Tensor, int, list) are allowed as output. The result of this call contains an unsupported type.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0252": [ + { + "Gb_type": "could not find name in object's mro", + "Context": "name={name}, object type={type(self.value)}, mro={type(self.value).__mro__}", + "Explanation": "Could not find name `{name}` in mro {type(self.value).__mro__}", + "Hints": [ + "Ensure the name `{name}` is defined somewhere in {self.value}'s type hierarchy.", + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB9860": [ + { + "Gb_type": "P2POp mutation", + "Context": "object={self}, name={name}, value={value}", + "Explanation": "Dynamo does not support mutating torch.distributed.P2POp instances.", + "Hints": [ + "Construct a new torch.distributed.P2POp instead of mutating an existing one inside torch.compile." + ] + } + ], + "GB0253": [ + { + "Gb_type": "call_method on generator", + "Context": "object={self.value}, method={name}, args={args}, kwargs={kwargs}", + "Explanation": "Detected a method call to a user-defined generator object. This is not fully supported.", + "Hints": [ + "Set `torch._dynamo.config.enable_faithful_generator_behavior = False`. Note that this ", + "may cause silent incorrectness, since we will eagerly unpack generators instead of lazily ", + "evaluating them." + ] + } + ], + "GB0254": [ + { + "Gb_type": "non-const setattr name on user-defined object", + "Context": "object={self}, name={name}, value={value}", + "Explanation": "Detected a call to `setattr` of a user-defined object with a non-constant name.", + "Hints": [ + "Ensure that the name is a string." + ] + } + ], + "GB0255": [ + { + "Gb_type": "attempted to call sourceless user-defined object as a method", + "Context": "object={self.value}, function={func}, args={args}, kwargs={kwargs}", + "Explanation": "Dynamo does not support this.", + "Hints": [ + "Ensure the user-defined object {self.value} is constructed outside the compiled region." + ] + } + ], + "GB0256": [ + { + "Gb_type": "User-defined object with non-function __getattr__", + "Context": "object={self.value}, name={name}, getattr_fn={getattr_fn}", + "Explanation": "Found a non-function __getattr__ {getattr_fn} from a user-defined object {self.value} when attempting to getattr `{name}`", + "Hints": [ + "Ensure the object's __getattr__ is a function type." + ] + } + ], + "GB0257": [ + { + "Gb_type": "TypedDict with optional keys", + "Context": "str(self.value)", + "Explanation": "Dyanmo does not support tracing TypedDict with optional keys", + "Hints": [ + "Avoid using TypedDict with optional keys", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0258": [ + { + "Gb_type": "collections.deque() with bad arguments", + "Context": "args={args}, kwargs={kwargs}", + "Explanation": "Detected call to collections.deque() with bad arguments.", + "Hints": [ + "Fix the call to collections.deque().", + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0259": [ + { + "Gb_type": "collections.deque() with bad iterable argument", + "Context": "args={args}, kwargs={kwargs}", + "Explanation": "Call to collections.deque() has an iterable argument that Dynamo cannot convert to a list.", + "Hints": [ + "Use a simpler sequence type that Dynamo can convert to a list ", + "(e.g. list, tuple, list iterator, etc.)", + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0260": [ + { + "Gb_type": "missing args to functools.partial", + "Context": "", + "Explanation": "functools.partial requires at least one argument", + "Hints": [ + "Fix the functools.partial call.", + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0261": [ + { + "Gb_type": "User-defined object method with non-function __func__", + "Context": "object={self.value}, name={name}, method={dynamic_subobj}, method.__self__={dynamic_subobj.__self__}, method.__func__={dynamic_subobj.__func__}", + "Explanation": "Method {dynamic_subobj} (name={name}) of user-defined object {self.value} has a __func__ ({dynamic_subobj.__func__}) that is not a function type.", + "Hints": [ + "Ensure that the method's __func__ is a function type." + ] + } + ], + "GB4927": [ + { + "Gb_type": "autograd.grad consumed returned tensor's grad_fn", + "Context": "Leaked output tensors: {leaked_str}", + "Explanation": "torch.autograd.grad() consumes grad_fns that are needed by tensors returned from this compiled function. This would cause 'backward through graph a second time' errors.\n The following returned tensors have consumed grad_fns: {leaked_str}", + "Hints": [ + "Detach the problematic tensor(s) before returning: e.g. `{leaked[0]}.detach()`", + "Call .detach() on the tensor before returning.", + "If you need to backward through the returned tensor, use retain_graph=True in autograd.grad()." + ] + }, + { + "Gb_type": "autograd.grad consumed returned tensor's grad_fn", + "Context": "", + "Explanation": "torch.autograd.grad() consumes grad_fns that are needed by tensors returned from this compiled function. This would cause 'backward through graph a second time' errors.", + "Hints": [ + "If you don't need to backward through the returned tensor, ", + "call .detach() before returning: `return loss.detach()`", + "If you need to backward through the returned tensor, use retain_graph=True in autograd.grad()." + ] + }, + { + "Gb_type": "autograd.grad consumed returned tensor's grad_fn", + "Context": "", + "Explanation": "torch.autograd.grad() consumes grad_fns that are needed by tensors returned from this compiled function. This would cause 'backward through graph a second time' errors.", + "Hints": [ + "If you don't need to backward through the returned tensor, call .detach() before returning: `return loss.detach()`", + "If you need to backward through the returned tensor, use retain_graph=True in autograd.grad()." + ] + } + ], + "GB6663": [ + { + "Gb_type": "autograd.grad with unsupported argument type", + "Context": "got {type(var).__name__}", + "Explanation": "torch.autograd.grad() received an argument of type {type(var).__name__} which is not supported. Expected tensor or sequence of tensors.", + "Hints": [ + "Ensure outputs and inputs arguments are tensors or sequences of tensors." + ] + } + ], + "GB1381": [ + { + "Gb_type": "autograd.grad with already consumed grad_fn", + "Context": "double consumed grad_fns: {len(double_consumed)}", + "Explanation": "torch.autograd.grad() is trying to consume grad_fns that were already consumed by a previous autograd.grad() call. This would cause 'backward through graph a second time' errors at runtime.", + "Hints": [ + "Use retain_graph=True in the first autograd.grad() call if you ", + "need to compute gradients through the same graph multiple times." + ] + } + ], + "GB0262": [ + { + "Gb_type": "unsupported contextlib.* API", + "Context": "{self.value}", + "Explanation": "{self.value} not supported. This may be due to its use of context-specific operations that are not supported in Dynamo yet (i.e. Exception handling)", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0263": [ + { + "Gb_type": "attempted to trace contextlib.contextmanager", + "Context": "args={args}", + "Explanation": "Tracing contextlib.contextmanager is disabled.", + "Hints": [ + "Set torch._dynamo.config.enable_trace_contextlib = True" + ] + } + ], + "GB0264": [ + { + "Gb_type": "Attempted to use `torch.nn.Parameter()` constructor with Dynamo", + "Context": "", + "Explanation": "Dynamo does not support this", + "Hints": [ + "Try to construct `torch.nn.Parameter()` outside the compiled region.", + "If this is not possible, turn `graph_break_on_nn_param_ctor` off", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0265": [ + { + "Gb_type": "FakeScriptObject missing method implementation", + "Context": "value={self.value}, method={name}", + "Explanation": "TorchScript object {self.value} doesn't define the method {name}.", + "Hints": [ + "Ensure the method {name} is implemented in {self.value}.", + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0266": [ + { + "Gb_type": "Weird method call on TorchScript object", + "Context": "value={self.value}, method={name}", + "Explanation": "This particular method call ({name}) is not supported (e.g. calling `__setattr__`). Most method calls to TorchScript objects should be supported.", + "Hints": [ + "Avoid calling this method." + ] + } + ], + "GB0267": [ + { + "Gb_type": "Attempted to access non-callable attribute of TorchScript object", + "Context": "value={self.value}, method={name}", + "Explanation": "Attribute accesses of TorchScript objects to non-callable attributes are not supported.", + "Hints": [ + "Use method calls instead of attribute access." + ] + } + ], + "GB0268": [ + { + "Gb_type": "Unsupported kwargs for itertools.product", + "Context": "call_function {self} {args} {kwargs}", + "Explanation": "Expected kwargs: 'repeat', but got {','.join(set(kwargs.keys()) - {'repeat'})}", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB6017": [ + { + "Gb_type": "executorch_call_delegate: first arg not supported", + "Context": "args: {args}, kwargs: {kwargs}", + "Explanation": "executorch_call_delegate expects the first argument to be a nn.Module (got {args[0]})", + "Hints": [] + } + ], + "GB0269": [ + { + "Gb_type": "Forced graph break on leaf function", + "Context": "", + "Explanation": "Forced graph break on non-inlining function for nested graph break testing purposes", + "Hints": [ + "Set torch._dynamo.config.debug_force_graph_break_on_leaf_return = False" + ] + }, + { + "Gb_type": "Forced graph break on leaf function", + "Context": "", + "Explanation": "Forced graph break for nested graph break testing purposes", + "Hints": [ + "Set torch._dynamo.config.debug_force_graph_break_on_leaf_return = False" + ] + } + ], + "GB0270": [ + { + "Gb_type": "unimplemented builtin op vars() with no arguments", + "Context": "vars: {self} {args}", + "Explanation": "Dynamo does not know how to trace builtin operator {self.fn} with no arguments", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0271": [ + { + "Gb_type": "Class attribute mutation when the __dict__ was already materialized", + "Context": "str(self.value)", + "Explanation": "Dyanmo does not support tracing mutations on a class when its __dict__ is materialized", + "Hints": [] + } + ], + "GB0272": [ + { + "Gb_type": "Failed to make weakref to User Object when storing by ID", + "Context": "user_objected: {obj}", + "Explanation": "Object does not allow us to make a weakref to it", + "Hints": [] + }, + { + "Gb_type": "Failed to make weakref to User Object", + "Context": "user_objected: {obj}", + "Explanation": "Object does not allow us to make a weakref to it", + "Hints": [] + } + ], + "GB0273": [ + { + "Gb_type": "Keyword args passed to exception constructor", + "Context": "{self} with kwargs {init_kwargs}", + "Explanation": "Dynamo does not know how to handle keyword args passed to an exception constructor", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0274": [ + { + "Gb_type": "Attempted to reconstruct context manager's __enter__ method", + "Context": "str(self.ctx)", + "Explanation": "Attempted to reconstruct context manager {type_str} while tracing `with ...:`", + "Hints": [ + "It is likely there is a graph break while tracing `with ctx:` ", + "but outside the actual `ctx.__enter__()` method. ", + "`torch.compile` does not expect this to happen.", + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0275": [ + { + "Gb_type": "torch._dynamo.step_unsupported() with empty checkpoint", + "Context": "", + "Explanation": "traced torch._dynamo.step_unsupported(), but there is no checkpoint to step_graph_break from. This graph break is used for debugging only.", + "Hints": [ + "Remove the torch._dynamo.step_unsupported() call.", + "Include at least one checkpoint: (1) include at least 2 ops and (2) make sure there is some ", + "line of code that is not in a try/with block, and has an empty Python stack.", + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB6604": [ + { + "Gb_type": "Missing CALL_INTRINSIC_2 handler", + "Context": "CALL_INTRINSIC_2 operand: {inst.argval}", + "Explanation": "No handler implemented for CALL_INTRINSIC_2 {inst.argval} instruction.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0276": [ + { + "Gb_type": "Failed to make weakref to User Object", + "Context": "user_object: {value}", + "Explanation": "Object does not allow us to make a weakref to it", + "Hints": [] + } + ], + "GB0277": [ + { + "Gb_type": "Attempted to wrap sparse Tensor with VariableTracker", + "Context": "str(example_value)", + "Explanation": "torch.compile does not support sparse Tensors with VariableTracker", + "Hints": [ + "Sparse tensor operations are not yet fully supported in torch.compile with fullgraph=True. Consider using fullgraph=False to allow graph breaks, or move sparse tensor creation outside the compiled region." + ] + } + ], + "GB0278": [ + { + "Gb_type": "Unsupported dict type for fromkeys()", + "Context": "{user_cls.__name__}.fromkeys(): {args} {kwargs}", + "Explanation": "Failed to call {user_cls.__name__}.fromkeys() because {user_cls.__name__} is not any type of dict, OrderedDict, or defaultdict", + "Hints": [ + "Ensure {user_cls.__name__} is a type of dict, OrderedDict, or defaultdict." + ] + } + ], + "GB0279": [ + { + "Gb_type": "torch.fx.traceback.annotate escaped from compiled region", + "Context": "str(self)", + "Explanation": "Dynamo doesn't support graph break on torch.fx.traceback.annotate.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB7324": [ + { + "Gb_type": "returning intermediate with requires_grad_()", + "Context": "graph output depends on source-less requires_grad_()", + "Explanation": "msg", + "Hints": [ + "If you only need the tensor values without gradients, ", + "call .detach() before returning.", + "Consume the gradient inside the compiled function ", + "(call backward() and use .grad), ", + "or move requires_grad_() outside torch.compile." + ] + } + ], + "GB0280": [ + { + "Gb_type": "1-arg super not implemented", + "Context": "", + "Explanation": "Dynamo failed to trace attribute `{name}` accessed via `super()` (for type `{self.typevar}` and object `{self.objvar}`) because one-argument of super() is not supported.", + "Hints": [ + "Use two-argument super(type, object_or_type)." + ] + } + ], + "GB3383": [ + { + "Gb_type": "Pydantic dataclass constructor", + "Context": "{self.value}", + "Explanation": "Dynamo graph breaks on pydantic dataclass constructors because validation mutates the instance outside traced bytecode.", + "Hints": [] + } + ], + "GB0281": [ + { + "Gb_type": "Invalid or non-const argument in nn.Module __getitem__", + "Context": "mp_subscript_impl: {self} {key}", + "Explanation": "Dynamo does not support calling method `__getitem__` of ``nn.Module`` {module} with a non-constant or non-(str, int) key.", + "Hints": [ + "Use constant arguments of type str or int for __getitem__" + ] + }, + { + "Gb_type": "Invalid or non-const argument in nn.Module __getitem__", + "Context": "call_method: {self} {name} {args} {kwargs}", + "Explanation": "Dynamo does not support calling method `{name}` of ``nn.Module`` {module} with a non-constant or non-(str, int) key.", + "Hints": [ + "Use constant arguments of type str or int for __getitem__" + ] + } + ], + "GB7685": [ + { + "Gb_type": "Attribute not found on opaque class", + "Context": "class={self.value}, attr={name}", + "Explanation": "The attribute '{name}' does not exist on opaque class {self.value}.", + "Hints": [ + "Ensure '{name}' is a valid attribute of {type(self.value)}." + ] + } + ], + "GB9567": [ + { + "Gb_type": "Unsupported descriptor on opaque class", + "Context": "class={self.value}, attr={name}, descriptor={type_name}", + "Explanation": "The attribute '{name}' is a descriptor of type '{type_name}' which is not supported.", + "Hints": [ + "Only staticmethod, property, and pybind11_static_property are supported.", + "Consider accessing this attribute outside of the compiled region." + ] + } + ], + "GB0282": [ + { + "Gb_type": "Placement with custom __getattr__ not supported", + "Context": "{value_type.__name__} with custom __getattr__", + "Explanation": "Dynamo does not support Placement types with custom __getattr__ methods", + "Hints": [ + "Use Placement types without custom __getattr__ methods", + "Move the Placement usage outside the compiled region" + ] + } + ], + "GB0283": [ + { + "Gb_type": "Failed to make weakref to graph-created external object", + "Context": "user_object: {example_value}", + "Explanation": "Object does not allow us to make a weakref to it", + "Hints": [] + } + ], + "GB0284": [ + { + "Gb_type": "cannot resume from torch._dynamo.step_unsupported()", + "Context": "", + "Explanation": "traced torch._dynamo.step_unsupported(), but Dynamo is instructed to error on graph break. This graph break is used for debugging only.", + "Hints": [ + "Remove the torch._dynamo.step_unsupported() call.", + "Make sure fullgraph=False and error_on_graph_break=False.", + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0285": [ + { + "Gb_type": "unsupported arguments to torch.accelerator.current_stream", + "Context": "args={args}, kwargs={kwargs}", + "Explanation": "torch.accelerator.current_stream accepts one optional argument `device`", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0286": [ + { + "Gb_type": "bad device argument to torch.get_device_module", + "Context": "args={args}, kwargs={kwargs}", + "Explanation": "Expected valid string/torch.device argument ('cpu', 'cuda', etc.)", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0287": [ + { + "Gb_type": "unsupported type.__dict__['__annotations__'].__get__ call", + "Context": "call_function {self}, args: {args}, kwargs: {kwargs}", + "Explanation": "`torch.compile` only supports calling type.__dict__['__annotations__'].__get__ on a single constant argument (i.e. a type).", + "Hints": [ + "Make sure your call to type.__dict__['__annotations__'] only has ", + "one positional argument (no keyword arguments).", + "Make sure the argument to type.__dict__['__annotations__'] is a constant ", + "(i.e. type). For example, `object`, `int`, `MyCustomClass`.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0288": [ + { + "Gb_type": "Can't extract message from torch._check()", + "Context": "str(message_vt)", + "Explanation": "The second argument of torch._check() must be a function defined within the torch.compile region that does not reference a non-local variable.", + "Hints": [ + "Make sure the message function is defined in the torch.compile region.", + "Remove any closure variables, e.g. ", + "remove references to closure variable `x` in `lambda: f'{x} failed check'`", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0289": [ + { + "Gb_type": "unsupported method call on `typing` variable", + "Context": "typing variable: {self.value}, method name: {name}, args: {args}, kwargs: {kwargs}", + "Explanation": "`torch.compile` does not support method call `{name}` on `typing` variable f{self.value}.", + "Hints": [ + "Avoid calling the {name} method on {self.value}.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0290": [ + { + "Gb_type": "attempted to trace numpy.* function as a method", + "Context": "numpy function: {self.value}, args: {args}, kwargs: {kwargs}", + "Explanation": "Tracing numpy.* functions as methods is not supported.", + "Hints": [ + "This graph break may be difficult to debug. Please report an issue to PyTorch for assistance." + ] + } + ], + "GB7906": [ + { + "Gb_type": "Missing len_impl", + "Context": "len({type(self).__name__})", + "Explanation": "Dynamo does not support len() on {type(self).__name__}. Add len_impl to this VariableTracker subclass.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0291": [ + { + "Gb_type": "logging.Logger method not supported for non-export cases", + "Context": "method: {self.value}.{name}, args: {args}, kwargs: {kwargs}", + "Explanation": "logging.Logger methods are not supported for non-export cases.", + "Hints": [ + "Add the logging method to `torch._dynamo.config.ignore_logging_functions`." + ] + }, + { + "Gb_type": "logging.Logger method not supported for non-export cases", + "Context": "method: {self.value}.{name}, args: {args}, kwargs: {kwargs}", + "Explanation": "logging.Logger methods are not supported for non-export cases.", + "Hints": [ + "Add the logging method to `torch._dynamo.config.ignore_logger_methods." + ] + } + ], + "GB0292": [ + { + "Gb_type": "constant-like method call with unsupported return type", + "Context": "{self._error_prefix}.{name}(*{args}, **{kwargs}) returned {result}", + "Explanation": "Attempted to call {self._error_prefix}.{name}, got unsupported return value {result}.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0293": [ + { + "Gb_type": "attempted to trace numpy function with config.trace_numpy=False", + "Context": "numpy function: {self.value}, args: {args}, kwargs: {kwargs}", + "Explanation": "Attempted to trace numpy function {self.value} while `torch._dynamo.config.trace_numpy` was set to False.", + "Hints": [ + "Set `torch._dynamo.config.trace_numpy` to True to trace numpy functions." + ] + } + ], + "GB0294": [ + { + "Gb_type": "attempted to trace numpy function unsupported by PyTorch", + "Context": "numpy function: {self.value}, args: {args}, kwargs: {kwargs} (corresponding torch function: {func})", + "Explanation": "Can't find numpy numpy function {self.value} in torch._numpy.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0295": [ + { + "Gb_type": "cannot reconstruct NullVariable in Python < 3.11", + "Context": "", + "Explanation": "Attempted to generate PUSH_NULL instruction in Python < 3.11; where this instruction does not exist.", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0296": [ + { + "Gb_type": "attempted to reorder a debugging function that can't actually be reordered", + "Context": "fn: {self.value}, args: {args}, kwargs: {kwargs}", + "Explanation": "`torch.compile` can only reorder functions where the arguments are Tensors, constants, or string formatters.", + "Hints": [ + "Avoid calling the logging function {self.value} with args that are not supported." + ] + } + ], + "GB0297": [ + { + "Gb_type": "random.Random() with improper arguments", + "Context": "args: {args}, kwargs: {kwargs}", + "Explanation": "random.Random() with > 1 arg or with kwargs is not supported.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0298": [ + { + "Gb_type": "attempted to trace torch._numpy.random function with config.use_numpy_random_stream=True", + "Context": "numpy function: {self.value}, args: {args}, kwargs: {kwargs} (corresponding torch function: {func})", + "Explanation": "Attempted to trace {self.value} when `torch._dynamo.config.use_numpy_random_stream` is set to True.", + "Hints": [ + "Set `torch._dynamo.config.use_numpy_random_stream` to False.", + "Avoid calling {self.value}." + ] + } + ], + "GB0299": [ + { + "Gb_type": "constant-like method call with non-constant args", + "Context": "{self._error_prefix}.{name}(*{args}, **{kwargs})", + "Explanation": "Attempted to call {self._error_prefix}.{name} with non-constant args.", + "Hints": [ + "Ensure that the args to the method call are constant (int, str, etc.)." + ] + } + ], + "GB0300": [ + { + "Gb_type": "numpy function that produces a const collection type encountered non-const arguments", + "Context": "numpy function: {self.value}, args: {args}, kwargs: {kwargs} (corresponding torch function: {func})", + "Explanation": "numpy function {self.value} that produces a const collection type (e.g. np.dtype, np.iinfo/np.finfo) received arguments that are not constant.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB9300": [ + { + "Gb_type": "P2P compilation disabled for batch_isend_irecv", + "Context": "{self.fn}", + "Explanation": "P2P compilation is disabled.", + "Hints": [ + "Set TORCHDYNAMO_ENABLE_P2P_COMPILATION=1 to enable." + ] + } + ], + "GB2696": [ + { + "Gb_type": "P2P compilation disabled for isend/irecv", + "Context": "{self.fn}", + "Explanation": "P2P compilation is disabled.", + "Hints": [ + "Set TORCHDYNAMO_ENABLE_P2P_COMPILATION=1 to enable." + ] + } + ], + "GB9277": [ + { + "Gb_type": "unsupported function.__get__ call", + "Context": "call_function {self}, args: {args}, kwargs: {kwargs}", + "Explanation": "Dynamo only supports function.__get__(func, obj[, cls]) where func is a user-defined function.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB1750": [ + { + "Gb_type": "P2P compilation disabled for P2POp construction", + "Context": "torch.distributed.P2POp", + "Explanation": "P2P compilation is disabled.", + "Hints": [ + "Set TORCHDYNAMO_ENABLE_P2P_COMPILATION=1 to enable." + ] + } + ], + "GB3343": [ + { + "Gb_type": "Cannot reconstruct P2POpVariable", + "Context": "P2POpVariable.reconstruct", + "Explanation": "P2POpVariable does not support reconstruction.", + "Hints": [] + } + ], + "GB0301": [ + { + "Gb_type": "HOP: non torch.Tensor leaf", + "Context": "args types: {[type(a.realize()) for a in args]}", + "Explanation": "Expected all leaves to be of torch.Tensor type.", + "Hints": [] + } + ], + "GB0302": [ + { + "Gb_type": "HOP: non-callable variable", + "Context": "arg name: {arg_name}, func_var type: {str(func_var)}", + "Explanation": "{arg_name} should be a callable but is of type {str(func_var)}.", + "Hints": [] + } + ], + "GB0303": [ + { + "Gb_type": "torch.while_loop: improper args/kwargs", + "Context": "args: {args}, kwargs: {kwargs}", + "Explanation": "torch.while_loop expects 4 positional arguments (got {len(args)}) and no keyword arguments (got {len(kwargs)}) Usage: while_loop(cond_fn, body_fn, operands)", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0304": [ + { + "Gb_type": "torch.while_loop: improper additional_inputs", + "Context": "str(additional_inputs)", + "Explanation": "Expected additional_inputs to be a list/tuple but got {additional_inputs.python_type()}", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0305": [ + { + "Gb_type": "invalid set_subgraph_inputs and sub_kwargs settings", + "Context": "set_subgraph_inputs: {set_subgraph_inputs}, sub_kwargs: {sub_kwargs}", + "Explanation": "`sub_kwargs` cannot be used when `set_subgraph_inputs` is not set to 'automatic'.", + "Hints": [ + "Use `set_subgraph_inputs='automatic'` when passing `sub_kwargs`.", + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0306": [ + { + "Gb_type": "unsupported HigherOrderOperator", + "Context": "str(value)", + "Explanation": "Unable to create higher order operator variable for {value.__name__}.", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0307": [ + { + "Gb_type": "unsupported HigherOrderOperator function call", + "Context": "str(self.value)", + "Explanation": "Unable to trace calling higher order operator variable for {self.value.__name__}.", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0308": [ + { + "Gb_type": "torch.while_loop: unsupported cond_fn return type", + "Context": "str(cond_r)", + "Explanation": "Expected cond_fn to return a scalar tensor or a bool but got {cond_r_meta.shape}.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0309": [ + { + "Gb_type": "torch.cond: improper args/kwargs", + "Context": "args: {args}, kwargs: {kwargs}", + "Explanation": "torch.cond expects 4 positional arguments (got {len(args)}) and no keyword arguments (got {len(kwargs)}) Usage: cond(pred, cond_fn, body_fn, operands)", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB2078": [ + { + "Gb_type": "Attempted to access unregistered member on an OpaqueObject", + "Context": "value={real_obj}, attr={name}", + "Explanation": "Member '{name}' is not registered for this opaque object type.", + "Hints": [ + "Register '{name}' with a MemberType in register_opaque_type(members=...)." + ] + }, + { + "Gb_type": "Attempted to access unregistered member on an OpaqueObject", + "Context": "value={self.value}, attr={name}", + "Explanation": "Member '{name}' is not registered for this opaque object type.", + "Hints": [ + "Register '{name}' with a MemberType in register_opaque_type(members=...)." + ] + } + ], + "GB5458": [ + { + "Gb_type": "Custom metaclass with __getattribute__", + "Context": "type({self.value}) = {metacls}", + "Explanation": "Dynamo does not trace attribute access on classes whose metaclass overrides __getattribute__", + "Hints": [] + } + ], + "GB0310": [ + { + "Gb_type": "torch.cond: improper predicate", + "Context": "str(pred)", + "Explanation": "Expected `pred` to be a bool or a boolean tensor with a single item but got {str(type(pred))} with original python type {str(pred.python_type())}.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0311": [ + { + "Gb_type": "torch.cond: improper operands", + "Context": "str(operands)", + "Explanation": "Expected `operands` to be a list/tuple but got {operands.python_type()}.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0312": [ + { + "Gb_type": "torch.cond: improper operands contents", + "Context": "str(operands)", + "Explanation": "Expected `operands` to be a list/tuple of pytrees that only consists of tensor leaves.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0313": [ + { + "Gb_type": "torch.cond: differing branch outputs", + "Context": "true_spec: {true_spec.treespec}, false_spec: {false_spec.treespec}, same_spec: {same_spec}", + "Explanation": "Expected branches to return the same pytree structure.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0314": [ + { + "Gb_type": "HOP body output unsupported", + "Context": "non-tensor outputs: {non_tensor_output}", + "Explanation": "HigherOrderOperator body's output must consist of tensors or ints/bools only but got {out.python_type()}.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB4315": [ + { + "Gb_type": "RuntimeError when making fake tensor call", + "Context": "", + "Explanation": "msg", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB2922": [ + { + "Gb_type": "RuntimeError when trying to get real value from fx.Node", + "Context": "", + "Explanation": "", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB6735": [ + { + "Gb_type": "torch.library.wrap_triton call with > 1 args", + "Context": "args={args}, kwargs={kwargs}", + "Explanation": "Attempted to call `torch.library.wrap_triton` with > 1 args. Dynamo does not support this.", + "Hints": [ + "Remove the torch.library.wrap_triton call or its additional args.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0315": [ + { + "Gb_type": "torch.associative_scan: improper xs", + "Context": "str(xs)", + "Explanation": "Expected xs to be a list/tuple but got {xs.python_type()}", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB9625": [ + { + "Gb_type": "traceback.tb_lasti not supported", + "Context": "{self} accessing 'tb_lasti'", + "Explanation": "Dynamo does not support accessing the tb_lasti attribute of traceback objects.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0316": [ + { + "Gb_type": "torch.associative_scan: improper additional_inputs", + "Context": "str(additional_inputs)", + "Explanation": "Expected additional_inputs to be a list/tuple but got {additional_inputs.python_type()}", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0317": [ + { + "Gb_type": "torch.associative_scan: zero-sized tensor", + "Context": "str(xs_vars[0])", + "Explanation": "associative_scan() operator doesn't support zero-sized tensors during tracing.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0318": [ + { + "Gb_type": "torch.associative_scan: combine_fn improper number of leaves", + "Context": "str(_combine_treespec.as_python_constant())", + "Explanation": "combine_fn needs to produce one pytree for the output but combine_fn produces the pytree {_combine_treespec.as_python_constant()}.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0319": [ + { + "Gb_type": "torch.associative_scan: mismatched input/output tree structure", + "Context": "xs: {xs_treespec.as_python_constant()}, output: {_combine_treespec.as_python_constant()}", + "Explanation": "The tree structure of the xs and the outs of the combine_fn are are expected to be identical, but got xs: {xs_treespec.as_python_constant()} vs output: {_combine_treespec.as_python_constant()}.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0320": [ + { + "Gb_type": "torch.scan: improper xs", + "Context": "str(xs)", + "Explanation": "Expected xs to be a list/tuple but got {xs.python_type()}", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0321": [ + { + "Gb_type": "torch.scan: improper init", + "Context": "str(init)", + "Explanation": "Expected init to be a list/tuple with at least one element but got {init.python_type()}", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0322": [ + { + "Gb_type": "torch.scan: no init leaves", + "Context": "", + "Explanation": "Expected init leaves.", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB1297": [ + { + "Gb_type": "Reconstruction failure (self-referential)", + "Context": "str(self)", + "Explanation": "Dynamo tried to reconstruct sourceless variable {self}, but it is self-referential. Dynamo must manually implement reconstruction rules for self-referentiable sourceless variables.", + "Hints": [ + "If Dynamo is attempting to trace a return statement and your code is attempting to return a variable ", + "that Dynamo cannot reconstruct, then remove it from the return statement.", + "Remove the self-reference in the variable. A self-referring list, for example, is `l = []; l.append(l)`.", + "Report an issue to PyTorch if you need self-referential reconstrtuction support.", + "This graph break may have been caused by an earlier graph break. Resolving the earlier graph break may resolve this one." + ] + } + ], + "GB1501": [ + { + "Gb_type": "sparse_grad_accumulate_grad", + "Context": "accumulate_grad_ with sparse gradient", + "Explanation": "Sparse gradients are not supported in compiled autograd", + "Hints": [] + } + ], + "GB0323": [ + { + "Gb_type": "torch.scan: improper additional_inputs", + "Context": "str(additional_inputs)", + "Explanation": "Expected additional_inputs to be a list/tuple but got {additional_inputs.python_type()}", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0324": [ + { + "Gb_type": "torch.scan: zero-sized tensor", + "Context": "str(xs_vars[0])", + "Explanation": "associative_scan() operator doesn't support zero-sized tensors during tracing.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB1140": [ + { + "Gb_type": "_autograd_grad with unsupported argument type", + "Context": "got {type(var).__name__}", + "Explanation": "_autograd_grad() received an argument of type {type(var).__name__} which is not supported. Expected tensor or sequence of tensors.", + "Hints": [ + "Ensure outputs and inputs arguments are tensors or sequences of tensors." + ] + } + ], + "GB0325": [ + { + "Gb_type": "torch.map: kwargs not supported", + "Context": "args: {args}, kwargs: {kwargs}", + "Explanation": "torch.map expects no keyword arguments (got {len(kwargs)})", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0326": [ + { + "Gb_type": "torch.map: improper inputs", + "Context": "str(sample_shape)", + "Explanation": "torch.map doesn't support scalar or non-zero sized tensors during tracing.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB8435": [ + { + "Gb_type": "missing_mp_subscript_impl", + "Context": "mp_subscript_impl not defined for {type(self).__name__}", + "Explanation": "'{self.python_type_name()}' subscript is not yet supported by Dynamo.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0327": [ + { + "Gb_type": "executorch_call_delegate: kwargs not supported", + "Context": "args: {args}, kwargs: {kwargs}", + "Explanation": "executorch_call_delegate expects no keyword arguments (got {len(kwargs)})", + "Hints": [] + } + ], + "GB0328": [ + { + "Gb_type": "torch.func.functional_call capture is disabled", + "Context": "", + "Explanation": "torch.func.functional_call capture is disabled", + "Hints": [ + "Set `torch._dynamo.config.inline_inbuilt_nn_modules=True` to enable." + ] + } + ], + "GB0329": [ + { + "Gb_type": "WrapHigherOrderVariable: kwargs unexpected", + "Context": "args: {args}, kwargs: {kwargs}", + "Explanation": "kwargs should have been flattened into lifted args.", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0330": [ + { + "Gb_type": "wrap_with_set_grad_enabled: unexpected kwargs", + "Context": "args: {args}, kwargs: {kwargs}", + "Explanation": "wrap_with_set_grad_enabled expects no keyword arguments (got {len(kwargs)}).", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0331": [ + { + "Gb_type": "wrap_with_set_grad_enabled: non-constant grad_enabled", + "Context": "str(grad_enabled)", + "Explanation": "wrap_with_set_grad_enabled expects grad_enabled argument to be a constant.", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0332": [ + { + "Gb_type": "wrap_with_set_grad_enabled: unexpected freevars", + "Context": "str(body_lifted_freevars)", + "Explanation": "wrap_with_set_grad_enabled expects no freevars.", + "Hints": [] + } + ], + "GB0333": [ + { + "Gb_type": "wrap_with_autocast: unexpected kwargs", + "Context": "args: {args}, kwargs: {kwargs}", + "Explanation": "wrap_with_autocast expects no keyword arguments (got {len(kwargs)}).", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0334": [ + { + "Gb_type": "wrap_with_autocast: unexpected freevars", + "Context": "str(body_lifted_freevars)", + "Explanation": "wrap_with_autocast expects no freevars.", + "Hints": [] + } + ], + "GB0335": [ + { + "Gb_type": "hints_wrapper: improper args/kwargs", + "Context": "args: {args}, kwargs: {kwargs}", + "Explanation": "hints_wrapper expects 3 positional arguments (got {len(args)}) and 1 keyword argument (got {len(kwargs)}). Usage: hints_wrapper(body_fn, args, kwargs, hints=...). args is expected to be list/tuple and kwargs is expected to be a dict.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0336": [ + { + "Gb_type": "out_dtype: unexpected kwargs", + "Context": "args: {args}, kwargs: {kwargs}", + "Explanation": "out_dtype expects no keyword arguments (got {len(kwargs)}).", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0337": [ + { + "Gb_type": "strict_mode: unexpected kwargs", + "Context": "args: {args}, kwargs: {kwargs}", + "Explanation": "strict_mode higher order op expects no keyword arguments (got {len(kwargs)}).", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0338": [ + { + "Gb_type": "invoke_subgraph: kwargs unexpected", + "Context": "args: {args}, kwargs: {kwargs}", + "Explanation": "kwargs should have been flattened into lifted args.", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0339": [ + { + "Gb_type": "torch.while_loop: infinite loop detected", + "Context": "str(cond_r)", + "Explanation": "Infinite loop detected because while_loop's cond_fn always returns the same value {pred}.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0340": [ + { + "Gb_type": "torch.cond: unsupported branch return type", + "Context": "str(ret_val)", + "Explanation": "Expected branches to return a possibly nested pytree of tensors or constant ints.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0341": [ + { + "Gb_type": "torch.associative_scan: improper args", + "Context": "args: {args}", + "Explanation": "torch.associative_scan expects 2 positional arguments (got {len(args)}) Usage: associative_scan(combine_fn, xs)", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0342": [ + { + "Gb_type": "torch.scan: improper combine_fn", + "Context": "str(combine_fn_var)", + "Explanation": "Expected combine_fn to be wrapped as functools.partial in scan user-facing api or a graph module if we're re-exporting but got {combine_fn_var.python_type()}.", + "Hints": [ + "This graph break may be difficult to debug. Please report an issue to PyTorch for assistance." + ] + } + ], + "GB0343": [ + { + "Gb_type": "torch.scan: improper combine_fn number of returns", + "Context": "str(combine_result_vars)", + "Explanation": "Expect combine_fn to return a tuple (next_carry, y) but got {combine_result_vars}.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB4501": [ + { + "Gb_type": "Reconstruction of FakeIdVariable", + "Context": "str(self.value)", + "Explanation": "A fake id produced by id() on a compile-time container cannot be reconstructed across a graph break.", + "Hints": [ + "Avoid using id() on containers in code that may graph-break." + ] + } + ], + "GB4198": [ + { + "Gb_type": "Attempted to copy.deepcopy a tensor", + "Context": "copy.deepcopy({self})", + "Explanation": "Dynamo does not support copy.deepcopy() on tensors.", + "Hints": [ + "Avoid calling copy.deepcopy() on tensors inside compiled regions." + ] + } + ], + "GB0344": [ + { + "Gb_type": "wrap_with_autocast: expected constant arg", + "Context": "str(args)", + "Explanation": "wrap_with_autocast expects device_type, dtype, enabled, and cache_enabled arguments to be constants.", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0345": [ + { + "Gb_type": "strict_mode: improper args", + "Context": "args: {args}, kwargs: {kwargs}", + "Explanation": "strict_mode higher order op expects flat inputs (list/tuple/dict/set)", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + }, + { + "Gb_type": "strict_mode: improper args", + "Context": "args: {args}, kwargs: {kwargs}", + "Explanation": "strict_mode higher order op expects flat inputs (list/tuple/dict)", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0346": [ + { + "Gb_type": "autograd.Function.apply: non-function or method forward", + "Context": "str(fn)", + "Explanation": "Expected {method_name} to be a function or method.", + "Hints": [] + }, + { + "Gb_type": "autograd.Function.apply: non-function or method forward", + "Context": "str(self.fwd_graph)", + "Explanation": "Expected forward function to be a function or method.", + "Hints": [] + } + ], + "GB0347": [ + { + "Gb_type": "autograd.Function.apply: _materialize_non_diff_grads mutation", + "Context": "", + "Explanation": "Mutations to autograd.Function.ctx._materialize_non_diff_grads are not supported.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0348": [ + { + "Gb_type": "autograd.Function.apply: non-function or method backward", + "Context": "str(self.bwd_graph)", + "Explanation": "Expected backward function to be a function or method.", + "Hints": [] + } + ], + "GB8887": [ + { + "Gb_type": "NamedTupleVariable.__setattr__ bad args", + "Context": "{len(args)} args and {len(kwargs)} kwargs", + "Explanation": "Expected exactly 2 positional args for __setattr__.", + "Hints": [] + } + ], + "GB0349": [ + { + "Gb_type": "cannot unwrap variable for check_meta_consistency", + "Context": "str(var)", + "Explanation": "Expected {var} to be TensorVariable, SymNodeVariable, or ConstantVariable", + "Hints": [] + } + ], + "GB0350": [ + { + "Gb_type": "torch.cond: unsupported branch return type (constant non-int)", + "Context": "str(ret_val)", + "Explanation": "Constants returned from branches must be ints.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0351": [ + { + "Gb_type": "HOP body taking non-Tensor as input", + "Context": "str(sub_args)", + "Explanation": "{description} with body that accepts non-Tensors as input. Got type {a.python_type()} at index {idx}.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0352": [ + { + "Gb_type": "autograd.Function.apply: non-function or method backward (2)", + "Context": "str(self.bwd_fn)", + "Explanation": "Expected backward function to be a function or method.", + "Hints": [] + }, + { + "Gb_type": "autograd.Function.apply: non-function or method backward (2)", + "Context": "str(self.bwd_graph)", + "Explanation": "Expected backward function to be a function or method.", + "Hints": [] + } + ], + "GB0353": [ + { + "Gb_type": "rewrite_signature: cannot trace optional function input", + "Context": "", + "Explanation": "Parameter {name} is optional with a default value of {param.default}. This is not supported yet.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0354": [ + { + "Gb_type": "failed to find name in frame builtins", + "Context": "", + "Explanation": "Failed to find name `{argval}` in frame's builtins.", + "Hints": [ + "This is likely to be a Dynamo bug. Please report an issue to PyTorch." + ] + } + ], + "GB0355": [ + { + "Gb_type": "non-single Tensor return unsupported", + "Context": "api: {api}, ret: {ret}", + "Explanation": "{api} over function that returns something other than one Tensor.", + "Hints": [] + } + ], + "GB0356": [ + { + "Gb_type": "failed to handle argument for FlexAttentionBackward HOP", + "Context": "args: {args}, kwargs: {kwargs}", + "Explanation": "Missing Dynamo support for FlexAttentionBackward HOP argument.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB7425": [ + { + "Gb_type": "Comprehension with captured tensor not in local slot", + "Context": "", + "Explanation": "Cannot use comprehension optimization when a captured tensor variable is not stored in its expected local slot.", + "Hints": [] + } + ], + "GB0357": [ + { + "Gb_type": "UnspecializedNNModuleVariable wrapped around ScriptModules unsupported", + "Context": "str(value)", + "Explanation": "ScriptModules aren't supported in UnspecializedNNModuleVariable because their .forward function isn't a static member of their type.", + "Hints": [ + "This graph break may be difficult to debug. Please report an issue to PyTorch for assistance." + ] + } + ], + "GB8985": [ + { + "Gb_type": "requires_grad_() intermediate leaked as output", + "Context": "call_method {self} requires_grad_", + "Explanation": "An intermediate tensor with requires_grad_() called on it (or a tensor derived from it) is returned from the compiled region. Graph breaking here to preserve partial acceleration.", + "Hints": [ + "Call .detach() before returning if you only need values.", + "Consume the gradient inside the compiled function ", + "(call backward() and use .grad), ", + "or move requires_grad_() outside torch.compile." + ] + } + ], + "GB0358": [ + { + "Gb_type": "optimizer: pending mutation on parameter", + "Context": "variable: {variable}, parameter: {p}", + "Explanation": "Pending mutations on a parameter (e.g. due to using closure) require a graph break.", + "Hints": [] + } + ], + "GB0359": [ + { + "Gb_type": "unsupported torch._C._SDPAParams attribute", + "Context": "name: {name}", + "Explanation": "Unable to fetch attribute {name} from torch._C._SDPAParams.", + "Hints": [ + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + } + ], + "GB0360": [ + { + "Gb_type": "torch.fx.experimental.symbolic_shapes.guard_scalar branch not supported", + "Context": "expr: {expr}", + "Explanation": "Expected `expr` to be a symbolic variable or constant.", + "Hints": [] + } + ], + "GB7286": [ + { + "Gb_type": "nb_float_impl not implemented", + "Context": "{type(self).__name__} has nb_float slot but no nb_float_impl override", + "Explanation": "The type {self.python_type_name()} has an nb_float C slot but the corresponding VariableTracker doesn't implement nb_float_impl.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0361": [ + { + "Gb_type": "triton kernel unsupported feature", + "Context": "", + "Explanation": "Encountered triton kernel unsupported feature: {msg}", + "Hints": [] + } + ], + "GB0362": [ + { + "Gb_type": "Attempted to access attributes/methods on an OpaqueObject", + "Context": "value={self.value}, attr={name}", + "Explanation": "Attribute/method access of OpaqueObjects is not supported.", + "Hints": [ + "Use custom operators instead of direct attribute/method access." + ] + } + ], + "GB0363": [ + { + "Gb_type": "An opaque object was created in the middle of the program.", + "Context": "Opaque object type: {self.value}.", + "Explanation": "Opaque objects cannot be created inside the torch.compile region. They must be created before entering the compiled function.", + "Hints": [ + "Please create the opaque object before calling torch.compile ", + "and pass it in as an argument or as a global variable." + ] + }, + { + "Gb_type": "An opaque object was created in the middle of the program.", + "Context": "Opaque object types: {self.value}", + "Explanation": "Opaque objects cannot be created inside the torch.compile region. They must be created before entering the compiled function.", + "Hints": [ + "Please create the opaque object before calling torch.compile ", + "and pass it in as an argument or as a global variable." + ] + } + ], + "GB0364": [ + { + "Gb_type": "User-defined object with overridden __hash__", + "Context": "hashing object of type={type(obj)} and variable tracker {vt}", + "Explanation": "Found a user-defined object {vt} with overridden __hash__ when attempting to hash it", + "Hints": [ + "Dynamo does not support hashing user-defined objects with overridden __hash__", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0365": [ + { + "Gb_type": "Dynamo cannot determine whether the underlying object is hashable", + "Context": "is_python_hashable {self}", + "Explanation": "Dynamo does not know whether the underlying python object for {self} is hashable", + "Hints": [ + "Consider using a different type of object as the dictionary key instead of {type_self}.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0366": [ + { + "Gb_type": "Dynamo cannot determine the hash of an object", + "Context": "get_python_hash {self}", + "Explanation": "Dynamo does not know the hash of the underlying python object for {self}", + "Hints": [ + "Consider using a different type of object as the dictionary key instead of {self.python_type()}.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0442": [ + { + "Gb_type": "backward() with empty inputs", + "Context": "backward(inputs=[...]) resulted in no valid leaf tensors", + "Explanation": "backward(inputs=[...]) requires at least one valid leaf tensor.", + "Hints": [ + "Ensure at least one tensor in inputs is a leaf (requires_grad=True, no grad_fn)" + ] + } + ], + "GB0367": [ + { + "Gb_type": "Dynamo cannot determine the equality comparison of an object", + "Context": "is_python_equal {self}", + "Explanation": "Dynamo does not know the equality comparison of the underlying python object for {self}", + "Hints": [ + "Consider using a different type of object as the dictionary key instead of {self.python_type()}.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0368": [ + { + "Gb_type": "Frozen dataclass with __post_init__", + "Context": "dataclass={dataclass_cls.__name__}", + "Explanation": "Cannot reconstruct frozen dataclass with __post_init__ method, as it may have side effects that would be incorrectly replayed.", + "Hints": [ + "Remove the __post_init__ method from the frozen dataclass.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0369": [ + { + "Gb_type": "Frozen dataclass with missing field", + "Context": "dataclass={dataclass_cls.__name__}, field={field.name}", + "Explanation": "Cannot reconstruct frozen dataclass: field '{field.name}' was not tracked during tracing.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0370": [ + { + "Gb_type": "record_function escaped from compiled region", + "Context": "str(self)", + "Explanation": "Dynamo doesn't support graph break inside record_function region.", + "Hints": [ + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0371": [ + { + "Gb_type": "autograd.grad with output that requires grad", + "Context": "context", + "Explanation": "The compiled function uses torch.autograd.grad() and returns a tensor that still requires gradients and is connected to the autograd.grad() computation. This would cause aot_autograd to attempt 'backward through graph a second time', which is not supported.", + "Hints": [ + "Call .detach() on the output tensor before returning it from the compiled function.", + "For example: `return loss.detach(), grads` instead of `return loss, grads`.", + "Or set create_graph=False in autograd.grad() if you don't need higher-order gradients.", + "Your code may result in an error when running in eager. Please double check that your code doesn't contain a similar error when actually running eager/uncompiled. You can do this by removing the `torch.compile` call, or by using `torch.compiler.set_stance(\"force_eager\")`. " + ] + }, + { + "Gb_type": "autograd.grad with output that requires grad", + "Context": "", + "Explanation": "The compiled function uses torch.autograd.grad() and returns a tensor that still requires gradients. This may cause aot_autograd to attempt 'backward through graph a second time', which is not supported.", + "Hints": [ + "Call .detach() on the output tensor before returning it from the compiled function.", + "For example: `return loss.detach(), grads` instead of `return loss, grads`.", + "Or set create_graph=False in autograd.grad() if you don't need higher-order gradients." + ] + } + ], + "GB0372": [ + { + "Gb_type": "autograd.grad with external grad_fn", + "Context": "context", + "Explanation": "torch.autograd.grad() cannot trace through the autograd graph because it's output depends on a tensor that was created outside the compiled region and has a grad_fn attached. The autograd graph extends beyond the compiled region boundary, which Dynamo cannot trace.", + "Hints": [ + "If you don't need gradients to flow back to the original tensor outside ", + "the compiled region, detach the input: `tensor.detach().requires_grad_(True)`.", + "Otherwise, move the autograd.grad() call outside the compiled region.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + }, + { + "Gb_type": "autograd.grad with external grad_fn", + "Context": "context", + "Explanation": "torch.autograd.grad() cannot trace through the autograd graph because the '{arg_name}' argument depends on a tensor that was created outside the compiled region and has a grad_fn attached. The autograd graph extends beyond the compiled region boundary, which Dynamo cannot trace.", + "Hints": [ + "Detach the input tensor before passing to the compiled function: ", + "`tensor.detach().requires_grad_(True)` to make it a leaf tensor.", + "Or call `.detach()` on the tensor before using it in autograd.grad().", + "Or move the autograd.grad() call outside the compiled region.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + }, + { + "Gb_type": "autograd.grad with external grad_fn", + "Context": "", + "Explanation": "torch.autograd.grad() cannot be used when the compiled function receives tensors with grad_fn created outside the compiled region. The autograd graph cannot extend beyond the compiled region boundary.", + "Hints": [ + "Detach the input tensor before passing to the compiled function: `tensor.detach().requires_grad_(True)` to make it a leaf tensor.", + "Or move the autograd.grad() call outside the compiled region.", + "It may be possible to write Dynamo tracing rules for this code. Please report an issue to PyTorch if you encounter this graph break often and it is causing performance issues." + ] + } + ], + "GB0373": [ + { + "Gb_type": "autograd.grad with compiled autograd", + "Context": "compiled_autograd is currently enabled", + "Explanation": "torch.autograd.grad() inside torch.compile is not supported when compiled autograd is enabled. These two features have conflicting requirements for how the autograd graph is traced.", + "Hints": [ + "Disable compiled autograd by removing the compiled_autograd context manager.", + "Or move the autograd.grad() call outside the torch.compile region.", + "Or restructure your code so autograd.grad() and compiled_autograd don't overlap." + ] + }, + { + "Gb_type": "autograd.grad with compiled autograd", + "Context": "torch.autograd.grad()", + "Explanation": "torch.autograd.grad() inside torch.compile is not supported when compiled autograd is enabled. These two features have conflicting requirements for how the autograd graph is traced.", + "Hints": [ + "Disable compiled autograd by removing the compiled_autograd context manager.", + "Or move the autograd.grad() call outside the torch.compile region." + ] + } + ], + "GB0374": [ + { + "Gb_type": "Sparse tensor creation not supported", + "Context": "function: {fn_name}", + "Explanation": "torch.compile does not support sparse tensor creation functions like {fn_name}. Sparse tensors require specialized handling that is not yet implemented in the compiler.", + "Hints": [ + "Sparse tensor operations are not yet fully supported in torch.compile with fullgraph=True. Consider using fullgraph=False to allow graph breaks, or move sparse tensor creation outside the compiled region." + ] + } + ] +} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/graph_bytecode_inputs.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/graph_bytecode_inputs.py new file mode 100644 index 0000000000000000000000000000000000000000..921757b0645a121d8400368e8fbe35e063b099dc --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/graph_bytecode_inputs.py @@ -0,0 +1,116 @@ +import weakref +from collections.abc import Callable +from typing import Any + +from torch._dynamo.source import Source + + +PyCodegen = Any + +# This file is to handle types that we don't want to support +# as explicit FX graph inputs. This uses a sidetable which +# we populate in bytecode and is loaded during graph execution + +# We use a dynamo-generated index as a level of indirection +# this allows us to register objects externally in pre-graph bytecode that we want +# to pass to the graph, but not support their types as graph inputs +index_to_bytecode_constructor: dict[int, Callable[[PyCodegen], None]] = {} + +index_to_external_object_weakref: dict[int, weakref.ReferenceType[Any]] = {} + +keep_alive: list[Any] = [] + + +def has_user_objects() -> bool: + return bool(index_to_bytecode_constructor) + + +def stash_graph_created_object(obj: Any) -> Any: + keep_alive.append(obj) + return obj + + +CURRENT_STREAM_INDEX = 0 + + +def set_external_object_by_index(index: int, value: Any) -> None: + """Update an entry in the external object registry at runtime.""" + keep_alive.append(value) + index_to_external_object_weakref[index] = weakref.ref(value) + + +def get_external_object_by_index(index: int) -> Any: + assert index in index_to_external_object_weakref, ( + "Index not registered in index_to_user_object_weakref" + ) + obj = index_to_external_object_weakref[index]() + assert obj is not None, "User object is no longer alive" + return index_to_external_object_weakref[index]() + + +def store_user_object_weakrefs(*args: Any) -> None: + global index_to_external_object_weakref + index_to_external_object_weakref.clear() + index_to_external_object_weakref.update( + {i: weakref.ref(arg) for i, arg in enumerate(args)} + ) + + +def reset_user_object_tracking() -> None: + index_to_bytecode_constructor.clear() + index_to_external_object_weakref.clear() + keep_alive.clear() + + +def register_graph_created_object( + example_value: Any, construct_fn: Callable[[int, PyCodegen], None] +) -> int: + global index_to_bytecode_constructor + global keep_alive + keep_alive.append(example_value) + index = len(index_to_bytecode_constructor) + index_to_bytecode_constructor[index] = lambda cg: construct_fn(index, cg) + try: + index_to_external_object_weakref[index] = weakref.ref(example_value) + except TypeError as e: + from .exc import unimplemented + + unimplemented( + gb_type="Failed to make weakref to graph-created external object", + context=f"user_object: {example_value}", + explanation="Object does not allow us to make a weakref to it", + hints=[], + from_exc=e, + ) + return index + + +# Register a user object to be used in the graph +def register_user_object(value: Any, source: Source) -> int: + global index_to_bytecode_constructor + index = len(index_to_bytecode_constructor) + index_to_bytecode_constructor[index] = lambda cg: cg(source) + try: + index_to_external_object_weakref[index] = weakref.ref(value) + except TypeError as e: + from .exc import unimplemented + + unimplemented( + gb_type="Failed to make weakref to User Object", + context=f"user_object: {value}", + explanation="Object does not allow us to make a weakref to it", + hints=[], + from_exc=e, + ) + return index + + +# Register a callback so invoke_leaf_function can retrieve nn.Module instances at runtime. +# We use a callback pattern instead of having invoke_leaf_function import get_external_object_by_index +# directly, because higher-order ops should not depend on dynamo (dynamo depends on them, not vice versa). +from torch._higher_order_ops.invoke_leaf_function import ( + set_leaf_function_module_retriever, +) + + +set_leaf_function_module_retriever(get_external_object_by_index) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/graph_deduplication.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/graph_deduplication.py new file mode 100644 index 0000000000000000000000000000000000000000..0fd2c2900148c6f6894b93d73abff6adf00b2e29 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/graph_deduplication.py @@ -0,0 +1,609 @@ +""" +This module implements graph deduplication functionality for TorchDynamo's optimization pipeline. +Graph deduplication identifies identical subgraphs in the computational graph and merges them +to reduce redundancy and improve performance. The process involves analyzing regions of the graph, +identifying structurally equivalent regions, and replacing them with a single shared implementation. +This optimization is particularly effective for models with repeated patterns or similar computational +structures across different parts of the network. +""" + +import logging +import operator +from collections import defaultdict, deque +from collections.abc import Generator, Iterable + +import torch +import torch.fx +from torch._dynamo import config +from torch.multiprocessing.reductions import StorageWeakRef +from torch.utils._ordered_set import OrderedSet + +from .graph_region_tracker import Node, Region +from .graph_utils import _detect_cycles, _get_flat_args, _get_flat_args_unique + + +# Represents an index into the region +# to select a node and then +# an index into that node's +# flattened arguments +UsageIndex = tuple[int, int] + +log = logging.getLogger(__name__) + +last_node_to_additional_deps: dict[Node, OrderedSet[Node]] | None = None + + +def apply_graph_deduplication(output_graph) -> dict[str, torch.fx.GraphModule]: # type: ignore[no-untyped-def] + """ + This is the main entry point for applying the graph deduplication pass. \ +Deduplication occurs in two phases: + 1. Subgraph creation: + Subgraph creation works by taking one representative region from each region \ +group and creating a subgraph from it, which will then be used to replace all regions \ +in the group. This is implemented by first copying all nodes of the region to the new \ +subgraph and then finding all inputs which are not within the region and creating placeholders \ +for them. For the outputs, all regions in a region group need to be scanned to ensure the \ +largest set of outputs is found, and then an output node is created which returns \ +a tuple of all outputs. + + 2. Graph replacement: + To replace each region with the extracted subgraph, the node index in the region \ +and argument index within the node's flattened args and kwargs are recorded once during \ +subgraph creation. This allows us to determine which (external to the region) nodes and \ +in which order these nodes are passed as inputs. For the outputs, getitem nodes are created \ +for each output, and all nodes in the region with external outputs are replaced by the proper \ +getitem node. Finally, all original nodes are erased (there should be no uses of these \ +left in the graph). + +The deduplication mutates the output_graph argument in place. + +Returns a mapping of nodes to their subgraph output replacement node to remap outputs +when they are created in output_graph. + """ + + duplicated_region_groups = output_graph.region_tracker.get_identical_regions( + output_graph.graph + ) + node_to_mutated_arg_positions = ( + output_graph.region_tracker.node_to_mutated_arg_positions + ) + node_to_additional_deps = _populate_additional_deps( + output_graph.graph, output_graph.region_tracker.node_to_mutated_arg_positions + ) + + sub_gms: dict[str, torch.fx.GraphModule] = {} + + for region_group in duplicated_region_groups: + inds_with_external_users = _get_all_output_indices(region_group) + region = region_group[0] + ( + subgraph, + external_node_usages, + node_usage_to_tuple_elems, + ind_to_tuple_spec, + ) = _create_subgraph(region, inds_with_external_users) + + # Ignore regions with no args for now, could they possibly be evaluated at compile time? + if not list(external_node_usages): + continue + + sub_gm = torch.fx.GraphModule(output_graph.nn_modules, subgraph) + subgraph_name = output_graph.install_subgraph("subgraph", sub_gm) + sub_gms[subgraph_name] = sub_gm + with output_graph.graph.inserting_before(): + get_subgraph_node = output_graph.graph.create_node( + "get_attr", subgraph_name, (), {} + ) + + for region in region_group: + _replace_region_with_subgraph( + output_graph.graph, + region, + get_subgraph_node, + external_node_usages, + node_usage_to_tuple_elems, + ind_to_tuple_spec, + inds_with_external_users, + subgraph_name, + node_to_additional_deps, + node_to_mutated_arg_positions, + ) + + # This is to expose the updated node_to_additional_deps to tests + global last_node_to_additional_deps + last_node_to_additional_deps = node_to_additional_deps + + _stable_topological_sort( + output_graph.graph, + node_to_additional_deps, + ) + return sub_gms + + +def _replace_region_with_subgraph( + graph: torch.fx.Graph, + region: Region, + get_subgraph_node: Node, + external_node_usages: Iterable[OrderedSet[UsageIndex]], + node_usage_to_tuple_elems: dict[UsageIndex, OrderedSet[int]], + ind_to_tuple_spec: dict[int, dict[tuple[int, ...], int]], + inds_with_external_users: list[int], + subgraph_name: str, + node_to_additional_deps: dict[Node, OrderedSet[Node]], + node_to_mutated_arg_positions: dict[Node, OrderedSet[int]], +) -> None: + sub_args = [] + flattened_getitem_nodes: OrderedSet[Node] = OrderedSet() + for usages in external_node_usages: + usage = next(iter(usages)) + node_ind, usage_ind = usage + node = region[node_ind] + flattened_args_kwargs = _get_flat_args(node, {}) + for user_ind, node_usage_ind in usages: + user = region[user_ind] + if user in node_to_mutated_arg_positions: + if node_usage_ind in node_to_mutated_arg_positions[user]: + log.debug( + "NYI: Failed to substitute region %s due to mutation", region + ) + return + if usage in node_usage_to_tuple_elems: + tuple_elems = [region[i] for i in node_usage_to_tuple_elems[usage]] + flattened_getitem_nodes.update(tuple_elems) + sub_args.extend(tuple_elems) + else: + sub_args.append(flattened_args_kwargs[usage_ind]) + + # Input/Output aliasing not supported in HOPs today + # Note: we should use the nodes in the original graph (the region here) + # because we use the original traced example values for this check + if _has_aliasing( + region, sub_args, inds_with_external_users, flattened_getitem_nodes + ): + return + + invoke_args = (get_subgraph_node, subgraph_name, *sub_args) + + invoke_subgraph_node = graph.create_node( + "call_function", + torch.ops.higher_order.invoke_subgraph, + invoke_args, # type: ignore[arg-type] + {}, + ) + + ind = 0 + flattened_output_nodes: OrderedSet[Node] = OrderedSet() + for external_user_ind in inds_with_external_users: + node = region[external_user_ind] + if _is_tuple_node(node): + tuple_spec = ind_to_tuple_spec[external_user_ind] + flattened_output_nodes.update( + _replace_tuple_outputs( + node, ind, tuple_spec, invoke_subgraph_node, graph + ) + ) + ind += len(tuple_spec) + else: + subgraph_output = graph.create_node( + "call_function", operator.getitem, (invoke_subgraph_node, ind), {} + ) + node.replace_all_uses_with(subgraph_output, propagate_meta=True) + ind += 1 + + # Erase in reverse topological order + for node in reversed(region): + if node in flattened_getitem_nodes: + # Don't erase these, since they will still be used + continue + + if node not in flattened_output_nodes: + graph.erase_node(node) + + # Remove any nodes with additional deps + # This is safe; we've guaranteed that there is + # no input mutation, so all additional deps + # will be internal to the subgraph + node_to_additional_deps.pop(node, None) + for deps in node_to_additional_deps.values(): + try: + deps.remove(node) + deps.add(invoke_subgraph_node) + except KeyError: + pass + + if config.graph_deduplication_lint: + print(_detect_cycles(graph, node_to_additional_deps)) + _stable_topological_sort(graph, node_to_additional_deps) + graph.lint() + + +def _get_external_inputs( + region: Region, +) -> dict[Node, OrderedSet[UsageIndex]]: + external_node_to_usages = defaultdict[Node, OrderedSet[UsageIndex]](OrderedSet) + region_unique = set(region) + for node_ind, node in enumerate(region): + flattened_args_kwargs = _get_flat_args(node, {}) + for arg_ind, in_node in enumerate(flattened_args_kwargs): + if isinstance(in_node, Node) and in_node not in region_unique: + # in_node may occur in multiple nodes' flat_args + # track this so we can check if the arg is mutated + # Previously, we only needed to track one occurrence + # to be able to map that node to a placeholder + external_node_to_usages[in_node].add((node_ind, arg_ind)) + + return external_node_to_usages + + +def _get_all_output_indices(regions: list[Region]) -> list[int]: + # Scan all regions to get the set of all possible output nodes indices in the region + # perhaps we can record this information during region creation for more efficiency? + inds_with_external_users: set[int] = set() + for region in regions: + _get_inds_with_external_users(region, inds_with_external_users) + + return sorted(inds_with_external_users) + + +def _get_inds_with_external_users(region: Region, inds_unique: set[int]) -> None: + for ind, node in enumerate(region): + for user in node.users: + if user not in region: + if ind not in inds_unique: + inds_unique.add(ind) + + +def _create_subgraph( + region: Region, + inds_with_external_users: list[int], +) -> tuple[ + torch.fx.Graph, + list[OrderedSet[UsageIndex]], + dict[UsageIndex, OrderedSet[int]], + dict[int, dict[tuple[int, ...], int]], +]: + subgraph: torch.fx.Graph = torch.fx.Graph() + external_input_to_usages = _get_external_inputs(region) + external_node_usages = list[OrderedSet[UsageIndex]]() + region_to_subgraph_node = {} + flattened_getitem_nodes: OrderedSet[Node] = OrderedSet() + node_usage_to_tuple_elems: dict[UsageIndex, OrderedSet[int]] = {} + + for node, usage_indices in external_input_to_usages.items(): + # We don't handle tuples as inputs today + if _is_tuple_node(node): + # If a node is a tuple we will possibly create multiple placeholders for them + # and track which nodes we won't copy into the subgraph because they are flattened away + # Later, when replacing each region with this subgraph, we will create a getitem node + # externally which will perform the flattening on the outer nodes. + flattened_node_indices = _get_flattened_node_indices(node, region) + for ind in flattened_node_indices: + placeholder = subgraph.placeholder( + f"supgraph_input_{node.name}_flattened_{ind}" + ) + region_to_subgraph_node[region[ind]] = placeholder + flattened_getitem_nodes.add(region[ind]) + node_usage_to_tuple_elems[next(iter(usage_indices))] = ( + flattened_node_indices + ) + else: + placeholder = subgraph.placeholder(f"subgraph_input_{node.name}") + region_to_subgraph_node[node] = placeholder + + external_node_usages.append(usage_indices) + + def map_arg(node: Node) -> Node: + if node in region_to_subgraph_node: + return region_to_subgraph_node[node] + else: + return node + + def copy_to_subgraph(node: Node) -> Node: + subgraph_node = subgraph.node_copy(node, lambda old: map_arg(old)) + region_to_subgraph_node[node] = subgraph_node + return subgraph_node + + output_list = [] + ind_to_tuple_spec = {} + for ind, node in enumerate(region): + if node not in flattened_getitem_nodes: + subgraph_node = copy_to_subgraph(node) + if ind in inds_with_external_users: + # flatten tuple outputs by generating a getitem node tree + if _is_tuple_node(node): + getitem_nodes, ind_to_tuple_spec[ind] = _create_getitem_nodes( + node, subgraph_node, subgraph + ) + output_list.extend(getitem_nodes) + else: + output_list.append(subgraph_node) + + subgraph.output(tuple(output_list)) + + return subgraph, external_node_usages, node_usage_to_tuple_elems, ind_to_tuple_spec + + +def _stable_topological_sort_impl( + graph: torch.fx.Graph, + node_to_additional_deps: dict[Node, OrderedSet[Node]], + do_sort: bool = True, +) -> bool: + # Nodes are in exactly one of these four collections: + + # - Nodes in `pending` are waiting to be processed (in reverse order): + pending = list(reversed(graph.nodes)) + + # - Nodes in `ready` have been processed and are already in the correct + # order. + ready = OrderedSet[Node]() + + # - `waiting` is a mapping from a dependency to nodes which depend on that + # dependency. + waiting = defaultdict(list) + + # - `outputs` are always at the end of the graph + outputs = OrderedSet[Node]() + + # The cursor indicates the last processed node so we can add new nodes + # after it. + cursor = None + while pending: + node = pending.pop() + + if node.target == "output": + outputs.add(node) + assert not node.users, "output nodes should have no users" + continue + + waiting_for = [ + x + for x in _get_flat_args_unique(node, node_to_additional_deps) + if x not in ready + ] + if waiting_for: + # We have unprocessed input nodes. Might as well wait for the last + # arg so an already sorted list will only recheck this node once. + waiting[waiting_for[-1]].append(node) + else: + ready.add(node) + if cursor and cursor.next is not node and do_sort: + cursor.append(node) + cursor = node + # Mark the nodes that have been waiting for this node to finish as + # ready to check again. + pending.extend(reversed(waiting.pop(node, ()))) + + ready.update(outputs) + return not waiting and len(ready) == len(graph.nodes) + + +def _stable_topological_sort( + graph: torch.fx.Graph, + node_to_additional_deps: dict[Node, OrderedSet[Node]], +) -> None: + assert _stable_topological_sort_impl(graph, node_to_additional_deps) + + +def _has_cycle( + graph: torch.fx.Graph, + node_to_additional_deps: dict[Node, OrderedSet[Node]], +) -> bool: + return not _stable_topological_sort_impl( + graph, node_to_additional_deps, do_sort=False + ) + + +def _populate_additional_deps( + graph: torch.fx.Graph, node_to_mutated_arg_positions: dict[Node, OrderedSet[int]] +) -> dict[Node, OrderedSet[Node]]: + node_to_additional_deps: dict[Node, OrderedSet[Node]] = defaultdict(OrderedSet) + _add_mutation_dependencies(node_to_mutated_arg_positions, node_to_additional_deps) + _add_global_state_dependencies(graph, node_to_additional_deps) + return node_to_additional_deps + + +def _add_global_state_dependencies( + graph: torch.fx.Graph, node_to_additional_deps: dict[Node, OrderedSet[Node]] +) -> None: + import torch.amp + + all_nodes = list(graph.nodes) + + # These are targets of the nodes which need to stay in the same relative place in the graph + global_state_targets = {torch.amp._enter_autocast, torch.amp._exit_autocast} + all_nodes_dep_on: list[Node] = [] + + def prev_cur_nodes( + all_nodes: list[Node], + ) -> Generator[tuple[list[Node], Node], None, None]: + prev_nodes: list[Node] = [] + next_nodes = list(reversed(all_nodes)) + + while next_nodes: + cur_node = next_nodes.pop() + yield prev_nodes, cur_node + prev_nodes.append(cur_node) + + for prev_nodes, cur_node in prev_cur_nodes(all_nodes): + args_unique = _get_flat_args_unique(cur_node, {}) + new_deps = [n for n in all_nodes_dep_on if n not in args_unique] + + if new_deps: + additional_deps = node_to_additional_deps[cur_node] + additional_deps.update(new_deps) + + if cur_node.target in global_state_targets: + additional_deps = node_to_additional_deps[cur_node] + additional_deps.update(n for n in prev_nodes if n not in args_unique) + all_nodes_dep_on.append(cur_node) + + +def _add_mutation_dependencies( + node_to_mutated_arg_positions: dict[Node, OrderedSet[int]], + node_to_additional_deps: dict[Node, OrderedSet[Node]], +) -> None: + for node, indices in node_to_mutated_arg_positions.items(): + flat_args_kwargs = _get_flat_args(node, {}) + + # for all mutated args, + # add dependency on usages which occur after node to ensure + # node will always be ordered before them + # also add node as a dependency on usages which + # occur before node to ensure node is ordered after them + for index in indices: + mutated_arg = flat_args_kwargs[index] + for user in mutated_arg.users: + if user is node: + continue + + elif user < node: + node_to_additional_deps[node].add(user) + + elif user > node: + node_to_additional_deps[user].add(node) + + +def _has_aliasing( + region: Region, + inputs: list[Node], + inds_with_external_users: list[int], + flattened_getitem_nodes: OrderedSet[Node], +) -> bool: + input_storages: dict[StorageWeakRef, Node] = dict() + for node in inputs: + if node in flattened_getitem_nodes: + continue + example_value = node.meta["example_value"] + if isinstance(example_value, torch.Tensor): + storage = StorageWeakRef(example_value._typed_storage()) + if storage in input_storages: + # input-input aliasing + log.debug( + "NYI: Failed to substitute region %s due to input-output aliasing detected at nodes %s, %s", + region, + input_storages[storage], + node, + ) + return True + input_storages[storage] = node + output_storages: dict[StorageWeakRef, Node] = dict() + for i in inds_with_external_users: + out_node = region[i] + if out_node in flattened_getitem_nodes: + continue + if out_node: + example_value = out_node.meta["example_value"] + assert not isinstance(example_value, list) + if isinstance(example_value, torch.Tensor): + storage = StorageWeakRef(example_value._typed_storage()) + if storage in output_storages: + # output-output aliasing + log.debug( + "NYI: Failed to substitute region %s due to output-output aliasing detected at nodes %s, %s", + region, + output_storages[storage], + out_node, + ) + return True + output_storages[storage] = out_node + intersected_storages = input_storages.keys() & output_storages.keys() + if len(intersected_storages) > 0: + # input-output aliasing + aliased = [ + (input_storages[s], output_storages[s]) for s in intersected_storages + ] + aliased = ", ".join([f"{i} and {o}" for i, o in aliased]) + log.debug( + "NYI: Failed to substitute region %s due to input-output aliasing detected at nodes %s", + region, + aliased, + ) + return True + return False + + +def _is_tuple_node(node: Node) -> bool: + return isinstance(node.meta["example_value"], tuple) + + +def _get_children_getitems(node: Node) -> Generator[Node, None, None]: + for user in node.users: + if user.target is operator.getitem and isinstance(user.args[1], int): + yield user + + +def _get_flattened_node_indices(node: Node, region: Region) -> OrderedSet[int]: + """Returns an ordered set of indices, each representing a node in the region which will be flattened""" + flattened_node_to_ind = {n: i for i, n in enumerate(region)} + node_indices: OrderedSet[int] = OrderedSet() + queue = deque(_get_children_getitems(node)) + while queue: + cur_node = queue.popleft() + if any(user in region for user in cur_node.users): + node_indices.add(flattened_node_to_ind[cur_node]) + for child in _get_children_getitems(cur_node): + queue.append(child) + return node_indices + + +def _create_getitem_nodes( + node: Node, subgraph_tuple_node: Node, subgraph: torch.fx.Graph +) -> tuple[list[Node], dict[tuple[int, ...], int]]: + tup = node.meta["example_value"] + assert isinstance(tup, tuple), "_get_getitem_children expects tuple" + + getitem_nodes: list[Node] = [] + queue = deque([(e, (i,), subgraph_tuple_node) for i, e in enumerate(tup)]) + path_to_output_index = {} + + while queue: + cur_elem, path, parent = queue.popleft() + + with subgraph.inserting_after(parent): + new_getitem_node = subgraph.create_node( + "call_function", operator.getitem, (parent, path[-1]), {} + ) + new_getitem_node.meta["example_value"] = cur_elem + + path_to_output_index[path] = len(getitem_nodes) + getitem_nodes.append(new_getitem_node) + + if isinstance(cur_elem, tuple): + queue.extend( + [(e, path + (i,), new_getitem_node) for i, e in enumerate(cur_elem)] # type: ignore[arg-type,misc] + ) + + return getitem_nodes, path_to_output_index # type: ignore[return-value] + + +def _replace_tuple_outputs( + node: Node, + output_index: int, + tuple_spec: dict[tuple[int, ...], int], + invoke_subgraph_node: Node, + graph: torch.fx.Graph, +) -> OrderedSet[Node]: + assert _is_tuple_node(node), "_replace_tuple_outputs expects a tuple node" + + queue = deque((c, (c.args[1],)) for c in _get_children_getitems(node)) + erased_nodes: OrderedSet[Node] = OrderedSet() + while queue: + cur_node, path = queue.pop() + + for c in _get_children_getitems(cur_node): + queue.append((c, path + (c.args[1],))) # type: ignore[return-value, arg-type] + + with graph.inserting_after(invoke_subgraph_node): + subgraph_output = graph.create_node( + "call_function", + operator.getitem, + (invoke_subgraph_node, output_index + tuple_spec[path]), # type: ignore[index] + {}, + ) + cur_node.replace_all_uses_with(subgraph_output, propagate_meta=True) + graph.erase_node(cur_node) + erased_nodes.add(cur_node) + + graph.erase_node(node) + erased_nodes.add(node) + return erased_nodes diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/graph_id_filter.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/graph_id_filter.py new file mode 100644 index 0000000000000000000000000000000000000000..173005605b2dbd4b983ed76c28898488a55a8c43 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/graph_id_filter.py @@ -0,0 +1,473 @@ +# mypy: disallow-untyped-defs + +from __future__ import annotations + +import functools +import logging +import re +import warnings +from typing import Any, Generic, TYPE_CHECKING, TypeVar + + +if TYPE_CHECKING: + from collections.abc import Callable + + from torch._guards import CompileId + + +log = logging.getLogger(__name__) + + +class GraphIdFilter: + """ + A filter for matching graph IDs based on various conditions. + Supports individual IDs, ranges, and comparison operators. + """ + + def __init__(self, filter_str: str) -> None: + self._explicit_ids: frozenset[int] = frozenset() + self._conditions: list[tuple[str, int]] = [] + self._parse(filter_str) + + def _parse(self, filter_str: str) -> None: + if not filter_str or not filter_str.strip(): + return + + explicit_ids: set[int] = set() + conditions: list[tuple[str, int]] = [] + + # Pattern for comparison operators (>=, >, <=, <) followed by a number + cmp_pattern = re.compile(r"^(>=|>|<=|<)(\d+)$") + # Pattern for ranges like "10-20" + range_pattern = re.compile(r"^(\d+)-(\d+)$") + + for part in filter_str.split(","): + part = part.strip() + if not part: + continue + + if match := cmp_pattern.match(part): + conditions.append((match.group(1), int(match.group(2)))) + elif match := range_pattern.match(part): + start, end = int(match.group(1)), int(match.group(2)) + explicit_ids.update(range(start, end + 1)) + else: + try: + explicit_ids.add(int(part)) + except ValueError: + log.warning("Invalid graph ID filter: %s", part) + + self._explicit_ids = frozenset(explicit_ids) + self._conditions = conditions + + def __contains__(self, graph_id: int) -> bool: + """Check if the given graph ID matches this filter.""" + if graph_id in self._explicit_ids: + return True + + for op, val in self._conditions: + if op == ">" and graph_id > val: + return True + elif op == ">=" and graph_id >= val: + return True + elif op == "<" and graph_id < val: + return True + elif op == "<=" and graph_id <= val: + return True + + return False + + def __repr__(self) -> str: + # pyrefly: ignore [implicit-any] + parts = [] + if self._explicit_ids: + parts.append(f"ids={sorted(self._explicit_ids)}") + if self._conditions: + parts.append(f"conditions={self._conditions}") + return f"GraphIdFilter({', '.join(parts) if parts else 'empty'})" + + +T = TypeVar("T") + + +class _GraphRouterBase(Generic[T]): + """ + Base class for routing graphs to different values based on their IDs. + + The router parses a configuration string with rules in the format: + "filter1:value1;filter2:value2;..." + + Rules are evaluated in order, and the first matching rule wins. + """ + + def __init__(self, config_str: str, rule_type: str) -> None: + self._rules: list[tuple[GraphIdFilter, T]] = [] + self._values: list[T | None] = [] + self._overflow_value: T | None = None + self._rule_type = rule_type + self._parse(config_str) + self._precompute() + + def _parse_value_str(self, value_str: str) -> T | None: + """Parse a value string into the appropriate type. Returns None to skip.""" + raise NotImplementedError + + def _parse(self, config_str: str) -> None: + if not config_str or not config_str.strip(): + return + + rule_strs = config_str.split(";") + for rule_str in rule_strs: + rule_str = rule_str.strip() + if not rule_str: + continue + + colon_idx = rule_str.find(":") + if colon_idx == -1: + log.warning( + "Invalid %s override rule (missing ':'): %s", + self._rule_type, + rule_str, + ) + continue + + filter_str = rule_str[:colon_idx].strip() + value_str = rule_str[colon_idx + 1 :].strip() + + if not filter_str or not value_str: + log.warning("Invalid %s override rule: %s", self._rule_type, rule_str) + continue + + value = self._parse_value_str(value_str) + if value is not None: + self._rules.append((GraphIdFilter(filter_str), value)) + + def _precompute(self) -> None: + if not self._rules: + return + + # Find max ID from explicit IDs and comparison thresholds + max_id = 0 + for f, _ in self._rules: + if f._explicit_ids: + max_id = max(max_id, *f._explicit_ids) + for _, val in f._conditions: + max_id = max(max_id, val) + + # Pre-compute values for IDs 0 to max_id + for i in range(max_id + 1): + self._values.append(self._match_rules(i)) + + # For IDs > max_id, the result is constant (only unbounded conditions apply) + self._overflow_value = self._match_rules(max_id + 1) + + def _match_rules(self, graph_id: int) -> T | None: + for f, value in self._rules: + if graph_id in f: + return value + return None + + def get_value_for_graph(self, graph_id: int) -> T | None: + """Get the value for a given graph ID. Returns None if no rule matches.""" + if graph_id < len(self._values): + return self._values[graph_id] + return self._overflow_value + + def is_empty(self) -> bool: + """Check if no rules are configured.""" + return len(self._rules) == 0 + + +class GraphBackendRouter(_GraphRouterBase[Any]): + """ + Routes graphs to different backends based on their IDs. + + The router parses a configuration string with rules in the format: + "filter1:backend1;filter2:backend2;..." + + If a graph ID matches multiple rules with different backends, a ValueError + is raised. + + Examples: + "0-5:eager;>5:inductor" - IDs 0-5 use eager, rest use inductor + ">10:aot_eager" - IDs > 10 use aot_eager + "<=3:eager;4-10:aot_eager" - IDs 0-3 use eager, 4-10 use aot_eager + + Supported backends include "eager", "aot_eager", "aot_eager_decomp_partition", + "inductor", and any other registered backend. + """ + + def __init__(self, config_str: str) -> None: + self._backend_names: dict[int, str] = {} + super().__init__(config_str, "backend") + + def _parse_value_str(self, value_str: str) -> Any | None: + """Look up a backend by name.""" + from .backends.registry import lookup_backend + from .eval_frame import cached_backends + + backend = lookup_backend(value_str) + + # Register the backend so its reset() is called during torch._dynamo.reset() + assert backend is not None, "Invalid override backend: " + value_str + cached_backends.setdefault(id(backend), backend) + self._backend_names[id(backend)] = value_str + return backend + + def _match_rules(self, graph_id: int) -> Any | None: + """Match rules with conflict detection for overlapping filters.""" + matches = {id(backend): backend for f, backend in self._rules if graph_id in f} + if len(matches) > 1: + names = [self._backend_names[bid] for bid in matches] + raise ValueError( + f"Conflicting backend override for graph {graph_id}: matched {names}" + ) + if matches: + return next(iter(matches.values())) + return None + + def __repr__(self) -> str: + if not self._rules: + return "GraphBackendRouter(empty)" + return f"GraphBackendRouter({self._rules})" + + +class GraphConfigRouter(_GraphRouterBase[dict[str, Any]]): + """ + Routes graphs to different inductor configs based on their IDs. + + The router parses a configuration string with rules in the format: + "filter1:config1;filter2:config2;..." + + All matching rules are aggregated: configs from all matching rules are merged + into a single dict. Conflicting keys (same key, different value) raise an error. + Config format is "key=value" or "key1=value1,key2=value2" for multiple settings. + + Examples: + "0-5:triton.cudagraph_skip_dynamic_graphs=False" + ">10:triton.cudagraphs=False,triton.cudagraph_trees=False" + + With "0:a=1;>=0:b=2", graph 0 gets {"a": 1, "b": 2} (both rules match). + With "0:a=1;>=0:a=2", graph 0 raises an error (conflicting values for "a"). + With "0:a=1;>=0:a=1", graph 0 gets {"a": 1} (same value is not a conflict). + """ + + def __init__(self, config_str: str) -> None: + super().__init__(config_str, "config") + + @staticmethod + def _parse_scalar_value(value_str: str) -> Any: + """Parse a string value into the appropriate Python type.""" + value_str = value_str.strip() + if value_str.lower() == "true": + return True + if value_str.lower() == "false": + return False + if value_str.lower() == "none": + return None + try: + if "." in value_str: + return float(value_str) + return int(value_str) + except ValueError: + return value_str + + def _match_rules(self, graph_id: int) -> dict[str, Any] | None: + """Aggregate configs from all matching rules. Conflicts raise an error.""" + result: dict[str, Any] = {} + for f, value in self._rules: + if graph_id in f: + for k, v in value.items(): + if k in result and result[k] != v: + raise ValueError( + f"Conflicting config override for graph {graph_id}: " + f"key '{k}' has value {result[k]!r} and {v!r}" + ) + result[k] = v + return result if result else None + + def _parse_value_str(self, value_str: str) -> dict[str, Any] | None: + """Parse a config string like 'key1=val1,key2=val2' into a dict.""" + result: dict[str, Any] = {} + for item in value_str.split(","): + item = item.strip() + if not item: + continue + if "=" not in item: + log.warning("Invalid config item (missing '='): %s", item) + continue + key, value = item.split("=", 1) + result[key.strip()] = self._parse_scalar_value(value) + return result if result else None + + def __repr__(self) -> str: + if not self._rules: + return "GraphConfigRouter(empty)" + return f"GraphConfigRouter({self._rules})" + + +def _get_override_for_compile_id( + compile_id: CompileId | None, + config_str: str, + create_router: Callable[[str], _GraphRouterBase[T]], + label: str, +) -> T | None: + """ + Get the override value for a given CompileId. + + Returns the value from the router, or None if no override applies. + """ + if compile_id is None or not config_str: + return None + + graph_id = compile_id.frame_id + if graph_id is None: + return None + + router = create_router(config_str) + value = router.get_value_for_graph(graph_id) + if value is not None: + log.info("Overriding %s: %s", label, value) + return value + + +@functools.lru_cache +def _create_backend_router(config_str: str) -> GraphBackendRouter: + """Create and cache GraphBackendRouter instances based on config string.""" + return GraphBackendRouter(config_str) + + +@functools.lru_cache +def _validate_backend_names(config_str: str) -> str | None: + """Return an error message if any backend name is invalid, else None.""" + if not config_str or not config_str.strip(): + return None + from .backends.registry import lookup_backend + + for rule_str in config_str.split(";"): + rule_str = rule_str.strip() + if not rule_str or ":" not in rule_str: + continue + backend_name = rule_str[rule_str.find(":") + 1 :].strip() + if not backend_name: + continue + try: + lookup_backend(backend_name) + except Exception: + return ( + f"TORCH_COMPILE_OVERRIDE_BACKENDS: " + f"'{backend_name}' is not a valid backend, " + f"see `torch._dynamo.list_backends()` for available backends" + ) + return None + + +@functools.lru_cache +def _validate_inductor_config_keys(config_str: str) -> str | None: + """Return an error message if any config key is invalid, else None.""" + router = GraphConfigRouter(config_str) + from torch._inductor import config + + for _, config_dict in router._rules: + for key in config_dict: + if not hasattr(config, key): + return ( + f"TORCH_COMPILE_OVERRIDE_INDUCTOR_CONFIGS: " + f"'{key}' is not a valid torch._inductor.config option" + ) + return None + + +@functools.lru_cache +def _validate_dynamo_config_keys(config_str: str) -> str | None: + """Return an error message if any config key is invalid, else None.""" + router = GraphConfigRouter(config_str) + from torch._dynamo import config + + for _, config_dict in router._rules: + for key in config_dict: + if not hasattr(config, key): + return ( + f"TORCH_COMPILE_OVERRIDE_DYNAMO_CONFIGS: " + f"'{key}' is not a valid torch._dynamo.config option" + ) + return None + + +@functools.lru_cache +def _create_inductor_config_router(config_str: str) -> GraphConfigRouter: + """Create and cache GraphConfigRouter for inductor config overrides.""" + return GraphConfigRouter(config_str) + + +@functools.lru_cache +def _create_dynamo_config_router(config_str: str) -> GraphConfigRouter: + """Create and cache GraphConfigRouter for dynamo config overrides. + + Warns that dynamo config overrides are keyed by frame ID and some configs + can affect graph breaks, which may shift frame IDs. + """ + router = GraphConfigRouter(config_str) + if not router.is_empty(): + warnings.warn( + "TORCH_COMPILE_OVERRIDE_DYNAMO_CONFIGS is set. Dynamo config overrides are " + "keyed by frame ID. Some dynamo configs can affect graph breaks, " + "which may alter the number of frames and shift frame IDs, causing " + "overrides to target the wrong graphs.", + ) + return router + + +def get_backend_override_for_compile_id( + compile_id: CompileId | None, + config_str: str, +) -> Any: + """ + Get the backend override for a given CompileId. + + Returns the backend function to use, or None if no override applies. + """ + return _get_override_for_compile_id( + compile_id, + config_str, + _create_backend_router, + "torch.compile backend", + ) + + +def get_inductor_config_override_for_compile_id( + compile_id: CompileId | None, + config_str: str, +) -> dict[str, Any] | None: + """ + Get the inductor config override for a given CompileId. + + Returns a dict of config patches to apply, or None if no override applies. + """ + return _get_override_for_compile_id( + compile_id, + config_str, + _create_inductor_config_router, # type: ignore[arg-type] + "inductor config", + ) + + +def get_dynamo_config_override_for_compile_id( + compile_id: CompileId | None, + config_str: str, +) -> dict[str, Any] | None: + """ + Get the dynamo config override for a given CompileId. + + Returns a dict of config patches to apply, or None if no override applies. + """ + return _get_override_for_compile_id( + compile_id, + config_str, + _create_dynamo_config_router, # type: ignore[arg-type] + "dynamo config", + ) + + +# Keep old name for backwards compatibility +_create_router = _create_backend_router diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/graph_region_tracker.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/graph_region_tracker.py new file mode 100644 index 0000000000000000000000000000000000000000..6df9feb6bad38365b40a22e24aac34ed961105ae --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/graph_region_tracker.py @@ -0,0 +1,503 @@ +""" +This module provides functionality for tracking and managing regions in computational graphs. +It supports graph optimization by identifying and grouping similar regions based on their +structure and behavior. The module implements algorithms for: + +1. Tracking nodes and their relationships in the computational graph +2. Identifying identical or similar regions across the graph +3. Managing graph regions for optimization purposes +4. Supporting deduplication and other graph transformation passes + +The core functionality revolves around the GraphRegionTracker class which maintains +mappings between nodes and their duplicates, enabling efficient graph analysis and +optimization operations. +""" + +from __future__ import annotations + +import copyreg +import io +import logging +import math +import operator +import pickle +from collections import defaultdict, deque +from dataclasses import fields +from typing import Any, TYPE_CHECKING, TypeVar + +import torch._logging +import torch.fx +from torch._subclasses.fake_tensor import FakeTensor +from torch.utils._ordered_set import OrderedSet +from torch.utils._pytree import tree_flatten + +from .graph_utils import _get_flat_args_unique + + +T = TypeVar("T") + + +if TYPE_CHECKING: + from collections.abc import Callable + + from .symbolic_convert import InstructionTranslatorBase + + +Node = torch.fx.Node +Region = list[Node] +IdenticalNodes = list[Node] +GlobalStateKey = tuple[ + bool, + bool, + int, + tuple[bool, bool], + tuple[bool, bool], + torch.dtype, + bool, + bool, + bool, + bool, +] + +log = logging.getLogger(__name__) +graph_expansion_log = torch._logging.getArtifactLogger( + __name__, "graph_region_expansion" +) + + +def debug_log(msg: str, *args) -> None: # type: ignore[no-untyped-def] + graph_expansion_log.debug(msg, *args) + + +def _extract_tensor_metadata_for_node_hash( + x: torch.Tensor, +) -> tuple[Callable[[T], T], tuple[Any, ...]]: + from torch._inductor.codecache import _ident, extract_tensor_metadata_for_cache_key + + out = [] + metadata = extract_tensor_metadata_for_cache_key(x) + for field in fields(metadata): + out.append(getattr(metadata, field.name)) + + return (_ident, tuple(out)) + + +class NodeHashException(Exception): + pass + + +class InputPickler(pickle.Pickler): + def __init__(self) -> None: + from torch._inductor.codecache import _ident + + stream = io.BytesIO() + self._stream = stream + super().__init__(stream) + self.dispatch_table = copyreg.dispatch_table.copy() + self.dispatch_table.update( + { + FakeTensor: _extract_tensor_metadata_for_node_hash, + torch.SymInt: lambda x: (_ident, (str(x),)), + torch.SymBool: lambda x: (_ident, (str(x),)), + torch.SymFloat: lambda x: (_ident, (str(x),)), + } + ) + self.fast = True + + def dumps(self, obj: Any) -> bytes: + """ + Pickle an object and return a byte string. + """ + try: + self.dump(obj) + return self._stream.getvalue() + except (TypeError, AttributeError) as e: + raise NodeHashException from e + finally: + self._stream.seek(0) + self._stream.truncate(0) + + +def _extract_args(arg: Any) -> Any: + if isinstance(arg, Node): + return arg.meta.get("example_value") + elif isinstance(arg, (torch.Tensor, int)): + return arg + else: + return None + + +def _normalize_args( + node: Node, +) -> tuple[tuple[str, ...], tuple[Any | None, ...]]: + flat_args, _ = tree_flatten(node.args) + sorted_kwargs = sorted(node.kwargs.items(), key=operator.itemgetter(0)) + sorted_keys = tuple(sorted(node.kwargs.keys())) + flat_kwargs, _ = tree_flatten(sorted_kwargs) + all_args = flat_args + flat_kwargs + return (sorted_keys, tuple(_extract_args(arg) for arg in all_args)) + + +def _sort_with_ref_region( + index_to_rank: dict[int, int], regions: list[list[Any]] +) -> None: + # sort topologically + # we need to handle edge cases where some nodes have no dependencies + # so first we map each node to its ranking + ref_region = regions[0] + sorted_indices = sorted(range(len(ref_region)), key=lambda i: index_to_rank[i]) + for region in regions: + region[:] = [region[i] for i in sorted_indices] + + +def get_global_state_key() -> GlobalStateKey: + return ( + torch.is_grad_enabled(), + torch.is_inference_mode_enabled(), + torch.get_num_threads(), + torch._C._get_cublas_allow_fp16_reduced_precision_reduction(), + torch._C._get_cublas_allow_bf16_reduced_precision_reduction(), + torch.get_default_dtype(), + torch.are_deterministic_algorithms_enabled(), + torch._C._get_cublas_allow_tf32(), + torch.is_deterministic_algorithms_warn_only_enabled(), + torch._C._autograd._saved_tensors_hooks_is_enabled(), # type: ignore[attr-defined] + ) + + +# This is typical BFS with the caveat +# that a node's children need to be explicitly +# added with the add_children() method +# The flow is yield a node and check if it's valid for all regions +# if not valid, discard and continue onto the next node +# Note: this iterates backward through the graph by looking at args/kwargs +# of a node +class BackwardBfsArgIter: + def __init__(self, origin: Node) -> None: + self._cur: Node | None = origin + self._queue: deque[Node | None] = deque() + + @staticmethod + def create(origin: Node) -> BackwardBfsArgIter: + it = BackwardBfsArgIter(origin) + it.add_children(origin) + # pop the origin node, since it is the origin of + # the region and does not need to be considered for addition + assert it.next() + return it + + def next(self) -> Node | None: + ret = self._cur + if not self._queue: + self._cur = None + else: + self._cur = self._queue.popleft() + return ret + + def peek(self) -> Node | None: + return self._cur + + def add_children(self, node: Node) -> None: + flat_args = _get_flat_args_unique(node, {}) + for arg in flat_args: + if isinstance(arg, Node): + self._append(arg) + + def _append(self, arg: Node) -> None: + if self._cur is None: + self._cur = arg + else: + self._queue.append(arg) + + def __str__(self) -> str: + return f"BackwardBfsArgIter(cur={self._cur}, queue={self._queue})" + + +class GraphRegionTracker: + """ + GraphRegionTracker tracks each node added to the output graph and generates a key based on the source location, + instruction pointer, input shapes, and global state at the time the node is inserted into the graph. Nodes with + the same key are grouped together in a list of identical nodes (the value of node_to_duplicates). + + hash_to_duplicates: Dict[str, IdenticalNodes] - A dictionary mapping the key to a list of identical nodes + node_to_duplicates: Dict[Node, IdenticalNodes] - A dictionary mapping a node to the list of identical nodes it belongs to + input_pickler: InputPickler - An instance of InputPickler used to generate a node hash + """ + + def __init__(self) -> None: + self.hash_to_duplicates: dict[str, IdenticalNodes] = defaultdict(list) + self.node_to_duplicates: dict[Node, IdenticalNodes] = {} + # Note: position is in flattened args/kwargs list + self.node_to_mutated_arg_positions: dict[Node, OrderedSet[int]] = {} + self.input_pickler = InputPickler() + + def _hash_node( + self, filename: str, lineno: int, instruction_pointer: int | None, node: Node + ) -> str: + from torch._inductor.codecache import sha256_hash + + key = ( + get_global_state_key(), + filename, + lineno, + instruction_pointer, + _normalize_args(node), + ) + return sha256_hash(self.input_pickler.dumps(key)) + + def _is_identical(self, n0: Node, n1: Node) -> bool: + return ( + n0 in self.node_to_duplicates + and n1 in self.node_to_duplicates + and self.node_to_duplicates[n0] is self.node_to_duplicates[n1] + and n0 is not n1 + ) + + def track_node(self, tx: InstructionTranslatorBase, node: Node) -> None: + """ + The main entry point for tracking a node. This function will hash the node argument and group + nodes with the same hash together. It updates the hash_to_duplicates and node_to_duplicates dictionaries + to track the new node. + """ + try: + if ( + node not in self.node_to_duplicates + ): # don't allow nodes to be added twice + duplicates = self.hash_to_duplicates[ + self._hash_node( + tx.f_code.co_filename, tx.lineno, tx.instruction_pointer, node + ) + ] + duplicates.append(node) + self.node_to_duplicates[node] = duplicates + except NodeHashException as e: + log.debug("Unable to hash node %s with exception %s", node, e) # noqa: G200 + + def track_node_mutations( + self, + node: Node, + flat_args_kwargs: list[Any], + id_to_initial_version: dict[int, int], + ) -> None: + """ + This function tracks which argument positions are mutated by the given node. Subgraph HOP does not support + input mutations today so we will skip regions which have inputs that are mutated. + """ + mutated_arg_positions = OrderedSet[int]() + for i, arg in enumerate(flat_args_kwargs): + val_id = id(arg) + if ( + val_id in id_to_initial_version + and id_to_initial_version[val_id] != arg._version + ): + mutated_arg_positions.add(i) + + if mutated_arg_positions: + self.node_to_mutated_arg_positions[node] = mutated_arg_positions + + def add_node_mutation( + self, + node: Node, + arg_pos: int, + ) -> None: + if node in self.node_to_mutated_arg_positions: + self.node_to_mutated_arg_positions[node].add(arg_pos) + else: + self.node_to_mutated_arg_positions[node] = OrderedSet([arg_pos]) + + def get_identical_regions(self, graph: torch.fx.Graph) -> list[list[Region]]: + """ + This function is responsible for extracting the largest regions of identical nodes from the given graph. + **Note**: This function assumes the nodes that have been tracked with track_node are in the provided graph argument. + + The algorithm proceeds as follows: + The nodes tracked via track_node above are organized into region groups. The initial region groups look like this: + [[IdenticalNode1], [IdenticalNode2], [IdenticalNode3]] and each sublist is called a region. For each region group + (starting at the topologically latest region group), the inner regions are gradually expanded one node at time from + the flattened args and kwargs of the node in each region provided that for all regions in the group, the nodes being + added are also identical (ie have the same key computed by track_node). This is checked by verifying that the two + nodes have the same identical node list in node_to_duplicates. + """ + topological_ranking = {node: i for i, node in enumerate(graph.nodes)} + region_groups_with_rank = [] + # needed to detect if replacing a region will create cycles + node_to_recursive_ancestors = _populate_recursive_ancestor_map(graph) + + # Create region groups; a region group is a group + # of regions that are all identical. In this initial state + # each region in the group is a single node, and we discard + # groups that are only a single region. + # We track the topological ranking to start with groups later in the graph + # the reason for this is that we will necessarily create the largest groups first. + for group in self.hash_to_duplicates.values(): + if len(group) > 1: + # pyrefly: ignore [implicit-any] + region_group = [] + min_rank = math.inf + + for node in group: + # some nodes aren't in the topo ranking? + if node in topological_ranking: + min_rank = min(min_rank, topological_ranking[node]) + region_group.append([node]) + + if len(region_group) > 1: + region_groups_with_rank.append((region_group, min_rank)) + + region_groups_with_rank.sort(key=lambda rg: -rg[1]) + region_groups = [rg for rg, _ in region_groups_with_rank] + + # We start from regions later in the graph and expand them earlier + # as a result, we will create the largest regions first and they won't + # overlap. + seen_nodes: set[Node] = set() + for region_group in region_groups: + fully_expand_region_group( + region_group, + seen_nodes, + node_to_recursive_ancestors, + self._is_identical, + ) + # sort topologically + # we need to handle edge cases where some nodes have no dependencies + # so first we map each node to its ranking, + ref_region = region_group[0] + index_to_rank = { + index: topological_ranking[n] for index, n in enumerate(ref_region) + } + _sort_with_ref_region(index_to_rank, region_group) + + return [ + region_group for region_group in region_groups if len(region_group[0]) > 1 + ] + + def __str__(self) -> str: + return f"GraphRegionTracker(hash_to_duplicates={self.hash_to_duplicates}, node_to_duplicates={self.node_to_duplicates})" + + +class RegionWrapper: + """Holds state for regions e.g. ancestors and new candidate nodes for consideration""" + + def __init__( + self, region: Region, node_to_recursive_ancestors: dict[Node, set[Node]] + ) -> None: + assert len(region) == 1, "all regions should start with one node" + node = region[0] + self.node_to_recursive_ancestors = node_to_recursive_ancestors + self.iter = BackwardBfsArgIter.create(node) + self.nodes_unique = OrderedSet([node]) + self.ancestors = set(node_to_recursive_ancestors[node]) + self.region = region + + def next_candidate(self) -> Node | None: + return self.iter.next() + + def will_inclusion_create_cycle(self, node: Node) -> bool: + external_users = [user for user in node.users if user not in self.nodes_unique] + for user in external_users: + if user in self.ancestors: + return True + + return False + + def add(self, node: Node) -> None: + self.nodes_unique.add(node) + self.region.append(node) + self.iter.add_children(node) + self.ancestors.update(self.node_to_recursive_ancestors[node]) + + +def fully_expand_region_group( + regions: list[Region], + seen_nodes: set[Node], + node_to_recursive_ancestors: dict[Node, set[Node]], + is_identical_fn: Callable[[Node, Node], bool], +) -> None: + debug_log("--------------------------------------------------") + debug_log("expanding new region group: %s", regions) + + # All regions should start with 1 node + assert all(len(region) == 1 for region in regions) + region_wrappers = [ + RegionWrapper(region, node_to_recursive_ancestors) for region in regions + ] + + nodes_to_add = OrderedSet[Node]() + current_node = region_wrappers[0].next_candidate() + + # No children + if current_node is None: + return + + # Loop incrementally adding new nodes to each region + # regions are only expanded if the node to add is valid + # for ALL regions + while current_node: + add_to_all_regions = not region_wrappers[0].will_inclusion_create_cycle( + current_node + ) + nodes_to_add.clear() + nodes_to_add.add(current_node) + for region_wrapper in region_wrappers[1:]: + candidate = region_wrapper.next_candidate() + + debug_log("--------------------") + debug_log( + "considering candidate: %s, cur_node: %s", candidate, current_node + ) + + if not candidate or not add_to_all_regions: + add_to_all_regions = False + continue + + debug_log( + "candidate in previously claimed nodes?: %s", candidate in seen_nodes + ) + debug_log("is_identical: %s", is_identical_fn(candidate, current_node)) + + add_to_all_regions &= ( + candidate not in seen_nodes + and candidate not in nodes_to_add + and candidate.op != "placeholder" + and candidate.op != "get_attr" + and is_identical_fn(candidate, current_node) + and not region_wrapper.will_inclusion_create_cycle(candidate) + ) + nodes_to_add.add(candidate) + + debug_log(f"add_to_all_regions: {add_to_all_regions}") + debug_log("--------------------") + + if add_to_all_regions: + assert len(region_wrappers) == len(nodes_to_add), ( + "Number of nodes to add must equal the number of regions" + ) + for region_wrapper, node in zip(region_wrappers, nodes_to_add): + region_wrapper.add(node) + debug_log("adding %s's children", node) + debug_log("%s %s", node.args, list(node.kwargs.items())) + seen_nodes.add(node) + + current_node = region_wrappers[0].next_candidate() + + # Ensure regions are sorted in topological order + for region in regions: + region.reverse() + + debug_log("end expand new region group: %s", regions) + debug_log("--------------------------------------------------") + + +def _populate_recursive_ancestor_map(graph: torch.fx.Graph) -> dict[Node, set[Node]]: + node_to_recursive_ancestors: dict[Node, set[Node]] = {} + for node in graph.nodes: + node_to_recursive_ancestors[node] = set() + for node in graph.nodes: + all_args = _get_flat_args_unique(node, {}) + for arg in all_args: + if isinstance(arg, Node): + node_to_recursive_ancestors[node].update( + node_to_recursive_ancestors[arg] + ) + node_to_recursive_ancestors[node].add(arg) + return node_to_recursive_ancestors diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/graph_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/graph_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..68aea6f96e102b8d0f4808ffeacd4fa95f4cd023 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/graph_utils.py @@ -0,0 +1,117 @@ +from typing import Any + +import torch +from torch.fx import Graph, map_arg, Node +from torch.utils._ordered_set import OrderedSet +from torch.utils._pytree import tree_flatten + + +# flattens with support for slices +# Note: a better way to do this would +# be register/unregister slices as pytree nodes +# but there is no unregister API in the pytorch +# pytree impl +def _get_flat_args( + node: Node, node_to_additional_deps: dict[Node, OrderedSet[Node]] +) -> list[Node]: + args = list[Any]() + map_arg((node.args, node.kwargs), args.append) + if node in node_to_additional_deps: + args.extend(node_to_additional_deps[node]) + return args + + +def _get_flat_args_unique( + node: Node, node_to_additional_deps: dict[Node, OrderedSet[Node]] +) -> OrderedSet[Node]: + args = OrderedSet[Node]() + map_arg((node.args, node.kwargs), args.add) + if node in node_to_additional_deps: + args.update(node_to_additional_deps[node]) + return args + + +def _detect_cycles( + graph: Graph, node_to_additional_deps: dict[Node, OrderedSet[Node]] +) -> str: + # States: 0=Unvisited, 1=Visiting, 2=Visited(Safe) + state: dict[Node, int] = {} + + for root in reversed(graph.nodes): + if root in state: + continue + + # Stack holds (current_node, children_iterator). + # Using an iterator allows us to pause and resume processing a node's children. + stack = [(root, iter(_get_flat_args_unique(root, node_to_additional_deps)))] + state[root] = 1 # Visiting + + while stack: + parent, children = stack[-1] + + try: + child = next(children) + + if not isinstance(child, Node): + continue + + child_state = state.get(child, 0) + + if child_state == 1: + # Back-edge: child is on the current DFS path -> cycle + cycle_path = [node for node, _ in stack] + [child] + return f"cycle detected in path: {cycle_path}" + + if child_state == 0: + state[child] = 1 + stack.append( + ( + child, + iter(_get_flat_args_unique(child, node_to_additional_deps)), + ) + ) + # child_state == 2 means already verified safe; skip. + + except StopIteration: + # All children processed — mark safe and pop. + stack.pop() + state[parent] = 2 + + return "no cycle detected" + + +def _graph_device_type(graph: Graph | None) -> str: + if graph is None: + return "cpu" + + def _device_type(x: Any) -> str: + if isinstance(x, torch.device): + return x.type + if isinstance(x, torch.Tensor): + return x.device.type + return "cpu" + + def _flatten_meta(node: Node, key: str) -> list[Any]: + if key not in node.meta: + return [] + flat, _ = tree_flatten(node.meta[key]) + return flat + + for node in graph.nodes: + for key in ("val", "example_value"): + for obj in _flatten_meta(node, key): + return _device_type(obj) + + # Check for device conversions + if node.op == "call_method": + for gpu in ["cuda", "xpu"]: + if node.target == gpu: + return gpu + if node.target == "to" and gpu in node.args: + return gpu + + # Check args/kwargs for non-CPU device specs + flat_args, _ = tree_flatten((node.args, node.kwargs)) + for obj in flat_args: + return _device_type(obj) + return "cpu" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/guards.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/guards.py new file mode 100644 index 0000000000000000000000000000000000000000..2687535b316c0786fc5cb85020b056d3b7ae7879 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/guards.py @@ -0,0 +1,5354 @@ +""" +Core guard system for Dynamo that detects when compiled code needs to be recompiled due to +changes in program state. Guards are conditions that must remain true for previously-compiled +code to be valid for reuse. + +This module provides the infrastructure for creating, managing and checking guards, including: +- Guard creation and composition +- Guard state management and invalidation +- Guard checking and failure handling +- Utilities for guard optimization and debugging +- Integration with Dynamo's compilation caching + +The guard system is critical for Dynamo's ability to efficiently reuse compiled code while +maintaining correctness by detecting when recompilation is necessary due to changes in +program state, tensor properties, or control flow. +""" + +from __future__ import annotations + +import ast +import builtins +import collections +import dataclasses +import enum +import functools +import importlib +import inspect +import io +import itertools +import logging +import math +import pickle +import sys +import textwrap +import traceback +import types +import warnings +import weakref +from contextlib import contextmanager +from copy import deepcopy +from inspect import currentframe +from typing import Any, NamedTuple, NoReturn, TYPE_CHECKING +from typing_extensions import LiteralString, TypeAliasType, TypeVar +from weakref import ReferenceType + +import torch +import torch.overrides +import torch.utils._device +from torch._C._dynamo.eval_frame import code_framelocals_names +from torch._C._dynamo.guards import ( + check_obj_id, + check_type_id, + ClosureGuardAccessor, + CodeGuardAccessor, + dict_version, + DictGetItemGuardAccessor, + DictGuardManager, + FuncDefaultsGuardAccessor, + FuncKwDefaultsGuardAccessor, + GetAttrGuardAccessor, + GetGenericDictGuardAccessor, + GuardAccessor, + GuardDebugInfo, + GuardManager, + install_no_tensor_aliasing_guard, + install_object_aliasing_guard, + install_storage_overlapping_guard, + install_symbolic_shape_guard, + LeafGuard, + profile_guard_manager, + RelationalGuard, + RootGuardManager, + TupleGetItemGuardAccessor, + TypeDictGuardAccessor, + TypeGuardAccessor, + TypeMROGuardAccessor, +) +from torch._dynamo.source import ( + get_global_source_name, + get_local_source_name, + IndexedSource, + is_from_flatten_script_object_source, + is_from_local_source, + is_from_optimizer_source, + is_from_skip_guard_source, + is_from_unspecialized_builtin_nn_module_source, + TensorProperty, + TensorPropertySource, +) +from torch._dynamo.utils import CompileEventLogger, get_metrics_context +from torch._guards import ( + CompileContext, + CompileId, + DuplicateInputs, + Guard, + GuardBuilderBase, + GuardEnvExpr, + GuardSource, + Source, + StorageOverlap, +) +from torch._inductor.utils import IndentedBuffer +from torch._library.opaque_object import get_opaque_obj_info, is_opaque_value_type +from torch._logging import structured +from torch._utils_internal import justknobs_check +from torch.fx.experimental.symbolic_shapes import ( + _CppShapeGuardsHelper, + _ShapeGuardsHelper, + EqualityConstraint, + is_symbolic, + SYMPY_INTERP, +) +from torch.utils import _pytree as pytree +from torch.utils._ordered_set import OrderedSet +from torch.utils._traceback import format_frame, report_compile_source_on_error +from torch.utils.weak import TensorWeakRef + +from . import config, convert_frame, exc +from .eval_frame import set_guard_error_hook +from .source import ( + AttrProxySource, + AttrSource, + CallFunctionNoArgsSource, + CallMethodItemSource, + CellContentsSource, + ChainedSource, + ClosureSource, + CodeSource, + ConstantSource, + ConstDictKeySource, + CurrentStreamSource, + DataclassFieldsSource, + DefaultsSource, + DictGetItemSource, + DictSubclassGetItemSource, + DynamicScalarSource, + FlattenScriptObjectSource, + FloatTensorSource, + FSDPNNModuleSource, + GenericAttrSource, + GetItemSource, + GlobalSource, + GlobalStateSource, + GlobalWeakRefSource, + GradSource, + ImportSource, + ListGetItemSource, + LocalSource, + NamedTupleFieldsSource, + NNModuleSource, + NonSerializableSetGetItemSource, + NumpyTensorSource, + OptimizerSource, + ScriptObjectQualifiedNameSource, + ShapeEnvSource, + SubclassAttrListSource, + TorchFunctionModeStackSource, + TupleIteratorGetItemSource, + TypeDictSource, + TypeMROSource, + TypeSource, + UnspecializedBuiltinNNModuleSource, + UnspecializedNNModuleSource, + UnspecializedParamBufferSource, + WeakRefCallSource, +) +from .types import ( # noqa: F401 + CacheEntry, + DynamoFrameType, + ExtraState, + GuardedCode, + GuardFail, + GuardFilterEntry, + GuardFn, +) +from .utils import ( + builtin_dict_keys, + common_constant_types, + dataclass_fields, + dict_keys, + get_current_stream, + get_torch_function_mode_stack, + get_torch_function_mode_stack_at, + guard_failures, + istype, + key_is_id, + key_to_id, + normalize_count_iter, + normalize_range_iter, + orig_code_map, + tensor_always_has_static_shape, + tuple_iterator_getitem, + tuple_iterator_len, + verify_guard_fn_signature, +) + + +if TYPE_CHECKING: + from collections.abc import Callable + + +guard_manager_testing_hook_fn: Callable[[Any, Any, Any], Any] | None = None +_COUNT_ITERATOR_TYPE = type(itertools.count()) + +try: + import numpy as np +except ModuleNotFoundError: + np = None # type: ignore[assignment] + + +if TYPE_CHECKING: + from collections.abc import Generator, KeysView, Sequence + + from sympy import Symbol + + from torch._C import DispatchKeySet + from torch._dynamo.output_graph import OutputGraphCommon, OutputGraphGuardsState + from torch._dynamo.package import SerializedCode + +T = TypeVar("T") +log = logging.getLogger(__name__) +guards_log = torch._logging.getArtifactLogger(__name__, "guards") +recompiles_log = torch._logging.getArtifactLogger(__name__, "recompiles") +recompiles_verbose_log = torch._logging.getArtifactLogger( + __name__, "recompiles_verbose" +) +verbose_guards_log = torch._logging.getArtifactLogger(__name__, "verbose_guards") + + +dunder_attrs_assumed_constants = ( + "__defaults__", + "__kwdefaults__", + "__code__", + "__closure__", + "__annotations__", + "__func__", + "__mro__", +) + + +def get_framelocals_idx(code: types.CodeType, var_name: str) -> int: + # Refer to index in the frame's localsplus directly. + # NOTE: name order for a code object doesn't change. + # NOTE: we need to find the LAST matching index because <= 3.10 contains + # duplicate names in the case of cells: a name can be both local and cell + # and will take up 2 slots of the frame's localsplus. The correct behavior + # is to refer to the cell, which has a higher index. + framelocals_names_reversed = code_framelocals_names_reversed_cached(code) + framelocals_idx = ( + len(framelocals_names_reversed) - framelocals_names_reversed.index(var_name) - 1 + ) + return framelocals_idx + + +class IndentedBufferWithPrefix(IndentedBuffer): + def prefix(self) -> str: + return "| " * (self._indent * self.tabwidth) + + def writeline(self, line: str, skip_prefix: bool = False) -> None: # type: ignore[override] + if skip_prefix: + super().writeline(line) + else: + super().writeline("+- " + line) + + +class GuardManagerWrapper: + """ + A helper class that contains the root guard manager. An instance of this + class is stored in the Dynamo cache entry, so that the cache entry can + access the RootGuardManager stored in the "root" attribute and directly call + the check_nopybind from C++. + """ + + def __init__(self, root: RootGuardManager | None = None) -> None: + if root is None: + self.root = RootGuardManager() + else: + self.root = root + + self.diff_guard_root: RootGuardManager | None = None + self.closure_vars: dict[str, Any] | None = None + self.args: list[str] | None = None + self.code_parts: list[str] = [] + self.verbose_code_parts: list[str] | None = None + self.global_scope: dict[str, Any] | None = None + self.guard_fail_fn: Callable[[GuardFail], None] | None = None + self.cache_entry: CacheEntry | None = None + self.extra_state: ExtraState | None = None + self.id_matched_objs: dict[str, ReferenceType[object]] = {} + self.no_tensor_aliasing_sources: list[str] = [] + + self.printed_relational_guards: set[RelationalGuard] = set() + + self.diff_guard_sources: OrderedSet[str] = OrderedSet() + + @contextmanager + def _preserve_printed_relational_guards(self) -> Generator[None, None, None]: + self.printed_relational_guards = set() + try: + yield + finally: + self.printed_relational_guards = set() + + # TODO: clarify what fn and attributes guard manager has to get the right things here + def collect_diff_guard_sources(self) -> OrderedSet[str]: + # At the time of finalize, we have only marked guard managers with + # TENSOR_MATCH guards as diff guard managers. So, we do a tree traversal + # and collect all the nodes in the tree (branches) that lead to tensor + # guards. + + # After a recompilation, some of guard managers will have a fail_count > + # 0, so we collect them as well. Later on, we accumulate the diff guard + # sources for all the guard managers. + + def visit_dict_manager(node: DictGuardManager) -> bool: + is_diff_guard_node = ( + node.get_source() in self.diff_guard_sources or node.fail_count() > 0 + ) + for _idx, (key_mgr, val_mgr) in sorted( + node.get_key_value_managers().items() + ): + is_diff_guard_node |= visit(key_mgr) | visit(val_mgr) + + if is_diff_guard_node: + self.diff_guard_sources.add(node.get_source()) + + return is_diff_guard_node + + def visit_manager(node: GuardManager) -> bool: + assert not isinstance(node, DictGuardManager) + + is_diff_guard_node = ( + node.get_source() in self.diff_guard_sources or node.fail_count() > 0 + ) + for child_mgr in node.get_child_managers(): + is_diff_guard_node |= visit(child_mgr) + + if is_diff_guard_node: + self.diff_guard_sources.add(node.get_source()) + + return is_diff_guard_node + + def visit(node: GuardManager) -> bool: + if node is None: + return False + if isinstance(node, DictGuardManager): + return visit_dict_manager(node) + return visit_manager(node) + + visit(self.root) + + return self.diff_guard_sources + + def finalize(self) -> None: + if config.use_recursive_dict_tags_for_guards and justknobs_check( + "pytorch/compiler:use_recursive_dict_tags_for_guards" + ): + self.find_tag_safe_roots() + self.prepare_diff_guard_manager() + + def prepare_diff_guard_manager(self) -> None: + self.collect_diff_guard_sources() + self.populate_diff_guard_manager() + + def find_tag_safe_roots(self) -> None: + """ + Identify ``tag safe nodes`` and ``tag safe roots`` within a guard tree. + + ----------------------------------------------------------------------- + tag safe node + ----------------------------------------------------------------------- + A *tag safe node* is a ``GuardManager`` whose guarded value satisfies one + of the following conditions: + + 1. Immutable value - The value is intrinsically immutable according to + ``is_immutable_object``. Tensors are considered immutable. To ensure + that symbolic guards run, we also check that the GuardManager has no + accessors. + + 2. Nested tag safe dictionary - The value is a ``dict`` whose keys and + values are all tag safe nodes (checked recursively). Such dictionaries + allow entire nested structures to be skipped once their identity tag + matches. + + 3. Pure ``nn.Module`` - The value is an ``nn.Module`` whose sole + accessor is ``GetGenericDictGuardAccessor``—i.e., it only exposes its + ``__dict__`` and nothing else that could mutate between runs. + + For every tag safe node, verifying the identity/tag of just the top-level + dictionary is enough to guarantee the entire subtree is unchanged, enabling + a *fast-path* guard check. + + ----------------------------------------------------------------------- + tag safe root + ----------------------------------------------------------------------- + A ``tag safe root`` is a tag safe node whose parent is not tag safe. + These boundary nodes mark the points where guard evaluation can safely + prune traversal: if a tag-safe root's dictionary tag matches, the entire + subtree beneath it is skipped. + + One strong requirement for tag safe root is for the guarded object to + support weakref. Refer to more details in the Recursive dict tag + matching note. In short, we need to save the weakref of the object on + first invocation, and check if it is still valid in later iterations, to + apply recursive dict tag optimizations. `dict` objects do NOT support + weakref. Therefore, as of now, we only mark nn module related guard + managers as tag safe roots. + + Algorithm + --------- + The search runs in post-order traversal + + 1. Visit leaves and classify them as tag safe or not. + 2. Propagate tag-safety upward: a parent dictionary becomes tag safe only if + all of its children are already tag-safe. + 3. Propagate tag-safe-rootness upward: if the whole subtree is tag safe, + the current node becomes the new tag safe root, otherwise propagate the + subtree tag safe roots. + 4. Collect every tag safe node and, by inspecting parent tags, label the + subset that are tag safe roots. + """ + + def check_tag_safety( + node: GuardManager, accepted_accessors: tuple[type[GuardAccessor], ...] + ) -> bool: + accessors = node.get_accessors() + child_mgrs = node.get_child_managers() + return all( + isinstance(accessor, accepted_accessors) and mgr.is_tag_safe() + for accessor, mgr in zip(accessors, child_mgrs) + ) + + def visit_dict_manager(node: DictGuardManager) -> list[GuardManager]: + # Just recurse through the key and value dict managers and check if + # all of them are tag safe nodes. + assert issubclass(node.get_type_of_guarded_value(), dict) + + tag_safe_roots = [] + is_subtree_tag_safe = True + + # Recurse to get the tag safe roots from subtree. + for _idx, (key_mgr, val_mgr) in sorted( + node.get_key_value_managers().items() + ): + if key_mgr is not None: + visit(key_mgr) + if val_mgr is not None: + tag_safe_roots.extend(visit(val_mgr)) + + for key_mgr, val_mgr in node.get_key_value_managers().values(): + if key_mgr: + is_subtree_tag_safe &= key_mgr.is_tag_safe() + + if val_mgr: + is_subtree_tag_safe &= val_mgr.is_tag_safe() + + if is_subtree_tag_safe: + node.mark_tag_safe() + return tag_safe_roots + + def visit_manager(node: GuardManager) -> list[GuardManager]: + assert not isinstance(node, DictGuardManager) + + # Collect the subtree tag safe roots + tag_safe_roots = [] + for child_mgr in node.get_child_managers(): + tag_safe_roots.extend(visit(child_mgr)) + + if node.is_guarded_value_immutable(): + # If the node guards a tensor, mark it tag safe only if there + # are no accessors. Presence of accessors means presence of + # symbolic shape guards. + if issubclass(node.get_type_of_guarded_value(), torch.Tensor): + if node.has_no_accessors() and not node.has_object_aliasing_guard(): + node.mark_tag_safe() + else: + node.mark_tag_safe() + elif issubclass(node.get_type_of_guarded_value(), dict): + accessors = node.get_accessors() + child_mgrs = node.get_child_managers() + is_subtree_tag_safe = all( + isinstance(accessor, DictGetItemGuardAccessor) and mgr.is_tag_safe() + for accessor, mgr in zip(accessors, child_mgrs) + ) + if is_subtree_tag_safe: + node.mark_tag_safe() + elif issubclass(node.get_type_of_guarded_value(), torch.nn.Module): + is_subtree_tag_safe = check_tag_safety( + node, (GetGenericDictGuardAccessor, TypeGuardAccessor) + ) + if is_subtree_tag_safe: + node.mark_tag_safe() + # Return the current node as tag safe root, discarding the + # subtree tag safe roots. + return [ + node, + ] + elif ( + node.get_type_of_guarded_value() + in ( + types.FunctionType, + types.MethodType, + staticmethod, + classmethod, + ) + and config.assume_dunder_attributes_remain_unchanged + ): + # Assumption: callers will not reassignthe attributes + # func.__code__, func.__closure__, func.__defaults__, or func.__kwdefaults__. + # Mutating the objects those attributes point to is fine; + # rebinding the attribute itself is not. + # Example ─ allowed: foo.__defaults__[0].bar = 99 + # forbidden: foo.__defaults__ = (3, 4) + is_subtree_tag_safe = check_tag_safety( + node, + ( + CodeGuardAccessor, + ClosureGuardAccessor, + FuncDefaultsGuardAccessor, + FuncKwDefaultsGuardAccessor, + GetAttrGuardAccessor, + ), + ) + + for accessor in node.get_accessors(): + if isinstance(accessor, GetAttrGuardAccessor): + is_subtree_tag_safe &= ( + accessor.get_attr_name() in dunder_attrs_assumed_constants + ) + + if is_subtree_tag_safe: + node.mark_tag_safe() + elif issubclass(node.get_type_of_guarded_value(), types.CellType): + is_subtree_tag_safe = check_tag_safety(node, (GetAttrGuardAccessor,)) + + is_subtree_tag_safe &= all( + isinstance(accessor, GetAttrGuardAccessor) + and accessor.get_attr_name() == "cell_contents" + for accessor in node.get_accessors() + ) + if is_subtree_tag_safe: + node.mark_tag_safe() + elif ( + issubclass(node.get_type_of_guarded_value(), tuple) + and node.get_source().endswith(dunder_attrs_assumed_constants) + and config.assume_dunder_attributes_remain_unchanged + ): + # We trust tuples obtained from a function's __closure__ or + # __defaults__. Any *other* tuple-valued attribute can be + # silently replaced—for example: + # + # foo.bar = (1, 2) # original + # foo.bar = (3, 4) # rebinding that our dict-tag optimisation won't see + # + # Therefore only tuples from __closure__ / __defaults__ participate in the + # recursive-dict-tag optimization; all others are ignored. + is_subtree_tag_safe = check_tag_safety( + node, (TupleGetItemGuardAccessor,) + ) + if is_subtree_tag_safe: + node.mark_tag_safe() + elif issubclass(node.get_type_of_guarded_value(), type): + is_subtree_tag_safe = check_tag_safety( + node, (TypeDictGuardAccessor, TypeMROGuardAccessor) + ) + if is_subtree_tag_safe: + node.mark_tag_safe() + + return tag_safe_roots + + def visit(node: GuardManager) -> list[GuardManager]: + if node is None: + return [] + if isinstance(node, DictGuardManager): + return visit_dict_manager(node) + return visit_manager(node) + + tag_safe_roots = visit(self.root) + for node in tag_safe_roots: + if issubclass(node.get_type_of_guarded_value(), torch.nn.Module): + node.mark_tag_safe_root() + + def populate_diff_guard_manager(self) -> None: + self.diff_guard_root = self.clone_with_chosen_sources(self.diff_guard_sources) + + # Ensure that that C++ side points to the updated diff guard manager. + # When a new GuardManagerWrapper is created, it does not have a + # cache_entry attribute, so it relies on the CacheEntry constructor to + # set the diff_guard_root in C++. But once it is saved in the Dynamo + # cache, C++ side adds a cache_entry attribute. On recompiles, this + # cache_entry is visible, so we update the C++ side to point to the + # update guard manager. + if self.cache_entry: + self.cache_entry.update_diff_guard_root_manager() + + def clone_with_chosen_sources( + self, chosen_sources: OrderedSet[str] + ) -> RootGuardManager: + def filter_fn(node_mgr: GuardManager) -> bool: + return node_mgr.get_source() in chosen_sources + + return self.root.clone_manager(filter_fn) + + def get_guard_lines(self, guard: LeafGuard) -> list[str]: + guard_name = guard.__class__.__name__ + parts = guard.verbose_code_parts() + parts = [guard_name + ": " + part for part in parts] + return parts + + def get_manager_line( + self, guard_manager: GuardManager, accessor_str: str | None = None + ) -> str: + source = guard_manager.get_source() + t = guard_manager.__class__.__name__ + s = t + ": source=" + source + if accessor_str: + s += ", " + accessor_str + s += f", type={guard_manager.get_type_of_guarded_value()}" + s += f", tag_safe=({guard_manager.is_tag_safe()}, {guard_manager.is_tag_safe_root()})" + return s + + def construct_dict_manager_string( + self, mgr: DictGuardManager, body: IndentedBufferWithPrefix + ) -> None: + for idx, (key_mgr, val_mgr) in sorted(mgr.get_key_value_managers().items()): + body.writeline(f"KeyValueManager pair at index={idx}") + with body.indent(): + if key_mgr: + body.writeline(f"KeyManager: {self.get_manager_line(key_mgr)}") + self.construct_manager_string(key_mgr, body) + + if val_mgr: + body.writeline(f"ValueManager: {self.get_manager_line(val_mgr)}") + self.construct_manager_string(val_mgr, body) + + def construct_manager_string( + self, mgr: GuardManager, body: IndentedBufferWithPrefix + ) -> None: + with body.indent(): + for guard in mgr.get_leaf_guards(): + if isinstance(guard, RelationalGuard): + if guard not in self.printed_relational_guards: + self.printed_relational_guards.add(guard) + + body.writelines(self.get_guard_lines(guard)) + else: + body.writelines( + [ + guard.__class__.__name__, + ] + ) + else: + body.writelines(self.get_guard_lines(guard)) + + # This works for both DictGuardManager and SubclassedDictGuardManager + if isinstance(mgr, DictGuardManager): + self.construct_dict_manager_string(mgr, body) + + # General case of GuardManager/RootGuardManager + for accessor, child_mgr in zip( + mgr.get_accessors(), mgr.get_child_managers() + ): + body.writeline( + self.get_manager_line(child_mgr, f"accessed_by={accessor.repr()}") + ) + self.construct_manager_string(child_mgr, body) + + def __str__(self) -> str: + with self._preserve_printed_relational_guards(): + body = IndentedBufferWithPrefix() + body.tabwidth = 1 + body.writeline("", skip_prefix=True) + body.writeline("TREE_GUARD_MANAGER:", skip_prefix=True) + body.writeline("RootGuardManager") + self.construct_manager_string(self.root, body) + if hasattr(self.root, "get_epilogue_lambda_guards"): + for guard in self.root.get_epilogue_lambda_guards(): + body.writelines(self.get_guard_lines(guard)) + return body.getvalue() + + def check(self, x: Any) -> bool: + # Only needed for debugging purposes. + return self.root.check(x) + + def check_verbose(self, x: Any) -> GuardDebugInfo: + # Only needed for debugging purposes. + return self.root.check_verbose(x) + + def populate_code_parts_for_debugging(self) -> None: + # This should be called when the guard manager is fully populated + relational_guards_seen = set() + + def get_code_parts(leaf_guard: LeafGuard) -> list[str]: + code_parts = [] + for verbose_code_part in leaf_guard.verbose_code_parts(): + code_part = verbose_code_part.split("#")[0].rstrip() + code_parts.append(code_part) + return code_parts + + def visit(mgr: GuardManager) -> None: + nonlocal relational_guards_seen + for guard in mgr.get_leaf_guards(): + if isinstance(guard, RelationalGuard): + if guard not in relational_guards_seen: + self.code_parts.extend(get_code_parts(guard)) + relational_guards_seen.add(guard) + else: + self.code_parts.extend(get_code_parts(guard)) + + for child_mgr in mgr.get_child_managers(): + visit(child_mgr) + + visit(self.root) + + +def from_numpy(a: Any) -> torch.Tensor: + # If not numpy array, piggy back on e.g. tensor guards to check type + # Re-enable torch function since we disable it on leaf guards + # we need it to properly construct the tensor if a default device is set + with torch.overrides._enable_torch_function(): + # pyrefly: ignore [missing-attribute] + return torch.as_tensor(a) if isinstance(a, (np.generic, np.ndarray)) else a + + +# For user stack printing +@functools.cache +def uninteresting_files() -> set[str]: + import torch._dynamo.external_utils + import torch._dynamo.polyfills + + mods = [torch._dynamo.external_utils, torch._dynamo.polyfills] + + from torch._dynamo.polyfills.loader import POLYFILLED_MODULES + + # pyrefly: ignore [bad-argument-type] + mods.extend(POLYFILLED_MODULES) + + return {inspect.getfile(m) for m in mods} + + +_CLOSURE_VARS: dict[str, object] | None = None + + +def _get_closure_vars() -> dict[str, object]: + global _CLOSURE_VARS + if _CLOSURE_VARS is None: + _CLOSURE_VARS = { + "___check_type_id": check_type_id, + "___check_obj_id": check_obj_id, + "___odict_getitem": collections.OrderedDict.__getitem__, + "___key_to_id": key_to_id, + "___dict_version": dict_version, + "___dict_contains": lambda a, b: dict.__contains__(b, a), + "___tuple_iterator_len": tuple_iterator_len, + "___normalize_count_iter": normalize_count_iter, + "___normalize_range_iter": normalize_range_iter, + "___tuple_iterator_getitem": tuple_iterator_getitem, + "___dataclass_fields": dataclass_fields, + "___namedtuple_fields": lambda x: x._fields, + "___get_torch_function_mode_stack_at": get_torch_function_mode_stack_at, + "___get_current_stream": get_current_stream, + "__math_isnan": math.isnan, + "__numpy_isnan": None if np is None else np.isnan, + "inf": float("inf"), + "__load_module": importlib.import_module, + "utils_device": torch.utils._device, + "device": torch.device, + "___from_numpy": from_numpy, + "___as_tensor": torch._as_tensor_fullprec, + "torch": torch, + "inspect": inspect, + } + return _CLOSURE_VARS + + +def _ast_unparse(node: ast.AST) -> str: + return ast.unparse(node).replace("\n", "") + + +strip_function_call = torch._C._dynamo.strip_function_call + + +def get_verbose_code_part(code_part: str, guard: Guard | None) -> str: + extra = "" + if guard is not None: + if guard.user_stack: + for fs in reversed(guard.user_stack): + if fs.filename not in uninteresting_files(): + extra = f" # {format_frame(fs, line=True)}" + if len(extra) > 1024: + # For fx graphs, the line can be very long in case of + # torch.stack ops, where many inputs are set to None + # after the operation. This increases the size of the + # guards log file. In such cases, do not print the line + # contents. + extra = f" # {format_frame(fs)}" + break + elif guard.stack: + summary = guard.stack.summary() + if len(summary) > 0: + extra = f" # {format_frame(summary[-1])}" + else: + extra = " # " + return f"{code_part:<60}{extra}" + + +def get_verbose_code_parts( + code_parts: str | list[str], + guard: Guard | None, + recompile_hint: str | None = None, +) -> list[str]: + if not isinstance(code_parts, list): + code_parts = [code_parts] + + verbose_code_parts = [ + get_verbose_code_part(code_part, guard) for code_part in code_parts + ] + + # For CellContentsSource (or any source with a CellContentsSource ancestor), + # add a hint explaining which closure variable is being checked. + # This helps users understand which closure variable caused the guard failure. + if guard is not None: + closure_hint = _get_closure_var_hint(guard.originating_source) + if closure_hint: + recompile_hint = ( + f"{closure_hint}, {recompile_hint}" if recompile_hint else closure_hint + ) + + if recompile_hint: + verbose_code_parts = [ + f"{part} (HINT: {recompile_hint})" for part in verbose_code_parts + ] + + return verbose_code_parts + + +def _get_closure_var_hint(source: Source | None) -> str | None: + """ + Walk up the source chain to find a CellContentsSource ancestor. + Returns a hint like 'guard on "varname".attr' or None if not found. + """ + if source is None: + return None + + full_name = source.name + current: Source | None = source + while current is not None: + if isinstance(current, CellContentsSource) and current.freevar_name: + # Compute the path suffix by comparing names + # e.g., full_name="x.__closure__[0].cell_contents.scale" + # current.name="x.__closure__[0].cell_contents" + # suffix=".scale" + path_suffix = full_name[len(current.name) :] + return f'guard on "{current.freevar_name}"{path_suffix}' + current = current.base if isinstance(current, ChainedSource) else None + return None + + +def convert_int_to_concrete_values(dim: Any) -> int | None: + if dim is None: + return None + if not is_symbolic(dim): + return dim + else: + assert isinstance(dim, torch.SymInt) + return dim.node.maybe_as_int() + + +def convert_to_concrete_values(size_or_stride: list[Any]) -> list[int | None]: + return [convert_int_to_concrete_values(dim) for dim in size_or_stride] + + +def get_tensor_guard_code_part( + value: torch.Tensor, + name: str, + sizes: list[int | None], + strides: list[int | None], + pytype: type, + dispatch_keys: DispatchKeySet, +) -> str: + dispatch_key = ( + dispatch_keys | torch._C._dispatch_tls_local_include_set() + ) - torch._C._dispatch_tls_local_exclude_set() + dtype = value.dtype + device_index = value.device.index + requires_grad = value.requires_grad + guard_str = ( + f"check_tensor({name}, {pytype.__qualname__}, {dispatch_key}, {dtype}, " + f"device={device_index}, requires_grad={requires_grad}, size={sizes}, stride={strides})" + ) + return guard_str + + +def get_key_index(dct: dict[Any, Any], key: Any) -> int: + # Ensure that we call dict.keys and not value.keys (which can call + # overridden keys method). In the C++ guards, we relied on PyDict_Next + # to traverse the dictionary, which uses the internal data structure and + # does not call the overridden keys method. + return list(builtin_dict_keys(dct)).index(key) + + +def get_key_index_source(source: Any, index: Any) -> str: + return f"list(dict.keys({source}))[{index}]" + + +def raise_local_type_error(obj: Any) -> NoReturn: + raise TypeError( + f"Type {type(obj)} for object {obj} cannot be saved " + + "into torch.compile() package since it's defined in local scope. " + + "Please define the class at global scope (top level of a module)." + ) + + +def should_optimize_getattr_on_nn_module(value: Any) -> bool: + return isinstance(value, torch.nn.Module) + + +@dataclasses.dataclass(frozen=True) +class NNModuleAttrAccessorInfo: + # Represents where is the attr name is present in the nn module attribute + # access + + # Tells that the attribute can be accessed via __dict__ + present_in_generic_dict: bool = False + + # Either the actual name or _parameters/_buffers/_modules + l1_key: str | None = None + + # Actual parameter/buffer/submodule name + l2_key: str | None = None + + +def getitem_on_dict_manager( + source: DictGetItemSource | DictSubclassGetItemSource, + base_guard_manager: DictGuardManager, + base_example_value: Any, + example_value: Any, + guard_manager_enum: GuardManagerType, +) -> GuardManager: + base_source_name = source.base.name + if isinstance(source.index, ConstDictKeySource): + index = source.index.index + else: + assert isinstance(base_example_value, dict) + index = get_key_index(base_example_value, source.index) + + key_source = get_key_index_source(base_source_name, index) + + # Ensure that we call dict.keys and not value.keys (which can call + # overridden keys method). In the C++ guards, we relied on PyDict_Next + # to traverse the dictionary, which uses the internal data structure and + # does not call the overridden keys method. + key_example_value = list(builtin_dict_keys(base_example_value))[index] + if isinstance(key_example_value, (int, str)): + value_source = f"{base_source_name}[{key_example_value!r}]" + else: + value_source = f"{base_source_name}[{key_source}]" + if not isinstance(source.index, ConstDictKeySource): + # We have to insert a key manager guard here + # TODO - source debug string is probably wrong here. + base_guard_manager.get_key_manager( + index=index, + source=key_source, + example_value=source.index, + guard_manager_enum=GuardManagerType.GUARD_MANAGER, + ).add_equals_match_guard( + source.index, [f"{key_source} == {key_example_value!r}"], None + ) + + return base_guard_manager.get_value_manager( + index=index, + source=value_source, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + + +def match_on_id_for_tensor(guard: Guard) -> bool: + source = guard.originating_source + # For numpy tensors, always use TENSOR_MATCH because __from_numpy leads + # to a new tensor every time and therefore id differs. + if isinstance(source, NumpyTensorSource): + return False + + if guard.is_specialized_nn_module(): + return True + + return source.is_dict_key() and not isinstance(source, GradSource) + + +# The ready to eval generated code (possibly multiple parts) for a guard, plus +# the original guard object that created it for provenance +@dataclasses.dataclass +class GuardCodeList: + code_list: list[str] + guard: Guard + + +class GuardManagerType(enum.Enum): + GUARD_MANAGER = 1 + DICT_GUARD_MANAGER = 2 + + +@functools.cache +def code_framelocals_names_reversed_cached(code: types.CodeType) -> list[str]: + return list(reversed(code_framelocals_names(code))) + + +class GuardCheckSpec(NamedTuple): + """Lightweight guard evaluation for subgraph reuse during tracing. + + WHY IS THIS NEEDED? + Normal guards are collected during tracing, installed via GuardBuilder, and + evaluated in C++ at runtime. GuardCheckSpec serves a different purpose: we + want to evaluate a guard *during tracing* to check whether a previously + traced partial subgraph can be reused. The guard is collected during the + first trace of subgraph and then evaluated with a new source value before we + begin tracing the second subgraph. + + Existing C++ guards don't satisfy this need because they're designed for + runtime evaluation, not mid-trace comparisons with different source values. + + NB - This separate spec table *increases maintenance burden*, but each + handler is small and the number of guard types is finite, so it's tractable. + + get_metadata_fn: (guard, value) -> metadata — captures the minimum + information from the guarded value at build time. + eval_fn: (value, metadata) -> bool — re-evaluates the guard at lookup + time using a fresh value and the previously saved metadata. + """ + + get_metadata_fn: Any + eval_fn: Any + + +SKIP_GUARD = object() + + +# Used by TENSOR_MATCH guard check spec +def extract_tensor_metadata(t: torch.Tensor) -> tuple[Any, ...]: + return (t.shape, t.stride(), t.dtype, t.device, t.requires_grad) + + +# Used by TENSOR_SUBCLASS_METADATA_MATCH guard check spec +def extract_subclass_metadata(guard: Any, value: Any) -> tuple[Any, ...]: + metadata = deepcopy(value.__tensor_flatten__()[1]) + cls = type(value) + has_custom_guard = hasattr(value, "__metadata_guard__") + return (metadata, cls, has_custom_guard) + + +# Used by TENSOR_SUBCLASS_METADATA_MATCH guard check spec +def check_subclass_metadata(value: Any, metadata: tuple[Any, ...]) -> bool: + saved_metadata, cls, has_custom_guard = metadata + if has_custom_guard: + return cls.__metadata_guard__(saved_metadata, value.__tensor_flatten__()[1]) + return value.__tensor_flatten__()[1] == saved_metadata + + +# Used by DTENSOR_SPEC_MATCH guard check spec +def extract_dtensor_spec(guard: Any, value: Any) -> Any: + return deepcopy(value) + + +# Used by DTENSOR_SPEC_MATCH guard check spec +def check_dtensor_spec(value: Any, metadata: Any) -> bool: + return value._check_equals(metadata, skip_shapes=True) + + +# Used by OPAQUE_OBJ_GUARD_FN_MATCH guard check spec +def extract_opaque_obj(guard: Any, value: Any) -> Any: + opaque_info = get_opaque_obj_info(type(value)) + if not opaque_info or not opaque_info.guard_fn: + return None + return deepcopy(opaque_info.guard_fn(value)) + + +# Used by OPAQUE_OBJ_GUARD_FN_MATCH guard check spec +def check_opaque_obj(value: Any, metadata: Any) -> bool: + if metadata is None: + return True + opaque_info = get_opaque_obj_info(type(value)) + if not opaque_info or not opaque_info.guard_fn: + return metadata is None + return opaque_info.guard_fn(value) == metadata + + +# Used by CLOSURE_MATCH guard check spec +def extract_closure(guard: Any, value: Any) -> Any: + if type(value) is types.FunctionType and hasattr(value, "__code__"): + return value.__code__ + return id(value) + + +# Used by CLOSURE_MATCH guard check spec +def check_closure(value: Any, metadata: Any) -> bool: + if type(value) is types.FunctionType and hasattr(value, "__code__"): + return value.__code__ is metadata + return id(value) == metadata + + +def _constant_subclass_base_value(value: Any) -> Any: + """Extract the base constant value from a constant subclass instance.""" + from .variables.user_defined import _CONSTANT_BASE_TYPES + + for t in _CONSTANT_BASE_TYPES: + if isinstance(value, t): + return t(value) # pyrefly: ignore[bad-argument-type] + raise TypeError(f"Not a constant subclass: {type(value)}") + + +def register_guard_check_spec( + get_metadata_fn, + eval_fn, +): + """Attach a GuardCheckSpec to a guard method for auto-dispatch.""" + handler = GuardCheckSpec(get_metadata_fn=get_metadata_fn, eval_fn=eval_fn) + + def decorator(fn): + fn.guard_check_spec = handler + return fn + + return decorator + + +def skip_guard_check_spec(fn): + """Mark a guard method as skipped during auto-cache dispatch.""" + fn.guard_check_spec = SKIP_GUARD + return fn + + +class UnsupportedGuardCheckSpec: + """Sentinel for guards with no check spec yet. + + Raises NotImplementedError if get_metadata_fn/eval_fn are accidentally called. + """ + + def __init__(self, name: str) -> None: + self._name = name + + @property + def get_metadata_fn(self): + raise NotImplementedError(f"Guard check spec not implemented for {self._name}") + + @property + def eval_fn(self): + raise NotImplementedError(f"Guard check spec not implemented for {self._name}") + + +def unsupported_guard_check_spec(fn): + """Mark a guard method as unsupported for auto-cache dispatch.""" + fn.guard_check_spec = UnsupportedGuardCheckSpec(fn.__name__) + return fn + + +class GuardBuilder(GuardBuilderBase): + def __init__( + self, + f_code: types.CodeType, + id_ref: Callable[[object, str], int], + source_ref: Callable[[Source], str], + lookup_weakrefs: Callable[[object], weakref.ref[object] | None], + local_scope: dict[str, object], + global_scope: dict[str, object], + guard_manager: GuardManagerWrapper, + check_fn_manager: CheckFunctionManager, + save_guards: bool = False, + runtime_global_scope: dict[str, object] | None = None, + guard_filter_fn: Callable[[Sequence[GuardFilterEntry]], Sequence[bool]] + | None = None, + ) -> None: + self.f_code = f_code + self.id_ref = id_ref + self.source_ref = source_ref + self.lookup_weakrefs = lookup_weakrefs + self.scope: dict[str, dict[str, object]] = {"L": local_scope, "G": global_scope} + self.src_get_value_cache: dict[Source, object] = {} + self.runtime_global_scope = runtime_global_scope or global_scope + self.scope["__builtins__"] = builtins.__dict__.copy() + for ( + name, + package_module, + ) in torch.package.package_importer._package_imported_modules.items(): + name = name.replace(">", "_").replace("<", "_").replace(".", "_dot_") + # Write the package module into the scope so that we can import it + self.scope["__builtins__"][name] = package_module + # Write the demangled name to the scope so that we can use it + self.scope[name] = package_module + self.guard_manager = guard_manager + + self.argnames: list[str] = [] + # Code is python expression strings generated for each guard + self.code: list[GuardCodeList] = [] + # shape_env_code is only used by builder and is used for + # shape env code. This exists only because we need to make sure + # shape env guards get run after tensor match guards (since the + # tensor match guards make sure we actually have tensors) + self.shape_env_code: list[GuardCodeList] = [] + + # Collect the guard managers and debug info to insert no tensor aliasing + # guards. + self.no_tensor_aliasing_names: list[str] = [] + self.no_tensor_aliasing_guard_managers: list[GuardManager] = [] + + self.check_fn_manager: CheckFunctionManager = check_fn_manager + + self.guard_tree_values: dict[int, Any] = {} + self.save_guards = save_guards + self.guard_filter_fn = guard_filter_fn + + # Collect the ids of dicts which need key order guarding. source_name is + # not sufficient because for nn modules, we can have different sources + # to access the same object - self._module["param"] is same as + # self.param. + self.key_order_guarded_dict_ids = set() + assert self.check_fn_manager.output_graph is not None + for source in self.check_fn_manager.output_graph.guard_on_key_order: + dict_obj = self.get(source) + self.key_order_guarded_dict_ids.add(id(dict_obj)) + + # Keep track of weak references of objects with ID_MATCH guard. This + # info is stored alongside optimized_code and guard_manager and is used to + # limit the number of cache entries with same ID_MATCH'd object. + self.id_matched_objs: dict[str, ReferenceType[object]] = {} + + # Save the guard managers to avoid repeatedly traversing sources. + self._cached_guard_managers: dict[str, GuardManager] = {} + self._cached_duplicate_input_guards: set[tuple[str, str]] = set() + self.object_aliasing_guard_codes: list[tuple[str, str]] = [] + self.guard_nn_modules = config.guard_nn_modules and justknobs_check( + "pytorch/compiler:guard_nn_modules" + ) + self.already_added_code_parts: OrderedSet[str] = OrderedSet() + + def guard_on_dict_keys_and_ignore_order( + self, example_value: dict[Any, Any], guard: Guard + ) -> None: + dict_mgr = self.get_guard_manager(guard) + if isinstance(dict_mgr, DictGuardManager): + raise NotImplementedError( + "Not expecting a DictGuardManager. Seems like Dynamo incorrectly " + f"added the dict to tx.output.guard_on_key_order for {guard.name}" + ) + + # Iterate over the dicts and install a dict_getitem_manager. + dict_source = guard.originating_source.name + + # Ensure that we call dict.keys and not value.keys (which can call + # overridden keys method). In the C++ guards, we relied on PyDict_Next + # to traverse the dictionary, which uses the internal data structure and + # does not call the overridden keys method. + for key in builtin_dict_keys(example_value): + value = example_value[key] + value_source = DictGetItemSource(guard.originating_source, index=key) + guard_manager_enum = self.get_guard_manager_type( + value_source, example_value + ) + dict_mgr.dict_getitem_manager( + key=key, + source=f"{dict_source}[{key!r}]", + example_value=value, + guard_manager_enum=guard_manager_enum, + ) + + def guard_on_dict_keys_and_order(self, value: dict[Any, Any], guard: Guard) -> None: + # Add key managers for the DictGuardManager. Then add either an + # ID_MATCH or EQUALS_MATCH guard on the key. + dict_mgr = self.get_guard_manager(guard) + if not isinstance(dict_mgr, DictGuardManager): + raise NotImplementedError( + "Expecting a DictGuardManager. Seems like Dynamo forgot " + f"to set the right guard manager enum for {guard.name}" + ) + assert isinstance(dict_mgr, DictGuardManager) + + # Ensure that we call dict.keys and not value.keys (which can call + # overridden keys method). In the C++ guards, we relied on PyDict_Next + # to traverse the dictionary, which uses the internal data structure and + # does not call the overridden keys method. + for idx, key in enumerate(builtin_dict_keys(value)): + key_source = get_key_index_source(guard.name, idx) + key_manager = dict_mgr.get_key_manager( + index=idx, + source=key_source, + example_value=key, + guard_manager_enum=GuardManagerType.GUARD_MANAGER, + ) + if key_is_id(key): + # Install ID_MATCH guard + id_val = self.id_ref(key, key_source) + key_manager.add_id_match_guard( + id_val, + get_verbose_code_parts( + f"__check_obj_id({key_source}, {id_val})", guard + ), + guard.user_stack, + ) + else: + # Install EQUALS_MATCH guard + key_manager.add_equals_match_guard( + key, + get_verbose_code_parts(f"{key_source} == {key!r}", guard), + guard.user_stack, + ) + + @staticmethod + def _get_generic_dict_manager_example_value(example_value: Any) -> Any | None: + # due to a bug in 3.13.0 (introduced by https://github.com/python/cpython/pull/116115, + # reported in https://github.com/python/cpython/issues/125608, + # fixed by https://github.com/python/cpython/pull/125611), we cannot take + # advantage of __dict__ versions to speed up guard checks. + if ( + config.issue_3_13_0_warning + and sys.version_info >= (3, 13) + and sys.version_info < (3, 13, 1) + ): + warnings.warn( + "Guards may run slower on Python 3.13.0. Consider upgrading to Python 3.13.1+.", + RuntimeWarning, + ) + return None + return example_value + + def getattr_on_nn_module( + self, + source: AttrSource, + base_guard_manager: GuardManager, + base_example_value: Any, + example_value: Any, + base_source_name: str, + source_name: str, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: + """ + This tries to avoid calling the expensive nn module custom getattr method by + checking if the attribute is accessible via __dict__. For attributes that + are not accessible via __dict__ (like descriptors), we fallback to + PyObject_GetAttr. + + There are two cases that we optimize for + 1) attributes present directly in __dict__, e.g training. + 2) parameters/buffers/modules - they can be accessed via _parameters, + _buffers, _modules keys in __dict__. For example, mod.linear can be + accessed as mod.__dict__["_parameters"]["linear"] + + The most common and expensive case for nn module guards is of type + mod.submod1.submod2.submod3.training. We avoid the python getattr of nn + modules by going through the __dict__. + """ + + def getitem_on_dict_mgr( + mgr: GuardManager, + key: Any, + source_name: str, + base_example_value: Any, + example_value: Any, + guard_manager_enum: GuardManagerType, + ) -> GuardManager: + if isinstance(mgr, DictGuardManager): + # Case where the user code relies on key order, e.g., + # named_parameters + index = get_key_index(base_example_value, key) + + # Install the key manager and add equals match guard + key_source = f"list(dict.keys({source_name}))[{index!r}]" + mgr.get_key_manager( + index=index, + source=key_source, + example_value=key, + guard_manager_enum=GuardManagerType.GUARD_MANAGER, + ).add_equals_match_guard(key, [f"{key_source} == {key!r}"], None) + + # Install the value manager + return mgr.get_value_manager( + index=index, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + else: + return mgr.dict_getitem_manager( + key=key, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + + attr_name = source.member + mod_dict = base_example_value.__dict__ + + all_class_attribute_names: set[str] = set() + for x in inspect.getmro(base_example_value.__class__): + all_class_attribute_names.update(x.__dict__.keys()) + + accessor_info = NNModuleAttrAccessorInfo(False, None, None) + + if attr_name in mod_dict: + accessor_info = NNModuleAttrAccessorInfo(True, attr_name, None) + elif "_parameters" in mod_dict and attr_name in mod_dict["_parameters"]: + accessor_info = NNModuleAttrAccessorInfo(True, "_parameters", attr_name) + elif "_buffers" in mod_dict and attr_name in mod_dict["_buffers"]: + accessor_info = NNModuleAttrAccessorInfo(True, "_buffers", attr_name) + elif ( + attr_name not in all_class_attribute_names + and "_modules" in mod_dict + and attr_name in mod_dict["_modules"] + ): + # Check test_attr_precedence test - instance attributes always take precedence unless its an nn.Module. + accessor_info = NNModuleAttrAccessorInfo(True, "_modules", attr_name) + + if not accessor_info.present_in_generic_dict: + # The attribute can be accessed by __getattribute__ call, so rely on + # PyObject_GetAttr + return base_guard_manager.getattr_manager( + attr=source.member, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + else: + assert accessor_info.l1_key + l1_key = accessor_info.l1_key + l2_key = accessor_info.l2_key + + # Set source strings for debug info + mod_dict_source = f"{base_source_name}.__dict__" + l1_source_name = l2_source_name = None + l1_value = l2_value = None + l1_guard_manager_enum = l2_guard_manager_enum = None + if l2_key: + l1_source = AttrSource(source.base, l1_key) + l1_source_name = l1_source.name + l1_value = mod_dict[l1_key] + # do not guard on key order for _parameters etc unless the user code + # actually needs the key order (e.g. calling named_parameters) + l1_guard_manager_enum = self.get_guard_manager_type(l1_source, l1_value) + + l2_source_name = source_name + l2_value = example_value + l2_guard_manager_enum = self.get_guard_manager_type( + source, example_value + ) + else: + l1_source_name = source_name + l1_value = example_value + l1_guard_manager_enum = self.get_guard_manager_type( + source, example_value + ) + + # Get __dict__ accessor. No need to guard on dict key order, so use base + # Guard Manager + mod_generic_dict_manager = base_guard_manager.get_generic_dict_manager( + source=mod_dict_source, + example_value=self._get_generic_dict_manager_example_value(mod_dict), + guard_manager_enum=GuardManagerType.GUARD_MANAGER, + ) + + l1_mgr = getitem_on_dict_mgr( + mgr=mod_generic_dict_manager, + key=l1_key, + source_name=l1_source_name, + base_example_value=mod_dict, + example_value=l1_value, + guard_manager_enum=l1_guard_manager_enum, + ) + + if l2_key: + assert l2_source_name is not None and l2_guard_manager_enum is not None + return getitem_on_dict_mgr( + mgr=l1_mgr, + key=l2_key, + source_name=l2_source_name, + base_example_value=l1_value, + example_value=l2_value, + guard_manager_enum=l2_guard_manager_enum, + ) + return l1_mgr + + def requires_key_order_guarding(self, source: Source) -> bool: + source_name = source.name + if source_name == "": + return False + obj_id = id(self.get(source)) + return obj_id in self.key_order_guarded_dict_ids + + def get_guard_manager_type( + self, + source: Source, + example_value: KeysView[Any] + | set[Any] + | frozenset[Any] + | dict[Any, Any] + | None, + ) -> GuardManagerType: + guard_manager_enum = GuardManagerType.GUARD_MANAGER + if self.requires_key_order_guarding(source): + # Fix this if condition + if isinstance(example_value, dict_keys): + guard_manager_enum = GuardManagerType.DICT_GUARD_MANAGER + elif isinstance(example_value, (set, frozenset)): + # we don't need to guard on key order for set/frozenset + # but the if above will be true for these types as set is + # implemented using a dict in Dynamo + guard_manager_enum = GuardManagerType.GUARD_MANAGER + else: + assert isinstance(example_value, dict) + guard_manager_enum = GuardManagerType.DICT_GUARD_MANAGER + return guard_manager_enum + + def manager_guards_on_keys(self, mgr_enum: GuardManagerType) -> bool: + return mgr_enum == GuardManagerType.DICT_GUARD_MANAGER + + def get_global_guard_manager(self) -> GuardManager: + return self.guard_manager.root.globals_dict_manager( + f_globals=self.runtime_global_scope, + source="G", + example_value=self.scope["G"], + guard_manager_enum=GuardManagerType.GUARD_MANAGER, + ) + + def get_guard_manager_from_source(self, source: Source) -> GuardManager: + root_guard_manager = self.guard_manager.root + + example_value = None + source_name = source.name + + if source_name != "" and source_name in self._cached_guard_managers: + return self._cached_guard_managers[source_name] + + if source_name != "": + example_value = self.get(source) + self.guard_tree_values[id(example_value)] = example_value + + guard_manager_enum = self.get_guard_manager_type(source, example_value) + + # Get base manager related information + base_source_name = None + base_example_value = None + base_guard_manager = None + base_guard_manager_enum = GuardManagerType.GUARD_MANAGER + if isinstance(source, ChainedSource): + base_source_name = source.base.name + base_example_value = self.get(source.base) + base_guard_manager = self.get_guard_manager_from_source(source.base) + base_guard_manager_enum = self.get_guard_manager_type( + source.base, base_example_value + ) + + # Use istype instead of isinstance to check for exact type of source. + if istype(source, LocalSource): + framelocals_idx = get_framelocals_idx(self.f_code, source.local_name) + out = root_guard_manager.framelocals_manager( + key=(source.local_name, framelocals_idx), + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, GlobalSource): + # Global manager accepts a dict but it is not a DictGuardManager + # because globals dict is big and we typically guard on a very + # selected items on globals. + out = self.get_global_guard_manager().dict_getitem_manager( + key=source.global_name, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, GlobalWeakRefSource): + out = self.get_global_guard_manager().global_weakref_manager( + global_name=source.global_name, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, GlobalStateSource): + # Don't do anything here. We guard on global state completely in + # C++. So just return the root mgr. + return root_guard_manager + elif istype(source, ShapeEnvSource): + return root_guard_manager + elif istype(source, TypeSource): + assert base_guard_manager # to make mypy happy + out = base_guard_manager.type_manager( + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, TypeDictSource): + assert base_guard_manager # to make mypy happy + out = base_guard_manager.type_dict_manager( + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, TypeMROSource): + assert base_guard_manager # to make mypy happy + out = base_guard_manager.type_mro_manager( + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype( + source, + ( + OptimizerSource, + NNModuleSource, + UnspecializedNNModuleSource, + UnspecializedBuiltinNNModuleSource, + FSDPNNModuleSource, + ), + ): + assert base_guard_manager # to make mypy happy + out = base_guard_manager + elif istype(source, ImportSource): + module = importlib.import_module(source.module_name) + out = root_guard_manager.lambda_manager( + python_lambda=lambda _, m=module: m, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, TorchFunctionModeStackSource): + out = root_guard_manager.lambda_manager( + python_lambda=lambda _: get_torch_function_mode_stack_at( + source._get_index() + ), + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, CurrentStreamSource): + out = root_guard_manager.lambda_manager( + python_lambda=lambda _: get_current_stream(source.device), + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, GradSource): + assert base_guard_manager # to make mypy happy + out = base_guard_manager.grad_manager( + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, GenericAttrSource): + assert base_guard_manager # to make mypy happy + out = base_guard_manager.generic_getattr_manager( + attr=source.member, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype( + source, (AttrSource, CellContentsSource, UnspecializedParamBufferSource) + ): + assert base_guard_manager # to make mypy happy + assert isinstance(source, AttrSource) + if should_optimize_getattr_on_nn_module(base_example_value): + assert base_source_name + out = self.getattr_on_nn_module( + source, + base_guard_manager, + base_example_value, + example_value, + base_source_name, + source_name, + guard_manager_enum, + ) + else: + out = base_guard_manager.getattr_manager( + attr=source.member, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, (DictGetItemSource, DictSubclassGetItemSource)): + assert base_guard_manager # to make mypy happy + assert isinstance(base_example_value, (dict, collections.OrderedDict)) + assert isinstance(source, (DictGetItemSource, DictSubclassGetItemSource)) + if isinstance(base_guard_manager, DictGuardManager): + assert self.manager_guards_on_keys(base_guard_manager_enum) + out = getitem_on_dict_manager( + source, + base_guard_manager, + base_example_value, + example_value, + guard_manager_enum, + ) + else: + if isinstance(source.index, ConstDictKeySource): + raise RuntimeError( + "Expecting clean index here. Likely Dynamo forgot to mark" + " a dict as guard_on_key_order" + ) + out = base_guard_manager.dict_getitem_manager( + key=source.index, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, TensorPropertySource): + out = getattr( + base_guard_manager, + f"tensor_property_{source.prop.name.lower()}_manager", + )( + idx=source.idx, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, IndexedSource): + assert base_guard_manager # to make mypy happy + + out = base_guard_manager.indexed_manager( + idx=source.idx, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, ListGetItemSource): + assert base_guard_manager # to make mypy happy + out = base_guard_manager.list_getitem_manager( + key=source.index, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, GetItemSource): + assert base_guard_manager # to make mypy happy + assert not isinstance( + base_example_value, (dict, collections.OrderedDict) + ), "Use DictGetItemSource" + if isinstance(base_example_value, list) and not source.index_is_slice: + out = base_guard_manager.list_getitem_manager( + key=source.index, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif isinstance(base_example_value, tuple) and not source.index_is_slice: + out = base_guard_manager.tuple_getitem_manager( + key=source.index, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + else: + index = source.index + if source.index_is_slice: + index = source.unpack_slice() + out = base_guard_manager.getitem_manager( + key=index, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, DefaultsSource): + assert base_guard_manager # to make mypy happy + assert base_source_name + assert callable(base_example_value) + if not source.is_kw: + out = base_guard_manager.func_defaults_manager( + source=base_source_name, + example_value=base_example_value.__defaults__, + guard_manager_enum=GuardManagerType.GUARD_MANAGER, + ).getitem_manager( + key=source.idx_key, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + else: + # kwdefauts is a dict, so use a DictGuardManager + kwdefaults = base_example_value.__kwdefaults__ + assert base_source_name is not None + kw_source = base_source_name + ".__kwdefaults__" + + # kwdefaults is a dict. No need to guard on dict order. + dict_mgr = base_guard_manager.func_kwdefaults_manager( + source=kw_source, + example_value=kwdefaults, + guard_manager_enum=GuardManagerType.GUARD_MANAGER, + ) + assert not isinstance(dict_mgr, DictGuardManager) + + out = dict_mgr.dict_getitem_manager( + key=source.idx_key, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, NumpyTensorSource): + assert base_guard_manager # to make mypy happy + out = base_guard_manager.lambda_manager( + python_lambda=from_numpy, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, SubclassAttrListSource): + assert base_guard_manager # to make mypy happy + out = base_guard_manager.lambda_manager( + python_lambda=lambda x: x.__tensor_flatten__()[0], + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, FlattenScriptObjectSource): + assert base_guard_manager # to make mypy happy + out = base_guard_manager.lambda_manager( + python_lambda=lambda x: x.__obj_flatten__(), + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, ScriptObjectQualifiedNameSource): + assert base_guard_manager # to make mypy happy + out = base_guard_manager.lambda_manager( + python_lambda=lambda x: x._type().qualified_name(), + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, AttrProxySource): + assert base_guard_manager # to make mypy happy + out = base_guard_manager.lambda_manager( + python_lambda=lambda x: x.get_base(), + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, CallMethodItemSource): + assert base_guard_manager # to make mypy happy + out = base_guard_manager.lambda_manager( + python_lambda=lambda x: x.item(), + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, FloatTensorSource): + assert base_guard_manager # to make mypy happy + out = base_guard_manager.lambda_manager( + python_lambda=lambda x: torch._as_tensor_fullprec(x), + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, TupleIteratorGetItemSource): + assert base_guard_manager # to make mypy happy + out = base_guard_manager.tuple_iterator_getitem_manager( + index=source.index, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif isinstance(source, ConstDictKeySource): + if not isinstance(base_guard_manager, DictGuardManager): + raise AssertionError( + "ConstDictKeySource can only work on DictGuardManager" + ) + out = base_guard_manager.get_key_manager( + index=source.index, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, NonSerializableSetGetItemSource): + assert base_guard_manager + out = base_guard_manager.set_getitem_manager( + index=source.index, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, WeakRefCallSource): + assert base_guard_manager # to make mypy happy + out = base_guard_manager.weakref_call_manager( + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, CallFunctionNoArgsSource): + assert base_guard_manager # to make mypy happy + out = base_guard_manager.call_function_no_args_manager( + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, DataclassFieldsSource): + assert base_guard_manager + out = base_guard_manager.lambda_manager( + python_lambda=lambda x: dataclass_fields(x), + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, NamedTupleFieldsSource): + assert base_guard_manager + out = base_guard_manager.lambda_manager( + python_lambda=lambda x: x._fields, + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, CodeSource): + assert base_guard_manager # to make mypy happy + out = base_guard_manager.code_manager( + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, ClosureSource): + assert base_guard_manager # to make mypy happy + out = base_guard_manager.closure_manager( + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + elif istype(source, DynamicScalarSource): + assert base_guard_manager + out = base_guard_manager.lambda_manager( + python_lambda=lambda x: int(x), + source=source_name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + else: + raise AssertionError( + f"missing guard manager builder {source} - {source.name}" + ) + + self._cached_guard_managers[source.name] = out + return out + + def get_guard_manager(self, guard: Guard) -> GuardManager: + return self.get_guard_manager_from_source(guard.originating_source) + + def add_python_lambda_leaf_guard_to_root( + self, + code_parts: list[str], + verbose_code_parts: list[str], + closure_vars: dict[str, object] | None = None, + is_epilogue: bool = True, + ) -> None: + if closure_vars is None: + closure_vars = _get_closure_vars() + # Adds a lambda leaf guard to the root guard manager. It wraps the + # code_parts in a function object which is then passed on to the leaf + # guard. + make_guard_fn_args = ", ".join(closure_vars.keys()) + _guard_body, pycode = build_guard_function(code_parts, make_guard_fn_args) + out: dict[str, Any] = {} + globals_for_guard_fn = {"G": self.scope["G"]} + guards_log.debug("Python shape guard function:\n%s", pycode) + exec(pycode, globals_for_guard_fn, out) + guard_fn = out["___make_guard_fn"](*closure_vars.values()) + if is_epilogue: + # Epilogue guards are run after all the other guards have finished. + # If epilogue guards contain a getattr or getitem access, one of the + # other guards would fail preventing the epilogue guards to run. + self.guard_manager.root.add_epilogue_lambda_guard( + guard_fn, + verbose_code_parts, + None, + ) + else: + self.guard_manager.root.add_lambda_guard(guard_fn, verbose_code_parts, None) + + # Warning: use this with care! This lets you access what the current + # value of the value you are guarding on is. You probably don't want + # to actually durably save this value though (because it's specific + # to this frame!) Instead, you should be reading out some property + # (like its type) which is what you permanently install into the + # guard code. + def get( + self, + guard_or_source: Guard | Source, + closure_vars: dict[str, Any] | None = None, + ) -> Any: + if isinstance(guard_or_source, Source): + src = guard_or_source + else: + src = guard_or_source.originating_source + if closure_vars is None: + closure_vars = _get_closure_vars() + ret = src.get_value(self.scope, closure_vars, self.src_get_value_cache) + return ret + + # Registers the usage of the source name referenced by the + # string (or stored in the Guard) as being guarded upon. It's important + # to call this before generating some code that makes use of 'guard', + # because without this call, we won't actually bind the variable + # you reference in the actual guard closure (oops!) + def arg_ref(self, guard: str | Guard) -> str: + name: str + if isinstance(guard, str): + name = guard + else: + name = guard.name + base = strip_function_call(name) + if base not in self.argnames: + is_valid = torch._C._dynamo.is_valid_var_name(base) + if is_valid: + if is_valid == 2: + log.warning("invalid var name: %s", guard) + self.argnames.append(base) + + return name + + def _guard_on_attribute( + self, + guard: Guard, + attr_name: str, + guard_fn: Callable[[GuardBuilderBase, Guard], Any], + ) -> None: + if attr_name == "__code__": + attr_source = CodeSource(guard.originating_source) + else: + attr_source = AttrSource(guard.originating_source, attr_name) # type: ignore[assignment] + # Copy the stack info + new_guard = Guard( + attr_source, guard_fn, stack=guard.stack, user_stack=guard.user_stack + ) + new_guard.create(self) + + # Note: the order of the guards in this file matters since we sort guards on the same object by lineno + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: ( + guard.create_fn.keywords["attr"], + hasattr(value, guard.create_fn.keywords["attr"]), + ), + eval_fn=lambda value, metadata: hasattr(value, metadata[0]) == metadata[1], + ) + def HASATTR(self, guard: Guard, attr: str | None = None) -> None: + assert attr is not None + source = guard.originating_source + if isinstance(source, NNModuleSource): + source = source.base + + ref = self.arg_ref(guard) + val = hasattr(self.get(guard), attr) + code = None + if val: + code = f"hasattr({ref}, {attr!r})" + else: + code = f"not hasattr({ref}, {attr!r})" + + if code in self.already_added_code_parts: + return + + self._set_guard_export_info( + guard, [code], provided_guarded_object=self.get(guard) + ) + + base_manager = self.get_guard_manager_from_source(source) + if val: + # Just install a getattr manager. GetAttrGuardAccessor itself + # acts as hasattr guard. + attr_source = AttrSource(source, attr) + example_value = self.get(attr_source) + base_example_value = self.get(guard) + guard_manager_enum = self.get_guard_manager_type(attr_source, example_value) + + # if the base value is nn.Module, check if we can speedup the + # guard by going through __dict__ attrs. + if should_optimize_getattr_on_nn_module(base_example_value): + self.getattr_on_nn_module( + attr_source, + base_manager, + base_example_value, + example_value, + source.name, + attr_source.name, + guard_manager_enum, + ) + else: + base_manager.getattr_manager( + attr=attr, + source=guard.name, + example_value=example_value, + guard_manager_enum=guard_manager_enum, + ) + else: + base_manager.add_no_hasattr_guard( + attr, get_verbose_code_parts(code, guard), guard.user_stack + ) + self.already_added_code_parts.add(code) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: guard.create_fn.keywords["attr"], + eval_fn=lambda value, metadata: metadata not in value.__dict__, + ) + def NOT_PRESENT_IN_GENERIC_DICT( + self, guard: Guard, attr: Any | None = None + ) -> None: + assert attr is not None + ref = self.arg_ref(guard) + val = self.get(guard) + + base_manager = self.get_guard_manager(guard) + + code = f"not ___dict_contains({attr!r}, {ref}.__dict__)" + if code in self.already_added_code_parts: + return + + mod_dict_source = f"{guard.name}.__dict__" + mod_generic_dict_manager = base_manager.get_generic_dict_manager( + source=mod_dict_source, + example_value=self._get_generic_dict_manager_example_value(val.__dict__), + guard_manager_enum=GuardManagerType.GUARD_MANAGER, + ) + + mod_generic_dict_manager.add_dict_contains_guard( + False, + attr, + get_verbose_code_parts(code, guard), + guard.user_stack, + ) + self.already_added_code_parts.add(code) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: type(value), + eval_fn=lambda value, metadata: type(value) is metadata, + ) + def TYPE_MATCH(self, guard: Guard) -> None: + # ___check_type_id is same as `id(type(x)) == y` + value = self.get(guard) + if isinstance(value, torch._subclasses.FakeTensor) and value.pytype: + t = value.pytype + else: + t = type(value) + + if t.__qualname__ != t.__name__: + # Type match guards must be local scope, this is + # raised in self.serialize_guards + guard._unserializable = True + + obj_id = self.id_ref(t, f"type({guard.name})") + type_repr = repr(t) + code = f"___check_type_id({self.arg_ref(guard)}, {obj_id}), type={type_repr}" + self._set_guard_export_info(guard, [code]) + + self.get_guard_manager(guard).add_type_match_guard( + obj_id, + get_verbose_code_parts( + code, guard, recompile_hint=f"type {t.__qualname__}" + ), + guard.user_stack, + ) + + # Dict versions change on any mutation, so a version captured during one + # trace is meaningless for a later subgraph reuse check. + @unsupported_guard_check_spec + def DICT_VERSION(self, guard: Guard) -> None: + # ___check_dict_version is same as `dict_version(x) == y` + ref = self.arg_ref(guard) + val = self.get(guard) + version = dict_version(self.get(guard)) + code = f"___dict_version({ref}) == {version}" + self._set_guard_export_info(guard, [code]) + + # TODO(anijain2305) - Delete this when DictGuardManager uses tags + # for dicts. + self.get_guard_manager(guard).add_dict_version_guard( + val, get_verbose_code_parts(code, guard), guard.user_stack + ) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: guard.create_fn.keywords["key"], + eval_fn=lambda value, metadata: metadata in value, + ) + def DICT_CONTAINS(self, guard: Guard, key: str) -> None: + dict_ref = self.arg_ref(guard) + + code = f"___dict_contains({key!r}, {dict_ref})" + if code in self.already_added_code_parts: + return + self._set_guard_export_info(guard, [code]) + + self.get_guard_manager(guard).add_dict_contains_guard( + True, + key, + get_verbose_code_parts(code, guard), + guard.user_stack, + ) + self.already_added_code_parts.add(code) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: guard.create_fn.keywords["key"], + eval_fn=lambda value, metadata: metadata not in value, + ) + def DICT_NOT_CONTAINS(self, guard: Guard, key: str) -> None: + dict_ref = self.arg_ref(guard) + + code = f"not ___dict_contains({key!r}, {dict_ref})" + if code in self.already_added_code_parts: + return + self._set_guard_export_info(guard, [code]) + + self.get_guard_manager(guard).add_dict_contains_guard( + False, + key, + get_verbose_code_parts(code, guard), + guard.user_stack, + ) + self.already_added_code_parts.add(code) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: guard.create_fn.keywords["key"], + eval_fn=lambda value, metadata: metadata in value, + ) + def SET_CONTAINS(self, guard: Guard, key: Any) -> None: + set_ref = self.arg_ref(guard) + item = key + + code = f"set.__contains__({set_ref}, {item!r})" + if code in self.already_added_code_parts: + return + + self._set_guard_export_info(guard, [code]) + + self.get_guard_manager(guard).add_set_contains_guard( + True, + item, + get_verbose_code_parts(code, guard), + guard.user_stack, + ) + self.already_added_code_parts.add(code) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: guard.create_fn.keywords["key"], + eval_fn=lambda value, metadata: metadata not in value, + ) + def SET_NOT_CONTAINS(self, guard: Guard, key: Any) -> None: + set_ref = self.arg_ref(guard) + item = key + + code = f"not set.__contains__({set_ref}, {item!r})" + if code in self.already_added_code_parts: + return + + self._set_guard_export_info(guard, [code]) + + self.get_guard_manager(guard).add_set_contains_guard( + False, + item, + get_verbose_code_parts(code, guard), + guard.user_stack, + ) + self.already_added_code_parts.add(code) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: value, + eval_fn=lambda value, metadata: value == metadata, + ) + def BOOL_MATCH(self, guard: Guard) -> None: + # checks val == True or val == False + ref = self.arg_ref(guard) + val = self.get(guard) + assert istype(val, bool) + code = [f"{ref} == {val!r}"] + self._set_guard_export_info(guard, code) + + if val: + self.get_guard_manager(guard).add_true_match_guard( + get_verbose_code_parts(code, guard), guard.user_stack + ) + else: + self.get_guard_manager(guard).add_false_match_guard( + get_verbose_code_parts(code, guard), guard.user_stack + ) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: None, + eval_fn=lambda value, metadata: value is None, + ) + def NONE_MATCH(self, guard: Guard) -> None: + # checks `val is None` + ref = self.arg_ref(guard) + val = self.get(guard) + assert val is None + code = [f"{ref} is None"] + self._set_guard_export_info(guard, code) + + self.get_guard_manager(guard).add_none_match_guard( + get_verbose_code_parts(code, guard), guard.user_stack + ) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: value, + eval_fn=lambda value, metadata: value is metadata, + ) + def ID_MATCH(self, guard: Guard, recompile_hint: str | None = None) -> None: + # TODO - Run a CI with the following uncommented to find the remaining places + # val = self.get(guard) + # if inspect.isclass(val): + # raise AssertionError(f"{guard.name} is a class, use CLASS_MATCH guard") + # if inspect.ismodule(val): + # raise AssertionError(f"{guard.name} is a module, use MODULE_MATCH guard") + return self.id_match_unchecked(guard, recompile_hint) + + def id_match_unchecked( + self, guard: Guard, recompile_hint: str | None = None + ) -> None: + # ___check_obj_id is same as `id(x) == y` + if isinstance(guard.originating_source, TypeSource): + # optional optimization to produce cleaner/faster guard code + return self.TYPE_MATCH( + Guard(guard.originating_source.base, GuardBuilder.TYPE_MATCH) # type: ignore[arg-type] + ) + + ref = self.arg_ref(guard) + val = self.get(guard) + id_val = self.id_ref(val, guard.name) + try: + type_repr = repr(val) + except Exception: + # During deepcopy reconstruction or other state transitions, + # objects may be in an incomplete state where repr() fails + type_repr = f"<{type(val).__name__}>" + code = f"___check_obj_id({ref}, {id_val}), type={type_repr}" + self._set_guard_export_info(guard, [code], provided_func_name="ID_MATCH") + self.get_guard_manager(guard).add_id_match_guard( + id_val, + get_verbose_code_parts(code, guard, recompile_hint), + guard.user_stack, + ) + + # Keep track of ID_MATCH'd objects. This will be used to modify the + # cache size logic + if isinstance(guard.originating_source, LocalSource): + # TODO(anijain2305) - This is currently restricted to nn.Module objects + # because many other ID_MATCH'd objects fail - like DeviceMesh. + # Increase the scope of ID_MATCH'd objects. + if isinstance(val, torch.nn.Module): + local_name = guard.originating_source.local_name + weak_id = self.lookup_weakrefs(val) + if weak_id is not None: + self.id_matched_objs[local_name] = weak_id + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: None, + eval_fn=lambda value, metadata: value is not None, + ) + def NOT_NONE_MATCH(self, guard: Guard, value: Any | None = None) -> None: + ref = self.arg_ref(guard) + val = self.get(guard) + assert isinstance(val, torch.Tensor) + code = f"{ref} is not None" + self._set_guard_export_info(guard, [code]) + + self.get_guard_manager(guard).add_not_none_guard( + get_verbose_code_parts(code, guard), guard.user_stack + ) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: value.raw_repr(), + eval_fn=lambda value, metadata: value.raw_repr() == metadata, + ) + def DISPATCH_KEY_SET_MATCH(self, guard: Guard) -> None: + ref = self.arg_ref(guard) + val = self.get(guard) + assert isinstance(val, torch._C.DispatchKeySet) + code_parts = f"{ref}.raw_repr() == {val!r}.raw_repr()" + + self.get_guard_manager(guard).add_dispatch_key_set_guard( + val, + get_verbose_code_parts(code_parts, guard), + guard.user_stack, + ) + + # Global state guard — not source-specific, checked separately at runtime. + @skip_guard_check_spec + def DUAL_LEVEL(self, guard: Guard) -> None: + # Invalidate dual level if current dual level is different than the one + # in the fx graph + assert self.check_fn_manager.output_graph is not None + dual_level = self.check_fn_manager.output_graph.dual_level + code = [f"torch.autograd.forward_ad._current_level == {dual_level}"] + self._set_guard_export_info(guard, code) + self.guard_manager.root.add_dual_level_match_guard( + dual_level, + get_verbose_code_parts(code, guard), + guard.user_stack, + ) + + # Global state guard — not source-specific, checked separately at runtime. + @skip_guard_check_spec + def FUNCTORCH_STACK_MATCH(self, guard: Guard) -> None: + # Invalidate functorch code if current level is different than + # the one when FX graph was generated + assert self.check_fn_manager.output_graph is not None + cis = self.check_fn_manager.output_graph.functorch_layers + states = [ci.get_state() for ci in cis] + code = [f"torch._functorch.pyfunctorch.compare_functorch_state({states})"] + self._set_guard_export_info(guard, code) + + # TODO(anijain2305) - Consider this moving this guard to C++ + compare_fn = torch._functorch.pyfunctorch.compare_functorch_state + + def fn(x: Any) -> bool: + return compare_fn(states) + + self.guard_manager.root.add_lambda_guard( + fn, get_verbose_code_parts(code, guard), guard.user_stack + ) + + # Global state guard — not source-specific, checked separately at runtime. + @skip_guard_check_spec + def AUTOGRAD_SAVED_TENSORS_HOOKS(self, guard: Guard) -> None: + get_hooks = torch._functorch._aot_autograd.utils.top_saved_tensors_hooks + are_inline_hooks = ( + torch._functorch._aot_autograd.utils.saved_tensors_hooks_are_inlineable + ) + + def hooks_ids_fn( + hooks: tuple[Callable[[torch.Tensor], Any], Callable[[Any], torch.Tensor]], + ) -> tuple[int, ...] | None: + if not are_inline_hooks(hooks): + return None + + return tuple(map(id, hooks)) + + guard_hooks_ids = hooks_ids_fn(get_hooks()) + + code = [ + f"torch._functorch.aot_autograd.utils.top_saved_tensors_hooks ids == {guard_hooks_ids}" + ] + self._set_guard_export_info(guard, code) + + def fn(x: Any) -> bool: + return guard_hooks_ids == hooks_ids_fn(get_hooks()) + + self.guard_manager.root.add_lambda_guard( + fn, get_verbose_code_parts(code, guard), guard.user_stack + ) + + @register_guard_check_spec( + get_metadata_fn=extract_subclass_metadata, + eval_fn=check_subclass_metadata, + ) + def TENSOR_SUBCLASS_METADATA_MATCH(self, guard: Guard) -> None: + value = self.get(guard) + + # Deepcopying SymInts result in an error from copying FakeTensors. + # Instead we just always assume the metadata is the same. + class _AnyCompare: + def __eq__(self, other: object) -> bool: + return True + + def __ne__(self, other: object) -> bool: + return False + + metadata = value.__tensor_flatten__()[1] + original_metadata = deepcopy( + pytree.tree_map_only(torch.SymInt, lambda _: _AnyCompare(), metadata) + ) + if hasattr(value, "__metadata_guard__"): + verify_guard_fn_signature(value) + cls = type(value) + + def metadata_checker(x: Any) -> bool: + return cls.__metadata_guard__( + original_metadata, x.__tensor_flatten__()[1] + ) + + else: + + def metadata_checker(x: Any) -> bool: + return x.__tensor_flatten__()[1] == original_metadata + + global_name = f"___check_metadata_{id(metadata_checker)}_c{CompileContext.current_compile_id()}" + self.get_guard_manager(guard).add_lambda_guard( + metadata_checker, + get_verbose_code_parts(global_name, guard), + guard.user_stack, + ) + + @register_guard_check_spec( + get_metadata_fn=extract_dtensor_spec, + eval_fn=check_dtensor_spec, + ) + def DTENSOR_SPEC_MATCH(self, guard: Guard) -> None: + # Copied from DTensor __metadata_guard__ + # TODO - Consider moving this to C++ if stable + expected = extract_dtensor_spec(guard, self.get(guard)) + + def guard_fn(x: Any) -> bool: + return check_dtensor_spec(x, expected) + + code = f"__dtensor_spec_{id(guard_fn)}" + self.get_guard_manager(guard).add_lambda_guard( + guard_fn, get_verbose_code_parts(code, guard), guard.user_stack + ) + + @register_guard_check_spec( + get_metadata_fn=extract_opaque_obj, + eval_fn=check_opaque_obj, + ) + def OPAQUE_OBJ_GUARD_FN_MATCH(self, guard: Guard) -> None: + """Guard on the values returned by the opaque object's guard_fn.""" + + value = self.get(guard) + original_values = extract_opaque_obj(guard, value) + + if original_values is None: + return + + def opaque_guard_checker(x: Any) -> bool: + return check_opaque_obj(x, original_values) + + global_name = f"___check_opaque_guard_fn_{id(opaque_guard_checker)}_c{CompileContext.current_compile_id()}" + self.get_guard_manager(guard).add_lambda_guard( + opaque_guard_checker, + get_verbose_code_parts(global_name, guard), + guard.user_stack, + ) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: value, + eval_fn=lambda value, metadata: value == metadata, + ) + def EQUALS_MATCH(self, guard: Guard, recompile_hint: str | None = None) -> None: + ref = self.arg_ref(guard) + val = self.get(guard) + if np: + np_types: tuple[type[Any], ...] = ( + np.int8, + np.int16, + np.int32, + np.int64, + np.uint8, + np.uint16, + np.uint32, + np.uint64, + np.float16, + np.float32, + np.float64, + ) + else: + np_types = () + + ok_mutable_types = (list, set) + + ok_types = tuple( + common_constant_types + | { + type, + tuple, + frozenset, + slice, + range, + dict_keys, + torch.Size, + torch.Stream, + torch.cuda.streams.Stream, + *np_types, + *ok_mutable_types, + } + ) + + if torch.distributed.is_available(): + from torch.distributed.device_mesh import _MeshLayout, DeviceMesh + from torch.distributed.tensor.placement_types import ( + _StridedShard, + Partial, + Replicate, + Shard, + ) + + ok_types = ok_types + ( + Shard, + Replicate, + Partial, + DeviceMesh, + _StridedShard, + _MeshLayout, + ) + + from torch.export.dynamic_shapes import _IntWrapper + + ok_types = ok_types + (_IntWrapper,) + + import torch.utils._pytree as pytree + + assert ( + isinstance(val, ok_types) + or pytree.is_constant_class(type(val)) + or is_opaque_value_type(type(val)) + ), f"Unexpected type {type(val)}" + + # Special case for nan because float("nan") == float("nan") evaluates to False + if istype(val, float) and math.isnan(val): + code = [f"(type({ref}) is float and __math_isnan({ref}))"] + self._set_guard_export_info(guard, code) + + self.get_guard_manager(guard).add_float_is_nan_guard( + get_verbose_code_parts(code, guard), + guard.user_stack, + ) + return + + # Python math library doesn't support complex nan, so we need to use numpy + # pyrefly: ignore [missing-attribute] + if istype(val, complex) and np.isnan(val): + code = [f"(type({ref}) is complex and __numpy_isnan({ref}))"] + self._set_guard_export_info(guard, code) + + self.get_guard_manager(guard).add_complex_is_nan_guard( + get_verbose_code_parts(code, guard), + guard.user_stack, + ) + return + + # Construct a debug string to put into the c++ equals match guard. + code = [f"{ref} == {val!r}"] + if istype(val, ok_mutable_types): + # C++ guards perform a pointer equality check to speedup guards, but the assumption is that the object + # is immutable. For a few corner cases like sets and lists, we make a deepcopy to purposefully fail the + # pointer equality check. + val = deepcopy(val) + + verbose_code_parts = get_verbose_code_parts(code, guard) + if recompile_hint: + verbose_code_parts = [ + f"{part} (HINT: {recompile_hint})" for part in verbose_code_parts + ] + + self.get_guard_manager(guard).add_equals_match_guard( + val, verbose_code_parts, guard.user_stack + ) + self._set_guard_export_info(guard, code) + return + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: value, + eval_fn=lambda value, metadata: value == metadata, + ) + def CONSTANT_MATCH(self, guard: Guard) -> None: + val = self.get(guard) + if istype(val, bool): + self.BOOL_MATCH(guard) + elif val is None: + self.NONE_MATCH(guard) + elif istype(val, types.CodeType): + self.ID_MATCH(guard) + else: + self.EQUALS_MATCH(guard) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: _constant_subclass_base_value(value), + eval_fn=lambda value, metadata: _constant_subclass_base_value(value) + == metadata, + ) + def CONSTANT_SUBCLASS_MATCH(self, guard: Guard) -> None: + """Guard for subclasses of constant types (int, float, str, etc.). + + Extracts the base value using the base type's converter (e.g., + int.__int__) to avoid calling user-overridden __eq__. + """ + from .variables.user_defined import _CONSTANT_BASE_TYPES + + val = self.get(guard) + ref = self.arg_ref(guard) + + # Find the constant base type + base_type = None + for t in _CONSTANT_BASE_TYPES: + if isinstance(val, t): + base_type = t + break + assert base_type is not None + + base_value = base_type(val) + code = [f"{base_type.__name__}({ref}) == {base_value!r}"] + + def check_fn(x: Any) -> bool: + return base_type(x) == base_value + + self.get_guard_manager(guard).add_lambda_guard( + check_fn, + get_verbose_code_parts(code, guard), + guard.user_stack, + ) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: value, + eval_fn=lambda value, metadata: value is metadata, + ) + def NN_MODULE(self, guard: Guard) -> None: + # don't support this in serialization because it uses unsupported ID_MATCH + self.ID_MATCH(guard, "[inline-inbuilt-nn-modules-candidate]") + val = self.get(guard) + if hasattr(val, "training"): + assert istype(val.training, bool) + if not self.guard_nn_modules: + # If guard_nn_modules is true, we will guard on the right set of guards + self._guard_on_attribute(guard, "training", GuardBuilder.CONSTANT_MATCH) # type: ignore[arg-type] + else: + exc.unimplemented( + gb_type="Attempted to guard on uninitialized nn.Module", + context="", + explanation="Attempted to setup an NN_MODULE guard on uninitialized " + f"nn.Module subclass `{type(val)}`.", + hints=[ + "Ensure the `nn.Module` subclass instance has called `super().__init__()`.", + ], + ) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: value, + eval_fn=lambda value, metadata: value is metadata, + ) + def FUNCTION_MATCH(self, guard: Guard) -> None: + """things like torch.add and user defined functions""" + # don't support this in serialization because it uses unsupported ID_MATCH + return self.ID_MATCH(guard) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: value, + eval_fn=lambda value, metadata: value is metadata, + ) + def CLASS_MATCH(self, guard: Guard) -> None: + """Equals ID_MATCH on classes - better readability than directly calling ID_MATCH""" + val = self.get(guard) + if not inspect.isclass(val): + raise AssertionError( + f"{guard.name} is not a class, but CLASS_MATCH is used" + ) + self.id_match_unchecked(guard) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: value, + eval_fn=lambda value, metadata: value is metadata, + ) + def MODULE_MATCH(self, guard: Guard) -> None: + """Equals ID_MATCH on modules - better readability than directly calling ID_MATCH""" + val = self.get(guard) + if not inspect.ismodule(val): + raise AssertionError( + f"{guard.name} is not a module, but MODULE_MATCH is used" + ) + self.id_match_unchecked(guard) + + @register_guard_check_spec( + get_metadata_fn=extract_closure, + eval_fn=check_closure, + ) + def CLOSURE_MATCH(self, guard: Guard) -> None: + """matches a closure by __code__ id.""" + # don't support this in serialization because it uses unsupported FUNCTION_MATCH + val = self.get(guard) + # Strictly only want user-defined functions + if type(val) is types.FunctionType and hasattr(val, "__code__"): + # No explicit HASATTR guard needed for __code__ — the getattr + # accessor installed by CONSTANT_MATCH implicitly guards hasattr. + self._guard_on_attribute(guard, "__code__", GuardBuilder.CONSTANT_MATCH) # type: ignore[arg-type] + else: + self.FUNCTION_MATCH(guard) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: value, + eval_fn=lambda value, metadata: value is metadata, + ) + def BUILTIN_MATCH(self, guard: Guard) -> None: + if self.save_guards: + # Record which builtin variables are used for pruning later. + if isinstance(guard.originating_source, DictGetItemSource): + self.check_fn_manager.used_builtin_vars.add( + guard.originating_source.index + ) + return self.id_match_unchecked(guard) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: len(value), + eval_fn=lambda value, metadata: len(value) == metadata, + ) + def SEQUENCE_LENGTH(self, guard: Guard) -> None: + # This guard is used to check length of PySequence objects like list, + # tuple, collections.deque etc + ref = self.arg_ref(guard) + value = self.get(guard) + + if not isinstance(value, dict): + # C++ DICT_LENGTH checks for type + self.TYPE_MATCH(guard) + + code = [] + if len(value) == 0: + code.append(f"not {ref}") + else: + code.append(f"len({ref}) == {len(value)}") + + self._set_guard_export_info(guard, code) + if isinstance(value, dict): + self.get_guard_manager(guard).add_dict_length_check_guard( + len(value), + get_verbose_code_parts(code, guard), + guard.user_stack, + ) + else: + self.get_guard_manager(guard).add_length_check_guard( + len(value), + get_verbose_code_parts(code, guard), + guard.user_stack, + ) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: tuple_iterator_len(value), + eval_fn=lambda value, metadata: tuple_iterator_len(value) == metadata, + ) + def TUPLE_ITERATOR_LEN(self, guard: Guard) -> None: + ref = self.arg_ref(guard) + value = self.get(guard) + t = type(value) + + code = [] + code.append(f"___tuple_iterator_len({ref}) == {tuple_iterator_len(value)}") + self._set_guard_export_info(guard, code) + + t = type(value) + obj_id = self.id_ref(t, f"type({guard.name})") + + self.get_guard_manager(guard).add_tuple_iterator_length_guard( + tuple_iterator_len(value), + obj_id, + get_verbose_code_parts(code, guard), + guard.user_stack, + ) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: normalize_range_iter(value), + eval_fn=lambda value, metadata: normalize_range_iter(value) == metadata, + ) + def RANGE_ITERATOR_MATCH(self, guard: Guard) -> None: + ref = self.arg_ref(guard) + value = self.get(guard) + t = type(value) + + code = [] + normalized_range_iter = normalize_range_iter(value) + code.append(f"___normalize_range_iter({ref}) == {normalized_range_iter}") + self._set_guard_export_info(guard, code) + + t = type(value) + obj_id = self.id_ref(t, f"type({guard.name})") + + start, stop, step = normalized_range_iter + self.get_guard_manager(guard).add_range_iterator_match_guard( + start, + stop, + step, + obj_id, + get_verbose_code_parts(code, guard), + guard.user_stack, + ) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: (type(value), normalize_count_iter(value)), + eval_fn=lambda value, metadata: ( + type(value) is metadata[0] and normalize_count_iter(value) == metadata[1] + ), + ) + def COUNT_ITERATOR_MATCH(self, guard: Guard) -> None: + ref = self.arg_ref(guard) + value = self.get(guard) + count_type = type(value) + normalized_count_iter = normalize_count_iter(value) + + def guard_fn(x: Any) -> bool: + return ( + type(x) is count_type + and normalize_count_iter(x) == normalized_count_iter + ) + + code = [f"___normalize_count_iter({ref}) == {normalized_count_iter}"] + self._set_guard_export_info(guard, code) + self.get_guard_manager(guard).add_lambda_guard( + guard_fn, get_verbose_code_parts(code, guard), guard.user_stack + ) + + # Multi-source guard (two inputs aliasing) — not expressible as a + # single source → value check. + # TODO(voz): Deduplicate w/ AOTAutograd dupe input guards + @skip_guard_check_spec + def DUPLICATE_INPUT(self, guard: Guard, source_b: Source) -> None: + if is_from_skip_guard_source( + guard.originating_source + ) or is_from_skip_guard_source(source_b): + return + + if self.save_guards: + if name := get_local_source_name(source_b): + self.check_fn_manager.additional_used_local_vars.add(name) + if name := get_global_source_name(source_b): + self.check_fn_manager.additional_used_global_vars.add(name) + + ref_a = self.arg_ref(guard) + ref_b = self.arg_ref(source_b.name) + + if is_from_optimizer_source( + guard.originating_source + ) or is_from_optimizer_source(source_b): + return + + # Check that the guard has not been inserted already + key = (ref_a, ref_b) + if key in self._cached_duplicate_input_guards: + return + + self._cached_duplicate_input_guards.add((ref_a, ref_b)) + self._cached_duplicate_input_guards.add((ref_b, ref_a)) + + code = [f"{ref_b} is {ref_a}"] + self._set_guard_export_info(guard, code) + + if config.use_lamba_guard_for_object_aliasing: + # Save the code part so that we can install a lambda guard at the + # end. Read the Note - On Lambda guarding of object aliasing - to + # get more information. + code_part = code[0] + verbose_code_part = get_verbose_code_parts(code_part, guard)[0] + self.object_aliasing_guard_codes.append((code_part, verbose_code_part)) + else: + install_object_aliasing_guard( + self.get_guard_manager(guard), + self.get_guard_manager_from_source(source_b), + get_verbose_code_parts(code, guard), + guard.user_stack, + ) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: None, + eval_fn=lambda value, metadata: value is not None, + ) + def WEAKREF_ALIVE(self, guard: Guard) -> None: + code = [f"{self.arg_ref(guard)} is not None"] + + self._set_guard_export_info(guard, code) + self.get_guard_manager(guard).add_not_none_guard( + get_verbose_code_parts(code, guard), guard.user_stack + ) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: list(value.keys()), + eval_fn=lambda value, metadata: list(value.keys()) == metadata, + ) + def MAPPING_KEYS_CHECK(self, guard: Guard) -> None: + """Guard on the key order of types.MappingProxyType object""" + ref = self.arg_ref(guard) + value = self.get(guard) + + code = [] + code.append(f"list({ref}.keys()) == {list(value.keys())}") + self._set_guard_export_info(guard, code) + self.get_guard_manager(guard).add_mapping_keys_guard( + value, code, guard.user_stack + ) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: list(dict.keys(value)), + eval_fn=lambda value, metadata: list(dict.keys(value)) == metadata, + ) + def DICT_KEYS_MATCH(self, guard: Guard) -> None: + """Insert guard to check that the keys of a dict are same""" + ref = self.arg_ref(guard) + value = self.get(guard) + + if value is torch.utils._pytree.SUPPORTED_NODES: + # For SUPPORTED_NODES, we can guard on the dictionary version (PEP509). + self.DICT_VERSION(guard) + return + + self.SEQUENCE_LENGTH(guard) + + code = [] + # Ensure that we call dict.keys and not value.keys (which can call + # overridden keys method). In the C++ guards, we relied on PyDict_Next + # to traverse the dictionary, which uses the internal data structure and + # does not call the overridden keys method. + code.append(f"list(dict.keys({ref})) == {list(builtin_dict_keys(value))!r}") + self._set_guard_export_info(guard, code) + + if self.requires_key_order_guarding(guard.originating_source): + self.guard_on_dict_keys_and_order(value, guard) + else: + self.guard_on_dict_keys_and_ignore_order(value, guard) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: None, + eval_fn=lambda value, metadata: len(value) == 0, + ) + def EMPTY_NN_MODULE_HOOKS_DICT(self, guard: Guard) -> None: + """Special guard to skip guards on empty hooks. This is controlled by skip_nnmodule_hook_guards""" + if config.skip_nnmodule_hook_guards: + # This is unsafe if you add/remove a hook on nn module variable + return + self.SEQUENCE_LENGTH(guard) + + # Global state guard — not source-specific, checked separately at runtime. + @skip_guard_check_spec + def GRAD_MODE(self, guard: Guard) -> None: + pass # we always guard on this via GlobalStateGuard() + + # Global state guard — not source-specific, checked separately at runtime. + @skip_guard_check_spec + def DETERMINISTIC_ALGORITHMS(self, guard: Guard) -> None: + pass # we always guard on this via GlobalStateGuard() + + # Global state guard — not source-specific, checked separately at runtime. + @skip_guard_check_spec + def FSDP_TRAINING_STATE(self, guard: Guard) -> None: + pass # we always guard on this via GlobalStateGuard() + + # Global state guard — not source-specific, checked separately at runtime. + @skip_guard_check_spec + def GLOBAL_STATE(self, guard: Guard) -> None: + output_graph = self.check_fn_manager.output_graph + assert output_graph is not None + global_state = output_graph.global_state_guard + self.check_fn_manager.global_state = global_state + + code = [ + f"___check_global_state() against {self.check_fn_manager.global_state.__getstate__()}" + ] + + self.guard_manager.root.add_global_state_guard( + global_state, code, guard.user_stack + ) + + # Global state guard — not source-specific, checked separately at runtime. + @skip_guard_check_spec + def TORCH_FUNCTION_STATE(self, guard: Guard) -> None: + assert self.check_fn_manager.torch_function_mode_stack is not None + self.check_fn_manager.torch_function_mode_stack_check_fn = ( + make_torch_function_mode_stack_guard( + self.check_fn_manager.torch_function_mode_stack + ) + ) + self.guard_manager.root.add_torch_function_mode_stack_guard( + self.check_fn_manager.torch_function_mode_stack, + ["___check_torch_function_mode_stack()"], + guard.user_stack, + ) + + # Global state guard — not source-specific, checked separately at runtime. + @skip_guard_check_spec + def DEFAULT_DEVICE(self, guard: Guard) -> None: + """Guard on CURRENT_DEVICE per torch.utils._device""" + assert guard.source is GuardSource.GLOBAL + + assert self.check_fn_manager.output_graph is not None + code = [ + f"utils_device.CURRENT_DEVICE == {self.check_fn_manager.output_graph.current_device!r}" + ] + self._set_guard_export_info(guard, code) + + self.get_guard_manager(guard).add_default_device_guard( + get_verbose_code_parts(code, guard), guard.user_stack + ) + + @skip_guard_check_spec + def SHAPE_ENV(self, guard: Guard) -> None: + from torch._dynamo.output_graph import OutputGraphCommon + + assert guard.name == "" + output_graph = self.check_fn_manager.output_graph + assert output_graph is not None + if self.check_fn_manager.shape_code_parts is not None: + shape_code_parts = self.check_fn_manager.shape_code_parts + python_code_parts = shape_code_parts.python_code_parts + verbose_code_parts = shape_code_parts.verbose_code_parts + if shape_code_parts.cpp_code_parts is not None: + cpp_code_parts = shape_code_parts.cpp_code_parts + python_fallback = shape_code_parts.python_fallback + else: + # Let's handle ShapeEnv guards. To do this, we will resolve + # shape variables to sources from tracked_fakes. This must happen after + # tensor checks. + # NB: self.output_graph can be None in the debug_nops tests + assert isinstance(output_graph, OutputGraphCommon) + assert output_graph.shape_env is not None + fs = output_graph.shape_env.tracked_fakes or [] + input_contexts = [a.symbolic_context for a in fs] + + def get_sources(t_id: int, dim: int) -> list[Source]: + # Looks up base sources mapped to a tensor id and uses them to create + # sources for the corresponding tensor dimension. + return [ + TensorPropertySource(source, TensorProperty.SIZE, dim) + # pyrefly: ignore [missing-attribute] + for source in output_graph.tracked_fakes_id_to_source[t_id] + ] + + if output_graph.export_constraints: + names: dict[str, tuple[int, int]] = {} + source_pairs: list[tuple[Source, Source]] = [] + derived_equalities: list[ # type: ignore[type-arg] + # pyrefly: ignore [implicit-any] + tuple[Source, Source | Symbol, Callable] + ] = [] + phantom_symbols: dict[str, Symbol] = {} + relaxed_sources: set[Source] = set() + for constraint in output_graph.export_constraints: # type: ignore[attr-defined] + if constraint.t_id in output_graph.tracked_fakes_id_to_source: + torch.export.dynamic_shapes._process_equalities( + constraint, + get_sources, + output_graph.shape_env, + names, + source_pairs, + derived_equalities, + phantom_symbols, + relaxed_sources, + ) + else: + log.warning("Untracked tensor used in export constraints") + equalities_inputs = EqualityConstraint( + source_pairs=source_pairs, + derived_equalities=derived_equalities, + phantom_symbols=list(phantom_symbols.values()), + relaxed_sources=relaxed_sources, + warn_only=False, + ) + else: + equalities_inputs = None + + def _get_code_parts(langs: tuple[str, ...]) -> list[_ShapeGuardsHelper]: + # pyrefly: ignore [missing-attribute] + return output_graph.shape_env.produce_guards_verbose( + [a.fake for a in fs], # type: ignore[misc] + [a.source for a in fs], + input_contexts=input_contexts, # type: ignore[arg-type] + equalities_inputs=equalities_inputs, + source_ref=self.source_ref, + # Export keeps static. + # pyrefly: ignore [missing-attribute] + ignore_static=(not output_graph.export), + langs=langs, + ) + + if config.enable_cpp_symbolic_shape_guards: + try: + # For exporting we need the python code parts + python_code_parts, verbose_code_parts, cpp_code_parts = ( + _get_code_parts(("python", "verbose_python", "cpp")) # type: ignore[assignment] + ) + python_fallback = False + except OverflowError: + # Cannot use int64_t + python_fallback = True + python_code_parts, verbose_code_parts = _get_code_parts( + ("python", "verbose_python") + ) + else: + python_fallback = True + python_code_parts, verbose_code_parts = _get_code_parts( + ("python", "verbose_python") + ) + + # When exporting, we may work with the shape constraints some more in + # postprocessing, so don't freeze yet + if not output_graph.export: + output_graph.shape_env.freeze() + + if self.save_guards: + # For SHAPE_ENV we want to skip serializing the entire ShapeEnv so instead + # we directly serialize the generated code here. + maybe_cpp_code_parts = locals().get("cpp_code_parts") + assert maybe_cpp_code_parts is None or isinstance( + maybe_cpp_code_parts, _CppShapeGuardsHelper + ) + maybe_shape_env_sources = ( + [] + if maybe_cpp_code_parts is None + else list(maybe_cpp_code_parts.source_to_symbol.keys()) + ) + self.check_fn_manager.shape_code_parts = ShapeCodeParts( + python_code_parts=python_code_parts, + verbose_code_parts=verbose_code_parts, + cpp_code_parts=maybe_cpp_code_parts, + python_fallback=python_fallback, + shape_env_sources=maybe_shape_env_sources, + ) + + for code in python_code_parts.exprs: + self._set_guard_export_info(guard, [code]) + + # Make ShapeEnv guards available for testing. + if compile_context := CompileContext.try_get(): + compile_context.shape_env_guards.extend(verbose_code_parts.exprs) + + int_source_to_symbol = [] + float_source_to_symbol = [] + + if not python_fallback: + assert cpp_code_parts # type: ignore[possibly-undefined] + code_parts, source_to_symbol = ( + # pyrefly: ignore [unbound-name] + cpp_code_parts.exprs, + # pyrefly: ignore [unbound-name, missing-attribute] + cpp_code_parts.source_to_symbol, + ) + + if not code_parts: + return + + for source, symbol in source_to_symbol.items(): + if isinstance(source, ConstantSource): + python_fallback = True + else: + example_value = self.get( + source, + closure_vars={**SYMPY_INTERP, **_get_closure_vars()}, + ) + if isinstance(example_value, int): + int_source_to_symbol.append((source, symbol)) + elif isinstance(example_value, float): + float_source_to_symbol.append((source, symbol)) + else: + # SymInts/SymFloats go through python guard as we only support + # int64_t/double in C++ guards for now. + python_fallback = True + + if not python_fallback: + import ctypes + + from torch._inductor.codecache import CppCodeCache + + assert cpp_code_parts # type: ignore[possibly-undefined] + code_parts, source_to_symbol = ( + # pyrefly: ignore [unbound-name] + cpp_code_parts.exprs, + # pyrefly: ignore [unbound-name, missing-attribute] + cpp_code_parts.source_to_symbol, + ) + + source_to_symbol = dict(int_source_to_symbol + float_source_to_symbol) + try: + guard_managers = [ + self.get_guard_manager_from_source(IndexedSource(source, i)) + for i, source in enumerate(source_to_symbol) + ] + + int_symbols_str = ", ".join( + f"{symbol} = int_values[{i}]" + for i, (_, symbol) in enumerate(int_source_to_symbol) + ) + float_symbols_str = ", ".join( + f"{symbol} = float_values[{i}]" + for i, (_, symbol) in enumerate(float_source_to_symbol) + ) + + if int_symbols_str: + int_symbols_str = f"int64_t {int_symbols_str};" + if float_symbols_str: + float_symbols_str = f"double {float_symbols_str};" + + func_str = textwrap.dedent( + f""" + #include + #include + #include + #include + + #if defined(_MSC_VER) + # define EXTERN_DLL_EXPORT extern "C" __declspec(dllexport) + #else + # define EXTERN_DLL_EXPORT extern "C" + #endif + + EXTERN_DLL_EXPORT int8_t guard(int64_t *int_values, double *float_values) {{ + {int_symbols_str} + {float_symbols_str} + return ({") && (".join(code_parts)}); + }} + """ + ) + guards_log.debug( + "C++ shape guard function: %s %s", + func_str, + verbose_code_parts.exprs, + ) + clib = CppCodeCache.load(func_str) + cguard = ctypes.cast(clib.guard, ctypes.c_void_p).value + assert cguard + except torch._inductor.exc.InvalidCxxCompiler: + # No valid C++ compiler to compile the shape guard + pass + else: + install_symbolic_shape_guard( + guard_managers, + len(int_source_to_symbol), + len(float_source_to_symbol), + cguard, + clib, + verbose_code_parts.exprs, + guard.user_stack, + ) + return + + # Install all the symbolic guards in one python lambda guard. These are run + # at the very end of the RootGuardManager via epilogue guards. + # TODO(anijain2305,williamwen42) - Consider moving this to C++. + if python_code_parts.exprs: + self.add_python_lambda_leaf_guard_to_root( + python_code_parts.exprs, + verbose_code_parts.exprs, + closure_vars={**SYMPY_INTERP, **_get_closure_vars()}, + ) + + @register_guard_check_spec( + get_metadata_fn=lambda guard, value: extract_tensor_metadata(value), + eval_fn=lambda value, metadata: ( + isinstance(value, torch.Tensor) + and extract_tensor_metadata(value) == metadata + ), + ) + def TENSOR_MATCH(self, guard: Guard, value: Any | None = None) -> None: + if config._unsafe_skip_fsdp_module_guards and guard.is_fsdp_module(): + return + # For tensors that are part of the Dynamo extracted Fx graph module, an + # ID_MATCH suffices. + if match_on_id_for_tensor(guard): + self.ID_MATCH(guard) + else: + if isinstance(value, TensorWeakRef): + value = value() + + value = value if value is not None else self.get(guard) + + pytype = type(value) + dispatch_keys = torch._C._dispatch_keys(value) + if isinstance(value, torch._subclasses.FakeTensor): + if value.pytype is not None: + pytype = value.pytype + if value.dispatch_keys is not None: + dispatch_keys = value.dispatch_keys + + assert isinstance(value, torch.Tensor) + + if config.log_compilation_metrics and isinstance(value, torch.nn.Parameter): + metrics_context = get_metrics_context() + if metrics_context.in_progress(): + metrics_context.increment("param_numel", value.numel()) + metrics_context.increment("param_bytes", value.nbytes) + metrics_context.increment("param_count", 1) + + tensor_name = self.arg_ref(guard) + # [Note - On Export Tensor Guards] + # + # In eager mode, tensor guards are evaluated through C++, in guards.cpp + # see [Note - On Eager Tensor Guards] for more info. + # + # In export mode, we instead maintain parallel logic between C++ and python + # here, with an exception of checking the dispatch key - with the idea that a dispatch key + # is an entirely runtime notion that would make no sense to keep in an exported graph. + # + # Now, this idea is okay, but to paraphrase @ezyang, this mental model is sufficient for now, although + # not entirely true. + # For example, suppose one of the input tensors had the negative dispatch key. + # You should end up with a graph that is specialized for tensors that have a negative dispatch key. + # If you allow a Tensor that does NOT have this bit set, you will accidentally run it "as if" it were negated. + # Now, negative key only shows up for complex numbers, and most likely, the exported to target doesn't + # support this feature at all, but the point stands that :some: tensor state only shows up on dispatch key. + # TODO(voz): Either populate a dispatch_key check into the guards, or error on users passing in an unsupported + # subset of keys during export. + # + # The list of tensor fields and calls we care about can be found in `terms` below. + # TODO(voz): We are missing storage offset in all our tensor guards? + code: list[str] = [] + assert self.check_fn_manager.output_graph is not None + if self.check_fn_manager.output_graph.export: + self.TYPE_MATCH(guard) + terms = [ + "dtype", + "device", + "requires_grad", + "ndimension", + ] + + for term in terms: + term_src = AttrSource(guard.originating_source, term) + if term == "ndimension": + term = "ndimension()" + term_src = CallFunctionNoArgsSource(term_src) + real_value = self.get(term_src) + if istype(real_value, (torch.device, torch.dtype)): + # copy pasted from EQUALS_MATCH + code.append(f"str({tensor_name}.{term}) == {str(real_value)!r}") + else: + code.append(f"{tensor_name}.{term} == {real_value}") + else: + guard_manager = self.get_guard_manager(guard) + + # skip_no_tensor_aliasing_guards_on_parameters bring + # unsoundness. If you compile a function with two different + # parameters, but later on you pass on same tensor as two + # different outputs (aliasing), Dynamo will not detect this. + # But we deliberately take this soundness hit because this + # usecase is quite rare and there is substantial reduction in + # guard overhead. + # For numpy tensors, since those are ephemeral, we don't have to + # insert aliasing guards on them + if not ( + config.skip_no_tensor_aliasing_guards_on_parameters + and ( + istype(value, torch.nn.Parameter) + or is_from_unspecialized_builtin_nn_module_source( + guard.originating_source + ) + ) + ) and not isinstance(guard.originating_source, NumpyTensorSource): + # Keep track of all the tensor guard managers to insert + # NoAliasing check at the end. + self.no_tensor_aliasing_names.append(tensor_name) + self.no_tensor_aliasing_guard_managers.append(guard_manager) + + output_graph = self.check_fn_manager.output_graph + metadata = output_graph.input_source_to_sizes_strides[ + guard.originating_source + ] + size = convert_to_concrete_values(metadata["size"]) + stride = convert_to_concrete_values(metadata["stride"]) + + verbose_code_parts = get_verbose_code_parts( + get_tensor_guard_code_part( + value, + tensor_name, + size, + stride, + pytype, + dispatch_keys, + ), + guard, + ) + user_stack = guard.user_stack + guard_manager.add_tensor_match_guard( + value, + size, # type: ignore[arg-type] + stride, # type: ignore[arg-type] + tensor_name, + verbose_code_parts, + user_stack, + pytype, + dispatch_keys, + ) + + # We consider TENSOR_MATCH guard to be important enough to be + # included in diff guard manager by default. + if not isinstance(value, torch.nn.Parameter): + self.guard_manager.diff_guard_sources.add(guard.name) + + # A frame is valid for reuse with dynamic dimensions if the new + # (user-requested) dynamic dimensions are a subset of the old + # (already compiled) dynamic dimensions. + # + # It's a little non-obvious why you'd want this: in particular, + # if an already compiled frame matches all of the guards, why + # not just use it, why force a recompile? + # + # We force it for two reasons: + # + # - The user *required* us to compile with a new dynamic dimension, + # we should not ignore that and serve up the old, specialized + # frame. Listen to the user! + # + # - In fact, we are obligated to *raise an error* if we fail to + # make the requested dimension dynamic. If we don't + # recompile, we can't tell if that dimension can actually be + # made dynamic. + # + # If the new dynamic dims are a subset of the old, we already know + # we can make them dynamic (since we made them dynamic in old). + # This is slightly unsound, because maybe your input size is + # [s0, s0, s1] and so you can do it dynamic if you say dynamic + # dims {0, 1, 2} but you can't if you only do {0, 2} (because now + # the second s0 is specialized). But we're not entirely sure if + # this is a good idea anyway lol... (if you want to try removing + # this logic, be my guest! -- ezyang 2024) + # + assert guard.source is not None + static, _reason = tensor_always_has_static_shape( + value, is_tensor=True, tensor_source=guard.originating_source + ) + + if not static: + if hasattr(value, "_dynamo_dynamic_indices"): + dynamic_indices = value._dynamo_dynamic_indices + code_part = f"(({tensor_name}._dynamo_dynamic_indices.issubset({dynamic_indices})) if hasattr({tensor_name}, '_dynamo_dynamic_indices') else True)" # noqa: B950 + code.append(code_part) + self.get_guard_manager(guard).add_dynamic_indices_guard( + dynamic_indices, + get_verbose_code_parts(code_part, guard), + guard.user_stack, + ) + # In the case of us not having any dynamic dimension indices, we compiled the frame with no chance of + # raising for this specific tensor - and any inputs with more dynamic user directives specified must be recompiled. + else: + code_part = ( + f"hasattr({tensor_name}, '_dynamo_dynamic_indices') == False" + ) + code.append(code_part) + self.get_guard_manager(guard).add_no_hasattr_guard( + "_dynamo_dynamic_indices", + get_verbose_code_parts(code_part, guard), + guard.user_stack, + ) + + # Guard on shape_ids for tensors marked with mark_unbacked(). + # - If the runtime tensor has _dynamo_unbacked_indices → check shape_ids match + # - If the runtime tensor doesn't have _dynamo_unbacked_indices → pass + # We must install guards even when shape_ids is None to detect runtime + # tensors that have the attribute when compile-time didn't. + if hasattr(value, "_dynamo_unbacked_indices"): + shape_ids = getattr(value, "_dynamo_shape_ids", None) + code_part = f"((getattr({tensor_name}, '_dynamo_shape_ids', None) == {shape_ids!r}) if hasattr({tensor_name}, '_dynamo_unbacked_indices') else True)" # noqa: B950 + code.append(code_part) + self.get_guard_manager(guard).add_lambda_guard( + lambda x, expected=shape_ids: ( + getattr(x, "_dynamo_shape_ids", None) == expected + if hasattr(x, "_dynamo_unbacked_indices") + else True + ), + get_verbose_code_parts(code_part, guard), + guard.user_stack, + ) + + # Guard on unbacked_bounds for tensors marked with mark_unbacked(). + # - If the runtime tensor has _dynamo_unbacked_indices → check bounds match + # - If the runtime tensor doesn't have _dynamo_unbacked_indices → pass + # We must install guards even when unbacked_bounds is None to detect runtime + # tensors that have the attribute when compile-time didn't. + if hasattr(value, "_dynamo_unbacked_indices"): + unbacked_bounds = getattr(value, "_dynamo_unbacked_bounds", None) + code_part = f"((getattr({tensor_name}, '_dynamo_unbacked_bounds', None) == {unbacked_bounds!r}) if hasattr({tensor_name}, '_dynamo_unbacked_indices') else True)" # noqa: B950 + code.append(code_part) + self.get_guard_manager(guard).add_lambda_guard( + lambda x, expected=unbacked_bounds: ( + getattr(x, "_dynamo_unbacked_bounds", None) == expected + if hasattr(x, "_dynamo_unbacked_indices") + else True + ), + get_verbose_code_parts(code_part, guard), + guard.user_stack, + ) + + # TODO we dont have guards on _dynamo_unbacked_indices like those of _dynamo_dynamic_indices this seems wrong!! + + if len(code) > 0: + self._set_guard_export_info(guard, code) + + # A util that in the case of export, adds data onto guards + def _set_guard_export_info( + self, + guard: Guard, + code_list: list[str], + provided_guarded_object: Any | None = None, + provided_func_name: str | None = None, + ) -> None: + # WARNING: It is important that cur_frame/caller do NOT stay in + # the current frame, because they will keep things live longer + # than they should. See TestMisc.test_release_module_memory + cur_frame = currentframe() + assert cur_frame is not None + caller = cur_frame.f_back + del cur_frame + assert caller is not None + func_name = provided_func_name or caller.f_code.co_name + del caller + # We use func_name for export, so might as well get a nice defensive check out of it + assert func_name in self.__class__.__dict__, ( + f"_produce_guard_code must be called from inside GuardedCode. Called from {func_name}" + ) + + # Not all guards have names, some can be installed globally (see asserts on HAS_GRAD) + if provided_guarded_object is None: + name = guard.name + guarded_object = None if not name else self.get(guard) + else: + guarded_object = provided_guarded_object + + guarded_object_type = ( + weakref.ref(type(guarded_object)) if guarded_object is not None else None + ) + obj_ref = None + # Not necessary to have weakref for Enum type, but there is a bug that + # makes hasattr(guarded_object.__class__, "__weakref__") return True. + supports_weakref = ( + getattr(guarded_object.__class__, "__weakrefoffset__", 0) != 0 + ) + # See D64140537 for why we are checking for tuple. + if supports_weakref and not isinstance( + guarded_object, (enum.Enum, tuple, weakref.ProxyTypes) + ): + obj_ref = weakref.ref(guarded_object) + + guard.set_export_info( + func_name, + guarded_object_type, + code_list, + obj_ref, + ) + + +# Common Sub-Expression Elimination for Python expressions. +# +# There are 2 steps to this pass: +# 1. Count the frequency of each sub-expression (i.e. inner +# node in the AST tree) +# +# 2. Replace those that occur more than once by a fresh variable 'v'. +# 'v' will be defined in the 'preface' list (output argument to +# 'NodeTransformer') +# +# NB: the use of 'ast.unparse' while visiting the nodes makes this pass +# quadratic on the depth of the tree. +# +# NB: this pass creates a new variable for each AST node that is repeated +# more than 'USE_THRESHOLD'. e.g. if 'a.b.c.d' is used 10 times, 'a.b.c' +# and 'a.b' are also used 10 times. So, there will be a new variable for +# each of them. +class PyExprCSEPass: + # Maximum number of times a given expression can be used without being + # replaced by a fresh variable. + USE_THRESHOLD = 1 + + # Ad-Hoc: AST nodes this pass focuses on. + ALLOWED_NODE_TYPES = (ast.Attribute, ast.Call, ast.Subscript) + + @dataclasses.dataclass + class Config: + expr_count: dict[str, int] + expr_to_name: dict[str, str] + + class ExprCounter(ast.NodeVisitor): + def __init__(self, config: PyExprCSEPass.Config) -> None: + self._config = config + + def visit(self, node: ast.AST) -> None: + if isinstance(node, PyExprCSEPass.ALLOWED_NODE_TYPES): + self._config.expr_count[_ast_unparse(node)] += 1 + super().visit(node) + + class Replacer(ast.NodeTransformer): + def __init__( + self, + config: PyExprCSEPass.Config, + gen_name: Callable[[], str], + ) -> None: + super().__init__() + self._config = config + self._gen_name = gen_name + self.preface: list[str] = [] + + def visit(self, node: ast.AST) -> Any: + if isinstance(node, PyExprCSEPass.ALLOWED_NODE_TYPES): + expr = _ast_unparse(node) + + # Replacement only occurs if a given expression is used more + # than once. + if self._config.expr_count[expr] > PyExprCSEPass.USE_THRESHOLD: + if expr not in self._config.expr_to_name: + # Parent 'visit' is called so that we CSE the inner expressions first. + # + # The resulting expression is used as right-hand-side of the variable + # assignment. i.e. we are CSE-ing the children before the parents. + # + # Indexing still uses the old 'node', since that's what was counted + # by the 'NodeVisitor'. + node_ = super().visit(node) + expr_ = _ast_unparse(node_) + var_name = self._gen_name() + self.preface.append(f"{var_name} = {expr_}") + self._config.expr_to_name[expr] = var_name + else: + var_name = self._config.expr_to_name[expr] + return ast.Name(var_name, ast.Load()) + + return super().visit(node) + + def __init__(self) -> None: + self._counter = 0 + self._config = self.Config( + expr_count=collections.defaultdict(lambda: 0), expr_to_name={} + ) + + def _new_var(self, prefix: str = "_var") -> str: + name = f"{prefix}{self._counter}" + self._counter += 1 + return name + + def count(self, exprs: list[str]) -> None: + counter = self.ExprCounter(self._config) + for e in exprs: + try: + counter.visit(ast.parse(e)) + except SyntaxError as ex: + log.exception("Failed to visit expr at line %s.\n%s", ex.lineno, e) + raise + + def replace(self, expr: str) -> tuple[list[str], str]: + replacer = self.Replacer(self._config, self._new_var) + new_node = replacer.visit(ast.parse(expr)) + return replacer.preface, _ast_unparse(new_node) + + +def must_add_nn_module_guards(guard: Guard) -> bool: + # For config.guard_nn_modules=False, we can skip all the guards that + # originate from inside of nn module except for a few categories. + return ( + # Guard for defaults + isinstance(guard.originating_source, DefaultsSource) + # Guard using dict tags if the config flag is set + or ( + config.guard_nn_modules_using_dict_tags + and guard.create_fn is GuardBuilder.NN_MODULE + ) + ) + + +class DeletedGuardManagerWrapper(GuardManagerWrapper): + def __init__(self, reason: str) -> None: + super().__init__() + self.invalidation_reason = reason + + def populate_diff_guard_manager(self) -> None: + self.diff_guard_root = None + + +@dataclasses.dataclass +class ShapeCodeParts: + python_code_parts: _ShapeGuardsHelper + verbose_code_parts: _ShapeGuardsHelper + cpp_code_parts: _CppShapeGuardsHelper | None + python_fallback: bool + shape_env_sources: list[Source] + + +@dataclasses.dataclass +class GuardsState: + output_graph: OutputGraphGuardsState + shape_code_parts: ShapeCodeParts | None + + +class _Missing: + def __init__(self, reason: str | None = None) -> None: + self._reason = reason + + def __repr__(self) -> str: + return f"_Missing({self._reason})" + + def __str__(self) -> str: + return f"_Missing({self._reason})" + + # Sometimes _Missing object is used as the callable with functools.partial, + # so we add a dummy __call__ here to bypass TypeError from partial(). + def __call__(self, *args: Any, **kwargs: Any) -> Any: + return _Missing() + + +@functools.cache +def _get_unsupported_types() -> tuple[type, ...]: + # We only do ID_MATCH on C objects which is already banned from guards serialization. + ret: tuple[type, ...] = ( + torch._C.Stream, + weakref.ReferenceType, + ) + try: + ret += (torch._C._distributed_c10d.ProcessGroup,) + except AttributeError: + pass + return ret + + +class GuardsStatePickler(pickle.Pickler): + def __init__( + self, + guard_tree_values: dict[int, Any], + empty_values: dict[int, Any], + missing_values: dict[int, Any], + *args: Any, + **kwargs: Any, + ) -> None: + super().__init__(*args, **kwargs) + self.fake_mode = torch._subclasses.FakeTensorMode() + self.tensor_converter = torch._subclasses.fake_tensor.FakeTensorConverter() + self.guard_tree_values = guard_tree_values + self.empty_values = empty_values + self.missing_values = missing_values + + @classmethod + def _unpickle_module(cls, state: Any) -> torch.nn.Module: + mod = torch.nn.Module() + mod.__setstate__(state) + return mod + + @classmethod + def _unpickle_tensor( + cls, + meta_tensor: torch.Tensor, + device: torch.device, + pytype: type, + dispatch_keys_raw: int, + grad: torch.Tensor, + ) -> torch.Tensor: + fake_mode = torch._subclasses.FakeTensorMode() + tensor_converter = torch._subclasses.fake_tensor.FakeTensorConverter() + ret = tensor_converter.from_meta_and_device( + fake_mode, + meta_tensor, + device, + pytype, + torch._C.DispatchKeySet.from_raw_repr(dispatch_keys_raw), + ) + ret.grad = grad + return ret + + @classmethod + def _unpickle_traceable_wrapper_subclass( + cls, + meta_tensor: torch.Tensor, + device: torch.device, + pytype: type, + dispatch_keys_raw: int, + ctx: Any, + inner_data: list[tuple[str, Any]], + ) -> torch.Tensor: + inner_tensors = dict(inner_data) + + outer_size, outer_stride = meta_tensor.shape, meta_tensor.stride() + out = type(meta_tensor).__tensor_unflatten__( # type: ignore[attr-defined] + inner_tensors, ctx, outer_size, outer_stride + ) + out.pytype = pytype + out.dispatch_keys = torch._C.DispatchKeySet.from_raw_repr(dispatch_keys_raw) + return out + + @classmethod + def _unpickle_python_module(cls, alias: str) -> types.ModuleType: + return importlib.import_module(alias) + + @classmethod + def _unpickle_dispatch_key_set(cls, raw_repr: int) -> torch._C.DispatchKeySet: + return torch._C.DispatchKeySet.from_raw_repr(raw_repr) + + @classmethod + def _unpickle_functorch_interpreter( + cls, json: bytes + ) -> torch._C._functorch.CInterpreter: + return torch._C._functorch.CInterpreter.deserialize(json) + + @classmethod + def _unpickle_mapping_proxy( + cls, d: dict[Any, Any] + ) -> types.MappingProxyType[Any, Any]: + return types.MappingProxyType(d) + + @classmethod + def _unpickle_dict_keys(cls, elems: list[Any]) -> Any: + return dict.fromkeys(elems).keys() + + @classmethod + def _unpickle_count_iter(cls, item: int, step: int) -> itertools.count[int]: + return itertools.count(item, step) + + @classmethod + def _unpickle_fsdp_module_type( + cls, original_type: type[torch.nn.Module] + ) -> type[torch.nn.Module]: + return torch.distributed.fsdp._fully_shard._fully_shard.get_cls_to_fsdp_cls()[ + original_type + ] + + @classmethod + def _unpickle_ddp_module( + cls, state: dict[str, Any] + ) -> torch.nn.parallel.DistributedDataParallel: + ty = torch.nn.parallel.DistributedDataParallel + ddp = ty.__new__(ty) + torch.nn.Module.__setstate__(ddp, state) + return ddp + + @classmethod + def _unpickle_c_op(cls, name: str) -> Any: + return getattr(torch.ops._C, name) + + @classmethod + def _unpickle_op(cls, namespace: str, opname: str, overloadname: str) -> Any: + return getattr(getattr(getattr(torch.ops, namespace), opname), overloadname) + + @classmethod + def _unpickle_bound_method(cls, func: Any, base: Any) -> Any: + return types.MethodType(func, base) + + @staticmethod + def _unpickle_sdp_backend(name: str) -> torch.nn.attention.SDPBackend: + # Reconstruct from the Python-facing enum namespace + return getattr(torch.nn.attention.SDPBackend, name) + + @classmethod + def _unpickle_cell(cls, val: Any) -> Any: + def _() -> Any: + return val + + assert _.__closure__ is not None + return _.__closure__[0] + + @classmethod + def _unpickle_named_tuple_type( + cls, name: str, fields: tuple[str, ...] + ) -> type[NamedTuple]: + # pyrefly: ignore [bad-return] + return collections.namedtuple(name, fields) + + @classmethod + def _unpickle_code(cls, serialized_code: SerializedCode) -> types.CodeType: + from torch._dynamo.package import SerializedCode + + return SerializedCode.to_code_object(serialized_code) + + @classmethod + def _unpickle_nested_function( + cls, + code: types.CodeType, + module: str, + qualname: str, + argdefs: tuple[object, ...] | None, + closure: tuple[types.CellType, ...] | None, + ) -> types.FunctionType: + f_globals = importlib.import_module(module).__dict__ + return types.FunctionType(code, f_globals, qualname, argdefs, closure) + + # pyrefly: ignore [bad-override] + def reducer_override( + self, obj: Any + ) -> tuple[Callable[..., Any], tuple[Any, ...]] | Any: + import sympy + + if id(obj) in self.empty_values: + return type(obj).__new__, (type(obj),) + + if inspect.iscode(obj): + from torch._dynamo.package import SerializedCode + + return type(self)._unpickle_code, (SerializedCode.from_code_object(obj),) + + if id(obj) in self.missing_values: + return _Missing, ("missing values",) + + if isinstance(obj, torch.Tensor) and obj.device.type != "meta": + from torch.utils._python_dispatch import is_traceable_wrapper_subclass + + if id(obj) not in self.guard_tree_values: + return _Missing, ("tensor guard tree",) + + if is_traceable_wrapper_subclass(obj): + # inner_data is a list of tuples of: + # (inner attr name, unpickle func, tuple of func inputs) + # This supports traceable wrapper subclass inner tensors. + inner_data = [] + attrs, ctx = obj.__tensor_flatten__() + # recursively call for inner tensor components + for attr in attrs: + inner = getattr(obj, attr) + if isinstance(inner, torch.Tensor): + self.guard_tree_values[id(inner)] = inner + inner_data.append((attr, inner)) + + return type(self)._unpickle_traceable_wrapper_subclass, ( + torch.empty_like(obj, device="meta"), + obj.device, + type(obj), + torch._C._dispatch_keys(obj).raw_repr(), + ctx, + inner_data, + ) + + # For FakeTensors, use pytype if set, otherwise default to + # torch.Tensor. This is important for cross-compilation where + # we compile with fake tensors but run with real tensors. + pytype = type(obj) + if isinstance(obj, torch._subclasses.FakeTensor): + pytype = obj.pytype if obj.pytype is not None else torch.Tensor + + return type(self)._unpickle_tensor, ( + torch.empty_like(obj, device="meta", requires_grad=obj.requires_grad), + obj.device, + pytype, + torch._C._dispatch_keys(obj).raw_repr(), + obj.grad, + ) + + elif isinstance(obj, torch.nn.Module): + if id(obj) not in self.guard_tree_values: + return _Missing, ("module guard tree",) + + for attr in obj.__dict__.values(): + if isinstance(attr, (torch.Tensor, torch.nn.Module)): + continue + if id(attr) in self.guard_tree_values: + continue + if callable(attr): + continue + self.missing_values[id(attr)] = attr + + # DDP module is a special case because it tries to restore unneeded + # data in custom __setstate__. We cannot skip ddp module because it + # is often a toplevel module. + if isinstance(obj, torch.nn.parallel.DistributedDataParallel): + return type(self)._unpickle_ddp_module, (obj.__getstate__(),) + + if type(obj).__qualname__ == type(obj).__name__: + return NotImplemented + if obj.__class__.__getstate__ == torch.nn.Module.__getstate__: + return type(self)._unpickle_module, (obj.__getstate__(),) + + elif inspect.ismodule(obj): + return type(self)._unpickle_python_module, (obj.__name__,) + + elif isinstance(obj, torch._C.DispatchKeySet): + return type(self)._unpickle_dispatch_key_set, (obj.raw_repr(),) + + elif isinstance(obj, torch._C._functorch.CInterpreter): + return type(self)._unpickle_functorch_interpreter, (obj.serialize(),) + + elif ( + inspect.isclass(obj) + and issubclass(obj, sympy.Function) + and hasattr(obj, "_torch_handler_name") + ): + assert hasattr(obj, "_torch_unpickler") + return obj._torch_unpickler, (obj._torch_handler_name,) + + elif ( + inspect.isclass(obj) + and issubclass(obj, tuple) + and hasattr(obj, "_fields") + and obj.__qualname__ != obj.__name__ + ): + return type(self)._unpickle_named_tuple_type, (obj.__name__, obj._fields) + + elif isinstance(obj, torch.SymInt): + raise RuntimeError(f"Cannot serialize SymInt {obj} (node: {obj.node})") + + elif isinstance(obj, types.MappingProxyType): + return type(self)._unpickle_mapping_proxy, (obj.copy(),) + + elif type(obj) is _COUNT_ITERATOR_TYPE: + item, step = normalize_count_iter(obj) + if item is not NotImplemented and step is not NotImplemented: + return type(self)._unpickle_count_iter, (item, step) + + elif isinstance(obj, torch._dynamo.utils.dict_keys): + return type(self)._unpickle_dict_keys, (list(obj),) + + elif isinstance( + obj, torch._ops.OpOverloadPacket + ) and obj._qualified_op_name.startswith("_C::"): + return type(self)._unpickle_c_op, (obj.__name__,) + + elif isinstance(obj, torch._ops.OpOverload): + return type(self)._unpickle_op, ( + obj.namespace, + obj._opname, + obj._overloadname, + ) + + elif ( + obj.__class__.__module__ == "builtins" + and obj.__class__.__name__ == "PyCapsule" + ): + # Skipping PyCapsule since there isn't much to be guarded about them. + return _Missing, ("capsule",) + + elif isinstance(obj, _get_unsupported_types()): + return _Missing, ("unsupported",) + + elif inspect.isfunction(obj): + if "" in obj.__qualname__: + return type(self)._unpickle_nested_function, ( + obj.__code__, + obj.__module__, + obj.__qualname__, + obj.__defaults__, + obj.__closure__, + ) + if obj.__module__ in sys.modules: + f = sys.modules[obj.__module__] + for name in obj.__qualname__.split("."): + f = getattr(f, name, None) # type: ignore[assignment] + if f is not obj: + return _Missing, ("fqn mismatch",) + elif inspect.ismethod(obj): + func = obj.__func__ + method_self = obj.__self__ + inner_func = getattr(method_self, func.__name__) + if inspect.ismethod(inner_func): + inner_func = inner_func.__func__ + if func is not inner_func: + return type(self)._unpickle_bound_method, (func, method_self) + + elif isinstance(obj, type((lambda x: lambda: x)(0).__closure__[0])): # type: ignore[index] # noqa: PLC3002 + return type(self)._unpickle_cell, (obj.cell_contents,) + + if hasattr(torch.distributed, "distributed_c10d") and isinstance( + obj, torch.distributed.distributed_c10d.Work + ): + if id(obj) not in self.guard_tree_values: + return _Missing, ("distributed_c10d.Work",) + + if isinstance(obj, torch.nn.attention.SDPBackend): + return type(self)._unpickle_sdp_backend, (obj.name,) + + if type(obj).__qualname__ != type(obj).__name__ and not isinstance(obj, tuple): + raise torch._dynamo.exc.PackageError( + f"Type {type(obj)} for object {obj} cannot be saved " + + "into torch.compile() package since it's defined in local scope. " + + "Please define the class at global scope (top level of a module)." + ) + + if ( + inspect.isclass(obj) + and hasattr(torch.distributed, "fsdp") + and issubclass(obj, torch.distributed.fsdp._fully_shard.FSDPModule) + ): + if obj is not torch.distributed.fsdp._fully_shard.FSDPModule: + original_type = obj.__mro__[2] + assert issubclass(original_type, torch.nn.Module) + assert ( + original_type + in torch.distributed.fsdp._fully_shard._fully_shard.get_cls_to_fsdp_cls() + ) + return type(self)._unpickle_fsdp_module_type, (original_type,) + + return NotImplemented + + +def make_guard_filter_entry(guard: Guard, builder: GuardBuilder) -> GuardFilterEntry: + MISSING = object() + name = strip_local_scope(guard.name) + if name == "": + has_value = False + value = MISSING + else: + try: + # Guard evaluation is expected to fail when we guard on + # things like "not hasattr(x, 'foo')". In cases like this, + # we don't have a well defined value because such thing + # doesn't exist. + value = builder.get(guard) + has_value = True + except: # noqa: B001,E722 + value = MISSING + has_value = False + is_global = get_global_source_name(guard.originating_source) is not None + return GuardFilterEntry( + name=name, + has_value=has_value, + value=value, + guard_type=guard.create_fn_name(), + derived_guard_types=(tuple(guard.guard_types) if guard.guard_types else ()), + is_global=is_global, + orig_guard=guard, + ) + + +def pickle_guards_state( + state: GuardsState, + builder: GuardBuilder, +) -> bytes: + buf = io.BytesIO() + empty_values = {} + missing_values = {} + guard_tree_values = builder.guard_tree_values + + leaves = pytree.tree_leaves(state.output_graph.local_scope) + for leaf in leaves: + if inspect.ismethod(leaf) and hasattr(leaf, "__self__"): + base = leaf.__self__ + if id(base) not in guard_tree_values: + try: + type(base).__new__(type(base)) + empty_values[id(base)] = base + except: # noqa: E722, B001 + pass + elif id(leaf) not in guard_tree_values: + # TODO See if we have lift this branch as the first one. + # Prune more objects in pytree hierarchy. + missing_values[id(leaf)] = leaf + pickler = GuardsStatePickler(guard_tree_values, empty_values, missing_values, buf) + + if all( + torch.compiler.keep_portable_guards_unsafe( + [ + make_guard_filter_entry(guard, builder) + for guard in state.output_graph.guards + ] + ) + ): + # Prune more values in AOT precompile when complex pickling structure is not needed. + state.output_graph.guard_on_key_order = set() + state.output_graph.global_scope = {} + + try: + pickler.dump(state) + except AttributeError as e: + raise torch._dynamo.exc.PackageError(str(e)) from e + return buf.getvalue() + + +# NB: Naively, you'd expect this to only be a function that produces +# the callable that constitutes the guard. However, there is some +# delicate handling for invalidating this check function when the +# locals/globals get invalidated, so there's some extra state +# we have to hold in this manager class. +class CheckFunctionManager: + def __init__( + self, + f_code: types.CodeType, + output_graph: OutputGraphCommon, + cache_entry: CacheEntry | None = None, + guard_fail_fn: Callable[[GuardFail], None] | None = None, + guard_filter_fn: Callable[[Sequence[GuardFilterEntry]], Sequence[bool]] + | None = None, + shape_code_parts: ShapeCodeParts | None = None, + runtime_global_scope: dict[str, Any] | None = None, + save_guards: bool = False, + strict_error: bool = False, + ) -> None: + guards = output_graph.guards if output_graph else None + self._weakrefs: dict[int, ReferenceType[object]] = {} + + existing_diff_guard_sources = ( + update_diff_guard_managers_for_existing_cache_entries(cache_entry) + ) + self.output_graph: OutputGraphCommon | None = output_graph + assert self.output_graph is not None + + # Only used for serialization. + self.shape_code_parts = shape_code_parts + + # NB: Until we trace device contexts, we need to use the stack recorded at the beginning of tracing + # in case a set default device call was made in the graph. + self.torch_function_mode_stack = ( + output_graph.torch_function_mode_stack if output_graph else None + ) + self.used_builtin_vars: OrderedSet[str] = OrderedSet() + self.additional_used_local_vars: OrderedSet[str] = OrderedSet() + self.additional_used_global_vars: OrderedSet[str] = OrderedSet() + self.runtime_global_scope = runtime_global_scope + self.global_state: torch._C._dynamo.guards.GlobalStateGuard | None = None + self.torch_function_mode_stack_check_fn: Callable[[], bool] | None = None + + if not justknobs_check("pytorch/compiler:guard_nn_modules"): + log.warning("guard_nn_modules is turned off using justknobs killswitch") + + # TODO Be more explicit about the behavior for the users. + if torch._dynamo.config.caching_precompile: + _guard_filter_fn = guard_filter_fn or (lambda gs: [True for g in gs]) + + def guard_filter_fn(guards: Sequence[GuardFilterEntry]) -> Sequence[bool]: + ret = [] + for keep, g in zip(_guard_filter_fn(guards), guards): + if not keep: + ret.append(False) + elif ( + g.guard_type + in ( + "ID_MATCH", + "CLOSURE_MATCH", + "WEAKREF_ALIVE", + "DICT_VERSION", + ) + or "ID_MATCH" in g.derived_guard_types + or "DICT_VERSION" in g.derived_guard_types + ): + log.warning( + "%s guard on %s is dropped with caching_precompile=True.", + g.guard_type, + g.orig_guard.name, + ) + ret.append(False) + else: + ret.append(True) + return ret + + sorted_guards = sorted(guards or (), key=Guard.sort_key) + + # Disable __torch_function__ dispatch during guard construction so + # modes with mutable state aren't triggered. We exit the context + # before the guard sanity check so GlobalStateGuard.check() sees + # the true runtime state. + with torch._C.DisableTorchFunction(): + if guard_filter_fn: + # If we're filtering guards, we need to build it an extra time first + # because filtering depends on the builder/guard_manager results + builder, guard_manager = self.build_guards( + sorted_guards, + existing_diff_guard_sources, + f_code, + output_graph, + False, + ) + + filter_results = guard_filter_fn( + [make_guard_filter_entry(guard, builder) for guard in sorted_guards] + ) + assert len(filter_results) == len(sorted_guards) + assert all(type(x) is bool for x in filter_results) + sorted_guards = [ + guard for i, guard in enumerate(sorted_guards) if filter_results[i] + ] + + # Redo the guards because filtering relies on the results from the last guard builder. + builder, guard_manager = self.build_guards( + sorted_guards, + existing_diff_guard_sources, + f_code, + output_graph, + save_guards, + guard_filter_fn=guard_filter_fn, + ) + + self.guard_manager = guard_manager + self.compile_check_fn(builder, sorted_guards, guard_fail_fn) + + # Keep track of weak references of objects with ID_MATCH guard. This + # info is stored alongside optimized_code and guard_manager and is used to + # limit the number of cache entries with same ID_MATCH'd object. + # TODO(anijain2305) - Currently this information is stored as an attr on + # the guard_manager itself to avoid changing CacheEntry data structure in + # eval_frame.c. In future, we should probably replace guard_manager with a + # queryable data structure such that this information is already present + # in some form. + self.guard_manager.id_matched_objs = builder.id_matched_objs + + guards_log.debug("%s", self.guard_manager) + self.guard_manager.id_matched_objs = builder.id_matched_objs + + # Check that the guard returns True. False means that we will always + # recompile. + # TODO(anijain2305, ydwu4) - Skipping export because of following test + # python -s test/dynamo/test_export.py -k test_export_with_symbool_inputs + latency = 0.0 + + if not output_graph.skip_guards_check and not output_graph.export: + if not self.guard_manager.check(output_graph.local_scope): + reasons = get_guard_fail_reason_helper( + self.guard_manager, + output_graph.local_scope, + CompileContext.current_compile_id(), + backend=None, # no need to set this because we are trying to find the offending guard entry + ) + raise AssertionError( + "Guard failed on the same frame it was created. This is a bug - please create an issue." + f"Guard fail reason: {reasons}" + ) + + if guard_manager_testing_hook_fn is not None: + guard_manager_testing_hook_fn( + self.guard_manager, output_graph.local_scope, builder + ) + + # NB for developers: n_iters is chosen to be 1 to prevent excessive + # increase in compile time. We first do a cache flush to measure the + # guard latency more accurately. This cache flush is expensive. + # Note - If you are working on a guard optimization, it might be a + # good idea to increase this number for more stability during + # development. + latency = profile_guard_manager( + self.guard_manager.root, output_graph.local_scope, 1 + ) + guards_log.debug("Guard eval latency = %s us", f"{latency:.2f}") + # Note: We use `increment_toplevel` instead of `compilation_metric` + # here. This is because, in scenarios where `torch._dynamo.reset` + # is invoked, the same frame ID and compile ID may be reused during + # a new compilation cycle. This behavior causes issues with + # `compilation_metric`, as it expects the metric field to be empty. + # Ideally, we would overwrite the existing entry in such cases, but + # we currently lack an API to support overwriting metrics. However, + # since these situations are rare and typically impractical to + # account for, we simply increment at the toplevel instead. + CompileEventLogger.increment_toplevel("guard_latency_us", int(latency)) + + self.guards_state: bytes | None = None + if save_guards: + from torch._dynamo.output_graph import OutputGraphCommon + + assert isinstance(self.output_graph, OutputGraphCommon) + try: + self.guards_state = self.serialize_guards( + builder, sorted_guards, self.output_graph + ) + except exc.PackageError as e: + if torch._dynamo.config.strict_precompile or strict_error: + raise e + self.output_graph.bypass_package( + f"Guard evaluation failed: {str(e)}", + traceback=traceback.format_exc().split("\n"), + ) + + # TODO: don't do the string rep, do something more structured here + torch._logging.trace_structured( + "dynamo_cpp_guards_str", + payload_fn=lambda: f"{self.guard_manager}\nGuard latency = {latency:.2f} us", + ) + # NB - We have to very careful of cleaning up here. Because of the + # invalidate function, we can create a weakref finalizer that keeps + # `self` alive for very long. Sometimes by mistake, we can run + # invalidate for a type/object (check id_ref method) that Python can + # leak by design, preventing us from calling the finalizer. In that + # case, the `self` will be alive even though the cache entry will be + # deleted (check invalidate method), which can cause a memory leak, + # e.g., not setting output_graph = None can keep hold of nn_modules. + self._weakrefs.clear() + self.output_graph = None + + UNSUPPORTED_SERIALIZATION_GUARD_TYPES: tuple[LiteralString, ...] = ( + "DICT_VERSION", + "NN_MODULE", + "ID_MATCH", + "FUNCTION_MATCH", + "CLASS_MATCH", + "MODULE_MATCH", + "CLOSURE_MATCH", + "WEAKREF_ALIVE", + ) + + def serialize_guards( + self, + builder: GuardBuilder, + sorted_guards: list[Guard], + output_graph: OutputGraphCommon, + ) -> bytes: + # We check whether our list of guards are serializable here + for guard in sorted_guards: + guard_type = guard.create_fn_name() + derived_guard_types = tuple(guard.guard_types) if guard.guard_types else () + # BUILTIN_MATCH calls TYPE_MATCH sometimes, so we need to check both for + # a chance that the guard is unserializable + if guard_type in ("TYPE_MATCH", "BUILTIN_MATCH"): + if guard._unserializable: + # Only call builder.get again if we know we're going to throw + obj = builder.get(guard) + raise_local_type_error(obj) + elif ( + guard_type in CheckFunctionManager.UNSUPPORTED_SERIALIZATION_GUARD_TYPES + ): + raise torch._dynamo.exc.PackageError( + f"{guard_type} guard cannot be serialized." + ) + elif failed := next( + ( + i + for i in derived_guard_types + if i in CheckFunctionManager.UNSUPPORTED_SERIALIZATION_GUARD_TYPES + ), + None, + ): + # Just raise the first failed guard name + raise torch._dynamo.exc.PackageError( + f"{failed} guard cannot be serialized." + ) + + builtins_dict_name = output_graph.name_of_builtins_dict_key_in_fglobals or "" + used_global_vars = set() + used_local_vars = set() + + def prune_variable(source: Source) -> None: + if name := get_global_source_name(source): + assert isinstance(name, str) + # Leave out the builtins dict key, as we will special handle + # it later because the guarded code rarely use the entire + # builtin dict in the common case. + if name != builtins_dict_name: + used_global_vars.add(name) + elif name := get_local_source_name(source): + assert isinstance(name, str) + used_local_vars.add(name) + + output_graph_guards_state = output_graph.dump_guards_state() + # Only serialize the global variables that are actually used in guards. + for guard in sorted_guards: + if isinstance(guard.originating_source, ShapeEnvSource): + assert self.shape_code_parts + for source in self.shape_code_parts.shape_env_sources: + prune_variable(source) + else: + prune_variable(guard.originating_source) + + for source in output_graph.guard_on_key_order: + prune_variable(source) + + def normalize_create_fn(x: Callable[..., None]) -> Callable[..., None]: + if isinstance(x, functools.partial): + + def _ref(x: Any) -> Any: + if isinstance(x, (TensorWeakRef, weakref.ref)): + return x() + return x + + new_args = tuple(_ref(a) for a in x.args) + new_keywords = {k: _ref(v) for k, v in x.keywords.items()} + return functools.partial(x.func, *new_args, **new_keywords) + + return x + + global_scope_state = { + k: v + for k, v in output_graph_guards_state.global_scope.items() + if k in used_global_vars or k in self.additional_used_global_vars + } + global_scope_state[builtins_dict_name] = { + k: v + # pyrefly: ignore [missing-attribute] + for k, v in output_graph_guards_state.global_scope[ + builtins_dict_name + ].items() # type: ignore[attr-defined] + if k in self.used_builtin_vars + } + output_graph_guards_state = dataclasses.replace( + output_graph_guards_state, + local_scope={ + k: v + for k, v in output_graph_guards_state.local_scope.items() + if k in used_local_vars or k in self.additional_used_local_vars + }, + global_scope=global_scope_state, + _guards=torch._guards.GuardsSet( + OrderedSet( + dataclasses.replace( + guard, + obj_weakref=None, + guarded_class_weakref=None, + create_fn=normalize_create_fn(guard.create_fn), + ) + for guard in sorted_guards + ) + ), + input_source_to_sizes_strides=pytree.tree_map( + convert_int_to_concrete_values, + output_graph_guards_state.input_source_to_sizes_strides, + ), + skip_guards_check=True, + ) + guards_state = GuardsState( + output_graph=output_graph_guards_state, + shape_code_parts=self.shape_code_parts, + ) + + return pickle_guards_state(guards_state, builder) + + def build_guards( + self, + sorted_guards: list[Guard], + existing_diff_guard_sources: OrderedSet[str], + f_code: types.CodeType, + output_graph: OutputGraphGuardsState, + save_guards: bool, + guard_filter_fn: Callable[[Sequence[GuardFilterEntry]], Sequence[bool]] + | None = None, + ) -> tuple[GuardBuilder, GuardManagerWrapper]: + guard_manager = GuardManagerWrapper() + guard_manager.diff_guard_sources = existing_diff_guard_sources + + w_builder = None + + def source_ref(source: Source) -> str: + guard_source = source.guard_source + if guard_source is GuardSource.CONSTANT: + # No need to track constants + return source.name + assert w_builder + r_builder = w_builder() + assert r_builder is not None + return r_builder.arg_ref(source.name) + + builder = GuardBuilder( + f_code, + self.id_ref, + source_ref, + self.lookup_weakrefs, + output_graph.local_scope, + output_graph.global_scope, + guard_manager, + self, + save_guards, + runtime_global_scope=self.runtime_global_scope, + guard_filter_fn=guard_filter_fn, + ) + + # Break retain cycle. See test_release_scope_memory + def cleanup_builder(weak_b: weakref.ref[GuardBuilder]) -> None: + b = weak_b() + if b: + b.scope = None # type: ignore[assignment] + + # Break retain cycle. See test_release_input_memory + w_builder = weakref.ref(builder, cleanup_builder) + + guard_on_nn_modules = config.guard_nn_modules and justknobs_check( + "pytorch/compiler:guard_nn_modules" + ) + + for guard in sorted_guards: + if ( + not guard_on_nn_modules + and guard.is_specialized_nn_module() + # Default func args must be guarded on. + # TODO: we could make use of 'DefaultsSource' and offer a .guard.is_defaults() API + and "__defaults__" not in guard.name + and "__kwdefaults__" not in guard.name + and (config.skip_nnmodule_hook_guards or "hooks" not in guard.name) + ): + continue + + guard.create(builder) + return builder, guard_manager + + def compile_check_fn( + self, + builder: GuardBuilder, + guards_out: list[Guard], + guard_fail_fn: Callable[[GuardFail], None] | None, + ) -> None: + # see parallel handling of ".0" / "___implicit0" in _eval_frame.c + largs = builder.argnames + largs += ["**___kwargs_ignored"] + + guards_log.debug("GUARDS:") + + # pyrefly: ignore [implicit-any] + code_parts = [] + verbose_code_parts = [] + structured_guard_fns: list[Callable[[], dict[str, Any]]] = [] + + # Add compile id info in the guard manager for debugging purpose + self.guard_manager.root.attach_compile_id( + str(CompileContext.current_compile_id()) + ) + + # Clear references to torch_function modes held in the list + self.torch_function_mode_stack = None + + def add_code_part( + code_part: str, guard: Guard | None, log_only: bool = False + ) -> None: + verbose_code_part = get_verbose_code_part(code_part, guard) + guards_log.debug("%s", verbose_code_part) + + structured_guard_fns.append( + lambda: { + "code": code_part, + "stack": ( + structured.from_traceback(guard.stack.summary()) + if guard and guard.stack + else None + ), + "user_stack": ( + structured.from_traceback(guard.user_stack) + if guard and guard.user_stack + else None + ), + } + ) + + if verbose_guards_log.isEnabledFor(logging.DEBUG): + maybe_stack = "" + maybe_user_stack = "" + if guard is not None: + if guard.stack: + maybe_stack = f"\nStack:\n{''.join(guard.stack.format())}" + if guard.user_stack: + maybe_user_stack = ( + f"\nUser stack:\n{''.join(guard.user_stack.format())}" + ) + verbose_guards_log.debug( + "Guard: %s%s%s", + code_part, + maybe_stack, + maybe_user_stack, + ) + + if not log_only: + code_parts.append(code_part) + verbose_code_parts.append(verbose_code_part) + + seen = set() + for gcl in builder.code: + for code in gcl.code_list: + if code not in seen: + # If Cpp guard manager is enabled, we don't need to add to + # code_parts. + add_code_part(code, gcl.guard, True) + seen.add(code) + + no_tensor_aliasing_names = builder.no_tensor_aliasing_names + check_tensors_fn = None + check_tensors_verbose_fn = None + + if len(no_tensor_aliasing_names) > 1: + # Install tensor aliasing guard. TENSOR_MATCH guards are already + # installed for cpp guard manager. + install_no_tensor_aliasing_guard( + builder.no_tensor_aliasing_guard_managers, + no_tensor_aliasing_names, + ["check_no_aliasing(" + ", ".join(no_tensor_aliasing_names) + ")"], + None, + ) + + # Note - On Lambda guarding of object aliasing + # We previously installed object-aliasing guards as relational guards, + # but that undermined the recursive-dict guard optimization: placing the + # aliasing guard at a leaf prevented the parent dict node from + # qualifying as a recursive-dict guard root. Because aliasing guards are + # rare, we now emit them as epilogue guards via a small Python lambda. + # This repeats the access in Python—adding a bit of work—but the + # overhead is outweighed by the gains from enabling recursive-dict guard + # optimization. + if ( + config.use_lamba_guard_for_object_aliasing + and builder.object_aliasing_guard_codes + ): + aliasing_code_parts, aliasing_verbose_code_parts = map( + list, zip(*builder.object_aliasing_guard_codes) + ) + builder.add_python_lambda_leaf_guard_to_root( + aliasing_code_parts, aliasing_verbose_code_parts + ) + + aotautograd_guards: list[GuardEnvExpr] = ( + self.output_graph.aotautograd_guards if self.output_graph else [] + ) + + # TODO(anijain2305) - There is a duplicate logic in Dynamo to find + # aliased input tensors. So most probably we don't need this here. + # Revisit. + for guard in aotautograd_guards: + if isinstance(guard, DuplicateInputs): + source_a = guard.input_source_a + source_b = guard.input_source_b + code_part = f"{source_a.name} is {source_b.name}" + install_object_aliasing_guard( + builder.get_guard_manager_from_source(source_a), + builder.get_guard_manager_from_source(source_b), + [code_part], + None, + ) + add_code_part(code_part, None, True) + elif isinstance(guard, StorageOverlap): + overlapping_guard_managers = [ + builder.get_guard_manager_from_source(s) + for s in guard.overlapping_sources + ] + non_overlapping_guard_managers = [ + builder.get_guard_manager_from_source(s) + for s in guard.non_overlapping_sources + ] + code_part = ( + """check_overlapping(""" + f"""overlapping=[{", ".join(s.name for s in guard.overlapping_sources)}], """ + f"""non_overlapping=[{", ".join(s.name for s in guard.non_overlapping_sources)}])""" + ) + install_storage_overlapping_guard( + overlapping_guard_managers, + non_overlapping_guard_managers, + [code_part], + None, + ) + add_code_part(code_part, None, True) + else: + raise RuntimeError(f"Unknown GuardEnvExpr: {guard}") + + # TODO: the "guard" here is actually just the top level SHAPE_ENV + # which is useless. Get ShapeEnv to pass in more provenance. + for gcl in builder.shape_env_code: + for code in gcl.code_list: + # Shape env guards are already added for CPP guard manager in + # SHAPE_ENV implementation. + add_code_part(code, gcl.guard, True) + + # OK, all done generating guards + if structured_guard_fns: + torch._logging.trace_structured( + "dynamo_guards", payload_fn=lambda: [f() for f in structured_guard_fns] + ) + + if convert_frame.initial_global_state is None: + # we should only hit this case in NopTests() + check_global_state = convert_frame.GlobalStateGuard().check + else: + check_global_state = getattr(self.global_state, "check", None) + closure_vars = { + "___check_tensors": check_tensors_fn, + "___check_tensors_verbose": check_tensors_verbose_fn, + "___check_global_state": check_global_state, + "___check_torch_function_mode_stack": self.torch_function_mode_stack_check_fn, + **SYMPY_INTERP, + **_get_closure_vars(), + } + + self.guard_manager.finalize() + + globals_for_guard_fn = {"G": builder.scope["G"]} + # Guard manager construction is complete. Ensure we did not miss to + # insert a guard in cpp guard manager. + assert len(code_parts) == 0 + + self.guard_manager.closure_vars = closure_vars + self.guard_manager.args = largs + self.guard_manager.populate_code_parts_for_debugging() + self.guard_manager.verbose_code_parts = verbose_code_parts + # Grab only G, but preserve "G" because guards access it as "G" + self.guard_manager.global_scope = globals_for_guard_fn + self.guard_manager.guard_fail_fn = guard_fail_fn + # will be populated by a non-owning reference to CacheEntry/ExtraState + # when the CacheEntry is constructed + self.guard_manager.cache_entry = None + self.guard_manager.extra_state = None + self.guard_manager.no_tensor_aliasing_sources = no_tensor_aliasing_names + + def invalidate(self, obj_str: str) -> None: + # Some tests reveal that CheckFunctionManager has no attribute + # guard_manager, but this case should not be of any concern. + # This case doesn't seem easy to repro. + if ( + hasattr(self, "guard_manager") + and not isinstance(self.guard_manager, DeletedGuardManagerWrapper) + and (cache_entry := self.guard_manager.cache_entry) is not None + and (extra_state := self.guard_manager.extra_state) is not None + ): + assert isinstance(cache_entry, CacheEntry) + + assert isinstance(extra_state, ExtraState) + reason = f"Cache line invalidated because {obj_str} got deallocated" + deleted_guard_manager = DeletedGuardManagerWrapper(reason) + + extra_state.invalidate(cache_entry, deleted_guard_manager) + self.guard_manager = deleted_guard_manager + + def id_ref(self, obj: object, obj_str: str) -> int: + """add a weakref, return the id""" + try: + if id(obj) not in self._weakrefs: + # We will clear the _weakrefs dict at the end of __init__ + # function, which will delete the callbacks as well. Therefore, + # we are using a finalizer which is kept alive. + self._weakrefs[id(obj)] = weakref.ref(obj) + weakref.finalize( + obj, functools.partial(self.invalidate, obj_str=obj_str) + ) + except TypeError: + pass # cannot weakref bool object + return id(obj) + + def lookup_weakrefs(self, obj: object) -> weakref.ref[object] | None: + """Lookup the _weakrefs created in id_ref function for ID_MATCH'd objects""" + if id(obj) in self._weakrefs: + return self._weakrefs[id(obj)] + return None + + +def build_guard_function(code_parts: list[str], closure_args: str) -> tuple[str, str]: + from torch._inductor.utils import IndentedBuffer + + csepass = PyExprCSEPass() + try: + csepass.count(code_parts) + + def replace(expr: str) -> tuple[list[str], str]: + return csepass.replace(expr) + + except RecursionError: + # If we hit recursion limits during CSE analysis, fall back to a no-op replace function + # This can happen with extremely complex guard expressions + def replace(expr: str) -> tuple[list[str], str]: + return [], expr + + # Generate the inner body of the guard function. + # i.e. if-chain of the guard expressions. + guard_body = IndentedBuffer() + for expr in code_parts: + preface, expr = replace(expr) + guard_body.writelines(preface) + guard_body.writeline(f"if not ({expr}):") + with guard_body.indent(): + guard_body.writeline("return False") + + # Wrap the inner body into the actual guard function. + guard = IndentedBuffer() + guard.writeline("def guard(L):") + with guard.indent(): + guard.splice(guard_body) + guard.writeline("return True") + + # Wrap the whole guard function into another function + # with the closure variables. + make_guard_fn = IndentedBuffer() + make_guard_fn.writeline(f"def ___make_guard_fn({closure_args}):") + with make_guard_fn.indent(): + make_guard_fn.splice(guard) + make_guard_fn.writeline("return guard") + + return guard_body.getvalue(), make_guard_fn.getvalue() + + +def is_recompiles_enabled() -> bool: + return torch._logging._internal.log_state.is_artifact_enabled("recompiles") + + +def is_recompiles_verbose_enabled() -> bool: + return torch._logging._internal.log_state.is_artifact_enabled("recompiles_verbose") + + +# this will only be used if cpp guards are disabled +def make_torch_function_mode_stack_guard( + initial_stack: list[torch.overrides.TorchFunctionMode], +) -> Callable[[], bool]: + types = [type(x) for x in initial_stack] + + def check_torch_function_mode_stack() -> bool: + cur_stack = get_torch_function_mode_stack() + + if len(cur_stack) != len(types): + return False + + for ty, mode in zip(types, cur_stack): + if ty is not type(mode): + return False + + return True + + return check_torch_function_mode_stack + + +Scope = TypeAliasType("Scope", dict[str, object]) + + +def recompilation_reason_for_no_tensor_aliasing_guard( + guard_manager: GuardManagerWrapper, scope: Scope +) -> list[str]: + assert guard_manager.global_scope is not None + global_scope = dict(guard_manager.global_scope) + ids_to_source = collections.defaultdict(list) + for tensor_source in guard_manager.no_tensor_aliasing_sources: + global_scope["__compile_source__"] = tensor_source + tensor_id = id(eval(tensor_source, global_scope, scope)) + ids_to_source[tensor_id].append(tensor_source) + + duplicate_tensors = [ + f"{ids_to_source[key]}" for key in ids_to_source if len(ids_to_source[key]) > 1 + ] + + reason = ", ".join(duplicate_tensors) + return [f"Duplicate tensors found: {reason}"] + + +def strip_local_scope(s: str) -> str: + """ + Replace occurrences of L[...] with just the inner content. + Handles both single and double quotes. + + This is to generate user friendly recompilation messages. + """ + import re + + pattern = r"L\[\s*['\"](.*?)['\"]\s*\]" + return re.sub(pattern, r"\1", s) + + +def format_user_stack_trace( + user_stack: traceback.StackSummary | None, +) -> str: + """ + Format the user stack trace for display in guard failure messages. + + Returns a formatted string representation of the stack trace, + or an empty string if no user stack is available. + """ + if user_stack is None or len(user_stack) == 0: + return "" + + lines: list[str] = [] + for frame in user_stack: + filename = frame.filename + lineno = frame.lineno + name = frame.name + source_line = frame.line.strip() if frame.line else "" + lines.append(f' File "{filename}", line {lineno}, in {name}') + if source_line: + lines.append(f" {source_line}") + return "\n".join(lines) + + +def get_guard_fail_reason_helper( + guard_manager: GuardManagerWrapper, + f_locals: dict[str, object], + compile_id: CompileId | None, + # pyrefly: ignore [implicit-any] + backend: Callable | None, +) -> str: + """ + Return the reason why `guard_manager` failed. + Updates `guard_failures` with the generated reason. + Only the first failed check of guard_manager is reported. + """ + + assert guard_manager.global_scope is not None + assert guard_manager.closure_vars is not None + scope = {"L": f_locals, "G": guard_manager.global_scope["G"]} + scope.update(guard_manager.closure_vars) + reasons: list[str] = [] + + cache_entry_backend = None + if guard_manager.cache_entry: + cache_entry_backend = guard_manager.cache_entry.backend + + no_tensor_aliasing_check_failed = False + + verbose_code_parts: list[str] = [] + guard_debug_info = guard_manager.check_verbose(f_locals) + user_stack_str = "" + + # For test_export_with_map_cond, the check_verbose fail even without the + # C++ guard manager. We need to fix the issue to remove the comment. + # assert not guard_debug_info.result + if not guard_debug_info.result: + verbose_code_parts = guard_debug_info.verbose_code_parts + # verbose_code_parts is either the actual reason (e.g. in case of + # TENSOR_MATCH) or it could be a list of verbose_code_part that we + # passed to the leaf guard at construction time. If its a list, we + # walk through this list and find the guard that failed. This is + # very important for symbolic shape guards which are currently + # installed as a lambda guard and can encompass a long list of code_parts. + + if len(verbose_code_parts) == 1: + if "Duplicate tensor found" in verbose_code_parts[0]: + no_tensor_aliasing_check_failed = True + else: + reasons = verbose_code_parts + verbose_code_parts = [] + + # Format user stack trace if available and recompile logging is enabled + if guard_debug_info.user_stack: + user_stack_str = format_user_stack_trace(guard_debug_info.user_stack) + elif cache_entry_backend != backend: + # None of the guard entries failed - a backend match issue + reason = ( + "BACKEND_MATCH failure: torch.compile detected different backend callables." + " If this is unexpected, wrap your backend in functools.partial (or reuse the" + " same cached backend) to avoid creating a new backend function each time." + " More details: https://github.com/pytorch/pytorch/issues/168373" + ) + reasons.append(reason) + else: + # Unexpected recompilation - points to a bug + reason = ( + "Unexpected recompilation: runtime guards failed even though they passed" + " during recompilation-reason analysis." + " Please open an issue with a minimal repro:" + " https://github.com/pytorch/pytorch" + ) + reasons.append(reason) + + if no_tensor_aliasing_check_failed: + reasons = recompilation_reason_for_no_tensor_aliasing_guard( + guard_manager, scope + ) + else: + for part in verbose_code_parts: + global_scope = dict(guard_manager.global_scope) + global_scope["__compile_source__"] = part + with report_compile_source_on_error(): + try: + fail_reason = eval(part, global_scope, scope) + except Exception: + if is_recompiles_verbose_enabled(): + continue + else: + raise + # Only ___check_tensors knows how to return a fancy fail reason; + # for everything else we just report the code that failed + + if isinstance(fail_reason, bool) and not fail_reason: + fail_reason = part + if isinstance(fail_reason, str): + reasons.append(fail_reason) + if not is_recompiles_verbose_enabled(): + break + + # Build reason string - simple format for normal logging + # Use singular "reason" when there's only one, plural "reasons" for multiple + if len(reasons) == 1: + reason_str = f"{compile_id}: {reasons[0]}" + else: + reason_str = f"{compile_id}: " + "; ".join(reasons) + if user_stack_str: + reason_str += f"\nUser stack trace:\n{user_stack_str}" + return strip_local_scope(reason_str) + + +def get_guard_fail_reason( + guard_manager: GuardManagerWrapper, + code: types.CodeType, + f_locals: dict[str, object], + compile_id: CompileId, + # pyrefly: ignore [implicit-any] + backend: Callable, + skip_logging: bool = False, +) -> str: + if isinstance(guard_manager, DeletedGuardManagerWrapper): + return f"{compile_id}: {guard_manager.invalidation_reason}" + reason_str = get_guard_fail_reason_helper( + guard_manager, f_locals, compile_id, backend + ) + if skip_logging: + return reason_str + guard_failures[orig_code_map[code]].append(reason_str) + + try: + if guard_manager.guard_fail_fn is not None: + guard_manager.guard_fail_fn( + GuardFail(reason_str or "unknown reason", orig_code_map[code]) + ) + except Exception: + log.exception( + "Failure in guard_fail_fn callback - raising here will cause a NULL Error on guard eval", + ) + + return reason_str + + +def get_and_maybe_log_recompilation_reasons( + cache_entry: CacheEntry | None, + frame: DynamoFrameType, + # pyrefly: ignore [implicit-any] + backend: Callable, + skip_logging: bool = False, +) -> list[str]: + """ + Return the list of guard failure reasons using cache_entry. + Logs the recompilation reason if `recompiles` logging is enabled. + Raises a RecompileError if `config.error_on_recompile` is enabled. + """ + # pyrefly: ignore [implicit-any] + reasons = [] + while cache_entry is not None: + reason = get_guard_fail_reason( + cache_entry.guard_manager, + cache_entry.code, + frame.f_locals, + cache_entry.compile_id, + backend, + skip_logging, + ) + if reason: + reasons.append(reason) + cache_entry = cache_entry.next + + code = frame.f_code + + if skip_logging: + return reasons + # at least one of "recompiles" or "recompiles_verbose" is enabled + do_recompiles_log = is_recompiles_enabled() or is_recompiles_verbose_enabled() + + if do_recompiles_log or config.error_on_recompile: + if is_recompiles_verbose_enabled(): + failures = "\n\n".join( + f"guard {i} failures:\n" + textwrap.indent(reason, "- ") + for i, reason in enumerate(reasons) + ) + else: + failures = textwrap.indent("\n".join(reasons), "- ") + guard_failure_details = ( + f"triggered by the following guard failure(s):\n{failures}" + ) + message = ( + f"Recompiling function {code.co_name} in {code.co_filename}:{code.co_firstlineno}\n" + f"{textwrap.indent(guard_failure_details, ' ')}" + ) + if do_recompiles_log: + if is_recompiles_verbose_enabled(): + recompiles_verbose_log.debug(message) + else: + recompiles_log.debug(message) + if config.error_on_recompile: + raise exc.RecompileError(message) + + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "recompile_reasons", + "encoding": "json", + }, + payload_fn=lambda: reasons[0] if len(reasons) == 1 else reasons, + ) + + return reasons + + +def update_diff_guard_managers_for_existing_cache_entries( + cache_entry: CacheEntry | None, +) -> OrderedSet[str]: + first_cache_entry = cache_entry + + # On the first pass, go through the cache entries and accumulate the diff + # guard sources. Different guard managers can fail with different sources. + # So, we collect all of them first. + acc_diff_guard_sources: OrderedSet[str] = OrderedSet() + while cache_entry is not None: + acc_diff_guard_sources.update( + cache_entry.guard_manager.collect_diff_guard_sources() + ) + cache_entry = cache_entry.next # type: ignore[assignment] + + # On the second pass, set the diff_guard_sources for each cache line to the + # accumulated value. And the re-populate the diff guard manager. + cache_entry = first_cache_entry + while cache_entry is not None: + cache_entry.guard_manager.diff_guard_sources = acc_diff_guard_sources + cache_entry.guard_manager.populate_diff_guard_manager() + cache_entry = cache_entry.next # type: ignore[assignment] + + # return the accumulated sources to set up the new cache line. + return acc_diff_guard_sources + + +def guard_error_hook( + guard_manager: GuardFn, + code: types.CodeType, + f_locals: dict[str, object], + index: int, + last: bool, +) -> None: + print( + f"ERROR RUNNING GUARDS {code.co_name} {code.co_filename}:{code.co_firstlineno}" + ) + print("lambda " + ", ".join(guard_manager.args) + ":") + print(" ", " and\n ".join(guard_manager.code_parts)) + + print(guard_manager) + + local_scope = {"L": f_locals, **guard_manager.closure_vars} + for guard in guard_manager.code_parts: + try: + eval(guard, guard_manager.global_scope, local_scope) + except: # noqa: B001,E722 + print(f"Malformed guard:\n{guard}") + + +set_guard_error_hook(guard_error_hook) + + +def unique(seq: Sequence[T]) -> Generator[T, None, None]: + seen = set() + for x in seq: + if x not in seen: + yield x + seen.add(x) + + +def make_dupe_guard( + obj_source: Source, dupe_source: Source | None +) -> functools.partial[Any] | None: + # Note - we may end up in a situation where we invoke something like + # def fn(x, y) + # with fn(x, x) + # Prior to the addition of tracking to all relevant objects, we would handle this just fine by + # eagerly re-entering VB and rewrapping inputs, correctly creating graphargs and placeholders. However, + # with tracking on inputs, duplicate inputs or aliased relationships may end up getting erased here - + # In the fn(x, x) example call above look like a graph with a single input. + # In order to ensure that we do not reuse fn(x, x) for fn(x, y), we create a duplicate input guard. + + # Note - we may not have a source, that is fine, it just means we had an object that is safe to have + # leave unsourced - like a local list created and discharged entirely within a local scope. + if dupe_source and dupe_source != obj_source: + ser_source_is_local = is_from_local_source(dupe_source) + source_is_local = is_from_local_source(obj_source) + if is_from_flatten_script_object_source( + dupe_source + ) or is_from_flatten_script_object_source(obj_source): + raise exc.UnsafeScriptObjectError( + f"{obj_source.name} is aliasing {dupe_source.name}. This is not supported." + f" Please do a clone for corresponding input." + ) + + # Note - both must be local, or global, or we will run afoul of a lack of merging in how we currently + # reconcile guards builder scopes in compile_check_fn. This technically means we miss a guard here, + # so maybe we should do this refactor before we land this... + # TODO(voz): Combine local and global guard builders. + if ser_source_is_local == source_is_local: + # Note - this is a little aggressive - these being duplicate input does not always matter. + # However, this should always be a sound guard to add here. + return functools.partial(GuardBuilder.DUPLICATE_INPUT, source_b=dupe_source) + return None + + +def install_guard(*guards: Guard, skip: int = 0) -> None: + """ + Add dynamo guards to the current tracing context. + + Args: + guards: guard(s) to add + skip: number of stack frames to ignore for debug stack trace + """ + from torch._guards import TracingContext + + guards_context = TracingContext.get().guards_context + if guards_context.skip_install: + return + + collect_debug_stack = guards_log.isEnabledFor( + logging.DEBUG + ) or verbose_guards_log.isEnabledFor(logging.DEBUG) + add = guards_context.dynamo_guards.add + for guard in guards: + assert isinstance(guard, Guard) + if is_from_skip_guard_source(guard.originating_source): + continue + add(guard, collect_debug_stack=collect_debug_stack, skip=skip + 1) + + +# --------------------------------------------------------------------------- +# Guard check spec dispatch table — built by scanning GuardBuilder methods +# for the guard_check_spec attribute set by the decorators above. +GUARD_VALUE_DISPATCH: dict[str, GuardCheckSpec | object] = { + name: handler + for name in dir(GuardBuilder) + if (handler := getattr(getattr(GuardBuilder, name), "guard_check_spec", None)) + is not None +} + +# Validate that every guard method has an explicit guard_check_spec. +# Guard methods are identified by: uppercase name, callable, and first +# non-self parameter is 'guard'. +MISSING_GUARD_SPECS = [ + _name + for _name in dir(GuardBuilder) + if _name[0].isupper() + and not _name.startswith("_") + and callable(_method := getattr(GuardBuilder, _name, None)) + and len(_params := list(inspect.signature(_method).parameters)) >= 2 + and _params[1] == "guard" + and not hasattr(_method, "guard_check_spec") +] +if MISSING_GUARD_SPECS: + raise RuntimeError( + f"GuardBuilder methods missing guard_check_spec decorator: " + f"{', '.join(MISSING_GUARD_SPECS)}. Use @register_guard_check_spec, " + f"@skip_guard_check_spec, or @unsupported_guard_check_spec." + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/hooks.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/hooks.py new file mode 100644 index 0000000000000000000000000000000000000000..a3074f5916592807c9d7d503f310e61a9868d5b4 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/hooks.py @@ -0,0 +1,26 @@ +"""Hook system for Dynamo's guard functionality. + +This module provides a way to register callback functions that are triggered during +guard-related operations. + +The Hooks class manages two types of hook functions: +- guard_export_fn: Called when guards need to be exported, taking a GuardsSet as input +- guard_fail_fn: Called when a guard check fails, taking a GuardFail object as input +These hooks enable customization of guard export and failure handling behaviors. +""" + +import dataclasses +from collections.abc import Callable, Sequence + +from torch._guards import GuardsSet + +from .types import GuardFail, GuardFilterEntry + + +@dataclasses.dataclass +class Hooks: + guard_export_fn: Callable[[GuardsSet], None] | None = None + guard_fail_fn: Callable[[GuardFail], None] | None = None + guard_filter_fn: Callable[[Sequence[GuardFilterEntry]], Sequence[bool]] | None = ( + None + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/logging.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/logging.py new file mode 100644 index 0000000000000000000000000000000000000000..74862962adaa10f294408fd8dbe172dcdc71d743 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/logging.py @@ -0,0 +1,73 @@ +"""Logging utilities for Dynamo and Inductor. + +This module provides specialized logging functionality including: +- Step-based logging that prepends step numbers to log messages +- Progress bar management for compilation phases +- Centralized logger management for Dynamo and Inductor components + +The logging system helps track the progress of compilation phases and provides structured +logging output for debugging and monitoring. +""" + +import itertools +import logging +from collections.abc import Callable +from typing import Any + +from torch.hub import _Faketqdm, tqdm + + +# Disable progress bar by default, not in dynamo config because otherwise get a circular import +disable_progress = True + + +# Return all loggers that torchdynamo/torchinductor is responsible for +def get_loggers() -> list[logging.Logger]: + return [ + logging.getLogger("torch.fx.experimental.symbolic_shapes"), + logging.getLogger("torch._dynamo"), + logging.getLogger("torch._inductor"), + ] + + +# Creates a logging function that logs a message with a step # prepended. +# get_step_logger should be lazily called (i.e. at runtime, not at module-load time) +# so that step numbers are initialized properly. e.g.: + +# @functools.cache +# def _step_logger(): +# return get_step_logger(logging.getLogger(...)) + +# def fn(): +# _step_logger()(logging.INFO, "msg") + +_step_counter = itertools.count(1) + +# Update num_steps if more phases are added: Dynamo, AOT, Backend +# This is very inductor centric +# _inductor.utils.has_triton() gives a circular import error here + +if not disable_progress: + try: + import triton # noqa: F401 + + num_steps = 3 + except ImportError: + num_steps = 2 + pbar = tqdm(total=num_steps, desc="torch.compile()", delay=0) + + +def get_step_logger(logger: logging.Logger) -> Callable[..., None]: + if not disable_progress: + pbar.update(1) + if not isinstance(pbar, _Faketqdm): + pbar.set_postfix_str(f"{logger.name}") + + step = next(_step_counter) + + def log(level: int, msg: str, **kwargs: Any) -> None: + if "stacklevel" not in kwargs: + kwargs["stacklevel"] = 2 + logger.log(level, "Step %s: %s", step, msg, **kwargs) + + return log diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/metrics_context.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/metrics_context.py new file mode 100644 index 0000000000000000000000000000000000000000..41ca9a94a1e67acfc43635ea5863b8e175e6847f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/metrics_context.py @@ -0,0 +1,251 @@ +"""Metrics collection and management system for Dynamo. + +This module provides context managers for gathering and reporting metrics during +compilation and runtime. + +It includes two main components: +- MetricsContext: A context manager for collecting metrics during compilation, supporting + nested contexts and various metric types (counters, sets, key-value pairs) +- RuntimeMetricsContext: A specialized context for runtime metrics collection that doesn't + require explicit context management + +The metrics system enables comprehensive monitoring and analysis of both compilation and +execution performance. +""" + +from __future__ import annotations + +import heapq +import logging +import time +from collections.abc import Callable +from typing import Any, TYPE_CHECKING, TypeAlias +from typing_extensions import Self + + +if TYPE_CHECKING: + from collections.abc import Iterator + +from torch.utils._traceback import CapturedTraceback + + +log = logging.getLogger(__name__) + + +class TopN: + """ + Helper to record a list of metrics, keeping only the top N "most expensive" elements. + """ + + def __init__(self, at_most: int = 25) -> None: + self.at_most = at_most + self.heap: list[tuple[int, Any]] = [] + + def add(self, key: Any, val: int) -> None: + # Push if we haven't reached the max size, else push and pop the smallest + fn = heapq.heappush if len(self.heap) < self.at_most else heapq.heappushpop + fn(self.heap, (val, key)) + + def __len__(self) -> int: + return len(self.heap) + + def __iter__(self) -> Iterator[tuple[Any, int]]: + return ((key, val) for val, key in sorted(self.heap, reverse=True)) + + +OnExitType: TypeAlias = Callable[ + [int, int, dict[str, Any], type[BaseException] | None, BaseException | None], + None, +] + + +class MetricsContext: + def __init__(self, on_exit: OnExitType) -> None: + """ + Use this class as a contextmanager to create a context under which to accumulate + a set of metrics, e.g., metrics gathered during a compilation. On exit of the + contextmanager, call the provided 'on_exit' function and pass a dictionary of + all metrics set during the lifetime of the contextmanager. + """ + self._on_exit = on_exit + self._metrics: dict[str, Any] = {} + self._start_time_ns: int = 0 + self._level: int = 0 + self._edits: list[tuple[CapturedTraceback, set[str]]] = [] + + def __enter__(self) -> Self: + """ + Initialize metrics recording. + """ + if self._level == 0: + # In case of recursion, track at the outermost context. + self._metrics = {} + self._start_time_ns = time.time_ns() + + self._level += 1 + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + _traceback: Any, + ) -> None: + """ + At exit, call the provided on_exit function. + """ + self._level -= 1 + assert self._level >= 0 + if self._level == 0: + try: + end_time_ns = time.time_ns() + self._on_exit( + self._start_time_ns, end_time_ns, self._metrics, exc_type, exc_value + ) + except Exception: + log.exception("Unexpected exception logging compilation metrics") + + def in_progress(self) -> bool: + """ + True if we've entered the context. + """ + return self._level > 0 + + def increment(self, metric: str, value: int) -> None: + """ + Increment a metric by a given amount. + """ + if self._level == 0: + raise RuntimeError(f"Cannot increment {metric} outside of a MetricsContext") + if metric not in self._metrics: + self._metrics[metric] = 0 + self._metrics[metric] += value + + def _render_edits(self, pred: set[str]) -> str: + return "\n\n" + "\n\n".join( + "Previous Traceback:\n" + "".join(e.format()) + for e, k in self._edits + if k & pred + ) + + def set(self, metric: str, value: Any, overwrite: bool = False) -> None: + """ + Set a metric to a given value. Raises if the metric has been assigned previously + in the current context. + """ + if self._level == 0: + raise RuntimeError(f"Cannot set {metric} outside of a MetricsContext") + if metric in self._metrics and not overwrite: + raise RuntimeError( + self._render_edits({metric}) + + f"\n\nRuntimeError: Metric '{metric}' has already been set in the current context " + "(see above for current and previous traceback)." + ) + self._edits.append((CapturedTraceback.extract(skip=1), {metric})) + self._metrics[metric] = value + + def set_key_value(self, metric: str, key: str, value: Any) -> None: + """ + Treats a give metric as a dictionary and set the k and value within it. + Note that the metric must be a dictionary or not present. + + We allow this to be called multiple times (i.e. for features, it's not uncommon + for them to be used multiple times within a single compilation). + """ + if self._level == 0: + raise RuntimeError(f"Cannot set {metric} outside of a MetricsContext") + if metric not in self._metrics: + self._metrics[metric] = {} + self._metrics[metric][key] = value + + def update(self, values: dict[str, Any], overwrite: bool = False) -> None: + """ + Set multiple metrics directly. This method does NOT increment. Raises if any + metric has been assigned previously in the current context and overwrite is + not set to True. + """ + if self._level == 0: + raise RuntimeError("Cannot update metrics outside of a MetricsContext") + existing = self._metrics.keys() & values.keys() + if existing and not overwrite: + raise RuntimeError( + self._render_edits(set(values.keys())) + + f"\n\nRuntimeError: Metric(s) {existing} have already been set in the current context. " + "(see above for current and previous traceback)." + ) + self._edits.append((CapturedTraceback.extract(skip=1), set(values.keys()))) + self._metrics.update(values) + + def update_outer(self, values: dict[str, Any]) -> None: + """ + Update, but only when at the outermost context. + """ + if self._level == 0: + raise RuntimeError("Cannot update metrics outside of a MetricsContext") + if self._level == 1: + self.update(values) + + def add_to_set(self, metric: str, value: Any) -> None: + """ + Records a metric as a set() of values. + """ + if self._level == 0: + raise RuntimeError(f"Cannot add {metric} outside of a MetricsContext") + if metric not in self._metrics: + self._metrics[metric] = set() + self._metrics[metric].add(value) + + def add_top_n(self, metric: str, key: Any, val: int) -> None: + """ + Records a metric as a TopN set of values. + """ + if self._level == 0: + return + if metric not in self._metrics: + self._metrics[metric] = TopN() + self._metrics[metric].add(key, val) + + +class RuntimeMetricsContext: + def __init__(self, on_exit: OnExitType) -> None: + """ + Similar to MetricsContext, but used to gather the runtime metrics that are + decoupled from compilation, where there's not a natural place to insert a + context manager. + """ + self._on_exit = on_exit + self._metrics: dict[str, Any] = {} + self._start_time_ns: int = 0 + + def increment( + self, metric: str, value: int, extra: dict[str, Any] | None = None + ) -> None: + """ + Increment a metric by a given amount. + """ + if not self._metrics: + # Start timing on the first entry + self._start_time_ns = time.time_ns() + if metric not in self._metrics: + self._metrics[metric] = 0 + self._metrics[metric] += value + + if extra: + for k, v in extra.items(): + if k not in self._metrics and v is not None: + self._metrics[k] = v + + def finish(self) -> None: + """ + Call the on_exit function with the metrics gathered so far and reset. + """ + if self._metrics: + try: + end_time_ns = time.time_ns() + self._on_exit( + self._start_time_ns, end_time_ns, self._metrics, None, None + ) + except Exception: + log.exception("Unexpected exception logging runtime metrics") + finally: + self._metrics = {} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/mutation_guard.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/mutation_guard.py new file mode 100644 index 0000000000000000000000000000000000000000..70f2d7dfc56fa57f180ac7c3169c901af343baba --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/mutation_guard.py @@ -0,0 +1,158 @@ +"""Mutation tracking and dynamic module detection system for Dynamo. + +This module provides mechanisms to track and respond to mutations in PyTorch modules +and detect dynamically created or modified modules. + +Key components: +- MutationTracker: Tracks mutations to objects and invalidates associated cached code +- GenerationTracker: Tracks module creation timing to identify dynamic instances +- Patching system for nn.Module to detect mutations and dynamic creation + +The system ensures that Dynamo's optimizations remain valid by detecting and responding +to runtime changes in module state and structure. +""" + +import functools +import weakref +from collections.abc import MutableMapping +from typing import Any + +import torch.nn +from torch.nn import Module + +from . import config +from .utils import ExactWeakKeyDictionary, nn_module_has_global_hooks + + +unpatched_nn_module_init = torch.nn.Module.__init__ + + +class MutationTracker: + db: ExactWeakKeyDictionary = ExactWeakKeyDictionary() + + def __init__(self) -> None: + self.mutation_count: int = 0 + self.watchers: list[weakref.ReferenceType[Any]] = [] + + def on_mutation(self, name: str) -> None: + self.mutation_count += 1 + tmp = self.watchers + self.watchers = [] + for ref in tmp: + guarded = ref() + if guarded is not None: + guarded.invalidate(ref) + + def track(self, guarded_code: Any) -> None: + self.watchers.append(weakref.ref(guarded_code)) + + +def watch(obj: Any, guarded_code: Any) -> None: + """invalidate guarded_code when obj is mutated""" + ensure_patched(type(obj)) + + if obj not in MutationTracker.db: + MutationTracker.db[obj] = MutationTracker() + tracker = MutationTracker.db[obj] + tracker.track(guarded_code) + + +def ensure_patched(cls: Any) -> None: + if getattr(cls, "___needs_mutation_patch", True): + cls.___needs_mutation_patch = False + original_setattr = cls.__setattr__ + + @functools.wraps(original_setattr) + def custom_setattr(self: Any, key: str, value: Any) -> None: + try: + MutationTracker.db[self].on_mutation(key) + except KeyError: + pass + return original_setattr(self, key, value) + + cls.__setattr__ = custom_setattr + + +class GenerationTracker: + generation: int = 0 + dynamic_classes: ExactWeakKeyDictionary = ExactWeakKeyDictionary() + generation_values: ExactWeakKeyDictionary = ExactWeakKeyDictionary() + + @classmethod + def tag(cls, obj: Any) -> None: + cls.generation_values[obj] = cls.generation + + @staticmethod + def mark_class_dynamic(cls: type[torch.nn.Module]) -> None: + assert issubclass(cls, torch.nn.Module) + GenerationTracker.dynamic_classes[cls] = True + + @classmethod + def get_generation_value(cls, obj: Any) -> int: + if obj not in cls.generation_values: + return -1 + return cls.generation_values[obj] + + @classmethod + def check(cls, obj: Any) -> bool: + return ( + obj in cls.generation_values + and cls.generation_values[obj] == cls.generation + ) + + @classmethod + def clear(cls) -> None: + cls.generation = 0 + cls.dynamic_classes = ExactWeakKeyDictionary() + cls.generation_values = ExactWeakKeyDictionary() + + +def is_dynamic_nn_module(obj: Any, is_export: bool) -> bool: + """Check for nn.Modules() created dynamically or mutated""" + if isinstance(obj, torch.nn.Module) and ( + "forward" in obj.__dict__ or isinstance(obj, (dict, MutableMapping)) + ): + # A monkey patched `.forward` indicates something wacky is going on + # Similarly a nn module also subclassed as a dict is unusual. + return True + if hasattr(obj, "torchdynamo_force_dynamic"): + return obj.torchdynamo_force_dynamic + if isinstance(obj, torch.nn.Module) and ( + not is_export or config.install_free_tensors + ): + return True + + if isinstance(obj, torch.nn.Module) and nn_module_has_global_hooks(): + return True + dyn = GenerationTracker.dynamic_classes.get(type(obj)) or GenerationTracker.check( + obj + ) + return dyn + + +def install_generation_tagging_init() -> None: + """ + Monkey patch torch.nn.Module.__init__ and torch.nn.Module.__setstate__ + so we can detect nn.Module instances created dynamically inside forward methods. + """ + + if getattr(Module, "___needs_generation_tag_patch", True): + init = Module.__init__ + + def patched_init(self: Module, *args: Any, **kwargs: Any) -> None: + init(self, *args, **kwargs) + GenerationTracker.tag(self) + + Module.__init__ = patched_init # type: ignore[method-assign] + + setstate = Module.__setstate__ + + def patched_setstate(self: Module, state: Any) -> None: + setstate(self, state) + GenerationTracker.tag(self) + + Module.__setstate__ = patched_setstate # type: ignore[method-assign] + + Module.___needs_generation_tag_patch = False # type: ignore[attr-defined] + + GenerationTracker.generation += 1 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/output_graph.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/output_graph.py new file mode 100644 index 0000000000000000000000000000000000000000..5d292aaac616fb33fb96ae9c449fbb0a1c800eaa --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/output_graph.py @@ -0,0 +1,4499 @@ +""" +Core graph building functionality for PyTorch's Dynamo system. This module contains +the essential components for constructing and managing FX graphs during compilation: + +- OutputGraph: Manages the overall graph construction and compilation process. It owns + a SubgraphTracer and handles graph compilation, execution, and state management. + OutputGraph also manages features like graph deduplication, symbolic shape handling, + and tracking of side effects. + +- SubgraphTracer: Handles the actual FX graph construction by tracing Python code. + It supports advanced features like higher-order operators through nested tracers, + lifting of free variables, and handling of symbolic shapes. + +The module supports key Dynamo features including: +- Higher-order operators through nested SubgraphTracers +- Graph deduplication for optimization +- Symbolic shape handling and propagation +- Side effect tracking and management +- Guard insertion and management +""" + +import collections +import contextlib +import copy +import dataclasses +import functools +import inspect +import itertools +import logging +import operator +import re +import sys +import time +import traceback +import types +import warnings +import weakref +from collections.abc import Callable, Generator, Sequence +from dataclasses import dataclass, field as dc_field +from types import CodeType +from typing import Any, cast, Optional, TYPE_CHECKING, Union +from typing_extensions import ParamSpec, TypeVar + +import sympy + +import torch._guards +import torch._logging +import torch.distributed as dist +import torch.nn +import torch.utils._pytree as pytree +from torch import fx, Tensor +from torch._C._dynamo import guards +from torch._dynamo.exc import ShortenTraceback, TensorifyScalarRestartAnalysis +from torch._guards import ( + CompileContext, + CompileId, + GlobalContextCheckpointState, + Source, + tracing, + TracingContext, +) +from torch._library.fake_class_registry import FakeScriptObject +from torch._library.opaque_object import is_opaque_type +from torch._subclasses.fake_tensor import FakeTensor +from torch._utils_internal import signpost_event +from torch.export.dynamic_shapes import _ConstraintTarget +from torch.fx._lazy_graph_module import _make_graph_module # type: ignore[attr-defined] +from torch.fx.experimental._backward_state import BackwardState +from torch.fx.experimental.symbolic_shapes import ( + free_symbols, + guard_scalar, + is_symbolic, + ShapeEnv, + Specialization, + uninteresting_files, +) +from torch.fx.node import Target +from torch.fx.passes.runtime_assert import insert_deferred_runtime_asserts +from torch.utils._ordered_set import OrderedSet +from torch.utils._python_dispatch import is_traceable_wrapper_subclass + +from . import config, exc, logging as torchdynamo_logging, variables +from .backends.registry import CompiledFn, CompilerFn +from .bytecode_transformation import ( + create_binary_slice, + create_binary_subscr, + create_build_tuple, + create_call_function, + create_dup_top, + create_instruction, + create_load_const, + create_rot_n, + create_swap, + Instruction, + unique_id, +) +from .code_context import code_context +from .codegen import PyCodegen +from .current_scope_id import enter_new_scope +from .device_interface import get_interface_for_device +from .exc import ( + BackendCompilerFailed, + exceptions_allowed_to_be_fallback, + SkipFrame, + unimplemented, + unimplemented_with_warning, +) +from .graph_bytecode_inputs import has_user_objects, index_to_bytecode_constructor +from .graph_deduplication import apply_graph_deduplication +from .graph_id_filter import ( + get_backend_override_for_compile_id, + get_inductor_config_override_for_compile_id, +) +from .graph_region_tracker import GraphRegionTracker +from .guards import GuardBuilder, install_guard +from .mutation_guard import is_dynamic_nn_module +from .side_effects import AttributeMutationExisting, SideEffects, ValueMutationExisting +from .source import ( + _get_source_debug_name, + AttrSource, + BackwardStateSource, + ConstantSource, + DictGetItemSource, + GetItemSource, + GlobalStateSource, + is_constant_source, + is_from_local_source, + LocalSource, + NumpyTensorSource, + ParamBufferSource, + ShapeEnvSource, + SyntheticLocalSource, + TensorProperty, + TensorPropertySource, +) +from .utils import ( + _extract_tensor_dict, + checkpoint_params, + CleanupHook, + clone_inputs, + compilation_time_metrics, + count_calls, + counters, + dynamo_timed, + get_chromium_event_logger, + get_instruction_source_311, + get_locals_to_steal, + get_static_address_type, + get_unique_name_wrt, + graph_break_reasons, + increment_op_count, + istype, + lazy_format_graph_code, + LazyString, + nn_module_proxy, + same, + set_example_value, +) +from .variables.builder import ( + BackwardStateGraphArg, + GraphArg, + TrackedFake, + wrap_fx_proxy, +) +from .variables.ctx_manager import ContextWrappingVariable +from .variables.functions import ClosureConversionError, VariableTracker +from .variables.lists import BaseListVariable +from .variables.misc import NullVariable +from .variables.nn_module import NNModuleVariable +from .variables.tensor import ( + NumpyNdarrayVariable, + SymNodeVariable, + UnspecializedPythonVariable, +) +from .variables.torch_function import TensorWithTFOverrideVariable +from .variables.user_defined import UserDefinedDictVariable + + +if TYPE_CHECKING: + from torch._dynamo.dynamo_profiler import DynamoProfilerState + from torch._dynamo.package import CompilePackage + from torch._dynamo.symbolic_convert import InstructionTranslatorBase + from torch._inductor import _CudagraphAnnotation + from torch.multiprocessing.reductions import StorageWeakRef + +log = logging.getLogger(__name__) +graph_tabular_log = torch._logging.getArtifactLogger(__name__, "graph") +graph_code_log = torch._logging.getArtifactLogger(__name__, "graph_code") +graph_sizes_log = torch._logging.getArtifactLogger(__name__, "graph_sizes") +trace_call_log = torch._logging.getArtifactLogger(__name__, "trace_call") + +RootGuardManager = guards.RootGuardManager + + +# Capture fn pointer at import time +# This is to guard against trying to mark the iterated tensors +# as static in case user overrides fn ptr +og_module_named_buffers_fn_ptr = torch.nn.Module.named_buffers +og_module_named_parameters_fn_ptr = torch.nn.Module.named_parameters + + +def _wrap_with_inductor_config( + compiler_fn: Any, config_patches: dict[str, Any] +) -> Callable[..., Any]: + """ + Wrap a compiler function to apply inductor config patches during compilation. + + Passes config_patches as a keyword argument so that compile_fx can + propagate them to backward compilation via its inner_compile wrapping. + """ + + def wrapped(gm: Any, example_inputs: Any) -> Any: + return compiler_fn(gm, example_inputs, config_patches=config_patches) + + # Preserve function metadata for logging + wrapped.__name__ = getattr(compiler_fn, "__name__", "") + wrapped.__wrapped__ = compiler_fn # type: ignore[attr-defined] + return wrapped + + +@dataclass(frozen=True) +class AliasingInfo: + has_aliasing: bool + msg: str + + +@dataclass(frozen=True) +class MutationInfo: + has_mutation: bool + msg: str + + +def collect_reachable_grad_fns( + tensors_with_sources: list[tuple[torch.Tensor, str | None]], + stop_at: set[torch.autograd.graph.Node] | None = None, +) -> set[torch.autograd.graph.Node]: + """Collect all grad_fns reachable from tensors' autograd graphs. + + Performs a DFS traversal and collects all visited grad_fns. + Optionally stops traversal nodes in stop_at set. This signals the + autograd.grad boundary. + + Args: + tensors_with_sources: List of (tensor, source_name) tuples to start search from. + stop_at: Optional set of grad_fns where traversal should stop (excluded from result). + + Returns: + Set of all reachable grad_fns. + """ + if stop_at is None: + stop_at = set() + + visited: set[torch.autograd.graph.Node] = set() + stack: list[torch.autograd.graph.Node] = [] + + for tensor, _ in tensors_with_sources: + if isinstance(tensor, torch.Tensor): + grad_fn = tensor.grad_fn + if grad_fn is not None: + stack.append(grad_fn) + + while stack: + node = stack.pop() + if node in visited: + continue + # Stop traversal at stop_at nodes and don't include them + # in consumed grad_fn list. + if node in stop_at: + continue + visited.add(node) + for next_fn, _ in node.next_functions: + if next_fn is not None: + stack.append(next_fn) + return visited + + +@functools.cache +def _step_logger() -> Any: + return torchdynamo_logging.get_step_logger(log) + + +@dataclass +class GraphCompileReason: + """Stores why a given output graph was compiled; i.e. what caused the graph break.""" + + reason: str + user_stack: list[traceback.FrameSummary] + + # Indicates if this was a graph break reason due to graph break. + graph_break: bool = True + + def __post_init__(self) -> None: + if self.graph_break: + graph_break_reasons.append(self) + + +def _get_gen_rand_values_fn(random_calls: Any) -> Callable[[], list[Any]]: + def _gen_rand_values() -> list[Any]: + return [fn(*args, **kwargs) for fn, args, kwargs in random_calls] + + return _gen_rand_values + + +class FakeRootModule(torch.nn.Module): + """Trick the constructor of fx.GraphModule""" + + def __init__(self, nn_modules: dict[str, torch.nn.Module]) -> None: + super().__init__() + for k, v in nn_modules.items(): + setattr(self, k, v) + + def __repr__(self) -> str: + return "FakeRootModule(...)" + + def add_nn_modules(self, nn_modules: dict[str, torch.nn.Module]) -> None: + for k, v in nn_modules.items(): + setattr(self, k, v) + + +class WrapperBackend: + def __init__(self, backend: CompilerFn) -> None: + self.backend: CompilerFn = backend + + def __call__( + self, gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor] + ) -> CompiledFn: + self.restore = checkpoint_params(gm) + self.gm = gm + copy_gm = copy.deepcopy(self.gm) + self.candidate = self.backend(copy_gm, example_inputs) + + if self.candidate is None or self.candidate is self.gm.forward: + return self.gm.forward + + if not config.verify_correctness: + return self.candidate + + # if verify_correctness=True + try: + correct = self.gm.forward(*clone_inputs(example_inputs)) + result = self.candidate(*clone_inputs(example_inputs)) + + # TODO: replace `same` function with the one in testing + if same(correct, result): + return self.candidate + + raise RuntimeError(f"incorrect results of backend {self}") + + except Exception: + log.exception("error in verify_correctness") + raise + finally: + self.restore() + + +Scope = dict[str, object] + + +@dataclass +class BytecodeTracingTimings: + """Accumulated wall-clock time (ns) for major components during Dynamo + bytecode tracing that are not related to variable trackers. Each field + is an int accumulator that gets bumped via ``time.time_ns()`` in the + corresponding hot-path wrapper. To add a new timer, add a field here + and wire up the wrapper in the relevant function.""" + + get_fake_value_ns: int = 0 + create_proxy_ns: int = 0 + wrap_to_fake_tensor_and_record_ns: int = 0 + variable_builder_call_ns: int = 0 + + def report_and_reset(self) -> None: + """Flush accumulated timings to the bytecode_tracing chromium event + and to compilation_time_metrics, then reset all counters.""" + chromium_log = get_chromium_event_logger() + # pyrefly: ignore [implicit-any] + event_data = {} + for f in dataclasses.fields(self): + ns_val = getattr(self, f.name) + if ns_val > 0: + key = f.name.removesuffix("_ns") + event_data[f"{key}_time_s"] = ns_val / 1e9 + compilation_time_metrics.setdefault(key, []).append(ns_val / 1e9) + setattr(self, f.name, 0) + if event_data: + chromium_log.try_add_event_data("bytecode_tracing", **event_data) + + +@dataclass +class OutputGraphGuardsState: + """ + A base class containing fields that are considered "persistent" when we + want to save all the important state for reconstrucing guards in a different + process. Normally we don't need to add states here, but we may have to when + the information is needed to serialize the guards, so the fields here are + supposed to be serializable as a requirement. + """ + + local_scope: Scope + global_scope: Scope + # This records the initial torch function mode stack for guarding + torch_function_mode_stack: list[torch.overrides.TorchFunctionMode] + guard_on_key_order: set[Source] + # Map from graph input's `Source` to sizes / strides metadata + input_source_to_sizes_strides: dict[Source, dict[str, Any]] + dual_level: int + functorch_layers: list[torch._functorch.pyfunctorch.FuncTorchInterpreter] + current_device: torch.device | None + global_state_guard: torch._C._dynamo.guards.GlobalStateGuard + _guards: torch._guards.GuardsSet + _aotautograd_guards: list[torch._guards.GuardEnvExpr] + + # Whether or not the guards should be checked for correctness + + export: bool = False + skip_guards_check: bool = False + export_constraints: bool = False + name_of_builtins_dict_key_in_fglobals: str | None = None + + @property + def shape_env(self) -> ShapeEnv: + raise AssertionError(f"shape_env shouldn't be accessed from {type(self)}") + + @property + def guards(self) -> torch._guards.GuardsSet: + return self._guards + + @property + def aotautograd_guards(self) -> list[torch._guards.GuardEnvExpr]: + return self._aotautograd_guards + + def dump_guards_state(self) -> "OutputGraphGuardsState": + # Dump a serializable version of self without extras + return OutputGraphGuardsState( + local_scope=self.local_scope, + global_scope=self.global_scope, + torch_function_mode_stack=self.torch_function_mode_stack, + guard_on_key_order=self.guard_on_key_order, + input_source_to_sizes_strides=self.input_source_to_sizes_strides, + dual_level=self.dual_level, + functorch_layers=self.functorch_layers, + current_device=self.current_device, + global_state_guard=self.global_state_guard, + name_of_builtins_dict_key_in_fglobals=self.name_of_builtins_dict_key_in_fglobals, + export=self.export, + export_constraints=self.export_constraints, + _guards=self.guards, + _aotautograd_guards=self.aotautograd_guards, + skip_guards_check=self.skip_guards_check, + ) + + +@dataclass +class StackLocalsMetadata: + """ + Stores metadata for a frame's stack and locals for the purposes of building resume functions + """ + + num_stack: int = 0 # number of stack elements, minus removed NULLs + locals_names: dict[str, int] = dc_field( + default_factory=dict + ) # order of locals codegen'd to the stack + stack_null_idxes: list[int] = dc_field(default_factory=list) + locals_null_keys: list[str] = dc_field(default_factory=list) + stack_ctx_args: list[tuple[int, tuple[Any, ...]]] = dc_field(default_factory=list) + stack_ctx_idxes_orig: list[int] = dc_field(default_factory=list) + locals_ctx_args: list[tuple[str, tuple[Any, ...]]] = dc_field(default_factory=list) + + +# TODO we should expand this to make it work for atribtrary in/out +@dataclass +class ExportMetaData: + # maps graph input index to its' source which is later + # used in export to map to correct user input. In its' flat form, + # just looks like GetItem(base=LocalSource("foo", idx=0)) + graph_input_idx_to_local_source: dict[int, Source | None] = dc_field( + default_factory=dict + ) + # maps user output idx to what type of output it is. There are 3 options: + # 1) graph out + # 2) user input + # 3) constants + output_return_type: dict[int, tuple[str, Any]] = dc_field(default_factory=dict) + # output spec of the traced function + out_spec: torch.utils._pytree.TreeSpec | torch.utils._pytree.LeafSpec = ( + torch.utils._pytree._LEAF_SPEC + ) + module_call_spec: dict[ + str, + dict[str, torch.utils._pytree.TreeSpec | torch.utils._pytree.LeafSpec], + ] = dc_field(default_factory=dict) + + +def get_builtins_dict(global_scope: Scope) -> dict[str, Any]: + # f_globals["__builtins__"] can be a dict or a module. This is an + # implementation detail - + # https://docs.python.org/3/library/builtins.html. + + # This makes guarding on any builtin messy because the guard check_fn + # has to check if the __builtins__ is a module or dict, and then access + # by either using getattr or getitem respectively. + + # To solve this problem, we insert a new entry in f_globals which points + # to the builtins __dict__ and then we guard any builtin on this dict. + # To avoid any collision with the pre-existing keys, we use the + # install_global to give us a unique dict key. + + f_builtins = global_scope["__builtins__"] + if not isinstance(f_builtins, dict): + f_builtins = f_builtins.__dict__ + return f_builtins + + +class OutputGraphCommon(OutputGraphGuardsState): + """ + A minimal interface for full graph capture. It is intended to be + the target of any tracer that feeds into backends. + + Currently dynamo's OutputGraph is the only known implementation + of this interface, used by (aot) precompile and (strict) export. + Importantly, that implementation also contains many other fields + that are using during tracing but not included in this interface + because they are not used once tracing is complete. + + It should be safe to assume that (caching) precompile also uses + this interface. + + In the future, we want make_fx, used by (non-strict) export, to + also implement this interface. + + The serializable part of this interface is OutputGraphGuardsState. + We do not need to serialize other parts; however it will pay to + be disciplined about what those other parts are, especially since + we want other tracers to be able to meaningfully implement them, + and we should generally try to cut them down when possible. + """ + + def __init__( + self, + output_graph_guards_state: OutputGraphGuardsState, + import_sources: dict[str, str] | None = None, + shape_env: ShapeEnv | None = None, + export_metadata: ExportMetaData | None = None, + tracked_fakes_id_to_source: dict[int, list[Source]] | None = None, + ) -> None: + super().__init__( + output_graph_guards_state.local_scope, + output_graph_guards_state.global_scope, + output_graph_guards_state.torch_function_mode_stack, + output_graph_guards_state.guard_on_key_order, + output_graph_guards_state.input_source_to_sizes_strides, + output_graph_guards_state.dual_level, + output_graph_guards_state.functorch_layers, + output_graph_guards_state.current_device, + output_graph_guards_state.global_state_guard, + output_graph_guards_state._guards, + output_graph_guards_state._aotautograd_guards, + output_graph_guards_state.export, + output_graph_guards_state.skip_guards_check, + output_graph_guards_state.export_constraints, + output_graph_guards_state.name_of_builtins_dict_key_in_fglobals, + ) + + self.import_sources = import_sources or {} + # The following fields are currently known to be used by clients. + # In particular, we need: + # - shape_env, for building guards + # - export_metadata, for un/flattening inputs and outputs + # - tracked_fakes_id_to_source, for processing tensor dim constraints + self._shape_env = shape_env or ShapeEnv() # private for inheritance + self.export_metadata = export_metadata or ExportMetaData() + self.tracked_fakes_id_to_source: dict[int, list[Source]] = ( + tracked_fakes_id_to_source or {} + ) + + @property + def shape_env(self) -> ShapeEnv: + return self._shape_env + + def bypass_package(self, reason: str = "", **kwargs: Any) -> None: + # NOTE: currently there are no tests for this but it is reachable + # when building guards, so technically necessary to include here. + # It is unclear whether we should include packaging altogether. + raise NotImplementedError + + +class OutputGraph(OutputGraphCommon): + """ + Wrapper class to hold outputs of InstructionTranslator. Mainly the + generated fx.Graph. + + OutputGraph is 1:1 with a frame being processed. Each frame is associated + with some root InstructionTranslator. When user code calls a function, + we construct a InliningInstructionTranslator that continues to write into + the root InstructionTranslator's OutputGraph. + """ + + side_effects: SideEffects + + def __init__( + self, + code_options: dict[str, Any], + compiler_fn: CompilerFn | None, + root_tx: "InstructionTranslatorBase", + export: bool, + export_constraints: Sequence[_ConstraintTarget], + frame_state: Any, + local_scope: Scope, + global_scope: Scope, + f_code: CodeType, + torch_function_mode_stack: list[torch.overrides.TorchFunctionMode], + package: Optional["CompilePackage"], + one_graph: bool = False, + ) -> None: + OutputGraphGuardsState.__init__( + self, + local_scope, + global_scope, + torch_function_mode_stack, + guard_on_key_order=set(), + input_source_to_sizes_strides={}, + dual_level=torch.autograd.forward_ad._current_level, + functorch_layers=torch._functorch.pyfunctorch.retrieve_all_functorch_interpreters(), + current_device=torch.utils._device.CURRENT_DEVICE, + # initial_global_state is only None during NopTest. + global_state_guard=torch._dynamo.convert_frame.initial_global_state + or torch._C._dynamo.guards.GlobalStateGuard(), + # These are set by @property instead, just initialize them as blank + _guards=torch._guards.GuardsSet(), + _aotautograd_guards=[], + ) + self.tracers = [SubgraphTracer(self, is_export=export)] + # Map from graph input's `Source` to its `VariableTracker` to + # de-duplicate graph inputs by source and reuse the tracker + self.input_source_to_var: dict[Source, VariableTracker] = {} + # List of TensorVariables that are leaf tensors created in-graph + # (e.g., nn.Parameter via tracable_create_parameter). These need to be + # tracked separately from input_source_to_var for backward() auto-detection. + self.leaf_var_creation_order: list[VariableTracker] = [] + self.export = export + self.export_constraints = export_constraints # type: ignore[assignment] + self.frame_state = frame_state + self.cleanup_hooks: list[Callable[[], Any]] = [] + # compile_id is an id number for the current torch.compile + self.compile_id: int = next(_compile_id_counter) + # Set of globals installed via install_global* APIs + self.installed_globals: set[str] = set() + + # TODO: maybe should just pass the entire f_code in here? Not + # sure... + self.co_fields = { + "co_name": f_code.co_name, + "co_filename": f_code.co_filename, + "co_firstlineno": f_code.co_firstlineno, + } + + # Cudagraph annotation is set during inlining in + # InliningInstructionTranslator.build_inline_tracer when a decorated + # function is inlined. + self.cudagraph_annotation: _CudagraphAnnotation | None = None + + self.region_tracker = GraphRegionTracker() + self._emit_debugger_breakpoint: bool = False + + # tracked_fakes says where any tensor that was wrapped to fake came + # from. It is similar to GraphArg, in that all GraphArgs will get + # will get added to TrackedFakes, but TrackedFakes also contains + # GraphArgs that got pruned, and things like Tensor attributes which + # aren't explicit graph inputs. Used by shape guard + self.tracked_fakes: list[TrackedFake] = [] + + shape_env = ShapeEnv( + # Reference Cycle! + # Share a reference to the list of TrackedFake. + # + # ShapeEnv needs this in order to be able to reproduce the call + # to produce_guards at an arbitrary time point. That is because + # TrackedFake instances may have its metadata changed throughout + # the program execution. + tracked_fakes=self.tracked_fakes, + # We want to allow capture scalar outputs and allow_dynamic_output_shape_ops when fullgraph=True + allow_scalar_outputs=one_graph or config.capture_scalar_outputs, + allow_dynamic_output_shape_ops=one_graph + or config.capture_dynamic_output_shape_ops, + prefer_deferred_runtime_asserts_over_guards=config.prefer_deferred_runtime_asserts_over_guards, + co_fields=self.co_fields, + ) + + # In export mode, we force the shape_env to strictly disallow any constraining + # of the user marked dynamic dims + import torch._functorch.config as _config + + with _config.patch(fake_tensor_allow_unsafe_data_ptr_access=False): + fake_mode = torch._subclasses.FakeTensorMode( + shape_env=shape_env, + # TODO (tmanlaibaatar) Remove this once we always lift params and buffers + allow_non_fake_inputs=bool(self.export), + export=self.export, + ) + self.tracing_context: TracingContext = TracingContext(fake_mode) + self.tracing_context.traced_code.append(f_code) + self.tracing_context.cudagraph_annotation = self.cudagraph_annotation + self.traced_code = self.tracing_context.traced_code + self.dynamo_compile_id: CompileId | None = CompileContext.current_compile_id() + self.init_ambient_guards() + + # Map each tensor id to a list of sources. This is necessary because + # tensor ids cannot be recovered from tracked fakes (in general). + # We use this map to interpret (i.e., check for violations of) constraints, + # specifically equality constraints, which have shared tensor ids in them. + # This map should also be generally useful, e.g., for (de)serialization. + self.tracked_fakes_id_to_source: dict[int, list[Source]] = ( + collections.defaultdict(list) + ) + # Stores the full fqn of a param or buffer to the relevant source. + self.param_name_to_source: dict[str, Source] | None = {} + self.side_effects = SideEffects(self) + # Cached variable trackers. This makes symbolic analysis of LOAD_GLOBAL + # and LOAD_ATTR for same python objects free. + self.variable_tracker_cache: dict[Source, VariableTracker] = {} + # Cache for sources resolved via MRO walk, keyed by id(obj). + # When the same descriptor (e.g. property) is reached from multiple + # subclasses, we reuse the first source to avoid redundant guards. + # We thought of rolling this in variable_tracker_cache but here + # different sources point to the same object, we also don't want it to + # go through the side effects cache because even though these objects + # are same, we dont want OBJECT_ALIASING guards on them. For these + # objects, we have DICT_CONTAINS absent guards on the mro walk, so there + # is no need of the OBJECT_ALIASING guards. + self.mro_source_cache: dict[tuple[int, str], DictGetItemSource] = {} + # Tracks (id(klass), attr_name) pairs that already have a + # DICT_CONTAINS absent guard installed during MRO walks. When + # multiple subclasses share the same intermediate MRO class, we + # only need to guard the absence once per (class, attr) pair. + self.guarded_mro_absent_keys: set[tuple[int, str]] = set() + # Cache for inspect.signature results: function -> VariableTracker + self.signature_cache: dict[Any, VariableTracker] = {} + self.unique_var_id = itertools.count() + self.code_options: dict[str, Any] = dict(code_options) + self.output_instructions: list[Instruction] = [] + # used to track nodes that are added between calls of copy_graphstate + # and restore_graphstate + self.timestamp = 0 + + # Maps stream id (id(stream_value)) → user stack trace for input + # mutations on that stream. Used to error when an event records on a + # stream that already has an input mutation (the epilogue copy_() + # wouldn't be captured). We key by id() of the underlying + # torch.Stream so we can peek lazy variables without realizing them. + self._input_mutation_streams: dict[int, traceback.StackSummary] = {} + self._last_checked_input_versions: dict[int, int] | None = None + + # A list of register_finalizer_fns to apply to the output graph module + self.register_finalizer_fns: list[Callable[[fx.GraphModule], None]] = [] + + # Not checkpointed + self.compiler_fn: CompilerFn | None = compiler_fn + self.root_tx = root_tx + + # Profiler state for tracking function trace timings + self.profiler_state: DynamoProfilerState | None = None + + self.package = package + # Given a source, what are the user stacks of all locations that + # accessed it? + # + # For efficiency, we only populate this: + # - During export, and + # - If the source could potentially lead to a spurious export input + # + # Feel free to populate this more frequently if other use-cases arise, + # but be aware that we have to generate full stacks for each + # recording! + self.source_to_user_stacks: dict[Source, list[traceback.StackSummary]] = {} + + self._current_tx: list[InstructionTranslatorBase] = [] + self.cleanups: list[CleanupHook] = [] + self.should_exit = False + self.unspec_variable_map: dict[str, UnspecializedPythonVariable] = {} + + # This returns false if TF Overall (both mode and subclass) is disabled OR that TF Mode stack is empty + self.torch_function_mode_enabled = torch._C._is_torch_function_mode_enabled() + + # Used to wrap the compiled graph at runtime with + # DisableTorchFunctionSubclass to prevent double dispatch. + self.torch_function_subclass_inlined = False + + # Tracks if the output graph has a user defined allowed function in the + # graph. This is used later to determine if we should fallback to eager + # for certain exceptions. THe idea is that if the user has applied + # allow_in_graph, they would like to see the error instead of falling + # back for backend errors. + self.has_user_defined_allowed_in_graph = False + + # Tracks ALL grad_fn nodes that are consumed by torch.autograd.grad(outputs, inputs). + # This is the set of all nodes reachable from outputs' grad_fns, excluding inputs' grad_fns + # (since autograd.grad stops at inputs without consuming them). + # Used to detect returning tensors connected to consumed grad_fns (would cause + # "backward through graph a second time" error in aot_autograd). + self.autograd_grad_consumed_grad_fns: set[torch.autograd.graph.Node] = set() + + # Tracks a list of called ops that were not tagged with "pt2_compliant_tag". + # This information is useful for logging. + self.non_compliant_ops: set[torch._ops.OpOverload] = set({}) + + # Tracks a list of called custom ops that were tagged with "pt2_compliant_tag". + # This information is useful for logging. + self.compliant_custom_ops: set[torch._ops.OpOverload] = set({}) + + # We save the global torch state here to be restored in case of graph + # breaks. The relevant issue is seen here + # https://github.com/pytorch/pytorch/pull/100570#issuecomment-1543427086 + # where inlining of a function changes the global state (because of the + # presence of torch.no_grad) and there is a graph break. + self.save_global_state() + + # Tracks the original FQNs of the constant tensors from the original graph, + # i.e. buffers and parameters. + self.dynamo_flat_name_to_original_fqn: dict[str, str] = {} + + # All calls to random() are replaced with a single call to __gen_rand_values + # functions that returns a tuple of random values for each original call. + # random_calls tracks calls to random() and random_values_var stores the name of + # the variable that stores __gen_rand_values results. + self.random_calls: list[ + tuple[Callable[..., object], tuple[object, ...], dict[str, object]] + ] = [] + self.random_values_var: Any = None + + # Bytecode to insert right before we call the graph + self.pregraph_bytecode: list[Instruction] = [] + + # Maps SyntheticLocalSource → (fn, args, arg_sources) for hoisted + # graph inputs created by synthetic_graph_input, so invoke_subgraph + # reuse can recreate them on cache hit. arg_sources allows stamp-out + # to resolve new ctor arg values via source replacement. + self.synthetic_source_ctor_info: dict[ + SyntheticLocalSource, + tuple[ + Callable[..., Any], tuple[Any, ...], tuple[Source | None, ...] | None + ], + ] = {} + + # Use to pass values to backward hooks when using compiled autograd + self.backward_state: dict[str, VariableTracker] = {} + self.backward_state_proxy: torch.fx.Proxy | None = None + self.backward_state_var: str | None = None + + # pyrefly: ignore [bad-override] + self.name_of_builtins_dict_key_in_fglobals: str = ( + self.install_builtins_dict_in_fglobals() + ) + + self.compiler_trace_stack = contextlib.ExitStack() + + self.bytecode_tracing_timings = BytecodeTracingTimings() + + # These are the ambient, currently-global saved_tensor_hooks stashed in autograd, + # that are set for the entire duration of the compiled region. + # This is an invariant today because we graph break on the saved_tensor_hook + # context manager inside a compiled region + self.saved_tensors_hooks_subgraph_names: list[str] | None = ( + self.maybe_install_saved_tensors_hooks_subgraphs() + ) + + # mangled alias -> module fqn name + self.import_sources: dict[str, str] = {} + + self.export_metadata = ExportMetaData() + + # Set of inlined unspecialized modules names to generate the + # dynamo_flat_name_to_original_fqn mapping. + self.used_inlined_inbuilt_modules_names: OrderedSet[str] = OrderedSet() + + self.attr_source_cache: dict[tuple[Source, str], AttrSource] = {} + self._cached_replayed_side_effect_source_refs: tuple[str, ...] | None = None + + def get_replayed_side_effect_source_refs( + self, *, populate_export_metadata: bool = False + ) -> list[str]: + """Return Python-side effect sources that Dynamo replays outside the FX graph.""" + if ( + not populate_export_metadata + and not self.side_effects.id_to_variable + and self._cached_replayed_side_effect_source_refs is not None + ): + return list(self._cached_replayed_side_effect_source_refs) + + from torch.export._trace import _ExportModuleSpecTrackerDict + + potential_side_effects = [] + for var in self.side_effects._get_modified_vars(): + if hasattr(var, "mutation_type"): + mut_type = var.mutation_type + # Skip codegen-specific mutations that never materialize as + # externally visible Python side effects. + if isinstance( + mut_type, (AttributeMutationExisting, ValueMutationExisting) + ): + if isinstance(var, UserDefinedDictVariable) and isinstance( + var.value, _ExportModuleSpecTrackerDict + ): + if populate_export_metadata: + assert var._base_vt is not None + for ( + k, + v, + ) in ( + var._base_vt.items.items() # pyrefly: ignore[missing-attribute] + ): + # pyrefly: ignore [implicit-any] + specs = {} + # pyrefly: ignore[missing-attribute] + for k_spec, val in v.items.items(): + specs[k_spec.vt.as_python_constant()] = ( + val.as_python_constant() + ) + assert ["in_spec", "out_spec"] == list(specs.keys()) + self.export_metadata.module_call_spec[ + # pyrefly: ignore[missing-attribute] + k.vt.as_python_constant() + ] = specs + # export uses tracepoint pass to dump submodule inp/out spec + # into global state, so we filter it here + if not ( + isinstance(var, UserDefinedDictVariable) + and isinstance(var.value, _ExportModuleSpecTrackerDict) + ): + potential_side_effects.append(var) + + return [_get_source_debug_name(var.source) for var in potential_side_effects] + + def get_chained_attr_source(self, base: Source, path: str) -> AttrSource: + parts = path.split(".") + key = (base, parts[0]) + if key not in self.attr_source_cache: + self.attr_source_cache[key] = AttrSource(base, parts[0]) + result = self.attr_source_cache[key] + for part in parts[1:]: + key = (result, part) + if key not in self.attr_source_cache: + self.attr_source_cache[key] = AttrSource(result, part) + result = self.attr_source_cache[key] + return result + + def get_chained_param_buffer_source( + self, base: Source, path: str + ) -> "ParamBufferSource": + parts = path.rsplit(".", 1) + if len(parts) == 1: + return ParamBufferSource(base, path) + intermediate_base = self.get_chained_attr_source(base, parts[0]) + return ParamBufferSource(intermediate_base, parts[1]) + + def mark_bytecode_tracing_start(self) -> None: + self.compiler_trace_stack.enter_context( + dynamo_timed( + "bytecode_tracing", + log_pt2_compile_event=True, + ) + ) + # Start profiler timing for the root function + if config.dynamo_profiler: + from torch._dynamo.dynamo_profiler import DynamoProfilerState + + if self.profiler_state is None: + self.profiler_state = DynamoProfilerState() + code = self.root_tx.f_code + self.profiler_state.push( + code.co_name, + code.co_filename, + code.co_firstlineno, + time.time_ns(), + ) + + def mark_bytecode_tracing_stop(self) -> None: + self.bytecode_tracing_timings.report_and_reset() + self.compiler_trace_stack.close() + # Record profiler timing for the root function and dump stats + if config.dynamo_profiler and self.profiler_state is not None: + from torch._dynamo.dynamo_profiler import FunctionTraceTiming + + stack_entry = self.profiler_state.pop() + trace_end_ns = time.time_ns() + if stack_entry is not None: + cumtime_ns = trace_end_ns - stack_entry.start_time_ns + tottime_ns = cumtime_ns - stack_entry.child_time_ns + timing = FunctionTraceTiming( + func_name=stack_entry.func_name, + filename=stack_entry.filename, + firstlineno=stack_entry.firstlineno, + cumtime_ns=cumtime_ns, + tottime_ns=tottime_ns, + bytecode_count=len(self.root_tx.f_code.co_code), + inline_depth=0, + caller_func_name=None, + caller_filename=None, + caller_firstlineno=None, + is_primitive_call=stack_entry.is_primitive_call, + call_stack=(), + ) + self.profiler_state.record_timing(timing) + + # Dump profiler stats + output_file = None + if isinstance(config.dynamo_profiler, str): + output_file = config.dynamo_profiler + self.profiler_state.dump_stats(output_file) + + def install_builtins_dict_in_fglobals(self) -> str: + f_builtins = get_builtins_dict(self.global_scope) + return self.install_global("__builtins_dict__", f_builtins) + + def add_backward_state_hook( + self, hook: VariableTracker, prefix: str = "hook" + ) -> tuple[str, torch.fx.Proxy]: + name = f"{prefix}{len(self.backward_state)}" + assert name not in self.backward_state + self.backward_state[name] = hook + return name, self.get_backward_state_proxy() + + def get_backward_state_proxy(self) -> torch.fx.Proxy: + if self.backward_state_proxy is None: + if self.export: + unimplemented( + gb_type="backward_state does not support export", + context="", + explanation="Compiled autograd doesn't work with `torch.export`.", + hints=[], + ) + example_value = BackwardState() + self.backward_state_proxy = self.root_tracer.create_graph_input( + "dynamo_backward_state", + type(example_value), + example_value, + source=BackwardStateSource(), + ) + self.backward_state_proxy.node.meta["grapharg"] = BackwardStateGraphArg() + self.backward_state_var = self.new_var() + return self.backward_state_proxy + + # This gets its own helper function so guards DEBUG logs are more informative + def init_ambient_guards(self) -> None: + # Register a SHAPE_ENV guard to make sure we setup shape guards + # that show up in ShapeEnv + self.guards.add(ShapeEnvSource().make_guard(GuardBuilder.SHAPE_ENV)) + + self.guards.add( + GlobalStateSource().make_guard(GuardBuilder.DETERMINISTIC_ALGORITHMS) + ) + + self.guards.add(GlobalStateSource().make_guard(GuardBuilder.GRAD_MODE)) + + self.guards.add(GlobalStateSource().make_guard(GuardBuilder.DEFAULT_DEVICE)) + + self.guards.add(GlobalStateSource().make_guard(GuardBuilder.GLOBAL_STATE)) + self.guards.add( + GlobalStateSource().make_guard(GuardBuilder.TORCH_FUNCTION_STATE) + ) + + ci = torch._C._functorch.peek_interpreter_stack() + if ci is not None: + self.guards.add( + GlobalStateSource().make_guard(GuardBuilder.FUNCTORCH_STACK_MATCH) + ) + if not torch._dynamo.compiled_autograd.in_compiled_autograd_region: + self.guards.add( + GlobalStateSource().make_guard( + GuardBuilder.AUTOGRAD_SAVED_TENSORS_HOOKS + ) + ) + + def maybe_install_saved_tensors_hooks_subgraphs(self) -> list[str] | None: + if torch._dynamo.compiled_autograd.in_compiled_autograd_region: + return None + + get_hooks = torch._functorch._aot_autograd.utils.top_saved_tensors_hooks + are_inline_hooks = ( + torch._functorch._aot_autograd.utils.saved_tensors_hooks_are_inlineable + ) + hooks = get_hooks() + if not are_inline_hooks(hooks): + return None + + # If GraphModule provided by user contains fx.wrap, + # We can only rely on user provided cache hash in this case. + # If user did not provide cache hash - then we always bypass cache. + + pack_gm, unpack_gm = hooks + pack_subgraph_name = self.install_subgraph( + "saved_tensors_hooks_pack", + torch.fx.GraphModule(self.nn_modules, pack_gm.graph), + ) + unpack_subgraph_name = self.install_subgraph( + "saved_tensors_hooks_unpack", + torch.fx.GraphModule(self.nn_modules, unpack_gm.graph), + ) + assert pack_subgraph_name == "saved_tensors_hooks_pack_0" + assert unpack_subgraph_name == "saved_tensors_hooks_unpack_0" + return [pack_subgraph_name, unpack_subgraph_name] + + def synthetic_graph_input( + self, + fn: Callable[..., Any], + args: tuple[Any, ...], + ctor_arg_sources: tuple[Source | None, ...] | None = None, + ) -> VariableTracker: + """ + call fn(*args) before the graph runs and turn the result into a fake input. + """ + example_value = fn(*args) + varname = self.new_var() + cg = PyCodegen(self.root_tx) + cg.add_push_null( + lambda: cg.load_import_from( + fn.__module__, + fn.__name__, + ) + ) + if ctor_arg_sources: + cg.foreach( + src if src is not None else variables.ConstantVariable.create(val) + for val, src in zip(args, ctor_arg_sources) + ) + else: + cg.foreach(map(variables.ConstantVariable.create, args)) + cg.call_function(len(args), False) + cg.store(varname) + self.pregraph_bytecode.extend(cg.get_instructions()) + source = SyntheticLocalSource(varname) + self.synthetic_source_ctor_info[source] = (fn, args, ctor_arg_sources) + result = VariableTracker.build(self.root_tx, example_value, source) + # Realize the VT because we will delete the guards on it in the next line. + result = result.realize() + TracingContext.get().guards_context.dynamo_guards.remove_guards_with_source( + source + ) + return result + + def add_cleanup_hook(self, fn: Callable[[], Any]) -> None: + self.cleanup_hooks.append(fn) + + def call_cleanup_hooks(self) -> None: + for hook in reversed(self.cleanup_hooks): + hook() + self.cleanup_hooks.clear() + + @property + def root_tracer(self) -> "SubgraphTracer": + return self.tracers[0] + + @property + def current_tracer(self) -> "SubgraphTracer": + return self.tracers[-1] + + def is_root_tracer(self) -> bool: + # Helper to tell if we are inside the higher order operator tracing. + return len(self.tracers) == 1 + + def check_input_mutation_on_current_stream( + self, tx: "InstructionTranslatorBase" + ) -> None: + """Record which stream index has input mutations by comparing current + tensor versions against the versions captured at graph input creation.""" + if not hasattr(tx, "symbolic_stream_state"): + return + if not tx.symbolic_stream_state.in_stream_context(): + return + + tracer = self.root_tracer + if self._last_checked_input_versions is None: + self._last_checked_input_versions = dict( + enumerate(tracer._input_versions_at_beginning) + ) + + cur_stream_index = tx.symbolic_stream_state.cur_stream_id() + input_idx = 0 + for node in tracer.graph.nodes: + if node.op != "placeholder": + break + example_value = node.meta.get("example_value") + if not isinstance(example_value, torch.Tensor): + continue + prev_version = self._last_checked_input_versions.get(input_idx) + cur_version = example_value._version + if prev_version is not None and cur_version > prev_version: + if cur_stream_index not in self._input_mutation_streams: + self._input_mutation_streams[cur_stream_index] = ( + TracingContext.extract_stack() + ) + self._last_checked_input_versions[input_idx] = cur_version + input_idx += 1 + + _EVENT_INPUT_MUTATION_FIX = ( + "To fix this, either:\n" + " 1. Move the input mutation after the event.record() call.\n" + " 2. Record the event outside the compiled region:\n" + " compiled_fn(x)\n" + " event.record(stream) # after torch.compile returns\n" + " 3. Insert a graph break before recording:\n" + " torch._dynamo.graph_break()\n" + " event.record(stream)\n" + " 4. Record the event on a stream that has no input mutations." + ) + + def check_event_record_after_input_mutation(self, stream_index: int) -> None: + """Error if an event is being recorded on a stream that already has + an input mutation. Called at record time so ordering is naturally + respected — records before mutations won't trigger this.""" + if stream_index not in self._input_mutation_streams: + return + + mutation_stack = self._input_mutation_streams[stream_index] + record_stack = TracingContext.extract_stack() + + msg = ( + "An event was recorded on a stream where a graph input was " + "previously mutated. The input mutation is applied via copy_() " + "in the runtime epilogue after the graph executes, so the event " + "would not capture the mutation, leading to incorrect " + "synchronization.\n\n" + "Input mutation occurred here:\n" + f"{''.join(mutation_stack.format())}\n" + "Event record occurred here:\n" + f"{''.join(record_stack.format())}\n" + self._EVENT_INPUT_MUTATION_FIX + ) + raise RuntimeError(msg) + + @property + def graph(self) -> torch.fx.Graph: + return self.current_tracer.graph + + # TODO(rzou): can delete after we refactor speculate_subgraph to use nested GraphTracer. + @graph.setter + def graph(self, value: torch.fx.Graph) -> None: + self.current_tracer.graph = value + + @property + def input_name_to_proxy(self) -> dict[str, fx.Proxy]: + return self.current_tracer.input_name_to_proxy + + @property + def real_value_cache(self) -> dict[fx.Node, torch.Tensor]: + return self.current_tracer.real_value_cache + + @property + def bound_symbols(self) -> dict[sympy.Symbol, Union[torch.fx.Proxy, "LazyProxy"]]: + return self.current_tracer.bound_symbols + + # If you are here, and you're looking for create_graph_input, + # to avoid ambiguity, please call one of the following: + # - self.current_tracer.create_graph_input + # - self.root_tracer.create_graph_input + # See NOTE [HigherOrderOperator tracing design] for more context. + + def create_proxy(self, *args: Any, **kwargs: Any) -> torch.fx.Proxy: + return self.current_tracer.create_proxy(*args, **kwargs) + + def create_node(self, *args: Any, **kwargs: Any) -> torch.fx.Node: + return self.current_tracer.create_node(*args, **kwargs) + + def remove_node(self, *args: Any, **kwargs: Any) -> None: + return self.current_tracer.remove_node(*args, **kwargs) + + @contextlib.contextmanager + def subtracer( + self, + source_target: Target | None, + prior_tracer: Optional["SubgraphTracer"], + description: str | None = None, + ) -> Generator["SubgraphTracer", None, None]: + new_scope_ctx = enter_new_scope() + try: + if prior_tracer: + # Lineage MUST stay preserved + assert prior_tracer.parent is self.current_tracer + new_scope_ctx.__enter__() + tracer = ( + prior_tracer + if prior_tracer + else SubgraphTracer( + self, + parent=self.current_tracer, + source_target=source_target, + is_export=self.current_tracer.is_export, + description=description, + ) + ) + self.tracers.append(tracer) + yield tracer + finally: + new_scope_ctx.__exit__(None, None, None) + self.tracers.pop() + + @property + def output(self) -> "OutputGraph": + return self + + @property + def fake_mode(self) -> torch._subclasses.FakeTensorMode: + assert self.tracing_context.fake_mode is not None + return self.tracing_context.fake_mode + + @property + def shape_env(self) -> ShapeEnv: + assert self.tracing_context.fake_mode is not None + assert self.tracing_context.fake_mode.shape_env is not None + return self.tracing_context.fake_mode.shape_env + + @property + def guards(self) -> torch._guards.GuardsSet: + return self.tracing_context.guards_context.dynamo_guards + + @property + def nn_modules(self) -> dict[str, Any]: + return self.tracing_context.module_context.nn_modules + + @property + def aotautograd_guards(self) -> list[torch._guards.GuardEnvExpr]: + return self.tracing_context.guards_context.aotautograd_guards + + def save_global_state( + self, out: dict[str, tuple[Callable[..., Any], bool]] | None = None + ) -> None: + """ + Saves to out if it is provided. Else saves to the tracing context's global_state. + """ + global_state = cast( + dict[str, tuple[Callable[..., Any], bool]], + ( + out + if out is not None + else self.tracing_context.global_context.global_state + ), + ) + + global_state["grad_enabled"] = (torch.set_grad_enabled, torch.is_grad_enabled()) + + gpu_type = ( + acc.type if (acc := torch.accelerator.current_accelerator()) else "cuda" + ) + global_state["autocast_enabled"] = ( + functools.partial(torch.set_autocast_enabled, gpu_type), + torch.is_autocast_enabled(gpu_type), + ) + global_state["autocast_cpu_enabled"] = ( + functools.partial(torch.set_autocast_enabled, "cpu"), + torch.is_autocast_enabled("cpu"), + ) + global_state["autocast_gpu_dtype"] = ( # type:ignore[assignment] + functools.partial(torch.set_autocast_dtype, gpu_type), + torch.get_autocast_dtype(gpu_type), + ) + global_state["autocast_cpu_dtype"] = ( # type:ignore[assignment] + functools.partial(torch.set_autocast_dtype, "cpu"), + torch.get_autocast_dtype("cpu"), + ) + global_state["autocast_cache_enabled"] = ( + torch.set_autocast_cache_enabled, + torch.is_autocast_cache_enabled(), + ) + + def push_tx(self, tx: "InstructionTranslatorBase") -> None: + self._current_tx.append(tx) + + def pop_tx(self) -> "InstructionTranslatorBase": + return self._current_tx.pop() + + @property + def current_tx(self) -> "InstructionTranslatorBase": + return self.root_tx if not self._current_tx else self._current_tx[-1] + + def resolve_source_value(self, source: Source) -> Any: + """ + Resolve the runtime value a Source points to using root_tx's frame. + + Useful to inspect the python object associated with the source during + debugging. Will also be used by invoke subgraph caching later on. + """ + return source.get_value( + {"G": self.root_tx.f_globals, "L": self.root_tx.f_locals}, + {}, + {}, # type: ignore[arg-type] + ) + + def count_calls(self) -> int: + return count_calls(self.graph) + + def is_empty_graph(self) -> bool: + return len(list(self.graph.nodes)) == 0 + + def has_outputs(self) -> bool: + return len([x for x in self.graph.nodes if x.op == "output"]) > 0 + + def get_submodule(self, keys: str) -> torch.nn.Module | Any: + assert keys + obj: torch.nn.Module | dict[str, torch.nn.Module] = self.nn_modules + for k in keys.split("."): + if isinstance(obj, dict): + obj = obj[k] + else: + obj = getattr(obj, k) + return obj + + def new_var(self, name: str = "tmp") -> str: + existing = set(self.code_options["co_varnames"]) + # In common case, this will be O(1) + while True: + var = f"{name}_{next(self.unique_var_id)}" + if var not in existing: + self.code_options["co_varnames"] += (var,) + return var + + def update_co_names(self, name: str) -> None: + """Ensure self.code_options.co_names contains name""" + if name not in self.code_options["co_names"]: + self.code_options["co_names"] += (name,) + + @staticmethod + def module_key_name(*names: Any) -> str: + # create a new unique name + name = "_".join(map(str, names)) + # Strip _buffers[..]/_parameters[..]/_modules[..] names + name = re.sub( + r"\._(?:modules|parameters|buffers)\[(['\"])([^'\"\]]+)\1\]", r".\2", name + ) + # Replace getattr(a, b) with a.b + name = re.sub( + r"getattr\(\s*([^,]+?)\s*,\s*(['\"])([^'\"]+)\2\s*\)", r"\1.\3", name + ) + # Strip the guard lookup L/G access + name = re.sub(r"^[GL]\['?(.*?)'?\]$", r"\1", name) + # e.g. replace abc.xyz[123].qkv with abc.xyz_123.qkv + name = re.sub(r"\[(\d+)\]", r"_\g<1>", name) + # e.g. replace abc.xyz_123.qkv with abc_xyz_123_qkv + name = re.sub(r"[^a-zA-Z0-9]", "_", name) + + if not name or not name[0].isalpha(): + name = "sub" + name + + return name + + def register_static_attr_and_return_proxy( + self, attr_prefix: str, attr_value: Any + ) -> fx.Proxy: + # Check if the module already exists, if it does, return the already + # added proxy. This is important for executorch tests. + if isinstance(attr_value, torch.nn.Module): + for name, mod in self.nn_modules.items(): + if mod is attr_value: + proxy = self.create_proxy("get_attr", name, (), {}) + return proxy + + attr_name = get_unique_name_wrt(attr_prefix, self.nn_modules) + # TODO `nn_modules` has been historically overloaded to store a lot more + # than just nn module objects, fix that. + self.nn_modules[attr_name] = attr_value + proxy = self.create_proxy("get_attr", attr_name, (), {}) + set_example_value(proxy.node, attr_value) + return proxy + + def register_attr_or_module( + self, + target: torch.nn.Module | torch.Tensor | Any, + *names: Any, + **options: Any, + ) -> VariableTracker: + if is_dynamic_nn_module(target, self.export): + # Instead of returning UnspecializedNNModuleVariable, call + # VariableTracker.build so that it is tracked for mutation. + return VariableTracker.build(self.current_tx, target, **options) + + options = dict(options) + assert "source" in options + source = options["source"] + assert not isinstance(source, ParamBufferSource) + + if isinstance(target, torch.Tensor): + tracer = self.current_tracer + if not self.is_root_tracer(): + # For higher order ops, we don't want to insert the get_attr in + # innermost graph. Instead, we want to raise the params/buffers + # as inputs to the higher-order graph, and register them as + # get_attrs in the root tracer. + + # Note that Dynamo will still call lift_tracked_freevar_to_input + # when these inputs are encountered for the inner graph. The + # only difference is what happens at the root tracer for + # nn.Parameters vs free inputs. The free inputs are registered + # as placeholders in the root graph, whereas the nn.Parameters + # are registered as get_attr nodes in the root graph. + tracer = self.root_tracer + + def wrap_name(module_key: str) -> VariableTracker: + assert self.param_name_to_source is not None + self.param_name_to_source[module_key] = source + + # Check if the attr has already been registered. This can happen + # when two different sources point to the same tensor. + assert self.root_tx is not None + if target in self.root_tx.output.side_effects: + return self.root_tx.output.side_effects[target] + + if get_static_address_type(target) == "guarded" and not isinstance( + source, NumpyTensorSource + ): + install_guard(source.make_guard(GuardBuilder.ID_MATCH)) + elif not is_constant_source(source): + install_guard(source.make_guard(GuardBuilder.TENSOR_MATCH)) + + vt = wrap_fx_proxy( + self.root_tx, + tracer.create_proxy("get_attr", module_key, (), {}), + example_value=target, + **options, + ) + + # Track the object so to avoid duplicate registration in case of + # different sources pointing to the same tensor object. + vt = self.root_tx.output.side_effects.track_object_existing(target, vt) + + assert "tensor_dict" not in vt.as_proxy().node.meta + # pyrefly: ignore [bad-argument-type] + vt.as_proxy().node.meta["tensor_dict"] = _extract_tensor_dict(target) + + return vt + + elif isinstance(target, torch.nn.Module): + assert isinstance(target, torch.nn.Module) + + if source: + install_guard(source.make_guard(GuardBuilder.NN_MODULE)) + + def wrap_name(module_key: str) -> VariableTracker: + # pyrefly: ignore [bad-argument-type] + return NNModuleVariable(type(target), module_key, target, **options) + + else: + # This is Dynamo created graph module, e.g., graph module coming + # from higher order ops. NNModuleVariable tracker can't be + # sourceless, so let's return a unspecializedNNModule variable + # tracker. + def wrap_name(module_key: str) -> VariableTracker: + # pyrefly: ignore[bad-argument-type] + return variables.UnspecializedNNModuleVariable(target, **options) + + elif isinstance(target, (torch.SymInt, torch.SymFloat)): + # HACKY CODE REGION BEGIN + # WE ARE PIGGYBACKING ON EXISTING INFRA TO REGISTER ATTRS + # This ultimately gets written to self.nn_modules, which is unfortunate + # Attrs that are tenors and symints and such need to be migrated to have their + # own storage + # alas, this is like this for now + + def wrap_name(module_key: str) -> VariableTracker: + return SymNodeVariable.create( + self.root_tx, + self.create_proxy("get_attr", module_key, (), {}), + sym_num=target, + **options, + ) + + # HACKY CODE REGION END + elif is_opaque_type(type(target)): + # HACKY CODE REGION BEGIN + # Same as SymInt/SymFloat above: piggybacking on self.nn_modules + # to store opaque objects as graph attributes. + + tracer = self.current_tracer + if not self.is_root_tracer(): + tracer = self.root_tracer + + def wrap_name(module_key: str) -> VariableTracker: + fake_script_obj = torch._library.fake_class_registry.maybe_to_fake_obj( + self.fake_mode, target + ) + proxy = tracer.create_proxy("get_attr", module_key, (), {}) + set_example_value(proxy.node, fake_script_obj) + return torch._dynamo.variables.script_object.TorchScriptObjectVariable.create( + proxy, fake_script_obj, **options + ) + + # HACKY CODE REGION END + + else: + + def wrap_name(module_key: str) -> VariableTracker: + self.output.update_co_names(module_key) + self.global_scope[module_key] = target + return VariableTracker.build( + self, # type: ignore[arg-type] + target, + ConstantSource(source_name=module_key), + ) + + for k, v in self.nn_modules.items(): + if v is target: + # it already exists + return wrap_name(k) + + name = OutputGraph.module_key_name(*names) + name = get_unique_name_wrt(name, self.nn_modules, self.global_scope) + self.nn_modules[name] = target + if isinstance(target, torch.nn.Module): + + def register_leaf_name(leaf_name: str) -> None: + assert self.param_name_to_source is not None + new_source = self.get_chained_param_buffer_source(source, leaf_name) + new_name = f"{name}.{leaf_name}" + self.param_name_to_source[new_name] = new_source + if isinstance(source, LocalSource): + self.dynamo_flat_name_to_original_fqn[ + OutputGraph.module_key_name(new_source.name) + ] = leaf_name + + # annoying, but there are cases when we do not have parameters + # see test_nn_moduledict_contains + if hasattr(target, "_parameters"): + for leaf_name, _ in target.named_parameters(): + register_leaf_name(leaf_name) + if hasattr(target, "_buffers"): + for leaf_name, _ in target.named_buffers(): + register_leaf_name(leaf_name) + + return wrap_name(name) + + def handle_aliases_for_stolen_lists( + self, tx: "InstructionTranslatorBase" + ) -> tuple[list[Instruction], dict[Source, Source]]: + # If list inputs are stolen, but still needed after the function call, create aliases to keep them alive + maybe_gm = self.local_scope.get("self") + stolen_list_names = get_locals_to_steal(maybe_gm) + if not stolen_list_names: + return [], {} + + alias_insts = [] + needs_alias: dict[str, list[VariableTracker]] = {} + + queue = [ + *tx.stack, + *tx.symbolic_locals.values(), + *self.side_effects.store_attr_mutations.keys(), + ] + + while queue: + x = queue.pop() + if isinstance(x, BaseListVariable): + assert isinstance(x.items, list) + queue += x.items + continue + + if not ( + ( + x not in self.side_effects.store_attr_mutations + or isinstance(x.mutation_type, AttributeMutationExisting) + ) + and isinstance(x.source, GetItemSource) + and isinstance(x.source.base, LocalSource) + and x.source.base.local_name in stolen_list_names + ): + continue + + stolen_name = x.source.base.local_name + if stolen_name not in needs_alias: + needs_alias[stolen_name] = [] + needs_alias[stolen_name].append(x) + + # pyrefly: ignore [implicit-any] + visited = {} + overridden_sources: dict[Source, Source] = {} + for arg in self.graphargs: + if not ( + isinstance(arg._example, list) + and isinstance(arg.source, LocalSource) + and arg.source.local_name in needs_alias + ): + continue + + # arg is a list that will be cleared by the compiled function + list_name = arg.source.local_name + assert list_name in self.code_options["co_varnames"] + for x in needs_alias[list_name]: + # Skip if already handled. + if x.source in overridden_sources: + continue + + # A small codegen optimization because we might have different + # VariableTrackers that share the same source. + assert x.source is not None + list_idx = x.source.index # type: ignore[attr-defined] + if list_idx not in visited: + alias_name = self.new_var( + f"{list_name}_ref" + ) # self.new_var already adds unique id suffix + + visited[list_idx] = alias_name + # bytecode of `alias_name = list_name[list_idx]` + alias_insts.extend( + [ + create_instruction("LOAD_FAST", argval=list_name), + create_load_const(list_idx), + create_binary_subscr(), + create_instruction("STORE_FAST", argval=alias_name), + ] + ) + + # operate on alias, handled by suffix codegen + assert x.source is not None + old_source = x.source + overridden_sources[old_source] = LocalSource(visited[list_idx]) + + # NOTE: we need `overridden_sources` because (1) we want to codegen for + # these list items to use the new local source, but (2) we want to avoid + # updating `source` in place because that might break invariants in + # other parts of Dynamo like guards. + return alias_insts, overridden_sources + + def _get_stack_values_to_restore( + self, tx: "InstructionTranslatorBase", stack_pops: int + ) -> tuple[list[VariableTracker], StackLocalsMetadata]: + """ + Gets the stack + locals values belonging to tx that need to be restored. + + Also prunes dead tx locals and realizes all VTs in the tx's stack. + + NullVariables in stack/locals will NOT be restored, unless they are the top `stack_pops` + elements of the stack - it is expected that the next instruction to run will pop the top + `stack_pops` elements of the stack, so we should codegen NULLs. + + Returns: + - stack_values: stack and locals values that need to be restored + - meta: locations of NULLs and ContextWrappingVariables in the stack/locals + (ignores the top `stack_pops` values on the stack) + """ + tx.prune_dead_locals() + + stack_values = [] + meta = StackLocalsMetadata() + + def ctx_exit_check(var: VariableTracker) -> None: + if type.__instancecheck__(variables.WithExitFunctionVariable, var): + raise AssertionError( + "Attempted to reconstruct WithExitFunctionVariable outside the stack" + ) + + # realize any unrealized tensor VTs in case they + # need to be added to self.nn_modules as attributes + for i, value in enumerate(tx.stack): + # Allow lazy constants through for values being returned (top of stack) + allow_lazy_constant = len(tx.stack) - i <= stack_pops + variables.LazyVariableTracker.realize_all( + value, allow_lazy_constant=allow_lazy_constant + ) + # Do not allow non-stack WithExitFunctionVariable reconstructions + if not isinstance(value, variables.WithExitFunctionVariable): + VariableTracker.visit(ctx_exit_check, value) + # ignore top `stack_pops` values on the stack + if allow_lazy_constant: + stack_values.append(value) + continue + if isinstance(value, NullVariable): + meta.stack_null_idxes.append(i) + else: + stack_values.append(value) + if isinstance(value, ContextWrappingVariable): + target_values = ( + () if value.target_values is None else tuple(value.target_values) + ) + # NOTE: track index in stack after NULLs have been removed + meta.stack_ctx_args.append((len(stack_values) - 1, target_values)) + meta.stack_ctx_idxes_orig.append(i) + + meta.num_stack = len(stack_values) + + cell_and_freevars = set(tx.cellvars() + tx.freevars()) + + # NB: Typically (i.e., for graph compile from RETURN_VALUE), + # symbolic_locals will be empty at this point, as prune_dead_locals + # will clear out all of symbolic_locals because RETURN_VALUE is the + # last instruction and no more locals are used. The fanciness here + # is only needed for partial graphs. + # NOTE: All cell and free variables are represented as CellVariable, + # so checks for NULLs and context managers in the case of codegen'ing resume + # functions will not be performed on them. This is expected behavior. + for k, v in tx.symbolic_locals.items(): + # Do not reconstruct WithExitFunctionVariable! + VariableTracker.visit(ctx_exit_check, v) + # Note! this explicitly uses .local_name for matching + # Failure to do so will cause spurious registrations in val_to_names. + # This will in turn result in spurious variables showing up in the graph. + # This was very tricky to debug. For an example, dump the graph at call_user_compiler + # while running test_subgraphs.py + # Do not include top-frame unmodified locals here - otherwise, the compiled graph may + # erroneously include them as part of the return. We manually codegen them afterward. + if ( + isinstance(v.source, LocalSource) + and v.source.local_name == k + and tx is self.root_tx + ): + continue + # Do not load cell/free vars + if k in cell_and_freevars: + continue + # Do not load variable if it is NULL. + if sys.version_info >= (3, 12): + # NOTE: do not use isinstance, since it realizes lazy VT's + # Continuation function will load the NULL for v. + if type.__instancecheck__(NullVariable, v): + meta.locals_null_keys.append(k) + continue + else: + # A variable should never be NULL in < 3.12 + assert not type.__instancecheck__(NullVariable, v) + meta.locals_names[k] = len(meta.locals_names) + if isinstance(v, ContextWrappingVariable): + target_values = ( + () if v.target_values is None else tuple(v.target_values) + ) + meta.locals_ctx_args.append((k, target_values)) + stack_values.append(v) + + return stack_values, meta + + def compile_subgraph( + self, + tx: "InstructionTranslatorBase", + reason: GraphCompileReason, + stack_pops: int = 0, + ) -> list[StackLocalsMetadata]: + """ + Compiles the current subgraph, with inputs w.r.t. self.root_tx, and codegens: + - Call the compiled subgraph + - Apply side effects + - Codegen stack and locals + - Store the locals + + Python does not allow NULL to be an arg to a function, so we do not codegen NULLs on the stack, + unless the value is one of the top `stack_pops` values on the stack (these values are expected to be + popped immediately after this generated code. The prologue of the resume function is expected to restore + any dropped NULLs. + + Returns stack indices and locals keys where we dropped NULLs, and where we found inactive context manager objects. + """ + + assert self.root_tx is not None + + if not config.nested_graph_breaks: + # expect to only compile 1 frame + assert self.root_tx is tx + + # bytecode tracing has finished. Pop the context manager for dynamo_timed + self.mark_bytecode_tracing_stop() + + self.compile_subgraph_reason = reason + self.should_exit = True + + log.debug("COMPILING GRAPH due to %s", reason) + + # prefix instructions (Python 3.11+) + prefix_insts: list[Instruction] = [] + if sys.version_info >= (3, 11): + for inst in self.root_tx.prefix_insts: + if inst.opname == "COPY_FREE_VARS": + prefix_insts.append( + create_instruction( + "COPY_FREE_VARS", + arg=len(self.root_tx.code_options["co_freevars"]), + ) + ) + else: + prefix_insts.append(copy.copy(inst)) + + # stack values and restore vars for each frame are pushed in reverse order + # i.e. last element corresponds to root frame (1), + # first element corresponds to current frame (N) + all_stack_values = [] + # pyrefly: ignore [implicit-any] + all_stack_locals_metas = [] + cur_tx: InstructionTranslatorBase | None = tx + while cur_tx is not None: + # this should have been checked by the caller + assert all(block.can_restore() for block in cur_tx.block_stack) + + stack_values, meta = self._get_stack_values_to_restore( + cur_tx, stack_pops if cur_tx is tx else 0 + ) + all_stack_values.append(stack_values) + all_stack_locals_metas.append(meta) + + # Exit from all context manager variables to make sure global state is restored + for block in reversed(cur_tx.block_stack): + block.exit(cur_tx, is_graph_break=reason.graph_break) + + cur_tx = cur_tx.parent + + # "Garbage collect the heap". + self.side_effects.prune_dead_object_new(tx) + + self.add_output_instructions(prefix_insts) + + if self._emit_debugger_breakpoint: + from .bytecode_transformation import create_breakpoint + + self.add_output_instructions(create_breakpoint()) + self._emit_debugger_breakpoint = False + + assert not (self.pregraph_bytecode and self.export), ( + "export does not support pregraph_bytecode" + ) + self.add_output_instructions(self.pregraph_bytecode) + + alias_insts, overridden_sources = self.handle_aliases_for_stolen_lists( + self.root_tx + ) + self.add_output_instructions(alias_insts) + + self.cleanup_graph() + + # Use nn.Module "proxies" in the constructed GraphModule so that + # the resulting GM does not hold additional strong references to the original modules. + # This prevents a strong ref cycle where Dynamo created code holds on to references + # to modules that also have Dynamo code cache invalidation checks. + # When cache invalidation runs, the generated GM will be invalidated, which also deletes + # the proxies. + nn_modules_proxies = { + name: nn_module_proxy(mod) for name, mod in self.nn_modules.items() + } + root = FakeRootModule(nn_modules_proxies) + + from .decorators import disable + + # to handle random calls + if len(self.random_calls) > 0: + random_calls_instructions = [] + self.random_values_var = self.new_var("random_values") + rand_fn = disable( + _get_gen_rand_values_fn(self.random_calls), + reason="do not trace into Dynamo rng recovery function", + ) + rand_fn_name = self.install_global("__gen_rand_values", rand_fn) + codegen = PyCodegen( + self.root_tx, root, overridden_sources=overridden_sources + ) + random_calls_instructions.extend( + codegen.load_function_name(rand_fn_name, True) + ) + random_calls_instructions.extend(create_call_function(0, False)) + random_calls_instructions.append( + codegen.create_store(self.random_values_var), + ) + self.add_output_instructions(random_calls_instructions) + + # Codegen stack convention before the unsupported instruction + # NOTE: in these comment blocks, "locals" EXCLUDE free and cell vars. + # NOTE: stack/locals/cells must be codegen'd BEFORE the unsupported instruction, since the latter + # can arbitrarily mutate the former. + # [frame N cells, .., frame 1 cells], + # [ + # frame N locals, + # frame N-1 stack + locals, + # ..., + # frame 1 stack + locals, + # ], frame N stack + + # see symbolic_convert.py for + # codegen stack convention after the unsupported instruction + # NOTE: cells will be loaded into continuation functions directly by symbolic_convert + + # this determines the order that values are codegen'd to the stack + stack_values_flat = [val for vals in all_stack_values for val in vals] + stored_graph_output_var = False + graph_output_var = None + + # call compiled fx graph and codegen all values - stack and locals + if ( + self.root_tx is tx # single frame + and stack_values_flat + and all( + not isinstance( + v, + ( + UnspecializedPythonVariable, + NumpyNdarrayVariable, + TensorWithTFOverrideVariable, + ), + ) + and not (isinstance(v, SymNodeVariable) and v.python_type() is float) + for v in stack_values_flat + ) + and all(x.is_tensor() for x in stack_values_flat) + and len(set(stack_values_flat)) == len(stack_values_flat) + and self.side_effects.is_empty() + and not tx.debug_locals + and not self.backward_state + and not all_stack_locals_metas[-1].stack_null_idxes + and not all_stack_locals_metas[-1].locals_null_keys + ): + # optimization to generate better code in a common case + + # codegen cells + # no side effects, so no new cells created - no need to call side_effects.codegen_save_tempvars + cell_cg = PyCodegen(self.root_tx) + self.codegen_cells(tx, cell_cg) + self.add_output_instructions( + [ + # load in reverse since UNPACK_SEQUENCE will reverse + *self.compile_and_call_fx_graph( + tx, list(reversed(stack_values_flat)), root + ), + *cell_cg.get_instructions(), + *create_swap(2), + create_instruction("UNPACK_SEQUENCE", arg=len(stack_values_flat)), + ] + ) + # function output will be moved to the correct places below + else: + graph_output_var = self.new_var("graph_out") + # load stack values in a flat manner - we will codegen bytecode to place them correctly + # according to our convention above + pass1 = PyCodegen( + self.root_tx, + root, + graph_output_var, + overridden_sources=overridden_sources, + ) + self.codegen_suffix(tx, stack_values_flat, pass1, False) + + # Use `pass1.uses` to selectively cache multi-user variables into a + # temporary local source. This (a). speeds up loading VTs with long + # chained source, and (b). avoids redundantly saving single-user VT + # into a temporary local. + tempvars = {} # type: ignore[var-annotated] + for val, count in pass1.uses.items(): + # If it's already a local source, no need to cache it + if count > 1 and not istype(val, (SyntheticLocalSource, LocalSource)): + tempvars[val] = None + pass2 = PyCodegen( + self.root_tx, + root, + graph_output_var, + tempvars=tempvars, + overridden_sources=overridden_sources, + ) + self.codegen_suffix(tx, stack_values_flat, pass2, True) + + if ( + torch._dynamo.config.log_graph_in_out_metadata + and stack_values_flat + and len(stack_values_flat) == 1 + ): + vt = stack_values_flat[0] + if ( + isinstance(vt, torch._dynamo.variables.NamedTupleVariable) + and vt.tuple_cls + is torch._dynamo.functional_export.ExportTracerOutput + ): + flat_returns = vt.items[0] + out_spec = vt.items[1] + assert isinstance( + flat_returns, torch._dynamo.variables.ListVariable + ) + + vt_to_graph_out_idx: dict[VariableTracker, int] = {} + for value in pass2.graph_outputs.values(): + assert isinstance(value, torch._dynamo.codegen.GraphOutputEntry) + variable: VariableTracker = value.variable + vt_to_graph_out_idx[variable] = value.index + + for idx, vt in enumerate(flat_returns.items): + if vt in vt_to_graph_out_idx: + self.export_metadata.output_return_type[idx] = ( + "graph_out", + vt_to_graph_out_idx[vt], + ) + elif ( + vt.source is not None + and (source := getattr(vt.source, "base", None)) # type: ignore[assignment] + and getattr(source, "is_input", False) + ): + self.export_metadata.output_return_type[idx] = ( + "input", + vt.source, + ) + elif vt.is_python_constant(): + self.export_metadata.output_return_type[idx] = ( + "constant", + vt.as_python_constant(), + ) + else: + raise AssertionError( + f"Encountered unrecognized type {vt} at output {idx}" + ) + try: + self.export_metadata.out_spec = out_spec.as_python_constant() + except ClosureConversionError as e: + unimplemented( + gb_type="nested function with non-constructible closure in output", + context=f"as_python_constant for out_spec {out_spec}", + explanation=( + "Cannot return a nested function with closure from a compiled function. " + "Dynamo failed to construct the function defined in the compiled region with closure objects." + ), + hints=[ + "Define the function at module scope instead of inside another function ", + "Ensure that all closure variables are constants.", + ], + from_exc=e, + ) + + output = [] + if count_calls(self.graph) != 0 or len(pass2.graph_outputs) != 0: + output.extend( + self.compile_and_call_fx_graph(tx, pass2.graph_output_vars(), root) + ) + + if len(pass2.graph_outputs) != 0: + output.append(pass2.create_store(graph_output_var)) + stored_graph_output_var = True + else: + output.append(create_instruction("POP_TOP")) + else: + # NB: Important to run compiler collective even when there is + # a graph break + self.run_compiler_collective() + self.add_output_instructions(output + pass2.get_instructions()) + + # store all stack and locals for each frame + # current state of the stack: + # all cells, + # *(frame N stack), *(frame N locals), + # ..., + # *(frame 1 stack), *(frame 1 locals) + + self.add_output_instructions( + [ + create_instruction( + "BUILD_LIST", + arg=len(stack_values_flat) - all_stack_locals_metas[0].num_stack, + ), + ] + ) + + # current state of the stack: + # all cells, + # *(frame N stack), [ + # *(frame N locals), + # *(frame N-1 stack), *(frame N-1 locals), + # ... + # *(frame 1 stack), *(frame 1 locals), + # ] + # iterate current frame (N) to root frame (1) + # sliding window over frame stack/locals + start_idx = 0 + end_idx = 0 + for i, meta in enumerate(all_stack_locals_metas): + # do not pack frame N's stack into the value list + n_vals = len(meta.locals_names) + if i != 0: + n_vals += meta.num_stack + if n_vals == 0: + self.add_output_instructions( + [ + create_instruction("BUILD_LIST", arg=0), + *create_swap(2), + ] + ) + # [], stack_values_flat + else: + end_idx += n_vals + self.add_output_instructions( + [ + create_dup_top(), + *create_binary_slice(start_idx, end_idx), + *create_swap(2), + ] + ) + start_idx += n_vals + # stack_values_flat[x:y], stack_values_flat + + # add root frame's unmodified locals here + if i == len(all_stack_locals_metas) - 1: + root_cg = PyCodegen(self.root_tx) + unmodified_locals_names: dict[str, int] = {} + for k, v in self.root_tx.symbolic_locals.items(): + if isinstance(v.source, LocalSource) and v.source.local_name == k: + root_cg.append_output(root_cg.create_load(k)) + unmodified_locals_names[k] = len(meta.locals_names) + len( + unmodified_locals_names + ) + self.add_output_instructions( + root_cg.get_instructions() + + [ + create_instruction( + "BUILD_LIST", arg=len(unmodified_locals_names) + ), + # arg=2 because we already swapped the locals list back + create_instruction("LIST_EXTEND", arg=2), + ] + ) + meta.locals_names.update(unmodified_locals_names) + + # *(frame N stack), metas[0] stack + locals, ..., metas[i] stack + locals, stack_values_flat + + # current state of the stack: + # all cells, + # *(frame N stack), + # frame N locals, + # frame N-1 stack, frame N-1 locals, + # ... + # frame 1 stack, frame 1 locals, + # stack_values_flat + # + + self.add_output_instructions( + [ + create_instruction("POP_TOP"), + create_instruction("BUILD_LIST", arg=len(all_stack_locals_metas)), + *create_rot_n(all_stack_locals_metas[0].num_stack + 1), + ] + ) + + # final state of the stack before running the unsupported bytecode: + # all cells, + # [ + # [frame N locals], + # [frame N-1 stack + locals], + # ..., + # [frame 1 stack + locals], + # ], *(frame N stack) + + if graph_output_var and stored_graph_output_var: + self.add_output_instructions( + [create_instruction("DELETE_FAST", argval=graph_output_var)] + ) + + if torch._dynamo.config.side_effect_replay_policy in ["warn", "error"]: + side_effect_refs = self.get_replayed_side_effect_source_refs( + populate_export_metadata=True + ) + if side_effect_refs: + if torch._dynamo.config.side_effect_replay_policy == "warn": + warnings.warn( + f"While compiling, we found certain side effects happened in the model.forward. " + f"Here are the list of potential sources you can double check: {side_effect_refs}" + ) + else: + raise RuntimeError( + f"While compiling, we found certain side effects happened in the model.forward. " + f"Here are the list of potential sources you can double check: {side_effect_refs}" + ) + + return all_stack_locals_metas + + def codegen_cells(self, tx: "InstructionTranslatorBase", cg: PyCodegen) -> None: + # no need to codegen if reason.graph_break is False (since we won't resume) + if self.compile_subgraph_reason.graph_break: + tx_cnt = 0 + cur_tx: InstructionTranslatorBase | None = tx + while cur_tx is not None: + # NOTE: we generate cells in the same order as resume_execution.py: sorted freevars + cellvars + # Emitting `LOAD_FAST/LOAD_CLOSURE` with names in `co_freevars` + # requires that in the generated bytecode, these cells would keep + # their original local names, which we ensure via + # `CellVariable.local_name`. + freevars = tuple(sorted(cur_tx.cell_and_freevars())) + for cell in freevars: + if cur_tx is self.root_tx: # root frame + cg.append_output(cg.create_load_closure(cell)) + else: # nested frame + assert cur_tx.post_prune_cell_and_freevars + cg(cur_tx.post_prune_cell_and_freevars[cell]) + cg.append_output(create_build_tuple(len(freevars))) + cur_tx = cur_tx.parent + tx_cnt += 1 + cg.append_output(create_instruction("BUILD_LIST", arg=tx_cnt)) + else: + cg.append_output(create_instruction("BUILD_LIST", arg=0)) + + def codegen_suffix( + self, + tx: "InstructionTranslatorBase", + stack_values: list[VariableTracker], + cg: PyCodegen, + log_side_effects: bool, + ) -> None: + # NOTE: `codegen_save_tempvars` must run first to update `source` fields + # for variables with `AttributeMutationNew`, as they don't implement + # `reconstruct` themselves. + self.side_effects.codegen_save_tempvars(cg) + if self.backward_state: + assert not self.export + for name, val in self.backward_state.items(): + cg(val) + assert self.backward_state_var is not None + cg.append_output(cg.create_load(self.backward_state_var)) + cg.store_attr(name) + if config.replay_side_effects: + self.side_effects.codegen_hooks(cg) + + # TODO get debug_locals working for nested graph breaks + # Return variables used for logging at the end + for debug_var, args in tx.debug_locals: + cg.add_push_null(lambda: cg(debug_var)) + for arg in args: + cg(arg) + cg.extend_output(create_call_function(len(args), False)) + cg.extend_output([create_instruction("POP_TOP")]) + + # codegen cells before we apply side effects + self.codegen_cells(tx, cg) + + cg.restore_stack(stack_values, value_from_source=not tx.export) + self.side_effects.codegen_update_mutated(cg, log_side_effects) + + def cleanup_graph(self) -> None: + """ + Remove "creation_timestamp" from node meta + + Remove this pattern from the graph: + torch._C._set_grad_enabled(False) + torch._C._set_grad_enabled(True) + """ + assert self.should_exit + nodes = list(self.graph.nodes) + for node in nodes: + node.meta.pop("creation_timestamp", None) + + grad_enabled = torch.is_grad_enabled() + for node1, node2 in itertools.pairwise(nodes): + if ( + node1.target is torch._C._set_grad_enabled + and tuple(node1.args) == (not grad_enabled,) + and not node1._erased + ): + grad_enabled = node1.args[0] + if ( + node2.target is torch._C._set_grad_enabled + and tuple(node2.args) == (not grad_enabled,) + and not node2._erased + ): + grad_enabled = node2.args[0] + self.graph.erase_node(node1) + self.graph.erase_node(node2) + + def bypass_package(self, reason: str = "", **kwargs: Any) -> None: + """ + Do not save this output graph to the CompilePackage + """ + if not self.package: + return + if torch._dynamo.config.strict_precompile: + raise torch._dynamo.exc.PackageError( + "Detected a package bypass: %s", reason + ) + log.warning("Detected a package bypass: %s", reason) + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "precompile_cache_bypass", + "encoding": "json", + }, + payload_fn=lambda: { + # precede with underscore so it always appear first in JSON in tlparse + "_reason": reason, + **kwargs, + }, + ) + self.package.bypass_current_entry() + self.package = None + + def get_graph_sizes_structured(self) -> dict[str, list[int | str]]: + ret: dict[str, list[int | str]] = {} + for node in self.graph.nodes: + example_value = node.meta.get("example_value", None) + if isinstance(example_value, torch._subclasses.FakeTensor): + size = example_value.shape + ret[node.name] = [s if isinstance(s, int) else repr(s) for s in size] + return ret + + def get_graph_sizes(self, name: str) -> str: + graph_sizes_str = "TRACED GRAPH TENSOR SIZES\n" + graph_sizes_str += f"===== {name} =====\n" + for node in self.graph.nodes: + example_value = node.meta.get("example_value", None) + if isinstance(example_value, torch._subclasses.FakeTensor): + size = example_value.shape + graph_sizes_str += f"{node.name}: {tuple(size)}\n" + concrete_size = [] + has_symint = False + for sz in size: + if isinstance(sz, int): + concrete_size.append(sz) + elif isinstance(sz, torch.SymInt): + has_symint = True + concrete_size.append(sz.node.hint) + else: + break + else: + if has_symint: + graph_sizes_str += ( + f"{node.name} (concrete): {tuple(concrete_size)}\n" + ) + return graph_sizes_str + + @contextlib.contextmanager + def restore_global_state(self) -> Any: + """ + Momentarily restores the global state to what it was prior to tracing the current output + """ + prior_global_state = self.tracing_context.global_context.copy_graphstate() + current_global_state: dict[str, tuple[Any, bool]] = {} + self.save_global_state(out=current_global_state) + try: + # Set to state prior to tracing the graph + self.tracing_context.global_context.restore_graphstate(prior_global_state) + yield + finally: + # Reset to state at the current time (e.g. before calling the user compiler) + self.tracing_context.global_context.restore_graphstate( + GlobalContextCheckpointState(current_global_state) + ) + + def run_compiler_collective(self) -> None: + tx = self.root_tx + assert tx is not None + if (ds := tx.distributed_state) is not None and ds.all_states is None: + compile_pg = ds.compile_pg + + log.info("compiler_collective %s", ds.local_state) + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "compiler_collective", + "encoding": "string", + }, + payload_fn=lambda: ds.local_state.render(), + ) + device_types = compile_pg._device_types + assert len(device_types) == 1, ( + "Expect only one device type but got {}".format("+".join(device_types)) + ) + with ( + get_interface_for_device(device_types.pop()).device( # type: ignore[attr-defined] + compile_pg.rank() % torch.accelerator.device_count() + ), + dynamo_timed("compiler_collective", log_pt2_compile_event=True), + ): + all_states: list[Any] = [None] * compile_pg.size() + + dist.all_gather_object(all_states, ds.local_state, group=compile_pg) + + ds.all_states = all_states + # Clear speculation log, because are tracing may diverge due to + # this information from the compiler collective + tx.speculation_log.clear() + raise exc.CompileCollectiveRestartAnalysis + + def _validate_outputs_safe_for_autograd_nodes( + self, rv: list["VariableTracker"], tx: "InstructionTranslatorBase" + ) -> None: + """ + Validate that if torch.autograd.grad is used in the graph and outputs + require grad, we trigger AutogradGradRestartAnalysis only if the output is connected + to the autograd.grad computation. + + rv here refers to list of variables that are being returned from dynamo graph. + + See Note [Tracing autograd.grad in dynamo] + """ + if not self.autograd_grad_consumed_grad_fns: + return + + from .variables.tensor import TensorVariable + + for var in rv: + if not isinstance(var, TensorVariable) or not var.requires_grad: + continue + + fake_tensor = var.as_proxy().node.meta.get("example_value") + assert isinstance(fake_tensor, torch._subclasses.fake_tensor.FakeTensor) + if fake_tensor.grad_fn is None: + continue + + # Traverse the entire autograd graph of the returned tensor to check + # if any node was consumed by autograd.grad + reachable_grad_fns = collect_reachable_grad_fns([(fake_tensor, None)]) + if reachable_grad_fns & self.autograd_grad_consumed_grad_fns: + # Record info about the leaked tensor for the error message + tensor_name = str(var.source) if var.source else var.proxy.node.name + tx.speculation_log.autograd_grad_leaked_tensors.append(tensor_name) + + if tx.speculation_log.autograd_grad_leaked_tensors: + # Set the flag to graph break at autograd.grad on retry + tx.speculation_log.graph_break_on_autograd_grad = True + raise exc.AutogradGradRestartAnalysis( + restart_reason="autograd.grad consumed grad_fns of returned tensors" + ) + + def _check_requires_grad_intermediate_outputs( + self, rv: list["VariableTracker"], tx: "InstructionTranslatorBase" + ) -> None: + """Skip frame if a source-less requires_grad_() intermediate leaks as output. + + AOTAutograd's functionalization drops requires_grad_() on intermediates, + so returning them (or tensors derived from them) produces wrong results. + We detect this via FX graph reachability: find the requires_grad_() nodes + for source-less intermediates, then check if any output is downstream. + """ + from .variables.tensor import TensorVariable + + # Collect FX nodes for source-less requires_grad_() intermediates + tainted_nodes: set[torch.fx.Node] = set() + for v in self.leaf_var_creation_order: + if isinstance(v, TensorVariable) and not v.source: + tainted_nodes.add(v.as_proxy().node) + + if not tainted_nodes: + return + + # Propagate taint forward through the FX graph + for node in self.graph.nodes: + if node in tainted_nodes: + continue + if any(inp in tainted_nodes for inp in node.all_input_nodes): + tainted_nodes.add(node) + + # Check leaked outputs: tainted + requires_grad means the output + # carries autograd state that AOTAutograd would silently drop. + # Detached outputs (requires_grad=False) are fine — no autograd to lose. + for var in rv: + if ( + isinstance(var, TensorVariable) + and var.requires_grad + and var.as_proxy().node in tainted_nodes + ): + msg = ( + "An intermediate tensor that had requires_grad_() called " + "on it (or a tensor derived from it) is being returned " + "from the compiled region. AOTAutograd's functionalization " + "drops the requires_grad_() effect on graph outputs, " + "producing wrong results. If you only need the tensor " + "values without gradients, call .detach() before returning." + ) + if tx.one_graph: + unimplemented( + gb_type="returning intermediate with requires_grad_()", + context="graph output depends on source-less requires_grad_()", + explanation=msg, + hints=[ + "If you only need the tensor values without gradients, " + "call .detach() before returning.", + "Consume the gradient inside the compiled function " + "(call backward() and use .grad), " + "or move requires_grad_() outside torch.compile.", + ], + ) + else: + tx.speculation_log.graph_break_on_requires_grad_ = True + raise exc.RequiresGradRestartAnalysis( + restart_reason="source-less requires_grad_() intermediate leaked as output" + ) + + def compile_and_call_fx_graph( + self, + tx: "InstructionTranslatorBase", + rv: list[VariableTracker], + root: FakeRootModule, + ) -> list[Instruction]: + """ + Generate code from self.graph and return the Instruction()s to + call that generated code. + + Code is generated w.r.t. self.root_tx. + tx is only used for preserving GraphModule metadata + """ + with torch._guards.TracingContext.clear_frame(): + from .decorators import disable + + assert self.should_exit + + self.run_compiler_collective() + if count_calls(self.graph) == 0 and len(rv) == 0: + return [] + + name = unique_id("__compiled_fn", with_uuid=True) + + assert isinstance(rv, list) + assert isinstance(root, FakeRootModule) + + # Error on source-less requires_grad_() outputs. + # Must run before autograd validation since detaching resolves the + # "consumed grad_fn" conflict for backward-consumed intermediates. + self._check_requires_grad_intermediate_outputs(rv, tx) + + # Check if autograd.grad is used with outputs that require grad + # This would cause double backward issues in aot_autograd + self._validate_outputs_safe_for_autograd_nodes(rv, tx) + + output_node = self.create_node( + "output", + "output", + (self.current_tracer.create_arg(tuple(x.as_proxy() for x in rv)),), + {}, + ) + sub_gms = self.dedup_pass() + root.add_nn_modules(sub_gms) # type: ignore[arg-type] + + self.current_tracer._maybe_preserve_original_meta(tx, output_node) + if not config.do_not_emit_runtime_asserts: + # There is a rare scenario where codegen_suffix adds a new entry + # to self.nn_modules while `root` knows only about the + # nn_modules at the time of its creation. This causes failures + # while creating the graph module because self.graph and root + # are out of sync. This only happens for `get_attr` nodes, so + # here we clean up the get_attr nodes that are unused. + with dynamo_timed("insert_deferred_runtime_asserts"): + for attr in dir(root): + subgraph = getattr(root, attr) + if isinstance(subgraph, fx.GraphModule): + insert_deferred_runtime_asserts( + subgraph, + self.shape_env, + name, + export=self.export, + ) + self.remove_unused_get_attr_nodes() + insert_deferred_runtime_asserts( + fx.GraphModule(root, self.graph), + self.shape_env, + name, + export=self.export, + ) + # NB: deferred runtime asserts can keep graphargs live, so make sure + # those are inserted before pruning + self.remove_unused_graphargs() + ncalls = count_calls(self.graph) + counters["stats"]["calls_captured"] += ncalls + + self.remove_tensorify_specialized_graphargs() + + # free a bit of memory + self.real_value_cache.clear() + + gm = _make_graph_module(root, self.graph) + + from .dce_extra_outputs import dce_hop_extra_outputs + + dce_hop_extra_outputs(gm) + + # Saved tensors hooks are not used by the graph. + # GraphModule by default only copies used in the graph submodules. + # Copying them into the result graph manually. + if self.saved_tensors_hooks_subgraph_names: + for subgraph_name in self.saved_tensors_hooks_subgraph_names: + setattr(gm, subgraph_name, getattr(root, subgraph_name)) + + for register_finalizer in self.register_finalizer_fns: + register_finalizer(gm) + + if next(gm.parameters(), None) is not None: + # If dynamo produces a graph with parameters, skip package stuff + # Bypass output graph + self.bypass_package( + "Graph contains named parameters due to static addresses.", + gm=gm.print_readable( + print_output=False, include_stride=True, include_device=True + ), + ) + + if self.package is not None: + gm._backend_id = name # pyrefly: ignore[bad-argument-type] + + # pyrefly: ignore[bad-argument-type] + gm.compile_subgraph_reason = self.compile_subgraph_reason + # pyrefly: ignore[bad-argument-type] + gm.meta["dynamo_flat_name_to_original_fqn"] = ( + self.dynamo_flat_name_to_original_fqn.copy() + ) + gm.meta["dynamo_compile_id"] = self.dynamo_compile_id + gm.meta["backend_id"] = name + + if self.cudagraph_annotation is not None: + gm.meta["cudagraph_annotation"] = self.cudagraph_annotation + + graph_code_log.debug( + "%s", + lazy_format_graph_code( + name, gm, include_stride=True, include_device=True, colored=True + ), + ) + torch._logging.trace_structured( + "dynamo_output_graph", + lambda: {"sizes": self.get_graph_sizes_structured()}, + payload_fn=lambda: gm.print_readable( + print_output=False, include_stride=True, include_device=True + ), + ) + self.call_cleanup_hooks() + old_fake_mode = self.tracing_context.fake_mode + assert old_fake_mode is not None + # Store old_fake_mode so it can be cleared at end of compile + self._old_fake_mode = old_fake_mode + if not self.export: + import torch._functorch.config as _config + + with _config.patch(fake_tensor_allow_unsafe_data_ptr_access=False): + # TODO(voz): The way export uses gm, and fake tensors, is not supported with us resetting + + # Why create a new FakeTensorMode? + # + # The reason this needs to be done is because when we do Dynamo tracing, fake + # tensors can have their metadata mutated. Thus, the fake tensor we allocated + # for any given tensor may no longer be valid for the beginning trace of the + # graph. Nor is it convenient to "clone" the input tensors before mutating them, + # since you have to preserve aliasing. So we just reconstruct the FakeTensorMode + # from scratch when we go to AOTAutograd. But the ShapeEnv must be preserved as + # Dynamo made decisions about what is dynamic or not / guards from the user code + # that is not in graph. + backend_fake_mode = torch._subclasses.FakeTensorMode( + shape_env=old_fake_mode.shape_env, + ) + # TODO(voz): Ostensibly, this should be scoped and + # restore back to old_fake_mode, but doing so currently violates + # a lot of fake_tensor ownership assumptions and runs afoul of detect_fake_mode + self.tracing_context.fake_mode = backend_fake_mode + + with self.restore_global_state(): + compiled_fn = self.call_user_compiler(gm, self.example_inputs()) + + from torch.fx._lazy_graph_module import _LazyGraphModule + + if isinstance(compiled_fn, _LazyGraphModule) or ( + isinstance(getattr(compiled_fn, "__self__", None), _LazyGraphModule) + and compiled_fn.__name__ == "_lazy_forward" # type: ignore[attr-defined] + ): + # Since dynamo will run the forward method for the GraphModule shortly + # anyways, it does not hurt to do the real recompilation here if + # this is a _LazyGraphModule. This makes it easier for dynamo to + # optimize a _LazyGraphModule. + + lazy_gm = ( + compiled_fn + if isinstance(compiled_fn, _LazyGraphModule) + else compiled_fn.__self__ # type: ignore[attr-defined] + ) + + _LazyGraphModule.force_recompile(lazy_gm) + + if not isinstance(compiled_fn, _LazyGraphModule): + # replace compiled_fn with the real forward method + compiled_fn = lazy_gm.forward + + if self.package is not None: + self.package.add_backend_id(name, compiled_fn) + + # If __torch_function__ subclass dispatch was inlined during + # tracing, wrap the compiled graph to disable __torch_function__ + # at runtime, preventing double dispatch (the C++ dispatcher + # would otherwise re-trigger __torch_function__ on subclass + # inputs that the graph already handles). + if self.torch_function_subclass_inlined: + real_compiled_fn = compiled_fn + + def _tf_disabled_wrapper(*args, **kwargs): + with torch._C.DisableTorchFunctionSubclass(): + return real_compiled_fn(*args, **kwargs) + + compiled_fn = _tf_disabled_wrapper + + compiled_fn = disable( + compiled_fn, reason="do not trace Dynamo-compiled graph" + ) + + counters["stats"]["unique_graphs"] += 1 + assert old_fake_mode.shape_env is not None + if specializations := old_fake_mode.shape_env.specializations: + specialization_guards = [] + specialization_cache: dict[Specialization, Callable[[Any], Any]] = {} + sources = [a.source for a in self.graphargs] + for specialization in specializations: + source_index = sources.index(specialization.source) + check_fn_source = inspect.getsource(specialization.check_fn).strip() + # Required because the LABDA_GUARD API requires a root guard manager + unused_root_guard_manager = RootGuardManager() + check_fn = guards.LAMBDA_GUARD( # type: ignore[attr-defined] + unused_root_guard_manager, + specialization.check_fn, + [check_fn_source], + None, # user_stack + ) + + log.debug( + "Compiling backend specialized graph with specialization=%s", + check_fn_source, + ) + + specialization_guards.append( + ( + functools.partial( + lambda idx, args, check_fn=check_fn: check_fn( + args[idx] + ), + source_index, + ), + specialization, + ) + ) + + @torch._dynamo.disable(reason="do not trace Dynamo-compiled graph") # type: ignore[misc] + def specialized_dispatch(*args: Any, **kwargs: Any) -> Any: + for check_fn, specialization in specialization_guards: + if check_fn(args): + if specialization in specialization_cache: + return specialization_cache[specialization]( + *args, **kwargs + ) + + with self.shape_env.patch_source_specialization( + specialization.source, specialization.check_fn + ): + # Modify gm so AOTAutogradCache key changes per specialization + gm.meta["specialization"] = specialization + example_inputs: list[Tensor] = list(args) + with tracing(self.tracing_context): + specialization_cache[specialization] = ( + self.call_user_compiler(gm, example_inputs) + ) + + return specialization_cache[specialization](*args, **kwargs) + return compiled_fn(*args, **kwargs) + + # This is safe because we pre-process name to be unique + self.install_global_unsafe(name, specialized_dispatch) + else: + # This is safe because we pre-process name to be unique + self.install_global_unsafe(name, compiled_fn) + + assert self.root_tx is not None + cg = PyCodegen(self.root_tx) + + if has_user_objects(): + # NB: This is where we store possible user objects before running the graph + # index_to_user_object_weakref is the function used in the graph to translate + # the dynamo-generated index into the actual object passed to the compiled function. + # We generate bytecode to store all user objects at the proper index in the below + # call. + cg.add_push_null( + lambda: cg.load_import_from( + torch._dynamo.graph_bytecode_inputs.__name__, + "store_user_object_weakrefs", + ) + ) + + tmp_vars = [] + for constructor in index_to_bytecode_constructor.values(): + constructor(cg) + var_name = ( + self.new_var() + ) # keep alive any user objects for the rest of the frame + # TODO: we could omit this for objects we create but shouldn't be too much overhead for now + cg.store(var_name) + tmp_vars.append(var_name) + + for var_name in tmp_vars: + cg.append_output(cg.create_load(var_name)) + + cg.call_function(len(index_to_bytecode_constructor), False) + cg.pop_top() + + for idx, arg in enumerate(self.graphargs): + self.export_metadata.graph_input_idx_to_local_source[idx] = arg.source + + cg.make_call_generated_code(name) + return cg.get_instructions() + + @property + def placeholders(self) -> list[fx.Node]: + return self.graph.find_nodes(op="placeholder") + + @property + def graphargs(self) -> list[GraphArg]: + return [node.meta["grapharg"] for node in self.placeholders] + + def call_user_compiler( + self, gm: fx.GraphModule, example_inputs: list[Tensor] + ) -> CompiledFn: + with dynamo_timed( + "OutputGraph.call_user_compiler", + phase_name="backend_compile", + log_pt2_compile_event=True, + log_waitcounter=True, + waitcounter_name_override="compile_aot_autograd", + dynamo_compile_column_us="aot_autograd_cumulative_compile_time_us", + ): + return self._call_user_compiler(gm, example_inputs) + + def _call_user_compiler( + self, gm: fx.GraphModule, example_inputs: list[Tensor] + ) -> CompiledFn: + assert self.compiler_fn is not None + tot = 0 + placeholders = [] + for node in gm.graph.nodes: + if node.op in ("call_function", "call_method", "call_module"): + tot += 1 + if node.op == "placeholder": + placeholders.append(node) + increment_op_count(tot) + for pl in placeholders: + if not hasattr(pl, "_dynamo_source"): + arg = pl.meta["grapharg"] + # TODO: Why isn't this stored in meta :think: + # NOTE: can't move these into meta: https://github.com/pytorch/pytorch/issues/141640 + pl._dynamo_source = arg.source + + # NOTE: can't move these into meta: https://github.com/pytorch/pytorch/issues/141640 + gm._param_name_to_source = self.param_name_to_source # type: ignore[assignment] + gm._source_to_user_stacks = self.source_to_user_stacks # type: ignore[assignment] + + # Check for per-graph backend override (for debugging/bisecting) + compiler_fn = ( + get_backend_override_for_compile_id( + self.dynamo_compile_id, config.debug_backend_override + ) + or self.compiler_fn + ) + + # Check for per-graph inductor config override (for debugging/bisecting) + inductor_config_override = get_inductor_config_override_for_compile_id( + self.dynamo_compile_id, config.debug_inductor_config_override + ) + if inductor_config_override: + compiler_fn = _wrap_with_inductor_config( + compiler_fn, inductor_config_override + ) + + name = ( + compiler_fn.__name__ + if hasattr(compiler_fn, "__name__") + else "" + ) + if config.inline_invoke_subgraph: + from torch._higher_order_ops.passes.inline_invoke_subgraph import ( + inline_invoke_subgraph, + ) + + gm = inline_invoke_subgraph(gm) + + try: + _step_logger()(logging.INFO, f"calling compiler function {name}") + if config.verify_correctness: + compiler_fn = WrapperBackend(compiler_fn) + compiled_fn = compiler_fn(gm, example_inputs) + _step_logger()(logging.INFO, f"done compiler function {name}") + assert callable(compiled_fn), "compiler_fn did not return callable" + except (TensorifyScalarRestartAnalysis, ShortenTraceback): + raise + except exceptions_allowed_to_be_fallback as e: + if self.has_user_defined_allowed_in_graph: + raise BackendCompilerFailed( + self.compiler_fn, e, inspect.currentframe() + ).with_traceback(e.__traceback__) from None + unimplemented_with_warning( + e, + self.root_tx.f_code, + gb_type="Backend compiler exception", + context=f"Backend: {name}\nException:{str(e)}\nTraceback:\n{self.root_tx.format_frame_summary()}", + explanation=f"Backend compiler `{name}` failed with {str(e)}. Adding a graph break.", + hints=[ + "Report an issue to the backend compiler repo.", + ], + ) + except SkipFrame: + # The backend compiler has requested that we skip the frame, instead of + # aborting execution. + raise + except Exception as e: + raise BackendCompilerFailed( + self.compiler_fn, e, inspect.currentframe() + ).with_traceback(e.__traceback__) from None + + signpost_event( + "dynamo", + "OutputGraph.call_user_compiler", + { + **self.co_fields, + "op_count": tot, + "node_count": len(gm.graph.nodes), + "input_count": len(placeholders), + }, + ) + + # pyrefly: ignore [bad-return] + return compiled_fn + + def dedup_pass(self) -> dict[str, torch.fx.GraphModule]: + if torch._dynamo.config.use_graph_deduplication: + return apply_graph_deduplication(self) + else: + return {} + + def install_subgraph(self, name: str, sub_gm: torch.fx.GraphModule) -> str: + next_name = get_unique_name_wrt(name, self.nn_modules, requires_suffix=True) + sub_gm.__name__ = next_name # type: ignore[assignment] + sub_gm.torchdynamo_force_dynamic = False # type: ignore[assignment] + # This graph module is not present in the user space, so it can't be + # accessed by a source. Set source=None. + self.register_attr_or_module(sub_gm, next_name, source=None) + return next_name + + def example_inputs(self) -> list[torch.Tensor]: + result = [arg.example for arg in self.graphargs] + # pyrefly: ignore[bad-return] + return result + + def remove_unused_get_attr_nodes(self) -> None: + for node in sorted(self.graph.find_nodes(op="get_attr"), reverse=True): + if len(list(node.users)) == 0: + self.remove_node(node) + + def remove_unused_graphargs(self) -> None: + # NB: It's OK to drop GraphArg for symbols that ended up being + # specialized iff they are not used in runtime assertions. You don't + # even have to make a guard for it, because ShapeEnv produce_guards + # operates on tracked_fakes, which never gets pruned. + # That being said, you'll get marginally better generated + # guard code if you promote the guard into a Dynamo guard (since that + # allows for the guard to be done using C++ guards.) If we get + # ShapeEnv guards to go into C++ guards, this will stop being a thing + # though! + + assert self.should_exit + + # Miniature DCE pass, but only for obviously trivial operations + def is_static_true(b_node: fx.node.Argument) -> bool: + if b_node is True: + return True + if not isinstance(b_node, fx.Node): + return False + b = b_node.meta.get("example_value") + if b is None: + return False + if b is True: + return True + if ( + isinstance(b, torch.SymBool) + and (r := b.node.maybe_as_bool()) is not None + ): + return r + # TODO: We can also technically remove all cases when the input + # doesn't have unbacked inputs, since it's all in the ShapeEnv + return False + + def is_symnode_arg(a: fx.node.Argument) -> bool: + from torch.fx.experimental.sym_node import SymTypes + + if isinstance(a, (int, float, bool)): + return True + if isinstance(a, fx.Node): + return isinstance(a.meta.get("example_value"), SymTypes) + return False + + # NB: We assume that you cannot do mutations on int/float/bool, + # because they are immutable types, and therefore is always safe to + # DCE. + def is_symnode_compute_node(node: fx.Node) -> bool: + from torch.fx.experimental.sym_node import SymTypes + + if node.op != "call_function": + return False + # TODO: I don't think it's possible to have a bare int/float here? + if not isinstance(node.meta.get("example_value"), SymTypes): + return False + # TODO: This will bail here if you ever end up with a more complicated + # computation function, like sum(list_of_ints), even though it + # should be DCE'able + if not all(is_symnode_arg(a) for a in node.args): + return False + if not all(is_symnode_arg(a) for a in node.kwargs.values()): + return False + return True + + from torch.fx.experimental.symbolic_shapes import is_accessor_node + + for node in reversed(list(self.graph.nodes)): + if len(list(node.users)) == 0: + if ( + node.op == "get_attr" + or (node.op == "call_function" and node.target is operator.getitem) + or ( + node.op == "call_function" + and node.target is torch._check + and is_static_true(node.args[0]) + ) + or is_symnode_compute_node(node) + or is_accessor_node(node) + ): + self.remove_node(node) + + def placeholder_binds_symbol(node: fx.Node) -> sympy.Symbol | None: + arg = node.meta["grapharg"] + example = arg.example + if isinstance(example, torch.SymInt) and isinstance( + example.node.expr, sympy.Symbol + ): + return example.node.expr + return None + + def remove_unused(node: fx.Node) -> None: + log.debug("REMOVE UNUSED GRAPHARG %s", node.meta["grapharg"].source.name) + # I'm not really sure why you need to delete these from the + # node since the node is going to get removed + del node.meta["grapharg"] + self.remove_node(node) + self.real_value_cache.pop(node, None) + + used_symbols: set[sympy.Symbol] = set() + + def update_used_symbols( + used_symbols: set[sympy.Symbol], fake: torch.SymInt | torch.Tensor + ) -> None: + used_symbols |= free_symbols(fake) + + recheck_placeholders = [] + for node in self.placeholders: + binds_symbol = placeholder_binds_symbol(node) is not None + # Don't delete symbol bindings yet + if binds_symbol: + if not node.users: + recheck_placeholders.append(node) + else: + if not node.users and not isinstance( + node.meta["grapharg"], BackwardStateGraphArg + ): + remove_unused(node) + else: + # Register the free symbols as uses + arg = node.meta["grapharg"] + if isinstance(arg, BackwardStateGraphArg): + continue + if isinstance(node.meta["grapharg"].example, torch.ScriptObject): + real_script_obj = node.meta["grapharg"].example + fake_script_obj = node.meta["grapharg"].example_strong_ref + if not torch._library.fake_class_registry.tracing_with_real( + real_script_obj + ): + flat_dict = dict(real_script_obj.__obj_flatten__()) # type: ignore[attr-defined] + for attr in flat_dict: + fake_attr_val = getattr( + fake_script_obj.wrapped_obj, attr + ) + pytree.tree_map_only( + (torch.SymInt, torch.Tensor), + lambda t: update_used_symbols(used_symbols, t), + fake_attr_val, + ) + continue + if is_opaque_type(type(node.meta["grapharg"].example)): + continue + fake = ( + arg.fake_tensor if arg.fake_tensor is not None else arg.example + ) + update_used_symbols(used_symbols, fake) + + # After removing unused graphargs, prune unused binds_symbol + for node in recheck_placeholders: + symbol = placeholder_binds_symbol(node) + if symbol is not None: + if symbol not in used_symbols: + remove_unused(node) + else: + # Make sure we delete later occurrences of the same symbol + used_symbols.remove(symbol) + + def remove_tensorify_specialized_graphargs(self) -> None: + # This is a pretty interesting function. Basically we have this problem + # where our compiler tends to choke when we have unused inputs. The way + # we support dynamic float arguments is by doing a joint fx pass and + # tensorifying away as many symfloats as we can. For the remaining symfloats + # we have no choice but to specialize... HOWEVER at that point in time + # we can no longer remove graph inputs. So our sledgehammer solution is to + # save the state of what inputs we should have specialized in dynamo and + # restart analysis. This function incorporates this "view from the future" + # state and specializes inputs that we know we won't be able to tensorify + # away in the joint pass. In principle we shouldn't choke on unused inputs + # and so this shouldn't be necessary. In practice CUDA graphs choke on + # unused inputs so we need this for now. + + # Import here to prevent circular import + from torch._dynamo.symbolic_convert import TensorifyState + + for node in self.graph.nodes: + example_value = node.meta.get("example_value") + if ( + isinstance(example_value, FakeTensor) + and example_value.item_memo is not None + and hasattr(example_value.item_memo.node._expr, "name") + and all(u.target == "item" for u in node.users) + and TensorifyState.should_specialize( + # We use _expr instead of expr b/c we want the symbol not the replacement + example_value.item_memo.node._expr.name + ) + ): + for u in list(node.users): + u.replace_all_uses_with(guard_scalar(example_value.item_memo)) + self.remove_node(u) + self.remove_node(node) + + def add_output_instructions(self, prefix: list[Instruction]) -> None: + """ + We call this on the creation of a new compiled subgraph that is inserted + before user code. + """ + self.output_instructions.extend(prefix) + self.should_exit = True + + def install_resume_function_global( + self, + name: str, + code: types.CodeType, + f_globals: dict[str, Any], + ) -> None: + """Install a resume function as a global. + + When the code has freevars, installs a factory that creates the + function with correct globals and closure (since MAKE_FUNCTION + inherits the current frame's globals, which is wrong for resume + functions from inlined frames). Otherwise installs the function + directly. + """ + if code.co_freevars: + + def _make_fn( + closure: tuple[types.CellType, ...], + ) -> types.FunctionType: + return types.FunctionType(code, f_globals, name, None, closure) + + self.install_global_unsafe(name, _make_fn) + else: + self.install_global_unsafe( + name, + types.FunctionType(code, f_globals, name), + ) + + def install_global_unsafe(self, name: str, value: Any) -> None: + """ + WARNING: prefer the safer `install_global_by_id/install_global`. + torch.compile instances should be independent of each other; + one footgun is to have one instance depend on the existence of + a global installed by another instance. This can happen if we mangle + a global the same way across both instances. + """ + assert name not in self.installed_globals + self.installed_globals.add(name) + self.cleanups.append(CleanupHook.create(self.global_scope, name, value)) + + def install_global_by_id(self, prefix: str, value: Any) -> str: + """ + Installs a global if it hasn't been installed already. + This is determined by (prefix, id(value)) pair. + + Returns the name of the newly installed global. + """ + # NB: need self.compile_id to distinguish this global + # from another global created in a different torch.compile instance + name = f"{prefix}_{id(value)}_c{self.compile_id}" + if name in self.installed_globals: + return name + self.install_global_unsafe(name, value) + return name + + def install_global(self, prefix: str, value: Any) -> str: + """ + Installs a global, generating a unique name for it. + + Returns the name of the newly installed global. + """ + # NB: unique_id is unique, even across torch.compile instances + name = unique_id(prefix) + self.install_global_unsafe(name, value) + return name + + def cleanup(self) -> None: + # There is a reference cycle between tracer and OutputGraph, causing + # some of the tensor objects to be held alive for longer than necessary. + self.root_tx = None # type: ignore[assignment] + self.nn_modules.clear() + self.used_inlined_inbuilt_modules_names.clear() + self.param_name_to_source = None + + for node in self.graph.nodes: + if "grapharg" in node.meta: + del node.meta["grapharg"] + self.real_value_cache.clear() + self.input_name_to_proxy.clear() + self._cached_replayed_side_effect_source_refs = tuple( + self.get_replayed_side_effect_source_refs() + ) + self.side_effects.clear() + self.variable_tracker_cache.clear() + self.mro_source_cache.clear() + self.guarded_mro_absent_keys.clear() + self.signature_cache.clear() + self.register_finalizer_fns.clear() + self.dynamo_flat_name_to_original_fqn.clear() + self.tracing_context.clear() + self.input_source_to_var.clear() + self.leaf_var_creation_order.clear() + self.unspec_variable_map.clear() + self.backward_state.clear() + + def add_graph_finalizer( + self, register_finalizer: Callable[[fx.GraphModule], None] + ) -> None: + self.register_finalizer_fns.append(register_finalizer) + + def example_value_from_input_node(self, node: torch.fx.Node) -> Any: + """Extract the non-fake example tensor""" + if node.op == "placeholder": + return node.meta["grapharg"].example + assert node.op == "get_attr" + return self.nn_modules[node.target] # type: ignore[index] + + def add_fqn_info_for_inlined_modules( + self, inlined_module: torch.nn.Module, source: Source + ) -> None: + name = OutputGraph.module_key_name(source.name) + name = get_unique_name_wrt( + name, self.used_inlined_inbuilt_modules_names, self.global_scope + ) + self.used_inlined_inbuilt_modules_names.add(name) + + def register_leaf_name(leaf_name: str) -> None: + assert self.param_name_to_source is not None + new_source = self.get_chained_param_buffer_source(source, leaf_name) + new_name = f"{name}.{leaf_name}" + self.param_name_to_source[new_name] = new_source + if isinstance(source, LocalSource): + self.dynamo_flat_name_to_original_fqn[ + OutputGraph.module_key_name(new_source.name) + ] = leaf_name + + # annoying, but there are cases when we do not have parameters + # see test_nn_moduledict_contains + if hasattr(inlined_module, "_parameters"): + if ( + callable(inlined_module.named_parameters) + and inlined_module.named_parameters.__func__ # type: ignore[attr-defined] + is og_module_named_parameters_fn_ptr + ): + for leaf_name, _ in inlined_module.named_parameters(): + register_leaf_name(leaf_name) + if hasattr(inlined_module, "_buffers"): + if ( + callable(inlined_module.named_buffers) + and inlined_module.named_buffers.__func__ # type: ignore[attr-defined] + is og_module_named_buffers_fn_ptr + ): + for leaf_name, _ in inlined_module.named_buffers(): + register_leaf_name(leaf_name) + + +class DynamoTracerOutput: + error_on_graph_break: bool + is_tracing_resume_prologue: bool + output_graph: OutputGraph | None + # output_graph_for_cleanup is set even when there's an error, to allow + # cleanup of graph nodes to break reference cycles + output_graph_for_cleanup: OutputGraph | None + closure: tuple[Any, ...] | None + f_globals: dict[str, Any] + + def __init__( + self, tracer: "InstructionTranslatorBase", error: Any | None = None + ) -> None: + self.error_on_graph_break = tracer.error_on_graph_break + self.is_tracing_resume_prologue = tracer.is_tracing_resume_prologue + self.closure = tracer.closure + self.f_globals = tracer.f_globals + self.output_graph_for_cleanup = tracer.output + if error: + self.output_graph = None + else: + self.output_graph = tracer.output + + def _cleanup_output_graph(self) -> None: + output_graph = self.output_graph_for_cleanup + if output_graph: + for tracer in output_graph.tracers: + tracer.graph._clear_nodes() + # Also clear tracked_fakes to break FakeTensorMode → ShapeEnv → TrackedFake → FakeTensor cycle + if ( + output_graph.tracing_context.fake_mode + and output_graph.tracing_context.fake_mode.shape_env + ): + output_graph.tracing_context.fake_mode.shape_env.tracked_fakes = None + + +err_epilogue = ( + "With the current config, we will graph break " + "(and fall back to eager-mode PyTorch) on all ops " + "that have do not have the 'pt2_compliant_tag'. " + "Please see the following doc for how to mark this op as PT2 compliant " + "https://pytorch.org/tutorials/advanced/custom_ops_landing_page.html" +) + + +def check_pt2_compliant_op( + output_graph: OutputGraph, kind: str, target: Any, args: Any, kwargs: Any +) -> None: + if kind != "call_function": + return + + def encountered_compliant_op(target: torch._ops.OpOverload) -> None: + if target.namespace in {"prim", "prims", "aten"}: + return + output_graph.compliant_custom_ops.add(target) + + def encountered_non_compliant_op(target: torch._ops.OpOverload, msg: str) -> None: + output_graph.non_compliant_ops.add(target) + if config.only_allow_pt2_compliant_ops: + unimplemented( + gb_type="Encountered non-PT2-compliant op", + context="", + explanation=msg + " " + err_epilogue, + hints=[], + ) + + if isinstance(target, torch._ops.OpOverload): + if torch.Tag.pt2_compliant_tag in target.tags: + encountered_compliant_op(target) + return + encountered_non_compliant_op( + target, + f"Encountered the torch.ops.OpOverload {target} that is not PT2 compliant.", + ) + return + + if isinstance(target, torch._ops.OpOverloadPacket): + overloads = tuple(target.overloads()) + # Optimization: Overload resolution is expensive. + # If there's only one overload, we know what it will resolve to. + if len(overloads) == 1: + op = getattr(target, overloads[0]) + if torch.Tag.pt2_compliant_tag in op.tags: + encountered_compliant_op(op) + return + encountered_non_compliant_op( + op, + f"Encountered the non-overloaded " + f"torch.ops.OpOverloadPacket {target} " + f"that is not PT2 compliant. ", + ) + return + + args, kwargs = torch._dynamo.utils.get_fake_values_from_nodes( + output_graph.current_tx, (args, kwargs), False + ) + try: + overload = torch._C._jit_resolve_packet( + target._qualified_op_name, *args, **kwargs + ) + except RuntimeError as e: + unimplemented( + gb_type="Error when attempting to resolve op packet", + context="", + explanation=str(e), + hints=[], + ) + + op = getattr(target, overload) + if torch.Tag.pt2_compliant_tag in op.tags: + encountered_compliant_op(op) + else: + encountered_non_compliant_op( + op, + f"Encountered the torch.ops.OpOverloadPacket {target} " + f"which resolves to the overload ({overload}) that is " + f"not PT2 compliant.", + ) + + +_compile_id_counter = itertools.count() + +P = ParamSpec("P") +R = TypeVar("R") + + +class LazyProxy: + def __init__( + self, + tracer: "SubgraphTracer", + fn: Callable[P, R], + *args: P.args, + **kwargs: P.kwargs, + ) -> None: + self.tracer = tracer + # pyrefly: ignore [invalid-type-var] + self.fn = fn + self.args = args + self.kwargs = kwargs + + def __call__(self) -> Any: + return self.fn(*self.args, **self.kwargs) + + +class SubgraphTracer(fx.Tracer): + """ + Holds an FX graph that is being traced. OutputGraph owns a SubgraphTracer + and the separation of responsibilities is that SubgraphTracer is + responsible for building the graph while OutputGraph is responsible for + compiling and executing the graph. + """ + + def __init__( + self, + output_graph: "OutputGraph", + parent: Optional["SubgraphTracer"] = None, + is_export: bool = False, + source_target: Target | None = None, + description: str | None = None, + ) -> None: + super().__init__() + self.output_graph = weakref.proxy(output_graph) + self.graph = torch.fx.Graph() + + # See note [Export inputs must be explicitly passed in] + self.is_export = is_export + # Map from graph input name to its placeholder proxy object, where the + # map's keys give all current placeholder node names and can be used to + # create unique node names + self.input_name_to_proxy: dict[str, fx.Proxy] = {} + # Node => computed real value (see utils.get_real_value) + self.real_value_cache: dict[fx.Node, torch.Tensor] = {} + + # SubgraphTracers can be nested. See NOTE [HigherOrderOperator tracing design] + self.parent = parent + self.source_target = source_target + self.description = description + # A dict mapping previously free variables (Proxy objects) + # to new Proxy objects that wrap inputs to this subgraph. + # + # This dict maps proxies in outer graphs to placeholders in current graph. + # It serves two purposes: + # - Proxies are associated with VariableTrackers. If we see + # the same VariableTracker twice (and it is a free variable), + # then we want to use the same Proxy in the current subgraph to + # record the tracing. + # - If we are tracing a HigherOrderOperator's body_fn, then we + # need to keep track of what free variables were lifted so we can + # rewrite the HigherOrderOperator call using the traced body_fn. + # Dicts maintain the order of args for the HigherOrderOperator call. + self.lifted_freevars: dict[fx.Proxy, fx.Proxy] = {} + + # map basic symbols (unbacked and unbacked) to their bound proxies. + # There are only two cases where bound_symbols will be recorded: + # 1. when we create_graph_input for a backed SymInt that's basic symbol + # 2. when we track_produced_symints for intermediate results + # bound_symbols always map the symbol to the proxy whose + # tracer is the current tracer that's readily accessible in current tracer's graph. + self.bound_symbols: dict[sympy.Symbol, torch.fx.Proxy | LazyProxy] = {} + + # Maps _DynamicScalar object ids to allocated SymInt nodes, for symbol reuse + self.dynamic_scalar_nodes: dict[int, torch.SymInt] = {} + + self.prev_inst = None + # True if we want to allow externally visible side-effects (doesn't throw error on their existence) + # during this tracer's tracing. This is currently only used by experimental AC out-of-tree + # via torch._dynamo.utils._disable_side_effect_safety_checks_for_current_subtracer. + # Note: Externally visible side-effects are allowed if this flag OR the above flag is True. + self.unsafe_allow_externally_visible_side_effects = False + self.traced_with_externally_visible_side_effects = False + # True if we want to allow side effects by returning them as extra outputs from the subgraph. + # This is set when enable_side_effects_in_hop=True for HOPs like invoke_subgraph + # and checkpoint (when skip_fwd_side_effects_in_bwd_under_checkpoint config is True). + self.allow_side_effects_in_hop = False + # User code stack at the point where an externally-visible side effect + # was first detected and allowed (not raised). None means no side + # effect; non-None means one occurred. + self.side_effect_stack: traceback.StackSummary | None = None + # All sources accessed via VariableBuilder (and cell loads) during + # this subtracer's trace. Used by invoke_subgraph reuse to collect + # guards and detect mutations on captured variables. + self.traced_sources: OrderedSet[Source] = OrderedSet() + + # True if this tracer is currently tracing (reconstructing) into a Python generator + self.is_reconstructing_generator = False + + self.debug_level: int = parent.debug_level + 1 if parent is not None else 0 + + self._cur_code = None + self._orig_gm_meta: list[Any] | None = None + self._orig_gm_lineno_map: dict[int, int | None] | None = None + self._orig_gm_firstlineno: int | None = None + # Each SubgraphTracer is associated with a source target, which indicates + # which operator this subgraph is attached to. We compute a source_fn_stack + # based on the source target. For the root tracer, it's set to []. + # This is useful for debugging and transforming the exported graph. + if self.parent is None: + self.source_fn_stack: list[Any] = [] + else: + self.source_fn_stack = self.parent.source_fn_stack + [ + (self.graph._target_to_str(source_target), source_target) + ] + + # This is used to create a unique name for the placeholder + self._used_names: OrderedSet[str] = OrderedSet() + # Stores the versions of the input tensors at the time they are inserted + # as placeholders in the graph. This is used to track input mutation. + self._input_versions_at_beginning: list[int] = [] + if torch.is_inference_mode_enabled(): + raise RuntimeError( + "Inference mode is supposed to be disabled during compilation. Please open an issue." + ) + + self.tracked_proxyable_vt: OrderedSet[VariableTracker] = OrderedSet() + + def record_proxyable_vt(self, vt: VariableTracker) -> None: + self.tracked_proxyable_vt.add(vt) + + # preserve original meta if it is available + def _maybe_preserve_original_meta( + self, tx: "InstructionTranslatorBase", node: fx.Node + ) -> None: + if ( + self._orig_gm_meta + and self._orig_gm_lineno_map + and self._orig_gm_firstlineno + ): + lineno = tx.current_instruction.starts_line + node_idx = None + if lineno is not None: + node_idx = self._orig_gm_lineno_map.get( + lineno - self._orig_gm_firstlineno, None + ) + if node_idx is not None: + meta = self._orig_gm_meta[node_idx] + for field in fx.proxy._COPY_META_FIELDS: + if field in meta: + node.meta[field] = meta[field] + if "stack_trace" in meta: + node.meta["stack_trace"] = meta["stack_trace"] + + def create_proxy( + self, + kind: str, + target: Any, + args: Any, + kwargs: Any, + name: str | None = None, + type_expr: Any | None = None, + proxy_factory_fn: Callable[[fx.Node], fx.Proxy] | None = None, + ) -> fx.Proxy: + _t0 = time.time_ns() + try: + return self._create_proxy( + kind, target, args, kwargs, name, type_expr, proxy_factory_fn + ) + finally: + self.output_graph.bytecode_tracing_timings.create_proxy_ns += ( + time.time_ns() - _t0 + ) + + def _create_proxy( + self, + kind: str, + target: Any, + args: Any, + kwargs: Any, + name: str | None = None, + type_expr: Any | None = None, + proxy_factory_fn: Callable[[fx.Node], fx.Proxy] | None = None, + ) -> fx.Proxy: + # NOTE: [Nested SubgraphTracer and free_variable handling] + # -------------------------------------------------------- + # Read NOTE [HigherOrderOperator tracing design] first. + # + # Let's say we're in the middle of introspecting the body of a possibly + # nested HigherOrderOperator, and we see a free variable. + # + # There are two cases: + # 1. We see a free variable that is already tracked by Dynamo. + # 2. We see a free variable that has not been tracked by Dynamo + # + # In case 1, we call `maybe_lift_tracked_freevar_to_input` (below) + # which will lift the freevar to be an input of this subgraph + # and also recursively lift it to be an input on the parent(s). + # + # In case 2, before the call to `create_proxy`, the InstructionTranslator + # will see the freevar when it gets loaded by Python bytecode. + # E.g. for Python 3.11 the bytecodes that may do this are LOAD_DEREF or + # LOAD_GLOBAL. + # There, the InstructionTranslator asks Dynamo to begin tracking the + # freevar by building a new Variable. + # Building a new Variable automatically lifts the freevar to be an + # input of the root SubgraphTracer. + # + # The implications for the code below are: + # - We will always be in Case 1 when we get to this code. + # - Any "free variable" we encounter here is guaranteed to already be + # bound, that is, it is either a graph input of the root graph, or + # some local variable of the root graph or a subgraph. + # - The additional work we need to do here is *only* that we need to + # lift this free variable into inputs (recursively) of each nested + # higher-order-op subgraph until we hit the subgraph where the free + # variable is bound + if self.parent is not None: + flat_args, tree_spec = pytree.tree_flatten((args, kwargs)) + new_flat_args = [] + for arg in flat_args: + maybe_new_arg = self.maybe_lift_tracked_freevar_to_input(arg) + new_flat_args.append(maybe_new_arg) + + args, kwargs = pytree.tree_unflatten(new_flat_args, tree_spec) + + rv = super().create_proxy( + kind, + target, + args, + kwargs, + name, + type_expr, + proxy_factory_fn, # type: ignore[arg-type] + ) + + # append stack trace to fx node + tx = self.output_graph.current_tx + + # log detailed location of line of code in 3.11 + if sys.version_info >= (3, 11) and kind in ( + "call_function", + "call_method", + "call_module", + ): + cur_inst = tx.current_instruction + if ( + cur_inst is not self.prev_inst + and cur_inst.positions is not None + and cur_inst.positions.lineno is not None + ): + tx_code = tx.f_code + header = tx.get_line_of_code_header(lineno=cur_inst.positions.lineno) + + def get_trace_call_log_str() -> str: + line = get_instruction_source_311(tx_code, cur_inst).rstrip() + return f"TRACE FX call {rv.node.name} from {header}\n{line}" + + trace_call_log.debug("%s", LazyString(get_trace_call_log_str)) + self.prev_inst = cur_inst + + # update reference to original meta if we're tracing a new code object + is_retracing = False + if tx.f_code is not self._cur_code: + orig_graphmodule_maybe = code_context.get_context(tx.f_code).get( + "orig_graphmodule", lambda: None + )() + if isinstance(orig_graphmodule_maybe, torch.fx.GraphModule): + is_retracing = True + self._orig_gm_meta = [ + nd.meta for nd in orig_graphmodule_maybe.graph.nodes + ] + self._orig_gm_lineno_map = orig_graphmodule_maybe._lineno_map + self._orig_gm_firstlineno = ( + orig_graphmodule_maybe.forward.__code__.co_firstlineno + ) + else: + self._orig_gm_meta = None + self._orig_gm_lineno_map = None + self._orig_gm_firstlineno = None + nn_module_stack = tx.nn_module_stack + if nn_module_stack: + rv.node.meta["nn_module_stack"] = nn_module_stack.copy() + + if kind in {"call_function", "call_method"}: + stack = (rv.node.name, target) + if nn_module_stack: + # Current codebase assumes that the nn_module_stack has the + # builtin modules in the stack. + current_nn_module = list(rv.node.meta["nn_module_stack"].values())[-1][ + 1 + ] + if current_nn_module.__module__.startswith( + ("torch.nn.modules", "torch.ao.") + ) and not current_nn_module.__module__.startswith( + "torch.nn.modules.container" + ): + stack = (rv.node.name, current_nn_module) + + rv.node.meta["source_fn_stack"] = self.source_fn_stack + [stack] + elif kind == "call_module": + if self.parent is not None: + unimplemented( + gb_type="Invoking an nn.Module inside a higher order operator", + context=f"Higher order op name: {self.source_target}", + explanation="This is not supported.", + hints=[], + ) + # For modules we store the class + rv.node.meta["source_fn_stack"] = self.source_fn_stack + [ + ( + rv.node.name, + next( + ty + for k, (_, ty) in rv.node.meta["nn_module_stack"].items() + if k.split("@")[0] == target + ), + ) + ] + + self._maybe_preserve_original_meta(tx, rv.node) + + if not is_retracing: + if "nn_module_stack" not in rv.node.meta: + nn_module_stack = tx.nn_module_stack + if nn_module_stack: + rv.node.meta["nn_module_stack"] = nn_module_stack.copy() + + if "source_fn_stack" not in rv.node.meta: + if kind in {"call_function", "call_method"}: + rv.node.meta["source_fn_stack"] = self.source_fn_stack + [ + (rv.node.name, target) + ] + elif kind == "call_module": + if self.parent is not None: + unimplemented( + gb_type="Invoking an nn.Module inside a HigherOrderOperator", + context="", + explanation="This is not supported.", + hints=[], + ) + # For modules we store the class + rv.node.meta["source_fn_stack"] = self.source_fn_stack + [ + ( + rv.node.name, + rv.node.meta["nn_module_stack"][target][1], + ) + ] + + if "stack_trace" not in rv.node.meta: + frame_summaries: list[traceback.FrameSummary] = [] + while tx: + # Avoid frame summaries from inside the torch/nn/modules. This ensures that we keep the stack trace of + # the user code. + if not tx.is_co_filename_from_nn_modules(): + frame_summaries.append(tx.frame_summary()) + tx = getattr(tx, "parent", None) + + filtered_frame_summaries = [ + frame + for frame in frame_summaries + if frame.filename not in uninteresting_files() + ] + + # Reverse the frame_summaries, such that the innermost frame is at the last + filtered_frame_summaries.reverse() + + # official from_list stub doesn't have new-style type + msgs = traceback.StackSummary.from_list(filtered_frame_summaries).format() + rv.node.stack_trace = "".join(msgs) + + if ( + torch._dynamo.config.use_graph_deduplication + or torch._dynamo.config.track_nodes_for_deduplication + ): + self.output_graph.region_tracker.track_node( + self.output_graph.current_tx, rv.node + ) + return rv + + def create_node( + self, + kind: str, + target: Target, + args: Any = None, + kwargs: Any = None, + name: str | None = None, + type_expr: Any | None = None, + ) -> fx.Node: + check_pt2_compliant_op(self.output_graph, kind, target, args, kwargs) + if self.parent is not None: + flat_args = pytree.arg_tree_leaves(*args, **kwargs) + for arg in flat_args: + if not isinstance(arg, torch.fx.Node): + continue + assert arg.graph == self.graph, ( + "create_node using arg not from this SubgraphTracer" + ) + + node = super().create_node(kind, target, args, kwargs, name, type_expr) + node.meta["creation_timestamp"] = self.output_graph.timestamp + self._used_names.add(node.name) + return node + + # Note: we did not override erase_node since + # we call self.graph.erase_node elsewhere + def remove_node(self, node: fx.Node) -> None: + if len(node.users) > 0: + user_graph_nodes: list[torch.fx.Node] = [] + for user in node.users: + # For the case where user.graph == self.graph, that is a real bug and will raise + # properly. + if user.graph != self.graph: + # This is a nested graph, which needs to be deleted. + # If we do not do this, we will raise on attempting to remove this. + # As we only get here during restoration cleanup, this is sound. + user_graph_nodes.extend(reversed(list(user.graph.nodes))) + for other_graph_node in user_graph_nodes: + other_graph_node.graph.erase_node(other_graph_node) + self.graph.erase_node(node) + self.input_name_to_proxy.pop(node.name, None) + + # when before=True, we will insert this input before the most recent + # inserted proxy. This is a hack to get around an ordering problem, + # where we first insert a tensor argument, and then insert bindings + # for SymInts that may occur in the tensor argument. + # Remove this if https://github.com/pytorch/pytorch/issues/99007 gets + # fixed. + def create_graph_input( + self, + name: str, + type_expr: Any, + example_value: Any, + before: bool = False, + source: Source | None = None, + ) -> fx.Proxy: + if isinstance(example_value, torch.Tensor): + self._input_versions_at_beginning.append(example_value._version) + log.debug( + "create_graph_input %s %s %s at debug_level %s before=%s", + name, + source.name if source is not None else "(none)", + example_value, + self.debug_level, + before, + ) + if source is None: + assert self.parent is not None, ( + f"you are required to provide a source for inputs {name} example_val {example_value} on the root tracer" + ) + + # Note [Export inputs must be explicitly passed in] + # In eager, we are generally OK with adding graph inputs whenever we + # want, because we take care of writing the bytecode that knows how + # to source all the inputs. + # + # In export, this is bad, because you want a self-contained export + # object which only depends on the inputs you explicitly passed to it. + # So we are a bit more strict about what sources can become inputs + # in export + if self.is_export and self.parent is None: + assert source is not None + if not is_from_local_source(source, only_allow_input=True): + self.output_graph.source_to_user_stacks.setdefault(source, []).append( + TracingContext.extract_stack() + ) + + # _used_names contains the names of all the nodes in the graph, + # including intermediates. This ensures that we do not have a name + # collision. + name = get_unique_name_wrt(name, self._used_names) + if self.input_name_to_proxy: + prev_name = next(reversed(self.input_name_to_proxy)) + node = self.input_name_to_proxy[prev_name].node + if before: + ctx = self.graph.inserting_before(node) + else: + ctx = self.graph.inserting_after(node) + else: + ctx = self.graph.inserting_before(None) + with ctx: + proxy = self.create_proxy("placeholder", name, (), {}, type_expr=type_expr) + set_example_value(proxy.node, example_value) + if self.input_name_to_proxy and before: + k, v = self.input_name_to_proxy.popitem() + self.input_name_to_proxy[name] = proxy + self.input_name_to_proxy[k] = v + else: + self.input_name_to_proxy[name] = proxy + + # For placeholder nodes, `name` is passed as a str to the target, + # and then torch.fx decides the node.name. So, record the `target` + # name as well in the _used_names to prevent any collision. + self._used_names.add(name) + + # NOTE: [Auto lift basic free symbols when create_graph_input] + # There are two sources of basic symbols: + # + # - They can come from inputs, e.g. when an input tensor is specified as dynamic. We handle + # this case by intercepting at create_graph_input. Whenever we call create_graph_input, we + # try to also lift the basic symbols in example values as graph input. + # + # 1. When create_graph_input for a tensor that has symbolic shapes, + # we look for basic symbols in its size and stride, we check if the symbol is bound + # in current graph (i.e. bound_symbols), it it's not bound, we'll create a placeholder + # for it then recursively check its parent, creates ph if not bound at parent until. + # reachting the top-level, where we require a source is attached to the proxy. + # + # 2. When create_graph_input for a tensor that contains compound exprs, + # for example, if an input to subgraph takes size [s1+s2//8], we'll look for the + # the free basic symbols in the sizes and lift all of them following 1. + # + # 3. When create_graph_input for a symint. The following invariants hold: + # a. if symint's expr is a basic symbol, we only lift it once. + # b. if symint's expr is compuned, we lift the expr as a single input. We won't lift The basic symbols + # in the compuned expr are NOT lifted. Because if the basic symbols are used inside the subgraph + # they will be lifted according to 3.a + # + # - They can come from intermediate results: + # For example, data-dependent operators such as t.item(), t.nonzero(), where basic symbols + # might be created. For this purpose, we track the basic symbols of intermediate results + # immediately after they're created at wrap_fx_proxy with track_produced_symints. Notice + # that for basic symbols that're already tracked by create_graph_input, we won't track it again. + # + # Also see NOTE: [Export inputs must be explicitly passed in] + is_strict_export = self.is_export + is_non_strict_export = torch.compiler.is_compiling() + if not is_strict_export and not is_non_strict_export: + if isinstance(example_value, torch.Tensor): + self._lift_basic_symbols(example_value, source) + elif isinstance(example_value, (list, tuple)): + for i, e in enumerate(example_value): + if not isinstance(e, torch.Tensor): + continue + + e_source = None + if source: + e_source = GetItemSource( + base=source, + index=i, + index_is_slice=False, + ) + + self._lift_basic_symbols(e, e_source) + + # Bound the symbol to ph if example_value is a SymInt with basic symbol. + if isinstance(example_value, torch.SymInt) and isinstance( + example_value.node.expr, sympy.Symbol + ): + self.bound_symbols[example_value.node.expr] = proxy + return proxy + + # See NOTE: [Nested SubgraphTracer and free_variable handling] for more details + def lift_tracked_freevar_to_input(self, proxy: fx.Proxy) -> LazyProxy | fx.Proxy: + # You're doing something wrong if we are the root SubgraphTracer because + # Dynamo adds tensors to graph inputs before creating a proxy for them. + assert self.parent is not None, ( + "lift_tracked_freevar_to_input should not be called on root SubgraphTracer" + ) + + example_value = proxy.node.meta["example_value"] + + # To avoid lifting the same symbol twice, we check whether basic symbols has been tracked. + # For example, the basic symbols may have already been lifted for current subgraph when + # we automatically lift basic symbols in the sizes/strides of a tensor t. + # Suppose parent graph calls sz = t.size()[0], it creates + # a proxy in parent and the subgraph accesses sz via closure. sz's proxy is not tracked + # in current sub-tracer so we may lift the same symbol twice. + if ( + isinstance(example_value, torch.SymInt) + and example_value.node.expr in self.bound_symbols + ): + return self.bound_symbols[example_value.node.expr] + + # Proxies are associated with VariableTracker. + # It is possible that we've already lifted the Proxy to be an input. + # If that is the case, just return the already lifted Proxy. + if proxy in self.lifted_freevars: + return self.lifted_freevars[proxy] + + # We first lift proxy to parent's graph then lift to current graph's input + # so that when we bind symints of the sizes in current graph, those symints + # would already be lifted as inputs to parent graph. + if proxy.tracer != self.parent: + self.parent.lift_tracked_freevar_to_input(proxy) + + example_value = proxy.node.meta["example_value"] + type_expr = ( + type(example_value.real_obj) + if isinstance(example_value, FakeScriptObject) + else type(example_value) + ) + new_proxy = self.create_graph_input(proxy.node.name, type_expr, example_value) + self.lifted_freevars[proxy] = new_proxy + return new_proxy + + def maybe_lift_tracked_freevar_to_input(self, arg: Any) -> Any: + """ + If arg is a free variable, then lift it to be an input. + Returns the new lifted arg (if arg was a freevar), else the + original arg. + """ + if not isinstance(arg, torch.fx.Proxy): + # Note: arg can be a python built-in slice type e.g. + # x[:max_seq] is represented as get_item(t, (slice(None, max_seq, None))) + # we need to also look into the slice variable itself to lift the + # proxies there. + if isinstance(arg, slice): + return slice( + *( + self.maybe_lift_tracked_freevar_to_input(sub_arg) + for sub_arg in (arg.start, arg.stop, arg.step) + ) + ) + else: + return arg + elif arg.tracer == self: + return arg + return self.lift_tracked_freevar_to_input(arg) + + # See NOTE: [Auto lift basic free symbols when create_graph_input] for overall design + # You MUST call this API every time when creating a proxy in wrap_fx_proxy for a call + # that produced symints or tensors with unbacked symint shapes. + # This function is used to track the symints with its proxies created during + # dynamo tracing so that subgraph knows how to bind a symbol input with parent's proxy. + # LazyProxy are created for tensor shapes that're unbacked so that we don't create proxies + # for symbols that're not going to be used, the LazyProxy will be turned into a proxy + # when it's lifted as input to subgraph. + def track_produced_symints( + self, example_value: Any, e_proxy: LazyProxy | torch.fx.Proxy + ) -> None: + # When binding the symbols in an example_value, we bind the symbols + # to the proxy's associated Tracer instead of current tracer. + # This is because: + # 1. We may be calling wrap_tensors during speculate_subgraph because + # the variables are lazily realized. The proxy are top-level phs but + # current tracer is a subtracer. + # 2. For autograd.Function, we trace the backward graph with a new tracer + # whose parent is the forward tracer, but we're using all the proxies created + # in forward tracer to trace the backward. + # For example, forward calls save_for_backward for a input tensor t. + # Backward calls t.tolist(). In this case, all the proxies that backward tracer + # sees are from parent tracer (i.e. the forward tracer). (e.g. t[0].item()) + # See test_validate_outputs_unbacked for repro on 2. + tracer = e_proxy.tracer + assert isinstance(tracer, SubgraphTracer) + + def need_bind(s: Any) -> bool: + from torch.fx.experimental.symbolic_shapes import is_symbolic + + return ( + is_symbolic(s) + and isinstance(s.node.expr, sympy.Symbol) + and s.node.expr not in self.bound_symbols + ) + + def _proxy_with_example_value( + example_value: Any, *args: Any, **kwargs: Any + ) -> fx.Proxy: + # We need to insert proxy for creating sym_size/sym_stride/sym_storage right after e_proxy + nonlocal e_proxy + e_proxy = e_proxy() if isinstance(e_proxy, LazyProxy) else e_proxy + assert isinstance(e_proxy, torch.fx.Proxy) + with tracer.graph.inserting_after(e_proxy.node): + proxy = tracer.create_proxy(*args, **kwargs) + set_example_value(proxy.node, example_value) + return proxy + + if isinstance(example_value, torch.Tensor): + for i, s in enumerate(example_value.size()): + if need_bind(s): + log.debug( + "track_produced_symints %s for %s.size()[%s] at debug_level %s", + s, + e_proxy, + i, + tracer.debug_level, + ) + lazy_proxy = LazyProxy( + tracer, + _proxy_with_example_value, + s, + "call_function", + torch.ops.aten.sym_size.int, + (e_proxy, i), + {}, + type_expr=type(s), + ) + self.track_produced_symints(s, lazy_proxy) + + storage_offset = example_value.storage_offset() + if need_bind(storage_offset): + log.debug( + "track_produced_symints %s for %s.storage_offset() at debug_level %s", + storage_offset, + e_proxy, + tracer.debug_level, + ) + lazy_proxy = LazyProxy( + tracer, + _proxy_with_example_value, + storage_offset, + "call_function", + torch.ops.aten.sym_storage_offset, + (e_proxy,), + {}, + type_expr=type(storage_offset), + ) + self.track_produced_symints(storage_offset, lazy_proxy) + + if example_value.layout is torch.strided: + for i, s in enumerate(example_value.stride()): + if need_bind(s): + log.debug( + "track_produced_symints %s for %s.stride()[%s] at debug_level %s", + s, + e_proxy, + i, + tracer.debug_level, + ) + lazy_proxy = LazyProxy( + tracer, + _proxy_with_example_value, + s, + "call_function", + torch.ops.aten.sym_stride.int, + (e_proxy, i), + {}, + type_expr=type(s), + ) + self.track_produced_symints(s, lazy_proxy) + + elif example_value.layout is torch.sparse_coo: + self.track_produced_symints(example_value._indices(), e_proxy) + self.track_produced_symints(example_value._values(), e_proxy) + elif example_value.layout in {torch.sparse_csr, torch.sparse_bsr}: + self.track_produced_symints(example_value.crow_indices(), e_proxy) + self.track_produced_symints(example_value.col_indices(), e_proxy) + elif example_value.layout in {torch.sparse_csc, torch.sparse_bsc}: + self.track_produced_symints(example_value.ccol_indices(), e_proxy) + self.track_produced_symints(example_value.row_indices(), e_proxy) + if is_traceable_wrapper_subclass(example_value): + attrs, ctx = example_value.__tensor_flatten__() + for attr in attrs: + inner_t = getattr(example_value, attr) + self.track_produced_symints(inner_t, getattr(e_proxy, attr)) + elif isinstance(example_value, torch.SymInt): + if need_bind(example_value): + expr = example_value.node.expr + tracer.bound_symbols[expr] = e_proxy + + # See Note [Auto lift basic free symbols when create_graph_input] + def _lift_basic_symbols( + self, example_value: torch.SymInt | torch.Tensor, src: Source | None + ) -> None: + # The before arg is for inserting symints in the sizes/strides of a tensor + # before the tensor. This ordering ensures that when we look at the tensor's + # symbols, they're already lifted/tracked. E.g. this assumption is used + # in insert_deferred_runtime_asserts. + def _lift_symbols_in_symint( + s: int | torch.SymInt, + source: Source | None, + before: bool = False, + ) -> None: + if not is_symbolic(s): + return + + assert isinstance(s, torch.SymInt) + self_to_be_bound = self.lookup_unbound_symbols(s) + if len(self_to_be_bound) == 0: + return + + # For subgraph + if self.parent is not None: + # Recursively lift symbols in symint until top-level. + self.parent._lift_basic_symbols(s, source) + for s0 in self_to_be_bound: + parent_proxy = self.parent.bound_symbols[s0] + example_val = parent_proxy.node.meta["example_value"] # type: ignore[union-attr] + assert isinstance(example_val, torch.SymInt) + ph = self.create_graph_input( + str(s0), + type(example_val), + example_val, + before=before, + source=source, + ) + log.debug( + "_lift_symbols_in_symint %s from %s at debug_level %s", + s0, + source.name if source is not None else "subgraph inputs", + self.debug_level, + ) + self.lifted_freevars[parent_proxy] = ph # type: ignore[index] + # For root_tracer: + else: + assert len(self_to_be_bound) == 1, ( + f"For root tracer, we only expect to bind basic symbols (compound symbols " + f"should be cached before) but got unbound symbols {self_to_be_bound} in {s}" + ) + assert source is not None, ( + f"Source of '{s}' is None when lifting it to input of top-level. If it's an unbacked symbol, " + "this could be because it's not tracked with lazy_bind_unbacked_symbols. " + f"Otherwise, should provide a source when create_graph_input for `{s}` at root tracer." + ) + s0 = next(iter(self_to_be_bound)) + ph = self.create_graph_input( + str(s0), + type(s), + s, + before=before, + source=source, + ) + log.debug( + "_lift_symbols_in_symint %s from %s at debug_level %s", + s, + source.name if source is not None else "subgraph inputs", + self.debug_level, + ) + ph.node.meta["grapharg"] = GraphArg( + source, + s, + pass_arg_as_tensor=False, + fake_tensor=None, + is_tensor=False, + ) + + if isinstance(example_value, torch.Tensor): + for i, s in enumerate(example_value.size()): + _lift_symbols_in_symint( + s, + ( + TensorPropertySource(src, TensorProperty.SIZE, i) + if src is not None + else None + ), + before=True, + ) + if example_value.layout is torch.strided: + for i, s in enumerate(example_value.stride()): + _lift_symbols_in_symint( + s, + ( + TensorPropertySource(src, TensorProperty.STRIDE, i) + if src is not None + else None + ), + before=True, + ) + _lift_symbols_in_symint( + example_value.storage_offset(), + ( + TensorPropertySource(src, TensorProperty.STORAGE_OFFSET) + if src is not None + else None + ), + before=True, + ) + elif example_value.layout is torch.sparse_coo: + self._lift_basic_symbols(example_value._indices(), src) + self._lift_basic_symbols(example_value._values(), src) + elif example_value.layout in {torch.sparse_csr, torch.sparse_bsr}: + self._lift_basic_symbols(example_value.crow_indices(), src) + self._lift_basic_symbols(example_value.col_indices(), src) + elif example_value.layout in {torch.sparse_csc, torch.sparse_bsc}: + self._lift_basic_symbols(example_value.ccol_indices(), src) + self._lift_basic_symbols(example_value.row_indices(), src) + if is_traceable_wrapper_subclass(example_value): + attrs, ctx = example_value.__tensor_flatten__() + for attr in attrs: + inner_t = getattr(example_value, attr) + self._lift_basic_symbols( + inner_t, AttrSource(src, attr) if src is not None else None + ) + elif isinstance(example_value, torch.SymInt): + _lift_symbols_in_symint( + example_value, + src, + ) + + # Lookup the proxy in current tracer for each symbol in expressions of s, + # See Note [Auto lift basic free symbols when create_graph_input] + def lookup_unbound_symbols(self, s: torch.SymInt) -> list[sympy.Symbol]: + free_symbols = s.node.expr.free_symbols + if len(free_symbols) == 0: + return [] + + to_be_bound = [] + for s0 in free_symbols: + if s0 not in self.bound_symbols: + to_be_bound.append(s0) + continue + + proxy = self.bound_symbols[s0] + if isinstance(proxy, LazyProxy): + proxy = proxy() + self.bound_symbols[s0] = proxy + assert isinstance(proxy, torch.fx.Proxy) and proxy.tracer is self, ( + f"The proxy of symbol {s0} doesn't belong to current tracer." + ) + # Sort the symbols so that we can have a deterministic lifting order + return sorted(to_be_bound, key=lambda s: s.name) + + def has_input_mutation(self) -> MutationInfo: + input_versions_at_beginning = self._input_versions_at_beginning + input_nodes = [] + + input_versions_at_end = [] + for node in self.graph.nodes: + if node.op == "placeholder": + example_value = node.meta["example_value"] + if isinstance(example_value, torch.Tensor): + input_versions_at_end.append(example_value._version) + input_nodes.append(node) + else: + break + + mutated_inputs = [ + i + for i, (v1, v2) in enumerate( + zip(input_versions_at_beginning, input_versions_at_end) + ) + if v1 != v2 + ] + + if mutated_inputs: + mutated_nodes = [input_nodes[i] for i in mutated_inputs] + msg = f"Input mutation detected at {mutated_nodes}" + return MutationInfo(True, msg) + + return MutationInfo(False, "") + + def has_aliasing(self) -> AliasingInfo: + from torch._dynamo.variables.higher_order_ops import get_tensor_storages + from torch._higher_order_ops.utils import _collect_fake_inputs + + input_storages: dict[StorageWeakRef, torch.fx.Node] = dict() + + for node in self.graph.nodes: + if node.op == "placeholder": + example_value = _collect_fake_inputs([node])[0] + if isinstance(example_value, torch.Tensor): + for storage in get_tensor_storages(example_value): + if storage in input_storages: + # input-input aliasing + msg = f"Input-to-input aliasing detected at nodes {input_storages[storage]} and {node}" + return AliasingInfo(True, msg) + input_storages[storage] = node + else: + break + + output_storages: dict[StorageWeakRef, torch.fx.Node] = dict() + out_nodes = self.graph.find_nodes(op="output")[0] + for out_node in pytree.tree_leaves(out_nodes.args[0]): + if out_node: + example_value = _collect_fake_inputs([out_node])[0] + assert not isinstance(example_value, list) + if isinstance(example_value, torch.Tensor): + for storage in get_tensor_storages(example_value): + if storage in output_storages: + # output-output aliasing + msg = f"Output-to-output aliasing detected at nodes {output_storages[storage]} and {out_node}" + return AliasingInfo(True, msg) + output_storages[storage] = out_node + + intersected_storages = input_storages.keys() & output_storages.keys() + if len(intersected_storages) > 0: + # input-output aliasing + aliased = [ + (input_storages[s], output_storages[s]) for s in intersected_storages + ] + aliased = ", ".join([f"{i} and {o}" for i, o in aliased]) + msg = f"Input-to-output aliasing detected at nodes {aliased}" + return AliasingInfo(True, msg) + + return AliasingInfo(False, "") + + +# NOTE: [HigherOrderOperator tracing design] +# Ignoring HigherOrderOperators for a moment, +# OutputGraph represents the graph being built by Dynamo that may be compiled +# and executed. It holds a root SubgraphTracer where the FX graph is built. +# +# HigherOrderOperators are operators that take functions as their arguments. +# When Dynamo encounters a HigherOrderOperator, then it attempts to introspect +# the function passed to it (call this the "body function"), capture it into a +# GraphModule, and rewrite the call to the HigherOrderOperator to use the +# GraphModule. +# +# The way we handle the capture of body functions is through having +# (possibly nested) SubgraphTracers, one per body function. +# +# Mechanically, we do the introspection by: +# - Creating a new SubgraphTracer via OutputGraph.subtracer +# - Executing the body function. +# This constructs the graph of the body function in the new SubgraphTracer +# while modifying the state of the OutputGraph. For example: +# - the OutputGraph can receive new GraphArgs (if we discover any new +# untracked Tensors) +# - side effects from the body function get accumulated into +# OutputGraph.side_effects +# - guards produced by the body function get accumulated into OutputGraph.guards +# +# The traced function has some special properties that make it easier for us +# to transform later down the line: +# - we lift all free variables to being inputs. +# +# If the introspection fails (due to the existence of graph breaks), then +# we roll back the current OutputGraph state and graph break on the +# HigherOrderOperator. diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/package.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/package.py new file mode 100644 index 0000000000000000000000000000000000000000..33ce8f55b922a8713f300c2fdeda597fe52ab7a5 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/package.py @@ -0,0 +1,1206 @@ +""" +This module provides the infrastructure for creating and managing compile package +for torch.compile. We mainly have two abstractions here: + - CompilePackage: Overarching data structure for store and lookup a list of compiled codes. + - CodeCacheEntry: Data structure for a single code being compiled by torch.compile. +The caching behavior is always under user control explicitly so that a stronger guarantee can +be provided about cache hit for a specific compiled model. Users can load the compile package +from a different process or host. +""" + +import abc +import ast +import contextlib +import dataclasses +import functools +import hashlib +import importlib +import inspect +import itertools +import json +import logging +import os +import pickle +import platform +import shutil +import sys +import types +from collections.abc import Callable, Generator, Iterator +from contextlib import nullcontext +from typing import Any, NewType, Optional, TYPE_CHECKING, Union +from typing_extensions import Never + +import torch +from torch._dynamo.exc import PackageError +from torch._dynamo.graph_utils import _graph_device_type +from torch.utils.weak import WeakIdKeyDictionary + +from .bytecode_transformation import get_code_keys +from .utils import counters, dynamo_timed, increment_frame + + +logger = logging.getLogger(__name__) + + +if TYPE_CHECKING: + from .guards import GuardManagerWrapper, GuardsState + + +_CODE_CACHE = WeakIdKeyDictionary() + + +def _code_cache(fn: Callable[..., Any]) -> Callable[..., Any]: + def _( + cls: type[Any], code: Union["SerializedCode", types.CodeType] + ) -> Union["SerializedCode", types.CodeType]: + if code in _CODE_CACHE: + return _CODE_CACHE[code] + res = fn(cls, code) + _CODE_CACHE[code] = res + return res + + return _ + + +@dataclasses.dataclass(frozen=True) +class SerializedCode: + co_argcount: int + co_posonlyargcount: int + co_kwonlyargcount: int + co_nlocals: int + co_stacksize: int + co_flags: int + co_code: bytes + co_consts: tuple[Any, ...] + co_names: tuple[str, ...] + co_varnames: tuple[str, ...] + co_filename: str + co_name: str + co_firstlineno: int + co_cellvars: tuple[str, ...] + co_freevars: tuple[str, ...] + co_linetable: bytes | None = None + co_qualname: str | None = None + co_exceptiontable: bytes | None = None + co_lnotab: str | None = None + + @classmethod + @_code_cache + def from_code_object(cls, code: types.CodeType) -> "SerializedCode": + kwargs = {key: getattr(code, key) for key in get_code_keys()} + kwargs["co_consts"] = tuple( + cls.from_code_object(c) if isinstance(c, types.CodeType) else c + for c in kwargs["co_consts"] + ) + return cls(**kwargs) + + @classmethod + @_code_cache + def to_code_object(cls, serialized_code: "SerializedCode") -> types.CodeType: + kwargs = {key: getattr(serialized_code, key) for key in get_code_keys()} + kwargs["co_consts"] = tuple( + cls.to_code_object(c) if isinstance(c, SerializedCode) else c + for c in kwargs["co_consts"] + ) + return types.CodeType( + *kwargs.values(), + ) + + +@dataclasses.dataclass +class _GuardedCodeCacheEntry: + """ + Contains the serializable information associated with a single compilation in dynamo. + To restore an execution of compiled code, we will need to serialize the following data: + - Dynamo bytecode for mapping Python inputs/outputs. + - Dynamo guards. + """ + + guards_state: bytes + dynamo_code: SerializedCode + + +def load_guards_state(guards_state: bytes) -> Any: + try: + import torch.distributed.fsdp._fully_shard._fully_shard as _fully_shard + + ctx = _fully_shard.disable_fsdp_module_new_init() + except ImportError: + ctx = nullcontext() # type: ignore[assignment] + with ctx: + return pickle.loads(guards_state) + + +def load_guard_manager( + guards_state: "GuardsState", + target_code: types.CodeType, + runtime_global_scope: Any, +) -> "GuardManagerWrapper": + from .output_graph import OutputGraphCommon + + return torch._dynamo.guards.CheckFunctionManager( + target_code, + OutputGraphCommon(guards_state.output_graph), + shape_code_parts=guards_state.shape_code_parts, + runtime_global_scope=runtime_global_scope, + ).guard_manager + + +_BackendId = NewType("_BackendId", str) # __compiled_fn +_FunctionId = NewType("_FunctionId", str) # __resume_at + + +@dataclasses.dataclass(frozen=True) +class InlinedSource: + module: str + firstlineno: int + lastlineno: int + checksum: str + content: str + + +@functools.cache +def _get_module_content(module: types.ModuleType) -> str: + return inspect.getsource(module) + + +@dataclasses.dataclass +class SourceInfo: + inlined_sources: set[InlinedSource] + + def add_code(self, code: types.CodeType) -> None: + module = inspect.getmodule(code) + if module is None: + return + sourcelines, firstlineno = inspect.getsourcelines(code) + lastlineno = firstlineno + len(sourcelines) + source = "".join(sourcelines) + assert source == "".join(_get_sourcelines(module, firstlineno, lastlineno)) + self.inlined_sources.add( + InlinedSource( + module=module.__name__, + firstlineno=firstlineno, + lastlineno=lastlineno, + checksum=_hash_source(source), + content=_get_module_content(module), + ) + ) + + +@dataclasses.dataclass +class _DynamoCodeCacheEntry: + """ + Contains the serializable information associated with a single code object + in dynamo. To restore an execution of compiled code, we will need the following + ingredients: + 1. The "original" code object, which serves as the entry point for eager + execution, i.e. the code only executed when there's no cache entry hit. + 2. The python module name this code object belongs to, for identifying the + enclosing global scope to inject compiled and resume functions. + 3. A list of function names that pointing to this code object. There could be + multiple function objects pointing to the same code such as recursive functions. + 4. A list of guarded code that eval frame dispatches to. + 5. A list of imported module objects unioned from all compiled branches. + 6. A list of "backends" (compiled fx graph) unioned from all compield branches. + 7. A string path used to access the original code object users defined. + A code object can be accessed by "{python_module}.{function_name}.{code_source}" . + 8. A boolean flag indicating whether the function is installed to global scope. + 9. A boolean flag indicating whether the function has a compile id. + 10. Whether or not this code entry was bypassed + """ + + python_code: SerializedCode + python_module: str + function_names: list[_FunctionId] + guarded_codes: list[_GuardedCodeCacheEntry] + import_sources: dict[str, str] + backend_ids: list[_BackendId] + code_source: str | None + install_to_global: bool + has_compile_id: bool = False + bypassed: bool = False + + +def _lookup_code(entry: _DynamoCodeCacheEntry) -> types.CodeType: + assert len(entry.function_names) == 1 + fn: Any = sys.modules[entry.python_module] + parts = entry.function_names[0].split(".") + for part in parts: + fn = getattr(fn, part) + if entry.code_source: + parts = entry.code_source.split(".") + for part in parts: + if part.endswith("]"): + index_begin = part.rfind("[") + assert isinstance(index_begin, int) and index_begin >= 0 + attr = getattr(fn, part[:index_begin], None) + if attr is None: + raise PackageError(f"Cannot find source for code entry {entry}") + fn = attr[ast.literal_eval(part[index_begin + 1 : -1])] + else: + fn = getattr(fn, part) + else: + raise PackageError(f"Cannot find source for code entry {entry}") + assert isinstance(fn, types.CodeType) + return fn + + +def _raise_resolution_error(code: types.CodeType, scope: Any) -> Never: + raise PackageError( + f"Cannot resolve a fully qualified name for {code}. Lookup scope: {scope}" + ) + + +def _get_code_source(code: types.CodeType) -> tuple[str, str]: + """ + Given a code object, return a fully qualified name which will be used as + a serialized handle to access the code object from the new process. + This is normally a straightforward process, but there are some corner cases: + 1. When a function is defined with decorator, then this function will be captured + inside a closure with the wrapper object. + 2. When a function is defined as a nested function, then the code object will be + stored on the co_consts field of the parent code object by Python compiler. + This function handles all of the corner cases above. + """ + + module = inspect.getmodule(code) + if module is None: + raise PackageError(f"Cannot find module for code {code}") + + toplevel: Any = module + if sys.version_info >= (3, 11): + parts = code.co_qualname.split(".") + + for part in parts: + if not hasattr(toplevel, part): + _raise_resolution_error(code, toplevel) + toplevel = getattr(toplevel, part) + if inspect.isfunction(toplevel) or inspect.ismethod(toplevel): + break + seen = set() + + def _find_code_source(obj: Any) -> str | None: + nonlocal toplevel + nonlocal seen + if obj in seen: + return None + + seen.add(obj) + + if inspect.iscode(obj): + if obj is code: + return "" + + for i, const in enumerate(obj.co_consts): + if (res := _find_code_source(const)) is not None: + return f".co_consts[{i}]{res}" + + if inspect.ismethod(obj): + if (res := _find_code_source(obj.__func__)) is not None: + toplevel = obj + return f".__func__{res}" + + if inspect.isfunction(obj): + if (res := _find_code_source(obj.__code__)) is not None: + toplevel = obj + return f".__code__{res}" + if obj.__closure__ is not None: + for i, cell in enumerate(obj.__closure__): + try: + cell_contents = cell.cell_contents + except ValueError: + continue + if not ( + inspect.isfunction(cell_contents) + or inspect.iscode(cell_contents) + or inspect.ismethod(cell_contents) + ): + continue + if (res := _find_code_source(cell_contents)) is not None: + toplevel = obj + return f".__closure__[{i}].cell_contents{res}" + + if sys.version_info < (3, 11): + if inspect.ismodule(obj): + for value in obj.__dict__.values(): + if not ( + inspect.isfunction(value) + or inspect.isclass(value) + or inspect.ismethod(value) + ): + continue + if (res := _find_code_source(value)) is not None: + return res + + if inspect.isclass(obj): + for name in itertools.chain(obj.__dict__.keys(), dir(obj)): + try: + value = getattr(obj, name) + except AttributeError: + continue + if not ( + inspect.isfunction(value) + or inspect.isclass(value) + or inspect.ismethod(value) + ): + continue + if (res := _find_code_source(value)) is not None: + if value.__name__ != name: + _raise_resolution_error(code, toplevel) + return res + return None + + code_source = _find_code_source(toplevel) + if code_source is None: + _raise_resolution_error(code, toplevel) + return toplevel.__qualname__, code_source.strip(".") + + +@dataclasses.dataclass(frozen=True) +class SystemInfo: + """ + System information including Python, PyTorch, and GPU details. + This information is used to ensure compiled artifacts can only be loaded + with compatible system configurations. + """ + + python_version: str + torch_version: str + toolkit_version: str | None + triton_version: tuple[int, int] | None + gpu_name: str | None + CHECK_GPUS = ("cuda", "xpu") + + @classmethod + def current(cls) -> "SystemInfo": + """Create a SystemInfo instance with current system information.""" + # Get GPU name if CUDA or XPU is available + gpu_name = None + from torch.utils._triton import get_triton_version + + gpu_name, toolkit_version = None, None + for device_type in cls.CHECK_GPUS: + if getattr(torch, device_type).is_available(): + try: + gpu_name = getattr(torch, device_type).get_device_name() + toolkit_version = getattr(torch.version, device_type) + break + except Exception: + pass + + return cls( + python_version=platform.python_version(), + torch_version=torch.__version__, + toolkit_version=toolkit_version, + triton_version=get_triton_version((0, 0)), + gpu_name=gpu_name, + ) + + def check_compatibility( + self, other: "SystemInfo", device_type: str = "cpu" + ) -> None: + """ + Check if this SystemInfo is compatible with another SystemInfo. + Raises RuntimeError if incompatible. + """ + if self.python_version != other.python_version: + raise RuntimeError( + f"Compile package was created with a different Python version: {self.python_version}" + ) + + if self.torch_version != other.torch_version: + raise RuntimeError( + f"Compile package was created with a different PyTorch version: {self.torch_version}" + ) + if device_type in self.CHECK_GPUS: + if not getattr(torch, device_type).is_available(): + raise RuntimeError(f"{device_type} is not available") + + if self.toolkit_version != other.toolkit_version: + raise RuntimeError( + f"Compile package was created with a different toolkit version: {self.toolkit_version}" + ) + + if ( + other.triton_version != (0, 0) + and self.triton_version != other.triton_version + ): + raise RuntimeError( + f"Compile package was created with a different Triton version: {self.triton_version}" + ) + + # Check GPU name if CUDA/XPU was used + if other.gpu_name is not None and self.gpu_name != other.gpu_name: + raise RuntimeError( + f"Compile package was created with different GPU: " + f"cached={self.gpu_name}, current={other.gpu_name}" + ) + + +@dataclasses.dataclass +class _DynamoCacheEntry: + codes: list[_DynamoCodeCacheEntry] + source_info: SourceInfo + device_type: str + system_info: SystemInfo = dataclasses.field(default_factory=SystemInfo.current) + fn_name: str | None = None + fn_first_lineno: str | None = None + + @property + def backend_ids(self) -> set[_BackendId]: + return {backend_id for code in self.codes for backend_id in code.backend_ids} + + def check_versions(self) -> None: + """Check if the current system is compatible with the system used to create this cache entry.""" + current_system_info = SystemInfo.current() + self.system_info.check_compatibility(current_system_info, self.device_type) + + def debug_info(self) -> dict[str, Any]: + assert len(self.codes) > 0 + return { + "num_codes": str(len(self.codes)), + "fn_name": self.fn_name, + "fn_first_lineno": self.fn_first_lineno, + "device_type": self.device_type, + "backend_ids": list(self.backend_ids), + } + + +from torch.compiler._cache import ( + CacheArtifact, + CacheArtifactFactory, + CacheArtifactManager, +) + + +@CacheArtifactFactory.register +class PrecompileCacheArtifact(CacheArtifact): + def populate_cache(self) -> None: + DynamoCache._write_to_local_cache(self.content, self.key) + + @staticmethod + def type() -> str: + return "precompile" + + +@dataclasses.dataclass +class PrecompileCacheEntry: + """ + A full cache entry for caching precompile, for a toplevel torch.compile. + Consists of a _DynamoCacheEntry, which contains all the dynamo related contents, + and a set of backends content. In general, the backend content here will always + be of type precompile_context.BackendCacheArtifact + """ + + dynamo: _DynamoCacheEntry + backends: dict[_BackendId, Any] + + @staticmethod + def from_cache_entry( + cache_entry: _DynamoCacheEntry, backends: dict[_BackendId, Any] + ) -> Optional["PrecompileCacheEntry"]: + backend_content: dict[_BackendId, Any] = {} + + for code in cache_entry.codes: + for backend_id in code.backend_ids: + if backend_id not in backends: + logger.warning("Backend not found") + debug_str = json.dumps( + { + "entry": cache_entry.debug_info(), + "missing_backend": backend_id, + } + ) + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "dynamo_cache_bypass", + "encoding": "json", + }, + payload_fn=lambda: debug_str, + expect_trace_id=False, + ) + code.bypassed = True + break + else: + backend_content[backend_id] = backends[backend_id] + + return PrecompileCacheEntry(dynamo=cache_entry, backends=backend_content) + + +def _hash_source(source: str) -> str: + sha256_hash = hashlib.sha256() + sha256_hash.update(source.encode()) + return sha256_hash.hexdigest() + + +def _get_sourcelines( + m: types.ModuleType, firstlineno: int, lastlineno: int +) -> list[str]: + return inspect.getsourcelines(m)[0][firstlineno - 1 : lastlineno - 1] + + +def _hash_sourcelines(m: types.ModuleType, firstlineno: int, lastlineno: int) -> str: + return _hash_source("".join(_get_sourcelines(m, firstlineno, lastlineno))) + + +def _compile_frame_context( + code: types.CodeType, +) -> contextlib.AbstractContextManager[None]: + from torch._dynamo.convert_frame import get_compile_id, log_dynamo_start + from torch._guards import compile_context, CompileContext + + # Each code represents a new compile frame + # recompiles on the same frame are all saved + # under the same cache entry, so we don't have recompile ids + # i.e. If cold start had 0/0, 0/1, 1/0, 1/1, these would be + # collapsed into 0/0, 1/0 on warm. + # pyrefly: ignore [deprecated] + @contextlib.contextmanager + def _ctx() -> Iterator[None]: + increment_frame() + compile_id = get_compile_id(frame_state={}) + with ( + compile_context(CompileContext(compile_id)), + dynamo_timed( + "_compile.compile_inner", + phase_name="entire_frame_compile", + dynamo_compile_column_us="dynamo_cumulative_compile_time_us", + # TODO: save all relevant compilation metrics + metadata={ + "frame_key": str(torch._dynamo.utils.curr_frame), + "co_name": code.co_name, + "co_filename": code.co_filename, + "co_firstlineno": code.co_firstlineno, + }, + ), + ): + log_dynamo_start(code) + yield + + return _ctx() + + +class CompilePackage: + """ + CompilePackage is considered a low level component and should not be directly exposed to + end users. It has the following interface: + + 1. `CompilePackage.__init__()` which optionally takes previously serialized dynamo states. + a. when `dynamo` argument is None, it will construct a brand new CompilePackage object. + b. when `dynamo` argument is not None, it will load a pre-compiled dynamo state. + 2. `package.save()` which dumps the dynamo and backend states to a DynamoCacheEntry object. + 3. `package.install(backends) which will handle all the side-effectful global scope + updates with compiled functions and resume functions. + """ + + def __init__( + self, + fn: Callable[..., Any] | None, + dynamo: _DynamoCacheEntry | None = None, + ignore_inlined_sources: bool = False, + ) -> None: + self._innermost_fn = None + self._codes: dict[types.CodeType, _DynamoCodeCacheEntry] = {} + + self._current_entry: _DynamoCodeCacheEntry | None = None + self._installed_globals: dict[types.ModuleType, list[str]] = {} + # device_type that model compiled with. + self._device_type = "cpu" + + # For debugging/testing purpose only. + self._cached_backends: dict[_BackendId, Any] = {} + self._source_info: SourceInfo = SourceInfo(inlined_sources=set()) + self._resume_codes: set[types.CodeType] = set() + self._initialized = False + if fn is not None: + self.initialize(fn, dynamo, ignore_inlined_sources) + self.uninstall() + self.validate() + + def is_initialized(self) -> bool: + return self._initialized + + def initialize( + self, + fn: Any, + dynamo: _DynamoCacheEntry | None = None, + ignore_inlined_sources: bool = False, + ) -> None: + from .eval_frame import innermost_fn + + assert not self._initialized + self._source_info = SourceInfo(inlined_sources=set()) + self._innermost_fn = innermost_fn(fn) # type: ignore[assignment] + assert self._innermost_fn is not None + if dynamo is not None: + assert isinstance(dynamo, _DynamoCacheEntry) + dynamo.check_versions() + if not ignore_inlined_sources: + for code in dynamo.source_info.inlined_sources: + m = importlib.import_module(code.module) + checksum = _hash_sourcelines(m, code.firstlineno, code.lastlineno) + if checksum != code.checksum: + raise RuntimeError( + f"Source code changes detected for {code.module} (line {code.firstlineno} - line {code.lastlineno})" + ) + + self._source_info = dynamo.source_info + + main, *codes = dynamo.codes + self._codes = {self._innermost_fn.__code__: main} + for code in codes: + self._codes[SerializedCode.to_code_object(code.python_code)] = code + else: + self._add_function( + self._innermost_fn.__code__, self._innermost_fn.__module__ + ) + self._initialized = True + + def _add_function( + self, + python_code: types.CodeType, + python_module: str, + function_name: _FunctionId | None = None, + code_source: str | None = None, + install_to_global: bool = False, + ) -> None: + if python_code not in self._codes: + code = _DynamoCodeCacheEntry( + python_code=SerializedCode.from_code_object(python_code), + python_module=python_module, + function_names=[], + guarded_codes=[], + import_sources={}, + backend_ids=[], + code_source=code_source, + install_to_global=install_to_global, + ) + self._codes[python_code] = code + else: + code = self._codes[python_code] + assert code.python_module == python_module + assert code.install_to_global == install_to_global + assert code.code_source == code_source + + if function_name is not None: + code.function_names.append(function_name) + + @property + def cached_backends(self) -> dict[_BackendId, Any]: + return self._cached_backends + + @functools.cached_property + def source_id(self) -> str: + assert self._innermost_fn is not None + return CompilePackage.source_id_from_fn(self._innermost_fn) + + def _add_user_function(self, code: types.CodeType) -> None: + function_name, code_source = _get_code_source(code) + module = inspect.getmodule(code) + if module is None: + raise PackageError(f"Cannot find module for code {code}") + self._add_function( + code, + module.__name__, + function_name=_FunctionId(function_name), + code_source=code_source, + ) + + @contextlib.contextmanager + def code_context(self, code: types.CodeType) -> Generator[None, None, None]: + assert self._current_entry is None + + # Sometimes user code cannot be inlined in dynamo resulting in extra user code + # being compiled. We should record these as when they are actually invoked. + if code not in self._codes: + self._add_user_function(code) + + entry = self._codes[code] + self._current_entry = entry + try: + yield + finally: + entry.has_compile_id = True + self._current_entry = None + + def add_guarded_code( + self, + guards_state: bytes, + dynamo_code: types.CodeType, + ) -> None: + assert self._current_entry is not None + if self._current_entry.bypassed: + return + guarded_code_entry = _GuardedCodeCacheEntry( + guards_state=guards_state, + dynamo_code=SerializedCode.from_code_object(dynamo_code), + ) + self._current_entry.guarded_codes.append(guarded_code_entry) + + def add_inlined_source(self, sources: list[types.CodeType]) -> None: + assert self._current_entry is not None + if self._current_entry.bypassed: + return + for code in sources: + if code in self._resume_codes: + continue + self._source_info.add_code(code) + + def update_device_type(self, graph: torch.fx.Graph | None) -> None: + self._device_type = _graph_device_type(graph) + + def bypass_current_entry(self) -> None: + assert self._current_entry is not None + self._current_entry.bypassed = True + + def add_resume_function( + self, + python_code: types.CodeType, + python_module: str, + function_name: str, + ) -> None: + self._add_function( + python_code, + python_module, + function_name=_FunctionId(function_name), + install_to_global=True, + ) + self._resume_codes.add(python_code) + + def add_import_source(self, alias: str, module_name: str) -> None: + assert self._current_entry is not None + self._current_entry.import_sources[alias] = module_name + + def add_backend_id(self, backend_id: str, backend: Any | None = None) -> None: + assert self._current_entry is not None + assert backend_id.startswith("__compiled_fn_") # sanity check + backend_id = _BackendId(backend_id) + self._current_entry.backend_ids.append(backend_id) + if backend is not None: + self._cached_backends[backend_id] = backend + + def validate(self) -> None: + assert self._current_entry is None + assert self._innermost_fn is not None + assert self._initialized + assert next(iter(self._codes)) is self._innermost_fn.__code__ + + def _install_global(self, module: types.ModuleType, name: str, value: Any) -> None: + module.__dict__[name] = value + self._installed_globals.setdefault(module, []).append(name) + + def uninstall(self) -> None: + from torch._C._dynamo.eval_frame import _reset_precompile_entries + + assert self._innermost_fn is not None + for module, names in self._installed_globals.items(): + for name in names: + module.__dict__.pop(name) + + self._installed_globals = {} + + _reset_precompile_entries(self._innermost_fn.__code__) + + def install(self, backends: dict[_BackendId, Any]) -> None: + """ + Sync the package states to the compiled function. This includes the following actions: + 1. Clean up the previously installed states. + 2. Install the compiled functions to global scopes. + 3. Install the precompiled cache entries to ExtraStates on the code object. + """ + from torch._C._dynamo.eval_frame import _load_precompile_entry + + from .output_graph import get_builtins_dict + + self.uninstall() + for code, entry in self._codes.items(): + context = ( + _compile_frame_context(code) + if entry.has_compile_id + else contextlib.nullcontext() + ) + with context: + module = sys.modules[entry.python_module] + for alias, module_name in entry.import_sources.items(): + self._install_global( + module, alias, importlib.import_module(module_name) + ) + target_code = code + if entry.install_to_global: + for function_name in entry.function_names: + if code.co_freevars: + # Resume functions with freevars need a factory + # that takes a closure tuple, matching + # install_resume_function_global in output_graph.py. + f_globals = module.__dict__ + fn_name = function_name + + def _make_fn( + closure: tuple[types.CellType, ...], + _code: types.CodeType = code, + _globals: dict[str, Any] = f_globals, + _name: str = fn_name, + ) -> types.FunctionType: + return types.FunctionType( + _code, _globals, _name, None, closure + ) + + self._install_global(module, function_name, _make_fn) + else: + fn = types.FunctionType( + code, module.__dict__, function_name + ) + self._install_global(module, function_name, fn) + if entry.code_source: + target_code = _lookup_code(entry) + + if entry.bypassed: + # If the entry is bypassed, do not install backends + # or guarded codes. + continue + + for backend_id in entry.backend_ids: + if backend_id not in backends: + raise RuntimeError( + f"Backend {backend_id} is not found in the given backends" + ) + with dynamo_timed( + "after_deserialization", phase_name="backend_compile" + ): + backend = backends[backend_id].after_deserialization() + self._install_global( + module, + backend_id, + torch._dynamo.disable(backend), + ) + + if len(entry.guarded_codes) == 0: + # Dynamo generates empty graph for trivial functions, should just skip them + # in these cases. + torch._dynamo.eval_frame.skip_code(target_code) + + for guarded_code in entry.guarded_codes: + with dynamo_timed("precompile_load_guards"): + guards_state = load_guards_state(guarded_code.guards_state) + runtime_global_scope = sys.modules[entry.python_module].__dict__ + # The installed builtins dict might be absent from the runtime + # while loading guards. Populate it if it's missing. + if ( + builtin_dict_name + := guards_state.output_graph.name_of_builtins_dict_key_in_fglobals + ): + builtins_dict = get_builtins_dict(runtime_global_scope) + if builtin_dict_name in runtime_global_scope: + assert ( + runtime_global_scope[builtin_dict_name] is builtins_dict + ) + else: + runtime_global_scope[builtin_dict_name] = builtins_dict + assert isinstance(guards_state, torch._dynamo.guards.GuardsState) + with dynamo_timed("precompile_build_guards"): + guard_manager = load_guard_manager( + guards_state, target_code, runtime_global_scope + ) + _load_precompile_entry( + target_code, + guard_manager, + SerializedCode.to_code_object(guarded_code.dynamo_code), + ) + + def cache_entry(self) -> _DynamoCacheEntry: + self.validate() + assert self._innermost_fn is not None + return _DynamoCacheEntry( + codes=list(self._codes.values()), + source_info=self._source_info, + device_type=self._device_type, + fn_name=self._innermost_fn.__qualname__, + fn_first_lineno=self._innermost_fn.__code__.co_firstlineno, + ) + + @staticmethod + def source_id_from_fn(fn: Callable[..., Any]) -> str: + from .eval_frame import innermost_fn + + innermost_fn_ = innermost_fn(fn) + + sha256_hash = hashlib.sha256() + sha256_hash.update(innermost_fn_.__qualname__.encode()) + sha256_hash.update(str(innermost_fn_.__code__.co_firstlineno).encode()) + return sha256_hash.hexdigest() + + +_Backends = dict[_BackendId, Any] + + +class DynamoStore(abc.ABC): + """ + A DynamoStore tracks active CompilePackages, and provides methods to store and retrieve them. + + This is an abstract base class for different storage implementations. + """ + + def record_package(self, package: CompilePackage) -> None: + """ + Records a package to PrecompileContext, so that it can be serialized later. + """ + from torch._dynamo.precompile_context import PrecompileContext + + cache_entry = package.cache_entry() + PrecompileContext.record_dynamo_cache_entry( + cache_entry=cache_entry, key=package.source_id + ) + + def record_eager_backend(self, backend_id: _BackendId, backend: Any) -> None: + """ + Records eager fx graphs to PrecompileContext for testing purposes. + """ + from torch._dynamo.precompile_context import ( + EagerCacheArtifact, + PrecompileContext, + ) + + result = EagerCacheArtifact(key=backend_id, content=backend) + PrecompileContext.record_artifact(result) + + @abc.abstractmethod + def clear(self) -> None: ... + + @abc.abstractmethod + def write( + self, + cache_entry: PrecompileCacheEntry, + path: str, + ) -> None: + """ + Abstract method to write dynamo cache entry and backends to storage. + + Args: + dynamo: The dynamo cache entry to write + backends: Dictionary of backend content to write + path: Path or key to identify where to write the data + """ + ... + + def save_cache_entry(self, cache_entry: _DynamoCacheEntry, key: str) -> None: + """ + Saves a package to a given path. Grabs backends from PrecompileContext. + """ + from torch._dynamo.precompile_context import ( + BackendCacheArtifact, + PrecompileContext, + ) + + backend_content: _Backends = {} + for backend_id in cache_entry.backend_ids: + serialized_backend = PrecompileContext.serialize_artifact_by_key(backend_id) + if serialized_backend is None: + raise RuntimeError( + f"Backend {backend_id} is not found in the given backends" + ) + assert isinstance(serialized_backend, BackendCacheArtifact) + backend_content[backend_id] = serialized_backend + + entry = PrecompileCacheEntry(cache_entry, backend_content) + + self.write(entry, key) + + def save_package(self, package: CompilePackage, key: str) -> None: + """ + Saves a package to a given path. Grabs backends from PrecompileContext. + """ + self.record_package(package) + cache_entry = package.cache_entry() + self.save_cache_entry(cache_entry, key) + + @abc.abstractmethod + def read(self, path: str) -> PrecompileCacheEntry: + """ + Abstract method to read dynamo cache entry and backends from storage. + + Args: + path: Path or key to identify where to read the data from + + Returns: + A tuple containing (dynamo_cache_entry, backend_content) + """ + ... + + def load_cache_entry(self, key: str) -> PrecompileCacheEntry: + from torch._dynamo.precompile_context import ( + BackendCacheArtifact, + PrecompileContext, + ) + + precompile_entry = self.read(key) + for backend in precompile_entry.backends.values(): + assert isinstance(backend, BackendCacheArtifact) + PrecompileContext.record_artifact(backend) + + return precompile_entry + + def load_package( + self, fn: Any, key: str + ) -> tuple[CompilePackage, dict[_BackendId, Any]]: + """ + Loads a package from a given path and returns it plus a list of deserialized backends + """ + entry = self.load_cache_entry(key) + package = CompilePackage(fn, entry.dynamo) + return package, entry.backends + + +class InMemoryDynamoStore(DynamoStore): + """ + A DynamoStore implementation that keeps state about CompilePackages in memory. + """ + + def __init__(self) -> None: + self.packages: dict[str, PrecompileCacheEntry] = {} + + def clear(self) -> None: + self.packages.clear() + + def write( + self, + cache_entry: PrecompileCacheEntry, + path: str, + ) -> None: + """ + Store the dynamo cache entry and backends in memory instead of writing to disk. + """ + self.packages[path] = cache_entry + + def read(self, path: str) -> PrecompileCacheEntry: + """ + Read dynamo cache entry and backends from memory. + """ + if path not in self.packages: + raise RuntimeError(f"No package found with key {path}") + + return self.packages[path] + + +class DiskDynamoStore(DynamoStore): + """ + A DynamoStore implementation that keeps state about CompilePackages on disk. + """ + + def __init__(self, path_prefix: str = "") -> None: + """ + Initialize a DiskDynamoStore with a path prefix. + + Args: + path_prefix: Prefix directory for where to put CompilePackages on disk + """ + self._path_prefix = path_prefix + + def path_prefix(self) -> str: + return self._path_prefix + + def clear(self) -> None: + """ + Clear all CompilePackages from disk. + """ + if self.path_prefix(): + shutil.rmtree(self.path_prefix(), ignore_errors=True) + + def write( + self, + cache_entry: PrecompileCacheEntry, + path: str, + ) -> None: + """ + Write dynamo cache entry and backends to disk. + """ + try: + pickled_content: bytes = pickle.dumps(cache_entry) + CacheArtifactManager.record_artifact( + PrecompileCacheArtifact.type(), path, pickled_content + ) + self._write_to_local_cache(pickled_content, path) + except Exception as e: + raise RuntimeError(f"Failed to save package to {path}: {e}") from e + + def _write_to_local_cache(self, pickled_content: bytes, path: str) -> None: + from torch._inductor.codecache import write_atomic + + path = os.path.join(self.path_prefix(), path) if self.path_prefix() else path + try: + os.makedirs(path, exist_ok=True) + write_atomic(os.path.join(path, "entry"), pickled_content) + except Exception as e: + raise RuntimeError(f"Failed to save package to {path}: {e}") from e + + def read(self, path: str) -> PrecompileCacheEntry: + """ + Read dynamo cache entry and backends from disk. + """ + path = os.path.join(self.path_prefix(), path) if self.path_prefix() else path + try: + with open(os.path.join(path, "entry"), "rb") as f: + pickled_content = f.read() + entry = pickle.loads(pickled_content) + return entry + except Exception as e: + raise RuntimeError(f"Failed to load package from path {path}: {e}") from e + + +class DiskDynamoCache(DiskDynamoStore): + """ + Special DiskDynamoStore which adds some helper functions for automatically + tracking paths of packages + """ + + def save(self, package: CompilePackage) -> None: + """ + Saves a package to a given path. Grabs backends from PrecompileContext. + """ + key = package.source_id + logger.info("Saving CompilePackage for %s", package.source_id) + super().save_package(package, key) + + def load(self, fn: Callable[..., Any]) -> PrecompileCacheEntry | None: + """ + Loads a package from a given path and returns it plus a list of deserialized backends + """ + key = CompilePackage.source_id_from_fn(fn) + logger.info("Loading CompilePackage for %s", key) + path = os.path.join(self.path_prefix(), key) + if os.path.exists(path): + try: + result = super().load_cache_entry(key) + counters["dynamo_cache"]["dynamo_cache_hit"] += 1 + return result + except Exception: + counters["dynamo_cache"]["dynamo_cache_error"] += 1 + logger.warning("Failed to load package from path %s", exc_info=True) + return None + logger.info("No package found for %s", key) + counters["dynamo_cache"]["dynamo_cache_miss"] += 1 + return None + + def load_and_install_package(self, fn: Callable[..., Any]) -> CompilePackage | None: + """ + Load directly into a package and install backends + """ + results = self.load(fn) + if results is None: + return None + else: + package = CompilePackage(fn, results.dynamo) + package.install(results.backends) + return package + + def path_prefix(self) -> str: + return os.path.join(cache_dir(), "dynamo") + + +def cache_dir() -> str: + from torch._inductor.runtime.cache_dir_utils import cache_dir + + return cache_dir() + + +DynamoCache = DiskDynamoCache(os.path.join(cache_dir(), "dynamo")) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/pgo.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/pgo.py new file mode 100644 index 0000000000000000000000000000000000000000..80bb6ddfda1ccb33808162cb06503feecd0726ff --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/pgo.py @@ -0,0 +1,1055 @@ +""" +Profile Guided Optimization (PGO) implementation for Dynamo. + +This module provides functionality for caching and managing code state profiles +that guide optimization decisions in Dynamo. It implements both local and remote +caching mechanisms for storing profile information across runs, handles profile +merging across distributed ranks, and manages the lifecycle of profile data +during compilation. The profiles track dynamic vs static properties of tensors +and help Dynamo make better specialization decisions. +""" + +from __future__ import annotations + +import base64 +import copy +import dataclasses +import enum +import functools +import logging +import os +import pickle +import re +import zlib +from collections import defaultdict +from typing import TYPE_CHECKING, TypeVar +from typing_extensions import override, Self + +import torch._dynamo.config +import torch._utils_internal +import torch.compiler.config +import torch.distributed as dist +from torch._dynamo.utils import ( + CompileEventLogger, + dynamo_timed, + set_feature_use, + warn_once, +) +from torch._environment import is_fbcode +from torch._logging._internal import trace_structured_artifact +from torch.compiler._cache import ( + CacheArtifact, + CacheArtifactFactory, + CacheArtifactManager, +) +from torch.utils._ordered_set import OrderedSet + + +if TYPE_CHECKING: + import types + + from torch._dynamo.symbolic_convert import InstructionTranslator + from torch._inductor.remote_cache import JsonDataTy, RemoteCache + + +class ReservedWorkflowIdUserError(ValueError): + pass + + +log = logging.getLogger(__name__) + +LOCK_TIMEOUT = 10 + +# How does in memory representation work? Concretely, this module is +# responsible for holding GLOBAL state representing the state it holds, no +# other copies permitted. So we retire frame_state entirely and store it +# here. This should be reset when Dynamo is reset. We never GC information +# (similar to how the filesystem doesn't get cleaned up except by tmp +# cleaner), so the expectation is the information is relatively cheap and we +# don't mind leaking it. + + +# How exactly did we design the cache key? Here are some of the questions: +# +# - JOB_ID: Do we have a unique identifier for the "training run" (such that +# it stays the same if we're running the same code, and changes if we're +# running something different). +# +# - RANK: Are we sharing the cache across ranks, or does each rank get +# an individual cache? +# +# We choose to require job_id for PGO cache. This is to prevent +# situations where unrelated invocations of PyTorch unpredictably cause +# changes to each other's behavior. With a job_id, at least you know there +# is some "state" associated with it. (State dict might be another way to +# tell if a run is related or not.) You can opt-in to YOLO everything +# aliases everything by passing a shared job_id for all your invocations. +# +# We choose to NOT share PGO cache across ranks. With no RANK_SHARING, there +# is never contention between runs, so we can leisurely update a bundle with +# information we need. Because we are grouped by job_id, we can have a single +# consolidated bundle for everything (or not; maybe worry about O(n^2) IO if +# we updated every compile--let's just instrument this.) Can even take a +# filelock for extra safety (expect no contention); expect 50ns overhead from +# uncontended filelock. +# +# If we did share ranks, everyone is storming to modify the same cache files. +# We can do this by having folks atomic write to a CAS-store and then having +# readers do on-the-fly merging (this can be implemented in remote using +# prefix iteration). As an optional optimization, one rank can be elected to +# handling bundling post facto (ideally, this is done async, after quiescence, +# without compiler collective need to wait for everyone to finish writing +# their bits.) Not sure how you can avoid a listdir because if some rank shows +# up with some new entries we need to pull them in ASAP (unless you want to +# delay bundling). +# +# But compiler collectives fill a similar niche: compilers chat with each +# other so rank 0 has collected everything. So elect rank 0 only to write the +# bundle. Don't even need CAS-store atomic write; just one rank writing an +# updating bundles. The point is that use compiler collectives to share +# profiles across ranks, but use the PGO cache to persist profiles per rank +# across attempts. No need to have one mechanism to do everything. + + +@functools.cache +def _hash_containing_file(filepath: str) -> str: + # if the file does not exists we consider filepath to be the hash. + if not os.path.exists(filepath): + return filepath + + with open(filepath, "rb") as file: + content = file.read() + crc32_value = zlib.crc32(content) + hash = format(crc32_value & 0xFFFFFFFF, "08x") + return hash + + +def _get_closure_content(content: types.CellType) -> object: + if callable(content) and hasattr(content, "__code__"): + return content.__code__ + return None + + +def _get_cell_hash_content(content: types.CellType) -> object: + # Safely extract cell contents without blowing up the entire tuple hash + try: + return _get_closure_content(content.cell_contents) + except ValueError: + return None + + +@dataclasses.dataclass(frozen=True) +class CodeId: + filename: str + firstlineno: int + name: str + # When a job restart, the code can be copied to a different path than the previous attempt. In that case + # self.filename will have a different value, we do not want to consider those differences. Instead we + # hash the content of the file and use it as an identifier of the file. + # + # self.filename is kept in the object to give readable information/pointer to the actual file, in a local + # code state it will refer to the first seen file path. + file_hash: str + closure_hash: int | None = None + + # Exclude file name. + def __eq__(self, other: object) -> bool: + if not isinstance(other, CodeId): + return False + return ( + self.file_hash == other.file_hash + and self.firstlineno == other.firstlineno + and self.name == other.name + and self.closure_hash == other.closure_hash + ) + + # Ensure if two CodeIds are the same, then they have the same hash by excluding filename. + def __hash__(self) -> int: + return hash((self.file_hash, self.name, self.firstlineno, self.closure_hash)) + + def __str__(self) -> str: + return f"hash({self.file_hash}){self.filename}:{self.firstlineno}:{self.name}" + + @staticmethod + def make( + code: types.CodeType, closure: tuple[types.CellType, ...] | None = None + ) -> CodeId: + closure_hash = None + if closure: + closure_hash = hash(tuple(_get_cell_hash_content(c) for c in closure)) + + return CodeId( + code.co_filename, + code.co_firstlineno, + code.co_name, + _hash_containing_file(code.co_filename), + closure_hash, + ) + + +@dataclasses.dataclass +class CodeState: + automatic_dynamic: defaultdict[str, FrameStateSizeEntry] = dataclasses.field( + default_factory=lambda: defaultdict(FrameStateSizeEntry) + ) + + +_INIT_CODE_STATE: defaultdict[CodeId, CodeState] | None = None +_CODE_STATE: defaultdict[CodeId, CodeState] | None = None +_LOGGED_DYNAMIC_ALLOWLIST: bool = False +_KNOWN_DYNAMIC_SOURCES: set[str] = set() + + +@dataclasses.dataclass(frozen=True) +class InferStride: + """ + Denotes the quantity stride[dim] * size[dim], which is what the stride would + be for the next physical dimension that results in a contiguous layout. + + For example, given size = [2, 3], stride = [3, 1], we can replace this with + stride = [InferStride(1), 1], because InferStride(1) = stride[1] * size[1] = 1 * 3 = 3 + + Indirecting the representation in this way is important for the join operation + on strides as if we join [2, 3][3, 1] and [2, 4][4, 1], + we don't want [2, None][None, 1] which would get eventually symbolized into + [2, s0][s1, 1] (notice that the relationship between s0 and s1 is broken). + If we instead rewrite the expressions as InferStride so we have [2, 3][InferStride(1), 1] + and [2, 4][InferStride(1), 1] we now join to [2, None][InferStride(1), 1] will + result in [2, s0][s0, 1], as desired. + """ + + dim: int + + +_T = TypeVar("_T") + + +class AutoUnset(enum.Enum): + """ + The identity element of our semilattice, a generic "don't know" element that + is always subsumed when we get more information. + """ + + token = 0 + + +auto_unset = AutoUnset.token + + +class AutoDynamic(enum.Enum): + """ + The top element of our (bounded) semilattice, whenever you merge this with + any other element you always get it again + """ + + token = 0 + + +auto_dynamic = AutoDynamic.token + + +@dataclasses.dataclass +class FrameStateSizeEntry: + scalar: int | AutoDynamic | AutoUnset = dataclasses.field(default=auto_unset) + # NB: We don't have cases where we have a known dimensionality but + # we know NOTHING about the individual sizes + size: AutoDynamic | AutoUnset | tuple[int | AutoDynamic, ...] = dataclasses.field( + default=auto_unset + ) + stride: AutoDynamic | AutoUnset | tuple[int | AutoDynamic | InferStride, ...] = ( + dataclasses.field(default=auto_unset) + ) + excluded_sizes: tuple[int | None, ...] | None = dataclasses.field( + default=None, compare=False + ) + excluded_scalar: int | None = dataclasses.field(default=None, compare=False) + + def render(self) -> str: + # Special cases + def render_single(s: int | AutoDynamic | AutoUnset | InferStride) -> str: + if s is auto_dynamic: + return "?" + elif s is auto_unset: + # This basically shouldn't happen, this is for debugging + return "auto unset" + elif isinstance(s, InferStride): + return f"S({s.dim})" + else: + return str(s) + + def render_tuple(ss: tuple[int | AutoDynamic | InferStride, ...]) -> str: + return "[" + ", ".join(render_single(s) for s in ss) + "]" + + # Common cases + if self.size is auto_dynamic and self.stride is auto_dynamic: + if self.scalar is auto_dynamic: + return "fully dynamic scalar or tensor" + else: + return f"scalar {self.scalar}" + elif self.scalar is auto_dynamic: + if isinstance(self.size, tuple) and isinstance(self.stride, tuple): + return f"tensor size={render_tuple(self.size)} stride={render_tuple(self.stride)}" + + # Fallback + return f"unusual {repr(self)}" + + def __post_init__(self) -> None: + assert not isinstance(self.scalar, torch.SymInt), self.scalar + if isinstance(self.size, tuple): + for s in self.size: + assert not isinstance(s, torch.SymInt), s + if isinstance(self.stride, tuple): + for s1 in self.stride: + assert not isinstance(s1, torch.SymInt), s1 + + def is_size_dynamic(self, dim: int) -> bool: + if self.size is auto_dynamic: + return True + if self.size is auto_unset: + return False + return self.size[dim] is auto_dynamic + + def is_stride_dynamic(self, dim: int) -> bool: + # At the moment, dynamic strides is a bit buggy. Good test case + # here is `PYTORCH_TEST_WITH_DYNAMO=1 python test/test_autograd.py + # TestAutograd.test_gradcheck_jacobian_mismatch` + # + # This if statement preserves historical behavior, which is that we + # ONLY make strides dynamic if the size is exactly static everywhere. + # We could potentially relax this but in general we should be very + # careful about when to infer dynamic strides. + # + # Actually, the existing algorithm is already somewhat problematic. + # Suppose a tensor that is sometimes: + # f32[2, 3, 5][15, 5, 1] and other times + # f32[2, 3, 5][5, 10, 1] (specifically, dim 0 and 1 are physically transposed). + # If we infer strides should be (DYNAMIC, DYNAMIC, 1). But this is + # silly: we really should have just guarded on dim order. + if not ( + isinstance(self.size, tuple) and all(type(s) is int for s in self.size) + ): + return False + if self.stride is auto_dynamic: + return True + if self.stride is auto_unset: + return False + return self.stride[dim] is auto_dynamic + + @staticmethod + def _munge_symint(xs: tuple[int, ...]) -> tuple[AutoDynamic | int, ...]: + return tuple(auto_dynamic if isinstance(x, torch.SymInt) else x for x in xs) + + @classmethod + def make_scalar(cls, x: int) -> FrameStateSizeEntry: + return FrameStateSizeEntry(scalar=x, size=auto_dynamic, stride=auto_dynamic) + + @classmethod + def make_tensor( + cls, size: tuple[int, ...], stride: tuple[int, ...] + ) -> FrameStateSizeEntry: + return FrameStateSizeEntry( + scalar=auto_dynamic, + size=cls._munge_symint(size), + stride=cls._munge_symint(stride), + ) + + @classmethod + def make_size(cls, size: tuple[int, ...]) -> FrameStateSizeEntry: + return FrameStateSizeEntry( + scalar=auto_unset, + size=cls._munge_symint(size), + stride=auto_unset, + ) + + @staticmethod + def _merge_atom(x: _T, y: _T) -> AutoDynamic | _T: + if x is auto_unset: + return y + if y is auto_unset: + return x + if x is auto_dynamic or y is auto_dynamic or x != y: + return auto_dynamic + return x + + @classmethod + def _merge_atom_tup( + cls, + xs: AutoDynamic | AutoUnset | tuple[_T, ...], + ys: AutoDynamic | AutoUnset | tuple[_T, ...], + ) -> AutoDynamic | AutoUnset | tuple[AutoDynamic | _T, ...]: + if xs is auto_unset: + return ys + if ys is auto_unset: + return xs + if xs is auto_dynamic or ys is auto_dynamic: + return auto_dynamic + if len(xs) != len(ys): + return auto_dynamic + return tuple(cls._merge_atom(x, y) for x, y in zip(xs, ys)) + + def __ior__(self, other: Self) -> Self: + # Record current static sizes before merge. For dims that become + # dynamic, the exclusion guard will reject these values so inputs + # fall through to the earlier, more specialized cache entry. + # Already-dynamic dims become None and are ignored by the guard. + # When no dim transitions, clear stale excluded_sizes so later + # compilations don't inherit exclusions from earlier transitions. + new_size = self._merge_atom_tup(self.size, other.size) + if isinstance(self.size, tuple): + if new_size != self.size: + self.excluded_sizes = tuple( + s if type(s) is int else None for s in self.size + ) + elif self.excluded_sizes is not None: + self.excluded_sizes = None + self.size = new_size + # Same idea for scalars: record the static value about to become dynamic. + # Re-derive like excluded_sizes: only set when transitioning from a + # concrete int, clear when already dynamic. + if ( + type(self.scalar) is int + and type(other.scalar) is int + and self.scalar != other.scalar + ): + self.excluded_scalar = self.scalar + elif self.scalar is auto_dynamic and self.excluded_scalar is not None: + self.excluded_scalar = None + self.scalar = self._merge_atom(self.scalar, other.scalar) + self.stride = self._merge_atom_tup(self.stride, other.stride) + return self + + +def update_automatic_dynamic( + tx: InstructionTranslator, + name: str, + entry: FrameStateSizeEntry, + *, + is_unspecialized_nn_module: bool = False, +) -> FrameStateSizeEntry: + code_id = CodeId.make(tx.f_code, tx.closure) + frame_state = get_code_state()[code_id] + if torch._dynamo.config.automatic_dynamic_shapes: + is_update = name in frame_state.automatic_dynamic + mut_entry = frame_state.automatic_dynamic[name] + old_entry = copy.copy(mut_entry) + mut_entry |= entry + + # Do some logs (damn, I spend more code logging than I do actually doing + # the updates lol) + if is_update and old_entry.scalar != mut_entry.scalar: + log.debug( + "automatic dynamic int %s val %s != %s", + name, + entry.scalar, + old_entry.scalar, + ) + CompileEventLogger.instant( + "automatic_dynamic", + { + "name": name, + "dim_changed": "scalar", + "reason": "scalar change", + "cached": str(old_entry.scalar), + "new": str(entry.scalar), + }, + ) + if is_unspecialized_nn_module: + log.info( + "%s is converted to a symbolic integer. It is an attribute of a " + "user defined nn module class. If you wish to keep it static, you can " + "mark the nn module class as `torch._dynamo.mark_static`.", + name, + ) + + def log_tup( + tup_name: str, short_reason: str, long_reason: str, i: int | None = None + ) -> None: + entry_tup = ( + getattr(entry, tup_name) if i is None else getattr(entry, tup_name)[i] + ) + old_entry_tup = ( + getattr(old_entry, tup_name) + if i is None + else getattr(old_entry, tup_name)[i] + ) + log.debug( + "automatic dynamic %s %s %s %s != %s", + tup_name, + name, + short_reason, + # NB: We used to only report len(...) here for dim mismatch + entry_tup, + old_entry_tup, + ) + CompileEventLogger.instant( + "automatic_dynamic", + { + "name": name, + "dim_changed": "all" if i is None else i, + "reason": long_reason, + "cached": str(old_entry_tup), + "new": str(entry_tup), + }, + ) + + if is_update and old_entry.size != mut_entry.size: + if isinstance(old_entry.size, tuple) and isinstance(entry.size, tuple): + if len(old_entry.size) != len(entry.size): + log_tup("size", "dim", "dimensionality change") + else: + for i in range(len(entry.size)): + if old_entry.size[i] != entry.size[i]: + log_tup("size", f"size({i})", "size change", i) + else: + log_tup("size", "other", "other") + + if is_update and old_entry.stride != mut_entry.stride: + if isinstance(old_entry.stride, tuple) and isinstance(entry.stride, tuple): + if len(old_entry.stride) != len(entry.stride): + log_tup("stride", "dim", "dimensionality change") + else: + for i in range(len(entry.stride)): + if old_entry.stride[i] != entry.stride[i]: + log_tup("stride", f"stride({i})", "stride change", i) + else: + log_tup("stride", "other", "other") + else: + old_entry = frame_state.automatic_dynamic[name] + log.debug( + "automatic dynamic is off, overwriting int %s val %s -> %s", + name, + old_entry.scalar, + entry.scalar, + ) + frame_state.automatic_dynamic[name] = entry + mut_entry = entry + + return mut_entry + + +def process_automatic_dynamic( + tx: InstructionTranslator, + name: str, + entry: FrameStateSizeEntry, + *, + is_unspecialized_nn_module: bool = False, +) -> FrameStateSizeEntry: + if (st := tx.distributed_state) is None: + return update_automatic_dynamic( + tx, + name, + entry, + is_unspecialized_nn_module=is_unspecialized_nn_module, + ) + elif st.all_states is None: + # Preflight, always pretend as if it's static. The point here + # is we want to get through the preflight quickly, and static + # will run faster. The preexisting frame state will get + # applied anyway after we do compiler collectives. + # TODO: I'm not sure if we should just bong the entire pgo + # state here, it kind of depends if we're going to have other + # things that talk in compiler collective. Also, the PGO + # state, if we've already inferred something is automatic + # dynamic, will have lost the actual input sizes, which might + # be useful for debugging purposes (e.g., observing 0/1 + # specialization). Bonging the entire PGO state here would + # let us delete this logic here; the compiler collective + # would just directly update_automatic_dynamic + st.local_state.automatic_dynamic[name] = entry + return entry + else: + # Apply the updates. NB: all_states includes the local state + # too. + res = None + for sub_state in st.all_states: + if name in sub_state.automatic_dynamic: + res = update_automatic_dynamic( + tx, + name, + sub_state.automatic_dynamic[name], + is_unspecialized_nn_module=is_unspecialized_nn_module, + ) + assert res is not None + return res + + +def format_cache_key(key: str) -> str: + # NB: We always use global rank for keys, even though they are overkill + # for local only cache + rank = None + if dist.is_available() and dist.is_initialized(): + rank = dist.get_rank() + + tag = torch.compiler.config.cache_key_tag + return f"{key}:{rank}:{tag}" + + +def get_cache_key() -> str | None: + # TODO: info versions of these logs that log only once + if torch.compiler.config.force_disable_caches: + warn_once( + "dynamo_pgo force disabled by torch.compiler.config.force_disable_caches" + ) + return None + + # NB: We namespace the cache keys so that only user-specified job id + # can alias with each other. + if (r := torch.compiler.config.job_id) is not None: + if r.startswith("mast:"): + raise ReservedWorkflowIdUserError( + "torch.compiler.config.job_id with prefix 'mast:' is reserved for " + "automatically generated job id associated with a specific MAST job " + "name and version." + ) + return format_cache_key(r) + + if (name_version := torch._utils_internal.get_mast_job_name_version()) is not None: + mast_job_name, mast_job_version = name_version + return format_cache_key(f"mast:{mast_job_name}:{mast_job_version}") + + return None + + +def get_extra_cache_key(sticky_key: str) -> str | None: + if torch.compiler.config.force_disable_caches: + warn_once( + "dynamo_pgo force disabled by torch.compiler.config.force_disable_caches" + ) + return None + + return format_cache_key(sticky_key) + + +# This solely controls local PGO +def code_state_path(cache_key: str) -> str | None: + if not torch._dynamo.config.automatic_dynamic_local_pgo: + log.debug("automatic_dynamic_local_pgo not enabled") + return None + + from torch._inductor.runtime.runtime_utils import cache_dir + + code_state_key = re.sub(r'[<>:"/\\|?*]', "_", f"code_state_{cache_key}.pkl") + return os.path.join(cache_dir(), "dynamo", code_state_key) + + +def should_use_remote_dynamo_pgo_cache() -> bool: + if torch.compiler.config.force_disable_caches: + return False + + if (r := torch._dynamo.config.automatic_dynamic_remote_pgo) is not None: + return r + + if not is_fbcode(): + return False + + if torch._utils_internal.is_fb_unit_test(): + return False + + try: + from torch._inductor.fb.remote_cache import REMOTE_CACHE_VERSION + except ModuleNotFoundError: + return False + + return REMOTE_CACHE_VERSION >= torch._utils_internal.justknobs_getval_int( + "pytorch/remote_cache:dynamo_pgo_version" + ) + + +def get_remote_cache() -> RemoteCache[JsonDataTy] | None: + from torch._inductor.remote_cache import create_cache + + if not should_use_remote_dynamo_pgo_cache(): + return None + + return create_cache( + "dynamo-pgo", + is_fbcode(), + "FbRemoteDynamoPGOCache", + "RemoteDynamoPGOCache", + ) + + +def _collect_dynamic_sources(code_state: CodeState) -> OrderedSet[str]: + dynamic_sources: OrderedSet[str] = OrderedSet() + for src, fs in code_state.automatic_dynamic.items(): + dynamic = False + if isinstance(fs.size, tuple): + dynamic = auto_dynamic in fs.size # type: ignore[operator] + elif fs.scalar == auto_dynamic: + dynamic = True + if dynamic: + dynamic_sources.add(src) + return dynamic_sources + + +def _collect_missing_sources(all_sources: OrderedSet[str]) -> OrderedSet[str]: + from torch._dynamo.variables.builder import is_dynamic_source + + global _KNOWN_DYNAMIC_SOURCES + missing_sources: OrderedSet[str] = OrderedSet() + for src in all_sources: + if src in _KNOWN_DYNAMIC_SOURCES: + continue + elif is_dynamic_source(src): + _KNOWN_DYNAMIC_SOURCES.add(src) + continue + missing_sources.add(src) + return missing_sources + + +def log_frame_dynamic_whitelist(f_code: types.CodeType) -> None: + global _KNOWN_DYNAMIC_SOURCES + code_id = CodeId.make(f_code, None) + frame_state = get_code_state()[code_id] + all_dynamic_sources = _collect_dynamic_sources(frame_state) + frame_whitelist = ",".join(all_dynamic_sources) + missing_whitelist = ",".join(_collect_missing_sources(all_dynamic_sources)) + if frame_whitelist: + with dynamo_timed(name := "pgo.dynamic_whitelist", log_pt2_compile_event=True): + CompileEventLogger.pt2_compile( + name, + recompile_dynamic_whitelist=frame_whitelist, + missing_dynamic_whitelist=missing_whitelist, + ) + + +def _log_size_mismatch_recompile() -> None: + global _LOGGED_DYNAMIC_ALLOWLIST + if not _LOGGED_DYNAMIC_ALLOWLIST: + torch._utils_internal.add_mlhub_insight( + category="dynamic_shapes_analysis", + insight="Dynamic shape recompilation detected", + insight_description="PGO detected a recompilation due to dynamic shapes. \ + Please follow the instruction from the action link to reduce \ + recompilation overhead.", + ) + # add mlhub insight only once per rank + _LOGGED_DYNAMIC_ALLOWLIST = True + + +def render_code_state(cs: defaultdict[CodeId, CodeState]) -> str: + code_state_str = "\n".join( + f"{k}:\n" + + "\n".join( + f" {src}: {fs.render()}" for src, fs in v.automatic_dynamic.items() + ) + for k, v in cs.items() + ) + dynamic_sources: OrderedSet[str] = OrderedSet() + for state in cs.values(): + dynamic_sources.update(_collect_dynamic_sources(state)) + if dynamic_sources: + code_state_str += ( + "\n\nPGO detected a recompilation due to dynamic shapes. " + "To reduce shape recompilations by compiling dynamically to start, " + f'set environment variable TORCH_COMPILE_DYNAMIC_SOURCES="{",".join(dynamic_sources)}"' + ) + return code_state_str + + +@CacheArtifactFactory.register +class PGOCacheArtifact(CacheArtifact): + @override + def populate_cache(self) -> None: + meta = write_local_impl( + self._rewrite_cache_key_for_mega_cache(self.key), self.content + ) + assert meta is not None + + @override + @staticmethod + def type() -> str: + return "pgo" + + @staticmethod + def _rewrite_cache_key_for_mega_cache(original_key: str) -> str: + """ + The PGO cache artifact key for a MAST job contains the job name and the version. + When we want to use the cache artifact on a different MAST job, we need to + update the key to use the new MAST job's name and version. + """ + if not original_key.startswith("mast:"): + # if original_key is overridden, then dont change it + return original_key + if (new_key := get_cache_key()) is not None: + return new_key + return original_key + + +def hit(key: str, ty: str) -> defaultdict[CodeId, CodeState]: + global _INIT_CODE_STATE + assert isinstance(_CODE_STATE, defaultdict) + log.info("get_code_state %s hit %s, %d entries", key, ty, len(_CODE_STATE)) + trace_structured_artifact( + f"get_{ty}_code_state", + "string", + lambda: render_code_state(_CODE_STATE), # type: ignore[arg-type] + ) + set_feature_use("pgo", True) + _INIT_CODE_STATE = copy.deepcopy(_CODE_STATE) + return _CODE_STATE + + +def get_local_code_state(cache_key: str) -> defaultdict[CodeId, CodeState] | None: + global _CODE_STATE + path = code_state_path(cache_key) + if path is not None and os.path.exists(path): + with dynamo_timed( + name := "pgo.get_local_code_state", log_pt2_compile_event=True + ): + CompileEventLogger.pt2_compile(name, cache_key=cache_key) + # Read lock not necessary as we always write atomically write to + # the actual location + with open(path, "rb") as f: + try: + content = f.read() + _CODE_STATE = pickle.loads(content) + CompileEventLogger.pt2_compile(name, cache_size_bytes=f.tell()) + except Exception: + log.warning( + "get_code_state failed while reading %s", path, exc_info=True + ) + else: + CacheArtifactManager.record_artifact( + PGOCacheArtifact.type(), cache_key, content + ) + return hit(path, "local") + return None + + +def lookup_remote_cache_entry( + remote_cache: RemoteCache[JsonDataTy], + cache_key: str, + event_name: str | None = None, +) -> defaultdict[CodeId, CodeState] | None: + code_state = None + try: + cache_data = remote_cache.get(cache_key) + except Exception: + log.warning("get_code_state failed remote read on %s", cache_key, exc_info=True) + else: + if cache_data is not None: + try: + assert isinstance(cache_data, dict) + data = cache_data["data"] + assert isinstance(data, str) + payload = base64.b64decode(data) + if event_name is not None: + CompileEventLogger.pt2_compile( + event_name, cache_size_bytes=len(payload) + ) + code_state = pickle.loads(payload) + except Exception: + log.warning( + "get_code_state failed parsing remote result on %s", + cache_key, + exc_info=True, + ) + else: + CacheArtifactManager.record_artifact( + PGOCacheArtifact.type(), cache_key, payload + ) + else: + log.info("get_code_state remote miss on %s", cache_key) + return code_state + + +def get_remote_code_state(cache_key: str) -> defaultdict[CodeId, CodeState] | None: + global _CODE_STATE + remote_cache = get_remote_cache() + if remote_cache is not None: + with dynamo_timed( + name := "pgo.get_remote_code_state", + log_pt2_compile_event=True, + dynamo_compile_column_us="pgo_get_remote_code_state_time_us", + ): + CompileEventLogger.pt2_compile(name, cache_key=cache_key) + code_state = lookup_remote_cache_entry(remote_cache, cache_key, name) + if code_state is not None: + _CODE_STATE = code_state + return hit(cache_key, "remote") + return None + + +def get_extra_remote_code_state(cache_key: str) -> None: + """ + Reads an additional PGO profile from the given cache key, and merges it with the default PGO profile. + """ + global _CODE_STATE + assert _CODE_STATE is not None + + remote_cache = get_remote_cache() + if remote_cache is not None: + with dynamo_timed( + name := "pgo.get_extra_remote_code_state", + log_pt2_compile_event=True, + dynamo_compile_column_us="pgo_get_remote_code_state_time_us", + ): + CompileEventLogger.pt2_compile(name, cache_key=cache_key) + code_state = lookup_remote_cache_entry(remote_cache, cache_key) + log.info( + "get_extra_code_state %s hit, %d entries", + cache_key, + len(code_state) if code_state is not None else 0, + ) + if code_state is not None: + assert not _CODE_STATE + _CODE_STATE = code_state + # log to tlparse + trace_structured_artifact( + "get_extra_remote_code_state", + "string", + lambda: render_code_state(code_state), + ) + + +def get_code_state() -> defaultdict[CodeId, CodeState]: + global _CODE_STATE, _INIT_CODE_STATE + if _CODE_STATE is not None: + return _CODE_STATE + + # Initialize it (even if we don't look up profile) + _CODE_STATE = defaultdict(CodeState) + + cache_key = get_cache_key() + if cache_key is None: + return _CODE_STATE + + # Attempt local + local_code_state = get_local_code_state(cache_key) + + # Attempt remote + if local_code_state is None: + get_remote_code_state(cache_key) + + # Attempt additional remote if neither local/default remote succeeded + if ( + not _CODE_STATE + and (sticky_read := torch.compiler.config.pgo_extra_read_key) is not None + ): + extra_read_key = get_extra_cache_key(sticky_read) + if extra_read_key is not None: + get_extra_remote_code_state(extra_read_key) + + log.info("get_code_state using default") + + assert _CODE_STATE is not None + return _CODE_STATE + + +def put_code_state() -> None: + if _CODE_STATE is None: + log.info("put_code_state: never initialized, will not write") + return + + if _CODE_STATE == _INIT_CODE_STATE: + log.info("put_code_state: no change, skipping") + return + + cache_key = get_cache_key() + if cache_key is None: + log.info("put_code_state: no cache key, skipping") + return + + put_local_code_state(cache_key) + put_remote_code_state(cache_key) + if (sticky_write := torch.compiler.config.pgo_extra_write_key) is not None: + extra_write_key = get_extra_cache_key(sticky_write) + if extra_write_key is not None: + put_remote_code_state(extra_write_key) + + +def write_local_impl(cache_key: str, pickled_code: bytes) -> tuple[str, int] | None: + path = code_state_path(cache_key) + + if path is None: + return None + + # If the user isn't misusing our API, we should have exclusive access to + # this directory. But it's not too hard + + tmp_path = path + ".tmp" + lock_path = path + ".lock" + # We /mostly/ don't need the lock but the tmp file could be clobbered + # TODO: use a safe tempfile create to eliminate lock + from torch.utils._filelock import FileLock + + os.makedirs(os.path.dirname(path), exist_ok=True) + + with FileLock(lock_path, timeout=LOCK_TIMEOUT): + with open(tmp_path, "wb") as f: + f.write(pickled_code) + size = f.tell() + os.replace(tmp_path, path) + return path, size + + +def put_local_code_state(cache_key: str) -> None: + with dynamo_timed(name := "pgo.put_local_code_state", log_pt2_compile_event=True): + CompileEventLogger.pt2_compile(name, cache_key=cache_key) + assert _CODE_STATE is not None + + pickled_code = pickle.dumps(_CODE_STATE) + + CacheArtifactManager.record_artifact( + PGOCacheArtifact.type(), cache_key, pickled_code + ) + + meta = write_local_impl(cache_key, pickled_code) + if meta is None: + log.info("put_code_state: local cache disabled") + return + path, size = meta + + CompileEventLogger.pt2_compile(name, cache_size_bytes=size) + log.info("put_code_state: wrote local %s, %d entries", path, len(_CODE_STATE)) + trace_structured_artifact( + "put_local_code_state", + "string", + lambda: render_code_state(_CODE_STATE), + ) + + +def put_remote_code_state(cache_key: str, extra_code_state: bool = False) -> None: + event_name = ( + "put_remote_code_state" + if not extra_code_state + else "put_extra_remote_code_state" + ) + with dynamo_timed( + name := f"pgo.{event_name}", + log_pt2_compile_event=True, + dynamo_compile_column_us="pgo_put_remote_code_state_time_us", + ): + CompileEventLogger.pt2_compile(name, cache_key=cache_key) + assert _CODE_STATE is not None + + remote_cache = get_remote_cache() + + if remote_cache is None: + log.info("%s: remote cache disabled", event_name) + return + + content = pickle.dumps(_CODE_STATE) + CompileEventLogger.pt2_compile(name, cache_size_bytes=len(content)) + cache_data: JsonDataTy = { + "data": base64.b64encode(content).decode("ascii"), + } + remote_cache.put(cache_key, cache_data) + log.info( + "%s: wrote remote %s, %d entries", event_name, cache_key, len(_CODE_STATE) + ) + # TODO: don't log this multiple times + trace_structured_artifact( + event_name, + "string", + lambda: render_code_state(_CODE_STATE), + ) + + +# NB: this does NOT reset the cached code state on disk +def reset_code_state() -> None: + global _CODE_STATE, _INIT_CODE_STATE, _LOGGED_DYNAMIC_ALLOWLIST + _CODE_STATE = None + _INIT_CODE_STATE = None + _LOGGED_DYNAMIC_ALLOWLIST = False diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c206572aee856f39ca062c269da28fa6c140f0db --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/__init__.py @@ -0,0 +1,700 @@ +""" +Python polyfills for common builtins. +""" + +# NOTE: 1. Please do not import any submodule in the directory here to avoid circular imports. +# 2. While adding a new polyfill module, also add it to POLYFILLED_MODULE_NAMES in loader.py. +# Add it in the TYPE_CHECKING block below as well. + +import types +from collections import OrderedDict +from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping, Sequence +from itertools import repeat as _repeat +from operator import eq, ne +from typing import Any, TYPE_CHECKING, TypeGuard, TypeVar +from typing_extensions import TypeIs + +import torch + + +T = TypeVar("T") +U = TypeVar("U") +C = TypeVar("C") + + +if TYPE_CHECKING: + from ..utils import dict_keys + + # Load by torch._dynamo.polyfills.loader + # See also the POLYFILLED_MODULE_NAMES in torch/_dynamo/polyfills/loader.py + # Put the submodules here to avoid circular imports + from . import ( + _collections as _collections, + builtins as builtins, + functools as functools, + itertools as itertools, + operator as operator, + os as os, + pytree as pytree, + struct as struct, + sys as sys, + torch_c_nn as torch_c_nn, + traceback as traceback, + ) + +from torch.overrides import BaseTorchFunctionMode + + +# These classes handle support for TorchFunctionModes across +# graph breaks +# Today the TorchFunctionMode enter (for the classes we support) +# simply pushes the mode onto the stack. Since after this occurs +# the stack is mutated, and we replay these mutations, we don't need +# any cleanup logic to be run once the graph break occurs, we simply replay +# these mutations to ensure at the graph break the torch function mode stack is correct +# and reconstruct the torch function mode stack normally +# when we compile the resume function on the other side of the break. +# However, to ensure we exit properly +# in the resume function, we need to re-enter the contexts as we do other contexts. +# These contexts do nothing on enter, but provide the correct exit logic to ensure +# the stack state is correct. +class NoEnterTorchFunctionMode(BaseTorchFunctionMode): + def __enter__(self) -> None: + pass + + +# Used by WrappedUserFunctionVariable and similar to inline decorated function +# calls with bytecode backing. Without this, the context enter/exit happens in +# Python-level VT code, so a nested graph break inside `fn` would skip applying +# the context in the compiled fn/resume. By inlining through this polyfill, the +# `with` statement has real bytecode that the resume function can continue from. +def _fn_with_ctx(ctx: Any, fn: Callable[..., T], *args: Any, **kwargs: Any) -> T: + with ctx: + return fn(*args, **kwargs) + + +def index( + iterator: Iterator[T], item: T, start: int = 0, end: int | None = None +) -> int: + from itertools import islice + + for i, elem in islice(enumerate(iterator), start, end): + if item == elem: + return i + # This will not run in dynamo + raise ValueError(f"{item} is not in {type(iterator)}") + + +def repeat(item: T, count: int) -> Iterator[T]: + for _ in range(count): + yield item + + +def radians(x: float) -> float: + import math + + return math.pi / 180.0 * x + + +def impl_IS_MAPPING(a: object) -> TypeIs[Mapping[Any, Any]]: + return isinstance(a, Mapping) + + +def impl_MATCH_SEQUENCE(a: object) -> TypeGuard[Sequence[Any]]: + return isinstance(a, Sequence) and not isinstance(a, (str, bytes, bytearray)) + + +def _match_class_attr(obj: object, name: str, seen: set[str]) -> object: + if name in seen: + raise TypeError(f"{type(obj)} got multiple sub-patterns for attribute {name}") + + attr = getattr(obj, name) + seen.add(name) + return attr + + +def impl_MATCH_CLASS( + subject: object, cls: type, nargs: int, kwargs: tuple[str, ...] +) -> tuple[object, ...] | None: + if not isinstance(cls, type): + raise TypeError("called match pattern must be a class") + + if not isinstance(subject, cls): + return None + + typ = type(subject) + match_self = False + match_args = () + + attrs = [] + seen = set() + + if nargs: + if hasattr(typ, "__match_args__"): + match_args = typ.__match_args__ + + if not isinstance(match_args, tuple): + raise TypeError( + f"{typ}.__match_args__ must be a tuple, (got {type(match_args)})" + ) + + for name in match_args[:nargs]: + if not isinstance(name, str): + raise TypeError( + f"__match_args__ elements must be strings (got {type(name)})" + ) + attrs.append(_match_class_attr(subject, name, seen)) + else: + # We should somehow check if the type has TPFLAGS_MATCH_SELF set + # match_self is only true if TPFLAGS_MATCH_SELF is set, but there is + # no way to check for it directly in Python. So we assume it is set + # if there are no __match_args__ + match_self = True + attrs.append(subject) + + allowed = 1 if match_self else len(match_args) + if allowed < nargs: + raise TypeError( + f"accepts {allowed} positional sub-patterns ({nargs} given)" + ) + + for name in kwargs: + attrs.append(_match_class_attr(subject, name, seen)) + + return tuple(attrs) + + +def impl_MATCH_KEYS(obj: Mapping[T, U], keys: tuple[T, ...]) -> tuple[U, ...] | None: + assert isinstance(obj, Mapping) + if all(key in obj for key in keys): + return tuple(obj[key] for key in keys) + else: + return None + + +def impl_CONTAINS_OP_fallback(a: T, b: Iterable[T]) -> bool: + # performs fallback "a in b" + if hasattr(b, "__iter__"): + # use __iter__ if __contains__ is not available + for x in b: + if x == a: + return True + return False + raise TypeError(f"argument of type {type(b)} is not iterable") + + +def accumulate_grad(x: torch.Tensor, new_grad: torch.Tensor | None) -> None: + # polyfills according to the Gradient Layout Contract + if new_grad is None: + return + new_grad_strided = torch.empty_like(x) + new_grad_strided.copy_(new_grad) + if x.grad is None: + x.grad = new_grad_strided + elif torch.is_grad_enabled(): + x.grad = x.grad + new_grad_strided + else: + x.grad.add_(new_grad_strided) + + +# This mirrors +# https://github.com/python/cpython/blob/a1c52d1265c65bcf0d9edf87e143843ad54f9b8f/Objects/listobject.c#L3352-L3413 +def list_cmp( + op: Callable[[Any, Any], bool], left: Sequence[T], right: Sequence[T] +) -> bool: + """emulate `(1,2,3) > (1,2)` etc""" + + # Optimization: For equality, short-circuit if lengths differ + # This avoids iterating through elements and triggering guards on SymInts + left_len = len(left) + right_len = len(right) + + if op is eq and left_len != right_len: + return False + if op is ne and left_len != right_len: + return True + + # Apply `op` to the first pair that differ + for a, b in zip(left, right): + if a != b: + return op(a, b) + + # No more pairs to compare, so compare sizes. + return op(left_len, right_len) + + +def dict___eq__(d: dict[T, U], other: dict[T, U]) -> bool: + if (len(d) != len(other)) or (d.keys() != other.keys()): + return False + + if all(isinstance(a, OrderedDict) for a in (d, other)): + return list(d.items()) == list(other.items()) + + for k, v in d.items(): + if v != other[k]: + return False + + return True + + +def set_symmetric_difference( + set1: Iterable[T], + set2: Iterable[T], + cls: type[Any] = set, +) -> Any: + symmetric_difference_set: set[T] = set() + for x in set1: + if x not in set2: + symmetric_difference_set.add(x) + for x in set2: + if x not in set1: + symmetric_difference_set.add(x) + return cls(symmetric_difference_set) + + +def set_symmetric_difference_update(set1: set[T], set2: set[T]) -> None: + result = set1.symmetric_difference(set2) + set1.clear() + set1.update(result) + + +def set_isdisjoint(set1: set[T], set2: set[T]) -> bool: + if not isinstance(set2, Iterable): + raise TypeError(f"'{type(set2)}' object is not iterable") + + for x in set1: + for y in set2: + if not isinstance(y, Hashable): + raise TypeError(f"unhashable type: '{type(y)}'") + if x == y: + return False + return True + + +def set_intersection( + set1: set[T], + *others: Iterable[T], + # See facebook/pyrefly#1496 - leave generic + cls: type[Any] = set, +) -> Any: + if len(others) == 0: + return set1.copy() + + if not all(isinstance(s, Iterable) for s in others): + raise TypeError(f"set.difference expected an iterable, got {type(others)}") + + for s in others: + if any(not isinstance(x, Hashable) for x in s): + raise TypeError("unhashable type") + + # return a new set with elements common in all sets + intersection_set = set() + for x in set1: + for set2 in others: + if not any(x == y for y in set2): + break + else: + intersection_set.add(x) + return cls(intersection_set) + + +def set_intersection_update(set1: set[T], *others: Iterable[T]) -> None: + result = set1.intersection(*others) + set1.clear() + set1.update(result) + + +def set_union( + set1: set[T], *others: Iterable[T], cls: type[C] | None = None +) -> C | set[T]: + # frozenset also uses this function + if cls is None: + # pyrefly: ignore[bad-assignment] + cls = type(set1) + + if len(others) == 0: + return set1.copy() + + if not all(isinstance(s, Iterable) for s in others): + raise TypeError(f"set.union expected an iterable, got {type(others)}") + + for s in others: + if any(not isinstance(x, Hashable) for x in s): + raise TypeError("unhashable type") + + union_set = set(set1.copy()) + for set2 in others: + set_update(union_set, set2) + + # frozenset also uses this function + # pyrefly: ignore [bad-argument-count, not-callable] + return cls(union_set) + + +# pyrefly: ignore [bad-return] +def set_update(set1: set[T], *others: Iterable[T]) -> set[T]: + if len(others) == 0: + return set1 + + for set2 in others: + for x in set2: + if x not in set1: + set1.add(x) + + +def set_difference( + set1: set[T], + *others: Iterable[T], + cls: type[Any] = set, +) -> Any: + if len(others) == 0: + return set1.copy() + + if not all(isinstance(s, Iterable) for s in others): + raise TypeError(f"set.difference expected an iterable, got {type(others)}") + + for s in others: + if any(not isinstance(x, Hashable) for x in s): + raise TypeError("unhashable type") + + difference_set = set() + for x in set1: + for set2 in others: + if x in set2: + break + else: + difference_set.add(x) + return cls(difference_set) + + +def set_difference_update(set1: set[T], *others: Iterable[T]) -> None: + result = set1.difference(*others) + set1.clear() + set1.update(result) + + +def assert_dict_equal( + self_: Any, d1: dict[T, U], d2: dict[T, U], msg: str | None = None +) -> None: + self_.assertTrue(d1 == d2, msg) + + +def assert_multi_line_equal( + self_: Any, first: T, second: T, msg: str | None = None +) -> None: + return self_.assertTrue(first == second, msg) + + +# The original impl. uses difflib +def assert_sequence_equal( + self_: Any, + seq1: Sequence[T], + seq2: Sequence[T], + msg: str | None = None, + seq_type: type[Any] | None = None, +) -> None: + return self_.assertTrue(seq1 == seq2, msg) + + +def getattr_and_trace(*args: Any, **kwargs: Any) -> Any: + wrapper_obj = args[0] + attr_name = args[1] + fn = getattr(wrapper_obj, attr_name) + return fn(*args[2:], **kwargs) + + +def mapping_get(obj: Mapping[T, U], key: T, value: U | None = None, /) -> U | None: + try: + return obj.__getitem__(key) + except KeyError: + return value + + +def instantiate_user_defined_class_object( + cls: type[T], /, *args: Any, **kwargs: Any +) -> T: + obj = cls.__new__(cls, *args, **kwargs) + + # Only call __init__ if the object's type is a subclass of cls. + # CPython uses PyType_IsSubtype(Py_TYPE(obj), type) at the C level, which does NOT + # go through metaclass __instancecheck__. Using isinstance() here would be wrong + # for classes with custom __instancecheck__ (e.g. torch.ByteStorage). + # Reference: https://github.com/python/cpython/blob/3.12/Objects/typeobject.c#L1670-L1673 + if issubclass(type(obj), cls): + obj.__init__(*args, **kwargs) + return obj + + +def mutable_mapping_update( + self, + data: Mapping[T, U] | Iterable[tuple[T, U]] = (), + /, + **kwargs: Any, +) -> None: + if isinstance(data, Mapping): + # Merge standard mapping with PyMapping_Items + for key, value in data.items(): + self[key] = value + # FIXME: Enabling the `elif`-branch below needs too many `VariableClass.call_obj_hasattr` changes. + # >>> class Foo: + # ... def __init__(self): + # ... self.keys = lambda: ['a', 'b', 'c'] # not required to be a method + # ... + # ... def __getitem__(self, key): + # ... return 0 + # ... + # >>> dict(Foo()) + # {'a': 0, 'b': 0, 'c': 0} + # + # > This is a rare case, so we comment it out for now. + # + # elif hasattr(data, "keys"): + # # Merge mapping-like object with PyMapping_Keys + PyObject_GetItem + # for key in data.keys(): + # self[key] = data[key] + else: + if not isinstance(data, Iterable): + raise TypeError(f"{type(data).__name__!r} object is not iterable") + # Likely a sequence of pairs + for key, value in data: + self[key] = value + + if kwargs: + for key, value in kwargs.items(): + self[key] = value + + +# Used with something like dict(obj) +def construct_dict( + cls: type[T], + data: Mapping[object, object] | Iterable[tuple[object, object]] = (), + /, + **kwargs: Any, +) -> T: + self = cls.__new__(cls) + mutable_mapping_update(self, data, **kwargs) + return self + + +def foreach_map_fn(*args: Any) -> Any: + op = args[0] + new_args: list[Any] = [] + at_least_one_list = False + for arg in args[1:]: + if not isinstance(arg, (list, tuple)): + new_args.append(_repeat(arg)) + else: + at_least_one_list = True + new_args.append(arg) + + # Just apply op once to args if there are no lists + if not at_least_one_list: + return op(*args[1:]) + + out = [] + for unpacked in zip(*new_args): + out.append(op(*unpacked)) + + return out + + +def foreach_lerp_inplace( + self, + end: list[torch.Tensor] | tuple[torch.Tensor, ...], + weight: float | int | torch.Tensor, +) -> None: + # Decompose lerp via addcmul_ for FMA. Uses the same dual-formula + # approach as CUDA's native lerp to get bitwise identical results: + # |w| < 0.5 (low): fma(w, diff, start) + # |w| >= 0.5 (high): fma(-(1-w), diff, end) + # For tensor weights (e.g. 0-dim tensor from tensor betas in Adam) the + # low formula is always used because the native lerp_scalar lowering + # would crash on float(weight) for symbolic expressions. + diff = torch._foreach_sub(end, self) + if isinstance(weight, torch.Tensor): + # Select base and weight for the dual formula before a single addcmul: + # low (|w| < 0.5): fma(w, diff, self) + # high (|w| >= 0.5): fma(-(1-w), diff, end) + mask = weight.abs() >= 0.5 + neg_omw = -(1.0 - weight) + w = torch.where(mask, neg_omw, weight) + bases = [torch.where(mask, e, s) for s, e in zip(self, end)] + w_list = [w] * len(diff) + torch._foreach_addcmul_(bases, w_list, diff) + for s, b in zip(self, bases): + s.copy_(b) + else: + abs_weight = weight if weight >= 0 else -weight + if abs_weight >= 0.5: + # High formula: end + (-(1-w)) * diff → fma(-(1-w), diff, end) + # Compute 1-w in target dtype to match CUDA rounding. + d0 = self[0] + neg_omw = -(1.0 - torch.tensor(weight, dtype=d0.dtype, device=d0.device)) + neg_omw_list = [neg_omw] * len(diff) + for s, e in zip(self, end): + s.copy_(e) + torch._foreach_addcmul_(self, neg_omw_list, diff) + else: + # Low formula: start + w * diff → fma(w, diff, start) + weights = [torch.full_like(d, weight) for d in diff] + torch._foreach_addcmul_(self, weights, diff) + return self + + +def foreach_pow_scalar( + scalar: Any, exps: Sequence[bool | complex | float | int] +) -> tuple[torch.Tensor, ...]: + return torch._foreach_pow([scalar for _ in exps], exps) + + +def predicate(obj: object) -> bool: + # This will cause the rest of dynamo to handle the if statement correctly, so we don't have to rewrite it here. + # We can't just use bool() here since we can't trace into that in general. + if obj: + return True + return False + + +def cmp_eq(a: object, b: object) -> bool: + # Note that the commented `is` check should ideally be removed. This is a + # CPython optimization that skips the __eq__ checks it the obj id's are + # same. But, these lines adds many `is` nodes in the Fx graph for + # SymNodeVariable. For now, we can just skip this check. This is STILL + # correct because one of the __eq__ checks will pass later, just could be + # slow in some corner cases. + # if a is b: + # return True + if isinstance(a, type): + # Default metaclass equality is identity-based. Preserve the reflected + # operand fallback without tracing through type.__eq__. + if type(a).__eq__ is type.__eq__: + result = True if a is b else NotImplemented + else: + result = type(a).__eq__(a, b) + else: + result = a.__eq__(b) + if result is NotImplemented: + if isinstance(b, type): + if type(b).__eq__ is type.__eq__: + result = True if a is b else NotImplemented + else: + result = type(b).__eq__(b, a) + else: + result = b.__eq__(a) + return result is not NotImplemented and result + + +def cmp_ne(a: object, b: object) -> bool: + if isinstance(a, type): + if type(a).__ne__ is type.__ne__: + result = False if a is b else NotImplemented + else: + result = type(a).__ne__(a, b) + if result is not NotImplemented: + return result + elif isinstance(type(a).__ne__, types.FunctionType): + result = a.__ne__(b) + if result is not NotImplemented: + return result + # Fall through to try b.__ne__(a) or cmp_eq + if isinstance(b, type): + if type(b).__ne__ is type.__ne__: + result = False if a is b else NotImplemented + else: + result = type(b).__ne__(b, a) + if result is not NotImplemented: + return result + elif isinstance(type(b).__ne__, types.FunctionType): + result = b.__ne__(a) + if result is not NotImplemented: + return result + return not cmp_eq(a, b) + + +def cmp_lt(a: Any, b: Any) -> bool: + result = a.__lt__(b) + if result is NotImplemented: + raise TypeError(f"{type(a)} does not support the < operator") + return result + + +def cmp_le(a: Any, b: Any) -> bool: + # Check if __le__ is overridden + if isinstance(type(a).__le__, types.FunctionType): + return a.__le__(b) + return cmp_eq(a, b) or cmp_lt(a, b) + + +def cmp_gt(a: Any, b: Any) -> bool: + # Check if __gt__ is overridden + if isinstance(type(a).__gt__, types.FunctionType): + return a.__gt__(b) + # a > b is equivalent to b < a + return cmp_lt(b, a) + + +def cmp_ge(a: Any, b: Any) -> bool: + # Check if __ge__ is overridden + if isinstance(type(a).__ge__, types.FunctionType): + return a.__ge__(b) + return cmp_eq(a, b) or cmp_gt(a, b) + + +def group_tensors_by_device_and_dtype( + tensorlistlist: list[list[torch.Tensor | None]], with_indices: bool = False +) -> dict[tuple[torch.device, torch.dtype], tuple[list[list[Any]], list[int]]]: + """Pure Python implementation of torch._C._group_tensors_by_device_and_dtype. + + Groups tensors by their device and dtype. This is useful before sending + tensors off to a foreach implementation, which requires tensors to be on + one device and dtype. + + Args: + tensorlistlist: A list of lists of tensors (tensors can be None). + with_indices: If True, track original indices in the output. + + Returns: + A dict mapping (device, dtype) tuples to (grouped_tensorlistlist, indices). + """ + # Result dict: (device, dtype) -> (list of lists, indices) + result: dict[ + tuple[torch.device, torch.dtype], tuple[list[list[Any]], list[int]] + ] = {} + + if not tensorlistlist or not tensorlistlist[0]: + return result + + num_lists = len(tensorlistlist) + num_tensors = len(tensorlistlist[0]) + + for idx in range(num_tensors): + # Find the first non-None tensor at this index to get device and dtype + first_tensor = None + for tlist in tensorlistlist: + if tlist is not None and idx < len(tlist) and tlist[idx] is not None: + first_tensor = tlist[idx] + break + + if first_tensor is None: + # All tensors at this index are None, skip + continue + + key = (first_tensor.device, first_tensor.dtype) + + if key not in result: + # Initialize empty lists for each tensorlist + result[key] = ([[] for _ in range(num_lists)], []) + + grouped_lists, indices = result[key] + + # Add tensors from each list at this index + for list_idx, tlist in enumerate(tensorlistlist): + if tlist is not None and idx < len(tlist): + grouped_lists[list_idx].append(tlist[idx]) + else: + grouped_lists[list_idx].append(None) + + if with_indices: + indices.append(idx) + + return result diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/_collections.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/_collections.py new file mode 100644 index 0000000000000000000000000000000000000000..9773635ae30587b06bb9f6b82c003392767b3873 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/_collections.py @@ -0,0 +1,33 @@ +""" +Python polyfills for builtins +""" + +from collections.abc import Iterable, MutableMapping +from typing import TypeVar + +from ..decorators import substitute_in_graph + + +__all__ = [] + + +T = TypeVar("T") + + +try: + import _collections # type: ignore[import-not-found] + + @substitute_in_graph(_collections._count_elements) + def _count_elements( + mapping: MutableMapping[T, int], + iterable: Iterable[T], + ) -> None: + "Tally elements from the iterable." + mapping_get = mapping.get + for elem in iterable: + mapping[elem] = mapping_get(elem, 0) + 1 + + __all__.append("_count_elements") + +except ImportError: + pass diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/builtins.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/builtins.py new file mode 100644 index 0000000000000000000000000000000000000000..386a96f6db4ef91ac531e90aa0a6d9d2553c644a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/builtins.py @@ -0,0 +1,129 @@ +""" +Python polyfills for builtins +""" + +from __future__ import annotations + +import builtins +import functools +import operator +import typing +from collections.abc import Callable +from typing import TYPE_CHECKING, TypeVar + +from ..decorators import substitute_in_graph + + +if TYPE_CHECKING: + from collections.abc import Iterable + + +__all__ = [ + "all", + "any", + "cast", + "enumerate", + "sum", +] + + +_T = TypeVar("_T") + + +@substitute_in_graph(builtins.all, can_constant_fold_through=True) +def all(iterable: Iterable[object], /) -> bool: + for elem in iterable: + if not elem: + return False + return True + + +@substitute_in_graph(builtins.any, can_constant_fold_through=True) +def any(iterable: Iterable[object], /) -> bool: + for elem in iterable: + if elem: + return True + return False + + +@substitute_in_graph(builtins.enumerate, is_embedded_type=True) # type: ignore[arg-type] +def enumerate(iterable: Iterable[_T], start: int = 0) -> Iterable[tuple[int, _T]]: + if not isinstance(start, int): + raise TypeError( + f"{type(start).__name__!r} object cannot be interpreted as an integer" + ) + + for x in iterable: + yield start, x + start += 1 + + +@substitute_in_graph(builtins.sum, can_constant_fold_through=True) # type: ignore[arg-type] +def sum(iterable: Iterable[_T], /, start: _T = 0) -> _T: # type: ignore[assignment] + return functools.reduce(operator.add, iterable, start) + + +class _CallableIterator: + def __init__(self, fn, sentinel): # type: ignore[no-untyped-def] + self.fn = fn + self.sentinel = sentinel + + def __iter__(self): # type: ignore[no-untyped-def] + return self + + def __next__(self): # type: ignore[no-untyped-def] + # The iterator created in this case will call object with no arguments + # for each call to its __next__() method; + r = self.fn() + + # If the value returned is equal to sentinel, StopIteration will be raised + if r == self.sentinel: + raise StopIteration + + # otherwise the value will be returned. + return r + + +_sentinel_missing = object() + + +# TODO(guilhermeleobas): use substitute_in_graph for iter() +def iter_(fn_or_iterable, sentinel=_sentinel_missing, /): # type: ignore[no-untyped-def] + # Without a second argument, object must be a collection object which supports + # the iterable (__iter__) or the sequence protocol (__getitem__ with an integer + # starting at 0) + if sentinel is _sentinel_missing: + iterable = fn_or_iterable + if hasattr(iterable, "__iter__"): + iterator = iterable.__iter__() + if hasattr(iterator, "__next__"): + return iterator + else: + raise TypeError(f"'{type(iterator)}' object is not iterable") + if hasattr(iterable, "__getitem__"): + # Needs to be a new function to avoid iter becoming a generator + def sequence_protocol(iterable): # type: ignore[no-untyped-def] + i = 0 + while True: + try: + yield iterable.__getitem__(i) + i += 1 + except IndexError: + break + + return sequence_protocol(iterable) + raise TypeError(f"'{type(iterable)}' object is not iterable") + else: + # If the second argument, sentinel, is given, then object must be a + # callable object. + fn = fn_or_iterable + + if not isinstance(fn, Callable): # type: ignore[arg-type] + raise TypeError("iter(v, w): v must be a callable") + + return _CallableIterator(fn, sentinel) + + +@substitute_in_graph(typing.cast, can_constant_fold_through=True) +def cast(typ: type, val: _T) -> _T: # type: ignore[type-var] + return val diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/copy.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/copy.py new file mode 100644 index 0000000000000000000000000000000000000000..0188945f038cb00ec49cb9de7c9da93bd0a730b5 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/copy.py @@ -0,0 +1,36 @@ +""" +Python polyfills for copy +""" + +from __future__ import annotations + +from typing import TypeVar + +from ..decorators import substitute_in_graph + + +__all__ = [ + "reduce_ex_user_defined_object", +] + +T = TypeVar("T") + + +@substitute_in_graph(object.__reduce_ex__, skip_signature_check=True) # type: ignore[arg-type] +def reduce_ex_user_defined_object(obj: T, protocol: int, /) -> tuple: # type: ignore[type-arg] + """Traceable polyfill for object.__reduce_ex__ on user-defined objects. + + Returns the same tuple that CPython's _common_reduce produces: + (copyreg.__newobj__, (cls,), obj.__dict__, None, None). + copy._reconstruct then calls cls.__new__(cls) and updates __dict__. + """ + import copyreg + + cls = type(obj) + return ( + copyreg.__newobj__, # pyrefly: ignore[missing-attribute] + (cls,), + obj.__dict__, + None, + None, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/functools.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/functools.py new file mode 100644 index 0000000000000000000000000000000000000000..6458b8080f93311ea93f6ba4a564e10a10fcd233 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/functools.py @@ -0,0 +1,46 @@ +""" +Python polyfills for functools +""" + +import functools +from collections.abc import Callable, Iterable +from typing import TypeVar + +from ..decorators import substitute_in_graph + + +__all__ = ["reduce"] + + +_T = TypeVar("_T") +_U = TypeVar("_U") + + +_initial_missing = object() + + +# Reference: https://docs.python.org/3/library/functools.html#functools.reduce +@substitute_in_graph(functools.reduce) +def reduce( + function: Callable[[_U, _T], _U], + iterable: Iterable[_T], + initial: _U = _initial_missing, # type: ignore[assignment] + /, +) -> _U: + it = iter(iterable) + + value: _U + if initial is _initial_missing: + try: + value = next(it) # type: ignore[assignment] + except StopIteration: + raise TypeError( + "reduce() of empty iterable with no initial value", + ) from None + else: + value = initial + + for element in it: + value = function(value, element) + + return value diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/fx.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/fx.py new file mode 100644 index 0000000000000000000000000000000000000000..5a5ed97e0899d94fc4478de5acfa7879f5560ab2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/fx.py @@ -0,0 +1,41 @@ +from collections.abc import Callable +from typing import Any + +from torch._C import _fx_map_aggregate, _fx_map_arg +from torch.fx.immutable_collections import immutable_dict, immutable_list +from torch.fx.node import Node + +from ..decorators import substitute_in_graph + + +@substitute_in_graph(_fx_map_arg, can_constant_fold_through=True) +def map_arg(a: Any, fn: Callable[[Node], Any]) -> Any: + return map_aggregate(a, lambda x: fn(x) if isinstance(x, Node) else x) + + +@substitute_in_graph(_fx_map_aggregate, can_constant_fold_through=True) +def map_aggregate(a: Any, fn: Callable[[Any], Any]) -> Any: + result: Any + if isinstance(a, tuple): + it = (map_aggregate(elem, fn) for elem in a) + # Support NamedTuple (if it has `_fields`) by repacking into original type. + result = type(a)(*it) if hasattr(a, "_fields") else tuple(it) + elif isinstance(a, list): + result = immutable_list([map_aggregate(elem, fn) for elem in a]) + elif isinstance(a, dict): + result = immutable_dict([(k, map_aggregate(v, fn)) for k, v in a.items()]) + elif isinstance(a, slice): + result = slice( + map_aggregate(a.start, fn), + map_aggregate(a.stop, fn), + map_aggregate(a.step, fn), + ) + else: + result = fn(a) + return result + + +__all__ = [ + "map_arg", + "map_aggregate", +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/heapq.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/heapq.py new file mode 100644 index 0000000000000000000000000000000000000000..feddb5723614f581fdd232a162feaf00a3ca2fae --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/heapq.py @@ -0,0 +1,119 @@ +""" +Python polyfills for heapq +""" + +from __future__ import annotations + +import heapq +import importlib +import sys +from typing import TYPE_CHECKING, TypeVar + +from ..decorators import substitute_in_graph + + +if TYPE_CHECKING: + from types import ModuleType + + +_T = TypeVar("_T") + + +# Partially copied from CPython test/support/import_helper.py +# https://github.com/python/cpython/blob/bb8791c0b75b5970d109e5557bfcca8a578a02af/Lib/test/support/import_helper.py +def _save_and_remove_modules(names: set[str]) -> dict[str, ModuleType]: + orig_modules = {} + prefixes = tuple(name + "." for name in names) + for modname in list(sys.modules): + if modname in names or modname.startswith(prefixes): + orig_modules[modname] = sys.modules.pop(modname) + return orig_modules + + +def import_fresh_module(name: str, blocked: list[str]) -> ModuleType: + # Keep track of modules saved for later restoration as well + # as those which just need a blocking entry removed + names = {name, *blocked} + orig_modules = _save_and_remove_modules(names) + for modname in blocked: + sys.modules[modname] = None # type: ignore[assignment] + + try: + return importlib.import_module(name) + finally: + _save_and_remove_modules(names) + sys.modules.update(orig_modules) + + +# Import the pure Python heapq module, blocking the C extension +py_heapq = import_fresh_module("heapq", blocked=["_heapq"]) + + +__all__ = [ + "_heapify_max", + "_heappop_max", + "_heapreplace_max", + "heapify", + "heappop", + "heappush", + "heappushpop", + "heapreplace", + "merge", + "nlargest", + "nsmallest", +] + + +@substitute_in_graph(heapq._heapify_max) +def _heapify_max(heap: list[_T], /) -> None: + return py_heapq._heapify_max(heap) + + +@substitute_in_graph(heapq._heappop_max) # type: ignore[attr-defined] +def _heappop_max(heap: list[_T]) -> _T: + return py_heapq._heappop_max(heap) + + +@substitute_in_graph(heapq._heapreplace_max) # type: ignore[attr-defined] +def _heapreplace_max(heap: list[_T], item: _T) -> _T: + return py_heapq._heapreplace_max(heap, item) + + +@substitute_in_graph(heapq.heapify) +def heapify(heap: list[_T], /) -> None: + return py_heapq.heapify(heap) + + +@substitute_in_graph(heapq.heappop) +def heappop(heap: list[_T], /) -> _T: + return py_heapq.heappop(heap) + + +@substitute_in_graph(heapq.heappush) +def heappush(heap: list[_T], item: _T) -> None: + return py_heapq.heappush(heap, item) + + +@substitute_in_graph(heapq.heappushpop) +def heappushpop(heap: list[_T], item: _T) -> _T: + return py_heapq.heappushpop(heap, item) + + +@substitute_in_graph(heapq.heapreplace) +def heapreplace(heap: list[_T], item: _T) -> _T: + return py_heapq.heapreplace(heap, item) + + +@substitute_in_graph(heapq.merge) # type: ignore[arg-type] +def merge(*iterables, key=None, reverse=False): # type: ignore[no-untyped-def] + return py_heapq.merge(*iterables, key=key, reverse=reverse) + + +@substitute_in_graph(heapq.nlargest) # type: ignore[arg-type] +def nlargest(n, iterable, key=None): # type: ignore[no-untyped-def] + return py_heapq.nlargest(n, iterable, key=key) + + +@substitute_in_graph(heapq.nsmallest) # type: ignore[arg-type] +def nsmallest(n, iterable, key=None): # type: ignore[no-untyped-def] + return py_heapq.nsmallest(n, iterable, key=key) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/itertools.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/itertools.py new file mode 100644 index 0000000000000000000000000000000000000000..c21c33516db90e54b14e913f72602ff78d44b99f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/itertools.py @@ -0,0 +1,338 @@ +""" +Python polyfills for itertools +""" + +from __future__ import annotations + +import itertools +import operator +from collections.abc import Callable +from typing import overload, TYPE_CHECKING, TypeAlias, TypeVar + +from ..decorators import substitute_in_graph + + +if TYPE_CHECKING: + from collections.abc import Iterable, Iterator + + +__all__ = [ + "accumulate", + "chain", + "chain_from_iterable", + "compress", + "cycle", + "dropwhile", + "filterfalse", + "islice", + "pairwise", + "starmap", + "takewhile", + "tee", + "zip_longest", +] + + +_T = TypeVar("_T") +_U = TypeVar("_U") +_Predicate: TypeAlias = Callable[[_T], object] +_T1 = TypeVar("_T1") +_T2 = TypeVar("_T2") + + +# Reference: https://docs.python.org/3/library/itertools.html#itertools.chain +@substitute_in_graph(itertools.chain, is_embedded_type=True) # type: ignore[arg-type] +def chain(*iterables: Iterable[_T]) -> Iterator[_T]: + for iterable in iterables: + yield from iterable + + +# Reference: https://docs.python.org/3/library/itertools.html#itertools.accumulate +@substitute_in_graph(itertools.accumulate, is_embedded_type=True) # type: ignore[arg-type] +def accumulate( + iterable: Iterable[_T], + func: Callable[[_T, _T], _T] | None = None, + *, + initial: _T | None = None, +) -> Iterator[_T]: + # call iter outside of the generator to match cypthon behavior + iterator = iter(iterable) + if func is None: + func = operator.add + + def _accumulate(iterator: Iterator[_T]) -> Iterator[_T]: + total = initial + if total is None: + try: + total = next(iterator) + except StopIteration: + return + + yield total + for element in iterator: + total = func(total, element) + yield total + + return _accumulate(iterator) + + +@substitute_in_graph(itertools.chain.from_iterable) # type: ignore[arg-type] +def chain_from_iterable(iterable: Iterable[Iterable[_T]], /) -> Iterator[_T]: + # previous version of this code was: + # return itertools.chain(*iterable) + # If iterable is an infinite generator, this will lead to infinite recursion + for it in iterable: + yield from it + + +chain.from_iterable = chain_from_iterable # type: ignore[attr-defined] + + +# Reference: https://docs.python.org/3/library/itertools.html#itertools.compress +@substitute_in_graph(itertools.compress, is_embedded_type=True) # type: ignore[arg-type] +def compress(data: Iterable[_T], selectors: Iterable[_U], /) -> Iterator[_T]: + return (datum for datum, selector in zip(data, selectors) if selector) + + +# Reference: https://docs.python.org/3/library/itertools.html#itertools.cycle +@substitute_in_graph(itertools.cycle, is_embedded_type=True) # type: ignore[arg-type] +def cycle(iterable: Iterable[_T]) -> Iterator[_T]: + iterator = iter(iterable) + + def _cycle(iterator: Iterator[_T]) -> Iterator[_T]: + # pyrefly: ignore [implicit-any] + saved = [] + for element in iterable: + yield element + saved.append(element) + + while saved: + for element in saved: + yield element + + return _cycle(iterator) + + +# Reference: https://docs.python.org/3/library/itertools.html#itertools.dropwhile +@substitute_in_graph(itertools.dropwhile, is_embedded_type=True) # type: ignore[arg-type] +def dropwhile(predicate: _Predicate[_T], iterable: Iterable[_T], /) -> Iterator[_T]: + # dropwhile(lambda x: x < 5, [1, 4, 6, 3, 8]) -> 6 3 8 + if not callable(predicate): + raise TypeError(f"'{type(predicate).__name__}' object is not callable") + + iterator = iter(iterable) + for x in iterator: + if not predicate(x): + yield x + break + + yield from iterator + + +# Reference: https://docs.python.org/3/library/itertools.html#itertools.takewhile +@substitute_in_graph(itertools.takewhile, is_embedded_type=True) # type: ignore[arg-type] +def takewhile(predicate: _Predicate[_T], iterable: Iterable[_T], /) -> Iterator[_T]: + # takewhile(lambda x: x<5, [1,4,6,3,8]) → 1 4 + if not callable(predicate): + raise TypeError(f"'{type(predicate).__name__}' object is not callable") + + for x in iterable: + if not predicate(x): + break + yield x + + +@overload +def starmap( + function: Callable[[], _U], + iterable: Iterable[tuple[()]], + /, +) -> itertools.starmap[_U]: ... + + +@overload +def starmap( + function: Callable[[_T], _U], + iterable: Iterable[tuple[_T]], + /, +) -> itertools.starmap[_U]: ... + + +@overload +def starmap( + function: Callable[[_T, _T1], _U], + iterable: Iterable[tuple[_T, _T1]], + /, +) -> itertools.starmap[_U]: ... + + +@overload +def starmap( + function: Callable[[_T, _T1, _T2], _U], + iterable: Iterable[tuple[_T, _T1, _T2]], + /, +) -> itertools.starmap[_U]: ... + + +# Reference: https://docs.python.org/3/library/itertools.html#itertools.starmap +@substitute_in_graph(itertools.starmap, is_embedded_type=True) # type: ignore[arg-type] +# pyrefly: ignore [implicit-any] +def starmap(function: Callable[..., _T], iterable: Iterable, /) -> Iterable[_T]: + # starmap(pow, [(2,5), (3,2), (10,3)]) → 32 9 1000 + if not callable(function): + raise TypeError(f"'{type(function).__name__}' object is not callable") + + for args in iterable: + yield function(*args) + + +@substitute_in_graph(itertools.filterfalse, is_embedded_type=True) # type: ignore[arg-type] +def filterfalse(function: _Predicate[_T], iterable: Iterable[_T], /) -> Iterator[_T]: + it = iter(iterable) + if function is None: + return filter(operator.not_, it) + else: + return filter(lambda x: not function(x), it) + + +# Reference: https://docs.python.org/3/library/itertools.html#itertools.islice +@substitute_in_graph(itertools.islice, is_embedded_type=True) # type: ignore[arg-type] +def islice(iterable: Iterable[_T], /, *args: int | None) -> Iterator[_T]: + s = slice(*args) + start = 0 if s.start is None else s.start + stop = s.stop + step = 1 if s.step is None else s.step + if start < 0 or (stop is not None and stop < 0) or step <= 0: + raise ValueError( + "Indices for islice() must be None or an integer: 0 <= x <= sys.maxsize.", + ) + + if stop is None: + # TODO: use indices = itertools.count() and merge implementation with the else branch + # when we support infinite iterators + next_i = start + for i, element in enumerate(iterable): + if i == next_i: + yield element + next_i += step + else: + indices = range(max(start, stop)) + next_i = start + for i, element in zip(indices, iterable): + if i == next_i: + yield element + next_i += step + + +# Reference: https://docs.python.org/3/library/itertools.html#itertools.pairwise +@substitute_in_graph(itertools.pairwise, is_embedded_type=True) # type: ignore[arg-type] +def pairwise(iterable: Iterable[_T], /) -> Iterator[tuple[_T, _T]]: + a = None + first = True + for b in iterable: + if first: + first = False + else: + yield a, b # type: ignore[misc] + a = b + + +# Reference: https://docs.python.org/3/library/itertools.html#itertools.tee +@substitute_in_graph(itertools.tee) +def tee(iterable: Iterable[_T], n: int = 2, /) -> tuple[Iterator[_T], ...]: + iterator = iter(iterable) + shared_link = [None, None] + + def _tee(link) -> Iterator[_T]: # type: ignore[no-untyped-def] + try: + while True: + if link[1] is None: + link[0] = next(iterator) + link[1] = [None, None] + value, link = link + yield value + except StopIteration: + return + + return tuple(_tee(shared_link) for _ in range(n)) + + +@overload +# pyrefly: ignore [inconsistent-overload] +def zip_longest( + iter1: Iterable[_T1], + /, + *, + fillvalue: _U = ..., +) -> Iterator[tuple[_T1]]: ... + + +@overload +# pyrefly: ignore [inconsistent-overload] +def zip_longest( + iter1: Iterable[_T1], + iter2: Iterable[_T2], + /, +) -> Iterator[tuple[_T1 | None, _T2 | None]]: ... + + +@overload +# pyrefly: ignore [inconsistent-overload] +def zip_longest( + iter1: Iterable[_T1], + iter2: Iterable[_T2], + /, + *, + fillvalue: _U = ..., +) -> Iterator[tuple[_T1 | _U, _T2 | _U]]: ... + + +@overload +# pyrefly: ignore [inconsistent-overload] +def zip_longest( + iter1: Iterable[_T], + iter2: Iterable[_T], + iter3: Iterable[_T], + /, + *iterables: Iterable[_T], +) -> Iterator[tuple[_T | None, ...]]: ... + + +@overload +# pyrefly: ignore [inconsistent-overload] +def zip_longest( + iter1: Iterable[_T], + iter2: Iterable[_T], + iter3: Iterable[_T], + /, + *iterables: Iterable[_T], + fillvalue: _U = ..., +) -> Iterator[tuple[_T | _U, ...]]: ... + + +# Reference: https://docs.python.org/3/library/itertools.html#itertools.zip_longest +@substitute_in_graph(itertools.zip_longest, is_embedded_type=True) # type: ignore[arg-type,misc] +def zip_longest( + *iterables: Iterable[_T], + fillvalue: _U = None, # type: ignore[assignment] +) -> Iterator[tuple[_T | _U, ...]]: + # zip_longest('ABCD', 'xy', fillvalue='-') -> Ax By C- D- + + iterators = list(map(iter, iterables)) + num_active = len(iterators) + if not num_active: + return + + while True: + values = [] + for i, iterator in enumerate(iterators): + try: + value = next(iterator) + except StopIteration: + num_active -= 1 + if not num_active: + return + iterators[i] = itertools.repeat(fillvalue) # type: ignore[arg-type] + value = fillvalue # type: ignore[assignment] + values.append(value) + yield tuple(values) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/loader.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/loader.py new file mode 100644 index 0000000000000000000000000000000000000000..cfce3edfb0ab05bdd5862fac7f3d917cf89e3d5a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/loader.py @@ -0,0 +1,48 @@ +# Used to load and initialize polyfill handlers when importing torch._dynamo +# Please add a new import when adding a new polyfill module. + +import importlib +from typing import TYPE_CHECKING + +import torch.utils._pytree as python_pytree + +from .. import polyfills, trace_rules + + +if TYPE_CHECKING: + from types import ModuleType + + +# See also the TYPE_CHECKING block in torch/_dynamo/polyfills/__init__.py +POLYFILLED_MODULE_NAMES: tuple[str, ...] = ( + "_collections", + "builtins", + "copy", + "functools", + "itertools", + "operator", + "os", + "struct", + "sys", + "fx", + "tensor", + "torch_c_nn", + "traceback", +) +if python_pytree._cxx_pytree_dynamo_traceable: + POLYFILLED_MODULE_NAMES += ("pytree",) + +POLYFILLED_MODULES: tuple["ModuleType", ...] = tuple( + importlib.import_module(f".{submodule}", package=polyfills.__name__) + for submodule in POLYFILLED_MODULE_NAMES +) + + +# Unregister the builtin functions from _builtin_function_ids to let them to be +# dispatched with the appropriate VariableTracker type. Otherwise, they will be +# dispatched with BuiltinVariable if present in _builtin_function_ids. +for polyfill_module in POLYFILLED_MODULES: + for polyfill_name in polyfill_module.__all__: + polyfill_handler = getattr(polyfill_module, polyfill_name) + original_fn = polyfill_handler.__torch_dynamo_original__ + trace_rules._builtin_function_ids.remove(id(original_fn)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/operator.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/operator.py new file mode 100644 index 0000000000000000000000000000000000000000..cae61df2c04307f294f1bf56fa68323acabc0e48 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/operator.py @@ -0,0 +1,119 @@ +""" +Python polyfills for operator +""" + +from __future__ import annotations + +import operator +from typing import Any, overload, TYPE_CHECKING, TypeVar +from typing_extensions import TypeVarTuple, Unpack + +from ..decorators import substitute_in_graph + + +if TYPE_CHECKING: + from collections.abc import Callable, Iterable + + +# Most unary and binary operators are handled by BuiltinVariable (e.g., `pos`, `add`) +__all__ = ["attrgetter", "itemgetter", "methodcaller", "countOf"] + + +_T = TypeVar("_T") +_T1 = TypeVar("_T1") +_T2 = TypeVar("_T2") +_Ts = TypeVarTuple("_Ts") +_U = TypeVar("_U") +_U1 = TypeVar("_U1") +_U2 = TypeVar("_U2") +_Us = TypeVarTuple("_Us") + + +@overload +# pyrefly: ignore [inconsistent-overload] +def attrgetter(attr: str, /) -> Callable[[Any], _U]: ... + + +@overload +# pyrefly: ignore [inconsistent-overload] +def attrgetter( + attr1: str, attr2: str, /, *attrs: str +) -> Callable[[Any], tuple[_U1, _U2, Unpack[_Us]]]: ... + + +# Reference: https://docs.python.org/3/library/operator.html#operator.attrgetter +@substitute_in_graph(operator.attrgetter, is_embedded_type=True) # type: ignore[arg-type,misc] +def attrgetter(*attrs: str) -> Callable[[Any], Any | tuple[Any, ...]]: + if len(attrs) == 0: + raise TypeError("attrgetter expected 1 argument, got 0") + + if any(not isinstance(attr, str) for attr in attrs): + raise TypeError("attribute name must be a string") + + def resolve_attr(obj: Any, attr: str) -> Any: + for name in attr.split("."): + obj = getattr(obj, name) + return obj + + if len(attrs) == 1: + attr = attrs[0] + + def getter(obj: Any) -> Any: + return resolve_attr(obj, attr) + + else: + + def getter(obj: Any) -> tuple[Any, ...]: # type: ignore[misc] + return tuple(resolve_attr(obj, attr) for attr in attrs) + + return getter + + +@overload +# pyrefly: ignore [inconsistent-overload] +def itemgetter(item: _T, /) -> Callable[[Any], _U]: ... + + +@overload +# pyrefly: ignore [inconsistent-overload] +def itemgetter( + item1: _T1, item2: _T2, /, *items: Unpack[_Ts] +) -> Callable[[Any], tuple[_U1, _U2, Unpack[_Us]]]: ... + + +# Reference: https://docs.python.org/3/library/operator.html#operator.itemgetter +@substitute_in_graph(operator.itemgetter, is_embedded_type=True) # type: ignore[arg-type,misc] +def itemgetter(*items: Any) -> Callable[[Any], Any | tuple[Any, ...]]: + if len(items) == 0: + raise TypeError("itemgetter expected 1 argument, got 0") + + if len(items) == 1: + item = items[0] + + def getter(obj: Any) -> Any: + return obj[item] + + else: + + def getter(obj: Any) -> tuple[Any, ...]: # type: ignore[misc] + return tuple(obj[item] for item in items) + + return getter + + +# Reference: https://docs.python.org/3/library/operator.html#operator.methodcaller +@substitute_in_graph(operator.methodcaller, is_embedded_type=True) # type: ignore[arg-type] +def methodcaller(name: str, /, *args: Any, **kwargs: Any) -> Callable[[Any], Any]: + if not isinstance(name, str): + raise TypeError("method name must be a string") + + def caller(obj: Any) -> Any: + return getattr(obj, name)(*args, **kwargs) + + return caller + + +# Reference: https://docs.python.org/3/library/operator.html#operator.countOf +@substitute_in_graph(operator.countOf, can_constant_fold_through=True) # type: ignore[arg-type,misc] +def countOf(a: Iterable[_T], b: _T, /) -> int: + return sum(it is b or it == b for it in a) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/os.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/os.py new file mode 100644 index 0000000000000000000000000000000000000000..595b7f3636a0ec6d19f0df78fe7c49babe080435 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/os.py @@ -0,0 +1,37 @@ +""" +Python polyfills for os +""" + +from __future__ import annotations + +import os +from typing import AnyStr + +from ..decorators import substitute_in_graph + + +__all__ = ["fspath"] + + +# Copied from os.py in the standard library +# pyrefly: ignore [bad-argument-type] +@substitute_in_graph(os.fspath, can_constant_fold_through=True) +def fspath(path: AnyStr | os.PathLike[AnyStr]) -> AnyStr: + if isinstance(path, (str, bytes)): + return path + + path_type = type(path) + try: + path_repr = path_type.__fspath__(path) # type: ignore[arg-type] + except AttributeError: + if hasattr(path_type, "__fspath__"): + raise + raise TypeError( + f"expected str, bytes or os.PathLike object, not {path_type.__name__}", + ) from None + if isinstance(path_repr, (str, bytes)): + return path_repr # type: ignore[return-value] + raise TypeError( + f"expected {path_type.__name__}.__fspath__() to return str or bytes, " + f"not {type(path_repr).__name__}", + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/pytree.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/pytree.py new file mode 100644 index 0000000000000000000000000000000000000000..d834ce177066756d9d2bc13fc44852fbd344ed35 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/pytree.py @@ -0,0 +1,759 @@ +# Owner(s): ["module: pytree"] + +""" +Python polyfills for torch.utils.pytree +""" + +from __future__ import annotations + +from collections import deque +from dataclasses import dataclass, field +from typing import Any, TYPE_CHECKING, TypeVar + +import optree +import optree._C +import optree.utils +from optree import ( + is_namedtuple, + is_namedtuple_class, + is_namedtuple_instance, + is_structseq, + is_structseq_class, + is_structseq_instance, + namedtuple_fields, + structseq_fields, +) + +import torch.utils._cxx_pytree as cxx_pytree # noqa: F401 +import torch.utils._pytree as python_pytree +from torch.utils._pytree import BUILTIN_TYPES, STANDARD_DICT_TYPES + +from ..decorators import substitute_in_graph + + +if TYPE_CHECKING: + import builtins + from collections.abc import Callable, Iterable, Mapping + from typing_extensions import Self, TypeIs + + from torch.utils._cxx_pytree import PyTree + + +__all__ = [ + "is_namedtuple", + "is_namedtuple_class", + "is_namedtuple_instance", + "is_structseq", + "is_structseq_class", + "is_structseq_instance", + "namedtuple_fields", + "structseq_fields", + "treespec_leaf", + "treespec_tuple", + "treespec_dict", + "tree_is_leaf", + "tree_iter", + "tree_leaves", + "tree_flatten", + "tree_flatten_with_path", + "tree_structure", + "tree_unflatten", +] + + +_T = TypeVar("_T") +_KT = TypeVar("_KT") +_VT = TypeVar("_VT") + + +@substitute_in_graph( + optree._C.is_dict_insertion_ordered, + can_constant_fold_through=True, +) +def _(*args: Any, **kwargs: Any) -> bool: + # In namespace 'torch', the dictionary is always traversed in insertion order. + # This function returns True. + raise ValueError( + "Should not be called directly " + "because the original function will be called in the constant fold path." + ) + + +__name = "" +for __name, __func in ( + ("is_namedtuple", is_namedtuple), + ("is_namedtuple_class", is_namedtuple_class), + ("is_namedtuple_instance", is_namedtuple_instance), + ("is_structseq", is_structseq), + ("is_structseq_class", is_structseq_class), + ("is_structseq_instance", is_structseq_instance), + ("namedtuple_fields", namedtuple_fields), + ("structseq_fields", structseq_fields), +): + globals()[__name] = substitute_in_graph( + __func, # type: ignore[arg-type] + can_constant_fold_through=True, + )(__func.__python_implementation__) # type: ignore[attr-defined] + del __func +del __name + + +@substitute_in_graph(optree.tree_is_leaf, can_constant_fold_through=True) # type: ignore[arg-type] +def tree_is_leaf( + tree: PyTree, + /, + is_leaf: Callable[[PyTree], bool] | None = None, + *, + none_is_leaf: bool = False, + namespace: str = "", +) -> bool: + if (tree is None and none_is_leaf) or (is_leaf is not None and is_leaf(tree)): + return True + if optree.register_pytree_node.get(type(tree), namespace=namespace) is None: + return True + return False + + +@substitute_in_graph(optree.tree_iter, can_constant_fold_through=False) # type: ignore[arg-type] +def tree_iter( + tree: PyTree, + /, + is_leaf: Callable[[PyTree], bool] | None = None, + *, + none_is_leaf: bool = False, + namespace: str = "", +) -> Iterable[Any]: + stack = [tree] + while stack: + node = stack.pop() + if tree_is_leaf( + node, + is_leaf=is_leaf, + none_is_leaf=none_is_leaf, + namespace=namespace, + ): + yield node + continue + + children, *_ = optree.tree_flatten_one_level( + node, + is_leaf=is_leaf, + none_is_leaf=none_is_leaf, + namespace=namespace, + ) + stack.extend(reversed(children)) + + +@substitute_in_graph(optree.tree_leaves, can_constant_fold_through=True) # type: ignore[arg-type] +def tree_leaves( + tree: PyTree, + /, + is_leaf: Callable[[PyTree], bool] | None = None, + *, + none_is_leaf: bool = False, + namespace: str = "", +) -> list[Any]: + return list( + tree_iter( + tree, + is_leaf=is_leaf, + none_is_leaf=none_is_leaf, + namespace=namespace, + ) + ) + + +class _Asterisk(str): + __slots__ = () + + def __new__(cls) -> Self: + return super().__new__(cls, "*") + + def __repr__(self) -> str: + return "*" # no quotes + + +_asterisk = _Asterisk() +del _Asterisk + + +@dataclass(frozen=True, slots=True) +class PyTreeSpec: + """Analog for :class:`optree.PyTreeSpec` in Python.""" + + _children: tuple[PyTreeSpec, ...] + _type: builtins.type | None + _metadata: Any + _entries: tuple[Any, ...] + _unflatten_func: Callable[[Any | None, Iterable[PyTree]], PyTree] | None + none_is_leaf: bool + namespace: str + + num_nodes: int = field(init=False) + num_leaves: int = field(init=False) + num_children: int = field(init=False) + + def __post_init__(self, /) -> None: + if self._type is None: + assert len(self._children) == 0 + assert self._metadata is None + assert self._entries == () + assert self._unflatten_func is None + num_nodes = 1 + num_leaves = 1 + num_children = 0 + else: + assert callable(self._unflatten_func) + num_nodes = 1 + num_leaves = 0 + for child in self._children: + num_nodes += child.num_nodes + num_leaves += child.num_leaves + num_children = len(self._children) + + object.__setattr__(self, "num_nodes", num_nodes) + object.__setattr__(self, "num_leaves", num_leaves) + object.__setattr__(self, "num_children", num_children) + + def __repr__(self, /) -> str: + def helper(treespec: PyTreeSpec) -> str: + if treespec.is_leaf(): + assert treespec.type is None + return _asterisk + + assert treespec.type is not None + assert callable(treespec._unflatten_func) + children_representations = [ + helper(subspec) for subspec in treespec._children + ] + if ( + treespec.type in BUILTIN_TYPES + or (treespec.type is type(None) and not self.none_is_leaf) + or optree.is_namedtuple_class(treespec.type) + or optree.is_structseq_class(treespec.type) + ): + return treespec._unflatten_func( + treespec._metadata, + children_representations, + ) + return ( + f"CustomTreeNode({treespec.type.__name__}[{treespec._metadata!r}], " + f"[{', '.join(children_representations)}])" + ) + + inner = [ + str(helper(self)), + *(["NoneIsLeaf"] if self.none_is_leaf else []), + f"namespace={self.namespace!r}", + ] + return f"PyTreeSpec({', '.join(inner)})" + + def __len__(self, /) -> int: + return self.num_leaves + + @property + def type(self, /) -> builtins.type | None: + return self._type + + def is_leaf(self, /) -> bool: + return self.num_nodes == 1 and self.num_leaves == 1 + + def paths(self, /) -> list[tuple[Any, ...]]: + def helper(treespec: PyTreeSpec, path_prefix: list[Any]) -> None: + if treespec.is_leaf(): + paths.append(path_prefix) + return + + for entry, subspec in zip( + treespec._entries, + treespec._children, + strict=True, + ): + helper(subspec, path_prefix + [entry]) + + paths: list[list[Any]] = [] + helper(self, []) + return [tuple(path) for path in paths] + + def accessors(self, /) -> list[optree.PyTreeAccessor]: + def helper( + treespec: PyTreeSpec, + entry_path_prefix: list[optree.PyTreeEntry], + ) -> None: + if treespec.is_leaf(): + entry_paths.append(entry_path_prefix) + return + + node_type = treespec.type + assert node_type is not None + handler = optree.register_pytree_node.get( + node_type, namespace=treespec.namespace + ) + assert handler is not None + kind: optree.PyTreeKind = handler.kind + path_entry_type: type[optree.PyTreeEntry] = handler.path_entry_type + + for entry, subspec in zip( + treespec._entries, + treespec._children, + strict=True, + ): + helper( + subspec, + entry_path_prefix + [path_entry_type(entry, node_type, kind)], + ) + + entry_paths: list[list[optree.PyTreeEntry]] = [] + helper(self, []) + return [optree.PyTreeAccessor(path) for path in entry_paths] + + def children(self, /) -> list[PyTreeSpec]: + return list(self._children) + + def child(self, index: int, /) -> PyTreeSpec: + return self._children[index] + + def entries(self, /) -> list[Any]: + return list(self._entries) + + def entry(self, index: int, /) -> Any: + return self._entries[index] + + def flatten_up_to(self, tree: PyTree, /) -> list[PyTree]: + def helper( + treespec: PyTreeSpec, + node: PyTree, + subtrees: list[PyTree], + ) -> None: + if treespec.is_leaf(): + subtrees.append(node) + return + + node_type = type(node) + if treespec.type not in BUILTIN_TYPES: + # Always require custom node types to match exactly + if node_type != treespec.type: + raise ValueError( + f"Type mismatch; " + f"expected {treespec.type!r}, but got {node_type!r}.", + ) + + children, metadata, *_ = optree.tree_flatten_one_level( + node, + none_is_leaf=self.none_is_leaf, + namespace=self.namespace, + ) + if len(children) != treespec.num_children: + raise ValueError( + f"Node arity mismatch; " + f"expected {treespec.num_children}, but got {len(children)}.", + ) + if metadata != treespec._metadata: + raise ValueError( + f"Node context mismatch for custom node type {treespec.type!r}.", + ) + else: + # For builtin dictionary types, we allow some flexibility + # Otherwise, we require exact matches + both_standard_dict = ( + treespec.type in STANDARD_DICT_TYPES + and node_type in STANDARD_DICT_TYPES + ) + if not both_standard_dict and node_type != treespec.type: + raise ValueError( + f"Node type mismatch; " + f"expected {treespec.type!r}, but got {node_type!r}.", + ) + if len(node) != treespec.num_children: + raise ValueError( + f"Node arity mismatch; " + f"expected {treespec.num_children}, but got {len(node)}.", + ) + + if both_standard_dict: + # dictionary types are compatible with each other + expected_keys = treespec.entries() + got_key_set = set(node) + expected_key_set = set(expected_keys) + if got_key_set != expected_key_set: + missing_keys = expected_key_set.difference(got_key_set) + extra_keys = got_key_set.difference(expected_key_set) + message = "" + if missing_keys: + message += f"; missing key(s): {missing_keys}" + if extra_keys: + message += f"; extra key(s): {extra_keys}" + raise ValueError(f"Node keys mismatch{message}.") + children = [node[key] for key in expected_keys] + else: + # node_type is treespec.type + children, metadata, *_ = optree.tree_flatten_one_level( + node, + none_is_leaf=self.none_is_leaf, + namespace=self.namespace, + ) + if ( + node_type is not deque # ignore mismatch of `maxlen` for deque + ) and metadata != treespec._metadata: + raise ValueError( + f"Node metadata mismatch for node type {treespec.type!r}; " + f"expected {treespec._metadata!r}, but got {metadata!r}.", # namedtuple type mismatch + ) + + for subtree, subspec in zip(children, treespec._children, strict=True): + helper(subspec, subtree, subtrees) + + subtrees: list[PyTree] = [] + helper(self, tree, subtrees) + return subtrees + + def unflatten(self, leaves: Iterable[Any], /) -> PyTree: + if not isinstance(leaves, (list, tuple)): + leaves = list(leaves) + if len(leaves) != self.num_leaves: + raise ValueError( + f"treespec.unflatten(leaves): `leaves` has length {len(leaves)} " + f"but the spec refers to a pytree that holds {self.num_leaves} " + f"items ({self}).", + ) + if self.is_leaf(): + return leaves[0] + + # Recursively unflatten the children + start = 0 + end = 0 + subtrees = [] + for subspec in self._children: + end += subspec.num_leaves + subtrees.append(subspec.unflatten(leaves[start:end])) + start = end + + assert callable(self._unflatten_func) + return self._unflatten_func(self._metadata, subtrees) + + +def _is_pytreespec_instance(obj: Any, /) -> TypeIs[PyTreeSpec | python_pytree.TreeSpec]: + return isinstance(obj, (PyTreeSpec, python_pytree.TreeSpec)) + + +@substitute_in_graph( # type: ignore[arg-type] + optree.treespec_leaf, + # We need to disable constant folding here because we want the function to reference the + # PyTreeSpec class defined above, not the one in the C++ module. + can_constant_fold_through=False, +) +def treespec_leaf( + *, + none_is_leaf: bool = False, + namespace: str = "", # unused +) -> PyTreeSpec: + return PyTreeSpec( + (), + None, + None, + (), + None, + none_is_leaf=none_is_leaf, + namespace="", + ) + + +@substitute_in_graph( # type: ignore[arg-type] + optree.treespec_tuple, + # We need to disable constant folding here because we want the function to reference the + # PyTreeSpec class defined above, not the one in the C++ module. + can_constant_fold_through=False, +) +def treespec_tuple( + iterable: Iterable[PyTreeSpec] = (), + /, + *, + none_is_leaf: bool = False, + namespace: str = "", +) -> PyTreeSpec: + children = tuple(iterable) + if any(not _is_pytreespec_instance(child) for child in children): + raise ValueError(f"Expected a tuple of PyTreeSpecs, got: {children!r}.") + if any(child.none_is_leaf != none_is_leaf for child in children): + raise ValueError( + "All children PyTreeSpecs must have the same `none_is_leaf` value " + f"as the parent; expected {none_is_leaf}, got: {children!r}.", + ) + if any(child.namespace not in (namespace, "") for child in children): + raise ValueError( + "All children PyTreeSpecs must have the same `namespace` value " + f"as the parent; expected {namespace!r}, got: {children!r}.", + ) + handler = optree.register_pytree_node.get(tuple, namespace=namespace) + assert handler is not None + return PyTreeSpec( + tuple(children), + tuple, + None, + tuple(range(len(children))), + handler.unflatten_func, + none_is_leaf=none_is_leaf, + namespace=namespace, + ) + + +@substitute_in_graph( # type: ignore[arg-type] + optree.treespec_dict, + # We need to disable constant folding here because we want the function to reference the + # PyTreeSpec class defined above, not the one in the C++ module. + can_constant_fold_through=False, +) +def treespec_dict( + mapping: Mapping[Any, PyTreeSpec] | Iterable[tuple[Any, PyTreeSpec]] = (), + /, + *, + none_is_leaf: bool = False, + namespace: str = "", + **kwargs: PyTreeSpec, +) -> PyTreeSpec: + dct = dict(mapping, **kwargs) + if any(not _is_pytreespec_instance(child) for child in dct.values()): + raise ValueError(f"Expected a dictionary of TreeSpecs, got: {dct!r}.") + if any(child.none_is_leaf != none_is_leaf for child in dct.values()): + raise ValueError( + "All children PyTreeSpecs must have the same `none_is_leaf` value " + f"as the parent; expected {none_is_leaf}, got: {dct!r}.", + ) + if any(child.namespace not in (namespace, "") for child in dct.values()): + raise ValueError( + "All children PyTreeSpecs must have the same `namespace` value " + f"as the parent; expected {namespace!r}, got: {dct!r}.", + ) + + ( + children, + metadata, + entries, + unflatten_func, + ) = optree.tree_flatten_one_level( # type: ignore[assignment,var-annotated] + dct, # type: ignore[arg-type] + none_is_leaf=none_is_leaf, + namespace=namespace, + ) + return PyTreeSpec( + tuple(children), # type: ignore[arg-type] + dict, + metadata, + entries, + unflatten_func, # type: ignore[arg-type] + none_is_leaf=none_is_leaf, + namespace=namespace, + ) + + +@substitute_in_graph( # type: ignore[arg-type] + optree.tree_flatten, + # We need to disable constant folding here because we want the function to reference the + # PyTreeSpec class defined above, not the one in the C++ module. + can_constant_fold_through=False, +) +def tree_flatten( + tree: PyTree, + /, + is_leaf: Callable[[PyTree], bool] | None = None, + *, + none_is_leaf: bool = False, + namespace: str = "", +) -> tuple[list[Any], PyTreeSpec]: + def helper(node: PyTree, leaves: list[Any]) -> PyTreeSpec: + if tree_is_leaf( + node, + is_leaf=is_leaf, + none_is_leaf=none_is_leaf, + namespace=namespace, + ): + leaves.append(node) + return PyTreeSpec( + (), + None, + None, + (), + None, + none_is_leaf=none_is_leaf, + namespace=namespace, + ) + + ( + children, + metadata, + entries, + unflatten_func, + ) = optree.tree_flatten_one_level( + node, + is_leaf=is_leaf, + none_is_leaf=none_is_leaf, + namespace=namespace, + ) + + # Recursively flatten the children + subspecs = tuple(helper(child, leaves) for child in children) + return PyTreeSpec( + subspecs, + type(node), + metadata, + entries, + unflatten_func, # type: ignore[arg-type] + none_is_leaf=none_is_leaf, + namespace=namespace, + ) # type: ignore[arg-type] + + leaves: list[Any] = [] + treespec = helper(tree, leaves) + return leaves, treespec + + +@substitute_in_graph( # type: ignore[arg-type] + optree._C.flatten, + # We need to disable constant folding here because we want the function to reference the + # PyTreeSpec class defined above, not the one in the C++ module. + can_constant_fold_through=False, +) +def _C_flatten( + tree: PyTree, + /, + leaf_predicate: Callable[[PyTree], bool] | None = None, + none_is_leaf: bool = False, + namespace: str = "", +) -> tuple[list[Any], PyTreeSpec]: + return tree_flatten( # type: ignore[return-value] + tree, + is_leaf=leaf_predicate, + none_is_leaf=none_is_leaf, + namespace=namespace, + ) + + +@substitute_in_graph( # type: ignore[arg-type] + optree.tree_flatten_with_path, + # We need to disable constant folding here because we want the function to reference the + # PyTreeSpec class defined above, not the one in the C++ module. + can_constant_fold_through=False, +) +def tree_flatten_with_path( + tree: PyTree, + /, + is_leaf: Callable[[PyTree], bool] | None = None, + *, + none_is_leaf: bool = False, + namespace: str = "", +) -> tuple[list[tuple[Any, ...]], list[Any], PyTreeSpec]: + leaves, treespec = tree_flatten( + tree, + is_leaf=is_leaf, + none_is_leaf=none_is_leaf, + namespace=namespace, + ) + return treespec.paths(), leaves, treespec # type: ignore[return-value] + + +@substitute_in_graph( # type: ignore[arg-type] + optree._C.flatten_with_path, + # We need to disable constant folding here because we want the function to reference the + # PyTreeSpec class defined above, not the one in the C++ module. + can_constant_fold_through=False, +) +def _C_flatten_with_path( + tree: PyTree, + /, + leaf_predicate: Callable[[PyTree], bool] | None = None, + none_is_leaf: bool = False, + namespace: str = "", +) -> tuple[list[tuple[Any, ...]], list[Any], PyTreeSpec]: + return tree_flatten_with_path( # type: ignore[return-value] + tree, + is_leaf=leaf_predicate, + none_is_leaf=none_is_leaf, + namespace=namespace, + ) + + +@substitute_in_graph( # type: ignore[arg-type] + optree.tree_structure, + # We need to disable constant folding here because we want the function to reference the + # PyTreeSpec class defined above, not the one in the C++ module. + can_constant_fold_through=False, +) +def tree_structure( + tree: PyTree, + /, + is_leaf: Callable[[PyTree], bool] | None = None, + *, + none_is_leaf: bool = False, + namespace: str = "", +) -> PyTreeSpec: + return tree_flatten( # type: ignore[return-value] + tree, + is_leaf=is_leaf, + none_is_leaf=none_is_leaf, + namespace=namespace, + )[1] + + +@substitute_in_graph( # type: ignore[arg-type] + optree.tree_unflatten, + # We need to disable constant folding here because we want the function to reference the + # PyTreeSpec class defined above, not the one in the C++ module. + can_constant_fold_through=False, +) +def tree_unflatten(treespec: PyTreeSpec, leaves: Iterable[Any]) -> PyTree: + if not _is_pytreespec_instance(treespec): + raise TypeError( + f"Expected `treespec` to be an instance of " + f"PyTreeSpec but got item of type {type(treespec)}." + ) + return treespec.unflatten(leaves) + + +_none_registration = optree.register_pytree_node.get(type(None)) +assert _none_registration is not None + + +@substitute_in_graph( # type: ignore[arg-type] + _none_registration.unflatten_func, + can_constant_fold_through=True, + skip_signature_check=True, +) +def none_unflatten(_: None, children: Iterable[_T], /) -> None: + if len(list(children)) != 0: + raise ValueError("Expected no children.") + return None + + +with optree.dict_insertion_ordered(False, namespace="torch"): + _dict_registration = optree.register_pytree_node.get(dict) + assert _dict_registration is not None + + +@substitute_in_graph( # type: ignore[arg-type] + _dict_registration.flatten_func, + can_constant_fold_through=True, + skip_signature_check=True, +) +def dict_flatten( + dct: dict[_KT, _VT], / +) -> tuple[list[_VT], tuple[list[_KT], list[_KT]], tuple[_KT, ...]]: + sorted_keys = optree.utils.total_order_sorted(dct) + values = [dct[key] for key in sorted_keys] + original_keys = list(dct) + return values, (original_keys, sorted_keys), tuple(sorted_keys) + + +@substitute_in_graph( # type: ignore[arg-type] + _dict_registration.unflatten_func, + can_constant_fold_through=True, + skip_signature_check=True, +) +def dict_unflatten( + metadata: tuple[list[_KT], list[_KT]], + values: Iterable[_VT], + /, +) -> dict[_KT, _VT]: + original_keys, sorted_keys = metadata + d = dict.fromkeys(original_keys) + d.update(zip(sorted_keys, values, strict=True)) + return d # type: ignore[return-value] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/struct.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/struct.py new file mode 100644 index 0000000000000000000000000000000000000000..f4522a12f7323e51da6f4454814e87daf82cea98 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/struct.py @@ -0,0 +1,27 @@ +""" +Python polyfills for struct +""" + +from __future__ import annotations + +import struct +from typing import Any +from typing_extensions import Buffer + +from ..decorators import substitute_in_graph + + +__all__ = [ + "pack", + "unpack", +] + + +@substitute_in_graph(struct.pack, can_constant_fold_through=True) # type: ignore[arg-type] +def pack(fmt: bytes | str, /, *v: Any) -> bytes: + return struct.pack(fmt, *v) + + +@substitute_in_graph(struct.unpack, can_constant_fold_through=True) # type: ignore[arg-type] +def unpack(format: bytes | str, buffer: Buffer, /) -> tuple[Any, ...]: + return struct.unpack(format, buffer) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/sys.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/sys.py new file mode 100644 index 0000000000000000000000000000000000000000..d04a8b2febc64a1f4d340f1e7c337c7ac6876054 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/sys.py @@ -0,0 +1,39 @@ +""" +Python polyfills for sys +""" + +from __future__ import annotations + +import sys + +from ..decorators import substitute_in_graph + + +__all__ = [ + "intern", + "getrecursionlimit", +] + + +# pyrefly: ignore [bad-argument-type] +@substitute_in_graph(sys.intern, can_constant_fold_through=True) +def intern(string: str, /) -> str: + return string + + +@substitute_in_graph(sys.getrecursionlimit, can_constant_fold_through=True) +def getrecursionlimit() -> int: + return sys.getrecursionlimit() + + +if sys.version_info >= (3, 11): + + @substitute_in_graph(sys.get_int_max_str_digits, can_constant_fold_through=True) + def get_int_max_str_digits() -> int: + return sys.get_int_max_str_digits() + + @substitute_in_graph(sys.set_int_max_str_digits, can_constant_fold_through=True) + def set_int_max_str_digits(maxdigits: int) -> None: + sys.set_int_max_str_digits(maxdigits) + + __all__ += ["get_int_max_str_digits", "set_int_max_str_digits"] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/tensor.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/tensor.py new file mode 100644 index 0000000000000000000000000000000000000000..dffa98f60f3b578810a2386255964d03858afa37 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/tensor.py @@ -0,0 +1,40 @@ +from typing import Any + +import torch + +from ..decorators import substitute_in_graph + + +@substitute_in_graph( # type: ignore[arg-type] + torch.Tensor._make_subclass +) +def make_subclass( + cls: type[Any], data: torch.Tensor, requires_grad: bool = False, **kwargs: Any +) -> Any: + with torch._C.DisableTorchFunctionSubclass(): + # This is a rough approximation of `THPVariable_make_subclass`. It should + # suffice for most of Dynamo tracing purposes. + # https://github.com/pytorch/pytorch/blob/ccfde4dadfa3c342076a1ee387017f84dd4ad2f7/torch/csrc/autograd/python_variable.cpp#L597-L650 + assert len(kwargs) == 0, ( + "_make_subclass only supports requires_grad as keyword arg" + ) + data = data.detach() + + # Avoid unnecessary `requires_grad` mutation, which isn't supported in Dynamo. + if data.requires_grad != requires_grad: + data.requires_grad = requires_grad + + # Dynamo can't yet handle upcasting to base tensor type via `as_subclass`. + if cls is torch.Tensor: + return torch.Tensor(data) + + # Calling `as_subclass` because + # 1. Dynamo knows how to handle it + # 2. the C impls match at this point -- both `THPVariable_make_subclass` and + # `THPVariable_as_subclass` calls `THPVariable_NewWithVar`. + return data.as_subclass(cls) + + +__all__ = [ + "make_subclass", +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/torch_c_nn.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/torch_c_nn.py new file mode 100644 index 0000000000000000000000000000000000000000..c2258adac4cd3c636172a822e0683caf5c2ede74 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/torch_c_nn.py @@ -0,0 +1,121 @@ +""" +Polyfills for torch._C._nn functions. +""" + +from typing import Any + +import torch +from torch.overrides import _is_torch_function_mode_enabled, _pop_mode_temporarily + +from ..decorators import substitute_in_graph + + +@substitute_in_graph(torch._C._nn._parse_to, skip_signature_check=True) +def _parse_to_polyfill( + *args: Any, **kwargs: Any +) -> tuple[torch.device, torch.dtype, bool, torch.memory_format]: + """ + Polyfill for torch._C._nn._parse_to that parses arguments to nn.Module.to(). + + Signature mirrors torch._C._nn._parse_to which accepts: + - to(device) - device as string or torch.device + - to(dtype) - dtype as torch.dtype + - to(tensor) - extracts device and dtype from tensor + - to(device=..., dtype=..., non_blocking=..., memory_format=...) + + Returns: + tuple: (device, dtype, non_blocking, memory_format) + """ + # Check for __torch_function__ mode and dispatch using handle_torch_function pattern + if _is_torch_function_mode_enabled(): + with _pop_mode_temporarily() as mode: + result = mode.__torch_function__( + torch._C._nn._parse_to, tuple(), args, kwargs or {} + ) + if result is not NotImplemented: + return result + + # Default implementation + device = None + dtype = None + non_blocking = False + memory_format = None + + # Handle positional arguments + if len(args) == 1: + arg = args[0] + # Check if it's a tensor + if isinstance(arg, torch.Tensor): + device = arg.device + dtype = arg.dtype + # Check if it's a dtype + elif isinstance(arg, torch.dtype): + dtype = arg + # Check if it's a device (string or torch.device) + elif isinstance(arg, (str, torch.device)): + device = torch.device(arg) if isinstance(arg, str) else arg + else: + raise TypeError( + f"to() received an invalid combination of arguments. Got: {type(arg)}" + ) + elif len(args) > 1: + raise TypeError( + f"to() received too many positional arguments. Got {len(args)}, expected at most 1" + ) + + # Handle keyword arguments + if "device" in kwargs: + device_arg = kwargs["device"] + if device_arg is not None: + device = ( + torch.device(device_arg) if isinstance(device_arg, str) else device_arg + ) + + if "dtype" in kwargs: + dtype = kwargs["dtype"] + + if "non_blocking" in kwargs: + non_blocking = kwargs["non_blocking"] + + if "memory_format" in kwargs: + memory_format = kwargs["memory_format"] + + # pyrefly: ignore[bad-return] + return (device, dtype, non_blocking, memory_format) + + +@substitute_in_graph(torch.__future__.get_swap_module_params_on_conversion) +def get_swap_module_params_on_conversion_polyfill() -> bool: + """ + Polyfill for torch.__future__.get_swap_module_params_on_conversion. + + Returns the actual value from the underlying global variable. + """ + # Access the module's global variable directly to avoid recursion + import torch.__future__ as torch_future + + return torch_future._swap_module_params_on_conversion + + +@substitute_in_graph(torch._has_compatible_shallow_copy_type) +def _has_compatible_shallow_copy_type_polyfill( + input: torch.Tensor, from_: torch.Tensor +) -> bool: + """ + Polyfill for torch._has_compatible_shallow_copy_type. + + Checks if two tensors have compatible types for shallow copying. + The C++ implementation checks if input's TensorImpl has compatible shallow copy type + with from_'s key_set. We approximate this by checking if both tensors are the same type. + """ + # Check if both tensors are the same type (handles both regular tensors and subclasses) + # This is more permissive than checking exact torch.Tensor type equality + # but properly handles subclasses by allowing same-type shallow copies + return type(input) is type(from_) + + +__all__ = [ + "_parse_to_polyfill", + "get_swap_module_params_on_conversion_polyfill", + "_has_compatible_shallow_copy_type_polyfill", +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/traceback.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/traceback.py new file mode 100644 index 0000000000000000000000000000000000000000..f44f59ad0c444e9da3f4b218ac2c4b5be9e6276d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/polyfills/traceback.py @@ -0,0 +1,39 @@ +""" +Python polyfills for traceback +""" + +import traceback +from traceback import StackSummary +from types import TracebackType + +from ..decorators import substitute_in_graph + + +__all__ = ["extract_tb", "clear_frames"] + + +@substitute_in_graph(traceback.extract_tb, can_constant_fold_through=True) +def extract_tb(tb: TracebackType | None, limit: int | None = None) -> StackSummary: + if tb is None: + return traceback.StackSummary.from_list([]) + # pyrefly: ignore [implicit-any] + frame_summary = [] + while tb is not None: + if limit: + if len(frame_summary) < limit: + frame_summary.append( + # pyrefly: ignore[missing-attribute] + tb.frame_summary + ) + else: + break + else: + frame_summary.append(tb.frame_summary) # pyrefly: ignore[missing-attribute] + tb = tb.tb_next + return traceback.StackSummary.from_list(frame_summary) + + +@substitute_in_graph(traceback.clear_frames, can_constant_fold_through=True) +def clear_frames(tb: TracebackType | None) -> None: + # no-op + return None diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/precompile_context.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/precompile_context.py new file mode 100644 index 0000000000000000000000000000000000000000..63569dc5b74ec1e454b1d65dcacdc3fa214fe7ce --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/precompile_context.py @@ -0,0 +1,231 @@ +import copy +import json +import logging +from abc import abstractmethod +from collections import defaultdict +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any, Generic, TypeVar + +import torch +from torch._dynamo.package import ( + _BackendId, + _DynamoCacheEntry, + DynamoCache, + PrecompileCacheEntry, +) + + +""" +Classes and implementations related to precompile +""" + +T = TypeVar("T") +logger = logging.getLogger(__name__) + + +@dataclass +class BackendCacheArtifact(Generic[T]): + """ + Represents a single serializable backend artifact from a dynamo backend. + Each BackendCacheArtifact has a key associated with it along with some + serializable content. + + Example implementation: + + class MyPrecompileCacheArtifact(PrecompileCacheArtifact[MySerializableType]): + my_field: int + + def after_deserialization(self) -> MySerializableType: + result = pickle.loads(self.content) + # Do some extra work post deserialization + result.my_post_deserialization_function(self.my_field) + return result + """ + + key: str + content: Any + + @abstractmethod + def after_deserialization(self) -> T: + """ + Code to be run after reading raw byte contents from disk. + Generally converts self.content from raw bytes back into its original form. + """ + ... + + def edit_contents(self, edit_fn: Callable[..., Any]) -> None: + """ + Edit the contents of the artifact. + """ + self.content = edit_fn(self.content) + + +class EagerCacheArtifact(BackendCacheArtifact[Any]): + def after_deserialization(self) -> Any: + return self.content + + +class BypassDynamoCacheEntry(Exception): + pass + + +class PrecompileContext: + """ + PrecompileContext is a special CacheArtifactManager for handling precompilation + It uses the same interface as CacheArtifactManager, but handles deserialization differently: instead + of placing each artifact into respective caches, it will stitch all the cache artifacts for a single key + together and place it into a global Precompile Cache. + + PrecompileContext has two main portions: dynamo_cache_entries and backend_cache_artifacts. + When saving, PrecompileContext.serialize() will serialize all dynamo cache entries along with any PrecompileCacheArtifacts that + are needed to save those dynamo cache entries. + + The following artifact types are supported by PrecompileContext: + - BundledAOTAutogradCacheArtifact + + """ + + # Protected by the compile_lock + # _backend_artifacts_by_key organizes results by the key of each artifact. + # Each object here must be serializable + _backend_artifacts_by_key: dict[_BackendId, BackendCacheArtifact[Any]] = {} + + # On call to `serialize()`, all cache artifacts in _dynamo_cache_entries are converted + # into DynamoCacheArtifacts and added to _new_cache_artifacts for serialization + _dynamo_cache_entries: dict[str, _DynamoCacheEntry] = {} + + @classmethod + def clear(cls) -> None: + cls._backend_artifacts_by_key.clear() + cls._dynamo_cache_entries.clear() + + @classmethod + def record_artifact( + cls, + artifact: BackendCacheArtifact[Any], + ) -> None: + """ + Records a backend artifact to be used with dynamo cache entries + """ + # Temporarily disable all dispatch modes (including FakeTensorMode) during + # deepcopy to avoid issues with cloning fake tensors (e.g., device mesh + # with meta tensors that fail when cloning due to device mismatches) + from torch.utils._mode_utils import no_dispatch + + with no_dispatch(): + cls._backend_artifacts_by_key[_BackendId(artifact.key)] = copy.deepcopy( + artifact + ) + + @classmethod + def record_dynamo_cache_entry( + cls, cache_entry: _DynamoCacheEntry, key: str + ) -> None: + cls._dynamo_cache_entries[key] = cache_entry + + @classmethod + def edit_artifact(cls, key: str, edit_fn: Callable[..., Any]) -> None: + """ + Edit the content of an existing artifact + """ + assert key in cls._backend_artifacts_by_key, f"Key {key} not found in artifacts" + artifact = cls._backend_artifacts_by_key[_BackendId(key)] + artifact.edit_contents(edit_fn) + + @classmethod + def serialize_artifact_by_key(cls, key: str) -> BackendCacheArtifact[Any] | None: + """ + Return the backend cache artifact with the associated key + """ + return cls._backend_artifacts_by_key.get(_BackendId(key), None) + + @staticmethod + def dump_debug_info( + dynamo_entries: dict[str, _DynamoCacheEntry], + backend_artifacts: dict[_BackendId, BackendCacheArtifact[Any]], + ) -> dict[str, Any]: + """ + Return a JSON serializable debug dump of all entries in the precompile context + Called in serialize before serialization, and in populate_caches after deserialization + """ + # Print debug information + debug_info: defaultdict[str, list[Any]] = defaultdict(list) + for key, cache_entry in dynamo_entries.items(): + info = cache_entry.debug_info() + info["key"] = key + debug_info["dynamo"].append(info) + + for artifact in backend_artifacts.values(): + debug_info["backends"].append(artifact.key) + + return debug_info + + @classmethod + def save_to_dynamo_cache(cls) -> dict[str, Any]: + precompile_cache_entries, debug_info = cls.create_cache_entries() + for key, entry in precompile_cache_entries.items(): + DynamoCache.write(entry, key) + return debug_info + + @classmethod + def create_cache_entries( + cls, + ) -> tuple[dict[str, PrecompileCacheEntry], dict[str, Any]]: + """ + Grabs all the cache entries in the precompile context and + stitches them together into full PrecompileCacheEntries. + """ + dynamo_entries = cls._dynamo_cache_entries + backend_artifacts = cls._backend_artifacts_by_key + + num_artifacts = len(dynamo_entries) + + debug_info = PrecompileContext.dump_debug_info( + dynamo_entries, backend_artifacts + ) + debug_str = json.dumps( + { + "num_entries": num_artifacts, + "artifacts": debug_info, + }, + ) + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "dynamo_cache_entries", + "encoding": "json", + }, + payload_fn=lambda: debug_str, + expect_trace_id=False, + ) + + precompile_cache_entries = {} + + for key, cache_entry in dynamo_entries.items(): + try: + result = PrecompileCacheEntry.from_cache_entry( + cache_entry, backend_artifacts + ) + if result is not None: + precompile_cache_entries[key] = result + except Exception as e: + logger.warning("Failed to create cache entry %s", key, exc_info=True) + + error = e + data = json.dumps( + { + "key": key, + "error": str(error), + } + ) + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "dynamo_cache_exception", + "encoding": "json", + }, + payload_fn=lambda: data, + ) + continue + return precompile_cache_entries, debug_info diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/profiler.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/profiler.py new file mode 100644 index 0000000000000000000000000000000000000000..de100c47f91b62a4d91d6a3830b27bf5cc2792f1 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/profiler.py @@ -0,0 +1,176 @@ +""" +Dynamo profiling implementation. + +This module provides profiling functionality for Dynamo, including: +- ProfileMetrics: Class for collecting and aggregating performance metrics like + execution time, operator counts, and fusion statistics +- ProfileResult: Class for analyzing and reporting profiling results +- Utilities for tracking missed/uncaptured operations +- Functions for instrumenting FX graphs with profiling capabilities + +The profiler helps measure and optimize the performance of Dynamo-compiled code +by tracking both captured and total operations, timing, and graph statistics. +""" + +from __future__ import annotations + +import dataclasses +import os +from typing import Any +from typing_extensions import Self + +import torch + +from .utils import print_once + + +@dataclasses.dataclass +class ProfileMetrics: + microseconds: float = 0.0 + operators: int = 0 + fusions: int = 0 + graphs: int = 0 + + def __iadd__(self, other: Self) -> Self: + self.microseconds += other.microseconds + self.operators += other.operators + self.fusions += other.fusions + return self + + def __add__(self, other: ProfileMetrics) -> ProfileMetrics: + assert isinstance(other, ProfileMetrics) + return ProfileMetrics( + self.microseconds + other.microseconds, + self.operators + other.operators, + self.fusions + other.fusions, + ) + + def __truediv__(self, other: Any) -> ProfileMetrics: + if isinstance(other, int): + other = ProfileMetrics(other, other, other) + return ProfileMetrics( + self.microseconds / max(1, other.microseconds), + # pyrefly: ignore [bad-argument-type] + self.operators / max(1, other.operators), + # pyrefly: ignore [bad-argument-type] + self.fusions / max(1, other.fusions), + ) + + def __str__(self) -> str: + return f"{self.operators:4.0%} ops {self.microseconds:4.0%} time" + + def tocsv(self) -> list[float]: + return [self.operators, self.microseconds] + + +class ProfileResult: + def __init__( + self, captured: ProfileMetrics, total: ProfileMetrics, unique_graphs: int + ) -> None: + self.captured: ProfileMetrics = captured or ProfileMetrics() + self.total: ProfileMetrics = total or ProfileMetrics() + self.unique_graphs: int = unique_graphs + + def __iadd__(self, other: Self) -> Self: + self.captured += other.captured + self.total += other.total + self.unique_graphs += other.unique_graphs + return self + + def percent(self) -> ProfileMetrics: + return self.captured / self.total + + def __str__(self) -> str: + return ( + f"{self.unique_graphs:2} graphs {self.captured.graphs:2} graph calls " + f"{self.captured.operators:4}/{self.total.operators:4} = " + + str(self.percent()) + ) + + def tocsv(self) -> list[Any]: + return [ + self.unique_graphs, + self.captured.graphs, + self.captured.operators, + self.total.operators, + ] + self.percent().tocsv() + + +def should_print_missing() -> bool: + return os.environ.get("TORCHDYNAMO_PRINT_MISSING") == "1" + + +def print_missing(stack: list[str]) -> None: + if any("/torch/autograd/profiler.py" in x for x in stack): + return + stack = [ + x for x in stack if ("> ".join(stack[-3:])) + + +class Profiler: + unique_graphs: int = 0 + + def __init__(self) -> None: + self.prof = torch.profiler.profile( + activities=[torch.profiler.ProfilerActivity.CPU], + with_stack=should_print_missing(), + ) + + def results(self) -> ProfileResult: + captured_regions = 0 + captured_ops = 0 + captured_microseconds = 0 + total_ops = 0 + total_microseconds = 0 + + last_op_end_time = -1 + captured_region_end_time = -1 + events = sorted(self.prof.events(), key=lambda x: x.time_range.start) + for e in events: + if e.name == "TORCHDYNAMO": + captured_region_end_time = e.time_range.end + captured_regions += 1 + # ignore `handle = torch.zeros(1)` in record_function.__init__() + total_ops -= 1 + elif e.time_range.start >= last_op_end_time: + last_op_end_time = e.time_range.end + if e.time_range.end <= captured_region_end_time: + captured_ops += 1 + captured_microseconds += e.time_range.elapsed_us() + elif should_print_missing(): + print_missing(e.stack) + total_ops += 1 + total_microseconds += e.time_range.elapsed_us() + else: + pass # ops recursively called from other ops (ignored) + + unique_graphs = Profiler.unique_graphs + Profiler.unique_graphs = 0 + # we counted one extra op that is part of the profiler setup code + total_ops -= 1 + + return ProfileResult( + captured=ProfileMetrics( + microseconds=captured_microseconds, + operators=captured_ops, + fusions=captured_ops - captured_regions, + graphs=captured_regions, + ), + total=ProfileMetrics( + microseconds=total_microseconds, + operators=total_ops, + fusions=total_ops - 1, + ), + unique_graphs=unique_graphs, + ) + + +def fx_insert_profiling(gm: torch.fx.GraphModule, example_inputs: list[Any]) -> Any: + def _wrapped(*args: Any) -> Any: + with torch.profiler.record_function("TORCHDYNAMO"): + return gm.forward(*args) + + Profiler.unique_graphs += 1 + return _wrapped diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/replay_record.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/replay_record.py new file mode 100644 index 0000000000000000000000000000000000000000..d65f5ef4e30466193ba077ca1f5c0ab5815ccc20 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/replay_record.py @@ -0,0 +1,130 @@ +""" +Python execution state recording and replay functionality. + +This module provides mechanisms for capturing and replaying Python execution state: + +- ModuleRecord: Tracks module access patterns and attribute usage +- DummyModule: Lightweight module substitute for replay +- ExecutionRecord: Manages execution context including globals, locals and builtins +- ExecutionRecorder: Records variable states and module access during execution + +The module enables serialization and reproduction of Python execution environments, +particularly useful for debugging and testing frameworks that need to capture +and recreate specific program states. +""" + +import dataclasses +from dataclasses import field +from io import BufferedReader, BufferedWriter +from types import CellType, CodeType, ModuleType +from typing import Any, IO +from typing_extensions import Self + +from torch.utils._import_utils import import_dill + + +dill = import_dill() + + +@dataclasses.dataclass +class ModuleRecord: + module: ModuleType + accessed_attrs: dict[str, Any] = field(default_factory=dict) + + +@dataclasses.dataclass +class DummyModule: + name: str + is_torch: bool = False + value: object = None + + @property + def __name__(self) -> str: + return self.name + + +@dataclasses.dataclass +class ExecutionRecord: + code: CodeType + closure: tuple[CellType] + globals: dict[str, Any] = field(default_factory=dict) + locals: dict[str, Any] = field(default_factory=dict) + builtins: dict[str, Any] = field(default_factory=dict) + code_options: dict[str, Any] = field(default_factory=dict) + + def dump(self, f: IO[str] | BufferedWriter) -> None: + assert dill is not None, "replay_record requires `pip install dill`" + dill.dump(self, f) + + @classmethod + def load(cls, f: IO[bytes] | BufferedReader) -> Self: + assert dill is not None, "replay_record requires `pip install dill`" + return dill.load(f) + + +@dataclasses.dataclass +class ExecutionRecorder: + LOCAL_MOD_PREFIX = "___local_mod_" + + code: CodeType + closure: tuple[CellType] + globals: dict[str, Any] = field(default_factory=dict) + locals: dict[str, Any] = field(default_factory=dict) + builtins: dict[str, Any] = field(default_factory=dict) + code_options: dict[str, Any] = field(default_factory=dict) + name_to_modrec: dict[str, ModuleRecord] = field(default_factory=dict) + + def add_local_var(self, name: str, var: Any) -> None: + if isinstance(var, ModuleType): + self.locals[name] = self._add_mod(var) + else: + self.locals[name] = var + + def add_global_var(self, name: str, var: Any) -> None: + if isinstance(var, ModuleType): + self.globals[name] = self._add_mod(var) + else: + self.globals[name] = var + + def add_local_mod(self, name: str, mod: ModuleType) -> None: + assert isinstance(mod, ModuleType) + self.add_global_var(name, mod) + + def record_module_access(self, mod: ModuleType, name: str, val: Any) -> None: + if isinstance(val, ModuleType): + self.name_to_modrec[mod.__name__].accessed_attrs[name] = self._add_mod(val) + return + + if mod.__name__ in self.name_to_modrec: + self.name_to_modrec[mod.__name__].accessed_attrs[name] = val + + def get_record(self) -> ExecutionRecord: + return ExecutionRecord( + self.code, + self.closure, + ExecutionRecorder._resolve_modules(self.globals), + ExecutionRecorder._resolve_modules(self.locals), + self.builtins.copy(), + self.code_options.copy(), + ) + + def _add_mod(self, mod: ModuleType) -> ModuleRecord: + if mod.__name__ not in self.name_to_modrec: + self.name_to_modrec[mod.__name__] = ModuleRecord(mod) + + return self.name_to_modrec[mod.__name__] + + @classmethod + def _resolve_modules(cls, vars: dict[str, Any]) -> dict[str, Any]: + def resolve_module(var: Any) -> Any: + if not isinstance(var, ModuleRecord): + return var + + dummy_mod = DummyModule(var.module.__name__) + for attr_name, attr_value in var.accessed_attrs.items(): + attr_value = resolve_module(attr_value) + dummy_mod.__setattr__(attr_name, attr_value) + + return dummy_mod + + return {k: resolve_module(v) for k, v in vars.items()} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/repro/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/repro/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/repro/after_aot.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/repro/after_aot.py new file mode 100644 index 0000000000000000000000000000000000000000..ca1883194feac3c8b74ff28439d26541b6d3fb0f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/repro/after_aot.py @@ -0,0 +1,1543 @@ +""" +Utilities for reproducing and debugging issues in PyTorch's Dynamo AOT compilation. + +This module provides tools and infrastructure for: +1. Generating minimal reproducible test cases ("repros") from failing compilations +2. Analyzing accuracy issues between eager and compiled execution +3. Minifying large models/inputs to isolate problematic patterns +4. Debugging compiler errors and accuracy divergences + +The main components include: +- Repro generation: Creates standalone Python files that reproduce compiler issues +- Minification: Reduces large graphs to minimal failing examples +- Accuracy analysis: Compares compiled vs eager execution, with fp64 reference +- Debug tools: Dumps graph state, tracks intermediates, analyzes divergences + +This is primarily used by PyTorch developers and researchers to debug issues in +the Dynamo AOT compilation pipeline, particularly for the Inductor backend. +""" + +from __future__ import annotations + +import argparse +import ast +import copy +import functools +import io +import logging +import os +import shutil +import subprocess +import sys +import textwrap +import typing +import uuid +from importlib import import_module +from tempfile import TemporaryFile +from typing import Any, IO, TYPE_CHECKING +from typing_extensions import Unpack + +import sympy + + +try: + import triton.language as tl + from triton.runtime.autotuner import Autotuner, Heuristics + from triton.runtime.jit import JITFunction + + TritonConstexpr = tl.constexpr +except ImportError: + + class Autotuner: # type: ignore[no-redef] + pass + + class JITFunction: # type: ignore[no-redef] + pass + + class Heuristics: # type: ignore[no-redef] + pass + + class TritonConstexpr: # type: ignore[no-redef] + pass + + +import torch +import torch.fx as fx +import torch.nn as nn +from torch._dynamo.debug_utils import ( + _cuda_system_info_comment, + AccuracyError, + backend_accuracy_fails, + BuckTargetWriter, + cast_to_fp64, + extra_deps, + extra_imports, + generate_config_string, + generate_env_vars_string, + helper_for_dump_minify, + InputReader, + InputWriter, + MAX_CONSTANT_NUMEL_INLINE, + minifier_dir, + NNModuleToString, + NopInputReader, + same_two_models, +) +from torch._dynamo.utils import clone_inputs, counters, same +from torch._environment import is_fbcode +from torch._higher_order_ops.triton_kernel_wrap import kernel_side_table +from torch._inductor.cpp_builder import normalize_path_separator +from torch._library.fake_class_registry import FakeScriptObject +from torch._ops import OpOverload +from torch.fx.experimental.proxy_tensor import make_fx +from torch.fx.experimental.symbolic_shapes import ( + fx_placeholder_targets, + has_free_symbols, +) +from torch.hub import tqdm + +from .. import config + + +def _find_repeat_interleave_constraints( + gm: torch.fx.GraphModule, +) -> list[tuple[str, str]]: + """ + Find repeat_interleave operations with output_size constraints. + + Returns list of (repeats_placeholder_name, output_size_placeholder_name) pairs. + These represent constraints where sum(repeats) must equal output_size. + """ + constraints = [] + for node in gm.graph.nodes: + if ( + node.op != "call_function" + or "repeat_interleave" not in str(node.target) + or not node.args + ): + continue + + output_size_node = node.kwargs.get("output_size") + repeats_node = node.args[0] + + # Both must be FX nodes (not constants) and direct placeholders + if ( + isinstance(repeats_node, torch.fx.Node) + and isinstance(output_size_node, torch.fx.Node) + and repeats_node.op == "placeholder" + and output_size_node.op == "placeholder" + ): + constraints.append((str(repeats_node.target), str(output_size_node.target))) + + return constraints + + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + + from torch._inductor.compile_fx import _CompileFxCallable, _CompileFxKwargs + from torch._inductor.output_code import OutputCode + from torch._inductor.utils import InputType + + +log = logging.getLogger(__name__) + + +inductor_config = import_module("torch._inductor.config") + + +def _extract_distributed_info( + gm: torch.fx.GraphModule, +) -> dict[str, dict[str, int]]: + """ + Extract process group information from distributed ops in the graph. + + Returns a dict mapping group names to dicts with 'size' and 'rank' keys. + Example: {'tp': {'size': 4, 'rank': 0}, 'dp': {'size': 2, 'rank': 0}} + """ + from torch.distributed import GroupName + from torch.fx.operator_schemas import normalize_function + + group_info: dict[str, dict[str, int]] = {} + + for node in gm.graph.nodes: + if node.op != "call_function": + continue + if not isinstance(node.target, OpOverload): + continue + if node.target.namespace not in {"_c10d_functional", "c10d_functional"}: + continue + + opt_args_kwargs = normalize_function( + node.target, + args=node.args, + kwargs=node.kwargs, + normalize_to_only_use_kwargs=True, + ) + if opt_args_kwargs is None: + continue + _, kwargs = opt_args_kwargs + + group_name_ = kwargs.get("group_name") + if not isinstance(group_name_, str): + continue + group_name = typing.cast(GroupName, group_name_) + + if group_name in group_info: + continue + + from torch.distributed.distributed_c10d import ( + _get_group_size_by_name, + _resolve_process_group, + ) + + group_size = _get_group_size_by_name(group_name) + pg = _resolve_process_group(group_name) + rank = pg.rank() + group_info[group_name] = {"size": group_size, "rank": rank} + + return group_info + + +def setup_fake_process_groups( + group_info: dict[str, dict[str, int]], +) -> None: + """ + Set up fake process groups for repro execution. + + Args: + group_info: dict mapping group_name -> {'size': group_size, 'rank': rank} + """ + import torch.distributed as dist + from torch.testing._internal.distributed.fake_pg import FakeStore + + if not group_info: + return + + world_size = max(info["size"] for info in group_info.values()) + + global_rank = 0 + for info in group_info.values(): + if info["size"] == world_size: + global_rank = info["rank"] + break + + store = FakeStore() + dist.init_process_group( + backend="fake", + rank=global_rank, + world_size=world_size, + store=store, + ) + + default_pg = dist.distributed_c10d._get_default_group() + torch._C._distributed_c10d._unregister_all_process_groups() + + for group_name, info in group_info.items(): + group_size = info["size"] + if group_size == world_size: + # pyrefly: ignore[bad-argument-type] + torch._C._distributed_c10d._register_process_group(group_name, default_pg) + else: + ranks = list(range(group_size)) + new_pg = dist.new_group(ranks) + # pyrefly: ignore[bad-argument-type] + torch._C._distributed_c10d._register_process_group(group_name, new_pg) + + +def generate_standalone_repro( + gm: torch.fx.GraphModule, + args: Sequence[Any], + *, + save_path: str | None = None, +) -> str: + """ + Generate a self-contained repro script from an FX graph. + """ + buf = io.StringIO() + save_graph_repro(buf, gm, args, "inductor", save_dir=None) + repro = buf.getvalue() + + if save_path is not None: + with open(save_path, "w") as f: + f.write(repro) + log.info("Saved standalone repro to %s", save_path) + + return repro + + +use_buck = is_fbcode() + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # +# MAIN ENTRY POINT +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # + + +def wrap_compiler_debug( + unconfigured_compiler_fn: _CompileFxCallable, + compiler_name: str, +) -> _CompileFxCallable: + """ + Minifier for Fx Graph modules after Aot Autograd has finished. We wrap both + forward and backward call separately with the backend compiler_fn - like + inductor or nvfuser. Intercepting after Aot Autograd presents neat + abstraction, where all the params are lifted as graph inputs, making it easy + to save the graph as a string. + """ + + @functools.wraps(unconfigured_compiler_fn) + def debug_wrapper( + gm: torch.fx.GraphModule, + example_inputs: Sequence[InputType], + compile_region_name: str | None = None, + **kwargs: Unpack[_CompileFxKwargs], + ) -> OutputCode: + from torch._subclasses import FakeTensorMode + + compiler_fn = functools.partial( + unconfigured_compiler_fn, + compile_region_name=compile_region_name, + **kwargs, + ) + + from torch._functorch.aot_autograd import get_aot_graph_name + + graph_name = get_aot_graph_name() + + # TODO: why do we need to deepcopy the original graph? + orig_graph = copy.deepcopy(gm.graph) + assert config.repro_after in ("dynamo", "aot", None) + + try: + # Call the compiler_fn - which is either aot_autograd or inductor + # with fake inputs + inner_compiled_fn = compiler_fn(gm, example_inputs) + except Exception: + # TODO: Failures here are troublesome because no real inputs, + # need a different serialization strategy + if config.repro_after == "aot": + if config.repro_level == 1: + dump_compiler_graph_state( + fx.GraphModule(gm, orig_graph), + example_inputs, + compiler_name, + ) + elif config.repro_level == 2: + dump_to_minify( + fx.GraphModule(gm, orig_graph), + example_inputs, + compiler_name, + ) + log.error("CompilerError") + raise + + # We may run regular PyTorch compute that may trigger Dynamo, do NOT + # recursively attempt to accuracy minify in that case! + def deferred_for_real_inputs( + real_inputs: Sequence[InputType], **_kwargs: object + ) -> Any: + # This is a bit obscure: if we recursively try to accuracy minify + # the SAME function, this would trigger. But most of the time + # we should never hit this branch + assert not _kwargs + if config.repro_after != "aot": + assert not isinstance(inner_compiled_fn, str) + return inner_compiled_fn(real_inputs) + with config.patch(repro_after=None): + return inner_debug_fn(real_inputs) + + def inner_debug_fn(real_inputs: Sequence[InputType]) -> Any: + """ + Aot Autograd fw_compiler and bw_compiler can have fake tensors. So, + example_inputs can be fake tensors. We can call compiler_fn (which is + inductor or nvfuser) with fake tensors but the actually compiled_fn + should be called with real tensors. Therefore, the actual invocation + is deferred. + """ + # Copy the tensor attrs like shape, stride etc by converting to Fake Tensor + # because inductor clears the tensor list in its codegen. And example_inputs + # are available only for the first invocation. + fake_mode = FakeTensorMode() + copy_tensor_attrs = [ + fake_mode.from_tensor(x) if isinstance(x, torch.Tensor) else x + for x in real_inputs + ] + if config.repro_level == 3: + # Always dump the original module in case we have segfaults + dump_to_minify( + fx.GraphModule(gm, orig_graph), real_inputs, compiler_name + ) + + if config.repro_level == 4: + if compiler_name != "inductor": + raise NotImplementedError( + "Accuracy minification is supported for inductor only" + ) + failed = not same_two_models( + gm, + inner_compiled_fn, # type: ignore[arg-type] + real_inputs, + only_fwd=True, + ignore_non_fp=config.repro_ignore_non_fp, + ) + + if failed: + log.warning( + "Accuracy failed for the AOT Autograd graph %s", graph_name + ) + dump_compiler_graph_state( + fx.GraphModule(gm, orig_graph), + real_inputs, + f"{compiler_name}_accuracy", + ) + dump_to_minify( + fx.GraphModule(gm, orig_graph), + real_inputs, + f"{compiler_name}_accuracy", + ) + raise AccuracyError("Bad accuracy detected") + else: + # Call the compiled function with real inputs + return inner_compiled_fn(real_inputs) # type: ignore[operator] + else: + try: + # Call the compiled function with real inputs + out = inner_compiled_fn(real_inputs) # type: ignore[operator] + # sync cuda kernels to ensure IMA detection + for arg in example_inputs: + if isinstance(arg, torch.Tensor) and arg.is_cuda: + torch.cuda.synchronize() + break + return out + except Exception: + if config.repro_level == 1: + dump_compiler_graph_state( + fx.GraphModule(gm, orig_graph), + copy_tensor_attrs, + compiler_name, + ) + elif config.repro_level == 2: + dump_to_minify( + fx.GraphModule(gm, orig_graph), + copy_tensor_attrs, + compiler_name, + ) + raise + + if config.repro_after == "aot": + compiled_fn = deferred_for_real_inputs + compiled_fn._boxed_call = True # type: ignore[attr-defined] + return compiled_fn # type: ignore[return-value] + else: + return inner_compiled_fn + + return debug_wrapper + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # +# DUMP REPROS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # + + +def maybe_fbcode_instructions() -> str: + if is_fbcode(): + extra_deps_formatted = "\n".join([f' "{dep}",' for dep in extra_deps]) + if len(extra_deps_formatted) > 0: + extra_deps_formatted = "\n" + extra_deps_formatted + return f"""\ +\"\"\" +To run this script in fbcode: +- Create a directory (//scripts/{{your_unixname}}/repro) +- Put this file in scripts/{{your_unixname}}/repro/fx_graph_runnable.py +- Add a TARGETS file that looks like the following +- `buck2 run //scripts/{{your_unixname}}/repro:repro` + +NOTE: you may need additional deps to actually be able to run the script. +``` +# Contents of TARGETS file +load("@fbcode_macros//build_defs:python_binary.bzl", "python_binary") + +python_binary( + name = "repro", + main_src = "fx_graph_runnable.py", + deps = [ + "//caffe2:torch",{extra_deps_formatted} + ], +) +``` +\"\"\" +""" + else: + return "" + + +def generate_custom_triton_kernel(kernel: Any) -> str: + res = "" + if isinstance(kernel, Autotuner): + # pyrefly: ignore [missing-attribute] + if isinstance(kernel.fn, Heuristics): + res += "ERROR: Repro will not work as intended, " + res += "triton.runtime.autotuner.Heuristics is not currently supported\n" + return res + + config_strs = [] + # pyrefly: ignore [missing-attribute] + for kernel_config in kernel.configs: + config_strs.append(f"""triton.Config( + {str(kernel_config.kwargs)}, + num_warps={kernel_config.num_warps}, + num_stages={kernel_config.num_stages}, + )""") + + config_str = ",".join(config_strs) + res += textwrap.dedent(f""" + @triton.autotune( + configs=[ + {config_str} + ], + key=[] + ) + """).strip() + + # pyrefly: ignore [missing-attribute] + src_code = kernel.src if isinstance(kernel, JITFunction) else kernel.fn.src + res += "\n@triton.jit\n" + res += src_code + res += "\n" + + return res + + +def generate_compiler_repro_string( + gm: torch.fx.GraphModule, + args: Sequence[Any], + *, + stable_output: bool = False, + save_dir: str | None = None, + stable_hash: bool = False, + has_distributed_ops: bool = False, +) -> str: + if save_dir is not None: + save_dir = normalize_path_separator(save_dir) + # Add distributed imports if needed + distributed_imports = "" + if has_distributed_ops: + distributed_imports = textwrap.dedent( + """ +import torch.distributed as dist +from torch.testing._internal.distributed.fake_pg import FakeStore + """ + ).strip() + + triton_imports = "" + + if len(kernel_side_table.id_to_kernel) > 0: + triton_imports = textwrap.dedent( + """ +import triton +import triton.language as tl + """ + ).strip() + + model_str = textwrap.dedent( + f""" +{generate_env_vars_string(stable_output=stable_output)} +import torch +from torch import tensor, device +import torch.fx as fx +from torch._dynamo.testing import rand_strided +from math import inf +import torch._inductor.inductor_prims +{distributed_imports} +{triton_imports} + +{generate_config_string(stable_output=stable_output)} + +isolate_fails_code_str = None + +{extra_imports} + +{maybe_fbcode_instructions()} + """ + ) + model_str += textwrap.dedent( + """ +if "__compile_source__" in globals(): + import inspect as __after_aot_inspect + import linecache as __after_aot_linecache + __after_aot_filename = __after_aot_inspect.currentframe().f_code.co_filename + __after_aot_linecache.cache[__after_aot_filename] = ( + len(__compile_source__), + None, + __compile_source__.splitlines(True), + __after_aot_filename, + ) +""" + ) + if not stable_output: + model_str += f"# torch version: {torch.version.__version__}\n" + if hasattr(torch.version, "cuda"): + model_str += f"# torch cuda version: {torch.version.cuda}\n" + if hasattr(torch.version, "git_version"): + model_str += f"# torch git version: {torch.version.git_version}\n\n\n" + model_str += _cuda_system_info_comment() + + kernel_side_table_prefix = ( + "torch._higher_order_ops.triton_kernel_wrap.kernel_side_table" + ) + + def get_fn_name(kernel: Any) -> str: + fn: Any = kernel if isinstance(kernel, JITFunction) else kernel.fn + return fn.__name__.split(".")[-1] + + def write_kernel_dependencies( + kernel: Any, + written_constexpr_vars: set[str], + written_nested_kernels: set[str], + ) -> str: + """Write out global tl.constexpr vars and nested kernel dependencies.""" + result = "" + jit_fn = kernel if isinstance(kernel, JITFunction) else kernel.fn + if not getattr(jit_fn, "fn", None) or not getattr(jit_fn, "src", None): + return result + + fn_globals = getattr(jit_fn.fn, "__globals__", {}) + src = jit_fn.src + full_src = src if src.strip().startswith("def ") else "def " + src + + referenced_names: set[str] = set() + called_names: set[str] = set() + for node in ast.walk(ast.parse(full_src)): + if isinstance(node, ast.Name): + referenced_names.add(node.id) + if isinstance(node, ast.Call) and isinstance(node.func, ast.Name): + called_names.add(node.func.id) + + # Write out global tl.constexpr variables + for name in referenced_names: + if name in written_constexpr_vars: + continue + val = fn_globals.get(name) + + if isinstance(val, TritonConstexpr) and getattr(val, "value", None): + result += f"{name} = tl.constexpr({val.value})\n" + elif isinstance(val, (int, float, str, bool)): + result += f"{name} = {val!r}\n" + else: + continue + written_constexpr_vars.add(name) + + # Write out nested kernel dependencies + for name in called_names: + val = fn_globals.get(name) + if not isinstance(val, JITFunction) or val is jit_fn: + continue + nested_fn_name = get_fn_name(val) + if nested_fn_name in written_nested_kernels: + continue + # Mark as written before recursing to prevent cycles + written_nested_kernels.add(nested_fn_name) + result += write_kernel_dependencies( + val, written_constexpr_vars, written_nested_kernels + ) + result += generate_custom_triton_kernel(val) + + return result + + written_nested_kernels: set[str] = set() + written_constexpr_vars: set[str] = set() + + model_str += f"{kernel_side_table_prefix}.reset_table()\n" + + for idx in kernel_side_table.id_to_kernel: + kernel = kernel_side_table.get_kernel(idx) + + try: + model_str += write_kernel_dependencies( + kernel, written_constexpr_vars, written_nested_kernels + ) + fn_name = get_fn_name(kernel) + + unique_name = f"{fn_name}_{idx}" + + kernel_code = generate_custom_triton_kernel(kernel) + kernel_code = kernel_code.replace( + f"def {fn_name}(", f"def {unique_name}(", 1 + ) + model_str += kernel_code + model_str += f"{kernel_side_table_prefix}.add_kernel({unique_name})\n" + except AttributeError as e: + model_str += "ERROR: Repro will not work as intended, " + model_str += f"User defined triton kernel exception: {e}\n" + + if len(kernel_side_table.constant_args) > 0: + model_str += f"{kernel_side_table_prefix}.constant_args={kernel_side_table.constant_args}\n" + + model_str += NNModuleToString.convert(gm) + + writer = InputWriter(save_dir, stable_hash=stable_hash) + # pyrefly: ignore [implicit-any] + used_syms = {} + + # Extract from graph placeholders and their corresponding arguments + placeholder_targets = fx_placeholder_targets(gm) + for placeholder, arg in zip(placeholder_targets, args): + if isinstance(arg, (int, torch.SymInt)): + writer.symint(placeholder, arg) + elif isinstance(arg, torch.Tensor): + # TODO: improve these names with FQN + writer.tensor(placeholder, arg) + elif arg is None: + writer.const(placeholder) + elif isinstance(arg, FakeScriptObject): + writer.opaque(placeholder, arg.script_class_name) + elif isinstance(arg, torch._C.Generator): + writer.generator(placeholder, arg) + else: + writer.unsupported(placeholder, arg) + + # Extract symbolic variables from the same arguments + + if ( + isinstance(arg, torch.SymInt) + # By checking sympy.Symbol, we are excluding any symbolic expressions. + # TODO: we may need to solve expressions to extract symbol definitions. + and isinstance(arg.node.expr, sympy.Symbol) + and arg.node.hint is not None + ): + used_syms[str(arg.node)] = arg.node.hint + elif isinstance(arg, torch.Tensor): + # Extract symbolic variables from tensor shapes and strides + for dim in arg.shape: + if ( + isinstance(dim, torch.SymInt) + and isinstance(dim.node.expr, sympy.Symbol) + and dim.node.hint is not None + ): + used_syms[str(dim.node)] = dim.node.hint + for stride in arg.stride(): + if ( + isinstance(stride, torch.SymInt) + and isinstance(stride.node.expr, sympy.Symbol) + and stride.node.hint is not None + ): + used_syms[str(stride.node)] = stride.node.hint + # Extract symbols from storage nbytes (can be a symbolic expression) + storage = arg.untyped_storage() + nbytes = storage.nbytes() + if isinstance(nbytes, torch.SymInt): + expr = nbytes.node.expr + shape_env = nbytes.node.shape_env + for sym in expr.free_symbols: + sym_name = str(sym) + if sym_name not in used_syms and shape_env is not None: + hint = shape_env.backed_var_to_val.get(sym) + if hint is not None: + used_syms[sym_name] = int(hint) + # Add symbolic variable definitions to the top of the generated code + if used_syms: + hint_lines = "\n".join( + f"{name} = {hint}" for name, hint in sorted(used_syms.items()) + ) + model_str = f"{hint_lines}\n\n{model_str}" + + # Add fixup code for repeat_interleave constraints + # When inputs are regenerated randomly, sum(repeats) != output_size + # This fixup adjusts the repeats tensor to satisfy the constraint + constraints = _find_repeat_interleave_constraints(gm) + if constraints: + placeholder_to_idx = {name: idx for idx, name in enumerate(placeholder_targets)} + for repeats_name, output_size_name in constraints: + repeats_idx = placeholder_to_idx.get(repeats_name) + output_size_idx = placeholder_to_idx.get(output_size_name) + if repeats_idx is not None and output_size_idx is not None: + # Guard with hasattr since NopInputReader doesn't have args + writer._lines.append( + "# Fixup: ensure sum(repeats) == output_size for repeat_interleave" + ) + writer._lines.append("if hasattr(reader, 'args'):") + writer._lines.append(f" _repeats = reader.args[{repeats_idx}]") + writer._lines.append( + f" _output_size = reader.args[{output_size_idx}]" + ) + writer._lines.append( + " if isinstance(_repeats, torch.Tensor) and _repeats.dtype == torch.int64:" + ) + writer._lines.append(" _n = _repeats.numel()") + writer._lines.append(" _repeats.fill_(_output_size // _n)") + writer._lines.append(" _repeats[:_output_size % _n] += 1") + + load_args_lines = writer.lines() + load_args_code = "\n".join(load_args_lines) + model_str += load_args_code + "\n" + + model_str += "mod = Repro()\n" + return model_str + + +def save_graph_repro( + fd: IO[Any], + gm: torch.fx.GraphModule, + args: Sequence[Any], + compiler_name: str, + *, + stable_output: bool = False, + save_dir: str | None = None, + command: str = "run", + accuracy: str | bool | None = None, + tracing_mode: str | None = None, + check_str: str | None = None, + stable_hash: bool = False, +) -> None: + if any( + isinstance(arg, torch.fx.experimental._backward_state.BackwardState) + for arg in args + ): + fd.write( + "Repro is not generated due to existence of BackwardState in graph input" + ) + return + + if save_dir is not None: + save_dir = normalize_path_separator(save_dir) + + # Extract distributed info from the graph + distributed_info = _extract_distributed_info(gm) + has_distributed_ops = len(distributed_info) > 0 + + fd.write( + generate_compiler_repro_string( + gm, + args, + stable_output=stable_output, + save_dir=save_dir, + stable_hash=stable_hash, + has_distributed_ops=has_distributed_ops, + ) + ) + if accuracy is None: + accuracy = "_accuracy" in compiler_name + if tracing_mode is None: + tracing_mode = "real" + if any( + has_free_symbols(a) for a in args if not isinstance(a, FakeScriptObject) + ): + tracing_mode = "symbolic" + fd.write("if __name__ == '__main__':\n") + fd.write(" from torch._dynamo.repro.after_aot import run_repro\n") + + # Add distributed initialization before run_repro if needed + if has_distributed_ops: + fd.write( + " from torch._dynamo.repro.after_aot import setup_fake_process_groups\n" + ) + fd.write(f" setup_fake_process_groups({distributed_info!r})\n") + + fd.write( + f" with torch.no_grad():\n" + f" run_repro(mod, load_args, accuracy={accuracy!r}, command={command!r}, " + f"save_dir={save_dir!r}, tracing_mode={tracing_mode!r}, check_str={check_str!r})\n" + f" # To run it separately, do \n" + f" # mod, args = run_repro(mod, load_args, accuracy={accuracy!r}, command='get_args', " + f"save_dir={save_dir!r}, tracing_mode={tracing_mode!r}, check_str={check_str!r})\n" + f" # mod(*args)" + ) + + # Add distributed cleanup after run_repro + if has_distributed_ops: + fd.write("\n dist.destroy_process_group()\n") + + +def dump_compiler_graph_state( + gm: torch.fx.GraphModule, + args: Sequence[Any], + compiler_name: str, + *, + accuracy: str | bool | None = None, +) -> None: + subdir = os.path.join(minifier_dir(), "checkpoints") + if not os.path.exists(subdir): + os.makedirs(subdir, exist_ok=True) + file_name = os.path.join(subdir, f"{len(gm.graph.nodes)}.py") + log.warning( + "Writing checkpoint with %s nodes to %s", len(gm.graph.nodes), file_name + ) + with open(file_name, "w") as fd: + save_graph_repro( + fd, gm, args, compiler_name, save_dir=subdir, accuracy=accuracy + ) + curdir = os.getcwd() + repro_path = os.path.join(curdir, "repro.py") + try: + shutil.copyfile(file_name, repro_path) + log.warning("Copying repro file for convenience to %s", repro_path) + if use_buck: + BuckTargetWriter(file_name).write() + except OSError: + log.warning("No write permissions for %s", repro_path) + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # +# DUMP MINIFIER +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # + + +def dump_to_minify( + gm: torch.fx.GraphModule, args: Sequence[Any], compiler_name: str +) -> None: + out = io.StringIO() + # TODO: factor this out + subdir = os.path.join(minifier_dir(), "checkpoints") + if not os.path.exists(subdir): + os.makedirs(subdir, exist_ok=True) + save_graph_repro(out, gm, args, compiler_name, save_dir=subdir, command="minify") + return helper_for_dump_minify(out.getvalue()) + + +def isolate_fails( + fx_g: torch.fx.GraphModule, + args: Sequence[Any], + compiler_name: str, + env: dict[str, Any] | None = None, + save_dir: str | None = None, + accuracy: bool | str | None = None, + tracing_mode: str | None = None, + check_str: str | None = None, +) -> bool: + if env is None: + env = {} + subdir = os.path.join(os.getcwd(), "isolate") + if not os.path.exists(subdir): + os.makedirs(subdir, exist_ok=True) + file_name = os.path.join(subdir, f"{str(uuid.uuid4())[:5]}.py") + with open(file_name, "w") as fd: + save_graph_repro( + fd, + fx_g, + args, + compiler_name, + save_dir=save_dir, + command="minifier-query", + accuracy=accuracy, + tracing_mode=tracing_mode, + check_str=check_str, + ) + # with open(file_name, "r") as fd: + # print(fd.read()) + new_env = os.environ.copy() + new_env = {**new_env, **env} + if use_buck: + cmd = BuckTargetWriter(file_name).write(print_msg=False) + else: + cmd = [sys.executable, file_name] + with ( + TemporaryFile() as stdout, + TemporaryFile() as stderr, + subprocess.Popen( + cmd, + cwd=subdir, + stdout=stdout, + stderr=stderr, + env=new_env, + ) as p, + ): + p.wait() + + stdout.seek(0) + stderr.seek(0) + print( + textwrap.indent(stdout.read().decode("utf-8"), prefix=">> "), + file=sys.stdout, + ) + print( + textwrap.indent(stderr.read().decode("utf-8"), prefix=">> "), + file=sys.stderr, + ) + # print(f"Isolated test failed - {file_name}") + return p.returncode != 0 + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # +# MINIFIER TOOLS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # + + +def inductor_fails( + fx_g: torch.fx.GraphModule, args: Sequence[Any], check_str: str | None = None +) -> bool: + has_cuda = False + for arg in args: + if isinstance(arg, torch.Tensor) and arg.is_cuda: + has_cuda = True + break + + def sync() -> None: + if has_cuda: + # Ensures that segfaults are surfaced + torch.cuda.synchronize() + + from torch._inductor.compile_fx import compile_fx_inner + + try: + result = fx_g(*args) + assert isinstance(result, (tuple, list)) + assert not any(isinstance(x, (tuple, list)) for x in result) + except Exception: + return False + + sync() + + try: + compile_mod = compile_fx_inner(fx_g, args) + assert not isinstance(compile_mod, str) + compile_mod(args) + sync() + except Exception as e: + if check_str is not None and check_str not in repr(e): + return False + print(repr(e)) + return True + return False + + +def inductor_accuracy_fails( + fx_g: torch.fx.GraphModule, + args: Sequence[Any], + check_str: str | None = None, + *, + require_fp64: bool = False, + ignore_non_fp: bool = False, +) -> bool: + from torch._inductor.compile_fx import compile_fx_inner + + return backend_aot_accuracy_fails( + fx_g, + args, # type: ignore[arg-type] + compile_fx_inner, # type: ignore[arg-type] + require_fp64=require_fp64, + ignore_non_fp=ignore_non_fp, + ) + + +backend_aot_accuracy_fails = functools.partial(backend_accuracy_fails, only_fwd=True) + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # +# REPRO MAIN +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # + + +def repro_common( + options: Any, mod: nn.Module, load_args: Any +) -> tuple[torch.fx.GraphModule, list[Any]]: + # Invariant for graphs we generate with the repro script + assert not any(mod.named_parameters()) + for n, b in mod.named_buffers(): + if b.numel() > MAX_CONSTANT_NUMEL_INLINE: + log.warning( + "Constant %s was not serialized, generated random data instead. " + "If you think this is affecting you, please comment on " + "https://github.com/pytorch/pytorch/issues/100468", + n, + ) + + if not hasattr(load_args, "_version"): + log.warning( + "load_args does not have a _version attribute, please file a bug to PyTorch " + "and describe how you generate this repro script" + ) + else: + if load_args._version > 0: + log.warning( + "load_args is version %s, but this version of PyTorch only supports " + "version 0. We will try to run it anyway but there may be an incompatibility; " + "if so, try upgrading your version of PyTorch.", + load_args._version, + ) + + nop_reader = NopInputReader() + load_args(nop_reader) + + with tqdm(desc="Loading inputs", total=nop_reader.total) as pbar: + input_reader = InputReader(save_dir=options.save_dir, pbar=pbar) + load_args(input_reader) + args = input_reader.args + + # Turn mod into a GraphModule the slow way + # TODO: speed this up + mod = make_fx(mod, tracing_mode=options.tracing_mode)(*args) + + # pyrefly: ignore [bad-assignment] + torch._inductor.config.generate_intermediate_hooks = True + + return mod, args + + +ACCURACY_FAILS: dict[str, Callable[[torch.fx.GraphModule, Any], bool]] = { + "": inductor_fails, + # This might look inverted but it's not. strict_accuracy means "we will + # minify any time we see anything that diverges", whereas accuracy is more + # conservative, and will only minify if there is a meaningful fp64 + # divergence + "accuracy": functools.partial( + inductor_accuracy_fails, require_fp64=True, ignore_non_fp=True + ), + "strict_accuracy": inductor_accuracy_fails, +} + + +def repro_minifier_query(options: Any, mod: nn.Module, load_args: Any) -> None: + mod, args = repro_common(options, mod, load_args) + fail_fn = functools.partial( + ACCURACY_FAILS[options.accuracy], + check_str=options.check_str, # type: ignore[call-arg] + ) + if fail_fn(mod, args): + sys.exit(1) + else: + sys.exit(0) + + +def repro_minify(options: Any, mod: nn.Module, load_args: Any) -> None: + from functorch.compile import minifier + + mod, args = repro_common(options, mod, load_args) + compiler_name = "inductor_accuracy" if options.accuracy != "" else "inductor" + + favored_device = 1 if torch.cuda.device_count() >= 2 else 0 + env_variables = {"CUDA_VISIBLE_DEVICES": str(favored_device)} + + module_fails: Any + if options.isolate: + module_fails = functools.partial( + isolate_fails, + env=env_variables, + compiler_name=compiler_name, + save_dir=options.save_dir, + accuracy=options.accuracy, + tracing_mode=options.tracing_mode, + ) + else: + module_fails = ACCURACY_FAILS[options.accuracy] + + minifier( + mod, + args, + module_fails=functools.partial(module_fails, check_str=options.check_str), + dump_state=functools.partial( + dump_compiler_graph_state, compiler_name=compiler_name + ), + save_dir=options.save_dir, + offload_to_disk=options.offload_to_disk, + skip_offload=options.skip_saving_eager_intermediates, + skip_sanity=options.skip_sanity, + max_granularity=options.max_granularity, + ) + + +def repro_analyze(options: Any, mod: nn.Module, load_args: Any) -> None: + from torch._inductor.compile_fx import compile_fx_inner + from torch._inductor.hooks import intermediate_hook + + mod, args = repro_common(options, mod, load_args) + + # TODO: The logic for cloning inputs/models here is intentionally + # modeled off of run_fwd_maybe_bwd, but arguably it is better not to + # clone inputs (as you are doubling your effective GPU memory usage). + # It is certainly faster though! It probably makes sense to let the + # user specify the offload strategy. + + with tqdm(desc="Compiling"): + compiled = compile_fx_inner(mod, args) + total = counters["inductor"]["intermediate_hooks"] + + known_names = set() + + def save_hook(name: str, val: Any) -> None: + known_names.add(name) + if not options.skip_saving_inductor_intermediates: + writer.write_tensor(os.path.join("inductor", name), val) + pbar.update(1) # type: ignore[has-type] + + writer = torch.utils._content_store.ContentStoreWriter( + options.save_dir, stable_hash=options.stable_hash + ) + reader = torch.utils._content_store.ContentStoreReader(options.save_dir) + + new_args = clone_inputs(args) + with ( + intermediate_hook(save_hook), + tqdm(desc="Saving inductor intermediates", total=total) as pbar, + ): + assert not isinstance(compiled, str) + compiled(new_args) # type: ignore[arg-type] + assert not new_args + + def compare_tuples(tuple1: tuple[Any], tuple2: tuple[Any]) -> str | None: + diff_indices = [i for i in range(len(tuple1)) if tuple1[i] != tuple2[i]] + diff_values = [(tuple1[i], tuple2[i]) for i in diff_indices] + + if not diff_values: + return None + else: + return " and ".join(f"{a} != {b}" for a, b in diff_values) + + def check_hook(name: str, val: Any) -> None: + meta = writer.compute_tensor_metadata(val) + meta2 = reader.read_tensor_metadata(os.path.join("inductor", name)) + reason = compare_tuples(meta, meta2) + if reason is not None: + pbar.write(f"NONDETERMINISTIC INDUCTOR at {name} ({reason})") + pbar.update(1) + + if not options.skip_check_deterministic: + new_args = clone_inputs(args) + with ( + intermediate_hook(check_hook), + tqdm(desc="Checking inductor determinism", total=total) as pbar, + ): + compiled(new_args) # type: ignore[arg-type] + assert not new_args + + class WriterInterp(fx.Interpreter): + def __init__(self, mod: torch.nn.Module, subdir: str) -> None: + super().__init__(mod) + self.subdir = subdir + + def run_node(self, n: torch.fx.Node) -> Any: + r = super().run_node(n) + name = n.name + if name in known_names: + pbar.update(1) + writer.write_tensor(os.path.join(self.subdir, name), r) + return r + + # NB: the module cast doesn't actually do anything, since there are no + # parameters/buffers on the module + if not options.skip_saving_float64_intermediates: + new_mod, new_args = cast_to_fp64(copy.deepcopy(mod), clone_inputs(args)) # type: ignore[arg-type] + with tqdm(desc="Saving float64 intermediates", total=total) as pbar: + WriterInterp(new_mod, "float64").boxed_run(new_args) + assert not new_args + + class ExactReaderInterp(fx.Interpreter): + def run_node(self, n: torch.fx.Node) -> Any: + r = super().run_node(n) + name = n.name + if name in known_names: + meta = writer.compute_tensor_metadata(r) + meta2 = reader.read_tensor_metadata(os.path.join("float64", name)) + reason = compare_tuples(meta, meta2) + if reason is not None: + pbar.write(f"NONDETERMINISTIC FLOAT64 at {name} ({reason})") + pbar.update(1) + return r + + # TODO: check eager determinism + + if not options.skip_check_deterministic: + new_mod, new_args = cast_to_fp64(copy.deepcopy(mod), clone_inputs(args)) # type: ignore[arg-type] + with tqdm(desc="Checking float64 determinism", total=total) as pbar: + ExactReaderInterp(new_mod).boxed_run(new_args) + assert not new_args + + # Now that we've saved everything, interp through the eager graph + # and do comparisons + class ReaderInterp(fx.Interpreter): + def run_node(self, n: torch.fx.Node) -> Any: + r = super().run_node(n) + name = n.name + if name in known_names: + inductor = reader.read_tensor(os.path.join("inductor", name)) + float64 = reader.read_tensor(os.path.join("float64", name)) + logged = False + + def log_error(msg: str, *args: Any) -> None: + nonlocal logged + logged = True + pbar.write(f"DIVERGED at {name}: {msg % args}") + + if not same( + r, + inductor, + float64, + tol=torch._dynamo.config.repro_tolerance, + equal_nan=True, + log_error=log_error, + ): + assert logged + pbar.update(1) + return r + + with tqdm(desc="Checking divergence", total=total) as pbar: + ReaderInterp(mod).boxed_run(args) + assert not args + + +def repro_get_args( + options: Any, mod: nn.Module, load_args: Any +) -> tuple[torch.fx.GraphModule, list[Any]]: + mod, args = repro_common(options, mod, load_args) + return mod, args + + +def repro_run(options: Any, mod: nn.Module, load_args: Any) -> None: + from torch._inductor.compile_fx import compile_fx_inner + + mod, args = repro_common(options, mod, load_args) + + from torch.cuda import synchronize + + compiled = compile_fx_inner(mod, args) + assert not isinstance(compiled, str) + + if options.accuracy != "": + # We don't really respect --accuracy vs --strict-accuracy here, it + # seems counterintuitive + if not same_two_models( + mod, + compiled, # type: ignore[arg-type] + args, + only_fwd=True, + ignore_non_fp=config.repro_ignore_non_fp, + ): + raise AccuracyError("Bad accuracy detected") + else: + need_sync = False + + for arg in args: + if isinstance(arg, torch.Tensor) and arg.is_cuda: + need_sync = True + break + + compiled(list(args)) + + if need_sync: + synchronize() # ensure segfaults are surfaced + + +# TODO: lazily load the inputs or something, rather than cloning them +def run_repro( + mod: nn.Module, + load_args: Any, + *, + command: str = "run", + accuracy: bool | str = "", + save_dir: str | None = None, + tracing_mode: str | None = None, + patch_code: str | None = None, + check_str: str | None = None, + **kwargs: Any, +) -> Any: + for k in kwargs: + log.warning( + "Unrecognized kwarg %s; perhaps this repro was made on a newer version of PyTorch", + k, + ) + + if accuracy is True: + accuracy = "accuracy" + elif accuracy is False: + accuracy = "" + + if patch_code is not None: + log.warning( + "patch_code no longer works on this version of PyTorch, silently ignoring" + ) + + parser = argparse.ArgumentParser( + description=f"""\ +An after_aot repro script, typically triggering a bug in PyTorch Inductor. +When run with no arguments, this script defaults to running '{command}'. +Extra flags may be available; to find out more, try '{command} --help'. +There are also alternate subcommands available, see below. + +default settings on this script: + {accuracy=} + {tracing_mode=} + {save_dir=} + {check_str=} +""", + formatter_class=argparse.RawTextHelpFormatter, + ) + + def common_flags(parser: argparse.ArgumentParser) -> None: + accuracy_group = parser.add_mutually_exclusive_group() + accuracy_group.add_argument( + "--no-accuracy", + dest="accuracy", + action="store_const", + const="", + default=accuracy, + help="do not test accuracy, just run the module and see if it errors", + ) + accuracy_group.add_argument( + "--accuracy", + action="store_const", + const="accuracy", + default=accuracy, + help="""\ +test if the RMSE between the compiled module and the fp64 reference is greater +than eager and the fp64 reference. This is usually more reliable than the +standard allclose test, as we expect numeric differences from compiling, often +improving accuracy over eager. RMSE test allows for compiled module to +diverge greatly from eager, as long as this divergence moves it closer to the +'true' mathematical value of the network. Caveats: (1) double precision can +still suffer from rounding error, so it is not a perfect reference (see for +example 'Herbie: Automatically Improving Floating Point Accuracy') for +approaches that detect the necessary working precision and compute it in +arbitrary precision floating point; unfortunately, this is not practical for +tensor computation; (2) if there are not enough samples in the output being +compared, we may get unlucky and have an unlucky greater RMSE than eager; this +could be overcome by applying a more rigorous statistical test at some +p-value, which we leave for future work. +""", + ) + accuracy_group.add_argument( + "--strict-accuracy", + dest="accuracy", + action="store_const", + const="strict_accuracy", + default=accuracy, + help="""\ +by default, when doing accuracy minification we will reject reductions which +change the divergence from a floating point divergence to a integral/boolean +divergence. This is because some operations like ReLU involve temporarily +sharp boundaries that smooth out again afterwards; without requiring +divergence on floating point, the minifier will often fixate on divergent +boolean tensor even though this is not the true source of the divergence. +However, rejecting these reductions makes it more difficult for the minifier +to make process. Using this option will let the minifier progress for ALL +divergences--you just might not end up with a useful repro in the end.""", + ) + + parser.add_argument( + "--save-dir", + type=str, + default=save_dir, + metavar="DIR", + help="directory where saved inputs live", + ) + parser.add_argument( + "--no-save-dir", + dest="save_dir", + action="store_const", + const=None, + help="don't use any directory for saved inputs", + ) + parser.add_argument( + "--tracing-mode", + type=str, + metavar="{real,fake,symbolic}", + default=tracing_mode, + help="how to trace the repro module into a GraphModule with metadata", + ) + + subparsers = parser.add_subparsers( + dest="command", metavar="{run,minify,analyze}", required=True + ) + + parser_run = subparsers.add_parser( + "run", + help="just run the repro", + ) + common_flags(parser_run) + + parser_minify = subparsers.add_parser( + "minify", help="run the minifier on the repro" + ) + common_flags(parser_minify) + parser_get_args = subparsers.add_parser("get_args", help="get the args") + common_flags(parser_get_args) + parser_minify_isolate = parser_minify.add_mutually_exclusive_group() + parser_minify_isolate.add_argument( + "--isolate", + action="store_true", + default=True, + help="run in separate processes to avoid interference (default)", + ) + parser_minify_isolate.add_argument( + "--no-isolate", + dest="isolate", + action="store_false", + help="speed up by running all compilation in same process", + ) + parser_minify.add_argument( + "--skip-saving-eager-intermediates", + action="store_true", + help="skip saving eager intermediates on --minify", + ) + # TODO: make this an option for --analyze too + parser_minify.add_argument( + "--offload-to-disk", + action="store_true", + help="during minification, offload delta debugging intermediates to disk. Use if you're OOMing", + ) + parser_minify.add_argument( + "--skip-sanity", + action="store_true", + help="skip sanity check at beginning of minification on original graph", + ) + parser_minify.add_argument( + "--max-granularity", + type=int, + default=None, + help="start at this granularity and work down; must be power of 2", + ) + parser_minify.add_argument( + "--check-str", + type=str, + default=check_str, + help="require minified program to fail with error containing this string", + ) + + parser_analyze = subparsers.add_parser( + "analyze", help="run the accuracy analyzer on the repro" + ) + common_flags(parser_analyze) + parser_analyze.add_argument( + "--skip-saving-inductor-intermediates", + action="store_true", + help="skip saving inductor intermediates on --analyze", + ) + parser_analyze.add_argument( + "--skip-saving-float64-intermediates", + action="store_true", + help="skip saving float64 intermediates", + ) + parser_analyze.add_argument( + "--skip-check-deterministic", + action="store_true", + help="skip checking that the network is deterministic", + ) + parser_analyze.add_argument( + "--stable-hash", + action="store_true", + help="use SHA-1 checksum instead of fast (but possibly unsound) hash", + ) + + # Run the repro in the context of minification, inverting exit code meaning + parser_minifier_query = subparsers.add_parser( + "minifier-query", + ) + common_flags(parser_minifier_query) + parser_minifier_query.add_argument( + "--check-str", + type=str, + default=check_str, + help="require minified program to fail with error containing this string", + ) + + args = None + if len(sys.argv) <= 1: + args = [command, *sys.argv[1:]] + + options = parser.parse_args(args) + COMMAND_FNS = { + "minify": repro_minify, + "analyze": repro_analyze, + "minifier-query": repro_minifier_query, + "run": repro_run, + "get_args": repro_get_args, + } + return COMMAND_FNS[options.command](options, mod, load_args) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/repro/after_dynamo.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/repro/after_dynamo.py new file mode 100644 index 0000000000000000000000000000000000000000..95c2ac5ac2aa6a33688efb806b6e69b7840f4064 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/repro/after_dynamo.py @@ -0,0 +1,637 @@ +""" +Utilities for reproducing and debugging issues in Dynamo after graph capture. + +This file provides tools and infrastructure for debugging problems that occur +after Dynamo has captured the graph but before/during backend compilation. +Key components include: + +- Minification tools to reduce large graphs to minimal failing examples +- Accuracy testing to validate compiled graph outputs match eager mode +- Repro generation to create standalone reproduction scripts +- Debug backends for capturing and analyzing failures +- Utilities for saving/loading graph states and inputs + +The tools here focus specifically on the post-graph-capture stage, making them +useful for debugging backend compilation issues, AOTAutograd problems, and +accuracy discrepancies between compiled and eager execution. +""" + +import argparse +import copy +import functools +import logging +import os +import shutil +import sys +import textwrap +from collections.abc import Callable, Sequence +from importlib import import_module +from typing import Any + +import torch +import torch.fx as fx +from torch._dynamo.debug_utils import ( + AccuracyError, + backend_accuracy_fails, + BUCK_CMD_PREFIX, + BuckTargetWriter, + extra_imports, + generate_config_string, + generate_env_vars_string, + helper_for_dump_minify, + InputReader, + InputWriter, + minifier_dir, + NNModuleToString, + NopInputReader, + run_fwd_maybe_bwd, + same_two_models, +) +from torch.fx.experimental.symbolic_shapes import fx_placeholder_targets +from torch.hub import tqdm + +from .. import config +from ..backends.registry import CompilerFn, lookup_backend, register_debug_backend +from ..debug_utils import clone_inputs_retaining_gradness + + +log = logging.getLogger(__name__) + + +inductor_config = import_module("torch._inductor.config") +use_buck = inductor_config.is_fbcode() + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # +# MAIN ENTRY POINT +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # + + +def _accuracy_fails( + gm: torch.fx.GraphModule, + example_inputs: Sequence[Any], + compiler_fn: Callable[[torch.fx.GraphModule, list[Any]], torch.fx.GraphModule], +) -> bool: + return backend_accuracy_fails( + gm, + example_inputs, + compiler_fn, + only_fwd=config.repro_forward_only, + ignore_non_fp=config.repro_ignore_non_fp, + ) + + +class WrapBackendDebug: + def __init__( + self, unconfigured_compiler_fn: CompilerFn, compiler_name: str | None + ) -> None: + functools.wraps(unconfigured_compiler_fn)(self) + self._torchdynamo_orig_backend = unconfigured_compiler_fn + self._compiler_name = compiler_name + if hasattr(unconfigured_compiler_fn, "__name__"): + self.__name__ = unconfigured_compiler_fn.__name__ + if hasattr(unconfigured_compiler_fn, "compiler_name"): + self.__name__ = unconfigured_compiler_fn.compiler_name # type: ignore[attr-defined] + if hasattr(unconfigured_compiler_fn, "get_compiler_config"): + self.get_compiler_config = unconfigured_compiler_fn.get_compiler_config # type: ignore[attr-defined] + + def __call__( + self, gm: torch.fx.GraphModule, example_inputs: list[Any], **kwargs: Any + ) -> torch.fx.GraphModule: + compiler_fn = functools.partial(self._torchdynamo_orig_backend, **kwargs) + assert config.repro_after in ("dynamo", "aot", None) + + if config.repro_after == "dynamo": + + def add_paths(exc: Exception) -> None: + exc.minifier_path = os.path.join(minifier_dir(), "minifier_launcher.py") # type: ignore[attr-defined] + if use_buck: + exc.buck_command = " ".join( # type: ignore[attr-defined] + BUCK_CMD_PREFIX + + [BuckTargetWriter(exc.minifier_path).cmd_line_path] # type: ignore[attr-defined] + ) + + if config.repro_level == 3: + dump_to_minify_after_dynamo(gm, example_inputs, self._compiler_name) + + # Check for either accuracy (level 4) or other type of failures. + if config.repro_level == 4: + # Check Accuracy + compiled_gm = compiler_fn(copy.deepcopy(gm), example_inputs) + if _accuracy_fails(gm, example_inputs, compiler_fn): # type: ignore[arg-type] + log.warning( + "Accuracy failed for the TorchDynamo produced graph. Creating script to minify the error." + ) + dump_to_minify_after_dynamo( + fx.GraphModule(gm, copy.deepcopy(gm.graph)), + example_inputs, + self._compiler_name, + ) + exc = AccuracyError("Bad accuracy detected.") + add_paths(exc) + raise exc + else: + try: + compiled_gm = compiler_fn(copy.deepcopy(gm), example_inputs) + run_fwd_maybe_bwd(compiled_gm, example_inputs) # type: ignore[arg-type] + except Exception as exc: + log.warning( + "Compiled Fx GraphModule failed. Creating script to minify the error." + ) + if config.repro_level == 1: + dump_state_fn = functools.partial( + dump_backend_state, compiler_name=self._compiler_name + ) + dump_state_fn( + fx.GraphModule(gm, copy.deepcopy(gm.graph)), example_inputs + ) + elif config.repro_level == 2: + dump_to_minify_after_dynamo( + fx.GraphModule(gm, copy.deepcopy(gm.graph)), + example_inputs, + self._compiler_name, + ) + add_paths(exc) + raise + else: + compiled_gm = compiler_fn(gm, example_inputs) + + return compiled_gm # type: ignore[return-value] + + +def wrap_backend_debug( + unconfigured_compiler_fn: CompilerFn, compiler_name: str | None +) -> WrapBackendDebug: + """ + A minifier decorator that wraps the TorchDynamo produced Fx graph modules. + As opposed to wrap_compiler_debug, this wrapper intercepts at the + TorchDynamo produced Fx Graph Module. This makes it backend-agnostic to some + level, e.g., it is useful for minifying issues related to Aot Autograd + tracing. If an error is found, we minify and save the minified repro in + repro.tar.gz. + """ + return WrapBackendDebug(unconfigured_compiler_fn, compiler_name) + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # +# REPRO DUMPERS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # + + +def generate_dynamo_fx_repro_string( + gm: torch.fx.GraphModule, + args: Sequence[Any], + compiler_name: str | None, + check_accuracy: bool = False, + *, + stable_output: bool = False, + save_dir: str | None = None, + command: str = "run", +) -> str: + """ + Generate a repro string for backend-agnostic minified version. + """ + + model_str = NNModuleToString.convert(gm) + + # TODO: Figure out why torch.compile'd hash isn't work on this codepath + writer = InputWriter(save_dir, stable_hash=True) + for placeholder, arg in zip(fx_placeholder_targets(gm), args): + if isinstance(arg, (int, torch.SymInt)): + writer.symint(placeholder, arg) + elif isinstance(arg, torch.Tensor): + # TODO: improve these names with FQN + writer.tensor(placeholder, arg) + else: + raise TypeError(f"arg is neither SymInt/int nor torch.Tensor, {arg}") + load_args = "\n".join(writer.lines()) + + return textwrap.dedent( + f""" +{generate_env_vars_string(stable_output=stable_output)} +from math import inf +import torch +from torch import tensor, device +import torch.fx as fx +import torch._dynamo +from torch._dynamo.testing import rand_strided +from torch._dynamo.debug_utils import run_fwd_maybe_bwd + +{generate_config_string(stable_output=stable_output)} + +{extra_imports} + +{model_str} +mod = Repro() + +{load_args} + +if __name__ == '__main__': + from torch._dynamo.repro.after_dynamo import run_repro + run_repro(mod, load_args, accuracy={check_accuracy!r}, command={command!r}, + save_dir={save_dir!r}, autocast={torch.is_autocast_enabled()!r}, backend={compiler_name!r}) +""" + ) + + +def dump_backend_repro_as_file( + gm: torch.fx.GraphModule, + args: Sequence[Any], + compiler_name: str | None, + check_accuracy: bool = False, +) -> None: + """ + Saves the repro to a repro.py file + """ + curdir = os.getcwd() + subdir = os.path.join(os.getcwd(), "checkpoints") + if not os.path.exists(subdir): + os.makedirs(subdir, exist_ok=True) + file_name = os.path.join(subdir, f"minified_{len(gm.graph.nodes)}_nodes.py") + log.warning( + "Writing checkpoint with %s nodes to %s", len(gm.graph.nodes), file_name + ) + + with open(file_name, "w") as fd: + fd.write( + generate_dynamo_fx_repro_string( + gm, args, compiler_name, check_accuracy, save_dir=subdir + ) + ) + latest_repro = os.path.join(curdir, "repro.py") + log.warning("Copying %s to %s for convenience", file_name, latest_repro) + + if use_buck: + BuckTargetWriter(latest_repro).write() + + shutil.copyfile(file_name, latest_repro) + + +def dump_backend_state( + gm: torch.fx.GraphModule, + args: Sequence[Any], + compiler_name: str | None, + check_accuracy: bool = False, +) -> None: + """ + Dumps the dynamo graph to repro the issue. + 1) It tries to convert Fx GraphModule to a string. If we can, it writes to a + repro.py file. + 2) If we can't convert Fx GraphModule to a string, we use to_folder to save + the module and save a tar file. + """ + assert NNModuleToString.can_convert_to_string(gm) + return dump_backend_repro_as_file(gm, args, compiler_name, check_accuracy) + # return dump_backend_repro_as_tarfile(gm, args, compiler_name) + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # +# MINIFIER DUMPER +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # + + +def dump_to_minify_after_dynamo( + gm: torch.fx.GraphModule, args: Sequence[Any], compiler_name: str | None +) -> None: + # TODO: factor this out + subdir = os.path.join(minifier_dir(), "checkpoints") + if not os.path.exists(subdir): + os.makedirs(subdir, exist_ok=True) + helper_for_dump_minify( + generate_dynamo_fx_repro_string( + gm, + args, + compiler_name, + check_accuracy=config.repro_level == 4, + save_dir=subdir, + command="minify", + ) + ) + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # +# MINIFIER BACKENDS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # + + +@register_debug_backend # type: ignore[arg-type] +def dynamo_minifier_backend( + gm: fx.GraphModule, example_inputs: Sequence[Any], compiler_name: str | None +) -> fx.GraphModule: + from functorch.compile import minifier + + compiler_fn = lookup_backend(compiler_name) # type: ignore[arg-type] + + # TODO: It's inconsistent to pass SymInt inputs but REAL tensors. + # We should pass ints and look at the GraphModule placeholders + # to resolve them to SymInt (if necessary) + example_inputs = [ + i.node.hint if isinstance(i, torch.SymInt) else i for i in example_inputs + ] + + try: + compiled_gm = compiler_fn(gm, example_inputs) + run_fwd_maybe_bwd(compiled_gm, example_inputs) # type: ignore[arg-type] + raise ValueError("No issue was detected") + except Exception as exc: + orig_failure = str(exc) + log.warning( + "Compiled Fx GraphModule failed. Creating script to minify the error." + ) + dump_state_fn = functools.partial( + dump_backend_state, compiler_name=compiler_name + ) + dump_state_fn(fx.GraphModule(gm, copy.deepcopy(gm.graph)), example_inputs) + fails_fn = functools.partial( + backend_fails, + compiler_fn=compiler_fn, + orig_failure=orig_failure, + ) + minifier( + gm, + example_inputs, + module_fails=fails_fn, + dump_state=dump_state_fn, + ) + return gm + + +@register_debug_backend # type: ignore[arg-type] +def dynamo_accuracy_minifier_backend( + gm: fx.GraphModule, example_inputs: Sequence[Any], compiler_name: str | None +) -> fx.GraphModule: + from functorch.compile import minifier + + compiler_fn = lookup_backend(compiler_name) # type: ignore[arg-type] + + # Set the eval mode to remove randomness. + gm.eval() + + # Check Accuracy + if _accuracy_fails(gm, example_inputs, compiler_fn): # type: ignore[arg-type] + log.warning("Accuracy failed for the TorchDynamo produced graph") + dump_state_fn = functools.partial( + dump_backend_state, compiler_name=compiler_name, check_accuracy=True + ) + fails_fn = functools.partial( + _accuracy_fails, + compiler_fn=compiler_fn, # type: ignore[arg-type] + ) + dump_state_fn(fx.GraphModule(gm, copy.deepcopy(gm.graph)), example_inputs) + minifier( + gm, + example_inputs, + module_fails=fails_fn, + dump_state=dump_state_fn, + ) + else: + log.error("Input graph does not fail accuracy testing") + return gm + + +def backend_fails( + gm: fx.GraphModule, + example_inputs: Sequence[Any], + compiler_fn: CompilerFn, + orig_failure: Sequence[Any], +) -> bool: + """ + Minifier uses this function to identify if the minified graph module fails + with the same error. + + One caveat is that minifier can potentially go into a wrong direction when + the resulting graph module fails for a different reason. To avoid this, we + save the string for the original exception and check similarity between new + and old exception. They can be somewhat different in some cases, when the + exception string depends on the failing node information. So, we have a + loose similarity metric to guide the minifier path. + """ + from difflib import SequenceMatcher + + try: + # Run the original gm to check eager validity + run_fwd_maybe_bwd(gm, clone_inputs_retaining_gradness(example_inputs)) + compiled_gm = compiler_fn(gm, example_inputs) # type: ignore[arg-type] + run_fwd_maybe_bwd(compiled_gm, clone_inputs_retaining_gradness(example_inputs)) # type: ignore[arg-type] + except Exception as e: + new_failure = str(e) + if SequenceMatcher(None, orig_failure, new_failure).ratio() > 0.5: + return True + return False + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # +# REPRO MAIN +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # + + +def run_load_args(options: Any, mod: torch.nn.Module, load_args: Any) -> list[Any]: + if not hasattr(load_args, "_version"): + log.warning( + "load_args does not have a _version attribute, please file a bug to PyTorch " + "and describe how you generate this repro script" + ) + else: + if load_args._version > 0: + log.warning( + "load_args is version %s, but this version of PyTorch only supports " + "version 0. We will try to run it anyway but there may be an incompatibility; " + "if so, try upgrading your version of PyTorch.", + load_args._version, + ) + + nop_reader = NopInputReader() + load_args(nop_reader) + + with tqdm(desc="Loading inputs", total=nop_reader.total) as pbar: + input_reader = InputReader(save_dir=options.save_dir, pbar=pbar) + load_args(input_reader) + args = input_reader.args + + return args + + +def repro_minify(options: Any, mod: torch.nn.Module, load_args: Any) -> None: + args = run_load_args(options, mod, load_args) + + # Setup debug minifier compiler + if not options.accuracy: + compiler_fn = lookup_backend("dynamo_minifier_backend") + else: + compiler_fn = lookup_backend("dynamo_accuracy_minifier_backend") + + if options.backend is None: + raise RuntimeError( + "Compiler name is None - this likely means that a custom compiler " + "was called by torchdynamo. Please remove this error, import your " + "custom compiler function, and replace the backend=None " + "line in run_repro to backend=" + ) + + dynamo_minifier_backend = functools.partial( + compiler_fn, + compiler_name=options.backend, # type: ignore[call-arg] + ) + opt_mod = torch._dynamo.optimize(dynamo_minifier_backend)(mod) + + with torch.amp.autocast("cuda", enabled=options.autocast): + opt_mod(*args) + + +def repro_run(options: Any, mod: torch.nn.Module, load_args: Any) -> None: + opt_mod = torch._dynamo.optimize(options.backend)(mod) + + if options.accuracy != "": + mod.eval() + opt_mod.eval() # type: ignore[union-attr] + + with torch.amp.autocast("cuda", enabled=options.autocast): + # TODO: disable clone + args = run_load_args(options, mod, load_args) + assert same_two_models(mod, mod, args), "Eager itself failed" # type: ignore[arg-type] + if not same_two_models( + mod, # type: ignore[arg-type] + opt_mod, # type: ignore[arg-type] + args, + only_fwd=config.repro_forward_only, + ignore_non_fp=config.repro_ignore_non_fp, + ): + raise AccuracyError("Dynamo failed") + else: + with torch.amp.autocast("cuda", enabled=options.autocast): + args = run_load_args(options, mod, load_args) + run_fwd_maybe_bwd(mod, args, only_fwd=options.only_fwd, disable_clone=True) # type: ignore[arg-type] + del args + + args = run_load_args(options, mod, load_args) + run_fwd_maybe_bwd( + opt_mod, # type: ignore[arg-type] + args, + only_fwd=options.only_fwd, + disable_clone=True, # type: ignore[arg-type] + ) + + +def run_repro( + mod: torch.nn.Module, + load_args: Any, + *, + command: str = "run", + accuracy: bool | str = "", + save_dir: str | None = None, + autocast: bool = False, + backend: str = "inductor", + **kwargs: Any, +) -> None: + for k in kwargs: + log.warning( + "Unrecognized kwarg %s; perhaps this repro was made on a newer version of PyTorch", + k, + ) + + if accuracy is True: + accuracy = "accuracy" + elif accuracy is False: + accuracy = "" + + parser = argparse.ArgumentParser( + description=f"""\ +An after_dynamo repro script, typically triggering a bug in Dynamo or +AOTAutograd. When run with no arguments, this script defaults to running +'{command}'. Extra flags may be available; to find out more, try '{command} +--help'. There are also alternate subcommands available, see below. + +default settings on this script: + {accuracy=} + {save_dir=} +""", + formatter_class=argparse.RawTextHelpFormatter, + ) + + def common_flags(parser: argparse.ArgumentParser) -> None: + accuracy_group = parser.add_mutually_exclusive_group() + accuracy_group.add_argument( + "--no-accuracy", + dest="accuracy", + action="store_const", + const="", + default=accuracy, + help="do not test accuracy, just run the module and see if it errors", + ) + accuracy_group.add_argument( + "--accuracy", + action="store_const", + const="accuracy", + default=accuracy, + help="test accuracy", + ) + parser.add_argument( + "--save-dir", + type=str, + default=save_dir, + metavar="DIR", + help="directory where saved inputs live", + ) + parser.add_argument( + "--no-save-dir", + dest="save_dir", + action="store_const", + const=None, + help="don't use any directory for saved inputs", + ) + parser.add_argument( + "--no-isolate", + dest="isolate", + action="store_false", + default=False, + help="no isolate (doesn't do anything for after_dynamo)", + ) + parser.add_argument( + "--autocast", + default=autocast, + action="store_true", + help="use torch.cuda.amp.autocast", + ) + parser.add_argument( + "--no-autocast", + dest="autocast", + action="store_false", + help="don't use torch.cuda.amp.autocast", + ) + parser.add_argument( + "--backend", + type=str, + default=backend, + metavar="BACKEND", + help="torch.compile backend to use", + ) + + subparsers = parser.add_subparsers( + dest="command", metavar="{run,minify}", required=True + ) + + parser_run = subparsers.add_parser( + "run", + help="just run the repro", + ) + common_flags(parser_run) + parser_run.add_argument( + "--only-fwd", + action="store_true", + help="don't run backwards compilation for testing", + ) + + parser_minify = subparsers.add_parser( + "minify", help="run the minifier on the repro" + ) + common_flags(parser_minify) + + args = None + if len(sys.argv) <= 1: + args = [command, *sys.argv[1:]] + + options = parser.parse_args(args) + COMMAND_FNS = { + "minify": repro_minify, + "run": repro_run, + } + COMMAND_FNS[options.command](options, mod, load_args) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/repro/aoti.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/repro/aoti.py new file mode 100644 index 0000000000000000000000000000000000000000..156055509e3ad38de9dfd9adae70cc82ab50708b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/repro/aoti.py @@ -0,0 +1,662 @@ +""" +Utilities for debugging and reproducing issues in Ahead of Time with Inductor (AOTI) compilation. + +This file provides tools and utilities for: +- Generating minimal reproducible test cases (minification) +- Handling exported programs and graph modules +- Creating debug repros for AOTI compilation issues +- Supporting both accuracy testing and error reproduction +- Managing configuration and environment for repro cases + +The main components include: +- Minification tools to reduce test cases while preserving errors +- Repro generation utilities for exported programs +- Error handling specific to AOTI compilation +- Command-line interface for running and managing repros +""" + +import argparse +import functools +import io +import logging +import os +import re +import shutil +import sys +import textwrap +from collections.abc import Sequence +from importlib import import_module +from typing import Any, IO + +import torch +from torch._dynamo.debug_utils import ( + _cuda_system_info_comment, + BuckTargetWriter, + extra_imports, + generate_config_string, + generate_env_vars_string, + helper_for_dump_minify, + InputReader, + minifier_dir, + NNModuleToString, + NopInputReader, +) +from torch.export import ExportedProgram +from torch.hub import tqdm + + +log = logging.getLogger(__name__) + + +inductor_config = import_module("torch._inductor.config") +use_buck = inductor_config.is_fbcode() + + +class AOTIMinifierError(Exception): + def __init__(self, original_exception: str | Exception) -> None: + additional_message = "This error is caused by a bug in the AOTI minifier, please report a bug to PyTorch" + full_message = f"{additional_message}: {str(original_exception)}" + super().__init__(full_message) + self.original_exception = original_exception + + +def dump_to_minify( + exported_program: ExportedProgram, + compiler_name: str, + command: str = "minify", + options: dict[str, Any] | None = None, +) -> None: + """ + If command is "minify": + Dump exported_program to `debug_dir/minifier/minifier_launcher.py`, with minify command. + If command is "run": + Dump exported_program to `cwd/repro.py`, with run command. + """ + assert command in ["minify", "run"] + + subdir = os.path.join(minifier_dir(), "checkpoints") + if not os.path.exists(subdir): + os.makedirs(subdir, exist_ok=True) + + if command == "minify": + out = io.StringIO() + save_graph_repro_ep( + out, + compiler_name, + exported_program=exported_program, + save_dir=subdir, + command="minify", + config_patches=options, + ) + return helper_for_dump_minify(out.getvalue()) + else: + curdir = os.getcwd() + file_name = os.path.join(curdir, "repro.py") + try: + with open(file_name, "w") as fd: + save_graph_repro_ep( + fd, + compiler_name, + exported_program=exported_program, + config_patches=options, + save_dir=subdir, + command="run", + module_in_comment=True, + ) + log.warning("Writing repro file to %s", file_name) + if use_buck: + BuckTargetWriter(file_name).write() + except OSError: + log.warning("No write permissions for %s", file_name) + + +def get_module_string(gm: torch.fx.GraphModule) -> str: + def _convert_to_comment(s_: str) -> str: + s = s_.split("\n") + if len(s) == 1: + return "# " + s_ + first = s.pop(0) + for i in range(len(s)): + line = s[i] + if line.strip() != "": + s[i] = "# " + line + else: + s[i] = "" + s = "\n".join(s) + s = first + "\n" + s + return s + + module_string = NNModuleToString.convert(gm) + return _convert_to_comment(module_string) + + +def save_graph_repro_ep( + fd: IO[Any], + compiler_name: str, + *, + exported_program: ExportedProgram | None = None, + gm: torch.nn.Module | None = None, + args: tuple[Any] | None = None, + config_patches: dict[str, str] | None = None, + stable_output: bool = False, + save_dir: str | None = None, + command: str = "run", + accuracy: str | bool | None = None, + check_str: str | None = None, + module_in_comment: bool = False, + strict: bool = False, +) -> None: + # Save graph for reproducing the error. + # Either exported_program or gm will be saved, depending on which one is defined. + # Only one of exported_program and gm should be defined. + + if exported_program is None and gm is None: + raise AOTIMinifierError("One of exported_program and gm must be defined") + if exported_program is not None and gm is not None: + raise AOTIMinifierError("Only one of exported_program and gm can be defined") + if gm is not None and args is None: + raise AOTIMinifierError("If gm is defined, args should also be defined") + + if exported_program is None: + assert gm is not None + assert args is not None + exported_program = torch.export.export(gm, args, strict=strict) + elif gm is None: + gm = exported_program.module(check_guards=False) + + # save a graph preview using gm + module_string = get_module_string(gm) # type: ignore[arg-type] + fd.write(module_string) + + # save a graph repro using exported_program + fd.write( + generate_compiler_repro_exported_program( + exported_program, + options=config_patches, + stable_output=stable_output, + save_dir=save_dir, + ) + ) + if accuracy is None: + accuracy = "_accuracy" in compiler_name + fd.write("if __name__ == '__main__':\n") + fd.write(" from torch._dynamo.repro.aoti import run_repro\n") + fd.write( + f" with torch.no_grad():\n" + f" run_repro(exported_program, config_patches=config_patches, accuracy={accuracy!r}, command={command!r}, " + f"save_dir={save_dir!r}, check_str={check_str!r})\n" + ) + + +def dump_compiler_graph_state( + gm: torch.fx.GraphModule, + args: Sequence[Any], + compiler_name: str, + *, + config_patches: dict[str, str] | None = None, + accuracy: str | bool | None = None, + strict: bool = False, +) -> None: + subdir = os.path.join(minifier_dir(), "checkpoints") + if not os.path.exists(subdir): + os.makedirs(subdir, exist_ok=True) + file_name = os.path.join(subdir, f"{len(gm.graph.nodes)}.py") + log.warning( + "Writing checkpoint with %s nodes to %s", len(gm.graph.nodes), file_name + ) + with open(file_name, "w") as fd: + save_graph_repro_ep( + fd, + compiler_name, + gm=gm, + args=tuple(args), + config_patches=config_patches, + save_dir=subdir, + accuracy=accuracy, + module_in_comment=True, + strict=strict, + ) + curdir = os.getcwd() + repro_path = os.path.join(curdir, "repro.py") + try: + shutil.copyfile(file_name, repro_path) + log.warning("Copying repro file for convenience to %s", repro_path) + if use_buck: + BuckTargetWriter(file_name).write() + except OSError: + log.warning("No write permissions for %s", repro_path) + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # +# DUMP REPROS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # + + +def generate_compiler_repro_exported_program( + exported_program: ExportedProgram, + *, + options: dict[str, str] | None = None, + stable_output: bool = False, + save_dir: str | None = None, +) -> str: + model_str = textwrap.dedent( + f""" +{generate_env_vars_string(stable_output=stable_output)} +import torch +import torch._inductor.inductor_prims + +{generate_config_string(stable_output=stable_output)} + +isolate_fails_code_str = None + +{extra_imports} + + """ + ) + if not stable_output: + model_str += f"# torch version: {torch.version.__version__}\n" + if hasattr(torch.version, "cuda"): + model_str += f"# torch cuda version: {torch.version.cuda}\n" + if hasattr(torch.version, "git_version"): + model_str += f"# torch git version: {torch.version.git_version}\n\n\n" + model_str += _cuda_system_info_comment() + if save_dir: + ep_path = os.path.join(save_dir, "exported_program.pt2") + else: + ep_path = "exported_program.pt2" + torch.export.save(exported_program, ep_path) + + model_str += f"exported_program = torch.export.load('{ep_path}')\n" + model_str += "# print(exported_program.graph)\n" + model_str += f"config_patches={options}\n" + return model_str + + +def repro_load_args(load_args: Any, save_dir: str | None) -> tuple[Any]: + if not hasattr(load_args, "_version"): + log.warning( + "load_args does not have a _version attribute, please file a bug to PyTorch " + "and describe how you generate this repro script" + ) + else: + if load_args._version > 0: + log.warning( + "load_args is version %s, but this version of PyTorch only supports " + "version 0. We will try to run it anyway but there may be an incompatibility; " + "if so, try upgrading your version of PyTorch.", + load_args._version, + ) + + nop_reader = NopInputReader() + load_args(nop_reader) + + with tqdm(desc="Loading inputs", total=nop_reader.total) as pbar: + input_reader = InputReader(save_dir=save_dir, pbar=pbar) + load_args(input_reader) + args = input_reader.args + + return tuple(args) + + +def repro_common( + options: Any, exported_program: ExportedProgram +) -> tuple[torch.fx.GraphModule, Any, Any]: + # pyrefly: ignore [bad-assignment] + torch._inductor.config.generate_intermediate_hooks = True + mod = exported_program.module(check_guards=False) + args, kwargs = exported_program.example_inputs + return mod, args, kwargs # type: ignore[return-value] + + +def repro_get_args( + options: Any, + exported_program: ExportedProgram, + config_patches: dict[str, Any] | None, +) -> tuple[torch.fx.GraphModule, Any, Any]: + mod, args, kwargs = repro_common(options, exported_program) + return mod, args, kwargs + + +def repro_run( + options: Any, + exported_program: ExportedProgram, + config_patches: dict[str, Any] | None, +) -> None: + from torch._inductor import _aoti_compile_and_package_inner + + gm, args, kwargs = repro_common(options, exported_program) + + from torch.cuda import synchronize + + _aoti_compile_and_package_inner( + gm, + args, + kwargs, + load_and_run=True, + check_accuracy=options.accuracy, + inductor_configs=config_patches, + ) + + need_sync = False + + for arg in args: + if isinstance(arg, torch.Tensor) and arg.is_cuda: + need_sync = True + break + + if need_sync: + synchronize() # ensure segfaults are surfaced + + +def export_for_aoti_minifier( + gm: torch.nn.Module, + tuple_inputs: tuple[Any], + strict: bool = False, + skip_export_error: bool = True, +) -> torch.nn.Module | None: + # Some graphs cannot be used for AOTI/export (illegal graphs), these should be + # considered as graphs that don't fail in the minifier, so the minifier keeps searching. + # In these case, we return None. Otherwise, we return the exported graph module. + # This won't affect the minifier result because the minifier is only responsible for catching + # errors in AOTI, not export. + # + # Please add to this list of illegal graphs if you change the implementation here. + # - graph output is not allowed by export + # + # If skip_export_error=True, then the errors in export will not be raised, and the minifier + # will keep exploring and ignore this graph. + from torch._dynamo.exc import UserError, UserErrorType + + try: + ep = torch.export.export(gm, tuple_inputs, strict=strict) + gm = ep.module(check_guards=False) + return gm + except Exception as e: + if skip_export_error: + return None + if isinstance(e, UserError) and e.error_type == UserErrorType.INVALID_OUTPUT: + # graph output is not allowed by export when strict=True + return None + if isinstance(e, RuntimeError): + # graph output is not allowed by export when strict=False + pattern = r"Found .* in output, which is not a known type\." + if re.search(pattern, str(e)) is not None: + return None + raise AOTIMinifierError(e) from e + # we should never reach here + # pyrefly: ignore [unreachable] + return None + + +def repro_minify( + options: Any, + exported_program: ExportedProgram, + config_patches: dict[str, Any] | None, +) -> None: + from functorch.compile import minifier + from torch._inductor import _aoti_compile_and_package_inner + from torch._inductor.compile_fx import _aoti_flatten_inputs + + mod, args, kwargs = repro_common(options, exported_program) + + # update serialized_in_spec and serialized_out_spec + flat_example_inputs, inductor_configs = _aoti_flatten_inputs( + mod, args, kwargs, options=config_patches + ) + compiler_name = "aot_inductor" + assert options.minifier_export_mode in ["dynamo", "python"] + strict = options.minifier_export_mode == "dynamo" + skip_export_error = options.skip_export_error + + from torch.cuda import synchronize + + need_sync = False + + for arg in args: + if isinstance(arg, torch.Tensor) and arg.is_cuda: + need_sync = True + break + + def module_fails( + gm: torch.fx.GraphModule, + flat_example_inputs: list[Any], + check_str: str | None = None, + ) -> bool: + # Need to export first so the in_spec and out_spec are populated + tuple_inputs = tuple(flat_example_inputs) + # pyrefly: ignore [bad-assignment] + gm = export_for_aoti_minifier( + gm, tuple_inputs, strict=strict, skip_export_error=skip_export_error + ) + + # Some graphs cannot be used for AOTI/export (illegal graphs), these should be + # considered as graphs that don't fail in the minifier, so the minifier keeps searching. + if gm is None: + return False + + assert isinstance(gm, torch.fx.GraphModule) + + try: + _aoti_compile_and_package_inner( + gm, + tuple_inputs, + load_and_run=True, + check_accuracy=options.accuracy, + inductor_configs=inductor_configs, + ) + if need_sync: + synchronize() # ensure segfaults are surfaced + return False + except Exception as e: + if check_str is not None and check_str not in repr(e): + return False + return True + + minifier( + mod, + flat_example_inputs, + module_fails=functools.partial(module_fails, check_str=options.check_str), + dump_state=functools.partial( + dump_compiler_graph_state, + compiler_name=compiler_name, + config_patches=config_patches, + accuracy=options.accuracy, + strict=strict, + ), + save_dir=options.save_dir, + offload_to_disk=options.offload_to_disk, + skip_offload=options.skip_saving_eager_intermediates, + skip_sanity=options.skip_sanity, + max_granularity=options.max_granularity, + ) + + +def run_repro( + exported_program: ExportedProgram, + *, + config_patches: dict[str, str] | None = None, + command: str = "run", + accuracy: bool | str = "", + save_dir: str | None = None, + tracing_mode: str | None = None, + check_str: str | None = None, + minifier_export_mode: str = "python", + skip_export_error: bool = True, + **more_kwargs: Any, +) -> Any: + for k in more_kwargs: + log.warning( + "Unrecognized kwarg %s; perhaps this repro was made on a newer version of PyTorch", + k, + ) + + if accuracy is True: + accuracy = "accuracy" + elif accuracy is False: + accuracy = "" + + parser = argparse.ArgumentParser( + description=f"""\ +An AOTI repro script, typically triggering a bug in PyTorch AOTInductor. +When run with no arguments, this script defaults to running '{command}'. +Extra flags may be available; to find out more, try '{command} --help'. +There are also alternate subcommands available, see below. + +default settings on this script: + {accuracy=} + {tracing_mode=} + {save_dir=} + {check_str=} +""", + formatter_class=argparse.RawTextHelpFormatter, + ) + + def common_flags(parser: argparse.ArgumentParser) -> None: + accuracy_group = parser.add_mutually_exclusive_group() + accuracy_group.add_argument( + "--no-accuracy", + dest="accuracy", + action="store_const", + const="", + default=accuracy, + help="do not test accuracy, just run the module and see if it errors", + ) + accuracy_group.add_argument( + "--accuracy", + action="store_const", + const="accuracy", + default=accuracy, + help="""\ +test if the RMSE between the compiled module and the fp64 reference is greater +than eager and the fp64 reference. This is usually more reliable than the +standard allclose test, as we expect numeric differences from compiling, often +improving accuracy over eager. RMSE test allows for compiled module to +diverge greatly from eager, as long as this divergence moves it closer to the +'true' mathematical value of the network. Caveats: (1) double precision can +still suffer from rounding error, so it is not a perfect reference (see for +example 'Herbie: Automatically Improving Floating Point Accuracy') for +approaches that detect the necessary working precision and compute it in +arbitrary precision floating point; unfortunately, this is not practical for +tensor computation; (2) if there are not enough samples in the output being +compared, we may get unlucky and have an unlucky greater RMSE than eager; this +could be overcome by applying a more rigorous statistical test at some +p-value, which we leave for future work. +""", + ) + accuracy_group.add_argument( + "--strict-accuracy", + dest="accuracy", + action="store_const", + const="strict_accuracy", + default=accuracy, + help="""\ +by default, when doing accuracy minification we will reject reductions which +change the divergence from a floating point divergence to a integral/boolean +divergence. This is because some operations like ReLU involve temporarily +sharp boundaries that smooth out again afterwards; without requiring +divergence on floating point, the minifier will often fixate on divergent +boolean tensor even though this is not the true source of the divergence. +However, rejecting these reductions makes it more difficult for the minifier +to make process. Using this option will let the minifier progress for ALL +divergences--you just might not end up with a useful repro in the end.""", + ) + + parser.add_argument( + "--save-dir", + type=str, + default=save_dir, + metavar="DIR", + help="directory where saved inputs live", + ) + parser.add_argument( + "--no-save-dir", + dest="save_dir", + action="store_const", + const=None, + help="don't use any directory for saved inputs", + ) + + subparsers = parser.add_subparsers( + dest="command", metavar="{run,minify}", required=True + ) + + parser_run = subparsers.add_parser( + "run", + help="just run the repro", + ) + common_flags(parser_run) + + parser_minify = subparsers.add_parser( + "minify", help="run the minifier on the repro" + ) + common_flags(parser_minify) + parser_get_args = subparsers.add_parser("get_args", help="get the args") + common_flags(parser_get_args) + parser_minify.add_argument( + "--skip-saving-eager-intermediates", + action="store_true", + help="skip saving eager intermediates on --minify", + ) + parser_minify.add_argument( + "--offload-to-disk", + action="store_true", + help="during minification, offload delta debugging intermediates to disk. Use if you're OOMing", + ) + parser_minify.add_argument( + "--skip-sanity", + action="store_true", + help="skip sanity check at beginning of minification on original graph", + ) + parser_minify.add_argument( + "--max-granularity", + type=int, + default=None, + help="start at this granularity and work down; must be power of 2", + ) + parser_minify.add_argument( + "--check-str", + type=str, + default=check_str, + help="require minified program to fail with error containing this string", + ) + parser_minify.add_argument( + "--minifier-export-mode", + type=str, + default=minifier_export_mode, + help=( + "The export mode used in minifier, either dynamo or python." + "`dynamo` corresponds to strict=True, and `python` corresponds to strict=False." + ), + ) + parser_minify.add_argument( + "--skip-export-error", + type=bool, + default=skip_export_error, + help="Skip intermediate graphs that cannot be exported.", + ) + + # Run the repro in the context of minification, inverting exit code meaning + parser_minifier_query = subparsers.add_parser( + "minifier-query", + ) + common_flags(parser_minifier_query) + parser_minifier_query.add_argument( + "--check-str", + type=str, + default=check_str, + help="require minified program to fail with error containing this string", + ) + + args = None + if len(sys.argv) <= 1: + args = [command, *sys.argv[1:]] + + options = parser.parse_args(args) + COMMAND_FNS = { + "minify": repro_minify, + "run": repro_run, + "get_args": repro_get_args, + } + return COMMAND_FNS[options.command]( + options, exported_program, config_patches=config_patches + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/resume_execution.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/resume_execution.py new file mode 100644 index 0000000000000000000000000000000000000000..cc3d0068e31529e82ecb0c8c7f23fbb32d762ed4 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/resume_execution.py @@ -0,0 +1,750 @@ +""" +This module provides functionality for resuming Python execution at specific points in code, +primarily used by PyTorch Dynamo for control flow handling and optimization. It implements +bytecode transformation and execution state management to enable: + +- Resuming execution at arbitrary points in Python bytecode +- Managing context managers and their state across execution boundaries +- Transforming and generating new code objects with preserved execution state +- Supporting Python 3.11+ exception handling and block management +- Restoring torch function mode stacks and other execution context + +The module is critical for PyTorch Dynamo's ability to optimize code while preserving +Python semantics and execution state. +""" + +import copy +import dataclasses +import sys +import types +from collections.abc import Callable, Iterable +from contextlib import AbstractContextManager +from typing import Any, cast + +from .bytecode_transformation import ( + add_push_null, + bytecode_from_template, + create_binary_subscr, + create_call_function, + create_call_function_ex, + create_instruction, + create_jump_absolute, + create_load_const, + Instruction, + overwrite_instruction, + transform_code_object, + unique_id, +) +from .utils import ExactWeakKeyDictionary + + +# taken from code.h in cpython +CO_OPTIMIZED = 0x0001 +CO_NEWLOCALS = 0x0002 +CO_VARARGS = 0x0004 +CO_VARKEYWORDS = 0x0008 +CO_NESTED = 0x0010 +CO_GENERATOR = 0x0020 +CO_NOFREE = 0x0040 +CO_COROUTINE = 0x0080 +CO_ITERABLE_COROUTINE = 0x0100 +CO_ASYNC_GENERATOR = 0x0200 + +# trace_rules.py import this constant for consistency +TORCH_DYNAMO_RESUME_IN_PREFIX = "torch_dynamo_resume_in" +IS_TRACING_RESUME_PROLOGUE_VARNAME = "__is_tracing_resume_prologue" + + +# If is_resume - this codegen is for a resume function +def _initial_push_null(insts: list[Instruction]) -> None: + if sys.version_info >= (3, 11): + insts.append(create_instruction("PUSH_NULL")) + if sys.version_info < (3, 13): + insts.append(create_instruction("SWAP", arg=2)) + + +# Generates bytecode from template and splits the code where LOAD_FAST dummy is present. +def _bytecode_from_template_with_split( + template: Callable[..., Any], + stack_index: int, + varname_map: dict[str, Any] | None = None, +) -> tuple[list[Instruction], list[Instruction]]: + template_code = bytecode_from_template(template, varname_map=varname_map) + template_code.append(create_instruction("POP_TOP")) + + # adjust exception table entry depth + for inst in template_code: + if inst.exn_tab_entry: + inst.exn_tab_entry.depth += stack_index + + # search for LOAD_FAST dummy and replace it with 2 NOPs (we can break up the bytecode between them) + dummy_idx, dummy_inst = next( + ( + (i, inst) + for i, inst in enumerate(template_code) + if inst.opname in ("LOAD_FAST", "LOAD_FAST_BORROW") + and inst.argval == "dummy" + ), + (None, None), + ) + assert dummy_idx is not None and dummy_inst is not None + + # replace LOAD_FAST dummy with first NOP marking exception area + overwrite_instruction(dummy_inst, [create_instruction("NOP")]) + + # POP_TOP follows LOAD_FAST dummy - replace with NOP marking end of exception area + assert template_code[dummy_idx + 1].opname == "POP_TOP" + overwrite_instruction(template_code[dummy_idx + 1], [create_instruction("NOP")]) + + return template_code[: dummy_idx + 1], template_code[dummy_idx + 1 :] + + +def _try_except_tf_mode_template(dummy: Any, stack_var_name: Any) -> None: + # NOTE: Make sure this name matches what is generated by symbolic_convert:import_source + # on torch._dynamo.utils. + # pyrefly: ignore [unknown-name] + global __import_torch_dot__dynamo_dot_utils + try: + dummy + except: # noqa: E722, B001 + __import_torch_dot__dynamo_dot_utils.set_torch_function_mode_stack( # type: ignore[name-defined] + stack_var_name + ) + raise + + +@dataclasses.dataclass(frozen=True) +class ReenterWith: + stack_index: int + target_values: tuple[Any, ...] | None = None + + def try_except_torch_function_mode( + self, code_options: dict[str, Any], cleanup: list[Instruction] + ) -> list[Instruction]: + """ + Codegen based off of: + try: + (rest) + except: + (restore previous tf mode stack) + raise + """ + from .variables.torch_function import get_prev_stack_var_name + + setup_try_except, epilogue = _bytecode_from_template_with_split( + _try_except_tf_mode_template, + self.stack_index, + varname_map={"stack_var_name": get_prev_stack_var_name()}, + ) + cleanup[:] = epilogue + cleanup + + return setup_try_except + + # If we do not want to destroy the stack, we can do the same thing as a + # `SETUP_WITH` block, only that we store the context manager in a local_symbol + def try_finally( + self, code_options: dict[str, Any], cleanup: list[Instruction] + ) -> list[Instruction]: + """ + Codegen based off of: + load args + enter context + try: + (rest) + finally: + exit context + """ + # NOTE: we assume that TOS is a context manager CLASS! + # pyrefly: ignore [implicit-any] + load_args = [] + if self.target_values: + load_args = [create_load_const(val) for val in self.target_values] + ctx_name = unique_id(f"___context_manager_{self.stack_index}") + if ctx_name not in code_options["co_varnames"]: + code_options["co_varnames"] += (ctx_name,) + for name in ["__enter__", "__exit__"]: + if name not in code_options["co_names"]: + code_options["co_names"] += (name,) + + create_ctx: list[Instruction] = [] + _initial_push_null(create_ctx) + create_ctx.extend( + [ + *load_args, + *create_call_function(len(load_args), False), + create_instruction("STORE_FAST", argval=ctx_name), + ] + ) + + def _template(ctx: AbstractContextManager[Any], dummy: Any) -> None: + ctx.__enter__() + try: + dummy + finally: + ctx.__exit__(None, None, None) + + setup_try_finally, epilogue = _bytecode_from_template_with_split( + _template, self.stack_index, varname_map={"ctx": ctx_name} + ) + cleanup[:] = epilogue + cleanup + return create_ctx + setup_try_finally + + def __call__( + self, code_options: dict[str, Any], cleanup: list[Instruction] + ) -> tuple[list[Instruction], Instruction | None]: + """ + Codegen based off of: + with ctx(args): + (rest) + """ + # NOTE: we assume that TOS is a context manager CLASS! + # pyrefly: ignore [implicit-any] + load_args = [] + if self.target_values: + load_args = [create_load_const(val) for val in self.target_values] + + create_ctx: list[Instruction] = [] + # Do not push NULL in Python 3.14+ since the NULL should be on the symbolic stack. + if sys.version_info < (3, 14): + _initial_push_null(create_ctx) + create_ctx.extend( + [ + *load_args, + *create_call_function(len(load_args), False), + ] + ) + + def _template(ctx: AbstractContextManager[Any], dummy: Any) -> None: + with ctx: + dummy + + setup_with, epilogue = _bytecode_from_template_with_split( + _template, self.stack_index + ) + cleanup[:] = epilogue + cleanup + + load_fast_ctx_inst = next( + ( + inst + for inst in setup_with + if inst.opname in ("LOAD_FAST", "LOAD_FAST_BORROW") + and inst.argval == "ctx" + ), + None, + ) + assert load_fast_ctx_inst is not None + # ctx already loaded on stack before the template - no need to LOAD_FAST + overwrite_instruction(load_fast_ctx_inst, [create_instruction("NOP")]) + + # 3.11+ only + push_exc_info_gen = ( + inst for inst in epilogue if inst.opname == "PUSH_EXC_INFO" + ) + push_exc_info_inst = next(push_exc_info_gen, None) + # expect only 1 PUSH_EXC_INFO in epilogue + assert next(push_exc_info_gen, None) is None + + return create_ctx + setup_with, push_exc_info_inst + + +@dataclasses.dataclass +class ResumeFunctionMetadata: + code: types.CodeType + instructions: list[Instruction] = dataclasses.field(default_factory=list) + # Python 3.11+ fields + # NOTE: Python 3.11 removed blocks, but for our purposes, a "block" consists + # of instructions of all exception table entries that have the same target. + + # map from PUSH_EXC_INFO's in the prefix to original block target offset + prefix_block_target_offset_remap: list[int] = dataclasses.field( + default_factory=list + ) + # per-offset map from new block target offsets to original block target offsets + block_target_offset_remap: dict[tuple[int, int], dict[int, int]] = ( + dataclasses.field(default_factory=dict) + ) + + +def _filter_iter( + l1: Iterable[Any], + l2: Iterable[Any], + cond: Callable[[Any, Any], bool], +) -> list[Any]: + """ + Two-pointer conditional filter. + e.g. _filter_iter(insts, sorted_offsets, lambda i, o: i.offset == o) + returns the instructions with offsets in sorted_offsets + """ + it = iter(l2) + res: list[Instruction] = [] + try: + cur = next(it) + for val in l1: + if cond(val, cur): + res.append(val) + cur = next(it) + except StopIteration: + pass + return res + + +def _load_tuple_and_call(tup: tuple[Any, ...]) -> list[Instruction]: + insts: list[Instruction] = [] + _initial_push_null(insts) + insts.extend(create_load_const(val) for val in tup) + insts.extend(create_call_function(len(tup), False)) + return insts + + +class ContinueExecutionCache: + cache = ExactWeakKeyDictionary() + generated_code_metadata = ExactWeakKeyDictionary() + + @classmethod + def lookup( + cls, code: types.CodeType, lineno: int, init_offset: int, *key: Any + ) -> types.CodeType: + if code not in cls.cache: + cls.cache[code] = {} + key = tuple(key) + if key not in cls.cache[code]: + cls.cache[code][key] = cls.generate(code, lineno, init_offset, *key) + return cls.cache[code][key] + + @classmethod + def generate( + cls, + code: types.CodeType, + lineno: int, + init_offset: int, + resume_offset: int, + setup_fn_target_offsets: tuple[int, ...], # only used in Python 3.11+ + nstack: int, + argnames: tuple[str, ...], + argnames_null: tuple[str, ...], + setup_fns: tuple[ReenterWith, ...], + handle_inactive_ctx: bool, + stack_ctx_vars: tuple[tuple[int, tuple[Any, ...]], ...], + argnames_ctx_vars: tuple[tuple[str, tuple[Any, ...]], ...], + null_idxes: tuple[int, ...], + # mainly used to ensure distinct code objects per stack trace, + # which prevents excessive recompilation of inner frames + nested_code_objs: tuple[types.CodeType], + # Are we currently graph breaking on an instruction that doesn't push + # its result to the stack? If so, and we are not the leaf resume, then we need to pop + # the result of calling the next resume function. + pop_nested_resume_result: bool, + ) -> types.CodeType: + assert resume_offset is not None + assert not ( + code.co_flags + & (CO_GENERATOR | CO_COROUTINE | CO_ITERABLE_COROUTINE | CO_ASYNC_GENERATOR) + ) + assert code.co_flags & CO_OPTIMIZED + if code in ContinueExecutionCache.generated_code_metadata: + return cls.generate_based_on_original_code_object( + code, + lineno, + init_offset, + resume_offset, + setup_fn_target_offsets, + nstack, + argnames, + argnames_null, + setup_fns, + handle_inactive_ctx, + stack_ctx_vars, + argnames_ctx_vars, + null_idxes, + nested_code_objs, + pop_nested_resume_result, + ) + + is_py311_plus = sys.version_info >= (3, 11) + meta = ResumeFunctionMetadata(code) + + def update( + instructions: list[Instruction], code_options: dict[str, Any] + ) -> None: + meta.instructions = copy.deepcopy(instructions) + + args = ["__nested_resume_fns", "__nested_frame_values"] + args += [f"___stack{i}" for i in range(nstack)] + args.extend(v for v in argnames if v not in args) + freevars = tuple(code_options["co_cellvars"] or []) + tuple( + code_options["co_freevars"] or [] + ) + freevars = tuple(sorted(freevars)) + code_options["co_name"] = ( + f"{TORCH_DYNAMO_RESUME_IN_PREFIX}_{code_options['co_name']}_at_{lineno}" + ) + if is_py311_plus: + qualified_path = code_options["co_qualname"].rsplit(".", maxsplit=1) + if len(qualified_path) == 1: + code_options["co_qualname"] = code_options["co_name"] + else: + assert len(qualified_path) == 2 + module_name, co_name = qualified_path + code_options["co_qualname"] = ( + f"{module_name}.{TORCH_DYNAMO_RESUME_IN_PREFIX}_{co_name}_at_{lineno}" + ) + code_options["co_firstlineno"] = lineno + code_options["co_cellvars"] = () + code_options["co_freevars"] = freevars + code_options["co_argcount"] = len(args) + code_options["co_posonlyargcount"] = 0 + code_options["co_kwonlyargcount"] = 0 + code_options["co_varnames"] = tuple( + args + + [v for v in argnames_null if v not in args] + + [v for v in code_options["co_varnames"] if v not in args] + + [IS_TRACING_RESUME_PROLOGUE_VARNAME] + ) + code_options["co_flags"] = code_options["co_flags"] & ~( + CO_VARARGS | CO_VARKEYWORDS + ) + target = next(i for i in instructions if i.offset == resume_offset) + + prefix = [] + if is_py311_plus: + if freevars: + prefix.append( + create_instruction("COPY_FREE_VARS", arg=len(freevars)) + ) + prefix.append(create_instruction("RESUME", arg=0)) + + # Set is_tracing_resume_prologue to prevent graph breaks. + # This doesn't really do anything at runtime, but dynamo will trace this + # and will know that we're in a resume function prologue. + prefix.extend( + [ + create_instruction("LOAD_CONST", argval=True), + create_instruction( + "STORE_FAST", argval=IS_TRACING_RESUME_PROLOGUE_VARNAME + ), + ] + ) + + cleanup: list[Instruction] = [] + hooks = {fn.stack_index: fn for fn in setup_fns} + hook_target_offsets = { + fn.stack_index: setup_fn_target_offsets[i] + for i, fn in enumerate(setup_fns) + } + offset_to_inst = {inst.offset: inst for inst in instructions} + # map old hook targets to new targets generated by the hook + # pyrefly: ignore [implicit-any] + old_hook_target_remap = {} + stack_i = 0 + null_i = 0 + stack_ctx_vars_d = dict(stack_ctx_vars) # type: ignore[var-annotated,arg-type] + for i in range(nstack + len(null_idxes)): + if null_i < len(null_idxes) and null_idxes[null_i] == i: + prefix.append(create_instruction("PUSH_NULL")) + null_i += 1 + else: + prefix.append( + create_instruction("LOAD_FAST", argval=f"___stack{stack_i}") + ) + if handle_inactive_ctx and stack_i in stack_ctx_vars_d: + # NOTE: we assume that current stack var is a context manager CLASS! + # Load args for context variable and construct it + prefix.extend(_load_tuple_and_call(stack_ctx_vars_d[stack_i])) + stack_i += 1 + + if i in hooks: + hook = hooks.pop(i) + hook_insts, exn_target = hook(code_options, cleanup) + prefix.extend(hook_insts) + if is_py311_plus: + hook_target_offset = hook_target_offsets.pop(i) + old_hook_target = offset_to_inst[hook_target_offset] + meta.prefix_block_target_offset_remap.append(hook_target_offset) + old_hook_target_remap[old_hook_target] = exn_target + + if is_py311_plus: + # reverse the mapping since targets of later/nested contexts are inserted + # into the mapping later, but show up earlier in the prefix. + meta.prefix_block_target_offset_remap = list( + reversed(meta.prefix_block_target_offset_remap) + ) + + assert not hooks + + # NOTE: we assume that local var is a context manager CLASS! + # initialize inactive context vars in argnames + if handle_inactive_ctx: + for name, vals in argnames_ctx_vars: + prefix.append(create_instruction("LOAD_FAST", argval=name)) + prefix.extend(_load_tuple_and_call(vals)) + prefix.append(create_instruction("STORE_FAST", argval=name)) + + # 3.12+: store NULL into variables that were NULL + if argnames_null: + assert sys.version_info >= (3, 12) + for v in argnames_null: + assert v not in args + prefix.extend( + [ + create_instruction("PUSH_NULL"), + create_instruction("STORE_FAST", argval=v), + ] + ) + + # Call nested resume function + if nested_code_objs: + prefix.extend( + [ + # set up __nested_resume_fns[-1] call + *add_push_null( + [ + create_instruction( + "LOAD_FAST", argval="__nested_resume_fns" + ), + create_instruction("LOAD_CONST", argval=-1), + create_binary_subscr(), + ] + ), + # del __nested_resume_fns[-1] + create_instruction("LOAD_FAST", argval="__nested_resume_fns"), + create_instruction("LOAD_CONST", argval=-1), + create_instruction("DELETE_SUBSCR"), + # load [__nested_resume_fns, __nested_frame_values] + create_instruction("LOAD_FAST", argval="__nested_resume_fns"), + create_instruction("LOAD_FAST", argval="__nested_frame_values"), + create_instruction("BUILD_LIST", arg=2), + # load __nested_frame_values[-1] + create_instruction("LOAD_FAST", argval="__nested_frame_values"), + create_instruction("LOAD_CONST", argval=-1), + create_binary_subscr(), + # create [ + # __nested_resume_fns, + # __nested_frame_values, + # *__nested_frame_values[-1], + # ] + create_instruction("LIST_EXTEND", arg=1), + # del __nested_frame_values[-1] + create_instruction("LOAD_FAST", argval="__nested_frame_values"), + create_instruction("LOAD_CONST", argval=-1), + create_instruction("DELETE_SUBSCR"), + # delete __nested values + create_instruction("DELETE_FAST", argval="__nested_resume_fns"), + create_instruction( + "DELETE_FAST", argval="__nested_frame_values" + ), + # Set is_tracing_resume_prologue back to allow graph breaks + # in the nested resume + create_instruction("LOAD_CONST", argval=False), + create_instruction( + "STORE_FAST", argval=IS_TRACING_RESUME_PROLOGUE_VARNAME + ), + # finish the call + *create_call_function_ex(False, False), + ] + ) + if pop_nested_resume_result: + # pop the result of calling the nested resume function + prefix.append(create_instruction("POP_TOP")) + else: + # Set is_tracing_resume_prologue back to allow graph breaks after the jump + prefix.extend( + [ + create_instruction("LOAD_CONST", argval=False), + create_instruction( + "STORE_FAST", argval=IS_TRACING_RESUME_PROLOGUE_VARNAME + ), + ] + ) + + prefix.append(create_jump_absolute(target)) + + # because the line number table monotonically increases from co_firstlineno + # remove starts_line for any instructions before the graph break instruction + # this will ensure the instructions after the break have the correct line numbers + for inst in instructions: + if inst.offset == target.offset: + break + inst.starts_line = None + if sys.version_info >= (3, 11): + inst.positions = None + + if cleanup: + prefix.extend(cleanup) + prefix.extend(cls.unreachable_codes(code_options)) + + # remap original instructions' exception table entries + if old_hook_target_remap: + # pyrefly: ignore [unbound-name] + assert is_py311_plus + for inst in instructions: + if ( + inst.exn_tab_entry + and inst.exn_tab_entry.target in old_hook_target_remap + ): + inst.exn_tab_entry.target = old_hook_target_remap[ # type: ignore[assignment] + inst.exn_tab_entry.target + ] + + # TODO(jansel): add dead code elimination here + instructions[:] = prefix + instructions + + new_code, _ = transform_code_object(code, update) + ContinueExecutionCache.generated_code_metadata[new_code] = meta + return new_code + + @staticmethod + def unreachable_codes(code_options: dict[str, Any]) -> list[Instruction]: + """Codegen a `raise None` to make analysis work for unreachable code""" + return [ + create_load_const(None), + create_instruction("RAISE_VARARGS", arg=1), + ] + + @classmethod + def generate_based_on_original_code_object( + cls, + code: types.CodeType, + lineno: int, + init_offset: int, + resume_offset: int, + setup_fn_target_offsets: tuple[int, ...], + *args: Any, + ) -> types.CodeType: + """ + This handles the case of generating a resume into code generated + to resume something else. We want to always generate starting + from the original code object so that if control flow paths + converge we only generated 1 resume function (rather than 2^n + resume functions). + """ + + meta: ResumeFunctionMetadata = ContinueExecutionCache.generated_code_metadata[ + code + ] + + def find_orig_offset(cur_offset: int) -> int: + orig_offset = -1 + + def find_orig_offset_transform( + instructions: list[Instruction], code_options: dict[str, Any] + ) -> None: + nonlocal orig_offset + (target,) = (i for i in instructions if i.offset == cur_offset) + # match the functions starting at the last instruction as we have added a prefix + new_target_tuple = tuple( + i2 + for i1, i2 in zip( + reversed(instructions), reversed(meta.instructions) + ) + if i1 is target + ) + + if not new_target_tuple: + # Instruction with cur_offset in instructions was not found + # in the original code - orig_offset left as -1. + # Caller expected to handle this case. + return + + assert len(new_target_tuple) == 1 + new_target = new_target_tuple[0] + + assert target.opcode == new_target.opcode + assert new_target.offset is not None + orig_offset = new_target.offset + + transform_code_object(code, find_orig_offset_transform) + return orig_offset + + orig_init_offset = find_orig_offset(init_offset) + # It is fine if the initial instruction is not found in the original code; + # this means we graph broke in the prefix, which only happens with nested graph breaks. + # We should not be running into ambiguous graph break issues here. + orig_resume_offset = find_orig_offset(resume_offset) + assert orig_resume_offset > -1, ( + "resume instruction not found in original code - this is a bug." + ) + + if sys.version_info >= (3, 11): + # setup_fn_target_offsets currently contains the target offset of + # each setup_fn, based on `code`. When we codegen the resume function + # based on the original code object, `meta.code`, the offsets in + # setup_fn_target_offsets must be based on `meta.code` instead. + offset_key = (orig_init_offset, orig_resume_offset) + # NOTE: we key by offset_key since the same resume function may graph + # break in multiple places and we need different block_target_offset_remap's + # for each graph break location. Keying by orig_resume_offset may not be enough + # if 2 graph breaks on different initial offsets resume on the same instruction + # (although this is rare and not tested anywhere). + if offset_key not in meta.block_target_offset_remap: + block_target_offset_remap = meta.block_target_offset_remap[ + offset_key + # pyrefly: ignore [implicit-any] + ] = {} + + def remap_block_offsets( + instructions: list[Instruction], code_options: dict[str, Any] + ) -> None: + # NOTE: each prefix block generates exactly one PUSH_EXC_INFO, + # so we can tell which block a prefix PUSH_EXC_INFO belongs to, + # by counting. Then we can use meta.prefix_block_target_offset_remap + # to determine where in the original code the PUSH_EXC_INFO offset + # replaced. + prefix_blocks: list[Instruction] = [] + for inst in instructions: + # NOTE meta.prefix_block_target_offset_remap is based off of how we codegen'd + # context managers at the prefix/prologue of the resume function. It is the same for + # every graph break in the same resume function, so we do not need to recompute + # for each graph break (unlike for meta.block_target_offset_remap) + if len(prefix_blocks) == len( + meta.prefix_block_target_offset_remap + ): + break + if inst.opname == "PUSH_EXC_INFO": + prefix_blocks.append(inst) + + # remap block target offsets for blocks generated in the resume prefix + for inst, o in zip( + prefix_blocks, meta.prefix_block_target_offset_remap + ): + block_target_offset_remap[cast(int, inst.offset)] = o + + # current bytecode targets are after the prefix PUSH_EXC_INFO's + cur_start_offset = ( + cast(int, prefix_blocks[-1].offset) if prefix_blocks else -1 + ) + # get the remaining block target offsets of the current bytecode + cur_inst_offsets = sorted( + n for n in setup_fn_target_offsets if n > cur_start_offset + ) + targets = _filter_iter( + instructions, cur_inst_offsets, lambda inst, o: inst.offset == o + ) + # The original code and resume code should have matching suffixes. + # Match the post-prefix block target offsets of the current resume code + # and the original code. + orig_targets = reversed( + _filter_iter( + zip(reversed(instructions), reversed(meta.instructions)), + reversed(targets), + lambda v1, v2: v1[0] is v2, + ) + ) + for orig, cur in zip(orig_targets, targets): + block_target_offset_remap[cur.offset] = orig[1].offset + + transform_code_object(code, remap_block_offsets) + + # if offset_key or offset is not in setup_fn_target_offsets, it is an error + # that needs to be fixed + setup_fn_target_offsets = tuple( + meta.block_target_offset_remap[offset_key][n] + for n in setup_fn_target_offsets + ) + return ContinueExecutionCache.lookup( + meta.code, + lineno, + orig_init_offset, + orig_resume_offset, + setup_fn_target_offsets, + *args, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/side_effects.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/side_effects.py new file mode 100644 index 0000000000000000000000000000000000000000..0d7d0eb325bdcb8aa33a3945ac63d8d4b861fa8b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/side_effects.py @@ -0,0 +1,1502 @@ +""" +Side effect tracking and management for TorchDynamo's compilation system. + +This module provides infrastructure for tracking and managing side effects that occur +during symbolic execution, including: + +- Tracking mutations to objects, attributes, and variables +- Managing context changes (cell variables, global namespace modifications) +- Handling aliasing and object identity preservation +- Managing stack frame state and local variable changes +- Tracking function calls with side effects + +Key classes: +- SideEffects: Main container for tracking all side effects during execution +- MutableSideEffects: Specialization for mutable object tracking +- AttributeMutation/ValueMutation: Track specific types of mutations +- Various specialized side effect classes for different scenarios + +The side effect system ensures that mutations performed during symbolic execution +are properly replayed during runtime, maintaining the correctness of compiled code +while enabling optimizations where safe. +""" + +import collections +import contextlib +import inspect +import logging +import textwrap +import traceback +import warnings +import weakref +from collections.abc import Generator, MutableMapping +from types import CellType +from typing import Any, TYPE_CHECKING + +import torch +import torch.nn +from torch._dynamo.variables.misc import AutogradFunctionContextVariable +from torch.utils._ordered_set import OrderedSet +from torch.utils._pytree import is_structseq_class + +from . import config, graph_break_hints, utils, variables +from .bytecode_transformation import ( + bytecode_from_template, + create_call_function, + create_call_method, + create_instruction, +) +from .codegen import PyCodegen +from .exc import collapse_resume_frames, get_stack_above_dynamo, unimplemented +from .source import AttrSource, GlobalSource, LocalCellSource, Source, TempLocalSource +from .utils import is_frozen_dataclass, is_namedtuple_cls, nn_module_new, object_new +from .variables.base import ( + AttributeMutation, + AttributeMutationExisting, + AttributeMutationNew, + is_side_effect_safe, + ValueMutationExisting, + ValueMutationNew, + VariableTracker, +) +from .variables.user_defined import FrozenDataClassVariable + + +if TYPE_CHECKING: + from torch._dynamo.output_graph import OutputGraph + from torch._dynamo.symbolic_convert import InstructionTranslatorBase + from torch._dynamo.variables.lists import ListVariable + + +side_effects_log = torch._logging.getArtifactLogger(__name__, "side_effects") + + +def _manual_dict_setitem( + dict_from: dict[Any, Any], dict_to: dict[Any, Any], mro_index: int +) -> None: + # Carefully calls the dict or OrderedDict `clear` or `__setitem__`. We have + # to be careful because we don't want to trigger the user defined object + # setitem or clear. The mro_index is used to find the dict/OrderedDict from + # the class mro. + dict_class = type(dict_to).__mro__[mro_index] + dict_class.clear(dict_to) # type: ignore[attr-defined] + for k, v in dict_from.items(): + dict_class.__setitem__(dict_to, k, v) # type: ignore[index] + + +def _manual_list_update(list_from: list[Any], list_to: list[Any]) -> None: + list.clear(list_to) + list.extend(list_to, list_from) + + +class SideEffects: + """ + Maintain records of mutations and provide methods to apply them during code generation. + + Handles tracking and applying side effects during PyTorch Dynamo compilation, + maintaining Python semantics by managing mutations, attribute modifications, + and other side effects that occur during program execution. + + Key responsibilities: + - Tracks mutations to Python objects, lists, and dictionaries that need to be + applied after an FX graph is run. + - Manages attribute modifications and deletions + - Handles tensor hooks and backward pass state + - Tracks cell variable mutations and global variable changes + - Ensures correct ordering and application of side effects after graph execution + + This ensures that optimized code behaves identically to the original Python code with + respect to object mutations and other side effects. + """ + + id_to_variable: dict[int, VariableTracker] + store_attr_mutations: dict[VariableTracker, dict[str, VariableTracker]] + keepalive: list[Any] + # Maps variable tracker to list of user stacks (StackSummary objects, formatted lazily) + mutation_user_stacks: dict[VariableTracker, list[traceback.StackSummary]] + + def __init__( + self, + output_graph: "OutputGraph", + id_to_variable: dict[int, VariableTracker] | None = None, + store_attr_mutations: dict[VariableTracker, dict[str, VariableTracker]] + | None = None, + mutation_user_stacks: dict[VariableTracker, list[traceback.StackSummary]] + | None = None, + keepalive: list[Any] | None = None, + save_for_backward: list[ + tuple[AutogradFunctionContextVariable, list[VariableTracker]] + ] + | None = None, + tensor_hooks: dict[ + int, + tuple[ + "variables.TensorVariable", + VariableTracker, + "variables.RemovableHandleVariable", + str, + ], + ] + | None = None, + ) -> None: + super().__init__() + self.output_graph_weakref = weakref.ref(output_graph) + self.id_to_variable = id_to_variable or {} + self.store_attr_mutations = store_attr_mutations or {} + self.mutation_user_stacks = mutation_user_stacks or {} + self.keepalive = keepalive or [] + self.save_for_backward = save_for_backward or [] + self.tensor_hooks = tensor_hooks or {} + # Used by MappingProxyVariable to graph break in case of any mutated + # dict + self._has_existing_dict_mutation = False + # Track Compiled Autograd final callbacks that must be called at the end of Compiled Autograd backward graph. + # Only applicable if this graph is created from Dynamo tracing in Compiled Autograd. + self.ca_final_callbacks_var: ListVariable | None = None + + # Tracks VariableTracker objects whose mutations can be skipped. + # For normal mutated variables, Dynamo generates code to replay/reconstruct + # the mutations after graph execution. However, variables in this set have + # their mutations ignored - the mutations happen during + # execution but don't need to be replayed in the generated code. + # Used for temporary mutations in contexts like torch.func.functional_call, + # where module parameters/buffers are modified but later restored. + self.ignore_mutation_on_these_variables: set[VariableTracker] = set() + # Sources mutated during tracing: AttrSource for attribute + # mutations, var.source for value mutations (list/dict/etc). + self.mutated_sources: OrderedSet[Source] = OrderedSet() + + def ignore_mutations_on(self, var: VariableTracker) -> None: + """Mutations to this variable will be executed but not not tracked, + typically used for temporary mutations that are later restored.""" + self.ignore_mutation_on_these_variables.add(var) + + def stop_ignoring_mutations_on(self, var: VariableTracker) -> None: + """Remove a variable from the skip mutation set, restoring normal mutation tracking.""" + if var in self.ignore_mutation_on_these_variables: + self.ignore_mutation_on_these_variables.remove(var) + + def _capture_user_stack(self, key: VariableTracker) -> None: + """Capture the current user stack from the instruction translator.""" + if config.side_effect_replay_policy == "silent": + return + if key not in self.mutation_user_stacks: + self.mutation_user_stacks[key] = [] + self.mutation_user_stacks[key].append( + torch._guards.TracingContext.extract_stack() + ) + + def __eq__(self, other: object) -> bool: + assert isinstance(other, SideEffects) + # NB: do NOT test keepalive + return ( + self.id_to_variable == other.id_to_variable + and self.store_attr_mutations == other.store_attr_mutations + and self.save_for_backward == other.save_for_backward + and self.tensor_hooks == other.tensor_hooks + ) + + def diff(self, other: "SideEffects") -> str | None: + if self.id_to_variable != other.id_to_variable: + sk_itv = self.id_to_variable.keys() + ok_itv = other.id_to_variable.keys() + if sk_itv != ok_itv: + return f"id_to_variable keys: {sk_itv} != {ok_itv}" + # Feel free to augment this with more fancy diffing logic + # if needed for debugging + return "id_to_variable: unknown diff" + elif self.store_attr_mutations != other.store_attr_mutations: + sk_sam = self.store_attr_mutations.keys() + ok_sam = other.store_attr_mutations.keys() + if sk_sam != ok_sam: + return f"store_attr_mutations keys: {sk_sam} != {ok_sam}" + return "store_attr_mutations: unknown diff" + elif self.save_for_backward != other.save_for_backward: + return "save_for_backward" + elif self.tensor_hooks != other.tensor_hooks: + return "tensor_hooks" + else: + return None + + def clone(self) -> "SideEffects": + """Create a shallow copy""" + ref = self.output_graph_weakref() + assert ref is not None + return self.__class__( + output_graph=ref, + id_to_variable=dict(self.id_to_variable), + store_attr_mutations={ + k: dict(v) for k, v in self.store_attr_mutations.items() + }, + mutation_user_stacks=self.mutation_user_stacks, + keepalive=list(self.keepalive), + save_for_backward=self.save_for_backward, + tensor_hooks=self.tensor_hooks, + ) + + def __contains__(self, item: Any) -> bool: + return id(item) in self.id_to_variable + + def __getitem__(self, item: Any) -> VariableTracker: + return self.id_to_variable[id(item)] + + def should_allow_externally_visible_side_effects_in_subtracer(self) -> bool: + output_graph = self.output_graph_weakref() + return bool( + output_graph + and output_graph.current_tx.output.current_tracer.unsafe_allow_externally_visible_side_effects + ) + + def should_allow_side_effects_in_hop(self) -> bool: + output_graph = self.output_graph_weakref() + return bool( + output_graph + and output_graph.current_tx.output.current_tracer.allow_side_effects_in_hop + ) + + def is_reconstructing_generator(self) -> bool: + output_graph = self.output_graph_weakref() + + return bool( + output_graph + and output_graph.current_tx.output.current_tracer.is_reconstructing_generator + ) + + def _maybe_record_side_effect(self, item: VariableTracker) -> None: + """Record the first externally-visible side effect on the current tracer.""" + if item.mutation_type is not None and not is_side_effect_safe( + item.mutation_type + ): + output_graph = self.output_graph_weakref() + if output_graph: + tracer = output_graph.current_tx.output.current_tracer + if tracer.side_effect_stack is None: + tracer.side_effect_stack = ( + torch._guards.TracingContext.extract_stack() + ) + + def check_allowed_side_effect(self, item: VariableTracker) -> bool: + from torch._dynamo.variables.misc import AutogradFunctionContextVariable + + # People do things like self.dim = dim inside autograd.Function. + # These are benign. + if isinstance(item, AutogradFunctionContextVariable): + return True + if self.should_allow_externally_visible_side_effects_in_subtracer(): + self._maybe_record_side_effect(item) + return True + if self.should_allow_side_effects_in_hop(): + self._maybe_record_side_effect(item) + return True + if self.is_reconstructing_generator(): + # This is missing the case where one mutates a tensor. See + # test_generator.py::test_reconstruct_generator_tensor_mutation + unimplemented( + gb_type="Generator reconstruction with mutations", + context=f"mutating object: {item}", + explanation="Cannot reconstruct a generator with variable mutations. " + "Dynamo needs to fully exhaust the generator, which may cause " + "unintended variable modifications.", + hints=[ + "Remove mutations from the generator.", + *graph_break_hints.FUNDAMENTAL, + ], + ) + assert item.mutation_type is not None + if not is_side_effect_safe(item.mutation_type): + unimplemented( + gb_type="HOP: Unsafe side effect", + context=f"Attempted to mutate {item}", + explanation="Mutating a variable from outside the scope of this HOP is not supported.", + hints=[ + "If the HOP is activation checkpointing (torch.utils.checkpoint.checkpoint), this points to a " + "side effect in forward method. Eager activation checkpointing replays that side-effect while " + "recomputing the forward in the backward. If you are ok with side-effect not replayed in the " + "backward, try setting `torch._dynamo.config.skip_fwd_side_effects_in_bwd_under_checkpoint = True`", + ], + ) + return False + + def store_attr( + self, item: VariableTracker, name: str, value: VariableTracker + ) -> None: + assert self.is_attribute_mutation(item) + self.check_allowed_side_effect(item) + if item not in self.store_attr_mutations: + self.store_attr_mutations[item] = {} + self.store_attr_mutations[item][name] = value + # Capture user stack for this mutation + self._capture_user_stack(item) + item_source = getattr(item, "source", None) + if item_source is not None: + self.mutated_sources.add(AttrSource(item_source, name)) + + def load_attr( + self, + item: VariableTracker, + name: str, + deleted_ok: bool = False, + check: bool = False, + ) -> VariableTracker: + if check: + assert self.is_attribute_mutation(item) + result = self.store_attr_mutations[item][name] + if not deleted_ok and isinstance(result, variables.DeletedVariable): + unimplemented( + gb_type="Attempted to read a deleted variable", + context=f"item: {item}, name: {name}", + explanation="", + hints=[*graph_break_hints.USER_ERROR], + ) + return result + + def store_cell(self, cellvar: VariableTracker, value: VariableTracker) -> None: + if cellvar.is_immutable(): + unimplemented( + gb_type="Write to immutable cell", + context=f"cellvar: {cellvar}, value: {value}", + explanation="Dynamo doesn't support writing to immutable/sourceless cell variables.", + hints=[*graph_break_hints.DIFFICULT], + ) + assert isinstance(cellvar, variables.CellVariable) + assert isinstance(value, variables.VariableTracker) + self.store_attr(cellvar, "cell_contents", value) + + def load_cell(self, cellvar: VariableTracker) -> VariableTracker: + assert isinstance(cellvar, variables.CellVariable) + # Track the cell_contents source during subgraph tracing so that + # mutations (e.g. nonlocal counter = 3) are detected by the reuse + # mechanism via set intersection with mutated_sources. + output_graph = self.output_graph_weakref() + if output_graph: + cell_source = getattr(cellvar, "source", None) + if cell_source is not None: + output_graph.current_tx.output.current_tracer.traced_sources.add( + AttrSource(cell_source, "cell_contents") + ) + if self.has_pending_mutation_of_attr(cellvar, "cell_contents"): + return self.load_attr(cellvar, "cell_contents", check=False) + if cellvar.pre_existing_contents: + return cellvar.pre_existing_contents + unimplemented( + gb_type="Read uninitialized cell", + context=str(cellvar), + explanation="Attempted to read a cell variable that has not been populated yet.", + hints=[*graph_break_hints.USER_ERROR], + ) + + def load_global(self, gvar: VariableTracker, name: str) -> VariableTracker: + assert isinstance(gvar, variables.VariableTracker) + return self.load_attr(gvar, name) + + def store_global( + self, gvar: VariableTracker, name: str, value: VariableTracker + ) -> None: + assert isinstance(gvar, variables.VariableTracker) + assert isinstance(value, variables.VariableTracker) + self.store_attr(gvar, name, value) + + @staticmethod + def cls_supports_mutation_side_effects(cls: type) -> bool: + return inspect.getattr_static(cls, "__getattribute__", None) in ( + object.__getattribute__, + dict.__getattribute__, + set.__getattribute__, + frozenset.__getattribute__, + int.__getattribute__, + str.__getattribute__, + list.__getattribute__, + tuple.__getattribute__, + BaseException.__getattribute__, + ) + + def is_attribute_mutation(self, item: VariableTracker) -> bool: + return isinstance(item.mutation_type, AttributeMutation) + + def has_pending_mutation(self, item: VariableTracker) -> bool: + return self.is_attribute_mutation(item) and bool( + self.store_attr_mutations.get(item) + ) + + def has_pending_mutation_of_attr(self, item: VariableTracker, name: str) -> bool: + return self.is_attribute_mutation( + item + ) and name in self.store_attr_mutations.get(item, ()) + + def is_modified(self, item: VariableTracker) -> bool: + if item.is_immutable(): + return False + if isinstance(item.mutation_type, (AttributeMutationNew, ValueMutationNew)): + return True + + if isinstance(item, variables.UserDefinedObjectVariable): + # Checks if the underlying dict or tuple vt has been modified + return item in self.store_attr_mutations or item.is_base_vt_modified(self) + + if self.is_attribute_mutation(item): + return item in self.store_attr_mutations + assert item.mutation_type is not None + return item.mutation_type.is_modified # type: ignore[attr-defined] + + def _track_obj( + self, + item: Any, + variable: VariableTracker, + mutation_type_cls: type = ValueMutationExisting, + ) -> VariableTracker: + """Start tracking an existing or new variable for mutation""" + if id(item) in self.id_to_variable: + raise AssertionError( + f"{variable} is already tracked for mutation. This could be " + "because you are not using VariableBuilder to construct " + "the variable tracker. " + f"Source of new object: {variable.source}. " + f"Source of previously tracked object: {self.id_to_variable[id(item)].source}." + ) + + variable.mutation_type = mutation_type_cls() + self.id_to_variable[id(item)] = variable + self.keepalive.append(item) + return variable + + track_mutable = _track_obj + + def track_object_existing( + self, + item: Any, + variable: VariableTracker, + ) -> VariableTracker: + # TODO: Modify this API so that we preserve type info of + # variable + return self._track_obj( + item, + variable, + mutation_type_cls=AttributeMutationExisting, + ) + + def track_object_new( + self, + cls_source: Source | None, + user_cls: Any, + variable_cls: Any, + options: dict[str, Any], + ) -> VariableTracker: + if user_cls is torch.autograd.function.FunctionCtx: + with warnings.catch_warnings(record=True): + obj = torch.autograd.Function() + else: + obj = object_new(user_cls) + variable = variable_cls( + obj, + mutation_type=AttributeMutationNew(cls_source), + **options, + ) + self.id_to_variable[id(obj)] = variable + self.keepalive.append(obj) + return variable + + def get_variable_cls(self, user_cls: type) -> type: + from torch.overrides import TorchFunctionMode + + from .variables.ctx_manager import GenericContextWrappingVariable + from .variables.torch_function import TorchFunctionModeVariable + from .variables.user_defined import is_forbidden_context_manager + + variable_cls: type[variables.UserDefinedObjectVariable] = ( + variables.UserDefinedObjectVariable + ) + if issubclass( + user_cls, TorchFunctionMode + ) and TorchFunctionModeVariable.is_supported_torch_function_mode(user_cls): + variable_cls = TorchFunctionModeVariable + elif ( + hasattr(user_cls, "__enter__") + and hasattr(user_cls, "__exit__") + and not is_forbidden_context_manager(user_cls) + ): + variable_cls = GenericContextWrappingVariable + elif issubclass(user_cls, torch.nn.Module): + variable_cls = variables.UnspecializedNNModuleVariable + elif issubclass(user_cls, (dict, collections.OrderedDict)): + variable_cls = variables.UserDefinedDictVariable + elif issubclass(user_cls, (set, frozenset)): + variable_cls = variables.UserDefinedSetVariable + elif issubclass(user_cls, tuple): + if is_namedtuple_cls(user_cls): + variable_cls = variables.UserDefinedTupleVariable.get_vt_cls(user_cls) + else: + variable_cls = variables.UserDefinedTupleVariable + elif issubclass(user_cls, list): + variable_cls = variables.UserDefinedListVariable + elif issubclass(user_cls, MutableMapping): + variable_cls = variables.MutableMappingVariable + elif is_frozen_dataclass(user_cls): + variable_cls = FrozenDataClassVariable + elif issubclass(user_cls, BaseException): + variable_cls = variables.UserDefinedExceptionObjectVariable + elif issubclass( + user_cls, + variables.user_defined._CONSTANT_BASE_TYPES, + ): + variable_cls = variables.UserDefinedConstantVariable + elif variables.InspectVariable.is_matching_class(user_cls): + variable_cls = variables.InspectVariable + assert issubclass(variable_cls, variables.UserDefinedObjectVariable) + return variable_cls + + def get_example_value( + self, + base_cls_vt: VariableTracker, + cls_vt: VariableTracker, + init_args: list[VariableTracker], + ) -> Any: + user_cls = cls_vt.value # type: ignore[attr-defined] + if issubclass(user_cls, torch.nn.Module): + # TODO(anijain2305) - Is it possible to remove this specialization? + obj = nn_module_new(user_cls) + else: + if isinstance(base_cls_vt, variables.BuiltinVariable): + base_cls = base_cls_vt.fn + elif isinstance(base_cls_vt, variables.DictBuiltinVariable): + base_cls = dict + elif isinstance(base_cls_vt, variables.ListBuiltinVariable): + base_cls = list + elif isinstance(base_cls_vt, variables.UserDefinedClassVariable): + base_cls = base_cls_vt.value + else: + raise RuntimeError(f"Unexpected base_cls_vt {base_cls_vt}") + + assert variables.UserDefinedClassVariable.is_supported_new_method( + base_cls.__new__ + ) + if is_structseq_class(user_cls): + # Structseq tp_new requires a sequence argument and rejects + # tuple.__new__, so create a dummy with None placeholders. + obj = user_cls([None] * user_cls.n_fields) + elif init_args and issubclass( + user_cls, + variables.user_defined._CONSTANT_BASE_TYPES, + ): + example_args = [arg.as_python_constant() for arg in init_args] + try: + obj = base_cls.__new__( # pyrefly: ignore[bad-specialization] + user_cls, *example_args + ) + except Exception: + # __new__ can raise (e.g., exceeding int str digit limits). + # Fall back to creating without args — the example value is + # only used for tracing, not for correctness. + obj = base_cls.__new__( # pyrefly: ignore[bad-specialization] + user_cls + ) + else: + obj = base_cls.__new__(user_cls) + return obj + + def track_new_user_defined_object( + self, + base_cls_vt: VariableTracker, + cls_vt: VariableTracker, + init_args: list[VariableTracker], + ) -> VariableTracker: + """ + Creates a UserDefinedObjectVariable (or its subclass) variable tracker + and mark it for attribute mutation tracking. + + Also records the variable trackers to call __new__ method on + reconstruction. Roughly, the reconstruction looks like this + base_cls_vt.__new__(user_cls, *init_args) + """ + cls_source = cls_vt.source + user_cls = cls_vt.value # type: ignore[attr-defined] + variable_cls = self.get_variable_cls(user_cls) + obj = self.get_example_value(base_cls_vt, cls_vt, init_args) + + variable = variable_cls( + obj, + cls_source=cls_vt.source, + base_cls_vt=base_cls_vt, + init_args=init_args, + mutation_type=AttributeMutationNew(cls_source), + ) + self.id_to_variable[id(obj)] = variable + self.keepalive.append(obj) + return variable + + def track_cell_new( + self, + ) -> VariableTracker: + obj = object() + variable = variables.CellVariable( + mutation_type=AttributeMutationNew(), + ) + self.id_to_variable[id(obj)] = variable + self.keepalive.append(obj) + return variable + + def track_cell_existing( + self, source: Source | None, cell: CellType, contents: VariableTracker + ) -> VariableTracker: + variable = variables.CellVariable( + # We don't support mutation to cell without source because we need + # source to properly codegen the mutations. + mutation_type=None if source is None else AttributeMutationExisting(), + pre_existing_contents=contents, + source=source, + ) + self.id_to_variable[id(cell)] = variable + self.keepalive.append(cell) + return variable + + def track_global_existing(self, source: Source, item: Any) -> VariableTracker: + variable = variables.NewGlobalVariable( + mutation_type=AttributeMutationExisting(), + source=source, + ) + self.id_to_variable[id(item)] = variable + self.keepalive.append(item) + return variable + + def track_save_for_backward( + self, ctx: VariableTracker, args: list[VariableTracker] + ) -> None: + assert isinstance(ctx, variables.AutogradFunctionContextVariable) + self.save_for_backward.append((ctx, args)) + + def track_runahead_tensor_and_symvar_side_effects( + self, other: "SideEffects" + ) -> None: + # In higher order ops we want to keep track of tensors seen in the + # speculate_subgraph so that we don't lift them again as a new input in + # other speculate_subgraph or in the root tracer. + for other_item in other.keepalive: + other_id = id(other_item) + other_variable = other.id_to_variable[other_id] + if other_id not in self.id_to_variable and isinstance( + other_variable, (variables.TensorVariable, variables.SymNodeVariable) + ): + self.track_object_existing(other_item, other_variable) + + def prune_dead_object_new(self, tx: "InstructionTranslatorBase") -> None: + # Avoid VT cycles from e.g., recursive function. + visited: set[VariableTracker] = set() + live_new_objects: set[VariableTracker] = set() + + def visit(var: VariableTracker) -> None: + if var in visited: + return + visited.add(var) + # Object may have been mutated, store this mutation. + if isinstance(var.mutation_type, AttributeMutationNew): + live_new_objects.add(var) + # It's possible that we have mutated the value of this variable + # to be another one. The new value is in store_attr_mutations. + # Also recurse through the new value to detect alive AttributeMutationNew. + if var in self.store_attr_mutations: + VariableTracker.visit( + visit, # noqa: F821 + self.store_attr_mutations[var], + ) + + def is_live(var: VariableTracker) -> bool: + if isinstance(var.mutation_type, AttributeMutationNew): + return var in live_new_objects + return True + + pre_existing_vars = [ + var + for var in self.id_to_variable.values() + if not isinstance(var.mutation_type, AttributeMutationNew) + ] + + # The only live side effects come from returns (tx.stack), any intermediates + # during a graph break (tx.symbolic_locals), and mutation on pre-existing variables. + # Recursively visit Variables and see if any of them have been mutated. + init_live_vars = [] + # gather stack/symbolic_locals for all tx's up the chain + cur_tx: InstructionTranslatorBase | None = tx + while cur_tx is not None: + init_live_vars.extend([cur_tx.stack, cur_tx.symbolic_locals]) + if cur_tx.parent is not None: + # for non-root tx'es, also keep the cells/freevars alive so they get codegen'd properly + # TODO see if we could prune dead cells - cell pruning information needs to be forwarded + # to the resume function creation as well. + assert cur_tx.post_prune_cell_and_freevars is not None + init_live_vars.append(cur_tx.post_prune_cell_and_freevars) + cur_tx = cur_tx.parent + VariableTracker.visit( + visit, + # TODO track from all possible sources. + init_live_vars + + [ + pre_existing_vars, + tx.output.backward_state, + self.tensor_hooks, + ], + ) + # Manually release the self-referential function, which indirectly + # captures certain `VariableTracker` and affects parts of PT test/logic + # that are sensitive to when certain objects get released. + del visit + + # NB: cell variable handling.is tricky. + # cell variables must stay alive if any NestedUserFunctionVariable + # are live. "visit"-ing the NestedUserFunctionVariable visits + # the .closures field, from which we will see if we need to keep + # any mutations to cell variables alive. + + self.id_to_variable = { + k: v for k, v in self.id_to_variable.items() if is_live(v) + } + self.store_attr_mutations = { + k: v for k, v in self.store_attr_mutations.items() if is_live(k) + } + + def mutation(self, var: VariableTracker) -> None: + if var in self.ignore_mutation_on_these_variables: + return + + self.check_allowed_side_effect(var) + # Capture user stack for this mutation + self._capture_user_stack(var) + + if isinstance(var.mutation_type, ValueMutationExisting): + var.mutation_type.is_modified = True + if var.source is not None: + self.mutated_sources.add(var.source) + if var.source and isinstance(var, variables.ConstDictVariable): + self._has_existing_dict_mutation = True + + def has_existing_dict_mutation(self) -> bool: + return self._has_existing_dict_mutation + + def _get_modified_vars(self) -> list[VariableTracker]: + return [var for var in self.id_to_variable.values() if self.is_modified(var)] + + def codegen_save_tempvars(self, cg: PyCodegen) -> None: + # We must codegen modified VT to their source by default, so that + # mutation and aliasing are properly accounted for. + # + # Since newly constructed objects don't have a source, we manually + # codegen their construction and store them to a newly assigned local + # source. Note that `ValueMutationNew` isn't tracked by SideEffects. + for var in self._get_modified_vars(): + if not isinstance(var.mutation_type, AttributeMutationNew): + assert var.source is not None + continue + + # Namedtuples/structseqs with no pending mutations should skip + # codegen_save_tempvars so that restore_stack handles them. In + # export, restore_stack uses value_from_source=False which makes + # child tensors become graph outputs. If we processed them here, + # add_cache would assign a TempLocalSource and restore_stack would + # load from cache with value_from_source=True, hiding the tensors + # from export. + if isinstance( + var, + (variables.NamedTupleVariable, variables.StructSequenceVariable), + ) and not self.has_pending_mutation(var): + continue + + if isinstance(var, variables.CellVariable): + # Cells created in the root frame are created either by + # `MAKE_CELL` or by them being in `co_cellvars`, so we only emit + # `make_cell` for the non-root-frame cells here. + # TODO generalize this so we never need to call `make_cell`. + if var.local_name is None: + cg.add_push_null( + lambda: cg.load_import_from(utils.__name__, "make_cell") + ) + cg.extend_output(create_call_function(0, False)) + cg.add_cache(var) + var.source = TempLocalSource(cg.tempvars[var]) # type: ignore[attr-defined] + elif var.source is None: + var.source = LocalCellSource(var.local_name) + elif var.is_tensor(): + # NOTE: for historical reasons we never assigned local sources + # to newly constructed tensor object, so we keep it that way. + # They are always loaded from output of the fx graph, so one can + # think of it as having a "OutputGraphSource" for codegen + # purposes. + # + # However, tensor subclass objects are different, because the + # reconstruction logic in `PyCodegen` loads the data tensor from + # graph output and then calls `as_subclass`, meaning we must + # assign a source to it to ensure we only reconstruct one + # subclass instance. + if isinstance( + var, variables.torch_function.TensorWithTFOverrideVariable + ): + # Don't codegen from temp source assigned from the 1st pass. + cg(var, allow_cache=False) + cg.add_cache(var) + # `add_cache` generates STORE and consumes TOS, but we never + # cleared it. TODO move this call into `add_cache` + cg.clear_tos() + var.source = TempLocalSource(cg.tempvars[var]) + elif isinstance(var, variables.AutogradFunctionContextVariable): + unimplemented( + gb_type="AutogradFunctionContextVariable escaped Dynamo-traced region", + context="", + explanation="We cannot reconstruct a torch.autograd.Function's context object.", + hints=[], + ) + else: + # Reconstruct the bytecode for + # base_cls.__new__(user_cls, *args) + if isinstance(var, variables.UserDefinedObjectVariable): + + def load_new_method() -> None: + # pyrefly: ignore [missing-attribute] + assert var.base_cls_vt is not None + cg(var.base_cls_vt) # type: ignore[attr-defined] + cg.extend_output([cg.create_load_attr("__new__")]) + + cg.add_push_null(load_new_method) + else: + cg.add_push_null( + lambda: cg.load_import_from(utils.__name__, "object_new") + ) + assert var.mutation_type.cls_source is not None + cg(var.mutation_type.cls_source) + + # Generate the args to the __new__ method + for arg in var.init_args: # type: ignore[attr-defined] + cg(arg) + + # Call the __new__ method + cg.extend_output(create_call_function(1 + len(var.init_args), False)) # type: ignore[attr-defined] + + cg.add_cache(var) + var.source = TempLocalSource(cg.tempvars[var]) + + # For frozen dataclasses, we must emit object.__setattr__ + # immediately after __new__ — before any other code can + # access the object. The suffix-based codegen in + # codegen_update_mutated runs too late: if intervening code + # calls __repr__ (e.g. f-strings), the attributes won't be + # set yet. + if ( + isinstance(var, variables.FrozenDataClassVariable) + and var in self.store_attr_mutations + ): + for name, value in self.store_attr_mutations[var].items(): + cg.load_import_from("builtins", "object") + cg.load_method("__setattr__") + cg(var.source) + cg(variables.ConstantVariable(name)) + cg(value) + cg.extend_output( + [*create_call_method(3), create_instruction("POP_TOP")] + ) + + for ctx, args in self.save_for_backward: + cg(ctx.source) + cg.load_method("save_for_backward") + for arg in args: + cg(arg) + cg.extend_output( + [ + *create_call_method(len(args)), + create_instruction("POP_TOP"), + ] + ) + + def register_hook( + self, + tensor: "variables.TensorVariable", + hook: VariableTracker, + handle: "variables.RemovableHandleVariable", + name: str, + ) -> None: + assert tensor.is_tensor() + assert isinstance(hook, variables.VariableTracker) + assert ( + isinstance(handle, variables.RemovableHandleVariable) + and handle.is_mutable() + ) + assert hasattr(torch.Tensor, name) + idx = len(self.tensor_hooks.keys()) + # duplicate index possible because of self.remove_hook() + while idx in self.tensor_hooks: + idx += 1 + self.tensor_hooks[idx] = (tensor, hook, handle, name) + assert not handle.idx + handle.idx = idx + + def remove_hook(self, idx: int) -> None: + del self.tensor_hooks[idx] + + def codegen_hooks(self, cg: PyCodegen) -> None: + for ( + tensor, + hook, + handle, + name, + ) in self.tensor_hooks.values(): + # Note: [On tensor.register_hook] + # + # register_hook on a tensor, AKA backward hooks, have slightly nuanced differences in how they are implemented + # when it comes to hooks on objects with sources (inputs, params) vs objects without sources (intermediaries). + # + # For tensors with a source, we bypass direct inclusion of register_hook calls in the graph. + # Instead, these are tracked and stashed as a global variable, enabling their association with tensors in + # the residuals. During dynamo's frame creation, these hooks are invoked seamlessly on known reconstructible/fetch-able + # tensors. Because a source indicates knowledge of this object outside the torch compile region, and + # because we are running residuals firmly before .backward() can be run, it is sound to invoke + # `register_hook` on a known tensor. + # + # For tensors without a source, we support a limited subset of hooks. Global functions only, and + # compiled_autograd must be enabled or we will graph break. + # + # Handling the Handle: When a user retains the register_hook result in a handle, we intercept the + # STORE_FAST operation to record the user-designated local variable name. This ensures the reconstructed + # bytecode retains this name. If no handle is defined, we simply pop the generated value to keep the + # stack intact. + # + # Dynamo Tensor Hooks Workflow: + # - Functions passed to register_hook are lifted globally. + # - For tensors with sources: + # - In the "side_effects" phase of codegen, we iterate over tensors with hooks to: + # - Generate the tensor. + # - Issue a register_hook call on the tensor, linking to the globally stored function. + # - Incorporate a handle if one was established in the eager phase. + # - For tensors without sources: + # - We don't generate any instructions for registering a hook. + # - Handles from intermediary hooks are NYI. + # - We produce a call function that utilizes the trace_wrapped higher order op, closing over it. + # - We then manually insert the call function above into the graph. + # - The handle's exact user-specified name, "user_code_variable_name", is discerned and associated during STORE_FAST. + assert tensor.source, "Hooks on non input tensors NYI - should not get here" + + def gen_fn() -> None: + cg(tensor) + cg.extend_output([cg.create_load_attr(name)]) + + cg.add_push_null(gen_fn) + cg(hook) + cg.extend_output(create_call_function(1, False)) + + # Adding the handle to the cache means RemovableHandleVariable().reconstruct() will + # be associated with the return value of register_hook(). This consumes the top of stack. + cg.add_cache(handle) + + def get_ca_final_callbacks_var(self) -> "variables.ListVariable": + from .variables.base import ValueMutationNew + + if self.ca_final_callbacks_var is None: + self.ca_final_callbacks_var = variables.ListVariable( + [], mutation_type=ValueMutationNew() + ) + + return self.ca_final_callbacks_var + + def _format_side_effect_message(self, var: VariableTracker) -> str: + """Format a side effect log message with user stack.""" + assert config.side_effect_replay_policy != "silent" + locations = self.mutation_user_stacks.get(var, []) + description = f"Mutating object of type {var.python_type_name()}" + source_info = " (no source)" + if var.source is not None: + if isinstance(var.source, TempLocalSource): + source_info = " (source: created in torch.compile region)" + elif isinstance(var, variables.CellVariable) and var.local_name is not None: + source_info = f" (source: {var.local_name})" + elif isinstance( + var, variables.torch_function.TorchFunctionModeStackVariable + ): + source_info = " (source: torch function mode stack mutation)" + else: + # NOTE: NotImplementedError from var.source.name is a bug and must be fixed! + source_info = f" (source name: {var.source.name})" + + if locations: + # Format and dedupe stacks using tuple representation for efficiency + seen = set() + unique_formatted_stacks: list[str] = [] + stack_above_dynamo = collapse_resume_frames(get_stack_above_dynamo()) + for stack in locations: + # Use tuple of frame info for fast deduplication + # Include position info (colno, end_lineno, end_colno) to distinguish + # multiple mutations on the same line (when available in Python 3.11+) + stack_tuple = tuple( + ( + f.filename, + f.lineno, + f.name, + f.line, + getattr(f, "colno", None), + getattr(f, "end_lineno", None), + getattr(f, "end_colno", None), + ) + for f in stack + ) + if stack_tuple not in seen: + seen.add(stack_tuple) + stack_augmented = collapse_resume_frames(stack_above_dynamo + stack) + unique_formatted_stacks.append( + "".join(traceback.format_list(stack_augmented)) + ) + formatted_lines: str = "\n********\n\n".join(unique_formatted_stacks) + log_str = f"{description}{source_info}\n\n{textwrap.indent(formatted_lines, ' ')}" + else: + log_str = ( + f"{description}{source_info} (unable to find user stacks for mutations)" + ) + + return log_str + + def _emit_side_effect_messages(self, side_effect_messages: list[str]) -> None: + if not side_effect_messages: + return + + for msg in side_effect_messages: + side_effects_log.debug(msg) + + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "dynamo_side_effects", + "encoding": "string", + }, + payload_fn=lambda: "\n\n========================================\n\n".join( + side_effect_messages + ), + ) + + def codegen_update_mutated( + self, cg: PyCodegen, log_side_effects: bool = False + ) -> None: + side_effect_messages: list[str] = [] + + # NOTE: should only be called once per VT - only if a side effect actually gets codegen'd! + def _maybe_log_side_effect(var: VariableTracker) -> None: + if config.side_effect_replay_policy != "silent" and log_side_effects: + msg = self._format_side_effect_message(var) + side_effect_messages.append(msg) + + suffixes = [] + for var in self._get_modified_vars(): + # When replay_side_effects=False, only update variables with TempLocalSource + if not config.replay_side_effects and not isinstance( + var.source, TempLocalSource + ): + continue + if isinstance(var, variables.ListVariable): + # old[:] = new + cg(var, allow_cache=False) # Don't codegen via source + cg(var.source) # type: ignore[attr-defined] + cg.extend_output( + [ + cg.create_load_const(None), + cg.create_load_const(None), + create_instruction("BUILD_SLICE", arg=2), + ] + ) + suffixes.append([create_instruction("STORE_SUBSCR")]) + _maybe_log_side_effect(var) + elif isinstance(var, variables.lists.DequeVariable): + # For limited maxlen, the order of operations matter for side + # effect, but we currently don't track the order, so no support. + if not var.maxlen.is_constant_none(): + unimplemented( + gb_type="Side effect on existing deque with limited maxlen", + context="", + explanation="This is not supported.", + hints=[ + "Don't use a deque with `maxlen` specified.", + ], + ) + + # old.extend(new), this runs last + cg(var.source) + cg.load_method("extend") + cg(var, allow_cache=False) # Don't codegen via source + suffixes.append( + [ + *create_call_method(1), + create_instruction("POP_TOP"), + ] + ) + + # old.clear(), this runs first + cg(var.source) + cg.load_method("clear") + suffixes.append( + [ + *create_call_method(0), + create_instruction("POP_TOP"), + ] + ) + _maybe_log_side_effect(var) + + elif isinstance(var, (variables.ConstDictVariable, variables.SetVariable)): + # Reconstruct works as follow: + # (1) Skip codegen if there are no new items + # (2) codegen(...) each pair of key/value + # (3) create a new dictionary with the pairs of key/values above + # (4) clear the original dictionary + # + only if a key was removed from the input dict + # (5) update the original dictionary with the dict created in (2) + + if var.has_new_items(): + cg(var.source) # type: ignore[attr-defined] + cg.load_method("update") + cg(var, allow_cache=False) # Don't codegen via source + + if var.should_reconstruct_all: + cg(var.source) # type: ignore[attr-defined] + cg.load_method("clear") + + suffixes.append( + [ + *create_call_method(1), # update + create_instruction("POP_TOP"), + ] + ) + + if var.should_reconstruct_all: + # clear will appear before "update" as the suffixes are + # applied in reverse order. + suffixes.append( + [ + *create_call_method(0), # clear + create_instruction("POP_TOP"), + ] + ) + _maybe_log_side_effect(var) + + elif isinstance( + var, variables.torch_function.TorchFunctionModeStackVariable + ): + # Needed in the finally block for stack restoration + cg.add_push_null( + lambda: cg.load_import_from( + utils.__name__, "get_torch_function_mode_stack" + ) + ) + cg.call_function(0, False) + name = variables.torch_function.get_prev_stack_var_name() + cg.code_options["co_varnames"] += (name,) + cg.append_output(create_instruction("STORE_FAST", argval=name)) + cg.add_push_null( + lambda: cg.load_import_from( + utils.__name__, "set_torch_function_mode_stack" + ) + ) + + cg.foreach(var.symbolic_stack) + cg.append_output( + create_instruction("BUILD_LIST", arg=len(var.symbolic_stack)) + ) + cg.call_function(1, False) + cg.append_output(create_instruction("POP_TOP")) + _maybe_log_side_effect(var) + + elif isinstance(var, variables.CellVariable) and var.local_name is not None: + # Emit more readable and performant bytecode. + # TODO generalize this for cells created during inlining. + if var in self.store_attr_mutations: + contents_var = self.load_cell(var) + cg(contents_var) + suffixes.append([cg.create_store_deref(var.local_name)]) + _maybe_log_side_effect(var) + + elif self.is_attribute_mutation(var): + # FrozenDataClassVariable attributes were emitted in + # codegen_save_tempvars right after __new__. Skip here to + # avoid double-emitting. + if isinstance(var.mutation_type, AttributeMutationNew) and isinstance( + var, variables.FrozenDataClassVariable + ): + continue + + if isinstance( + var, + variables.UserDefinedDictVariable, + ) and self.is_modified( + var._base_vt # pyrefly: ignore[bad-argument-type] + ): + # Do dict related update manually here. The store_attr + # mutations will be applied later. + varname_map = {} + for name in _manual_dict_setitem.__code__.co_varnames: + varname_map[name] = cg.tx.output.new_var() + + try: + mro_index = type(var.value).__mro__.index( + collections.OrderedDict + ) + except ValueError: + mro_index = type(var.value).__mro__.index(dict) + + cg.extend_output( + [ + create_instruction("LOAD_CONST", argval=mro_index), + create_instruction( + "STORE_FAST", argval=varname_map["mro_index"] + ), + ] + ) + + cg(var.source) # type: ignore[attr-defined] + cg.extend_output( + [ + create_instruction( + "STORE_FAST", argval=varname_map["dict_to"] + ) + ] + ) + + cg(var._base_vt, allow_cache=False) # Don't codegen via source + cg.extend_output( + [ + create_instruction( + "STORE_FAST", argval=varname_map["dict_from"] + ) + ] + ) + + dict_update_insts = bytecode_from_template( + _manual_dict_setitem, varname_map=varname_map + ) + + suffixes.append( + [ + *dict_update_insts, + create_instruction("POP_TOP"), + ] + ) + _maybe_log_side_effect( + var._base_vt # pyrefly: ignore[bad-argument-type] + ) + elif isinstance( + var, + variables.UserDefinedListVariable, + ) and self.is_modified( + var._base_vt # pyrefly: ignore[bad-argument-type] + ): + # Update the list to the updated items. Be careful in + # calling the list methods and not the overridden methods. + varname_map = {} + for name in _manual_list_update.__code__.co_varnames: + varname_map[name] = cg.tx.output.new_var() + + cg(var.source) # type: ignore[attr-defined] + cg.extend_output( + [ + create_instruction( + "STORE_FAST", argval=varname_map["list_to"] + ) + ] + ) + + cg(var._base_vt, allow_cache=False) # Don't codegen via source + cg.extend_output( + [ + create_instruction( + "STORE_FAST", argval=varname_map["list_from"] + ) + ] + ) + + list_update_insts = bytecode_from_template( + _manual_list_update, varname_map=varname_map + ) + + suffixes.append( + [ + *list_update_insts, + create_instruction("POP_TOP"), + ] + ) + _maybe_log_side_effect( + var._base_vt # pyrefly: ignore[bad-argument-type] + ) + + # Applying mutations involves two steps: 1) Push all + # reconstructed objects onto the stack. 2) Call STORE_ATTR to + # apply the mutations. + # + # Dynamo must ensure that mutations are applied in the same + # order as in the original program. Therefore, two reverse + # operations occur below. + # + # The first reverse operation concerns `suffixes`. We apply + # suffixes in reverse order due to the way Python handles the + # stack. In Step 1, we push all reconstructed objects onto the + # stack, but the item at the top of the stack refers to the last + # attribute in the mutation order. If not fixed, this will apply + # the mutations of attributes in the reverse order. To account + # for this reversal, we iterate through the mutable attributes + # in reverse order. + side_effect_occurred = False + for name, value in reversed( + self.store_attr_mutations.get(var, {}).items() + ): + if isinstance(var, variables.NewGlobalVariable): + cg.tx.output.update_co_names(name) + cg(value) + assert isinstance(var.source, GlobalSource) # type: ignore[attr-defined] + suffixes.append( + [create_instruction("STORE_GLOBAL", argval=name)] + ) + side_effect_occurred = True + elif isinstance(value, variables.DeletedVariable): + if isinstance( + var.mutation_type, AttributeMutationExisting + ) and hasattr(getattr(var, "value", None), name): + cg.tx.output.update_co_names(name) + cg(var.source) + suffixes.append( + [create_instruction("DELETE_ATTR", argval=name)] + ) + side_effect_occurred = True + elif isinstance( + var, variables.UserDefinedObjectVariable + ) and var.should_skip_descriptor_setter(name): + cg.add_push_null( + lambda: cg.load_import_from( + utils.__name__, "object_setattr_ignore_descriptor" + ) + ) + cg(var.source) # type: ignore[attr-defined] + cg(variables.ConstantVariable(name)) + cg(value) + suffixes.append( + [ + *create_call_function(3, False), + create_instruction("POP_TOP"), + ] + ) + side_effect_occurred = True + elif ( + isinstance(var, variables.UserDefinedObjectVariable) + and var.needs_slow_setattr() + ): + # __setattr__ is defined on this object, so call object.__setattr__ directly + cg.load_import_from("builtins", "object") + cg.load_method("__setattr__") + cg(var.source) # type: ignore[attr-defined] + cg(variables.ConstantVariable(name)) + cg(value) + suffixes.append( + [*create_call_method(3), create_instruction("POP_TOP")] + ) + side_effect_occurred = True + else: + cg.tx.output.update_co_names(name) + cg(value) + cg(var) + suffixes.append([create_instruction("STORE_ATTR", argval=name)]) + side_effect_occurred = True + + if side_effect_occurred: + _maybe_log_side_effect(var) + elif isinstance(var, variables.ListIteratorVariable): + for _ in range(var.index): + cg.add_push_null( + lambda: cg.load_import_from(utils.__name__, "iter_next") + ) + cg(var.source) # type: ignore[attr-defined] + cg.call_function(1, False) + cg.pop_top() + _maybe_log_side_effect(var) + elif isinstance(var, variables.CountIteratorVariable): + for _ in range(var.advance_count): + cg.add_push_null( + lambda: cg.load_import_from(utils.__name__, "iter_next") + ) + cg(var.source) # type: ignore[attr-defined] + cg.call_function(1, False) + cg.pop_top() + _maybe_log_side_effect(var) + elif isinstance(var, variables.RandomVariable): + # set correct random seed state + def gen_fn() -> None: + cg(var.source) # type: ignore[attr-defined] + cg.load_attr("setstate") + + cg.add_push_null(gen_fn) + cg(var.wrap_state(var.random.getstate())) + + suffixes.append( + [ + *create_call_function(1, False), # setstate + create_instruction("POP_TOP"), + ] + ) + _maybe_log_side_effect(var) + else: + raise AssertionError(type(var)) + + # do all the actual mutations at the very end to handle dependencies + for suffix in reversed(suffixes): + cg.extend_output(suffix) + + # Send batched structured trace for all side effects in this compilation + if log_side_effects and side_effect_messages: + self._emit_side_effect_messages(side_effect_messages) + + def log_side_effects_summary(self) -> None: + if config.side_effect_replay_policy == "silent": + return + if not side_effects_log.isEnabledFor(logging.DEBUG): + return + for var in self._get_modified_vars(): + msg = self._format_side_effect_message(var) + side_effects_log.debug(msg) + + def is_empty(self) -> bool: + return not ( + any(map(self.is_modified, self.id_to_variable.values())) + or self.tensor_hooks + or self.save_for_backward + or self.tensor_hooks + ) + + def clear(self) -> None: + self.keepalive.clear() + self.id_to_variable.clear() + + +@contextlib.contextmanager +def allow_side_effects_in_hop( + tx: "InstructionTranslatorBase", +) -> Generator[None, None, None]: + """Context manager to temporarily allow side effects with extra outputs. + + This is used for special cases (like FSDP functions) that need to perform + side effects even when the general policy is to disallow them. + """ + orig_val = tx.output.current_tracer.allow_side_effects_in_hop + try: + tx.output.current_tracer.allow_side_effects_in_hop = True + yield + finally: + tx.output.current_tracer.allow_side_effects_in_hop = orig_val + + +@contextlib.contextmanager +def allow_externally_visible_side_effects_in_subtracer( + tx: "InstructionTranslatorBase", +) -> Generator[None, None, None]: + orig_val = tx.output.current_tracer.unsafe_allow_externally_visible_side_effects + try: + tx.output.current_tracer.unsafe_allow_externally_visible_side_effects = True + tx.output.current_tracer.traced_with_externally_visible_side_effects = True + yield + finally: + tx.output.current_tracer.unsafe_allow_externally_visible_side_effects = orig_val + + +@contextlib.contextmanager +def disallow_side_effects_in_generator( + tx: "InstructionTranslatorBase", +) -> Generator[None, None, None]: + orig_val = tx.output.current_tracer.is_reconstructing_generator + try: + tx.output.current_tracer.is_reconstructing_generator = True + yield + finally: + tx.output.current_tracer.is_reconstructing_generator = orig_val diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/source.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/source.py new file mode 100644 index 0000000000000000000000000000000000000000..3f9453f95c585f0ce31a662883fe8530e0a4750d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/source.py @@ -0,0 +1,1297 @@ +""" +This module provides Source classes that track the origins of values in PyTorch Dynamo. +Sources represent where values come from (e.g. local variables, globals, attributes) and +are used for guard generation and code reconstruction during compilation. + +The module includes specialized sources for: +- Local variables and synthetic locals +- Global variables and constants +- Object attributes and method calls +- NN module specialization (specialized vs unspecialized) +- Random values and tensor properties +- Default argument handling +- FSDP (Fully Sharded Data Parallel) modules + +Sources play a key role in Dynamo's guard system by tracking value origins for +guard generation, and in code reconstruction by providing methods to rebuild +the code needed to recreate values. +""" + +import dataclasses +import enum +import functools +from collections.abc import Callable +from typing import Any, TYPE_CHECKING + +from torch import device as device_type +from torch._guards import ( + ChainedSource, + dataclass_with_cached_hash, + Guard, + GuardSource, + Source, +) + +from . import utils +from .bytecode_transformation import ( + create_binary_subscr, + create_build_tuple, + create_call_function, +) + + +if TYPE_CHECKING: + from .codegen import PyCodegen + +# It shouldn't be supported to construct an NNModuleVariable inside an FSDP module, +# so those cases are omitted intentionally + +# represents nn.Modules tracked with NNModuleVariable (specialized is implicit in the variable name) +_GUARD_SOURCE_SPECIALIZED_NN_MODULE = { + GuardSource.LOCAL: GuardSource.LOCAL_SPECIALIZED_NN_MODULE, + GuardSource.GLOBAL: GuardSource.GLOBAL_SPECIALIZED_NN_MODULE, + GuardSource.LOCAL_SPECIALIZED_NN_MODULE: GuardSource.LOCAL_SPECIALIZED_NN_MODULE, + GuardSource.GLOBAL_SPECIALIZED_NN_MODULE: GuardSource.GLOBAL_SPECIALIZED_NN_MODULE, + # Just to ensure that guard_source() works + GuardSource.LOCAL_UNSPECIALIZED_NN_MODULE: GuardSource.LOCAL_UNSPECIALIZED_NN_MODULE, + GuardSource.GLOBAL_UNSPECIALIZED_NN_MODULE: GuardSource.GLOBAL_UNSPECIALIZED_NN_MODULE, + GuardSource.LOCAL_UNSPECIALIZED_BUILTIN_NN_MODULE: GuardSource.LOCAL_UNSPECIALIZED_BUILTIN_NN_MODULE, + GuardSource.GLOBAL_UNSPECIALIZED_BUILTIN_NN_MODULE: GuardSource.GLOBAL_UNSPECIALIZED_BUILTIN_NN_MODULE, + GuardSource.LOCAL_FSDP_MODULE: GuardSource.LOCAL_FSDP_MODULE, + GuardSource.GLOBAL_FSDP_MODULE: GuardSource.GLOBAL_FSDP_MODULE, +} + +# represents nn.Modules tracked with UnspecializedNNModuleVariable +_GUARD_SOURCE_UNSPECIALIZED_NN_MODULE = { + GuardSource.LOCAL: GuardSource.LOCAL_UNSPECIALIZED_NN_MODULE, + GuardSource.GLOBAL: GuardSource.GLOBAL_UNSPECIALIZED_NN_MODULE, + GuardSource.LOCAL_UNSPECIALIZED_NN_MODULE: GuardSource.LOCAL_UNSPECIALIZED_NN_MODULE, + GuardSource.GLOBAL_UNSPECIALIZED_NN_MODULE: GuardSource.GLOBAL_UNSPECIALIZED_NN_MODULE, + # this happens for an UnspecializedNNModule submodule on a NNModuleVariable + GuardSource.LOCAL_SPECIALIZED_NN_MODULE: GuardSource.LOCAL_UNSPECIALIZED_NN_MODULE, + GuardSource.GLOBAL_SPECIALIZED_NN_MODULE: GuardSource.GLOBAL_UNSPECIALIZED_NN_MODULE, + # Just to ensure that guard_source() works + GuardSource.LOCAL_UNSPECIALIZED_BUILTIN_NN_MODULE: GuardSource.LOCAL_UNSPECIALIZED_BUILTIN_NN_MODULE, + GuardSource.GLOBAL_UNSPECIALIZED_BUILTIN_NN_MODULE: GuardSource.GLOBAL_UNSPECIALIZED_BUILTIN_NN_MODULE, + GuardSource.LOCAL_FSDP_MODULE: GuardSource.LOCAL_FSDP_MODULE, + GuardSource.GLOBAL_FSDP_MODULE: GuardSource.GLOBAL_FSDP_MODULE, +} + +# represents nn.Modules tracked with UnspecializedBuiltinNNModuleVariable +_GUARD_SOURCE_UNSPECIALIZED_BUILTIN_NN_MODULE = { + GuardSource.LOCAL: GuardSource.LOCAL_UNSPECIALIZED_BUILTIN_NN_MODULE, + GuardSource.GLOBAL: GuardSource.GLOBAL_UNSPECIALIZED_BUILTIN_NN_MODULE, + GuardSource.LOCAL_UNSPECIALIZED_NN_MODULE: GuardSource.LOCAL_UNSPECIALIZED_BUILTIN_NN_MODULE, + GuardSource.GLOBAL_UNSPECIALIZED_NN_MODULE: GuardSource.GLOBAL_UNSPECIALIZED_BUILTIN_NN_MODULE, + GuardSource.LOCAL_SPECIALIZED_NN_MODULE: GuardSource.LOCAL_UNSPECIALIZED_BUILTIN_NN_MODULE, + GuardSource.GLOBAL_SPECIALIZED_NN_MODULE: GuardSource.GLOBAL_UNSPECIALIZED_BUILTIN_NN_MODULE, + # Just to ensure that guard_source() works + GuardSource.LOCAL_UNSPECIALIZED_BUILTIN_NN_MODULE: GuardSource.LOCAL_UNSPECIALIZED_BUILTIN_NN_MODULE, + GuardSource.GLOBAL_UNSPECIALIZED_BUILTIN_NN_MODULE: GuardSource.GLOBAL_UNSPECIALIZED_BUILTIN_NN_MODULE, + GuardSource.LOCAL_FSDP_MODULE: GuardSource.LOCAL_FSDP_MODULE, + GuardSource.GLOBAL_FSDP_MODULE: GuardSource.GLOBAL_FSDP_MODULE, +} + +_GUARD_SOURCE_FSDP_MODULE = { + GuardSource.LOCAL: GuardSource.LOCAL_FSDP_MODULE, + GuardSource.GLOBAL: GuardSource.GLOBAL_FSDP_MODULE, + GuardSource.LOCAL_SPECIALIZED_NN_MODULE: GuardSource.LOCAL_FSDP_MODULE, + GuardSource.GLOBAL_SPECIALIZED_NN_MODULE: GuardSource.GLOBAL_FSDP_MODULE, + GuardSource.LOCAL_FSDP_MODULE: GuardSource.LOCAL_FSDP_MODULE, + GuardSource.GLOBAL_FSDP_MODULE: GuardSource.GLOBAL_FSDP_MODULE, + GuardSource.LOCAL_UNSPECIALIZED_NN_MODULE: GuardSource.LOCAL_FSDP_MODULE, + GuardSource.GLOBAL_UNSPECIALIZED_NN_MODULE: GuardSource.GLOBAL_FSDP_MODULE, + GuardSource.LOCAL_UNSPECIALIZED_BUILTIN_NN_MODULE: GuardSource.LOCAL_FSDP_MODULE, + GuardSource.GLOBAL_UNSPECIALIZED_BUILTIN_NN_MODULE: GuardSource.GLOBAL_FSDP_MODULE, +} + + +def is_constant_source(source: Source) -> bool: + if isinstance(source, ConstantSource): + return True + try: + if source.guard_source == GuardSource.CONSTANT: + return True + except NotImplementedError: + pass + + return False + + +def _get_source_debug_name(source: Source | None) -> str: + if source is None: + return "" + else: + try: + return source.name + except NotImplementedError: + return "" + + +def _esc_str(s: Any, apply_repr: bool = False) -> str: + """ + Escapes curly brackets for format strings. + e.g. "frozenset({0})" becomes "frozenset({{0}})". + This is used by _name_template for example, because it's + expected to return a format string, but we may wish to include + strings that should not be accidentally formatted. + """ + if apply_repr: + s = repr(s) + else: + s = str(s) + return s.replace("{", "{{").replace("}", "}}") + + +@dataclass_with_cached_hash(frozen=True) +class LocalSource(Source): + local_name: str + + # Whether this local is an input to the root frame. + is_input: bool = False + + # Whether we know this input is dynamic (based on example_inputs) + # For non tensors, we simply look at the first index of the tuple + dynamism: frozenset[str] | None = None + + # Whether the item at this source is the _content_ of a cell that is + # dereferenced from the root frame, i.e., it's a part of the `co_cellvars` + # or `co_freevars`. + is_derefed_cell_contents: bool = False + + def reconstruct(self, codegen: "PyCodegen") -> None: + if self.is_derefed_cell_contents: + codegen.load_deref(self.local_name) + else: + codegen.append_output(codegen.create_load(self.local_name)) + + @property + def guard_source(self) -> GuardSource: + return GuardSource.LOCAL + + @functools.cached_property + def _name_template(self) -> str: + return f"L[{_esc_str(self.local_name, apply_repr=True)}]" + + +@dataclass_with_cached_hash(frozen=True) +class TempLocalSource(Source): + # like LocalSource, but cannot be guarded on + local_name: str + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.append_output(codegen.create_load(self.local_name)) + + @property + def guard_source(self) -> GuardSource: + return GuardSource.TEMP_LOCAL + + @property + def _name_template(self) -> str: + raise NotImplementedError( + "Cannot create guard on TempLocalSource - this is an internal Dynamo bug. Please file an issue on GitHub." + ) + + +@dataclass_with_cached_hash(frozen=True) +class SyntheticLocalSource(Source): + local_name: str + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.append_output(codegen.create_load(self.local_name)) + + @property + def guard_source(self) -> GuardSource: + return GuardSource.SYNTHETIC_LOCAL + + @functools.cached_property + def _name_template(self) -> str: + return f"SYNTHETIC_LOCAL[{_esc_str(self.local_name, apply_repr=True)}]" + + +@dataclass_with_cached_hash(frozen=True) +class RandomValueSource(Source): + random_call_index: int + + @property + def guard_source(self) -> GuardSource: + return GuardSource.RANDOM_VALUE + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.append_output(codegen.create_load(codegen.tx.output.random_values_var)) + codegen.append_output(codegen.create_load_const(self.random_call_index)) + codegen.append_output(create_binary_subscr()) + + @functools.cached_property + def _name_template(self) -> str: + return f"random_value_{_esc_str(self.random_call_index)}" + + +@dataclass_with_cached_hash(frozen=True) +class GlobalSource(Source): + global_name: str + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.append_output(codegen.create_load_global(self.global_name, add=True)) + + @property + def guard_source(self) -> GuardSource: + return GuardSource.GLOBAL + + @functools.cached_property + def _name_template(self) -> str: + return f"G[{_esc_str(self.global_name, apply_repr=True)}]" + + +@dataclass_with_cached_hash(frozen=True) +class GlobalWeakRefSource(Source): + global_name: str + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.append_output( + codegen.create_load_global(self.global_name, add=True) + ) + ) + codegen.extend_output(create_call_function(0, False)) + + @property + def guard_source(self) -> GuardSource: + return GuardSource.GLOBAL + + @functools.cached_property + def _name_template(self) -> str: + return f"G[{_esc_str(self.global_name, apply_repr=True)}]()" + + +@dataclass_with_cached_hash(frozen=True) +class WeakRefCallSource(ChainedSource): + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null(lambda: codegen(self.base)) + codegen.extend_output(create_call_function(0, False)) + + @property + def _name_template(self) -> str: + return "{0}()" + + +@dataclass_with_cached_hash(frozen=True) +class CallFunctionNoArgsSource(WeakRefCallSource): + pass + + +@dataclass_with_cached_hash(frozen=True) +class AttrSource(ChainedSource): + member: str + + def __post_init__(self) -> None: + assert self.base, "Can't construct an AttrSource without a valid base source" + assert "." not in self.member, ( + f"AttrSource member must not contain '.', got {self.member!r}. " + "Use OutputGraph.get_chained_attr_source() for dotted paths." + ) + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen(self.base) + codegen.extend_output(codegen.create_load_attrs(self.member)) + + @functools.cached_property + def _name_template(self) -> str: + if not self.member.isidentifier(): + return f"getattr({{0}}, {_esc_str(self.member, apply_repr=True)})" + return f"{{0}}.{_esc_str(self.member)}" + + +@dataclass_with_cached_hash(frozen=True) +class CellContentsSource(AttrSource): + """ + Source for closure cell contents that also stores the freevar name. + This allows guard failure messages to show which variable the closure cell refers to. + """ + + freevar_name: str = dataclasses.field(default="") + + def __post_init__(self) -> None: + assert self.base, ( + "Can't construct a CellContentsSource without a valid base source" + ) + assert self.member == "cell_contents", ( + "CellContentsSource should only be used for cell_contents" + ) + + +@dataclass_with_cached_hash(frozen=True) +class GenericAttrSource(ChainedSource): + member: str + + def __post_init__(self) -> None: + assert self.base, ( + "Can't construct a GenericAttrSource without a valid base source" + ) + assert "." not in self.member, ( + f"GenericAttrSource member must not contain '.', got {self.member!r}. " + "Use OutputGraph.get_chained_attr_source() for dotted paths." + ) + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen(self.base) + codegen.extend_output(codegen.create_load_attrs(self.member)) + + @functools.cached_property + def _name_template(self) -> str: + return ( + f"object.__getattribute__({{0}}, {_esc_str(self.member, apply_repr=True)})" + ) + + +# Represents obj.__dict__ where obj is a type object +@dataclass_with_cached_hash(frozen=True) +class TypeDictSource(ChainedSource): + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen(self.base) + codegen.extend_output(codegen.create_load_attrs("__dict__")) + + @property + def _name_template(self) -> str: + # type(ob).__dict__ can return a proxy of the dict. But in the C++ + # guard accessor, we are use type->tp_dict which is a dict. So, + # forcefully pass a dict object to ensure that the GuardManager + # registers that its working on a dict object. + return "dict({0}.__dict__)" + + +# Represents obj.__mro__ where object is type object +@dataclass_with_cached_hash(frozen=True) +class TypeMROSource(ChainedSource): + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen(self.base) + codegen.extend_output(codegen.create_load_attrs("__mro__")) + + @property + def _name_template(self) -> str: + return "{0}.__mro__" + + +@dataclass_with_cached_hash(frozen=True) +class LocalCellSource(Source): + """ + Conceptually, this class is `LocalSource` for cell objects implicitly + generated by Python (e.g., captured variables). + """ + + local_name: str + + def reconstruct(self, codegen: "PyCodegen") -> None: + # Although `LOAD_FAST` and `LOAD_CLOSURE` have the same semantics, + # Dynamo's bytecode transformation differentiates them slightly, so we + # always emit `LOAD_CLOSURE` here. + codegen.append_output(codegen.create_load_closure(self.local_name)) + + # All the other methods are intentionally unimplemented because e.g., a + # local cell object should never be used for guards. + + +# Represents obj.__code__ where object is type object +@dataclass_with_cached_hash(frozen=True) +class CodeSource(ChainedSource): + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen(self.base) + codegen.extend_output(codegen.create_load_attrs("__code__")) + + @property + def _name_template(self) -> str: + return "{0}.__code__" + + +# Represents obj.__closure__ where object is type object +@dataclass_with_cached_hash(frozen=True) +class ClosureSource(ChainedSource): + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen(self.base) + codegen.extend_output(codegen.create_load_attrs("__closure__")) + + @property + def _name_template(self) -> str: + return "{0}.__closure__" + + +# Represents tensor.grad source. It could be represented by AttrSource as well. +# But, we could access grad field on tensor directly in C++ without going +# through the Python bytecodes. Therefore, we use a separate source for grad +# field. +@dataclass_with_cached_hash(frozen=True) +class GradSource(ChainedSource): + member: str = "grad" + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen(self.base) + codegen.extend_output(codegen.create_load_attrs(self.member)) + + @functools.cached_property + def _name_template(self) -> str: + return f"{{0}}.{_esc_str(self.member)}" + + +@dataclass_with_cached_hash(frozen=True) +class ParamBufferSource(AttrSource): + @functools.cached_property + def guard_source(self) -> GuardSource: + return _GUARD_SOURCE_SPECIALIZED_NN_MODULE[self.base.guard_source] + + +# Special AttrSource to differentiate module._buffers or module._parameters +@dataclass_with_cached_hash(frozen=True) +class UnspecializedParamBufferSource(AttrSource): + pass + + +# This source is intended to be used in places where a source is needed but it is expected +# that the symbol will be simplified out later on. Symbols with ephemeral sources are +# prioritized to be simplified out when e.g. compared against a symbol without an ephemeral +# source. Guarding on this source is an error. +# +# Example: During subclass view fake-ification, any close-over ViewFunc state should be +# symbolicized / fake-ified to avoid invalid specialization during view replay. This source +# is useful for symbols utilized in the middle of the view chain that are not expected to be +# present within the final view shape metadata. +@dataclass_with_cached_hash(frozen=True) +class EphemeralSource(Source): + desc: str | None = None + + @property + def guard_source(self) -> GuardSource: + return GuardSource.EPHEMERAL + + @functools.cached_property + def _name_template(self) -> str: + desc = ": " + self.desc if self.desc is not None else "" + return f"" + + def make_guard(self, fn: Callable[..., Any]) -> Guard: + raise NotImplementedError + + def is_ephemeral(self) -> bool: + return True + + +@dataclass_with_cached_hash(frozen=True) +class SkipGuardSource(ChainedSource): + def reconstruct(self, codegen: "PyCodegen") -> None: + self.base.reconstruct(codegen) + + @property + def _name_template(self) -> str: + return "{0}" + + +class TensorProperty(enum.Enum): + SIZE = 0 + STRIDE = 1 + STORAGE_OFFSET = 2 + + def method_name(self) -> str: + if self is TensorProperty.SIZE: + return "size" + elif self is TensorProperty.STRIDE: + return "stride" + elif self is TensorProperty.STORAGE_OFFSET: + return "storage_offset" + else: + raise AssertionError(f"unhandled {_esc_str(self)}") + + +@dataclass_with_cached_hash(frozen=True) +class TensorPropertySource(ChainedSource): + prop: TensorProperty + idx: int | None = None # None for STORAGE_OFFSET + + def __post_init__(self) -> None: + assert self.base is not None + if self.prop is TensorProperty.STORAGE_OFFSET: + assert self.idx is None + else: + assert self.idx is not None + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.load_import_from( + utils.__name__, f"call_{_esc_str(self.prop.method_name())}" + ) + ) + codegen(self.base) + + if self.idx is not None: + codegen.append_output(codegen.create_load_const(self.idx)) + codegen.extend_output( + create_call_function(2 if self.idx is not None else 1, False) + ) + + @functools.cached_property + def _name_template(self) -> str: + if self.prop is TensorProperty.SIZE: + return f"{{0}}.size()[{_esc_str(self.idx)}]" + elif self.prop is TensorProperty.STRIDE: + return f"{{0}}.stride()[{_esc_str(self.idx)}]" + elif self.prop is TensorProperty.STORAGE_OFFSET: + assert self.idx is None + return "{0}.storage_offset()" + else: + raise AssertionError(f"unhandled {_esc_str(self.prop)}") + + +@dataclass_with_cached_hash(frozen=True) +class IndexedSource(ChainedSource): + idx: int + + def __post_init__(self) -> None: + assert self.base is not None + + def reconstruct(self, codegen: "PyCodegen") -> None: + raise NotImplementedError + + @functools.cached_property + def _name_template(self) -> str: + return f"({_esc_str(self.idx)}, {{0}})" + + +@dataclass_with_cached_hash(frozen=True) +class NegateSource(ChainedSource): + def __post_init__(self) -> None: + assert self.base is not None + + def reconstruct(self, codegen: "PyCodegen") -> None: + raise NotImplementedError + + @property + def _name_template(self) -> str: + # NB: use method call so that function stripping regexes work + return "{0}.__neg__()" + + +@dataclass_with_cached_hash(frozen=True) +class ConvertIntSource(ChainedSource): + def __post_init__(self) -> None: + assert self.base is not None + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen(self.base) + + @property + def _name_template(self) -> str: + return "cast_symbool_to_symint_guardless({0})" + + +@dataclass_with_cached_hash(frozen=True) +class DynamicScalarSource(ChainedSource): + is_int: bool + + def __post_init__(self) -> None: + assert self.base is not None + + def reconstruct(self, codegen: "PyCodegen") -> None: + # Integer casting at reconstruction helps reduce the amount of DynamicInts returned + # to the user, in favor of plain ints. + # For example, a compiled region that only does int arithmetic could return a + # DynamicInt without the casting here. + codegen.add_push_null(lambda: codegen.load_import_from("builtins", "int")) + codegen(self.base) + codegen.extend_output(create_call_function(1, False)) + + @property + def _name_template(self) -> str: + return "int({0})" + + +@dataclass_with_cached_hash(frozen=True) +class FlattenScriptObjectSource(ChainedSource): + def __post_init__(self) -> None: + assert self.base is not None + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen(self.base) + + @property + def _name_template(self) -> str: + return "{0}.__obj_flatten__()" + + +@dataclass_with_cached_hash(frozen=True) +class ScriptObjectQualifiedNameSource(ChainedSource): + def __post_init__(self) -> None: + assert self.base is not None + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen(self.base) + + @property + def _name_template(self) -> str: + return "{0}._type().qualified_name()" + + +class AttrProxySource(ChainedSource): + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen(self.base) + + @property + def _name_template(self) -> str: + return "{0}.get_base()" + + +@dataclass_with_cached_hash(frozen=True) +class DefaultsSource(ChainedSource): + idx_key: int | str + is_kw: bool = False + field: str = dataclasses.field(init=False, repr=False, compare=False) + _name: str = dataclasses.field(init=False, repr=False, compare=False) + + def __post_init__(self) -> None: + assert self.base, ( + "Base must be a valid source in order to properly track and guard this Defaults to its origin." + ) + if self.is_kw: + assert isinstance(self.idx_key, str) + object.__setattr__(self, "field", "__kwdefaults__") + object.__setattr__( + self, + "_name", + f"{{0}}.{_esc_str(self.field)}['{_esc_str(self.idx_key)}']", + ) + else: + assert isinstance(self.idx_key, int) + object.__setattr__(self, "field", "__defaults__") + object.__setattr__( + self, "_name", f"{{0}}.{_esc_str(self.field)}[{_esc_str(self.idx_key)}]" + ) + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen(self.base) + codegen.extend_output(codegen.create_load_attrs(self.field)) + codegen.append_output(codegen.create_load_const(self.idx_key)) + codegen.append_output(create_binary_subscr()) + + @functools.cached_property + def _name_template(self) -> str: + return self._name + + +@dataclass_with_cached_hash(frozen=True) +class GetItemSource(ChainedSource): + index: Any + index_is_slice: bool = False + + def __post_init__(self) -> None: + assert self.base is not None + if isinstance(self.index, slice): + # store the hashable version of the slice so the whole GetItemSource is hashable + super().__setattr__("index", self.index.__reduce__()) + super().__setattr__("index_is_slice", True) + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen(self.base) + if self.index_is_slice: + codegen.append_output(codegen.create_load_const(self.unpack_slice())) + else: + codegen.append_output(codegen.create_load_const(self.index)) + codegen.append_output(create_binary_subscr()) + + def unpack_slice(self) -> slice: + assert self.index_is_slice + slice_class, slice_args = self.index + return slice_class(*slice_args) + + @functools.cached_property + def _name_template(self) -> str: + # Index can be of following types + # 1) index is a slice - example 1:4 + # 2) index is a constant - example string, integer + assert not isinstance(self.index, Source) + if self.index_is_slice: + return f"{{0}}[{_esc_str(self.unpack_slice(), apply_repr=True)}]" + else: + return f"{{0}}[{_esc_str(self.index, apply_repr=True)}]" + + +@dataclass_with_cached_hash(frozen=True) +class ConstDictKeySource(ChainedSource): + index: Any + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.load_import_from(utils.__name__, "dict_keys_getitem") + ) + codegen(self.base) + codegen.append_output(codegen.create_load_const(self.index)) + codegen.extend_output(create_call_function(2, False)) + + @functools.cached_property + def _name_template(self) -> str: + # The list creation will be CSE'd by PyExprCSEPass + return f"list(dict.keys({{0}}))[{_esc_str(self.index, apply_repr=True)}]" + + def is_dict_key(self) -> bool: + return True + + +@dataclass_with_cached_hash(frozen=True) +class NonSerializableSetGetItemSource(ChainedSource): + index: int + + def __post_init__(self) -> None: + from .variables import ConstantVariable + + assert ConstantVariable.is_literal(self.index) + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.load_import_from(utils.__name__, "set_getitem") + ) + codegen(self.base) + codegen.append_output(codegen.create_load_const(self.index)) + codegen.extend_output(create_call_function(2, False)) + + @functools.cached_property + def _name_template(self) -> str: + # set ordering might not be stable + return f"list({{0}})[{_esc_str(self.index, apply_repr=True)}]" + + def is_dict_key(self) -> bool: + return False + + +# Used to access an item from the dictionary +@dataclass_with_cached_hash(frozen=True) +class DictGetItemSource(ChainedSource): + # Key to access in the dictionary. It can be one of the following types + # 1) ConstDictKeySource + # 2) constant - like string, integer + index: Any + + def __post_init__(self) -> None: + from .variables import ConstantVariable + + assert isinstance( + self.index, ConstDictKeySource + ) or ConstantVariable.is_literal(self.index) + + def reconstruct(self, codegen: "PyCodegen") -> None: + # Load dict + codegen(self.base) + + # Load key + if isinstance(self.index, Source): + codegen(self.index) + else: + codegen.append_output(codegen.create_load_const(self.index)) + codegen.append_output(create_binary_subscr()) + + @functools.cached_property + def _name_template(self) -> str: + if isinstance(self.index, ConstDictKeySource): + return f"{{0}}[{_esc_str(self.index.name)}]" + else: + return f"{{0}}[{_esc_str(self.index, apply_repr=True)}]" + + +# Same as DictGetItemSource but used for dict.__getitem__ calls to ensure that +# torch.compile does not run the overridden __getitem__ method +@dataclass_with_cached_hash(frozen=True) +class DictSubclassGetItemSource(ChainedSource): + # Key to access in the dictionary. It can be one of the following types + # 1) ConstDictKeySource + # 2) constant - like string, integer + index: Any + + def __post_init__(self) -> None: + from .variables import ConstantVariable + + assert isinstance( + self.index, ConstDictKeySource + ) or ConstantVariable.is_literal(self.index) + + def reconstruct(self, codegen: "PyCodegen") -> None: + # reconstruct dict.__getitem__(dct, key) + + # Load dict.__getitem__ + codegen.add_push_null( + lambda: codegen.load_import_from(utils.__name__, "dict_getitem") + ) + + # Load dict + codegen(self.base) + + # Load key + if isinstance(self.index, Source): + codegen(self.index) + else: + codegen.append_output(codegen.create_load_const(self.index)) + + codegen.extend_output(create_call_function(2, False)) + + @functools.cached_property + def _name_template(self) -> str: + if isinstance(self.index, ConstDictKeySource): + return f"dict.__getitem__({{0}}, {_esc_str(self.index.name)})" + else: + return f"{{0}}[{_esc_str(self.index, apply_repr=True)}]" + + +@dataclass_with_cached_hash(frozen=True) +class ListGetItemSource(GetItemSource): + """ + Same as GetItemSource with reconstruct and name overridden to be list specific. + """ + + def reconstruct(self, codegen: "PyCodegen") -> None: + # Reconstruct list.__getitem__(lst, index) to avoid any side effects + # from possibly overridden __getitem__. + + # Load list.__getitem__ + codegen.add_push_null( + lambda: codegen.load_import_from(utils.__name__, "list_getitem") + ) + + # Load the list + codegen(self.base) + + # Load the index + if self.index_is_slice: + raise RuntimeError( + "List[slice] is a temporary object and should not have a source" + ) + else: + codegen.append_output(codegen.create_load_const(self.index)) + + codegen.extend_output(create_call_function(2, False)) + + @functools.cached_property + def _name_template(self) -> str: + # Index can be of following types + # 1) index is a slice - example 1:4 + # 2) index is a constant - example string, integer + assert not isinstance(self.index, Source) + if self.index_is_slice: + raise RuntimeError( + "List[slice] is a temporary object and should not have a source" + ) + else: + return f"list.__getitem__({{0}}, {_esc_str(self.index, apply_repr=True)})" + + +@dataclass_with_cached_hash(frozen=True) +class TupleIteratorGetItemSource(GetItemSource): + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.load_import_from(utils.__name__, "tuple_iterator_getitem") + ) + codegen(self.base) + codegen.append_output(codegen.create_load_const(self.index)) + codegen.extend_output(create_call_function(2, False)) + + @functools.cached_property + def _name_template(self) -> str: + return ( + f"___tuple_iterator_getitem({{0}}, {_esc_str(self.index, apply_repr=True)})" + ) + + +@dataclass_with_cached_hash(frozen=True) +class NamedTupleFieldsSource(ChainedSource): + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen(self.base) + codegen.extend_output(codegen.create_load_attrs("_fields")) + + @property + def _name_template(self) -> str: + return "___namedtuple_fields({0})" + + +@dataclass_with_cached_hash(frozen=True) +class DataclassFieldsSource(ChainedSource): + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.load_import_from(utils.__name__, "dataclass_fields") + ) + codegen(self.base) + codegen.extend_output(create_call_function(1, False)) + + @property + def _name_template(self) -> str: + return "___dataclass_fields({0})" + + +@dataclass_with_cached_hash(frozen=True) +class TypeSource(ChainedSource): + def __post_init__(self) -> None: + assert self.base is not None + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null(lambda: codegen.load_import_from("builtins", "type")) + codegen(self.base) + codegen.extend_output(create_call_function(1, False)) + + @property + def _name_template(self) -> str: + return "type({0})" + + +@dataclass_with_cached_hash(frozen=True) +class OptimizerSource(ChainedSource): + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen(self.base) + + @property + def _name_template(self) -> str: + return "{0}" + + +@dataclass_with_cached_hash(frozen=True) +class NNModuleSource(ChainedSource): + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen(self.base) + + @functools.cached_property + def guard_source(self) -> GuardSource: + return _GUARD_SOURCE_SPECIALIZED_NN_MODULE[self.base.guard_source] + + @property + def _name_template(self) -> str: + return "{0}" + + +@dataclass_with_cached_hash(frozen=True) +class UnspecializedNNModuleSource(NNModuleSource): + @functools.cached_property + def guard_source(self) -> GuardSource: + return _GUARD_SOURCE_UNSPECIALIZED_NN_MODULE[self.base.guard_source] + + +@dataclass_with_cached_hash(frozen=True) +class UnspecializedBuiltinNNModuleSource(UnspecializedNNModuleSource): + @functools.cached_property + def guard_source(self) -> GuardSource: + return _GUARD_SOURCE_UNSPECIALIZED_BUILTIN_NN_MODULE[self.base.guard_source] + + +@dataclass_with_cached_hash(frozen=True) +class FSDPNNModuleSource(NNModuleSource): + @functools.cached_property + def guard_source(self) -> GuardSource: + return _GUARD_SOURCE_FSDP_MODULE[self.base.guard_source] + + +@dataclass_with_cached_hash(frozen=True) +class GlobalStateSource(Source): + @property + def _name_template(self) -> str: + return "" + + @property + def guard_source(self) -> GuardSource: + return GuardSource.GLOBAL + + +@dataclass_with_cached_hash(frozen=True) +class ImportSource(Source): + """Points to an imported module - used instead of GlobalSource + in case the user has overridden the module name in their local namespace""" + + module_name: str + + def __post_init__(self) -> None: + from .guards import GuardBuilder, install_guard + + install_guard(self.make_guard(GuardBuilder.ID_MATCH)) + + @functools.cached_property + def _name_template(self) -> str: + return f"__import__('{self.module_name}')" + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.extend_output( + [ + codegen.create_load_const(0), # level + create_build_tuple(0), # fromlist + codegen.create_import_name(self.module_name), + ] + ) + + @property + def guard_source(self) -> GuardSource: + return GuardSource.GLOBAL + + +@dataclass_with_cached_hash(frozen=True) +class TorchFunctionModeStackSource(Source): + ind: int + + @functools.cached_property + def _name_template(self) -> str: + return f"___get_torch_function_mode_stack_at({_esc_str(self._get_index())})" + + def _get_index(self) -> int: + from .variables.torch_function import TorchFunctionModeStackVariable + + return TorchFunctionModeStackVariable.get_mode_index(self.ind) + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.load_import_from( + utils.__name__, "get_torch_function_mode_stack_at" + ) + ) + codegen.extend_output([codegen.create_load_const(self._get_index())]) + codegen.extend_output(create_call_function(1, False)) + + @property + def guard_source(self) -> GuardSource: + return GuardSource.GLOBAL + + +@dataclass_with_cached_hash(frozen=True) +class ConstantSource(Source): + source_name: str + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.append_output(codegen.create_load_global(self.source_name, add=False)) + + @property + def guard_source(self) -> GuardSource: + return GuardSource.CONSTANT + + @functools.cached_property + def _name_template(self) -> str: + return self.source_name + + def make_guard(self, fn: Any) -> Any: + raise NotImplementedError + + +@dataclass_with_cached_hash(frozen=True) +class NumpyTensorSource(ChainedSource): + @property + def _name_template(self) -> str: + return "___from_numpy({0})" + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null(lambda: codegen.load_import_from("torch", "as_tensor")) + codegen(self.base) + codegen.extend_output(create_call_function(1, False)) + + +@dataclass_with_cached_hash(frozen=True) +class SubclassAttrListSource(ChainedSource): + @property + def _name_template(self) -> str: + return "{0}.__tensor_flatten__()[0]" + + +# NB: We don't expect you to actually ever generate guards against this +# source, it is ephemeral +@dataclass_with_cached_hash(frozen=True) +class FloatTensorSource(ChainedSource): + @property + def _name_template(self) -> str: + return "___as_tensor({0})" + + +@dataclass_with_cached_hash(frozen=True) +class CallMethodItemSource(ChainedSource): + @property + def _name_template(self) -> str: + return "{0}.item()" + + +# This is a synthetic source that is associated with the singleton +# shape env guard we always register for all frames. We get the actual +# guard contents from the ambient ShapeEnv +@dataclass_with_cached_hash(frozen=True) +class ShapeEnvSource(Source): + @property + def _name_template(self) -> str: + return "" + + @property + def guard_source(self) -> GuardSource: + return GuardSource.SHAPE_ENV + + +@dataclass_with_cached_hash(frozen=True) +class CurrentStreamSource(Source): + device: device_type + + @functools.cached_property + def _name_template(self) -> str: + return f"___get_current_stream(torch.device('{_esc_str(self.device.type)}', {_esc_str(self.device.index)}))" + + def reconstruct(self, codegen: "PyCodegen") -> None: + num_args = 1 + codegen.add_push_null( + lambda: codegen.load_import_from(utils.__name__, "get_current_stream") + ) + codegen.add_push_null(lambda: codegen.load_import_from("torch", "device")) + codegen.extend_output([codegen.create_load_const(self.device.type)]) + if self.device.index is not None: + num_args += 1 + codegen.extend_output([codegen.create_load_const(self.device.index)]) + codegen.extend_output(create_call_function(num_args, False)) + codegen.extend_output(create_call_function(1, False)) + + @property + def guard_source(self) -> GuardSource: + return GuardSource.GLOBAL + + +@dataclass_with_cached_hash(frozen=True) +class BackwardStateSource(Source): + @property + def _name_template(self) -> str: + return "" + + @property + def guard_source(self) -> GuardSource: + return GuardSource.BACKWARD_STATE + + +@functools.lru_cache +def get_local_source_name( + source: Source, *, only_allow_input: bool = False +) -> str | None: + if isinstance(source, ChainedSource): + return get_local_source_name(source.base, only_allow_input=only_allow_input) + if not isinstance(source, LocalSource): + return None + if only_allow_input and not source.is_input: + return None + return source.local_name + + +@functools.lru_cache +def is_from_local_source(source: Source, *, only_allow_input: bool = False) -> bool: + return get_local_source_name(source, only_allow_input=only_allow_input) is not None + + +@functools.lru_cache +def is_from_global_source(source: Source) -> bool: + return get_global_source_name(source) is not None + + +@functools.lru_cache +def get_global_source_name(source: Source | None) -> str | None: + if isinstance(source, ChainedSource): + return get_global_source_name(source.base) + if not isinstance(source, GlobalSource): + return None + return source.global_name + + +@functools.lru_cache +def is_from_nonlocal_source(source: Source) -> bool: + if isinstance(source, ChainedSource): + return is_from_nonlocal_source(source.base) + return ( + isinstance(source, LocalSource) + and source.is_derefed_cell_contents + and not source.is_input + ) + + +@functools.lru_cache +def is_from_closure_source(source: Source) -> bool: + if isinstance(source, ClosureSource): + return True + if isinstance(source, ChainedSource): + return is_from_closure_source(source.base) + return False + + +@functools.lru_cache +def is_from_source(source: Source, target: Source) -> bool: + if source == target: + return True + if isinstance(source, ChainedSource): + return is_from_source(source.base, target) + return False + + +@functools.lru_cache +def is_from_unspecialized_nn_module_source(source: Source) -> bool: + if isinstance(source, UnspecializedNNModuleSource): + return True + if isinstance(source, ChainedSource): + return is_from_unspecialized_nn_module_source(source.base) + return False + + +@functools.lru_cache +def is_from_unspecialized_builtin_nn_module_source(source: Source) -> bool: + if isinstance(source, UnspecializedBuiltinNNModuleSource): + return True + if isinstance(source, ChainedSource): + return is_from_unspecialized_builtin_nn_module_source(source.base) + return False + + +@functools.lru_cache +def is_from_unspecialized_param_buffer_source(source: Source) -> bool: + if isinstance(source, UnspecializedParamBufferSource): + return True + if isinstance(source, ChainedSource): + return is_from_unspecialized_param_buffer_source(source.base) + return False + + +@functools.lru_cache +def is_from_flatten_script_object_source(source: Source) -> bool: + if isinstance(source, FlattenScriptObjectSource): + return True + elif isinstance(source, ChainedSource): + return is_from_flatten_script_object_source(source.base) + return False + + +@functools.lru_cache +def is_from_optimizer_source(source: Source) -> bool: + if isinstance(source, OptimizerSource): + return True + if isinstance(source, ChainedSource): + return is_from_optimizer_source(source.base) + return False + + +# TODO: can probably write a generic "test this on everything in the chain" +# helper +@functools.lru_cache +def is_from_defaults(source: Source) -> bool: + if isinstance(source, DefaultsSource): + return True + + # Accessed with func.__kwdefaults__["foo"] + if ( + isinstance(source, DictGetItemSource) + and isinstance(source.base, AttrSource) + and source.base.member == "__kwdefaults__" + ): + return True + + # Accessed with func.__defaults__[0] + if ( + isinstance(source, GetItemSource) + and isinstance(source.base, AttrSource) + and source.base.member == "__defaults__" + ): + return True + + if isinstance(source, ChainedSource): + return is_from_defaults(source.base) + return False + + +@functools.lru_cache +def is_from_skip_guard_source(source: Source) -> bool: + if isinstance(source, SkipGuardSource): + return True + + if isinstance(source, ChainedSource): + return is_from_skip_guard_source(source.base) + + return False diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/symbolic_convert.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/symbolic_convert.py new file mode 100644 index 0000000000000000000000000000000000000000..5017c2ae54cd97efb0040468abe5b9804096831d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/symbolic_convert.py @@ -0,0 +1,5848 @@ +""" +Core module responsible for converting Python bytecode into TorchDynamo's symbolic execution format. + +This module implements the bytecode-level tracing system that allows TorchDynamo to analyze +and transform Python code. It converts Python bytecode instructions into a symbolic format +that tracks the flow of tensors and other values through the program. + +Key components: +- InstructionTranslatorBase: Base class for converting bytecode to symbolic execution +- InstructionTranslator: Main translator for function bytecode +- InliningInstructionTranslator: Handles inlining of called functions +- SpeculationLog: Manages state for speculative execution and rollback + +The symbolic conversion process handles: +- Control flow (loops, conditionals, etc.) +- Function inlining and call stack management +- Tracking of program values and side effects +- Graph breaks and resumption points +- Exception handling and stack frame management + +This is a core part of TorchDynamo's tracing system that enables ahead-of-time +optimization of PyTorch programs. +""" + +from __future__ import annotations + +import collections +import collections.abc +import contextlib +import copy +import dataclasses +import dis +import functools +import importlib +import inspect +import itertools +import linecache +import logging +import operator +import re +import sys +import threading +import time +import traceback +import types +import weakref +from collections import deque +from typing import Any, cast, NoReturn, TYPE_CHECKING, TypeAlias, TypeVar +from typing_extensions import TypeIs + +import torch +import torch._logging +from torch._dynamo.dynamo_profiler import DynamoProfilerState, FunctionTraceTiming +from torch._dynamo.exc import ObservedException, TensorifyScalarRestartAnalysis +from torch._guards import InlinedCodeCache, tracing, TracingContext +from torch._logging.structured import dump_file +from torch.fx.experimental.symbolic_shapes import guard_bool +from torch.utils._functools import cache_method + +from . import ( + config, + exc, + graph_break_hints, + logging as torchdynamo_logging, + trace_rules, + variables, +) +from .bytecode_analysis import ( + get_indexof, + JUMP_OPNAMES, + livevars_analysis, + propagate_line_nums, +) +from .bytecode_transformation import ( + cleaned_instructions, + create_binary_slice, + create_call_function, + create_call_function_ex, + create_copy, + create_dup_top, + create_instruction, + create_jump_absolute, + create_rot_n, + create_swap, + get_code_keys, + Instruction, + is_generator, + is_jump_absolute, + unique_id, +) +from .code_context import code_context +from .codegen import PyCodegen +from .comprehension_graph_break import maybe_setup_comprehension_speculation +from .exc import ( + augment_exc_message_with_hop_name, + BackendCompilerFailed, + collapse_resume_frames, + format_frame_info, + get_stack_above_dynamo, + ResumePrologueTracingError, + StepUnsupported, + unimplemented, + Unsupported, + UserError, +) +from .funcname_cache import get_funcname +from .guards import GuardBuilder, install_guard +from .output_graph import GraphCompileReason, OutputGraph, StackLocalsMetadata +from .polyfills import ( + impl_CONTAINS_OP_fallback, + impl_IS_MAPPING, + impl_MATCH_CLASS, + impl_MATCH_KEYS, + impl_MATCH_SEQUENCE, +) +from .replay_record import DummyModule, ExecutionRecorder +from .resume_execution import ( + ContinueExecutionCache, + IS_TRACING_RESUME_PROLOGUE_VARNAME, + ReenterWith, +) +from .source import ( + AttrSource, + DictGetItemSource, + GlobalSource, + GlobalWeakRefSource, + LocalCellSource, + LocalSource, + SkipGuardSource, + Source, +) +from .trace_rules import is_builtin_constant, is_forbidden +from .utils import ( + _get_error_on_graph_break, + counters, + get_fake_value, + get_instruction_source_311, + get_metrics_context, + graph_break_dup_warning_checker, + istype, + LazyString, + proxy_args_kwargs, +) +from .variables.base import typestr, ValueMutationNew, VariableTracker +from .variables.builder import FrameStateSizeEntry, VariableBuilder, wrap_fx_proxy +from .variables.builtin import BuiltinVariable, DictBuiltinVariable +from .variables.constant import ConstantVariable +from .variables.ctx_manager import ( + ContextWrappingVariable, + GenericContextWrappingVariable, + WithEnterFunctionVariable, + WithExitFunctionVariable, +) +from .variables.dicts import ConstDictVariable +from .variables.functions import ( + BaseUserFunctionVariable, + LocalGeneratorFunctionVariable, + LocalGeneratorObjectVariable, + NestedUserFunctionVariable, + SkipFunctionVariable, + UserFunctionVariable, + UserMethodVariable, +) +from .variables.iter import MAX_ITERATOR_LIMIT +from .variables.lazy import LazyVariableTracker +from .variables.lists import ( + BaseListVariable, + IteratorVariable, + ListIteratorVariable, + ListVariable, + SliceVariable, + TupleVariable, +) +from .variables.misc import ( + CellVariable, + ExceptionVariable, + GetAttrVariable, + NullVariable, + PythonModuleVariable, + TracebackVariable, + UnknownVariable, +) +from .variables.nn_module import NNModuleVariable, UnspecializedNNModuleVariable +from .variables.sets import SetVariable +from .variables.streams import SymbolicStreamState +from .variables.tensor import supported_comparison_ops, SymNodeVariable, TensorVariable +from .variables.torch_function import ( + SymbolicTorchFunctionState, + TorchFunctionModeVariable, +) +from .variables.user_defined import ( + RemovableHandleVariable, + UserDefinedClassVariable, + UserDefinedExceptionClassVariable, + UserDefinedExceptionObjectVariable, + UserDefinedObjectVariable, +) + + +if TYPE_CHECKING: + from collections.abc import Callable, Generator, Sequence + + from torch._subclasses.fake_tensor import FakeTensorMode + + from .package import CompilePackage + +log = logging.getLogger(__name__) +graph_break_log = torch._logging.getArtifactLogger(__name__, "graph_breaks") +trace_call_log = torch._logging.getArtifactLogger(__name__, "trace_call") +trace_source_log = torch._logging.getArtifactLogger(__name__, "trace_source") +trace_bytecode_log = torch._logging.getArtifactLogger(__name__, "trace_bytecode") +tls = threading.local() +compare_op_handlers: dict[str, Any] = { + k: BuiltinVariable(v).call_function for k, v in supported_comparison_ops.items() +} +handle_contains = BuiltinVariable(operator.contains).call_function +handle_not = BuiltinVariable(operator.not_).call_function +compare_op_handlers["in"] = lambda tx, args, _: handle_contains( + tx, [*reversed(args)], {} +) +compare_op_handlers["not in"] = lambda tx, args, _: handle_not( + tx, [handle_contains(tx, [*reversed(args)], {})], {} +) + +ExceptionVals: TypeAlias = ( + variables.ExceptionVariable + | UserDefinedExceptionClassVariable + | UserDefinedExceptionObjectVariable +) + + +@functools.cache +def _import_module(name: str) -> types.ModuleType: + """ + Import the named module and cache the result. importlib.import_module() + seems to do some filesystem checking to validate the name so not caching + this can be slow. + """ + return importlib.import_module(name) + + +@dataclasses.dataclass +class SpeculationEntry: + filename: str + lineno: int + instruction_pointer: int + inst: Instruction # for debugging only + _failed: bool = False + # TX error_on_graph_break setting at the time of failure + error_on_graph_break: bool | None = None + reason: GraphCompileReason | None = None + + def fail_and_restart_analysis(self, error_on_graph_break: bool) -> None: + """ + Start tracing of the current frame over again, and don't take this branch. + """ + self._failed = True + self.error_on_graph_break = error_on_graph_break + if self.reason is not None: + restart_reason = self.reason.reason + else: + restart_reason = "Unknown fail_and_restart_analysis" + raise exc.SpeculationRestartAnalysis(restart_reason=restart_reason) + + def failed(self, tx: InstructionTranslatorBase) -> bool: + if self._failed: + assert self.error_on_graph_break is not None + tx.error_on_graph_break = self.error_on_graph_break + return True + return False + + +@dataclasses.dataclass +class SpeculationLog: + """ + SpeculationLog replaces the prior copy_graphstate/restore_graphstate + checkpointing. Rather than saving/restoring state, we restart the + dynamo conversion process over from the beginning -- but when we + hit the start of the speculation that failed, we instead generate + a graph break. + """ + + entries: list[SpeculationEntry] = dataclasses.field(default_factory=list) + index: int = 0 + # If True, graph break at autograd.grad instead of tracing it. + # Set when we detect that autograd.grad consumed grad_fns that are returned. + graph_break_on_autograd_grad: bool = False + # Names of output tensors whose grad_fns were consumed by autograd.grad. + autograd_grad_leaked_tensors: list[str] = dataclasses.field(default_factory=list) + # If True, graph break at requires_grad_() on source-less intermediates + # instead of tracing it. Set when we detect that such an intermediate + # leaks as a graph output with requires_grad=True. + graph_break_on_requires_grad_: bool = False + + def restart(self) -> None: + self.index = 0 + + def clear(self) -> None: + self.entries.clear() + self.index = 0 + + def next( + self, filename: str, lineno: int, instruction_pointer: int, inst: Instruction + ) -> SpeculationEntry: + """ + Lookup or create a SpeculationEntry() that is shared across + RestartAnalysis calls. Args are used only for debug checks. + """ + if len(self.entries) == self.index: + self.entries.append( + SpeculationEntry(filename, lineno, instruction_pointer, inst) + ) + entry = self.entries[self.index] + prev_entry_msg = "" + if self.index != 0: + prev_entry = self.entries[self.index - 1] + prev_entry_msg = ( + f"Previous instruction: {prev_entry.filename}:{prev_entry.lineno}" + f"({prev_entry.inst.opname} @ {prev_entry.instruction_pointer})\n" + ) + if not ( + entry.instruction_pointer == instruction_pointer + and entry.filename == filename + and entry.lineno == lineno + ): + raise SpeculationLogDivergence( + f""" +SpeculationLog diverged at index {self.index} (log had {len(self.entries)} entries): +- Expected: {entry.filename}:{entry.lineno} ({entry.inst.opname} at ip={entry.instruction_pointer}) +- Actual: {filename}:{lineno} ({inst.opname} at ip={instruction_pointer}) +{prev_entry_msg} +There are two usual reasons why this may have occurred: +- When Dynamo analysis restarted, the second run took a different path than + the first. If this occurred, the previous instruction is the critical instruction that + behaved differently. +- Speculation entries are only added under certain conditions (as seen in + step()), e.g., there must exist operators in the graph; those conditions may + have changed on restart. + +If this divergence was intentional, clear the speculation log before restarting (do NOT +do this for graph breaks, you will infinite loop). + +Otherwise, please submit a bug report, ideally including the contents of TORCH_LOGS=+dynamo +""" + ) + self.index += 1 + return entry + + +@dataclasses.dataclass +class LocalState: + automatic_dynamic: dict[str, FrameStateSizeEntry] = dataclasses.field( + default_factory=dict + ) + + def render(self) -> str: + return "\n".join( + f"{k}: {v.render()}" for k, v in self.automatic_dynamic.items() + ) + + +# Mutable box that is shared across restarts +@dataclasses.dataclass +class DistributedState: + compile_pg: Any + local_state: LocalState + all_states: list[LocalState] | None = None + + +class TensorifyState: + # These are the set of string symfloats names (eg. "zf0") that we collect + # from the tensorify_python_scalars.py joint fx pass to inform us about + # which float inputs we should specialize when we restart analysis. + force_specializations: set[str] = set() + + @classmethod + def specialize(cls, index: str) -> None: + cls.force_specializations.add(index) + + @classmethod + def should_specialize(cls, index: str) -> bool: + return index in cls.force_specializations + + @classmethod + def clear(cls) -> None: + cls.force_specializations.clear() + + @classmethod + def empty(cls) -> bool: + return len(cls.force_specializations) == 0 + + +@functools.cache +def _step_logger() -> Callable[..., None]: + return torchdynamo_logging.get_step_logger(log) + + +@contextlib.contextmanager +def save_and_restart_speculation_log( + tx: InstructionTranslatorBase, +) -> Generator[None, None, None]: + # When reconstructing a generator after a graph break, we advance it until + # it is fully exhausted. This process adds new entries to the speculation + # log that were not previously observed. Without temporarily clearing the + # speculation log, this could lead to a divergence error. + + entries = tx.speculation_log.entries + index = tx.speculation_log.index + try: + tx.speculation_log.entries = [] + tx.speculation_log.index = 0 + yield + finally: + tx.speculation_log.entries = entries + tx.speculation_log.index = index + + +@contextlib.contextmanager +def temporarely_allow_writes_to_output_graph( + tx: InstructionTranslatorBase, +) -> Generator[None, None, None]: + try: + tmp = tx.output.should_exit + tx.output.should_exit = False + yield + finally: + tx.output.should_exit = tmp + + +@dataclasses.dataclass +class BlockStackEntry: + # Current instruction that pushes something to block_stack + inst: Instruction + target: Instruction | None + stack_index: int + with_context: ContextWrappingVariable | GenericContextWrappingVariable | None = None + + def can_restore(self) -> bool: + return self.with_context is not None + + def resume_fn(self) -> ReenterWith: + assert self.stack_index is not None + if ( + self.with_context + and hasattr(self.with_context, "target_values") + and self.with_context.target_values + ): + return ReenterWith( + self.stack_index - 1, tuple(self.with_context.target_values) + ) + else: + return ReenterWith(self.stack_index - 1) + + def exit( + self, tx: InstructionTranslatorBase, is_graph_break: bool + ) -> VariableTracker | None: + assert self.with_context is not None + if ( + is_graph_break and self.with_context.exit_on_graph_break() + ) or not is_graph_break: + return self.with_context.exit(tx) # type: ignore[arg-type] + return None + + +class SpeculationLogDivergence(AssertionError): + pass + + +class ReturnValueOp(Exception): + pass + + +class YieldValueOp(Exception): + """ + Signal to the symbolic tracer to stop and return control flow to the + caller + """ + + +def stack_op(fn: Callable[..., object]) -> Callable[..., Any]: + nargs = len(inspect.signature(fn).parameters) + fn_var = BuiltinVariable(fn) + + @functools.wraps(fn) + def impl(self: InstructionTranslator, inst: Instruction) -> None: + self.push(fn_var.call_function(self, self.popn(nargs), {})) + + return impl + + +def is_stdlib(mod: object) -> bool: + if not isinstance(mod, types.ModuleType): + return False + return mod.__name__.split(".")[0] in sys.stdlib_module_names + + +@functools.cache +def get_assert_bytecode_sequence(with_msg: bool) -> list[str]: + if with_msg: + + def fn(x: Any) -> None: + assert x, "msg" + else: + + def fn(x: Any) -> None: + assert x + + insts = [inst.opname for inst in dis.get_instructions(fn)] + + # expect to find POP_JUMP_[FORWARD_]IF_TRUE + begin_idx = next(i for i, inst in enumerate(insts) if inst.startswith("POP_JUMP")) + end_idx = insts.index("RAISE_VARARGS") + + return insts[begin_idx + 1 : end_idx + 1] + + +def _detect_and_normalize_assert_statement( + self: InstructionTranslatorBase, + truth_fn: Callable[[object], bool], + push: bool, +) -> bool: + # Detect if this jump instruction is assert and normalize the assert + # by pushing dummy error message when nothing is given. + # + # Python 3.9-3.13 assertion is in following format (minus small differences) + # 18 POP_JUMP_IF_TRUE 28 + # 20 LOAD_ASSERTION_ERROR + # 22 LOAD_CONST 3 ('Assert message') -> optional instruction + # 24 CALL_FUNCTION 1 -> optional instruction + # 26 RAISE_VARARGS + + if (truth_fn is not operator.truth) or push: + return False + + assert isinstance(self.instruction_pointer, int) + current_instruction_pointer = self.instruction_pointer + + for with_msg in (False, True): + assert_insts = get_assert_bytecode_sequence(with_msg) + cur_insts = self.instructions[ + current_instruction_pointer : current_instruction_pointer + + len(assert_insts) + ] + cur_insts = [inst.opname for inst in cur_insts] + if cur_insts == assert_insts: + if with_msg: + load_const_idx = assert_insts.index("LOAD_CONST") + error_msg = self.instructions[ + current_instruction_pointer + load_const_idx + ].argval + else: + error_msg = "assertion error" + self.push(VariableTracker.build(self, error_msg)) + return True + + return False + + +explain = False + + +# [NOTE] graph break handling in symbolic_convert +# There are 4 possible graph break cases that InstructionTranslatorBase handles: +# 1. Regular graph breaks from CALL, BINARY_SUBSCR, etc. (implemented by break_graph_if_unsupported) +# 2. Data-dependent condition graph breaks (implemented by generic_jump) +# 4. All other unhandled graph breaks - unsupported step graph breaks (implemented in InstructionTranslatorBase.step) +# +# Graph breaks are handled in the following manner: +# 1. The Unsupported exception is caught. If we cannot compile a partial graph (should_compile_partial_graph() is False), +# then propagate the exception upward. For unsupported step graph breaks, the condition to abort partial compilation is +# more restrictive (see InstructionTranslatorBase.step). +# 2. If the Unsupported exception escapes symbolic_convert.py, then we are done. +# Otherwise, we want to attempt partial compilation. +# Log the graph break via log_graph_break. If we're handling a data-dependent graph break (type 2.), then we can immediately +# codegen the compiled graph and resume function and we're done. This is because the jump instruction we graph break on is +# limited in how it can manipulate Python state (say, in comparison, to CALL, which can modify Python state arbitrarily). +# Otherwise, we need to restart compilation. We need to restart because by processing the unsupported instruction, +# we may have modified the VariableTrackers, and we need all of our VariableTrackers to be in the state BEFORE tracing the +# unsupported instruction. +# 3. During the first compilation, we updated a speculation log, indicating points in the code that we can resume from. +# On the second compilation, we will stop tracing at the first speculation log that fails. Then we compile the partial +# graph and resume function. +# +# Logging invariants: +# 1. No logs need to be made if Unsupported escapes symbolic_convert.py. Python's default exception printing will +# print out all of the necessary information and no partial compilation will be attempted. +# 2. log_graph_break should be called as soon as Unsupported is caught and we determined we want to partial compile. +# This always happens on the first compilation, NOT the restart handling this graph +# 3. Any compile_subgraph call should be preceded immediately by a log in the form of "... triggered compile". + + +def get_node_source_info(n: torch.fx.Node) -> str: + """Extract the innermost user source location from an FX node's stack trace.""" + st = n.meta.get("stack_trace", "") or getattr(n, "stack_trace", "") + if not st: + return "" + trace_lines = st.strip().split("\n") + last_file_idx = -1 + for i, line in enumerate(trace_lines): + if line.strip().startswith("File "): + last_file_idx = i + if last_file_idx < 0: + return "" + file_line = trace_lines[last_file_idx].strip() + code = "" + if last_file_idx + 1 < len(trace_lines): + code = trace_lines[last_file_idx + 1].strip() + return f"{file_line}" + (f", code: {code}" if code else "") + + +def format_tensor_computation_trace(value: VariableTracker, max_lines: int = 20) -> str: + """Walk the FX graph backwards from a TensorVariable to show how it was + computed, with user source locations. Graph inputs (placeholders) are always + shown first, followed by operations in dataflow order ending at the branch + condition.""" + if not isinstance(value, TensorVariable): + return "" + try: + node = value.proxy.node + + # Collect operations and placeholders separately so placeholders (root + # cause) always appear first regardless of traversal order. + op_blocks: list[list[str]] = [] + placeholder_blocks: list[list[str]] = [] + visited: set[str] = set() + + def fmt_arg(a: object) -> str: + return a.name if isinstance(a, torch.fx.Node) else repr(a) + + def walk(n: torch.fx.Node) -> None: + if n.name in visited: + return + visited.add(n.name) + source = get_node_source_info(n) + + if n.op == "placeholder": + block = [] + if source: + block.append(f"# {source}") + block.append(f"{n.name}: graph input ({n.target})") + placeholder_blocks.append(block) + return + + if n.op == "call_function" and len(op_blocks) < max_lines: + target_name = getattr(n.target, "__name__", str(n.target)) + args_str = ", ".join(fmt_arg(a) for a in n.args) + block = [] + if source: + block.append(f"# {source}") + block.append(f"{n.name} = {target_name}({args_str})") + op_blocks.append(block) + + for a in n.args: + if isinstance(a, torch.fx.Node): + walk(a) + + walk(node) + + if not op_blocks and not placeholder_blocks: + return "" + + # Placeholders first (root cause), then ops in dataflow order (reversed + # from the DFS collection order) ending at the branch condition. + all_lines: list[str] = [] + for block in placeholder_blocks + list(reversed(op_blocks)): + all_lines.extend(block) + all_lines.append("") + + return ( + "\n\n The branch condition involves a tensor computed as follows:\n" + + "\n".join(f" {line}" for line in all_lines) + ) + except Exception: + log.debug("format_tensor_computation_trace failed", exc_info=True) + return "" + + +def generic_jump( + truth_fn: Callable[[object], bool], push: bool +) -> Callable[[InstructionTranslatorBase, Instruction], None]: + def raise_jump_graph_break(value: VariableTracker) -> NoReturn: + trace_info = format_tensor_computation_trace(value) + hints: list[str] = [] + if isinstance(value, TensorVariable): + try: + example = value.proxy.node.meta.get("example_value") + if ( + example is not None + and example.dim() == 0 + and example.dtype + in ( + torch.bool, + torch.int32, + torch.int64, + ) + ): + hints.append( + "The branch condition uses a scalar integer tensor. " + "Consider rewriting the computation to use plain Python " + "ints (e.g. use int attributes instead of tensor buffers) " + "so the condition becomes a shape guard instead of " + "data-dependent branching." + ) + except Exception: + pass + hints.extend(graph_break_hints.FUNDAMENTAL) + hints.append("Use `torch.cond` to express dynamic control flow.") + unimplemented( + gb_type="Data-dependent branching", + context=f"attempted to jump with {value}", + explanation="Detected data-dependent branching (e.g. `if my_tensor.sum() > 0:`). " + "Dynamo does not support tracing dynamic control flow." + trace_info, + hints=hints, + ) + + def jump_graph_break( + self: InstructionTranslatorBase, + inst: Instruction, + value: VariableTracker, + extra_msg: str = "", + ) -> None: + assert self.should_compile_partial_graph() + + exc = None + try: + raise_jump_graph_break(value) + except (Unsupported, UserError) as e: + exc = e + + assert exc is not None + + # compile a partial subgraph prefix then skip the rest of user code + if self.maybe_has_backedge(): + self.raise_loop_graph_break(self.f_code, exc) + + self.log_graph_break( + self.code_options, + reason=str(exc), + exc=exc, + ) + + self.push(value) + log.debug("generic_jump triggered compile") + all_stack_locals_metadata = self.output.compile_subgraph( + self, + reason=GraphCompileReason( + f"generic_jump {typestr(value)}{extra_msg}", [self.frame_summary()] + ), + stack_pops=1, + ) + self.pop() + + if_next = self.create_call_resume_at( + self.next_instruction, + all_stack_locals_metadata, + ) + if push: + self.push(value) + assert inst.target is not None + if_jump = self.create_call_resume_at( + inst.target, + all_stack_locals_metadata, + ) + + if sys.version_info >= (3, 13): + # 3.13 requires stack[-1] to be bool type + self.output.add_output_instructions([create_instruction("TO_BOOL")]) + + jump_inst = create_instruction(inst.opname, target=if_jump[0]) + jump_inst.copy_positions(inst) + self.output.add_output_instructions([jump_inst] + if_next + if_jump) + + def inner(self: InstructionTranslatorBase, inst: Instruction) -> None: + value: VariableTracker = self.pop() + if ( + config.rewrite_assert_with_torch_assert + and sys.flags.optimize == 0 + and _detect_and_normalize_assert_statement(self, truth_fn, push) + ): + error_msg: VariableTracker = self.pop() + # Skip over things like `assert True` + if value.is_python_constant(): + if bool(value.as_python_constant()): + return self.jump(inst) + elif self.should_compile_partial_graph(): + jump_graph_break(self, inst, value) + return + else: + unimplemented( + gb_type="Data-dependent assertion failed (cannot compile partial graph)", + context=f"value: {value}", + explanation="Dynamo has determined when encountering a data-dependent assert failure " + "that it should not compile the partial graph.", + hints=[ + *graph_break_hints.FUNDAMENTAL, + "Use `torch._assert()` to raise a hard AssertionError when the check fails. " + "This error will propagate back the user code " + "that called the compiled function (i.e. Dynamo will not trace any exception handling).", + "Remove the assert statement.", + "Move the assert statement outside of any context managers in order to graph break with " + "partial graph compilation (if fullgraph=False).", + ], + ) + + # TODO maybe should respect DtoH sync intention of users later?? + # Manually insert torch._assert_async instead of python assert and jump over + # assert related instructions as we don't need them anymore. + + # if we see Tensor as assert statement, no need to call scalar_tensor + if value.is_tensor(): + self.output.create_proxy( + "call_function", + torch._assert_async, + *proxy_args_kwargs((value, error_msg), {}), + ) + self.jump(inst) + return + + if isinstance(value, SymNodeVariable): + # if the assertion is normal shape expression. + # just install guard and bail out. + sym_expr = value.sym_num + if not isinstance(sym_expr, torch.SymBool): + sym_expr = sym_expr != 0 + + result = torch.fx.experimental.symbolic_shapes.expect_true(sym_expr) + if not result: + unimplemented( + gb_type="Assertion failed on symbolic shapes", + context=str(sym_expr), + explanation="", + hints=[*graph_break_hints.USER_ERROR], + ) + self.jump(inst) + return + + scalar_to_tensor_proxy = self.output.create_proxy( + "call_function", torch.scalar_tensor, *proxy_args_kwargs((value,), {}) + ) + + scalar_to_tensor = wrap_fx_proxy( + self, + scalar_to_tensor_proxy, + example_value=get_fake_value(scalar_to_tensor_proxy.node, self), + ) + + self.output.create_proxy( + "call_function", + torch._assert_async, + *proxy_args_kwargs((scalar_to_tensor, error_msg), {}), + ) + self.jump(inst) + return + + if value.is_python_constant(): + # ConstDictVariable is optimized to be very lazy about insertion of + # guards, so we have to manually insert a SEQUENCE_LENGTH guard + # here. + if isinstance(value, ConstDictVariable) and value.source: + install_guard(value.source.make_guard(GuardBuilder.SEQUENCE_LENGTH)) + if truth_fn(value.as_python_constant()): + if push: + self.push(value) + self.jump(inst) + elif value.is_tensor() and self.should_compile_partial_graph(): + jump_graph_break(self, inst, value) + elif isinstance(value, NNModuleVariable): + # Equivalent of "self.nn_module is not None" + mod = self.output.get_submodule(value.module_key) + if truth_fn(mod): + if push: + self.push(value) + self.jump(inst) + elif isinstance(value, UserDefinedObjectVariable): + try: + x = value.var_getattr(self, "__bool__") # type: ignore[arg-type] + except exc.ObservedAttributeError: + exc.handle_observed_exception(self) + # if __bool__ is missing, trying __len__ to infer a truth value. + try: + x = value.var_getattr(self, "__len__") # type: ignore[arg-type] + except exc.ObservedAttributeError: + exc.handle_observed_exception(self) + x = None + + # __bool__ or __len__ is function + if isinstance(x, (GetAttrVariable, UserMethodVariable)): + result = x.call_function(self, [], {}) # type: ignore[arg-type, assignment] + method_name = getattr(getattr(x, "fn", None), "__name__", None) + if result.is_python_constant(): + result_value = result.as_python_constant() + if method_name == "__bool__" and not isinstance(result_value, bool): + exc.raise_observed_exception( + TypeError, + self, + args=[ + f"__bool__ should return bool, returned {type(result_value).__name__}" + ], + ) + if isinstance(result_value, (bool, int)) and truth_fn(result_value): + if push: + self.push(value) + self.jump(inst) + elif isinstance(result, SymNodeVariable): + if result.evaluate_expr(): + if push: + self.push(value) + self.jump(inst) + else: + unimplemented( + gb_type="Data-dependent branching with non-constant __bool__", + context=f"method: {x}, result: {result}", + explanation="Attempted to perform data-dependent branching on a user-defined " + "object with a __bool__ method that did not return a constant.", + hints=[], + ) + # __bool__ or __len__ is non-function or not existed in the user defined object + else: + if truth_fn(True): + if push: + self.push(value) + self.jump(inst) + elif not value.is_tensor() and value.has_unpack_var_sequence(self): + if truth_fn(len(value.unpack_var_sequence(self))): + if push: + self.push(value) + self.jump(inst) + elif isinstance(value, SymNodeVariable): + try: + # if the user is branching on a SymBool, guard on it + # if the user has code like: + # if size: + # ... + # then they are just testing truthiness: guard that the expr != 0 + if isinstance(value.sym_num, torch.SymBool): + eval_result = value.evaluate_expr(self.output) + else: + eval_result = guard_bool(value.sym_num != 0) + except exc.UserError as e: + if self.should_compile_partial_graph(): + return jump_graph_break(self, inst, value, extra_msg=f"\n{e}") + raise + if truth_fn(eval_result): + if push: + self.push(value) + self.jump(inst) + elif isinstance(value, variables.BackwardHookVariable): + if truth_fn(True): + if push: + self.push(value) + self.jump(inst) + else: + from .source import is_constant_source + + if value.source is not None and is_constant_source(value.source): + if truth_fn(value.get_real_value()): # type: ignore[attr-defined] + if push: + self.push(value) + self.jump(inst) + else: + raise_jump_graph_break(value) + + return inner + + +def _reconstruct_block_stack( + tx: InstructionTranslatorBase, cg: PyCodegen, cleanup: list[Instruction] +) -> None: + """Generates bytecode to restore the block stack for running the unsupported instruction + in the compiled bytecode.""" + # Reconstruct the context variable CLASS in the block stack + all_txes: list[InstructionTranslatorBase] = [] + cur_tx: InstructionTranslatorBase | None = tx + while cur_tx is not None: + all_txes.append(cur_tx) + cur_tx = cur_tx.parent + for tx in reversed(all_txes): + for b in tx.block_stack: + # Don't exit any modes we have entered, + # output bytecode will mutate the tf mode stack accordingly + if isinstance(b.with_context, TorchFunctionModeVariable): + cg.extend_output( + b.resume_fn().try_except_torch_function_mode( + cg.code_options, cleanup + ) + ) + continue + assert b.with_context is not None + assert isinstance(b.with_context, (ContextWrappingVariable)) + b.with_context.reconstruct_type(cg) + cg.extend_output(b.resume_fn().try_finally(cg.code_options, cleanup)) + + +# NOTE: for the purposes of nested graph breaks, break_graph_if_unsupported only works on instructions +# with 0 or 1 outputs. If you wish to support bytecodes with 2+ outputs, either rewrite the instruction +# into a sequence of simpler instructions, or file an issue for consultation. +# There is an additional requirement that if the instruction causes a function call, e.g. STORE_ATTR, +# nothing should happen to the result of the function call. +def break_graph_if_unsupported( + *, push: bool, msg_prefix: str +) -> Callable[ + [Callable[..., None]], Callable[[InstructionTranslatorBase, Instruction], None] +]: + def decorator( + inner_fn: Callable[..., None], + ) -> Callable[[InstructionTranslatorBase, Instruction], None]: + @functools.wraps(inner_fn) + def wrapper(self: InstructionTranslatorBase, inst: Instruction) -> None: + prev_push = self.current_instruction_push + self.current_instruction_push = push + speculation = self.speculate() + if speculation.failed(self): + # no need to restore current_instruction_push if speculation failed + assert speculation.reason is not None + return handle_graph_break(self, inst, speculation.reason) + try: + return inner_fn(self, inst) + except (Unsupported, UserError) as excp: + if self.active_generic_context_managers: + # raise original graph break if fullgraph/error_on_graph_break=True + if self.one_graph or self.error_on_graph_break: + raise + + # We don't support graph break under GenericContextWrappingVariable, + # If there is, we roll back to the checkpoint and fall back. + if isinstance(excp, Unsupported): + excp.remove_from_stats() + unimplemented( + gb_type="Graph break under GenericContextWrappingVariable", + context=f"Active generic context managers: {self.active_generic_context_managers}", + explanation="Attempted to graph break in an active context manager(s) that doesn't support graph breaking.", + hints=[ + "Move the offending context manager(s) to outside the compiled region.", + *graph_break_hints.CAUSED_BY_EARLIER_GRAPH_BREAK, + ], + from_exc=excp, + ) + + if getattr(excp, "skip_frame", False): + raise + + if not self.should_compile_partial_graph(): + raise + + if self.maybe_has_backedge(): + self.raise_loop_graph_break(self.f_code, excp) + + self.log_graph_break( + self.code_options, + reason=f"{msg_prefix}:\n\n{str(excp)}", + exc=excp, + ) + + if isinstance(excp, Unsupported): + excp.remove_from_stats() + excp.add_to_stats("graph_break") + speculation.reason = GraphCompileReason( + getattr(excp, "msg", str(excp)), + getattr(excp, "real_stack", [self.frame_summary()]), + ) + finally: + self.current_instruction_push = prev_push + speculation.fail_and_restart_analysis(self.error_on_graph_break) + + def handle_graph_break( + self: InstructionTranslatorBase, + inst: Instruction, + reason: GraphCompileReason, + ) -> None: + if ( + sys.version_info >= (3, 11) + and sys.version_info < (3, 12) + and inst.opname == "CALL" + ): + # stack effect for PRECALL + CALL is split between the two instructions + stack_effect = dis.stack_effect( + dis.opmap["PRECALL"], inst.arg + ) + dis.stack_effect(dis.opmap["CALL"], inst.arg) + else: + stack_effect = dis.stack_effect(inst.opcode, inst.arg) + + log.debug("%s triggered compile", inst.opname) + all_stack_locals_metadata = self.output.compile_subgraph( + self, reason=reason, stack_pops=int(push) - stack_effect + ) + cg = PyCodegen(self.output.root_tx) + cleanup: list[Instruction] = [] + _reconstruct_block_stack(self, cg, cleanup) + self.output.add_output_instructions(cg.get_instructions()) + del cg + + if sys.version_info >= (3, 11) and inst.opname == "CALL": + kw_names = ( + self.kw_names.as_python_constant() + if self.kw_names is not None + else () + ) + if len(kw_names) > 0: + # KW_NAMES no longer used in 3.13 + assert sys.version_info < (3, 13) + self.output.add_output_instructions( + [create_instruction("KW_NAMES", argval=kw_names)] + ) + assert inst.arg is not None + call_insts = create_call_function(inst.arg, False) + call_insts[-1].copy_positions(inst) + self.output.add_output_instructions(call_insts) + else: + # copy instruction, but without exception table data + assert inst.target is None + inst_copy = copy.copy(inst) + inst_copy.exn_tab_entry = None + self.output.add_output_instructions([inst_copy]) + + self.output.add_output_instructions(cleanup) + + self.popn(int(push) - stack_effect) + if push: + self.push(UnknownVariable()) + self.output.add_output_instructions( + self.create_call_resume_at( + self.next_instruction, + all_stack_locals_metadata, + ) + ) + + return wrapper + + return decorator + + +class BytecodeDispatchTableMeta(type): + """Installs a `cls.dispatch_table` on every subclass to speed up calls to self.OPCODE()""" + + def __init__(cls: type, name: str, bases: Any, dct: Any) -> None: + super().__init__(name, bases, dct) # type: ignore[misc] + + def _missing(opname: str, *args: Any) -> None: + unimplemented( + gb_type="Missing bytecode handler", + context=f"{opname} with args {args}", + explanation=f"Dynamo does not know how to handle the bytecode instruction `{opname}`.", + hints=[ + f"Do not trace code that produces the `{opname}` bytecode instruction " + "(see https://docs.python.org/3/library/dis.html for bytecode semantics).", + *graph_break_hints.SUPPORTABLE, + ], + ) + + dispatch_table = { + op: getattr(cls, opname, functools.partial(_missing, opname)) + for opname, op in dis.opmap.items() + } + cls.dispatch_table = [dispatch_table.get(i) for i in range(2**8)] + + +@dataclasses.dataclass +class ExceptionStack: + """ + Exception stack that it is shared among all InstructionTranslator instances + """ + + # Exception handling in CPython is a bit confusing and some of the bytecode + # have a slightly different behavior than what is documented. While reading + # the documentation, is important to notice that the terms "current exception" + # and "stack" sometimes refers to a C variable with the same name and the + # exception stack, respectively. + # + # The lifetime of an exception is (Python 3.11+): + # + tx._raise_exception_variable(...) := sets the current_exception variable + # + PUSH_EXC_INFO := pushes the current_exception to the *exception stack* + # + POP_EXCEPT := pops TOS from the *exception stack* + + _exc_stack: list[ExceptionVals] = dataclasses.field(default_factory=list) + _current_exception: ExceptionVals | None = dataclasses.field(default=None) + + def clear_current_exception(self) -> None: + self._current_exception = None + + def set_current_exception(self, val: ExceptionVals) -> None: + self._set_context_and_break_context_reference_cycle(val) + self._current_exception = val + + def move_current_exception_to_stack(self) -> None: + assert self._current_exception is not None + self.append(self._current_exception) + self.clear_current_exception() + + def get_current_exception(self) -> ExceptionVals: + assert self._current_exception is not None + return self._current_exception + + def _set_context_recursive( + self, val: ExceptionVals, prev_idx: int + ) -> ExceptionVals: + if (ctx := val.__context__) and type(ctx) is not ConstantVariable: # type: ignore[union-attr] + return val + if len(self._exc_stack) + prev_idx > 0: + prev = self._exc_stack[prev_idx] + self._set_context_recursive(prev, prev_idx - 1) + if prev is not val: + val.set_context(prev) # type: ignore[union-attr, arg-type] + return val + + def _break_context_reference_cycle(self, val: ExceptionVals) -> None: + # See test_exceptions::test_raise_does_not_create_context_chain_cycle + # Based on https://github.com/python/cpython/blob/e635bf2e49797ecb976ce45a67fce2201a25ca68/Python/errors.c#L207-L228 + # As noted on CPython, this is O(chain length) but the context chains + # are usually very small + o = slow_o = val + slow_update_toggle = False # floyd's algorithm for detecting cycle + while True: + context = o.__context__ # type: ignore[union-attr] + if type(context) is ConstantVariable: # context not set + break + + if context is val: + o.set_context(ConstantVariable.create(None)) # type: ignore[union-attr, arg-type] + break + + o = context # type: ignore[assignment] + if o is slow_o: + # pre-existing cycle - all exceptions on the path were + # visited and checked + break + + if slow_update_toggle: + # visited all exceptions + slow_o = slow_o.__context__ # type: ignore[union-attr, assignment] + slow_update_toggle = not slow_update_toggle + + def _set_context_and_break_context_reference_cycle( + self, val: ExceptionVals + ) -> None: + # set Exception.__context__ + self._set_context_recursive(val, len(self._exc_stack) - 1) + self._break_context_reference_cycle(val) + + def pop(self) -> ExceptionVals: + return self._exc_stack.pop() + + def append(self, val: ExceptionVals) -> None: + self._exc_stack.append(val) + + def __len__(self) -> int: + return len(self._exc_stack) + + def __getitem__(self, index: int) -> ExceptionVals: + return self._exc_stack[index] + + def __str__(self) -> str: + return f"{self._exc_stack=} - {self._current_exception=}" + + __repr__ = __str__ + + +class InstructionTranslatorBase( + metaclass=BytecodeDispatchTableMeta, +): + output: OutputGraph + symbolic_locals: dict[str, VariableTracker] + symbolic_globals: dict[str, VariableTracker] + symbolic_torch_function_state: SymbolicTorchFunctionState + symbolic_stream_state: SymbolicStreamState + post_prune_cell_and_freevars: dict[str, VariableTracker] | None + stack: list[VariableTracker] + instruction_pointer: int | None + current_instruction: Instruction + current_instruction_push: bool + block_stack: list[BlockStackEntry] + lineno: int + kw_names: ConstantVariable | None + accept_prefix_inst: bool + prefix_insts: list[Instruction] + inline_depth: int + inconsistent_side_effects: bool + current_speculation: SpeculationEntry | None + dispatch_table: list[Any] + exn_vt_stack: ExceptionStack + exec_recorder: ExecutionRecorder | None + strict_checks_fn: Callable[[VariableTracker], bool] | None + start_point: int | None + # Does this function make no inlined function calls? + has_no_inlined_calls: bool + parent: InstructionTranslatorBase | None + # Does this tx currently have a child tx tracing? + # Used to correctly implement should_compile_partial_graph + is_child_tracer_active: bool + debug_locals: list[tuple[VariableTracker, list[VariableTracker]]] + package: CompilePackage | None + latest_bytecode_queue: deque[str] + # Store the latest bytecode before graph_break() call by user + + def mark_inconsistent_side_effects(self) -> None: + """ + InstructionTranslator has encountered instructions which may cause + dynamo to see a different version of history from eager + See: https://github.com/pytorch/pytorch/issues/110765 + """ + self.inconsistent_side_effects = True + + def maybe_has_backedge(self) -> bool: + # This function employs a heuristic. It does not reliably detect a backedge. + # The heuristic is straightforward: starting from the current instruction and + # continuing to the end, if any jump instruction targets an instruction before + # the current one, there might be a backedge. + + # Python 3.12 introduced changes to bytecode that group common paths in + # blockstacks (with or try...else) and allow for early returns. Consequently, + # there can be multiple RETURN_VALUE instructions. Another heuristic is to + # halt detection upon encountering the first RETURN_VALUE or RETURN_CONST. + + # These heuristics can result in both false positives and negatives, but + # in either case, the Dynamo code remains valid. For false positives + # (where an edge is incorrectly marked as a backedge), Dynamo will + # graph break with a frame skip instead of potentially applying optimizations. For + # false negatives (where an edge that should be marked as a backedge + # isn't), multiple graphs may be generated if there's a break in the + # graph during a for loop. In general, its better to have fewer false + # negatives so that Dynamo does not skip the whole frame. + + # If any parent tx has a backedge, then return True + cur_tx: InstructionTranslatorBase | None = self + while cur_tx is not None: + cur_offset = cur_tx.current_instruction.offset + assert cur_tx.instruction_pointer is not None + for inst in cur_tx.instructions[cur_tx.instruction_pointer :]: + if inst.opname in ("RETURN_VALUE", "RETURN_CONST"): + break + if inst.opname in JUMP_OPNAMES: + jump_offset = inst.argval + if jump_offset < cur_offset: + return True + cur_tx = cur_tx.parent + return False + + def cellvars(self) -> list[str]: + return self.code_options["co_cellvars"] + + def freevars(self) -> list[str]: + return self.code_options["co_freevars"] + + def cell_and_freevars(self) -> list[str]: + if not hasattr(self, "_cell_and_freevars"): + self._cell_and_freevars = self.cellvars() + self.freevars() + return self._cell_and_freevars + + def prune_dead_locals(self) -> None: + # keep cell and freevar references alive + self.post_prune_cell_and_freevars = { + k: v + for k, v in self.symbolic_locals.items() + if k in self.cell_and_freevars() + } + # Only keep the locals that must remain on the stack. + reads = livevars_analysis(self.instructions, self.current_instruction) + self.symbolic_locals = { + k: v for k, v in self.symbolic_locals.items() if k in reads + } + + def call_function( + self, + fn: VariableTracker, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> None: + assert isinstance(fn, VariableTracker) + assert isinstance(args, list) + assert isinstance(kwargs, dict) + assert all( + isinstance(x, VariableTracker) + for x in itertools.chain(args, kwargs.values()) + ) + inner_fn = None + if hasattr(fn, "value"): + inner_fn = fn.value + if hasattr(fn, "fn"): + inner_fn = fn.fn + if inner_fn and callable(inner_fn) and is_forbidden(inner_fn): + raise AssertionError(f"Attempt to trace forbidden callable {inner_fn}") + self.push(fn.call_function(self, args, kwargs)) # type: ignore[arg-type] + + def inline_generator_function( + self, + fn: BaseUserFunctionVariable, + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + """ + Redirect the call to the generator "call_function" + """ + if not isinstance(fn, LocalGeneratorFunctionVariable): + fn = LocalGeneratorFunctionVariable(fn) + return fn.call_function(self, args, kwargs) # type: ignore[arg-type] + + def inline_user_function_return( + self, + fn: BaseUserFunctionVariable, + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> Any: + """ + A call to some user defined function by inlining it. + """ + if config.enable_faithful_generator_behavior and is_generator(fn.get_code()): + return self.inline_generator_function(fn, args, kwargs) + else: + return InliningInstructionTranslator.inline_call(self, fn, args, kwargs) + + def get_line_of_code_header(self, lineno: int | None = None) -> str: + if lineno is None: + lineno = self.lineno + inline_depth_str = ( + f" (inline depth: {self.inline_depth})" if self.inline_depth > 0 else "" + ) + funcname = get_funcname(self.f_code.co_filename, lineno) + funcname_str = "" if funcname is None else f" ({funcname})" + return f"{self.f_code.co_filename}:{lineno} in {self.f_code.co_name}{funcname_str}{inline_depth_str}" + + def get_log_starts_line_log_str(self) -> str: + log_str = f"TRACE starts_line {self.get_line_of_code_header()}\n" + line = linecache.getline(self.f_code.co_filename, self.lineno).rstrip() + log_str += f" {line}" + return log_str + + def starts_line(self, lineno: int) -> None: + if self.lineno == lineno: + return + self.lineno = lineno + TracingContext.set_current_loc( + self.f_code.co_filename, lineno, self.f_code.co_name + ) + + if self.is_trace_source_log_enabled: + trace_source_log.debug("%s", LazyString(self.get_log_starts_line_log_str)) + + def step(self) -> bool: + """Process exactly one instruction, return False we should exit""" + self.error_on_graph_break = _get_error_on_graph_break() + + ip = self.instruction_pointer + if ip is None: + return False + self.current_instruction = inst = self.instructions[ip] + self.instruction_pointer = ip + 1 + + if inst.starts_line: + self.starts_line(inst.starts_line) + + if ( + not self.stack + and self.should_compile_partial_graph() + and self.is_non_empty_graph() + ): + self.current_speculation = self.speculate() + if self.current_speculation.failed(self): + self.step_graph_break(inst) + return False + + if self.is_trace_bytecode_log_enabled: + trace_bytecode_log.debug( + "TRACE %s %s %s", inst.opname, inst.argval, repr(self.stack) + ) + + # Store the latest 20 bytecode execution for the process, + # Used repr for byte processing and limiting the length to 2048 + if config.verbose: + try: + stack_repr = repr(self.stack) + except ValueError: + # Handle large integers that exceed sys.int_info.str_digits_check_threshold + stack_repr = "" + self.latest_bytecode_queue.append( + f"TRACE {inst.opname} {repr(inst.argval)} {stack_repr}" + ) + + self.update_block_stack(inst) + + try: + self.dispatch_table[inst.opcode](self, inst) + return not self.output.should_exit + except TensorifyScalarRestartAnalysis: + raise + except exc.ObservedException as e: + self.exception_handler(e) + return True + except (ReturnValueOp, YieldValueOp): + return False + except (Unsupported, UserError, StepUnsupported) as e: + # More restrictive condition than should_compile_partial_graph: + # if this condition is true, then we SHOULD NOT attempt to find + # a previous checkpoint to resume from and try to resume - we should + # immediately error out. + # The condition is more restrictive because, it may be possible to resume significantly earlier + # in the code (the most recent speculation point). This happens, for example, in the case + # of a graph break in a try block. + if ( + self.one_graph + or self.error_on_graph_break + or self.is_tracing_resume_prologue + or getattr(e, "skip_frame", False) + ): + if isinstance(e, StepUnsupported): + unimplemented( + gb_type="cannot resume from torch._dynamo.step_unsupported()", + context="", + explanation="traced torch._dynamo.step_unsupported(), but Dynamo is instructed " + "to error on graph break. This graph break is used for debugging only.", + hints=[ + "Remove the torch._dynamo.step_unsupported() call.", + "Make sure fullgraph=False and error_on_graph_break=False.", + *graph_break_hints.DYNAMO_BUG, + ], + ) + raise + if self.current_speculation is None: + log.debug("empty checkpoint - cannot resume from graph break") + if isinstance(e, StepUnsupported): + unimplemented( + gb_type="torch._dynamo.step_unsupported() with empty checkpoint", + context="", + explanation="traced torch._dynamo.step_unsupported(), but there is no checkpoint " + "to step_graph_break from. This graph break is used for debugging only.", + hints=[ + "Remove the torch._dynamo.step_unsupported() call.", + "Include at least one checkpoint: (1) include at least 2 ops and (2) make sure there is some " + "line of code that is not in a try/with block, and has an empty Python stack.", + *graph_break_hints.DYNAMO_BUG, + ], + skip_frame=True, + ) + e.skip_frame = True + raise + reason = ( + "Encountered graph break that we cannot resume from. " + "Compiling up to the previous resumable state, " + "then skipping the rest of the function. " + f"Graph break encountered:\n\n{str(e)}" + ) + self.log_graph_break( + self.code_options, + reason=reason, + exc=e, + ) + + self.current_speculation.fail_and_restart_analysis(self.error_on_graph_break) + return False + + if sys.version_info >= (3, 11): + + def update_block_stack(self, inst: Instruction) -> None: + # 3.11+ no longer uses a block stack, but we still keep track of one + # so that we know which contexts are currently active. + # For our purposes, all exception table entries with the same target + # are considered to be part of the same "block". + # NOTE: we only keep track of with blocks that are not contained in try blocks. + # This is because we will not create continuation functions on graph breaks in try blocks, + # but we may for with blocks. We do not push blocks here since + # with blocks are pushed when handling BEFORE_WITH. + entry = inst.exn_tab_entry + if entry: + # Detect when we have exited the top with block. + # The with blocks on the block stack are not enclosed in try + # blocks, so a with block's cleanup code should be in the + # previous with block (if any). + if ( + len(self.block_stack) >= 2 + and entry.target is not self.block_stack[-1].target + and entry.target is self.block_stack[-2].target + ): + # exit the current block + self.block_stack.pop() + else: + # no longer in any block + # It is possible for NOPs to be between two instructions + # in the same block, but the NOPs are not covered by an + # exception table entry. In this case, assume that we + # are still in the same block. + # In 3.12+, JUMP_BACKWARD might also not be covered by + # an exception table entry, so we also assume that we + # are still in the same block. It is probably safe to do + # this in 3.11, even though we haven't encountered this case before. + # In 3.14+, NOT_TAKEN might also not be covered by an exn table entry. + if self.block_stack and inst.opname not in ( + "NOP", + "JUMP_BACKWARD", + "NOT_TAKEN", + ): + # If we really escape from a block and the current + # instruction is not in another block, then there + # should be no other nested blocks that we are in. + assert len(self.block_stack) == 1 + self.block_stack.pop() + + else: + + def update_block_stack(self, inst: Instruction) -> None: + pass + + @property + def next_instruction(self) -> Instruction: + assert self.instruction_pointer is not None + return self.instructions[self.instruction_pointer] + + def step_graph_break(self, continue_inst: Instruction) -> None: + # generate code from checkpoint + assert not self.output.output_instructions + assert self.current_speculation is not None + # NOTE: adding an assert here since it seems like the only place + # where we call step_graph_break right now is when the stack is empty, + # so let's enforce that for now. + assert not self.stack + # NOTE: if we support non-empty self.stack in the future, the `stack_pops` argument + # below should be set to the stack length to ensure that the stack is codegen'd + # for the rest of the function. + log.debug("step triggered compile") + all_stack_locals_metadata = self.output.compile_subgraph( + self, + reason=GraphCompileReason("step_unsupported", [self.frame_summary()]), + ) + # current frame state + # cells, + # [ + # frame N locals, + # frame N-1 stack + locals, + # ..., + # frame 1 stack + locals, + # ], + if self.parent: + from .eval_frame import skip_code + + # nested graph break + assert config.nested_graph_breaks + cg = PyCodegen(self.output.root_tx) + + # codegen cells and frame values only for frame N + cg.extend_output( + [ + *create_copy(2), + cg.create_load_const(0), + cg.create_binary_subscr(), + create_instruction("BUILD_LIST", arg=1), + *create_copy(2), + cg.create_load_const(0), + cg.create_binary_subscr(), + create_instruction("BUILD_LIST", arg=1), + ] + ) + # No need to fix stack, since stack is assumed to be empty here. + # Do NOT handle_inactive_ctx because we will be skipping this resume code. + leaf_resume_code, leaf_resume_name = self.create_resume( + 0, continue_inst, all_stack_locals_metadata[0], [], cg, True, False + ) + skip_code(leaf_resume_code) + + cleanup: list[Instruction] = [] + _reconstruct_block_stack(self.parent, cg, cleanup) + + # current frame state + # cells, + # [ + # frame N locals, + # frame N-1 stack + locals, + # ..., + # frame 1 stack + locals, + # ], [frame N cells], [frame N locals], + self.codegen_call_resume([leaf_resume_code], [leaf_resume_name], cg) + + cg.extend_output(cleanup) + + # current frame state + # cells, + # [ + # frame N locals, + # frame N-1 stack + locals, + # ..., + # frame 1 stack + locals, + # ], leaf_resume result + + # pop frame N cells and locals + cg.extend_output( + [ + *create_copy(2), + cg.create_load_const(0), + create_instruction("DELETE_SUBSCR"), + *create_copy(3), + cg.create_load_const(0), + create_instruction("DELETE_SUBSCR"), + ] + ) + + # current frame state + # cells, frame_values, leaf_resume result + # extract frame N-1 stack + num_stack = all_stack_locals_metadata[1].num_stack + cg.extend_output( + [ + *create_copy(2), + cg.create_load_const(0), + cg.create_binary_subscr(), + *create_binary_slice(0, num_stack), + ] + ) + + # current frame state + # cells, frame_values, leaf_resume result, frame N-1 stack + # add the leaf_resume result to frame N-1 stack + cg.extend_output( + [ + *create_swap(2), + create_instruction("LIST_APPEND", arg=1), + ] + ) + self.parent.push(UnknownVariable()) + all_stack_locals_metadata[1].num_stack += 1 + + # current frame state + # cells, frame_values, frame N-1 stack + leaf_resume result + # remove frame N-1 stack from frame_values + if num_stack > 0: + cg.extend_output( + # frame_values[0] = frame_values[0][num_stack:] + [ + *create_copy(2), + cg.create_load_const(0), + cg.create_binary_subscr(), + *create_binary_slice(num_stack, None), + *create_copy(3), + cg.create_load_const(0), + create_instruction("STORE_SUBSCR"), + ] + ) + + # current frame state + # cells, frame_values, frame N-1 stack + leaf_resume result + # unpack the stack (need to unpack twice since UNPACK_SEQUENCE unpacks in reverse order) + cg.extend_output( + [ + create_instruction("UNPACK_SEQUENCE", arg=num_stack + 1), + create_instruction("BUILD_LIST", arg=num_stack + 1), + create_instruction("UNPACK_SEQUENCE", arg=num_stack + 1), + ] + ) + + # call the remaining resume functions + # current frame state + # [frame N-1 cells, ..., frame 1 cells], + # [ + # frame N-1 locals, + # frame N-2 stack + locals, + # ..., + # frame 1 stack + locals, + # ], *(frame N-1 stack), leaf_resume result + self.output.add_output_instructions( + cg.get_instructions() + + self.parent.create_call_resume_at( + self.parent.next_instruction, all_stack_locals_metadata[1:] + ) + ) + else: + # NOTE: if WithExitFunctionVariable is reconstructed here, then the generated bytecode will be wrong. + # However, we don't expect this to happen since WithExitFunctionVariable can only be present on the stack, + # which must be empty in step graph breaks. + # If we do decide to support step graph breaks with WithExitFunctionVariable in the future, we should + # either call a skipped resume function as in the nested step graph break case, or reconstruct the + # proper context manager object from the class (like what we used to do historically in variables/ctx_manager.py). + + # pop cells + self.output.add_output_instructions( + [ + *create_swap(2), + create_instruction("POP_TOP"), + ] + ) + # load locals from frame values + cg = PyCodegen(self.output.root_tx) + self.output.add_output_instructions( + [ + cg.create_load_const(-1), + cg.create_binary_subscr(), + ] + ) + for local, idx in all_stack_locals_metadata[-1].locals_names.items(): + self.output.add_output_instructions( + [ + create_dup_top(), + cg.create_load_const(idx), + cg.create_binary_subscr(), + cg.create_store(local), + ] + ) + self.output.add_output_instructions( + [ + create_instruction("POP_TOP"), + create_jump_absolute(continue_inst), + *self.instructions, + ] + ) + + def run_ctx_mgr(self) -> Any: + # NB: Don't push the top level frame summary; set_current_loc will + # take care of it. However, DO make sure we attach real_stack to + # exceptions + return TracingContext.current_frame(None) + + def run(self) -> None: + with self.run_ctx_mgr(): + dump_file(self.f_code.co_filename) + try: + self.output.push_tx(self) + self.start_point = self.instruction_pointer + try: + while self.step(): + pass + except Exception as e: + if self.is_tracing_resume_prologue: + raise ResumePrologueTracingError( + "Error while tracing through a Dynamo-generated resume function prologue. " + "Errors are not allowed when tracing resume function prologues.\n" + f"{type(e).__qualname__}: {str(e)}" + ).with_traceback(e.__traceback__) from None + raise + except TensorifyScalarRestartAnalysis: + raise + except BackendCompilerFailed: + raise + except RuntimeError as e: + # If the root tx fails to handle the graph break, then the caller (convert_frame) + # will skip the frame and fall back to eager. + # This code path happens e.g. for bytecodes we don't support + # or when we are unable to resume from a graph break. + if ( + isinstance(e, (Unsupported, UserError)) + and isinstance(self, InstructionTranslator) + and not self.error_on_graph_break + and not self.one_graph + ): + # log graph break if we won't error + reason = ( + "Failed to handle graph break gracefully. " + "Skipping the function and falling back to eager. " + f"Graph break encountered:\n\n{str(e)}" + ) + self.log_graph_break( + self.code_options, + reason=reason, + exc=e, + ) + + if not isinstance(e, exc.RestartAnalysis): + self.output.side_effects.log_side_effects_summary() + + if hasattr(e, "msg") and "Data-dependent" in e.msg: + readable_graph = torch.fx.GraphModule( + self.output.nn_modules, self.output.graph + ).print_readable( + print_output=False, include_stride=True, include_device=True + ) + e.partial_fx_graph = readable_graph # type: ignore[attr-defined] + raise + + raise + except Exception as e: + if self.exec_recorder: + e.exec_record = self.exec_recorder.get_record() # type: ignore[attr-defined] + if not isinstance(e, exc.RestartAnalysis): + self.output.side_effects.log_side_effects_summary() + + raise + finally: + self.output.pop_tx() + # Cleanup the outputGraph to delete the held tensors. We perform the + # cleanup only for InstructionTranslator and not + # InliningInstructionTranslator. The InliningInstructionTranslator + # mutates the output object and is restored to original state if + # there was an exception. + if isinstance(self, InstructionTranslator): + self.output.cleanup() + + # Note that this call maybe redundant if compile_subgraph is + # called. This is ok, because calling exit stack close() + # twice is not an issue (second stop is a no op). + self.output.mark_bytecode_tracing_stop() + + def push(self, val: VariableTracker) -> None: + assert isinstance(val, VariableTracker), ( + f"push expects VariableTracker, got {typestr(val)}" + ) + self.stack.append(val) + + def push_many(self, vals: list[VariableTracker]) -> None: + for val in vals: + self.push(val) + + def pop(self) -> VariableTracker: + return self.stack.pop() + + def popn(self, n: int) -> list[VariableTracker]: + return [*reversed([self.pop() for _ in range(n)])] + + def LOAD_FAST(self, inst: Instruction) -> None: + name = inst.argval + if self.exec_recorder and name in self.f_locals: + self.exec_recorder.add_local_var(name, self.f_locals[name]) + + try: + self.push(self.symbolic_locals[name].unwrap()) + except KeyError: + if name.startswith("."): + try: + # This happens in dict/list comprehensions + new_name = name.replace(".", "implicit") + self.push(self.symbolic_locals[new_name]) + except KeyError: + unimplemented( + gb_type="Attempted to read undefined local variable (implicit)", + context=f"LOAD_FAST {name}", + explanation=f"Could not find an implicit local variable with name `{name}`", + hints=[ + "This happens in dict/list comprehensions", + *graph_break_hints.USER_ERROR, + ], + ) + else: + unimplemented( + gb_type="Attempted to read undefined local variable", + context=f"LOAD_FAST {name}", + explanation=f"Could not find a local variable with name `{name}`", + hints=[*graph_break_hints.USER_ERROR], + ) + + # for continuation functions + if name.startswith("__stack"): + self.symbolic_locals.pop(name) + + def LOAD_DEREF(self, inst: Instruction) -> None: + assert inst.argval in self.cell_and_freevars() + cell = self.symbolic_locals[inst.argval] + contents_var = self.output.side_effects.load_cell(cell) + self.push(contents_var) + + if self.exec_recorder and inst.argval in self.f_locals: + self.exec_recorder.add_local_var(inst.argval, self.f_locals[inst.argval]) + + def STORE_FAST(self, inst: Instruction) -> None: + name = inst.argval + loaded_vt = self.pop() + loaded_vt.set_name_hint(name) + self.symbolic_locals[name] = loaded_vt + if name == IS_TRACING_RESUME_PROLOGUE_VARNAME: + val = loaded_vt.as_python_constant() + assert type(val) is bool + self.is_tracing_resume_prologue = val + + def DELETE_FAST(self, inst: Instruction) -> None: + var = self.symbolic_locals.get(inst.argval) + if isinstance(var, TensorVariable): + self._maybe_emit_sync_dealloc(var) + del self.symbolic_locals[inst.argval] + + def _maybe_emit_sync_dealloc(self, var: TensorVariable) -> None: + from .variables.streams import get_current_stream, new_event + + device = var.device + if device is None or device.type not in ("cuda", "xpu"): + return + + node = var.proxy.node + alloc_stream = node.meta.get("custom", {}).get("stream", None) + + users = set(node.users.keys()) + if not users: + return + last_user = None + for n in self.output.graph.nodes: + if n in users: + last_user = n + if last_user is None or last_user.op == "output": + return + + last_use_stream = last_user.meta.get("custom", {}).get("stream", None) + if alloc_stream == last_use_stream: + return + + if alloc_stream is None: + alloc_stream = get_current_stream(device) + if last_use_stream is None: + last_use_stream = get_current_stream(device) + + event_idx = new_event() + self.output.create_proxy( + "call_function", + torch.ops.streams.record_event, + (event_idx, last_use_stream), + {}, + ) + self.output.create_proxy( + "call_function", + torch.ops.streams.sync_dealloc, + (event_idx, alloc_stream, var.as_proxy()), + {}, + ) + + def STORE_DEREF(self, inst: Instruction) -> None: # type: ignore[override] + assert inst.argval in self.cell_and_freevars() + cell = self.symbolic_locals[inst.argval] + val = self.pop() + self.output.side_effects.store_cell(cell, val) + + assert isinstance(cell, CellVariable) # tame mypy + if cell.local_name is not None: + val.set_name_hint(cell.local_name) # type: ignore[attr-defined] + + LOAD_CLOSURE = LOAD_FAST + + def _load_const(self, inst: Instruction) -> VariableTracker: + i = inst.arg + if i is None: + return ConstantVariable.create(value=inst.argval) # type: ignore[return-value] + val = self._constants_cache[i] + if not val: + self._constants_cache[i] = ConstantVariable.create(value=inst.argval) # type: ignore[call-overload] + val = self._constants_cache[i] + assert val is not None + return val + + def LOAD_CONST(self, inst: Instruction) -> None: + self.push(self._load_const(inst)) + + def _load_global(self, inst: Instruction) -> None: + name = inst.argval + + if self.exec_recorder: + if name in self.f_globals: + self.exec_recorder.add_global_var(name, self.f_globals[name]) + else: + assert name in self.f_builtins + self.exec_recorder.builtins[name] = self.f_builtins[name] + + if name not in self.f_globals: + return self.load_builtin(inst) + + if name in self.symbolic_globals: + variable = self.output.side_effects[self.symbolic_globals[name]] + self.push(self.output.side_effects.load_global(variable, name)) + return + + value = self.f_globals[name] + self.push(VariableTracker.build(self, value, GlobalSource(name))) + + @functools.cached_property + def nn_modules_globals_vt(self) -> VariableTracker: + module_name = "torch.nn.modules.module" + module_source = self.import_source(module_name) + fglobals_value = _import_module(module_name) + return VariableTracker.build(self, fglobals_value, module_source) + + def LOAD_GLOBAL(self, inst: Instruction) -> None: + assert inst.arg is not None + if sys.version_info >= (3, 11) and sys.version_info < (3, 13) and inst.arg % 2: + self.PUSH_NULL(inst) + self._load_global(inst) + if sys.version_info >= (3, 13) and inst.arg % 2: + self.PUSH_NULL(inst) + + def STORE_GLOBAL(self, inst: Instruction) -> None: + value = self.pop() + name = inst.argval + source = GlobalSource(name) + if name not in self.symbolic_globals: + self.symbolic_globals[name] = object() # type: ignore[assignment] # sentinel object + variable = self.output.side_effects.track_global_existing( + source, self.symbolic_globals[name] + ) + if isinstance(value, RemovableHandleVariable): + unimplemented( + gb_type="Storing Tensor hook handle in globals", + context=name, + explanation="This is not supported.", + hints=[], + ) + self.output.side_effects.store_global(variable, name, value) + + # Cache note: This cache only exists for the duration of this + # InstructionTranslator - so it should be safe to do. + @cache_method + def import_source(self, module_name: str) -> GlobalSource: + """Create an alias to a module for use in guards""" + if "torch_package" in module_name: + value = torch.package.package_importer._package_imported_modules[ + module_name + ] + alias = ( + module_name.replace(">", "_").replace("<", "_").replace(".", "_dot_") + ) + else: + value = _import_module(module_name) + alias = f"__import_{module_name.replace('.', '_dot_')}" + + if self.package is not None: + self.package.add_import_source(alias, module_name) + self.output.import_sources[alias] = module_name + f_globals = self.output.global_scope + assert alias not in f_globals or f_globals[alias] is value + f_globals[alias] = value + self.output.update_co_names(alias) + return GlobalSource(alias) + + def resolve_name(self, name: str, package: str, level: int) -> str: + """ + Copied from the Cpython implementation of __import__ + Resolve a relative module name to an absolute one. + https://github.com/python/cpython/blob/5a094f0255eea1db58fb2cf14c200971e64ec36e/Lib/importlib/_bootstrap.py#L902 + """ + bits = package.rsplit(".", level - 1) + if len(bits) < level: + raise ImportError("attempted relative import beyond top-level package") + base = bits[0] + return f"{base}.{name}" if name else base + + def calc_package(self) -> str: + """ + Copied from the Cpython implementation of __import__ + https://github.com/python/cpython/blob/5a094f0255eea1db58fb2cf14c200971e64ec36e/Lib/importlib/_bootstrap.py#L1090 + """ + package = self.f_globals.get("__package__") + spec = self.f_globals.get("__spec__") + if package is not None: + if spec is not None and package != spec.parent: + log.warning( + "__package__ != __spec__.parent (%r != %r)", + package, + spec.parent, + stacklevel=3, + ) + return package + elif spec is not None: + return spec.parent + else: + log.warning( + "can't resolve package from __spec__ or __package__, " + "falling back on __name__ and __path__", + stacklevel=3, + ) + package = self.f_globals["__name__"] + if "__path__" not in self.f_globals: + package = package.rpartition(".")[0] + return package + + def IMPORT_NAME(self, inst: Instruction) -> None: + level, fromlist = self.popn(2) + level = level.as_python_constant() + fromlist = fromlist.as_python_constant() + module_name = inst.argval + + # Are we replaying? if so, load recorded module + recorded_name = ( + f"{ExecutionRecorder.LOCAL_MOD_PREFIX}_{level}_{fromlist}_{module_name}" + ) + if recorded_name in self.f_globals: + value = self.f_globals[recorded_name] + source = GlobalSource(recorded_name) + else: + try: + value = __import__( + module_name, + fromlist=fromlist, + level=level, + globals=self.f_globals, + ) + except ImportError: + unimplemented( + gb_type="Import failure", + context=f"module_name: {module_name}, fromlist: {fromlist}, level={level}", + explanation="Failure when attempting to import.", + hints=[*graph_break_hints.USER_ERROR], + ) + + if level != 0: + pkg = self.calc_package() + module_name = self.resolve_name(module_name, pkg, level) + + # For __import__, when the name variable is of the form package.module, + # normally, the top-level package (the name up till the first dot) is + # returned, not the module named by module_name. However, when a + # non-empty fromlist argument is given, the module named by name is + # returned. Therefore, we set the source correctly here. + if not fromlist: + top_level_module_name = module_name.partition(".")[0] + source = self.import_source(top_level_module_name) + else: + source = self.import_source(module_name) + + if self.exec_recorder: + # pyrefly: ignore [unbound-name] + self.exec_recorder.add_local_mod(recorded_name, value) + + # pyrefly: ignore [unbound-name] + if isinstance(value, (types.ModuleType, DummyModule)): + # pyrefly: ignore [unbound-name, bad-argument-type] + self.push(PythonModuleVariable(value, source=source)) + else: + unimplemented( + gb_type="Bad import result", + # pyrefly: ignore [unbound-name] + context=typestr(value), + explanation="Import result is not a Python module.", + hints=[], + ) + + # fb internal 3.12 opcode + EAGER_IMPORT_NAME = IMPORT_NAME + + def IMPORT_FROM(self, inst: Instruction) -> None: + self.DUP_TOP(inst) + self._load_attr(inst.argval) + + # Cache note: This cache only exists for the duration of this + # InstructionTranslator - so it should be safe to do. + @cache_method + def load_builtin_from_argval(self, argval: Any) -> VariableTracker: + if argval not in self.f_builtins: + unimplemented( + gb_type="failed to find name in frame builtins", + context="", + explanation=f"Failed to find name `{argval}` in frame's builtins.", + hints=[ + *graph_break_hints.DYNAMO_BUG, + ], + ) + val = self.f_builtins[argval] + + if callable(val): + builtins_source = GlobalSource( + self.output.name_of_builtins_dict_key_in_fglobals + ) + var_source = DictGetItemSource(builtins_source, argval) + return VariableTracker.build(self, val, var_source) + else: + assert is_builtin_constant(val) + return VariableTracker.build(self, val) + + def load_builtin(self, inst: Instruction) -> None: + self.push(self.load_builtin_from_argval(inst.argval)) + + def jump(self, inst: Instruction | BlockStackEntry) -> None: + assert self.instruction_pointer is not None + assert self.start_point is not None + assert inst.target is not None + get_metrics_context().increment( + "ir_count", self.instruction_pointer - self.start_point + ) + self.instruction_pointer = self.indexof[inst.target] + self.start_point = self.instruction_pointer + + JUMP_FORWARD = jump + JUMP_ABSOLUTE = jump + + POP_JUMP_IF_FALSE = generic_jump(operator.not_, False) + POP_JUMP_IF_TRUE = generic_jump(operator.truth, False) + JUMP_IF_FALSE_OR_POP = generic_jump(operator.not_, True) + JUMP_IF_TRUE_OR_POP = generic_jump(operator.truth, True) + + def SETUP_LOOP(self, inst: Instruction) -> None: + # only exists in python<=3.7 + assert inst.target is not None + self.block_stack.append(BlockStackEntry(inst, inst.target, len(self.stack))) + + def SETUP_EXCEPT(self, inst: Instruction) -> None: + # only exists in python<=3.7 + assert inst.target is not None + self.block_stack.append(BlockStackEntry(inst, inst.target, len(self.stack))) + + def POP_BLOCK(self, inst: Instruction) -> None: + self.block_stack.pop() + + def SETUP_WITH(self, inst: Instruction) -> None: + self.setup_or_before_with(inst) + + def SETUP_FINALLY(self, inst: Instruction) -> None: + assert inst.target is not None + self.block_stack.append(BlockStackEntry(inst, inst.target, len(self.stack))) + + def FOR_ITER(self, inst: Instruction) -> None: + it = self.pop().realize() + self.push(it) + try: + val = it.next_variable(self) + self.push(val) + except (StopIteration, exc.ObservedUserStopIteration) as e: + if isinstance(e, exc.ObservedUserStopIteration): + exc.handle_observed_exception(self) + + if sys.version_info >= (3, 12): + # CPython 3.12 actually jumps to the instruction after the END_FOR + # and performs the action of END_FOR as part of FOR_ITER. We jump + # to the END_FOR and run it, so we need to make sure 2 values are + # on the stack for it to pop. + self.push(ConstantVariable.create(None)) + else: + # pop the iterator in Python < 3.12 + self.pop() + self.jump(inst) + + def _create_exception_type(self, val: VariableTracker) -> VariableTracker: + if isinstance( + val, (variables.BuiltinVariable, UserDefinedExceptionClassVariable) + ): + # Create the instance of the exception type + # https://github.com/python/cpython/blob/3.11/Python/ceval.c#L6547-L6549 + val = val.call_function(self, [], {}) # type: ignore[arg-type] + return val + + def _attach_traceback_to_exception(self, exc: ExceptionVals) -> None: + # based on CPython's PyTraceBack_Here impl + frame_summary = self.frame_summary() + tb = exc.var_getattr( + # pyrefly: ignore [bad-argument-type] + self, + "__traceback__", + ) + assert isinstance( + tb, (ConstantVariable, TracebackVariable) + ) # make pyrefly happy + new_tb = TracebackVariable.from_frame_summary(frame_summary, tb) + exc.call_method( + # pyrefly: ignore [bad-argument-type] + self, + "__setattr__", + [VariableTracker.build(self, "__traceback__"), new_tb], + {}, + ) + + def _raise_exception_variable(self, val: VariableTracker) -> NoReturn: + # User can raise exception in 2 ways + # 1) raise exception type - raise NotImplementedError + # 2) raise exception instance - raise NotImplementedError("foo") + + # 1) when user raises exception type + val = self._create_exception_type(val) + + # Handle https://peps.python.org/pep-0479/ + # CPython 3.12+ has a specific bytecode instruction (CALL_INTRINSIC_1 3) for this + if ( + is_generator(self.f_code) + and isinstance(val, variables.ExceptionVariable) + and val.exc_type is StopIteration + ): + val = VariableTracker.build(self, RuntimeError).call_function(self, [], {}) # type: ignore[arg-type] + + # Capture the python_stack when the exception is first raised. + # This preserves the original exception location even if the exception + # is later re-raised (e.g., in context manager cleanup). + # ExceptionVariable and UserDefinedExceptionObjectVariable both have + # a python_stack attribute. + if ( + self._isinstance_exception(val) + and getattr(val, "python_stack", None) is None + ): + val.python_stack = torch._guards.TracingContext.extract_stack() # type: ignore[union-attr] + + # 2) when user raises exception instance + if self._isinstance_exception(val): + # Save the exception in a global data structure + self.exn_vt_stack.set_current_exception(val) # type: ignore[arg-type] + + observed_exception_type = exc.get_dynamo_observed_exception(val.exc_type) # type: ignore[attr-defined, union-attr] + # Pass the stored python_stack to preserve the original exception location + python_stack = getattr(val, "python_stack", None) + raise observed_exception_type( + f"raised exception {val.debug_repr()}", real_stack=python_stack + ) + + exc.raise_observed_exception( + TypeError, + self, + args=[ + f"exceptions must derive from BaseException, not {val.python_type_name()}", + ], + ) + + def RAISE_VARARGS(self, inst: Instruction) -> None: + if inst.arg == 0: + if not len(self.exn_vt_stack): + exc.raise_observed_exception( + RuntimeError, self, args=["No active exception to reraise"] + ) + + # re-raise the previous exception. Here CPython refers to the exception + # on top of the exception stack + assert len(self.exn_vt_stack) + val = self.exn_vt_stack[-1] + assert self._isinstance_exception(val), val + self._raise_exception_variable(val) + elif inst.arg == 1: + # raise TOS + val = self.stack[-1] # type: ignore[assignment] + try: + self._raise_exception_variable(val) + finally: + # Update __traceback__ in the raised exception + curr_exc = self.exn_vt_stack.get_current_exception() + self._attach_traceback_to_exception(curr_exc) + else: + # raise .. from ... + from_vt = self.pop() + val = self.pop() # type: ignore[assignment] + try: + self._raise_exception_variable(val) + finally: + # Update __cause__/__suppress_context__ in the raised exception + curr_exc = self.exn_vt_stack.get_current_exception() + self._attach_traceback_to_exception(curr_exc) + cause = self._create_exception_type(from_vt) + curr_exc.call_method( + self, # pyrefly: ignore [bad-argument-type] + "__setattr__", + [VariableTracker.build(self, "__cause__"), cause], + {}, + ) # type: ignore[arg-type, union-attr, assignment] + + def CLEANUP_THROW(self, inst: Instruction) -> None: + # https://github.com/python/cpython/pull/96010 + tos = self.stack[-1] + assert isinstance(tos, ExceptionVariable) + if tos.exc_type is StopIteration: + unimplemented( + gb_type="CLEANUP_THROW with StopIteration", + context="", + explanation="Received StopIteration when handling generator.throw/close. This is not supported.", + hints=[], + ) + else: + self.RERAISE(inst) + + def RERAISE(self, inst: Instruction) -> None: + # https://docs.python.org/3/library/dis.html#opcode-RERAISE + # Re-raises the exception currently on top of the stack. If oparg is + # non-zero, pops an additional value from the stack which is used to + # set f_lasti of the current frame. + + if sys.version_info >= (3, 11): + # RERAISE is currently supported in a narrow case of `raise ... from None` + val = self.pop() + if inst.argval: + # RERAISE 1 + _ = self.pop() + self._raise_exception_variable(val) + else: + # RERAISE 0 + self.push(val) + self._raise_exception_variable(val) + else: + _exc = self.pop() + val = self.pop() + _tb = self.pop() + self._raise_exception_variable(val) + + def _isinstance_exception(self, val: VariableTracker) -> TypeIs[ExceptionVals]: + return isinstance(val, ExceptionVals) + + def WITH_EXCEPT_START(self, inst: Instruction) -> None: + args: list[VariableTracker] = [] + if sys.version_info >= (3, 11): + fn_loc = 4 if sys.version_info < (3, 14) else 5 + # At the top of the stack are 4 values: + # - TOP = exc_info() + # - SECOND = previous exception + # - THIRD: lasti of exception in exc_info() + # - FOURTH: the context.__exit__ bound method + # We call FOURTH(type(TOP), TOP, GetTraceback(TOP)). + # Then we push the __exit__ return value. + # In Python 3.14+, there is a NULL placed between the context.__exit__ bound method and the lasti, + # that is, fn is now the 5th from TOS. + assert len(self.stack) >= fn_loc + fn = self.stack[-fn_loc] + val = self.stack[-1] + assert self._isinstance_exception(val) + typ = BuiltinVariable(val.exc_type) # type: ignore[attr-defined, union-attr] + tb = val.var_getattr( + # pyrefly: ignore[bad-argument-type] + self, + "__traceback__", + ) + if sys.version_info >= (3, 14): + if not isinstance(self.stack[-4], NullVariable): + args.append(self.stack[-4]) + else: + assert len(self.stack) >= 7 + fn = self.stack[-7] + val = self.stack[-2] + assert self._isinstance_exception(val) + typ = BuiltinVariable(val.exc_type) # type: ignore[attr-defined] + + tb = val.var_getattr(self, "__traceback__") + + args += [typ, val, tb] + self.call_function(fn, args, {}) + + def exception_handler(self, raised_exception: ObservedException) -> None: + observed_exn_gb_explanation = ( + "Dynamo found no exception handler at the top-level compiled function " + "when encountering an exception. Exception will propagate outside the compiled region." + ) + + def bubble_exception_to_interpreter() -> None: + # Bubble the exception to the interpreter + curr_exc = self.exn_vt_stack.get_current_exception() + dynamo_exc = exc.get_dynamo_observed_exception(curr_exc.python_type()) + assert isinstance(raised_exception, dynamo_exc) # sanity check + unimplemented( + gb_type="Observed exception", + context=f"raised exception {curr_exc.debug_repr()}", + explanation=observed_exn_gb_explanation, + hints=[ + *graph_break_hints.USER_ERROR, + *graph_break_hints.SUPPORTABLE, + ], + from_exc=raised_exception, + ) + + if sys.version_info >= (3, 11): + exn_tab_entry = self.current_instruction.exn_tab_entry + if exn_tab_entry: + # Implementation is based on https://github.com/python/cpython/blob/3.11/Objects/exception_handling_notes.txt + + # 1) pop values from the stack until it matches the stack depth + # for the handler + while len(self.stack) > exn_tab_entry.depth: + self.pop() + + # 2) if 'lasti' is true, then push the offset that the exception was raised at + if exn_tab_entry.lasti: + self.push( + VariableTracker.build(self, self.current_instruction.offset) + ) + + # 3) push the exception to the stack + self.push(self.exn_vt_stack.get_current_exception()) + + # 4) jump to the handler + self.jump(exn_tab_entry) # type: ignore[arg-type] + else: + # No handler found. Bubble the exception to the parent + # instruction translator. We use special exception for this. + self.stack.clear() + + # attach traceback to the exception and set it as current exception + curr_exc = self.exn_vt_stack.get_current_exception() + self._attach_traceback_to_exception(curr_exc) + + if type(self) is InstructionTranslator: + bubble_exception_to_interpreter() + raise raised_exception + else: + if len(self.block_stack): + # base implementation - https://github.com/python/cpython/blob/3.10/Python/ceval.c#L4455 + + block_stack_entry = self.block_stack.pop() + + while block_stack_entry.inst.opname == "EXCEPT_HANDLER": + # TODO(anijain2305) - This is not tested .. unable to create a testcase + # https://github.com/python/cpython/blob/3.10/Python/ceval.c#L1456 + self.popn(3) + self.exn_vt_stack.pop() + if len(self.block_stack) == 0: + # No handler found in this frame. Bubble the exception to the parent + # instruction translator. + self.stack.clear() + if type(self) is InstructionTranslator: + unimplemented( + gb_type="Observed exception (EXCEPT_HANDLER)", + context=str(raised_exception), + explanation=observed_exn_gb_explanation + + " This graph break is unexpected.", + hints=[*graph_break_hints.DYNAMO_BUG], + from_exc=raised_exception, + ) + + raise raised_exception + block_stack_entry = self.block_stack.pop() + + exception_var = self.exn_vt_stack.get_current_exception() + self.exn_vt_stack.move_current_exception_to_stack() + + # 1) pop values from the stack until it matches the stack depth + # for the handler + while len(self.stack) > block_stack_entry.stack_index: + self.pop() + + # Push a dummy block stack entry of EXCEPT_HANDLER + # https://github.com/python/cpython/blob/3.10/Python/ceval.c#L1456 + except_handler_inst = Instruction(int(1e6), "EXCEPT_HANDLER", None, 0) + self.block_stack.append( + BlockStackEntry(except_handler_inst, None, len(self.stack)) + ) + + # Push old exception + if len(self.exn_vt_stack) >= 2: + old_exception = self.exn_vt_stack[-2] + + # Push the old exception on to stack - tb, value, type + # Traceback is currently mapped to UnknownVariable + self.push(variables.UnknownVariable()) + self.push(old_exception) + + self.push(variables.BuiltinVariable(old_exception.exc_type)) + else: + # Push empty exception tb, value, type + self.push(ConstantVariable.create(None)) + self.push(ConstantVariable.create(None)) + self.push(ConstantVariable.create(None)) + + # Push new exception - tb, val, type + # Traceback is currently mapped to UnknownVariable + self.push(variables.UnknownVariable()) + self.push(exception_var) + + self.push(variables.BuiltinVariable(exception_var.exc_type)) + + # Jump to target + self.jump(block_stack_entry) + else: + # No handler found. Bubble the exception to the parent + # instruction translator. We use special exception for this. + self.stack.clear() + if type(self) is InstructionTranslator: + bubble_exception_to_interpreter() + raise raised_exception + + def PUSH_EXC_INFO(self, inst: Instruction) -> None: + # https://docs.python.org/3/library/dis.html#opcode-PUSH_EXC_INFO + # Pops a value from the stack. Pushes the current exception to the top + # of the stack. Pushes the value originally popped back to the stack. + # + # The behavior of this opcode in CPython is a bit different than what it + # is described. It pops a value from the stack, pushes the top of the + # exception stack to the interpreter stack and moves the + # "current exception" to the exception stack. + # + # As an example, suppose the stack is in the following state: + # + stack = [..., ConstantVariable(1), ConstantVariable(2)] + # + current_exception = TypeError + # + exception_stack = [ValueError] + # + # After PUSH_EXC_INFO is executed + # + stack = [..., ConstantVariable(1), ValueError, ConstantVariable(2)] + # + current_exception = None + # + exception_stack = [ValueError, TypeError] + + val = self.pop() + if len(self.exn_vt_stack) == 0: + prev_exc: VariableTracker = ConstantVariable.create(None) + else: + prev_exc = self.exn_vt_stack[-1] + self.push(prev_exc) + self.push(val) + self.exn_vt_stack.move_current_exception_to_stack() + + def POP_EXCEPT(self, inst: Instruction) -> None: + if sys.version_info >= (3, 11): + _ = self.pop() + # This exception is handled and therefore we can clear the error indicator + assert len(self.exn_vt_stack) + self.exn_vt_stack.pop() + else: + assert len(self.block_stack) > 0 + if self.block_stack[-1].inst.opname != "EXCEPT_HANDLER": + raise AssertionError( + "Bug in Dynamo tracing of exception handling." + "Top of the block stack is not EXCEPT_HANDLER." + ) + self.block_stack.pop() + + self.popn(3) + + # This exception is handled and therefore we can clear the error indicator + assert len(self.exn_vt_stack) + self.exn_vt_stack.pop() + + def check_if_exc_matches(self) -> bool: + assert len(self.stack) >= 2 + expected_exc_types = self.pop() + if sys.version_info >= (3, 11): + # CHECK_EXC_MATCH (which is used from 3.11 onwards) does not pop. + # This is the description from the disassembly doc + # + # Performs exception matching for ``except``. Tests whether the ``STACK[-2]`` + # is an exception matching ``STACK[-1]``. Pops ``STACK[-1]`` and pushes the boolean + # result of the test. + exc_instance = self.stack[-1] + else: + # This is used prior to 3.11 via opcode JUMP_IF_NOT_EXC_MATCH + # There is no documentation but here is the code pointer that does 2 pops + # https://github.com/python/cpython/blob/3.10/Python/ceval.c#L3650-L3665 + exc_instance = self.stack.pop() + + # Users can check exception in 3 ways + # 1) except NotImplementedError --> BuiltinVariable + # 2) except CustomException --> UserDefinedExceptionClassVariable + # 3) except (NotImplementedError, AttributeError) -> TupleVariable + + if not isinstance( + expected_exc_types, + ( + BuiltinVariable, + TupleVariable, + UserDefinedExceptionClassVariable, + UserDefinedExceptionObjectVariable, + ), + ): + unimplemented( + gb_type="Exception with bad expected type", + context=str(expected_exc_types), + explanation=f"`except ...` has unsupported type {expected_exc_types}.", + hints=[*graph_break_hints.USER_ERROR], + ) + + if sys.version_info >= (3, 11): + if not self._isinstance_exception(exc_instance): + unimplemented( + gb_type="Caught non-Exception value", + context=str(exc_instance), + explanation=f"Except expects to receive an object of Exception type but received {exc_instance}.", + hints=[*graph_break_hints.USER_ERROR], + ) + + if isinstance(expected_exc_types, TupleVariable): + expected_types = expected_exc_types.items + else: + expected_types = [ + expected_exc_types, + ] + + for expected_type in expected_types: + if not isinstance( + expected_type, + ( + BuiltinVariable, + UserDefinedExceptionObjectVariable, + UserDefinedExceptionClassVariable, + ), + ): + unimplemented( + gb_type="Exception with non-type expectation", + context=str(expected_type), + explanation=f"`except ...` expects a non-type: {expected_type}.", + hints=[*graph_break_hints.USER_ERROR], + ) + if self._isinstance_exception(exc_instance) and issubclass( + exc_instance.exc_type, # type: ignore[union-attr] + expected_type.fn, # type: ignore[attr-defined] + ): + return True + elif isinstance(exc_instance, variables.BuiltinVariable) and issubclass( + exc_instance.fn, + # pyrefly: ignore [invalid-argument] + expected_type.fn, + ): + return True + + return False + + def CHECK_EXC_MATCH(self, inst: Instruction) -> None: + self.push(VariableTracker.build(self, self.check_if_exc_matches())) + + def JUMP_IF_NOT_EXC_MATCH(self, inst: Instruction) -> None: + if not self.check_if_exc_matches(): + self.jump(inst) + + def COMPARE_OP(self, inst: Instruction) -> None: + if inst.argval == "exception match": + self.CHECK_EXC_MATCH(inst) + else: + self.push(compare_op_handlers[inst.argval](self, self.popn(2), {})) + + def GET_ITER(self, inst: Instruction) -> None: + self.call_function(VariableTracker.build(self, iter), [self.pop()], {}) + + @break_graph_if_unsupported( + push=True, + msg_prefix="Encountered graph break when attempting to trace CALL_FUNCTION: a call to a regular function, e.g. f(x, y)", + ) + def CALL_FUNCTION(self, inst: Instruction) -> None: + args = self.popn(inst.argval) + fn = self.pop() + self.call_function(fn, args, {}) + + @break_graph_if_unsupported( + push=True, + msg_prefix="Encountered graph break when attempting to trace CALL_FUNCTION_EX: a variadic function call, e.g. f(*args)", + ) + def CALL_FUNCTION_EX(self, inst: Instruction) -> None: + kwargsvars: VariableTracker + if inst.argval == 0: + kwargsvars = ConstDictVariable({}) + argsvars = self.pop() + elif inst.argval == 1 or sys.version_info >= (3, 14): + # Python 3.14+ removed the argval and replaced it with a possibly NULL kwargs + kwargsvars = self.pop() + if isinstance(kwargsvars, NullVariable): + kwargsvars = ConstDictVariable({}) + argsvars = self.pop() + else: + unimplemented( + gb_type="Variadic function call with bad flags", + context=f"flags: {inst.argval}", + explanation=f"Attempted to call a variadic function (CALL_FUNCTION_EX) with bad flags {inst.argval}", + hints=[*graph_break_hints.DYNAMO_BUG], + ) + + if sys.version_info >= (3, 13): + # 3.13 swapped null and callable + null = self.pop() + assert isinstance(null, NullVariable) + + fn = self.pop() + + if sys.version_info >= (3, 11) and sys.version_info < (3, 13): + null = self.pop() + assert isinstance(null, NullVariable) + + if not isinstance( + argsvars, + BaseListVariable, + ) and argsvars.has_force_unpack_var_sequence(self): + argsvars = TupleVariable(argsvars.force_unpack_var_sequence(self)) + + # Unpack for cases like fn(**obj) where obj is a map + if isinstance(kwargsvars, UserDefinedObjectVariable): + kwargsvars = DictBuiltinVariable.call_custom_dict(self, dict, kwargsvars) # type: ignore[arg-type] + + # pyrefly: ignore [unbound-name] + if not isinstance(argsvars, BaseListVariable) or not isinstance( + # pyrefly: ignore [unbound-name] + kwargsvars, + ConstDictVariable, + ): + unimplemented( + gb_type="Variadic function call with bad args/kwargs type", + # pyrefly: ignore [unbound-name] + context=f"args type: {typestr(argsvars)}, kwargs type: {typestr(kwargsvars)}", + explanation="Expected args to be a list and kwargs to be a dict", + hints=[*graph_break_hints.USER_ERROR], + ) + + # Map to a dictionary of str -> VariableTracker + # pyrefly: ignore [bad-assignment, unbound-name] + kwargsvars = kwargsvars.keys_as_python_constant() + # pyrefly: ignore [bad-argument-type, unbound-name] + self.call_function(fn, argsvars.items, kwargsvars) + + @break_graph_if_unsupported( + push=True, + msg_prefix="Encountered graph break when attempting to trace CALL_FUNCTION_KW: " + "a function call with keyword arguments, e.g. f(x=True)", + ) + def CALL_FUNCTION_KW(self, inst: Instruction) -> None: + argnames = self.pop() + args = self.popn(inst.argval) + fn = self.pop() + assert isinstance(argnames, TupleVariable) and argnames.is_python_constant() + argnames = argnames.as_python_constant() + args, kwargs_list = args[: -len(argnames)], args[-len(argnames) :] + kwargs = dict(zip(argnames, kwargs_list)) + assert len(kwargs) == len(argnames) + self.call_function(fn, args, kwargs) + + def LOAD_METHOD_SUPER(self, inst: Instruction) -> None: + self.CALL_FUNCTION(dataclasses.replace(inst, argval=2)) + arg = inst.argval[0] + argval = self.code_options["co_names"][arg] + if sys.version_info < (3, 11): + self._load_attr(argval) + else: + self.LOAD_METHOD(dataclasses.replace(inst, argval=argval)) + + def LOAD_ATTR_SUPER(self, inst: Instruction) -> None: + self.CALL_FUNCTION(dataclasses.replace(inst, argval=2)) + arg = inst.argval[0] + argval = self.code_options["co_names"][arg] + self._load_attr(argval) + + def LOAD_METHOD(self, inst: Instruction) -> None: + self._load_attr(inst.argval) + obj = self.pop() + if sys.version_info >= (3, 13): + self.push(obj) + self.PUSH_NULL(inst) + elif sys.version_info >= (3, 11): + # always follow the NULL + fn convention, since if obj + # is actually a method, self is already bound to it, so it + # doesn't need to be passed in as an arg. + self.PUSH_NULL(inst) + self.push(obj) + else: + raise AssertionError( + "LOAD_METHOD should have been rewritten to LOAD_ATTR. We should never reach here." + ) + + def CALL_METHOD(self, inst: Instruction) -> None: + raise AssertionError( + "CALL_METHOD should have been rewritten to CALL_FUNCTION. This function should never be called." + ) + + def _load_attr(self, attr: Any) -> None: + obj = self.pop() + result = VariableTracker.build(self, getattr).call_function( + self, # type: ignore[arg-type] + [obj, VariableTracker.build(self, attr)], + {}, + ) + self.push(result) + + def LOAD_ATTR(self, inst: Instruction) -> None: + if sys.version_info >= (3, 12): + # pyrefly: ignore [unsupported-operation] + if inst.arg % 2: + self.LOAD_METHOD(inst) + return + self._load_attr(inst.argval) + + @break_graph_if_unsupported( + push=False, + msg_prefix="Encountered graph break when attempting to trace STORE_ATTR: storing an object's attribute, e.g. x.attr = y", + ) + def STORE_ATTR(self, inst: Instruction) -> None: + val, obj = self.popn(2) + VariableTracker.build(self, setattr).call_function( + self, # type: ignore[arg-type] + [obj, VariableTracker.build(self, inst.argval), val], + {}, + ) + + def DELETE_ATTR(self, inst: Instruction) -> None: + obj = self.pop() + self._maybe_sync_dealloc_attr(obj, inst.argval) + VariableTracker.build(self, delattr).call_function( + self, # type: ignore[arg-type] + [obj, VariableTracker.build(self, inst.argval)], + {}, + ) + + def _maybe_sync_dealloc_attr(self, obj: VariableTracker, name: str) -> None: + # Only check side_effects — a pure dict lookup with no observable + # side effects. We intentionally avoid var_getattr here because it + # can trigger __getattr__, add graph nodes, or cause graph breaks. + if self.output.side_effects.has_pending_mutation_of_attr(obj, name): + attr_var = self.output.side_effects.load_attr(obj, name) + if isinstance(attr_var, TensorVariable): + self._maybe_emit_sync_dealloc(attr_var) + + @staticmethod + def codegen_return_with_pops( + inst: Instruction, num_stack: int + ) -> list[Instruction]: + """ + Debug CPython expects the stack to be empty after the return. + Calling compile_subgraph will push cells and frame values to TOS. + This function will pop those 2 values from the stack before actually returning. + + Expects the stack to be: + cells, frame values, current frame stack (0 or 1 values) + + Pops cells and frame values, leaving the current frame stack as TOS. + A return instruction is included. + """ + insts = [] + # NOTE: Debug CPython expects the stack to be empty after the return. + # Expect the current stack to be in the state + # cells, frame values, current frame stack (0 or 1 values) + assert num_stack <= 1 + if num_stack == 1: + insts.extend(create_swap(3)) + return_inst = ( + create_instruction("RETURN_VALUE") + if inst.opname == "RETURN_VALUE" + else create_instruction("RETURN_CONST", argval=inst.argval) + ) + insts.extend( + [create_instruction("POP_TOP"), create_instruction("POP_TOP"), return_inst] + ) + return insts + + def create_resume( + self, + idx: int, + resume_inst: Instruction, + meta: StackLocalsMetadata, + resume_codes: list[types.CodeType], + cg: PyCodegen, + is_leaf: bool, + handle_inactive_ctx: bool, + ) -> tuple[types.CodeType, str]: + """ + Creates the resume function for the frame corresponding to `self`. + + Expects the TOS to be: + [frame N cells, ..., frame 1 cells], + [ + frame N stack + locals, + ..., + frame 1 stack + locals + ] + + Some additional codegen may happen to prepare the frame stack + locals values for the generated resume function: + - inactive context variables in the stack and locals will be replaced by their types + - if the frame is a leaf frame, prune dead locals + + Regardless of codegen, the stack will be left in the same state as before. + + Args: + - idx: depth of this frame: 0 corresponds to the leaf frame (frame N), N-1 to the root frame (frame 1). + - resume_inst: the instruction that this frame should resume at + - meta: metadata for this frame returned from OutputGraph.compile_subgraph + - resume_codes: nested resume code objects generated from previous create_resume calls. + - cg: codegen object to output to + - is_leaf: True if `self` corresponds to the leaf frame. + - handle_inactive_ctx: If True, handles inactive context variables as described above. This is necessary + iff the resume function is traced + """ + # Handle inactive context variables. + # The resume function assumes that context variables are the class, NOT the object. + # e.g. torch.set_grad_enabled(True) will be reconstructed as torch.set_grad_enabled + # NOTE: if the unsupported instruction modifies the inactive context variable, it may + # result in silent incorrectness! + if handle_inactive_ctx: + for (j, _), j_orig in zip(meta.stack_ctx_args, meta.stack_ctx_idxes_orig): + # Replace the stack var with the context class + ctx = cast(ContextWrappingVariable, self.stack[j_orig]) + # frames[idx][j] = reconstructed_ctx + cg.append_output(create_dup_top()) + ctx.reconstruct_type(cg) + cg.extend_output( + [ + *create_swap(2), + cg.create_load_const(idx), + cg.create_binary_subscr(), + cg.create_load_const(j), + create_instruction("STORE_SUBSCR"), + ] + ) + + for name, _ in meta.locals_ctx_args: + # Replace the local with the context class + ctx = cast(ContextWrappingVariable, self.symbolic_locals[name]) + # frames[idx][meta.num_stack +meta.locals_names[name]] = reconstructed_ctx + cg.append_output(create_dup_top()) + ctx.reconstruct_type(cg) + cg.extend_output( + [ + *create_swap(2), + cg.create_load_const(idx), + cg.create_binary_subscr(), + cg.create_load_const(meta.num_stack + meta.locals_names[name]), + create_instruction("STORE_SUBSCR"), + ] + ) + + # If the resume instruction is a jump absolute, then resume + # at the target instead. This handles the case where we + # graph break again in a nested function before jump-resuming + # this frame. + if is_jump_absolute(resume_inst): + assert resume_inst.target + resume_inst = resume_inst.target + + resume_name = unique_id(f"__resume_at_{resume_inst.offset}") + + # More locals may have been pruned in the current/leaf frame + # after the unsupported instruction (e.g. branch). + # There should not be any pruning in the other frames since + # the current instruction there should be a CALL. + if is_leaf: + reads = livevars_analysis(self.instructions, resume_inst) + all_argnames = tuple( + k + for k in self.symbolic_locals + if k in reads and k not in self.cell_and_freevars() + ) + argnames_null_set = set(meta.locals_null_keys) + argnames = tuple(k for k in all_argnames if k not in argnames_null_set) + argnames_null = tuple(k for k in all_argnames if k in argnames_null_set) + + # codegen filter for current frame's locals + # current stack state: frames + cg.extend_output( + [ + create_dup_top(), + cg.create_load_const(idx), + cg.create_binary_subscr(), + create_dup_top(), + ] + ) + for arg in argnames: + # current stack state: frames, frames[i], *(prev locals), frames[i] + cg.extend_output( + [ + create_dup_top(), + cg.create_load_const(meta.num_stack + meta.locals_names[arg]), + cg.create_binary_subscr(), + *create_swap(2), + ], + ) + # current stack state: frames, frames[i], *(frame i live locals), frames[i] + cg.extend_output( + [ + create_instruction("POP_TOP"), + create_instruction("BUILD_LIST", arg=len(argnames)), + *create_swap(2), + # frames, frames i live locals, frames[i] + *create_binary_slice(meta.num_stack, None, True), + # frames[i][num_stack:] = frame i live locals + ] + ) + # current stack state: frames + else: + argnames = tuple(meta.locals_names.keys()) + argnames_null = tuple(meta.locals_null_keys) + + if sys.version_info < (3, 12): + assert len(argnames_null) == 0, "variables should not be NULL in < 3.12" + + # compile_subgraph did not codegen any NULLs, + # so we should not count NullVariables + stack_len = len(self.stack) - len(meta.stack_null_idxes) + + assert self.current_instruction.offset is not None + new_code: types.CodeType = ContinueExecutionCache.lookup( + self.f_code, + self.lineno, + self.current_instruction.offset, + resume_inst.offset, + # pyre: ignore[missing-attribute] + tuple(b.target.offset for b in self.block_stack), + stack_len, + argnames, + argnames_null, + tuple(b.resume_fn() for b in self.block_stack), + handle_inactive_ctx, + tuple(meta.stack_ctx_args), + tuple(meta.locals_ctx_args), + tuple(meta.stack_null_idxes), + tuple(resume_codes), + not self.current_instruction_push, + ) + + # Add original GraphModule context to the resume function to handle + # the case of a graph break while tracing a GraphModule + orig_graphmodule_maybe = code_context.get_context(self.f_code).get( + "orig_graphmodule", lambda: None + )() + if orig_graphmodule_maybe is not None: + code_context.get_context(new_code)["orig_graphmodule"] = weakref.ref( + orig_graphmodule_maybe + ) + + # add resume function to the global scope + self.output.install_resume_function_global( + resume_name, new_code, self.f_globals + ) + if self.package is not None: + self.package.add_resume_function( + new_code, self.f_globals["__name__"], resume_name + ) + + counters["resumes"][new_code.co_name] += 1 + + return new_code, resume_name + + def create_call_resume_at( + self, + inst: Instruction, + all_stack_locals_metadata: list[StackLocalsMetadata], + ) -> list[Instruction]: + """ + Codegen all resume function(s) from the frame stack starting at `self`, call them, + and return the result. + Assumes that the unsupported instruction has already been run. + + Expects the TOS to be: + [ + frame N locals, + frame N-1 stack + locals, + ..., + frame 1 stack + locals + ], *(frame N stack (post-unsupported instruction)) + + Leaves the result of calling the resume functions on the stack and returns it + (empty stack after return). + + Args: + - inst: the instruction of the current (deepest) frame to resume at + - all_stack_locals_metadata: metadata returned from OutputGraph.compile_subgraph - contains + metadata such as local names, NULL positions, stack length, etc. + """ + + self.instruction_pointer = None + + cg = PyCodegen(self.output.root_tx) + + # NOTE: We do not need to codegen frames whose resume instruction is RETURN_VALUE + # We could also do something similar for RETURN_CONST, but a lot more code is necessary + # since we would need to track RETURN_CONST values and inject the constant in the right places. + + # Filter out tx'es that are resuming on RETURN_*. + txes: list[InstructionTranslatorBase] = [] + idxes: list[int] = [] + resume_insts: list[Instruction] = [] + cur_tx: InstructionTranslatorBase | None = self + idx = 0 + while cur_tx is not None: + if cur_tx is self: + resume_inst = inst + else: + resume_inst = cur_tx.next_instruction + if resume_inst.opname != "RETURN_VALUE": + txes.append(cur_tx) + idxes.append(idx) + resume_insts.append(resume_inst) + + cur_tx = cur_tx.parent + idx += 1 + + current_num_stack = len(self.stack) - len( + all_stack_locals_metadata[0].stack_null_idxes + ) + + # Every tx is returning - no need to call a resume function. + if not txes: + # Pop everything but TOS, then return the TOS. + # Frame N's stack must have length >= 1 since it's about to RETURN_VALUE. + # Frame N actually should have stack length == 1, because debug CPython expects + # empty stacks after return, but there is no guarantee written down anywhere. + assert current_num_stack >= 1 + cg.extend_output(create_swap(current_num_stack + 2)) + for _ in range(current_num_stack + 1): + cg.append_output(create_instruction("POP_TOP")) + cg.append_output(create_instruction("RETURN_VALUE")) + + return cg.get_instructions() + + # Let frame k be the deepest frame where the resume function is not RETURN_VALUE + # - If k == N, then the frame N stack is prepended to the frame N locals. + # - If k != N, then frame N's TOS is added to frame k's stack. + + # Rearrange the TOS to be compatible with create_resume and codegen_call_resume: + # [ + # frame N stack + locals, + # ..., + # frame 1 stack + locals + # ] + + # create the stack values that should be moved + if txes[0] is self: + # Frame N is non-returning, pack all of frame N's stack to + # be moved to frame N's frame values + cg.append_output(create_instruction("BUILD_LIST", arg=current_num_stack)) + # frame N stack is not yet on the frame N's frame values + stack_insert_idx = 0 + all_stack_locals_metadata[0].num_stack = current_num_stack + else: + # Frame N is returning. Let frame k be the deepest non-returning frame. + # Add frame N's TOS to frame k's stack. + # pop frame N stack except TOS + cg.extend_output(create_swap(current_num_stack)) + for _ in range(current_num_stack - 1): + cg.append_output(create_instruction("POP_TOP")) + cg.append_output(create_instruction("BUILD_LIST", arg=1)) + # frame k stack is already on frame k's frame values + stack_insert_idx = all_stack_locals_metadata[idxes[0]].num_stack + all_stack_locals_metadata[idxes[0]].num_stack += 1 + txes[0].push(UnknownVariable()) + + # move the predetermined stack value(s) to the deepest non-returning frame + cg.extend_output( + [ + *create_copy(2), + # frame_values, return_const, frame_values + cg.create_load_const(idxes[0]), + cg.create_binary_subscr(), + *create_binary_slice(stack_insert_idx, stack_insert_idx, True), + # frame_values[idxes[0]][stack_insert_idx:stack_insert_idx] = frame N stack/[return_const/TOS] + # frame_values left on top of stack + ] + ) + + # filter out frame values of skipped tx'es + filter_insts = [] + for idx in idxes: + filter_insts.extend( + [ + create_dup_top(), + cg.create_load_const(idx), + cg.create_binary_subscr(), + *create_swap(2), + ] + ) + # TOS: cells, frame_values[idxes[0]], ..., frame_values[idxes[...]], frame_values + filter_insts.extend( + [ + create_instruction("POP_TOP"), + create_instruction("BUILD_LIST", arg=len(idxes)), + ] + ) + # TOS: cells, filtered frame_values + + cg.extend_output(filter_insts) + # filter out cells of skipped tx'es using the same instructions in filter_insts, + # but with cells as TOS instead of frame values + cg.extend_output( + [ + *create_swap(2), + *copy.deepcopy(filter_insts), + *create_swap(2), + ] + ) + # TOS: filtered cells, filtered frame_values + + resume_codes: list[types.CodeType] = [] + resume_names = [] + for i, cur_tx in enumerate(txes): + resume_code, resume_name = cur_tx.create_resume( + i, + resume_insts[i], + all_stack_locals_metadata[idxes[i]], + resume_codes, + cg, + cur_tx is self, + True, + ) + resume_codes.append(resume_code) + resume_names.append(resume_name) + + self.codegen_call_resume(resume_codes, resume_names, cg) + cg.append_output(create_instruction("RETURN_VALUE")) + + return cg.get_instructions() + + @staticmethod + def codegen_call_resume( + resume_codes: list[types.CodeType], resume_names: list[str], cg: PyCodegen + ) -> None: + """ + Calls the provided resume functions. + + Expects the TOS to be in the state: + [frame N cells, ..., frame 1 cells], + [ + frame N stack + locals, + frame N-1 stack + locals, + ..., + frame 1 stack + locals + ] + + Pops the cells and frame values, leaving the result of calling the resume functions on TOS. + + Args: + - resume_codes: list of resume function code objects to call + - resume_names: list of the corresponding names of the resume functions + - cg: PyCodegen object to output instructions to + """ + # NOTE: We will load cells as we load resume functions + + # load resume functions except the root's + cg.extend_output(create_copy(2)) + for i, (name, code) in enumerate(zip(resume_names, resume_codes)): + if i == len(resume_names) - 1: + break + # stack: cells, frames, *(resume 1, ...), cells + if code.co_freevars: + cg.extend_output( + [ + create_dup_top(), + cg.create_load_const(i), + cg.create_binary_subscr(), + ] + ) + # Call the factory function (stored under resume_name) to + # create the resume function with correct globals and closure. + cg.extend_output( + [ + cg.create_load_global(name, add=True), + *create_swap(2), + *create_call_function(1, push_null=True), + ] + ) + else: + cg.extend_output(cg.load_function_name(name, False, 0)) + cg.extend_output(create_swap(2)) + cg.extend_output( + [ + create_instruction("POP_TOP"), + create_instruction("BUILD_LIST", arg=len(resume_codes) - 1), + ] + ) + + # stack: cells, frames, [resume 1, ..., resume N - 1] + # load root resume function + cg.extend_output(create_swap(3)) + if resume_codes[-1].co_freevars: + cg.extend_output( + [ + cg.create_load_const(-1), + cg.create_binary_subscr(), + ] + ) + # Call the factory function to create the resume function with + # correct globals and closure. + cg.extend_output( + [ + cg.create_load_global(resume_names[-1], add=True), + *create_swap(2), + *create_call_function(1, push_null=True), + ] + ) + cg.extend_output( + [ + *create_rot_n(3), + ] + ) + else: + cg.extend_output( + [ + create_instruction("POP_TOP"), + *cg.load_function_name(resume_names[-1], False), + *create_rot_n(3), + ] + ) + + # resume 1, [resume N, ..., resume 2], frames + + # load top level-frame; final stack state should be: + # first resume function (+ NULL), + # [ + # [resume N, ..., resume 2], + # [ + # frame N stack + locals, + # ..., + # frame 2 stack + locals, + # ], *(frame 1 stack + locals) + # ] + cg.extend_output( + [ + create_dup_top(), + create_dup_top(), + # frames, frames, frames + cg.create_load_const(-1), + cg.create_binary_subscr(), + # frames, frames, frames[-1] + *create_swap(2), + # frames, frames[-1], frames + cg.create_load_const(-1), + create_instruction("DELETE_SUBSCR"), + ] + ) + + # TOS: resume 1, remaining resumes, frames (popped), frame 1 stack + locals + cg.extend_output( + [ + *create_rot_n(3), + create_instruction("BUILD_LIST", arg=2), + *create_swap(2), + # [resumes, frames (popped)], frame 1 stack + locals + create_instruction("LIST_EXTEND", arg=1), + ] + ) + + # TOS: resume 1, [remaining resumes, frames, *(frame 1 stack + locals)] + cg.extend_output(create_call_function_ex(False, True)) + + def should_compile_partial_graph(self) -> bool: + if sys.version_info >= (3, 11): + # Do not compile if current instruction's block is not the top with block + entry = self.current_instruction.exn_tab_entry + if entry and ( + not self.block_stack or entry.target is not self.block_stack[-1].target + ): + return False + return ( + all(b.can_restore() for b in self.block_stack) + and not self.one_graph + # Only the leaf tracer's error_on_graph_break should be used + and (self.is_child_tracer_active or not self.error_on_graph_break) + and not self.is_tracing_resume_prologue + and not self.active_generic_context_managers + # Do not allow nested graph breaks in HOPs + and self.output.current_tracer.parent is None + ) + + @break_graph_if_unsupported( + push=False, + msg_prefix="Encountered graph break when attempting to trace STORE_SUBSCR: trying to store subscript, e.g. x[key] = y", + ) + def STORE_SUBSCR(self, inst: Instruction) -> None: + val, obj, key = self.popn(3) + obj.call_method(self, "__setitem__", [key, val], {}) + + def DELETE_SUBSCR(self, inst: Instruction) -> None: + obj, key = self.popn(2) + # Check for tensor items using side-effect-free internal lookups + # only. We avoid call_method("__getitem__") because it can execute + # user code and add unwanted graph nodes. + self._maybe_sync_dealloc_subscr(obj, key) + obj.call_method(self, "__delitem__", [key], {}) + + def _maybe_sync_dealloc_subscr( + self, obj: VariableTracker, key: VariableTracker + ) -> None: + from .variables.dicts import ConstDictVariable + from .variables.lists import BaseListVariable + + item_var = None + try: + if isinstance(obj, BaseListVariable): + item_var = obj.getitem_const( + self, # pyrefly: ignore [bad-argument-type] + key, + ) + elif isinstance(obj, ConstDictVariable): + item_var = obj.maybe_getitem_const(key) + except Exception: + pass + if isinstance(item_var, TensorVariable): + self._maybe_emit_sync_dealloc(item_var) + + def BUILD_TUPLE(self, inst: Instruction) -> None: + items = self.popn(inst.argval) + self.push(TupleVariable(items)) + + def BUILD_SLICE(self, inst: Instruction) -> None: + items = self.popn(inst.argval) + self.push(SliceVariable(items, tx=self)) # type: ignore[arg-type] + + def _can_speculate_comprehension_nested(self) -> bool: + """Check if comprehension speculation is allowed in nested context. + + For the base class (non-inlined), this always returns False. + """ + return False + + def BUILD_LIST(self, inst: Instruction) -> None: + if maybe_setup_comprehension_speculation(self, inst): + return + + items = self.popn(inst.argval) + self.push(VariableTracker.build(self, items)) + + def BUILD_SET(self, inst: Instruction) -> None: + if config.inject_BUILD_SET_unimplemented_TESTING_ONLY: + unimplemented( + gb_type="missing BUILD_SET handler", + context="", + explanation="Missing BUILD_SET bytecode handler (for testing purposes).", + hints=[], + ) + items = self.popn(inst.argval) + new_set = SetVariable(items, mutation_type=ValueMutationNew()) + self.push(new_set) + + def BUILD_LIST_UNPACK(self, inst: Instruction, cls: type = ListVariable) -> None: + seqs = self.popn(inst.argval) + items = [] + for seq in seqs: + try: + items.extend(seq.force_unpack_var_sequence(self)) + except NotImplementedError: + unimplemented( + gb_type="Failed to unpack object for BUILD_LIST_UNPACK", + context=str(seq), + explanation=f"{seq} cannot be unpacked into a list for the BUILD_LIST_UNPACK " + "bytecode (`[*x, *y, ...]`).", + hints=[*graph_break_hints.USER_ERROR], + ) + self.push(cls(items, mutation_type=ValueMutationNew())) + + def BUILD_TUPLE_UNPACK(self, inst: Instruction) -> None: + self.BUILD_LIST_UNPACK(inst, cls=TupleVariable) + + BUILD_TUPLE_UNPACK_WITH_CALL = BUILD_TUPLE_UNPACK + + def BUILD_MAP(self, inst: Instruction) -> None: + if maybe_setup_comprehension_speculation(self, inst): + return + + items = self.popn(inst.argval * 2) + d = dict(zip(items[::2], items[1::2])) + self.push(VariableTracker.build(self, d)) + + def BUILD_MAP_UNPACK(self, inst: Instruction) -> None: + items = self.popn(inst.argval) + # ensure everything is a dict + items = [ + VariableTracker.build(self, dict).call_function(self, [x], {}) + for x in items + ] # type: ignore[arg-type] + result: dict[Any, Any] = {} + for x in items: + assert isinstance(x, ConstDictVariable) + result.update(x.items) + self.push( + VariableTracker.build( + self, + result, + ) + ) + + BUILD_MAP_UNPACK_WITH_CALL = BUILD_MAP_UNPACK + + def BUILD_CONST_KEY_MAP(self, inst: Instruction) -> None: + keys = self.pop() + values = self.popn(inst.argval) + assert isinstance(keys, TupleVariable) + assert keys.is_python_constant() + + keys = keys.force_unpack_var_sequence(self) + assert len(keys) == len(values) + + self.push( + VariableTracker.build( + self, + dict(zip(keys, values)), + ) + ) + + def MAP_ADD(self, inst: Instruction) -> None: + k, v = self.popn(2) + assert inst.argval > 0 + assert inst.arg is not None + obj = self.stack[-inst.arg].realize() + assert isinstance(obj, ConstDictVariable) + obj.call_method(self, "__setitem__", (k, v), {}) # type: ignore[arg-type] + + def SET_ADD(self, inst: Instruction) -> None: + v = self.pop() + assert inst.argval > 0 + assert inst.arg is not None + obj = self.stack[-inst.arg] + assert isinstance(obj, SetVariable) + assert obj.is_mutable() + obj.call_method(self, "add", [v], {}) # type: ignore[arg-type] + + def SET_UPDATE(self, inst: Instruction) -> None: + v = self.pop() + assert inst.argval > 0 + assert inst.arg is not None + obj = self.stack[-inst.arg] + assert isinstance(obj, SetVariable) + assert obj.is_mutable() + obj.call_method(self, "update", [v], {}) # type: ignore[arg-type] + + def LIST_APPEND(self, inst: Instruction) -> None: + v = self.pop() + assert inst.argval > 0 + assert inst.arg is not None + obj = self.stack[-inst.arg].realize() + assert isinstance(obj, ListVariable) + assert obj.is_mutable() + self.output.side_effects.mutation(obj) + obj.items.append(v) + + def MAKE_FUNCTION(self, inst: Instruction) -> None: + flags = inst.arg + if sys.version_info < (3, 11): + fn_name = self.pop() + code = self.pop() + if sys.version_info >= (3, 11): + # MAKE_FUNCTION behavior actually changed in 3.11, see + # https://github.com/python/cpython/pull/93189/ + assert hasattr(code.value, "co_qualname") # type: ignore[attr-defined] + fn_name = VariableTracker.build(self, code.value.co_qualname) # type: ignore[attr-defined] + defaults = None + closure = None + annotations = None + kwdefaults = None + + if sys.version_info < (3, 13): + # in 3.13, this is handled in SET_FUNCTION_ATTRIBUTE + if flags is not None: + if flags & 0x08: + closure = self.pop() + if flags & 0x04: + annotations = self.pop() + if flags & 0x02: + kwdefaults = self.pop() + if flags & 0x01: + defaults = self.pop() + + fn = NestedUserFunctionVariable( + fn_name, + code, + self.f_globals, + defaults, + kwdefaults, + closure, + ) + if annotations: + assert isinstance(annotations, TupleVariable) + # Convert the attribute to a dictionary before assigning it + # https://github.com/python/cpython/blob/28fb13cb33d569720938258db68956b5f9c9eb40/Objects/funcobject.c#L574-L594 + items = annotations.items + ann = ConstDictVariable( + dict(zip(items[::2], items[1::2], strict=True)), + mutation_type=ValueMutationNew(), + ) + fn.get_dict_vt(self).setitem( # pyrefly: ignore[bad-argument-type] + "__annotations__", ann + ) + self.push(fn) + + def UNPACK_SEQUENCE(self, inst: Instruction) -> None: + seq = self.pop() + if seq.is_tensor(): + val = seq.unpack_var_sequence(self, idxes=range(inst.argval)) # type: ignore[arg-type] + elif isinstance(seq, GetAttrVariable) and seq.obj.is_tensor(): + # x, y = a.shape + proxy = getattr(seq.obj.as_proxy(), seq.name) + val = [wrap_fx_proxy(self, proxy[i]) for i in range(inst.argval)] + elif seq.has_force_unpack_var_sequence(self): + val = seq.force_unpack_var_sequence(self) + else: + unimplemented( + gb_type="Failed to unpack object for UNPACK_SEQUENCE", + context=str(seq), + explanation=f"{seq} cannot be unpacked into a list for the UNPACK_SEQUENCE bytecode " + "(i.e. `a, b, c = d`).", + hints=[*graph_break_hints.USER_ERROR], + ) + if len(val) != inst.argval: + unimplemented( + gb_type="Length mismatch when unpacking object for UNPACK_SEQUENCE", + context=f"expected length: {inst.argval}, actual: {len(val)}", + explanation=f"{seq} unpacked to a list for the UNPACK_SEQUENCE bytecode " + "(i.e. `a, b, c = d`) with unexpected length.", + hints=[*graph_break_hints.DYNAMO_BUG], + ) + for i in reversed(val): + self.push(i) + + def UNPACK_EX(self, inst: Instruction) -> None: + assert 0 <= inst.argval <= 0xFFFF + prefix = inst.argval & 0xFF # low byte + suffix = inst.argval >> 8 # high byte + seq = self.pop() + if seq.has_force_unpack_var_sequence(self): + vals = list(seq.force_unpack_var_sequence(self)) + assert len(vals) >= prefix + suffix + vals_prefix = vals[:prefix] + vals_list = vals[prefix : len(vals) - suffix] + vals_suffix = vals[len(vals) - suffix :] + for item in reversed(vals_suffix): + self.push(item) + self.push(TupleVariable(vals_list)) + for item in reversed(vals_prefix): + self.push(item) + else: + unimplemented( + gb_type="Failed to unpack object for UNPACK_EX", + context=str(seq), + explanation=f"{seq} cannot be unpacked into a list for the UNPACK_EX bytecode.", + hints=[*graph_break_hints.USER_ERROR], + ) + + @break_graph_if_unsupported( + push=False, msg_prefix="Encountered intentional debugging graph break" + ) + def graph_break_on_leaf_function(self, inst: Instruction) -> None: + if self.has_no_inlined_calls: + unimplemented( + gb_type="Forced graph break on leaf function", + context="", + explanation="Forced graph break on non-inlining function for " + "nested graph break testing purposes", + hints=[ + "Set torch._dynamo.config.debug_force_graph_break_on_leaf_return = False", + ], + ) + + def NOP(self, inst: Instruction) -> None: + # Dynamo-specific testing behavior + if inst.argval == "GRAPH_BREAK_IF_LEAF": + self.graph_break_on_leaf_function(inst) + + def POP_TOP(self, inst: Instruction) -> None: + self.pop() + + def ROT_TWO(self, inst: Instruction) -> None: + a = self.pop() + b = self.pop() + self.push(a) + self.push(b) + + def ROT_THREE(self, inst: Instruction) -> None: + a = self.pop() + b = self.pop() + c = self.pop() + self.push(a) + self.push(c) + self.push(b) + + def ROT_FOUR(self, inst: Instruction) -> None: + a = self.pop() + b = self.pop() + c = self.pop() + d = self.pop() + self.push(a) + self.push(d) + self.push(c) + self.push(b) + + def DUP_TOP(self, inst: Instruction) -> None: + a = self.pop() + self.push(a) + self.push(a) + + def DUP_TOP_TWO(self, inst: Instruction) -> None: + a = self.pop() + b = self.pop() + self.push(b) + self.push(a) + self.push(b) + self.push(a) + + def _convert_value(self, value: VariableTracker, flag: int) -> VariableTracker: + if flag == 1: + return VariableTracker.build(self, str).call_function(self, [value], {}) # type: ignore[arg-type] + elif flag == 2: + return VariableTracker.build(self, repr).call_function(self, [value], {}) # type: ignore[arg-type] + elif flag == 3: + return VariableTracker.build(self, ascii).call_function(self, [value], {}) # type: ignore[arg-type] + return value + + def _format_value(self, fmt_spec: VariableTracker, flags: int) -> None: + value = self.pop() + if isinstance(value, SymNodeVariable): + from torch._dynamo.variables.lazy import ( + LazySymNodeFormatString, + LazyVariableTracker, + ) + + value = LazyVariableTracker.create( + LazySymNodeFormatString(value, fmt_spec), source=value.source + ) + self.push(value) + return + + value = self._convert_value(value, flags & 0x03) + + fmt_var = VariableTracker.build( + self, "{:" + fmt_spec.as_python_constant() + "}" + ) + + self.call_function(BuiltinVariable(str.format), [fmt_var, value], {}) + + def FORMAT_VALUE(self, inst: Instruction) -> None: + flags = inst.arg + assert flags is not None + if (flags & 0x04) == 0x04: + fmt_spec = self.pop() + else: + fmt_spec = VariableTracker.build(self, "") + return self._format_value(fmt_spec, flags) + + def BUILD_STRING(self, inst: Instruction) -> None: + format_string_parts: list[str] = [] + args: list[VariableTracker] = [] + kwargs: dict[str, VariableTracker] = {} + assert inst.arg is not None + for part in self.popn(inst.arg): + if part.is_python_constant(): + format_string_parts.append("{}") + args.append(part) + elif isinstance(part, variables.StringFormatVariable): + format_string_parts.append(part.format_string) + args.extend(part.sym_args) + if set(kwargs.keys()) & set(part.sym_kwargs.keys()): + unimplemented( + gb_type="BUILD_STRING key conflict", + context=f"format_string_parts: {format_string_parts}, kwargs: {kwargs}, part.sym_kwargs: {part.sym_kwargs}", + explanation="Failed to build format string due to key conflict", + hints=[*graph_break_hints.USER_ERROR], + ) + kwargs.update(part.sym_kwargs) + else: + unimplemented( + gb_type="BUILD_STRING type error", + context=str(part), + explanation="Format string part type is not correct - expected constant or format string.", + hints=[*graph_break_hints.USER_ERROR], + ) + self.push( + variables.StringFormatVariable.create( + "".join(format_string_parts), args, kwargs + ) + ) + + def IS_OP(self, inst: Instruction) -> None: + assert inst.argval == 0 or inst.argval == 1 + if inst.argval == 0: + new_argval = "is" + else: + new_argval = "is not" + new_inst = create_instruction("COMPARE_OP", argval=new_argval) + self.COMPARE_OP(new_inst) + + def CONTAINS_OP(self, inst: Instruction) -> None: + assert inst.argval == 0 or inst.argval == 1 + left, right = self.popn(2) + op = inst.argval + try: + self.push(right.call_method(self, "__contains__", [left], {})) + except ( + # right.__contains__ can raise TypeError + exc.ObservedTypeError, + # Ideally we should only capture TypeError here but some VTs don't + # implement hasattr(vt, "__contains__") entirely + Unsupported, + ) as excp: # object doesn't support __contains__ + # Use __iter__ as fallback + if isinstance(excp, Unsupported): + if excp.skip_frame: + # do not absorb graph break with skip_frame set + raise + excp.remove_from_stats() + self.push( + self.inline_user_function_return( + VariableTracker.build(self, impl_CONTAINS_OP_fallback), + [left, right], + {}, + ) + ) + if op == 1: + self.UNARY_NOT(inst) + + def LIST_EXTEND(self, inst: Instruction) -> None: + v = self.pop() + assert inst.argval > 0 + assert inst.arg is not None + obj = self.stack[-inst.arg] + assert isinstance(obj, ListVariable) + assert obj.is_mutable() + obj.call_method(self, "extend", [v], {}) # type: ignore[arg-type] + + def LIST_TO_TUPLE(self, inst: Instruction) -> None: + self.push( + VariableTracker.build(self, tuple).call_function(self, [self.pop()], {}) + ) # type: ignore[arg-type] + + def STOPITERATION_ERROR(self, inst: Instruction) -> None: + # wrap the generator body in a try: ... except StopIteration: ... which + # converts the StopIteration into a RuntimeError + # https://peps.python.org/pep-0479/ + # https://github.com/python/cpython/pull/99006 + # https://github.com/python/cpython/commit/28187141cc34063ef857976ddbca87ba09a882c2 + val = self.stack[-1] + assert self._isinstance_exception(val) + if val.exc_type is StopIteration: # type: ignore[union-attr] + new_val = VariableTracker.build(self, RuntimeError).call_function( + self, # type: ignore[arg-type] + [VariableTracker.build(self, "generator raised StopIteration")], + {}, + ) + new_val.call_setattr(self, VariableTracker.build(self, "__context__"), val) # type: ignore[attr-defined] + new_val.call_setattr(self, VariableTracker.build(self, "__cause__"), val) # type: ignore[attr-defined] + self.stack[-1] = new_val + + def DICT_MERGE(self, inst: Instruction) -> None: + v = self.pop() + assert inst.argval > 0 + assert inst.arg is not None + obj = self.stack[-inst.arg].realize() + assert isinstance(obj, ConstDictVariable) + assert obj.is_mutable() + obj.call_method(self, "update", [v], {}) # type: ignore[arg-type] + + DICT_UPDATE = DICT_MERGE + + def GEN_START(self, inst: Instruction) -> None: + self.pop() + + def GET_LEN(self, inst: Instruction) -> None: + tos = self.stack[-1] + if tos.is_python_constant(): + self.push(VariableTracker.build(self, len(tos.as_python_constant()))) + else: + self.push(tos.call_method(self, "__len__", [], {})) + + def MATCH_MAPPING(self, inst: Instruction) -> None: + """ + If STACK[-1] is an instance of collections.abc.Mapping, push True. + Otherwise, push False + """ + tos = self.stack[-1] + self.push( + self.inline_user_function_return( + VariableTracker.build(self, impl_IS_MAPPING), + [tos], + {}, + ) + ) + + def MATCH_SEQUENCE(self, inst: Instruction) -> None: + tos = self.stack[-1] + self.push( + self.inline_user_function_return( + VariableTracker.build(self, impl_MATCH_SEQUENCE), + [tos], + {}, + ) + ) + + def MATCH_CLASS(self, inst: Instruction) -> None: + subject, cls, names = self.popn(3) + arg = VariableTracker.build(self, inst.arg) + self.push( + self.inline_user_function_return( + VariableTracker.build(self, impl_MATCH_CLASS), + [subject, cls, arg, names], + {}, + ) + ) + + if sys.version_info < (3, 11): + # for versions < 3.11, also push the boolean result + tos = self.stack[-1] + self.push(VariableTracker.build(self, not istype(tos, ConstantVariable))) + + def MATCH_KEYS(self, inst: Instruction) -> None: + keys = self.stack[-1] + obj = self.stack[-2] + + assert isinstance(keys, TupleVariable) + + self.push( + self.inline_user_function_return( + VariableTracker.build(self, impl_MATCH_KEYS), [obj, keys], {} + ) + ) + + if sys.version_info < (3, 11): + # for versions < 3.11, also push the boolean result + tos = self.stack[-1] + self.push(VariableTracker.build(self, not istype(tos, ConstantVariable))) + + def LOAD_ASSERTION_ERROR(self, inst: Instruction) -> None: + self.push(self.load_builtin_from_argval("AssertionError")) + + def LOAD_BUILD_CLASS(self, inst: Instruction) -> None: + self.push(self.load_builtin_from_argval("__build_class__")) + + UNARY_POSITIVE = stack_op(operator.pos) + UNARY_NEGATIVE = stack_op(operator.neg) + UNARY_NOT = stack_op(operator.not_) + UNARY_INVERT = stack_op(operator.invert) + + BINARY_POWER = stack_op(operator.pow) + BINARY_MULTIPLY = stack_op(operator.mul) + BINARY_MATRIX_MULTIPLY = stack_op(operator.matmul) + BINARY_FLOOR_DIVIDE = stack_op(operator.floordiv) + BINARY_TRUE_DIVIDE = stack_op(operator.truediv) + BINARY_MODULO = stack_op(operator.mod) + BINARY_REMAINDER = stack_op(operator.mod) + BINARY_ADD = stack_op(operator.add) + BINARY_SUBTRACT = stack_op(operator.sub) + + BINARY_SUBSCR = break_graph_if_unsupported( + push=True, + msg_prefix="Encountered graph break when attempting to trace BINARY_SUBSCR: a binary subscript, e.g. x[attr]", + )(stack_op(operator.getitem)) + BINARY_LSHIFT = stack_op(operator.lshift) + BINARY_RSHIFT = stack_op(operator.rshift) + BINARY_AND = stack_op(operator.and_) + BINARY_OR = stack_op(operator.or_) + BINARY_XOR = stack_op(operator.xor) + + INPLACE_POWER = stack_op(operator.ipow) + INPLACE_MULTIPLY = stack_op(operator.imul) + INPLACE_MATRIX_MULTIPLY = stack_op(operator.imatmul) + INPLACE_FLOOR_DIVIDE = stack_op(operator.ifloordiv) + INPLACE_TRUE_DIVIDE = stack_op(operator.itruediv) + INPLACE_MODULO = stack_op(operator.imod) + INPLACE_REMAINDER = stack_op(operator.imod) + INPLACE_ADD = stack_op(operator.iadd) + INPLACE_SUBTRACT = stack_op(operator.isub) + INPLACE_LSHIFT = stack_op(operator.ilshift) + INPLACE_RSHIFT = stack_op(operator.irshift) + INPLACE_AND = stack_op(operator.iand) + INPLACE_XOR = stack_op(operator.ixor) + INPLACE_OR = stack_op(operator.ior) + + # 3.11 opcodes + def RESUME(self, inst: Instruction) -> None: + if inst.arg == 0: + self.append_prefix_inst(inst) + self.accept_prefix_inst = False + else: + assert not self.accept_prefix_inst + + if sys.version_info >= (3, 11): + + def BINARY_OP(self, inst: Instruction) -> None: + assert inst.arg is not None + return _binary_op_lookup[inst.arg](self, inst) + + def PRECALL(self, inst: Instruction) -> None: + pass + + def KW_NAMES(self, inst: Instruction) -> None: + kw_names = self.code_options["co_consts"][inst.arg] + assert isinstance(kw_names, tuple) + for name in kw_names: + assert isinstance(name, str) + assert self.kw_names is None + self.kw_names = ConstantVariable.create(value=kw_names) # type: ignore[assignment] + + def PUSH_NULL(self, inst: Instruction) -> None: + self.push(NullVariable()) + + def _call(self, inst: Instruction, call_kw: bool = False) -> None: + # see https://docs.python.org/3.11/library/dis.html#opcode-CALL + # for convention + if call_kw: + # TOS is kw_names for CALL_KW instruction + assert sys.version_info >= (3, 13) + kw_names = self.pop() + assert isinstance(kw_names, TupleVariable) and kw_names.is_python_constant() + kw_names = kw_names.as_python_constant() + else: + kw_names = self.kw_names.value if self.kw_names else () + + assert inst.arg is not None + contents = self.popn(inst.arg + 2) + if sys.version_info >= (3, 13): + # NULL and callable swapped + fn = contents[0] + args = [] if isinstance(contents[1], NullVariable) else [contents[1]] + else: + if isinstance(contents[0], NullVariable): + fn = contents[1] + # pyrefly: ignore [implicit-any] + args = [] + else: + fn = contents[0] + args = [contents[1]] + + if kw_names: + args = args + contents[2 : -len(kw_names)] + + kwargs_list = contents[-len(kw_names) :] + + kwargs = dict(zip(kw_names, kwargs_list)) + + assert len(kwargs) == len(kw_names) + else: + args = args + contents[2:] + # pyrefly: ignore [implicit-any] + kwargs = {} + + try: + # if call_function fails, need to set kw_names to None, otherwise + # a subsequent call may have self.kw_names set to an old value + self.call_function(fn, args, kwargs) + finally: + self.kw_names = None + + @break_graph_if_unsupported( + push=True, + msg_prefix="Encountered graph break when attempting to trace CALL: a function call, e.g. f(x, y)", + ) + def CALL(self, inst: Instruction) -> None: + self._call(inst) + + def COPY(self, inst: Instruction) -> None: + assert inst.arg is not None + self.push(self.stack[-inst.arg]) + + def SWAP(self, inst: Instruction) -> None: + assert inst.arg is not None + self.stack[-1], self.stack[-inst.arg] = self.stack[-inst.arg], self.stack[-1] + + JUMP_BACKWARD = jump + JUMP_BACKWARD_NO_INTERRUPT = jump + + POP_JUMP_FORWARD_IF_TRUE = generic_jump(operator.truth, False) + POP_JUMP_BACKWARD_IF_TRUE = generic_jump(operator.truth, False) + POP_JUMP_FORWARD_IF_FALSE = generic_jump(operator.not_, False) + POP_JUMP_BACKWARD_IF_FALSE = generic_jump(operator.not_, False) + + def CACHE(self, inst: Instruction) -> None: + pass + + def BEFORE_WITH(self, inst: Instruction) -> None: + self.setup_or_before_with(inst) + + def enter_ctx( + self, + ctx: ContextWrappingVariable | GenericContextWrappingVariable, + inst: Instruction, + ) -> VariableTracker: + if ( + isinstance(ctx, GenericContextWrappingVariable) + and not ctx.supports_graph_breaks() + ): + self.active_generic_context_managers.append(ctx) + + if sys.version_info >= (3, 11): + # See update_block_stack/create_resume for block stack details. + # Only push a block if the current instruction's block is a + # with block that is not nested in a try block - that is, the current + # instruction's block target is the same as the top block's target. + if inst.exn_tab_entry and ( + not self.block_stack + or inst.exn_tab_entry.target is not self.block_stack[-1].target + ): + target = None + else: + assert self.next_instruction.exn_tab_entry is not None + target = self.next_instruction.exn_tab_entry.target + else: + target = inst.target + + if target: + if isinstance(self, InstructionTranslator) or config.nested_graph_breaks: + self.block_stack.append( + BlockStackEntry(inst, target, len(self.stack), ctx) + ) + else: + self.block_stack.append(BlockStackEntry(inst, target, len(self.stack))) + + return ctx.enter(self) # type: ignore[arg-type] + + @staticmethod + def unsupported_ctx_graph_break(ctx: VariableTracker) -> NoReturn: + unimplemented( + gb_type="Unsupported context manager", + context=f"Attempted SETUP_WITH/BEFORE_WITH/LOAD_SPECIAL on {ctx}", + explanation=f"Dynamo does not know how to enter a `{ctx.python_type_name()}` context manager.", + hints=[ + "Avoid using the unsupported context manager.", + "If the context manager seems like it should be supported (e.g. torch.set_grad_enabled), then " + "it may be the case that it was created outside the compiled region, which Dynamo does not support. " + "Supported context managers can cross graph break boundaries only if they are local non-closure " + "variables, or are intermediate values.", + "File an issue to PyTorch. Simple context managers can potentially be supported, " + "but note that context managers can't be supported in general", + ], + ) + + def setup_or_before_with(self, inst: Instruction) -> None: + ctx = self.pop() + if not isinstance( + ctx, (ContextWrappingVariable, GenericContextWrappingVariable) + ): + self.unsupported_ctx_graph_break(ctx) + + # Need this redundant check for mypy + assert isinstance( + ctx, (ContextWrappingVariable, GenericContextWrappingVariable) + ) + + self.push(WithExitFunctionVariable(ctx, inst.target)) + self.push(self.enter_ctx(ctx, inst)) + + def append_prefix_inst(self, inst: Instruction) -> None: + assert self.accept_prefix_inst + self.prefix_insts.append(inst) + + def MAKE_CELL(self, inst: Instruction) -> None: + if sys.version_info >= (3, 12) and not self.accept_prefix_inst: + # In 3.12+, MAKE_CELL is not longer necessarily a prefix instruction. + # It can be generated by inlined comprehensions. + assert isinstance(self.symbolic_locals[inst.argval], NullVariable) + self.symbolic_locals[inst.argval] = ( + self.output.side_effects.track_cell_new() + ) + else: + self.append_prefix_inst(inst) + + def COPY_FREE_VARS(self, inst: Instruction) -> None: + self.append_prefix_inst(inst) + + def RETURN_GENERATOR(self, inst: Instruction) -> None: + self.append_prefix_inst(inst) + + # 3.12 opcodes + # BINARY/STORE_SLICE opcodes are broken down into + # BUILD_SLICE 2 and BINARY/STORE_SUBSCR + + def END_FOR(self, inst: Instruction) -> None: + if sys.version_info >= (3, 13): + self.pop() + else: + self.popn(2) + + # Decrement comprehension depth if exiting a comprehension layer + if sys.version_info >= (3, 12): + current_ip = self.indexof[inst] + if current_ip in self._comprehension_end_for_ips: + self._comprehension_end_for_ips.discard(current_ip) + self._comprehension_depth -= 1 + + def LOAD_FAST_CHECK(self, inst: Instruction) -> None: + if istype(self.symbolic_locals.get(inst.argval, None), NullVariable): + unimplemented( + gb_type="LOAD_FAST_CHECK on uninitialized variable", + context=inst.argval, + explanation=f"Attempted to load uninitialized local variable {inst.argval}", + hints=[*graph_break_hints.USER_ERROR], + ) + self.LOAD_FAST(inst) + + def LOAD_FAST_AND_CLEAR(self, inst: Instruction) -> None: + if inst.argval not in self.symbolic_locals: + self.push(NullVariable()) + else: + self.LOAD_FAST(inst) + self.symbolic_locals[inst.argval] = NullVariable() + + def LOAD_SUPER_ATTR(self, inst: Instruction) -> None: + self.CALL_FUNCTION(dataclasses.replace(inst, argval=2)) + assert inst.arg is not None + if inst.arg & 1: + self.LOAD_METHOD(inst) + else: + self._load_attr(inst.argval) + + def CALL_INTRINSIC_1(self, inst: Instruction) -> None: + if inst.argval == 3: + # INTRINSIC_STOPITERATION_ERROR + self.STOPITERATION_ERROR(inst) + elif inst.argval == 5: + # INTRINSIC_UNARY_POSITIVE + self.UNARY_POSITIVE(inst) + elif inst.argval == 6: + # INTRINSIC_LIST_TO_TUPLE + self.push(TupleVariable(self.pop().force_unpack_var_sequence(self))) + elif inst.argval == 7: + # INTRINSIC_TYPEVAR + v = self.pop().as_python_constant() + tv = variables.TypingVariable(TypeVar(v)) + self.push(tv) + else: + unimplemented( + gb_type="Missing CALL_INTRINSIC_1 handler", + context=f"CALL_INTRINSIC_1 operand: {inst.argval}", + explanation=f"No handler implemented for CALL_INTRINSIC_1 {inst.argval} instruction.", + hints=[*graph_break_hints.SUPPORTABLE], + ) + + def CALL_INTRINSIC_2(self, inst: Instruction) -> None: + arg2 = self.pop() + arg1 = self.pop() + if inst.argval == 4: + # INTRINSIC_SET_FUNCTION_TYPE_PARAMS + # same as => arg1.__type_params__ = arg2 + assert isinstance(arg1, BaseUserFunctionVariable) + arg1.call_method( + self, # pyrefly: ignore[bad-argument-type] + "__setattr__", + [ConstantVariable.create("__type_params__"), arg2], + {}, + ) + self.push(arg1) + else: + unimplemented( + gb_type="Missing CALL_INTRINSIC_2 handler", + context=f"CALL_INTRINSIC_2 operand: {inst.argval}", + explanation=f"No handler implemented for CALL_INTRINSIC_2 {inst.argval} instruction.", + hints=[*graph_break_hints.SUPPORTABLE], + ) + + def END_SEND(self, inst: Instruction) -> None: + tos = self.pop() + self.pop() + self.push(tos) + + # 3.13 opcodes + # fused instructions LOAD_FAST_LOAD_FAST, STORE_FAST_STORE_FAST, STORE_FAST_LOAD_FAST + # are broken down. + @break_graph_if_unsupported( + push=True, + msg_prefix="Encountered graph break when attempting to trace CALL_KW: " + "a function call with keyword arguments, e.g. f(x=True)", + ) + def CALL_KW(self, inst: Instruction) -> None: + self._call(inst, call_kw=True) + + def TO_BOOL(self, inst: Instruction) -> None: + # TO_BOOL only precedes a conditional jump or UNARY_NOT (see compile.c in CPython) + # So we can skip this instruction as long as we remember to codegen a TO_BOOL + # before conditional jumps/UNARY_NOT. + assert self.next_instruction.opname in ( + "POP_JUMP_IF_TRUE", + "POP_JUMP_IF_FALSE", + "UNARY_NOT", + ) + + def SET_FUNCTION_ATTRIBUTE(self, inst: Instruction) -> None: + flags = inst.arg + assert flags is not None + fn = self.pop() + assert isinstance(fn, NestedUserFunctionVariable) + attr = self.pop() + + if flags & 0x10: + assert sys.version_info >= (3, 14) + + # maybe use Format.VALUE_WITH_FAKE_GLOBALS instead? + # https://docs.python.org/3/library/annotationlib.html#annotationlib.Format.VALUE_WITH_FAKE_GLOBALS + attr = attr.call_function(self, [VariableTracker.build(self, 1)], {}) + fn.call_method( + self, # pyrefly: ignore[bad-argument-type] + "__setattr__", + [ConstantVariable.create("__annotations__"), attr], + {}, + ) + elif flags & 0x08: + fn.closure = attr + elif flags & 0x04: + assert isinstance(attr, TupleVariable) + # Convert the attribute to a dictionary before assigning it + # https://github.com/python/cpython/blob/28fb13cb33d569720938258db68956b5f9c9eb40/Objects/funcobject.c#L574-L594 + items = attr.items + ann = ConstDictVariable( + dict(zip(items[::2], items[1::2], strict=True)), + mutation_type=ValueMutationNew(), + ) + fn.get_dict_vt(self).setitem( # pyrefly: ignore[bad-argument-type] + "__annotations__", ann + ) + elif flags & 0x02: + fn.kwdefaults = attr + elif flags & 0x01: + fn.defaults = attr + + self.push(fn) + + def CONVERT_VALUE(self, inst: Instruction) -> None: + assert inst.arg is not None + self.push(self._convert_value(self.pop(), inst.arg)) + + def FORMAT_SIMPLE(self, inst: Instruction) -> None: + self._format_value(VariableTracker.build(self, ""), 0) + + def FORMAT_WITH_SPEC(self, inst: Instruction) -> None: + self._format_value(self.pop(), 0) + + # 3.14 opcodes + LOAD_FAST_BORROW = LOAD_FAST + NOT_TAKEN = NOP + POP_ITER = POP_TOP + + # See + # https://github.com/python/cpython/blob/805e3368d6d07e58430654d1365283924fdf4143/Python/ceval.c#L559 + # for the LOAD_SPECIAL table - make sure it matches for Python 3.14+ + _load_special_names = ( + "__enter__", + "__exit__", + "__aenter__", + "__aexit__", + ) + + def LOAD_SPECIAL(self, inst: Instruction) -> None: + assert isinstance(inst.arg, int), "expected LOAD_SPECIAL arg to be set to int" + attr = self._load_special_names[inst.arg] + if attr in ("__enter__", "__exit__"): + ctx = self.pop() + if not isinstance( + ctx, (ContextWrappingVariable, GenericContextWrappingVariable) + ): + self.unsupported_ctx_graph_break(ctx) + + # Need this redundant check for mypy + assert isinstance( + ctx, (ContextWrappingVariable, GenericContextWrappingVariable) + ) + if attr == "__enter__": + self.push(WithEnterFunctionVariable(ctx)) + self.PUSH_NULL(inst) + else: + # WithExitFunctionVariable doesn't really do anything with target for 3.11+ + self.push(WithExitFunctionVariable(ctx, None)) + self.PUSH_NULL(inst) + else: + # Implementation is similar to LOAD_METHOD for 3.13+ + self._load_attr(attr) + obj = self.pop() + self.push(obj) + self.PUSH_NULL(inst) + + def LOAD_SMALL_INT(self, inst: Instruction) -> None: + self.push(VariableTracker.build(self, inst.argval)) + + # See + # https://github.com/python/cpython/blob/7519ac294fc5c4fd7fb9cb8dc0edc960688cf887/Python/pylifecycle.c#L814 + # for the common constants - make sure it matches for Python 3.14+. + # The common constants are all attributes of `builtins`. + _common_constants = ( + "AssertionError", + "NotImplementedError", + "tuple", + "all", + "any", + ) + + def LOAD_COMMON_CONSTANT(self, inst: Instruction) -> None: + assert isinstance(inst.arg, int), ( + "expected LOAD_COMMON_CONSTANT arg to be set to int" + ) + self.push(self.load_builtin_from_argval(self._common_constants[inst.arg])) + + def is_non_empty_graph(self) -> bool: + if self.output.count_calls() > 1: + # perf optimization only + self.is_non_empty_graph = lambda: True # type: ignore[method-assign] + return True + return False + + def format_frame_summary( + self, additional_stack_frames: list[Any] | None = None + ) -> str: + if additional_stack_frames is None: + additional_stack_frames = [] + return "".join( + traceback.format_list( + [self.frame_summary()] + list(reversed(additional_stack_frames)) + ) + ) + + def frame_summary(self) -> traceback.FrameSummary: + return traceback.FrameSummary( + getattr(self.f_code, "co_filename", ""), + self.lineno, + getattr(self.f_code, "co_name", ""), + lookup_line=False, + ) + + def is_co_filename_from_nn_modules(self) -> bool: + filename = getattr(self.f_code, "co_filename", "") + nn_modules_pattern = re.compile(r".*torch/nn/modules.*") + return nn_modules_pattern.match(filename) is not None + + def store_global_weakref_by_id(self, prefix: str, value: Any) -> str: + global_name = self.output.install_global_by_id(prefix, weakref.ref(value)) + install_guard( + GlobalWeakRefSource(global_name).make_guard(GuardBuilder.WEAKREF_ALIVE) + ) + return global_name + + @property + def fake_mode(self) -> FakeTensorMode | None: + return self.output.tracing_context.fake_mode + + @contextlib.contextmanager + def strict_translation_mode( + self, check_fn: Callable[[VariableTracker], bool] + ) -> Any: + """ + Strict mode is enabled on a per-VariableTracker level depending on the return value of check_fn(node). + """ + prior = self.strict_checks_fn + self.strict_checks_fn = check_fn + try: + yield + finally: + self.strict_checks_fn = prior + + def speculate(self) -> SpeculationEntry: + assert self.instruction_pointer is not None + assert self.instruction_pointer > 0 + return self.speculation_log.next( + self.f_code.co_filename, + self.lineno, + self.instruction_pointer - 1, + self.instructions[self.instruction_pointer - 1], + ) + + def _make_frame_loc( + self, filename: str, lineno: int | None, fallback_lineno: int + ) -> tuple[str, int]: + if lineno is None or lineno < 0: + return (filename, fallback_lineno) + return (filename, lineno) + + def _get_frame_loc_chain( + self, frame_loc: tuple[str, int] + ) -> tuple[tuple[str, int], ...]: + frame_loc_chain_list: list[tuple[str, int]] = [] + + if config.nested_graph_breaks: + current_tx: InstructionTranslatorBase | None = self.parent + while current_tx is not None: + parent_frame_loc = self._make_frame_loc( + current_tx.f_code.co_filename, + current_tx.lineno, + current_tx.f_code.co_firstlineno, + ) + frame_loc_chain_list.append(parent_frame_loc) + current_tx = current_tx.parent + + frame_loc_chain_list.reverse() + frame_loc_chain_list.append(frame_loc) + return tuple(frame_loc_chain_list) + + def log_graph_break( + self, + code_options: dict[str, Any], + reason: str, + exc: Unsupported | UserError | StepUnsupported, + ) -> None: + if exc.logged: + return + + user_stack = getattr(exc, "real_stack", None) + + if user_stack is None: + user_stack = torch._guards.TracingContext.extract_stack() + + try: + if config.nested_graph_breaks and self.parent is not None: + frame_loc = self._make_frame_loc( + self.f_code.co_filename, + self.lineno, + self.f_code.co_firstlineno, + ) + else: + frame_loc = self._make_frame_loc( + user_stack[-1].filename, + user_stack[-1].lineno, + 0, + ) + except IndexError: + # first instruction + frame_loc = ( + code_options["co_filename"], + code_options["co_firstlineno"], + ) + frame_loc_chain = self._get_frame_loc_chain(frame_loc) + stack_above_dynamo_formatted = "" + if config.verbose: + stack_above_dynamo = get_stack_above_dynamo() + stack_above_dynamo_formatted = "".join( + traceback.format_list(stack_above_dynamo) + ) + else: + user_stack = get_stack_above_dynamo() + user_stack # type: ignore[assignment] + user_stack = collapse_resume_frames(user_stack) + user_stack_formatted = "".join(traceback.format_list(user_stack)) + + # Add HOP context after the first line of reason if present + if exc is not None: + reason = augment_exc_message_with_hop_name(exc, reason) + + user_stack_trace = ( + f"Graph break in user code at {frame_loc[0]}:{frame_loc[1]}\n" + f"Graph Break Reason: {reason}\n" + "\nUser code traceback:\n" + ) + + if config.verbose: + user_stack_trace += ( + f"{stack_above_dynamo_formatted}\n" + "========== most recent `torch.compile` tracing attempt started here ==========\n\n" + f"{user_stack_formatted}\n" + "NOTE: the most recent `torch.compile` tracing attempt might not be where you applied `torch.compile`! " + "This is due to how graph breaks are implemented - the optimized code object returned by Dynamo will call another " + "Dynamo-generated resume function and tracing is re-enabled by calling the resume function as a normal Python " + "function, which Dynamo intercepts as a top-level frame.\n" + ) + else: + user_stack_trace += str(user_stack_formatted) + + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "dynamo_graph_break_reason", + "encoding": "string", + }, + payload_fn=lambda: f"{user_stack_trace}\n{traceback.format_exc()}", + ) + + # torch._dynamo.explain() formats this a little nicer, and presents a slightly + # more actionable user code pointer + gb_type = exc.gb_type if isinstance(exc, Unsupported) else type(exc) + if ( + graph_break_log.isEnabledFor(logging.DEBUG) + and not explain + and graph_break_dup_warning_checker.add((gb_type, frame_loc_chain)) # type: ignore[arg-type] + ): + # This log line MUST contain the string "Graph break in user code", + # This log line is exercised from + # python test/dynamo/test_exc.py -k test_graph_break_log + if config.verbose: + user_stack_trace += ( + "\nMost recent bytecode instructions traced (max 20):\n" + ) + user_stack_trace += "\n".join(self.latest_bytecode_queue) + "\n" + + graph_break_log.debug( + user_stack_trace, + ) + else: + # This log line MUST not contain the string "Graph break in user code", + # exercised by + # python test/dynamo/test_misc.py -k test_duplicate_graph_break_log + graph_break_log.debug( + "Graph break (user stack suppressed due to duplicate graph break) in user code at %s:%s\nGraph Break Reason: %s", + frame_loc[0], + frame_loc[1], + reason, + ) + + exc.logged = True + + @staticmethod + def raise_loop_graph_break( + code: types.CodeType, exc: Unsupported | UserError + ) -> NoReturn: + unimplemented( + gb_type="graph break in loop", + context=f"frame skipped: {format_frame_info(code)}", + explanation="torch.compile detected a graph break in a for/while loop. " + "Skipping the frame and falling back to eager, as graph breaks in loops are not supported.", + hints=[*graph_break_hints.CAUSED_BY_EARLIER_GRAPH_BREAK], + from_exc=exc, + skip_frame=True, + ) + + def __init__( + self, + output: OutputGraph, + instructions: list[Instruction], + f_locals: dict[str, Any], + f_globals: dict[str, Any], + f_builtins: dict[str, Any], + code_options: dict[str, Any], + symbolic_locals: dict[str, VariableTracker], + symbolic_globals: dict[str, VariableTracker], + symbolic_torch_function_state: SymbolicTorchFunctionState, + symbolic_stream_state: SymbolicStreamState, + f_code: types.CodeType, + export: bool, + inline_depth: int, + speculation_log: SpeculationLog, + exn_vt_stack: ExceptionStack, + distributed_state: DistributedState | None, + # This determines whether to use the execution recorder. + closure: tuple[types.CellType] | None = None, + package: CompilePackage | None = None, + # Pre-computed indexof for cache reuse + indexof: dict[Instruction, int] | None = None, + ) -> None: + super().__init__() + self.speculation_log = speculation_log + self.distributed_state = distributed_state + + # Mutable state checkpointed by copy_graphstate() + self.output = output + self.symbolic_locals = symbolic_locals + self.symbolic_globals = symbolic_globals + self.symbolic_torch_function_state = symbolic_torch_function_state + self.symbolic_stream_state = symbolic_stream_state + # used to keep cell/freevars alive after pruning symbolic_locals (prune_dead_locals) + # in order to generate any nested closures + self.post_prune_cell_and_freevars = None + self.stack: list[VariableTracker] = [] + self.instruction_pointer = 0 + self.start_point = None + self.current_instruction = create_instruction("NOP") + self.current_instruction_push = True + self.block_stack = [] + # states before SETUP_WITH for checkpointing and fallback + self.active_generic_context_managers: list[GenericContextWrappingVariable] = [] + self.lineno = -1 + self.kw_names = None + self.accept_prefix_inst = True + self.prefix_insts = [] + self.exn_vt_stack = exn_vt_stack + self.latest_bytecode_queue = deque(maxlen=20) + self._comprehension_depth = 0 + self._comprehension_end_for_ips: set[int] = set() + + # Properties of the input/output code + self.instructions: list[Instruction] = instructions + self.indexof: dict[Instruction, int] = ( + indexof if indexof is not None else get_indexof(self.instructions) + ) + self.f_locals: dict[str, Any] = ( + f_locals # needed for recording accessed locals for replay + ) + self.f_globals: dict[str, Any] = f_globals + self.f_builtins: dict[str, Any] = f_builtins + self.code_options: dict[str, Any] = code_options + self.f_code: types.CodeType = f_code + self.closure = closure + + # Execution record for replaying errors + if closure is not None and config.replay_record_enabled: + self.exec_recorder = ExecutionRecorder( + code=f_code, closure=closure, code_options=code_options + ) + else: + self.exec_recorder = None + # Stack of module being parsed, current nn.module is at the end of ordered dict. + # The first field of tuple is the fully qualified name of current module + # in original hierarchy. The second field is the type of current nn.module + self.nn_module_stack: dict[str, tuple[str, type[Any]]] = {} + self.num_calls: dict[str, int] = {} + # Flag to indicate whether tracing is used for export. + self.export = export + # NOTE: one_graph is used for export/fullgraph=True to always force errors on graph breaks. + # To toggle erroring/resuming on graph breaks during fullgraph=False compile, self.error_on_graph_break + # is used instead. Every step(), its value is updated to the global tls.error_on_graph_break. + # We mirror this value since cleanup may (correctly) inadvertently change tls.error_on_graph_break. + # This assumes that we cannot both trace a change to tls.error_on_graph_break and graph break on + # the same instruction. + self.one_graph = False + self.error_on_graph_break = False + # Also do not graph break when tracing resume function prologues + self.is_tracing_resume_prologue = False + + self.current_speculation = None + + self.strict_checks_fn = None + + self.has_no_inlined_calls = True + self.parent = None + self.is_child_tracer_active = False + self.debug_locals = [] + + self.package = package + + from .resume_execution import ( + CO_ASYNC_GENERATOR, + CO_COROUTINE, + CO_GENERATOR, + CO_ITERABLE_COROUTINE, + ) + + if f_code.co_flags & ( + CO_GENERATOR | CO_COROUTINE | CO_ITERABLE_COROUTINE | CO_ASYNC_GENERATOR + ): + self.push(BuiltinVariable(None)) + + self.inline_depth = inline_depth + self.inconsistent_side_effects = False + self._constants_cache: list[ConstantVariable | SliceVariable | None] = [ + None + ] * len(f_code.co_consts) + + self.is_trace_bytecode_log_enabled: bool | None = ( + trace_bytecode_log.isEnabledFor(logging.DEBUG) + ) + self.is_trace_source_log_enabled: bool | None = trace_source_log.isEnabledFor( + logging.DEBUG + ) + linecache.lazycache(f_code.co_filename, f_globals) + + +class InstructionTranslator(InstructionTranslatorBase): + @staticmethod + def current_tx() -> InstructionTranslator: + return tls.current_tx + + @contextlib.contextmanager + def set_current_tx(self) -> Any: + prior = getattr(tls, "current_tx", None) + tls.current_tx = self + try: + yield + finally: + tls.current_tx = prior + + def __init__( + self, + instructions: list[Instruction], + f_code: types.CodeType, + f_locals: dict[str, Any], + f_globals: dict[str, Any], + f_builtins: dict[str, Any], + closure: tuple[Any, ...] | None, + torch_function_mode_stack: Any, + code_options: dict[str, Any], + compiler_fn: Any, + one_graph: bool, + export: bool, + export_constraints: Any, + frame_state: Any, + speculation_log: SpeculationLog, + exn_vt_stack: ExceptionStack, + distributed_state: DistributedState | None, + package: CompilePackage | None, + ) -> None: + _step_logger()( + logging.INFO, + f"torchdynamo start tracing {f_code.co_name} {code_options['co_filename']}:{code_options['co_firstlineno']}", + ) + super().__init__( + output=OutputGraph( + code_options, + compiler_fn, + self, + export, + export_constraints, + frame_state, + local_scope=f_locals, + global_scope=f_globals, + f_code=f_code, + torch_function_mode_stack=torch_function_mode_stack, + one_graph=one_graph, + package=package, + ), + instructions=instructions, + f_locals=f_locals, + f_globals=f_globals, + f_builtins=f_builtins, + closure=closure, + code_options=code_options, + symbolic_locals={}, # set below + # A global var is inserted only after a STORE_GLOBAL happens to it + symbolic_globals={}, + symbolic_torch_function_state=None, # type: ignore[arg-type] # set below + symbolic_stream_state=None, # type: ignore[arg-type] # set below + f_code=f_code, + export=export, + inline_depth=0, + speculation_log=speculation_log, + exn_vt_stack=exn_vt_stack, + distributed_state=distributed_state, + package=package, + ) + + self._throw_if_in_functorch() + + # as soon as we create the tracing context we should keep it active, so any calls + # into dynamo apis can rely on finding it + with tracing(self.output.tracing_context), self.set_current_tx(): + self.one_graph: bool = one_graph + self.export = export + if self.export: + assert self.one_graph, ( + "Export without one graph - something has gone wrong." + ) + + self.symbolic_locals = {} + # Populate `symbolic_locals` with non-cell variables. + cell_and_freevars: set[str] = set(self.cell_and_freevars()) + + dynamism = code_context.get_context(f_code).get("dynamism", None) + for name, value in f_locals.items(): + if name not in cell_and_freevars: + local_dynamism = None + if dynamism: + local_dynamism = frozenset(dynamism.get(name, {}).items()) + var = LazyVariableTracker.create( + value, + LocalSource( + name, + is_input=True, + dynamism=local_dynamism, + ), + ) + self.symbolic_locals[name] = var + + # Populate `symbolic_locals` with cells created by this frame, + # effectively implementing the `MAKE_CELL` instructions. + side_effects = self.output.side_effects + for name in self.cellvars(): + if name in f_locals: + # This models cells that are also function inputs. + value = f_locals[name] + # NOTE: root frame inputs that are captured by a nested + # function become special cell objects -- they exist in + # `f_locals` as contents of the cells, rather than the cells + # objects themselves. + # + # In Dynamo, we choose to represent such input cell objects + # as newly created (rather than pre-existing) cell objects, + # because + # + # 1. The reason for representing a pre-existing cell object + # is to emit guard or codegen mutations. However, local + # cells should never be used for guards. Moreover, at this + # point these input cell objects should've never been + # accessed by anyone else, since Dynamo intercepts the frame + # right after its evaluation starts, i.e., right after these + # cell objects are created. So they should have no external + # reference, meaning no mutation needs to be propagated. + # + # 2. This conveniently allows codegen to prune away + # mutations to these cells, unless they escape the frame. + contents_source = LocalSource( + name, + is_input=True, + is_derefed_cell_contents=True, + ) + contents_var: VariableTracker = LazyVariableTracker.create( + value, contents_source + ) + cell_var = side_effects.track_cell_new() + side_effects.store_cell(cell_var, contents_var) + else: + cell_var = side_effects.track_cell_new() + cell_var.local_name = name # type: ignore[attr-defined] + self.symbolic_locals[name] = cell_var + + # Populate `symbolic_locals` with cells captured by this frame, + # effectively implementing the `COPY_FREE_VARS` instruction. + assert closure is not None + for name, cell in zip(self.freevars(), closure): + cell_source = LocalCellSource(name) + contents_source = LocalSource(name, is_derefed_cell_contents=True) + try: + contents_var = LazyVariableTracker.create( + cell.cell_contents, contents_source + ) + except ValueError: + # Cell has not yet been assigned + contents_var = variables.DeletedVariable() + cell_var = side_effects.track_cell_existing( + cell_source, cell, contents_var + ) + cell_var.local_name = name # type: ignore[attr-defined] + self.symbolic_locals[name] = cell_var + + self.symbolic_torch_function_state = SymbolicTorchFunctionState( + torch_function_mode_stack + ) + + self.symbolic_stream_state = SymbolicStreamState() + + if export: + # export gets confused if we never realize unused inputs + # in export mode just eagerly realize everything + self.symbolic_locals = variables.LazyVariableTracker.realize_all( + self.symbolic_locals + ) + + def _throw_if_in_functorch(self) -> None: + # Fallback to eager in case of a graph break inside vmap + eager = torch._dynamo.lookup_backend("eager") + compiler_fn = inspect.getattr_static( + self.output.compiler_fn, "compiler_fn", self.output.compiler_fn + ) + ci = torch._C._functorch.peek_interpreter_stack() + forbidden_keys = ( + torch._C._functorch.TransformType.Vmap, + torch._C._functorch.TransformType.Grad, + torch._C._functorch.TransformType.Jvp, + ) + + if ci is not None and ci.key() in forbidden_keys and compiler_fn is not eager: + name = ci.key().name.lower() + msg = ( + "If you are reaching here, it means dynamo failed for one of the following reasons:\n" + # Calling a torch.compiled function + f"- Calling torch.func.{name}(compiled_fn) function from eager mode is not supported. " + f"Ensure that torch.func.{name} is also wrapped within a torch.compile function. " + "For more information, see PyTorch issue #128711.\n" + # if it reaches here, it means Dynamo failed to inline a functorch function + f"- torch.func.{name}(fn) requires the function to be inlined by dynamo" + ) + unimplemented( + gb_type="Unsupported functorch tracing attempt", + context="", + explanation=msg, + hints=[], + ) + + def get_example_value(self, source: Source) -> Any: + if isinstance(source, LocalSource): + return self.f_locals[source.local_name] + if isinstance(source, GlobalSource): + return self.f_globals[source.global_name] + raise KeyError + + def symbolic_locals_contain_module_class(self) -> bool: + for v in self.symbolic_locals.values(): + if isinstance(v, UserDefinedClassVariable) and issubclass( + v.as_python_constant(), torch.nn.Module + ): + return True + return False + + def replace_tos_if_return_is_generator(self) -> None: + if ( + len(self.stack) + and (tos := self.stack[-1]) + and isinstance(tos, LocalGeneratorObjectVariable) + ): + self.stack[-1] = ListIteratorVariable( + tos.force_unpack_var_sequence(self), + mutation_type=ValueMutationNew(), + ) + + def _return(self, inst: Instruction) -> None: + self.replace_tos_if_return_is_generator() + assert self.instruction_pointer is not None + assert self.start_point is not None + get_metrics_context().increment( + "ir_count", self.instruction_pointer - self.start_point + ) + + if ( + not config.allow_empty_graphs + and self.output.count_calls() == 0 + and not self.inconsistent_side_effects + and not self.symbolic_locals_contain_module_class() + and not self.export + and not self.one_graph + and not self.error_on_graph_break + and not self.is_tracing_resume_prologue + ): + raise exc.SkipFrame( + "No ops traced for the FX graph. `torch.compile` will skip the frame and fall back to eager.\n" + f"Frame info: {format_frame_info(self.f_code)}" + ) + + self.instruction_pointer = None + _step_logger()( + logging.INFO, + f"torchdynamo done tracing {self.f_code.co_name} ({inst.opname})", + ) + log.debug("return triggered compile") + all_stack_locals_metadata = self.output.compile_subgraph( + self, + reason=GraphCompileReason( + "return_value", [self.frame_summary()], graph_break=False + ), + # the value to be returned + stack_pops=1 if inst.opname == "RETURN_VALUE" else 0, + ) + # check that our stack/locals meta are correct: + # we should only be tracing 1 frame, and there should not be any NULLs on the stack + assert len(all_stack_locals_metadata) == 1 + assert not all_stack_locals_metadata[0].stack_null_idxes + self.output.add_output_instructions( + self.codegen_return_with_pops(inst, all_stack_locals_metadata[0].num_stack) + ) + raise ReturnValueOp + + def RETURN_VALUE(self, inst: Instruction) -> None: + self._return(inst) + + def RETURN_CONST(self, inst: Instruction) -> None: + self._return(inst) + + +if sys.version_info >= (3, 11): + _binary_op_lookup = [ + getattr( + InstructionTranslator, + opname[3:] if "INPLACE" in opname else f"BINARY_{opname[3:]}", + ) + for opname, _ in dis._nb_ops # type: ignore[attr-defined] + ] + + +@contextlib.contextmanager +def profile_inline_call( + output: OutputGraph, + code: types.CodeType, + get_inline_depth: Callable[[], int], +) -> Generator[None, None, None]: + """ + Context manager for profiling inline calls. + + Args: + output: The OutputGraph containing profiler_state + code: The code object being inlined (for timing metadata) + get_inline_depth: Callable that returns inline_depth (called after work completes) + + Yields: + None (profiling happens around the with block) + """ + if not config.dynamo_profiler: + yield + return + + if output.profiler_state is None: + output.profiler_state = DynamoProfilerState() + + caller_info = output.profiler_state.get_current_caller() + call_stack = output.profiler_state.get_call_stack() + + output.profiler_state.push( + code.co_name, code.co_filename, code.co_firstlineno, time.time_ns() + ) + + trace_success = False + try: + yield + trace_success = True + finally: + stack_entry = output.profiler_state.pop() + trace_end_ns = time.time_ns() + + if trace_success and stack_entry is not None: + inline_depth = get_inline_depth() + cumtime_ns = trace_end_ns - stack_entry.start_time_ns + tottime_ns = cumtime_ns - stack_entry.child_time_ns + + timing = FunctionTraceTiming( + func_name=stack_entry.func_name, + filename=stack_entry.filename, + firstlineno=stack_entry.firstlineno, + cumtime_ns=cumtime_ns, + tottime_ns=tottime_ns, + bytecode_count=len(code.co_code), + inline_depth=inline_depth, + caller_func_name=caller_info[0] if caller_info else None, + caller_filename=caller_info[1] if caller_info else None, + caller_firstlineno=caller_info[2] if caller_info else None, + is_primitive_call=stack_entry.is_primitive_call, + call_stack=call_stack, + ) + output.profiler_state.record_timing(timing) + output.profiler_state.add_child_time(cumtime_ns) + + +class InliningInstructionTranslator(InstructionTranslatorBase): + """Trace and inline a called method""" + + symbolic_result: VariableTracker | None + # pyrefly: ignore [bad-override] + parent: InstructionTranslatorBase + + @classmethod + def inline_call( + cls, + parent: Any, + func: BaseUserFunctionVariable, + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + tracer = None + with profile_inline_call( + parent.output, func.get_code(), lambda: parent.inline_depth + 1 + ): + tracer = cls.build_inline_tracer(parent, func, args, kwargs) + return tracer.inline_call_() + + @staticmethod + def check_inlineable( + func: BaseUserFunctionVariable, + ) -> trace_rules.SkipResult: + if func.has_self(): + unimplemented( + gb_type="Inline attempt with __self__", + context=str(func), + explanation="Attempted to inline a function with the `__self__` attribute. " + "Dynamo is expected to decompose method calls into function calls with a `self` argument.", + hints=[], + ) + + if isinstance(func, UserFunctionVariable) and inspect.getattr_static( + func.get_function(), "_torchdynamo_disable", False + ): + msg = inspect.getattr_static( + func.get_function(), "_torchdynamo_disable_msg", None + ) + unimplemented( + gb_type="Skip inlining `torch.compiler.disable()`d function", + context=str(func.get_function()), + explanation=f"Skip inlining function {func.get_function()} since it was wrapped " + f"with `torch.compiler.disable` (reason: {msg})", + hints=[ + "Remove the `torch.compiler.disable` call", + ], + ) + + result = trace_rules.check_verbose(func, is_inlined_call=True) + if result.skipped: + from torch._dynamo.variables.misc import produce_trampoline_autograd_apply + + # _origin marks this as coming from an internal dynamo known function that is safe to + # trace through. + if ( + hasattr(func, "fn") + and hasattr(func.fn, "_origin") + and func.fn._origin is produce_trampoline_autograd_apply + ): + # Known sound + return trace_rules.SkipResult( + False, "allowlist in dynamo known function" + ) + fn_qualname = func.fn.__qualname__ if hasattr(func, "fn") else "" + hints = [ + f"Avoid calling the function `{fn_qualname}`.", + ] + if "_dynamo" not in func.get_filename(): + hints += [ + f"Apply `@torch._dynamo.dont_skip_tracing` to the function `{fn_qualname}` " + "to force tracing into the function. " + "More graph breaks may occur as a result of attempting to trace into the function.", + "Please file an issue to PyTorch.", + ] + unimplemented( + gb_type="Attempted to inline function marked as skipped", + context=f"qualname: {fn_qualname}, name: {func.get_name()}, " + f"filename: `{func.get_filename()}`, skip reason: {result.reason}", + explanation=f"Dynamo developers have intentionally marked that the function `{fn_qualname}` " + "should not be traced.", + hints=hints, + ) + + return result + + @staticmethod + def build_inline_tracer( + parent: Any, + func: BaseUserFunctionVariable, + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> InliningInstructionTranslator: + assert isinstance( + func, + ( + UserFunctionVariable, + NestedUserFunctionVariable, + LocalGeneratorFunctionVariable, + ), + ) + code: types.CodeType = func.get_code() + result = None + tracing_ctx = parent.output.tracing_context + + # Check if we have already identified this function to be inline-able. + # The exception is dont_skip_tracing flag which affects the inline + # behavior. If the flag is True, don't rely on previous results. + if not config.dont_skip_tracing and tracing_ctx: + if previous_result := tracing_ctx.previously_inlined_functions.get( + code, None + ): + result = previous_result + + if result is None: + result = InliningInstructionTranslator.check_inlineable(func) + assert result.skipped is False + + if not config.dont_skip_tracing and tracing_ctx: + tracing_ctx.previously_inlined_functions[code] = result + + sub_locals = None + try: + sub_locals = func.bind_args(parent, args, kwargs) + except TypeError as e: + unimplemented( + gb_type="failed to bind arguments when attempting to inline", + context=f"func='{func.get_name()}' {func.get_filename()}:{func.get_code().co_firstlineno}; " + f"args = {[arg.python_type() for arg in args]}; kwargs = {kwargs}", + explanation=f"Argument mismatch when attempting to trace function {func.get_name()}.", + hints=[ + *graph_break_hints.USER_ERROR, + ], + from_exc=e, + ) + + assert sub_locals is not None + + for v in itertools.chain(sub_locals.values()): + if not isinstance(v, VariableTracker): + unimplemented( + gb_type="Encountered unconverted argument when attempting to inline", + context=f"func: {func}, arg: {v}", + explanation="An argument to an inlined function was not successfully converted to a VariableTracker.", + hints=[*graph_break_hints.DYNAMO_BUG], + ) + + if code.co_name in ("__setitem__", "__setattr__") and not ( + args and isinstance(args[0], variables.UserDefinedObjectVariable) + ): + unimplemented( + gb_type="Unsupported __setitem__/__setattr__ inline attempt", + context=f"code name: {code.co_name}, args: {args}", + explanation=f"Attempted to inline {code.co_name} where first argument (self) is not a user-defined object.", + hints=[], + ) + + suffix = "" + # TODO: mlazos, add support for enabling multiple artifact logs + # with a single alias + if torch._logging._internal.log_state.is_artifact_enabled("bytecode"): + suffix = f"\n{dis.Bytecode(code).dis()}" + if sys.version_info >= (3, 11): + cur_inst = parent.current_instruction + parent_code = parent.f_code + + def get_trace_call_log_str() -> str: + header = parent.get_line_of_code_header( + lineno=cur_inst.positions.lineno + ) + line = get_instruction_source_311(parent_code, cur_inst).rstrip() + return f"TRACE inlined call {code.co_name} from {header}\n{line}" + + trace_call_log.debug("%s", LazyString(get_trace_call_log_str)) + log.debug("INLINING %s%s, %s", code, suffix, result.reason) + + # Detect inline GraphModule calls in order to propagate node metadata, + # by checking if the first argument (self) is a variable tracking a GraphModule. + # For unrealized lazy VTs, use peek_type to skip non-Module args + # without realizing them (which would install unnecessary guards). + if args: + arg0 = args[0] + should_check = True + if isinstance(arg0, LazyVariableTracker) and not arg0.is_realized(): + if issubclass(arg0.peek_type(), torch.nn.Module): + arg0 = arg0.realize() + else: + should_check = False + + if should_check: + if isinstance(arg0, NNModuleVariable): + module = parent.output.get_submodule(arg0.module_key) + if isinstance(module, torch.fx.GraphModule): + code_context.get_context(module.forward.__code__)[ + "orig_graphmodule" + ] = weakref.ref(module) + elif isinstance(arg0, UnspecializedNNModuleVariable): + module = arg0.value + if isinstance(module, torch.fx.GraphModule): + code_context.get_context(module.forward.__code__)[ + "orig_graphmodule" + ] = weakref.ref(module) + + assert not isinstance(func, SkipFunctionVariable) + tracer: InliningInstructionTranslator + if is_generator(code): + tracer = InliningGeneratorInstructionTranslator( + parent, + code, + sub_locals, + parent.symbolic_globals, + parent.symbolic_torch_function_state, + parent.symbolic_stream_state, + func, + ) + else: + tracer = InliningInstructionTranslator( + parent, + code, + sub_locals, + parent.symbolic_globals, + parent.symbolic_torch_function_state, + parent.symbolic_stream_state, + func, + ) + return tracer + + def inline_call_(self) -> VariableTracker: + parent = self.parent + parent.has_no_inlined_calls = False + parent.is_child_tracer_active = True + code = self.f_code + + strict_ctx: Any = contextlib.nullcontext() + if parent.strict_checks_fn: + strict_ctx = self.strict_translation_mode(parent.strict_checks_fn) + + try: + with strict_ctx: + self.run() + except exc.ObservedException as e: + msg = f"Observed exception DURING INLING {code} : {e}" + log.debug(msg) + # bubble up the exception to the parent frame. + raise + except (Unsupported, UserError) as e: + # If this graph break has skip_frame set, unset it + # since it refers to the current frame and not the parent. + e.skip_frame = False + raise + except Exception: + log.debug("FAILED INLINING %s", code) + raise + finally: + # Pass inlined tx's error_on_graph_break to parent. + # Deals with the case where the parent's error_on_graph_break is True + # while the inlined tx's error_on_graph_break was set to False. + parent.error_on_graph_break = self.error_on_graph_break + parent.is_child_tracer_active = False + + if self.output.should_exit: + # graph break + return ConstantVariable.create(None) # return dummy variable + + assert self.symbolic_result is not None + + if self.f_globals is parent.f_globals: + # Merge symbolic_globals back if parent and child are in the same namespace + parent.symbolic_globals.update(self.symbolic_globals) + + parent.inconsistent_side_effects |= self.inconsistent_side_effects + + log.debug("DONE INLINING %s", code) + self.output.tracing_context.traced_code.append(code) + + if config.enable_faithful_generator_behavior or ( + isinstance(self, InliningGeneratorInstructionTranslator) + and self.is_generator_from_ctx_manager + ): + if ( + is_generator(code) + and isinstance(self, InliningGeneratorInstructionTranslator) + and self.generator_exhausted + ): + assert isinstance(self, InliningGeneratorInstructionTranslator) + # When the generator returns None, we raise StopIteration + # pyrefly: ignore [implicit-any] + args = [] + if not self.symbolic_result.is_constant_none(): + args = [self.symbolic_result] + exc.raise_observed_exception(StopIteration, self, args=args) + else: + return self.symbolic_result + else: + if is_generator(code): + assert isinstance(self, InliningGeneratorInstructionTranslator) + assert self.symbolic_result.is_constant_none() + return ListIteratorVariable( + self.generated_items, + mutation_type=ValueMutationNew(), + ) + else: + return self.symbolic_result + + def __init__( + self, + parent: InstructionTranslatorBase, + code: types.CodeType, + symbolic_locals: dict[str, VariableTracker], + symbolic_globals: dict[str, VariableTracker], + symbolic_torch_function_state: SymbolicTorchFunctionState, + symbolic_stream_state: SymbolicStreamState, + funcvar: BaseUserFunctionVariable | LocalGeneratorObjectVariable, + ) -> None: + f_globals = funcvar.get_globals() + f_builtins = f_globals["__builtins__"] + if not isinstance(f_builtins, dict): + f_builtins = f_builtins.__dict__ + + # Get the cached code data. This cache combines instructions, indexof, and + # code_options to avoid recomputing them for frequently-inlined functions. + tracing_ctx = parent.output.tracing_context + cached = tracing_ctx.inlined_code_cache.get(code) if tracing_ctx else None + + if cached is not None: + instructions = cached.instructions + indexof = cached.indexof + code_options = cached.code_options + else: + instructions = cleaned_instructions(code) + propagate_line_nums(instructions) + indexof = get_indexof(instructions) + code_options = {k: getattr(code, k) for k in get_code_keys()} + if tracing_ctx: + tracing_ctx.inlined_code_cache[code] = InlinedCodeCache( + instructions=instructions, + indexof=indexof, + code_options=code_options, + ) + + super().__init__( + output=parent.output, + f_locals={}, + f_globals=f_globals, + f_builtins=f_builtins, + symbolic_locals=symbolic_locals, + symbolic_globals=symbolic_globals, + symbolic_torch_function_state=symbolic_torch_function_state, + symbolic_stream_state=symbolic_stream_state, + instructions=instructions, + code_options=code_options, + f_code=code, + export=parent.export, + inline_depth=parent.inline_depth + 1, + speculation_log=parent.speculation_log, + exn_vt_stack=parent.exn_vt_stack, + distributed_state=parent.distributed_state, + package=parent.package, + indexof=indexof, + ) + self.funcvar = funcvar + self.parent = parent + self.num_calls = parent.num_calls + self.symbolic_result = None + self.nn_module_stack = parent.nn_module_stack.copy() + self.one_graph = parent.one_graph + + @property + def fake_mode(self) -> FakeTensorMode | None: + return self.parent.fake_mode + + def run_ctx_mgr(self) -> Any: + return TracingContext.current_frame(self.parent.frame_summary()) + + def _can_speculate_comprehension_nested(self) -> bool: + """Check if comprehension speculation is allowed in this inlined context. + + Unlike should_compile_partial_graph(), this skips the exception table entry check. + """ + if not config.nested_graph_breaks: + return False + if not self.funcvar.should_allow_nested_graph_breaks(): + return False + if not self.parent.should_compile_partial_graph(): + return False + return True + + def should_compile_partial_graph(self) -> bool: + if config.nested_graph_breaks: + if not self.funcvar.should_allow_nested_graph_breaks(): + return False + if not self.parent.should_compile_partial_graph(): + return False + return super().should_compile_partial_graph() + return False # inlining functions is all-or-nothing + + def create_call_resume_at( + self, + inst: Instruction, + all_stack_locals_metadata: list[StackLocalsMetadata], + ) -> list[Instruction]: + if config.nested_graph_breaks: + return super().create_call_resume_at(inst, all_stack_locals_metadata) + unimplemented( + gb_type="Graph break in inlined function", + context="", + explanation="Graph breaks in an inlined call are not supported.", + hints=[], + ) + + def RETURN_VALUE(self, inst: Instruction) -> None: + self.symbolic_result = self.pop() + self.instruction_pointer = None + raise ReturnValueOp + + def RETURN_CONST(self, inst: Instruction) -> None: + self.symbolic_result = self._load_const(inst) + self.instruction_pointer = None + raise ReturnValueOp + + def get_globals_source_and_value( + self, name: str + ) -> tuple[Any, VariableTracker, Source]: + # NamedTuple's `__new__` has a fake global scope that's not an actual + # module. TODO generalize the check for other non-importable cases. + # https://github.com/python/cpython/blob/8421b03b16a4852a527256cb7cdce2ab2d318548/Lib/collections/__init__.py#L441-L447 + if "__name__" in self.f_globals and not self.f_globals["__name__"].startswith( + "namedtuple_" + ): + module_name = self.f_globals["__name__"] + module_source = self.import_source(module_name) + if "torch_package" in module_name: + fglobals_value = ( + torch.package.package_importer._package_imported_modules[ + module_name + ] + ) # type: ignore[assignment] + else: + fglobals_value = _import_module(module_name) + # Dont use lazy vt because we will do a setattr afterwards + # TODO: fix InstructionTranslator -> InstructionTranslatorBase + # pyrefly: ignore[bad-argument-type] + fglobals_vt = VariableBuilder(self, module_source)(fglobals_value) + global_source = AttrSource(module_source, name) + else: + globals_name = self.output.install_global_by_id( + "___unnamed_scope", self.f_globals + ) + globals_source = GlobalSource(globals_name) + fglobals_value = self.f_globals # type: ignore[assignment] + # Dont use lazy vt because we will do a setattr afterwards + # pyrefly: ignore[bad-argument-type] + fglobals_vt = VariableBuilder(self, globals_source)(fglobals_value) + global_source = DictGetItemSource(globals_source, name) # type: ignore[assignment] + + if is_stdlib(fglobals_value): + # Users don't inplace mutate a stdlib attribute (like inspect, + # collections), skip guards that originate from the stdlib modules. + global_source = SkipGuardSource(global_source) # type: ignore[assignment] + + return fglobals_value, fglobals_vt, global_source + + def _load_global(self, inst: Instruction) -> None: + name = inst.argval + if name not in self.f_globals: + return self.load_builtin(inst) + + if self.output.global_scope is self.f_globals: + # If the global scope matches that of the root frame, use handler in + # root frame instruction translator, to enforce consistency. + super()._load_global(inst) + else: + _, fglobals_vt, global_source = self.get_globals_source_and_value(name) + if self.output.side_effects.has_pending_mutation_of_attr(fglobals_vt, name): + self.push(self.output.side_effects.load_attr(fglobals_vt, name)) + else: + value = self.f_globals[name] + self.push(VariableTracker.build(self, value, global_source)) + + def STORE_GLOBAL(self, inst: Instruction) -> None: + if self.output.global_scope is self.f_globals: + # If the global scope matches that of the root frame, use handler in + # root frame instruction translator, to enforce consistency. + super().STORE_GLOBAL(inst) + else: + value = self.pop() + if isinstance(value, RemovableHandleVariable): + unimplemented( + gb_type="Storing Tensor hook handle in globals (inline call)", + context=inst.argval, + explanation="This is not supported.", + hints=[], + ) + name = inst.argval + _fglobals_value, fglobals_vt, _ = self.get_globals_source_and_value(name) + self.output.side_effects.store_attr(fglobals_vt, name, value) + + +class InliningGeneratorInstructionTranslator(InliningInstructionTranslator): + generated_items: list[VariableTracker] + # Flag whether or not the InlineGenerator should consume the entire iterator + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.generated_items = [] + self.generator_exhausted = False + self.is_generator_from_ctx_manager = False + + def inline_call_(self) -> VariableTracker: + with profile_inline_call(self.output, self.f_code, lambda: self.inline_depth): + return super().inline_call_() + + def should_compile_partial_graph(self) -> bool: + # resuming on graph break on inlined generator not supported + return False + + def YIELD_VALUE(self, inst: Instruction) -> None: + top = self.pop() + self.generated_items.append(top) + if len(self.generated_items) > MAX_ITERATOR_LIMIT: + raise exc.InfiniteGeneratorError + self.push(ConstantVariable.create(None)) + if ( + config.enable_faithful_generator_behavior + or self.is_generator_from_ctx_manager + ): + self.symbolic_result = top + # Stop tracing + raise YieldValueOp + + def GET_YIELD_FROM_ITER(self, inst: Instruction) -> None: + tos = self.stack[-1] + if not isinstance(tos, ListIteratorVariable): + self.pop() + res = VariableTracker.build(self, iter).call_function(self, [tos], {}) # type: ignore[arg-type] + self.push(res) + + def RETURN_VALUE(self, inst: Instruction) -> None: + self.generator_exhausted = True + return super().RETURN_VALUE(inst) + + def RETURN_CONST(self, inst: Instruction) -> None: + self.generator_exhausted = True + return super().RETURN_CONST(inst) + + def YIELD_FROM(self, inst: Instruction) -> None: + assert len(self.stack) >= 2 + val = self.pop() + tos = self.stack[-1] + if not val.is_constant_none(): + # invoke send + # Unreachable code - if you hit this, you are implementing generator support and have + # lifted the `unimplemented("generator")` in frame conversion. This codepath handles + # subgenerator and lines up with this line in Python 3.10 + # https://github.com/python/cpython/blob/3.10/Python/ceval.c#L2599 + unimplemented( + gb_type="Unreachable sub-generator code", + context="", + explanation="Should only be encountered while implementing generator support.", + hints=[], + ) + + try: + val = tos.next_variable(self) + except (StopIteration, exc.ObservedUserStopIteration) as ex: + if isinstance(ex, exc.ObservedUserStopIteration): + exc.handle_observed_exception(self) + + # The iterator is exhausted. Stop the loop and return. + self.pop() + self.push(ConstantVariable.create(ex.value)) + else: + # Repeat the YIELD_FROM instruction in the next eval loop + assert ( + isinstance(self.instruction_pointer, int) + and self.instruction_pointer > 0 + ) + self.instruction_pointer -= 1 + + self.push(val) + # Add the value to yield into generated_items and replace the top of the stack with None + self.YIELD_VALUE(inst) + + def SEND(self, inst: Instruction) -> None: + assert len(self.stack) >= 2 + val = self.pop() + tos = self.stack[-1] + if isinstance(tos, (IteratorVariable, LocalGeneratorObjectVariable)) or ( + isinstance(tos, UserDefinedObjectVariable) + and isinstance(tos.value, collections.abc.Iterator) + ): + if val.is_constant_none(): + try: + val = tos.next_variable(self) # type: ignore[arg-type] + except (StopIteration, exc.ObservedUserStopIteration) as ex: + # To implement SEND, we have to look at the implementation + # when the iterator returns StopIteration. This translates to this code + # 3.11: https://github.com/python/cpython/blob/3.11/Python/ceval.c#L2613-L2619 + # 3.12: https://github.com/python/cpython/blob/3.12/Python/bytecodes.c#L863-L866 + # The implementation is different in 3.11 and 3.12. In 3.12, we rely + # on END_SEND to clean up. In 3.11, SEND does the cleanup as well. + if sys.version_info < (3, 12): + self.pop() # Python 3.12 uses new opcode END_SEND + self.push(ConstantVariable.create(ex.value)) + self.jump(inst) + else: + self.push(val) + else: + # invoke send + # Unreachable code - if you hit this, you are implementing generator support and have + # lifted the `unimplemented("generator")` in frame conversion. This codepath handles + # subgenerator and lines up with this line in Python 3.11 + # https://github.com/python/cpython/blob/3.11/Python/ceval.c#L2597 + unimplemented( + gb_type="Unreachable sub-generator code", + context="", + explanation="Should only be encountered while implementing generator support.", + hints=[], + ) + else: + unimplemented( + gb_type="SEND with bad type", + context=f"TOS type: {typestr(tos)}", + explanation=f"Attempted to SEND with unsupported type {typestr(tos)}.", + hints=[], + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/tensor_version_op.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/tensor_version_op.py new file mode 100644 index 0000000000000000000000000000000000000000..8709c5618d8594422a7793c07130c2d5b284f313 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/tensor_version_op.py @@ -0,0 +1,70 @@ +"""This module implements tensor version operations for Dynamo tracing. + +It provides primitives for handling tensor versioning during tracing, particularly in the +context of functionalization where version operations are handled eagerly on fake tensors. + +When we functionalize _tensor_version + _unsafe_set_version_counter, the ops disappear from +the traced graph. We run them eagerly on the fake tensors used for tracing, in order to get +past asserts that would fail in autograd. + +Why is this ok? +1) Versions on functional tensors do not make any sense since you cannot mutate a functional + tensor. +2) The whole point of version munging is to trick autograd into doing what we want, and after + AotAutograd there is no longer any need for these ops. + +Note this is similar to how no_grad is handled. +""" + +from contextlib import AbstractContextManager +from typing import Any + +import torch +from torch import SymInt +from torch._prims import _make_prim, RETURN_TYPE +from torch._subclasses import FakeTensorMode +from torch._subclasses.functional_tensor import FunctionalTensorMode + + +_tensor_version = _make_prim( + schema="_tensor_version(Tensor self) -> SymInt", + return_type=RETURN_TYPE.NEW, + meta=torch.ops.aten._version.default, + impl_aten=torch.ops.aten._version.default, + doc="Tracable unbacked SymInt version of torch.Tensor._version", +) + + +@_tensor_version.py_impl(FakeTensorMode) # type: ignore[misc] +def _tensor_version_fake(fake_mode: FakeTensorMode, self_tensor: Any) -> SymInt: + """ + The initial dynamo capture of _tensor_version + _unsafe_set_version_counter turns the + `._version` into an unbacked SymInt so that we don't need to specialize on the `._version` + of input tensors to the graph. + """ + assert fake_mode.shape_env is not None + return fake_mode.shape_env.create_unbacked_symint() + + +_unsafe_set_version_counter = _make_prim( + schema="_unsafe_set_version_counter(Tensor[] tensors, SymInt[] versions) -> ()", + return_type=RETURN_TYPE.NEW, + meta=lambda self, version: None, + impl_aten=torch._C._autograd._unsafe_set_version_counter, + doc="Tracable+SymInt version of torch._C._autograd._unsafe_set_version_counter", +) +torch.fx.node.has_side_effect(_unsafe_set_version_counter) + + +@_tensor_version.py_impl(FunctionalTensorMode) # type: ignore[misc] +def _tensor_version_functional(mode: FunctionalTensorMode, self: Any) -> int: + return self._version + + +@_unsafe_set_version_counter.py_impl(FunctionalTensorMode) # type: ignore[misc] +def _unsafe_set_version_counter_functional( + ctx: AbstractContextManager[Any], + tensors: tuple[torch.Tensor, ...], + versions: tuple[int, ...], +) -> None: + torch._C._autograd._unsafe_set_version_counter(tensors, versions) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/test_case.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/test_case.py new file mode 100644 index 0000000000000000000000000000000000000000..f8047cc1b043e97866e0630afad662efe4c0d685 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/test_case.py @@ -0,0 +1,235 @@ +"""Testing utilities for Dynamo, providing a specialized TestCase class and test running functionality. + +This module extends PyTorch's testing framework with Dynamo-specific testing capabilities. +It includes: +- A custom TestCase class that handles Dynamo-specific setup/teardown +- Test running utilities with dependency checking +- Automatic reset of Dynamo state between tests +- Proper handling of gradient mode state +""" + +import contextlib +import importlib +import inspect +import logging +import os +import re +import sys +import unittest +from collections.abc import Callable +from typing import Any + +import torch +import torch.testing +from torch._dynamo import polyfills +from torch._logging._internal import trace_log +from torch.testing._internal.common_utils import ( # type: ignore[attr-defined] + IS_WINDOWS, + TEST_WITH_CROSSREF, + TEST_WITH_TORCHDYNAMO, + TestCase as TorchTestCase, +) + +from . import config, reset, utils + + +log = logging.getLogger(__name__) + + +def run_tests(needs: str | tuple[str, ...] = ()) -> None: + from torch.testing._internal.common_utils import run_tests + + if TEST_WITH_TORCHDYNAMO or TEST_WITH_CROSSREF: + return # skip testing + + if ( + not torch.xpu.is_available() + and IS_WINDOWS + and os.environ.get("TORCHINDUCTOR_WINDOWS_TESTS", "0") == "0" + ): + return + + if isinstance(needs, str): + needs = (needs,) + for need in needs: + if need == "cuda": + if not torch.cuda.is_available(): + return + else: + try: + importlib.import_module(need) + except ImportError: + return + + run_tests() + + +class TestCase(TorchTestCase): + _exit_stack: contextlib.ExitStack + + @classmethod + def tearDownClass(cls) -> None: + cls._exit_stack.close() + super().tearDownClass() + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls._exit_stack = contextlib.ExitStack() # type: ignore[attr-defined] + cls._exit_stack.enter_context( # type: ignore[attr-defined] + config.patch( + raise_on_ctx_manager_usage=True, + suppress_errors=False, + log_compilation_metrics=False, + ), + ) + + def setUp(self) -> None: + self._prior_is_grad_enabled = torch.is_grad_enabled() + self._prior_nested_graph_breaks = config.nested_graph_breaks + config.nested_graph_breaks = True + super().setUp() + reset() + utils.counters.clear() + self.handler = logging.NullHandler() + trace_log.addHandler(self.handler) + + def tearDown(self) -> None: + trace_log.removeHandler(self.handler) + for k, v in utils.counters.items(): + log.debug("%s %s", k, v.most_common()) + reset() + utils.counters.clear() + torch._C._autograd._saved_tensors_hooks_enable() + super().tearDown() + if self._prior_is_grad_enabled is not torch.is_grad_enabled(): + log.warning("Running test changed grad mode") + torch.set_grad_enabled(self._prior_is_grad_enabled) + config.nested_graph_breaks = self._prior_nested_graph_breaks + + def assertEqual(self, x: Any, y: Any, *args: Any, **kwargs: Any) -> None: # type: ignore[override] + if ( + config.debug_disable_compile_counter + and isinstance(x, utils.CompileCounterInt) + or isinstance(y, utils.CompileCounterInt) + ): + return + return super().assertEqual(x, y, *args, **kwargs) + + # assertExpectedInline might also need to be disabled for wrapped nested + # graph break tests + + +class CPythonTestCase(TestCase): + """ + Test class for CPython tests located in "test/dynamo/CPython/Py_version/*". + + This class enables specific features that are disabled by default, such as + tracing through unittest methods. + """ + + _stack: contextlib.ExitStack + dynamo_strict_nopython = True + + # Restore original unittest methods to simplify tracing CPython test cases. + assertEqual = unittest.TestCase.assertEqual # type: ignore[assignment] + assertNotEqual = unittest.TestCase.assertNotEqual # type: ignore[assignment] + assertTrue = unittest.TestCase.assertTrue + assertFalse = unittest.TestCase.assertFalse + assertIs = unittest.TestCase.assertIs + assertIsNot = unittest.TestCase.assertIsNot + assertIsNone = unittest.TestCase.assertIsNone + assertIsNotNone = unittest.TestCase.assertIsNotNone + assertIn = unittest.TestCase.assertIn + assertNotIn = unittest.TestCase.assertNotIn + assertIsInstance = unittest.TestCase.assertIsInstance + assertNotIsInstance = unittest.TestCase.assertNotIsInstance + assertAlmostEqual = unittest.TestCase.assertAlmostEqual + assertNotAlmostEqual = unittest.TestCase.assertNotAlmostEqual + assertGreater = unittest.TestCase.assertGreater + assertGreaterEqual = unittest.TestCase.assertGreaterEqual + assertLess = unittest.TestCase.assertLess + assertLessEqual = unittest.TestCase.assertLessEqual + assertRegex = unittest.TestCase.assertRegex + assertNotRegex = unittest.TestCase.assertNotRegex + assertCountEqual = unittest.TestCase.assertCountEqual + assertMultiLineEqual = polyfills.assert_multi_line_equal + assertSequenceEqual = polyfills.assert_sequence_equal + assertListEqual = unittest.TestCase.assertListEqual + assertTupleEqual = unittest.TestCase.assertTupleEqual + assertSetEqual = unittest.TestCase.assertSetEqual + # pyrefly: ignore [bad-override] + assertDictEqual = polyfills.assert_dict_equal + # pyrefly: ignore [bad-override] + assertRaises = unittest.TestCase.assertRaises + # pyrefly: ignore [bad-override] + assertRaisesRegex = unittest.TestCase.assertRaisesRegex + assertWarns = unittest.TestCase.assertWarns + assertWarnsRegex = unittest.TestCase.assertWarnsRegex + assertLogs = unittest.TestCase.assertLogs + fail = unittest.TestCase.fail + failureException = unittest.TestCase.failureException + + def compile_fn( + self, + fn: Callable[..., Any], + backend: str | Callable[..., Any], + nopython: bool, + ) -> Callable[..., Any]: + # We want to compile only the test function, excluding any setup code + # from unittest + + method = getattr(self, self._testMethodName) + method = torch._dynamo.optimize(backend, error_on_graph_break=nopython)(method) + + setattr(self, self._testMethodName, method) + return fn + + def _dynamo_test_key(self) -> str: + suffix = super()._dynamo_test_key() + test_cls = self.__class__ + test_file = inspect.getfile(test_cls).split(os.sep)[-1].split(".")[0] + py_ver = re.search(r"/([\d_]+)/", inspect.getfile(test_cls)) + if py_ver: + py_ver = py_ver.group().strip(os.sep).replace("_", "") # type: ignore[assignment] + else: + return suffix + return f"CPython{py_ver}-{test_file}-{suffix}" + + @classmethod + def tearDownClass(cls) -> None: + cls._stack.close() + super().tearDownClass() + + @classmethod + def setUpClass(cls) -> None: + # Skip test if python versions doesn't match + prefix = os.path.join("dynamo", "cpython") + os.path.sep + regex = re.escape(prefix) + r"\d_\d{2}" + search_path = inspect.getfile(cls) + m = re.search(regex, search_path) + if m: + test_py_ver = tuple(map(int, m.group().removeprefix(prefix).split("_"))) + py_ver = sys.version_info[:2] + if py_ver != test_py_ver: + expected = ".".join(map(str, test_py_ver)) + got = ".".join(map(str, py_ver)) + raise unittest.SkipTest( + f"Test requires Python {expected} but got Python {got}" + ) + else: + raise unittest.SkipTest( + f"Test requires a specific Python version but not found in path {inspect.getfile(cls)}" + ) + + super().setUpClass() + cls._stack = contextlib.ExitStack() # type: ignore[attr-defined] + cls._stack.enter_context( # type: ignore[attr-defined] + config.patch( + enable_trace_unittest=True, + ), + ) + + # pyrefly: ignore [implicit-any] + def wrap_with_policy(self, method_name: str, policy: Callable) -> None: + pass diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/test_dont_skip_tracing_functions.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/test_dont_skip_tracing_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..1edce5ff857fb2c0e35f4ac5debc42291a9d073e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/test_dont_skip_tracing_functions.py @@ -0,0 +1,40 @@ +""" +Functions used to test torch._dynamo.dont_skip_tracing. +This file is located in torch/_dynamo so that it is skipped by trace rules. +There is a special rule in trace_rules that doesn't skip this file when +dont_skip_tracing is active. +""" + +import torch + + +def f1(x: torch.Tensor) -> torch.Tensor: + return x + 1 + + +def f2(x: torch.Tensor) -> torch.Tensor: + return x + 1 + + +def f3(x: torch.Tensor) -> torch.Tensor: + return f2(x) + + +def f4(x: torch.Tensor) -> torch.Tensor: + x = f5(x, 1) + x = torch._dynamo.dont_skip_tracing(f6)(x) + x = f5(x, 8) + return x + + +def f5(x: torch.Tensor, n: int) -> torch.Tensor: + if torch.compiler.is_compiling(): + return x + n + return x + + +def f6(x: torch.Tensor) -> torch.Tensor: + x = f5(x, 2) + torch._dynamo.graph_break() + x = f5(x, 4) + return x diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/test_minifier_common.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/test_minifier_common.py new file mode 100644 index 0000000000000000000000000000000000000000..40a1eaa811176b2f35d41e4d8070d2dd78de6000 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/test_minifier_common.py @@ -0,0 +1,323 @@ +"""Common utilities for testing Dynamo's minifier functionality. + +This module provides the base infrastructure for running minification tests in Dynamo. +It includes: +- MinifierTestResult: A dataclass for storing and processing minifier test results +- MinifierTestBase: A base test class with utilities for: + - Running tests in isolated environments + - Managing temporary directories and configurations + - Executing minifier launcher scripts + - Running and validating reproduction scripts + - Supporting both compile-time and runtime error testing + +The minifier helps reduce failing Dynamo compilations to minimal reproductions. +""" + +import dataclasses +import io +import logging +import os +import re +import shutil +import subprocess +import sys +import tempfile +import traceback +from collections.abc import Sequence +from typing import Any +from unittest.mock import patch + +import torch +import torch._dynamo +import torch._dynamo.test_case +from torch._dynamo.trace_rules import _as_posix_path +from torch.utils._traceback import report_compile_source_on_error + + +@dataclasses.dataclass +class MinifierTestResult: + minifier_code: str + repro_code: str + + def _get_module(self, t: str) -> str: + match = re.search(r"class Repro\(torch\.nn\.Module\):\s+([ ].*\n| *\n)+", t) + assert match is not None, "failed to find module" + r = match.group(0) + r = re.sub(r"\s+$", "\n", r, flags=re.MULTILINE) + r = re.sub(r"\n{3,}", "\n\n", r) + return r.strip() + + def get_exported_program_path(self) -> str | None: + # Extract the exported program file path from AOTI minifier's repro.py + # Regular expression pattern to match the file path + pattern = r'torch\.export\.load\(\s*["\'](.*?)["\']\s*\)' + # Search for the pattern in the text + match = re.search(pattern, self.repro_code) + # Extract and print the file path if a match is found + if match: + file_path = match.group(1) + return file_path + return None + + def minifier_module(self) -> str: + return self._get_module(self.minifier_code) + + def repro_module(self) -> str: + return self._get_module(self.repro_code) + + +class MinifierTestBase(torch._dynamo.test_case.TestCase): + DEBUG_DIR = tempfile.mkdtemp() + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + if not os.path.exists(cls.DEBUG_DIR): + cls.DEBUG_DIR = tempfile.mkdtemp() + cls._exit_stack.enter_context( # type: ignore[attr-defined] + torch._dynamo.config.patch(debug_dir_root=cls.DEBUG_DIR) + ) + # These configurations make new process startup slower. Disable them + # for the minification tests to speed them up. + cls._exit_stack.enter_context( # type: ignore[attr-defined] + torch._inductor.config.patch( + { + # https://github.com/pytorch/pytorch/issues/100376 + "pattern_matcher": False, + # multiprocess compilation takes a long time to warmup + "compile_threads": 1, + # https://github.com/pytorch/pytorch/issues/100378 + "cpp.vec_isa_ok": False, + } + ) + ) + + @classmethod + def tearDownClass(cls) -> None: + if os.getenv("PYTORCH_KEEP_TMPDIR", "0") != "1": + shutil.rmtree(cls.DEBUG_DIR) + else: + print(f"test_minifier_common tmpdir kept at: {cls.DEBUG_DIR}") + cls._exit_stack.close() # type: ignore[attr-defined] + + def _gen_codegen_fn_patch_code(self, device: str, bug_type: str) -> str: + assert bug_type in ("compile_error", "runtime_error", "accuracy") + return f"""\ +{torch._dynamo.config.codegen_config()} +{torch._inductor.config.codegen_config()} +torch._inductor.config.{"cpp" if device == "cpu" else "triton"}.inject_relu_bug_TESTING_ONLY = {bug_type!r} +""" + + def _maybe_subprocess_run( + self, args: Sequence[Any], *, isolate: bool, cwd: str | None = None + ) -> subprocess.CompletedProcess[bytes]: + from torch._inductor.cpp_builder import normalize_path_separator + + if not isolate: + assert len(args) >= 2, args + assert args[0] == "python3", args + if args[1] == "-c": + assert len(args) == 3, args + code = args[2] + args = ["-c"] + else: + assert len(args) >= 2, args + with open(args[1]) as f: + # Need normalize path of the code. + code = normalize_path_separator(f.read()) + args = args[1:] + + # WARNING: This is not a perfect simulation of running + # the program out of tree. We only interpose on things we KNOW we + # need to handle for tests. If you need more stuff, you will + # need to augment this appropriately. + + # NB: Can't use save_config because that will omit some fields, + # but we must save and reset ALL fields + dynamo_config = torch._dynamo.config.get_config_copy() + inductor_config = torch._inductor.config.get_config_copy() + try: + stderr = io.StringIO() + log_handler = logging.StreamHandler(stderr) + log = logging.getLogger("torch._dynamo") + log.addHandler(log_handler) + try: + prev_cwd = _as_posix_path(os.getcwd()) + if cwd is not None: + cwd = _as_posix_path(cwd) + os.chdir(cwd) + with patch("sys.argv", args), report_compile_source_on_error(): + exec(code, {"__name__": "__main__", "__compile_source__": code}) + rc = 0 + except Exception: + rc = 1 + traceback.print_exc(file=stderr) + finally: + log.removeHandler(log_handler) + if cwd is not None: + os.chdir(prev_cwd) # type: ignore[possibly-undefined] + # Make sure we don't leave buggy compiled frames lying + # around + torch._dynamo.reset() + finally: + torch._dynamo.config.load_config(dynamo_config) + torch._inductor.config.load_config(inductor_config) + + # TODO: return a more appropriate data structure here + return subprocess.CompletedProcess( + args, + rc, + b"", + stderr.getvalue().encode("utf-8"), + ) + else: + if cwd is not None: + cwd = _as_posix_path(cwd) + return subprocess.run(args, capture_output=True, cwd=cwd, check=False) + + # Run `code` in a separate python process. + # Returns the completed process state and the directory containing the + # minifier launcher script, if `code` outputted it. + def _run_test_code( + self, code: str, *, isolate: bool + ) -> tuple[subprocess.CompletedProcess[bytes], str | Any]: + proc = self._maybe_subprocess_run( + ["python3", "-c", code], isolate=isolate, cwd=self.DEBUG_DIR + ) + + print("test stdout:", proc.stdout.decode("utf-8")) + print("test stderr:", proc.stderr.decode("utf-8")) + repro_dir_match = re.search( + r"(\S+)minifier_launcher.py", proc.stderr.decode("utf-8") + ) + if repro_dir_match is not None: + return proc, repro_dir_match.group(1) + return proc, None + + # Runs the minifier launcher script in `repro_dir` + def _run_minifier_launcher( + self, + repro_dir: str, + isolate: bool, + *, + minifier_args: Sequence[Any] = (), + repro_after: str | None = None, + ) -> tuple[subprocess.CompletedProcess[bytes], str]: + self.assertIsNotNone(repro_dir) + launch_file = _as_posix_path(os.path.join(repro_dir, "minifier_launcher.py")) + with open(launch_file) as f: + launch_code = f.read() + + self.assertTrue(os.path.exists(launch_file)) + + args = ["python3", launch_file, "minify", *minifier_args] + if not isolate and repro_after != "aot_inductor": + # AOTI minifier doesn't have --no-isolate flag. + # Everything in AOTI minifier is in no-isolate mode. + args.append("--no-isolate") + launch_proc = self._maybe_subprocess_run(args, isolate=isolate, cwd=repro_dir) + print("minifier stdout:", launch_proc.stdout.decode("utf-8")) + stderr = launch_proc.stderr.decode("utf-8") + print("minifier stderr:", stderr) + + self.assertNotIn("Input graph did not fail the tester", stderr) + + return launch_proc, launch_code + + # Runs the repro script in `repro_dir` + def _run_repro( + self, repro_dir: str, *, isolate: bool = True + ) -> tuple[subprocess.CompletedProcess[bytes], str]: + self.assertIsNotNone(repro_dir) + repro_file = _as_posix_path(os.path.join(repro_dir, "repro.py")) + with open(repro_file) as f: + repro_code = f.read() + + self.assertTrue(os.path.exists(repro_file)) + + repro_proc = self._maybe_subprocess_run( + ["python3", repro_file], isolate=isolate, cwd=repro_dir + ) + print("repro stdout:", repro_proc.stdout.decode("utf-8")) + print("repro stderr:", repro_proc.stderr.decode("utf-8")) + return repro_proc, repro_code + + # Template for testing code. + # `run_code` is the code to run for the test case. + # `patch_code` is the code to be patched in every generated file; usually + # just use this to turn on bugs via the config + def _gen_test_code(self, run_code: str, repro_after: str, repro_level: int) -> str: + repro_after_line = "" + if repro_after == "aot_inductor": + repro_after_line = ( + "torch._inductor.config.aot_inductor.dump_aoti_minifier = True" + ) + elif repro_after: + repro_after_line = f"""\ +torch._dynamo.config.repro_after = "{repro_after}" + """ + return f"""\ +import torch +import torch._dynamo +import torch._inductor +{_as_posix_path(torch._dynamo.config.codegen_config())} +{_as_posix_path(torch._inductor.config.codegen_config())} +{repro_after_line} +torch._dynamo.config.repro_level = {repro_level} +torch._inductor.config.aot_inductor.repro_level = {repro_level} +torch._dynamo.config.debug_dir_root = "{_as_posix_path(self.DEBUG_DIR)}" +{run_code} +""" + + # Runs a full minifier test. + # Minifier tests generally consist of 3 stages: + # 1. Run the problematic code + # 2. Run the generated minifier launcher script + # 3. Run the generated repro script + # + # If possible, you should run the test with isolate=False; use + # isolate=True only if the bug you're testing would otherwise + # crash the process + def _run_full_test( + self, + run_code: str, + repro_after: str, + expected_error: str | None, + *, + isolate: bool, + minifier_args: Sequence[Any] = (), + ) -> MinifierTestResult | None: + if isolate: + repro_level = 3 + elif expected_error is None or expected_error == "AccuracyError": + repro_level = 4 + else: + repro_level = 2 + test_code = self._gen_test_code(run_code, repro_after, repro_level) + print("running test", file=sys.stderr) + test_proc, repro_dir = self._run_test_code(test_code, isolate=isolate) + if expected_error is None: + # Just check that there was no error + self.assertEqual(test_proc.returncode, 0) + + self.assertIsNone(repro_dir) + return None + # NB: Intentionally do not test return code; we only care about + # actually generating the repro, we don't have to crash + + self.assertIn(expected_error, test_proc.stderr.decode("utf-8")) + + self.assertIsNotNone(repro_dir) + print("running minifier", file=sys.stderr) + _minifier_proc, minifier_code = self._run_minifier_launcher( + repro_dir, + isolate=isolate, + minifier_args=minifier_args, + repro_after=repro_after, + ) + print("running repro", file=sys.stderr) + repro_proc, repro_code = self._run_repro(repro_dir, isolate=isolate) + + self.assertIn(expected_error, repro_proc.stderr.decode("utf-8")) + self.assertNotEqual(repro_proc.returncode, 0) + return MinifierTestResult(minifier_code=minifier_code, repro_code=repro_code) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/testing.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/testing.py new file mode 100644 index 0000000000000000000000000000000000000000..22badc5814b6e20dd7b17117726aed6c4874d061 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/testing.py @@ -0,0 +1,612 @@ +"""Testing utilities and infrastructure for Dynamo. + +This module provides a comprehensive set of testing utilities including: +- Test result collection and validation +- Graph manipulation and comparison tools +- Test case management and execution helpers +- Specialized test decorators for different Python versions and features +- RNG state management +- Compilation counting and monitoring +- Debug utilities for bytecode transformation + +The utilities in this module are used across Dynamo's test suite to ensure +consistent testing patterns and proper test isolation. +""" + +import contextlib +import dis +import functools +import logging +import os.path +import random +import re +import sys +import types +import unittest +from collections.abc import Callable, Generator, Sequence +from typing import Any, overload, TypeVar +from typing_extensions import ParamSpec +from unittest.mock import patch + +import torch +from torch import fx +from torch._dynamo.backends.debugging import aot_eager +from torch._dynamo.output_graph import OutputGraph + +from . import config, eval_frame, optimize_assert, reset +from .bytecode_transformation import ( + create_instruction, + debug_checks, + is_generator, + transform_code_object, +) +from .guards import CheckFunctionManager, CompileId, GuardedCode +from .types import ConvertFrameReturn, DynamoFrameType, wrap_guarded_code +from .utils import CompileCounterInt, same + + +np: types.ModuleType | None = None +try: + import numpy as np +except ModuleNotFoundError: + np = None + + +unsupported = eval_frame.unsupported +three = 3 + +log = logging.getLogger(__name__) + +_P = ParamSpec("_P") + + +def clone_me(x: torch.Tensor | None) -> torch.Tensor | None: + if x is None: + return None + return x.detach().clone().requires_grad_(x.requires_grad) + + +def remove_optimized_module_prefix(name: str) -> str: + return re.sub(r"^_orig_mod[.]", "", name) + + +def extract_graph_and_tracker(fn, *args, **kwargs): # type: ignore[no-untyped-def] + from torch._dynamo.symbolic_convert import InstructionTranslator + + gm = None + region_tracker = None + + def extract_graph_backend(_gm, *args, **kwargs): # type: ignore[no-untyped-def] + nonlocal gm + nonlocal region_tracker + gm = _gm + region_tracker = InstructionTranslator.current_tx().output.region_tracker + return _gm + + torch.compile(backend=extract_graph_backend, fullgraph=True)(fn)(*args, **kwargs) + return gm.graph, region_tracker # type: ignore[union-attr] + + +def extract_graph(fn, *args, **kwargs): # type: ignore[no-untyped-def] + backend = AotEagerAndRecordGraphs() + result = torch.compile(backend=backend)(fn)(*args, **kwargs) + return result, backend.graphs, backend.fw_graphs, backend.bw_graphs + + +def collect_results( + model: torch.nn.Module, prediction: Any, loss: Any, example_inputs: Any +) -> list[Any]: + results = [] + results.append(prediction) + results.append(loss) + # if isinstance(loss, torch.Tensor) and loss.item() > 1: + # log.warning( + # f"High loss value alert - {loss:.2f}. Can result in unstable gradients." + # ) + + grads = {} + params = {} + for name, param in model.named_parameters(): + if isinstance(model, eval_frame.OptimizedModule): + name = remove_optimized_module_prefix(name) + param_copy = param + grad = param.grad + # Treat None and zero grad as same + if param.grad is None: + grad = torch.zeros_like(param) + grads[name + ".grad"] = grad + params[name] = param_copy + results.append(grads) + results.append(params) + buffers = {} + for name, buffer in model.named_buffers(): + if isinstance(model, eval_frame.OptimizedModule): + name = remove_optimized_module_prefix(name) + buffers[name] = buffer + results.append(buffers) + for example in example_inputs: + if isinstance(example, (tuple, list)): + results.extend(inp.grad for inp in example if isinstance(inp, torch.Tensor)) + else: + if isinstance(example, torch.Tensor): + results.append(example.grad) + return results + + +def requires_bwd_pass(out: Any) -> bool: + if isinstance(out, torch.Tensor): + return out.requires_grad + elif isinstance(out, (list, tuple)): + return any(requires_bwd_pass(x) for x in out) + elif out is None: + return False + elif isinstance(out, int): + return False + raise NotImplementedError("Don't know how to reduce", type(out)) + + +@overload +def reduce_to_scalar_loss(out: torch.Tensor) -> torch.Tensor: ... + + +@overload +def reduce_to_scalar_loss( + out: list[Any] | tuple[Any, ...] | dict[Any, Any], +) -> float: ... + + +def reduce_to_scalar_loss(out: Any) -> torch.Tensor | float: + """Reduce the output of a model to get scalar loss""" + if isinstance(out, torch.Tensor): + # Mean does not work on integer tensors + return out.sum() / out.numel() + elif isinstance(out, (list, tuple)): + return sum(reduce_to_scalar_loss(x) for x in out) / len(out) + elif type(out).__name__ in ( + "MaskedLMOutput", + "Seq2SeqLMOutput", + "CausalLMOutputWithCrossAttentions", + ): + return reduce_to_scalar_loss(out.logits) + elif type(out).__name__ == "SquashedNormal": + return out.mean.sum() + elif isinstance(out, dict): + return sum(reduce_to_scalar_loss(value) for value in out.values()) / len( + out.keys() + ) + raise NotImplementedError("Don't know how to reduce", type(out)) + + +def debug_dir() -> str: + path = os.path.join(os.path.dirname(__file__), "../debug") + if not os.path.exists(path): + os.mkdir(path) + return path + + +def debug_dump(name: str, code: types.CodeType, extra: str = "") -> None: + with open(os.path.join(debug_dir(), name), "w") as fd: + fd.write( + f"{dis.Bytecode(code).info()}\n\n{dis.Bytecode(code).dis()}\n\n{extra}\n" + ) + + +def debug_insert_nops( + frame: DynamoFrameType, cache_size: int, hooks: Any, _: Any, *, skip: int = 0 +) -> ConvertFrameReturn: + """used to debug jump updates""" + + def insert_nops(instructions: list[Any], code_options: Any) -> None: + instructions.insert(0, create_instruction("NOP")) + instructions.insert(0, create_instruction("NOP")) + + metrics_context = torch._dynamo.utils.get_metrics_context() + with torch._dynamo.utils.dynamo_timed("debug_insert_nops"), metrics_context: + if is_generator(frame.f_code): + return ConvertFrameReturn() + + debug_checks(frame.f_code) + code, _ = transform_code_object(frame.f_code, insert_nops) + graph = OutputGraph( + code_options={}, + compiler_fn=None, + root_tx=None, # type: ignore[arg-type] + export=False, + export_constraints=[], + frame_state={"_id": 0}, + # TODO: shouldn't this be f_locals/f_globals from frame? + local_scope=locals(), + global_scope=globals(), + f_code=frame.f_code, + torch_function_mode_stack=[], + package=None, + ) + + return wrap_guarded_code( + GuardedCode( + code, + CheckFunctionManager(frame.f_code, graph).guard_manager, # type: ignore[arg-type] + CompileId(frame_id=0, frame_compile_id=0), + ) + ) + + +class CompileCounter: + def __init__(self) -> None: + self.frame_count: int | CompileCounterInt = 0 + self.clear() + + def __call__( + self, gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor] + ) -> Callable[..., Any]: + self.frame_count += 1 + for node in gm.graph.nodes: + if "call" in node.op: + self.op_count += 1 + return gm.forward + + def clear(self) -> None: + if config.debug_disable_compile_counter: + self.frame_count = CompileCounterInt(0) + else: + self.frame_count = 0 + self.op_count = 0 + + +class CompileCounterWithBackend: + def __init__(self, backend: str) -> None: + self.frame_count: int | CompileCounterInt = 0 + self.backend = backend + self.graphs: list[torch.fx.GraphModule] = [] + self.clear() + + def __call__( + self, gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor] + ) -> Callable[..., Any]: + from .backends.registry import lookup_backend + + self.frame_count += 1 + for node in gm.graph.nodes: + if "call" in node.op: + self.op_count += 1 + self.graphs.append(gm) + return lookup_backend(self.backend)(gm, example_inputs) + + def clear(self) -> None: + if config.debug_disable_compile_counter: + self.frame_count = CompileCounterInt(0) + else: + self.frame_count = 0 + self.op_count = 0 + self.graphs = [] + + +# Equivalent to backend="eager", but also records graphs that +# we can assert on +class EagerAndRecordGraphs: + def __init__(self) -> None: + self.graphs: list[torch.fx.GraphModule] = [] + + def __call__( + self, gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor] + ) -> Callable[..., Any]: + self.graphs.append(gm) + return gm.forward + + +class AotEagerAndRecordGraphs: + def __init__(self) -> None: + self.graphs: list[torch.fx.GraphModule] = [] + self.fw_graphs: list[torch.fx.GraphModule] = [] + self.bw_graphs: list[torch.fx.GraphModule] = [] + + def __call__( + self, gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor] + ) -> Callable[..., Any]: + self.graphs.append(gm) + + def fw_compiler( + gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor] + ) -> Callable[..., Any]: + self.fw_graphs.append(gm) + return gm.forward + + def bw_compiler( + gm: torch.fx.GraphModule, example_inputs: list[torch.Tensor] + ) -> Callable[..., Any]: + self.bw_graphs.append(gm) + return gm.forward + + return aot_eager( + gm, + example_inputs, + fw_compiler=fw_compiler, + bw_compiler=bw_compiler, + ) + + +class InductorAndRecordGraphs: + def __init__(self) -> None: + self.graphs: list[torch.fx.GraphModule] = [] + self.inductor_graphs: list[torch.fx.GraphModule] = [] + + def __call__(self, gm, example_inputs): # type: ignore[no-untyped-def] + import torch._inductor.compile_fx as compile_fx_mod + + self.graphs.append(gm) + + old_compile_fx_inner = compile_fx_mod._compile_fx_inner + + def patched(*args, **kwargs): # type: ignore[no-untyped-def] + self.inductor_graphs.append(args[0]) + return old_compile_fx_inner(*args, **kwargs) + + with patch.object(compile_fx_mod, "_compile_fx_inner", new=patched): + return compile_fx_mod.compile_fx(gm, example_inputs) + + +def strip_comment(code: str) -> str: + return re.sub(r"(?m)^ *#.*\n?", "", code) + + +def remove_trailing_space(code: str) -> str: + return "\n".join([line.rstrip() for line in code.split("\n")]) + + +def _squash_blank_lines(code: str) -> str: + lines = code.split("\n") + result: list[str] = [] + saw_blank = False + for line in lines: + if line.strip() == "": + if saw_blank: + continue + saw_blank = True + else: + saw_blank = False + result.append(line) + return "\n".join(result) + + +def normalize_gm(gm_str: str) -> str: + # strip comments as comments have path to files which may differ from + # system to system. + stripped = strip_comment(gm_str) + no_trailing = remove_trailing_space(stripped) + return _squash_blank_lines(no_trailing) + + +def empty_line_normalizer(code: str) -> str: + """ + Normalize code: remove empty lines. + """ + normal_code = re.sub(r"[\r\n]+", "\n", code) + return normal_code + + +def standard_test( + self: Any, + fn: Callable[..., Any], + nargs: int, + expected_ops: int | None = None, + expected_ops_dynamic: int | None = None, + expected_frame_count: int = 1, +) -> None: + if not config.assume_static_by_default and expected_ops_dynamic is not None: + expected_ops = expected_ops_dynamic + + actual = CompileCounter() + + args1 = [torch.randn(10, 10) for _ in range(nargs)] + args2 = [torch.randn(10, 10) for _ in range(nargs)] + correct1 = fn(*args1) + correct2 = fn(*args2) + reset() + opt_fn = optimize_assert(actual)(fn) + val1a = opt_fn(*args1) + val2a = opt_fn(*args2) + val1b = opt_fn(*args1) + val2b = opt_fn(*args2) + reset() + self.assertTrue(same(val1a, correct1)) + self.assertTrue(same(val1b, correct1)) + self.assertTrue(same(val2a, correct2)) + self.assertTrue(same(val2b, correct2)) + self.assertEqual(actual.frame_count, expected_frame_count) + if expected_ops is not None: + self.assertEqual(actual.op_count, expected_ops) + + +def dummy_fx_compile( + gm: fx.GraphModule, example_inputs: list[torch.Tensor] +) -> Callable[..., Any]: + return gm.forward + + +def format_speedup( + speedup: float, + pvalue: float, + is_correct: bool = True, + pvalue_threshold: float = 0.1, +) -> str: + if not is_correct: + return "ERROR" + if pvalue > pvalue_threshold: + return f"{speedup:.3f}x SAME" + return f"{speedup:.3f}x p={pvalue:.2f}" + + +def rand_strided( + size: Sequence[int], + stride: Sequence[int], + dtype: torch.dtype = torch.float32, + device: str | torch.device = "cpu", + extra_size: int = 0, +) -> torch.Tensor: + needed_size = extra_size + if all(s > 0 for s in size): + # only need to allocate if all sizes are non-zero + needed_size += ( + sum((shape - 1) * stride for shape, stride in zip(size, stride)) + 1 + ) + if dtype.is_floating_point: + if dtype == torch.float4_e2m1fn_x2: + buffer = torch.randint( + 0, 256, (needed_size,), dtype=torch.uint8, device=device + ).view(torch.float4_e2m1fn_x2) + elif dtype.itemsize == 1: + """ + normal distribution kernel is not implemented for fp8.. + Workaround that by creating a fp16 tensor and then cast. + """ + buffer = torch.randn(needed_size, dtype=torch.float16, device=device).to( + dtype=dtype + ) + else: + buffer = torch.randn(needed_size, dtype=dtype, device=device) + else: + buffer = torch.zeros(size=[needed_size], dtype=dtype, device=device) + return torch.as_strided(buffer, size, stride) + + +_T = TypeVar("_T") + + +def check_dynamic_shape_capture() -> bool: + # This also mirrors config from `test/dynamo/test_dynamic_shapes.py:make_dynamic_cls` + return not config.assume_static_by_default + + +def _make_fn_with_patches(fn: Callable[_P, _T], *patches: Any) -> Callable[_P, _T]: + @functools.wraps(fn) + def _fn(*args: _P.args, **kwargs: _P.kwargs) -> _T: + with contextlib.ExitStack() as stack: + for module, attr, val in patches: + stack.enter_context(patch.object(module, attr, val)) + + return fn(*args, **kwargs) + + return _fn + + +def make_test_cls_with_patches( + cls: type, + cls_prefix: str, + fn_suffix: str, + *patches: Any, + xfail_prop: str | None = None, + decorator: Callable[[Callable[..., Any]], Callable[..., Any]] = lambda x: x, +) -> type: + DummyTestClass = type(f"{cls_prefix}{cls.__name__}", cls.__bases__, {}) + DummyTestClass.__qualname__ = DummyTestClass.__name__ + + for name in dir(cls): + if name.startswith("test_"): + fn = getattr(cls, name) + if not callable(fn): + setattr(DummyTestClass, name, getattr(cls, name)) + continue + new_name = f"{name}{fn_suffix}" + new_fn = _make_fn_with_patches(fn, *patches) + new_fn.__name__ = new_name + if xfail_prop is not None and hasattr(fn, xfail_prop): + new_fn = unittest.expectedFailure(new_fn) + setattr(DummyTestClass, new_name, decorator(new_fn)) + # NB: Doesn't handle slots correctly, but whatever + elif not hasattr(DummyTestClass, name): + setattr(DummyTestClass, name, getattr(cls, name)) + + return DummyTestClass + + +# test Python 3.11+ specific features +def skipIfNotPy311(fn: Callable[_P, _T]) -> Callable[_P, _T]: + if sys.version_info >= (3, 11): + return fn + # pyrefly: ignore [bad-return, bad-argument-type] + return unittest.skip(fn) + + +def skipIfNotPy312(fn: Callable[_P, _T]) -> Callable[_P, _T]: + if sys.version_info >= (3, 12): + return fn + return unittest.skip("Requires Python 3.12+")(fn) + + +def skipIfOnlyNotPy312(fn: Callable[_P, _T]) -> Callable[_P, _T]: + if sys.version_info >= (3, 13) or sys.version_info < (3, 12): + return unittest.skip("Requires Python 3.12")(fn) + return fn + + +def xfailIfPy312(fn: Callable[_P, _T]) -> Callable[_P, _T]: + if sys.version_info >= (3, 12): + return unittest.expectedFailure(fn) + return fn + + +def skipIfPy312(fn: Callable[_P, _T]) -> Callable[_P, _T]: + if sys.version_info >= (3, 12): + return unittest.skip("Not supported in Python 3.12+")(fn) + return fn + + +# Controls tests generated in test/inductor/test_torchinductor_dynamic_shapes.py +# and test/dynamo/test_dynamic_shapes.py +def expectedFailureDynamic(fn: Callable[_P, _T]) -> Callable[_P, _T]: + fn._expected_failure_dynamic = True # type: ignore[attr-defined] + return fn + + +# Controls tests generated in test/inductor/test_torchinductor_codegen_dynamic_shapes.py +def expectedFailureCodegenDynamic(fn: Callable[_P, _T]) -> Callable[_P, _T]: + fn._expected_failure_codegen_dynamic = True # type: ignore[attr-defined] + return fn + + +# Controls test generated in test/inductor/test_cpp_wrapper.py +def expectedFailureDynamicWrapper(fn: Callable[_P, _T]) -> Callable[_P, _T]: + fn._expected_failure_dynamic_wrapper = True # type: ignore[attr-defined] + return fn + + +def reset_rng_state(use_xla: bool = False) -> None: + torch.manual_seed(1337) + random.seed(1337) + if np: + np.random.seed(1337) + if use_xla: + import torch_xla.core.xla_model as xm + + xm.set_rng_state(1337, str(xm.xla_device())) + + +def _skipped_function_for_test_reconstruct( + f: Callable[_P, _T], *args: _P.args, **kwargs: _P.kwargs +) -> _T: + return f(*args, **kwargs) + + +_testing_invoke_subgraph_inductor_compile_captured_gms = None + + +@contextlib.contextmanager +def _testing_capture_invoke_subgraph_inductor_compile_gms() -> Generator[ + list[torch.fx.GraphModule] +]: + """ + Context manager to capture graph modules compiled by invoke_subgraph_inductor_compile. + + Usage: + with _testing_capture_invoke_subgraph_inductor_compile_gms() as captured_gms: + # code that triggers invoke_subgraph_inductor_compile + pass + # captured_gms will contain the list of captured graph modules + """ + global _testing_invoke_subgraph_inductor_compile_captured_gms + # pyrefly: ignore [implicit-any] + _testing_invoke_subgraph_inductor_compile_captured_gms = [] + try: + yield _testing_invoke_subgraph_inductor_compile_captured_gms + finally: + _testing_invoke_subgraph_inductor_compile_captured_gms = None diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/trace_rules.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/trace_rules.py new file mode 100644 index 0000000000000000000000000000000000000000..9f757639f1b7dba10fe0b946fc94592ad9b293e7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/trace_rules.py @@ -0,0 +1,4167 @@ +""" +Tracing rules and policies for TorchDynamo compilation decisions. + +This module defines the rules that govern what code TorchDynamo should trace and compile +versus what should be executed eagerly. It contains functions and classes that determine: + +- Which modules, functions, and objects should be skipped during tracing +- Which parts of the code should cause graph breaks +- How to handle different Python libraries and third-party packages +- Rules for determining when to inline functions vs calling them eagerly + +Key components: +- Skip rules: Functions that return True if an object should be skipped during tracing +- Inlining rules: Policies for when to inline function calls during compilation +- Library-specific handling: Special cases for popular Python packages +- Performance heuristics: Rules that balance compilation overhead vs runtime benefits + +These rules are critical for TorchDynamo's ability to automatically determine +compilation boundaries and optimize PyTorch programs effectively. +""" + +import abc +import builtins +import contextlib +import copy +import dataclasses +import functools +import importlib +import inspect +import linecache +import operator +import os +import random +import re +import sys +import types +import typing +import unittest +from collections import defaultdict +from collections.abc import Callable, Iterator +from pathlib import Path +from typing import Any + +import torch +import torch._inductor.test_operators +import torch.distributed +import torch.utils._content_store +from torch._environment import is_fbcode +from torch.utils import _config_module + +from . import config +from .resume_execution import TORCH_DYNAMO_RESUME_IN_PREFIX +from .utils import ( + getfile, + hashable, + is_lru_cache_wrapped_function, + NP_SUPPORTED_MODULES, + unwrap_if_wrapper, +) +from .variables import ( + BuiltinVariable, + DictBuiltinVariable, + FunctionalCallVariable, + FunctorchHigherOrderVariable, + InspectSignatureVariable, + IterBuiltinVariable, + ListBuiltinVariable, + LocalGeneratorFunctionVariable, + LocalGeneratorObjectVariable, + NestedUserFunctionVariable, + PolyfilledFunctionVariable, + PyTreeGetNodeTypeFunctionVariable, + PyTreeTreeIsLeafFunctionVariable, + ReparametrizeModuleCallVariable, + SkipFunctionVariable, + SparseTensorCreationSkipVariable, + TorchInGraphFunctionVariable, + UserFunctionVariable, + UserMethodVariable, +) +from .variables.base import VariableTracker + + +np: types.ModuleType | None = None +try: + import numpy as np +except ModuleNotFoundError: + pass + + +""" +A note on skip/inline rules: + +Dynamo consults this file to determine whether function should be inlined or skipped. + +A skip applies at the frame boundary, meaning dynamo either triggers a graph break +at the beginning of the frame or attempts to trace/inline the whole frame. When skipping +a frame, recursively called frames are still traced by dynamo unless also skipped. + +Skipfiles (skipped at the file level instead of function level) still apply on a +frame-by-frame boundary as dynamo traces, but apply to all functions in that file. + +@skip is a helper decorator that can be applied to your function to cause it to be +included here. + +Dynamo skip/inline rules & priorities are defined as follows: +* Inline is the default behavior and will be used unless explicitly skipped. +* Dynamo has two SKIPLIST: BUILTIN_SKIPLIST and THIRDPARTY_SKIPLIST. + * BUILTIN_SKIPLIST contains builtin python modules, such as abc, collections, etc. + * THIRDPARTY_SKIPLIST contains common third party libraries, such as numpy, pandas, etc. +* Functions in these two SKIPLISTs are always skipped, except: + * They have explicitly defined rule in `manual_torch_name_rule_map`; + * The corresponding python module has been put into MOD_INLINELIST. +* PyTorch(torch) is in the BUILTIN_SKIPLIST by default, but there are many cases + where we want inline the functions under torch namespace. + We should specify inline for the functions in `manual_torch_name_rule_map` or + put the corresponding python module into MOD_INLINELIST to make dynamo inline them. +* If you call functions under skipped modules/files, Dynamo will wrap these functions + as SkipFunctionVariable. There are a few functions(e.g, collections.OrderedDict) that + we have special handling at SkipFunctionVariable.call_function. + +Overall: *_INLINELIST has precedence over *_SKIPLIST has precedence over DEFAULT (inline) + +To figure out what the behavior is, check the following list in order: +* `manual_torch_name_rule_map` (Inline if YES) +* MOD_INLINELIST (Inline if YES) +* BUILTIN_SKIPLIST & THIRDPARTY_SKIPLIST (Skip if YES) +* MOD_SKIPLIST (Skip if YES) +* Inline by default + +In general, if you want to force inline a function or module, please consider adding +the function's python module to MOD_INLINELIST first. +Use the `manual_torch_name_rule_map` only when there are other functions under the same module that +you don't want to inline them. +""" + +""" +Map of function objects to their tracing rules (Dynamo variables). +* TorchInGraphFunctionVariable: The functions should be put into the FX graph or can be constant folded. E.g., + - torch.add: should be put into the FX graph. + - torch.is_floating_point: constant folded. +* SkipFunctionVariable: The objects should be skipped from tracing. +* UserFunctionVariable: The functions should be inlined. + +For developers: If you add/remove a torch level API, it may trigger failures from +test/dynamo/test_trace_rules.py:test_torch_name_rule_map_updated. To fix the failures: +If you are adding a new torch level API or Dynamo implementation: +* Add the name with the corresponding tracing rule to this map + if you are adding a new in graph function or Dynamo implementation for an existing function. +* Remove the object name from test/dynamo/test_trace_rules.ignored_c_binding_in_graph_function_names if it's there. + +If you are removing an existing torch level API: +* Remove the entry represented the API from this map or test/dynamo/test_trace_rules.ignored_c_binding_in_graph_function_names + depends on where it is. + + +""" +manual_torch_name_rule_map: dict[ + str, + type[TorchInGraphFunctionVariable] + | type[SkipFunctionVariable] + | type[UserFunctionVariable], +] = { + "torch.onnx.is_in_onnx_export": TorchInGraphFunctionVariable, + "torch.onnx.operators.shape_as_tensor": TorchInGraphFunctionVariable, + "torch.overrides.is_tensor_like": TorchInGraphFunctionVariable, + "torch.jit.is_scripting": TorchInGraphFunctionVariable, + "torch.jit.is_tracing": TorchInGraphFunctionVariable, + "torch.jit.annotate": TorchInGraphFunctionVariable, + "torch.distributed.is_available": TorchInGraphFunctionVariable, + "torch.distributed.is_initialized": TorchInGraphFunctionVariable, + "torch.distributed.get_rank": TorchInGraphFunctionVariable, + "torch.distributed.get_world_size": TorchInGraphFunctionVariable, + "torch.distributed.tensor._api.DTensor#from_local": TorchInGraphFunctionVariable, + "torch.distributed.device_mesh.DeviceMesh#__init__": SkipFunctionVariable, + "torch.distributed.distributed_c10d._get_group_size_by_name": TorchInGraphFunctionVariable, + "torch.distributed.distributed_c10d._resolve_group_name_by_ranks_and_tag": TorchInGraphFunctionVariable, + "torch.distributed.distributed_c10d._get_group_tag": TorchInGraphFunctionVariable, + "torch.distributed.distributed_c10d.get_process_group_ranks": TorchInGraphFunctionVariable, + "torch.distributed.destroy_process_group": SkipFunctionVariable, + "torch._utils.is_compiling": TorchInGraphFunctionVariable, + "torch._utils._chunk_or_narrow_cat": UserFunctionVariable, + "torch.fx._symbolic_trace.is_fx_tracing": TorchInGraphFunctionVariable, + "torch.fx._symbolic_trace.is_fx_symbolic_tracing": TorchInGraphFunctionVariable, + "torch._dynamo.external_utils.is_compiling": TorchInGraphFunctionVariable, + "torch._dynamo.utils._disable_side_effect_safety_checks_for_current_subtracer": UserFunctionVariable, + "torch.compiler.is_compiling": TorchInGraphFunctionVariable, + "torch.compiler.is_dynamo_compiling": TorchInGraphFunctionVariable, + "torch.compiler.is_exporting": TorchInGraphFunctionVariable, + "torch._dynamo.eval_frame._is_in_optimized_module": TorchInGraphFunctionVariable, + "torch._C._to_dlpack": SkipFunctionVariable, + "torch._C._group_tensors_by_device_and_dtype": TorchInGraphFunctionVariable, + "torch.to_dlpack": SkipFunctionVariable, + "torch._check": TorchInGraphFunctionVariable, + # We graph break on RNG state setters or getters like + # `torch.get_rng_state`, `torch.set_rng_state`, and + # `torch.Generator.manual_seed`. These functions are not aten + # operations and therefore they are completely ignored by the AOT + # dispatcher. As a result, the AOT graph does not have these setter + # or getter functions, producing an incorrect graph when it comes + # to rng states. + "torch.get_rng_state": SkipFunctionVariable, + "torch.cuda.get_rng_state": SkipFunctionVariable, + "torch.set_rng_state": SkipFunctionVariable, + "torch.cuda.set_rng_state": SkipFunctionVariable, + # https://github.com/pytorch/pytorch/issues/107187 + "torch.manual_seed": SkipFunctionVariable, + # https://github.com/pytorch/pytorch/issues/93501 + "torch.nn.utils.rnn.pack_padded_sequence": SkipFunctionVariable, + "torch.nn.Parameter": TorchInGraphFunctionVariable, + "torch.nn.Buffer": TorchInGraphFunctionVariable, + "torch._nested_tensor_from_mask": SkipFunctionVariable, + "torch.nested._internal.nested_tensor.nested_from_padded": TorchInGraphFunctionVariable, + "torch.nested.nested_tensor_from_jagged": UserFunctionVariable, + "torch.nested.nested_tensor_from_padded": UserFunctionVariable, + # torch.fx map utils + "torch.fx.node.map_aggregate": UserFunctionVariable, + "torch.fx.node.map_arg": UserFunctionVariable, + "torch.fx.immutable_collections._no_mutation": UserFunctionVariable, + "torch.fx.immutable_collections._immutable_list_flatten": UserFunctionVariable, + "torch.fx.immutable_collections._immutable_list_unflatten": UserFunctionVariable, + "torch.fx.immutable_collections._immutable_dict_flatten": UserFunctionVariable, + "torch.fx.immutable_collections._immutable_dict_unflatten": UserFunctionVariable, + # symbol operators implemented in Python + "torch.sym_not": TorchInGraphFunctionVariable, + "torch.sym_float": TorchInGraphFunctionVariable, + "torch.sym_int": TorchInGraphFunctionVariable, + "torch.sym_max": TorchInGraphFunctionVariable, + "torch.sym_min": TorchInGraphFunctionVariable, + "torch.sym_sqrt": TorchInGraphFunctionVariable, + "torch.sym_ite": TorchInGraphFunctionVariable, + "torch.sym_sum": TorchInGraphFunctionVariable, + "torch.sym_fresh_size": UserFunctionVariable, + "torch.Tensor#_make_wrapper_subclass": SkipFunctionVariable, + "torch.Tensor#__init__": SkipFunctionVariable, + "torch.Tensor#split": TorchInGraphFunctionVariable, + "torch.cuda.set_device": SkipFunctionVariable, + "torch.cuda.current_device": TorchInGraphFunctionVariable, + "torch.autograd.grad": TorchInGraphFunctionVariable, + "torch.autograd.backward": SkipFunctionVariable, + "torch.distributions.constraints.is_dependent": SkipFunctionVariable, + "torch.jit.isinstance": SkipFunctionVariable, + "torch._C.set_anomaly_enabled": SkipFunctionVariable, + "torch._C.set_autocast_cpu_dtype": SkipFunctionVariable, + "torch._C.set_autocast_cpu_enabled": SkipFunctionVariable, + "torch._C.set_autocast_gpu_dtype": SkipFunctionVariable, + "torch._C.set_autocast_ipu_dtype": SkipFunctionVariable, + "torch._C.set_autocast_ipu_enabled": SkipFunctionVariable, + "torch._C.set_autocast_xla_dtype": SkipFunctionVariable, + "torch._C.set_autocast_xla_enabled": SkipFunctionVariable, + "torch.resize_as_": SkipFunctionVariable, + "torch._functorch.predispatch._add_batch_dim": TorchInGraphFunctionVariable, + "torch._functorch.predispatch._remove_batch_dim": TorchInGraphFunctionVariable, + "torch.resize_as_sparse_": SkipFunctionVariable, + "torch.get_default_device": TorchInGraphFunctionVariable, + # functorch/vmap + "torch._functorch.vmap._check_int_or_none": UserFunctionVariable, + "torch._functorch.vmap._check_out_dims_is_int_or_int_pytree": UserFunctionVariable, + "torch._functorch.vmap._check_randomness_arg": UserFunctionVariable, + "torch._functorch.vmap._chunked_vmap": UserFunctionVariable, + "torch._functorch.vmap._concat_chunked_outputs": UserFunctionVariable, + "torch._functorch.vmap._create_batched_inputs": UserFunctionVariable, + "torch._functorch.vmap._flat_vmap": UserFunctionVariable, + "torch._functorch.vmap._flatten_chunks_output": UserFunctionVariable, + "torch._functorch.vmap._get_chunked_inputs": UserFunctionVariable, + "torch._functorch.vmap._get_name": UserFunctionVariable, + "torch._functorch.vmap._maybe_remove_batch_dim": UserFunctionVariable, + "torch._functorch.vmap._num_outputs": UserFunctionVariable, + "torch._functorch.vmap._process_batched_inputs": UserFunctionVariable, + "torch._functorch.vmap._unwrap_batched": UserFunctionVariable, + "torch._functorch.vmap._validate_and_get_batch_size": UserFunctionVariable, + "torch._functorch.vmap.doesnt_support_saved_tensors_hooks": UserFunctionVariable, + "torch._functorch.vmap.get_chunk_sizes": UserFunctionVariable, + # lazy_load_decompositions uses a lock that is not supported yet in dynamo + # "torch._functorch.vmap.lazy_load_decompositions": UserFunctionVariable, + "torch._functorch.vmap.restore_vmap": UserFunctionVariable, + "torch._functorch.apis.vmap": UserFunctionVariable, + "torch._functorch.vmap.unwrap_batched": UserFunctionVariable, + "torch._functorch.vmap.vmap_impl": FunctorchHigherOrderVariable, + "torch._functorch.vmap.wrap_batched": UserFunctionVariable, + # functorch/grad + "torch._functorch.eager_transforms.grad_impl": FunctorchHigherOrderVariable, + "torch._functorch.apis.grad_and_value": UserFunctionVariable, + "torch._functorch.eager_transforms._as_tuple": UserFunctionVariable, + "torch._functorch.eager_transforms._check_unique_non_empty": UserFunctionVariable, + "torch._functorch.eager_transforms._create_differentiable": UserFunctionVariable, + "torch._functorch.eager_transforms._slice_argnums": UserFunctionVariable, + "torch._functorch.eager_transforms._undo_create_differentiable": UserFunctionVariable, + "torch._functorch.eager_transforms._validate_and_wrap_argnum": UserFunctionVariable, + "torch._functorch.eager_transforms._validate_and_wrap_argnums": UserFunctionVariable, + "torch._functorch.eager_transforms._wrap_all_tensors": UserFunctionVariable, + "torch._functorch.eager_transforms._wrap_tensor_for_grad": UserFunctionVariable, + # functorch/jacrev + "torch._functorch.eager_transforms.jacrev": FunctorchHigherOrderVariable, + "torch._functorch.eager_transforms.error_if_complex": UserFunctionVariable, + "torch._functorch.eager_transforms._chunked_standard_basis_for_": UserFunctionVariable, + "torch._functorch.eager_transforms._safe_zero_index": UserFunctionVariable, + # functorch/vjp + "torch._functorch.eager_transforms.vjp": FunctorchHigherOrderVariable, + "torch._functorch.eager_transforms._vjp_with_argnums": UserFunctionVariable, + "torch._functorch.eager_transforms.assert_non_empty_tensor_output": UserFunctionVariable, + # functorch/jvp + "torch._functorch.eager_transforms._jvp_with_argnums": UserFunctionVariable, + "torch._functorch.eager_transforms.jvp": FunctorchHigherOrderVariable, + "torch._functorch.eager_transforms._replace_args": UserFunctionVariable, + "torch._functorch.eager_transforms.safe_unpack_dual": UserFunctionVariable, + "torch._functorch.eager_transforms.assert_non_empty_list_of_tensors": UserFunctionVariable, + "torch._functorch.eager_transforms.assert_output_is_tensor_or_tensors": UserFunctionVariable, + "torch.autograd.forward_ad.enter_dual_level": UserFunctionVariable, + "torch.autograd.forward_ad.exit_dual_level": UserFunctionVariable, + "torch.autograd.forward_ad.make_dual": UserFunctionVariable, + "torch.autograd.forward_ad.unpack_dual": UserFunctionVariable, + # functorch/linearize + "torch._functorch.eager_transforms.linearize": FunctorchHigherOrderVariable, + # functorch/jacfwd + "torch._functorch.eager_transforms.jacfwd": FunctorchHigherOrderVariable, + "torch._functorch.eager_transforms._construct_standard_basis_for": UserFunctionVariable, + "torch._functorch.eager_transforms.safe_unflatten": UserFunctionVariable, + # functorch/hessian + "torch._functorch.eager_transforms.hessian": FunctorchHigherOrderVariable, + # functional_call + "torch._functorch.functional_call.functional_call": FunctionalCallVariable, + "torch.nn.utils.stateless._groupby_tensor": TorchInGraphFunctionVariable, + "torch.nn.utils.stateless._reparametrize_module": ReparametrizeModuleCallVariable, + # functorch/deprecated + "torch._functorch.deprecated.jvp": UserFunctionVariable, + "torch._functorch.deprecated.hessian": UserFunctionVariable, + "torch._functorch.deprecated.jacfwd": UserFunctionVariable, + "torch._functorch.deprecated.jacrev": UserFunctionVariable, + "torch._functorch.deprecated.grad": UserFunctionVariable, + "torch._functorch.deprecated.grad_and_value": UserFunctionVariable, + "torch._functorch.deprecated.vjp": UserFunctionVariable, + # functorch/C++ bindings + "torch._C._functorch._wrap_for_grad": TorchInGraphFunctionVariable, + "torch._C._functorch._unwrap_for_grad": TorchInGraphFunctionVariable, + "torch._C._functorch._unwrap_batched": TorchInGraphFunctionVariable, + "torch._C._functorch.current_level": TorchInGraphFunctionVariable, + "torch._C._functorch.maybe_current_level": TorchInGraphFunctionVariable, + "torch._C._functorch.is_batchedtensor": TorchInGraphFunctionVariable, + "torch._C._functorch.peek_interpreter_stack": TorchInGraphFunctionVariable, + "torch._C._functorch.unwrap_if_dead": TorchInGraphFunctionVariable, + "torch._functorch.predispatch._vmap_increment_nesting": TorchInGraphFunctionVariable, + "torch._functorch.predispatch._vmap_decrement_nesting": TorchInGraphFunctionVariable, + # everything else + "torch._functorch.pyfunctorch.coerce_cinterpreter": TorchInGraphFunctionVariable, + "torch._higher_order_ops.triton_kernel_wrap.do_prune_configs": UserFunctionVariable, + "torch._higher_order_ops.foreach_map.foreach_map": UserFunctionVariable, + "torch._constrain_as_size": UserFunctionVariable, + "torch._tensor._convert": UserFunctionVariable, + "torch.jit._unwrap_optional": UserFunctionVariable, + "torch.backends.mha.get_fastpath_enabled": UserFunctionVariable, + "torch._dynamo.dont_skip_tracing": UserFunctionVariable, + "torch._dynamo.mark_static": UserFunctionVariable, + "torch._dynamo.nonstrict_trace": UserFunctionVariable, + "torch._dynamo.bytecode_debugger.breakpoint": UserFunctionVariable, + "torch._dynamo.patch_dynamo_config": UserFunctionVariable, + "torch._dynamo.error_on_graph_break": UserFunctionVariable, + "torch._dynamo.override_cudagraphs": UserFunctionVariable, + "torch.fx.experimental.symbolic_shapes.guard_size_oblivious": TorchInGraphFunctionVariable, + "torch.fx.experimental.symbolic_shapes.is_nested_int": UserFunctionVariable, + "torch.fx.experimental.symbolic_shapes.guarding_hint_or_throw": TorchInGraphFunctionVariable, + "torch.fx.experimental.symbolic_shapes.optimization_hint": TorchInGraphFunctionVariable, + "torch.fx.experimental.symbolic_shapes.guard_or_true": TorchInGraphFunctionVariable, + "torch.fx.experimental.symbolic_shapes.guard_or_false": TorchInGraphFunctionVariable, + "torch.fx.experimental.symbolic_shapes.statically_known_true": TorchInGraphFunctionVariable, + "torch.fx.experimental.symbolic_shapes.statically_known_false": TorchInGraphFunctionVariable, + "torch.fx.experimental.symbolic_shapes.sym_and": TorchInGraphFunctionVariable, + "torch.fx.experimental.symbolic_shapes.sym_or": TorchInGraphFunctionVariable, + "torch.fx.experimental.symbolic_shapes.guard_scalar": TorchInGraphFunctionVariable, + "torch.fx.experimental.symbolic_shapes.has_static_value": TorchInGraphFunctionVariable, + "torch.fx.experimental.symbolic_shapes.has_free_unbacked_symbols": TorchInGraphFunctionVariable, + "torch.cuda._get_device_properties": TorchInGraphFunctionVariable, + "torch.utils.hooks.BackwardHook": TorchInGraphFunctionVariable, + "torch.set_default_device": UserFunctionVariable, + "torch.sparse_bsc_tensor": SparseTensorCreationSkipVariable, + "torch.sparse_bsr_tensor": SparseTensorCreationSkipVariable, + "torch.sparse_csc_tensor": SparseTensorCreationSkipVariable, + "torch.sparse_csr_tensor": SparseTensorCreationSkipVariable, + "torch.sparse_compressed_tensor": SparseTensorCreationSkipVariable, + "torch._C._autograd._unsafe_set_version_counter": TorchInGraphFunctionVariable, + "torch.xpu.get_rng_state": SkipFunctionVariable, + "torch.xpu.set_rng_state": SkipFunctionVariable, + "torch.library.wrap_triton": TorchInGraphFunctionVariable, + # avoid skipping user defined modules in distributed unit tests + "torch/testing/_internal/common_fsdp.py#forward": UserFunctionVariable, + f"torch/testing/_internal/common_fsdp.py#{TORCH_DYNAMO_RESUME_IN_PREFIX}": UserFunctionVariable, + "torch/testing/_internal/distributed/_tensor/common_dtensor.py#forward": UserFunctionVariable, + f"torch/testing/_internal/distributed/_tensor/common_dtensor.py#{TORCH_DYNAMO_RESUME_IN_PREFIX}": UserFunctionVariable, + "torch/testing/_internal/common_distributed.py#forward": UserFunctionVariable, + f"torch/testing/_internal/common_distributed.py#{TORCH_DYNAMO_RESUME_IN_PREFIX}": UserFunctionVariable, + "torch.utils._pytree._get_node_type": PyTreeGetNodeTypeFunctionVariable, + "torch.utils._pytree.tree_is_leaf": PyTreeTreeIsLeafFunctionVariable, + "torch._utils_internal.justknobs_check": UserFunctionVariable, + "inspect.signature": InspectSignatureVariable, +} + +# Keep this in sync with the stateful generator methods exposed from +# torch/csrc/Generator.cpp. +_GENERATOR_METHODS_THAT_GRAPH_BREAK = ( + "clone_state", + "get_offset", + "get_state", + "graphsafe_get_state", + "graphsafe_set_state", + "initial_seed", + "manual_seed", + "seed", + "set_offset", + "set_state", +) + +for generator_prefix in ("torch.default_generator", "torch._C.Generator"): + for method_name in _GENERATOR_METHODS_THAT_GRAPH_BREAK: + manual_torch_name_rule_map[f"{generator_prefix}#{method_name}"] = ( + SkipFunctionVariable + ) + + +# In graph functions (including constant folding) that are C bindings +torch_c_binding_in_graph_functions = dict.fromkeys( + [ + "math.acos", + "math.acosh", + "math.asin", + "math.asinh", + "math.atan", + "math.atan2", + "math.atanh", + "math.ceil", + "math.comb", + "math.copysign", + "math.cos", + "math.cosh", + "math.degrees", + "math.dist", + "math.erf", + "math.erfc", + "math.exp", + "math.expm1", + "math.fabs", + "math.factorial", + "math.floor", + "math.fmod", + "math.frexp", + "math.fsum", + "math.gamma", + "math.gcd", + "math.hypot", + "math.isclose", + "math.isfinite", + "math.isinf", + "math.isnan", + "math.isqrt", + "math.lcm", + "math.ldexp", + "math.lgamma", + "math.log", + "math.log10", + "math.log1p", + "math.log2", + "math.modf", + "math.nextafter", + "math.perm", + "math.pow", + "math.prod", + "math.radians", + "math.remainder", + "math.sin", + "math.sinh", + "math.tan", + "math.tanh", + "math.trunc", + "math.ulp", + "torch._adaptive_avg_pool2d", + "torch._adaptive_avg_pool3d", + "torch._add_batch_dim", + "torch._add_relu_", + "torch._add_relu", + "torch._addmm_activation", + "torch._aminmax", + "torch._amp_foreach_non_finite_check_and_unscale_", + "torch._amp_update_scale_", + "torch._assert_async", + "torch._assert_tensor_metadata", + "torch._batch_norm_impl_index", + "torch._C._accelerator_getAccelerator", + "torch._C._accelerator_getDeviceIndex", + "torch._C._accelerator_getStream", + "torch._C._accelerator_getAllocatorSettings", + "torch._C._accelerator_setAllocatorSettings", + "torch._C._accelerator_setStream", + "torch._C._accelerator_synchronizeDevice", + "torch._C._activate_gpu_trace", + "torch._C._add_cached_tensor", + "torch._C._add_docstr", + "torch._C._are_functorch_transforms_active", + "torch._C._autograd_init", + "torch._C._autograd._saved_tensors_hooks_disable", + "torch._C._autograd._saved_tensors_hooks_enable", + "torch._C._awaitable_nowait", + "torch._C._awaitable_wait", + "torch._C._awaitable", + "torch._C._backport_for_mobile_from_buffer_to_buffer", + "torch._C._backport_for_mobile_from_buffer", + "torch._C._backport_for_mobile_to_buffer", + "torch._C._backport_for_mobile", + "torch._C._broadcast_coalesced", + "torch._C._broadcast_out", + "torch._C._broadcast", + "torch._C._c10d_init", + "torch._C._calculate_package_version_based_on_upgraders", + "torch._C._can_use_flash_attention", + "torch._C._can_use_mem_efficient_attention", + "torch._C._can_use_cudnn_attention", + "torch._C._check_onnx_proto", + "torch._C._check_sparse_tensor_invariants", + "torch._C._collect_all", + "torch._C._commit_update", + "torch._C._compile_graph_to_code_table", + "torch._C._construct_CUDA_Tensor_From_Storage_And_Metadata", + "torch._C._construct_storage_from_data_pointer", + "torch._C._conv_determine_backend_memory_format", + "torch._C._cpu._init_amx", + "torch._C._cpu._get_cpu_capability", + "torch._C._crash_if_aten_asan", + "torch._C._crash_if_csrc_asan", + "torch._C._crash_if_csrc_ubsan", + "torch._C._crash_if_debug_asserts_fail", + "torch._C._crash_if_vptr_ubsan", + "torch._C._create_function_from_graph", + "torch._C._create_function_from_trace_with_dict", + "torch._C._create_function_from_trace", + "torch._C._create_graph_by_tracing", + "torch._C._create_module_with_type", + "torch._C._create_object_with_type", + "torch._C._cuda_attach_out_of_memory_observer", + "torch._C._cuda_beginAllocateCurrentStreamToPool", + "torch._C._cuda_canDeviceAccessPeer", + "torch._C._cuda_changeCurrentAllocator", + "torch._C._cuda_checkPoolLiveAllocations", + "torch._C._cuda_clearCublasWorkspaces", + "torch._C._cuda_cudaCachingAllocator_raw_alloc", + "torch._C._cuda_cudaCachingAllocator_raw_delete", + "torch._C._cuda_cudaHostAllocator", + "torch._C._cuda_customAllocator", + "torch._C._cuda_emptyCache", + "torch._C._cuda_endAllocateToPool", + "torch._C._cuda_exchangeDevice", + "torch._C._cuda_get_conv_benchmark_empty_cache", + "torch._C._cuda_get_cudnn_benchmark_limit", + "torch._C._cuda_get_sync_debug_mode", + "torch._C._cuda_getAllocator", + "torch._C._cuda_getAllocatorBackend", + "torch._C._cuda_getArchFlags", + "torch._C._cuda_getCheckpointState", + "torch._C._cuda_getCompiledVersion", + "torch._C._cuda_getCurrentBlasHandle", + "torch._C._cuda_getCurrentRawStream", + "torch._C._cuda_getCurrentStream", + "torch._C._cuda_getDefaultStream", + "torch._C._cuda_getDevice", + "torch._C._cuda_getDeviceCount", + "torch._C._cuda_hasPrimaryContext", + "torch._C._cuda_hostMemoryStats", + "torch._C._cuda_init", + "torch._C._cuda_ipc_collect", + "torch._C._cuda_isCurrentStreamCapturing", + "torch._C._cuda_isHistoryEnabled", + "torch._C._cuda_isInBadFork", + "torch._C._cuda_jiterator_compile_and_launch_kernel", + "torch._C._cuda_maybeExchangeDevice", + "torch._C._cuda_memorySnapshot", + "torch._C._cuda_memoryStats", + "torch._C._cuda_record_memory_history_legacy", + "torch._C._cuda_record_memory_history", + "torch._C._cuda_releasePool", + "torch._C._cuda_resetAccumulatedHostMemoryStats", + "torch._C._cuda_resetAccumulatedMemoryStats", + "torch._C._cuda_resetPeakHostMemoryStats", + "torch._C._cuda_resetPeakMemoryStats", + "torch._C._cuda_set_cudnn_benchmark_limit", + "torch._C._cuda_set_sync_debug_mode", + "torch._C._cuda_setCheckpointPoolState", + "torch._C._cuda_setDevice", + "torch._C._cuda_setMemoryFraction", + "torch._C._cuda_setStream", + "torch._C._cuda_sleep", + "torch._C._cuda_synchronize", + "torch._C._cudnn_set_conv_benchmark_empty_cache", + "torch._C._cudnn.getCompileVersion", + "torch._C._cudnn.getRuntimeVersion", + "torch._C._cudnn.getVersionInt", + "torch._C._current_autograd_node", + "torch._C._current_graph_task_execution_order", + "torch._C._current_graph_task_id", + "torch._C._cxx_flags", + "torch._C._debug_get_fusion_group_inlining", + "torch._C._debug_only_are_vmap_fallback_warnings_enabled", + "torch._C._debug_only_display_vmap_fallback_warnings", + "torch._C._debug_set_autodiff_subgraph_inlining", + "torch._C._debug_set_fusion_group_inlining", + "torch._C._demangle", + "torch._C._disabled_torch_dispatch_impl", + "torch._C._dispatch_call_boxed", + "torch._C._dispatch_check_all_invariants", + "torch._C._dispatch_check_invariants", + "torch._C._dispatch_dump_table", + "torch._C._dispatch_dump", + "torch._C._dispatch_find_dangling_impls", + "torch._C._dispatch_find_schema_or_throw", + "torch._C._dispatch_get_all_op_names", + "torch._C._dispatch_get_backend_keyset_from_autograd", + "torch._C._dispatch_get_registrations_for_dispatch_key", + "torch._C._dispatch_has_backend_fallback", + "torch._C._dispatch_has_computed_kernel_for_dispatch_key", + "torch._C._dispatch_has_kernel_for_any_dispatch_key", + "torch._C._dispatch_has_kernel_for_dispatch_key", + "torch._C._dispatch_has_kernel", + "torch._C._dispatch_is_alias_key", + "torch._C._dispatch_is_included_in_alias", + "torch._C._dispatch_isTensorSubclassLike", + "torch._C._dispatch_key_for_device", + "torch._C._dispatch_key_name", + "torch._C._dispatch_key_parse", + "torch._C._dispatch_key_set", + "torch._C._dispatch_keys", + "torch._C._dispatch_keyset_full_after", + "torch._C._dispatch_keyset_full", + "torch._C._dispatch_keyset_to_string", + "torch._C._dispatch_library", + "torch._C._dispatch_num_backends", + "torch._C._dispatch_print_registrations_for_dispatch_key", + "torch._C._dispatch_pystub", + "torch._C._dispatch_set_report_error_callback", + "torch._C._dispatch_tls_is_dispatch_key_excluded", + "torch._C._dispatch_tls_is_dispatch_key_included", + "torch._C._dispatch_tls_local_exclude_set", + "torch._C._dispatch_tls_local_include_set", + "torch._C._dispatch_tls_set_dispatch_key_excluded", + "torch._C._dispatch_tls_set_dispatch_key_included", + "torch._C._dist_autograd_init", + "torch._C._dump_local_tls_set", + "torch._C._dump_upgraders_map", + "torch._C._enable_mobile_interface_call_export", + "torch._C._enter_dual_level", + "torch._C._error_if_any_worker_fails", + "torch._C._exit_dual_level", + "torch._C._export_operator_list", + "torch._C._export_opnames", + "torch._C._faulty_agent_init", + "torch._C._fft.fft_fft", + "torch._C._fft.fft_fft2", + "torch._C._fft.fft_fftfreq", + "torch._C._fft.fft_fftn", + "torch._C._fft.fft_fftshift", + "torch._C._fft.fft_hfft", + "torch._C._fft.fft_hfft2", + "torch._C._fft.fft_hfftn", + "torch._C._fft.fft_ifft", + "torch._C._fft.fft_ifft2", + "torch._C._fft.fft_ifftn", + "torch._C._fft.fft_ifftshift", + "torch._C._fft.fft_ihfft", + "torch._C._fft.fft_ihfft2", + "torch._C._fft.fft_ihfftn", + "torch._C._fft.fft_irfft", + "torch._C._fft.fft_irfft2", + "torch._C._fft.fft_irfftn", + "torch._C._fft.fft_rfft", + "torch._C._fft.fft_rfft2", + "torch._C._fft.fft_rfftfreq", + "torch._C._fft.fft_rfftn", + "torch._C._free_And_Remove_DeleterFn", + "torch._C._freeze_module", + "torch._C._from_dlpack", + "torch._C._functionality_to_backend_keys", + "torch._C._functionalization_reapply_views_tls", + "torch._C._functorch._grad_decrement_nesting", + "torch._C._functorch._grad_increment_nesting", + "torch._C._functorch.set_inplace_requires_grad_allowed", + "torch._C._fuse_to_static_module", + "torch._C._gather_out", + "torch._C._gather", + "torch._C._generate_upgraders_graph", + "torch._C._get_autograd_fallback_mode", + "torch._C._get_backcompat_broadcast_warn", + "torch._C._get_backcompat_keepdim_warn", + "torch._C._get_blas_preferred_backend", + "torch._C._get_caught_jit_exception_class_name", + "torch._C._get_caught_jit_exception_original_msg", + "torch._C._get_constant_bool_symnode", + "torch._C._get_cpp_backtrace", + "torch._C._get_cpu_capability", + "torch._C._get_cublas_allow_bf16_reduced_precision_reduction", + "torch._C._get_cublas_allow_fp16_reduced_precision_reduction", + "torch._C._get_cublas_allow_tf32", + "torch._C._get_cudnn_allow_tf32", + "torch._C._get_cudnn_benchmark", + "torch._C._get_miopen_immediate", + "torch._C._get_cudnn_deterministic", + "torch._C._get_cudnn_enabled", + "torch._C._get_custom_class_python_wrapper", + "torch._C._get_default_device", + "torch._C._get_deterministic_algorithms_warn_only", + "torch._C._get_deterministic_algorithms", + "torch._C._get_deterministic_fill_uninitialized_memory", + "torch._C._get_dispatch_mode", + "torch._C._get_dispatch_stack_at", + "torch._C._get_file_format", + "torch._C._get_flash_sdp_enabled", + "torch._C._get_float32_matmul_precision", + "torch._C._get_function_stack_at", + "torch._C._get_graph_executor_optimize", + "torch._C._get_linalg_preferred_backend", + "torch._C._get_rocm_fa_preferred_backend", + "torch._C._get_math_sdp_enabled", + "torch._C._get_math_sdp_allow_fp16_bf16_reduction", + "torch._C._get_max_operator_version", + "torch._C._get_mem_efficient_sdp_enabled", + "torch._C._get_mkldnn_enabled", + "torch._C._get_cudnn_sdp_enabled", + "torch._C._get_overrideable_sdp_enabled", + "torch._C._set_sdp_use_cudnn", + "torch._C._get_mobile_model_contained_types_from_buffer", + "torch._C._get_mobile_model_contained_types", + "torch._C._get_model_bytecode_version_from_buffer", + "torch._C._get_model_bytecode_version", + "torch._C._get_model_extra_files_from_buffer", + "torch._C._get_model_extra_files", + "torch._C._get_model_ops_and_info_from_buffer", + "torch._C._get_model_ops_and_info", + "torch._C._get_module_info_from_flatbuffer", + "torch._C._get_nnpack_enabled", + "torch._C._get_obj_in_tls", + "torch._C._get_operation_overload", + "torch._C._get_operator_version_map", + "torch._C._get_privateuse1_backend_name", + "torch._C._get_qengine", + "torch._C._get_schema", + "torch._C._get_sm_carveout_experimental", + "torch._C._get_nested_int", + "torch._C._get_tensor_metadata", + "torch._C._get_tracing_state", + "torch._C._get_upgrader_ranges", + "torch._C._get_upgraders_entry_map", + "torch._C._get_upgraders_map_size", + "torch._C._get_value_trace", + "torch._C._get_version_calculator_flag", + "torch._C._get_warnAlways", + "torch._C._graph_pool_handle", + "torch._C._hack_do_not_use_clone_module_with_class", + "torch._C._has_distributed", + "torch._C._has_Standard_Deleter", + "torch._C._has_storage", + "torch._C._has_tensorexpr_cpp_tests", + "torch._C._run_tensorexpr_cpp_tests", + "torch._C._has_torch_function_unary", + "torch._C._has_torch_function_variadic", + "torch._C._has_torch_function", + "torch._C._import_ir_module_from_package", + "torch._C._increment_version", + "torch._C._infer_size", + "torch._C._init_names", + "torch._C._initExtension", + "torch._C._is_alias_of", + "torch._C._is_any_autocast_enabled", + "torch._C._is_autocast_available", + "torch._C._is_cached_tensor", + "torch._C._is_flash_attention_available", + "torch._C._is_fwd_grad_enabled", + "torch._C._is_key_in_tls", + "torch._C._is_multithreading_enabled", + "torch._C._is_torch_function_enabled", + "torch._C._is_torch_function_mode_enabled", + "torch._C._is_torch_function_all_disabled", + "torch._C._is_tracing", + "torch._C._is_view_replay_enabled", + "torch._C._is_xnnpack_enabled", + "torch._C._itt.is_available", + "torch._C._itt.mark", + "torch._C._itt.rangePop", + "torch._C._itt.rangePush", + "torch._C._ivalue_debug_python_object", + "torch._C._ivalue_tags_match", + "torch._C._jit_assert_is_instance", + "torch._C._jit_can_fuse_on_cpu_legacy", + "torch._C._jit_can_fuse_on_cpu", + "torch._C._jit_can_fuse_on_gpu", + "torch._C._jit_cat_wo_conditionals", + "torch._C._jit_check_alias_annotation", + "torch._C._jit_clear_class_registry", + "torch._C._jit_debug_fuser_num_cached_kernel_specs", + "torch._C._jit_debug_module_iterators", + "torch._C._jit_decay_packed_param_input_types", + "torch._C._jit_decomposition_graph_for_node", + "torch._C._jit_differentiate", + "torch._C._jit_erase_non_input_shape_information", + "torch._C._jit_flatten", + "torch._C._jit_fuser_get_fused_kernel_code", + "torch._C._jit_get_all_schemas", + "torch._C._jit_get_custom_class_schemas", + "torch._C._jit_get_emit_hooks", + "torch._C._jit_get_inline_everything_mode", + "torch._C._jit_get_logging_option", + "torch._C._jit_get_num_profiled_runs", + "torch._C._jit_get_operation", + "torch._C._jit_get_schemas_for_operator", + "torch._C._jit_get_te_cuda_pointwise_block_count", + "torch._C._jit_get_te_cuda_pointwise_block_size", + "torch._C._jit_get_te_cuda_pointwise_loop_levels", + "torch._C._jit_get_te_generate_block_code", + "torch._C._jit_get_te_must_use_llvm_cpu", + "torch._C._jit_get_tracer_state_warn", + "torch._C._jit_has_cpp_tests", + "torch._C._jit_init", + "torch._C._jit_interpret_graph", + "torch._C._jit_is_onnx_log_enabled", + "torch._C._jit_is_script_object", + "torch._C._jit_llga_enabled", + "torch._C._jit_nvfuser_can_be_enabled", + "torch._C._jit_nvfuser_clear_comparison_callback", + "torch._C._jit_nvfuser_enabled", + "torch._C._jit_nvfuser_horizontal_mode", + "torch._C._jit_nvfuser_set_comparison_callback", + "torch._C._jit_nvfuser_single_node_mode", + "torch._C._jit_object_is_non_holding", + "torch._C._jit_onnx_convert_pattern_from_subblock", + "torch._C._jit_onnx_create_full_scope_name", + "torch._C._jit_onnx_list_model_parameters", + "torch._C._jit_onnx_log", + "torch._C._jit_opt_conditionals", + "torch._C._jit_override_can_fuse_on_cpu_legacy", + "torch._C._jit_override_can_fuse_on_cpu", + "torch._C._jit_override_can_fuse_on_gpu", + "torch._C._jit_pass_autocast", + "torch._C._jit_pass_batch_mm", + "torch._C._jit_pass_canonicalize_graph_fuser_ops", + "torch._C._jit_pass_canonicalize", + "torch._C._jit_pass_complete_shape_analysis", + "torch._C._jit_pass_concat_frozen_linear", + "torch._C._jit_pass_constant_loop_unrolling", + "torch._C._jit_pass_constant_pooling", + "torch._C._jit_pass_constant_propagation_immutable_types", + "torch._C._jit_pass_constant_propagation", + "torch._C._jit_pass_convert_frozen_ops_to_mkldnn", + "torch._C._jit_pass_create_autodiff_subgraphs", + "torch._C._jit_pass_create_functional_graphs", + "torch._C._jit_pass_cse", + "torch._C._jit_pass_custom_pattern_based_rewrite_graph", + "torch._C._jit_pass_custom_pattern_based_rewrite", + "torch._C._jit_pass_dbr_quant_remove_redundant_aliases", + "torch._C._jit_pass_dce_allow_deleting_nodes_with_side_effects", + "torch._C._jit_pass_dce", + "torch._C._jit_pass_decompose_ops", + "torch._C._jit_pass_dedup_module_uses", + "torch._C._jit_pass_erase_number_types", + "torch._C._jit_pass_erase_shape_information", + "torch._C._jit_pass_filter_non_tensor_arguments", + "torch._C._jit_pass_fixup_onnx_controlflow_node", + "torch._C._jit_pass_fold_convbn", + "torch._C._jit_pass_fold_frozen_conv_add_or_sub", + "torch._C._jit_pass_fold_frozen_conv_bn", + "torch._C._jit_pass_fold_frozen_conv_mul_or_div", + "torch._C._jit_pass_fold_frozen_linear_bn", + "torch._C._jit_pass_fold_prepacking_ops", + "torch._C._jit_pass_functional_to_inplace_activation", + "torch._C._jit_pass_fuse_add_relu", + "torch._C._jit_pass_fuse_addmm", + "torch._C._jit_pass_fuse_clamp_w_prepacked_linear_conv", + "torch._C._jit_pass_fuse_frozen_conv_add_relu", + "torch._C._jit_pass_fuse_linear", + "torch._C._jit_pass_fuse_quantized_add_relu", + "torch._C._jit_pass_fuse_tensorexprs", + "torch._C._jit_pass_fuse", + "torch._C._jit_pass_inline_fork_wait", + "torch._C._jit_pass_inline_functional_graphs", + "torch._C._jit_pass_inline", + "torch._C._jit_pass_inplace_to_functional_activation", + "torch._C._jit_pass_insert_observer_method_for_ondevice_ptq", + "torch._C._jit_pass_insert_observers", + "torch._C._jit_pass_insert_prepack_unpack", + "torch._C._jit_pass_insert_prepacked_ops", + "torch._C._jit_pass_insert_quant_dequant_for_ondevice_ptq", + "torch._C._jit_pass_insert_quant_dequant", + "torch._C._jit_pass_integer_value_refinement", + "torch._C._jit_pass_lint", + "torch._C._jit_pass_loop_unrolling", + "torch._C._jit_pass_lower_all_tuples", + "torch._C._jit_pass_lower_graph", + "torch._C._jit_pass_metal_fold_prepacking_ops", + "torch._C._jit_pass_metal_fuse_clamp_w_prepacked_conv", + "torch._C._jit_pass_metal_insert_prepacked_ops", + "torch._C._jit_pass_metal_optimize_for_mobile", + "torch._C._jit_pass_onnx_assign_output_shape", + "torch._C._jit_pass_onnx_assign_scoped_names_for_node_and_value", + "torch._C._jit_pass_onnx_autograd_function_process", + "torch._C._jit_pass_onnx_block", + "torch._C._jit_pass_onnx_cast_all_constant_to_floating", + "torch._C._jit_pass_onnx_clear_scope_records", + "torch._C._jit_pass_onnx_constant_fold", + "torch._C._jit_pass_onnx_deduplicate_initializers", + "torch._C._jit_pass_onnx_eliminate_unused_items", + "torch._C._jit_pass_onnx_eval_peephole", + "torch._C._jit_pass_onnx_function_extraction", + "torch._C._jit_pass_onnx_function_substitution", + "torch._C._jit_pass_onnx_graph_shape_type_inference", + "torch._C._jit_pass_onnx_lint", + "torch._C._jit_pass_onnx_node_shape_type_inference", + "torch._C._jit_pass_onnx_peephole", + "torch._C._jit_pass_onnx_preprocess_caffe2", + "torch._C._jit_pass_onnx_preprocess", + "torch._C._jit_pass_onnx_quantization_insert_permutes", + "torch._C._jit_pass_onnx_remove_inplace_ops_for_onnx", + "torch._C._jit_pass_onnx_remove_print", + "torch._C._jit_pass_onnx_scalar_type_analysis", + "torch._C._jit_pass_onnx_set_dynamic_input_shape", + "torch._C._jit_pass_onnx_track_scope_attributes", + "torch._C._jit_pass_onnx_unpack_quantized_weights", + "torch._C._jit_pass_onnx", + "torch._C._jit_pass_optimize_for_inference", + "torch._C._jit_pass_optimize_for_mobile", + "torch._C._jit_pass_optimize_frozen_graph", + "torch._C._jit_pass_pattern_based_rewrite", + "torch._C._jit_pass_peephole_list_idioms", + "torch._C._jit_pass_peephole", + "torch._C._jit_pass_prepare_division_for_onnx", + "torch._C._jit_pass_propagate_device", + "torch._C._jit_pass_propagate_dtype", + "torch._C._jit_pass_propagate_shapes_on_graph_and_build_compute", + "torch._C._jit_pass_propagate_shapes_on_graph", + "torch._C._jit_pass_quant_finalize_for_ondevice_ptq", + "torch._C._jit_pass_quant_finalize", + "torch._C._jit_pass_quant_fusion", + "torch._C._jit_pass_refine_integer_values", + "torch._C._jit_pass_refine_tuple_types", + "torch._C._jit_pass_remove_dropout", + "torch._C._jit_pass_remove_expands", + "torch._C._jit_pass_remove_inplace_ops", + "torch._C._jit_pass_remove_mutation", + "torch._C._jit_pass_replace_old_ops_with_upgraders", + "torch._C._jit_pass_replicate_dequantize", + "torch._C._jit_pass_run_decompositions", + "torch._C._jit_pass_specialize_autogradzero", + "torch._C._jit_pass_swap_functional_linear", + "torch._C._jit_pass_transform_conv1d_to_conv2d", + "torch._C._jit_pass_transpose_frozen_linear", + "torch._C._jit_pass_vulkan_fold_prepacking_ops", + "torch._C._jit_pass_vulkan_fuse_clamp_w_prepacked_conv", + "torch._C._jit_pass_vulkan_insert_prepacked_ops", + "torch._C._jit_pass_vulkan_optimize_for_mobile", + "torch._C._jit_register_decomposition_for_schema", + "torch._C._jit_register_shape_compute_graph_for_node", + "torch._C._jit_resolve_packet", + "torch._C._jit_run_cpp_tests", + "torch._C._jit_script_class_compile", + "torch._C._jit_script_compile_overload", + "torch._C._jit_script_compile", + "torch._C._jit_script_interface_compile", + "torch._C._jit_set_autocast_mode", + "torch._C._jit_set_bailout_depth", + "torch._C._jit_set_emit_hooks", + "torch._C._jit_set_fusion_strategy", + "torch._C._jit_set_inline_everything_mode", + "torch._C._jit_set_llga_enabled", + "torch._C._jit_set_logging_option", + "torch._C._jit_set_logging_stream", + "torch._C._jit_set_num_profiled_runs", + "torch._C._jit_set_nvfuser_enabled", + "torch._C._jit_set_nvfuser_guard_mode", + "torch._C._jit_set_nvfuser_horizontal_mode", + "torch._C._jit_set_nvfuser_single_node_mode", + "torch._C._jit_set_nvfuser_skip_node_kind", + "torch._C._jit_set_onnx_log_enabled", + "torch._C._jit_set_onnx_log_output_stream", + "torch._C._jit_set_profiling_executor", + "torch._C._jit_set_profiling_mode", + "torch._C._jit_set_symbolic_shapes_test_mode", + "torch._C._jit_set_te_cuda_pointwise_block_count", + "torch._C._jit_set_te_cuda_pointwise_block_size", + "torch._C._jit_set_te_cuda_pointwise_loop_levels", + "torch._C._jit_set_te_generate_block_code", + "torch._C._jit_set_te_must_use_llvm_cpu", + "torch._C._jit_set_texpr_dynamic_shape_enabled", + "torch._C._jit_set_texpr_fuser_enabled", + "torch._C._jit_set_texpr_reductions_enabled", + "torch._C._jit_set_tracer_state_warn", + "torch._C._jit_set_utf8_decoding_ignore", + "torch._C._jit_shape_compute_graph_for_node", + "torch._C._jit_symbolic_shapes_test_mode_enabled", + "torch._C._jit_texpr_dynamic_shape_enabled", + "torch._C._jit_texpr_fallback_allowed", + "torch._C._jit_texpr_fuser_enabled", + "torch._C._jit_texpr_reductions_enabled", + "torch._C._jit_texpr_set_fallback_allowed", + "torch._C._jit_to_backend_selective", + "torch._C._jit_to_backend", + "torch._C._jit_to_static_module", + "torch._C._jit_trace_graph", + "torch._C._jit_trace_module", + "torch._C._jit_tree_views.FalseLiteral", + "torch._C._jit_tree_views.NoneLiteral", + "torch._C._jit_tree_views.TrueLiteral", + "torch._C._jit_try_infer_type", + "torch._C._jit_unflatten", + "torch._C._last_executed_optimized_graph", + "torch._C._len_torch_dispatch_stack", + "torch._C._len_torch_function_stack", + "torch._C._linalg._linalg_eigvals", + "torch._C._linalg.linalg_cholesky_ex", + "torch._C._linalg.linalg_cholesky", + "torch._C._linalg.linalg_cond", + "torch._C._linalg.linalg_cross", + "torch._C._linalg.linalg_det", + "torch._C._linalg.linalg_diagonal", + "torch._C._linalg.linalg_eig", + "torch._C._linalg.linalg_eigh", + "torch._C._linalg.linalg_eigvals", + "torch._C._linalg.linalg_eigvalsh", + "torch._C._linalg.linalg_householder_product", + "torch._C._linalg.linalg_inv_ex", + "torch._C._linalg.linalg_inv", + "torch._C._linalg.linalg_ldl_factor_ex", + "torch._C._linalg.linalg_ldl_factor", + "torch._C._linalg.linalg_ldl_solve", + "torch._C._linalg.linalg_lstsq", + "torch._C._linalg.linalg_lu_factor_ex", + "torch._C._linalg.linalg_lu_factor", + "torch._C._linalg.linalg_lu_solve", + "torch._C._linalg.linalg_lu", + "torch._C._linalg.linalg_matmul", + "torch._C._linalg.linalg_matrix_exp", + "torch._C._linalg.linalg_matrix_norm", + "torch._C._linalg.linalg_matrix_power", + "torch._C._linalg.linalg_matrix_rank", + "torch._C._linalg.linalg_multi_dot", + "torch._C._linalg.linalg_norm", + "torch._C._linalg.linalg_pinv", + "torch._C._linalg.linalg_qr", + "torch._C._linalg.linalg_slogdet", + "torch._C._linalg.linalg_solve_ex", + "torch._C._linalg.linalg_solve_triangular", + "torch._C._linalg.linalg_solve", + "torch._C._linalg.linalg_svd", + "torch._C._linalg.linalg_svdvals", + "torch._C._linalg.linalg_tensorinv", + "torch._C._linalg.linalg_tensorsolve", + "torch._C._linalg.linalg_vander", + "torch._C._linalg.linalg_vecdot", + "torch._C._linalg.linalg_vector_norm", + "torch._C._llvm_enabled", + "torch._C._load_for_lite_interpreter_from_buffer", + "torch._C._load_for_lite_interpreter", + "torch._C._load_jit_module_from_bytes", + "torch._C._load_jit_module_from_file", + "torch._C._load_mobile_module_from_bytes", + "torch._C._load_mobile_module_from_file", + "torch._C._log_api_usage_metadata", + "torch._C._log_api_usage_once", + "torch._C._logging_set_logger", + "torch._C._meta_in_tls_dispatch_include", + "torch._C._mps_acquireEvent", + "torch._C._mps_currentAllocatedMemory", + "torch._C._mps_deviceSynchronize", + "torch._C._mps_driverAllocatedMemory", + "torch._C._mps_recommendedMaxMemory", + "torch._C._mps_elapsedTimeOfEvents", + "torch._C._mps_emptyCache", + "torch._C._mps_get_default_generator", + "torch._C._mps_is_available", + "torch._C._mps_is_in_bad_fork", + "torch._C._mps_is_on_macos_13_or_newer", + "torch._C._mps_profilerStartTrace", + "torch._C._mps_profilerStopTrace", + "torch._C._mps_queryEvent", + "torch._C._mps_recordEvent", + "torch._C._mps_releaseEvent", + "torch._C._mps_setMemoryFraction", + "torch._C._mps_synchronizeEvent", + "torch._C._mps_waitForEvent", + "torch._C._multiprocessing_init", + "torch._C._nccl_all_gather", + "torch._C._nccl_all_reduce", + "torch._C._nccl_broadcast", + "torch._C._nccl_init_rank", + "torch._C._nccl_reduce_scatter", + "torch._C._nccl_reduce", + "torch._C._nccl_unique_id", + "torch._C._nccl_version_suffix", + "torch._C._nccl_version", + "torch._C._nested.nested_tensor", + "torch._C._nested.nested_to_padded_tensor", + "torch._C._new_symbolic_shape_symbol", + "torch._C._nn_module_to_mobile", + "torch._C._nn._conv_depthwise2d", + "torch._C._nn._pad_circular", + "torch._C._nn._pad_enum", + "torch._C._nn._test_ambiguous_defaults", + "torch._C._nn._test_optional_filled_intlist", + "torch._C._nn._test_optional_floatlist", + "torch._C._nn._test_optional_intlist", + "torch._C._nn._test_string_default", + "torch._C._nn._test_warn_in_autograd", + "torch._C._nn._upsample_bicubic2d_aa", + "torch._C._nn._upsample_bilinear2d_aa", + "torch._C._nn._upsample_lanczos2d_aa", + "torch._C._nn._upsample_nearest_exact1d", + "torch._C._nn._upsample_nearest_exact2d", + "torch._C._nn._upsample_nearest_exact3d", + "torch._C._nn.adaptive_avg_pool2d", + "torch._C._nn.adaptive_avg_pool3d", + "torch._C._nn.adaptive_max_pool2d", + "torch._C._nn.adaptive_max_pool3d", + "torch._C._nn.avg_pool2d", + "torch._C._nn.avg_pool3d", + "torch._C._nn.binary_cross_entropy", + "torch._C._nn.col2im", + "torch._C._nn.conv_depthwise3d", + "torch._C._nn.cross_entropy_loss", + "torch._C._nn.elu_", + "torch._C._nn.elu", + "torch._C._nn.flatten_dense_tensors", + "torch._C._nn.fractional_max_pool2d", + "torch._C._nn.fractional_max_pool3d", + "torch._C._nn.gelu_", + "torch._C._nn.gelu", + "torch._C._nn.glu", + "torch._C._nn.hardsigmoid_", + "torch._C._nn.hardsigmoid", + "torch._C._nn.hardswish_", + "torch._C._nn.hardswish", + "torch._C._nn.hardtanh_", + "torch._C._nn.hardtanh", + "torch._C._nn.huber_loss", + "torch._C._nn.im2col", + "torch._C._nn.l1_loss", + "torch._C._nn.leaky_relu_", + "torch._C._nn.leaky_relu", + "torch._C._nn.linear", + "torch._C._nn.log_sigmoid", + "torch._C._nn.max_pool2d_with_indices", + "torch._C._nn.max_pool3d_with_indices", + "torch._C._nn.max_unpool2d", + "torch._C._nn.max_unpool3d", + "torch._C._nn.mish_", + "torch._C._nn.mish", + "torch._C._nn.mkldnn_linear", + "torch._C._nn.mkldnn_reorder_conv2d_weight", + "torch._C._nn.mkldnn_reorder_conv3d_weight", + "torch._C._nn.mse_loss", + "torch._C._nn.multi_margin_loss", + "torch._C._nn.multilabel_margin_loss", + "torch._C._nn.nll_loss_nd", + "torch._C._nn.nll_loss", + "torch._C._nn.nll_loss2d", + "torch._C._nn.one_hot", + "torch._C._nn.pad_sequence", + "torch._C._nn.pad", + "torch._C._nn.reflection_pad1d", + "torch._C._nn.reflection_pad2d", + "torch._C._nn.reflection_pad3d", + "torch._C._nn.relu6_", + "torch._C._nn.relu6", + "torch._C._nn.replication_pad1d", + "torch._C._nn.replication_pad2d", + "torch._C._nn.replication_pad3d", + "torch._C._nn.rrelu_with_noise_", + "torch._C._nn.rrelu_with_noise", + "torch._C._nn.scaled_dot_product_attention", + "torch._C._nn.silu_", + "torch._C._nn.silu", + "torch._C._nn.slow_conv_dilated2d", + "torch._C._nn.slow_conv_dilated3d", + "torch._C._nn.slow_conv_transpose2d", + "torch._C._nn.slow_conv_transpose3d", + "torch._C._nn.slow_conv3d", + "torch._C._nn.smooth_l1_loss", + "torch._C._nn.soft_margin_loss", + "torch._C._nn.softplus", + "torch._C._nn.softshrink", + "torch._C._nn.thnn_conv2d", + "torch._C._nn.unflatten_dense_tensors", + "torch._C._nn.upsample_bicubic2d", + "torch._C._nn.upsample_bilinear2d", + "torch._C._nn.upsample_linear1d", + "torch._C._nn.upsample_nearest1d", + "torch._C._nn.upsample_nearest2d", + "torch._C._nn.upsample_nearest3d", + "torch._C._nn.upsample_trilinear3d", + "torch._C._non_sym_sizes", + "torch._C._overlaps", + "torch._C._parallel_info", + "torch._C._parse_dispatch_key", + "torch._C._parse_source_def", + "torch._C._pop_torch_dispatch_stack", + "torch._C._pop_torch_function_stack", + "torch._C._propagate_and_assign_input_shapes", + "torch._C._propagate_shapes", + "torch._C._propagate_xla_data", + "torch._C._push_on_torch_dispatch_stack", + "torch._C._push_on_torch_function_stack", + "torch._C._quantize_ondevice_ptq_dynamic", + "torch._C._register_py_class_for_device", + "torch._C._remove_cached_tensor", + "torch._C._remove_worker_pids", + "torch._C._rename_privateuse1_backend", + "torch._C._replace_", + "torch._C._replace_overloaded_method_decl", + "torch._C._resolve_type_from_object", + "torch._C._resolve_type", + "torch._C._rocm_is_backward_pass", + "torch._C._rpc_init", + "torch._C._run_emit_module_hook", + "torch._C._save_jit_module_to_bytes", + "torch._C._save_jit_module", + "torch._C._save_mobile_module_to_bytes", + "torch._C._save_mobile_module", + "torch._C._save_parameters", + "torch._C._scatter_out", + "torch._C._scatter", + "torch._C._select_conv_backend", + "torch._C._select_batch_norm_backend", + "torch._C._set_autograd_fallback_mode", + "torch._C._set_backcompat_broadcast_warn", + "torch._C._set_backcompat_keepdim_warn", + "torch._C._set_blas_preferred_backend", + "torch._C._set_cached_tensors_enabled", + "torch._C._set_check_sparse_tensor_invariants", + "torch._C._set_conj", + "torch._C._set_cublas_allow_bf16_reduced_precision_reduction", + "torch._C._set_cublas_allow_fp16_reduced_precision_reduction", + "torch._C._set_cublas_allow_tf32", + "torch._C._set_cudnn_allow_tf32", + "torch._C._set_cudnn_benchmark", + "torch._C._set_cudnn_deterministic", + "torch._C._set_cudnn_enabled", + "torch._C._set_default_dtype", + "torch._C._set_default_mobile_cpu_allocator", + "torch._C._set_default_tensor_type", + "torch._C._set_deterministic_algorithms", + "torch._C._set_deterministic_fill_uninitialized_memory", + "torch._C._set_dispatch_mode", + "torch._C._set_float32_matmul_precision", + "torch._C._set_fwd_grad_enabled", + "torch._C._set_grad_enabled", + "torch._C._set_graph_executor_optimize", + "torch._C._set_linalg_preferred_backend", + "torch._C._set_rocm_fa_preferred_backend", + "torch._C._set_meta_in_tls_dispatch_include", + "torch._C._set_mkldnn_enabled", + "torch._C._set_multithreading_enabled", + "torch._C._set_neg", + "torch._C._set_nnpack_enabled", + "torch._C._set_print_stack_traces_on_fatal_signal", + "torch._C._set_qengine", + "torch._C._set_sdp_use_flash", + "torch._C._set_sdp_use_math", + "torch._C._set_math_sdp_allow_fp16_bf16_reduction", + "torch._C._set_sdp_use_mem_efficient", + "torch._C._set_sdp_use_overrideable", + "torch._C._set_should_use_format_with_string_table", + "torch._C._set_sm_carveout_experimental", + "torch._C._set_storage_access_error_msg", + "torch._C._set_tensor_metadata", + "torch._C._set_tracing_state", + "torch._C._set_value_trace", + "torch._C._set_view_replay_enabled", + "torch._C._set_warnAlways", + "torch._C._set_worker_pids", + "torch._C._set_worker_signal_handlers", + "torch._C._should_allow_numbers_as_tensors", + "torch._C._show_config", + "torch._C._sparse._sparse_addmm", + "torch._C._sparse._sparse_log_softmax", + "torch._C._sparse._sparse_mm_reduce_impl", + "torch._C._sparse._sparse_mm", + "torch._C._sparse._sparse_softmax", + "torch._C._sparse._spdiags", + "torch._C._sparse.sparse_sampled_addmm", + "torch._C._special.special_airy_ai", + "torch._C._special.special_bessel_j0", + "torch._C._special.special_bessel_j1", + "torch._C._special.special_bessel_y0", + "torch._C._special.special_bessel_y1", + "torch._C._special.special_chebyshev_polynomial_t", + "torch._C._special.special_chebyshev_polynomial_u", + "torch._C._special.special_chebyshev_polynomial_v", + "torch._C._special.special_chebyshev_polynomial_w", + "torch._C._special.special_digamma", + "torch._C._special.special_entr", + "torch._C._special.special_erf", + "torch._C._special.special_erfc", + "torch._C._special.special_erfcx", + "torch._C._special.special_erfinv", + "torch._C._special.special_exp2", + "torch._C._special.special_expit", + "torch._C._special.special_expm1", + "torch._C._special.special_gammainc", + "torch._C._special.special_gammaincc", + "torch._C._special.special_gammaln", + "torch._C._special.special_hermite_polynomial_h", + "torch._C._special.special_hermite_polynomial_he", + "torch._C._special.special_i0", + "torch._C._special.special_i0e", + "torch._C._special.special_i1", + "torch._C._special.special_i1e", + "torch._C._special.special_laguerre_polynomial_l", + "torch._C._special.special_legendre_polynomial_p", + "torch._C._special.special_log_ndtr", + "torch._C._special.special_log_softmax", + "torch._C._special.special_log1p", + "torch._C._special.special_logit", + "torch._C._special.special_logsumexp", + "torch._C._special.special_modified_bessel_i0", + "torch._C._special.special_modified_bessel_i1", + "torch._C._special.special_modified_bessel_k0", + "torch._C._special.special_modified_bessel_k1", + "torch._C._special.special_multigammaln", + "torch._C._special.special_ndtr", + "torch._C._special.special_ndtri", + "torch._C._special.special_polygamma", + "torch._C._special.special_psi", + "torch._C._special.special_round", + "torch._C._special.special_scaled_modified_bessel_k0", + "torch._C._special.special_scaled_modified_bessel_k1", + "torch._C._special.special_shifted_chebyshev_polynomial_t", + "torch._C._special.special_shifted_chebyshev_polynomial_u", + "torch._C._special.special_shifted_chebyshev_polynomial_v", + "torch._C._special.special_shifted_chebyshev_polynomial_w", + "torch._C._special.special_sinc", + "torch._C._special.special_softmax", + "torch._C._special.special_spherical_bessel_j0", + "torch._C._special.special_xlog1py", + "torch._C._special.special_xlogy", + "torch._C._special.special_zeta", + "torch._C._stash_obj_in_tls", + "torch._C._storage_id", + "torch._C._storage_Use_Count", + "torch._C._supported_qengines", + "torch._C._te.abs", + "torch._C._te.acos", + "torch._C._te.annotate_input_shapes", + "torch._C._te.asin", + "torch._C._te.atan", + "torch._C._te.atan2", + "torch._C._te.ceil", + "torch._C._te.Compute", + "torch._C._te.Compute2", + "torch._C._te.construct_codegen", + "torch._C._te.cos", + "torch._C._te.cosh", + "torch._C._te.erf", + "torch._C._te.erfc", + "torch._C._te.exp", + "torch._C._te.expm1", + "torch._C._te.fixup_missing_shape_info", + "torch._C._te.floor", + "torch._C._te.fmod", + "torch._C._te.frac", + "torch._C._te.ifThenElse", + "torch._C._te.is_graph_compilable", + "torch._C._te.isnan", + "torch._C._te.lgamma", + "torch._C._te.log", + "torch._C._te.log10", + "torch._C._te.log1p", + "torch._C._te.log2", + "torch._C._te.lower", + "torch._C._te.make_shapes_symbolic", + "torch._C._te.pow", + "torch._C._te.Reduce", + "torch._C._te.remainder", + "torch._C._te.remove_graph_output", + "torch._C._te.remove_unused_self_argument", + "torch._C._te.replace_list_output_with_tuple", + "torch._C._te.round", + "torch._C._te.rsqrt", + "torch._C._te.sigmoid", + "torch._C._te.simplify", + "torch._C._te.sin", + "torch._C._te.sinh", + "torch._C._te.sqrt", + "torch._C._te.tan", + "torch._C._te.tanh", + "torch._C._te.trim_graph", + "torch._C._te.trunc", + "torch._C._tensor_impl_raw_handle", + "torch._C._test_only_add_entry_to_op_version_map", + "torch._C._test_only_populate_upgraders", + "torch._C._test_only_remove_entry_to_op_version_map", + "torch._C._test_only_remove_upgraders", + "torch._C._to_functionality_key", + "torch._C._tracer_set_force_outplace", + "torch._C._tracer_set_get_unique_name_fn", + "torch._C._tracer_warn_use_python", + "torch._C._unset_default_mobile_cpu_allocator", + "torch._C._unset_dispatch_mode", + "torch._C._valgrind_supported_platform", + "torch._C._valgrind_toggle_and_dump_stats", + "torch._C._valgrind_toggle", + "torch._C._verbose.mkl_set_verbose", + "torch._C._verbose.mkldnn_set_verbose", + "torch._C._vmapmode_decrement_nesting", + "torch._C._vmapmode_increment_nesting", + "torch._C._warn_deprecation", + "torch._C._warn", + "torch._C._will_engine_execute_node", + "torch._C._wrap_tensor_impl", + "torch._C._xpu_emptyCache", + "torch._C._xpu_getArchFlags", + "torch._C._xpu_getCurrentStream", + "torch._C._xpu_getCurrentRawStream", + "torch._C._xpu_getDeviceCount", + "torch._C._xpu_getDevice", + "torch._C._xpu_getMemoryInfo", + "torch._C._xpu_getStreamFromExternal", + "torch._C._xpu_isInBadFork", + "torch._C._xpu_init", + "torch._C._xpu_memoryStats", + "torch._C._xpu_resetAccumulatedMemoryStats", + "torch._C._xpu_resetPeakMemoryStats", + "torch._C._xpu_setStream", + "torch._C._xpu_synchronize", + "torch._C.autocast_decrement_nesting", + "torch._C.autocast_increment_nesting", + "torch._C.clear_autocast_cache", + "torch._C.fork", + "torch._C.get_autocast_cpu_dtype", + "torch._C.get_autocast_dtype", + "torch._C.get_autocast_gpu_dtype", + "torch._C.get_autocast_ipu_dtype", + "torch._C.get_autocast_xla_dtype", + "torch._C.get_default_dtype", + "torch._C.get_num_interop_threads", + "torch._C.get_num_threads", + "torch._C.import_ir_module_from_buffer", + "torch._C.import_ir_module", + "torch._C.init_num_threads", + "torch._C.is_anomaly_check_nan_enabled", + "torch._C.is_anomaly_enabled", + "torch._C.is_autocast_cache_enabled", + "torch._C.is_autocast_cpu_enabled", + "torch._C.is_autocast_enabled", + "torch._C.is_autocast_ipu_enabled", + "torch._C.is_autocast_xla_enabled", + "torch._C.is_grad_enabled", + "torch._C.is_inference_mode_enabled", + "torch._C.merge_type_from_type_comment", + "torch._C.parse_ir", + "torch._C.parse_schema", + "torch._C.parse_type_comment", + "torch._C.set_autocast_cache_enabled", + "torch._C.set_autocast_enabled", + "torch._C.unify_type_list", + "torch._C.wait", + "torch._cast_Byte", + "torch._cast_Char", + "torch._cast_Double", + "torch._cast_Float", + "torch._cast_Half", + "torch._cast_Int", + "torch._cast_Long", + "torch._cast_Short", + "torch._choose_qparams_per_tensor", + "torch._chunk_cat", + "torch._coalesce", + "torch._compute_linear_combination", + "torch._conj_copy", + "torch._conj_physical", + "torch._conj", + "torch._convert_indices_from_coo_to_csr", + "torch._convert_indices_from_csr_to_coo", + "torch._convert_weight_to_int4pack", + "torch._convert_weight_to_int4pack_for_cpu", + "torch._convolution_mode", + "torch._convolution", + "torch._copy_from_and_resize", + "torch._copy_from", + "torch._cslt_compress", + "torch._cslt_sparse_mm", + "torch._ctc_loss", + "torch._cudnn_ctc_loss", + "torch._cudnn_init_dropout_state", + "torch._cudnn_rnn_flatten_weight", + "torch._cudnn_rnn", + "torch._cufft_clear_plan_cache", + "torch._cufft_get_plan_cache_max_size", + "torch._cufft_get_plan_cache_size", + "torch._cufft_set_plan_cache_max_size", + "torch._cummax_helper", + "torch._cummin_helper", + "torch._debug_has_internal_overlap", + "torch._dim_arange", + "torch._dirichlet_grad", + "torch._disable_functionalization", + "torch._dyn_quant_matmul_4bit", + "torch._dyn_quant_pack_4bit_weight", + "torch._efficientzerotensor", + "torch._embedding_bag_forward_only", + "torch._embedding_bag", + "torch._empty_affine_quantized", + "torch._empty_per_channel_affine_quantized", + "torch._enable_functionalization", + "torch._euclidean_dist", + "torch._fake_quantize_learnable_per_channel_affine", + "torch._fake_quantize_learnable_per_tensor_affine", + "torch._fake_quantize_per_tensor_affine_cachemask_tensor_qparams", + "torch._fft_c2c", + "torch._fft_c2r", + "torch._fft_r2c", + "torch._fill_mem_eff_dropout_mask_", + "torch._foobar", + "torch._foreach_abs_", + "torch._foreach_abs", + "torch._foreach_acos_", + "torch._foreach_acos", + "torch._foreach_add_", + "torch._foreach_add", + "torch._foreach_addcdiv_", + "torch._foreach_addcdiv", + "torch._foreach_addcmul_", + "torch._foreach_addcmul", + "torch._foreach_asin_", + "torch._foreach_asin", + "torch._foreach_atan_", + "torch._foreach_atan", + "torch._foreach_ceil_", + "torch._foreach_ceil", + "torch._foreach_clamp_max_", + "torch._foreach_clamp_max", + "torch._foreach_clamp_min_", + "torch._foreach_clamp_min", + "torch._foreach_clone", + "torch._foreach_copy_", + "torch._foreach_cos_", + "torch._foreach_cos", + "torch._foreach_cosh_", + "torch._foreach_cosh", + "torch._foreach_div_", + "torch._foreach_div", + "torch._foreach_erf_", + "torch._foreach_erf", + "torch._foreach_erfc_", + "torch._foreach_erfc", + "torch._foreach_exp_", + "torch._foreach_exp", + "torch._foreach_expm1_", + "torch._foreach_expm1", + "torch._foreach_floor_", + "torch._foreach_floor", + "torch._foreach_frac_", + "torch._foreach_frac", + "torch._foreach_lerp_", + "torch._foreach_lerp", + "torch._foreach_lgamma_", + "torch._foreach_lgamma", + "torch._foreach_log_", + "torch._foreach_log", + "torch._foreach_log10_", + "torch._foreach_log10", + "torch._foreach_log1p_", + "torch._foreach_log1p", + "torch._foreach_log2_", + "torch._foreach_log2", + "torch._foreach_maximum_", + "torch._foreach_maximum", + "torch._foreach_minimum_", + "torch._foreach_minimum", + "torch._foreach_mul_", + "torch._foreach_mul", + "torch._foreach_neg_", + "torch._foreach_neg", + "torch._foreach_norm", + "torch._foreach_pow_", + "torch._foreach_pow", + "torch._foreach_reciprocal_", + "torch._foreach_reciprocal", + "torch._foreach_round_", + "torch._foreach_round", + "torch._foreach_sigmoid_", + "torch._foreach_sigmoid", + "torch._foreach_rsqrt_", + "torch._foreach_rsqrt", + "torch._foreach_sign_", + "torch._foreach_sign", + "torch._foreach_sin_", + "torch._foreach_sin", + "torch._foreach_sinh_", + "torch._foreach_sinh", + "torch._foreach_sqrt_", + "torch._foreach_sqrt", + "torch._foreach_sub_", + "torch._foreach_sub", + "torch._foreach_tan_", + "torch._foreach_tan", + "torch._foreach_tanh_", + "torch._foreach_tanh", + "torch._foreach_trunc_", + "torch._foreach_trunc", + "torch._foreach_zero_", + "torch._freeze_functional_tensor", + "torch._from_functional_tensor", + "torch._functional_assert_async", + "torch._functional_sym_constrain_range_for_size", + "torch._functional_sym_constrain_range", + "torch._functionalize_are_all_mutations_hidden_from_autograd", + "torch._functionalize_commit_update", + "torch._functionalize_enable_reapply_views", + "torch._functionalize_has_data_mutation", + "torch._functionalize_has_metadata_mutation", + "torch._functionalize_is_multi_output_view", + "torch._functionalize_mark_mutation_hidden_from_autograd", + "torch._functionalize_replace", + "torch._functionalize_sync", + "torch._functionalize_was_storage_changed", + "torch._fused_adagrad_", + "torch._fused_adam_", + "torch._fused_adamw_", + "torch._fused_dropout", + "torch._fused_moving_avg_obs_fq_helper", + "torch._fused_sdp_choice", + "torch._fw_primal_copy", + "torch._grid_sampler_2d_cpu_fallback", + "torch._grouped_mm", + "torch._histogramdd_bin_edges", + "torch._histogramdd_from_bin_cts", + "torch._histogramdd_from_bin_tensors", + "torch._index_put_impl_", + "torch._indices_copy", + "torch._int_mm", + "torch._is_all_true", + "torch._is_any_true", + "torch._is_functional_tensor", + "torch._is_zerotensor", + "torch._linalg_check_errors", + "torch._linalg_det", + "torch._linalg_eigh", + "torch._linalg_eigvals", + "torch._linalg_slogdet", + "torch._linalg_solve_ex", + "torch._linalg_svd", + "torch._log_softmax_backward_data", + "torch._log_softmax", + "torch._logcumsumexp", + "torch._lstm_mps", + "torch._lu_with_info", + "torch._make_dep_token", + "torch._make_dual_copy", + "torch._make_dual", + "torch._make_per_channel_quantized_tensor", + "torch._make_per_tensor_quantized_tensor", + "torch._masked_scale", + "torch._masked_softmax", + "torch._mirror_autograd_meta_to", + "torch._mixed_dtypes_linear", + "torch._mkldnn_reshape", + "torch._mkldnn_transpose_", + "torch._mkldnn_transpose", + "torch._mps_convolution_transpose", + "torch._mps_convolution", + "torch._native_batch_norm_legit_no_training", + "torch._native_batch_norm_legit", + "torch._native_multi_head_attention", + "torch._neg_view_copy", + "torch._neg_view", + "torch._nested_from_padded_and_nested_example", + "torch._nested_from_padded_tensor", + "torch._nested_tensor_from_mask_left_aligned", + "torch._nested_tensor_from_tensor_list", + "torch._nested_tensor_softmax_with_shape", + "torch._nested_view_from_buffer_copy", + "torch._nested_view_from_buffer", + "torch._nnpack_available", + "torch._nnpack_spatial_convolution", + "torch._pack_padded_sequence", + "torch._pad_packed_sequence", + "torch._pin_memory", + "torch._prelu_kernel", + "torch._propagate_xla_data", + "torch._remove_batch_dim", + "torch._reshape_alias_copy", + "torch._reshape_from_tensor", + "torch._resize_output_", + "torch._rowwise_prune", + "torch._sample_dirichlet", + "torch._saturate_weight_to_fp16", + "torch._scaled_dot_product_attention_math", + "torch._scaled_dot_product_efficient_attention", + "torch._scaled_dot_product_flash_attention", + "torch._scaled_dot_product_flash_attention_for_cpu", + "torch._scaled_dot_product_cudnn_attention", + "torch._scaled_mm", + "torch._scaled_grouped_mm", + "torch._shape_as_tensor", + "torch._sobol_engine_draw", + "torch._sobol_engine_ff_", + "torch._sobol_engine_initialize_state_", + "torch._sobol_engine_scramble_", + "torch._softmax_backward_data", + "torch._softmax", + "torch._sparse_broadcast_to_copy", + "torch._sparse_broadcast_to", + "torch._sparse_csr_prod", + "torch._sparse_csr_sum", + "torch._sparse_log_softmax_backward_data", + "torch._sparse_semi_structured_addmm", + "torch._sparse_semi_structured_linear", + "torch._sparse_semi_structured_mm", + "torch._sparse_softmax_backward_data", + "torch._sparse_sparse_matmul", + "torch._sparse_sum", + "torch._stack", + "torch._standard_gamma_grad", + "torch._standard_gamma", + "torch._test_autograd_multiple_dispatch_view_copy", + "torch._test_autograd_multiple_dispatch_view", + "torch._test_autograd_multiple_dispatch", + "torch._test_check_tensor", + "torch._test_functorch_fallback", + "torch._test_serialization_subcmul", + "torch._to_cpu", + "torch._to_functional_tensor", + "torch._to_sparse_semi_structured", + "torch._transform_bias_rescale_qkv", + "torch._transformer_encoder_layer_fwd", + "torch._trilinear", + "torch._triton_multi_head_attention", + "torch._triton_scaled_dot_attention", + "torch._unique", + "torch._unique2", + "torch._unpack_dual", + "torch._unsafe_index_put", + "torch._unsafe_index", + "torch._unsafe_masked_index_put_accumulate", + "torch._unsafe_masked_index", + "torch._use_cudnn_ctc_loss", + "torch._use_cudnn_rnn_flatten_weight", + "torch._values_copy", + "torch._weight_int4pack_mm", + "torch._weight_int4pack_mm_for_cpu", + "torch._weight_int4pack_mm_with_scales_and_zeros", + "torch._weight_int8pack_mm", + "torch._weight_norm_interface", + "torch._weight_norm", + "torch.abs_", + "torch.abs", + "torch.absolute", + "torch.acos_", + "torch.acos", + "torch.acosh_", + "torch.acosh", + "torch.adaptive_avg_pool1d", + "torch.adaptive_max_pool1d", + "torch.add", + "torch.addbmm", + "torch.addcdiv", + "torch.addcmul", + "torch.addmm", + "torch.addmv_", + "torch.addmv", + "torch.addr", + "torch.adjoint", + "torch.affine_grid_generator", + "torch.alias_copy", + "torch.all", + "torch.allclose", + "torch.alpha_dropout_", + "torch.alpha_dropout", + "torch.amax", + "torch.amin", + "torch.aminmax", + "torch.angle", + "torch.any", + "torch.arange", + "torch.arccos_", + "torch.arccos", + "torch.arccosh_", + "torch.arccosh", + "torch.arcsin_", + "torch.arcsin", + "torch.arcsinh_", + "torch.arcsinh", + "torch.arctan_", + "torch.arctan", + "torch.arctan2", + "torch.arctanh_", + "torch.arctanh", + "torch.argmax", + "torch.argmin", + "torch.argsort", + "torch.argwhere", + "torch.as_strided_", + "torch.as_strided_copy", + "torch.as_strided_scatter", + "torch.as_strided", + "torch.as_tensor", + "torch.asarray", + "torch.asin_", + "torch.asin", + "torch.asinh_", + "torch.asinh", + "torch.atan_", + "torch.atan", + "torch.atan2", + "torch.atanh_", + "torch.atanh", + "torch.avg_pool1d", + "torch.baddbmm", + "torch.bartlett_window", + "torch.batch_norm_backward_elemt", + "torch.batch_norm_backward_reduce", + "torch.batch_norm_elemt", + "torch.batch_norm_gather_stats_with_counts", + "torch.batch_norm_gather_stats", + "torch.batch_norm_stats", + "torch.batch_norm_update_stats", + "torch.batch_norm", + "torch.bernoulli", + "torch.bilinear", + "torch.binary_cross_entropy_with_logits", + "torch.bincount", + "torch.binomial", + "torch.bitwise_and", + "torch.bitwise_left_shift", + "torch.bitwise_not", + "torch.bitwise_or", + "torch.bitwise_right_shift", + "torch.bitwise_xor", + "torch.blackman_window", + "torch.bmm", + "torch.broadcast_to", + "torch.bucketize", + "torch.can_cast", + "torch.cat", + "torch.ccol_indices_copy", + "torch.ceil_", + "torch.ceil", + "torch.celu_", + "torch.celu", + "torch.channel_shuffle", + "torch.cholesky_inverse", + "torch.cholesky_solve", + "torch.cholesky", + "torch.choose_qparams_optimized", + "torch.chunk", + "torch.clamp_", + "torch.clamp_max_", + "torch.clamp_max", + "torch.clamp_min_", + "torch.clamp_min", + "torch.clamp", + "torch.clip_", + "torch.clip", + "torch.clone", + "torch.col_indices_copy", + "torch.column_stack", + "torch.combinations", + "torch.complex", + "torch.concat", + "torch.concatenate", + "torch.conj_physical_", + "torch.conj_physical", + "torch.conj", + "torch.constant_pad_nd", + "torch.conv_tbc", + "torch.conv_transpose1d", + "torch.conv_transpose2d", + "torch.conv_transpose3d", + "torch.conv1d", + "torch.conv2d", + "torch.conv3d", + "torch.convolution", + "torch.copysign", + "torch.corrcoef", + "torch.cos_", + "torch.cos", + "torch.cosh_", + "torch.cosh", + "torch.cosine_embedding_loss", + "torch.cosine_similarity", + "torch.count_nonzero", + "torch.cov", + "torch.cross", + "torch.crow_indices_copy", + "torch.ctc_loss", + "torch.cudnn_affine_grid_generator", + "torch.cudnn_batch_norm", + "torch.cudnn_convolution_add_relu", + "torch.cudnn_convolution_relu", + "torch.cudnn_convolution_transpose", + "torch.cudnn_convolution", + "torch.cudnn_grid_sampler", + "torch.cudnn_is_acceptable", + "torch.cummax", + "torch.cummin", + "torch.cumprod", + "torch.cumsum", + "torch.cumulative_trapezoid", + "torch.deg2rad_", + "torch.deg2rad", + "torch.dequantize", + "torch.det", + "torch.detach_", + "torch.detach_copy", + "torch.detach", + "torch.diag_embed", + "torch.diag", + "torch.diagflat", + "torch.diagonal_copy", + "torch.diagonal_scatter", + "torch.diagonal", + "torch.diff", + "torch.digamma", + "torch.dist", + "torch.div", + "torch.divide", + "torch.dot", + "torch.dropout_", + "torch.dropout", + "torch.dsmm", + "torch.dsplit", + "torch.dstack", + "torch.embedding_bag", + "torch.embedding_renorm_", + "torch.embedding", + "torch.empty_like", + "torch.empty_permuted", + "torch.empty_quantized", + "torch.empty_strided", + "torch.empty", + "torch.eq", + "torch.equal", + "torch.erf_", + "torch.erf", + "torch.erfc_", + "torch.erfc", + "torch.erfinv", + "torch.exp_", + "torch.exp", + "torch.exp2_", + "torch.exp2", + "torch.expand_copy", + "torch.expm1_", + "torch.expm1", + "torch.eye", + "torch.fake_quantize_per_channel_affine", + "torch.fake_quantize_per_tensor_affine", + "torch.fbgemm_linear_fp16_weight_fp32_activation", + "torch.fbgemm_linear_fp16_weight", + "torch.fbgemm_linear_int8_weight_fp32_activation", + "torch.fbgemm_linear_int8_weight", + "torch.fbgemm_linear_quantize_weight", + "torch.fbgemm_pack_gemm_matrix_fp16", + "torch.fbgemm_pack_quantized_matrix", + "torch.feature_alpha_dropout_", + "torch.feature_alpha_dropout", + "torch.feature_dropout_", + "torch.feature_dropout", + "torch.fill_", + "torch.fill", + "torch.fix_", + "torch.fix", + "torch.flatten", + "torch.flip", + "torch.fliplr", + "torch.flipud", + "torch.float_power", + "torch.floor_", + "torch.floor_divide", + "torch.floor", + "torch.fmax", + "torch.fmin", + "torch.fmod", + "torch.frac_", + "torch.frac", + "torch.frexp", + "torch.frobenius_norm", + "torch.from_file", + "torch.from_numpy", + "torch.frombuffer", + "torch.full_like", + "torch.full", + "torch.fused_moving_avg_obs_fake_quant", + "torch.gather", + "torch.gcd_", + "torch.gcd", + "torch.ge", + "torch.geqrf", + "torch.ger", + "torch.get_device", + "torch.get_device_module", + "torch.gradient", + "torch.greater_equal", + "torch.greater", + "torch.grid_sampler_2d", + "torch.grid_sampler_3d", + "torch.grid_sampler", + "torch.group_norm", + "torch.gru_cell", + "torch.gru", + "torch.gt", + "torch.hamming_window", + "torch.hann_window", + "torch.hardshrink", + "torch.hash_tensor", + "torch.heaviside", + "torch.hinge_embedding_loss", + "torch.histc", + "torch.histogram", + "torch.histogramdd", + "torch.hsmm", + "torch.hsplit", + "torch.hspmm", + "torch.hstack", + "torch.hypot", + "torch.i0_", + "torch.i0", + "torch.igamma", + "torch.igammac", + "torch.imag", + "torch.index_add", + "torch.index_copy", + "torch.index_fill", + "torch.index_put_", + "torch.index_put", + "torch.index_reduce", + "torch.index_select", + "torch.indices_copy", + "torch.inner", + "torch.instance_norm", + "torch.int_repr", + "torch.inverse", + "torch.is_complex", + "torch.is_conj", + "torch.is_distributed", + "torch.is_floating_point", + "torch.is_inference", + "torch.is_neg", + "torch.is_nonzero", + "torch.is_same_size", + "torch.is_signed", + "torch.is_vulkan_available", + "torch.isclose", + "torch.isfinite", + "torch.isin", + "torch.isinf", + "torch.isnan", + "torch.isneginf", + "torch.isposinf", + "torch.isreal", + "torch.istft", + "torch.kaiser_window", + "torch.kl_div", + "torch.kron", + "torch.kthvalue", + "torch.layer_norm", + "torch.lcm_", + "torch.lcm", + "torch.ldexp_", + "torch.ldexp", + "torch.le", + "torch.lerp", + "torch.less_equal", + "torch.less", + "torch.lgamma", + "torch.linspace", + "torch.log_", + "torch.log_softmax", + "torch.log", + "torch.log10_", + "torch.log10", + "torch.log1p_", + "torch.log1p", + "torch.log2_", + "torch.log2", + "torch.logaddexp", + "torch.logaddexp2", + "torch.logcumsumexp", + "torch.logdet", + "torch.logical_and", + "torch.logical_not", + "torch.logical_or", + "torch.logical_xor", + "torch.logit_", + "torch.logit", + "torch.logspace", + "torch.logsumexp", + "torch.lstm_cell", + "torch.lstm", + "torch.lt", + "torch.lu_solve", + "torch.lu_unpack", + "torch.margin_ranking_loss", + "torch.masked_fill", + "torch.masked_scatter", + "torch.masked_select", + "torch.matmul", + "torch.matrix_exp", + "torch.matrix_power", + "torch.max_pool1d_with_indices", + "torch.max_pool1d", + "torch.max_pool2d", + "torch.max_pool3d", + "torch.max", + "torch.maximum", + "torch.mean", + "torch.median", + "torch.min", + "torch.minimum", + "torch.miopen_batch_norm", + "torch.miopen_convolution_add_relu", + "torch.miopen_convolution_relu", + "torch.miopen_convolution_transpose", + "torch.miopen_convolution", + "torch.miopen_depthwise_convolution", + "torch.miopen_rnn", + "torch.mkldnn_adaptive_avg_pool2d", + "torch.mkldnn_convolution", + "torch.mkldnn_linear_backward_weights", + "torch.mkldnn_max_pool2d", + "torch.mkldnn_max_pool3d", + "torch.mkldnn_rnn_layer", + "torch.mm", + "torch.mode", + "torch.moveaxis", + "torch.movedim", + "torch.msort", + "torch.mul", + "torch.multinomial", + "torch.multiply", + "torch.mv", + "torch.mvlgamma", + "torch.nan_to_num_", + "torch.nan_to_num", + "torch.nanmean", + "torch.nanmedian", + "torch.nanquantile", + "torch.nansum", + "torch.narrow_copy", + "torch.narrow", + "torch.native_batch_norm", + "torch.native_channel_shuffle", + "torch.native_dropout", + "torch.native_group_norm", + "torch.native_layer_norm", + "torch.native_norm", + "torch.ne", + "torch.neg_", + "torch.neg", + "torch.negative_", + "torch.negative", + "torch.nextafter", + "torch.nonzero_static", + "torch.nonzero", + "torch.norm_except_dim", + "torch.normal", + "torch.not_equal", + "torch.nuclear_norm", + "torch.numel", + "torch.ones_like", + "torch.ones", + "torch.orgqr", + "torch.ormqr", + "torch.outer", + "torch.pairwise_distance", + "torch.pdist", + "torch.permute_copy", + "torch.permute", + "torch.pinverse", + "torch.pixel_shuffle", + "torch.pixel_unshuffle", + "torch.poisson_nll_loss", + "torch.poisson", + "torch.polar", + "torch.polygamma", + "torch.positive", + "torch.pow", + "torch.prelu", + "torch._print", + "torch.prod", + "torch.promote_types", + "torch.put", + "torch.q_per_channel_axis", + "torch.q_per_channel_scales", + "torch.q_per_channel_zero_points", + "torch.q_scale", + "torch.q_zero_point", + "torch.qr", + "torch.quantile", + "torch.quantize_per_channel", + "torch.quantize_per_tensor_dynamic", + "torch.quantize_per_tensor", + "torch.quantized_batch_norm", + "torch.quantized_gru_cell", + "torch.quantized_lstm_cell", + "torch.quantized_max_pool1d", + "torch.quantized_max_pool2d", + "torch.quantized_max_pool3d", + "torch.quantized_rnn_relu_cell", + "torch.quantized_rnn_tanh_cell", + "torch.rad2deg_", + "torch.rad2deg", + "torch.rand_like", + "torch.rand", + "torch.randint_like", + "torch.randint", + "torch.randn_like", + "torch.randn", + "torch.randperm", + "torch.range", + "torch.ravel", + "torch.real", + "torch.reciprocal_", + "torch.reciprocal", + "torch.relu_", + "torch.relu", + "torch.remainder", + "torch.renorm", + "torch.repeat_interleave", + "torch.reshape", + "torch.resolve_conj", + "torch.resolve_neg", + "torch.result_type", + "torch.rms_norm", + "torch.rnn_relu_cell", + "torch.rnn_relu", + "torch.rnn_tanh_cell", + "torch.rnn_tanh", + "torch.roll", + "torch.rot90", + "torch.round_", + "torch.round", + "torch.row_indices_copy", + "torch.row_stack", + "torch.rrelu_", + "torch.rrelu", + "torch.rsqrt_", + "torch.rsqrt", + "torch.rsub", + "torch.saddmm", + "torch.scalar_tensor", + "torch.scatter_add", + "torch.scatter_reduce", + "torch.scatter", + "torch.searchsorted", + "torch.segment_reduce", + "torch.select_copy", + "torch.select_scatter", + "torch.select", + "torch.selu_", + "torch.selu", + "torch.set_autocast_dtype", + "torch.sgn", + "torch.sigmoid_", + "torch.sigmoid", + "torch.sign", + "torch.signal.windows.windows.sqrt", + "torch.signbit", + "torch.sin_", + "torch.sin", + "torch.sinc_", + "torch.sinc", + "torch.sinh_", + "torch.sinh", + "torch.slice_copy", + "torch.slice_scatter", + "torch.slogdet", + "torch.smm", + "torch.softmax", + "torch.sort", + "torch.split_copy", + "torch.split_with_sizes_copy", + "torch.split_with_sizes", + "torch.spmm", + "torch.sqrt_", + "torch.sqrt", + "torch.square_", + "torch.square", + "torch.squeeze_copy", + "torch.squeeze", + "torch.sspaddmm", + "torch.stack", + "torch.std_mean", + "torch.std", + "torch.sub", + "torch.subtract", + "torch.sum", + "torch.svd", + "torch.swapaxes", + "torch.swapdims", + "torch.sym_constrain_range_for_size", + "torch.sym_constrain_range", + "torch.t_copy", + "torch.t", + "torch.take_along_dim", + "torch.take", + "torch.tan_", + "torch.tan", + "torch.tanh_", + "torch.tanh", + "torch.tensor_split", + "torch.tensor", + "torch.threshold_", + "torch.threshold", + "torch.tile", + "torch.topk", + "torch.trace", + "torch.transpose_copy", + "torch.transpose", + "torch.trapezoid", + "torch.trapz", + "torch.triangular_solve", + "torch.tril_indices", + "torch.tril", + "torch.triplet_margin_loss", + "torch.triu_indices", + "torch.triu", + "torch.true_divide", + "torch.trunc_", + "torch.trunc", + "torch.unbind_copy", + "torch.unbind", + "torch.unflatten", + "torch.unfold_copy", + "torch.unsafe_chunk", + "torch.unsafe_split_with_sizes", + "torch.unsafe_split", + "torch.unsqueeze_copy", + "torch.unsqueeze", + "torch.values_copy", + "torch.vander", + "torch.var_mean", + "torch.var", + "torch.vdot", + "torch.view_as_complex_copy", + "torch.view_as_complex", + "torch.view_as_real_copy", + "torch.view_as_real", + "torch.view_copy", + "torch.vsplit", + "torch.vstack", + "torch.where", + "torch.xlogy_", + "torch.xlogy", + "torch.zero_", + "torch.zeros", + "torch.zeros_like", + "torch._fused_sgd_", + "torch.slice_inverse", + "torch._assert_scalar", + "torch._functional_assert_scalar", + "torch.xpu._get_device_properties", + ], + TorchInGraphFunctionVariable, +) + + +if sys.version_info >= (3, 11): + torch_c_binding_in_graph_functions["math.exp2"] = TorchInGraphFunctionVariable + torch_c_binding_in_graph_functions["math.cbrt"] = TorchInGraphFunctionVariable + +if sys.version_info >= (3, 13): + torch_c_binding_in_graph_functions["math.fma"] = TorchInGraphFunctionVariable + +# In graph functions (including constant folding) that are not C bindings +# NOTE: [Cacheability of in-graph torch functions] +# Functions in this list have the property that graphs containing them are safe to cache/serialize. +# serialize given only the information in the graph. I.e, either: +# - Your function does not access or close over global state, or +# - Your function closes over global state, but this state is guarded by dynamo, either +# through constant folding or other mechanisms +# If your function needs a custom special handler (via @register on TorchInGraphFunctionVariable), +# or captures global state, please add it to manual_torch_name_rule_map instead +torch_non_c_binding_in_graph_functions = dict.fromkeys( + [ + "torch.__future__.get_overwrite_module_params_on_conversion", + "torch.__future__.set_overwrite_module_params_on_conversion", + "torch.__getattr__", + "torch._assert", + "torch._check_index", + "torch._check_is_size", + "torch._check_not_implemented", + "torch._check_tensor_all_with", + "torch._check_tensor_all", + "torch._check_type", + "torch._check_value", + "torch._check_with", + "torch._compile._disable_dynamo", + "torch._functorch.apis.chunk_vmap", + "torch._functorch.batch_norm_replacement.batch_norm_without_running_stats", + "torch._functorch.batch_norm_replacement.replace_all_batch_norm_modules_", + "torch._functorch.deprecated.combine_state_for_ensemble", + "torch._functorch.deprecated.functionalize", + "torch._functorch.deprecated.get_warning", + "torch._functorch.deprecated.make_functional_with_buffers", + "torch._functorch.deprecated.make_functional", + "torch._functorch.deprecated.setup_docs", + "torch._functorch.deprecated.warn_deprecated", + "torch._functorch.eager_transforms._any_differentiable", + "torch._functorch.eager_transforms._autograd_grad", + "torch._functorch.eager_transforms._set_tensor_requires_grad", + "torch._functorch.eager_transforms._is_differentiable", + "torch._functorch.eager_transforms._maybe_unwrap_functional_tensor", + "torch._functorch.eager_transforms._maybe_wrap_functional_tensor", + "torch._functorch.eager_transforms._unwrap_all_tensors_from_functional", + "torch._functorch.eager_transforms._wrap_all_tensors_to_functional", + "torch._functorch.eager_transforms.assert_flat_tuple_of_tensors", + "torch._functorch.eager_transforms.functionalize", + "torch._functorch.eager_transforms.lazy_dynamo_disable", + "torch._functorch.eager_transforms.noop", + "torch._functorch.utils.enable_single_level_autograd_function", + "torch._functorch.utils.exposed_in", + "torch._functorch.utils.unwrap_dead_wrappers", + "torch._functorch.predispatch.lazy_load_decompositions", + "torch._functorch.predispatch._vmap_increment_nesting", + "torch._functorch.predispatch._vmap_decrement_nesting", + "torch._functorch.predispatch._add_batch_dim", + "torch._functorch.predispatch._remove_batch_dim", + "torch._guards.compile_context", + "torch._guards.detect_fake_mode", + "torch._guards.tracing", + "torch._higher_order_ops.map._has_potential_branch_input_alias", + "torch._higher_order_ops.map._has_potential_branch_input_mutation", + "torch._higher_order_ops.map._stack_pytree", + "torch._higher_order_ops.map._unstack_pytree", + "torch._higher_order_ops.map.create_fw_bw_graph", + "torch._higher_order_ops.map.map_autograd", + "torch._higher_order_ops.map.map_dense", + "torch._higher_order_ops.map.map_fake_tensor_mode", + "torch._higher_order_ops.map.map_functionalize", + "torch._higher_order_ops.map.map_proxy_torch_dispatch_mode", + "torch._higher_order_ops.map.map_wrapper", + "torch._higher_order_ops.map.trace_map", + "torch._higher_order_ops.out_dtype.elementwise_dtypes", + "torch._higher_order_ops.out_dtype.is_int_mm", + "torch._higher_order_ops.out_dtype.out_dtype_dense", + "torch._higher_order_ops.out_dtype.out_dtype_fake_tensor_mode", + "torch._higher_order_ops.out_dtype.out_dtype_fallback", + "torch._higher_order_ops.out_dtype.out_dtype_func", + "torch._higher_order_ops.out_dtype.out_dtype_proxy", + "torch._higher_order_ops.out_dtype.trace_out_dtype", + "torch._higher_order_ops.utils.autograd_not_implemented_inner", + "torch._higher_order_ops.utils.autograd_not_implemented", + "torch._linalg_utils._symeig", + "torch._linalg_utils.basis", + "torch._linalg_utils.bform", + "torch._linalg_utils.eig", + "torch._linalg_utils.get_floating_dtype", + "torch._linalg_utils.is_sparse", + "torch._linalg_utils.lstsq", + "torch._linalg_utils.matmul", + "torch._linalg_utils.matrix_rank", + "torch._linalg_utils.qform", + "torch._linalg_utils.solve", + "torch._linalg_utils.symeig", + "torch._load_global_deps", + "torch._lowrank._svd_lowrank", + "torch._lowrank.get_approximate_basis", + "torch._lowrank.pca_lowrank", + "torch._lowrank.svd_lowrank", + "torch._preload_cuda_deps", + "torch._register_device_module", + "torch._utils._dummy_type", + "torch._utils._flatten_dense_tensors", + "torch._utils._unflatten_dense_tensors", + "torch._weights_only_unpickler._get_allowed_globals", + "torch._weights_only_unpickler.load", + "torch.accelerator.current_accelerator", + "torch.accelerator.current_device_index", + "torch.accelerator.current_stream", + "torch.accelerator.device_count", + "torch.accelerator.is_available", + "torch.accelerator.set_stream", + "torch.accelerator.synchronize", + "torch.align_tensors", + "torch.amp.autocast_mode.autocast_decorator", + "torch.amp.autocast_mode.custom_bwd", + "torch.amp.autocast_mode.custom_fwd", + "torch.are_deterministic_algorithms_enabled", + "torch.atleast_1d", + "torch.atleast_2d", + "torch.atleast_3d", + "torch.autograd._calculate_shape", + "torch.autograd._is_checkpoint_valid", + "torch.autograd._profiler_enabled", + "torch.autograd._make_grads", + "torch.autograd._register_py_tensor_class_for_device", + "torch.autograd._tensor_or_tensors_to_tuple", + "torch.autograd.forward_ad._maybe_load_decompositions", + "torch.autograd.function._iter_filter", + "torch.autograd.function._iter_jit_values", + "torch.autograd.function._iter_None_tensors", + "torch.autograd.function._iter_tensors_permissive", + "torch.autograd.function._iter_tensors", + "torch.autograd.function._jit_unwrap_structured", + "torch.autograd.function._map_tensor_data", + "torch.autograd.function._nested_map", + "torch.autograd.function._unflatten", + "torch.autograd.function.once_differentiable", + "torch.autograd.function.traceable", + "torch.autograd.functional._as_tuple_nocheck", + "torch.autograd.functional._as_tuple", + "torch.autograd.functional._autograd_grad", + "torch.autograd.functional._check_requires_grad", + "torch.autograd.functional._construct_standard_basis_for", + "torch.autograd.functional._fill_in_zeros", + "torch.autograd.functional._grad_postprocess", + "torch.autograd.functional._grad_preprocess", + "torch.autograd.functional._jacfwd", + "torch.autograd.functional._tuple_postprocess", + "torch.autograd.functional._validate_v", + "torch.autograd.functional.hessian", + "torch.autograd.functional.hvp", + "torch.autograd.functional.jacobian", + "torch.autograd.functional.jvp", + "torch.autograd.functional.vhp", + "torch.autograd.functional.vjp", + "torch.autograd.grad_mode._enter_inference_mode", + "torch.autograd.grad_mode._exit_inference_mode", + "torch.autograd.graph._get_sid", + "torch.autograd.graph._get_tid", + "torch.autograd.graph.allow_mutation_on_saved_tensors", + "torch.autograd.graph.get_gradient_edge", + "torch.autograd.graph.increment_version", + "torch.autograd.graph.register_multi_grad_hook", + "torch.autograd.variable", + "torch.backends.__allow_nonbracketed_mutation", + "torch.backends.cpu.get_cpu_capability", + "torch.backends.cuda.can_use_efficient_attention", + "torch.backends.cuda.can_use_flash_attention", + "torch.backends.cuda.can_use_cudnn_attention", + "torch.backends.cuda.enable_flash_sdp", + "torch.backends.cuda.enable_math_sdp", + "torch.backends.cuda.allow_fp16_bf16_reduction_math_sdp", + "torch.backends.cuda.enable_mem_efficient_sdp", + "torch.backends.cuda.flash_sdp_enabled", + "torch.backends.cuda.is_built", + "torch.backends.cuda.is_flash_attention_available", + "torch.backends.cuda.math_sdp_enabled", + "torch.backends.cuda.fp16_bf16_reduction_math_sdp_allowed", + "torch.backends.cuda.mem_efficient_sdp_enabled", + "torch.backends.cuda.cudnn_sdp_enabled", + "torch.backends.cuda.enable_cudnn_sdp", + "torch.backends.cuda.preferred_blas_library", + "torch.backends.cuda.preferred_linalg_library", + "torch.backends.cuda.preferred_rocm_fa_library", + "torch.backends.cuda.sdp_kernel", + "torch.backends.cudnn._init", + "torch.backends.cudnn.flags", + "torch.backends.cudnn.is_acceptable", + "torch.backends.cudnn.is_available", + "torch.backends.cudnn.set_flags", + "torch.backends.cudnn.version", + "torch.backends.disable_global_flags", + "torch.backends.flags_frozen", + "torch.backends.mkl.is_available", + "torch.backends.mkldnn.flags", + "torch.backends.mkldnn.is_available", + "torch.backends.mkldnn.set_flags", + "torch.backends.mps._init", + "torch.backends.mps.is_available", + "torch.backends.mps.is_built", + "torch.backends.mps.is_macos13_or_newer", + "torch.backends.openmp.is_available", + "torch.backends.quantized._get_qengine_id", + "torch.backends.quantized._get_qengine_str", + "torch.block_diag", + "torch.broadcast_tensors", + "torch.cartesian_prod", + "torch.cdist", + "torch.chain_matmul", + "torch.compile", + "torch.compiled_with_cxx11_abi", + "torch.cpu._init_amx", + "torch.cpu.get_capabilities", + "torch.cpu.current_device", + "torch.cpu.current_stream", + "torch.cpu.device_count", + "torch.cpu.is_available", + "torch.cpu.set_device", + "torch.cpu.stream", + "torch.cpu.synchronize", + "torch.cuda._check_capability", + "torch.cuda._check_cubins", + "torch.cuda._device_count_amdsmi", + "torch.cuda._device_count_nvml", + "torch.cuda._get_amdsmi_handler", + "torch.cuda._get_amdsmi_device_index", + "torch.cuda._get_device", + "torch.cuda._get_generator", + "torch.cuda._get_nvml_device_index", + "torch.cuda._get_pynvml_handler", + "torch.cuda._get_rng_state_offset", + "torch.cuda._is_compiled", + "torch.cuda._lazy_call", + "torch.cuda._lazy_init", + "torch.cuda._memory_viz._block_extra_legacy", + "torch.cuda._memory_viz._block_extra", + "torch.cuda._memory_viz._format_size", + "torch.cuda._memory_viz._format_viz", + "torch.cuda._memory_viz._frame_filter", + "torch.cuda._memory_viz._frame_fmt", + "torch.cuda._memory_viz._frames_fmt", + "torch.cuda._memory_viz._profile_to_snapshot", + "torch.cuda._memory_viz._report_free", + "torch.cuda._memory_viz._write_blocks", + "torch.cuda._memory_viz.calc_active", + "torch.cuda._memory_viz.compare", + "torch.cuda._memory_viz.format_flamegraph", + "torch.cuda._memory_viz.memory", + "torch.cuda._memory_viz.profile_plot", + "torch.cuda._memory_viz.segment_plot", + "torch.cuda._memory_viz.segments", + "torch.cuda._memory_viz.segsum", + "torch.cuda._memory_viz.trace_plot", + "torch.cuda._memory_viz.trace", + "torch.cuda._nvml_based_avail", + "torch.cuda._parse_visible_devices", + "torch.cuda._raw_device_count_amdsmi", + "torch.cuda._raw_device_count_nvml", + "torch.cuda._raw_device_uuid_amdsmi", + "torch.cuda._raw_device_uuid_nvml", + "torch.cuda._register_triton_kernels", + "torch.cuda._set_rng_state_offset", + "torch.cuda._set_stream_by_id", + "torch.cuda._sleep", + "torch.cuda._transform_uuid_to_ordinals", + "torch.cuda._utils._get_device_index", + "torch.cuda.amp.autocast_mode._cast", + "torch.cuda.amp.autocast_mode.custom_bwd", + "torch.cuda.amp.autocast_mode.custom_fwd", + "torch.cuda.amp.common.amp_definitely_not_available", + "torch.amp.grad_scaler._refresh_per_optimizer_state", + "torch.cuda.can_device_access_peer", + "torch.cuda.check_error", + "torch.cuda.clock_rate", + "torch.cuda.cudart", + "torch.cuda.current_blas_handle", + "torch.cuda.current_stream", + "torch.cuda.default_stream", + "torch.cuda.device_count", + "torch.cuda.device_memory_used", + "torch.cuda.get_arch_list", + "torch.cuda.get_device_capability", + "torch.cuda.get_device_name", + "torch.cuda.get_device_properties", + "torch.cuda.get_gencode_flags", + "torch.cuda.get_sync_debug_mode", + "torch.cuda.synchronize", + "torch.cuda.graphs.graph_pool_handle", + "torch.cuda.graphs.is_current_stream_capturing", + "torch.cuda.graphs.make_graphed_callables", + "torch.cuda.init", + "torch.cuda.ipc_collect", + "torch.cuda.is_available", + "torch.cuda.is_bf16_supported", + "torch.cuda.is_initialized", + "torch.cuda.jiterator._create_jit_fn", + "torch.cuda.jiterator._create_multi_output_jit_fn", + "torch.cuda.memory_usage", + "torch.cuda.memory._dump_snapshot", + "torch.cuda.memory._get_current_allocator", + "torch.cuda.memory._host_allocator", + "torch.cuda.memory._record_memory_history_impl", + "torch.cuda.memory._record_memory_history_legacy", + "torch.cuda.memory._record_memory_history", + "torch.cuda.memory._save_memory_usage", + "torch.cuda.memory._save_segment_usage", + "torch.cuda.memory._set_allocator_settings", + "torch.cuda.memory._snapshot", + "torch.cuda.memory.caching_allocator_alloc", + "torch.cuda.memory.caching_allocator_delete", + "torch.cuda.memory.caching_allocator_enable", + "torch.cuda.memory.change_current_allocator", + "torch.cuda.memory.empty_cache", + "torch.cuda.memory.get_allocator_backend", + "torch.cuda.memory.get_per_process_memory_fraction", + "torch.cuda.memory.host_memory_stats_as_nested_dict", + "torch.cuda.memory.host_memory_stats", + "torch.cuda.memory.list_gpu_processes", + "torch.cuda.memory.max_memory_allocated", + "torch.cuda.memory.max_memory_cached", + "torch.cuda.memory.max_memory_reserved", + "torch.cuda.memory.mem_get_info", + "torch.cuda.memory.memory_allocated", + "torch.cuda.memory.memory_cached", + "torch.cuda.memory.memory_reserved", + "torch.cuda.memory.memory_snapshot", + "torch.cuda.memory.memory_stats_as_nested_dict", + "torch.cuda.memory.memory_stats", + "torch.cuda.memory.memory_summary", + "torch.cuda.memory.reset_accumulated_host_memory_stats", + "torch.cuda.memory.reset_accumulated_memory_stats", + "torch.cuda.memory.reset_max_memory_allocated", + "torch.cuda.memory.reset_max_memory_cached", + "torch.cuda.memory.reset_peak_host_memory_stats", + "torch.cuda.memory.reset_peak_memory_stats", + "torch.cuda.memory.set_per_process_memory_fraction", + "torch.cuda.nccl._check_sequence_type", + "torch.cuda.nccl.all_gather", + "torch.cuda.nccl.all_reduce", + "torch.cuda.nccl.broadcast", + "torch.cuda.nccl.init_rank", + "torch.cuda.nccl.is_available", + "torch.cuda.nccl.reduce_scatter", + "torch.cuda.nccl.reduce", + "torch.cuda.nccl.unique_id", + "torch.cuda.nccl.version", + "torch.cuda.nvtx.mark", + "torch.cuda.nvtx.range_end", + "torch.cuda.nvtx.range_pop", + "torch.cuda.nvtx.range_push", + "torch.cuda.nvtx.range_start", + "torch.cuda.nvtx.range", + "torch.cuda.power_draw", + "torch.cuda.profiler.init", + "torch.cuda.profiler.profile", + "torch.cuda.profiler.start", + "torch.cuda.profiler.stop", + "torch.cuda.random.get_rng_state_all", + "torch.cuda.random.initial_seed", + "torch.cuda.random.manual_seed_all", + "torch.cuda.random.manual_seed", + "torch.cuda.random.seed_all", + "torch.cuda.random.seed", + "torch.cuda.random.set_rng_state_all", + "torch.cuda.set_stream", + "torch.cuda.set_sync_debug_mode", + "torch.cuda.stream", + "torch.cuda.temperature", + "torch.cuda.utilization", + "torch.einsum", + "torch.functional._check_list_size", + "torch.functional._consecutive_return_counts", + "torch.functional._consecutive_return_inverse_false", + "torch.functional._consecutive_return_inverse_true", + "torch.functional._consecutive_return_inverse", + "torch.functional._consecutive_return_output", + "torch.functional._lu_impl", + "torch.functional._lu_no_infos", + "torch.functional._lu_with_infos", + "torch.functional._meshgrid", + "torch.functional._return_counts", + "torch.functional._return_inverse_false", + "torch.functional._return_inverse_true", + "torch.functional._return_inverse", + "torch.functional._return_output", + "torch.functional._unique_consecutive_impl", + "torch.functional._unique_impl", + "torch.functional._unravel_index", + "torch.functional.broadcast_shapes", + "torch.functional.lu", + "torch.functional.unique", + "torch.functional.unravel_index", + "torch.futures.collect_all", + "torch.futures.wait_all", + "torch.fx.experimental.const_fold.split_const_subgraphs", + "torch.fx.experimental.proxy_tensor.make_fx", + "torch.get_deterministic_debug_mode", + "torch.get_float32_matmul_precision", + "torch.is_deterministic_algorithms_warn_only_enabled", + "torch.is_storage", + "torch.is_tensor", + "torch.is_warn_always_enabled", + "torch.masked._ops._any", + "torch.masked._ops._apply_docstring_templates", + "torch.masked._ops._canonical_dim", + "torch.masked._ops._combine_input_and_mask", + "torch.masked._ops._generate_docstring", + "torch.masked._ops._input_mask", + "torch.masked._ops._output_mask", + "torch.masked._ops._reduction_identity", + "torch.masked._ops._sparse_coo_flatten_indices", + "torch.masked._ops._sparse_coo_scatter_reduction_helper", + "torch.masked._ops._sparse_coo_where", + "torch.masked._ops._sparse_csr_segment_reduction_helper", + "torch.masked._ops._sparse_csr_where", + "torch.masked._ops._std_var", + "torch.masked._ops._where", + "torch.masked._ops.amax", + "torch.masked._ops.amin", + "torch.masked._ops.argmax", + "torch.masked._ops.argmin", + "torch.masked._ops.corresponding_real_dtype", + "torch.masked._ops.cumprod", + "torch.masked._ops.cumsum", + "torch.masked._ops.log_softmax", + "torch.masked._ops.logaddexp", + "torch.masked._ops.logsumexp", + "torch.masked._ops.mean", + "torch.masked._ops.median", + "torch.masked._ops.norm", + "torch.masked._ops.normalize", + "torch.masked._ops.prod", + "torch.masked._ops.softmax", + "torch.masked._ops.softmin", + "torch.masked._ops.std", + "torch.masked._ops.sum", + "torch.masked._ops.var", + "torch.meshgrid", + "torch.mps._get_default_mps_generator", + "torch.mps.current_allocated_memory", + "torch.mps.driver_allocated_memory", + "torch.mps.empty_cache", + "torch.mps.get_rng_state", + "torch.mps.manual_seed", + "torch.mps.profiler.profile", + "torch.mps.profiler.start", + "torch.mps.profiler.stop", + "torch.mps.seed", + "torch.mps.set_per_process_memory_fraction", + "torch.mps.set_rng_state", + "torch.mps.synchronize", + "torch.nested._internal.nested_tensor.buffer_from_jagged", + "torch.nested._internal.nested_tensor.get_tensor_symint", + "torch.nested._internal.nested_tensor.is_expandable_to", + "torch.nested._internal.nested_tensor.jagged_from_list", + "torch.nested._internal.nested_tensor.jagged_from_tensor_and_lengths", + "torch.nested._internal.nested_tensor.nested_view_from_values_offsets", + "torch.nested._internal.nested_tensor.nested_view_from_values_offsets_lengths", + "torch.nested.as_nested_tensor", + "torch.nested.narrow", + "torch.nested.nested_tensor", + "torch.nn._reduction.get_enum", + "torch.nn._reduction.legacy_get_enum", + "torch.nn._reduction.legacy_get_string", + "torch.nn.factory_kwargs", + "torch.nn.functional.adaptive_avg_pool2d", + "torch.nn.functional.adaptive_avg_pool3d", + "torch.nn.functional.adaptive_max_pool1d_with_indices", + "torch.nn.functional.adaptive_max_pool1d", + "torch.nn.functional.adaptive_max_pool2d_with_indices", + "torch.nn.functional.adaptive_max_pool2d", + "torch.nn.functional.adaptive_max_pool3d_with_indices", + "torch.nn.functional.adaptive_max_pool3d", + "torch.nn.functional.affine_grid", + "torch.nn.functional.alpha_dropout", + "torch.nn.functional.assert_int_or_pair", + "torch.nn.functional.batch_norm", + "torch.nn.functional.binary_cross_entropy_with_logits", + "torch.nn.functional.binary_cross_entropy", + "torch.nn.functional.celu", + "torch.nn.functional.cosine_embedding_loss", + "torch.nn.functional.cross_entropy", + "torch.nn.functional.ctc_loss", + "torch.nn.functional.dropout", + "torch.nn.functional.dropout1d", + "torch.nn.functional.dropout2d", + "torch.nn.functional.dropout3d", + "torch.nn.functional.elu", + "torch.nn.functional.embedding_bag", + "torch.nn.functional.embedding", + "torch.nn.functional.feature_alpha_dropout", + "torch.nn.functional.fold", + "torch.nn.functional.fractional_max_pool2d_with_indices", + "torch.nn.functional.fractional_max_pool2d", + "torch.nn.functional.fractional_max_pool3d_with_indices", + "torch.nn.functional.fractional_max_pool3d", + "torch.nn.functional.gaussian_nll_loss", + "torch.nn.functional.glu", + "torch.nn.functional.grid_sample", + "torch.nn.functional.group_norm", + "torch.nn.functional.gumbel_softmax", + "torch.nn.functional.hardsigmoid", + "torch.nn.functional.hardswish", + "torch.nn.functional.hardtanh", + "torch.nn.functional.hinge_embedding_loss", + "torch.nn.functional.huber_loss", + "torch.nn.functional.instance_norm", + "torch.nn.functional.interpolate", + "torch.nn.functional.kl_div", + "torch.nn.functional.l1_loss", + "torch.nn.functional.layer_norm", + "torch.nn.functional.leaky_relu", + "torch.nn.functional.local_response_norm", + "torch.nn.functional.log_softmax", + "torch.nn.functional.lp_pool1d", + "torch.nn.functional.lp_pool2d", + "torch.nn.functional.margin_ranking_loss", + "torch.nn.functional.max_pool1d_with_indices", + "torch.nn.functional.max_pool1d", + "torch.nn.functional.max_pool2d_with_indices", + "torch.nn.functional.max_pool2d", + "torch.nn.functional.max_pool3d_with_indices", + "torch.nn.functional.max_pool3d", + "torch.nn.functional.max_unpool1d", + "torch.nn.functional.max_unpool2d", + "torch.nn.functional.max_unpool3d", + "torch.nn.functional.mish", + "torch.nn.functional.mse_loss", + "torch.nn.functional.multi_head_attention_forward", + "torch.nn.functional.multi_margin_loss", + "torch.nn.functional.multilabel_margin_loss", + "torch.nn.functional.multilabel_soft_margin_loss", + "torch.nn.functional.nll_loss", + "torch.nn.functional.normalize", + "torch.nn.functional.poisson_nll_loss", + "torch.nn.functional.relu", + "torch.nn.functional.relu6", + "torch.nn.functional.rrelu", + "torch.nn.functional.selu", + "torch.nn.functional.sigmoid", + "torch.nn.functional.silu", + "torch.nn.functional.smooth_l1_loss", + "torch.nn.functional.soft_margin_loss", + "torch.nn.functional.softmax", + "torch.nn.functional.softmin", + "torch.nn.functional.softsign", + "torch.nn.functional.tanh", + "torch.nn.functional.tanhshrink", + "torch.nn.functional.triplet_margin_loss", + "torch.nn.functional.unfold", + "torch.nn.functional.upsample_bilinear", + "torch.nn.functional.upsample_nearest", + "torch.nn.functional.upsample", + "torch.nn.grad._pair", + "torch.nn.grad._single", + "torch.nn.grad._triple", + "torch.nn.grad.conv1d_input", + "torch.nn.grad.conv1d_weight", + "torch.nn.grad.conv2d_input", + "torch.nn.grad.conv2d_weight", + "torch.nn.grad.conv3d_input", + "torch.nn.grad.conv3d_weight", + "torch.nn.modules.activation._is_make_fx_tracing", + "torch.nn.modules.utils._list_with_default", + "torch.nn.modules.utils._ntuple", + "torch.nn.modules.utils._quadruple", + "torch.nn.modules.utils._reverse_repeat_tuple", + "torch.nn.modules.utils.consume_prefix_in_state_dict_if_present", + "torch.nn.parameter.is_lazy", + "torch.norm", + "torch.quantization.default_eval_fn", + "torch.random._seed_custom_device", + "torch.random.fork_rng", + "torch.random.initial_seed", + "torch.random.seed", + "torch.return_types.pytree_register_structseq", + "torch.set_default_dtype", + "torch.set_default_tensor_type", + "torch.set_deterministic_debug_mode", + "torch.set_float32_matmul_precision", + "torch.set_warn_always", + "torch.signal.windows.windows._add_docstr", + "torch.signal.windows.windows._window_function_checks", + "torch.signal.windows.windows.bartlett", + "torch.signal.windows.windows.blackman", + "torch.signal.windows.windows.cosine", + "torch.signal.windows.windows.exponential", + "torch.signal.windows.windows.gaussian", + "torch.signal.windows.windows.general_cosine", + "torch.signal.windows.windows.general_hamming", + "torch.signal.windows.windows.hamming", + "torch.signal.windows.windows.hann", + "torch.signal.windows.windows.kaiser", + "torch.signal.windows.windows.merge_dicts", + "torch.signal.windows.windows.nuttall", + "torch.signal.windows.windows.parse_kwargs", + "torch.sparse.semi_structured.to_sparse_semi_structured", + "torch.sparse.sum", + "torch.split", + "torch.stft", + "torch.sym_float", + "torch.sym_int", + "torch.sym_ite", + "torch.sym_max", + "torch.sym_min", + "torch.sym_not", + "torch.tensordot", + "torch.unique_consecutive", + "torch.use_deterministic_algorithms", + "torch.xpu._get_device", + "torch.xpu._get_generator", + "torch.xpu._get_rng_state_offset", + "torch.xpu._is_compiled", + "torch.xpu._lazy_call", + "torch.xpu._lazy_init", + "torch.xpu._set_rng_state_offset", + "torch.xpu._set_stream_by_id", + "torch.xpu._utils._get_device_index", + "torch.xpu.current_device", + "torch.xpu.current_stream", + "torch.xpu.device_count", + "torch.xpu.get_arch_list", + "torch.xpu.get_device_capability", + "torch.xpu.get_device_name", + "torch.xpu.get_device_properties", + "torch.xpu.get_gencode_flags", + "torch.xpu.get_stream_from_external", + "torch.xpu.init", + "torch.xpu.is_available", + "torch.xpu.is_bf16_supported", + "torch.xpu.is_initialized", + "torch.xpu.memory.empty_cache", + "torch.xpu.memory.max_memory_allocated", + "torch.xpu.memory.max_memory_reserved", + "torch.xpu.memory.mem_get_info", + "torch.xpu.memory.memory_allocated", + "torch.xpu.memory.memory_reserved", + "torch.xpu.memory.memory_stats_as_nested_dict", + "torch.xpu.memory.memory_stats", + "torch.xpu.memory.reset_accumulated_memory_stats", + "torch.xpu.memory.reset_peak_memory_stats", + "torch.xpu.random.initial_seed", + "torch.xpu.random.seed_all", + "torch.xpu.random.seed", + "torch.xpu.set_stream", + "torch.xpu.stream", + "torch.xpu.synchronize", + ], + TorchInGraphFunctionVariable, +) + + +torch_name_rule_map = [ + manual_torch_name_rule_map, + torch_c_binding_in_graph_functions, + torch_non_c_binding_in_graph_functions, +] + + +""" +Generate the torch object - Dynamo tracing rule (the wrapping variable) map. +""" + + +@functools.cache +def get_torch_obj_rule_map() -> dict[Any, type["VariableTracker"]]: + d: dict[Any, type[VariableTracker]] = {} + for m in torch_name_rule_map: + for k, v in m.items(): # type: ignore[attr-defined] + if ".py#" not in k: + obj = load_object(k) + else: + torch_dir = _module_dir(torch) + if torch_dir is None: + continue + obj = torch_dir + k[len("torch/") :] + if obj is not None: + if is_lru_cache_wrapped_function(obj): + obj = obj.__wrapped__ + if obj in d and d[obj] != v: + raise AssertionError( + f"Duplicate torch object {obj} with different rules: {v}, {d[obj]}" + ) + else: + d[obj] = v + return d + + +def _load_obj_from_str(fully_qualified_name: str) -> Any: + module, obj_name = fully_qualified_name.rsplit(".", maxsplit=1) + return getattr(importlib.import_module(module), obj_name) + + +""" +Load string represented torch objects. +""" + + +def load_object(name: str) -> Any: + try: + x = name.split("#") + if len(x) == 2: + obj = _load_obj_from_str(x[0]) + val = getattr(obj, x[1]) + else: + assert len(x) == 1, f"Invalid obj name {name}" + val = _load_obj_from_str(x[0]) + val = unwrap_if_wrapper(val) + except (AttributeError, ImportError): + val = None + return val + + +""" +Get all torch.Tensor methods which are allowed to be in graph functions. +""" + + +@functools.cache +def get_tensor_method() -> frozenset[Any]: + disallowed_tensor_methods = {"__new__", "_make_wrapper_subclass", "_make_subclass"} + s = set() + for name in dir(torch.Tensor): + method = getattr(torch.Tensor, name) + if ( + isinstance( + method, + ( + types.MethodDescriptorType, + types.WrapperDescriptorType, + types.BuiltinFunctionType, + ), + ) + and name not in disallowed_tensor_methods + ): + s.add(method) + + # mlazos: these are functions which we handle specially in TensorVariable + s.add(torch.Tensor.__contains__) # type: ignore[arg-type] + s.add(torch.Tensor.register_hook) # type: ignore[arg-type] + return frozenset(s) + + +""" +Return if a torch object is ATen op or torch.Tensor method. +""" + + +def is_aten_op_or_tensor_method(obj: Any) -> bool: + return obj in get_tensor_method() or isinstance( + obj, + (torch._ops.OpOverloadPacket, torch._ops.OpOverload), + ) + + +class FunctionIdSet: + """ + Track a set of `id()`s of objects which are either allowed or not + allowed to go into the generated FX graph. Use to test for torch.*, + numpy.*, builtins.*, etc. + + Support user modification to permit customization of what can be + added to the graph and what will cause a graph break. + """ + + function_ids: set[int] | None = None + function_names: dict[int, str] | None = None + + def __init__( + self, lazy_initializer: Callable[[], dict[int, str] | set[int]] + ) -> None: + self.lazy_initializer = lazy_initializer + + def __call__(self) -> set[int]: + if self.function_ids is None: + value = self.lazy_initializer() + if isinstance(value, dict): + self.function_ids = set(value.keys()) + self.function_names = value + else: + assert isinstance(value, set) + self.function_ids = value + return self.function_ids + + def get_name(self, idx: int, default: str) -> str: + self() # lazy init + assert self.function_names is not None + return self.function_names.get(idx, default) + + def add(self, idx: int) -> None: + function_ids = self() # lazy init + function_ids.add(idx) + + def remove(self, idx: int) -> None: + function_ids = self() + if idx in function_ids: + function_ids.remove(idx) + + def __contains__(self, idx: int) -> bool: + return idx in self() + + +@FunctionIdSet +def _allowed_callable_ids() -> dict[int, str]: + rv: dict[int, str] = {} + return rv + + +@FunctionIdSet +def _leaf_function_ids() -> dict[int, str]: + rv: dict[int, str] = {} + return rv + + +@FunctionIdSet +def _disallowed_callable_ids() -> dict[int, str]: + rv: dict[int, str] = {} + return rv + + +@FunctionIdSet +def _nonstrict_trace_callable_ids() -> dict[int, str]: + rv: dict[int, str] = {} + return rv + + +@FunctionIdSet +def _builtin_function_ids() -> dict[int, str]: + # See also torch/_dynamo/polyfills/loader.py, which removes items in _builtin_function_ids + rv = { + id(v): f"builtins.{k}" + for k, v in builtins.__dict__.items() + if not k.startswith("_") and callable(v) + } + rv.update( + { + id(v): f"operator.{k}" + for k, v in operator.__dict__.items() + if not k.startswith("_") and callable(v) + } + ) + return rv + + +@FunctionIdSet +def _polyfilled_function_ids() -> set[int]: + # See also @torch._dynamo.decorators.substitute_in_graph(...), which adds items in _polyfilled_function_ids + return set() + + +@FunctionIdSet +def _numpy_function_ids() -> dict[int, str]: + unsupported_funcs = { + "seed", + "ranf", + "get_bit_generator", + "RandomState", + "set_bit_generator", + "sample", + } + + def is_supported(k: str, v: Any, mod: Any) -> bool: + if not callable(v): + return False + if not getattr(v, "__module__", None): + return True + if v.__module__ == mod.__name__: + return True + if ( + v.__module__ == "numpy.random.mtrand" + and mod.__name__ == "numpy.random" + and k not in unsupported_funcs + ): + return True + return False + + rv = {} + for mod in NP_SUPPORTED_MODULES: + for k, v in mod.__dict__.items(): + if is_supported(k, v, mod): + rv[id(v)] = f"{mod.__name__}.{k}" + return rv + + +@FunctionIdSet +def _builtin_constant_ids() -> dict[int, str]: + """ + Collects constant builtins by eliminating callable items. + """ + rv = { + id(v): f"builtins.{k}" + for k, v in builtins.__dict__.items() + if not k.startswith("_") and not callable(v) + } + return rv + + +_lazy_module_init: dict[str, list[Callable[[], None]]] = defaultdict(list) + + +def add_module_init_func(name: str, init_func: Callable[[], None]) -> None: + """Register a module without eagerly importing it""" + # If the module is already imported, eagerly run init + assert "." not in name, f"Expected a root module name, but got {name}" + assert name not in _lazy_module_init + _lazy_module_init[name].append(init_func) + + +def _maybe_init_lazy_module(obj: object) -> None: + module = getattr(obj, "__module__", None) + if module is None: + return + + base_module = module.split(".")[0] + init_funcs = _lazy_module_init.pop(base_module, None) + if init_funcs is not None: + for fn in init_funcs: + fn() + + +def is_callable_allowed(obj: Any) -> bool: + _maybe_init_lazy_module(obj) + return id(obj) in _allowed_callable_ids + + +def is_nonstrict_trace_callable(obj: Any) -> bool: + _maybe_init_lazy_module(obj) + return id(obj) in _nonstrict_trace_callable_ids + + +def is_leaf_function(obj: Any) -> bool: + _maybe_init_lazy_module(obj) + return id(obj) in _leaf_function_ids + + +def is_callable_disallowed(obj: Any) -> bool: + _maybe_init_lazy_module(obj) + return id(obj) in _disallowed_callable_ids + + +def is_forbidden(obj: Any) -> bool: + _maybe_init_lazy_module(obj) + return inspect.getattr_static(obj, "_dynamo_forbidden", False) + + +def is_builtin_callable(obj: Any) -> bool: + # See also torch/_dynamo/polyfills/loader.py, which removes items in _builtin_function_ids + return id(obj) in _builtin_function_ids + + +def is_builtin_constant(obj: Any) -> bool: + return id(obj) in _builtin_constant_ids + + +def is_polyfilled_callable(obj: Any) -> bool: + # See also @torch._dynamo.decorators.substitute_in_graph(...), which adds items in _polyfilled_function_ids + return id(obj) in _polyfilled_function_ids + + +def is_numpy(obj: Any) -> bool: + if np is None: + return False + return isinstance(obj, (np.ndarray, np.generic)) or id(obj) in _numpy_function_ids + + +def is_numpy_dtype(obj: Any) -> bool: + if np is None: + return False + return isinstance(obj, np.dtype) + + +def is_numpy_type_info(obj: Any) -> bool: + if np is None: + return False + return isinstance(obj, (np.finfo, np.iinfo)) + + +BUILTIN_SKIPLIST = ( + abc, + copy, + random, + linecache, +) + +# Builtin modules that should be skipped at the top-level (PEP 523 frame +# evaluation) but inlined when called from code dynamo is already tracing. +# For example, copy.deepcopy should be inlined when the user calls it inside +# a compiled function, but copy module frames should be skipped when they +# appear as top-level frames (e.g. called internally by autograd). +BUILTIN_INLINE_WHEN_CALLED: set[str] = set() + +# third party libraries skiplist is defined by str, because users may not use these libraries. +# we should use lazy import & skip in the future. +THIRDPARTY_SKIPLIST = ( + "fx2trt_oss", + "hypothesis", + "networkx", + "numpy", + "onnx", + "onnxruntime", + "onnx_tf", + "pandas", + "sklearn", + "tabulate", + "tensorflow", + "tensorrt", + "torch2trt", + "tqdm", + "tree", + "tvm", + "xarray", +) + + +def _as_posix_path(path: str) -> str: + posix_path = Path(os.path.normpath(path)).as_posix() + # os.path.normpath and pathlib.Path remove trailing slash, so we need to add it back + if path.endswith((os.path.sep, "/")): + posix_path += "/" + return posix_path + + +def _strip_init_py(s: str) -> str: + suffix = "__init__.py" + s = s.removesuffix(suffix) + return _as_posix_path(s) + + +def _module_dir(m: types.ModuleType) -> str | None: + # Protect against a module not exporting __file__ - this can happen for + # frozen modules, for example. + file = getattr(m, "__file__", None) + return file and _strip_init_py(file) + + +# These are legacy workarounds, don't add new modules to this list. +# Please use the MOD_INLINELIST instead to force inline functions under particular modules. +# +# NB: The only thing that is different about MOD_INLINELIST and LEGACY_MOD_INLINELIST +# is the behavior of a function f2 in the module when called by a function f1 +# in a module in MOD_SKIPLIST (see MOD_SKIPLIST for more details) +# +# LEGACY_MOD_INLINELIST is the same thing as Dynamo's behavior on a module that +# is not in any *_INLINELIST or *_SKIPLIST. +# That being said, we prefer people to add things to MOD_INLINELIST over +# LEGACY_MOD_INLINELIST because it is less likely to break existing tests. +LEGACY_MOD_INLINELIST = { + "torch._dynamo.external_utils", + "torch._export.db.examples", + "torch._export.wrappers", + "torch._functorch.apis", + "torch._functorch.deprecated", + "torch._library.fake_class_registry", + "torch.utils._typing_utils", + "torch.nn.attention.flex_attention", + "torch.ao.quantization.stubs", + "torch.ao.quantization.pt2e.export_utils", + "torch.ao.quantization.pt2e.qat_utils", + "torch.ao.quantization.pt2e.representation.rewrite", + "torch.ao.quantization.pt2e.utils", + "torch.ao.quantization.quantizer.xnnpack_quantizer", + "torch.export.unflatten", +} + +if torch.distributed.is_available(): + LEGACY_MOD_INLINELIST |= { + "torch.distributed.tensor._api", + "torch.distributed.tensor.device_mesh", + "torch.distributed.device_mesh", + "torch.distributed.algorithms._checkpoint.checkpoint_wrapper", + "torch.distributed.tensor.parallel._data_parallel_utils", + "torch.distributed.tensor.parallel._utils", + "torch.distributed.tensor.parallel.style", + # we have to add replicate to LEGACY_MOD_INLINELIST to ensure + # the forward_hook won't be ignored. + "torch.distributed._composable.replicate", + } + if not config.skip_fsdp_hooks: + LEGACY_MOD_INLINELIST.add("torch.distributed.fsdp._fully_shard") + +# Force inline functions under these modules, even they are in *_SKIPLIST. +# We are using python module name instead of file or directory object to avoid circular dependency. +# Please keep this sorted alphabetically. +# +# Btw, it is not "ideal" for something to be in MOD_INLINELIST. If Dynamo +# fully supports a module, then the ideal case is that it is not in +# any *_INLINELIST or *_SKIPLIST: then, the behavior of Dynamo is that +# it will always inline into functions in the module. +MOD_INLINELIST = [ + "torch._decomp", + "torch._dynamo._trace_wrapped_higher_order_op", + "torch._dynamo.compiled_autograd", + "torch._dynamo.comptime", + "torch._dynamo.polyfills", + "torch._dynamo.test_case", + "torch._export.non_strict_utils", + "torch._functorch._aot_autograd.subclass_parametrization", + "torch._functorch.autograd_function", + "torch._functorch.eager_transforms", + "torch._functorch.functional_call", + "torch._functorch.pyfunctorch", + "torch._functorch.vmap", + "torch._inductor.test_operators", + "torch._library.autograd", + "torch._library.custom_ops", + "torch._ops", + "torch._prims", + "torch._refs", + "torch._tensor", + "torch.amp.autocast_mode", + "torch.ao.nn", + "torch.autograd.function", + "torch.backends.cuda", + "torch.cuda.amp.autocast_mode", + "torch.distributions", + "torch.export._patches", + "torch.export._tree_utils", + "torch.export._unlift", + "torch.export._wrapper_utils", + "torch.fx._pytree", + "torch.fx._symbolic_trace", + "torch.fx.experimental.proxy_tensor", + "torch.fx.passes.shape_prop", + "torch.fx.traceback", + "torch.nn", + "torch.overrides", + "torch.random", + "torch.return_types", + "torch.sparse", + "torch.testing", + "torch.utils._content_store", + "torch.utils._contextlib", + "torch.utils._cxx_pytree", + "torch.utils._device", + "torch.utils._foreach_utils", + "torch.utils._ordered_set", + "torch.utils._python_dispatch", + "torch.utils._pytree", + "torch.utils.hooks", +] +assert sorted(set(MOD_INLINELIST)) == MOD_INLINELIST +MOD_INLINELIST = set(MOD_INLINELIST) + + +if torch.distributed.is_available(): + MOD_INLINELIST.add("torch.distributed") + if not config.skip_fsdp_hooks: + MOD_INLINELIST.add("torch.distributed.fsdp._fully_shard") + + +# By default, all functions under these modules are skipped. +# All the other knobs +# (torch_name_rule_map, MOD_INLINELIST, LEGACY_MOD_INLINELIST) +# take precedence over this list; e.g. if a function is in +# MOD_INLINELIST and MOD_SKIPLIST, then it will be inlined. +# See "A note on skip/inline rules" for more details. +# +# The skip is NOT recursive. If a function f1 in a module in MOD_SKIPLIST +# calls out to another function f2 in some other module, then Dynamo's +# behavior (skip/inline) depends on what we've marked f2 as: +# - if f2 is a function in a module in MOD_SKIPLIST, then we skip f2 +# - if f2 is a function in a module in MOD_INLINELIST, then we skip f2 +# - if f2 is a function in a module in LEGACY_MOD_INLINELIST, then we inline f2 +# - if f2 is a function in a module not in any *_LIST, then we inline f2 +MOD_SKIPLIST = [ + "torch._VF", + "torch.__future__", + "torch.__init__", + "torch._awaits", + "torch._classes", + "torch._compile", + "torch._custom_op", + "torch._custom_ops", + "torch._decomp", + "torch._dispatch", + "torch._dynamo", + "torch._export", + "torch._functorch", + "torch._guards", + "torch._higher_order_ops.effects", + "torch._higher_order_ops.torchbind", + "torch._higher_order_ops.wrap", + "torch._inductor", + "torch._jit_internal", + "torch._lazy", + "torch._library", + "torch._linalg_utils", + "torch._lobpcg", + "torch._logging", + "torch._lowrank", + "torch._meta_registrations", + "torch._namedtensor_internals", + "torch._numpy", + "torch._ops", + "torch._prims", + "torch._prims_common", + "torch._python_dispatcher", + "torch._refs", + "torch._strobelight", + "torch._subclasses", + "torch._tensor", + "torch._tensor_str", + "torch._thread_safe_fork", + "torch._utils", + "torch._utils_internal", + "torch._vmap_internals", + "torch._weights_only_unpickler", + "torch.accelerator", + "torch.amp", + "torch.ao", + "torch.autograd", + "torch.backends", + "torch.compiler", + "torch.contrib", + "torch.cpu", + "torch.cuda", + "torch.distributed", + "torch.distributions", + "torch.export", + "torch.fb", + "torch.fft", + "torch.functional", + "torch.futures", + "torch.fx", + "torch.hub", + "torch.jit", + "torch.library", + "torch.linalg", + "torch.masked", + "torch.monitor", + "torch.mps", + "torch.mtia", + "torch.multiprocessing", + "torch.nested", + "torch.nn", + "torch.onnx", + "torch.overrides", + "torch.package", + "torch.profiler", + "torch.quantization", + "torch.quasirandom", + "torch.random", + "torch.serialization", + "torch.signal", + "torch.sparse", + "torch.special", + "torch.storage", + "torch.testing", + "torch.types", + "torch.utils", + "torch.xpu", +] + +assert sorted(set(MOD_SKIPLIST)) == MOD_SKIPLIST +MOD_SKIPLIST = set(MOD_SKIPLIST) + + +@functools.cache +def get_legacy_mod_inlinelist() -> set[str]: + torch_dir = _module_dir(torch) + if torch_dir is None: + return set() + inlinelist = { + _as_posix_path(torch_dir + m[len("torch.") :].replace(".", "/")) + for m in LEGACY_MOD_INLINELIST + } + return inlinelist + + +@functools.cache +def get_mod_inlinelist() -> set[str]: + torch_dir = _module_dir(torch) + if torch_dir is None: + return set() + inlinelist = { + _as_posix_path(torch_dir + m[len("torch.") :].replace(".", "/")) + for m in MOD_INLINELIST + } + return inlinelist + + +@functools.cache +def get_mod_skiplist() -> set[str]: + torch_dir = _module_dir(torch) + if torch_dir is None: + return set() + skiplist = { + _as_posix_path(torch_dir + m[len("torch.") :].replace(".", "/")) + for m in MOD_SKIPLIST + } + return skiplist + + +# skip some standard python builtin libs +SKIP_DIRS = [ + "", + _as_posix_path(_config_module.__file__), + "triton/backends", +] +SKIP_DIRS.extend(map(_as_posix_path, filter(None, map(_module_dir, BUILTIN_SKIPLIST)))) + +BUILTIN_INLINE_WHEN_CALLED.update(filter(None, (_module_dir(copy),))) + +SKIP_DIRS_RE = re.compile(r"match nothing^") + +# Skip fbcode paths(including torch.package paths) containing +# one of the following strings. +FBCODE_SKIP_DIRS: set[str] = set() + +FBCODE_SKIP_DIRS_RE = re.compile(f".*({'|'.join(map(re.escape, FBCODE_SKIP_DIRS))})") + +# Remove this after fbcode is fully migrated to tracing through torchrec. +FBCODE_SKIP_TORCHREC_DIRS = { + "torchrec/distributed", + "torchrec/fb/distributed", + "caffe2/torch/fb/sparsenn/pooled_embeddings_modules.py", +} + +FBCODE_SKIP_TORCHREC_DIRS_RE = re.compile( + f".*({'|'.join(re.escape(_as_posix_path(d)) for d in FBCODE_SKIP_TORCHREC_DIRS)})" +) + +# TODO(yanboliang, anijain2305) - There are a few concerns that we should +# resolve +# 1) Audit if torchrec/distributed is even required in FBCODE_SKIPS_DIR +# 2) To inline just one file but skip others in a directory, we could use +# manual_torch_name_rule_map but this one is hard because FBCODE can add unusual +# names like torch_package. +# So, this is a stop gap solution till then. +FBCODE_INLINE_FILES_IN_SKIPPED_DIRS = { + "torchrec/distributed/types.py", +} +FBCODE_INLINE_FILES_IN_SKIPPED_DIRS_RE = re.compile( + f".*({'|'.join(re.escape(_as_posix_path(d)) for d in FBCODE_INLINE_FILES_IN_SKIPPED_DIRS)})" +) + +# torch.optim is a special case, +# we usually want to inline it, but the directory +# structure does not match the module structure +# and we want to skip the functions in optim/lr_scheduler.py +# this has precedence over all other rules in check_file +FORCE_SKIP_FILES = {f"{_module_dir(torch)}optim/lr_scheduler.py"} + + +def _recompile_re() -> None: + global SKIP_DIRS_RE + SKIP_DIRS_RE = re.compile( + rf"^[^\s<]*({'|'.join(re.escape(_as_posix_path(d)) for d in SKIP_DIRS)})" + ) + + +def add(import_name: str) -> None: + if isinstance(import_name, types.ModuleType): + return add(import_name.__name__) + assert isinstance(import_name, str) + from importlib.util import find_spec + + module_spec = find_spec(import_name) + if not module_spec: + return + origin = module_spec.origin + if origin is None: + return + SKIP_DIRS.append(_strip_init_py(origin)) + _recompile_re() + + +@dataclasses.dataclass +class SkipResult: + skipped: bool + reason: str | None + + +def check_file(filename: str | None, is_inlined_call: bool = False) -> SkipResult: + """Should skip this file?""" + if filename is None: + return SkipResult( + True, "cannot determine source file (likely a C extension or builtin)" + ) + filename = _as_posix_path(filename) + if filename in FORCE_SKIP_FILES: + return SkipResult(True, f"file is force-skipped ({filename})") + + for d in get_legacy_mod_inlinelist(): + if filename.startswith(d): + return SkipResult(False, f"file matches LEGACY_MOD_INLINELIST ({d})") + if is_inlined_call and is_torch_inline_allowed(filename): + return SkipResult(False, f"file matches MOD_INLINELIST ({filename})") + if is_inlined_call and any( + filename.startswith(d) for d in BUILTIN_INLINE_WHEN_CALLED + ): + return SkipResult( + False, f"file matches BUILTIN_INLINE_WHEN_CALLED ({filename})" + ) + if ( + is_fbcode() + and FBCODE_SKIP_DIRS + and bool(FBCODE_SKIP_DIRS_RE.match(filename)) + and not bool(FBCODE_INLINE_FILES_IN_SKIPPED_DIRS_RE.match(filename)) + ): + return SkipResult(True, "file matches FBCODE_SKIP_DIRS") + + if ( + is_fbcode() + and config.skip_torchrec + and FBCODE_SKIP_TORCHREC_DIRS + and bool(FBCODE_SKIP_TORCHREC_DIRS_RE.match(filename)) + and not bool(FBCODE_INLINE_FILES_IN_SKIPPED_DIRS_RE.match(filename)) + ): + return SkipResult(True, "file matches FBCODE_SKIP_TORCHREC_DIRS") + + unittest_dir = _module_dir(unittest) + if ( + unittest_dir is not None + and filename.startswith(unittest_dir) + and not torch._dynamo.config.enable_trace_unittest + ): + return SkipResult(True, "file is in unittest directory") + + if bool(SKIP_DIRS_RE.match(filename)): + matched_dir = next((d for d in SKIP_DIRS if filename.startswith(d)), filename) + return SkipResult(True, f"file is under skip directory ({matched_dir})") + + for d in get_mod_skiplist(): + if filename.startswith(d): + return SkipResult(True, f"file matches MOD_SKIPLIST ({d})") + return SkipResult(False, "inlined by default") + + +@dataclasses.dataclass +class FunctionInfo: + py_obj: object | None + name: str | None + filename: str + code: types.CodeType | None + + +""" +This is the main entry point to determine whether an object (function) should be inlined or skipped. +Let's illustrate the logic with an example: + @torch.compile + def f1(x, y): + ...... + f2(x, y) + ...... + + def f2(x, y): + ...... + f3(x, y) + ...... + + def f3(x, y): + ...... + +There are mainly three call sites of check/check_verbose: +* The compile region entrance (like function f1), the corresponding code is located at eval_frame.py. +* When tracing the recursively called functions (like function f2 and f3). + * Dynamo decides inline/skip every time it encounters a new recursively function call, and the call site + is in InliningInstructionTranslator.check_inlineable of symbolic_convert.py. + * If f2 is skipped by Dynamo, when evaluating the frame of f3, Dynamo need the inline/skip check again + and the call site is in catch_errors_wrapper.catch_errors of convert_frame.py. +* For global variables and function arguments, Dynamo needs to decide if they are wrapped as SkipFunctionVariable in builder.py. + +`is_inlined_call` is used to indicate if the current function call is inlined (f2 is inlined call if it passes check) +or not (f3 is not inlined call if f2 is skipped). Inside of the `check_verbose` function, there are more rules +to be checked if this `is_inlined_call`. +The reason to have this flag is that if the upper level function call (e.g, f2) is skipped, +we don't want to inline the lower level function call (e.g, f3) by default. +""" + +_force_inline_flag = False + + +# pyrefly: ignore [deprecated] +@contextlib.contextmanager +def _force_inline() -> Iterator[None]: + """ + A context manager used within the dynamo codebase that forces a function + and nested function calls to be inlined during dynamo tracing. + + When active, check_verbose() will skip all inline/skip decision logic and + always return SkipResult(False, ...), meaning functions will be inlined. + + See _make_inlined() in utils.py which uses this to ensure that + a python function is fully traced to produce the needed variable trackers. + """ + global _force_inline_flag + old_val = _force_inline_flag + try: + _force_inline_flag = True + yield + finally: + _force_inline_flag = old_val + + +def check_verbose( + obj: Any, is_inlined_call: bool = False, frame: Any | None = None +) -> SkipResult: + if _force_inline_flag: + return SkipResult( + False, + "don't skip because we're inside _force_inline() context", + ) + + # For eval frame callback (not inlined calls), allow tracing inbuilt + # nn.Module.forward methods when the module has hooks. Any hook can cause + # a graph break (via @torch._dynamo.disable, print, unsupported ops, etc.), + # which skips the entire _call_impl frame. By allowing forward to be traced, + # Dynamo can capture the module's operations in a new graph after the break. + if ( + not is_inlined_call + and frame is not None + and isinstance(obj, types.CodeType) + and obj.co_name == "forward" + ): + from .utils import nnmodule_has_hooks + + module = frame.f_locals.get("self") + if ( + module is not None + and isinstance(module, torch.nn.Module) + and module.__class__.__module__.startswith(("torch.nn.", "torch.ao.")) + and nnmodule_has_hooks(module, check_forward_hooks=True) + ): + return SkipResult( + False, + "inbuilt nn.Module.forward allowed - module has hooks", + ) + + if isinstance( + obj, + ( + UserFunctionVariable, + UserMethodVariable, + NestedUserFunctionVariable, + LocalGeneratorFunctionVariable, + LocalGeneratorObjectVariable, + ), + ): + try: + py_obj = obj.get_function() + except NotImplementedError: + py_obj = None + fi = FunctionInfo(py_obj, obj.get_name(), obj.get_filename(), obj.get_code()) + elif isinstance(obj, types.CodeType): + fi = FunctionInfo(None, obj.co_name, obj.co_filename, obj) + elif isinstance(obj, (types.FunctionType, types.MethodType)): + filename = getfile(obj) + assert filename is not None + fi = FunctionInfo( + obj, + obj.__name__, + filename, + obj.__code__, # type: ignore[union-attr] # FIXME Add MethodType.__code__ to typeshed + ) + else: + filename = getfile(obj) + assert filename is not None + fi = FunctionInfo(obj, None, filename, None) + + # typing.cast is a polyfilled no-op, but unlike C builtins it has a code + # object that PEP 523 can intercept as a standalone frame after a graph + # break. Skip it at the top level to avoid installing unnecessary guards. + if fi.code is not None and fi.code is typing.cast.__code__: + return SkipResult(True, "typing.cast is a no-op, skip at top level") + + # Consulte the central trace rules defined in torch._dynamo.trace_rules. + reasons: set[str] = set() + rule = lookup_inner(fi.py_obj, fi.name, fi.filename, is_inlined_call, reasons) + assert rule is not None + if issubclass( + rule, + ( + UserFunctionVariable, + LocalGeneratorFunctionVariable, + PolyfilledFunctionVariable, + ), + ): + return SkipResult(False, reasons.pop()) + elif issubclass(rule, TorchInGraphFunctionVariable): + return SkipResult(False, reasons.pop()) + else: + assert rule == SkipFunctionVariable, rule + return SkipResult(True, reasons.pop()) + + +def check(obj: Any, is_inlined_call: bool = False, frame: Any | None = None) -> bool: + return check_verbose(obj, is_inlined_call, frame).skipped + + +def get_skip_reason(obj: Any) -> str: + """Compute a descriptive skip reason for a callable. Only called on graph break.""" + if is_callable_disallowed(obj): + return _disallowed_callable_ids.get_name(id(obj), repr(obj)) + + filename = getfile(obj) + if filename is not None: + skip_result = check_file(filename) + if skip_result.reason is not None: + return skip_result.reason + + module = getattr(obj, "__module__", None) or "" + return ( + f"cannot determine source file for {module} (likely a C extension or builtin)" + ) + + +# skip common third party libs +for _name in THIRDPARTY_SKIPLIST: + add(_name) + +_recompile_re() + + +def is_torch_inline_allowed(filename: str) -> bool: + return any(filename.startswith(d) for d in get_mod_inlinelist()) + + +@functools.cache +def dynamo_dir() -> str | None: + import torch._dynamo + + return _module_dir(torch._dynamo) + + +def is_torch(filename: str) -> bool: + dynamo_path = dynamo_dir() + if dynamo_path is not None and filename.startswith(dynamo_path): + return False + torch_path = _module_dir(torch) + return torch_path is not None and filename.startswith(torch_path) + + +""" +Main entry point for looking up the trace rule (the Dynamo variable) for a given callable object. +""" + +BUILTIN_CALLABLES = { + dict: DictBuiltinVariable, + iter: IterBuiltinVariable, + list: ListBuiltinVariable, +} + + +def lookup_callable(obj: Callable[..., Any]) -> type[VariableTracker] | None: + if not hashable(obj): + return None + # Custom allow/disallow in graph takes precedence over the general lookup. + if is_callable_disallowed(obj): + return SkipFunctionVariable + if is_callable_allowed(obj): + return TorchInGraphFunctionVariable + if is_polyfilled_callable(obj): + return PolyfilledFunctionVariable + if obj in BUILTIN_CALLABLES: + return BUILTIN_CALLABLES[obj] + if is_builtin_callable(obj): + return BuiltinVariable + return None + + +""" +Main entry point for looking up the trace rule (the Dynamo variable) for a given function object. +E.g, the lookup result of `torch.sin` is `TorchInGraphFunctionVariable`. +""" + + +def lookup(obj: Any) -> type[VariableTracker] | None: + return lookup_inner(obj) + + +# also takes config.dont_skip_tracing into account +def lookup_inner( + obj: Any, + name: str | None = None, + filename: str | None = None, + is_direct_call: bool = True, + reasons: None | set[str] = None, +) -> type[VariableTracker] | None: + result = _lookup_inner( + obj, + name=name, + filename=filename, + is_direct_call=is_direct_call, + reasons=reasons, + ) + # There are still some modules we should absolutely NOT trace into - e.g. most of torch._dynamo, + # as this can result in really weird tracing behaviors. + # Note that if a torch._dynamo function is already not skipped (e.g. functions in external_utils.py), + # then this branch does not apply. + if config.dont_skip_tracing and result is SkipFunctionVariable: + if filename is None: + filename = getfile(obj) + assert filename is not None + filename = _as_posix_path(filename) + torch_dir = _module_dir(torch) + if torch_dir is not None: + dynamo_path = _as_posix_path(torch_dir) + "_dynamo" + if filename.startswith(dynamo_path) and not filename.endswith( + "test_dont_skip_tracing_functions.py" + ): + return SkipFunctionVariable + if reasons is not None: + reasons.add( + "Attempted skip but we are ignoring skips due to torch._dynamo.config.dont_skip_tracing" + ) + return UserFunctionVariable + return result + + +def _lookup_inner( + obj: Any, + name: str | None = None, + filename: str | None = None, + is_direct_call: bool = True, + reasons: set[str] | None = None, +) -> type[VariableTracker] | None: + # Step 1: lookup obj's tracing rule in `torch_name_rule_map`. + # The rules defined in `torch_name_rule_map` mainly includes two parts: + # - Manually defined rules for any functions. + # - The list of torch in graph functions. + try: + can_hash = hashable(obj) + except Exception: + can_hash = False + if not can_hash: + if reasons is not None: + reasons.add("obj is not hashable") + return None + if obj is not None: + if is_aten_op_or_tensor_method(obj): + return TorchInGraphFunctionVariable + rule = get_torch_obj_rule_map().get(obj, None) + if rule is not None: + if reasons is not None: + reasons.add("get_torch_obj_rule_map") + return rule + elif name is not None and filename is not None and not is_direct_call: + if name.startswith(TORCH_DYNAMO_RESUME_IN_PREFIX): + rule = get_torch_obj_rule_map().get( + filename + "#" + TORCH_DYNAMO_RESUME_IN_PREFIX, None + ) + else: + rule = get_torch_obj_rule_map().get(filename + "#" + name, None) + if rule is not None: + if reasons is not None: + reasons.add("get_torch_obj_rule_map") + return rule + elif name == "": + if reasons is not None: + reasons.add("inlining frame from list comprehension") + return UserFunctionVariable + + # Step 2: lookup obj's tracing rule by function name. + if is_direct_call: + if name == "patched_init": + if reasons is not None: + reasons.add("func name is patched_init") + return SkipFunctionVariable + elif name == "__torch_function__" or ( + obj and getattr(obj, "__name__", None) == "__torch_function__" + ): + if reasons is not None: + reasons.add("func name is __torch_function__") + return UserFunctionVariable + + if not is_direct_call: + if name == "__getattr__": + # is_direct_call = False indicates that this is the top-level frame + # being traced (i.e., it is not inlined and not called from + # InliningInstructionTranslator). Tracing __getattr__ at the top + # level is unlikely because we inline it for + # UserDefinedObjectVariable. This scenario occurs only for + # UnspecializedNNModuleVariable, where Dynamo directly calls + # __getattr__ during trace time, generating LOAD_ATTR bytecode + # without going through the underlying __getattr__ data structures. + # When this optimized bytecode is executed, Dynamo is triggered + # again on the __getattr__ call. Therefore, we skip Dynamo tracing + # in this case. + if reasons is not None: + reasons.add( + "Tracing __getattr__ as the top level frame, unsuitable for tracing." + ) + return SkipFunctionVariable + + # Step 3: lookup obj's tracing rule by filename. + if filename is None: + filename = getfile(obj) + + skip_result = check_file(filename, is_direct_call) + if reasons is not None and skip_result.reason is not None: + reasons.add(skip_result.reason) + if skip_result.skipped: + return SkipFunctionVariable + else: + return UserFunctionVariable + + +def clear_lru_cache() -> None: + torch._dynamo.trace_rules.get_torch_obj_rule_map.cache_clear() + torch._dynamo.trace_rules.get_tensor_method.cache_clear() + torch._dynamo.trace_rules.get_legacy_mod_inlinelist.cache_clear() + torch._dynamo.trace_rules.get_mod_inlinelist.cache_clear() + torch._dynamo.trace_rules.dynamo_dir.cache_clear() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/types.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/types.py new file mode 100644 index 0000000000000000000000000000000000000000..bbf76c0ee055038aad9063aa81792ee094b2a82a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/types.py @@ -0,0 +1,140 @@ +"""This module contains the core type definitions and protocols used throughout Dynamo. + +The types defined here fall into several categories: +- Guard related types (GuardFn, GuardFail, GuardedCode): Used for tracking and managing guards that protect compiled code +- Frame and cache types (FrameState, CacheEntry): Used for managing interpreter frame state and caching +- Callback protocols (DynamoCallbackFn): Define the interface for frame evaluation callbacks +- Hook protocols (DynamoGuardHook, ProfilerStartHook, ProfilerEndHook, BytecodeHook): Define various hook points for + instrumentation and customization + +These types provide the foundational interfaces that enable Dynamo's dynamic compilation and optimization system, +ensuring type safety and clear contracts between different components of the system. +""" + +import dataclasses +import types +from collections.abc import Callable +from typing import Any, NamedTuple, Protocol + +# CacheEntry has a `guard_manager` field for the guard, and a `code` field for the code object. +from torch._C._dynamo.eval_frame import ( + _CacheEntry as CacheEntry, + _ExtraState as ExtraState, + _FrameAction as FrameAction, + _FrameExecStrategy as FrameExecStrategy, + _PyInterpreterFrame as DynamoFrameType, +) +from torch._guards import CompileId, Guard + + +# We use a dict to store additional data per frame. +FrameState = dict[Any, Any] + + +class GuardFail(NamedTuple): + # A string repr of the piece of failed guard code we eval-ed + reason: str + # A code object where we failed a guard + orig_code: types.CodeType + + +@dataclasses.dataclass(frozen=True) +class GuardFilterEntry: + name: str + has_value: bool + value: object + guard_type: str + derived_guard_types: tuple[str, ...] + is_global: bool + orig_guard: Guard + + +class GuardFn(Protocol): + closure_vars: dict[str, object] + args: list[str] + code_parts: list[str] + verbose_code_parts: list[str] + global_scope: dict[str, object] + guard_fail_fn: Callable[[GuardFail], None] | None + cache_entry: CacheEntry | None + extra_state: ExtraState | None + + # maps locals of user function to bool + def __call__(self, f_locals: dict[str, object]) -> bool: ... + + +@dataclasses.dataclass +class GuardedCode: + code: types.CodeType + guard_manager: GuardFn + compile_id: CompileId + trace_annotation: str = "Unknown" + + +@dataclasses.dataclass +class ConvertFrameReturn: + # default return is no compiled code (i.e. `return None`): + # strategy is to skip non-recursively, for all future intercepted frames too + + # eval frame execution strategy for this frame + frame_exec_strategy: FrameExecStrategy = dataclasses.field( + default_factory=lambda: FrameExecStrategy(FrameAction.SKIP, FrameAction.DEFAULT) + ) + # also apply frame_exec strategy to future frames with same code + apply_to_code: bool = True + guarded_code: GuardedCode | None = None + + +def wrap_guarded_code(guarded_code: GuardedCode) -> ConvertFrameReturn: + return ConvertFrameReturn( + frame_exec_strategy=FrameExecStrategy(FrameAction.DEFAULT, FrameAction.DEFAULT), + guarded_code=guarded_code, + ) + + +class DynamoCallbackFn(Protocol): + def __call__( + self, + frame: DynamoFrameType, + cache_entry: CacheEntry | None, + frame_state: FrameState, + ) -> ConvertFrameReturn: ... + + +DynamoCallback = DynamoCallbackFn | None | bool + + +class DynamoGuardHook(Protocol): + def __call__( + self, + guard_manager: GuardFn, + code: types.CodeType, + f_locals: dict[str, object], + index: int, + last: bool, + ) -> None: ... + + +class DynamoGuardCompleteHook(Protocol): + def __call__( + self, + cache_hit: bool, + ) -> bool: ... + + +class ProfilerStartHook(Protocol): + def __call__( + self, + name: str, + # TODO(whc) how do I annotate a _RecordFunction here? + ) -> Any: ... + + +class ProfilerEndHook(Protocol): + def __call__(self, record: Any) -> None: ... + + +class BytecodeHook(Protocol): + def __call__( + self, code: types.CodeType, new_code: types.CodeType + ) -> types.CodeType | None: ... diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..366a5a9e6b6ceb3daa0415fe341a6c9f2a371197 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/utils.py @@ -0,0 +1,5381 @@ +""" +Utility functions and classes used throughout the TorchDynamo system. + +This module contains a collection of helper utilities used by various parts of Dynamo for: +- Performance metrics collection and reporting +- Compilation timing and debugging +- Graph manipulation and tensor operations +- Runtime guards and checks +- Common data structure operations +- Testing and development tools + +This is an internal module that provides shared functionality used across the Dynamo codebase. +""" + +from __future__ import annotations + +import atexit +import collections +import contextlib +import copy +import dataclasses +import datetime +import dis +import enum +import functools +import gc +import importlib +import inspect +import itertools +import json +import linecache +import logging +import math +import operator +import os +import re +import sys +import textwrap +import threading +import time +import traceback +import types +import typing +import uuid +import warnings +import weakref +from collections import Counter, OrderedDict +from contextlib import AbstractContextManager, contextmanager +from dataclasses import is_dataclass +from functools import lru_cache +from types import CodeType, MethodWrapperType +from typing import ( + Any, + cast, + ClassVar, + Generic, + Literal, + NoReturn, + overload, + TypeAlias, + TypeGuard, + TypeVar, +) +from typing_extensions import ParamSpec, TypeIs + +import torch +import torch._functorch.config +import torch.fx.experimental.symbolic_shapes +import torch.utils._pytree as pytree +from torch import fx +from torch._C import ( + _instruction_counter, + _len_torch_function_stack, + _pop_torch_function_stack, + _push_on_torch_function_stack, +) +from torch._dispatch.python import enable_python_dispatcher +from torch._dynamo.metrics_context import MetricsContext, RuntimeMetricsContext +from torch._guards import CompileId, Source, TracingContext +from torch._subclasses.meta_utils import is_sparse_compressed +from torch._utils_internal import ( + justknobs_check, + log_chromium_event_internal, + log_compilation_event, + record_chromium_event_internal, + signpost_event, +) +from torch.fx._utils import _format_graph_code, lazy_format_graph_code +from torch.monitor import _WaitCounter +from torch.nn.modules.lazy import LazyModuleMixin +from torch.utils._ordered_set import OrderedSet +from torch.utils._python_dispatch import is_traceable_wrapper_subclass +from torch.utils._triton import has_triton, has_triton_package +from torch.utils.hooks import RemovableHandle + +from .graph_utils import _get_flat_args + + +if typing.TYPE_CHECKING: + from collections.abc import ( + Callable, + Container, + Generator, + ItemsView, + Iterable, + Iterator, + KeysView, + Mapping, + Sequence, + ValuesView, + ) + + from torch._dynamo.bytecode_transformation import Instruction + from torch._dynamo.replay_record import ExecutionRecord + from torch._dynamo.symbolic_convert import ( + InstructionTranslator, + InstructionTranslatorBase, + ) + from torch._dynamo.variables.base import VariableTracker + from torch._prims_common import DeviceLikeType + from torch._subclasses import FakeTensorMode + + +try: + import numpy as np +except ModuleNotFoundError: + np = None # type: ignore[assignment] + +try: + import torch._logging + import torch._numpy as tnp + from torch._guards import detect_fake_mode # noqa: F401 + from torch._logging import LazyString + + from . import config + + # NOTE: Make sure `NP_SUPPORTED_MODULES` and `NP_TO_TNP_MODULE` are in sync. + if np: + NP_SUPPORTED_MODULES: tuple[types.ModuleType, ...] = ( + np, + np.fft, + np.linalg, + np.random, + ) + + NP_TO_TNP_MODULE = { + np: tnp, + np.fft: tnp.fft, + np.linalg: tnp.linalg, + np.random: tnp.random, + } + else: + NP_SUPPORTED_MODULES = () + + # pyrefly: ignore [implicit-any] + NP_TO_TNP_MODULE = {} + from torch._subclasses.fake_tensor import FakeTensor, is_fake, maybe_get_fake_mode +except ImportError: + pass + + +T = TypeVar("T") +T1 = TypeVar("T1") +T2 = TypeVar("T2") +T3 = TypeVar("T3") +T4 = TypeVar("T4") +T5 = TypeVar("T5") +T6 = TypeVar("T6") +T7 = TypeVar("T7") +T8 = TypeVar("T8") +T9 = TypeVar("T9") +T10 = TypeVar("T10") +R = TypeVar("R") +_P = ParamSpec("_P") + +unpatched_nn_module_getattr = torch.nn.Module.__getattr__ +unpatched_nn_module_call = torch.nn.Module.__call__ +unpatched_nn_module_call_impl = torch.nn.Module._call_impl + +counters: collections.defaultdict[str, Counter[str]] = collections.defaultdict( + collections.Counter +) +optimus_scuba_log: dict[str, Any] = {} +troubleshooting_url = "https://docs.pytorch.org/docs/main/user_guide/torch_compiler/compile/programming_model.recompilation.html" +nnmodule_doc_url = "https://docs.pytorch.org/docs/main/user_guide/torch_compiler/torch.compiler_nn_module.html" +nnmodule_doc_url_msg = f"See {nnmodule_doc_url} for more information and limitations." +log = logging.getLogger(__name__) + +# profiling compilation time by function +compilation_time_metrics: dict[str, list[float]] = {} + +# This supports calculate_time_spent(), which reports cumulative times +# across the process for any "phase" populated by dynamo_timed. Reset if +# reset_frame_count() is called. +cumulative_time_spent_ns: dict[str, float] = collections.defaultdict(float) + +timer_counter = itertools.count() + + +# Abstraction on top of counters. +class ReInplaceTrigger(enum.Enum): + AUTO_FUNC_V1 = 1 + AUTO_FUNC_V2 = 2 + TRITON_OPS = 3 + + +class ReinplaceCounters: + _values: collections.defaultdict[str, int] = collections.defaultdict(int) + + # Track sizes of known not re-inplaced tensors (exclude dynamic shapes). + @classmethod + def add_missed_bytes(cls, trigger: ReInplaceTrigger, bytes: int) -> None: + if bytes != 0: + cls._values[f"missed_bytes_{trigger.name}"] += bytes + + # Track number of not re-inplaced tensors. + @classmethod + def add_missed_opportunities(cls, trigger: ReInplaceTrigger, count: int) -> None: + if count != 0: + cls._values[f"missed_tensors_{trigger.name}"] += count + + @classmethod + def clear(cls) -> None: + cls._values.clear() + + @classmethod + def get_total_missed(cls) -> int: + sum = 0 + for trigger in ReInplaceTrigger: + sum += cls._values.get(f"missed_tensors_{trigger.name}", 0) + return sum + + @classmethod + def get_total_missed_bytes(cls) -> int: + sum = 0 + for trigger in ReInplaceTrigger: + sum += cls._values.get(f"missed_bytes_{trigger.name}", 0) + return sum + + @classmethod + def log(cls) -> None: + # if not empty log. + if cls._values: + signpost_event("inductor", "reinplace_counters", cls._values) + + +def tabulate( + rows: list[tuple[str, Any]] | list[list[Any]], + headers: tuple[str, ...] | list[str], +) -> str: + try: + import tabulate + + return tabulate.tabulate(rows, headers=headers) + except ImportError: + return "\n".join( + ", ".join(map(str, row)) for row in itertools.chain([headers], rows) + ) + + +curr_frame = 0 + + +# Note: Called for you by dynamo - you almost never ever want to invoke this yourself. +def increment_frame() -> None: + global curr_frame + curr_frame = curr_frame + 1 + + +# Note: Called for you by dynamo - you almost never ever want to invoke this yourself. +def reset_frame_count() -> None: + global curr_frame + cumulative_time_spent_ns.clear() + compilation_time_metrics.clear() + curr_frame = 0 + + +_recompile_user_contexts: list[Callable[[], str]] | None = None + + +def register_hook_for_recompile_user_context(hook: Callable[[], str]) -> None: + """ + Register a hook to be called when a recompile is triggered. The hook + should return a string describing user contexts that are not available + to the compiler, such as the current training epoch. This is useful for + debugging and data analysis for recompile. For data retention purposes, + the user context string is capped at 256 characters. + """ + global _recompile_user_contexts + if _recompile_user_contexts is None: + _recompile_user_contexts = [] + _recompile_user_contexts.append(hook) + + +def get_hook_for_recompile_user_context() -> list[Callable[[], str]] | None: + return _recompile_user_contexts + + +def reset_recompile_user_contexts() -> None: + """Clear any registered recompile user-context hooks (test helper).""" + global _recompile_user_contexts + _recompile_user_contexts = None + + +op_count = 0 + + +def increment_op_count(cnt: int) -> None: + global op_count + op_count += cnt + + +# Get the total time in seconds for each "phase" +# For example, {'entire_frame_compile':8.574629999999999, 'backend_compile':5.26806} +def calculate_time_spent() -> dict[str, float]: + total_by_key = {} + for phase, timing in cumulative_time_spent_ns.items(): + total_by_key[phase] = timing / 1e9 + + total_by_key["total_wall_time"] = total_by_key.get( + "entire_frame_compile", 0 + ) + total_by_key.get("entire_backward_compile", 0) + return total_by_key + + +# Print a report of time spent so far +# Ex: +# TIMING: +# entire_frame_compile:8.574629999999999 +# backend_compile:5.26806 +def print_time_report() -> None: + total_by_key = calculate_time_spent() + + out = "TIMING:" + for key, value in total_by_key.items(): + out = f"{out} {key}:{round(value, 5)}" + + print(out) + + +# Use the following singleton to capture and log CompilationMetrics. Entering the context +# manager allocates a new record to be logged when it exits. (You should not need to use +# this directly unless you introduce a new code path where compilation metrics would be +# gathered). While compiling, use the setters or timer in MetricsContext to update fields +# in the current context. For example: +# +# To set a single field once (use overwrite=True to overwrite): +# get_metrics_context().set("metric_name", value) +# +# To set multiple fields at once (use overwrite=True to overwrite): +# get_metrics_context().update({"name1": val1, "name2": val2}) +# +# To increment an integer field: +# get_metrics_context().increment("metric_name", value) +# +# To record execution time, MetricsContext works with dynamo_timed: +# def foo(...): +# # Updates the "metric_us" field. +# with dynamo_timed("metric", dynamo_compile_column_us="metric_us") +# ... +# +_metrics_context_tls = threading.local() + + +def get_metrics_context() -> MetricsContext: + if not hasattr(_metrics_context_tls, "metrics_context"): + _metrics_context_tls.metrics_context = MetricsContext( + on_exit=record_compilation_metrics + ) + return _metrics_context_tls.metrics_context + + +def get_runtime_metrics_context() -> RuntimeMetricsContext: + if not hasattr(_metrics_context_tls, "runtime_metrics_context"): + _metrics_context_tls.runtime_metrics_context = RuntimeMetricsContext( + on_exit=record_compilation_metrics + ) + return _metrics_context_tls.runtime_metrics_context + + +class CompileEventLogLevel(enum.Enum): + """ + Enum that loosely corresponds with a "log level" of a given event. + + CHROMIUM_EVENT: Logs only to tlparse. + COMPILE_EVENT: Logs to tlparse + PT2 Compile Events + COMPILATION_METRIC: Logs to tlparse, PT2 Compile Events, and dynamo_compile + """ + + CHROMIUM = 1 + PT2_COMPILE = 2 + COMPILATION_METRIC = 3 + + +class CompileEventLogger: + """ + Helper class for representing adding metadata(i.e. columns) to various compile events. + Use CompileEventLogger to add event data to: + - Chromium events + - PT2 Compile Events + - CompilationMetrics + + This should be used in conjunction with dynamo_timed() and metrics contexts, which create + timed spans and events. CompileEventLogger uses three log levels (described in CompileEventLogLevel), + where each log level logs to all sources below it in the hierarchy. + + Example usages: + - I want to log to an existing chromium event within dynamo timed: + with dynamo_timed("my_event"): + CompileEventLogger.chromium("my_event", foo=bar) + + - I want to log my event to both chromium + pt2_compile_events: + with dynamo_timed("my_event", log_pt2_compile_event=True): + CompileEventLogger.pt2_compile("my_event", foo=bar) + + - I want to add information to dynamo events and dynamo_compile + CompileEventLogger.compilation_metric(foo=bar) + """ + + @staticmethod + def log_instant_event( + event_name: str, + metadata: dict[str, Any], + time_ns: int | None = None, + log_level: CompileEventLogLevel = CompileEventLogLevel.CHROMIUM, + ) -> None: + if time_ns is None: + time_ns = time.time_ns() + chromium_log = get_chromium_event_logger() + if log_level == CompileEventLogLevel.CHROMIUM: + log_pt2_compile_event = False + elif log_level == CompileEventLogLevel.PT2_COMPILE: + log_pt2_compile_event = True + else: + raise RuntimeError( + "Cannot log instant event at COMPILATION_METRIC level. Please choose one of CHROMIUM_EVENT or COMPILE_EVENT" + ) + chromium_log.log_instant_event( + event_name, time_ns, metadata, log_pt2_compile_event + ) + + @staticmethod + def add_data( + event_name: str, + log_level: CompileEventLogLevel, + overwrite: bool = False, + **metadata: object, + ) -> None: + """ + Centralized API for adding data to various events + Log an event to a toplevel "dynamo" event or metrics context + depending on log level. + """ + chromium_log = get_chromium_event_logger() + pt2_compile_substack = chromium_log.get_pt2_compile_substack() + + if log_level == CompileEventLogLevel.CHROMIUM: + chromium_log.add_event_data(event_name, **metadata) + elif log_level == CompileEventLogLevel.PT2_COMPILE: + pt2_compile_substack = chromium_log.get_pt2_compile_substack() + if event_name not in pt2_compile_substack: + raise RuntimeError( + "Error: specified log level PT2_COMPILE, but the event %s" + " is not logged to pt2_compile_events. Make sure the event is active and you passed " + "log_pt2_compile_event=True to dynamo_timed", + event_name, + ) + chromium_log.add_event_data(event_name, **metadata) + else: + assert log_level == CompileEventLogLevel.COMPILATION_METRIC + top_event = chromium_log.get_outermost_event() + + if event_name != top_event: + raise RuntimeError( + "Log level is COMPILATION_METRIC, but event_name isn't the toplevel event. " + "CompilationMetrics must be logged to the toplevel event. Consider using `log_toplevel_event_data` directly." + ) + metrics_context = get_metrics_context() + if not metrics_context.in_progress(): + raise RuntimeError( + "No metrics context is in progress. Please only call this function within a metrics context." + ) + + # TODO: should we assert that the keys of metadata are in CompilationMetrics? + metrics_context.update(metadata, overwrite) + chromium_log.add_event_data(event_name, **metadata) + + @staticmethod + def add_toplevel( + log_level: CompileEventLogLevel, overwrite: bool = False, **metadata: object + ) -> None: + """ + Syntactic sugar for logging to the toplevel event + """ + top_event = get_chromium_event_logger().get_outermost_event() + if top_event is None: + raise RuntimeError( + "No toplevel event active. Please only call this function within a dynamo_timed context." + ) + CompileEventLogger.add_data(top_event, log_level, overwrite, **metadata) + + @staticmethod + def increment( + event_name: str, log_level: CompileEventLogLevel, key: str, value: int + ) -> None: + """ + Increments an existing field, or adds it + """ + chromium_log = get_chromium_event_logger() + if ( + log_level == CompileEventLogLevel.CHROMIUM + or log_level == CompileEventLogLevel.PT2_COMPILE + ): + chromium_log.increment(event_name, key, value) + else: + assert log_level == CompileEventLogLevel.COMPILATION_METRIC + top_event = chromium_log.get_outermost_event() + if event_name != top_event: + raise RuntimeError( + "Log level is COMPILATION_METRIC, but event_name isn't the toplevel event. " + "CompilationMetrics must be logged to the toplevel event. Consider using `increment_toplevel` directly." + ) + + metrics_context = get_metrics_context() + if not metrics_context.in_progress(): + raise RuntimeError( + "No metrics context is in progress. Please only call this function within a metrics context/dynamo_timed." + ) + + metrics_context.increment(key, value) + chromium_log.increment(event_name, key, value) + + @staticmethod + def increment_toplevel( + key: str, + value: int = 1, + log_level: CompileEventLogLevel = CompileEventLogLevel.COMPILATION_METRIC, + ) -> None: + """ + Increments a value on the toplevel metric. By default, logs to metric. + """ + chromium_log = get_chromium_event_logger() + top_event = chromium_log.get_outermost_event() + if top_event is None: + raise RuntimeError( + "No toplevel event active. Please only call this function within a metrics context/dynamo_timed." + ) + CompileEventLogger.increment(top_event, log_level, key, value) + + @staticmethod + def add_to_set( + event_name: str, log_level: CompileEventLogLevel, key: str, value: Any + ) -> None: + """ + Add metadata to a set of values with key . Creates a set if it doesn't exist. + """ + chromium_log = get_chromium_event_logger() + if ( + log_level == CompileEventLogLevel.CHROMIUM + or log_level == CompileEventLogLevel.PT2_COMPILE + ): + chromium_log.add_to_set(event_name, key, value) + else: + assert log_level == CompileEventLogLevel.COMPILATION_METRIC + top_event = chromium_log.get_outermost_event() + if event_name != top_event: + raise RuntimeError( + "Log level is COMPILATION_METRIC, but event_name isn't the toplevel event. " + "CompilationMetrics must be logged to the toplevel event. Consider using `add_to_set_metric` directly." + ) + + metrics_context = get_metrics_context() + if not metrics_context.in_progress(): + raise RuntimeError( + "No metrics context is in progress. Please only call this function within a metrics context/dynamo_timed." + ) + + metrics_context.add_to_set(key, value) + chromium_log.add_to_set(event_name, key, value) + + @staticmethod + def add_to_set_toplevel( + key: str, + value: Any, + log_level: CompileEventLogLevel = CompileEventLogLevel.COMPILATION_METRIC, + ) -> None: + """ + Same as add to set, just does it automatically to the toplevel event instead of having to explicitly name it. + Defaults to COMPILATION_METRIC log level. + """ + chromium_log = get_chromium_event_logger() + top_event = chromium_log.get_outermost_event() + if top_event is None: + raise RuntimeError( + "No toplevel event active. Please only call this function within a metrics context/dynamo_timed." + ) + CompileEventLogger.add_to_set(top_event, log_level, key, value) + + # Helper functions that are syntactic sugar + + @staticmethod + def chromium(event_name: str, **metadata: object) -> None: + """ + Add to in chromium. Each key/value of metadata will appear in the chromium trace. + should be the name of a timed event span passed to `dynamo_timed`. + """ + CompileEventLogger.add_data( + event_name, CompileEventLogLevel.CHROMIUM, overwrite=False, **metadata + ) + + @staticmethod + def pt2_compile(event_name: str, **metadata: object) -> None: + """ + Add to in chromium and PT2 Compile Events. + Each key/value of metadata will appear in the chromium trace. Each kwarg name becomes + a column in PT2 Compile Events, with the corresponding kwarg value. + should be the name of a timed event span passed to `dynamo_timed`, + with log_to_pt2_compile_events=True. + """ + CompileEventLogger.add_data( + event_name, CompileEventLogLevel.PT2_COMPILE, overwrite=False, **metadata + ) + + @staticmethod + def add_record_function_data(event_name: str, **metadata: object) -> None: + """ + Add record function data to the profiler event. + + This emits profiler event data so compilation events show up in stack profilers + like the PyTorch profiler. + + Args: + event_name: Name of the event to record + **metadata: Additional metadata to attach to the record function + """ + if torch.autograd.profiler._is_profiler_enabled and metadata: + metadata_str = ", ".join(f"{k}={v}" for k, v in metadata.items()) + with torch.autograd.profiler.record_function( + f"{event_name}_data: {metadata_str}" + ): + pass + + @staticmethod + def compilation_metric(overwrite: bool = False, **metadata: object) -> None: + """ + Add to the CompilationMetrics context. Also logs to PT2 Compile Events + and chromium. + Each key/value of metadata will appear in the chromium trace. Each kwarg name becomes + a column in PT2 Compile Events and Dynamo Compile, with the corresponding kwarg value. + """ + CompileEventLogger.add_toplevel( + CompileEventLogLevel.COMPILATION_METRIC, overwrite, **metadata + ) + + @staticmethod + def instant( + event_name: str, metadata: dict[str, Any], time_ns: int | None = None + ) -> None: + """ + Log an instant event to chromium logs with name at time . The `args` field in + Perfetto will point to metadata. should be a value obtained from time.time_ns(). + """ + CompileEventLogger.log_instant_event( + event_name, metadata, time_ns, CompileEventLogLevel.CHROMIUM + ) + + @staticmethod + def try_add_pt2_compile(event_name: str, **metadata: object) -> None: + """ + Adds to an existing pt2_compile event, but silently returns if the event doesn't exist + or ChromiumEventLogger is not initialized. + This function is syntactic sugar for chromium_event_logger().try_add_event_data. + """ + if not chromium_event_log_active(): + return + chromium_log = get_chromium_event_logger() + chromium_log.try_add_event_data(event_name, **metadata) + + @staticmethod + def try_(method_fn: Callable[_P, Any], *args: _P.args, **kwargs: _P.kwargs) -> None: + """ + Special function that quietly runs a given method, returning if CHROMIUM_EVENT_LOG is None or metrics context is not set + """ + if not chromium_event_log_active(): + return + metrics_context = get_metrics_context() + if not metrics_context.in_progress(): + return + method_fn(*args, **kwargs) + + +_dynamo_timed_tls = threading.local() + + +@contextmanager +def compile_time_record_function(name: str) -> Generator[Any, None, None]: + """ + A context manager for compile-time profiling that uses _RecordFunctionFast + for lower overhead than torch.profiler.record_function. + + This is intended for use during compilation (dynamo, inductor, etc.) where + we want profiling support but with minimal overhead. Moreover, we do not + want the record_function call inside torch.compile to be dispatched. + + Args: + name: The name of the record function event that will appear in profiles. + """ + if torch.autograd.profiler._is_profiler_enabled: + rf = torch._C._profiler._RecordFunctionFast(name) + rf.__enter__() + try: + yield + finally: + rf.__exit__(None, None, None) + else: + yield + + +@contextmanager +def dynamo_timed( + key: str, + # TODO(masneral): Deprecate this param. + phase_name: str | None = None, + log_pt2_compile_event: bool = False, + metadata: dict[str, object] | None = None, + dynamo_compile_column_us: str | None = None, + compile_id: CompileId | None = None, + is_backward: bool | None = None, + log_waitcounter: bool = False, + waitcounter_name_override: str | None = None, +) -> Generator[Any, None, None]: + """ + dynamo_timed is a context manager + By wrapping a function in dynamo_timed, we can get a few things: + + 1) Optionally log timings to pt2_compile_events. + 2) Optionally log timings to CompilationMetrics (dynamo_compile). + 3) Optionally log chromium events. + 4) Optionally increment a WaitCounter. + 5) Store a record in compilation_time_metrics + For example: + + def _foo(...): + with dynamo_timed("_foo"): + ... + + Would show up as an entry in our timing dict: + OrderedDict([('_foo', [0.083690, 0.23949, 3.1425e-05])]) + This is extremely useful for granular debugging. + + Although it is tempting to use dynamo_timed as a decorator, please do not. + In its decorator form it makes cProfile traces less useful as dynamo_timed + suddenly becomes a bottleneck for lots of function calls (as only one parent + pointer is recorded). + + Params: + - key: key into compile_time_metrics. If phase_name is not provided, this is + also the event name used for pt2_compile_events logs and chromium events. + - phase_name: Optional override for the event name. + - log_pt2_compile_event: Whether to log a pt2 compile event internally. + - metadata: Extra metadata to put in pt2_compile_events. + - dynamo_compile_column_us: If provided, updates the specified CompilationMetrics + field to be logged to dyname_compile column. We expect all columns to be _us; + therefore, the field name must end with "_us". + - compile_id: In the typical case, this parameter should not be needed. Use to + supply the compile_id for those cases where we want to log a compile_id where + it's not naturally available, e.g., for runtime autotuning. + - is_backward: Specify forward/backward directly when not available in a + CompileContext, e.g., during runtime autotuning. + that support it. + - log_waitcounter: If set, we'll log a waitcounter of the form "pytorch.dynamo_timed.{key}" + """ + if phase_name: + event_name = phase_name + fn_name = key + else: + event_name = key + fn_name = None + + if key not in compilation_time_metrics: + compilation_time_metrics[key] = [] + + metrics = compilation_time_metrics[key] + event_metadata = {} + if metadata: + event_metadata.update(metadata) + if fn_name: + event_metadata.update({"fn_name": fn_name}) + if is_backward is not None: + event_metadata.update({"is_backward": is_backward}) + + chromium_log: ChromiumEventLogger = get_chromium_event_logger() + start_ns = time.time_ns() + chromium_log.log_event_start( + event_name, start_ns, event_metadata, log_pt2_compile_event, compile_id + ) + + cx_mgrs: list[typing.Any] = [compile_time_record_function(f"{key} (dynamo_timed)")] + if log_waitcounter: + wc_name = waitcounter_name_override if waitcounter_name_override else key + cx_mgrs.append(_WaitCounter(f"pytorch.wait_counter.{wc_name}").guard()) + + is_compile_time = torch._guards.CompileContext.current_compile_id() is not None + if dynamo_compile_column_us: + # We're standardizing on microseconds for dynamo_compile timings. + assert dynamo_compile_column_us.endswith("_us") + + # Track nested dynamo_timed calls that update CompilationMetrics so we can + # bump a total duration only for the outermost metric. + if not hasattr(_dynamo_timed_tls, "depth"): + _dynamo_timed_tls.depth = 0 + _dynamo_timed_tls.depth += 1 + + # The corresponding WaitCounters that we bump for all overheads + if _dynamo_timed_tls.depth == 1: + cx_mgrs.append(_WaitCounter("pytorch.wait_counter.dynamo_compile").guard()) + if not is_compile_time: + runtime_wc = "pytorch.wait_counter.compile_runtime_overheads" + cx_mgrs.append(_WaitCounter(runtime_wc).guard()) + + try: + with contextlib.ExitStack() as stack: + for cx in cx_mgrs: + stack.enter_context(cx) + yield + finally: + end_ns = time.time_ns() + time_spent_ns = end_ns - start_ns + metrics.append(time_spent_ns / 1e9) + chromium_log.log_event_end( + event_name, end_ns, {}, start_ns, log_pt2_compile_event, compile_id + ) + if dynamo_compile_column_us: + # TODO: the events that we capture in calculate_time_spent() seem a little + # arbitrary. Currently, it's only those fields that are present in + # CompilationMetrics (but note that we accumulate by the associated event + # name, not the field name in CompilationMetrics). Do we want to keep it + # this way? + cumulative_time_spent_ns[event_name] += time_spent_ns + + # Bump the total duration for every outer event. + _dynamo_timed_tls.depth -= 1 + is_outer_event = _dynamo_timed_tls.depth == 0 + + duration_us = time_spent_ns // 1000 + if is_compile_time: + metrics_context = get_metrics_context() + if metrics_context.in_progress(): + metrics_context.increment(dynamo_compile_column_us, duration_us) + if is_outer_event: + metrics_context.increment("duration_us", duration_us) + else: + runtime_context = get_runtime_metrics_context() + runtime_context.increment(dynamo_compile_column_us, duration_us) + if is_outer_event: + extra = { + "compile_id": compile_id, + "is_runtime": True, + "is_forward": not is_backward, + } + runtime_context.increment("duration_us", duration_us, extra) + + +@overload +def compile_times(repr: Literal["str"], aggregate: bool = False) -> str: ... + + +@overload +# pyrefly: ignore [inconsistent-overload] +def compile_times( + repr: Literal["csv"], aggregate: bool = False +) -> tuple[list[str], list[object]]: ... + + +def compile_times( # type: ignore[misc] + repr: str = "str", aggregate: bool = False +) -> str | None | tuple[list[str], list[str]]: + """ + Get metrics about torchdynamo frontend/backend compilation times. + + Accumulates information from functions tagged with `dynamo_timed`. + + repr='str' returns a printable string for user interaction, and 'csv' + returns headers, rows which can be logged for output + + aggregate causes values from multiple compilations (e.g. split graphs) + to be accumulated into one value. If false, expect more than one value + per metric. + """ + + def fmt_fn(values: list[float], item_fn: Callable[[float], str] = str) -> str: + if aggregate: + return item_fn(sum(values)) + return ", ".join(map(item_fn, values)) + + if repr == "str": + rows = [ + (k, fmt_fn(compilation_time_metrics[k], item_fn=lambda x: f"{x:.4f}")) + for k in compilation_time_metrics + ] + out = "TorchDynamo compilation metrics:\n" + out += tabulate(rows, headers=("Function", "Runtimes (s)")) + return out + elif repr == "csv": + values = [ + fmt_fn(v, item_fn=lambda x: f"{x:.6f}") + for v in compilation_time_metrics.values() + ] + headers = list(compilation_time_metrics.keys()) + return headers, values + return None + + +@atexit.register +def dump_compile_times() -> None: + log.info(compile_times(repr="str", aggregate=True)) + + +tensortype_to_dtype = { + torch.FloatTensor: (torch.float32, torch.float), + torch.DoubleTensor: (torch.float64, torch.double), + torch.HalfTensor: (torch.float16, torch.half), + torch.BFloat16Tensor: (torch.bfloat16,), + torch.ByteTensor: (torch.uint8,), + torch.CharTensor: (torch.int8,), + torch.LongTensor: (torch.int64, torch.long), + torch.IntTensor: (torch.int32, torch.int), + torch.ShortTensor: (torch.int16, torch.short), + torch.BoolTensor: (torch.bool,), +} + + +class DuplicateWarningChecker: + def __init__(self, maxsize: int = 4096) -> None: + self.maxsize = maxsize + self.reset() + + def reset(self) -> None: + self.set: OrderedDict[Any, Any] = OrderedDict() + + def add(self, key: str | tuple[object, object]) -> bool: + if key in self.set: + self.set.move_to_end(key, last=True) + if not config.verbose: + return False + else: + self.set[key] = None + while len(self.set) > self.maxsize: + self.set.popitem(last=False) + return True + + +graph_break_dup_warning_checker = DuplicateWarningChecker() + + +def setup_compile_debug() -> contextlib.ExitStack: + compile_debug = os.environ.get("TORCH_COMPILE_DEBUG", "0") == "1" + + if compile_debug: + return add_file_handler() + + return contextlib.ExitStack() + + +def reset_graph_break_dup_checker() -> None: + graph_break_dup_warning_checker.reset() + + +# Matches ANSI escape sequences (CSI) +ANSI_ESCAPE_PATTERN = re.compile( + r""" + \x1B # ESC + \[ # [ + [0-?]* # Parameter bytes + [ -/]* # Intermediate bytes + [@-~] # Final byte + """, + re.VERBOSE, +) + + +class StripAnsiFormatter(logging.Formatter): + """Logging formatter that strips ANSI escape codes.""" + + def format(self, record: logging.LogRecord) -> str: + msg = super().format(record) + return ANSI_ESCAPE_PATTERN.sub("", msg) + + +def add_file_handler() -> contextlib.ExitStack: + log_path = os.path.join(get_debug_dir(), "torchdynamo") + os.makedirs(log_path, exist_ok=True) + + log_file_handler = logging.FileHandler(os.path.join(log_path, "debug.log")) + log_file_handler.setFormatter(StripAnsiFormatter("%(message)s")) + logger = logging.getLogger("torch._dynamo") + logger.addHandler(log_file_handler) + + exitstack = contextlib.ExitStack() + exitstack.callback(lambda: logger.removeHandler(log_file_handler)) + return exitstack + + +def setup_log_file() -> contextlib.ExitStack: + exitstack = contextlib.ExitStack() + if config.log_file_name is not None: + log_file_handler = logging.FileHandler(config.log_file_name) + for logger in torch._logging._internal.get_loggers(): + logger.addHandler(log_file_handler) + exitstack.callback(lambda: logger.removeHandler(log_file_handler)) + return exitstack + + return exitstack + + +def gen_record_file_name(exc: Exception, code: CodeType) -> str: + return f"{get_debug_dir()}/error_recordings/\ +{code.co_name}_{type(exc).__name__}_{code.co_firstlineno}.rec" + + +def write_record_to_file(filename: str, exec_record: ExecutionRecord) -> None: + try: + if os.path.exists(filename): + log.warning( + "Unable to write execution record %s; file already exists.", filename + ) + else: + os.makedirs(os.path.dirname(filename), exist_ok=True) + with open(filename, "wb") as f: + exec_record.dump(f) + except Exception: + log.exception("Unable to write execution record %s", filename) + + +def count_calls(g: fx.Graph) -> int: + c = 0 + for n in g.nodes: + if "call" in n.op: + c += 1 + return c + + +def identity(x: T) -> T: + return x + + +def hashable(x: Any) -> bool: + try: + hash(x) + return True + except TypeError: + return False + # cannot hash writable memoryview object + except ValueError: + return False + + +def nothing(*args: Any, **kwargs: Any) -> None: + pass + + +class ExactWeakKeyDictionary: + """Similar to weakref.WeakKeyDictionary, but use `is`/`id` rather than `==` to compare equality""" + + def __init__(self) -> None: + self.values: dict[int, Any] = {} + self.refs: dict[int, weakref.ReferenceType[Any]] = {} + + def __getitem__(self, key: Any) -> Any: + return self.values[id(key)] + + def get(self, key: Any, default: Any = None) -> Any: + return self.values.get(id(key), default) + + def __contains__(self, key: Any) -> bool: + return id(key) in self.values + + def __setitem__(self, key: Any, value: Any) -> None: + idx = id(key) + if idx not in self.refs: + self.refs[idx] = weakref.ref(key, lambda ref: self._remove_id(idx)) + self.values[idx] = value + + def _remove_id(self, idx: int) -> None: + if idx in self.values: + del self.values[idx] + if idx in self.refs: + del self.refs[idx] + + def clear(self) -> None: + self.refs.clear() + self.values.clear() + + +@overload +def istype(obj: object, allowed_types: type[T]) -> TypeIs[T]: ... + + +@overload +def istype( + obj: object, allowed_types: tuple[type[list[T]], type[tuple[T, ...]]] +) -> TypeIs[T]: ... + + +# This can be simplified once TypeVarTuple objects can be expanded into TypeIs. +@overload +def istype( + obj: object, allowed_types: tuple[type[T1], type[T2]] +) -> TypeIs[T1 | T2]: ... + + +@overload +def istype( + obj: object, allowed_types: tuple[type[T1], type[T2], type[T3]] +) -> TypeIs[T1 | T2 | T3]: ... + + +@overload +def istype( + obj: object, allowed_types: tuple[type[T1], type[T2], type[T3], type[T4]] +) -> TypeIs[T1 | T2 | T3 | T4]: ... + + +@overload +def istype( + obj: object, allowed_types: tuple[type[T1], type[T2], type[T3], type[T4], type[T5]] +) -> TypeIs[T1 | T2 | T3 | T4 | T5]: ... + + +@overload +def istype( + obj: object, + allowed_types: tuple[type[T1], type[T2], type[T3], type[T4], type[T5], type[T6]], +) -> TypeIs[T1 | T2 | T3 | T4 | T5 | T6]: ... + + +@overload +def istype( + obj: object, + allowed_types: tuple[ + type[T1], type[T2], type[T3], type[T4], type[T5], type[T6], type[T7] + ], +) -> TypeIs[T1 | T2 | T3 | T4 | T5 | T6 | T7]: ... + + +@overload +def istype( + obj: object, + allowed_types: tuple[ + type[T1], type[T2], type[T3], type[T4], type[T5], type[T6], type[T7], type[T8] + ], +) -> TypeIs[T1 | T2 | T3 | T4 | T5 | T6 | T7 | T8]: ... + + +@overload +def istype( + obj: object, + allowed_types: tuple[ + type[T1], + type[T2], + type[T3], + type[T4], + type[T5], + type[T6], + type[T7], + type[T8], + type[T9], + ], +) -> TypeIs[T1 | T2 | T3 | T4 | T5 | T6 | T7 | T8 | T9]: ... + + +@overload +def istype( + obj: object, + allowed_types: tuple[ + type[T1], + type[T2], + type[T3], + type[T4], + type[T5], + type[T6], + type[T7], + type[T8], + type[T9], + type[T10], + ], +) -> TypeIs[T1 | T2 | T3 | T4 | T5 | T6 | T7 | T8 | T9 | T10]: ... + + +@overload +def istype( + obj: object, allowed_types: tuple[type, ...] | list[type] | set[type] +) -> bool: ... + + +def istype(obj: object, allowed_types: Any) -> bool: + """isinstance() without subclasses""" + if isinstance(allowed_types, (tuple, list, set)): + return type(obj) in allowed_types + return type(obj) is allowed_types + + +_builtin_final_typing_classes: tuple[Any, ...] = tuple() +if sys.version_info >= (3, 12): + # Some typing classes moved to C in 3.12, + # which no longer have the _Final mixin. + # Check for consistency e.g. here: + # https://github.com/python/cpython/blob/f2b82b3b3b1f8c7a81e84df35ee921e44517cf32/Lib/typing.py#L32 + _builtin_final_typing_classes = ( + typing.ParamSpecArgs, + typing.ParamSpecKwargs, + typing.ParamSpec, + typing.TypeVar, + typing.TypeVarTuple, + typing.TypeAliasType, + ) + + +def get_inputs_devices( + inputs: collections.abc.Sequence[object], + model: torch.fx.GraphModule, +) -> list[torch.device | None]: + all_inputs = pytree.tree_flatten(inputs)[0] + [ + node.meta["val"] for node in list(model.graph.nodes) if "val" in node.meta + ] + devices: list[torch.device | None] = list( + OrderedSet([i.device for i in all_inputs if hasattr(i, "device")]) + ) + return [ + i for i in devices if (isinstance(i, torch.device) and i.type != "meta") + ] + [None] + + +if sys.version_info >= (3, 14): + _builtin_final_typing_classes += (typing.Union,) + + +def is_typing(value: Any) -> bool: + # _Final catches most of typing classes: + # - Any + # - Callable + # - Union (Python < 3.14) + # ... + # + # NB: we intentionally ignore classes that inherit from Generic, since they + # can be used as both TypingVariable as well as UserDefinedClassVariable. + if sys.version_info >= (3, 12) and isinstance(value, _builtin_final_typing_classes): + return True + return ( + isinstance(value, (types.UnionType, typing._Final)) # type: ignore[attr-defined] + or value is typing.Generic + or value is typing.Union + ) + + +def is_numpy_int_type(value: Any) -> bool: + if not np: + return False + + return istype( + value, + ( + np.int8, + np.int16, + np.int32, + np.int64, + np.uint8, + np.uint16, + np.uint32, + np.uint64, + ), + ) + + +def is_numpy_float_type(value: Any) -> bool: + if not np: + return False + + return istype( + value, + ( + np.float16, + np.float32, + np.float64, + ), + ) + + +@overload +def is_lru_cache_wrapped_function( + value: Callable[..., T], +) -> TypeGuard[functools._lru_cache_wrapper[T]]: ... + + +@overload +def is_lru_cache_wrapped_function( + value: Any, +) -> TypeGuard[functools._lru_cache_wrapper[Any]]: ... + + +def is_lru_cache_wrapped_function( + value: Any, +) -> bool: + return isinstance(value, functools._lru_cache_wrapper) and is_function( + inspect.getattr_static(value, "__wrapped__") + ) + + +_FuncTypes: TypeAlias = ( + types.FunctionType + | types.BuiltinFunctionType + | types.MethodDescriptorType + | types.WrapperDescriptorType +) + + +def is_function_or_wrapper( + value: Any, +) -> TypeIs[_FuncTypes | torch._ops.OpOverloadPacket | torch._ops.OpOverload]: + return is_function(value) or isinstance( + value, (torch._ops.OpOverloadPacket, torch._ops.OpOverload) + ) + + +def is_function( + value: Any, +) -> TypeIs[_FuncTypes]: + return isinstance( + value, + ( + types.FunctionType, + types.BuiltinFunctionType, + types.MethodDescriptorType, + types.WrapperDescriptorType, + ), + ) + + +cmp_name_to_op_mapping = { + "__eq__": operator.eq, + "__ne__": operator.ne, + "__lt__": operator.lt, + "__le__": operator.le, + "__gt__": operator.gt, + "__ge__": operator.ge, +} + + +cmp_name_to_op_str_mapping = { + "__eq__": "==", + "__ne__": "!=", + "__lt__": "<", + "__le__": "<=", + "__gt__": ">", + "__ge__": ">=", +} + + +def is_wrapper_or_member_descriptor( + value: Any, +) -> TypeIs[ + types.GetSetDescriptorType + | types.MethodDescriptorType + | types.WrapperDescriptorType + | types.MemberDescriptorType + | types.MethodWrapperType +]: + return isinstance( + value, + ( + # set up by PyGetSetDef + types.GetSetDescriptorType, + # set by PyMethodDef, e.g. list.append + types.MethodDescriptorType, + # slots - list.__add__ + types.WrapperDescriptorType, + # set up by PyMemberDef + types.MemberDescriptorType, + # wrapper over C functions + types.MethodWrapperType, + ), + ) + + +def unwrap_if_wrapper(fn: Any) -> Any: + return unwrap_with_attr_name_if_wrapper(fn)[0] + + +def unwrap_with_attr_name_if_wrapper(fn: Any) -> tuple[Any, str | None]: + # TODO(anijain2305) - Investigate if we can get rid of this function + # unpack @torch._dynamo.optimize()(fn) wrapped function + if is_function(fn) and inspect.getattr_static(fn, "_torchdynamo_inline", False): + fn = inspect.getattr_static(fn, "_torchdynamo_inline", fn) + attr_name = "_torchdynamo_inline" + else: + attr_name = None + return fn, attr_name + + +def is_numpy_ndarray(value: Any) -> TypeGuard[np.ndarray]: # type: ignore[type-arg] + if not np: + return False + + return istype(value, np.ndarray) + + +def istensor(obj: Any) -> bool: + """Check of obj is a tensor""" + tensor_list: tuple[type, ...] = ( + torch.Tensor, + torch.nn.Parameter, + *config.traceable_tensor_subclasses, + ) + tensor_list = tensor_list + (torch._subclasses.FakeTensor,) + return istype(obj, tensor_list) + + +def is_lazy_module(mod: Any) -> bool: + return isinstance(mod, LazyModuleMixin) + + +@functools.lru_cache(4096) +def print_once(*args: Any) -> None: + print(*args) + + +def make_cell(val: Any = None) -> types.CellType: + """Some black magic to create a cell object that usually only exists in a closure""" + x = val + + def f() -> Any: + return x + + assert f.__closure__ is not None and len(f.__closure__) == 1 + return f.__closure__[0] + + +def proxy_args_kwargs(args: Any, kwargs: Any) -> tuple[tuple[Any, ...], dict[str, Any]]: + try: + proxy_args = tuple(arg.as_proxy() for arg in args) + proxy_kwargs = {key: arg.as_proxy() for key, arg in kwargs.items()} + return proxy_args, proxy_kwargs + except NotImplementedError as e: + from .exc import unimplemented + from .variables.base import typestr + + unimplemented( + gb_type="Failed to convert args/kwargs to proxy", + context=f"call_function args: {typestr(*args)} {typestr(*list(kwargs.values()))}", + explanation="Missing `as_proxy()` implementation for some arg/kwarg.", + hints=[], + from_exc=e, + ) + + +def to_int_ms(v: float | None) -> int | None: + return None if v is None else int(v * 1000) + + +# float64 timestamp has a quarter microsecond precision in 2024, so while +# this is suboptimal we shouldn't meaningfully lose precision +def to_int_us(v: float | None) -> int | None: + return None if v is None else int(v * 1_000_000) + + +# Version field added to every log. Increment to make it easier to distinguish new +# vs. old entries when you make a substantive change to how the logs are populated. +LOG_FORMAT_VERSION = 3 + + +@dataclasses.dataclass +class CompilationMetrics: + compile_id: str | None = None + frame_key: str | None = None + co_name: str | None = None + co_filename: str | None = None + co_firstlineno: int | None = None + cache_size: int | None = None + accumulated_cache_size: int | None = None + guard_count: int | None = None + shape_env_guard_count: int | None = None + graph_op_count: int | None = None + graph_node_count: int | None = None + graph_input_count: int | None = None + start_time: float | None = None + entire_frame_compile_time_s: float | None = None + backend_compile_time_s: float | None = None + inductor_compile_time_s: float | None = None + code_gen_time_s: float | None = None + fail_type: str | None = None + fail_reason: str | None = None + fail_user_frame_filename: str | None = None + fail_user_frame_lineno: int | None = None + non_compliant_ops: set[str] | None = None + compliant_custom_ops: set[str] | None = None + restart_reasons: set[str] | None = None + dynamo_time_before_restart_s: float | None = None + stack_trace: list[str] | None = None + exception_stack_trace: list[str] | None = None + graph_node_shapes: str | None = None + # Sometimes, we will finish analyzing a frame but conclude we don't want + # to install any guarded code. True means we actually decided to install + # a compiled frame + has_guarded_code: bool | None = None + remote_cache_time_saved_s: float | None = None + structured_logging_overhead_s: float | None = None + config_suppress_errors: bool | None = None + config_inline_inbuilt_nn_modules: bool | None = None + specialize_float: bool | None = None + dynamo_config: str | None = None + compiler_config: str | None = None + is_forward: bool | None = None + num_triton_bundles: int | None = None + remote_fx_graph_cache_get_time_ms: int | None = None + remote_fx_graph_cache_put_time_ms: int | None = None + start_time_us: int | None = None + duration_us: int | None = None + dynamo_cumulative_compile_time_us: int | None = None + aot_autograd_cumulative_compile_time_us: int | None = None + inductor_cumulative_compile_time_us: int | None = None + inductor_code_gen_cumulative_compile_time_us: int | None = None + triton_compile_time_us: int | None = None + runtime_cudagraphify_time_us: int | None = None + runtime_triton_autotune_time_us: int | None = None + dynamo_compile_time_before_restart_us: int | None = None + distributed_ephemeral_timeout_us: int | None = None + structured_logging_overhead_us: int | None = None + remote_fx_graph_cache_get_time_us: int | None = None + remote_fx_graph_cache_put_time_us: int | None = None + backward_cumulative_compile_time_us: int | None = None + end_time_us: int | None = None + pre_grad_pass_time_us: int | None = None + post_grad_pass_time_us: int | None = None + joint_graph_pass_time_us: int | None = None + log_format_version: int = LOG_FORMAT_VERSION + inductor_config: str | None = None + remote_cache_version: int | None = None + inductor_fx_remote_cache_hit_count: int | None = 0 + inductor_fx_remote_cache_miss_count: int | None = 0 + inductor_fx_remote_cache_backend_type: str | None = None + inductor_fx_remote_cache_hit_keys: str | None = None + inductor_fx_remote_cache_miss_keys: str | None = None + inductor_fx_local_cache_hit_count: int | None = 0 + inductor_fx_local_cache_miss_count: int | None = 0 + aotautograd_remote_cache_hit_count: int | None = 0 + aotautograd_remote_cache_miss_count: int | None = 0 + aotautograd_local_cache_hit_count: int | None = 0 + aotautograd_local_cache_miss_count: int | None = 0 + cuda_version: str | None = None + triton_version: str | None = None + feature_usage: dict[str, bool] | None = None + compile_time_autotune_time_us: int | None = None + is_runtime: bool | None = False + gc_time_us: int | None = None + tensorify_float_attempt: bool | None = None + tensorify_float_success: bool | None = None + tensorify_float_failure: set[str] | None = None + guard_latency_us: float | None = None + recompile_reason: str | None = None + num_graph_breaks: int | None = None + triton_kernel_compile_times_us: str | None = None + ir_count: int | None = None + cudagraph_skip_reason: str | None = None + python_version: str | None = None + pgo_put_remote_code_state_time_us: int | None = None + pgo_get_remote_code_state_time_us: int | None = None + # The number of elements within parameters. This is classically what people + # think of when they think of parameters in a ML model. + param_numel: int | None = None + # The number of elements counted by bytes - i.e. a float32 is 4 bytes + # per element. + param_bytes: int | None = None + # The number of parameters counted by fields. This is mostly a proxy for + # the number of distinct type of params. + param_count: int | None = None + recompile_user_contexts: set[str] | None = None + inline_inbuilt_nn_modules_candidate: bool | None = False + pytorch_version: str | None = None + inductor_provenance: set[str] | None = None + + @classmethod + def create(cls, metrics: dict[str, Any]) -> CompilationMetrics: + """ + Factory method to create a CompilationMetrics from a dict of fields. + Includes the logic to add legacy fields and any pre-processing, e.g., + we transform some fields to comma-separated strings for scuba logging. + """ + + def us_to_s(metric: int | None) -> float | None: + return metric / 1e6 if metric is not None else None + + def us_to_ms(metric: int | None) -> int | None: + return metric // 1000 if metric is not None else None + + def collection_to_str(metric: Any | None) -> str | None: + def safe_str(item: Any) -> str: + try: + return str(item) + except Exception: + return "" + + if metric is None: + return None + + if not isinstance(metric, (set, list)): + return "" + + return ",".join(safe_str(item) for item in sorted(metric)) + + def collection_to_json_str(metric: Any | None) -> str | None: + if metric is None: + return None + try: + return json.dumps(list(metric)) + except Exception: + return "" + + # TODO: The following are legacy fields, populated from the fields that replace + # them. Remove these when we decide we can really deprecate them. + legacy_metrics = { + "start_time": us_to_s(metrics.get("start_time_us")), + "entire_frame_compile_time_s": us_to_s( + metrics.get("dynamo_cumulative_compile_time_us") + ), + "backend_compile_time_s": us_to_s( + metrics.get("aot_autograd_cumulative_compile_time_us") + ), + "inductor_compile_time_s": us_to_s( + metrics.get("inductor_cumulative_compile_time_us") + ), + "code_gen_time_s": us_to_s( + metrics.get("inductor_code_gen_cumulative_compile_time_us") + ), + "remote_cache_time_saved_s": us_to_s( + metrics.get("distributed_ephemeral_timeout_us") + ), + "remote_fx_graph_cache_get_time_ms": us_to_ms( + metrics.get("remote_fx_graph_cache_get_time_us") + ), + "remote_fx_graph_cache_put_time_ms": us_to_ms( + metrics.get("remote_fx_graph_cache_put_time_us") + ), + "structured_logging_overhead_s": us_to_s( + metrics.get("structured_logging_overhead_us") + ), + } + + all_metrics = {**legacy_metrics, **metrics} + + # Processing before logging: + all_metrics["inductor_fx_remote_cache_hit_keys"] = collection_to_str( + all_metrics.get("inductor_fx_remote_cache_hit_keys") + ) + all_metrics["inductor_fx_remote_cache_miss_keys"] = collection_to_str( + all_metrics.get("inductor_fx_remote_cache_miss_keys") + ) + all_metrics["triton_kernel_compile_times_us"] = collection_to_json_str( + all_metrics.get("triton_kernel_compile_times_us") + ) + compile_id = all_metrics.get("compile_id") + all_metrics["compile_id"] = str(compile_id) if compile_id else None + + # pyrefly: ignore [bad-argument-type] + return cls(**all_metrics) + + +DEFAULT_COMPILATION_METRICS_LIMIT = 64 + + +_compilation_metrics: collections.deque[CompilationMetrics] = collections.deque( + maxlen=DEFAULT_COMPILATION_METRICS_LIMIT +) + + +def add_compilation_metrics_to_chromium(c: CompilationMetrics) -> None: + """ + These are the common fields in CompilationMetrics that existed before + metrics_context, and aren't set by MetricsContext.set(). We add the subset + of them that make sense in `dynamo`/toplevel events in PT2 Compile Events + directly. + + If you're tempted to add to this list, consider using CompileEventLogger.compilation_metric() + instead, which will automatically also add it to tlparse and PT2 Compile Events. + TODO: Get rid of this function and replace it with CompileEventLogger directly instead. + """ + event_logger = get_chromium_event_logger() + event_name = event_logger.get_outermost_event() + if not event_name: + return + event_logger.add_event_data( + event_name=event_name, + frame_key=c.frame_key, + co_name=c.co_name, + co_filename=c.co_filename, + co_firstlineno=c.co_firstlineno, + cache_size=c.cache_size, + accumulated_cache_size=c.accumulated_cache_size, + guard_count=c.guard_count, + shape_env_guard_count=c.shape_env_guard_count, + graph_op_count=c.graph_op_count, + graph_node_count=c.graph_node_count, + graph_input_count=c.graph_input_count, + fail_type=c.fail_type, + fail_reason=c.fail_reason, + fail_user_frame_filename=c.fail_user_frame_filename, + fail_user_frame_lineno=c.fail_user_frame_lineno, + # Sets aren't JSON serializable + non_compliant_ops=( + list(c.non_compliant_ops) if c.non_compliant_ops is not None else None + ), + compliant_custom_ops=( + list(c.compliant_custom_ops) if c.compliant_custom_ops is not None else None + ), + restart_reasons=( + list(c.restart_reasons) if c.restart_reasons is not None else None + ), + dynamo_time_before_restart_s=c.dynamo_time_before_restart_s, + has_guarded_code=c.has_guarded_code, + dynamo_config=c.dynamo_config, + ) + + +def _get_dynamo_config_for_logging() -> str | None: + def clean_for_json(d: dict[str, Any]) -> dict[str, Any]: + blocklist = { + "TYPE_CHECKING", + "log_file_name", + "verbose", + "repro_after", + "repro_level", + "repro_forward_only", + "repro_tolerance", + "repro_ignore_non_fp", + "same_two_models_use_fp64", + "base_dir", + "debug_dir_root", + "_save_config_ignore", + "log_compilation_metrics", + "inject_BUILD_SET_unimplemented_TESTING_ONLY", + "_autograd_backward_strict_mode_banned_ops", + "reorderable_logging_functions", + "ignore_logger_methods", + "ignore_logging_functions", + "traceable_tensor_subclasses", + "nontraceable_tensor_subclasses", + "_custom_ops_profile", + } + + return { + key: sorted(value) if isinstance(value, set) else value + for key, value in d.items() + if key not in blocklist + } + + config_dict = clean_for_json(config.get_config_copy()) + return json.dumps(config_dict, sort_keys=True) + + +def _compiler_config_for_logging() -> str | None: + def clean_for_json(d: dict[str, Any]) -> dict[str, Any]: + blocklist = { + "TYPE_CHECKING", + } + + return { + key: sorted(value) if isinstance(value, set) else value + for key, value in d.items() + if key not in blocklist + } + + if not torch.compiler.config: + return None + + try: + compiler_config_copy = torch.compiler.config.get_config_copy() # type: ignore[attr-defined] + except (TypeError, AttributeError): + return "Compiler Config cannot be pickled" + + config_dict = clean_for_json(compiler_config_copy) + return json.dumps(config_dict, sort_keys=True) + + +def _scrubbed_inductor_config_for_logging() -> str | None: + """ + Method to parse and scrub uninteresting configs from inductor config + """ + + # TypeSafeSerializer for json.dumps() + # Skips complex types as values in config dict + class TypeSafeSerializer(json.JSONEncoder): + def default(self, o: Any) -> Any: + try: + return super().default(o) + except Exception: + return "Value is not JSON serializable" + + keys_to_scrub: set[Any] = set() + inductor_conf_str = None + inductor_config_copy = None + + if torch._inductor.config: + try: + inductor_config_copy = torch._inductor.config.get_config_copy() + except (TypeError, AttributeError, RuntimeError, AssertionError): + inductor_conf_str = "Inductor Config cannot be pickled" + + if inductor_config_copy is not None: + try: + for key, val in inductor_config_copy.items(): + if not isinstance(key, str): + keys_to_scrub.add(key) + # Convert set() to list for json.dumps() + if isinstance(val, set): + inductor_config_copy[key] = list(val) + # Evict unwanted keys + for key in keys_to_scrub: + del inductor_config_copy[key] + # Stringify Inductor config + inductor_conf_str = json.dumps( + inductor_config_copy, + cls=TypeSafeSerializer, + skipkeys=True, + sort_keys=True, + ) + except Exception: + # Don't crash because of runtime logging errors + inductor_conf_str = "Inductor Config is not JSON serializable" + return inductor_conf_str + + +def record_compilation_metrics( + start_time_ns: int, + end_time_ns: int, + metrics: dict[str, Any], + exc_type: type[BaseException] | None, + exc_value: BaseException | None, +) -> None: + if torch._inductor.utils.should_use_remote_fx_graph_cache(): + try: + from torch._inductor.fb.remote_cache import REMOTE_CACHE_VERSION + + remote_cache_version = REMOTE_CACHE_VERSION + inductor_fx_remote_cache_backend_type = "_ManifoldCache" + except ModuleNotFoundError: + remote_cache_version = None + inductor_fx_remote_cache_backend_type = None + else: + inductor_fx_remote_cache_backend_type = None + remote_cache_version = None + + # Populate the compile_id from the metrics context if it's set. Otherwise, + # look for it in the current compile context. + compile_id = metrics.get("compile_id") + if not compile_id: + compile_id = torch._guards.CompileContext.current_compile_id() + + common_metrics = { + "compile_id": compile_id, + "start_time_us": start_time_ns // 1000, + "end_time_us": end_time_ns // 1000, + "fail_type": exc_type.__qualname__ if exc_type else None, + "fail_reason": str(exc_value) if exc_value else None, + "structured_logging_overhead_us": to_int_us( + torch._logging.get_structured_logging_overhead() + ), + "dynamo_config": _get_dynamo_config_for_logging(), + "config_suppress_errors": config.suppress_errors, + "config_inline_inbuilt_nn_modules": True, + "inductor_config": _scrubbed_inductor_config_for_logging(), + "compiler_config": _compiler_config_for_logging(), + "cuda_version": torch.version.cuda, + "triton_version": triton.__version__ if has_triton() else "", + "remote_cache_version": remote_cache_version, + "inductor_fx_remote_cache_backend_type": inductor_fx_remote_cache_backend_type, + "python_version": sys.version, + "pytorch_version": torch.__version__, + } + + compilation_metrics = CompilationMetrics.create({**common_metrics, **metrics}) + _compilation_metrics.append(compilation_metrics) + + name = "compilation_metrics" + if compilation_metrics.is_forward is False: + name = "bwd_" + name + if compilation_metrics.is_runtime is True: + name = name + "_runtime" + + torch._logging.trace_structured( + name, + lambda: { + k: list(v) if isinstance(v, set) else v + for k, v in dataclasses.asdict(compilation_metrics).items() + }, + # NB: Because compilation metrics *includes* the logging overhead time, + # we can't both *measure* the logging overhead of compilation metrics + # without making it inconsistent with compilation metrics itself, so + # we ignore the (hopefully small) time spent logging compilation metrics + record_logging_overhead=False, + # These may be runtime logs, e.g., runtime autotunning, so we provide + # the CompileId from the compilation metrics in case it's not available + # in the current trace. + compile_id=compile_id, + ) + + # If there's a chromium event in flight, add the CompilationMetrics to it. + add_compilation_metrics_to_chromium(compilation_metrics) + + # Finally log the compilation metrics. + if config.log_compilation_metrics: + log_compilation_event(compilation_metrics) + + +def set_compilation_metrics_limit(new_size: int) -> None: + global _compilation_metrics + while len(_compilation_metrics) > new_size: + _compilation_metrics.popleft() + new_deque = collections.deque(_compilation_metrics, maxlen=new_size) + _compilation_metrics = new_deque + + +def clear_compilation_metrics() -> None: + global _compilation_metrics + _compilation_metrics.clear() + + +def get_compilation_metrics() -> list[CompilationMetrics]: + return list(_compilation_metrics) + + +class ChromiumEventLogger: + """Logs chromium events to structured logs. tlparse will concatenate these into a perfetto UI link. + + Also emits RecordFunction calls to torch.profiler when enabled. + + See https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#heading=h.yr4qxyxotyw for + a specification of the Chromium Event JSON format. + """ + + def get_stack(self) -> list[str]: + """ + The main event stack, with every chromium event. + Logged to tlparse. + """ + if hasattr(self.tls, "stack"): + return self.tls.stack + else: + self.tls.stack = [] + return self.tls.stack + + def get_outermost_event(self) -> str | None: + """ + Get the outermost event name (i.e. the longest running event) + or None if the stack is empty. + """ + stack = self.get_stack() + return stack[0] if stack else None + + def get_pt2_compile_substack(self) -> list[str]: + """ + A smaller subset of the main stack that gets used to log + PT2 Compile Events internally. + """ + if hasattr(self.tls, "pt2_compile_substack"): + return self.tls.pt2_compile_substack + else: + self.tls.pt2_compile_substack = [] + return self.tls.pt2_compile_substack + + def get_event_data(self) -> dict[str, Any]: + if not hasattr(self.tls, "event_data"): + self.tls.event_data = {} + return self.tls.event_data + + def get_record_functions(self) -> dict[str, AbstractContextManager[None]]: + if not hasattr(self.tls, "record_functions"): + self.tls.record_functions = {} + return self.tls.record_functions + + def __init__(self) -> None: + self.tls = threading.local() + + from . import config + + # Generate a unique id for this logger, which we can use in scuba to filter down + # to a single python run. + if config.pt2_compile_id_prefix: + self.id_ = f"{config.pt2_compile_id_prefix}-{uuid.uuid4()}" + else: + self.id_ = str(uuid.uuid4()) + + # TODO: log to init/id tlparse after I add support for it + log.info("ChromiumEventLogger initialized with id %s", self.id_) + + def try_add_event_data(self, event_name: str, **kwargs: Any) -> None: + """ + Same as add_event_data, but will silently not log if the event isn't in the stack. + """ + if event_name not in self.get_stack(): + return + self.add_event_data(event_name, **kwargs) + + def add_event_data( + self, + event_name: str, + **kwargs: Any, + ) -> None: + """ + Adds additional metadata info to an in-progress event + This metadata is recorded in the END event + """ + if event_name not in self.get_stack(): + raise RuntimeError( + f"Event {repr(event_name)} not in {self.get_stack()}. " + "Cannot add metadata to events that aren't in progress. " + "Please make sure the event has started and hasn't ended." + ) + event_data = self.get_event_data() + if event_name not in event_data: + event_data[event_name] = {} + event_data[event_name].update(kwargs) + + def increment(self, event_name: str, key: str, value: int) -> None: + """ + Increment an integer event data field by the given amount + """ + if event_name not in self.get_stack(): + raise RuntimeError( + f"Event {repr(event_name)} not in {self.get_stack()}. " + "Cannot add metadata to events that aren't in progress. " + "Please make sure the event has started and hasn't ended." + ) + + event_data = self.get_event_data() + if event_name not in event_data: + event_data[event_name] = {} + if key not in event_data[event_name]: + event_data[event_name][key] = 0 + event_data[event_name][key] += value + + def add_to_set( + self, + event_name: str, + key: str, + value: Any, + ) -> None: + """ + Add a value to a set within a event_name's metadata if it exists + """ + if event_name not in self.get_stack(): + raise RuntimeError( + f"Event {repr(event_name)} not in {self.get_stack()}. " + "Cannot add metadata to events that aren't in progress. " + "Please make sure the event has started and hasn't ended." + ) + event_data = self.get_event_data() + if event_name not in event_data: + event_data[event_name] = {} + if key not in event_data[event_name]: + event_data[event_name][key] = set() + event_data[event_name][key].add(value) + + def log_event_start( + self, + event_name: str, + time_ns: int, + metadata: dict[str, Any], + log_pt2_compile_event: bool = False, + compile_id: CompileId | None = None, + ) -> None: + """ + Logs the start of a single event. + :param str event_name Name of event to appear in trace + :param time_ns Timestamp in nanoseconds + :param metadata: Any extra metadata associated with this event + :param log_pt2_compile_event: If True, log to pt2_compile_events + :param compile_id: Explicit compile_id (rather than using the current context) + """ + compile_id = compile_id or torch._guards.CompileContext.current_compile_id() + metadata["compile_id"] = str(compile_id) + self._log_timed_event( + event_name, + time_ns, + "B", + metadata, + ) + self.get_stack().append(event_name) + # Add metadata from start event + self.add_event_data(event_name, **metadata) + if log_pt2_compile_event: + self.get_pt2_compile_substack().append(event_name) + + # Emit profiler event so compilation events show up in stock PyTorch profiler + if torch.autograd.profiler._is_profiler_enabled: + rf = torch._C._profiler._RecordFunctionFast(event_name) + rf.__enter__() + self.get_record_functions()[event_name] = rf + + # Add metadata to the profiler event if present + if metadata: + CompileEventLogger.add_record_function_data(event_name, **metadata) + + def reset(self) -> None: + # We this on every compile in case a compile crashes or restarts and we haven't + # cleared the stack. + stack = self.get_stack() + substack = self.get_pt2_compile_substack() + stack.clear() + substack.clear() + event_data = self.get_event_data() + event_data.clear() + # Clean up any lingering record functions (shouldn't happen in normal operation) + record_functions = self.get_record_functions() + if record_functions: + for rf in record_functions.values(): + rf.__exit__(None, None, None) + record_functions.clear() + + def log_event_end( + self, + event_name: str, + time_ns: int, + metadata: dict[str, Any], + start_time_ns: int, + log_pt2_compile_event: bool, + compile_id: CompileId | None = None, + ) -> None: + """ + Logs the end of a single event. This function should only be + called after log_event_start with the same event_name. + :param event_name: Name of event to appear in trace + :param time_ns: Timestamp in nanoseconds + :param metadata: Any extra metadata associated with this event + :param start_time_ns: The start time timestamp in nanoseconds + :param log_pt_compile_event: If True, log to pt2_compile_events + :param compile_id: Explicit compile_id (rather than using the current context) + """ + compile_id = compile_id or torch._guards.CompileContext.current_compile_id() + metadata["compile_id"] = str(compile_id) + + # Grab metadata collected during event span + all_event_data = self.get_event_data() + if event_name in all_event_data: + event_metadata = all_event_data[event_name] + del all_event_data[event_name] + else: + # pyrefly: ignore [implicit-any] + event_metadata = {} + # Add the passed in metadata + event_metadata.update(metadata) + + event = self._log_timed_event( + event_name, + time_ns, + "E", + event_metadata, + ) + + def pop_stack(stack: list[str]) -> None: + while event_name != stack[-1]: + # If the event isn't the most recent one to end, pop + # off the stack until it is. + # Since event_name in self.stack, this pop is always safe + log.warning( + "ChromiumEventLogger: Detected overlapping events, fixing stack" + ) + stack.pop() + + event_stack = self.get_stack() + # These stack health checks currently never happen, + # but they're written this way to future proof any weird event + # overlaps in the future. + if event_name not in event_stack: + # Something went wrong, we never called start on this event, + # or it was skipped due to overlapping events below + log.warning("ChromiumEventLogger: Start event not in stack, ignoring") + return + + pop_stack(event_stack) + + if log_pt2_compile_event: + pt2_compile_substack = self.get_pt2_compile_substack() + pop_stack(pt2_compile_substack) + log_chromium_event_internal( + event, pt2_compile_substack, self.id_, start_time_ns + ) + # Pop actual event off of stack + pt2_compile_substack.pop() + + # Finally pop the actual event off the stack + event_stack.pop() + + # End profiler event so compilation events show up in stock PyTorch profiler + record_functions = self.get_record_functions() + if event_name in record_functions: + rf = record_functions.pop(event_name) + rf.__exit__(None, None, None) + + def _log_timed_event( + self, + event_name: str, + time_ns: int, + phase: str, + metadata: dict[str, Any] | None = None, + ) -> dict[str, Any]: + """ + Logs a timed event in chromium format. See log_event_start, log_event_end, etc. + """ + event = { + "name": event_name, + "ts": time_ns / 1000, # Chromium events are in micro seconds + "args": metadata, + "ph": phase, + # These categories are needed in all chromium traces + "cat": "dynamo_timed", + "tid": 0, + "pid": 0, # pid should be specified on all logs, we don't personally care about the actual process id + } + torch._logging.trace_structured( + "chromium_event", + payload_fn=lambda: event, + suppress_context=False, + expect_trace_id=False, # Not every chromium event will have a trace_id + ) + record_chromium_event_internal(event) + return event + + def log_instant_event( + self, + event_name: str, + time_ns: int, + metadata: dict[str, Any] | None = None, + # By default, an instant event isn't logged internally, only to structured logging. + log_pt2_compile_event: bool = False, + ) -> None: + """ + Log an instant event with no associated duration. + :param str event_name: Name of event to appear in trace + :param int time_ns Timestamp in nanoseconds + :param Optional[Dict[str, Any]] metadata: Any extra metadata associated with this event + :param str cname optional color for the arrow in the trace + """ + if metadata is None: + metadata = {} + compile_id = str(torch._guards.CompileContext.current_compile_id()) + metadata["compile_id"] = compile_id + event = { + "name": event_name, + "ts": time_ns / 1000, + "args": metadata, + "ph": "i", + # These categories are needed in all chromium traces + "cat": "dynamo_timed", + "tid": 0, + "pid": 0, + "s": "p", # We use "process" level instant events so they all appear on the same row in the trace. + } + torch._logging.trace_structured( + "chromium_event", + payload_fn=lambda: event, + suppress_context=False, + expect_trace_id=True, + ) + if log_pt2_compile_event: + # Log an instant event with the same start and end time + log_chromium_event_internal( + event, self.get_pt2_compile_substack(), self.id_, time_ns + ) + + +CHROMIUM_EVENT_LOG: ChromiumEventLogger | None = None + + +def get_chromium_event_logger() -> ChromiumEventLogger: + global CHROMIUM_EVENT_LOG + if CHROMIUM_EVENT_LOG is None: + CHROMIUM_EVENT_LOG = ChromiumEventLogger() + return CHROMIUM_EVENT_LOG + + +def chromium_event_log_active() -> bool: + global CHROMIUM_EVENT_LOG + return CHROMIUM_EVENT_LOG is not None + + +@contextmanager +def chromium_event_timed( + event_name: str, + reset_event_log_on_exit: bool = False, + log_pt2_compile_event: bool = False, +) -> Generator[Any, None, None]: + """ + Context manager that creates a chromium start and end event. Chromium event + logging is integrated with dynamo_timed, so you probably want to use that + instead. Use this context manager only if you want to avoid dynamo_timed. + """ + chromium_event_log = get_chromium_event_logger() + chromium_start_time = time.time_ns() + chromium_event_log.log_event_start( + event_name, + chromium_start_time, + {}, + log_pt2_compile_event, + ) + try: + yield + finally: + chromium_event_log.log_event_end( + event_name, + time.time_ns(), + {}, + chromium_start_time, + log_pt2_compile_event, + ) + if reset_event_log_on_exit: + chromium_event_log.reset() + + +@dataclasses.dataclass +class CleanupHook: + """Remove a global variable when hook is called""" + + scope: dict[str, Any] + name: str + + def __call__(self, *args: Any) -> None: + # Make sure we're not shutting down + if CleanupManager is not None: + CleanupManager.count -= 1 + del self.scope[self.name] + + @staticmethod + def create(scope: dict[str, Any], name: str, val: Any) -> CleanupHook: + assert name not in scope + CleanupManager.count += 1 + scope[name] = val + return CleanupHook(scope, name) + + +class CleanupManager(ExactWeakKeyDictionary): + count = 0 + instance: ClassVar[CleanupManager] + + def _remove_id(self, idx: int) -> None: + for hook in self.values[idx]: + hook() + super()._remove_id(idx) + + +CleanupManager.instance = CleanupManager() + + +def clone_tensor(x: torch.Tensor) -> torch.Tensor: + """Clone the tensor and its gradient""" + y = x.clone().requires_grad_(x.requires_grad) + if x.is_leaf and x.grad is not None: + y.grad = x.grad.clone() + return y + + +def _copy_dynamo_attr(src: torch.Tensor, dst: torch.Tensor, attr: str) -> None: + """Copy a single dynamo attribute from src to dst, or remove it from dst if src doesn't have it.""" + if hasattr(src, attr): + setattr(dst, attr, getattr(src, attr).copy()) + elif hasattr(dst, attr): + delattr(dst, attr) + + +def copy_dynamo_tensor_attributes(src: torch.Tensor, dst: torch.Tensor) -> None: + """ + Copy dynamo-specific tensor attributes from src to dst. + These attributes are used for dynamic shape marking and must be preserved + when cloning or casting tensors. If src doesn't have an attribute but dst does, + the attribute is removed from dst. + """ + _copy_dynamo_attr(src, dst, "_dynamo_dynamic_indices") + _copy_dynamo_attr(src, dst, "_dynamo_unbacked_indices") + _copy_dynamo_attr(src, dst, "_dynamo_hint_overrides") + _copy_dynamo_attr(src, dst, "_dynamo_shape_ids") + _copy_dynamo_attr(src, dst, "_dynamo_strict_unbacked_indices") + _copy_dynamo_attr(src, dst, "_dynamo_weak_dynamic_indices") + + +def clone_input(x: torch.Tensor, *, dtype: torch.dtype | None = None) -> torch.Tensor: + """copy while preserving strides""" + # TODO: this is questionable + if is_fake(x): + # this func fails on fake tensors in __torch_dispatch__ + return x + + def torch_clone(x: torch.Tensor) -> torch.Tensor: + y = torch.clone(x) + if x.is_leaf: + y.requires_grad_(x.requires_grad) + if x.is_leaf and x.grad is not None: + y.grad = clone_input(x.grad, dtype=dtype) + copy_dynamo_tensor_attributes(x, y) + return y + + with torch.no_grad(): + if x.device.type == "xla": + # Access data_ptr() for a xla tensor will cause crash + return torch_clone(x) + + # Handle sparse storage (no stride). + if x.layout is torch.sparse_coo: + return torch.sparse_coo_tensor( + torch_clone(x._indices()), + torch_clone(x._values()), + x.shape, + is_coalesced=x.is_coalesced(), + ) + elif is_sparse_compressed(x): + if x.layout in {torch.sparse_csr, torch.sparse_bsr}: + compressed_indices = x.crow_indices() + plain_indices = x.col_indices() + else: + compressed_indices = x.ccol_indices() + plain_indices = x.row_indices() + return torch.sparse_compressed_tensor( + torch_clone(compressed_indices), + torch_clone(plain_indices), + torch_clone(x.values()), + x.shape, + layout=x.layout, + ) + elif is_traceable_wrapper_subclass(x): + # Questionable - but this is required to not fail executorch related + # torchao tests. + return torch_clone(x) + + needed_size = sum( + (shape - 1) * stride for shape, stride in zip(x.size(), x.stride()) + ) + if x.is_quantized: + result = torch.empty_quantized((needed_size + 32,), x) + else: + result = torch.empty( + needed_size + 32, dtype=dtype or x.dtype, device=x.device + ) + cache_line_offset = ( + (x.data_ptr() - result.data_ptr()) % 32 + ) // x.element_size() + result.as_strided_(x.size(), x.stride(), cache_line_offset) + try: + result.copy_(x.clone()) + if x.is_leaf: + result.requires_grad_(x.requires_grad) + if x.is_leaf and x.grad is not None: + result.grad = clone_input(x.grad, dtype=dtype) + except RuntimeError: + # RuntimeError: unsupported operation: more than one element of the written-to + # tensor refers to a single memory location. Please clone() the tensor before + # performing the operation. + return torch_clone(x) + copy_dynamo_tensor_attributes(x, result) + return result + + +@overload +def clone_inputs( + example_inputs: dict[str, T | tuple[T, ...]], +) -> dict[str, list[T]]: ... + + +@overload +def clone_inputs(example_inputs: Sequence[T]) -> list[T]: ... + + +def clone_inputs(example_inputs: Any) -> Any: + res: dict[str, Any] | list[Any] + if type(example_inputs) is dict: + res = dict(example_inputs) + for key, value in res.items(): + if isinstance(value, tuple): + res[key] = clone_inputs(value) + else: + assert isinstance(value, torch.Tensor), type(value) + res[key] = clone_input(value) + return res + + res = list(example_inputs) + for i in range(len(res)): + if isinstance(res[i], torch.Tensor): + res[i] = clone_input(res[i]) + return res + + +def skip_frame_if_in_functorch_mode(val: torch.Tensor) -> None: + try: + val.data_ptr() # will throw for functorch tensors + except RuntimeError as e: + from .exc import unimplemented + + # This will be GradTrackingTensor/BatchedTensor/etc + functorch_subclass_name = re.sub(r"\(.*", "", repr(val)) + + unimplemented( + gb_type="skip frame due to being in functorh mode", + context="", + explanation=f"torch.compile cannot be run in context: {functorch_subclass_name}. Skipping frame.", + hints=[], + from_exc=e, + skip_frame=True, + ) + + +@contextmanager +def preserve_rng_state() -> Generator[None, None, None]: + disable_functorch = torch._C._DisableFuncTorch + disable_current_modes = torch.utils._python_dispatch._disable_current_modes + with disable_current_modes(), disable_functorch(): + rng_state = torch.clone(torch.random.get_rng_state()) + skip_frame_if_in_functorch_mode(rng_state) + if torch.cuda.is_available(): + cuda_rng_state = torch.clone(torch.cuda.get_rng_state()) + if torch.xpu.is_available(): + xpu_rng_state = torch.clone(torch.xpu.get_rng_state()) + try: + yield + finally: + with torch.utils._python_dispatch._disable_current_modes(): + torch.random.set_rng_state(rng_state) + if torch.cuda.is_available(): + torch.cuda.set_rng_state(cuda_rng_state) # type: ignore[possibly-undefined] + if torch.xpu.is_available(): + torch.xpu.set_rng_state(xpu_rng_state) # type: ignore[possibly-undefined] + + +def is_jit_model( + model0: Any, +) -> TypeIs[ + torch.jit._trace.TopLevelTracedModule + | torch.jit._script.RecursiveScriptModule + | torch.jit.ScriptFunction[Any, Any] + | torch.jit.ScriptModule +]: + return isinstance( + model0, + ( + torch.jit._trace.TopLevelTracedModule, + torch.jit._script.RecursiveScriptModule, + torch.jit.ScriptFunction, + torch.jit.ScriptModule, + ), + ) + + +def torchscript(model: Any, example_inputs: Any, verbose: bool = False) -> Any: + if is_jit_model(model): + # already done? + return model + + try: + return torch.jit.trace(model, example_inputs) + except Exception: + try: + return torch.jit.script(model) + except Exception: + if verbose: + log.exception("jit error") + else: + log.error("Both torch.jit.trace and torch.jit.script failed") + return None + + +def getfile(obj: Any) -> str | None: + try: + return inspect.getfile(obj) + except (TypeError, OSError): + return None + + +def is_namedtuple(obj: Any) -> bool: + """Test if an object is a namedtuple or a torch.return_types.* quasi-namedtuple""" + return is_namedtuple_cls(type(obj)) + + +def is_namedtuple_cls(cls: Any) -> bool: + """Test if an object is a namedtuple or a (torch.return_types|torch.autograd.forward_ad).* quasi-namedtuple""" + try: + if issubclass(cls, tuple): + module = getattr(cls, "__module__", None) + if module in ("torch.return_types", "torch.autograd.forward_ad"): + return True + if isinstance(getattr(cls, "_fields", None), tuple) and callable( + getattr(cls, "_make", None) + ): + # The subclassing style namedtuple can have an extra base `typing.Generic` + bases = tuple(t for t in cls.__bases__ if t is not Generic) + if bases == (tuple,): + # This is a namedtuple type directly created by `collections.namedtuple(...)` + return True + if bases and any( + ( + # Subclass of namedtuple + is_namedtuple_cls(t) + # For subclasses of namedtuple, the __new__ method should not be customized + and cls.__new__ is t.__new__ + ) + for t in bases + ): + return True + except TypeError: + pass + return False + + +@functools.lru_cache(1) +def namedtuple_fields(cls: type) -> tuple[str, ...]: + """Get the fields of a namedtuple or a torch.return_types.* quasi-namedtuple""" + if cls is slice: + return ("start", "stop", "step") + + assert issubclass(cls, tuple) + if hasattr(cls, "_fields"): + # normal namedtuples + return cls._fields + + @dataclasses.dataclass + class Marker: + index: int + + # frustrating ones e.g. torch.return_types.max + assert cls.__module__ == "torch.return_types" + obj = cls(map(Marker, range(cls.n_fields))) # type: ignore[attr-defined] + fields: dict[str, int] = {} + for name in dir(obj): + if name[0] != "_" and isinstance(getattr(obj, name), Marker): + fields[name] = getattr(obj, name).index + assert len(fields) == cls.n_fields # type: ignore[attr-defined] + return tuple(sorted(fields, key=fields.get)) # type: ignore[arg-type] + + +def checkpoint_params(gm: torch.fx.GraphModule) -> Callable[[], None]: + with torch.no_grad(): + rng_state = torch.clone(torch.random.get_rng_state()) + if torch.cuda.is_available(): + cuda_rng_state = torch.clone(torch.cuda.get_rng_state()) + saved_state = [ + (param, param._version, torch.clone(param)) + # pyrefly: ignore [bad-argument-type] + for param in itertools.chain(gm.parameters(), gm.buffers()) + ] + + def restore() -> None: + with torch.no_grad(): + torch.random.set_rng_state(rng_state) + if torch.cuda.is_available(): + torch.cuda.set_rng_state(cuda_rng_state) + for param, version, original_value in saved_state: + if param._version != version: + param.copy_(original_value) + + return restore + + +def timed( + model: Any, example_inputs: Iterable[Any], times: int = 1 +) -> tuple[Any, float]: + if torch.cuda.is_available(): + synchronize = torch.cuda.synchronize + else: + synchronize = nothing + + synchronize() + gc.collect() + torch.manual_seed(1337) + t0 = time.perf_counter() + for _ in range(times): + result = model(*example_inputs) + synchronize() + t1 = time.perf_counter() + return result, t1 - t0 # type: ignore[possibly-undefined] + + +def check_is_cuda(gm: torch.fx.GraphModule, example_inputs: Iterable[Any]) -> bool: + return all(x.is_cuda for x in itertools.chain(example_inputs, gm.parameters(True))) + + +@lru_cache(32) +def rot_n_helper(n: int) -> Callable[..., Any]: + assert n > 1 + vars = [f"v{i}" for i in range(n)] + rotated = reversed(vars[-1:] + vars[:-1]) + fn = eval(f"lambda {','.join(vars)}: ({','.join(rotated)})") + fn.__name__ = f"rot_{n}_helper" + return fn + + +common_constant_types: set[type] = { + int, + float, + complex, + bool, + str, + bytes, + type(None), + Ellipsis.__class__, + NotImplemented.__class__, + types.CodeType, + # Commonly used immutable types from torch. + torch.device, + torch.dtype, + torch.memory_format, + torch.layout, + torch.finfo, + torch.iinfo, + torch.nn.attention.SDPBackend, + torch.cuda._CudaDeviceProperties, + # Pytree key types (frozen dataclasses used in tree_map_with_path) + torch.utils._pytree.SequenceKey, + torch.utils._pytree.MappingKey, + torch.utils._pytree.GetAttrKey, +} + +if has_triton_package(): + import triton + + common_constant_types.add(triton.language.dtype) + +""" + Difference between is_safe_constant and common_constant_types. + * common_constant_types: Constants would be wrapped by VariableBuilder.wrap_literal + as ConstantVariable. + * is_safe_constant: Constants can be loaded by LOAD_CONST bytecode. +""" + + +def is_safe_constant(v: Any) -> bool: + if istype(v, (tuple, frozenset)): + return all(map(is_safe_constant, v)) + return isinstance( + v, + ( + enum.Enum, + type, + torch.Size, + typing._GenericAlias, # type: ignore[attr-defined] + types.GenericAlias, + ), + ) or istype( + v, + common_constant_types | {slice}, + ) + + +@functools.cache +def common_constants() -> set[int]: + return { + # We zero-one specialize shapes, so specialize these constants + # too + 0, + 1, + } + + +def is_torch_sym(value: Any) -> TypeGuard[torch.SymBool | torch.SymInt]: + return isinstance(value, (torch.SymBool, torch.SymInt)) and not isinstance( + value.node, torch.nested._internal.nested_int.NestedIntNode + ) + + +def is_int_specialization_case(value: Any, source: Any) -> bool: + from .source import is_from_defaults + + return not TracingContext.get().force_unspec_int_unbacked_size_like and ( + # Assume integers from global variables want to be specialized + not source.guard_source.is_local() + # Assume that integers that came from NN modules want to be + # specialized (as we don't expect users to be changing the + # NN modules on the fly), unless explicitly disabled + or ( + source.guard_source.is_specialized_nn_module() + and not config.allow_unspec_int_on_nn_module + ) + or ( + source.guard_source.is_unspecialized_builtin_nn_module() + and not config.allow_unspec_int_on_nn_module + ) + or ( + source.guard_source.is_unspecialized_nn_module() + and not config.allow_unspec_int_on_nn_module + ) + or is_from_defaults(source) + # TODO: Delete this condition when rollout is done. NB: this + # condition never evaluates True in open source + or ( + not justknobs_check("pytorch/dynamo:enable_unspecialize_zero_one_plain_int") + and value in common_constants() + ) + ) + + +def specialize_symnode(arg: Any) -> Any: + from .variables import ConstantVariable, LazyVariableTracker, SymNodeVariable + + # Guard and specialize + if isinstance(arg, LazyVariableTracker) and not arg.is_realized(): + # Find if the arg would be realized as SymNodeVariable later on. If yes, + # realize it and specialize. Else return the arg. + + source = arg.original_source() + value = arg.original_value() + + is_symnode_vt = is_torch_sym(value) or ( + not config.specialize_int + and type(value) is int + and not is_int_specialization_case(value, source) + ) + + if not is_symnode_vt: + return arg + + if isinstance(arg, SymNodeVariable): + return ConstantVariable.create(arg.evaluate_expr()) + return arg + + +def guard_if_dyn(arg: Any) -> Any: + from .variables import VariableTracker + + arg = specialize_symnode(arg) + + if isinstance(arg, VariableTracker) and arg.is_python_constant(): + return arg.as_python_constant() + + return arg + + +def check_constant_args(args: Iterable[Any], kwargs: Mapping[Any, Any]) -> bool: + return all(x.is_python_constant() for x in itertools.chain(args, kwargs.values())) + + +def check_unspec_python_args(args: Iterable[Any], kwargs: Mapping[Any, Any]) -> bool: + from .variables import VariableTracker + from .variables.tensor import UnspecializedPythonVariable + + unspec_count = 0 + for x in itertools.chain(args, kwargs.values()): + if isinstance(x, UnspecializedPythonVariable): + unspec_count += 1 + elif not (isinstance(x, VariableTracker) and x.is_python_constant()): + return False + return unspec_count > 0 + + +def check_unspec_or_constant_args( + args: Iterable[Any], kwargs: Mapping[Any, Any] +) -> bool: + # A fused version of: + # return check_constant_args(args, kwargs) or check_unspec_python_args(args, kwargs) + from .variables.tensor import UnspecializedPythonVariable + + for x in itertools.chain(args, kwargs.values()): + if not (x.is_python_constant() or isinstance(x, UnspecializedPythonVariable)): + return False + return True + + +def check_numpy_ndarray_args(args: Iterable[Any], kwargs: Mapping[Any, Any]) -> bool: + from .variables.tensor import NumpyNdarrayVariable + + return any( + isinstance(x, NumpyNdarrayVariable) + for x in itertools.chain(args, kwargs.values()) + ) + + +dict_keys: type[KeysView[Any]] = type({}.keys()) +dict_values: type[ValuesView[Any]] = type({}.values()) +dict_items: type[ItemsView[Any, Any]] = type({}.items()) +odict_values: type[ValuesView[Any]] = type(OrderedDict().values()) +# pyrefly: ignore [bad-assignment] +tuple_iterator: type[Iterator[Any]] = type(iter(())) +# pyrefly: ignore [bad-assignment] +range_iterator: type[Iterator[Any]] = type(iter(range(0))) +tuple_iterator_len = tuple_iterator.__length_hint__ # type: ignore[attr-defined] +object_new = object.__new__ +dict_new = dict.__new__ +dict_methods = { + method + for method in itertools.chain(dict.__dict__.values(), OrderedDict.__dict__.values()) + if callable(method) +} +set_methods = {method for method in set.__dict__.values() if callable(method)} +frozenset_methods = { + method for method in frozenset.__dict__.values() if callable(method) +} + +tuple_new = tuple.__new__ +tuple_methods = {method for method in tuple.__dict__.values() if callable(method)} +list_methods = {method for method in list.__dict__.values() if callable(method)} +list_getitem = list.__getitem__ + +str_methods = {method for method in str.__dict__.values() if callable(method)} + +# EnumType is the metaclass for Enum classes +enum_type_methods = { + method for method in type(enum.Enum).__dict__.values() if callable(method) +} + +K = TypeVar("K") +V = TypeVar("V") + + +def builtin_dict_keys(d: dict[K, V]) -> KeysView[K]: + # Avoids overridden keys method of the dictionary + assert isinstance(d, dict) + return dict.keys(d) + + +def get_items_from_dict(obj: dict[K, V]) -> Iterable[tuple[K, V | Any]]: + # Get items without calling the user defined __getitem__ or keys method. + assert isinstance(obj, dict) + if istype(obj, (dict, OrderedDict)): + return obj.items() + elif isinstance(obj, OrderedDict): + return [(k, OrderedDict.__getitem__(obj, k)) for k in OrderedDict.keys(obj)] + else: + return [(k, dict.__getitem__(obj, k)) for k in dict.keys(obj)] + + +def enumerate_items_with_dict_position( + obj: dict[K, V], +) -> Iterable[tuple[int, K, V | Any]]: + """Enumerate dict items yielding (dict_keys_position, key, value). + + For OrderedDicts where move_to_end/prepend has been used, the OrderedDict + iteration order can differ from dict.keys() order. We iterate in + OrderedDict order (correct execution semantics) but return each key's + dict.keys() position so that ConstDictKeySource indices stay consistent + with PyDict_Next / C++ DictGuardManager. + """ + items = get_items_from_dict(obj) + if isinstance(obj, OrderedDict): + key_to_pos = {k: i for i, k in enumerate(dict.keys(obj))} + return ((key_to_pos[k], k, v) for k, v in items) + return ((i, k, v) for i, (k, v) in enumerate(items)) + + +def nn_module_new(cls: Any) -> Any: + obj = object_new(cls) + torch.nn.Module.__init__(obj) + return obj + + +def product(it: Iterable[T]) -> int: + return functools.reduce(operator.mul, it, 1) + + +def tuple_iterator_getitem(it: Any, index: int) -> Any: + _, (obj,), start = it.__reduce__() + return obj[start + index] + + +def dataclass_fields(cls: Any) -> Any: + return torch._dynamo.disable(dataclasses.fields)(cls) + + +iter_next = next + + +def normalize_count_iter(count_iter: Iterator[Any]) -> tuple[Any, Any]: + try: + _, args = count_iter.__reduce__() + except TypeError: + # Python 3.14 no longer pickles itertools.count, so fall back to the + # repr and only recover literal arguments. Non-literal arguments still + # fall back to user-defined handling via the NotImplemented sentinel. + import ast + + count_repr = repr(count_iter) + if not count_repr.startswith("count(") or not count_repr.endswith(")"): + return (NotImplemented, NotImplemented) + try: + args = ast.literal_eval(f"({count_repr[6:-1]},)") + except (SyntaxError, ValueError): + return (NotImplemented, NotImplemented) + if not isinstance(args, tuple) or not 1 <= len(args) <= 2: + return (NotImplemented, NotImplemented) + if len(args) == 1: + return (args[0], 1) + return (args[0], args[1]) + + +def normalize_range_iter(range_iter: Any) -> tuple[int, int, int]: + _, (range_obj,), maybe_idx = range_iter.__reduce__() + # In 3.12+, `maybe_idx` could be None, and `range_obj.start` would've been + # already incremented by the current index. + # The index (maybe_idx) is the number of steps taken so far. To get the + # correct start value, one must add (maybe_idx * step) to the original + # start. See: + # https://github.com/python/cpython/blob/ea77feecbba389916af8f90b2fc77f07910a2963/Objects/rangeobject.c#L885-L899 + start = range_obj.start + (maybe_idx or 0) * range_obj.step + stop = range_obj.stop + step = range_obj.step + return (start, stop, step) + + +def to_subclass(t: Any, cls: type) -> Any: + return t.as_subclass(cls) + + +dict_getitem = dict.__getitem__ + + +@torch.fx.wrap +def dict_keys_getitem(d: dict[Any, Any], n: int) -> Any: + # Use dict.keys() to match the iteration order of PyDict_Next used by + # the C++ DictGuardManager and by ConstDictKeySource._name_template. + # pyrefly: ignore [bad-argument-type] + return next(itertools.islice(dict.keys(d), n, n + 1)) + + +def set_getitem(s: set[T], n: int) -> T: + # Set ordering might not be stable + return list(s)[n] + + +def enum_repr(value: Any, local: bool) -> str: + # enum class can override __str__ method. Use __class__ and name attribute + # to extract the class name and key name. + name = value.__class__.__name__ + val = value.name + scope = "L" if local else "G" + local_name = f'{scope}["{name}"].{val}' + return local_name + + +def set_example_value(node: torch.fx.Node, example_value: Any) -> None: + # NB: example_value is a bit of a misnomer, because this is always a fake + # tensor of some sort. Furthermore, these example values serve as the + # runtime state of Dynamo tracing, which means if metadata mutation + # occurs, the example_value gets directly updated (so you can't rely on + # this to accurately reflect what the state of the value was at the time + # the program was traced). + node.meta["example_value"] = example_value + fake_mode = TracingContext.get().fake_mode + assert fake_mode is not None + shape_env = fake_mode.shape_env + if ( + symbol_to_path + := torch.fx.experimental.symbolic_shapes.compute_unbacked_bindings( + shape_env, example_value + ) + ): + node.meta["unbacked_bindings"] = symbol_to_path + + +def _get_fake_tensor(vt: VariableTracker) -> Any: + fake_tensor = vt.as_proxy().node.meta.get("example_value") + if not is_fake(fake_tensor): + from . import graph_break_hints + from .exc import unimplemented + + unimplemented( + gb_type="Cannot check Tensor object identity without its fake value", + context=str(fake_tensor), + explanation="TensorVariable is missing a fake example_value.", + hints=[*graph_break_hints.DYNAMO_BUG], + ) + return fake_tensor + + +def slice_length(s: slice, seq_len: int) -> int: + start, stop, step = s.indices(seq_len) + return max(0, (stop - start + (step - (1 if step > 0 else -1))) // step) + + +def raise_args_mismatch( + tx: InstructionTranslatorBase, + name: str, + expect: str = "", + actual: str = "", +) -> None: + from torch._dynamo.exc import raise_observed_exception + + msg_str = ( + f"wrong number of arguments or keyword arguments for {name}() call.\n" + f" Expect: {expect}\n" + f" Actual: {actual}" + ) + + raise_observed_exception( + TypeError, + tx, + args=[msg_str], + ) + + +def iter_contains( + items: Iterable[Any], + search: Any, + tx: InstructionTranslator, + check_tensor_identity: bool = False, +) -> Any: + from .variables import ConstantVariable + + if search.is_python_constant(): + found_const = any( + x.is_python_constant() + and x.as_python_constant() == search.as_python_constant() + for x in items + ) + return ConstantVariable.create(found_const) + + must_check_tensor_id = False + if check_tensor_identity and search.is_tensor(): + must_check_tensor_id = True + # Match of Tensor means match of FakeTensor + search = _get_fake_tensor(search) + + found: VariableTracker | None = None + for x in items: + if must_check_tensor_id: + if x.is_tensor(): + if search is _get_fake_tensor(x): # Object equivalence + return ConstantVariable.create(True) + else: + from torch._dynamo.variables.builder import SourcelessBuilder + + check = SourcelessBuilder.create(tx, operator.eq).call_function( + tx, [x, search], {} + ) + if found is None: + found = check + else: + found = SourcelessBuilder.create(tx, operator.or_).call_function( + tx, [check, found], {} + ) + if found is None: + found = ConstantVariable.create(False) + return found + + +def key_is_id( + k: Any, +) -> TypeIs[torch.Tensor | torch.nn.Module | MethodWrapperType]: + """Returns whether it indexes dictionaries using its id""" + return isinstance(k, (torch.Tensor, torch.nn.Module, MethodWrapperType)) + + +def key_to_id(value: Any) -> list[Any]: + return [id(k) if key_is_id(k) else k for k in value] + + +def const_repr(x: Any, *, local: Any) -> str: + from .trace_rules import is_builtin_callable + + if isinstance(x, (list, tuple)): + elems_repr = ",".join(const_repr(s, local=local) for s in x) + if isinstance(x, list): + return f"[{elems_repr}]" + else: + assert isinstance(x, tuple) + if len(x) == 1: + return f"({elems_repr},)" + else: + return f"({elems_repr})" + elif isinstance(x, enum.Enum): + # To workaround repr(Enum) returning invalid global reference before python 3.11 + # by calling enum_repr and removing quotes to render enum in guard code. + return enum_repr(x, local=local).replace("'", "") + elif is_builtin_callable(x): + return x.__name__ + elif isinstance(x, type): + + def fullname(o: Any) -> str: + klass = o.__class__ + module = klass.__module__ + if module == "builtins": + return klass.__qualname__ # avoid outputs like 'builtins.str' + return module + "." + klass.__qualname__ + + return fullname(x) + else: + return f"{x!r}" + + +def dict_keys_repr(const_keys: Any, *, local: Any) -> str: + keys_str = ",".join(const_repr(s, local=local) for s in const_keys) + return "[" + keys_str + "]" + + +GLOBAL_KEY_PREFIX = "__dict_key" + + +from torch._subclasses import UnsupportedFakeTensorException # noqa: F401 + + +def get_safe_global_name(tx: InstructionTranslatorBase, root: str, obj: Any) -> str: + # The global_mangled_class_name should be different for different + # invocations of torch.compile. Otherwise, we can run into a situation + # where multiple torch.compile invocations reuse the same global name, + # but the global's lifetime is tied to the first invocation (and + # may be deleted when the first torch.compile invocation is deleted) + # We mangle it based off of the output_graph's id. + return f"{root}_{id(obj)}_c{tx.output.compile_id}" + + +def is_in(item: T, *containers: Container[T]) -> bool: + for container in containers: + if item in container: + return True + return False + + +def get_unique_name_wrt( + prefix: str, *containers: Any, requires_suffix: bool = False +) -> str: + """ + Return a name that starts with `prefix` and is not in any of the + `containers` (e.g., map, set). + """ + if not requires_suffix and not is_in(prefix, *containers): + return prefix + + for i in itertools.count(): + candidate = f"{prefix}_{i}" + if not is_in(candidate, *containers): + return candidate + + raise AssertionError("unreachable") + + +def wrap_fake_exception(fn: Callable[[], Any]) -> Any: + try: + return fn() + except UnsupportedFakeTensorException as e: + from .exc import unimplemented + + msg = f"Encountered exception ({e.reason}) during fake tensor propagation." + log.warning(msg) + unimplemented( + gb_type="Fake tensor propagation exception", + context=str(e.reason), + explanation=msg, + hints=[], + from_exc=e, + ) + + +def deepcopy_to_fake_tensor( + obj: Any, fake_mode: torch._subclasses.fake_tensor.FakeTensorMode +) -> Any: + with torch._subclasses.fake_tensor.FakeCopyMode(fake_mode): + return wrap_fake_exception(lambda: copy.deepcopy(obj)) + + +def rmse(ref: torch.Tensor, res: torch.Tensor) -> torch.Tensor: + """ + Calculate root mean squared error + """ + return torch.sqrt(torch.mean(torch.square(ref - res))) + + +def bitwise_same(ref: Any, res: Any, equal_nan: bool = False) -> bool: + return same( + ref, + res, + tol=0.0, + equal_nan=equal_nan, + ) + + +def same( + ref: Any, + res: Any, + fp64_ref: Any = None, + cos_similarity: bool = False, + tol: float = 1e-4, + equal_nan: bool = False, + exact_dtype: bool = True, + relax_numpy_equality: bool = False, + ignore_non_fp: bool = False, + log_error: Callable[..., None] = log.error, + use_larger_multiplier_for_smaller_tensor: bool = False, + force_max_multiplier: bool = False, + use_iou_for_bool: bool = False, + iou_threshold: float = 0.99, +) -> bool: + """Check correctness to see if ref and res match""" + if fp64_ref is None: + fp64_ref = ref + if isinstance( + ref, (list, tuple, collections.deque, torch.nn.ParameterList, torch.Size) + ): + assert isinstance(res, (list, tuple, collections.deque)), ( + f"type mismatch {type(ref)} {type(res)}" + ) + if len(ref) != len(res): + log_error("Length mismatch") + return False + return len(ref) == len(res) and all( + same( + ai, + bi, + fp64_refi, + cos_similarity, + tol, + equal_nan, + exact_dtype, + relax_numpy_equality, + ignore_non_fp, + log_error=log_error, + use_larger_multiplier_for_smaller_tensor=use_larger_multiplier_for_smaller_tensor, + force_max_multiplier=force_max_multiplier, + use_iou_for_bool=use_iou_for_bool, + iou_threshold=iou_threshold, + ) + for ai, bi, fp64_refi in zip(ref, res, fp64_ref) + ) + elif type(ref).__name__ == "QuestionAnsweringModelOutput": + # This skips checking accuracy for start_logits/end_logits. + # Tentatively, start_logits/end_logits appear to be very prone to + # inaccuracies and is somewhat subsumed by checking the loss. + return same( + ref.loss, + res.loss, + fp64_ref.loss, + cos_similarity, + tol, + equal_nan, + exact_dtype, + relax_numpy_equality, + ignore_non_fp, + log_error=log_error, + use_larger_multiplier_for_smaller_tensor=use_larger_multiplier_for_smaller_tensor, + force_max_multiplier=force_max_multiplier, + use_iou_for_bool=use_iou_for_bool, + iou_threshold=iou_threshold, + ) + elif isinstance(ref, dict): + assert isinstance(res, dict) + assert set(ref.keys()) == set(res.keys()), ( + f"keys mismatch {set(ref.keys())} == {set(res.keys())}" + ) + for k in sorted(ref.keys()): + if not ( + same( + ref[k], + res[k], + fp64_ref[k], + cos_similarity=cos_similarity, + tol=tol, + equal_nan=equal_nan, + exact_dtype=exact_dtype, + relax_numpy_equality=relax_numpy_equality, + ignore_non_fp=ignore_non_fp, + log_error=log_error, + use_larger_multiplier_for_smaller_tensor=use_larger_multiplier_for_smaller_tensor, + force_max_multiplier=force_max_multiplier, + use_iou_for_bool=use_iou_for_bool, + iou_threshold=iou_threshold, + ) + ): + log_error("Accuracy failed for key name %s", k) + return False + return True + elif isinstance(ref, set): + assert isinstance(res, set) + assert set(ref) == set(res), f"elements mismatch {set(ref)} == {set(res)}" + return True + elif isinstance(ref, (torch.Tensor, float)): + assert not isinstance(ref, torch._subclasses.FakeTensor) + assert not isinstance(res, torch._subclasses.FakeTensor) + + def to_tensor(t: Any) -> torch.Tensor: + return t if isinstance(t, torch.Tensor) else torch.tensor(t) + + ref, res, fp64_ref = (to_tensor(val) for val in (ref, res, fp64_ref)) + + if ref.is_sparse: + assert res.is_sparse + ref = ref.to_dense() + res = res.to_dense() + assert isinstance(res, torch.Tensor), f"type mismatch {type(ref)} {type(res)}" + if exact_dtype: + if ref.dtype != res.dtype: + log_error("dtype mismatch %s, %s", ref.dtype, res.dtype) + return False + if ref.dtype == torch.bool: + if ignore_non_fp: + return True + if use_iou_for_bool: + # Use IoU (Intersection over Union) metric for boolean mask comparison. + # This is useful for segmentation models where small floating-point + # differences get thresholded into boolean masks. + intersection = (ref & res).sum().float() + union = (ref | res).sum().float() + if union == 0: + # Both masks are empty + return bool(intersection == 0) + iou = (intersection / union).item() + if iou < iou_threshold: + log_error( + "IoU accuracy failed: %.4f < %.2f (intersection=%d, union=%d, ref_sum=%d, res_sum=%d, shape=%s)", + iou, + iou_threshold, + int(intersection.item()), + int(union.item()), + int(ref.sum().item()), + int(res.sum().item()), + list(ref.shape), + ) + return False + return True + # triton stores bool as int8, so add this for more accurate checking + r = torch.allclose( + ref.to(dtype=torch.uint8), + res.to(dtype=torch.uint8), + atol=tol, + rtol=tol, + equal_nan=equal_nan, + ) + if not r: + log_error("Accuracy failed: uint8 tensor did not match") + return r + + if cos_similarity: + ref = ref.flatten().to(torch.float32) + res = res.flatten().to(torch.float32) + if torch.allclose(ref, res, atol=tol, rtol=tol, equal_nan=True): + # early exit that handles zero/nan better + # cosine_similarity(zeros(10), zeros(10), dim=0) is 0 + return True + score = torch.nn.functional.cosine_similarity(ref, res, dim=0, eps=1e-6) + if score < 0.99: + log.warning("Similarity score=%s", score.detach().cpu().item()) + return bool(score >= 0.99) + else: + if not exact_dtype: + ref = ref.to(res.dtype) + + # First try usual allclose + if torch.allclose(ref, res, atol=tol, rtol=tol, equal_nan=equal_nan): + return True + + # Check error from fp64 version + if fp64_ref.dtype == torch.float64: + # Fix a corner case that res and fp64_ref does not contains NaN and match (with loose tolerance) + # while the ref contains NaN. In this case, RMSE should not match any ways. + # But res is 'BETTER' than ref so we count it pass. + # + # This happens for Super_SloMo when loop ordering after fusion is enabled: + # https://gist.github.com/shunting314/11f235c70f7db0d52718d26f4a701cab + loose_tol = 1e-2 * 4 + if ( + not fp64_ref.isnan().any() + and not res.isnan().any() + and ref.isnan().any() + and torch.allclose( + fp64_ref.to(dtype=res.dtype), + res, + atol=loose_tol, + rtol=loose_tol, + equal_nan=equal_nan, + ) + ): + return True + ref_error = rmse(fp64_ref, ref).item() + # ref unable to produce this with stable numerics in this precision, ignore + if math.isnan(ref_error): + log.warning( + "Found nan in reference. Consider running in higher precision." + ) + + res_error = rmse(fp64_ref, res).item() + + def get_multiplier() -> float: + # In some particular cases, we expect high difference in results. + # At the moment one of this cases is inductor freezing bfloat16 convolution const folding. + # In case of it the res_error is at least one order of magnitude higher. + if force_max_multiplier: + return 10.0 + # In the case of using AMP (Automatic Mixed Precision), certain models have + # failed the benchmark's correctness check. However, the end-to-end model's + # accuracy when comparing AMP with FP32 is within a difference of less than 0.1%. + # Thus, it's possible that the correctness check failures for these models are + # false alarms. We use multiplier of 3 instead of 2 to avoid these false alarms. + multiplier = ( + 3.0 if res.dtype in (torch.float16, torch.bfloat16) else 2.0 + ) + + if use_larger_multiplier_for_smaller_tensor and ( + fp64_ref.numel() <= 10 + ): + multiplier = 10.0 + elif use_larger_multiplier_for_smaller_tensor and ( + fp64_ref.numel() <= 500 + ): + multiplier = 8.0 + elif ( + fp64_ref.numel() < 1000 + or (ref.ndim == 4 and ref.shape[-1] == ref.shape[-2] == 1) + # large tol means a benchmark has been specified as REQUIRE_HIGHER_TOLERANCE + or tol >= 2 * 1e-2 + ): + # In the presence of noise, noise might dominate our error + # metric for smaller tensors. + # Similarly, for 1x1 kernels, there seems to be high noise with amp. + multiplier = 3.0 + return multiplier + + multiplier = get_multiplier() + + passes_test = res_error <= (multiplier * ref_error + tol / 10.0) + if ( + not passes_test + and equal_nan + and math.isnan(ref_error) + and math.isnan(res_error) + # Some unit test for the accuracy minifier relies on + # returning false in this case. + and not torch._inductor.config.cpp.inject_relu_bug_TESTING_ONLY + ): + passes_test = True + if not passes_test: + log_error( + "RMSE (res-fp64): %.5f, (ref-fp64): %.5f and shape=%s. res.dtype: %s, multiplier: %f, tol: %f" + ", use_larger_multiplier_for_smaller_tensor: %d", + res_error, + ref_error, + res.size(), + res.dtype, + multiplier, + tol, + use_larger_multiplier_for_smaller_tensor, + ) + return passes_test + + if ignore_non_fp: + return True + + log_error("Accuracy failed: allclose not within tol=%s", tol) + return False + elif isinstance(ref, (str, int, type(None), bool, torch.device)): + if ignore_non_fp: + return True + r = ref == res + if not r: + log_error("Accuracy failed (%s): %s != %s", type(ref), ref, res) + return r + elif is_numpy_int_type(ref) or is_numpy_float_type(ref): + if relax_numpy_equality and not ( + is_numpy_int_type(res) or is_numpy_float_type(res) + ): + ref = ref.item() + r = (type(ref) is type(res)) and (ref == res) + if not r: + log_error("Accuracy failed (numpy): %s != %s", ref, res) + return r + elif is_numpy_ndarray(ref): + return (type(ref) is type(res)) and same( + torch.as_tensor(ref), + torch.as_tensor(res), + fp64_ref, + cos_similarity=cos_similarity, + tol=tol, + equal_nan=equal_nan, + exact_dtype=exact_dtype, + relax_numpy_equality=relax_numpy_equality, + ignore_non_fp=ignore_non_fp, + log_error=log_error, + use_larger_multiplier_for_smaller_tensor=use_larger_multiplier_for_smaller_tensor, + ) + elif type(ref).__name__ in ( + "MaskedLMOutput", + "Seq2SeqLMOutput", + "CausalLMOutputWithCrossAttentions", + "LongformerMaskedLMOutput", + "Instances", + "SquashedNormal", + "Boxes", + "Normal", + "TanhTransform", + "Foo", + "Variable", + ): + assert type(ref) is type(res) + return all( + same( + getattr(ref, key), + getattr(res, key), + getattr(fp64_ref, key), + cos_similarity=cos_similarity, + tol=tol, + equal_nan=equal_nan, + exact_dtype=exact_dtype, + relax_numpy_equality=relax_numpy_equality, + ignore_non_fp=ignore_non_fp, + log_error=log_error, + use_larger_multiplier_for_smaller_tensor=use_larger_multiplier_for_smaller_tensor, + ) + for key in ref.__dict__ + ) + else: + raise RuntimeError(f"unsupported type: {type(ref).__name__}") + + +def format_func_info(code: CodeType) -> str: + short_filename = code.co_filename.split("/")[-1] + return f"'{code.co_name}' ({short_filename}:{code.co_firstlineno})" + + +@contextlib.contextmanager +def disable_cache_limit() -> Generator[None, None, None]: + prior = config.recompile_limit + # pyrefly: ignore [bad-assignment] + config.recompile_limit = sys.maxsize + prior_acc_limit = config.accumulated_recompile_limit + # pyrefly: ignore [bad-assignment] + config.accumulated_recompile_limit = sys.maxsize + + try: + yield + finally: + config.recompile_limit = prior + config.accumulated_recompile_limit = prior_acc_limit + + +# map from transformed code back to original user code +orig_code_map = ExactWeakKeyDictionary() + +# keep a record of code_obj -> list of guard failure reasons for logging +guard_failures: collections.defaultdict[Any, list[Any]] = collections.defaultdict(list) + +# Keep a record of graph break reasons for logging +graph_break_reasons: list[torch._dynamo.output_graph.GraphCompileReason] = [] + +# keep record of compiled code, if we are in "error if recompile" +# to track code that dynamo has compiled previously +seen_code_map = ExactWeakKeyDictionary() + + +# return same dir unless user changes config between calls +@functools.cache +def _get_debug_dir(root_dir: str) -> str: + dir_name = ( + "run_" + + datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S_%f") + # use pid to avoid conflicts among ranks + + "-pid_" + + str(os.getpid()) + ) + return os.path.join(root_dir, dir_name) + + +def get_debug_dir() -> str: + debug_root = config.debug_dir_root + return _get_debug_dir(debug_root) + + +def extract_fake_example_value(node: torch.fx.Node, required: bool = True) -> Any: + if "example_value" in node.meta and is_fake(node.meta["example_value"]): + return node.meta["example_value"] + elif required: + from torch._dynamo.exc import unimplemented + + from . import graph_break_hints + + unimplemented( + gb_type="Missing FakeTensor example value", + context=str(node), + explanation=f"`FakeTensor` example value was required for {node} but not available.", + hints=[*graph_break_hints.DYNAMO_BUG], + ) + else: + return None + + +def ensure_graph_fake(e: Any, tx: InstructionTranslatorBase) -> Any: + assert maybe_get_fake_mode(e) is tx.fake_mode + return e + + +def get_fake_values_from_nodes( + tx: InstructionTranslatorBase, nodes: Any, allow_non_graph_fake: bool +) -> Any: + def visit(n: torch.fx.Node) -> Any: + if n.op == "call_function" and "example_value" not in n.meta: + # fake tensor validity is checked inside get_fake_value using + # ensure_graph_fake + return get_fake_value(n, tx, allow_non_graph_fake) + + elif n.op == "get_attr" and "example_value" not in n.meta: + assert n.target in tx.output.nn_modules + gm = tx.output.nn_modules[n.target] # type: ignore[index] + assert isinstance(gm, torch.fx.GraphModule) + return gm + + out = n.meta["example_value"] + if not allow_non_graph_fake and isinstance(out, torch.Tensor): + return ensure_graph_fake(out, tx) + return out + + return torch.fx.node.map_arg(nodes, visit) + + +def get_concrete_sizes_from_symints(msg: str, fake_mode: FakeTensorMode | None) -> str: + """ + Replace symbolic size expressions (like 's0', 's94') in error messages + with their concrete runtime values for better readability. + + Example: "size (s94)" -> "size (s94: hint= 10)" if s94's value is 10. + """ + import re + + from sympy.core.numbers import Integer + + if fake_mode is None: + return msg + + pattern = r"\(s(\d+)\)" + assert fake_mode.shape_env is not None + shape_env = fake_mode.shape_env + backed_var_to_val = shape_env.backed_var_to_val + + def replace_sym(match: Any) -> str: + sym_name = f"s{match.group(1)}" + val = next( + (v for k, v in backed_var_to_val.items() if k.name == sym_name), + None, + ) + if isinstance(val, (int, Integer)): + return f"({sym_name}: hint = {str(val)})" + return match.group(0) + + msg = re.sub(pattern, replace_sym, msg) + return msg + + +def _wrap_graph_break_with_torch_runtime_err(gb_fn: Callable[[], NoReturn]) -> NoReturn: + from .exc import TorchRuntimeError, Unsupported + + try: + gb_fn() + except Unsupported as e: + exc = TorchRuntimeError(str(e), getattr(e, "real_stack", None)) + raise exc.with_traceback(e.__traceback__) from None + raise AssertionError("should be unreachable") + + +def get_fake_value( + node: torch.fx.Node, + tx: InstructionTranslatorBase, + allow_non_graph_fake: bool = False, +) -> Any: + _t0 = time.time_ns() + try: + return _get_fake_value_impl(node, tx, allow_non_graph_fake) + finally: + tx.output.bytecode_tracing_timings.get_fake_value_ns += time.time_ns() - _t0 + + +def _get_fake_value_impl( + node: torch.fx.Node, + tx: InstructionTranslatorBase, + allow_non_graph_fake: bool = False, +) -> Any: + """ + Run the computation represented by `node` using fake tensors and return the result. + + allow_non_graph_fake: whether to allow the return result to be: + 1. non-fake or 2. fake that is not created by this instance of Dynamo. + If `True`, you must be prepared to deal with such return values, ideally + by further wrapping them as this graph's fakes. + """ + from torch.utils._sympy.value_ranges import ValueRangeError + + from . import graph_break_hints + from .exc import unimplemented, Unsupported, UserError, UserErrorType + + op = node.op + + # FX Node should always return the same fake value + if "example_value" in node.meta and is_fake(node.meta["example_value"]): + return node.meta["example_value"] + + args, kwargs = get_fake_values_from_nodes( + tx, (node.args, node.kwargs), allow_non_graph_fake + ) + + if ( + torch._dynamo.config.use_graph_deduplication + or torch._dynamo.config.track_nodes_for_deduplication + ): + flat_args_kwargs = get_fake_values_from_nodes( + tx, _get_flat_args(node, {}), allow_non_graph_fake + ) + id_to_initial_version = { + id(arg): arg._version for arg in flat_args_kwargs if is_fake(arg) + } + else: + # pyrefly: ignore [implicit-any] + flat_args_kwargs = [] + # pyrefly: ignore [implicit-any] + id_to_initial_version = {} + + nnmodule = None + fake_mode = tx.fake_mode + assert fake_mode is not None + if op == "call_method" and len(args) > 0 and isinstance(args[0], torch.nn.Module): + # If the first argument is nn.Module, should copy to fake mode. + args = (deepcopy_to_fake_tensor(args[0], fake_mode),) + tuple(args[1:]) + + if op == "call_module": + nnmodule = tx.output.nn_modules[node.target] # type: ignore[index] + + if is_lazy_module(nnmodule) and hasattr(nnmodule, "_initialize_hook"): + # In the case of a lazy module, we want to run + # the pre-hooks which initialize it. + # Afterwards, lazy module deletes its pre-hooks + # to avoid treating it as lazy on subsequent recompile. + nnmodule._infer_parameters(nnmodule, args) + + # no matter it's lazy module or not, we should copy to fake mode. + nnmodule = deepcopy_to_fake_tensor(nnmodule, fake_mode) + + if node.name in ["interpolate", "is_integer", "wrapped_gradient"] or any( + isinstance(a, complex) for a in args + ): + # We need to specialize symfloats for now. Eventually we should do a tensorify pass in dynamo. + args = tuple( + ( + float(arg) + if isinstance(arg, torch.SymFloat) and arg.node.hint is not None + else arg + ) + for arg in args + ) + + try: + with fake_mode, enable_python_dispatcher(): + ret_val = wrap_fake_exception( + lambda: run_node(tx.output, node, args, kwargs, nnmodule) + ) + except Unsupported: + raise + except RuntimeError as e: + cause: BaseException = e + if e.__cause__ is not None: + cause = e.__cause__ + + if isinstance( + cause, torch._subclasses.fake_tensor.DataDependentOutputException + ): + # capture_scalar_outputs only works for these ops right now + # see torch/_subclasses/fake_impls.py + if cause.func in ( + torch.ops.aten.item.default, + torch.ops.aten._local_scalar_dense.default, + ): + # does this actually get triggered? + hints = [ + "Enable tracing of data-dependent output operators with " + "`torch._dynamo.config.capture_scalar_outputs = True`", + ] + else: + hints = [ + "Consider wrapping the operator into a PyTorch-understood custom operator " + "(see https://pytorch.org/tutorials/advanced/custom_ops_landing_page.html)", + ] + unimplemented( + gb_type="Data dependent operator", + context=str(cause.func), + explanation=f"Operator `{cause.func}` has a non-Tensor output " + "whose value is dependent on the data of Tensor inputs.", + hints=hints, + from_exc=cause, + ) + elif isinstance( + cause, torch._subclasses.fake_tensor.DynamicOutputShapeException + ): + if not torch._dynamo.config.capture_dynamic_output_shape_ops: + unimplemented( + gb_type="Dynamic shape operator", + context=str(cause.func), + explanation=f"Operator `{cause.func}`'s output shape depends on input Tensor data.", + hints=[ + "Enable tracing of dynamic shape operators with " + "`torch._dynamo.config.capture_dynamic_output_shape_ops = True`", + ], + from_exc=cause, + ) + else: + unimplemented( + gb_type="Dynamic shape operator (no meta kernel)", + context=str(cause.func), + explanation=f"Operator `{cause.func}` does not have a meta kernel that supports dynamic output shapes", + hints=[ + "Please report an issue to PyTorch", + ], + from_exc=cause, + ) + elif isinstance( + cause, torch._subclasses.fake_tensor.UnsupportedOperatorException + ): + op = cause.func # type: ignore[assignment] + import_suggestion = "" + if isinstance(op, torch._ops.OpOverload): + maybe_pystub = torch._C._dispatch_pystub( + op._schema.name, op._schema.overload_name + ) + if maybe_pystub is not None: + module, ctx = maybe_pystub + import_suggestion = ( + f"It's possible that the support was implemented in " + f"module `{module}` and you may need to `import {module}`" + f"({ctx}), otherwise " + ) + unimplemented( + gb_type="Operator does not support running with fake tensors", + context=f"unsupported operator: {cause.func}", + explanation="", + hints=[ + f"{import_suggestion}see " + "https://docs.google.com/document/d/1GgvOe7C8_NVOMLOCwDaYV1mXXyHMXY7ExoewHqooxrs/edit#heading=h.64r4npvq0w0" + " for how to fix", + ], + from_exc=cause, + ) + elif isinstance( + cause, torch.fx.experimental.symbolic_shapes.GuardOnDataDependentSymNode + ): + raise UserError( + UserErrorType.CONSTRAINT_VIOLATION, + str(cause), + case_name="constrain_as_size_example", + ) from cause + elif isinstance(cause, ValueRangeError): + raise UserError(UserErrorType.CONSTRAINT_VIOLATION, e.args[0]) from e + elif isinstance(cause, TypeError) and "argument" in str(cause): + unimplemented( + gb_type="TypeError when making fake tensor call", + context=f"TypeError {node.target}: {cause}", + explanation="", + hints=[*graph_break_hints.USER_ERROR], + from_exc=cause, + ) + msg = get_concrete_sizes_from_symints(str(e), fake_mode) + _wrap_graph_break_with_torch_runtime_err( + lambda: unimplemented( + gb_type="RuntimeError when making fake tensor call", + context="", + explanation=msg, + hints=[*graph_break_hints.USER_ERROR], + from_exc=cause, + ) + ) + raise AssertionError("should not reachable") from None + + if not allow_non_graph_fake: + _ = pytree.tree_map_only( + torch.Tensor, functools.partial(ensure_graph_fake, tx=tx), ret_val + ) + + if ( + torch._dynamo.config.use_graph_deduplication + or torch._dynamo.config.track_nodes_for_deduplication + ): + tx.output.region_tracker.track_node_mutations( + node, + flat_args_kwargs, + id_to_initial_version, + ) + + return ret_val + + +_current_node = threading.local() + + +def get_current_node() -> torch.fx.Node | None: + return getattr(_current_node, "value", None) + + +@contextmanager +def set_current_node(node: torch.fx.Node) -> Generator[None, None, None]: + old = get_current_node() + _current_node.value = node + try: + yield + finally: + _current_node.value = old + + +def run_node( + tracer: Any, node: torch.fx.Node, args: Any, kwargs: Any, nnmodule: Any +) -> Any: + """ + Runs a given node, with the given args and kwargs. + + Behavior is dictated by a node's op. + + run_node is useful for extracting real values out of nodes. + See get_real_value for more info on common usage. + + Note: The tracer arg is only used for 'get_attr' ops + Note: The nnmodule arg is only used for 'call_module' ops + + Nodes that are not call_function, call_method, call_module, or get_attr will + raise an AssertionError. + """ + op = node.op + + with set_current_node(node): + + def make_error_message(e: Any) -> str: + return ( + f"Dynamo failed to run FX node with fake tensors: {op} {node.target}(*{args}, **{kwargs}): got " + + repr(e) + ) + + from .exc import Unsupported + + try: + if op == "call_function": + return node.target(*args, **kwargs) # type: ignore[operator] + elif op == "call_method": + if not hasattr(args[0], node.target): # type: ignore[arg-type] + from . import graph_break_hints + from .exc import unimplemented + + unimplemented( + gb_type="Missing attribute when running call_method node", + context="", + explanation=make_error_message("attribute not defined"), + hints=[*graph_break_hints.USER_ERROR], + ) + return getattr(args[0], node.target)(*args[1:], **kwargs) # type: ignore[arg-type] + elif op == "call_module": + assert nnmodule is not None + return nnmodule(*args, **kwargs) + elif op == "get_attr": + return tracer.output_graph.get_submodule(node.target) + elif op == "placeholder": + assert "example_value" in node.meta + return node.meta["example_value"] + + except (NotImplementedError, UnsupportedFakeTensorException) as e: + # NB: mimic how wrap_fake_exception does it + from . import graph_break_hints + from .exc import unimplemented + + hints = [*graph_break_hints.USER_ERROR] + if isinstance(e, NotImplementedError): + hints += [ + "If the op is a custom op, did you implement a fake tensor implementation? " + "(e.g. with `@my_custom_op.register_fake`)", + "If the op is a PyTorch op, please file an issue to PyTorch.", + ] + + unimplemented( + gb_type="NotImplementedError/UnsupportedFakeTensorException when running FX node", + context="", + explanation=make_error_message(e), + hints=hints, + from_exc=e, + ) + except Unsupported: + raise + except Exception as e: + raise RuntimeError(make_error_message(e)).with_traceback( + e.__traceback__ + ) from e + + raise AssertionError(op) + + +def get_real_value(node: torch.fx.Node, tracer: Any) -> Any: + """ + Run the actual computation represented by `node` and return the result. + This will execute any dependent nodes in the graph as well. + """ + from . import graph_break_hints + from .exc import unimplemented + + cache = tracer.real_value_cache + if node in cache: + return cache[node] + + op = node.op + args, kwargs = torch.fx.node.map_arg( # type: ignore[misc] + (node.args, node.kwargs), + lambda n: get_real_value(n, tracer), + ) + + if op == "placeholder" and "grapharg" in node.meta: + return node.meta["grapharg"].example + + if op == "call_module": + nn_module = tracer.output_graph.nn_modules[node.target] + if not is_lazy_module(nn_module): + nn_module = copy.deepcopy(nn_module) + else: + # In the case of a lazy module, we want to run + # the pre-hooks which initialize it + nn_module(*args, **kwargs) + else: + nn_module = None + + try: + real_value = run_node(tracer, node, args, kwargs, nn_module) + cache[node] = real_value + except RuntimeError as e: + exn = e # to make typing happy for the lambda + _wrap_graph_break_with_torch_runtime_err( + lambda: unimplemented( + gb_type="RuntimeError when trying to get real value from fx.Node", + context="", + explanation="", + hints=[*graph_break_hints.USER_ERROR], + from_exc=exn, + ) + ) + raise AssertionError("should not be reachable") from None + return real_value + + +def assert_no_fake_params_or_buffers(gm: torch.nn.Module) -> None: + from torch._subclasses.fake_tensor import FakeTensorConfig, is_fake + + def stack_or_hint(t: Any) -> str: + if FakeTensorConfig.debug: + import traceback + + return f"FAKE TENSOR CREATION TRACEBACK: \n {traceback.format_list(t._debug_trace)}" + else: + return "Enable TORCH_FAKE_TENSOR_DEBUG=1 to get creation stack traces on fake tensors." + + for name, buffer in gm.named_buffers(): + assert not is_fake(buffer), ( + f"Unexpected fake buffer {name} {stack_or_hint(buffer)}" + ) + for name, param in gm.named_parameters(): + assert not is_fake(param), ( + f"Unexpected fake param {name} {stack_or_hint(param)}" + ) + + +def fqn(obj: Any) -> str: + """ + Returns the fully qualified name of the object. + """ + return f"{obj.__module__}.{obj.__qualname__}" + + +def ifdynstaticdefault(count1: Any, count2: Any) -> Any: + if torch._dynamo.config.assume_static_by_default: + return count1 + else: + return count2 + + +def import_submodule(mod: types.ModuleType) -> None: + """ + Ensure all the files in a given submodule are imported + """ + for filename in sorted(os.listdir(os.path.dirname(cast(str, mod.__file__)))): + if filename.endswith(".py") and filename[0] != "_": + importlib.import_module(f"{mod.__name__}.{filename[:-3]}") + + +def object_has_getattribute(value: Any) -> bool: + return class_has_getattribute(type(value)) + + +def object_setattr_ignore_descriptor(obj: Any, name: str, value: Any) -> None: + # https://github.com/python/cpython/blob/3.11/Objects/object.c#L1286-L1335 + d = object.__getattribute__(obj, "__dict__") + d[name] = value + + +def class_has_getattribute(cls: type) -> bool: + try: + if isinstance( + inspect.getattr_static(cls, "__getattribute__"), + types.FunctionType, + ): + return True + except AttributeError: + pass + return False + + +def get_custom_getattr( + value: Any, ignore_nn_module_getattr: bool = False +) -> Any | None: + try: + getattr_fn = inspect.getattr_static(type(value), "__getattr__") + except AttributeError: + getattr_fn = None + if ignore_nn_module_getattr and getattr_fn is torch.nn.Module.__getattr__: + # ignore this case of getattr + getattr_fn = None + return getattr_fn + + +class TensorStaticReason(enum.Enum): + PARAMETER = 2 + NOT_TENSOR = 4 + NN_MODULE_PROPERTY = 5 + + +def tensor_static_reason_to_message(reason: TensorStaticReason) -> str: + if reason == TensorStaticReason.PARAMETER: + return "mark_dynamic on parameter, parameters are always static today." + if reason == TensorStaticReason.NOT_TENSOR: + return "mark_dynamic on a non tensor, how did this happen?" + if reason == TensorStaticReason.NN_MODULE_PROPERTY: + return "tensor is static because it is nn module associated." + raise AssertionError(f"Illegal reason {reason}") + + +def tensor_always_has_static_shape( + tensor: torch.Tensor | Any, + is_tensor: bool, + tensor_source: Source, +) -> tuple[bool, TensorStaticReason | None]: + """ + Given a tensor, source, and is_tensor flag, determine if a shape should be static. + + Args: + tensor - the real tensor to evaluate, parameters force a static shape. + is_tensor - internal dynamo check, essentially "is_tensor": target_cls is TensorVariable, + tensors not in a TensorVariable for whatever reason are forced static. + + Returns a tuple, where the first element is the bool of whether or not this tensor should have a static shape. + The second element is a TensorStaticReason, useful for passing to tensor_static_reason_to_message if needed. + """ + from .source import is_from_unspecialized_param_buffer_source + + if ( + tensor_source.guard_source.is_specialized_nn_module() + or tensor_source.guard_source.is_unspecialized_builtin_nn_module() + ) and config.force_nn_module_property_static_shapes: + return True, TensorStaticReason.NN_MODULE_PROPERTY + + if ( + type(tensor) is torch.nn.Parameter + or is_from_unspecialized_param_buffer_source(tensor_source) + ) and config.force_parameter_static_shapes: + return True, TensorStaticReason.PARAMETER + if not is_tensor: + return True, TensorStaticReason.NOT_TENSOR + return False, None + + +def lazy_format_graph_tabular(fn_name: str, gm: torch.fx.GraphModule) -> Any: + def inner() -> str: + try: + from tabulate import tabulate # TODO: Check that this is installed + except ImportError: + return ( + "Tabulate module missing, please install tabulate to log the graph in tabular format, logging code instead:\n" + + str(lazy_format_graph_code(fn_name, gm)) + ) + + node_specs = [ + [n.op, n.name, n.target, n.args, n.kwargs] for n in gm.graph.nodes + ] + graph_str = tabulate( + node_specs, headers=["opcode", "name", "target", "args", "kwargs"] + ) + return _format_graph_code(fn_name, gm.forward.__code__.co_filename, graph_str) + + return LazyString(inner) + + +def format_bytecode( + prefix: str, name: str, filename: str, line_no: int, code: Any +) -> str: + return f"{prefix} {name} {filename} line {line_no} \n{dis.Bytecode(code).dis()}\n" + + +forward_hook_names = [ + "_forward_pre_hooks", + "_forward_pre_hooks_with_kwargs", + "_forward_hooks_with_kwargs", + "_forward_hooks", +] +backward_hook_names = ["_backward_pre_hooks", "_backward_hooks"] +state_dict_hook_names = [ + "_state_dict_pre_hooks", + "_state_dict_hooks", + "_load_state_dict_pre_hooks", + "_load_state_dict_post_hooks", +] +all_hook_names = forward_hook_names + backward_hook_names + state_dict_hook_names + + +def nn_module_has_global_hooks() -> bool: + # This is limited to backward hooks for now because NNModuleVariable + # supports fwd hooks underneath. + return bool( + len(torch.nn.modules.module._global_backward_hooks) + or len(torch.nn.modules.module._global_backward_pre_hooks) + ) + + +def nn_module_get_all_hooks( + mod: torch.nn.Module, + check_forward_hooks: bool = False, + check_backward_hooks: bool = False, + check_state_dict_hooks: bool = False, +) -> list[Any]: + """ + Sometimes its useful to differentiate between types of hooks such as forward/backward/pre + hooks executed during module.__call__, and state_dict hooks which are executed separately. + """ + hook_dicts_to_check = [] + check_all_hooks = ( + not check_forward_hooks + and not check_backward_hooks + and not check_state_dict_hooks + ) + if check_forward_hooks or check_all_hooks: + hook_dicts_to_check.extend(forward_hook_names) + if check_backward_hooks or check_all_hooks: + hook_dicts_to_check.extend(backward_hook_names) + if check_state_dict_hooks: + hook_dicts_to_check.extend(state_dict_hook_names) + + all_hooks = [] + for hook_dict_name in hook_dicts_to_check: + hooks = getattr(mod, hook_dict_name, []) + for hook_name in hooks: + hook = hooks[hook_name] + + all_hooks.append(hook) + return all_hooks + + +def nnmodule_has_hooks( + mod: torch.nn.Module, + check_forward_hooks: bool = False, + check_backward_hooks: bool = False, + check_state_dict_hooks: bool = False, +) -> bool: + """ + Helper function to check if a module has any hooks attached to it. + """ + hooks = nn_module_get_all_hooks( + mod, + check_forward_hooks=check_forward_hooks, + check_backward_hooks=check_backward_hooks, + check_state_dict_hooks=check_state_dict_hooks, + ) + return bool(hooks) + + +def to_numpy_helper(value: Any) -> Any: + """Convert tensor and tnp.ndarray to numpy.ndarray.""" + if is_fake(value): + return value + if isinstance(value, tnp.ndarray): + return to_numpy_helper(value.tensor) + elif isinstance(value, torch.Tensor): + return value.numpy(force=True) + elif isinstance(value, (tuple, list)): + return type(value)(to_numpy_helper(obj) for obj in value) + else: + return value + + +def numpy_to_tensor(value: Any) -> Any: + """Convert tnp.ndarray to tensor, leave other types intact. If a list/tuple, loop through it to convert.""" + assert np is not None + if isinstance(value, np.ndarray): + return torch.as_tensor(value) + if isinstance(value, tnp.ndarray): + return value.tensor + elif isinstance(value, (tuple, list)): + return type(value)(numpy_to_tensor(obj) for obj in value) + else: + return value + + +class numpy_to_tensor_wrapper(Generic[_P, R]): + def __init__(self, f: Callable[_P, R]) -> None: + self.f = f + self.__name__ = "wrapped_" + self.f.__name__ + + def __repr__(self) -> str: + return f">" + + def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> Any: + out = self.f(*args, **kwargs) + return numpy_to_tensor(out) + + +def numpy_attr_wrapper(obj: Any, name: str) -> Any: + if isinstance(obj, tnp.ndarray): + out = getattr(obj, name) + return numpy_to_tensor(out) + elif isinstance(obj, torch.Tensor): + out = getattr(tnp.ndarray(obj), name) + return numpy_to_tensor(out) + + +class numpy_method_wrapper: + """Convert obj from torch.Tensor to tnp.ndarray and call method. Then convert result back to torch.Tensor.""" + + def __init__(self, method: str) -> None: + self.method = method + self.__name__ = "wrapped_" + self.method + + def __repr__(self) -> str: + return f">" + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + obj = args[0] + if isinstance(obj, torch.Tensor): + obj = tnp.ndarray(obj) + method_callable = getattr(obj, self.method) + out = method_callable(*args[1:], **kwargs) + return numpy_to_tensor(out) + + +class numpy_operator_wrapper(Generic[_P, R]): + """Implements dunder methods for tnp.ndarray via functions from the operator library""" + + def __init__(self, op: Callable[..., Any]) -> None: + self.op = op + self.__name__ = f"wrapped_{op.__name__}" + + def __repr__(self) -> str: + return f">" + + def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> Any: + assert not kwargs + + # pyrefly: ignore [bad-assignment] + args = ( + tnp.ndarray(arg) if isinstance(arg, torch.Tensor) else arg for arg in args + ) + out = self.op(*args) + return numpy_to_tensor(out) + + +def defake(x: Any) -> Any: + if not isinstance(x, FakeTensor): + return x + size: torch._prims_common.ShapeType + stride: torch._prims_common.StrideType + if x._has_symbolic_sizes_strides: + # optimization_hint is appropriate here because defake only needs a + # plausible concrete shape to allocate a real tensor; it does not need + # to install guards. For unbacked symbols the heuristic fallback is fine. + size = [ + torch.fx.experimental.symbolic_shapes.optimization_hint(s) for s in x.size() + ] + stride = [ + torch.fx.experimental.symbolic_shapes.optimization_hint(s) + for s in x.stride() + ] + else: + size = x.size() + stride = x.stride() + y = torch.empty_strided( + size, + stride, + dtype=x.dtype, + device=x.device, + requires_grad=x.requires_grad, + ) + y.zero_() + return y + + +def _disable_side_effect_safety_checks_for_current_subtracer( + fn: Callable[_P, R], *args: _P.args, **kwargs: _P.kwargs +) -> R: + return fn(*args, **kwargs) + + +def is_utils_checkpoint(obj: Any) -> bool: + # Lazy import to avoid circular dependencies + import torch.utils.checkpoint + + return obj is torch.utils.checkpoint.checkpoint + + +def is_invoke_subgraph(obj: Any) -> bool: + from torch._higher_order_ops.invoke_subgraph import invoke_subgraph_placeholder + + return obj is invoke_subgraph_placeholder + + +def build_invoke_subgraph_variable(**options: Any) -> Any: + from .variables.higher_order_ops import TorchHigherOrderOperatorVariable + + return TorchHigherOrderOperatorVariable.make( + torch._higher_order_ops.invoke_subgraph, + **options, + ) + + +def build_checkpoint_variable(**options: Any) -> Any: + import torch._higher_order_ops.wrap as higher_order_ops + + from .variables.higher_order_ops import TorchHigherOrderOperatorVariable + + # TODO - This is a temporary situation where we have two versions of + # checkpointing implementation. We will converge on one and remove the other. + activation_checkpoint_op: torch._ops.HigherOrderOperator = ( + higher_order_ops.tag_activation_checkpoint + ) + if torch._functorch.config.functionalize_rng_ops: + activation_checkpoint_op = higher_order_ops.wrap_activation_checkpoint + + return TorchHigherOrderOperatorVariable.make( + activation_checkpoint_op, + **options, + ) + + +def is_compile_supported(device_type: DeviceLikeType) -> Any: + from .eval_frame import is_dynamo_supported + + type = torch.device(device_type).type + compile_supported = is_dynamo_supported() + if type == "cpu": + pass + elif type in ["cuda", "xpu", "mtia"] and compile_supported: + compile_supported = has_triton() + else: + compile_supported = False + return compile_supported + + +# The following 3.11 source code functions are adapted from +# https://github.com/python/cpython/blob/v3.11.4/Lib/traceback.py +# in order to output source code corresponding to bytecode in 3.11+. +# We need our own versions since we want to support multiline expressions. +def _fix_offset(str: str, offset: int) -> int: + """ + Convert byte offset `offset` of `str` into character offset. + Byte offset is used for 3.11+ instruction column data. + Takes things like unicode characters into consideration. + + Unchanged from CPython implementation. + """ + as_utf8 = str.encode("utf-8") + return len(as_utf8[:offset].decode("utf-8", errors="replace")) + + +@dataclasses.dataclass +class _Anchors: + # inclusive + left_end_lineno: int + left_end_offset: int + right_start_lineno: int + # exclusive + right_start_offset: int + + +def _extract_anchors_from_expr(segment: str) -> _Anchors | None: + """ + Given source code `segment` corresponding to a bytecode + instruction, determine: + - for binary ops, the location of the binary op + - for indexing, the location of the brackets. + `segment` is expected to be a valid Python expression + """ + assert sys.version_info >= (3, 11) + + import ast + + tree: Any | None = None + try: + # Without brackets, `segment` is parsed as a statement. + # We expect an expression, so wrap `segment` in + # brackets to handle multi-line expressions. + tree = ast.parse("(\n" + segment + "\n)") + except SyntaxError: + return None + assert tree is not None + + if len(tree.body) != 1: + return None + + lines = segment.split("\n") + + # get character index given byte offset + def normalize(lineno: int, offset: int) -> int: + return _fix_offset(lines[lineno], offset) + + # Gets the next valid character index in `lines`, if + # the current location is not valid. Handles empty lines. + def next_valid_char(lineno: int, col: int) -> tuple[int, int]: + while lineno < len(lines) and col >= len(lines[lineno]): + col = 0 + lineno += 1 + assert lineno < len(lines) and col < len(lines[lineno]) + return lineno, col + + # Get the next valid character index in `lines`. + def increment(lineno: int, col: int) -> tuple[int, int]: + col += 1 + lineno, col = next_valid_char(lineno, col) + assert lineno < len(lines) and col < len(lines[lineno]) + return lineno, col + + # Get the next valid character at least on the next line + def nextline(lineno: int, col: int) -> tuple[int, int]: + col = 0 + lineno += 1 + lineno, col = next_valid_char(lineno, col) + assert lineno < len(lines) and col < len(lines[lineno]) + return lineno, col + + statement = tree.body[0] + if isinstance(statement, ast.Expr): + expr = statement.value + if isinstance(expr, ast.BinOp): + # ast gives locations for BinOp subexpressions, e.g. + # ( left_expr ) + ( right_expr ) + # left^^^^^ right^^^^^ + # -2 since end_lineno is 1-indexed and because we added an extra + # bracket to `segment` when calling ast.parse + cur_lineno = cast(int, expr.left.end_lineno) - 2 + assert expr.left.end_col_offset is not None + cur_col = normalize(cur_lineno, expr.left.end_col_offset) + cur_lineno, cur_col = next_valid_char(cur_lineno, cur_col) + + # Heuristic to find the operator character. + # The original CPython implementation did not look for ), \, or #, + # leading to incorrect anchor location, e.g. + # (x) + (y) + # ~~^~~~~~~ + while (ch := lines[cur_lineno][cur_col]).isspace() or ch in ")\\#": + if ch in "\\#": + cur_lineno, cur_col = nextline(cur_lineno, cur_col) + else: + cur_lineno, cur_col = increment(cur_lineno, cur_col) + + # binary op is 1 or 2 characters long, on the same line + right_col = cur_col + 1 + if ( + right_col < len(lines[cur_lineno]) + and not (ch := lines[cur_lineno][right_col]).isspace() + and ch not in "\\#" + ): + right_col += 1 + # right_col can be invalid since it is exclusive + + return _Anchors(cur_lineno, cur_col, cur_lineno, right_col) + elif isinstance(expr, ast.Subscript): + # ast gives locations for value and slice subexpressions, e.g. + # ( value_expr ) [ slice_expr ] + # value^^^^^ slice^^^^^ + # subscript^^^^^^^^^^^^^^^^^^^^ + # find left bracket (first '[' after value) + left_lineno = cast(int, expr.value.end_lineno) - 2 + assert expr.value.end_col_offset is not None + left_col = normalize(left_lineno, expr.value.end_col_offset) + left_lineno, left_col = next_valid_char(left_lineno, left_col) + while lines[left_lineno][left_col] != "[": + left_lineno, left_col = increment(left_lineno, left_col) + # find right bracket (final character of expression) + right_lineno = cast(int, expr.end_lineno) - 2 + assert expr.end_col_offset is not None + right_col = normalize(right_lineno, expr.end_col_offset) + return _Anchors(left_lineno, left_col, right_lineno, right_col) + elif isinstance(expr, ast.Call): + # ( func_expr ) (args, kwargs) + # func^^^^^ + # call^^^^^^^^^^^^^^^^^^^^^^^^ + # find left bracket (first '(' after func) + left_lineno = cast(int, expr.func.end_lineno) - 2 + assert expr.func.end_col_offset is not None + left_col = normalize(left_lineno, expr.func.end_col_offset) + left_lineno, left_col = next_valid_char(left_lineno, left_col) + while lines[left_lineno][left_col] != "(": + left_lineno, left_col = increment(left_lineno, left_col) + # find right bracket (final character of expression) + right_lineno = cast(int, expr.end_lineno) - 2 + assert expr.end_col_offset is not None + right_col = normalize(right_lineno, expr.end_col_offset) + return _Anchors(left_lineno, left_col, right_lineno, right_col) + + return None + + +def get_instruction_source_311(code: types.CodeType, inst: Instruction) -> str: + """ + Python 3.11+ only. Returns lines of source code (from code object `code`) + corresponding to `inst`'s location data, and underlines relevant code to `inst`. + + Example: CALL on `g`: + f(g( + ^^ + h(x))) + ^^^^^ + + We need our own implementation in < 3.13 since `format_frame_summary` in + Python's `traceback` module doesn't handle multi-line expressions + (and their anchor extraction code is not completely correct). + """ + if sys.version_info >= (3, 13): + # multiline traceback implemented in 3.13+ + frame_summary = traceback.FrameSummary( + code.co_filename, + inst.positions.lineno, + code.co_name, + end_lineno=inst.positions.end_lineno, + colno=inst.positions.col_offset, + end_colno=inst.positions.end_col_offset, + ) + result = traceback.format_list([frame_summary])[0] + # remove first line containing filename info + result = "\n".join(result.splitlines()[1:]) + # indent lines with original indentation + orig_lines = [ + linecache.getline(code.co_filename, lineno).rstrip() + for lineno in range(inst.positions.lineno, inst.positions.end_lineno + 1) + ] + orig_lines_dedent = textwrap.dedent("\n".join(orig_lines)).splitlines() + indent_len = len(orig_lines[0]) - len(orig_lines_dedent[0]) + indent = orig_lines[0][:indent_len] + result = textwrap.indent(textwrap.dedent(result), indent) + return result + + assert hasattr(inst, "positions") and inst.positions is not None + if inst.positions.lineno is None: + return "" + # The rstrip + "\n" pattern is used throughout this function to handle + # linecache.getline errors. Error lines are treated as empty strings "", but we want + # to treat them as blank lines "\n". + first_line = linecache.getline(code.co_filename, inst.positions.lineno).rstrip() + if inst.positions.end_lineno is None: + return first_line + if inst.positions.col_offset is None or inst.positions.end_col_offset is None: + return first_line + + # character index of the start of the instruction + start_offset = _fix_offset(first_line, inst.positions.col_offset) + # character index of the end of the instruction + # compute later since end may be a different line + end_offset = None + # expression corresponding to the instruction so we can get anchors + segment = "" + # underline markers to be printed - start with `~` marker and replace with `^` later + markers = [] + + # Compute segment and initial markers + if inst.positions.end_lineno == inst.positions.lineno: + end_offset = _fix_offset(first_line, inst.positions.end_col_offset) + segment = first_line[start_offset:end_offset] + markers.append(" " * start_offset + "~" * (end_offset - start_offset)) + else: + segment = first_line[start_offset:] + "\n" + markers.append(" " * start_offset + "~" * (len(first_line) - start_offset)) + last_line = linecache.getline( + code.co_filename, inst.positions.end_lineno + ).rstrip() + end_offset = _fix_offset(last_line, inst.positions.end_col_offset) + for lineno in range(inst.positions.lineno + 1, inst.positions.end_lineno): + line = linecache.getline(code.co_filename, lineno).rstrip() + segment += line + "\n" + # don't underline leading spaces + num_spaces = len(line) - len(line.lstrip()) + markers.append(" " * num_spaces + "~" * (len(line) - num_spaces)) + segment += last_line[:end_offset] + num_spaces = len(last_line) - len(last_line.lstrip()) + markers.append(" " * num_spaces + "~" * (end_offset - num_spaces)) + + anchors: _Anchors | None = None + try: + anchors = _extract_anchors_from_expr(segment) + except AssertionError: + pass + + # replace `~` markers with `^` where necessary + if anchors is None: + markers = [marker.replace("~", "^") for marker in markers] + else: + # make markers mutable + mutable_markers: list[list[str]] = [list(marker) for marker in markers] + + # anchor positions do not take start_offset into account + if anchors.left_end_lineno == 0: + anchors.left_end_offset += start_offset + if anchors.right_start_lineno == 0: + anchors.right_start_offset += start_offset + + # Turn `~`` markers between anchors to `^` + for lineno in range(len(markers)): + for col in range(len(mutable_markers[lineno])): + if lineno < anchors.left_end_lineno: + continue + if lineno == anchors.left_end_lineno and col < anchors.left_end_offset: + continue + if ( + lineno == anchors.right_start_lineno + and col >= anchors.right_start_offset + ): + continue + if lineno > anchors.right_start_lineno: + continue + if mutable_markers[lineno][col] == "~": + mutable_markers[lineno][col] = "^" + + # make markers into strings again + markers = ["".join(marker) for marker in mutable_markers] + + result = "" + for i in range(len(markers)): + result += ( + linecache.getline(code.co_filename, inst.positions.lineno + i).rstrip() + + "\n" + ) + result += markers[i] + "\n" + return result + + +def get_static_address_type(t: Any) -> Any: + if isinstance(t, torch.Tensor): + return getattr(t, "_dynamo_static_input_type", None) + + return None + + +def is_rng_state_getter_or_setter(value: Any) -> bool: + getters = ( + # The following two functions are not identical, so don't remove anyone! + torch._C.Generator.get_state, + torch.default_generator.get_state, + torch.get_rng_state, + torch.cuda.get_rng_state, + ) + setters = ( + torch._C.Generator.set_state, + torch.default_generator.set_state, + torch.set_rng_state, + torch.cuda.set_rng_state, + ) + return value in (*setters, *getters) + + +def is_tensor_base_attr_getter(value: Any) -> bool: + return ( + isinstance(value, types.MethodWrapperType) + and value.__name__ == "__get__" + and hasattr(value.__self__, "__objclass__") + and value.__self__.__objclass__ is torch._C._TensorBase # type: ignore[attr-defined] + ) + + +def is_tensor_getset_descriptor(name: str) -> bool: + try: + attr = inspect.getattr_static(torch.Tensor, name) + return type(attr) is types.GetSetDescriptorType + except AttributeError: + return False + + +def is_torch_function_object(value: Any) -> bool: + return hasattr(value, "__torch_function__") + + +def has_torch_function(vt: VariableTracker) -> bool: + # This emulates + # https://github.com/pytorch/pytorch/blob/8d81806211bc3c0ee6c2ef235017bacf1d775a85/torch/csrc/utils/disable_torch_function.cpp#L315-L323 + from torch._dynamo.variables import UserDefinedObjectVariable + from torch._dynamo.variables.torch_function import TensorWithTFOverrideVariable + + # Note on lazy vars: The value will either be realized or not throughout the course of execution + # if the value has a torch function, it will eventually be realized so we can realize it here + # if the value does not have a torch function, it may or may not be realized + # if it is realized it will be used and guards will be installed properly + # if it is not used, guards won't be installed, and it doesn't matter + # if the value has a torch function or not, so we should *not* realize it. + # NB: We technically know that if is_realized is False, LazyVariableTracker has the peek_value method + # but mypy does not unfortunately + if vt.is_realized() or ( + hasattr(vt, "peek_value") and hasattr(vt.peek_value(), "__torch_function__") + ): + func = None + if isinstance(vt, TensorWithTFOverrideVariable): + func = getattr(vt.class_type, "__torch_function__", None) + + elif isinstance(vt, UserDefinedObjectVariable): + func = getattr(vt.value, "__torch_function__", None) + + return func not in (None, torch._C._disabled_torch_function_impl) + + return False + + +# see note [Tensor Fakification and Symbol Caching] +def to_fake_tensor( + t: torch.Tensor, fake_mode: torch._subclasses.fake_tensor.FakeTensorMode +) -> Any: + symbolic_context = None + source = None + if tracing_context := torch._guards.TracingContext.try_get(): + if t in tracing_context.tensor_to_context: + symbolic_context = tracing_context.tensor_to_context[t] + source = symbolic_context.tensor_source + + return fake_mode.from_tensor( + t, static_shapes=False, symbolic_context=symbolic_context, source=source + ) + + +# NB: this works for both classes and instances +def is_frozen_dataclass(value: Any) -> bool: + return ( + not object_has_getattribute(value) + and not class_has_getattribute(value) + and is_dataclass(value) + and hasattr(value, "__dataclass_params__") + and hasattr(value.__dataclass_params__, "frozen") + and value.__dataclass_params__.frozen + ) + + +def get_first_attr(obj: Any, *attrs: str) -> Any: + """ + Return the first available attribute or throw an exception if none is present. + """ + for attr in attrs: + if hasattr(obj, attr): + return getattr(obj, attr) + + raise AssertionError(f"{obj} does not has any of the attributes: {attrs}") + + +@contextlib.contextmanager +def maybe_enable_compiled_autograd( + should_enable: bool, fullgraph: bool = True, dynamic: bool = True +) -> Generator[Any, None, None]: + if not should_enable: + yield + else: + + def compiler_fn(gm: Any) -> Any: + def inner_compiler(gm_: Any, example_inputs_: Any) -> Any: + torch._dynamo.utils.counters["compiled_autograd"]["compiles"] += 1 + return torch._inductor.compile(gm_, example_inputs_) + + return torch.compile( + gm, backend=inner_compiler, fullgraph=fullgraph, dynamic=dynamic + ) + + with torch._dynamo.compiled_autograd._enable(compiler_fn) as ctx: + yield ctx + + +def invalid_removeable_handle() -> RemovableHandle: + # need a subclass so weakref works + class Invalid(dict): # type: ignore[type-arg] + pass + + return RemovableHandle(Invalid()) + + +# Returns a "proxy" (new object with the same class and dict) for (non-GraphModule) nn.Module's. +# Attribute changes to the original object/proxy will be reflected in the other. +# This is useful for cases where we want a keep-alive reference to a module without increasing +# its reference count. +def nn_module_proxy(mod: Any) -> Any: + if not isinstance(mod, torch.nn.Module): + return mod + if isinstance(mod, torch.fx.GraphModule): + # Dynamo-generated GM's shouldn't contain user-created GM's + return mod + proxy = mod.__class__.__new__(mod.__class__) + proxy.__dict__ = mod.__dict__ + return proxy + + +class GmWrapper(torch.nn.Module): + def __init__( + self, gm: torch.fx.GraphModule, unflatten_fn: Callable[[list[Any]], Any] + ) -> None: + super().__init__() + self.gm = gm + self.unflatten_fn = unflatten_fn + + def forward(self, *args: Any) -> Any: + # pyrefly: ignore [redefinition] + args: list[Any] = list(args) + return self.gm(*self.unflatten_fn(args)) + + +def flatten_graph_inputs( + gm: torch.fx.GraphModule, inputs: Any, compile_gm: Callable[[Any, Any], Any] +) -> Callable[..., Any]: + """ + Mutate inputs so that they are flat and wrap gm such that it + accepts those inputs. This is needed for graphs that take + bumpy inputs. + """ + inputs_idx_to_clear = [ + i + for i, node in enumerate(gm.graph.nodes) + if node.op == "placeholder" and node.meta.get("steal_arg", False) + ] + + if torch._dynamo.compiled_autograd.in_compiled_autograd_region: + # fast path, avoid pytree overhead + # compiled autograd inputs are always a list of tensors, maybe followed by symints + assert inputs_idx_to_clear == [0] + assert isinstance(inputs[0], list) + boxed_inputs_count = len(inputs[0]) + + def flatten_fn(args: Any) -> Any: + return args[0] + list(args[1:]) + + def unflatten_fn(flat_args: Any) -> Any: + return (flat_args[:boxed_inputs_count], *flat_args[boxed_inputs_count:]) + + compiled_fn = compile_gm(GmWrapper(gm, unflatten_fn), flatten_fn(inputs)) + else: + # slow path, don't know inputs structure + flat_inputs, spec = pytree.tree_flatten(inputs) + unflatten_fn = functools.partial(pytree.tree_unflatten, treespec=spec) + compiled_fn = compile_gm(GmWrapper(gm, unflatten_fn), flat_inputs) + # note this doesn't check the spec, assuming it is the same + flatten_fn = pytree.arg_tree_leaves + + def wrapper(*args: Any) -> Any: + flat_args = flatten_fn(args) + + # flat_args is a new list, so we need to clear references from the old list + for i in inputs_idx_to_clear: + args[i].clear() + + # this call is boxed to avoid increasing refcount until we reach aot_module_simplified forward + return compiled_fn(flat_args) + + return wrapper + + +def get_locals_to_steal(maybe_gm: Any) -> list[Any]: + if not isinstance(maybe_gm, torch.fx.GraphModule) or not hasattr(maybe_gm, "meta"): + return [] + return maybe_gm.meta.get("locals_to_steal", []) + + +def set_locals_to_steal(gm: torch.fx.GraphModule, locals_to_steal: list[Any]) -> None: + gm.meta["locals_to_steal"] = locals_to_steal + + +class Lit: + def __init__(self, s: str) -> None: + self.s = s + + def __repr__(self) -> str: + return self.s + + +warn_once_cache: set[str] = set() + + +def warn_once(msg: str, stacklevel: int = 1) -> None: + # Dynamo causes all warnings.warn (in user code and in Dynamo code) to print all the time. + # https://github.com/pytorch/pytorch/issues/128427. + # warn_once is a workaround: if the msg has been warned on before, then we will not + # warn again. + # NB: it's totally ok to store a cache of all the strings: this is what warnings.warn does as well. + if msg in warn_once_cache: + return + warn_once_cache.add(msg) + warnings.warn(msg, stacklevel=stacklevel + 1) + + +def strip_color_from_string(text: str) -> str: + # This regular expression matches ANSI escape codes + ansi_escape = re.compile(r"\x1B[@-_][0-?]*[ -/]*[@-~]") + return ansi_escape.sub("", text) + + +@contextlib.contextmanager +def _disable_saved_tensors_hooks_during_tracing() -> Generator[None, None, None]: + # See NOTE: [Deferring tensor pack/unpack hooks until runtime] + try: + prior = torch._C._autograd._saved_tensors_hooks_set_tracing(True) + yield + finally: + torch._C._autograd._saved_tensors_hooks_set_tracing(prior) + + +def is_parameter_freezing() -> bool: + return torch._inductor.config.freezing and not torch.is_grad_enabled() + + +def get_torch_function_mode_stack() -> list[Any]: + return [ + get_torch_function_mode_stack_at(i) for i in range(_len_torch_function_stack()) + ] + + +def get_torch_function_mode_stack_at(ind: int) -> Any: + assert ind < _len_torch_function_stack() and ind >= 0 + return torch._C._get_function_stack_at(ind) + + +def set_torch_function_mode_stack(stack: list[Any]) -> None: + for _ in range(_len_torch_function_stack()): + _pop_torch_function_stack() + + for mode in stack: + _push_on_torch_function_stack(mode) + + +def clear_torch_function_mode_stack() -> None: + for _ in range(_len_torch_function_stack()): + _pop_torch_function_stack() + + +def get_current_stream(device: torch.device) -> torch.Stream: + return torch.accelerator.current_stream(device) + + +# call from C dynamo in order to inspect values in pdb +def _breakpoint_for_c_dynamo(*args: Any) -> None: + breakpoint() + + +def verify_guard_fn_signature(value: Any) -> None: + fn = value.__metadata_guard__ + sig = inspect.signature(fn) + if len(sig.parameters) != 2: + from .exc import InternalTorchDynamoError + + raise InternalTorchDynamoError( + "Tensor subclass method __metadata_guard__ must take exactly two subclass metadata arguments" + ) + if fn.__self__ != value.__class__: + from .exc import InternalTorchDynamoError + + raise InternalTorchDynamoError( + "Tensor subclass method __metadata_guard__ must be a classmethod" + ) + + +def does_not_override_dict_iter_methods(user_cls: Any) -> bool: + return ( + user_cls.items in (dict.items, OrderedDict.items) + and user_cls.values in (dict.values, OrderedDict.values) + and user_cls.keys in (dict.keys, OrderedDict.keys) + and user_cls.__iter__ in (dict.__iter__, OrderedDict.__iter__) + ) + + +# Helper functions below are to prevent TorchDynamo to prevent tracing of +# __torch_function__ calls triggered on tensor properties in the pre graph +# bytecode. +@torch._disable_dynamo +def call_size(x: Any, i: int) -> int: + return x.size(i) + + +@torch._disable_dynamo +def call_stride(x: Any, i: int) -> int: + return x.stride(i) + + +@torch._disable_dynamo +def call_storage_offset(x: Any) -> int: + return x.storage_offset() + + +# Helper function to extract relevant parts of a tensor's __dict__ to store in node meta. +# To avoid ref cycles, it's important that no tensors are present here, so leave those out. +def _extract_tensor_dict(t: torch.Tensor) -> dict[str, Any]: + KEYS_TO_COPY = [ + "_dynamo_static_input_type", + "tag", + ] + + tensor_dict = { + key: copy.copy(t.__dict__[key]) for key in KEYS_TO_COPY if key in t.__dict__ + } + + return tensor_dict + + +def build_stream(args: tuple[Any], kwargs: dict[Any, Any]) -> torch.Stream: + return torch._C.Stream(*args, **kwargs) + + +def build_event(args: tuple[Any], kwargs: dict[Any, Any]) -> torch.Event: + return torch._C.Event(*args, **kwargs) + + +class CompileTimeInstructionCounter: + _counter: int = 0 + _id: int = -1 + _depth = 0 + + @classmethod + def start(cls) -> None: + cls._depth = cls._depth + 1 + if cls._depth == 1: + cls._id = _instruction_counter.start() + + @classmethod + def end(cls) -> None: + cls._depth = cls._depth - 1 + if cls._depth == 0: + cls._counter += _instruction_counter.end(cls._id) + cls._id = -1 + + @classmethod + def clear(cls) -> None: + cls._counter = 0 + + @classmethod + def value(cls) -> int: + return cls._counter + + @classmethod + @contextmanager + def record(cls) -> Generator[None, None, None]: + try: + if config.record_compile_time_instruction_count: + cls.start() + yield + finally: + if config.record_compile_time_instruction_count: + cls.end() + + +class CompileCounterInt(int): + def __add__(self, other: Any) -> CompileCounterInt: + return CompileCounterInt(super().__add__(other)) + + +def set_feature_use(feature: str, usage: bool) -> None: + """ + Records whether we are using a feature + Generally a feature is a JK. + """ + # Note that sometimes (tests etc...) we're not in a context which we can record into + if get_metrics_context().in_progress(): + get_metrics_context().set_key_value("feature_usage", feature, usage) + + +_ddp_optimization_mode: tuple[str, ...] = ( + "ddp_optimizer", + "python_reducer", # experimental mode + "python_reducer_without_compiled_forward", + "no_optimization", +) + + +def get_optimize_ddp_mode() -> str: + optimize_ddp = config.optimize_ddp + if isinstance(optimize_ddp, bool): + mode = "ddp_optimizer" if optimize_ddp else "no_optimization" + elif isinstance(optimize_ddp, str): + mode = optimize_ddp + else: + raise ValueError( + f"Invalid dynamo config optimize_ddp type {type(optimize_ddp)=}" + ) + + assert mode in _ddp_optimization_mode, ( + f"Invalid dynamo config optimize_ddp value {mode=}" + ) + return mode + + +@contextmanager +def maybe_disable_inference_mode() -> Generator[None, None, None]: + """ + Disables torch.inference_mode for the compilation (still on at runtime). + This simplifies the compile stack where we can assume that inference_mode + will always be off. + + Since inference_mode is equivalent to no_grad + some optimizations (version + counts etc), we turn on no_grad here. The other optimizations are not + relevant to torch.compile. + """ + is_inference_mode_on = ( + config.fake_tensor_disable_inference_mode and torch.is_inference_mode_enabled() + ) + if is_inference_mode_on: + with ( + torch.inference_mode(False), + torch.no_grad(), + ): + yield + else: + yield + + +@contextmanager +def maybe_disable_inference_mode_for_fake_prop() -> Generator[None, None, None]: + """ + Turns off tracking of inference_mode for fake tensor propagation. With this + context manager, when a real tensor is converted to fake tensor, the fake + tensor looses its inference-ness. + """ + if config.fake_tensor_disable_inference_mode: + with torch._subclasses.meta_utils.disable_inference_mode_for_fake_prop(): + yield + else: + yield + + +def is_node_meta_valid(node: torch.fx.Node | None) -> bool: + return node is None or "example_value" in node.meta or "val" in node.meta + + +# If True, enforce fullgraph=True - raise errors on graph break +_error_on_graph_break = False + + +def _get_error_on_graph_break() -> bool: + return _error_on_graph_break + + +def _set_error_on_graph_break(value: bool) -> None: + global _error_on_graph_break + _error_on_graph_break = value + + +@torch._disable_dynamo +def record_pregraph_bytecode_enter() -> AbstractContextManager[None]: + cm: AbstractContextManager[None] = ( + torch._C._profiler._RecordFunctionFast("Pregraph bytecode") + if torch.autograd.profiler._is_profiler_enabled + else contextlib.nullcontext() + ) + cm.__enter__() + return cm + + +@torch._disable_dynamo +def record_pregraph_bytecode_exit(cm: AbstractContextManager[None]) -> None: + cm.__exit__(None, None, None) + + +# Returns a set of code objects present traced in the current TracingContext, or None +# if there is no current TracingContext. +def get_traced_code() -> list[CodeType] | None: + from torch._guards import TracingContext + + return TracingContext.get_traced_code() + + +def raise_on_overridden_hash(obj: Any, vt: VariableTracker) -> None: + from . import graph_break_hints + from .exc import unimplemented + + is_overridden = type(obj).__dict__.get("__hash__", False) + + if is_overridden: + unimplemented( + gb_type="User-defined object with overridden __hash__", + context=f"hashing object of type={type(obj)} and variable tracker {vt}", + explanation=f"Found a user-defined object {vt} with overridden __hash__ when attempting to hash it", + hints=[ + "Dynamo does not support hashing user-defined objects with overridden __hash__", + *graph_break_hints.SUPPORTABLE, + ], + ) + + +def _make_inlined( + tx: InstructionTranslator, f: Callable[..., Any] +) -> Callable[..., VariableTracker]: + assert callable(f), "Expect f to be a python callable." + + def inline_call( + *args: VariableTracker, **kwargs: VariableTracker + ) -> VariableTracker: + from torch._dynamo.trace_rules import _force_inline + from torch._dynamo.variables.functions import UserFunctionVariable + + with _force_inline(): + return UserFunctionVariable(f).call_function(tx, args, kwargs) # type: ignore[arg-type] + + return inline_call diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c8529febd62a9bc9a9729e1ad95dc57984e79212 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/__init__.py @@ -0,0 +1,263 @@ +""" +This package implements variable tracking and symbolic execution capabilities for Dynamo, +which are essential for converting Python code into FX graphs. It provides a comprehensive +set of variable types that handle different Python constructs during tracing. + +Each variable type (like BuiltinVariable, TensorVariable, NNModuleVariable, etc.) is responsible +for tracking and symbolically executing operations on specific Python objects. This enables +Dynamo to: +- Track the flow of values through Python code +- Maintain correct semantics during graph conversion +- Handle complex Python features like context managers, iterators, and custom objects +- Support both eager and symbolic execution modes + +The VariableTracker base class provides the foundation for all variable types, with each +subclass implementing specific behavior for different Python constructs. This modular design +allows Dynamo to accurately trace and optimize Python code while preserving its semantics. +""" + +from .base import VariableTracker +from .builtin import ( + BaseBuiltinVariable, + BuiltinVariable, + DictBuiltinVariable, + IterBuiltinVariable, + ListBuiltinVariable, +) +from .constant import ConstantVariable +from .ctx_manager import ( + CatchWarningsCtxManagerVariable, + ContextWrappingVariable, + CUDADeviceVariable, + CudagraphOverrideVariable, + DisabledSavedTensorsHooksVariable, + DualLevelContextManager, + DynamoConfigPatchVariable, + ErrorOnGraphBreakVariable, + FSDPParamGroupUseTrainingStateVariable, + FxTracebackAnnotateVariable, + GenericContextWrappingVariable, + GradIncrementNestingCtxManagerVariable, + GradInplaceRequiresGradCtxManagerVariable, + GradModeVariable, + InferenceModeVariable, + JvpIncrementNestingCtxManagerVariable, + SDPAKernelVariable, + SetFwdGradEnabledContextManager, + TemporarilyPopInterpreterStackCtxManagerVariable, + VmapIncrementNestingCtxManagerVariable, + WithEnterFunctionVariable, + WithExitFunctionVariable, +) +from .dicts import ( + ConstDictVariable, + DefaultDictVariable, + DictItemsVariable, + DunderDictVariable, + MappingProxyVariable, + NNModuleHooksDictVariable, +) +from .distributed import BackwardHookVariable, DistributedVariable +from .functions import ( + BaseUserFunctionVariable, + BuiltinMethodVariable, + CollectionsNamedTupleFunction, + CreateTMADescriptorExperimentalVariable, + CreateTMADescriptorStableVariable, + FunctionDecoratedByContextlibContextManagerVariable, + FunctoolsPartialVariable, + InspectSignatureVariable, + LocalGeneratorFunctionVariable, + LocalGeneratorObjectVariable, + NestedUserFunctionVariable, + PolyfilledFunctionVariable, + PyTreeGetNodeTypeFunctionVariable, + PyTreeTreeIsLeafFunctionVariable, + SkipFunctionVariable, + SparseTensorCreationSkipVariable, + TMADescriptorExperimentalVariable, + TMADescriptorStableVariable, + TritonSetAllocatorVariable, + UserFunctionVariable, + UserMethodVariable, + WrapperUserFunctionVariable, + WrapperUserMethodVariable, +) +from .higher_order_ops import ( + FunctionalCallVariable, + FunctorchHigherOrderVariable, + ReparametrizeModuleCallVariable, + TorchHigherOrderOperatorVariable, +) +from .iter import ( + CountIteratorVariable, + FilterVariable, + IteratorVariable, + ItertoolsVariable, + MapVariable, + ObjectIteratorVariable, + RepeatIteratorVariable, + ZipVariable, +) +from .lazy import LazyConstantVariable, LazyVariableTracker +from .lists import ( + BaseListVariable, + ListIteratorVariable, + ListVariable, + RangeVariable, + SliceVariable, + TupleIteratorVariable, + TupleVariable, +) +from .misc import ( + AutogradFunctionContextVariable, + AutogradFunctionVariable, + CellVariable, + DeletedVariable, + ExceptionVariable, + GetAttrVariable, + LambdaVariable, + MethodWrapperVariable, + NewGlobalVariable, + NumpyVariable, + ObjectVariable, + PythonModuleVariable, + RandomClassVariable, + RandomVariable, + StringFormatVariable, + SuperVariable, + TorchVersionVariable, + TracebackVariable, + TypingVariable, + UnknownVariable, + WeakRefVariable, +) +from .nn_module import ( + FSDPManagedNNModuleVariable, + NNModuleVariable, + UnspecializedBuiltinNNModuleVariable, + UnspecializedNNModuleVariable, +) +from .optimizer import OptimizerVariable +from .sdpa import SDPAParamsVariable +from .sets import ( + DictKeySetVariable, + FrozensetVariable, + OrderedSetClassVariable, + OrderedSetVariable, + SetVariable, +) +from .streams import ( + CudaStreamVariable, + EventVariable, + StreamContextVariable, + StreamVariable, +) +from .tensor import ( + DataPtrVariable, + FakeItemVariable, + NumpyNdarrayVariable, + SymNodeVariable, + TensorVariable, + UnspecializedPythonVariable, + UntypedStorageVariable, +) +from .torch import TorchCtxManagerClassVariable, TorchInGraphFunctionVariable +from .user_defined import ( + FrozenDataClassVariable, + InspectVariable, + MutableMappingVariable, + NamedTupleVariable, + RemovableHandleVariable, + StructSequenceVariable, + UserDefinedClassVariable, + UserDefinedConstantVariable, + UserDefinedDictVariable, + UserDefinedExceptionClassVariable, + UserDefinedExceptionObjectVariable, + UserDefinedListVariable, + UserDefinedObjectVariable, + UserDefinedSetVariable, + UserDefinedTupleVariable, + UserDefinedVariable, +) + + +__all__ = [ + "AutogradFunctionContextVariable", + "AutogradFunctionVariable", + "BackwardHookVariable", + "BaseBuiltinVariable", + "BaseListVariable", + "BuiltinVariable", + "CatchWarningsCtxManagerVariable", + "ConstantVariable", + "ConstDictVariable", + "DictBuiltinVariable", + "ContextWrappingVariable", + "CountIteratorVariable", + "CreateTMADescriptorExperimentalVariable", + "CreateTMADescriptorStableVariable", + "CUDADeviceVariable", + "CudagraphOverrideVariable", + "DataPtrVariable", + "DefaultDictVariable", + "DeletedVariable", + "DictKeySetVariable", + "DynamoConfigPatchVariable", + "FakeItemVariable", + "GetAttrVariable", + "GradModeVariable", + "InspectSignatureVariable", + "InspectVariable", + "IterBuiltinVariable", + "IteratorVariable", + "ItertoolsVariable", + "LambdaVariable", + "LazyConstantVariable", + "LazyVariableTracker", + "ListBuiltinVariable", + "ListIteratorVariable", + "ListVariable", + "NestedUserFunctionVariable", + "CellVariable", + "NewGlobalVariable", + "NNModuleVariable", + "NumpyNdarrayVariable", + "NumpyVariable", + "OptimizerVariable", + "PolyfilledFunctionVariable", + "PythonModuleVariable", + "RangeVariable", + "RemovableHandleVariable", + "RepeatIteratorVariable", + "SDPAParamsVariable", + "ErrorOnGraphBreakVariable", + "SkipFunctionVariable", + "SliceVariable", + "StringFormatVariable", + "SuperVariable", + "TemporarilyPopInterpreterStackCtxManagerVariable", + "TensorVariable", + "TMADescriptorExperimentalVariable", + "TMADescriptorStableVariable", + "TorchCtxManagerClassVariable", + "TorchInGraphFunctionVariable", + "TorchVersionVariable", + "TupleVariable", + "UnknownVariable", + "UnspecializedNNModuleVariable", + "UnspecializedPythonVariable", + "UntypedStorageVariable", + "UserDefinedClassVariable", + "UserDefinedTupleVariable", + "NamedTupleVariable", + "StructSequenceVariable", + "UserDefinedObjectVariable", + "UserFunctionVariable", + "UserMethodVariable", + "VariableTracker", + "WithEnterFunctionVariable", + "WithExitFunctionVariable", + "MappingProxyVariable", +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/base.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/base.py new file mode 100644 index 0000000000000000000000000000000000000000..d6101f8c8b5af35498307cf0630873fb3cdf8769 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/base.py @@ -0,0 +1,1204 @@ +""" +Core variable tracking functionality for Dynamo. This module defines the fundamental +classes and systems used to track and manage variables during Dynamo's operation. + +The module provides: +1. VariableTracker - The base class for tracking variables during compilation +2. MutationType system - Classes for tracking and managing mutations to variables +3. Source type management - Utilities for tracking variable origins and scope +4. Variable state management - Tools for managing variable state and transformations + +These components form the foundation of Dynamo's variable handling system, +enabling accurate tracking and transformation of Python code into optimized +computations. +""" + +from __future__ import annotations + +import collections +import functools +import logging +from collections.abc import Callable, ItemsView, KeysView, Sequence, ValuesView +from contextvars import ContextVar +from enum import Enum +from typing import Any, NoReturn, TYPE_CHECKING + +from .. import graph_break_hints, variables +from ..current_scope_id import current_scope_id +from ..exc import raise_observed_exception, unimplemented +from ..guards import GuardBuilder, install_guard +from ..source import AttrSource, Source +from ..utils import cmp_name_to_op_mapping, istype + + +if TYPE_CHECKING: + from torch._guards import Guard + from torch.fx.proxy import Node + + from ..codegen import PyCodegen + from ..side_effects import SideEffects + from ..symbolic_convert import InstructionTranslator + from .constant import ConstantVariable + from .functions import UserFunctionVariable + + +log = logging.getLogger(__name__) + +# Tracks active method calls on VariableTracker instances to detect self-referential +# calls (e.g., as_python_constant on a list that contains itself). Maps +# (id(instance), id(original_method)) tuples to track which calls are in progress. +# We use id(original_method) rather than the method name string so that super() +# delegation within a class hierarchy (e.g. TorchScriptObjectVariable.as_python_constant +# calling UserDefinedObjectVariable.as_python_constant) is not a false positive. +_vt_active_calls: ContextVar[set[tuple[int, int]] | None] = ContextVar( + "_vt_active_calls", default=None +) + + +class SourceType(Enum): + """ + This Enum divides VariableTracker into 2 cases, depending on the variable + it represents: + - already existed that Dynamo began tracking while introspection (Existing) + - is a new variable that is created during Dynamo introspection (New) + + In general, we have these invariants: + 1. for `VariableTracker` associated with `Existing`, its `source` field must not be None. + 2. for `VariableTracker` associated with `New`, most of the time its + `source` field is None, except for cases like side effect codegen for + `AttributeMutationNew`, during which we generate a + `LocalSource('tmp...')` for such variable, to facilitate codegen. + """ + + Existing = 0 + New = 1 + + +class MutationType: + """ + Base class for Variable.mutation_type. It encodes information about + 1. The type of mutation Dynamo allows on the variable. + 2. Whether the value represented by this variable already existed before + Dynamo tracing. + """ + + def __init__(self, typ: SourceType) -> None: + # In HigherOrderOperator tracing, we need to distinguish + # between MutationTypes inside the HigherOrderOperator and + # ones outside it. For example, it is not safe to mutate + # `a` in the following example because it was constructed + # in a different scope. + # + # def f(x): + # a = 1 + # def g(x): + # nonlocal a + # a = 2 + # return x + # return wrap(g, x) + a + # + # We use self.scope to distinguish this. + # scope == 0: The object was an existing variable + # scope == 1: The object was created while Dynamo + # was introspecting a function + # (and no HigherOrderOps were involved) + # scope >= 2: The object was created through + # Dynamo introspection of a HigherOrderOp. + # The exact number corresponds to the level + # of nested HigherOrderOps. + if typ is SourceType.Existing: + self.scope = 0 + elif typ is SourceType.New: + self.scope = current_scope_id() + else: + unimplemented( + gb_type="Unsupported SourceType", + context=f"MutationType.__init__ {self} {typ}", + explanation=f"Dynamo does not support the type `{typ}`", + hints=[ + "This branch is not supposed to be reachable.", + *graph_break_hints.DYNAMO_BUG, + ], + ) + + +class ValueMutationNew(MutationType): + """ + This case of VariableTracker.mutation_type marker indicates + 1. Dynamo allows mutation on the value itself (rather than its attributes). + 2. The value is created by the bytecode Dynamo is tracing through. + + For instance, Dynamo could model a newly created list with this marker, + indicating that while we need to model mutations to this list, we don't have + to emit bytecode for these mutations if the list doesn't escape into the + Python world. + """ + + def __init__(self) -> None: + super().__init__(SourceType.New) + + def __hash__(self) -> int: + return id(self) + + def __eq__(self, other: object) -> bool: + return self is other + + +class ValueMutationExisting(MutationType): + """ + This case of VariableTracker.mutation_type marker indicates + 1. Dynamo allows mutation on the value itself (rather than its attributes). + 2. The value exists before Dynamo tracing started. + + For instance, Dynamo could model a pre-existing list with this marker, + indicating that if we encounter mutations to this list, we need to buffer + and re-apply those mutations after the graph runs, since the list might be + used afterwards in Python. + """ + + # A flag to indicate whether mutation happened on the associated + # `VariableTracker`. This enables SideEffects to accurately and quickly + # filter out which pre-existing values it needs to generate mutation for. + is_modified: bool + + def __init__(self, is_modified: bool = False) -> None: + super().__init__(SourceType.Existing) + self.is_modified = is_modified + + +class AttributeMutation(MutationType): + """ + This case of VariableTracker.mutation_type marker indicates that Dynamo + allows mutation on the value's attributes. + """ + + +class AttributeMutationExisting(AttributeMutation): + """ + This case of VariableTracker.mutation_type marker indicates + 1. Dynamo allows mutation on the value's attributes. + 2. The value exists before Dynamo tracing started. + + For instance, Dynamo could model a pre-existing object with this marker, + indicating that if we encounter mutations to this object, we need to buffer + then re-apply those mutations after the graph runs, since the object might + be used afterwards in Python. + """ + + def __init__(self) -> None: + super().__init__(SourceType.Existing) + + +class AttributeMutationNew(AttributeMutation): + """ + This case of VariableTracker.mutation_type marker indicates + 1. Dynamo allows mutation on the value's attributes. + 2. The value is created by the bytecode Dynamo is tracing through. + + For instance, Dynamo could model a newly created object with this marker, + indicating that while we need to model mutations to this object, we don't + have to emit bytecode for these mutations if the object doesn't escape into + the Python world. + """ + + def __init__(self, cls_source: Source | None = None) -> None: + super().__init__(SourceType.New) + self.cls_source = cls_source + + +def _is_top_level_scope(scope_id: int) -> bool: + return scope_id == 1 + + +def is_side_effect_safe(m: MutationType) -> bool: + scope_id = current_scope_id() + + # In the top-level scope (if no HigherOrderOperators are involved), + # we are allowed to modify variables created in this scope as well + # as existing variables. + if _is_top_level_scope(scope_id): + return True + # Otherwise, only allow local mutation of variables created in the current scope + return m.scope == scope_id + + +class NO_SUCH_SUBOBJ: + """Sentinel indicating no concrete Python object is available.""" + + +# This helps users of `as_python_constant` to catch unimplemented error with +# more information; it inherits `NotImplementedError` for backward +# compatibility reasons. +class AsPythonConstantNotImplementedError(NotImplementedError): + vt: VariableTracker + + def __init__(self, vt: VariableTracker, msg: str | None = None) -> None: + msg = f"{vt} is not a constant" if msg is None else msg + super().__init__(msg) + self.vt = vt + + +class VariableTrackerMeta(type): + all_subclasses: list[type] = [] + + def __new__( + mcs: type, name: str, bases: tuple[type, ...], attrs: dict[str, Any] + ) -> type: + # Determine which metaclass to use based on the class attributes + # Classes with _no_implicit_realize = True should NOT implicitly realize + # (they need standard isinstance behavior to avoid infinite recursion) + # Check if any base class has _no_implicit_realize set, or if it's in attrs + no_implicit_realize = attrs.get("_no_implicit_realize", False) or any( + getattr(base, "_no_implicit_realize", False) for base in bases + ) + if no_implicit_realize or name == "VariableTracker": + # Use base VariableTrackerMeta (no custom __instancecheck__) + return super().__new__(VariableTrackerMeta, name, bases, attrs) + else: + # Use ImplicitRealizingVariableTrackerMeta for all other subclasses + return super().__new__( + ImplicitRealizingVariableTrackerMeta, name, bases, attrs + ) + + def __init__( + cls: type, name: str, bases: tuple[type, ...], attrs: dict[str, Any] + ) -> None: + super().__init__(name, bases, attrs) # type: ignore[misc] + VariableTrackerMeta.all_subclasses.append(cls) + + +class ImplicitRealizingVariableTrackerMeta(VariableTrackerMeta): + def __instancecheck__(self, instance: object) -> bool: + """Make isinstance work with LazyVariableTracker""" + if instancecheck(LazyVariableTracker, instance): + return instance.lazy_isinstance(self) # pyrefly: ignore[missing-attribute] + return instancecheck(self, instance) + + +class VariableTracker(metaclass=VariableTrackerMeta): + """ + Base class for tracked locals and stack values + + VariableTracker instances are immutable and should be copied in + order to change them. + + Prefer the factory function VariableTracker.build() over VariableTracker.__init__(). + """ + + # The CPython type(s) this VT is designed to represent. + # Single type or tuple of types. None means no static CPython type mapping + # (e.g., dynamic types like UserDefinedObjectVariable, or Dynamo-internal VTs). + _cpython_type: type | tuple[type, ...] | None = None + + # fields to leave unmodified in apply() + _nonvar_fields = { + "value", + "guards", + "source", + "mutation_type", + "parents_tracker", + "user_code_variable_name", + } + + def clone(self, **kwargs: Any) -> VariableTracker: + """Shallow copy with some (optional) changes""" + args = dict(self.__dict__) + args.update(kwargs) + return self.__class__(**args) + + @classmethod + def visit( + cls, + fn: Callable[[VariableTracker], None], + value: Any, + cache: dict[int, Any] | None = None, + side_effects: SideEffects | None = None, + ) -> None: + """ + Walk value and call fn on all the VariableTracker instances. + + When side_effects is provided, also walks attributes stored in + store_attr_mutations (e.g. dataclass fields set during tracing + that aren't in the VT's __dict__). + """ + if cache is None: + cache = {} + + idx = id(value) + if idx in cache: + return + # save `value` to keep it alive and ensure id() isn't reused + cache[idx] = value + + if isinstance(value, VariableTracker): + value = value.unwrap() + fn(value) + value = value.unwrap() # calling fn() might have realized it + nonvars = value._nonvar_fields + for key, subvalue in value.__dict__.items(): + if key not in nonvars: + cls.visit(fn, subvalue, cache, side_effects) + if side_effects is not None and value in side_effects.store_attr_mutations: + for attr_vt in side_effects.store_attr_mutations[value].values(): + cls.visit(fn, attr_vt, cache, side_effects) + elif istype(value, (list, tuple)): + for subvalue in value: + cls.visit(fn, subvalue, cache, side_effects) + elif istype(value, (dict, collections.OrderedDict)): + for subvalue in value.values(): + cls.visit(fn, subvalue, cache, side_effects) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}()" + + def debug_repr(self) -> str: + # Intended to be overridden to provide more info + try: + return repr(self.as_python_constant()) + except NotImplementedError: + return repr(self) + + def python_type(self) -> type: + """ + Abstract method to be implemented by subclasses of VariableTracker. + + This method should return the type represented by the instance of the subclass. + The purpose is to provide a standardized way to retrieve the Python type information + of the variable being tracked. + + Returns: + type: The Python type (such as int, str, list, etc.) of the variable tracked by + the subclass. If the type cannot be determined or is not relevant, + leaving it undefined or invoking super() is always sound. + + Note: + This is an abstract method and may be overridden in subclasses. + + Example: + class SetVariable(VariableTracker): + def python_type(self): + return set + + Raises: + NotImplementedError: If the method is not implemented in a subclass. + """ + try: + return type(self.as_python_constant()) + except NotImplementedError: + raise NotImplementedError(f"{self} has no type") from None + + def python_type_name(self) -> str: + try: + return self.python_type().__name__ + except NotImplementedError: + return "" + + def as_python_constant(self) -> Any: + """For constants""" + raise AsPythonConstantNotImplementedError(self) + + def guard_as_python_constant(self) -> Any: + """Similar to as_python_constant(), but add ID_MATCH guards to try to force things to become constants""" + try: + return self.as_python_constant() + except NotImplementedError: + unimplemented( + gb_type="Not a Python constant", + context=f"guard_as_python_constant {self}", + explanation=f"Failed to convert {self} into a Python constant.", + hints=[], + ) + + def is_python_constant(self) -> bool: + try: + self.as_python_constant() + return True + except NotImplementedError: + return False + + def bool_impl(self, tx: InstructionTranslator) -> VariableTracker | None: + # Mirrors CPython's tp_as_number->nb_bool slot. + # https://github.com/python/cpython/blob/c09ccd9c429/Objects/object.c#L2135-L2158 + # + # Returns None when the type has no nb_bool, causing generic_bool to + # fall through to length check, then truthy default. + return None + + def is_constant_match(self, *values: Any) -> bool: + """ + Check if this variable is a python constant matching one of the given values. + + Examples: + var.is_constant_match(None) # True if var is constant None + var.is_constant_match(True, False) # True if var is constant True or False + var.is_constant_match(NotImplemented) # True if var is constant NotImplemented + """ + return False + + def is_constant_none(self) -> bool: + """Check if this variable is a constant None value.""" + return False + + def make_guard(self, fn: Callable[..., Any]) -> Guard: + if self.source: + return self.source.make_guard(fn) + raise NotImplementedError + + # TODO[@lucaskabela] - change this type to `InstructionTranslatorBase` + # and cascade that (large blast radius) + def const_getattr(self, tx: InstructionTranslator, name: str) -> Any: + """getattr(self, name) returning a python constant""" + raise NotImplementedError + + def is_symnode_like(self) -> bool: + """Return True for values that can participate in SymNode operations""" + return False + + def is_tensor(self) -> bool: + """Return True for TensorVariable instances""" + return False + + def var_getattr(self, tx: InstructionTranslator, name: str) -> VariableTracker: + """getattr(self, name) returning a new variable""" + value = self.const_getattr(tx, name) + if not variables.ConstantVariable.is_literal(value): + raise NotImplementedError + source = self.source and AttrSource(self.source, name) + if source and not self.is_python_constant(): + # The second condition is to avoid guards on const getattr objects + # like __code__.co_argcount + install_guard(source.make_guard(GuardBuilder.CONSTANT_MATCH)) + return variables.ConstantVariable.create(value, source=source) + + def is_proxy(self) -> bool: + try: + self.as_proxy() + return True + except NotImplementedError: + return False + + def as_proxy(self) -> Any: + raise NotImplementedError(str(self)) + + def maybe_fx_node(self) -> Node | None: + try: + proxy = self.as_proxy() + import torch.fx + + if isinstance(proxy, torch.fx.Proxy): + return proxy.node + return None + except NotImplementedError: + return None + + def _contains_self_reference(self) -> bool: + """Check if this variable references itself (directly or indirectly).""" + found_self = False + + def check(vt: VariableTracker) -> None: + nonlocal found_self + if vt is self: + found_self = True + + # unwrap first iteration - otherwise we can't detect if we revisit self + for key, subvalue in self.__dict__.items(): + if key not in self._nonvar_fields: + VariableTracker.visit(check, subvalue) + + return found_self + + def reconstruct(self, codegen: PyCodegen) -> None: + raise NotImplementedError + + def unpack_var_sequence(self, tx: Any) -> list[VariableTracker]: + raise NotImplementedError + + def force_unpack_var_sequence(self, tx: Any) -> list[VariableTracker]: + # like unpack_var_sequence, but should only be used when it is + # safe to eagerly (vs. lazily) unpack this variable. + # e.g. map(f, x) is normally evaluated lazily but sometimes + # we want to force eager unpacking, e.g. when converting to a list. + # NOTE: this method is allowed to mutate the VariableTracker, so + # it should only be called once. + return self.unpack_var_sequence(tx) + + def has_unpack_var_sequence(self, tx: Any) -> bool: + try: + self.unpack_var_sequence(tx) + return True + except NotImplementedError: + return False + + # NB: don't call force_unpack_var_sequence, especially if it mutates! + def has_force_unpack_var_sequence(self, tx: Any) -> bool: + return self.has_unpack_var_sequence(tx) + + # Forces unpacking the var sequence while also applying a function to each element. + # Only use when it is safe to eagerly unpack this variable (like force_unpack_var_sequence). + # INVARIANT: variable must satisfy has_force_unpack_var_sequence() == True! + def force_apply_to_var_sequence( + self, tx: Any, fn: Callable[[VariableTracker], Any] + ) -> None: + assert self.has_force_unpack_var_sequence(tx) + for v in self.unpack_var_sequence(tx): + fn(v) + + def call_obj_hasattr( + self, tx: InstructionTranslator, name: str + ) -> ConstantVariable: + unimplemented( + gb_type="Unsupported hasattr call", + context=f"call_obj_hasattr {self} {name}", + explanation=f"Dynamo does not know how to trace the function `{self.debug_repr()}`", + hints=[ + f"Avoid calling `hasattr({self.__class__.__name__}, {name})` in your code.", + *graph_break_hints.SUPPORTABLE, + ], + ) + + def call_function( + self, + tx: Any, + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + unimplemented( + gb_type="Unsupported function call", + context=f"call_function {self} {args} {kwargs}", + explanation=f"Dynamo does not know how to trace the function `{self.debug_repr()}`", + hints=[ + f"Avoid calling `{self.debug_repr()}` in your code.", + "Please report an issue to PyTorch.", + ], + ) + + def sq_length(self, tx: Any) -> VariableTracker: + """Called when sq_length is not implemented.""" + raise_observed_exception( + TypeError, + tx, + args=[f"object of type '{self.python_type_name()}' has no len()"], + ) + + def mp_length(self, tx: Any) -> VariableTracker: + """Called when mp_length is not implemented.""" + raise_observed_exception( + TypeError, + tx, + args=[f"object of type '{self.python_type_name()}' has no len()"], + ) + + def mp_subscript_impl( + self, + tx: InstructionTranslator, + key: VariableTracker, + ) -> VariableTracker: + # PyObject_GetItem: https://github.com/python/cpython/blob/62a6e898e01/Objects/abstract.c#L155-L206 + # TODO: raise TypeError for non-subscriptable objects (blocked on + # branch 3 __class_getitem__ support for type objects). + unimplemented( + gb_type="missing_mp_subscript", + context=f"mp_subscript_impl not defined for {type(self).__name__}", + explanation=f"Dynamo does not yet support subscripting '{self.python_type_name()}'.", + hints=[*graph_break_hints.SUPPORTABLE], + ) + + def call_method( + self, + tx: Any, + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "__getitem__": + if len(args) == 1 and not kwargs: + return self.mp_subscript_impl(tx, args[0]) + from ..utils import raise_args_mismatch + + raise_args_mismatch( + tx, + name, + "1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + elif name == "__len__" and not (args or kwargs): + from .object_protocol import generic_len + + return generic_len(tx, self) + elif ( + name == "__getattr__" + and len(args) == 1 + and args[0].is_python_constant() + and not kwargs + ): + return self.var_getattr(tx, args[0].as_python_constant()) + elif name == "__index__" and not args and not kwargs: + return self.nb_index_impl(tx) + elif name == "__int__" and not args and not kwargs: + return self.nb_int_impl(tx) + elif name == "__float__" and not args and not kwargs: + return self.nb_float_impl(tx) + elif name in cmp_name_to_op_mapping and len(args) == 1 and not kwargs: + other = args[0] + if not isinstance(self, type(other)) and not ( + isinstance(self, variables.GetAttrVariable) + or isinstance(other, variables.GetAttrVariable) + ): + # NB: GetAttrVariable is a special case because sometimes an + # object can map to GetAttrVariable but other time as + # SkipFunctionVariable if it is an input to the compiled + # function, e.g. tensor.data_ptr + return variables.ConstantVariable.create(NotImplemented) + # NB : Checking for mutation is necessary because we compare + # constant values + if ( + not self.is_python_constant() + or not other.is_python_constant() + or tx.output.side_effects.has_pending_mutation(self) + or tx.output.side_effects.has_pending_mutation(other) + ): + unimplemented( + gb_type="Builtin `operator.*` comparison with constant `self` failed", + context=f"call_method {self} {name} {args} {kwargs}", + explanation=f"Failed to compare {self} with {other}, " + + f"because {other} is not a Python constant or its mutation check fails.", + hints=[], + ) + + try: + return variables.ConstantVariable.create( + cmp_name_to_op_mapping[name]( + self.as_python_constant(), other.as_python_constant() + ) + ) + except Exception as e: + raise_observed_exception( + type(e), + tx, + args=list(e.args), + ) + # __reduce_ex__ is a C builtin (object.__reduce_ex__) that Dynamo + # cannot trace into. Constant-fold it for VTs backed by a real + # Python object so that copy.deepcopy can trace through. + if ( + name == "__reduce_ex__" + and len(args) == 1 + and not kwargs + and self.is_python_constant() + ): + protocol = args[0].as_python_constant() + return VariableTracker.build( + tx, self.as_python_constant().__reduce_ex__(protocol) + ) + + hints = [ + f"Avoid calling `{self.python_type_name()}.{name}` in your code.", + "Please report an issue to PyTorch.", + ] + # additional hint for method calls on improperly constructed iterators + if isinstance(self, variables.UserDefinedObjectVariable) and name in ( + "__iter__", + "__next__", + ): + if isinstance(self.value, (KeysView, ItemsView, ValuesView)): + hints.append( + "Consider moving the creation of dict view object (e.g. `dict.keys()`, `dict.items()`,) " + "to the compiled region, instead of passing it as an input to the compiled region." + ) + hints.append( + "Dynamo does not fully support tracing builtin iterators (e.g. `map`, `zip`, `enumerate`) " + "passed in from uncompiled to compiled regions (e.g. `torch.compile(fn)(enumerate(...))`). " + "This can happen unintentionally if a previous graph break happens with a builtin iterator " + "in the local scope." + ) + hints.append( + "List/dict comprehensions in Python <= 3.11 result in implicit function calls, which Dynamo " + "cannot trace as a top level frame. Possible workarounds are (1) use a loop instead of a comprehension, " + "(2) fix any graph breaks in the function above the comprehension, (3) wrap the comprehension in a " + "function, or (4) use Python 3.12+." + ) + unimplemented( + gb_type="Unsupported method call", + context=f"call_method {self} {name} {args} {kwargs}", + explanation=f"Dynamo does not know how to trace method `{name}` of class `{self.python_type_name()}`", + hints=hints, + ) + + def call_tree_map( + self, + tx: Any, + tree_map_fn: UserFunctionVariable, + map_fn: VariableTracker, + rest: Sequence[VariableTracker], + tree_map_kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + """Performance optimization to implement optree.tree_map faster than tracing it""" + is_leaf_var = tree_map_kwargs.get("is_leaf") + if is_leaf_var is not None and not is_leaf_var.is_constant_none(): + pred_result = is_leaf_var.call_function(tx, [self], {}) + try: + leaf_decision = pred_result.as_python_constant() + except NotImplementedError: + return self._tree_map_fallback( + tx, + tree_map_fn, + map_fn, + rest, + tree_map_kwargs, + ) + if leaf_decision: + return map_fn.call_function(tx, [self, *rest], {}) + + return self.call_tree_map_branch( + tx, + tree_map_fn, + map_fn, + rest, + tree_map_kwargs, + ) + + def call_tree_map_branch( + self, + tx: Any, + tree_map_fn: UserFunctionVariable, + map_fn: VariableTracker, + rest: Sequence[VariableTracker], + tree_map_kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + """Emulate optree.tree_map without is_leaf/none_is_leaf checks (handled above)""" + return self._tree_map_fallback( + tx, + tree_map_fn, + map_fn, + rest, + tree_map_kwargs, + ) + + def _tree_map_fallback( + self, + tx: Any, + tree_map_fn: UserFunctionVariable, + map_fn: VariableTracker, + rest: Sequence[VariableTracker], + tree_map_kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + tree_map_fn_copy = tree_map_fn.clone() + tree_map_fn_copy._maybe_call_tree_map_fastpath = lambda *args, **kwargs: None # type: ignore[missing-attribute] + log.debug( + "tree_map fastpath fallback triggered for %s (rest=%s, kwargs=%s)", + self, + rest, + tree_map_kwargs, + ) + return tree_map_fn_copy.call_function( + tx, + [map_fn, self, *rest], + tree_map_kwargs, + ) + + def call_tree_map_with_path( + self, + tx: Any, + tree_map_fn: UserFunctionVariable, + map_fn: VariableTracker, + rest: Sequence[VariableTracker], + tree_map_kwargs: dict[str, VariableTracker], + keypath: tuple[Any, ...], + ) -> VariableTracker: + """Performance optimization to implement tree_map_with_path faster than tracing it""" + is_leaf_var = tree_map_kwargs.get("is_leaf") + if is_leaf_var is not None and not is_leaf_var.is_constant_none(): + pred_result = is_leaf_var.call_function(tx, [self], {}) + try: + leaf_decision = pred_result.as_python_constant() + except NotImplementedError: + return self._tree_map_with_path_fallback( + tx, + tree_map_fn, + map_fn, + rest, + tree_map_kwargs, + keypath, + ) + if leaf_decision: + keypath_var = variables.TupleVariable( + [VariableTracker.build(tx, k) for k in keypath] + ) + return map_fn.call_function(tx, [keypath_var, self, *rest], {}) + + return self.call_tree_map_with_path_branch( + tx, + tree_map_fn, + map_fn, + rest, + tree_map_kwargs, + keypath, + ) + + def call_tree_map_with_path_branch( + self, + tx: Any, + tree_map_fn: UserFunctionVariable, + map_fn: VariableTracker, + rest: Sequence[VariableTracker], + tree_map_kwargs: dict[str, VariableTracker], + keypath: tuple[Any, ...], + ) -> VariableTracker: + """Handle tree_map_with_path for leaf nodes (default behavior)""" + keypath_var = variables.TupleVariable( + [VariableTracker.build(tx, k) for k in keypath] + ) + return map_fn.call_function(tx, [keypath_var, self, *rest], {}) + + def _tree_map_with_path_fallback( + self, + tx: Any, + tree_map_fn: UserFunctionVariable, + map_fn: VariableTracker, + rest: Sequence[VariableTracker], + tree_map_kwargs: dict[str, VariableTracker], + keypath: tuple[Any, ...], + ) -> VariableTracker: + tree_map_fn_copy = tree_map_fn.clone() + tree_map_fn_copy._maybe_call_tree_map_fastpath = lambda *args, **kwargs: None # type: ignore[missing-attribute] + log.debug( + "tree_map_with_path fastpath fallback triggered for %s (rest=%s, kwargs=%s, keypath=%s)", + self, + rest, + tree_map_kwargs, + keypath, + ) + return tree_map_fn_copy.call_function( + tx, + [map_fn, self, *rest], + tree_map_kwargs, + ) + + def set_name_hint(self, name: str) -> None: + pass + + def realize(self) -> VariableTracker: + """Used by LazyVariableTracker to build the real VariableTracker""" + return self + + def unwrap(self) -> VariableTracker: + """Used by LazyVariableTracker to return the real VariableTracker if it already exists""" + return self + + def is_realized(self) -> bool: + """Used by LazyVariableTracker to indicate an unrealized node""" + return True + + def next_variable(self, tx: Any) -> VariableTracker: + unimplemented( + gb_type="Unsupported next() call", + context=f"next({self})", + explanation=f"Dynamo does not know how to trace calling `next()` on variable `{self}`.", + hints=[*graph_break_hints.USER_ERROR], + ) + + def is_strict_mode(self, tx: Any) -> bool: + return bool(tx.strict_checks_fn and tx.strict_checks_fn(self)) + + def is_mutable(self) -> bool: + """Whether Dynamo allows mutation on this variable.""" + return not self.is_immutable() + + def is_immutable(self) -> bool: + """Whether Dynamo bans mutation on this variable.""" + return self.mutation_type is None + + @staticmethod + def build( + tx: Any, + value: Any, + source: Source | None = None, + realize: bool = False, + ) -> Any: + """Create a new VariableTracker from a value and optional Source""" + if source is None: + return builder.SourcelessBuilder.create(tx, value) + elif realize: + return builder.VariableBuilder(tx, source)(value) + elif type(value) in variables.LazyConstantVariable.supported_types: + # Use LazyConstantVariable for primitives to enable deferred + # guard installation - constants that are just passed through + # won't cause recompilation when their values change. + return variables.LazyConstantVariable.create(value, source) + else: + return variables.LazyVariableTracker.create(value, source) + + def is_python_hashable(self) -> bool: + """ + Unlike the variable tracker's own __hash__, this method checks whether + the underlying Python object referenced by this variable tracker is hashable. + """ + try: + type_self = self.python_type() + except NotImplementedError: + type_self = type(self) + + unimplemented( + gb_type="Dynamo cannot determine whether the underlying object is hashable", + context=f"is_python_hashable {self}", + explanation=f"Dynamo does not know whether the underlying python object for {self} is hashable", + hints=[ + ( + f"Consider using a different type of object as the dictionary key instead of {type_self}." + ), + *graph_break_hints.SUPPORTABLE, + ], + ) + + def get_python_hash(self) -> int: + """ + Unlike the variable tracker’s own __hash__, this method is used by + ConstDictVariableTracker to compute the hash of the underlying key object. + """ + unimplemented( + gb_type="Dynamo cannot determine the hash of an object", + context=f"get_python_hash {self}", + explanation=f"Dynamo does not know the hash of the underlying python object for {self}", + hints=[ + ( + f"Consider using a different type of object as the dictionary key instead of {self.python_type()}." + ), + *graph_break_hints.SUPPORTABLE, + ], + ) + + def get_real_python_backed_value(self) -> object: + """Return the Python object this VT wraps, for `is` comparison. + + Returns NO_SUCH_SUBOBJ if no concrete Python object is available. + """ + return NO_SUCH_SUBOBJ + + def is_python_equal(self, other: object) -> bool: + """ + NB - Deliberately not overriding the __eq__ method because that can + disable the __hash__ for the vt itself. + """ + unimplemented( + gb_type="Dynamo cannot determine the equality comparison of an object", + context=f"is_python_equal {self}", + explanation=f"Dynamo does not know the equality comparison of the underlying python object for {self}", + hints=[ + ( + f"Consider using a different type of object as the dictionary key instead of {self.python_type()}." + ), + *graph_break_hints.SUPPORTABLE, + ], + ) + + def nb_index_impl( + self, + tx: Any, + ) -> VariableTracker: + """Mirrors CPython's PyNumber_Index / nb_index slot. + + https://github.com/python/cpython/blob/c09ccd9c429/Objects/abstract.c#L1411-L1450 + + The base implementation raises TypeError, matching CPython's behavior + when tp_as_number->nb_index is NULL (_PyIndex_Check fails). + """ + raise_observed_exception( + TypeError, + tx, + args=[ + f"'{self.python_type_name()}' object cannot be interpreted as an integer" + ], + ) + + def nb_int_impl( + self, + tx: Any, + ) -> VariableTracker: + """Mirrors CPython's tp_as_number->nb_int slot. + + Called when type_implements_nb_int returns True for this type. + Subclasses override to provide the actual conversion. + """ + unimplemented( + gb_type="nb_int_impl not implemented", + context=f"{type(self).__name__} has nb_int slot but no nb_int_impl override", + explanation=f"The type {self.python_type_name()} has an nb_int C slot but " + "the corresponding VariableTracker doesn't implement nb_int_impl.", + hints=[*graph_break_hints.SUPPORTABLE], + ) + + def nb_float_impl( + self, + tx: Any, + ) -> VariableTracker: + """Mirrors CPython's tp_as_number->nb_float slot. + + Called when type_implements_nb_float returns True for this type. + Subclasses override to provide the actual conversion. + """ + unimplemented( + gb_type="nb_float_impl not implemented", + context=f"{type(self).__name__} has nb_float slot but no nb_float_impl override", + explanation=f"The type {self.python_type_name()} has an nb_float C slot but " + "the corresponding VariableTracker doesn't implement nb_float_impl.", + hints=[*graph_break_hints.SUPPORTABLE], + ) + + def __init__( + self, + *, + source: Source | None = None, + mutation_type: MutationType | None = None, + ) -> None: + super().__init__() + self.source = source + self.mutation_type = mutation_type + + # NOTE sometimes mutation_type is set afterwards for implementation + # convenience, we don't validate those cases at the moment. + if mutation_type is not None: + if isinstance(mutation_type, (ValueMutationNew, AttributeMutationNew)): + # If this fails, it's either + # 1. one mistakenly passed in a source + # 2. `mutation_type` is incorrect + assert source is None + else: + assert isinstance( + mutation_type, (ValueMutationExisting, AttributeMutationExisting) + ) + # If this fails, it's either + # 1. one forgot to pass in a source + # 2. `mutation_type` is incorrect + assert source is not None + + def __init_subclass__(cls, **kwargs: Any) -> None: + """ + Wraps all subclasses' `as_python_constant` and `reconstruct` so that it cannot be + called twice in the same call chain - i.e. self-referential objects. + For `as_python_constant` - self-referential objects are NOT treated as constants. + For `reconstruct` - we will graph break. The graph break can be avoided if the VT subclass + can generate and cache itself before recursively `reconstruct`ing - see ListVariable for an example. + """ + super().__init_subclass__(**kwargs) + + def as_python_constant_failure(self) -> NoReturn: + raise AsPythonConstantNotImplementedError( + self, msg=f"{self} is self-referential" + ) + + VariableTracker._add_call_once_guard( + cls, "as_python_constant", as_python_constant_failure + ) + + def reconstruct_failure(self) -> NoReturn: + unimplemented( + gb_type="Reconstruction failure (self-referential)", + context=str(self), + explanation=f"Dynamo tried to reconstruct sourceless variable {self}, but it is self-referential. " + "Dynamo must manually implement reconstruction rules for self-referentiable sourceless variables.", + hints=[ + "If Dynamo is attempting to trace a return statement and your code is attempting to return a variable " + "that Dynamo cannot reconstruct, then remove it from the return statement.", + "Remove the self-reference in the variable. A self-referring list, for example, is `l = []; l.append(l)`.", + *graph_break_hints.CAUSED_BY_EARLIER_GRAPH_BREAK, + "Report an issue to PyTorch if you need self-referential reconstrtuction support.", + ], + ) + + VariableTracker._add_call_once_guard(cls, "reconstruct", reconstruct_failure) + + @staticmethod + def _add_call_once_guard( + cls: type[VariableTracker], + method: str, + callback: Callable[[VariableTracker], Any], + ) -> None: + original_method = getattr(cls, method) + + if original_method is getattr(VariableTracker, method) or hasattr( + original_method, "_call_once_guarded" + ): + return + + @functools.wraps(original_method) + def guarded_method(self, *args: Any, **kwargs: Any) -> VariableTracker: + active = _vt_active_calls.get() + if active is None: + active = set() + _vt_active_calls.set(active) + + key = (id(self), id(original_method)) + if key in active: + callback(self) + + active.add(key) + try: + return original_method(self, *args, **kwargs) + finally: + active.discard(key) + + guarded_method._call_once_guarded = True # pyrefly: ignore[missing-attribute] + setattr(cls, method, guarded_method) + + +def typestr(*objs: object) -> str: + if len(objs) == 1: + (obj,) = objs + if isinstance(obj, VariableTracker): + return str(obj) + else: + return type(obj).__name__ + else: + return " ".join(map(typestr, objs)) + + +instancecheck = type.__instancecheck__ + + +_CPYTHON_BASE_URL = "https://github.com/python/cpython/blob/v3.13.0/" + + +def print_cpython_to_vt_mapping() -> None: + """Print the mapping from CPython types to Dynamo VariableTracker classes. + + Reads _cpython_type from each VT subclass (own attribute only, not inherited). + """ + # Ensure all VT modules are imported so all_subclasses is complete + from . import ( # noqa: F401 + builtin, + constant, + ctx_manager, + dicts, + distributed, + functions, + higher_order_ops, + iter, + lazy, + lists, + misc, + nn_module, + streams, + tensor, + torch, + torch_function, + user_defined, + ) + + mapping: dict[type, list[type]] = {} + for vt_cls in VariableTrackerMeta.all_subclasses: + cpython_type = vt_cls.__dict__.get("_cpython_type") + if cpython_type is None: + continue + types = cpython_type if isinstance(cpython_type, tuple) else (cpython_type,) + for t in types: + mapping.setdefault(t, []).append(vt_cls) + + for py_type, vt_classes in sorted(mapping.items(), key=lambda x: x[0].__qualname__): + for vt_cls in vt_classes: + print( + f"{py_type.__module__}.{py_type.__qualname__:30s} -> {vt_cls.__name__}" + ) + + +from . import builder +from .lazy import LazyVariableTracker diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/builder.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/builder.py new file mode 100644 index 0000000000000000000000000000000000000000..482af7ae134809fcab816d1c3bead02c5949f4ad --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/builder.py @@ -0,0 +1,4492 @@ +""" +This module contains classes and utilities for building variable trackers in Dynamo. +Variable trackers are used to convert Python values into symbolic representations +that can be traced and transformed during graph capture. + +The key classes are: + +- VariableBuilder: Handles source-tracked objects that need guards and proper + reconstruction in the output graph. Used for inputs, module attributes, etc. + +- SourcelessBuilder: Handles ephemeral objects created during tracing that don't + need source tracking or guards. Used for temporary lists, intermediate values, etc. + +Variable trackers enable Dynamo to track the flow of values through the program, +maintain guards for dynamic properties, and reconstruct values in the output graph. +The builders in this module handle converting Python values into appropriate +VariableTracker instances based on their type and usage context. +""" + +import abc +import collections +import contextlib +import copy +import dataclasses +import enum +import functools +import inspect +import itertools +import logging +import math +import operator +import random +import re +import sys +import time +import types +import weakref +from collections.abc import Callable, MutableMapping +from types import ModuleType +from typing import Any, NamedTuple, NoReturn, overload, TYPE_CHECKING, Union + +import sympy + +import torch +from torch import SymInt +from torch._dispatch.python import enable_python_dispatcher +from torch._dynamo.graph_bytecode_inputs import ( + CURRENT_STREAM_INDEX, + get_external_object_by_index, + register_user_object, +) +from torch._dynamo.utils import ( + get_metrics_context, + is_int_specialization_case, + is_torch_sym, + normalize_count_iter, + set_feature_use, +) +from torch._guards import TracingContext +from torch._higher_order_ops.flat_apply import flat_apply +from torch._higher_order_ops.torchbind import call_torchbind +from torch._library.opaque_object import ( + is_opaque_reference_type, + is_opaque_type, + is_opaque_value_type, + should_hoist, +) +from torch._opaque_base import OpaqueBase +from torch._ops import HigherOrderOperator, OpOverload, OpOverloadPacket +from torch._subclasses.fake_tensor import ( + FakeTensor, + FakeTensorMode, + is_fake, + maybe_get_fake_mode, +) +from torch._subclasses.meta_utils import is_sparse_any, safe_grad +from torch._utils_internal import justknobs_check +from torch.fx.experimental._backward_state import BackwardState +from torch.fx.experimental._dynamism import normalize_source_name +from torch.fx.experimental.sym_node import _DynamicScalar, DynamicInt +from torch.fx.experimental.symbolic_shapes import ( + _constrain_range_for_size, + _nested_int_aware_sort, + DimDynamic, + RelaxedUnspecConstraint, + StatefulSymbolicContext, + SubclassSymbolicContext, + SymbolicContext, + SymIntSymbolicContext, + TrackedFake, +) +from torch.fx.immutable_collections import immutable_dict, immutable_list +from torch.nn.utils._expanded_weights import ExpandedWeight +from torch.utils._ordered_set import OrderedSet +from torch.utils._python_dispatch import ( + is_traceable_wrapper_subclass, + is_traceable_wrapper_subclass_type, +) +from torch.utils._sympy.value_ranges import ValueRanges +from torch.utils.weak import TensorWeakRef + +from .. import config, graph_break_hints, mutation_guard, replay_record, trace_rules +from ..device_interface import get_registered_device_interfaces +from ..exc import InternalTorchDynamoError, raise_observed_exception, unimplemented +from ..guards import GuardBuilder, install_guard, make_dupe_guard +from ..pgo import ( + auto_dynamic, + auto_unset, + FrameStateSizeEntry, + InferStride, + process_automatic_dynamic, +) +from ..side_effects import SideEffects +from ..source import ( + AttrProxySource, + AttrSource, + CallMethodItemSource, + ChainedSource, + ConstDictKeySource, + ConvertIntSource, + CurrentStreamSource, + DictGetItemSource, + DictSubclassGetItemSource, + DynamicScalarSource, + FloatTensorSource, + GetItemSource, + GradSource, + is_constant_source, + is_from_closure_source, + is_from_global_source, + is_from_nonlocal_source, + is_from_optimizer_source, + is_from_unspecialized_nn_module_source, + ListGetItemSource, + LocalSource, + NonSerializableSetGetItemSource, + NumpyTensorSource, + OptimizerSource, + RandomValueSource, + SkipGuardSource, + Source, + SubclassAttrListSource, + TupleIteratorGetItemSource, + UnspecializedBuiltinNNModuleSource, + UnspecializedNNModuleSource, +) +from ..utils import ( + _extract_tensor_dict, + build_checkpoint_variable, + build_invoke_subgraph_variable, + clone_input, + common_constant_types, + dict_keys, + enumerate_items_with_dict_position, + get_fake_value, + get_locals_to_steal, + get_static_address_type, + is_frozen_dataclass, + is_function, + is_function_or_wrapper, + is_invoke_subgraph, + is_lru_cache_wrapped_function, + is_namedtuple, + is_parameter_freezing, + is_typing, + is_utils_checkpoint, + is_wrapper_or_member_descriptor, + istype, + namedtuple_fields, + odict_values, + proxy_args_kwargs, + range_iterator, + set_example_value, + tensor_always_has_static_shape, + tuple_iterator, + tuple_iterator_getitem, + tuple_iterator_len, + unwrap_with_attr_name_if_wrapper, + wrap_fake_exception, +) +from .base import ( + AttributeMutationExisting, + AttributeMutationNew, + typestr, + ValueMutationExisting, + ValueMutationNew, + VariableTracker, + VariableTrackerMeta, +) +from .builtin import BuiltinVariable +from .constant import ConstantVariable +from .ctx_manager import ( + AutocastModeVariable, + CudagraphOverrideVariable, + DynamoConfigPatchVariable, + ErrorOnGraphBreakVariable, + NullContextVariable, + PreserveVersionContextVariable, +) +from .dicts import ( + ConstDictVariable, + DefaultDictVariable, + MappingProxyVariable, + SetVariable, +) +from .distributed import WorldMetaClassVariable +from .functions import ( + BuiltinMethodVariable, + CollectionsNamedTupleFunction, + CollectiveFunctionRewriteVariable, + CreateTMADescriptorExperimentalVariable, + CreateTMADescriptorStableVariable, + FunctoolsPartialVariable, + SysFunctionVariable, + TritonKernelVariable, + TritonSetAllocatorVariable, + UserFunctionVariable, + WrapperUserFunctionVariable, +) +from .higher_order_ops import ( + LocalMapWrappedHigherOrderVariable, + TorchHigherOrderOperatorVariable, +) +from .iter import CountIteratorVariable, ItertoolsVariable +from .lazy import LazyConstantVariable, LazyVariableTracker +from .lists import ( + BaseListVariable, + ListIteratorVariable, + ListVariable, + RangeVariable, + SizeVariable, + SliceVariable, + TupleIteratorVariable, + TupleVariable, +) +from .misc import ( + AutogradEngineVariable, + AutogradFunctionContextVariable, + AutogradFunctionVariable, + ComptimeVariable, + ConstantLikeVariable, + DebuggingVariable, + DelayGraphBreakVariable, + GetAttrVariable, + GetSetDescriptorVariable, + IgnoredFunctionVariable, + LambdaVariable, + LoggingLoggerVariable, + MethodWrapperVariable, + NumpyDTypeVariable, + NumpyVariable, + ObjectVariable, + PythonModuleVariable, + RandomClassVariable, + RandomVariable, + SavedTensorBox, + StringFormatVariable, + TorchVersionVariable, + TypingVariable, + WeakRefVariable, +) +from .nn_module import ( + FSDPManagedNNModuleVariable, + UnspecializedBuiltinNNModuleVariable, + UnspecializedNNModuleVariable, +) +from .optimizer import OptimizerVariable +from .script_object import OpaqueObjectClassVariable, TorchScriptObjectVariable +from .sdpa import SDPAParamsVariable +from .sets import ( + DictKeySetVariable, + FrozensetVariable, + OrderedSetClassVariable, + OrderedSetVariable, +) +from .streams import EventVariable, StreamContextVariable, StreamVariable +from .tensor import ( + NumpyNdarrayVariable, + supported_const_comparison_op_values, + SymNodeVariable, + TensorSubclassVariable, + TensorVariable, + UnspecializedPythonVariable, +) +from .torch import ( + DispatchKeySetVariable, + FuncTorchInterpreterVariable, + TorchCtxManagerClassVariable, + TorchInGraphFunctionVariable, +) +from .torch_function import ( + TensorWithTFOverrideVariable, + torch_function_mode_stack_state_mgr, + TorchFunctionModeVariable, +) +from .user_defined import ( + FrozenDataClassVariable, + InspectVariable, + IntWrapperVariable, + KeyedJaggedTensorVariable, + MutableMappingVariable, + SourcelessGraphModuleVariable, + UserDefinedClassVariable, + UserDefinedConstantVariable, + UserDefinedDictVariable, + UserDefinedExceptionClassVariable, + UserDefinedListVariable, + UserDefinedObjectVariable, + UserDefinedSetVariable, + UserDefinedTupleVariable, +) + + +try: + import numpy as np +except ModuleNotFoundError: + np: ModuleType = None # type: ignore[assignment] + + +if TYPE_CHECKING: + from torch._dynamo.codegen import PyCodegen + from torch._dynamo.symbolic_convert import ( + InstructionTranslator, + InstructionTranslatorBase, + ) + + +log = logging.getLogger(__name__) +static_inputs_log = torch._logging.getArtifactLogger( + __name__, "cudagraph_static_inputs" +) +from typing import TypeVar + + +# Placeholder for a VariableTracker to be used in proxy +# creation so that we don't type erase +VTTypeAlias = TypeVar("VTTypeAlias") +T = TypeVar("T") + +DimList = list + + +def safe_has_grad(t: object) -> bool: + with torch._logging.hide_warnings(torch._logging._internal.safe_grad_filter): + return hasattr(t, "grad") + + +class _missing: + pass + + +@dataclasses.dataclass +class GraphArg: + source: Source | None + # TODO: storing a SymInt here but not a FakeTensor is a pretty strange + # thing to do. Probably should have example (which stores an int) and + # fake_example + _example: Any + # When True, this indicates that this GraphArg is a Python quantity (e.g., + # a float or int) which we pass to the FX graph as a Tensor. This + # controls how we codegen calls into the Dynamo graph: we will call + # torch.as_tensor on the quantity before passing it in. + # + # Note that we typically do not pass dynamic integers as tensors, because + # they will most frequently just be used for size computation. But this + # is a policy decision that we can change our mind on; in particular, when + # an int comes from a random number generator (e.g., random.randint), we + # DO pass it as a tensor. + # + # It's also worth noting that our current tracing rules for + # pass_arg_as_tensor as subtly broken: we just pun the variable as a + # 0d scalar Tensor and pray that the semantics are the same. Which they + # often are, but not necessarily. ezyang(May 2024) plans to fix this + # soon. + pass_arg_as_tensor: bool + fake_tensor: torch._subclasses.fake_tensor.FakeTensor | None + # UnspecializedPythonVariable often masquerades as a tensor. + # We MUST NOT generate shape guard code + # that actually tries to access tensor properties on these values. + # is_tensor lets us tell if this graph arg actually is a tensor + # or not. + is_tensor: bool = True + # Sometimes, the Tensor we pass to example is freshly allocated (smh). + # Then we cannot only keep a weak reference to it. This lets you + # stash a strong reference too. + example_strong_ref: torch.Tensor | torch.SymInt | None = None + + def __setattr__(self, name: str, value: Any) -> None: + # Use object.__setattr__ to bypass Dynamo's STORE_ATTR interception. + # This is needed because when PYTORCH_TEST_WITH_DYNAMO=1, even internal + # GraphArg creation can be traced, and with replay_side_effects=False, + # normal STORE_ATTR bytecode only records mutations without applying them. + object.__setattr__(self, name, value) + + @property + def example(self) -> torch.Tensor | torch.SymInt | BackwardState | None: + if isinstance(self._example, TensorWeakRef): + r = self._example() + assert r is not None + return r + else: + return self._example + + def __post_init__(self) -> None: + if isinstance(self._example, torch.Tensor): + self._example = TensorWeakRef(self._example) + assert is_fake(self.fake_tensor) + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen(self.source) + + def erase(self) -> None: + self._example = None + self.example_strong_ref = None + + def __eq__(self, other: object) -> bool: + if not isinstance(other, GraphArg): + return False + if self.source is None: + return other.source is None + else: + if other.source is None: + return False + return self.source.name == other.source.name + + +class BackwardStateGraphArg(GraphArg): + def __init__(self) -> None: + super().__init__( + source=None, + _example=BackwardState(), + pass_arg_as_tensor=False, + fake_tensor=None, + is_tensor=False, + ) + + def reconstruct(self, codegen: "PyCodegen") -> None: + assert codegen.tx.output.backward_state_var + codegen.add_push_null( + lambda: codegen.load_import_from(BackwardState.__module__, "BackwardState") + ) + codegen.call_function(0, False) + codegen.dup_top() + codegen.store(codegen.tx.output.backward_state_var) + + +# All class-based iterators in itertools +# NOTE: use id() because some objects are not hashable, it will raise error during lookup +ITERTOOLS_TYPE_IDS: frozenset[int] = frozenset( + id(member) + for name, member in vars(itertools).items() + if not name.startswith("_") and inspect.isclass(member) +) +# Will be updated later in substitute_in_graph in torch/_dynamo/polyfills/itertools.py +ITERTOOLS_POLYFILLED_TYPE_IDS: set[int] = set() + +# Capture fn pointer at import time +# This is to guard against trying to mark the iterated tensors +# as static in case user overrides fn ptr +og_module_named_buffers_fn_ptr = torch.nn.Module.named_buffers +og_module_named_parameters_fn_ptr = torch.nn.Module.named_parameters + + +class VariableBuilder: + """Wrap a python value in a VariableTracker() instance""" + + def __init__( + self, + tx: "InstructionTranslator", + source: Source, + allow_lazy_constant: bool = True, + ) -> None: + assert source is not None, ( + "Consider SourcelessBuilder for ephemeral objects, usually objects created locally." + ) + assert TracingContext.try_get() is not None, "Expected active TracingContext" + super().__init__() + self.tx = tx + self.source = source + self.name = source.name + # allow_lazy_constant controls whether LazyConstantVariable can be returned + # for int/float/bool/str. Set to False when called from LazyCache.realize() + # to prevent double-wrapping (LazyVariableTracker containing LazyConstantVariable). + self.allow_lazy_constant = allow_lazy_constant + + def __call__(self, value: object) -> VariableTracker: + _t0 = time.time_ns() + try: + return self._call_impl(value) + finally: + self.tx.output.bytecode_tracing_timings.variable_builder_call_ns += ( + time.time_ns() - _t0 + ) + + def _call_impl(self, value: object) -> VariableTracker: + self.tx.output.current_tracer.traced_sources.add(self.source) + if value in self.tx.output.side_effects: + side_effect_result = self.tx.output.side_effects[value] + dup_guard = make_dupe_guard(self.source, side_effect_result.source) + if dup_guard: + self.install_guards(dup_guard) + + if isinstance(value, torch.nn.Module) and isinstance( + side_effect_result, UnspecializedNNModuleVariable + ): + # This means that two nn module instances with different sources + # have the same id. NN modules are somewhat special objects, + # because we have to track their nn_module_stack for ease of + # use. But if we don't do anything, we will just return the + # older variable tracker with the older nn_module_stack. So, + # lets return the old variable tracker but update its + # nn_module_stack + side_effect_result.set_nn_module_stack_source(self.source) + return side_effect_result + + cached_vt = self.tx.output.variable_tracker_cache.get(self.source) + if cached_vt: + # If allow_lazy_constant=False but the cached VT is a lazy variable, + # we need to rebuild to get a non-lazy version. This happens when + # LazyConstantVariable.realize() calls VariableBuilder. + if self.allow_lazy_constant or not isinstance( + cached_vt, LazyVariableTracker + ): + return cached_vt + + vt = self._wrap(value) + + if vt.source is None: + vt.source = self.source + + def _is_deduplicable_sym_variable(value: Any, vt: VariableTracker) -> bool: + # Constants like 0, 1, 2, etc. can be unspecialized as SymNodeVariables sometimes, but we + # should NOT track them. If we use a single SymNodeVariable instance to track them + # across multiple uses, then guards created for one usage will incorrectly apply to + # all other usages of that constant, leading to unnecessary recompilations. + return ( + is_torch_sym(value) or isinstance(value, _DynamicScalar) + ) and isinstance(vt, SymNodeVariable) + + if ( + ( + self._can_lift_attrs_to_inputs(vt) + or _is_deduplicable_sym_variable(value, vt) + ) + and value not in self.tx.output.side_effects + and not is_wrapper_or_member_descriptor(value) + ): + vt = self.tx.output.side_effects.track_object_existing(value, vt) + + # Skip caching for JVP_NESTING source because + # JvpIncrementNestingCtxManagerVariable hides global JVP mutation from + # Dynamo, resulting in stale value. We attempted a fix in + # https://github.com/pytorch/pytorch/pull/174329 but it exposed other + # issues. This only affects cache hit rate, NOT correctness. + if "JVP_NESTING" not in self.source.name: + self.tx.output.variable_tracker_cache[self.source] = vt + return vt + + def _can_lift_attrs_to_inputs(self, vt: VariableTracker) -> bool: + return type(vt) in { + TensorVariable, + TensorWithTFOverrideVariable, + UserDefinedObjectVariable, + NumpyNdarrayVariable, + TorchScriptObjectVariable, + } + + def get_source(self) -> Source: + return self.source + + def install_guards(self, *guards: Callable[..., Any]) -> dict[str, Any] | None: + source = self.get_source() + try: + tmp = [source.make_guard(guard) for guard in guards] + except NotImplementedError: + return None + install_guard(*tmp, skip=1) + return {} + + @classmethod + def _type_dispatch(cls) -> dict[object, Callable[..., Any]]: + return cls._type_dispatch_impl(config.trace_numpy) + + @classmethod + @functools.cache + def _type_dispatch_impl(cls, trace_numpy: bool) -> dict[object, Callable[..., Any]]: + # NB: Careful not to close over self to avoid ref cycle from lru_cache + entries = [ + ( + ( + torch.Tensor, + torch.nn.Parameter, + torch._subclasses.FakeTensor, + torch._subclasses.functional_tensor.FunctionalTensor, + ), + cls.wrap_tensor, + ), + ( + (tuple, list, odict_values, collections.deque, torch.Size), + cls.wrap_listlike, + ), + (itertools.count, cls.wrap_itertools_count), + (tuple_iterator, cls.wrap_tuple_iterator), + (range_iterator, cls.wrap_range_iterator), + ((slice, range), cls.wrap_slice_range), + (tuple(common_constant_types), cls.wrap_literal), + (re.Pattern, cls.wrap_regex_pattern), + (weakref.ReferenceType, cls.wrap_weakref), + (torch.utils.hooks.RemovableHandle, cls.wrap_removable_handle), + (torch.jit.ScriptFunction, cls.wrap_jit_function), + (types.MappingProxyType, cls.wrap_mapping_proxy), + ] + + if trace_numpy and np: + # pyrefly: ignore [bad-argument-type] + entries.append((np.ndarray, cls.wrap_numpy_ndarray)) + + # pyrefly: ignore [implicit-any] + result = {} + for ts, fn in entries: + for t in ts if isinstance(ts, tuple) else (ts,): + assert t not in result + result[t] = fn + + return result + + def wrap_regex_pattern(self, value: re.Pattern[Any]) -> ConstantLikeVariable: + # TODO(jansel): something like a REPR_MATCH might be more robust here + self.install_guards(GuardBuilder.ID_MATCH) + return ConstantLikeVariable(value) + + def wrap_weakref(self, value: weakref.ReferenceType[Any]) -> WeakRefVariable: + self.install_guards(GuardBuilder.TYPE_MATCH) + return WeakRefVariable.build(self.tx, value, source=self.source) + + def wrap_removable_handle( + self, value: torch.utils.hooks.RemovableHandle + ) -> NoReturn: + # This means that the removable handle was created in some other frame. + # Our current infra requires the hook to be registered and removed in + # the same frame. So graph break. + # Related test - PYTORCH_TEST_WITH_DYNAMO=1 python test/test_autograd.py -k TestAutograd.test_hooks + unimplemented( + gb_type="Attempted to represent unregistered RemovableHandle", + context="", + explanation="Dynamo attempted to build a representation of a torch.utils.hooks.RemovableHandle, " + "which is not supported. This happens because the RemovableHandle was created in another frame.", + hints=[], + ) + + def wrap_jit_function(self, value: Any) -> WrapperUserFunctionVariable: + if not hasattr(value, "_torchdynamo_inline"): + unimplemented( + gb_type="wrap_jit_function: missing _torchdynamo_inline", + context=f"type: {type(value).__name__}", + explanation="Dynamo expected a JIT function with a _torchdynamo_inline attribute, " + "but the object does not have one.", + hints=[*graph_break_hints.SUPPORTABLE], + ) + self.install_guards(GuardBuilder.TYPE_MATCH) + return WrapperUserFunctionVariable( + value, + "_torchdynamo_inline", + source=self.source, + mutation_type=AttributeMutationExisting(), + ) + + def wrap_mapping_proxy(self, value: Any) -> VariableTracker: + self.install_guards(GuardBuilder.TYPE_MATCH) + # This might be suboptimal compared to dict guards. But mappingproxy is + # not very common, so its ok to guard on all keys. + self.install_guards(GuardBuilder.MAPPING_KEYS_CHECK) + all_const = all(ConstantVariable.is_literal(k) for k in value) + + if not all_const: + unimplemented( + gb_type="non-const keys in mappingproxy", + context=f"non-const keys: {[k for k in value.keys() if not ConstantVariable.is_literal(k)]}", # noqa: SIM118 + explanation="Dynamo expects mappingproxy keys to be constants.", + hints=[ + "Ensure your mappingproxy keys are constants (e.g. int, float, strings)", + ], + ) + + def build_key_value(k: Any, v: Any) -> tuple[VariableTracker, VariableTracker]: + key = ConstantVariable.create(k) + source_key = k + + source_value = GetItemSource(self.get_source(), source_key) + res_value = LazyVariableTracker.create(v, source_value) + + return key, res_value + + items = dict(build_key_value(k, v) for k, v in value.items()) + + # Create a dict_vt to be used in the mapping proxy variable + dict_vt = ConstDictVariable(items, source=None) + result = MappingProxyVariable(dict_vt, source=self.source) + return self.tx.output.side_effects.track_mutable(value, result) + + @classmethod + @functools.cache + def _id_dispatch( + cls, + ) -> dict[int, Callable[["VariableBuilder", Any], VariableTracker]]: + from ..comptime import comptime + + entries = [ + (comptime, lambda self, value: ComptimeVariable()), + ( + dataclasses.fields, + lambda self, value: LambdaVariable( + _dataclasses_fields_lambda, + source=self.source, + **self.install_guards(GuardBuilder.CLOSURE_MATCH), + ), + ), + (torch.__version__, lambda self, value: TorchVersionVariable()), + ] + + # pyrefly: ignore [implicit-any] + result = {} + for ts, fn in entries: + for t in ts if isinstance(ts, (tuple, list)) else (ts,): + assert t not in result + result[id(t)] = fn + + return result + + def _wrap(self, value: Any) -> VariableTracker: + # import here to avoid circular dependencies + from torch.utils._triton import ( + has_triton, + has_triton_experimental_host_tma, + has_triton_tensor_descriptor_host_tma, + ) + + from ..decorators import ( + CudagraphOverrideContextManager, + DynamoConfigPatchProxy, + ErrorOnGraphBreakDecoratorContextManager, + ) + + if has_triton(): + from triton.runtime.autotuner import Autotuner + from triton.runtime.jit import JITFunction + else: + + class JITFunction: + pass + + class Autotuner: + pass + + # default implementations, in case we don't have triton (or the wrong triton version) + def create_1d_tma_descriptor() -> None: + pass + + def create_2d_tma_descriptor() -> None: + pass + + class TensorDescriptor: + @staticmethod + def from_tensor() -> None: + pass + + def set_allocator() -> None: + pass + + if has_triton_experimental_host_tma(): + from triton.tools.experimental_descriptor import ( # noqa: F811 + create_1d_tma_descriptor, + create_2d_tma_descriptor, + ) + if has_triton_tensor_descriptor_host_tma(): + from triton.tools.tensor_descriptor import TensorDescriptor # noqa: F811 + if has_triton(): + import triton as triton_mod + + if hasattr(triton_mod, "set_allocator"): + set_allocator = triton_mod.set_allocator # noqa: F811 + + # Handle exact type() match + type_dispatch = self._type_dispatch().get(type(value)) + if type_dispatch is not None: + return type_dispatch(self, value) + + # Handle exact id() match + id_dispatch = self._id_dispatch().get(id(value)) + if id_dispatch is not None: + return id_dispatch(self, value) + + # Everything else (NB: order matters!) + if ( + isinstance(value, torch.Tensor) + and type(value) + not in ( + # These torch-native subclasses have overly restrictive + # `__torch_function__` which prevents Dynamo from reading their + # tensor attributes like `is_nested` or calling methods like + # `_is_view`. + torch.nn.parameter.UninitializedBuffer, + torch.nn.parameter.UninitializedParameter, + ExpandedWeight, + ) + and type(value) not in config.nontraceable_tensor_subclasses + ): + if ( + type(value).__torch_dispatch__ is torch.Tensor.__torch_dispatch__ + or is_traceable_wrapper_subclass(value) + ): + return self.wrap_tensor(value) + + if is_namedtuple(value): + self.install_guards(GuardBuilder.SEQUENCE_LENGTH) + output: list[VariableTracker] = [ + LazyVariableTracker.create( + getattr(value, name), + source=AttrSource(self.source, name), + ) + for name in namedtuple_fields(type(value)) + ] + + tuple_vt = TupleVariable( + output, + source=self.source, + mutation_type=ValueMutationExisting(), + ) + result = UserDefinedTupleVariable.get_vt_cls(type(value))( + value, + source=self.source, + tuple_vt=tuple_vt, + ) + return self.tx.output.side_effects.track_object_existing(value, result) + elif istype(value, (dict, collections.defaultdict, collections.OrderedDict)): + self.install_guards(GuardBuilder.TYPE_MATCH) + all_const = all(ConstantVariable.is_literal(k) for k in value) + + # For all_const, we don't have to guard on anything yet. We guard on + # keys lazily by adding a dict_getitem entry for each accessed key. + # For cases where we need to guard on all keys, we lazily put guards + # during the dict call_method (check dicts.py) + if not all_const: + # Guard on the key order + # This is not ideal, i.e., there is no need to guard on the key + # order. But we guard on the key order because of the complexity + # + # 1) For non-constant objects, we can't save the key in the + # guard context because it can be memory heavy. We can add + # weakrefs but this complicates the accesses. + # + # 2) For non-constant objects, we also have to guard on the keys + # (like TENSOR_MATCH on tensor). We might also have guards on + # the attributes of the keys (like tensor.grad). To make this + # work in tree structure is complicated. + # + # So, instead we guard on the key order. While guarding on key + # order, we just save the indices and use it to access keys and + # values. Indices are cheap to save. + self.tx.output.guard_on_key_order.add(self.source) + + # We need all the keys to be hashable. We do this within the + # HashableTracker class in hashable.py + def build_key_value( + i: Any, k: Any, v: Any + ) -> tuple[VariableTracker, VariableTracker]: + base = self.get_source() + if all_const: + key = ConstantVariable.create(k) + source_key = k + else: + source_key = ConstDictKeySource(base, i) + key = LazyVariableTracker.create(k, source_key) + source_value = DictGetItemSource(base, source_key) + res_value = LazyVariableTracker.create(v, source_value) + + return key, res_value + + result = dict( + build_key_value(i, k, v) + for i, k, v in enumerate_items_with_dict_position(value) + ) + + if istype(value, collections.defaultdict): + factory_source = AttrSource(self.source, "default_factory") + result = DefaultDictVariable( + result, # type: ignore[arg-type] + type(value), + default_factory=VariableBuilder(self.tx, factory_source)( + value.default_factory + ), + source=self.source, + ) + else: + result = ConstDictVariable( + result, # type: ignore[arg-type] + user_cls=type(value), + source=self.source, + ) + + return self.tx.output.side_effects.track_mutable(value, result) + elif isinstance(value, torch.nn.Module): + return self.wrap_module(value) + elif ConstantVariable.is_literal(value): # non-atomic literals + return self.wrap_literal(value) + elif isinstance(value, torch.overrides.TorchFunctionMode): + var = TorchFunctionModeVariable(value, source=self.source) + self.tx.output.side_effects.track_object_existing(value, var) + return var + elif istype(value, (set, OrderedSet)): + if any(isinstance(x, torch.Tensor) for x in value): + unimplemented( + gb_type="Attempted to wrap a set with tensors", + context="Python set containing torch.Tensor elements", + explanation=( + "Dynamo cannot trace sets of tensors. To get a stable ordering, " + "Dynamo needs to convert the set into a list and the order might not be " + "stable if the set contains tensors." + ), + hints=[ + "Use a dictionary where the keys are tensors.", + *graph_break_hints.SUPPORTABLE, + ], + ) + + self.install_guards(GuardBuilder.TYPE_MATCH) + self.install_guards(GuardBuilder.SEQUENCE_LENGTH) + set_var_cls = SetVariable + + if istype(value, OrderedSet): + # Guard on the internal dict of OrderedSet + internal_dict_source = AttrSource(self.source, "_dict") + install_guard( + internal_dict_source.make_guard(GuardBuilder.DICT_KEYS_MATCH) + ) + self.tx.output.guard_on_key_order.add(internal_dict_source) + set_var_cls = OrderedSetVariable + + # The list gives a ordering for the set items. The ordering is based + # on the Python hash and it is not related to object ordering inside + # the set object. The order being incorrect at runtime will lead to + # a recompilation. + L = list(value) + items = [ + LazyVariableTracker.create( + v, source=NonSerializableSetGetItemSource(self.source, i) + ) + for i, v in enumerate(L) + ] + result = set_var_cls(items, source=self.source) + return self.tx.output.side_effects.track_object_existing(value, result) + elif istype(value, frozenset) and all( + ( + # For DBR quantization, we could get a frozenset of torch funcs. + (type(x) is types.BuiltinMethodType and x.__module__ == "torch") + or + # Another commonly used frozenset of types. + x in torch.utils._pytree.BUILTIN_TYPES + or + # For activation checkpointing, we could get a frozenset of torch ops. + isinstance(x, (OpOverload, OpOverloadPacket)) + ) + for x in value + ): + # For the limited cases of frozenset here, we know the items won't + # change across runs, so we can safely create sourceless VTs for + # them and guard on the frozenset contents via EQUALS_MATCH. + # TODO support source for sets and remove the special logics here. + items = [SourcelessBuilder.create(self.tx, v) for v in value] + self.install_guards(GuardBuilder.EQUALS_MATCH) + return FrozensetVariable(items, source=self.source) + elif isinstance( + value, + (enum.Enum, torch.DispatchKey, torch._C._functorch.TransformType), + ): + self.install_guards(GuardBuilder.ID_MATCH) + return UserDefinedObjectVariable(value, source=self.source) + elif DebuggingVariable.is_reorderable_logging_function(value): + # Put this above builtin_callable so that print() can be handled + # along with other builtin debugging functions + self.install_guards(GuardBuilder.BUILTIN_MATCH) + return DebuggingVariable(value, source=self.source) + elif callable(value) and any( + value is fn for fn in torch._dynamo.config.ignore_logging_functions + ): + # Treat ignored functions as full no-ops + self.install_guards(GuardBuilder.ID_MATCH) + return IgnoredFunctionVariable(value, source=self.source) + elif isinstance(value, logging.Logger): + self.install_guards(GuardBuilder.TYPE_MATCH) + return LoggingLoggerVariable(value, source=self.source) + elif is_utils_checkpoint(value): + return build_checkpoint_variable(source=self.source) + elif is_invoke_subgraph(value): + return build_invoke_subgraph_variable(source=self.source) + elif LocalMapWrappedHigherOrderVariable.should_wrap_in_hop(value): + return LocalMapWrappedHigherOrderVariable.build(source=self.source) + elif isinstance(value, functools.partial): + func_src = AttrSource(self.get_source(), "func") + func_obj = VariableBuilder(self.tx, func_src)(value.func) + + args = [] + args_source = AttrSource(self.get_source(), "args") + for i, arg in enumerate(value.args): + args.append( + VariableBuilder(self.tx, GetItemSource(args_source, i))(arg) + ) + + keywords = {} + keywords_source = AttrSource(self.get_source(), "keywords") + for k, v in value.keywords.items(): + if not ConstantVariable.is_literal(k): + unimplemented( + gb_type="functools.partial() with non-literal keyword", + context=f"non-literal keyword: {k}", + explanation="functools.partial() expects literal/string keywords", + hints=[*graph_break_hints.USER_ERROR], + ) + keywords[k] = VariableBuilder( + self.tx, DictGetItemSource(keywords_source, k) + )(v) + + install_guard( + self.get_source().make_guard(GuardBuilder.TYPE_MATCH), + keywords_source.make_guard(GuardBuilder.DICT_KEYS_MATCH), + args_source.make_guard(GuardBuilder.SEQUENCE_LENGTH), + ) + # Preserve cache_hash for SAC context_fn caching + original_cache_hash = getattr(value, "cache_hash", None) + return FunctoolsPartialVariable( + func_obj, args, keywords, original_cache_hash=original_cache_hash + ) + elif is_typing(value): + # typing.List, typing.Mapping, etc. + self.install_guards(GuardBuilder.ID_MATCH) + return TypingVariable( + value, + source=self.source, + ) + elif np is not None and isinstance(value, np.generic): + # numpy array scalars: convert to 0D arrays + return self.wrap_numpy_ndarray(np.asarray(value)) + elif trace_rules.is_numpy(value): + assert np + if istype(value, types.MethodType): + # Dont guard on cython functions as they dont change ids + if inspect.isfunction(value.__func__): + install_guard( + AttrSource(self.source, "__func__").make_guard( + GuardBuilder.CLOSURE_MATCH + ) + ) + elif inspect.isclass(value): + self.install_guards(GuardBuilder.CLASS_MATCH) + elif inspect.isfunction(value): + self.install_guards(GuardBuilder.CLOSURE_MATCH) + elif callable(value): + self.install_guards(GuardBuilder.ID_MATCH) + else: + self.install_guards(GuardBuilder.TYPE_MATCH) + return NumpyVariable(value, source=self.source) + elif trace_rules.is_numpy_dtype(value): + self.install_guards(GuardBuilder.ID_MATCH) + return NumpyDTypeVariable(value, source=self.source) + elif trace_rules.is_numpy_type_info(value): + if isinstance(value, np.iinfo): + self.install_guards(GuardBuilder.TYPE_MATCH) + dt_source = AttrSource(self.source, "dtype") + install_guard(dt_source.make_guard(GuardBuilder.ID_MATCH)) + else: + self.install_guards(GuardBuilder.ID_MATCH) + return ConstantLikeVariable(value, source=self.source) + # NB: These can't be put in type_dispatch, they have to run later + elif CollectiveFunctionRewriteVariable.can_rewrite(value): + self.install_guards(GuardBuilder.CLOSURE_MATCH) + return CollectiveFunctionRewriteVariable.create( + self.tx, + value, + source=self.source, + ) + elif istype(value, torch.autograd.function.FunctionMeta): + self.install_guards(GuardBuilder.CLASS_MATCH) + return AutogradFunctionVariable( + value, + source=self.source, + ) + elif isinstance(value, torch.autograd.function.FunctionCtx): + actual_saved_tensors = None + try: + # type: ignore[attr-defined] + actual_saved_tensors = value.saved_tensors + except RuntimeError: + pass + + saved_tensors = [] + guards = [self.source.make_guard(GuardBuilder.TYPE_MATCH)] + if isinstance(actual_saved_tensors, tuple): + saved_tensors_source = AttrSource(self.source, "saved_tensors") + guards.append( + saved_tensors_source.make_guard(GuardBuilder.SEQUENCE_LENGTH) + ) + for i, v in enumerate(actual_saved_tensors): + saved_tensors.append( + VariableBuilder( + self.tx, GetItemSource(saved_tensors_source, i) + )(v) + ) + install_guard(*guards) + + return self.tx.output.side_effects.track_object_existing( + value, + AutogradFunctionContextVariable( + value, + source=self.source, + saved_tensors=SavedTensorBox(saved_tensors), + ), + ) + elif ( + isinstance(value, types.MethodType) + and istype( + getattr(value, "__self__", None), torch.autograd.function.FunctionMeta + ) + and getattr(value, "__name__", "") == "apply" + and value == getattr(value.__self__, "apply", None) + ): + # handle aliased autograd function `apply` calls + install_guard( + AttrSource(self.get_source(), "__func__").make_guard( + GuardBuilder.CLOSURE_MATCH + ) + ) + return GetAttrVariable( + AutogradFunctionVariable( + value.__self__, + source=AttrSource(self.source, member="__self__"), + ), + "apply", + py_type=type(value), + ) + elif isinstance(value, torch._C._ImperativeEngine): + self.install_guards(GuardBuilder.ID_MATCH) + return AutogradEngineVariable(value, source=self.source) + elif ( + value + is torch._dynamo.external_utils.FakeCompiledAutogradEngine._exec_final_callbacks_stub + ): + self.install_guards(GuardBuilder.CLOSURE_MATCH) + return LambdaVariable( + lambda: UserFunctionVariable( + torch._dynamo.external_utils.FakeCompiledAutogradEngine.exec_final_callbacks, + ).call_function( + self.tx, + (self.tx.output.side_effects.get_ca_final_callbacks_var(),), + {}, + ) + ) + elif isinstance(value, DynamoConfigPatchProxy): + return DynamoConfigPatchVariable(value.changes) + elif isinstance(value, ErrorOnGraphBreakDecoratorContextManager): + return ErrorOnGraphBreakVariable(value.error_on_graph_break) + elif isinstance(value, CudagraphOverrideContextManager): + return CudagraphOverrideVariable(value.fwd, value.bwd) + elif callable(value) and trace_rules.lookup_callable(value) is not None: + if trace_rules.is_callable_allowed(value): + self.tx.output.has_user_defined_allowed_in_graph = True + # type: ignore[attr-defined] + return trace_rules.lookup_callable(value).create_with_source( + value, source=self.source + ) + elif np and isinstance(value, np.number): + return self.wrap_unspecialized_primitive(value) + elif isinstance(value, HigherOrderOperator): + if value is torch._higher_order_ops.invoke_subgraph: + unimplemented( + gb_type="Attempted to wrap torch._higher_order_ops.invoke_subgraph", + context="", + explanation="Directly using invoke_subgraph is not supported. Use nested_compile_region", + hints=[], + ) + self.install_guards(GuardBuilder.TYPE_MATCH) + return TorchHigherOrderOperatorVariable.make(value, source=self.source) + elif isinstance(value, torch.cuda.StreamContext): + self.install_guards(GuardBuilder.ID_MATCH) + stream_source = AttrSource(self.source, "stream") + stream_var = VariableBuilder(self.tx, stream_source)(value.stream) + # type: ignore[arg-type] + return StreamContextVariable.create(self.tx, stream_var) + elif isinstance(value, torch.Stream): + self.install_guards(GuardBuilder.TYPE_MATCH) + if isinstance(self.source, CurrentStreamSource): + # Reuse the index pre-allocated in SymbolicStreamState.__init__ + index = CURRENT_STREAM_INDEX + else: + index = register_user_object(value, self.source) + stream_proxy = self.tx.output.create_proxy( + "call_function", get_external_object_by_index, (index,), {} + ) + set_example_value(stream_proxy.node, value) + var = StreamVariable( + stream_proxy, value, source=self.source, user_object_index=index + ) + return self.tx.output.side_effects.track_object_existing(value, var) + elif isinstance(value, (torch._C._SDPAParams)): + self.install_guards(GuardBuilder.TYPE_MATCH) + return SDPAParamsVariable.create(self.tx, value, self.source) + elif isinstance(value, torch._functorch.pyfunctorch.FuncTorchInterpreter): + self.install_guards(GuardBuilder.ID_MATCH) + return FuncTorchInterpreterVariable(value) + elif isinstance(value, torch.Event): + self.install_guards(GuardBuilder.TYPE_MATCH) + index = register_user_object(value, self.source) + event_proxy = self.tx.output.create_proxy( + "call_function", + get_external_object_by_index, + (index,), + {}, + ) + set_example_value(event_proxy.node, value) + return EventVariable( + event_proxy, + value, + index, + source=self.source, + ) + elif ( + istype(value, contextlib.nullcontext) + and inspect.getattr_static(value, "enter_result", None) is None + ): + self.install_guards(GuardBuilder.TYPE_MATCH) + return NullContextVariable(source=self.source) + elif KeyedJaggedTensorVariable.is_matching_object(value): + self.install_guards(GuardBuilder.TYPE_MATCH) + result = KeyedJaggedTensorVariable(value, source=self.source) + # TODO: this doing it manually is bad + return self.tx.output.side_effects.track_object_existing(value, result) + elif isinstance(value, torch.optim.Optimizer): + self.install_guards(GuardBuilder.ID_MATCH) + self.source = OptimizerSource(self.source) + return OptimizerVariable( + value, + source=self.source, + mutation_type=AttributeMutationExisting(), + ) + elif isinstance(value, torch.DispatchKeySet): + self.install_guards(GuardBuilder.DISPATCH_KEY_SET_MATCH) + return DispatchKeySetVariable(value) + elif WorldMetaClassVariable.is_group_member_type(value): + return WorldMetaClassVariable(value, source=self.source) + elif value is OrderedSet: + self.install_guards(GuardBuilder.ID_MATCH) + return OrderedSetClassVariable() + elif ( + id(value) in ITERTOOLS_TYPE_IDS + and id(value) not in ITERTOOLS_POLYFILLED_TYPE_IDS + ): + self.install_guards(GuardBuilder.CLASS_MATCH) + return ItertoolsVariable(value, source=self.source) + elif isinstance(value, _DynamicScalar): + is_int = isinstance(value, DynamicInt) + source = DynamicScalarSource(self.source, is_int) + if id(value) in self.tx.output.root_tracer.dynamic_scalar_nodes: + # If we've already seen this dynamic scalar, reuse the existing + # SymInt/SymFloat node. + node = self.tx.output.root_tracer.dynamic_scalar_nodes[id(value)] + else: + sym = self.tx.output.shape_env.create_unspecified_symbol( + value.real, # type: ignore[attr-defined] + source=source, + dynamic_dim=DimDynamic.DYNAMIC, + ) + node = self.tx.output.shape_env.create_symintnode( + sym, + hint=value.real, # type: ignore[attr-defined] + source=source, + ) + if not isinstance(node, SymInt): + raise AssertionError(f"Expected SymInt, got {type(node)}") + + # Bind to graph input + sym_node_proxy = self.tx.output.root_tracer.create_graph_input( + re.sub(r"[^a-zA-Z0-9]+", "_", self.name), + type(node), + node, + source=source, + ) + sym_node_proxy.node.meta["grapharg"] = GraphArg( + source, + node, + False, + None, + is_tensor=False, + example_strong_ref=node, + ) + sym_expr = node.node.expr + assert isinstance(sym_expr, sympy.Symbol), ( + f"{sym_expr} is not a basic Symbol." + ) + self.tx.output.tracked_fakes.append(TrackedFake(node, source, None)) + return SymNodeVariable.create(self.tx, sym_node_proxy, node) + elif is_torch_sym(value): + # Note: this doesn't handle nested symints. + # For SymBool input, we reuse the infra for SymInt by simulating SymBool with a SymInt in dynamo. + + # Concretely, + # 1. We create a SymInt in dynamo's shape_env, whose source is constructed as ConvertIntSource(self.source). + # so that guards on the SymInts can be effectively applied on the original SymBool in user program. + # 2. We create a SymBool based on the SymInt in dynamo's ShapeEnv. Because the original user program + # depends on the value being a SymBool. This allows dynamo to interpret the user's program correctly. + source = ( + self.source + if isinstance(value, torch.SymInt) + else ConvertIntSource(self.source) + ) + new_symint = None + if value.node.has_hint(): + new_symint = ( + self.tx.output.shape_env.create_unspecified_symint_and_symbol( + int(value.node.hint), + source, + dynamic_dim=DimDynamic.DYNAMIC, + ) + ) + else: + if isinstance(value, torch.SymBool): + # We need to create an unbacked symint to replace the unbacked symbool. + new_symint = self.tx.output.shape_env.create_unbacked_symint() + else: + # TODO (yidi): we need to figure out a way to propagate the guards + # we accumulated when tracing the subggraph to outer shape_env. For normal symints, + # this is automatically done by evaluating the guards once but this + # will cause data-dependent error when we evaluate the outer unbacked symints. + # The test case that triggers this graph break is test_cond_unbacked_symint_closure + unimplemented( + gb_type="Attempted to wrap unbacked SymInt", + context="", + explanation="Unbacked SymInt input is not supported yet.", + hints=[*graph_break_hints.SUPPORTABLE], + ) + assert new_symint is not None + if not isinstance(new_symint, SymInt): + raise AssertionError(f"Expected SymInt, got {type(new_symint)}") + sym_node_proxy = self.tx.output.root_tracer.create_graph_input( + re.sub(r"[^a-zA-Z0-9]+", "_", self.name), + type(new_symint), + new_symint, + source=source, + ) + + sym_node_proxy.node.meta["grapharg"] = GraphArg( + source, + new_symint, + False, + None, + is_tensor=False, + example_strong_ref=new_symint, + ) + # We bind the new_symint to graph input. + sym_expr = new_symint.node.expr + assert isinstance(sym_expr, sympy.Symbol), ( + f"{sym_expr} is not a basic Symbol." + ) + self.tx.output.tracked_fakes.append(TrackedFake(new_symint, source, None)) + + tracing_symint = ( + new_symint if isinstance(value, torch.SymInt) else new_symint == 1 + ) # cast it back to symbool for tracing + return SymNodeVariable(sym_node_proxy, tracing_symint) + + elif isinstance(value, (JITFunction, Autotuner)): + self.install_guards(GuardBuilder.ID_MATCH) + return TritonKernelVariable( + value, + None, # No kernel idx provided + None, # No grid provided + source=self.source, + ) + elif value is create_1d_tma_descriptor: + return CreateTMADescriptorExperimentalVariable(rank=1) + elif value is create_2d_tma_descriptor: + return CreateTMADescriptorExperimentalVariable(rank=2) + elif value is TensorDescriptor.from_tensor: + return CreateTMADescriptorStableVariable() + elif value is set_allocator: + return TritonSetAllocatorVariable(value) + elif isinstance(value, torch.amp.autocast_mode.autocast): + if isinstance(value, torch.amp.autocast_mode._UnmanagedAutocast): + return self.wrap_user_defined(value) + else: + self.install_guards(GuardBuilder.ID_MATCH) + return AutocastModeVariable( + target_values=[ + value.device, + value.fast_dtype, + value._enabled, + value._cache_enabled, + ], + source=self.source, + ) + elif TorchCtxManagerClassVariable.is_matching_cls(value): + if inspect.isclass(value): + self.install_guards(GuardBuilder.CLASS_MATCH) + elif inspect.isfunction(value): + self.install_guards(GuardBuilder.CLOSURE_MATCH) + return TorchCtxManagerClassVariable(value, source=self.source) + elif inspect.getattr_static(value, "__script_if_tracing_wrapper", False): + self.install_guards(GuardBuilder.TYPE_MATCH) + return WrapperUserFunctionVariable( + value, + "__original_fn", + source=self.source, + mutation_type=AttributeMutationExisting(), + ) + elif is_lru_cache_wrapped_function(value): + self.install_guards(GuardBuilder.TYPE_MATCH) + return WrapperUserFunctionVariable( + value, + "__wrapped__", + source=self.source, + mutation_type=AttributeMutationExisting(), + ) + elif value is sys.exc_info or ( + sys.version_info >= (3, 11) and value is sys.exception + ): + return SysFunctionVariable(value, source=self.source) + elif is_function_or_wrapper(value) and inspect.getattr_static( + value, "_torchdynamo_inline", False + ): + self.install_guards(GuardBuilder.TYPE_MATCH) + return WrapperUserFunctionVariable( + value, + "_torchdynamo_inline", + source=self.source, + mutation_type=AttributeMutationExisting(), + ) + elif value is collections.namedtuple: + self.install_guards(GuardBuilder.ID_MATCH) + return CollectionsNamedTupleFunction(value, source=self.source) + elif isinstance( + value, types.BuiltinMethodType + ) and BuiltinMethodVariable.is_supported_builtin_method(value): + self.install_guards(GuardBuilder.ID_MATCH) + return BuiltinMethodVariable(value, source=self.source) + elif is_function(value) and value in (float.fromhex, float.hex): + self.install_guards(GuardBuilder.ID_MATCH) + return GetAttrVariable( + BuiltinVariable(float, source=self.source), + value.__name__, + py_type=type(value), + ) + elif is_function_or_wrapper(value): + value, attr_name = unwrap_with_attr_name_if_wrapper(value) + # For these wrappers, Dynamo points to the wrapped function, + # so source needs to be updated as well. + if attr_name is not None: + self.source = AttrSource(self.source, attr_name) + # type: ignore[attr-defined] + return trace_rules.lookup(value).create_with_source( + value, source=self.source + ) + elif value is random.Random: + self.install_guards(GuardBuilder.ID_MATCH) + return RandomClassVariable(source=self.source) + elif istype(value, random.Random) and RandomVariable.is_supported_random_obj( + value + ): + self.install_guards(GuardBuilder.TYPE_MATCH) + result = RandomVariable(value, source=self.source) + self.tx.output.side_effects.track_mutable(value, result) + return result + # Don't use istype, since some python modules are not subclasses of types.ModuleType directly. + # E.g, type(torch.ops) -> , + # type(torch.backends.cudnn) -> + elif isinstance(value, (types.ModuleType, replay_record.DummyModule)): + self.install_guards(GuardBuilder.MODULE_MATCH) + result = PythonModuleVariable( + # type: ignore[arg-type] + value, + source=self.source, + ) + self.tx.output.side_effects.track_object_existing(value, result) + return result + elif isinstance(value, types.GetSetDescriptorType): + # GetSet descriptors are C functions attached to an attribute lookup + # using PyGetSetDef. Python, on attribute lookup, can decide to + # create a new object on the fly, and therefore the `id` of the + # descriptors is not guaranteed to be same for different attribute + # accesses. Since these are unlikely to change during the program + # execution, we can skip guarding on them. + return GetSetDescriptorVariable(value) + elif isinstance(value, types.MethodWrapperType): + # Method-wrappers are written in C, and they are not guaranteed to + # return the same object on attribute lookup. Therefore, we cannot + # insert a ID_MATCH guard here. method-wrappers are very + # unlikely to change, so its ok to skip the guard here. + return MethodWrapperVariable(value) + elif issubclass(type(value), type) and issubclass(value, BaseException): + # match user defined exceptions + self.install_guards(GuardBuilder.ID_MATCH) + return UserDefinedExceptionClassVariable(value) + elif issubclass(type(value), type): + if value in ( + torch.utils.hooks.BackwardHook, + torch.nn.Parameter, + torch.nn.Buffer, + ): + # TODO(jansel): combine this case with the one above + # type: ignore[attr-defined] + return trace_rules.lookup(value).create_with_source( + value, source=self.source + ) + if value is torch.autograd._unsafe_preserve_version_counter: + self.install_guards(GuardBuilder.CLASS_MATCH) + return PreserveVersionContextVariable.constructor(self.tx) + if ( + # `value` must be a strict subclass of `torch.Tensor` + issubclass(value, torch.Tensor) + and value is not torch.Tensor + # `TensorSubclassVariable` is not for subclass that overrides + # `torch_dispatch`. + and value.__torch_dispatch__ is torch.Tensor.__torch_dispatch__ + # `TensorSubclassVariable` would lead to construction of + # `TensorWithTFOverrideVariable`, but we don't want that for + # traceable wrapper subclasses (we wrap those subclass instances + # into `TensorVariable`). + and not is_traceable_wrapper_subclass_type(value) + ): + return TensorSubclassVariable(value, source=self.source) + + if not is_from_closure_source(self.source): + # For closure source, the variable comes from LOAD_SUPER_ATTR, + # which calls self.__class__. This is internal Cpython + # implementation, and it is rare for the user to modify + # self.__class__ manually. + # For other cases, this is a userdefined class, so install an + # ID_MATCH even if its a global variable. + self.install_guards(GuardBuilder.CLASS_MATCH) + + if isinstance(value, type) and issubclass(value, enum.Enum): + return UserDefinedClassVariable( + value, + source=self.source, + ) + + if is_opaque_type(value): + return OpaqueObjectClassVariable( + value, + source=self.source, + ) + + return UserDefinedClassVariable( + value, + source=self.source, + ) + elif TorchScriptObjectVariable.is_matching_cls(type(value)): + from ..source import ( + FlattenScriptObjectSource, + ScriptObjectQualifiedNameSource, + ) + + # Unwrap FakeScriptObject to the underlying real object so the + # rest of this branch (guards, graph inputs, etc.) operates on + # the real opaque object type. + if isinstance(value, torch._library.fake_class_registry.FakeScriptObject): + value = value.real_obj + + # type: ignore[arg-type] + if torch._library.fake_class_registry.tracing_with_real(value): + proxy = self.tx.output.root_tracer.create_graph_input( + re.sub(r"[^a-zA-Z0-9]+", "_", self.name), + type(value), + value, + source=self.source, + ) + + # setting is_unspecialized=False to not insert a as_tensor call in reconstruct by default + # setting example to be real value because these example values will be used + # as example_inputs for user compiler. + proxy.node.meta["grapharg"] = GraphArg( + self.source, + value, + False, + None, + False, + value, # type: ignore[arg-type] + ) + return TorchScriptObjectVariable.create( + proxy, + value, + source=self.source, + ) + + if is_opaque_value_type(type(value)): + # Value-type: guard on equality (will use __eq__) + self.install_guards(GuardBuilder.CONSTANT_MATCH) + elif is_opaque_reference_type(type(value)): + # Reference-type: guard only on type, and registered guard_fn + self.install_guards(GuardBuilder.TYPE_MATCH) + self.install_guards(GuardBuilder.OPAQUE_OBJ_GUARD_FN_MATCH) + elif not hasattr(value, "__obj_flatten__"): + # This exists to allow a smoother transition. + # The implications are: + # The script objects won't be tracked as proxies. + # Methods on these objects won't show up in the graph. + # The original script object might be mutated. + return self.wrap_user_defined(value) + else: + # Install the guards on the fully qualified name of the script object + LazyVariableTracker.realize_all( + VariableBuilder( + self.tx, ScriptObjectQualifiedNameSource(self.source) + )( + value._type().qualified_name() # type: ignore[attr-defined] + ) + ) + # Install the guards on the content of the script object by setting the source + # to be FlattenScriptObjectSource, which calls __obj_flatten__() to get the contents. + LazyVariableTracker.realize_all( + VariableBuilder(self.tx, FlattenScriptObjectSource(self.source))( + value.__obj_flatten__() + ) + ) + + fake_script_obj = torch._library.fake_class_registry.maybe_to_fake_obj( + self.tx.output.fake_mode, value + ) + if is_opaque_value_type(type(value)) and not should_hoist(type(value)): + fake_script_obj = value + proxy = value + + elif config.install_free_tensors and ( + is_from_global_source(self.source) + or is_from_nonlocal_source(self.source) + or is_from_unspecialized_nn_module_source(self.source) + ): + return self.tx.output.register_attr_or_module( + value, self.name, source=self.source + ) + + else: + proxy = self.tx.output.root_tracer.create_graph_input( + re.sub(r"[^a-zA-Z0-9]+", "_", self.name), + type(value), + fake_script_obj, + source=self.source, + ) + # setting is_unspecialized=False to not insert a as_tensor call in reconstruct by default + # setting example to be real value because these example values will be used + # as example_inputs for user compiler. + proxy.node.meta["grapharg"] = GraphArg( + self.source, + value, # type: ignore[arg-type] + False, + None, + False, + fake_script_obj, # type: ignore[arg-type] + ) + + return TorchScriptObjectVariable.create( + proxy, # pyrefly: ignore[bad-argument-type] + fake_script_obj, + source=self.source, + ) + elif ( + isinstance(value, (dict, collections.OrderedDict)) + and type(value).__new__ is dict.__new__ + ): + # Construct a dict_vt that will reside inside the UserDefinedDictVariable + self.install_guards(GuardBuilder.TYPE_MATCH) + self.install_guards(GuardBuilder.SEQUENCE_LENGTH) + + # Guard on the key order + self.tx.output.guard_on_key_order.add(self.source) + + # We need all the keys to be hashable. We do this within the + # HashableTracker class in hashable.py + def build_key_value( + i: Any, k: Any, v: Any + ) -> tuple[VariableTracker, VariableTracker]: + base = self.get_source() + source_key = ConstDictKeySource(base, i) + key = LazyVariableTracker.create(k, source_key) + + source_value = DictSubclassGetItemSource(base, source_key) + res_value = LazyVariableTracker.create(v, source_value) + + return key, res_value + + result = dict( + build_key_value(i, k, v) + for i, k, v in enumerate_items_with_dict_position(value) + ) + + dict_vt = ConstDictVariable( + result, + user_cls=( + collections.OrderedDict + if isinstance(value, collections.OrderedDict) + else dict + ), + mutation_type=ValueMutationExisting(), + source=self.source, + ) + # Force this to reconstruct on mutation to keep the reconstruction + # bytecode simple + dict_vt.should_reconstruct_all = True + + result = UserDefinedDictVariable(value, dict_vt=dict_vt, source=self.source) + return self.tx.output.side_effects.track_object_existing(value, result) + elif isinstance(value, tuple): + self.install_guards(GuardBuilder.TYPE_MATCH) + self.install_guards(GuardBuilder.SEQUENCE_LENGTH) + + # NB - Be careful in not triggering user code. Guards also work on + # the underlying tuple data structure. + output = [ + LazyVariableTracker.create( + tuple.__getitem__(value, i), + source=GetItemSource(self.get_source(), i), + ) + for i in range(tuple.__len__(value)) + ] + + tuple_vt = TupleVariable( + output, # type: ignore[arg-type] + source=self.source, + mutation_type=ValueMutationExisting(), + ) + result = UserDefinedTupleVariable( + value, tuple_vt=tuple_vt, source=self.source + ) + return self.tx.output.side_effects.track_object_existing(value, result) + elif isinstance(value, list): + self.install_guards(GuardBuilder.TYPE_MATCH) + self.install_guards(GuardBuilder.SEQUENCE_LENGTH) + + # NB - Be careful in not triggering user code. Guards also work on + # the underlying list data structure. + output = [ + LazyVariableTracker.create( + list.__getitem__(value, i), + source=ListGetItemSource(self.get_source(), i), + ) + for i in range(list.__len__(value)) + ] + list_vt = ListVariable( + output, # type: ignore[arg-type] + source=self.source, + mutation_type=ValueMutationExisting(), + ) + result = UserDefinedListVariable(value, list_vt=list_vt, source=self.source) + return self.tx.output.side_effects.track_object_existing(value, result) + elif isinstance(value, (set, frozenset)): + self.install_guards(GuardBuilder.TYPE_MATCH) + self.install_guards(GuardBuilder.SEQUENCE_LENGTH) + + L = list(dict.fromkeys(value)) + output = [ + LazyVariableTracker.create( + list.__getitem__(L, i), + source=NonSerializableSetGetItemSource(self.get_source(), i), + ) + for i in range(list.__len__(L)) + ] + if isinstance(value, set): + set_vt_cls = SetVariable + else: + assert isinstance(value, frozenset) + set_vt_cls = FrozensetVariable + + set_vt = set_vt_cls( + output, source=self.source, mutation_type=ValueMutationExisting() + ) + result = UserDefinedSetVariable(value, set_vt=set_vt, source=self.source) + return self.tx.output.side_effects.track_object_existing(value, result) + elif issubclass(type(value), MutableMapping): + self.install_guards(GuardBuilder.TYPE_MATCH) + result = MutableMappingVariable(value, source=self.source) + return self.tx.output.side_effects.track_object_existing(value, result) + elif is_frozen_dataclass(value): + self.install_guards(GuardBuilder.TYPE_MATCH) + result = FrozenDataClassVariable(value, source=self.source) + return self.tx.output.side_effects.track_object_existing(value, result) + elif isinstance(value, dict_keys): + if all(ConstantVariable.is_literal(k) for k in value): + # If the dict_keys object is passed from outside the compile region, it must either be passed along with + # the corresponding dict object or treated as a set (when only the keys are passed into the compiled region). + # - If it is passed along with the dict, the dict object itself is already guarded. + # - If only the dict_keys object is passed, we add EQUALS_MATCH and SEQUENCE_LENGTH guards + # to ensure it remains unchanged across multiple runs. + items = [SourcelessBuilder.create(self.tx, v) for v in value] + install_guard( + self.get_source().make_guard(GuardBuilder.SEQUENCE_LENGTH), + self.get_source().make_guard(GuardBuilder.EQUALS_MATCH), + ) + return DictKeySetVariable(items, source=self.source) + else: + unimplemented( + gb_type="non-const keys in dict_keys", + context=f"non-const keys: {[k for k in value if not ConstantVariable.is_literal(k)]}", + explanation="Dynamo expects dict_keys keys to be constants.", + hints=[ + "Ensure your dict_keys keys are constants (e.g. int, float, strings)", + ], + ) + elif IntWrapperVariable.is_matching_object(value): + from torch.export.dynamic_shapes import _DimHintType + + if value.dynamism is None or value.dynamism.type == _DimHintType.STATIC: + return self.wrap_symint(value.val) + elif value.dynamism.type == _DimHintType.DYNAMIC: + log.debug( + "%s marked %s via IntWrapper", + self.source.name, + DimDynamic.DYNAMIC, + ) + return self.wrap_symint( + value.val, + dynamism=DimDynamic.DYNAMIC, + context=SymIntSymbolicContext( + constraint=RelaxedUnspecConstraint(warn_only=False) + ), + ) + elif value.dynamism.type == _DimHintType.AUTO: + log.debug( + "%s marked %s via IntWrapper", + self.source.name, + DimDynamic.DYNAMIC, + ) + return self.wrap_symint(value.val, dynamism=DimDynamic.DYNAMIC) + else: + raise RuntimeError(f"Undefined dynamism {value.dynamism}") + elif istype(value, object): + self.install_guards(GuardBuilder.TYPE_MATCH) + return ObjectVariable(value, source=self.source) + else: + return self.wrap_user_defined(value) + + def wrap_user_defined(self, value: Any) -> VariableTracker: + from .user_defined import _CONSTANT_BASE_TYPES + + self.install_guards(GuardBuilder.TYPE_MATCH) + if InspectVariable.is_matching_object(value): + # Skip guards on inspect related variable trackers because they are + # not important for recompiles (something else will also change to + # cause recompiles) and can cause a large number of OBJECT_ALIASING + # guards. + result = InspectVariable(value, source=SkipGuardSource(self.source)) + elif ( + isinstance(value, _CONSTANT_BASE_TYPES) + and type(value) not in common_constant_types + ): + self.install_guards(GuardBuilder.CONSTANT_SUBCLASS_MATCH) + result = UserDefinedConstantVariable(value, source=self.source) + else: + result = UserDefinedObjectVariable(value, source=self.source) + if not SideEffects.cls_supports_mutation_side_effects(type(value)): + # don't allow STORE_ATTR mutation with custom __setattr__ + return result + return self.tx.output.side_effects.track_object_existing(value, result) + + def wrap_listlike( + self, value: Union[tuple[Any, ...], list[Any], odict_values, NamedTuple] + ) -> VariableTracker: + if config.specialize_int and type(value) is torch.Size: + self.install_guards(GuardBuilder.CONSTANT_MATCH) + return ConstantVariable.create(value=value) + + # One can index a tensor with a list/tuple. Therefore, we need to + # have a stricter match. + self.install_guards(GuardBuilder.SEQUENCE_LENGTH) + + # Tuples are immutable objects, so we should mark its items static. This + # avoids wrapping of tuple items as symints. This helps for nn module + # attributes like conv2d strides, dilations. + if ( + istype(value, tuple) + and all(ConstantVariable.is_literal(item) for item in value) + and self.source.guard_source.is_unspecialized_nn_module() + ): + self.install_guards(GuardBuilder.CONSTANT_MATCH) + return TupleVariable([ConstantVariable.create(item) for item in value]) + + output = [ + LazyVariableTracker.create( + item, + source=GetItemSource(self.get_source(), i), + ) + for i, item in enumerate(value) + ] + + maybe_gm = self.tx.output.local_scope.get("self") + if isinstance( + self.source, LocalSource + ) and self.source.local_name in get_locals_to_steal(maybe_gm): + # The input tensor list to dynamo from compiled autograd may contain activations + # which are freed as they are used in inductor. Dynamo's default behavior is to + # lift all tensors to the graph inputs, but this will cause dynamo to hold an + # extra reference to the activation tensors and increase peak memory usage. + # To allow freeing ASAP, we keep the list as graph argument to the dynamo output + # graph, and unpack it locally. + # e.g. instead of `def forward(self, L_inputs_0_, L_inputs_1_, ...):`, we have + # `def forward(self, L_inputs_):` + source = self.source + assert isinstance(value, list) + tensor_list_proxy = self.tx.output.root_tracer.create_graph_input( + re.sub(r"[^a-zA-Z0-9]+", "_", self.name), + type(value), + value, + source=source, + ) + tensor_list_proxy.node.meta["steal_arg"] = True + + list_variable = wrap_fx_proxy_cls( + target_cls=TensorVariable, + tx=self.tx, + proxy=tensor_list_proxy, + example_value=value, + subclass_type=None, + source=source, + ) + + # Apply relevant logic from `VariableTracker.build(value[i])` + # (except for the `create_graph_input` stuff). + guards = [] + # type: ignore[attr-defined] + for i, tensor_variable in enumerate(list_variable.items): + source_i = GetItemSource(base=source, index=i, index_is_slice=False) + # access unpacked tensor from this list instead of from a lifted arg + self.tx.output.input_source_to_var[source_i] = tensor_variable + tensor_variable.proxy.node.meta["tensor_dict"] = _extract_tensor_dict( + value[i] + ) + guard = functools.partial( + GuardBuilder.TENSOR_MATCH, value=TensorWeakRef(value[i]) + ) + guards.append(source_i.make_guard(guard)) + + install_guard(*guards, skip=1) + + grapharg = GraphArg( + source, + value, + pass_arg_as_tensor=False, + fake_tensor=None, + is_tensor=False, + ) + tensor_list_proxy.node.meta["grapharg"] = grapharg + + # The following is very important for maintaining the "python object + # <==> variable tracker" 1-to-1 mapping, which is mainly handled via + # `side_effects`. Note that constructing `tensor_variable` above + # already adds it to graph arg, but we never registered it with + # `side_effects`. The preemptive `realize` calls here basically + # does that registration (at the end of `self.__call__`). + # + # A slightly cleaner alternative is to register the + # `tensor_variable`s above with `side_effects` directly, and just + # return the `list_variable`, but that breaks some tensor-subclass + # related tests like `test_inputs_aliasing_bytecode_stack_restore`, + # because `tensor_variable` is constructed via + # `handle_traced_output`, which doesn't really expect/handle tensor + # subclass. + # + # Eventually, we expect to fix remove all of these by having Dynamo + # auto-boxing inputs to the compiled graph, see + # https://github.com/pytorch/pytorch/issues/153701. + for vt in output: + vt.realize() + # type: ignore[arg-type] + result = BaseListVariable.cls_for_instance(value)(output, source=self.source) + if istype(value, (list, collections.deque)): + return self.tx.output.side_effects.track_mutable(value, result) + return result + + def wrap_tuple_iterator(self, value: tuple_iterator) -> VariableTracker: + self.install_guards(GuardBuilder.TUPLE_ITERATOR_LEN) + output = [ + VariableBuilder(self.tx, TupleIteratorGetItemSource(self.get_source(), i))( + tuple_iterator_getitem(value, i) + ) + for i in range(tuple_iterator_len(value)) + ] + result = TupleIteratorVariable(output, source=self.source) + return self.tx.output.side_effects.track_mutable(value, result) + + def wrap_range_iterator(self, value: range_iterator) -> VariableTracker: + self.install_guards(GuardBuilder.RANGE_ITERATOR_MATCH) + # Get all the values from the range iterator; no need to install guards + # on items since `RANGE_ITERATOR_MATCH` guarantees the same items. + items: list[VariableTracker] = [ + ConstantVariable.create(v) for v in copy.deepcopy(value) + ] + result = ListIteratorVariable(items, source=self.source) + return self.tx.output.side_effects.track_mutable(value, result) + + def wrap_itertools_count(self, value: Any) -> VariableTracker: + current_item, step = normalize_count_iter(value) + if not ( + ConstantVariable.is_literal(current_item) + and ConstantVariable.is_literal(step) + ): + return self.wrap_user_defined(value) + + self.install_guards(GuardBuilder.COUNT_ITERATOR_MATCH) + result = CountIteratorVariable( + ConstantVariable.create(current_item), + ConstantVariable.create(step), + source=self.source, + ) + return self.tx.output.side_effects.track_mutable(value, result) + + def wrap_slice_range(self, value: slice | range) -> SliceVariable | RangeVariable: + items = [ + VariableBuilder(self.tx, AttrSource(self.get_source(), k))( + getattr(value, k) + ) + for k in ("start", "stop", "step") + ] + self.install_guards(GuardBuilder.TYPE_MATCH) + if isinstance(value, slice): + return SliceVariable(items, self.tx, source=self.source) + else: + return RangeVariable(items, source=self.source) + + def mark_static_input(self, value: torch.Tensor, guard: bool) -> None: + from ..decorators import mark_static_address + + static_inputs_log.debug( + "Marking static input %s, id: %s)", self.source.name, id(value) + ) + mark_static_address(value, guard=guard) + + # Check if we've seen this tensor before and update graph metadata if needed + # As long as this runs before AOT this is sound + if value in self.tx.output.side_effects: + var = self.tx.output.side_effects[value] + # type: ignore[attr-defined] + var.proxy.node.meta["tensor_dict"]["_dynamo_static_input_type"] = ( + # type: ignore[attr-defined] + value._dynamo_static_input_type + ) + + def wrap_module(self, value: torch.nn.Module) -> VariableTracker: + from ..eval_frame import OptimizedModule + + if len(value.__dict__) == 0: + unimplemented( + gb_type="Uninitialized nn.Module", + context=typestr(value), + explanation=f"Attempted to trace an uninitialized nn.Module of type {typestr(value)}.", + hints=[ + *graph_break_hints.USER_ERROR, + "Ensure your nn.Module instance has called `super().__init__()`.", + ], + ) + if istype(value, OptimizedModule): + # Check if the optimized module was disabled + if inspect.getattr_static(value.forward, "_torchdynamo_disable", False): + # This bytecode is mostly of kind LOAD_ATTR or LOAD_METHOD. If + # we graph break here, Dynamo does not know how to create + # continuation functions for such bytecodes. So, we delay the + # graph break to CALL_FUNCTION. + msg = inspect.getattr_static( + value.forward, "_torchdynamo_disable_msg", None + ) + return DelayGraphBreakVariable( + source=self.source, + msg=f"Optimized `nn.Module` is wrapped with `torch.compiler.disable` (reason: {msg})", + ) + + self.install_guards(GuardBuilder.TYPE_MATCH) + self.source = AttrSource(self.source, "_orig_mod") + return self.wrap_module(value._orig_mod) + + if type(value) is torch.jit._script.RecursiveScriptModule: + unimplemented( + gb_type="torch.jit.script/freeze modules unsupported", + context=str(value), + explanation="Dynamo does not support tracing into torch.jit.script or " + "torch.jit.freeze modules because they execute in the TorchScript " + "runtime, not Python. Replace the ScriptModule submodule with the " + "original eager nn.Module.", + hints=[ + *graph_break_hints.FUNDAMENTAL, + ], + ) + + if ( + isinstance(value, (torch.nn.RNN, torch.nn.GRU, torch.nn.LSTM)) + and not config.allow_rnn + ): + unimplemented( + gb_type="Attempted to wrap RNN, GRU, or LSTM", + context=str(value), + explanation="Dynamo does not support RNN, GRU, or LSTM.", + hints=[ + "Set torch._dynamo.config.allow_rnn=True to enable experimental support for RNN, GRU, and LSTM in Dynamo", + *graph_break_hints.SUPPORTABLE, + ], + ) + + if getattr(value, "_is_fsdp_managed_module", False): + # See note [Dynamo treats FSDP wrapped modules as UnspecializedNNModule] + # in fully_sharded_data_parallel.py for more information + + # we can't do this assert inside FSDP constructor, + # since we don't know yet whether dynamo will be used + if not getattr(value, "_fsdp_use_orig_params", False): + unimplemented( + gb_type="FSDP with use_orig_params=False", + context="", + explanation="Dynamo only supports FSDP with use_orig_params=True", + hints=[], + ) + + # Note on FSDP guarding + # Eager FSDP already assumes (requires, but without enforcement) + # that users don't mutate their model parameters/structure after + # FSDP wrapping, because FSDP wouldn't notice or update its + # FlatParams. + # + # Therefore, torch.compile can skip guarding on params or submodule + # structure of fsdp_managed modules, by using FSDPNNModuleSource as + # the guard source. This behavior is gated on + # config.skip_fsdp_guards. + self.install_guards(GuardBuilder.TYPE_MATCH) + result = FSDPManagedNNModuleVariable(value, source=self.get_source()) + if not SideEffects.cls_supports_mutation_side_effects(type(value)): + # don't allow STORE_ATTR mutation with custom __setattr__ + return result + return self.tx.output.side_effects.track_object_existing(value, result) + elif mutation_guard.is_dynamic_nn_module(value, self.tx.export): + # created dynamically, don't specialize on it + + # Note [Tracing a torch.compiled function] + # when make_fx tracing a compiled function, we need + if isinstance(value, torch.fx.experimental.proxy_tensor._AttrProxy): + # type: ignore[attr-defined] + value = value.get_base() + self.source = AttrProxySource(self.source) + + freezing = is_parameter_freezing() + + # Guard against the case where user may overwrite named parameters + # / named buffers + # NOTE: This is not likely to happen but worth guarding to avoid + # exception + if ( + callable(value.named_parameters) + # type: ignore[attr-defined] + and value.named_parameters.__func__ is og_module_named_parameters_fn_ptr + ): + try: # catch TypeErrors in named_parameters() from unserializable nn modules + # type: ignore[attr-defined] + for _, p in value.named_parameters(): + self.mark_static_input(p, guard=freezing) + except TypeError as e: + raise_observed_exception(type(e), self.tx, args=list(e.args)) + + if ( + callable(value.named_buffers) + # type: ignore[attr-defined] + and value.named_buffers.__func__ is og_module_named_buffers_fn_ptr + ): + try: # catch TypeErrors in named_parameters() from unserializable nn modules + # type: ignore[attr-defined] + for _, b in value.named_buffers(): + self.mark_static_input(b, guard=freezing) + except TypeError as e: + raise_observed_exception(type(e), self.tx, args=list(e.args)) + + if freezing: + # we need to add the module to tracing context + # in order to allow its params to get invalidated + # this will get cleaned up once compile ends + self.tx.output.nn_modules[self.name] = value + + if ( + value.__module__.startswith(("torch.nn.modules", "torch.ao.")) + and not value.__module__.startswith("torch.nn.modules.container") + ) or getattr(value.__class__, "_dynamo_marked_static", False): + new_source = self.source + if not self.tx.output.export or config.install_free_tensors: + new_source = UnspecializedBuiltinNNModuleSource(self.source) + result = UnspecializedBuiltinNNModuleVariable(value, source=new_source) + install_guard(new_source.make_guard(GuardBuilder.TYPE_MATCH)) + else: + new_source = self.source + if not self.tx.output.export or config.install_free_tensors: + new_source = UnspecializedNNModuleSource(self.source) + result = UnspecializedNNModuleVariable(value, source=new_source) + install_guard(new_source.make_guard(GuardBuilder.TYPE_MATCH)) + + self.tx.output.add_fqn_info_for_inlined_modules(value, self.source) + + if not SideEffects.cls_supports_mutation_side_effects(type(value)): + # don't allow STORE_ATTR mutation with custom __setattr__ + return result + return self.tx.output.side_effects.track_object_existing(value, result) + elif issubclass( + value.__class__, torch.nn.parallel.distributed.DistributedDataParallel + ): + self.install_guards(GuardBuilder.TYPE_MATCH) + return UnspecializedNNModuleVariable(value, source=self.get_source()) + else: + return self.tx.output.register_attr_or_module( + value, + self.name, + source=self.get_source(), + # Guards are added inside register_attr_or_module + ) + + def wrap_literal(self, value: object) -> VariableTracker: + if type(value) is int: + assert isinstance(value, int) + # allowlist has higher precedence over specialization control. + if is_dynamic_source(self.source.name): + log.debug("%s marked dynamic via source whitelist", self.source.name) + return self.wrap_symint(value, dynamism=DimDynamic.DYNAMIC) + + if is_unbacked_source(self.source.name): + log.debug("%s marked unbacked via source whitelist", self.source.name) + return self.wrap_symint(value, dynamism=DimDynamic.UNBACKED) + + if not config.specialize_int: + # unspecializing int by default, but still + # specialize for the following conditions + if is_int_specialization_case(value, self.source): + recompile_hint = None + if ( + self.source.guard_source.is_unspecialized_builtin_nn_module() + or self.source.guard_source.is_unspecialized_nn_module() + ): + # This means that it is an integer from a NN module. + # Dynamo considers nn module int attributes to be static + # (a good heuristic). But a user might want to mark the + # int attribute to be a symint, so track this integer + # for recompilation later. + recompile_hint = ( + "torch.compile considers integer attributes of the nn.Module to be static. " + "If you are observing recompilation, you might want to make this integer dynamic " + "using torch._dynamo.config.allow_unspec_int_on_nn_module = True, or convert this " + "integer into a tensor." + ) + + process_automatic_dynamic( + self.tx, + self.source.name, + FrameStateSizeEntry.make_scalar(value), + is_unspecialized_nn_module=self.source.guard_source.is_unspecialized_nn_module(), + ) + self.install_guards( + functools.partial( + GuardBuilder.EQUALS_MATCH, recompile_hint=recompile_hint + ) + ) + return ConstantVariable.create(value=value, source=self.source) + + return self._wrap_lazy_constant(value, self._wrap_symint_for_lazy) + + return self._wrap_lazy_constant(value) + elif type(value) is float: + assert isinstance(value, float) + if not config.specialize_float: + return self._wrap_lazy_constant(value, self._wrap_symfloat_for_lazy) + + return self._wrap_lazy_constant(value) + elif type(value) in (bool, str): + assert isinstance(value, (bool, str)) + return self._wrap_lazy_constant(value) + else: + self.install_guards(GuardBuilder.CONSTANT_MATCH) + result = ConstantVariable.create(value=value, source=self.source) + if isinstance(value, (list, set)): + return self.tx.output.side_effects.track_mutable(value, result) + return result + + def _wrap_symint_for_lazy(self, value: int) -> VariableTracker: + return self.wrap_symint(value) + + def _wrap_symfloat_for_lazy(self, value: float) -> VariableTracker: + return self.wrap_symfloat(value) + + @overload + def _wrap_lazy_constant( + self, + value: int, + wrap_fn: Callable[[int], VariableTracker], + ) -> VariableTracker: ... + + @overload + def _wrap_lazy_constant( + self, + value: float, + wrap_fn: Callable[[float], VariableTracker], + ) -> VariableTracker: ... + + @overload + def _wrap_lazy_constant( + self, + value: int | float | bool | str, + wrap_fn: None = None, + ) -> VariableTracker: ... + + def _wrap_lazy_constant( + self, + value: int | float | bool | str, + wrap_fn: Callable[[Any], VariableTracker] | None = None, + ) -> VariableTracker: + """Wrap a primitive constant, deferring guard installation if allowed.""" + if not self.allow_lazy_constant: + if wrap_fn is not None: + return wrap_fn(value) + self.install_guards(GuardBuilder.CONSTANT_MATCH) + return ConstantVariable.create(value=value, source=self.source) + return LazyConstantVariable.create(value, source=self.source) + + def assert_not_wrapped_by_this_graph(self, value: torch.Tensor) -> None: + if is_fake(value) and maybe_get_fake_mode(value) is self.tx.fake_mode: + raise InternalTorchDynamoError( + "Cannot wrap a Tensor that has already been", + "wrapped by this instance of Dynamo", + ) + + def wrap_tensor(self, value: torch.Tensor) -> VariableTracker: + source = self.get_source() + + # We cannot already be tracking the tensor, which implies + # it would have already been wrapped + assert value not in self.tx.output.side_effects + + is_static_input = get_static_address_type(value) is not None + + if not is_static_input and ( + isinstance(value, torch.nn.Parameter) + # mark tensor attributes of nn modules static + or (source and source.guard_source.is_unspecialized_nn_module()) + ): + self.mark_static_input(value, guard=is_parameter_freezing()) + is_static_input = True + + # Install any tensors which are "free" variables; that is: + # 1. Globals + # 2. NonLocals + # 3. tensors that are attributes of nn module + should_install_free_tensor = config.install_free_tensors and ( + is_from_global_source(source) + or is_from_nonlocal_source(source) + or is_from_unspecialized_nn_module_source(source) + ) + + make_graph_attribute = is_static_input and ( + is_parameter_freezing() or torch._dynamo.config.prepare_freezing + ) + + if should_install_free_tensor or ( + (source.guard_source.is_specialized_nn_module() or make_graph_attribute) + and not source.guard_source.is_fsdp_module() + ): + self.assert_not_wrapped_by_this_graph(value) + return self.tx.output.register_attr_or_module( + value, self.name, source=source + ) + + if get_static_address_type(value) == "guarded": + # If it's a guarded tensor, we can install the parameter directly + # into the Fx graph instead of lifting it as an input. Lifting + # offers no benefit, such as regional compilation, since we still + # guard on the tensor's ID. Moreover, installing it in the Fx graph + # eliminates the pre-graph bytecode required to extract the tensor + # from locals/globals, reducing overhead. This can lead to + # significant cost savings, especially for optimizers handling many + # tensors. + self.install_guards(GuardBuilder.ID_MATCH) + self.assert_not_wrapped_by_this_graph(value) + return self.tx.output.register_attr_or_module( + value, self.name, source=source + ) + + if is_constant_source(source): + self.assert_not_wrapped_by_this_graph(value) + return self.tx.output.register_attr_or_module( + value, + re.sub(r"[^a-zA-Z0-9]+", "_", self.name), + source=source, + # Guards are added inside register_attr_or_module + ) + + # NB: this just says we accessed a tensor from the same source again + # (e.g., a tensor lives in a global foo, and we LOAD_GLOBAL it twice). + # This is distinct from two distinct sources mapping to the same + # Tensor (per id())! No guard is necessary here. See below for the + # other case. + is_duplicate_tensor = source in self.tx.output.input_source_to_var + if is_duplicate_tensor: + return self.tx.output.input_source_to_var[source] + + options = {} + subclass_type = infer_subclass_type(value) + if subclass_type is not None: + self.install_guards(GuardBuilder.TYPE_MATCH) + + if get_static_address_type(value) == "guarded": + self.install_guards(GuardBuilder.ID_MATCH) + + # By this point, we should have deduplicated all tensors + self.assert_not_wrapped_by_this_graph(value) + + if ( + isinstance(value, torch.Tensor) + and value.is_nested + and not isinstance(value, torch.nested._internal.nested_tensor.NestedTensor) + ): + unimplemented( + gb_type="Attempted to wrap strided NestedTensor", + context="", + explanation="torch.compile does not support strided NestedTensor", + hints=[], + ) + + # TODO(pearu,sparse-team) - Add the corresponding SPARSE_TENSOR_MATCH guards + if ( + isinstance(value, torch.Tensor) + and is_sparse_any(value) + and (not self.tx.export or not config.capture_sparse_compute) + ): + # A hot fix for sparse tensors + torch.compile. Support for + # export + sparsity is being added but we need to create + # SPARSE_TENSOR_GUARDS for guards to work properly. + unimplemented( + gb_type="Attempted to wrap sparse Tensor", + context="", + explanation="torch.compile does not support sparse Tensors", + hints=[*graph_break_hints.SPARSE_TENSOR], + ) + + if ( + safe_has_grad(value) + and safe_grad(value) is not None + # type: ignore[attr-defined] + and value.dtype != safe_grad(value).dtype + ): + safe_grad_val = safe_grad(value) + grad_str = str(safe_grad_val.dtype) if safe_grad_val is not None else "None" + unimplemented( + gb_type="dtype mismatch between tensor and its gradient", + context=f"tensor dtype: {value.dtype}; grad dtype: {grad_str}", + explanation="Inconsistent dtype between tensor and its gradient. " + "This can happen in FSDP and crashes meta tensor creation.", + hints=[*graph_break_hints.SUPPORTABLE], + ) + + # tx.output has multiple tracers if we're introspecting HigherOrderOperator. + # When we've discovered an untracked tensor, then we actually need + # to get Dynamo to track the tensor (which is what this function does) + # and put it as a graph input on the root tracer. Later on, + # if the input is actually used in the body of the HigherOrderOperator, + # then the relevant SubgraphTracer will lift it to being an input of + # the subgraph. + # See NOTE [HigherOrderOperator tracing design] for more details. + + example_value = wrap_to_fake_tensor_and_record( + value, tx=self.tx, is_tensor=True, source=source + ) + + tensor_proxy = self.tx.output.root_tracer.create_graph_input( + re.sub(r"[^a-zA-Z0-9]+", "_", self.name), + type(value), + example_value, + source=source, + ) + cache_real_value_when_export(self.tx, tensor_proxy, value) + + tensor_variable = wrap_fx_proxy( + tx=self.tx, + proxy=tensor_proxy, + example_value=example_value, + subclass_type=subclass_type, + source=source, + **options, + ) + + # Track input tensors for attribute mutation, matching how + # handle_traced_output tracks intermediate tensors with AttributeMutationNew. + # This enables setattr on input tensors (e.g. tensor.custom_attr = val) + # without graph breaking. + self.tx.output.side_effects.track_object_existing(value, tensor_variable) + + if value._is_view(): + # If value is a view, add its base tensor to the tracked fakes list. + # This is so we are able to access the correct source for its symbolic + # shape values, in case we need them. + wrap_to_fake_tensor_and_record( + value._base, + tx=self.tx, + source=AttrSource(source, "_base"), + is_tensor=True, + ) + + guard_type = GuardBuilder.TENSOR_MATCH + + if isinstance(source, GradSource) and is_from_optimizer_source(source): + guard_type = GuardBuilder.NOT_NONE_MATCH + + is_dtensor = torch.distributed.is_available() and isinstance( + value, torch.distributed.tensor.DTensor + ) + if not is_dtensor: + # We guard on the _local_tensor and the _spec, and therefore we dont + # have to guard on the outer DTensor. + self.install_guards( + functools.partial( + guard_type, + value=( + value + if isinstance(source, NumpyTensorSource) + else TensorWeakRef(value) + ), + ) + ) + + # We install TYPE_MATCH guards for traceable wrapper subclass object, + # and recursively install corresponding guard for each inner attribute. + if is_traceable_wrapper_subclass(value): + # Tensor subclass guards are very expensive because they are + # implemented in Python. Since DTensor is PyTorch-maintained class, + # we can skip a lot of these guards. + if is_dtensor: + self.install_guards(GuardBuilder.TYPE_MATCH) + + inner_attrs = value.__tensor_flatten__()[0] + if inner_attrs != ["_local_tensor", "device_mesh"]: + raise RuntimeError( + "Expecting DTensor inner attrs to be ['_local_tensor', 'device_mesh']" + ) + + flattening_ctx = value.__tensor_flatten__()[1] + if not ( + len(flattening_ctx) == 4 + and flattening_ctx[0] == value._spec.placements + and flattening_ctx[1] == value._spec.tensor_meta + and flattening_ctx[2] == value._spec.shard_order + and flattening_ctx[3] == value.requires_grad + ): + raise RuntimeError( + "Expecting DTensor flattening ctx to be (placements, tensor_meta, shard_order, requires_grad)" + ) + # Guard on the dtensor spec + install_guard( + AttrSource(self.source, "_spec").make_guard( + GuardBuilder.DTENSOR_SPEC_MATCH + ) + ) + # Move this to C++ + install_guard( + AttrSource(self.source, "requires_grad").make_guard( + GuardBuilder.EQUALS_MATCH + ) + ) + else: + self.install_guards(GuardBuilder.TENSOR_SUBCLASS_METADATA_MATCH) + self.install_guards(GuardBuilder.TYPE_MATCH) + install_guard( + SubclassAttrListSource(source).make_guard(GuardBuilder.EQUALS_MATCH) + ) + + attrs, _ = value.__tensor_flatten__() + for attr in attrs: + inner_value = getattr(value, attr) + # FakeScriptObject wraps the real opaque object during + # fake-mode tracing; unwrap before the type check. + inner_type = type(inner_value) + if isinstance( + inner_value, + torch._library.fake_class_registry.FakeScriptObject, + ): + inner_type = type(inner_value.real_obj) + if not isinstance( + inner_value, torch.Tensor + ) and not is_opaque_reference_type(inner_type): + raise RuntimeError( + f"{type(inner_value).__name__!r} found in tensor attrs of " + f"{type(value).__name__}.__tensor_flatten__(). " + "Only tensors and reference-type opaques are allowed " + "in tensor attrs." + ) + inner_source = AttrSource(self.source, attr) + LazyVariableTracker.realize_all( + VariableBuilder(self.tx, inner_source)(inner_value) + ) + + self.tx.output.input_source_to_var[source] = tensor_variable + assert "tensor_dict" not in tensor_proxy.node.meta + tensor_proxy.node.meta["tensor_dict"] = _extract_tensor_dict(value) + + # Note: this information is conveyed via subclass_type now + # type: ignore[attr-defined] + fake_tensor_value = tensor_variable.proxy.node.meta["example_value"] + if maybe_get_fake_mode(fake_tensor_value) is not self.tx.fake_mode: + raise InternalTorchDynamoError("Wrapped Tensor must be this graph's fake") + + grapharg = GraphArg(source, value, False, fake_tensor_value) + tensor_proxy.node.meta["grapharg"] = grapharg + return tensor_variable + + def wrap_numpy_ndarray(self, value: Any) -> VariableTracker: + assert np is not None + assert isinstance(value, np.ndarray) + + source = NumpyTensorSource(self.get_source()) + + from torch._numpy import _util + + readonly = not value.flags.writeable + if readonly: + try: + value.flags.writeable = True + except ValueError: + # One can not easily make nditer elements writable, + # but warning is not the end of the world + assert isinstance(value.base, np.nditer) + tensor_value = None + with torch_function_mode_stack_state_mgr.temp_restore_stack(): + try: + tensor_value = _util._try_convert_to_tensor(value) + if readonly: + from torch._prims_common import clone_preserve_strides + + tensor_value = clone_preserve_strides(tensor_value) + except NotImplementedError as e: + # failed to convert to tensor, graph break + unimplemented( + gb_type="failed to convert numpy.ndarray to Tensor", + context=str(value), + explanation="Exception encountered when attempting to convert numpy.ndarray to Tensor", + hints=[], + from_exc=e, + ) + assert tensor_value is not None + + # We do this because we want the full behavior of guarding the numpy ndarray as if it were + # a tensor. It's a little annoying to make a VT to throw out, but there's so many side effects here + # that there's not another great way to do this atm. + # This creates the right graphargs, as well as registration for guards in tensor names and shape env. + LazyVariableTracker.realize_all(VariableBuilder(self.tx, source)(tensor_value)) + example_value = wrap_to_fake_tensor_and_record( + tensor_value, + tx=self.tx, + is_tensor=False, + source=source, + ) + proxy = self.tx.output.root_tracer.create_graph_input( + re.sub(r"[^a-zA-Z0-9]+", "_", self.name), + type(tensor_value), + example_value, + source=source, + ) + cache_real_value_when_export(self.tx, proxy, tensor_value) + options = {"source": source} + numpy_ndarray_variable = wrap_fx_proxy_cls( + target_cls=NumpyNdarrayVariable, + tx=self.tx, + proxy=proxy, + example_value=example_value, + subclass_type=None, + **options, + ) + + self.tx.output.input_source_to_var[source] = numpy_ndarray_variable + # type: ignore[attr-defined] + example_value = numpy_ndarray_variable.proxy.node.meta["example_value"] + + # pass_arg_as_tensor should be true because we are wrapping a np.ndarray as argument input, and it needs to be + # converted to a tensor. + grapharg = GraphArg( + source, + tensor_value, + pass_arg_as_tensor=True, + fake_tensor=example_value, + is_tensor=True, + example_strong_ref=tensor_value, + ) + proxy.node.meta["grapharg"] = grapharg + + # TODO - Why do we need to set the source of the np ndarray vt back to + # original source. Many tests fails. + numpy_ndarray_variable.source = self.source + + return numpy_ndarray_variable + + def wrap_symint( + self, + value: int, + dynamism: DimDynamic | None = None, + context: SymIntSymbolicContext | None = None, + ) -> VariableTracker: + assert type(value) is int + + if self.name in self.tx.output.unspec_variable_map: + return self.tx.output.unspec_variable_map[self.name] + + shape_env = self.tx.output.shape_env + frame_state_entry: FrameStateSizeEntry | None = None + if TracingContext.get().force_unspec_int_unbacked_size_like: + wrapped_value = shape_env.create_unbacked_symint() + _constrain_range_for_size(wrapped_value) + self.tx.output.tracked_fakes.append( + TrackedFake(wrapped_value, self.source, None) + ) + + # NB: We do not do float. For motivation, see + # https://docs.google.com/document/d/1INSCdYu1PxXcr43HrD82OudeEuS-qxQe1yZmLg2wy6A/edit + # but the general idea is that we generate kernels that can + # take unspecialized floats and use them in sizevar computation + elif not is_constant_source(self.get_source()): + if dynamism is None and torch._dynamo.config.specialize_int: + # If specialize_int is False, also return + # a constant (but this should have been handled + # in the caller, TBH). But if `dynamism` is set, then actually + # turn it into a symint + self.install_guards(GuardBuilder.CONSTANT_MATCH) + return ConstantVariable.create(value=value, source=self.source) + + name = self.source.name + + frame_state_entry = process_automatic_dynamic( + self.tx, + name, + FrameStateSizeEntry.make_scalar(value), + is_unspecialized_nn_module=self.source.guard_source.is_unspecialized_nn_module(), + ) + + # TODO: This should be dynamic, as we in general do not + # know if bare integers are actually going to be sizevars + # and it is inappropriate to eagerly duck size them with + # real sizevars + normalized_source_name = normalize_source_name(self.source.name) + base_source = self.source + if isinstance(base_source, ChainedSource): + base_source = base_source.get_base() + + if dynamism is not None: + dynamic_dim = dynamism + elif ( + config.automatic_dynamic_shapes + and frame_state_entry.scalar is auto_dynamic + ): + set_feature_use("dynamo.automatic_dynamic_shapes", True) + dynamic_dim = get_automatic_dynamic_shapes_mark_as() + elif ( + isinstance(base_source, LocalSource) + and base_source.dynamism is not None + # pyrefly: ignore[no-matching-overload] + and dict(base_source.dynamism).get(normalized_source_name, {0: False})[ + 0 + ] + ) or not config.assume_static_by_default: + dynamic_dim = DimDynamic.DYNAMIC + else: # assume_static_by_default + # TODO: dynamic_dim = DimDynamic.STATIC should work but + # for some reason it doesn't + if frame_state_entry.scalar is auto_dynamic: + set_feature_use("dynamo.automatic_dynamic_shapes", False) + self.install_guards(GuardBuilder.CONSTANT_MATCH) + return ConstantVariable.create(value=value) + + excluded_scalar = ( + frame_state_entry.excluded_scalar + if config.automatic_dynamic_exclusion_guard + and frame_state_entry is not None + else None + ) + wrapped_value = shape_env.create_unspecified_symint_and_symbol( + value, + source=self.source, + dynamic_dim=dynamic_dim, + excluded_value=excluded_scalar, + ) + if not isinstance(wrapped_value, SymInt): + raise AssertionError(f"Expected SymInt, got {type(wrapped_value)}") + + self.tx.output.tracked_fakes.append( + TrackedFake(wrapped_value, self.source, context) + ) + else: + assert is_constant_source(self.get_source()) + # TODO: Do I actually need guard for constant source? + self.install_guards(GuardBuilder.CONSTANT_MATCH) + return ConstantVariable.create(value=value, source=self.source) + + assert not isinstance(self.get_source(), RandomValueSource) + install_guard(self.get_source().make_guard(GuardBuilder.TYPE_MATCH)) + + options = {"source": self.get_source()} + + proxy = self.tx.output.root_tracer.create_graph_input( + re.sub(r"[^a-zA-Z0-9]+", "_", self.name), + type(wrapped_value), + wrapped_value, + source=self.get_source(), + ) + + sym_expr = wrapped_value.node.expr + assert isinstance(sym_expr, sympy.Symbol), f"{sym_expr} is not a basic Symbol." + self.tx.output.root_tracer.bound_symbols[sym_expr] = proxy + unspec_var = SymNodeVariable.create(self.tx, proxy, wrapped_value, **options) + # type: ignore[assignment] + self.tx.output.unspec_variable_map[self.name] = unspec_var + + if not is_constant_source(self.get_source()): + proxy.node.meta["grapharg"] = GraphArg( + self.get_source(), + wrapped_value, + pass_arg_as_tensor=False, + fake_tensor=None, + is_tensor=False, + example_strong_ref=wrapped_value, + ) + + return unspec_var + + def wrap_symfloat(self, value: float) -> VariableTracker: + # SymFloat wrapping is special. We first wrap it in the same way we + # do an unspecialized primitive, and then we item() it into a + # SymFloat. Removal of the item() call is left to a later FX pass, + # mostly because that pass is more easily done after we have lowered + # to ATen ops. (Dynamo doesn't do decomposition right now). + + if self.name in self.tx.output.unspec_variable_map: + return self.tx.output.unspec_variable_map[self.name] + + frame_state_entry = process_automatic_dynamic( + self.tx, + self.source.name, + # type: ignore[arg-type] + FrameStateSizeEntry.make_scalar(value), + is_unspecialized_nn_module=self.source.guard_source.is_unspecialized_nn_module(), + ) + + # NB: we specialize on nan input, because our guard modeling in + # ShapeEnv cannot deal with nan + if ( + torch._dynamo.config.specialize_float + or is_constant_source(self.get_source()) + or math.isnan(value) + or math.isinf(value) + # We don't support cudagraphs for now. Without this cudagraphs + # break because they expect all cuda inputs but our tensorified + # float will be a f64[] cpu tensor. Fixes the following test + # when specialize_float=False + # python test/inductor/test_compiled_optimizers.py CompiledOptimizerTests.test_rmsprop_weight_decay_maximize_capturable_cuda # noqa: B950 + or torch._inductor.config.triton.cudagraphs + or justknobs_check("pytorch/compiler:unspecialize_float_killswitch", False) + or ( + config.assume_static_by_default + and frame_state_entry.scalar is not auto_dynamic + ) + ): + self.install_guards(GuardBuilder.CONSTANT_MATCH) + return ConstantVariable.create(value=value, source=self.source) + + # NB: At the point we've gotten here, we don't assume static by + # default. Since we have a guard mechanism, there isn't really any + # downside to trying to be dynamic for float all the time. Unlike + # ints, this won't make codegen perf worse. Modest cost to compile + # time. + + wrapped_value = torch.tensor(value, dtype=torch.float64) + + # We don't support specializing floats for grad checking tensors + # See https://github.com/pytorch/pytorch/pull/140828 for more + # context. + if torch._C._functorch.is_gradtrackingtensor(wrapped_value): + self.install_guards(GuardBuilder.CONSTANT_MATCH) + return ConstantVariable.create(value=value, source=self.source) + + # TODO: Switch RandomValueSource over to use this, this is more + # accurate + assert not isinstance(self.get_source(), RandomValueSource) + install_guard(self.get_source().make_guard(GuardBuilder.TYPE_MATCH)) + + # The FloatTensorSource here is just for pedantic correctness: if you + # guard against an UnspecializedPythonVariable, you need to guard + # against the tensor-ified version of the local, otherwise it's not a + # Tensor. However, we never let the UnspecializedPythonVariable escape + # here, so there should never actually be any guards against this + # source. + source = FloatTensorSource(self.get_source()) + options = {"source": source, "raw_value": value} + + # TODO: Maybe the tensor-ification should be built into the source, + # rather than by special pattern match + example_value = wrap_to_fake_tensor_and_record( + wrapped_value, tx=self.tx, is_tensor=False, source=source + ) + proxy = self.tx.output.root_tracer.create_graph_input( + re.sub(r"[^a-zA-Z0-9]+", "_", self.name), + type(wrapped_value), + example_value, + source=source, + ) + cache_real_value_when_export(self.tx, proxy, wrapped_value) + + unspec_var = wrap_fx_proxy_cls( + UnspecializedPythonVariable, + tx=self.tx, + proxy=proxy, + example_value=example_value, + subclass_type=None, + **options, + ) + assert isinstance(unspec_var, UnspecializedPythonVariable) + self.tx.output.unspec_variable_map[self.name] = unspec_var + + if self.tx.export and not isinstance(self.get_source(), LocalSource): + raise AssertionError( + f"Dynamo attempts to add additional input during export: value={wrapped_value}, source={self.get_source()}" + ) + fake_tensor_value = None + example_value = unspec_var.proxy.node.meta["example_value"] + assert is_fake(example_value) + + fake_tensor_value = example_value + # type: ignore[attr-defined] + assert fake_tensor_value.fake_mode is self.tx.fake_mode, ( + f"fake mode ({fake_tensor_value.fake_mode}) from fake tensor metadata doesn't match mode" + "({self.tx.fake_mode}) from InstructionTranslator" + ) + + # There's something a bit incoherent about pass_arg_as_tensor, + # specifically regarding sources. + # + # Specifically, suppose we have "x: float" local argument. We + # eventually end up with an UnspecializedPythonVariable denoting + # torch.as_tensor(x)... but it's source is still L['x'] (which if you + # accessed it directly is a float!) So you gotta be careful when + # setting up your guards, because it's still going to be a float at + # this point, the conversion happens only precisely at the point we're + # actually calling the FX graph. This happens to be what we want for + # shape guard generation, but it's kind of unintuitive. + proxy.node.meta["grapharg"] = GraphArg( + self.get_source(), + wrapped_value, + pass_arg_as_tensor=True, + # type: ignore[arg-type] + fake_tensor=fake_tensor_value, + is_tensor=False, + example_strong_ref=wrapped_value, + ) + + # Directly do item to bypass capture_scalar_outputs + r = wrap_fx_proxy( + self.tx, + self.tx.output.create_proxy( + "call_method", + "item", + *proxy_args_kwargs([unspec_var], {}), + ), + ) + # type: ignore[attr-defined] + self.tx.output.tracked_fakes.append(TrackedFake(r.sym_num, self.source, None)) + + get_metrics_context().set("tensorify_float_attempt", True, overwrite=True) + + return r + + def wrap_unspecialized_primitive(self, value: Any) -> VariableTracker: + if self.name in self.tx.output.unspec_variable_map: + return self.tx.output.unspec_variable_map[self.name] + + wrapped_value = torch.tensor(value) + if not isinstance(self.get_source(), RandomValueSource): + install_guard(self.get_source().make_guard(GuardBuilder.TYPE_MATCH)) + + options = {"source": self.get_source()} + options.update({"raw_value": value}) + + example_value = wrap_to_fake_tensor_and_record( + wrapped_value, tx=self.tx, is_tensor=False, source=self.get_source() + ) + proxy = self.tx.output.root_tracer.create_graph_input( + re.sub(r"[^a-zA-Z0-9]+", "_", self.name), + type(wrapped_value), + example_value, + source=self.get_source(), + ) + cache_real_value_when_export(self.tx, proxy, wrapped_value) + + unspec_var = wrap_fx_proxy_cls( + UnspecializedPythonVariable, + tx=self.tx, + proxy=proxy, + example_value=example_value, + subclass_type=None, + **options, + ) + # type: ignore[assignment] + self.tx.output.unspec_variable_map[self.name] = unspec_var + if not is_constant_source(self.get_source()): + if self.tx.export and not isinstance(self.get_source(), LocalSource): + raise AssertionError( + f"Dynamo attempts to add additional input during export: value={wrapped_value}, source={self.get_source()}" + ) + fake_tensor_value = None + if unspec_var.is_python_constant(): + # TODO: when can this happen? + example_value = unspec_var.as_python_constant() + else: + # type: ignore[attr-defined] + example_value = unspec_var.proxy.node.meta["example_value"] + assert is_fake(example_value) + + fake_tensor_value = example_value + # type: ignore[attr-defined] + assert fake_tensor_value.fake_mode is self.tx.fake_mode, ( + f"fake mode ({fake_tensor_value.fake_mode}) from fake tensor metadata doesn't match mode" + "({self.tx.fake_mode}) from InstructionTranslator" + ) + + proxy.node.meta["grapharg"] = GraphArg( + self.get_source(), + wrapped_value, + pass_arg_as_tensor=True, + # type: ignore[arg-type] + fake_tensor=fake_tensor_value, + is_tensor=False, + example_strong_ref=wrapped_value, + ) + return unspec_var + + +def _dataclasses_fields_lambda(obj: VariableTracker) -> TupleVariable: + value = None + if isinstance(obj, UserDefinedObjectVariable): + value = obj.value + else: + unimplemented( + gb_type="dataclass fields failure", + context=f"obj: {obj}; variable type: {type(obj)}", + explanation=f"Dataclass fields handling fails for {obj}. Expected it to be a user-defined object.", + hints=[], + ) + assert value is not None + items = [] + # type: ignore[arg-type] + for field in dataclasses.fields(value): + source = None + if obj.source: + base_src = AttrSource(obj.source, "__dataclass_fields__") + source = DictGetItemSource(base_src, field.name) + items.append(UserDefinedObjectVariable(field, source=source)) + # pyrefly: ignore [bad-argument-type] + return TupleVariable(items) + + +def _clone_input(value: Any, fake_mode: FakeTensorMode | None) -> Any: + if isinstance(value, torch.Tensor): + # tensor subclasses will not be converted to FakeTensors and need to be cloned + if not ( + isinstance(value, FakeTensor) + or ( + # Is functional tensor fakeified by this instance of Dynamo + torch._is_functional_tensor(value) + and maybe_get_fake_mode(value) is fake_mode + ) + or value.is_nested + ): + # NB: ensure strides are preserved + value = clone_input(value) + + return value + + +def wrap_fx_proxy( + tx: "InstructionTranslatorBase", + proxy: Any, + example_value: Any | None = None, + subclass_type: type | None = None, + **options: Any, +) -> VariableTracker: + kwargs = { + "tx": tx, + "proxy": proxy, + "example_value": example_value, + "subclass_type": subclass_type, + **options, + } + if subclass_type is None: + # pyrefly: ignore[bad-argument-type] + return wrap_fx_proxy_cls(target_cls=TensorVariable, **kwargs) + else: + # pyrefly: ignore[bad-argument-type] + result = wrap_fx_proxy_cls(target_cls=TensorWithTFOverrideVariable, **kwargs) + # type: ignore[attr-defined] + result.install_global(tx) + return result + + +def cache_real_value_when_export( + tx: "InstructionTranslatorBase", proxy: Any, example_value: Any +) -> None: + if tx.export: + # The legacy behavior for real value cache with subclasses was + # to perform a clone WITHOUT preserving the subclass. It's + # not entirely clear this is what you actually want though. + with torch._C.DisableTorchFunctionSubclass(): + proxy.tracer.real_value_cache[proxy.node] = _clone_input( + example_value, tx.fake_mode + ) + + +# Note: Unfortunate split due to some gross classes existing that subclass TensorVariable +# Should be compositional instead +# +# This is a horribly complicated function that does too many things, to +# explain what it does, let's first talk about the classic usage wrap_fx_proxy +# for a TensorVariable. There are two primary modes of use: +# +# 1. Wrapping a pre-existing Tensor. In this case, example_value is set +# to the pre-existing Tensor. (Note that this example_value will NOT +# be the final example_value we put into node.meta['example_value'], +# instead it is converted into a fake tensor using +# wrap_to_fake_tensor_and_record and registered as a graph input.) +# +# 2. "Wrapping" the result of some Tensor operation Dynamo traced over. In +# this case, example_value is None (and we are going to figure it out +# ourselves using FakeTensors, via get_fake_value, which will run +# the operation represented by the (singular!) FX node referenced by +# the passed in proxy.) +# +# The expectation is you end up with a Tensor output, and everything is +# straightforwardly traced into the graph. +# +# In all cases, the returned `TensorVariable` subclass will have an `example_value` +# and that `example_value` must be a `FakeTensor` produced by the currently running +# instance of Dynamo. +# +# Upon closer inspection, you may notice that there are a slurry of non-Tensor +# output cases in handle_traced_output. What gives? Well, we sometimes trace operations into the +# graph that don't involve tensors. +# +# * Some operators return tuples; we need to recursively handle their +# contents +# +# * Some operators have side effects that will affect subsequent AOTAutograd +# tracing but don't otherwise return anything. +# +# * Some operators return symbolic ints/floats/bools which can go in the +# graph and be traced (but only if they're actually symbolic! If they're +# static you don't want to put them in the graph, which means you +# shouldn't call this function.) +# +# The common theme is that you only use this function WHEN YOU ARE TRACING +# SOMETHING INTO THE GRAPH. This is sort of obvious, because you can't call +# this function without a proxy. +def wrap_fx_proxy_cls( + target_cls: type[VTTypeAlias], + tx: "InstructionTranslatorBase", + proxy: Any, + example_value: Any | None = None, + subclass_type: type | None = None, + **options: Any, +) -> VTTypeAlias: + if example_value is None: + out: VTTypeAlias = _wrap_fx_proxy( + target_cls, tx, proxy, example_value, subclass_type, **options + ) + elif isinstance(example_value, torch.Tensor): + out = _wrap_fx_preexisting_tensor( + target_cls, tx, proxy, example_value, subclass_type, **options + ) + else: + # This will skip tracing an op and recursively reinvoke wrap_fx_proxy_cls on supported + # data structures. In essence this just handles tracing some other value which may + # contain Fake Tensors or is otherwise proxyable. + # pyrefly: ignore[bad-assignment] + out = handle_traced_output( + example_value, tx, proxy, options, subclass_type, target_cls + ) + + if ( + isinstance( + out, + ( + torch._dynamo.variables.TensorVariable, + torch._dynamo.variables.SymNodeVariable, + ), + ) + and proxy.node.op != "placeholder" + ): + tx.output.current_tracer.record_proxyable_vt(out) + return out + + +# This is 1 above (wrapping a preexisting tensor) +def _wrap_fx_preexisting_tensor( + target_cls: type[VTTypeAlias], + tx: "InstructionTranslatorBase", + proxy: torch.fx.Proxy, + tensor: torch.Tensor, + subclass_type: type | None = None, + **options: Any, +) -> VTTypeAlias: + from ..symbolic_convert import InstructionTranslatorBase + + assert isinstance(tensor, torch.Tensor), ( + f"_wrap_fx_preexisting_tensor expected tensor, got {type(tensor)}" + ) + + assert isinstance(tx, InstructionTranslatorBase) + if "guards" in options and options["guards"] is not None: + tx.output.guards.update(options["guards"]) + + # Placeholders always carry example_value in node.meta. + # non-placeholders always have no example_value in node.meta + if proxy.node.op == "placeholder": + assert "example_value" in proxy.node.meta, ( + f"placeholder {proxy} doesn't have 'example_value' in node.meta" + ) + else: + assert "example_value" not in proxy.node.meta, ( + f"{proxy.node.meta['example_value']}" + ) + + # See NOTE: [Deferring tensor pack/unpack hooks until runtime] + with torch._dynamo.utils._disable_saved_tensors_hooks_during_tracing(): + # Handle recursive calls here + if maybe_get_fake_mode(tensor) is tx.fake_mode: + pass + else: + cache_real_value_when_export(tx, proxy, tensor) + if tx.export: + # The legacy behavior for real value cache with subclasses was + # to perform a clone WITHOUT preserving the subclass. It's + # not entirely clear this is what you actually want though. + with torch._C.DisableTorchFunctionSubclass(): + # type: ignore[attr-defined] + proxy.tracer.real_value_cache[proxy.node] = _clone_input( + tensor, tx.fake_mode + ) + # NB: If we're ignoring subclass, then the expectation is you will + # take the returned TensorVariable and wrap it into a more + # accurate TensorVariable that is able to track subclass-ness; + # otherwise this is wrong! + kwargs = { + "is_tensor": target_cls + in (TensorVariable, TensorWithTFOverrideVariable), + } + assert "source" in options and options["source"] is not None + kwargs["source"] = options["source"] + # pyrefly: ignore [missing-argument] + tensor = wrap_to_fake_tensor_and_record(tensor, tx=tx, **kwargs) + + if tensor.device.type != "meta" and ( + maybe_get_fake_mode(tensor) is not tx.fake_mode + ): + raise InternalTorchDynamoError( + "`tensor` needs to be a `FakeTensor`" + f"wrapped by this instance of Dynamo. Found: {tensor}" + ) + + return construct_tensor_variable( + target_cls, tx, proxy, tensor, subclass_type, options + ) + + +# This is 2 in the above comment (wrapping the output of a traced op) +def _wrap_fx_proxy( + target_cls: type[VTTypeAlias], + tx: "InstructionTranslatorBase", + proxy: torch.fx.Proxy, + example_value: Any | None = None, + subclass_type: type | None = None, + **options: Any, +) -> VTTypeAlias: + from ..symbolic_convert import InstructionTranslatorBase + + assert isinstance(tx, InstructionTranslatorBase) + if "guards" in options and options["guards"] is not None: + tx.output.guards.update(options["guards"]) + + assert "example_value" not in proxy.node.meta, f"{proxy.node.meta['example_value']}" + + # See NOTE: [Deferring tensor pack/unpack hooks until runtime] + with torch._dynamo.utils._disable_saved_tensors_hooks_during_tracing(): + # with preserve_rng_state(): + # only allow_non_graph_fake in this instance because we handle the non-fake + # cases properly below. + example_value = get_fake_value(proxy.node, tx, allow_non_graph_fake=True) + + # pyrefly: ignore[bad-return] + return handle_traced_output( + # type: ignore[arg-type] + example_value, + tx, + proxy, + options, + subclass_type, + target_cls, + ) + + +# This handles wrapping of the output of an op traced into the graphs +def handle_traced_output( + example_value: Any, + tx: "InstructionTranslatorBase", + proxy: torch.fx.Proxy, + options: dict[str, Any], + subclass_type: type | None, + target_cls: type[VTTypeAlias], +) -> VariableTracker: + import torch._functorch.vmap + import torch._subclasses.fake_tensor + import torch._utils + + if isinstance(example_value, torch.Tensor): + # Check if the result is a sparse tensor - + # We generally don't support sparse tensor so better to graph break here + if is_sparse_any(example_value) and ( + not tx.export or not config.capture_sparse_compute + ): + unimplemented( + gb_type="Attempted to wrap sparse Tensor with VariableTracker", + context=str(example_value), + explanation="torch.compile does not support sparse Tensors with VariableTracker", + hints=[*graph_break_hints.SPARSE_TENSOR], + ) + var = construct_tensor_variable( + target_cls, tx, proxy, example_value, subclass_type, options + ) + # NOTE: [Side effect tracking for newly constructed tensor] + # For newly constructed objects that have mutable attributes, we usually + # construct their VariableTracker via `track_object_new`, but since + # tensor variable construction is a bit different, we handle them + # specially here. This ensures that codegen will actually generate the + # attribute mutations on this tensor. + # + # NOTE we pass a dummy object as the `item` argument to avoid + # constructing a dummy _tensor_ object. The object isn't used for + # newly constructed VTs anyways. + assert isinstance(var, VariableTracker) + tx.output.side_effects._track_obj( + proxy, var, mutation_type_cls=AttributeMutationNew + ) + return var + elif ( + hasattr(proxy.node.target, "__name__") + and proxy.node.target.__name__ == "set_state" + # type: ignore[attr-defined] + and isinstance(proxy.node.target.__self__, torch._C.Generator) + or proxy.node.target is torch.random.set_rng_state + ): + assert type(proxy.node.target) is not str + # pyrefly: ignore[bad-argument-type] + return TorchInGraphFunctionVariable(proxy.node.target) + elif ( + proxy.node.target is torch._C._DisableFuncTorch + or proxy.node.target is torch.cuda._is_in_bad_fork + ): + return UserDefinedObjectVariable(example_value) + elif istype(example_value, torch.Size) and all( + isinstance(x, int) for x in example_value + ): + sizes = [ConstantVariable.create(x) for x in example_value] + return SizeVariable(sizes, **options) + elif isinstance(example_value, (tuple, list)): + set_example_value(proxy.node, example_value) + unpacked = [] + for i, val in enumerate(example_value): + if val is None: + # nn.MultiheadAttention() can return None, see issue #175 + unpacked.append( + ConstantVariable.create(None, **options), + ) + else: + proxy_i = proxy.tracer.create_proxy( + kind="call_function", + target=operator.getitem, + args=(proxy, i), + kwargs={}, + ) + + if "source" in options: + # This path should only trigger for list stealing, so it's + # safe to use `GetItemSource`. + assert isinstance(example_value, list) + source = options["source"] + options_i = options.copy() + options_i["source"] = GetItemSource( + base=source, + index=i, + index_is_slice=False, + ) + else: + # use the same options object as parent + options_i = options + + # WARNING: this assumes the same target_cls as this tuple/list call + unpacked.append( + # pyrefly: ignore [bad-argument-type] + wrap_fx_proxy_cls( + # pyrefly: ignore[bad-argument-type] + target_cls=target_cls, + tx=tx, + proxy=proxy_i, + example_value=val, + **options_i, + ) + ) + if isinstance(example_value, torch.Size): + # NB: Keep the old proxy around. See SizeVariable for an + # explanation why + return SizeVariable(unpacked, proxy, **options) + elif istype(example_value, tuple): + return TupleVariable(unpacked, **options) + elif istype(example_value, (list, immutable_list)): + return ListVariable(unpacked, **options) + else: + assert is_namedtuple(example_value), ( + f"expected namedtuple or structseq but got {type(example_value)}" + ) + tuple_vt = TupleVariable( + unpacked, + mutation_type=options.get("mutation_type", ValueMutationNew()), + ) + return UserDefinedTupleVariable.get_vt_cls(type(example_value))( + example_value, + tuple_vt=tuple_vt, + **options, # type: ignore[arg-type] + ) + elif example_value is None or proxy.node.target is torch.manual_seed: + return ConstantVariable.create(None, **options) + elif isinstance(example_value, (torch.SymInt, torch.SymFloat, torch.SymBool)): + tx.output.current_tracer.track_produced_symints(example_value, proxy) + set_example_value(proxy.node, example_value) + return SymNodeVariable.create(tx, proxy, example_value, **options) + elif ( + isinstance(example_value, torch.Stream) + and proxy.node.target is get_external_object_by_index + ) or proxy.node.target in [ + device_interface.current_stream + for _, device_interface in get_registered_device_interfaces() + ]: + set_example_value(proxy.node, example_value) + index = None + if proxy.node.target is get_external_object_by_index: + index = proxy.node.args[0] + # type: ignore[arg-type] + return StreamVariable(proxy, example_value, index, **options) + elif ( + isinstance(example_value, torch.Event) + and proxy.node.target is get_external_object_by_index + ) or proxy.node.target in [ + device_interface.current_stream + for _, device_interface in get_registered_device_interfaces() + ]: + index = None + if proxy.node.target is get_external_object_by_index: + index = proxy.node.args[0] + set_example_value(proxy.node, example_value) + # type: ignore[arg-type] + return EventVariable(proxy, example_value, index, **options) + elif ( + inspect.isclass(proxy.node.target) + and issubclass(proxy.node.target, torch.Event) + ) or proxy.node.target in [ + device_interface.Event + for _, device_interface in get_registered_device_interfaces() + ]: + set_example_value(proxy.node, example_value) + return EventVariable(proxy, example_value, None, **options) + elif proxy.node.target == "query" and proxy.node.op == "call_method": + set_example_value(proxy.node, example_value) + return ConstantVariable(example_value, **options) + elif ( + example_value is not None + and isinstance(example_value, torch.Event) + and proxy.node.target == "record_event" + and proxy.node.op == "call_method" + ): + set_example_value(proxy.node, example_value) + return EventVariable(proxy, example_value, None, **options) + elif isinstance(example_value, int) and ( + proxy.node.target + in [ + torch.sym_int, + torch.sym_max, + torch.sym_min, + getattr, + operator.getitem, + torch._utils._element_size, + torch.seed, + operator.mod, + torch._functorch.vmap._validate_and_get_batch_size, + torch._functorch.predispatch._vmap_increment_nesting, + torch._functorch.predispatch._vmap_decrement_nesting, + # some mac builds are missing torch.distributed.get_rank() + getattr(torch.distributed, "get_rank", _missing), + getattr(torch.distributed, "get_world_size", _missing), + # This always wants to be in the graph, even if the constraint + # results in a constant int + torch._constrain_as_size, + ] + or ( + # TODO: this is a little sus, because we didn't check what the self is + proxy.node.op == "call_method" and proxy.node.target == "bit_length" + ) + ): + set_example_value(proxy.node, example_value) + return ConstantVariable.create(example_value, **options) + elif isinstance(example_value, torch.backends.cuda.SDPAParams): + from .sdpa import SDPAParamsVariable + + set_example_value(proxy.node, example_value) + return SDPAParamsVariable(proxy, **options) + elif isinstance(example_value, bool) and ( + proxy.node.target + in [ + torch._C._are_functorch_transforms_active, + torch._C._functorch.is_batchedtensor, + torch.backends.cuda.is_flash_attention_available, + torch.backends.cuda.can_use_flash_attention, + torch.backends.cuda.can_use_efficient_attention, + torch._C._get_cudnn_sdp_enabled, + torch._C._get_flash_sdp_enabled, + torch._C._get_mem_efficient_sdp_enabled, + torch._C._get_math_sdp_enabled, + torch._C._get_overrideable_sdp_enabled, + torch._C._is_autocast_available, + "is_integer", + ] + + list(supported_const_comparison_op_values.keys()) + ): + set_example_value(proxy.node, example_value) + return ConstantVariable.create(example_value, **options) + elif isinstance(example_value, (int, float, bool)) and ( + proxy.node.target is call_torchbind + or proxy.node.target is flat_apply + or (proxy.node.op == "call_method" and proxy.node.target == "item") + ): + set_example_value(proxy.node, example_value) + return ConstantVariable.create(example_value, **options) + elif isinstance(example_value, float) or proxy.node.target in ["hex", "__round__"]: + set_example_value(proxy.node, example_value) + return ConstantVariable.create(example_value, **options) + elif isinstance(example_value, torch._library.fake_class_registry.FakeScriptObject): + # example_value is already a FakeScriptObject (e.g. returned by getitem + # on a container whose fake kernel returns a FakeScriptObject). No need + # to convert it — just wrap the proxy directly. + return TorchScriptObjectVariable.create( + proxy, + example_value, + ) + elif is_opaque_type(type(example_value)): + # This is for handling opaque objects in custom ops + if is_opaque_value_type(type(example_value)): + return TorchScriptObjectVariable.create( + example_value, # pyrefly: ignore[bad-argument-type] + example_value, + ) + fake_script_obj = torch._library.fake_class_registry.maybe_to_fake_obj( + tx.output.fake_mode, example_value + ) + return TorchScriptObjectVariable.create( + proxy, + fake_script_obj, + ) + else: + unimplemented( + gb_type="torch.* op returned non-Tensor", + context=f"example_value type: {typestr(example_value)}; op: {proxy.node.op}; target: {proxy.node.target}", + explanation="torch.* ops that return a non-Tensor cannot be traced into the Dynamo FX graph output", + hints=[], + ) + + +def infer_subclass_type(value: T) -> type[T] | None: + if type(value) in ( + torch.Tensor, + torch.nn.Parameter, + torch._subclasses.fake_tensor.FakeTensor, + torch._subclasses.functional_tensor.FunctionalTensor, + ) or is_traceable_wrapper_subclass(value): + # Ordinarily, we would fakeify a tensor so that it can get dynamic + # shapes and be computed on without triggering actual operations. + # However, how can we fakeify a tensor subclass? Ordinary + # inheritance (nor multiple inheritance) won't work work. + # + # Instead, our plan is to *manually simulate* the tensor subclass + # inheriting from a fake tensor with dynamo. This means our + # data representation for a tensor subclass will be a fake tensor + # + tensor subclass type + any extra data the subclass may have + # been storing on the tensor. Because all Python accesses are + # mediated through TensorWithTFOverrideVariable, we can ensure + # that we dispatch differently, e.g., according to + # __torch_function__ + # + # To simplify things for now, the __dict__ tracking bits haven't + # been implemented yet, but they can be added into this design at + # a later point in time. + return None + else: + return type(value) + + +def get_specialized_props( + target_cls: Any, + tx: "InstructionTranslatorBase", + example_value: Any, + subclass_type: type | None, +) -> dict[str, Any]: + specialized_props = target_cls.specialize(example_value) + # TODO: not sure about this fake mode test + if ( + isinstance(example_value, torch._subclasses.fake_tensor.FakeTensor) + and example_value.fake_mode is tx.fake_mode + ): + if subclass_type: + tensor_type = subclass_type + elif isinstance(example_value, torch.nn.Parameter): + tensor_type = torch.nn.Parameter + elif isinstance(example_value, torch.nn.Buffer): + tensor_type = torch.nn.Buffer + else: + tensor_type = torch.Tensor + specialized_props["class_type"] = tensor_type + + return specialized_props + + +def construct_tensor_variable( + target_cls: type[VTTypeAlias], + tx: "InstructionTranslatorBase", + proxy: torch.fx.Proxy, + example_value: Any, + subclass_type: type | None, + options: dict[str, Any], +) -> VTTypeAlias: + """ + Actually construct a tensor variable after all the pre-processing from + wrapping a pre-existing or newly created tensor value. + """ + # NB: In most (all?) cases, this does not actually do a clone. + # (WARNING: this means that if we mutate metadata on the fake + # tensor, the stored example value will update too!) + example_value = _clone_input(example_value, tx.fake_mode) + set_example_value(proxy.node, example_value) + # We bind the unbacked symints in sizes/trdies of tensor lazily. + # So that subgraphs can access the unbacked symbol's proxy in parent graph + # when lifting unbacked symbols of input tensors to subgraph inputs. + # We do it lazily because the tensor may not be used in subgraphs. + if proxy.node.op != "placeholder": + tx.output.current_tracer.track_produced_symints(example_value, proxy) + options.update(get_specialized_props(target_cls, tx, example_value, subclass_type)) + # pyrefly: ignore [bad-argument-count] + return target_cls(proxy, **options) + + +def get_automatic_dynamic_shapes_mark_as() -> DimDynamic: + if config.automatic_dynamic_shapes_mark_as == "dynamic": + return DimDynamic.DYNAMIC + elif config.automatic_dynamic_shapes_mark_as == "unbacked": + return DimDynamic.UNBACKED + else: + raise ValueError( + f"invalid automatic_dynamic_shapes_mark_as = {config.automatic_dynamic_shapes_mark_as}" + ) + + +_DYNAMIC_SOURCES: set[str] | None = None +_DYNAMIC_SOURCES_CONFIG_HASH: int | None = None + + +def get_dynamic_sources() -> set[str]: + global _DYNAMIC_SOURCES, _DYNAMIC_SOURCES_CONFIG_HASH + + current_hash = hash(torch.compiler.config.dynamic_sources) + + # If we have already calculated the sources and the config hasn't changed, return cached result + if _DYNAMIC_SOURCES is not None and _DYNAMIC_SOURCES_CONFIG_HASH == current_hash: + return _DYNAMIC_SOURCES + + # Config has changed or first time, (re)calculate the sources + _DYNAMIC_SOURCES = { + s + for s in torch.compiler.config.dynamic_sources.replace(" ", "").split(",") + if s + } + _DYNAMIC_SOURCES_CONFIG_HASH = current_hash + + return _DYNAMIC_SOURCES + + +def is_dynamic_source(source_name: str) -> bool: + dynamic_sources = get_dynamic_sources() + for pattern in dynamic_sources: + if pattern == source_name or re.match(pattern, source_name): + log.debug( + "%s was marked dynamic due to dynamic source allowlist pattern: %s", + source_name, + pattern, + ) + return True + return False + + +def record_automatic_dynamic( + tx: "InstructionTranslatorBase", name: str, e: torch.Tensor +) -> FrameStateSizeEntry: + # This mimics stride inference algorithm in _create_symbolic_sizes_strides_storage_offset + ex_size = e.size() + if not is_sparse_any(e): + ex_stride = e.stride() + dim = e.dim() + + stride = [None] * dim + pending = [(ex_stride[i], -i) for i in range(dim)] + pending.sort(key=_nested_int_aware_sort) + candidates = {} + for i_stride, neg_i in pending: + i = -neg_i + # pyrefly: ignore [unsupported-operation] + stride[i] = candidates.get(i_stride, i_stride) + # pyrefly: ignore [no-matching-overload] + candidates.setdefault(i_stride * ex_size[i], InferStride(i)) + else: + # pyrefly: ignore [implicit-any] + stride = [] + + return process_automatic_dynamic( + # type: ignore[arg-type]ks + tx, + name, + # type: ignore[arg-type] + FrameStateSizeEntry.make_tensor(tuple(ex_size), tuple(stride)), + ) + + +_UNBACKED_SOURCES: set[str] | None = None +_UNBACKED_SOURCES_CONFIG_HASH: int | None = None + + +def get_unbacked_sources() -> set[str]: + global _UNBACKED_SOURCES, _UNBACKED_SOURCES_CONFIG_HASH + + current_hash = hash(torch.compiler.config.unbacked_sources) + + # If we have already calculated the sources and the config hasn't changed, return cached result + if _UNBACKED_SOURCES is not None and _UNBACKED_SOURCES_CONFIG_HASH == current_hash: + return _UNBACKED_SOURCES + + # Config has changed or first time, (re)calculate the sources + _UNBACKED_SOURCES = { + s + for s in torch.compiler.config.unbacked_sources.replace(" ", "").split(",") + if s + } + _UNBACKED_SOURCES_CONFIG_HASH = current_hash + + return _UNBACKED_SOURCES + + +def is_unbacked_source(source_name: str) -> bool: + unbacked_sources = get_unbacked_sources() + for pattern in unbacked_sources: + if pattern == source_name or re.match(pattern, source_name): + log.debug( + "%s was marked unbacked due to unbacked source allowlist pattern: %s", + source_name, + pattern, + ) + return True + return False + + +# Performs automatic dynamic dim determination. +# Returns a SymbolicContext +def _automatic_dynamic( + e: Any, + tx: "InstructionTranslatorBase", + source: Source, + static_shapes: bool, + outer_only: bool = False, +) -> SymbolicContext: + # strided NT not supported + if e.is_nested and not isinstance( + e, torch.nested._internal.nested_tensor.NestedTensor + ): + unimplemented( + gb_type="Encountered strided NestedTensor in automatic dynamic dim determination", + context="", + explanation="torch.compile does not support strided NestedTensor", + hints=[], + ) + + name = source.name + prior_policy = tx.output.tracing_context.tensor_to_context.get(e, None) + shape_env_to_source_to_symbol_cache = ( + prior_policy.shape_env_to_source_to_symbol_cache if prior_policy else {} + ) + + # Get base context if the tensor is a view + view_base_context: SymbolicContext | None = None + if e._is_view(): + base_source = AttrSource(source, "_base") + view_base_context = _automatic_dynamic(e._base, tx, base_source, static_shapes) + + if is_traceable_wrapper_subclass(e) and not outer_only: + # Get symbolic context for outer tensor + outer_context = _automatic_dynamic( + e, tx, source, static_shapes, outer_only=True + ) + assert isinstance(outer_context, StatefulSymbolicContext) + + # Get symbolic contexts for inner tensors + inner_contexts = {} # mapping from attr -> symbolic context + attrs, _ = type(e).__tensor_flatten__(e) + for attr in attrs: + match getattr(e, attr): + case torch.Tensor() as inner_value: + inner_source = AttrSource(source, attr) + inner_contexts[attr] = _automatic_dynamic( + inner_value, tx, inner_source, static_shapes + ) + case OpaqueBase(): + pass + case unexpected: + raise AssertionError( + f"expected Tensor or OpaqueBase, got {type(unexpected)}" + ) + + return SubclassSymbolicContext( + dynamic_sizes=outer_context.dynamic_sizes, + dynamic_strides=outer_context.dynamic_strides, + constraint_sizes=outer_context.constraint_sizes, + constraint_strides=outer_context.constraint_strides, + view_base_context=view_base_context, + tensor_source=outer_context.tensor_source, + shape_env_to_source_to_symbol_cache=outer_context.shape_env_to_source_to_symbol_cache, + inner_contexts=inner_contexts, + ) + + if static_shapes and not is_dynamic_source(name): + return StatefulSymbolicContext( + dynamic_sizes=[DimDynamic.STATIC] * e.dim(), + dynamic_strides=[DimDynamic.INFER_STRIDE] * e.dim(), + constraint_sizes=[None] * e.dim(), + constraint_strides=[None] * e.dim(), + view_base_context=view_base_context, + tensor_source=source, + shape_env_to_source_to_symbol_cache=shape_env_to_source_to_symbol_cache, + ) + + # We preserve the dynamism of inputs. For example, when users call + # make_fx(torch.cond, tracing_mode="symbolic")(*args), inputs have SymInt sizes. + from torch.fx.experimental.symbolic_shapes import is_nested_int + + if any(isinstance(s, SymInt) and not is_nested_int(s) for s in e.size()): + return StatefulSymbolicContext( + dynamic_sizes=[ + DimDynamic.DYNAMIC if isinstance(s, SymInt) else DimDynamic.STATIC + for s in e.size() + ], + dynamic_strides=[DimDynamic.INFER_STRIDE] * e.dim(), + constraint_sizes=[None] * e.dim(), + constraint_strides=[None] * e.dim(), + view_base_context=view_base_context, + tensor_source=source, + shape_env_to_source_to_symbol_cache=shape_env_to_source_to_symbol_cache, + ) + + # Prep for automatic dynamic + frame_state_entry = record_automatic_dynamic(tx, name, e) + + # TODO: index export_constraints ahead of time so we don't have to + # do a linear scan every time here + t_id = id(e) + # pyrefly: ignore [implicit-any] + dim2constraint = {} + + def update_dim2constraint( + dim: int, constraint_range: "StrictMinMaxConstraint", name: str + ) -> None: + if dim in dim2constraint: + from torch.fx.experimental.symbolic_shapes import StrictMinMaxConstraint + + old_constraint_range, old_name = dim2constraint[dim] + new_constraint_range = StrictMinMaxConstraint( + vr=constraint_range.vr & old_constraint_range.vr, + warn_only=False, + ) + # It is possible for (non-None) old_name and name to be different + # but this will only happen the corresponding Dims can be derived equal. + new_name = old_name or name + dim2constraint[dim] = new_constraint_range, new_name + else: + dim2constraint[dim] = constraint_range, name + + from torch.export.dynamic_shapes import _RelaxedConstraint + + if tx.output.export_constraints is not None: + # type: ignore[iterable] + for constraint in tx.output.export_constraints: + if isinstance(constraint, _RelaxedConstraint): + continue + if constraint.t_id == t_id: + update_dim2constraint( + constraint.dim, constraint.constraint_range, constraint.name + ) + + dynamic_sizes = [] + dynamic_strides = [] + constraint_sizes = [] + constraint_strides = [] + specialize_on = [] + for i in range(e.dim()): + # NB: mark dynamic has precedence over static + marked_strict_unbacked = i in getattr( + e, "_dynamo_strict_unbacked_indices", set() + ) + marked_unbacked = i in getattr(e, "_dynamo_unbacked_indices", set()) + marked_dynamic = i in getattr(e, "_dynamo_dynamic_indices", set()) + marked_weak_dynamic = i in getattr(e, "_dynamo_weak_dynamic_indices", set()) + marked_static = i in getattr(e, "_dynamo_static_indices", set()) + + specialize_on.append(getattr(e, "_specialize_on", {}).get(i, [])) + + # Reflect the user directive in the frame_state + # For dynamic, apply None always + + normalized_source_name = normalize_source_name(source.name) + base_source = source + if isinstance(base_source, ChainedSource): + base_source = base_source.get_base() + + if marked_dynamic or ( + isinstance(base_source, LocalSource) + and base_source.dynamism is not None + # pyrefly: ignore[no-matching-overload] + and dict(base_source.dynamism).get(normalized_source_name, {i: False})[i] + ): + # TODO: This can be batched + # TODO: Doing this here is kind of sus, maybe better to set this + # up when we initially created the FrameStateSizeEntry to bong + # into the mutable state + log.debug("automatic dynamic %s marked dynamic", name) + mark_size = [auto_unset] * e.dim() + # pyrefly: ignore [unsupported-operation] + mark_size[i] = auto_dynamic + # pyrefly: ignore [bad-argument-type] + frame_state_entry |= FrameStateSizeEntry.make_size(size=mark_size) + + # NB: both static and dynamic have precedence over + automatic_dynamic_size = ( + config.automatic_dynamic_shapes and frame_state_entry.is_size_dynamic(i) + ) + # NB: previously, if size was dynamic, we wouldn't make its stride + # dynamic. But now, because of InferStride concept, we will properly + # not make stride dynamic even if it's wobbling + automatic_dynamic_stride = ( + config.automatic_dynamic_shapes and frame_state_entry.is_stride_dynamic(i) + ) + + if is_dynamic_source(name): + log.debug("%s marked dynamic via source whitelist", name) + automatic_dynamic_size = True + + if is_unbacked_source(name): + log.debug("%s marked unbacked via source whitelist", name) + automatic_dynamic_size = True + + automatic_dynamic = automatic_dynamic_size or automatic_dynamic_stride + + # We will process constraints first, as they will imply that we + # have a dynamic dimension + # Precedence: export constraints > eager constraints + constraint = dim2constraint.get(i) + if constraint is None: + constraint_size = None + constraint_stride = None + if marked_dynamic and not config.allow_ignore_mark_dynamic: + # constraint_stride is deliberaly kept None because no easy way to provide value ranges for mark dynamic + constraint_stride = None + if hasattr(e, "_dynamo_dynamic_range"): + dim_range = [ + dr for dr in e._dynamo_dynamic_range if dr.dim == i + ].pop() + if dim_range.min is None and dim_range.max is None: + constraint_size = RelaxedUnspecConstraint(warn_only=False) + else: + from torch.fx.experimental.symbolic_shapes import ( + StrictMinMaxConstraint, + ) + + constraint_size = StrictMinMaxConstraint( + vr=ValueRanges(lower=dim_range.min, upper=dim_range.max), + warn_only=False, + ) + else: + constraint_size = RelaxedUnspecConstraint(warn_only=False) + elif marked_strict_unbacked: + constraint_size = RelaxedUnspecConstraint(warn_only=False) + elif not marked_static and automatic_dynamic: + set_feature_use("dynamo.automatic_dynamic_shapes", True) + if automatic_dynamic_size: + constraint_size = RelaxedUnspecConstraint(warn_only=True) + if automatic_dynamic_stride: + constraint_stride = RelaxedUnspecConstraint(warn_only=True) + else: + if not marked_static and not config.automatic_dynamic_shapes: + set_feature_use("dynamo.automatic_dynamic_shapes", False) + constraint_size = None + constraint_stride = None + else: + constraint_size, name_ = constraint + constraint_stride = None + dim_name = f"{name}.size()[{i}]" + tx.output.shape_env.source_name_to_debug_name[dim_name] = name_ + constraint_sizes.append(constraint_size) + constraint_strides.append(constraint_stride) + + if marked_unbacked or is_unbacked_source(name): + dynamic_size = DimDynamic.UNBACKED + elif ( + constraint_size is not None + or marked_dynamic + or marked_weak_dynamic + or is_nested_int(e.size()[i]) + ): + # NB: We could assert static_shapes is False here, but it + # seems better to allow the user to override symbolic_context in this + # case + if automatic_dynamic: + dynamic_size = get_automatic_dynamic_shapes_mark_as() + else: + dynamic_size = DimDynamic.DYNAMIC + elif static_shapes or config.assume_static_by_default or marked_static: + dynamic_size = DimDynamic.STATIC + else: + # TODO: When does this show up? + dynamic_size = DimDynamic.DUCK + + if constraint_stride is not None: + dynamic_stride = DimDynamic.DYNAMIC + else: + dynamic_stride = DimDynamic.INFER_STRIDE + + dynamic_sizes.append(dynamic_size) + dynamic_strides.append(dynamic_stride) + + return StatefulSymbolicContext( + dynamic_sizes=dynamic_sizes, + dynamic_strides=dynamic_strides, + constraint_sizes=constraint_sizes, + # pyrefly: ignore [bad-argument-type] + constraint_strides=constraint_strides, + specialize_on=specialize_on, + view_base_context=view_base_context, + tensor_source=source, + shape_env_to_source_to_symbol_cache=shape_env_to_source_to_symbol_cache, + shape_ids=getattr(e, "_dynamo_shape_ids", None), + unbacked_bounds=getattr(e, "_dynamo_unbacked_bounds", None), + excluded_sizes=frame_state_entry.excluded_sizes, + ) + + +# See note [Tensor Fakification and Symbol Caching] +def wrap_to_fake_tensor_and_record( + e: Any, + tx: "InstructionTranslatorBase", + *, + source: Source | None, + is_tensor: bool, + parent_context: Any | None = None, +) -> Any: + _t0 = time.time_ns() + try: + return _wrap_to_fake_tensor_and_record_impl( + e, tx, source=source, is_tensor=is_tensor, parent_context=parent_context + ) + finally: + tx.output.bytecode_tracing_timings.wrap_to_fake_tensor_and_record_ns += ( + time.time_ns() - _t0 + ) + + +def _wrap_to_fake_tensor_and_record_impl( + e: Any, + tx: "InstructionTranslatorBase", + *, + source: Source | None, + is_tensor: bool, + parent_context: Any | None = None, +) -> Any: + if ( + type(e) in (torch.Tensor, torch.nn.Parameter, FakeTensor) + or isinstance(e, torch.Tensor) + or is_traceable_wrapper_subclass(e) + ): + assert source is not None + static_shapes, _reason = tensor_always_has_static_shape( + e, + is_tensor, + tensor_source=source, + ) + + if not parent_context: + symbolic_context = _automatic_dynamic(e, tx, source, static_shapes) + else: + # Parent contexts are passed in when we are recursively creating + # fake tensors for subclasses. A better design would be not to create a + # parent/child relationship, but to recursively call _automatic_dynamic + # as we recursively call wrap_to_fake_tensor_and_record. This runs + # into bugs around how meta_utils knows and works to create fake tensors + # with tensor subclasses. Ideally, dynamo would drive both the recursive + # wrap_to_fake_tensor_and_record and _automatic_dynamic policy creation. + assert isinstance(source, AttrSource) + inner_context_name = source.member + symbolic_context = parent_context.inner_contexts[inner_context_name] + + log.debug( + "wrap_to_fake %s %s %s %s", + source.name, + tuple(e.shape), + symbolic_context, + type(e), + ) + + # Note [enable_python_dispatcher in dynamo] + # Dynamo disables itself when it runs fake tensor prop, which means that tensor subclasses + # have no way to know (purely based off of global state) if they are currently being run under compile or not. + # we use enable_python_dispatcher mainly to tweak the DispatchKeyState so that subclass authors + # can check it to know if they are running in an eager context or not + with enable_python_dispatcher(): + assert tx.fake_mode is not None + fake_e = wrap_fake_exception( + lambda: tx.fake_mode.from_tensor( + e, # type: ignore[arg-type] + source=source, + symbolic_context=symbolic_context, + ) + ) + if ( + source is not None + and isinstance(fake_e, FakeTensor) + and (sym_val := fake_e.item_memo) is not None + ): + # Match the peephole in FakeTensorConverter.from_real_tensor that + # strips FloatTensorSource before calling create_symbol. Without + # this, the tracked fake source name won't match source_to_var and + # produce_guards_verbose will report "(unknown source)". + if isinstance(source, FloatTensorSource): + item_source = source.base + else: + item_source = CallMethodItemSource(source) + tx.output.tracked_fakes.append( + TrackedFake(sym_val, item_source, symbolic_context) + ) + + if is_traceable_wrapper_subclass(fake_e): + attrs, _ = fake_e.__tensor_flatten__() + for attr in attrs: + fake_inner = getattr(fake_e, attr) + inner = getattr(e, attr) + inner_source = AttrSource(source, attr) + wrap_to_fake_tensor_and_record( + inner, + tx, + source=inner_source, + is_tensor=isinstance(fake_inner, torch.Tensor), + parent_context=symbolic_context, + ) + + tx.output.tracing_context.tensor_to_context[e] = symbolic_context + if is_sparse_any(fake_e): + # TODO: for TensorGuards, this eventually may need more + # fields for the size/stride of any other constituents + values = fake_e._values() if fake_e.is_sparse else fake_e.values() + tx.output.input_source_to_sizes_strides[source] = { + "size": fake_e.size(), + # TODO: revise this, but for now this stride instead of () + # avoids SegFault with PYTORCH_TEST_WITH_DYNAMO=1 + "stride": (1,) * fake_e.ndim, + "values_size": values.size(), + "values_stride": values.stride(), + } + else: + tx.output.input_source_to_sizes_strides[source] = { + "size": fake_e.size(), + "stride": fake_e.stride(), + } + + if ( + is_tensor + and not (static_shapes and source.is_specialized_nn_module()) + and not is_constant_source(source) + ): + tx.output.tracked_fakes.append( + TrackedFake(fake_e, source, symbolic_context) + ) + tx.output.tracked_fakes_id_to_source[id(e)].append(source) + + return fake_e + else: + return e + + +class SourcelessBuilder: + """ + Like builder, but stateless and does not require a source. Useful for simple type->VT objects, or objects + that are being created/evaporated during inlining (ex: consider a locally made list of tensors we then iterate over + .), such a list should not show up as an artifact from inputs, nor in reconstruction, nor in the graph. However, + there may be reasons to represent it as a ListVariable internally. + + NOTE - Objects produced here are born UNGUARDED due to the nature of sources! + + NOTE - This class is very new! It will have some rough edges, but it was created to stem the bleeding of giant + if/else type->VariableTracker trees that were cropping up all over dynamo. + """ + + def __init__(self) -> None: + raise AssertionError("Use SourcelessBuilder.create()") + + @overload + @staticmethod + def create( + tx: "InstructionTranslatorBase", + value: type[set[Any]] + | type[dict[Any, Any]] + | type[tuple[Any, ...]] + | type[list[Any]], + ) -> BuiltinVariable: ... + + @overload + @staticmethod + def create(tx: "InstructionTranslatorBase", value: list[Any]) -> ListVariable: ... + + @overload + @staticmethod + def create( + tx: "InstructionTranslatorBase", value: tuple[Any, ...] + ) -> TupleVariable: ... + + @overload + @staticmethod + def create( + tx: "InstructionTranslatorBase", value: bool | int | float | str + ) -> ConstantVariable: ... + + @overload + @staticmethod + def create(tx: "InstructionTranslatorBase", value: Any) -> VariableTracker: ... + + @staticmethod + def create(tx: "InstructionTranslatorBase", value: Any) -> VariableTracker: + value_type = type(value) + # type: ignore[attr-defined] + fast_handler = SourcelessBuilder._type_handlers.get(value_type) + if fast_handler: + return fast_handler(tx, value) + + if isinstance(value, VariableTracker): + # This is always valid to call, and useful for recursive calls. + return value + elif is_opaque_value_type(type(value)) and not isinstance(value, enum.Enum): + return TorchScriptObjectVariable.create(value, value) + elif is_opaque_reference_type(type(value)): + # This is for handling opaque objects in custom ops + fake_script_obj = torch._library.fake_class_registry.maybe_to_fake_obj( + tx.output.fake_mode, value + ) + return TorchScriptObjectVariable.create( + value, # pyrefly: ignore[bad-argument-type] + fake_script_obj, + ) + # type: ignore[attr-defined] + elif isinstance(value, dataclasses._HAS_DEFAULT_FACTORY_CLASS): + return UserDefinedObjectVariable(value) + elif ConstantVariable.is_literal(value): + return ConstantVariable.create(value) + elif callable(value) and trace_rules.lookup_callable(value) is not None: + if trace_rules.is_callable_allowed(value): + tx.output.has_user_defined_allowed_in_graph = True + # pyrefly: ignore[not-callable, bad-argument-count] + return trace_rules.lookup_callable(value)(value) + elif callable(value) and UserDefinedClassVariable.is_supported_new_method( + value + ): + # NamedTuple._make uses an alias of tuple.__new__ + # pyrefly: ignore[not-callable, bad-argument-count, missing-attribute] + obj = trace_rules.lookup_callable(value.__self__)(value.__self__) + return GetAttrVariable(obj, "__new__", py_type=type(value)) + elif is_function_or_wrapper(value): + # pyrefly: ignore[not-callable, bad-argument-count] + return trace_rules.lookup(value)(value) + elif isinstance( + value, + (enum.Enum, torch.DispatchKey, torch._C._functorch.TransformType), + ): + return UserDefinedObjectVariable(value) + elif isinstance(value, (type, abc.ABCMeta)): + if issubclass(type(value), type) and issubclass(value, BaseException): + return UserDefinedExceptionClassVariable(value) + return UserDefinedClassVariable(value) + elif isinstance(value, types.MethodWrapperType): + return MethodWrapperVariable(value) + elif isinstance(value, types.MethodType): + if isinstance(value.__self__, (type, abc.ABCMeta)): + # value is a classmethod + assert getattr(value.__self__, value.__func__.__name__) == value + cls_obj_vt = SourcelessBuilder.create(tx, value.__self__) + try: + # pyrefly: ignore[bad-argument-type] + return cls_obj_vt.var_getattr(tx, value.__func__.__name__) + except NotImplementedError: + pass # failthrough to unimplemented branch + else: + # Instance method — look up the VT for __self__ via side effects + obj_vt = tx.output.side_effects.id_to_variable.get(id(value.__self__)) + if obj_vt is not None: + return torch._dynamo.variables.UserMethodVariable( + value.__func__, obj_vt + ) + elif isinstance(value, torch.fx.graph_module.GraphModule): + return SourcelessGraphModuleVariable(value) + elif isinstance(value, torch.utils._pytree.TreeSpec): + return UserDefinedObjectVariable(value) + elif isinstance(value, re.Pattern): + return ConstantLikeVariable(value) + elif isinstance(value, torch._dynamo.variables.lazy.LazySymNodeFormatString): + try: + return ConstantVariable.create(str(value)) + # If we cannot create due to error in str() call, we should + # try explicitly for string format variable + except ( + torch._dynamo.exc.UserError, + torch.fx.experimental.symbolic_shapes.GuardOnDataDependentSymNode, + ): + return StringFormatVariable.create( + value.fmt_var.as_python_constant(), + [value.sym_node_var], + {}, + ) + elif isinstance(value, type(torch._higher_order_ops.flex_attention_backward)): + return torch._dynamo.variables.higher_order_ops.FlexAttentionBackwardHighOrderVariable( + value + ) + elif isinstance(value, (types.GenericAlias, types.UnionType)): + return TypingVariable(value) + elif is_namedtuple(value): + output = [ + SourcelessBuilder.create(tx, getattr(value, name)) + for name in namedtuple_fields(type(value)) + ] + tuple_vt = TupleVariable(output, mutation_type=ValueMutationNew()) + return UserDefinedTupleVariable.get_vt_cls(type(value))( + value, tuple_vt=tuple_vt + ) + elif ( + isinstance(value, torch.SymInt) + and value.node.expr in tx.output.bound_symbols + ): + proxy = tx.output.bound_symbols[value.node.expr] + return SymNodeVariable.create(tx, proxy) + elif isinstance(value, slice): + items = [ + SourcelessBuilder.create(tx, getattr(value, k)) + for k in ("start", "stop", "step") + ] + return SliceVariable(items, tx) # pyrefly: ignore[bad-argument-type] + elif istype(value, object): + return ObjectVariable(value) + unimplemented( + gb_type="Unexpected type in sourceless builder", + context=f"{value_type.__module__}.{value_type.__qualname__}", + explanation=f"SourcelessBuilder.create does not know how to wrap {value_type}", + hints=[*graph_break_hints.DYNAMO_BUG], + ) + + @staticmethod + def wrap_constant_literal(value: object) -> VariableTracker: + assert ConstantVariable.is_literal(value) + return ConstantVariable.create(value=value) + + @staticmethod + def make_type_handlers() -> dict[ + type, Callable[["InstructionTranslator", Any], VariableTracker] + ]: + create = SourcelessBuilder.create + handlers: dict[ + type, Callable[[InstructionTranslator, Any], VariableTracker] + ] = {} + for t in common_constant_types: + handlers[t] = lambda tx, value: ConstantVariable(value) + handlers[set] = lambda tx, value: SetVariable( + [create(tx, x) for x in value], mutation_type=ValueMutationNew() + ) + handlers[OrderedSet] = lambda tx, value: OrderedSetVariable( + [create(tx, x) for x in value], mutation_type=ValueMutationNew() + ) + handlers[dict] = lambda tx, value: ConstDictVariable( + {create(tx, k): create(tx, v) for k, v in value.items()}, + type(value), + mutation_type=ValueMutationNew(), + ) + handlers[list] = lambda tx, value: ListVariable( + [create(tx, x) for x in value], mutation_type=ValueMutationNew() + ) + handlers[tuple] = lambda tx, value: TupleVariable( + [create(tx, x) for x in value] + ) + handlers[torch.Size] = lambda tx, value: SizeVariable( + [create(tx, x) for x in value] + ) + handlers[collections.OrderedDict] = handlers[dict] + handlers[immutable_dict] = handlers[dict] + handlers[immutable_list] = handlers[list] + # Sourceless MappingProxyType object can be encountered while tracing + # type.__dict__["__dict__"].__get__ + handlers[types.MappingProxyType] = lambda tx, value: MappingProxyVariable( + ConstDictVariable( + {create(tx, k): create(tx, v) for k, v in value.items()}, + dict, + mutation_type=ValueMutationNew(), + ), + ) + handlers[types.GetSetDescriptorType] = ( + lambda tx, value: GetSetDescriptorVariable(value) + ) + handlers[inspect.Parameter] = lambda tx, value: UserDefinedObjectVariable( + value, mutation_type=ValueMutationNew() + ) + handlers[random.Random] = lambda tx, value: RandomClassVariable() + handlers[types.ModuleType] = lambda tx, value: PythonModuleVariable(value) + + handlers[torch.DispatchKeySet] = lambda tx, value: DispatchKeySetVariable( + value, mutation_type=ValueMutationNew() + ) + handlers[torch._functorch.pyfunctorch.FuncTorchInterpreter] = ( + lambda tx, value: FuncTorchInterpreterVariable( + value, mutation_type=ValueMutationNew() + ) + ) + + handlers[torch.distributions.constraints._Real] = ( + lambda tx, value: UserDefinedObjectVariable( + value, mutation_type=ValueMutationNew() + ) + ) + handlers[torch.distributions.constraints._Interval] = ( + lambda tx, value: UserDefinedObjectVariable( + value, mutation_type=ValueMutationNew() + ) + ) + handlers[torch.distributions.constraints.Constraint] = ( + lambda tx, value: UserDefinedObjectVariable( + value, mutation_type=ValueMutationNew() + ) + ) + + def passthrough(tx: "InstructionTranslator", value: T) -> T: + return value + + for cls in VariableTrackerMeta.all_subclasses: + handlers[cls] = passthrough + return handlers + + +SourcelessBuilder._type_handlers = SourcelessBuilder.make_type_handlers() + + +class SourcelessUserDefinedObjectBuilder: + """ + SourceLessBuilder does not return a UserDefinedObjectVariable, but in some + cases it might be ok to return UserDefinedObjects. In such case, use this + builder. + """ + + def __init__(self) -> None: + raise AssertionError("Use SourcelessUserDefinedObjectBuilder.create()") + + @staticmethod + def create(tx: "InstructionTranslator", value: Any) -> VariableTracker: + value_type = type(value) + if issubclass(value_type, MutableMapping): + return MutableMappingVariable(value, mutation_type=ValueMutationNew()) + elif isinstance(value, torch.nn.Module): + return UnspecializedNNModuleVariable( + value, mutation_type=ValueMutationNew() + ) + else: + return UserDefinedObjectVariable(value, mutation_type=ValueMutationNew()) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/builtin.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/builtin.py new file mode 100644 index 0000000000000000000000000000000000000000..37526143ad46c19c798ae8d3fa247affb43d983c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/builtin.py @@ -0,0 +1,3504 @@ +""" +Built-in function and type variable tracking for TorchDynamo's symbolic execution. + +This module contains variable tracker classes for Python built-in functions, types, +and operations during graph compilation. It handles symbolic execution of: + +- Built-in functions (len, getattr, isinstance, etc.) +- Type constructors (int, float, str, list, dict, etc.) +- Built-in operators and methods +- Special Python constructs (super, hasattr, etc.) + +Key classes: +- BuiltinVariable: Tracks built-in functions and handles their execution +- TypeVariable: Manages type constructor calls and type checking +- SuperVariable: Handles super() calls in class hierarchies + +These variable trackers ensure that built-in Python operations are correctly +handled during symbolic execution, either by executing them directly when safe +or by creating appropriate graph nodes when needed. +""" + +import contextlib +import functools +import inspect +import itertools +import logging +import math +import operator +import sys +import types +import typing +import unittest +from collections import defaultdict, OrderedDict +from collections.abc import Callable, Iterable, KeysView, Sequence +from typing import Any, cast, TYPE_CHECKING + +import torch +from torch._subclasses.meta_utils import is_sparse_any +from torch.overrides import BaseTorchFunctionMode +from torch.utils._python_dispatch import is_traceable_wrapper_subclass + +from .. import config, graph_break_hints, polyfills, variables +from ..exc import ( + ObservedAttributeError, + ObservedUserStopIteration, + raise_observed_exception, + unimplemented, + Unsupported, + UserError, + UserErrorType, +) +from ..guards import GuardBuilder, install_guard +from ..replay_record import DummyModule +from ..source import ( + AttrSource, + GetItemSource, + GlobalSource, + is_constant_source, + Source, + TypeSource, +) +from ..utils import ( + check_constant_args, + check_numpy_ndarray_args, + check_unspec_or_constant_args, + check_unspec_python_args, + cmp_name_to_op_mapping, + dict_methods, + extract_fake_example_value, + frozenset_methods, + get_fake_value, + guard_if_dyn, + is_tensor_getset_descriptor, + is_wrapper_or_member_descriptor, + istype, + numpy_operator_wrapper, + proxy_args_kwargs, + raise_args_mismatch, + set_methods, + specialize_symnode, + str_methods, + tensortype_to_dtype, +) +from .base import ( + AsPythonConstantNotImplementedError, + NO_SUCH_SUBOBJ, + ValueMutationNew, + VariableTracker, +) +from .constant import ConstantVariable, FakeIdVariable +from .dicts import ( + ConstDictVariable, + DefaultDictVariable, + DictItemsVariable, + DictKeysVariable, + DictViewVariable, +) +from .hashable import is_hashable +from .lists import ( + BaseListVariable, + ListIteratorVariable, + ListVariable, + RangeVariable, + SizeVariable, + TupleIteratorVariable, + TupleVariable, +) +from .misc import NullVariable +from .sets import FrozensetVariable, OrderedSetClassVariable, SetVariable +from .tensor import ( + FakeItemVariable, + supported_comparison_ops, + SymNodeVariable, + TensorVariable, + UnspecializedPythonVariable, +) +from .user_defined import ( + MutableMappingVariable, + UserDefinedDictVariable, + UserDefinedObjectVariable, + UserDefinedVariable, +) + + +if TYPE_CHECKING: + # Cyclic dependency... + from torch._dynamo.codegen import PyCodegen + from torch._dynamo.symbolic_convert import InstructionTranslator + +log = logging.getLogger(__name__) + + +IN_PLACE_DESUGARING_MAP = { + operator.iadd: operator.add, + operator.isub: operator.sub, + operator.imul: operator.mul, + operator.ifloordiv: operator.floordiv, + operator.itruediv: operator.truediv, + operator.imod: operator.mod, + operator.imatmul: operator.imatmul, + operator.ilshift: operator.lshift, + operator.irshift: operator.rshift, + operator.ipow: operator.pow, + operator.iand: operator.and_, + operator.ior: operator.or_, + operator.ixor: operator.xor, +} + +_BUILTIN_CONSTANT_FOLDABLE_METHODS: dict[type, frozenset[str]] = { + int: frozenset({"__new__", "from_bytes"}), + bool: frozenset({"__new__", "from_bytes"}), + float: frozenset({"fromhex", "hex"}), +} +if sys.version_info >= (3, 14): + _BUILTIN_CONSTANT_FOLDABLE_METHODS[complex] = frozenset({"from_number"}) + + +_HandlerCallback = Callable[ + ["InstructionTranslator", typing.Any, typing.Any], VariableTracker | None +] +_TrackersType = type[VariableTracker] | tuple[type[VariableTracker], ...] +polyfill_fn_mapping = { + operator.eq: polyfills.cmp_eq, + operator.ne: polyfills.cmp_ne, + operator.lt: polyfills.cmp_lt, + operator.le: polyfills.cmp_le, + operator.gt: polyfills.cmp_gt, + operator.ge: polyfills.cmp_ge, +} + +bin_ops = ( + operator.pow, + operator.mul, + operator.matmul, + operator.floordiv, + operator.truediv, + operator.mod, + operator.add, + operator.lt, + operator.gt, + operator.ge, + operator.le, + operator.ne, + operator.eq, + operator.sub, + operator.ipow, + operator.imul, + operator.imatmul, + operator.ifloordiv, + operator.itruediv, + operator.imod, + operator.iadd, + operator.isub, +) + +bin_int_ops = ( + operator.and_, + operator.or_, + operator.xor, + operator.iand, + operator.ixor, + operator.ior, +) + +un_int_ops = (operator.invert,) + +tensor_and_int_ops = ( + operator.lshift, + operator.rshift, + operator.ilshift, + operator.irshift, + operator.getitem, +) + +un_ops = ( + operator.abs, + operator.pos, + operator.neg, + operator.not_, # Note: this has a local scalar dense call + operator.length_hint, +) + +_SET_LIKE_OP_SUPPORT: tuple[type[VariableTracker], ...] = ( + DictItemsVariable, + DictKeysVariable, + SetVariable, + UserDefinedObjectVariable, +) + +BUILTIN_TO_TENSOR_FN_MAP: dict[Callable[..., Any], Callable[..., Any]] = {} + +# These functions represent the r* versions of the above ops +# Basically, if __add__(1, Tensor) is called, it is translated +# to __radd__(Tensor, 1). +# In the builtin var, we check if there is a tensor in the first args position, +# if not, we swap the args and use the r* version of the op. +BUILTIN_TO_TENSOR_RFN_MAP: dict[Callable[..., Any], Callable[..., Any]] = {} + + +def populate_builtin_to_tensor_fn_map() -> None: + global BUILTIN_TO_TENSOR_FN_MAP + if len(BUILTIN_TO_TENSOR_FN_MAP) > 0: + # Only populate once; after there are elements present no need to + # repopulate + return + most_recent_func: Callable[..., Any] | None = None + + class GetMethodMode(BaseTorchFunctionMode): + """ + Mode to extract the correct methods from torch function invocations + (Used to get the correct torch.Tensor methods from builtins) + """ + + def __torch_function__( + self, + func: Callable[..., Any], + types: Any, + args: Sequence[Any] = (), + kwargs: dict[str, Any] | None = None, + ) -> Any: + kwargs = kwargs or {} + nonlocal most_recent_func + most_recent_func = func + return func(*args, **kwargs) + + inp0 = torch.ones(1) + inp1 = torch.ones(1) + inp0_int = torch.ones(1, dtype=torch.int32) + inp1_int = torch.ones(1, dtype=torch.int32) + with GetMethodMode(): + setups_and_oplists: list[tuple[Callable[..., Any], Iterable[Any]]] = [ + (lambda o: o(inp0), un_ops), + (lambda o: o(inp0_int), un_int_ops), + (lambda o: o(inp0, inp1), bin_ops), + (lambda o: o(inp0_int, inp1_int), bin_int_ops), + (lambda o: o(inp0_int, 0), tensor_and_int_ops), + ] + for setup_fn, op_list in setups_and_oplists: + for op in op_list: + setup_fn(op) + assert most_recent_func is not None + BUILTIN_TO_TENSOR_FN_MAP[op] = most_recent_func + + # gather the reverse functions + rsetups_and_oplists: list[tuple[Callable[..., Any], Iterable[Any]]] = [ + ( + lambda o: o(1, inp1), + bin_ops, + ), # Get r* ops, (ex. __sub__(int, Tensor) -> __rsub__(Tensor, int)) + (lambda o: o(1, inp1_int), bin_int_ops), + (lambda o: o(0, inp0_int), tensor_and_int_ops), + ] + + rskips = {operator.matmul, operator.imatmul, operator.getitem} + for setup_fn, op_list in rsetups_and_oplists: + for op in op_list: + if op in rskips: + continue + setup_fn(op) + assert most_recent_func is not None + if most_recent_func != BUILTIN_TO_TENSOR_FN_MAP[op]: + BUILTIN_TO_TENSOR_RFN_MAP[op] = most_recent_func + + +class BaseBuiltinVariable(VariableTracker): + """ + Common base class for all builtin variable trackers (BuiltinVariable, + DictBuiltinVariable, IterBuiltinVariable, and future specialized builtins). + + Provides shared implementations for guard installation, hasattr tracing, + and Python-level hashability/equality. + + Specialized subclasses (e.g. DictBuiltinVariable) set `_fn` as a class + attribute. BuiltinVariable stores the callable on the instance as `self.fn` + and overrides as_python_constant / reconstruct / var_getattr accordingly. + """ + + _fn: Any = None + + @classmethod + def create_with_source(cls, value: Any, source: Source) -> "BaseBuiltinVariable": + install_guard(source.make_guard(GuardBuilder.BUILTIN_MATCH)) + return cls(source=source) + + def as_python_constant(self) -> Any: + return self._fn + + def reconstruct(self, codegen: "PyCodegen") -> None: + name = self.as_python_constant().__name__ + assert name not in codegen.tx.f_globals, "shadowed global" + codegen.append_output(codegen.create_load_global(name, add=True)) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + source = self.source and AttrSource(self.source, name) + attr = getattr(self._fn, name, None) + return variables.GetAttrVariable( + self, name, py_type=type(attr) if attr is not None else None, source=source + ) + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> ConstantVariable: + return VariableTracker.build(tx, hasattr(self.as_python_constant(), name)) # type: ignore[return-value] + + def is_python_hashable(self) -> bool: + return True + + def get_python_hash(self) -> int: + return hash(self.as_python_constant()) + + def is_python_equal(self, other: object) -> bool: + return isinstance(other, BaseBuiltinVariable) and ( + self.as_python_constant() is other.as_python_constant() # type: ignore[union-attr] + ) + + +class BuiltinVariable(BaseBuiltinVariable): + """ + A VariableTracker that represents a built-in value (functions and operators). + A lot of the code here assumes it will be a function object. + + The BuiltinVariable class wraps Python built-in functions (like len, isinstance, etc.) + and operators (like +, -, *, etc.) to enable symbolic execution during tracing. This allows + Dynamo to properly handle these operations when converting Python code to FX graphs while + maintaining correct semantics and enabling optimizations. + """ + + _SENTINEL = object() + _nonvar_fields = { + "fn", + *VariableTracker._nonvar_fields, + } + + @classmethod + def create_with_source(cls, value: Any, source: Source) -> "BuiltinVariable": + install_guard(source.make_guard(GuardBuilder.BUILTIN_MATCH)) + return cls(value, source=source) + + @staticmethod + @functools.cache + def _constant_fold_functions() -> set[Callable[..., Any]]: + fns: set[Callable[..., Any]] = { + abs, + all, + any, + bool, + callable, + chr, + complex, + divmod, + float, + getattr, + int, + len, + max, + min, + ord, + pow, + repr, + round, + str, + str.format, + sum, + type, + operator.abs, + operator.pos, + operator.neg, + operator.not_, + operator.truth, + operator.invert, + operator.pow, + operator.mul, + operator.matmul, + operator.floordiv, + operator.truediv, + operator.mod, + operator.add, + operator.sub, + operator.getitem, + operator.length_hint, + operator.lshift, + operator.rshift, + operator.and_, + operator.or_, + operator.xor, + operator.ipow, + operator.imul, + operator.imatmul, + operator.ifloordiv, + operator.itruediv, + operator.imod, + operator.iadd, + operator.isub, + operator.ilshift, + operator.irshift, + operator.iand, + operator.ixor, + operator.ior, + operator.index, + } + from .tensor import supported_comparison_ops + + fns.update(supported_comparison_ops.values()) + fns.update(x for x in math.__dict__.values() if isinstance(x, type(math.sqrt))) + return fns + + def can_constant_fold_through(self) -> bool: + return self.fn in self._constant_fold_functions() + + @staticmethod + @functools.cache + def _fx_graph_functions() -> set[Callable[..., Any]]: + fns = { + operator.abs, + operator.pos, + operator.neg, + operator.not_, + operator.invert, + operator.pow, + operator.mul, + operator.matmul, + operator.floordiv, + operator.truediv, + operator.mod, + operator.add, + operator.lt, + operator.gt, + operator.ge, + operator.le, + operator.ne, + operator.eq, + operator.sub, + operator.length_hint, + operator.lshift, + operator.rshift, + operator.and_, + operator.or_, + operator.xor, + operator.ipow, + operator.imul, + operator.imatmul, + operator.ifloordiv, + operator.itruediv, + operator.getitem, + operator.imod, + operator.iadd, + operator.isub, + operator.ilshift, + operator.irshift, + operator.iand, + operator.ixor, + operator.ior, + } + return fns # type: ignore[return-value] + + @staticmethod + @functools.cache + def _binops() -> dict[ + Callable[..., object], tuple[list[str], Callable[..., object]] + ]: + # function -> ([forward name, reverse name, in-place name], in-place op) + fns: dict[Callable[..., object], tuple[list[str], Callable[..., object]]] = { + operator.add: (["__add__", "__radd__", "__iadd__"], operator.iadd), + operator.sub: (["__sub__", "__rsub__", "__isub__"], operator.isub), + operator.mul: (["__mul__", "__rmul__", "__imul__"], operator.imul), + operator.truediv: ( + ["__truediv__", "__rtruediv__", "__itruediv__"], + operator.itruediv, + ), + operator.floordiv: ( + ["__floordiv__", "__rfloordiv__", "__ifloordiv__"], + operator.ifloordiv, + ), + operator.mod: (["__mod__", "__rmod__", "__imod__"], operator.imod), + pow: (["__pow__", "__rpow__", "__ipow__"], operator.ipow), + operator.pow: (["__pow__", "__rpow__", "__ipow__"], operator.ipow), + operator.lshift: ( + ["__lshift__", "__rlshift__", "__ilshift__"], + operator.ilshift, + ), + operator.rshift: ( + ["__rshift__", "__rrshift__", "__irshift__"], + operator.irshift, + ), + operator.xor: (["__xor__", "__rxor__", "__ixor__"], operator.xor), + # NB: The follow binary operators are not supported for now, since the + # corresponding magic methods aren't defined on SymInt / SymFloat: + # operator.matmul + # divmod + # operator.and_ + # operator.or_ + } + return fns + + @staticmethod + @functools.cache + def _binop_handlers() -> dict[ + Callable[..., object], + list[ + tuple[ + tuple[ + type[VariableTracker], + _TrackersType, + ], + _HandlerCallback, + ] + ], + ]: + # Multiple dispatch mechanism defining custom binop behavior for certain type + # combinations. Handlers are attempted in order, and will be used if the type checks + # match. They are expected to have the signature: + # fn(tx, arg0: VariableTracker, arg1: VariableTracker) -> VariableTracker + from .functions import BaseUserFunctionVariable + from .nn_module import NNModuleVariable + from .tensor import supported_const_comparison_ops + from .torch import BaseTorchVariable + from .user_defined import UserDefinedVariable + + # Override table contains: op_fn -> [list of handlers] + op_handlers: dict[Any, list[Any]] = {} + for ( + op, + (magic_method_names, in_place_op), + ) in BuiltinVariable._binops().items(): + op_handlers[op] = [] + op_handlers[in_place_op] = [] + + forward_name, reverse_name, inplace_name = magic_method_names + + # User-defined args (highest precedence) + def user_defined_handler( + tx: "InstructionTranslator", + a: VariableTracker, + b: VariableTracker, + *, + forward_name: str = forward_name, + reverse_name: str = reverse_name, + ) -> VariableTracker: + # Manually handle reversing logic if needed (e.g. call __radd__) + + # TODO: If we expand this to handle tensor args, we need to manually + # handle cases like this: + # + # class A(int): + # def __radd__(self, other): + # print("woof") + # torch.randn(3) + A(3) + # + # In this example, A.__radd__() is not called -> nothing is printed, because + # Tensor.__add__ only does a subtype test against int, ignoring the subclass. + # To be fully correct, we should not call A.__radd__() here, and there may be + # other cases to reason about and add exceptions for. + if isinstance(a, UserDefinedVariable): + return a.call_method(tx, forward_name, [b], {}) + else: + return b.call_method(tx, reverse_name, [a], {}) + + op_handlers[op].append( + ((UserDefinedVariable, VariableTracker), user_defined_handler) + ) + op_handlers[op].append( + ((VariableTracker, UserDefinedVariable), user_defined_handler) + ) + + def user_defined_inplace_handler( + tx: "InstructionTranslator", + a: VariableTracker, + b: VariableTracker, + *, + forward_name: str = inplace_name, + ) -> VariableTracker: + return a.call_method(tx, forward_name, [b], {}) + + op_handlers[in_place_op].append( + ((UserDefinedVariable, VariableTracker), user_defined_inplace_handler) + ) + op_handlers[in_place_op].append( + ((VariableTracker, UserDefinedVariable), user_defined_inplace_handler) + ) + + # Dynamic shape args + def dynamic_handler( + tx: "InstructionTranslator", + a: VariableTracker, + b: VariableTracker, + *, + fn: Callable[..., Any] = op, + ) -> VariableTracker: + from .builder import wrap_fx_proxy + + return wrap_fx_proxy( + tx, + tx.output.create_proxy( + "call_function", fn, *proxy_args_kwargs([a, b], {}) + ), + ) + + op_handlers[op].append( + ((SymNodeVariable, VariableTracker), dynamic_handler) + ) + op_handlers[op].append( + ((VariableTracker, SymNodeVariable), dynamic_handler) + ) + + # NB: Prefer out-of-place op when calling in-place op to generate valid graph + op_handlers[in_place_op].append( + ((SymNodeVariable, VariableTracker), dynamic_handler) + ) + op_handlers[in_place_op].append( + ((VariableTracker, SymNodeVariable), dynamic_handler) + ) + + # Special cases - lower precedence but still prefer these over constant folding + + # List-like addition (e.g. [1, 2] + [3, 4]) + def tuple_add_handler( + tx: "InstructionTranslator", a: BaseListVariable, b: VariableTracker + ) -> VariableTracker: + return TupleVariable([*a.items, *b.unpack_var_sequence(tx)]) + + def size_add_handler( + tx: "InstructionTranslator", a: BaseListVariable, b: VariableTracker + ) -> VariableTracker: + return SizeVariable([*a.items, *b.unpack_var_sequence(tx)]) + + list_like_addition_handlers: list[ + tuple[ + tuple[ + type[VariableTracker], + _TrackersType, + ], + _HandlerCallback, + ] + ] = [ + # NB: Prefer the tuple-specific logic over base logic because of + # some SizeVariable weirdness. Specifically, the tuple-specific logic + # drops the subclass type (e.g. SizeVariable) and returns TupleVariables. + ( + (SizeVariable, SizeVariable), + size_add_handler, + ), + ( + (SizeVariable, TupleVariable), + size_add_handler, + ), + ( + (TupleVariable, SizeVariable), + size_add_handler, + ), + ( + (TupleVariable, TupleVariable), + tuple_add_handler, + ), + ( + (TupleVariable, ConstantVariable), + tuple_add_handler, + ), + ( + (ConstantVariable, TupleVariable), + lambda tx, a, b: TupleVariable( + [ + *a.unpack_var_sequence(tx), + *b.items, + ], + ), + ), + ( + ( + ListVariable, + (BaseListVariable, ConstantVariable, ListIteratorVariable), + ), + lambda tx, a, b: ListVariable( + [*a.items, *b.unpack_var_sequence(tx)], + mutation_type=ValueMutationNew(), + ), + ), + ( + (BaseListVariable, BaseListVariable), + lambda tx, a, b: type(a)( + [ + *a.items, + *b.items, + ] + ), + ), + ] + op_handlers[operator.add].extend(list_like_addition_handlers) + + def list_iadd_handler( + tx: "InstructionTranslator", a: BaseListVariable, b: VariableTracker + ) -> Any: + if a.is_immutable() or not b.has_unpack_var_sequence(tx): + # Handler doesn't apply + return None + + seq = b.unpack_var_sequence(tx) + tx.output.side_effects.mutation(a) + a.items.extend(seq) + return a + + list_like_iadd_handlers: list[Any] = [ + ( + (ListVariable, VariableTracker), + list_iadd_handler, + ), + ( + (TupleVariable, TupleVariable), + tuple_add_handler, + ), + ( + (TupleVariable, ConstantVariable), + tuple_add_handler, + ), + ] + op_handlers[operator.iadd].extend(list_like_iadd_handlers) + + # List-like expansion (e.g. [1, 2, 3] * 3) + def expand_list_like( + tx: "InstructionTranslator", lst: VariableTracker, const: VariableTracker + ) -> VariableTracker: + if not isinstance(lst, BaseListVariable) and lst.is_python_constant(): + lst, const = const, lst + try: + assert isinstance(lst, BaseListVariable) + return lst.__class__( + items=lst.items * const.as_python_constant(), + mutation_type=ValueMutationNew(), + ) + except MemoryError as exc: + raise_observed_exception( + type(exc), + tx, + args=list(exc.args), + ) + + list_like_expansion_handlers: list[ + tuple[ + tuple[type[VariableTracker], type[VariableTracker]], + _HandlerCallback, + ] + ] = [ + ((ListVariable, ConstantVariable), expand_list_like), + ((TupleVariable, ConstantVariable), expand_list_like), + ((ConstantVariable, ListVariable), expand_list_like), + ((ConstantVariable, TupleVariable), expand_list_like), + ] + op_handlers[operator.mul].extend(list_like_expansion_handlers) + + def create_cmp_op_handlers( + op: Callable[..., Any], + ) -> list[tuple[tuple[_TrackersType, _TrackersType], _HandlerCallback]]: + def compare_by_value( + tx: "InstructionTranslator", a: VariableTracker, b: VariableTracker + ) -> VariableTracker: + try: + return VariableTracker.build(tx, op(a.value, b.value)) # type: ignore[attr-defined] + except TypeError as exc: + raise_observed_exception( + type(exc), + tx, + args=list(exc.args), + ) + + result: list[ + tuple[ + tuple[ + _TrackersType, + _TrackersType, + ], + _HandlerCallback, + ] + ] = [((ConstantVariable, ConstantVariable), compare_by_value)] + + if op in polyfill_fn_mapping: + # For constants, speedup the comparison instead of using + # polyfill. Removing this line causes major regression for pr + # time benchmark - add_loop_eager. + result = [ + ((ConstantVariable, ConstantVariable), compare_by_value), + ] + + op_var = BuiltinVariable(op) + # Special handling of SymNode variable + result.extend( + [ + ( + (SymNodeVariable, VariableTracker), + op_var._comparison_with_symnode, + ), + ( + (VariableTracker, SymNodeVariable), + op_var._comparison_with_symnode, + ), + ] + ) + + def handler( + tx: "InstructionTranslator", a: VariableTracker, b: VariableTracker + ) -> VariableTracker: + return tx.inline_user_function_return( + VariableTracker.build(tx, polyfill_fn_mapping[op]), [a, b], {} + ) + + result.append(((VariableTracker, VariableTracker), handler)) + return result + + result = [((ConstantVariable, ConstantVariable), compare_by_value)] + + if op in supported_const_comparison_ops.values() and op.__name__.startswith( + "is_" + ): + # Tensor is None, List is not None, etc + none_result = op(object(), None) + + def never( + tx: "InstructionTranslator", a: VariableTracker, b: VariableTracker + ) -> VariableTracker: + return VariableTracker.build(tx, none_result) + + obj_op_none = never + none_op_obj = never + + types_that_are_never_none = ( + TensorVariable, + SymNodeVariable, + NNModuleVariable, + BaseListVariable, + UserDefinedVariable, + BaseUserFunctionVariable, + ConstDictVariable, + BaseTorchVariable, + ) + result.extend( + [ + ( + (types_that_are_never_none, ConstantVariable), + obj_op_none, + ), + ( + (ConstantVariable, types_that_are_never_none), + none_op_obj, + ), + ] + ) + + op_var = BuiltinVariable(op) + result.extend( + [ + ( + (TensorVariable, VariableTracker), + op_var._comparison_with_tensor, + ), + ( + (VariableTracker, TensorVariable), + op_var._comparison_with_tensor, + ), + ( + (SymNodeVariable, VariableTracker), + op_var._comparison_with_symnode, + ), + ( + (VariableTracker, SymNodeVariable), + op_var._comparison_with_symnode, + ), + ] + ) + + def handle_is( + tx: "InstructionTranslator", + left: VariableTracker, + right: VariableTracker, + ) -> VariableTracker | None: + from .object_protocol import vt_identity_compare + + result = vt_identity_compare(left, right) + if result is None: + return None + is_same = result.as_python_constant() + return VariableTracker.build( + tx, is_same if op.__name__ == "is_" else not is_same + ) + + result.append(((VariableTracker, VariableTracker), handle_is)) # type: ignore[arg-type] + + return result + + for op in supported_comparison_ops.values(): + assert callable(op) + assert op not in op_handlers + op_handlers[op] = create_cmp_op_handlers(op) + + return op_handlers + + @staticmethod + def _find_binop_handler( + op: Callable[..., Any], a_type: type[VariableTracker], b_type: type + ) -> list[_HandlerCallback] | None: + handlers = BuiltinVariable._binop_handlers().get(op) + if handlers is None: + return None + + matches = [] + for (type1, type2), handler in handlers: + if issubclass(a_type, type1) and issubclass(b_type, type2): + matches.append(handler) + return matches + + def can_insert_in_graph(self) -> bool: + return self.fn in self._fx_graph_functions() + + # Builtins that have been promoted to their own VT classes. Creating a + # BuiltinVariable for these is a bug; use the specialized class instead. + MUST_USE_SPECIALIZED: frozenset[Any] = frozenset({dict, iter, list}) + + def __init__(self, fn: Any, **kwargs: Any) -> None: + assert fn not in self.MUST_USE_SPECIALIZED, ( + f"Use the specialized VT class for {fn!r}, not BuiltinVariable. " + f"E.g. DictBuiltinVariable for dict." + ) + super().__init__(**kwargs) + self.fn = fn + + def __repr__(self) -> str: + if self.fn is None: + name = "None" + else: + name = self.fn.__name__ + + return f"{self.__class__.__name__}({name})" + + def as_python_constant(self) -> Any: + return self.fn + + def get_real_python_backed_value(self) -> Any: + return self.fn + + def as_proxy(self) -> Any: + DTYPE = { + bool: torch.bool, + int: torch.int64, + float: torch.float64, + } + if self.fn in DTYPE: + return DTYPE[self.fn] + return super().as_proxy() + + def reconstruct(self, codegen: "PyCodegen") -> None: + name = self.fn.__name__ + assert self.fn.__module__ == "builtins" + assert name not in codegen.tx.f_globals, "shadowed global" + codegen.append_output(codegen.create_load_global(name, add=True)) + + def constant_args(self, *args: VariableTracker, **kwargs: VariableTracker) -> bool: + return check_constant_args(args, kwargs) + + def tensor_args(self, *args: VariableTracker) -> bool: + any_tensor = False + for arg in args: + if isinstance(arg, variables.GetAttrVariable): + return False + any_tensor = any_tensor or arg.is_tensor() + return any_tensor + + def tensor_args_type(self, arg_types: list[type]) -> bool: + any_tensor = False + for arg_type in arg_types: + if issubclass(arg_type, variables.GetAttrVariable): + return False + any_tensor = any_tensor or issubclass(arg_type, variables.TensorVariable) + return any_tensor + + def python_and_tensor_constant_only( + self, *args: VariableTracker, **kwargs: VariableTracker + ) -> bool: + tensor_args = [] + non_tensor_args = [] + for i in itertools.chain(args, kwargs.values()): + if i.is_tensor(): + tensor_args.append(i) + else: + non_tensor_args.append(i) + return all( + is_constant_source(t.source) if t.source is not None else False + for t in tensor_args + ) and self.constant_args(*non_tensor_args) + + @staticmethod + def unwrap_unspec_args_kwargs( + args: Sequence[VariableTracker], kwargs: dict[str, VariableTracker] + ) -> tuple[list[Any], dict[str, Any]]: + return [x.as_python_constant() for x in args], { + k: v.as_python_constant() for k, v in kwargs.items() + } + + def has_constant_handler( + self, args: Sequence[VariableTracker], kwargs: dict[str, VariableTracker] + ) -> bool: + return self.can_constant_fold_through() and check_unspec_or_constant_args( + args, kwargs + ) + + @staticmethod + def _make_handler( + fn: Callable[..., Any], arg_types: list[type], has_kwargs: bool + ) -> Callable[ + [ + "InstructionTranslator", + list[VariableTracker], + dict[str, VariableTracker], + ], + VariableTracker | None, + ]: + from .lazy import LazyVariableTracker + + obj = BuiltinVariable(fn) + handlers: list[_HandlerCallback] = [] + + if any(issubclass(t, LazyVariableTracker) for t in arg_types): + return lambda tx, args, kwargs: obj.call_function( + tx, [v.realize() for v in args], kwargs + ) + + if inspect.isclass(fn) and ( + issubclass(fn, BaseException) + # GeneratorExit doesn't inherit from Exception + # >>> issubclass(GeneratorExit, Exception) + # False + or fn is GeneratorExit + ): + + def create_exception_class_object( + tx: "InstructionTranslator", + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if fn is AssertionError and not all( + x.is_python_constant() and isinstance(x.as_python_constant(), str) + for x in args + ): + unimplemented( + gb_type="assert with non-string message", + context=str(args), + explanation="Dynamo only supports asserts with string messages", + hints=[*graph_break_hints.SUPPORTABLE], + ) + + return variables.ExceptionVariable(fn, args, kwargs) + + return create_exception_class_object + + if obj.can_insert_in_graph() and not ( + fn is operator.getitem + and not issubclass(arg_types[0], variables.TensorVariable) + ): + if obj.tensor_args_type(arg_types): + return obj._handle_insert_op_in_graph + elif has_kwargs: + # need runtime check for kwargs + handlers.append(obj._handle_insert_op_in_graph) + + # Handle binary ops (e.g. __add__ / __radd__, __iadd__, etc.) + # NB: Tensor args are handled above and not here + if len(arg_types) == 2 and not has_kwargs: + # Try to find a handler for the arg types; otherwise, fall through to constant handler + binop_handlers = BuiltinVariable._find_binop_handler(fn, *arg_types) + if not binop_handlers: + pass + elif len(binop_handlers) == 1: + (binop_handler,) = binop_handlers + handlers.append(lambda tx, args, _: binop_handler(tx, *args)) + else: + + def call_binop_handlers( + tx: "InstructionTranslator", args: Any, _: Any + ) -> Any: + # pyrefly: ignore [not-iterable] + for fn in binop_handlers: + rv = fn(tx, *args) + if rv: + return rv + return None + + handlers.append(call_binop_handlers) + + self_handler = getattr(obj, f"call_{fn.__name__}", None) + if self_handler: + + def call_self_handler( + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker | None: + try: + # pyrefly: ignore [not-callable] + return self_handler(tx, *args, **kwargs) + except TypeError: + # Check if binding is bad. inspect signature bind is expensive. + # So check only when handler call fails. + try: + # pyrefly: ignore [bad-argument-type] + inspect.signature(self_handler).bind(tx, *args, **kwargs) + except TypeError as e: + has_constant_handler = obj.has_constant_handler(args, kwargs) + if not has_constant_handler: + log.warning( # noqa: G200 + "incorrect arg count %s %s and no constant handler", + self_handler, + e, + ) + unimplemented( + gb_type="invalid call to builtin op handler", + context=f"invalid args to {self_handler}: {args} {kwargs}", + explanation=f"Encountered TypeError when trying to handle op {fn.__name__}", + hints=[*graph_break_hints.DIFFICULT], + ) + else: + raise + except Unsupported as exc: + has_constant_handler = obj.has_constant_handler(args, kwargs) + if not has_constant_handler: + raise + # Actually, we will handle this just fine + exc.remove_from_stats() + return None + + handlers.append(call_self_handler) + + if obj.can_constant_fold_through(): + if ( + all(issubclass(x, ConstantVariable) for x in arg_types) + and not has_kwargs + ): + + def constant_fold_handler( + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker | None: + # fast path + try: + res = fn( + *[x.as_python_constant() for x in args], + ) + except Exception as exc: + raise_observed_exception( + type(exc), + tx, + args=list(exc.args), + ) + except AsPythonConstantNotImplementedError as exc: + unimplemented( + gb_type="constant fold exception", + context=f"attempted to run function {fn} with arguments {args}", + explanation="Encountered exception when attempting to constant fold.", + hints=[*graph_break_hints.DYNAMO_BUG], + from_exc=exc, + ) + return VariableTracker.build(tx, res) + + else: + + def constant_fold_handler( + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker | None: + # path with a runtime check + if check_unspec_or_constant_args(args, kwargs): + try: + res = fn( + *[x.as_python_constant() for x in args], + **{ + k: v.as_python_constant() for k, v in kwargs.items() + }, + ) + except AsPythonConstantNotImplementedError as exc: + unimplemented( + gb_type="constant fold exception", + context=f"attempted to run function {fn} with arguments {args}", + explanation="Encountered exception when attempting to constant fold.", + hints=[*graph_break_hints.DYNAMO_BUG], + from_exc=exc, + ) + except Exception as exc: + raise_observed_exception( + type(exc), + tx, + args=list(exc.args), + ) + return VariableTracker.build(tx, res) + return None + + handlers.append(constant_fold_handler) + + def call_unimplemented(args: Sequence[VariableTracker]) -> None: + real_arg_types = [arg.python_type_name() for arg in args] + unimplemented( + gb_type="Failed to trace builtin operator", + context=f"builtin {fn.__name__} {arg_types} {has_kwargs}", + explanation=f"Dynamo does not know how to trace builtin operator `{fn.__name__}` " + f"with argument types {real_arg_types} (has_kwargs {has_kwargs})", + hints=[ + f"Avoid calling builtin `{fn.__name__}` with argument types {real_arg_types}. " + f"Consider using an equivalent alternative function/method to `{fn.__name__}`.", + "If you are attempting to call a logging function (e.g. `print`), " + "you can try adding it to `torch._dynamo.config.reorderable_logging_functions`.", + "Please report an issue to PyTorch.", + ], + ) + + if len(handlers) == 0: + return lambda tx, args, kwargs: call_unimplemented(args) + elif len(handlers) == 1: + (handler,) = handlers + + def builtin_dispatch( + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker | None: + rv = handler(tx, args, kwargs) + if rv: + return rv + call_unimplemented(args) + return rv + + else: + + def builtin_dispatch( + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker | None: + rv = None + for fn in handlers: + rv = fn(tx, args, kwargs) + if rv: + return rv + call_unimplemented(args) + return rv + + return builtin_dispatch + + def call_vars(self, tx: "InstructionTranslator", *args: Any) -> VariableTracker: + if len(args) == 0: + return self._call_frame_locals_snapshot(tx) + assert len(args) == 1 + # vars(obj) is obj.__dict__ if __dict__ is present else TypeError + try: + return args[0].var_getattr(tx, "__dict__") + except ObservedAttributeError: + raise_observed_exception(TypeError, tx) + + def call_locals( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + if len(args) != 0: + raise_observed_exception(TypeError, tx) + return self._call_frame_locals_snapshot(tx) + + @staticmethod + def _call_frame_locals_snapshot(tx: "InstructionTranslator") -> VariableTracker: + frame_local_names = set(tx.f_code.co_varnames) | set(tx.cell_and_freevars()) + cell_and_freevars = set(tx.cell_and_freevars()) + frame_locals = {} + for name, value in tx.symbolic_locals.items(): + if name not in frame_local_names: + continue + if name in cell_and_freevars: + value = tx.output.side_effects.load_cell(value) + if type.__instancecheck__(NullVariable, value) or isinstance( + value, variables.DeletedVariable + ): + continue + frame_locals[ConstantVariable.create(name)] = value + return ConstDictVariable( + frame_locals, + dict, + mutation_type=ValueMutationNew(), + ) + + def _handle_insert_op_in_graph( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker | None: + from .builder import wrap_fx_proxy, wrap_fx_proxy_cls + + if kwargs and not self.tensor_args(*args, *kwargs.values()): + return None + + # insert handling for torch function here + from .builder import SourcelessBuilder + from .torch_function import can_dispatch_torch_function, dispatch_torch_function + + global BUILTIN_TO_TENSOR_RFN_MAP, BUILTIN_TO_TENSOR_FN_MAP + if can_dispatch_torch_function(tx, args, kwargs): + # Only remap the fn to tensor methods if we aren't exporting + # export serde does not handle method descriptors today + if not tx.export: + # Ensure the builtin maps are populated before accessing them + populate_builtin_to_tensor_fn_map() + # Use sourceless builder, we built the map ourselves + if not args[0].is_tensor(): + if self.fn in BUILTIN_TO_TENSOR_RFN_MAP: + func = BUILTIN_TO_TENSOR_RFN_MAP[self.fn] + else: + func = BUILTIN_TO_TENSOR_FN_MAP[self.fn] + + tmp = args[0] + # swap args and call reverse version of func + args[0] = args[1] # type: ignore[index] + args[1] = tmp # type: ignore[index] + else: + func = BUILTIN_TO_TENSOR_FN_MAP[self.fn] + else: + func = self.fn + + fn_var = SourcelessBuilder.create(tx, func) + + return dispatch_torch_function(tx, fn_var, args, kwargs) + + fn = self.fn + try: + # Constant fold for constant tensor and python constants + if self.python_and_tensor_constant_only(*args, **kwargs): + from ..bytecode_transformation import unique_id + from .functions import invoke_and_store_as_constant + + return invoke_and_store_as_constant( + tx, fn, unique_id(fn.__name__), args, kwargs + ) + + if fn in IN_PLACE_DESUGARING_MAP and isinstance( + args[0], variables.ConstantVariable + ): + # In-place operators like += usually mustate tensor + # values, but in the edge case of immutable values they + # re-bind the variable. + # + # The easiest way to keep the graph consistent in this + # scenario is to de-sugar eagerly. + fn = IN_PLACE_DESUGARING_MAP[fn] + args = [args[0], args[1]] # type: ignore[assignment] + + if fn is operator.getitem and isinstance(args[1], SymNodeVariable): + # Standard indexing will force specialization due to + # __index__. Rewrite as a regular torch op which will + # trace fine + fn = torch.select + args = [ + args[0], + variables.VariableTracker.build(tx, 0), + args[1], + ] # type: ignore[assignment] + + # Interaction between ndarray and tensors: + # We prefer the tensor op whenever there are tensors involved + # NB: Use exact type check here - NumpyNdarrayVariable is a TensorVariable + # subclass but should NOT trigger the tensor path + if check_numpy_ndarray_args(args, kwargs) and not any( + type(arg) is TensorVariable for arg in args + ): + proxy = tx.output.create_proxy( + "call_function", + numpy_operator_wrapper(fn), + *proxy_args_kwargs(args, kwargs), + ) + + return wrap_fx_proxy_cls(variables.NumpyNdarrayVariable, tx, proxy) + + if fn is operator.eq and len(args) == 2 and args[0].is_tensor(): + # Dynamo expects `__eq__` str while operator.eq gives just `eq` + # TODO - supporting all comparison operators could also work but + # it fails lots of tests because graph str changes. + return args[0].call_method(tx, "__eq__", list(args[1:]), kwargs) + proxy = tx.output.create_proxy( + "call_function", + fn, + *proxy_args_kwargs(args, kwargs), + ) + if any(isinstance(arg, FakeItemVariable) for arg in args): + return wrap_fx_proxy_cls( + FakeItemVariable, + tx, + proxy, + ) + elif check_unspec_python_args(args, kwargs): + _args, _kwargs = self.unwrap_unspec_args_kwargs(args, kwargs) + raw_value = fn(*_args, **_kwargs) + + need_unwrap = any( + x.need_unwrap + for x in itertools.chain(args, kwargs.values()) + if isinstance(x, variables.UnspecializedPythonVariable) + ) + + return wrap_fx_proxy_cls( + UnspecializedPythonVariable, + tx, + proxy, + raw_value=raw_value, + need_unwrap=need_unwrap, + ) + elif all(isinstance(x, SymNodeVariable) for x in args): + return SymNodeVariable.create(tx, proxy, None) + else: + # Work around for vision_maskrcnn due to precision difference + # specialize the dividend when float divide by tensor + if fn is operator.truediv and isinstance( + args[0], variables.UnspecializedPythonVariable + ): + args = list(args) + args[0] = args[0].as_python_constant() + return wrap_fx_proxy(tx, proxy) + + except NotImplementedError: + unimplemented( + gb_type="unimplemented builtin op on tensor arguments", + context=f"partial tensor op: {self} {args} {kwargs}", + explanation=f"Dynamo does not know how to trace builtin operator {self.fn} with tensor arguments", + hints=[*graph_break_hints.SUPPORTABLE], + ) + + call_function_handler_cache: dict[ + tuple[object, ...], + Callable[ + [ + "InstructionTranslator", + Sequence[VariableTracker], + dict[str, VariableTracker], + ], + VariableTracker, + ], + ] = {} + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + key: tuple[object, ...] + if kwargs: + kwargs = {k: v.realize() for k, v in kwargs.items()} + key = (self.fn, *(type(x) for x in args), True) + else: + key = (self.fn, *(type(x) for x in args)) + + handler = self.call_function_handler_cache.get(key) + if not handler: + self.call_function_handler_cache[key] = handler = self._make_handler( # type: ignore[assignment] + self.fn, [type(x) for x in args], bool(kwargs) + ) + assert handler is not None + return handler(tx, args, kwargs) # type: ignore[return-value] + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if self.fn is object and name == "__setattr__": + assert len(args) == 3 + assert len(kwargs) == 0 + obj, name_var, val = args + obj = obj.realize() + if ( + isinstance(obj, UserDefinedObjectVariable) + and tx.output.side_effects.is_attribute_mutation(obj) + and name_var.is_python_constant() + ): + return obj.method_setattr_standard(tx, name_var, val) + + if name == "__new__": + # Supported __new__ methods + if self.fn is object and len(args) == 1: + assert len(kwargs) == 0 + return tx.output.side_effects.track_new_user_defined_object( + self, args[0], args[1:] + ) + + if ( + self.fn is tuple + and len(args) == 2 + and args[1].has_force_unpack_var_sequence(tx) + and not kwargs + ): + if isinstance(args[0], BuiltinVariable) and args[0].fn is tuple: + init_args = args[1].force_unpack_var_sequence(tx) + return variables.TupleVariable( + init_args, mutation_type=ValueMutationNew() + ) + + return tx.output.side_effects.track_new_user_defined_object( + self, + args[0], + args[1:], + ) + + if name in _BUILTIN_CONSTANT_FOLDABLE_METHODS.get(self.fn, ()): + if all(a.is_python_constant() for a in args) and all( + v.is_python_constant() for v in kwargs.values() + ): + try: + fn = getattr(self.fn, name) + res = fn( + *(a.as_python_constant() for a in args), + **{k: v.as_python_constant() for k, v in kwargs.items()}, + ) + return VariableTracker.build(tx, res) + except Exception as e: + raise_observed_exception( + type(e), + tx, + args=list(e.args), + ) + + if self.fn is object and name == "__init__": + # object.__init__ is a no-op + return variables.ConstantVariable.create(None) + + if self.fn is set: + resolved_fn = getattr(self.fn, name) + if resolved_fn in set_methods: + if isinstance(args[0], variables.UserDefinedSetVariable): + assert args[0]._base_vt is not None + return args[0]._base_vt.call_method(tx, name, args[1:], kwargs) + elif isinstance(args[0], variables.SetVariable): + return args[0].call_method(tx, name, args[1:], kwargs) + + if self.fn is frozenset: + resolved_fn = getattr(self.fn, name) + if resolved_fn in frozenset_methods: + if isinstance(args[0], variables.FrozensetVariable): + return args[0].call_method(tx, name, args[1:], kwargs) + + if self.fn is str and len(args) >= 1: + resolved_fn = getattr(self.fn, name) + if resolved_fn in str_methods: + # Only delegate to ConstantVariable, not other types that happen to be constants + if isinstance(args[0], ConstantVariable): + return args[0].call_method(tx, name, args[1:], kwargs) + + if self.fn is float and len(args) >= 1: + # Only delegate to ConstantVariable, not other types that happen to be constants + if isinstance(args[0], ConstantVariable): + return VariableTracker.build( + tx, getattr(float, name)(args[0].as_python_constant()) + ) + + if name == "__len__" and len(args) == 1 and not kwargs: + # type.__len__(instance) → len(instance) + # e.g. list.__len__(my_list) → len(my_list) + from .object_protocol import generic_len + + return generic_len(tx, args[0]) + + return super().call_method(tx, name, args, kwargs) + + def call_int( + self, tx: "InstructionTranslator", arg: VariableTracker + ) -> VariableTracker | None: + from .object_protocol import generic_int + + return generic_int(tx, arg) + + def call_float( + self, tx: "InstructionTranslator", arg: VariableTracker + ) -> VariableTracker | None: + from .object_protocol import generic_float + + return generic_float(tx, arg) + + def call_bool( + self, tx: "InstructionTranslator", arg: VariableTracker + ) -> VariableTracker | None: + # Emulate PyBool_Type.tp_vectorcall which boils down to PyObject_IsTrue. + from .object_protocol import generic_bool + + return generic_bool(tx, arg) + + def call_repr( + self, tx: "InstructionTranslator", arg: VariableTracker + ) -> VariableTracker | None: + """Handle repr() on user defined objects.""" + if isinstance( + arg, + (variables.ExceptionVariable, variables.UserDefinedExceptionObjectVariable), + ): + try: + const_args = tuple(a.as_python_constant() for a in arg.args) + except NotImplementedError: + return None + if len(const_args) == 0: + value = f"{arg.exc_type.__name__}()" + elif len(const_args) == 1: + value = f"{arg.exc_type.__name__}({const_args[0]!r})" + else: + value = f"{arg.exc_type.__name__}{const_args!r}" + return VariableTracker.build(tx, value) + if isinstance(arg, variables.UserDefinedObjectVariable): + repr_method = arg.value.__repr__ + + if type(arg.value).__repr__ is object.__repr__: + # Default repr - build and trace it + fn_vt = VariableTracker.build(tx, repr_method) + return fn_vt.call_function(tx, [], {}) + elif is_wrapper_or_member_descriptor(repr_method): + unimplemented( + gb_type="Attempted to call repr() method implemented in C/C++", + context="", + explanation=f"{type(arg.value)} has a C/C++ based repr method. This is not supported.", + hints=["Write the repr method in Python"], + ) + else: + bound_method = repr_method.__func__ + fn_vt = VariableTracker.build(tx, bound_method) + return fn_vt.call_function(tx, [arg], {}) + if isinstance(arg, variables.UserDefinedClassVariable): + if type(arg.value).__repr__ is type.__repr__: + return VariableTracker.build(tx, repr(arg.value)) + if isinstance( + arg, + ( + RangeVariable, + ConstDictVariable, + DefaultDictVariable, + OrderedSetClassVariable, + DictViewVariable, + ), + ): + return VariableTracker.build(tx, arg.debug_repr()) + return None + + def call_str( + self, tx: "InstructionTranslator", arg: VariableTracker + ) -> VariableTracker | None: + if isinstance( + arg, + (variables.ExceptionVariable, variables.UserDefinedExceptionObjectVariable), + ): + if len(arg.args) == 0: + return VariableTracker.build(tx, "") + elif len(arg.args) == 1: + return BuiltinVariable(str).call_function(tx, [arg.args[0]], {}) + else: + tuple_var = variables.TupleVariable(list(arg.args)) + return BuiltinVariable(str).call_function(tx, [tuple_var], {}) + + # Handle `str` on a user defined function or object + if isinstance(arg, (variables.UserFunctionVariable)): + return VariableTracker.build(tx, str(arg.fn)) + elif isinstance(arg, (variables.UserDefinedObjectVariable)): + # Check if object has __str__ method + if hasattr(arg.value, "__str__"): + str_method = arg.value.__str__ + elif hasattr(arg.value, "__repr__"): + # account for __repr__ functions when __str__ is absent + str_method = arg.value.__repr__ + else: + unimplemented( + gb_type="failed to call str() on user defined object", + context=str(arg), + explanation="User defined object has no __str__ or __repr__ method", + hints=[*graph_break_hints.USER_ERROR], + ) + + if type(arg.value).__str__ is object.__str__: + # Rely on the object str method + try: + # pyrefly: ignore [unbound-name] + return VariableTracker.build(tx, str_method()) + except AttributeError: + # Graph break + return None + elif is_wrapper_or_member_descriptor(str_method): + unimplemented( + gb_type="Attempted to a str() method implemented in C/C++", + context="", + explanation=f"{type(arg.value)} has a C/C++ based str method. This is not supported.", + hints=["Write the str method in Python"], + ) + else: + # Overrides for custom str method + # Pass method as function to call tx.inline_user_function_return + bound_method = str_method.__func__ # type: ignore[attr-defined] + + try: + # Only supports certain function types + user_func_variable = VariableTracker.build(tx, bound_method) + except AssertionError: + # Won't be able to do inline the str method, return to avoid graph break + log.warning("Failed to create UserFunctionVariable", exc_info=True) + return None + + # Inline the user function + return user_func_variable.call_function(tx, [arg], {}) + return None + + def _call_min_max( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker | None: + if len(args) == 1 and args[0].has_force_unpack_var_sequence(tx): + items = args[0].force_unpack_var_sequence(tx) + return self._call_min_max_seq(tx, items) + elif len(args) == 2: + return self._call_min_max_binary(tx, args[0], args[1]) + elif len(args) > 2: + return self._call_min_max_seq(tx, args) + return None + + def _call_min_max_seq( + self, tx: "InstructionTranslator", items: Sequence[VariableTracker] + ) -> VariableTracker: + assert len(items) > 0 + if len(items) == 1: + return items[0] + + return functools.reduce(functools.partial(self._call_min_max_binary, tx), items) # type: ignore[arg-type,return-value] + + def _call_min_max_binary( + self, + tx: "InstructionTranslator", + a: VariableTracker | None, + b: VariableTracker | None, + ) -> VariableTracker | None: + if a is None or b is None: + # a or b could be none if we reduce and _call_min_max_binary failed + # to return something + return None + if self.tensor_args(a, b): + if not a.is_tensor(): + a, b = b, a + assert a.is_tensor() + + # result of an item call is a scalar convert to a tensor + if isinstance(a, FakeItemVariable): + a = variables.TorchInGraphFunctionVariable(torch.tensor).call_function( + tx, [a], {} + ) + + # Dynamic input does not get resolved, rather, gets stored as call_function + if isinstance(a, SymNodeVariable) or isinstance(b, SymNodeVariable): + from .builder import wrap_fx_proxy_cls + + return wrap_fx_proxy_cls( + type(a), + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + self.fn, + *proxy_args_kwargs([a, b], {}), + ), + ) + + # convert min/max to torch ops + if b.is_python_constant(): + fn: VariableTracker + if isinstance(a, variables.NumpyNdarrayVariable): + import numpy as np + + fn = variables.NumpyVariable(np.clip) + else: + fn = variables.TorchInGraphFunctionVariable(torch.clamp) + kwargs = {"min": b} if (self.fn is max) else {"max": b} + result = fn.call_function(tx, [a], kwargs) + else: + if isinstance(a, variables.NumpyNdarrayVariable): + import numpy as np + + np_fn = {max: np.maximum, min: np.minimum}[self.fn] + fn = variables.NumpyVariable(np_fn) + else: + torch_fn = {max: torch.maximum, min: torch.minimum}[self.fn] + fn = variables.TorchInGraphFunctionVariable(torch_fn) + result = fn.call_function(tx, [a, b], {}) + + # return unspec if both a, b are unspec or const + if all( + isinstance( + i, + ( + variables.UnspecializedPythonVariable, + variables.ConstantVariable, + ), + ) + for i in [a, b] + ): + if any(isinstance(val, FakeItemVariable) for val in [a, b]): + # type: ignore[arg-type] + return variables.FakeItemVariable.from_tensor_variable(result) + + if b.is_python_constant(): + raw_b = b.as_python_constant() + else: + raw_b = b.raw_value # type: ignore[attr-defined] + if self.fn is max: + raw_res = max(a.raw_value, raw_b) # type: ignore[attr-defined] + else: + raw_res = min(a.raw_value, raw_b) # type: ignore[attr-defined] + + need_unwrap = any( + x.need_unwrap + for x in [a, b] + if isinstance(x, variables.UnspecializedPythonVariable) + ) + return variables.UnspecializedPythonVariable.from_tensor_variable( + result, # type: ignore[arg-type] + raw_res, + need_unwrap, + ) + # otherwise return tensor + else: + return result + elif isinstance(a, SymNodeVariable) or isinstance(b, SymNodeVariable): + py_fn = torch.sym_max if self.fn is max else torch.sym_min + proxy = tx.output.create_proxy( + "call_function", py_fn, *proxy_args_kwargs([a, b], {}) + ) + return SymNodeVariable.create(tx, proxy, None) + elif isinstance(a, ConstantVariable) and isinstance(b, ConstantVariable): + value = self.fn( + a.as_python_constant(), + b.as_python_constant(), + ) + return VariableTracker.build(tx, value) + return None + + call_min = _call_min_max + call_max = _call_min_max + + def call_abs( + self, tx: "InstructionTranslator", arg: VariableTracker + ) -> VariableTracker: + from .builder import SourcelessBuilder + + # Call arg.__abs__() + abs_method = SourcelessBuilder.create(tx, getattr).call_function( + tx, [arg, VariableTracker.build(tx, "__abs__")], {} + ) + return abs_method.call_function(tx, [], {}) + + def call_pos( + self, tx: "InstructionTranslator", arg: VariableTracker + ) -> VariableTracker: + from .builder import SourcelessBuilder + + # Call arg.__pos__() + pos_method = SourcelessBuilder.create(tx, getattr).call_function( + tx, [arg, VariableTracker.build(tx, "__pos__")], {} + ) + return pos_method.call_function(tx, [], {}) + + def call_index( + self, tx: "InstructionTranslator", arg: VariableTracker + ) -> VariableTracker: + # Specialize SymNodeVariable to a constant first, matching CPython's + # PyNumber_Index which forces a concrete int. + arg = specialize_symnode(arg) + return arg.nb_index_impl(tx) + + def call_round( + self, + tx: "InstructionTranslator", + arg: VariableTracker, + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + from .builder import SourcelessBuilder + + # Call arg.__round__() + round_method = SourcelessBuilder.create(tx, getattr).call_function( + tx, [arg, VariableTracker.build(tx, "__round__")], {} + ) + return round_method.call_function(tx, args, kwargs) + + def call_range( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker | None: + if check_unspec_or_constant_args(args, {}): + return variables.RangeVariable(args) + elif self._dynamic_args(*args): + args = tuple(VariableTracker.build(tx, guard_if_dyn(arg)) for arg in args) + return variables.RangeVariable(args) + # None no-ops this handler and lets the driving function proceed + return None + + def _dynamic_args(self, *args: VariableTracker, **kwargs: VariableTracker) -> bool: + return any(isinstance(x, SymNodeVariable) for x in args) or any( + isinstance(x, SymNodeVariable) for x in kwargs.values() + ) + + def call_slice( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + return variables.SliceVariable(args, tx) + + def _dyn_proxy( + self, tx: "InstructionTranslator", *args: Any, **kwargs: Any + ) -> VariableTracker: + from .builder import wrap_fx_proxy + + return wrap_fx_proxy( + tx, + tx.output.create_proxy( + "call_function", self.fn, *proxy_args_kwargs(args, kwargs) + ), + ) + + # NOTE must handle IteratorVariable separately! + def _call_iter_tuple_list( + self, + tx: "InstructionTranslator", + obj: VariableTracker | None = None, + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker | None: + assert not isinstance(obj, variables.IteratorVariable) + + if self._dynamic_args(*args, **kwargs): + return self._dyn_proxy(tx, *args, **kwargs) + + cls = variables.BaseListVariable.cls_for(self.fn) + if obj is None: + return cls( + [], + mutation_type=ValueMutationNew(), + ) + elif obj.has_unpack_var_sequence(tx): + if obj.source and not is_constant_source(obj.source): + if isinstance(obj, TupleIteratorVariable): + install_guard( + obj.source.make_guard(GuardBuilder.TUPLE_ITERATOR_LEN) + ) + else: + if getattr(obj, "source", False) and isinstance( + obj, + ( + ConstDictVariable, + variables.OrderedSetVariable, + variables.DictKeySetVariable, + ), + ): + tx.output.guard_on_key_order.add(obj.source) + + if isinstance(obj, variables.MappingProxyVariable): + # This could be an overguarding, but its rare to iterate + # through a mapping proxy and not use the keys. + install_guard( + obj.source.make_guard(GuardBuilder.MAPPING_KEYS_CHECK) + ) + elif not isinstance(obj, variables.UnspecializedNNModuleVariable): + # Prevent calling __len__ method for guards, the tracing + # of __iter__ will insert the right guards later. + install_guard( + obj.source.make_guard(GuardBuilder.SEQUENCE_LENGTH) + ) + + return cls( + list(obj.unpack_var_sequence(tx)), + mutation_type=ValueMutationNew(), + ) + return None + + def _call_iter_tuple_generator( + self, + tx: "InstructionTranslator", + obj: VariableTracker, + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + cls = variables.BaseListVariable.cls_for(self.fn) + return cls( + list(obj.force_unpack_var_sequence(tx)), # exhaust generator + mutation_type=ValueMutationNew(), + ) + + def _call_tuple_list( + self, + tx: "InstructionTranslator", + obj: VariableTracker | None = None, + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker | None: + if isinstance(obj, variables.IteratorVariable): + cls = variables.BaseListVariable.cls_for(self.fn) + return cls( + list(obj.force_unpack_var_sequence(tx)), + mutation_type=ValueMutationNew(), + ) + elif isinstance(obj, variables.LocalGeneratorObjectVariable) or ( + isinstance(obj, UserDefinedObjectVariable) + and obj.has_force_unpack_var_sequence(tx) + ): + return self._call_iter_tuple_generator(tx, obj, *args, **kwargs) + else: + return self._call_iter_tuple_list(tx, obj, *args, **kwargs) + + call_tuple = _call_tuple_list + + def call_callable( + self, tx: "InstructionTranslator", arg: VariableTracker + ) -> VariableTracker | None: + from .functions import BaseUserFunctionVariable, FunctoolsPartialVariable + from .nn_module import NNModuleVariable + + if isinstance( + arg, + ( + variables.UserDefinedClassVariable, + BaseUserFunctionVariable, + FunctoolsPartialVariable, + NNModuleVariable, + ), + ): + return variables.ConstantVariable.create(True) + elif isinstance(arg, UserDefinedVariable): + return VariableTracker.build(tx, callable(arg.value)) + elif isinstance( + arg, + ( + ConstantVariable, + SymNodeVariable, + TensorVariable, + ListVariable, + TupleVariable, + ListIteratorVariable, + ), + ): + return variables.ConstantVariable.create(False) + else: + return None + + def call_cast( + self, _: Any, *args: VariableTracker, **kwargs: VariableTracker + ) -> VariableTracker | None: + if len(args) == 2: + return args[1] + + unimplemented( + gb_type="bad args to builtin cast()", + context=f"got args {args} {kwargs}", + explanation="Dynamo expects exactly 2 args to builtin cast().", + hints=["Ensure your call to cast() has exactly 2 arguments."], + ) + + def call_dir( + self, tx: "InstructionTranslator", arg: VariableTracker + ) -> VariableTracker | None: + if isinstance(arg, variables.UserDefinedClassVariable): + return VariableTracker.build(tx, dir(arg.value)) + if isinstance(arg, BuiltinVariable): + return VariableTracker.build(tx, dir(arg.fn)) + # Enable specialized VTs for constants to work with dir() + if arg.is_python_constant(): + return VariableTracker.build(tx, dir(arg.as_python_constant())) + return None + + def call_set( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + from .builder import SourcelessBuilder + + assert not kwargs + if not args: + return SetVariable([], mutation_type=ValueMutationNew()) + if len(args) != 1: + raise_observed_exception( + TypeError, + tx, + args=[f"set() takes 1 positional argument but {len(args)} were given"], + ) + arg = args[0] + if istype(arg, variables.SetVariable): + return arg.clone(mutation_type=ValueMutationNew()) + elif arg.has_force_unpack_var_sequence(tx): + items = arg.force_unpack_var_sequence(tx) + return SetVariable(items, mutation_type=ValueMutationNew()) + elif isinstance(arg, variables.UserDefinedObjectVariable) and isinstance( + arg.value, KeysView + ): + iter_fn = arg.var_getattr(tx, "__iter__") + if isinstance(iter_fn, variables.UserMethodVariable): + out = tx.inline_user_function_return(iter_fn, args, kwargs) + if isinstance(out, SetVariable): + return out + return SourcelessBuilder.create(tx, set).call_set(tx, out) + raise_observed_exception( + TypeError, + tx, + args=["failed to construct builtin set()"], + ) + + def call_frozenset( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + assert not kwargs + if not args: + return FrozensetVariable([]) + if len(args) != 1: + raise_observed_exception( + TypeError, + tx, + args=[ + f"frozenset() takes 1 positional argument but {len(args)} were given" + ], + ) + arg = args[0] + if istype(arg, variables.FrozensetVariable): + return FrozensetVariable([x.vt for x in arg.set_items]) + elif arg.has_force_unpack_var_sequence(tx): + items = arg.force_unpack_var_sequence(tx) + return FrozensetVariable(items) + raise_observed_exception( + TypeError, + tx, + args=["failed to construct builtin frozenset()"], + ) + + def call_zip( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + from .builder import SourcelessBuilder + + if kwargs: + if not (len(kwargs) == 1 and "strict" in kwargs): + raise_args_mismatch( + tx, + "zip", + "1 kwargs (`strict`)", + f"{len(kwargs)} kwargs", + ) + strict = kwargs.pop("strict", ConstantVariable.create(False)) + iter_args = [ + SourcelessBuilder.create(tx, iter).call_function(tx, [arg], {}) + for arg in args + ] + return variables.ZipVariable( + iter_args, + strict=strict.as_python_constant(), + mutation_type=ValueMutationNew(), + ) + + def call_len( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + from .object_protocol import generic_len + + return generic_len(tx, args[0]) + + def call_getitem( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + from .object_protocol import vt_getitem + + return vt_getitem(tx, args[0], args[1]) + + def call_isinstance( + self, + tx: "InstructionTranslator", + arg: VariableTracker, + isinstance_type_var: VariableTracker, + ) -> VariableTracker: + try: + arg_type = arg.python_type() + except NotImplementedError: + unimplemented( + gb_type="builtin isinstance() cannot determine type of argument", + context=f"isinstance({arg}, {isinstance_type_var})", + explanation=f"Dynamo doesn't have a rule to determine the type of argument {arg}", + hints=[*graph_break_hints.DYNAMO_BUG], + ) + isinstance_type = isinstance_type_var.as_python_constant() + if isinstance(arg, variables.TensorVariable) and arg.dtype is not None: + + def _tensor_isinstance( + tensor_var: VariableTracker, tensor_type: Any + ) -> bool: + def check_type(ty: Any) -> bool: + if ty not in tensortype_to_dtype: + example_val = arg.as_proxy().node.meta["example_value"] + if ( + is_traceable_wrapper_subclass(example_val) + and ty is torch.nn.parameter.Parameter + ): + # N.B: we are calling isinstance directly on the example value. + # torch.nn.Parameter has a meta-class that overrides __isinstance__, + # the isinstance check here allows us to invoke that logic. + return isinstance(example_val, ty) + else: + return issubclass(arg.python_type(), ty) + + dtypes = tensortype_to_dtype[ty] + # pyrefly: ignore [missing-attribute] + return arg.dtype in dtypes + + if type(tensor_type) is tuple: + return any(check_type(ty) for ty in tensor_type) + else: + return check_type(tensor_type) + + return VariableTracker.build(tx, _tensor_isinstance(arg, isinstance_type)) + # UserDefinedObject with C extensions can have torch.Tensor attributes, + # so break graph. + if isinstance(arg, variables.UserDefinedObjectVariable) and isinstance( + arg.value, types.MemberDescriptorType + ): + unimplemented( + gb_type="isinstance() called on user defined object with C extensions", + context=f"isinstance({arg}, {isinstance_type})", + explanation="User-defined object with C extensions can have torch.Tensor " + "attributes; intentionally graph breaking.", + hints=[*graph_break_hints.SUPPORTABLE], + ) + # handle __instancecheck__ defined in user class + if ( + isinstance(arg, variables.UserDefinedObjectVariable) + and "__instancecheck__" in isinstance_type.__class__.__dict__ + ): + return VariableTracker.build( + tx, + isinstance_type.__class__.__instancecheck__(isinstance_type, arg.value), + ) + + if isinstance(arg, variables.UserDefinedExceptionClassVariable): + # pyrefly: ignore [unbound-name] + return VariableTracker.build(tx, isinstance(arg_type, isinstance_type)) + + isinstance_type_tuple: tuple[type, ...] + if isinstance(isinstance_type, type) or callable( + # E.g. isinstance(obj, typing.Sequence) + getattr(isinstance_type, "__instancecheck__", None) + ): + isinstance_type_tuple = (isinstance_type,) + elif isinstance(isinstance_type, types.UnionType): + isinstance_type_tuple = isinstance_type.__args__ + elif isinstance(isinstance_type, tuple) and all( + isinstance(tp, type) or callable(getattr(tp, "__instancecheck__", None)) + for tp in isinstance_type + ): + isinstance_type_tuple = isinstance_type + else: + raise_observed_exception( + TypeError, + tx, + args=[ + "isinstance() arg 2 must be a type, a tuple of types, or a union" + ], + ) + + try: + # NB: `isinstance()` does not call `__subclasscheck__` but use `__instancecheck__`. + # But usually `isinstance(obj, type_info)` and `issubclass(type(obj), type_info)` gives + # the same result. + # WARNING: This might run arbitrary user code `__subclasscheck__` and we did not trace + # through it. This is a limitation of the current implementation. + # Usually `__subclasscheck__` and `__instancecheck__` can be constant fold through, it + # might not be a big issue and we trade off it for performance. + val = issubclass(arg_type, isinstance_type_tuple) + except TypeError: + val = arg_type in isinstance_type_tuple + return VariableTracker.build(tx, val) + + def call_issubclass( + self, + tx: "InstructionTranslator", + left_ty: VariableTracker, + right_ty: VariableTracker, + ) -> VariableTracker: + """Checks if first arg is subclass of right arg""" + try: + left_ty_py = left_ty.as_python_constant() + right_ty_py = right_ty.as_python_constant() + except NotImplementedError: + unimplemented( + gb_type="issubclass() with non-constant arguments", + context=f"issubclass({left_ty}, {right_ty})", + explanation="issubclass() with non-constant arguments not supported.", + hints=[ + "Make sure your arguments are types.", + *graph_break_hints.USER_ERROR, + ], + ) + + # WARNING: This might run arbitrary user code `__subclasscheck__`. + # See the comment in call_isinstance above. + # pyrefly: ignore [unbound-name] + return VariableTracker.build(tx, issubclass(left_ty_py, right_ty_py)) + + def call_super( + self, tx: "InstructionTranslator", a: VariableTracker, b: VariableTracker + ) -> VariableTracker: + return variables.SuperVariable(a, b) + + def call_next( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + arg = args[0] + try: + return arg.next_variable(tx) + except ObservedUserStopIteration: + if len(args) == 2: + return args[1] + raise + except Unsupported as ex: + if isinstance(arg, variables.BaseListVariable): + ex.remove_from_stats() + return arg.items[0] + raise + + def call_hasattr( + self, tx: "InstructionTranslator", obj: VariableTracker, attr: VariableTracker + ) -> VariableTracker | None: + if attr.is_python_constant(): + name = attr.as_python_constant() + return obj.call_obj_hasattr(tx, name) + return None + + def call_map( + self, + tx: "InstructionTranslator", + fn: VariableTracker, + *seqs: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + strict = ConstantVariable.create(False) + if kwargs: + if sys.version_info >= (3, 14): + if not (len(kwargs) == 1 and "strict" in kwargs): + raise_args_mismatch( + tx, + "map", + "1 kwargs (`strict`)", + f"{len(kwargs)} kwargs", + ) + strict = kwargs.pop("strict", ConstantVariable.create(False)) + else: + raise_args_mismatch( + tx, + "map", + "0 kwargs", + f"{len(kwargs)} kwargs", + ) + + seq_list = [ + seq.unpack_var_sequence(tx) if seq.has_unpack_var_sequence(tx) else seq + for seq in seqs + ] + return variables.MapVariable( + fn, + seq_list, # type: ignore[arg-type] + strict=strict.as_python_constant(), + mutation_type=ValueMutationNew(), + ) + + def call_filter( + self, tx: "InstructionTranslator", fn: VariableTracker, seq: VariableTracker + ) -> VariableTracker: + seq_or_list = ( + seq.unpack_var_sequence(tx) if seq.has_unpack_var_sequence(tx) else seq + ) + return variables.FilterVariable( + fn, + seq_or_list, # type: ignore[arg-type] + mutation_type=ValueMutationNew(), + ) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + source = self.source and AttrSource(self.source, name) + if name == "__name__": + return VariableTracker.build(tx, self.fn.__name__, source) + if self.fn is object: + # for object, we can just directly read the attribute + try: + value = getattr(self.fn, name) + except AttributeError: + raise_observed_exception(AttributeError, tx) + if not callable(value): + return VariableTracker.build(tx, value, source) + attr = getattr(self.fn, name, None) + return variables.GetAttrVariable( + self, name, py_type=type(attr) if attr is not None else None, source=source + ) + + def call_getattr( + self, + tx: "InstructionTranslator", + obj: VariableTracker, + name_var: VariableTracker, + default: VariableTracker | None = None, + ) -> VariableTracker | None: + if not name_var.is_python_constant(): + unimplemented( + gb_type="getattr() with non-constant name argument", + context=f"getattr({obj}, {name_var}, {default})", + explanation="getattr() with non-constant name argument is not supported", + hints=["Ensure the name argument of getattr() is a string"], + ) + + name = name_var.as_python_constant() + + # See NOTE [Tensor "grad" and "_grad" attr] + if obj.is_tensor() and name == "_grad": + name = "grad" + + if tx.output.side_effects.is_attribute_mutation(obj): + if isinstance(obj, variables.UnspecializedNNModuleVariable): + if ( + name + in ( + "named_parameters", + "parameters", + "named_buffers", + "buffers", + "named_modules", + "modules", + ) + and obj.is_state_mutated + and tx.output.side_effects.has_pending_mutation(obj) + ): + unimplemented( + gb_type="getattr() on nn.Module with pending mutation", + context=f"getattr({obj}, {name}, {default})", + explanation="Intentionally graph breaking on getattr() on a nn.Module " + "with a pending mutation", + hints=[], + ) + + if tx.output.side_effects.has_pending_mutation_of_attr(obj, name): + return tx.output.side_effects.load_attr(obj, name) + + if default is not None: + hasattr_var = self.call_hasattr(tx, obj, name_var) + if hasattr_var is not None: + assert hasattr_var.is_constant_match(True, False) + if not hasattr_var.as_python_constant(): + return default + else: + return default + + source = obj.source and AttrSource(obj.source, name) + if name in {"__bases__", "__base__", "__flags__"}: + try: + value = obj.as_python_constant() + if isinstance(value, type): + if name == "__bases__": + tuple_args = [ + VariableTracker.build( + tx, b, source and GetItemSource(source, i) + ) + for i, b in enumerate(value.__bases__) + ] + return variables.TupleVariable(tuple_args, source=source) + if name == "__base__": + return VariableTracker.build(tx, value.__base__, source) + if name == "__flags__": + return VariableTracker.build(tx, value.__flags__) + except NotImplementedError: + pass + + if isinstance(obj, variables.NNModuleVariable): + return obj.var_getattr(tx, name) + elif isinstance( + obj, + ( + variables.TensorVariable, + variables.ConstantVariable, + variables.DefaultDictVariable, + variables.DistributedVariable, + variables.UserDefinedClassVariable, + variables.UserDefinedObjectVariable, + ), + ): + if ( + isinstance(obj, variables.UserDefinedObjectVariable) + and issubclass(obj.value.__class__, unittest.TestCase) + and config.enable_trace_unittest + and name + in ( + "assertRaisesRegex", + "assertNotWarns", + "assertWarnsRegex", + "assertWarns", + ) + ): + unimplemented( + gb_type="Failed to trace unittest method", + context=f"function: unittest.TestCase.{name}", + explanation=f"Dynamo does not know how to trace unittest method `{name}` ", + hints=[ + f"Avoid calling `TestCase.{name}`. " + "Please report an issue to PyTorch.", + ], + ) + if obj.is_tensor(): + # pyrefly: ignore[missing-attribute] + fake_val = obj.as_proxy().node.meta["example_value"] + if ( + isinstance(fake_val, torch.Tensor) + and is_sparse_any(fake_val) + and (not tx.export or not config.capture_sparse_compute) + ): + unimplemented( + gb_type="Attempted to wrap sparse Tensor", + context="", + explanation="torch.compile does not support sparse Tensors", + hints=[*graph_break_hints.SPARSE_TENSOR], + ) + + try: + return obj.var_getattr(tx, name) + except AsPythonConstantNotImplementedError: + # dont fallback on as_python_constant error because this leads + # to a failure later on, and leads to a wrong stacktrace + raise + except NotImplementedError: + return variables.GetAttrVariable(obj, name, source=source) + elif isinstance(obj, variables.TorchInGraphFunctionVariable): + # Get OpOverload from an OpOverloadPacket, e.g., torch.ops.aten.add.default. + try: + member = getattr(obj.value, name) + except AttributeError: + raise_observed_exception(AttributeError, tx) + raise + + if isinstance( + member, (torch._ops.OpOverloadPacket, torch._ops.OpOverload) + ) and torch._dynamo.trace_rules.is_aten_op_or_tensor_method(member): + return variables.TorchInGraphFunctionVariable(member, source=source) + elif name in cmp_name_to_op_mapping: + return variables.GetAttrVariable( + obj, name, py_type=type(member), source=source + ) + else: + return None + elif isinstance(obj, DummyModule): + # TODO(mlazos) - Do we need this? + if obj.is_torch or name not in obj.value.__dict__: + member = getattr(obj.value, name) + else: + member = obj.value.__dict__[name] + + if config.replay_record_enabled: + tx.exec_recorder.record_module_access(obj.value, name, member) # type: ignore[arg-type, union-attr] + return VariableTracker.build(tx, member, source) + else: + try: + return obj.var_getattr(tx, name) + except NotImplementedError: + return variables.GetAttrVariable(obj, name, source=source) + + def call_setattr( + self, + tx: "InstructionTranslator", + obj: VariableTracker, + name_var: VariableTracker, + val: VariableTracker, + ) -> VariableTracker | None: + if isinstance( + obj, + ( + variables.DefaultDictVariable, + variables.UserDefinedObjectVariable, + variables.NestedUserFunctionVariable, + variables.ExceptionVariable, + variables.TracebackVariable, + ), + ): + return obj.call_method(tx, "__setattr__", [name_var, val], {}) + elif ( + tx.output.side_effects.is_attribute_mutation(obj) + and name_var.is_python_constant() + ): + name = name_var.as_python_constant() + if obj.is_tensor(): + from .builder import wrap_fx_proxy + + # Some special handling for tensor attributes. + if name == "requires_grad": + # TODO(voz): Make it work properly + unimplemented( + gb_type="setattr() on Tensor.requires_grad", + context=f"setattr({obj}, {name}, {val})", + explanation="setattr() on Tensor.requires_grad not supported. " + "Mutating requires_grad can introduce a new leaf from non-leaf or vice versa in " + "the middle of the graph, which AOTAutograd does not currently know how to handle.", + hints=[*graph_break_hints.SUPPORTABLE], + ) + elif name == "data": + # See comments on `test_set_data_on_scoped_tensor` for plans + # to support this. + if obj.source is None: + unimplemented( + gb_type="Failed to mutate tensor data attribute", + context=f"setattr({obj}, {name}, {val})", + explanation="Dyanmo only supports mutating `.data`" + " of tensor created outside `torch.compile` region", + hints=[ + "Don't mutate `.data` on this tensor, or move " + "the mutation out of `torch.compile` region", + ], + ) + elif obj.dtype != val.dtype: # type: ignore[attr-defined] + unimplemented( + gb_type="Failed to mutate tensor data attribute to different dtype", + context=f"setattr({obj}, {name}, {val})", + explanation="Dyanmo only supports mutating `.data`" + " of tensor to a new one with the same dtype", + hints=[ + "Don't mutate `.data` on this tensor, or move " + "the mutation out of `torch.compile` region", + ], + ) + + # Remove the old reference in tracked fakes - if we don't do this + # new .data value size and shape differences will cause + # tracked fakes to produce incorrect guards. This is sound because the TensorVariable + # coming out of set_() below will be a new one, and get + # installed in tracked fakes. + to_remove = [ + tf for tf in tx.output.tracked_fakes if tf.source == obj.source + ] + for tf in to_remove: + tx.output.tracked_fakes.remove(tf) + + # Step 1 - disable grads + with dynamo_disable_grad(tx), torch.no_grad(): + # Step 2 - call `set_` + out = wrap_fx_proxy( + tx, + tx.output.create_proxy( + "call_function", + torch.Tensor.set_, + *proxy_args_kwargs([obj, val], {}), + ), + ) + + # Step 3 - drop the version counter - this is a step required to get + # .data setting to play correctly with the autograd engine. + # Essentially, dynamo is trying to faithfully preserve the (absurd) + # behavior of .data= from eager mode + def _lower_version_count_by_1(x: torch.Tensor) -> torch.Tensor: + version = x._version + if version > 0: + version = version - 1 + torch._C._autograd._unsafe_set_version_counter((x,), (version,)) + return x + + tx.output.create_proxy( + "call_function", + _lower_version_count_by_1, + (out.as_proxy(),), + {}, + ) + _lower_version_count_by_1(obj.as_proxy().node.meta["example_value"]) + # This handles options prop, guards and ends with a clone + # Step 4 - replace all reference to the current object with the new one + return out + elif name in ("_grad", "grad"): + # NOTE: [Tensor "grad" and "_grad" attr] + # _grad and grad share the same setter/getter, see + # THPVariable_properties, and here we make sure setting one + # enables reading `val` from the other, by routing all + # read/write to `grad`. + name = "grad" + elif is_tensor_getset_descriptor(name): + # Attribute like `torch.Tensor.real` has special setters we + # don't yet support; it's not as simple adding an entry to + # the side effect mapping. + unimplemented( + gb_type="Failed to set tensor attribute", + context=f"setattr({obj}, {name}, {val})", + explanation="Dyanmo doesn't support setting these tensor attributes", + hints=[ + f"Don't mutate attribute '{name}' on tensors, or " + "move the mutation out of `torch.compile` region", + ], + ) + + tx.output.side_effects.store_attr(obj, name, val) + return val + elif isinstance(obj, variables.NNModuleVariable): + if not tx.output.is_root_tracer(): + unimplemented( + gb_type="nn.Module mutation in HigherOrderOp", + context=f"nn.Module: {obj}", + explanation="Inplace modifying nn.Module params/buffers inside HigherOrderOps is not allowed.", + hints=[ + "Remove the mutation or move it outside of the HigherOrderOp.", + *graph_break_hints.FUNDAMENTAL, + ], + ) + if name_var.is_python_constant() and isinstance( + val, variables.TensorVariable + ): + assigning_fake_val = get_fake_value(val.as_proxy().node, tx) + + try: + getattr_var = obj.var_getattr(tx, name_var.as_python_constant()) + except (AttributeError, ObservedAttributeError): + getattr_var = None + + if getattr_var is not None and getattr_var.is_tensor(): + # get_fake_val will get the same fake tensor + existing_fake_attr = get_fake_value(getattr_var.as_proxy().node, tx) + + # same tensor identity, setattr is a no-op + mod_setattr = inspect.getattr_static(obj.module_type, "__setattr__") + if ( + existing_fake_attr is assigning_fake_val + and mod_setattr is torch.nn.Module.__setattr__ + ): + return getattr_var + + obj.convert_to_unspecialized(tx) + return None + + def call_delattr( + self, + tx: "InstructionTranslator", + obj: VariableTracker, + name_var: VariableTracker, + ) -> VariableTracker: + return obj.call_method(tx, "__delattr__", [name_var], {}) + + def call_type( + self, tx: "InstructionTranslator", obj: VariableTracker + ) -> VariableTracker: + try: + py_type = obj.python_type() + except NotImplementedError as error: + raise UserError( + UserErrorType.INVALID_INPUT, + str(error), + case_name="unknown_python_type", + ) from None + + source = obj.source and TypeSource(obj.source) + if ( + source is None + and isinstance(obj, variables.UserDefinedObjectVariable) + and obj.cls_source + ): + source = obj.cls_source + if py_type is torch.Tensor: + # In some cases torch isn't available in globals + name = tx.output.install_global_by_id("", torch) + source = AttrSource(GlobalSource(name), "Tensor") + + return VariableTracker.build(tx, py_type, source) + + def call_reversed( + self, tx: "InstructionTranslator", obj: VariableTracker + ) -> VariableTracker | None: + if obj.has_unpack_var_sequence(tx): + items = list(reversed(obj.unpack_var_sequence(tx))) + return variables.TupleVariable(items) + return None + + def call_sorted( + self, + tx: "InstructionTranslator", + obj: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker | None: + if obj.has_force_unpack_var_sequence(tx) and not isinstance( + obj, variables.TensorVariable + ): + list_var = variables.ListVariable( + obj.force_unpack_var_sequence(tx), + mutation_type=ValueMutationNew(), + ) + list_var.call_method(tx, "sort", [], kwargs) + return list_var + return None + + # neg is a constant fold function, so we only get here if constant fold is not valid + def call_neg( + self, tx: "InstructionTranslator", a: VariableTracker + ) -> VariableTracker | None: + if isinstance(a, SymNodeVariable): + return SymNodeVariable.create( + tx, + (operator.neg)(a.as_proxy()), + sym_num=None, + ) + + if ( + isinstance(a, UserDefinedObjectVariable) + and a.call_obj_hasattr(tx, "__neg__").value # type: ignore[attr-defined] + ): + return a.call_method(tx, "__neg__", [], {}) + + # None no-ops this handler and lets the driving function proceed + return None + + def call_format( + self, + tx: "InstructionTranslator", + _format_string: VariableTracker, + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + format_string = _format_string.as_python_constant() + format_string = str(format_string) + return variables.StringFormatVariable.create(format_string, args, kwargs) + + def call_id( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + if len(args) > 0 and isinstance(args[0], variables.NNModuleVariable): + nn_mod_variable = args[0] + mod = tx.output.get_submodule(nn_mod_variable.module_key) + return VariableTracker.build(tx, id(mod)) + elif len(args) == 1 and args[0].is_tensor(): + tensor_variable = cast(TensorVariable, args[0]) + return tensor_variable.call_id(tx) + elif istype(args[0], variables.FunctoolsPartialVariable): + return VariableTracker.build(tx, id(args[0].fake_value)) + elif len(args) == 1: + arg = args[0] + if isinstance( + arg, + ( + variables.UserDefinedClassVariable, + variables.UserDefinedObjectVariable, + ), + ): + if arg.source: + if isinstance(arg, variables.UserDefinedClassVariable): + install_guard(arg.source.make_guard(GuardBuilder.CLASS_MATCH)) + else: + install_guard(arg.source.make_guard(GuardBuilder.ID_MATCH)) + real_val = arg.get_real_python_backed_value() + if real_val is not NO_SUCH_SUBOBJ: + return VariableTracker.build(tx, id(real_val)) + return FakeIdVariable(id(arg)) + else: + unimplemented( + gb_type="id() with unsupported args", + context=str(args), + explanation=f"Dynamo doesn't know how to trace id() call with args {args}", + hints=[ + "Supported args are Tensors, and functions/nn.Modules/user-defined objects " + "from outside the compiled region.", + *graph_break_hints.SUPPORTABLE, + ], + ) + + def call_deepcopy( + self, tx: "InstructionTranslator", x: VariableTracker + ) -> VariableTracker: + unimplemented( + gb_type="copy.deepcopy()", + context=f"copy.deepcopy({x})", + explanation="Dynamo does not support copy.deepcopy()", + hints=[ + "Avoid calling copy.deepcopy()", + *graph_break_hints.SUPPORTABLE, + ], + ) + + def _comparison_with_tensor( + self, tx: "InstructionTranslator", left: VariableTracker, right: VariableTracker + ) -> VariableTracker: + from .builder import wrap_fx_proxy_cls + from .tensor import supported_tensor_comparison_op_values + + op = self.fn + + if op in [operator.is_, operator.is_not]: + is_result = ( + left.is_tensor() + and right.is_tensor() + and id(extract_fake_example_value(left.as_proxy().node)) + == id(extract_fake_example_value(right.as_proxy().node)) + ) + if op is operator.is_: + return VariableTracker.build(tx, is_result) + else: + return VariableTracker.build(tx, not is_result) + + if op not in supported_tensor_comparison_op_values: + unimplemented( + gb_type="unsupported Tensor comparison op", + context=f"{op.__name__}({left}, {right})", + explanation=f"Dynamo does not support the comparison op {op.__name__} " + f"with Tensor arguments {left}, {right}", + hints=[*graph_break_hints.SUPPORTABLE], + ) + if ( + isinstance(left, TensorVariable) + and isinstance(right, TensorVariable) + and (left.size and right.size) is not None + and left.size != right.size + ): + try: + torch.broadcast_shapes(left.size, right.size) + except RuntimeError: + # not broadcastable, can't be compared + unimplemented( + gb_type="failed to broadcast when attempting Tensor comparison op", + context=f"{op.__name__}({left}, {right})", + explanation=f"Dynamo was unable to broad cast the arguments {left}, {right} " + f"when attempting to trace the comparison op {op.__name__}.", + hints=[*graph_break_hints.USER_ERROR], + ) + tensor_cls = left if left.is_tensor() else right + proxy = tx.output.create_proxy( + "call_function", op, (left.as_proxy(), right.as_proxy()), {} + ) + return wrap_fx_proxy_cls( + type(tensor_cls), # handle Ndarrays and Tensors + tx, + proxy, + ) + + def _comparison_with_symnode( + self, tx: "InstructionTranslator", left: VariableTracker, right: VariableTracker + ) -> VariableTracker: + from .tensor import supported_tensor_comparison_op_values + + op = self.fn + + if op not in supported_tensor_comparison_op_values: + unimplemented( + gb_type="unsupported SymNode comparison op", + context=f"{op.__name__}({left}, {right})", + explanation=f"Dynamo does not support the comparison op {op.__name__} " + f"with SymNode arguments {left}, {right}", + hints=[*graph_break_hints.SUPPORTABLE], + ) + + # SymNodes are numeric (int/float/bool). The non-SymNode operand + # must be a type that can participate in a traced numeric comparison. + # Anything else (classes, DataPtrVariable, etc.) is a different type + # entirely — the comparison result is known at compile time. + non_symnode = right if isinstance(left, SymNodeVariable) else left + if not isinstance( + non_symnode, (SymNodeVariable, ConstantVariable, TensorVariable) + ): + # pyrefly: ignore [bad-argument-type] + return VariableTracker.build(tx, op(object(), None)) + + proxy = tx.output.create_proxy( + "call_function", op, (left.as_proxy(), right.as_proxy()), {} + ) + return SymNodeVariable.create( + tx, + proxy, + sym_num=None, + ) + + def call_xor( + self, tx: "InstructionTranslator", a: VariableTracker, b: VariableTracker + ) -> VariableTracker | None: + # Rely on constant_handler + if isinstance(a, ConstantVariable) and isinstance(b, ConstantVariable): + return None + if a.is_symnode_like() and b.is_symnode_like(): + return SymNodeVariable.create( + tx, + tx.output.create_proxy( + "call_function", operator.xor, *proxy_args_kwargs([a, b], {}) + ), + sym_num=None, + ) + + if isinstance(a, _SET_LIKE_OP_SUPPORT): + return a.call_method(tx, "__xor__", [b], {}) + return None + + def call_ixor( + self, tx: "InstructionTranslator", a: VariableTracker, b: VariableTracker + ) -> VariableTracker | None: + if isinstance(a, _SET_LIKE_OP_SUPPORT): + return a.call_method(tx, "__ixor__", [b], {}) + return None + + def call_sub( + self, tx: "InstructionTranslator", a: VariableTracker, b: VariableTracker + ) -> VariableTracker | None: + if isinstance(a, _SET_LIKE_OP_SUPPORT): + return a.call_method(tx, "__sub__", [b], {}) + return None + + def call_isub( + self, tx: "InstructionTranslator", a: VariableTracker, b: VariableTracker + ) -> VariableTracker | None: + if isinstance(a, _SET_LIKE_OP_SUPPORT): + return a.call_method(tx, "__isub__", [b], {}) + return None + + def call_and_( + self, tx: "InstructionTranslator", a: VariableTracker, b: VariableTracker + ) -> VariableTracker | None: + # Rely on constant_handler + if isinstance(a, ConstantVariable) and isinstance(b, ConstantVariable): + return None + if a.is_symnode_like() and b.is_symnode_like(): + return SymNodeVariable.create( + tx, + tx.output.create_proxy( + "call_function", operator.and_, *proxy_args_kwargs([a, b], {}) + ), + sym_num=None, + ) + if isinstance(a, _SET_LIKE_OP_SUPPORT): + return a.call_method(tx, "__and__", [b], {}) + # None no-ops this handler and lets the driving function proceed + return None + + def call_iand( + self, tx: "InstructionTranslator", a: VariableTracker, b: VariableTracker + ) -> VariableTracker | None: + # Rely on constant_handler + if isinstance(a, ConstantVariable) and isinstance(b, ConstantVariable): + return None + if a.is_symnode_like() and b.is_symnode_like(): + return SymNodeVariable.create( + tx, + tx.output.create_proxy( + "call_function", operator.iand, *proxy_args_kwargs([a, b], {}) + ), + sym_num=None, + ) + if isinstance(a, _SET_LIKE_OP_SUPPORT): + return a.call_method(tx, "__iand__", [b], {}) + return None + + def call_or_( + self, tx: "InstructionTranslator", a: VariableTracker, b: VariableTracker + ) -> VariableTracker | None: + # Rely on constant_handler + if isinstance(a, ConstantVariable) and isinstance(b, ConstantVariable): + return None + + # Constant fold or_ for class/type variables (e.g. Shard | _StridedShard + # producing a types.UnionType for isinstance checks). This handles cases + # like OpaqueObjectClassVariable where is_python_constant() returns False + # but as_python_constant() works. + try: + a_const = a.as_python_constant() + b_const = b.as_python_constant() + if isinstance(a_const, type) and isinstance(b_const, type): + return VariableTracker.build(tx, a_const | b_const) + except NotImplementedError: + pass + + if a.is_symnode_like() and b.is_symnode_like(): + return SymNodeVariable.create( + tx, + tx.output.create_proxy( + "call_function", operator.or_, *proxy_args_kwargs([a, b], {}) + ), + sym_num=None, + ) + + # This call looks like `{"one": torch.ones(1)} | {"two": torch.ones(2)}`. + if isinstance( + a, + ( + *_SET_LIKE_OP_SUPPORT, + ConstDictVariable, + MutableMappingVariable, + UserDefinedDictVariable, + ), + ): + # TODO(guilhermeleobas): forward the call to b.__ror__(a) if + # a.__ror__(b) returns NotImplemented + return a.call_method(tx, "__or__", [b], {}) + + # None no-ops this handler and lets the driving function proceed + return None + + def call_ior( + self, tx: "InstructionTranslator", a: VariableTracker, b: VariableTracker + ) -> VariableTracker | None: + # Rely on constant_handler + if isinstance(a, ConstantVariable) and isinstance(b, ConstantVariable): + return None + if a.is_symnode_like() and b.is_symnode_like(): + return SymNodeVariable.create( + tx, + tx.output.create_proxy( + "call_function", operator.ior, *proxy_args_kwargs([a, b], {}) + ), + sym_num=None, + ) + + # This call looks like `{"one": torch.ones(1)} |= {"two": torch.ones(2)}`. + if isinstance( + a, + ( + *_SET_LIKE_OP_SUPPORT, + ConstDictVariable, + MutableMappingVariable, + ), + ): + return a.call_method(tx, "__ior__", [b], {}) + + # None no-ops this handler and lets the driving function proceed + return None + + def call_not_( + self, tx: "InstructionTranslator", a: VariableTracker + ) -> VariableTracker | None: + if isinstance(a, SymNodeVariable): + return SymNodeVariable.create( + tx, + tx.output.create_proxy( + "call_function", operator.not_, *proxy_args_kwargs([a], {}) + ), + sym_num=None, + ) + + # Unwrap the underlying ConstDictVariable + if isinstance(a, DictViewVariable): + a = a.dv_dict + if isinstance(a, (ListVariable, ConstDictVariable, SetVariable)): + return VariableTracker.build(tx, len(a.items) == 0) + if isinstance(a, UserDefinedObjectVariable): + bool_result = self.call_bool(tx, a) + return VariableTracker.build(tx, not bool_result.value) # type: ignore[missing-attribute] + + return None + + def call_contains( + self, tx: "InstructionTranslator", a: VariableTracker, b: VariableTracker + ) -> VariableTracker: + return a.call_method(tx, "__contains__", [b], {}) + + def is_python_equal(self, other: object) -> bool: + return isinstance(other, variables.BuiltinVariable) and self.fn is other.fn + + +class DictBuiltinVariable(BaseBuiltinVariable): + """Variable tracker for the `dict` builtin constructor.""" + + _fn = dict + + def __init__(self, value: type = dict, **kwargs: Any) -> None: + assert value is dict + super().__init__(**kwargs) + + def __repr__(self) -> str: + return "DictBuiltinVariable()" + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + return DictBuiltinVariable.call_custom_dict(tx, dict, *args, **kwargs) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "__new__": + if len(args) == 1 and not kwargs: + dict_vt = ConstDictVariable({}, dict, mutation_type=ValueMutationNew()) + if isinstance(args[0], DictBuiltinVariable): + return dict_vt + return tx.output.side_effects.track_new_user_defined_object( + self, args[0], args[1:] + ) + + if name == "fromkeys": + return DictBuiltinVariable.call_custom_dict_fromkeys( + tx, dict, *args, **kwargs + ) + + resolved_fn = getattr(dict, name, None) + if resolved_fn is not None and resolved_fn in dict_methods: + if isinstance(args[0], variables.UserDefinedDictVariable): + assert args[0]._base_vt is not None + return args[0]._base_vt.call_method(tx, name, args[1:], kwargs) + elif isinstance(args[0], ConstDictVariable): + return args[0].call_method(tx, name, args[1:], kwargs) + + return super().call_method(tx, name, args, kwargs) + + @staticmethod + def call_custom_dict( + tx: "InstructionTranslator", + user_cls: type, + /, + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + args_list = list(args) + return tx.inline_user_function_return( + VariableTracker.build(tx, polyfills.construct_dict), + [VariableTracker.build(tx, user_cls), *args_list], + kwargs, + ) + + @staticmethod + def call_custom_dict_fromkeys( + tx: "InstructionTranslator", + user_cls: type, + /, + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + if user_cls not in {dict, OrderedDict, defaultdict}: + unimplemented( + gb_type="Unsupported dict type for fromkeys()", + context=f"{user_cls.__name__}.fromkeys(): {args} {kwargs}", + explanation=f"Failed to call {user_cls.__name__}.fromkeys() because " + f"{user_cls.__name__} is not any type of dict, OrderedDict, or defaultdict", + hints=[ + f"Ensure {user_cls.__name__} is a type of dict, OrderedDict, or defaultdict.", + ], + ) + if kwargs: + # Only `OrderedDict.fromkeys` accepts `value` passed by keyword + if ( + user_cls is not OrderedDict + or len(args) != 1 + or len(kwargs) != 1 + or "value" not in kwargs + ): + raise_args_mismatch( + tx, + f"{user_cls.__name__}.fromkeys", + "1 args and 1 kwargs (`value`)", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + args = (*args, kwargs.pop("value")) + if len(args) == 0: + raise_args_mismatch( + tx, + f"{user_cls.__name__}.fromkeys", + "at least 1 args", + f"{len(args)} args", + ) + if len(args) == 1: + args = (*args, ConstantVariable.create(None)) + if len(args) != 2: + raise_args_mismatch( + tx, + f"{user_cls.__name__}.fromkeys", + "2 args", + f"{len(args)} args", + ) + + arg, value = args + DictVariableType = ( + ConstDictVariable if user_cls is not defaultdict else DefaultDictVariable + ) + + if isinstance(arg, dict): + arg_list = [VariableTracker.build(tx, k) for k in arg] + return DictVariableType( + dict.fromkeys(arg_list, value), + user_cls, + mutation_type=ValueMutationNew(), + ) + elif arg.has_force_unpack_var_sequence(tx): + keys = arg.force_unpack_var_sequence(tx) + if all(is_hashable(v) for v in keys): + return DictVariableType( + dict.fromkeys(keys, value), + user_cls, + mutation_type=ValueMutationNew(), + ) + + unimplemented( + gb_type="failed to call dict.fromkeys()", + context=f"{user_cls.__name__}.fromkeys(): {args} {kwargs}", + explanation=f"Failed to call {user_cls.__name__}.fromkeys() because " + "arguments could not be automatically converted to a list, " + "or some dict key is not hashable.", + hints=[ + "Manually convert the argument to a list.", + "Ensure all keys are hashable.", + ], + ) + + +class IterBuiltinVariable(BaseBuiltinVariable): + """Variable tracker for the `iter` builtin.""" + + _fn = iter + + def __init__(self, value: Any = iter, **kwargs: Any) -> None: + assert value is iter + super().__init__(**kwargs) + + def __repr__(self) -> str: + return "IterBuiltinVariable()" + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if not args: + unimplemented( + gb_type="iter() with no arguments", + context="iter()", + explanation="iter() requires at least one argument", + hints=[*graph_break_hints.USER_ERROR], + ) + obj, *rest = args + + # Fast path: for known iterable VT types, call __iter__ directly + # instead of going through the polyfill, saving tracing overhead. + if ( + not rest + and not kwargs + and isinstance( + obj, + ( + variables.ListVariable, + variables.RangeVariable, + variables.IteratorVariable, + variables.ConstDictVariable, + variables.NNModuleVariable, + variables.TensorVariable, + variables.TupleVariable, + variables.UserDefinedClassVariable, + DictViewVariable, + ), + ) + ): + return obj.call_method(tx, "__iter__", [], {}) + + # General case: inline the polyfill which handles __iter__ and __getitem__ + ret = variables.UserFunctionVariable( + polyfills.builtins.iter_ # type: ignore[arg-type] + ).call_function(tx, [obj, *rest], {}) + + if rest: + # iter(obj, sentinel) returns a callable iterator; wrap it so + # Dynamo knows to forward __next__ calls to the returned object. + ret = variables.ObjectIteratorVariable(ret) + return ret + + +class ListBuiltinVariable(BaseBuiltinVariable): + """Variable tracker for the `list` builtin constructor.""" + + _fn = list + + def __init__(self, value: type = list, **kwargs: Any) -> None: + assert value is list + super().__init__(**kwargs) + + def __repr__(self) -> str: + return "ListBuiltinVariable()" + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .user_defined import UserDefinedObjectVariable + + obj = args[0] if args else None + + if isinstance( + obj, (variables.IteratorVariable, variables.LocalGeneratorObjectVariable) + ) or ( + isinstance(obj, UserDefinedObjectVariable) + and obj.has_force_unpack_var_sequence(tx) + ): + return ListVariable( + list(obj.force_unpack_var_sequence(tx)), + mutation_type=ValueMutationNew(), + ) + + if obj is None: + return ListVariable([], mutation_type=ValueMutationNew()) + + if obj.has_unpack_var_sequence(tx): + if obj.source and not is_constant_source(obj.source): + if isinstance(obj, TupleIteratorVariable): + install_guard( + obj.source.make_guard(GuardBuilder.TUPLE_ITERATOR_LEN) + ) + else: + if isinstance(obj, ConstDictVariable) and not istype( + obj, (SetVariable, FrozensetVariable) + ): + tx.output.guard_on_key_order.add(obj.source) + if isinstance(obj, variables.MappingProxyVariable): + install_guard( + obj.source.make_guard(GuardBuilder.MAPPING_KEYS_CHECK) + ) + elif not isinstance(obj, variables.UnspecializedNNModuleVariable): + install_guard( + obj.source.make_guard(GuardBuilder.SEQUENCE_LENGTH) + ) + return ListVariable( + list(obj.unpack_var_sequence(tx)), + mutation_type=ValueMutationNew(), + ) + + arg_types = [type(a).__name__ for a in args] + unimplemented( + gb_type="Failed to trace list()", + context=f"list({arg_types})", + explanation=f"Dynamo does not know how to construct a list from argument types {arg_types}", + hints=[*graph_break_hints.SUPPORTABLE], + ) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "__new__": + if len(args) == 1 and not kwargs: + list_vt = ListVariable([], mutation_type=ValueMutationNew()) + if isinstance(args[0], ListBuiltinVariable): + return list_vt + return tx.output.side_effects.track_new_user_defined_object( + self, args[0], args[1:] + ) + + return super().call_method(tx, name, args, kwargs) + + +# pyrefly: ignore [deprecated] +@contextlib.contextmanager +def dynamo_disable_grad(tx: "InstructionTranslator") -> typing.Iterator[None]: + from . import GradModeVariable + + gmv = GradModeVariable.create(tx, False) + try: + gmv.enter(tx) + yield + finally: + gmv.exit(tx) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/constant.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/constant.py new file mode 100644 index 0000000000000000000000000000000000000000..c8061f4e7336a64d7bfd0333428a53a3a958ab42 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/constant.py @@ -0,0 +1,482 @@ +""" +Constant variable tracking in Dynamo. + +This module is fundamental to Dynamo's ability to track and propagate constant +values during compilation, ensuring proper handling of Python literals and +maintaining type safety through the compilation process. +""" + +from __future__ import annotations + +import operator +from typing import Any, Literal, overload, TYPE_CHECKING +from typing_extensions import override + +import torch +from torch._dynamo.source import GetItemSource + +from .. import variables +from ..exc import raise_observed_exception, unimplemented +from ..utils import common_constant_types, istype, np, raise_args_mismatch +from .base import ValueMutationNew, VariableTracker + + +if TYPE_CHECKING: + from collections.abc import Sequence + + from torch._dynamo.symbolic_convert import InstructionTranslator + + from .functions import UserFunctionVariable + + +class ConstantVariable(VariableTracker): + """ + Variable tracker for Python literals and basic immutable types, with automatic + routing support for collection types (lists, tuples, sets, etc.). + + The create() method intelligently constructs appropriate variable types for + nested collections. + """ + + # PyLong_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/longobject.c#L6585 + # PyFloat_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/floatobject.c#L1880 + # PyBool_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/boolobject.c#L171 + # PyUnicode_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/unicodeobject.c#L14931 + # PyBytes_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/bytesobject.c#L3017 + # PyComplex_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/complexobject.c#L1099 + # _PyNone_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/object.c#L2022 + _cpython_type = (int, float, str, bytes, bool, type(None), complex, type(...)) + + @overload + @staticmethod + def create(value: None) -> ConstantVariable: ... + + @overload + @staticmethod + def create(value: bool) -> ConstantVariable: ... + + @overload + @staticmethod + def create(value: Any, **kwargs: Any) -> VariableTracker: ... + + @staticmethod + def create(value: Any, **kwargs: Any) -> VariableTracker: + """ + Create a `ConstantVariable` based on the given value, and supports + automatic routing for collection types like `tuple` (in which case we'd + create `ConstantVariable` for the leaf items). + + NOTE: the caller must install the proper guards if needed; most often + the guard will be `CONSTANT_MATCH`. + """ + # Return pre-allocated sentinels for None/True/False when there are + # no extra kwargs (source, etc.) that would differentiate the instance. + if not kwargs: + match value: + case None: + return CONSTANT_VARIABLE_NONE + case True: + return CONSTANT_VARIABLE_TRUE + case False: + return CONSTANT_VARIABLE_FALSE + + source = kwargs.get("source") + + # Routing for supported collection literals. + if isinstance(value, set): + items = [ConstantVariable.create(x) for x in value] + return variables.SetVariable(items, **kwargs) # type: ignore[arg-type] + elif isinstance(value, frozenset): + items = [ConstantVariable.create(x) for x in value] + return variables.FrozensetVariable(items, **kwargs) # type: ignore[arg-type] + elif isinstance(value, slice): + slice_args = (value.start, value.stop, value.step) + slice_args_vars = tuple(ConstantVariable.create(arg) for arg in slice_args) + return variables.SliceVariable(slice_args_vars, **kwargs) + elif isinstance(value, (list, tuple)): + items = [] + for i, x in enumerate(value): + item_source = GetItemSource(source, i) if source else None + items.append( + ConstantVariable.create( + x, + source=item_source, + ) + ) + return variables.BaseListVariable.cls_for(type(value))(items, **kwargs) + + return ConstantVariable(value, **kwargs) + + def __init__(self, value: Any, **kwargs: Any) -> None: + super().__init__(**kwargs) + assert ConstantVariable.is_base_literal(value), f""" +Cannot construct `ConstantVariable` for value of type {type(value)}. + +This failure likely due to PyTorch-internal use of `ConstantVariable` on +non-literal python values, please try using `VariableTracker.build` instead. If +you believe it's a necessary and legitimate use case (the value is immutable and +can't easily be represented with another `VariableTracker` class), please add +its type to `common_constant_types`. +""" + if np is not None and isinstance(value, np.number): + self.value = value.item() + else: + self.value = value + + def as_proxy(self) -> Any: + return self.value + + def __repr__(self) -> str: + return f"ConstantVariable({type(self.value).__name__}: {repr(self.value)})" + + def as_python_constant(self) -> Any: + return self.value + + def is_python_constant(self) -> Literal[True]: + return True + + def is_symnode_like(self) -> bool: + return isinstance(self.value, (int, bool)) + + def is_constant_match(self, *values: Any) -> bool: + return self.value in values + + def is_constant_none(self) -> bool: + return self.value is None + + @property + def items(self) -> list[VariableTracker]: + """ + Need this when adding a BaseListVariable and a ConstantVariable together. + Happens in detectron2. + """ + return self.unpack_var_sequence(tx=None) + + def getitem_const( + self, tx: InstructionTranslator, arg: VariableTracker + ) -> VariableTracker: + return ConstantVariable.create( + self.value[arg.as_python_constant()], + ) + + @staticmethod + def is_base_literal(obj: object) -> bool: + return type(obj) in common_constant_types + + @staticmethod + def is_literal(obj: object, cache: dict[int, object] | None = None) -> bool: + if cache is None: + cache = {} + if id(obj) in cache: + # no-op if there is a cyclical reference + return True + if type(obj) in (list, tuple, set, frozenset, torch.Size): + cache[id(obj)] = obj + return all(ConstantVariable.is_literal(x, cache) for x in obj) # type: ignore[attr-defined] + return ConstantVariable.is_base_literal(obj) + + def unpack_var_sequence( + self, tx: InstructionTranslator | None + ) -> list[VariableTracker]: + try: + return [ConstantVariable.create(x) for x in self.as_python_constant()] + except TypeError as e: + raise NotImplementedError from e + + def len_impl(self, tx: InstructionTranslator) -> VariableTracker: + """Generic len for any constant value (sequence or mapping).""" + try: + return ConstantVariable.create(len(self.value)) + except TypeError as e: + raise_observed_exception(type(e), tx, args=list(e.args)) + + def sq_length(self, tx: InstructionTranslator) -> VariableTracker: + """Sequence length - delegates to len_impl for constants.""" + return self.len_impl(tx) + + def mp_length(self, tx: InstructionTranslator) -> VariableTracker: + """Mapping length - delegates to len_impl for constants.""" + return self.len_impl(tx) + + def const_getattr(self, tx: InstructionTranslator, name: str) -> VariableTracker: + if not hasattr(self.value, name): + raise_observed_exception(AttributeError, tx, args=[name]) + member = getattr(self.value, name) + if callable(member): + raise NotImplementedError + return member + + def call_method( + self, + tx: InstructionTranslator, + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .tensor import SymNodeVariable + + if name == "format" and istype(self.value, str): + return variables.BuiltinVariable(str.format).call_function( + tx, [self, *args], kwargs + ) + elif name == "join" and istype(self.value, str): + if kwargs or len(args) != 1: + raise_args_mismatch( + tx, + name, + "1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + arg_unpacked = args[0].force_unpack_var_sequence(tx) + try: + arg_const = [x.as_python_constant() for x in arg_unpacked] + return ConstantVariable.create(self.value.join(arg_const)) + except NotImplementedError: + return super().call_method(tx, name, args, kwargs) + elif name == "__iter__" and istype(self.value, str): + # this could be some generic iterator to avoid the circular import, + # but ListIterator does what we want + from .lists import ListIteratorVariable + + return ListIteratorVariable( + self.unpack_var_sequence(tx), mutation_type=ValueMutationNew() + ) + + if any(isinstance(x, SymNodeVariable) for x in args): + # Promote to SymNodeVariable for operations involving dynamic shapes. + return variables.SymNodeVariable.create( + tx, self.as_proxy(), self.value + ).call_method(tx, name, args, kwargs) + + try: + const_args = [a.as_python_constant() for a in args] + const_kwargs = {k: v.as_python_constant() for k, v in kwargs.items()} + except NotImplementedError: + return super().call_method(tx, name, args, kwargs) + + if isinstance(self.value, str) and name in str.__dict__: + method = getattr(self.value, name) + try: + return ConstantVariable.create(method(*const_args, **const_kwargs)) + except Exception as e: + raise_observed_exception(type(e), tx) + elif isinstance(self.value, (float, int)) and hasattr(self.value, name): + if not (args or kwargs): + try: + return ConstantVariable.create(getattr(self.value, name)()) + except (OverflowError, ValueError) as exc: + raise_observed_exception( + type(exc), + tx, + args=list(exc.args), + ) + if ( + hasattr(operator, name) + and len(args) == 1 + and args[0].is_python_constant() + ): + add_target = const_args[0] + op = getattr(operator, name) + if isinstance( + add_target, (torch.SymBool, torch.SymFloat, torch.SymInt) + ): + # Addition between a non sym and sym makes a sym + proxy = tx.output.create_proxy( + "call_function", op, (self.value, add_target), {} + ) + return SymNodeVariable.create(tx, proxy, add_target) + else: + try: + return ConstantVariable.create(op(self.value, add_target)) + except Exception as e: + raise_observed_exception(type(e), tx, args=list(e.args)) + elif isinstance(self.value, bytes) and name == "decode": + method = getattr(self.value, name) + return ConstantVariable.create(method(*const_args, **const_kwargs)) + elif type(self.value) is complex and name in complex.__dict__: + method = getattr(self.value, name) + try: + return ConstantVariable.create(method(*const_args, **const_kwargs)) + except Exception as e: + raise_observed_exception(type(e), tx) + + if name == "__round__" and len(args) == 1 and args[0].is_python_constant(): + try: + return ConstantVariable.create( + round(self.value, args[0].as_python_constant()) + ) + except Exception as e: + raise_observed_exception(type(e), tx, args=list(e.args)) + elif name == "__contains__" and len(args) == 1 and args[0].is_python_constant(): + assert not kwargs + search = args[0].as_python_constant() + try: + result = search in self.value + return ConstantVariable.create(result) + except TypeError as e: + raise_observed_exception(type(e), tx, args=list(e.args)) + return super().call_method(tx, name, args, kwargs) + + def call_tree_map( + self, + tx: InstructionTranslator, + tree_map_fn: UserFunctionVariable, + map_fn: VariableTracker, + rest: Sequence[VariableTracker], + tree_map_kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if self.value is None: + none_is_leaf_var = tree_map_kwargs.get("none_is_leaf") + if none_is_leaf_var is not None: + try: + none_is_leaf = bool(none_is_leaf_var.as_python_constant()) + except NotImplementedError: + return self._tree_map_fallback( + tx, + tree_map_fn, + map_fn, + rest, + tree_map_kwargs, + ) + else: + tree_map_module = getattr( + getattr(tree_map_fn, "fn", None), "__module__", "" + ) + # torch.utils._pytree and torch.utils._cxx_pytree treat None as a leaf + # by default, while optree keeps it as an internal node unless + # none_is_leaf=True is provided. + none_is_leaf = not tree_map_module.startswith("optree") + if none_is_leaf: + return map_fn.call_function(tx, [self, *rest], {}) + else: + for other in rest: + if not other.is_constant_none(): + return self._tree_map_fallback( + tx, + tree_map_fn, + map_fn, + rest, + tree_map_kwargs, + ) + return self.clone() + if isinstance(self.value, (int, float, bool, complex, str, bytes, torch.dtype)): + return map_fn.call_function(tx, [self, *rest], {}) + return super().call_tree_map( + tx, + tree_map_fn, + map_fn, + rest, + tree_map_kwargs, + ) + + @override + def call_obj_hasattr( + self, tx: InstructionTranslator, name: str + ) -> ConstantVariable: + result = hasattr(self.value, name) + return variables.ConstantVariable.create(result) + + def is_python_hashable(self) -> Literal[True]: + return True + + def get_python_hash(self) -> int: + return hash(self.value) + + def is_python_equal(self, other: object) -> bool: + from .tensor import SymNodeVariable + + if isinstance(other, SymNodeVariable): + return self.as_python_constant() == other.evaluate_expr() + return ( + isinstance(other, VariableTracker) + and self.as_python_constant() == other.as_python_constant() + ) + + def get_real_python_backed_value(self) -> object: + return self.value + + def nb_index_impl( + self, + tx: Any, + ) -> VariableTracker: + # CPython: int and bool define nb_index (returns self for int, + # int(self) for bool). All other constant types do not. + if isinstance(self.value, (int, bool)): + return ConstantVariable.create(operator.index(self.value)) + return super().nb_index_impl(tx) + + def nb_int_impl( + self, + tx: Any, + ) -> VariableTracker: + # CPython: int defines nb_int (long_long, returns copy). + # bool inherits nb_int from int via slot inheritance. + # float defines nb_int (truncates toward zero via PyLong_FromDouble). + return ConstantVariable.create(int(self.value)) + + def nb_float_impl( + self, + tx: Any, + ) -> VariableTracker: + # CPython: float defines nb_float (float_float, returns copy). + # int defines nb_float (long_float, converts to float). + # bool inherits nb_float from int via slot inheritance. + return ConstantVariable.create(float(self.value)) + + +CONSTANT_VARIABLE_NONE = ConstantVariable(None) +CONSTANT_VARIABLE_TRUE = ConstantVariable(True) +CONSTANT_VARIABLE_FALSE = ConstantVariable(False) + + +class FakeIdVariable(VariableTracker): + """A compile-time-only id value that can be used as a dict key but cannot + be reconstructed across graph breaks. + + When dynamo evaluates ``id(x)`` on a variable tracker that has no + corresponding runtime object (e.g. a ``ConstDictVariable`` created during + tracing), we mint a fake integer id. This variable holds that id and + supports the minimal interface needed to participate as a dict key + (hashing and equality). It intentionally blocks reconstruction so that a + graph break does not silently bake a stale id into the resumed bytecode. + """ + + # PyLong_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/longobject.c#L6585 + _cpython_type = int + + def __init__(self, value: int, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.value = value + + def as_python_constant(self) -> int: + return self.value + + def is_python_constant(self) -> bool: + return False + + def python_type(self) -> type: + return int + + def is_python_hashable(self) -> bool: + return True + + def get_python_hash(self) -> int: + return hash(self.value) + + def is_python_equal(self, other: object) -> bool: + if isinstance(other, (FakeIdVariable, ConstantVariable)): + return self.value == other.as_python_constant() + return False + + def reconstruct(self, codegen: Any) -> None: + unimplemented( + gb_type="Reconstruction of FakeIdVariable", + context=str(self.value), + explanation=( + "A fake id produced by id() on a compile-time container " + "cannot be reconstructed across a graph break." + ), + hints=[ + "Avoid using id() on containers in code that may graph-break.", + ], + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/ctx_manager.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/ctx_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..30ebc0ca8735b1598b2c5c9fdcbcf130a224d52e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/ctx_manager.py @@ -0,0 +1,1712 @@ +""" +This file contains a collection of context manager classes used by Dynamo for tracking +and managing various PyTorch runtime states during graph compilation. These context +managers handle different aspects of PyTorch's execution environment, including: + +- Autograd states (grad mode, inference mode) +- CUDA streams and events +- Profiling contexts +- Deterministic algorithms +- Forward/backward AD modes +- SDPA (Scaled Dot Product Attention) kernels +- FSDP (Fully Sharded Data Parallel) states +- AMP (Automatic Mixed Precision) autocast states + +The context managers ensure proper state transitions during graph compilation by +tracking enter/exit points and managing cleanup operations. They help maintain +consistency between eager execution and compiled graph behavior by capturing and +restoring state changes. +""" + +import contextlib +import inspect +import logging +import types +import warnings +from collections.abc import Callable, Sequence, Sized +from contextlib import AbstractContextManager, ExitStack +from typing import Any, TYPE_CHECKING + +import torch._C +from torch._dynamo import config +from torch._guards import Guard +from torch._logging import warning_once + +from .. import graph_break_hints, variables +from ..bytecode_transformation import create_call_function +from ..exc import unimplemented +from ..guards import GuardBuilder, install_guard +from ..source import AttrSource, GlobalStateSource +from ..utils import _get_error_on_graph_break, _set_error_on_graph_break +from .base import VariableTracker +from .functions import ( + NestedUserFunctionVariable, + SkipFunctionVariable, + UserFunctionVariable, + UserMethodVariable, + WrappedNestedUserFunctionVariable, + WrappedSkipFunctionVariable, + WrappedUserFunctionVariable, + WrappedUserMethodVariable, +) +from .user_defined import UserDefinedObjectVariable + + +if TYPE_CHECKING: + from torch._dynamo.codegen import PyCodegen + from torch._dynamo.symbolic_convert import InstructionTranslator + +log = logging.getLogger(__name__) + + +class ContextWrappingVariable(VariableTracker): + _nonvar_fields = { + "cm_obj", + "target_values", + "initial_values", + "state", + *VariableTracker._nonvar_fields, + } + + def __init__( + self, target_values: Any, initial_values: Any | None = None, **kwargs: Any + ) -> None: + super().__init__(**kwargs) + self.target_values = target_values + self.initial_values = initial_values + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + if hasattr(self, "_call_func"): + self._call_func(tx, self.target_values) + self.set_cleanup_hook(tx) + return variables.ConstantVariable.create(None) + + def set_cleanup_hook( + self, tx: "InstructionTranslator", fn: Callable[..., Any] | None = None + ) -> None: + if fn is None: + + def fn() -> None: + if hasattr(self, "_call_func"): + self._call_func(tx, self.initial_values) + + self.cleanup_fn: Callable[..., Any] | None = fn + tx.output.add_cleanup_hook(self.cleanup) + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + self.cleanup_assert() + return variables.ConstantVariable.create(None) + + def reconstruct_type(self, codegen: "PyCodegen") -> None: + codegen( + AttrSource(codegen.tx.import_source(self.module_name()), self.fn_name()) + ) + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null(lambda: self.reconstruct_type(codegen)) + target_values = self.target_values + if not target_values: + target_values = () + codegen.extend_output([codegen.create_load_const(val) for val in target_values]) + codegen.extend_output(create_call_function(len(target_values), False)) + + def module_name(self) -> str: + raise NotImplementedError("module_name called on base") + + def fn_name(self) -> str: + raise NotImplementedError("fn_name called on base") + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + assert len(args) == 1 + assert isinstance( + args[0], + ( + NestedUserFunctionVariable, + SkipFunctionVariable, + UserMethodVariable, + UserFunctionVariable, + ), + ) + + if isinstance(args[0], NestedUserFunctionVariable): + return WrappedNestedUserFunctionVariable(args[0], self) + elif isinstance(args[0], SkipFunctionVariable): + return WrappedSkipFunctionVariable(args[0], self) + elif isinstance(args[0], UserMethodVariable): + return WrappedUserMethodVariable(args[0], self) + elif isinstance(args[0], UserFunctionVariable): + return WrappedUserFunctionVariable(args[0], self) + else: + raise AssertionError("Unexpected arg type") + + def supports_graph_breaks(self) -> bool: + return True + + def exit_on_graph_break(self) -> bool: + return True + + def cleanup(self) -> None: + if self.cleanup_fn is not None: + self.cleanup_fn() + self.cleanup_fn = None + + def cleanup_assert(self) -> None: + assert self.cleanup_fn, "multiple exits?" + self.cleanup() + + +class GenericContextWrappingVariable(UserDefinedObjectVariable): + # Some methods in ContextWrappingVariable assumes the arguments are + # python constants. Which might not always be the case here. + def __init__(self, cm_obj: AbstractContextManager[Any], **kwargs: Any) -> None: + assert cm_obj is not None + super().__init__( + value=cm_obj, + value_type=cm_obj.__class__, + **kwargs, + ) + self.cm_obj = cm_obj + + def module_name(self) -> str: + return self.cm_obj.__module__ + + def fn_name(self) -> str: + return type(self.cm_obj).__name__ + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + source = None if self.source is None else AttrSource(self.source, "__enter__") + return variables.UserMethodVariable( + self.cm_obj.__enter__.__func__, # type: ignore[attr-defined] + self, + source=source, + ).call_function(tx, [], {}) + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + source = None if self.source is None else AttrSource(self.source, "__exit__") + x = variables.UserMethodVariable( + self.cm_obj.__exit__.__func__, # type: ignore[attr-defined] + self, + source=source, + ).call_function(tx, list(args), {}) + tx.active_generic_context_managers.pop() + return x + + def supports_graph_breaks(self) -> bool: + return False + + def exit_on_graph_break(self) -> bool: + return True + + +class RepararametrizeModuleContextVariable(GenericContextWrappingVariable): + def __init__(self, ctx_manager_vt: ContextWrappingVariable, mod: Any) -> None: + self.cm_vt = ctx_manager_vt + self.mod = mod + # We don't call super().__init__() because we're delegating most methods to cm_vt + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + # Custom enter implementation with side effects. Additionally, do not + # worry about side effects for the hop, as they cancel each other out. + with torch._dynamo.variables.higher_order_ops.dynamo_allow_side_effects_in_hop( + tx + ): + self.old_parameters_var = self.mod.var_getattr(tx, "_parameters").realize() + self.old_buffer_var = self.mod.var_getattr(tx, "_buffers").realize() + tx.output.side_effects.ignore_mutations_on(self.old_parameters_var) + tx.output.side_effects.ignore_mutations_on(self.old_buffer_var) + return self.cm_vt.enter(tx) + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + # Custom enter implementation with side effects. Additionally, do not + # worry about side effects for the hop, as they cancel each other out. + with torch._dynamo.variables.higher_order_ops.dynamo_allow_side_effects_in_hop( + tx + ): + x = self.cm_vt.exit(tx, *args) + tx.output.side_effects.stop_ignoring_mutations_on(self.old_buffer_var) + tx.output.side_effects.stop_ignoring_mutations_on(self.old_parameters_var) + return x + + # Forward all other method calls to self.cm_vt + def __getattr__(self, name: str) -> Any: + # This will be called for any attribute not explicitly defined in this class + return getattr(self.cm_vt, name) + + +class GradInplaceRequiresGradCtxManagerVariable(ContextWrappingVariable): + """represents torch grad requires grad""" + + @staticmethod + def create( + tx: "InstructionTranslator", target_values: Any, **kwargs: Any + ) -> "GradInplaceRequiresGradCtxManagerVariable": + return GradInplaceRequiresGradCtxManagerVariable( + target_values=target_values, + initial_values=None, + **kwargs, + ) + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + [enabled] = self.target_values + self.prev_state = torch._C._functorch.get_inplace_requires_grad_allowed() + torch._C._functorch.set_inplace_requires_grad_allowed(enabled) + self.set_cleanup_hook( + tx, + lambda: torch._C._functorch.set_inplace_requires_grad_allowed( + self.prev_state + ), + ) + self.proxy = tx.output.create_node( + "call_function", + torch._C._functorch.set_inplace_requires_grad_allowed, + (enabled,), + {}, + ) + return variables.ConstantVariable.create(None) + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + self.cleanup() + tx.output.create_node( + "call_function", + torch._C._functorch.set_inplace_requires_grad_allowed, + (self.prev_state,), + {}, + ) + return variables.ConstantVariable.create(None) + + +class TemporarilyPopInterpreterStackCtxManagerVariable(ContextWrappingVariable): + """represents torch._functorch.pyfunction.temporarily_pop_interpreter_stack()""" + + @staticmethod + def create( + tx: "InstructionTranslator", target_values: Any, **kwargs: Any + ) -> "TemporarilyPopInterpreterStackCtxManagerVariable": + return TemporarilyPopInterpreterStackCtxManagerVariable( + target_values=target_values, + initial_values=None, + **kwargs, + ) + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + self.saved = torch._C._functorch.pop_dynamic_layer_stack() + self.set_cleanup_hook( + tx, + lambda: torch._C._functorch.push_dynamic_layer_stack(self.saved), + ) + self.proxy = tx.output.create_node( + "call_function", + torch._C._functorch.pop_dynamic_layer_stack, + (), + {}, + ) + return variables.ConstantVariable.create(None) + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + self.cleanup() + tx.output.create_node( + "call_function", + torch._C._functorch.push_dynamic_layer_stack, + (self.proxy,), + {}, + ) + return variables.ConstantVariable.create(None) + + +class JvpIncrementNestingCtxManagerVariable(ContextWrappingVariable): + """represents torch.func.jvp increment/decrement nesting""" + + # A guard is needed as the grad level is baked into the torch FX graph + # This is fine if jvp is only called from within the function + # being compiled. But the FX graph may be invalid in the case of a jvp + # call from eager that calls the compiled function, as the jvp levels + # may be different. + _guards_singleton = Guard(GlobalStateSource(), GuardBuilder.FUNCTORCH_STACK_MATCH) # type: ignore[arg-type] + + @staticmethod + def create( + tx: "InstructionTranslator", **kwargs: Any + ) -> "JvpIncrementNestingCtxManagerVariable": + var = JvpIncrementNestingCtxManagerVariable( + target_values=None, + initial_values=None, + **kwargs, + ) + return var + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + install_guard(self._guards_singleton) + jvp_level = torch._functorch.eager_transforms.enter_jvp_nesting() + self.set_cleanup_hook( + tx, lambda: torch._functorch.eager_transforms.exit_jvp_nesting() + ) + self.proxy = tx.output.create_node( + "call_function", + torch._C._functorch._jvp_increment_nesting, + (), + {}, + ) + return variables.VariableTracker.build(tx, jvp_level) + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + self.cleanup() + tx.output.create_node( + "call_function", torch._C._functorch._jvp_decrement_nesting, (), {} + ) + return variables.ConstantVariable.create(None) + + +class SetFwdGradEnabledContextManager(ContextWrappingVariable): + """represents torch.autograd.forward_ad._set_fwd_grad_enabled() to enable/disable fwd grad""" + + @staticmethod + def create( + tx: "InstructionTranslator", target_values: Any, **kwargs: Any + ) -> "SetFwdGradEnabledContextManager": + return SetFwdGradEnabledContextManager( + target_values=target_values, + initial_values=None, + **kwargs, + ) + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + [mode] = self.target_values + self.prev_state = torch._C._is_fwd_grad_enabled() + torch._C._set_fwd_grad_enabled(mode) + self.set_cleanup_hook( + tx, + lambda: torch._C._set_fwd_grad_enabled(self.prev_state), + ) + self.proxy = tx.output.create_node( + "call_function", + torch._C._set_fwd_grad_enabled, + (mode,), + {}, + ) + return variables.ConstantVariable.create(None) + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + self.cleanup() + tx.output.create_node( + "call_function", + torch._C._set_fwd_grad_enabled, + (self.prev_state,), + {}, + ) + return variables.ConstantVariable.create(None) + + def python_type(self) -> type: + return torch.autograd.forward_ad._set_fwd_grad_enabled + + +class DualLevelContextManager(ContextWrappingVariable): + """Represents torch.autograd.forward_ad.dual_level ctx manager""" + + _guards_singleton = Guard(GlobalStateSource(), GuardBuilder.DUAL_LEVEL) # type: ignore[arg-type] + + @staticmethod + def create(tx: "InstructionTranslator", **kwargs: Any) -> "DualLevelContextManager": + return DualLevelContextManager( + target_values=None, + initial_values=None, + **kwargs, + ) + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + install_guard(self._guards_singleton) + self.new_level = torch.autograd.forward_ad.enter_dual_level() + self.set_cleanup_hook( + tx, lambda: torch.autograd.forward_ad.exit_dual_level(level=self.new_level) + ) + self.proxy = tx.output.create_node( + "call_function", + torch._C._enter_dual_level, + (), + {}, + ) + return variables.VariableTracker.build(tx, self.new_level) + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + self.cleanup() + tx.output.create_node( + "call_function", + torch._C._exit_dual_level, + (self.new_level,), + {}, + ) + return variables.ConstantVariable.create(None) + + def python_type(self) -> type: + return torch.autograd.forward_ad.dual_level + + +class GradIncrementNestingCtxManagerVariable(ContextWrappingVariable): + """represents torch.func.grad increment/decrement nesting""" + + # A guard is needed as the grad level is baked into the torch FX graph + # This is fine if grad is only called from within the function + # being compiled. But the FX graph may be invalid in the case of a grad + # call from eager that calls the compiled function, as the grad levels + # may be different. + _guards_singleton = Guard(GlobalStateSource(), GuardBuilder.FUNCTORCH_STACK_MATCH) # type: ignore[arg-type] + + @staticmethod + def create( + tx: "InstructionTranslator", **kwargs: Any + ) -> "GradIncrementNestingCtxManagerVariable": + var = GradIncrementNestingCtxManagerVariable( + target_values=None, + initial_values=None, + **kwargs, + ) + return var + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + install_guard(self._guards_singleton) + grad_level = torch._C._functorch._grad_increment_nesting() + self.set_cleanup_hook(tx, lambda: torch._C._functorch._grad_decrement_nesting()) + self.proxy = tx.output.create_node( + "call_function", + torch._C._functorch._grad_increment_nesting, + (), + {}, + ) + return variables.VariableTracker.build(tx, grad_level) + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + self.cleanup() + tx.output.create_node( + "call_function", torch._C._functorch._grad_decrement_nesting, (), {} + ) + return variables.ConstantVariable.create(None) + + +class CatchWarningsCtxManagerVariable(ContextWrappingVariable): + """Delay a call to warnings.catch_warnings""" + + @staticmethod + def create( + tx: "InstructionTranslator", catch_warnings_args: dict[str, VariableTracker] + ) -> "CatchWarningsCtxManagerVariable": + return CatchWarningsCtxManagerVariable( + catch_warnings_args=catch_warnings_args, + target_values=None, + initial_values=None, + ) + + def __init__( + self, + catch_warnings_args: dict[str, VariableTracker], + target_values: Any | None = None, + initial_values: Any | None = None, + **kwargs: Any, + ) -> None: + assert isinstance(catch_warnings_args, dict), catch_warnings_args + super().__init__( + target_values=target_values, initial_values=initial_values, **kwargs + ) + self.catch_warnings_args = catch_warnings_args + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + kwargs = { + k: v.as_python_constant() for k, v in self.catch_warnings_args.items() + } + ctx_val = warnings.catch_warnings(**kwargs) + self.set_cleanup_hook(tx, lambda: ctx_val.__exit__(None, None, None)) + return variables.VariableTracker.build(tx, ctx_val.__enter__()) + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.load_import_from("warnings", "catch_warnings") + ) + codegen.foreach(self.catch_warnings_args.values()) + keys = tuple(self.catch_warnings_args.keys()) + codegen.extend_output(codegen.create_call_function_kw(len(keys), keys, False)) + + def python_type(self) -> type: + return warnings.catch_warnings + + +class VmapIncrementNestingCtxManagerVariable(ContextWrappingVariable): + """represents torch VMap increment/decrement nesting""" + + # A guard is needed as the vmap level is baked into the torch FX graph + # generated. This is fine if vmap is only called from within the function + # being compiled. But the FX graph may be invalid in the case of a vmap + # call from eager that calls the compiled function, as the vmap levels + # may be different. + _guards_singleton = Guard(GlobalStateSource(), GuardBuilder.FUNCTORCH_STACK_MATCH) # type: ignore[arg-type] + + @staticmethod + def create( + tx: "InstructionTranslator", + target_values: Sequence[VariableTracker], + **kwargs: Any, + ) -> "VmapIncrementNestingCtxManagerVariable": + var = VmapIncrementNestingCtxManagerVariable( + target_values=target_values, + initial_values=None, + **kwargs, + ) + return var + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + install_guard(self._guards_singleton) + batch_size, randomness = self.target_values + if isinstance(batch_size, variables.SymNodeVariable): + batch_size_value = batch_size.sym_num + else: + batch_size_value = batch_size.as_python_constant() + randomness = randomness.as_python_constant() + vmap_level = torch._C._functorch._vmap_increment_nesting( + batch_size_value, randomness + ) + self.set_cleanup_hook(tx, lambda: torch._C._functorch._vmap_decrement_nesting()) + self.proxy = tx.output.create_proxy( + "call_function", + torch._functorch.predispatch._vmap_increment_nesting, + (batch_size.as_proxy(), randomness), + {}, + ) + return variables.VariableTracker.build(tx, vmap_level) + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + self.cleanup() + tx.output.create_node( + "call_function", + torch._functorch.predispatch._vmap_decrement_nesting, + (), + {}, + ) + return variables.ConstantVariable.create(None) + + +class GradModeVariable(ContextWrappingVariable): + """represents torch.{no_grad,enable_grad,set_grad_mode}()""" + + _guards_singleton = Guard(GlobalStateSource(), GuardBuilder.GRAD_MODE) # type: ignore[arg-type] + + @staticmethod + def create( + tx: "InstructionTranslator", + target_value: Any, + initialized: bool = False, + **kwargs: Any, + ) -> "GradModeVariable": + var = GradModeVariable( + target_values=[target_value], + initial_values=[torch.is_grad_enabled()], + **kwargs, + ) + if initialized: + var._call_func(tx, var.target_values) + return var + + def __init__( + self, + target_values: Any, + initial_values: Sequence[bool] | None = None, + initialized: bool = True, + **kwargs: Any, + ) -> None: + super().__init__( + target_values=target_values, initial_values=initial_values, **kwargs + ) + install_guard(self._guards_singleton) + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + self._call_func(tx, self.target_values) + return variables.ConstantVariable.create(None) + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + self._call_func(tx, self.initial_values) + return variables.ConstantVariable.create(None) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + self._call_func(tx, self.initial_values) # undo eager initialization + return super().call_function(tx, args, kwargs) + + def _call_func(self, tx: "InstructionTranslator", values: Any) -> None: + assert len(values) == 1 + value = values[0] + # Coalesce grad mode mutations + if torch.is_grad_enabled() != value: + tx.output.create_node( + "call_function", torch._C._set_grad_enabled, (value,), {} + ) + torch._C._set_grad_enabled(value) + + def module_name(self) -> str: + return "torch" + + def fn_name(self) -> str: + return "set_grad_enabled" + + def python_type(self) -> type: + return torch.set_grad_enabled + + +class InferenceModeVariable(ContextWrappingVariable): + @staticmethod + def create( + tx: "InstructionTranslator", target_value: Any, **kwargs: Any + ) -> "InferenceModeVariable": + var = InferenceModeVariable( + [target_value], initial_values=torch.is_inference_mode_enabled(), **kwargs + ) + return var + + def __init__( + self, + target_values: Any, + initial_values: bool | None = None, + **kwargs: Any, + ) -> None: + if initial_values is None: + # This must be called here since function defaults are evaluated at import time + initial_values = torch.is_inference_mode_enabled() + super().__init__( + target_values=target_values, initial_values=initial_values, **kwargs + ) + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + self.cleanup_assert() + tx.output.create_node( + "call_function", + torch.autograd.grad_mode._exit_inference_mode, + (self.proxy,), + {}, + ) + return variables.ConstantVariable.create(None) + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + disabled_inference_mode_forcibly = False + if ( + torch._dynamo.config.fake_tensor_disable_inference_mode + and self.target_values[0] + ): + # Do not set the inference mode because we keep it off during + # compilation. Set the grad_enabled to False to reflect the relevant + # part of inference_mode to torch.compile. + disabled_inference_mode_forcibly = True + prior = torch.is_grad_enabled() + torch._C._set_grad_enabled(False) + else: + ctx = torch.autograd.grad_mode._enter_inference_mode(*self.target_values) + + def cleanup_hook() -> None: + if disabled_inference_mode_forcibly: + torch._C._set_grad_enabled(prior) + else: + torch.autograd.grad_mode._exit_inference_mode(ctx) + + self.set_cleanup_hook(tx, cleanup_hook) + self.proxy = tx.output.create_node( + "call_function", + torch.autograd.grad_mode._enter_inference_mode, + (*self.target_values,), + {}, + ) + return variables.ConstantVariable.create(None) + + def module_name(self) -> str: + return "torch" + + def fn_name(self) -> str: + return "inference_mode" + + def python_type(self) -> type: + return torch.inference_mode + + +class CUDADeviceVariable(ContextWrappingVariable): + """represents torch.cuda.device""" + + @staticmethod + def create( + tx: "InstructionTranslator", device: Any, **kwargs: Any + ) -> "CUDADeviceVariable": + var = CUDADeviceVariable( + target_values=[torch.cuda._get_device_index(device, optional=True)], + initial_values=None, + **kwargs, + ) + return var + + def __init__( + self, + target_values: Any, + initial_values: Any | None = None, + **kwargs: Any, + ) -> None: + super().__init__( + target_values=target_values, initial_values=initial_values, **kwargs + ) + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + self.cleanup_assert() + tx.output.create_node( + "call_function", + torch.cuda._maybe_exchange_device, + (self.proxy,), + {}, + ) + return variables.ConstantVariable.create(False) + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + prev_idx = torch.cuda._exchange_device(*self.target_values) + self.set_cleanup_hook(tx, lambda: torch.cuda._maybe_exchange_device(prev_idx)) + self.proxy = tx.output.create_node( + "call_function", + torch.cuda._exchange_device, + (*self.target_values,), + {}, + ) + return variables.ConstantVariable.create(None) + + def module_name(self) -> str: + return "torch.cuda" + + def fn_name(self) -> str: + return "device" + + def python_type(self) -> type: + return torch.cuda.device + + +class TorchFunctionDisableVariable(ContextWrappingVariable): + """represents whether torch function overrides are enabled or not""" + + _guards_singleton = Guard(GlobalStateSource(), GuardBuilder.TORCH_FUNCTION_STATE) # type: ignore[arg-type] + + @staticmethod + def create( + tx: "InstructionTranslator", **kwargs: Any + ) -> "TorchFunctionDisableVariable": + var = TorchFunctionDisableVariable( + tx, + target_values=[], + initial_values=[], + **kwargs, + ) + return var + + def __init__( + self, + tx: "InstructionTranslator", + target_values: Sized, + initial_values: Sized | None = None, + only_subclass: bool = True, + **kwargs: Any, + ) -> None: + assert len(target_values) == 0 + assert initial_values is not None and len(initial_values) == 0 + self.only_subclass = only_subclass + self.initial_torch_function_subclass_enabled = ( + tx.symbolic_torch_function_state.torch_function_subclass_enabled + ) + self.initial_torch_function_mode_enabled = ( + tx.symbolic_torch_function_state.torch_function_mode_enabled + ) + + super().__init__( + target_values=target_values, initial_values=initial_values, **kwargs + ) + install_guard(self._guards_singleton) + + def set_cleanup_hook( + self, + tx: "InstructionTranslator", + fn: Callable[..., Any] | None = None, + ) -> None: + if fn is None: + + def fn() -> None: + tx.symbolic_torch_function_state.torch_function_subclass_enabled = ( + self.initial_torch_function_subclass_enabled + ) + if not self.only_subclass: + tx.symbolic_torch_function_state.torch_function_mode_enabled = ( + self.initial_torch_function_mode_enabled + ) + + self.cleanup_fn = fn + tx.output.add_cleanup_hook(self.cleanup) + + def _call_func(self, tx: "InstructionTranslator", values: Sized) -> None: + assert len(values) == 0 + tx.symbolic_torch_function_state.torch_function_subclass_enabled = False + if not self.only_subclass: + tx.symbolic_torch_function_state.torch_function_mode_enabled = False + + def module_name(self) -> str: + return "torch._C" + + def fn_name(self) -> str: + if self.only_subclass: + return "DisableTorchFunctionSubclass" + return "DisableTorchFunction" + + def python_type(self) -> type: + if self.only_subclass: + return torch._C.DisableTorchFunctionSubclass # pyrefly: ignore[bad-return] + return torch._C.DisableTorchFunction # pyrefly: ignore[bad-return] + + +class DisabledSavedTensorsHooksVariable(ContextWrappingVariable): + """represents torch.autograd.graph.disable_saved_tensors_hook.""" + + @staticmethod + def create( + tx: "InstructionTranslator", target_value: str | None, **kwargs: Any + ) -> "DisabledSavedTensorsHooksVariable": + var = DisabledSavedTensorsHooksVariable( + target_values=[target_value], + initial_values=[ + torch._C._autograd._saved_tensors_hooks_get_disabled_error_message() + ], + **kwargs, + ) + var._call_func(tx, [target_value]) + var.set_cleanup_hook(tx) + return var + + def __init__( + self, + target_values: Sequence[str | None], + initial_values: Sequence[str | None] | None = None, + **kwargs: Any, + ) -> None: + super().__init__( + target_values=target_values, initial_values=initial_values, **kwargs + ) + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + return variables.ConstantVariable.create(None) + + def _call_func( + self, tx: "InstructionTranslator", values: Sequence[str | None] + ) -> None: + assert len(values) == 1 + value = values[0] + if value is not None: + # Disable `saved_tensors_hooks` with message (`value`) + # OR + # we are exiting this context and restoring the previous message. + tx.output.create_node( + "call_function", + torch._C._autograd._saved_tensors_hooks_disable, + (value,), + {}, + ) + torch._C._autograd._saved_tensors_hooks_disable(value) + else: + # We are exiting this context and if prev_message was None, we re-enable `saved_tensors_hooks`. + tx.output.create_node( + "call_function", torch._C._autograd._saved_tensors_hooks_enable, (), {} + ) + torch._C._autograd._saved_tensors_hooks_enable() + + def module_name(self) -> str: + return "torch.autograd.graph" + + def fn_name(self) -> str: + return "disable_saved_tensors_hooks" + + def python_type(self) -> type: + return contextlib._GeneratorContextManager + + +class AutocastModeVariable(ContextWrappingVariable): + @staticmethod + def create( + func: torch.amp.autocast_mode.autocast, + args: Sequence[Any], + kwargs: dict[str, Any], + ) -> "AutocastModeVariable": + assert func in [ + torch.amp.autocast_mode.autocast, + torch.cuda.amp.autocast, + torch.cpu.amp.autocast, + ] + # device_type : str, + # dtype : Optional[_dtype] = None, + # enabled : bool = True, + # cache_enabled : Optional[bool] = None):cache_enabled + bound_args = inspect.signature(func).bind(*args, **kwargs) + bound_args.apply_defaults() + target_values = [] + kwargs.clear() + + for key in ["device_type", "dtype", "enabled", "cache_enabled"]: + if key == "device_type" and func in [ + torch.cuda.amp.autocast, + torch.cpu.amp.autocast, + ]: + # pyrefly: ignore [unnecessary-comparison] + arg = "cuda" if func is torch.cuda.amp.autocast else "cpu" + else: + arg = bound_args.arguments[key] + if isinstance(arg, VariableTracker): + target_values.append(arg.as_python_constant()) + else: + target_values.append(arg) + + var = AutocastModeVariable(target_values, initial_values=None, **kwargs) + return var + + def __init__( + self, + target_values: Sequence[Any], + initial_values: Any | None = None, + **kwargs: Any, + ) -> None: + super().__init__( + target_values=target_values, initial_values=initial_values, **kwargs + ) + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + self.cleanup_assert() + tx.output.create_node( + "call_function", torch.amp._exit_autocast, (self.proxy,), {} + ) + return variables.ConstantVariable.create(None) + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + ctx = torch.amp._enter_autocast(*self.target_values) + self.set_cleanup_hook(tx, lambda: torch.amp._exit_autocast(ctx)) + self.proxy = tx.output.create_node( + "call_function", torch.amp._enter_autocast, (*self.target_values,), {} + ) + return variables.ConstantVariable.create(None) + + def module_name(self) -> str: + return "torch.amp.autocast_mode" + + def fn_name(self) -> str: + return "autocast" + + def python_type(self) -> type: + return torch.amp.autocast_mode.autocast + + +class NullContextVariable(ContextWrappingVariable): + """ + This class represents Python contextlib.nullcontext. + """ + + def __init__(self, target_values: Any | None = None, **kwargs: Any) -> None: + super().__init__(target_values=target_values, **kwargs) + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + none = variables.ConstantVariable.create(None) + return self.target_values if self.target_values else none + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + return variables.ConstantVariable.create(None) + + def module_name(self) -> str: + return "contextlib" + + def fn_name(self) -> str: + return "nullcontext" + + def python_type(self) -> type: + return contextlib.nullcontext + + +class ProfilerContextVariable(ContextWrappingVariable): + """ + This class represents a set of torch profiler context objects, where Dynamo + ignores all the side-effects in the __init__, __enter__ and __exit__ methods + by treating the object mostly as a `contextlib.nullcontext`, except for edge + cases like the `__enter__` method which returns the object itself rather + than `None`, per implementation of the torch objects. + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(target_values=None, **kwargs) + + def python_type(self) -> type: + return torch.profiler.profile + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + return self + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + return variables.ConstantVariable.create(None) + + def module_name(self) -> str: + return "contextlib" + + def fn_name(self) -> str: + return "nullcontext" + + def reconstruct(self, codegen: "PyCodegen") -> None: + unimplemented( + gb_type="torch.profiler object escaped from compiled region", + context=str(self), + explanation="Dynamo doesn't support compiling a region that returns a torch.profiler context manager.", + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + ) + + +class ProfilerRecordFunctionContextVariable(ContextWrappingVariable): + """ + This class represents torch profiler context objects. + + For record_function: emits torch.ops.profiler._record_function_enter_new + to the graph on enter, and torch.ops.profiler._record_function_exit on exit. + But if emit_profiler_ops=False, behaves like nullcontext. + + For profile: behaves like nullcontext, ignoring all side-effects. + """ + + _nonvar_fields = { + "emit_profiler_ops", + *ContextWrappingVariable._nonvar_fields, + } + + @staticmethod + def create( + func: Any, + record_args: Sequence[VariableTracker], + record_kwargs: "dict[str, VariableTracker]", + **kwargs: Any, + ) -> "ProfilerRecordFunctionContextVariable": + target_values = None + if config.capture_profiler_record_function: + # Extract name and args for record_function + # record_function(name: str, args: Optional[str] = None) + name = ( + record_args[0].as_python_constant() + if record_args + else kwargs.get( + "name", variables.ConstantVariable.create("unknown") + ).as_python_constant() + ) + record_args_const = None + if len(record_args) > 1: + record_args_const = record_args[1].as_python_constant() + elif "args" in kwargs: + record_args_const = kwargs["args"].as_python_constant() + target_values = [name, record_args_const] + else: + warning_once(log, "Profiler record function %s will be ignored", func) + return ProfilerRecordFunctionContextVariable( + target_values=target_values, + initial_values=None, + **kwargs, + ) + + def __init__( + self, + target_values: Any = None, + initial_values: Any | None = None, + **kwargs: Any, + ) -> None: + super().__init__( + target_values=target_values, initial_values=initial_values, **kwargs + ) + + def python_type(self) -> type: + return torch.autograd.profiler.record_function + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + if config.capture_profiler_record_function: + name, args = self.target_values + # Create the profiler entry node in the graph + self.proxy = tx.output.create_node( + "call_function", + torch.ops.profiler._record_function_enter_new, + (name, args), + {}, + ) + return self + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + if config.capture_profiler_record_function: + # Create the profiler exit node in the graph + tx.output.create_node( + "call_function", + torch.ops.profiler._record_function_exit._RecordFunction, + (self.proxy,), + {}, + ) + return variables.ConstantVariable.create(None) + + def module_name(self) -> str: + return ( + "torch.autograd.profiler" + if config.capture_profiler_record_function + else "contextlib" + ) + + def fn_name(self) -> str: + return ( + "record_function" + if config.capture_profiler_record_function + else "nullcontext" + ) + + def reconstruct_type(self, codegen: "PyCodegen") -> None: + # This will be called if we try to reconstruct the record_function type + # when there's a graph break. The _set_error_on_graph_break(True) in enter() + # will cause the graph break to raise an error before we get here. + if config.capture_profiler_record_function: + unimplemented( + gb_type="record_function escaped from compiled region", + context=str(self), + explanation="Dynamo doesn't support graph break inside record_function region.", + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + ) + else: + # If capture is disabled, allow reconstruction (behaves like nullcontext) + super().reconstruct_type(codegen) + + +class PreserveVersionContextVariable(ContextWrappingVariable): + """ + Wraps torch.autograd._unsafe_preserve_version_counter + """ + + @staticmethod + def _create_lambda_from_tensors( + tx: "InstructionTranslator", + tensors: VariableTracker, + ) -> "PreserveVersionContextVariable": + if tensors.is_tensor(): + versions = variables.TupleVariable( + [x.var_getattr(tx, "_version") for x in [tensors]] + ) + tensors_tuple = variables.TupleVariable([tensors]) + else: + assert isinstance(tensors, variables.TupleVariable) + versions = variables.TupleVariable( + [x.var_getattr(tx, "_version") for x in tensors.items] + ) + tensors_tuple = tensors + return PreserveVersionContextVariable(tensors_tuple, versions) + + @staticmethod + def constructor(tx: "InstructionTranslator") -> VariableTracker: + return variables.LambdaVariable( + lambda tensors: PreserveVersionContextVariable._create_lambda_from_tensors( + tx, tensors + ) + ) + + def __init__( + self, + tensors: VariableTracker, + prev_versions: VariableTracker, + **kwargs: Any, + ) -> None: + kwargs.setdefault("target_values", None) + super().__init__(**kwargs) + self.tensors = tensors + self.prev_versions = prev_versions + # The context manager accepts Union[Tensor, Tuple[Tensor]] + if self.tensors.is_tensor(): + self.tensors = variables.TupleVariable([self.tensors]) + if self.prev_versions.is_symnode_like(): + self.prev_versions = variables.TupleVariable([self.prev_versions]) + + def python_type(self) -> type: + return torch.autograd.grad_mode._unsafe_preserve_version_counter + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + return variables.ConstantVariable.create(None) + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + from ..tensor_version_op import _unsafe_set_version_counter + + return variables.TorchInGraphFunctionVariable( + _unsafe_set_version_counter + ).call_function(tx, [self.tensors, self.prev_versions], {}) + + def reconstruct(self, codegen: "PyCodegen") -> None: + unimplemented( + gb_type="torch.autograd._unsafe_preserve_version_counter escaped from compiled region", + context=str(self), + explanation=( + "Dynamo doesn't support compiling a region that returns " + "a torch.autograd._unsafe_preserve_version_counter context manager." + ), + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + ) + + +class FSDPParamGroupUseTrainingStateVariable(ContextWrappingVariable): + _guards_singleton = Guard(GlobalStateSource(), GuardBuilder.FSDP_TRAINING_STATE) # type: ignore[arg-type] + + @staticmethod + def create( + tx: "InstructionTranslator", + param_group_var: Any, + target_value: Any, + **kwargs: Any, + ) -> "FSDPParamGroupUseTrainingStateVariable": + var = FSDPParamGroupUseTrainingStateVariable( + param_group_var=param_group_var, + target_values=[target_value], + initial_values=[param_group_var.value._training_state], + **kwargs, + ) + return var + + def __init__( + self, + param_group_var: Any, + target_values: Sequence[Any], + initial_values: Sequence[Any] | None = None, + **kwargs: Any, + ) -> None: + super().__init__( + target_values=target_values, initial_values=initial_values, **kwargs + ) + self.param_group_var = param_group_var + install_guard(self._guards_singleton) + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + self._call_func(tx, self.target_values) + return variables.ConstantVariable.create(None) + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + self._call_func(tx, self.initial_values) # type: ignore[arg-type] + return variables.ConstantVariable.create(None) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + # undo eager initialization + self._call_func(tx, self.initial_values) # type: ignore[arg-type] + return super().call_function(tx, args, kwargs) + + def _call_func(self, tx: "InstructionTranslator", values: Sequence[Any]) -> None: + assert len(values) == 1 + value = values[0] + if self.param_group_var.value._training_state != value: + self.param_group_var.call_method( + tx, + "__setattr__", + ( + variables.VariableTracker.build(tx, "_training_state"), + variables.VariableTracker.build(tx, value), + ), + {}, + ) + self.param_group_var.value._training_state = value + + def module_name(self) -> str: + return "torch.distributed.fsdp._fully_shard._fsdp_param_group.FSDPParamGroup" + + def fn_name(self) -> str: + return "use_training_state" + + +class SDPAKernelVariable(ContextWrappingVariable): + """represents torch.nn.attention.sdpa_kernel""" + + @staticmethod + def create( + tx: "InstructionTranslator", + backends: Any, + set_priority: bool = False, + **kwargs: Any, + ) -> "SDPAKernelVariable": + if isinstance(backends, torch.nn.attention.SDPBackend): + backends = [backends] + var = SDPAKernelVariable( + target_values=backends, + initial_values=None, + set_priority=set_priority, + **kwargs, + ) + return var + + def __init__( + self, + target_values: list[torch.nn.attention.SDPBackend], + initial_values: Any = None, + set_priority: bool = False, + **kwargs: Any, + ) -> None: + super().__init__( + target_values=target_values, initial_values=initial_values, **kwargs + ) + self.set_priority = set_priority + + @staticmethod + def _backends_to_nodes( + tx: "InstructionTranslator", + backends: list[Any], + ) -> list[Any]: + # convert to/from string in order to bake the backend into FX graph + nodes = [ + tx.output.create_node( + "call_function", + torch.nn.attention._backend_from_string, + (backend.name,), + {}, + ) + for backend in backends + ] + return nodes + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + self.prev_backends = torch.nn.attention._cur_sdpa_kernel_backends( + with_priority=self.set_priority + ) + self.set_cleanup_hook( + tx, + lambda: torch.nn.attention._sdpa_kernel( + self.prev_backends, set_priority=self.set_priority + ), + ) + torch.nn.attention._sdpa_kernel( + self.target_values, set_priority=self.set_priority + ) + arg = self._backends_to_nodes(tx, self.target_values) + tx.output.create_node( + "call_function", + torch.nn.attention._sdpa_kernel, + (arg, bool(self.set_priority)), + {}, + ) + return variables.ConstantVariable.create(None) + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + self.cleanup_assert() + arg = self._backends_to_nodes(tx, self.prev_backends) + tx.output.create_node( + "call_function", + torch.nn.attention._sdpa_kernel, + (arg, bool(self.set_priority)), + {}, + ) + return variables.ConstantVariable.create(None) + + def module_name(self) -> str: + return "torch.nn.attention" + + # use a private version of sdpa_kernel that accepts variadic arguments + # since dynamo reconstructs the contents of target_values one-by-one + def fn_name(self) -> str: + return "_sdpa_kernel_variadic" + + def python_type(self) -> type: + return contextlib._GeneratorContextManager + + +class FxTracebackAnnotateVariable(ContextWrappingVariable): + """ + fx.traceback.annotate is a context manager that allows users to annotate the + fx graph nodes with custom metadata. In the context of Dynamo, we don't have + to trace the body of the context manager. Instead we want to directly run + the body of the context manager, so the Dynamo created Fx graphs have the + right custom metadata. This variable tracker just runs __enter__ and + __exit__ method (instead of tracing). + """ + + def __init__( + self, target_values: Any, initial_values: Any = None, **kwargs: Any + ) -> None: + super().__init__( + target_values=target_values, initial_values=initial_values, **kwargs + ) + + def enter( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + # Run the annotation ctx manager in eager. Also ensure that + # preserve_node_meta context manager is setup. This is important to pass + # on the metadata to the create_proxy nodes. + stack = ExitStack() + stack.enter_context(torch.fx.traceback.annotate(self.target_values)) + stack.enter_context(torch.fx.traceback.preserve_node_meta()) + self.set_cleanup_hook(tx, lambda: stack.close()) + return variables.ConstantVariable.create(None) + + def module_name(self) -> str: + return "torch.fx.traceback" + + def fn_name(self) -> str: + return "annotate" + + def python_type(self) -> type: + return contextlib._GeneratorContextManager + + def reconstruct_type(self, codegen: "PyCodegen") -> None: + unimplemented( + gb_type="torch.fx.traceback.annotate escaped from compiled region", + context=str(self), + explanation="Dynamo doesn't support graph break on torch.fx.traceback.annotate.", + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + ) + + +class DynamoConfigPatchVariable(ContextWrappingVariable): + """represents torch._dynamo.patch_dynamo_config""" + + # NOTE: no need to guard on dynamo config because dynamo config should not affect soundness + # (though it may affect tracing behavior) + def __init__(self, target_values: dict[str, Any], **kwargs: Any) -> None: + target_values_tuple = tuple(target_values.items()) + super().__init__( + target_values=(target_values_tuple,), initial_values=None, **kwargs + ) + # pyrefly: ignore [implicit-any] + initial_values_dict = {} + for key, _ in target_values_tuple: + initial_values_dict[key] = torch._dynamo.config.__getattr__(key) # type: ignore[attr-defined] + self.initial_values = (tuple(initial_values_dict.items()),) + + def _call_func(self, tx: "InstructionTranslator", values: Any) -> None: + assert len(values) == 1 + value = values[0] + # manually patch dynamo config + for key, val in value: + torch._dynamo.config.__setattr__(key, val) # type: ignore[attr-defined] + # No need to keep track of global side effects because + # dynamo will properly restore this context manager for + # unsupported instructions and continuation functions. + # Dynamo config also should not affect the semantics of the compiled graph. + + def module_name(self) -> str: + return "torch._dynamo" + + def fn_name(self) -> str: + return "patch_dynamo_config" + + def python_type(self) -> type: + from torch._dynamo.decorators import DynamoConfigPatchProxy + + return DynamoConfigPatchProxy + + +class ErrorOnGraphBreakVariable(ContextWrappingVariable): + """represents torch._dynamo.error_on_graph_break""" + + def __init__(self, error_on_graph_break: bool, **kwargs: Any) -> None: + super().__init__( + target_values=(error_on_graph_break,), + initial_values=(_get_error_on_graph_break(),), + **kwargs, + ) + + def _call_func(self, tx: "InstructionTranslator", values: Sequence[bool]) -> None: + assert len(values) == 1 + _set_error_on_graph_break(values[0]) + + def module_name(self) -> str: + return "torch._dynamo" + + def fn_name(self) -> str: + return "error_on_graph_break" + + def python_type(self) -> type: + from torch._dynamo.decorators import ErrorOnGraphBreakDecoratorContextManager + + return ErrorOnGraphBreakDecoratorContextManager + + +class CudagraphOverrideVariable(ContextWrappingVariable): + """represents torch._dynamo.override_cudagraphs""" + + def __init__(self, fwd: bool | None, bwd: bool | None, **kwargs: Any) -> None: + super().__init__( + target_values=(fwd, bwd), + initial_values=None, # Captured in enter() + **kwargs, + ) + + def enter(self, tx: "InstructionTranslator") -> "VariableTracker": + # Capture current annotation before overwriting + prev = tx.output.cudagraph_annotation + if prev is not None: + self.initial_values = (prev.fwd, prev.bwd) + else: + self.initial_values = (None,) + return super().enter(tx) + + def _call_func(self, tx: "InstructionTranslator", values: tuple[Any, ...]) -> None: + from torch._inductor import _CudagraphAnnotation + + if len(values) == 1 and values[0] is None: + # Restoring to no annotation + tx.output.cudagraph_annotation = None + tx.output.tracing_context.cudagraph_annotation = None + else: + assert len(values) == 2 + fwd, bwd = values + annotation = _CudagraphAnnotation(fwd=fwd, bwd=bwd) + tx.output.cudagraph_annotation = annotation + tx.output.tracing_context.cudagraph_annotation = annotation + + def module_name(self) -> str: + return "torch._dynamo" + + def fn_name(self) -> str: + return "override_cudagraphs" + + def python_type(self) -> type: + from torch._dynamo.decorators import CudagraphOverrideContextManager + + return CudagraphOverrideContextManager + + def exit_on_graph_break(self) -> bool: + # Annotation persists until graph is compiled; each resume function + # will reconstruct the context manager and call enter() again + return False + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> "VariableTracker": + # Override to NOT call cleanup here. The cleanup will happen in + # call_cleanup_hooks() during compile_subgraph, which is AFTER + # the annotation is copied to gm.meta. + return variables.ConstantVariable.create(None) + + +class WithEnterFunctionVariable(VariableTracker): + def __init__( + self, + ctx: ContextWrappingVariable | GenericContextWrappingVariable, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.ctx = ctx + + def python_type(self) -> type: + return types.MethodType + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + assert not args + assert not kwargs + # NOTE: we assume that the instruction immediately after the current CALL instruction + # is the first instruction of the block. + + return tx.enter_ctx(self.ctx, tx.current_instruction) + + def reconstruct(self, codegen: "PyCodegen") -> None: + try: + type_str = f"{self.ctx.module_name()}.{self.ctx.fn_name()}" + except NotImplementedError: + type_str = str(type(self.ctx)) + unimplemented( + gb_type="Attempted to reconstruct context manager's __enter__ method", + context=str(self.ctx), + explanation=f"Attempted to reconstruct context manager {type_str} while tracing `with ...:`", + hints=[ + "It is likely there is a graph break while tracing `with ctx:` " + "but outside the actual `ctx.__enter__()` method. " + "`torch.compile` does not expect this to happen.", + *graph_break_hints.DIFFICULT, + *graph_break_hints.DYNAMO_BUG, + ], + ) + + +class WithExitFunctionVariable(VariableTracker): + _nonvar_fields = { + "target", + *VariableTracker._nonvar_fields, + } + + def __init__( + self, + ctx: ContextWrappingVariable | GenericContextWrappingVariable, + target: Any, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + assert isinstance( + ctx, (ContextWrappingVariable, GenericContextWrappingVariable) + ) + self.ctx = ctx + self.target = target + + def python_type(self) -> type: + return types.MethodType + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + assert not kwargs + return self.ctx.exit(tx, *args) + + def reconstruct(self, codegen: "PyCodegen") -> None: + # Note here we reconstruct the context manager rather than the + # exit function. The handler generated by BlockStackEntry + # will re-enter the context in the resume function. + self.ctx.reconstruct_type(codegen) # type: ignore[union-attr] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/dicts.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/dicts.py new file mode 100644 index 0000000000000000000000000000000000000000..27f674b2e159b18c587fc15988109d71338d0db4 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/dicts.py @@ -0,0 +1,1535 @@ +""" +Dictionary-related variable tracking classes for PyTorch Dynamo. + +This module implements variable tracking for different types of dictionary-like objects: +- Regular Python dictionaries (dict) +- Ordered dictionaries (collections.OrderedDict) +- Default dictionaries (collections.defaultdict) +- Dictionary views (keys and values) + +These classes are responsible for tracking dictionary operations during graph compilation, +maintaining proper guards for dictionary mutations and key existence checks. They handle +dictionary creation, modification, key/value access, and view operations while ensuring +correct behavior in the compiled code through appropriate guard installation. + +The implementation uses a special HashableTracker wrapper to handle +dictionary keys while preserving proper aliasing semantics. Set-related classes live +in sets.py. +""" + +import collections +import functools +import types +from collections.abc import Callable, Iterator, Sequence +from typing import Any, Literal, TYPE_CHECKING, Union + +from torch.utils._pytree import MappingKey + +from .. import graph_break_hints, polyfills, variables +from ..bytecode_transformation import ( + create_call_function, + create_call_method, + create_dup_top, + create_instruction, +) +from ..exc import raise_observed_exception, unimplemented +from ..guards import GuardBuilder, install_guard +from ..source import ( + AttrSource, + DictGetItemSource, + is_constant_source, + is_from_local_source, +) +from ..utils import ( + cmp_name_to_op_mapping, + dict_items, + dict_keys, + dict_values, + istype, + raise_args_mismatch, +) +from .base import ( + AttributeMutationExisting, + AttributeMutationNew, + NO_SUCH_SUBOBJ, + ValueMutationNew, + VariableTracker, +) +from .constant import ConstantVariable +from .hashable import HashableTracker, is_hashable, raise_unhashable +from .sets import SetVariable + + +if TYPE_CHECKING: + from torch._dynamo.codegen import PyCodegen + from torch._dynamo.symbolic_convert import InstructionTranslator + + from .functions import UserFunctionVariable + + +# [Adding a new supported class within the keys of ConstDictVariable] +# - Implement is_python_hashable() method in the VariableTracker subclass +# - Implement get_python_hash() and is_python_equal() methods for hashable types + + +class ConstDictVariable(VariableTracker): + # PyDict_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/dictobject.c#L4825 + _cpython_type = dict + + CONTAINS_GUARD = GuardBuilder.DICT_CONTAINS + NOT_CONTAINS_GUARD = GuardBuilder.DICT_NOT_CONTAINS + + _nonvar_fields = { + "user_cls", + *VariableTracker._nonvar_fields, + } + + def __init__( + self, + items: dict[VariableTracker, VariableTracker], + user_cls: type = dict, + **kwargs: Any, + ) -> None: + # .clone() pass these arguments in kwargs but they're recreated a few + # lines below + if "original_items" in kwargs: + kwargs.pop("original_items") + if "should_reconstruct_all" in kwargs: + kwargs.pop("should_reconstruct_all") + + super().__init__(**kwargs) + + Hashable = HashableTracker + + # Keys will just be HashableTrackers when cloning, in any other case they'll be VariableTrackers + assert all( + isinstance(x, (VariableTracker, Hashable)) + and isinstance(v, VariableTracker) + for x, v in items.items() + ) + + def make_hashable( + key: Union[VariableTracker, "HashableTracker"], + ) -> "HashableTracker": + return key if isinstance(key, Hashable) else Hashable(key) + + dict_cls = self._get_dict_cls_from_user_cls(user_cls) + self.items = dict_cls({make_hashable(x): v for x, v in items.items()}) + # need to reconstruct everything if the dictionary is an intermediate value + # or if a pop/delitem was executed + self.should_reconstruct_all = ( + not is_from_local_source(self.source) if self.source else True + ) + self.original_items = items.copy() + self.user_cls = user_cls + + def _get_dict_cls_from_user_cls(self, user_cls: type) -> type: + accepted_dict_types = (dict, collections.OrderedDict, collections.defaultdict) + + # avoid executing user code if user_cls is a dict subclass + if user_cls in accepted_dict_types: + dict_cls = user_cls + else: + # + dict_cls = next( + base for base in user_cls.__mro__ if base in accepted_dict_types + ) + assert dict_cls in accepted_dict_types, dict_cls + + # Use a dict instead as the call "defaultdict({make_hashable(x): v ..})" + # would fail as defaultdict expects a callable as first argument + if dict_cls is collections.defaultdict: + dict_cls = dict + return dict_cls + + def as_proxy(self) -> dict[Any, Any]: + return {k.vt.as_proxy(): v.as_proxy() for k, v in self.items.items()} + + def debug_repr(self) -> str: + items: list[str] = [] + for k, v in self.items.items(): + key_str = repr(k.vt.value) if hasattr(k.vt, "value") else k.vt.debug_repr() + val_str = repr(v.value) if hasattr(v, "value") else v.debug_repr() + items.append(f"{key_str}: {val_str}") + return "{" + ", ".join(items) + "}" + + def as_python_constant(self) -> dict[Any, Any]: + return { + k.vt.as_python_constant(): v.as_python_constant() + for k, v in self.items.items() + } + + def keys_as_python_constant(self) -> dict[Any, VariableTracker]: + self.install_dict_keys_match_guard() + return {k.vt.as_python_constant(): v for k, v in self.items.items()} + + def python_type(self) -> type: + return self.user_cls + + def __contains__(self, vt: VariableTracker) -> bool: + assert isinstance(vt, VariableTracker) + Hashable = HashableTracker + if not is_hashable(vt): + return False + key = Hashable(vt) + return key in self.items and not isinstance( + self.items[key], variables.DeletedVariable + ) + + def call_tree_map_branch( + self, + tx: "InstructionTranslator", + tree_map_fn: "UserFunctionVariable", + map_fn: VariableTracker, + rest: Sequence[VariableTracker], + tree_map_kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + other_dicts: list[ConstDictVariable] = [] + for candidate in rest: + candidate = candidate.realize() + if not isinstance(candidate, ConstDictVariable) or len( + candidate.items + ) != len(self.items): + return self._tree_map_fallback( + tx, tree_map_fn, map_fn, rest, tree_map_kwargs + ) + other_dicts.append(candidate) + + new_items_hashed = type(self.items)() + for key_tracker, value in self.items.items(): + sibling_leaves: list[VariableTracker] = [] + for candidate in other_dicts: + try: + sibling_leaves.append(candidate.items[key_tracker]) + except KeyError: + return self._tree_map_fallback( + tx, tree_map_fn, map_fn, rest, tree_map_kwargs + ) + new_items_hashed[key_tracker] = value.call_tree_map( + tx, + tree_map_fn, + map_fn, + sibling_leaves, + tree_map_kwargs, + ) + + updated_original_items = { + key_tracker.vt: new_items_hashed[key_tracker] + for key_tracker in new_items_hashed + } + + return self.clone( + items=new_items_hashed, + original_items=updated_original_items, + should_reconstruct_all=True, + source=None, + mutation_type=ValueMutationNew(), + ) + + def call_tree_map_with_path_branch( + self, + tx: "InstructionTranslator", + tree_map_fn: "UserFunctionVariable", + map_fn: VariableTracker, + rest: Sequence[VariableTracker], + tree_map_kwargs: dict[str, VariableTracker], + keypath: tuple[Any, ...], + ) -> VariableTracker: + other_dicts: list[ConstDictVariable] = [] + for candidate in rest: + candidate = candidate.realize() + if not isinstance(candidate, ConstDictVariable) or len( + candidate.items + ) != len(self.items): + return self._tree_map_with_path_fallback( + tx, tree_map_fn, map_fn, rest, tree_map_kwargs, keypath + ) + other_dicts.append(candidate) + + new_items_hashed = type(self.items)() + for key_tracker, value in self.items.items(): + sibling_leaves: list[VariableTracker] = [] + for candidate in other_dicts: + try: + sibling_leaves.append(candidate.items[key_tracker]) + except KeyError: + return self._tree_map_with_path_fallback( + tx, tree_map_fn, map_fn, rest, tree_map_kwargs, keypath + ) + key_const = key_tracker.vt.as_python_constant() + child_keypath = keypath + (MappingKey(key_const),) + new_items_hashed[key_tracker] = value.call_tree_map_with_path( + tx, + tree_map_fn, + map_fn, + sibling_leaves, + tree_map_kwargs, + child_keypath, + ) + + updated_original_items = { + key_tracker.vt: new_items_hashed[key_tracker] + for key_tracker in new_items_hashed + } + + return self.clone( + items=new_items_hashed, + original_items=updated_original_items, + should_reconstruct_all=True, + source=None, + mutation_type=ValueMutationNew(), + ) + + def len(self) -> int: + return sum( + not isinstance(x, variables.DeletedVariable) for x in self.items.values() + ) + + def has_new_items(self) -> bool: + return self.should_reconstruct_all or any( + self.is_new_item(self.original_items.get(key.vt), value) + for key, value in self.items.items() + ) + + def is_new_item( + self, value: VariableTracker | None, other: VariableTracker + ) -> bool: + # compare the id of the realized values if both values are not lazy VTs + if value and value.is_realized() and other.is_realized(): + return id(value.realize()) != id(other.realize()) + return id(value) != id(other) + + def reconstruct_kvs_into_new_dict(self, codegen: "PyCodegen") -> None: + # Build a dictionary that contains the keys and values. + num_args = 0 + for key, value in self.items.items(): + # We can safely call realize() here as it won't introduce any new guards + item = self.original_items.get(key.vt) + if self.is_new_item(item, value) or self.should_reconstruct_all: + codegen(key.vt) + codegen(value) + num_args += 1 + codegen.append_output(create_instruction("BUILD_MAP", arg=num_args)) + + def reconstruct(self, codegen: "PyCodegen") -> None: + if self.user_cls is collections.OrderedDict: + # emit `OrderedDict(constructed_dict)` + codegen.add_push_null( + lambda: codegen.extend_output( + [ + codegen.create_load_python_module(collections), + codegen.create_load_attr("OrderedDict"), + ] + ) + ) + if self._contains_self_reference(): + codegen.extend_output( + [ + *create_call_function(0, False), + create_dup_top(), + ] + ) + codegen.add_cache(self) + + codegen.append_output(create_dup_top()) + codegen.load_method("update") + self.reconstruct_kvs_into_new_dict(codegen) + codegen.extend_output( + [ + *create_call_method(1), + create_instruction("POP_TOP"), + ] + ) + else: + self.reconstruct_kvs_into_new_dict(codegen) + codegen.extend_output(create_call_function(1, False)) + else: + if self._contains_self_reference(): + codegen.extend_output( + [ + create_instruction("BUILD_MAP", arg=0), + create_dup_top(), + ] + ) + codegen.add_cache(self) + self.reconstruct_kvs_into_new_dict(codegen) + codegen.append_output(create_instruction("DICT_UPDATE", arg=1)) + else: + # Non-self-referential: use simple codegen + self.reconstruct_kvs_into_new_dict(codegen) + + def getitem_const_raise_exception_if_absent( + self, tx: "InstructionTranslator", arg: VariableTracker + ) -> VariableTracker: + key = HashableTracker(arg) + if key not in self.items: + raise_observed_exception(KeyError, tx, args=[arg]) + return self.items[key] + + def getitem_const( + self, tx: "InstructionTranslator", arg: VariableTracker + ) -> VariableTracker: + key = HashableTracker(arg) + if key not in self.items: + msg = f"Dictionary key {arg.value} not found during tracing" # type: ignore[attr-defined] + unimplemented( + gb_type="key not found in dict", + context=f"Key {arg.value}", # type: ignore[attr-defined] + explanation=msg, + hints=[ + "Check if the key exists in the dictionary before accessing it.", + *graph_break_hints.USER_ERROR, + ], + ) + return self.items[key] + + def maybe_getitem_const(self, arg: VariableTracker) -> VariableTracker | None: + key = HashableTracker(arg) + if key not in self.items: + return None + return self.items[key] + + def realize_key_vt(self, arg: VariableTracker) -> None: + # Realize the LazyVT on a particular index + assert arg in self + key = HashableTracker(arg) + index = tuple(self.items.keys()).index(key) + original_key_vt = tuple(self.original_items.keys())[index] + if isinstance(original_key_vt, variables.LazyVariableTracker): + original_key_vt.realize() + + def install_dict_keys_match_guard(self) -> None: + if self.source: + install_guard(self.make_guard(GuardBuilder.DICT_KEYS_MATCH)) + + def install_dict_contains_guard( + self, tx: "InstructionTranslator", args: list[VariableTracker] + ) -> None: + # Key guarding - These are the cases to consider + # 1) The dict has been mutated. In this case, we would have already + # inserted a DICT_KEYS_MATCH guard, so we can skip. + # + # 2) args[0].source is None. This happens for const keys. Here, we + # have to insert the DICT_CONTAINS guard. + # + # 3) args[0].source is not None. This can happen for non-const VTs. + # 3a) contains=True. In this case, we can access the lazyVT from + # original_items and selectively realize it. + # 3b) contains=False. There is no easy way to selectively apply this + # DICT_NOT_CONTAINS guard because our guard are represented via trees. + # Be conservative and add DICT_KEYS_MATCH guard. + + if not self.source: + return + + if tx.output.side_effects.is_modified(self): + return + + contains = args[0] in self + if args[0].source is None and args[0].is_python_constant(): + guard_fn = ( + type(self).CONTAINS_GUARD if contains else type(self).NOT_CONTAINS_GUARD + ) + install_guard( + self.make_guard( + functools.partial( + guard_fn, + key=args[0].as_python_constant(), + ) + ) + ) + elif args[0].source: + if contains: + self.realize_key_vt(args[0]) + else: + self.install_dict_keys_match_guard() + + def mp_subscript_impl( + self, + tx: "InstructionTranslator", + key: VariableTracker, + ) -> VariableTracker: + # dict_subscript: https://github.com/python/cpython/blob/62a6e898e01/Objects/dictobject.c#L3673-L3706 + # Unhashable key check happens inside _HashableTracker (raise_unhashable → TypeError). + return self.getitem_const_raise_exception_if_absent(tx, key) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + # NB - Both key and value are LazyVariableTrackers in the beginning. So, + # we have to insert guards when a dict method is accessed. For this to + # be simple, we are conservative and overguard. We skip guard only for + # get/__getitem__ because the key guard will be inserted by the + # corresponding value VT. For __contains__, we add a DICT_CONTAINS + # guard. But for all the other methods, we insert the DICT_KEYS_MATCH + # guard to be conservative. + from . import DictBuiltinVariable + from .builder import SourcelessBuilder + + Hashable = HashableTracker + + if name == "__init__": + temp_dict_vt = DictBuiltinVariable.call_custom_dict( + tx, dict, *args, **kwargs + ) + tx.output.side_effects.mutation(self) + self.items.update(temp_dict_vt.items) # type: ignore[attr-defined] + return ConstantVariable.create(None) + elif name == "items": + if args or kwargs: + raise_args_mismatch( + tx, + name, + "0 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + self.install_dict_keys_match_guard() + if self.source: + tx.output.guard_on_key_order.add(self.source) + return DictItemsVariable(self) + elif name == "keys": + if len(args): + raise_args_mismatch(tx, name, "0 args", f"{len(args)} args") + self.install_dict_keys_match_guard() + if self.source: + tx.output.guard_on_key_order.add(self.source) + return DictKeysVariable(self) + elif name == "values": + if args or kwargs: + raise_args_mismatch( + tx, + name, + "0 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + self.install_dict_keys_match_guard() + if self.source: + tx.output.guard_on_key_order.add(self.source) + if args or kwargs: + raise_observed_exception(TypeError, tx) + return DictValuesVariable(self) + elif name == "copy": + self.install_dict_keys_match_guard() + if args or kwargs: + raise_args_mismatch( + tx, + name, + "0 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + return self.clone( + items=self.items.copy(), mutation_type=ValueMutationNew(), source=None + ) + elif name == "__setitem__" and self.is_mutable(): + arg_hashable = args and is_hashable(args[0]) + if not arg_hashable: + raise_unhashable(args[0], tx) + + self.install_dict_keys_match_guard() + if kwargs or len(args) != 2: + raise_args_mismatch( + tx, + name, + "2 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + tx.output.side_effects.mutation(self) + self.items[Hashable(args[0])] = args[1] + return ConstantVariable.create(None) + elif name == "__delitem__" and self.is_mutable(): + arg_hashable = args and is_hashable(args[0]) + if arg_hashable: + self.install_dict_keys_match_guard() + self.should_reconstruct_all = True + tx.output.side_effects.mutation(self) + self.items.__delitem__(Hashable(args[0])) + return ConstantVariable.create(None) + else: + return super().call_method(tx, name, args, kwargs) + elif name == "get": + if len(args) not in (1, 2): + raise_args_mismatch(tx, name, "1 or 2 args", f"{len(args)} args") + + arg_hashable = args and is_hashable(args[0]) + if not arg_hashable: + raise_unhashable(args[0], tx) + + if args[0] not in self: + self.install_dict_contains_guard(tx, args) + if len(args) == 1: + # if default is not given, return None + return ConstantVariable.create(None) + return args[1] + # Key guarding - Nothing to do. + return self.getitem_const(tx, args[0]) + elif name == "pop" and self.is_mutable(): + if len(args) not in (1, 2): + raise_args_mismatch(tx, name, "1 or 2 args", f"{len(args)} args") + + arg_hashable = args and is_hashable(args[0]) + if not arg_hashable: + raise_unhashable(args[0], tx) + + if args[0] not in self: + # missing item, return the default value. Install no DICT_CONTAINS guard. + self.install_dict_contains_guard(tx, args) + if len(args) == 1: + # if default is not given, raise KeyError + raise_observed_exception(KeyError, tx) + return args[1] + + self.should_reconstruct_all = True + tx.output.side_effects.mutation(self) + return self.items.pop(Hashable(args[0])) + elif name == "popitem" and self.is_mutable(): + if ( + issubclass(self.user_cls, dict) + and not issubclass(self.user_cls, collections.OrderedDict) + and len(args) + ): + raise_args_mismatch(tx, name) + + if not self.items: + raise_observed_exception( + KeyError, + tx, + args=[ + "popitem(): dictionary is empty", + ], + ) + + if self.user_cls is collections.OrderedDict and ( + len(args) == 1 or "last" in kwargs + ): + if len(args) == 1 and args[0].is_python_constant(): + last = args[0].as_python_constant() + elif (v := kwargs.get("last")) and v.is_python_constant(): + last = v.as_python_constant() + else: + raise_args_mismatch(tx, name) + k, v = self.items.popitem(last=last) # type: ignore[possibly-undefined] + else: + k, v = self.items.popitem() + + self.should_reconstruct_all = True + tx.output.side_effects.mutation(self) + + return variables.TupleVariable([k.vt, v]) + elif name == "clear": + if args or kwargs: + raise_args_mismatch( + tx, + name, + "0 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + self.should_reconstruct_all = True + tx.output.side_effects.mutation(self) + self.items.clear() + return ConstantVariable.create(None) + elif name == "update" and self.is_mutable(): + # In general, this call looks like `a.update(b, x=1, y=2, ...)`. + # Either `b` or the kwargs is omittable, but not both. + self.install_dict_keys_match_guard() + has_arg = len(args) == 1 + has_kwargs = len(kwargs) > 0 + if has_arg or has_kwargs: + tx.output.side_effects.mutation(self) + if has_arg: + dict_vt: VariableTracker + if isinstance(args[0], ConstDictVariable): + # NB - Guard on all the keys of the other dict to ensure + # correctness. + args[0].install_dict_keys_match_guard() + dict_vt = args[0] + else: + dict_vt = DictBuiltinVariable.call_custom_dict( + tx, dict, args[0] + ) + self.items.update(dict_vt.items) # type: ignore[attr-defined] + if has_kwargs: + # Handle kwargs + kwargs_hashable = { + Hashable(VariableTracker.build(tx, k)): v + for k, v in kwargs.items() + } + self.items.update(kwargs_hashable) + return ConstantVariable.create(None) + else: + return super().call_method(tx, name, args, kwargs) + elif name == "__contains__": + if not len(args): + raise_args_mismatch( + tx, + name, + "more than 1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + + arg_hashable = args and is_hashable(args[0]) + if not arg_hashable: + raise_unhashable(args[0], tx) + + self.install_dict_contains_guard(tx, args) + contains = args[0] in self + return VariableTracker.build(tx, contains) + elif name == "setdefault" and self.is_mutable(): + if len(args) not in (1, 2): + raise_args_mismatch( + tx, + name, + "1 or 2 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + + arg_hashable = args and is_hashable(args[0]) + if not arg_hashable: + raise_unhashable(args[0], tx) + + self.install_dict_keys_match_guard() + if kwargs or len(args) > 2: + raise_args_mismatch( + tx, + name, + "at most 2 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + value = self.maybe_getitem_const(args[0]) + if value is not None: + return value + else: + if len(args) == 1: + x = ConstantVariable.create(None) + else: + x = args[1] + tx.output.side_effects.mutation(self) + self.items[Hashable(args[0])] = x + return x + elif name == "move_to_end": + self.install_dict_keys_match_guard() + tx.output.side_effects.mutation(self) + if args[0] not in self: + raise_observed_exception(KeyError, tx) + + last = True + if len(args) == 2 and args[1].is_python_constant(): + last = args[1].as_python_constant() + + if kwargs and "last" in kwargs and kwargs["last"].is_python_constant(): + last = kwargs.get("last").as_python_constant() # type: ignore[union-attr] + + key = Hashable(args[0]) + self.items.move_to_end(key, last=last) + return ConstantVariable.create(None) + elif name == "__eq__" and istype( + self, ConstDictVariable + ): # don't let Set use this function + if len(args) != 1: + raise_args_mismatch(tx, name, "1 args", f"{len(args)} args") + + return SourcelessBuilder.create(tx, polyfills.dict___eq__).call_function( + tx, [self, args[0]], {} + ) + elif name == "__ne__": + return VariableTracker.build( + tx, + not self.call_method(tx, "__eq__", args, kwargs).value, # type: ignore[attr-defined] + ) + elif name == "__or__": + if len(args) != 1: + raise_args_mismatch(tx, name, "1 args", f"{len(args)} args") + other = args[0] + + # Method resolution for binops works as follow (using __or__ as example): + # (1) dict.__or__(dict) => dict + # (2) dict.__or__(subclass): return NotImplemented + # (3) Check if subclass implements __ror__ => forward the call + # to subclass.__ror__(dict) + + # Let's not forward the call to __ror__ yet because __ror__ can be + # implemented in C (i.e. OrderedDict subclass) which Dynamo cannot + # trace + # if istype(other, variables.UserDefinedDictVariable): + # if other.call_obj_hasattr(tx, "__ror__").value: + # return other.call_method(tx, "__ror__", [self], kwargs) + + # The three dict types Dynamo can handle are dict, OrderedDict and + # defaultdict. + + # TODO(guilhermeleobas): this check should be on builtin.py::call_or_ + if istype( + other, + ( + ConstDictVariable, + variables.UserDefinedDictVariable, + variables.DefaultDictVariable, + ), + ): + # Unwrap UserDefinedDictVariable to its underlying ConstDictVariable + if isinstance(other, variables.UserDefinedDictVariable): + assert other._base_vt is not None + assert isinstance(other._base_vt, ConstDictVariable) + other = other._base_vt + + # Always return the specialized dictionary, and in the case + # both are specialized, take the first to be the type of the + # new dictionary + if self.user_cls is not dict: + user_cls = self.user_cls + to_cpy = self + else: + user_cls = other.user_cls + to_cpy = other + + to_cpy.install_dict_keys_match_guard() + new_dict_vt = to_cpy.clone( + items=self.items.copy(), + mutation_type=ValueMutationNew(), + source=None, + user_cls=user_cls, + ) + + # NB - Guard on all the keys of the other dict to ensure + # correctness. + args[0].install_dict_keys_match_guard() # type: ignore[attr-defined] + new_dict_vt.items.update(args[0].items) # type: ignore[attr-defined] + return new_dict_vt + else: + raise_observed_exception( + TypeError, + tx, + args=[ + f"unsupported operand type(s) for |: '{self.python_type().__name__}'" + f"and '{other.python_type().__name__}'" + ], + ) + elif name == "__ior__": + self.call_method(tx, "update", args, kwargs) + return self + elif name == "__iter__": + from .lists import ListIteratorVariable + + if self.source and not is_constant_source(self.source): + tx.output.guard_on_key_order.add(self.source) + return ListIteratorVariable( + self.unpack_var_sequence(tx), mutation_type=ValueMutationNew() + ) + else: + return super().call_method(tx, name, args, kwargs) + + def unpack_var_sequence(self, tx: "InstructionTranslator") -> list[VariableTracker]: + self.install_dict_keys_match_guard() + return [x.vt for x in self.items] + + def mp_length(self, tx: "InstructionTranslator") -> VariableTracker: + """Mapping length for dict objects.""" + self.install_dict_keys_match_guard() + return VariableTracker.build(tx, len(self.items)) + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> ConstantVariable: + # dict not allow setting arbitrary attributes. OrderedDict and + # defaultdict allow arbitrary setattr, but not deletion of default attrs + if any( + self.user_cls is t + for t in (dict, collections.OrderedDict, collections.defaultdict) + ): + if hasattr(self.user_cls, name): + return ConstantVariable.create(True) + if self.user_cls is dict: + return ConstantVariable.create(False) + + msg = f"hasattr on {self.user_cls} is not supported" + unimplemented( + gb_type="unsupported hasattr operation", + context=f"Class {self.user_cls}", + explanation=msg, + hints=[ + "Consider using a regular dictionary instead", + *graph_break_hints.SUPPORTABLE, + ], + ) + + def clone(self, **kwargs: Any) -> VariableTracker: + self.install_dict_keys_match_guard() + return super().clone(**kwargs) + + def is_python_hashable(self) -> bool: + """ + Dictionaries are mutable and therefore not hashable in Python. + """ + return False + + def var_getattr(self, tx: "InstructionTranslator", name: str): + if name == "__class__": + return VariableTracker.build(tx, self.python_type()) + return super().var_getattr(tx, name) + + +class MappingProxyVariable(VariableTracker): + # PyDictProxy_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/descrobject.c#L1995 + _cpython_type = types.MappingProxyType + + # proxies to the original dict_vt + def __init__(self, dv_dict: ConstDictVariable, **kwargs: Any) -> None: + super().__init__(**kwargs) + assert isinstance(dv_dict, ConstDictVariable) + self.dv_dict = dv_dict + + def python_type(self) -> type: + return types.MappingProxyType + + def unpack_var_sequence(self, tx: "InstructionTranslator") -> list[VariableTracker]: + return self.dv_dict.unpack_var_sequence(tx) + + def reconstruct(self, codegen: "PyCodegen") -> None: + # load types.MappingProxyType + if self.source: + msg = ( + f"Preexisting MappingProxyVariable (source: {self.source}) cannot be reconstructed " + "because the connection to the original dict will be lost." + ) + unimplemented( + gb_type="mapping proxy cannot be reconstructed", + context=f"Source: {self.source}", + explanation=msg, + hints=[ + "Use a mapping proxy constructed in the same `torch.compile` region.", + *graph_break_hints.SUPPORTABLE, + ], + ) + codegen.add_push_null( + lambda: codegen.extend_output( + [ + codegen.create_load_python_module(types), + codegen.create_load_attr("MappingProxyType"), + ] + ) + ) + codegen(self.dv_dict) + codegen.extend_output(create_call_function(1, False)) + + def _check_mutation_guard(self, tx: "InstructionTranslator") -> None: + if self.source and tx.output.side_effects.has_existing_dict_mutation(): + msg = ( + "A dict has been modified while we have an existing mappingproxy object. " + "A mapping proxy object, as the name suggest, proxies a mapping " + "object (usually a dict). If the original dict object mutates, it " + "is reflected in the proxy object as well. For an existing proxy " + "object, we do not know the original dict it points to. Therefore, " + "for correctness we graph break when there is dict mutation and we " + "are trying to access a proxy object." + ) + + unimplemented( + gb_type="mapping proxy affected by dictionary mutation", + context=f"Source: {self.source}, Dict mutation detected", + explanation=msg, + hints=[ + "Avoid modifying dictionaries that might be referenced by mapping proxy objects", + "Or avoid using the mapping proxy objects after modifying its underlying dictionary", + ], + ) + + def mp_subscript_impl( + self, + tx: "InstructionTranslator", + key: VariableTracker, + ) -> VariableTracker: + # mappingproxy_getitem: https://github.com/python/cpython/blob/62a6e898e01/Objects/descrobject.c#L1052-L1056 + # TODO(follow-up): add tests for invalid key type, missing key + self._check_mutation_guard(tx) + return self.dv_dict.mp_subscript_impl(tx, key) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + self._check_mutation_guard(tx) + return self.dv_dict.call_method(tx, name, args, kwargs) + + def mp_length(self, tx: "InstructionTranslator") -> VariableTracker: + return self.dv_dict.mp_length(tx) + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> ConstantVariable: + if self.python_type() is types.MappingProxyType: + return VariableTracker.build(tx, name in types.MappingProxyType.__dict__) + return super().call_obj_hasattr(tx, name) + + +class NNModuleHooksDictVariable(ConstDictVariable): + # Special class to avoid adding any guards on the nn module hook ids. + def install_dict_keys_match_guard(self) -> None: + pass + + def install_dict_contains_guard( + self, tx: "InstructionTranslator", args: list[VariableTracker] + ) -> None: + pass + + +class DefaultDictVariable(ConstDictVariable): + _cpython_type = collections.defaultdict + + def __init__( + self, + items: dict[VariableTracker, VariableTracker], + user_cls: type, + default_factory: VariableTracker | None = None, + **kwargs: Any, + ) -> None: + super().__init__(items, user_cls, **kwargs) + assert user_cls is collections.defaultdict + if default_factory is None: + default_factory = ConstantVariable.create(None) + self.default_factory = default_factory + + def is_python_constant(self) -> bool: + # Return false for unsupported defaults. This ensures that a bad handler + # path is not taken in BuiltinVariable for getitem. + if self.default_factory not in [list, tuple, dict] and not self.items: + return False + return super().is_python_constant() + + def debug_repr(self) -> str: + assert self.default_factory is not None + return ( + f"defaultdict({self.default_factory.debug_repr()}, {super().debug_repr()})" + ) + + @staticmethod + def is_supported_arg(arg: VariableTracker) -> bool: + return isinstance( + arg, + ( + variables.BaseBuiltinVariable, + variables.functions.BaseUserFunctionVariable, + variables.functions.PolyfilledFunctionVariable, + ), + ) or (isinstance(arg, variables.ConstantVariable) and arg.value is None) + + def mp_subscript_impl( + self, + tx: "InstructionTranslator", + key: VariableTracker, + ) -> VariableTracker: + # Mirrors CPython's defaultdict.__getitem__ (dict_subscript → __missing__). + # defaultdict.__missing__: https://github.com/python/cpython/blob/62a6e898e01/Modules/_collectionsmodule.c#L2233-L2254 + # Key present → normal dict lookup (same as ConstDictVariable.mp_subscript_impl). + if key in self: + return self.getitem_const(tx, key) + + if self.default_factory.is_constant_none(): + raise_observed_exception(KeyError, tx, args=[key]) + else: + default_var = self.default_factory.call_function(tx, [], {}) + super().call_method(tx, "__setitem__", [key, default_var], {}) + return default_var + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "__setattr__" and self.is_mutable: + if len(args) != 2: + raise_args_mismatch(tx, name, "2 args", f"{len(args)} args") + # Setting a default factory must be a callable or None type + if ( + istype(args[0], ConstantVariable) and args[0].value == "default_factory" + ) and self.is_supported_arg(args[1]): + tx.output.side_effects.mutation(self) + self.default_factory = args[1] + return ConstantVariable.create(None) + return super().call_method(tx, name, args, kwargs) + elif name == "__eq__": + if len(args) != 1: + raise_args_mismatch(tx, name, "1 args", f"{len(args)} args") + + return VariableTracker.build(tx, polyfills.dict___eq__).call_function( + tx, [self, args[0]], {} + ) + else: + return super().call_method(tx, name, args, kwargs) + + def var_getattr( + self, + tx: "InstructionTranslator", + name: str, + ) -> VariableTracker: + if name == "default_factory": + return self.default_factory + return super().var_getattr(tx, name) + + def reconstruct(self, codegen: "PyCodegen") -> None: + # emit `defaultdict(default_factory, new_dict)` + codegen.add_push_null( + lambda: codegen.extend_output( + [ + codegen.create_load_python_module(collections), + codegen.create_load_attr("defaultdict"), + ] + ) + ) + codegen(self.default_factory) + codegen.extend_output( + [ + *create_call_function(1, False), + create_dup_top(), + ] + ) + codegen.add_cache(self) + + codegen.append_output(create_dup_top()) + codegen.load_method("update") + self.reconstruct_kvs_into_new_dict(codegen) + codegen.extend_output( + [ + *create_call_method(1), + create_instruction("POP_TOP"), + ] + ) + + +class DictViewVariable(VariableTracker): + """ + Models _PyDictViewObject + + This is an "abstract" class. Subclasses will override kv and the items method + """ + + kv: str | None = None + + def __init__(self, dv_dict: ConstDictVariable, **kwargs: Any) -> None: + super().__init__(**kwargs) + assert self.kv in ("keys", "values", "items") + assert isinstance(dv_dict, ConstDictVariable) + self.dv_dict = dv_dict + + @property + def view_items(self) -> Any: + assert self.kv is not None + return getattr(self.dv_dict.items, self.kv)() + + @property + def view_items_vt(self) -> list[VariableTracker]: + # Returns an iterable of the unpacked items + # Implement in the subclasses + raise NotImplementedError + + def unpack_var_sequence(self, tx: "InstructionTranslator") -> list[VariableTracker]: + return self.view_items_vt + + def reconstruct(self, codegen: "PyCodegen") -> None: + assert self.kv is not None + codegen(self.dv_dict) + codegen.load_method(self.kv) + codegen.call_method(0) + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> ConstantVariable: + assert self.kv is not None + if name in self.python_type().__dict__: + return ConstantVariable.create(True) + return ConstantVariable.create(False) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "__iter__": + from .lists import ListIteratorVariable + + return ListIteratorVariable( + self.view_items_vt, mutation_type=ValueMutationNew() + ) + elif name == "__repr__": + return VariableTracker.build(tx, self.debug_repr()) + return super().call_method(tx, name, args, kwargs) + + def sq_length(self, tx: "InstructionTranslator") -> VariableTracker: + """Sequence length for dict view objects.""" + return VariableTracker.build(tx, len(self.view_items)) + + +class DictKeysVariable(DictViewVariable): + # PyDictKeys_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/dictobject.c#L6365 + _cpython_type = dict_keys + + kv = "keys" + + @property + def set_items(self) -> set[VariableTracker]: + return set(self.view_items) + + @property + def view_items_vt(self) -> list[VariableTracker]: + # Returns an iterable of the unpacked items + return [x.vt for x in self.view_items] + + def python_type(self) -> type: + return dict_keys + + def debug_repr(self) -> str: + if not self.view_items: + return "dict_keys([])" + else: + items: list[str] = [] + for k in self.view_items: + key_str = ( + repr(k.vt.value) if hasattr(k.vt, "value") else k.vt.debug_repr() + ) + items.append(key_str) + return "dict_keys([" + ",".join(items) + "])" + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "__contains__": + return self.dv_dict.call_method(tx, name, args, kwargs) + elif name in ( + "__and__", + "__iand__", + "__or__", + "__ior__", + "__sub__", + "__isub__", + "__xor__", + "__ixor__", + ): + # These methods always returns a set + m = getattr(self.set_items, name) + r = m(args[0].set_items) # type: ignore[attr-defined] + return SetVariable(r) + elif name in cmp_name_to_op_mapping: + if not isinstance( + args[0], + ( + SetVariable, + variables.UserDefinedSetVariable, + DictItemsVariable, + DictKeysVariable, + ), + ): + return VariableTracker.build(tx, NotImplemented) + return VariableTracker.build( + tx, + cmp_name_to_op_mapping[name](self.set_items, args[0].set_items), # type: ignore[attr-defined] + ) + return super().call_method(tx, name, args, kwargs) + + +class DictValuesVariable(DictViewVariable): + # PyDictValues_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/dictobject.c#L6567 + _cpython_type = dict_values + + # DictValuesVariable is an iterable but cannot be compared. + kv = "values" + + @property + def view_items_vt(self) -> list[VariableTracker]: + return list(self.view_items) + + def python_type(self) -> type: + return dict_values + + def debug_repr(self) -> str: + if not self.view_items: + return "dict_values([])" + else: + items: list[str] = [] + for v in self.view_items: + val_str = repr(v.value) if hasattr(v, "value") else v.debug_repr() + items.append(val_str) + return "dict_values([" + ",".join(items) + "])" + + +class DictItemsVariable(DictViewVariable): + # PyDictItems_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/dictobject.c#L6477 + _cpython_type = dict_items + + kv = "items" + + @property + def set_items(self) -> set["HashableTracker"]: + return { + HashableTracker(variables.TupleVariable([k.vt, v])) + for k, v in self.view_items + } + + @property + def view_items_vt(self) -> list[VariableTracker]: + # Returns an iterable of the unpacked items + return [variables.TupleVariable([k.vt, v]) for k, v in self.view_items] + + def python_type(self) -> type: + return dict_items + + def debug_repr(self) -> str: + if not self.view_items: + return "dict_items([])" + else: + items: list[str] = [] + for k, v in self.view_items: + key_str = ( + repr(k.vt.value) if hasattr(k.vt, "value") else k.vt.debug_repr() + ) + val_str = repr(v.value) if hasattr(v, "value") else v.debug_repr() + items.append(f"({key_str}, {val_str})") + return "dict_items([" + ",".join(items) + "])" + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + # TODO(guilhermeleobas): This should actually check if args[0] + # implements the mapping protocol. + if name == "__eq__": + if len(args) != 1: + raise_args_mismatch(tx, name, "1 args", f"{len(args)} args") + if isinstance(args[0], DictItemsVariable): + return self.dv_dict.call_method(tx, "__eq__", [args[0].dv_dict], {}) + elif isinstance( + args[0], + ( + SetVariable, + variables.UserDefinedSetVariable, + DictItemsVariable, + DictKeysVariable, + ), + ): + return VariableTracker.build( + tx, + len(self.set_items ^ args[0].set_items) == 0, + ) + return ConstantVariable.create(False) + elif name == "__iter__": + from .lists import ListIteratorVariable + + return ListIteratorVariable( + self.view_items_vt, mutation_type=ValueMutationNew() + ) + elif name in ( + "__and__", + "__iand__", + "__or__", + "__ior__", + "__sub__", + "__isub__", + "__xor__", + "__ixor__", + ): + # These methods always returns a set + fn_hdl = getattr(self.set_items, name) + ret_val = fn_hdl(args[0].set_items) # type: ignore[attr-defined] + return SetVariable(ret_val) + elif name in cmp_name_to_op_mapping: + if not isinstance( + args[0], + ( + SetVariable, + variables.UserDefinedSetVariable, + DictItemsVariable, + DictKeysVariable, + ), + ): + return VariableTracker.build(tx, NotImplemented) + return VariableTracker.build( + tx, + cmp_name_to_op_mapping[name](self.set_items, args[0].set_items), # type: ignore[attr-defined] + ) + return super().call_method(tx, name, args, kwargs) + + def is_python_hashable(self) -> Literal[False]: + """ + Dictionary item views are not hashable in Python. + """ + return False + + +kV = HashableTracker | str + + +class SideEffectsProxyDict(collections.abc.MutableMapping[kV, VariableTracker]): + """ + A proxy dict that allows us to track mutations to the dict using side + effects table as storage. + """ + + @staticmethod + def get_example_value_dict(vt: VariableTracker) -> dict[str, object]: + if istype(vt, variables.NestedUserFunctionVariable): + # NestedUserFunctionVariable is created with MAKE_FUNCTION and its + # __dict__ starts empty. Any mutation will actually be recorded in + # the side effects table. + return {} + elif isinstance(vt, variables.LocalGeneratorFunctionVariable): + return SideEffectsProxyDict.get_example_value_dict(vt.vt) + else: + value = vt.get_real_python_backed_value() + if value is not NO_SUCH_SUBOBJ: + if isinstance(vt, variables.UserDefinedObjectVariable): + return vt._getattr_static("__dict__") # type: ignore[bad-return] + else: + return object.__getattribute__(value, "__dict__") + else: + unimplemented( + gb_type="unsupported variable type for __dict__ access", + context=f"VariableTracker type: {type(vt)}", + explanation=f"Dynamo does not know how to get __dict__ from {type(vt)}", + hints=[ + *graph_break_hints.DYNAMO_BUG, + ], + ) + + @staticmethod + def get_value___dict__( + tx: "InstructionTranslator", vt: VariableTracker + ) -> dict[str, VariableTracker]: + example_value_dict = SideEffectsProxyDict.get_example_value_dict(vt) + + return { + key: VariableTracker.build( + tx, + value, + source=vt.source + and DictGetItemSource(AttrSource(vt.source, "__dict__"), key), + ) + for key, value in example_value_dict.items() + } + + def __init__(self, item: VariableTracker, tx: "InstructionTranslator") -> None: + self.item = item + self.side_effects = tx.output.side_effects + self.item_dict = self.get_value___dict__(tx, item) + + def _maybe_unwrap_key(self, key: kV) -> str: + Hasher = HashableTracker + return key.vt.as_python_constant() if istype(key, Hasher) else key + + def side_effects_table(self) -> dict[str, VariableTracker]: + return self.side_effects.store_attr_mutations.get(self.item, {}) + + def __getitem__(self, key: kV) -> VariableTracker: + name = self._maybe_unwrap_key(key) + if self.side_effects.has_pending_mutation_of_attr(self.item, name): + return self.side_effects.load_attr(self.item, name, deleted_ok=True) + return self.item_dict[name] + + def __setitem__(self, key: kV, value: VariableTracker) -> None: + # Find a way to not hash the key using HashableTracker + name = self._maybe_unwrap_key(key) + assert istype(name, str) + self.side_effects.store_attr(self.item, name, value) + + def __delitem__(self, key: kV) -> None: + name = self._maybe_unwrap_key(key) + self.side_effects.store_attr(self.item, name, variables.DeletedVariable()) + + def __contains__(self, key: kV) -> bool: # type: ignore[bad-override] + name = self._maybe_unwrap_key(key) + table = self.side_effects_table() + # if name in side effects, then it is only contained if it's not a DeletedVariable + # even if the original dict contains it + if name in table: + return not isinstance(table[name], variables.DeletedVariable) + else: + return name in self.item_dict + + def __len__(self) -> int: + return sum(1 for _ in self) + + def __iter__(self) -> Iterator[HashableTracker]: + Hasher = HashableTracker + d = self.side_effects_table() + for k, v in d.items(): + if isinstance(v, variables.DeletedVariable): + continue + yield Hasher(ConstantVariable.create(k)) + + for k, v in self.item_dict.items(): + if k not in d: + yield Hasher(ConstantVariable.create(k)) + + +class DunderDictVariable(ConstDictVariable): + """represents object.__dict__""" + + @classmethod + def create( + cls, + tx: "InstructionTranslator", + vt: VariableTracker, + ) -> "DunderDictVariable": + mutation = AttributeMutationExisting() if vt.source else AttributeMutationNew() + source = vt.source and AttrSource(vt.source, "__dict__") + + return cls( + vt, + tx=tx, + mutation_type=mutation, + source=source, + ) + + def __init__( + self, + vt: VariableTracker, + tx: "InstructionTranslator", + **kwargs: Any, + ) -> None: + super().__init__({}, **kwargs) + self.items = SideEffectsProxyDict(vt, tx) + + def setitem(self, name: str, value: VariableTracker) -> None: + self.items[name] = value + + def getitem(self, name: str) -> VariableTracker: + return self.items[name] + + def contains(self, name: str) -> bool: + return name in self.items + + def getitem_or_default( + self, + name: str, + default: Callable[[], VariableTracker], + ) -> VariableTracker: + if self.contains(name): + return self.getitem(name) + else: + value = default() + self.items[name] = value + return value + + # Mutations to __dict__ are tracked through side effects (SideEffectsProxyDict), + # so we don't need to install guards. Guard installation is overridden to no-op. + def install_dict_keys_match_guard(self) -> None: + pass + + def install_dict_contains_guard( + self, tx: "InstructionTranslator", args: list[VariableTracker] + ) -> None: + pass diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/distributed.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/distributed.py new file mode 100644 index 0000000000000000000000000000000000000000..9714a9960ed0dced3d12277197bc278254d3a204 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/distributed.py @@ -0,0 +1,258 @@ +""" +Distributed computing variable tracking classes for PyTorch Dynamo. + +This module implements variable tracking for distributed computing components: +- Process Groups (for collective communication) +- Device Meshes (for distributed tensor sharding) +- Placement Types (for specifying distribution strategies) +- Distributed Tensors and their operations +- Backward hooks for distributed module operations + +These classes are responsible for tracking distributed operations during graph +compilation while maintaining proper guards and handling distributed-specific +behaviors. They ensure correct handling of distributed components like process +groups, device meshes, and placement strategies while preserving proper semantics +for distributed tensor operations in the compiled code. + +The implementation provides special handling for distributed package availability +checks and proper tracking of distributed state and operations across processes. +""" + +import functools +import inspect +from typing import Any, Literal, TYPE_CHECKING + +import torch +from torch.fx.experimental._backward_state import BackwardState + +from .. import compiled_autograd +from .._trace_wrapped_higher_order_op import trace_wrapped +from ..exc import unimplemented +from ..external_utils import call_module_hooks_from_backward_state +from ..guards import GuardBuilder, install_guard +from ..source import AttrSource +from .base import VariableTracker + + +if TYPE_CHECKING: + from torch._dynamo.symbolic_convert import InstructionTranslator + + +class DistributedVariable(VariableTracker): + """ + The base distributed variable that encapsulates common methods + for the distributed objects (i.e. ProcessGroup, DeviceMesh, etc.). + Concrete distributed objects could inherit this class and add object + specific logic. + + i.e. It provides the check on the distributed package existence + and hold the tracking value for the corresponding distributed object. + """ + + def __init__(self, value: Any, **kwargs: Any) -> None: + super().__init__(**kwargs) + if not DistributedVariable.is_available(): + unimplemented( + gb_type="torch.distributed package is not available!", + context="", + explanation="The PyTorch package doesn't include torch.distributed when building from source.", + hints=[ + "Set USE_DISTRIBUTED=1 to enable it when building PyTorch from source." + ], + ) + self.value = value + + def python_type(self) -> type: + return type(self.value) + + @staticmethod + def is_available() -> bool: + # check if the distributed package is available or not + return torch.distributed.is_available() + + def is_python_hashable(self) -> Literal[True]: + return True + + def get_python_hash(self) -> int: + return hash(self.value) + + def is_python_equal(self, other: object) -> bool: + return ( + isinstance(other, VariableTracker) + and self.as_python_constant() == other.as_python_constant() + ) + + +def is_from_local(value: object) -> bool: + if not DistributedVariable.is_available(): + return False + from torch.distributed.tensor import DTensor + + return inspect.isfunction(value) and value is DTensor.from_local + + +def is_constant_pg_functions(value: object) -> bool: + if not DistributedVariable.is_available(): + return False + + from torch.distributed.distributed_c10d import ( + _get_group_size_by_name, + _get_group_tag, + _rank_not_in_group, + _resolve_group_name_by_ranks_and_tag, + get_process_group_ranks, + ) + + constant_processgroup_functions = [ + _get_group_size_by_name, + _get_group_tag, + _rank_not_in_group, + get_process_group_ranks, + _resolve_group_name_by_ranks_and_tag, + ] + + return inspect.isfunction(value) and value in constant_processgroup_functions + + +class WorldMetaClassVariable(DistributedVariable): + """ + Tracks torch.distributed.GroupMember and torch.distributed.group, which are + instances of the metaclass _WorldMeta. + """ + + @classmethod + def is_group_member_type(cls, value: object) -> bool: + if not cls.is_available(): + return False + + from torch.distributed.distributed_c10d import _WorldMeta + + return type(value) is _WorldMeta + + def python_type(self) -> type: + return type(self.value) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + if name == "WORLD": + from .builder import SourcelessBuilder + + assert self.source + source = AttrSource(base=self.source, member="WORLD") + install_guard(source.make_guard(GuardBuilder.ID_MATCH)) + return SourcelessBuilder.create(tx, self.value.WORLD) + elif name == "NON_GROUP_MEMBER": + assert self.source + source = AttrSource(base=self.source, member="NON_GROUP_MEMBER") + install_guard(source.make_guard(GuardBuilder.ID_MATCH)) + return VariableTracker.build(tx, self.value.NON_GROUP_MEMBER) + return super().var_getattr(tx, name) + + +class BackwardHookVariable(VariableTracker): + """ + Handles torch.utils.hooks.BackwardHook for module-level backward + hooks. + """ + + @staticmethod + def create( + tx: "InstructionTranslator", + module: VariableTracker, + user_hooks: VariableTracker, + user_pre_hooks: VariableTracker, + ) -> "BackwardHookVariable": + if not compiled_autograd.compiled_autograd_enabled: + unimplemented( + gb_type="Module-level backwards hooks require compiled autograd.", + context="", + explanation="", + hints=[ + "Enable compiled autograd by setting torch._dynamo.config.compiled_autograd = True." + ], + ) + + def _in_graph_bw_hooks( + bw_state: BackwardState, + ) -> torch.utils.hooks.BackwardHook: + """ + Rather than installing the user hooks in the graph (which + don't survive AotAutograd), we install hooks that will call + trace_wrapped in the backward pass that CompiledAutograd + can turn into actual hook calls. + """ + return torch.utils.hooks.BackwardHook( + None, + ( + functools.partial( + trace_wrapped, + fn=call_module_hooks_from_backward_state, + bw_state=bw_state, + hooks_name=user_hooks_name, + module_name=module_name, + ), + ), + ( + functools.partial( + trace_wrapped, + fn=call_module_hooks_from_backward_state, + bw_state=bw_state, + hooks_name=user_pre_hooks_name, + module_name=module_name, + ), + ), + ) + + module_name, bw_state_proxy = tx.output.add_backward_state_hook(module, "mod") + user_pre_hooks_name, _ = tx.output.add_backward_state_hook(user_pre_hooks) + user_hooks_name, _ = tx.output.add_backward_state_hook(user_hooks) + proxy = tx.output.create_proxy( + "call_function", + _in_graph_bw_hooks, + (bw_state_proxy,), + {}, + ) + proxy.node.meta["example_value"] = torch.utils.hooks.BackwardHook(None, (), ()) + return BackwardHookVariable(proxy, module, user_hooks, user_pre_hooks) + + def __init__( + self, + proxy: torch.fx.Proxy, + module: VariableTracker, + user_hooks: VariableTracker, + user_pre_hooks: VariableTracker, + **options: Any, + ) -> None: + super().__init__(**options) + self.proxy = proxy + self.module = module + self.user_hooks = user_hooks + self.user_pre_hooks = user_pre_hooks + + def as_proxy(self) -> torch.fx.Proxy: + return self.proxy + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name in ("setup_input_hook", "setup_output_hook"): + return self._setup_hook(tx, name, *args, **kwargs) + return super().call_method(tx, name, args, kwargs) + + def _setup_hook( + self, tx: "InstructionTranslator", hook_method_name: str, args: VariableTracker + ) -> VariableTracker: + from .builder import wrap_fx_proxy + + return wrap_fx_proxy( + tx, + tx.output.create_proxy( + "call_method", + hook_method_name, + (self.as_proxy(), args.as_proxy()), + {}, + ), + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/functions.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/functions.py new file mode 100644 index 0000000000000000000000000000000000000000..9f17481accb5e79897e7a54f3e6927dda7fcbe6e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/functions.py @@ -0,0 +1,3658 @@ +""" +Function-related variable tracking classes for Dynamo's symbolic execution. + +This module contains classes that track different types of functions during graph +compilation, including: +- User-defined functions and methods +- Built-in functions and methods +- Wrapped functions (e.g. from decorators) +- Special function types (e.g. functools.partial) +- Triton kernels and related function types + +These classes are responsible for: +- Tracking function calls and their arguments +- Managing function closures and cell variables +- Handling function attributes and special methods +- Maintaining guards for function identity and closure contents +- Supporting function inlining and specialization +- Enabling proper symbolic execution of different function types + +The variable trackers here work together with the rest of Dynamo to enable +accurate graph capture while handling Python's various function-related behaviors. +""" + +import builtins +import functools +import inspect +import itertools +import logging +import os +import sys +import traceback +import types +from collections import namedtuple +from collections.abc import Callable, Sequence +from types import CellType, FunctionType +from typing import Any, cast, Literal, Optional, TYPE_CHECKING, TypeVar +from typing_extensions import Never +from weakref import WeakKeyDictionary + +import torch +from torch._dynamo.exc import get_stack_above_dynamo +from torch._guards import Source +from torch.utils._pytree import is_namedtuple_class + +from .. import config, graph_break_hints, polyfills, variables +from ..bytecode_transformation import create_call_function, create_rot_n, is_generator +from ..exc import ( + format_frame_info, + get_dynamo_observed_exception, + handle_observed_exception, + InfiniteGeneratorError, + ObservedException, + ObservedGeneratorExit, + ObservedUserStopIteration, + raise_observed_exception, + raise_type_error, + StepUnsupported, + unimplemented, + Unsupported, +) +from ..guards import GuardBuilder, install_guard +from ..source import ( + AttrSource, + CellContentsSource, + ClosureSource, + ConstantSource, + DefaultsSource, + GetItemSource, + ImportSource, + SkipGuardSource, + TypeSource, +) +from ..utils import ( + check_constant_args, + check_unspec_or_constant_args, + cmp_name_to_op_mapping, + identity, + is_function, + is_wrapper_or_member_descriptor, + istype, + make_cell, +) +from .base import ( + AsPythonConstantNotImplementedError, + AttributeMutationNew, + ValueMutationNew, + VariableTracker, +) +from .constant import ConstantVariable +from .user_defined import UserDefinedObjectVariable + + +try: + from torch.distributed.fsdp._fully_shard import _fsdp_param_group +except ModuleNotFoundError: + _fsdp_param_group = None # type: ignore[assignment] + + +if TYPE_CHECKING: + from torch._dynamo.codegen import PyCodegen + from torch._dynamo.symbolic_convert import ( + InliningGeneratorInstructionTranslator, + InliningInstructionTranslator, + InstructionTranslator, + InstructionTranslatorBase, + ) + from torch._dynamo.variables.ctx_manager import ContextWrappingVariable + from torch._higher_order_ops.triton_kernel_wrap import ( + TritonGridType, + TritonKernelType, + ) + + from .dicts import DunderDictVariable + from .lists import BaseListVariable, ListVariable + from .tensor import TensorVariable + + +_F = TypeVar("_F", bound=Callable[..., Any]) +CO_VARARGS = 0x04 +CO_VARKEYWORDS = 0x08 +_SUPPORTED_TREE_MAP_KWARGS = frozenset({"namespace", "none_is_leaf", "is_leaf"}) +_TREE_MAP_ONLY_SUPPORTED_KWARGS = frozenset({"is_leaf"}) + +PT2_ISSUE_TRACKER_URL = "https://github.com/pytorch/pytorch/issues/new?&labels=oncall%3A+pt2&projects=&template=pt2-bug-report.yml" + +# Module-level cache keyed by the function object +_spec_cache: WeakKeyDictionary[Any, Any] = WeakKeyDictionary() + + +# Raised when get_function() cannot convert a nested function to a Python function. +class ClosureConversionError(NotImplementedError): + pass + + +@functools.lru_cache +def get_pytree_SUPPORTED_NODES_source() -> AttrSource: + return AttrSource( + AttrSource(AttrSource(ImportSource("torch"), "utils"), "_pytree"), + "SUPPORTED_NODES", + ) + + +class FunctionSpec: + def __init__(self, func: FunctionType) -> None: + code = func.__code__ + vn = code.co_varnames + + self.posonly_count = code.co_posonlyargcount + self.arg_count = code.co_argcount + self.kwonly_count = code.co_kwonlyargcount + + self.posonly_names = vn[: self.posonly_count] + self.pos_or_kw_names = vn[self.posonly_count : self.arg_count] + self.all_pos_names = self.posonly_names + self.pos_or_kw_names + self.kwonly_names = vn[self.arg_count : self.arg_count + self.kwonly_count] + + off = self.arg_count + self.kwonly_count + self.varargs_name = vn[off] if code.co_flags & CO_VARARGS else None + off += 1 if self.varargs_name else 0 + self.varkw_name = vn[off] if code.co_flags & CO_VARKEYWORDS else None + + def update_defaults(self, func: FunctionType) -> None: + # Defaults can change from function call to function call. So re-update + # them on every call. + self.defaults = func.__defaults__ or () + self.kwdefaults = func.__kwdefaults__ or {} + + # Map positional-default names → their index in self.defaults + self.pos_default_map = dict( + zip(self.all_pos_names[-len(self.defaults) :], range(len(self.defaults))) + ) + + +def _get_spec(func: FunctionType) -> FunctionSpec: + spec = _spec_cache.get(func) + if spec is None: + spec = FunctionSpec(func) + _spec_cache[func] = spec + return spec + + +def bind_args_cached( + func: FunctionType, + tx: "InstructionTranslator", + fn_source: Source | None, + args: Sequence[Any], + kwargs: dict[str, Any], +) -> dict[str, VariableTracker]: + spec = _get_spec(func) + + # Fast path: simple positional-only, no defaults, no varargs/varkw + # This is the common case for small utility functions called repeatedly. + if ( + len(args) == spec.arg_count + and not func.__defaults__ + and not kwargs + and not spec.varargs_name + and not spec.varkw_name + and not spec.kwonly_names + ): + return { + name: wrap_bound_arg(tx, args[i]) + for i, name in enumerate(spec.all_pos_names) + } + + # Full path with all features + spec.update_defaults(func) + ba = {} + rem_kw = dict(kwargs) + + # 1) Bind all positional (pos-only + pos-or-kw) + # 1.1) Apply pos-defaults first (maybe overridden later) + for name, idx in spec.pos_default_map.items(): + default_source = None + if fn_source and not ( + ConstantVariable.is_literal(spec.defaults[idx]) + and config.skip_guards_on_constant_func_defaults + ): + default_source = DefaultsSource(fn_source, idx) + ba[name] = wrap_bound_arg(tx, spec.defaults[idx], default_source) + # 1.2) Fill in provided positional args + for i, name in enumerate(spec.all_pos_names): + if i < len(args): + # Maybe override pos-defaults applied above + ba[name] = wrap_bound_arg(tx, args[i]) + elif name in rem_kw and ( + # `kwargs` can have the same key as a pos-only arg `name`. + # If this case happens, we should not consume the `name` here and + # keep it in `kwargs`: + # >>> def fn(a, /, **kwargs): return (a, kwargs) + # >>> fn(1, a=2) + # (1, {'a': 2}) + name not in spec.posonly_names + ): + # Maybe override pos-defaults applied above + ba[name] = wrap_bound_arg(tx, rem_kw.pop(name)) + elif name not in ba: + raise TypeError(f"missing required positional argument: {name}") + + # 2) *args + extra = args[len(spec.all_pos_names) :] + if spec.varargs_name: + ba[spec.varargs_name] = wrap_bound_arg(tx, tuple(extra)) + elif extra: + raise TypeError( + f"Too many positional arguments: got {len(args)}, expected {len(spec.all_pos_names)}" + ) + + # 3) Keyword-only + for name in spec.kwonly_names: + if name in rem_kw: + ba[name] = wrap_bound_arg(tx, rem_kw.pop(name)) + elif name in spec.kwdefaults: + kwdefault_source = None + if fn_source: + kwdefault_source = DefaultsSource(fn_source, name, is_kw=True) + ba[name] = wrap_bound_arg(tx, spec.kwdefaults[name], kwdefault_source) + else: + raise TypeError(f"Missing required keyword-only argument: {name}") + + # 4) **kwargs + if spec.varkw_name: + ba[spec.varkw_name] = wrap_bound_arg(tx, rem_kw) + elif rem_kw: + raise TypeError(f"Unexpected keyword arguments: {list(rem_kw)}") + + return ba + + +def wrap_bound_arg( + tx: "InstructionTranslator", val: Any, source: Source | None = None +) -> VariableTracker: + # Source propagation is best effort since not every object we encounter has a source to begin with. + if isinstance(val, VariableTracker): + return val + elif not source: + return VariableTracker.build(tx, val) + else: + # Create a lazy variable to avoid guarding on __defaults__ unless really + # needed. + return variables.LazyVariableTracker.create(val, source) + + +def wrap_args_kwargs(tx: "InstructionTranslator", result: dict[str, Any]) -> None: + for k, v in list(result.items()): + if isinstance(v, (tuple, dict)): + # args/kwargs + result[k] = wrap_bound_arg(tx, v) + + +def init_cellvars( + parent: "InstructionTranslator", + result: dict[str, VariableTracker], + code: types.CodeType, +) -> None: + """ + Update `result` to add mapping from local name to new cells created + directly by `code`, or update SideEffects in `parent` if the a local cell is + already in `result` (cell argument). + """ + side_effects = parent.output.side_effects + + for name in code.co_cellvars: + new_cell = side_effects.track_cell_new() + if name in result: + # This handles when a function argument is a cell (e.g., captured by + # a nested func). See `MAKE_CELL` bytecode for more info. + side_effects.store_cell(new_cell, result.pop(name)) + result[name] = new_cell + + +def _create_nested_fn( + code: types.CodeType, + f_globals: dict[str, Any], + name: str, + defaults: tuple[object, ...] | None, + closure: tuple[CellType] | None, + kwdefaults: dict[str, Any] | None, + annotations: dict[str, Any] | None, +) -> types.FunctionType: + from types import FunctionType + + func = FunctionType(code, f_globals, name, defaults, closure) + func.__kwdefaults__ = kwdefaults + + if isinstance(annotations, tuple): + from itertools import pairwise + + annotations = dict(pairwise(annotations)) + + # TypeError: __annotations__ must be set to a dict object + assert annotations is None or isinstance(annotations, dict) + func.__annotations__ = annotations # type: ignore[assignment] + + return func + + +fn_known_dunder_attrs = { + "__annotations__", + "__closure__", + "__code__", + "__defaults__", + "__doc__", + "__globals__", + "__kwdefaults__", + "__name__", + "__module__", +} + + +def fn_var_getattr( + tx: "InstructionTranslator", fn: object, source: Source | None, name: str +) -> VariableTracker: + source = source and AttrSource(source, name) + + if source and name == "__annotations__": + # We get a large number of silly guards from annotations from inspect + # module. Changing annotations is rare, and it impacting the extracted + # graph is even rarer. So skip guards. + source = SkipGuardSource(source) + + subobj = None + try: + subobj = inspect.getattr_static(fn, name) + except AttributeError: + # function does not have a __getattr__ or __getattribute__ method, + # so we can safely assume that this attribute is absent + raise_observed_exception(AttributeError, tx) + + # Special handling for known dunder attributes + # TODO(guilhermeleobas): this check should go through fn.__dict__ first as + # functools.partial can override it + if name in fn_known_dunder_attrs: + subobj = getattr(fn, name) + if source: + return variables.LazyVariableTracker.create(subobj, source) + return VariableTracker.build(tx, subobj) + + +class BaseUserFunctionVariable(VariableTracker): + def __init__( + self, dict_vt: "DunderDictVariable | None" = None, **kwargs: Any + ) -> None: + super().__init__(**kwargs) + self.dict_vt: DunderDictVariable | None = dict_vt + + def get_source(self) -> Source | None: + return self.source + + def get_dict_vt(self, tx: "InstructionTranslator") -> "DunderDictVariable": + if self.dict_vt is None: + self.dict_vt = variables.DunderDictVariable.create(tx, self) + return self.dict_vt + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "__setattr__": + return self.get_dict_vt(tx).call_method( + tx, "__setitem__", list(args), kwargs + ) + elif name == "__delattr__": + return self.get_dict_vt(tx).call_method(tx, "__delitem__", list(args), {}) + return super().call_method(tx, name, list(args), kwargs) + + def get_filename(self) -> str: + return self.get_code().co_filename + + def get_name(self) -> str: + return self.get_code().co_name + + def get_qualname(self) -> str: + if sys.version_info >= (3, 11): + return self.get_code().co_qualname + else: + return self.get_name() + + def get_doc(self) -> str | None: + # stored in code.co_consts[0] + return self.get_code().co_consts[0] + + def get_globals(self) -> dict[str, Any]: + raise NotImplementedError + + def get_code(self) -> types.CodeType: + raise NotImplementedError + + def has_self(self) -> bool: + raise NotImplementedError + + def get_function(self) -> types.FunctionType: + raise NotImplementedError + + def get_module(self) -> str: + return self.get_globals()["__name__"] + + def var_getattr(self, tx: "InstructionTranslator", name: str): + fn_dict = self.get_dict_vt(tx) + + # missing: __globals__, __closure__, __kwdefautls__, __defaults__ + if name in ("__name__", "__qualname__", "__doc__", "__module__", "__code__"): + val = getattr(self, f"get_{name[2:-2]}")() + if fn_dict.contains(name): + return fn_dict.getitem(name) + return ConstantVariable.create( + val, source=self.source and AttrSource(self.source, name) + ) + elif name == "__dict__": + return fn_dict + elif name == "__annotations__": + return fn_dict.getitem_or_default( + name, + lambda: variables.ConstDictVariable( + {}, + mutation_type=ValueMutationNew(), + ), + ) + elif name == "__type_params__": + return fn_dict.getitem_or_default( + name, + lambda: variables.TupleVariable( + [], + mutation_type=ValueMutationNew(), + ), + ) + else: + if fn_dict.contains(name): + return fn_dict.getitem(name) + else: + raise_observed_exception(AttributeError, tx) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + # Ignore patch_track_step_called from torch/optim/lr_scheduler.py - it just patches + # the optimizer.step method and we don't need to trace it + if ( + self.get_name() == "patch_track_step_called" + and self.get_filename().endswith("torch/optim/lr_scheduler.py") + ): + return ConstantVariable.create(None) + return tx.inline_user_function_return(self, [*self.self_args(), *args], kwargs) # type: ignore[attr-defined] + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> ConstantVariable: + result = False + + if name in fn_known_dunder_attrs or name == "__dict__": + result = True + else: + try: + result = hasattr(self.get_function(), name) # type: ignore[attr-defined] + except NotImplementedError: + result = False + return VariableTracker.build(tx, result) + + def closure_vars(self, tx: "InstructionTranslator") -> dict[str, VariableTracker]: + return {} + + # Override to set whether or not nested graph breaks should be allowed + # if we create an inlining tx for this BaseUserFunctionVariable. + # See symbolic_convert.py for where this function is called. + def should_allow_nested_graph_breaks(self) -> bool: + return True + + +class UserFunctionVariable(BaseUserFunctionVariable): + """Some unsupported user-defined global function""" + + # PyFunction_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/funcobject.c#L1046 + _cpython_type = types.FunctionType + + _nonvar_fields = { + "fn", + "is_constant", + *BaseUserFunctionVariable._nonvar_fields, + } + + _TREE_MAP_MODULES = frozenset( + { + "optree", + "optree.ops", + "torch.utils._pytree", + "torch.utils._cxx_pytree", + } + ) + + @classmethod + def create_with_source(cls, value: Any, source: Any) -> "UserFunctionVariable": + install_guard(source.make_guard(GuardBuilder.CLOSURE_MATCH)) + return cls(value, source=source) + + def __init__( + self, + fn: types.FunctionType | torch.jit.ScriptFunction, # type: ignore[type-arg] + is_constant: bool = False, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + if getattr(fn, "_dynamo_marked_constant", False): + # This method should be treated as a constant for the purposes of compilation + self.is_constant = True + else: + self.is_constant = False + + # TODO putting this here to avoid duplication, because we could hit this + # from several paths (e.g., SuperVariable or `var_getattr`s). + if not isinstance(fn, (types.FunctionType, torch.jit.ScriptFunction)): + unimplemented( + gb_type="can't handle functions not implemented in python ", + context=f"{fn}", + explanation="Dynamo can only handle functions defined in python", + hints=[ + "Move usage of this function out of `torch.compile` region", + *graph_break_hints.INFERENCE_MODE, + ], + ) + # TODO(anijain2305) - Replace directly calling UserFunctionVariable with + # VariableBuilder, which handles the wrapping of _torchdynamo_inline. + # unpack @torch._dynamo.optimize()(fn) wrapped function + fn = inspect.getattr_static(fn, "_torchdynamo_inline", fn) + self.fn = fn + + def as_python_constant(self) -> Any: + if istype(self, UserFunctionVariable): + return self.fn + # subclasses (such as methods) usually aren't a constant + return super().as_python_constant() + + def get_real_python_backed_value(self) -> Any: + if istype(self, UserFunctionVariable): + return self.fn + return super().get_real_python_backed_value() + + def self_args(self) -> list[VariableTracker]: + return [] + + def get_function(self) -> types.FunctionType: + return self.fn + + def get_code(self) -> types.CodeType: + return self.fn.__code__ + + def python_type(self) -> type: + return types.FunctionType + + def has_self(self) -> bool: + return getattr(self.fn, "__self__", None) is not None + + def get_globals(self) -> dict[str, Any]: + return self.fn.__globals__ + + def get_source(self) -> Source: + source = self.source + + if source and isinstance(self, variables.UserMethodVariable): + source = self.source_fn # type: ignore[assignment] + return source # type: ignore[return-value] + + def bind_args( + self, + parent: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> dict[str, VariableTracker]: + """ + Assume `args` and `kwargs` are VariableTracker arguments for a call to + this function, create new bindings for initial locals. + """ + assert not self.is_constant + + fn: types.FunctionType = self.fn + + if not isinstance(fn, FunctionType): + raise TypeError("Only supports regular Python functions.") + root_tx = parent.output.root_tx + + source = self.get_source() + result = bind_args_cached(fn, root_tx, source, args, kwargs) # type: ignore[arg-type] + + init_cellvars(parent, result, fn.__code__) + closure = self.fn.__closure__ or () + assert len(closure) == len(self.fn.__code__.co_freevars) + for idx, name, cell in zip( + itertools.count(), self.fn.__code__.co_freevars, closure + ): + # TODO refactor these 3 branches. + side_effects = parent.output.side_effects + if cell in side_effects: + cell_var = side_effects[cell] + + elif source: + closure_cell = GetItemSource(ClosureSource(source), idx) + closure_cell_contents = CellContentsSource( + closure_cell, "cell_contents", freevar_name=name + ) + try: + contents_var = VariableTracker.build( + parent, cell.cell_contents, closure_cell_contents + ) + except ValueError: + # Cell has not yet been assigned + contents_var = variables.DeletedVariable() + cell_var = side_effects.track_cell_existing( + closure_cell, cell, contents_var + ) + + else: + # TODO figure out why source isn't available here, and whether + # we can fix that and remove this branch. + try: + contents_var = VariableTracker.build(parent, cell.cell_contents) + except ValueError: + # Cell has not yet been assigned + contents_var = variables.DeletedVariable() + cell_var = side_effects.track_cell_existing(None, cell, contents_var) + + result[name] = cell_var + + return result + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + if name == "__dict__": + return super().var_getattr(tx, name) + elif name in cmp_name_to_op_mapping: + return variables.GetAttrVariable( + self, name, py_type=type(getattr(self.fn, name)) + ) + source = self.get_source() + return fn_var_getattr(tx, self.fn, source, name) + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> ConstantVariable: + result = hasattr(self.fn, name) + return VariableTracker.build(tx, result) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + # Handle patch_dynamo_config call + if self.fn is torch._dynamo.patch_dynamo_config: + try: + args_const = [arg.as_python_constant() for arg in args] + kwargs_const = { + key: val.as_python_constant() for key, val in kwargs.items() + } + changes = torch._dynamo.patch_dynamo_config( + *args_const, **kwargs_const + ).changes + return variables.DynamoConfigPatchVariable(changes) + except AsPythonConstantNotImplementedError as e: + raise RuntimeError( + "Cannot convert patch_dynamo_config args/kwargs to constants. " + "Please fix your call to patch_dynamo_config by using simpler inputs. " + f"args: {args}, kwargs: {kwargs}" + ) from e + elif self.fn is torch._dynamo.error_on_graph_break: + try: + bound = inspect.signature(self.fn).bind(*args, **kwargs) + error_on_graph_break = bound.arguments[ + "error_on_graph_break" + ].as_python_constant() + assert isinstance(error_on_graph_break, bool) + return variables.ErrorOnGraphBreakVariable(error_on_graph_break) + except Exception as e: + raise RuntimeError( + "Improper error_on_graph_break() call. Please fix your call to error_on_graph_break(). " + f"args: {args}, kwargs: {kwargs}" + ) from e + elif self.fn is torch._dynamo.override_cudagraphs: + try: + bound = inspect.signature(self.fn).bind(*args, **kwargs) + bound.apply_defaults() + fwd = bound.arguments["fwd"] + bwd = bound.arguments["bwd"] + if isinstance(fwd, VariableTracker): + fwd = fwd.as_python_constant() + if isinstance(bwd, VariableTracker): + bwd = bwd.as_python_constant() + return variables.CudagraphOverrideVariable(fwd, bwd) + except Exception as e: + raise RuntimeError( + "Improper override_cudagraphs() call. Please fix your call to override_cudagraphs(). " + f"args: {args}, kwargs: {kwargs}" + ) from e + elif self.fn is torch._dynamo.bytecode_debugger.breakpoint: + tx.output._emit_debugger_breakpoint = True + return variables.ConstantVariable.create(None) + # Handle a `nonstrict_trace(fn)` call + elif self.fn is torch._dynamo.nonstrict_trace: + bound = inspect.signature(self.fn).bind(*args, **kwargs) + fn_var = bound.args[0] + if not isinstance(fn_var, BaseUserFunctionVariable): + typ = fn_var.python_type() + msg = f"`nonstrict_trace` expects a callable, but got value of type <{typ.__name__}>" + unimplemented( + gb_type="TypeError from user code", + context=f"call_function({self.value}, {args}, {kwargs})", # type: ignore[attr-defined] + explanation=msg, + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + + if not isinstance(fn_var, UserFunctionVariable): + fn_name = fn_var.get_name() + msg = f"Applying `nonstrict_trace` to function <{fn_name}>; however, `nonstrict_trace` currently requires the function to be defined outside `torch.compile` region." # noqa: B950 + unimplemented( + gb_type="Limitation of `nonstrict_trace", + context=f"{self}", + explanation=msg, + hints=[ + f"make sure definition of {fn_name} is outside ", + "`torch.compile` region", + ], + ) + + fn = fn_var.fn + return variables.TorchInGraphFunctionVariable( + fn, kind=variables.torch.AllowInGraphKind.NONSTRICT_TRACE + ) + + if self.is_constant: + return invoke_and_store_as_constant( + tx, self.fn, self.get_name(), args, kwargs + ) + + if ( + not tx.output.current_tracer.unsafe_allow_externally_visible_side_effects + and self.fn + is torch._dynamo.utils._disable_side_effect_safety_checks_for_current_subtracer + ): + with torch._dynamo.side_effects.allow_externally_visible_side_effects_in_subtracer( + tx + ): + return super().call_function(tx, args, kwargs) + + if ( + getattr(tx.output.current_tracer, "description", None) + == "torch.utils.checkpoint.checkpoint" + and not tx.output.current_tracer.allow_side_effects_in_hop + ): + try: + from torch.distributed.fsdp._fully_shard._fsdp_state import FSDPState + except Exception: + FSDPState = None # type: ignore[assignment, misc] + if FSDPState is not None and self.fn in [ + FSDPState._pre_forward, + FSDPState._post_forward, + ]: + with torch._dynamo.side_effects.allow_side_effects_in_hop(tx): + return super().call_function(tx, args, kwargs) + + tree_map_result = self._maybe_call_tree_map_fastpath(tx, args, kwargs) + if tree_map_result is not None: + return tree_map_result + + return super().call_function(tx, args, kwargs) + + def _maybe_call_tree_map_fastpath( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker | None: + rewrite = self._rewrite_tree_map_only_call(tx, args, kwargs) + if rewrite is not None: + tree_map_fn, tree_map_args, tree_map_kwargs = rewrite + else: + tree_map_fn = self + tree_map_args = args + tree_map_kwargs = kwargs + + is_tree_map = ( + isinstance(tree_map_fn, UserFunctionVariable) + and tree_map_fn._is_tree_map_function() + ) + is_tree_map_with_path = ( + isinstance(tree_map_fn, UserFunctionVariable) + and tree_map_fn._is_tree_map_with_path_function() + ) + + if not (is_tree_map or is_tree_map_with_path): + return None + if {*tree_map_kwargs} - _SUPPORTED_TREE_MAP_KWARGS: + return None + if len(tree_map_args) < 2: + return None + + map_fn = tree_map_args[0] + first_tree = tree_map_args[1] + rest = tree_map_args[2:] + + if is_tree_map_with_path: + return first_tree.call_tree_map_with_path( + tx, + tree_map_fn, + map_fn, + rest, + tree_map_kwargs, + keypath=(), + ) + else: + return first_tree.call_tree_map( + tx, + tree_map_fn, + map_fn, + rest, + tree_map_kwargs, + ) + + def _is_tree_map_function(self) -> bool: + return ( + getattr(self.fn, "__name__", None) == "tree_map" + and getattr(self.fn, "__module__", None) in self._TREE_MAP_MODULES + ) + + def _is_tree_map_with_path_function(self) -> bool: + return ( + getattr(self.fn, "__name__", None) == "tree_map_with_path" + and getattr(self.fn, "__module__", None) in self._TREE_MAP_MODULES + ) + + def _is_tree_map_only_function(self) -> bool: + return ( + getattr(self.fn, "__name__", None) == "tree_map_only" + and getattr(self.fn, "__module__", None) in self._TREE_MAP_MODULES + ) + + def _rewrite_tree_map_only_call( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> ( + tuple[ + "UserFunctionVariable", + Sequence[VariableTracker], + dict[str, VariableTracker], + ] + | None + ): + if not self._is_tree_map_only_function(): + return None + + if len(args) != 3: + return None + if {*kwargs} - _TREE_MAP_ONLY_SUPPORTED_KWARGS: + return None + + type_selector, map_fn, tree_arg = args + allowed_types = self._extract_tree_map_only_types(type_selector) + if allowed_types is None: + return None + + tree_map_callable = self._lookup_tree_map_function() + if tree_map_callable is None: + return None + + wrapped_map_fn = TreeMapOnlyFunctionVariable( + allowed_types, + map_fn, + source=getattr(map_fn, "source", None), + ) + tree_map_variable = VariableTracker.build(tx, tree_map_callable) + return tree_map_variable, [wrapped_map_fn, tree_arg], dict(kwargs) + + def _lookup_tree_map_function(self) -> types.FunctionType | None: + module_name = getattr(self.fn, "__module__", None) + if not module_name: + return None + module = sys.modules.get(module_name) + if module is None: + return None + tree_map = getattr(module, "tree_map", None) + if isinstance(tree_map, types.FunctionType): + return tree_map + return None + + def _extract_tree_map_only_types( + self, selector: VariableTracker + ) -> tuple[type, ...] | None: + if not selector.is_python_constant(): + return None + try: + raw_value = selector.as_python_constant() + except NotImplementedError: + return None + + flattened = self._flatten_type_spec(raw_value) + if not flattened: + return None + if not all(isinstance(typ, type) for typ in flattened): + return None + return tuple(dict.fromkeys(flattened)) + + def _flatten_type_spec(self, value: Any) -> list[type] | None: + if isinstance(value, type): + return [value] + if isinstance(value, tuple): + collected: list[type] = [] + for entry in value: + flat = self._flatten_type_spec(entry) + if flat is None: + return None + collected.extend(flat) + return collected + union_type = getattr(types, "UnionType", None) + if union_type is not None and isinstance(value, union_type): + collected = [] + for entry in value.__args__: + flat = self._flatten_type_spec(entry) + if flat is None: + return None + collected.extend(flat) + return collected + return None + + def is_python_hashable(self) -> Literal[True]: + return True + + def get_python_hash(self) -> int: + return hash(self.fn) + + def is_python_equal(self, other: object) -> bool: + return isinstance(other, variables.UserFunctionVariable) and self.fn is other.fn + + +class InspectSignatureVariable(UserFunctionVariable): + """ + Variable tracker for inspect.signature with caching support. + + inspect.Signature is expensive to trace. When inspect.signature is called + repeatedly on the same function during tracing, we cache the result to avoid + retracing the signature construction each time. Although this is different + from CPython behavior, it is safe to do so because inspect.signature does + not change across different calls to the same function. + """ + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + # Fast path: cache results for repeated calls on the same function + if len(args) == 1 and not kwargs: + target_arg = args[0] + cache_key = None + + if isinstance(target_arg, (UserFunctionVariable, UserMethodVariable)): + cache_key = target_arg.get_function() + + if cache_key is not None: + if cache_key in tx.output.signature_cache: + return tx.output.signature_cache[cache_key] + + result = super().call_function(tx, args, kwargs) + tx.output.signature_cache[cache_key] = result + return result + + return super().call_function(tx, args, kwargs) + + +class TreeMapOnlyFunctionVariable(BaseUserFunctionVariable): + _nonvar_fields = { + "allowed_types", + *BaseUserFunctionVariable._nonvar_fields, + } + + def __init__( + self, + allowed_types: tuple[type, ...], + map_fn: VariableTracker, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.allowed_types = allowed_types + self.map_fn = map_fn + + def python_type(self) -> type: + return FunctionType + + def _matches_allowed_type(self, node: VariableTracker) -> bool: + try: + node_type = node.python_type() + except NotImplementedError: + return False + return any(issubclass(node_type, allowed) for allowed in self.allowed_types) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if not args: + return self.map_fn.call_function(tx, args, kwargs) + leaf = args[0] + if self._matches_allowed_type(leaf): + return self.map_fn.call_function(tx, args, kwargs) + if len(args) != 1 or kwargs: + # Defer to the original map function so we fall back to normal + # tracing instead of triggering a graph break. + return self.map_fn.call_function(tx, args, kwargs) + return leaf + + +class BuiltinMethodVariable(BaseUserFunctionVariable): + def __init__( + self, fn: types.BuiltinMethodType, is_constant: bool = False, **kwargs: Any + ) -> None: + super().__init__(**kwargs) + assert isinstance(fn, types.BuiltinMethodType) + self.fn = fn + + def python_type(self) -> type: + return types.BuiltinMethodType + + @staticmethod + def is_supported_builtin_method(obj: Any) -> bool: + method_self = obj.__self__ + method_name = obj.__name__ + + # TODO(anijain2305) - Add support for more builtin methods + # Supports tuple.__new__ and frozenset({....}).__contains__ + return (method_self is tuple and method_name == "__new__") or ( + type(method_self) is frozenset and method_name == "__contains__" + ) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + method_self = self.fn.__self__ + name = self.fn.__name__ + obj_source = self.source and AttrSource(self.source, "__self__") + obj_vt = VariableTracker.build(tx, method_self, obj_source, realize=True) + return obj_vt.call_method(tx, name, args, kwargs) + + +class LocalGeneratorObjectVariable(VariableTracker): + # PyGen_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/genobject.c#L814 + _cpython_type = types.GeneratorType + + def __init__( + self, + code: types.CodeType, + f_globals: dict[str, Any], + inline_tracer: "InliningGeneratorInstructionTranslator", + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.code = code + self.f_globals = f_globals + self.inline_tracer = inline_tracer + + def get_code(self) -> types.CodeType: + return self.code + + def get_filename(self) -> str: + return self.get_code().co_filename + + def get_name(self) -> str: + return self.get_code().co_name + + def get_function(self) -> Never: + raise NotImplementedError("get_function") + + def has_self(self) -> bool: + return False + + def __name__(self) -> str: + return self.get_name() + + def __str__(self) -> str: + return f"{self.__class__.__name__}({self.get_name()})" + + __repr__ = __str__ + + def reconstruct(self, codegen: "PyCodegen") -> None: + from torch._dynamo.side_effects import disallow_side_effects_in_generator + from torch._dynamo.symbolic_convert import ( + save_and_restart_speculation_log, + temporarely_allow_writes_to_output_graph, + ) + + tx = codegen.tx + save = save_and_restart_speculation_log(tx) + disallow = disallow_side_effects_in_generator(tx) + temp = temporarely_allow_writes_to_output_graph(tx) + + with save, disallow, temp: + tracer = self.inline_tracer + if not tracer.generator_exhausted: + self.remaining_items = self.force_unpack_var_sequence(tx) + variables.ListIteratorVariable(self.remaining_items).reconstruct(codegen) + + def get_globals(self) -> dict[str, Any]: + return self.f_globals + + def python_type(self) -> type: + return types.GeneratorType + + def next_variable(self, tx: "InstructionTranslatorBase") -> VariableTracker: + tracer = self.inline_tracer + + if self._is_generator_exhausted(): + raise_observed_exception(StopIteration, tx) + + try: + # Hierarchically, tx can be seen as the parent of the inline tracer + # created on call_function. Any exception needs to be propagated to tx + # for Dynamo to behave correctly + return tracer.inline_call_() + except ObservedException as e: + tracer.generator_exhausted = True + raise e + except InfiniteGeneratorError: + # test/dynamo/test_misc.py::test_iterator_limit + unimplemented( + gb_type="infinite generator detected", + context="", + explanation="Dynamo traced the YIELD_VALUE bytecode too many times. This could mean " + "that we have attempted to trace an infinite generator.", + hints=[ + f"If you are sure that your generator is not infinite, please report a bug at {PT2_ISSUE_TRACKER_URL}.", + *graph_break_hints.USER_ERROR, + ], + ) + except Unsupported as e: + torch._dynamo.eval_frame.skip_code(self.get_code()) + e.skip_frame = True + if not tx.one_graph and not tx.error_on_graph_break: + e.msg += "\n\nSkipping frame due to graph break in a generator's next() call." + raise + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> ConstantVariable: + if name in self.python_type().__dict__: + return ConstantVariable.create(True) + return ConstantVariable.create(False) + + def has_unpack_var_sequence(self, tx: "InstructionTranslator") -> bool: + return False + + def has_force_unpack_var_sequence(self, tx: "InstructionTranslator") -> bool: + return True + + def force_unpack_var_sequence( + self, tx: "InstructionTranslatorBase" + ) -> list[VariableTracker]: + result: list[VariableTracker] = [] + self.force_apply_to_var_sequence(tx, result.append) + return result + + def force_apply_to_var_sequence( + self, tx: "InstructionTranslatorBase", fn: Callable[[VariableTracker], Any] + ) -> None: + while True: + try: + fn(self.next_variable(tx)) + except ObservedUserStopIteration: + handle_observed_exception(tx) + break + + # no nested graph breaks in generators + def should_allow_nested_graph_breaks(self) -> Literal[False]: + return False + + def _setup_exception( + self, tx: "InstructionTranslator", exc: VariableTracker + ) -> None: + tracer = self.inline_tracer + try: + tracer._raise_exception_variable(exc) + except ObservedException as e: + # if no handler is available (i.e. user code doesn't catch it), the + # exception is raised again. + tracer.exception_handler(e) + + def _is_generator_just_started(self) -> bool: + return self.inline_tracer is None or self.inline_tracer.instruction_pointer == 0 + + def _is_generator_exhausted(self) -> bool: + return getattr(self.inline_tracer, "generator_exhausted", False) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "__next__": + return self.next_variable(tx) + elif name == "__iter__": + # iter(gen) returns itself + return self + elif name == "send": + # Sends a value into the generator function. Returns the next value + # yielded by the generator, or raises StopIteration if the generator + # exits without yielding another value + if self._is_generator_just_started() and len(args): + # can't send non-None value to a just-started generator + # Test: GeneratorCPythonTests.test_send_non_none_to_new_gen + if not all(arg.is_constant_none() for arg in args): + raise_observed_exception(TypeError, tx) + tracer = self.inline_tracer + tracer.push_many(args) + return self.next_variable(tx) + elif name == "close": + # * Raises a GeneratorExit at the point where the generator function was paused. + # * If the generator function catches the exception and returns a + # value, this value is returned from close() - Python 3.13+ + # * If the generator function is already closed, or raises GeneratorExit + # (by not catching the exception), close() returns None. + # * If the generator yields a value, a RuntimeError is raised. + # * If the generator raises any other exception, it is propagated to the caller. + # * If the generator has already exited due to an exception or normal + # exit, close() returns None and has no other effect. + + # Return None if close is called on a just-started generator + # See test GeneratorCloseCpythonTests::test_close_not_started + + tracer = self.inline_tracer + if self._is_generator_just_started() or self._is_generator_exhausted(): + tracer.generator_exhausted = True + return variables.ConstantVariable.create(None) + + # Raise GeneratorExit to see if user code catches it. Any other exception + # is propagated to the parent frame. + try: + self._setup_exception( + tx, variables.ExceptionVariable(GeneratorExit, []) + ) + # There's an extra block on Python 3.12+ to handle StopIteration + # see: https://github.com/python/cpython/blob/8f93dd8a8f237b277abad20d566df90c5cbd7f1e/Objects/genobject.c#L394-L397 + # + # 1 0 RETURN_GENERATOR + # 2 POP_TOP + # 4 RESUME 0 + + # 2 6 LOAD_CONST 1 (1) + # 8 YIELD_VALUE 1 + # 10 RESUME 1 + # 12 POP_TOP + # 14 RETURN_CONST 0 (None) + # >> 16 CALL_INTRINSIC_1 3 (INTRINSIC_STOPITERATION_ERROR) + # 18 RERAISE 1 + # ExceptionTable: + # 4 to 14 -> 16 [0] lasti + if ( + sys.version_info >= (3, 12) + and tracer.next_instruction.opname == "CALL_INTRINSIC_1" + ): + tracer.generator_exhausted = True + return variables.ConstantVariable.create(None) + except ObservedGeneratorExit: + # If it doesn't catch, we just return None, as per the text above + tracer.generator_exhausted = True + return variables.ConstantVariable.create(None) + + try: + # Raise RuntimeError if the generator yields any other value + if self.next_variable(tx): + raise_observed_exception(RuntimeError, tx) + except ObservedGeneratorExit: + tracer.generator_exhausted = True + return variables.ConstantVariable.create(None) + except ObservedUserStopIteration: + # In Python 3.13+, one can capture GeneratorExit and return a value + # See test_generator.py::test_close_capture_GeneratorExit_return + # https://discuss.python.org/t/let-generator-close-return-stopiteration-value/24786/26 + # https://github.com/python/cpython/pull/104771 + assert tracer.symbolic_result is not None + return tracer.symbolic_result + elif name == "throw": + # * Raises an exception at the point where the generator was paused, and + # returns the next value yielded by the generator. + # * If the generator exits without yielding, raise StopIteration + # * If the generator function does not catch the passed-in exception, + # or raises a different exception, then that exception propagates to the caller. + + # Setup the exception table and jump target in case of try...finally + tracer = self.inline_tracer + try: + # In Python 3.9, the exception is represented as a triple (typ, val, tb) + # In such cases, we re-raise the exception object given to avoid + # creating a new object, so that IS_OP works. + # See: https://github.com/pytorch/pytorch/pull/146496 + self._setup_exception(tx, args[1] if len(args) == 3 else args[0]) + except ObservedException: # noqa: TRY203 + # propagate the exception back to the parent caller + raise + + retval = self.next_variable(tx) + + # The exception raised before is still active. We need to check the exception + # table one more time to find the next target. But why? Let's walk + # through an example and its generated bytecode: https://godbolt.org/z/ebdTbMv8M + # + # z = 0 + # def whoo(): + # global z + # z = 0 + # try: + # yield 1 + # except ValueError: + # yield 2 + # finally: + # z += 1 + # z += 10 + # + # gen = whoo() + # next(gen) + # gen.throw(ValueError) + # print('z', z) -> z = 1 + # + # ... + # >> 58 PUSH_EXC_INFO + # + # 8 60 LOAD_GLOBAL 2 (ValueError) + # 70 CHECK_EXC_MATCH + # 72 POP_JUMP_IF_FALSE 7 (to 88) + # 74 POP_TOP + # + # 9 76 LOAD_CONST 3 (2) + # 78 YIELD_VALUE 3 <------ ValueError is still active here + # 80 RESUME 1 + # 82 POP_TOP + # 84 POP_EXCEPT + # 86 jump_backward 34 (to 20) + # ... + # + # ExceptionTable: + # 4 to 8 -> 124 [0] lasti + # 12 to 18 -> 58 [0] + # 20 to 56 -> 124 [0] lasti + # 58 to 82 -> 90 [1] lasti <------ move to 90 + # 84 to 86 -> 96 [0] + # 88 to 88 -> 90 [1] lasti + # 90 to 94 -> 96 [0] + # 96 to 116 -> 118 [1] lasti + # 118 to 122 -> 124 [0] lasti + # + # In this scenario, a generator can yield after `throw()` is called. Even + # after the exception is raised a few lines above, it remains active + # within the `78 YIELD_VALUE` instruction. When the generator resumes + # after the second yield on instruction `80 RESUME`, we cannot simply + # return the control flow to the next instruction. Instead, one must + # check the exception table (or equivalent) to find the next target + # In this case, it says the instruction pointer must be moved to 90. + # + # Without this step, if we let the trace proceed to the next + # instruction, it would follow the control flow where the exception + # raised by `throw()` was handled and swallowed, potentially leading + # to incorrect behavior. + exc_type = type("__InternalThrowException", (Exception,), {}) + + try: + self._setup_exception(tx, variables.ExceptionVariable(exc_type, [])) + self.next_variable(tx) + except get_dynamo_observed_exception(exc_type): + # We should get back the exception raised before. + pass + else: + raise_observed_exception(RuntimeError, tracer) + return retval + + return super().call_method(tx, name, args, kwargs) + + +class ContextlibContextManagerLocalGeneratorObjectVariable( + LocalGeneratorObjectVariable +): + """ + .. note:: + + This is only used when the function is annotated with @contextlib.contextmanager + + It is a special case of a generator function as we do not allow return a context manager + from a torch.compile function. + """ + + +class LocalGeneratorFunctionVariable(BaseUserFunctionVariable): + """functions that behaves like iterators + + .. note:: + + This is a wrapper around (Nested)UserFunctionVariable + """ + + def python_type(self) -> type: + return types.FunctionType + + def __init__( + self, + vt: BaseUserFunctionVariable, + *, + generator_cls: type = LocalGeneratorObjectVariable, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.vt = vt + self.generator_cls = generator_cls + + def __getattr__(self, name: str) -> Any: + if name in self.__class__.__dict__: + return getattr(self, name) + return getattr(self.vt, name) + + # These need to be explicit so the custom __getattr__ doesn't fall back to the unimplemented base class version + def get_code(self) -> types.CodeType: + return self.vt.get_code() + + def get_globals(self) -> dict[str, Any]: + return self.vt.get_globals() + + def has_self(self) -> bool: + return self.vt.has_self() + + def _build_inline_tracer( + self, + tx: "InstructionTranslatorBase", + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> "InliningInstructionTranslator": + from torch._dynamo.symbolic_convert import InliningInstructionTranslator + + return InliningInstructionTranslator.build_inline_tracer( + tx, + self, + args, + kwargs, + ) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if not is_generator(self.vt.get_code()): + unimplemented( + gb_type="non-generator contextlib.contextmanager", + context=str(self.vt.get_code()), + explanation="Cannot compile function decorated with `@contextlib.contextmanager` that is not a generator" + ", i.e. does not use `yield`", + hints=[ + "Use `yield` in the function body instead of `return`.", + "Remove the `@contextlib.contextmanager` decorator.", + ], + ) + + inline_tracer = self._build_inline_tracer(tx, list(args), kwargs) + code = self.vt.get_code() + f_globals = self.vt.get_globals() + + # calling a generator returns a generator object + return self.generator_cls( + code, + f_globals, + inline_tracer, # type: ignore[arg-type] + source=self.source, + ) + + def get_real_python_backed_value(self) -> object: + return self.vt.get_real_python_backed_value() + + +class FunctionDecoratedByContextlibContextManagerVariable( + LocalGeneratorFunctionVariable +): + """ + .. note:: + + This is only used when the function is annotated with @contextlib.contextmanager + """ + + def __init__(self, vt: BaseUserFunctionVariable, **kwargs: Any) -> None: + super().__init__( + vt, + generator_cls=ContextlibContextManagerLocalGeneratorObjectVariable, + **kwargs, + ) + + def _build_inline_tracer( + self, + tx: "InstructionTranslatorBase", + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> "InliningGeneratorInstructionTranslator": + # NOTE: This only exists to not break support for context manager when + # config.enable_faithful_generator_behavior = False and + # config.enable_trace_contextlib = True. In case the former is false, + # Dynamo should still be able to trace through @contextmanager functions + tracer = super()._build_inline_tracer(tx, args, kwargs) + assert isinstance( + tracer, + torch._dynamo.symbolic_convert.InliningGeneratorInstructionTranslator, + ) + tracer.is_generator_from_ctx_manager = True + return tracer + + +class UserMethodVariable(UserFunctionVariable): + """Some unsupported user-defined method""" + + # PyMethod_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/classobject.c#L332 + _cpython_type = types.MethodType + + def __init__( + self, + fn: Callable[..., Any], + obj: VariableTracker, + source_fn: Source | None = None, + **kwargs: Any, + ) -> None: + super().__init__(fn=fn, **kwargs) # type: ignore[arg-type] + self.obj = obj + self.source_fn = source_fn + # Note on source and source_fn + # Be careful with `source` when delegating to UserFunctionVariable + # (base-class) methods. In this __init__, `source` is a *bound method* + # object, but the base class expects the underlying *function* object. + # One way is to simplly use `__func__` to unwrap it. + # + # For recursive dict-tag optimizations, it can be faster to fetch the + # function directly from `cls.__dict__`; that's why we pass on + # `source_fn`. Whenever it is possible to access the function from + # cls.__dict__, we pass that on to `source_fn`. Because bind_args + # operates on the unbound function, most guards should target + # `source_fn` rather than the original `source`. + if source_fn is None and kwargs.get("source") is not None: + self.source_fn = AttrSource(kwargs.get("source"), "__func__") # type: ignore[assignment, arg-type] + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.fn}, {self.obj})" + + def self_args(self) -> list[VariableTracker]: + return [self.obj] + + def python_type(self) -> type[types.MethodType]: + return types.MethodType + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + # NOTE this is to handle methods annotated by `nonstrict_trace`. + # a `nonstrict_trace`-ed function will be wrapped by + # `VariableTracker.build` and route to `TorchInGraphFunctionVariable`, + # but in the case of method, we manually wrap it with `UserMethodVariable` + # inside `UserDefinedObjectVariable.var_getattr`. + # + # We might be able to simplify this away by canonicalizing the + # function/method wrapping code paths. + from ..trace_rules import is_leaf_function, is_nonstrict_trace_callable + + if is_nonstrict_trace_callable(self.fn): + call_args = [*self.self_args(), *args] + var = variables.TorchInGraphFunctionVariable( + self.fn, kind=variables.torch.AllowInGraphKind.NONSTRICT_TRACE + ) + return var.call_function(tx, call_args, kwargs) + + if is_leaf_function(self.fn): + call_args = [*self.self_args(), *args] + var = variables.TorchInGraphFunctionVariable( + self.fn, kind=variables.torch.AllowInGraphKind.LEAF_FUNCTION + ) + return var.call_function(tx, call_args, kwargs) + + # For nn.Module methods, redirecting to NNModuleVariable.call_method for optimized solution + # rather than simple inlining. E.g, putting `call_method` op in FX graph for `forward` method + # since we ensure `forward` of allowed modules can be traced by AOT safely. + # Note this is not only for allowed modules, as user customized modules can extend from + # allowed modules but using parent's `forward` method, which is also covered by this branch. + + # If we are tracing the higher order op, we want Dynamo to step inside + # the module call so that Dynamo can see the underlying parameters and + # buffers and raise them as inputs to the graph. The is_root_tracer + # check bypasses the if condition for non-root tracers and directly + # calls the super().call_function at the end, which is basically + # equivalent of inlining the method. + if tx.output.is_root_tracer() and isinstance( + self.obj, variables.NNModuleVariable + ): + module_attr = getattr(self.fn, "__module__", "") + # inline torch.nn.utils.parametrize + if ( + module_attr is not None + and module_attr.startswith("torch.nn.") + and module_attr != "torch.nn.utils.parametrize" + or self.is_constant + ): + return self.obj.call_method( + tx, self.fn.__name__, list(args), kwargs, constant=self.is_constant + ) + elif ( + _fsdp_param_group is not None + and self.fn is _fsdp_param_group.FSDPParamGroup.use_training_state # type: ignore[attr-defined] + ): + return variables.TorchCtxManagerClassVariable(self.fn).call_function( + tx, (self.obj, *args), kwargs + ) + if self.is_constant: + fn = getattr(self.obj.value, self.fn.__name__) # type: ignore[attr-defined] + return invoke_and_store_as_constant(tx, fn, self.get_name(), args, kwargs) + return super().call_function(tx, args, kwargs) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + if name == "__self__": + return self.obj + if name == "__func__": + # We might have a better way to access the function object, this + # information is stored in self.source_fn, use that to construct the + # variable tracker. + return VariableTracker.build(tx, self.fn, self.source_fn) # type: ignore[arg-type] + return super().var_getattr(tx, name) + + def get_real_python_backed_value(self) -> Any: + return self.fn + + +class WrappedUserMethodVariable(UserMethodVariable): + def __init__( + self, + wrapped: UserMethodVariable, + context: "ContextWrappingVariable", + **kwargs: Any, + ) -> None: + kwargs.pop("fn", None) + kwargs.pop("obj", None) + super().__init__(wrapped.fn, wrapped.obj, **kwargs) + self.wrapped = wrapped + self.context = context + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if config.nested_graph_breaks: + wrapper_fn = UserFunctionVariable(polyfills._fn_with_ctx) + return wrapper_fn.call_function( + tx, [self.context, self.wrapped] + list(args), kwargs + ) + self.context.enter(tx) + result = super().call_function(tx, args, kwargs) + self.context.exit(tx) + return result + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null(lambda: codegen(self.context)) # type: ignore[arg-type] + codegen(self.wrapped) + codegen.extend_output(create_call_function(1, False)) + + +class WrappedUserFunctionVariable(UserFunctionVariable): + def __init__( + self, + wrapped: UserFunctionVariable, + context: "ContextWrappingVariable", + **kwargs: Any, + ) -> None: + kwargs.pop("fn", None) + super().__init__(wrapped.fn, **kwargs) + self.wrapped = wrapped + self.context = context + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if config.nested_graph_breaks: + wrapper_fn = UserFunctionVariable(polyfills._fn_with_ctx) + return wrapper_fn.call_function( + tx, [self.context, self.wrapped] + list(args), kwargs + ) + self.context.enter(tx) + result = super().call_function(tx, args, kwargs) + self.context.exit(tx) + return result + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null(lambda: codegen(self.context)) # type: ignore[arg-type] + codegen(self.wrapped) + codegen.extend_output(create_call_function(1, False)) + + +def invoke_and_store_as_constant( + tx: "InstructionTranslator", + fn: Callable[..., Any], + name: str, + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], +) -> VariableTracker: + def convert(x: VariableTracker) -> Any: + if x.is_tensor(): + return cast("TensorVariable", x).get_real_value() + if isinstance(x, UserDefinedObjectVariable): + if x.source is not None: + install_guard(x.make_guard(GuardBuilder.ID_MATCH)) + return x.value + try: + return x.as_python_constant() + except AsPythonConstantNotImplementedError: + unimplemented( + gb_type="assume_constant_result argument conversion failed", + context=f"function {name}, variable type {type(x).__name__}", + explanation=f"Cannot convert argument of type {type(x).__name__} to a Python constant " + f"for function {name} marked with torch._dynamo.assume_constant_result. " + f"The variable tracker does not support constant conversion.", + hints=[ + "Remove torch._dynamo.assume_constant_result from this function", + "Ensure all arguments passed to the function can be converted to constants", + ], + ) + + args = [convert(x) for x in args] + kwargs = {k: convert(v) for k, v in kwargs.items()} + res = fn(*args, **kwargs) + return tx.output.register_attr_or_module( + res, + name, + source=ConstantSource(name), + ) + + +class NestedUserFunctionVariable(BaseUserFunctionVariable): + _nonvar_fields = { + "f_globals", + *BaseUserFunctionVariable._nonvar_fields, + } + + def __init__( + self, + fn_name: VariableTracker, + code: VariableTracker, + f_globals: dict[str, Any], + defaults: VariableTracker | None, + kwdefaults: VariableTracker | None, + closure: VariableTracker | None, + # This is present when this function is created by + # `functools.wrap(wrapped_fn)(this_fn)`. + wrapped_fn: VariableTracker | None = None, + **kwargs: Any, + ) -> None: + if kwargs.get("mutation_type") is None: + kwargs.update(mutation_type=AttributeMutationNew()) + super().__init__(**kwargs) + assert isinstance(fn_name.as_python_constant(), str) + assert isinstance(code.as_python_constant(), types.CodeType) + assert isinstance(f_globals, dict) + self.fn_name = fn_name + self.code = code + self.f_globals = f_globals + self.defaults = defaults + self.kwdefaults = kwdefaults + self.closure = closure + self.wrapped_fn: VariableTracker | None = wrapped_fn + + def self_args(self) -> list[VariableTracker]: + return [] + + def as_python_constant(self) -> types.FunctionType: + return self.get_function() + + def get_code(self) -> types.CodeType: + return self.code.as_python_constant() + + def python_type(self) -> type[types.FunctionType]: + return types.FunctionType + + def get_function(self, _converting: set[int] | None = None) -> types.FunctionType: + # _converting is used a way to break cycles when + # two nested_functions refer to each other. + from .base import AsPythonConstantNotImplementedError + + self_id = id(self) + if _converting is None: + _converting = set() + if self_id in _converting: + raise ClosureConversionError( + "cycle detected in mutually recursive closures" + ) + _converting.add(self_id) + try: + return self._get_function_impl(_converting) + except AsPythonConstantNotImplementedError as e: + raise ClosureConversionError( + "failed to convert closure cell to Python constant" + ) from e + finally: + _converting.discard(self_id) + + def is_python_constant(self) -> bool: + try: + self.as_python_constant() + return True + except (NotImplementedError, Unsupported): + return False + + def _get_function_impl(self, _converting: set[int]) -> types.FunctionType: + closure_cells = None + if self.closure: + from torch._dynamo.symbolic_convert import InstructionTranslator + + tx = InstructionTranslator.current_tx() + cells = [] + + for cell_var in self.closure.items: # type: ignore[attr-defined] + # Get the cell contents from side_effects or pre_existing_contents + # load_cell will replay the side-effects + cell_contents = tx.output.side_effects.load_cell(cell_var) + + # Check for self-referential closure (function capturing itself for recursion) + # For example: + # def outer(): + # def helper(n): + # if n <= 0: + # return 0 + # return n + helper(n - 1) # helper calls itself + # return helper + if cell_contents is self: + raise ClosureConversionError("self-referential nested function") + + # If the cell contents is a NestedUserFunctionVariable, call get_function + # directly to properly propagate the _converting set for cycle detection + if isinstance(cell_contents, NestedUserFunctionVariable): + value = cell_contents.get_function(_converting) + else: + value = cell_contents.as_python_constant() + cells.append(make_cell(value)) + closure_cells = tuple(cells) + + func = types.FunctionType( + self.code.as_python_constant(), + self.f_globals, + self.fn_name.as_python_constant(), + argdefs=None, + closure=closure_cells, + ) + if self.defaults: + func.__defaults__ = self.defaults.as_python_constant() + if self.kwdefaults: + func.__kwdefaults__ = self.kwdefaults.as_python_constant() + if self.dict_vt and self.dict_vt.contains("__annotations__"): + annotations = self.dict_vt.getitem("__annotations__").as_python_constant() + if isinstance(annotations, tuple): + from itertools import pairwise + + annotations = dict(pairwise(annotations)) + + # TypeError: __annotations__ must be set to a dict object + assert isinstance(annotations, dict) + func.__annotations__ = annotations + return func + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + if name in ( + "__annotations__", + "__dict__", + "__doc__", + "__code__", + "__module__", + "__name__", + "__qualname__", + "__type_params__", + ): + return super().var_getattr(tx, name) + if name == "__defaults__": + d = getattr(self, "defaults", None) + return d.as_python_constant() if d else ConstantVariable.create(None) + elif name in cmp_name_to_op_mapping: + return variables.GetAttrVariable( + self, name, py_type=type(getattr(types.FunctionType, name)) + ) + else: + return super().var_getattr(tx, name) + + def has_closure(self) -> bool: + return self.closure is not None + + def const_getattr(self, tx: "InstructionTranslator", name: str) -> Any: + if name == "__name__": + return self.get_name() + if name == "__code__": + return self.get_code() + if name == "__defaults__": + d = getattr(self, "defaults", None) + return d.as_python_constant() if d else None + return super().const_getattr(tx, name) + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> ConstantVariable: + if name == "__code__": + return VariableTracker.build(tx, hasattr(self, "code")) + if name == "__defaults__": + return VariableTracker.build(tx, hasattr(self, "defaults")) + vt = ConstantVariable.create(name) + if vt in self.get_dict_vt(tx): + return ConstantVariable.create(True) + return super().call_obj_hasattr(tx, name) + + def has_self(self) -> bool: + return False + + def get_globals(self) -> dict[str, Any]: + return self.f_globals + + def bind_args( + self, + parent: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> dict[str, VariableTracker]: + code = self.get_code() + func = types.FunctionType( + code, + self.f_globals, + self.fn_name.as_python_constant(), + tuple(self.defaults.items) if self.defaults else None, # type: ignore[attr-defined] + tuple(make_cell(None) for _ in range(len(self.get_code().co_freevars))), + ) + if self.kwdefaults: + func.__kwdefaults__ = self.kwdefaults.keys_as_python_constant() # type: ignore[attr-defined] + bound = inspect.signature(func).bind(*args, **kwargs) + bound.apply_defaults() + result = dict(bound.arguments.items()) + wrap_args_kwargs(parent.output.root_tx, result) # type: ignore[arg-type] + init_cellvars(parent, result, code) + + for idx, name in enumerate(code.co_freevars): + assert name not in result + cell = self.closure.items[idx] # type: ignore[attr-defined, union-attr] + result[name] = cell + + return result + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.load_import_from(__name__, "_create_nested_fn") + ) + codegen(self.code) + codegen.extend_output([codegen.create_load_const_unchecked(self.f_globals)]) + codegen(ConstantVariable.create(self.code.value.co_name)) # type: ignore[attr-defined] + + if self.defaults: + codegen(self.defaults) + else: + codegen.extend_output([codegen.create_load_const(None)]) + + if self.closure: + codegen(self.closure) + else: + codegen.extend_output([codegen.create_load_const(None)]) + + if self.kwdefaults: + codegen(self.kwdefaults) + else: + codegen.extend_output([codegen.create_load_const(None)]) + + if self.dict_vt and self.dict_vt.contains("__annotations__"): + annotations = self.dict_vt.getitem("__annotations__") + try: + annotations = annotations.as_python_constant() + codegen.extend_output( + [codegen.create_load_const_unchecked(annotations)] + ) + except NotImplementedError: + codegen(annotations) + else: + codegen.extend_output([codegen.create_load_const(None)]) + + codegen.extend_output(create_call_function(7, False)) + + if self.wrapped_fn: + codegen.add_push_null( + lambda: codegen.load_import_from("functools", "wraps") + ) + codegen(self.wrapped_fn) + codegen.extend_output(create_call_function(1, False)) + codegen.extend_output(create_rot_n(2)) + codegen.extend_output(create_call_function(1, True)) + + # codegen attributes + tx = codegen.tx + if tx.output.side_effects.has_pending_mutation(self): + for name, value in tx.output.side_effects.store_attr_mutations[ + self + ].items(): + codegen.dup_top() + codegen(value) + codegen.extend_output(create_rot_n(2)) + codegen.store_attr(name) + + +class WrappedNestedUserFunctionVariable(NestedUserFunctionVariable): + def __init__( + self, + wrapped: NestedUserFunctionVariable, + context: "ContextWrappingVariable", + **kwargs: Any, + ) -> None: + kwargs.pop("fn_name", None) + kwargs.pop("code", None) + kwargs.pop("f_globals", None) + kwargs.pop("defaults", None) + kwargs.pop("kwdefaults", None) + kwargs.pop("annotations", None) + kwargs.pop("closure", None) + kwargs.pop("wrapped_fn", None) + super().__init__( + wrapped.fn_name, + wrapped.code, + wrapped.f_globals, + wrapped.defaults, + wrapped.kwdefaults, + wrapped.closure, + wrapped.wrapped_fn, + ) + self.wrapped = wrapped + self.context = context + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if config.nested_graph_breaks: + wrapper_fn = UserFunctionVariable(polyfills._fn_with_ctx) + return wrapper_fn.call_function( + tx, [self.context, self.wrapped] + list(args), kwargs + ) + self.context.enter(tx) + result = super().call_function(tx, args, kwargs) + self.context.exit(tx) + return result + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null(lambda: codegen(self.context)) + codegen(self.wrapped) + codegen.extend_output(create_call_function(1, False)) + + +class SkipFunctionVariable(VariableTracker): + _nonvar_fields = { + "value", + "reason", + *VariableTracker._nonvar_fields, + } + + def __init__(self, value: Any, reason: str | None = None, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.value = value + self.reason = reason + + def as_python_constant(self) -> Any: + return self.value + + def get_real_python_backed_value(self) -> Any: + return self.value + + @classmethod + def create_with_source(cls, value: Any, source: Source) -> "SkipFunctionVariable": + # Use closure match guard (i.e. guard on __code__ object instead of + # function id) to avoid guarding on nested functions. + if inspect.getattr_static(value, "_torchdynamo_disable", False): + # For torch._dynamo.disable function, ensure that the original + # function is guarded. Otherwise, the else branch will guard on the + # _dynamo.disable.__code__ + guard_on_source = source + guard_on_value = value + + while getattr(guard_on_value, "_torchdynamo_orig_callable", False): + guard_on_value = guard_on_value._torchdynamo_orig_callable + guard_on_source = AttrSource( + guard_on_source, "_torchdynamo_orig_callable" + ) + + guard_on_source.make_guard(GuardBuilder.CLOSURE_MATCH) + elif inspect.isbuiltin(value): + # Bound builtin methods (e.g. obj.__reduce_ex__) are created fresh + # on every attribute access, so their id() is unstable. Skip the + # id-based BUILTIN_MATCH guard for them — the type guard on + # the owner object is sufficient. + if not hasattr(value, "__self__") or isinstance( + value.__self__, types.ModuleType + ): + install_guard(source.make_guard(GuardBuilder.BUILTIN_MATCH)) + elif not is_wrapper_or_member_descriptor(value): + # These descriptors are not guaranteed to return the same object on + # attribute lookup. They are unlikely to be changed, so we can skip + # guarding them. + install_guard(source.make_guard(GuardBuilder.CLOSURE_MATCH)) + return cls(value, source=source) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if inspect.getattr_static(self.value, "_torchdynamo_disable", False): + msg = inspect.getattr_static(self.value, "_torchdynamo_disable_msg", None) + unimplemented( + gb_type="Skip calling `torch.compiler.disable()`d function", + context=str(self.value), + explanation=f"Skip calling function `{self.value}` since it was wrapped " + f"with `torch.compiler.disable` (reason: {msg})", + hints=[ + "Remove the `torch.compiler.disable` call", + ], + ) + elif self.value is torch._dynamo.graph_break: + graph_break_msg = kwargs.get("msg") + if graph_break_msg: + graph_break_msg = graph_break_msg.as_python_constant() + unimplemented( + gb_type="Call to `torch._dynamo.graph_break()`", + context=f"Called `torch._dynamo.graph_break()` with args `{args}`, kwargs `{kwargs}`", + explanation=f"User-inserted graph break. Message: {graph_break_msg}", + hints=[ + "Remove the `torch._dynamo.graph_break()` call.", + ], + ) + elif self.value is torch._dynamo.skip_frame: + skip_frame_msg = kwargs.get("msg") + if skip_frame_msg: + skip_frame_msg = skip_frame_msg.as_python_constant() + else: + skip_frame_msg = "" + unimplemented( + gb_type="Call to `torch._dynamo.skip_frame()`", + context=f"Called `torch._dynamo.skip_frame()` with args `{args}`, kwargs `{kwargs}`. " + f"Skipping frame {format_frame_info(tx.f_code)}.", + explanation=f"User-inserted skip frame. Message: {skip_frame_msg}", + hints=[ + "Remove the `torch._dynamo.skip_frame()` call.", + ], + skip_frame=True, + ) + elif self.value is torch._dynamo.step_unsupported: + try: + unimplemented( + gb_type="Call to `torch._dynamo.step_unsupported()`", + context="", + explanation="User-inserted step_unsupported.", + hints=[ + "Remove the `torch._dynamo.step_unsupported()` call.", + ], + ) + except Unsupported as e: + raise StepUnsupported(e.msg) from None + elif self.value is types.FunctionType.__get__: + # function.__get__(func, obj[, cls]) produces a bound method. + # This is called by inspect._descriptor_get when resolving + # descriptors during inspect.signature(). + # Note that function.__get__ does not use the 3rd argument. The + # reason it still has the 3rd argument is because descriptors follow + # a function signature that takes 3 arguments, and other descriptors + # (not function.__get__) can use the 3rd argument. + if len(args) in (2, 3) and not kwargs: + func_var = args[0] + obj_var = args[1] + if isinstance(func_var, UserFunctionVariable): + return UserMethodVariable( + func_var.fn, obj_var, source_fn=func_var.source + ) + unimplemented( + gb_type="unsupported function.__get__ call", + context=f"call_function {self}, args: {args}, kwargs: {kwargs}", + explanation="Dynamo only supports function.__get__(func, obj[, cls]) " + "where func is a user-defined function.", + hints=[*graph_break_hints.SUPPORTABLE], + ) + else: + if config.dont_skip_tracing: + from .builder import SourcelessBuilder + + # re-build the function, attempting to not skip + rebuilt_fn = SourcelessBuilder.create(tx, self.value) + # if we still get SkipFunctionVariable, then we *really* should skip this function + if not isinstance(rebuilt_fn, SkipFunctionVariable): + return rebuilt_fn.call_function(tx, args, kwargs) + qualname = getattr(self.value, "__qualname__", "") + module_or = getattr(self.value, "__module__", None) + module_name = "" if module_or is None else str(module_or) + try: + path = inspect.getfile(self.value) + explanation = ( + f"Dynamo developers have intentionally marked that the function `{qualname}` " + f"in file `{path}` should not be traced." + ) + hints = [ + f"Avoid calling the function `{qualname}`.", + ] + # TODO improve trace_rules reasoning to provide better hints. + # How do we tell that a function/file should NOT be removed from skip files? + # Do a very basic check for now. + if "_dynamo" not in path: + hints += [ + f"Apply `@torch._dynamo.dont_skip_tracing` to the function `{qualname}` " + "to force tracing into the function. " + "More graph breaks may occur as a result of attempting to trace into the function.", + "Please file an issue to PyTorch.", + ] + except TypeError: + known_python_builtin_modules = {"_abc", "_warnings"} + if module_or in known_python_builtin_modules: + explanation = ( + f"Dynamo does not know how to trace the Python builtin " + f"`{module_name}.{qualname}`." + ) + hints = [ + "If you are attempting to call a logging function (e.g. `_warnings.warn`), " + "you can try adding it to `torch._dynamo.config.reorderable_logging_functions`.", + "Please file an issue on GitHub " + "so the PyTorch team can add support for it. ", + ] + elif module_or is not None and module_or.startswith("optree"): + explanation = f"Dynamo cannot trace optree C/C++ function {module_name}.{qualname}." + hints = [ + " Consider using torch.utils._pytree - " + "https://github.com/pytorch/pytorch/blob/main/torch/utils/_pytree.py" + ] + # also warn on it because most users won't see the graph break message + torch._dynamo.utils.warn_once(explanation + "\n" + "\n".join(hints)) + else: + explanation = ( + f"Dynamo does not know how to trace the builtin `{module_name}.{qualname}.` " + f"This function is either a Python builtin (e.g. _warnings.warn) " + f"or a third-party C/C++ Python extension (perhaps created with pybind)." + ) + hints = [ + "If it is a Python builtin, please file an issue on GitHub " + "so the PyTorch team can add support for it and see the next case for a workaround.", + "If it is a third-party C/C++ Python extension, please " + "either wrap it into a PyTorch-understood custom operator " + "(see https://pytorch.org/tutorials/advanced/custom_ops_landing_page.html " + "for more details) or, if it is traceable, use " + "`torch.compiler.allow_in_graph`.", + ] + # also warn on it because most users won't see the graph break message + torch._dynamo.utils.warn_once(explanation + "\n" + "\n".join(hints)) + if qualname == "allow_in_graph": + explanation = ( + "torch.compiler.allow_in_graph (or torch._dynamo.allow_in_graph) " + "was called inside a compiled region. Dynamically annotating functions " + "inside a compiled region is not supported." + ) + hints = [ + "Apply @torch.compiler.allow_in_graph as a decorator before compilation, " + "not inside the compiled function.", + ] + if self.reason: + reason = self.reason + else: + from ..trace_rules import get_skip_reason + + reason = get_skip_reason(self.value) + unimplemented( + gb_type="Attempted to call function marked as skipped", + context=f"module: {module_name}, qualname: {qualname}, skip reason: {reason}", + explanation=explanation, + hints=hints, + ) + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> ConstantVariable: + return VariableTracker.build(tx, hasattr(self.value, name)) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + if name in cmp_name_to_op_mapping: + return variables.GetAttrVariable( + self, name, py_type=type(getattr(self.value, name)) + ) + + return fn_var_getattr(tx, self.value, self.source, name) + + def is_python_hashable(self) -> bool: + return True + + def get_python_hash(self) -> int: + return hash(self.value) + + def is_python_equal(self, other: object) -> bool: + return ( + isinstance(other, VariableTracker) + and self.as_python_constant() == other.as_python_constant() + ) + + +class WrappedSkipFunctionVariable(SkipFunctionVariable): + def __init__( + self, + wrapped: SkipFunctionVariable, + context: "ContextWrappingVariable", + **kwargs: Any, + ) -> None: + kwargs.pop("value", None) + kwargs.pop("reason", None) + super().__init__(wrapped.value, reason=wrapped.reason, **kwargs) + self.wrapped = wrapped + self.context = context + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if config.nested_graph_breaks: + wrapper_fn = UserFunctionVariable(polyfills._fn_with_ctx) + return wrapper_fn.call_function( + tx, [self.context, self.wrapped] + list(args), kwargs + ) + self.context.enter(tx) + result = super().call_function(tx, args, kwargs) + self.context.exit(tx) + return result + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null(lambda: codegen(self.context)) + codegen(self.wrapped) + codegen.extend_output(create_call_function(1, False)) + + +class WrapperUserFunctionVariable(BaseUserFunctionVariable): + """ + Used to represent a wrapper object that contains the actual callable as an + attribute. For example, torch.jit.script/trace have the original function at + their _torchdynamo_inline attribute. Similarly, functions with + __script_if_tracing_wrapper have the original attr at "__original_fn". + """ + + def python_type(self) -> type: + return types.FunctionType + + def __init__(self, wrapper_obj: Any, attr_to_trace: str, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.wrapper_obj = wrapper_obj + self.attr_to_trace = attr_to_trace + + def get_module(self) -> str: + return self.wrapper_obj.__module__ + + def get_name(self) -> str: + return self.wrapper_obj.__name__ + + def get_code(self) -> types.CodeType: + return self.get_function().__code__ + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + if name == self.attr_to_trace: + val = getattr(self.wrapper_obj, self.attr_to_trace) + source = self.source and AttrSource(self.source, name) + return VariableTracker.build(tx, val, source) + return super().var_getattr(tx, name) + + def get_function(self): + return getattr(self.wrapper_obj, self.attr_to_trace) + + def self_args(self) -> list[VariableTracker]: + return [] + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if hasattr(self.wrapper_obj, "cache_info"): + target_fn = getattr(self.wrapper_obj, self.attr_to_trace, None) + module_name = getattr(target_fn, "__module__", "") or "" + + if module_name.split(".", maxsplit=1)[0] != "torch": + frame_summary = tx.frame_summary() + filename = os.path.basename(frame_summary.filename) + lineno = frame_summary.lineno + msg = ( + "Dynamo detected a call to a `functools.lru_cache`-wrapped " + f"function at '{filename}:{lineno}'. Dynamo ignores the " + "cache wrapper and directly traces the wrapped function. " + "Silent incorrectness is only a *potential* risk, not " + "something we have observed. " + "Enable TORCH_LOGS=+dynamo for a DEBUG stack trace.\n\n" + "This call originates from:\n" + f"{''.join(traceback.format_list([frame_summary]))}" + ) + + torch._dynamo.utils.warn_once(msg) + + dynamo_logger = torch._dynamo.utils.logging.getLogger("torch._dynamo") + if dynamo_logger.isEnabledFor(logging.DEBUG): + user_stack = torch._guards.TracingContext.extract_stack() + user_stack = get_stack_above_dynamo() + user_stack + frame_loc = (user_stack[-1].filename, user_stack[-1].lineno) + user_stack_formatted = "".join(traceback.format_list(user_stack)) + user_stack_trace = f"call to a lru_cache wrapped function at: {frame_loc[0]}:{frame_loc[1]}\n" + user_stack_trace += str(user_stack_formatted) + dynamo_logger.debug(user_stack_trace) + + all_args = self.self_args() + list(args) + return VariableTracker.build( + tx, + polyfills.getattr_and_trace, # type: ignore[arg-type] + ).call_function( + tx, + [self, VariableTracker.build(tx, self.attr_to_trace), *all_args], + kwargs, + ) + + def get_real_python_backed_value(self) -> object: + return getattr(self.wrapper_obj, self.attr_to_trace) + + +class WrapperUserMethodVariable(WrapperUserFunctionVariable): + """ + Similar to WrapperUserFunctionVariable, but for methods. The only delta is + saving the vt for `self` object of the method which is then used by + WrapperUserFunctionVariable in `call_function` method. + """ + + def python_type(self) -> type: + return types.MethodType + + def __init__( + self, + wrapper_obj: Any, + attr_to_trace: str, + self_obj: VariableTracker, + **kwargs: Any, + ) -> None: + super().__init__(wrapper_obj, attr_to_trace, **kwargs) + self.obj = self_obj + + def self_args(self) -> list[VariableTracker]: + return [self.obj] + + +def _traceable_collective_remaps() -> dict[Any, Any]: + # We can't rely on importing from distributed, since it's not always built + if torch.distributed.is_available(): + from torch.distributed._functional_collectives import ( + traceable_collective_remaps, + ) + + return traceable_collective_remaps + return {} + + +def _traceable_collectives_source( + tx: "InstructionTranslator", fn: Callable[..., Any] +) -> AttrSource: + assert torch.distributed.is_available(), "Illegal invocation." + assert fn in _traceable_collective_remaps().values() + + inner_name = fn.__name__ + path_source = tx.import_source("torch.distributed._functional_collectives") + return AttrSource(path_source, inner_name) + + +class CollectiveFunctionRewriteVariable(UserFunctionVariable): + """ + Some of the torch.distributed.* collective APIs are possible to rewrite to 'traceable' collectives. + + This class provides both a way to check if a function is remappable, and perform the remapping. + + In the case that a function is 'remappable' but only for some combinations of call-time arguments, + we check the args at `call_function` time and fall back to graph-breaking if needed. This is no worse + than status-quo as we currently graph-break on all distributed.* collectives. + """ + + def __init__( + self, + fn: Callable[..., Any], + *, + replacement_var: UserFunctionVariable, + **kwargs: Any, + ) -> None: + super().__init__(fn, **kwargs) # type: ignore[arg-type] + assert isinstance(replacement_var, UserFunctionVariable) + self.replacement_var = replacement_var + + @staticmethod + def create( + tx: "InstructionTranslator", + old_fn: Callable[..., Any], + source: Source, + **options: Any, + ) -> "CollectiveFunctionRewriteVariable": + new_fn, new_source = CollectiveFunctionRewriteVariable.rewrite(tx, old_fn) + return CollectiveFunctionRewriteVariable( + old_fn, + replacement_var=UserFunctionVariable(new_fn, source=new_source, **options), + source=source, + **options, + ) + + @staticmethod + def can_rewrite(variable: Any) -> bool: + return ( + inspect.isfunction(variable) and variable in _traceable_collective_remaps() + ) + + @staticmethod + def rewrite( + tx: "InstructionTranslator", fn: Callable[..., Any] + ) -> tuple[Any, AttrSource]: + new_fn = _traceable_collective_remaps()[fn] + return new_fn, _traceable_collectives_source(tx, new_fn) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + # call_function must check any unsupported arguments and graph-break. + # It's safe to assume args/kwargs from orig_fn map 1:1 to args/kwargs of remapped_fn, + # since that's the contract for putting a mapping in `traceable_collective_remaps` + import torch.distributed as dist + from torch.distributed._functional_collectives import REDUCE_OP_TO_STR + + # Merge args into kwargs so positional and keyword args + # can be processed the same way. + signature = inspect.signature(self.fn) + kwargs = dict(signature.bind(*args, **kwargs).arguments) + args = () + + if "async_op" in kwargs and kwargs["async_op"].as_python_constant(): + unimplemented( + gb_type="async_op=True for distributed collectives", + context=f"{self.fn}, {args=}, {kwargs=}", + explanation=f"`torch.compile` doesn't support `async_op=True for {self.fn}", + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + ) + + if self.fn == dist.batch_isend_irecv: + if not config.enable_p2p_compilation: + unimplemented( + gb_type="P2P compilation disabled for batch_isend_irecv", + context=f"{self.fn}", + explanation="P2P compilation is disabled.", + hints=[ + "Set TORCHDYNAMO_ENABLE_P2P_COMPILATION=1 to enable.", + ], + ) + + p2p_ops = kwargs["p2p_op_list"] + if not isinstance(p2p_ops, variables.ListVariable): + raise torch._dynamo.exc.InternalTorchDynamoError( + "`P2POp` used incorrectly" + ) + + ops: list[VariableTracker] = list() + peers = list() + tags = list() + tensors = list() + group_var: VariableTracker | None = None + + for item in p2p_ops.items: + if item.python_type() is not dist.P2POp: + raise torch._dynamo.exc.InternalTorchDynamoError( + "`P2POp` used incorrectly" + ) + + op_var = item.var_getattr(tx, "op") + if op_var.is_python_constant(): + op = op_var.as_python_constant() + if op not in (dist.isend, dist.irecv): + raise torch._dynamo.exc.InternalTorchDynamoError( + f"unexpected P2POp op {op}" + ) + op_var = variables.ConstantVariable.create(op.__name__) + elif hasattr(op_var, "get_name"): + op_var = variables.ConstantVariable.create(op_var.get_name()) + else: + raise torch._dynamo.exc.InternalTorchDynamoError( + f"unexpected P2POp op variable {op_var}" + ) + + ops.append(op_var) + tensors.append(item.var_getattr(tx, "tensor")) + peers.append(item.var_getattr(tx, "peer")) + tags.append(item.var_getattr(tx, "tag")) + if group_var is None: + group_var = item.var_getattr(tx, "group") + + assert group_var is not None + new_args: tuple[VariableTracker, ...] = () + new_kwargs: dict[str, VariableTracker] = { + "op_list": variables.ListVariable(ops), + "peer_list": variables.ListVariable(peers), + "tag_list": variables.ListVariable(tags), + "tensors": variables.ListVariable(tensors), + "group_name": group_var, + } + return self.replacement_var.call_function(tx, new_args, new_kwargs) + + if self.fn in (dist.isend, dist.irecv): + if not config.enable_p2p_compilation: + unimplemented( + gb_type="P2P compilation disabled for isend/irecv", + context=f"{self.fn}", + explanation="P2P compilation is disabled.", + hints=[ + "Set TORCHDYNAMO_ENABLE_P2P_COMPILATION=1 to enable.", + ], + ) + + return self.replacement_var.call_function(tx, args, kwargs) + + if self.fn in ( + dist.all_reduce, + dist.reduce_scatter_tensor, + # pyrefly: ignore [deprecated] + dist._reduce_scatter_base, + ): + reduce_op_var = kwargs.get("op") + reduce_op = ( + reduce_op_var.value # type: ignore[attr-defined] + if reduce_op_var is not None + else signature.parameters["op"].default + ) + if reduce_op not in REDUCE_OP_TO_STR: + raise ValueError(f"Unsupported all_reduce op: {reduce_op}") + kwargs["op"] = VariableTracker.build(tx, REDUCE_OP_TO_STR[reduce_op]) + return self.replacement_var.call_function(tx, args, kwargs) + + +class CollectionsNamedTupleFunction(UserFunctionVariable): + def as_python_constant(self) -> Any: + return self.fn + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + constant_args = check_constant_args(args, kwargs) + if constant_args: + try: + value = self.fn( + *[x.as_python_constant() for x in args], + **{k: v.as_python_constant() for k, v in kwargs.items()}, + ) + except TypeError as exc: + raise_observed_exception( + type(exc), + tx, + args=list(exc.args), + ) + return variables.UserDefinedClassVariable( + value, + mutation_type=ValueMutationNew(), + ) + unimplemented( + gb_type="namedtuple construction", + context=f"{args=}, {kwargs=}", + explanation="`torch.compile` only support certain input types for namedtuple", + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + ) + + +class FunctoolsPartialVariable(VariableTracker): + # partial_type_spec: https://github.com/python/cpython/blob/v3.13.0/Modules/_functoolsmodule.c#L538 + _cpython_type = functools.partial + + _nonvar_fields = { + "original_cache_hash", + *VariableTracker._nonvar_fields, + } + + def __init__( + self, + func: VariableTracker, + args: Sequence[VariableTracker], + keywords: dict[str, VariableTracker], + original_cache_hash: Any = None, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.func = func + assert isinstance(args, list) + self.args = args + assert isinstance(keywords, dict) + self.keywords = keywords + # fake_value is used for id calculation. Creating this value and id'ng + # on it is sufficient for the tracing purposes. + self.fake_value = functools.partial(identity) + # Store cache_hash from the original partial for SAC context_fn caching + self.original_cache_hash = original_cache_hash + + def python_type(self) -> type: + return functools.partial + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null(lambda: codegen.load_import_from("functools", "partial")) + codegen(self.func) + if self.args: + codegen.foreach(self.args) + if not self.keywords: + codegen.extend_output(create_call_function(len(self.args) + 1, False)) + return + + codegen.foreach(self.keywords.values()) + keys = tuple(self.keywords.keys()) + codegen.extend_output( + codegen.create_call_function_kw(len(keys) + len(self.args) + 1, keys, False) + ) + + def get_function(self) -> Any: + return self.as_python_constant() + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + merged_args = self.args + list(args) + merged_kwargs = {**self.keywords, **kwargs} + return self.func.call_function(tx, merged_args, merged_kwargs) + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> ConstantVariable: + # functools.partial uses slots, so attributes are constant + return VariableTracker.build(tx, hasattr(functools.partial(identity), name)) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + source = self.source and AttrSource(self.source, name) + # Handle __slots__ + if name == "func": + return self.func + if name == "args": + return variables.ListVariable(self.args, source=source) + if name == "keywords": + items = {VariableTracker.build(tx, k): v for k, v in self.keywords.items()} + return variables.ConstDictVariable(items, source=source) + if name in cmp_name_to_op_mapping: + return variables.GetAttrVariable( + self, name, py_type=type(getattr(functools.partial, name)) + ) + raise_observed_exception(AttributeError, tx) + + def as_python_constant(self) -> Any: + return functools.partial( + self.func.as_python_constant(), + *[arg.as_python_constant() for arg in self.args], + **{k: v.as_python_constant() for k, v in self.keywords.items()}, + ) + + def guard_as_python_constant(self) -> Any: + """Similar to as_python_constant(), but add ID_MATCH guards to try to force things to become constants""" + result = functools.partial( + self.func.guard_as_python_constant(), + *[v.guard_as_python_constant() for v in self.args], + **{k: v.guard_as_python_constant() for k, v in self.keywords.items()}, + ) + # Preserve cache_hash for SAC context_fn caching + if self.original_cache_hash is not None: + result.cache_hash = self.original_cache_hash # type: ignore[missing-attribute] + return result + + def is_python_hashable(self) -> bool: + return ( + self.func.is_python_hashable() + and all(arg.is_python_hashable() for arg in self.args) + and all(value.is_python_hashable() for value in self.keywords.values()) + ) + + def get_python_hash(self) -> int: + func_hash = self.func.get_python_hash() + args_hash = (arg.get_python_hash() for arg in self.args) + values_hash = (value.get_python_hash() for value in self.keywords.values()) + return hash((func_hash, *args_hash, *values_hash)) + + def is_python_equal(self, other: object) -> bool: + return ( + isinstance(other, FunctoolsPartialVariable) + and self.func.is_python_equal(other.func) + and all( + arg_a.is_python_equal(arg_b) + for (arg_a, arg_b) in zip(self.args, other.args) + ) + and all( + value_a.is_python_equal(value_b) + for (value_a, value_b) in zip( + self.keywords.values(), other.keywords.values() + ) + ) + ) + + +class PolyfilledFunctionVariable(VariableTracker): + _nonvar_fields = { + "fn", + "wrapped_fn", + "traceable_fn", + *VariableTracker._nonvar_fields, + } + + @classmethod + @functools.cache + def _get_polyfill_handlers(cls) -> dict[Callable[..., Any], types.FunctionType]: + return {} + + @classmethod + def create_with_source( + cls, value: Any, source: Source + ) -> "PolyfilledFunctionVariable": + install_guard(source.make_guard(GuardBuilder.CLOSURE_MATCH)) + + return cls(value, source=source) + + def __init__(self, fn: _F, **kwargs: Any) -> None: + super().__init__(**kwargs) + # pyrefly: ignore[invalid-type-var] + self.fn: _F = fn + + handler = self._get_polyfill_handlers().get(fn, fn) + traceable_fn = None + assert callable(handler), f"Polyfill handler {handler} is not callable for {fn}" + for candidate_attr in ( + "__torch_dynamo_polyfill__", # registered polyfill + "__python_implementation__", # self handler from third-party libraries + ): + candidate = getattr(handler, candidate_attr, None) + if candidate: + assert callable(candidate) + traceable_fn = candidate + break + else: + raise RuntimeError( + f"Polyfill handler {handler} does not have a traceable function" + ) + + self.wrapped_fn = handler + # pyrefly: ignore[invalid-type-var] + self.traceable_fn: _F = traceable_fn + + @property + def polyfill_fn(self) -> Callable[..., Any]: + return self.traceable_fn + + def can_constant_fold_through(self) -> bool: + return getattr( + self.wrapped_fn, "__torch_dynamo_can_constant_fold_through__", False + ) + + def get_function(self) -> Any: + return self.as_python_constant() + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if self.can_constant_fold_through() and check_unspec_or_constant_args( + args, kwargs + ): + result = ( + self.fn( # use the original function which is faster than the polyfill + *[x.as_python_constant() for x in args], + **{k: v.as_python_constant() for k, v in kwargs.items()}, + ) + ) + return VariableTracker.build(tx, result) + + # Special case for sum on tuple/list of ints + if ( + self.fn is builtins.sum + and len(args) == 1 + and not kwargs + and isinstance(args[0], (variables.ListVariable, variables.TupleVariable)) + and all( + (x.is_python_constant() and isinstance(x.as_python_constant(), int)) + or (isinstance(x, variables.SymNodeVariable) and x.python_type() is int) + for x in args[0].items + ) + ): + return variables.SymNodeVariable.create( + tx, + tx.output.create_proxy( + "call_function", + torch.sym_sum, + (tuple(a.as_proxy() for a in args[0].items),), + {}, + ), + sym_num=torch.sym_sum( + [ + ( + x.as_python_constant() + if x.is_python_constant() + else x.sym_num # type: ignore[attr-defined] + ) + for x in args[0].items + ] + ), + ) + + traceable_function_variable = VariableTracker.build(tx, self.traceable_fn) + return traceable_function_variable.call_function(tx, args, kwargs) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "__call__": + return self.call_function(tx, args, kwargs) + + method = getattr(self.fn, name, None) + if not (method or is_function(method)): + raise_type_error(tx, f"Cannot find callable {name} in {self.fn}") + options = {} + if self.source: + options["source"] = AttrSource(self.source, name) + polyfilled_method_variable = PolyfilledFunctionVariable(method, **options) + return polyfilled_method_variable.call_function(tx, args, kwargs) + + def as_python_constant(self) -> Any: + return self.fn + + +class SysFunctionVariable(VariableTracker): + def __init__(self, value: Any, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.value = value + + def python_type(self) -> type: + return types.BuiltinFunctionType + + def exc_info(self, tx: "InstructionTranslator") -> "variables.TupleVariable": + if len(tx.exn_vt_stack): + exn = tx.exn_vt_stack[-1] + typ = exn.exc_type # type: ignore[union-attr] + tb = exn.var_getattr(tx, "__traceback__") + items = [VariableTracker.build(tx, typ), exn, tb] + else: + items = [ + ConstantVariable.create(None), + ConstantVariable.create(None), + ConstantVariable.create(None), + ] + return variables.TupleVariable(items) # type: ignore[arg-type] + + def exception(self, tx: "InstructionTranslator") -> VariableTracker: + return self.exc_info(tx).items[1] + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if self.value is sys.exc_info: + return self.exc_info(tx) + + assert self.value is sys.exception + return self.exception(tx) + + +from torch._higher_order_ops.triton_kernel_wrap import ( + create_tma_experimental_metadata, + create_tma_stable_metadata, + TMADescriptorMetadata, + TritonHOPifier, +) + + +class DynamoTritonHOPifier(TritonHOPifier): + def raise_unsupported(self, msg: str) -> Never: + unimplemented( + gb_type="triton kernel unsupported feature", + context="", + explanation=f"Encountered triton kernel unsupported feature: {msg}", + hints=[], + ) + + def is_callable(self, maybe_callable: VariableTracker) -> bool: + return isinstance( + maybe_callable, (NestedUserFunctionVariable, UserFunctionVariable) + ) + + def get_value(self, val: VariableTracker) -> Any: + return val.value # type: ignore[attr-defined] + + def check_grid(self, grid: "BaseListVariable") -> tuple[torch.fx.proxy.Proxy, ...]: + from .lists import BaseListVariable + + if isinstance(grid, BaseListVariable): + return grid.as_proxy() + else: + unimplemented( + gb_type="unsupported grid type for triton hop check_grid", + context=f"grid type = {type(grid)}", + explanation="`torch.compile` only supports list-like grid for check_grid", + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + ) + + def call_grid( + self, grid: Any, meta: dict[str, Any], tx: "InstructionTranslator" + ) -> Any: + meta_var = {VariableTracker.build(tx, k): v for k, v in meta.items()} + grid = grid.call_function(tx, [meta_var], {}) + return grid + + # We use this function to wrap call_prune_configs + def call_user_defined_fn( + self, + user_fn: Callable[..., Any], + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + tx: Optional["InstructionTranslator"], + variable: Any, + ) -> VariableTracker: + from .builder import SourcelessBuilder + + wrapped_user_function = SourcelessBuilder.create(tx, user_fn) # type: ignore[arg-type] + result = wrapped_user_function.call_function(tx, args, kwargs) + return result + + def wrap_user_defined_obj( + self, + user_obj: Any, + tx: Optional["InstructionTranslator"], + variable: Any, + name: str, + ) -> VariableTracker: + from .builder import VariableBuilder + + assert tx is not None + # Route through VariableBuilder.__call__ so already-tracked mutable + # objects (for example autotuner config lists) are reused instead of + # being registered for mutation twice in the same trace. + wrapped_user_obj = VariableBuilder( + tx, AttrSource(variable.kernel_source, f"{name}") + )(user_obj) + return wrapped_user_obj + + def maybe_unpack_configs( + self, configs: Any, tx: Optional["InstructionTranslator"] + ) -> list[Any]: + # unpack the list of configs + configs = configs.unpack_var_sequence(tx) + + # guard_as_python_constant inserts guards for Dynamo to check if the configs object changed. + configs = [config.guard_as_python_constant() for config in configs] + + return configs + + def maybe_unpack_heuristic_result(self, result: VariableTracker) -> Any: + if not result.is_python_constant(): + self.raise_unsupported( + "@triton.heuristics must return constant values because configs can only contain constant values." + ) + + return result.guard_as_python_constant() + + # We need to override call_getitem here so that we can add the source in the case + # where we call the triton kernel with a grid + def call_getitem( # type: ignore[override] + self, + variable: "TritonKernelVariable", + args: Sequence[Any], + ) -> "TritonKernelVariable": + # __getitem__ should only be called if we don't already have a grid + # Only grid needs to be passed + if variable.grid is not None or len(args) != 1: + self.raise_unsupported( + "Triton kernels should be called with only a single grid" + ) + return type(variable)( + kernel=variable.kernel, + kernel_idx=variable.kernel_idx, + grid=args[0], + kernel_source=variable.kernel_source, + ) + + def call_HOP( + self, + variable: "TritonKernelVariable", + grids: Any, + combined_args: dict[str, Any], + tx: "InstructionTranslator", + ) -> "variables.ConstantVariable": + from .dicts import ConstDictVariable + + # as we can only pass tensors as non-const args in fx graph, + # here we replace TMA descriptors + # (TMADescriptorExperimentalVariable and TMADescriptorStableVariable + # instances) with the underlying tensors, while moving the + # TMA descriptor-related metadata to a separate argument, + # so that we can reconstruct the TMA descriptors downstream + tma_descriptor_metadata: TMADescriptorMetadata = {} + for k in list(combined_args.keys()): + v = combined_args[k] + if isinstance( + v, (TMADescriptorExperimentalVariable, TMADescriptorStableVariable) + ): + tma_descriptor_metadata[k] = v.to_metadata() + combined_args[k] = v.get_tensor() + + combined_args_vt = { + VariableTracker.build(tx, k): v for k, v in combined_args.items() + } + + from torch._higher_order_ops.triton_kernel_wrap import ( + kernel_side_table, + triton_kernel_wrapper_mutation, + ) + + # Combine args and kwargs and pass as a dict so that if user defined triton + # kernel uses variables as 'grid' or 'kernel', it does not conflict with + # parameters of the wrapper function + constant_args = { + k: v.as_python_constant() + for k, v in combined_args.items() + if isinstance(v, VariableTracker) and v.is_python_constant() + } + non_constant_args = { + k: v + for k, v in combined_args_vt.items() + if not (isinstance(v, VariableTracker) and v.is_python_constant()) + } + + for v in non_constant_args.values(): + v = v.realize() + if not (v.is_tensor() or v.is_symnode_like()): + self.raise_unsupported( + f"Unexpected argument type for a Triton kernel: {repr(v)}." + ) + + constant_args_idx = kernel_side_table.add_constant_args(constant_args) + meta = ConstDictVariable(non_constant_args, dict) + tx.output.create_proxy( + "call_function", + triton_kernel_wrapper_mutation, + (), + { + "kernel_idx": variable.kernel_idx, + "constant_args_idx": constant_args_idx, + "grid": grids, + "tma_descriptor_metadata": tma_descriptor_metadata, + "kwargs": meta.as_proxy(), + }, + ) + + return VariableTracker.build( + tx, + None, + ) + + +dynamo_triton_hopifier_singleton = DynamoTritonHOPifier() + + +class TritonKernelVariable(VariableTracker): + grid: "TritonGridType" + kernel: "TritonKernelType" + kernel_idx: int | None + kernel_source: Source | None + + def __init__( + self, kernel: Any, kernel_idx: int | None, grid: Any, **kwargs: Any + ) -> None: + self.kernel_source = kwargs.pop("kernel_source", kwargs.get("source")) + super().__init__(**kwargs) + dynamo_triton_hopifier_singleton.init_variable(self, kernel, kernel_idx, grid) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + return dynamo_triton_hopifier_singleton.call_triton_kernel( # type: ignore[return-value] + self, args, kwargs, tx + ) + + def mp_subscript_impl( + self, + tx: "InstructionTranslator", + key: VariableTracker, + ) -> VariableTracker: + # Triton kernel[grid] — triton-specific, not a CPython slot. + # TODO(follow-up): add test for invalid key type + return dynamo_triton_hopifier_singleton.call_getitem(self, [key]) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "run": + return dynamo_triton_hopifier_singleton.call_run(self, args, kwargs, tx) # type: ignore[return-value] + + # Bail out to parent's implementation + return super().call_method(tx, name, args, kwargs) + + def specialize_symbolic(self, arg: Any) -> Any: + from .constant import ConstantVariable + from .tensor import SymNodeVariable + + # See [Note: Specialize tl.constexpr args in user-defined triton kernels] + if isinstance(arg, SymNodeVariable): + return ConstantVariable.create(arg.evaluate_expr()) + return arg + + +class TMADescriptorExperimentalVariable(VariableTracker): + def __init__( + self, + data_ptr: "variables.DataPtrVariable", + dims: list[VariableTracker], + block_dims: list[VariableTracker], + element_size: VariableTracker, + **kwargs: Any, + ) -> None: + assert isinstance(data_ptr, variables.DataPtrVariable) + super().__init__(**kwargs) + self.data_ptr = data_ptr + self.dims = dims + self.block_dims = block_dims + self.element_size = element_size + + def to_metadata(self) -> Any: + return create_tma_experimental_metadata( + [dim.as_proxy() for dim in self.dims], + [dim.as_proxy() for dim in self.block_dims], + self.element_size.as_proxy(), + ) + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.load_import_from( + "triton.tools.experimental_descriptor", + f"create_{len(self.dims)}d_tma_descriptor", + ) + ) + self.data_ptr.reconstruct(codegen) + args = [*self.dims, *self.block_dims, self.element_size] + codegen.foreach(args) + codegen.call_function(len(args) + 1, False) + + def get_tensor(self) -> VariableTracker: + return self.data_ptr.from_tensor + + +class TMADescriptorStableVariable(VariableTracker): + def __init__( + self, + tensor: "TensorVariable", + block_shape: "ListVariable", + **kwargs: Any, + ) -> None: + assert tensor.is_tensor() + super().__init__(**kwargs) + self.tensor = tensor + self.block_shape = block_shape + + def to_metadata(self) -> Any: + return create_tma_stable_metadata( + self.block_shape.as_proxy(), + ) + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.load_import_from( + "triton.tools.tensor_descriptor", + "TensorDescriptor", + ) + ) + codegen.load_method("from_tensor") + self.tensor.reconstruct(codegen) + codegen(self.block_shape) + codegen.call_method(2) + + def get_tensor(self) -> Any: + return self.tensor + + +class CreateTMADescriptorExperimentalVariable(VariableTracker): + def __init__( + self, + rank: int, + **kwargs: Any, + ) -> None: + assert rank in (1, 2) + super().__init__(**kwargs) + self.rank = rank + + def python_type(self) -> type: + return types.FunctionType + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + ptr = kwargs["ptr"] if "ptr" in kwargs else args[0] + + if not isinstance(ptr, variables.DataPtrVariable): + unimplemented( + gb_type="invalid ptr argument for create_tma_descriptor", + context=f"args = {args}, kwargs = {kwargs}", + explanation=f"Expected `ptr` argument of `create_{self.rank}d_tma_descriptor`" + "to be from a `.data_ptr()` call, represented internally by `DataPtrVariable`", + hints=[ + "`torch.compile` may fail to internally represent result of `.data_ptr()` " + "with `DataPtrVariable` due to a graph break between the `.data_ptr()` call and " + f"`create_{self.rank}d_tma_descriptor`. Please ensure there were no graph breaks " + "between these two calls.", + ], + ) + + if self.rank == 1: + if len(args) + len(kwargs) != 4: + raise_type_error( + tx, + f"TMA metadata rank=1 requires exactly 4 arguments, got {len(args) + len(kwargs)}", + ) + dims = [ + kwargs["dim"] if "dim" in kwargs else args[1], + ] + block_dims = [ + kwargs["block_dim"] if "block_dim" in kwargs else args[2], + ] + else: + if len(args) + len(kwargs) != 6: + raise_type_error( + tx, + f"TMA metadata rank=2 requires exactly 6 arguments, got {len(args) + len(kwargs)}", + ) + dims = [ + kwargs["dim1"] if "dim1" in kwargs else args[1], + kwargs["dim0"] if "dim0" in kwargs else args[2], + ] + block_dims = [ + kwargs["block_dim1"] if "block_dim1" in kwargs else args[3], + kwargs["block_dim0"] if "block_dim0" in kwargs else args[4], + ] + element_size = kwargs["element_size"] if "element_size" in kwargs else args[-1] + + # to make pyrefy happy + assert isinstance(ptr, variables.DataPtrVariable) + + return TMADescriptorExperimentalVariable( + data_ptr=ptr, + dims=dims, + block_dims=block_dims, + element_size=element_size, + ) + + +class CreateTMADescriptorStableVariable(VariableTracker): + def python_type(self) -> type: + return types.FunctionType + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + tensor = kwargs["tensor"] if "tensor" in kwargs else args[0] + block_shape = kwargs["block_shape"] if "block_shape" in kwargs else args[1] + + return TMADescriptorStableVariable( + tensor=tensor, # type: ignore[arg-type] + block_shape=block_shape, # type: ignore[arg-type] + ) + + +class PyTreeGetNodeTypeFunctionVariable(UserFunctionVariable): + """ + `torch.utils._pytree._get_node_type` function is very hot function. We want to special case it to reduce Dynamo tracing time. + + def _get_node_type(tree: Any) -> Any: + node_type = type(tree) + # All namedtuple types are implicitly registered as pytree nodes. + # XXX: Other parts of the codebase expect namedtuple types always return + # `namedtuple` instead of the actual namedtuple type. Even if the type + # is explicitly registered. + if is_namedtuple_class(node_type): + return namedtuple + return node_type + """ + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if len(args) != 1: + raise_type_error( + tx, f"pytree_get_node_type requires exactly 1 argument, got {len(args)}" + ) + type_source = None + if args[0].source: + install_guard(args[0].source.make_guard(GuardBuilder.TYPE_MATCH)) + type_source = TypeSource(args[0].source) + python_type = args[0].python_type() + if is_namedtuple_class(python_type): + type_source = AttrSource(ImportSource("collections"), "namedtuple") + return VariableTracker.build(tx, namedtuple, type_source) + return VariableTracker.build(tx, python_type, source=type_source) + + +class PyTreeTreeIsLeafFunctionVariable(UserFunctionVariable): + """ + `torch.utils._pytree.tree_is_leaf` function is a hot function. We want to special case it to reduce Dynamo tracing time. + + def tree_is_leaf( + tree: PyTree, + is_leaf: Callable[[PyTree], bool] | None = None, + ) -> bool: + if is_leaf is not None and is_leaf(tree): + return True + return _get_node_type(tree) not in SUPPORTED_NODES + + When is_leaf is None (the common case), we can optimize by not tracing into the function. + When is_leaf is not None, we fall back to regular tracing since it requires executing user code. + """ + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + # tree_is_leaf(tree, is_leaf=None) + if len(args) < 1 or len(args) > 2: + raise_type_error( + tx, f"tree_is_leaf requires 1 or 2 arguments, got {len(args)}" + ) + + # Check if is_leaf parameter is provided + is_leaf = kwargs.get("is_leaf", ConstantVariable.create(None)) + if len(args) == 2: + is_leaf = args[1] + + if not is_leaf.is_constant_none(): + return super().call_function(tx, args, kwargs) + + # Optimize the case where is_leaf is None + # return _get_node_type(tree) not in SUPPORTED_NODES + tree = args[0] + node_type_var = PyTreeGetNodeTypeFunctionVariable( + torch.utils._pytree._get_node_type + ).call_function(tx, [tree], {}) + + # If the SUPPORTED_NODES was seen earlier and mutated, there would be a + # source and that will give us the mutated SUPPORTED_NODES. + supported_nodes_var = VariableTracker.build( + tx, + torch.utils._pytree.SUPPORTED_NODES, + source=get_pytree_SUPPORTED_NODES_source(), + ) + out = supported_nodes_var.call_method(tx, "__contains__", [node_type_var], {}) + return VariableTracker.build(tx, not out.value) + + +class SparseTensorCreationSkipVariable(SkipFunctionVariable): + """ + Skip variable for sparse tensor factory functions with clear messaging regarding lack of support. + """ + + def __init__(self, value: Any, **kwargs: Any) -> None: + reason = "sparse tensor creation is not supported in torch.compile" + super().__init__(value, reason=reason, **kwargs) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .. import graph_break_hints + + fn_name = getattr(self.value, "__name__", str(self.value)) + unimplemented( + gb_type="Sparse tensor creation not supported", + context=f"function: {fn_name}", + explanation=( + f"torch.compile does not support sparse tensor creation functions like {fn_name}. " + "Sparse tensors require specialized handling that is not yet implemented in the compiler." + ), + hints=[*graph_break_hints.SPARSE_TENSOR], + ) + + +def emit_noargs_leaf_function_to_graph( + tx: "InstructionTranslator", + real_impl: Callable[[], None], + name: str, +) -> None: + """Emit an invoke_leaf_function node for a side-effectful function with no + tensor inputs or outputs. + + The function is captured as a closure inside _LeafCallable objects and + registered as a static attribute on the graph module. Because + invoke_leaf_function is registered as EffectType.ORDERED, effect tokens + prevent DCE and maintain execution ordering relative to other ops. + + Use this when Dynamo needs to preserve a pure-side-effect call (like + setting global runtime state) in the compiled graph so that it replays + at the correct position at runtime. + """ + import torch.utils._pytree as pytree + from torch._higher_order_ops.invoke_leaf_function import ( + _LeafCallable, + invoke_leaf_function, + make_leaf_function_wrappers, + ) + + def fake_impl(): + return None + + captured_out_spec: list[pytree.TreeSpec | None] = [None] + wrapped_real, wrapped_fake = make_leaf_function_wrappers( + real_impl, fake_impl, captured_out_spec + ) + + real_callable = _LeafCallable(wrapped_real) + fake_callable = _LeafCallable(wrapped_fake) + input_spec = pytree.tree_flatten(((), {}))[1] + + def make_proxy(attr_name: str, val: Any) -> Any: + proxy = tx.output.register_static_attr_and_return_proxy(attr_name, val) + proxy.node.type = type(val) + return proxy + + invoke_args = ( + make_proxy(f"{name}_real_fn", real_callable), + make_proxy(f"{name}_fake_fn", fake_callable), + make_proxy(f"{name}_input_spec", input_spec), + "", # mutated_flat_indices + ) + tx.output.create_proxy("call_function", invoke_leaf_function, invoke_args, {}) + + +class TritonSetAllocatorVariable(VariableTracker): + """Trace triton.set_allocator as an invoke_leaf_function node in the + graph so that it executes at the right point at runtime, ordered by + effect tokens.""" + + def __init__(self, value: Any, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.value = value + + def python_type(self) -> type: + return type(self.value) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + assert len(args) == 1 and not kwargs + alloc_fn = args[0].as_python_constant() + + # Emit an invoke_leaf_function node so it runs at runtime. + set_allocator = self.value + + def real_impl(): + set_allocator(alloc_fn) + return None + + emit_noargs_leaf_function_to_graph(tx, real_impl, "set_alloc") + + return ConstantVariable.create(None) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/hashable.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/hashable.py new file mode 100644 index 0000000000000000000000000000000000000000..5896479eaa5361e44eb585bda298f78794253652 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/hashable.py @@ -0,0 +1,163 @@ +""" +Hashability utilities for PyTorch Dynamo variable tracking. + +This module provides the HashableTracker wrapper class and associated utilities +for making VariableTracker instances usable as dictionary keys and set elements +during symbolic execution. Used by both ConstDictVariable and SetVariable. +""" + +from typing import TYPE_CHECKING + +import torch + +from .. import variables +from ..exc import raise_observed_exception +from ..utils import specialize_symnode +from .base import VariableTracker + + +if TYPE_CHECKING: + from torch._dynamo.symbolic_convert import InstructionTranslator + + +def raise_unhashable( + arg: VariableTracker, tx: "InstructionTranslator | None" = None +) -> None: + if tx is None: + from torch._dynamo.symbolic_convert import InstructionTranslator + + tx = InstructionTranslator.current_tx() + try: + arg_type = arg.python_type() + except Exception: + arg_type = type(arg) + + raise_observed_exception( + TypeError, + tx, + args=[ + f"unhashable type: {arg_type!r} and variable tracker = {type(arg.realize())}", + ], + ) + + +def is_hashable(x: VariableTracker) -> bool: + # NB - performing isinstance check on a LazVT realizes the VT, accidentally + # inserting the guard. To avoid this, lazyVT `is_hashable` methods looks at + # the underlying value without realizing the VT. Consider updating the + # lazyVT `is_hashable` method if you see unnecessary guarding for a key VT. + if ( + isinstance(x, variables.LazyVariableTracker) + and not x.is_realized() + and x.is_hashable() + ): + return True + return x.is_python_hashable() + + +class HashableTracker: + """ + Class that wraps a VariableTracker and makes it hashable. + Note that it's fine to put VTs into dictionaries and sets, but doing so + does not take into account aliasing. + """ + + _MISSING = object() + + def __init__(self, vt: VariableTracker) -> None: + # We specialize SymNodes + vt = specialize_symnode(vt) + + # If Dynamo does not know the hashability of the vt, it will raise unsupported here + # TODO(follow-up): check tp_hash via C-level slot detection — unhashable keys + # (e.g. list) should raise TypeError, not graph break via is_python_hashable/unimplemented. + if not is_hashable(vt): + raise_unhashable(vt) + self.vt = vt + + @classmethod + def _maybe_constant_torch_size(cls, vt: VariableTracker) -> object: + from .lists import SizeVariable + from .tensor import TensorVariable + + if ( + isinstance(vt, variables.LazyVariableTracker) + and not vt.is_realized() + and isinstance(vt.original_value(), torch.Size) + ): + return vt.original_value() + + if not isinstance(vt, SizeVariable): + return cls._MISSING + + items = [] + for item in vt.items: + if item.is_python_constant(): + items.append(item.as_python_constant()) + continue + + if isinstance(item, TensorVariable): + proxy = getattr(item, "proxy", None) + node = getattr(proxy, "node", None) + meta = getattr(node, "meta", None) if node is not None else None + example_value = ( + meta.get("example_value") if isinstance(meta, dict) else None + ) + constant = getattr(example_value, "constant", None) + + if isinstance(constant, torch.Tensor) and constant.numel() == 1: + items.append(constant.item()) + continue + + return cls._MISSING + + return torch.Size(items) + + def __hash__(self) -> int: + """ + Computes the hash value for the wrapped VariableTracker. + + For unrealized LazyVariableTrackers, uses the hash of the original value + to avoid realizing the tracker and inserting unnecessary guards. + For all other cases, delegates to the VariableTracker's get_python_hash method. + + Returns: + The hash value of the underlying variable tracker + """ + if ( + isinstance(self.vt, variables.LazyVariableTracker) + and not self.vt.is_realized() + and self.vt.is_hashable() + ): + return hash(self.vt.original_value()) + + maybe_constant = self._maybe_constant_torch_size(self.vt) + if maybe_constant is not self._MISSING: + return hash(maybe_constant) + + return self.vt.get_python_hash() + + def __eq__(self, other: object) -> bool: + """ + Checks equality between two HashableTracker instances. + + Delegates to the VariableTracker's is_python_equal method to compare + the underlying variable trackers for Python-level equality. + + Args: + other: Another HashableTracker instance to compare with + + Returns: + True if the underlying variable trackers are Python-equal, False otherwise + """ + if not isinstance(other, HashableTracker): + return False + if self.vt is other.vt: + return True + + self_constant = self._maybe_constant_torch_size(self.vt) + other_constant = self._maybe_constant_torch_size(other.vt) + if self_constant is not self._MISSING and other_constant is not self._MISSING: + return self_constant == other_constant + + return self.vt.is_python_equal(other.vt) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/higher_order_ops.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/higher_order_ops.py new file mode 100644 index 0000000000000000000000000000000000000000..18f99046ce767fb71e2e2ea39baf9dad6910deba --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/higher_order_ops.py @@ -0,0 +1,5706 @@ +""" +This module contains classes and utilities for handling higher-order operators in Dynamo. +It provides functionality for tracing and transforming control flow constructs like +conditions (torch.cond), loops (torch.while_loop), maps (torch.ops.higher_order.map), +and other higher-order operations. + +The module includes specialized VariableTracker classes for different types of +higher-order operations, along with utilities for: +- Speculating and capturing subgraphs +- Managing control flow +- Handling autograd function applications +- Supporting function transformations +- Processing activation checkpoints + +These classes work together to enable Dynamo to correctly trace and compile code +containing complex control flow patterns and higher-order functions while preserving +their semantic behavior. +""" + +import contextlib +import copy +import functools +import inspect +import itertools +import logging +import traceback +import types +import warnings +from collections.abc import Sequence +from dataclasses import dataclass, field +from typing import Any, cast, Literal, Optional, TYPE_CHECKING, Union + +import torch._C +import torch.fx +import torch.nn +from torch._dispatch.python import enable_python_dispatcher +from torch._dynamo.utils import get_fake_value +from torch._dynamo.variables.constant import ConstantVariable +from torch._dynamo.variables.ctx_manager import RepararametrizeModuleContextVariable +from torch._dynamo.variables.functions import UserFunctionVariable +from torch._dynamo.variables.nn_module import UnspecializedNNModuleVariable +from torch._dynamo.variables.script_object import TorchScriptObjectVariable +from torch._dynamo.variables.tensor import SymNodeVariable, TensorVariable +from torch._guards import Source +from torch._higher_order_ops.invoke_subgraph import NestedCompileRegionOptions +from torch._ops import HigherOrderOperator +from torch.fx.graph_module import GraphModule +from torch.fx.passes.shape_prop import _extract_tensor_metadata +from torch.fx.proxy import Proxy +from torch.multiprocessing.reductions import StorageWeakRef +from torch.utils import _pytree as pytree +from torch.utils._ordered_set import OrderedSet + +from .. import graph_break_hints, variables +from ..exc import ( + ObservedException, + UncapturedHigherOrderOpError, + unimplemented, + Unsupported, +) +from ..source import AttrSource, DictGetItemSource +from ..utils import proxy_args_kwargs, set_example_value +from .base import VariableTracker +from .dicts import ConstDictVariable +from .lazy import LazyVariableTracker +from .lists import ListVariable, TupleVariable +from .sets import SetVariable + + +if TYPE_CHECKING: + from torch._dynamo.output_graph import SubgraphTracer + from torch._dynamo.symbolic_convert import InstructionTranslator + from torch._subclasses.fake_tensor import FakeTensor, FakeTensorMode + from . import AutogradFunctionContextVariable + +from collections.abc import Generator, Iterable +from typing import ParamSpec, TypeVar + + +P = ParamSpec("P") +R = TypeVar("R") +HOP_VT_Alias = TypeVar("HOP_VT_Alias", bound="TorchHigherOrderOperatorVariable") + +log = logging.getLogger(__name__) +hc_log = torch._logging.getArtifactLogger(__name__, "hierarchical_compile") + + +@dataclass +class OutputSpec: + """ + Contains the treespec of the output of the speculated subgraph, and the + information to mask out the constant values from the output during + flattening and inserting them back during unflattening. Cleaning up + constants from the graph makes the graph simpler for AOTDispatcher and + Inductor. + """ + + treespec: pytree.TreeSpec + # list of True/False to identify the locations of const values in the + # subgraph output. True means that value at that index is a constant. + masks_to_filter_const_values: list[bool] | None = None + # The actual constant values that were present in the subgraph output. Note + # that this is the same length as the mask, we just look at the indices + # where mask is True. + const_values: list[Any] | None = None + # Number of intermediate nodes that are also made subgraph outputs. + num_intermediate_nodes_as_outputs: int = 0 + + def __post_init__(self) -> None: + if ( + self.masks_to_filter_const_values is not None + and self.const_values is not None + ): + assert len(self.masks_to_filter_const_values) == len(self.const_values) + + +@dataclass(frozen=True) +class SubgraphTracingInfo: + """Properties observed during subgraph tracing. + + Expandable container for metadata collected while tracing a HOP subgraph. + Future additions may include aliasing info, mutation info, etc. + """ + + # User code stack at the point where the first externally-visible side + # effect was detected. None means no side effect was observed. + side_effect_stack: traceback.StackSummary | None = None + # All sources accessed via VariableBuilder during the subgraph trace. + traced_sources: OrderedSet[Any] = field(default_factory=OrderedSet) + + +# This function is a syntax sugar for creating a dummy new subtracer so that +# newly added nodes are added to a separate subgraph in this subtracer instead of affecting +# the main graph. This is useful for creating sample inputs for tracing the subgraph. +# For example, in FlexAttentionHigherOrderVariable, we want to create several scalars +# to trace the score_mod function but we don't want the operators that creates the scalar to +# show up in the graph, we could this function to discard the graph changes. +# Example usage: +# with discard_graph_changes(): +# sample_input= create_sample_inputs() +# speculate_subgraph(tx, f, sample_inputs, {}) +@contextlib.contextmanager +def discard_graph_changes(tx: "InstructionTranslator") -> Generator[None, None, None]: + ctx = tx.output.subtracer("subgraph_wrapper", None) + try: + ctx.__enter__() + yield + finally: + ctx.__exit__(None, None, None) + + +def check_meta_consistency_vt( + vars1: list[VariableTracker], + vars2: list[VariableTracker], + lhs_name: str, + rhs_name: str, + include_contiguity: bool = True, +) -> None: + from torch._higher_order_ops.utils import check_meta_consistency + + def _unwrap_var(var: VariableTracker) -> Any: + if var.is_tensor(): + # pyrefly: ignore[missing-attribute] + return var.proxy.node.meta["example_value"] + elif isinstance(var, SymNodeVariable): + return var.sym_num + elif var.is_python_constant(): + return var.as_python_constant() + else: + unimplemented( + gb_type="cannot unwrap variable for check_meta_consistency", + context=str(var), + explanation=f"Expected {var} to be TensorVariable, SymNodeVariable, or ConstantVariable", + hints=[], + ) + + unwrapped1 = [_unwrap_var(var) for var in vars1] + unwrapped2 = [_unwrap_var(var) for var in vars2] + + return check_meta_consistency( + unwrapped1, + unwrapped2, + lhs_name, + rhs_name, + include_contiguity=include_contiguity, + ) + + +@contextlib.contextmanager +def dynamo_enable_grad( + tx: "InstructionTranslator", enable: bool = True +) -> Generator[None, None, None]: + from . import GradModeVariable + + org_value = torch.is_grad_enabled() + try: + GradModeVariable.create(tx, enable, initialized=True) + yield + finally: + GradModeVariable.create(tx, org_value, initialized=True) + + +@contextlib.contextmanager +def dynamo_allow_side_effects_in_hop( + tx: "InstructionTranslator", +) -> Generator[None, None, None]: + orig_val = tx.output.current_tracer.allow_side_effects_in_hop + try: + tx.output.current_tracer.allow_side_effects_in_hop = True + yield + finally: + tx.output.current_tracer.allow_side_effects_in_hop = orig_val + + +def find_mismatched_vars( + var: Any, types: type | tuple[type, ...], allow_none: bool = False +) -> set[VariableTracker]: + """ + Recursively finds variables whose type is not an instance of the specified types. + Args: + var: The variable to check. + types: A tuple of allowed types. + allow_none (bool): Whether to allow None values. Defaults to False. + Returns: + A set of variables whose type is not an instance of the specified types. + """ + mismatched_vars = set() + if isinstance(var, (list, tuple)): + for item in var: + mismatched_vars.update(find_mismatched_vars(item, types, allow_none)) + elif isinstance(var, (TupleVariable, ListVariable)): + for item in var.items: + mismatched_vars.update(find_mismatched_vars(item, types, allow_none)) + elif isinstance(var, ConstDictVariable): + for value in var.items.values(): + mismatched_vars.update(find_mismatched_vars(value, types, allow_none)) + elif isinstance(var, SetVariable): + for key in var.items: + mismatched_vars.update(find_mismatched_vars(key.vt, types, allow_none)) + else: + if not isinstance(var, types) and not (allow_none and var.is_constant_none()): + mismatched_vars.add(var) + return mismatched_vars + + +def only_consist_of( + var: Any, types: tuple[type, ...], allow_none: bool = False +) -> bool: + mismatch_vars = find_mismatched_vars(var, types, allow_none=allow_none) + return len(mismatch_vars) == 0 + + +# A more read-able syntax sugar for creating a UserFunctionVariable for f +# and run call_function on it. Make it return a function to preserve the calling +# convention of the original f. +from torch._dynamo.utils import _make_inlined + + +def add_call_function( + tx: "InstructionTranslator", + fn: Any, + args: tuple[Any, ...], + kwargs: dict[str, Any], + flat_example_value: Any, + config: NestedCompileRegionOptions | None = None, +) -> VariableTracker: + from .builder import wrap_fx_proxy + + proxy = tx.output.create_proxy( + "call_function", + fn, + args=args, + kwargs=kwargs, + ) + + # Set backend metadata if provided + if config is not None: + if "custom" not in proxy.node.meta: + # pyrefly: ignore [implicit-any] + proxy.node.meta["custom"] = {} + proxy.node.meta["custom"]["nested_region_config"] = config + assert proxy.node.target == torch._higher_order_ops.invoke_subgraph + + # Store the invocation as a call + flat_variable = wrap_fx_proxy( + tx=tx, + proxy=proxy, + example_value=flat_example_value, + ) + return flat_variable + + +def overwrite_tensor_vt_requires_grad( + graph_output_vts: Iterable[VariableTracker], flat_variable: VariableTracker +) -> None: + # All outputs of autograd.Function have requires_grad=True. We turn off + # grad_mode in autograd.Function, so our outputs naively have + # requires_grad=False. So we hackily force them back on here. A better + # solution would be to write python code that Dynamo could trace but we + # decided that it wasn't worth it. + # pyrefly: ignore[missing-attribute] + for orig_vt, subgraph_vt in zip(graph_output_vts, flat_variable.items): + if isinstance(orig_vt, variables.TensorVariable): + assert isinstance(subgraph_vt, variables.TensorVariable) + orig_vt.requires_grad = subgraph_vt.requires_grad + if orig_vt.requires_grad: + orig_vt.has_grad_fn = True + + +def overwrite_tensor_vt_proxy( + graph_output_vts: Iterable[VariableTracker], flat_variable: VariableTracker +) -> None: + # wrap_fx_proxy creates fresh variable trackers. However, the main program + # after the speculate subgraph can still use the original tensor vts that + # are still pointing to the nodes present in the subgraph. So, we reproxify + # the original tensor vts with the subgraph outputs. This way, whenever the + # outer graph uses an original vt, it uses the subgraph output. + # + # This is critical for maintaining the separation between: + # - `body_r`: The output VT structure that Dynamo continues tracing (may + # contain non-proxyable objects, nested structures, etc.) + # - `graph_output_vts`: Only the tensor/symint VTs that were actual graph + # outputs from speculate_subgraph + # + # By overwriting the proxies of VTs in `body_r` with the proxies from the + # HOP call, we ensure the outer graph correctly references the HOP outputs + # while still allowing `body_r` to contain arbitrary Python objects. + # pyrefly: ignore[missing-attribute] + for orig_vt, subgraph_vt in zip(graph_output_vts, flat_variable.items): + if isinstance( + orig_vt, + ( + variables.SymNodeVariable, + variables.TensorVariable, + TorchScriptObjectVariable, + ), + ): + assert subgraph_vt.is_tensor() or isinstance( + subgraph_vt, (SymNodeVariable, TorchScriptObjectVariable) + ) + orig_vt.proxy = subgraph_vt.proxy + + +def _call_function_with_auto_output_flattening( + tx: "InstructionTranslator", + fn: Any, + args: tuple[Any, ...], + kwargs: dict[str, Any], + flat_example_value: Any, + body_r: VariableTracker | None, + graph_output_vts: VariableTracker | tuple[VariableTracker, ...], + config: NestedCompileRegionOptions | None = None, +) -> VariableTracker | None: + """ + Create HOP call node and reproxify output VTs for HOPs with auto output semantics. + + This function is used by HOPs with auto output semantics (see speculate_subgraph_with_auto_output_flattening) + to create the actual HOP call in the FX graph and properly handle the output variable trackers. + + The key operation is "reproxifying" - updating the proxies of the original tensor VTs + (from body_r) to point to the HOP call outputs, ensuring the outer graph correctly + references the HOP outputs while allowing body_r to contain arbitrary Python objects. + + Args: + tx: The instruction translator + fn: The HOP function to call + args: Arguments for the HOP call (typically includes the subgraph node) + kwargs: Keyword arguments for the HOP call + flat_example_value: Example value for the HOP output + body_r: The output VT structure that Dynamo continues tracing with (may be None) + graph_output_vts: Tensor/symint VTs that were actual graph outputs + + Returns: + The body_r VT (unchanged), which Dynamo will continue tracing with + """ + + flat_variable = add_call_function(tx, fn, args, kwargs, flat_example_value, config) + if body_r is not None: + # pyrefly: ignore[bad-argument-type] + overwrite_tensor_vt_proxy(graph_output_vts, flat_variable) + return body_r + + +def _call_function_and_unflatten_output( + tx: "InstructionTranslator", + fn: Any, + args: tuple[Any, ...], + kwargs: dict[str, Any], + flat_example_value: Any, + ret_spec: OutputSpec, + body_r: VariableTracker | None, +) -> VariableTracker: + from .builder import SourcelessBuilder, wrap_fx_proxy + + # Store the invocation as a call + flat_variable = wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + fn, + args=args, + kwargs=kwargs, + ), + example_value=flat_example_value, + ) + + # wrap_fx_proxy creates fresh variable trackers. However, the main program + # after the speculate subgraph can still use the original tensor vts that + # are still pointing to the nodes present in the subgraph. So, we reproxify + # the original tensor vts with the subgraph outputs. This way, whenever the + # outer graph uses an original vt, it uses the subgraph output. + if body_r is not None: + # mypy: ignore[attr-defined] + for orig_vt, subgraph_vt in zip(body_r.items, flat_variable.items): + if orig_vt.is_tensor() or isinstance(orig_vt, SymNodeVariable): + assert subgraph_vt.is_tensor() or isinstance( + subgraph_vt, SymNodeVariable + ) + orig_vt.proxy = subgraph_vt.proxy + + if ret_spec.num_intermediate_nodes_as_outputs: + # The treespec was computed w/o any extra intermediate outputs. At this + # point, it is safe to just get rid of the extra outputs + flat_variable = SourcelessBuilder.create( + tx, + flat_variable.items[ # mypy: ignore[attr-defined] + : -ret_spec.num_intermediate_nodes_as_outputs + ], + ) + + if ret_spec.masks_to_filter_const_values: + from torch._dynamo.external_utils import insert_const_values_with_mask + + # During flattening, we removed the constant values. To ensure Dynamo + # can trace correctly, insert back the constant values in the output. + flat_variable = _make_inlined(tx, insert_const_values_with_mask)( + flat_variable, ret_spec.masks_to_filter_const_values, ret_spec.const_values + ) + + # Transform variable back into a list (previously made into a tuple by + # speculate_subgraph function) so as to respect the pytree API typing. + flat_list_variable = SourcelessBuilder.create(tx, list).call_function( + tx, [flat_variable], {} + ) + return ( + _make_inlined(tx, pytree.tree_unflatten)(flat_list_variable, ret_spec.treespec) + if ret_spec.treespec + else flat_variable + ) + + +def _assert_tensors_nonaliasing(inputs: Any, outputs: Any) -> None: + input_tensor_ids = { + id(t) for t in pytree.tree_leaves(inputs) if isinstance(t, torch.Tensor) + } + output_tensor_ids = { + id(t) for t in pytree.tree_leaves(outputs) if isinstance(t, torch.Tensor) + } + assert input_tensor_ids.isdisjoint(output_tensor_ids), ( + "inputs to function body cannot alias outputs" + ) + + +def get_tensor_storages(tensor: torch.Tensor) -> set[StorageWeakRef]: + """ + Get storage references from a tensor. + + Handles regular tensors. Raises NotImplementedError for sparse tensors + and traceable wrapper subclasses. + + Args: + tensor: The tensor to extract storages from + + Returns: + Set of StorageWeakRef objects for the tensor's storage(s) + """ + from torch.multiprocessing.reductions import StorageWeakRef + from torch.utils._python_dispatch import is_traceable_wrapper_subclass + + storages: set[StorageWeakRef] = set() + + if not isinstance(tensor, torch.Tensor): + return storages + + if tensor.is_sparse or tensor.is_sparse_csr: + raise NotImplementedError("get_tensor_storages does not support sparse tensors") + + if is_traceable_wrapper_subclass(tensor): + raise NotImplementedError( + "get_tensor_storages does not support traceable wrapper subclasses" + ) + else: + storages.add(StorageWeakRef(tensor._typed_storage())) + + return storages + + +class StorageAliasingTracker: + """ + Tracks storage references to detect aliasing between tensors. + + This class encapsulates the logic for collecting storages from tensors + and checking for aliasing conflicts. Used to filter intermediate outputs + that would create input-output or output-output aliasing. + """ + + def __init__(self) -> None: + self.excluded_storages: set[StorageWeakRef] = set() + + def _collect_storages_from_tensor(self, example_value: torch.Tensor) -> None: + self.excluded_storages.update(get_tensor_storages(example_value)) + + def collect_from_inputs(self, tx: "InstructionTranslator") -> None: + """Collect storages from graph input placeholders.""" + from torch._higher_order_ops.utils import _collect_fake_inputs + + for node in tx.output.graph.nodes: + if node.op == "placeholder": + example_value = _collect_fake_inputs([node])[0] + if isinstance(example_value, torch.Tensor): + self._collect_storages_from_tensor(example_value) + else: + break + + def collect_from_outputs(self, graph_output_vts: Sequence[VariableTracker]) -> None: + """Collect storages from existing graph outputs.""" + from torch._higher_order_ops.utils import _collect_fake_inputs + + for vt in graph_output_vts: + proxy = vt.as_proxy() + example_value = _collect_fake_inputs([proxy.node])[0] + if isinstance(example_value, torch.Tensor): + self._collect_storages_from_tensor(example_value) + + def check_and_track(self, proxy_node: Proxy) -> bool: + """ + Check if a tensor can be added as a subgraph output without causing aliasing issues. + + Given a proxy node, extracts its example tensor value and checks if its storage + aliases with any previously tracked storages (from inputs or other outputs). + If there's no aliasing conflict, the tensor's storage is added to the tracked set. + + Args: + proxy_node: An FX proxy node whose example_value is the tensor to check. + + Returns: + True if the tensor doesn't alias with tracked storages (safe to add as output), + False if it aliases (should be filtered out). + """ + from torch._higher_order_ops.utils import _collect_fake_inputs + from torch.multiprocessing.reductions import StorageWeakRef + from torch.utils._python_dispatch import is_traceable_wrapper_subclass + + example_value = _collect_fake_inputs([proxy_node])[0] + + # Non-tensor outputs (e.g., symints) don't have aliasing concerns + if not isinstance(example_value, torch.Tensor): + return True + + # Check if any storage aliases with existing inputs/outputs + tensor_storages = get_tensor_storages(example_value) + if tensor_storages & self.excluded_storages: + return False + + # Track this tensor's storage (for wrapper subclasses, inner storages were already checked) + if not is_traceable_wrapper_subclass(example_value): + if not (example_value.is_sparse or example_value.is_sparse_csr): + self.excluded_storages.add( + StorageWeakRef(example_value._typed_storage()) + ) + + return True + + +def collect_intermediate_outputs( + tx: "InstructionTranslator", + subtracer: "SubgraphTracer", + graph_output_vts: Sequence[VariableTracker], + filter_aliased_intermediates: bool = False, +) -> list[VariableTracker]: + extra_outputs = [] + existing_out_proxies = {vt.as_proxy() for vt in graph_output_vts} + + # Build the aliasing tracker if we're filtering + tracker = None + if filter_aliased_intermediates: + tracker = StorageAliasingTracker() + tracker.collect_from_inputs(tx) + tracker.collect_from_outputs(graph_output_vts) + + for out in subtracer.tracked_proxyable_vt: + proxy = out.as_proxy() + + # Skip if already in output + if proxy in existing_out_proxies: + continue + + # TODO floats are not supported in HOP input/output + if isinstance(out, SymNodeVariable) and out.python_type() is float: + continue + + if not filter_aliased_intermediates: + extra_outputs.append(out) + else: + # Filter out intermediates that alias with inputs or outputs. + # This is needed for HOPs like invoke_subgraph that don't support aliasing. + # TODO: If a filtered intermediate is captured by side effects (e.g., appended + # to a list), it will fail later with "does not belong to this Graph" error + # when the outer graph tries to use it. See test_side_effect_with_aliased_intermediate. + assert tracker is not None + if tracker.check_and_track(proxy.node): + extra_outputs.append(out) + + return extra_outputs + + +def _check_all_tensorvariable(args: Sequence[VariableTracker]) -> None: + if not all(type(a.realize()) is TensorVariable for a in args): + unimplemented( + gb_type="HOP: non torch.Tensor leaf", + context=f"args types: {[type(a.realize()) for a in args]}", + explanation="Expected all leaves to be of torch.Tensor type.", + hints=[], + ) + + +def _check_supported_callable_arg( + tx: "InstructionTranslator", func_var: VariableTracker, arg_name: str +) -> None: + from .builder import SourcelessBuilder + + is_callable = ( + SourcelessBuilder.create(tx, callable) + .call_function(tx, [func_var], {}) + .as_python_constant() + ) + if not is_callable: + unimplemented( + gb_type="HOP: non-callable variable", + context=f"arg name: {arg_name}, func_var type: {str(func_var)}", + explanation=f"{arg_name} should be a callable but is of type {str(func_var)}.", + hints=[], + ) + + +def _call_while_loop( + self: Union[ + "WhileLoopHigherOrderVariable", "WhileLoopStackOutputHigherOrderVariable" + ], + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + stack_output: bool, + hop_name: str, +) -> VariableTracker: + from torch._higher_order_ops.while_loop import _create_unbacked_symint + + args, kwargs = LazyVariableTracker.realize_all((args, kwargs)) + cond_fn, body_fn, operands, additional_inputs = args + + # Input checks + for i, k in enumerate(["cond_fn", "body_fn", "operands"]): + if v := kwargs.pop(k, None): + assert i == len(args), ( + "did not provide the right number of non-keyword args" + ) + args.append(v) + + if kwargs or len(args) != 4: + unimplemented( + gb_type="torch.while_loop: improper args/kwargs", + context=f"args: {args}, kwargs: {kwargs}", + explanation=f"torch.while_loop expects 4 positional arguments (got {len(args)}) " + f"and no keyword arguments (got {len(kwargs)}) " + "Usage: while_loop(cond_fn, body_fn, operands)", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + + # cond_fn and body_fn input check + _check_supported_callable_arg(tx, cond_fn, "cond_fn") + _check_supported_callable_arg(tx, body_fn, "body_fn") + + # operands input check + operands_seq = operands.unpack_var_sequence(tx) + + # additional_inputs input check + if not isinstance(additional_inputs, (ListVariable, TupleVariable)): + unimplemented( + gb_type="torch.while_loop: improper additional_inputs", + context=str(additional_inputs), + explanation=f"Expected additional_inputs to be a list/tuple but got {additional_inputs.python_type()}", + hints=[ + *graph_break_hints.DYNAMO_BUG, + ], + ) + additional_inputs_seq = additional_inputs.unpack_var_sequence(tx) + + with discard_graph_changes(tx): + # Note: this must be run under discard graph changes. + def unspecialize_carried_inputs( + tx: "InstructionTranslator", carry: VariableTracker + ) -> VariableTracker: + # See NOTE [unspecialize int carry with unbacked symints] + if ( + carry.is_python_constant() + and isinstance(carry.as_python_constant(), int) + ) or isinstance(carry, SymNodeVariable): + example_value = _create_unbacked_symint( + tx.output.fake_mode, ignore_fresh_unbacked_symbols=True + ) + proxy = tx.output.current_tracer.create_graph_input( + "unbacked_symint", type(example_value), example_value + ) + return SymNodeVariable.create(tx, proxy, example_value) + else: + # See NOTE [unspecialize constant tensor carry] + assert carry.is_tensor() + cloned_carry = carry.clone() + # type: ignore[attr-defined] + cloned_carry.proxy.node.meta["example_value"].constant = None + return cloned_carry + + # clone inputs across subgraphs, to avoid unbacked memoization in fake prop + cond_operands_seq = [ + unspecialize_carried_inputs( + tx, + ( + carry.call_method(tx, "clone", args=(), kwargs={}) + if carry.is_tensor() + else carry + ), + ) + for carry in operands_seq + ] + body_operands_seq = [ + unspecialize_carried_inputs( + tx, + ( + carry.call_method(tx, "clone", args=(), kwargs={}) + if carry.is_tensor() + else carry + ), + ) + for carry in operands_seq + ] + + # create cond subgrpahs + ( + (cond_r, _cond_treespec), + cond_graph, + cond_lifted_freevars, + ) = speculate_subgraph( + tx, + cond_fn, + cond_operands_seq + additional_inputs_seq, + {}, + hop_name, + source_target=self.value, + # NOTE [why we cannot use "automatic" for while_loop]: + # The reason is that we want to enforce + # the ordering of inputs and outputs to be consistent and the ordering + # of cond_fn and body_fn to the consistent. + # e.g. suppose we use "automatic" and we have: + # + # def body_fn(ph1, ph2): + # new_a, new_b = ph2.cos(), ph1.sin() + # return new_a, new_b + # + # a, b = torch.randn(3), torch.randn(3) + # new_a, new_b = body_fn(a, b) + # + # Using automatic, the ordering of arguments will be the order that they're + # used. In this example, the capture graph looks like: + # + # def captured_body(ph1, ph2): + # new_a, new_b = ph1.cos(), ph2.add_(1) + # return new_a, new_b + # + # This is fine when we change the calling convention of captured_body to be + # new_a, new_b = captured_body(b, a). + # But for while_loop, the next iteration's input is previous iteration output + # we'll end up feeding captured_body(new_a, new_b) instead. + # So it's best we always enforce the ordering of carried_inputs the same as outputs + # with "flatten_manual". + set_subgraph_inputs="flatten_manual", + supports_input_mutation=self.supports_input_mutation, + supports_aliasing=self.supports_aliasing, + remove_consts_from_outputs=False, + ) + cond_nn_modules = dict(tx.output.nn_modules) + validate_subgraph_output_types(cond_r) + if cond_r.is_tensor(): + cond_r_meta = _extract_tensor_metadata( + # type: ignore[attr-defined] + cond_r.proxy.node.meta["example_value"], + include_contiguity=False, + ) + if cond_r_meta.dtype != torch.bool or cond_r_meta.shape != torch.Size([]): + unimplemented( + gb_type="torch.while_loop: unsupported cond_fn return type", + context=str(cond_r), + explanation=f"Expected cond_fn to return a scalar tensor or a bool but got {cond_r_meta.shape}.", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + elif cond_r.is_python_constant(): + # short-circuiting while_loop when cond_fn returns a constant such as 0, 1 True or False + pred = cond_r.as_python_constant() + if pred: + unimplemented( + gb_type="torch.while_loop: infinite loop detected", + context=str(cond_r), + explanation=f"Infinite loop detected because while_loop's cond_fn always returns the same value {pred}.", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + else: + return operands + + # create body subgraph + ( + (body_r, body_treespec), + body_graph, + body_lifted_freevars, + ) = speculate_subgraph( + tx, + body_fn, + body_operands_seq + additional_inputs_seq, + {}, + hop_name, + source_target=self.value, + set_subgraph_inputs="flatten_manual", + should_flatten_outputs=True, + supports_input_mutation=False, + supports_aliasing=False, + remove_consts_from_outputs=False, + ) + validate_subgraph_output_types(body_r) + + # We set include contiguity=False because we have vmap x HOP tests, where if + # include_contiguity=True will call t.is_contiguous inside of vmap and get an error + # "querying is_contiguous inside of vmap for memory_format other than + # torch.contiguous_format is not yet implemented". This is okay because stride + # is still checked. + check_meta_consistency_vt( + body_r.unpack_var_sequence(tx), + operands_seq, + "body_fn_output", + "carried_inputs", + include_contiguity=False, + ) + + ( + cond_graph, + body_graph, + cond_shared, + _body_shared, + cond_unique, + body_unique, + ) = _merge_graph_inputs( + cond_graph, + cond_lifted_freevars, + "cond_fn", + body_graph, + body_lifted_freevars, + "body_fn", + ) + + # Note: cond_shared and body_shared refer to the same proxy in parent graph + # so using either of them is OK. Use cond_shared as it doesn't matter. + additional_lifted_inputs = cond_shared + cond_unique + body_unique + + body_nn_modules = dict(tx.output.nn_modules) + + cond_gm = torch.fx.GraphModule(cond_nn_modules, cond_graph) + body_gm = torch.fx.GraphModule(body_nn_modules, body_graph) + cond_name = tx.output.install_subgraph("cond_fn", cond_gm) + body_name = tx.output.install_subgraph("body_fn", body_gm) + + cond_node = make_attr(tx, cond_name) + body_node = make_attr(tx, body_name) + + operands_proxy = tuple(operand.as_proxy() for operand in operands_seq) + additional_inputs_proxy = tuple( + [inp.as_proxy() for inp in additional_inputs_seq] + additional_lifted_inputs + ) + p_args = ( + cond_node, + body_node, + operands_proxy, + additional_inputs_proxy, + ) + return _call_function_and_unflatten_output( + tx, + self.value, + p_args, + {}, + None, + body_treespec, + body_r, + ) + + +def are_same_graph_modules( + fn_name: str, a_mod: GraphModule, b_mod: GraphModule, fake_mode: "FakeTensorMode" +) -> bool: + from torch._subclasses._fake_tensor_utils import _CacheKeyState + from torch._subclasses.fake_tensor import extract_tensor_metadata + + # Maps the equivalent nodes from a to b + # pyrefly: ignore [implicit-any] + node_map = {} + + def check_all_args(a_nodes: Iterable[Any], b_nodes: Iterable[Any]) -> bool: + for arg_a, arg_b in zip(a_nodes, b_nodes): + if isinstance(arg_a, torch.fx.Node): + if node_map[arg_a] != arg_b: + return False + elif isinstance(arg_a, slice): + if not isinstance(arg_b, slice): + return False + if not check_all_args( + (arg_a.start, arg_a.stop, arg_a.step), + (arg_b.start, arg_b.stop, arg_b.step), + ): + return False + elif arg_a != arg_b: + # This is a catch-all for everything else. `slice` was a + # surprise but can there be other data structures that can + # contain fx.Nodes in them? + return False + return True + + for a_node, b_node in zip(a_mod.graph.nodes, b_mod.graph.nodes): + if a_node.op != b_node.op: + return False + + if a_node.op == "placeholder": + a_value = a_node.meta["example_value"] + b_value = b_node.meta["example_value"] + + if isinstance(a_value, torch.Tensor): + if not isinstance(b_value, torch.Tensor): + return False + # Extract fake tensor metadata for a and b and then compare + # pyrefly: ignore [implicit-any] + a_result = [] + state = _CacheKeyState(fake_mode.shape_env) + a_metadata = extract_tensor_metadata(a_value) + a_metadata._flatten_into(a_result, fake_mode, state) + + b_result = [] + state = _CacheKeyState(fake_mode.shape_env) + b_metadata = extract_tensor_metadata(b_value) + b_metadata._flatten_into(b_result, fake_mode, state) + if a_result != b_result: + return False + elif isinstance(a_value, torch.SymInt): + if not isinstance(b_value, torch.SymInt): + return False + if a_value is not b_value: + return False + elif a_node.op == "call_function": + if a_node.target is not b_node.target: + return False + a_flat, _ = pytree.tree_flatten((a_node.args, a_node.kwargs)) + b_flat, _ = pytree.tree_flatten((b_node.args, b_node.kwargs)) + if not check_all_args(a_flat, b_flat): + hc_log.debug( + "%s: Graph comparison failed at node (call_function): %s", + fn_name, + a_node, + ) + return False + elif a_node.op == "call_method": + if a_node.target != b_node.target: + return False + a_flat, _ = pytree.tree_flatten((a_node.args, a_node.kwargs)) + b_flat, _ = pytree.tree_flatten((b_node.args, b_node.kwargs)) + if not check_all_args(a_flat, b_flat): + hc_log.debug( + "%s: Graph comparison failed at node (call_method) : %s", + fn_name, + a_node, + ) + return False + elif a_node.op == "output": + a_flat, _ = pytree.tree_flatten((a_node.args, a_node.kwargs)) + b_flat, _ = pytree.tree_flatten((b_node.args, b_node.kwargs)) + if not check_all_args(a_flat, b_flat): + hc_log.debug("%s: Graph comparison failed at the output node", fn_name) + return False + elif a_node.op == "get_attr": + a_attr = getattr(a_mod, a_node.target) + b_attr = getattr(b_mod, b_node.target) + if isinstance(a_attr, torch.fx.GraphModule): + if not isinstance(b_attr, torch.fx.GraphModule): + return False + # This is an example of a HOP inside a HOP + if not are_same_graph_modules(fn_name, a_attr, b_attr, fake_mode): + return False + else: + # TODO - write an example with tensor as a graph attribute in + # the Fx graph + raise NotImplementedError(f"get_attr with {type(a_attr)}") + else: + # TODO - call_module is not supported because Dynamo Fx graph does + # not install a call_module + raise NotImplementedError(f"Graph equivalence check saw a {a_node.op}") + + # Two nodes are equal - add them to them map + node_map[a_node] = b_node + + return True + + +def validate_args_and_maybe_create_graph_inputs( + sub_args: list[VariableTracker], + tracer: "SubgraphTracer", + tx: "InstructionTranslator", + set_subgraph_inputs: str, + description: str, + sub_args_names: Sequence[str] | None = None, +) -> list[Any]: + from . import AutogradFunctionContextVariable + from .builder import SourcelessBuilder, wrap_fx_proxy_cls + + assert tracer.parent is not None + + if set_subgraph_inputs == "flatten_manual": + flat_args, tree_spec = _make_inlined(tx, pytree.tree_flatten)( + SourcelessBuilder.create(tx, sub_args) + ).unpack_var_sequence(tx) + + flat_inputs = validate_args_and_maybe_create_graph_inputs( + flat_args.unpack_var_sequence(tx), + tracer, + tx, + set_subgraph_inputs="manual", + description=description, + ) + + return _make_inlined(tx, pytree.tree_unflatten)( + SourcelessBuilder.create(tx, list(flat_inputs)), tree_spec + ).unpack_var_sequence(tx) + else: + if sub_args_names is not None: + # Can be greater if user passes some args as kwargs + assert len(sub_args_names) >= len(sub_args) + args = [] + for idx, a in enumerate(sub_args): + assert isinstance(a, VariableTracker) + new_arg = None + if set_subgraph_inputs == "automatic": + args.append(a) + continue + elif set_subgraph_inputs == "automatic_with_forced_inputs": + if isinstance(a, variables.TensorVariable): + node = a.maybe_fx_node() + assert node is not None + example_value = node.meta["example_value"] + arg_name = ( + a.as_proxy().node.name + if sub_args_names is None + else sub_args_names[idx] + ) + new_proxy = tracer.create_graph_input( + arg_name, a.python_type(), example_value + ) + example_value = node.meta.get("example_value", None) + a = wrap_fx_proxy_cls( + target_cls=type(a), + tx=tx, + proxy=new_proxy, + example_value=example_value, + ) + args.append(a) + continue + + if a.is_python_constant(): + # This arg is not used in the body of the higher order op. + # Currently, this new input is added to make the calls + # happy, which expect a fixed number of arguments. In + # future, we can clean this up. + arg_name = ( + "const_unused" + if sub_args_names is None + else f"const_unused_{sub_args_names[idx]}" + ) + tracer.create_graph_input( + arg_name, a.python_type(), a.as_python_constant() + ) + new_arg = a + # Weird special case, we probably want to delete it or fold it + # into the next case (of `a` being placeable into a graph) + elif isinstance(a, AutogradFunctionContextVariable): + example_value = a.as_proxy().node.meta["example_value"] + arg_name = ( + a.as_proxy().node.name + if sub_args_names is None + else sub_args_names[idx] + ) + tracer.create_graph_input(arg_name, a.python_type(), example_value) + new_arg = a + # If `a` can be put into a graph + elif a.maybe_fx_node() is not None: + node = a.maybe_fx_node() + assert node is not None + example_value = node.meta.get("example_value", None) + arg_name = node.name if sub_args_names is None else sub_args_names[idx] + new_proxy = tracer.create_graph_input( + arg_name, a.python_type(), example_value + ) + new_arg = wrap_fx_proxy_cls( + target_cls=type(a), + tx=tx, + proxy=new_proxy, + example_value=example_value, + ) + # If `a` cannot be put into a graph + else: + # HOPs work much better if they use speculate_subgraph(set_subgraph_inputs="automatic"). + unimplemented( + gb_type="HOP body taking non-Tensor as input", + context=str(sub_args), + explanation=f"{description} with body that accepts non-Tensors as input. " + f"Got type {a.python_type()} at index {idx}.", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + args.append(new_arg) + return args + + +# This helper function is used to make sure two graphs share the same input signature. For example, +# in torch.cond, two branches might lift different set of tensors as inputs. This function helps to +# dedup the inputs and modify the graphs to take the same set of inputs. +def _merge_graph_inputs( + l_graph: torch.fx.Graph, + l_lifted_freevars: dict[Proxy, Proxy], + l_name: str, + r_graph: torch.fx.Graph, + r_lifted_freevars: dict[Proxy, Proxy], + r_name: str, +) -> tuple[ + torch.fx.Graph, torch.fx.Graph, list[Proxy], list[Proxy], list[Proxy], list[Proxy] +]: + def dedup_and_sort_lifted_freevars( + l_lifted_freevars: dict[Proxy, Proxy], r_lifted_freevars: dict[Proxy, Proxy] + ) -> tuple[list[Proxy], list[Proxy], list[Proxy], list[Proxy]]: + # The nn module attributes are guaranteed to be registered into the top-level graph module during + # higher order op speculation. Therefore, get_attr nodes in two branches with the same + # target refer to the same attribute and we can safely deduplicate them with their target. + # + # Note: ideally, dynamo should just create a single proxy for the same attribute of a nn module. But + # true_branch and false_branch belong to two separate tracing contexts, they may register the same + # attribute to top level separately. This creates two get_attr proxies for the same attribute + # that have different meta data such as stack_trace (one stack trace for the true_branch, + # and the other for false_branch). It seems better to discard the proxy explicitly in cond + # than make dynamo create a single proxy for the same get_attr target. + def shared_getattrs( + l_lifted_proxies: Sequence[Proxy], r_lifted_proxies: Sequence[Proxy] + ) -> tuple[dict[Proxy, Proxy], dict[Proxy, Proxy]]: + true_targets = { + proxy.node.target: proxy + for proxy in l_lifted_proxies + if proxy.node.op == "get_attr" + } + l_shared_getattrs = {} + r_shared_getattrs = {} + + for false_proxy in r_lifted_proxies: + if ( + false_proxy.node.op == "get_attr" + and false_proxy.node.target in true_targets + ): + true_proxy = true_targets[false_proxy.node.target] + l_shared_getattrs[true_proxy] = true_proxy + r_shared_getattrs[false_proxy] = true_proxy + return l_shared_getattrs, r_shared_getattrs + + l_shared_getattrs, r_shared_getattrs = shared_getattrs( + l_lifted_freevars.keys(), # type: ignore[arg-type] + r_lifted_freevars.keys(), # type: ignore[arg-type] + ) + + l_shared_freevars = (l_lifted_freevars.keys() & r_lifted_freevars.keys()).union( + l_shared_getattrs.keys() + ) + r_shared_freevars = (l_lifted_freevars.keys() & r_lifted_freevars.keys()).union( + r_shared_getattrs.keys() + ) + unique_l_freevars = l_lifted_freevars.keys() - l_shared_freevars + unique_r_freevars = r_lifted_freevars.keys() - r_shared_freevars + + def _sort_by_name(vars: list[Proxy]) -> list[Proxy]: + return sorted(vars, key=lambda var: var.node.name) + + return ( + list(_sort_by_name(list(l_shared_freevars))), + list(_sort_by_name(list(r_shared_freevars))), + list(_sort_by_name(list(unique_l_freevars))), + list(_sort_by_name(list(unique_r_freevars))), + ) + + (l_shared, r_shared, unique_l, unique_r) = dedup_and_sort_lifted_freevars( + l_lifted_freevars, r_lifted_freevars + ) + + # Let's say we capture cond(pred, true_fn, false_fn, (x,)) + # With set_graph_input set to automatic, + # true_fn has lifted variables x, a, b, c + # false_fn has lifted variables x, a, b, d + # Then fixup_branch_inps make sure both branches have the same signature, i.e.: + # - true_fn(x, a, b, c_true_branch, d_false_branch) + # - false_fn(x, a, b, c_true_branch, d_false_branch) + # + # More formally, the signature has three parts in the following order: + # 1. used in both branches: x, a, b + # 2. only used in true branches: c, suffixed with _true_branch + # 3. only used in false branches: d, suffixed with _false_branch + # Within each part, we re-order the nodes by name to have a derterministic ordering for testing. + def fixup_branch_inps( + graph: torch.fx.Graph, + lifted_freevars: dict[Proxy, Proxy], + shared: Sequence[Proxy], + unique_l: Sequence[Proxy], + unique_r: Sequence[Proxy], + ) -> None: + def _insert_or_replace_phs(new_args: Sequence[Proxy], name_suffix: str) -> None: + for arg in new_args: + new_ph = graph.placeholder(arg.node.name + name_suffix) + new_ph.meta = arg.node.meta + # Override with new_ph if there exists a old placeholder. + if arg in lifted_freevars: + old_ph = lifted_freevars[arg].node + old_ph.replace_all_uses_with(new_ph) + # replace_all_uses_with doesn't clean users. Clean it manually so that we could erase it. + old_ph.users = {} + graph.erase_node(old_ph) + + first_not_ph_node = next( + node for node in graph.nodes if node.op != "placeholder" + ) + with graph.inserting_before(first_not_ph_node): + _insert_or_replace_phs(shared, "") + _insert_or_replace_phs(unique_l, "_" + l_name) + _insert_or_replace_phs(unique_r, "_" + r_name) + + fixup_branch_inps(l_graph, l_lifted_freevars, l_shared, unique_l, unique_r) + fixup_branch_inps(r_graph, r_lifted_freevars, r_shared, unique_l, unique_r) + return l_graph, r_graph, l_shared, r_shared, unique_l, unique_r + + +# NOTE: [HigherOrderOperator subgraph input ordering] +# The input ordering of the higher order ops is determined by the order of +# the creation of the placeholder. +# Manually created inputs are created in validate_args_and_maybe_create_graph_inputs before +# speculating subgraph. +# During subgraph speculation, we may lift closured tensors and free symbols as inputs, +# their ordering is determined by the time they are lifted: earlier lifted ones precede later +# lifted ones. +# +# Suppose the placeholders are +# O1, O2, X1, O3, O4, X2, X3, O5 where Xs are lifted phs +# The following code re-order the placeholders to +# O1, O2, O3, O4, O5, X1, X2, X3 +def move_lifted_freevars_phs_to_end( + graph: torch.fx.Graph, lifted_freevars: dict[Proxy, Proxy] +) -> None: + lifted_ph_set = {child_p.node for child_p in lifted_freevars.values()} + + prev_phs = [n for n in graph.nodes if n.op == "placeholder"] + + # No need to reorder when graph doesn't have args or doesn't + # have lifted freevars or all inputs are lifted freevars. + if ( + len(prev_phs) == 0 + or len(lifted_ph_set) == 0 + or len(prev_phs) == len(lifted_ph_set) + ): + return + + # Step 1: find first X1 + for x1 in prev_phs: + if x1 in lifted_ph_set: + break + + assert x1 is not None and x1.op == "placeholder" + # Step 2: starting from the X1, skip Xs and prepend Os before X1. + cand_x = x1.next + while cand_x is not None and cand_x.op == "placeholder": + if cand_x in lifted_ph_set: + cand_x = cand_x.next + else: + nxt = cand_x.next + cand_x._remove_from_list() + x1.prepend(cand_x) + cand_x = nxt + + # Step 3: assert that all placeholders are in the correct order as . + # in lifted_freevars + after_phs = [node for node in graph.nodes if node.op == "placeholder"][ + -len(lifted_freevars) : + ] + assert len(after_phs) == len(lifted_freevars) + for child_proxy, ph in zip(lifted_freevars.values(), after_phs): + assert child_proxy.node is ph, ( + "The order of placeholders is different from the order of lifted_freevars" + ) + + graph.lint() + + +def check_aliasing_and_input_mutation( + subtracer: "SubgraphTracer", + graph: torch.fx.Graph, + supports_input_mutation: bool, + supports_aliasing: bool, + source_target: Optional["HigherOrderOperator"], +) -> None: + name = source_target.name if source_target else "" + if not supports_input_mutation: + mutation_info = subtracer.has_input_mutation() + if mutation_info.has_mutation: + context = f"{mutation_info.msg} in\n {graph}" + unimplemented( + gb_type="Encountered input mutation during higher order op tracing", + context=context, + explanation=f"Higher order ops do not support input mutation. Found in {name}", + hints=[ + "Consider using the debug context to change user code to avoid mutation.", + "Please open an issue.", + ], + ) + + if not supports_aliasing: + aliasing_info = subtracer.has_aliasing() + if aliasing_info.has_aliasing: + context = f"{aliasing_info.msg} in\n {graph}" + unimplemented( + gb_type="Encountered aliasing during higher order op tracing", + context=context, + explanation=f"Higher order ops do not support aliasing. Found in {name}", + hints=[ + "Replace `return input` with `return input.clone()` to avoid aliasing.", + "Consider using the debug context to change user code to avoid aliasing.", + "Please open an issue.", + ], + ) + + +def trace_hop_function( + f: VariableTracker, + tx: "InstructionTranslator", + subtracer: "SubgraphTracer", + enable_grad: bool | None, + restore_side_effects: bool, + args: Sequence[VariableTracker], + sub_kwargs: dict[str, VariableTracker], +) -> VariableTracker: + # For autograd.Function and other legacy HOPs, we do NOT couple + # restore_side_effects with allow_side_effects_in_hop. + # This preserves the old behavior where: + # - restore_side_effects=False means ctx mutations persist + # - But non-ctx side effects still cause graph breaks (under_activation_checkpoint was False) + enable_side_effects_with_extra_outputs = False + + autograd_ctx = ( + dynamo_enable_grad(tx, enable_grad) + if enable_grad is not None + else contextlib.nullcontext() + ) + side_effects_ctx = ( + dynamo_allow_side_effects_in_hop(tx) + if enable_side_effects_with_extra_outputs + else contextlib.nullcontext() + ) + + # For handling side effects, we can make an argument that we don't + # have to do anything here. The side effects infra does a good job + # of graph breaking if we mutate any nonlocal or global variable + # while subtracing. As a result if tracing succeeds, side effects + # data structure will only contain read-only data structures that + # are put there for tracking purposes. + # But on the other hand, there is an argument that if we ever write + # a new side effect in Dynamo which does not go through the side + # effect infra, we can end up in bad state. + # Therefore we restore the side effects after tracing. The catch is + # that we have to special handle tensor variables. If we have seen a + # nonlocal variable tensor during subtracing, we want to keep a + # track of that tensor, so that later subtracing or the root tracer + # itself does not create a new proxy for the already observed tensor + # variable. + prev_side_effects = None + if restore_side_effects: + prev_side_effects = tx.output.side_effects.clone() + + with autograd_ctx, side_effects_ctx: + output = f.call_function(tx, args, sub_kwargs) + + if restore_side_effects: + new_side_effects = tx.output.side_effects.clone() + assert prev_side_effects is not None + prev_side_effects.track_runahead_tensor_and_symvar_side_effects( + new_side_effects + ) + tx.output.side_effects = prev_side_effects + return output + + +def trace_hop_function_with_auto_output_flattening( + f: VariableTracker, + tx: "InstructionTranslator", + subtracer: "SubgraphTracer", + enable_grad: bool | None, + allow_side_effects: bool, + args: Sequence[VariableTracker], + sub_kwargs: dict[str, VariableTracker], +) -> VariableTracker: + autograd_ctx = ( + dynamo_enable_grad(tx, enable_grad) + if enable_grad is not None + else contextlib.nullcontext() + ) + side_effects_ctx = ( + dynamo_allow_side_effects_in_hop(tx) + if allow_side_effects + else contextlib.nullcontext() + ) + + with autograd_ctx, side_effects_ctx: + output = f.call_function(tx, args, sub_kwargs) + + return output + + +def get_hop_args( + tx: "InstructionTranslator", + f: VariableTracker, + subtracer: "SubgraphTracer", + sub_args: list[VariableTracker], + sub_kwargs: dict[str, VariableTracker], + set_subgraph_inputs: str, + description: str, +) -> list[VariableTracker]: + sub_args_names = maybe_positional_arg_names(f) + # User mismatch in the number of args. Will eventually lead to an error. + if sub_args_names is not None and len(sub_args_names) < len(sub_args): + sub_args_names = None + args = validate_args_and_maybe_create_graph_inputs( + sub_args, + subtracer, + tx, + set_subgraph_inputs, + description, + sub_args_names, + ) + + validate_args_and_maybe_create_graph_inputs( + sub_kwargs.values(), # type: ignore[arg-type] + subtracer, + tx, + set_subgraph_inputs="automatic", + description=description, + ) + return args + + +# TODO - The eventual goal is to replace +# speculate_subgraph_with_auto_output_flattening with speculate_subgraph or +# merge them two into one. We are following a staged approach because of +# existing implementation complexity for control flow ops. +def speculate_subgraph_with_auto_output_flattening( + tx: "InstructionTranslator", + f: VariableTracker, + sub_args: Sequence[VariableTracker], + sub_kwargs: dict[str, VariableTracker] | None, + description: str, + *, + # source_target is the .value of HigherOrderOpVariable and is the + # target of the proxy that we created for the higherOrderOperator. + source_target: HigherOrderOperator | None = None, + enable_grad: bool | None = None, + # automatic: relies on Dynamo to find the used tensors and lift them as + # inputs. + # + # automatic_with_forced_inputs: relies on the function arg names to create + # a new proxy. Also, it will always INSERT a tensor placeholder as input, + # even though it might not be used in the graph and they will also be in the + # same order as the original function (as opposed to automatic which will + # not insert the unused placeholder and can insert other placeholders in the + # order they are see while tracing). This is useful for autograd.Function + # backward where we do need to account for all the inputs of the backwards + # to be lifted as inputs for making the fwd-bwd graph consistent. + set_subgraph_inputs: Literal[ + "automatic", "automatic_with_forced_inputs", "flatten_manual", "manual" + ] = "automatic", + # If True, exposes intermediates to subgraph outputs to allow later tensor ops to + # access intermediates from the subgraph, this is useful for mutation + allow_side_effects: bool = False, + # Controls whether to filter aliased intermediates when collecting extra outputs. + # This is only relevant when allow_side_effects=True. + # - True: Filter out intermediates that alias with inputs or outputs (strict, for invoke_subgraph) + # - False: Allow aliased intermediates (for checkpoint/autograd.Function which get desugared/inlined) + # + # Example where filtering is needed: + # + # @invoke_subgraph + # def gn(x): + # view = x.view(2, 4) # intermediate that aliases input x + # y = torch.sin(view) + # return torch.cos(view) + # + # def fn(x): + # res = gn(x) + # return res + 4 + # + # In this case, if we don't filter `view`, we would later error because some HOPs + # have strict aliasing checks on inputs/outputs. + # + # This does however introduce a subtle issue when we do something like: + # + # captured = [] + # + # @invoke_subgraph + # def gn(x): + # view = x.view(2, 4) # intermediate that aliases input x + # y = torch.sin(view) + # captured.append(view) + # return torch.cos(view) + # + # def fn(x): + # res = gn(x) + # return res + captured[0] + # + # In this case, we will not replay the side effect on `captured` in the graph, + # which fails with a not-so-nice error. We will address this in a follow-up PR + # because this case is rare. This is not a regression because side effects were + # never supported for invoke_subgraph anyway. + filter_aliased_intermediates: bool = False, + # TODO - supports input_mutation and aliasing should be False by default for strictness + supports_input_mutation: bool = True, + supports_aliasing: bool = True, + # Pass in an originating tracer - this is needed for preserving context + # across fwd-bwd for autograd.Function + tracer: Optional["SubgraphTracer"] = None, +) -> tuple[ + VariableTracker, # output: The VT that Dynamo continues tracing with + torch.fx.Graph, # graph: The FX graph representing the subgraph computation + dict[ + torch.fx.Proxy, torch.fx.Proxy + ], # lifted_freevars: Free variables lifted as inputs + VariableTracker + | tuple[ + VariableTracker, ... + ], # graph_output_vts: Tensor/symint VTs that are actual FX graph outputs + SubgraphTracingInfo, # tracing_info: Properties observed during subgraph tracing +]: + """ + Speculate subgraph for Higher-Order Operators (HOPs) with automatic output flattening. + + ## Automatic output flattening + + For many HOPs, the representation exists only as a container for the + subgraph. In later compiler stages or at runtime, the HOP is desugared and + simply executes the subgraph directly, as if it were inlined. For such hops, + we follow automatic output flattening. + For example: + - invoke_subgraph + - activation checkpointing (torch.utils.checkpoint.checkpoint) + - autograd.Function + - nested_compile_region + + This is in contrast to control flow HOPs which do not follow this desugaring: + - torch.cond (conditional execution based on predicate) + - torch.while_loop (iterative execution) + - torch.map (parallel execution over batch dimension) + + For control flow HOPs, the HOP behavior is fundamentally different from just + running the body function once. + + ## Key Advantage: Disentangling VTs from Graph Outputs + + Desugaring simplify HOP processing by allowing us to disentangle the output + variable trackers (VTs) from the HOP subgraph outputs. This mirrors typical + Dynamo processing where: + - VTs "run ahead" representing the program state for continued tracing + - The graph is a side data structure tracking computation seen so far + + This separation is crucial for HOPs with non-proxyable outputs (e.g., custom + user-defined objects containing tensors). The function may return complex Python + objects for Dynamo to continue tracing, but only the tensor/symint VTs need to + be registered as actual FX graph outputs. + + Example: + class Foo: + def __init__(self, a, b): + self.a = a # tensor + self.b = b # tensor + + def gn(x): + return Foo(torch.sin(x), torch.cos(x)) + + result = some_hop(gn, x) # Returns Foo instance + out = result.a + result.b # Dynamo can continue tracing + + Here, `output` VT is a UserDefinedObjectVariable wrapping Foo, but + `graph_output_vts` contains only the tensor VTs (a and b) that should be + actual FX graph outputs. This allows Dynamo to continue tracing with the + Foo object while the graph only needs to output the constituent tensors. + + ## Return Values + + Unlike `speculate_subgraph`, this function returns: + - output: The VT that Dynamo continues tracing with (may be complex Python objects) + - graph: The FX graph representing the subgraph computation + - lifted_freevars: Free variables lifted as inputs to the subgraph + - graph_output_vts: Only the tensor/symint VTs that are actual FX graph outputs + + The key difference is `graph_output_vts` instead of `treespec`, which gives more + flexibility for handling non-proxyable outputs. + """ + if sub_kwargs is None: + sub_kwargs = {} + + assert set_subgraph_inputs in { + "automatic", + "automatic_with_forced_inputs", + "flatten_manual", + "manual", + }, "Please use one of the supported set_subgraph_inputs options." + + # See NOTE [Temporary argument `set_subgraph_inputs`] + if sub_kwargs and set_subgraph_inputs != "automatic": + unimplemented( + gb_type="invalid set_subgraph_inputs and sub_kwargs settings", + context=f"set_subgraph_inputs: {set_subgraph_inputs}, sub_kwargs: {sub_kwargs}", + explanation="`sub_kwargs` cannot be used when `set_subgraph_inputs` is not set to 'automatic'.", + hints=[ + "Use `set_subgraph_inputs='automatic'` when passing `sub_kwargs`.", + *graph_break_hints.USER_ERROR, + ], + ) + + try: + with tx.output.subtracer(source_target, tracer, description) as subtracer: + args = get_hop_args( + tx, + f, + subtracer, + list(sub_args), + sub_kwargs, + set_subgraph_inputs, + description, + ) + + # Special case - if users uses + # `traced_with_externally_visible_side_effects`, we still need to + # return the intermediates as outputs. However, this API gets + # triggered during the hop tracing, and we don't know at this point + # of time, if the API will take into effect. To handle this, we have + # a flag traced_with_externally_visible_side_effects (default=False) + # that is set to True anytime + # `traced_with_externally_visible_side_effects` is set. We reset it + # with the old value after the hop is traced out. + old_value = ( + tx.output.current_tracer.traced_with_externally_visible_side_effects + ) + + output = trace_hop_function_with_auto_output_flattening( + f, + tx, + subtracer, + enable_grad, + allow_side_effects, + args, + sub_kwargs, + ) + + # NOTE: [Separation of graph outputs and output VTs] + # In Dynamo (outside of speculate_subgraph), VTs and the graph are + # separate concepts: + # - VTs (VariableTrackers) can "run ahead" and continue Dynamo tracing + # - The graph is just a side data structure tracking computation seen so far + # + # This separation is crucial for HOPs with non-proxyable outputs (e.g., + # custom user-defined objects containing tensors). The function may return + # complex Python objects for Dynamo to continue tracing, but only the + # tensor/symint VTs need to be registered as actual graph outputs. + # + # Example: + # class Foo: + # def __init__(self, a, b): + # self.a = a # tensor + # self.b = b # tensor + # + # def gn(x): + # return Foo(torch.sin(x), torch.cos(x)) + # + # Here, `output` VT is a UserDefinedObjectVariable wrapping Foo, but + # `graph_output_vts` contains only the tensor VTs (a and b) that should + # be actual FX graph outputs. + # Collect only tensor and symint VTs that should be graph outputs. + # We walk the output structure and extract proxyable VTs. + graph_output_vt_list = [] + + def visit(vt: VariableTracker) -> None: + if vt.is_tensor() or isinstance( + vt, (SymNodeVariable, TorchScriptObjectVariable) + ): + graph_output_vt_list.append(vt) + + VariableTracker.visit(visit, output, side_effects=tx.output.side_effects) + graph_output_vts = tuple(graph_output_vt_list) + + # NOTE - [Return subgraph intermediates as subgraph outputs] + # This helps HOPs which allow side effects. Consider the + # following example + # + # def gn(x, z): + # o = torch.matmul(x, x) @ x + # out = x.sin() + # z.append(out) + # return torch.cos(torch.sin(o)) + + # def fn(x): + # z = [] + # out1 = torch.utils.checkpoint.checkpoint( + # gn, + # x, + # z, + # use_reentrant=False, + # ) + # return out1, z[0] + # + # In this example, list `z` is in outer scope and gets appended + # in the subgraph with `out`. But `out` is not an output of the + # subgraph. This can cause issue because later on when the outer + # graph returns `z[0]` it needs to have access to the graph node + # `out`. To solve this problem, we just return all intermediates + # from the subgraph. + + # TODO - Today this is supported only for AC. AC HOP gets + # desugared in AOTDispatcher so even though subgraph has extra + # unused outputs in Dynamo, its ok even if we don't DCE them in + # Dynamo. As AOTDispatcher desugars/inlines the subgraph, the + # subgraph boundary disappears. And even for AC, today this only + # works when the skip_fwd_side_effects_in_bwd_under_checkpoint + # flag is True, i.e., only when we allow side-effects. But, we + # want this to be supported for other Hops as well, specifically + # nested_compile_region and autograd.Function. Today, its safe + # because we error out on seeing a side-effect. + + traced_externally = ( + tx.output.current_tracer.traced_with_externally_visible_side_effects + ) + has_side_effects = ( + subtracer.side_effect_stack is not None or traced_externally + ) + if (allow_side_effects or traced_externally) and has_side_effects: + extra_outputs = collect_intermediate_outputs( + tx, subtracer, graph_output_vts, filter_aliased_intermediates + ) + graph_output_vts = graph_output_vts + tuple(extra_outputs) + + tx.output.current_tracer.traced_with_externally_visible_side_effects = ( + old_value + ) + + validate_subgraph_output_types(graph_output_vts) + + # The output proxies might not belong to this SubgraphTracer + # (if they are free variables that were never lifted) + # so lift them here. + # output_proxies = output.as_proxy() + if isinstance(graph_output_vts, tuple): + output_proxies = [a.as_proxy() for a in graph_output_vts] # type: ignore[attr-defined] + output_proxies = pytree.tree_map( + subtracer.maybe_lift_tracked_freevar_to_input, output_proxies + ) + output_proxies = tuple(output_proxies) + else: + output_proxies = output.as_proxy() + output_proxies = pytree.tree_map( + subtracer.maybe_lift_tracked_freevar_to_input, output_proxies + ) + + tx.output.create_node( + "output", + "output", + (subtracer.create_arg((output_proxies,))), + {}, + ) + graph = tx.output.graph + graph.lint() + lifted_freevars = subtracer.lifted_freevars + + if len(lifted_freevars) > 0: + move_lifted_freevars_phs_to_end(graph, lifted_freevars) + + check_aliasing_and_input_mutation( + subtracer, + graph, + supports_input_mutation, + supports_aliasing, + source_target, + ) + # Return both the output VT and the graph output VTs separately: + # - `output`: The VT that Dynamo continues tracing with (may be + # complex Python objects, tuples, dicts, etc.) + # - `graph`: The FX graph representing the subgraph computation + # - `lifted_freevars`: Free variables lifted as inputs to the subgraph + # - `graph_output_vts`: Only the tensor/symint VTs that are actual + # FX graph outputs (basically the vts associated with graph outputs) + # - `tracing_info`: Properties observed during subgraph tracing + tracing_info = SubgraphTracingInfo( + side_effect_stack=subtracer.side_effect_stack, + traced_sources=subtracer.traced_sources, + ) + + return ( + output, + graph, + lifted_freevars, + graph_output_vts, + tracing_info, + ) + except Unsupported as ex: + f_name = f"{type(f).__name__}" + if isinstance(f, UserFunctionVariable): + f_name = f.get_name() + msg = ( + f"speculate_subgraph: while introspecting {description}, we were unable " + f"to trace function `{f_name}` into a single graph. This means " + f"that Dynamo was unable to prove safety for this API and will " + f"fall back to eager-mode PyTorch, which could lead to a slowdown." + ) + log.info(msg) + log.info(ex) # noqa: G200 + raise ex + + +# See NOTE [HigherOrderOperator tracing design] for details of the design +def speculate_subgraph( + tx: "InstructionTranslator", + f: VariableTracker, + sub_args: Sequence[VariableTracker], + sub_kwargs: dict[str, VariableTracker] | None, + description: str, + *, + # source_target is the .value of HigherOrderOpVariable and is the + # target of the proxy that we created for the higherOrderOperator. + source_target: HigherOrderOperator | None = None, + always_restore: bool = False, + enable_grad: bool | None = None, + # NOTE [argument `set_subgraph_inputs`] + # set_subgraph_inputs controls what how to construct subgraphs' placeholders from sub_args. + # 1. if your HOP supports arbitrary inputs, use set_subgraph_inputs="automatic" (most recommended). + # 2. if your HOP supports only Tensor and symnode inputs, use set_subgraph_inputs="flatten_manual" (recommended). + # If sub_args contain Pytree structure (e.g. dict/list/tuple/set), the sub_args will be flattened first. + # Then the flattened args are manually set as subgraph's placeholders. + # 3. if your HOP must preserve inputs that are not tensor or symnode as placeholders e.g. AutogradFunctionContextVariable + # use set_subgraph_inputs="manual" (not recommended). We do not recommend it in general because it has the + # restriction that user need to manually control how to create placeholders and VariableTrackers for the args. + set_subgraph_inputs: Literal[ + "automatic", "semi_automatic", "flatten_manual", "manual" + ] = "automatic", + restore_side_effects: bool = True, + should_flatten_outputs: bool = False, + # if should_flatten_outputs is True, `remove_consts_from_outputs` remove the + # const outputs from the subgraph output. + remove_consts_from_outputs: bool = True, + # TODO - supports input_mutation and aliasing should be False by default for strictness + supports_input_mutation: bool = True, + supports_aliasing: bool = True, + # Pass in an originating tracer - this is needed for preserving context + # across fwd-bwd for autograd.Function + tracer: Optional["SubgraphTracer"] = None, +) -> tuple[tuple[VariableTracker, OutputSpec], torch.fx.Graph, dict[Proxy, Proxy]]: + if sub_kwargs is None: + sub_kwargs = {} + + from .builder import SourcelessBuilder + + assert set_subgraph_inputs in { + "automatic", + "automatic_with_forced_inputs", + "flatten_manual", + "manual", + }, "Please use one of the supported set_subgraph_inputs options." + + # See NOTE [Temporary argument `set_subgraph_inputs`] + if sub_kwargs and set_subgraph_inputs != "automatic": + unimplemented( + gb_type="invalid set_subgraph_inputs and sub_kwargs settings", + context=f"set_subgraph_inputs: {set_subgraph_inputs}, sub_kwargs: {sub_kwargs}", + explanation="`sub_kwargs` cannot be used when `set_subgraph_inputs` is not set to 'automatic'.", + hints=[ + "Use `set_subgraph_inputs='automatic'` when passing `sub_kwargs`.", + *graph_break_hints.USER_ERROR, + ], + ) + + try: + # ensure guards on args get installed in parent subgraph + f, sub_args, sub_kwargs = LazyVariableTracker.realize_all( + (f, sub_args, sub_kwargs), + ) + + with tx.output.subtracer(source_target, tracer, description) as subtracer: + args = get_hop_args( + tx, f, subtracer, sub_args, sub_kwargs, set_subgraph_inputs, description + ) + + output = trace_hop_function( + f, + tx, + subtracer, + enable_grad, + restore_side_effects, + args, + sub_kwargs, + ) + + treespec = None + masks_to_filter_const_values = None + const_values = None + if should_flatten_outputs: + from torch._dynamo.external_utils import filter_out_const_values + + # Flatten the speculated subgraph output. + output, treespec = _make_inlined(tx, pytree.tree_flatten)( + output + ).unpack_var_sequence(tx) + + # Actually, transform the list (returned by flatten) into a tuple + # for dynamo consistency. + output = SourcelessBuilder.create(tx, tuple).call_function( + tx, [output], {} + ) + + if remove_consts_from_outputs: + # Filter out the constants and save them into a spec. Filtering + # out constants makes the graph simpler for the backends. We + # need to ensure that after unflattening the constants are + # inserted back at the right positions for the Dynamo tracing to + # continue. This is done by filter_const_spec + output_proxies = output.as_proxy() + masks_to_filter_const_values = pytree.tree_map( + lambda x: not isinstance(x, torch.fx.Proxy), output_proxies + ) + const_values = pytree.tree_map( + lambda x: None if isinstance(x, torch.fx.Proxy) else x, + output_proxies, + ) + output = _make_inlined(tx, filter_out_const_values)( + output, masks_to_filter_const_values + ) + + # TODO - clean up num_intermediate_nodes_as_outputs - we do not need + # after AC moved to auto_output_flattening + num_intermediate_nodes_as_outputs = 0 + # Register output to graph + # Modeled off of compile_and_call_fx_graph + # TODO: support pytree output + # We check always_restore because we dont use the output or side effects of always_restore code, + # like bwd. + if always_restore: + # Nothing left to do here + return ( + ( + output, + OutputSpec( + treespec, # type: ignore[arg-type] + masks_to_filter_const_values, + const_values, + num_intermediate_nodes_as_outputs, + ), + ), + tx.output.graph, + subtracer.lifted_freevars, + ) + else: + validate_subgraph_output_types(output) + + # The output proxies might not belong to this SubgraphTracer + # (if they are free variables that were never lifted) + # so lift them here. + output_proxies = output.as_proxy() + output_proxies = pytree.tree_map( + subtracer.maybe_lift_tracked_freevar_to_input, output_proxies + ) + + tx.output.create_node( + "output", + "output", + (subtracer.create_arg((output_proxies,))), + {}, + ) + graph = tx.output.graph + graph.lint() + lifted_freevars = subtracer.lifted_freevars + + if len(lifted_freevars) > 0: + move_lifted_freevars_phs_to_end(graph, lifted_freevars) + check_aliasing_and_input_mutation( + subtracer, + graph, + supports_input_mutation, + supports_aliasing, + source_target, + ) + + return ( + ( + output, + OutputSpec( + treespec, # type: ignore[arg-type] + masks_to_filter_const_values, + const_values, + num_intermediate_nodes_as_outputs, + ), + ), + graph, + lifted_freevars, + ) + + except Unsupported as ex: + f_name = f"{type(f).__name__}" + if isinstance(f, UserFunctionVariable): + f_name = f.get_name() + msg = ( + f"speculate_subgraph: while introspecting {description}, we were unable " + f"to trace function `{f_name}` into a single graph. This means " + f"that Dynamo was unable to prove safety for this API and will " + f"fall back to eager-mode PyTorch, which could lead to a slowdown." + ) + log.info(msg) + log.info(ex) # noqa: G200 + raise ex + + +def make_attr(tx: "InstructionTranslator", name: str) -> Proxy: + node = tx.output.create_proxy( + "get_attr", + name, + (), + {}, + ) + return node + + +def add_hop_context(cls: type[HOP_VT_Alias]) -> type[HOP_VT_Alias]: + """ + Class decorator that adds HOP context to exceptions raised in call_function. + + Requires the class to have _HOP_NAME and _ALLOW_FALLBACK_TO_EAGER set. + """ + + if hasattr(cls.call_method, "_hop_wrapped"): + return cls + + if cls._HOP_NAME is None: + raise TypeError(f"{cls.__name__} must define _HOP_NAME class attribute.") + if cls._ALLOW_FALLBACK_TO_EAGER is None: + raise TypeError( + f"{cls.__name__} must define _ALLOW_FALLBACK_TO_EAGER class attribute." + ) + + original_call_function = cls.call_function + + @functools.wraps(original_call_function) + def wrapped_call_function(self, *args: Any, **kwargs: Any) -> VariableTracker: + try: + return original_call_function(self, *args, **kwargs) + except UncapturedHigherOrderOpError as e: + if not hasattr(e, "_hop_name"): + e._hop_name = self._HOP_NAME # pyrefly: ignore[missing-attribute] + raise + except (Unsupported, ObservedException) as e: + # Only tag if not already tagged (reports deepest HOP only) + if hasattr(e, "_hop_name"): + raise + + if self._ALLOW_FALLBACK_TO_EAGER: + # Tag the exception with HOP name for later formatting in exc.py + # NOTE: because nested graph breaks are NOT supported on HOPs, we will + # NEVER log a HOP graph break before running this + e._hop_name = self._HOP_NAME # pyrefly: ignore[missing-attribute] + raise + else: + real_stack = getattr(e, "real_stack", None) + full_msg = ( + "This higher order operator doesn't work unless it is " + "captured completely with torch.compile. Got graph break/error:" + f"\n\n{str(e)}" + ) + exc = UncapturedHigherOrderOpError(full_msg, real_stack) + exc._hop_name = self._HOP_NAME # pyrefly: ignore[missing-attribute] + raise exc.with_traceback(e.__traceback__) from None + + wrapped_call_function._hop_wrapped = True # pyrefly: ignore[missing-attribute] + cls.call_function = wrapped_call_function + return cls + + +class TorchHigherOrderOperatorVariable(VariableTracker): + # Subclasses should set _HOP_NAME to enable automatic HOP context in error messages + _HOP_NAME: str | None = None + # Set to False for HOPs that hard error on graph break (e.g., cond, map, scan); otherwise + # HOPs will fall back to eager. + _ALLOW_FALLBACK_TO_EAGER: bool = True + + def __init_subclass__(cls, **kwargs: Any) -> None: + super().__init_subclass__(**kwargs) + add_hop_context(cls) + + def __init__( + self, value: HigherOrderOperator, source: Source | None = None, **kwargs: Any + ) -> None: + super().__init__(**kwargs) + self.value = value + self.source = source + + @staticmethod + def make( + value: HigherOrderOperator, source: Source | None = None, **kwargs: Any + ) -> "TorchHigherOrderOperatorVariable": + variable_class = _hop_name_to_variable_class.get(value.__name__) + if variable_class is not None: + return variable_class(value, source, **kwargs) + + from torch._higher_order_ops import BaseHOP + + if isinstance(value, BaseHOP): + return BaseHOPVariable(value, source, **kwargs) + unimplemented( + gb_type="unsupported HigherOrderOperator", + context=str(value), + explanation=f"Unable to create higher order operator variable for {value.__name__}.", + hints=[ + *graph_break_hints.DYNAMO_BUG, + ], + ) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .torch_function import can_dispatch_torch_function, dispatch_torch_function + + if can_dispatch_torch_function(tx, args, kwargs): + return dispatch_torch_function(tx, self, args, kwargs) + + return self._call_function(tx, args, kwargs) + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + unimplemented( + gb_type="unsupported HigherOrderOperator function call", + context=str(self.value), + explanation=f"Unable to trace calling higher order operator variable for {self.value.__name__}.", + hints=[ + *graph_break_hints.DYNAMO_BUG, + ], + ) + + def as_python_constant(self) -> HigherOrderOperator: + return self.value + + def is_python_hashable(self) -> bool: + return True + + def get_python_hash(self) -> int: + return hash(self.as_python_constant()) + + def is_python_equal(self, other: object) -> bool: + return ( + isinstance(other, VariableTracker) + and self.as_python_constant() == other.as_python_constant() + ) + + +class CustomFunctionHigherOrderOperatorVariable(TorchHigherOrderOperatorVariable): + """ + Wraps torch._functorch.autograd_function.custom_function_call + """ + + _HOP_NAME = "torch.ops.higher_order.custom_function_call" + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + assert self.source is not None + return torch._dynamo.variables.UserMethodVariable( + self.value.__call__.__func__, + torch._dynamo.variables.UserDefinedObjectVariable( + self.value, source=self.source + ), + source=AttrSource(self.source, "__call__"), + ).call_function(tx, args, kwargs) + + +class CondHigherOrderVariable(TorchHigherOrderOperatorVariable): + _HOP_NAME = "torch.cond" + _ALLOW_FALLBACK_TO_EAGER = False + supports_input_mutation = False + supports_aliasing = False + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from . import ListVariable + + self.supports_input_mutation = not torch.is_grad_enabled() + self.supports_aliasing = not torch.is_grad_enabled() + + args, kwargs = LazyVariableTracker.realize_all((args, kwargs)) + + for i, k in enumerate(["pred", "true_fn", "false_fn", "operands"]): + if v := kwargs.pop(k, None): + assert i == len(args), ( + "did not provide the right number of non-keyword args" + ) + args.append(v) + + # TODO(voz): Support fake tensor dispatch for recursive + # ops - see torch/dispatch/_dispatcher.py + if len(args) != 4 or kwargs: + unimplemented( + gb_type="torch.cond: improper args/kwargs", + context=f"args: {args}, kwargs: {kwargs}", + explanation=f"torch.cond expects 4 positional arguments (got {len(args)}) " + f"and no keyword arguments (got {len(kwargs)}) " + "Usage: cond(pred, cond_fn, body_fn, operands)", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + + # Specialize into one of the branches since pred is constant + pred, true_fn, false_fn, operands = args + if type(args[0]) is ConstantVariable: + warnings.warn( + "Pred is a Python constant. When used with torch.cond, it specializes on one of the branches." + " If you want torch.cond to preserve two branches, please make the predicate a boolean tensor or a SymBool.", + UserWarning, + ) + if pred.as_python_constant(): + return true_fn.call_function(tx, operands.unpack_var_sequence(tx), {}) + else: + return false_fn.call_function(tx, operands.unpack_var_sequence(tx), {}) + + # predicate + if type(pred.realize()) not in ( + ConstantVariable, + TensorVariable, + SymNodeVariable, + ): + unimplemented( + gb_type="torch.cond: improper predicate", + context=str(pred), + explanation="Expected `pred` to be a bool or a boolean tensor with a single item " + f"but got {str(type(pred))} with original python type {str(pred.python_type())}.", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + + # operands + if not isinstance(operands, (ListVariable, TupleVariable)): + unimplemented( + gb_type="torch.cond: improper operands", + context=str(operands), + explanation="Expected `operands` to be a list/tuple " + f"but got {operands.python_type()}.", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + + operands_seq = operands.unpack_var_sequence(tx) + if not only_consist_of( + operands, (TensorVariable, ConstantVariable, SymNodeVariable) + ): + unimplemented( + gb_type="torch.cond: improper operands contents", + context=str(operands), + explanation="Expected `operands` to be a list/tuple of pytrees that only consists of tensor leaves.", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + + # branches + _check_supported_callable_arg(tx, true_fn, "true_fn") + _check_supported_callable_arg(tx, false_fn, "false_fn") + + # Our strategy for tracing the true/false branches of cond + # are to checkpoint our graphstate, run the true branch, + # roll it back to the checkpoint, and run the false + # branch, and then merge the graphstates. Well, perhaps + # "merge" is too strong a word: we mostly assert that + # the resulting graphstates have to be the same. + # + # We only permit guards to diverge (we union the guards from + # both branches). In particular, this means that side + # effects are NOT permitted inside true/false branches; this + # would be difficult to implement, because of the path + # explosion problem. + + def speculate_branch( + branch: bool, + ) -> tuple[VariableTracker, OutputSpec, torch.fx.Graph, dict[Proxy, Proxy]]: + # NB: 0 is predicate + ix = 1 if branch else 2 + assert self._HOP_NAME is not None + # TODO: Support kwargs + ( + (ret_val, ret_spec), + ret_graph, + ret_lifted_freevars, + ) = speculate_subgraph( + tx, + args[ix], + operands_seq, + {}, + self._HOP_NAME, + source_target=self.value, + should_flatten_outputs=True, + # TODO - removing consts from control flow ops need more work + remove_consts_from_outputs=False, + supports_input_mutation=self.supports_input_mutation, + supports_aliasing=self.supports_aliasing, + ) + + # need to ensure we increase epoch so we don't memoize unbacked bindings + # across different subgraphs which can interfere with runtime assertion + # generation. + assert tx.fake_mode is not None + tx.fake_mode.epoch += 1 + + if not only_consist_of(ret_val, (TensorVariable, ConstantVariable)): + unimplemented( + gb_type="torch.cond: unsupported branch return type", + context=str(ret_val), + explanation="Expected branches to return a possibly nested pytree of tensors or constant ints.", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + for ret in ret_val.unpack_var_sequence(tx): + if ret.is_python_constant() and not isinstance( + ret.as_python_constant(), int + ): + unimplemented( + gb_type="torch.cond: unsupported branch return type (constant non-int)", + context=str(ret_val), + explanation="Constants returned from branches must be ints.", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + return ret_val, ret_spec, ret_graph, ret_lifted_freevars + + (true_r, true_spec, true_graph, true_lifted_freevars) = speculate_branch(True) + true_nn_modules = dict(tx.output.nn_modules) + + ( + false_r, + false_spec, + false_graph, + false_lifted_freevars, + ) = speculate_branch(False) + false_nn_modules = dict(tx.output.nn_modules) + + same_spec = _make_inlined(tx, pytree.TreeSpec.__eq__)( + true_spec.treespec, false_spec.treespec + ).as_python_constant() + # 3.14: NotImplemented cannot be converted to bool + if same_spec is not NotImplemented and not same_spec: + unimplemented( + gb_type="torch.cond: differing branch outputs", + context=f"true_spec: {true_spec.treespec}, false_spec: {false_spec.treespec}, same_spec: {same_spec}", + explanation="Expected branches to return the same pytree structure.", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + + ( + true_graph, + false_graph, + true_shared, + _false_shared, + unique_true, + unique_false, + ) = _merge_graph_inputs( + true_graph, + true_lifted_freevars, + "true_branch", + false_graph, + false_lifted_freevars, + "false_branch", + ) + + true_name = tx.output.install_subgraph( + "cond_true", + torch.fx.GraphModule(true_nn_modules, true_graph), + ) + false_name = tx.output.install_subgraph( + "cond_false", + torch.fx.GraphModule(false_nn_modules, false_graph), + ) + + true_node = make_attr(tx, true_name) + false_node = make_attr(tx, false_name) + + p_args = ( + pred.as_proxy(), + true_node, + false_node, + # We pick true_shared but it shouldn't matter + tuple(true_shared + unique_true + unique_false), + ) + + return _call_function_and_unflatten_output( + tx, + torch.ops.higher_order.cond, + p_args, + {}, + None, + true_spec, + true_r, + ) + + +class CallTorchbindHigherOrderVariable(TorchHigherOrderOperatorVariable): + _HOP_NAME = "torch.ops.higher_order.call_torchbind" + + def __init__( + self, + hop: HigherOrderOperator, + source: Source | None, + script_obj_var: Any, + method_name: str, + ) -> None: + super().__init__(hop, source) + self.script_obj_var = script_obj_var + self.method_name = method_name + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .builder import wrap_fx_proxy + + args, kwargs = LazyVariableTracker.realize_all((args, kwargs)) + + args_proxy = [arg.as_proxy() for arg in args] + kwargs_proxy = {k: v.as_proxy() for k, v in kwargs.items()} + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + self.value, + args=tuple( + [self.script_obj_var.as_proxy(), self.method_name] + args_proxy + ), + kwargs=kwargs_proxy, + ), + ) + + +def validate_subgraph_output_types( + output: VariableTracker | Sequence[VariableTracker], +) -> None: + """Verify that that the output of the subgraph is a tensor, + int, bool, SymBool, or SymInt. + """ + from . import TensorVariable + + if non_tensor_output := find_mismatched_vars( + output, TensorVariable, allow_none=True + ): + for out in non_tensor_output: + if ( + (isinstance(out, SymNodeVariable) and out.python_type() in (int, bool)) + or ( + out.is_python_constant() + and isinstance(out.as_python_constant(), (int, bool)) + ) + or isinstance(out, TorchScriptObjectVariable) + ): + continue + unimplemented( + gb_type="HOP body output unsupported", + context=f"non-tensor outputs: {non_tensor_output}", + explanation="HigherOrderOperator body's output must consist of tensors or ints/bools only " + f"but got {out.python_type()}.", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + + +class WhileLoopHigherOrderVariable(TorchHigherOrderOperatorVariable): + _HOP_NAME = "torch.while_loop" + _ALLOW_FALLBACK_TO_EAGER = False + supports_input_mutation = False + supports_aliasing = False + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + assert self._HOP_NAME is not None + return _call_while_loop( + self, tx, args, kwargs, stack_output=False, hop_name=self._HOP_NAME + ) + + +class WhileLoopStackOutputHigherOrderVariable(TorchHigherOrderOperatorVariable): + _HOP_NAME = "torch.while_loop" + _ALLOW_FALLBACK_TO_EAGER = False + supports_input_mutation = False + supports_aliasing = False + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + assert self._HOP_NAME is not None + return _call_while_loop( + self, tx, args, kwargs, stack_output=True, hop_name=self._HOP_NAME + ) + + +class AssociativeScanHigherOrderVariable(TorchHigherOrderOperatorVariable): + _HOP_NAME = "torch.ops.higher_order.associative_scan" + _ALLOW_FALLBACK_TO_EAGER = False + supports_input_mutation = False + supports_aliasing = False + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from torch._higher_order_ops.utils import first_slice_copy + + args, kwargs = LazyVariableTracker.realize_all((args, kwargs)) + + def arg_extractor( + combine_fn: VariableTracker, + xs: VariableTracker, + additional_inputs: VariableTracker, + ) -> tuple[VariableTracker, VariableTracker, VariableTracker]: + return combine_fn, xs, additional_inputs + + combine_fn, xs, additional_inputs = arg_extractor(*args, **kwargs) + + if args[0].python_type() is functools.partial: + # This is the standard case when the user calls the frontend + # and the frontend invokes dynamo + if len(args) != 2: + unimplemented( + gb_type="torch.associative_scan: improper args", + context=f"args: {args}", + explanation=f"torch.associative_scan expects 2 positional arguments (got {len(args)}) " + "Usage: associative_scan(combine_fn, xs)", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + + xs_treespec = args[0].keywords["spec"] + + # combine_fn input check + # We need to get the pure combine_fn from the functools.partial + _check_supported_callable_arg( + tx, + combine_fn.keywords["combine_fn"], # type: ignore[attr-defined] + "combine_fn", + ) + else: + # This case is hit during re-tracing, for example in export tests + # In this case, the combine_fn is a callable and not a functools.partial + xs_treespec = _make_inlined(tx, pytree.tree_structure)(xs) + + _check_supported_callable_arg(tx, combine_fn, "combine_fn") + + # xs input check + if not isinstance(xs, (ListVariable, TupleVariable)): + unimplemented( + gb_type="torch.associative_scan: improper xs", + context=str(xs), + explanation=f"Expected xs to be a list/tuple but got {xs.python_type()}", + hints=[ + *graph_break_hints.DYNAMO_BUG, + ], + ) + xs_vars = xs.unpack_var_sequence(tx) + _check_all_tensorvariable(xs_vars) + + # additional_inputs input check + if not isinstance(additional_inputs, (ListVariable, TupleVariable)): + unimplemented( + gb_type="torch.associative_scan: improper additional_inputs", + context=str(additional_inputs), + explanation=f"Expected additional_inputs to be a list/tuple but got {additional_inputs.python_type()}", + hints=[ + *graph_break_hints.DYNAMO_BUG, + ], + ) + additional_inputs_vars = additional_inputs.unpack_var_sequence(tx) + _check_all_tensorvariable(additional_inputs_vars) + + scan_length = get_fake_value(xs_vars[0].as_proxy().node, tx).size()[0] + if scan_length == 0: + unimplemented( + gb_type="torch.associative_scan: zero-sized tensor", + context=str(xs_vars[0]), + explanation="associative_scan() operator doesn't support zero-sized tensors during tracing.", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + + # Trace the subgraph + # The sub_args is a slice of original input, e.g. if input.size is (3, 4), and scan dim=0 + # the sub_args shape will be (4, ). + with discard_graph_changes(tx): + sub_args = [ + _make_inlined(tx, first_slice_copy)(leaf) + for leaf in itertools.chain(xs_vars, xs_vars) + ] + sub_args_additional_inputs = [ + t.call_method(tx, "clone", args=[], kwargs={}) + for t in additional_inputs_vars + ] + + sub_args = sub_args + sub_args_additional_inputs + assert self._HOP_NAME is not None + ( + (combine_result, _combine_spec), + combine_graph, + combine_lifted_freevars, + ) = speculate_subgraph( + tx, + combine_fn, + sub_args, + sub_kwargs={}, + description=self._HOP_NAME, + source_target=self.value, + set_subgraph_inputs="flatten_manual", + supports_input_mutation=self.supports_input_mutation, + supports_aliasing=self.supports_aliasing, + ) + + # Ensure that the output of scan is a flattened list of elements, + # because downstream operations assume that the output of HOPs + # is flattened + output_node = combine_graph.find_nodes(op="output")[0] + output_node.args = (pytree.tree_leaves(output_node.args),) + combine_graph.lint() + + # Collect the results from the combine_fn + results, _combine_treespec = _make_inlined(tx, pytree.tree_flatten)( + combine_result + ).unpack_var_sequence(tx) + + # Check whether the combine_fn returns one child tree for the output. + if _combine_treespec.as_python_constant().num_leaves < 1: + unimplemented( + gb_type="torch.associative_scan: combine_fn improper number of leaves", + context=str(_combine_treespec.as_python_constant()), + explanation="combine_fn needs to produce one pytree for the output " + f"but combine_fn produces the pytree {_combine_treespec.as_python_constant()}.", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + + # Check whether the outs produced by combine_fn has the same treespec as xs + # We need to have this check this way, because in case init is a TreeSpec and carry + # but carry is only a LeafSpec, these two cannot be compared correctly. + if ( + xs_treespec.as_python_constant().is_leaf() + != _combine_treespec.as_python_constant().is_leaf() + ) or not _make_inlined(tx, pytree.TreeSpec.__eq__)( + xs_treespec, _combine_treespec + ).as_python_constant(): + unimplemented( + gb_type="torch.associative_scan: mismatched input/output tree structure", + context=f"xs: {xs_treespec.as_python_constant()}, output: {_combine_treespec.as_python_constant()}", + explanation="The tree structure of the xs and the outs of the combine_fn are are expected to be identical, but got " + f"xs: {xs_treespec.as_python_constant()} vs output: {_combine_treespec.as_python_constant()}.", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + + # We set include contiguity=False because we have vmap x HOP tests, where if + # include_contiguity=True will call t.is_contiguous inside of vmap and get an error + # "querying is_contiguous inside of vmap for memory_format other than + # torch.contiguous_format is not yet implemented". This is okay because stride + # is still checked. + check_meta_consistency_vt( + [_make_inlined(tx, first_slice_copy)(t) for t in xs_vars], + results.items, # type: ignore[attr-defined] + "initial_xs", + "combine_fn_output", + include_contiguity=False, + ) + + combine_gm = torch.fx.GraphModule(dict(tx.output.nn_modules), combine_graph) + combine_freevars_proxy = tuple(combine_lifted_freevars.keys()) + + # Compute the proxies for the input check + proxy_vars_inputcheck = ( + tuple(sarg.as_proxy() for sarg in sub_args) + combine_freevars_proxy + ) + + from torch._higher_order_ops.utils import _maybe_fake_tracing + from torch._inductor.utils import is_pointwise_use + + assert tx.fake_mode is not None + with tx.fake_mode: + sub_args_fake = [ + ( + leaf.node.meta["example_value"].clone() + if hasattr(leaf.node.meta["example_value"], "clone") + else leaf.node.meta["example_value"] + ) + for leaf in pytree.tree_leaves(proxy_vars_inputcheck) + ] + pre_dispatch = False + + fx = _maybe_fake_tracing( + combine_gm, sub_args_fake, pre_dispatch=pre_dispatch + ) + + for node in fx.graph.nodes: + # Check that the combine_fn is pointwise, if combine_mode='pointwise' + if not all( + is_pointwise_use(use) or use.op == "output" for use in node.users + ): + raise RuntimeError( + "For combine_mode='pointwise', the combine_fn needs to be pointwise" + ) + + combine_fn_name = tx.output.install_subgraph( + "associative_scan_combine_fn", combine_gm + ) + + # Compute the proxies + xs_proxy = xs.as_proxy() + combine_freevars_proxy = tuple(combine_lifted_freevars.keys()) + additional_inputs_proxy = additional_inputs.as_proxy() + combine_freevars_proxy + + p_args = ( + make_attr(tx, combine_fn_name), + xs_proxy, + additional_inputs_proxy, + ) + + return _call_function_and_unflatten_output( + tx, + torch.ops.higher_order.associative_scan, + p_args, + {}, + None, + OutputSpec(xs_treespec), # type: ignore[arg-type] + None, + ) + + +class ScanHigherOrderVariable(TorchHigherOrderOperatorVariable): + _HOP_NAME = "torch.ops.higher_order.scan" + _ALLOW_FALLBACK_TO_EAGER = False + supports_input_mutation = False + supports_aliasing = False + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from torch._higher_order_ops.scan import _extract_carry_and_out + from torch._higher_order_ops.utils import first_slice_copy + + args, kwargs = LazyVariableTracker.realize_all((args, kwargs)) + + # combine_fn input check + def _check_combine_fn_is_normalized(combine_fn_var: VariableTracker) -> bool: + if not isinstance( + combine_fn_var, + ( + variables.nn_module.NNModuleVariable, + variables.nn_module.UnspecializedNNModuleVariable, + variables.FunctoolsPartialVariable, + ), + ): + unimplemented( + gb_type="torch.scan: improper combine_fn", + context=str(combine_fn_var), + explanation="Expected combine_fn to be wrapped as functools.partial in scan user-facing api " + f"or a graph module if we're re-exporting but got {combine_fn_var.python_type()}.", + hints=[ + *graph_break_hints.DIFFICULT, + ], + ) + return isinstance( + combine_fn_var, + ( + variables.nn_module.NNModuleVariable, + variables.nn_module.UnspecializedNNModuleVariable, + ), + ) + + def arg_extractor( + combine_fn: VariableTracker, + init: VariableTracker, + xs: VariableTracker, + additional_inputs: VariableTracker, + ) -> tuple[VariableTracker, VariableTracker, VariableTracker, VariableTracker]: + return combine_fn, init, xs, additional_inputs + + combine_fn, init, xs, additional_inputs = arg_extractor(*args, **kwargs) + init_vars = init.unpack_var_sequence(tx) + xs_vars = xs.unpack_var_sequence(tx) + additional_inputs_vars = additional_inputs.unpack_var_sequence(tx) + + # combine_fn input check + combine_fn_is_normalized = _check_combine_fn_is_normalized(combine_fn) + if combine_fn_is_normalized: + combine_gm = combine_fn.value # type: ignore[attr-defined] + assert isinstance(combine_gm, torch.fx.GraphModule), ( + combine_fn, + combine_gm, + ) + else: + # combine_fn input check + # We need to get the pure combine_fn from the functools.partial + _check_supported_callable_arg( + tx, + combine_fn.keywords["combine_fn"], # type: ignore[attr-defined] + "combine_fn", + ) + # xs input check + if not isinstance(xs, (ListVariable, TupleVariable)): + unimplemented( + gb_type="torch.scan: improper xs", + context=str(xs), + explanation=f"Expected xs to be a list/tuple but got {xs.python_type()}", + hints=[ + *graph_break_hints.DYNAMO_BUG, + ], + ) + # init input check + if not isinstance(init, (ListVariable, TupleVariable)): + unimplemented( + gb_type="torch.scan: improper init", + context=str(init), + explanation=f"Expected init to be a list/tuple with at least one element but got {init.python_type()}", + hints=[ + *graph_break_hints.DYNAMO_BUG, + ], + ) + + if len(init_vars) == 0: + unimplemented( + gb_type="torch.scan: no init leaves", + context="", + explanation="Expected init leaves.", + hints=[ + *graph_break_hints.DYNAMO_BUG, + ], + ) + + # additional_inputs input check + if not isinstance(additional_inputs, (ListVariable, TupleVariable)): + unimplemented( + gb_type="torch.scan: improper additional_inputs", + context=str(additional_inputs), + explanation=f"Expected additional_inputs to be a list/tuple but got {additional_inputs.python_type()}", + hints=[ + *graph_break_hints.DYNAMO_BUG, + ], + ) + # scan_length check + scan_length = get_fake_value(xs_vars[0].as_proxy().node, tx).size()[0] + if scan_length == 0: + unimplemented( + gb_type="torch.scan: zero-sized tensor", + context=str(xs_vars[0]), + explanation="associative_scan() operator doesn't support zero-sized tensors during tracing.", + hints=[ + *graph_break_hints.USER_ERROR, + *graph_break_hints.SUPPORTABLE, + ], + ) + _check_all_tensorvariable(init_vars) + _check_all_tensorvariable(xs_vars) + _check_all_tensorvariable(additional_inputs_vars) + + with discard_graph_changes(tx): + sub_args_init = [ + ini.call_method(tx, "clone", args=[], kwargs={}) for ini in init_vars + ] + # The sub_args_inp is a slice of original input, e.g. if input.size is (3, 4), and scan dim=0 + # the sub_args_inp shape will be (4, ). + sub_args_inp = [_make_inlined(tx, first_slice_copy)(inp) for inp in xs_vars] + sub_args_additional_inputs = [ + t.call_method(tx, "clone", args=[], kwargs={}) + for t in additional_inputs_vars + ] + + sub_args = sub_args_init + sub_args_inp + sub_args_additional_inputs + assert self._HOP_NAME is not None + ( + (combine_result, _combine_spec), + combine_graph, + combine_lifted_freevars, + ) = speculate_subgraph( + tx, + combine_fn, + sub_args, + sub_kwargs={}, + description=self._HOP_NAME, + source_target=self.value, + set_subgraph_inputs="flatten_manual", + supports_input_mutation=self.supports_input_mutation, + supports_aliasing=self.supports_aliasing, + ) + + # Ensure that the output of scan is a flattened list of elements, + # because downstream operations assume that the output of HOPs + # is flattened + output_node = combine_graph.find_nodes(op="output")[0] + output_node.args = (pytree.tree_leaves(output_node.args),) + combine_graph.lint() + combine_freevars_proxy = list(combine_lifted_freevars.keys()) + combine_result_vars = combine_result.unpack_var_sequence(tx) + + if combine_fn_is_normalized: + carry_vars, out_vars = _extract_carry_and_out( + combine_result_vars, len(init_vars) + ) + else: + if len(combine_result_vars) != 2: + unimplemented( + gb_type="torch.scan: improper combine_fn number of returns", + context=str(combine_result_vars), + explanation=f"Expect combine_fn to return a tuple (next_carry, y) but got {combine_result_vars}.", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + carry_tree, out_vars = combine_result_vars + carry_vars, _ = _make_inlined(tx, pytree.tree_flatten)( + carry_tree + ).unpack_var_sequence(tx) + carry_vars = carry_vars.unpack_var_sequence(tx) + out_vars = _make_inlined(tx, pytree.tree_leaves)( + out_vars + ).unpack_var_sequence(tx) + + # additional output checking + _combine_spec = OutputSpec( + _make_inlined(tx, pytree.tree_structure)(combine_result) # type: ignore[arg-type] + ) + + check_meta_consistency_vt( + init_vars, + carry_vars, + "init", + "carry", + ) + + # Check meta data of carries and inits. If we pass this stage, we are sure that the init and carries + # have the same tree structure. + # We set include contiguity=False because we have vmap x HOP tests, where if + # include_contiguity=True will call t.is_contiguous inside of vmap and get an error + # "querying is_contiguous inside of vmap for memory_format other than + # torch.contiguous_format is not yet implemented". This is okay because stride + # is still checked. + check_meta_consistency_vt( + init_vars, + carry_vars, + "init", + "carry", + include_contiguity=False, + ) + + xs_proxy = xs.as_proxy() + init_proxy = init.as_proxy() + additional_inputs_proxy = list(additional_inputs.as_proxy()) + list( + combine_freevars_proxy + ) + + combine_gm = torch.fx.GraphModule(dict(tx.output.nn_modules), combine_graph) + combine_fn_name = tx.output.install_subgraph("scan_combine_fn", combine_gm) + + p_args = ( + make_attr(tx, combine_fn_name), + init_proxy, + xs_proxy, + additional_inputs_proxy, + ) + + return _call_function_and_unflatten_output( + tx, + torch.ops.higher_order.scan, + p_args, + {}, + None, + _combine_spec, + None, + ) + + +class MapHigherOrderVariable(TorchHigherOrderOperatorVariable): + _HOP_NAME = "torch.ops.higher_order.map_impl" + _ALLOW_FALLBACK_TO_EAGER = False + supports_input_mutation = False + supports_aliasing = False + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + args, kwargs = LazyVariableTracker.realize_all((args, kwargs)) + + if len(kwargs) > 0: + unimplemented( + gb_type="torch.map: kwargs not supported", + context=f"args: {args}, kwargs: {kwargs}", + explanation=f"torch.map expects no keyword arguments (got {len(kwargs)})", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + + _check_supported_callable_arg(tx, args[0], "map_fn") + + # args = f, flat_xs, flat_args + assert isinstance(args[1], (ListVariable, TupleVariable)), args[1] + assert isinstance(args[2], (ListVariable, TupleVariable)), args[2] + unpacked_xs = args[1].unpack_var_sequence(tx) + unpacked_args = args[2].unpack_var_sequence(tx) + + sample_shape = get_fake_value(unpacked_xs[0].as_proxy().node, tx).size() + + if len(sample_shape) < 1 or sample_shape[0] == 0: + unimplemented( + gb_type="torch.map: improper inputs", + context=str(sample_shape), + explanation="torch.map doesn't support scalar or non-zero sized tensors during tracing.", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + + # To get the example output from map() we will need to provide at least one sample to + # the loop body. In our case we will always use xs[0], and our map() won't support zero + # sized tensor during tracing. + with discard_graph_changes(tx): + sliced_xs = [ + xs.call_method( + tx, + "select", + args=[VariableTracker.build(tx, 0), VariableTracker.build(tx, 0)], + kwargs={}, + ) + for xs in unpacked_xs + ] + assert self._HOP_NAME is not None + + # TODO: Support kwargs + ( + (body_r, body_spec), + body_graph, + body_lifted_freevars, + ) = speculate_subgraph( + tx, + args[0], + [ + *sliced_xs, + *unpacked_args, + ], + {}, + self._HOP_NAME, + source_target=self.value, + set_subgraph_inputs="flatten_manual", + should_flatten_outputs=True, + # TODO - removing consts from control flow ops need more work + remove_consts_from_outputs=False, + supports_input_mutation=self.supports_input_mutation, + supports_aliasing=self.supports_aliasing, + ) + + # Check all outputs of map are tensors. + # For map, outputting None is OK, thus ignore None values in the check + body_r_vars = body_r.unpack_var_sequence(tx) + none_mask = [x.is_constant_none() for x in body_r_vars] + _check_all_tensorvariable( + [br for bm, br in zip(none_mask, body_r_vars) if not bm] + ) + + body_nn_modules = dict(tx.output.nn_modules) + + body_name = tx.output.install_subgraph( + "map_body", + torch.fx.GraphModule(body_nn_modules, body_graph), + ) + + body_node = make_attr(tx, body_name) + + p_args = ( + body_node, + [xs.as_proxy() for xs in unpacked_xs], + [arg.as_proxy() for arg in unpacked_args] + + list(body_lifted_freevars.keys()), + ) + + return _call_function_and_unflatten_output( + tx, torch.ops.higher_order.map_impl, p_args, {}, None, body_spec, body_r + ) + + +class PrintHigherOrderVariable(TorchHigherOrderOperatorVariable): + _HOP_NAME = "torch.ops.higher_order.print" + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .builder import wrap_fx_proxy + + args, kwargs = LazyVariableTracker.realize_all((args, kwargs)) + + args_proxy = [arg.as_proxy() for arg in args] + kwargs_proxy = {k: v.as_proxy() for k, v in kwargs.items()} + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + self.value, + args=tuple(args_proxy), + kwargs=kwargs_proxy, + ), + ) + + +class ExecutorchCallDelegateHigherOrderVariable(TorchHigherOrderOperatorVariable): + _HOP_NAME = "torch.ops.higher_order.executorch_call_delegate" + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .builder import wrap_fx_proxy + + # This is operator for delegation within Executorch which calls a + # specific function in the given lowered module with the given + # operators. The actual operator is defined in the Executorch codebase. + # This is a bad hierarchical violation since + # executorch_call_delegate sits at a higher level than dynamo, but + # there's no real solution to this issue yet. + if len(kwargs) > 0: + unimplemented( + gb_type="executorch_call_delegate: kwargs not supported", + context=f"args: {args}, kwargs: {kwargs}", + explanation=f"executorch_call_delegate expects no keyword arguments (got {len(kwargs)})", + hints=[], + ) + lowered_module, lowered_node = None, None + if isinstance(args[0], variables.NNModuleVariable): + lowered_module = tx.output.get_submodule(args[0].module_key) + lowered_node = make_attr(tx, args[0].module_key) + elif isinstance(args[0], variables.UnspecializedNNModuleVariable): + # This nn module is special sa delegated by executorch. Just + # install it as a attr in the graph. + lowered_module = args[0].value + lowered_node = tx.output.register_static_attr_and_return_proxy( + "delegate", lowered_module + ) + else: + unimplemented( + gb_type="executorch_call_delegate: first arg not supported", + context=f"args: {args}, kwargs: {kwargs}", + explanation=f"executorch_call_delegate expects the first argument to be a nn.Module (got {args[0]})", + hints=[], + ) + + p_args = tuple(arg.as_proxy() for arg in args[1:]) + real_sub_args = pytree.tree_map_only( + torch.fx.Proxy, lambda a: get_fake_value(a.node, tx), p_args + ) + assert tx.fake_mode is not None + with tx.fake_mode: + example_value = lowered_module.original_module.module()(*real_sub_args) # type: ignore[attr-defined] + + # NOTE [Guaranteeing the 1-1 correspondence of FakeTensors and real tensors]: + # executorch modules promise not to alias inputs and outputs. + # Thus, output FakeTensors will correctly not alias input FakeTensors. + _assert_tensors_nonaliasing(real_sub_args, example_value) + + p_args = (lowered_node,) + p_args + + # Store the invocation as a call + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + self.value, + args=tuple(p_args), + kwargs={}, + ), + example_value=example_value, + ) + + +class FunctorchHigherOrderVariable(UserFunctionVariable): + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + return super().call_function(tx, args, kwargs) + + def should_allow_nested_graph_breaks(self) -> bool: + return False + + +class FunctionalCallVariable(FunctorchHigherOrderVariable): + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + return super().call_function(tx, args, kwargs) + + +class ReparametrizeModuleCallVariable(FunctorchHigherOrderVariable): + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + ctx_manager_vt = super().call_function(tx, args, kwargs) + return RepararametrizeModuleContextVariable(ctx_manager_vt, args[0]) # type: ignore[arg-type] + + +class WrapHigherOrderVariable(TorchHigherOrderOperatorVariable): + _HOP_NAME = "torch.ops.higher_order.wrap" + supports_input_mutation = True + supports_aliasing = True + allow_side_effects = False + + def install_subgraph_in_output_graph( + self, + tx: "InstructionTranslator", + fn_vt: VariableTracker, + fn_args_vt: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + body_gmod: GraphModule, + attr_name: str = "wrap_body", + ) -> str: + return tx.output.install_subgraph( + f"{attr_name}", + body_gmod, + ) + + def create_wrapped_node( + self, + tx: "InstructionTranslator", + fn_vt: VariableTracker, + fn_args_vt: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + description: str, + *, + subgraph_name: str = "wrap_body", + ) -> tuple[ + tuple[Proxy, ...], + dict[str, VariableTracker], + Any, + VariableTracker, + GraphModule, + str, + VariableTracker | tuple[VariableTracker, ...], + SubgraphTracingInfo, + ]: + # See NOTE [HigherOrderOperator tracing design] for more details + ( + body_r, + body_graph, + body_lifted_freevars, + body_graph_output_vts, + tracing_info, + ) = speculate_subgraph_with_auto_output_flattening( + tx, + fn_vt, + fn_args_vt, + kwargs, + description, + source_target=self.value, + allow_side_effects=self.allow_side_effects, + filter_aliased_intermediates=getattr( + self, "filter_aliased_intermediates", False + ), + supports_input_mutation=self.supports_input_mutation, + supports_aliasing=self.supports_aliasing, + ) + + body_gmod = torch.fx.GraphModule(tx.output.nn_modules, body_graph) + body_name = self.install_subgraph_in_output_graph( + tx, + fn_vt, + fn_args_vt, + kwargs, + body_gmod, + attr_name=subgraph_name, + ) + body_node = make_attr(tx, body_name) + + # Since, we call `speculate_subgraph` with `set_subgraph_inputs="automatic`, + # all the arguments are lifted. + lifted_args = tuple(arg for arg in body_lifted_freevars) + + proxy_args = (body_node,) + lifted_args + + example_value = pytree.tree_map_only( + torch.fx.Node, + lambda a: a.meta["example_value"], + body_graph.find_nodes(op="output")[0].args[0], + ) + + return ( + proxy_args, + {}, + example_value, + body_r, + body_gmod, + body_name, + body_graph_output_vts, + tracing_info, + ) + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + # This flattens the kwargs into lifted args + ( + p_args, + p_kwargs, + _example_value, + body_r, + _, + _, + body_graph_output_vts, + _, + ) = self.create_wrapped_node(tx, args[0], args[1:], kwargs, "wrap") + + if len(p_kwargs) > 0: + unimplemented( + gb_type="WrapHigherOrderVariable: kwargs unexpected", + context=f"args: {args}, kwargs: {kwargs}", + explanation="kwargs should have been flattened into lifted args.", + hints=[ + *graph_break_hints.DYNAMO_BUG, + ], + ) + + return _call_function_with_auto_output_flattening( # type: ignore[return-value] + tx, + self.value, + tuple(p_args), + p_kwargs, + _example_value, + body_r, + body_graph_output_vts, + ) + + +class WrapWithSetGradEnabledHigherOrderVariable(TorchHigherOrderOperatorVariable): + """ + This hop is not exposed to users but is inserted into the graph + after export as a post-processing step. + """ + + _HOP_NAME = "torch.ops.higher_order.wrap_with_set_grad_enabled" + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + args, kwargs = LazyVariableTracker.realize_all((args, kwargs)) + + if kwargs: + unimplemented( + gb_type="wrap_with_set_grad_enabled: unexpected kwargs", + context=f"args: {args}, kwargs: {kwargs}", + explanation=f"wrap_with_set_grad_enabled expects no keyword arguments (got {len(kwargs)}).", + hints=[ + *graph_break_hints.DYNAMO_BUG, + ], + ) + + grad_enabled, fn_var, *rest_args = args + + if not grad_enabled.is_python_constant(): + unimplemented( + gb_type="wrap_with_set_grad_enabled: non-constant grad_enabled", + context=str(grad_enabled), + explanation="wrap_with_set_grad_enabled expects grad_enabled argument to be a constant.", + hints=[ + *graph_break_hints.DYNAMO_BUG, + ], + ) + + _check_supported_callable_arg(tx, fn_var, "enable_grad_fn") + + with torch.set_grad_enabled(grad_enabled.as_python_constant()): + assert self._HOP_NAME is not None + ( + (body_r, treespec), + body_graph, + body_lifted_freevars, + ) = speculate_subgraph( + tx, + fn_var, + [*rest_args], + {}, + self._HOP_NAME, + source_target=self.value, + set_subgraph_inputs="manual", + should_flatten_outputs=True, + ) + + if len(body_lifted_freevars) > 0: + unimplemented( + gb_type="wrap_with_set_grad_enabled: unexpected freevars", + context=str(body_lifted_freevars), + explanation="wrap_with_set_grad_enabled expects no freevars.", + hints=[], + ) + + body_gmod = torch.fx.GraphModule(tx.output.nn_modules, body_graph) + body_name = tx.output.install_subgraph( + "wrap_body", + body_gmod, + ) + + body_node = make_attr(tx, body_name) + + proxy_args = tuple( + [ + grad_enabled.as_python_constant(), + body_node, + ] + + [operand.as_proxy() for operand in rest_args] + ) + example_value = pytree.tree_map_only( + torch.fx.Proxy, + lambda a: a.node.meta["example_value"], + body_r.as_proxy(), + ) + return _call_function_and_unflatten_output( + tx, self.value, proxy_args, {}, example_value, treespec, body_r + ) + + +class WrapWithAutocastHigherOrderVariable(TorchHigherOrderOperatorVariable): + """ + This hop is not exposed to users but is inserted into the graph + after export as a post-processing step. + """ + + _HOP_NAME = "torch.ops.higher_order.wrap_with_autocast" + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + args, kwargs = LazyVariableTracker.realize_all((args, kwargs)) + + if kwargs: + unimplemented( + gb_type="wrap_with_autocast: unexpected kwargs", + context=f"args: {args}, kwargs: {kwargs}", + explanation=f"wrap_with_autocast expects no keyword arguments (got {len(kwargs)}).", + hints=[ + *graph_break_hints.DYNAMO_BUG, + ], + ) + + device_type, dtype, enabled, cache_enabled, fn_var, *rest_args = args + + for arg in [device_type, dtype, enabled, cache_enabled]: + if not arg.is_python_constant(): + unimplemented( + gb_type="wrap_with_autocast: expected constant arg", + context=str(args), + explanation="wrap_with_autocast expects device_type, dtype, enabled, " + "and cache_enabled arguments to be constants.", + hints=[ + *graph_break_hints.DYNAMO_BUG, + ], + ) + + _check_supported_callable_arg(tx, fn_var, "autocast") + + python_constants = [ + arg.as_python_constant() + for arg in [device_type, dtype, enabled, cache_enabled] + ] + + with torch.autocast(*python_constants): + assert self._HOP_NAME is not None + ( + (body_r, treespec), + body_graph, + body_lifted_freevars, + ) = speculate_subgraph( + tx, + fn_var, + [*rest_args], + {}, + self._HOP_NAME, + source_target=self.value, + set_subgraph_inputs="manual", + should_flatten_outputs=True, + ) + + if len(body_lifted_freevars) > 0: + unimplemented( + gb_type="wrap_with_autocast: unexpected freevars", + context=str(body_lifted_freevars), + explanation="wrap_with_autocast expects no freevars.", + hints=[], + ) + + body_gmod = torch.fx.GraphModule(tx.output.nn_modules, body_graph) + body_name = tx.output.install_subgraph( + "wrap_body", + body_gmod, + ) + + body_node = make_attr(tx, body_name) + + proxy_args = tuple( + [ + *python_constants, + body_node, + ] + + [operand.as_proxy() for operand in rest_args] + ) + example_value = pytree.tree_map_only( + torch.fx.Proxy, + lambda a: a.node.meta["example_value"], + body_r.as_proxy(), + ) + + return _call_function_and_unflatten_output( + tx, self.value, proxy_args, {}, example_value, treespec, body_r + ) + + +class HintsWrapperHigherOrderVariable(WrapHigherOrderVariable): + _HOP_NAME = "torch.ops.higher_order.hints_wrapper" + _ALLOW_FALLBACK_TO_EAGER = False + + def install_subgraph_in_output_graph( + self, + tx: "InstructionTranslator", + fn_vt: VariableTracker, + fn_args_vt: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + body_gmod: GraphModule, + attr_name: str = "wrap_body", + ) -> str: + return tx.output.install_subgraph( + "hints_wrapper_body", + body_gmod, + ) + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + _check_supported_callable_arg(tx, args[0], "body_fn") + + # inputs + if ( + len(args) != 3 + or not isinstance(args[1], (ListVariable, TupleVariable)) + or not isinstance(args[2], ConstDictVariable) + or len(kwargs) != 1 + or "hints" not in kwargs + ): + unimplemented( + gb_type="hints_wrapper: improper args/kwargs", + context=f"args: {args}, kwargs: {kwargs}", + explanation=f"hints_wrapper expects 3 positional arguments (got {len(args)}) " + f"and 1 keyword argument (got {len(kwargs)}). " + "Usage: hints_wrapper(body_fn, args, kwargs, hints=...). " + "args is expected to be list/tuple and kwargs is expected to be a dict.", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + + operands = args[1].unpack_var_sequence(tx) + fn_kwargs = args[2].as_python_constant() + assert self._HOP_NAME is not None + # Use create_wrapped_node from WrapHigherOrderVariable + ( + p_args, + _, + example_value, + body_r, + body_gmod, + _, + body_graph_output_vts, + _, + ) = self.create_wrapped_node( + tx, + args[0], # function + operands, + fn_kwargs, + self._HOP_NAME, + ) + + # hints_wrapper expects (body_node, args, kwargs) as positional args + # So we need to restructure p_args from (body_node, *lifted_args) + # to (body_node, lifted_args_tuple, {}) + body_node = p_args[0] + lifted_args = p_args[1:] + # pyrefly: ignore [implicit-any] + p_args = (body_node, tuple(lifted_args), {}) + + # add hints into p_kwargs + p_kwargs = {} + p_kwargs["hints"] = kwargs["hints"].as_python_constant() + + return _call_function_with_auto_output_flattening( # type: ignore[return-type] + tx, + self.value, + p_args, + p_kwargs, + example_value, + body_r, + body_graph_output_vts, + ) + + +class OutDtypeHigherOrderVariable(TorchHigherOrderOperatorVariable): + _HOP_NAME = "torch.ops.higher_order.out_dtype" + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .builder import wrap_fx_proxy + + if len(kwargs) > 0: + unimplemented( + gb_type="out_dtype: unexpected kwargs", + context=f"args: {args}, kwargs: {kwargs}", + explanation=f"out_dtype expects no keyword arguments (got {len(kwargs)}).", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + + p_args = tuple(arg.as_proxy() for arg in args) + op = p_args[0] + output_dtype = p_args[1] + fake_sub_args = pytree.tree_map_only( + torch.fx.Proxy, lambda a: a.node.meta["example_value"], p_args[2:] + ) + # This is a simplified implementation of this operator just for tracing. + # Actual implementation may also first promote the arguments + example_value = op(*fake_sub_args).to(dtype=output_dtype) + + # Store the invocation as a call + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + self.value, + args=tuple(p_args), + kwargs={}, + ), + example_value=example_value, + ) + + +class StrictModeHigherOrderVariable(TorchHigherOrderOperatorVariable): + _HOP_NAME = "torch.ops.higher_order.strict_mode" + _ALLOW_FALLBACK_TO_EAGER = False + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + unpacked_sequence = args[1].unpack_var_sequence(tx) + # TODO (tmanlaibaatar) support pytree here + for arg in unpacked_sequence: + if isinstance( + arg, (ListVariable, TupleVariable, ConstDictVariable, SetVariable) + ): + unimplemented( + gb_type="strict_mode: improper args", + context=f"args: {args}, kwargs: {kwargs}", + explanation="strict_mode higher order op expects flat inputs (list/tuple/dict/set)", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + + if kwargs: + unimplemented( + gb_type="strict_mode: unexpected kwargs", + context=f"args: {args}, kwargs: {kwargs}", + explanation=f"strict_mode higher order op expects no keyword arguments (got {len(kwargs)}).", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + + assert self._HOP_NAME is not None + ( + (ret_val, ret_spec), + ret_graph, + ret_lifted_freevars, + ) = speculate_subgraph( + tx, + args[0], + unpacked_sequence, + {}, + self._HOP_NAME, + source_target=self.value, + should_flatten_outputs=True, + ) + + strict_mode_nn_modules = dict(tx.output.nn_modules) + + strict_mode_name = tx.output.install_subgraph( + "strict_mode_body", + torch.fx.GraphModule(strict_mode_nn_modules, ret_graph), + ) + + strict_mode_node = make_attr(tx, strict_mode_name) + p_args = ( + strict_mode_node, + tuple(ret_lifted_freevars.keys()), + ) + + flat_example_value = pytree.tree_map_only( + torch.fx.Proxy, + lambda a: a.node.meta["example_value"], + ret_val.as_proxy(), + ) + + return _call_function_and_unflatten_output( + tx, + torch.ops.higher_order.strict_mode, + p_args, + {}, + flat_example_value, + ret_spec, + ret_val, + ) + + +class CheckpointHigherOrderVariable(WrapHigherOrderVariable): + _HOP_NAME = "torch.utils.checkpoint.checkpoint" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.allow_side_effects = ( + torch._dynamo.config.skip_fwd_side_effects_in_bwd_under_checkpoint + ) + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from torch._higher_order_ops.wrap import TagActivationCheckpoint + from torch.utils.checkpoint import noop_context_fn + + context_fn = None + if "context_fn" in kwargs and kwargs["context_fn"] is not noop_context_fn: + ctx = kwargs.pop("context_fn") + if isinstance(ctx, torch._dynamo.variables.UserFunctionVariable): + context_fn = ctx.fn + elif isinstance( + ctx, torch._dynamo.variables.functions.FunctoolsPartialVariable + ): + context_fn = ctx.guard_as_python_constant() + else: + raise NotImplementedError( + f"checkpoint not implemented for {type(ctx)} context_fn" + ) + + checkpoint_kwargs, gmod_kwargs = TagActivationCheckpoint.divide_kwargs(kwargs) + + # Here we use checkpoint_kwargs (and not gmod kwargs). gmod_kwargs are + # already flattened above and managed inside the fx graph. + ( + p_args, + _, + example_value, + _body_r, + checkpointed_gmod, + _, + body_graph_output_vts, + _, + ) = self.create_wrapped_node( + tx, + args[0], + args[1:], + gmod_kwargs, + "torch.utils.checkpoint.checkpoint", + ) + if context_fn is not None: + checkpointed_gmod.meta["_checkpoint_context_fn"] = context_fn + + _, checkpoint_kwargs = proxy_args_kwargs([], checkpoint_kwargs) + + return _call_function_with_auto_output_flattening( # type: ignore[return-value] + tx, + self.value, + p_args, + checkpoint_kwargs, + example_value, + _body_r, + body_graph_output_vts, + ) + + +class DynamoBypassingWrapperHigherOrderVariable(WrapHigherOrderVariable): + _HOP_NAME = "torch.ops.higher_order.dynamo_bypassing_wrapper" + + def __init__(self, hop: HigherOrderOperator, source: Source | None) -> None: + super().__init__(hop, source) + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + func_var = args[0] + + if isinstance(func_var, torch._dynamo.variables.UserFunctionVariable): + func = func_var.fn + elif isinstance( + func_var, torch._dynamo.variables.functions.FunctoolsPartialVariable + ): + func = func_var.as_python_constant() + else: + raise RuntimeError( + f"DynamoBypassingWrapperHigherOrderVariable: Unsupported function {type(func_var)}" + ) + ( + p_args, + _, + example_value, + _body_r, + gmod, + _, + body_graph_output_vts, + _, + ) = self.create_wrapped_node( + tx, + args[1], + args[2:], + kwargs, + str(func), + ) + + # Alternatively, we could've stored only the function's fqn and + # reconstructed, but that requires the function to be a global. + gmod_meta_key = "_dynamo_bypassing_wrapper_fn" + gmod.meta[gmod_meta_key] = func + + return _call_function_with_auto_output_flattening( # type: ignore[return-value] + tx, + self.value, + (gmod_meta_key,) + tuple(p_args), + {}, + example_value, + _body_r, + body_graph_output_vts, + ) + + +class ExportTracepointHigherOrderVariable(TorchHigherOrderOperatorVariable): + _HOP_NAME = "torch.ops.higher_order._export_tracepoint" + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .builder import wrap_fx_proxy + + p_args = tuple(arg.as_proxy() for arg in args) + p_kwargs = {key: arg.as_proxy() for key, arg in kwargs.items()} + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + self.value, + args=p_args, + kwargs=p_kwargs, + ), + example_value=None, + ) + + +class RunWithRNGStateHigherOrderVariable(TorchHigherOrderOperatorVariable): + _HOP_NAME = "torch.ops.higher_order.run_with_rng_state" + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .builder import wrap_fx_proxy + + p_args = tuple(arg.as_proxy() for arg in args) + p_kwargs = {key: arg.as_proxy() for key, arg in kwargs.items()} + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + self.value, + args=p_args, + kwargs=p_kwargs, + ), + example_value=None, + ) + + +class InlineAsmElementwiseHigherOrderVariable(TorchHigherOrderOperatorVariable): + _HOP_NAME = "inline_asm_elementwise" + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .builder import wrap_fx_proxy + + p_args = tuple(arg.as_proxy() for arg in args) + p_kwargs = {key: arg.as_proxy() for key, arg in kwargs.items()} + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + self.value, + args=p_args, + kwargs=p_kwargs, + ), + example_value=None, + ) + + +class AutoFunctionalizeHigherOrderVariable(TorchHigherOrderOperatorVariable): + _HOP_NAME = "torch.ops.higher_order.auto_functionalized" + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .builder import wrap_fx_proxy + + p_args = tuple(arg.as_proxy() for arg in args) + p_kwargs = {key: arg.as_proxy() for key, arg in kwargs.items()} + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + self.value, + args=p_args, + kwargs=p_kwargs, + ), + example_value=None, + ) + + +class FlexAttentionBackwardHighOrderVariable(TorchHigherOrderOperatorVariable): + _HOP_NAME = "torch.ops.higher_order.flex_attention_backward" + + @staticmethod + def _uses_pretraced_graphs( + fw_graph: VariableTracker, joint_graph: VariableTracker + ) -> bool: + return not joint_graph.is_constant_none() or isinstance( + fw_graph, UnspecializedNNModuleVariable + ) + + def proxy_submod( + self, tx: "InstructionTranslator", arg: UnspecializedNNModuleVariable + ) -> Proxy: + assert arg.source and isinstance(arg.source.base, DictGetItemSource) # type: ignore[attr-defined] + submod_name = tx.output.install_subgraph(arg.source.base.index, arg.value) # type: ignore[arg-type] + p_submod = make_attr(tx, submod_name) + set_example_value(p_submod.node, arg.value) + return p_submod + + def to_proxy(self, tx: "InstructionTranslator", arg: VariableTracker) -> Any: + if isinstance(arg, UnspecializedNNModuleVariable): + return self.proxy_submod(tx, arg) + elif isinstance(arg, (ListVariable, TupleVariable)): + return arg.python_type()( + self.to_proxy(tx, nested_arg) for nested_arg in arg.items + ) + else: + return arg.as_proxy() + + def create_wrapped_node( + self, + tx: "InstructionTranslator", + query: VariableTracker, + fn: VariableTracker, + fn_name: str, + other_buffers: Sequence[VariableTracker], + ) -> tuple[Proxy, tuple[Proxy, ...], torch.fx.GraphModule]: + from .._trace_wrapped_higher_order_op import TransformGetItemToIndex + + def create_scalar() -> VariableTracker: + return query.call_method( + tx, + "new_empty", + [VariableTracker.build(tx, [])], + {"dtype": VariableTracker.build(tx, torch.int32)}, + ) + + with discard_graph_changes(tx): + bhmn = [create_scalar() for _ in range(4)] + if fn_name == "score_mod": + scores_require_grad: bool = query.requires_grad # type: ignore[attr-defined] + score = query.call_method( + tx, + "new_empty", + [VariableTracker.build(tx, [])], + {"requires_grad": VariableTracker.build(tx, scores_require_grad)}, + ) + new_args = [score, *bhmn, *other_buffers] + else: + assert fn_name == "mask_fn", "Illegal function name: " + fn_name + new_args = [*bhmn, *other_buffers] + + with TransformGetItemToIndex(): + ( + (_body_output, _body_spec), + body_graph, + body_lifted_freevars, + ) = speculate_subgraph( + tx, + fn, + new_args, + {}, + description=f"{self._HOP_NAME}: {fn_name}", + source_target=self.value, + set_subgraph_inputs="flatten_manual", + ) + + gm = torch.fx.GraphModule(tx.output.nn_modules, body_graph) + body_name = tx.output.install_subgraph(fn_name, gm) + return make_attr(tx, body_name), tuple(body_lifted_freevars), gm + + @staticmethod + def _buffer_example_value(buffer: VariableTracker) -> Any: + proxy = buffer.as_proxy() + if isinstance(proxy, Proxy): + return proxy.node.meta["example_value"] + return proxy + + def _derive_joint_graph( + self, + tx: "InstructionTranslator", + query: VariableTracker, + fw_graph_gm: torch.fx.GraphModule, + score_mod_other_buffers: TupleVariable, + fw_graph_lifted_args: tuple[Proxy, ...], + ) -> Proxy: + from torch._higher_order_ops.flex_attention import create_fw_bw_graph + + query_example = query.as_proxy().node.meta["example_value"] + example_vals = ( + query_example.new_zeros((), requires_grad=True), + query_example.new_zeros((), dtype=torch.int), + query_example.new_zeros((), dtype=torch.int), + query_example.new_zeros((), dtype=torch.int), + query_example.new_zeros((), dtype=torch.int), + ) + all_buffer_examples = tuple( + self._buffer_example_value(buf) for buf in score_mod_other_buffers.items + ) + tuple(buf.node.meta["example_value"] for buf in fw_graph_lifted_args) + + _, joint_gm = create_fw_bw_graph(fw_graph_gm, example_vals, all_buffer_examples) + joint_gm = cast(GraphModule, joint_gm) + + submod_name = tx.output.install_subgraph("joint_graph", joint_gm) + p_submod = make_attr(tx, submod_name) + set_example_value(p_submod.node, joint_gm) + return p_submod + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .builder import wrap_fx_proxy + + if len(args) != 14: + return self._call_function_fallback(tx, args, kwargs) + + ( + query, + key, + value, + out, + logsumexp, + grad_out, + grad_logsumexp, + fw_graph, + joint_graph, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ) = args + + if ( + not isinstance(block_mask, TupleVariable) + or not isinstance(score_mod_other_buffers, TupleVariable) + or not isinstance(mask_mod_other_buffers, TupleVariable) + or len(block_mask.items) < 1 + ): + return self._call_function_fallback(tx, args, kwargs) + + if self._uses_pretraced_graphs(fw_graph, joint_graph): + return self._call_function_fallback(tx, args, kwargs) + + fw_graph_node, fw_graph_lifted_args, fw_graph_gm = self.create_wrapped_node( + tx, query, fw_graph, "score_mod", score_mod_other_buffers.items + ) + + joint_graph_node = self._derive_joint_graph( + tx, + query, + fw_graph_gm, + score_mod_other_buffers, + fw_graph_lifted_args, + ) + + mask_fn = block_mask.items[-1] + if mask_fn.is_python_constant() and mask_fn.as_python_constant() is None: + mask_fn = VariableTracker.build( + tx, + torch.nn.attention.flex_attention.noop_mask, + source=mask_fn.source, + ) + mask_fn_node, mask_fn_lifted_args, _ = self.create_wrapped_node( + tx, query, mask_fn, "mask_fn", mask_mod_other_buffers.items + ) + + proxied_args = [ + query, + key, + value, + out, + logsumexp, + grad_out, + grad_logsumexp, + TupleVariable(block_mask.items[:-1], source=block_mask.source), + scale, + kernel_options, + ] + inp_args, _ = proxy_args_kwargs(proxied_args, {}) + proxied_score_mod_other_buffers = tuple( + self.to_proxy(tx, arg) for arg in score_mod_other_buffers.items + ) + proxied_mask_mod_other_buffers = tuple( + self.to_proxy(tx, arg) for arg in mask_mod_other_buffers.items + ) + + ( + inp_q, + inp_k, + inp_v, + inp_out, + inp_lse, + inp_grad_out, + inp_grad_lse, + inp_block_mask, + inp_scale, + inp_kernel_options, + ) = inp_args + + block_mask_proxy = tuple(inp_block_mask + (mask_fn_node,)) + + with torch.fx.experimental.proxy_tensor.set_original_aten_op(self.value): + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + self.value, + args=( + inp_q, + inp_k, + inp_v, + inp_out, + inp_lse, + inp_grad_out, + inp_grad_lse, + fw_graph_node, + joint_graph_node, + block_mask_proxy, + inp_scale, + inp_kernel_options, + proxied_score_mod_other_buffers + fw_graph_lifted_args, + proxied_mask_mod_other_buffers + mask_fn_lifted_args, + ), + kwargs={}, + ), + example_value=None, + ) + + def _call_function_fallback( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .builder import wrap_fx_proxy + + p_args, p_kwargs = None, None + try: + p_args = tuple(self.to_proxy(tx, arg) for arg in args) + p_kwargs = {key: self.to_proxy(tx, arg) for key, arg in kwargs.items()} + except (NotImplementedError, Unsupported) as err: + unimplemented( + gb_type="failed to handle argument for FlexAttentionBackward HOP", + context=f"args: {args}, kwargs: {kwargs}", + explanation="Missing Dynamo support for FlexAttentionBackward HOP argument.", + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + from_exc=err, + ) + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + self.value, + args=p_args, + kwargs=p_kwargs, + ), + example_value=None, + ) + + +class TraceWrappedHigherOrderOperatorVariable(TorchHigherOrderOperatorVariable): + """ + Handles torch._dynamo._trace_wrapped_higher_order_op.inner_trace + by unwrapping the higher order op and inlining through it. This op + is created by dynamo to survive through AotAutograd, then unwrapped + here in the call to dynamo from compiled autograd. + """ + + _HOP_NAME = "torch._dynamo._trace_wrapped_higher_order_op.inner_trace" + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + kwargs = dict(kwargs) + fn = kwargs.pop("fn") + return fn.call_function(tx, args, kwargs) + + +class FlexAttentionHigherOrderVariable(TorchHigherOrderOperatorVariable): + _HOP_NAME = "torch.ops.higher_order.flex_attention" + + @staticmethod + def normalize_to_args( + args: list[VariableTracker], kwargs: dict[str, VariableTracker] + ) -> list[VariableTracker]: + # input signature is (query, key, value, score_mod, block_mask, *other_buffers), + # block_mask is a tuple, and we don't want to flatten it. + # only flatten kwargs into lists + flat_kwargs = pytree.tree_flatten(kwargs)[0] + + # Combine the flattened lists + all_args = args + flat_kwargs + return all_args + + def create_wrapped_node( + self, + tx: "InstructionTranslator", + query: VariableTracker, + fn: VariableTracker, + fn_name: str, + ) -> tuple[Proxy, tuple[Proxy, ...]]: + from .._trace_wrapped_higher_order_op import TransformGetItemToIndex + + def create_scalar() -> VariableTracker: + return query.call_method( + tx, + "new_empty", + [ + VariableTracker.build(tx, []), + ], + { + "dtype": VariableTracker.build(tx, torch.int32), + }, + ) + + with discard_graph_changes(tx): + bhmn = [create_scalar() for _ in range(4)] + if fn_name == "score_mod": + scores_require_grad: bool = query.requires_grad # type: ignore[attr-defined] + score = query.call_method( + tx, + "new_empty", + [ + VariableTracker.build(tx, []), + ], + {"requires_grad": VariableTracker.build(tx, scores_require_grad)}, + ) + new_args = [score, *bhmn] + else: + assert fn_name == "mask_fn", "Illegal function name: " + fn_name + new_args = [*bhmn] + + with TransformGetItemToIndex(): + ( + (_body_output, _body_spec), + body_graph, + body_lifted_freevars, + ) = speculate_subgraph( + tx, + fn, + new_args, + {}, # expect only args no kwargs for now + description=f"{self._HOP_NAME}: {fn_name}", + source_target=self.value, + set_subgraph_inputs="flatten_manual", + ) + + body_name = tx.output.install_subgraph( + fn_name, + torch.fx.GraphModule(tx.output.nn_modules, body_graph), + ) + + body_node = make_attr(tx, body_name) + + # It is possible that the score-mod function captures some free variables that are not + # passed in as arguments. In this case, we need to lift them, which is handled by speculate_subgraph. + # We then need to create proxies for this + the inputs. + + lifted_args = tuple(arg for arg in body_lifted_freevars) + + proxy_args = (body_node, lifted_args) + + return proxy_args + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .builder import wrap_fx_proxy + + ( + query, + key, + value, + score_mod, + block_mask, + scale, + kernel_options, + ) = self.normalize_to_args(list(args), kwargs) + + score_mod_node, score_mod_lifted_args = self.create_wrapped_node( + tx, query, score_mod, "score_mod" + ) + mask_fn = block_mask.items[-1] # type: ignore[attr-defined] + if mask_fn.is_python_constant() and mask_fn.as_python_constant() is None: + mask_fn = VariableTracker.build( + tx, + torch.nn.attention.flex_attention.noop_mask, + source=mask_fn.source, + ) + mask_fn_node, mask_fn_lifted_args = self.create_wrapped_node( + tx, query, mask_fn, "mask_fn" + ) + + proxied_args = [ + query, + key, + value, + TupleVariable(block_mask.items[:-1], source=block_mask.source), # type: ignore[attr-defined] + scale, + kernel_options, + ] + + # Store the invocation as a call + # Norm_kwargs contains the score_function and we dont want to proxy this because + # Proxying user defined functions is not supported. + inp_args, _ = proxy_args_kwargs(proxied_args, {}) + + # Compose the ordered HOO args: + # - inp_args: [query, key, value, block_mask, scale, kernel_options] + # - subgraph node: [score_mod, mask_fn_node] + # - lifted args from tracing subgraph: [score_mod_other_buffers, mask_fn_other_buffers] + _, _, _, inp_arg_block_mask, inp_arg_scale, inp_arg_kernel_options = inp_args + block_mask = tuple(inp_arg_block_mask + (mask_fn_node,)) + with torch.fx.experimental.proxy_tensor.set_original_aten_op(self.value): + proxy = wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + self.value, + args=inp_args[:3] + + ( + score_mod_node, + block_mask, + inp_arg_scale, + inp_arg_kernel_options, + score_mod_lifted_args, + mask_fn_lifted_args, + ), + kwargs={}, + ), + example_value=None, + ) + return proxy + + +@add_hop_context +class AutogradFunctionApplyVariable(VariableTracker): + _HOP_NAME: str = "autograd.Function" + _ALLOW_FALLBACK_TO_EAGER = True + + def __init__( + self, fwd_fn: Any, bwd_fn: Any, parent_source: Source | None, **kwargs: Any + ) -> None: + super().__init__(**kwargs) + self.fwd_fn = fwd_fn + self.bwd_fn = bwd_fn + self.parent_source = parent_source + + def python_type(self) -> type: + return types.BuiltinMethodType + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + """ + At the highest level, the goal of tracing an autograd.Function is to + essentially emit a new autograd.Function object. To do this, Dynamo + traces fwd and bwd graph and then inserts a AutogradFunctionApply HOP in + the graph that call the traced fwd and bwd graph in the `forward` and + `backward` methods respectively. AOTDispatcher desugars this HOP and + just inlines the hop fwd and bwd into the main graph during its tracing. + + However, the traced forward and backward graphs cannot be directly + placed in the new autograd.Function because autograd.Function has some + requirements. + + a) # fwd graph inputs = # bwd graph outputs + b) # fwd graph outputs = # bwd graph inputs + c) Since the graphs do not have ctx variable, we have to manually return + the saved_tensors from the forward and have additional inputs in the + backward, and wire the connections. + + Unfortunately, reworking the initial traced fwd and bwd graphs to + satisfy the above 3 conditions leads to a very tedious codebase. + + Lets look at an example + + class Foo: + def __init__(self): + self.a = 4 + + class MySin(torch.autograd.Function): + @staticmethod + def forward(ctx, x, foo): + ctx.save_for_backward(x) + return x.sin() + foo.a + + @staticmethod + def backward(ctx, grad): + x, = ctx.saved_tensors + return grad * x.cos() + + We want the resulting graphs to look like: + + # Note that Dynamo lifts the foo_a directly as an input. + def fwd(ctx, x, foo_a): + # (output, saved tensors / attrs) + return (x.sin() + foo_a, (x)) + + # Note that backward graph has None as the second output to match the + # fwd requirements (even though the original backward function has just + # output) + def bwd(ctx, grad, x): + return grad * x.cos(), None + + + To accomplish this, we're going to: + 1. Construct a ctx object + 2. Speculate subgraph forward + 3. Speculate subgraph backward + 4. rewired_bwd_graph_inputs - Use the traced fwd graph as the anchor point, and rewire the backward graph outputs + 5. handle_saved_tensors_wiring - Handle the saved tensors, as mentioned in (c) + """ + + fwd_tracer = torch._dynamo.output_graph.SubgraphTracer( + tx.output, + parent=tx.output.current_tracer, + source_target=self._HOP_NAME, + ) + + ctx = self.prepare_ctx_vt(tx, args, kwargs) + + fwd_fn, fwd_out, fwd_graph, fwd_freevars, fwd_graph_output_vts = ( + self.trace_forward_graph(tx, ctx, fwd_tracer, args, kwargs) + ) + + bwd_args, bwd_out, bwd_graph, bwd_freevars, bwd_graph_output_vts = ( + self.trace_backward_graph(tx, ctx, fwd_tracer, fwd_out, fwd_fn) + ) + + self.rewire_bwd_graph_outputs( + fwd_freevars, bwd_out, bwd_graph, bwd_freevars, args + ) + + fwd_graph, bwd_graph = self.handle_saved_tensors_wiring( + fwd_out, + fwd_graph, + fwd_freevars, + fwd_graph_output_vts, # type: ignore[arg-type] + bwd_graph, + bwd_freevars, + ) + + # If users call ctx.mark_non_differentiable, we should capture these output tensors who + # are marked as non-differentiable and pass them to ApplyTemplate + # at torch._functorch.autograd_function.AutogradFunctionApply for reconstruction. + non_differentiable_idx = [] + if ctx.non_differentiable is not None: + non_differentiable_set = set(ctx.non_differentiable) + assert isinstance(fwd_out, variables.BaseListVariable) + for i, x in enumerate(fwd_out.items): + if x.is_tensor() and x.as_proxy() in non_differentiable_set: + non_differentiable_idx.append(i) + + # See Note [Activations with no version counter checks in eager] + # Compute which tensors in bwd_freevars came from ctx.save_for_backward. + # This allows AOT autograd to distinguish between tensors saved via + # save_for_backward vs those stashed directly on ctx (e.g., ctx.x = x). + saved_for_backward_idx = [] + if ctx.saved_tensors is not None and len(ctx.saved_tensors.tensors) > 0: + # Build a set of proxies that were passed to save_for_backward + saved_tensor_proxies = OrderedSet() + for tensor_vt in ctx.saved_tensors.tensors: + if tensor_vt.is_tensor(): + saved_tensor_proxies.add(tensor_vt.as_proxy()) + + # bwd_freevars is a dict of outer-graph proxy -> inner-graph proxy + # for all tensors passed from fwd to bwd. Find which indices + # correspond to save_for_backward tensors. + for i, fwd_proxy in enumerate(bwd_freevars.keys()): + if fwd_proxy in saved_tensor_proxies: + saved_for_backward_idx.append(i) + + # Store fwd_body + fwd_nn_modules = tx.output.tracing_context.module_context.copy_graphstate() + fwd_name = tx.output.install_subgraph( + "fwd_body", + torch.fx.GraphModule(fwd_nn_modules.nn_modules, fwd_graph), + ) + fwd_node = make_attr(tx, fwd_name) + + # Store bwd_body + bwd_nn_modules = tx.output.tracing_context.module_context.copy_graphstate() + bwd_name = tx.output.install_subgraph( + "bwd_body", + torch.fx.GraphModule(bwd_nn_modules.nn_modules, bwd_graph), + ) + bwd_node = make_attr(tx, bwd_name) + + p_args = ( + fwd_node, + bwd_node, + *list(fwd_freevars.keys()), + ) + kwargs_for_fn = { + "non_differentiable_idx": non_differentiable_idx, + "saved_for_backward_idx": saved_for_backward_idx, + } + + # Store the invocation as a call + from torch._functorch.autograd_function import autograd_function_apply + + # We use speculate_subgraph to get the fwd graph, but it's always under no grad mode like what eager mode does. + # The fwd outputs (tensor's example_value) need to be inferred from fake tensor prop to get the correct attributes + # (e.g, tensor.requires_grad), which would be used by downstream Dynamo tracing. + # Since there can be other ops like Triton kernels, which depends on python dispatcher, we have to enable it. + # TODO - revisit if we need the python dispatcher + with enable_python_dispatcher(): + with tx.output.fake_mode: + fwd_freevars_args = [_get_fake_value(arg) for arg in fwd_freevars] + + example_value = autograd_function_apply( + tx.output.nn_modules[fwd_node.node.name], + tx.output.nn_modules[bwd_node.node.name], + *fwd_freevars_args, + **kwargs_for_fn, + ) + + flat_variable = add_call_function( + tx, autograd_function_apply, p_args, kwargs_for_fn, example_value + ) + # type: ignore[arg-type] + overwrite_tensor_vt_proxy(fwd_graph_output_vts, flat_variable) + # type: ignore[arg-type] + overwrite_tensor_vt_requires_grad(fwd_graph_output_vts, flat_variable) + return fwd_out + + def prepare_ctx_vt( + self, tx: "InstructionTranslator", args: Any, kwargs: Any + ) -> "AutogradFunctionContextVariable": + from . import AutogradFunctionContextVariable + + ctx = AutogradFunctionContextVariable.create(tx, args, kwargs) + with discard_graph_changes(tx): + # A little hacky, but we need a dummy ctx proxy for speculate_subgraph. + # We should clean this up at some point. + proxy = tx.output.create_proxy( + "call_function", torch.autograd.function.FunctionCtx, (), {} + ) + # type: ignore[attr-defined] + set_example_value(proxy.node, ctx.value) + # type: ignore[attr-defined] + ctx.proxy = proxy + # pyrefly: ignore[bad-return] + return ctx + + def trace_forward_graph( + self, + tx: "InstructionTranslator", + ctx: "AutogradFunctionContextVariable", + fwd_tracer: "SubgraphTracer", + args: Any, + kwargs: Any, + ) -> tuple[ + VariableTracker, + VariableTracker, + torch.fx.Graph, + dict[Proxy, Proxy], + VariableTracker | tuple[VariableTracker, ...], + ]: + """ + Traces the forward method of the autograd.Function object. + """ + from torch._functorch.autograd_function import DynamoAutogradFunctionTraceHelper + + fwd_fn, fwd_args = self.prepare_fn_vt(tx, ctx, "forward", args) + + # autograd.Function forward does a few things like running in no_grad + # mode and also applying view_as for input tensors that are returned as + # outputs. Therefore, we wrap the original forward in a helper that have + # those extra bits for Dynamo to trace. + fwd_fn = _make_inlined(tx, DynamoAutogradFunctionTraceHelper.fwd_trace_helper)( + fwd_fn + ) + + # Speculate subgraph on the fwd + fwd_out, fwd_graph, fwd_freevars, fwd_graph_output_vts, _ = ( + speculate_subgraph_with_auto_output_flattening( + tx, + fwd_fn, + fwd_args, + kwargs, + self._HOP_NAME, + enable_grad=None, + set_subgraph_inputs="automatic", + allow_side_effects=True, + filter_aliased_intermediates=True, + tracer=fwd_tracer, + ) + ) + + # There could be unused inputs in the forward, and Dynamo might not + # capture them. We must lift them as inputs, because even though they + # are not used in forward, we still need to account for their gradients + # in the backward. + for arg in args: + if arg.is_tensor(): + fwd_tracer.maybe_lift_tracked_freevar_to_input(arg.as_proxy()) + + if ctx in tx.output.side_effects.store_attr_mutations: + if ( + "_materialize_non_diff_grads" + in tx.output.side_effects.store_attr_mutations[ctx] + ): + unimplemented( + gb_type="autograd.Function.apply: _materialize_non_diff_grads mutation", + context="", + explanation="Mutations to autograd.Function.ctx._materialize_non_diff_grads are not supported.", + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + ) + + return fwd_fn, fwd_out, fwd_graph, fwd_freevars, fwd_graph_output_vts + + def trace_backward_graph( + self, + tx: "InstructionTranslator", + ctx: "AutogradFunctionContextVariable", + fwd_tracer: "SubgraphTracer", + fwd_out: VariableTracker, + fwd_fn: VariableTracker, + ) -> tuple[ + Sequence[VariableTracker], + VariableTracker, + torch.fx.Graph, + dict[Proxy, Proxy], + VariableTracker | tuple[VariableTracker, ...], + ]: + """ + Traces the backward method of the autograd.Function object. + """ + from . import UserMethodVariable + + # Note that for the forward, we do not restore side effects, because we + # want the later tracing to see the side-effects. But for backward, we + # are just trying to capture the graph, and therefore we must restore + # the side effects. + prev_side_effects = tx.output.side_effects + + # Speculate subgraph on the backward. We make the bwd tracer a child of + # the fwd tracer, because backward may rely on tensors/attrs created in + # the fwd tracer. + bwd_tracer = torch._dynamo.output_graph.SubgraphTracer( + tx.output, + parent=fwd_tracer, + source_target=self._HOP_NAME, + ) + + bwd_args = [] + if fwd_out.is_tensor(): + bwd_args.append(fwd_out) + else: + assert isinstance(fwd_out, variables.BaseListVariable) + for i in fwd_out.items: + if i.is_tensor(): + bwd_args.append(i) + else: + bwd_args.append(ConstantVariable.create(None)) + + bwd_fn, bwd_args = self.prepare_fn_vt(tx, ctx, "backward", bwd_args) + + def is_strict_for(v: VariableTracker) -> bool: + if v.is_tensor(): + # we can be more lax for stuff from forward + return v.proxy.tracer is not fwd_tracer # type: ignore[attr-defined] + return True + + # automatic_with_forced_inputs relies on the function arg names to + # create a new proxy. Also, it will always INSERT a tensor placeholder + # as input, even though it might not be used in the graph. This allows + # us to make a mapping for the backward graph. + with ( + tx.output.subtracer(fwd_fn, fwd_tracer), # type: ignore[arg-type] + tx.strict_translation_mode(is_strict_for), + ): + try: + bwd_out, bwd_graph, bwd_freevars, bwd_graph_output_vts, _ = ( + speculate_subgraph_with_auto_output_flattening( + tx, + bwd_fn, + bwd_args, + {}, + self._HOP_NAME, + # TODO - revisit if we need enable_grad + enable_grad=False, + set_subgraph_inputs="automatic_with_forced_inputs", + allow_side_effects=False, + tracer=bwd_tracer, + ) + ) + except torch._dynamo.exc.UnknownPropertiesDuringBackwardTrace as e: + # TODO - Do not support this path because of eager + # divergence forced by contiguous calls. Instead suggested + # nonstrict_trace. + from unittest import mock + + bwd_tracer = torch._dynamo.output_graph.SubgraphTracer( + tx.output, + parent=fwd_tracer, + source_target=self._HOP_NAME, + ) + from .._trace_wrapped_higher_order_op import ( + autograd_function_backward_rewritten, + ) + from .builder import SourcelessBuilder + + if isinstance(self.bwd_fn, types.FunctionType): + bwd_fn = SourcelessBuilder.create( + tx, autograd_function_backward_rewritten(self.bwd_fn) + ) + elif isinstance(self.bwd_fn, types.MethodType): + bwd_fn = UserMethodVariable( + autograd_function_backward_rewritten(self.bwd_fn.__func__), + VariableTracker.build(tx, self.bwd_fn.__class__), + ) + else: + unimplemented( + gb_type="autograd.Function.apply: non-function or method backward (2)", + context=str(self.bwd_fn), + explanation="Expected backward function to be a function or method.", + hints=[], + from_exc=e, + ) + + with mock.patch( + "torch._dynamo.config._autograd_backward_strict_mode_conditional_banned_ops", + [], + ): + bwd_out, bwd_graph, bwd_freevars, bwd_graph_output_vts, _ = ( + speculate_subgraph_with_auto_output_flattening( + tx, + bwd_fn, + bwd_args, + {}, + self._HOP_NAME, + enable_grad=False, + set_subgraph_inputs="automatic_with_forced_inputs", + allow_side_effects=False, + tracer=bwd_tracer, + ) + ) + + # Restore the side effects + tx.output.side_effects = prev_side_effects + + return bwd_args, bwd_out, bwd_graph, bwd_freevars, bwd_graph_output_vts + + def rewire_bwd_graph_outputs( + self, + fwd_freevars: dict[Proxy, Proxy], + bwd_out: VariableTracker, + bwd_graph: torch.fx.Graph, + bwd_freevars: dict[Proxy, Proxy], + orig_fwd_args: Sequence[VariableTracker], + ) -> None: + # --------------------------------------------------------------------- + # Forward–Backward Input/Output Alignment + # + # autograd.Function requires that the outputs of backward() correspond + # exactly to the inputs of forward(). Normally this alignment is the + # user’s responsibility. However, when Dynamo synthesizes a new + # autograd.Function for a traced region, Dynamo must perform this + # alignment automatically. + # + # To do this, Dynamo uses the *original* forward call site as the anchor + # that defines how forward inputs map to backward outputs. + # + # --------------------------------------------------------------------- + # Terminology + # + # fwd_freevars / bwd_freevars: + # Maps from *outer-graph proxies* to *inner-graph placeholder + # proxies*. Keys are always outer-graph proxies (these may be actual + # user inputs or intermediate values lifted into the subgraph). + # + # orig_fwd_args: + # VariableTrackers for the forward() inputs. Since these correspond + # to user-exposed arguments, each tracker points to an *outer-graph* + # proxy. + # + # bwd_outs: + # VariableTrackers for the backward() outputs. These usually point to + # *inner-graph* proxies, except for cases where a forward input is + # passed directly through to a backward output—in which case the + # tracker may still refer to an outer-graph proxy. + # + # --------------------------------------------------------------------- + # Goal + # + # To ensure forward–backward consistency, we must rewire the backward + # graph outputs so that they line up with the forward graph inputs. + # + # We build a mapping from outer-graph proxy → inner-graph proxy using + # orig_fwd_args and bwd_outs, then iterate over the fwd_graph inputs to + # determine which backward outputs must be generated (or padded with + # None) to satisfy autograd’s calling convention. + # + # --------------------------------------------------------------------- + # Example + # + # Suppose the forward receives a user-defined object: + # + # @dataclass + # class Weird: + # x: int + # b: torch.Tensor + # c: torch.Tensor + # + # class Foo(torch.autograd.Function): + # @staticmethod + # def forward(ctx, x: torch.Tensor, weird: Weird, z: torch.Tensor): + # ctx.save_for_backward(weird.b, weird.c) + # return weird.b * weird.c * x.clone() + # + # @staticmethod + # def backward(ctx, grad): + # b, c = ctx.saved_tensors + # return grad * b * c, None, grad * 2 + # + # Dynamo lifts the tensor fields of the user-defined object for the trace: + # + # fwd_graph(): + # %l_weird_b : FakeTensor = placeholder[target=l_weird_b] + # %l_weird_c : FakeTensor = placeholder[target=l_weird_c] + # %l_x_ : FakeTensor = placeholder[target=l_x_] + # %l_z_ : FakeTensor = placeholder[target=l_z_] + # ... + # return (outs,) + # + # The initial backward graph: + # + # bwd_graph(): + # %grad : Tensor = placeholder[target=grad] + # %l_weird_b : FakeTensor = placeholder[target=l_weird_b] + # %l_weird_c : FakeTensor = placeholder[target=l_weird_c] + # ... + # return (mul_1, mul_2) + # + # The forward graph has 4 inputs, but the backward graph produces only 2 + # outputs, and their ordering does not match the forward argument order. + # + # So Dynamo rewires the backward graph outputs to align with the forward + # inputs: + # + # bwd_graph(): + # ... + # return (None, None, mul_1, mul_2) + # + # This ensures the synthesized autograd.Function conforms to PyTorch’s + # forward/backward contract. + # --------------------------------------------------------------------- + + def get_bwd_node(vt: VariableTracker) -> torch.fx.Node: + # Backward tensor vt here can be - (1) an intermediate, or (2) input + # to the backward graph. If it is an input to the backward graph, we have to lookup bwd_freevars to get the inner proxy. + return bwd_freevars.get(vt.proxy, vt.proxy).node # type: ignore[attr-defined] + + # Find the mapping between orig_fwd_args and bwd_out + # pyrefly: ignore [implicit-any] + outer_fwd_proxy_to_bwd_node = {} + if isinstance(bwd_out, variables.BaseListVariable): + bwd_outs = bwd_out.items + for idx, fwd_arg in enumerate(orig_fwd_args): + # We care about tensor args. For non-tensor args, the bwd output returns None. + if fwd_arg.is_tensor(): + bwd_out_at_idx = bwd_outs[idx] + if bwd_out_at_idx.is_tensor(): + # type: ignore[attr-defined] + outer_fwd_proxy_to_bwd_node[fwd_arg.proxy] = get_bwd_node( + bwd_out_at_idx + ) + else: + # backward can return None at the output + assert ( + isinstance(bwd_out_at_idx, variables.ConstantVariable) + and bwd_out_at_idx.value is None + ) + # type: ignore[attr-defined] + outer_fwd_proxy_to_bwd_node[fwd_arg.proxy] = None + + elif bwd_out.is_tensor(): + # type: ignore[attr-defined] + outer_fwd_proxy_to_bwd_node[orig_fwd_args[0].proxy] = get_bwd_node(bwd_out) + + # Ideally, we should have walked through the fwd placeholders. But we + # can instead walk through the fwd_freevars, which is a insertion sorted + # dictionary and therefore represents the outer_proxies for the + # placeholder in the same order as that as placeholders. + rewired_bwd_outputs = [ + outer_fwd_proxy_to_bwd_node.get(fwd_proxy) for fwd_proxy in fwd_freevars + ] + + for node in bwd_graph.find_nodes(op="output"): + bwd_graph.erase_node(node) + break + bwd_graph.output(tuple(rewired_bwd_outputs)) + bwd_graph.lint() + + def handle_saved_tensors_wiring( + self, + fwd_out: VariableTracker, + fwd_graph: torch.fx.Graph, + fwd_freevars: dict[Proxy, Proxy], + fwd_graph_body_outputs: Sequence[VariableTracker], + bwd_graph: torch.fx.Graph, + bwd_freevars: dict[Proxy, Proxy], + ) -> tuple[torch.fx.Graph, torch.fx.Graph]: + # --------------------------------------------------------------------- + # Rewiring Forward Outputs to Backward Inputs (and Handling Saved Tensors) + # + # In `rewire_bwd_graph_outputs`, we aligned the *forward inputs* with the + # *backward outputs*. This method performs the complementary task: + # aligning the *forward outputs* with the *backward inputs*, while also + # incorporating all tensors saved via ctx.save_for_backward. + # + # There are two main issues we must resolve: + # + # (1) Forward outputs may contain non-tensor values. + # This means the number of tensors visible in fwd_out may not match + # the number of tensors produced by the traced forward graph. As a + # result, the backward graph’s placeholders may not line up with the + # actual tensor outputs. + # + # (2) The backward graph may require intermediate tensors saved during + # the forward pass (via save_for_backward), but those intermediates + # might not currently be included among the forward graph’s outputs. + # + # Together, these issues mean that the bwd_graph input signature may be + # inconsistent with what fwd_graph outputs, and we need to rewrite both. + # + # Lets look at an example to understand the transformation + # + # class Add(torch.autograd.Function): + # @staticmethod + # def forward(ctx, x, y): + # a = torch.sin(x) + # b = torch.cos(y) + # ctx.save_for_backward(a) + # return Foo(a, b), x * y + + # @staticmethod + # def backward(ctx, grad_a, grad_b): + # (a,) = ctx.saved_tensors + # return grad_b * 2, a * grad_b * 3 + + # Before + # fwd_graph(): + # %l_x_ : torch._subclasses.fake_tensor.FakeTensor [num_users=2] = placeholder[target=l_x_] + # %l_y_ : torch._subclasses.fake_tensor.FakeTensor [num_users=2] = placeholder[target=l_y_] + # .... + # return (a, b, out) + # + # bwd_graph(): + # %grad_b : torch.Tensor [num_users=2] = placeholder[target=grad_b] + # %a : torch._subclasses.fake_tensor.FakeTensor [num_users=1] = placeholder[target=a] + # .... + # return (mul, mul_2) + # + # The problems here: + # (1) fwd_graph has 3 tensor outputs (a, b, out), but bwd_graph has + # only 1 gradient input - grad_b. We need 3. + # + # (2) bwd_graph uses `a` (a saved tensor) as an input, but fwd_graph + # does not currently return `a`. To make `a` available to the + # backward graph, the forward graph must expose it as part of its + # output signature. + # + # After this transformation + # fwd_graph(): + # %l_x_ : torch._subclasses.fake_tensor.FakeTensor [num_users=2] = placeholder[target=l_x_] + # %l_y_ : torch._subclasses.fake_tensor.FakeTensor [num_users=2] = placeholder[target=l_y_] + # ..... + # return ((a, b, out), (a,)) + # bwd_graph(): + # %unused_0 : [num_users=0] = placeholder[target=unused_0] + # %unused_1 : [num_users=0] = placeholder[target=unused_1] + # %grad_b : [num_users=2] = placeholder[target=grad_b] + # %a : [num_users=1] = placeholder[target=a] + # ..... + # return (mul, mul_2) + # + # Key changes: + # + # 1) The forward graph now returns: + # (existing_outputs), (saved_tensors) + # This exposes saved intermediates (`a`) as part of the fwd output + # structure, making them available to backward. + # + # 2) The backward graph input signature is rewritten to: + # (*grads_for_existing_outputs, *saved_tensors) + # This ensures the counts and ordering match the new fwd_graph + # output structure. Placeholders corresponding to tensors whose + # gradients are unused (e.g., `a`, `b`) appear as `%unused_*`. + # + # This alignment ensures that the synthesized autograd.Function follows + # PyTorch’s forward/backward calling convention and that all required + # saved tensors are available to the backward graph. + # --------------------------------------------------------------------- + + # To address Problem (1), we must determine which backward-graph inputs + # correspond to the forward-graph outputs. + # + # We use two facts: + # • `fwd_out` preserves the original forward output order. + # • Backward-graph inputs are also ordered according to the backward() + # method signature, thanks to automatic_with_forced_inputs. + # + # For any forward output that is *not* a tensor, there is no + # corresponding tensor placeholder in the backward graph. During tracing, + # we intentionally inserted a `None` VariableTracker for these positions, + # so the backward graph contains no placeholder for them. + bwd_input_nodes = list(bwd_graph.find_nodes(op="placeholder")) + # pyrefly: ignore [implicit-any] + fwd_vt_to_bwd_node = {} + bwd_idx = 0 + if isinstance(fwd_out, variables.BaseListVariable): + for fwd_vt in fwd_out.items: + if fwd_vt.is_tensor(): + fwd_vt_to_bwd_node[fwd_vt] = bwd_input_nodes[bwd_idx] + bwd_idx += 1 + else: + if fwd_out.is_tensor(): + fwd_vt_to_bwd_node[fwd_out] = bwd_input_nodes[bwd_idx] + bwd_idx += 1 + + rewired_bwd_graph_inputs = [] + for fwd_graph_vt in fwd_graph_body_outputs: + # for tensor vts that were part of a user-defined object (like in + # the above example), we just set None for now. Later, we will use + # these None to insert a unused placeholder. + # type: ignore[arg-type] + rewired_bwd_graph_inputs.append(fwd_vt_to_bwd_node.get(fwd_graph_vt)) + + # To address Problem (2), we must incorporate any tensors that were saved + # (or otherwise smuggled) from the forward pass into the backward graph. + # + # Fortunately, these are easy to identify: they appear in `bwd_freevars`. + # `bwd_freevars` maps outer-graph lifted proxies to inner-graph placeholder + # proxies. Because the backward graph is traced using proxies originating + # from `fwd_out`, any value lifted into the backward graph represents a + # saved/smuggled tensor. + # + # Once we identify these saved tensors, we must also locate their + # corresponding forward-graph proxies so that the forward graph can return + # these tensors as part of its output signature. + extra_fwd_output_nodes = [] + for fwd_proxy, bwd_inner_proxy in bwd_freevars.items(): + # For backward, its easy, just get the node from bwd_inner_proxy + rewired_bwd_graph_inputs.append(bwd_inner_proxy.node) + + # For the fwd_proxy, it could be a proxy from the outer graph, or it + # could be an intermediate. + # First ensure that's its inner fwd proxy + inner_fwd_proxy = fwd_freevars.get(fwd_proxy, fwd_proxy) + extra_fwd_output_nodes.append(inner_fwd_proxy.node) + + # Mechanical steps from here on. We have the extra_fwd_outputs and rewired_bwd_inputs. Lets make the changes. + # Lets change the fwd graph outputs. + # pyrefly: ignore [implicit-any] + fwd_output_nodes = [] + for node in fwd_graph.find_nodes(op="output"): + fwd_output_nodes = node.args[0] + fwd_graph.erase_node(node) + break + + # The signature is now ((*existing_outputs), (*extra_outputs)). Please + # take a look at AutogradFunctionApply where we take the saved_tensors + # out in the forward method to save for backward. + new_fwd_graph_outputs = (fwd_output_nodes, tuple(extra_fwd_output_nodes)) + fwd_graph.output(new_fwd_graph_outputs) + fwd_graph.lint() + + # Now lets change the bwd graph. + new_graph = torch.fx.Graph() + env = {} + + count = itertools.count() + + for node in rewired_bwd_graph_inputs: + if node is None: + new_node = new_graph.placeholder(f"unused_{next(count)}") + else: + new_node = new_graph.placeholder(node.name) + new_node.meta = copy.copy(node.meta) + env[node] = new_node + + for node in bwd_graph.nodes: + if node.op == "placeholder": + assert node in env + else: + env[node] = new_graph.node_copy(node, lambda x: env[x]) + env[node].meta = copy.copy(node.meta) + + new_graph.lint() + return fwd_graph, new_graph + + def prepare_fn_vt( + self, + tx: "InstructionTranslator", + ctx: "AutogradFunctionContextVariable", + method_name: str, + args: Sequence[VariableTracker], + ) -> tuple[VariableTracker, Sequence[VariableTracker]]: + from . import UserMethodVariable + + source = None + if self.parent_source: + source = AttrSource(self.parent_source, member=method_name) + + if method_name == "forward": + fn = self.fwd_fn + else: + fn = self.bwd_fn + + fn_vt, fn_args = None, None + if isinstance(fn, types.FunctionType): + fn_vt = VariableTracker.build(tx, fn, source=source) + fn_args = [ctx, *args] + elif isinstance(fn, types.MethodType): + cls_vt = VariableTracker.build(tx, fn.__class__) + fn_vt = UserMethodVariable( + fn.__func__, + cls_vt, + source=source, + ) + fn_args = [cls_vt, ctx, *args] + else: + unimplemented( + gb_type="autograd.Function.apply: non-function or method forward", + context=str(fn), + explanation=f"Expected {method_name} to be a function or method.", + hints=[], + ) + assert fn_vt is not None and fn_args is not None + return fn_vt, fn_args + + +def _get_fake_value(x: Union[VariableTracker, Proxy, "FakeTensor"]) -> "FakeTensor": + if isinstance(x, variables.VariableTracker): + return x.as_proxy().node.meta["example_value"] + elif isinstance(x, torch.fx.Proxy): + return x.node.meta["example_value"] + else: + return x + + +def maybe_positional_arg_names(func: VariableTracker) -> list[str] | None: + result = [] + if not hasattr(func, "get_function"): + return None + try: + fn = func.get_function() + except (Unsupported, NotImplementedError): + return None + try: + sig = inspect.signature(fn) + except ValueError: + return None + for name, param in sig.parameters.items(): + if param.kind is inspect.Parameter.VAR_POSITIONAL: + return None + if ( + param.kind is inspect.Parameter.POSITIONAL_ONLY + or param.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD + ): + if name == "self": + # FX graphs can't have a placeholder named self + result.append("self_") + else: + result.append(name) + return result + + +class BaseHOPVariable(WrapHigherOrderVariable): + # Generic fallback for BaseHOP instances not explicitly mapped + # The actual HOP name comes from self.value._name at runtime + _HOP_NAME = "base HOP (name not yet determined)" + supports_input_mutation = False + supports_aliasing = False + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self._HOP_NAME = self.value._name + + def python_type(self) -> type: + return type(self.value) + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + ( + p_args, + p_kwargs, + example_value, + body_r, + _, + _, + body_graph_output_vts, + _, + ) = self.create_wrapped_node( + tx, args[0], args[1:], {}, self.value._name, subgraph_name="subgraph" + ) + assert len(p_kwargs) == 0 + + p_kwargs = {key: value.as_proxy() for key, value in kwargs.items()} + return _call_function_with_auto_output_flattening( # type: ignore[return-value] + tx, + self.value, + p_args, + p_kwargs, + example_value, + body_r, + body_graph_output_vts, + ) + + +class LocalMapWrappedHigherOrderVariable(WrapHigherOrderVariable): + _HOP_NAME = "torch.ops.higher_order.local_map_hop" + supports_input_mutation = False + supports_aliasing = False + + # Subclasses aren't supported by speculate_subgraph yet + # So this HOP is only usable with plain tensors + _enabled = False + + @classmethod + @contextlib.contextmanager + def enable(cls) -> Generator[None, None, None]: + """Context manager to temporarily enable local map wrapping. + Will be removed when speculate_subgraph supports subclass inputs: + https://github.com/pytorch/pytorch/issues/161456. + + Usage: + with LocalMapWrappedHigherOrderVariable.enable_wrapping(): + # Code where should_wrap_in_hop will return True + pass + """ + old_value = cls._enabled + cls._enabled = True + try: + yield + finally: + cls._enabled = old_value + + @classmethod + def should_wrap_in_hop(cls, value: Any) -> bool: + if not torch.distributed.is_available(): + return False + + from torch.distributed.tensor.experimental._func_map import _local_map_wrapped + + # check is important to avoid subclass dispatch + if type(value) is not type(_local_map_wrapped): + return False + + return value is _local_map_wrapped and cls._enabled + + @staticmethod + # pyrefly: ignore[bad-override] + def build(**options: Any) -> "TorchHigherOrderOperatorVariable": + return TorchHigherOrderOperatorVariable.make( + torch._higher_order_ops.local_map_hop, + **options, + ) + + def python_type(self) -> type: + return type(self.value) + + def _call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + """ + Goal of this function is to rewrite local_map usage as a HOP: + local_map(func, ...) -> local_map_hop(gm, ...) + """ + + ( + user_func, + out_placements, + in_placements, + in_grad_placements, + device_mesh, + redistribute_inputs, + *user_args, + ) = args + + # None placements are used to pass non-Tensors into the local_map function. + # Containers passed this way can not hold tensors. Thus, Dynamo would have inlined + # into them, and we handle None placements by assuming they will be desugared away. + # This will need to be adjusted for dynamic shapes support. + def check_none_last(placements: Sequence[Any | None]) -> int: + seen_none = 0 + for p in placements: + if p is None: + seen_none += 1 + else: + assert seen_none == 0, ( + "Tracing local_map is only currently supported with None placements last." + ) + return seen_none + + inputs_none_placements = check_none_last(in_placements.value) # type: ignore[attr-defined] + output_none_placements = check_none_last(out_placements.value) # type: ignore[attr-defined] + + local_map_kwargs = { + "out_placements": out_placements.value, # type: ignore[attr-defined] + "in_placements": in_placements.value, # type: ignore[attr-defined] + "redistribute_inputs": redistribute_inputs.value, # type: ignore[attr-defined] + "in_grad_placements": in_grad_placements.value, # type: ignore[attr-defined] + "device_mesh": device_mesh.value, # type: ignore[attr-defined] + } + assert local_map_kwargs["device_mesh"] is not None, ( + "Not yet implemented, please manually provide a device_mesh to local_map." + ) + mesh = local_map_kwargs["device_mesh"] + + # For Autoparallel, the initial trace is done with global shapes, then we decide model weights sharding, + # and reuse the graph. Since the sharding decision is after the initial trace, we can't trace with local shapes. + # For local_map however, since we specify all placements, we can trace with local shapes. + + # Step 1: Validate the annotated function matches the input_placements (i.e. that it can run in eager) + template = ( + "Expecting {expected} {inputs_or_outputs} to local_map function based on placements" + ", but found {actual}. Please ensure the count matches for eager. " + ) + assert len(in_placements.value) == len(user_args), template.format( # type: ignore[attr-defined] + expected=len(in_placements.value), # type: ignore[attr-defined] + inputs_or_outputs="inputs", + actual=len(user_args), + ) + + from torch._higher_order_ops.local_map import ( + redistribute_fw_inputs, + redistribute_fw_outputs, + ) + + # Step 2: Convert inputs to local shapes + priors = {} + for placements, vt in zip(in_placements.value, user_args): # type: ignore[attr-defined] + if isinstance(vt, variables.lazy.LazyVariableTracker): + vt = variables.lazy.LazyVariableTracker.realize_all(vt) + + if not vt.is_tensor(): + assert placements is None + continue + + global_tensor = vt.as_proxy().node.meta["example_value"] + # NOTE: We don't support local_map region relying on exact grad_fn information + # This is okay since accessing grad_fn is a graph break. + local_tensor = redistribute_fw_inputs( + (global_tensor,), + (placements,), + mesh, + ) + local_tensor = local_tensor[0] + + priors[vt] = global_tensor + vt.as_proxy().node.meta["example_value"] = local_tensor + # pyrefly: ignore [missing-attribute] + vt.synchronize_attributes(tx) + + # Step 3: Trace local_map subgraph with local tensors + ( + p_args, + p_kwargs, + example_value, + body_r, + body_gmod, + body_name, + body_graph_output_vts, + _, + ) = self.create_wrapped_node( + tx, user_func, user_args, kwargs, self.value._name, subgraph_name="subgraph" + ) + + # Step 4: Validate traced graph signature still matches placement information + expected_num_inputs = len(in_placements.value) - inputs_none_placements # type: ignore[attr-defined] + actual_num_inputs = len(body_gmod.graph.find_nodes(op="placeholder")) + expected_num_outputs = len(out_placements.value) - output_none_placements # type: ignore[attr-defined] + assert len(body_gmod.graph.find_nodes(op="output")) == 1 + actual_num_outputs = len(body_gmod.graph.find_nodes(op="output")[0].args[0]) + + template = ( + "Expecting {expected} {inputs_or_outputs} to local_map function based on placements" + ", but found {actual}. If the count matches for eager, " + "Dynamo may have flattened {inputs_or_outputs} to the function or found additional " + "tensors used via closures. " + "Please adjust the input placements to match what the traced graph sees: \n{gm_str}." + ) + + def make_error_msg(*args: Any) -> str: + expected_num, actual_num, inputs_or_outputs = args + gm_str = body_gmod.print_readable(print_output=False) + return template.format( + expected=expected_num, + inputs_or_outputs=inputs_or_outputs, + actual=actual_num, + gm_str=gm_str, + ) + + if expected_num_inputs != actual_num_inputs: + raise AssertionError( + make_error_msg(expected_num_inputs, actual_num_inputs, "inputs") + ) + if expected_num_outputs != actual_num_outputs: + raise AssertionError( + make_error_msg(expected_num_outputs, actual_num_outputs, "outputs") + ) + + if inputs_none_placements > 0: + expected_input_nodes = [ + arg.as_proxy().node for arg in user_args[:-inputs_none_placements] + ] + else: + expected_input_nodes = [arg.as_proxy().node for arg in user_args] + actual_input_nodes = [proxy.node for proxy in p_args] + assert actual_input_nodes[0].op == "get_attr" + assert "subgraph" in actual_input_nodes[0].target # type: ignore[attr-defined] + assert len(expected_input_nodes) == len(actual_input_nodes) - 1 + for expected_order, actual_order in zip( + expected_input_nodes, actual_input_nodes[1:] + ): + assert expected_order == actual_order, ( + "Dynamo changed the order of inputs to the local_map function, please adjust " + f"the order of inputs and input_placements from {expected_input_nodes}, to: {actual_input_nodes[1:]}" + ) + assert len(p_kwargs) == 0 + + # Step 5: Install local_map subgraph + p_kwargs = {key: value.as_proxy() for key, value in kwargs.items()} + out = _call_function_with_auto_output_flattening( + tx, + self.value, + p_args, + p_kwargs, + example_value, + body_r, + body_graph_output_vts, + ) + + # Step 6: Restore inputs and outputs to global shapes + for vt, global_tensor in priors.items(): + vt.as_proxy().node.meta["example_value"] = global_tensor + # pyrefly: ignore [missing-attribute] + vt.synchronize_attributes(tx) + + outs = out.items if isinstance(out, TupleVariable) else [out] + assert len(outs) == len(out_placements.value) # type: ignore[attr-defined] + for placements, vt in zip(out_placements.value, outs): # type: ignore[attr-defined] + if not vt.is_tensor(): # type: ignore[attr-defined] + assert placements is None + continue + + local_tensor = vt.as_proxy().node.meta["example_value"] # type: ignore[attr-defined] + + # NOTE: We don't support code after the local_map region relying on exact grad_fn information + # This is okay since accessing grad_fn is a graph break. + global_tensor = redistribute_fw_outputs( + (local_tensor,), + (placements,), + mesh, + num_activations=0, # this is not the joint + ) + global_tensor = global_tensor[0] + + vt.as_proxy().node.meta["example_value"] = global_tensor # type: ignore[attr-defined] + vt.synchronize_attributes(tx) # type: ignore[attr-defined] + + # TODO: Figure out how to handle output order diverging from eager + + # Treat as const, so we don't have to deal with Placement types in fx IR + # Guarded with EQUALS_MATCH on local_map call's arguments + body_gmod.meta["local_map_kwargs"] = { + "out_placements": out_placements.value[:expected_num_outputs], # type: ignore[attr-defined] + "in_placements": in_placements.value[:expected_num_inputs], # type: ignore[attr-defined] + "redistribute_inputs": redistribute_inputs.value, # type: ignore[attr-defined] + "in_grad_placements": in_grad_placements.value, # type: ignore[attr-defined] + "device_mesh": device_mesh.value, # type: ignore[attr-defined] + } + assert out is not None + return out + + +from .invoke_subgraph import InvokeSubgraphHigherOrderVariable # noqa: E402 + + +# Map operator names to their corresponding variable for fast TorchHigherOrderOperatorVariable.make() +_hop_name_to_variable_class = { + "cond": CondHigherOrderVariable, + "while_loop": WhileLoopHigherOrderVariable, + "while_loop_stack_output": WhileLoopStackOutputHigherOrderVariable, + "map_impl": MapHigherOrderVariable, + "executorch_call_delegate": ExecutorchCallDelegateHigherOrderVariable, + "out_dtype": OutDtypeHigherOrderVariable, + "wrap": WrapHigherOrderVariable, + "hints_wrapper": HintsWrapperHigherOrderVariable, + "flex_attention": FlexAttentionHigherOrderVariable, + "flex_attention_backward": FlexAttentionBackwardHighOrderVariable, + "wrap_activation_checkpoint": CheckpointHigherOrderVariable, + "tag_activation_checkpoint": CheckpointHigherOrderVariable, + "_export_tracepoint": ExportTracepointHigherOrderVariable, + "trace_wrapped": TraceWrappedHigherOrderOperatorVariable, + "strict_mode": StrictModeHigherOrderVariable, + "run_with_rng_state": RunWithRNGStateHigherOrderVariable, + "associative_scan": AssociativeScanHigherOrderVariable, + "scan": ScanHigherOrderVariable, + "call_torchbind": CallTorchbindHigherOrderVariable, + "print": PrintHigherOrderVariable, + "wrap_with_set_grad_enabled": WrapWithSetGradEnabledHigherOrderVariable, + "wrap_with_autocast": WrapWithAutocastHigherOrderVariable, + "dynamo_bypassing_wrapper": DynamoBypassingWrapperHigherOrderVariable, + "auto_functionalized": AutoFunctionalizeHigherOrderVariable, + "auto_functionalized_v2": AutoFunctionalizeHigherOrderVariable, + "inline_asm_elementwise": InlineAsmElementwiseHigherOrderVariable, + "invoke_subgraph": InvokeSubgraphHigherOrderVariable, + "custom_function_call": CustomFunctionHigherOrderOperatorVariable, + "local_map_hop": LocalMapWrappedHigherOrderVariable, +} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/invoke_subgraph.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/invoke_subgraph.py new file mode 100644 index 0000000000000000000000000000000000000000..0c135b3060d294edb25927b493f65acbada0bce8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/invoke_subgraph.py @@ -0,0 +1,1344 @@ +""" +This module contains the InvokeSubgraphHigherOrderVariable class and its +supporting helpers for subgraph reuse (auto-cache) in Dynamo's invoke_subgraph +higher-order operator. +""" + +import enum +import logging +import traceback +import types +from dataclasses import dataclass +from typing import Any, cast, NamedTuple, TYPE_CHECKING + +import torch +import torch._higher_order_ops +from torch._dynamo import graph_break_hints +from torch._dynamo.exc import unimplemented +from torch._dynamo.guards import ( + extract_tensor_metadata, + GUARD_VALUE_DISPATCH, + GuardCheckSpec, + SKIP_GUARD, + UnsupportedGuardCheckSpec, +) +from torch._dynamo.source import SyntheticLocalSource +from torch._dynamo.variables.base import VariableTracker +from torch._dynamo.variables.constant import ConstantVariable +from torch._dynamo.variables.functions import UserFunctionVariable +from torch._dynamo.variables.higher_order_ops import WrapHigherOrderVariable +from torch._dynamo.variables.lists import ListVariable, TupleVariable +from torch._dynamo.variables.nn_module import UnspecializedNNModuleVariable +from torch._dynamo.variables.tensor import SymNodeVariable, TensorVariable +from torch._guards import ( + Guard, + InvokeSubgraphReuseCondition, + InvokeSubgraphReuseEntry, + Source, +) +from torch._higher_order_ops.invoke_subgraph import NestedCompileRegionOptions +from torch.fx.graph_module import GraphModule +from torch.fx.proxy import Proxy +from torch.utils import _pytree as pytree +from torch.utils._ordered_set import OrderedSet + + +if TYPE_CHECKING: + from collections.abc import Sequence + + from torch._dynamo.symbolic_convert import InstructionTranslator + from torch._dynamo.variables.higher_order_ops import SubgraphTracingInfo + +log = logging.getLogger(__name__) +hc_log = torch._logging.getArtifactLogger(__name__, "hierarchical_compile") + +# Note: [invoke_subgraph subgraph reuse] +# +# When mark_compile_region wraps a function called N times (e.g. 80 identical +# transformer layers), Dynamo traces the subgraph once and stamps out cached +# copies for subsequent calls. It does safety checks to ensure that a subgraph +# is reusable, if not (e.g. side-effect), it will fallback to tracing the +# next invocation. +# +# HIGH-LEVEL FLOW +# =============== +# User code: model.layers[0](x), model.layers[1](x), ..., model.layers[79](x) +# | | | +# v v v +# +--------------+ +--------------+ +--------------+ +# | First Call | | Second Call | ... | 80th Call | +# +------+-------+ +------+-------+ +------+-------+ +# | | | +# v v v +# +--------------+ +--------------+ +--------------+ +# | Full subgraph| | Cache lookup | | Cache lookup | +# | trace | | (is_reusable) | | (is_reusable) | +# +------+-------+ +------+-------+ +------+-------+ +# | | | +# v v v +# +--------------+ +--------------+ +--------------+ +# | save_reuse_ | | stamp_out_ | | stamp_out_ | +# | entry() | | subgraph() | | subgraph() | +# +--------------+ +--------------+ +--------------+ +# +# WHAT GETS CACHED +# ================ +# After the first trace, save_reuse_entry stores an InvokeSubgraphReuseEntry +# (in _guards.py) containing: +# - body_name/body_gmod: the traced subgraph +# - arg_sources: sources of the original call's arguments +# - subgraph_input_mapping: how each lifted arg maps back to user inputs or captures +# - output_metadata: shape/stride/dtype/device of outputs +# +# Paired with an InvokeSubgraphReuseCondition containing: +# - input_checks: (tag, tensor_metadata) per input +# - guards: (source, handler, expected, guard) tuples +# - treespec: pytree structure of the args +# - traced_sources: sources accessed during the trace +# +# CACHE LOOKUP (is_reusable) +# ========================== +# On subsequent calls: +# 1. Input structure match -- same treespec, tags, tensor metadata. +# 2. Source replacement -- clone each guard's source with a replacement map +# (old: L['self'].layers[0].weight -> new: L['self'].layers[1].weight), +# then evaluate against the new source's runtime value. +# 3. Mutation check -- reject if the subgraph mutated any captured var. +# +# A shared resolve_cache memoizes intermediate source resolution (e.g. +# L['self'].layers is evaluated once and reused across all guards). +# +# STAMP OUT (stamp_out_subgraph) +# ============================== +# On cache hit, reconstruct the argument list using the freevar mapping +# (list[LiftedArgOrigin]): +# +# LiftedUserArg(index) +# User arg (activation / explicit input). +# Looked up from new call's flat proxies. +# +# LiftedCapturedSource(source) +# Sourceful captured var (weight, param, etc). +# Source is cloned with replacement map, resolved via +# VariableBuilder. Deduplicates via input_source_to_var. +# +# LiftedSyntheticObject(ctor_fn, ctor_args, ctor_arg_sources) +# Synthetic object (opaque type with SyntheticLocalSource). +# Reconstructed via synthetic_graph_input with cached constructor info. +# +# SAFETY +# ====== +# In normal Dynamo compilation, safety is enforced at runtime: guards are +# installed during tracing and re-evaluated on every subsequent call against +# real Python objects. Subgraph reuse operates differently — we are in the +# middle of tracing, there are no real Python objects, only VariableTrackers. +# We must answer: what could cause the second invocation of a nested compile +# region to produce a different trace than the first? +# +# VariableTrackers fall into two categories: +# +# 1. Intermediates — values produced during tracing with no originating source +# (e.g. the result of a prior FX op). These can reach a nested compile region +# only via (a) the region's explicit function arguments, or (b) closure +# capture. We do not support nested-function regions that close over tensors, +# so only (a) applies. For explicit arguments, the set of types that can +# appear is small and well-defined: TensorVariable, SymNodeVariable, +# ConstantVariable, and NNModuleVariable. Each has a cheap structural +# comparison (tensor metadata, symnode identity, constant value equality). +# We also snapshot the pytree treespec of the argument list and verify it +# matches on lookup, ensuring the flattened structure is identical. +# +# 2. Sourceful variables — values with a known originating source (e.g. a +# module attribute or a local variable visible in the outer frame). For these +# we collect the guard delta from the first trace, parameterize the guard +# sources by replacing the original arg sources with the new arg sources, and +# re-evaluate the guards by resolving each source against the live f_locals / +# f_globals. The one extra hazard here is mutation: if the outer trace +# mutates a sourceful object between the first and second invocations, the +# cached guards would evaluate against stale values. We therefore also check +# that none of the sources read by the cached subgraph have been mutated in +# the outer SideEffects tracker before accepting a reuse. +# +# - max_reuse_entries (default 8, configurable via nested_compile_region arg) +# caps cache entries per function. Exceeding it raises RuntimeError. +# - Guard failures logged with guard type + user stack trace. +# Enable: TORCH_LOGS='+hierarchical_compile' +# --------------------------------------------------------------------------- +# Auto-cache helpers for invoke_subgraph +# --------------------------------------------------------------------------- + + +class InputTag(enum.Enum): + TENSOR = "tensor" + SYMNODE = "symnode" + CONSTANT = "constant" + MODULE = "module" + + +class InputFingerprint(NamedTuple): + # (InputTag, VariableTracker) pairs for each leaf input. + flat_vts: list[tuple[InputTag, VariableTracker]] + # 1-1 mapping to flat_vts: source for each leaf, or None if the VT has no source. + arg_sources: list[Source | None] + # True if any leaf VT had an unsupported type for reuse. + has_unknown: bool = False + # TreeSpec from pytree.tree_flatten of the (args, kwargs) structure. + treespec: pytree.TreeSpec | None = None + + +def classify_vt(vt: Any) -> InputTag | None: + """Return the tag for a leaf VT, or None if unsupported.""" + if isinstance(vt, TensorVariable): + return InputTag.TENSOR + elif isinstance(vt, SymNodeVariable): + return InputTag.SYMNODE + elif isinstance(vt, ConstantVariable): + return InputTag.CONSTANT + elif isinstance(vt, UnspecializedNNModuleVariable): + return InputTag.MODULE + return None + + +def build_input_fingerprint( + tx: "InstructionTranslator", + fn_args_vt: Any, + kwargs: dict[str, Any], +) -> InputFingerprint: + """Build an InputFingerprint by flattening (args, kwargs) via pytree. + + Uses _make_inlined(tx, pytree.tree_flatten) to recursively flatten + the argument structure into leaf VTs, classifying each leaf as + tensor/symnode/constant/module. Also records the TreeSpec so that + cache lookups can verify structural equivalence. + + Fast path: when kwargs is empty and all args are already leaf VTs + (tensor/symnode/constant/module), skip the expensive pytree flatten. + """ + # Fast path: flat args, no kwargs — skip pytree machinery. + if not kwargs: + all_leaf = True + for vt in fn_args_vt: + if classify_vt(vt) is None: + all_leaf = False + break + if all_leaf: + return build_fingerprint_fast(fn_args_vt) + + return build_fingerprint_with_pytree(tx, fn_args_vt, kwargs) + + +def build_fingerprint_fast(fn_args_vt: Any) -> InputFingerprint: + """Build fingerprint for the common case of flat leaf args, no kwargs.""" + flat_vts: list[tuple[InputTag, VariableTracker]] = [] + arg_sources: list[Source | None] = [] + for vt in fn_args_vt: + tag = classify_vt(vt) + assert tag is not None + flat_vts.append((tag, vt)) + # Always append (even None) to keep positional alignment with flat_vts + # so that source_replacement zip pairing is correct across calls. + arg_sources.append(getattr(vt, "source", None)) + return InputFingerprint(flat_vts, arg_sources) + + +def build_fingerprint_with_pytree( + tx: "InstructionTranslator", + fn_args_vt: Any, + kwargs: dict[str, Any], +) -> InputFingerprint: + """Build fingerprint via pytree flatten for nested/kwargs cases.""" + from torch._dynamo.variables.builder import SourcelessBuilder + from torch._dynamo.variables.higher_order_ops import _make_inlined + + container_vt = SourcelessBuilder.create(tx, (list(fn_args_vt), kwargs)) + flat_list_vt, treespec_vt = _make_inlined(tx, pytree.tree_flatten)( + container_vt + ).unpack_var_sequence(tx) + treespec = treespec_vt.as_python_constant() + + flat_vts: list[tuple[InputTag, VariableTracker]] = [] + arg_sources: list[Source | None] = [] + has_unknown = False + + for vt in flat_list_vt.unpack_var_sequence(tx): + tag = classify_vt(vt) + if tag is not None: + flat_vts.append((tag, vt)) + else: + has_unknown = True + continue + + # Always append (even None) to keep positional alignment with flat_vts. + arg_sources.append(getattr(vt, "source", None)) + + return InputFingerprint(flat_vts, arg_sources, has_unknown, treespec) + + +def get_flat_proxies(fingerprint: InputFingerprint) -> list[Proxy]: + """Collect deduplicated proxies from tensor/symnode leaves.""" + seen: set[torch.fx.Node] = set() + flat_proxies: list[Proxy] = [] + for tag, vt in fingerprint.flat_vts: + if tag in (InputTag.TENSOR, InputTag.SYMNODE): + proxy = vt.as_proxy() + if proxy.node not in seen: + seen.add(proxy.node) + flat_proxies.append(proxy) + return flat_proxies + + +@dataclass +class LiftedUserArg: + """Lifted arg that came from a user argument (intermediate activation or explicit input).""" + + index: int + + +@dataclass +class LiftedCapturedSource: + """Lifted arg that is a captured variable (e.g. a weight or parameter) with a Source.""" + + source: Any # Source + + +@dataclass +class LiftedSyntheticObject: + """Lifted arg that is a TorchScriptObject with a SyntheticLocalSource.""" + + ctor_fn: Any # Callable + ctor_args: tuple[Any, ...] + ctor_arg_sources: tuple[Any, ...] | None + + +@dataclass +class LiftedBoundSymbol: + """Lifted arg that is a SymInt already bound as a graph input. + + SymInt graph inputs are created during tensor wrapping (not through + VariableBuilder.wrap_symint), so they aren't registered in + unspec_variable_map or variable_tracker_cache. Using LiftedCapturedSource + for these would resolve the source to a concrete Python int via + source.get_value() instead of reusing the existing symbolic proxy. + """ + + expr: Any # sympy.Expr + + +LiftedArgOrigin = ( + LiftedUserArg | LiftedCapturedSource | LiftedSyntheticObject | LiftedBoundSymbol +) + + +def get_fn_code(fn_var: Any) -> types.CodeType | None: + if isinstance(fn_var, UserFunctionVariable): + return fn_var.get_function().__code__ + elif isinstance(fn_var, UnspecializedNNModuleVariable): + return ( + fn_var.value.forward.__func__.__code__ # pyrefly: ignore[missing-attribute] + ) + return None + + +def has_mutated_vars( + tx: "InstructionTranslator", + traced_sources: OrderedSet[Source], +) -> bool: + """Check if any source accessed by the subgraph has been mutated. + + SideEffects.mutated_sources records the exact AttrSource for every + store_attr call. A simple set intersection with traced_sources tells + us whether any source the subgraph read was later written to. + """ + overlap = tx.output.side_effects.mutated_sources & traced_sources + if overlap: + hc_log.debug( + "subgraph_reuse: mutated sources detected -- %s", + overlap, + ) + return True + return False + + +def is_reuse_eligible( + tx: "InstructionTranslator", + body_r: Any, + fingerprint: InputFingerprint, + tracing_info: "SubgraphTracingInfo", + traced_sources: OrderedSet[Source] | None = None, + has_reuse_hash_fn: bool = False, +) -> bool: + """Best-effort check for whether a traced subgraph result can be reused. + + It is possible that a subgraph is morally reusable but does not fall + into the limited support that Dynamo has today. Current limitations: + - The subgraph must not have side effects. + - No sourceful variable accessed by the subgraph may have been + mutated, because guards are snapshotted on source values at trace + time — if the underlying object changed since then, the cached + guards would silently evaluate against stale values. + - Output must be a single tensor, or a tuple/list of plain tensors. + - All flattened inputs must be one of: tensor, symnode, constant, + unspecialized NN module — for sourceless or other input types we + rely on the treespec and tags for structural matching, so only + types with well-defined comparison semantics are supported. + + When ``has_reuse_hash_fn`` is True, side-effect and mutation checks are + skipped because the hash key replaces guards — there are no guards to + go stale from mutations. + """ + if not has_reuse_hash_fn: + if tracing_info.side_effect_stack is not None: + stack_msg = "\n" + "".join( + traceback.format_list(tracing_info.side_effect_stack) + ) + hc_log.debug( + "subgraph_reuse: not eligible -- subgraph has side effects%s", + stack_msg, + ) + return False + + if traced_sources and has_mutated_vars(tx, traced_sources): + return False + + if isinstance(body_r, TensorVariable): + pass + elif isinstance(body_r, (TupleVariable, ListVariable)): + non_tensor = [ + type(item).__name__ + for item in body_r.items + if not isinstance(item, TensorVariable) + ] + if non_tensor: + hc_log.debug( + "subgraph_reuse: not eligible -- output contains non-tensor types: %s", + non_tensor, + ) + return False + else: + hc_log.debug( + "subgraph_reuse: not eligible -- output type %s is not tensor or tuple/list", + type(body_r).__name__, + ) + return False + + if fingerprint.has_unknown: + hc_log.debug( + "subgraph_reuse: not eligible -- unsupported input VT types", + ) + return False + + return True + + +def build_reuse_condition( + tx: "InstructionTranslator", + fingerprint: InputFingerprint, + traced_sources: OrderedSet[Source], +) -> InvokeSubgraphReuseCondition | None: + """Build an InvokeSubgraphReuseCondition from a traced subgraph. + + A reuse condition is a mix of two kinds of checks: + + 1. **Input tag checks** (from flat_vts): For each flattened leaf VT, + we record its tag (_VtTag.TENSOR/SYMNODE/CONSTANT/MODULE) and + metadata (e.g. tensor shape/stride/dtype/device/requires_grad). + At lookup time, the treespec ensures structural equivalence, and + then we compare tags and metadata leaf-by-leaf. + + 2. **Guard checks** (from traced_sources): During the subgraph trace, + every source accessed via VariableBuilder is recorded. We look up + all guards installed on those sources (and on the arg_sources) to + build the set of guards that must be re-evaluated on cache hit. + This is more robust than guard diffing because it catches guards + that were already installed before the subgraph trace began. + + Raise if any guard type is unsupported, as a feedback for compiler + developers to support that guard type. + """ + from torch._guards import InvokeSubgraphReuseCondition + + input_checks: list[tuple[InputTag, object]] = [] + for tag, vt in fingerprint.flat_vts: + if tag == InputTag.TENSOR: + assert isinstance(vt, TensorVariable) + example = vt.proxy.node.meta.get("example_value", None) + if example is None: + hc_log.debug( + "subgraph_reuse: cannot build condition -- tensor input has no example_value" + ) + return None + input_checks.append((InputTag.TENSOR, extract_tensor_metadata(example))) + elif tag == InputTag.SYMNODE: + assert isinstance(vt, SymNodeVariable) + # Store the SymInt/SymFloat/SymBool object itself. Two accesses to + # the same symbolic dimension (e.g. x.shape[0] twice) produce the + # same Python object, so identity comparison in is_reusable is + # correct and avoids false matches between distinct symbols. + input_checks.append((InputTag.SYMNODE, vt.sym_num)) + elif tag == InputTag.CONSTANT: + assert isinstance(vt, ConstantVariable) + input_checks.append((InputTag.CONSTANT, vt.value)) + elif tag == InputTag.MODULE: + input_checks.append((InputTag.MODULE, None)) + else: + raise AssertionError( + f"Unexpected input tag '{tag}' for {type(vt).__name__} -- " + f"is_reuse_eligible should have rejected this" + ) + + # Collect all guards for sources accessed during the subgraph trace + # and for the flattened arg sources. + all_sources = set(traced_sources) + all_sources.update(s for s in fingerprint.arg_sources if s is not None) + all_relevant_guards: set[Guard] = set() + for source in all_sources: + all_relevant_guards.update(tx.output.guards.get_guards_for_source(source)) + + guard_tuples: list[tuple[Source, GuardCheckSpec, object, Guard]] = [] + for guard in all_relevant_guards: + source = guard.originating_source + type_str = guard.create_fn_name() + handler = GUARD_VALUE_DISPATCH.get(type_str) + + if handler is SKIP_GUARD: + continue + + if handler is None or isinstance(handler, UnsupportedGuardCheckSpec): + raise RuntimeError( + f"subgraph_reuse: unsupported guard type '{type_str}' on source '{source.name}'" + ) + + try: + value = tx.output.resolve_source_value(source) + except Exception: + raise RuntimeError( + f"subgraph_reuse: failed to resolve source '{source.name}' for {type_str} guard" + ) from None + + # TODO(anijain2305): vLLM workaround -- skip CONSTANT_MATCH on + # strings. Re-evaluate once vLLM migrates off this pattern. + # if type_str == "CONSTANT_MATCH" and isinstance(value, str): + # continue + + handler = cast(GuardCheckSpec, handler) + expected = handler.get_metadata_fn(guard, value) + guard_tuples.append((source, handler, expected, guard)) + + hc_log.debug("Number of guards %s", len(guard_tuples)) + + return InvokeSubgraphReuseCondition( + input_checks=input_checks, + guards=guard_tuples, + treespec=fingerprint.treespec, + traced_sources=traced_sources, + ) + + +def build_source_replacement( + old_arg_sources: list[Source | None], + new_arg_sources: list[Source | None], +) -> dict[Source, Source]: + """Map old arg sources to new arg sources for remapping captured variable sources.""" + return { + old: new + for old, new in zip(old_arg_sources, new_arg_sources) + if old is not None and new is not None and old != new + } + + +def is_reusable( + tx: "InstructionTranslator", + condition: "InvokeSubgraphReuseCondition", + fingerprint: InputFingerprint, + cached_entry: InvokeSubgraphReuseEntry, +) -> bool: + """Check if a cached subgraph can be reused for the current call. + + Three-phase check: + (1) Verify that intermediates (tensor metadata, symnode types, constant + values) match the cached input_checks — these are lightweight + structural comparisons that don't require source resolution. + (2) Check for mutations on the remapped traced_sources — if any source + the subgraph read has been mutated since the original trace, the + cached guards would evaluate against stale values. + (3) Build a source replacement mapping (old sources → new sources) and + re-evaluate the snapshotted guards under the new sources. + """ + # Structural check: treespec must match first. + if condition.treespec is not None and fingerprint.treespec != condition.treespec: + hc_log.debug( + "subgraph_reuse: reuse failed -- treespec mismatch", + ) + return False + + # Input count, tags, and metadata must match. + # Tensor metadata (shape, stride, dtype, device, requires_grad) is checked + # here because TENSOR_MATCH guards for subgraph inputs typically already + # exist in the outer graph before tracing and thus won't appear in the + # guard delta. + if len(condition.input_checks) != len(fingerprint.flat_vts): + hc_log.debug( + "subgraph_reuse: reuse failed -- input count mismatch: cached %d vs current %d", + len(condition.input_checks), + len(fingerprint.flat_vts), + ) + return False + + for i, ((cached_tag, cached_val), (cur_tag, cur_vt)) in enumerate( + zip(condition.input_checks, fingerprint.flat_vts) + ): + if cached_tag != cur_tag: + hc_log.debug( + "subgraph_reuse: reuse failed -- input %d tag mismatch: cached '%s' vs current '%s'", + i, + cached_tag, + cur_tag, + ) + return False + if cached_tag == InputTag.TENSOR: + assert isinstance(cur_vt, TensorVariable) + example = cur_vt.proxy.node.meta.get("example_value", None) + if example is None: + hc_log.debug( + "subgraph_reuse: reuse failed -- input %d tensor has no example_value", + i, + ) + return False + cur_meta = extract_tensor_metadata(example) + if cur_meta != cached_val: + hc_log.debug( + "subgraph_reuse: reuse failed -- input %d tensor metadata mismatch", + i, + ) + return False + elif cached_tag == InputTag.SYMNODE: + assert isinstance(cur_vt, SymNodeVariable) + if cur_vt.sym_num is not cached_val: + return False + elif cached_tag == InputTag.CONSTANT: + assert isinstance(cur_vt, ConstantVariable) + if cur_vt.value != cached_val: + # If both the cached and current arg have sources, source + # replacement in stamp_out will resolve the correct value. + cached_src = ( + cached_entry.arg_sources[i] + if i < len(cached_entry.arg_sources) + else None + ) + new_src = ( + fingerprint.arg_sources[i] + if i < len(fingerprint.arg_sources) + else None + ) + if cached_src is None or new_src is None: + return False + + source_replacement = build_source_replacement( + cached_entry.arg_sources, fingerprint.arg_sources + ) + + # Parameterized source - this function gives you new sources parameterized + # on the arg_sources. For example, if the input to the nested compile region + # is a nn Module layer with source `layers[0]`, then old source + # `layers[0].weight` gets remapped to `layers[1].weight`. This + # parameterization is central in getting the new sources and then running + # guards on them. + def replacement_fn(s: Source) -> Source: + return source_replacement.get(s, s) + + # Check for mutations on remapped traced_sources. + if source_replacement: + remapped = OrderedSet(s.clone(replacement_fn) for s in condition.traced_sources) + else: + remapped = condition.traced_sources + if has_mutated_vars(tx, remapped): + return False + + # If no sources changed, all guards were already checked during the + # original trace and will trivially pass again. + if not source_replacement: + return True + + # Shared resolution context so source.get_value memoizes intermediate + # results (e.g. common base sources) across all guards in this check. + resolve_globals: dict[str, Any] = { + "G": tx.output.root_tx.f_globals, + "L": tx.output.root_tx.f_locals, + } + resolve_locals: dict[str, Any] = {} + resolve_cache: dict[Source, Any] = {} + + for source, handler, expected, guard in condition.guards: + new_source = source.clone(replacement_fn) + # Source unchanged after replacement — guard already passed during + # the original trace, skip re-evaluation. + if new_source == source: + continue + + try: + value = new_source.get_value(resolve_globals, resolve_locals, resolve_cache) + except Exception: + hc_log.debug( + "subgraph_reuse: reuse failed -- cannot resolve source\n" + " guard type: %s\n" + " guard source: %s\n" + " guard source name: %s\n" + " user stack:\n%s", + guard.create_fn_name(), + new_source, + new_source.name, + "".join(guard.user_stack.format()) + if guard.user_stack + else "", + ) + return False + + if not handler.eval_fn(value, expected): + hc_log.debug( + "subgraph_reuse: reuse failed --\n" + " guard type: %s\n" + " guard source: %s\n" + " guard source name: %s\n" + " expected: %s\n" + " got: %s\n" + " user stack:\n%s", + guard.create_fn_name(), + new_source, + new_source.name, + expected, + value, + "".join(guard.user_stack.format()) + if guard.user_stack + else "", + ) + return False + + return True + + +def has_reuse_entries( + tx: "InstructionTranslator", + fn_var: Any, +) -> bool: + """Cheap check: does the cache have any entries for this function?""" + from torch._guards import InvokeSubgraphCache + + invoke_subgraph_cache = tx.output.tracing_context.hop_dispatch_set_cache.get_cache( + torch._higher_order_ops.invoke_subgraph + ) + if not isinstance(invoke_subgraph_cache, InvokeSubgraphCache): + return False + fn_code = get_fn_code(fn_var) + return fn_code is not None and fn_code in invoke_subgraph_cache.subgraph_reuse_cache + + +def find_reuse_match( + tx: "InstructionTranslator", + fn_var: Any, + fingerprint: InputFingerprint, +) -> InvokeSubgraphReuseEntry | None: + from torch._guards import InvokeSubgraphCache + + invoke_subgraph_cache = tx.output.tracing_context.hop_dispatch_set_cache.get_cache( + torch._higher_order_ops.invoke_subgraph + ) + if not isinstance(invoke_subgraph_cache, InvokeSubgraphCache): + return None + fn_code = get_fn_code(fn_var) + if fn_code is None: + return None + + # this evaluator function is called one by one for all the invoke subgraph + # reuse entries - the one that evaluates to True is stamped out in the + # graph. + def evaluator( + cond: "InvokeSubgraphReuseCondition", entry: InvokeSubgraphReuseEntry + ) -> bool: + return is_reusable(tx, cond, fingerprint, entry) + + return invoke_subgraph_cache.find_reuse_entry(fn_code, evaluator) + + +def save_reuse_entry( + tx: "InstructionTranslator", + fn_var: Any, + fingerprint: InputFingerprint, + body_name: str, + body_gmod: torch.fx.GraphModule, + config: NestedCompileRegionOptions | None, + p_args: tuple[Any, ...], + body_r: VariableTracker, + example_value: Any, + max_reuse_entries: int = 8, + condition: "InvokeSubgraphReuseCondition | None" = None, + hash_key: int | None = None, +) -> None: + """Save a traced subgraph into the reuse cache for future cache hits. + + Builds an InvokeSubgraphReuseEntry with the freevar mapping (how each + lifted arg maps back to user inputs or captured variables), output + metadata, and arg sources. On a future cache hit, stamp_out_subgraph + uses this entry to emit a new invoke_subgraph call without re-tracing. + + Exactly one of ``condition`` or ``hash_key`` must be provided. + ``condition`` stores the entry in the guard-based cache (linear scan); + ``hash_key`` stores it in the hash-key cache (O(1) lookup). + """ + from torch._guards import InvokeSubgraphCache + + assert (condition is None) != (hash_key is None), ( + "Exactly one of condition or hash_key must be provided" + ) + + invoke_subgraph_cache = tx.output.tracing_context.hop_dispatch_set_cache.get_cache( + torch._higher_order_ops.invoke_subgraph + ) + if not isinstance(invoke_subgraph_cache, InvokeSubgraphCache): + return + + fn_code = get_fn_code(fn_var) + if fn_code is None: + return + + subgraph_input_mapping = build_subgraph_input_mapping( + tx, p_args, fingerprint.flat_vts + ) + single_tensor_output = isinstance(body_r, TensorVariable) + + # Count user-visible outputs from body_r. The graph may have additional + # outputs from side-effect intermediates that stamp_out_subgraph must + # not include when reconstructing the user-visible return value. + user_output_vts: list[VariableTracker] = [] + VariableTracker.visit( + lambda vt: user_output_vts.append(vt) + if vt.is_tensor() or isinstance(vt, SymNodeVariable) + else None, + body_r, + ) + num_user_outputs = len(user_output_vts) + + # Cache output tensor metadata so we can construct fresh FakeTensors on + # cache hit without re-running the subgraph. This is safe because + # invoke_subgraph does not support aliasing between inputs and outputs + # (speculate_subgraph will fail if that happens). + # example_value may contain SymInts (e.g. shape values for backward); + # only record metadata for actual tensors. + output_metadata = [ + (t.shape, t.stride(), t.dtype, t.device, t.requires_grad) + for t in example_value + if isinstance(t, torch.Tensor) + ] + + entry = InvokeSubgraphReuseEntry( + body_name=body_name, + body_gmod=body_gmod, + config=config, + subgraph_input_mapping=subgraph_input_mapping, + single_tensor_output=single_tensor_output, + output_metadata=output_metadata, + # Record arg sources so that on cache hit we can build a + # source replacement mapping (old sources → new sources) to + # rewrite captured variable sources for the current invocation. + arg_sources=fingerprint.arg_sources, + num_user_outputs=num_user_outputs, + ) + if condition is not None: + invoke_subgraph_cache.add_reuse_entry( + fn_code, condition, entry, max_reuse_entries + ) + else: + assert hash_key is not None + invoke_subgraph_cache.add_reuse_entry_by_key( + fn_code, hash_key, entry, max_reuse_entries + ) + + +def trace_reuse_hash_fn( + tx: "InstructionTranslator", + reuse_hash_fn: Any, + fn_args_vt: "Sequence[VariableTracker]", + kwargs: dict[str, VariableTracker], +) -> int: + """Trace the user's reuse_hash_fn to get a constant integer hash key. + + Guards installed during the hash function tracing are skipped — the hash + key itself is the reuse condition, not the guards. + """ + from torch._dynamo.exc import Unsupported + from torch._dynamo.utils import _make_inlined + + with tx.output.tracing_context.guards_context.skip_guard_install(): + try: + result = _make_inlined(tx, reuse_hash_fn)(*fn_args_vt, **kwargs) + except Unsupported as e: + raise RuntimeError( + f"reuse_hash_fn must be fully traceable without graph breaks. Got: {e}" + ) from e + + if not isinstance(result, ConstantVariable) or not isinstance(result.value, int): + raise RuntimeError( + f"reuse_hash_fn must return a constant integer, got {result}" + ) + + return result.value + + +def find_reuse_entry_by_key( + tx: "InstructionTranslator", + fn_var: Any, + hash_key: int, +) -> InvokeSubgraphReuseEntry | None: + from torch._guards import InvokeSubgraphCache + + invoke_subgraph_cache = tx.output.tracing_context.hop_dispatch_set_cache.get_cache( + torch._higher_order_ops.invoke_subgraph + ) + if not isinstance(invoke_subgraph_cache, InvokeSubgraphCache): + return None + fn_code = get_fn_code(fn_var) + if fn_code is None: + return None + return invoke_subgraph_cache.find_reuse_entry_by_key(fn_code, hash_key) + + +def stamp_out_subgraph( + tx: "InstructionTranslator", + fingerprint: InputFingerprint, + cached: InvokeSubgraphReuseEntry, +) -> VariableTracker: + """Emit a new invoke_subgraph call by stamping out a cached subgraph. + + Sources in the cached entry are parameterized: they refer to the original + call's sources and must be rewritten to the current call's sources via + source replacement before we can look up or create the corresponding + graph placeholders. + """ + from torch._dynamo.variables.builder import VariableBuilder + from torch._dynamo.variables.higher_order_ops import add_call_function, make_attr + + flat_proxies = get_flat_proxies(fingerprint) + new_arg_sources = fingerprint.arg_sources + + source_replacement = build_source_replacement(cached.arg_sources, new_arg_sources) + + new_lifted_args = [] + # Shared resolution context so get_value memoizes intermediate results + # (e.g. L['self'].layers) across all freevars in this stamp-out. + resolve_globals: dict[str, Any] = { + "G": tx.output.root_tx.f_globals, + "L": tx.output.root_tx.f_locals, + } + resolve_locals: dict[str, Any] = {} + resolve_cache: dict[Source, Any] = {} + + # Find the args for the about-to-be-inserted invoke_subgraph call. + for subgraph_input in cached.subgraph_input_mapping: + if isinstance(subgraph_input, LiftedUserArg): + new_lifted_args.append(flat_proxies[subgraph_input.index]) + elif isinstance(subgraph_input, LiftedBoundSymbol): + from torch._dynamo.output_graph import LazyProxy + + proxy = tx.output.current_tracer.bound_symbols[subgraph_input.expr] + if isinstance(proxy, LazyProxy): + proxy = proxy() + tx.output.current_tracer.bound_symbols[subgraph_input.expr] = proxy + new_lifted_args.append(proxy) + elif isinstance(subgraph_input, LiftedSyntheticObject): + ctor_args = subgraph_input.ctor_args + ctor_arg_sources = subgraph_input.ctor_arg_sources + if ctor_arg_sources and source_replacement: + new_ctor_args = [] + new_ctor_arg_sources = [] + for val, arg_src in zip(ctor_args, ctor_arg_sources): + if arg_src is not None: + new_src = arg_src.clone(lambda s: source_replacement.get(s, s)) + val = new_src.get_value( + resolve_globals, resolve_locals, resolve_cache + ) + arg_src = new_src + new_ctor_args.append(val) + new_ctor_arg_sources.append(arg_src) + ctor_args = tuple(new_ctor_args) + ctor_arg_sources = tuple(new_ctor_arg_sources) + vt = tx.output.synthetic_graph_input( + subgraph_input.ctor_fn, ctor_args, ctor_arg_sources + ) + new_lifted_args.append(vt.as_proxy()) + elif isinstance(subgraph_input, LiftedCapturedSource): + new_source = subgraph_input.source + if source_replacement: + new_source = new_source.clone(lambda s: source_replacement.get(s, s)) + # VariableBuilder deduplicates via input_source_to_var, + # so this reuses existing graph placeholders automatically. + value = new_source.get_value(resolve_globals, resolve_locals, resolve_cache) + vt = VariableBuilder(tx, new_source)(value) + new_lifted_args.append(vt.as_proxy()) + + # Generate fake tensor outputs + assert tx.fake_mode is not None + with tx.fake_mode: + example_value = tuple( + torch.empty_strided( + shape, + stride, + dtype=dtype, + device=device, + requires_grad=req_grad, + ) + for shape, stride, dtype, device, req_grad in cached.output_metadata + ) + + # Install the invoke_subgraph call + body_node = make_attr(tx, cached.body_name) + p_args = (body_node, cached.body_name, *new_lifted_args) + flat_variable = add_call_function( + tx, + torch._higher_order_ops.invoke_subgraph, + tuple(p_args), + {}, + example_value, + cached.config, + ) + + # Return only the user-visible outputs. The graph may have extra + # intermediate outputs from side effects (allow_side_effects=True) + # that should not be part of the user-facing return value. + if cached.single_tensor_output: + items = flat_variable.items # pyrefly: ignore[missing-attribute] + assert isinstance(items[0], TensorVariable), ( + f"Expected tensor output but got {type(items[0]).__name__}" + ) + return items[0] + + items = flat_variable.items # pyrefly: ignore[missing-attribute] + n = cached.num_user_outputs + if n > 0 and n < len(items): + from .builder import SourcelessBuilder + + return SourcelessBuilder.create(tx, tuple(items[:n])) + return flat_variable + + +def build_subgraph_input_mapping( + tx: "InstructionTranslator", + p_args: tuple[Any, ...], + flat_vts: list[tuple[InputTag, VariableTracker]], +) -> list[LiftedArgOrigin]: + """Build a mapping that records the origin of each lifted arg for a subgraph. + + On a cache hit, we stamp out a new invoke_subgraph call and need to + reconstruct its argument list in the correct order. Each lifted arg + (p_args[2:], skipping body_node and body_name) comes from one of: + + - LiftedUserArg: a user argument (intermediate activation or explicit input) + - LiftedCapturedSource: a captured variable (e.g. a weight or parameter) + - LiftedSyntheticObject: a TorchScriptObject with a SyntheticLocalSource + - LiftedBoundSymbol: a SymInt already bound as a graph input + """ + proxy_node_to_idx: dict[torch.fx.Node, int] = {} + idx = 0 + for tag, vt in flat_vts: + if tag in (InputTag.TENSOR, InputTag.SYMNODE): + node = vt.as_proxy().node + if node not in proxy_node_to_idx: + proxy_node_to_idx[node] = idx + idx += 1 + + subgraph_input_mapping: list[LiftedArgOrigin] = [] + for outer_proxy in p_args[2:]: + matched_idx = proxy_node_to_idx.get(outer_proxy.node, -1) + if matched_idx >= 0: + subgraph_input_mapping.append(LiftedUserArg(matched_idx)) + else: + grapharg = outer_proxy.node.meta.get("grapharg", None) + source = grapharg.source if grapharg is not None else None + # SymInt freevars must reuse the existing symbolic proxy rather + # than resolving via source.get_value() (which returns the + # concrete int). They appear as either: + # - placeholder nodes with grapharg.example being a SymInt + # - call_function nodes (e.g. sym_size_int) with no grapharg + # In both cases, store the sympy expression and look it up in + # bound_symbols during stamp-out. + example = ( + grapharg.example + if grapharg is not None + else outer_proxy.node.meta.get("example_value", None) + ) + if isinstance(example, torch.SymInt): + subgraph_input_mapping.append(LiftedBoundSymbol(example.node.expr)) + continue + assert source is not None, ( + f"Freevar has no source: node.op={outer_proxy.node.op} " + f"node.name={outer_proxy.node.name} -- this likely means a " + f"function argument was not included in the proxy matching" + ) + if isinstance(source, SyntheticLocalSource): + ctor_info = tx.output.synthetic_source_ctor_info.get(source) + if ctor_info is not None: + ctor_fn, ctor_args, ctor_arg_sources = ctor_info + subgraph_input_mapping.append( + LiftedSyntheticObject(ctor_fn, ctor_args, ctor_arg_sources) + ) + continue + subgraph_input_mapping.append(LiftedCapturedSource(source)) + return subgraph_input_mapping + + +class InvokeSubgraphHigherOrderVariable(WrapHigherOrderVariable): + _HOP_NAME = "torch.ops.higher_order.invoke_subgraph" + _ALLOW_FALLBACK_TO_EAGER = False + supports_input_mutation = True + supports_aliasing = False + allow_side_effects = True + # invoke_subgraph is NOT desugared in AOTAutograd, so the HOP input/output + # shouldn't alias. For checkpoint HOP, we inline it so we don't need + # alias analysis as functionalization would just work on the flat graph. + filter_aliased_intermediates = True + + # pyrefly: ignore[bad-override] + def install_subgraph_in_output_graph( + self, + tx: "InstructionTranslator", + fn_vt: VariableTracker, + fn_args_vt: "Sequence[VariableTracker]", + kwargs: dict[str, VariableTracker], + body_gmod: GraphModule, + attr_name: str, + ) -> str: + # Check if the subgraph from speculate_subgraph (body_gmod) and the fake + # inputs have already been seen before. If yes, the subgraph is already + # installed in the output graph and we can just access the subgraph + # using the saved attr name. + + if not isinstance(fn_vt, (UnspecializedNNModuleVariable, UserFunctionVariable)): + unimplemented( + gb_type="Encountered non user function variable during invoke_subgraph HOP tracing", + context=str(fn_vt), + explanation="invoke_subgraph does not support non user function variable", + hints=[*graph_break_hints.SUPPORTABLE], + ) + + invoke_subgraph_cache = ( + tx.output.tracing_context.hop_dispatch_set_cache.get_cache( + torch._higher_order_ops.invoke_subgraph + ) + ) + + if isinstance(fn_vt, UserFunctionVariable): + fn_code = fn_vt.get_function().__code__ + fn_name = fn_vt.get_function().__name__ + else: + assert isinstance(fn_vt, UnspecializedNNModuleVariable) + fn_code = fn_vt.value.forward.__func__.__code__ # type: ignore[attr-defined] + fn_name = fn_vt.value.forward.__name__ # type: ignore[attr-defined] + # pyrefly: ignore [implicit-any] + previously_installed_submodules = [] + if invoke_subgraph_cache: + previously_installed_submodules = ( + invoke_subgraph_cache.get_dynamo_installed_submodules(fn_code) + ) + current_mod = body_gmod + # NB - reverse is more likely to cause a hit sooner because first + # graph can have requires_grad=False for a few inputs + for submodule_name in reversed(previously_installed_submodules): + assert submodule_name in tx.output.nn_modules + previous_mod = tx.output.nn_modules[submodule_name] + assert tx.fake_mode + from torch._dynamo.variables.higher_order_ops import ( + are_same_graph_modules, + ) + + if are_same_graph_modules( + fn_name, previous_mod, current_mod, tx.fake_mode + ): + return submodule_name + + body_name = super().install_subgraph_in_output_graph( + tx, fn_vt, fn_args_vt, kwargs, body_gmod, "subgraph" + ) + hc_log.debug( + "%s: Installing subgraph with identifier '%s', bringing total count for '%s' function to %s", + fn_name, + body_name, + fn_name, + len(previously_installed_submodules) + 1, + ) + if invoke_subgraph_cache: + invoke_subgraph_cache.add_dynamo_installed_submodule(fn_code, body_name) + + return body_name + + def _call_function( + self, + tx: "InstructionTranslator", + args: "Sequence[VariableTracker]", + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from torch._dynamo.utils import dynamo_timed + from torch._dynamo.variables.higher_order_ops import ( + _call_function_with_auto_output_flattening, + ) + + fn_var = args[0] + fn_args_vt = args[1:] + + config = None + max_reuse_entries = 8 + reuse_hash_fn = None + if hasattr(fn_var, "get_function"): + try: + fn = fn_var.get_function() + config = getattr(fn, "__marked_compile_region_config__", None) + max_reuse_entries = getattr( + fn, "__marked_compile_region_max_reuse_entries__", 8 + ) + reuse_hash_fn = getattr( + fn, "__marked_compile_region_reuse_hash_fn__", None + ) + except Exception: + log.warning( + "Failed to extract nested_compile_region() config from InvokeSubgraphHigherOrderVariable. ", + exc_info=True, + ) + raise + + # TODO (anijain2305) - Collect issues why this does not work for export, + # and enable if request arises. + reuse = not tx.output.export + + # User-provided reuse_hash_fn path: hash key determines cache lookup. + if reuse and reuse_hash_fn is not None: + with dynamo_timed("invoke_subgraph_reuse_hash_fn"): + hash_key = trace_reuse_hash_fn(tx, reuse_hash_fn, fn_args_vt, kwargs) + + cached = find_reuse_entry_by_key(tx, fn_var, hash_key) + if cached is not None: + hc_log.debug( + "subgraph_reuse: hash key %d hit for '%s', reusing subgraph '%s'", + hash_key, + fn_var, + cached.body_name, + ) + fingerprint = build_input_fingerprint(tx, fn_args_vt, kwargs) + with dynamo_timed("invoke_subgraph_reuse_stamp_out"): + return stamp_out_subgraph(tx, fingerprint, cached) + + # Automatic reuse lookup (guard-based): check fn_code first (cheap) to + # avoid the expensive pytree flatten in build_input_fingerprint on + # the first call when there's nothing in the cache yet. + elif reuse and has_reuse_entries(tx, fn_var): + with dynamo_timed("invoke_subgraph_reuse_lookup"): + fingerprint = build_input_fingerprint(tx, fn_args_vt, kwargs) + match = find_reuse_match( + tx, + fn_var, + fingerprint, + ) + if match is not None: + hc_log.debug( + "subgraph_reuse: cache hit for '%s', reusing subgraph '%s'", + fn_var, + match.body_name, + ) + with dynamo_timed("invoke_subgraph_reuse_stamp_out"): + return stamp_out_subgraph(tx, fingerprint, match) + + assert self._HOP_NAME is not None + with dynamo_timed("invoke_subgraph_trace"): + ( + p_args, + p_kwargs, + example_value, + body_r, + body_gmod, + body_name, + body_graph_output_vts, + tracing_info, + ) = self.create_wrapped_node(tx, fn_var, fn_args_vt, kwargs, self._HOP_NAME) + + if len(p_kwargs) > 0: + unimplemented( + gb_type="invoke_subgraph: kwargs unexpected", + context=f"args: {args}, kwargs: {kwargs}", + explanation="kwargs should have been flattened into lifted args.", + hints=[ + *graph_break_hints.DYNAMO_BUG, + ], + ) + + # Store config in the body graph module meta + if isinstance(config, NestedCompileRegionOptions): + body_gmod.meta["nested_region_config"] = config + + p_args = ( + p_args[0], + body_name, + *p_args[1:], + ) + + # Subgraph reuse: save entry for future cache hits + if reuse: + fingerprint = build_input_fingerprint(tx, fn_args_vt, kwargs) + if reuse_hash_fn is not None: + traced_sources = tracing_info.traced_sources + if not is_reuse_eligible( + tx, + body_r, + fingerprint, + tracing_info, + traced_sources, + has_reuse_hash_fn=True, + ): + raise RuntimeError( + "reuse_hash_fn was provided but the subgraph is not " + "eligible for reuse. Check the logs with " + "TORCH_LOGS='+hierarchical_compile' for details." + ) + save_reuse_entry( + tx, + fn_var, + fingerprint, + body_name, + body_gmod, + config, + p_args, + body_r, + example_value, + max_reuse_entries, + hash_key=hash_key, # type: ignore[possibly-undefined] + ) + else: + traced_sources = tracing_info.traced_sources + if is_reuse_eligible( + tx, body_r, fingerprint, tracing_info, traced_sources + ): + condition = build_reuse_condition( + tx, + fingerprint, + traced_sources, + ) + if condition is not None: + save_reuse_entry( + tx, + fn_var, + fingerprint, + body_name, + body_gmod, + config, + p_args, + body_r, + example_value, + max_reuse_entries, + condition=condition, + ) + + return _call_function_with_auto_output_flattening( # type: ignore[return-value] + tx, + torch._higher_order_ops.invoke_subgraph, + tuple(p_args), + p_kwargs, + example_value, + body_r, + body_graph_output_vts, + config=config, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/iter.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/iter.py new file mode 100644 index 0000000000000000000000000000000000000000..f4c3ebba8ddf0d1fde357169dc0446c07b531c49 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/iter.py @@ -0,0 +1,657 @@ +""" +This module provides iterator-related variable tracking functionality for Dynamo. +It implements variable classes for handling Python iterators and itertools functions +during symbolic execution and tracing. + +The module includes: +- Base iterator variable classes for tracking iterator state +- Implementations of built-in iterators (zip, map, filter) +- Support for itertools functions (product, accumulate, combinations, etc.) +- Mutation tracking and reconstruction capabilities for iterator operations + +These classes integrate with Dynamo's variable tracking system to enable proper +handling of iterator operations during code transformation and optimization. +""" + +import itertools +import sys +from collections.abc import Callable, Sequence +from typing import Any, TYPE_CHECKING + +from .. import graph_break_hints, polyfills, variables +from ..bytecode_transformation import ( + create_build_tuple, + create_call_function, + create_call_function_ex, + create_instruction, +) +from ..exc import ( + handle_observed_exception, + ObservedUserStopIteration, + raise_observed_exception, + unimplemented, + UserError, +) +from .base import ValueMutationNew, VariableTracker +from .constant import ConstantVariable + + +if TYPE_CHECKING: + from torch._dynamo.codegen import PyCodegen + from torch._dynamo.symbolic_convert import InstructionTranslator + + +MAX_ITERATOR_LIMIT = 100 * 1024 # 100k + + +class ItertoolsVariable(VariableTracker): + def __init__(self, value: Any, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.value = value + + def __repr__(self) -> str: + return f"ItertoolsVariable({self.value})" + + def as_python_constant(self) -> Any: + return self.value + + def get_real_python_backed_value(self) -> Any: + return self.value + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence["VariableTracker"], + kwargs: "dict[str, VariableTracker]", + ) -> "VariableTracker": + # See also: module `torch._dynamo.polyfills.itertools` + + if self.value is itertools.product: + if any(kw != "repeat" for kw in kwargs): + unimplemented( + gb_type="Unsupported kwargs for itertools.product", + context=f"call_function {self} {args} {kwargs}", + explanation=f"Expected kwargs: 'repeat', but got " + f"{','.join(set(kwargs.keys()) - {'repeat'})}", + hints=[*graph_break_hints.USER_ERROR], + ) + + if "repeat" in kwargs: + r = kwargs["repeat"].as_python_constant() + else: + r = 1 + seqs = [arg.force_unpack_var_sequence(tx) for arg in args] + items = [ + variables.TupleVariable(list(item)) + for item in itertools.product(*seqs, repeat=r) + ] + return variables.ListIteratorVariable( + items, # type: ignore[arg-type] + mutation_type=ValueMutationNew(), + ) + elif ( + self.value is itertools.combinations + and not kwargs + and len(args) == 2 + and args[0].has_unpack_var_sequence(tx) + and args[1].is_python_constant() + ): + iterable = args[0].unpack_var_sequence(tx) + r = args[1].as_python_constant() + + items = [] + for item in itertools.combinations(iterable, r): + items.append(variables.TupleVariable(list(item))) + return variables.ListIteratorVariable( + items, # type: ignore[arg-type] + mutation_type=ValueMutationNew(), + ) + elif self.value is itertools.groupby: + if any(kw != "key" for kw in kwargs): + unimplemented( + gb_type="Unsupported kwargs for itertools.groupby", + context=f"call_function {self} {args} {kwargs}", + explanation=f"Expected kwargs: 'key', but got " + f"{','.join(set(kwargs.keys()) - {'key'})}", + hints=[*graph_break_hints.USER_ERROR], + ) + + def retrieve_const_key(key: VariableTracker) -> Any: + if isinstance(key, variables.SymNodeVariable): + return key.evaluate_expr() + elif key.is_python_constant(): + return key.as_python_constant() + else: + unimplemented( + gb_type="Unsupported key type for itertools.groupby", + context=f"call_function {self} {args} {kwargs}", + explanation="Dynamo does not know how to trace " + f"itertools.groupby with key type: {str(type(key))}. " + "We only support grouping keys that are constants (int, float, str, etc.)", + hints=[*graph_break_hints.SUPPORTABLE], + ) + + if len(args) == 1 and args[0].has_unpack_var_sequence(tx): + seq = args[0].unpack_var_sequence(tx) + else: + unimplemented( + gb_type="Unsupported arguments for itertools.groupby", + context=f"call_function {self} {args} {kwargs}", + explanation="Dynamo does not know how to trace " + f"itertools.groupby with args: {args} and kwargs: {kwargs}. " + "itertools.groupby expects an iterable to group and an " + "optional key function to determine groupings.", + hints=[ + "Make sure the arguments to itertools.groupby are correct.", + *graph_break_hints.SUPPORTABLE, + ], + ) + + if "key" in kwargs: + + def keyfunc(x: VariableTracker) -> Any: + return retrieve_const_key( + kwargs.get("key").call_function(tx, [x], {}) # type: ignore[union-attr] + ) + + else: + + def keyfunc(x: VariableTracker) -> Any: + return retrieve_const_key(x) + + result = [] + try: + for k, v in itertools.groupby(seq, key=keyfunc): + result.append( + variables.TupleVariable( + [ + ( + variables.ConstantVariable.create(k) + if variables.ConstantVariable.is_literal(k) + else k + ), + variables.ListIteratorVariable( + list(v), mutation_type=ValueMutationNew() + ), + ], + mutation_type=ValueMutationNew(), + ) + ) + except Exception as e: + unimplemented( + gb_type="Unexpected failure during itertools.groupby() iteration", + context=f"call_function {self} {args} {kwargs}", + explanation="Unexpected failure in invoking function during groupby", + hints=[*graph_break_hints.SUPPORTABLE], + from_exc=e, + ) + return variables.ListIteratorVariable( + result, # type: ignore[arg-type] + mutation_type=ValueMutationNew(), + ) + elif self.value is itertools.repeat: + if len(args) < 2: + return variables.RepeatIteratorVariable( + *args, mutation_type=ValueMutationNew() + ) + + return tx.inline_user_function_return( + VariableTracker.build(tx, polyfills.repeat), args, kwargs + ) + elif self.value is itertools.count and not kwargs: + if len(args) == 0: + return variables.CountIteratorVariable(mutation_type=ValueMutationNew()) + if len(args) == 1: + return variables.CountIteratorVariable( + item=args[0], mutation_type=ValueMutationNew() + ) + if len(args) == 2: + return variables.CountIteratorVariable( + item=args[0], + step=args[1], + mutation_type=ValueMutationNew(), + ) + return super().call_function(tx, args, kwargs) + elif ( + self.value is itertools.permutations + and (len(args) == 1 or (len(args) == 2 and args[1].is_python_constant())) + and not kwargs + ): + if len(args) == 2: + r = args[1].as_python_constant() + else: + r = None + items = [ + variables.TupleVariable(list(item)) + for item in itertools.permutations( + args[0].force_unpack_var_sequence(tx), r + ) + ] + return variables.ListIteratorVariable( + items, # type: ignore[arg-type] + mutation_type=ValueMutationNew(), + ) + else: + return super().call_function(tx, args, kwargs) + + +class IteratorVariable(VariableTracker): + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + def next_variable(self, tx: "InstructionTranslator") -> VariableTracker: + unimplemented( + gb_type="Unimplemented next() call", + context=f"next({self})", + explanation="This abstract method must be implemented", + hints=[*graph_break_hints.DYNAMO_BUG], + ) + + # NOTE: only call when unpacking this iterator safely done eagerly! + # Normally, iterators are accessed lazily. + # Example of safe eager unpacking: list(map(f, seq)) + # Example of unsafe eager unpacking: list(islice(map(f, seq), 5)) + def force_unpack_var_sequence( + self, tx: "InstructionTranslator" + ) -> list[VariableTracker]: + result: list[VariableTracker] = [] + self.force_apply_to_var_sequence(tx, result.append) + return result + + def force_apply_to_var_sequence( + self, tx: "InstructionTranslator", fn: Callable[[Any], Any] + ) -> None: + while True: + try: + fn(self.next_variable(tx)) + except ObservedUserStopIteration: + handle_observed_exception(tx) + break + + # don't call force_unpack_var_sequence since it can mutate + # IteratorVariable state! + def has_force_unpack_var_sequence(self, tx: "InstructionTranslator") -> bool: + return True + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> "ConstantVariable": + if name == "__iter__" or name == "__next__": + return variables.ConstantVariable.create(True) + return super().call_obj_hasattr(tx, name) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "__iter__": + return self + elif name == "__next__": + return self.next_variable(tx) + return super().call_method(tx, name, args, kwargs) + + +class ObjectIteratorVariable(IteratorVariable): + """ + VariableTracker for iter(obj) that implements the iterator protocol (i.e., + has a `__next__` method). + + We use this class to track the state of the iterator and handle the case + when the iterator is exhausted: + + Example usage: + > b = iter(obj) + > list(b) # exhaust the iterator + > list(b) # empty list + """ + + def __init__(self, obj: VariableTracker, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.obj = obj + self.generator_exhausted = False + + def next_variable(self, tx: "InstructionTranslator") -> VariableTracker: + if self.generator_exhausted: + raise_observed_exception(StopIteration, tx) + + try: + return self.obj.next_variable(tx) + except ObservedUserStopIteration: + # Do not rely on the object to always return StopIteration once it + # is exhausted. + self.generator_exhausted = True + raise + + +class RepeatIteratorVariable(IteratorVariable): + def __init__(self, item: VariableTracker, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.item = item + + def python_type(self) -> type: + return itertools.repeat + + # Repeat needs no mutation, clone self + def next_variable(self, tx: "InstructionTranslator") -> VariableTracker: + return self.item + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.extend_output( + [ + codegen.create_load_python_module(itertools), + codegen.create_load_attr("repeat"), + ] + ) + ) + codegen(self.item) + codegen.extend_output(create_call_function(1, False)) + + +class CountIteratorVariable(IteratorVariable): + # advance_count tracks how many next() calls were made during tracing, + # used by side_effects.py to replay them on the real iterator post-execution. + _nonvar_fields = { + "advance_count", + *IteratorVariable._nonvar_fields, + } + + def python_type(self) -> type: + return itertools.count + + def __init__( + self, + item: int | VariableTracker = 0, + step: int | VariableTracker = 1, + advance_count: int = 0, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + if not isinstance(item, VariableTracker): + item = ConstantVariable.create(item) + if not isinstance(step, VariableTracker): + step = ConstantVariable.create(step) + self.item = item + self.step = step + self.advance_count = advance_count + + def next_variable(self, tx: "InstructionTranslator") -> VariableTracker: + assert self.is_mutable() + old_item = self.item + tx.output.side_effects.mutation(self) + self.item = self.item.call_method(tx, "__add__", [self.step], {}) + self.advance_count += 1 + return old_item + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.extend_output( + [ + codegen.create_load_python_module(itertools), + codegen.create_load_attr("count"), + ] + ) + ) + codegen(self.item) + codegen(self.step) + codegen.extend_output(create_call_function(2, False)) + + +class ZipVariable(IteratorVariable): + """ + Represents zip(*iterables) + """ + + # PyZip_Type: https://github.com/python/cpython/blob/v3.13.0/Python/bltinmodule.c#L3011 + _cpython_type = zip + + _nonvar_fields = { + "index", + "strict", + *IteratorVariable._nonvar_fields, + } + + def __init__( + self, + iterables: list[VariableTracker], + strict: bool = False, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + assert isinstance(iterables, list) + # can be list[Variable] or VariableTracker (with next_variable implemented) + self.iterables = iterables + self.index = 0 + self.strict = strict + + def python_type(self) -> type[zip]: # type: ignore[type-arg] + return zip + + def has_unpack_var_sequence(self, tx: "InstructionTranslator") -> bool: + return all( + isinstance(it, list) or it.has_unpack_var_sequence(tx) + for it in self.iterables + ) + + def unpack_var_sequence( + self, tx: "InstructionTranslator" + ) -> list["VariableTracker"]: + assert self.has_unpack_var_sequence(tx) + iterables = [] + for it in self.iterables: + if isinstance(it, list): + iterables.append(it[self.index :]) + else: + iterables.append(it.unpack_var_sequence(tx)) + kwargs = {"strict": self.strict} if self.strict else {} + zipped = zip(*iterables, **kwargs) + return [variables.TupleVariable(list(var)) for var in zipped] + + def next_variable(self, tx: "InstructionTranslator") -> VariableTracker: + assert self.is_mutable() + + if len(self.iterables) == 0: + raise_observed_exception(StopIteration, tx) + + old_index = self.index + args = [] + + def get_item( + it: list[VariableTracker] | VariableTracker, + ) -> VariableTracker: + if isinstance(it, list): + if old_index >= len(it): + raise_observed_exception(StopIteration, tx) + return it[old_index] + else: + return it.next_variable(tx) + + idx: int | None = None + try: + for idx, it in enumerate(self.iterables): # noqa:B007 + args.append(get_item(it)) + except ObservedUserStopIteration: + if self.strict: + if idx == 0: + # all other iterables should be exhausted + for it in self.iterables: + try: + get_item(it) + except ObservedUserStopIteration: + handle_observed_exception(tx) + continue + # no ObservedUserStopIteration - fall through to UserError + break + else: + # all iterables exhausted, raise original error + raise + handle_observed_exception(tx) + raise UserError( + ValueError, # type: ignore[arg-type] + "zip() has one argument of len differing from others", + ) from None + raise + + tx.output.side_effects.mutation(self) + self.index += 1 + return variables.TupleVariable(args) + + def reconstruct_items(self, codegen: "PyCodegen") -> None: + for it in self.iterables: + if isinstance(it, list): + remaining_items = it[self.index :] + codegen.foreach(remaining_items) + codegen.append_output(create_build_tuple(len(remaining_items))) + else: + codegen(it) + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.load_import_from("builtins", "zip"), call_function_ex=True + ) + self.reconstruct_items(codegen) + codegen.append_output(create_build_tuple(len(self.iterables))) + codegen.extend_output( + [ + codegen.create_load_const("strict"), + codegen.create_load_const(self.strict), + create_instruction("BUILD_MAP", arg=1), + *create_call_function_ex(True, False), + ] + ) + + +class MapVariable(ZipVariable): + """ + Represents map(fn, *iterables) + """ + + # PyMap_Type: https://github.com/python/cpython/blob/v3.13.0/Python/bltinmodule.c#L1484 + _cpython_type = map + + def __init__( + self, + fn: VariableTracker, + iterables: list[VariableTracker], + **kwargs: Any, + ) -> None: + super().__init__(iterables, **kwargs) + self.fn = fn + + def python_type(self) -> type: + return map + + def has_unpack_var_sequence(self, tx: "InstructionTranslator") -> bool: + return False + + def next_variable(self, tx: "InstructionTranslator") -> VariableTracker: + args = super().next_variable(tx) + return self.fn.call_function(tx, args.items, {}) # type: ignore[attr-defined] + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.load_import_from("builtins", "map"), call_function_ex=True + ) + codegen(self.fn) + self.reconstruct_items(codegen) + codegen.append_output(create_build_tuple(len(self.iterables) + 1)) + if self.strict: + assert sys.version_info >= (3, 14), ( + "Unexpected bug: map(strict=True) requires Python 3.14+" + ) + codegen.extend_output( + [ + codegen.create_load_const("strict"), + codegen.create_load_const(self.strict), + create_instruction("BUILD_MAP", arg=1), + *create_call_function_ex(True, False), + ] + ) + else: + codegen.extend_output(create_call_function_ex(False, False)) + + +class FilterVariable(IteratorVariable): + """ + Represents filter(fn, iterable) + """ + + # PyFilter_Type: https://github.com/python/cpython/blob/v3.13.0/Python/bltinmodule.c#L630 + _cpython_type = filter + + _nonvar_fields = { + "index", + *IteratorVariable._nonvar_fields, + } + + def __init__( + self, + fn: VariableTracker, + iterable: list[VariableTracker], + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.fn = fn + self.iterable = iterable + self.index = 0 + + def python_type(self) -> type: + return filter + + def has_unpack_var_sequence(self, tx: "InstructionTranslator") -> bool: + return isinstance(self.iterable, list) or self.iterable.has_unpack_var_sequence( + tx + ) + + def unpack_var_sequence( + self, tx: "InstructionTranslator" + ) -> list["VariableTracker"]: + assert self.has_unpack_var_sequence(tx) + it = None + if isinstance(self.iterable, list): + it = self.iterable[self.index :] + else: + it = self.iterable.unpack_var_sequence(tx) + filtered = self.fn.call_function(tx, it, {}) + return [variables.TupleVariable([filtered])] + + def next_variable(self, tx: "InstructionTranslator") -> VariableTracker: + def _next() -> VariableTracker: + old_index = self.index + if isinstance(self.iterable, list): + if old_index >= len(self.iterable): + raise_observed_exception(StopIteration, tx) + return self.iterable[old_index] + else: + return self.iterable.next_variable(tx) + + # A do-while loop to find elements that make fn return true + while True: + item = _next() + self.index += 1 + if self.fn.is_constant_none(): + res = item + else: + res = self.fn.call_function(tx, [item], {}) + pred_res = variables.UserFunctionVariable( + polyfills.predicate # type: ignore[arg-type] + ).call_function(tx, [res], {}) + if pred_res.as_python_constant(): + return item + + def reconstruct_items(self, codegen: "PyCodegen") -> None: + if isinstance(self.iterable, list): + remaining_items = self.iterable[self.index :] + codegen.foreach(remaining_items) + codegen.append_output(create_build_tuple(len(remaining_items))) + else: + codegen(self.iterable) + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null(lambda: codegen.load_import_from("builtins", "filter")) + codegen(self.fn) + self.reconstruct_items(codegen) + codegen.extend_output(create_call_function(2, False)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/lazy.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/lazy.py new file mode 100644 index 0000000000000000000000000000000000000000..def844adda73afea8560e3329509738b187b7836 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/lazy.py @@ -0,0 +1,306 @@ +from __future__ import annotations + +import collections +import functools +import inspect +from typing import Any, TYPE_CHECKING + +from ..utils import is_function_or_wrapper +from .base import VariableTracker, VariableTrackerMeta + + +if TYPE_CHECKING: + from collections.abc import Callable + from typing_extensions import Self + + from .tensor import SymNodeVariable + + +class LazyCache: + """Container to cache the real VariableTracker""" + + def __init__(self, value: Any, source: Any) -> None: + if not isinstance(value, LazySymNodeFormatString): + assert source + self.value = value + self.source = source + self.name_hint: str | None = None + self.vt: VariableTracker | None = None + + def realize(self) -> None: + assert self.vt is None + from ..symbolic_convert import InstructionTranslator + from . import builder + + tx = InstructionTranslator.current_tx() + + if isinstance(self.value, LazySymNodeFormatString): + self.vt = builder.SourcelessBuilder.create(tx, self.value) + else: + # Pass allow_lazy_constant=False to prevent VariableBuilder from + # returning LazyConstantVariable, which would cause infinite recursion + # when LazyVariableTracker.realize() returns LazyConstantVariable. + self.vt = builder.VariableBuilder( + tx, self.source, allow_lazy_constant=False + )(self.value) + + if self.name_hint is not None: + self.vt.set_name_hint(self.name_hint) + + del self.value + del self.source + del self.name_hint + + +class LazyVariableTracker(VariableTracker, metaclass=VariableTrackerMeta): + """ + A structure that defers the creation of the actual VariableTracker + for a given underlying value until it is accessed. + + The `realize` function invokes VariableTracker.build() to produce the real object. + Once a LazyVariableTracker has been realized, internal bookkeeping will + prevent double realization. + + This object should be utilized for processing containers, or objects that + reference other objects where we may not want to take on creating all the + VariableTrackers right away. + """ + + # Flag to prevent implicit realization in isinstance checks (inherited by subclasses) + _no_implicit_realize = True + _nonvar_fields = {"_cache", *VariableTracker._nonvar_fields} + + @staticmethod + def create(value: Any, source: Any, **options: Any) -> VariableTracker: + if type(value) in LazyConstantVariable.supported_types: + return LazyConstantVariable.create(value, source, **options) + + # Cache based on source when no extra options are passed + if source is not None and not options: + from ..symbolic_convert import InstructionTranslator + + tx = InstructionTranslator.current_tx() + if tx is not None: + cache = tx.output.variable_tracker_cache + cached = cache.get(source) + if cached is not None: + return cached + vt = LazyVariableTracker(LazyCache(value, source), source=source) + cache[source] = vt + return vt + + return LazyVariableTracker(LazyCache(value, source), source=source, **options) + + def __init__(self, _cache: LazyCache, **kwargs: Any) -> None: + assert isinstance(_cache, LazyCache) + super().__init__(**kwargs) + self._cache = _cache + + def realize(self) -> VariableTracker: + """Force construction of the real VariableTracker""" + if self._cache.vt is None: + self._cache.realize() + assert self._cache.vt is not None + return self._cache.vt + + def lazy_isinstance(self, cls: type) -> bool: + """Check isinstance after realizing, used by ImplicitRealizingVariableTrackerMeta""" + return type.__instancecheck__(cls, self.realize()) + + def unwrap(self) -> VariableTracker | Self: + """Return the real VariableTracker if it already exists""" + if self.is_realized(): + assert self._cache.vt is not None + return self._cache.vt + return self + + def is_realized(self) -> bool: + return self._cache.vt is not None + + def clone(self, **kwargs: Any) -> VariableTracker: + assert kwargs.get("_cache", self._cache) is self._cache + if kwargs.get("source", self.source) is not self.source: + self.realize() + return VariableTracker.clone(self.unwrap(), **kwargs) + + def peek_type(self) -> type[Any]: + assert not self.is_realized() + return type(self._cache.value) + + def peek_value(self) -> Any: + assert not self.is_realized() + return self._cache.value + + def set_name_hint(self, name: str) -> None: + if self.is_realized(): + self._cache.vt.set_name_hint(name) # type: ignore[union-attr] + else: + self._cache.name_hint = name + + def __str__(self) -> str: + variable_info = "LazyVariableTracker(" + if self.is_realized(): + variable_info += f"realized: {repr(self.unwrap())})" + else: + variable_info += f"unrealized: {self.peek_type()})" + + return variable_info + + def __getattr__(self, item: str) -> Any: + return getattr(self.realize(), item) + + # most methods are auto-generated below, these are the ones we want to exclude + visit = VariableTracker.visit # type: ignore[assignment] + __repr__ = __str__ + + @classmethod + def realize_all( + cls, + value: Any, + cache: dict[int, tuple[Any, Any]] | None = None, + *, + allow_lazy_constant: bool = False, + ) -> Any: + """ + Walk an object and realize all LazyVariableTrackers inside it. + """ + if cache is None: + cache = {} + + idx = id(value) + if idx in cache: + return cache[idx][0] + + value_cls = type(value) + if issubclass(value_cls, LazyVariableTracker): + # Allow LazyConstantVariable to stay lazy when returning from a frame + keep_lazy = allow_lazy_constant and isinstance(value, LazyConstantVariable) + if keep_lazy: + result = value + else: + result = cls.realize_all( + value.realize(), cache, allow_lazy_constant=allow_lazy_constant + ) + elif issubclass(value_cls, VariableTracker): + # update value in-place + result = value + # update cache now to prevent infinite recursion + cache[idx] = (result, value) + value_dict = value.__dict__ + nonvars = value._nonvar_fields + for key in value_dict: + if key not in nonvars: + value_dict[key] = cls.realize_all( + value_dict[key], cache, allow_lazy_constant=allow_lazy_constant + ) + elif value_cls is list: + result = [ + cls.realize_all(v, cache, allow_lazy_constant=allow_lazy_constant) + for v in value + ] + elif value_cls is tuple: + result = tuple( + cls.realize_all(v, cache, allow_lazy_constant=allow_lazy_constant) + for v in value + ) + elif value_cls in (dict, collections.OrderedDict): + result = { + k: cls.realize_all(v, cache, allow_lazy_constant=allow_lazy_constant) + for k, v in list(value.items()) + } + else: + result = value + + # save `value` to keep it alive and ensure id() isn't reused + cache[idx] = (result, value) + return result + + def is_hashable(self) -> bool: + # Checks that the underlying value is hashable without realizing the VT. + # This is used by ConstDictVariable tracker to find if the key LazyVT + # can be hashed. + def _helper(value: Any) -> bool: + # TODO: Add support for more types + return ( + inspect.isbuiltin(value) + or issubclass(type(value), type) + or is_function_or_wrapper(value) + ) + + assert not self.is_realized() + value = self._cache.value + if isinstance(value, tuple): + return all(_helper(v) for v in value) + return _helper(value) + + def original_value(self) -> Any: + # Returns the value without realizing the VT. + assert not self.is_realized() + return self._cache.value + + def original_source(self) -> Any: + # Returns the source without realizing the VT. + assert not self.is_realized() + return self._cache.source + + +class LazyConstantVariable(LazyVariableTracker): + """ + A lazy variable tracker for constants (int, float, bool, str) that defers + guarding until the value is actually used in a way that requires it. + + This allows constants that are just passed through (e.g., returned without + being used in control flow or math) to avoid unnecessary recompilation when + their values change. + """ + + supported_types = (int, float, bool, str) + + @staticmethod + def create( + value: Any, + source: Any, + **options: Any, + ) -> LazyConstantVariable: + assert type(value) in LazyConstantVariable.supported_types + return LazyConstantVariable(LazyCache(value, source), source=source, **options) + + +class LazySymNodeFormatString: + def __init__( + self, sym_node_variable: SymNodeVariable, fmt_spec_var: VariableTracker + ) -> None: + from .constant import ConstantVariable + + self.sym_node_var = sym_node_variable + self.fmt_var = ConstantVariable.create( + "{:" + fmt_spec_var.as_python_constant() + "}" + ) + + def __repr__(self) -> str: + return str.format( + self.fmt_var.as_python_constant(), + str(self.sym_node_var.evaluate_expr()), + ) + + +def _create_realize_and_forward( + name: str, +) -> Callable[[LazyVariableTracker, Any, Any], Any]: + @functools.wraps(getattr(VariableTracker, name)) + def realize_and_forward( + self: LazyVariableTracker, *args: Any, **kwargs: Any + ) -> Any: + return getattr(self.realize(), name)(*args, **kwargs) + + return realize_and_forward + + +def _populate() -> None: + for name, value in VariableTracker.__dict__.items(): + if name not in LazyVariableTracker.__dict__: + if callable(value): + setattr(LazyVariableTracker, name, _create_realize_and_forward(name)) + + +_populate() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/lists.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/lists.py new file mode 100644 index 0000000000000000000000000000000000000000..4149d56f450767d7b857a32fb3cd69fd2b9574f2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/lists.py @@ -0,0 +1,1797 @@ +""" +Variable tracking implementations for list-like data structures in Dynamo. + +This module provides specialized variable tracking for various collection types: +- Lists and list subclasses (including torch.nn.ModuleList, ParameterList) +- Tuples and named tuples +- Ranges and slices +- Collections.deque +- torch.Size with special proxy handling + +The implementations support both mutable and immutable collections, iteration, +and common sequence operations. Each collection type has a dedicated Variable +class that handles its unique behaviors while integrating with Dynamo's +variable tracking system. +""" + +import collections +import operator +import sys +from collections.abc import Sequence +from typing import Any, Literal, Optional, TYPE_CHECKING + +import torch +import torch.fx +from torch.utils._pytree import SequenceKey + +from .. import graph_break_hints, polyfills, variables +from ..bytecode_transformation import ( + create_build_tuple, + create_call_function, + create_call_method, + create_dup_top, + create_instruction, +) +from ..exc import raise_observed_exception, raise_type_error, unimplemented +from ..source import AttrSource +from ..utils import ( + cmp_name_to_op_mapping, + cmp_name_to_op_str_mapping, + get_fake_value, + guard_if_dyn, + iter_contains, + odict_values, + raise_args_mismatch, + range_iterator, + set_example_value, +) +from .base import AsPythonConstantNotImplementedError, ValueMutationNew, VariableTracker +from .constant import ConstantVariable +from .functions import UserFunctionVariable +from .iter import IteratorVariable + + +if TYPE_CHECKING: + from torch._dynamo.codegen import PyCodegen + from torch._dynamo.symbolic_convert import InstructionTranslator + + +class BaseListVariable(VariableTracker): + @staticmethod + def cls_for_instance(obj: Any) -> type["BaseListVariable"]: + return BaseListVariable.cls_for(type(obj)) + + @staticmethod + def cls_for(obj: Any) -> type: + return { + iter: ListIteratorVariable, + list: ListVariable, + slice: SliceVariable, + torch.Size: SizeVariable, + tuple: TupleVariable, + odict_values: ListVariable, + torch.nn.ParameterList: ListVariable, + torch.nn.ModuleList: ListVariable, + collections.deque: DequeVariable, + }[obj] + + def __init__( + self, + items: list[VariableTracker], + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + assert isinstance(items, list) + assert all(isinstance(x, VariableTracker) for x in items) + self.items: list[VariableTracker] = items + + def _as_proxy(self) -> list[Any]: + return [x.as_proxy() for x in self.items] + + def modified( + self, items: list[VariableTracker], **kwargs: Any + ) -> "BaseListVariable": + return type(self)(items, **kwargs) + + @property + def value(self) -> Any: + return self.as_python_constant() + + def debug_repr_helper(self, prefix: str, suffix: str) -> str: + return prefix + ", ".join(i.debug_repr() for i in self.items) + suffix + + def as_python_constant(self) -> Any: + return self.python_type()([x.as_python_constant() for x in self.items]) + + def as_proxy(self) -> Any: + assert self.python_type() is not SizeVariable + return self.python_type()(self._as_proxy()) + + def getitem_const( + self, tx: "InstructionTranslator", arg: VariableTracker + ) -> VariableTracker: + # TODO(follow-up): this assumes the caller (mp_subscript_impl) has already + # run _PyIndex_Check → nb_index_impl. Direct callers bypassing + # mp_subscript_impl will skip that validation. + from .tensor import SymNodeVariable + + if isinstance(arg, SymNodeVariable): + index = arg.sym_num + else: + index = arg.as_python_constant() + + if isinstance(index, slice): + if index.step == 0: + raise_observed_exception( + ValueError, tx, args=["slice step cannot be zero"] + ) + # Set source to None because slicing a list gives a new local + return self.clone( + items=self.items[index], + source=None, + mutation_type=ValueMutationNew() if self.mutation_type else None, + ) + else: + assert isinstance(index, (int, torch.SymInt)) + try: + return self.items[index] + except IndexError: + raise_observed_exception( + IndexError, tx, args=["list index out of range"] + ) + + def unpack_var_sequence(self, tx: "InstructionTranslator") -> list[VariableTracker]: + return list(self.items) + + def sq_length(self, tx: "InstructionTranslator") -> VariableTracker: + """Sequence length for lists, tuples, and range objects.""" + return VariableTracker.build(tx, len(self.items)) + + def call_tree_map_branch( + self, + tx: "InstructionTranslator", + tree_map_fn: UserFunctionVariable, + map_fn: VariableTracker, + rest: Sequence[VariableTracker], + tree_map_kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if not isinstance(self, (ListVariable, TupleVariable)): + return self._tree_map_fallback( + tx, tree_map_fn, map_fn, rest, tree_map_kwargs + ) + + other_lists: list[BaseListVariable] = [] + for candidate in rest: + if ( + not isinstance(candidate, BaseListVariable) + or len(candidate.items) != len(self.items) + or self.python_type() != candidate.python_type() + ): + return self._tree_map_fallback( + tx, tree_map_fn, map_fn, rest, tree_map_kwargs + ) + other_lists.append(candidate) + + new_items: list[VariableTracker] = [] + for idx, item in enumerate(self.items): + sibling_leaves = [candidate.items[idx] for candidate in other_lists] + new_items.append( + item.call_tree_map( + tx, + tree_map_fn, + map_fn, + sibling_leaves, + tree_map_kwargs, + ) + ) + + return self.clone( + items=new_items, + source=None, + mutation_type=ValueMutationNew(), + ) + + def call_tree_map_with_path_branch( + self, + tx: "InstructionTranslator", + tree_map_fn: UserFunctionVariable, + map_fn: VariableTracker, + rest: Sequence[VariableTracker], + tree_map_kwargs: dict[str, VariableTracker], + keypath: tuple[Any, ...], + ) -> VariableTracker: + if not isinstance(self, (ListVariable, TupleVariable)): + return self._tree_map_with_path_fallback( + tx, tree_map_fn, map_fn, rest, tree_map_kwargs, keypath + ) + + other_lists: list[BaseListVariable] = [] + for candidate in rest: + if ( + not isinstance(candidate, BaseListVariable) + or len(candidate.items) != len(self.items) + or self.python_type() != candidate.python_type() + ): + return self._tree_map_with_path_fallback( + tx, tree_map_fn, map_fn, rest, tree_map_kwargs, keypath + ) + other_lists.append(candidate) + + new_items: list[VariableTracker] = [] + for idx, item in enumerate(self.items): + sibling_leaves = [candidate.items[idx] for candidate in other_lists] + child_keypath = keypath + (SequenceKey(idx),) + new_items.append( + item.call_tree_map_with_path( + tx, + tree_map_fn, + map_fn, + sibling_leaves, + tree_map_kwargs, + child_keypath, + ) + ) + + return self.clone( + items=new_items, + source=None, + mutation_type=ValueMutationNew(), + ) + + def mp_subscript_impl( + self, + tx: "InstructionTranslator", + key: VariableTracker, + ) -> VariableTracker: + # list_subscript: https://github.com/python/cpython/blob/62a6e898e01/Objects/listobject.c#L3689-L3710 + # _PyIndex_Check: https://github.com/python/cpython/blob/62a6e898e01/Include/internal/pycore_abstract.h#L13-L17 + # TODO(follow-up): replace hasattr(key_type, "__index__") with + # has_slot(num_slots, PyNumberSlots.NB_INDEX) for C extension types. + try: + key_type = key.python_type() + except NotImplementedError: + key_type = None + if key_type not in (int, bool, slice): + if key_type is not None and not hasattr(key_type, "__index__"): + container_name = self.python_type_name() + raise_observed_exception( + TypeError, + tx, + args=[ + f"{container_name} indices must be integers or slices, not {key.python_type_name()}" + ], + ) + key = key.nb_index_impl(tx) + + return self.getitem_const(tx, key) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .builder import SourcelessBuilder + + if name == "__contains__": + if kwargs or len(args) != 1: + raise_args_mismatch( + tx, + name, + "1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + return iter_contains(self.unpack_var_sequence(tx), args[0], tx) + elif name == "index": + if not len(args): + raise_args_mismatch( + tx, + name, + "0 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + + try: + # Speedup trace times for constant data structures + items = [item.as_python_constant() for item in self.items] + const_args = [arg.as_python_constant() for arg in args] + const_kwargs = {k: v.as_python_constant() for k, v in kwargs.items()} + try: + return VariableTracker.build( + tx, items.index(*const_args, **const_kwargs) + ) + except ValueError: + raise_observed_exception( + ValueError, + tx, + args=["tuple.index()"], + ) + except AsPythonConstantNotImplementedError: + return tx.inline_user_function_return( + VariableTracker.build(tx, polyfills.index), + [self] + list(args), + kwargs, + ) + elif name == "count": + if len(args) != 1: + raise_args_mismatch( + tx, + name, + "1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + return VariableTracker.build(tx, operator.countOf).call_function( + tx, + [self, args[0]], + kwargs, + ) + elif name in ("__add__", "__iadd__"): + if kwargs or len(args) != 1: + raise_args_mismatch( + tx, + name, + "1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + + if type(self) is not type(args[0]): + tp_name = self.python_type_name() + other = args[0].python_type_name() + raise_observed_exception( + TypeError, + tx, + args=[ + f'can only concatenate {tp_name} (not "{other}") to {tp_name}' + ], + ) + + if name == "__add__": + return type(self)(self.items + args[0].items, source=self.source) # type: ignore[attr-defined] + else: + self.items += args[0].items # type: ignore[attr-defined] + return self + elif name in ("__mul__", "__imul__"): + if kwargs or len(args) != 1: + raise_args_mismatch( + tx, + name, + "1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + + if not (args[0].is_python_constant() and args[0].python_type() is int): + raise_observed_exception( + TypeError, + tx, + args=[ + f"can't multiply sequence by non-int type of '{args[0].python_type_name()}'" + ], + ) + + val = args[0].as_python_constant() + + if name == "__mul__": + return type(self)(self.items * val, source=self.source) + else: + self.items *= val + return self + elif name in cmp_name_to_op_mapping: + if len(args) != 1: + raise_args_mismatch( + tx, + name, + "1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + + left = self + right = args[0] + # TODO this type check logic mirrors the following + # https://github.com/python/cpython/blob/a1c52d1265c65bcf0d9edf87e143843ad54f9b8f/Objects/object.c#L991-L1007 + # But we should probably move it up the stack to so that we don't + # need to duplicate it for different VTs. + if not isinstance(left, BaseListVariable) or not isinstance( + right, BaseListVariable + ): + if name == "__eq__": + return SourcelessBuilder.create(tx, operator.is_).call_function( + tx, (left, right), {} + ) + elif name == "__ne__": + return SourcelessBuilder.create(tx, operator.is_not).call_function( + tx, (left, right), {} + ) + else: + op_str = cmp_name_to_op_str_mapping[name] + left_ty = left.python_type_name() + right_ty = right.python_type_name() + raise_observed_exception( + TypeError, + tx, + args=[ + f"{op_str} not supported between instances of '{left_ty}' and '{right_ty}'" + ], + ) + + return SourcelessBuilder.create(tx, polyfills.list_cmp).call_function( + tx, + [ + SourcelessBuilder.create(tx, cmp_name_to_op_mapping[name]), + left, + right, + ], + {}, + ) + elif name == "__iter__": + return ListIteratorVariable(self.items, mutation_type=ValueMutationNew()) + + return super().call_method(tx, name, args, kwargs) + + +class RangeVariable(BaseListVariable): + # PyRange_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/rangeobject.c#L767 + _cpython_type = range + + def __init__(self, items: Sequence[VariableTracker], **kwargs: Any) -> None: + items_to_map = items + start = variables.ConstantVariable.create(0) + stop = None + step = variables.ConstantVariable.create(1) + + if len(items_to_map) == 1: + (stop,) = items_to_map + elif len(items_to_map) == 2: + start, stop = items_to_map + elif len(items_to_map) == 3: + start, stop, step = items_to_map + else: + raise AssertionError + + def maybe_as_int(x: VariableTracker) -> VariableTracker: + return ( + ConstantVariable.create(int(x.as_python_constant())) + if x.is_python_constant() + else x + ) + + # cast each argument to an integer + start = maybe_as_int(start) + step = maybe_as_int(step) + stop = maybe_as_int(stop) + + assert stop is not None + super().__init__([start, stop, step], **kwargs) + + def debug_repr(self) -> str: + repr = f"range({self.start()}, {self.stop()}" + if self.step() != 1: + repr += f", {self.step()}" + repr += ")" + return repr + + def python_type(self) -> type: + return range + + def start(self) -> Any: + return self.items[0].as_python_constant() + + def stop(self) -> Any: + return self.items[1].as_python_constant() + + def step(self) -> Any: + return self.items[2].as_python_constant() + + def range_length(self) -> int: + lo = self.start() + hi = self.stop() + step = self.step() + + assert step != 0 + if step > 0 and lo < hi: + return 1 + (hi - 1 - lo) // step + elif step < 0 and lo > hi: + return 1 + (lo - 1 - hi) // (0 - step) + else: + return 0 + + def _get_slice_indices(self, length: int, slice: slice) -> list[int]: + step_is_negative = 0 + + if slice.step is None: + step = 1 + step_is_negative = False + else: + step = slice.step + step_is_negative = slice.step < 0 + + # Find lower and upper bounds for start and stop. + if step_is_negative: + lower = -1 + upper = length + lower + else: + lower = 0 + upper = length + + # Compute start + if slice.start is None: + start = upper if step_is_negative else lower + else: + start = slice.start + + if start < 0: + start += length + if start < lower: + start = lower + else: + if start > upper: + start = upper + + # Compute stop. + if slice.stop is None: + stop = lower if step_is_negative else upper + + else: + stop = slice.stop + + if stop < 0: + stop += length + if stop < lower: + stop = lower + else: + if stop > upper: + stop = upper + + return [start, stop, step] + + def apply_index(self, tx: "InstructionTranslator", index: int) -> VariableTracker: + length = self.range_length() + if index < 0: + index = length + index + + if index < 0 or index >= length: + raise_observed_exception( + IndexError, + tx, + args=["range object index out of range"], + ) + + return VariableTracker.build(tx, self.start() + (index * self.step())) + + def apply_slice(self, slice: slice) -> "RangeVariable": + (slice_start, slice_stop, slice_step) = self._get_slice_indices( + self.range_length(), slice + ) + + def compute_item(index: int) -> int: + return self.start() + (index * self.step()) + + sub_step = self.step() * slice_step + sub_start = compute_item(slice_start) + sub_stop = compute_item(slice_stop) + + result = RangeVariable( + [ + variables.ConstantVariable.create(x) + for x in [sub_start, sub_stop, sub_step] + ], + mutation_type=ValueMutationNew() if self.mutation_type else None, + ) + return result + + def as_python_constant(self) -> range: + return range(*[x.as_python_constant() for x in self.items]) + + def getitem_const( + self, tx: "InstructionTranslator", arg: VariableTracker + ) -> VariableTracker: + # range_subscript: https://github.com/python/cpython/blob/main/Objects/rangeobject.c + # TODO(follow-up): this assumes the caller (mp_subscript_impl) has already + # run _PyIndex_Check → nb_index_impl. Direct callers bypassing + # mp_subscript_impl will skip that validation. + index = arg.as_python_constant() + + if isinstance(index, slice): + return self.apply_slice(index) + elif isinstance(index, int): + return self.apply_index(tx, index) + else: + raise_observed_exception( + TypeError, tx, args=["range indices must be integers or slices"] + ) + + def as_proxy(self) -> range: + return self.python_type()(*self._as_proxy()) + + def unpack_var_sequence( + self, tx: Optional["InstructionTranslator"] = None + ) -> list[VariableTracker]: + return [variables.ConstantVariable.create(x) for x in self.as_python_constant()] + + def sq_length(self, tx: "InstructionTranslator") -> VariableTracker: + """Sequence length for range objects.""" + length = self.range_length() + if length > sys.maxsize: + raise_observed_exception(OverflowError, tx) + return VariableTracker.build(tx, length) + + def reconstruct(self, codegen: "PyCodegen") -> None: + assert "range" not in codegen.tx.f_globals + codegen.add_push_null( + lambda: codegen.append_output(codegen.create_load_python_module(range)) # type: ignore[arg-type] + ) + codegen.foreach(self.items) + codegen.extend_output(create_call_function(3, False)) + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> ConstantVariable: + if self.python_type() is range: + return VariableTracker.build(tx, name in range.__dict__) + return super().call_obj_hasattr(tx, name) + + def range_equals(self, other: "RangeVariable") -> bool: + r0, r1 = self, other + if ( + self.range_length() != r1.range_length() + or self.range_length() == 0 + or r0.start() != r1.start() + ): + return False + + if self.range_length() == 1: + return True + + return r0.step() == r1.step() + + def range_count(self, x: VariableTracker) -> int: + # Based on CPython + # https://github.com/guilhermeleobas/cpython/blob/baefaa6cba1d69efd2f930cdc56bca682c54b139/Objects/rangeobject.c#L442-L486 + x = x.as_python_constant() + if type(x) not in (bool, int, float): + return 0 + + start, stop, step = self.start(), self.stop(), self.step() + + if step == 0: + return 0 + + in_range = (start <= x < stop) if step > 0 else (stop < x <= start) + + if in_range: + re = ((x - start) % step) == 0 + return int(re) + return 0 + + def mp_subscript_impl( + self, + tx: "InstructionTranslator", + key: VariableTracker, + ) -> VariableTracker: + # range_subscript: https://github.com/python/cpython/blob/62a6e898e01/Objects/rangeobject.c#L729-L748 + # CPython: range_subscript checks _PyIndex_Check → PyNumber_Index for non-slice keys + try: + key_type = key.python_type() + except NotImplementedError: + key_type = None + if key_type not in (int, bool, slice): + if key_type is not None and not hasattr(key_type, "__index__"): + raise_observed_exception( + TypeError, + tx, + args=[ + f"range indices must be integers or slices, not {key.python_type_name()}" + ], + ) + key = key.nb_index_impl(tx) + return self.getitem_const(tx, key) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .builder import SourcelessBuilder + + if name == "__iter__": + if not all(var.is_python_constant() for var in self.items): + # Can't represent a `range_iterator` without well defined bounds + return variables.misc.DelayGraphBreakVariable( + msg="Cannot create range_iterator: bounds (start, stop, step) must be fully defined as concrete constants.", + ) + return RangeIteratorVariable( + self.start(), self.stop(), self.step(), self.range_length() + ) + elif name in ("count", "__contains__"): + return SourcelessBuilder.create(tx, self.range_count(*args)) + elif name == "index": + x = args[0].as_python_constant() + start, stop, step = self.start(), self.stop(), self.step() + in_range = (start <= x < stop) if step > 0 else (stop < x <= start) + if in_range and ((x - start) % step) == 0: + return VariableTracker.build(tx, (x - start) // step) + raise_observed_exception( + ValueError, + tx, + args=[f"{x} is not in range"], + ) + elif name in cmp_name_to_op_mapping: + other = args[0] + pt = other.python_type() + if name not in ("__eq__", "__ne__"): + msg = f"{name} not supported between instances of 'range' and '{pt}'" + raise_observed_exception( + TypeError, + tx, + args=[msg], + ) + + if pt is not range: + return VariableTracker.build(tx, NotImplemented) + + if isinstance(other, RangeVariable): + cmp = self.range_equals(other) + else: + cmp = False + + # Two ranges are equal if they produce the same sequence of values + if name == "__eq__": + return SourcelessBuilder.create(tx, cmp) + else: + return SourcelessBuilder.create(tx, not cmp) + return super().call_method(tx, name, args, kwargs) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + fields = ["start", "stop", "step"] + if name in fields: + return self.items[fields.index(name)] + return super().var_getattr(tx, name) + + def is_python_hashable(self) -> Literal[True]: + return True + + def get_python_hash(self) -> int: + l = self.range_length() + start = self.start() + step = self.step() + return hash((l, start, step)) + + def is_python_equal(self, other: object) -> bool: + if not isinstance(other, variables.RangeVariable): + return False + + return ( + self.start() == other.start() + and self.step() == other.step() + and self.stop() == other.stop() + ) + + +class CommonListMethodsVariable(BaseListVariable): + """ + Implement methods common to List and other List-like things + """ + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .tensor import SymNodeVariable + + if name == "append" and self.is_mutable(): + if kwargs or len(args) != 1: + raise_args_mismatch( + tx, + name, + "1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + (arg,) = args + tx.output.side_effects.mutation(self) + self.items.append(arg) + return ConstantVariable.create(None) + elif name == "extend" and self.is_mutable(): + if kwargs or len(args) != 1: + raise_args_mismatch( + tx, + name, + "1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + + if not args[0].has_force_unpack_var_sequence(tx): + raise_observed_exception( + TypeError, tx, args=[f"{type(args[0])} object is not iterable"] + ) + + (arg,) = args + arg.force_apply_to_var_sequence( + tx, lambda item: self.call_method(tx, "append", [item], {}) + ) + return ConstantVariable.create(None) + elif name == "insert" and self.is_mutable(): + if kwargs or len(args) != 2: + raise_args_mismatch( + tx, + name, + "2 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + idx, value = args + if isinstance(idx, SymNodeVariable): + const_idx = idx.evaluate_expr() + else: + const_idx = idx.as_python_constant() + tx.output.side_effects.mutation(self) + # type: ignore[arg-type] + self.items.insert(const_idx, value) + return ConstantVariable.create(None) + elif name == "pop" and self.is_mutable(): + if kwargs or len(args) > 1: + raise_args_mismatch( + tx, + name, + "at most 1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + + if len(self.items) == 0: + raise_observed_exception(IndexError, tx, args=["pop from empty list"]) + + if len(args): + idx = args[0].as_python_constant() + if idx >= len(self.items): + raise_observed_exception( + IndexError, tx, args=["pop index out of range"] + ) + tx.output.side_effects.mutation(self) + return self.items.pop(*[a.as_python_constant() for a in args]) + elif name == "clear" and self.is_mutable(): + if args or kwargs: + raise_args_mismatch( + tx, + name, + "0 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + tx.output.side_effects.mutation(self) + self.items.clear() + return ConstantVariable.create(None) + elif name == "__setitem__" and self.is_mutable() and args: + # Realize args[0] to get the concrete type for proper type checking + key = args[0].realize() + if not ( + key.is_python_constant() + or isinstance(key, SymNodeVariable) + or ( + isinstance(key, SliceVariable) + and all( + s.is_python_constant() or isinstance(s, SymNodeVariable) + for s in key.items + ) + ) + ): + return super().call_method(tx, name, args, kwargs) + if kwargs: + raise_args_mismatch(tx, name, "0 kwargs", f"{len(kwargs)} kwargs") + value = args[1] + tx.output.side_effects.mutation(self) + if isinstance(key, SymNodeVariable): + # pyrefly: ignore[unsupported-operation] + self.items[key.evaluate_expr()] = value + elif isinstance(key, SliceVariable): + if key.is_python_constant(): + self.items[key.as_python_constant()] = list(value.items) # type: ignore[attr-defined] + else: + items_slice = slice( + *[ + ( + s.evaluate_expr() + if isinstance(s, SymNodeVariable) + else s.as_python_constant() + ) + for s in key.items + ] + ) + self.items[items_slice] = list(value.items) # type: ignore[attr-defined] + else: + self.items[key.as_python_constant()] = value + return ConstantVariable.create(None) + elif name == "__delitem__" and self.is_mutable(): + if kwargs or len(args) != 1: + raise_args_mismatch( + tx, + name, + "1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + + tx.output.side_effects.mutation(self) + if args[0].is_python_constant() and isinstance( + args[0].as_python_constant(), (int, slice) + ): + if isinstance(args[0], SymNodeVariable): + idx = args[0].evaluate_expr() + else: + idx = args[0].as_python_constant() + + try: + self.items.__delitem__(idx) # type: ignore[arg-type] + + except (IndexError, ValueError) as exc: + raise_observed_exception( + type(exc), + tx, + args=list(exc.args), + ) + else: + msg = f"list indices must be integers or slices, not {args[0].python_type_name()}" + raise_type_error(tx, msg) + return ConstantVariable.create(None) + elif name == "copy": + # List copy() doesn't have args and kwargs + if args or kwargs: + raise_args_mismatch( + tx, + name, + "0 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + items_lst: list[VariableTracker] = list(self.items) + return self.modified(items_lst, mutation_type=ValueMutationNew()) + elif name == "reverse" and self.is_mutable(): + if args or kwargs: + raise_args_mismatch( + tx, + name, + "0 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + self.items.reverse() + tx.output.side_effects.mutation(self) + return ConstantVariable.create(None) + elif name == "remove" and self.is_mutable(): + if kwargs or len(args) != 1: + raise_args_mismatch( + tx, + name, + "1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + + idx = self.call_method(tx, "index", args, kwargs) + self.call_method(tx, "pop", [idx], {}) + return ConstantVariable.create(None) + else: + return super().call_method(tx, name, args, kwargs) + + +class ListVariable(CommonListMethodsVariable): + # PyList_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/listobject.c#L3776 + _cpython_type = list + _has_instance_dict = False + + def python_type(self) -> type: + return list + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(length={len(self.items)})" + + def debug_repr(self) -> str: + return self.debug_repr_helper("[", "]") + + def reconstruct(self, codegen: "PyCodegen") -> None: + if self._contains_self_reference(): + # Self-referential list: create empty, cache, then extend + codegen.extend_output( + [ + create_instruction("BUILD_LIST", arg=0), + create_dup_top(), + ] + ) + codegen.add_cache(self) + + codegen.foreach(self.items) + codegen.extend_output( + [ + create_instruction("BUILD_LIST", arg=len(self.items)), + create_instruction("LIST_EXTEND", arg=1), + ] + ) + else: + # Non-self-referential: use simple codegen + codegen.foreach(self.items) + codegen.append_output(create_instruction("BUILD_LIST", arg=len(self.items))) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .tensor import SymNodeVariable + + if name == "__setitem__" and self.is_mutable(): + if kwargs or len(args) != 2: + raise_args_mismatch( + tx, + name, + "2 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + key, value = args + + if not key.is_python_constant(): + # probably will graph-break + super().call_method(tx, name, args, kwargs) + + tx.output.side_effects.mutation(self) + if isinstance(key, SliceVariable): + if not value.has_force_unpack_var_sequence(tx): + raise_observed_exception( + TypeError, tx, args=["can only assign an iterable"] + ) + + key_as_const = key.as_python_constant() + if key_as_const.step == 0: + raise_observed_exception( + ValueError, tx, args=["slice step cannot be zero"] + ) + + value_unpack = value.force_unpack_var_sequence(tx) + try: + self.items[key_as_const] = value_unpack + except Exception as exc: + raise_observed_exception( + type(exc), + tx, + args=list(exc.args), + ) + else: + if isinstance(key, SymNodeVariable): + key = key.evaluate_expr() + else: + key = key.as_python_constant() + + try: + # pyrefly: ignore[unsupported-operation] + self.items[key] = value + except (IndexError, TypeError) as e: + raise_observed_exception( + type(e), tx, args=[VariableTracker.build(tx, a) for a in e.args] + ) + return ConstantVariable.create(None) + + if name == "sort" and self.is_mutable(): + if len(args) != 0: + raise_args_mismatch(tx, name, "0 args", f"{len(args)} args") + key_fn_var = kwargs.pop("key", ConstantVariable.create(None)) + reverse = kwargs.pop( + "reverse", ConstantVariable.create(False) + ).as_python_constant() + if len(kwargs) != 0: + raise_args_mismatch(tx, name, "0 kwargs", f"{len(kwargs)} kwargs") + + if key_fn_var.is_constant_none(): + keys = self.items.copy() + else: + keys = [key_fn_var.call_function(tx, [x], {}) for x in self.items] + + if not all(k.is_python_constant() for k in keys): + first_non_constant_key = None + for k in keys: + if not k.is_python_constant(): + first_non_constant_key = k + assert first_non_constant_key is not None + + try: + python_type = str(first_non_constant_key.python_type()) + except NotImplementedError: + python_type = "unknown" + + unimplemented( + gb_type="sort with non-constant keys", + context=str(first_non_constant_key), + explanation=( + f"Cannot perform sort with non-constant key. " + f"First non-constant key type: {python_type}. " + f"Most notably, we cannot sort with Tensor or SymInt keys, but we can " + f"sort ints." + ), + hints=["Use something else as the key."], + ) + + try: + tx.output.side_effects.mutation(self) + sorted_items_with_keys = sorted( + ( + ( + x, + k.as_python_constant(), + -i if reverse else i, # extra key to ensure stable sort + ) + for i, (k, x) in enumerate(zip(keys, self.items)) + ), + key=operator.itemgetter(1, 2), + reverse=reverse, + ) + self.items[:] = [x for x, *_ in sorted_items_with_keys] + except Exception as e: + raise_observed_exception(type(e), tx, args=list(e.args)) + return ConstantVariable.create(None) + + if name == "__init__" and self.is_mutable(): + if kwargs: + raise_args_mismatch(tx, name, "0 kwargs", f"{len(kwargs)} kwargs") + if len(args) == 0: + return ConstantVariable.create(None) + elif len(args) == 1 and args[0].has_force_unpack_var_sequence(tx): + (arg,) = args + tx.output.side_effects.mutation(self) + self.items[:] = arg.force_unpack_var_sequence(tx) + return ConstantVariable.create(None) + + return super().call_method(tx, name, args, kwargs) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + if name == "__class__": + source = AttrSource(self.source, name) if self.source else None + class_type = self.python_type() + if class_type is list: + return VariableTracker.build(tx, class_type, source=source) + else: + return VariableTracker.build(tx, class_type, source) + return super().var_getattr(tx, name) + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> ConstantVariable: + if self.python_type() is not list: + return super().call_obj_hasattr(tx, name) + return VariableTracker.build(tx, hasattr([], name)) + + def is_python_hashable(self) -> bool: + return False + + +# TODO(follow-up): DequeVariable inherits BaseListVariable.mp_subscript_impl which +# accepts slices. CPython's deque only has sq_item (Modules/_collectionsmodule.c:1888), +# not mp_subscript — deque[slice] should raise TypeError. Override mp_subscript_impl +# to reject slices and only accept integer-like keys via _PyIndex_Check → nb_index_impl. +# Also add tests for: negative index, __index__ object key, invalid type key. +class DequeVariable(CommonListMethodsVariable): + # deque_spec: https://github.com/python/cpython/blob/v3.13.0/Modules/_collectionsmodule.c#L1866 + _cpython_type = collections.deque + + def __init__( + self, + items: list[VariableTracker], + maxlen: VariableTracker | None = None, + **kwargs: Any, + ) -> None: + if maxlen is None: + maxlen = ConstantVariable.create(None) + assert maxlen.is_python_constant(), ( + f"maxlen must be a constant, got: {maxlen.debug_repr()}" + ) + self.maxlen = maxlen + items = list(items) + if self.maxlen.as_python_constant() is not None: + items = items[-maxlen.as_python_constant() :] + super().__init__(items, **kwargs) + + def python_type(self) -> type: + return collections.deque + + def debug_repr(self) -> str: + if self.maxlen.as_python_constant() is None: + return self.debug_repr_helper( + "deque([", "], maxlen=" + self.maxlen.debug_repr() + ")" + ) + return self.debug_repr_helper("deque([", "])") + + def as_python_constant(self) -> collections.deque[Any]: + return self.python_type()( + [x.as_python_constant() for x in self.items], + maxlen=self.maxlen.as_python_constant(), + ) + + def reconstruct(self, codegen: "PyCodegen") -> None: + # To deal with self-referential sets + codegen.add_push_null( + lambda: codegen.append_output( + codegen.create_load_python_module(collections.deque) # type: ignore[arg-type] + ) + ) + codegen.append_output(create_instruction("BUILD_LIST", arg=0)) + codegen(self.maxlen) + codegen.extend_output( + [ + *codegen.create_call_function_kw(2, ("maxlen",), False), + create_dup_top(), + ] + ) + codegen.add_cache(self) + + codegen.append_output(create_dup_top()) + codegen.load_method("extend") + + codegen.foreach(self.items) + codegen.extend_output( + [ + create_instruction("BUILD_LIST", arg=len(self.items)), + *create_call_method(1), + create_instruction("POP_TOP"), + ] + ) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + if name == "maxlen": + return self.maxlen + return super().var_getattr(tx, name) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if ( + name == "__setitem__" + and self.is_mutable() + and args + and args[0].is_python_constant() + ): + if kwargs or len(args) != 2: + raise_args_mismatch( + tx, + name, + "2 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + key, value = args + assert key.is_python_constant() + assert isinstance(key.as_python_constant(), int) + tx.output.side_effects.mutation(self) + self.items[key.as_python_constant()] = value + return ConstantVariable.create(None) + + maxlen = self.maxlen.as_python_constant() + if maxlen is not None: + slice_within_maxlen = slice(-maxlen, None) + else: + slice_within_maxlen = None + + if ( + name == "extendleft" + and self.is_mutable() + and len(args) > 0 + and args[0].has_force_unpack_var_sequence(tx) + ): + if kwargs or len(args) != 1: + raise_args_mismatch( + tx, + name, + "1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + # NOTE this is inefficient, but the alternative is to represent self.items + # as a deque, which is a more intrusive change. + args[0].force_apply_to_var_sequence( + tx, lambda item: self.call_method(tx, "appendleft", [item], {}) + ) + slice_within_maxlen = slice(None, maxlen) + result = ConstantVariable.create(None) + elif name == "popleft" and self.is_mutable(): + if kwargs or len(args) > 0: + raise_args_mismatch( + tx, + name, + "0 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + tx.output.side_effects.mutation(self) + result, *self.items[:] = self.items + elif name == "appendleft" and len(args) > 0 and self.is_mutable(): + if kwargs or len(args) != 1: + raise_args_mismatch( + tx, + name, + "1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + tx.output.side_effects.mutation(self) + self.items[:] = [args[0], *self.items] + slice_within_maxlen = slice(None, maxlen) + result = ConstantVariable.create(None) + elif name == "insert" and len(args) > 0 and self.is_mutable(): + if kwargs or len(args) != 2: + raise_args_mismatch( + tx, + name, + "2 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + if maxlen is not None and len(self.items) == maxlen: + raise_observed_exception( + IndexError, tx, args=["deque already at its maximum size"] + ) + result = super().call_method(tx, name, args, kwargs) + else: + result = super().call_method(tx, name, args, kwargs) + + if ( + slice_within_maxlen is not None + and maxlen is not None + and len(self.items) > maxlen + ): + self.items[:] = self.items[slice_within_maxlen] + return result + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> ConstantVariable: + if self.python_type() is collections.deque: + return VariableTracker.build(tx, name in collections.deque.__dict__) + return super().call_obj_hasattr(tx, name) + + +class TupleVariable(BaseListVariable): + # PyTuple_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/tupleobject.c#L846 + _cpython_type = tuple + + def python_type(self) -> type[tuple]: # type: ignore[type-arg] + return tuple + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(length={len(self.items)})" + + def debug_repr(self) -> str: + return self.debug_repr_helper("(", ")") + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.foreach(self.items) + codegen.append_output(create_build_tuple(len(self.items))) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + if name == "__class__": + source = AttrSource(self.source, name) if self.source else None + class_type = self.python_type() + return VariableTracker.build(tx, class_type, source=source) + return super().var_getattr(tx, name) + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> ConstantVariable: + if self.python_type() is not tuple: + return super().call_obj_hasattr(tx, name) + return VariableTracker.build(tx, hasattr((), name)) + + def is_python_hashable(self) -> bool: + return all(item.is_python_hashable() for item in self.items) + + def get_python_hash(self) -> int: + items = tuple(x.get_python_hash() for x in self.items) + return hash(items) + + def is_python_equal(self, other: object) -> bool: + return isinstance(other, variables.TupleVariable) and all( + a.is_python_equal(b) for (a, b) in zip(self.items, other.items) + ) + + +class SizeVariable(TupleVariable): + """torch.Size(...)""" + + _cpython_type = torch.Size + + _nonvar_fields = { + "proxy", + *TupleVariable._nonvar_fields, + } + + def __init__( + self, + items: list[VariableTracker], + proxy: torch.fx.Proxy | None = None, + **kwargs: Any, + ) -> None: + self.proxy = proxy + super().__init__(items, **kwargs) + + def debug_repr(self) -> str: + return self.debug_repr_helper("torch.Size([", "])") + + def python_type(self) -> type: + return torch.Size + + def as_proxy(self) -> Any: + if self.proxy is not None: + return self.proxy + + # torch.Size needs special handling. Normally, we pun a list-like + # container to directly contain Proxy/Node objects from FX, and FX + # knows to look inside containers (via map_aggregate). But torch.Size + # is weird; although it subclasses from tuple, it doesn't allow + # members which aren't int-like (rejecting Proxy and Node). This + # means we can't use the normal representation trick + # torch.Size([proxy0, proxy1]). I looked into seeing if I could + # relax torch.Size in PyTorch proper, but if torch.Size constructor + # sees a type that it doesn't recognize, it will try to call + # __index__() on it, so there is no BC way to actually change this + # behavior (though it occurs to me that I could have just added a + # YOLO no checking alternate constructor.) + # + # To work around this problem, I represent a torch.Size proxy as + # a straight up proxy, that would have been constructed by taking + # the constituent proxies as arguments. This trick can be generally + # used for any construct that we need a proxy for but we can't + # directly represent as an aggregate; I don't see very many examples + # of this in torchdynamo though! + + # Look for a proxy. If there are none, do the legacy behavior + tracer = None + proxies = self._as_proxy() + for proxy in proxies: + if isinstance(proxy, torch.fx.Proxy): + tracer = proxy.tracer + break + + if tracer is None: + return torch.Size(proxies) + + proxy = tracer.create_proxy("call_function", torch.Size, (proxies,), {}) + set_example_value( + proxy.node, + torch.Size( + [ + p.node.meta["example_value"] if not isinstance(p, int) else p + for p in proxies + ] + ), + ) + return proxy + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null(lambda: codegen.load_import_from("torch", "Size")) + codegen.foreach(self.items) + build_torch_size = [ + create_build_tuple(len(self.items)), + ] + create_call_function(1, False) + codegen.extend_output(build_torch_size) + + def unpack_var_sequence(self, tx: "InstructionTranslator") -> list[VariableTracker]: + return list(self.items) + + def numel(self, tx: "InstructionTranslator") -> VariableTracker: + from .builder import SourcelessBuilder + from .tensor import SymNodeVariable + + const_result = 1 + # pyrefly: ignore [implicit-any] + sym_sizes = [] + + for v in self.items: + if v.is_python_constant(): + const_result *= v.as_python_constant() + else: + assert isinstance(v, SymNodeVariable), type(v) + # Delay proxy calls until we know it will be necessary + sym_sizes.append(v) + + result = VariableTracker.build(tx, const_result) + if sym_sizes and const_result == 1: + # Skip multiplying by 1 + result, *sym_sizes = sym_sizes + + if not sym_sizes or const_result == 0: + return result + + mul = SourcelessBuilder.create(tx, operator.mul) + for v in sym_sizes: + result = mul.call_function(tx, [result, v], {}) + return result + + def mp_subscript_impl( + self, + tx: "InstructionTranslator", + key: VariableTracker, + ) -> VariableTracker: + # tuple_subscript: https://github.com/python/cpython/blob/62a6e898e01/Objects/tupleobject.c#L877-L930 + # CPython: tuplesubscript checks _PyIndex_Check → PyNumber_AsSsize_t for non-slice keys + try: + key_type = key.python_type() + except NotImplementedError: + key_type = None + if key_type not in (int, bool, slice): + if key_type is not None and not hasattr(key_type, "__index__"): + raise_observed_exception( + TypeError, + tx, + args=[ + f"tuple indices must be integers or slices, not {key.python_type_name()}" + ], + ) + key = key.nb_index_impl(tx) + return self.get_item_dyn(tx, key) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "numel": + if args or kwargs: + raise_args_mismatch( + tx, + name, + "0 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + return self.numel(tx) + + return super().call_method(tx, name, args, kwargs) + + def get_item_dyn( + self, tx: "InstructionTranslator", arg: VariableTracker + ) -> VariableTracker: + from .tensor import SymNodeVariable, TensorVariable + + if isinstance(arg, SymNodeVariable): + index = arg.sym_num + elif isinstance(arg, TensorVariable): + value = get_fake_value(arg.as_proxy().node, tx) + if value.constant is None or value.constant.numel() != 1: + unimplemented( + gb_type="Indexing torch.Size with non-scalar tensor", + context=f"get_item_dyn {self} {arg}", + explanation=( + "Attempted to index torch.Size with a tensor that is not a scalar constant." + ), + hints=[*graph_break_hints.USER_ERROR], + ) + index = value.constant.item() + else: + index = arg.as_python_constant() + + if isinstance(index, slice): + return SizeVariable(self.items[index]) + else: + assert isinstance(index, (int, torch.SymInt)) + return self.items[index] + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> ConstantVariable: + return VariableTracker.build(tx, hasattr(torch.Size, name)) + + +class SliceVariable(VariableTracker): + # PySlice_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/sliceobject.c#L689 + _cpython_type = slice + + def __init__( + self, + items: Sequence[VariableTracker], + tx: Optional["InstructionTranslator"] = None, + **kwargs: Any, + ) -> None: + items_to_map = items + start, stop, step = [variables.ConstantVariable.create(None)] * 3 + + if len(items_to_map) == 1: + (stop,) = items_to_map + elif len(items_to_map) == 2: + start, stop = items_to_map + elif len(items_to_map) == 3: + start, stop, step = items_to_map + else: + raise AssertionError + + # Convert TensorVariable to SymIntVariable by calling .item() + # This decomposes a[:t] to u=t.item(); a[:u] at the dynamo level + if start.is_tensor(): + assert tx is not None, ( + "tx is required when slice indices are TensorVariables" + ) + start = start.call_method(tx, "item", [], {}) + if stop.is_tensor(): + assert tx is not None, ( + "tx is required when slice indices are TensorVariables" + ) + stop = stop.call_method(tx, "item", [], {}) + if step.is_tensor(): + assert tx is not None, ( + "tx is required when slice indices are TensorVariables" + ) + step = step.call_method(tx, "item", [], {}) + + self.items = (start, stop, step) + + super().__init__(**kwargs) + + def debug_repr(self) -> str: + return "slice(" + ", ".join(i.debug_repr() for i in self.items) + ")" + + def as_proxy(self) -> slice: + return slice(*[x.as_proxy() for x in self.items]) + + def python_type(self) -> type: + return slice + + def as_python_constant(self) -> slice: + return slice(*[guard_if_dyn(x) for x in self.items]) + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.foreach(self.items) + codegen.append_output(create_instruction("BUILD_SLICE", arg=len(self.items))) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + if name in cmp_name_to_op_mapping: + return variables.GetAttrVariable( + self, name, py_type=type(getattr(slice, name)) + ) + fields = ["start", "stop", "step"] + if name not in fields: + unimplemented( + gb_type="Unsupported attribute for slice() object", + context=f"var_getattr {self} {name}", + explanation=f"Expected attribute to be one of {','.join(fields)} " + f"but got {name}", + hints=[*graph_break_hints.USER_ERROR], + ) + return self.items[fields.index(name)] + + +class ListIteratorVariable(IteratorVariable): + # PyListIter_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/listobject.c#L3842 + _cpython_type = type(iter([])) + + _nonvar_fields = { + "index", + *IteratorVariable._nonvar_fields, + } + + def __init__( + self, items: list[VariableTracker], index: int = 0, **kwargs: Any + ) -> None: + super().__init__(**kwargs) + assert isinstance(items, list) + # Removing this check as it slows things down too much + # https://github.com/pytorch/pytorch/pull/87533#issuecomment-1287574492 + + # assert all(isinstance(x, VariableTracker) for x in items) + self.items = items + self.index = index + self.is_exhausted = False + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(length={len(self.items)}, index={repr(self.index)})" + + def next_variable(self, tx: "InstructionTranslator") -> VariableTracker: + assert self.is_mutable() + old_index = self.index + if old_index >= len(self.items) or self.is_exhausted: + self.is_exhausted = True + raise_observed_exception(StopIteration, tx) + + tx.output.side_effects.mutation(self) + self.index += 1 + return self.items[old_index] + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> ConstantVariable: + return VariableTracker.build(tx, hasattr(iter([]), name)) + + def python_type(self) -> type: + return type(iter([])) + + def as_python_constant(self) -> Any: + if self.index > 0: + raise NotImplementedError + return iter([x.as_python_constant() for x in self.items]) + + def has_unpack_var_sequence(self, tx: "InstructionTranslator") -> bool: + return True + + def unpack_var_sequence(self, tx: "InstructionTranslator") -> list[VariableTracker]: + if self.is_exhausted: + return [] + self.is_exhausted = True + return list(self.items[self.index :]) + + def force_unpack_var_sequence( + self, tx: "InstructionTranslator" + ) -> list[VariableTracker]: + return self.unpack_var_sequence(tx) + + def reconstruct(self, codegen: "PyCodegen") -> None: + if not self.is_exhausted: + remaining_items = self.items[self.index :] + else: + # pyrefly: ignore [implicit-any] + remaining_items = [] + codegen.foreach(remaining_items) + codegen.extend_output( + [ + create_build_tuple(len(remaining_items)), + create_instruction("GET_ITER"), + ] + ) + + +class TupleIteratorVariable(ListIteratorVariable): + # PyTupleIter_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/tupleobject.c#L1067 + _cpython_type = type(iter(())) + + +class RangeIteratorVariable(IteratorVariable): + # PyRangeIter_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/rangeobject.c#L896 + _cpython_type = type(iter(range(0))) + + # only needed for isinstance(..., range_iterator) to work + _nonvar_fields = { + "iter_obj", + } + + def __init__( + self, start: int, stop: int, step: int, len_: int, **kwargs: Any + ) -> None: + super().__init__(**kwargs) + self.start = start + self.stop = stop + self.step = step + self.len = len_ + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "__next__": + return self.next_variable(tx) + elif name == "__iter__": + return self + return super().call_method(tx, name, args, kwargs) + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> ConstantVariable: + if self.python_type() is range_iterator: + ri = iter(range(0)) + return VariableTracker.build(tx, hasattr(ri, name)) + return super().call_obj_hasattr(tx, name) + + def next_variable(self, tx: "InstructionTranslator") -> VariableTracker: + if self.len <= 0: + raise_observed_exception(StopIteration, tx) + + self.len -= 1 + current = self.start + self.start += self.step + return VariableTracker.build(tx, current) + + def python_type(self) -> type: + return range_iterator + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.append_output(codegen.create_load_python_module(range)) # type: ignore[arg-type] + ) + codegen.append_output(codegen.create_load_const(self.start)) + codegen.append_output(codegen.create_load_const(self.stop)) + codegen.append_output(codegen.create_load_const(self.step)) + codegen.extend_output(create_call_function(3, False)) + codegen.append_output(create_instruction("GET_ITER")) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/misc.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/misc.py new file mode 100644 index 0000000000000000000000000000000000000000..c297b0a7136efd9fd21aab772c7ff94166e8d6b7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/misc.py @@ -0,0 +1,2478 @@ +""" +This module contains miscellaneous variable tracker implementations for various Python types +and features used in Dynamo's symbolic execution. These classes help track and propagate +information about different kinds of variables during graph capture. + +Key classes include: +- SuperVariable: Handles super() calls and method resolution +- ExceptionVariable: Tracks exception objects +- RandomVariable: Manages random number generators +- GetAttrVariable: Tracks attribute access +- MethodWrapperVariable: Handles method wrappers +- PythonModuleVariable: Tracks Python modules +- NumpyVariable: Handles numpy functions and types +- StringFormatVariable: Manages string formatting +- DebuggingVariable: Handles print and logging +""" + +import builtins +import dataclasses +import enum +import functools +import inspect +import itertools +import logging +import random +import re +import sys +import traceback +import types +import weakref +from collections.abc import Callable, Sequence +from random import Random +from types import BuiltinFunctionType +from typing import Any, Literal, TYPE_CHECKING, TypeGuard, Union + +import torch._C +import torch._numpy as tnp +import torch.utils._pytree as pytree +from torch._dynamo.variables.base import MutationType +from torch._dynamo.variables.lists import TupleVariable +from torch._guards import Source + +from .. import config, graph_break_hints, trace_rules, variables +from ..bytecode_transformation import ( + create_call_function, + create_call_function_ex, + create_instruction, +) +from ..create_parameter_op import do_not_convert_to_tracable_parameter +from ..exc import raise_observed_exception, raise_type_error, unimplemented +from ..guards import GuardBuilder, install_guard +from ..mutation_guard import unpatched_nn_module_init +from ..source import ( + AttrSource, + GenericAttrSource, + GetItemSource, + TypeMROSource, + TypeSource, + WeakRefCallSource, +) +from ..utils import ( + check_unspec_or_constant_args, + cmp_name_to_op_mapping, + identity, + is_tensor_base_attr_getter, + istype, + proxy_args_kwargs, + raise_args_mismatch, +) +from .base import AsPythonConstantNotImplementedError, NO_SUCH_SUBOBJ, VariableTracker +from .constant import ConstantVariable +from .functions import NestedUserFunctionVariable, UserFunctionVariable +from .user_defined import call_random_fn, is_standard_setattr, UserDefinedObjectVariable + + +if TYPE_CHECKING: + from torch._dynamo.codegen import PyCodegen + from torch._dynamo.symbolic_convert import InstructionTranslator + + +class SuperVariable(VariableTracker): + # PySuper_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/typeobject.c#L11511 + _cpython_type = super + + _nonvar_fields = { + *VariableTracker._nonvar_fields, + } + + def __init__( + self, + typevar: VariableTracker, + objvar: VariableTracker | None = None, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + # typevar is the first argument to super(). In the case where no argument + # is provided to super(), it is the __class__ object where + # the super() function is being called + self.typevar = typevar + # objvar here must be an instance or subtype of typevar. + # In the case where super() is called without arguments, it is the first argument + # to the current function where super() is called from (self for regular method, + # cls for a classmethod) + self.objvar = objvar + + def python_type(self) -> type: + return builtins.super + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null(lambda: codegen(variables.BuiltinVariable(super))) + codegen(self.typevar) + if self.objvar is not None: + codegen(self.objvar) + codegen.extend_output(create_call_function(2, False)) + else: + codegen.extend_output(create_call_function(1, False)) + + def _resolved_getattr_and_source( + self, tx: "InstructionTranslator", name: str + ) -> tuple[Any, AttrSource | None]: + if not self.objvar: + unimplemented( + gb_type="1-arg super not implemented", + context="", + explanation=f"Dynamo failed to trace attribute `{name}` accessed " + f"via `super()` (for type `{self.typevar}` and object `{self.objvar}`) " + "because one-argument of super() is not supported.", + hints=[ + "Use two-argument super(type, object_or_type).", + ], + ) + assert self.objvar is not None + search_type = self.typevar.as_python_constant() + + # The rest of this function does two things: + # - Walk the mro to find where the attribute comes from to be + # able to provide accurate source + # - Call the getattr to get the object + + # Find the class object, where the function lives. + # When objvar is "self", use type(self), when objvar is "cls", use it as-is + type_to_use = self.objvar.python_type() + type_to_use_source: Source | None = ( + TypeSource(self.objvar.source) if self.objvar.source else None + ) + if issubclass(type_to_use, type): + type_to_use = self.objvar.value # type: ignore[attr-defined] + type_to_use_source = self.objvar.source + + source = None + search_mro = type_to_use.__mro__ + + try: + start_index = search_mro.index(search_type) + 1 + except ValueError: + # Corner case where the typevar is not in the mro of the objvar + # https://github.com/python/cpython/blob/3.11/Objects/typeobject.c#L8843-L8844 + return getattr(super(search_type, type_to_use), name), None + # Implemented based on https://github.com/python/cpython/blob/3.11/Objects/typeobject.c#L8812 + # super has its getattro implementation. The key point is that instead of calling getattr, it checks the + # attribute in the class __dict__ + for index in range(start_index, len(search_mro)): + # Dont call getattr, just check the __dict__ of the class + if resolved_getattr := search_mro[index].__dict__.get(name, NO_SUCH_SUBOBJ): + if resolved_getattr is not NO_SUCH_SUBOBJ: + # Equivalent of something like type(L['self']).__mro__[1].attr_name + if type_to_use_source: + source = AttrSource( + GetItemSource(TypeMROSource(type_to_use_source), index), + name, + ) + return resolved_getattr, source + + unimplemented( + gb_type="Unable to resolve super getattr", + context="", + explanation=f"Dynamo failed to trace attribute `{name}` accessed " + f"via `super()` (for type `{self.typevar}` and object `{self.objvar}`) " + "because the resolved attribute type is not supported.", + hints=[ + "Ensure the attribute exists in the parent class.", + "Check the arguments passed to `super()`.", + ], + ) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + # Check if getattr is a constant. If not, delay the actual work by + # wrapping the result in GetAttrVariable. Mostly super is called with a + # method, so most of the work is delayed to call_function. + # + # We could have just implemented a const_getattr. However, super is + # special when it comes to finding sources. Compared to other VTs, super + # requires the attr name to walk the mro and find the actual source (and + # not just AttrSource). + value, source = self._resolved_getattr_and_source(tx, name) + if not variables.ConstantVariable.is_literal(value): + return GetAttrVariable(self, name, py_type=type(value)) + if source: + install_guard(source.make_guard(GuardBuilder.CONSTANT_MATCH)) + return variables.ConstantVariable.create(value, source=source) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + inner_fn, source = self._resolved_getattr_and_source(tx, name) + assert self.objvar is not None + # This essentially simulates CPython's `super_getattro`: + # https://github.com/python/cpython/blob/a1c52d1265c65bcf0d9edf87e143843ad54f9b8f/Objects/typeobject.c#L11138-L11168 + # where `inner_fn` is the VT for `res = _super_lookup_descr(...)`. + # + # However, `res`'s type needs to be checked for `tp_descr_get`, and + # applied if it has one. We currently don't have polyfills for all the + # relevant `tp_descr_get`, so we explicitly handle the cases we care + # about here (e.g., note the staticmethod, classmethod cases). + if inner_fn is object.__init__: + return LambdaVariable(identity) + elif inner_fn is torch.nn.Module.__init__: + objvar = self.objvar + from ..side_effects import AttributeMutationNew + + if ( + isinstance(objvar, variables.UserDefinedObjectVariable) + and isinstance(objvar.mutation_type, AttributeMutationNew) + and not (args or kwargs) + ): + with do_not_convert_to_tracable_parameter(): + fn_vt = VariableTracker.build( + tx, unpatched_nn_module_init, source=source + ) + return fn_vt.call_function(tx, [self.objvar] + args, kwargs) + else: + unimplemented( + gb_type="Unsupported super().__init__() call", + context=f"call_method {self} {name} {args} {kwargs}", + explanation="Dynamo encountered a super().__init__() call " + f"on {objvar} that resolved to a `torch.nn.Module.__init__()` " + "call that we cannot trace.", + hints=[*graph_break_hints.DIFFICULT], + ) + elif ( + self.objvar.source + and hasattr(inner_fn, "__name__") + and inner_fn.__name__ == "__new__" + and variables.UserDefinedClassVariable.is_supported_new_method(inner_fn) + ): + user_cls = inner_fn.__self__ + if hasattr(user_cls, "__module__") and user_cls.__module__ == "builtins": + user_cls_vt: VariableTracker = VariableTracker.build(tx, user_cls) + else: + assert source is not None + user_cls_source = source.member + user_cls_vt = variables.UserDefinedClassVariable( + user_cls, source=user_cls_source + ) + return user_cls_vt.call_method(tx, "__new__", args, kwargs) + elif isinstance(inner_fn, staticmethod) and isinstance( + inner_fn.__func__, types.FunctionType + ): + fn_vt = VariableTracker.build( + tx, inner_fn.__func__, source=source, realize=True + ) + return fn_vt.call_function(tx, args, kwargs) + elif isinstance(inner_fn, classmethod) and isinstance( + inner_fn.__func__, types.FunctionType + ): + if isinstance(self.objvar, variables.UserDefinedClassVariable): + # super().classmethod is called from a classmethod itself. So, + # super was converted to super(__class__, cls) in bytecode and + # therefore we have to propagate the cls. + cls_variable = self.objvar + else: + # current function is an instance method, therefore super was + # converted to super(__class__, self). We have to find + # type(self) to bind the cls to the parent classmethod. + # Note that it can't be the self.typevar because __class__ is + # the class where the method is defined, which could be + # different from type(self) with polymorphism. + cls_source = None + if self.objvar.source: + cls_source = TypeSource(self.objvar.source) + cls_variable = VariableTracker.build( + tx, + self.objvar.value_type, # type: ignore[attr-defined] + cls_source, + ) + assert source is not None + fn_vt = VariableTracker.build( + tx, + inner_fn.__func__, + source=AttrSource(source, "__func__"), + realize=True, + ) + return fn_vt.call_function(tx, [cls_variable, *args], kwargs) + elif isinstance(inner_fn, types.FunctionType): + fn_vt = VariableTracker.build(tx, inner_fn, source=source, realize=True) + return fn_vt.call_function(tx, [self.objvar] + args, kwargs) + elif isinstance(inner_fn, types.MethodType): + return variables.UserMethodVariable( + inner_fn.__func__, self.objvar, source=source + ).call_function(tx, args, kwargs) + elif is_standard_setattr(inner_fn) and isinstance( + self.objvar, UserDefinedObjectVariable + ): + # type: ignore[arg-type] + return self.objvar.method_setattr_standard(tx, *args, **kwargs) + elif inner_fn is object.__delattr__: + attr = args[0] + try: + attr = attr.as_python_constant() + except NotImplementedError as exc: + unimplemented( + gb_type="Non-constant attribute given to `super().__delattr__()`", + context=f"call_method {self} {name}", + explanation="Dynamo requires the attribute name passed to " + "`super().__delattr__(...)` to be a constant (string).", + hints=[ + "Ensure the attribute name is a string literal or a constant variable." + ], + from_exc=exc, + ) + if not tx.output.side_effects.is_attribute_mutation(self.objvar): + unimplemented( + gb_type="Attempted super().__delattr__() on an object without mutation tracking", + context=f"call_method {self} {name}", + explanation="Dynamo needs to track mutations on an object " + "before `super().__delattr__` can be used on it. But the " + f"object ({self.objvar}) doesn't have attribute mutation " + "tracking enabled.", + hints=[ + "Ensure the object is tracked by Dynamo's side effect system.", + *graph_break_hints.DYNAMO_BUG, + ], + ) + assert isinstance(attr, str) + tx.output.side_effects.store_attr( + self.objvar, attr, variables.DeletedVariable() + ) + return variables.ConstantVariable.create(None) + elif ( + isinstance(self.objvar, variables.UserDefinedObjectVariable) + and self.objvar._base_vt is not None + and self.objvar._base_methods is not None + and inner_fn in self.objvar._base_methods + ): + return self.objvar._base_vt.call_method(tx, name, args, kwargs) + elif inner_fn is object.__getattribute__: + # object.__getattribute__ has no side-effects. We can directly call + # __getattribute__ to access the attribute. + attr_name = args[0].value # type: ignore[attr-defined] + if tx.output.side_effects.has_pending_mutation_of_attr( + self.objvar, attr_name + ): + result = tx.output.side_effects.load_attr( + self.objvar, attr_name, deleted_ok=True + ) + if isinstance(result, variables.DeletedVariable): + raise_observed_exception(AttributeError, tx) + return result + + attr_value = None + try: + # NB - use object.__getattribute__ to prevent running any user code + # type: ignore[attr-defined] + attr_value = object.__getattribute__(self.objvar.value, attr_name) + except AttributeError: + raise_observed_exception(AttributeError, tx) + + attr_source = None + if self.objvar.source is not None: + # setup a object.__getattribute__(self.objvar, name) source + attr_source = GenericAttrSource(self.objvar.source, attr_name) + return VariableTracker.build(tx, attr_value, attr_source) + elif inner_fn is torch._C._disabled_torch_function_impl: + # See `THPModule_disable_torch_function` for the C impl. + # The signature of _disabled_torch_function_impl is similar to + # `__torch_function__`, just without the first `cls` argument: + # * (func, types, args, kwargs) + func = args[0] + # pyrefly: ignore [implicit-any] + tf_kwargs = {} + tf_args = args[2].items # type: ignore[attr-defined] + # type: ignore[attr-defined] + for hash_key_vt, value_vt in args[3].items.items(): + key_str = hash_key_vt.vt.as_python_constant() + tf_kwargs[key_str] = value_vt + + tx_old = tx.symbolic_torch_function_state.torch_function_subclass_enabled + tx.symbolic_torch_function_state.torch_function_subclass_enabled = False + try: + return func.call_function(tx, tf_args, tf_kwargs) + finally: + tx.symbolic_torch_function_state.torch_function_subclass_enabled = ( + tx_old + ) + elif ( + isinstance(inner_fn, types.MethodDescriptorType) + and inner_fn in trace_rules.get_tensor_method() + ): + # FunctionType but implementation is in C, we support some of these, + # e.g., tensor ops like `torch.Tensor.to`. + fn_var = VariableTracker.build(tx, inner_fn, source, realize=True) + return fn_var.call_function(tx, [self.objvar] + args, kwargs) + + unimplemented( + gb_type="Attempted to call a super() attribute that is " + "not a function or method", + context=f"call_method {self} {name}", + explanation="Dynamo does not know how to trace the call " + f"`super().{name}()` because `super().{name}` is not a " + "function or method attribute.", + hints=[ + "Ensure the attribute accessed via `super()` is a standard method or function.", + ], + ) + + +class FrameSummaryVariable(VariableTracker): + def __init__(self, frame_summary: traceback.FrameSummary, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.frame_summary = frame_summary + + def python_type(self) -> type: + return traceback.FrameSummary + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + if name == "lineno": + return VariableTracker.build(tx, self.frame_summary.lineno) + elif name == "filename": + return VariableTracker.build(tx, self.frame_summary.filename) + elif name == "name": + return VariableTracker.build(tx, self.frame_summary.name) + elif name == "line": + return VariableTracker.build(tx, self.frame_summary.line) + return super().var_getattr(tx, name) + + +class TracebackVariable(VariableTracker): + def __init__( + self, + frame_summary: FrameSummaryVariable, + tb_next: Union["TracebackVariable", ConstantVariable], + **kwargs: Any, + ) -> None: + # The traceback holds four attributes: + # - tb_frame + # - tb_lineno + # - tb_lasti + # - tb_next + + super().__init__(**kwargs) + self.frame_summary = frame_summary + # the next traceback in the chain + assert tb_next is not None + self.tb_next = tb_next + + @classmethod + def from_frame_summary( + cls, + frame_summary: traceback.FrameSummary, + tb_next: Union["TracebackVariable", ConstantVariable], + ) -> "TracebackVariable": + return cls(FrameSummaryVariable(frame_summary), tb_next=tb_next) + + @staticmethod + def is_valid_traceback(obj: VariableTracker) -> bool: + return istype(obj, TracebackVariable) or obj.is_constant_none() + + def extract_tb(self) -> list[traceback.FrameSummary | FrameSummaryVariable]: + if istype(self.tb_next, ConstantVariable): + return [self.frame_summary] + return [self.frame_summary] + self.tb_next.extract_tb() + + def has_reference_cycle(self, tb: VariableTracker) -> bool: + # checks if `tb` is in the chain of tb_next starting from `self` + curr_tb: TracebackVariable | ConstantVariable = self + while istype(curr_tb, TracebackVariable): + if curr_tb is tb: + return True + curr_tb = curr_tb.tb_next + return False + + def python_type(self) -> type[types.TracebackType]: + return types.TracebackType + + def call_setattr( + self, + tx: "InstructionTranslator", + name_var: VariableTracker, + val: VariableTracker, + ) -> VariableTracker: + name = name_var.as_python_constant() + if name == "tb_next": + if not self.is_valid_traceback(val): + raise_observed_exception(TypeError, tx) + assert isinstance(val, (TracebackVariable, ConstantVariable)) + if self.has_reference_cycle(val) or ( + istype(val, TracebackVariable) and val.has_reference_cycle(self) + ): + raise_observed_exception(ValueError, tx) + self.tb_next = val + return variables.ConstantVariable.create(None) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + if name == "tb_next": + return self.tb_next + elif name == "tb_lineno": + return self.frame_summary.var_getattr(tx, "lineno") + elif name == "frame_summary": + return self.frame_summary + elif name == "tb_lasti": + unimplemented( + gb_type="traceback.tb_lasti not supported", + context=f"{self} accessing 'tb_lasti'", + explanation="Dynamo does not support accessing the tb_lasti attribute of traceback objects.", + hints=[*graph_break_hints.SUPPORTABLE], + ) + return super().var_getattr(tx, name) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "__eq__": + # Two traceback variables are only equal if they are the same object + return VariableTracker.build(tx, self is args[0]) + elif name == "__setattr__": + return self.call_setattr(tx, *args) + return super().call_method(tx, name, args, kwargs) + + +class ExceptionVariable(VariableTracker): + # _PyExc_BaseException: https://github.com/python/cpython/blob/v3.13.0/Objects/exceptions.c + _cpython_type = BaseException + + # The ExceptionVariable corresponds to the BaseException class in Python + def __init__( + self, + exc_type: Any, + args: list[VariableTracker], + init_kwargs: dict[str, VariableTracker] | None = None, + source: Source | None = None, + mutation_type: MutationType | None = None, + ) -> None: + super().__init__(source=source, mutation_type=mutation_type) + self.exc_type = exc_type + self.args = args + if init_kwargs: + unimplemented( + gb_type="Keyword args passed to exception constructor", + context=f"{self} with kwargs {init_kwargs}", + explanation="Dynamo does not know how to handle keyword args passed to an exception constructor", + hints=[*graph_break_hints.SUPPORTABLE], + ) + # When raising a new exception while another exception is already being + # handled, the new exception's __context__ attribute is automatically + # set to the handled exception. + self.__context__: VariableTracker = ConstantVariable.create(None) + # Set when user raised an exception from another: + # raise ... from ... + self.__cause__: VariableTracker = ConstantVariable.create(None) + # Boolean flag that controls whether the __context__ attribute is set + self.__suppress_context__: VariableTracker = ConstantVariable.create(False) + # Contains the call stack where the exception was raised. + self.__traceback__: VariableTracker = ConstantVariable.create(None) + # The user stack at the time this exception was first raised. + # Used to preserve the original exception location when re-raising. + self.python_stack: traceback.StackSummary | None = None + + def set_context(self, context: VariableTracker) -> None: + self.__context__ = context + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.load_import_from("builtins", self.exc_type.__name__) + ) + codegen.foreach(self.args) + codegen.call_function(len(self.args), False) + + def codegen_attr(name: str) -> None: + attr = getattr(self, name) + if istype(attr, ConstantVariable): + assert attr.value in (True, False, None), attr + else: + codegen.dup_top() + codegen(attr) + codegen.extend_output(codegen.rot_n(2)) + codegen.store_attr(name) + + codegen_attr("__context__") + codegen_attr("__cause__") + codegen_attr("__suppress_context__") + + def python_type(self) -> type: + return self.exc_type + + def call_setattr( + self, + tx: "InstructionTranslator", + name_var: VariableTracker, + val: VariableTracker, + ) -> VariableTracker: + name = name_var.as_python_constant() + if name == "__context__": + # Constant can be either an Exceptior or None + assert val.is_constant_none() or isinstance( + val, + ( + variables.ExceptionVariable, + variables.UserDefinedExceptionClassVariable, + variables.UserDefinedExceptionObjectVariable, + ), + ), f"{val} is not a valid exception context" + self.set_context(val) + elif name == "__cause__": + if val.is_constant_none() or isinstance( + val, + ( + variables.BuiltinVariable, + variables.ExceptionVariable, + variables.UserDefinedExceptionClassVariable, + variables.UserDefinedExceptionObjectVariable, + ), + ): + self.__cause__ = val + self.__suppress_context__ = variables.ConstantVariable.create(True) + else: + raise_type_error( + tx, "exception cause must be None or derive from BaseException" + ) + elif name == "__suppress_context__": + if val.is_constant_match(True, False): + self.__suppress_context__ = val + else: + raise_type_error( + tx, "exception cause must be None or derive from BaseException" + ) + elif name == "__traceback__": + if not TracebackVariable.is_valid_traceback(val): + raise_type_error(tx, "__traceback__ must be a traceback object or None") + self.__traceback__ = val + else: + unimplemented( + gb_type="Unsupported attribute assignment on Exception object", + context=f"call_setattr {self} {name}", + explanation="Dynamo does not support setting the attribute " + f"'{name}' on tracked exception objects. Only `__context__`, " + "`__cause__`, `__suppress_context__`, and `__traceback__` are supported.", + hints=[*graph_break_hints.SUPPORTABLE], + ) + return variables.ConstantVariable.create(None) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "__setattr__": + return self.call_setattr(tx, *args) + elif name == "with_traceback": + [tb] = args + self.call_setattr(tx, VariableTracker.build(tx, "__traceback__"), tb) + return self + else: + return super().call_method(tx, name, args, kwargs) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + if name == "__class__": + return VariableTracker.build(tx, self.exc_type) + elif name == "__context__": + return self.__context__ + elif name == "__cause__": + return self.__cause__ + elif name == "__suppress_context__": + return self.__suppress_context__ + elif name == "__traceback__": + return self.__traceback__ + elif name == "args": + return VariableTracker.build( + tx, + tuple(self.args), + source=self.source and AttrSource(self.source, "args"), + ) + return super().var_getattr(tx, name) + + def __str__(self) -> str: + return f"{self.__class__.__name__}({self.exc_type})" + + __repr__ = __str__ + + @staticmethod + def _debug_format_arg(arg: VariableTracker) -> str: + try: + return repr(arg.as_python_constant()) + except Exception: + return arg.debug_repr() + + def debug_repr(self) -> str: + args = ", ".join(self._debug_format_arg(arg) for arg in self.args) + return f"{self.python_type_name()}({args})" + + +class UnknownVariable(VariableTracker): + """ + It could be anything! + """ + + +class DelayGraphBreakVariable(UnknownVariable): + """ + Used to insert a dummy variable in the stack to do the graph break at CALL_FUNCTION. + """ + + def __init__(self, msg: str | None = None, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.msg = msg + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + name = "" if self.source is None else self.source.name + unimplemented( + gb_type="Unsupported function call (delayed)", + context=f"source: {self.source}", + explanation="Dynamo determined that a graph break should occur " + f"when calling `{name}`. Reason: {self.msg}", + hints=[], + ) + + +class ComptimeVariable(VariableTracker): + """ + This variable is special, it lets you execute arbitrary code at + Dynamo compile time + """ + + def reconstruct(self, codegen: "PyCodegen") -> None: + raise NotImplementedError("comptime is special form") + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + from ..comptime import comptime + + assert self.source is not None + # To support the comptime.print_graph convenience accessors + return VariableTracker.build( + tx, getattr(comptime, name), source=AttrSource(self.source, name) + ) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from ..comptime import ComptimeContext + + # TODO: support an expression form as well + # Second argument is runtime lambda, ignored + if kwargs or len(args) > 2: + raise_args_mismatch( + tx, + "comptime()", + "at most 2 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + fn = args[0] + if isinstance(fn, UserFunctionVariable): + fn.get_function()(ComptimeContext(tx)) + elif isinstance(fn, NestedUserFunctionVariable): + # We have to manually bind the freevars ourselves + code = fn.get_code() + if fn.closure: + raise_type_error( + tx, + f"comptime function must not have free variables, but these variables were free: {code.co_freevars}", + ) + func = types.FunctionType( + code, + fn.f_globals, + fn.fn_name.as_python_constant(), + # type: ignore[attr-defined] + tuple(fn.defaults.items) if fn.defaults else None, + # We could automatically promote free variables into + # ComptimeVar but this is confusing if you access + # a free variable that we actually DO have the runtime + # value for + # tuple(make_cell(ComptimeVar(i)) for i in fn.closure.items) + (), + ) + func(ComptimeContext(tx)) + else: + raise RuntimeError(f"unsupported argument to comptime: {type(fn)}") + + return variables.ConstantVariable.create(None) + + +class CellVariable(VariableTracker): + # PyCell_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/cellobject.c#L151 + _cpython_type = types.CellType + + # If the cell existed before Dynamo tracing started, this will be the + # VariableTracker that represents the cell content. + # + # Note that all mutation to the cell (i.e., its content) will be buffered in + # SideEffects, rather than being reflected here. One can think of + # `CellVariable` as a special case for `UserDefinedObjectVariable`. + pre_existing_contents: VariableTracker | None + + # This is set when this cell can be referenced via `LOAD/STORE_DEREF` in the + # root frame via this name (e.g., the name is in `co_cellvars/co_freevars`). + local_name: str | None = None + + def __init__( + self, pre_existing_contents: VariableTracker | None = None, **kwargs: Any + ) -> None: + super().__init__(**kwargs) + self.pre_existing_contents = pre_existing_contents + + def python_type(self) -> type: + return types.CellType + + +class NewGlobalVariable(VariableTracker): + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + +def produce_trampoline_autograd_apply(fn_cls: Any) -> Callable[..., Any]: + def trampoline_autograd_apply(*args: Any, **kwargs: Any) -> Any: + return fn_cls.apply(*args, **kwargs) + + # type: ignore[attr-defined] + trampoline_autograd_apply._origin = produce_trampoline_autograd_apply + return trampoline_autograd_apply + + +class AutogradFunctionVariable(VariableTracker): + """represents a torch.autograd.Function subclass""" + + _nonvar_fields = { + "fn_cls", + *VariableTracker._nonvar_fields, + } + + def __init__(self, fn_cls: Any, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.fn_cls = fn_cls + + def python_type(self) -> type: + return type + + def call_apply( + self, + tx: "InstructionTranslator", + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + requires_grad = False + + def visit(vt: VariableTracker) -> None: + nonlocal requires_grad + if vt.is_tensor(): + # type: ignore[attr-defined] + if vt.requires_grad is not False: + requires_grad = True + if isinstance(vt, variables.NNModuleVariable): + if vt.is_training(tx): + requires_grad = True + + VariableTracker.visit(visit, (args, kwargs)) + + if requires_grad and torch.is_grad_enabled(): + source = self.source + + from torch._functorch.autograd_function import ( + autograd_function_forward_rewritten, + ) + from torch.autograd.function import _is_setup_context_defined + + forward_fn = self.fn_cls.forward + + is_setup_ctx_defined = _is_setup_context_defined(self.fn_cls.setup_context) + if is_setup_ctx_defined: + # If setup_context is defined, we generate a new forward function which includes + # the original forward and setup_context function, and trace the new forward function. + forward_fn = autograd_function_forward_rewritten( + self.fn_cls.forward, self.fn_cls.setup_context + ) + # The forward points to a new function now, so we can't use the + # old source. Later on, we guard specifically on + # is_setup_ctx_defined + source = None + + vjp_fn = self.fn_cls.vjp # type: ignore[attr-defined] + if vjp_fn is not torch.autograd.Function.vjp: + unimplemented( + gb_type="Unsupported custom vjp", + context=f"call_apply {self} {args} {kwargs}", + explanation="Dynamo does not support tracing " + "`torch.autograd.Function` subclasses that define " + "a custom `vjp` method.", + hints=[ + "Remove the custom `vjp` method if possible.", + "Use standard `backward` instead if applicable.", + *graph_break_hints.SUPPORTABLE, + ], + ) + + jvp_fn = self.fn_cls.jvp # type: ignore[attr-defined] + if jvp_fn is not torch.autograd.Function.jvp: + unimplemented( + gb_type="Unsupported custom jvp", + context=f"call_apply {self} {args} {kwargs}", + explanation="Dynamo does not support tracing " + "`torch.autograd.Function` subclasses that define " + "a custom `jvp` method.", + hints=[ + "Remove the custom `jvp` method if possible.", + *graph_break_hints.SUPPORTABLE, + ], + ) + + from .higher_order_ops import AutogradFunctionApplyVariable + + if source is None and not is_setup_ctx_defined: + source = AttrSource( + tx.import_source(self.fn_cls.__module__), self.fn_cls.__name__ + ) + apply_source = source and AttrSource(source, member="apply") + val = AutogradFunctionApplyVariable( + forward_fn, + self.fn_cls.backward, + source, + source=apply_source, + ).call_function(tx, args, kwargs) + if self.source and is_setup_ctx_defined: + fwd_src = AttrSource(self.source, "forward") + install_guard(fwd_src.make_guard(GuardBuilder.CLOSURE_MATCH)) + setup_ctx_src = AttrSource(self.source, "setup_context") + install_guard(setup_ctx_src.make_guard(GuardBuilder.CLOSURE_MATCH)) + + return val + + if self.source: + source = AttrSource(self.source, "forward") + else: + source = None + + fn = self.fn_cls.forward + ctx = AutogradFunctionContextVariable.create(tx, args, kwargs) + args = [ctx, *args] + if isinstance(fn, types.FunctionType): + sig = inspect.signature(fn) + if len(args) - 1 == len(sig.parameters): + args = args[1:] # Don't use context + fn_vt = VariableTracker.build(tx, fn, source=source, realize=True) + return fn_vt.call_function(tx, args, kwargs) + elif isinstance(fn, types.MethodType): + return variables.UserMethodVariable( + fn.__func__, + variables.UserDefinedClassVariable(self.fn_cls), + source=source, + ).call_function(tx, args, kwargs) + else: + unimplemented( + gb_type="Non-function or method in subclass of torch.autograd.Function", + context=f"call_apply {self} {args} {kwargs}", + explanation="Dynamo requires the `forward` attribute of a " + "`torch.autograd.Function` subclass to be a standard Python " + f"function or method. Found type `{type(fn).__name__}` instead.", + hints=[ + "Ensure the `forward` method is defined as a regular " + "function or instance method." + ], + ) + + def call_backward( + self, + tx: "InstructionTranslator", + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + fn = self.fn_cls.backward + # type: ignore[attr-defined] + assert type(args[0].value) is torch._dynamo.external_utils.FakeBackwardCFunction + assert isinstance(fn, types.FunctionType) + assert self.source is not None + fn_source = AttrSource(self.source, "backward") + fn_vt = VariableTracker.build(tx, fn, source=fn_source, realize=True) + return fn_vt.call_function(tx, args, kwargs) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> "AutogradFunctionVariable": + return AutogradFunctionVariable(self.fn_cls) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .builder import wrap_fx_proxy + + if name == "apply": + if trace_rules.is_callable_allowed(self.fn_cls): + trampoline_autograd_apply = produce_trampoline_autograd_apply( + self.fn_cls + ) + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + trampoline_autograd_apply, + *proxy_args_kwargs(args, kwargs), + ), + ) + else: + return self.call_apply(tx, args, kwargs) + + elif name == "backward": + return self.call_backward(tx, args, kwargs) + else: + source = AttrSource(self.source, name) if self.source is not None else None + try: + obj = inspect.getattr_static(self.fn_cls, name) + except AttributeError: + obj = None + + if isinstance(obj, staticmethod): + func = obj.__get__(self.fn_cls) + traced = trace_rules.lookup(func) + assert traced is not None + if source is not None: + return ( + # type: ignore[attr-defined] + traced.create_with_source(func, source=source).call_function( + tx, args, kwargs + ) + ) + else: + # type: ignore[misc] + return traced(func).call_function(tx, args, kwargs) + elif isinstance(obj, classmethod): + return variables.UserMethodVariable( + obj.__func__, self, source=source + ).call_function(tx, args, kwargs) + else: + unimplemented( + gb_type="Unsupported autograd.Function method", + context=f"call_method {self} {name}", + explanation="Dynamo does not support calling the method " + f"`{name}` directly on the `torch.autograd.Function` " + "instance. Supported methods include `apply`, `backward`, " + "static methods, and class methods.", + hints=[ + "Ensure the method is decorated with `@staticmethod` " + "or `@classmethod` if it's meant to be called on the class.", + ], + ) + + +@dataclasses.dataclass +class SavedTensorBox: + tensors: list[VariableTracker] = dataclasses.field(default_factory=list) + + +class AutogradFunctionContextVariable(UserDefinedObjectVariable): + """ + Tracks an autograd.Function() context using mutation tracking in side_effects.py + """ + + _nonvar_fields = { + "proxy", + "inference", + "saved_tensors", + *UserDefinedObjectVariable._nonvar_fields, + } + + def __init__( + self, + value: Any, + value_type: type | None = None, + inference: bool = False, + saved_tensors: Any | None = None, + needs_input_grad: tuple[bool, ...] | None = None, + non_differentiable: Any | None = None, + **kwargs: Any, + ) -> None: + super().__init__(value=value, value_type=value_type, **kwargs) + self.inference = inference + self.saved_tensors = saved_tensors + self.needs_input_grad = needs_input_grad + self.non_differentiable = non_differentiable + + @staticmethod + def create( + tx: "InstructionTranslator", + args: Sequence[VariableTracker] | None = None, + kwargs: dict[str, VariableTracker] | None = None, + ) -> VariableTracker: + needs_input_grad = None + if args and not kwargs: + # type: ignore[attr-defined] + needs_input_grad = tuple(x.is_tensor() and x.requires_grad for x in args) + out = tx.output.side_effects.track_object_new( + None, + torch.autograd.function.FunctionCtx, + functools.partial( + AutogradFunctionContextVariable, + inference=True, + saved_tensors=SavedTensorBox(), + needs_input_grad=needs_input_grad, + ), + {}, + ) + return out + + def as_proxy(self) -> Any: + # type: ignore[attr-defined] + if self.proxy is None: + unimplemented( + gb_type="proxy not set", + context=f"as_proxy {self}", + explanation="Dynamo requires the autograd.Function context " + "to be initialized with a proxy.", + hints=[*graph_break_hints.DYNAMO_BUG], + ) + # type: ignore[attr-defined] + return self.proxy + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "__setattr__": + return super().call_method(tx, name, args, kwargs) + elif name == "mark_non_differentiable": + if kwargs: + raise_args_mismatch(tx, name, "0 kwargs", f"{len(kwargs)} kwargs") + self.non_differentiable = proxy_args_kwargs(args, {})[0] + return variables.ConstantVariable.create(None) + + if name != "save_for_backward": + unimplemented( + gb_type="Unsupported autograd.Function context method", + context=f"call_method {self} {name}", + explanation="Dynamo does not support calling the method " + f"`{name}` on `autograd.Function` context objects. Supported " + "methods are `__setattr__`, `save_for_backward` and " + "`mark_non_differentiable`.", + hints=[*graph_break_hints.SUPPORTABLE], + ) + if self.saved_tensors is None: + unimplemented( + gb_type="Unsupported autograd.Function context `save_for_backward`", + context=f"call_method {self} {name}", + explanation="Dynamo requires the `saved_tensors` attribute " + "to be initialized on the `autograd.Function` context object.", + hints=[ + "Ensure that the `saved_tensors` attribute is properly " + "initialized before calling `save_for_backward`. " + "`save_for_backward` only supported on a newly constructed `torch.autograd.function.FunctionCtx`.", + ], + ) + assert self.saved_tensors is not None + if not self.inference: + if kwargs or not self.source: + raise_type_error( + tx, "save_for_backward() requires a source and no keyword arguments" + ) + tx.output.side_effects.track_save_for_backward(self, args) + + # In eager mode, multiple calls to .save_for_backward() will overwrite previous calls. + if len(self.saved_tensors.tensors) > 0: + # pyrefly: ignore [implicit-any] + self.saved_tensors.tensors = [] + for arg in args: + self.saved_tensors.tensors.append(arg) + return variables.ConstantVariable.create(None) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + if name in ["save_for_backward", "mark_non_differentiable"]: + return LambdaVariable( + lambda *args, **kwargs: self.call_method(tx, name, list(args), kwargs) + ) + if name == "saved_tensors" and self.saved_tensors is not None: + return variables.TupleVariable(list(self.saved_tensors.tensors)) + if name == "needs_input_grad": + if self.needs_input_grad is not None: + return variables.ConstantVariable.create(self.needs_input_grad) + if self.source: + source = AttrSource(self.source, "needs_input_grad") + # type: ignore[attr-defined] + return VariableTracker.build(tx, self.value.needs_input_grad, source) + + return super().var_getattr(tx, name) + + +class AutogradEngineVariable(UserDefinedObjectVariable): + """ + Represents a torch._C._ImperativeEngine instance. + """ + + def __init__( + self, + value: torch._C._ImperativeEngine, + value_type: type[torch._C._ImperativeEngine] | None = None, + **kwargs: Any, + ) -> None: + super().__init__(value=value, value_type=value_type, **kwargs) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "queue_callback": + if torch._dynamo.compiled_autograd.in_compiled_autograd_region: + assert tx.one_graph or tx.error_on_graph_break, ( + "queue_callback() is only supported when Compiled Autograd is enabled with fullgraph=True" + ) + # queue_callback is a method-wrapper, no need to insert a guard. + fn_vt = VariableTracker.build( + tx, + torch._dynamo.external_utils.FakeCompiledAutogradEngine.queue_callback, + ) + return fn_vt.call_function( + tx, + (tx.output.side_effects.get_ca_final_callbacks_var(), *args), + kwargs, + ) + else: + unimplemented( + gb_type="Unsupported torch._C._ImperativeEngine.queue_callback()", + context=f"call_method {self} {name}", + explanation="queue_callback() is only supported when " + "Compiled Autograd is enabled with fullgraph=True.", + hints=[], + ) + else: + unimplemented( + gb_type="Unsupported torch._C._ImperativeEngine method", + context=f"call_method {self} {name}", + explanation="Dynamo only supports the `queue_callback` method " + f"on a torch._C._ImperativeEngine instance, but found: `{name}`.", + hints=[], + ) + + +class LambdaVariable(VariableTracker): + # TODO: change to Ts = TypeVarTuple("Ts") for py 3.11+ + def __init__(self, fn: Callable[..., VariableTracker], **kwargs: Any) -> None: + super().__init__(**kwargs) + self.fn = fn + + def python_type(self) -> type: + return types.FunctionType + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + return self.fn(*args, **kwargs) + + +class GetAttrVariable(VariableTracker): + _nonvar_fields = { + "name", + "py_type", + *VariableTracker._nonvar_fields, + } + + def __init__( + self, + obj: VariableTracker, + name: str, + py_type: type | None = None, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + assert isinstance(obj, VariableTracker) + assert isinstance(name, str) + self.obj = obj + self.name = name + self.py_type = py_type # In some cases we know the type (ex. tensor methods) + + def python_type(self) -> type: + if self.py_type is not None: + return self.py_type + else: + return super().python_type() + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.obj}, {self.name})" + + @staticmethod + def create_getattr_proxy(base_proxy: torch.fx.Proxy, attr: str) -> Any: + return getattr(base_proxy, attr) + + def as_proxy(self) -> Any: + return GetAttrVariable.create_getattr_proxy(self.obj.as_proxy(), self.name) + + def as_python_constant(self) -> Any: + constant = self.obj.as_python_constant() + try: + return getattr(constant, self.name) + except AttributeError: + raise NotImplementedError(f"{self} is not a constant") from None + + def const_getattr(self, tx: "InstructionTranslator", name: str) -> Any: + if not isinstance(self.obj, variables.NNModuleVariable): + raise NotImplementedError + step1 = tx.output.get_submodule(self.obj.module_key) + if self.name not in step1.__dict__: + raise NotImplementedError + step2 = inspect.getattr_static(step1, self.name) + if name not in step2.__dict__: + raise NotImplementedError + return inspect.getattr_static(step2, name) + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen(self.obj) + codegen.extend_output(codegen.create_load_attrs(self.name)) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + return self.obj.call_method(tx, self.name, list(args), kwargs) + + +class MethodWrapperVariable(VariableTracker): + def __init__(self, method_wrapper: types.MethodWrapperType, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.method_wrapper = method_wrapper + + def get_real_python_backed_value(self) -> types.MethodWrapperType: + return self.method_wrapper + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if is_tensor_base_attr_getter(self.method_wrapper) and isinstance( + args[0], variables.TensorVariable + ): + if not (len(args) == 1 and len(kwargs) == 0): + raise_type_error( + tx, "tensor attribute getter takes exactly one argument" + ) + # type: ignore[arg-type, attr-defined] + return args[0].var_getattr(tx, self.method_wrapper.__self__.__name__) + + # method-wrapper variables are common in __init__ calls. For example, + # str("foo").__init__ is a method-wrapper. These method wrappers point + # to C functions. Here we intercept if these method-wrappers are from + # builtins and then call the function counterpart directly by obtaining + # the self object. + self_obj = self.method_wrapper.__self__ + wrapper_name = self.method_wrapper.__name__ + # TODO(dynamo-team) - We can perhaps expand the scope to more names and + # more builtins. + if wrapper_name == "__init__": + fn_obj = type(self_obj).__init__ + if fn_obj is object.__init__: + return VariableTracker.build(tx, object).call_method( + tx, + wrapper_name, + # type: ignore[arg-type, list-item] + [self_obj, *args], + kwargs, + ) + elif ( + sys.version_info >= (3, 14) + # for some reason, even if the below check passes, + # self.method_wrapper may not be the same as type.__dict__["__annotations__"].__get__ + and self_obj is type.__dict__["__annotations__"] + and wrapper_name == "__get__" + ): + from .builder import SourcelessBuilder + + if len(args) == 1 and not kwargs: + try: + return SourcelessBuilder.create( + tx, self.method_wrapper(args[0].as_python_constant()) + ) + except AttributeError: + raise_observed_exception(AttributeError, tx) + except AsPythonConstantNotImplementedError: + pass + + unimplemented( + gb_type="unsupported type.__dict__['__annotations__'].__get__ call", + context=f"call_function {self}, args: {args}, kwargs: {kwargs}", + explanation="`torch.compile` only supports calling type.__dict__['__annotations__'].__get__ " + "on a single constant argument (i.e. a type).", + hints=[ + "Make sure your call to type.__dict__['__annotations__'] only has " + "one positional argument (no keyword arguments).", + "Make sure the argument to type.__dict__['__annotations__'] is a constant " + "(i.e. type). For example, `object`, `int`, `MyCustomClass`.", + *graph_break_hints.SUPPORTABLE, + ], + ) + elif (self_obj is type.__dict__["__mro__"] and wrapper_name == "__get__") or ( + self_obj is type.__dict__["__dict__"] and wrapper_name == "__get__" + ): + attr_name = ( + "__mro__" if self_obj is type.__dict__["__mro__"] else "__dict__" + ) + + if len(args) == 1 and not kwargs: + try: + value = self.method_wrapper(args[0].as_python_constant()) + except AsPythonConstantNotImplementedError: + pass + else: + # Use a sourced variable when the descriptor is the + # standard one from type (not overridden by a metaclass). + source = args[0].source + if source is not None: + cls_val = args[0].as_python_constant() + static_desc = inspect.getattr_static(type(cls_val), attr_name) + if static_desc is self_obj: + if attr_name == "__mro__": + source = TypeMROSource(source) + else: + source = AttrSource(source, attr_name) + return VariableTracker.build(tx, value, source) + + from .builder import SourcelessBuilder + + return SourcelessBuilder.create(tx, value) + + unimplemented( + gb_type=f"unsupported type.__dict__['{attr_name}'].__get__ call", + context=f"call_function {self}, args: {args}, kwargs: {kwargs}", + explanation=f"`torch.compile` only supports calling type.__dict__['{attr_name}'].__get__ " + "on a single constant argument (i.e. a type).", + hints=[ + f"Make sure your call to type.__dict__['{attr_name}'].__get__ only has " + "one positional argument (no keyword arguments).", + f"Make sure the argument to type.__dict__['{attr_name}'].__get__ is a constant " + "(i.e. type). For example, `object`, `int`, `MyCustomClass`.", + *graph_break_hints.SUPPORTABLE, + ], + ) + + return super().call_function(tx, args, kwargs) + + def is_python_constant(self) -> Literal[True]: + return True + + def as_python_constant(self) -> types.MethodWrapperType: + return self.method_wrapper + + def is_python_hashable(self) -> Literal[True]: + return True + + def get_python_hash(self) -> int: + return hash(self.as_python_constant()) + + def is_python_equal(self, other: object) -> bool: + return ( + isinstance(other, VariableTracker) + and self.as_python_constant() == other.as_python_constant() + ) + + +class GetSetDescriptorVariable(VariableTracker): + def __init__(self, desc: types.GetSetDescriptorType, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.desc = desc + + def get_real_python_backed_value(self) -> types.GetSetDescriptorType: + return self.desc + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + if name == "__get__" and self.source: + source = AttrSource(self.source, "__get__") + return VariableTracker.build(tx, self.desc.__get__, source) + elif name in ("__objclass__", "__name__"): + source = self.source and AttrSource(self.source, name) + return VariableTracker.build(tx, getattr(self.desc, name), source) + else: + return super().var_getattr(tx, name) + + def is_python_constant(self) -> Literal[True]: + return True + + def as_python_constant(self) -> types.GetSetDescriptorType: + return self.desc + + +class PythonModuleVariable(VariableTracker): + # PyModule_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/moduleobject.c#L1203 + _cpython_type = types.ModuleType + + _nonvar_fields = { + "value", + "is_torch", + *VariableTracker._nonvar_fields, + } + + def __init__(self, value: types.ModuleType, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.value = value + self.is_torch = self.value is torch or self.value.__name__.startswith("torch.") + + def python_type(self) -> type[types.ModuleType]: + return types.ModuleType + + def as_python_constant(self) -> types.ModuleType: + return self.value + + def get_real_python_backed_value(self) -> types.ModuleType: + return self.value + + def __repr__(self) -> str: + return f"PythonModuleVariable({self.value})" + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> "ConstantVariable": + result = hasattr(self.value, name) + return VariableTracker.build(tx, result) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + if tx.output.side_effects.has_pending_mutation_of_attr(self, name): + return tx.output.side_effects.load_attr(self, name) + + attr_value = None + if self.is_torch or name not in self.value.__dict__: + try: + attr_value = getattr(self.value, name) + except AttributeError: + raise_observed_exception(AttributeError, tx) + else: + attr_value = self.value.__dict__[name] + + source = self.source and AttrSource(self.source, name) + return VariableTracker.build(tx, attr_value, source) + + +class TypingVariable(VariableTracker): + def __init__(self, value: Any, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.value = value + + def mp_subscript_impl( + self, + tx: "InstructionTranslator", + key: VariableTracker, + ) -> VariableTracker: + # e.g., List[int] → typing.List[int] + # TODO(follow-up): add test for invalid subscript type + new_typing = self.value[key.as_python_constant()] + return TypingVariable(new_typing) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "__eq__": + if len(args) == 1 and not kwargs: + result = istype(args[0], TypingVariable) and self.value == args[0].value + return variables.ConstantVariable.create(result) + unimplemented( + gb_type="unsupported method call on `typing` variable", + context=f"typing variable: {self.value}, method name: {name}, args: {args}, kwargs: {kwargs}", + explanation=f"`torch.compile` does not support method call `{name}` on `typing` variable f{self.value}.", + hints=[ + f"Avoid calling the {name} method on {self.value}.", + *graph_break_hints.SUPPORTABLE, + ], + ) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + from .builder import SourcelessBuilder, VariableBuilder + + if name in cmp_name_to_op_mapping: + return variables.GetAttrVariable( + self, name, py_type=type(getattr(self.value, name)) + ) + + if tx.output.side_effects.has_pending_mutation_of_attr(self, name): + return tx.output.side_effects.load_attr(self, name) + + value = getattr(self.value, name) + if self.source: + attr_source = AttrSource(self.source, name) + return VariableBuilder(tx, attr_source)(value) + else: + return SourcelessBuilder.create(tx, value) + + def as_python_constant(self) -> Any: + return self.value + + def get_real_python_backed_value(self) -> Any: + return self.value + + def reconstruct(self, codegen: "PyCodegen") -> None: + if not isinstance(self.value, types.GenericAlias): + return super().reconstruct(codegen) + # We're just trying to load the type here. Reconstructing the type from + # scratch is tricky - for a type like `typing.List[int]` we'd need to + # deconstruct the origin and args. The origin for `List[int]` is `list` + # and the args is `(int,)`. When we recombine those we get the parts + # back and need to emit code for: + # + # `typing.List[int]` + # + # But it's # worse than that - what if `typing` isn't in the globals (or + # was loaded like `import typing as _typing ; _typing.List[int]`?) so we + # really need to do something like: + # + # `sys.modules["typing"].List[int]` + # + # Argh - but what if they rewrote the global `int`? So we have to do: + # + # `sys.modules["typing"].List[sys.modules["builtins"].int]` + # + # But where do we get `sys`? What if they never imported it or have + # something ELSE called `sys`? + # + # Let's skip all that noise and just emit it as a simple const. + # + codegen.append_output(codegen.create_load_const(self.value)) + + def is_python_hashable(self) -> Literal[True]: + return True + + def get_python_hash(self) -> int: + return hash(self.as_python_constant()) + + def is_python_equal(self, other: object) -> bool: + return ( + isinstance(other, VariableTracker) + and self.as_python_constant() == other.as_python_constant() + ) + + +@functools.lru_cache(maxsize=1) +def get_np_to_tnp_map() -> dict[types.BuiltinFunctionType, types.FunctionType]: + """ + This generates a mapping from numpy modules to their torch._numpy + modules equivalents. + """ + from ..utils import NP_TO_TNP_MODULE + + np_fn_to_tnp_fn = {} + + for np_mod, tnp_mod in NP_TO_TNP_MODULE.items(): + for fn_name, tnp_fn in tnp_mod.__dict__.items(): + if callable(tnp_fn): + # some internal details do leak from tnp + # which are not part of numpy API. + if np_fn := getattr(np_mod, fn_name, None): + np_fn_to_tnp_fn[np_fn] = tnp_fn + + return np_fn_to_tnp_fn + + +@functools.lru_cache(maxsize=1) +def get_tnp_to_np_map() -> dict[types.FunctionType, types.BuiltinFunctionType]: + """ + This is just the reverse mapping of get_np_to_tnp_map() - mapping from + torch._numpy modules to numpy equivalents. + """ + m = get_np_to_tnp_map() + return {v: k for k, v in m.items()} + + +class NumpyVariable(VariableTracker): + """ + Wrapper around `numpy.*`. Currently, is able to trace a small subset of numpy functions as well as numpy dtypes. + """ + + constant_fold_functions = (tnp.issubdtype,) + + def __init__(self, value: Any, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.value = value + + def get_real_python_backed_value(self) -> Any: + return self.value + + @classmethod + def can_constant_fold_through(cls, fn: types.FunctionType) -> bool: + mod = fn.__module__.split(".") + assert len(mod) >= 2 and mod[:2] == ["torch", "_numpy"] + return fn in cls.constant_fold_functions + + @classmethod + def get_constant_collection_for_func(cls, fn: types.FunctionType) -> Any: + mod = fn.__module__.split(".") + assert len(mod) >= 2 and mod[:2] == ["torch", "_numpy"] + return np_constant_collections_map.get(fn) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if not config.trace_numpy: + unimplemented( + gb_type="attempted to trace numpy function with config.trace_numpy=False", + context=f"numpy function: {self.value}, args: {args}, kwargs: {kwargs}", + explanation=f"Attempted to trace numpy function {self.value} " + "while `torch._dynamo.config.trace_numpy` was set to False.", + hints=[ + "Set `torch._dynamo.config.trace_numpy` to True to trace numpy functions.", + ], + ) + + from ..utils import numpy_to_tensor_wrapper + from .tensor import NumpyNdarrayVariable + + func = get_np_to_tnp_map().get(self.value) + if func is None: + unimplemented( + gb_type="attempted to trace numpy function unsupported by PyTorch", + context=f"numpy function: {self.value}, args: {args}, kwargs: {kwargs} (corresponding torch function: {func})", + explanation=f"Can't find numpy numpy function {self.value} in torch._numpy.", + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + ) + + # We are dealing with a function that produces a const collection type (np.dtype, np.iinfo/np.finfo) + assert func is not None + if ( + collection_variable_typ := self.get_constant_collection_for_func(func) + ) is not None: + try: + return collection_variable_typ( + self.value( + *[x.as_python_constant() for x in args], + **{k: v.as_python_constant() for k, v in kwargs.items()}, + ) + ) + except AsPythonConstantNotImplementedError: + unimplemented( + gb_type="numpy function that produces a const collection type encountered non-const arguments", + context=f"numpy function: {self.value}, args: {args}, kwargs: {kwargs} (corresponding torch function: {func})", + explanation=f"numpy function {self.value} that produces a const collection type " + "(e.g. np.dtype, np.iinfo/np.finfo) " + "received arguments that are not constant.", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + else: + if ( + func.__module__ == "torch._numpy.random" + and config.use_numpy_random_stream + ): + unimplemented( + gb_type="attempted to trace torch._numpy.random function with config.use_numpy_random_stream=True", + context=f"numpy function: {self.value}, args: {args}, kwargs: {kwargs} (corresponding torch function: {func})", + explanation=f"Attempted to trace {self.value} when `torch._dynamo.config.use_numpy_random_stream` " + "is set to True.", + hints=[ + "Set `torch._dynamo.config.use_numpy_random_stream` to False.", + f"Avoid calling {self.value}.", + ], + ) + + args, kwargs = NumpyNdarrayVariable.patch_args(func.__name__, args, kwargs) + + if self.can_constant_fold_through(func) and ( + check_unspec_or_constant_args(args, kwargs) + ): + # constant fold + return VariableTracker.build( + tx, + self.as_python_constant()( + *[x.as_python_constant() for x in args], + **{k: v.as_python_constant() for k, v in kwargs.items()}, + ), + ) + + # TODO Add all the functions that go from constants to constants to can_constant_fold_through + proxy = tx.output.create_proxy( + "call_function", + numpy_to_tensor_wrapper(func), + *proxy_args_kwargs(args, kwargs), + ) + return NumpyNdarrayVariable.create(tx, proxy) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + unimplemented( + gb_type="attempted to trace numpy.* function as a method", + context=f"numpy function: {self.value}, args: {args}, kwargs: {kwargs}", + explanation="Tracing numpy.* functions as methods is not supported.", + hints=[ + *graph_break_hints.DIFFICULT, + ], + ) + + def as_python_constant(self) -> BuiltinFunctionType: + return self.value + + def as_proxy(self) -> Any: + if config.trace_numpy: + # Can replace with EnumType once we drop 3.10 support + if isinstance(self.value, enum.EnumMeta): + # This is mostly for np._CopyMode + return self.value + if isinstance(self.value, type): + # This handles numpy dtype attributes such as np.float32 + # We return a string as we don't want to serialize non-PyTorch objects in the output FX graph + # In torch/_numpy we normalize strings to their dtypes when the input is a dtype, as NumPy does + return self.value.__name__ + + return super().as_proxy() + + def is_python_hashable(self) -> Literal[True]: + return True + + def get_python_hash(self) -> int: + return hash(self.as_python_constant()) + + def is_python_equal(self, other: object) -> bool: + return ( + isinstance(other, VariableTracker) + and self.as_python_constant() == other.as_python_constant() + ) + + +# Used to keep track of NULLs pushed on the stack for Python 3.11 function calls +class NullVariable(VariableTracker): + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + def __repr__(self) -> str: + return "NullVariable" + + def reconstruct(self, codegen: "PyCodegen") -> None: + if sys.version_info < (3, 11): + unimplemented( + gb_type="cannot reconstruct NullVariable in Python < 3.11", + context="", + explanation="Attempted to generate PUSH_NULL instruction in Python < 3.11; " + "where this instruction does not exist.", + hints=[ + *graph_break_hints.DYNAMO_BUG, + ], + ) + codegen.append_output(create_instruction("PUSH_NULL")) + + +class DeletedVariable(VariableTracker): + """Marker used to implement delattr()""" + + +class StringFormatVariable(VariableTracker): + """ + Represents a call to str.format(), we delay calling format until after the graph. + """ + + _nonvar_fields = {"format_string", *VariableTracker._nonvar_fields} + + def python_type(self) -> type: + return str + + @classmethod + def create( + cls, + format_string: str, + sym_args: Sequence[VariableTracker], + sym_kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if all( + x.is_python_constant() + for x in itertools.chain(sym_args, sym_kwargs.values()) + ): + return variables.ConstantVariable.create( + format_string.format( + *[v.as_python_constant() for v in sym_args], + **{k: v.as_python_constant() for k, v in sym_kwargs.items()}, + ) + ) + return cls(format_string, list(sym_args), dict(sym_kwargs)) + + def __init__( + self, + format_string: str, + sym_args: Sequence[VariableTracker], + sym_kwargs: dict[str, VariableTracker], + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + assert isinstance(format_string, str) + self.format_string = format_string + self.sym_args = sym_args + self.sym_kwargs = sym_kwargs + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.format_string!r}, {self.sym_args!r}, {self.sym_kwargs!r})" + + @staticmethod + def _debug_format_arg(arg: VariableTracker) -> object: + try: + return arg.as_python_constant() + except Exception: + return arg.debug_repr() + + def debug_repr(self) -> str: + try: + rendered = self.format_string.format( + *[self._debug_format_arg(arg) for arg in self.sym_args], + **{ + key: self._debug_format_arg(value) + for key, value in self.sym_kwargs.items() + }, + ) + except Exception: + return repr(self) + return repr(rendered) + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.extend_output( + [ + codegen.create_load_const(self.format_string), + codegen.create_load_attr("format"), + ] + ), + call_function_ex=True, + ) + codegen(variables.TupleVariable(list(self.sym_args))) + kwargs = { + variables.ConstantVariable.create(k): v for k, v in self.sym_kwargs.items() + } + codegen(variables.ConstDictVariable(kwargs)) + codegen.extend_output(create_call_function_ex(True, False)) + + +class ObjectVariable(VariableTracker): + # PyBaseObject_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/typeobject.c#L7243 + _cpython_type = object + + # placeholder for unknown / opaque values + def __init__(self, value: object, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.value = value + + def get_real_python_backed_value(self) -> object: + return self.value + + def python_type(self) -> type[object]: + return object + + +class DebuggingVariable(VariableTracker): + """ + Represents a call to a debugging function like print(), or something + registered to config.reorderable_logging_functions. + """ + + def __init__(self, value: Any, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.value = value + + def python_type(self) -> type: + return type(self.value) + + @staticmethod + def is_reorderable_logging_function( + obj: Any, + ) -> TypeGuard[types.FunctionType | types.BuiltinFunctionType]: + return ( + callable(obj) + and isinstance(obj, (types.FunctionType, types.BuiltinFunctionType)) + and obj in torch._dynamo.config.reorderable_logging_functions + ) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if tx.export: + # For export cases, we can just make debugging functions no-ops + return ConstantVariable.create(None) + + if not self.can_reorder_logs(self.value, args, kwargs): + unimplemented( + gb_type="attempted to reorder a debugging function that can't actually be reordered", + context=f"fn: {self.value}, args: {args}, kwargs: {kwargs}", + explanation="`torch.compile` can only reorder functions where the arguments " + "are Tensors, constants, or string formatters.", + hints=[ + f"Avoid calling the logging function {self.value} with args that are not supported.", + ], + ) + + tx.debug_locals.append((self, list(args))) + return ConstantVariable.create(None) + + def reconstruct(self, codegen: "PyCodegen") -> None: + assert self.source is not None + return self.source.reconstruct(codegen) + + @staticmethod + def can_reorder_logs(fn: Any, args: Sequence[Any], kwargs: dict[str, Any]) -> bool: + """ + Run some additional checks for what sort of function calls can we + actually reorder. + """ + + allowed_input_types = ( + variables.TensorVariable, + variables.ConstantVariable, + StringFormatVariable, + ) + + flat_args = pytree.tree_leaves([args, kwargs]) + for arg in flat_args: + if not isinstance(arg, allowed_input_types): + return False + + return True + + +class IgnoredFunctionVariable(VariableTracker): + """ + Represents a call to an arbitrary function that should be ignored. + """ + + def __init__(self, value: Any, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.value = value + + def python_type(self) -> type: + return type(self.value) + + def get_real_python_backed_value(self) -> Any: + return self.value + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + return variables.ConstantVariable.create(None) + + +class LoggingLoggerVariable(VariableTracker): + """ + Represents a call to any logging.Logger methods. + """ + + def __init__(self, value: logging.Logger, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.value = value + + def python_type(self) -> type: + return type(self.value) + + def get_real_python_backed_value(self) -> logging.Logger: + return self.value + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if tx.export: + # For export cases, we can just make logging functions no-ops. + return variables.ConstantVariable.create(None) + + method = getattr(self.value, name, None) + function = getattr(method, "__func__", None) + + # Unified ignore set + ignore_set = torch._dynamo.config.ignore_logging_functions + + if method in ignore_set or function in ignore_set: + return variables.ConstantVariable.create(None) + + unimplemented( + gb_type="logging.Logger method not supported for non-export cases", + context=f"method: {self.value}.{name}, args: {args}, kwargs: {kwargs}", + explanation="logging.Logger methods are not supported for non-export cases.", + hints=[ + "Add the logging method to `torch._dynamo.config.ignore_logging_functions`.", + ], + ) + + +class ConstantLikeVariable(VariableTracker): + """self.value is a compile-time constant, but not a literal""" + + try: + from numpy import ( + dtype as np_dtype, + floating as np_floating, + generic as np_generic, + ) + except ImportError: + # type: ignore[misc, assignment] + np_floating = type("invalid_type", (), {}) + # type: ignore[misc, assignment] + np_dtype = type("invalid_type", (), {}) + + def __init__(self, value: Any, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.value = value + + @property + def _error_prefix(self) -> str: + """Dynamically compute the prefix from the value's type""" + t = type(self.value) + + # For builtins (int, str, etc.), just return the name + if t.__module__ == "builtins": + return t.__qualname__ + + return f"{t.__module__}.{t.__qualname__}" + + def as_python_constant(self) -> Any: + return self.value + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + # pyrefly: ignore [implicit-any] + cargs, ckwargs = [], {} + try: + # we only support constant propagation for methods + cargs = [x.as_python_constant() for x in args] + ckwargs = {k: v.as_python_constant() for k, v in kwargs.items()} + except NotImplementedError: + unimplemented( + gb_type="constant-like method call with non-constant args", + context=f"{self._error_prefix}.{name}(*{args}, **{kwargs})", + explanation=f"Attempted to call {self._error_prefix}.{name} with non-constant args.", + hints=[ + "Ensure that the args to the method call are constant (int, str, etc.).", + ], + ) + + result = getattr(self.value, name)(*cargs, **ckwargs) + + if variables.ConstantVariable.is_literal(result): + return VariableTracker.build(tx, result) + if isinstance(result, re.Match): + return ConstantLikeVariable(result) + + unimplemented( + gb_type="constant-like method call with unsupported return type", + context=f"{self._error_prefix}.{name}(*{args}, **{kwargs}) returned {result}", + explanation=f"Attempted to call {self._error_prefix}.{name}, got unsupported return value {result}.", + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + ) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + result = getattr(self.value, name) + if isinstance(result, self.np_floating): + result = float(result) + if isinstance(result, self.np_dtype): + return NumpyDTypeVariable(result) + if isinstance(result, type) and issubclass(result, self.np_generic): + # things like x.dtype.type + return NumpyVariable(result) + if variables.ConstantVariable.is_literal(result): + return VariableTracker.build(tx, result) + return GetAttrVariable(self, name, py_type=type(result)) + + +class TorchVersionVariable(ConstantLikeVariable): + _error_prefix = "torch.__version__" + + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("value", torch.__version__) + assert kwargs["value"] is torch.__version__ + super().__init__(**kwargs) + + +class NumpyDTypeVariable(ConstantLikeVariable): + def as_proxy(self) -> str: + """Similar to how numpy dtype descriptors (e.g. np.float32 ) are handled by NumpyVariable: + + np.dtype() objects are serialized as strings, torch._numpy wrappers will normalize to the torch dtype. + This also handles unsupported things nicely (i.e. structured arrays and object arrays). + """ + return self.value.type.__name__ + + +np_constant_collections_map = { + tnp.finfo: ConstantLikeVariable, + tnp.iinfo: ConstantLikeVariable, + tnp.dtype: NumpyDTypeVariable, +} + + +class RandomClassVariable(VariableTracker): + """random.Random""" + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + def python_type(self) -> type: + return type + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> "RandomVariable": + if len(args) > 1 or kwargs: + unimplemented( + gb_type="random.Random() with improper arguments", + context=f"args: {args}, kwargs: {kwargs}", + explanation="random.Random() with > 1 arg or with kwargs is not supported.", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + seed = variables.ConstantVariable.create(None) if len(args) == 0 else args[0] + return RandomVariable( + seed=seed, mutation_type=variables.base.ValueMutationNew() + ) + + +class RandomVariable(VariableTracker): + """random.Random() + + Implemented by wrapping a VariableTracker around a random.Random object. + The supported methods for the random.Random object cannot be overridden. + Assumes that random objects behave the same given a set seed or state. + """ + + _cpython_type = random.Random + + _nonvar_fields = { + "random", + *VariableTracker._nonvar_fields, + } + + _supported_fn_names = { + "random", + "randint", + "randrange", + "uniform", + } + + def __init__( + self, + rand: random.Random | None = None, + seed: VariableTracker | None = None, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + if rand is not None: + assert self.is_supported_random_obj(rand) + self.random = random.Random() + self.random.setstate(rand.getstate()) + else: + seed = seed.as_python_constant() if seed is not None else None + self.random = random.Random(seed) + + def python_type(self) -> type[random.Random]: + return random.Random + + def as_python_constant(self) -> random.Random: + return self.random + + @staticmethod + def is_supported_random_obj(val: Random) -> bool: + if type(val) is not random.Random: + return False + for name in itertools.chain( + RandomVariable._supported_fn_names, ("seed", "getstate", "setstate") + ): + if not hasattr(val, name): + return False + meth = getattr(val, name) + if inspect.isbuiltin(meth): + # e.g. random.Random.random + if meth != getattr(random.Random, name).__get__(val): + return False + else: + if getattr(meth, "__func__", None) is not getattr(random.Random, name): + return False + return True + + @staticmethod + def check_state(state: tuple[int, tuple[int, ...], float | None]) -> None: + assert type(state) is tuple + assert type(state[0]) is int + assert type(state[1]) is tuple + assert all(type(x) is int for x in state[1]) + assert state[2] is None or type(state[2]) is float + + @staticmethod + def wrap_state(state: tuple[int, tuple[int, ...], float | None]) -> TupleVariable: + RandomVariable.check_state(state) + return variables.TupleVariable( + [ + variables.ConstantVariable.create(state[0]), + variables.TupleVariable( + [variables.ConstantVariable.create(x) for x in state[1]] + ), + variables.ConstantVariable.create(state[2]), + ] + ) + + @staticmethod + def unwrap_state( + state: VariableTracker, + ) -> tuple[int, tuple[int, ...], float | None]: + state_obj = state.as_python_constant() + RandomVariable.check_state(state_obj) + return state_obj + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "seed": + tx.output.side_effects.mutation(self) + self.random.seed( + *[x.as_python_constant() for x in args], + **{key: val.as_python_constant() for key, val in kwargs.items()}, + ) + return variables.ConstantVariable.create(None) + elif name == "getstate": + return self.wrap_state(self.random.getstate()) + elif name == "setstate": + tx.output.side_effects.mutation(self) + self.random.setstate(self.unwrap_state(args[0])) + return variables.ConstantVariable.create(None) + elif name in self._supported_fn_names: + tx.output.side_effects.mutation(self) + state = self.random.getstate() + + def call_random_meth(*args: Any, **kwargs: Any) -> Any: + r = random.Random() + r.setstate(state) + return getattr(r, name)(*args, **kwargs) + + # self.random state not actually updated by call_random_meth, so update here + # by calling the method + getattr(self.random, name)( + *[x.as_python_constant() for x in args], + **{k: v.as_python_constant() for k, v in kwargs.items()}, + ) + + return call_random_fn(tx, call_random_meth, args, kwargs) + return super().call_method(tx, name, args, kwargs) + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.extend_output( + [ + codegen.create_load_python_module(random), + codegen.create_load_attr("Random"), + ] + ) + ) + codegen.call_function(0, False) + # NOTE using add_push_null may result in NULL being duplicated + # so defer the push_null to call_function + codegen.dup_top() + codegen.load_attr("setstate") + codegen(self.wrap_state(self.random.getstate())) + codegen.call_function(1, True) + codegen.pop_top() + + +class WeakRefVariable(VariableTracker): + def python_type(self) -> type: + return weakref.ref + + @staticmethod + # pyrefly: ignore [bad-override, bad-param-name-override] + def build( + tx: "InstructionTranslator", + weakref_value: weakref.ReferenceType[Any], + source: Source | None, + **options: Any, + ) -> "WeakRefVariable": + assert source is not None + callback = weakref_value.__callback__ + callback_source = source and AttrSource(source, "__callback__") + callback_vt = VariableTracker.build(tx, callback, callback_source) + referent = weakref_value() + source = source and WeakRefCallSource(source) + referent_vt = VariableTracker.build(tx, referent, source) + options["source"] = source + return WeakRefVariable(referent_vt, callback_vt, **options) + + def __init__( + self, referent_vt: VariableTracker, callback_vt: VariableTracker, **options: Any + ) -> None: + super().__init__(**options) + self.referent_vt = referent_vt + self.callback_vt = callback_vt + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + return self.referent_vt + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null(lambda: codegen.load_import_from("weakref", "ref")) + codegen(self.referent_vt) + codegen(self.callback_vt) + codegen.extend_output(create_call_function(2, False)) + + def is_python_hashable(self) -> bool: + return self.referent_vt.is_python_hashable() + + def get_python_hash(self) -> int: + # weakref relies on the referent's hash + return self.referent_vt.get_python_hash() + + def is_python_equal(self, other: object) -> bool: + if not isinstance(other, WeakRefVariable): + return False + return self.referent_vt.is_python_equal(other.referent_vt) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/nn_module.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/nn_module.py new file mode 100644 index 0000000000000000000000000000000000000000..ece303e249f604e99c131f97889bb019a4f644dc --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/nn_module.py @@ -0,0 +1,1394 @@ +""" +This module implements variable tracking for PyTorch nn.Module instances during Dynamo tracing. + +It provides specialized handling for different types of nn.Module instances through several key classes: + +- NNModuleVariable: Handles instance-specific module tracing, specializing on module id() and placing + parameters directly on the torch.fx.GraphModule. This creates one graph per module instance. + +- UnspecializedNNModuleVariable: Provides class-level module tracing, treating nn.Modules like other + user-defined objects and passing parameters as inputs to the FX graph. This creates one graph per + module class. + +- UnspecializedBuiltinNNModuleVariable: Specifically handles built-in PyTorch modules (e.g. nn.Linear) + with appropriate optimizations. + +- FSDPManagedNNModuleVariable: Special handling for FSDP-wrapped modules with modified guarding behavior + and parameter handling. + +The module integrates with Dynamo's broader tracing functionality to handle module method calls, +parameter access, hooks, and other nn.Module behaviors while maintaining proper scoping and guarding +of module state. +""" + +import functools +import inspect +import itertools +import re +import types +from collections.abc import Iterable, Sequence +from contextlib import contextmanager, nullcontext +from typing import Any, TYPE_CHECKING + +import torch.nn +from torch._guards import Source + +from .. import graph_break_hints, trace_rules, variables +from ..exc import ( + handle_observed_exception, + ObservedAttributeError, + raise_observed_exception, + raise_type_error, + unimplemented, + UnspecializeRestartAnalysis, + Unsupported, +) +from ..guards import GuardBuilder, install_guard +from ..mutation_guard import GenerationTracker +from ..source import ( + AttrSource, + ConstDictKeySource, + DictGetItemSource, + FSDPNNModuleSource, + GetItemSource, + NNModuleSource, + UnspecializedNNModuleSource, +) +from ..utils import ( + enumerate_items_with_dict_position, + get_custom_getattr, + get_fake_value, + is_lazy_module, + is_namedtuple, + is_safe_constant, + istensor, + istype, + nnmodule_has_hooks, + object_has_getattribute, + proxy_args_kwargs, + raise_args_mismatch, + set_example_value, + unpatched_nn_module_call, + unpatched_nn_module_call_impl, +) +from .base import typestr, ValueMutationNew, VariableTracker +from .functions import invoke_and_store_as_constant +from .lazy import LazyVariableTracker +from .user_defined import UserDefinedObjectVariable + + +if TYPE_CHECKING: + from torch._dynamo.symbolic_convert import InstructionTranslator + + from .constant import ConstantVariable + from .dicts import DunderDictVariable + + +def initialize_lazy_module( + tx: "InstructionTranslator", + mod: torch.nn.Module, + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], +) -> None: + """ + Fairly coupled helper used by NNModuleVariable and UnspecializedNNModuleVariable. + + Used to cause lazy module to be initialized (and delete its init hook) before tracing. Especially + useful now that 'allowed' modules graph-break on hooks, calling this first ensures there is no hook + by the time we trace __call__ and thus no graph-break for lazy allowed modules. + """ + if hasattr(mod, "_initialize_hook"): + + def convert_to_fake(x: Any) -> Any: + if is_namedtuple(x): + return type(x)(*(convert_to_fake(elem) for elem in x)) + elif isinstance(x, dict): + return {k: convert_to_fake(v) for k, v in x.items()} # type: ignore[misc] + elif isinstance(x, (list, tuple, set)): + return type(x)(convert_to_fake(elem) for elem in x) + elif isinstance(x, torch.fx.Proxy): + return get_fake_value(x.node, tx) + else: + return x + + proxy_args, proxy_kwargs = proxy_args_kwargs(args, kwargs) + fake_args = [convert_to_fake(arg) for arg in proxy_args] + fake_kwargs = {k: convert_to_fake(v) for k, v in proxy_kwargs.items()} + try: + mod._infer_parameters(mod, fake_args, fake_kwargs) # type: ignore[operator] + except AttributeError: + # Re-raise with the original error message from the AttributeError + raise_observed_exception( + AttributeError, + tx, + args=["AttributeError during lazy module initialization"], + ) + + +@contextmanager +def record_nn_module_stack( + module_key: str, source: Source, tx: "InstructionTranslator", mod: torch.nn.Module +) -> Any: + fully_qualified_name = source.name + # Remove redundant namings + fully_qualified_name = re.sub( + r"\._(?:modules|parameters|buffers)\[(['\"])([^'\"\]]+)\1\]", + r".\2", + fully_qualified_name, + ) + num_calls = tx.num_calls.get(fully_qualified_name, 0) + module_key = f"{module_key}@{num_calls}" if num_calls > 0 else module_key + try: + tx.nn_module_stack[module_key] = (fully_qualified_name, mod.__class__) + tx.num_calls[fully_qualified_name] = num_calls + 1 + yield + finally: + del tx.nn_module_stack[module_key] + + +def guard_to_detect_forward_monkeypatching( + source: Source | None, mod: torch.nn.Module +) -> None: + # Users sometimes patch the forward method of a nn module instance to + # perform optimizations like quantization. Though this is not a good + # software practice, but python allows this and Dynamo needs to detect + # this patching. + # + # One way to do this is to add an ID_MATCH guard on every function + # getting inlined (https://github.com/pytorch/pytorch/pull/124975). But + # this increased guard overhead by around 20%. + # + # To keep the guard overhead down, we just guard on the `forward` being + # not present in the mod __dict__. The common case of patching forward + # method adds `forward` in the instance __dict__, whereas the unpatched + # `forward` sits in the type(mod).__dict__ + if source: + if "forward" in mod.__dict__ and callable(mod.__dict__["forward"]): + # Monkeypatched forward method, add an ID_MATCH guard on forward function + fwd = mod.__dict__["forward"] + forward_source = AttrSource(source, "forward") + if type(fwd) is types.MethodType: + forward_source = AttrSource(forward_source, "__func__") + install_guard(forward_source.make_guard(GuardBuilder.CLOSURE_MATCH)) + else: + # Common case - check that the forward key is absent in mod __dict__ + install_guard( + source.make_guard( + functools.partial( + GuardBuilder.NOT_PRESENT_IN_GENERIC_DICT, attr="forward" + ) + ) + ) + + +class NNModuleVariable(VariableTracker): + _nonvar_fields = { + "module_type", + "module_key", + "value", + "nn_module_stack_source", + *VariableTracker._nonvar_fields, + } + + def __init__( + self, module_type: type, module_key: str, value: torch.nn.Module, **kwargs: Any + ) -> None: + super().__init__(**kwargs) + self.module_type = module_type + self.module_key = module_key + self.value = value + # pyrefly: ignore[bad-override] + # NOTE: Don't remove this; better than adding suppressions + # everywhere else with asserts + self.source: Source = self.source + self.nn_module_stack_source = self.source + + def get_dict_vt(self, tx: "InstructionTranslator") -> "DunderDictVariable": + if not hasattr(self, "dict_vt"): + self.dict_vt = variables.DunderDictVariable.create(tx, self) + return self.dict_vt + + def get_nn_module_stack_source(self) -> Source: + res = self.nn_module_stack_source or self.source + assert res + return res + + def set_nn_module_stack_source(self, source: Source) -> None: + self.nn_module_stack_source = source + + def python_type(self) -> type: + return self.module_type + + def get_real_python_backed_value(self) -> object: + return self.value + + def bool_impl(self, tx: "InstructionTranslator") -> VariableTracker: + """nb_bool for nn.Module. + + nn.Module itself has no __bool__ or __len__, so bare modules are always + truthy. Subclasses like ModuleList/ModuleDict define __len__, so + bool(module) calls PyObject_IsTrue which falls through nb_bool (NULL) + to sq_length/mp_length. We evaluate on the real module to capture this. + """ + from .constant import ConstantVariable + + mod = tx.output.get_submodule(self.module_key) + return ConstantVariable.create(bool(mod)) + + def _wrap_submodule( + self, + tx: "InstructionTranslator", + source: Source, + submod: torch.nn.Module, + *key_extra: Any, + **options: Any, + ) -> None: + return + + def unpack_var_sequence(self, tx: "InstructionTranslator") -> list[VariableTracker]: + # implement list/iter/tuple/etc calls + base = tx.output.get_submodule(self.module_key) + result: list[VariableTracker] = [] + if isinstance(base, torch.nn.ModuleDict): + for name, submod in base.items(): + name_var = VariableTracker.build(tx, name) + tx.output.register_attr_or_module( + submod, + self.module_key, + name, + source=NNModuleSource(GetItemSource(self.source, name)), # type: ignore[arg-type] + ) + result.append(name_var) + return result + + assert isinstance( + base, (torch.nn.ModuleList, torch.nn.ParameterList, torch.nn.Sequential) + ), typestr(base) + for idx, submod in enumerate(base): + result.append( + tx.output.register_attr_or_module( + submod, + self.module_key, + idx, + source=NNModuleSource(GetItemSource(self.source, idx)), + ) + ) + return result + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> "ConstantVariable": + mod = tx.output.get_submodule(self.module_key) + result = hasattr(mod, name) + install_guard( + self.source.make_guard(functools.partial(GuardBuilder.HASATTR, attr=name)) + ) + return VariableTracker.build(tx, result) + + def is_training(self, tx: "InstructionTranslator") -> bool: + mod = tx.output.get_submodule(self.module_key) + return getattr(mod, "training", False) + + def convert_to_unspecialized(self, tx: "InstructionTranslator") -> None: + """Restart analysis treating this module as an UnspecializedNNModuleVariable""" + mod = tx.output.get_submodule(self.module_key) + GenerationTracker.tag(mod) + + # Mark the class dynamic unless its module initialization + if tx.f_code.co_name != "__init__": + GenerationTracker.mark_class_dynamic(type(mod)) + raise UnspecializeRestartAnalysis + + def _custom_getattr_fallback( + self, + base: torch.nn.Module, + tx: "InstructionTranslator", + name: str, + obj_source: Source, + ) -> VariableTracker | None: + """Check for a __getattr__ and handle it specially if it is implemented""" + if object_has_getattribute(base): + getattribute_fn = inspect.getattr_static(type(base), "__getattribute__") + new_source = ( + AttrSource(obj_source, "__getattribute__") if obj_source else None + ) + try: + return variables.UserMethodVariable( + getattribute_fn, + self, + source=new_source, + ).call_function(tx, [variables.ConstantVariable.create(name)], {}) + except ObservedAttributeError: + handle_observed_exception(tx) + except Unsupported: + unimplemented( + gb_type="Custom __getattribute__ in nn.Module attribute access", + context=f"var_getattr {self} {name}", + explanation="Dynamo could not trace through the custom " + "`__getattribute__` method on this `nn.Module`.", + hints=[ + "Simplify your `__getattribute__` implementation, " + "or replace it with a targeted `@property`.", + *graph_break_hints.SUPPORTABLE, + ], + ) + + getattr_fn = get_custom_getattr(base, ignore_nn_module_getattr=True) + if getattr_fn is None: + return None + + if not isinstance(getattr_fn, types.FunctionType): + unimplemented( + gb_type="torch.nn.Module with a non-function custom __getattr__", + context=f"var_getattr {self} {name}", + explanation=( + "Dynamo detected a nn.Module object with a custom " + "`__getattr__` method, but this method is not a standard " + "Python function (e.g., it might be implemented in C/C++). " + "Dynamo cannot currently trace into such non-standard " + "`__getattr__` methods." + ), + hints=[ + "Avoid using objects with non-standard __getattr__ methods " + "within the compiled region. If possible, implement " + "__getattr__ as a standard Python function.", + *graph_break_hints.SUPPORTABLE, + ], + ) + + options = {"source": AttrSource(obj_source, "__getattr__")} + + return variables.UserMethodVariable(getattr_fn, self, **options).call_function( + tx, [VariableTracker.build(tx, name)], {} + ) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + source = self.source and AttrSource(self.source, name) + + base = tx.output.get_submodule(self.module_key) + # NB: We look up attributes in __dict__ directly, bypassing any custom + # __getattribute__. Custom __getattribute__ is only traced through as a + # fallback (via _custom_getattr_fallback) for attributes not found here. + base_dict = object.__getattribute__(base, "__dict__") + object_member = True + all_class_attribute_names = set() + for x in inspect.getmro(base.__class__): + all_class_attribute_names.update(x.__dict__.keys()) + + if not self.source: + unimplemented( + gb_type="getattr with no source", + context=f"var_getattr {self} {name}", + explanation="Dynamo does not know how to access an attribute " + "on an `nn.Module` instance that lacks a source. This is " + "usually an internal error in Dynamo.", + hints=[*graph_break_hints.DYNAMO_BUG], + ) + + if name == "__dict__": + return self.get_dict_vt(tx) + + subobj = None + if name in base_dict: + subobj = base_dict[name] + elif ( + "_modules" in base_dict + and name in base_dict["_modules"] + and name not in all_class_attribute_names + ): + subobj = base_dict["_modules"][name] + elif "_parameters" in base_dict and name in base_dict["_parameters"]: + subobj = base_dict["_parameters"][name] + elif "_buffers" in base_dict and name in base_dict["_buffers"]: + subobj = base_dict["_buffers"][name] + else: + try: + subobj = inspect.getattr_static(base, name) + object_member = False + except AttributeError: + # see if we can fallback to __getattr__, which is not checked by getattr_static + result = self._custom_getattr_fallback( + base=base, tx=tx, name=name, obj_source=self.source + ) + if result is not None: + return result + # if we can't find a __getattr__, we can't parse this, raise attribute error + raise_observed_exception( + AttributeError, + tx, + args=[f"'{type(base).__name__}' object has no attribute '{name}'"], + ) + + if name == "forward": + guard_to_detect_forward_monkeypatching(self.source, base) + + if name == "__class__" and not object_member: + return VariableTracker.build(tx, base.__class__, source=source) + + if object_member: + out = VariableTracker.build(tx, subobj, NNModuleSource(source)) # type: ignore[arg-type] + + if isinstance(out, (NNModuleVariable, UnspecializedNNModuleVariable)): + # nn_module_stack source is BC surface area. Ensure that + # mod._modules["linear"] is reflected as mod.linear for + # nn_module_stack. + out.set_nn_module_stack_source( + AttrSource(self.get_nn_module_stack_source(), name) + ) + return out + + else: + if istype(subobj, property): + if self.source: + # Read the class attribute to reach the property + source = AttrSource(AttrSource(self.source, "__class__"), name) + # Get the getter function + source = AttrSource(source, "fget") + return variables.UserFunctionVariable( + subobj.fget, # pyrefly: ignore[bad-argument-type] + source=source, + ).call_function(tx, [(self)], {}) + elif istype(subobj, classmethod): + return variables.UserMethodVariable( + subobj.__func__, + variables.UserDefinedObjectVariable(type(base)), + source=source, + ) + elif istype(subobj, staticmethod): + return variables.UserFunctionVariable( + subobj.__get__(base), + source=source, + ) + elif istype(subobj, types.FunctionType): + return variables.UserMethodVariable(subobj, self, source=source) + elif is_safe_constant(subobj) or istensor(subobj): + # Support possibly common cases of class members + return VariableTracker.build(tx, subobj, NNModuleSource(source)) # type: ignore[arg-type] + else: + unimplemented( + gb_type="Unsupported nn.Module attribute type", + context=f"nn.Module subclass: {typestr(base)}, name: {name}, attribute type: {typestr(subobj)}", + explanation=f"Dynamo does not support tracing nn.Module attributes of type `{typestr(subobj)}`", + hints=[ + f"Refactor your code so that `{name}` (type `{typestr(subobj)}`) is not an attribute of `{typestr(base)}`", + "Currently supported attribute types are methods, classmethods, staticmethods, " + "properties, constants, and tensors.", + *graph_break_hints.SUPPORTABLE, + ], + ) + + return super().var_getattr(tx, name) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + mod = tx.output.get_submodule(self.module_key) + + with record_nn_module_stack( + self.module_key, self.get_nn_module_stack_source(), tx, mod + ): + is_lazy = is_lazy_module(mod) + if ( + isinstance(mod, torch.nn.Sequential) + and mod.__class__.forward is torch.nn.Sequential.forward + ): + if nnmodule_has_hooks(mod): + # We do not want to unroll sequential if it has hooks, since evaporating it + # will cause hooks to not fire! + # This terminates and restart the tracing process + self.convert_to_unspecialized(tx) + + # Unroll sequential + assert not is_lazy, ( + "Expected lazy sequential isn't a valid combination?" + ) + if kwargs: + raise_args_mismatch( + tx, + "torch.nn.Module.Sequential", + "0 kwargs", + f"{len(kwargs)} kwargs", + ) + (arg,) = args + # TODO: Use named_children when it supports remove_duplicate=False. + for child_name, submod in mod._modules.items(): + tx.call_function( + tx.output.register_attr_or_module( + submod, + self.module_key, + child_name, + source=NNModuleSource(AttrSource(self.source, child_name)), # type: ignore[arg-type] + ), + [arg], + {}, + ) + arg = tx.pop() + return arg + + if is_lazy: + # The module type will change after it is called + if mod.cls_to_become is not None: + self.module_type = mod.cls_to_become # type: ignore[assignment] + + # The pre-hook runs to initialize the module shapes, then deletes itself. After this, + # the module is more or less not lazy and can be treated as a normal module regardless of + # is_allowed or other variations. + initialize_lazy_module(tx, mod, args, kwargs) + + # If we are tracing the higher order op, we want Dynamo to step + # inside the module call so that Dynamo can see the underlying + # parameters and buffers and raise them as inputs to the graph. + # + # NB: torch.nn.utils.parametrize changes the class type of a + # parametrized module such that its __module__ points to + # "torch.nn.utils.parametrize". + if ( + tx.output.is_root_tracer() + and mod.__module__.startswith(("torch.nn.", "torch.ao.")) + and mod.__module__ != "torch.nn.utils.parametrize" + ): + if nnmodule_has_hooks( + mod, check_forward_hooks=True, check_backward_hooks=True + ): + # End of fn, this bubbles up and restarts tracing. + self.convert_to_unspecialized(tx) + + from .builder import wrap_fx_proxy + + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_module", + self.module_key, + *proxy_args_kwargs(args, kwargs), + ), + ) + else: + if isinstance(mod, torch.fx.GraphModule): + # TODO: do we want to support __call__ for GM's? + # If so at least some changes are needed, we don't allow inlining + # the call_wrapped currently, and maybe other issues too + fn = mod.forward + fn_source = AttrSource(self.source, "forward") + else: + fn = mod._call_impl + fn_source = AttrSource(self.source, "_call_impl") + if istype(fn, types.MethodType): + fn = fn.__func__ + fn_source = AttrSource(fn_source, "__func__") + args = [self] + list(args) + else: + assert istype(fn, types.FunctionType) + return tx.inline_user_function_return( + variables.UserFunctionVariable(fn, source=fn_source), + args, + kwargs, + ) + + def mp_subscript_impl( + self, + tx: "InstructionTranslator", + key: "VariableTracker", + ) -> "VariableTracker": + # nn.Module containers (ModuleList/Dict/Sequential/ParameterDict/ParameterList) + # These are Python-level __getitem__, not CPython C slots. + # TODO(follow-up): add tests for ModuleList negative index, ModuleList/Sequential + # slice, ModuleDict missing key, invalid key types + from .lists import SliceVariable + from .tensor import SymNodeVariable + + module = tx.output.get_submodule(self.module_key) + + builtin_supported = ( + torch.nn.ModuleDict.__getitem__, + torch.nn.ModuleList.__getitem__, + torch.nn.ParameterDict.__getitem__, + torch.nn.ParameterList.__getitem__, + torch.nn.Sequential.__getitem__, + ) + # pyrefly: ignore[missing-attribute] + if type(module).__getitem__ not in builtin_supported: + if not ( + key.is_python_constant() + and isinstance(key.as_python_constant(), (str, int)) + ): + unimplemented( + gb_type="Invalid or non-const argument in nn.Module __getitem__", + context=f"mp_subscript_impl: {self} {key}", + explanation="Dynamo does not support calling " + f"method `__getitem__` of ``nn.Module`` {module} with a non-constant or non-(str, int) key.", + hints=["Use constant arguments of type str or int for __getitem__"], + ) + fn = module.__getitem__.__func__ # pyrefly: ignore[missing-attribute] + + assert isinstance(fn, types.FunctionType) + + src = AttrSource(AttrSource(self.source, "__getitem__"), "__func__") # type: ignore[arg-type] + return tx.inline_user_function_return( + variables.UserFunctionVariable(fn, source=src), + [self, key], + {}, + ) + + if isinstance(key, SliceVariable): + # TODO(anijain2305,export-team) - Remove this if condition when inlining of inbuilt nn modules is + # enabled for export. + if tx.output.export: + result = [] + keys = list(range(len(module)))[key.as_python_constant()] # type: ignore[arg-type] + for idx, submod in enumerate(module[key.as_python_constant()]): # type: ignore[arg-type] + k = keys[idx] + src = NNModuleSource(GetItemSource(self.source, k)) + result.append( + tx.output.register_attr_or_module( + submod, + k, + source=src, + ) + ) + + new_module = module[key.as_python_constant()] # type: ignore[index] + new_module_variable = tx.output.register_attr_or_module( + new_module, + f"{self}.__getitem__(slice)", + source=NNModuleSource( + GetItemSource(self.source, key.as_python_constant()) + ), + ) + return new_module_variable + else: + # slice on nn module results in a creation of new module instance, so we need to make it sourceless. + # Convert to unspecialized so that UnspecializedNNModule variable can take care of it. + self.convert_to_unspecialized(tx) + + key_value = 0 + if isinstance(key, SymNodeVariable): + key_value = key.evaluate_expr(tx.output) + elif key.is_python_constant(): + key_value = key.as_python_constant() + else: + unimplemented( + gb_type="Unsupported key type for nn.Module.__getitem__", + context=f"mp_subscript_impl: {self} {key}", + explanation="Dynamo does not support getitem on " + "`nn.Module` with non-constant key.", + hints=[], + ) + + submod = module[key_value] # type: ignore[index] + return tx.output.register_attr_or_module( + submod, + self.module_key, + key_value, + source=NNModuleSource(GetItemSource(self.source, key_value)), + ) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + constant: bool = False, + ) -> VariableTracker: + from . import ListIteratorVariable, TupleVariable + from .constant import ConstantVariable + + key = self.module_key + module = tx.output.get_submodule(key) + + def generic_call_method_helper(name: str) -> VariableTracker: + # Helper function to put a `call_method` node in FX graph, + # with nn.Module as the first arg. + mod_proxy = tx.output.create_proxy( + "get_attr", + self.module_key, + (), + {}, + ) + set_example_value(mod_proxy.node, module) + + proxy_args, proxy_kwargs = proxy_args_kwargs(args, kwargs) + + from .builder import wrap_fx_proxy + + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_method", + name, + args=(mod_proxy, *proxy_args), + kwargs=proxy_kwargs, + ), + ) + + if name in ["_call_impl", "_wrapped_call_impl"]: + # Example: `self.layer.__call__(x)` + # This is used for explicit calling `__call__` in a forward function. + # Dynamo inlines `__call__`, includes hooks. + return self.call_function(tx, args, kwargs) + elif name == "forward": + # Example: `self.layer.forward(x)` + # This is used for explicit calling `forward` in a forward function. + # Dynamo puts `call_method` node in FX, doesn't trigger hooks. + with record_nn_module_stack( + self.module_key, self.get_nn_module_stack_source(), tx, module + ): + return generic_call_method_helper(name) + + if name == "_check_input_dim" and trace_rules.is_torch_inline_allowed( + inspect.getfile(module.__class__._check_input_dim) # type: ignore[union-attr] + ): + return ConstantVariable.create(True) + + if name == "_get_item_by_idx": + if not args[1].is_python_constant(): + raise_type_error( + tx, + f"``nn.Module`` {module}'s call method {name} requires a constant index argument", + ) + if not isinstance(args[0], TupleVariable): + raise_type_error( + tx, + f"``nn.Module`` {module}'s call method {name} requires a tuple as first argument", + ) + mod_var = args[0].items[args[1].value] # type: ignore[attr-defined] + if isinstance(mod_var, UnspecializedNNModuleVariable): + return mod_var + key = mod_var.module_key # type: ignore[attr-defined] + submod = tx.output.get_submodule(key) + return tx.output.register_attr_or_module( + submod, + key, + key, + source=NNModuleSource(GetItemSource(self.source, key)), + ) + + if constant: + fn = getattr(module, name) + name = f"{module.__class__.__name__}_{name}_result" + return invoke_and_store_as_constant(tx, fn, name, args, kwargs) + + def assert_all_args_kwargs_const() -> None: + if not all( + x.is_python_constant() for x in itertools.chain(args, kwargs.values()) + ): + unimplemented( + gb_type="non-const argument in nn.Module method", + context=f"call_method: {self} {name} {args} {kwargs}", + explanation="Dynamo does not support calling " + f"method `{name}` of ``nn.Module`` {module} with non-constant arguments.", + hints=[], + ) + + def get_kwargs(*names: str) -> dict[str, Any]: + assert_all_args_kwargs_const() + fn = getattr(module, name) + bound_args = inspect.signature(fn).bind( + *([x.as_python_constant() for x in args]), + **{k: v.as_python_constant() for k, v in kwargs.items()}, + ) + bound_args.apply_defaults() + bound_args = bound_args.arguments + return {k: bound_args[k] for k in names} + + def wrap_values( + items: Iterable[tuple[Any, Any]], + ) -> "variables.ListIteratorVariable": + named_children: list[VariableTracker] = [] + for name, submod in items: + named_children.append( + tx.output.register_attr_or_module( + submod, + key, + name, + source=NNModuleSource(gen_source(self.source, name)), + ) + ) + return ListIteratorVariable( + named_children, mutation_type=ValueMutationNew() + ) + + def named_embed(name: str, obj: Any) -> "variables.TupleVariable": + return TupleVariable( + [ + VariableTracker.build(tx, name), + tx.output.register_attr_or_module( + obj, + key, + name, + source=NNModuleSource(gen_source(self.source, name)), + ), + ] + ) + + def gen_source(source: Source, name: str) -> Source: + name_split = name.split(".") + if name_split[0] == "": + return source + while len(name_split) > 0: + x = name_split.pop(0) + source = AttrSource(source, x) + return source + + if name == "named_children": + tx.output.guard_on_key_order.add(AttrSource(self.source, "_modules")) + if args or kwargs: + raise_args_mismatch( + tx, + name, + "0 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + named_children: list[VariableTracker] = [] + for name, submod in module.named_children(): + named_children.append(named_embed(name, submod)) + return ListIteratorVariable( + named_children, mutation_type=ValueMutationNew() + ) + elif name == "named_parameters": + tx.output.guard_on_key_order.add(AttrSource(self.source, "_parameters")) + named_parameters: list[VariableTracker] = [] + for name, param in module.named_parameters( + **get_kwargs("prefix", "recurse") + ): + named_parameters.append(named_embed(name, param)) + return ListIteratorVariable( + named_parameters, mutation_type=ValueMutationNew() + ) + elif name == "named_buffers": + tx.output.guard_on_key_order.add(AttrSource(self.source, "_buffers")) + named_buffers: list[VariableTracker] = [] + for name, buffer in module.named_buffers( + **get_kwargs("prefix", "recurse", "remove_duplicate") + ): + named_buffers.append(named_embed(name, buffer)) + return ListIteratorVariable(named_buffers, mutation_type=ValueMutationNew()) + elif name == "named_modules": + tx.output.guard_on_key_order.add(AttrSource(self.source, "_modules")) + named_modules_list: list[VariableTracker] = [] + for name, submod in module.named_modules( + **get_kwargs("memo", "prefix", "remove_duplicate") + ): + named_modules_list.append(named_embed(name, submod)) + return ListIteratorVariable( + named_modules_list, mutation_type=ValueMutationNew() + ) + elif name == "children": + tx.output.guard_on_key_order.add(AttrSource(self.source, "_modules")) + if args or kwargs: + raise_args_mismatch( + tx, + name, + "0 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + return wrap_values(module.named_children()) + elif name == "modules": + tx.output.guard_on_key_order.add(AttrSource(self.source, "_modules")) + return wrap_values(module.named_modules()) + elif name == "parameters": + tx.output.guard_on_key_order.add(AttrSource(self.source, "_parameters")) + return wrap_values(module.named_parameters(**get_kwargs("recurse"))) + elif name == "buffers": + tx.output.guard_on_key_order.add(AttrSource(self.source, "_buffers")) + return wrap_values(module.named_buffers(**get_kwargs("recurse"))) + elif name == "keys": + if args or kwargs: + raise_args_mismatch( + tx, + name, + "0 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + result = [] + # pyrefly: ignore[not-iterable] + for tmp in module: + result.append(VariableTracker.build(tx, tmp)) + return ListIteratorVariable(result, mutation_type=ValueMutationNew()) + elif name == "values": + if args or kwargs: + raise_args_mismatch( + tx, + name, + "0 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + return wrap_values(module.items()) # type: ignore[operator] + elif name == "items": + if args or kwargs: + raise_args_mismatch( + tx, + name, + "0 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + items_result: list[VariableTracker] = [] + for name, submod in module.items(): # type: ignore[operator] + items_result.append(named_embed(name, submod)) + return ListIteratorVariable(items_result, mutation_type=ValueMutationNew()) + elif name == "__iter__": + return ListIteratorVariable( + self.unpack_var_sequence(tx), mutation_type=ValueMutationNew() + ) + elif ( + name == "__contains__" + and isinstance(module, (torch.nn.ModuleDict, torch.nn.ParameterDict)) + and args + and args[0].is_python_constant() + ): + return VariableTracker.build( + tx, args[0].as_python_constant() in module._modules + ) + elif ( + name == "_get_abs_string_index" + or ( + isinstance(module, torch.nn.modules.conv._ConvNd) + and name == "_conv_forward" + ) + or ( + isinstance(module, torch.nn.modules.conv._ConvTransposeNd) + and name == "_output_padding" + ) + ): + # Inline the function + fn = getattr(module, name).__func__ + fn_source = AttrSource(AttrSource(self.source, name), "__func__") # type: ignore[arg-type] + return tx.inline_user_function_return( + variables.UserFunctionVariable(fn, source=fn_source), + [self] + list(args), + kwargs, + ) + # A loose heuristic, but seems to be generally good before we drop into the + # manual handling of inputs + elif ( + name in module.__class__.__dict__ + and callable(module.__class__.__dict__[name]) + and all(x.is_tensor() for x in itertools.chain(args, kwargs.values())) + ): + return generic_call_method_helper(name) + else: + return super().call_method(tx, name, list(args), kwargs) + + def sq_length(self, tx: "InstructionTranslator") -> "VariableTracker": + """Sequence length for container modules (e.g., nn.Sequential).""" + module = tx.output.get_submodule(self.module_key) + return VariableTracker.build(tx, len(module)) # type: ignore[arg-type] + + +class UnspecializedNNModuleVariable(UserDefinedObjectVariable): + _nonvar_fields = { + "value_type", + "is_state_mutated", + "nn_module_stack_source", + *UserDefinedObjectVariable._nonvar_fields, + } + + """ + The above class will specialize on the id() of a module and place + parameters on the torch.fx.GraphModule. Giving one graph per + module instance. This version treats nn.Modules() like other user + defined objects and will pass parameters into the FX graph as inputs. + Giving one graph per module class. + """ + + def __init__(self, value: torch.nn.Module, **kwargs: Any) -> None: + if "value_type" in kwargs: + lazy_value_to_become = getattr(kwargs["value_type"], "cls_to_become", None) + if type(value) is lazy_value_to_become: + # We may have cloned a variabletracker for a LazyModule earlier (e.g. tracking side-effects) + # and then later we called and mutated the LazyModule into a MaterializedModule. + # We do not do the mutation upon first seeing a LazyModule since we preserve eager semantics to only + # mutate upon first call, but this requires we update multiple copies of the VariableTracker post-mutation. + kwargs["value_type"] = type(value) + + super().__init__(value=value, **kwargs) + self.is_state_mutated = False + # nn_module_stack_source is used to ensure BC for nn_module_stack. + # Downstream users prefer mod.linear instead of mod._modules['linear'] + # as the module stack. When Dynamo inlines the __getattr__ method, we + # cannot use self.source for nn_module_stack because it will be similar + # to mod._modules['linear']. In these cases, we set the + # nn_module_stack_source appropriately to resemble mod.linear. + self.nn_module_stack_source = self.source + + def _wrap_source(self, attr_source: Source) -> Source: + # the vt is already wrapped with UnspecializedNNModuleSource + return attr_source + + def get_nn_module_stack_source(self) -> Source: + res = self.nn_module_stack_source or self.source + assert res + return res + + def set_nn_module_stack_source(self, source: Source) -> None: + self.nn_module_stack_source = source + + @staticmethod + @functools.cache + def _nn_module_method_ids() -> set[int]: + # Allow __setattr__ to fall through to base class handler + supported = { + torch.nn.Module.__setattr__, + torch.nn.Module.__init__, + torch.nn.Module.__delattr__, + } + return { + id(x.__code__) + for x in torch.nn.Module.__dict__.values() + if hasattr(x, "__code__") and x not in supported + } + + def unpack_var_sequence(self, tx: "InstructionTranslator") -> list[VariableTracker]: + try: + fn = inspect.getattr_static(self.value_type, "__iter__") + except AttributeError as e: + raise NotImplementedError from e + + if fn in ( + torch.nn.ModuleList.__iter__, + torch.nn.ParameterList.__iter__, + torch.nn.Sequential.__iter__, + ): + # The program can mutate the nn module object but the saved `value` + # will not reflect the mutations. So, trace through the `__iter__` + # function to reflect any tracked mutations. + return tx.inline_user_function_return( + VariableTracker.build(tx, fn), + [ + self, + ], + {}, + ).unpack_var_sequence(tx) + + return super().unpack_var_sequence(tx) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + mod = self.value + # see comment on lazy module handling in NNModuleVariable.call_function for context + if is_lazy_module(mod): # type: ignore[arg-type] + if mod.cls_to_become is not None: # type: ignore[attr-defined] + self.value_type = mod.cls_to_become # type: ignore[attr-defined,assignment] + initialize_lazy_module(tx, mod, args, kwargs) # type: ignore[arg-type] + + if not isinstance(mod, torch.fx.GraphModule): + name = "__call__" + fn = getattr(self.value_type, name) + else: + name = "_call_impl" + fn = getattr(self.value_type, name) + + # Check if we can short circuit nn.Module._call_impl to the forward + # method. NB - This is done to reduce the compile time of Dynamo. + if ( + istype(mod.__call__, types.MethodType) # type: ignore[operator] + and istype(mod._call_impl, types.MethodType) # type: ignore[attr-defined] + and mod.__call__.__func__ is unpatched_nn_module_call # type: ignore[operator] + and mod._call_impl.__func__ is unpatched_nn_module_call_impl # type: ignore[attr-defined] + # Consult pending STORE_ATTR side effects too. During tracing the + # patched forward may not be visible in mod.__dict__ yet. + and not self.has_key_in_generic_dict(tx, "forward") + ): + forward_method = inspect.getattr_static(mod, "forward") + if isinstance(forward_method, types.FunctionType): + globals_vt = tx.nn_modules_globals_vt + if not ( + self.var_getattr(tx, "_backward_hooks").realize().len() # type: ignore[attr-defined] + or self.var_getattr(tx, "_backward_pre_hooks").realize().len() # type: ignore[attr-defined] + or self.var_getattr(tx, "_forward_hooks").realize().len() # type: ignore[attr-defined] + or self.var_getattr(tx, "_forward_hooks_with_kwargs") # type: ignore[attr-defined] + .realize() + .len() + or self.var_getattr(tx, "_forward_pre_hooks").realize().len() # type: ignore[attr-defined] + or self.var_getattr(tx, "_forward_pre_hooks_with_kwargs") # type: ignore[attr-defined] + .realize() + .len() + or globals_vt.var_getattr(tx, "_global_backward_pre_hooks").len() # type: ignore[attr-defined] + or globals_vt.var_getattr(tx, "_global_backward_hooks").len() # type: ignore[attr-defined] + or globals_vt.var_getattr(tx, "_global_forward_hooks").len() # type: ignore[attr-defined] + or globals_vt.var_getattr(tx, "_global_forward_pre_hooks").len() # type: ignore[attr-defined] + or globals_vt.var_getattr(tx, "_global_backward_pre_hooks").len() # type: ignore[attr-defined] + or globals_vt.var_getattr(tx, "_global_backward_hooks").len() # type: ignore[attr-defined] + or globals_vt.var_getattr(tx, "_global_forward_hooks").len() # type: ignore[attr-defined] + or globals_vt.var_getattr(tx, "_global_forward_pre_hooks").len() # type: ignore[attr-defined] + ): + name = "forward" + fn = self.value_type.forward # type: ignore[attr-defined] + + if self.source: + source = self.get_source_by_walking_mro(tx, name) + else: + source = None + + guard_to_detect_forward_monkeypatching(self.source, mod) # type: ignore[arg-type] + + ctx = ( + record_nn_module_stack( + str(id(mod)), + self.get_nn_module_stack_source(), + tx, + mod, # type: ignore[arg-type] + ) + if self.source + else nullcontext() + ) + with ctx: + if not isinstance(fn, (types.FunctionType, torch.jit.ScriptFunction)): + fn_vt = VariableTracker.build(tx, fn, source=source, realize=True) + return fn_vt.call_function(tx, [self] + list(args), kwargs) + else: + # Ideally we would have just used VariableTracker.build(tx, fn, + # source=source) but that introduces guard on the + # `forward.__code__` object. Given that we already guard on the + # forward not present in generic dict, we dont need this guard. + return variables.UserFunctionVariable(fn, source=source).call_function( + tx, [self] + list(args), kwargs + ) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name in ["_call_impl", "_wrapped_call_impl"]: + fn = getattr(self.value_type, name) + if self.source: + source = self.get_source_by_walking_mro(tx, name) + else: + source = None + + fn_vt = VariableTracker.build(tx, fn, source=source, realize=True) + return fn_vt.call_function(tx, [self] + list(args), kwargs) + + if not self.has_key_in_generic_dict(tx, name): + try: + method = inspect.getattr_static(type(self.value), name) + except AttributeError: + method = None + + if isinstance(method, staticmethod): + source = AttrSource( + self.get_source_by_walking_mro(tx, name), "__func__" + ) + fn_vt = VariableTracker.build( + tx, method.__func__, source=source, realize=True + ) + return fn_vt.call_function(tx, args, kwargs) + + if ( + hasattr(method, "__code__") + and id(method.__code__) in self._nn_module_method_ids() + ): + unimplemented( + gb_type="UnspecializedNNModuleVariable missing method", + context=f"call_method: {self} {name} {args} {kwargs}", + explanation=f"Dynamo does not support tracing method {name} of nn.Module {self.value}", + hints=[ + "Dynamo does not really define unspecialized nn.Module very well.", + *graph_break_hints.DIFFICULT, + ], + ) + + # "_parameters" in self.value.__dict__ checks that module is initialized + if name == "__setattr__" and "_parameters" in self.value.__dict__: + # Record if mutations happens on parameters/buffers/modules. The + # mutations on these are not tracked by base class + # UserDefinedObject vt. This will be used later to graph break + # on seeing a parameters() and family calls. + # TODO(anijain2305) - This might not be needed if we let Dynamo + # inline both getattr and setattr. In that case, it should see + # the lowest level dicts - _parameters and family and + # automatically track mutations on those. Investigate if that + # can be done. + attr_name = args[0].as_python_constant() + value = args[1] + + # This is reverse engineered by looking at nn module __setattr__ + # logic. + if ( + value.is_tensor() and value.python_type() is torch.nn.Parameter + ) or attr_name in self.value.__dict__["_parameters"]: + # Handle parameters + self.is_state_mutated = True + elif attr_name in self.value.__dict__["_buffers"]: + # Handle buffers + self.is_state_mutated = True + elif ( + isinstance( + value, + ( + variables.NNModuleVariable, + variables.UnspecializedNNModuleVariable, + ), + ) + or attr_name in self.value.__dict__["_modules"] + ): + # Handle submodules + self.is_state_mutated = True + + if ( + method is torch.nn.Module.__setattr__ + and isinstance(args[1], variables.DeletedVariable) + ) or method is torch.nn.Module.__delattr__: + # Trace through __delattr__ to track mutations on the module + # members like `_modules``. + fn_vt = VariableTracker.build(tx, torch.nn.Module.__delattr__) + return fn_vt.call_function(tx, [self, args[0]], kwargs) + + return super().call_method(tx, name, list(args), kwargs) + + def getattr_helper( + self, tx: "InstructionTranslator", field: str, name_vt: VariableTracker + ) -> VariableTracker | None: + dict_vt = self.var_getattr(tx, field) + if isinstance(dict_vt, variables.ConstDictVariable): + return dict_vt.maybe_getitem_const(name_vt) + return None + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + # Allow skipping of empty hook dict guards on inbuilt nn modules + if name in ( + "_backward_hooks", + "_backward_pre_hooks", + "_forward_hooks_with_kwargs", + "_forward_hooks", + "_forward_pre_hooks_with_kwargs", + "_forward_pre_hooks", + ): + # For empty hooks, make an EMPTY_NN_MODULE_HOOKS_DICT. This allows us to control the installation of empty + # hooks guard via skip_nnmodule_hook_guards + if not tx.output.side_effects.has_pending_mutation_of_attr(self, name): + hooks_dict = getattr(self.value, name) + if isinstance(hooks_dict, dict) and len(hooks_dict) == 0: + if self.source: + hooks_source = AttrSource(self.source, name) + install_guard( + hooks_source.make_guard( + GuardBuilder.EMPTY_NN_MODULE_HOOKS_DICT + ) + ) + return variables.ConstDictVariable({}) + + # For non-empty hook dicts, one way is to just fallback to VariableTracker.build() and create a ConstDictVariable. + # However, ConstDictVariable guards on keys. This can cause recompiles when the same hook is installed for + # different nn module instances, because the key keeps changing (look more into RemovableHandle to understand why + # key changes - also related https://github.com/pytorch/pytorch/issues/125836). Here, we carefully craft a + # NNModuleHooksDictVariable (a subclass of ConstDictVariable) to avoid any guard on the keys. + if ( + self.source + and name + in ( + "_forward_pre_hooks_with_kwargs", + "_forward_pre_hooks", + "_forward_hooks_with_kwargs", + "_forward_hooks", + ) + and not tx.output.side_effects.has_pending_mutation_of_attr(self, name) + ): + hooks_dict = getattr(self.value, name) + hooks_dict_source = AttrSource(self.source, name) + install_guard(hooks_dict_source.make_guard(GuardBuilder.SEQUENCE_LENGTH)) + tx.output.guard_on_key_order.add(hooks_dict_source) + + def build_key_value( + i: int, k: Any, v: Any + ) -> tuple[VariableTracker, VariableTracker]: + # Make key sourceless to avoid any guard on it + key = VariableTracker.build(tx, k) + + # Instead of using dict[key] to access the value, use a dict[dict.keys()[index]] to access the + # value. This removes the reliance on the actual key value. + source_key = ConstDictKeySource(hooks_dict_source, i) + source_value = DictGetItemSource(hooks_dict_source, source_key) + value = LazyVariableTracker.create(v, source_value) + return key, value + + result = dict( + build_key_value(i, k, v) + for i, k, v in enumerate_items_with_dict_position(hooks_dict) + ) + + return variables.NNModuleHooksDictVariable( + result, type(hooks_dict), source=hooks_dict_source + ) + return super().var_getattr(tx, name) + + def manually_trace_nn_module_getattr( + self, tx: "InstructionTranslator", name: str + ) -> VariableTracker: + """ + Dynamo tracing of nn.Module __getattr__ can be expensive if the model + has deep submodule hierarchy. Since the __getattr__ is stable, we can + directly look into the underlying datastructures. This saves a lot of + compilation time. + """ + from .builder import SourcelessBuilder + + name_vt = SourcelessBuilder.create(tx, name) + out = self.getattr_helper(tx, "_parameters", name_vt) + if out is None: + out = self.getattr_helper(tx, "_modules", name_vt) + if out is None: + out = self.getattr_helper(tx, "_buffers", name_vt) + if out is None: + raise_observed_exception( + AttributeError, + tx, + args=[ + f"'{type(self.value).__name__}' object has no attribute '{name}'" + ], + ) + assert out is not None + return out + + +class UnspecializedBuiltinNNModuleVariable(UnspecializedNNModuleVariable): + """ + Differentiates between builtin nn modules (e.g. torch.nn.Linear) and user defined nn modules. + """ + + def _wrap_source(self, attr_source: Source) -> Source: + # vt is already wrapped with the UnspecializedBuiltinNNModuleSource + return attr_source + + +class FSDPManagedNNModuleVariable(UnspecializedNNModuleVariable): + """ + Tracing behavior: trace into submodules and treat them as Unspecialized, do not + register parameters to the top-level, treat them as function inputs. + + Guards behavior: if 'skip_fsdp_guards', many guards that would be installed + by a vanilla UnspecializedNNModuleVariable are simply dropped, on the basis + that a user wrapping their model in FSDP(model) is already opting into a + requirement to not modify internal model state, which would already break FSDP without + compilation. + """ + + def __init__(self, value: torch.nn.Module, **kwargs: Any) -> None: + source = kwargs.get("source") + assert source is not None, ( + "FSDPManagedNNModule depends on having an accurate source to control guarding." + ) + + super().__init__(value=value, **kwargs) + self.source = source + + def _wrap_source(self, attr_source: Any) -> Any: + if not isinstance( + attr_source, (FSDPNNModuleSource, UnspecializedNNModuleSource) + ): + if torch._dynamo.config.skip_fsdp_guards: + return FSDPNNModuleSource(attr_source) + else: + return UnspecializedNNModuleSource(attr_source) + return attr_source diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/object_protocol.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/object_protocol.py new file mode 100644 index 0000000000000000000000000000000000000000..217a7a796f582c65705b68a9adc8417ad409fe56 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/object_protocol.py @@ -0,0 +1,319 @@ +""" +Dynamo implementations of CPython's PyObject_* default slot algorithms. + +Analogous to CPython's Objects/object.c, this module holds the general +dispatch machinery that is independent of any specific type. +Per-type hook implementations (bool_impl, richcompare_impl, etc.) +live in their respective VT files. +""" + +from functools import lru_cache +from typing import TYPE_CHECKING + +from torch._C._dynamo import ( + get_type_slots, + has_slot, + PyMappingSlots, + PyNumberSlots, + PySequenceSlots, +) + +from .. import graph_break_hints +from ..exc import ( + handle_observed_exception, + ObservedTypeError, + raise_observed_exception, + raise_type_error, + unimplemented, +) +from ..utils import istype +from .base import NO_SUCH_SUBOBJ, VariableTracker +from .constant import ConstantVariable + + +if TYPE_CHECKING: + from ..symbolic_convert import InstructionTranslator + + +def vt_identity_compare( + left: VariableTracker, + right: VariableTracker, +) -> "VariableTracker | None": + """Try to determine Python identity (left is right) at trace time. + + Returns ConstantVariable(True/False) if determinable, else None. + Mirrors the logic in BuiltinVariable's handle_is handler. + """ + if left is right: + return ConstantVariable.create(True) + + left_val = left.get_real_python_backed_value() + right_val = right.get_real_python_backed_value() + left_known = left_val is not NO_SUCH_SUBOBJ + right_known = right_val is not NO_SUCH_SUBOBJ + + if left_known and right_known: + return ( + ConstantVariable.create(True) + if left_val is right_val + else ConstantVariable.create(False) + ) + + # One side has a concrete backing object, the other doesn't — they can't + # be the same object. + if left_known != right_known: + return ConstantVariable.create(False) + + # Mutable containers created during tracing: VT identity = Python identity. + from .dicts import ConstDictVariable + from .lists import ListVariable + from .sets import SetVariable + + if isinstance(left, (ConstDictVariable, ListVariable, SetVariable)): + return ConstantVariable.create(False) + + # Different Python types can never be the same object. + try: + if left.python_type() is not right.python_type(): + return ConstantVariable.create(False) + except NotImplementedError: + pass + + # Different exception types are never identical. + from .. import variables + + if ( + istype(left, variables.ExceptionVariable) + and istype(right, variables.ExceptionVariable) + and left.exc_type is not right.exc_type # type: ignore[attr-defined] + ): + return ConstantVariable.create(False) + + return None + + +@lru_cache(maxsize=256) +def _get_cached_slots(obj_type: type) -> tuple[int, int, int, int]: + """Get all type slots for a type (cached).""" + return get_type_slots(obj_type) + + +def type_implements_sq_length(obj_type: type) -> bool: + """Check whether obj_type implements __len__ as sequence protocol""" + seq_slots, _, _, _ = _get_cached_slots(obj_type) + return has_slot(seq_slots, PySequenceSlots.SQ_LENGTH) + + +def type_implements_mp_length(obj_type: type) -> bool: + """Check whether obj_type implements __len__ as mapping protocol""" + _, map_slots, _, _ = _get_cached_slots(obj_type) + return has_slot(map_slots, PyMappingSlots.MP_LENGTH) + + +def type_implements_nb_bool(obj_type: type) -> bool: + """Check whether obj_type implements the nb_bool slot (i.e. has __bool__ or __len__).""" + _, _, number_slots, _ = _get_cached_slots(obj_type) + return has_slot(number_slots, PyNumberSlots.NB_BOOL) + + +def type_implements_nb_int(obj_type: type) -> bool: + """Check whether obj_type implements the nb_int slot.""" + _, _, number_slots, _ = _get_cached_slots(obj_type) + return has_slot(number_slots, PyNumberSlots.NB_INT) + + +def type_implements_nb_index(obj_type: type) -> bool: + """Check whether obj_type implements the nb_index slot.""" + _, _, number_slots, _ = _get_cached_slots(obj_type) + return has_slot(number_slots, PyNumberSlots.NB_INDEX) + + +def type_implements_nb_float(obj_type: type) -> bool: + """Check whether obj_type implements the nb_float slot.""" + _, _, number_slots, _ = _get_cached_slots(obj_type) + return has_slot(number_slots, PyNumberSlots.NB_FLOAT) + + +def maybe_get_python_type(obj: VariableTracker) -> type: + try: + return obj.python_type() + except NotImplementedError: + unimplemented( + gb_type="Unsupported python_type() call", + context=f"{obj} does not implement python_type()", + explanation="This VariableTracker does not implement python_type(), " + "which is required for object protocol operations.", + hints=[ + *graph_break_hints.DYNAMO_BUG, + ], + ) + + +def vt_mapping_size( + tx: "InstructionTranslator", obj: "VariableTracker" +) -> "VariableTracker": + # ref: https://github.com/python/cpython/blob/v3.13.3/Objects/abstract.c#L2308-L2330 + T = maybe_get_python_type(obj) + if type_implements_mp_length(T): + return obj.mp_length(tx) + + if type_implements_sq_length(T): + raise_type_error(tx, f"{obj.python_type_name()} is not a mapping") + + raise_type_error(tx, f"object of type {obj.python_type_name()} has no len()") + + +def generic_len( + tx: "InstructionTranslator", obj: "VariableTracker" +) -> "VariableTracker": + # ref: https://github.com/python/cpython/blob/v3.13.3/Objects/abstract.c#L53-L69 + """ + Implements PyObject_Size/PyObject_Length semantics for VariableTracker objects. + Dispatches to sq_length (sequences) or mp_length (mappings) depending on the VT type. + """ + + T = maybe_get_python_type(obj) + if type_implements_sq_length(T): + return obj.sq_length(tx) + return vt_mapping_size(tx, obj) + + +def generic_bool(tx: "InstructionTranslator", obj: VariableTracker) -> VariableTracker: + """Mirrors PyObject_IsTrue. + + https://github.com/python/cpython/blob/c09ccd9c429/Objects/object.c#L2135-L2158 + + Resolution order: constants → nb_bool → mp_length/sq_length → truthy. + """ + from .constant import ConstantVariable + + if obj.is_python_constant(): + return ConstantVariable.create(bool(obj.as_python_constant())) + + obj_type = maybe_get_python_type(obj) + + if type_implements_nb_bool(obj_type): + result = obj.bool_impl(tx) + if result is not None: + return result + + try: + length = generic_len(tx, obj) + from .tensor import SymNodeVariable + + if isinstance(length, SymNodeVariable): + return SymNodeVariable.create(tx, length.as_proxy() > 0) + return ConstantVariable.create(length.as_python_constant() > 0) + except ObservedTypeError: + handle_observed_exception(tx) + + return ConstantVariable.create(True) + + +def vt_getitem( + tx: "InstructionTranslator", + obj: VariableTracker, + key: VariableTracker, +) -> VariableTracker: + """CPython's PyObject_GetItem — dispatch to the type's mp_subscript/sq_item. + + PyObject_GetItem: https://github.com/python/cpython/blob/62a6e898e01/Objects/abstract.c#L155-L206 + + CPython checks three branches in order: + 1. tp_as_mapping->mp_subscript (L161-166) + 2. tp_as_sequence->sq_item (L168-181) — only if key passes _PyIndex_Check + 3. PyType_Check(o) (L183-203) — type[int] → GenericAlias/__class_getitem__ + + Branch 1 is the common path (list, tuple, dict, range all have mp_subscript). + TODO(follow-up): use has_slot(map_slots, PyMappingSlots.MP_SUBSCRIPT) to gate + Branch 1 and has_slot(seq_slots, PySequenceSlots.SQ_ITEM) to gate Branch 2, + matching CPython's dispatch order. + TODO(follow-up): Branch 2 (sq_item) for C extension types that only have + tp_as_sequence (e.g. deque — Modules/_collectionsmodule.c:1888). + Branch 3 is handled by TypingVariable.mp_subscript_impl for typing module types + and by BuiltinVariable for builtin types like list[int]. + + Types that work via constant fold fallback (no dedicated mp_subscript_impl): + TODO(follow-up): str (unicode_subscript, Objects/unicodeobject.c:13809) + TODO(follow-up): bytes (bytes_subscript, Objects/bytesobject.c) + """ + return obj.mp_subscript_impl(tx, key) + + +def generic_int(tx: "InstructionTranslator", obj: VariableTracker) -> VariableTracker: + """Mirrors PyNumber_Long (int(x) dispatch). + + https://github.com/python/cpython/blob/v3.13.0/Objects/abstract.c#L1520-L1632 + + Resolution: nb_int → nb_index → str/bytes/bytearray parsing → TypeError. + """ + from .constant import ConstantVariable + + # Fast path for int (sub)class instances — mirrors PyLong_Check at the + # top of PyNumber_Long (abstract.c:1531). Avoids infinite recursion for + # int subclasses like IntEnum whose __int__ calls int() again. + if obj.is_python_constant() and isinstance(obj.as_python_constant(), int): + return ConstantVariable.create(int(obj.as_python_constant())) + + obj_type = maybe_get_python_type(obj) + + if type_implements_nb_int(obj_type): + return obj.nb_int_impl(tx) + + if type_implements_nb_index(obj_type): + return obj.nb_index_impl(tx) + + # String/bytes/bytearray parsing fallback. + # https://github.com/python/cpython/blob/v3.13.0/Objects/abstract.c#L1598-L1612 + if obj.is_python_constant() and isinstance( + obj.as_python_constant(), (str, bytes, bytearray) + ): + try: + return ConstantVariable.create(int(obj.as_python_constant())) + except ValueError as e: + raise_observed_exception(ValueError, tx, args=[str(e)]) + + raise_type_error( + tx, + f"int() argument must be a string, a bytes-like object " + f"or a real number, not '{obj.python_type_name()}'", + ) + + +def generic_float(tx: "InstructionTranslator", obj: VariableTracker) -> VariableTracker: + """Mirrors PyNumber_Float (float(x) dispatch). + + https://github.com/python/cpython/blob/v3.13.0/Objects/abstract.c#L1635-L1692 + + Resolution: nb_float → nb_index → str parsing → TypeError. + """ + from .constant import ConstantVariable + + # Fast path: if the value is already a float constant, return it directly. + # Mirrors PyFloat_CheckExact fast path at the top of PyNumber_Float + # (abstract.c:1641-1643). + if obj.is_python_constant() and isinstance(obj.as_python_constant(), float): + return ConstantVariable.create(float(obj.as_python_constant())) + + obj_type = maybe_get_python_type(obj) + + if type_implements_nb_float(obj_type): + return obj.nb_float_impl(tx) + + # https://github.com/python/cpython/blob/v3.13.0/Objects/abstract.c#L1674-L1685 + if type_implements_nb_index(obj_type): + return obj.nb_index_impl(tx) + + # PyFloat_FromString fallback — handles str and bytes. + # https://github.com/python/cpython/blob/v3.13.0/Objects/abstract.c#L1691 + if obj.is_python_constant() and isinstance(obj.as_python_constant(), (str, bytes)): + try: + return ConstantVariable.create(float(obj.as_python_constant())) + except ValueError as e: + raise_observed_exception(ValueError, tx, args=[str(e)]) + + raise_type_error( + tx, + f"float() argument must be a string or a real number, " + f"not '{obj.python_type_name()}'", + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/optimizer.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/optimizer.py new file mode 100644 index 0000000000000000000000000000000000000000..137f6f158f58080870f9e72e40f8b1edbad95373 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/optimizer.py @@ -0,0 +1,425 @@ +""" +This module implements variable tracking for PyTorch optimizers during Dynamo tracing. + +The OptimizerVariable class provides specialized handling for optimizer instances by: +- Optimizing the tracing of expensive optimizer initialization +- Managing optimizer state and parameter group tracking +- Handling tensor sources and guards for optimizer state tensors +- Supporting CUDA graph execution through static tensor address management +- Providing special handling for parameter gradients and optimizer state tensors + +Key features include: +- Efficient initialization tracing via _init_group optimization +- Automatic marking of optimizer state tensors as static for CUDA graphs +- Proper source tracking for parameter groups, gradients, and state tensors +- Guard installation for optimizer state structure +- Support for both CPU and GPU tensor handling +- Cleanup of static tensor references via finalizers + +The module integrates with Dynamo's broader tracing system while providing +optimizer-specific optimizations and safety guarantees. +""" + +import logging +import weakref +from collections.abc import Iterable +from typing import Any, TYPE_CHECKING + +import torch +from torch._dynamo.variables.tensor import TensorVariable +from torch._guards import Source +from torch._logging import getArtifactLogger +from torch.utils._pytree import tree_map_only + +from ..guards import GuardBuilder, install_guard +from ..source import ( + AttrSource, + ConstDictKeySource, + DictGetItemSource, + GetItemSource, + GlobalWeakRefSource, + GradSource, +) +from ..utils import GLOBAL_KEY_PREFIX +from .base import VariableTracker +from .constant import ConstantVariable +from .dicts import ConstDictVariable +from .hashable import HashableTracker +from .lists import ListVariable +from .misc import GetAttrVariable +from .user_defined import UserDefinedObjectVariable + + +if TYPE_CHECKING: + from torch._dynamo.symbolic_convert import InstructionTranslator + + +class ArgMappingException(Exception): + pass + + +class GuardInstallException(Exception): + pass + + +perf_hint_log = getArtifactLogger(__name__, "perf_hints") + + +def _is_static_for_cudagraphs(x: torch.Tensor) -> bool: + from torch._inductor.cudagraph_trees import get_manager + + if x.is_cuda: + manager = get_manager(x.device.index, False) + is_static_address = torch._dynamo.utils.get_static_address_type(x) is not None + if manager: + assert manager.current_node is not None + return ( + is_static_address + or manager.current_node._is_cuda_graph_recorded_tensor(x) + ) + else: + return is_static_address + else: + # Don't print a warning for non-cuda tensors + return True + + +class OptimizerVariable(UserDefinedObjectVariable): + _nonvar_fields = { + "grad_to_source", + "tensor_to_source", + "static_tensor_names", + *UserDefinedObjectVariable._nonvar_fields, + } + + def __init__( + self, + value: torch.optim.Optimizer, + grad_to_source: dict[Any, GradSource] | None = None, + static_tensor_names: set[str] | None = None, + tensor_to_source: dict[torch.Tensor, Source] | None = None, + **kwargs: Any, + ) -> None: + super().__init__(value, **kwargs) + # pyrefly: ignore [bad-override] + self.value: torch.optim.Optimizer = value + self.grad_to_source = grad_to_source or {} + self.tensor_to_source = tensor_to_source or {} + self.static_tensor_names = static_tensor_names or set() + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> "VariableTracker": + """This is an optimization to avoid tracing the very slow initialization of the optimizer""" + if name == "_init_group": + if not hasattr(self.value, "_init_group"): + # Fallback: if the optimizer does not have _init_group, trace normally + return super().call_method(tx, name, args, kwargs) + try: + self.graph_break_if_pending_mutation(tx) + self.move_step_if_cpu() + py_args, py_kwargs = self.get_python_args(*args, **kwargs) + ret_val = self.value._init_group(*py_args, **py_kwargs) + self.map_sources_and_install_guards(tx) + self.update_list_args(tx, args, kwargs, py_args, py_kwargs) + # stash a weak_ptr to optimizer to invalidate code + # if the optimizer object dies + mangled_name = f"__optimizer_{id(self.value)}" + tx.store_global_weakref_by_id(mangled_name, self.value) + self.create_finalizer(tx) + + # This is currently safe only because the only actual `ret_val`s returned + # by the `_init_group` of existing optimizers are properties that are invariant + # to the input tensors (e.g. dtype, layout). Changing these would trigger a + # recompilation and hence never result in the wrong specialization of `ret_val`. + return ConstantVariable.create(ret_val) + except (ArgMappingException, GuardInstallException) as _: + # trace normally if we can't map args or install guards correctly + pass + + return super().call_method(tx, name, args, kwargs) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + # Note: this allows us to intercept the call in call_method + # in the typical case, we return a UserMethodVariable + # which will directly inline + if name in ("_init_group"): + assert self.source + return GetAttrVariable( + self, + name, + py_type=type(getattr(self.value, name)), + source=AttrSource(self.source, name), + ) + + if name == "param_groups": + from ..decorators import mark_static_address + + for group in self.value.param_groups: + for p in group["params"]: + mark_static_address(p, guard=True) + + self._set_capturable(tx) + + return super().var_getattr(tx, name) + + def graph_break_if_pending_mutation(self, tx: "InstructionTranslator") -> None: + # If there are pending mutations on a parameter (due to using closure) + # then we need to graph break to allow the python version of the parameter + # to update, so that running _init_group will initialize the states with + # the correct values + for g in self.value.param_groups: + for p in g["params"]: + side_effects = tx.output.side_effects + variable = side_effects.id_to_variable.get(id(p), None) + if variable and side_effects.has_pending_mutation(variable): + from ..exc import unimplemented + + unimplemented( + gb_type="optimizer: pending mutation on parameter", + context=f"variable: {variable}, parameter: {p}", + explanation="Pending mutations on a parameter (e.g. due to using closure) require a graph break.", + hints=[], + ) + + def _set_capturable(self, tx: "InstructionTranslator") -> None: + from . import LazyVariableTracker + + # We only set capturable if params are on cuda + # and the state is not initialized + def safe_to_set_capturable(group: dict[str, Any]) -> bool: + all_uninitialized = True + all_gpu = True + + for p in group.get("params", []): + all_gpu &= p.is_cuda or p.is_xpu + all_uninitialized &= p not in self.value.state + + return "capturable" in group and all_uninitialized and all_gpu + + # track indices to not set so we don't need to + # in the variable tracker realize the whole state + # we handle guarding the state specially + for group in self.value.param_groups: + if safe_to_set_capturable(group): + group["capturable"] = True + + source = self.source and AttrSource(self.source, "param_groups") + param_groups_vt = LazyVariableTracker.realize_all( + VariableTracker.build(tx, self.value.param_groups, source) + ) + for param_group_vt in param_groups_vt.items: + key = HashableTracker(ConstantVariable.create("capturable")) + param_group_vt.items[key] = ConstantVariable.create(True) + + def get_python_args( + self, *args: Any, **kwargs: Any + ) -> tuple[list[Any], dict[str, Any]]: + """Get python values equivalent to the variable tracker args""" + + def map_arg(arg: Any) -> Any: + if isinstance(arg, VariableTracker) and arg.is_python_constant(): + return arg.as_python_constant() + elif isinstance(arg, ListVariable) and not arg.items: + # pyrefly: ignore [implicit-any] + return [] + elif ( + isinstance(arg, ConstDictVariable) + and isinstance(arg.source, GetItemSource) + and isinstance(arg.source.base, AttrSource) + and arg.source.base.member == "param_groups" + ): + return self.value.param_groups[arg.source.index] + + raise ArgMappingException + + new_args = [map_arg(arg) for arg in args] + new_kwargs = {k: map_arg(v) for k, v in kwargs.items()} + + return new_args, new_kwargs + + # If users load an old state dictionary, + # it's possible that step could be on the cpu + # if this is the case, move it to the GPU + # corresponding to the parameter + # in most cases this is a no-op because the state is empty + def move_step_if_cpu(self) -> None: + for p, state in self.value.state.items(): + if "step" in state and state["step"].is_cpu: + state["step"] = state["step"].to(p.device) + + def map_sources_and_install_guards(self, tx: "InstructionTranslator") -> None: + from ..decorators import mark_static_address + from .lazy import LazyVariableTracker + + self.grad_to_source = {} + self.tensor_to_source = {} + + def mark_static(x: Any) -> None: + mark_static_address(x, guard=True) + + tree_map_only(torch.Tensor, mark_static, self.value.state) + + # Recursively realize the variable trackers for optim.state and + # optim.param_groups, which recursively install the necessary guards. + params_groups_source = self.source and AttrSource(self.source, "param_groups") + param_groups_vt = LazyVariableTracker.realize_all( + VariableTracker.build(tx, self.value.param_groups, params_groups_source) + ) + + state_source = self.source and AttrSource(self.source, "state") + state_vt = VariableTracker.build(tx, self.value.state, state_source) + + # We need to realize the top level state dict to populate + # the guard locals + state_vt.realize() + assert state_source is not None + tx.output.guard_on_key_order.add(state_source) + + # Populate self.grad_to_source and self.tensor_to_source so that we can + # manually update_list_args + for group, group_vt in zip(self.value.param_groups, param_groups_vt.items): + # we assume here that all params within a param group + # are initialized similarly + if len(group["params"]) > 0: + for param in group["params"]: + if param.grad is not None: + key_index = None + for i, k in enumerate(self.value.state.keys()): + if k is param: + key_index = i + break + if key_index: + LazyVariableTracker.realize_all( + VariableTracker.build( + tx, + self.value.state[param], + DictGetItemSource( + state_source, + ConstDictKeySource(state_source, key_index), + ), + ) + ) + break + + params_vt = group_vt.getitem_const(tx, ConstantVariable.create("params")) + all_static = True + non_static_grads = [] + for p, p_vt in zip(group["params"], params_vt.unpack_var_sequence(tx)): + param_source = p_vt.source + self.tensor_to_source[p] = param_source + grad_source = GradSource( + param_source, + "grad", + ) + + if p.grad is not None: + self.grad_to_source[p.grad] = grad_source + if not _is_static_for_cudagraphs(p.grad): + all_static = False + non_static_grads.append(grad_source) + else: + install_guard(grad_source.make_guard(GuardBuilder.CONSTANT_MATCH)) + + # Note: to avoid spam logs only warn if perf hint artifact is enabled + # (NB: artifacts are only enabled at the debug or warning level) + if not all_static and perf_hint_log.isEnabledFor(logging.DEBUG): + non_static_grad_names = [src.name for src in non_static_grads] + perf_hint_log.warning( + ( + "Grad tensors %s will be copied during cudagraphs execution." + "If using cudagraphs and the grad tensor addresses will be the same across runs," + " use torch._dynamo.decorators.mark_static_address to elide this copy.", + ), + non_static_grad_names, + ) + + # We have to again iterate over the state dict to collect the + # tensor_to_source dict. This is used for the finalizer. + for idx, value in enumerate(self.value.state.values()): + p_state_source = DictGetItemSource( + state_source, ConstDictKeySource(state_source, idx) + ) + tx.output.guard_on_key_order.add(p_state_source) + for inner_idx, v in enumerate(value.values()): + if ( + isinstance(v, torch.Tensor) + and v not in self.grad_to_source + and v not in self.tensor_to_source + ): + self.tensor_to_source[v] = DictGetItemSource( + p_state_source, ConstDictKeySource(p_state_source, inner_idx) + ) + + def wrap_tensor( + self, tx: "InstructionTranslator", tensor_value: torch.Tensor + ) -> TensorVariable: + """Wrap state tensor in a TensorVariable""" + from ..decorators import mark_static_address + + # If we have a source for a tensor already use it, + # if we have not seen a tensor before, stash and use a + # global weak ref source, since it must be an optimizer tensor + # that we have missed + + if tensor_value in self.tensor_to_source: + # mark these tensors as static for cudagraphs + mark_static_address(tensor_value, guard=True) + source = self.tensor_to_source[tensor_value] + self.static_tensor_names.add(tx.output.module_key_name(source.name)) + elif tensor_value in self.grad_to_source: + source = self.grad_to_source[tensor_value] + else: + # mark these tensors as static for cudagraphs + mark_static_address(tensor_value, guard=True) + + global_name = tx.store_global_weakref_by_id(GLOBAL_KEY_PREFIX, tensor_value) + source = GlobalWeakRefSource(global_name) + self.static_tensor_names.add(tx.output.module_key_name(source.name)) + + return VariableTracker.build(tx, tensor_value, source) + + def update_list_args( + self, + tx: "InstructionTranslator", + args: Iterable[VariableTracker], + kwargs: Any, + py_args: Iterable[Any], + py_kwargs: Any, + ) -> None: + """Update the args and kwargs to the traced optimizer call""" + for arg, py_arg in zip(args, py_args): + if isinstance(arg, ListVariable): + assert isinstance(py_arg, list), ( + "py_arg should be a list in optimizer variable" + ) + for i, val in enumerate(py_arg): + tx.output.side_effects.mutation(arg) + if isinstance(val, torch.Tensor): + arg.items.append(self.wrap_tensor(tx, val)) + else: + source = arg.source and GetItemSource(arg.source, i) + arg.items.append(VariableTracker.build(tx, val, source)) + + def create_finalizer(self, tx: "InstructionTranslator") -> None: + names_to_delete = self.static_tensor_names + value = self.value + tc = tx.output.tracing_context + + def init_finalizer(gm: torch.fx.GraphModule) -> None: + def clear_static_tensor_refs() -> None: + for name in names_to_delete: + gm._buffers.pop(name, None) + gm._parameters.pop(name, None) + if tc.params_flat: + tc.params_flat.clear() + if tc.params_flat_unwrap_subclasses: + tc.params_flat_unwrap_subclasses.clear() + + weakref.finalize(value, clear_static_tensor_refs) + + tx.output.add_graph_finalizer(init_finalizer) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/script_object.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/script_object.py new file mode 100644 index 0000000000000000000000000000000000000000..e0d4f6cb45d11f7ddad1d36dfece767af6eaf5f1 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/script_object.py @@ -0,0 +1,532 @@ +""" +This module implements variable tracking for TorchScript objects during Dynamo tracing. + +The TorchScriptObjectVariable class provides specialized handling for TorchScript +objects with strong safety guarantees by: +- Enforcing method-call-only access to prevent unsafe attribute manipulation +- Converting graph breaks into hard errors via _raise_hard_error_if_graph_break +- Proper proxy and source tracking for TorchScript method calls +- Integration with higher-order operators for method call handling + +Key safety features: +- Strict validation that only method calls are allowed (no direct attribute access) +- Immediate error reporting for potentially unsafe operations +- Proper source tracking for debugging and guard installation +- Safe handling of TorchScript object method calls through torchbind + +The module ensures that TorchScript objects are handled safely during tracing +by limiting operations to known-safe patterns and failing fast for unsafe usage. +""" + +import enum +import functools +import inspect +import types +from collections.abc import Callable, Iterable, Sequence +from typing import Any, TYPE_CHECKING, TypeVar +from typing_extensions import ParamSpec + +import torch +import torch.utils._pytree as pytree +from torch._guards import Source +from torch._library.fake_class_registry import FakeScriptObject +from torch._library.opaque_object import ( + get_member_type, + is_opaque_reference_type, + is_opaque_type, + is_opaque_value_type, + MemberType, + should_hoist, +) +from torch.fx.proxy import Proxy + +from .. import graph_break_hints +from ..eval_frame import skip_code +from ..exc import ( + raise_observed_exception, + unimplemented, + UnsafeScriptObjectError, + Unsupported, +) +from ..source import AttrSource +from ..utils import proxy_args_kwargs +from .base import VariableTracker +from .constant import ConstantVariable +from .dicts import ConstDictVariable +from .lists import TupleVariable +from .misc import LambdaVariable +from .user_defined import UserDefinedObjectVariable, UserDefinedVariable + + +if TYPE_CHECKING: + from torch._dynamo.symbolic_convert import InstructionTranslator + +_P = ParamSpec("_P") +_T = TypeVar("_T") + + +def _raise_hard_error_if_graph_break( + reason: str, +) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]: + def deco(fn: Callable[_P, _T]) -> Callable[_P, _T]: + @functools.wraps(fn) + def graph_break_as_hard_error(*args: _P.args, **kwargs: _P.kwargs) -> _T: + try: + return fn(*args, **kwargs) + except Unsupported as e: + raise UnsafeScriptObjectError(e.msg) from e + + return graph_break_as_hard_error + + return deco + + +class OpaqueObjectClassVariable(UserDefinedVariable): + """ + A variable that represents an opaque object class (not instance). + Since UserDefinedClassVariable has some special handling for side effects, + we have a separate class here which will directly return the object when + __init__ is called. + """ + + def __init__(self, value: Any, **kwargs: Any) -> None: + assert not (isinstance(value, type) and issubclass(value, enum.Enum)), ( + f"Enum class {value} should use UserDefinedClassVariable, " + "not OpaqueObjectClassVariable" + ) + super().__init__(**kwargs) + self.value = value + + def as_python_constant(self) -> Any: + return self.value + + def is_python_constant(self) -> bool: + # prevents constant folding of attribute accesses on + # opaque classes. this ensures var_getattr is called, + # allowing for proper validation and error handling + return False + + def is_python_hashable(self) -> bool: + return is_opaque_value_type(self.value) # pyrefly: ignore[bad-argument-type] + + def get_python_hash(self) -> int: + return hash(self.value) + + def as_proxy(self) -> Any: + return self.value + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.value})" + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + obj = None + try: + obj = inspect.getattr_static(self.value, name) + except AttributeError: + unimplemented( + gb_type="Attribute not found on opaque class", + context=f"class={self.value}, attr={name}", + explanation=f"The attribute '{name}' does not exist on opaque class {self.value}.", + hints=[ + f"Ensure '{name}' is a valid attribute of {type(self.value)}.", + ], + ) + + if isinstance(obj, staticmethod): + obj = obj.__get__(self.value) + elif isinstance(obj, property): + obj = obj.__get__(None, self.value) # pyrefly: ignore[no-matching-overload] + elif hasattr(obj, "__get__"): + if not isinstance(type(obj).__dict__.get("__get__"), types.FunctionType): + # C-level descriptors are safe to resolve dynamically. + obj = getattr(self.value, name) + else: + type_name = type(obj).__name__ + unimplemented( + gb_type="Unsupported descriptor on opaque class", + context=f"class={self.value}, attr={name}, descriptor={type_name}", + explanation=f"The attribute '{name}' is a descriptor of type '{type_name}' which is not supported.", + hints=[ + "Only staticmethod, property, and pybind11_static_property are supported.", + "Consider accessing this attribute outside of the compiled region.", + ], + ) + + if ConstantVariable.is_literal(obj): + return VariableTracker.build(tx, obj) + + source = AttrSource(self.source, name) if self.source else None + return VariableTracker.build(tx, obj, source) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + # disallow creating reference-type opaque objects in the middle of the + # program + if is_opaque_reference_type(self.value): + # Skip __init__ to prevent dynamo from tracing it during resume + skip_code(self.value.__init__.__code__) + + unimplemented( + gb_type="An opaque object was created in the middle of the program.", + context=f"Opaque object type: {self.value}.", + explanation=( + "Opaque objects cannot be created inside the torch.compile region. " + "They must be created before entering the compiled function." + ), + hints=[ + "Please create the opaque object before calling torch.compile " + "and pass it in as an argument or as a global variable." + ], + ) + + var_args = TupleVariable(list(args)) + var_kwargs = ConstDictVariable( + {VariableTracker.build(tx, k): v for k, v in kwargs.items()} + ) + if should_hoist(self.value): + with tx.output.tracing_context.guards_context.skip_guard_install(): + constant_args = var_args.as_python_constant() + constant_kwargs = var_kwargs.as_python_constant() + else: + constant_args = var_args.as_python_constant() + constant_kwargs = var_kwargs.as_python_constant() + opaque_obj = self.value( # pyrefly: ignore[not-callable] + *constant_args, **constant_kwargs + ) + + # Capture sources from the VT args so subgraph reuse can apply + # source replacement to resolve new ctor arg values on stamp-out. + ctor_arg_sources = tuple(getattr(a, "source", None) for a in args) + + if is_opaque_value_type(type(opaque_obj)): + fake_script_obj = opaque_obj + else: + fake_script_obj = torch._library.fake_class_registry.maybe_to_fake_obj( + tx.output.fake_mode, opaque_obj + ) + + return TorchScriptObjectVariable.create( + opaque_obj, + fake_script_obj, + (constant_args, constant_kwargs), + ctor_arg_sources=ctor_arg_sources, + ) + + +class TorchScriptObjectVariable(UserDefinedObjectVariable): + _fake_script_object_cache: dict[int, "TorchScriptObjectVariable"] = {} + + @classmethod + def is_matching_cls(cls, user_cls: type) -> bool: + return ( + issubclass(user_cls, torch.ScriptObject) + or is_opaque_type(user_cls) + or issubclass(user_cls, FakeScriptObject) + ) + + @staticmethod + def create( + proxy: Proxy, + value: Any, + ctor_args_kwargs: Any = None, + ctor_arg_sources: tuple[Source | None, ...] | None = None, + **options: Any, + ) -> "TorchScriptObjectVariable": + assert not isinstance(value, enum.Enum), ( + f"Enum {type(value)} should use UserDefinedObjectVariable, not TorchScriptObjectVariable" + ) + out = TorchScriptObjectVariable( + proxy, value, ctor_args_kwargs, ctor_arg_sources=ctor_arg_sources, **options + ) + if isinstance(proxy, torch.fx.Proxy) and proxy.node.op != "placeholder": + from torch._dynamo.symbolic_convert import InstructionTranslator + + tx = InstructionTranslator.current_tx() + tx.output.current_tracer.record_proxyable_vt(out) + return out + + def __init__( + self, + proxy: Proxy, + value: Any, + ctor_args_kwargs: Any = None, + source: Source | None = None, + ctor_arg_sources: tuple[Source | None, ...] | None = None, + **kwargs: Any, + ) -> None: + super().__init__(value, **kwargs) + self.proxy = proxy + if isinstance(self.proxy, torch.fx.Proxy): + self.proxy.node.meta["example_value"] = value + self.source = source + # If the OpaqueObject is sourceless, then this is + # the constant (args, kwargs) that Dynamo used to construct it. + self.ctor_args_kwargs = ctor_args_kwargs + # Sources of the constructor args, used by subgraph reuse to + # resolve new values via source replacement on stamp-out. + self.ctor_arg_sources = ctor_arg_sources + + def as_proxy(self) -> Proxy: + if not isinstance(self.proxy, torch.fx.Proxy): + # If we have a hoisted value type, then lazily lift it to be a graph + # input when as_proxy() is called. + assert is_opaque_value_type(type(self.proxy)) + if should_hoist(type(self.proxy)): + from torch._dynamo.symbolic_convert import InstructionTranslator + + tx = InstructionTranslator.current_tx() + # if any kwargs (synthetic_graph_input doesn't support them yet) + # not a graph break because hard error more explicit here + # (and opaque objects are really just used for compile) + if self.ctor_args_kwargs[1]: + raise RuntimeError( + "NYI: hoisted opaque objects that accept kwargs, please pass as args" + ) + hoisted_vt = tx.output.synthetic_graph_input( + type(self.proxy), + self.ctor_args_kwargs[0], + ctor_arg_sources=self.ctor_arg_sources, + ) + self.proxy = hoisted_vt.as_proxy() + + return self.proxy + + def __str__(self) -> str: + value = ( + self.value.real_obj + if isinstance(self.value, FakeScriptObject) + else self.value + ) + return f"{self.__class__.__name__}({value})" + + __repr__ = __str__ + + @_raise_hard_error_if_graph_break( + "Dynamo cannot safely trace script object due to graph break." + ) + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + from torch._higher_order_ops.torchbind import call_torchbind + + from .higher_order_ops import TorchHigherOrderOperatorVariable + + real_obj = self.as_python_constant() + real_obj_type = type(real_obj) + if is_opaque_type(real_obj_type): + member_type = get_member_type(real_obj_type, name) + + if member_type == MemberType.USE_REAL: + value = getattr(real_obj, name) + if inspect.ismethod(value) or isinstance( + value, types.MethodWrapperType + ): + return LambdaVariable( + lambda *args, **kwargs: self.call_method(tx, name, args, kwargs) + ) + else: + return super().var_getattr(tx, name) + + elif member_type == MemberType.INLINED: + value = getattr(real_obj, name) + if ( + inspect.ismethod(value) + or isinstance(value, types.MethodWrapperType) + ) and self.source is None: + # When we don't have a source, fall back to call_method + # which creates a proxy node. + return LambdaVariable( + lambda *args, **kwargs: self.call_method(tx, name, args, kwargs) + ) + return super().var_getattr(tx, name) + + elif is_opaque_value_type(real_obj_type): + return super().var_getattr(tx, name) + + elif name in ("__bool__", "__len__") and not hasattr(real_obj, name): + # Special case: __bool__ and __len__ are used for truthiness checks. + # If they're not registered and the real object doesn't have them, + # raise ObservedAttributeError so the caller can fall back to + # treating the object as truthy (Python default behavior + raise_observed_exception(AttributeError, tx) + + else: + unimplemented( + gb_type="Attempted to access unregistered member on an OpaqueObject", + context=f"value={real_obj}, attr={name}", + explanation=f"Member '{name}' is not registered for this opaque object type.", + hints=[ + f"Register '{name}' with a MemberType in register_opaque_type(members=...).", + ], + ) + + method = getattr(self.value, name, None) + if method is None: + unimplemented( + gb_type="FakeScriptObject missing method implementation", + context=f"value={self.value}, method={name}", + explanation=f"TorchScript object {self.value} doesn't define the method {name}.", + hints=[ + f"Ensure the method {name} is implemented in {self.value}.", + *graph_break_hints.USER_ERROR, + ], + ) + + if not callable(method): + unimplemented( + gb_type="Attempted to access non-callable attribute of TorchScript object", + context=f"value={self.value}, method={name}", + explanation="Attribute accesses of TorchScript objects to non-callable attributes are not supported.", + hints=[ + "Use method calls instead of attribute access.", + ], + ) + + assert self.source is not None + return TorchHigherOrderOperatorVariable.make( + call_torchbind, + source=AttrSource(self.source, name), + script_obj_var=self, + method_name=name, + ) + + def mp_subscript_impl( + self, + tx: "InstructionTranslator", + key: "VariableTracker", + ) -> "VariableTracker": + # Call call_method directly on this class to avoid the __getitem__ → + # mp_subscript_impl loop in VariableTracker.call_method. + return TorchScriptObjectVariable.call_method(self, tx, "__getitem__", [key], {}) + + # We only support method calls on script objects. Interpreting the bytecodes + # should go through var_getattr then call_function instead of call_method. + + # However, it's possible for call_method to be used directly e.g. for __setattr__. + @_raise_hard_error_if_graph_break( + "Dynamo cannot safely trace script object due to graph break." + ) + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: Iterable[Any], + kwargs: dict[str, Any], + ) -> VariableTracker: + from .builder import wrap_fx_proxy + + real_obj = self.as_python_constant() + real_obj_type = type(real_obj) + if is_opaque_type(real_obj_type): + member_type = get_member_type(real_obj_type, name) + + if member_type == MemberType.USE_REAL: + if ( + inspect.getattr_static(real_obj_type, "__getattr__", None) + is not None + ): + unimplemented( + gb_type="Opaque object with custom __getattr__ not supported", + context=f"{real_obj_type.__name__} with custom __getattr__", + explanation="Dynamo does not support opaque objects types with custom __getattr__ methods", + hints=[], + ) + + args_const = [x.as_python_constant() for x in args] + kwargs_const = {k: v.as_python_constant() for k, v in kwargs.items()} + + method = getattr(real_obj, name) + + if name == "__setattr__": + method(*args_const, **kwargs_const) + return real_obj # pyrefly: ignore[bad-return] + + constant_val = method(*args_const, **kwargs_const) + + if any( + is_opaque_reference_type(type(r)) + for r in pytree.tree_leaves(constant_val) + ): + unimplemented( + gb_type="Opaque object member with method-type USE_REAL returned a reference-type opaque object.", + context=f"Opaque object type: {real_obj_type}. Method name: '{name}'", + explanation=( + "To properly guard reference-type opaque objects, " + "we must lift them as inputs to the graph. In order " + "to do this, they must all have a source, meaning they " + "come from a global value or are an attribute of an input." + ), + hints=[ + f"Register member '{name}' with MemberType.INLINED in " + f"register_opaque_type({real_obj_type}, members=...).", + ], + ) + + return VariableTracker.build(tx, constant_val) + + elif member_type == MemberType.INLINED or is_opaque_value_type( + real_obj_type + ): + proxy_args, proxy_kwargs = proxy_args_kwargs(args, kwargs) + + proxy = tx.output.create_proxy( + "call_method", + name, + args=(self.proxy, *proxy_args), + kwargs=proxy_kwargs, + ) + + return wrap_fx_proxy(tx=tx, proxy=proxy) + + else: + unimplemented( + gb_type="Attempted to access unregistered member on an OpaqueObject", + context=f"value={real_obj}, attr={name}", + explanation=f"Member '{name}' is not registered for this opaque object type.", + hints=[ + f"Register '{name}' with a MemberType in register_opaque_type(members=...).", + ], + ) + + unimplemented( + gb_type="Weird method call on TorchScript object", + context=f"value={self.value}, method={name}", + explanation=( + f"This particular method call ({name}) is not supported (e.g. calling `__setattr__`). " + "Most method calls to TorchScript objects should be supported." + ), + hints=[ + "Avoid calling this method.", + ], + ) + + def as_python_constant(self) -> Any: + if isinstance(self.value, FakeScriptObject): + return self.value.real_obj + elif is_opaque_value_type(type(self.value)): + return self.value + elif isinstance(self.value, torch.ScriptObject): + return self.value + return super().as_python_constant() + + def is_python_hashable(self) -> bool: + try: + self.get_python_hash() + return True + except TypeError: + return False + + def get_python_hash(self) -> int: + real_obj = self.as_python_constant() + return hash(real_obj) + + def is_python_equal(self, other: object) -> bool: + assert isinstance(other, VariableTracker) + real_self = self.as_python_constant() + real_other = other.as_python_constant() + return real_self == real_other + + def get_real_value(self) -> Any: + return self.as_python_constant() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/sdpa.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/sdpa.py new file mode 100644 index 0000000000000000000000000000000000000000..48de0f9ab4e3cbccb38de3b0ae2f0f26689dcb76 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/sdpa.py @@ -0,0 +1,98 @@ +from collections.abc import Sequence +from inspect import getattr_static +from typing import Any, TYPE_CHECKING, TypeGuard + +from torch._guards import Source +from torch.backends.cuda import SDPAParams +from torch.fx.proxy import Proxy + +from ..bytecode_transformation import create_call_function +from ..exc import unimplemented +from ..source import AttrSource +from .base import VariableTracker + + +if TYPE_CHECKING: + from torch._dynamo.codegen import PyCodegen + from torch._dynamo.symbolic_convert import InstructionTranslator + +PARAM_NAMES = [ + "query", + "key", + "value", + "attn_mask", + "dropout", + "is_causal", + "enable_gqa", +] + + +class SDPAParamsVariable(VariableTracker): + """Represents the c++ params struct for scaled dot product attention. + This is a read-only container.""" + + @staticmethod + def create( + tx: "InstructionTranslator", value: Any, source: Source + ) -> VariableTracker: + from .torch import TorchInGraphFunctionVariable + + params = [ + VariableTracker.build(tx, getattr(value, p), AttrSource(source, p)) + for p in PARAM_NAMES + ] + return TorchInGraphFunctionVariable(SDPAParams).call_function(tx, params, {}) + + def __init__( + self, proxy: Proxy, param_vars: Sequence[VariableTracker], **kwargs: Any + ) -> None: + self.proxy = proxy + self.param_vars = param_vars + super().__init__(**kwargs) + + def python_type(self) -> type: + return SDPAParams + + def reconstruct(self, codegen: "PyCodegen") -> None: + assert self.source is None + assert self.param_vars is not None + codegen.add_push_null( + lambda: codegen.load_import_from("torch._C", "_SDPAParams") + ) + codegen.foreach(self.param_vars) + codegen.extend_output(create_call_function(len(self.param_vars), False)) + + def as_proxy(self) -> Proxy: + return self.proxy + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + import torch._C + + from .builder import wrap_fx_proxy + from .misc import GetAttrVariable + + try: + getattr_static(torch._C._SDPAParams, name) + except AttributeError: + import torch._dynamo.graph_break_hints as graph_break_hints + + unimplemented( + gb_type="unsupported torch._C._SDPAParams attribute", + context=f"name: {name}", + explanation=f"Unable to fetch attribute {name} from torch._C._SDPAParams.", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + + proxy = GetAttrVariable.create_getattr_proxy(self.as_proxy(), name) + if self.source is not None: + return wrap_fx_proxy( + tx=tx, proxy=proxy, source=AttrSource(self.source, name) + ) + else: + return wrap_fx_proxy(tx=tx, proxy=proxy) + + @staticmethod + def is_sdpa_params(value: Any) -> TypeGuard["SDPAParams"]: + return value is SDPAParams diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/sets.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/sets.py new file mode 100644 index 0000000000000000000000000000000000000000..acbcd62333e324cc9cf65f21ddd16693a86fd9a2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/sets.py @@ -0,0 +1,797 @@ +""" +Set-related variable tracking classes for PyTorch Dynamo. + +This module implements variable tracking for different types of set-like objects: +- Regular Python sets (set) +- Frozen sets (frozenset) +- Ordered sets (torch.utils._ordered_set.OrderedSet) +- Dictionary key sets (dict_keys views used as sets) + +These classes are responsible for tracking set operations during graph compilation, +maintaining proper guards for set mutations and element existence checks. + +The implementation uses a special HashableTracker wrapper to handle set elements +while preserving proper aliasing semantics. Sets are modeled internally as +dictionaries with None values. +""" + +import functools +import operator +from collections.abc import Iterable, Sequence +from typing import Any, Literal, TYPE_CHECKING + +from torch.utils._ordered_set import OrderedSet + +from .. import polyfills, variables +from ..bytecode_transformation import create_call_function, create_instruction +from ..exc import raise_observed_exception +from ..guards import GuardBuilder, install_guard +from ..source import AttrSource, is_constant_source, is_from_local_source +from ..utils import cmp_name_to_op_mapping, istype, raise_args_mismatch +from .base import ValueMutationNew, VariableTracker +from .constant import ConstantVariable +from .hashable import HashableTracker, is_hashable, raise_unhashable + + +if TYPE_CHECKING: + from torch._dynamo.codegen import PyCodegen + from torch._dynamo.symbolic_convert import InstructionTranslator + from torch._dynamo.variables.builtin import BuiltinVariable + + +# [Adding a new supported class within the keys of SetVariable] +# see steps outlined for ConstDictVariable + + +class SetVariable(VariableTracker): + """Represents a Python set during symbolic execution.""" + + # PySet_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/setobject.c#L2436 + _cpython_type = set + + CONTAINS_GUARD = GuardBuilder.SET_CONTAINS + NOT_CONTAINS_GUARD = GuardBuilder.SET_NOT_CONTAINS + + def __init__( + self, + items: Iterable[VariableTracker | HashableTracker], + **kwargs: Any, + ) -> None: + # .clone() passes these arguments in kwargs but they're recreated below + if "original_items" in kwargs: + kwargs.pop("original_items") + if "should_reconstruct_all" in kwargs: + kwargs.pop("should_reconstruct_all") + + super().__init__(**kwargs) + + # Items can be either VariableTrackers or HashableTrackers (from set ops). + # For VariableTrackers, realize them to ensure aliasing guards are installed + # when the same object appears multiple times. + hashable_items = [] + for item in items: + if isinstance(item, HashableTracker): + # Already a HashableTracker from a set operation + hashable_items.append(item) + else: + # VariableTracker - realize to install guards, then wrap + # pyrefly: ignore [bad-argument-type] + hashable_items.append(HashableTracker(item.realize())) + self.items = dict.fromkeys(hashable_items, SetVariable._default_value()) + self.should_reconstruct_all = ( + not is_from_local_source(self.source) if self.source else True + ) + self.original_items = dict.fromkeys( + hashable_items, SetVariable._default_value() + ) + + def debug_repr(self) -> str: + if not self.items: + return "set()" + else: + items: list[str] = [] + for v in self.items: + vt = v.vt if isinstance(v, HashableTracker) else v + val_str = repr(vt.value) if hasattr(vt, "value") else vt.debug_repr() + items.append(val_str) + return "{" + ",".join(items) + "}" + + @property + def set_items(self) -> set["HashableTracker"]: + return set(self.items.keys()) + + @staticmethod + def _default_value() -> VariableTracker: + # Variable to fill in the keys of the dictionary + return ConstantVariable.create(None) + + def as_proxy(self) -> Any: + return {k.vt.as_proxy() for k in self.set_items} + + def python_type(self) -> type: + return set + + def as_python_constant(self) -> Any: + return {k.vt.as_python_constant() for k in self.set_items} + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.foreach([x.vt for x in self.set_items]) + codegen.append_output(create_instruction("BUILD_SET", arg=len(self.set_items))) + + def __contains__(self, vt: VariableTracker) -> bool: + assert isinstance(vt, VariableTracker) + if not is_hashable(vt): + return False + key = HashableTracker(vt) + return key in self.items and not isinstance( + self.items[key], variables.DeletedVariable + ) + + def len(self) -> int: + return sum( + not isinstance(x, variables.DeletedVariable) for x in self.items.values() + ) + + def has_new_items(self) -> bool: + return self.should_reconstruct_all or any( + self.is_new_item(self.original_items.get(key.vt), value) + for key, value in self.items.items() + ) + + def is_new_item( + self, value: VariableTracker | None, other: VariableTracker + ) -> bool: + if value and value.is_realized() and other.is_realized(): + return id(value.realize()) != id(other.realize()) + return id(value) != id(other) + + def unpack_var_sequence(self, tx: "InstructionTranslator") -> list[VariableTracker]: + return [x.vt for x in self.items] + + def clone(self, **kwargs: Any) -> VariableTracker: + return super().clone(**kwargs) + + def is_python_hashable(self) -> bool: + return False + + def var_getattr(self, tx: "InstructionTranslator", name: str): + if name == "__class__": + return VariableTracker.build(tx, self.python_type()) + return super().var_getattr(tx, name) + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> ConstantVariable: + return VariableTracker.build(tx, hasattr(set, name)) + + def install_set_contains_guard( + self, tx: "InstructionTranslator", args: list[VariableTracker] + ) -> None: + if not self.source: + return + + if tx.output.side_effects.is_modified(self): + return + + contains = args[0] in self + if args[0].source is None and args[0].is_python_constant(): + guard_fn = ( + type(self).CONTAINS_GUARD if contains else type(self).NOT_CONTAINS_GUARD + ) + install_guard( + self.make_guard( + functools.partial( + guard_fn, + key=args[0].as_python_constant(), + ) + ) + ) + + def _fast_set_method( + self, + tx: "InstructionTranslator", + fn: Any, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + try: + res = fn( + *[x.as_python_constant() for x in [self, *args]], + **{k: v.as_python_constant() for k, v in kwargs.items()}, + ) + except Exception as exc: + raise_observed_exception(type(exc), tx, args=list(exc.args)) + return VariableTracker.build(tx, res) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from ..utils import check_constant_args + from .builder import SourcelessBuilder + + if ( + name + in ( + "isdisjoint", + "union", + "intersection", + "difference", + "symmetric_difference", + ) + and check_constant_args(args, kwargs) + and self.python_type() is set + ): + py_type = self.python_type() + return self._fast_set_method(tx, getattr(py_type, name), args, kwargs) + + # Lazy imports to avoid circular dependencies + from .dicts import DictItemsVariable, DictKeysVariable + + if name == "__init__": + temp_set_vt = SourcelessBuilder.create(tx, set).call_set( + tx, *args, **kwargs + ) + tx.output.side_effects.mutation(self) + self.items.clear() + self.items.update(temp_set_vt.items) # type: ignore[attr-defined] + return ConstantVariable.create(None) + elif name == "add": + if kwargs or len(args) != 1: + raise_args_mismatch( + tx, + name, + "1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + # Convert add to __setitem__ with None value + if not is_hashable(args[0]): + raise_unhashable(args[0], tx) + tx.output.side_effects.mutation(self) + self.items[HashableTracker(args[0])] = SetVariable._default_value() + return ConstantVariable.create(None) + elif name == "pop": + if kwargs or args: + raise_args_mismatch( + tx, + name, + "0 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + # Choose an item at random and pop it + try: + result: VariableTracker = self.set_items.pop().vt # type: ignore[assignment] + except KeyError as e: + raise_observed_exception(KeyError, tx, args=list(e.args)) + self.should_reconstruct_all = True + tx.output.side_effects.mutation(self) + self.items.pop(HashableTracker(result)) + return result + elif name == "isdisjoint": + if kwargs or len(args) != 1: + raise_args_mismatch( + tx, + name, + "1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + return SourcelessBuilder.create(tx, polyfills.set_isdisjoint).call_function( + tx, [self, args[0]], {} + ) + elif name == "intersection": + if kwargs: + raise_args_mismatch(tx, name, "0 kwargs", f"{len(kwargs)} kwargs") + return SourcelessBuilder.create( + tx, polyfills.set_intersection + ).call_function( + tx, + [self, *args], + {"cls": self.python_type_var()}, + ) + elif name == "intersection_update": + if kwargs: + raise_args_mismatch(tx, name, "0 kwargs", f"{len(kwargs)} kwargs") + return SourcelessBuilder.create( + tx, polyfills.set_intersection_update + ).call_function(tx, [self, *args], {}) + elif name == "union": + if kwargs: + raise_args_mismatch(tx, name, "0 kwargs", f"{len(kwargs)} kwargs") + return SourcelessBuilder.create(tx, polyfills.set_union).call_function( + tx, + [self, *args], + {"cls": self.python_type_var()}, + ) + elif name == "difference": + if kwargs: + raise_args_mismatch( + tx, name, f"Expect: 0 kwargs, Actual: {len(kwargs)} kwargs" + ) + return SourcelessBuilder.create(tx, polyfills.set_difference).call_function( + tx, + [self, *args], + {"cls": self.python_type_var()}, + ) + elif name == "difference_update": + if kwargs: + raise_args_mismatch(tx, name, "0 kwargs", f"{len(kwargs)} kwargs") + return SourcelessBuilder.create( + tx, polyfills.set_difference_update + ).call_function(tx, [self, *args], {}) + elif name == "symmetric_difference": + if kwargs or len(args) != 1: + raise_args_mismatch( + tx, + name, + "1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + return SourcelessBuilder.create( + tx, polyfills.set_symmetric_difference + ).call_function( + tx, + [self, *args], + {"cls": self.python_type_var()}, + ) + elif name == "symmetric_difference_update": + if kwargs or len(args) != 1: + raise_args_mismatch( + tx, + name, + "1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + return SourcelessBuilder.create( + tx, polyfills.set_symmetric_difference_update + ).call_function(tx, [self, *args], {}) + elif name == "update" and self.is_mutable(): + if kwargs: + raise_args_mismatch(tx, name, "0 kwargs", f"{len(kwargs)} kwargs") + return SourcelessBuilder.create(tx, polyfills.set_update).call_function( + tx, [self, *args], {} + ) + elif name == "remove": + if kwargs or len(args) != 1: + raise_args_mismatch( + tx, + name, + "1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + if args[0] not in self: + raise_observed_exception(KeyError, tx, args=args) + self.should_reconstruct_all = True + tx.output.side_effects.mutation(self) + self.items.pop(HashableTracker(args[0])) + return ConstantVariable.create(None) + elif name == "discard": + if kwargs or len(args) != 1: + raise_args_mismatch( + tx, + name, + "1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + if args[0] in self: + self.should_reconstruct_all = True + tx.output.side_effects.mutation(self) + self.items.pop(HashableTracker(args[0])) + return ConstantVariable.create(None) + elif name in ("issubset", "issuperset"): + if len(args) != 1: + raise_args_mismatch(tx, name, "1 args", f"{len(args)} args") + + op = { + "issubset": operator.le, + "issuperset": operator.ge, + } + other = args[0].realize() + if not istype(other, SetVariable): + other = SourcelessBuilder.create(tx, set).call_function(tx, [other], {}) + return SourcelessBuilder.create(tx, op.get(name)).call_function( + tx, [self, other], {} + ) + elif name in ("__and__", "__or__", "__xor__", "__sub__"): + m = { + "__and__": "intersection", + "__or__": "union", + "__xor__": "symmetric_difference", + "__sub__": "difference", + }.get(name) + if not isinstance( + args[0], + ( + SetVariable, + variables.UserDefinedSetVariable, + DictItemsVariable, + DictKeysVariable, + ), + ): + raise_observed_exception( + TypeError, + tx, + args=[ + f"unsupported operand type(s) for {name}: '{self.python_type_name()}' and '{args[0].python_type_name()}'" + ], + ) + assert m is not None + return self.call_method(tx, m, args, kwargs) + elif name in ("__rand__", "__ror__", "__rxor__", "__rsub__"): + m = { + "__rand__": "__and__", + "__ror__": "__or__", + "__rxor__": "__xor__", + "__rsub__": "__sub__", + }.get(name) + if not isinstance( + args[0], + ( + SetVariable, + variables.UserDefinedSetVariable, + DictItemsVariable, + DictKeysVariable, + ), + ): + raise_observed_exception( + TypeError, + tx, + args=[ + f"unsupported operand type(s) for {name}: '{args[0].python_type_name()}' and '{self.python_type_name()}'" + ], + ) + assert m is not None + return args[0].call_method(tx, m, [self], kwargs) + elif name in ("__iand__", "__ior__", "__ixor__", "__isub__"): + if not isinstance( + args[0], + ( + SetVariable, + variables.UserDefinedSetVariable, + DictItemsVariable, + DictKeysVariable, + ), + ): + raise_observed_exception( + TypeError, + tx, + args=[ + f"unsupported operand type(s) for {name}: '{self.python_type_name()}' and '{args[0].python_type_name()}'" + ], + ) + m = { + "__iand__": "intersection_update", + "__ior__": "update", + "__ixor__": "symmetric_difference_update", + "__isub__": "difference_update", + }.get(name) + assert m is not None + self.call_method(tx, m, args, kwargs) + return self + elif name == "__eq__": + if not isinstance( + args[0], + ( + SetVariable, + variables.UserDefinedSetVariable, + DictItemsVariable, + DictKeysVariable, + ), + ): + return ConstantVariable.create(False) + r = self.call_method(tx, "symmetric_difference", args, kwargs) + return VariableTracker.build(tx, len(r.set_items) == 0) # type: ignore[attr-defined] + elif name == "__ne__": + eq_result = self.call_method(tx, "__eq__", args, kwargs) + return VariableTracker.build(tx, not eq_result.value) # type: ignore[attr-defined] + elif name in cmp_name_to_op_mapping: + if not isinstance( + args[0], + ( + SetVariable, + variables.UserDefinedSetVariable, + DictItemsVariable, + DictKeysVariable, + ), + ): + return VariableTracker.build(tx, NotImplemented) + return VariableTracker.build( + tx, + cmp_name_to_op_mapping[name](self.set_items, args[0].set_items), # type: ignore[attr-defined] + ) + elif name == "__contains__": + if not len(args): + raise_args_mismatch( + tx, + name, + "more than 1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + if not (args and is_hashable(args[0])): + raise_unhashable(args[0], tx) + self.install_set_contains_guard(tx, args) + contains = args[0] in self + return VariableTracker.build(tx, contains) + elif name == "__len__": + if args or kwargs: + raise_args_mismatch( + tx, + name, + "0 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + return VariableTracker.build(tx, len(self.items)) + elif name == "copy": + if args or kwargs: + raise_args_mismatch( + tx, + name, + "0 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + return self.clone( + items=self.items.copy(), mutation_type=ValueMutationNew(), source=None + ) + elif name == "clear": + if args or kwargs: + raise_args_mismatch( + tx, + name, + "0 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + self.should_reconstruct_all = True + tx.output.side_effects.mutation(self) + self.items.clear() + return ConstantVariable.create(None) + elif name == "__iter__": + from .lists import ListIteratorVariable + + if self.source and not is_constant_source(self.source): + tx.output.guard_on_key_order.add(self.source) + return ListIteratorVariable( + self.unpack_var_sequence(tx), mutation_type=ValueMutationNew() + ) + return super().call_method(tx, name, args, kwargs) + + def python_type_var(self) -> "BuiltinVariable": + return variables.BuiltinVariable(set) + + def getitem_const( + self, tx: "InstructionTranslator", arg: VariableTracker + ) -> VariableTracker: + raise RuntimeError("Illegal to getitem on a set") + + def sq_length(self, tx: "InstructionTranslator") -> VariableTracker: + return VariableTracker.build(tx, len(self.set_items)) + + +class OrderedSetClassVariable(VariableTracker): + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + def as_python_constant(self) -> type[OrderedSet[Any]]: + return OrderedSet + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + if name == "__new__": + from .misc import GetAttrVariable + + if self.source: + attr_source = AttrSource(self.source, name) + else: + attr_source = None + return GetAttrVariable( + self, name, py_type=type(getattr(OrderedSet, name)), source=attr_source + ) + else: + return super().var_getattr(tx, name) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .builtin import set_methods + + if name == "__new__": + if len(args) != 2 or kwargs: + raise_args_mismatch( + tx, + name, + "OrderedSet.__new__ only accepts one arg" + f"{len(args)} args and {len(kwargs)} kwargs", + ) + + return variables.OrderedSetVariable([], mutation_type=ValueMutationNew()) + + resolved_fn = getattr(set, name) + if resolved_fn in set_methods and isinstance(args[0], variables.SetVariable): + return args[0].call_method(tx, name, args[1:], kwargs) + + return super().call_method(tx, name, args, kwargs) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> "OrderedSetVariable": + if len(args) > 1 or kwargs: + raise_args_mismatch( + tx, + "OrderedSet", + "OrderedSet only accepts one arg" + f"{len(args)} args and {len(kwargs)} kwargs", + ) + + if len(args) == 0: + # pyrefly: ignore [implicit-any] + items = [] + else: + items = args[0].force_unpack_var_sequence(tx) + return variables.OrderedSetVariable(items, mutation_type=ValueMutationNew()) + + +class OrderedSetVariable(SetVariable): + def debug_repr(self) -> str: + if not self.items: + return "OrderedSet([])" + else: + items: list[str] = [] + for k in self.items: + key_str = ( + repr(k.vt.value) if hasattr(k.vt, "value") else k.vt.debug_repr() + ) + items.append(key_str) + return "OrderedSet([" + ",".join(items) + "])" + + def as_python_constant(self) -> OrderedSet[Any]: + return OrderedSet([k.vt.as_python_constant() for k in self.set_items]) + + def python_type(self) -> type[OrderedSet[Any]]: + return OrderedSet + + # pyrefly: ignore[bad-override] + def python_type_var(self) -> OrderedSetClassVariable: + return OrderedSetClassVariable() + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.load_import_from("torch.utils._ordered_set", "OrderedSet") + ) + codegen.foreach([x.vt for x in self.set_items]) + codegen.append_output(create_instruction("BUILD_LIST", arg=len(self.set_items))) + codegen.extend_output(create_call_function(1, False)) + + +class FrozensetVariable(SetVariable): + # PyFrozenSet_Type: https://github.com/python/cpython/blob/v3.13.0/Objects/setobject.c#L2526 + _cpython_type = frozenset + + def debug_repr(self) -> str: + if not self.items: + return "frozenset()" + else: + items: list[str] = [] + for k in self.items: + key_str = ( + repr(k.vt.value) if hasattr(k.vt, "value") else k.vt.debug_repr() + ) + items.append(key_str) + return "{" + ",".join(items) + "}" + + @property + def set_items(self) -> set["HashableTracker"]: + return set(self.items.keys()) + + def python_type(self) -> type: + return frozenset + + def python_type_var(self) -> "BuiltinVariable": + return variables.BuiltinVariable(frozenset) + + def as_python_constant(self) -> Any: + return frozenset({k.vt.as_python_constant() for k in self.set_items}) + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.extend_output( + [ + codegen.create_load_global("frozenset"), + ] + ) + ) + codegen.foreach([x.vt for x in self.set_items]) + codegen.extend_output( + [ + create_instruction("BUILD_LIST", arg=len(self.set_items)), + *create_call_function(1, False), + ] + ) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name in ["add", "pop", "update", "remove", "discard", "clear"]: + raise RuntimeError(f"Illegal call_method {name} on a frozenset") + elif name == "__init__": + # frozenset is immutable. Calling __init__ again shouldn't have any effect + return ConstantVariable.create(None) + elif name in ( + "copy", + "difference", + "intersection", + "symmetric_difference", + ): + r = super().call_method(tx, name, args, kwargs) + return FrozensetVariable(r.items) # type: ignore[attr-defined] + return super().call_method(tx, name, args, kwargs) + + def is_python_hashable(self) -> Literal[True]: + """ + Frozensets are immutable and hashable in Python. + """ + return True + + def get_python_hash(self) -> int: + return hash(self.as_python_constant()) + + def is_python_equal(self, other: object) -> bool: + return ( + isinstance(other, VariableTracker) + and self.as_python_constant() == other.as_python_constant() + ) + + +class DictKeySetVariable(SetVariable): + def debug_repr(self) -> str: + if not self.items: + return "dict_keys([])" + else: + items: list[str] = [] + for k in self.items: + key_str = ( + repr(k.vt.value) if hasattr(k.vt, "value") else k.vt.debug_repr() + ) + items.append(key_str) + return "dict_keys([" + ",".join(items) + "])" + + def install_set_contains_guard( + self, tx: "InstructionTranslator", args: list[VariableTracker] + ) -> None: + # Already EQUALS_MATCH guarded + pass + + @property + def set_items(self) -> Any: + return self.items + + def python_type(self) -> type: + from ..utils import dict_keys + + return dict_keys + + def as_python_constant(self) -> Any: + return dict.fromkeys( + {k.vt.as_python_constant() for k in self.set_items}, None + ).keys() + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name in ["add", "pop", "update", "remove", "discard", "clear"]: + raise RuntimeError(f"Illegal call_method {name} on a dict_keys") + return super().call_method(tx, name, args, kwargs) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/streams.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/streams.py new file mode 100644 index 0000000000000000000000000000000000000000..c42e6a84d1aaa83c0964ac4ced07cda6b8b11916 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/streams.py @@ -0,0 +1,724 @@ +import collections +from collections.abc import Callable +from typing import Any, Optional + +import torch +from torch._dynamo.variables.dicts import ConstDictVariable +from torch._dynamo.variables.lists import TupleVariable +from torch.fx import has_side_effect, Proxy + +from .. import graph_break_hints +from ..bytecode_transformation import create_call_function +from ..exc import TYPE_CHECKING, unimplemented +from ..graph_bytecode_inputs import ( + CURRENT_STREAM_INDEX, + get_external_object_by_index, + register_graph_created_object, + register_user_object, + reset_user_object_tracking, +) +from ..source import CurrentStreamSource +from .base import VariableTracker +from .constant import ConstantVariable +from .ctx_manager import FxTracebackAnnotateVariable +from .lazy import LazyVariableTracker + + +if TYPE_CHECKING: + from torch._dynamo.symbolic_convert import InstructionTranslator + + from ..codegen import PyCodegen + +from torch._library.custom_ops import custom_op + + +Tensor = torch.Tensor + + +def new_event(*args: Any, **kwargs: Any) -> int: + event = torch.Event(*args, **kwargs) + return register_graph_created_object( + event, + EventVariable.make_construct_in_graph_event_fn( + TupleVariable([]), ConstDictVariable({}) + ), + ) + + +def new_stream(*args: tuple[Any], **kwargs: Any) -> int: + stream = torch.Stream(*args, **kwargs) # type: ignore[no-matching-overload,call-overload] + return register_graph_created_object( + stream, + StreamVariable.make_construct_in_graph_stream_fn( + TupleVariable([]), ConstDictVariable({}) + ), + ) + + +def _codegen_current_stream(device: torch.device, cg: "PyCodegen") -> None: + cg.add_push_null( + lambda: cg.load_import_from( + torch._dynamo.graph_bytecode_inputs.__name__, # type: ignore[implicit-imports] + "stash_graph_created_object", + ) + ) + cg(CurrentStreamSource(device)) + cg.extend_output(create_call_function(1, False)) + + +def get_current_stream(device: torch.device) -> int: + stream = torch.accelerator.current_stream(device) + return register_graph_created_object( + stream, lambda _, cg: _codegen_current_stream(device, cg) + ) + + +def _get_stream_by_index(index: int) -> torch.Stream: + stream = get_external_object_by_index(index) + assert isinstance(stream, torch.Stream), ( + f"Fork/join stream expected a stream object at index {index}" + ) + return stream + + +def _get_event_by_index(index: int) -> torch.Event: + event = get_external_object_by_index(index) + assert isinstance(event, torch.Event), ( + f"Record/wait event expected an event object at index {index}" + ) + return event + + +@custom_op("streams::fork", mutates_args=()) +def fork_stream( + from_index: int, # kept to make stream transitions clearer + to_index: int, +) -> None: + torch.accelerator.set_stream(_get_stream_by_index(to_index)) + + +@fork_stream.register_fake +def _( + from_index: int, # kept to make stream transitions clearer + to_index: int, +) -> None: + pass + + +has_side_effect(torch.ops.streams.fork.default) + + +@custom_op("streams::join", mutates_args=()) +def join_stream(from_index: int, to_index: int) -> None: + torch.accelerator.set_stream(_get_stream_by_index(to_index)) + + +@join_stream.register_fake +def _( + from_index: int, + to_index: int, +) -> None: + pass + + +has_side_effect(torch.ops.streams.join.default) + + +@custom_op("streams::record_event", mutates_args=()) +def record_event(event_index: int, stream_index: int) -> None: + event = _get_event_by_index(event_index) + stream = _get_stream_by_index(stream_index) + event.record(stream) + + +@record_event.register_fake +def _( + event_index: int, + stream_index: int, +) -> None: + pass + + +has_side_effect(torch.ops.streams.record_event.default) + + +@custom_op("streams::wait_event", mutates_args=()) +def wait_event(event_index: int, stream_index: int) -> None: + event = _get_event_by_index(event_index) + stream = _get_stream_by_index(stream_index) + event.wait(stream) + + +@wait_event.register_fake +def _( + event_index: int, + stream_index: int, +) -> None: + pass + + +has_side_effect(torch.ops.streams.wait_event.default) + + +@custom_op("streams::synchronize_event", mutates_args=()) +def synchronize_event(event_index: int) -> None: + event = _get_event_by_index(event_index) + event.synchronize() + + +@synchronize_event.register_fake +def _(event_index: int) -> None: + pass + + +has_side_effect(torch.ops.streams.synchronize_event.default) + + +@custom_op("streams::synchronize_device", mutates_args=()) +def synchronize_device(device_type: str, device_index: int) -> None: + torch.accelerator.synchronize(torch.device(device_type, device_index)) + + +@synchronize_device.register_fake +def _(device_type: str, device_index: int) -> None: + pass + + +has_side_effect(torch.ops.streams.synchronize_device.default) + + +@custom_op("streams::synchronize_stream", mutates_args=()) +def synchronize_stream(stream_index: int) -> None: + stream = _get_stream_by_index(stream_index) + stream.synchronize() + + +@synchronize_stream.register_fake +def _(stream_index: int) -> None: + pass + + +has_side_effect(torch.ops.streams.synchronize_stream.default) + + +@custom_op("streams::wait_stream", mutates_args=()) +def wait_stream(waiting_stream_index: int, waited_on_stream_index: int) -> None: + waiting = _get_stream_by_index(waiting_stream_index) + waited_on = _get_stream_by_index(waited_on_stream_index) + waiting.wait_stream(waited_on) + + +@wait_stream.register_fake +def _( + event_index: int, + stream_index: int, +) -> None: + pass + + +has_side_effect(torch.ops.streams.wait_stream.default) + + +@custom_op("streams::sync_dealloc", mutates_args=()) +def sync_dealloc( + wait_event_index: int, src_stream_index: int, to_dealloc: torch.Tensor +) -> None: + """An op which waits on an event and moves the last usage of to_dealloc + after the wait, so that after the sync occurs, the deallocation or + subsequent reuse of the tensor's memory will be guaranteed to happen + after a side stream is finished using it. + See https://docs.pytorch.org/docs/stable/generated/torch.Tensor.record_stream.html#torch.Tensor.record_stream + for more details""" + torch.ops.streams.wait_event.default(wait_event_index, src_stream_index) + + +@sync_dealloc.register_fake +def _( + wait_event_index: int, + src_stream_index: int, + to_dealloc: torch.Tensor, +) -> None: + pass + + +has_side_effect(torch.ops.streams.sync_dealloc.default) + + +@custom_op("streams::record_stream", mutates_args=()) +def record_stream(tensor: torch.Tensor, stream_index: int) -> None: + tensor.record_stream(_get_stream_by_index(stream_index)) + + +@record_stream.register_fake +def _( + tensor: torch.Tensor, + stream_index: int, +) -> None: + pass + + +has_side_effect(torch.ops.streams.record_stream.default) + + +class SymbolicStreamState: + """Track the currently entered stream if any""" + + def __init__(self) -> None: + from ..source import CurrentStreamSource + + cur_stack: list[StreamVariable] = [] + if torch.accelerator.is_available(): + # Reset the registry so the current stream is guaranteed index 0. + reset_user_object_tracking() + stream = torch.accelerator.current_stream() + source = CurrentStreamSource(stream.device) + # Register the current stream so it gets index 0 (registry is + # fresh at tracing start). The inductor wrapper updates this + # entry at runtime so cudagraph capture uses the capture stream + # instead of this stale trace-time stream. + index = register_user_object(stream, source) + assert index == CURRENT_STREAM_INDEX, ( + f"Current stream must be registered at index {CURRENT_STREAM_INDEX}, " + f"got {index}" + ) + stream_var = LazyVariableTracker.create(stream, source=source) + # Set user_object_index as an instance attribute so accessing it + # does NOT trigger LazyVariableTracker realization. + stream_var.user_object_index = index # type: ignore[union-attr] + cur_stack = [stream_var] # type: ignore[list-item] + + self.cur_stream_stack: collections.deque[StreamVariable] = collections.deque( + cur_stack + ) + + def enter_stream(self, stream: "StreamVariable") -> None: + self.cur_stream_stack.append(stream) + + def exit_stream(self) -> None: + self.cur_stream_stack.pop() + + def cur_stream(self, device: torch.device | None = None) -> "StreamVariable": + if device is not None: + for stream in reversed(self.cur_stream_stack): + if stream.device == device: + return stream + + return self.cur_stream_stack[-1] + + def in_stream_context(self) -> bool: + return len(self.cur_stream_stack) > 0 + + def cur_stream_id(self) -> int: + """Get a Python object id for the current stream without realizing lazy variables.""" + stream = self.cur_stream_stack[-1] + if isinstance(stream, LazyVariableTracker) and not stream.is_realized(): + return id(stream.peek_value()) + return id(stream.value) + + +class StreamContextVariable(FxTracebackAnnotateVariable): + """This represents torch.cuda.StreamContext""" + + @staticmethod + def create( + tx: "InstructionTranslator", + stream_to_enter: "StreamVariable", + **kwargs: dict[str, Any], + ) -> "StreamContextVariable": + return StreamContextVariable( + stream_to_enter, + **kwargs, + ) + + def __init__(self, stream: Optional["StreamVariable"], **kwargs: Any) -> None: + self.stream = stream + super().__init__( + target_values={"stream": self.get_stream().user_object_index}, + initial_values=None, + **kwargs, + ) + + def enter( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + # to stream, from stream is the order of the arguments + # we are entering the target, and leaving the initial stream + tx.symbolic_stream_state.enter_stream(self.get_stream()) + return super().enter(tx) + + def exit( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> VariableTracker: + # to stream, from stream is the order of the arguments + # we are leaving the target, and entering the initial stream + tx.symbolic_stream_state.exit_stream() + return super().exit(tx, *args) + + def python_type(self) -> type: + return torch.cuda.StreamContext + + def supports_graph_breaks(self) -> bool: + return True + + def get_stream(self) -> "StreamVariable": + assert self.stream, "Stream context should have a separate stream" + return self.stream + + +class StreamVariable(StreamContextVariable): + """Represents the device-agnostic torch.Stream class""" + + _cpython_type = torch.Stream + + def __init__( + self, + proxy: Proxy, + value: torch.Stream, + user_object_index: int | None = None, + **kwargs: Any, + ) -> None: + # Index into the user object table + # used to pass arbitrary objects to the graph + if proxy is not None and "example_value" in proxy.node.meta: + assert proxy.node.meta["example_value"] == value + + self.proxy = proxy + self.value = value + self.device = value.device + + self.user_object_index = user_object_index + super().__init__(None, **kwargs) + + def python_type(self) -> type: + return torch.Stream + + def get_real_python_backed_value(self) -> object: + return self.value + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + assert hasattr(self.value, name), f"no stream method found named {name}" + + from ..utils import cmp_name_to_op_mapping, proxy_args_kwargs + from .builder import wrap_fx_proxy_cls + + if name == "wait_event": + event_arg = args[0] + assert isinstance(event_arg, EventVariable) + tx.output.create_proxy( + "call_function", + torch.ops.streams.wait_event, + (event_arg.user_object_index, self.user_object_index), + {}, + ) + return ConstantVariable.create(None) + elif name == "wait_stream": + other_stream = args[0] + assert isinstance(other_stream, StreamVariable) + tx.output.create_proxy( + "call_function", + torch.ops.streams.wait_stream, + (self.user_object_index, other_stream.user_object_index), + {}, + ) + return ConstantVariable.create(None) + elif name == "synchronize": + tx.output.create_proxy( + "call_function", + torch.ops.streams.synchronize_stream, + (self.user_object_index,), + {}, + ) + return ConstantVariable.create(None) + elif name == "query": + return wrap_fx_proxy_cls( + target_cls=ConstantVariable, + tx=tx, + proxy=tx.output.create_proxy( + "call_method", name, *proxy_args_kwargs([self] + args, kwargs) + ), + ) + elif name == "record_event": + from .builder import wrap_fx_proxy + + tx.output.check_event_record_after_input_mutation(id(self.value)) + if args and isinstance(args[0], EventVariable): + event_var = args[0] + event = event_var.value + event_index = event_var.user_object_index + else: + event = self.value.record_event() + event_index = register_graph_created_object( + event, + EventVariable.make_construct_in_graph_event_fn( + TupleVariable([]), ConstDictVariable({}) + ), + ) + tx.output.create_proxy( + "call_function", + torch.ops.streams.record_event, + (event_index, self.user_object_index), + {}, + ) + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + get_external_object_by_index, + (event_index,), + {}, + ), + ) + elif name in cmp_name_to_op_mapping and len(args) == 1 and not kwargs: + from ..guards import GuardBuilder, install_guard + + if self.source: + install_guard(self.source.make_guard(GuardBuilder.EQUALS_MATCH)) + + # NB : Checking for mutation is necessary because we compare + # constant values + other = args[0] + if not isinstance(other, StreamVariable): + return VariableTracker.build(tx, NotImplemented) + + if other.source: + assert self.source is not None + install_guard(self.source.make_guard(GuardBuilder.EQUALS_MATCH)) + return VariableTracker.build( + tx, + cmp_name_to_op_mapping[name](self.value, other.value), # type: ignore[arg-type] + ) + + return super().call_method(tx, name, args, kwargs) + + def as_proxy(self) -> Proxy: + return self.proxy + + def module_name(self) -> str: + return "torch._C" + + def fn_name(self) -> str: + return "Stream" + + def reconstruct(self, codegen: "PyCodegen") -> None: + # If we got here, this stream is fully subsumed by the graph - this means it is + # not an input or global + assert not self.source + if self.user_object_index is not None: + codegen.add_push_null( + lambda: codegen.load_import_from( + torch._dynamo.graph_bytecode_inputs.__name__, + "get_external_object_by_index", + ) + ) + codegen.append_output(codegen.create_load_const(self.user_object_index)) + codegen.extend_output(create_call_function(1, False)) + else: + # This will support the legacy behavior + prefix = f"_stream_{self.device}" + name = codegen.tx.output.install_global_by_id(prefix, self.value) + codegen.append_output(codegen.create_load_global(name, add=True)) + + def get_stream(self) -> "StreamVariable": + return self + + @staticmethod + def make_construct_in_graph_stream_fn( + args: TupleVariable, kwargs: ConstDictVariable + ) -> Callable[[int, "PyCodegen"], None]: + def fn(index: int, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.load_import_from( + torch._dynamo.graph_bytecode_inputs.__name__, # type: ignore[implicit-imports] + "stash_graph_created_object", + ) + ) + codegen.add_push_null( + lambda: codegen.load_import_from( + torch._dynamo.utils.__name__, "build_stream" + ) + ) + codegen(args) + codegen(kwargs) + codegen.extend_output(create_call_function(2, False)) + codegen.extend_output(create_call_function(1, False)) + + return fn + + +class CudaStreamVariable(StreamVariable): + """Represents torch.cuda.Stream, preserving device-specific type and attributes.""" + + _cpython_type = torch.cuda.Stream + + def python_type(self) -> type: + return torch.cuda.Stream + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> "VariableTracker": + from . import ConstantVariable + + if name == "cuda_stream": + from ..guards import GuardBuilder, install_guard + + if self.source: + install_guard(self.source.make_guard(GuardBuilder.EQUALS_MATCH)) + + if hasattr(self.value, "cuda_stream"): + return ConstantVariable.create(self.value.cuda_stream) + + if hasattr(self.value, "native_handle"): + return ConstantVariable.create(self.value.native_handle) + + return super().var_getattr(tx, name) + + +class EventVariable(VariableTracker): + def __init__( + self, + proxy: Proxy, + value: torch.Event, + user_object_index: int | None, + **kwargs: Any, + ) -> None: + if proxy is not None and "example_value" in proxy.node.meta: + assert proxy.node.meta["example_value"] == value + super().__init__(**kwargs) + self.proxy = proxy + self.value = value + self.user_object_index = user_object_index + + def python_type(self) -> type: + return torch.Event + + def get_real_python_backed_value(self) -> object: + return self.value + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from ..utils import proxy_args_kwargs + from .builder import wrap_fx_proxy_cls + + if name == "wait": + _, stream_index = EventVariable._get_stream_arg(tx, args, kwargs) + tx.output.create_proxy( + "call_function", + torch.ops.streams.wait_event, + ( + self.user_object_index, + stream_index, + ), + {}, + ) + return ConstantVariable.create(None) + elif name == "record": + stream_arg, stream_index = EventVariable._get_stream_arg(tx, args, kwargs) + tx.output.check_event_record_after_input_mutation(id(stream_arg.value)) + tx.output.create_proxy( + "call_function", + torch.ops.streams.record_event, + ( + self.user_object_index, + stream_index, + ), + {}, + ) + return ConstantVariable.create(None) + elif name == "synchronize": + tx.output.create_proxy( + "call_function", + torch.ops.streams.synchronize_event, + (self.user_object_index,), + {}, + ) + return ConstantVariable.create(None) + elif name == "query": + return wrap_fx_proxy_cls( + target_cls=ConstantVariable, + tx=tx, + proxy=tx.output.create_proxy( + "call_method", name, *proxy_args_kwargs([self] + args, kwargs) + ), + ) + else: + method_name = ( + f"{type(self.value).__module__}.{type(self.value).__qualname__}.{name}" + ) + unimplemented( + gb_type="Unsupported event method", + context=str(name), + explanation=f"Dynamo doesn't support tracing the {method_name} method. " + f"We currently support wait, record, synchronize, and query.", + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + ) + + def as_proxy(self) -> Proxy: + return self.proxy + + @staticmethod + def _get_stream_arg( + tx: "InstructionTranslator", + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> tuple["StreamVariable", int]: + """Returns (stream_variable, stream_index_for_op). + + The ambient current stream is registered at index 0 in the external + object registry. The inductor wrapper updates index 0 at runtime so + that cudagraph capture sees the capture stream, not the stale + trace-time default stream. + """ + stream_arg = None + if args: + stream_arg = args[0] + elif kwargs: + stream_arg = kwargs.get("stream") + + if not stream_arg: + stream_var = tx.symbolic_stream_state.cur_stream() + return stream_var, stream_var.user_object_index # type: ignore[return-value] + + return stream_arg, stream_arg.user_object_index # type: ignore[return-value] + + @staticmethod + def make_construct_in_graph_event_fn( + args: TupleVariable, kwargs: ConstDictVariable + ) -> Callable[[int, "PyCodegen"], None]: + def fn(index: int, codegen: "PyCodegen") -> None: + codegen.add_push_null( + lambda: codegen.load_import_from( + torch._dynamo.graph_bytecode_inputs.__name__, # type: ignore[implicit-imports] + "stash_graph_created_object", + ) + ) + codegen.add_push_null( + lambda: codegen.load_import_from( + torch._dynamo.utils.__name__, "build_event" + ) + ) + codegen(args) + codegen(kwargs) + codegen.extend_output(create_call_function(2, False)) + codegen.extend_output(create_call_function(1, False)) + + return fn + + def reconstruct(self, codegen: "PyCodegen") -> None: + # If we got here, this event is fully subsumed by the graph - this means it is + # not an input or global + assert not self.source + # Similar to stream handling, we lift the event into a global and then codegen bytecode to load it from there. + prefix = "_event" + name = codegen.tx.output.install_global_by_id(prefix, self.value) + codegen.append_output(codegen.create_load_global(name, add=True)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/tensor.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/tensor.py new file mode 100644 index 0000000000000000000000000000000000000000..45aff0fae9fbac6cc111f2a22a99a48e0564d924 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/tensor.py @@ -0,0 +1,2692 @@ +""" +This module contains variable tracker classes for handling tensors and tensor-related operations in Dynamo. + +The main class is TensorVariable which represents torch.Tensor inputs and intermediate values in the FX graph. +It handles tensor operations, method calls, and maintains metadata about tensor properties like dtype, device, etc. + +Other key classes include: +- SymNodeVariable: Represents symbolic scalars (int/float/bool) used for size computation and unspecialized values +- NumpyNdarrayVariable: Handles numpy array interop through torch._numpy +- UnspecializedPythonVariable: Represents unspecialized Python numeric values as 1-element tensors +- TensorSubclassVariable: Handles tensor subclasses with __torch_function__ overrides +- UntypedStorageVariable: Represents tensor storage objects +- DataPtrVariable: Handles tensor data pointer operations + +These classes work together to track tensor operations and properties during Dynamo's tracing process. +""" + +import functools +import logging +import operator +import textwrap +import traceback +import types +from collections.abc import Iterable, Sequence +from contextlib import nullcontext +from itertools import chain +from types import NoneType +from typing import Any, NoReturn, Optional, TYPE_CHECKING + +import sympy + +import torch._numpy as tnp +import torch.fx +import torch.random +from torch import sym_float, sym_int +from torch._dynamo import compiled_autograd +from torch._library.opaque_object import is_opaque_reference_type +from torch._opaque_base import OpaqueBase +from torch._subclasses.meta_utils import is_sparse_any +from torch.fx.experimental.symbolic_shapes import ( + guard_scalar, + GuardOnDataDependentSymNode, + has_free_symbols, + is_symbolic, + SymTypes, +) +from torch.utils._python_dispatch import is_traceable_wrapper_subclass + +from .. import config, graph_break_hints, variables +from .._trace_wrapped_higher_order_op import trace_wrapped +from ..exc import ( + ObservedAttributeError, + raise_observed_exception, + TorchRuntimeError, + unimplemented, + UnknownPropertiesDuringBackwardTrace, + UserError, + UserErrorType, +) +from ..external_utils import _ApplyBackwardHook, call_hook_from_backward_state +from ..guards import GuardBuilder, install_guard +from ..source import AttrSource +from ..utils import ( + fqn, + get_custom_getattr, + get_fake_value, + get_real_value, + guard_if_dyn, + object_has_getattribute, + product, + proxy_args_kwargs, + raise_args_mismatch, + set_example_value, + tensortype_to_dtype, +) +from .base import AttributeMutationNew, ValueMutationNew, VariableTracker +from .constant import ConstantVariable +from .lists import ListIteratorVariable, SizeVariable +from .script_object import TorchScriptObjectVariable +from .user_defined import UserDefinedClassVariable + + +try: + import numpy as np +except ModuleNotFoundError: + np = None # type: ignore[assignment] + + +if TYPE_CHECKING: + from torch._dynamo.codegen import PyCodegen + from torch._dynamo.output_graph import OutputGraph + from torch._dynamo.symbolic_convert import ( + InstructionTranslator, + InstructionTranslatorBase, + ) + + from .functions import UserFunctionVariable + from .torch_function import TensorWithTFOverrideVariable + + +log = logging.getLogger(__name__) + +# Ops that allow tensor tensor +supported_tensor_comparison_ops = { + ">": operator.gt, + "<": operator.lt, + ">=": operator.ge, + "<=": operator.le, + "==": operator.eq, + "!=": operator.ne, + "is": operator.is_, + "is not": operator.is_not, +} +# Ops that allow tensor None +supported_const_comparison_ops = { + "is": operator.is_, + "is not": operator.is_not, + "==": operator.eq, + "!=": operator.ne, +} +supported_comparison_ops = { + **supported_tensor_comparison_ops, + **supported_const_comparison_ops, +} +supported_tensor_comparison_op_values = dict.fromkeys( + supported_tensor_comparison_ops.values() +) +supported_const_comparison_op_values = dict.fromkeys( + supported_const_comparison_ops.values() +) + + +def is_bound_tensor_method(value: object) -> bool: + return bool( + callable(value) + and not torch._dynamo.utils.object_has_getattribute(value) + and hasattr(value, "__self__") + and isinstance(value.__self__, torch.Tensor) + and getattr(value.__self__, value.__name__, None) + ) + + +# instead of using inspect.getattr_static, we directly lookup the appropriate +# dicts. It is necessary to keep the torch._C.TensorBase first in the or +# operation, because the second arg takes priority in or operation when there +# are common keys. +all_tensor_attrs = torch._C.TensorBase.__dict__ | torch.Tensor.__dict__ + + +class TensorVariable(VariableTracker): + """A torch.Tensor input or an intermediate value in the FX graph""" + + _nonvar_fields = { + "proxy", + "dtype", + "device", + "layout", + "ndim", + "size", + "stride", + "requires_grad", + "is_quantized", + "is_contiguous", + "is_nested", + "is_sparse", + "class_type", + "specialized_value", + "_is_name_set", + *VariableTracker._nonvar_fields, + } + + def get_real_value(self) -> torch.Tensor: + """ + Get the actual value represented by this variable if computation is run + using the user-provided inputs. + NOTE: this runs actual tensor computation and may be + slow and memory-intensive. + """ + return get_real_value(self.proxy.node, self.proxy.tracer) + + def __init__( + self, + proxy: torch.fx.Proxy, + *, + dtype: torch.dtype, + device: torch.device, + layout: torch.layout, + ndim: int, + requires_grad: bool, + is_nested: bool, + is_quantized: bool, + is_sparse: bool, + class_type: type, + has_grad_fn: bool, + _size: tuple[Any, ...] | None = None, + stride: tuple[Any, ...] | None = None, + is_contiguous: bool | None = None, + _is_name_set: bool | None = None, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.proxy = proxy + self.dtype = dtype + self.device = device + self.layout = layout + self.ndim = ndim + self._size = _size # this is accessed as a property for validation + self.stride = stride + self.requires_grad = requires_grad + self.is_quantized = is_quantized + self.is_contiguous = is_contiguous + self.is_nested = is_nested + self.is_sparse = is_sparse + self.class_type = class_type + self.has_grad_fn = has_grad_fn + if _is_name_set is None: + # no need to rename inputs + _is_name_set = self.proxy.node.op == "placeholder" + self._is_name_set: bool = _is_name_set + + def synchronize_attributes( + self, tx: "InstructionTranslator", target_cls: type | None = None + ) -> None: + from .builder import get_specialized_props, infer_subclass_type + + if target_cls is None: + target_cls = type(self) + + example_value = self.proxy.node.meta.get("example_value") + specialized_props = get_specialized_props( + target_cls, tx, example_value, infer_subclass_type(example_value) + ) + for k, v in specialized_props.items(): + setattr(self, k, v) + + def _get_fake_version(self) -> int | None: + """Get the current version of self's fake tensor, or None if unavailable.""" + self_fake = self.proxy.node.meta.get("example_value") + return self_fake._version if self_fake is not None else None + + def _sync_if_inplace_mutation( + self, + tx: "InstructionTranslator", + version_before: int | None, + has_tensor_arg: bool, + ) -> None: + """ + Sync attributes if self was mutated by an inplace operation. + + See Note [Inplace ops and VariableTracker metadata] + """ + version_after = self._get_fake_version() + if ( + version_before is not None + and version_after is not None + and version_after > version_before + ): + if has_tensor_arg: + self.synchronize_attributes(tx) + tx.output.check_input_mutation_on_current_stream(tx) + + def debug_repr(self) -> str: + # TODO: strip off fake tensor from repr here + return repr(self.proxy.node.meta["example_value"]) + + def as_proxy(self) -> torch.fx.Proxy: + return self.proxy + + def python_type(self) -> type: + return self.class_type + + def is_tensor(self) -> bool: + return True + + def bool_impl(self, tx: "InstructionTranslator") -> VariableTracker: + # THPVariable_bool calls at::Tensor::is_nonzero(), i.e. .item() != 0. + from .constant import ConstantVariable + + item = self.call_method(tx, "item", [], {}) + if isinstance(item, SymNodeVariable) and isinstance( + item.sym_num, torch.SymBool + ): + return item + if isinstance(item, ConstantVariable): + return VariableTracker.build(tx, bool(item.value)) + return SymNodeVariable.create(tx, item.as_proxy() != 0) + + @staticmethod + def specialize(value: torch.Tensor) -> dict[str, Any]: + props: dict[str, Any] = { + "dtype": value.dtype, + "device": value.device, + "layout": value.layout, + "ndim": int(value.ndim), + "requires_grad": value.requires_grad, + "is_nested": value.is_nested, + "is_quantized": value.is_quantized, + "is_sparse": value.is_sparse, + "class_type": type(value), + } + try: + props["has_grad_fn"] = value.grad_fn is not None + except Exception: + # Workaround for issues with create_parameter_op in Dynamo. Reading + # grad_fn should never cause an issue. + props["has_grad_fn"] = False + + if is_sparse_any(value) and not has_free_symbols(value): + props["_size"] = tuple( + int(s) if is_symbolic(s) else s for s in value.size() + ) + elif not has_free_symbols(value): + # this is a fully static shape, and the keys on props here inform specialization. + # We have to cast to int here, because these might get accessed as ConstantVariable, which has + # a strict no-symint policy. If we got here due to not having free symbols, this is a known constant + # already. We could remove the discrepancy here, by having ConstantVariable be more permissive for + # constant backed SymInts, but that assert being strict has led to some good signal in hunting bugs, and + # I'd like to keep it around for now. + props["_size"] = tuple( + # the non is_symbolic case applies to the jagged layout + # NestedTensor case as singleton ints are not symbolic + int(s) if is_symbolic(s) else s + for s in value.size() + ) + props["stride"] = tuple(value.stride()) + if torch._C._functorch.is_batchedtensor(value): + # Batched tensors does not support contiguity patterns, so + # we refrain from computing the `is_contiguous` property + props["is_contiguous"] = None + else: + props["is_contiguous"] = tuple( + x + for x in torch._prims_common._memory_formats + if value.is_contiguous(memory_format=x) + ) + return props + + def dynamic_getattr( + self, tx: "InstructionTranslator", name: str + ) -> VariableTracker: + fake_val = self.proxy.node.meta["example_value"] + # For getattrs on tensors without sources, + # we can do better than the default (creating a GetAttrVariable) + # if: + # (1) the tensor is a traceable tensor subclass + # (2) We are getattr'ing an inner tensor from that subclass + if not self.source and is_traceable_wrapper_subclass(fake_val): + attrs, _ctx = fake_val.__tensor_flatten__() + proxy = getattr(self.as_proxy(), name) + example_value = getattr(fake_val, name) + if name in attrs: + # attrs returned from tensor_flatten are always tensors or opaques + assert isinstance(example_value, (torch.Tensor, OpaqueBase)) + from .builder import wrap_fx_proxy + + return wrap_fx_proxy(tx=tx, proxy=proxy, example_value=example_value) + elif is_opaque_reference_type(type(example_value)): + fake_script_obj = torch._library.fake_class_registry.maybe_to_fake_obj( + tx.output.fake_mode, example_value + ) + return TorchScriptObjectVariable.create(proxy, fake_script_obj) + elif isinstance( + example_value, + torch._library.fake_class_registry.FakeScriptObject, + ): + return TorchScriptObjectVariable.create(proxy, example_value) + # any other attributes on the subclass (that are not methods) + # are assumed to be constant metadata. + elif not callable(example_value): + return VariableTracker.build(tx, example_value) + + if not (self.source and self.source.subguards_allowed()): + raise NotImplementedError + + # For local source, we associate the real value. We use this real value + # for implementing getattr fallthrough on the variable tracker base class. + + # Note - this scope construction is mirrored in guards + # A subsequent PR will introduce a util. + scope = {"L": tx.output.local_scope, "G": tx.output.global_scope} + try: + # We raise in case we get a typerror bug w/ SuperSource. + # SuperSource has bugs in it atm, and can produce code like + # eval("super(L['mod'].model.model.encoder.embed_positions.forward__class__, + # L['mod'].model.model.encoder.embed_positions)", scope) + # Which is incorrect, and violates the invariant that all sources should be eval()-able against the scope. + _input_associated_real_value = eval(self.source.name, scope) + except Exception as exc: + raise NotImplementedError from exc + + if _input_associated_real_value is None: + raise NotImplementedError + + if object_has_getattribute(_input_associated_real_value): + raise NotImplementedError + + if get_custom_getattr(_input_associated_real_value): + raise NotImplementedError + + try: + real_value = getattr(_input_associated_real_value, name) + except AttributeError: + raise_observed_exception( + AttributeError, + tx, + args=[ + f"'{type(_input_associated_real_value).__name__}' object has no attribute '{name}'" + ], + ) + + attr_source = AttrSource(self.source, name) + + # Typically we'd want to use variable builder here + # but unfortunately id(real_value.__self__) is not id() + if is_bound_tensor_method(real_value): + # No need to install the guard because its a bound tensor method + from .misc import GetAttrVariable + + return GetAttrVariable( + self, name, source=attr_source, py_type=type(real_value) + ) + + install_guard( + self.source.make_guard(functools.partial(GuardBuilder.HASATTR, attr=name)) + ) + return VariableTracker.build(tx, real_value, attr_source) + + def method_attr_ndim(self, tx: "InstructionTranslator") -> VariableTracker: + if self.ndim is not None: + return VariableTracker.build(tx, self.ndim) + else: + return self.call_method(tx, "dim", [], {}) + + def method_attr_dtype(self, tx: "InstructionTranslator") -> VariableTracker | None: + if self.dtype is not None: + return VariableTracker.build(tx, self.dtype) + return None + + def method_attr_device(self, tx: "InstructionTranslator") -> VariableTracker | None: + if self.device is not None: + return VariableTracker.build(tx, self.device) + return None + + def method_attr_layout(self, tx: "InstructionTranslator") -> VariableTracker | None: + if self.layout is not None: + return VariableTracker.build(tx, self.layout) + return None + + def method_attr_is_cuda( + self, tx: "InstructionTranslator" + ) -> ConstantVariable | None: + if self.device is not None: + return VariableTracker.build(tx, self.device.type == "cuda") + return None + + def method_attr_shape(self, tx: "InstructionTranslator") -> VariableTracker: + if self.valid_size(): + sizes: list[VariableTracker] = [ + VariableTracker.build(tx, x) for x in self.size + ] + return SizeVariable(sizes) + else: + return self.call_method(tx, "size", [], {}) + + def method_attr_requires_grad( + self, tx: "InstructionTranslator" + ) -> ConstantVariable | None: + if self.requires_grad is not None: + return VariableTracker.build(tx, self.requires_grad) + return None + + def method_attr_is_quantized( + self, tx: "InstructionTranslator" + ) -> ConstantVariable | None: + if self.is_quantized is not None: + return VariableTracker.build(tx, self.is_quantized) + return None + + def method_attr_is_sparse( + self, tx: "InstructionTranslator" + ) -> ConstantVariable | None: + if self.is_sparse is not None: + return VariableTracker.build(tx, self.is_sparse) + return None + + def method_attr_is_nested( + self, tx: "InstructionTranslator" + ) -> ConstantVariable | None: + if self.is_nested is not None: + return VariableTracker.build(tx, self.is_nested) + return None + + def method_attr_retain_grad(self, tx: "InstructionTranslator") -> NoReturn: + unimplemented( + gb_type="Tensor.retain_grad() with AOTDispatcher", + context=f"var_getattr {self} retain_grad", + explanation="`Tensor.retain_grad()` does not work with AOTDispatcher.", + hints=[], + ) + + def method_attr_grad(self, tx: "InstructionTranslator") -> VariableTracker | None: + if tx.output.side_effects.has_pending_mutation_of_attr(self, "grad"): + return tx.output.side_effects.load_attr(self, "grad") + # None tells var_getattr to use default .grad handling + return None + + def method_attr_data(self, tx: "InstructionTranslator") -> VariableTracker: + return variables.TorchInGraphFunctionVariable( + torch._C._autograd._get_data_attr # type: ignore[attr-defined] + ).call_function(tx, [self], {}) + + def method_attr_grad_fn( + self, tx: "InstructionTranslator" + ) -> ConstantVariable | None: + if self.has_grad_fn: + unimplemented( + gb_type="Tensor with grad_fn()", + context=f"var_getattr {self} grad_fn", + explanation="Dynamo does not support tracing tensors with a grad_fn directly.", + hints=[], + ) + else: + return variables.ConstantVariable.create(None) + + def method_attr__version(self, tx: "InstructionTranslator") -> VariableTracker: + from ..tensor_version_op import _tensor_version + + return variables.TorchInGraphFunctionVariable(_tensor_version).call_function( + tx, [self], {} + ) + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> ConstantVariable: + from . import GetAttrVariable + + # TODO - This is not a good solution but solves an accuracy issue. + # Today, var_getattr returns GetAttrVariable for both non-existent + # attributes and existing attributes. This is a bug and requires more + # deep dive. + if name in all_tensor_attrs: + return ConstantVariable.create(True) + + try: + var = VariableTracker.build(tx, getattr).call_function( + tx, [self, VariableTracker.build(tx, name)], {} + ) + # in the event that TensorVariable returns NotImplemented + # BuiltinVariable.call_getattr returns GetAttrVariable + ret_val = not isinstance(var, GetAttrVariable) + except (AttributeError, ObservedAttributeError): + ret_val = False + + if self.source: + install_guard( + self.source.make_guard( + functools.partial(GuardBuilder.HASATTR, attr=name) + ) + ) + + return VariableTracker.build(tx, ret_val) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + if self.is_strict_mode(tx): + if name in self._strict_mode_banned_ops(): + unimplemented( + gb_type="Strict mode banned op", + context=f"var_getattr {self} {name}", + explanation=f"Getattr invocation '{name}' in strict mode is not supported.", + hints=[ + f"Remove `{name}` from the list of banned ops by " + "setting `torch._dynamo.config._autograd_backward_strict_mode_banned_ops`.", + ], + ) + elif name in self._strict_mode_conditional_banned_ops(): + raise UnknownPropertiesDuringBackwardTrace( + f"Unknown property {name} during speculating backward, dynamo will insert contiguous call ahead and speculate it again" # noqa: B950 + ) + + if name == "__class__": + return VariableTracker.build(tx, self.python_type()) + + handler = getattr(self, f"method_attr_{name}", None) + result = handler(tx) if handler is not None else None + + # Add a guard for type matching, these guards are checked before tensor guards + # In some cases, a . guard can be evaluated first, and break if + # is later changed to another type + if ( + result is not None + and self.source + and self.source.subguards_allowed() + and not ( + name not in ("grad", "requires_grad") and result.is_python_constant() + ) + ): + install_guard(self.make_guard(GuardBuilder.TYPE_MATCH)) + result.source = AttrSource(self.source, name) + + # It's hard to get inplace view (metadata mutation) on graph input work properly across + # dynamo/aot/inductor, just fall back. + if self.source is not None and hasattr(torch.ops.aten, name): + fn = getattr(torch.ops.aten, name) + if ( + hasattr(fn, "overloads") + and hasattr(fn, fn.overloads()[0]) + and torch.Tag.inplace_view in getattr(fn, fn.overloads()[0]).tags + ): + # Delay the graph break to the actual call of unsqueeze_/resize_/resize_as_ etc. + return variables.misc.DelayGraphBreakVariable( + source=AttrSource(self.source, name), + msg="Getting an inplace view on a graph input is not supported", + ) + + # For attributes (not methods) that were not caught in the special handling above, + # (e.g. tensor.real), we handle these generically, assuming that the output type is + # a tensor. + if result is None and name != "grad": + + def try_generic_attr_handling() -> VariableTracker | None: + from .builder import wrap_fx_proxy + from .misc import GetAttrVariable + + static_attr = all_tensor_attrs.get(name, None) + if static_attr is None: + return None + + # Make sure this is an attribute, not a method. + # type(torch.Tensor.H) should be "getset_descriptor" + # This is a because of CPython implementation, see THPVariableType: + # these attributes are implemented under tp_getset, which appear + # as `getset_descriptor`s, (compared to, say, methods which appear + # as `method_descriptor`s) + if type(static_attr) is not types.GetSetDescriptorType: + return None + + proxy = GetAttrVariable.create_getattr_proxy(self.as_proxy(), name) + if self.source is not None: + return wrap_fx_proxy( + tx=tx, proxy=proxy, source=AttrSource(self.source, name) + ) + else: + return wrap_fx_proxy(tx=tx, proxy=proxy) + + result = try_generic_attr_handling() + + if result is None: + result = self.dynamic_getattr(tx, name) + + if result is None: + raise NotImplementedError + return result + + def call_id(self, tx: "InstructionTranslator") -> VariableTracker: + if not self.source: + unimplemented( + gb_type="Unsupported call_id() without source", + context=f"call_id {self}", + explanation="call_id() not supported for sourceless TensorVariable.", + hints=[], + ) + + assert self.source + # For local source, we associate the real value. We use this real value + scope = {"L": tx.output.local_scope, "G": tx.output.global_scope} + _input_associated_real_value = None + try: + _input_associated_real_value = eval(self.source.name, scope) + except Exception as exc: + unimplemented( + gb_type="Error getting associated real value", + context=f"call_id {self}", + explanation="Dynamo encountered an error while trying to " + "get the associated real value.", + hints=[], + from_exc=exc, + ) + + if _input_associated_real_value is None: + unimplemented( + gb_type="call_id() without associated real value", + context=f"call_id {self}", + explanation="Dynamo could not find an associated real value for the tensor.", + hints=[], + ) + + install_guard(self.source.make_guard(GuardBuilder.ID_MATCH)) + id_value = id(_input_associated_real_value) + return VariableTracker.build(tx, id_value) + + def has_unpack_var_sequence(self, tx: "InstructionTranslator") -> bool: + return self.ndim > 0 + + def unpack_var_sequence( + self, tx: "InstructionTranslator", idxes: Sequence[int] | None = None + ) -> list[VariableTracker]: + from .builder import wrap_fx_proxy_cls + from .torch_function import TensorWithTFOverrideVariable + + if self.valid_size(): + size_len = len(self.size) + else: + size_var = self.call_method(tx, "size", [], {}) + assert isinstance(size_var, SizeVariable) + size_len = len(size_var.items) + # Ensure we don't unpack a scalar tensor. + assert size_len != 0, "Can't unpack scalar tensors." + + if self.valid_size(): + length = self.size[0] + else: + dyn_length = self.call_method( + tx, "size", [VariableTracker.build(tx, 0)], {} + ) + # SymNodeVariable for symbolic sizes, ConstantVariable for constants OR values produced through + # symbolic_shapes, but that end up as int/sympy.Integer + assert ( + isinstance(dyn_length, SymNodeVariable) + or dyn_length.is_python_constant() + ) + if isinstance(dyn_length, SymNodeVariable): + length = dyn_length.evaluate_expr(tx.output) + else: + length = dyn_length.as_python_constant() + + if idxes is None: + idxes = range(length) # type: ignore[arg-type] + else: + assert len(idxes) == length, ( + f"Can't unpack a tensor of {length} rows into a tuple of {len(idxes)} elements." + ) + + # preserve tensor subclass type when unpacking + if isinstance(self, TensorWithTFOverrideVariable): + base_vars = [ + wrap_fx_proxy_cls( + target_cls=TensorVariable, tx=tx, proxy=self.as_proxy()[i] + ) + for i in idxes + ] + return [ + TensorWithTFOverrideVariable.from_tensor_var( + tx, v, self.class_type, self.source + ) + for v in base_vars + ] + + return [ + wrap_fx_proxy_cls(target_cls=type(self), tx=tx, proxy=self.as_proxy()[i]) + for i in idxes + ] + + def call_tree_map( + self, + tx: "InstructionTranslator", + tree_map_fn: "UserFunctionVariable", + map_fn: VariableTracker, + rest: Sequence[VariableTracker], + tree_map_kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + return map_fn.call_function(tx, [self, *rest], {}) + + def valid_size(self) -> bool: + return self._size is not None + + @property + def size(self) -> tuple[Any, ...]: + assert self._size is not None, "accessing None size in TensorVariable" + return self._size + + def _strict_mode_banned_ops(self) -> list[str]: + return torch._dynamo.config._autograd_backward_strict_mode_banned_ops + + def _strict_mode_conditional_banned_ops(self) -> list[str]: + return ( + torch._dynamo.config._autograd_backward_strict_mode_conditional_banned_ops + ) + + def mp_subscript_impl( + self, + tx: "InstructionTranslator", + key: VariableTracker, + ) -> VariableTracker: + # Tensor.__getitem__ is a custom C slot, not CPython's mp_subscript. + # TODO(follow-up): add tests for negative index, bool index, invalid key type + from .builder import SourcelessBuilder, VariableBuilder + from .torch_function import can_dispatch_torch_function, dispatch_torch_function + + if self.is_strict_mode(tx) and "__getitem__" in self._strict_mode_banned_ops(): + unimplemented( + gb_type="Illegal __getitem__ invocation in strict mode", + context=f"mp_subscript_impl {self} {key}", + explanation="Dynamo currently does not support __getitem__ " + "invocation in strict mode.", + hints=[], + ) + + empty_kwargs: dict[str, VariableTracker] = {} + static_attr = all_tensor_attrs.get("__getitem__", None) + if static_attr is not None and can_dispatch_torch_function( + tx, (self, key), empty_kwargs + ): + if self.source: + func_var = VariableBuilder( + tx, + AttrSource(AttrSource(self.source, "__class__"), "__getitem__"), + )(static_attr) + else: + func_var = SourcelessBuilder.create(tx, torch.Tensor.__getitem__) + return dispatch_torch_function(tx, func_var, (self, key), empty_kwargs) + + return self.method___getitem__(tx, key) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: Sequence[VariableTracker], + kwargs: "dict[str, VariableTracker]", + ) -> VariableTracker: + from .builder import SourcelessBuilder, VariableBuilder + from .torch_function import can_dispatch_torch_function, dispatch_torch_function + + if self.is_strict_mode(tx) and name in self._strict_mode_banned_ops(): + unimplemented( + gb_type="Illegal method invocation in strict mode", + context=f"call_method {self} {name} {args} {kwargs}", + explanation="Dynamo currently does not support this method " + f"({name}) invocation in strict mode.", + hints=[], + ) + + if name == "__deepcopy__": + unimplemented( + gb_type="Attempted to copy.deepcopy a tensor", + context=f"copy.deepcopy({self})", + explanation="Dynamo does not support copy.deepcopy() on tensors.", + hints=[ + "Avoid calling copy.deepcopy() on tensors inside compiled regions.", + ], + ) + + # Only override builtin tensor methods + # The user can manually add override handling + # with a decorator for other methods (e.g. a dispatch subclass with other methods) + static_attr = all_tensor_attrs.get(name, None) + is_base_tensor_method = static_attr is not None + + if ( + can_dispatch_torch_function(tx, tuple([self] + list(args)), kwargs) + and is_base_tensor_method + ): + if self.source: + func_var = VariableBuilder( + tx, AttrSource(AttrSource(self.source, "__class__"), name) + )(static_attr) + else: + func_var = SourcelessBuilder.create(tx, getattr(torch.Tensor, name)) + + return dispatch_torch_function( + tx, func_var, tuple([self] + list(args)), kwargs + ) + + """ + Dispatch to a method-specific handler defined below. If the + handler returns None (or doesn't exist) we put the method call + in the graph. + """ + + # This is seen in inspect signature where we check if the value is a default value + if name == "__eq__" and isinstance(args[0], UserDefinedClassVariable): + return variables.ConstantVariable.create(False) + + if name == "wait": + if args or kwargs: + raise torch._dynamo.exc.InternalTorchDynamoError( + "`wait` and `wait_tensor` do not take any arguments" + ) + from torch.distributed._functional_collectives import wait_tensor + + from .builder import wrap_fx_proxy + + return wrap_fx_proxy( + tx, + tx.output.create_proxy( + "call_function", wait_tensor, (self.as_proxy(),), {} + ), + ) + + # For historical reasons, these ops decompose down to syntactically + # invalid aten ops because they contain the python keyword `from`, see + # discussions in #151432 for more details. + # We graph break for now since this use case is uncommon. + if name == "random_": + unimplemented( + gb_type="Tensor.random_ op", + context=f"Tensor.{name}({args=}, {kwargs=})", + explanation="This is currently not supported.", + hints=[ + "Use the out-of-place version of this op", + *graph_break_hints.SUPPORTABLE, + ], + ) + elif name == "uniform_" and "from" in kwargs: + unimplemented( + gb_type="Tensor.uniform_ op called with `from` keyword", + context=f"Tensor.{name}({args=}, {kwargs=})", + explanation="This is currently not supported.", + hints=[ + "Avoid using the `from` keyword.", + *graph_break_hints.SUPPORTABLE, + ], + ) + + try: + handler_method = getattr(self, f"method_{name}") + except AttributeError: + pass + else: + try: + # Realize any LazyVariableTracker in kwargs before calling handler. + realized_kwargs = {k: v.realize() for k, v in kwargs.items()} + result = handler_method(tx, *args, **realized_kwargs) + if result: + return result + except TypeError as e: + unimplemented( + gb_type="Unhandled args for method", + context=f"call_method {self} {name} {args} {kwargs}", + explanation="Dynamo encountered an error while calling " + f"the method `{name}`.", + hints=[], + from_exc=e, + ) + + # Guard against unknown methods reaching the generic proxy path. + # For traceable wrapper subclasses (DTensor, NestedTensor), class_type + # is torch.Tensor, so check the example_value's actual type instead. + example_value = self.proxy.node.meta.get("example_value") + check_type = ( + type(example_value) if example_value is not None else self.class_type + ) + if not hasattr(check_type, name): + unimplemented( + gb_type="Unhandled tensor method", + context=f"call_method {self} {name} {args} {kwargs}", + explanation=f"Tensor method `{name}` is not defined on " + f"{check_type.__name__} and does not have an explicit " + "handler in TensorVariable.", + hints=[*graph_break_hints.SUPPORTABLE], + ) + + from .builder import wrap_fx_proxy + + proxy = tx.output.create_proxy( + "call_method", + name, + *proxy_args_kwargs([self, *args], kwargs), + ) + + # [Note: Inplace ops and VariableTracker metadata] + # For inplace operations, we need to propagate tensor metadata from the + # arguments to self. For example: + # x.add_(y) where y.requires_grad=True => x.requires_grad becomes True + # We detect inplace ops by checking if self's fake tensor version changes + # after wrap_fx_proxy (which runs get_fake_value internally). + # We only synchronize when there's a tensor argument, since that's when + # metadata propagation is relevant. + version_before = self._get_fake_version() + result = wrap_fx_proxy(tx, proxy) + self._sync_if_inplace_mutation( + tx, version_before, any(arg.is_tensor() for arg in args) + ) + + return result + + def method_size( + self, tx: "InstructionTranslator", *args: Any, **kwargs: Any + ) -> VariableTracker | None: + return self._method_size_stride("size", *args, **kwargs) + + def method_stride( + self, tx: "InstructionTranslator", *args: Any, **kwargs: Any + ) -> VariableTracker | None: + return self._method_size_stride("stride", *args, **kwargs) + + def _method_size_stride( + self, name: str, dim: Any | None = None + ) -> VariableTracker | None: + dim = guard_if_dyn(dim) + + def make_const_size_variable(x: Sequence[Any], **options: Any) -> SizeVariable: + return SizeVariable( + [ConstantVariable.create(y, **options) for y in x], **options + ) + + RetVariable = ( + make_const_size_variable if name == "size" else ConstantVariable.create + ) + + # Technically, this should not be necessary, but I'm including it + # for enhanced BC, in case example_value is sometimes not set + # (it really should always be set though!) + if name != "size": + r = getattr(self, name) + elif name == "size" and self.valid_size(): + r = self.size + else: + r = None + + if r is not None: + if dim is None: + return RetVariable(r) + else: + return ConstantVariable.create(r[dim]) + + # It might still be constant! Consult the fake tensor and see + if (fake := self.proxy.node.meta.get("example_value")) is not None: + if dim is None: + fake_r = getattr(fake, name)() + if not has_free_symbols(fake_r): + # int conversion for safety, in case a SymInt refined + # to constant + return RetVariable(tuple(int(r) for r in fake_r)) + else: + fake_r = getattr(fake, name)(dim) + if not has_free_symbols(fake_r): + return ConstantVariable.create(int(fake_r)) + return None + + def method_numel(self, tx: "InstructionTranslator") -> VariableTracker | None: + if self.valid_size(): + return VariableTracker.build(tx, product(self.size)) + + # It might still be constant! Consult the fake tensor and see + if (fake := self.proxy.node.meta.get("example_value")) is not None: + fake_r = fake.numel() + if not has_free_symbols(fake_r): + return VariableTracker.build(tx, int(fake_r)) + return None + + method_nelement = method_numel + + def method_dim(self, tx: "InstructionTranslator") -> VariableTracker | None: + if self.ndim is not None: + return VariableTracker.build(tx, self.ndim) + return None + + method_ndimension = method_dim + + def method_is_floating_point( + self, tx: "InstructionTranslator" + ) -> ConstantVariable | None: + if self.dtype is not None: + return VariableTracker.build(tx, self.dtype.is_floating_point) + return None + + def method_is_inference( + self, tx: "InstructionTranslator" + ) -> ConstantVariable | None: + if config.fake_tensor_disable_inference_mode: + unimplemented( + gb_type="Encountered tensor.is_inference() during tracing", + context="", + explanation="tensor.is_inference() is not supported", + hints=[ + *graph_break_hints.FUNDAMENTAL, + *graph_break_hints.INFERENCE_MODE, + ], + ) + if (fake := self.proxy.node.meta.get("example_value")) is not None: + return VariableTracker.build(tx, fake.is_inference()) + return None + + def method_is_complex(self, tx: "InstructionTranslator") -> ConstantVariable | None: + if self.dtype is not None: + return VariableTracker.build(tx, self.dtype.is_complex) + return None + + def method_is_contiguous( + self, tx: "InstructionTranslator", memory_format: VariableTracker | None = None + ) -> ConstantVariable | None: + memory_format_const = ( + memory_format.as_python_constant() + if memory_format is not None + else torch.contiguous_format + ) + if self.is_contiguous is not None: + # pyrefly: ignore[not-iterable] + return VariableTracker.build(tx, memory_format_const in self.is_contiguous) + elif (fake := self.proxy.node.meta.get("example_value")) is not None: + return VariableTracker.build( + tx, fake.is_contiguous(memory_format=memory_format_const) + ) + return None + + def method_type( + self, + tx: "InstructionTranslator", + dtype: Any | None = None, + non_blocking: bool = False, + **kwargs: Any, + ) -> VariableTracker | None: + if ( + dtype is None + and self.dtype is not None + and isinstance(self.device, torch.device) + ): + tensortype = next( + k for k, v in tensortype_to_dtype.items() if self.dtype in v + ) + if self.device.type == "cpu": + return VariableTracker.build(tx, f"torch.{tensortype.__name__}") + else: + return VariableTracker.build( + tx, f"torch.{self.device.type}.{tensortype.__name__}" + ) + elif ( + dtype is not None + and fqn(type(dtype.as_python_constant())) == "torch.tensortype" + ): + # torch.FloatTensor, etc. are all of type "torch.tensortype". + # torch.fx's tracer fails on these types, because it doesn't support arguments of torch.tensortype type. + # So, we pass it in as a string (which is also supported, see above implementation for .type() with 0 args) + tensor_type = dtype.as_python_constant() + tensor_type_const = VariableTracker.build(tx, fqn(tensor_type)) + + from .builder import wrap_fx_proxy + + if non_blocking: + kwargs = {"non_blocking": non_blocking, **kwargs} + + return wrap_fx_proxy( + tx, + tx.output.create_proxy( + "call_method", + "type", + *proxy_args_kwargs([self, tensor_type_const], kwargs), + ), + ) + return None + + def method_as_subclass( + self, tx: "InstructionTranslator", cls: VariableTracker + ) -> "TensorWithTFOverrideVariable": + if isinstance(cls, TensorSubclassVariable) and cls.source: + from .torch_function import TensorWithTFOverrideVariable + + py_cls = cls.as_python_constant() + var = TensorWithTFOverrideVariable.from_tensor_var( + tx, self, py_cls, cls.source + ) + # See NOTE [Side effect tracking for newly constructed tensor] + tx.output.side_effects._track_obj( + object(), var, mutation_type_cls=AttributeMutationNew + ) + return var + unimplemented( + gb_type="Argument of `as_subclass` must be a non-dispatcher-style tensor subclass", + context=f"{self}.as_subclass({cls})", + explanation="Currently not supported", + hints=[ + "Avoid this call or move it outside `torch.compile` regione", + *graph_break_hints.SUPPORTABLE, + ], + ) + + def method_get_device(self, tx: "InstructionTranslator") -> VariableTracker | None: + if isinstance(self.device, torch.device): + index = self.device.index if self.device.type != "cpu" else -1 + return VariableTracker.build(tx, index) + return None + + def method_element_size(self, tx: "InstructionTranslator") -> VariableTracker: + return VariableTracker.build(tx, self.dtype.itemsize) + + def method_numpy( + self, tx: "InstructionTranslator", *, force: VariableTracker | bool = False + ) -> "NumpyNdarrayVariable": + if not config.trace_numpy: + unimplemented( + gb_type="Tensor.numpy() with trace_numpy=False", + context=f"call_method {self} numpy", + explanation="`Tensor.numpy()` was called, but the `trace_numpy` " + "configuration was manually disabled.", + hints=[ + "Set `torch._dynamo.config.trace_numpy = True` to allow " + "Dynamo to trace through NumPy.", + ], + ) + if not np: + unimplemented( + gb_type="Tensor.numpy() without NumPy installed", + context=f"call_method {self} numpy", + explanation="`Tensor.numpy()` was called, but the NumPy library " + "is not available in the current environment.", + hints=[ + "Ensure NumPy is installed in your Python environment.", + ], + ) + if self.layout != torch.strided: + raise TypeError( + f"can't convert {self.layout} layout tensor to numpy. Use Tensor.to_dense() first" + ) + + # We don't check that the tensor is on CPU when force is False, as this + # allows us to execute NumPy code on CUDA. Same for requires_grad=True + if force and force.as_python_constant(): # type: ignore[attr-defined] + # If the user set force=True we try to preserve the semantics (no gradients, move to CPU...) + t = self.call_method(tx, "detach", [], {}) + proxy = tx.output.create_proxy("call_method", "cpu", (t.as_proxy(),), {}) + else: + # Hacky way to create a view of self that will be marked as NumpyNdarrayVariable + proxy = tx.output.create_proxy( + "call_method", "view_as", *proxy_args_kwargs([self, self], {}) + ) + return NumpyNdarrayVariable.create(tx, proxy) + + def method_tolist(self, tx: "InstructionTranslator") -> VariableTracker: + from .builder import wrap_fx_proxy + + def tolist(tensor: torch.Tensor, sub_proxy: torch.fx.Proxy) -> Any | list[Any]: + def wrap(i: Any, sub_proxy: torch.fx.Proxy) -> VariableTracker: + return wrap_fx_proxy( + tx, + sub_proxy.item(), + ) + + if tensor.dtype not in [ + torch.int8, + torch.int16, + torch.int32, + torch.int64, + ]: + unimplemented( + gb_type="Tensor.tolist() with non-integer tensor", + context=f"call_method {self} to_list", + explanation="Dynamo currently does not support tracing " + "`tolist()` on non-integer tensors.", + hints=[ + "Ensure the input tensor to `tolist()` is an integer " + "type (e.g., int8, int16, int32, int64)." + ], + ) + + if tensor.dim() == 0: + return wrap(tensor, sub_proxy) + + if tensor.dim() == 1: + return [wrap(val, sub_proxy[i]) for i, val in enumerate(tensor)] + + return [ + tolist(sub_tensor, sub_proxy=sub_proxy[i]) + for i, sub_tensor in enumerate(tensor) + ] + + tensor = self.as_proxy().node.meta["example_value"] + out = tolist(tensor, self.as_proxy()) + return VariableTracker.build(tx, out) + + def _collect_backward_inputs( + self, vars_iter: Iterable[VariableTracker], error_on_non_leaf: bool = False + ) -> list[VariableTracker] | None: + """ + Collect unique leaf tensors from vars_iter for backward. + + Only collects leaf tensors (no grad_fn). Non-leaf tensors are skipped + (or error if error_on_non_leaf=True) because when auto-detecting inputs, + we must not stop gradients at non-leafs - they are intermediates, and the + real leaf tensors (parameters) are further up the autograd graph. + + Deduplicates by proxy.node. + Returns list of unique leaf tensor variables. + """ + from ..source import SyntheticLocalSource + + result = [] + seen_nodes: set[torch.fx.Node] = set() + for var in vars_iter: + if isinstance(var, TensorVariable) and var.requires_grad: + # Non-leaf tensors (has_grad_fn=True) must be skipped because: + # 1. Semantically: they're intermediates, not the leaves we want gradients for + # 2. Implementation: accumulate_grad polyfill can't handle .grad on non-leafs + # (Dynamo creates GetAttrVariable instead of TensorVariable) + # + # In-graph created tensors without proper source also can't be handled + # when user explicitly passes them as inputs, because + # subguards_allowed() returns False for SyntheticLocalSource. + # However, in auto-detect mode (error_on_non_leaf=False), source-less + # leaves are valid backward targets — they gained requires_grad via + # requires_grad_() and accumulate_grad_ writes to .grad directly. + if var.has_grad_fn: + if error_on_non_leaf: + unimplemented( + gb_type="backward() with non-leaf tensor", + context=f"backward(inputs=[...]) with non-leaf tensor: {var}", + explanation="backward(inputs=[...]) with non-leaf tensors is not yet supported.", + hints=[ + "Only pass leaf tensors (parameters, graph inputs) to backward(inputs=...)", + ], + ) + elif error_on_non_leaf and ( + not var.source or isinstance(var.source, SyntheticLocalSource) + ): + unimplemented( + gb_type="backward() with in-graph created tensor", + context=f"backward(inputs=[...]) with in-graph created tensor: {var}", + explanation="backward(inputs=[...]) with tensors created inside the " + "compiled function is not yet supported.", + hints=[ + "Only pass tensors that are inputs to the compiled function or captured from outside", + ], + ) + else: + node = var.proxy.node + if node not in seen_nodes: + seen_nodes.add(node) + result.append(var) + # pyrefly: ignore [bad-return] + return result + + def method_backward( + self, + tx: "InstructionTranslator", + gradient: VariableTracker | None = None, + retain_graph: VariableTracker | None = None, + create_graph: VariableTracker | None = None, + inputs: VariableTracker | None = None, + ) -> VariableTracker | None: + """ + Trace tensor.backward() by rewriting as autograd.grad() + accumulate_grad. + + Implementation: + 1. Collect leaf tensors to compute gradients for + 2. Call autograd.grad(loss, inputs) to compute gradients + 3. For each leaf tensor, call accumulate_grad to update .grad + + Non-leaf tensor handling: + - Auto-detect (inputs=None): Non-leaf tensors are silently skipped. + This matches eager where only leaves get .grad. + - User-provided (inputs=[...]): Errors if any non-leaf tensor is found. + While eager backward(inputs=[non_leaf]) works, Dynamo cannot trace it + because the accumulate_grad polyfill accesses .grad, and Dynamo creates + a generic GetAttrVariable for .grad on non-leaf tensors (instead of a + TensorVariable), which cannot be used in tensor operations. + + TODO: Support non-leaf tensors by fixing .grad access on non-leaf in Dynamo. + """ + if not config.trace_autograd_ops: + unimplemented( + gb_type="Unsupported Tensor.backward() call", + context=f"call_method {self} backward {gradient} {retain_graph} {create_graph} {inputs}", + explanation="Dynamo currently does not support tracing `Tensor.backward()` when trace_autograd_ops is off.", + hints=["Set torch._dynamo.trace_autograd_ops=True"], + ) + + if not self.requires_grad and not self.has_grad_fn: + raise TorchRuntimeError( + "tensor does not require grad and does not have a grad_fn" + ) + + # Step 1: Collect leaf tensors to compute gradients for + # + # Note: We rely on the autograd.grad handler to validate that the generated + # autograd.grad call is legal (i.e., doesn't traverse external grad_fns). + # If the loss depends on leaves we don't know about, the autograd.grad + # handler will catch it via the external_grad_fns check. + auto_detect = inputs is None + if auto_detect: + # Sources can be either user inputs (params are included here) + # or parameters that are created in forward. + all_vars = chain( + tx.output.leaf_var_creation_order, + tx.output.input_source_to_var.values(), + ) + input_vars = self._collect_backward_inputs(all_vars) + if not input_vars: + # No leaf tensors found - nothing to accumulate gradients into. + # This matches eager behavior where backward() is a no-op if there + # are no leaves requiring grad. + return ConstantVariable.create(None) + else: + provided_vars = ( + inputs.items + if isinstance(inputs, variables.BaseListVariable) + else [inputs] + ) + input_vars = self._collect_backward_inputs( + provided_vars, error_on_non_leaf=True + ) + if not input_vars: + # User explicitly provided inputs but none were valid leaf tensors. + # This would cause "grad requires non-empty inputs" error at runtime. + unimplemented( + gb_type="backward() with empty inputs", + context="backward(inputs=[...]) resulted in no valid leaf tensors", + explanation="backward(inputs=[...]) requires at least one valid leaf tensor.", + hints=[ + "Ensure at least one tensor in inputs is a leaf (requires_grad=True, no grad_fn)", + ], + ) + + # Build autograd.grad call + grad_kwargs = {"allow_unused": VariableTracker.build(tx, auto_detect)} + if retain_graph is not None: + grad_kwargs["retain_graph"] = retain_graph + if create_graph is not None: + grad_kwargs["create_graph"] = create_graph + + inputs_var = VariableTracker.build(tx, input_vars) + grad_args = [self, inputs_var] + if gradient is not None: + grad_args.append(gradient) + + autograd_grad_fn = VariableTracker.build(tx, torch.autograd.grad) + grads_var = autograd_grad_fn.call_function(tx, grad_args, grad_kwargs) + + # Accumulate gradients for unique leaf tensors under no_grad context + # to replicate eager autograd engine. + from .ctx_manager import GradModeVariable + + grad_mode_var = GradModeVariable.create(tx, False, initialized=True) + grad_mode_var.enter(tx) + + accumulate_grad_fn = VariableTracker.build( + tx, torch.ops.inductor.accumulate_grad_.default + ) + assert input_vars is not None + for idx, input_var in enumerate(input_vars): + grad_i = grads_var.call_method( + tx, "__getitem__", [VariableTracker.build(tx, idx)], {} + ) + accumulate_grad_fn.call_function(tx, [input_var, grad_i], {}) + + grad_mode_var.exit(tx) + + return ConstantVariable.create(None) + + def method_data_ptr( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> "DataPtrVariable": + return DataPtrVariable(self) + + def method_record_stream( + self, + tx: "InstructionTranslator", + stream: VariableTracker, + ) -> VariableTracker: + from .streams import StreamVariable + + if not isinstance(stream, StreamVariable): + raise RuntimeError( + f"record_stream() expects a Stream argument, got {stream.python_type().__name__}" + ) + tx.output.create_proxy( + "call_function", + torch.ops.streams.record_stream, + (self.as_proxy(), stream.user_object_index), + {}, + ) + return ConstantVariable.create(None) + + def method_item( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> None: + # We enable capture_scalar_outputs when full_graph=True by default. + if not tx.one_graph and not config.capture_scalar_outputs: + self._warn_capture_scalar_outputs() + unimplemented( + gb_type="Unsupported Tensor.item() call with capture_scalar_outputs=False", + context=f"call_method {self} item {args} {kwargs}", + explanation="Dynamo does not support tracing `Tensor.item()` " + "with config.capture_scalar_outputs=False.", + hints=[ + "Set `torch._dynamo.config.capture_scalar_outputs = True` " + "or `export TORCHDYNAMO_CAPTURE_SCALAR_OUTPUTS=1` " + "to include these operations in the captured graph.", + ], + ) + return None + + def nb_index_impl( + self, + tx: "InstructionTranslator", + ) -> VariableTracker: + # CPython: only integer tensors of a single element can be converted + # to an index. Mirrors THPVariable_index_scalar in + # https://github.com/pytorch/pytorch/blob/7cfd054075b/tools/autograd/templates/python_variable_methods.cpp#L372-L385 + if self.dtype is not None and ( + not self.dtype.is_floating_point and not self.dtype.is_complex + ): + item = self.call_method(tx, "item", [], {}) + from .builder import wrap_fx_proxy + + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + sym_int, + (item.as_proxy(),), + {}, + ), + ) + raise_observed_exception( + TypeError, + tx, + args=[ + "only integer tensors of a single element can be converted to an index" + ], + ) + + def nb_int_impl( + self, + tx: "InstructionTranslator", + ) -> VariableTracker: + # CPython: THPVariable_integral_scalar handles both integer and float + # tensors (floats are truncated to int). Complex tensors raise + # RuntimeError at runtime. + if self.dtype is not None and self.dtype.is_complex: + raise_observed_exception( + RuntimeError, + tx, + args=["value cannot be converted to type int64_t without overflow"], + ) + # For known non-complex dtypes and unknown dtype (None), emit the + # proxy and let it fail at runtime if the dtype is unsupported. + item = self.call_method(tx, "item", [], {}) + from .builder import wrap_fx_proxy + + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + sym_int, + (item.as_proxy(),), + {}, + ), + ) + + def method___int__(self, tx: "InstructionTranslator") -> VariableTracker: + return self.nb_int_impl(tx) + + def nb_float_impl( + self, + tx: "InstructionTranslator", + ) -> VariableTracker: + # CPython: THPVariable_float_scalar dispatches to double. + # Complex tensors raise RuntimeError at runtime. + if self.dtype is not None and self.dtype.is_complex: + raise_observed_exception( + RuntimeError, + tx, + args=["value cannot be converted to type double without overflow"], + ) + item = self.call_method(tx, "item", [], {}) + from .builder import wrap_fx_proxy + + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + sym_float, + (item.as_proxy(),), + {}, + ), + ) + + def method___float__(self, tx: "InstructionTranslator") -> VariableTracker: + return self.nb_float_impl(tx) + + def method___getitem__( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + from .builder import wrap_fx_proxy + + if isinstance(args[0], SymNodeVariable): + # Standard indexing will force specialization due to + # __index__. Rewrite as a regular torch op which will + # trace fine + fn, args = ( # type: ignore[assignment] + torch.select, + [ + VariableTracker.build(tx, 0), + args[0], + ], + ) + else: + fn = operator.getitem + + proxy = tx.output.create_proxy( + "call_function", + fn, + *proxy_args_kwargs([self] + list(args), kwargs), + ) + + return wrap_fx_proxy(tx, proxy) + + @staticmethod + @functools.cache + def _warn_capture_scalar_outputs() -> None: + user_stack = torch._guards.TracingContext.extract_stack() + user_stack_formatted = "".join(traceback.format_list(user_stack)) + log.warning( + textwrap.dedent( + """\ + Graph break from `Tensor.item()`, consider setting: + torch._dynamo.config.capture_scalar_outputs = True + or: + env TORCHDYNAMO_CAPTURE_SCALAR_OUTPUTS=1 + to include these operations in the captured graph. + + Graph break: from user code at: + %s + """ + ), + user_stack_formatted, + ) + + def method___len__(self, tx: "InstructionTranslator") -> VariableTracker: + return self.sq_length(tx) + + def sq_length(self, tx: "InstructionTranslator") -> VariableTracker: + """Sequence length for tensors (size along first dimension).""" + return self.call_method(tx, "size", [VariableTracker.build(tx, 0)], {}) + + def method___iter__(self, tx: "InstructionTranslator") -> ListIteratorVariable: + return ListIteratorVariable( + self.unpack_var_sequence(tx), mutation_type=ValueMutationNew() + ) + + def method_addcmul_( + self, + tx: "InstructionTranslator", + tensor1: Any, + tensor2: Any, + *, + value: Any | None = None, + ) -> Any | None: + if value is not None and config.enable_dynamo_decompositions: + from torch._inductor import inductor_prims + + mul_var = variables.TorchInGraphFunctionVariable(torch.mul) + fma_var = variables.TorchInGraphFunctionVariable(inductor_prims.fma) + product = mul_var.call_function(tx, [tensor1, tensor2], {}) + result = fma_var.call_function(tx, [product, value, self], {}) + return self.call_method(tx, "copy_", [result], {}) + return None + + def method___setitem__( + self, + tx: "InstructionTranslator", + key: VariableTracker, + value: VariableTracker, + ) -> VariableTracker: + proxy = tx.output.create_proxy( + "call_function", + operator.setitem, + *proxy_args_kwargs([self, key, value], {}), + ) + + # See Note [Inplace ops and VariableTracker metadata] + # __setitem__ is always an inplace operation. We need to run fake execution + # and propagate metadata if self was mutated. + # The context managers handle saved tensor hooks and unbacked symbols. + version_before = self._get_fake_version() + + with ( + torch._dynamo.utils._disable_saved_tensors_hooks_during_tracing(), + tx.fake_mode.shape_env.ignore_fresh_unbacked_symbols() + if tx.fake_mode and tx.fake_mode.shape_env + else nullcontext(), + ): + get_fake_value(proxy.node, tx, allow_non_graph_fake=False) + + self._sync_if_inplace_mutation(tx, version_before, value.is_tensor()) + + if config.use_graph_deduplication or config.track_nodes_for_deduplication: + tx.output.region_tracker.add_node_mutation(proxy.node, 0) + + return ConstantVariable.create(None) + + def method_resize_( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> NoReturn: + unimplemented( + gb_type="Unsupported Tensor.resize_() call", + context=f"call_method {self} resize_ {args} {kwargs}", + explanation="Dynamo currently does not support tracing `Tensor.resize_()`.", + hints=[], + ) + + def method_resize_as_( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> NoReturn: + unimplemented( + gb_type="Unsupported Tensor.resize_as_() call", + context=f"call_method {self} resize_as_ {args} {kwargs}", + explanation="Dynamo currently does not support tracing `Tensor.resize_as_()`.", + hints=[], + ) + + def method_sparse_resize_( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> NoReturn: + unimplemented( + gb_type="Unsupported Tensor.sparse_resize_() call", + context=f"call_method {self} sparse_resize_ {args} {kwargs}", + explanation="Dynamo currently does not support tracing `Tensor.sparse_resize_()`.", + hints=[], + ) + + def method_sparse_resize_and_clear_( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> NoReturn: + unimplemented( + gb_type="Unsupported Tensor.sparse_resize_and_clear_() call", + context=f"call_method {self} sparse_resize_and_clear_ {args} {kwargs}", + explanation="Dynamo currently does not support tracing `Tensor.sparse_resize_and_clear_()`.", + hints=[], + ) + + def method_set_( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> None: + if len(args) > 1: + # torch.Tensor.set_() has several overloads. + # aten::set_.source_Tensor(Tensor) gets special handling + # in AOTAutograd and functionalization, because it is the most common + # overload and is used by FSDP. + # graph-breaking on aten::set_source_Tensor_storage_offset for now, + # unless we find that we need to make it work. + unimplemented( + gb_type="Unsupported Tensor.set_() call", + context=f"call_method {self} set_ {args} {kwargs}", + explanation="Dynamo currently does not support tracing `Tensor.set_()` " + "overloads that include more than one argument.", + hints=[*graph_break_hints.SUPPORTABLE], + ) + return None + + def method_add_( + self, + tx: "InstructionTranslator", + other: VariableTracker, + *, + alpha: VariableTracker | None = None, + ) -> VariableTracker | None: + # Tensor alpha: ATen would call .item() and fma internally. + # Use fma directly to avoid the item() graph break. + if ( + alpha is not None + and isinstance(alpha, TensorVariable) + and config.enable_dynamo_decompositions + ): + from torch._inductor import inductor_prims + + fma_var = variables.TorchInGraphFunctionVariable(inductor_prims.fma) + result = fma_var.call_function(tx, [other, alpha, self], {}) + return self.call_method(tx, "copy_", [result], {}) + return None + + def method_addcdiv_( + self, + tx: "InstructionTranslator", + tensor1: "TensorVariable", + tensor2: "TensorVariable", + *, + value: VariableTracker | None = None, + ) -> VariableTracker | None: + if value is not None and config.enable_dynamo_decompositions: + result = variables.TorchInGraphFunctionVariable(torch.div).call_function( + tx, [tensor1, tensor2], {} + ) + from torch._inductor import inductor_prims + + fma_var = variables.TorchInGraphFunctionVariable(inductor_prims.fma) + fma_result = fma_var.call_function(tx, [result, value, self], {}) + return self.call_method(tx, "copy_", [fma_result], {}) + return None + + def method___contains__( + self, tx: "InstructionTranslator", arg: VariableTracker + ) -> VariableTracker: + # Rewrite __contains__ here so that downstream passes can trace through + # without dealing with unbacked symbool. Roughly the code we translate is: + # def __contains__(self, x): + # return (x == self).any().item() + result = variables.TorchInGraphFunctionVariable(torch.eq).call_function( + tx, [self, arg], {} + ) + result = variables.TorchInGraphFunctionVariable(torch.any).call_function( + tx, [result], {} + ) + return result.call_method(tx, "item", [], {}) + + def method_register_hook( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + return self._method_register_hook(tx, "register_hook", *args, **kwargs) + + def method_register_post_accumulate_grad_hook( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + return self._method_register_hook( + tx, "register_post_accumulate_grad_hook", *args, **kwargs + ) + + def _method_register_hook( + self, tx: "InstructionTranslator", name: str, hook: VariableTracker + ) -> VariableTracker: + # Note - do not arbitrarily add hooks here - make sure they match the same contract + # see [On tensor.register_hook] + + if not self.source: + # For intermediate tensors (those without a source), we have two approaches: + # 1. When compiled autograd is enabled: use BackwardState to defer hook execution + # 2. When compiled autograd is NOT enabled: use a custom autograd function + if compiled_autograd.compiled_autograd_enabled: + # Use the BackwardState approach for compiled autograd + hook_name, bw_state_proxy = tx.output.add_backward_state_hook(hook) + + def _register_hook_trampoline( + tensor: torch.Tensor, bw_state: compiled_autograd.BackwardState + ) -> None: + register_hook = getattr(tensor, name) + register_hook( + functools.partial( + trace_wrapped, + fn=call_hook_from_backward_state, + bw_state=bw_state, + hook_name=hook_name, + ) + ) + # TODO(jansel): returning None here is wrong, it should be + # RemovableHandle, but we need some extra work to support + # this properly. + return None + + from .builder import wrap_fx_proxy + + self_proxy = self.as_proxy() + self_proxy.node.meta["has_backward_hook"] = True + + return wrap_fx_proxy( + tx, + tx.output.create_proxy( + "call_function", + _register_hook_trampoline, + (self_proxy, bw_state_proxy), + {}, + ), + ) + # ----------Handling intermediate tensor custom hooks------ + # Rewrite intermediate tensor hook as custom autograd function + # Given: + # glb_list = [] + # glb_dict = {} + # + # def fn(x): + # y = x * 2 + # glb_list.append(y) + # glb_dict['tensor'] = y + # a = glb_list[0] * 3 # Should use output of register_hook + # b = glb_dict['tensor'] + 1 # Should use hooked_y + # y.register_hook(lambda grad: grad + 1) + # return (a + b).sum() + # We basically want to replace y.register_hook(lambda grad: grad + 1) with + # custom autograd function where the forward is just identity while backward + # calls custom hook. + # The algo works by: + # 1. When we see a hook, create a node with custom autograd function apply (y') + # 2. Move the custom autograd node just after definition of the intermediate tensor (y in above), + # THEN update references to y with y'. + # As a result of this algo, above example turns into: + # def fn(x): + # y = x * 2 + # y_prime = custom_autograd_function.apply() + # glb_list.append(y_prime) + # glb_dict['tensor'] = y_prime + # a = glb_list[0] * 3 + # b = glb_dict['tensor'] + 1 + # return (a + b).sum() + # Get the original tensor's node and save its current users + tensor_node = self.as_proxy().node + + users_to_replace = list(tensor_node.users.keys()) + + # Create the ApplyBackwardHook call + apply_hook_var = variables.AutogradFunctionVariable(_ApplyBackwardHook) + result = apply_hook_var.call_apply(tx, [self, hook], {}) + + # Get the hooked tensor's node (this is the getitem node) + tensor_prime_node = result.as_proxy().node + + # DFS to collect all nodes that tensor_prime_node depends on, + # stopping at tensor_node. These are the nodes we need to move right + # after tensor_node. + nodes_to_move: list[torch.fx.Node] = [] + visited: set[torch.fx.Node] = set() + + def collect_deps(node: torch.fx.Node, stop_at: torch.fx.Node) -> None: + if node in visited or node is stop_at: + return + for arg in node.args: + if isinstance(arg, torch.fx.Node): + collect_deps(arg, stop_at) + for kwarg in node.kwargs.values(): + if isinstance(kwarg, torch.fx.Node): + collect_deps(kwarg, stop_at) + visited.add(node) + nodes_to_move.append(node) + + collect_deps(tensor_prime_node, tensor_node) + + # Move each node to right after tensor_node using node.append() + insert_point = tensor_node + for node in nodes_to_move: + insert_point.append(node) + insert_point = node + + # Replace uses of tensor with tensor_prime, but only for the users + # that existed before we created the hook node + for user in users_to_replace: + user.replace_input_with(tensor_node, tensor_prime_node) + + # Update tensor to point to the tensor_prime + assert isinstance(result, TensorVariable) + self.proxy = result.as_proxy() + # TensorVariable doesn't actually store the grad_fn + # so this is fine. + self.synchronize_attributes(tx) + + # Return a RemovableHandleVariable for API compatibility + # can't fall through because side_effects.register_hook + # require source. + return variables.RemovableHandleVariable( + mutation_type=variables.base.ValueMutationNew(), + ) + + handle_variable = variables.RemovableHandleVariable( + mutation_type=variables.base.ValueMutationNew(), + ) + tx.output.side_effects.register_hook(self, hook, handle_variable, name) + return handle_variable + + def method_requires_grad_( + self, tx: "InstructionTranslator", requires_grad: bool | VariableTracker = True + ) -> VariableTracker: + if requires_grad is not True: + requires_grad = requires_grad.as_python_constant() # type: ignore[attr-defined] + + node = self.as_proxy().node + example_value = node.meta["example_value"] + if example_value.requires_grad != requires_grad: + # For graph inputs (tensors with source), requires_grad_() is a + # metadata mutation that we can't trace — graph break as before. + if self.source: + unimplemented( + gb_type="Unsupported Tensor.requires_grad_() call", + context=f"call_method {self} requires_grad_", + explanation="Dynamo does not support changes to a Tensor's " + "`requires_grad` through calling `requires_grad_()`.", + hints=[], + ) + # On a previous attempt, we traced through requires_grad_() but + # discovered at compile time that the tainted intermediate leaked + # as a graph output. Graph break here to preserve partial + # acceleration for code before requires_grad_(). + if tx.speculation_log.graph_break_on_requires_grad_: + unimplemented( + gb_type="requires_grad_() intermediate leaked as output", + context=f"call_method {self} requires_grad_", + explanation="An intermediate tensor with requires_grad_() called " + "on it (or a tensor derived from it) is returned from the " + "compiled region. Graph breaking here to preserve partial " + "acceleration.", + hints=[ + "Call .detach() before returning if you only need values.", + "Consume the gradient inside the compiled function " + "(call backward() and use .grad), " + "or move requires_grad_() outside torch.compile.", + ], + ) + # AOTAutograd re-traces the FX graph under functorch transforms + # (functionalization). Functorch's checkSupportsInplaceRequiresGrad() + # rejects requires_grad_() when the dynamic layer stack is non-empty. + # We wrap the call with set_inplace_requires_grad_allowed(True) to + # bypass this check, matching GradInplaceRequiresGradCtxManagerVariable + # in ctx_manager.py (which handles the explicit context manager case). + # + # Lines below do two things in parallel: + # 1. Mutate trace-time state so example_value.requires_grad_() works + # 2. Emit FX nodes so the same toggle happens during AOTAutograd re-trace + prev_state = torch._C._functorch.get_inplace_requires_grad_allowed() + torch._C._functorch.set_inplace_requires_grad_allowed(True) + try: + tx.output.create_node( + "call_function", + torch._C._functorch.set_inplace_requires_grad_allowed, + (True,), + {}, + ) + tx.output.create_proxy( + "call_method", + "requires_grad_", + (self.as_proxy(),), + {}, + ) + tx.output.create_node( + "call_function", + torch._C._functorch.set_inplace_requires_grad_allowed, + (prev_state,), + {}, + ) + finally: + torch._C._functorch.set_inplace_requires_grad_allowed(prev_state) + example_value.requires_grad_(requires_grad) + self.requires_grad = requires_grad + if requires_grad: + tx.output.leaf_var_creation_order.append(self) + # For source-less intermediates, initialize .grad = None in + # side effects so the accumulate_grad polyfill can read/write + # .grad naturally. Graph inputs don't need this — they handle + # .grad through their source. + if not self.source and tx.output.side_effects.is_attribute_mutation( + self + ): + tx.output.side_effects.store_attr( + self, "grad", variables.ConstantVariable.create(None) + ) + return self + + def method_detach_(self, tx: "InstructionTranslator") -> "TensorVariable": + from .builder import wrap_fx_proxy + + proxy = tx.output.create_proxy( + "call_method", + "detach_", + (self.as_proxy(),), + {}, + ) + # Run the fake op so the proxy metadata reflects the detached tensor state. + wrap_fx_proxy(tx, proxy) + self.synchronize_attributes(tx) + return self + + def method_share_memory_(self) -> NoReturn: + unimplemented( + gb_type="Unsupported Tensor.share_memory_() call", + context=f"call_method {self} share_memory_", + explanation="Dynamo does not support Tensor.share_memory_() which modifies tensor storage for IPC", + hints=[ + "Move share_memory_() call outside the compiled region. ", + *graph_break_hints.SUPPORTABLE, + ], + ) + + def method_new( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker | None: + # Convert x.new(torch.Size) into x.new_empty(torch.Size), + # as Tensor.new acts differently with a Size input versus a tuple input. + if (len(args) == 1 and isinstance(args[0], SizeVariable)) or ( + len(args) >= 1 + and all( + a.is_python_constant() and isinstance(a.as_python_constant(), int) + for a in args + ) + ): + return self.call_method(tx, "new_empty", args, kwargs) + return None + + def method_new_tensor( + self: "TensorVariable", + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker | None: + if len(args) != 1: + return None + data_arg = args[0] + layout = kwargs.get("layout") + if layout is not None: + if not layout.is_python_constant(): + return None + if layout.as_python_constant() != torch.strided: + return None + fwd_kwargs = dict(kwargs) + fwd_kwargs.pop("layout", None) + fwd_kwargs.setdefault("dtype", self.var_getattr(tx, "dtype")) + fwd_kwargs.setdefault("device", self.var_getattr(tx, "device")) + return variables.TorchInGraphFunctionVariable(torch.tensor).call_function( + tx, + [data_arg], + fwd_kwargs, + ) + + def method_untyped_storage( + self, tx: "InstructionTranslator" + ) -> "UntypedStorageVariable": + return UntypedStorageVariable( + self, self.as_proxy().node.meta["example_value"].untyped_storage() + ) + + def set_name_hint(self, name: str) -> None: + if not self._is_name_set: + self.proxy.node._rename(name) + self._is_name_set = True + return None + + def is_python_hashable(self) -> bool: + # Tensors are hashable if they have an example_value (a fake tensor) + # Most VT's should have one. + # It'd be nice if at some point we could assert that they all have one + return self.as_proxy().node.meta["example_value"] is not None + + def get_python_hash(self) -> int: + return hash(self.as_proxy().node.meta["example_value"]) + + def is_python_equal(self, other: object) -> bool: + if not isinstance(other, VariableTracker): + return False + a = self.as_proxy().node.meta["example_value"] + b = other.as_proxy().node.meta["example_value"] + return a is b + + +class SymNodeVariable(VariableTracker): + """ + Represents a symbolic scalar, either int, float or bool. This is most commonly used to + handle symbolic size computation, e.g., tensor.size(0), but it is also used to + handle logic like float_tensor.item() or unspecialized float inputs. + """ + + _nonvar_fields = { + "proxy", + "sym_num", + *VariableTracker._nonvar_fields, + } + + def debug_repr(self) -> str: + return repr(self.sym_num) + + @classmethod + def create( + cls, + tx: "InstructionTranslatorBase", + proxy: Any, + sym_num: Any | None = None, + **options: Any, + ) -> "VariableTracker": + if sym_num is None: + sym_num = get_fake_value(proxy.node, tx) + if "example_value" in proxy.node.meta: + assert proxy.node.meta["example_value"] == sym_num + set_example_value(proxy.node, sym_num) + + if isinstance(sym_num, (sympy.Integer, int, bool)): + sym_num = int(sym_num) if isinstance(sym_num, sympy.Integer) else sym_num + return VariableTracker.build(tx, sym_num) + + out = SymNodeVariable(proxy, sym_num, **options) + if proxy.node.op != "placeholder": + tx.output.current_tracer.record_proxyable_vt(out) + return out + + def __init__(self, proxy: Any, sym_num: Any, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.proxy = proxy + # TODO: Should we allow non SymTypes here? Today it is allowed + self.sym_num = sym_num + self._tensor_var: TensorVariable | None = None + + def python_type(self) -> type: + if isinstance(self.sym_num, SymTypes): + return self.sym_num.node.pytype + else: + return type(self.sym_num) + + def is_symnode_like(self) -> bool: + return True + + def as_proxy(self) -> Any: + return self.proxy + + def bool_impl( + self, + tx: "InstructionTranslatorBase", + ) -> VariableTracker: + # long_bool / float_bool: non-zero check. SymBool is already boolean. + # https://github.com/python/cpython/blob/c09ccd9c429/Objects/longobject.c#L5200 + # https://github.com/python/cpython/blob/c09ccd9c429/Objects/floatobject.c#L853 + if isinstance(self.sym_num, torch.SymBool): + return self + assert isinstance(self.sym_num, (torch.SymInt, torch.SymFloat)) + return SymNodeVariable.create(tx, self.as_proxy() != 0) + + def as_tensor(self, tx: "InstructionTranslatorBase", dtype: Any) -> TensorVariable: + if self._tensor_var is None: + self._tensor_var = VariableTracker.build( + tx, torch.scalar_tensor + ).call_function(tx, [self], {"dtype": VariableTracker.build(tx, dtype)}) + return self._tensor_var + + def evaluate_expr( + self, output_graph: Optional["OutputGraph"] = None + ) -> bool | int | float: + try: + return guard_scalar(self.sym_num) + except GuardOnDataDependentSymNode as e: + if torch.fx.experimental._config.no_data_dependent_graph_break: + raise + + raise UserError( # noqa: B904 + UserErrorType.ANTI_PATTERN, + f"Consider annotating your code using torch._check*(). {str(e)}", + case_name="constrain_as_size_example", + ) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .builder import wrap_fx_proxy + + return wrap_fx_proxy( + tx, + tx.output.create_proxy( + "call_method", + name, + *proxy_args_kwargs([self, *args], kwargs), + ), + ) + + def nb_int_impl( + self, + tx: "InstructionTranslator", + ) -> VariableTracker: + # SymInt.__int__: https://github.com/pytorch/pytorch/blob/ee336ca5440939b8ad65e916d47421f849e56178/torch/__init__.py#L462 + # SymFloat.__int__: https://github.com/pytorch/pytorch/blob/ee336ca5440939b8ad65e916d47421f849e56178/torch/__init__.py#L682 + # SymBool.__int__: https://github.com/pytorch/pytorch/blob/ee336ca5440939b8ad65e916d47421f849e56178/torch/__init__.py#L784 + from .builder import wrap_fx_proxy + + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + sym_int, + (self.as_proxy(),), + {}, + ), + ) + + def method___int__( + self, tx: "InstructionTranslator", *args: Any, **kwargs: Any + ) -> VariableTracker: + return self.nb_int_impl(tx) + + def nb_float_impl( + self, + tx: "InstructionTranslator", + ) -> VariableTracker: + # SymFloat.__float__: https://github.com/pytorch/pytorch/blob/ee336ca5440939b8ad65e916d47421f849e56178/torch/__init__.py#L679 + from .builder import wrap_fx_proxy + + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + sym_float, + (self.as_proxy(),), + {}, + ), + ) + + def method___float__( + self, tx: "InstructionTranslator", *args: Any, **kwargs: Any + ) -> VariableTracker: + return self.nb_float_impl(tx) + + def is_python_hashable(self) -> bool: + return True + + def get_python_hash(self) -> int: + # Essentially convert the SymNode to a constant variable whenever its + # searched for a dict key. + return hash(self.evaluate_expr()) + + def is_python_equal(self, other: object) -> bool: + if isinstance(other, SymNodeVariable): + return self.evaluate_expr() == other.evaluate_expr() + # could be constant variable as well + return ( + isinstance(other, VariableTracker) + and self.evaluate_expr() == other.as_python_constant() + ) + + +class NumpyNdarrayVariable(TensorVariable): + """ + Represents a np.ndarray, but backed by torch Tensor via torch._numpy.ndarray. + Use this for Tensor.numpy() call. + """ + + @staticmethod + def create( + tx: "InstructionTranslator", proxy: torch.fx.Proxy, **options: Any + ) -> "NumpyNdarrayVariable": + from .builder import wrap_fx_proxy_cls + + return wrap_fx_proxy_cls( + target_cls=NumpyNdarrayVariable, + tx=tx, + proxy=proxy, + **options, + ) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + # NB: This INTENTIONALLY does not call super(), because there is + # no intrinsic reason ndarray properties are related to Tensor + # properties. The inheritance here is for implementation sharing. + + from ..utils import numpy_attr_wrapper + from .builder import wrap_fx_proxy + + result = None + + example_value = self.as_proxy().node.meta["example_value"] + example_ndarray = tnp.ndarray(example_value) + + def insert_into_graph() -> VariableTracker: + return wrap_fx_proxy( + tx, + tx.output.create_proxy( + "call_function", numpy_attr_wrapper, (self.as_proxy(), name), {} + ), + ) + + if name in ["T", "real", "imag", "flat"]: + proxy = tx.output.create_proxy( + "call_function", + numpy_attr_wrapper, + (self.as_proxy(), name), + {}, + ) + result = NumpyNdarrayVariable.create(tx, proxy) + + # These are awkward to implement. The standard playbook for torch._numpy + # interop is to trace a call into the torch._numpy wrapper which works for + # Tensor operations. However, we don't want to do this for calls + # that don't return Tensors, because in those cases we may not want + # to trace the attribute access into the graph at all (it is sort + # of harmless to do so, because AOTAutograd will eliminate them, + # but it's best not to trace them in to begin with.) But in any + # case, tracing these into the graph is like trying to fit a square + # peg into a round hole; best not to do it. So instead we + # painstakingly implement these by hand + # + # NB: only ALWAYS specialized attributes can go here; notably, + # size/shape not allowed! + elif name in ("ndim", "itemsize"): + return VariableTracker.build(tx, getattr(example_ndarray, name)) + elif name in ("shape", "stride"): + if not has_free_symbols(r := getattr(example_ndarray, name)): + return VariableTracker.build(tx, tuple(int(r) for r in r)) + return insert_into_graph() + elif name == "size": + if not has_free_symbols(r := example_ndarray.size): + return VariableTracker.build(tx, int(r)) + return insert_into_graph() + elif name in ["base", "flags", "dtype"]: + unimplemented( + gb_type="Unsupported ndarray attribute access", + context=f"var_getattr {self} {name}", + explanation=f"Dynamo currently does not support tracing `ndarray.{name}`.", + hints=[], + ) + elif name == "__version__": + unimplemented( + gb_type="Unsupported ndarray.__version__ access", + context=f"var_getattr {self} {name}", + explanation=f"Dynamo currently does not support tracing `ndarray.{name}`.", + hints=[], + ) + if result is None: + raise NotImplementedError + return result + + @staticmethod + def patch_args( + name: str, args: Sequence[VariableTracker], kwargs: dict[str, VariableTracker] + ) -> tuple[Sequence[VariableTracker], dict[str, VariableTracker]]: + if name == "clip": + kwargs_rename = {"a_min": "min", "a_max": "max"} + kwargs = {kwargs_rename.get(k, k): v for k, v in kwargs.items()} + return args, kwargs + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from ..exc import unimplemented + from ..utils import numpy_method_wrapper + + args, kwargs = self.patch_args(name, args, kwargs) + + if name == "astype": + from .builtin import BuiltinVariable + + dtype_arg = None + if "dtype" in kwargs: + dtype_arg = kwargs["dtype"] + elif len(args) > 0: + dtype_arg = args[0] + is_object_str = dtype_arg is not None and dtype_arg.is_constant_match("O") + is_object_type = ( + isinstance(dtype_arg, BuiltinVariable) and dtype_arg.fn is object + ) + if is_object_str or is_object_type: + unimplemented( + gb_type="ndarray.astype(object)", + context=f"call_method {self} {name} {args} {kwargs}", + explanation=( + "`ndarray.astype('O')` or `ndarray.astype(object)` is not supported " + "by torch.compile, as there is no equivalent to object type in torch.Tensor. " + "This will be executed eagerly." + ), + hints=[*graph_break_hints.FUNDAMENTAL], + ) + if name in ["__len__", "size", "tolist", "__iter__"]: + # delegate back to TensorVariable + return super().call_method(tx, name, args, kwargs) + if name in ("tostring", "tobytes", "__delattr__"): + unimplemented( + gb_type="Unsupported ndarray method call", + context=f"call_method {self} {name} {args} {kwargs}", + explanation=f"`ndarray.{name}()` is not modelled in `torch._numpy`.", + hints=[], + ) + proxy = tx.output.create_proxy( + "call_function", + numpy_method_wrapper(name), + *proxy_args_kwargs([self] + list(args), kwargs), + ) + return NumpyNdarrayVariable.create(tx, proxy) + + def python_type(self) -> type: + if np is not None: + return np.ndarray + else: + return NoneType + + +class UnspecializedPythonVariable(TensorVariable): + """ + This is a 1-element tensor represents unspecialized python float/int. + """ + + _nonvar_fields = { + "raw_value", + "need_unwrap", + *TensorVariable._nonvar_fields, + } + + def __init__( + self, + proxy: torch.fx.Proxy, + *, + raw_value: float | int | None = None, + need_unwrap: bool = True, + **kwargs: Any, + ) -> None: + super().__init__(proxy, **kwargs) + self.raw_value = raw_value + self.need_unwrap = need_unwrap + + @classmethod + def from_tensor_variable( + cls, + tensor_variable: TensorVariable, + raw_value: float | int | None, + need_unwrap: bool = True, + ) -> "UnspecializedPythonVariable": + # Convert a `TensorVariable` instance into an `UnspecializedPythonVariable` instance. + return UnspecializedPythonVariable( + **dict(tensor_variable.__dict__), + raw_value=raw_value, + need_unwrap=need_unwrap, + ) + + +class FakeItemVariable(TensorVariable): + """An unspecialized python variable which prevents access to the underlying raw value. + This is needed if item is called on a FakeTensor.""" + + _nonvar_fields = { + "need_unwrap", + *TensorVariable._nonvar_fields, + } + + def __init__(self, proxy: torch.fx.Proxy, **kwargs: Any) -> None: + need_unwrap = kwargs.pop("need_unwrap", False) + super().__init__(proxy, **kwargs) + self.need_unwrap = need_unwrap + + @classmethod + def from_tensor_variable( + cls, tensor_variable: TensorVariable + ) -> "FakeItemVariable": + return FakeItemVariable(**dict(tensor_variable.__dict__)) + + +class TensorSubclassVariable(UserDefinedClassVariable): + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + # Handle `Subclass(existing_tensor, ...)` calls. + from .torch_function import TensorWithTFOverrideVariable + + new_func = self.value.__new__ + if new_func is torch.Tensor.__new__: + var = None + if len(args) == 1 and args[0].is_tensor() and len(kwargs) == 0: + data = args[0] + # Simulate `torch.Tensor.__new__` as shallow-copying the input + # tensor data with a new type. TODO polyfill? + var = TensorWithTFOverrideVariable.from_tensor_var( + tx, data, self.value, self.source + ) + else: + unimplemented( + gb_type="Calling subclass default constructor with more than tensor argument", + context=f"{self.value}(args={args}, kwargs={kwargs})", + explanation="Currently not supported", + hints=[ + "Avoid this constructor call or move it outside " + "`torch.compile` regione", + *graph_break_hints.SUPPORTABLE, + ], + ) + else: + # Let Dynamo trace through custom `__new__` + var = VariableTracker.build(tx, new_func).call_function( + tx, [self] + list(args), kwargs + ) + assert var is not None + # Let Dynamo trace through custom `__init__` + init_func = self.value.__init__ + # TODO builder should be able to handle `torch.Tensor.__init__`, + # which is `object.__init__`, so that we can remove this check. + if init_func is not torch.Tensor.__init__: + VariableTracker.build(tx, init_func).call_function(tx, [var], kwargs) + + # See NOTE [Side effect tracking for newly constructed tensor] + tx.output.side_effects._track_obj( + object(), var, mutation_type_cls=AttributeMutationNew + ) + return var + + def as_python_constant(self) -> type: + return self.value + + +class UntypedStorageVariable(VariableTracker): + _nonvar_fields = { + "example_value", + *VariableTracker._nonvar_fields, + } + + def __init__( + self, + from_tensor: TensorVariable, + example_value: torch.UntypedStorage, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.from_tensor = from_tensor + # Example_value will always have device="meta" + self.example_value = example_value + + def python_type(self) -> type: + return torch.UntypedStorage + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "size": + if args or kwargs: + raise_args_mismatch( + tx, + name, + "0 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + result = self.example_value.size() + if not has_free_symbols(result): + # avoid creating a node in the graph + return VariableTracker.build(tx, int(result)) + else: + from ..external_utils import untyped_storage_size + from .builder import wrap_fx_proxy + + return wrap_fx_proxy( + tx, + tx.output.create_proxy( + "call_function", + untyped_storage_size, + (self.from_tensor.as_proxy(),), + {}, + ), + ) + if name == "resize_" and len(args) == 1: + if kwargs: + raise_args_mismatch(tx, name, "0 kwargs", f"{len(kwargs)} kwargs") + tx.output.create_proxy( + "call_function", + torch.ops.inductor.resize_storage_bytes_, + (self.from_tensor.as_proxy(), args[0].as_proxy()), + {}, + ) + return self + + return super().call_method(tx, name, args, kwargs) + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen(self.from_tensor) + codegen.load_method("untyped_storage") + codegen.call_method(0) + + +class DataPtrVariable(VariableTracker): + def __init__( + self, + from_tensor: TensorVariable, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.from_tensor = from_tensor + + def python_type(self) -> type: + return int + + def reconstruct(self, codegen: "PyCodegen") -> None: + codegen(self.from_tensor) + codegen.load_method("data_ptr") + codegen.call_method(0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/torch.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/torch.py new file mode 100644 index 0000000000000000000000000000000000000000..a69112187f60d05d01a77bd80ae1781ab26d0975 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/torch.py @@ -0,0 +1,3505 @@ +""" +This module implements variable tracking for torch functions and operations during Dynamo tracing. + +It provides classes to handle different types of torch operations: + +TorchInGraphFunctionVariable: Handles torch.* functions that should be captured in the FX graph. +Provides special handling for constant folding, tensor methods, and torch function overrides. +Manages complex cases like out= variants and parameter construction. + +TorchCtxManagerClassVariable: Handles torch context managers like torch.no_grad(), autocast, etc. +Provides implementations for entering/exiting these contexts during tracing. + +DispatchKeySetVariable: Represents torch.DispatchKeySet for managing dispatch keys and +device-specific operations during tracing. + +The module includes special handling for: +- Constant folding of pure functions +- Tensor method calls +- torch.nn.Parameter construction +- __torch_function__ overrides +- Context manager state tracking +- Device and dtype management + +This is a core part of Dynamo's tracing system, translating torch operations into +traceable graph nodes while preserving correct semantics and handling edge cases. +""" + +import enum +import functools +import inspect +import logging +import math +import re +from collections.abc import Callable, Iterable, Sequence +from contextlib import nullcontext +from typing import Any, NoReturn, TYPE_CHECKING, TypeVar, Union +from typing_extensions import TypeIs + +import torch._C +import torch._refs +import torch.fx +import torch.nn +import torch.utils._pytree as _pytree +from torch._C import DispatchKeySet +from torch._dynamo.variables.constant import ConstantVariable +from torch._dynamo.variables.streams import StreamVariable +from torch._dynamo.variables.torch_function import TorchFunctionModeVariable +from torch._guards import Guard, Source, TracingContext +from torch._logging import warning_once +from torch.autograd.graph import GradientEdge +from torch.utils._python_dispatch import is_traceable_wrapper_subclass_type + +from .. import config, graph_break_hints, polyfills, variables +from ..codegen import PyCodegen +from ..create_parameter_op import ( + can_convert_to_tracable_parameter, + new_parameter_placeholder, + tracable_create_parameter, +) +from ..device_interface import get_registered_device_interfaces +from ..exc import ( + raise_observed_exception, + raise_type_error, + unimplemented, + UserError, + UserErrorType, +) +from ..guards import GuardBuilder, install_guard +from ..source import ( + AttrSource, + CallFunctionNoArgsSource, + GlobalStateSource, + ImportSource, + SyntheticLocalSource, +) +from ..utils import ( + check_unspec_or_constant_args, + guard_if_dyn, + has_torch_function, + hashable, + is_wrapper_or_member_descriptor, + product, + proxy_args_kwargs, + unwrap_if_wrapper, +) +from .base import typestr, VariableTracker +from .ctx_manager import ( + AutocastModeVariable, + ProfilerContextVariable, + ProfilerRecordFunctionContextVariable, + TorchFunctionDisableVariable, +) +from .distributed import DistributedVariable +from .functions import bind_args_cached, NestedUserFunctionVariable +from .lists import ListVariable, TupleVariable +from .script_object import TorchScriptObjectVariable +from .torch_function import ( + can_dispatch_torch_function, + dispatch_torch_function, + TensorWithTFOverrideVariable, + TorchFunctionModeStackVariable, +) +from .user_defined import UserDefinedTupleVariable + + +try: + import numpy as np +except ModuleNotFoundError: + np = None # type: ignore[assignment] + +try: + from torch.distributed.fsdp._fully_shard import _fsdp_param_group +except ModuleNotFoundError: + _fsdp_param_group = None # type: ignore[assignment] + + +if TYPE_CHECKING: + from torch._dynamo.symbolic_convert import InstructionTranslator + from torch._opaque_base import OpaqueBase + from torch.utils._pytree import TreeSpec + + +V = TypeVar("V") +T = TypeVar("T") + +log = logging.getLogger(__name__) + +supported_ctx_manager_classes = dict.fromkeys( + [ + torch.profiler.profiler.profile, + torch.autograd.forward_ad._set_fwd_grad_enabled, + torch.autograd.forward_ad.dual_level, + torch.autograd.profiler.profile, + torch.autograd.profiler.record_function, + torch._C.DisableTorchFunctionSubclass, + torch._C.DisableTorchFunction, + torch._functorch.vmap.vmap_increment_nesting, + torch._functorch.eager_transforms.grad_increment_nesting, + torch._functorch.eager_transforms.jvp_increment_nesting, + torch._functorch.eager_transforms.enable_inplace_requires_grad, + torch.amp.autocast_mode.autocast, + torch.autograd.grad_mode.enable_grad, + torch.autograd.grad_mode.inference_mode, + torch.autograd.grad_mode.no_grad, + torch.autograd.grad_mode.set_grad_enabled, + torch.autograd.graph.disable_saved_tensors_hooks, + torch.cpu.amp.autocast_mode.autocast, + torch.cuda.amp.autocast_mode.autocast, + torch.fx.traceback.annotate, + torch.fx.traceback.annotate.__wrapped__, # type: ignore[attr-defined] + # We'll let Dynamo inline into the contextlib part of these context + # manager instances, all the way till it invokes the wrapped function + # itself (at which point we wrap it back to special context manager + # VTs). + # + # This allows us to support calling functions decorated with these + # context managers, without much extra effort or code dup. + torch.nn.attention.sdpa_kernel.__wrapped__, # type: ignore[attr-defined] + ] +) + + +REWRITE_OPS_TO_TENSOR_SIZE_METHOD = dict.fromkeys( + [ + torch._shape_as_tensor, + ] +) + +constant_fold_functions_need_guards = [ + torch.accelerator.current_device_index, + torch.accelerator.current_accelerator, + torch.cuda.current_device, + torch.cuda.is_initialized, + torch.xpu.current_device, + torch.xpu.is_initialized, + torch.__future__.get_overwrite_module_params_on_conversion, +] + +constant_fold_functions = [ + torch._assert, + torch._utils._get_device_index, + torch._C._get_cublas_allow_tf32, + torch._C._is_any_autocast_enabled, + torch.accelerator.is_available, + torch.cuda.get_device_properties, + torch.cuda.is_available, + torch.distributed.is_available, + torch.get_autocast_dtype, + torch.get_autocast_gpu_dtype, + torch.get_default_dtype, + torch.is_autocast_cache_enabled, + torch.is_autocast_cpu_enabled, + torch.is_autocast_enabled, + torch.is_complex, + torch.is_floating_point, + torch.nn.functional._Reduction.get_enum, # type: ignore[attr-defined] + torch.promote_types, + torch._C._get_privateuse1_backend_name, + torch.autograd._is_checkpoint_valid, + torch.xpu.get_device_properties, + torch.xpu.is_available, +] + constant_fold_functions_need_guards +if torch.distributed.is_available(): + constant_fold_functions.extend( + [ + torch.distributed.is_initialized, + torch.distributed.get_rank, + torch.distributed.get_world_size, + ] + ) +# Convert to dict for O(1) access times +constant_fold_functions_need_guards = dict.fromkeys(constant_fold_functions_need_guards) +constant_fold_functions = dict.fromkeys(constant_fold_functions) + + +@functools.cache +def tracing_state_functions() -> dict[Callable[[], Any], bool | None]: + # Defined as a function to avoid circular import like torch.onnx + return { + torch.jit.is_scripting: False, + torch.jit.is_tracing: False, + torch._C._get_tracing_state: None, + torch.fx._symbolic_trace.is_fx_tracing: False, + torch.fx._symbolic_trace.is_fx_symbolic_tracing: False, + torch.onnx.is_in_onnx_export: False, + # pyrefly: ignore [deprecated] + torch._dynamo.external_utils.is_compiling: True, + # pyrefly: ignore [deprecated] + torch._utils.is_compiling: True, + torch.compiler.is_compiling: True, + torch.compiler.is_dynamo_compiling: True, + torch.compiler.is_exporting: True, + torch._dynamo.eval_frame._is_in_optimized_module: True, + # Look into https://github.com/pytorch/pytorch/pull/164721 why this is + # turned to True for Dynamo. + torch.nn.modules.activation._is_make_fx_tracing: True, + } + + +bin_ops = dict.fromkeys(["add", "sub", "mul", "div", "sqrt"]) + +dispatch_key_set_functions = { + torch._C._dispatch_keys, + torch._C._dispatch_tls_local_include_set, + torch._C._dispatch_tls_local_exclude_set, +} + + +def _check_for_gradient_edge(var: VariableTracker, arg_name: str) -> None: + """Check if var contains a GradientEdge from outside the compiled region. + + Used by handle_autograd_grad to reject external GradientEdge objects that + cannot be traced through. + """ + from .lists import BaseListVariable + + if isinstance(var, UserDefinedTupleVariable) and type(var.value) is GradientEdge: + # Try to get source info for context + source_info = var.source.name if var.source else None + context = f"GradientEdge in {arg_name}" + if source_info: + context += f": {source_info}" + + unimplemented( + gb_type="autograd.grad with external GradientEdge", + context=context, + explanation=( + "torch.autograd.grad() cannot be used with GradientEdge inputs " + "passed from outside the compiled region. The GradientEdge contains " + "a reference to an autograd node that was created before torch.compile " + "started tracing, so Dynamo cannot trace through its computation." + ), + hints=[ + "Move the autograd.grad() call outside the torch.compile region.", + "Or use tensor inputs directly instead of GradientEdge objects.", + *graph_break_hints.SUPPORTABLE, + ], + ) + elif isinstance(var, BaseListVariable): + for i, item in enumerate(var.items): + _check_for_gradient_edge(item, f"{arg_name}[{i}]") + + +def _collect_all_grad_fns(tensor: torch.Tensor) -> set[torch.autograd.graph.Node]: + from torch._subclasses.fake_tensor import get_plain_tensors + from torch.utils._python_dispatch import is_traceable_wrapper_subclass + + grad_fns: set[torch.autograd.graph.Node] = set() + + plain_tensors: list[torch.SymInt | torch.Tensor | int | OpaqueBase] = [] + # Get all plain tensors (handles nested subclasses) + if is_traceable_wrapper_subclass(tensor): + get_plain_tensors(tensor, out=plain_tensors) + else: + plain_tensors.append(tensor) + + for t in plain_tensors: + if not isinstance(t, torch.Tensor): + continue + + if t.grad_fn is not None: + grad_fns.add(t.grad_fn) + + # For views, also include the base tensor's grad_fn + if t._base is not None and t._base.grad_fn is not None: + grad_fns.add(t._base.grad_fn) + + return grad_fns + + +def _collect_tensors_with_sources( + var: VariableTracker, +) -> list[tuple[torch.Tensor, str | None]]: + """Extract (fake_tensor, source_name) pairs from a VariableTracker. + + Used by handle_autograd_grad to collect tensors from the outputs and inputs + arguments for grad_fn reachability analysis. + """ + from torch.utils._python_dispatch import is_traceable_wrapper_subclass + + from .lazy import LazyVariableTracker + from .lists import BaseListVariable + from .tensor import TensorVariable + + results: list[tuple[torch.Tensor, str | None]] = [] + if isinstance(var, TensorVariable): + fake_tensor = var.as_proxy().node.meta.get("example_value") + assert isinstance(fake_tensor, torch.Tensor) + if isinstance(fake_tensor, torch._subclasses.fake_tensor.FakeTensor): + pass + elif is_traceable_wrapper_subclass(fake_tensor): + # For tensor subclasses (e.g. DTensor), verify the inner tensors + # are FakeTensors but keep the original subclass for grad_fn + # reachability analysis. + plain: list[object] = [] + torch._subclasses.fake_tensor.get_plain_tensors( + fake_tensor, # pyrefly: ignore[bad-argument-type] + out=plain, # pyrefly: ignore[bad-argument-type] + ) + assert all( + isinstance(t, torch._subclasses.fake_tensor.FakeTensor) + for t in plain + if isinstance(t, torch.Tensor) + ), ( + f"Expected all plain tensors to be FakeTensors, got {[type(t) for t in plain]}" + ) + else: + raise AssertionError( + f"Expected FakeTensor or subclass, got {type(fake_tensor)}" + ) + source_name = var.source.name if var.source else None + results.append((fake_tensor, source_name)) + elif isinstance(var, LazyVariableTracker): + # Realize the lazy var to get the actual TensorVariable + results.extend(_collect_tensors_with_sources(var.realize())) + elif isinstance(var, BaseListVariable): + for item in var.items: + results.extend(_collect_tensors_with_sources(item)) + else: + unimplemented( + gb_type="autograd.grad with unsupported argument type", + context=f"got {type(var).__name__}", + explanation=( + f"torch.autograd.grad() received an argument of type {type(var).__name__} " + "which is not supported. Expected tensor or sequence of tensors." + ), + hints=[ + "Ensure outputs and inputs arguments are tensors or sequences of tensors.", + ], + ) + return results + + +def _collect_placeholder_nodes(var: "VariableTracker") -> list[torch.fx.Node]: + """Recursively collect FX placeholder nodes from a VariableTracker. + + The returned placeholder nodes carry grapharg.example (real tensor) and + example_value (FakeTensor) metadata — comparing these reveals lost + autograd linkage (e.g., grad_fn dropped during tracing). + See NOTE [Detecting lost autograd linkage in closure-captured tensors]. + """ + from .lazy import LazyVariableTracker + from .lists import BaseListVariable + from .tensor import TensorVariable + + result: list[torch.fx.Node] = [] + if isinstance(var, TensorVariable): + node = var.as_proxy().node + if node.op == "placeholder": + result.append(node) + elif isinstance(var, LazyVariableTracker): + result.extend(_collect_placeholder_nodes(var.realize())) + elif isinstance(var, BaseListVariable): + for item in var.items: + result.extend(_collect_placeholder_nodes(item)) + else: + unimplemented( + gb_type="_autograd_grad with unsupported argument type", + context=f"got {type(var).__name__}", + explanation=( + f"_autograd_grad() received an argument of type {type(var).__name__} " + "which is not supported. Expected tensor or sequence of tensors." + ), + hints=[ + "Ensure outputs and inputs arguments are tensors or sequences of tensors.", + ], + ) + return result + + +@functools.cache +def get_overridable_functions() -> set[Callable[..., Any]]: + from itertools import chain + + from torch.overrides import get_overridable_functions as get_overridable_functions_ + from torch.utils._device import _device_constructors + + funcs = set(chain.from_iterable(get_overridable_functions_().values())) + funcs.update(_device_constructors()) + more: set[Callable[..., Any]] = { + torch.ones_like, + torch.zeros_like, + torch.empty_like, + torch.full_like, + } + funcs.update(more) + return funcs + + +class BaseTorchVariable(VariableTracker): + """common base for all torch.* functions, classes, modules and other things""" + + @classmethod + def create_with_source(cls, value: Any, source: Source) -> "BaseTorchVariable": + if inspect.isclass(value): + install_guard(source.make_guard(GuardBuilder.CLASS_MATCH)) + elif inspect.ismodule(value): + install_guard(source.make_guard(GuardBuilder.MODULE_MATCH)) + elif inspect.isfunction(value): + install_guard(source.make_guard(GuardBuilder.CLOSURE_MATCH)) + elif inspect.isbuiltin(value) or isinstance( + value, (torch._ops.OpOverload, torch._ops.OpOverloadPacket) + ): + install_guard(source.make_guard(GuardBuilder.BUILTIN_MATCH)) + elif is_wrapper_or_member_descriptor(value) or isinstance( + value, torch._dynamo.compiled_autograd.Op + ): + # Dont need to guard on wrappers + pass + else: + install_guard(source.make_guard(GuardBuilder.FUNCTION_MATCH)) + return cls(value, source=source) + + def __init__(self, value: Any, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.value = value + + def reconstruct(self, codegen: "PyCodegen") -> None: + try: + name = f"{self.value.__module__}.{self.value.__name__}" + except Exception: + name = f"torch_obj_{id(self.value)}" + unique_var_name = "__" + re.sub(r"[^a-zA-Z0-9_]+", "_", name) + codegen.extend_output( + codegen.setup_globally_cached(unique_var_name, self.value) + ) + + def as_proxy(self) -> Any: + return self.value + + def as_python_constant(self) -> Any: + return self.value + + def get_real_python_backed_value(self) -> Any: + return self.value + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> ConstantVariable: + result = hasattr(self.value, name) + return VariableTracker.build(tx, result) + + def can_constant_fold_through(self) -> bool: + if self.value in constant_fold_functions: + return True + + if ( + self.value is torch.autograd._profiler_enabled + and config.constant_fold_autograd_profiler_enabled + ): + # The relevant flag is enabled only for export. One might wonder + # why? + # + # Actually we would like to not graph break even in the case of + # Dynamo. But there is a weird-unsolved bug with Kineto + Dynamo + # when there are distributed jobs that lead to NCCL timeouts. This + # bug is a rare edege case, but we have not been able to root cause + # it yet. See https://www.internalfb.com/sevmanager/view/560336 for + # more details. + # + # So is this safe for export? Yes, for export, we do not anticipate + # JIT tracing in distributed job training, and the weird edge-case + # interaction with Kineto is not a valid usecase. So, this is ok. + return True + + return getattr(self.value, "__module__", None) == "math" + + +class TorchCtxManagerClassVariable(BaseTorchVariable): + """Points to a context manager class in torch.* that dynamo has implementations""" + + def __repr__(self) -> str: + return f"TorchCtxManagerClassVariable({self.value})" + + @staticmethod + def is_matching_cls(value: Any) -> bool: + # Unwrap if it's a functools.lru_cache wrapper + value = unwrap_if_wrapper(value) + # We can't do isinstance(value, type) check because some ctx managers + # are implemented as a function decorated by contextlib.contextmanager, + # E.g., torch._functorch.vmap.vmap_increment_nesting. + return ( + # Context manager type or function with @contextmanager is callable + callable(value) + and ( + hashable(value) # accesses value.__hash__() + and value in supported_ctx_manager_classes + ) + ) + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: "dict[str, VariableTracker]", + ) -> "VariableTracker": + from . import ( + DisabledSavedTensorsHooksVariable, + DualLevelContextManager, + FSDPParamGroupUseTrainingStateVariable, + FxTracebackAnnotateVariable, + GradIncrementNestingCtxManagerVariable, + GradInplaceRequiresGradCtxManagerVariable, + GradModeVariable, + InferenceModeVariable, + JvpIncrementNestingCtxManagerVariable, + SDPAKernelVariable, + SetFwdGradEnabledContextManager, + StreamVariable, + VmapIncrementNestingCtxManagerVariable, + ) + + if self.value is torch.no_grad: + if len(args) == 1 and isinstance( + args[0], variables.functions.BaseUserFunctionVariable + ): + ctx = GradModeVariable.create(tx, False) + return ctx.call_function(tx, args, kwargs) + else: + return GradModeVariable.create(tx, False) + elif self.value is torch.enable_grad: + if len(args) == 1 and isinstance( + args[0], variables.functions.BaseUserFunctionVariable + ): + ctx = GradModeVariable.create(tx, True) + return ctx.call_function(tx, args, kwargs) + return GradModeVariable.create(tx, True) + elif self.value is torch.set_grad_enabled and len(args) == 1: + return GradModeVariable.create( + tx, args[0].as_python_constant(), initialized=True + ) + elif self.value is torch.inference_mode: + assert len(args) <= 1 and len(kwargs) == 0 + inf_mode = args[0].as_python_constant() if len(args) == 1 else True + return InferenceModeVariable.create(tx, inf_mode) + elif self.value in ( + torch.fx.traceback.annotate, + torch.fx.traceback.annotate.__wrapped__, # type: ignore[attr-defined] + ): + assert len(args) <= 1 and len(kwargs) == 0 + return FxTracebackAnnotateVariable( + args[0].as_python_constant(), source=self.source + ) + elif inspect.isclass(self.value) and issubclass(self.value, torch.Stream): + from torch._dynamo.variables.builder import wrap_fx_proxy_cls + + return wrap_fx_proxy_cls( + StreamVariable, + tx, + tx.output.create_proxy( + "call_function", + self.value, + (), + {}, + ), + ) + elif self.value in ( + torch.amp.autocast_mode.autocast, + torch.cuda.amp.autocast, + torch.cpu.amp.autocast, + ): + # pyrefly: ignore [bad-argument-type] + return AutocastModeVariable.create(self.value, args, kwargs) + elif self.value in ( + torch.profiler.record_function, + torch.autograd.profiler.record_function, + ): + return ProfilerRecordFunctionContextVariable.create( + func=self.value, record_args=args, record_kwargs=kwargs + ) + elif self.value in ( + torch.profiler.profile, + torch.autograd.profiler.profile, + ): + warning_once(log, "Profiler function %s will be ignored", self.value) + return ProfilerContextVariable() + elif ( + self.value is torch._C.DisableTorchFunctionSubclass + or self.value is torch._C.DisableTorchFunction + ): + assert not (args or kwargs) + return TorchFunctionDisableVariable.create( + tx, only_subclass=self.value is torch._C.DisableTorchFunctionSubclass + ) + elif self.value is torch._functorch.vmap.vmap_increment_nesting: + assert len(args) == 2 + return VmapIncrementNestingCtxManagerVariable.create( + tx, + args, + ) + elif self.value is torch._functorch.eager_transforms.jvp_increment_nesting: + assert len(args) == 0 + return JvpIncrementNestingCtxManagerVariable.create(tx) + elif self.value is torch.autograd.forward_ad._set_fwd_grad_enabled: + assert len(args) == 1 + return SetFwdGradEnabledContextManager.create( + tx, + [guard_if_dyn(x) for x in args], + ) + elif self.value is torch.autograd.forward_ad.dual_level: + assert len(args) == 0 + return DualLevelContextManager.create(tx) + elif self.value is torch._functorch.eager_transforms.grad_increment_nesting: + assert len(args) == 0 + return GradIncrementNestingCtxManagerVariable.create(tx) + elif ( + self.value is torch._functorch.eager_transforms.enable_inplace_requires_grad + ): + assert len(args) == 1 + return GradInplaceRequiresGradCtxManagerVariable.create( + tx, + [guard_if_dyn(x) for x in args], + ) + elif self.value is torch.autograd.graph.disable_saved_tensors_hooks: + assert len(args) == 1 + return DisabledSavedTensorsHooksVariable.create( + tx, args[0].as_python_constant() + ) + elif ( + _fsdp_param_group is not None + and self.value is _fsdp_param_group.FSDPParamGroup.use_training_state + ): + assert len(args) == 2 + return FSDPParamGroupUseTrainingStateVariable.create( + tx, args[0], args[1].as_python_constant() + ) + elif self.value is torch.nn.attention.sdpa_kernel.__wrapped__: # type: ignore[attr-defined] + name_to_arg_map = bind_args_cached( + self.value, + tx, + self.source, + args, + kwargs, + ) + backends = name_to_arg_map["backends"].as_python_constant() + set_priority = name_to_arg_map["set_priority"].as_python_constant() + return SDPAKernelVariable.create(tx, backends, set_priority) + + return super().call_function(tx, args, kwargs) + + +class AllowInGraphKind(enum.Enum): + DEFAULT = "default" + NONSTRICT_TRACE = "nonstrict_trace" + LEAF_FUNCTION = "leaf_function" + + +class TorchInGraphFunctionVariable(BaseTorchVariable): + """Points to a torch function/method that should be put in FX graph""" + + def __init__( + self, + value: Callable[..., Any], + kind: AllowInGraphKind | None = None, + **kwargs: Any, + ) -> None: + super().__init__(value, **kwargs) + from ..trace_rules import is_leaf_function, is_nonstrict_trace_callable + + if kind is None: + if is_leaf_function(value): + kind = AllowInGraphKind.LEAF_FUNCTION + elif is_nonstrict_trace_callable(value): + kind = AllowInGraphKind.NONSTRICT_TRACE + else: + kind = AllowInGraphKind.DEFAULT + + self.kind = kind + + def __repr__(self) -> str: + return f"TorchInGraphFunctionVariable({self.value}, kind={self.kind})" + + def get_function(self) -> Callable[..., Any]: + return self.value + + @staticmethod + @functools.cache + def _get_handlers() -> dict[Callable[..., Any], Callable[..., Any]]: + """Build a dict from function -> method to handle it so that we are O(1) + in terms of the number of function with special handling.""" + # pyrefly: ignore [implicit-any] + handlers = {} + + def register( + *fns: Callable[..., Any], + ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + def _register(handler: Callable[..., Any]) -> Callable[..., Any]: + for fn in fns: + assert fn not in handlers, fn + handlers[fn] = handler + return handler + + assert callable(fns[0]) + return _register + + from torch.backends.cuda import SDPAParams + + from . import ( + ConstantVariable, + GradModeVariable, + StreamContextVariable, + SymNodeVariable, + TensorVariable, + UserDefinedObjectVariable, + ) + from .builder import wrap_fx_proxy, wrap_fx_proxy_cls + + @register(*tracing_state_functions()) + def handle_tracing_state_functions( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + assert not args and not kwargs + # See: https://github.com/pytorch/pytorch/issues/110765 + if self.value in ( + # pyrefly: ignore [deprecated] + torch._utils.is_compiling, + # pyrefly: ignore [deprecated] + torch._dynamo.external_utils.is_compiling, + torch.compiler.is_compiling, + torch.compiler.is_dynamo_compiling, + torch.compiler.is_exporting, + torch._dynamo.eval_frame._is_in_optimized_module, + ): + tx.mark_inconsistent_side_effects() + # Read dynamically: the cached dict always has True, but + # is_exporting() should only be True during torch.export. + if self.value is torch.compiler.is_exporting: + return VariableTracker.build(tx, torch.compiler._is_exporting_flag) + return VariableTracker.build(tx, tracing_state_functions()[self.value]) + + @register(*dispatch_key_set_functions) + def handle_dispatch_key_set_functions( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + assert not kwargs + if self.value is torch._C._dispatch_keys: + assert len(args) == 1 + assert args[0].is_tensor() + # pyrefly: ignore[missing-attribute] + example_value = args[0].proxy.node.meta["example_value"] + dks = self.value(example_value) + # Remove Python and PythonTLSSnapshot from the dispatch key set, + # as they originate from FakeTensor propagation. + # This should only be done if the example_value is a FakeTensor. + # However, if tensor subclasses are present, + # it is reasonable for Python to remain in the dispatch key set. + if isinstance(example_value, torch._subclasses.FakeTensor): + dks = ( + dks + - torch._C.DispatchKeySet(torch._C.DispatchKey.Python) + - torch._C.DispatchKeySet( + torch._C.DispatchKey.PythonTLSSnapshot + ) + ) + return DispatchKeySetVariable.create(dks) + else: + assert not args + return DispatchKeySetVariable.create(self.value()) + + @register(torch.overrides.get_default_nowrap_functions.__wrapped__) + def handle_get_default_nowrap_functions( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + # [Note: __torch_function__] we return empty here because we restrict + # the set of functions that we trace __torch_function__ on to + # functions outside of the actual set. Implementing this properly will require implementing + # some variable types to track and compare tensor getset descriptors + return VariableTracker.build( + tx, torch.overrides.get_default_nowrap_functions() + ) + + @register(torch.ops.inductor.accumulate_grad_.default) + def handle_accumulate_grad_( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + return tx.inline_user_function_return( + VariableTracker.build(tx, polyfills.accumulate_grad), args, kwargs + ) + + @register(math.radians) + def handle_radians( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker | None: + if not check_unspec_or_constant_args(args, kwargs): + # Use polyfill to convert math.radians(x) into math.pi * x / 180.0 + return tx.inline_user_function_return( + VariableTracker.build(tx, polyfills.radians), args, kwargs + ) + + if hasattr(math, "fma"): # Python 3.13+ + + @register(math.fma) + def handle_fma( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker | None: + if len(args) != 3 or kwargs: + return None + + if all(arg.is_tensor() for arg in args): + x, y, z = args + addcmul_fn = TorchInGraphFunctionVariable(torch.addcmul) + return addcmul_fn.call_function(tx, [z, x, y], {}) + + # Use math.fma if constants + return None + + @register(torch.is_inference_mode_enabled) + def handle_is_inference_mode_enabled( + self, tx: "InstructionTranslator" + ) -> NoReturn: + unimplemented( + gb_type="Encountered torch.is_inference_mode_enabled during tracing", + context="", + explanation="torch.is_inference_mode_enabled() is not supported", + hints=[ + *graph_break_hints.FUNDAMENTAL, + *graph_break_hints.INFERENCE_MODE, + ], + ) + + @register(torch.is_tensor, torch.overrides.is_tensor_like) + def handle_is_tensor( + self, tx: "InstructionTranslator", arg: VariableTracker + ) -> VariableTracker: + if arg.is_tensor() or ( + self.value is torch.overrides.is_tensor_like + and isinstance(arg, UserDefinedObjectVariable) + and hasattr(arg.value, "__torch_function__") + ): + return ConstantVariable.create(True) + else: + return ConstantVariable.create(False) + + @register( + torch.is_floating_point, + torch.is_complex, + ) + def handle_is_floating_point( + self, tx: "InstructionTranslator", input: Any + ) -> VariableTracker | None: + input_arg = input + if input_arg.is_tensor() and input_arg.dtype is not None: + if self.value is torch.is_floating_point: + return VariableTracker.build(tx, input_arg.dtype.is_floating_point) + elif self.value is torch.is_complex: + return VariableTracker.build(tx, input_arg.dtype.is_complex) + else: + raise AssertionError(f"calling {self.value}") + return None + + @register(torch.numel) + def handle_numel( + self, tx: "InstructionTranslator", input: Any + ) -> VariableTracker | None: + if input.is_tensor() and input.valid_size(): + return VariableTracker.build(tx, product(input.size)) + elif input.is_tensor(): + # Workaround dynamic shapes issue + return input.call_method(tx, "numel", [], {}) + return None + + @register(torch.compile) + def handle_torch_compile( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + if len(args) == 1: + # torch.compile is a no-op in dynamo + return args[0] + + unimplemented( + gb_type="torch.compile call with > 1 args", + context=f"args={args}, kwargs={kwargs}", + explanation="Attempted to call `torch.compile` with > 1 args. Dynamo does not support this.", + hints=[ + "Remove the torch.compile call or its additional args.", + *graph_break_hints.SUPPORTABLE, + ], + ) + + @register(torch.library.wrap_triton) + def handle_wrap_triton( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + if len(args) == 1: + # torch.library.wrap_triton is a no-op in dynamo + return args[0] + + unimplemented( + gb_type="torch.library.wrap_triton call with > 1 args", + context=f"args={args}, kwargs={kwargs}", + explanation="Attempted to call `torch.library.wrap_triton` with > 1 args. Dynamo does not support this.", + hints=[ + "Remove the torch.library.wrap_triton call or its additional args.", + *graph_break_hints.SUPPORTABLE, + ], + ) + + @register(*REWRITE_OPS_TO_TENSOR_SIZE_METHOD) + def handle_tensor_size_rewrites( + self, tx: "InstructionTranslator", input: VariableTracker + ) -> VariableTracker: + assert input.is_tensor() + return input.call_method(tx, "size", [], {}) + + @register( + torch.nn.modules.utils._single, + torch.nn.modules.utils._pair, + torch.nn.modules.utils._triple, + torch.nn.modules.utils._quadruple, + torch.nn.modules.utils._ntuple, + ) + def handle_ntuple( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + return self._call_ntuple(tx, args, kwargs) + + @register(torch.is_grad_enabled) + def handle_is_grad_enabled( + self, tx: "InstructionTranslator" + ) -> ConstantVariable: + install_guard(GradModeVariable._guards_singleton) + return VariableTracker.build(tx, torch.is_grad_enabled()) + + @register(torch.use_deterministic_algorithms) + def handle_use_deterministic_algorithms( + self, tx: "InstructionTranslator", mode: Any, warn_only: bool = False + ) -> VariableTracker: + # pyrefly: ignore [missing-attribute] + if warn_only and warn_only.as_python_constant(): + unimplemented( + gb_type="Attempted to use torch.use_deterministic_algorithms(warn_only=True)", + context=f"mode={mode}, warn_only={warn_only}", + explanation="Dynamo does not support this.", + hints=[ + "Remove param warn_only in function call torch.use_deterministic_algorithms.", + *graph_break_hints.SUPPORTABLE, + ], + ) + + value = mode.as_python_constant() + tx.output.create_node( + "call_function", torch._C._set_deterministic_algorithms, (value,), {} + ) + torch._C._set_deterministic_algorithms(value) + return ConstantVariable.create(None) + + @register(torch.autocast_increment_nesting) + def handle_autocast_increment_nesting( + self, tx: "InstructionTranslator" + ) -> VariableTracker: + tx.output.create_node( + "call_function", torch.autocast_increment_nesting, (), {} + ) + prev = torch.autocast_increment_nesting() + tx.output.add_cleanup_hook(lambda: torch.autocast_decrement_nesting()) + return VariableTracker.build(tx, prev) + + @register(torch.autocast_decrement_nesting) + def handle_autocast_decrement_nesting( + self, tx: "InstructionTranslator" + ) -> VariableTracker: + tx.output.create_node( + "call_function", torch.autocast_decrement_nesting, (), {} + ) + prev = torch.autocast_decrement_nesting() + tx.output.add_cleanup_hook(lambda: torch.autocast_increment_nesting()) + return VariableTracker.build(tx, prev) + + @register(torch.set_autocast_enabled) + def handle_set_autocast_enabled( + self, + tx: "InstructionTranslator", + device_type: VariableTracker, + enabled: VariableTracker, + ) -> VariableTracker: + tx.output.create_node( + "call_function", + torch.set_autocast_enabled, + (device_type.as_proxy(), enabled.as_proxy()), + ) + dev_py_const = device_type.as_python_constant() + prev = torch.is_autocast_enabled(dev_py_const) + torch.set_autocast_enabled(dev_py_const, enabled.as_python_constant()) + tx.output.add_cleanup_hook( + lambda: torch.set_autocast_enabled(dev_py_const, prev) + ) + return ConstantVariable.create(None) + + @register(torch.set_autocast_cache_enabled) + def handle_set_autocast_cache_enabled( + self, tx: "InstructionTranslator", enabled: VariableTracker + ) -> VariableTracker: + tx.output.create_node( + "call_function", torch.set_autocast_cache_enabled, (enabled.as_proxy(),) + ) + prev = torch.is_autocast_cache_enabled() + torch.set_autocast_cache_enabled(enabled.as_python_constant()) + tx.output.add_cleanup_hook(lambda: torch.set_autocast_cache_enabled(prev)) + return ConstantVariable.create(None) + + @register(torch._C._functorch._grad_increment_nesting) + def handle_grad_increment_nesting( + self, tx: "InstructionTranslator" + ) -> VariableTracker: + tx.output.create_node( + "call_function", torch._C._functorch._grad_increment_nesting + ) + level = torch._C._functorch._grad_increment_nesting() + tx.output.add_cleanup_hook(torch._C._functorch._grad_decrement_nesting) + return VariableTracker.build(tx, level) + + @register(torch._C._functorch._grad_decrement_nesting) + def handle_grad_decrement_nesting( + self, tx: "InstructionTranslator" + ) -> VariableTracker: + tx.output.create_node( + "call_function", torch._C._functorch._grad_decrement_nesting + ) + level = torch._C._functorch._grad_decrement_nesting() + tx.output.add_cleanup_hook(torch._C._functorch._grad_increment_nesting) + return VariableTracker.build(tx, level) + + @register(torch._C._functorch.set_inplace_requires_grad_allowed) + def handle_set_inplace_requires_grad_allowed( + self, tx: "InstructionTranslator", allowed: VariableTracker + ) -> VariableTracker: + tx.output.create_node( + "call_function", + torch._C._functorch.set_inplace_requires_grad_allowed, + (allowed.as_proxy(),), + ) + prev = torch._C._functorch.get_inplace_requires_grad_allowed() + torch._C._functorch.set_inplace_requires_grad_allowed( + allowed.as_python_constant() + ) + tx.output.add_cleanup_hook( + lambda: torch._C._functorch.set_inplace_requires_grad_allowed(prev) + ) + return ConstantVariable.create(None) + + @register(torch.are_deterministic_algorithms_enabled) + def handle_are_deterministic_algorithms_enabled( + self, tx: "InstructionTranslator" + ) -> ConstantVariable: + guard = Guard( + GlobalStateSource(), + GuardBuilder.DETERMINISTIC_ALGORITHMS, # type: ignore[arg-type] + ) + install_guard(guard) + return VariableTracker.build( + tx, torch.are_deterministic_algorithms_enabled() + ) + + @register(torch._C._is_torch_function_enabled) + def handle_is_torch_function_enabled( + self, tx: "InstructionTranslator" + ) -> ConstantVariable: + install_guard(TorchFunctionDisableVariable._guards_singleton) + # see comment on SymbolicTorchFunctionState class as to why + # this is not a bug + return VariableTracker.build( + tx, tx.symbolic_torch_function_state.torch_function_subclass_enabled + ) + + @register(torch._C._is_torch_function_all_disabled) + def handle_is_torch_function_all_disabled( + self, tx: "InstructionTranslator" + ) -> ConstantVariable: + install_guard(TorchFunctionDisableVariable._guards_singleton) + return VariableTracker.build( + tx, not tx.symbolic_torch_function_state.torch_function_mode_enabled + ) + + @register(torch._C._is_torch_function_mode_enabled) + def handle_is_torch_function_mode_enabled( + self, tx: "InstructionTranslator" + ) -> ConstantVariable: + install_guard(TorchFunctionDisableVariable._guards_singleton) + # _is_torch_function_mode_enabled returns True only if: + # 1. Torch function modes are not disabled (DisableTorchFunction not entered) + # 2. There are actually modes on the stack + return VariableTracker.build( + tx, + tx.symbolic_torch_function_state.torch_function_mode_enabled + and tx.symbolic_torch_function_state.in_torch_function_mode(), + ) + + @register( + torch.overrides.has_torch_function, + torch.overrides.has_torch_function_variadic, + torch.overrides.has_torch_function_unary, + ) + def handle_has_torch_function( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> ConstantVariable: + elems = ( + args[0].unpack_var_sequence(tx) + if len(args) == 1 and isinstance(args[0], TupleVariable) + else args + ) + return VariableTracker.build(tx, any(has_torch_function(x) for x in elems)) + + @register( + *dict.fromkeys( # remove duplicates + device_interface.stream + for _, device_interface in get_registered_device_interfaces() + ) + ) + def handle_device_interface_stream( + self, tx: "InstructionTranslator", stream: "StreamVariable" + ) -> StreamContextVariable: + return StreamContextVariable.create(tx, stream) + + @register(torch.from_numpy) + def handle_from_numpy( + self, tx: "InstructionTranslator", *args: VariableTracker + ) -> TensorVariable: + if not config.trace_numpy: + unimplemented( + gb_type="call `torch.from_numpy` with `torch._dynamo.config.trace_numpy=False`", + context=f"trace_numpy={config.trace_numpy}", + explanation=( + "Attempted to call `torch.from_numpy` with config " + "`torch._dynamo.config.trace_numpy` set to `False`." + ), + hints=[ + "Change `torch._dynamo.config.trace_numpy` to `True`.", + ], + ) + if not np: + unimplemented( + gb_type="`torch.from_numpy` with NumPy unavailable", + context="", + explanation="Attempted to call `torch.numpy` but NumPy could not be imported.", + hints=[ + "Check NumPy version and installation in your environment.", + *graph_break_hints.USER_ERROR, + ], + ) + return wrap_fx_proxy_cls( + target_cls=TensorVariable, + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + torch.as_tensor, + *proxy_args_kwargs(args, {}), + ), + example_value=None, + ) + + @register(torch.jit.annotate) + def handle_jit_annotate( + self, tx: "InstructionTranslator", the_type: Any, the_value: V + ) -> V: + return the_value + + @register(torch.backends.cudnn.is_acceptable) + def handle_cudnn_is_acceptable( + self, tx: "InstructionTranslator", tensor: Any, *extra: Any + ) -> ConstantVariable: + # is_acceptable(tensor) returns true if + # (a) tensor dtype/device are supported by cudnn + # (b) cudnn is available + # (c) some initialization has completed + # technically, it depends on some global state from (c) (torch.backends.cudnn.__cudnn_version) + assert not extra, "Expect 1 input to cudnn.is_acceptable" + assert tensor.is_tensor(), ( + "Expect input to cudnn.is_acceptable to be a tensor" + ) + tensor_inp = torch.tensor(0, dtype=tensor.dtype, device=tensor.device) + return VariableTracker.build( + tx, torch.backends.cudnn.is_acceptable(tensor_inp) + ) + + @register(torch.utils.hooks.BackwardHook) + def handle_backward_hook( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + return variables.BackwardHookVariable.create(tx, *args, **kwargs) + + @register(torch.nn.Parameter) + def handle_parameter( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + return self.call_nn_parameter(tx, *args, **kwargs) + + @register(torch.ops.aten.sym_size, torch.ops.aten.sym_size.int) + def handle_sym_size( + self_: Any, tx: "InstructionTranslator", self, dim: Any | None = None + ) -> VariableTracker | None: + # we see this when retracing already traced code + if dim is not None: + return self.call_method(tx, "size", [dim], {}) + return None + + @register(torch.ops.aten.sym_stride, torch.ops.aten.sym_stride.int) + def handle_sym_stride( + self_: Any, tx: "InstructionTranslator", self, dim: Any | None = None + ) -> VariableTracker | None: + if dim is not None: + return self.call_method(tx, "stride", [dim], {}) + return None + + @register(torch.addcdiv) + def handle_addcdiv( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker | None: + if len(args) == 3 and "value" in kwargs and len(kwargs) == 1: + # decompose addcdiv into constituent ops, prevents a graph break due to converting + # value to a scalar + result = TorchInGraphFunctionVariable(torch.div).call_function( + tx, [*args[1:]], {} + ) + result = TorchInGraphFunctionVariable(torch.mul).call_function( + tx, [result, kwargs["value"]], {} + ) + return TorchInGraphFunctionVariable(torch.add).call_function( + tx, [args[0], result], {} + ) + return None + + @register(torch.full) + def handle_full( + self, + tx: "InstructionTranslator", + size: VariableTracker, + fill_value: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker | None: + if fill_value.is_tensor(): + # Decompose: create empty tensor and fill it + # This avoids the scalar extraction at compile time + empty_result = TorchInGraphFunctionVariable(torch.empty).call_function( + tx, [size], kwargs + ) + # Call fill_ method on the empty tensor + return empty_result.call_method(tx, "fill_", [fill_value], {}) + return None + + @register(torch._foreach_lerp_) + def handle_inplace_foreach_lerp_scalar( + _: Any, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker | None: + # Decompose via addcmul_ so tensor weights (e.g. 0-dim tensor + # from tensor betas in Adam) stay in tensor arguments instead of + # hitting float() in the native lerp_scalar lowering. + if len(args) == 3 and not isinstance(args[2], ListVariable) and not kwargs: + return tx.inline_user_function_return( + VariableTracker.build(tx, polyfills.foreach_lerp_inplace), + args, + kwargs, + ) + return None + + @register(torch._foreach_pow) + def handle_foreach_pow_scalar( + _: Any, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker | None: + if not config.enable_dynamo_decompositions: + return None + + # In eager it's more performant to call item() from within the C op implementation + # in compile, it's more performant to not graph break. + if len(args) == 2 and args[0].is_tensor() and not kwargs: + return tx.inline_user_function_return( + VariableTracker.build(tx, polyfills.foreach_pow_scalar), + args, + kwargs, + ) + return None + + @register(torch._C._group_tensors_by_device_and_dtype) + def handle_group_tensors_by_device_and_dtype( + _: Any, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + return tx.inline_user_function_return( + VariableTracker.build(tx, polyfills.group_tensors_by_device_and_dtype), + args, + kwargs, + ) + + @register(torch._assert) + def handle_assert( + self, + tx: "InstructionTranslator", + condition: VariableTracker, + message: VariableTracker, + ) -> VariableTracker | None: + if (condition.is_python_constant() and condition.as_python_constant()) or ( + isinstance(condition, variables.SymNodeVariable) + and condition.evaluate_expr() + ): + return ConstantVariable.create(None) + return None + + @register(SDPAParams) + def handle_sdpa_params( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + return wrap_fx_proxy( + tx, + proxy=tx.output.create_proxy( + "call_function", + torch._C._SDPAParams, + *proxy_args_kwargs(args, kwargs), + ), + param_vars=args, + ) + + if DistributedVariable.is_available(): + from torch.distributed.distributed_c10d import ( + _get_group_size_by_name, + _get_group_tag, + _rank_not_in_group, + _resolve_group_name_by_ranks_and_tag, + get_process_group_ranks, + get_rank, + get_world_size, + ) + + @register( + _get_group_size_by_name, + _get_group_tag, + _rank_not_in_group, + get_process_group_ranks, + _resolve_group_name_by_ranks_and_tag, + get_rank, + get_world_size, + ) + def handle_constant_processgroup_functions( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + # We desugar it at trace-time into ranks by directly calling util + # bake the result into the trace + if len(args) == 0 and len(kwargs) == 0: + # get_rank() or get_world_size() with no args (uses default group) + pass + elif len(args) == 1 and len(kwargs) == 0: + # group or group name + assert args[0].is_python_constant() or ( + isinstance(args[0], TorchScriptObjectVariable) + and args[ # pyrefly: ignore[missing-attribute] + 0 + ].value.script_class_name # pyrefly: ignore[missing-attribute] + == "torch.distributed.distributed_c10d.ProcessGroup" + ) + elif len(args) == 2 and len(kwargs) == 0: + # ranks + tag + assert ( + isinstance(args[0], ListVariable) + and args[1].is_python_constant() + ) + elif len(args) == 0 and len(kwargs) > 0: + # All keyword arguments (e.g., get_world_size(group=...)) + pass + else: + raise AssertionError( + f"Invalid group value ({args}, {kwargs}) for constant pg " + f"function {self.value}" + ) + + def get_arg_value(arg: VariableTracker) -> Any: + # TorchScriptObjectVariable for ProcessGroup doesn't support + # as_python_constant(), so extract real_obj directly + if isinstance(arg, TorchScriptObjectVariable): + return arg.value.real_obj # pyrefly: ignore[missing-attribute] + return arg.as_python_constant() + + args_as_value = [get_arg_value(arg) for arg in args] + kwargs_as_value = {k: get_arg_value(v) for k, v in kwargs.items()} + invocation_result = self.value(*args_as_value, **kwargs_as_value) + + # Note - while we *could* cook up sources around invocations, like a FunctionSource + # the space of invoking functions in the middle of the guard chain is very iffy. As such, + # guard propagation via options is the best we can do. + return VariableTracker.build(tx, invocation_result) + + @register(torch.nested.nested_tensor) + def handle_nested_tensor( + self, + tx: "InstructionTranslator", + tensor_list: VariableTracker | None = None, + *args: VariableTracker, + layout: Any | None = None, + **kwargs: VariableTracker, + ) -> None: + from .lists import BaseListVariable + + if layout and layout.is_constant_match(torch.strided): + unimplemented( + gb_type="Attempted to use strided NestedTensor", + context=f"layout={layout}", + explanation="Dynamo does not support this.", + hints=[ + "Change layout=torch.jagged.", + *graph_break_hints.SUPPORTABLE, + ], + ) + if not isinstance(tensor_list, BaseListVariable): + unimplemented( + gb_type="Attempted to use `nested_tensor` with non-list input", + context=f"tensor_list={tensor_list}", + explanation="Dynamo does not support this.", + hints=[ + "Change `nested_tensor` with list input.", + *graph_break_hints.USER_ERROR, + ], + ) + return None + + @register(torch.nn.functional.one_hot) + def handle_one_hot( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> None: + if len(args) + len(kwargs) == 1 or ( + len(args) == 2 and args[1].is_constant_match(-1) + ): + unimplemented( + gb_type="Attempted to use `torch.nn.functional.one_hot` with data-dependent output shape", + context=f"args={args}, kwargs={kwargs}", + explanation="Dynamo does not support this.", + hints=[ + "Explicitly set the `num_classes` param of the function call " + "`torch.nn.functional.one_hot` to something other than -1.", + ], + ) + return None + + @register(torch.fx.experimental.symbolic_shapes.guarding_hint_or_throw) + def handle_guarding_hint_or_throw( + self, + tx: "InstructionTranslator", + expr: VariableTracker, + ) -> VariableTracker | None: + if isinstance(expr, SymNodeVariable): + return VariableTracker.build( + tx, + torch.fx.experimental.symbolic_shapes.guarding_hint_or_throw( + expr.sym_num + ), + ) + elif expr.is_python_constant(): + return expr + else: + return None + + @register(torch.fx.experimental.symbolic_shapes.optimization_hint) + def handle_optimization_hint( + self, + tx: "InstructionTranslator", + expr: VariableTracker, + fallback: VariableTracker | None = None, + ) -> VariableTracker | None: + fallback_int = fallback.as_python_constant() if fallback else None + if isinstance(expr, SymNodeVariable): + return VariableTracker.build( + tx, + torch.fx.experimental.symbolic_shapes.optimization_hint( + expr.sym_num, fallback_int + ), + ) + elif expr.is_python_constant(): + return expr + else: + return None + + @register(torch.fx.experimental.symbolic_shapes.guard_size_oblivious) # type: ignore[deprecated] + def handle_guard_size_oblivious( + self, tx: "InstructionTranslator", expr: VariableTracker + ) -> VariableTracker | None: + if isinstance(expr, SymNodeVariable): + # TODO: this probably should be folded somewhere else but I'm not sure where + # TODO: some of the other symbolic_shapes special tools can also get this treatment too + return VariableTracker.build( + tx, + torch.fx.experimental.symbolic_shapes.guard_size_oblivious( # type: ignore[deprecated] + expr.sym_num + ), + ) + elif expr.is_python_constant(): + return expr + else: + return None + + @register(torch.fx.experimental.symbolic_shapes.guard_or_true) + def handle_guard_or_true( + self, tx: "InstructionTranslator", expr: VariableTracker + ) -> VariableTracker | None: + if isinstance(expr, SymNodeVariable): + # TODO: this probably should be folded somewhere else but I'm not sure where + # TODO: some of the other symbolic_shapes special tools can also get this treatment too + return VariableTracker.build( + tx, + torch.fx.experimental.symbolic_shapes.guard_or_true(expr.sym_num), + ) + elif expr.is_python_constant(): + return expr + else: + return None + + @register(torch.fx.experimental.symbolic_shapes.guard_or_false) + def handle_guard_or_false( + self, tx: "InstructionTranslator", expr: VariableTracker + ) -> VariableTracker | None: + if isinstance(expr, SymNodeVariable): + # TODO: this probably should be folded somewhere else but I'm not sure where + # TODO: some of the other symbolic_shapes special tools can also get this treatment too + return VariableTracker.build( + tx, + torch.fx.experimental.symbolic_shapes.guard_or_false(expr.sym_num), + ) + elif expr.is_python_constant(): + return expr + else: + return None + + @register(torch.fx.experimental.symbolic_shapes.statically_known_false) + def handle_statically_known_false( + self, tx: "InstructionTranslator", expr: VariableTracker + ) -> VariableTracker | None: + if isinstance(expr, SymNodeVariable): + return VariableTracker.build( + tx, + torch.fx.experimental.symbolic_shapes.statically_known_false( + expr.sym_num + ), + ) + elif expr.is_python_constant(): + return expr + else: + return None + + @register(torch.fx.experimental.symbolic_shapes.has_free_unbacked_symbols) + def handle_has_free_unbacked_symbols( + self, tx: "InstructionTranslator", x: VariableTracker + ) -> VariableTracker | None: + from .tensor import TensorVariable + + if isinstance(x, TensorVariable): + example_value = x.as_proxy().node.meta.get("example_value") + if example_value is not None: + return variables.ConstantVariable.create( + torch.fx.experimental.symbolic_shapes.has_free_unbacked_symbols( + example_value + ) + ) + return None + + @register(torch.fx.experimental.symbolic_shapes.guard_scalar) + def guard_scalar( + self, tx: "InstructionTranslator", expr: VariableTracker + ) -> VariableTracker: + if isinstance(expr, SymNodeVariable): + val = expr.sym_num + elif expr.is_python_constant(): + val = expr.as_python_constant() + else: + unimplemented( + gb_type="torch.fx.experimental.symbolic_shapes.guard_scalar branch not supported", + context=f"expr: {expr}", + explanation="Expected `expr` to be a symbolic variable or constant.", + hints=[], + ) + return VariableTracker.build( + tx, + # pyrefly: ignore [bad-argument-type, unbound-name] + torch.fx.experimental.symbolic_shapes.guard_scalar(val), + ) + + @register(torch.fx.experimental.symbolic_shapes.statically_known_true) + def handle_statically_known_true( + self, tx: "InstructionTranslator", expr: VariableTracker + ) -> VariableTracker | None: + if isinstance(expr, SymNodeVariable): + return VariableTracker.build( + tx, + torch.fx.experimental.symbolic_shapes.statically_known_true( + expr.sym_num + ), + ) + elif expr.is_python_constant(): + return expr + else: + return None + + @register(torch.fx.experimental.symbolic_shapes.sym_and) + def handle_sym_and( + self, tx: "InstructionTranslator", *terms: VariableTracker + ) -> VariableTracker | None: + if all(isinstance(x, SymNodeVariable) for x in terms): + return SymNodeVariable.create( + tx, + torch.fx.experimental.symbolic_shapes.sym_and( + *(x.as_proxy() for x in terms) + ), + sym_num=None, + ) + return None + + @register(torch.fx.experimental.symbolic_shapes.sym_or) + def handle_sym_or( + self, tx: "InstructionTranslator", *terms: VariableTracker + ) -> VariableTracker | None: + if all(isinstance(x, SymNodeVariable) for x in terms): + return SymNodeVariable.create( + tx, + torch.fx.experimental.symbolic_shapes.sym_or( + *(x.as_proxy() for x in terms) + ), + sym_num=None, + ) + return None + + @register(torch.fx.experimental.symbolic_shapes.has_static_value) + def handle_has_static_value( + self, tx: "InstructionTranslator", expr: VariableTracker + ) -> VariableTracker | None: + if isinstance(expr, SymNodeVariable): + val = expr.sym_num + elif expr.is_python_constant(): + val = expr.as_python_constant() + else: + return None + + return VariableTracker.build( + tx, torch.fx.experimental.symbolic_shapes.has_static_value(val) + ) + + @register(torch._C._autograd._unsafe_set_version_counter) + def handle_unsafe_set_version_counter( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + from ..tensor_version_op import _unsafe_set_version_counter + + return TorchInGraphFunctionVariable( + _unsafe_set_version_counter + ).call_function(tx, [*args], kwargs) + + @register(torch._C._functorch.peek_interpreter_stack) + def handle_functorch_peek_interpreter_stack( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> UserDefinedObjectVariable: + # Wrap C++ interpreter (torch._C._functorch.CInterpreter) as UserDefinedObjectVariable, + # but Python interpreter (torch._functorch.pyfunctorch.FuncTorchInterpreter) as FuncTorchInterpreterVariable. + return UserDefinedObjectVariable( + torch._C._functorch.peek_interpreter_stack() + ) + + @register(torch._functorch.pyfunctorch.coerce_cinterpreter) + def handle_functorch_pyfunctorch_coerce_cinterpreter( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> FuncTorchInterpreterVariable: + # pyrefly: ignore[missing-attribute] + cinterpreter = args[0].value + return FuncTorchInterpreterVariable( + torch._functorch.pyfunctorch.coerce_cinterpreter(cinterpreter) + ) + + @register(torch.tensor) + def handle_torch_tensor( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker | None: + def check_any_unspec(x: VariableTracker) -> bool: + # NB: This includes UnspecializedPythonVariable + if x.is_tensor() or isinstance(x, SymNodeVariable): + return True + elif isinstance(x, (ListVariable, TupleVariable)): + return any(check_any_unspec(y) for y in x.items) + # TODO: there maybe other recursive structures you need to + # check + else: + return False + + data_arg = None + if args: + data_arg = args[0] + elif "data" in kwargs: + data_arg = kwargs["data"] + + # NB: OK to pass torch.tensor(tensor), this will trace fine + if ( + data_arg is not None + and not data_arg.is_tensor() + and check_any_unspec(data_arg) + ): + # This is slower and less canonical, so only use it if we + # have to + return TorchInGraphFunctionVariable(torch._refs.tensor).call_function( + tx, [*args], kwargs + ) + else: + return None + + @register(torch._C._pop_torch_function_stack) + def handle_pop_torch_function( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> TorchFunctionModeVariable: + assert not args and not kwargs + if not tx.symbolic_torch_function_state.mode_stack: + unimplemented( + gb_type="Attempted to pop from empty torch function mode stack", + context="", + explanation="Called `torch._C._pop_torch_function_stack` when torch function mode stack is empty.", + hints=[ + "Do not pop from empty torch function mode stack.", + *graph_break_hints.USER_ERROR, + ], + ) + TorchFunctionModeStackVariable.register_mutation(tx) + return tx.symbolic_torch_function_state.pop_torch_function_mode() + + @register(torch._C._push_on_torch_function_stack) + def handle_push_torch_function( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + if len(args) != 1 or kwargs: + raise_type_error( + tx, + f"push_torch_function takes exactly one argument ({len(args)} given)", + ) + TorchFunctionModeStackVariable.register_mutation(tx) + # type: ignore[arg-type] + tx.symbolic_torch_function_state.push_torch_function_mode(args[0]) + return ConstantVariable.create(None) + + @register(torch._C._len_torch_function_stack) + def handle_len_torch_function( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + if args or kwargs: + raise_type_error(tx, "len_torch_function_stack takes no arguments") + return VariableTracker.build( + tx, len(tx.symbolic_torch_function_state.mode_stack) + ) + + @register(torch._C._get_function_stack_at) + def handle_get_stack_at( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> TorchFunctionModeVariable: + if len(args) != 1 or kwargs: + raise_type_error( + tx, + f"get_function_stack_at takes exactly one argument ({len(args)} given)", + ) + ind = args[0].as_python_constant() + assert ind >= 0 and ind < len(tx.symbolic_torch_function_state.mode_stack) + return tx.symbolic_torch_function_state.mode_stack[ind] + + @register(torch.get_device_module.__wrapped__) + def handle_get_device_module( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + if len(args) + len(kwargs) > 1 or (kwargs and "device" not in kwargs): + unimplemented( + gb_type="improper torch.get_device_module arguments", + context=f"args={args}, kwargs={kwargs}", + explanation="torch.get_device_module accepts 1 optional argument `device`", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + try: + if kwargs: + device = kwargs["device"].as_python_constant() + elif args: + device = args[0].as_python_constant() + else: + device = None + module = torch.get_device_module(device) + except Exception as e: + unimplemented( + gb_type="bad device argument to torch.get_device_module", + context=f"args={args}, kwargs={kwargs}", + explanation="Expected valid string/torch.device argument ('cpu', 'cuda', etc.)", + hints=[*graph_break_hints.USER_ERROR], + from_exc=e, + ) + + # need to guard only on no-arg get_device_module + if device is None: + source = CallFunctionNoArgsSource(self.source) + install_guard(source.make_guard(GuardBuilder.ID_MATCH)) + # assumes `module` is in the form `torch.xyz` + new_source = AttrSource( + ImportSource("torch"), + module.__name__.rsplit(".", maxsplit=1)[-1], + ) + return VariableTracker.build(tx, module, new_source) + + @register(torch.accelerator.current_stream, torch.cuda.current_stream) + def handle_current_stream( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> StreamVariable: + from .streams import CudaStreamVariable + + if len(args) + len(kwargs) > 1 or (kwargs and "device" not in kwargs): + unimplemented( + gb_type="unsupported arguments to torch.accelerator.current_stream", + context=f"args={args}, kwargs={kwargs}", + explanation="torch.accelerator.current_stream accepts one optional argument `device`", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + try: + if kwargs: + device = torch.device(kwargs["device"].as_python_constant()) + elif args: + device = torch.device(args[0].as_python_constant()) + else: + device = None + + stream_var = tx.symbolic_stream_state.cur_stream(device) + if self.value is torch.cuda.current_stream and not isinstance( + stream_var, CudaStreamVariable + ): + stream_var = CudaStreamVariable( + stream_var.proxy, + stream_var.value, + stream_var.user_object_index, + source=stream_var.source, + ) + return stream_var + except Exception as e: + unimplemented( + gb_type="bad device argument to torch.accelerator.current_stream", + context=f"args={args}, kwargs={kwargs}", + explanation="Expected valid string/torch.device argument ('cpu', 'cuda', etc.)", + hints=[*graph_break_hints.USER_ERROR], + from_exc=e, + ) + + _synchronize_fn_to_device_type = { + torch.cuda.synchronize: "cuda", + torch.xpu.synchronize: "xpu", + torch.mps.synchronize: "mps", + torch.cpu.synchronize: "cpu", + } + + @register( + torch.accelerator.synchronize, + torch.cuda.synchronize, + torch.xpu.synchronize, + torch.mps.synchronize, + torch.cpu.synchronize, + ) + def handle_synchronize( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + device = None + if kwargs and "device" in kwargs: + device = torch.device(kwargs["device"].as_python_constant()) + elif args: + device = torch.device(args[0].as_python_constant()) + + if device is None: + device_type = _synchronize_fn_to_device_type.get(self.value) + if device_type is None: + # torch.accelerator.synchronize with no args + accelerator = torch.accelerator.current_accelerator() + assert accelerator is not None + device_type = accelerator.type + device = torch.device(device_type) + + # CPU synchronize is a no-op, skip emitting the op + if device.type == "cpu": + return ConstantVariable.create(None) + + tx.output.create_proxy( + "call_function", + torch.ops.streams.synchronize_device, + (device.type, device.index or 0), + {}, + ) + return ConstantVariable.create(None) + + @register(torch.set_default_device) + def handle_set_default_device( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + # Today this is inserted in the graph, once TF mode + # handling is complete, we can trace the device context + # like any other TF mode and remove this special handling + # Insert the TF mode representing the device context at + # the bottom of the stack to match the eager semantics + # Running the graph will ensure that the DeviceContext mode is + # at the correct position in the stack + TorchFunctionModeStackVariable.register_mutation(tx) + if args[0].is_constant_none(): + TorchFunctionModeStackVariable.clear_default_device(tx) + else: + TorchFunctionModeStackVariable.register_device_context_insertion(tx) + + return ConstantVariable.create(None) + + from torch._prims_common import elementwise_dtypes + + @register(elementwise_dtypes) + def handle_elementwise_dtypes( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + from .builder import SourcelessBuilder + + type_promotion_kind = kwargs["type_promotion_kind"].as_python_constant() + real_args = [] + for arg in args: + if isinstance(arg, TensorVariable): + real_args.append(arg.as_proxy().node.meta["example_value"]) + elif arg.is_python_constant(): + real_args.append(arg.as_python_constant()) + else: + unimplemented( + gb_type="elementwise_dtypes unsupported arg type", + context=str(arg), + explanation=( + "elementwise_dtypes requires tensor or constant arguments, " + f"got {type(arg).__name__}" + ), + hints=[*graph_break_hints.SUPPORTABLE], + ) + result = elementwise_dtypes( + *real_args, type_promotion_kind=type_promotion_kind + ) + return SourcelessBuilder.create(tx, result) + + @register(torch._check) + def handle_check( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + predicate_vt = None + message_vt = None + + if args: + predicate_vt = args[0] + rest_args = args[1:] + else: + rest_args = () + + if predicate_vt is None and "cond" in kwargs: + predicate_vt = kwargs.pop("cond") + + if rest_args: + message_vt = rest_args[0] + elif "message" in kwargs: + message_vt = kwargs.pop("message") + + if predicate_vt is None: + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + self.value, + (), + {}, + ), + ) + + message_eager = None + message_graph_proxy = None + if message_vt is not None: + if ( + not isinstance(message_vt, NestedUserFunctionVariable) + or message_vt.has_closure() + ): + unimplemented( + gb_type="Can't extract message from torch._check()", + context=str(message_vt), + explanation=( + "The second argument of torch._check() must be a function " + "defined within the torch.compile region " + "that does not reference a non-local variable." + ), + hints=[ + "Make sure the message function is defined in the torch.compile region.", + "Remove any closure variables, e.g. " + "remove references to closure variable `x` in `lambda: f'{x} failed check'`", + *graph_break_hints.SUPPORTABLE, + ], + ) + message_eager = message_vt.get_function() + + message_graph_proxy = tx.output.register_static_attr_and_return_proxy( + "_check_message", message_eager + ) + + if predicate_vt.is_python_constant(): + self.value(predicate_vt.as_python_constant(), message_eager) + return ConstantVariable.create(None) + + predicate_proxy = predicate_vt.as_proxy() + + proxy_args: tuple[Any, ...] + if message_graph_proxy is None: + proxy_args = (predicate_proxy,) + else: + proxy_args = (predicate_proxy, message_graph_proxy) + + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + self.value, + proxy_args, + {}, + ), + ) + + def exchange_device_helper( + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + fn: Callable[[int], int | None], + ) -> VariableTracker: + if len(args) != 1 or kwargs: + raise_type_error( + tx, + f"{fn.__name__} takes exactly one argument ({len(args)} given)", + ) + current_device_source = CallFunctionNoArgsSource( + AttrSource(AttrSource(ImportSource("torch"), "cuda"), "current_device") + ) + install_guard(current_device_source.make_guard(GuardBuilder.EQUALS_MATCH)) + arg = args[0].as_python_constant() + prev = fn(arg) + tx.output.create_node( + "call_function", + fn, + (arg,), + {}, + ) + tx.output.add_cleanup_hook(lambda: torch.cuda.set_device(prev)) + return VariableTracker.build(tx, prev) + + @register(torch.cuda._exchange_device) + def handle_exchange_device( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + return exchange_device_helper(tx, args, kwargs, torch.cuda._exchange_device) + + @register(torch.cuda._maybe_exchange_device) + def handle_maybe_exchange_device( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker: + return exchange_device_helper( + tx, args, kwargs, torch.cuda._maybe_exchange_device + ) + + @register(torch.autograd.grad) + def handle_autograd_grad(self, tx: "InstructionTranslator", *args, **kwargs): + """ + Handle torch.autograd.grad() calls within compiled regions. + + NOTE [Tracing autograd.grad in dynamo] + + We validate two things: + + 1. External grad_fns cannot be consumed: The grad_fn on external inputs + could change at runtime, so we would need to guard on it if we wanted + to trace through it. For now, we reject this case. + We compute "consumed" grad_fns (reachable from outputs, excluding + autograd.grad inputs parameter) and verify no graph input's grad_fn is in this set. + + 2. Returned tensors cannot have consumed grad_fns: If autograd.grad + consumes a grad_fn and we return a tensor connected to it, the user + would get "backward through graph a second time" error. We track + consumed grad_fns and check at output time. If violated, we retry + with a graph break at autograd.grad. + + Safe vs Unsafe Cases: + + Case 1 - Safe (external tensor is autograd.grad input): + x = torch.randn(4, requires_grad=True) + external = x * 2 # has external grad_fn + + @torch.compile + def fn(external_input): + loss = external_input.sum() + return torch.autograd.grad(loss, external_input) + + Safe because autograd.grad stops at external_input, never consuming + its external grad_fn. + + Case 2 - Unsafe (external grad_fn in path): + @torch.compile + def fn(external_input): + loss = mod(external_input).sum() + return torch.autograd.grad(loss, mod.weight) + + Unsafe because autograd.grad must traverse through external_input's + grad_fn to reach mod.weight. The external grad_fn could change at + runtime, so we would need to guard on it (like AOTAutograd does). + For now, we reject this case. + + Case 3 - Unsafe (returning tensor with consumed grad_fn): + @torch.compile + def fn(x): + y = x * 2 + grad = torch.autograd.grad(y.sum(), x) + return y, grad # y's grad_fn was consumed! + + Unsafe because y's grad_fn was consumed by autograd.grad. Trying to + backward through y later would error. + """ + from .. import compiled_autograd, config + from .builder import wrap_fx_proxy + from .constant import ConstantVariable + from .tensor import TensorVariable + + if not config.trace_autograd_ops: + unimplemented( + gb_type="using `torch.autograd.grad` with `torch._dynamo.config.trace_autograd_ops=False`", + context=f"trace_autograd_ops={config.trace_autograd_ops}", + explanation=( + "Attempted to call `torch.autograd.grad` with config " + "`torch._dynamo.config.trace_autograd_ops` set to `False`." + ), + hints=[ + "Change `torch._dynamo.config.trace_autograd_ops` to `True`.", + ], + ) + + # Graph break if we detected on a previous attempt that autograd.grad + # consumed grad_fns of returned tensors. This gives better compile + # coverage than failing the entire compile. + if tx.speculation_log.graph_break_on_autograd_grad: + leaked = tx.speculation_log.autograd_grad_leaked_tensors + leaked_str = ", ".join(leaked) if leaked else "unknown" + unimplemented( + gb_type="autograd.grad consumed returned tensor's grad_fn", + context=f"Leaked output tensors: {leaked_str}", + explanation=( + "torch.autograd.grad() consumes grad_fns that are needed by tensors " + "returned from this compiled function. This would cause 'backward " + "through graph a second time' errors.\n" + f" The following returned tensors have consumed grad_fns: {leaked_str}" + ), + hints=[ + f"Detach the problematic tensor(s) before returning: e.g. `{leaked[0]}.detach()`" + if leaked + else "Call .detach() on the tensor before returning.", + "If you need to backward through the returned tensor, use retain_graph=True in autograd.grad().", + ], + ) + + # Graph break if compiled_autograd is enabled. + # Compiled autograd has limitations (e.g., view_fn in CopySlices) + # that would cause errors during fake tensor execution. + if compiled_autograd.compiled_autograd_enabled: + unimplemented( + gb_type="autograd.grad with compiled autograd", + context="compiled_autograd is currently enabled", + explanation=( + "torch.autograd.grad() inside torch.compile is not supported when " + "compiled autograd is enabled. These two features have conflicting " + "requirements for how the autograd graph is traced." + ), + hints=[ + "Disable compiled autograd by removing the compiled_autograd context manager.", + "Or move the autograd.grad() call outside the torch.compile region.", + "Or restructure your code so autograd.grad() and compiled_autograd don't overlap.", + ], + ) + + # Check for external GradientEdge objects in outputs and inputs args + # if there is it will be a graph break + if len(args) >= 1: + _check_for_gradient_edge(args[0], "outputs") + if len(args) >= 2: + _check_for_gradient_edge(args[1], "inputs") + + # Collect external grad_fn objects from graph inputs, along with their sources. + # We need to collect ALL grad_fns associated with each input tensor: + # - Direct grad_fn, base tensor's grad_fn (for views) + # - Inner tensors (for subclasses) + external_grad_fns: set[torch.autograd.graph.Node] = set() + # Map grad_fn -> source name for better error messages + grad_fn_to_source: dict[torch.autograd.graph.Node, str] = {} + for var in tx.output.input_source_to_var.values(): + if isinstance(var, TensorVariable): + fake_tensor = var.as_proxy().node.meta.get("example_value") + assert isinstance(fake_tensor, torch.Tensor) + tensor_grad_fns = _collect_all_grad_fns(fake_tensor) + external_grad_fns.update(tensor_grad_fns) + # Track source name for error messages + if var.source is not None: + for gf in tensor_grad_fns: + grad_fn_to_source[gf] = var.source.name + + # Collect tensors from outputs and inputs args + from ..output_graph import collect_reachable_grad_fns + + outputs_with_sources = ( + _collect_tensors_with_sources(args[0]) if len(args) >= 1 else [] + ) + inputs_with_sources = ( + _collect_tensors_with_sources(args[1]) if len(args) >= 2 else [] + ) + + # Collect grad_fns from the autograd.grad inputs tensors to use as stop points. + # For non-leaf tensors: we stop at their grad_fn + # For leaf tensors (requires_grad=True, grad_fn=None): we don't add anything here, + # but this is fine because their AccumulateGrad is created during fake tensor + # tracing and is not in external_grad_fns, so it won't trigger a false positive. + inputs_grad_fns: set[torch.autograd.graph.Node] = set() + for tensor, _ in inputs_with_sources: + if isinstance(tensor, torch.Tensor) and tensor.grad_fn is not None: + inputs_grad_fns.add(tensor.grad_fn) + + # Collect all consumed grad_fns that are reachable from outputs, stopping at inputs. + # + # Note: Do not try to "optimize" by only checking inputs in the `inputs` arg. + # Without guarding on the autograd graph, we can't distinguish: + # Case 1: x, y are independent leaves -> OK, y's path not consumed + # Case 2: y = x * 2 (y.grad_fn external) -> BAD, we hit external grad_fn + # Since the same compiled code could be called with either, we must check + # ALL graph inputs for external grad_fns. + consumed_grad_fns = collect_reachable_grad_fns( + outputs_with_sources, stop_at=inputs_grad_fns + ) + + # Check if any graph input's grad_fn is in the consumed set. + # If so, autograd.grad would need to traverse through external autograd nodes, + # which we cannot trace. (If a graph input is also an autograd.grad input, + # its grad_fn is already excluded from consumed_grad_fns via stop_at.) + external_in_consumed = consumed_grad_fns & external_grad_fns + + if external_in_consumed: + sources = [ + grad_fn_to_source[gf] + for gf in external_in_consumed + if gf in grad_fn_to_source + ] + context = f"inputs with external grad_fn: {sources}" if sources else "" + unimplemented( + gb_type="autograd.grad with external grad_fn", + context=context, + explanation=( + "torch.autograd.grad() cannot trace through the autograd graph because " + "it's output depends on a tensor that was created outside " + "the compiled region and has a grad_fn attached. The autograd graph " + "extends beyond the compiled region boundary, which Dynamo cannot trace." + ), + hints=[ + "If you don't need gradients to flow back to the original tensor outside " + "the compiled region, detach the input: `tensor.detach().requires_grad_(True)`.", + "Otherwise, move the autograd.grad() call outside the compiled region.", + *graph_break_hints.SUPPORTABLE, + ], + ) + + # Track consumed grad_fns for later validation + # (to detect returning tensors whose grad_fn was consumed by autograd.grad) + # Skip if retain_graph=True or create_graph=True since the graph is not + # consumed in those cases and can be traversed again. + retain_graph = kwargs.get("retain_graph") + create_graph = kwargs.get("create_graph") + graph_preserved = ( + isinstance(retain_graph, ConstantVariable) + and retain_graph.value is True + ) or ( + isinstance(create_graph, ConstantVariable) + and create_graph.value is True + ) + if not graph_preserved: + # Filter out AccumulateGrad nodes - they're never actually "consumed" + # by autograd. They just accumulate gradients into leaf.grad and can + # be traversed multiple times without issues. + non_leaf_consumed = { + gf + for gf in consumed_grad_fns + if type(gf).__name__ != "AccumulateGrad" + } + + # Check for double-consumption: if any grad_fn was already consumed + # by a previous autograd.grad, that's an error. + already_consumed = tx.output.autograd_grad_consumed_grad_fns + double_consumed = non_leaf_consumed & already_consumed + if double_consumed: + unimplemented( + gb_type="autograd.grad with already consumed grad_fn", + context=f"double consumed grad_fns: {len(double_consumed)}", + explanation=( + "torch.autograd.grad() is trying to consume grad_fns that were " + "already consumed by a previous autograd.grad() call. This would " + "cause 'backward through graph a second time' errors at runtime." + ), + hints=[ + "Use retain_graph=True in the first autograd.grad() call if you " + "need to compute gradients through the same graph multiple times.", + ], + ) + tx.output.autograd_grad_consumed_grad_fns.update(non_leaf_consumed) + + with ( + torch.fx.traceback.preserve_node_meta(), + torch.fx.traceback._set_autograd_backward(), + ): + proxy = tx.output.create_proxy( + "call_function", + torch.autograd.grad, + *proxy_args_kwargs(args, kwargs), + ) + return wrap_fx_proxy(tx=tx, proxy=proxy) + + @register(torch._functorch.eager_transforms._autograd_grad) + def handle_functorch_autograd_grad( + self, + tx: "InstructionTranslator", + *args: VariableTracker, + **kwargs: VariableTracker, + ) -> VariableTracker | None: + """Graph-break when closure-captured tensors lose their grad_fn. + + NOTE [Detecting lost autograd linkage in closure-captured tensors] + + Functorch transforms (vjp, grad, jacrev) return closures that capture + tensors with grad_fn. When such a closure is compiled separately, those + tensors become graph placeholders whose FakeTensors lose grad_fn, + causing _autograd_grad to silently return zeros. + + _collect_placeholder_nodes gathers placeholder nodes from the + outputs/inputs args. For each, we compare grapharg.example (the real + tensor, retains grad_fn) against example_value (FakeTensor, grad_fn + is None). A mismatch means autograd linkage was lost, so we + graph-break. + + This is a pre-check only: kwargs (retain_graph, create_graph, + grad_outputs) don't affect linkage detection and are handled by + the default proxy path when this returns None. + """ + outputs_var = args[0] if len(args) >= 1 else None + inputs_var = args[1] if len(args) >= 2 else None + + if outputs_var is None or inputs_var is None: + return None + + output_placeholder_nodes = _collect_placeholder_nodes(outputs_var) + input_placeholder_nodes = _collect_placeholder_nodes(inputs_var) + + if output_placeholder_nodes and input_placeholder_nodes: + for node in output_placeholder_nodes: + fake = node.meta.get("example_value") + grapharg = node.meta.get("grapharg") + if ( + grapharg is not None + and isinstance(fake, torch.Tensor) + and fake.grad_fn is None + ): + real = grapharg.example + if isinstance(real, torch.Tensor) and real.grad_fn is not None: + unimplemented( + gb_type="_autograd_grad with lost grad_fn linkage", + context="outputs lost autograd linkage during tracing", + explanation=( + "_autograd_grad() received tensors whose grad_fn " + "was lost during tracing - this silently produces " + "zero gradients." + ), + hints=[ + "Compile the full transform instead of the returned " + "closure: torch.compile(lambda x: torch.func.vjp(f, x))", + *graph_break_hints.SUPPORTABLE, + ], + ) + return None + + return handlers + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: "dict[str, VariableTracker]", + ) -> "VariableTracker": + from . import SymNodeVariable + from .builder import wrap_fx_proxy + + if self.kind == AllowInGraphKind.NONSTRICT_TRACE: + return self._call_nonstrict_traceable_function(tx, args, kwargs) + + if self.kind == AllowInGraphKind.LEAF_FUNCTION: + return self._call_leaf_function(tx, args, kwargs) + + if self.torch_function_override_enabled(tx, args, kwargs): + return dispatch_torch_function(tx, self, args, kwargs) + + if self.can_constant_fold_through() and check_unspec_or_constant_args( + args, kwargs + ): + # constant fold functions need to be guarded. + if self.value in constant_fold_functions_need_guards: + assert self.source is not None + source = CallFunctionNoArgsSource(self.source) + install_guard(source.make_guard(GuardBuilder.EQUALS_MATCH)) + # constant fold + try: + return VariableTracker.build( + tx, + self.as_python_constant()( + *[x.as_python_constant() for x in args], + **{k: v.as_python_constant() for k, v in kwargs.items()}, + ), + ) + except (OverflowError, TypeError, ValueError) as exc: + raise_observed_exception( + type(exc), + tx, + args=list(exc.args), + ) + + if self.is_tensor_method(): + name = self.value.__name__ + # Guard against inplace view op on input tensor (not supported) + if args and args[0].is_tensor(): + tensor_var = args[0] + # Check if input tensor and inplace_view op specifically + if tensor_var.source is not None and hasattr(torch.ops.aten, name): + fn = getattr(torch.ops.aten, name) + if ( + hasattr(fn, "overloads") + and hasattr(fn, fn.overloads()[0]) + and torch.Tag.inplace_view + in getattr(fn, fn.overloads()[0]).tags + ): + unimplemented( + gb_type="Inplace op on input tensor", + context="", + explanation=f"Attempted to trace an inplace view op on input tensor {typestr(self.value)}.", + hints=[ + *graph_break_hints.SUPPORTABLE, + "Ensure you do not modify input tensor in place.", + ], + ) + return self.call_tensor_method(tx, list(args), kwargs) + + special_handler = self._get_handlers().get(self.value) + if special_handler: + result = special_handler(self, tx, *args, **kwargs) + if result: + return result + + any_symints_or_symfloats = any(isinstance(x, SymNodeVariable) for x in args) + + all_ints_or_floats = all( + isinstance(x, SymNodeVariable) or x.is_python_constant() for x in args + ) + if ( + getattr(self.value, "__module__", "") == "torch" + and self.value.__name__ in bin_ops + and any_symints_or_symfloats + and all_ints_or_floats + ): + msg = f"""\ +Calling {str(self.value)} on only torch.SymInt arguments is not yet supported. +To support this behavior, we need to allow const-propping tensors that store symint data. +For now, dynamo will explicitly graph break when it encounters user code with this behavior. +""" + log.warning(msg) + unimplemented( + gb_type="Attempted to call torch in-graph function on only torch.SymInt arguments", + context=f"fn={self.value}, args={args}, kwargs={kwargs}", + explanation=( + f"Attempted to call {str(self.value)} (that should be put in the FX graph) on only torch.SymInt arguments. " + "Dynamo does not support this." + ), + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + ) + + # TODO(voz): Replace w/ dynamic shape rewrite table. + # Ideally, we would be able to do this at ctor time, but alas we need a combination + # of value + args to determine this. + fn_ = self.value + if any_symints_or_symfloats: + torch_sym_op = f"_sym_{self.value.__name__}" + if getattr(self.value, "__module__", None) == "math" and hasattr( + torch, torch_sym_op + ): + fn_ = getattr(torch, torch_sym_op) + + # TODO for each of the following check on `out=` or `requires_grad=` + # variant torch ops, the original function could come from a user + # defined `@allow_in_graph` function as well, which doesn't have the + # same semantics as the torch ops. + + # Calling fake tensor propagation can mutate the out= tensor in + # tx.output.tracked_fakes. tracked_fakes are used to apply + # symbolic_shape guards. Mutating them destroys the information + # prior to tracing, which is essential for creating right + # guards. So save the shape now, and check later if it has + # changed. If it has, graph break. + saved_out_shapes = None + out_kwarg_vt = None + if "out" in kwargs: + out_kwarg_vt = kwargs["out"] + + # e.g., out=(t1, t2, ...) + if isinstance(out_kwarg_vt, (TupleVariable, ListVariable)): + saved_out_shapes = [] + for vt in out_kwarg_vt.items: + if vt.is_tensor(): + shape = vt.as_proxy().node.meta["example_value"].shape + else: + shape = None + saved_out_shapes.append(shape) + + # e.g., out=output_tensor + if out_kwarg_vt.is_tensor(): + saved_out_shapes = ( + out_kwarg_vt.as_proxy().node.meta["example_value"].shape + ) + + # Ops that consume scalar values from tensors (via .item()) for computation only, + # not for output shapes. When capture_scalar_outputs is enabled, these ops would + # create unbacked symbols that are not in the outputs, causing + # PendingUnbackedSymbolNotFound errors. We ignore these fresh unbacked symbols + # since they only affect tensor values, not shapes. + ops_consuming_unbacked_scalars = { + # foreach ops with scalar/alpha arguments + torch._foreach_add, + torch._foreach_add_, + torch._foreach_sub, + torch._foreach_sub_, + torch._foreach_mul, + torch._foreach_mul_, + torch._foreach_div, + torch._foreach_div_, + torch._foreach_clamp_max, + torch._foreach_clamp_max_, + torch._foreach_clamp_min, + torch._foreach_clamp_min_, + torch._foreach_maximum, + torch._foreach_maximum_, + torch._foreach_minimum, + torch._foreach_minimum_, + torch._foreach_pow, + torch._foreach_pow_, + torch._foreach_lerp, + torch._foreach_lerp_, + torch._foreach_addcmul, + torch._foreach_addcmul_, + torch._foreach_addcdiv, + torch._foreach_addcdiv_, + } + ctx = nullcontext + if fn_ in ops_consuming_unbacked_scalars: + if tx.fake_mode and tx.fake_mode.shape_env: + ctx = tx.fake_mode.shape_env.ignore_fresh_unbacked_symbols + + with ctx(): + tensor_variable = wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + fn_, + *proxy_args_kwargs(args, kwargs), + ), + ) + + # Handle e.g., `torch.ones(10, requires_grad=True)` + if ( + tensor_variable.is_tensor() + and "requires_grad" in kwargs + and kwargs["requires_grad"].as_python_constant() + ): + unimplemented( + gb_type="Attempted to use tensor creation function with requires_grad=True", + context=f"fn={self.value}, args={args}, kwargs={kwargs}", + explanation="Dynamo does not support this.", + hints=[ + "Create the tensor outside the compiled region.", + "Do not set `requires_grad=True`.", + *graph_break_hints.SUPPORTABLE, + ], + ) + + # Handle e.g., `torch.add(a, b, out=result)` + if saved_out_shapes is not None: + # out variants of torch operators like torch.sort and torch.sigmoid + # mutate the tensors in the out field. + # + # However, it's non-trivial to update all references of the old + # `TensorVariable` to the new one returned (`result_var`), so we + # take the conservative approach to graph break on size changes, and + # assume other cases can fall through soundly. + # + # Note that although these tensor variables would hold different + # proxies, the in-place mutation semantics is preserved in the FX + # graph, so we won't have correctness issues. + if isinstance(saved_out_shapes, list): + for out_tensor_vt, saved_out_shape in zip( + out_kwarg_vt.items, # type: ignore[union-attr] + saved_out_shapes, + ): + if saved_out_shape is None: + # This should be extremely rare, but it's kept for now + # until we invest in enforcing the `out=` kwarg for only + # torch methods. + continue + + assert out_tensor_vt.is_tensor() + fake_out = out_tensor_vt.proxy.node.meta["example_value"] + if saved_out_shape != fake_out.shape: + # It's hard to get out variants with resizing on graph inputs work + # properly across dynamo/aot/inductor, just fall back. + unimplemented( + gb_type="Shape mismatch with out= list of tensor variants", + context=f"fn={self.value}, args={args}, kwargs={kwargs}", + explanation=( + f"Shape mismatch when calling {self.value} with `out=`. " + f"Provided `out=` shape: {saved_out_shape}. Actual shape: {fake_out.shape}." + ), + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + ) + if not torch._prims_common.is_contiguous(fake_out): + # It's difficult to handle strides correctly in functionalization + # when calling an out= op with a non-contiguous out argument + unimplemented( + gb_type="Attempted to call op with non-contiguous `out=` list of tensors", + context=f"self.value={self.value}, args={args}, kwargs={kwargs}", + explanation="Dynamo does not support this.", + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + ) + else: + assert out_kwarg_vt is not None and out_kwarg_vt.is_tensor() + assert "example_value" in out_kwarg_vt.as_proxy().node.meta + fake_out = out_kwarg_vt.as_proxy().node.meta["example_value"] + if saved_out_shapes != fake_out.shape: + # It's hard to get out variants with resizing on graph inputs work + # properly across dynamo/aot/inductor, just fall back. + unimplemented( + gb_type="Shape mismatch with out= tensor variant", + context=f"fn={self.value}, args={args}, kwargs={kwargs}", + explanation=( + f"Shape mismatch when calling {self.value} with `out=`. " + f"Provided `out=` shape: {saved_out_shapes}. Actual shape: {fake_out.shape}." + ), + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + ) + if not torch._prims_common.is_contiguous_or_false(fake_out): + # It's difficult to handle strides correctly in functionalization + # when calling an out= op with a non-contiguous out argument + unimplemented( + gb_type="Attempted to call op with non-contiguous `out=` tensor", + context=f"self.value={self.value}, args={args}, kwargs={kwargs}", + explanation="Dynamo does not support this.", + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + ) + + return tensor_variable + + def _call_nonstrict_traceable_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: "dict[str, VariableTracker]", + ) -> VariableTracker: + from torch._dynamo.utils import _make_inlined + from torch._higher_order_ops.flat_apply import ( + flat_apply, + is_graphable_type, + is_valid_output, + to_graphable, + ) + from torch._higher_order_ops.invoke_leaf_function import _LeafCallable + from torch._subclasses.fake_tensor import fake_tensor_tls + from torch.utils._pytree import tree_flatten + + from .base import AsPythonConstantNotImplementedError + from .builder import SourcelessBuilder, wrap_fx_proxy + + # 1. Convert `args, kwargs` into pytree-flattened proxy forms. + # + # Rather than reconstructing `args, kwargs` into python objects and + # then tree_flatten them, we just let Dynamo symbolically interpret + # `tree_flatten((args, kwargs))`. This saves us from having to + # worry about the reconstruction logic, side effects, and guards. + args_with_states, kwargs_with_states = self._extract_nn_module_states( + tx, args, kwargs + ) + flat_args_vts, input_spec_vt = _make_inlined(tx, tree_flatten)( + VariableTracker.build(tx, (args_with_states, kwargs_with_states)) + ).unpack_var_sequence(tx) + assert isinstance(flat_args_vts, ListVariable) + + # Handle the case when the input contains a non-graphable type. + for flat_arg_vt in flat_args_vts.items: + arg_type = flat_arg_vt.python_type() + if not is_graphable_type(arg_type): + type_name = flat_arg_vt.python_type().__qualname__ + unimplemented( + gb_type="Invalid input type for nonstrict_trace-ed function", + context=f"Encountered input of type <{type_name}>.", + explanation=( + "For `nonstrict_trace`-ed functions, only basic types (e.g., torch.Tensor, int, float) " + "or pytree containers of those are allowed as inputs. The provided argument contains " + "an unsupported type." + ), + hints=[ + "Use one of the following to register the type with pytree:\n" + "* `torch.utils._pytree.register_constant`\n" + "* `torch.utils._pytree.register_dataclass`\n" + "* `torch.utils._pytree.register_pytree_node`", + ], + ) + + # Since we checked with `is_graphable` above, `as_proxy` on the + # flat_arg VT should always work. + proxified_flat_args = [ + flat_arg_vt.as_proxy() for flat_arg_vt in flat_args_vts.items + ] + + # The downstream `flat_apply` call requires the input spec; however, + # the spec not a graphable type, so we still have to reconstruct it + # into a python object, and store it as a constant attribute on the + # fx graph. + try: + input_spec = input_spec_vt.as_python_constant() + except AsPythonConstantNotImplementedError as e: + typ = e.vt.python_type() + type_name = typ.__qualname__ + import torch.utils._pytree as pytree + + if pytree.is_constant_class(typ): + unimplemented( + gb_type="Input marked with `pytree.register_constant` constructed in the `torch.compile` region", + context=f"Input={input_spec_vt}, offending type <{type_name}>.", + explanation=( + "Calling a `nonstrict_trace`-ed function with an input that contains an object " + f"of type <{type_name}>, which was marked with `pytree.register_constant`. However, the object " + "was constructed _inside_ the `torch.compile` region. This is not supported." + ), + hints=[ + "Construct the object _outside_ the `torch.compile` region, or submit an issue to GitHub.", + *graph_break_hints.SUPPORTABLE, + ], + from_exc=e, + ) + else: + unimplemented( + gb_type="Invalid use of pytree_flatten with nonstrict_trace-ed function", + context=f"Input={input_spec_vt}, offending type <{type_name}>.", + explanation=( + "Calling a `nonstrict_trace`-ed function where one of the inputs has been registered " + f"with a `pytree_flatten` that places an object of type <{type_name}> into the context." + ), + hints=[ + "Modifying the `pytree_flatten` to avoid placing the object into the context.", + f"Apply one of the following to <{type_name}>:\n" + "* `torch.utils._pytree.register_constant`\n" + "* `torch.utils._pytree.register_dataclass`\n" + "* `torch.utils._pytree.register_pytree_node`", + *graph_break_hints.SUPPORTABLE, + ], + from_exc=e, + ) + + fn = self.value + + def patched_fn( + *args: VariableTracker, **kwargs: VariableTracker + ) -> VariableTracker: + # This enables reads to global/captured tensors, and we'll just + # treat them as constants in the graph. Note that after + # AOTDispatcher, this logic would disappear. + old_val = fake_tensor_tls.allow_non_fake_inputs_override + fake_tensor_tls.allow_non_fake_inputs_override = True + try: + res = fn(*args, **kwargs) + finally: # reset even when `fn` raises + fake_tensor_tls.allow_non_fake_inputs_override = old_val + return res + + f_callable = _LeafCallable(patched_fn) + + f_callable_proxy = tx.output.register_static_attr_and_return_proxy( + f"{fn.__name__}_callable", f_callable + ) + input_spec_proxy = tx.output.register_static_attr_and_return_proxy( + fn.__name__ + "_input_spec", + # pyrefly: ignore [unbound-name] + input_spec, + ) + f_callable_proxy.node.type = type(f_callable) + # pyrefly: ignore [unbound-name] + input_spec_proxy.node.type = type(input_spec) + all_args = (f_callable_proxy, input_spec_proxy, *proxified_flat_args) + + # 2. Create a proxy call to `flat_apply`, then fake-tensor propagate + # the call and wrap output into a VariableTracker. + + # What's going on here? The output of the nonstrict-traced function must + # be something we can put into the graph. This means it has to be Tuple, + # int, str, etc or lists/tuples of those (or lists of lists of those, + # etc). So by default we don't handle PyTree-able outputs. + + # To handle PyTree-able outputs we flatten the output to a flattened + # list of graph types and then trace the unflattening into the graph. + captured_spec: TreeSpec | None = None + + def flat_apply_capture(*args: Any) -> list[object]: + nonlocal captured_spec + out = flat_apply(*args, checked_output=False) + # Output is handled similar to flat_apply input but reverse by + # tree_flattening the output and trace the unflattening. Note that + # wrapped functions must return the same pytree structure every time + # they're called. + flat_out, spec = to_graphable(out) + if captured_spec is None: + captured_spec = spec + else: + assert captured_spec == spec, ( + "Error: nonstrict-traced functions must return the same " + f"output shape every time. got {spec!r} vs but expected {captured_spec!r}" + ) + assert is_valid_output(flat_out) + return flat_out + + proxy = tx.output.create_proxy( + "call_function", flat_apply_capture, all_args, {} + ) + + # Instead of calling tree_unflatten at runtime, symbolically trace it + # just like we did for tree_flatten on inputs. This lets Dynamo + # capture the unflatten into the FX graph as well. + + # Build VTs representing (flat_output_list, out_spec) + try: + proxy_list_vt = wrap_fx_proxy(tx, proxy) + except ( + # From `handle_traced_output`. + torch._dynamo.exc.Unsupported, + # From `flat_apply` assert on output type. + torch._dynamo.exc.TorchRuntimeError, + ): + unimplemented( + gb_type="Unsupported output type for nonstrict_trace-ed function", + context=f"Function: {fn.__name__}", + explanation=( + "For `nonstrict_trace`-ed functions, only basic types (e.g., torch.Tensor, int, list)" + " are allowed as output. The result of this call contains an unsupported type." + ), + hints=[*graph_break_hints.SUPPORTABLE], + ) + # pyrefly error: why doesn't it recognize unimplemented() as NoReturn? + raise AssertionError("unreachable") # noqa: B904 + + assert captured_spec is not None + out_spec_vt = VariableTracker.build(tx, captured_spec) + + # Reuse the same pattern used above for tree_flatten: call the python + # function through Dynamo so it symbolically interprets it. + out_vt = SourcelessBuilder.create(tx, _pytree.tree_unflatten).call_function( + tx, [proxy_list_vt, out_spec_vt], {} + ) + + return out_vt + + def _extract_nn_module_states( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> tuple[VariableTracker, VariableTracker]: + """ + Extract nn.Module states from arguments for leaf function invocation. + + Replaces nn.Module arguments with LeafModuleState objects containing + the module's index (for later retrieval), parameters, and buffers. + """ + import torch.utils._pytree as pytree + from torch._dynamo.graph_bytecode_inputs import register_user_object + from torch._dynamo.utils import _make_inlined + from torch._higher_order_ops.invoke_leaf_function import ( + convert_modules_to_states, + ) + + from .nn_module import NNModuleVariable, UnspecializedNNModuleVariable + + def is_module_variable( + var: VariableTracker, + ) -> TypeIs[Union["NNModuleVariable", "UnspecializedNNModuleVariable"]]: + return isinstance(var, (NNModuleVariable, UnspecializedNNModuleVariable)) + + flat_args_var, tree_spec_var = _make_inlined(tx, pytree.tree_flatten)( + VariableTracker.build(tx, (args, kwargs)) + ).unpack_var_sequence(tx) + + module_to_index: dict[int, int] = {} + for arg in flat_args_var.unpack_var_sequence(tx): + if is_module_variable(arg): + if arg.source is None: + unimplemented( + gb_type="leaf_function: nn.Module argument without source", + context=f"module type: {type(arg.value).__name__}", + explanation=( + "leaf_function received an nn.Module argument that cannot be " + "traced back to its origin. This typically happens when the " + "module is created dynamically inside the compiled region." + ), + hints=[ + "Ensure the nn.Module is created outside the compiled function " + "and passed as an argument.", + "If the module is a class attribute, access it via self.module_name.", + ], + ) + assert arg.source is not None # make linter happy + module_to_index[id(arg.value)] = register_user_object( + arg.value, arg.source + ) + + if not module_to_index: + args_var = VariableTracker.build(tx, tuple(args)) + kwargs_var = VariableTracker.build(tx, kwargs) + return args_var, kwargs_var + + module_to_index_var = VariableTracker.build(tx, module_to_index) + + result_var = _make_inlined(tx, convert_modules_to_states)( + VariableTracker.build(tx, (args, kwargs)), module_to_index_var + ) + return result_var.unpack_var_sequence(tx) # pyrefly: ignore [bad-return] + + def _call_leaf_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + import torch.utils._pytree as pytree + from torch._dynamo.utils import _make_inlined + from torch._higher_order_ops.invoke_leaf_function import ( + _LeafCallable, + invoke_leaf_function, + make_leaf_function_wrappers, + ) + + from .builder import wrap_fx_proxy + + decorated_fn = self.value + real_impl = decorated_fn._torchdynamo_leaf_real_fn + fake_impl = decorated_fn._torchdynamo_leaf_fake_fn + mutates_args = decorated_fn._torchdynamo_leaf_mutates_args + + if fake_impl is None: + raise ValueError( + f"leaf_function '{getattr(decorated_fn, '__name__', decorated_fn)}' " + "requires a fake implementation. Please provide one using the @.register_fake " + "decorator. See the leaf_function docstring for details." + ) + + args_with_states, kwargs_with_states = self._extract_nn_module_states( + tx, args, kwargs + ) + + flat_args_var, input_spec_var = _make_inlined(tx, pytree.tree_flatten)( + VariableTracker.build(tx, (args_with_states, kwargs_with_states)) + ).unpack_var_sequence(tx) + flat_arg_proxies = [ + arg.as_proxy() for arg in flat_args_var.unpack_var_sequence(tx) + ] + input_spec = input_spec_var.as_python_constant() + + mutated_flat_indices = "" + if mutates_args: + from torch._higher_order_ops.invoke_leaf_function import ( + _resolve_mutated_flat_indices, + ) + + try: + mutated_flat_indices = _resolve_mutated_flat_indices( + real_impl, mutates_args, len(flat_arg_proxies), input_spec + ) + except ValueError as e: + raise UserError(UserErrorType.INVALID_INPUT, str(e)) from e + + # Single-element mutable list so the wrappers can write back the output + # TreeSpec. Read captured_out_spec[0] after the wrappers have been called. + captured_out_spec: list[pytree.TreeSpec | None] = [None] + wrapped_real_impl, wrapped_fake_impl = make_leaf_function_wrappers( + real_impl, fake_impl, captured_out_spec + ) + + real_impl_callable = _LeafCallable(wrapped_real_impl) + fake_impl_callable = _LeafCallable(wrapped_fake_impl) + + def make_callable_proxy(name: str, spec: Any) -> Any: + proxy = tx.output.register_static_attr_and_return_proxy(name, spec) + proxy.node.type = type(spec) + return proxy + + real_impl_proxy = make_callable_proxy("real_fn", real_impl_callable) + fake_impl_proxy = make_callable_proxy("fake_fn", fake_impl_callable) + input_spec_proxy = make_callable_proxy("input_spec", input_spec) + + invoke_args = ( + real_impl_proxy, + fake_impl_proxy, + input_spec_proxy, + mutated_flat_indices, + *flat_arg_proxies, + ) + result_proxy = tx.output.create_proxy( + "call_function", invoke_leaf_function, invoke_args, {} + ) + + flat_output_vt = wrap_fx_proxy(tx, result_proxy) + + assert captured_out_spec[0] is not None, ( + "Output spec was not captured during fake tensor propagation. " + "This should not happen - please report a bug." + ) + out_spec_vt = VariableTracker.build(tx, captured_out_spec[0]) + return _make_inlined(tx, _pytree.tree_unflatten)(flat_output_vt, out_spec_vt) + + def _call_ntuple( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + """inline behavior of torch.nn.modules.utils._ntuple""" + if self.value is torch.nn.modules.utils._ntuple: + count = args[0].as_python_constant() + else: + count = self.value.__closure__[0].cell_contents + assert isinstance(count, int) + assert not kwargs + + def handle_ntuple(value: VariableTracker) -> VariableTracker: + if value.has_unpack_var_sequence(tx): + return variables.TupleVariable( + list(value.unpack_var_sequence(tx)), + ) + elif value.is_python_constant(): + # constant prop through it + return VariableTracker.build( + tx, + torch.nn.modules.utils._ntuple(count)(value.as_python_constant()), + ) + else: + unimplemented( + gb_type="Attempted to use `torch.nn.modules.utils._ntuple` with unsupported argument type", + context=f"value={value}", + explanation="Dynamo does not support this.", + hints=[ + "Change use of _ntuple with argument as constant or tensor.", + ], + ) + + if self.value is torch.nn.modules.utils._ntuple: + return variables.LambdaVariable(handle_ntuple) + else: + return handle_ntuple(args[0]) + + @classmethod + def call_nn_parameter( + cls, + tx: "InstructionTranslator", + data: Any | None = None, + requires_grad: bool = True, + ) -> VariableTracker: + """A call to torch.nn.Parameter() gets lifted to before the graph""" + if tx.export: + unimplemented( + gb_type="Attempted to use `torch.nn.Parameter()` with export", + context="", + explanation="Dynamo does not support this.", + hints=[ + "Do not use `torch.nn.Parameter()` with export.", + *graph_break_hints.SUPPORTABLE, + ], + ) + + if isinstance(requires_grad, variables.VariableTracker): + try: + requires_grad = requires_grad.as_python_constant() + except NotImplementedError: + unimplemented( + gb_type="non-constant `requires_grad` argument to `torch.nn.Parameter`", + context=f"requires_grad={requires_grad}", + explanation="Dynamo does not support this.", + hints=[ + "Change `requires_grad` to be a bool.", + *graph_break_hints.USER_ERROR, + ], + ) + + if data is None or not data.is_tensor(): + unimplemented( + gb_type="`torch.nn.Parameter()` with unsupported data type", + context=f"data={data}", + explanation="Called `torch.nn.Parameter()` with non-Tensor argument.", + hints=[ + "Ensure the argument to `torch.nn.Parameter()` is a `torch.Tensor`.", + *graph_break_hints.USER_ERROR, + ], + ) + + # this results in cleaner graphs, but only works for inputs + if data.source: + return cls._nn_param_via_prefix_insert(tx, data, requires_grad) + + if config.graph_break_on_nn_param_ctor: + # Need user to manually move since we cannot + unimplemented( + gb_type="Attempted to use `torch.nn.Parameter()` constructor with Dynamo", + context="", + explanation="Dynamo does not support this", + hints=[ + "Try to construct `torch.nn.Parameter()` outside the compiled region.", + "If this is not possible, turn `graph_break_on_nn_param_ctor` off", + *graph_break_hints.SUPPORTABLE, + ], + ) + + # TODO[@lucaskabela]: Remove the behavior below since it is deprecated + if isinstance( + data, + TensorWithTFOverrideVariable, + ) or is_traceable_wrapper_subclass_type(data.class_type): + unimplemented( + gb_type="Attempted to use torch.nn.Parameter constructor with tensor subclass", + context=str(data), + explanation="Dynamo does not support this.", + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + ) + + if not can_convert_to_tracable_parameter(): + unimplemented( + gb_type="`torch.nn.Parameter`: cannot convert to traceable tracable", + context="", + explanation="convert_tracable_parameter is set to False.", + hints=[ + "Check usage of context manager: do_not_convert_to_tracable_parameter", + *graph_break_hints.DIFFICULT, + ], + ) + + try: + shape = tuple(data.var_getattr(tx, "shape").as_python_constant()) + dtype = data.var_getattr(tx, "dtype").as_python_constant() + device = data.var_getattr(tx, "device").as_python_constant() + except NotImplementedError as e: + unimplemented( + gb_type="`torch.nn.Parameter` with non-constant Tensor attributes", + context=f"data={data}", + explanation="Dynamo does not support this.", + hints=[ + "Ensure the Tensor argument's shape, dtype, and device are correct.", + *graph_break_hints.USER_ERROR, + ], + from_exc=e, + ) + + placeholder = tx.output.synthetic_graph_input( + new_parameter_placeholder, + (shape, dtype, device, requires_grad), + ) + if data.requires_grad: + data = data.call_method(tx, "detach", [], {}) + + from .builder import wrap_fx_proxy + + result = wrap_fx_proxy( + tx, + tx.output.create_proxy( + "call_function", + tracable_create_parameter, + (data.as_proxy(), placeholder.as_proxy()), + {}, + ), + # In reconstruct() we should use the original parameter. The one + # returned by the graph will be an alias. + source=placeholder.source, + ) + assert result.is_tensor() + result.class_type = torch.nn.Parameter # type: ignore[union-attr] + + # TODO(jansel/bdhirsh) - There is some issue with + # tracable_create_parameter. It does not seem to use the right + # grad_enabled. Since this is parameter, we can just override the + # has_grad_fn field to False to workaround the issue. + result.has_grad_fn = False # type: ignore[union-attr] + + # Register this parameter as a leaf tensor for backward() auto-detection. + # When backward() is called without inputs, we need to find all leaf tensors, + # including those created in-graph like nn.Parameter. + tx.output.leaf_var_creation_order.append(result) + + # TODO(jansel): if the new param falls out of scope, currently it won't get freed until + # the end of the graph. We should fix this. + return result + + @staticmethod + def _nn_param_via_prefix_insert( + tx: "InstructionTranslator", data: Any, requires_grad: bool + ) -> VariableTracker: + # Alternate version if we have a .source + varname = tx.output.new_var() + + # construct the nn.Parameter before the graph save it to varname + assert tx.output.root_tx is not None + cg = PyCodegen(tx.output.root_tx) + cg.add_push_null(lambda: cg.load_import_from("torch.nn", "Parameter")) + cg(data.source) + cg(VariableTracker.build(tx, requires_grad)) + cg.call_function(2, False) + cg.store(varname) + tx.output.pregraph_bytecode.extend(cg.get_instructions()) + + data_node = data.as_proxy().node + if data_node.op not in ("placeholder", "get_attr"): + unimplemented( + gb_type="Unexpected type of data placeholder op for parameter construction", + context=f"data_node.op={data_node.op}", + explanation="Data node op should be placeholder or get_attr.", + hints=[ + *graph_break_hints.DIFFICULT, + ], + ) + + # add the newly constructed nn.Parameter as a graph input + source = SyntheticLocalSource(varname) + example_value = torch.nn.Parameter( + tx.output.example_value_from_input_node(data.as_proxy().node), + requires_grad=requires_grad, + ) + result = VariableTracker.build(tx, example_value, source) + # Realize the VT because we will delete the guards on it in the next line. + result = result.realize() + # No need to guard on this since we already guarded on `data`. + # These guards would fail since varname doesn't exist until after the function starts + TracingContext.get().guards_context.dynamo_guards.remove_guards_with_source( + source + ) + return result + + def call_tensor_method( + self, + tx: "InstructionTranslator", + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + return args[0].call_method(tx, self.get_function().__name__, args[1:], kwargs) + + def is_tensor_method(self) -> bool: + from ..trace_rules import get_tensor_method + + return ( + inspect.ismethoddescriptor(self.get_function()) + and hasattr(self.get_function(), "__objclass__") + # pyrefly: ignore[missing-attribute] + and self.get_function().__objclass__ == torch._C.TensorBase + ) or self.get_function() in get_tensor_method() + + def torch_function_override_enabled( + self, tx: "InstructionTranslator", args: Iterable[Any], kwargs: dict[str, Any] + ) -> bool: + return ( + self.get_function() in get_overridable_functions() + or isinstance( + self.get_function(), + (torch._ops.OpOverload, torch._ops.OpOverloadPacket), + ) + ) and can_dispatch_torch_function(tx, args, kwargs) + + def is_python_hashable(self) -> bool: + return True + + def get_python_hash(self) -> int: + return hash(self.value) + + def is_python_equal(self, other: object) -> bool: + if not isinstance(other, VariableTracker): + return False + return self.as_python_constant() == other.as_python_constant() + + +class DispatchKeySetVariable(BaseTorchVariable): + """represents torch.DispatchKeySet""" + + @staticmethod + def create(value: DispatchKeySet, **kwargs: Any) -> "DispatchKeySetVariable": + return DispatchKeySetVariable(value, **kwargs) + + @classmethod + def create_with_source( + cls, value: DispatchKeySet, source: Source + ) -> "DispatchKeySetVariable": + install_guard(source.make_guard(GuardBuilder.DISPATCH_KEY_SET_MATCH)) + return cls(value, source=source) + + def is_constant_fold_method(self, name: str) -> bool: + return name == "has" + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> "VariableTracker": + if self.is_constant_fold_method(name) and check_unspec_or_constant_args( + args, kwargs + ): + method = getattr(self.value, name) + return VariableTracker.build( + tx, + method( + *[x.as_python_constant() for x in args], + **{k: v.as_python_constant() for k, v in kwargs.items()}, + ), + ) + elif name == "highestPriorityTypeId": + return VariableTracker.build(tx, self.value.highestPriorityTypeId()) + return super().call_method(tx, name, args, kwargs) + + +class FuncTorchInterpreterVariable(BaseTorchVariable): + """represents torch._functorch.pyfunctorch.FuncTorchInterpreter""" + + @classmethod + def create_with_source( + cls, value: Any, source: Source + ) -> "FuncTorchInterpreterVariable": + install_guard(source.make_guard(GuardBuilder.ID_MATCH)) + return cls(value, source=source) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> "VariableTracker": + if name == "key": + return VariableTracker.build(tx, self.value.key()) + elif name == "process": + return tx.inline_user_function_return( + VariableTracker.build(tx, self.value.process.__func__), + [self] + args, + kwargs, + ) + elif name in ["level", "batch_size", "randomness"]: + return VariableTracker.build(tx, getattr(self.value, name)()) + elif name == "lower": + assert not args and not kwargs + return variables.TemporarilyPopInterpreterStackCtxManagerVariable.create( + tx, None + ) + return super().call_method(tx, name, args, kwargs) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/torch_function.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/torch_function.py new file mode 100644 index 0000000000000000000000000000000000000000..c379158a1fa0fc9517a908db32cfa737280b86d0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/torch_function.py @@ -0,0 +1,781 @@ +"""TorchDynamo support for __torch_function__ tensor subclasses. + +This module implements support for tensor subclasses with __torch_function__ overrides. +A tensor subclass instance is represented as a TensorWithTFOverrideVariable, which handles +dispatching __torch_function__ on attribute accesses, method calls, and torch API calls. + +Unsupported features: +- Triggering __torch_function__ on tensor subclass non-tensor custom attributes +- Graph breaking on mutating guardable tensor properties within a __torch_function__ context + (can cause excessive recompiles in certain cases) +- Matching exact eager behavior of ignoring __torch_function__ objects in non-tensor + argument positions of Torch API calls + +Supported features: +- Static method implementations of __torch_function__ on custom objects (triggers on torch + API calls with the object as any argument) +- Triggering __torch_function__ on torch API calls with tensor subclass arguments +- __torch_function__ calls on base tensor attribute access and method calls for tensor + subclass instances +- Matches dispatch ordering behavior of eager __torch_function__ with subclass/object + arguments in any position + +See https://docs.google.com/document/d/1WBxBSvW3NXhRp9ncmtokJloMLCtF4AYNhJaffvHe8Kw/edit#heading=h.vacn73lozd9w +for more information on the design. +""" + +import collections +import contextlib +import functools +import inspect +import operator +from collections.abc import Generator, Iterable, Sequence +from types import TracebackType +from typing import Any, TYPE_CHECKING + +import torch._C +import torch.utils._pytree as pytree +from torch._guards import Source +from torch.overrides import ( + _get_overloaded_args, + get_default_nowrap_functions, + TorchFunctionMode, +) +from torch.utils._device import DeviceContext + +from .. import graph_break_hints +from ..exc import unimplemented +from ..guards import GuardBuilder, install_guard +from ..polyfills import NoEnterTorchFunctionMode +from ..source import AttrSource, GlobalSource, TorchFunctionModeStackSource, TypeSource +from ..utils import ( + class_has_getattribute, + clear_torch_function_mode_stack, + get_safe_global_name, + has_torch_function, + is_tensor_base_attr_getter, + set_torch_function_mode_stack, +) +from .base import VariableTracker +from .constant import ConstantVariable +from .ctx_manager import GenericContextWrappingVariable +from .functions import UserMethodVariable +from .lazy import LazyVariableTracker +from .lists import TupleVariable +from .tensor import TensorSubclassVariable, TensorVariable +from .user_defined import UserDefinedObjectVariable + + +if TYPE_CHECKING: + from torch._dynamo.codegen import PyCodegen + from torch._dynamo.symbolic_convert import InstructionTranslator + + +bin_ops = [ + operator.pow, + operator.mul, + operator.matmul, + operator.floordiv, + operator.truediv, + operator.mod, + operator.add, + operator.lt, + operator.gt, + operator.ge, + operator.le, + operator.ne, + operator.eq, + operator.sub, + operator.ipow, + operator.imul, + operator.imatmul, + operator.ifloordiv, + operator.itruediv, + operator.imod, + operator.iadd, + operator.isub, +] + +bin_int_ops = [ + operator.and_, + operator.or_, + operator.xor, + operator.iand, + operator.ixor, + operator.ior, +] + +un_int_ops = [operator.invert] + +tensor_and_int_ops = [ + operator.lshift, + operator.rshift, + operator.ilshift, + operator.irshift, + operator.getitem, +] + +un_ops = [ + operator.abs, + operator.pos, + operator.neg, + operator.not_, # Note: this has a local scalar dense call + operator.length_hint, +] + + +banned_attrs = [ + fn.__self__.__name__ # type: ignore[attr-defined] + for fn in get_default_nowrap_functions() + if is_tensor_base_attr_getter(fn) +] + + +@functools.cache +def get_prev_stack_var_name() -> str: + from ..bytecode_transformation import unique_id + + return unique_id("___prev_torch_function_mode_stack") + + +class TorchFunctionModeVariable(GenericContextWrappingVariable): + @staticmethod + def is_supported_torch_function_mode(ty: type[TorchFunctionMode]) -> bool: + # Supported in this sense means we can support graph breaks under the + # context. + # We are able to trace custom modes but if there are graph breaks under them + # and they have a custom __enter__/__exit__ we don't handle this for the + # same reason we don't handle generic context managers: there may be side effects + # that are now affected by executing the function across two frames instead of one + # Today we support the enter/exit of the default TorchFunctionMode as well as + # DeviceContext (which is used for set_default_device) + return issubclass(ty, (NoEnterTorchFunctionMode, DeviceContext)) or ( + not class_has_getattribute(ty) + and inspect.getattr_static(ty, "__enter__") is TorchFunctionMode.__enter__ + and inspect.getattr_static(ty, "__exit__") is TorchFunctionMode.__exit__ + ) + + def __init__( + self, + value: TorchFunctionMode | None, + source: Source | None = None, + **kwargs: Any, + ) -> None: + if value is not None: + super().__init__(value, **kwargs) + self.value = value + # needed for BC with calling enter from CM code + self.cm_obj = value # type: ignore[assignment] + self.source = source # type: ignore[assignment] + + def reconstruct(self, codegen: "PyCodegen") -> None: + # This shouldn't be called unless we have a source + assert self.source + self.source.reconstruct(codegen) + + def module_name(self) -> str: + return self.value.__module__ + + def fn_name(self) -> str: + return type(self.value).__name__ + + def python_type(self) -> type: + return type(self.value) + + def call_torch_function( + self, + tx: "InstructionTranslator", + fn: VariableTracker, + types: TupleVariable, + args: Iterable[Any], + kwargs: dict[str, Any], + ) -> VariableTracker: + return call_torch_function( + tx, + get_torch_function_fn(tx, self), # type: ignore[arg-type] + fn, + types, + args, + kwargs, + ) + + def enter(self, tx: "InstructionTranslator") -> VariableTracker: + from .torch import TorchInGraphFunctionVariable + + if isinstance(self.value, NoEnterTorchFunctionMode): + return ConstantVariable.create(None) + + TorchInGraphFunctionVariable( + torch._C._push_on_torch_function_stack + ).call_function(tx, [self], {}) + return ConstantVariable.create(None) + + def exit(self, tx: "InstructionTranslator", *args: Any) -> VariableTracker: + from .torch import TorchInGraphFunctionVariable + + TorchInGraphFunctionVariable(torch._C._pop_torch_function_stack).call_function( + tx, [], {} + ) + return ConstantVariable.create(None) + + def reconstruct_type(self, codegen: "PyCodegen") -> None: + ty = NoEnterTorchFunctionMode + codegen( + AttrSource( + codegen.tx.import_source(ty.__module__), + ty.__name__, + ) + ) + + def supports_graph_breaks(self) -> bool: + return True + + def exit_on_graph_break(self) -> bool: + return False + + +# Used to clear/restore the python torch function mode stack and temporarily restore it as needed +class TorchFunctionModeStackStateManager: + def __init__(self) -> None: + self.stack: list[Any] = [] + + def __enter__(self) -> None: + self.stack = torch.overrides._get_current_function_mode_stack() + clear_torch_function_mode_stack() + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + set_torch_function_mode_stack(self.stack) + self.stack = [] + + @contextlib.contextmanager + def temp_restore_stack(self) -> Generator[None, None, None]: + prev = torch.overrides._get_current_function_mode_stack() + set_torch_function_mode_stack(self.stack) + try: + yield + finally: + set_torch_function_mode_stack(prev) + + +torch_function_mode_stack_state_mgr = TorchFunctionModeStackStateManager() + + +class SymbolicTorchFunctionState: + def __init__(self, py_stack: Iterable[Any]) -> None: + # This is annoyingly complicated because of how the torch function subclass + mode C API was designed + # There are two exposed C knobs here as contexts: torch._C.DisableTorchFunction and torch._C.DisableTorchFunctionSubclass + # These are their definitions: + # 1) torch._C._is_torch_function_enabled indicates that neither of the above knobs have been entered + # (if either are entered, this will be False) + # 2) torch._C._is_torch_function_mode_enabled indicates that either the torch mode stack is empty OR + # torch._C.DisableTorchFunction has been entered + # To disambiguate these and keep myself sane I added a C API to check whether all torch function + # concepts (modes and subclasses) are enabled. + # This only returns true iff we have not entered torch._C.DisableTorchFunction and allows us to separate + # the stack length from the enablement state of torch function modes. + # This is important because now if a mode is pushed while dynamo is tracing, we know whether + # or not torch function modes are enabled and whether we should trace it. + self.torch_function_subclass_enabled = torch._C._is_torch_function_enabled() + + # This differs from the C API of the same name + # this will only be false iff we have entered torch._C.DisableTorchFunction + # and does not take into account the mode stack length, while the C API bundles these + # two concepts + self.torch_function_mode_enabled = ( + not torch._C._is_torch_function_all_disabled() + ) + + self.cur_mode = None + + TorchFunctionModeStackVariable.reset() + + self.mode_stack: collections.deque[TorchFunctionModeVariable] = ( + collections.deque() + ) + + for i, val in enumerate(py_stack): + self.mode_stack.append( + LazyVariableTracker.create(val, source=TorchFunctionModeStackSource(i)) # type: ignore[arg-type] + ) + + def in_torch_function_mode(self) -> bool: + return len(self.mode_stack) > 0 + + def pop_torch_function_mode(self) -> TorchFunctionModeVariable: + return self.mode_stack.pop() + + def push_torch_function_mode(self, mode_var: TorchFunctionModeVariable) -> None: + self.mode_stack.append(mode_var) + + def call_torch_function_mode( + self, + tx: "InstructionTranslator", + fn: VariableTracker, + types: TupleVariable, + args: Iterable[Any], + kwargs: dict[str, Any], + ) -> Any: + with self._pop_mode_for_inlining() as cur_mode: + return cur_mode.call_torch_function(tx, fn, types, args, kwargs) + + @contextlib.contextmanager + def _pop_mode_for_inlining( + self, + ) -> Generator[TorchFunctionModeVariable, None, None]: + old_mode = self.cur_mode + self.cur_mode = self.pop_torch_function_mode() # type: ignore[assignment] + try: + yield self.cur_mode # type: ignore[misc] + finally: + mode = self.cur_mode + self.cur_mode = old_mode + self.push_torch_function_mode(mode) # type: ignore[arg-type] + + +class TorchFunctionModeStackVariable(VariableTracker): + """Fake VT to use as a dummy object, indicating the presence of torch function mode stack mutation""" + + # singleton value representing the global torch function mode stack + # singleton (it exists in C++) + stack_value_singleton = object() + + # offset is used to track if we have inserted/removed a + # device context which is always placed at the bottom of the stack + # if a device context is inserted, the graph will run this mutation + # so when we want to reconstruct any other modes on the stack + # their indices should be shifted right by 1 (+1) + # Conversely, if there was a device context on the stack, and the graph + # mutates the stack to remove that context (set default device to None) + # each of the indices of other modes should be shifted left by 1 (-1) + offset = 0 + + def __init__( + self, + source: Source, + symbolic_stack: collections.deque[TorchFunctionModeVariable], + ) -> None: + self.source = source + self.symbolic_stack = symbolic_stack + + @classmethod + def reset(cls) -> None: + cls.offset = 0 + + @classmethod + def register_mutation(cls, tx: "InstructionTranslator") -> None: + if cls.stack_value_singleton not in tx.output.side_effects: + var = cls( + source=Source(), + symbolic_stack=tx.symbolic_torch_function_state.mode_stack, + ) + tx.output.side_effects.track_mutable(cls.stack_value_singleton, var) + tx.output.side_effects.mutation(var) + + @classmethod + def register_device_context_insertion(cls, tx: "InstructionTranslator") -> None: + stack = tx.symbolic_torch_function_state.mode_stack + if stack and cls.is_device_context(stack[0]): + return + else: + cls.offset += 1 + stack.insert( + 0, + TorchFunctionModeVariable( + None, source=TorchFunctionModeStackSource(-cls.offset) + ), + ) + + @classmethod + def clear_default_device(cls, tx: "InstructionTranslator") -> None: + stack = tx.symbolic_torch_function_state.mode_stack + if stack and cls.is_device_context(stack[0]): + stack.popleft() + cls.offset -= 1 + + @staticmethod + def is_device_context(var: TorchFunctionModeVariable) -> bool: + return isinstance(var.value, DeviceContext) or var.value is None + + @classmethod + def get_mode_index(cls, ind: int) -> int: + return ind + cls.offset + + +def _get_all_args( + args: Iterable[Any], kwargs: dict[str, Any] +) -> Iterable[VariableTracker]: + return _flatten_vts(pytree.arg_tree_leaves(*args, **kwargs)) + + +def _flatten_vts(vts: Iterable[VariableTracker]) -> list[VariableTracker]: + from collections import deque + + from .dicts import ConstDictVariable + from .lists import ListVariable + + vts = deque(vts) + output = [] + + while vts: + vt = vts.popleft() + + if not vt.is_realized() and vt.peek_type() in (dict, list, tuple): # type: ignore[attr-defined] + vt.realize() + + if vt.is_realized(): + if isinstance(vt, ListVariable): + vts.extend(vt.items) + continue + elif isinstance(vt, ConstDictVariable): + vts.extend(vt.items.values()) + continue + + output.append(vt) + + return output + + +def _get_subclass_type(var: VariableTracker) -> type: + assert isinstance(var, (TensorWithTFOverrideVariable, UserDefinedObjectVariable)) + return var.python_type() + + +def _get_subclass_type_var( + tx: "InstructionTranslator", var: VariableTracker +) -> VariableTracker: + if isinstance(var, TensorWithTFOverrideVariable): + return var.class_type_var(tx) + elif isinstance(var, UserDefinedObjectVariable): + source = var.source and TypeSource(var.source) + return VariableTracker.build(tx, var.python_type(), source) + else: + raise AssertionError(f"Unexpected type {type(var)}") + + +def _is_attr_overridden( + tx: "InstructionTranslator", var: VariableTracker, name: str +) -> bool: + if not isinstance(var, (TensorWithTFOverrideVariable, UserDefinedObjectVariable)): + return False + import torch + + overridden = False + try: + attr_val = inspect.getattr_static(var.python_type(), name) + overridden |= attr_val != getattr(torch.Tensor, name) + except AttributeError: + pass + + return overridden + + +def call_torch_function( + tx: "InstructionTranslator", + torch_function_var: VariableTracker, + fn: VariableTracker, + types: TupleVariable, + args: Iterable[Any], + kwargs: dict[str, Any], + *, + is_subclass_dispatch: bool = False, +) -> Any: + # This emulates calling __torch_function__, which has a signature + # def __torch_function__(cls, func, types, args=(), kwargs=None): + # + # Also notice the `cls` is not explicitly passed in the reference + # implementations: + # 1. https://github.com/pytorch/pytorch/blob/8d81806211bc3c0ee6c2ef235017bacf1d775a85/torch/csrc/utils/python_arg_parser.cpp#L368-L374 # noqa: B950 + # 2. https://github.com/pytorch/pytorch/blob/8d81806211bc3c0ee6c2ef235017bacf1d775a85/torch/overrides.py#L1741-L1743 + tf_args = [ + fn, + types, + VariableTracker.build(tx, tuple(args)), + VariableTracker.build(tx, kwargs), + ] + # Mirror the C++ THPModule_disable_torch_function behavior: disable + # __torch_function__ subclass dispatch during the call to prevent + # re-entrant dispatch on operations inside __torch_function__. + # Only do this for subclass dispatch, not mode dispatch. Modes need + # subclass dispatch to remain enabled because the mode's + # __torch_function__ may re-dispatch to the subclass. + tf_state = tx.symbolic_torch_function_state + old_subclass_enabled = tf_state.torch_function_subclass_enabled + if is_subclass_dispatch and old_subclass_enabled: + tf_state.torch_function_subclass_enabled = False + try: + return torch_function_var.call_function(tx, tf_args, {}) + finally: + tf_state.torch_function_subclass_enabled = old_subclass_enabled + + +def get_torch_function_fn( + tx: "InstructionTranslator", vt: VariableTracker +) -> VariableTracker: + # The underlying function could be a classmethod, staticmethod, regular + # function or a function with C-implementation. It doesn't matter as long as + # they satisfy the calling convention in `call_torch_function`. + + args = [vt, VariableTracker.build(tx, "__torch_function__")] + func_vt = VariableTracker.build(tx, getattr).call_function(tx, args, {}) + return func_vt + + +def can_dispatch_torch_function( + tx: "InstructionTranslator", args: Iterable[Any], kwargs: dict[str, Any] +) -> bool: + has_overridden_args = any( + has_torch_function(arg) for arg in _get_all_args(args, kwargs) + ) + tf_state = tx.symbolic_torch_function_state + return (has_overridden_args and tf_state.torch_function_subclass_enabled) or ( + tf_state.torch_function_mode_enabled and tf_state.in_torch_function_mode() + ) + + +def dispatch_torch_function( + tx: "InstructionTranslator", + fn: VariableTracker, + args: Iterable[Any], + kwargs: dict[str, Any], +) -> Any: + """Gathers all args that are TensorWithTFOverrideVariable and dispatches based on the ordering in _get_overloaded_args""" + + all_args = _get_all_args(args, kwargs) + overloaded_args = _get_overloaded_args( + [arg for arg in all_args if has_torch_function(arg)], + _get_subclass_type, + ) + + types = TupleVariable([_get_subclass_type_var(tx, arg) for arg in overloaded_args]) + + if tx.symbolic_torch_function_state.in_torch_function_mode(): + res = tx.symbolic_torch_function_state.call_torch_function_mode( + tx, fn, types, args, kwargs + ) + if not res.is_constant_match(NotImplemented): + return res + + for arg in overloaded_args: + res = arg.call_torch_function( + tx, + fn, + types, + args, + kwargs, + ) + + if not res.is_constant_match(NotImplemented): + tx.output.torch_function_subclass_inlined = True + return res + + unimplemented( + gb_type="All __torch_function__ overrides returned NotImplemented due to TypeError from user code", + context=f"{fn=}, {args=}, {kwargs=}", + explanation=f"All __torch_function__ overrides for for function {fn} returned NotImplemented", + hints=[ + *graph_break_hints.USER_ERROR, + ], + ) + + +class TensorWithTFOverrideVariable(TensorVariable): + """ + Represents a tensor subclass instance with a __torch_function__ override. + """ + + @classmethod + def from_tensor_var( + cls, + tx: "InstructionTranslator", + tensor_var: VariableTracker, + class_type: type, + cls_source: Source | None, + ) -> "TensorWithTFOverrideVariable": + # [Note: __torch_function__] coerce `tensor_var` into a + # TensorWithTFOverrideVariable. In eager, this is just a type change. + import torch + + # This simulates shallow-copying the tensor object. + kwargs = dict(tensor_var.__dict__) + input_tensor_type = kwargs.pop("class_type") + assert input_tensor_type in (torch.Tensor, torch.nn.Parameter) or issubclass( + input_tensor_type, torch.Tensor + ), ( + f"invalid class type {input_tensor_type} in TensorWithTFOverrideVariable.from_tensor_var" + ) + var = cls(class_type=class_type, **kwargs) + var.install_global(tx) + return var + + def install_global(self, tx: "InstructionTranslator") -> None: + # stash the subclass type to rewrap an output tensor if needed + # this is needed because the actual type needs to be available + # each time the compiled artifact is run and outputs a wrapped tensor. + if self.global_mangled_class_name(tx) not in tx.output.global_scope: + # Safe because global_mangled_class_name figures it out + tx.output.install_global_unsafe( + self.global_mangled_class_name(tx), self.class_type + ) + + def python_type(self) -> type: + return self.class_type + + def class_type_var(self, tx: "InstructionTranslator") -> VariableTracker: + return TensorSubclassVariable( + self.class_type, source=GlobalSource(self.global_mangled_class_name(tx)) + ) + + def global_mangled_class_name(self, tx: "InstructionTranslator") -> str: + return get_safe_global_name( + tx, f"__subclass_{self.class_type.__name__}", self.class_type + ) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + # [Note: __torch_function__] We currently only support attributes that are defined on + # base tensors, custom attribute accesses will graph break. + import torch + + # I think only `_base` is breaking because we aren't modelling view + # relationship perfectly in some scenarios. + if name in banned_attrs: + unimplemented( + gb_type="Unsupported tensor subclass attribute access", + context=f"{name}", + explanation="`torch.compile` currently can't trace this", + hints=[ + f"Avoid accessing {name} of tensor subclass in torch.compile region", + *graph_break_hints.SUPPORTABLE, + ], + ) + + # Handle non-overridden attributes inherited from `torch.Tensor`. + attr_is_overridden = _is_attr_overridden(tx, self, name) + if ( + hasattr(torch.Tensor, name) + and not attr_is_overridden + and not inspect.ismethoddescriptor(getattr(torch.Tensor, name)) + ): + args = [self] + kwargs: dict[Any, Any] = {} + if can_dispatch_torch_function(tx, args, kwargs): + get_fn = VariableTracker.build(tx, getattr(torch.Tensor, name).__get__) + + return self.call_torch_function( + tx, + get_fn, + TupleVariable([self.class_type_var(tx)]), + args, + kwargs, + ) + else: + # `TensorVariable.var_getattr` doesn't handle user-defined + # function/attribute well, so we explicitly handle them here. + # + # TODO move this logic into `TensorVariable`, or try to merge it + # with similar logic in `UserDefinedObjectVariable`. + try: + attr = inspect.getattr_static(self.class_type, name) + except AttributeError: + pass + else: + import types + + cls_source = GlobalSource(self.global_mangled_class_name(tx)) + attr_source = AttrSource(cls_source, name) + if isinstance(attr, types.FunctionType): + install_guard(attr_source.make_guard(GuardBuilder.CLOSURE_MATCH)) + return UserMethodVariable(attr, self) + + elif isinstance(attr, property): + getter_source = AttrSource(attr_source, "fget") + getter = attr.fget + getter_var = VariableTracker.build( + tx, getter, source=getter_source, realize=True + ) + return getter_var.call_function(tx, [self], {}) + + elif isinstance(attr, classmethod): + return UserMethodVariable( + attr.__func__, self.class_type_var(tx), source=attr_source + ) + + elif attr_is_overridden: + unimplemented( + gb_type="Unsupported tensor subclass overridden attribute access", + context=f"{name}", + explanation="`torch.compile` only support tracing certain types of overridden tensor subclass attributes", + hints=[ + f"Avoid accessing {name} of tensor subclass in torch.compile region", + f"Renaming attribute `{name}` of type {self.class_type}", + *graph_break_hints.SUPPORTABLE, + ], + ) + + return super().var_getattr(tx, name) + + def call_torch_function( + self, + tx: "InstructionTranslator", + fn: VariableTracker, + types: TupleVariable, + args: Iterable[Any], + kwargs: dict[str, Any], + ) -> Any: + # NOTE this assumes `__torch_function__` isn't modified during tracing. + if not hasattr(self, "torch_function_fn"): + self.torch_function_fn = get_torch_function_fn(tx, self) + + return call_torch_function( + tx, + self.torch_function_fn, + fn, + types, + args, + kwargs, + is_subclass_dispatch=True, + ) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: Sequence[VariableTracker], + kwargs: "dict[str, VariableTracker]", + ) -> "VariableTracker": + # This code block implements inlining the __torch_function__ override + # of `call_method`. + tf_args = [self] + list(args) + if can_dispatch_torch_function(tx, tf_args, kwargs): + import torch + + if _is_attr_overridden(tx, self, name): + unimplemented( + gb_type="Tensor subclass overridden method call", + context=f"{name}", + explanation="`torch.compile` currently can't trace this", + hints=[ + f"Avoid calling {name} of tensor subclass in torch.compile region", + f"Renaming method `{name}` of type {self.class_type}", + *graph_break_hints.SUPPORTABLE, + ], + ) + + # [Note: __torch_function__] Currently we only support methods that are defined on tensor + # we will graph break in other cases this will need a bigger overhaul of extracting methods/comparing them for equality + # We've established with the above check that the method is not overridden, so we guard that the method is the same + # as the impl defined on tensor and retrieve it + if self.source: + source = AttrSource(AttrSource(self.source, "__class__"), name) + value = inspect.getattr_static(self.python_type(), name) + else: + source = None + value = getattr(torch.Tensor, name) + func_var = VariableTracker.build(tx, value, source) + return dispatch_torch_function(tx, func_var, tf_args, kwargs) + else: + return super().call_method(tx, name, args, kwargs) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/user_defined.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/user_defined.py new file mode 100644 index 0000000000000000000000000000000000000000..c8b8a3b1023e73ded4cabab90356f6a168e170f2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_dynamo/variables/user_defined.py @@ -0,0 +1,3517 @@ +""" +This module contains variable classes for handling user-defined objects in Dynamo's tracing system. + +The key classes are: +- UserDefinedVariable: Base class for representing custom Python objects +- UserDefinedClassVariable: Handles Python class objects/types +- UserDefinedObjectVariable: Fallback class for instance objects, with support for method calls, + attribute access, and other Python object behaviors. +- Specialized subclasses for common patterns: + - UserDefinedDictVariable: For dict subclasses + - UserDefinedSetVariable: For set subclasses + - UserDefinedTupleVariable: For tuple subclasses + - UserDefinedExceptionObjectVariable: For exception subclasses + - FrozenDataClassVariable: Special handling of frozen dataclasses + - MutableMappingVariable: For collections.abc.MutableMapping subclasses + +Dynamo specializes to VariableTracker subclasses like FrozenDataClassVariable if available; if no +subclass qualifies, it falls back to UserDefinedObjectVariable. + +These classes help Dynamo track and handle arbitrary Python objects during tracing, +maintaining proper semantics while enabling optimizations where possible. +""" + +import _collections # type: ignore[import-not-found] +import builtins +import collections +import contextlib +import dataclasses +import enum +import functools +import inspect +import random +import sys +import threading +import traceback +import types +import warnings +import weakref +from collections.abc import Callable, Iterable, Sequence +from typing import Any, Literal, TYPE_CHECKING, Union +from typing_extensions import is_typeddict + +import torch._dynamo.config +import torch.nn +from torch._guards import Source, TracingContext +from torch.utils._python_dispatch import is_traceable_wrapper_subclass_type +from torch.utils._pytree import GetAttrKey, is_structseq_class + +from .. import config, graph_break_hints, polyfills, variables +from ..bytecode_transformation import create_call_function +from ..create_parameter_op import do_not_convert_to_tracable_parameter +from ..exc import ( + handle_observed_exception, + ObservedAttributeError, + ObservedKeyError, + ObservedTypeError, + ObservedUserStopIteration, + raise_observed_exception, + raise_type_error, + unimplemented, +) +from ..graph_bytecode_inputs import get_external_object_by_index +from ..guards import GuardBuilder, install_guard +from ..source import ( + AttrSource, + CallFunctionNoArgsSource, + DictGetItemSource, + GetItemSource, + RandomValueSource, + TypeDictSource, + TypeMROSource, + TypeSource, + UnspecializedParamBufferSource, +) +from ..utils import ( + check_constant_args, + cmp_name_to_op_mapping, + dict_methods, + frozenset_methods, + get_custom_getattr, + has_torch_function, + is_lru_cache_wrapped_function, + is_namedtuple_cls, + is_wrapper_or_member_descriptor, + istype, + list_methods, + namedtuple_fields, + object_has_getattribute, + proxy_args_kwargs, + raise_args_mismatch, + raise_on_overridden_hash, + set_methods, + tensortype_to_dtype, + tuple_methods, + unpatched_nn_module_getattr, +) +from .base import MutationType, NO_SUCH_SUBOBJ, ValueMutationNew, VariableTracker +from .dicts import ConstDictVariable, DefaultDictVariable +from .hashable import HashableTracker +from .sets import SetVariable + + +try: + import numpy as np +except ModuleNotFoundError: + np = None # type: ignore[assignment] + +try: + from torch.utils._cxx_pytree import PyTreeSpec +except ImportError: + PyTreeSpec = type(None) # type: ignore[misc, assignment] + + +if TYPE_CHECKING: + from torch._dynamo.codegen import PyCodegen + from torch._dynamo.side_effects import SideEffects + from torch._dynamo.symbolic_convert import InstructionTranslator + from torch._dynamo.variables.constant import ConstantVariable + + from .dicts import DunderDictVariable + from .lists import ListVariable, TupleVariable + + +def is_standard_setattr(val: object) -> bool: + return val in (object.__setattr__, BaseException.__setattr__) + + +def is_standard_delattr(val: object) -> bool: + return val in (object.__delattr__, BaseException.__delattr__) + + +def is_forbidden_context_manager(ctx: object) -> bool: + f_ctxs: list[Any] = [] + + try: + from _pytest.python_api import RaisesContext # type: ignore[attr-defined] + from _pytest.recwarn import WarningsChecker # type: ignore[attr-defined] + + f_ctxs.append(RaisesContext) + f_ctxs.append(WarningsChecker) + except ImportError: + pass + + if m := sys.modules.get("torch.testing._internal.jit_utils"): + f_ctxs.append(m._AssertRaisesRegexWithHighlightContext) + + return ctx in f_ctxs + + +def is_cython_function(obj: object) -> bool: + return ( + callable(obj) + and hasattr(type(obj), "__name__") + and type(obj).__name__ == "cython_function_or_method" + ) + + +def is_pydantic_dataclass_cls(value: object) -> bool: + return ( + inspect.isclass(value) + and dataclasses.is_dataclass(value) + and "__is_pydantic_dataclass__" in getattr(value, "__dict__", {}) + ) + + +# Types whose instances are data descriptors (have __get__ + (__set__ or __delete__)). +# CPython invokes data descriptors found on the type MRO *before* checking +# the instance __dict__. This set is used by is_data_descriptor for a fast +# O(1) check before falling back to the generic hasattr probe. +KNOWN_DATA_DESCRIPTOR_TYPES: frozenset[type] = frozenset( + { + property, + _collections._tuplegetter, + types.MemberDescriptorType, + types.GetSetDescriptorType, + } +) + + +def is_data_descriptor(obj: object) -> bool: + """Return True if *obj* is a data descriptor (has __get__ and (__set__ or __delete__)).""" + tp = type(obj) + if tp in KNOWN_DATA_DESCRIPTOR_TYPES: + return True + return hasattr(tp, "__get__") and ( + hasattr(tp, "__set__") or hasattr(tp, "__delete__") + ) + + +class UserDefinedVariable(VariableTracker): + value: object + + def _maybe_get_baseclass_method(self, name: str) -> Any: + """Get method from the base class if not overridden in value's __dict__.""" + if name not in getattr(self.value, "__dict__", {}): + try: + return inspect.getattr_static(type(self.value), name) + except AttributeError: + pass + return None + + +class UserDefinedClassVariable(UserDefinedVariable): + # pyrefly: ignore[bad-override] + value: type[object] + + def __init__(self, value: type[object], **kwargs: Any) -> None: + super().__init__(**kwargs) + self.value = value + # Used when we materialize class.__dict__ to a MappingProxyObject. In + # this case, we don't want to allow mutation in the class because there + # is no way to reflect it in the created MappingProxyVariable. + self.ban_mutation = False + + def as_python_constant(self) -> type[object]: + return self.value + + def as_proxy(self) -> object: + return self.value + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.value})" + + @staticmethod + @functools.cache + def _constant_fold_classes() -> set[type[object]]: + return { + torch.device, + torch.finfo, + torch.iinfo, + torch.Size, + } + + @staticmethod + @functools.cache + def _in_graph_classes() -> set[type[object]]: + _in_graph_class_list = { + torch.Tensor, + torch.cuda.FloatTensor, # type: ignore[attr-defined] + torch.cuda.DoubleTensor, # type: ignore[attr-defined] + torch.cuda.HalfTensor, # type: ignore[attr-defined] + torch.cuda.BFloat16Tensor, # type: ignore[attr-defined] + torch.cuda.ByteTensor, # type: ignore[attr-defined] + torch.cuda.CharTensor, # type: ignore[attr-defined] + torch.cuda.IntTensor, # type: ignore[attr-defined] + torch.cuda.ShortTensor, # type: ignore[attr-defined] + torch.cuda.LongTensor, # type: ignore[attr-defined] + torch.Stream, + torch.Event, + torch.cuda.Stream, + torch.cuda.Event, + torch.xpu.Stream, + torch.xpu.Event, + } + if hasattr(torch, "hpu"): + _in_graph_class_list.update( + { + torch.hpu.Stream, + torch.hpu.Event, + } + ) + + return set(tensortype_to_dtype.keys()) | _in_graph_class_list + + @staticmethod + @functools.cache + def supported_c_new_functions() -> set[Any]: + exceptions: set[Any] = { + getattr(builtins, name).__new__ + for name in dir(builtins) + if isinstance(getattr(builtins, name), type) + and issubclass(getattr(builtins, name), BaseException) + } + c_new_fns: set[Any] = { + object.__new__, + dict.__new__, + set.__new__, + frozenset.__new__, + tuple.__new__, + list.__new__, + int.__new__, + float.__new__, + str.__new__, + } + return c_new_fns.union(exceptions) + + @staticmethod + def is_supported_new_method(value: object) -> bool: + if value in UserDefinedClassVariable.supported_c_new_functions(): + return True + # Structseq types each define their own C tp_new. + owner = getattr(value, "__self__", None) + return isinstance(owner, type) and is_structseq_class(owner) + + def can_constant_fold_through(self) -> bool: + if self.value in self._constant_fold_classes(): + return True + # Enum class calls (e.g., Color(1)) are value lookups that return + # existing singleton members, so they can always be constant-folded. + return isinstance(self.value, type) and issubclass(self.value, enum.Enum) + + def lookup_cls_mro_attr(self, name: str) -> object: + """Walk cls.__mro__ only (not the metaclass chain) to find *name*.""" + for base in self.value.__mro__: + if name in base.__dict__: + return base.__dict__[name] + return NO_SUCH_SUBOBJ + + def lookup_metaclass_attr(self, name: str) -> object: + """Walk type(cls).__mro__ (the metaclass chain) to find *name*.""" + for base in type(self.value).__mro__: + if name in base.__dict__: + return base.__dict__[name] + return NO_SUCH_SUBOBJ + + def bool_impl( + self, + tx: "InstructionTranslator", + ) -> "VariableTracker": + from .constant import ConstantVariable + + # bool() on a class consults the metaclass __bool__. + # If the metaclass is the default `type`, all classes are truthy. + metaclass = type(self.value) + if hasattr(metaclass, "__bool__") and metaclass is not type: + return self.call_method(tx, "__bool__", [], {}) + return ConstantVariable.create(True) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + source = AttrSource(self.source, name) if self.source is not None else None + + # --- Dynamo-specific pre-checks --- + + # Wrap OrderedDict/defaultdict.fromkeys as GetAttrVariable so it's + # handled uniformly in call_method(). + if ( + self.value in {collections.OrderedDict, collections.defaultdict} + and name == "fromkeys" + ): + return super().var_getattr(tx, name) + + # Custom metaclasses that override __getattribute__ replace the entire + # lookup algorithm; bail out for those. Standard metaclasses (ABCMeta, + # EnumType, etc.) that don't override __getattribute__ use + # type.__getattribute__ which is the algorithm we implement below. + metacls = type(self.value) + if metacls is not type and "__getattribute__" in metacls.__dict__: + unimplemented( + gb_type="Custom metaclass with __getattribute__", + context=f"type({self.value}) = {metacls}", + explanation="Dynamo does not trace attribute access on classes whose " + "metaclass overrides __getattribute__", + hints=graph_break_hints.SUPPORTABLE, + ) + + # ---- CPython type_getattro algorithm ---- + # https://github.com/python/cpython/blob/3.13/Objects/typeobject.c#L5417-L5505 + # 1. meta_attr = lookup name in type(cls).__mro__ (metaclass chain) + # 2. if meta_attr is a DATA descriptor → invoke + # 3. cls_attr = lookup name in cls.__mro__ (class chain) + # 4. if cls_attr has __get__ → invoke cls_attr.__get__(None, cls) + # 5. if cls_attr exists (plain) → return as-is + # 6. if meta_attr is a non-data descriptor or plain → return + # 7. raise AttributeError + + # Step 1-2: Metaclass data descriptors. + # For type(cls) is type, these are C-level getset/member descriptors + # for __dict__, __mro__, __name__, __qualname__, __doc__, etc. + meta_attr = self.lookup_metaclass_attr(name) + if meta_attr is not NO_SUCH_SUBOBJ and is_data_descriptor(meta_attr): + return self.resolve_meta_data_descriptor(tx, name, meta_attr, source) + + # Step 3-5: Class MRO lookup. + cls_attr = self.lookup_cls_mro_attr(name) + if cls_attr is not NO_SUCH_SUBOBJ: + if hasattr(type(cls_attr), "__get__"): + # Step 4: Descriptor — invoke __get__(None, cls). + return self.resolve_cls_descriptor(tx, name, cls_attr, source) + # Step 5: Plain attribute. + return self.resolve_cls_plain_attr(tx, name, cls_attr, source) + + # Step 6: Metaclass non-data descriptor or plain attr. + # These are non-data descriptors on the metaclass (e.g. type.__call__, + # type.__subclasses__, type.mro). We use GetAttrVariable to defer to + # runtime rather than VariableTracker.build, because build would create + # a variable for the raw C-level descriptor which then fails when + # called (e.g. type.__subclasses__ is a method_descriptor that dynamo + # can't trace). GetAttrVariable defers the access and lets + # call_method handle it. + if meta_attr is not NO_SUCH_SUBOBJ: + return variables.GetAttrVariable(self, name, type(meta_attr), source=source) + + # __getattr__ on metaclass (not part of type_getattro proper — + # CPython handles this via slot_tp_getattr_hook). + metacls = type(self.value) + if metacls is not type: + meta_getattr = self.lookup_metaclass_attr("__getattr__") + if meta_getattr is not NO_SUCH_SUBOBJ and isinstance( + meta_getattr, types.FunctionType + ): + return variables.UserMethodVariable(meta_getattr, self).call_function( + tx, [variables.ConstantVariable.create(name)], {} + ) + + # Step 7: AttributeError. + raise_observed_exception( + AttributeError, + tx, + args=[f"type object '{self.value.__name__}' has no attribute '{name}'"], + ) + + def resolve_meta_data_descriptor( + self, + tx: "InstructionTranslator", + name: str, + meta_attr: object, + source: Source | None, + ) -> VariableTracker: + """Handle data descriptors from the metaclass MRO (type.__dict__ slots).""" + if name == "__dict__": + return VariableTracker.build( + tx, + self.value.__dict__, + source=self.source and AttrSource(self.source, "__dict__"), + ) + if name == "__mro__": + attr_source = self.source and TypeMROSource(self.source) + return VariableTracker.build(tx, self.value.__mro__, attr_source) + # __name__, __qualname__, __doc__, __module__, __bases__, + # __abstractmethods__, etc. — all C-level getset descriptors on type. + resolved = type.__getattribute__(self.value, name) + if source: + return VariableTracker.build(tx, resolved, source) + from . import ConstantVariable + + if ConstantVariable.is_literal(resolved): + return VariableTracker.build(tx, resolved) + return variables.GetAttrVariable(self, name, type(resolved), source=source) + + def resolve_cls_descriptor( + self, + tx: "InstructionTranslator", + name: str, + cls_attr: object, + source: Source | None, + ) -> VariableTracker: + """Handle descriptors found in cls.__mro__.""" + if isinstance(cls_attr, staticmethod): + return VariableTracker.build(tx, cls_attr.__get__(self.value), source) + + if isinstance(cls_attr, classmethod): + if isinstance(cls_attr.__func__, property): + fget_vt = VariableTracker.build(tx, cls_attr.__func__.fget) + return fget_vt.call_function(tx, [self], {}) + return variables.UserMethodVariable(cls_attr.__func__, self, source=source) + + if isinstance(cls_attr, types.ClassMethodDescriptorType): + func = cls_attr.__get__(None, self.value) + return VariableTracker.build(tx, func, source) + + # property and _tuplegetter accessed on the class return the + # descriptor itself (descriptor.__get__(None, cls) is descriptor). + # Build directly — no need to invoke __get__. + if isinstance(cls_attr, (property, _collections._tuplegetter)): + if source: + return VariableTracker.build(tx, cls_attr, source) + return UserDefinedObjectVariable(cls_attr) + + # Comparison dunders inherited from object — defer to runtime. + if name in cmp_name_to_op_mapping and not isinstance( + cls_attr, types.FunctionType + ): + return variables.GetAttrVariable( + self, name, py_type=type(cls_attr), source=source + ) + + # User-defined descriptor with Python __get__. + # For torch-internal classes or attributes in the class's own __dict__, + # defer descriptor invocation to runtime via VariableTracker.build to + # avoid compile-time side effects (e.g. deprecation warnings from + # _ClassPropertyDescriptor on torch.FloatStorage.dtype). + get_fn = inspect.getattr_static(type(cls_attr), "__get__", None) + if isinstance(get_fn, types.FunctionType): + if source and ( + name in getattr(self.value, "__dict__", {}) + or self.value.__module__.startswith("torch.") + or self.value.__module__ == "torch" + ): + return VariableTracker.build(tx, cls_attr, source) + return self.invoke_cls_descriptor_get(tx, name, cls_attr, source) + + # C-level descriptors (WrapperDescriptor, MethodDescriptor, etc.) + # Build directly when the attribute lives in the class's own __dict__ + # or the class belongs to torch (needed for e.g. torch.Tensor.dim). + # OrderedDict's C-level methods are handled at runtime. + if inspect.ismethoddescriptor(cls_attr) or is_wrapper_or_member_descriptor( + cls_attr + ): + if ( + source + and self.value is not collections.OrderedDict + and ( + name in getattr(self.value, "__dict__", {}) + or self.value.__module__.startswith("torch.") + or self.value.__module__ == "torch" + ) + ): + return VariableTracker.build(tx, cls_attr, source) + return variables.GetAttrVariable(self, name, type(cls_attr), source=source) + + # Everything else: FunctionType, etc. + return VariableTracker.build(tx, cls_attr, source) + + def resolve_cls_plain_attr( + self, + tx: "InstructionTranslator", + name: str, + cls_attr: object, + source: Source | None, + ) -> VariableTracker: + """Handle non-descriptor attributes from cls.__mro__.""" + if name == "__new__" and UserDefinedClassVariable.is_supported_new_method( + cls_attr + ): + return super().var_getattr(tx, name) + if self.value is collections.OrderedDict: + return variables.GetAttrVariable(self, name, py_type=type(cls_attr)) + return VariableTracker.build(tx, cls_attr, source) + + def invoke_cls_descriptor_get( + self, + tx: "InstructionTranslator", + name: str, + descriptor: object, + source: Source | None, + ) -> VariableTracker: + """Trace a class-MRO descriptor's __get__(None, cls) call.""" + from .constant import ConstantVariable + + descriptor_source = None + descriptor_get_source = None + if self.source: + descriptor_source = AttrSource(self.source, name) + descriptor_get_source = AttrSource(TypeSource(descriptor_source), "__get__") + descriptor_var = VariableTracker.build(tx, descriptor, descriptor_source) + else: + descriptor_var = UserDefinedObjectVariable(descriptor) + + none_var = ConstantVariable.create(None) + return variables.UserMethodVariable( + descriptor.__get__.__func__, # type: ignore[union-attr] + descriptor_var, + source=descriptor_get_source, + ).call_function(tx, [none_var, self], {}) + + def len_impl(self, tx: "InstructionTranslator") -> VariableTracker: + m = self._maybe_get_baseclass_method("__len__") + if m: + source = self.source and AttrSource(self.source, "__len__") + return variables.UserMethodVariable( + m, self, source_fn=source + ).call_function(tx, [], {}) + raise_type_error(tx, f"object of type {self.python_type_name()} has no length") + + def sq_length(self, tx: "InstructionTranslator") -> VariableTracker: + return self.len_impl(tx) + + def mp_length(self, tx: "InstructionTranslator") -> VariableTracker: + return self.len_impl(tx) + + def _call_cross_entropy_loss( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + """ + functional: input, target, weight=None, size_average=None, ignore_index=- 100, reduce=None, reduction='mean', + label_smoothing=0.0 + + non functional ctor: weight=None, size_average=None, ignore_index=- 100, reduce=None, reduction='mean', + label_smoothing=0.0 + + non functional loss call: input, target, optional_output + """ + from . import ConstantVariable + + def normalize_args( + weight: VariableTracker = ConstantVariable.create(None), + size_average: VariableTracker = ConstantVariable.create(None), + ignore_index: VariableTracker = ConstantVariable.create(-100), + reduce: VariableTracker = ConstantVariable.create(None), + reduction: VariableTracker = ConstantVariable.create("mean"), + label_smoothing: VariableTracker = ConstantVariable.create(0.0), + ) -> tuple[VariableTracker, ...]: + return ( + weight, + size_average, + ignore_index, + reduce, + reduction, + label_smoothing, + ) + + ( + weight, + size_average, + ignore_index, + reduce_arg, + reduction, + label_smoothing, + ) = normalize_args(*args, **kwargs) + + def fake_cross_entropy_loss( + input: VariableTracker, target: VariableTracker + ) -> VariableTracker: + from .builder import wrap_fx_proxy + + return wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + torch.nn.functional.cross_entropy, + *proxy_args_kwargs( + [ + input, + target, + weight, + size_average, + ignore_index, + reduce_arg, + reduction, + label_smoothing, + ], + {}, + ), + ), + ) + + return variables.LambdaVariable(fake_cross_entropy_loss) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .builder import SourcelessBuilder + + if ( + name == "__subclasses__" + and len(args) == 0 + and not kwargs + and "__subclasses__" not in self.value.__dict__ + ): + source = self.source + if self.source: + source = AttrSource(self.source, "__subclasses__") + source = CallFunctionNoArgsSource(source) + return VariableTracker.build(tx, self.value.__subclasses__(), source) + elif ( + self.value in {collections.OrderedDict, collections.defaultdict} + and name == "fromkeys" + ): + return variables.DictBuiltinVariable.call_custom_dict_fromkeys( + tx, self.value, *args, **kwargs + ) + elif self.value is collections.OrderedDict and name == "move_to_end": + return args[0].call_method(tx, name, [*args[1:]], kwargs) + elif name == "__len__" and len(args) == 1 and not kwargs: + from .object_protocol import generic_len + + return generic_len(tx, args[0]) + elif name == "__eq__" and len(args) == 1 and hasattr(args[0], "value"): + return VariableTracker.build(tx, self.value == args[0].value) + elif name == "__ne__" and len(args) == 1 and hasattr(args[0], "value"): + return VariableTracker.build(tx, self.value != args[0].value) + elif issubclass(self.value, dict) and name != "__new__": + # __new__ is handled below + return SourcelessBuilder.create(tx, dict).call_method( + tx, name, args, kwargs + ) + elif issubclass(self.value, (set, frozenset)) and name != "__new__": + # __new__ is handled below + return SourcelessBuilder.create(tx, set).call_method(tx, name, args, kwargs) + elif ( + name == "__new__" + and self.value is collections.OrderedDict + and isinstance(args[0], UserDefinedClassVariable) + and args[0].value is collections.OrderedDict + ): + if kwargs and len(args) != 1: + raise_args_mismatch( + tx, + name, + "1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + return ConstDictVariable( + {}, collections.OrderedDict, mutation_type=ValueMutationNew() + ) + elif ( + len(args) == 1 + and isinstance(args[0], variables.GenericContextWrappingVariable) + and name == "__enter__" + ): + return args[0].enter(tx) + elif name == "__new__" and UserDefinedClassVariable.is_supported_new_method( + self.value.__new__ + ): + return tx.output.side_effects.track_new_user_defined_object( + self, + args[0], + args[1:], + ) + elif name == "__setattr__" and self.ban_mutation: + unimplemented( + gb_type="Class attribute mutation when the __dict__ was already materialized", + context=str(self.value), + explanation="Dyanmo does not support tracing mutations on a class when its __dict__ is materialized", + hints=graph_break_hints.SUPPORTABLE, + ) + + # Dispatch dunder methods defined on the metaclass (e.g., EnumType.__contains__). + # In Python, `x in Color` calls `type(Color).__contains__(Color, x)`. + metaclass = type(self.value) + if metaclass is not type: + # Look up the method on the metaclass MRO, not the class MRO + for klass in metaclass.__mro__: + if name in klass.__dict__: + method = klass.__dict__[name] + if isinstance(method, types.FunctionType): + source = self.source and AttrSource(self.source, name) + return variables.UserMethodVariable( + method, self, source=source + ).call_function(tx, args, kwargs) + break + + return super().call_method(tx, name, args, kwargs) + + def unpack_var_sequence( + self, tx: "InstructionTranslator" + ) -> list["VariableTracker"]: + if isinstance(self.value, type) and issubclass(self.value, enum.Enum): + return [VariableTracker.build(tx, item) for item in self.value] + raise NotImplementedError + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from ..side_effects import SideEffects + from .builder import SourcelessBuilder, wrap_fx_proxy + from .ctx_manager import GenericContextWrappingVariable + + constant_args = check_constant_args(args, kwargs) + + if torch.distributed.is_available() and self.value is torch.distributed.P2POp: + if not config.enable_p2p_compilation: + unimplemented( + gb_type="P2P compilation disabled for P2POp construction", + context="torch.distributed.P2POp", + explanation="P2P compilation is disabled.", + hints=[ + "Set TORCHDYNAMO_ENABLE_P2P_COMPILATION=1 to enable.", + ], + ) + var = tx.output.side_effects.track_new_user_defined_object( + SourcelessBuilder.create(tx, object), + self, + [], + ) + var.call_method(tx, "__init__", list(args), kwargs) # type: ignore[arg-type] + return var + + if self.can_constant_fold_through() and constant_args: + # constant fold + return VariableTracker.build( + tx, + self.as_python_constant()( # type: ignore[operator] + *[x.as_python_constant() for x in args], + **{k: v.as_python_constant() for k, v in kwargs.items()}, + ), + ) + elif self.value is torch.nn.CrossEntropyLoss: + return self._call_cross_entropy_loss(tx, args, kwargs) + elif self.value is contextlib.nullcontext: + # import here to avoid circular dependency + from .ctx_manager import NullContextVariable + + return NullContextVariable(*args, **kwargs) + elif self.value is collections.OrderedDict: + return tx.inline_user_function_return( + VariableTracker.build(tx, polyfills.construct_dict), + [self, *args], + kwargs, + ) + elif self.value is collections.defaultdict: + if len(args) == 0: + default_factory = variables.ConstantVariable.create(None) + elif len(args) == 1: + # In the case the argument is a builtin, then we can take the callable as the factory method. + # Otherwise, it must be a ConstantVariable holding None. + if not DefaultDictVariable.is_supported_arg(args[0]): + raise_observed_exception(TypeError, tx, args=[args[0]]) + default_factory = args[0] + args = [] + else: + default_factory, *args = args + dict_vt = variables.DictBuiltinVariable.call_custom_dict( + tx, dict, *args, **kwargs + ) + return DefaultDictVariable( + dict_vt.items, # type: ignore[attr-defined] + collections.defaultdict, + default_factory, + mutation_type=ValueMutationNew(), + ) + elif is_typeddict(self.value): + if self.value.__optional_keys__: # type: ignore[attr-defined] + unimplemented( + gb_type="TypedDict with optional keys", + context=str(self.value), + explanation="Dyanmo does not support tracing TypedDict with optional keys", + hints=[ + "Avoid using TypedDict with optional keys", + *graph_break_hints.SUPPORTABLE, + ], + ) + return variables.DictBuiltinVariable.call_custom_dict( + tx, dict, *args, **kwargs + ) + elif self.value is collections.deque: + maxlen = variables.ConstantVariable.create(None) + + def deque_signature( + iterable: Iterable[Any] | None = None, maxlen: int | None = None + ) -> Any: + pass + + bound_args = None + try: + bound_args = inspect.signature(deque_signature).bind(*args, **kwargs) + except TypeError as e: + unimplemented( + gb_type="collections.deque() with bad arguments", + context=f"args={args}, kwargs={kwargs}", + explanation="Detected call to collections.deque() with bad arguments.", + hints=[ + "Fix the call to collections.deque().", + *graph_break_hints.USER_ERROR, + ], + from_exc=e, + ) + assert bound_args is not None + if "iterable" in bound_args.arguments: + if not bound_args.arguments["iterable"].has_force_unpack_var_sequence( + tx + ): + unimplemented( + gb_type="collections.deque() with bad iterable argument", + context=f"args={args}, kwargs={kwargs}", + explanation="Call to collections.deque() has an iterable argument that Dynamo cannot " + "convert to a list.", + hints=[ + "Use a simpler sequence type that Dynamo can convert to a list " + "(e.g. list, tuple, list iterator, etc.)", + *graph_break_hints.USER_ERROR, + ], + ) + items = bound_args.arguments["iterable"].force_unpack_var_sequence(tx) + else: + # pyrefly: ignore [implicit-any] + items = [] + + if "maxlen" in bound_args.arguments: + maxlen = bound_args.arguments["maxlen"] + + return variables.lists.DequeVariable( + items, maxlen=maxlen, mutation_type=ValueMutationNew() + ) + elif ( + # https://github.com/python/cpython/blob/33efd7178e269cbd04233856261fd0aabbf35447/Lib/contextlib.py#L475-L477 + self.value is types.MethodType + and len(args) == 2 + and isinstance(args[0], variables.UserFunctionVariable) + and isinstance(args[1], GenericContextWrappingVariable) + and args[0].get_name() in ("__enter__", "__exit__") + ): + cm_obj = args[1].cm_obj + fn = getattr(cm_obj, args[0].get_name()).__func__ + return variables.UserMethodVariable(fn, args[1], source=self.source) + elif self.value is weakref.ref: + if len(args) > 1: + callback = args[1] + else: + callback = variables.ConstantVariable.create(None) + return variables.WeakRefVariable(args[0], callback) + elif self.value is functools.partial: + if not args: + unimplemented( + gb_type="missing args to functools.partial", + context="", + explanation="functools.partial requires at least one argument", + hints=[ + "Fix the functools.partial call.", + *graph_break_hints.USER_ERROR, + ], + ) + # The first arg, a callable (the ctor below will assert on types) + fn = args[0] + rest_args = args[1:] + # guards for the produced FunctoolsPartialVariable are installed in FunctoolsPartialVariable ctor from the + # args and keywords + return variables.functions.FunctoolsPartialVariable( + fn, args=rest_args, keywords=kwargs + ) + elif self.value is warnings.catch_warnings and not args: + return variables.CatchWarningsCtxManagerVariable.create(tx, kwargs) + elif self.value is torch.cuda.device and not kwargs and len(args) == 1: + if not args[0].is_python_constant(): + raise_type_error(tx, "torch.cuda.device() requires a constant argument") + return variables.CUDADeviceVariable.create(tx, args[0].as_python_constant()) + elif ( + issubclass(type(self.value), type) + and hasattr( + self.value, "__enter__" + ) # TODO(voz): These can invoke user code! + and hasattr( + self.value, "__exit__" + ) # TODO(voz): These can invoke user code! + and self.is_standard_new() + and SideEffects.cls_supports_mutation_side_effects(self.value) + and self.source + and not is_forbidden_context_manager(self.value) + ): + from . import TorchCtxManagerClassVariable + from .functions import ( + BaseUserFunctionVariable, + FunctionDecoratedByContextlibContextManagerVariable, + ) + + # graph break on any contextlib.* that it is not contextlib.contextmanager + # Some of the APIs below are not supported because they rely on features + # that Dynamo doesn't play well today (i.e. contextlib.suppress) + if self.value in ( + contextlib._AsyncGeneratorContextManager, + contextlib.closing, + contextlib.redirect_stdout, + contextlib.redirect_stderr, + contextlib.AsyncExitStack, + ): + # We are not changing the behavior of Dynamo as these function were + # already ignored on trace_rules.py before #136033 landed + unimplemented( + gb_type="unsupported contextlib.* API", + context=f"{self.value}", + explanation=f"{self.value} not supported. This may be due to its use of " + "context-specific operations that are not supported in " + "Dynamo yet (i.e. Exception handling)", + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + ) + arg_new = args + if self.value is contextlib._GeneratorContextManager and isinstance( + args[0], (BaseUserFunctionVariable, TorchCtxManagerClassVariable) + ): + if not torch._dynamo.config.enable_trace_contextlib: + unimplemented( + gb_type="attempted to trace contextlib.contextmanager", + context=f"args={args}", + explanation="Tracing contextlib.contextmanager is disabled.", + hints=[ + "Set torch._dynamo.config.enable_trace_contextlib = True", + ], + ) + + # Special treatments for certain context managers created via + # contextlib, because + # 1. we (pytorch) own their impls + # 2. it's tedious to trace through them, so we effectively + # "allow in graph" them without sacrificing soundness. + # + # We would typically reach here via either + # 1. the instance construction in `with ctx_manager(...):`: + # https://github.com/python/cpython/blob/3.12/Lib/contextlib.py#L301 + # 2. calling a function decorated with a context manager: + # https://github.com/python/cpython/blob/3.12/Lib/contextlib.py#L122 + # + # So we basically trace through the surface part of the + # contextlib code, and then special case the shared remaining + # logic (the actual context manager instance construction and + # usage later on). + if isinstance(args[0], TorchCtxManagerClassVariable): + fn_var = args[0] + args_list = args[1].items # type: ignore[union-attr,attr-defined] + kwargs_dict = args[2].keys_as_python_constant() # type: ignore[union-attr,attr-defined] + return fn_var.call_function(tx, args_list, kwargs_dict) + + # Wrap UserFunctionVariable in FunctionDecoratedByContextlibContextManagerVariable + # if the function is annotated with @contextlib.contextmanager + # This shouldn't be necessary once generator functions are fully + # supported in dynamo + # pyrefly: ignore[unsupported-operation] + arg_new = [ + FunctionDecoratedByContextlibContextManagerVariable( + args[0], source=args[0].source + ) + ] + args[1:] + + return tx.inline_user_function_return( + VariableTracker.build( + tx, polyfills.instantiate_user_defined_class_object + ), + [self, *arg_new], + kwargs, + ) + elif is_namedtuple_cls(self.value): + if is_structseq_class(self.value): + if kwargs or len(args) != 1: + raise_args_mismatch( + tx, + "torch.return_types", + "1 args and 0 kwargs", + f"{len(args)} args and {len(kwargs)} kwargs", + ) + # Structseq tp_new is a C function, so we can't trace into + # it like namedtuples. Use track_new_user_defined_object + # directly with self as both base_cls_vt and cls_vt. + return tx.output.side_effects.track_new_user_defined_object( + self, + self, + list(args), + ) + else: + # Namedtuple __new__ is a Python function that calls + # tuple.__new__(cls, (field_values,)). Let Dynamo trace + # into it so default values and kwargs are handled by + # the generated __new__ itself. + return tx.inline_user_function_return( + VariableTracker.build( + tx, polyfills.instantiate_user_defined_class_object + ), + [self, *args], + kwargs, + ) + elif self.value is torch.Size: + # This simulates `THPSize_pynew`, the C impl for `Size.__new__`. + from .lists import SizeVariable + + tup = SourcelessBuilder.create(tx, tuple).call_function(tx, args, kwargs) + return SizeVariable(tup.items) # type: ignore[missing-attribute] + elif is_pydantic_dataclass_cls(self.value): + # Pydantic populates dataclass fields through an external validator, + # so tracing through the constructor misses the instance mutations. + unimplemented( + gb_type="Pydantic dataclass constructor", + context=f"{self.value}", + explanation="Dynamo graph breaks on pydantic dataclass constructors " + "because validation mutates the instance outside traced bytecode.", + hints=graph_break_hints.SUPPORTABLE, + ) + elif ( + self.value in self._in_graph_classes() + or is_traceable_wrapper_subclass_type(self.value) + ): + # torch.LongTensor cannot accept a list of FakeTensors. + # So we stack the list of FakeTensors instead. + from .lists import ListVariable + + if ( + np + and self.value in tensortype_to_dtype + and len(args) == 1 + and isinstance(args[0], ListVariable) + and len(args[0].items) > 1 + and all(x.is_tensor() for x in args[0].items) + ): + # Stack FakeTensor + stacked = wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + torch.stack, + *proxy_args_kwargs(args, kwargs), + ), + ) + args = [stacked] + + if issubclass(self.value, torch.Stream): + from .lists import TupleVariable + + var_kwargs = ConstDictVariable( + {VariableTracker.build(tx, k): v for k, v in kwargs.items()} + ) + var_args = TupleVariable(list(args)) + stream = self.value( + *(var_args.as_python_constant()), + **(var_kwargs.as_python_constant()), + ) + from ..graph_bytecode_inputs import register_graph_created_object + from .streams import StreamVariable + + ind = register_graph_created_object( + stream, + StreamVariable.make_construct_in_graph_stream_fn( + var_args, var_kwargs + ), + ) + tensor_variable = wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", get_external_object_by_index, (ind,), {} + ), + ) + elif issubclass(self.value, torch.Event): + from .lists import TupleVariable + + # Register newly created event for reconstruction + var_kwargs = ConstDictVariable( + {VariableTracker.build(tx, k): v for k, v in kwargs.items()} + ) + var_args = TupleVariable(list(args)) + event = self.value( + *(var_args.as_python_constant()), + **(var_kwargs.as_python_constant()), + ) + from ..graph_bytecode_inputs import register_graph_created_object + from .streams import EventVariable + + ind = register_graph_created_object( + event, + EventVariable.make_construct_in_graph_event_fn( + var_args, var_kwargs + ), + ) + tensor_variable = wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", get_external_object_by_index, (ind,), {} + ), + ) + else: + tensor_variable = wrap_fx_proxy( + tx=tx, + proxy=tx.output.create_proxy( + "call_function", + self.value, + *proxy_args_kwargs(args, kwargs), + ), + ) + + return tensor_variable + elif self.value is random.Random: + if len(args) == 1 and args[0].is_python_constant(): + seed = args[0].as_python_constant() + else: + seed = None + random_object = random.Random(seed) + return RandomVariable(random_object) + elif ( + self.value is types.MappingProxyType + and len(args) == 1 + and isinstance(args[0], ConstDictVariable) + ): + # types.MappingProxyType is a read-only proxy of the dict. If the + # original dict changes, the changes are reflected in proxy as well. + return variables.MappingProxyVariable(args[0]) + elif SideEffects.cls_supports_mutation_side_effects(self.value) and self.source: + with do_not_convert_to_tracable_parameter(): + return tx.inline_user_function_return( + VariableTracker.build( + tx, polyfills.instantiate_user_defined_class_object + ), + [self, *args], + kwargs, + ) + + return super().call_function(tx, args, kwargs) + + def is_standard_new(self) -> bool: + """Check for __new__ being overridden""" + new_fn = inspect.getattr_static(self.value, "__new__", None) + if isinstance(new_fn, staticmethod): + new_fn = new_fn.__func__ + return new_fn is object.__new__ + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> "ConstantVariable": + if self.source: + install_guard( + self.source.make_guard( + functools.partial(GuardBuilder.HASATTR, attr=name) + ) + ) + return VariableTracker.build(tx, hasattr(self.value, name)) + + def const_getattr(self, tx: "InstructionTranslator", name: str) -> Any: + if name == "__name__": + return self.value.__name__ + return super().const_getattr(tx, name) + + def is_python_hashable(self) -> Literal[True]: + return True + + def get_python_hash(self) -> int: + return hash(self.value) + + def is_python_equal(self, other: object) -> bool: + return ( + isinstance(other, variables.UserDefinedClassVariable) + and self.value is other.value + ) + + def get_real_python_backed_value(self) -> object: + return self.value + + +class UserDefinedExceptionClassVariable(UserDefinedClassVariable): + @property + def fn(self) -> type[object]: + return self.value + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + from .builder import SourcelessBuilder + + if self.source is None: + # NB: If source is added via side effects, create the exception + # object through side_effects as well. See FrozenDataClass creation + var = tx.output.side_effects.track_new_user_defined_object( + SourcelessBuilder.create(tx, BaseException), + self, + list(args), + ) + var.call_method(tx, "__init__", list(args), dict(kwargs)) + return var + return super().call_function(tx, args, kwargs) + + +class RemovableHandleClass: + # Dummy class to pass to python_type of + # RemovableHandleVariable + # Useful for isinstance check on hooks + pass + + +def call_random_fn( + tx: "InstructionTranslator", + fn: Callable[..., Any], + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], +) -> VariableTracker: + from .builder import VariableBuilder + + args = [x.as_python_constant() for x in args] + kwargs = {k: v.as_python_constant() for k, v in kwargs.items()} + random_call_index = len(tx.output.random_calls) + # NB: it is probably not important for the example_value to be exactly correct, + # we just need the right type + example_value = fn(*args, **kwargs) + source = RandomValueSource(random_call_index) + tx.output.random_calls.append((fn, args, kwargs)) # type: ignore[arg-type] + # TODO: arguably, this should route to wrap_symint/wrap_symfloat + # (currently hypothetical), but I'm not going to poke my hand in + # this nest for now + return VariableBuilder(tx, source).wrap_unspecialized_primitive(example_value) + + +class UserDefinedObjectVariable(UserDefinedVariable): + """ + Mostly objects of defined type. Catch-all for something where we only know the type. + """ + + # VT representing the base built-in type's data for subclassed built-in types + # (e.g., ConstDictVariable for dict subclasses, ListVariable for list subclasses). + # None for plain user-defined objects that don't subclass a built-in container. + _base_vt: VariableTracker | None = None + + # Set of base class methods that can be delegated to _base_vt. + # Used to check whether a method is overridden before delegating. + _base_methods: set[Any] | None = None + + _nonvar_fields = { + "value", + "value_type", + *UserDefinedVariable._nonvar_fields, + } + + def __init__( + self, + value: object, + *, + value_type: type | None = None, + cls_source: TypeSource | None = None, + base_cls_vt: VariableTracker | None = None, + init_args: Sequence[VariableTracker] | None = None, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.value = value + self.value_type = value_type or type(value) + assert type(value) is self.value_type + # This is used with __new__, when the new object is sourceless but the user class can be sourceful. + self.cls_source = cls_source + if cls_source is None and self.source is not None: + self.cls_source = TypeSource(self.source) + + # These attributes are used to reconstruct the user defined object. The + # pseudo code looks like this. Builtin C __new__ do not support kwargs, + # so init_args is sufficient. + # obj = base_cls.__new__(user_cls, *args) + self.base_cls_vt = base_cls_vt + self.init_args = init_args + + # This records the attributes that were modified via instance + # `__dict__` directly, rather than the normal setattr path. + self.dict_vt: DunderDictVariable | None = None + + # Cache inspect.getattr_static outputs for the same name. This is fine + # because if there is a mutation for the name, we use side-effects infra + # to early return the mutated value. + self._looked_up_attrs: dict[str, object] = {} + + # This is to avoid getattr_static calls to look up the subobj from the self.value.__class__ + self._subobj_from_class: dict[str, object] = {} + + import torch.utils._pytree as pytree + + self.is_pytree_constant_class = pytree.is_constant_class(self.value_type) + if pytree.is_constant_class(self.value_type) and self.source: + install_guard(self.source.make_guard(GuardBuilder.EQUALS_MATCH)) + + self._object_has_getattribute = object_has_getattribute(self.value) + + def __str__(self) -> str: + inner = self.value_type.__name__ + if inner in [ + "builtin_function_or_method", + "getset_descriptor", + "method_descriptor", + "method", + ]: + inner = str(getattr(self.value, "__name__", None)) + return f"{self.__class__.__name__}({inner})" + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.value_type.__name__})" + + def get_dict_vt(self, tx: "InstructionTranslator") -> "DunderDictVariable": + if self.dict_vt is None: + self.dict_vt = variables.DunderDictVariable.create(tx, self) + return self.dict_vt + + def is_base_vt_modified(self, side_effects: "SideEffects") -> bool: + if self._base_vt is not None: + return side_effects.is_modified(self._base_vt) + return False + + def python_type(self) -> type: + return self.value_type # type: ignore[return-value] + + def get_real_python_backed_value(self) -> object: + return self.value + + def as_python_constant(self) -> object: + if isinstance( + self.value, + (enum.Enum, torch.DispatchKey, torch._C._functorch.TransformType), + ): + return self.value + + if self.is_pytree_constant_class and self.source: + # NOTE pytree constants created in the torch.compile region will + # NOT be guarded (even though they have a source set) + return self.value + # TODO else try reconstructing the object by, e.g., leveraging side + # effects and `as_python_constant`. + + # Special case for _MaskModWrapper during legacy export: Dynamo creates + # objects via __new__ without calling __init__, so self.value.fn is unset. + # Reconstruct from the tracked side-effect attribute instead. + from torch.nn.attention.flex_attention import _MaskModWrapper + + if isinstance(self.value, _MaskModWrapper): + from torch._dynamo.symbolic_convert import InstructionTranslator + + tx = InstructionTranslator.current_tx() + if tx is not None and tx.export: + fn_vt = tx.output.side_effects.load_attr(self, "fn", deleted_ok=True) + if fn_vt is not None: + # Let as_python_constant() raise the proper exception + # (e.g., ClosureConversionError for non-constant closures) + fn = fn_vt.as_python_constant() + return _MaskModWrapper(fn) + + return super().as_python_constant() + + def as_proxy(self) -> object: + if isinstance(self.value, enum.Enum): + if isinstance(self.value, int): + return int(self.value) + return self.value + return super().as_proxy() + + def guard_as_python_constant(self) -> object: + if self.source: + install_guard(self.source.make_guard(GuardBuilder.ID_MATCH)) + return self.value + return super().guard_as_python_constant() + + def bool_impl( + self, + tx: "InstructionTranslator", + ) -> "VariableTracker | None": + # Mirrors slot_nb_bool: + # https://github.com/python/cpython/blob/c09ccd9c429/Objects/typeobject.c#L9408-L9458 + if self._maybe_get_baseclass_method("__bool__"): + result = self.call_method(tx, "__bool__", [], {}) + if result.is_python_constant(): + result_value = result.as_python_constant() + if not isinstance(result_value, bool): + raise_observed_exception( + TypeError, + tx, + args=[ + f"__bool__ should return bool, returned {type(result_value).__name__}" + ], + ) + return result + return None + + def nb_index_impl( + self, + tx: "InstructionTranslator", + ) -> VariableTracker: + # CPython: PyNumber_Index checks tp_as_number->nb_index. + # For user-defined types, __index__ in tp_dict means nb_index is set. + type_attr = inspect.getattr_static(type(self.value), "__index__", None) + if type_attr is None: + return super().nb_index_impl(tx) + source = self.source and self.get_source_by_walking_mro(tx, "__index__") + method_var = self.resolve_type_attr(tx, "__index__", type_attr, source) + result = method_var.call_function(tx, [], {}) + # CPython validates that __index__ returns an int. + # https://github.com/python/cpython/blob/c09ccd9c429/Objects/abstract.c#L1433-L1438 + if result.is_python_constant() and not isinstance( + result.as_python_constant(), int + ): + raise_observed_exception( + TypeError, + tx, + args=[ + f"__index__ returned non-int (type {type(result.as_python_constant()).__name__})" + ], + ) + return result + + def nb_int_impl( + self, + tx: "InstructionTranslator", + ) -> VariableTracker: + # CPython: slot_nb_int calls __int__(), PyNumber_Long validates the return type. + # https://github.com/python/cpython/blob/v3.13.0/Objects/abstract.c#L1538-L1550 + source = self.source and self.get_source_by_walking_mro(tx, "__int__") + method_var = self.resolve_type_attr( + tx, + "__int__", + inspect.getattr_static(type(self.value), "__int__"), + source, + ) + result = method_var.call_function(tx, [], {}) + if not issubclass(result.python_type(), int): + raise_observed_exception( + TypeError, + tx, + args=[ + f"__int__ returned non-int (type {result.python_type().__name__})" + ], + ) + return result + + def nb_float_impl( + self, + tx: "InstructionTranslator", + ) -> VariableTracker: + # CPython: slot_nb_float calls __float__(), PyNumber_Float validates the return type. + # https://github.com/python/cpython/blob/v3.13.0/Objects/abstract.c#L1647-L1658 + source = self.source and self.get_source_by_walking_mro(tx, "__float__") + method_var = self.resolve_type_attr( + tx, + "__float__", + inspect.getattr_static(type(self.value), "__float__"), + source, + ) + result = method_var.call_function(tx, [], {}) + if not issubclass(result.python_type(), float): + raise_observed_exception( + TypeError, + tx, + args=[ + f"__float__ returned non-float (type {result.python_type().__name__})" + ], + ) + return result + + def torch_function_check(self) -> None: + assert has_torch_function(self), ( + f"calling torch function on object without __torch_function__ {self}" + ) + + def get_torch_fn(self, tx: "InstructionTranslator") -> VariableTracker: + self.torch_function_check() + from .torch_function import get_torch_function_fn + + return get_torch_function_fn(tx, self) + + def call_torch_function( + self, + tx: "InstructionTranslator", + fn: VariableTracker, + types: "TupleVariable", + args: Sequence[Any], + kwargs: dict[str, Any], + ) -> VariableTracker: + self.torch_function_check() + + from .torch_function import call_torch_function + + return call_torch_function( + tx, + self.get_torch_fn(tx), + fn, + types, + args, + kwargs, + ) + + @staticmethod + @functools.cache + def _supported_random_functions() -> set[Any]: + fns = { + random.random, + random.randint, + random.randrange, + random.uniform, + } + return fns + + def mp_subscript_impl( + self, + tx: "InstructionTranslator", + key: VariableTracker, + ) -> VariableTracker: + # PyObject_GetItem: https://github.com/python/cpython/blob/62a6e898e01/Objects/abstract.c#L155-L206 + method = self._maybe_get_baseclass_method("__getitem__") + if ( + self._base_vt is not None + and self._base_methods is not None + and method in self._base_methods + ): + return self._base_vt.mp_subscript_impl(tx, key) + if isinstance(method, types.FunctionType): + source_fn = self.source and self.get_source_by_walking_mro( + tx, "__getitem__" + ) + return variables.UserMethodVariable( + method, self, source_fn=source_fn, source=self.source + ).call_function(tx, [key], {}) + return super().mp_subscript_impl(tx, key) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[Any], + kwargs: dict[str, Any], + ) -> VariableTracker: + from .. import trace_rules + from . import UserMethodVariable + from .constant import ConstantVariable + + method = self._maybe_get_baseclass_method(name) + if method is not None: + if method is object.__init__: + return ConstantVariable.create(None) + + if is_standard_setattr(method) or isinstance(self.value, threading.local): + return self.method_setattr_standard(tx, *args, **kwargs) + + if is_standard_delattr(method): + return self.method_setattr_standard( + tx, args[0], variables.DeletedVariable() + ) + + if method is object.__eq__ and len(args) == 1 and not kwargs: + other = args[0] + if not isinstance(other, UserDefinedObjectVariable): + return VariableTracker.build(tx, NotImplemented) + + # TODO(anijain2305) - Identity checking should already be a part + # of the cmp_eq polyfill function. + return VariableTracker.build(tx, self.value is other.value) + + if torch._dynamo.config.enable_faithful_generator_behavior and isinstance( + self.value, types.GeneratorType + ): + unimplemented( + gb_type="call_method on generator", + context=f"object={self.value}, method={name}, args={args}, kwargs={kwargs}", + explanation="Detected a method call to a user-defined generator object. " + "This is not fully supported.", + hints=[ + "Set `torch._dynamo.config.enable_faithful_generator_behavior = False`. Note that this " + "may cause silent incorrectness, since we will eagerly unpack generators instead of lazily " + "evaluating them.", + ], + ) + + # torch.Generator methods like manual_seed(), get_state(), etc. + # are stateful RNG operations that cannot be soundly traced. + if ( + isinstance(self.value, torch._C.Generator) + and name in trace_rules._GENERATOR_METHODS_THAT_GRAPH_BREAK + ): + unimplemented( + gb_type="torch.Generator method", + context=f"torch.Generator.{name}", + explanation=f"torch.Generator.{name}() is a stateful RNG " + "operation that cannot be soundly traced in the FX graph.", + hints=[*graph_break_hints.FUNDAMENTAL], + ) + + # Delegate to _base_vt for non-overridden base-class methods + if ( + self._base_vt is not None + and self._base_methods is not None + and method in self._base_methods + ): + return self._base_vt.call_method(tx, name, args, kwargs) + + # check for methods implemented in C++ + if isinstance(method, types.FunctionType): + source = self.source + source_fn = None + if source: + source_fn = self.get_source_by_walking_mro(tx, name) + # TODO(jansel): add a guard to check for monkey patching? + from ..mutation_guard import unpatched_nn_module_init + + if method is torch.nn.Module.__init__: + method = unpatched_nn_module_init + return UserMethodVariable( + method, self, source_fn=source_fn, source=source + ).call_function(tx, args, kwargs) # type: ignore[arg-type] + + if method is list.__len__ and self.source and not (args or kwargs): + install_guard(self.source.make_guard(GuardBuilder.SEQUENCE_LENGTH)) + return VariableTracker.build(tx, len(self.value)) # type: ignore[arg-type] + + return super().call_method(tx, name, args, kwargs) + + def len_impl(self, tx: "InstructionTranslator") -> VariableTracker: + method = self._maybe_get_baseclass_method("__len__") + if method is not None: + type_attr = self.lookup_class_mro_attr("__len__") + source = self.source and self.get_source_by_walking_mro(tx, "__len__") + method_var = self.resolve_type_attr(tx, "__len__", type_attr, source) + if not isinstance(method_var, variables.GetAttrVariable): + return method_var.call_function(tx, [], {}) + + unimplemented( + gb_type="Cannot trace user-defined __len__", + context=f"{self.python_type_name()}.__len__()", + explanation=( + f"Dynamo cannot trace len() on {self.python_type_name()} because the __len__ " + "method is either not traceable (e.g., defined in C or built-in) or returns a " + "non-constant value." + ), + hints=[ + *graph_break_hints.SUPPORTABLE, + ], + ) + + def sq_length(self, tx: "InstructionTranslator") -> VariableTracker: + if ( + self._base_vt is not None + and self._base_methods is not None + and self._maybe_get_baseclass_method("__len__") in self._base_methods + ): + return self._base_vt.sq_length(tx) + return self.len_impl(tx) + + def mp_length(self, tx: "InstructionTranslator") -> VariableTracker: + if ( + self._base_vt is not None + and self._base_methods is not None + and self._maybe_get_baseclass_method("__len__") in self._base_methods + ): + return self._base_vt.mp_length(tx) + return self.len_impl(tx) + + def method_setattr_standard( + self, + tx: "InstructionTranslator", + name: VariableTracker, + value: VariableTracker, + directly_update_dict: bool = False, + ) -> VariableTracker: + name_str = "" + try: + name_str = name.as_python_constant() + except NotImplementedError: + unimplemented( + gb_type="non-const setattr name on user-defined object", + context=f"object={self}, name={name}, value={value}", + explanation="Detected a call to `setattr` of a user-defined object with a non-constant name.", + hints=["Ensure that the name is a string."], + ) + assert tx.output.side_effects.is_attribute_mutation(self), ( + "Attempted setattr on a user-defined object that does not have " + "an AttributeMutation mutation_type" + ) + + if ( + torch.distributed.is_available() + and type(self.value) is torch.distributed.P2POp + and ( + tx.output.side_effects.has_pending_mutation_of_attr(self, name_str) + or name_str in self.value.__dict__ + ) + ): + unimplemented( + gb_type="P2POp mutation", + context=f"object={self}, name={name}, value={value}", + explanation="Dynamo does not support mutating torch.distributed.P2POp instances.", + hints=[ + "Construct a new torch.distributed.P2POp instead of mutating an existing one inside torch.compile.", + ], + ) + + if name_str == "__class__": + unimplemented( + gb_type="__class__ assignment on user-defined object", + context=f"object={self}, value={value}", + explanation="Dynamo does not support reassigning __class__ on user-defined objects.", + hints=[ + "Move the __class__ assignment outside of the torch.compile region.", + ], + ) + + if directly_update_dict: + self.get_dict_vt(tx).setitem(name_str, value) + else: + tmp = self.try_get_descritor_and_setter_py_func(name_str) + if tmp: + descriptor, setter = tmp + # Emulate + # https://github.com/python/cpython/blob/3.11/Objects/object.c#L1371-L1452 + desc_source = None + func_source = None + if self.cls_source: + desc_source = self.get_source_by_walking_mro(tx, name_str) + # use `type(...)` to ignore instance attrs. + func_source = AttrSource(TypeSource(desc_source), "__set__") + desc_var = VariableTracker.build(tx, descriptor, desc_source) + func_var = VariableTracker.build(tx, setter, func_source, realize=True) + if isinstance(descriptor, property): + args = [self, value] # property.fset(self, value) + else: + args = [desc_var, self, value] # __set__(desc, self, value) + return func_var.call_function(tx, args, {}) + + # Handle Python property descriptors whose __set__ is a C slot + # wrapper (not a Python function), which the above check misses. + # Mirrors the property getter handling in var_getattr. + descriptor = inspect.getattr_static(type(self.value), name_str, None) + if isinstance(descriptor, property) and descriptor.fset is not None: + fset_source = None + if self.cls_source: + fset_source = AttrSource( + self.get_source_by_walking_mro(tx, name_str), "fset" + ) + fset_var = VariableTracker.build( + tx, descriptor.fset, source=fset_source + ) + return fset_var.call_function(tx, [self, value], {}) + + # NOTE: else we assume the descriptor (if any) has a + # side-effect-free `__set__` as far as Dynamo tracing is concerned. + + # If the code reaches here, the attribute is either: + # 1) a slot descriptor + # 2) a plain attribute with no descriptor + # If the object has no __dict__, only slot descriptors (member_descriptor) + # allow mutation. Any other attribute assignment raises AttributeError. + if not hasattr(self.value, "__dict__"): + descriptor = self.lookup_class_mro_attr(name_str) + if not inspect.ismemberdescriptor(descriptor): + error_msg = VariableTracker.build( + tx, + f"'{type(self.value).__name__}' object has no attribute '{name_str}'", + ) + raise_observed_exception(AttributeError, tx, args=[error_msg]) + + tx.output.side_effects.store_attr(self, name_str, value) + return variables.ConstantVariable.create(None) + + def needs_slow_setattr(self) -> bool: + return not is_standard_setattr( + inspect.getattr_static(self.value, "__setattr__", None) + ) and not isinstance(self.value, threading.local) + + def unpack_var_sequence(self, tx: "InstructionTranslator") -> list[VariableTracker]: + if self._base_vt is not None and self._base_methods is not None: + iter_method = self._maybe_get_baseclass_method("__iter__") + if iter_method is not None and iter_method in self._base_methods: + return self._base_vt.unpack_var_sequence(tx) + if ( + self.source + and self._maybe_get_baseclass_method("__iter__") is list.__iter__ + and self._maybe_get_baseclass_method("__len__") is list.__len__ + and self._maybe_get_baseclass_method("__getitem__") is list.__getitem__ + ): + install_guard(self.source.make_guard(GuardBuilder.SEQUENCE_LENGTH)) + return [ + variables.LazyVariableTracker.create( + self.value[k], # type: ignore[index] + source=GetItemSource(self.source, k), + ) + for k in range(len(self.value)) # type: ignore[arg-type] + ] + return super().unpack_var_sequence(tx) + + def has_force_unpack_var_sequence(self, tx: "InstructionTranslator") -> bool: + from .builder import SourcelessBuilder + + try: + SourcelessBuilder.create(tx, iter).call_function(tx, [self], {}) + return True + except ObservedTypeError: + handle_observed_exception(tx) + return False + + def force_unpack_var_sequence( + self, tx: "InstructionTranslator" + ) -> list[VariableTracker]: + from .builder import SourcelessBuilder + + result = [] + iter_ = SourcelessBuilder.create(tx, iter).call_function(tx, [self], {}) + + while True: + try: + r = iter_.next_variable(tx) + result.append(r) + except ObservedUserStopIteration: + handle_observed_exception(tx) + break + return result + + def next_variable(self, tx: "InstructionTranslator") -> VariableTracker: + return self.call_method(tx, "__next__", [], {}) + + def is_supported_random(self) -> bool: + try: + return self.value in self._supported_random_functions() + except TypeError: + # TypeError: unhashable type + return False + + def call_function( + self, + tx: "InstructionTranslator", + args: Sequence[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if ( + self.is_supported_random() + and all(k.is_python_constant() for k in args) + and all(v.is_python_constant() for v in kwargs.values()) + ): + return call_random_fn(tx, self.value, args, kwargs) # type: ignore[arg-type] + elif istype(self.value, types.MethodType): + func = self.value.__func__ + obj = self.value.__self__ + if ( + func is torch.utils._contextlib._DecoratorContextManager.clone + and variables.TorchCtxManagerClassVariable.is_matching_cls( + obj.__class__ + ) + and not (args or kwargs) + ): + return variables.TorchCtxManagerClassVariable( + obj.__class__ + ).call_function(tx, args, kwargs) + + if ( + func is torch.autograd.grad_mode.inference_mode.clone + and obj.__class__ is torch.autograd.grad_mode.inference_mode + ): + # simulate the inference_mode.clone implementation + var = VariableTracker.build(tx, obj.mode) # type: ignore[attr-defined] + return variables.TorchCtxManagerClassVariable( + obj.__class__ + ).call_function(tx, [var], kwargs) + + if self.source is None: + unimplemented( + gb_type="attempted to call sourceless user-defined object as a method", + context=f"object={self.value}, function={func}, args={args}, kwargs={kwargs}", + explanation="Dynamo does not support this.", + hints=[ + f"Ensure the user-defined object {self.value} is constructed outside the compiled region.", + ], + ) + assert self.source is not None + func_src = AttrSource(self.source, "__func__") + func_var = VariableTracker.build(tx, func, func_src, realize=True) + obj_src = AttrSource(self.source, "__self__") + obj_var = VariableTracker.build(tx, obj, obj_src) + return func_var.call_function(tx, [obj_var] + args, kwargs) # type: ignore[arg-type] + elif callable(self.value): + if self.source: + assert self.cls_source is not None + source_attr = AttrSource(self.cls_source, "__call__") + install_guard(source_attr.make_guard(GuardBuilder.CLOSURE_MATCH)) + return self.call_method(tx, "__call__", args, kwargs) # type: ignore[arg-type] + + return super().call_function(tx, args, kwargs) + + def _check_for_getattr(self) -> object: + return get_custom_getattr(self.value) + + def _is_c_defined_property(self, subobj: object) -> bool: + if not isinstance(subobj, property): + return False + + # pybind def_readwrite is implemented via PyCFunction. At the python level, it is visible as a property whose + # fget is an instancemethod wrapper - https://docs.python.org/3/c-api/method.html#c.PyInstanceMethod_Check + + # If we have a PyCFunction, we make an assumption that there is no side effect. + return isinstance( + subobj.fget, types.BuiltinFunctionType + ) or torch._C._dynamo.utils.is_instancemethod(subobj.fget) # type: ignore[attr-defined] + + def _getattr_static(self, name: str) -> object: + if name in self._looked_up_attrs: + return self._looked_up_attrs[name] + + subobj = inspect.getattr_static(self.value, name, NO_SUCH_SUBOBJ) + + # In some cases, we have to do dynamic lookup because getattr_static is not enough. For example, threading.local + # has side-effect free __getattribute__ and the attribute is not visible without a dynamic lookup. + # NOTE we assume the following descriptors are side-effect-free as far + # as Dynamo tracing is concerned. + if not self._object_has_getattribute and ( + subobj is NO_SUCH_SUBOBJ # e.g., threading.local + or inspect.ismemberdescriptor(subobj) # e.g., __slots__ + or inspect.isgetsetdescriptor(subobj) # e.g., __dict__ + or self._is_c_defined_property(subobj) + ): + # Call __getattribute__, we have already checked that this is not overridden and side-effect free. We don't + # want to call getattr because it can be user-overridden. + subobj = type(self.value).__getattribute__(self.value, name) + elif self._object_has_getattribute and subobj is NO_SUCH_SUBOBJ: + # If the object has an overridden getattribute method, Dynamo has + # already tried tracing it, and encountered an AttributeError. We + # call getattr_static only when the __getattribute__ tracing fails + # (check var_getattr impl). So, it is safe here to raise the + # AttributeError. + raise AttributeError + + self._looked_up_attrs[name] = subobj + return subobj + + def lookup_class_mro_attr(self, name: str) -> object: + """Walk type(obj).__mro__ to find *name* in the class hierarchy. + + This only searches the class chain (type(obj).__mro__), NOT the + metaclass chain (type(type(obj)).__mro__). The distinction matters + because inspect.getattr_static conflates both chains — it can return + metaclass descriptors (e.g. type.__dict__['__annotations__'], a + getset_descriptor) when the attribute doesn't exist on the class MRO. + Walking cls.__mro__ directly avoids that leak. + """ + if name in self._subobj_from_class: + return self._subobj_from_class[name] + result = NO_SUCH_SUBOBJ + for base in self.value.__class__.__mro__: + if name in base.__dict__: + result = base.__dict__[name] + break + self._subobj_from_class[name] = result + return result + + def should_skip_descriptor_setter(self, attr_name: str) -> bool: + # Check if `attr_name` corresponds to a descriptor. + descriptor = inspect.getattr_static(type(self.value), attr_name, None) + setter = inspect.getattr_static(type(descriptor), "__set__", None) + if setter: + # Skip if `__set__` was traceable (no need to redo the side effect). + if inspect.isfunction(setter): + return True + # For untraceable `__set__` we should still skip if the attribute + # was mutated via instance `__dict__`. + elif self.dict_vt and self.dict_vt.contains(attr_name): + return True + return False + + def try_get_descritor_and_setter_py_func( + self, attr_name: str + ) -> tuple[object, object] | None: + descriptor = inspect.getattr_static(type(self.value), attr_name, None) + # Handle property descriptors with setters - call fset directly + if isinstance(descriptor, property) and descriptor.fset is not None: + return (descriptor, descriptor.fset) + setter = inspect.getattr_static(type(descriptor), "__set__", None) + if inspect.isfunction(setter): + return (descriptor, setter) + return None + + def has_key_in_generic_dict(self, tx: "InstructionTranslator", key: str) -> bool: + if tx.output.side_effects.has_pending_mutation_of_attr(self, key): + mutated_attr = tx.output.side_effects.load_attr(self, key, deleted_ok=True) + return not isinstance(mutated_attr, variables.DeletedVariable) + + # TODO(guilhermeleobas): This can trigger a side effect + return key in self.value.__dict__ + + def get_source_by_walking_mro( + self, tx: "InstructionTranslator", name: str + ) -> DictGetItemSource: + assert self.cls_source is not None + + for idx, klass in enumerate(type(self.value).__mro__): + if name in klass.__dict__: + descriptor = klass.__dict__[name] + + # Guard that intermediate MRO classes don't shadow this + # attribute, deduplicating by (id(klass), name) across + # subclasses that share the same intermediate MRO class. + # Safe because TYPE_MATCH guards fix the MRO, so the same + # id(klass) always refers to the same class object. + for absent_idx in range(1, idx): + absent_klass = type(self.value).__mro__[absent_idx] + cache_key = (id(absent_klass), name) + if cache_key in tx.output.guarded_mro_absent_keys: + continue + tx.output.guarded_mro_absent_keys.add(cache_key) + mro_source = TypeMROSource(self.cls_source) + klass_source: Source = GetItemSource(mro_source, absent_idx) + dict_source = TypeDictSource(klass_source) + install_guard( + dict_source.make_guard( + functools.partial(GuardBuilder.DICT_NOT_CONTAINS, key=name) + ) + ) + + # Guard that the instance __dict__ does not shadow the + # class attribute. Skipped for data descriptors (those + # with __set__, e.g. property) because Python gives data + # descriptors priority over instance __dict__ in attribute + # lookup — the instance dict can only be populated by + # directly writing to obj.__dict__, not via setattr. + if ( + self.source + and hasattr(self.value, "__dict__") + and name not in self.value.__dict__ + and not hasattr(descriptor, "__set__") + ): + install_guard( + self.source.make_guard( + functools.partial( + GuardBuilder.NOT_PRESENT_IN_GENERIC_DICT, attr=name + ) + ) + ) + + # Reuse the source if we've already resolved the same + # descriptor object for the same attribute name (e.g. same + # property reached via different subclasses) to avoid + # redundant ID_MATCH guards. We include name in the key + # because distinct attributes can point to the same object + # (e.g. a = b = some_obj, or interned small integers). + cache_key = (id(descriptor), name) + cache = tx.output.mro_source_cache + if cache_key in cache: + return cache[cache_key] + + if idx != 0: + mro_source = TypeMROSource(self.cls_source) + klass_source = GetItemSource(mro_source, idx) + else: + klass_source = self.cls_source + dict_source = TypeDictSource(klass_source) + out_source = DictGetItemSource(dict_source, name) + cache[cache_key] = out_source + return out_source + + unimplemented( + gb_type="could not find name in object's mro", + context=f"name={name}, object type={type(self.value)}, mro={type(self.value).__mro__}", + explanation=f"Could not find name `{name}` in mro {type(self.value).__mro__}", + hints=[ + f"Ensure the name `{name}` is defined somewhere in {self.value}'s type hierarchy.", + *graph_break_hints.USER_ERROR, + ], + ) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + source: Source | None = AttrSource(self.source, name) if self.source else None + + if self._object_has_getattribute: + getattribute_fn = inspect.getattr_static( + type(self.value), "__getattribute__" + ) + new_source: AttrSource | None = ( + AttrSource(self.source, "__getattribute__") if self.source else None + ) + + try: + return variables.UserMethodVariable( + getattribute_fn, + self, + source=new_source, + ).call_function(tx, [VariableTracker.build(tx, name)], {}) + except ObservedAttributeError: + # Pass through to __getattr__ if __getattribute__ fails + handle_observed_exception(tx) + + if tx.output.side_effects.has_pending_mutation_of_attr(self, name): + result = tx.output.side_effects.load_attr(self, name, deleted_ok=True) + if isinstance(result, variables.DeletedVariable): + raise_observed_exception( + AttributeError, + tx, + args=[ + f"'{type(self.value).__name__}' object has no attribute '{name}'", + ], + ) + return result + + if name == "__dict__": + if not hasattr(self.value, "__dict__"): + raise_observed_exception(AttributeError, tx) + return self.get_dict_vt(tx) + + # TODO(anijain2305) - Investigate if we need specialization for more + # dunder attrs. inspect.getattr_static does not return correct value for + # them. + if name == "__class__": + cls_source: Source | None = source + if source is None: + cls_source = self.cls_source + else: + cls_source = source + return VariableTracker.build(tx, type(self.value), cls_source) + + from ..mutation_guard import unpatched_nn_module_init + + # ---- CPython attribute lookup algorithm ---- + # Mirror object.__getattribute__ (PyObject_GenericGetAttr): + # 1. type_attr = lookup name in type(obj).__mro__ + # 2. if type_attr is a DATA descriptor → invoke it + # 3. if name in obj.__dict__ → return as-is (no descriptor invocation) + # 4. if type_attr is a non-data descriptor → invoke it + # 5. if type_attr is a plain class variable → return it + # 6. __getattr__ fallback + # 7. raise AttributeError + # + # Between steps 5 and 6, we also handle objects with custom storage + # that aren't visible via the MRO walk or instance __dict__ (step 5b). + # + # Step 1: Single MRO walk on the type (cached). + type_attr = self.lookup_class_mro_attr(name) + + # Dynamo patches nn.Module.__init__ at import time to inject tracing + # hooks. Undo that here so the unpatched original is traced instead. + if type_attr is torch.nn.Module.__init__: + type_attr = unpatched_nn_module_init + + # Step 2: Data descriptors on the type take priority over instance dict. + if type_attr is not NO_SUCH_SUBOBJ and is_data_descriptor(type_attr): + return self.resolve_data_descriptor(tx, name, type_attr, source) + + # Step 3: Instance __dict__ — return as-is, no descriptor invocation. + # TODO(guilhermeleobas): step 3 should look into dict_vt and not self.value.__dict__ + # as the object could have mutated an attribute via setattr + if hasattr(self.value, "__dict__") and name in self.value.__dict__: + subobj = self.value.__dict__[name] + source = self.maybe_wrap_nn_module_source_for_instance(tx, name, source) + return VariableTracker.build(tx, subobj, source) + + # Step 4-5: Non-data descriptor or plain class attribute. + if type_attr is not NO_SUCH_SUBOBJ: + return self.resolve_type_attr(tx, name, type_attr, source) + + # Step 5b: Dynamic fallback for attributes that exist on the live + # object but aren't visible to the static MRO walk or instance + # __dict__ check above. This covers objects with custom storage + # backends (e.g. threading.local uses a per-thread dict not + # accessible via obj.__dict__) and C extensions that store data + # outside the normal Python object layout. + # + # This is NOT the same as the C-level data descriptor fallback in + # resolve_data_descriptor (step 2): that handles descriptors found + # on the type MRO (like member_descriptor for __slots__), while this + # handles attributes that aren't on the type MRO at all. + # + # Only safe when the class doesn't override __getattribute__, + # otherwise we'd run arbitrary user code. + if not self._object_has_getattribute: + try: + resolved = type(self.value).__getattribute__(self.value, name) + source = self.maybe_wrap_nn_module_source_for_instance(tx, name, source) + return VariableTracker.build(tx, resolved, source) + except AttributeError: + pass + + # Step 6: __getattr__ fallback. + getattr_fn = self._check_for_getattr() + if isinstance(getattr_fn, types.FunctionType): + if ( + getattr_fn is unpatched_nn_module_getattr + and isinstance(self, variables.UnspecializedNNModuleVariable) + and istype(self.value._parameters, dict) # type: ignore[attr-defined] + and istype(self.value._buffers, dict) # type: ignore[attr-defined] + and istype(self.value._modules, dict) # type: ignore[attr-defined] + ): + out = self.manually_trace_nn_module_getattr(tx, name) + else: + new_source = None + if self.source: + new_source = AttrSource(self.source, "__getattr__") + out = variables.UserMethodVariable( + getattr_fn, self, source=new_source + ).call_function(tx, [variables.ConstantVariable.create(name)], {}) + + if self.source and getattr_fn is torch.nn.Module.__getattr__: + if isinstance( + out, + ( + variables.UnspecializedNNModuleVariable, + variables.NNModuleVariable, + ), + ): + out.set_nn_module_stack_source( # type: ignore[attr-defined] + AttrSource(self.get_nn_module_stack_source(), name) # type: ignore[attr-defined] + ) + return out + + elif getattr_fn is not None: + unimplemented( + gb_type="User-defined object with non-function __getattr__", + context=f"object={self.value}, name={name}, getattr_fn={getattr_fn}", + explanation=f"Found a non-function __getattr__ {getattr_fn} from a user-defined object {self.value} " + f" when attempting to getattr `{name}`", + hints=[ + "Ensure the object's __getattr__ is a function type.", + ], + ) + + # Step 7: AttributeError. + raise_observed_exception( + AttributeError, + tx, + args=[f"'{type(self.value).__name__}' object has no attribute '{name}'"], + ) + + def resolve_data_descriptor( + self, + tx: "InstructionTranslator", + name: str, + type_attr: object, + source: Source | None, + ) -> VariableTracker: + """Handle data descriptors found on the type MRO (property, _tuplegetter, etc.).""" + if isinstance(type_attr, property) and not self._is_c_defined_property( + type_attr + ): + # Python property — trace fget directly. + if self.source: + source = AttrSource(self.get_source_by_walking_mro(tx, name), "fget") + fget_vt = VariableTracker.build( + tx, type_attr.fget, source=source, realize=True + ) + return fget_vt.call_function(tx, [self], {}) + + get_fn = inspect.getattr_static(type(type_attr), "__get__", None) + if isinstance(get_fn, types.FunctionType): + # User-defined data descriptor with a Python __get__. + return self.invoke_descriptor_get(tx, name, type_attr, source) + + # C-level data descriptor (property with C fget, member/getset + # descriptors, Cython attrs, etc.) — resolve via + # object.__getattribute__ which is side-effect free. + # Uninitialized slots raise AttributeError which must be surfaced + # as ObservedAttributeError so dynamo's try/except tracing works. + try: + resolved = type(self.value).__getattribute__(self.value, name) + except AttributeError: + raise_observed_exception( + AttributeError, + tx, + args=[ + f"'{type(self.value).__name__}' object has no attribute '{name}'" + ], + ) + return VariableTracker.build(tx, resolved, source) + + def resolve_type_attr( + self, + tx: "InstructionTranslator", + name: str, + type_attr: object, + source: Source | None, + ) -> VariableTracker: + """Handle non-data descriptors and plain class attributes from the type MRO.""" + from ..mutation_guard import unpatched_nn_module_init + + if ( + type_attr is unpatched_nn_module_init + or type_attr is torch.nn.Module.__init__ + ): + type_attr = unpatched_nn_module_init + + can_use_mro_source = self.cls_source is not None and self.source is not None + + if isinstance(type_attr, staticmethod): + # type_attr is the raw staticmethod wrapper from cls.__dict__ + # (not the unwrapped function). We call __get__ to unwrap it, + # but the *source* must go through __func__ on the descriptor + # (not the resolved function) because the guard needs to watch + # the descriptor object in the class dict, not the result. + if can_use_mro_source: + source = AttrSource( + self.get_source_by_walking_mro(tx, name), "__func__" + ) + func = type_attr.__get__(self.value) + return VariableTracker.build(tx, func, source) + elif isinstance(type_attr, classmethod): + source_fn = None + if can_use_mro_source: + source_fn = AttrSource( + self.get_source_by_walking_mro(tx, name), "__func__" + ) # type: ignore[assignment] + return variables.UserMethodVariable( + type_attr.__func__, + self.var_getattr(tx, "__class__"), + source_fn=source_fn, + source=source, + ) + elif isinstance(type_attr, types.ClassMethodDescriptorType): + func = type_attr.__get__(self.value, None) + return VariableTracker.build(tx, func, source) + elif is_lru_cache_wrapped_function(type_attr): + return variables.WrapperUserMethodVariable( + type_attr, "__wrapped__", self, source=source + ) + elif isinstance(type_attr, types.FunctionType): + while hasattr(type_attr, "_torchdynamo_inline"): + type_attr = type_attr._torchdynamo_inline # type: ignore[union-attr] + source = AttrSource(source, "_torchdynamo_inline") if source else None + # Function on the type MRO + not in instance dict → bound method. + var_source = None + if can_use_mro_source: + var_source = self.get_source_by_walking_mro(tx, name) + return variables.UserMethodVariable( + type_attr, self, source_fn=var_source, source=source + ) + # Check for a Python-level __get__ (non-data descriptor with traceable __get__). + get_fn = inspect.getattr_static(type(type_attr), "__get__", None) + if isinstance(get_fn, types.FunctionType): + return self.invoke_descriptor_get(tx, name, type_attr, source) + + # C-level non-data descriptors / opaque callables — defer to runtime. + # MethodDescriptorType: e.g. list.append (PyMethodDef) + # WrapperDescriptorType: e.g. list.__add__ (slot wrappers) + # MethodWrapperType: e.g. [].__add__ (bound slot wrappers) + # + # Exception: if the descriptor has a registered polyfill, return the + # polyfill as a bound method so Dynamo can trace through it. + if ( + isinstance( + type_attr, + ( + types.MethodDescriptorType, + types.WrapperDescriptorType, + types.MethodWrapperType, + ), + ) + or torch._C._dynamo.utils.is_instancemethod(type_attr) # type: ignore[attr-defined] + or is_cython_function(type_attr) + ): + from .. import trace_rules + + if trace_rules.is_polyfilled_callable(type_attr): # type: ignore[arg-type] + from .functions import PolyfilledFunctionVariable + + polyfill_handlers = PolyfilledFunctionVariable._get_polyfill_handlers() + wrapped: Any = polyfill_handlers.get(type_attr) # type: ignore[arg-type] + if wrapped is not None: + traceable_fn = wrapped.__torch_dynamo_polyfill__ + return variables.UserMethodVariable(traceable_fn, self) + return variables.GetAttrVariable(self, name, type(type_attr), source=source) + + # Plain class variable (or MethodType, C-level non-data descriptor + # without __get__, etc.). + if can_use_mro_source: + source = self.get_source_by_walking_mro(tx, name) + elif not source and self.cls_source is not None: + source = AttrSource(self.cls_source, name) + return VariableTracker.build(tx, type_attr, source) + + def invoke_descriptor_get( + self, + tx: "InstructionTranslator", + name: str, + descriptor: object, + source: Source | None, + ) -> VariableTracker: + """Trace a descriptor's __get__(instance, owner) call.""" + descriptor_source = None + descriptor_get_source = None + if self.cls_source: + descriptor_source = self.get_source_by_walking_mro(tx, name) + descriptor_get_source = AttrSource(TypeSource(descriptor_source), "__get__") + descriptor_var = VariableTracker.build(tx, descriptor, descriptor_source) + else: + descriptor_var = UserDefinedObjectVariable(descriptor) + + owner_var = UserDefinedClassVariable(type(self.value)) + return variables.UserMethodVariable( + descriptor.__get__.__func__, # type: ignore[union-attr] + descriptor_var, + source=descriptor_get_source, + ).call_function(tx, [self, owner_var], {}) + + def maybe_wrap_nn_module_source_for_instance( + self, + tx: "InstructionTranslator", + name: str, + source: Source | None, + ) -> Source | None: + """Wrap source for nn.Module instance dict attribute access if needed.""" + if ( + source + and isinstance(self, variables.UnspecializedNNModuleVariable) + and (not tx.output.export or torch._dynamo.config.install_free_tensors) + ): + if name in ("_buffers", "_parameters"): + assert self.source is not None + source = UnspecializedParamBufferSource(self.source, name) + source = self._wrap_source(source) + return source + + def call_obj_hasattr( + self, tx: "InstructionTranslator", name: str + ) -> "ConstantVariable": + if self.source: + install_guard( + self.source.make_guard( + functools.partial(GuardBuilder.HASATTR, attr=name) + ) + ) + + try: + var_vt = self.var_getattr(tx, name) + return VariableTracker.build( + tx, not isinstance(var_vt, variables.DeletedVariable) + ) + except ObservedAttributeError: + handle_observed_exception(tx) + return variables.ConstantVariable.create(False) + + def is_python_hashable(self) -> bool: + raise_on_overridden_hash(self.value, self) + if self._base_vt is not None: + return self._base_vt.is_python_hashable() + return True + + def get_python_hash(self) -> int: + if self._base_vt is not None: + return self._base_vt.get_python_hash() + return hash(self.value) + + def is_python_equal(self, other: object) -> bool: + if ( + isinstance(other, VariableTracker) + and self.is_python_constant() + and other.is_python_constant() + ): + return self.as_python_constant() == other.as_python_constant() + # id check + if not isinstance(other, UserDefinedVariable): + return False + return self.value is other.value + + def call_tree_map_branch( + self, + tx: "InstructionTranslator", + tree_map_fn: "variables.functions.UserFunctionVariable", + map_fn: "VariableTracker", + rest: "collections.abc.Sequence[VariableTracker]", + tree_map_kwargs: "dict[str, VariableTracker]", + ) -> "VariableTracker": + """Emulate tree_map behavior for user-defined objects. + + In pytree, a type is a leaf if it is NOT in SUPPORTED_NODES. + User-defined objects (that are not registered with register_pytree_node) + are always treated as leaves. This works for both torch.utils._pytree + and optree implementations. + """ + # Determine which tree_map implementation is being used + tree_map_module = getattr(getattr(tree_map_fn, "fn", None), "__module__", "") + is_optree = tree_map_module.startswith("optree") + + if is_optree: + # Check optree's registry - need to handle namespaces + # In optree, types can be registered globally (type in registry) + # or with a namespace ((namespace, type) in registry) + try: + import optree + from optree.registry import _NODETYPE_REGISTRY + + # Check if registered globally + # Namedtuples and structseqs are implicitly pytree nodes + is_registered = ( + self.value_type in _NODETYPE_REGISTRY + or optree.is_namedtuple_class(self.value_type) + or optree.is_structseq_class(self.value_type) + ) + + # Also check if registered with a namespace that's being used + if not is_registered: + namespace_var = tree_map_kwargs.get("namespace") + if namespace_var is not None: + try: + namespace = namespace_var.as_python_constant() + # Check for namespaced registration + is_registered = ( + namespace, + self.value_type, + ) in _NODETYPE_REGISTRY + except NotImplementedError: + # Can't determine namespace at compile time, fall back + return self._tree_map_fallback( + tx, + tree_map_fn, + map_fn, + rest, + tree_map_kwargs, + ) + except ImportError: + # Can't import optree registry, fall back to tracing + import logging + + log = logging.getLogger(__name__) + log.warning( + "Failed to import optree.registry._NODETYPE_REGISTRY, " + "falling back to tracing for tree_map" + ) + return self._tree_map_fallback( + tx, + tree_map_fn, + map_fn, + rest, + tree_map_kwargs, + ) + else: + # Check pytorch's pytree registry + import torch.utils._pytree as pytree + + # Namedtuples and structseqs are implicitly pytree nodes + is_registered = ( + self.value_type in pytree.SUPPORTED_NODES + or pytree.is_namedtuple_class(self.value_type) + or pytree.is_structseq_class(self.value_type) + ) + + # If not registered, it's a leaf and we should apply the map_fn directly + if not is_registered: + return map_fn.call_function(tx, [self, *rest], {}) + + # The type is registered in pytree - we need to fall back to tracing + # the actual tree_map implementation since we don't have the flattening + # logic implemented here + return self._tree_map_fallback( + tx, + tree_map_fn, + map_fn, + rest, + tree_map_kwargs, + ) + + def call_tree_map_with_path_branch( + self, + tx: "InstructionTranslator", + tree_map_fn: "variables.functions.UserFunctionVariable", + map_fn: "VariableTracker", + rest: "collections.abc.Sequence[VariableTracker]", + tree_map_kwargs: "dict[str, VariableTracker]", + keypath: "tuple[Any, ...]", + ) -> "VariableTracker": + """Emulate tree_map_with_path behavior for user-defined objects. + + Same logic as call_tree_map_branch but passes keypath to the map function. + """ + tree_map_module = tree_map_fn.get_module() + is_optree = tree_map_module.startswith("optree") + + if is_optree: + try: + import optree + from optree.registry import _NODETYPE_REGISTRY + + is_registered = ( + self.value_type in _NODETYPE_REGISTRY + or optree.is_namedtuple_class(self.value_type) + or optree.is_structseq_class(self.value_type) + ) + + if not is_registered: + namespace_var = tree_map_kwargs.get("namespace") + if namespace_var is not None: + try: + namespace = namespace_var.as_python_constant() + is_registered = ( + namespace, + self.value_type, + ) in _NODETYPE_REGISTRY + except NotImplementedError: + return self._tree_map_with_path_fallback( + tx, + tree_map_fn, + map_fn, + rest, + tree_map_kwargs, + keypath, + ) + except ImportError: + return self._tree_map_with_path_fallback( + tx, + tree_map_fn, + map_fn, + rest, + tree_map_kwargs, + keypath, + ) + else: + import torch.utils._pytree as pytree + + is_registered = ( + self.value_type in pytree.SUPPORTED_NODES + or pytree.is_namedtuple_class(self.value_type) + or pytree.is_structseq_class(self.value_type) + ) + + if not is_registered: + keypath_var = variables.TupleVariable( + [VariableTracker.build(tx, k) for k in keypath] + ) + return map_fn.call_function(tx, [keypath_var, self, *rest], {}) + + return self._tree_map_with_path_fallback( + tx, + tree_map_fn, + map_fn, + rest, + tree_map_kwargs, + keypath, + ) + + +class FrozenDataClassVariable(UserDefinedObjectVariable): + """Frozen dataclass variable for as_proxy/as_python_constant/hashability. + + Construction is handled by the generic polyfill path (tracing through + the auto-generated __init__). Field values are retrieved dynamically + via var_getattr using InstructionTranslator.current_tx(). + """ + + def _get_field_vt(self, field_name: str) -> VariableTracker: + from torch._dynamo.symbolic_convert import InstructionTranslator + + tx = InstructionTranslator.current_tx() + return self.var_getattr(tx, field_name) + + def as_python_constant(self) -> object: + from dataclasses import fields + + import torch.utils._pytree as pytree + + if not istype( + self.value, (pytree.TreeSpec, pytree.LeafSpec, pytree.ConstantNode) + ): + raise NotImplementedError( + "currently can't reconstruct arbitrary frozen dataclass instances" + ) + + if istype(self.value, pytree.LeafSpec): + return pytree.treespec_leaf() + + args: list[object] = [] + kwargs: dict[str, object] = {} + for field in fields(self.value): # type: ignore[arg-type] + if field.init: + data = self._get_field_vt(field.name).as_python_constant() + if getattr(field, "kw_only", False): + kwargs[field.name] = data + else: + args.append(data) + + return self.python_type()(*args, **kwargs) + + def as_proxy(self) -> object: + from dataclasses import fields + + args: list[object] = [] + kwargs: dict[str, object] = {} + for field in fields(self.value): # type: ignore[arg-type] + proxy = self._get_field_vt(field.name).as_proxy() + if hasattr(field, "kw_only") and field.kw_only: + kwargs[field.name] = proxy + else: + args.append(proxy) + + return self.python_type()(*args, **kwargs) + + def reconstruct(self, codegen: "PyCodegen") -> None: + if self.source is not None: + codegen(self.source) + return + codegen.append_output( + codegen.create_load_const_unchecked(self.as_python_constant()) + ) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.value_type.__name__})" + + def is_python_hashable(self) -> Literal[True]: + return True + + def get_python_hash(self) -> int: + from dataclasses import fields as dc_fields + + return hash( + tuple( + self._get_field_vt(f.name).get_python_hash() + for f in dc_fields(self.value) # type: ignore[arg-type] + ) + ) + + def is_python_equal(self, other: object) -> bool: + if not isinstance(other, FrozenDataClassVariable): + return False + if self.python_type() is not other.python_type(): + return False + from dataclasses import fields as dc_fields + + return all( + self._get_field_vt(f.name).is_python_equal(other._get_field_vt(f.name)) + for f in dc_fields(self.value) # type: ignore[arg-type] + ) + + +class SourcelessGraphModuleVariable(UserDefinedObjectVariable): + def __init__( + self, + value: object, + **kwargs: Any, + ) -> None: + super().__init__(value, **kwargs) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + fn_variable = VariableTracker.build(tx, self.value.forward.__func__) # type: ignore[attr-defined] + args = [self] + args + return tx.inline_user_function_return( + fn_variable, + args, + kwargs, + ) + + +class UserDefinedExceptionObjectVariable(UserDefinedObjectVariable): + def __init__(self, value: object, **kwargs: Any) -> None: + super().__init__(value, **kwargs) + init_args = kwargs.get("init_args", []) + self.exc_vt = variables.ExceptionVariable(self.value_type, init_args) + + @property + def fn(self) -> Callable[..., object]: + return self.value_type + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if ( + name == "__init__" + and (method := self._maybe_get_baseclass_method(name)) + and inspect.ismethoddescriptor(method) + and len(kwargs) == 0 + ): + return variables.ConstantVariable.create(None) + elif ( + name == "__setattr__" + and len(args) == 2 + and args[0].is_constant_match( + "__cause__", "__context__", "__suppress_context__", "__traceback__" + ) + ): + self.exc_vt.call_setattr(tx, args[0], args[1]) + elif name == "with_traceback": + return self.exc_vt.call_method(tx, name, args, kwargs) + return super().call_method(tx, name, args, kwargs) + + def var_getattr(self, tx: "InstructionTranslator", name: str): + if name in ( + "args", + "__cause__", + "__context__", + "__suppress_context__", + "__traceback__", + ): + return self.exc_vt.var_getattr(tx, name) + return super().var_getattr(tx, name) + + @property + def __context__(self) -> "ConstantVariable": + # type: ignore[return-value] + return self.exc_vt.__context__ + + @property + def args(self) -> list[VariableTracker]: + return self.exc_vt.args + + def set_context(self, context: "variables.ExceptionVariable") -> None: + return self.exc_vt.set_context(context) + + @property + def exc_type(self) -> type[BaseException]: + return self.exc_vt.exc_type + + @property + def python_stack(self) -> traceback.StackSummary | None: + return self.exc_vt.python_stack + + def debug_repr(self) -> str: + return self.exc_vt.debug_repr() + + @python_stack.setter + def python_stack(self, value: traceback.StackSummary) -> None: + self.exc_vt.python_stack = value + + +class InspectVariable(UserDefinedObjectVariable): + """Handles inspect.Signature and inspect.Parameter objects. + + Short-circuits property accesses to avoid tracing property getters, + redirecting them to the underlying private attributes directly. + """ + + _PROPERTY_REDIRECTS: dict[type, dict[str, str]] = { + inspect.Signature: {"parameters": "_parameters"}, + inspect.Parameter: {"kind": "_kind", "name": "_name"}, + } + + @staticmethod + def is_matching_object(obj: object) -> bool: + return type(obj) in InspectVariable._PROPERTY_REDIRECTS + + @staticmethod + def is_matching_class(obj: object) -> bool: + return obj in InspectVariable._PROPERTY_REDIRECTS + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + redirects = self._PROPERTY_REDIRECTS.get(type(self.value), {}) + if name in redirects: + return super().var_getattr(tx, redirects[name]) + return super().var_getattr(tx, name) + + +class KeyedJaggedTensorVariable(UserDefinedObjectVariable): + @staticmethod + def is_matching_object(obj: object) -> bool: + mod = sys.modules.get("torchrec.sparse.jagged_tensor") + return mod is not None and type(obj) is mod.KeyedJaggedTensor + + def __init__(self, value: object, **kwargs: Any) -> None: + from torchrec.sparse.jagged_tensor import ( # type: ignore[import-not-found] + KeyedJaggedTensor, + ) + + assert type(value) is KeyedJaggedTensor + super().__init__(value, **kwargs) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + if ( + torch._dynamo.config.force_unspec_int_unbacked_size_like_on_torchrec_kjt + and self.source is not None + and name in ("_length_per_key", "_offset_per_key") + ): + with TracingContext.patch(force_unspec_int_unbacked_size_like=True): + return super().var_getattr(tx, name) + return super().var_getattr(tx, name) + + +_CONSTANT_BASE_TYPES = (int, float, str) + +_constant_base_methods: dict[type, set[Any]] = { + t: {m for m in t.__dict__.values() if callable(m)} for t in _CONSTANT_BASE_TYPES +} + + +class UserDefinedConstantVariable(UserDefinedObjectVariable): + """ + Represents user-defined objects that subclass immutable constant types + (int, float, str). + + Uses a ConstantVariable as _base_vt for the underlying constant value. + """ + + def __init__(self, value: Any, **kwargs: Any) -> None: + from .constant import ConstantVariable + + super().__init__(value, **kwargs) + for base in type(value).__mro__: + if base in _CONSTANT_BASE_TYPES: + self._base_vt = ConstantVariable.create(base(value)) + self._base_methods = _constant_base_methods[base] + break + assert self._base_vt is not None + + def as_python_constant(self) -> Any: + return self.value + + def as_proxy(self) -> object: + assert self._base_vt is not None + return self._base_vt.as_proxy() + + +class IntWrapperVariable(UserDefinedObjectVariable): + # Dummy class to check if the object is an IntWrapper, and turn it into a + # symint + @staticmethod + def is_matching_object(obj: object) -> bool: + mod = sys.modules.get("torch.export.dynamic_shapes") + return mod is not None and type(obj) is mod._IntWrapper + + +class RemovableHandleVariable(VariableTracker): + REMOVED = -1 + + def __init__( + self, + mutation_type: MutationType | None = None, + # index of the registration in the side_effects owned register_hook/handle list, used during removal. + idx: int | None = None, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.mutation_type = mutation_type + self.idx = idx + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "remove": + if self.idx != self.REMOVED: + assert self.idx is not None + tx.output.side_effects.remove_hook(self.idx) + self.idx = self.REMOVED + return variables.ConstantVariable.create(None) + return super().call_method(tx, name, args, kwargs) + + def reconstruct(self, codegen: "PyCodegen") -> None: + if self.idx == self.REMOVED: + # Hook has already been removed, return a dummy handle + codegen.add_push_null( + lambda: codegen.load_import_from( + "torch._dynamo.utils", "invalid_removeable_handle" + ) + ) + codegen.extend_output(create_call_function(0, False)) + return + # unreachable due to codegen.add_cache() when the hook is installed + super().reconstruct(codegen) + + def python_type(self) -> type[object]: + return RemovableHandleClass + + +class UserDefinedDictVariable(UserDefinedObjectVariable): + """ + Represents user defined objects that are subclasses of dict/OrderedDict. + + Internally, it uses a ConstDictVariable to represent the dict part of the + variable tracker. For everything else, it falls back to + UserDefinedObjectVariable. + """ + + def __init__( + self, + value: object, + dict_vt: ConstDictVariable | None = None, + **kwargs: Any, + ) -> None: + super().__init__(value, **kwargs) + if dict_vt is None: + assert self.source is None, ( + "dict_vt must be constructed by builder.py when source is present" + ) + self._base_vt = ConstDictVariable( + {}, + user_cls=( + collections.OrderedDict + if isinstance(value, collections.OrderedDict) + else dict + ), + mutation_type=ValueMutationNew(), + ) + else: + self._base_vt = dict_vt + self._base_methods = dict_methods + assert self._base_vt is not None + + def sq_length(self, tx: "InstructionTranslator") -> VariableTracker: + # Dict implements __len__ via mp_length (mapping protocol), not + # sq_length (sequence protocol). Redirect so generic_len works. + return self.mp_length(tx) + + def mp_subscript_impl( + self, + tx: "InstructionTranslator", + key: VariableTracker, + ) -> VariableTracker: + # dict_subscript: https://github.com/python/cpython/blob/62a6e898e01/Objects/dictobject.c#L3673-L3706 + # TODO(follow-up): add test for unhashable/invalid key type, Counter missing key + method = self._maybe_get_baseclass_method("__getitem__") + if method in self._base_methods: + assert self._base_vt is not None + try: + return self._base_vt.mp_subscript_impl(tx, key) + except ObservedKeyError: + if issubclass( + self.python_type(), dict + ) and self._maybe_get_baseclass_method("__missing__"): + return self.call_method(tx, "__missing__", [key], {}) + else: + raise + return super().mp_subscript_impl(tx, key) + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + # Dict subclasses can override __missing__ to provide fallback + # behavior instead of raising a KeyError. This is used, for example, + # by collections.Counter. + if ( + name == "__getitem__" + and self._maybe_get_baseclass_method("__getitem__") in self._base_methods + and self._maybe_get_baseclass_method("__missing__") + ): + assert self._base_vt is not None + try: + return self._base_vt.call_method(tx, name, args, kwargs) + except ObservedKeyError: + handle_observed_exception(tx) + return self.call_method(tx, "__missing__", args, kwargs) + return super().call_method(tx, name, args, kwargs) + + +class UserDefinedSetVariable(UserDefinedObjectVariable): + """ + Represents user defined objects that are subclasses of set. + + Internally, it uses a SetVariable to represent the set part of the + variable tracker. For everything else, it falls back to + UserDefinedObjectVariable. + """ + + def __init__( + self, value: object, set_vt: SetVariable | None = None, **kwargs: Any + ) -> None: + from .builder import SourcelessBuilder + + tx = kwargs.pop("tx", None) + super().__init__(value, **kwargs) + + python_type = set if isinstance(value, set) else frozenset + self._base_methods = set_methods if python_type is set else frozenset_methods + + if set_vt is None: + assert self.source is None, ( + "set_vt must be constructed by builder.py when source is present" + ) + if python_type is set: + # set is initialized later + self._base_vt = variables.SetVariable( + set(), + mutation_type=ValueMutationNew(), + ) + else: + init_args = kwargs.get("init_args", {}) + if tx is None: + tx = torch._dynamo.symbolic_convert.InstructionTranslator.current_tx() + self._base_vt = SourcelessBuilder.create(tx, python_type).call_function( # type: ignore[assignment] + tx, init_args, {} + ) + else: + self._base_vt = set_vt + assert self._base_vt is not None + + def as_python_constant(self) -> object: + assert self._base_vt is not None + return self._base_vt.as_python_constant() + + @property + def set_items(self) -> set[Any]: + assert self._base_vt is not None + return self._base_vt.set_items # pyrefly: ignore[missing-attribute] + + @property + def items(self) -> dict[HashableTracker, VariableTracker]: + assert self._base_vt is not None + return self._base_vt.items # pyrefly: ignore[missing-attribute] + + def is_python_equal(self, other: object) -> bool: + assert self._base_vt is not None + return isinstance( + other, UserDefinedSetVariable + ) and self._base_vt.is_python_equal(other._base_vt) + + +class UserDefinedListVariable(UserDefinedObjectVariable): + """ + Represents user defined objects that are subclasses of lists. + + Internally, it uses a ListVariable to represent the list part of the + variable tracker. For everything else, it falls back to + UserDefinedObjectVariable. + """ + + def __init__( + self, value: object, list_vt: Union["ListVariable", None] = None, **kwargs: Any + ) -> None: + from .lists import ListVariable + + super().__init__(value, **kwargs) + if list_vt is None: + assert self.source is None, ( + "list_vt must be constructed by builder.py when source is present" + ) + self._base_vt = ListVariable([], mutation_type=ValueMutationNew()) + else: + self._base_vt = list_vt + self._base_methods = list_methods + assert self._base_vt is not None + + +class UserDefinedTupleVariable(UserDefinedObjectVariable): + """ + Represents user defined objects that are subclasses of tuple. + + Internally, it uses a TupleVariable to represent the tuple part of the + variable tracker. For everything else, it falls back to + UserDefinedObjectVariable. + + NamedTupleVariable and StructSequenceVariable are subclasses that handle + namedtuples and structseqs (torch.return_types.*) respectively. + """ + + _nonvar_fields = { + "tuple_cls", + *UserDefinedObjectVariable._nonvar_fields, + } + + @staticmethod + def get_vt_cls(cls: type) -> type["UserDefinedTupleVariable"]: + if is_structseq_class(cls): + return StructSequenceVariable + return NamedTupleVariable + + def __init__(self, value, tuple_vt=None, init_args=None, **kwargs): # type: ignore[all] + from .lists import TupleVariable + + tx = kwargs.pop("tx", None) + super().__init__(value, init_args=init_args, **kwargs) + if tuple_vt is None: + assert self.source is None, ( + "tuple_vt must be constructed by builder.py when source is present" + ) + assert init_args, "init_args must be provided when tuple_vt is None" + # Emulate `tuple.__new__` + # https://github.com/python/cpython/blob/3.11/Objects/tupleobject.c#L697-L710 + # + # TODO this duplicates the logic in `BuiltinVariable(tuple)` + if tx is None: + from torch._dynamo.symbolic_convert import InstructionTranslator + + tx = InstructionTranslator.current_tx() + elems = init_args[0].force_unpack_var_sequence(tx) + self._base_vt = TupleVariable(elems, mutation_type=ValueMutationNew()) + else: + self._base_vt = tuple_vt + self.tuple_cls = type(value) + self._base_methods = tuple_methods + assert self._base_vt is not None + + @property + def items(self) -> list[VariableTracker]: + assert self._base_vt is not None + return self._base_vt.items # type: ignore[return-value] + + def call_method( + self, + tx: "InstructionTranslator", + name: str, + args: list[VariableTracker], + kwargs: dict[str, VariableTracker], + ) -> VariableTracker: + if name == "__eq__": + if len(args) != 1 or kwargs: + raise ValueError("Improper arguments for method.") + return VariableTracker.build(tx, self.is_python_equal(args[0])) + elif name == "__ne__": + if len(args) != 1 or kwargs: + raise ValueError("Improper arguments for method.") + return VariableTracker.build(tx, not self.is_python_equal(args[0])) + return super().call_method(tx, name, args, kwargs) + + def reconstruct(self, codegen: "PyCodegen") -> None: + # Sourceless namedtuples/structseqs (e.g. tensor subclass metadata from + # SourcelessBuilder) aren't in id_to_variable so codegen_save_tempvars + # never processes them. When they appear in return values, codegen falls + # through to call_reconstruct. This is the same pattern as other + # sourceless containers (ConstDictVariable, TupleVariable, etc.). + # UserDefinedDictVariable doesn't need this because it's never created + # sourceless — it only comes from VariableBuilder which always has a + # source. + assert self.source is None + create_fn = self.get_construct_fn() + codegen.add_push_null( + lambda: codegen.append_output( + codegen.create_load_const_unchecked(create_fn) + ) + ) + codegen(self._base_vt) + codegen.extend_output(create_call_function(1, False)) + + def get_construct_fn(self) -> Callable[..., Any]: + raise NotImplementedError + + def _validate_rest_for_tree_map( + self, rest: "collections.abc.Sequence[VariableTracker]" + ) -> list["UserDefinedTupleVariable"] | None: + """Validate that rest args are compatible for tree_map fast-path.""" + others: list[UserDefinedTupleVariable] = [] + n = len(self.items) + for candidate in rest: + if ( + not isinstance(candidate, UserDefinedTupleVariable) + or len(candidate.items) != n + or candidate.tuple_cls is not self.tuple_cls + ): + return None + others.append(candidate) + return others + + def _make_tree_map_result( + self, new_items: list[VariableTracker] + ) -> "UserDefinedTupleVariable": + from .lists import TupleVariable + + tuple_vt = TupleVariable(new_items, mutation_type=ValueMutationNew()) + return type(self)( + self.value, + tuple_vt=tuple_vt, + mutation_type=ValueMutationNew(), + ) + + def _is_pytree_node(self) -> bool: + from torch.utils._pytree import is_namedtuple_class + + return is_namedtuple_class(self.tuple_cls) or is_structseq_class(self.tuple_cls) + + def call_tree_map_branch( + self, + tx: "InstructionTranslator", + tree_map_fn: "variables.functions.UserFunctionVariable", + map_fn: "VariableTracker", + rest: "collections.abc.Sequence[VariableTracker]", + tree_map_kwargs: "dict[str, VariableTracker]", + ) -> "VariableTracker": + if not self._is_pytree_node(): + return super().call_tree_map_branch( + tx, tree_map_fn, map_fn, rest, tree_map_kwargs + ) + others = self._validate_rest_for_tree_map(rest) + if others is None: + return self._tree_map_fallback( + tx, tree_map_fn, map_fn, rest, tree_map_kwargs + ) + + new_items: list[VariableTracker] = [] + for idx, item in enumerate(self.items): + sibling_leaves = [o.items[idx] for o in others] + new_items.append( + item.call_tree_map( + tx, tree_map_fn, map_fn, sibling_leaves, tree_map_kwargs + ) + ) + + return self._make_tree_map_result(new_items) + + def call_tree_map_with_path_branch( + self, + tx: "InstructionTranslator", + tree_map_fn: "variables.functions.UserFunctionVariable", + map_fn: "VariableTracker", + rest: "collections.abc.Sequence[VariableTracker]", + tree_map_kwargs: "dict[str, VariableTracker]", + keypath: "tuple[Any, ...]", + ) -> "VariableTracker": + if not self._is_pytree_node(): + return super().call_tree_map_with_path_branch( + tx, tree_map_fn, map_fn, rest, tree_map_kwargs, keypath + ) + others = self._validate_rest_for_tree_map(rest) + if others is None: + return self._tree_map_with_path_fallback( + tx, tree_map_fn, map_fn, rest, tree_map_kwargs, keypath + ) + + fields = namedtuple_fields(self.tuple_cls) + new_items: list[VariableTracker] = [] + for idx, item in enumerate(self.items): + sibling_leaves = [o.items[idx] for o in others] + child_keypath = keypath + (GetAttrKey(fields[idx]),) + new_items.append( + item.call_tree_map_with_path( + tx, + tree_map_fn, + map_fn, + sibling_leaves, + tree_map_kwargs, + child_keypath, + ) + ) + + return self._make_tree_map_result(new_items) + + def is_python_equal(self, other: object) -> bool: + assert self._base_vt is not None + other = other._base_vt if isinstance(other, UserDefinedTupleVariable) else other + return self._base_vt.is_python_equal(other) + + +class NamedTupleVariable(UserDefinedTupleVariable): + """Represents Python namedtuples (created via collections.namedtuple). + + Namedtuples use _tuplegetter descriptors for field access and + Type(*args) / Type._make(iterable) for construction. + """ + + def resolve_data_descriptor( + self, + tx: "InstructionTranslator", + name: str, + type_attr: object, + source: Source | None, + ) -> VariableTracker: + if isinstance(type_attr, _collections._tuplegetter): + # namedtuple fields are _tuplegetter descriptors implemented in C. + # We emulate _tuplegetter.__get__ by indexing into the tracked + # tuple items, because self.value may not hold actual runtime values. + _, (idx, _) = type_attr.__reduce__() + return self.items[idx] # type: ignore[union-attr] + return super().resolve_data_descriptor(tx, name, type_attr, source) + + def get_construct_fn(self) -> Callable[..., Any]: + return self.tuple_cls._make # type: ignore[attr-defined] + + def as_python_constant(self) -> Any: + items = [x.as_python_constant() for x in self.items] + return self.tuple_cls(*items) # type: ignore[arg-type] + + def as_proxy(self) -> Any: + items = [x.as_proxy() for x in self.items] + return self.tuple_cls(*items) # type: ignore[arg-type] + + +class StructSequenceVariable(UserDefinedTupleVariable): + """Represents C-implemented PyStructSequence types (torch.return_types.*). + + Structseqs use Type(iterable) calling convention and reject tuple.__new__. + """ + + def resolve_data_descriptor( + self, + tx: "InstructionTranslator", + name: str, + type_attr: object, + source: Source | None, + ) -> VariableTracker: + if isinstance(type_attr, types.MemberDescriptorType): + # Structseq fields are member_descriptor objects. We emulate + # field access by looking up the field name in _fields and + # indexing into the tracked tuple items. + fields = namedtuple_fields(self.tuple_cls) + if name in fields: + return self.items[fields.index(name)] + return super().resolve_data_descriptor(tx, name, type_attr, source) + + def get_construct_fn(self) -> Callable[..., Any]: + return self.tuple_cls + + def as_python_constant(self) -> Any: + items = [x.as_python_constant() for x in self.items] + return self.tuple_cls(items) + + def as_proxy(self) -> Any: + items = [x.as_proxy() for x in self.items] + return self.tuple_cls(items) + + +class MutableMappingVariable(UserDefinedObjectVariable): + def __init__(self, value: object, **kwargs: Any) -> None: + super().__init__(value, **kwargs) + + def method_setattr_standard( + self, + tx: "InstructionTranslator", + name: VariableTracker, + value: VariableTracker, + directly_update_dict: bool = False, + ) -> VariableTracker: + """Override to handle property setters on MutableMapping subclasses. + + This is needed because property.__set__ is a slot wrapper (C function), + not a Python function, so the base class's try_get_descritor_and_setter_py_func + returns None for properties. But property.fset IS a Python function we can trace. + + Without this, property setters on newly created MutableMapping objects fail + when accessing nested objects (which haven't been initialized yet on the + example value). By tracing the fset, we capture the setter logic in the graph + instead of running it on uninitialized example objects. + + TODO(compiler): This fix is scoped to MutableMapping only because tracing + property setters on ALL UserDefinedObjectVariable can cause failures when + the fset calls untraceable C++ functions (e.g., pybind functions). Ideally, + this should be extended to all user-defined classes with a graceful fallback + when tracing the fset hits an untraceable function. + See: https://github.com/pytorch/pytorch/issues/172000 + """ + if isinstance(name, variables.ConstantVariable) and isinstance(name.value, str): + name_str = name.value + descriptor = inspect.getattr_static(type(self.value), name_str, None) + if isinstance(descriptor, property) and descriptor.fset is not None: + fset_source = None + if self.cls_source: + desc_source = self.get_source_by_walking_mro(tx, name_str) + fset_source = AttrSource(desc_source, "fset") + fset_vt = VariableTracker.build(tx, descriptor.fset, fset_source) + return fset_vt.call_function(tx, [self, value], {}) + + return super().method_setattr_standard(tx, name, value, directly_update_dict) + + def var_getattr(self, tx: "InstructionTranslator", name: str) -> VariableTracker: + # A common pattern in the init code of MutableMapping objects is to + # update the __dict__ attribute. The parent class + # (UserDefinedObjectVariable) implements __dict__ lookups using a VT + # (self.dict_vt) that uses the side effects table as source of truth. + if name == "get" and type(self.value).get in ( # type: ignore[attr-defined] + collections.abc.Mapping.get, + dict.get, + ): + return variables.UserMethodVariable(polyfills.mapping_get, self) + else: + return super().var_getattr(tx, name) + + def mp_length(self, tx: "InstructionTranslator") -> VariableTracker: + if self._maybe_get_baseclass_method("__len__") in dict_methods: + return VariableTracker.build(tx, len(self.value)) # type: ignore[bad-argument-type] + return super().mp_length(tx) + + +class RandomVariable(UserDefinedObjectVariable): + pass diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_environment.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_environment.py new file mode 100644 index 0000000000000000000000000000000000000000..67e5784dfc1a5104f92b4d2144189976fcce43e8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_environment.py @@ -0,0 +1,9 @@ +from typing import Literal + + +def is_fbcode() -> Literal[False]: + return False + + +def is_prod() -> Literal[False]: + return False diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7a46a50c04b3ffe6aac78ca2978cdde293f2a17b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/__init__.py @@ -0,0 +1,189 @@ +# mypy: allow-untyped-defs +import copy +import dataclasses +import functools +import io +import json +import logging +import os +import re +import sys +import types +import warnings +import weakref +import zipfile +from collections import OrderedDict +from contextlib import contextmanager +from functools import lru_cache + +from typing import Any, Optional, TYPE_CHECKING, Union +from collections.abc import Callable +from unittest.mock import patch + +import torch +import torch.fx +import torch.utils._pytree as pytree + +from torch._dispatch.python import enable_python_dispatcher +from torch._guards import compile_context +from torch._utils_internal import log_export_usage +from torch.export._tree_utils import reorder_kwargs +from torch.export.graph_signature import ( + ArgumentSpec, + ConstantArgument, + ExportGraphSignature, + InputKind, + InputSpec, + OutputKind, + OutputSpec, + SymIntArgument, + SymBoolArgument, + SymFloatArgument, + TensorArgument, +) +from torch.fx import traceback as fx_traceback +from torch.fx._compatibility import compatibility +from torch.fx.experimental.proxy_tensor import make_fx +from torch.fx.graph import _PyTreeCodeGen, _PyTreeInfo + +from .wrappers import _wrap_submodules +from .utils import _materialize_cpp_cia_ops +from . import config + +if TYPE_CHECKING: + from torch._C._aoti import AOTIModelContainerRunner + +log = logging.getLogger(__name__) + +@dataclasses.dataclass +class ExportDynamoConfig: + """ + Manage Export-specific configurations of Dynamo. + """ + allow_rnn: bool = True + + +# We only want to print this once to avoid flooding logs in workflows where aot_compile_warning +# is called multiple times. +@lru_cache +def aot_compile_warning(): + + log.warning("+============================+") + log.warning("| !!! WARNING !!! |") + log.warning("+============================+") + log.warning( + "torch._export.aot_compile()/torch._export.aot_load() is being deprecated, please switch to " + "directly calling torch._inductor.aoti_compile_and_package(torch.export.export())/" + "torch._inductor.aoti_load_package() instead.") + + +def aot_compile( + f: Callable, + args: tuple[Any, ...], + kwargs: dict[str, Any] | None = None, + *, + dynamic_shapes: dict[str, Any] | None = None, + options: dict[str, Any] | None = None, + remove_runtime_assertions: bool = False, + disable_constraint_solver: bool = False, + same_signature: bool = True, +) -> list[Any] | str: + """ + Note: this function is not stable yet + + Traces either an nn.Module's forward function or just a callable with PyTorch + operations inside, generates executable cpp code from the program, and returns + the path to the generated shared library + + Args: + f: the `nn.Module` or callable to trace. + + args: example positional inputs. + + kwargs: optional example keyword inputs. + + dynamic_shapes: Should either be: + 1) a dict from argument names of ``f`` to their dynamic shape specifications, + 2) a tuple that specifies dynamic shape specifications for each input in original order. + If you are specifying dynamism on keyword args, you will need to pass them in the order that + is defined in the original function signature. + + The dynamic shape of a tensor argument can be specified as either + (1) a dict from dynamic dimension indices to :func:`Dim` types, where it is + not required to include static dimension indices in this dict, but when they are, + they should be mapped to None; or (2) a tuple / list of :func:`Dim` types or None, + where the :func:`Dim` types correspond to dynamic dimensions, and static dimensions + are denoted by None. Arguments that are dicts or tuples / lists of tensors are + recursively specified by using mappings or sequences of contained specifications. + + options: A dictionary of options to control inductor + + disable_constraint_solver: Whether the dim constraint solver must be disabled. + + Returns: + Path to the generated shared library + """ + from torch.export._trace import _export_to_torch_ir + from torch._inductor.decomposition import select_decomp_table + from torch._inductor import config as inductor_config + + aot_compile_warning() + + if inductor_config.is_predispatch: + gm = torch.export._trace._export(f, args, kwargs, dynamic_shapes, pre_dispatch=True).module() + else: + # We want to export to Torch IR here to utilize the pre_grad passes in + # inductor, which run on Torch IR. + with torch._export.config.patch(use_new_tracer_experimental=True): + gm = _export_to_torch_ir( + f, + args, + kwargs, + dynamic_shapes, + disable_constraint_solver=disable_constraint_solver, + same_signature=same_signature, + # Disabling this flag, because instead we can rely on the mapping + # dynamo_flat_name_to_original_fqn which is coming from Dynamo. + restore_fqn=False, + ) + + with torch.no_grad(): + so_path = torch._inductor.aot_compile(gm, args, kwargs, options=options) # type: ignore[arg-type] + + if not isinstance(so_path, (str, list)): + raise AssertionError(f"expected str or list, got {type(so_path)}") + return so_path + +def aot_load(so_path: str, device: str) -> Callable: + """ + Loads a shared library generated by aot_compile and returns a callable + + Args: + so_path: Path to the shared library + + Returns: + A callable + """ + aot_compile_warning() + + if device == "cpu": + runner: AOTIModelContainerRunner = torch._C._aoti.AOTIModelContainerRunnerCpu(so_path, 1) + elif device == "cuda" or device.startswith("cuda:"): + runner = torch._C._aoti.AOTIModelContainerRunnerCuda(so_path, 1, device) + elif device == "xpu" or device.startswith("xpu:"): + runner = torch._C._aoti.AOTIModelContainerRunnerXpu(so_path, 1, device) + elif device == "mps" or device.startswith("mps:"): + runner = torch._C._aoti.AOTIModelContainerRunnerMps(so_path, 1) + else: + raise RuntimeError("Unsupported device " + device) + + def optimized(*args, **kwargs): + call_spec = runner.get_call_spec() + in_spec = pytree.treespec_loads(call_spec[0]) + out_spec = pytree.treespec_loads(call_spec[1]) + flat_inputs = pytree.tree_flatten((args, reorder_kwargs(kwargs, in_spec)))[0] + flat_inputs = [x for x in flat_inputs if isinstance(x, torch.Tensor)] + flat_outputs = runner.run(flat_inputs) + return pytree.tree_unflatten(flat_outputs, out_spec) + + return optimized diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/config.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/config.py new file mode 100644 index 0000000000000000000000000000000000000000..ec3963eaa34acfbbe4f21354b0a84ab54cccfd71 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/config.py @@ -0,0 +1,45 @@ +""" +Configuration module for torch.export.export. + +This module contains various configuration flags and settings that control torch.export's +behavior, including: +- Runtime behavior flags +- Debugging and development options +""" + +import sys +from typing import Any, TYPE_CHECKING + +from torch._environment import is_fbcode +from torch.utils._config_module import install_config_module + + +# this flag controls whether we use new functional tracer. It +# should be True in the long term. +use_new_tracer_experimental = True + +# this flag is used to control whether we want to instrument +# fake tensor creation to track potential leaks. It is off +# by default, but user can turn it on to debug leaks. +detect_non_strict_fake_tensor_leaks = False + +# error on potentially pre-dispatch/non-strict tracing limitation +# this type of error usually happens when we encounter an op +# that we don't know how to proxy, resulting in untracked fake tensors +error_on_lifted_constant_tensors = True + +# enable auto_functionalized_v2 in export +# We turn this off in fbcode due to downstream users not +# being ready to handle auto_functionalized_v2. +enable_auto_functionalized_v2_for_export = not is_fbcode() + +use_legacy_dynamo_graph_capture = True + + +if TYPE_CHECKING: + from torch.utils._config_typing import * # noqa: F401, F403 + + def _make_closure_patcher(**changes: Any) -> Any: ... + + +install_config_module(sys.modules[__name__]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/converter.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/converter.py new file mode 100644 index 0000000000000000000000000000000000000000..e69ca58d3ae7e787bf91a222a05e27f013d93fc0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/converter.py @@ -0,0 +1,1622 @@ +# mypy: allow-untyped-defs +import builtins +import logging +import operator +import typing +import warnings +from collections.abc import Callable, Sequence +from contextlib import contextmanager +from typing import Any + +import torch +import torch.export._trace +from torch import _C +from torch._export.passes.replace_quantized_ops_with_standard_ops_pass import ( + replace_quantized_ops_with_standard_ops, +) +from torch.export.dynamic_shapes import _tree_map_with_path, Dim +from torch.export.exported_program import ExportedProgram +from torch.export.graph_signature import ( + ConstantArgument, + CustomObjArgument, + InputKind, + InputSpec, + OutputKind, + OutputSpec, + TensorArgument, +) +from torch.fx import subgraph_rewriter + + +log = logging.getLogger(__name__) + + +def _get_param_count_list(method_graph, args_params): + param_count_list = [] + for input_, arg_params_ in zip(method_graph.inputs(), args_params): + if "PackedParams" in str(input_.type()): + in_vars, _ = torch.jit._flatten(arg_params_) + param_count_list.append(len(in_vars)) + else: + param_count_list.append(arg_params_ is not None) + + return param_count_list + + +def _trace_and_get_graph_from_model(model, args): + # A basic sanity check: make sure the state_dict keys are the same + # before and after running the model. Fail fast! + orig_state_dict_keys = torch.jit._unique_state_dict(model).keys() + + # Disable Autocast cache because it replaces kernel's weight and bias + # by (undesired) constants. + # No perf impact for when there are reused weights since https://github.com/pytorch/pytorch/pull/85665 + prev_autocast_cache_enabled = torch.is_autocast_cache_enabled() + torch.set_autocast_cache_enabled(False) + trace_graph, torch_out, _inputs_states = torch.jit._get_trace_graph( + model, + args, + strict=False, + _force_outplace=False, + _return_inputs_states=True, + ) + torch.set_autocast_cache_enabled(prev_autocast_cache_enabled) + + if orig_state_dict_keys != torch.jit._unique_state_dict(model).keys(): + raise RuntimeError( + "state_dict changed after running the tracer; " + "something weird is happening in your model!" + ) + + return trace_graph, torch_out + + +def _create_jit_graph( + model: torch.nn.Module | torch.jit.ScriptFunction, args: Sequence[Any] +) -> tuple[torch.Graph, list["_C.IValue"], Any, torch.ScriptModule | None]: + if isinstance(model, (torch.jit.ScriptFunction, torch.jit.ScriptModule)): + flattened_args = tuple(torch.jit._flatten(tuple(args))[0]) + torch_out = None + + if isinstance(model, torch.jit.ScriptModule): + try: + graph = model.forward.graph # type: ignore[attr-defined] + except AttributeError as e: + raise RuntimeError("'forward' method must be a script method") from e + _C._jit_pass_onnx_function_substitution(graph) + freezed_module = _C._freeze_module( + typing.cast(_C.ScriptModule, model._c), preserveParameters=True + ) + module, params = _C._jit_onnx_list_model_parameters(freezed_module) + method_graph = module._get_method("forward").graph + args_params = tuple(args) + tuple(params) + param_count_list = _get_param_count_list(method_graph, args_params) + in_vars, _ = torch.jit._flatten(args_params) + graph = _C._propagate_and_assign_input_shapes( + method_graph, tuple(in_vars), param_count_list, False, False + ) + return graph, params, torch_out, module + + # torch.jit.ScriptFunction + params = [] + graph = model.graph + _C._jit_pass_onnx_function_substitution(graph) + param_count_list = _get_param_count_list(graph, args) + graph = _C._propagate_and_assign_input_shapes( + graph, flattened_args, param_count_list, False, False + ) + return graph, params, torch_out, None + + graph, torch_out = _trace_and_get_graph_from_model(model, args) + _C._jit_pass_onnx_lint(graph) + state_dict = torch.jit._unique_state_dict(model) + params = list(state_dict.values()) + graph_inputs = list(graph.inputs()) + user_input_num = len(graph_inputs) - len(state_dict) + param_names = list(state_dict.keys()) + for i, inp in enumerate(graph_inputs): + if i >= user_input_num: + inp.setDebugName(param_names[i - user_input_num]) + _C._jit_pass_onnx_function_substitution(graph) + return graph, params, torch_out, None + + +def list_add(a, b): + return a + b + + +def list_append(container, element): + return container + [element] + + +def execute_subgraph_from_prim_loop( + subgraph, iter_idx, len_loop_local_arguments, *args, **kwargs +): + """ + subgraph: GraphModule from sub-block. + iter_idx: The index of interaction. + len_loop_local_arguments: The number of loop local arguments in args. + """ + + # Loop local variables. TS graph create those as inputs because their values + # are updated inside the loop. + loop_local_args = args[:len_loop_local_arguments] + # Global variables that are not passed in as inputs to the loop sub-blocks + # but are directly used. Most of time, their values are not updated, but + # the only exception is when there are some operations that perform inplace + # updates. + global_args = args[len_loop_local_arguments:] + return subgraph(*global_args, iter_idx, *loop_local_args, **kwargs) + + +def inplace_optimize_sym_size_div(gm: torch.fx.GraphModule): + def pattern(im, dim, scale): + sym_size_int = torch.ops.aten.sym_size.int(im, dim) + scalar_tensor = torch.ops.aten.scalar_tensor(sym_size_int) + div_scalar_mode = torch.ops.aten.div.Scalar_mode( + scalar_tensor, scale, rounding_mode="trunc" + ) + int_tensor = torch.ops.aten.Int.Tensor(div_scalar_mode) + return int_tensor + + def replacement(im, dim, scale): + sym_size_int = torch.ops.aten.sym_size.int(im, dim) + return sym_size_int // scale + + subgraph_rewriter.replace_pattern(gm, pattern, replacement) + + +def is_valid_for_codegen(name): + if len(name) == 0: + raise RuntimeError("Empty argument name for codegen") + if name[0].isdigit(): + return False + return True + + +def normalize_name(name: str, prefix: str = "rename") -> str: + name = name.replace(".", "_") + if is_valid_for_codegen(name): + return name + return f"{prefix}_{name}" + + +def ir_name_to_func_name(name: str) -> str: + """prim::If -> convert_prim_If""" + name_list = name.split("::") + return "convert_" + "_".join(name_list) + + +def get_node_as_placeholder_or_get_attr(fx_graph, name, is_top_level_graph): + if is_top_level_graph: + return fx_graph.get_attr(name) + return fx_graph.placeholder(name) + + +_TORCH_DTYPE_TO_ENUM = { + torch.uint8: 0, + torch.int8: 1, + torch.int16: 2, + torch.int32: 3, + torch.int64: 4, + torch.float16: 5, + torch.float32: 6, + torch.float64: 7, + torch.complex32: 8, + torch.complex64: 9, + torch.complex128: 10, + torch.bool: 11, + torch.qint8: 12, + torch.quint8: 13, + torch.bfloat16: 15, +} + +_TORCH_ENUM_TO_DTYPE = {value: key for key, value in _TORCH_DTYPE_TO_ENUM.items()} + + +def get_dtype_as_int(tensor): + """ + prim::dtype has the signature "Tensor a) -> int", where it gets the dtype of + the tensor and returns the integer corresponding to this dtype based on the + enum in ScalarType.h + """ + dtype = tensor.dtype + if dtype not in _TORCH_DTYPE_TO_ENUM: + raise RuntimeError(f"Unsupported dtype {dtype}") + return _TORCH_DTYPE_TO_ENUM[dtype] + + +# Those operators will be automatically populated to a instance method +# of TS2FXGraphConverter with name convert__(). +# Please check __init__ for method population implementations. +kind_to_standard_operators: dict[str, Callable[..., Any]] = { + "prim::max": builtins.max, + "prim::min": builtins.min, + "prim::TupleIndex": operator.getitem, + "aten::__is__": operator.is_, + "aten::__isnot__": operator.is_not, + "aten::__not__": operator.not_, + "aten::__contains__": operator.contains, + "prim::dtype": get_dtype_as_int, + "aten::len": len, + # Mapping from specialized op to its symbolic counterpart. + # They currently do not have any other overrides. + "aten::numel": torch.ops.aten.sym_numel, + "aten::size": torch.ops.aten.sym_size, + "aten::storage_offset": torch.ops.aten.sym_storage_offset, + "aten::stride": torch.ops.aten.sym_stride, +} + + +def get_ir_value_parent_name_and_attr_name(node): + irv_parent_name, irv_name = node.input().debugName(), node.output().debugName() + attr_name = node.s("name") + return irv_name, irv_parent_name, attr_name + + +def construct_fqn(ir, ref_map, name_map): + name_list = [] + while ir in ref_map: + name_list.append(name_map[ir]) + ir = ref_map[ir] + return ".".join(reversed(name_list)) + + +def get_block_to_lifted_attrs( + graph: torch._C.Graph, +) -> tuple[dict[torch._C.Block, set[str]], dict[str, str]]: + """ + Perform two passes to get a mapping of blocks to a set of FQNs of its lifted attributes. + When a graph has control flow, the graph will be divided into multiple blocks. We want to convert + each block to a graph which will be passed into torch.cond. A restriction for torch.cond is that model + parameters/buffers are expected to be lifted as inputs to the subgraphs. Before converting the model, + we will run this pass which will: + 1. Figure out which params/buffers are used within blocks through tracing the GetAttr calls. + 2. Process the graph bottom up to find the lifted attributes of each block by taking the union + of the attributes used in the current block, and the lifted attributes of all its child blocks. + + Returns: + A mapping of blocks to a set of FQNs of its lifted attributes, and a + mapping of node names to the FQNs of its lifted attributes. + """ + + # A map from a block to its expected to be lifted arguments. + blocks_to_lifted_attrs: dict[torch._C.Block, set[str]] = {} + + # Reference map stores the input (i.e., src) and output (i.e., dest) IR of a + # GetAttr node. By traversing this reference map, we can figure out the + # full IR aliasing pass and figure out the FQN of an attribute. + # E.g., %2 = GetAttr(linear)[%1] --> node_to_parent_map["%2"] = "%1" + node_to_parent_map: dict[str, str] = {} + + # Used for reconstructing the FQN of an attribute based on the reference map. + # In nutshell, for each GetAttr call, GetAttr(input IR, attribute name) -> output IR + # This name map stores which attribute name is called for a src IR --> dest IR action. + # E.g., %2 = GetAttr(linear)[%1] --> node_to_attr_name["%2"] = "linear" + node_to_attr_name: dict[str, str] = {} + + def _dfs_get_attr_dependency(entry): + """ + First DFS path to construct reference map and name map. + """ + for node in entry.nodes(): + if node.kind() == "prim::GetAttr": + ( + irv_name, + irv_parent_name, + attr_name, + ) = get_ir_value_parent_name_and_attr_name(node) + node_to_parent_map[irv_name] = irv_parent_name + node_to_attr_name[irv_name] = attr_name + for block in node.blocks(): + _dfs_get_attr_dependency(block) + + def _map_blocks_to_lifted_attrs(entry): + """ + Walk the graph in a bottom-up fashion to build the expected to be + lifted arguments for each block. + """ + arguments: set[str] = set() + for node in entry.nodes(): + for block in node.blocks(): + # Recursively build. + arguments = arguments.union(_map_blocks_to_lifted_attrs(block)) + if node.kind() == "prim::GetAttr": + irv_name = node.output().debugName() + # Skip for intermediate GetAttr, which will anyway not result a FQN. + # E.g., node_to_parent_name: {"%3": "%2", "%2": "%1"} + # node_to_attr_name: {"%3": "weight", "%2": "linear", "%1": "self"} + # There is only one FQN %3-->%2-->%1: self.linear.weight + # %2-->%1 is not a FQN: self.linear + if irv_name not in set(node_to_parent_map.values()): + arguments.add( + construct_fqn(irv_name, node_to_parent_map, node_to_attr_name) + ) + if not isinstance(entry, torch._C.Graph): # Skip the top level. + blocks_to_lifted_attrs[entry] = arguments + return arguments + + _dfs_get_attr_dependency(graph) + _map_blocks_to_lifted_attrs(graph) + + return blocks_to_lifted_attrs, node_to_attr_name + + +def get_attribute_fqn_from_ts_node( + name_to_attribute_fqn: dict[str, str], node: torch._C.Node +) -> str: + def get_attr(name: str): + if name in name_to_attribute_fqn: + return name_to_attribute_fqn[name] + else: + raise ValueError(f"Attribute {name} not found") + + if node.kind() == "prim::SetAttr": + input_name = next(node.inputs()).debugName() + elif node.kind() == "prim::GetAttr": + input_name = node.input().debugName() + else: + raise RuntimeError( + f"Unexpected node kind when getting attribute fqn. node: {node} " + ) + + attr_name = node.s("name") + root_attr_name = get_attr(input_name) + attr_fqn = f"{root_attr_name}.{attr_name}" if root_attr_name else attr_name + + return attr_fqn + + +def get_op_overload(node: torch._C.Node): + schema_str = node.schema() + if schema_str == "(no schema)": + raise AssertionError(f"got empty schema for {node}") + schema: torch._C.FunctionSchema = torch._C.parse_schema(schema_str) + ns, op_name = str(schema.name).split("::") + override = schema.overload_name + + try: + op_overload_mod = getattr(torch.ops, ns) + op_overload_packet = getattr(op_overload_mod, op_name) + if override: + op_overload = getattr(op_overload_packet, override) + else: + op_overload = op_overload_packet.default + except Exception as e: + raise RuntimeError( + f"Unable to find operator {node.kind()} with schema {node.schema()}" + ) from e + + return op_overload + + +class TS2FXGraphConverter: + def __init__( + self, + ts_graph: torch._C.Graph | torch._C.Block, + name_to_param: dict[str, torch.Tensor], + name_to_buffer: dict[str, torch.Tensor], + blocks_to_lifted_attrs: dict[torch._C.Block, set[str]], + name_to_non_tensor_attribute: dict[str, Any], + name_to_constant: dict[str, Any], + name_to_attribute_fqn: dict[str, str], + ): + self.ts_graph = ts_graph + # Mapping of parameter FQN to actual parameter value + self.name_to_param = name_to_param + # Mapping of buffer FQN to actual buffer value + self.name_to_buffer = name_to_buffer + + self.fx_graph: torch.fx.Graph = torch.fx.Graph() + self.input_specs: list[InputSpec] = [] + self.output_specs: list[OutputSpec] = [] + + # Mapping of TS node name to converted FX node + self.name_to_node: dict[ + str, torch.fx.Node | list[torch.fx.Node] | dict[Any, torch.fx.Node] + ] = {} + # Mapping of TS node name to constant value (int, str, TorchBind obj, + # tensor constants ...) + self.name_to_constant: dict[str, Any] = name_to_constant + + # Mapping from torchscript node output name to attribute fully qualified name + self.name_to_attribute_fqn: dict[str, str] = name_to_attribute_fqn + + # Mapping from fully qualified name to real values or a fx graph node + # During convert, this represents the current value of a non-tensor attribute + # One use case is: + # def forward(self, x): + # c1 = self.count + # self.count += 1 + # c2 = self.count + # return x + c1 + c2 + self.name_to_non_tensor_attribute_node: dict[str, Any] = {} + + # Mapping from fully qualified name to initial real values inputs + # We separate it from self.name_to_non_tensor_attribute_node since + # we need initial real value input when we construct fx.GraphModule + self.name_to_non_tensor_attribute: dict[str, Any] = name_to_non_tensor_attribute + + self.subgraphs: dict[str, torch.fx.GraphModule] = {} + + # Mapping of block to list of attributes that need to be lifted for each + # block + self.blocks_to_lifted_attrs = blocks_to_lifted_attrs + + # Populate methods for the standard operators. + for k in kind_to_standard_operators: + handler_func_name = ir_name_to_func_name(k) + # Create an indirect function call: + # convert__ --> lambda node: _convert_standard_operator(node) + setattr( + self, + handler_func_name, + lambda node: self._convert_standard_operators(node), + ) + + # This stores a list of return results that do not appear in the original TS + # graph's outputs. The reason we maintain this is because some operations in the sub-block + # might have inplace updates to the variable defined in the parent fx graph. After + # the execution of that sub-block, the variable defined in the parent fx graph also + # needs to be updated. + self.name_update_from_subblock_to_parent: set[str] = set() + + def _is_get_attr_node(self, fqn): + return ( + fqn in self.name_to_buffer + or fqn in self.name_to_param + or ( + fqn in self.name_to_constant + and isinstance(self.name_to_constant[fqn], torch.ScriptObject) + ) + ) + + def _convert_block_to_subgraph(self, node: torch._C.Node, arguments: list[str]): + subgraph_nodes, subgraph_converters = [], [] + for block in node.blocks(): + subgraph_converter = TS2FXGraphConverter( + block, + self.name_to_param, + self.name_to_buffer, + self.blocks_to_lifted_attrs, + {}, + self.name_to_constant, + self.name_to_attribute_fqn, + ) + + for block_arg in arguments: + normalized_block_arg_name = normalize_name(block_arg) + placeholder_node = subgraph_converter.fx_graph.placeholder( + normalized_block_arg_name + ) + subgraph_converter.name_to_node[block_arg] = placeholder_node + + subgraph = subgraph_converter.convert() + subgraph_name = self.add_subgraph(subgraph) + subgraph_nodes.append(self.fx_graph.get_attr(subgraph_name)) + subgraph_converters.append(subgraph_converter) + return subgraph_nodes, subgraph_converters + + def _identify_inputs_as_arguments(self, entry): + """ + Identify inputs from the innermost sub-block. This is needed + for nested sub-blocks when the input is hidden in the nested sub-block. + E.g., example IR of input is hidden in the nested sub-block. + Graph[x.1] + %1 = ... + Block[] + Block[x.1] + %2 = x.1 ... + """ + arguments: set[str] = set() + for block in entry.blocks(): + for block_node in block.nodes(): + for block_node_in in block_node.inputs(): + if ( + block_node_in.debugName() in self.name_to_node + and block_node_in.debugName() not in self.name_to_attribute_fqn + ): + arguments.add(block_node_in.debugName()) + arguments = arguments.union( + self._identify_inputs_as_arguments(block_node) + ) + return arguments + + def is_top_level_graph(self): + return isinstance(self.ts_graph, torch._C.Graph) + + def add_subgraph(self, subgraph) -> str: + name = f"subgraph_{len(self.subgraphs)}" + self.subgraphs[name] = subgraph + return name + + def get_args_kwargs(self, node: torch._C.Node, schema): + args = [] + kwargs = {} + for input, schema_arg in zip(node.inputs(), schema.arguments): + if schema_arg.kwarg_only: + kwargs[schema_arg.name] = self.get_fx_value_by_ir_value(input) + else: + args.append(self.get_fx_value_by_ir_value(input)) + + return tuple(args), kwargs + + def get_fx_value_by_ir_value(self, value: torch._C.Value): + value_name = value.debugName() + + if value_name in self.name_to_node: + input_node = self.name_to_node[value_name] + return input_node + elif value_name in self.name_to_constant: + if isinstance(self.name_to_constant[value_name], torch.ScriptObject): + return self.fx_graph.get_attr(value_name) + return self.name_to_constant[value_name] + elif value_name in self.name_to_attribute_fqn: + return self.get_fx_value_by_fqn(self.name_to_attribute_fqn[value_name]) + else: + raise ValueError(f"Input {value_name} not found") + + def get_fx_value_by_fqn(self, name): + if name in self.name_to_node: + fx_node = self.name_to_node[name] + elif name in self.name_to_constant: + fx_node = self.name_to_constant[name] + elif name in self.name_to_non_tensor_attribute_node: + fx_node = self.name_to_non_tensor_attribute_node[name] + elif name in self.name_to_non_tensor_attribute: + fx_node = self.name_to_non_tensor_attribute[name] + else: + raise ValueError(f"Attribute {name} not found") + return fx_node + + def convert(self) -> torch.fx.GraphModule: + self.convert_graph_inputs() + + for node in self.ts_graph.nodes(): + self.convert_node(node) + + self.convert_graph_outputs() + + # Pass parameter and buffer to the root for lookup. + gm = torch.fx.GraphModule( + { + **self.subgraphs, + **self.name_to_param, + **self.name_to_buffer, + **self.name_to_non_tensor_attribute, + **self.name_to_constant, + }, + self.fx_graph, + ) + + inplace_optimize_sym_size_div(gm) + + gm.graph.lint() + + return gm + + def convert_graph_inputs(self): + for graph_input in self.ts_graph.inputs(): + name = graph_input.debugName() + + if name in self.name_to_param: + normalized_name = normalize_name(name) + self.input_specs.append( + InputSpec( + InputKind.PARAMETER, + arg=TensorArgument(name=normalized_name), + target=name, + ) + ) + fx_node = get_node_as_placeholder_or_get_attr( + self.fx_graph, name, self.is_top_level_graph() + ) + elif name in self.name_to_buffer: + normalized_name = normalize_name(name) + self.input_specs.append( + InputSpec( + InputKind.BUFFER, + arg=TensorArgument(name=normalized_name), + target=name, + persistent=True, + ) + ) + fx_node = get_node_as_placeholder_or_get_attr( + self.fx_graph, name, self.is_top_level_graph() + ) + elif name in self.name_to_constant: + if not isinstance(self.name_to_constant[name], torch.ScriptObject): + raise AssertionError( + f"Input conversion only handles ScriptObject, got {type(self.name_to_constant[name])}" + ) + normalized_name = normalize_name(name) + self.input_specs.append( + InputSpec( + InputKind.CUSTOM_OBJ, + arg=CustomObjArgument( + name=normalized_name, class_fqn=normalized_name + ), + target=name, + persistent=False, + ) + ) + fx_node = get_node_as_placeholder_or_get_attr( + self.fx_graph, name, self.is_top_level_graph() + ) + elif isinstance(graph_input.type(), torch.ClassType): + # Directly skip inputs that are ScriptObject but not used in the graph. + continue + else: + normalized_name = normalize_name(name, prefix="input") + self.input_specs.append( + InputSpec( + InputKind.USER_INPUT, + arg=TensorArgument(name=normalized_name), + target=name, + ) + ) + fx_node = self.fx_graph.placeholder(normalized_name) + + self.name_to_node[name] = fx_node + + def convert_aten_Float(self, node: torch._C.Node): + def to_float_tensor(t): + return t.to(dtype=torch.float).item() + + inp_list = [self.get_fx_value_by_ir_value(inp) for inp in node.inputs()] # noqa: C416 + fx_node = self.fx_graph.call_function( + to_float_tensor, + tuple(inp_list), + ) + self.name_to_node[node.output().debugName()] = fx_node + + def convert_aten_tensor(self, node: torch._C.Node): + """aten::tensor creates a constant tensor ad-hoc --> GetAttr""" + args, kwargs = self.get_args_kwargs(node, torch.ops.aten.tensor.default._schema) + + for k in kwargs: + if k == "requires_grad": + kwargs[k] = bool(kwargs[k]) # 0 -> False, 1 -> True + + to_tensor = ( + torch.tensor + if all(isinstance(a, int) for a in args) + else torch._refs.tensor + ) + + def target(*args, **kwargs): + if "dtype" in kwargs and kwargs["dtype"] is not None: + kwargs["dtype"] = _TORCH_ENUM_TO_DTYPE[kwargs["dtype"]] + return to_tensor(*args, **kwargs) + + # def to_dynamic_tensor(*args, **kwargs): + # if "dtype" in kwargs and kwargs["dtype"] is not None: + # kwargs["dtype"] = _TORCH_ENUM_TO_DTYPE[kwargs["dtype"]] + # return torch._refs.tensor(*args, **kwargs) + + output_name = node.output().debugName() + fx_node = self.fx_graph.call_function(target, args, kwargs) + self.name_to_node[output_name] = fx_node + + def convert_aten_append(self, node: torch._C.Node): + # special handle python list append: "aten::append.t(t[](a!) self, t(c -> *) el) -> t[](a!)" + + # inplace append to the list!! This is kinda crazy, as we are inplace mutating the list + # This makes the converter "non-functional", and the result depends on the order of the nodes being converter + # In a sense, the converter now becomes an stateful interpreter + warnings.warn( + "Converting aten::append.t, which is a inplace mutation of the list. " + "This makes the converter non-functional: the result depends on the order of the append nodes being converter!", + stacklevel=2, + ) + + args = tuple(self.get_fx_value_by_ir_value(inp) for inp in node.inputs()) + fx_node = self.fx_graph.call_function(list_append, args) + self.name_to_node[node.output().debugName()] = fx_node + + # inplace mutate arg[0], which is the python list + self.name_to_node[node.inputsAt(0).debugName()] = fx_node + + # Variables that need to be updated to parent module. + if not self.is_top_level_graph() and args[0].op == "placeholder": + self.name_update_from_subblock_to_parent.add(node.inputsAt(0).debugName()) + + def convert_prim_Constant(self, node: torch._C.Node): + name = node.output().debugName() + + value: Any = None + if node.hasAttribute("value"): + constant_kind = node.kindOf("value") + if constant_kind == "i": + value = node.i("value") + elif constant_kind == "f": + value = node.f("value") + elif constant_kind == "s": + value = node.s("value") + elif constant_kind == "t": + alias_name = ( + f"lifted_tensor_{name}" # Follow naming convention from EP tracing. + ) + fx_node = self.fx_graph.get_attr(alias_name) + self.name_to_node[name] = fx_node + name, value = alias_name, node.t("value") + elif constant_kind == "ival": + value = node.ival("value") + else: + raise ValueError(f"Unsupported constant type: {node.kindOf('value')}") + else: + value = None + + self.name_to_constant[name] = value + + def convert_prim_CallMethod(self, node: torch._C.Node): + inp_list = [self.get_fx_value_by_ir_value(inp) for inp in node.inputs()] # noqa: C416 + fx_node = self.fx_graph.call_method( + node.s("name"), + tuple(inp_list), + ) + self.name_to_node[node.output().debugName()] = fx_node + + def convert_prim_device(self, node: torch._C.Node): + input_type = node.input().type() + if input_type.isSubtypeOf(torch._C.TensorType.get()): + device = input_type.device() # type: ignore[attr-defined] + output_name = node.output().debugName() + self.name_to_constant[output_name] = device + else: + raise ValueError(f"Unsupported JitType ({input_type}) when get device") + + def convert_prim_GetAttr(self, node: torch._C.Node): + # Build fully qualified name + attr_fqn = get_attribute_fqn_from_ts_node(self.name_to_attribute_fqn, node) + output_name = node.output().debugName() + self.name_to_attribute_fqn[output_name] = attr_fqn + + if self.is_top_level_graph(): + if self._is_get_attr_node(attr_fqn): + # We insert a get_attr node due to two reasons. + # First, ts graph does not lift tensor constants as input nodes. So + # tensor constants may be ignored by in convert_graph_inputs(). + # Second, attr_fqn may have been written to via SetAttr. Two + # GetAttr may give different values. + self.name_to_node[output_name] = self.fx_graph.get_attr(attr_fqn) + else: + if attr_fqn not in self.name_to_non_tensor_attribute_node: + self.name_to_non_tensor_attribute_node[attr_fqn] = ( + self.name_to_non_tensor_attribute[attr_fqn] + ) + self.name_to_node[output_name] = self.name_to_non_tensor_attribute_node[ + attr_fqn + ] + else: + # Special support for if blocks which do not allow SetAttr TorchScript + # node and get_attr FX Graph Node. + if self._is_get_attr_node(attr_fqn): + self.name_to_node[output_name] = self.name_to_node[attr_fqn] + + def convert_prim_SetAttr(self, node: torch._C.Node): + attr_fqn = get_attribute_fqn_from_ts_node(self.name_to_attribute_fqn, node) + attr_value = tuple(node.inputs())[1] + ts_graph_tensor_input = self.get_fx_value_by_ir_value(attr_value) + if self._is_get_attr_node(attr_fqn): + fx_attr_node = self.fx_graph.get_attr(attr_fqn) + self.fx_graph.call_function( + torch.Tensor.copy_, (fx_attr_node, ts_graph_tensor_input) + ) + else: + self.name_to_non_tensor_attribute_node[attr_fqn] = ts_graph_tensor_input + + def convert_call_function_op(self, node: torch._C.Node): + target = get_op_overload(node) + + args, kwargs = self.get_args_kwargs(node, target._schema) + + fx_node = self.fx_graph.call_function(target, args, kwargs) + + # TODO: convert sourceRange() into stack_trace + # fx_node.meta["stack_trace"] = node.sourceRange() + + if node.outputsSize() == 1: + output_name = node.output().debugName() + self.name_to_node[output_name] = fx_node + else: + for i, outp in enumerate(node.outputs()): + output_name = outp.debugName() + next_fx_node = self.fx_graph.call_function( + operator.getitem, (fx_node, i) + ) + self.name_to_node[output_name] = next_fx_node + + def convert_prim_TupleConstruct(self, node: torch._C.Node): + self._convert_prim_iterator(node) + + def convert_prim_ListConstruct(self, node: torch._C.Node): + self._convert_prim_iterator(node) + + def _convert_prim_iterator(self, node: torch._C.Node): + output_list = [self.get_fx_value_by_ir_value(inp) for inp in node.inputs()] + + output_name = node.output().debugName() + self.name_to_node[output_name] = output_list + + def convert_prim_DictConstruct(self, node: torch._C.Node): + output_dict = {} + k, v = None, None + for i, inp in enumerate(node.inputs()): + # We assume key value are stored in pair in the DictConstruct. + # The first element is the key and the following is the value. + if i % 2 == 0: + k = self.get_fx_value_by_ir_value(inp) + else: + v = self.get_fx_value_by_ir_value(inp) + if k is None or v is None: + raise AssertionError("DictConstruct has an empty key value pair.") + output_dict[k] = v + k, v = None, None + + if k is not None or v is not None: + raise AssertionError( + "DictConstruct has an odd number of elements (violating our assumption)." + ) + + output_name = node.output().debugName() + self.name_to_node[output_name] = output_dict + + def convert_prim_ListUnpack(self, node: torch._C.Node): + self._convert_prim_unpack_iterator(node) + + def convert_prim_TupleUnpack(self, node: torch._C.Node): + self._convert_prim_unpack_iterator(node) + + def _convert_prim_unpack_iterator(self, node: torch._C.Node): + # Single input and multiple outputs for unpacking. + for i, outp in enumerate(node.outputs()): + outp_name = outp.debugName() + inp = self.get_fx_value_by_ir_value(node.input()) + fx_node = self.fx_graph.call_function(operator.getitem, (inp, i)) + self.name_to_node[outp_name] = fx_node + + def convert_aten_Int(self, node: torch._C.Node): + # converts aten::Int as aten._to_copy + aten::_local_scalar_dense + target = torch.ops.aten._to_copy.default + args = tuple(self.get_fx_value_by_ir_value(input) for input in node.inputs()) + to_copy_node = self.fx_graph.call_function(target, args, {"dtype": torch.int32}) + + fx_node = self.fx_graph.call_function( + torch.ops.aten._local_scalar_dense.default, (to_copy_node,) + ) + + # TODO: convert sourceRange() into stack_trace + # fx_node.meta["stack_trace"] = node.sourceRange() + + output_name = node.output().debugName() + self.name_to_node[output_name] = fx_node + + def convert_prim_NumToTensor(self, node: torch._C.Node): + # Converts prim::NumToTensor as aten.scalar_tensor. + # prim::NumToTensor IRs are currently triggered by: + # .size() https://github.com/pytorch/pytorch/blob/main/torch/csrc/jit/frontend/tracer.cpp#L950 + # .numel() https://github.com/pytorch/pytorch/blob/main/torch/csrc/jit/frontend/tracer.cpp#L971 + # For both of those APIs, torch.jit.trace implicitly sets the output tensor type + # to be LongTensor. + target = torch.ops.aten.scalar_tensor + args = tuple(self.get_fx_value_by_ir_value(input) for input in node.inputs()) + + fx_node = self.fx_graph.call_function(target, args, {"dtype": torch.long}) + output_name = node.output().debugName() + self.name_to_node[output_name] = fx_node + + def convert_prim_CreateObject(self, node: torch._C.Node): + output_name = node.output().debugName() + self.name_to_attribute_fqn[output_name] = "" + + def convert_aten__convolution(self, node: torch._C.Node): + # converts aten::_convolution as aten.convolution, since aten::_convolution + # doesn't have a meta function + target = torch.ops.aten.convolution.default + args, kwargs = self.get_args_kwargs(node, target._schema) + + fx_node = self.fx_graph.call_function(target, args, kwargs) + + output_name = node.output().debugName() + self.name_to_node[output_name] = fx_node + + def convert_aten_div(self, node: torch._C.Node): + target = get_op_overload(node) + schema = target._schema + + args, kwargs = self.get_args_kwargs(node, schema) + + # converts aten::div.Tensor_mode(x, tensor_constant) + # as aten.div.Scalar_mode(x, tensor_constant.item()) + if schema.overload_name == "Tensor_mode": + arg1_name = args[1].name + if arg1_name in self.name_to_constant and isinstance( + self.name_to_constant[arg1_name], torch.Tensor + ): + tensor_constant = self.name_to_constant[arg1_name] + if tensor_constant.numel() == 1: + updated_args = list(args) + updated_args[1] = self.name_to_constant[arg1_name].item() + + fx_node = self.fx_graph.call_function( + torch.ops.aten.div.Scalar_mode, + tuple(updated_args), + kwargs, + ) + + # TODO: convert sourceRange() into stack_trace + # fx_node.meta["stack_trace"] = node.sourceRange() + + output_name = node.output().debugName() + self.name_to_node[output_name] = fx_node + return + + self.convert_call_function_op(node) + + def convert_aten___getitem__(self, node: torch._C.Node): + input_container, index = tuple( + self.get_fx_value_by_ir_value(input) for input in node.inputs() + ) + fx_node = self.fx_graph.call_function( + operator.getitem, (input_container, index) + ) + output_name = node.output().debugName() + self.name_to_node[output_name] = fx_node + + def convert_aten_to(self, node: torch._C.Node): + target = get_op_overload(node) + args, _kwargs = self.get_args_kwargs(node, target._schema) + + # special handle aten.to.dtype and aten.to.prim_dtype followed by inplace_mutation_op + # coz aten.to + inplace_mutation_op pattern would trigger + # "cannot mutate tensors with frozen storage" functionalization error. + # To work around the issue, we override the copy to be True, so that the output + # is for sure not an alias of input + if target is torch.ops.aten.to.dtype or target is torch.ops.aten.to.prim_dtype: + user_nodes = [use.user for use in node.output().uses()] + user_targets = [ + get_op_overload(user_node) + for user_node in user_nodes + if user_node.schema() != "(no schema)" + ] + has_mutable_target = any( + target._schema.is_mutable for target in user_targets + ) + + if has_mutable_target: + if len(args) < 4: + raise AssertionError(f"expected at least 4 args, got {len(args)}") + new_args = list(args) + new_args[3] = True # copy, override to True + fx_node = self.fx_graph.call_function( + torch.ops.aten.to.dtype, tuple(new_args) + ) + # temp hack to work around the issue https://github.com/pytorch/pytorch/issues/131679 + # When this issue is fixed, the clone node would be no longer needed + clone_node = self.fx_graph.call_function( + torch.ops.aten.clone.default, (fx_node,) + ) + output_name = node.output().debugName() + self.name_to_node[output_name] = clone_node + return + + self.convert_call_function_op(node) + + def convert_aten_add(self, node: torch._C.Node): + if node.schema() == "(no schema)": + if isinstance(node.inputsAt(0).type(), torch.ListType) and isinstance( + node.inputsAt(1).type(), torch.ListType + ): + target = torch.ops.aten.add.t + else: + raise RuntimeError(f"unable to determined the target for {node}") + else: + target = get_op_overload(node) + + if target is torch.ops.aten.add.t: + # special handle python list/tuple add: "aten::add.t(t[] a, t[] b) -> t[]" for + # RuntimeError: aten::add() Expected a value of type 'List[t]' for argument 'a' but instead found type 'immutable_list'. + args, _kwargs = self.get_args_kwargs(node, target._schema) + output_name = node.output().debugName() + self.name_to_node[output_name] = self.fx_graph.call_function(list_add, args) + else: + self.convert_call_function_op(node) + + def _check_prim_loop_support(self, node): + inputs = list(node.inputs()) + + # TODO: (1/N) stage. + if inputs[0].debugName() not in self.name_to_constant: + raise RuntimeError( + "prim::Loop currently cannot run with dynamic value of number of iterations." + ) + + # Make sure the condition is not updated in the subblock. + subblock = next(node.blocks()) + condition_output_name = next(subblock.outputs()).debugName() + for node in subblock.nodes(): + if ( + node.outputsSize() == 1 + and node.output().debugName() == condition_output_name + ): + raise RuntimeError( + "prim::Loop currently cannot run with dynamic value of condition." + ) + if node.outputsSize() >= 2: + for outp in node.outputs(): + if outp.debugName() == condition_output_name: + raise RuntimeError( + "prim::Loop currently cannot run with dynamic value of condition." + ) + + def convert_prim_Loop(self, node: torch._C.Node): + inputs = list(node.inputs()) + self._check_prim_loop_support(node) + + num_iterations = self.get_fx_value_by_ir_value(inputs[0]) + + # Find inputs. + loop_local_arguments = [inp.debugName() for inp in inputs[2:]] + + global_arguments = self._identify_inputs_as_arguments(node) + + # Lift parameters as inputs. + for block in node.blocks(): + global_arguments = global_arguments.union( + self.blocks_to_lifted_attrs[block] + ) + + global_arguments = list(global_arguments) + + subgraph_nodes, subgraph_converters = self._convert_block_to_subgraph( + node, global_arguments + ) + + if len(subgraph_nodes) != 1: + raise AssertionError(f"expected 1 subgraph node, got {len(subgraph_nodes)}") + subgraph_converter = subgraph_converters[0] + if not self.is_top_level_graph(): + self.name_update_from_subblock_to_parent = ( + self.name_update_from_subblock_to_parent.union( + subgraph_converter.name_update_from_subblock_to_parent + ) + ) + + fx_block_args = [ + self.get_fx_value_by_fqn(name) + for name in loop_local_arguments + global_arguments + ] + for iter_idx in range(num_iterations): + loop_node = self.fx_graph.call_function( + execute_subgraph_from_prim_loop, + # Check execute_node function for the expected arguments order. + ( + subgraph_nodes[0], + iter_idx, + len(loop_local_arguments), + *fx_block_args, + ), + {}, + ) + + # Update the value of loop local variables. + if node.outputsSize() >= 1: + for i, outp in enumerate(node.outputs()): + output_name = outp.debugName() + self.name_to_node[output_name] = self.fx_graph.call_function( + operator.getitem, + ( + loop_node, + i + 1, + ), # + 1 because the 0th element is the condition. + ) + fx_block_args[i] = self.name_to_node[output_name] + + # Update the value of global variables, whose values are modified inplace. + + for i, name in enumerate( + subgraph_converter.name_update_from_subblock_to_parent + ): + self.name_to_node[name] = self.fx_graph.call_function( + operator.getitem, + ( + loop_node, + i + node.outputsSize() + 1, + ), # + 1 because the 0th element is the condition. + ) + global_argument_index = global_arguments.index(name) + fx_block_args[i + node.outputsSize() + global_argument_index] = ( + self.name_to_node[name] + ) + + def _check_set_attr_in_if_block(self, if_node: torch._C.Node): + for block in if_node.blocks(): + for node in block.nodes(): + if node.kind() == "prim::SetAttr": + raise RuntimeError( + "During converting prim::If to torch.cond, found prim::SetAttr op" + " which is not supported yet. Please file an issue if you come " + "across this error." + ) + + def convert_prim_If(self, node: torch._C.Node): + self._check_set_attr_in_if_block(node) + + inputs = list(node.inputs()) + if len(inputs) != 1: + raise AssertionError(f"expected 1 input for prim::If, got {len(inputs)}") + predicate = self.get_fx_value_by_ir_value(inputs[0]) + + # Find inputs. + arguments = self._identify_inputs_as_arguments(node) + + # Lift parameters as inputs. + for block in node.blocks(): + arguments = arguments.union(self.blocks_to_lifted_attrs[block]) + + arguments = list(arguments) + subgraph_nodes, _ = self._convert_block_to_subgraph(node, arguments) + + if len(subgraph_nodes) != 2: + raise AssertionError( + f"expected 2 subgraph nodes, got {len(subgraph_nodes)}" + ) + + fx_block_args = [self.get_fx_value_by_fqn(name) for name in arguments] + + args = ( + predicate, + subgraph_nodes[0], + subgraph_nodes[1], + tuple(fx_block_args), + ) + + cond_node = self.fx_graph.call_function(torch.cond, args, {}) + + # prim::If can also have zero output. + if node.outputsSize() == 1: + output_name = node.output().debugName() + self.name_to_node[output_name] = cond_node + elif node.outputsSize() > 1: + for i, output in enumerate(node.outputs()): + output_name = output.debugName() + getitem = self.fx_graph.call_function(operator.getitem, (cond_node, i)) + self.name_to_node[output_name] = getitem + + def convert_aten_Bool(self, node: torch._C.Node): + self._convert_as_noop(node) + + def convert_prim_Enter(self, node: torch._C.Node): + # export generally treats prim::Enter as noop + # The only context manager export supports is aten::enable_grad. + # Unfortunately, TorchScript does not support aten::enable_grad yet. + # TODO: support aten::enable_grad in both TorchScript and Converter. + return + + def convert_prim_Exit(self, node: torch._C.Node): + # export treats prim::Exit as noop + return + + def _convert_as_noop(self, node: torch._C.Node): + # Converts the node as a no-op by mapping its output node as arg[0] + + target = get_op_overload(node) + schema = target._schema + + args, _kwargs = self.get_args_kwargs(node, schema) + + output_name = node.output().debugName() + self.name_to_node[output_name] = args[0] + + def convert_profiler__record_function_exit(self, node: torch._C.Node): + # _record_function_exit has side effect so we keep it in fx.graph + # currently, _record_function_enter_new and _record_function_exit are + # discarded during `retrace_as_exported_program`. + target = torch.ops.profiler._record_function_exit + args = tuple(self.get_fx_value_by_ir_value(input) for input in node.inputs()) + self.fx_graph.call_function(target, args) + + def convert_prim_tolist(self, node: torch._C.Node): + # prim::tolist cannot be supported by `_convert_standard_operators` + # since it requires call_method instead of call_function. + target = "tolist" + args = (self.get_fx_value_by_ir_value(next(node.inputs())),) + fx_node = self.fx_graph.call_method(target, args) + output_name = node.output().debugName() + self.name_to_node[output_name] = fx_node + + def convert_prim_Uninitialized(self, node: torch._C.Node): + # `prim::Uninitialized` is inserted by the compiler when it can prove + # the value will never be used. It can be introduced by exceptions, + # breaks, continues, and returns. + # So we add a dummy constant to the graph. + output_name = node.output().debugName() + self.name_to_constant[output_name] = torch.Tensor() + + def _convert_standard_operators(self, node: torch._C.Node): + target = kind_to_standard_operators[node.kind()] + args = tuple(self.get_fx_value_by_ir_value(input) for input in node.inputs()) + fx_node = self.fx_graph.call_function(target, args) + output_name = node.output().debugName() + self.name_to_node[output_name] = fx_node + + def convert_node(self, node: torch._C.Node): + node_kind = node.kind() + + # Get handler based on namespace and operator name. + # Provide a default node handler as well in case we don't find + # matching converter for that. + handler_func_name = ir_name_to_func_name(node_kind) + handler_func = getattr(self, handler_func_name, self.convert_call_function_op) + + # str calls print function implemented in CPP. To avoid repeating + # the entire logic here, we simply keep first line from node string (getting rid + # of sub-blocks IR prints). + node_str = "".join(str(node).split("\n")[:1]) + log.debug("[%s] converts [%s]", handler_func.__name__, node_str) + try: + handler_func(node) + except Exception as e: + raise RuntimeError(f"TS2EPConverter failed for node {node_kind}") from e + + def convert_graph_outputs(self): + args = [] + outp_name_list = [outp.debugName() for outp in self.ts_graph.outputs()] + list( + self.name_update_from_subblock_to_parent + ) + for output_name in outp_name_list: + if output_name in self.name_to_node: + fx_node = self.name_to_node[output_name] + # TODO: Revisit this later after HigherOrderOp design changes. + # Currently, we cannot directly return input as output. + if ( + not self.is_top_level_graph() + and isinstance(fx_node, torch.fx.Node) + and fx_node.op == "placeholder" + ): + fx_node = self.fx_graph.call_function(torch.clone, (fx_node,)) + args.append(fx_node) + self.output_specs.append( + OutputSpec( + OutputKind.USER_OUTPUT, + arg=TensorArgument(name=output_name), + target=output_name, + ) + ) + elif output_name in self.name_to_constant: + args.append(self.name_to_constant[output_name]) + self.output_specs.append( + OutputSpec( + OutputKind.USER_OUTPUT, + arg=ConstantArgument( + name=output_name, value=self.name_to_constant[output_name] + ), + target=output_name, + ) + ) + else: + raise ValueError(f"Output {output_name} not found") + + if len(args) == 0: + # Sub-block of prim::If can have zero output. + self.fx_graph.output([]) + elif len(args) == 1: + self.fx_graph.output( + args[0] + ) # Get rid of an extra list wrapped around final output. + elif len(args) > 1: + self.fx_graph.output( + args + ) # For prim::Loop and prim::If with multiple outputs. + else: + # Sub-block of prim::Loop can have multiple outputs. + self.fx_graph.output(args) + + +class ExplainTS2FXGraphConverter(TS2FXGraphConverter): + """ + Run TS2FXGraphConverter in an explain mode. It collects all failed operators conversions + and provide that information to users. In order to collect all failed conversions, it + also mocks some internal attributes (e.g., name_to_node). + """ + + class _DictMock(dict): + def __init__(self, dict_data, mock_value): + super().__init__(dict_data) + self.mock_value = mock_value + + def __getitem__(self, key): + # If the original dictionary has the key, return its value. + # Otherwise, return the mock value. + if not super().__contains__(key): + return self.mock_value + return super().__getitem__(key) + + def __contains__(self, key): + return True + + def __init__( + self, + ts_graph: torch._C.Graph | torch._C.Block, + name_to_param: dict[str, torch.Tensor], + name_to_buffer: dict[str, torch.Tensor], + blocks_to_lifted_attrs: dict[torch._C.Block, set[str]], + name_to_non_tensor_attribute: dict[str, Any], + name_to_constant: dict[str, Any], + name_to_attribute_fqn: dict[str, str], + ): + super().__init__( + ts_graph, + name_to_param, + name_to_buffer, + blocks_to_lifted_attrs, + name_to_non_tensor_attribute, + name_to_constant, + name_to_attribute_fqn, + ) + + # Data to keep track of unsupported nodes. + self.unsupported_node_list: list[torch._C.Node] = [] + + # Add mock to needed attributes. + self.name_to_node = ExplainTS2FXGraphConverter._DictMock( + self.name_to_node, + # Dummy node. + torch.fx.Node( + None, # type: ignore[arg-type] + "mock", + "call_function", + lambda: None, + (), + {}, + ), + ) + + def explain(self): + self.convert_graph_inputs() + for node in self.ts_graph.nodes(): + self.convert_node(node) + self.convert_graph_outputs() + + def convert_node(self, node): + try: + super().convert_node(node) + except Exception: + self.unsupported_node_list.append(node) + + +@contextmanager +def disable_logging(log): + disabled = log.disabled + log.disabled = True + try: + yield + finally: + log.disabled = disabled + + +class TS2EPConverter: + # TorchScript model to ExportedProgram converter + def __init__( + self, + ts_model: torch.jit.ScriptModule | torch.jit.ScriptFunction, + sample_args: tuple[Any, ...], + sample_kwargs: dict[str, Any] | None = None, + ): + self.ts_model = ts_model + self.ts_graph, self.params, _, _ = _create_jit_graph(ts_model, sample_args) + + self.sample_args = sample_args + self.sample_kwargs = sample_kwargs + + self.name_to_param: dict[str, torch.Tensor] = {} + self.name_to_buffer: dict[str, torch.Tensor] = {} + param_list = ( + list(self.ts_model.parameters()) + if not isinstance(self.ts_model, torch._C.ScriptFunction) + else [] + ) + if not isinstance(self.ts_model, torch._C.ScriptFunction): + for k, tensor in self.ts_model.state_dict().items(): # type: ignore[union-attr] + # Check if tensor belongs to any parameter. + if any( + (tensor == param).all() + for param in param_list + if tensor.shape == param.shape + ): + self.name_to_param[k] = tensor + else: + self.name_to_buffer[k] = tensor + + self.name_to_non_tensor_attributes: dict[str, Any] = {} + self.name_to_constant: dict[str, Any] = {} + + self.lift_get_attr() + + def convert(self) -> ExportedProgram: + log.info( + """ +TS2EPConverter logging starts from here. + +INFO: (TORCH_LOGS="export" ) + * Log TorchScript IR. + +DEBUG: (TORCH_LOGS="+export" ), additionally + * Log conversion IR by IR in a format of [] converts []. + """ + ) + log.info("TorchScript graph\n\n%s\n", self.ts_graph) + + blocks_to_lifted_attrs, name_to_attribute_fqn = get_block_to_lifted_attrs( + self.ts_graph + ) + + graph_converter = TS2FXGraphConverter( + self.ts_graph, + self.name_to_param, + self.name_to_buffer, + blocks_to_lifted_attrs, + self.name_to_non_tensor_attributes, + self.name_to_constant, + name_to_attribute_fqn, + ) + gm = graph_converter.convert() + + # Post-processing step to deal with quantized operators. + replace_quantized_ops_with_standard_ops(gm) + log.info("GraphModule: %s", gm.print_readable(print_output=False)) + + ep = self.retrace_as_exported_program( + gm, + graph_converter.name_to_constant, + ) + log.info("%s", ep) + + # Post-processing step to ensure ExportedProgram has the same state_dict as + # the original TorchScript model. Throw warnings for additionally populated + # state_dict entries. + if not isinstance(self.ts_model, torch._C.ScriptFunction): + for k, tensor in self.ts_model.state_dict().items(): # type: ignore[union-attr] + if k not in ep.state_dict: + warnings.warn( + f"Manually populate {k} into state_dict ExportedProgram, but it is never used by the ExportedProgram.", + stacklevel=2, + ) + ep.state_dict[k] = tensor + + return ep + + @disable_logging(log) + def explain(self, print_output=True): + blocks_to_lifted_attrs, name_to_attribute_fqn = get_block_to_lifted_attrs( + self.ts_graph + ) + + graph_converter = ExplainTS2FXGraphConverter( + self.ts_graph, + self.name_to_param, + self.name_to_buffer, + blocks_to_lifted_attrs, + self.name_to_non_tensor_attributes, + self.name_to_constant, + name_to_attribute_fqn, + ) + graph_converter.explain() + if len(graph_converter.unsupported_node_list) > 0: + explain_str = "Unsupported nodes are found in the following list:" + for i, n in enumerate(graph_converter.unsupported_node_list): + node_str = "".join(str(n).split("\n")[:1]) + explain_str += f"\n\n {i}. {n.kind()} [{node_str}]" + else: + explain_str = "Success!" + if print_output: + print(explain_str) + return explain_str + + def retrace_as_exported_program( + self, + gm: torch.fx.GraphModule, + name_to_constant: dict[str, Any], + ): + dynamic_shapes = _tree_map_with_path( + lambda path, x: ( + [Dim.AUTO] * x.dim() if isinstance(x, torch.Tensor) else None + ), + self.sample_args, + ) + + # TODO: adjust input orders to match GraphSignature convention + ep = torch.export._trace._export( + gm, + self.sample_args, + dynamic_shapes=dynamic_shapes, + strict=False, + pre_dispatch=True, + ) + + # Post-processing to make sure the ExportedProgram states are correct. + # Because during conversion, we set tensor constants as GetAttr, + # retracing cannot recognize them as tensor constants but instead + # treat them as buffers. We need to set them again here. + ep._constants.update( + { + k: v + for k, v in name_to_constant.items() + if isinstance(v, (torch.Tensor, torch.ScriptObject)) + } + ) + for k in name_to_constant: + ep.state_dict.pop(k, None) + + for spec in ep.graph_signature.input_specs: + # Mark as constant tensors for erroneously traced buffers. + if spec.kind == InputKind.BUFFER and spec.target in name_to_constant: + if not isinstance(name_to_constant[spec.target], torch.Tensor): + raise AssertionError( + f"{type(name_to_constant[spec.target])} has been erroneously marked as buffer" + ) + spec.kind = InputKind.CONSTANT_TENSOR + spec.persistent = None + ep.verifier().check(ep) + + return ep + + def lift_get_attr(self): + # This function lifts multiple data types. + + # 1. Tensor constants attributes (e.g., self.data = torch.tensor([2,3])) + # to buffers. Currently, when there are tensor constants, export + # would error and ask users to register tensor constants as buffers. + # Since it is hard to manually do so for TorchScript models + # (e.g., source code is missing), this function automatically + # lifts tensor constants to be buffers. + + # 2. ScriptObbject to constant. It will then be converted to getattr in + # in the fx graph. + # + # This function should happen in TS2EPConverter instead of + # TS2FXGraphConverter since it gets attributes from self.ts_model + # which is not accessible in TS2FXGraphConverter. It is similar to where + # we collect self.name_to_param and self.name_to_buffer. + name_to_attribute_fqn: dict[str, str] = {} + + def get_attr(fqn: str): + name = fqn.split(".") + v = self.ts_model + for n in name: + v = getattr(v, n) + return v + + def get_fqn(node: torch._C.Node): + attr_name = node.s("name") + input_name = node.input().debugName() + root_attr_name = name_to_attribute_fqn[input_name] + attr_fqn = f"{root_attr_name}.{attr_name}" if root_attr_name else attr_name + return attr_fqn + + def _dfs_get_attr(block): + for node in block.nodes(): + if node.kind() == "prim::CreateObject": + output_name = node.output().debugName() + name_to_attribute_fqn[output_name] = "" + + if node.kind() == "prim::GetAttr": + attr_fqn = get_fqn(node) + value = get_attr(attr_fqn) + output_name = node.output().debugName() + name_to_attribute_fqn[output_name] = attr_fqn + if isinstance(value, torch.Tensor): + if attr_fqn not in self.name_to_buffer: + # Lift tensor constants to be a buffer + self.name_to_buffer[attr_fqn] = value + elif isinstance(value, torch.ScriptObject): + if attr_fqn not in self.name_to_constant: + self.name_to_constant[attr_fqn] = value + else: + self.name_to_non_tensor_attributes[attr_fqn] = value + + for subblock in node.blocks(): + _dfs_get_attr(subblock) + + _dfs_get_attr(self.ts_graph) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..10a55772ab58b21573a6eba0356ddd3080164ac7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/case.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/case.py new file mode 100644 index 0000000000000000000000000000000000000000..476fe9793a55ca6cf91277da97a8c7c5c02223f1 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/case.py @@ -0,0 +1,177 @@ +# mypy: allow-untyped-defs +import inspect +import re +import string +from dataclasses import dataclass, field +from enum import Enum +from typing import Any +from types import ModuleType + +import torch + +_TAGS: dict[str, dict[str, Any]] = { + "torch": { + "cond": {}, + "dynamic-shape": {}, + "escape-hatch": {}, + "map": {}, + "dynamic-value": {}, + "operator": {}, + "mutation": {}, + }, + "python": { + "assert": {}, + "builtin": {}, + "closure": {}, + "context-manager": {}, + "control-flow": {}, + "data-structure": {}, + "standard-library": {}, + "object-model": {}, + }, +} + + +class SupportLevel(Enum): + """ + Indicates at what stage the feature + used in the example is handled in export. + """ + + SUPPORTED = 1 + NOT_SUPPORTED_YET = 0 + + +ArgsType = tuple[Any, ...] + + +def check_inputs_type(args, kwargs): + if not isinstance(args, tuple): + raise ValueError( + f"Expecting args type to be a tuple, got: {type(args)}" + ) + if not isinstance(kwargs, dict): + raise ValueError( + f"Expecting kwargs type to be a dict, got: {type(kwargs)}" + ) + for key in kwargs: + if not isinstance(key, str): + raise ValueError( + f"Expecting kwargs keys to be a string, got: {type(key)}" + ) + +def _validate_tag(tag: str): + parts = tag.split(".") + t = _TAGS + for part in parts: + if not set(part) <= set(string.ascii_lowercase + "-"): + raise AssertionError(f"Tag contains invalid characters: {part}") + if part in t: + t = t[part] + else: + raise ValueError(f"Tag {tag} is not found in registered tags.") + + +@dataclass(frozen=True) +class ExportCase: + example_args: ArgsType + description: str # A description of the use case. + model: torch.nn.Module + name: str + example_kwargs: dict[str, Any] = field(default_factory=dict) + extra_args: ArgsType | None = None # For testing graph generalization. + # Tags associated with the use case. (e.g dynamic-shape, escape-hatch) + tags: set[str] = field(default_factory=set) + support_level: SupportLevel = SupportLevel.SUPPORTED + dynamic_shapes: dict[str, Any] | None = None + + def __post_init__(self): + check_inputs_type(self.example_args, self.example_kwargs) + if self.extra_args is not None: + check_inputs_type(self.extra_args, {}) + + for tag in self.tags: + _validate_tag(tag) + + if not isinstance(self.description, str) or len(self.description) == 0: + raise ValueError(f'Invalid description: "{self.description}"') + + +_EXAMPLE_CASES: dict[str, ExportCase] = {} +_MODULES: set[ModuleType] = set() +_EXAMPLE_CONFLICT_CASES: dict[str, list[ExportCase]] = {} +_EXAMPLE_REWRITE_CASES: dict[str, list[ExportCase]] = {} + + +def register_db_case(case: ExportCase) -> None: + """ + Registers a user provided ExportCase into example bank. + """ + if case.name in _EXAMPLE_CASES: + if case.name not in _EXAMPLE_CONFLICT_CASES: + _EXAMPLE_CONFLICT_CASES[case.name] = [_EXAMPLE_CASES[case.name]] + _EXAMPLE_CONFLICT_CASES[case.name].append(case) + return + + _EXAMPLE_CASES[case.name] = case + + +def to_snake_case(name): + name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) + return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower() + + +def _make_export_case(m, name, configs): + if not isinstance(m, torch.nn.Module): + raise TypeError("Export case class should be a torch.nn.Module.") + + if "description" not in configs: + # Fallback to docstring if description is missing. + if m.__doc__ is None: + raise AssertionError( + f"Could not find description or docstring for export case: {m}" + ) + configs = {**configs, "description": m.__doc__} + # pyrefly: ignore [bad-argument-type] + return ExportCase(**{**configs, "model": m, "name": name}) + + +def export_case(**kwargs): + """ + Decorator for registering a user provided case into example bank. + """ + + def wrapper(m): + configs = kwargs + module = inspect.getmodule(m) + if module in _MODULES: + raise RuntimeError("export_case should only be used once per example file.") + + if module is None: + raise AssertionError("module must not be None") + _MODULES.add(module) + module_name = module.__name__.split(".")[-1] + case = _make_export_case(m, module_name, configs) + register_db_case(case) + return case + + return wrapper + + +def export_rewrite_case(**kwargs): + def wrapper(m): + configs = kwargs + + parent = configs.pop("parent") + if not isinstance(parent, ExportCase): + raise AssertionError(f"expected ExportCase, got {type(parent)}") + key = parent.name + if key not in _EXAMPLE_REWRITE_CASES: + _EXAMPLE_REWRITE_CASES[key] = [] + + configs["example_args"] = parent.example_args + case = _make_export_case(m, to_snake_case(m.__name__), configs) + _EXAMPLE_REWRITE_CASES[key].append(case) + return case + + return wrapper diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..834dbce32f10bfb339fd2182a2455b43914441c9 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/__init__.py @@ -0,0 +1,61 @@ +# mypy: allow-untyped-defs +import dataclasses +import glob +import inspect +from os.path import basename, dirname, isfile, join + +import torch +from torch._export.db.case import ( + _EXAMPLE_CASES, + _EXAMPLE_CONFLICT_CASES, + _EXAMPLE_REWRITE_CASES, + SupportLevel, + export_case, + ExportCase, +) + + +def _collect_examples(): + case_names = glob.glob(join(dirname(__file__), "*.py")) + case_names = [ + basename(f)[:-3] for f in case_names if isfile(f) and not f.endswith("__init__.py") + ] + + case_fields = {f.name for f in dataclasses.fields(ExportCase)} + for case_name in case_names: + case = __import__(case_name, globals(), locals(), [], 1) + variables = [name for name in dir(case) if name in case_fields] + export_case(**{v: getattr(case, v) for v in variables})(case.model) + +_collect_examples() + +def all_examples(): + return _EXAMPLE_CASES + + +if len(_EXAMPLE_CONFLICT_CASES) > 0: + + def get_name(case): + model = case.model + if isinstance(model, torch.nn.Module): + model = type(model) + return model.__name__ + + msg = "Error on conflict export case name.\n" + for case_name, cases in _EXAMPLE_CONFLICT_CASES.items(): + msg += f"Case name {case_name} is associated with multiple cases:\n " + msg += f"[{','.join(map(get_name, cases))}]\n" + + raise RuntimeError(msg) + + +def filter_examples_by_support_level(support_level: SupportLevel): + return { + key: val + for key, val in all_examples().items() + if val.support_level == support_level + } + + +def get_rewrite_cases(case): + return _EXAMPLE_REWRITE_CASES.get(case.name, []) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/assume_constant_result.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/assume_constant_result.py new file mode 100644 index 0000000000000000000000000000000000000000..931ce7f7a50fc5a175101ac57c424c88cf31b54c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/assume_constant_result.py @@ -0,0 +1,20 @@ +# mypy: allow-untyped-defs +import torch +import torch._dynamo as torchdynamo + + +class AssumeConstantResult(torch.nn.Module): + """ + Applying `assume_constant_result` decorator to burn make non-tracable code as constant. + """ + + @torchdynamo.assume_constant_result + def get_item(self, y): + return y.int().item() + + def forward(self, x, y): + return x[: self.get_item(y)] + +example_args = (torch.randn(3, 2), torch.tensor(4)) +tags = {"torch.escape-hatch"} +model = AssumeConstantResult() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/autograd_function.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/autograd_function.py new file mode 100644 index 0000000000000000000000000000000000000000..efd645d13a7d5a13dc69d9ab3593772520b726c0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/autograd_function.py @@ -0,0 +1,25 @@ +# mypy: allow-untyped-defs +import torch + +class MyAutogradFunction(torch.autograd.Function): + @staticmethod + # pyrefly: ignore [bad-override] + def forward(ctx, x): + return x.clone() + + @staticmethod + # pyrefly: ignore [bad-override] + def backward(ctx, grad_output): + return grad_output + 1 + +class AutogradFunction(torch.nn.Module): + """ + TorchDynamo does not keep track of backward() on autograd functions. We recommend to + use `allow_in_graph` to mitigate this problem. + """ + + def forward(self, x): + return MyAutogradFunction.apply(x) + +example_args = (torch.randn(3, 2),) +model = AutogradFunction() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/class_method.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/class_method.py new file mode 100644 index 0000000000000000000000000000000000000000..f701f54d4f4ea1cb5816292cd60bb4df3d03c5e8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/class_method.py @@ -0,0 +1,22 @@ +# mypy: allow-untyped-defs +import torch + +class ClassMethod(torch.nn.Module): + """ + Class methods are inlined during tracing. + """ + + @classmethod + def method(cls, x): + return x + 1 + + def __init__(self) -> None: + super().__init__() + self.linear = torch.nn.Linear(4, 2) + + def forward(self, x): + x = self.linear(x) + return self.method(x) * self.__class__.method(x) * type(self).method(x) + +example_args = (torch.randn(3, 4),) +model = ClassMethod() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/cond_branch_class_method.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/cond_branch_class_method.py new file mode 100644 index 0000000000000000000000000000000000000000..22600cc504348d1d261b0ea2b9ed2d57af76b0a3 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/cond_branch_class_method.py @@ -0,0 +1,44 @@ +# mypy: allow-untyped-defs +import torch + +from functorch.experimental.control_flow import cond + +class MySubModule(torch.nn.Module): + def foo(self, x): + return x.cos() + + def forward(self, x): + return self.foo(x) + +class CondBranchClassMethod(torch.nn.Module): + """ + The branch functions (`true_fn` and `false_fn`) passed to cond() must follow these rules: + - both branches must take the same args, which must also match the branch args passed to cond. + - both branches must return a single tensor + - returned tensor must have the same tensor metadata, e.g. shape and dtype + - branch function can be free function, nested function, lambda, class methods + - branch function can not have closure variables + - no inplace mutations on inputs or global variables + + + This example demonstrates using class method in cond(). + + NOTE: If the `pred` is test on a dim with batch size < 2, it will be specialized. + """ + + def __init__(self) -> None: + super().__init__() + self.subm = MySubModule() + + def bar(self, x): + return x.sin() + + def forward(self, x): + return cond(x.shape[0] <= 2, self.subm.forward, self.bar, [x]) + +example_args = (torch.randn(3),) +tags = { + "torch.cond", + "torch.dynamic-shape", +} +model = CondBranchClassMethod() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/cond_branch_nested_function.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/cond_branch_nested_function.py new file mode 100644 index 0000000000000000000000000000000000000000..b28ceeddc7956d136a8cf786c283344731d3e7ac --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/cond_branch_nested_function.py @@ -0,0 +1,41 @@ +# mypy: allow-untyped-defs +import torch + +from functorch.experimental.control_flow import cond + +class CondBranchNestedFunction(torch.nn.Module): + """ + The branch functions (`true_fn` and `false_fn`) passed to cond() must follow these rules: + - both branches must take the same args, which must also match the branch args passed to cond. + - both branches must return a single tensor + - returned tensor must have the same tensor metadata, e.g. shape and dtype + - branch function can be free function, nested function, lambda, class methods + - branch function can not have closure variables + - no inplace mutations on inputs or global variables + + This example demonstrates using nested function in cond(). + + NOTE: If the `pred` is test on a dim with batch size < 2, it will be specialized. + """ + + def forward(self, x): + def true_fn(x): + def inner_true_fn(y): + return x + y + + return inner_true_fn(x) + + def false_fn(x): + def inner_false_fn(y): + return x - y + + return inner_false_fn(x) + + return cond(x.shape[0] < 10, true_fn, false_fn, [x]) + +example_args = (torch.randn(3),) +tags = { + "torch.cond", + "torch.dynamic-shape", +} +model = CondBranchNestedFunction() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/cond_branch_nonlocal_variables.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/cond_branch_nonlocal_variables.py new file mode 100644 index 0000000000000000000000000000000000000000..50d0ec87a690d063cb0e841fc057a6ae95c369fb --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/cond_branch_nonlocal_variables.py @@ -0,0 +1,59 @@ +# mypy: allow-untyped-defs +import torch + +from functorch.experimental.control_flow import cond + +class CondBranchNonlocalVariables(torch.nn.Module): + """ + The branch functions (`true_fn` and `false_fn`) passed to cond() must follow these rules: + - both branches must take the same args, which must also match the branch args passed to cond. + - both branches must return a single tensor + - returned tensor must have the same tensor metadata, e.g. shape and dtype + - branch function can be free function, nested function, lambda, class methods + - branch function can not have closure variables + - no inplace mutations on inputs or global variables + + This example demonstrates how to rewrite code to avoid capturing closure variables in branch functions. + + The code below will not work because capturing closure variables is not supported. + ``` + my_tensor_var = x + 100 + my_primitive_var = 3.14 + + def true_fn(y): + nonlocal my_tensor_var, my_primitive_var + return y + my_tensor_var + my_primitive_var + + def false_fn(y): + nonlocal my_tensor_var, my_primitive_var + return y - my_tensor_var - my_primitive_var + + return cond(x.shape[0] > 5, true_fn, false_fn, [x]) + ``` + + NOTE: If the `pred` is test on a dim with batch size < 2, it will be specialized. + """ + + def forward(self, x): + my_tensor_var = x + 100 + my_primitive_var = 3.14 + + def true_fn(x, y, z): + return x + y + z + + def false_fn(x, y, z): + return x - y - z + + return cond( + x.shape[0] > 5, + true_fn, + false_fn, + [x, my_tensor_var, torch.tensor(my_primitive_var)], + ) + +example_args = (torch.randn(6),) +tags = { + "torch.cond", + "torch.dynamic-shape", +} +model = CondBranchNonlocalVariables() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/cond_closed_over_variable.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/cond_closed_over_variable.py new file mode 100644 index 0000000000000000000000000000000000000000..183180ab4fc825385170fea2bec6af184374a67e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/cond_closed_over_variable.py @@ -0,0 +1,22 @@ +# mypy: allow-untyped-defs +import torch + +from functorch.experimental.control_flow import cond + +class CondClosedOverVariable(torch.nn.Module): + """ + torch.cond() supports branches closed over arbitrary variables. + """ + + def forward(self, pred, x): + def true_fn(val): + return x * 2 + + def false_fn(val): + return x - 2 + + return cond(pred, true_fn, false_fn, [x + 1]) + +example_args = (torch.tensor(True), torch.randn(3, 2)) +tags = {"torch.cond", "python.closure"} +model = CondClosedOverVariable() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/cond_operands.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/cond_operands.py new file mode 100644 index 0000000000000000000000000000000000000000..60a75d24639cdac991298e99acf96b8eadbff442 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/cond_operands.py @@ -0,0 +1,35 @@ +# mypy: allow-untyped-defs +import torch + +from torch.export import Dim + +x = torch.randn(3, 2) +y = torch.randn(2) +dim0_x = Dim("dim0_x") + +class CondOperands(torch.nn.Module): + """ + The operands passed to cond() must be: + - a list of tensors + - match arguments of `true_fn` and `false_fn` + + NOTE: If the `pred` is test on a dim with batch size < 2, it will be specialized. + """ + + def forward(self, x, y): + def true_fn(x, y): + return x + y + + def false_fn(x, y): + return x - y + + return torch.cond(x.shape[0] > 2, true_fn, false_fn, [x, y]) + +example_args = (x, y) +tags = { + "torch.cond", + "torch.dynamic-shape", +} +extra_inputs = (torch.randn(2, 2), torch.randn(2)) +dynamic_shapes = {"x": {0: dim0_x}, "y": None} +model = CondOperands() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/cond_predicate.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/cond_predicate.py new file mode 100644 index 0000000000000000000000000000000000000000..68bb8850bba909a0c6546c8f12a1a3fa1bdc70d1 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/cond_predicate.py @@ -0,0 +1,25 @@ +# mypy: allow-untyped-defs +import torch + +from functorch.experimental.control_flow import cond + +class CondPredicate(torch.nn.Module): + """ + The conditional statement (aka predicate) passed to cond() must be one of the following: + - torch.Tensor with a single element + - boolean expression + + NOTE: If the `pred` is test on a dim with batch size < 2, it will be specialized. + """ + + def forward(self, x): + pred = x.dim() > 2 and x.shape[2] > 10 + + return cond(pred, lambda x: x.cos(), lambda y: y.sin(), [x]) + +example_args = (torch.randn(6, 4, 3),) +tags = { + "torch.cond", + "torch.dynamic-shape", +} +model = CondPredicate() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/constrain_as_size_example.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/constrain_as_size_example.py new file mode 100644 index 0000000000000000000000000000000000000000..934746aaf6739de7a37077d8ec3c2776586a5657 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/constrain_as_size_example.py @@ -0,0 +1,23 @@ +# mypy: allow-untyped-defs +import torch + + +class ConstrainAsSizeExample(torch.nn.Module): + """ + If the value is not known at tracing time, you can provide hint so that we + can trace further. Please look at torch._check APIs. + """ + + def forward(self, x): + a = x.item() + torch._check(a >= 0) + torch._check(a <= 5) + return torch.zeros((a, 5)) + + +example_args = (torch.tensor(4),) +tags = { + "torch.dynamic-value", + "torch.escape-hatch", +} +model = ConstrainAsSizeExample() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/constrain_as_value_example.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/constrain_as_value_example.py new file mode 100644 index 0000000000000000000000000000000000000000..22f791a3e80474257c27d927bad56cf4c2fbce78 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/constrain_as_value_example.py @@ -0,0 +1,26 @@ +# mypy: allow-untyped-defs +import torch + + +class ConstrainAsValueExample(torch.nn.Module): + """ + If the value is not known at tracing time, you can provide hint so that we + can trace further. Please look at torch._check API. + """ + + def forward(self, x, y): + a = x.item() + torch._check(a >= 0) + torch._check(a <= 5) + + if a < 6: + return y.sin() + return y.cos() + + +example_args = (torch.tensor(4), torch.randn(5, 5)) +tags = { + "torch.dynamic-value", + "torch.escape-hatch", +} +model = ConstrainAsValueExample() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/decorator.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/decorator.py new file mode 100644 index 0000000000000000000000000000000000000000..7d24cc681a6b62adf40bfd9a2e5283afb3515131 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/decorator.py @@ -0,0 +1,23 @@ +# mypy: allow-untyped-defs +import functools + +import torch + +def test_decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + 1 + + return wrapper + +class Decorator(torch.nn.Module): + """ + Decorators calls are inlined into the exported function during tracing. + """ + + @test_decorator + def forward(self, x, y): + return x + y + +example_args = (torch.randn(3, 2), torch.randn(3, 2)) +model = Decorator() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dictionary.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dictionary.py new file mode 100644 index 0000000000000000000000000000000000000000..49e688bc0ac1f09567e3b877aaca29a1d02b4121 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dictionary.py @@ -0,0 +1,17 @@ +# mypy: allow-untyped-defs +import torch + +class Dictionary(torch.nn.Module): + """ + Dictionary structures are inlined and flattened along tracing. + """ + + def forward(self, x, y): + elements = {} + elements["x2"] = x * x + y = y * elements["x2"] + return {"y": y} + +example_args = (torch.randn(3, 2), torch.tensor(4)) +tags = {"python.data-structure"} +model = Dictionary() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dynamic_shape_assert.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dynamic_shape_assert.py new file mode 100644 index 0000000000000000000000000000000000000000..abf40db97dd459b24dad35a14d1576ae6d1c93a1 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dynamic_shape_assert.py @@ -0,0 +1,18 @@ +# mypy: allow-untyped-defs +import torch + +class DynamicShapeAssert(torch.nn.Module): + """ + A basic usage of python assertion. + """ + + def forward(self, x): + # assertion with error message + assert x.shape[0] > 2, f"{x.shape[0]} is greater than 2" # noqa: S101 + # assertion without error message + assert x.shape[0] > 1 # noqa: S101 + return x + +example_args = (torch.randn(3, 2),) +tags = {"python.assert"} +model = DynamicShapeAssert() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dynamic_shape_constructor.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dynamic_shape_constructor.py new file mode 100644 index 0000000000000000000000000000000000000000..157e460274ad58ba71c886b35364ddc0cd4d886a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dynamic_shape_constructor.py @@ -0,0 +1,15 @@ +# mypy: allow-untyped-defs +import torch + +class DynamicShapeConstructor(torch.nn.Module): + """ + Tensor constructors should be captured with dynamic shape inputs rather + than being baked in with static shape. + """ + + def forward(self, x): + return torch.zeros(x.shape[0] * 2) + +example_args = (torch.randn(3, 2),) +tags = {"torch.dynamic-shape"} +model = DynamicShapeConstructor() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dynamic_shape_if_guard.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dynamic_shape_if_guard.py new file mode 100644 index 0000000000000000000000000000000000000000..21824ef3a0f66eb25f4d8e8c1ba92f53fdd4c275 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dynamic_shape_if_guard.py @@ -0,0 +1,19 @@ +# mypy: allow-untyped-defs +import torch + +class DynamicShapeIfGuard(torch.nn.Module): + """ + `if` statement with backed dynamic shape predicate will be specialized into + one particular branch and generate a guard. However, export will fail if the + the dimension is marked as dynamic shape from higher level API. + """ + + def forward(self, x): + if x.shape[0] == 3: + return x.cos() + + return x.sin() + +example_args = (torch.randn(3, 2, 2),) +tags = {"torch.dynamic-shape", "python.control-flow"} +model = DynamicShapeIfGuard() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dynamic_shape_map.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dynamic_shape_map.py new file mode 100644 index 0000000000000000000000000000000000000000..f8066aed556b9ee588b9744d17ba16c35d8fed6c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dynamic_shape_map.py @@ -0,0 +1,19 @@ +# mypy: allow-untyped-defs +import torch + +from functorch.experimental.control_flow import map + +class DynamicShapeMap(torch.nn.Module): + """ + functorch map() maps a function over the first tensor dimension. + """ + + def forward(self, xs, y): + def body(x, y): + return x + y + + return map(body, xs, y) + +example_args = (torch.randn(3, 2), torch.randn(2)) +tags = {"torch.dynamic-shape", "torch.map"} +model = DynamicShapeMap() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dynamic_shape_round.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dynamic_shape_round.py new file mode 100644 index 0000000000000000000000000000000000000000..decbf036553cb76544a19e531e5aee98d792ae0b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dynamic_shape_round.py @@ -0,0 +1,21 @@ +# mypy: allow-untyped-defs +import torch + +from torch._export.db.case import SupportLevel +from torch.export import Dim + +class DynamicShapeRound(torch.nn.Module): + """ + Calling round on dynamic shapes is not supported. + """ + + def forward(self, x): + return x[: round(x.shape[0] / 2)] + +x = torch.randn(3, 2) +dim0_x = Dim("dim0_x") +example_args = (x,) +tags = {"torch.dynamic-shape", "python.builtin"} +support_level = SupportLevel.NOT_SUPPORTED_YET +dynamic_shapes = {"x": {0: dim0_x}} +model = DynamicShapeRound() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dynamic_shape_slicing.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dynamic_shape_slicing.py new file mode 100644 index 0000000000000000000000000000000000000000..360fe15f6f98d9d735366bfa53371d79e0b00209 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dynamic_shape_slicing.py @@ -0,0 +1,15 @@ +# mypy: allow-untyped-defs +import torch + +class DynamicShapeSlicing(torch.nn.Module): + """ + Slices with dynamic shape arguments should be captured into the graph + rather than being baked in. + """ + + def forward(self, x): + return x[: x.shape[0] - 2, x.shape[1] - 1 :: 2] + +example_args = (torch.randn(3, 2),) +tags = {"torch.dynamic-shape"} +model = DynamicShapeSlicing() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dynamic_shape_view.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dynamic_shape_view.py new file mode 100644 index 0000000000000000000000000000000000000000..c45d4aeebb0282a0f56c58a587b4bfe1655f50e3 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/dynamic_shape_view.py @@ -0,0 +1,17 @@ +# mypy: allow-untyped-defs +import torch + +class DynamicShapeView(torch.nn.Module): + """ + Dynamic shapes should be propagated to view arguments instead of being + baked into the exported graph. + """ + + def forward(self, x): + new_x_shape = x.size()[:-1] + (2, 5) + x = x.view(*new_x_shape) + return x.permute(0, 2, 1) + +example_args = (torch.randn(10, 10),) +tags = {"torch.dynamic-shape"} +model = DynamicShapeView() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/fn_with_kwargs.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/fn_with_kwargs.py new file mode 100644 index 0000000000000000000000000000000000000000..46b2637b398c21bf9399d0a3fa2a964354beea3e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/fn_with_kwargs.py @@ -0,0 +1,30 @@ +# mypy: allow-untyped-defs +import torch + +class FnWithKwargs(torch.nn.Module): + """ + Keyword arguments are not supported at the moment. + """ + + def forward(self, pos0, tuple0, *myargs, mykw0, **mykwargs): + out = pos0 + for arg in tuple0: + out = out * arg + for arg in myargs: + out = out * arg + out = out * mykw0 + out = out * mykwargs["input0"] * mykwargs["input1"] + return out + +example_args = ( + torch.randn(4), + (torch.randn(4), torch.randn(4)), + *[torch.randn(4), torch.randn(4)] +) +example_kwargs = { + "mykw0": torch.randn(4), + "input0": torch.randn(4), + "input1": torch.randn(4), +} +tags = {"python.data-structure"} +model = FnWithKwargs() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/list_contains.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/list_contains.py new file mode 100644 index 0000000000000000000000000000000000000000..150070945cfa7f2ef7aca1f8c3dfece0f07cbecd --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/list_contains.py @@ -0,0 +1,17 @@ +# mypy: allow-untyped-defs +import torch + +class ListContains(torch.nn.Module): + """ + List containment relation can be checked on a dynamic shape or constants. + """ + + def forward(self, x): + assert x.size(-1) in [6, 2] # noqa: S101 + assert x.size(0) not in [4, 5, 6] # noqa: S101 + assert "monkey" not in ["cow", "pig"] # noqa: S101 + return x + x + +example_args = (torch.randn(3, 2),) +tags = {"torch.dynamic-shape", "python.data-structure", "python.assert"} +model = ListContains() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/list_unpack.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/list_unpack.py new file mode 100644 index 0000000000000000000000000000000000000000..98533cfab5498934a61fbe693bb2497d5dbc9738 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/list_unpack.py @@ -0,0 +1,21 @@ +# mypy: allow-untyped-defs + +import torch + +class ListUnpack(torch.nn.Module): + """ + Lists are treated as static construct, therefore unpacking should be + erased after tracing. + """ + + def forward(self, args: list[torch.Tensor]): + """ + Lists are treated as static construct, therefore unpacking should be + erased after tracing. + """ + x, *y = args + return x + y[0] + +example_args = ([torch.randn(3, 2), torch.tensor(4), torch.tensor(5)],) +tags = {"python.control-flow", "python.data-structure"} +model = ListUnpack() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/model_attr_mutation.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/model_attr_mutation.py new file mode 100644 index 0000000000000000000000000000000000000000..122b0ddfc3429fb31415a146e8e1dcfddb2fe031 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/model_attr_mutation.py @@ -0,0 +1,24 @@ +# mypy: allow-untyped-defs +import torch + + +class ModelAttrMutation(torch.nn.Module): + """ + Attribute mutation raises a warning. Covered in the test_export.py test_detect_leak_strict test. + """ + + def __init__(self) -> None: + super().__init__() + self.attr_list = [torch.randn(3, 2), torch.randn(3, 2)] + + def recreate_list(self): + return [torch.zeros(3, 2), torch.zeros(3, 2)] + + def forward(self, x): + self.attr_list = self.recreate_list() + return x.sum() + self.attr_list[0].sum() + + +example_args = (torch.randn(3, 2),) +tags = {"python.object-model"} +model = ModelAttrMutation() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/nested_function.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/nested_function.py new file mode 100644 index 0000000000000000000000000000000000000000..e4076ac14dada40b4d78812666a9ec6b5e67045b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/nested_function.py @@ -0,0 +1,23 @@ +# mypy: allow-untyped-defs +import torch + +class NestedFunction(torch.nn.Module): + """ + Nested functions are traced through. Side effects on global captures + are not supported though. + """ + + def forward(self, a, b): + x = a + b + z = a - b + + def closure(y): + nonlocal x + x += 1 + return x * y + z + + return closure(x) + +example_args = (torch.randn(3, 2), torch.randn(2)) +tags = {"python.closure"} +model = NestedFunction() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/null_context_manager.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/null_context_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..80d09f68097edbe676077be183711dabe5cbc664 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/null_context_manager.py @@ -0,0 +1,21 @@ +# mypy: allow-untyped-defs +import contextlib + +import torch + +class NullContextManager(torch.nn.Module): + """ + Null context manager in Python will be traced out. + """ + + def forward(self, x): + """ + Null context manager in Python will be traced out. + """ + ctx = contextlib.nullcontext() + with ctx: + return x.sin() + x.cos() + +example_args = (torch.randn(3, 2),) +tags = {"python.context-manager"} +model = NullContextManager() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/optional_input.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/optional_input.py new file mode 100644 index 0000000000000000000000000000000000000000..41e66a7c977a83bf59116864c7f443387277f06e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/optional_input.py @@ -0,0 +1,20 @@ +# mypy: allow-untyped-defs +import torch +from torch._export.db.case import SupportLevel + + +class OptionalInput(torch.nn.Module): + """ + Tracing through optional input is not supported yet + """ + + def forward(self, x, y=torch.randn(2, 3)): + if y is not None: + return x + y + return x + + +example_args = (torch.randn(2, 3),) +tags = {"python.object-model"} +support_level = SupportLevel.SUPPORTED +model = OptionalInput() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/pytree_flatten.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/pytree_flatten.py new file mode 100644 index 0000000000000000000000000000000000000000..804e73c5a6d58ad5b5be179bf67a5d5bc38c2e2b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/pytree_flatten.py @@ -0,0 +1,16 @@ +# mypy: allow-untyped-defs +import torch + +from torch.utils import _pytree as pytree + +class PytreeFlatten(torch.nn.Module): + """ + Pytree from PyTorch can be captured by TorchDynamo. + """ + + def forward(self, x): + y, _spec = pytree.tree_flatten(x) + return y[0] + 1 + +example_args = ({1: torch.randn(3, 2), 2: torch.randn(3, 2)},), +model = PytreeFlatten() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/scalar_output.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/scalar_output.py new file mode 100644 index 0000000000000000000000000000000000000000..86d3b4645330c47c3625736b695d635f4ab58c70 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/scalar_output.py @@ -0,0 +1,23 @@ +# mypy: allow-untyped-defs +import torch + +from torch.export import Dim + +x = torch.randn(3, 2) +dim1_x = Dim("dim1_x") + +class ScalarOutput(torch.nn.Module): + """ + Returning scalar values from the graph is supported, in addition to Tensor + outputs. Symbolic shapes are captured and rank is specialized. + """ + def __init__(self) -> None: + super().__init__() + + def forward(self, x): + return x.shape[1] + 1 + +example_args = (x,) +tags = {"torch.dynamic-shape"} +dynamic_shapes = {"x": {1: dim1_x}} +model = ScalarOutput() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/specialized_attribute.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/specialized_attribute.py new file mode 100644 index 0000000000000000000000000000000000000000..f17092f9afc681b91a982a8a2479ac1dde4f455d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/specialized_attribute.py @@ -0,0 +1,26 @@ +# mypy: allow-untyped-defs +from enum import Enum + +import torch + +class Animal(Enum): + COW = "moo" + +class SpecializedAttribute(torch.nn.Module): + """ + Model attributes are specialized. + """ + + def __init__(self) -> None: + super().__init__() + self.a = "moo" + self.b = 4 + + def forward(self, x): + if self.a == Animal.COW.value: + return x * x + self.b + else: + raise ValueError("bad") + +example_args = (torch.randn(3, 2),) +model = SpecializedAttribute() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/static_for_loop.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/static_for_loop.py new file mode 100644 index 0000000000000000000000000000000000000000..aa62b86d16d9b6a1c539976a891f58bd732ae30d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/static_for_loop.py @@ -0,0 +1,16 @@ +# mypy: allow-untyped-defs +import torch + +class StaticForLoop(torch.nn.Module): + """ + A for loop with constant number of iterations should be unrolled in the exported graph. + """ + + def forward(self, x): + # constant + ret = [i + x for i in range(10)] + return ret + +example_args = (torch.randn(3, 2),) +tags = {"python.control-flow"} +model = StaticForLoop() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/static_if.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/static_if.py new file mode 100644 index 0000000000000000000000000000000000000000..f169380159a45489142ce5ae3523b2e4504c6721 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/static_if.py @@ -0,0 +1,18 @@ +# mypy: allow-untyped-defs +import torch + +class StaticIf(torch.nn.Module): + """ + `if` statement with static predicate value should be traced through with the + taken branch. + """ + + def forward(self, x): + if len(x.shape) == 3: + return x + torch.ones(1, 1, 1) + + return x + +example_args = (torch.randn(3, 2, 2),) +tags = {"python.control-flow"} +model = StaticIf() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/tensor_setattr.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/tensor_setattr.py new file mode 100644 index 0000000000000000000000000000000000000000..8fbc263e7ff2240a3cf8618c56f152e744aa40e8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/tensor_setattr.py @@ -0,0 +1,15 @@ +# mypy: allow-untyped-defs +import torch + + +class TensorSetattr(torch.nn.Module): + """ + setattr() call onto tensors is not supported. + """ + def forward(self, x, attr): + setattr(x, attr, torch.randn(3, 2)) + return x + 4 + +example_args = (torch.randn(3, 2), "attr") +tags = {"python.builtin"} +model = TensorSetattr() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/type_reflection_method.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/type_reflection_method.py new file mode 100644 index 0000000000000000000000000000000000000000..99ad42a153c512d65aaae1bcac2377ee1e124f25 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/type_reflection_method.py @@ -0,0 +1,22 @@ +# mypy: allow-untyped-defs +import torch + +class A: + @classmethod + def func(cls, x): + return 1 + x + +class TypeReflectionMethod(torch.nn.Module): + """ + type() calls on custom objects followed by attribute accesses are not allowed + due to its overly dynamic nature. + """ + + def forward(self, x): + a = A() + return type(a).func(x) + + +example_args = (torch.randn(3, 4),) +tags = {"python.builtin"} +model = TypeReflectionMethod() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/unsupported_operator.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/unsupported_operator.py new file mode 100644 index 0000000000000000000000000000000000000000..ba5475c5f15fe35c9ba2be57f295f568602b82c5 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/unsupported_operator.py @@ -0,0 +1,18 @@ +# mypy: allow-untyped-defs +import torch +from torch._export.db.case import SupportLevel + + +class TorchSymMin(torch.nn.Module): + """ + torch.sym_min operator is supported in export. + """ + + def forward(self, x): + return x.sum() + torch.sym_min(x.size(0), 100) + + +example_args = (torch.randn(3, 2),) +tags = {"torch.operator"} +support_level = SupportLevel.SUPPORTED +model = TorchSymMin() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/user_input_mutation.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/user_input_mutation.py new file mode 100644 index 0000000000000000000000000000000000000000..3156b3a1bf2ec6f6361395de3dacb098ecf20c3a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/examples/user_input_mutation.py @@ -0,0 +1,17 @@ +# mypy: allow-untyped-defs +import torch + + +class UserInputMutation(torch.nn.Module): + """ + Directly mutate user input in forward + """ + + def forward(self, x): + x.mul_(2) + return x.cos() + + +example_args = (torch.randn(3, 2),) +tags = {"torch.mutation"} +model = UserInputMutation() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/gen_example.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/gen_example.py new file mode 100644 index 0000000000000000000000000000000000000000..5c422f457bc3c7c1d7bc5d17dd4e93b8558a5de4 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/gen_example.py @@ -0,0 +1,23 @@ +import os +import sys + +import torch._export.db.examples as examples + +TEMPLATE = '''import torch + +def {case_name}(x): + """ + """ + + return +''' + +if __name__ == "__main__": + if len(sys.argv) != 2: + raise AssertionError(f"expected 2 arguments, got {len(sys.argv)}") + root_dir = examples.__name__.replace(".", "/") + if not os.path.exists(root_dir): + raise AssertionError(f"root_dir does not exist: {root_dir}") + with open(os.path.join(root_dir, sys.argv[1] + ".py"), "w") as f: + print("Writing to", f.name, "...") + f.write(TEMPLATE.format(case_name=sys.argv[1])) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/logging.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/logging.py new file mode 100644 index 0000000000000000000000000000000000000000..1416b1e99c2d163bcceced1ef827a5292bed0565 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/db/logging.py @@ -0,0 +1,46 @@ + +def exportdb_error_message(case_name: str) -> str: + from .examples import all_examples + from torch._utils_internal import log_export_usage + + ALL_EXAMPLES = all_examples() + # Detect whether case_name is really registered in exportdb. + if case_name in ALL_EXAMPLES: + url_case_name = case_name.replace("_", "-") + return f"See {case_name} in exportdb for unsupported case. \ + https://pytorch.org/docs/main/generated/exportdb/index.html#{url_case_name}" + else: + log_export_usage( + event="export.error.casenotregistered", + message=case_name, + ) + return f"{case_name} is unsupported." + + +def get_class_if_classified_error(e: Exception) -> str | None: + """ + Returns a string case name if the export error e is classified. + Returns None otherwise. + """ + + from torch._dynamo.exc import TorchRuntimeError, Unsupported, UserError + + ALWAYS_CLASSIFIED = "always_classified" + DEFAULT_CLASS_SIGIL = "case_name" + + # add error types that should be classified, along with any attribute name + # whose presence acts like a sigil to further distinguish which errors of + # that type should be classified. If the attribute name is None, then the + # error type is always classified. + _ALLOW_LIST = { + Unsupported: DEFAULT_CLASS_SIGIL, + UserError: DEFAULT_CLASS_SIGIL, + TorchRuntimeError: None, + } + if type(e) in _ALLOW_LIST: + # pyrefly: ignore [bad-index, index-error] + attr_name = _ALLOW_LIST[type(e)] + if attr_name is None: + return ALWAYS_CLASSIFIED + return getattr(e, attr_name, None) + return None diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/error.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/error.py new file mode 100644 index 0000000000000000000000000000000000000000..03b7f52fb9de435b9e58fa4a0bb141cc191e84c5 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/error.py @@ -0,0 +1,56 @@ +from enum import Enum + + +class ExportErrorType(Enum): + # User providing invalid inputs to either tracer, or other public facing APIs + INVALID_INPUT_TYPE = 1 + + # User returning values from their models that we don't support. + INVALID_OUTPUT_TYPE = 2 + + # Generated IR does not conform to Export IR Specification. + VIOLATION_OF_SPEC = 3 + + # User's code contains types and functionalities we don't support. + NOT_SUPPORTED = 4 + + # User's code didn't provide necessary details for us to successfully trace and export. + # For example, we use a lot of decorators and ask users to annotate their model. + MISSING_PROPERTY = 5 + + # User is using an API without proper initialization step. + UNINITIALIZED = 6 + + +def internal_assert(pred: bool, assert_msg: str) -> None: + """ + This is exir's custom assert method. It internally just throws InternalError. + Note that the sole purpose is to throw our own error while maintaining similar syntax + as python assert. + """ + + if not pred: + raise InternalError(assert_msg) + + +class InternalError(Exception): + """ + Raised when an internal invariance is violated in EXIR stack. + Should hint users to report a bug to dev and expose the original + error message. + """ + + def __init__(self, message: str) -> None: + super().__init__(message) + + +class ExportError(Exception): + """ + This type of exception is raised for errors that are directly caused by the user + code. In general, user errors happen during model authoring, tracing, using our public + facing APIs, and writing graph passes. + """ + + def __init__(self, error_code: ExportErrorType, message: str) -> None: + prefix = f"[{error_code}]: " + super().__init__(prefix + message) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/non_strict_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/non_strict_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..2f1aae39d0a1ab866d1d188cdb92112b832dc1f0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/non_strict_utils.py @@ -0,0 +1,1170 @@ +# mypy: allow-untyped-defs +import builtins +import contextlib +import functools +import inspect +import logging +import math +import sys +from collections import defaultdict +from collections.abc import Callable, Sequence +from contextlib import contextmanager +from typing import Any, TYPE_CHECKING, TypeGuard + +import torch +import torch.utils._pytree as pytree +from torch._dynamo.source import ( + AttrSource, + GetItemSource, + LocalSource, + TensorProperty, + TensorPropertySource, +) +from torch._dynamo.variables.builder import TrackedFake +from torch._export.passes.lift_constants_pass import ConstantAttrMap +from torch._export.utils import _fakify_params_buffers +from torch._guards import Source +from torch._library.fake_class_registry import FakeScriptObject +from torch._library.opaque_object import is_opaque_value +from torch._opaque_base import OpaqueBase +from torch._subclasses.fake_tensor import FakeTensorMode +from torch.export import Constraint +from torch.export.dynamic_shapes import ( + _check_dynamic_shapes, + _combine_args, + _DimHint, + _DimHintType, + _IntWrapper, + _process_dynamic_shapes, + _RelaxedConstraint, + _tree_map_with_path, +) +from torch.export.graph_signature import CustomObjArgument +from torch.fx.experimental import _config as config +from torch.fx.experimental.symbolic_shapes import ( + _find_user_code_frame, + _suggest_fixes_for_data_dependent_error_non_strict, + ConstraintViolationError, + DimDynamic, + EqualityConstraint, + GuardOnDataDependentSymNode, + RelaxedUnspecConstraint, + ShapeEnv, + StatelessSymbolicContext, + SymIntSymbolicContext, + ValueRanges, +) +from torch.utils._pytree import ( + GetAttrKey, + KeyPath, + MappingKey, + SequenceKey, + tree_map_with_path, +) +from torch.utils._sympy.numbers import int_oo + + +if TYPE_CHECKING: + from sympy import Symbol + + +log = logging.getLogger(__name__) + + +class _KeyPath: + """ + Wraps `KeyPath` to aid `isinstance` checks. + """ + + def __init__(self, kp: KeyPath): + self.kp = kp + + +class _KeyPathTrie: + """ + Builds a trie of `KeyPath` prefixes mapping to `Source` leaves. + """ + + def __init__(self): + self.root = {} + + def add(self, kp: KeyPath, src: Source): + if len(kp) == 0: + raise AssertionError("KeyPath must not be empty") + *path, leaf = kp + node = self.root + for k in path: + if k not in node: + node[k] = {} + node = node[k] + node[leaf] = src + + def get(self, kp: KeyPath) -> tuple[Source, KeyPath]: + node = self.root + # pyrefly: ignore [bad-assignment] + while not isinstance(node, Source): + if len(kp) == 0: + raise AssertionError("KeyPath exhausted before reaching Source") + k, *kp = kp # type: ignore[assignment] + node = node[k] + # pyrefly: ignore [bad-return] + return node, kp + + +def make_sourced_prefixes(nn_module, args, kwargs) -> _KeyPathTrie: + kp_args, kp_kwargs = tree_map_with_path( + lambda kp, _: _KeyPath(kp), + (tuple(None for _ in args), {k: None for k in kwargs}), # noqa: C420 + ) + kp_combined_args = _combine_args(nn_module, kp_args, kp_kwargs) + + sourced_prefixes = _KeyPathTrie() + for name, struct in kp_combined_args.items(): + src = LocalSource(name) + + if isinstance(struct, _KeyPath): + sourced_prefixes.add(struct.kp, src) + elif isinstance(struct, tuple): + for i, prefix in enumerate(struct): + if not isinstance(prefix, _KeyPath): + raise AssertionError(f"expected _KeyPath, got {type(prefix)}") + sourced_prefixes.add(prefix.kp, GetItemSource(src, i)) + elif isinstance(struct, dict): + for k, prefix in struct.items(): + if not isinstance(prefix, _KeyPath): + raise AssertionError(f"expected _KeyPath, got {type(prefix)}") + sourced_prefixes.add(prefix.kp, GetItemSource(src, k)) + + return sourced_prefixes + + +def key_path_to_source( + kp: KeyPath, sourced_prefixes: _KeyPathTrie | None = None +) -> Source: + """ + Given a key path, return the source for the key path. + """ + if sourced_prefixes is None: + source: Source = LocalSource("args") + else: + source, kp = sourced_prefixes.get(kp) + + for k in kp: + if isinstance(k, SequenceKey): + source = GetItemSource(source, k.idx) + elif isinstance(k, MappingKey): + source = GetItemSource(source, k.key) + elif isinstance(k, GetAttrKey): + source = AttrSource(source, k.name) + else: + raise ValueError(f"Unknown KeyEntry {k}") + + return source + + +def _is_constant_argument(t): + return t is None or isinstance(t, (float, bool, str)) + + +def fakify( + mode: FakeTensorMode, + kp: KeyPath, + t: Any, + t_constraints: dict[int, dict[int, Constraint]], + sources: dict[tuple[int, int], list[Source]], + sourced_prefixes: _KeyPathTrie | None = None, +): + source = key_path_to_source(kp, sourced_prefixes=sourced_prefixes) + if ( + _is_constant_argument(t) + or isinstance(t, (torch.ScriptObject, torch.nn.Module)) + or is_opaque_value(t) + ): + return t + + if isinstance(t, _IntWrapper): + if t.dynamism is not None and t.dynamism.type in ( # type: ignore[union-attr] + _DimHintType.DYNAMIC, + _DimHintType.AUTO, + ): + symint = mode.shape_env.create_unspecified_symint_and_symbol( # type: ignore[union-attr] + t.val, source, DimDynamic.DYNAMIC + ) + context = ( + SymIntSymbolicContext( + constraint=RelaxedUnspecConstraint(warn_only=False) + ) + if t.dynamism.type == _DimHintType.DYNAMIC # type: ignore[union-attr] + else None + ) + mode.shape_env.tracked_fakes.append( # type: ignore[union-attr] + TrackedFake(symint, source, context) + ) + return symint + else: + return t.val + + if not isinstance(t, torch.Tensor): + raise ValueError( + f"Unsupported input type {type(t)}. " + "Export only supports pytree containers of basic types (Tensor, int, float, ...) as input. " + "To register a custom dataclass, use torch.export.register_dataclass. " + "To register a custom container type, use torch.utils._pytree.register_pytree_node. " + "To register a constant input, use torch.utils._pytree.register_constant" + ) + + # Create symbolic context (handles subclass recursion internally) + symbolic_context = _create_symbolic_context_for_tensor( + t, source, t_constraints, sources, mode + ) + + fake = mode.from_tensor(t, source=source, symbolic_context=symbolic_context) + mode.shape_env.tracked_fakes.append(TrackedFake(fake, source, symbolic_context)) # type: ignore[union-attr] + return fake + + +def _create_symbolic_context_for_tensor(t, source, t_constraints, sources, mode): + """Helper function to create symbolic context for a tensor.""" + from torch._dynamo.source import AttrSource + from torch.fx.experimental.symbolic_shapes import ( + DimDynamic, + RelaxedUnspecConstraint, + SubclassSymbolicContext, + ) + from torch.utils._python_dispatch import is_traceable_wrapper_subclass + + # Common dynamic dimension logic for both regular tensors and subclasses + n_dims = len(t.shape) + dynamic_sizes = [] + constraint_sizes = [None] * n_dims + + for i in range(n_dims): + if i in getattr(t, "_dynamo_weak_dynamic_indices", {}): + dynamic_sizes.append(DimDynamic.DYNAMIC) + elif i in getattr(t, "_dynamo_dynamic_indices", {}): + # bit annoying, but we need to replicate process in _dynamo/variables/builder.py + # where a RelaxedUnspecConstraint is created for Dim.DYNAMIC, so constraint violations + # are raised when specializing. + dynamic_sizes.append(DimDynamic.DYNAMIC) + constraint_sizes[i] = RelaxedUnspecConstraint(warn_only=False) # type: ignore[call-overload] + else: + dynamic_sizes.append(DimDynamic.STATIC) + + # Handle nested subclasses + if is_traceable_wrapper_subclass(t): + # Get inner contexts recursively + inner_contexts = {} + attrs, _ = type(t).__tensor_flatten__(t) + + # Propagate outer tensor constraints to inner tensors if not already present + for attr in attrs: + match getattr(t, attr): + case torch.Tensor() as inner_value: + inner_source = AttrSource(source, attr) + inner_contexts[attr] = _create_symbolic_context_for_tensor( + inner_value, inner_source, t_constraints, sources, mode + ) + case OpaqueBase(): + pass + case unexpected: + raise AssertionError( + f"expected Tensor or OpaqueBase, got {type(unexpected)}" + ) + + symbolic_context = SubclassSymbolicContext( + dynamic_sizes=dynamic_sizes, + constraint_sizes=constraint_sizes, # type: ignore[arg-type] + view_base_context=None, + tensor_source=source, + shape_env_to_source_to_symbol_cache={}, + inner_contexts=inner_contexts, + ) + else: + symbolic_context: StatelessSymbolicContext = ( # type: ignore[no-redef] + StatelessSymbolicContext( + dynamic_sizes=dynamic_sizes, + constraint_sizes=constraint_sizes, # type: ignore[arg-type] + ) + ) + + # Apply constraints (common logic) + t_id = id(t) + if mode.shape_env is None: + raise AssertionError("mode.shape_env must not be None") + if t_id in t_constraints: + for i, constraint in t_constraints[t_id].items(): + src = TensorPropertySource(base=source, prop=TensorProperty.SIZE, idx=i) + sources[(t_id, i)].append(src) + if isinstance(constraint, _RelaxedConstraint): + continue + symbolic_context.constraint_sizes[i] = constraint.constraint_range + mode.shape_env.source_name_to_debug_name[src.name] = constraint.name # type: ignore[assignment] + + return symbolic_context + + +def _is_unbacked_symint(symbol): + if not isinstance(symbol, torch.SymInt): + return False + + return symbol.node.shape_env.is_unbacked_symint(symbol.node.expr) + + +def _tensor_min_max(*args, real_callable, tensor_callable, **kwargs): + """ + This logic is replicated from dynamo/variables/builtin.py + """ + if len(args) == 2 and not kwargs: + arg1, arg2 = args + + # Case 1: Both are tensors + if isinstance(arg1, torch.Tensor) and isinstance(arg2, torch.Tensor): + return tensor_callable(arg1, arg2) + + # Case 2: One tensor, one scalar + elif isinstance(arg1, torch.Tensor) or isinstance(arg2, torch.Tensor): + if not isinstance(arg1, torch.Tensor): + arg1, arg2 = arg2, arg1 + + if isinstance(arg2, (int, float)): + kwarg = {"min" if tensor_callable is torch.maximum else "max": arg2} + return torch.clamp(arg1, **kwarg) # type: ignore[call-overload] + else: + return real_callable(arg1, arg2) + + # Case 3: SymInts + elif isinstance(arg1, torch.SymInt) or isinstance(arg2, torch.SymInt): + return ( + torch.sym_max(arg1, arg2) + if tensor_callable is torch.maximum + else torch.sym_min(arg1, arg2) + ) + + # Fallback + else: + return real_callable(arg1, arg2) + + # Single iterable argument handling + if len(args) == 1 and not kwargs: + iterable = args[0] + + if isinstance(iterable, torch.Tensor): + return tensor_callable(iterable) + try: + iterator = iter(iterable) + except TypeError: + pass + else: + items = list(iterator) + if not items: + raise ValueError(f"{real_callable.__name__}() arg is an empty sequence") + + return functools.reduce( + lambda a, b: _tensor_min_max( + a, b, real_callable=real_callable, tensor_callable=tensor_callable + ), + items, + ) + + # Fallback to original callable + return real_callable(*args, **kwargs) + + +@contextmanager +def _override_builtin_ops(): + original_max = builtins.max + original_min = builtins.min + original_pow = math.pow + + # pyrefly: ignore [bad-assignment] + builtins.max = functools.partial( + _tensor_min_max, real_callable=original_max, tensor_callable=torch.maximum + ) + + # pyrefly: ignore [bad-assignment] + builtins.min = functools.partial( + _tensor_min_max, real_callable=original_min, tensor_callable=torch.minimum + ) + + math.pow = lambda x, y: x**y # type: ignore[operator] + + try: + yield + finally: + builtins.max = original_max + builtins.min = original_min + math.pow = original_pow + + +def make_fake_inputs( + nn_module, + args, + kwargs, + dynamic_shapes, + prefer_deferred_runtime_asserts_over_guards=False, +): + """ + Given an nn module, example inputs, and constraints, return a new fake mode, + fake inputs created in that mode whose dynamic shape dimensions are constrained + by the given ranges, and sources for pairs of dynamic shape dimensions that are + constrained to be equal. + """ + # TODO(avik): refactor Dynamo to avoid duplication of the following code + # between non-strict and strict. + # Specifically, here (non-strict) we do the following pre-tracing steps: + # - Fakify inputs. + # - Process input shape equalities. + # In strict, these steps are spread across multiple files: + # - output_graph.py fakifies inputs. + # - [post-tracing] guards.py processes input shape equalities. + import torch._functorch.config as _config + + # Map ints to a wrapper structure to help us mark it as dynamic, if it is + # dynamic. We will unwrap ints in fakify later. + args, kwargs = pytree.tree_map_only(int, lambda a: _IntWrapper(a), (args, kwargs)) + + combined_args = _combine_args(nn_module, args, kwargs) + _check_dynamic_shapes(combined_args, dynamic_shapes) + constraints = _process_dynamic_shapes(combined_args, dynamic_shapes) + t_constraints: dict[int, dict[int, Constraint]] = defaultdict(dict) + for constraint in constraints: + t_constraints[constraint.t_id][constraint.dim] = constraint + + context = torch._guards.TracingContext.try_get() + if context is not None: + # This occurs when we are exporting within dynamo. There already exists + # a toplevel TracingContext with a fake mode, so we do not want to + # create another fake mode. + fake_mode = context.fake_mode + if fake_mode is None: + raise AssertionError("context.fake_mode must not be None") + else: + if isinstance(nn_module.forward, functools.partial): + # functools handles nesting by itself, no need to recurse + code = nn_module.forward.func.__code__ + elif ( + sys.version_info >= (3, 14) + and (fwd := getattr(nn_module.forward, "__func__", None)) + and isinstance(fwd, functools.partial) + ): + # functools.partial is now a method descriptor: + # https://docs.python.org/3/whatsnew/3.14.html#changes-in-the-python-api + code = fwd.func.__code__ + else: + code = nn_module.forward.__code__ + co_fields = { + "co_name": code.co_name, + "co_filename": code.co_filename, + "co_firstlineno": code.co_firstlineno, + } + with _config.patch(fake_tensor_allow_unsafe_data_ptr_access=False): + fake_mode = FakeTensorMode( + shape_env=ShapeEnv( + tracked_fakes=[], + co_fields=co_fields, + prefer_deferred_runtime_asserts_over_guards=prefer_deferred_runtime_asserts_over_guards, + trace_asserts=True, + ), + allow_non_fake_inputs=True, + export=True, + ) + if fake_mode.shape_env is None or fake_mode.shape_env.tracked_fakes is None: + raise ValueError( + "Detected fake_mode does not have a shape_env with tracked fakes. " + "If you constructed the module under a FakeTensorMode, " + "please initialize it like: FakeTensorMode(shape_env=ShapeEnv(tracked_fakes=[]))" + ) + + with fake_mode: + original_signature = inspect.signature(nn_module.forward) + sources: dict[tuple[int, int], list[Source]] = defaultdict(list) + sourced_prefixes = make_sourced_prefixes(nn_module, args, kwargs) + fake_args, fake_kwargs = tree_map_with_path( + lambda kp, val: fakify( + fake_mode, + kp, + val, + t_constraints, + sources, + sourced_prefixes=sourced_prefixes, + ), + (args, kwargs), + ) + + names: dict[str, tuple[int, int]] = {} + source_pairs: list[tuple[Source, Source]] = [] + derived_equalities: list[tuple[Source, Source | Symbol, Callable]] = [] + phantom_symbols: dict[str, Symbol] = {} + relaxed_sources: set[Source] = set() + for constraint in constraints: + torch.export.dynamic_shapes._process_equalities( + constraint, + lambda t_id, dim: sources[(t_id, dim)], + fake_mode.shape_env, + names, + source_pairs, + derived_equalities, + phantom_symbols, + relaxed_sources, + ) + + equalities_inputs = EqualityConstraint( + source_pairs=source_pairs, + derived_equalities=derived_equalities, + phantom_symbols=list(phantom_symbols.values()), + relaxed_sources=relaxed_sources, + warn_only=False, + ) + return ( + fake_mode, + fake_args, + fake_kwargs, + equalities_inputs, + original_signature, + dynamic_shapes, + ) + + +def _flatten_dynamic_shapes( + combined_args: dict[str, Any], + dynamic_shapes: dict[str, Any] | tuple[Any] | list[Any], +) -> list[Any]: + flat_shapes = [] + + def _tree_map_helper(path, t, shape): + nonlocal flat_shapes + flat_shapes.append(shape) + + _tree_map_with_path(_tree_map_helper, combined_args, dynamic_shapes) + return flat_shapes + + +def _clean_dynamic_markers(tensor: torch.Tensor) -> None: + for attr in [ + "_dynamo_weak_dynamic_indices", + "_dynamo_dynamic_indices", + "_dynamo_dynamic_range", + "_dynamo_static_indices", + "_dynamo_unbacked_indices", + ]: + if hasattr(tensor, attr): + delattr(tensor, attr) + + +def produce_guards_and_solve_constraints( + fake_mode: FakeTensorMode, + gm: torch.fx.GraphModule, + dynamic_shapes: dict[str, Any] | tuple[Any] | list[Any] | None, + equalities_inputs: EqualityConstraint, + original_signature: inspect.Signature, +): + """ + Given a fake mode, sources pairs corresponding to equal dynamic shape dimensions, + and a graph module, produce guards on the fake mode's shape env (raising constraint + violations if any), solve (to suggest simplifications or fixes). + Dynamo already performs this, so this is for non-strict mode. + + Additional inputs: + equalities_inputs: the equality constraints to use for guards + original_signature: the signature of the forward method + """ + shape_env = fake_mode.shape_env + if shape_env is None: + raise AssertionError("fake_mode.shape_env must not be None") + if shape_env.tracked_fakes is None: + raise AssertionError("shape_env.tracked_fakes must not be None") + + placeholders = [tf.fake for tf in shape_env.tracked_fakes] + sources = [tf.source for tf in shape_env.tracked_fakes] + input_contexts = [tf.symbolic_context for tf in shape_env.tracked_fakes] + constraint_violation_error = None + try: + shape_env.produce_guards( + placeholders, + sources, + input_contexts=input_contexts, + equalities_inputs=equalities_inputs, + ignore_static=False, + ) + except ConstraintViolationError as e: + constraint_violation_error = e + + shape_env.frozen = True + dim_constraints = shape_env.dim_constraints + if dim_constraints is None: + # Expected when shape_env.produce_guards throws an early constraint violation error. + # There is nothing to solve for in this case. + # TODO(avik): Maybe record the constraint violation error instead and replay later? + if not constraint_violation_error: + raise AssertionError( + "expected constraint_violation_error when dim_constraints is None" + ) + raise constraint_violation_error + dim_constraints.solve() + forced_specializations = dim_constraints.forced_specializations() + + msg = dim_constraints.prettify_results( + original_signature, + dynamic_shapes, # type: ignore[arg-type] + constraint_violation_error, + forced_specializations, # type: ignore[arg-type] + ) + + if constraint_violation_error: + if constraint_violation_error.args: + constraint_violation_error.args = ( + constraint_violation_error.args[0] + msg, + ) + else: + constraint_violation_error.args = (msg,) + elif forced_specializations: + constraint_violation_error = ConstraintViolationError(msg) + if constraint_violation_error: + raise constraint_violation_error + + +def is_int(x: object) -> TypeGuard[int | torch.SymInt]: + return isinstance(x, int) or (isinstance(x, torch.SymInt) and x.node.expr.is_number) + + +def _constrain_user_specified_dimhint_range( + symint: torch.SymInt, + hint: int, + dim: _DimHint, + range_constraints, + shape_env, + keypath: KeyPath, + i: int | None = None, +) -> str | None: + trace_vr = ( + range_constraints[symint.node.expr] + if not is_int(symint) + else ValueRanges(int(symint), int(symint)) + ) + + # warn on 0/1 specialization for Dim.AUTO; not an actual error + if dim.type == _DimHintType.AUTO and trace_vr.is_singleton() and hint in (0, 1): + pathstr = f"inputs{pytree.keystr(keypath)}" + if i is not None: + pathstr += f".shape[{i}]" + msg = ( + f"dimension {pathstr} 0/1 specialized; Dim.AUTO was specified along " + + f"with a sample input with hint = {hint}." + ) + log.warning(msg) + + try: + user_vr = ValueRanges( + lower=0 if dim.min is None else dim.min, + upper=int_oo if dim.max is None else dim.max, + ) + if is_int(symint): + out_vr = trace_vr & user_vr + else: + range_constraints[symint.node.expr] &= user_vr + shape_env.var_to_range[symint.node._expr] &= user_vr + out_vr = range_constraints[symint.node.expr] + + # check for Dim.DYNAMIC specializations; special case error message on 0/1 + if dim.type == _DimHintType.DYNAMIC and out_vr.is_singleton(): + path = f"inputs{pytree.keystr(keypath)}" + if i is not None: + path += f".shape[{i}]" + if ( + trace_vr.is_singleton() + and hint in (0, 1) + and not torch.fx.experimental._config.backed_size_oblivious + ): + msg = ( + f"- Received user-specified dim hint Dim.DYNAMIC(min={dim.min}, max={dim.max}), " + f"but export 0/1 specialized due to hint of {hint} for dimension {path}." + ) + else: + msg = ( + f"- Received user-specified dim hint Dim.DYNAMIC(min={dim.min}, max={dim.max}), " + f"but tracing inferred a static shape of {out_vr.lower} for dimension {path}." + ) + return msg + + except torch.utils._sympy.value_ranges.ValueRangeError: + path = f"inputs{pytree.keystr(keypath)}" + if i is not None: + path += f".shape[{i}]" + msg = ( + f"- Received user-specified min/max range of [{dim.min}, {dim.max}], " + f"conflicting with the inferred min/max range of [{trace_vr.lower}, {trace_vr.upper}], " + f"for {path}." + ) + return msg + + return None + + +def make_constraints( + fake_mode: FakeTensorMode, + gm: torch.fx.GraphModule, + combined_args: dict[str, Any], + dynamic_shapes: dict[str, Any] | tuple[Any] | list[Any] | None, + num_lifted_inputs: int, +): + """ + Given a fake mode's shape env and user-specified dynamic shapes, + return the resulting range constraints and equality constraints. + + Additional args: + num_lifted_inputs: the number of non-user-input placeholder nodes in the graph + (used only to enumerate the user-input nodes) + """ + + shape_env = fake_mode.shape_env + if shape_env is None: + raise AssertionError("fake_mode.shape_env must not be None") + inline_constraints = gm.meta.get("inline_constraints", []) + range_constraints = defaultdict(lambda: ValueRanges(0, int_oo)) | inline_constraints + if not dynamic_shapes: + return dict(range_constraints) + + # clean up dynamic markers from tensors + flat_paths, flat_args = zip(*pytree.tree_flatten_with_path(combined_args)[0]) + for arg in flat_args: + if isinstance(arg, torch.Tensor): + _clean_dynamic_markers(arg) + + # get individual dynamic shapes spec for each input + if not isinstance(dynamic_shapes, dict): + if not isinstance(dynamic_shapes, (tuple, list)): + raise AssertionError( + f"expected dict, tuple, or list for dynamic_shapes, got {type(dynamic_shapes)}" + ) + combined_args = type(dynamic_shapes)(combined_args.values()) # type: ignore[assignment, misc] + flat_dynamic_shapes = _flatten_dynamic_shapes(combined_args, dynamic_shapes) + + # check number of shapes vs. number of inputs + num_placeholders = [node.op == "placeholder" for node in gm.graph.nodes].count(True) + if len(flat_dynamic_shapes) != num_placeholders - num_lifted_inputs: + raise AssertionError( + f"expected {num_placeholders - num_lifted_inputs} shapes, got {len(flat_dynamic_shapes)}" + ) + + free_symbols = set() + range_violations = [] + for input_index, node in enumerate(gm.graph.nodes): + meta_val = node.meta.get("val") + + if ( + input_index < num_lifted_inputs + or node.op != "placeholder" + or meta_val is None + ): + continue + + elif _is_constant_argument(meta_val) or isinstance(meta_val, CustomObjArgument): + continue + + shape_spec = flat_dynamic_shapes[input_index - num_lifted_inputs] + keypath = flat_paths[input_index - num_lifted_inputs] + flat_arg = flat_args[input_index - num_lifted_inputs] + + if isinstance(meta_val, int) or ( + isinstance(meta_val, torch.SymInt) and meta_val.node.expr.is_number + ): + pass + + elif isinstance(meta_val, torch.SymInt): + if shape_spec is not None and isinstance(shape_spec, _DimHint): + hint = flat_arg + range_constraints[meta_val.node.expr] &= shape_env.bound_sympy( + meta_val.node._expr + ) + violation = _constrain_user_specified_dimhint_range( + meta_val, + hint, + shape_spec, + range_constraints, + shape_env, + keypath, + None, + ) + if violation: + range_violations.append(violation) + else: + raise RuntimeError("nyi") + free_symbols.update(meta_val.node.expr.free_symbols) + + elif isinstance(meta_val, torch.Tensor): + for i, d in enumerate(node.meta["val"].shape): + dim = None + if isinstance(shape_spec, (list, tuple)): + dim = shape_spec[i] + elif isinstance(shape_spec, dict): + dim = shape_spec.get(i) + if not is_int(d): + # Compute the range constraint for the symbolic expression corresponding + # to this shape dimension and store it. + if dim is None or isinstance(dim, _DimHint): + range_constraints[d.node.expr] &= shape_env.bound_sympy( + d.node.expr + ) + else: + range_constraints[d.node.expr] &= ValueRanges( + lower=dim.min, upper=dim.max + ) + + free_symbols.update(d.node.expr.free_symbols) + + # check user-specified min/max range for DimHints; + # we might want to do this even if model tracing inferred a static dimension. + if isinstance(dim, _DimHint): + hint = flat_arg.shape[i] + violation = _constrain_user_specified_dimhint_range( + d, hint, dim, range_constraints, shape_env, keypath, i + ) + if violation: + range_violations.append(violation) + else: + raise RuntimeError(f"Unfamiliar meta val: {meta_val}") + + if range_violations: + prefix = "Found the following conflicts between user-specified ranges and inferred ranges from model tracing:\n" + raise ValueError(prefix + "\n".join(range_violations)) + + for symbol in free_symbols: + if symbol not in range_constraints: + # Placeholders can have symbolic shapes that are derived expressions. + # The above code will record direct range constraints for them + # so that we can do runtime assertions. In addition, for serde checks + # we want to record range constraints for their root symbols. + range_constraints[symbol] = shape_env.var_to_range[symbol] + + return dict(range_constraints) + + +def _gather_constant_attrs(m: torch.nn.Module) -> ConstantAttrMap: + """Search the module hierarchy, gathering up all tensor and ScriptObject constants. + + Returns a dictionary mapping hash(value) to the name of the constant. We + have to abuse `hash` here unfortunately, see: [ScriptObject hash]. + """ + constants = ConstantAttrMap() + buffers_parameters = set(m.buffers()) + buffers_parameters.update(m.parameters()) + + def inner(m: torch.nn.Module, prefix_atoms: list[str], constants): + for k, v in m.__dict__.items(): + if isinstance( + v, + ( + torch.Tensor, + torch.ScriptObject, + FakeScriptObject, + ), + ): + if v in buffers_parameters: + # filter out buffers and parameters, leaving only constants + continue + + fqn = ".".join(prefix_atoms + [k]) + constants.add(v, fqn) + for k, v in m.named_children(): + inner(v, prefix_atoms + [k], constants) + + inner(m, [], constants) + return constants + + +def _get_graph_inputs_of_type_nn_module( + args: tuple[tuple[Any], dict[Any, Any]] | None, +) -> set[type[torch.nn.Module]]: + if args is None: + return set() + module_types = set() + for arg in pytree.tree_leaves(args): + if isinstance(arg, torch.nn.Module): + module_types.add(type(arg)) + return module_types + + +def _enter_enable_graph_inputs_of_type_nn_module( + module_types: set[type[torch.nn.Module]], +) -> None: + for t in module_types: + torch._export.utils.register_module_as_pytree_input_node(t) + + +def _exit_enable_graph_inputs_of_type_nn_module( + module_types: set[type[torch.nn.Module]], +) -> None: + for t in module_types: + torch._export.utils.deregister_module_as_pytree_input_node(t) + + +@contextlib.contextmanager +def _enable_graph_inputs_of_type_nn_module( + args: tuple[tuple[Any], dict[Any, Any]] | None, +): + if args is None: + yield + return + + module_types = _get_graph_inputs_of_type_nn_module(args) + _enter_enable_graph_inputs_of_type_nn_module(module_types) + try: + yield + finally: + _exit_enable_graph_inputs_of_type_nn_module(module_types) + + +@contextlib.contextmanager +def _fakify_module_inputs( + args: tuple[Any], + kwargs: dict[Any, Any], + fake_mode: torch._subclasses.fake_tensor.FakeTensorMode, +): + # This context manager is used to fakify module inputs. + # Inputs: + # args, kwargs: the args and kwargs containing module inputs that haven't been fakified. + # fake_mode: the fake mode to be used for fakifying script objects. It's the same mode that fakify input tensors. + + ctxs = [_enable_graph_inputs_of_type_nn_module((args, kwargs))] + for arg in pytree.tree_leaves((args, kwargs)): + if isinstance(arg, torch.nn.Module): + fake_params_buffers = _fakify_params_buffers(fake_mode, arg) + ctxs.append( + torch.nn.utils.stateless._reparametrize_module( + arg, + fake_params_buffers, + tie_weights=True, + strict=True, + stack_weights=True, + ) + ) + with contextlib.ExitStack() as stack: + for ctx in ctxs: + stack.enter_context(ctx) + yield + + +@contextlib.contextmanager +def _fakify_script_objects( + mod: torch.nn.Module, + args: Sequence[Any], + kwargs: dict[Any, Any], + fake_mode: torch._subclasses.fake_tensor.FakeTensorMode | None, +): + # This context manager is used to fakify script objects into FakeScriptObject. + # Inputs: + # mod: the module to be exported, it (and its recursive submodules)'s script object attrs haven't been fakified. + # args, kwargs: the args and kwargs inputs for mod, script object inputs haven't been fakified. + # fake_mode: the fake mode to be used for fakifying script objects. It's the same mode that fakify input tensors. + # + # Returns: + # mod: the patched module, its (and its recursive submodules) script object attrs have been fakified. + # fake_args, fake_kwargs: new fakified args and kwargs. + # Script object inputs have been fakified. Don't touch the tensors. + # fake_constant_attrs: a new map from FakeScriptObject to the fqn of the original script object. + # fake_to_real: a mapping between FakeScriptObject and the original script object in order to un-do the patching. + + constant_attrs: ConstantAttrMap = _gather_constant_attrs(mod) + if any(isinstance(obj, FakeScriptObject) for obj in constant_attrs.values()): + raise AssertionError("Mod shouldn't contain any FakeScriptObject.") + if pytree.tree_any(lambda obj: isinstance(obj, FakeScriptObject), (args, kwargs)): + raise AssertionError("args and kwargs shouldn't contain any FakeScriptObject.") + + patched_attr = {} + fake_constant_attrs = ConstantAttrMap() + fake_to_real = {} + + def _maybe_fakify_obj(obj): + fake_obj = torch._library.fake_class_registry.maybe_to_fake_obj(fake_mode, obj) + fake_to_real[fake_obj] = obj + return fake_obj + + def _leaf_mod_and_attr( + mod: torch.nn.Module, attr_fqn: str + ) -> tuple[torch.nn.Module, str]: + *prefix_attr, last_attr = attr_fqn.split(".") + cur_mod = mod + for attr in prefix_attr: + cur_mod = getattr(cur_mod, attr) + return cur_mod, last_attr + + try: + for obj, fqns in constant_attrs.items(): + if torch._library.fake_class_registry._is_script_object( + obj + ) or is_opaque_value(obj): + fake_script_obj = _maybe_fakify_obj(obj) + for fqn in fqns: + cur_mod, attr = _leaf_mod_and_attr(mod, fqn) + if obj is not getattr(cur_mod, attr): + raise AssertionError( + f"obj mismatch at {fqn}: expected {obj}, got {getattr(cur_mod, attr)}" + ) + setattr(cur_mod, attr, fake_script_obj) + fake_constant_attrs.add(fake_script_obj, fqn) + patched_attr[fqn] = obj + else: + for fqn in fqns: + fake_constant_attrs.add(obj, fqn) + + fake_args, fake_kwargs = pytree.tree_map_only( + torch.ScriptObject, _maybe_fakify_obj, (args, kwargs) + ) + yield (mod, fake_args, fake_kwargs, fake_constant_attrs, fake_to_real) + finally: + for fqn, orig_obj in patched_attr.items(): + cur_mod, attr = _leaf_mod_and_attr(mod, fqn) + setattr(cur_mod, attr, orig_obj) + + +class _NonStrictTorchFunctionHandler(torch.overrides.TorchFunctionMode): + """ + 1. Handles data-dependent errors raised by torch function calls in non-strict. + + Any data-dependent error is due to some condition on unbacked symints + that cannot be resolved. A mechanical way of fixing the error is to use + a torch._check() call to assert either that condition or its negation. + The handler suggests these options as code and points to the location + of the torch function call that raised the error as part of the error + message shown to the user, who can then simply select and copy-paste + a suggested fix at that location. + + NOTE: Not all data-dependent errors are raised by torch function calls. + In particular, conditions on unbacked symints can appear outside such + calls, and as such are not handled here. + + 2. Overrides torch functions that are known to cause problems in non-strict. + + Certain Python features, such as indexing/slicing, cannot be intercepted + in non-strict. Likewise, certain legacy ops, such as distributed collectives, + may need to be mapped to other ops. When there is special handling in Dynamo + for such things, tracing can fail in non-strict (while succeeding in strict). + Fortunately, redirecting to other torch functions can often fix such issues. + + 3. Handles line-of-code logging for each torch function call in non-strict. + + Usage: TORCHEXPORT_EXTENDED_DEBUG_CURRENT_LOC=1 TORCH_LOGS="+export" ... + """ + + def _override(self, func, args, kwargs): + if torch.distributed.is_available(): + from torch.distributed._functional_collectives import ( + REDUCE_OP_TO_STR, + traceable_collective_remaps, + ) + + if func in traceable_collective_remaps: + # Redirect to a corresponding functional collective, following Dynamo. + # See torch/distributed/_functional_collectives.py for details. + # The following is an adaptation of CollectiveFunctionRewriteVariable. + mapped_func = traceable_collective_remaps[func] + signature = inspect.signature(func) + kwargs = dict(signature.bind(*args, **kwargs).arguments) + args = () + if func in ( + torch.distributed.all_reduce, + torch.distributed.reduce_scatter_tensor, + torch.distributed._reduce_scatter_base, + ): + if "op" in kwargs: + kwargs["op"] = REDUCE_OP_TO_STR[kwargs["op"]] + return mapped_func, args, kwargs + if func is torch.tensor: + # Redirect to Python implementation of torch.tensor for data with symints. + # NOTE(avik): We don't unconditionally redirect to this implementation + # because it has some known incompletenesses, e.g., it doesn't support + # empty data. See https://github.com/pytorch/pytorch/issues/143216 + if any( + isinstance(a, (torch.SymInt, torch.SymFloat, torch.SymBool)) + for a in pytree.tree_flatten(args[0])[0] + ): + return torch._refs.tensor, args, kwargs + if func.__name__ == "__getitem__" and isinstance(args[0], torch.Tensor): + + def rewrite(dim, item): + # Redirect to torch.select for indexing. + if item is None: + return dim + 1, (torch.unsqueeze, [dim]) + if isinstance(item, (int, torch.SymInt)): + return dim, (torch.select, [dim, item]) + # Redirect to torch.ops.aten.slice for slicing. + if isinstance(item, slice): + step = item.step or 1 + if item.start is None and item.stop is None and step == 1: + # no-op + return dim + 1, (lambda t: t, []) + return dim + 1, ( + torch.ops.aten.slice, + [dim, item.start, item.stop, step], + ) + # Otherwise do nothing. + + items = list(args[1]) if isinstance(args[1], tuple) else [args[1]] + + has_symint = False + index_ellipsis = None + t = args[0] + n_none_slices = t.ndim + 1 + for i, item in enumerate(items): + if isinstance(item, torch.SymInt) or ( + isinstance(item, slice) + and any( + isinstance(s, torch.SymInt) + for s in (item.start, item.stop, item.step) + ) + ): + has_symint = True + if item is Ellipsis: + index_ellipsis = i + if item is not None: + n_none_slices -= 1 + + # only rewrite when there are symints + if has_symint: + if index_ellipsis is not None: + none_slices = [slice(None)] * n_none_slices + items[index_ellipsis : index_ellipsis + 1] = none_slices + + dim = 0 + # Sequence rewrites. + sequence = [] + for item in items: + if (r := rewrite(dim, item)) is None: + return func, args, kwargs + dim, call_spec = r + sequence.append(call_spec) + + def run(): + # Run sequence. + # pyrefly: ignore [bad-index, index-error] + t = args[0] + for _method, _args in sequence: + t = _method(t, *_args) + return t + + return run, [], {} + + return func, args, kwargs + + def __torch_function__(self, func, types, args=(), kwargs=None): + kwargs = kwargs or {} + if torch.compiler.is_dynamo_compiling(): + return func(*args, **kwargs) + + if log.isEnabledFor(logging.DEBUG) and config.extended_debug_current_loc: + frame = _find_user_code_frame() + if frame is not None: + log.debug( + "%s called at %s:%s in %s", + func.__qualname__, + frame.f_code.co_filename, + frame.f_lineno, + frame.f_code.co_name, + ) + + func, args, kwargs = self._override(func, args, kwargs) + try: + return func(*args, **kwargs) + except GuardOnDataDependentSymNode as e: + _suggest_fixes_for_data_dependent_error_non_strict(e) + raise diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/pass_base.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/pass_base.py new file mode 100644 index 0000000000000000000000000000000000000000..6200264118fc7dd50ab32f0646e5afe5322ab513 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/pass_base.py @@ -0,0 +1,497 @@ +# mypy: allow-untyped-defs +import operator +import traceback +from collections.abc import Callable +from contextlib import nullcontext +from typing import Any + +import torch +from torch import fx +from torch._dispatch.python import enable_python_dispatcher +from torch._export.pass_infra.node_metadata import NodeMetadata +from torch._export.pass_infra.proxy_value import ProxyValue +from torch._higher_order_ops.map import _unstack_pytree +from torch._subclasses import FakeTensor, UnsupportedFakeTensorException +from torch._subclasses.fake_tensor import FakeTensorMode +from torch.fx import traceback as fx_traceback +from torch.fx.experimental.proxy_tensor import PythonKeyTracer +from torch.fx.experimental.symbolic_shapes import ( + compute_unbacked_bindings, + PropagateUnbackedSymInts, +) +from torch.fx.graph import CodeGen +from torch.fx.passes.infra.pass_base import PassBase, PassResult +from torch.fx.passes.shape_prop import _extract_tensor_metadata, TensorMetadata +from torch.utils import _pytree as pytree + + +__all__ = ["_ExportPassBaseDeprecatedDoNotUse"] + + +Argument = Any +Value = Any +Fn = Callable[..., Any] +PassType = Callable[[torch.fx.GraphModule], PassResult | None] + + +_TORCH_SYM_OPS: set[Callable] = { + torch.sym_int, + torch.sym_float, + torch.sym_ite, + torch.sym_max, + torch.sym_min, + torch.sym_not, + torch.sym_sqrt, +} + + +class ExportPassBaseError(RuntimeError): + pass + + +class _ExportPassBaseDeprecatedDoNotUse(PassBase): + """ + Interpreter-based pass class to help users maintain the IR spec while writing + transformations. + """ + + @staticmethod + def _create_dummy_node_metadata(): + return NodeMetadata({"stack_trace": "".join(traceback.format_stack(limit=1))}) + + class ExportTracer(PythonKeyTracer): + def __init__( + self, callback: "_ExportPassBaseDeprecatedDoNotUse", codegen: CodeGen + ) -> None: + super().__init__() + self.callback = callback + self.root = torch.nn.Module() + self.graph = torch.fx.Graph() + self.graph.set_codegen(codegen) + self.tensor_attrs: dict[str, torch.Tensor] = {} # type: ignore[assignment] + self.fake_tensor_mode: FakeTensorMode | None = None + self.submodules: dict[torch.nn.Module, str] = {} + + def trace(self) -> None: # type: ignore[override] + raise ExportPassBaseError("ExportTracer doesn't support trace().") + + def create_arg(self, a: Argument) -> torch.fx.Node: + if isinstance(a, torch.nn.Module): + if a not in self.submodules: + name_submodule = f"submodule_{len(self.submodules)}" + self.root.add_module(name_submodule, a) + self.submodules[a] = name_submodule + elif isinstance(a, FakeTensor): + if not hasattr(a, "constant") or a.constant is None: + raise ExportPassBaseError(f"Cannot add {a} to graph.") + a = a.constant + node = super().create_arg(a) + if ( + isinstance(a, torch.Tensor) + and isinstance(node, torch.fx.Node) + and node.op == "get_attr" + ): + self.set_metadata(node, a) + self.callback.on_attr(ProxyValue(a, node)) + return node + + def set_metadata( + self, + node: torch.fx.Node, + value: Argument, + ) -> None: + # propagate the fake tensor or sym nodes + def make_val( + x: Argument, + ) -> ( + FakeTensor + | torch.SymInt + | torch.SymFloat + | torch.SymBool + | int + | float + | bool + | str + | None + ): + if isinstance(x, FakeTensor): + return x + elif isinstance(x, torch.Tensor): + if x.is_quantized: + # TODO (tmanlaibaatar) properly support Quantized FakeTensor + x = torch.dequantize(x) + + try: + if self.fake_tensor_mode is None: + raise AssertionError("fake_tensor_mode must not be None") + # TODO we should allocate static shapes + # for param/buffer values + if isinstance(x, torch.nn.Parameter): + fake_tensor = self.fake_tensor_mode.from_tensor( + x, static_shapes=True + ) + else: + fake_tensor = self.fake_tensor_mode.from_tensor(x) + except UnsupportedFakeTensorException: + # TODO: This is just a workaround to get over the + # x.as_subclass error + print( + "Fakeifying a Tensor subclass is not supported \ + right now. Instead a TensorMetadata is used." + ) + fake_tensor = None + return fake_tensor + elif isinstance( + x, + ( + torch.SymInt, + torch.SymFloat, + torch.SymBool, + int, + float, + bool, + str, + ), + ): + return x + else: + return None + + node.meta["val"] = pytree.tree_map(make_val, value) + + # Set the tensor_metadata for values that do not have a corresponding FakeTensor + def make_tensor_meta(x: Argument) -> TensorMetadata | None: + if not isinstance(x, FakeTensor) and isinstance(x, torch.Tensor): + if x.is_quantized: + # TODO (tmanlaibaatar) properly support Quantized FakeTensor + x = torch.dequantize(x) + + try: + if self.fake_tensor_mode is None: + raise AssertionError("fake_tensor_mode must not be None") + _ = self.fake_tensor_mode.from_tensor(x) + tensor_meta = None + except UnsupportedFakeTensorException: + # TODO: This is just a workaround to get over the + # x.as_subclass error + tensor_meta = _extract_tensor_metadata(x) + return tensor_meta + else: + return None + + node.meta["tensor_meta"] = pytree.tree_map(make_tensor_meta, value) + + class ExportInterpreter(fx.Interpreter): + def __init__( + self, callback: "_ExportPassBaseDeprecatedDoNotUse", gm: fx.GraphModule + ) -> None: + super().__init__(gm) + self.callback = callback + self.node: torch.fx.Node = next(iter(gm.graph.nodes)) + + # pyrefly: ignore [bad-override] + def placeholder( + self, + target: str, # type: ignore[override] + args: tuple[Argument, ...], + kwargs: dict[str, Argument], + ) -> ProxyValue: + arg = super().placeholder(target, args, kwargs) + return self.callback.placeholder(target, arg, NodeMetadata(self.node.meta)) + + def output( + self, + target: torch.fx.node.Target, + args: tuple[Argument, ...], + kwargs: dict[str, Argument], + ) -> ProxyValue: + return self.callback.output(args[0], NodeMetadata(self.node.meta)).data # type: ignore[return-value] + + def call_function( + self, + target: torch.fx.node.Target, + args: tuple[Argument, ...], + kwargs: dict[str, Argument], + ) -> ProxyValue: + meta = NodeMetadata(self.node.meta) + + if target is operator.getitem: + value, key = args + return self.callback.call_getitem(value, key, meta) + elif getattr(target, "__module__", None) in { + "_operator", + "builtins", + "math", + }: + if not callable(target): + raise AssertionError(f"expected callable target, got {target}") + return self.callback.call_sym(target, args, meta) + elif target in _TORCH_SYM_OPS: + if not callable(target): + raise AssertionError(f"expected callable target, got {target}") + return self.callback.call_sym(target, args, meta) + elif isinstance( + target, (torch._ops.OpOverload, torch._ops.OpOverloadPacket) + ): + return self.callback.call_operator( + target, + args, + kwargs, + meta, + ) + elif target is torch.ops.higher_order.cond: + pred, true_fn, false_fn, inputs = args + return self.callback.call_cond(pred, true_fn, false_fn, inputs, meta) + elif target is torch.ops.higher_order.map_impl: + f, mapped_args, operands = args # type: ignore[assignment] + return self.callback.call_map(f, mapped_args, operands, meta) + # For other unregistered HigherOrderOps, just interpret them blindly + elif isinstance(target, torch._ops.HigherOrderOperator): + return self.callback._fx( + "call_function", + target, + args, + kwargs, + meta, + ) + else: + raise ExportPassBaseError(f"Unsupported target type: {target}") + + def get_attr( # type: ignore[override] + self, + target: str, + args: tuple[Argument, ...], + kwargs: dict[str, Argument], + ) -> Argument: + return super().get_attr(target, args, kwargs) + + def call_module( + self, + target: torch.fx.node.Target, + args: tuple[Argument, ...], + kwargs: dict[str, Argument], + ) -> None: + raise ExportPassBaseError("call_module is not supported.") + + def call_method( # type: ignore[override] + self, + target: str, + args: tuple[Argument, ...], + kwargs: dict[str, Argument], + ) -> None: + raise ExportPassBaseError("call_method is not supported.") + + def run_node(self, n: torch.fx.Node) -> Argument: + self.node = n + self.callback.node_debug_str = n.format_node() + return super().run_node(n) + + def __init__(self) -> None: + self.interpreter = PropagateUnbackedSymInts( + torch.fx.GraphModule(torch.nn.Module(), torch.fx.Graph()) + ) + self.tracer = self.ExportTracer(self, CodeGen()) + self.fake_tensor_mode: FakeTensorMode | None = None + self._initialized = True + self.node_debug_str: str | None = None + + def _fx( + self, + kind: str, + target: torch.fx.node.Target, + args: tuple[Argument, ...], + kwargs: dict[str, Argument], + meta: NodeMetadata, + ) -> ProxyValue: + args_data, kwargs_data = pytree.tree_map_only( + ProxyValue, lambda x: x.data, (args, kwargs) + ) + res_data = getattr(self.interpreter, kind)(target, args_data, kwargs_data) + args_proxy, kwargs_proxy = pytree.tree_map_only( + ProxyValue, lambda x: x.proxy, (args, kwargs) + ) + + name = None + if isinstance(target, torch._ops.OpOverload): + name = self.tracer.graph._target_to_str(target.overloadpacket.__name__) + + res_proxy = self.tracer.create_proxy( + kind, target, args_proxy, kwargs_proxy, name=name + ) + res_proxy.node.meta.update(meta.data) + if self.fake_tensor_mode and (shape_env := self.fake_tensor_mode.shape_env): + if symbol_to_path := compute_unbacked_bindings(shape_env, res_data): + res_proxy.node.meta["unbacked_bindings"] = symbol_to_path + self.tracer.set_metadata(res_proxy.node, res_data) + return ProxyValue(res_data, res_proxy) + + def inputs(self, graph_module: torch.fx.GraphModule) -> list[Argument]: + # TODO(angelayi): Update this with what we decide to do for metadata in + # the exported graph module + if (args := graph_module.meta.get("args", None)) is not None: + return list(args) + + def extract_input(node: torch.fx.Node) -> FakeTensor | None: + if "val" in node.meta: + fake = node.meta["val"] + if hasattr(fake, "constant") and fake.constant is not None: + return fake.constant + return fake + elif tensor_meta := node.meta.get("tensor_meta"): + if self.fake_tensor_mode is None: + raise AssertionError("fake_tensor_mode must not be None") + return FakeTensor( + self.fake_tensor_mode, + torch.empty( + tensor_meta.shape, + dtype=tensor_meta.dtype, + device="meta", + requires_grad=tensor_meta.requires_grad, + memory_format=tensor_meta.memory_format, + ), + torch.device("cpu"), + ) + elif len(node.users) == 0: + return None + raise ExportPassBaseError( + f"Cannot construct an input for graph module: {graph_module}.", + ) + + return [ + extract_input(node) + for node in graph_module.graph.nodes + if node.op == "placeholder" + ] + + def on_attr(self, attr: ProxyValue) -> None: + pass + + def placeholder(self, name: str, arg: Argument, meta: NodeMetadata) -> ProxyValue: + arg_proxy = self.tracer.create_proxy("placeholder", name, (), {}) + arg_proxy.node.meta = meta.data + self.tracer.set_metadata(arg_proxy.node, arg) + return ProxyValue(arg, arg_proxy) + + def call_operator( + self, + op, + args: tuple[Argument, ...], + kwargs: dict[str, Argument], + meta: NodeMetadata, + ) -> ProxyValue: + return self._fx("call_function", op, args, kwargs, meta) + + def call_sym( + self, + target: Fn, + args: tuple[Argument, ...], + meta: NodeMetadata, + ) -> ProxyValue: + return self._fx("call_function", target, args, {}, meta) + + def call_cond( + self, + pred: ProxyValue, + true_fn: torch.fx.GraphModule, + false_fn: torch.fx.GraphModule, + inputs: list[Argument], + meta: NodeMetadata, + ) -> ProxyValue: + true_branch = self.call_submodule(true_fn, tuple(inputs)) + false_branch = self.call_submodule(false_fn, tuple(inputs)) + if true_branch is None: + raise AssertionError("true_branch must not be None") + if false_branch is None: + raise AssertionError("false_branch must not be None") + return self._fx( + "call_function", + torch.ops.higher_order.cond, + (pred, true_branch.graph_module, false_branch.graph_module, list(inputs)), + {}, + meta, + ) + + def call_map( + self, + f: torch.fx.GraphModule, + mapped_args: list[ProxyValue], + operands: list[ProxyValue], + meta: NodeMetadata, + ) -> ProxyValue: + xs = _unstack_pytree([arg.data for arg in mapped_args])[0] + f_branch = self.call_submodule(f, tuple(xs + [arg.data for arg in operands])) + if f_branch is None: + raise AssertionError("f_branch must not be None") + return self._fx( + "call_function", + torch.ops.higher_order.map_impl, + (f_branch.graph_module, mapped_args, operands), + {}, + meta, + ) + + def call_getitem( + self, value: ProxyValue, key: int, meta: NodeMetadata + ) -> ProxyValue: + return self._fx("call_function", operator.getitem, (value, key), {}, meta) + + def output(self, results: list[Argument], meta: NodeMetadata) -> ProxyValue: + return self._fx("output", "output", (results,), {}, meta) + + def call_submodule( + self, graph_module: fx.GraphModule, inputs: tuple[Argument, ...] + ) -> PassResult: + prev_tracer, self.tracer = ( + self.tracer, + self.ExportTracer(self, graph_module.graph._codegen), + ) + self.tracer.fake_tensor_mode = prev_tracer.fake_tensor_mode + interpreter = self.ExportInterpreter(self, graph_module) + # pyrefly: ignore [bad-assignment] + prev_interpreter, self.interpreter = ( + self.interpreter, + torch.fx.Interpreter( # type: ignore[assignment] + torch.fx.GraphModule(torch.nn.Module(), torch.fx.Graph()) + ), + ) + inputs_data = pytree.tree_map_only(ProxyValue, lambda x: x.data, inputs) + with fx_traceback.preserve_node_meta(): + interpreter.run(*inputs_data) + + new_graph_module = torch.fx.GraphModule(self.tracer.root, self.tracer.graph) + + self.tracer = prev_tracer + self.interpreter = prev_interpreter + return PassResult( + new_graph_module, + True, + ) + + def call(self, graph_module: fx.GraphModule) -> PassResult: + if not getattr(self, "_initialized", False): + raise ExportPassBaseError( + "ExportPass is not initialized with __init__().", + ) + + inputs = self.inputs(graph_module) + + fake_tensor_mode = None + for i in inputs: + if isinstance(i, FakeTensor): + if fake_tensor_mode is not None and fake_tensor_mode is not i.fake_mode: + raise AssertionError("Multiple fake tensor mode detected.") + fake_tensor_mode = i.fake_mode + if fake_tensor_mode is None: + self.tracer.fake_tensor_mode = FakeTensorMode(allow_non_fake_inputs=True) + fake_tensor_mode = nullcontext() # type: ignore[assignment] + dispatcher_mode = nullcontext() # type: ignore[assignment] + else: + fake_tensor_mode.allow_non_fake_inputs = True + self.tracer.fake_tensor_mode = fake_tensor_mode + dispatcher_mode = enable_python_dispatcher() # type: ignore[assignment] + self.fake_tensor_mode = self.tracer.fake_tensor_mode + + with fake_tensor_mode, dispatcher_mode: # type: ignore[assignment, union-attr] + result = self.call_submodule(graph_module, tuple(inputs)) + + return result diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/pass_infra/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/pass_infra/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/pass_infra/node_metadata.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/pass_infra/node_metadata.py new file mode 100644 index 0000000000000000000000000000000000000000..9874dc1520fdbd6f4adc061dd7bccee031710797 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/pass_infra/node_metadata.py @@ -0,0 +1,32 @@ +from typing import Any + + +NodeMetadataValue = Any + + +PROTECTED_KEYS: set[str] = { + "val", + "stack_trace", + "nn_module_stack", + "debug_handle", + "tensor_meta", +} + + +class NodeMetadata: + def __init__(self, data: dict[str, Any]) -> None: + self.data: dict[str, Any] = data.copy() + + def __getitem__(self, key: str) -> NodeMetadataValue: + return self.data[key] + + def __setitem__(self, key: str, value: NodeMetadataValue) -> NodeMetadataValue: + if key in PROTECTED_KEYS: + raise RuntimeError(f"Could not override node key: {key}") + self.data[key] = value + + def __contains__(self, key: str) -> bool: + return key in self.data + + def copy(self) -> "NodeMetadata": + return NodeMetadata(self.data.copy()) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/pass_infra/proxy_value.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/pass_infra/proxy_value.py new file mode 100644 index 0000000000000000000000000000000000000000..d4a3e1cc4a63c8e26cda39346507a3b9f96b1efa --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/pass_infra/proxy_value.py @@ -0,0 +1,49 @@ +# pyre-strict +from collections.abc import Iterable, Iterator +from typing import Generic, TypeVar + +import torch + + +_T = TypeVar("_T") + + +class ProxyValue(Generic[_T]): + # pyre-ignore + def __init__(self, data: Iterable[_T], proxy: torch.fx.Proxy | torch.fx.Node): + # pyre-ignore + self.data = data + self.proxy_or_node = proxy + + @property + def node(self) -> torch.fx.Node: + if isinstance(self.proxy_or_node, torch.fx.Node): + return self.proxy_or_node + if not isinstance(self.proxy_or_node, torch.fx.Proxy): + raise AssertionError( + f"expected Node or Proxy, got {type(self.proxy_or_node)}" + ) + return self.proxy_or_node.node + + @property + def proxy(self) -> torch.fx.Proxy: + if not isinstance(self.proxy_or_node, torch.fx.Proxy): + raise RuntimeError( + f"ProxyValue doesn't have attached Proxy object. Node: {self.proxy_or_node.format_node()}" + ) + return self.proxy_or_node + + def to_tensor(self) -> torch.Tensor: + if not isinstance(self.data, torch.Tensor): + raise AssertionError(f"expected Tensor, got {type(self.data)}") + return self.data + + def is_tensor(self) -> bool: + return isinstance(self.data, torch.Tensor) + + # pyre-ignore + def __iter__(self) -> Iterator[_T]: + yield from self.data + + def __bool__(self) -> bool: + return bool(self.data) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..aa9ce2ac03c23600c86ff02e38a2a4bfeefef9e2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/__init__.py @@ -0,0 +1 @@ +from .replace_view_ops_with_view_copy_ops_pass import ReplaceViewOpsWithViewCopyOpsPass diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/_node_metadata_hook.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/_node_metadata_hook.py new file mode 100644 index 0000000000000000000000000000000000000000..414bc04d1bad980fde10dd7e9dd3148445240da7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/_node_metadata_hook.py @@ -0,0 +1,112 @@ +# mypy: allow-untyped-defs +import contextlib +from typing import Any + +import torch +import torch.utils._pytree as pytree +from torch._dispatch.python import enable_python_dispatcher +from torch._subclasses.fake_tensor import FakeTensorMode +from torch.fx.graph_module import GraphModule + + +_EMPTY_NN_MODULE_STACK_KEY = "_empty_nn_module_stack_from_metadata_hook" + + +def _node_metadata_hook( + node: torch.fx.Node, + metadata: dict[str, Any] | None = None, + fake_mode: FakeTensorMode | None = None, +) -> None: + """ + Hook for adding the appropriate metadata to nodes that are created during a + pass using graph.create_node. An example of how to use it: + + ``` + with _set_node_metadata_hook(gm, + functools.partial(_node_metadata_hook, metadata={"stack_trace": "file"}) + ): + pass(gm) + ``` + + This hook should not work for all generic cases -- specifically it assumes + that nodes being added are only call_function nodes, and copies over the + first argument node's nn_module_stack. + """ + # pyrefly: ignore [bad-assignment] + fake_mode = fake_mode or contextlib.nullcontext() + + if node.op != "call_function" or not callable(node.target): + raise AssertionError(f"node: {node}, target: {node.target}") + + if ( + isinstance(node.target, torch._ops.OpOverload) + and len(node.target._schema.returns) == 0 + ): + node.meta["val"] = None + else: + fake_args, fake_kwargs = pytree.tree_map_only( + torch.fx.Node, lambda arg: arg.meta["val"], (node.args, node.kwargs) + ) + # pyrefly: ignore [bad-context-manager] + with fake_mode, enable_python_dispatcher(): + fake_res = node.target(*fake_args, **fake_kwargs) + node.meta["val"] = fake_res + + if metadata is not None: + for k, v in metadata.items(): + node.meta[k] = v + + # Copy over metadata from argument nodes + arg_meta = [ + arg.meta + for arg in pytree.tree_flatten((node.args, node.kwargs))[0] + if isinstance(arg, torch.fx.Node) + ] + if len(arg_meta) == 0: + return + arg_meta = arg_meta[0] + + node.meta["nn_module_stack"] = node.meta.get( + "nn_module_stack", + arg_meta.get( + "nn_module_stack", + { + _EMPTY_NN_MODULE_STACK_KEY: ( + _EMPTY_NN_MODULE_STACK_KEY, + _EMPTY_NN_MODULE_STACK_KEY, + ) + }, + ), + ) + + node.meta["torch_fn"] = node.meta.get( + "torch_fn", + ( + f"{node.target.__name__}_0", + f"{node.target.__class__.__name__}.{node.target.__name__}", + ), + ) + + node.meta["custom"] = node.meta.get("custom", arg_meta.get("custom", {})) + + +@contextlib.contextmanager +def _set_node_metadata_hook(gm: torch.fx.GraphModule, f): + """ + Takes a callable which will be called after we create a new node. The + callable takes the newly created node as input and returns None. + """ + if not callable(f): + raise AssertionError("node_metadata_hook must be a callable.") + + # Add the hook to all submodules + for m in gm.modules(): + if isinstance(m, GraphModule): + m._register_create_node_hook(f) + try: + yield + finally: + # Restore hook for all submodules + for m in gm.modules(): + if isinstance(m, GraphModule): + m._unregister_create_node_hook(f) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/add_runtime_assertions_for_constraints_pass.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/add_runtime_assertions_for_constraints_pass.py new file mode 100644 index 0000000000000000000000000000000000000000..337921cdf0a24a214898319f99d7de8b8dad0ba6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/add_runtime_assertions_for_constraints_pass.py @@ -0,0 +1,255 @@ +# mypy: allow-untyped-defs +import math +import operator +import traceback +from functools import partial +from typing import NamedTuple, TYPE_CHECKING + +import sympy + +import torch +import torch.fx +from torch.fx.experimental.symbolic_shapes import free_unbacked_symbols +from torch.fx.passes.infra.pass_base import PassBase, PassResult +from torch.utils._sympy.numbers import int_oo +from torch.utils._sympy.value_ranges import ValueRanges + + +if TYPE_CHECKING: + from collections.abc import Callable + + +__all__ = ["InputDim"] + + +class InputDim(NamedTuple): + input_name: str + dim: int + + +def _convert_to_int(val): + # Convert simple sympy Integers into concrete int + if val in (sympy.oo, int_oo): + return math.inf + if val in (-sympy.oo, -int_oo): + return -math.inf + if isinstance(val, sympy.Integer): + return int(val) + raise RuntimeError("Export constraints cannot be non-integer expressions") + + +def _convert_range_to_int(range: ValueRanges): + if not isinstance(range, ValueRanges): + raise AssertionError(f"expected ValueRanges, got {type(range)}") + min_val = _convert_to_int(range.lower) + max_val = _convert_to_int(range.upper) + return min_val, max_val + + +class _AddRuntimeAssertionsForInlineConstraintsPass(PassBase): + def __init__( + self, + range_constraints: dict[sympy.Symbol, ValueRanges], + ): + super().__init__() + self.range_constraints: dict[sympy.Symbol, ValueRanges] = range_constraints + self._asserts_generated_unbacked_symbols: set[sympy.Symbol] = set() + self.counter = 0 + + def _assert_range_constraint(self, node, lower, upper, assert_msg): + last_node = node + if lower > -math.inf: + last_node = self._insert_assert_async( + last_node, operator.ge, node, lower, assert_msg + ) + + if upper < math.inf: + last_node = self._insert_assert_async( + last_node, operator.le, node, upper, assert_msg + ) + + def _insert_assert_async(self, last_node, op, lower, upper, assert_msg): + """ + Inserts assert_async call_function nodes in the graph. This function is + called **during** the interpreter-based pass. + """ + self.counter += 1 + graph = last_node.graph + with graph.inserting_after(last_node): + cmp = graph.call_function(op, (lower, upper), {}) + with graph.inserting_after(cmp): + cmp_tensor = graph.call_function( + torch.ops.aten.scalar_tensor.default, (cmp,), {} + ) + with graph.inserting_after(cmp_tensor): + assert_async = graph.call_function( + torch.ops.aten._assert_async.msg, + (cmp_tensor, assert_msg), + {}, + ) + return assert_async + + def call(self, graph_module) -> PassResult: + self.existing_inline_assertions = _get_existing_inline_assertions( + graph_module, self.range_constraints + ) + + for module in graph_module.modules(): + if not isinstance(module, torch.fx.GraphModule): + continue + for node in module.graph.nodes: + if node.op != "call_function": + continue + if "val" not in node.meta: + continue + + val = node.meta["val"] + # In general, we may have to deal the case such as: ret[1].shape[0]. + # We need first find out what symbols require assertion, then we need to follow the path + # from ret to the symbol, construct the proxies along the way and construct the messages + # piece-wise at the same time. + # + # We use post-order traversal to collect all the proxies callbacks needed, construct + # the error message callbacks, and at the top-level traversal tree we execute all the callbacks. + # We need the callbacks because, in order to call the function to create a proxy for shape[0], we + # need the proxy for shape, which further requires the proxy for ret[1], etc. + + def add_assertions(val): + call_backs: list[Callable] = [] + messages: list[str] = [] + if isinstance(val, (torch.SymInt, torch.SymFloat, torch.SymBool)): + symbol = val.node.expr + if symbol in self.existing_inline_assertions: + return call_backs, messages + if isinstance(symbol, sympy.Symbol) and free_unbacked_symbols( + symbol + ): + if symbol in self._asserts_generated_unbacked_symbols: + return call_backs, messages + # We only care about unbacked symints for these inline + # constraints, which are prefixed with 'u' + constraint = self.range_constraints[symbol] + min_val, max_val = _convert_range_to_int(constraint) + assert_msg = f" is outside of inline constraint [{min_val}, {max_val}]." + call_backs.append( + partial( + self._assert_range_constraint, + lower=min_val, + upper=max_val, + ) + ) + messages.append(assert_msg) + self._asserts_generated_unbacked_symbols.add(symbol) + + elif isinstance(val, torch.Tensor): + for i, sym in enumerate(val.shape): + cbs, msgs = add_assertions(sym) + for cb, msg in zip(cbs, msgs): + + def sym_size_cb(node, assert_msg, dim): + with node.graph.inserting_after(node): + dim_node = module.graph.call_function( + torch.ops.aten.sym_size.int, + (node, dim), + {}, + ) + cb(node=dim_node, assert_msg=assert_msg) + + call_backs.append(partial(sym_size_cb, dim=i)) + messages.append(f".shape[{i}]" + msg) + return call_backs, messages + + callbacks, messages = add_assertions(val) + for cb, msg in zip(callbacks, messages): + cb(node=node, assert_msg=f"{node}" + msg) + + module.recompile() + + # Sometimes this pass would return a wrong graph where we have mismatched + # node names in signature. Before we fix it, let's just skip it. + if ( + self.counter == 0 + and type(self) is _AddRuntimeAssertionsForInlineConstraintsPass + ): + return PassResult(graph_module, False) + + # Populate the stack trace with dummy vals to respect IR + for node in graph_module.graph.nodes: + if not node.meta.get("stack_trace", None) and node.op not in [ + "placeholder", + "output", + ]: + node.meta["stack_trace"] = "".join(traceback.format_stack(limit=1)) + return PassResult(graph_module, True) + + +def _get_existing_inline_assertions( + graph_module: torch.fx.GraphModule, + range_constraints: dict[sympy.Symbol, ValueRanges], +) -> dict[sympy.Symbol, ValueRanges]: + existing_inline_assertions: dict[sympy.Symbol, ValueRanges] = {} + + for module in graph_module.modules(): + if not isinstance(module, torch.fx.GraphModule): + continue + + # Find all the existing inline assertions. They will look something like: + # %_local_scalar_dense = call_function[target=torch.ops.aten._local_scalar_dense.default](args = (%arg1_1,), kwargs = {}) + # %ge = call_function[target=operator.ge](args = (%_local_scalar_dense, 0), kwargs = {}) + # %_assert_scalar = call_function[target=torch.ops.aten._assert_scalar.default](args = (%scalar_tensor, "..."), kwargs = {}) + for node in module.graph.nodes: + if node.target != torch.ops.aten._assert_scalar.default: + continue + + compare_arg = node.args[0] + if not ( + isinstance(compare_arg, torch.fx.Node) + and compare_arg.op == "call_function" + and compare_arg.target in (operator.le, operator.ge) + and len(compare_arg.args) == 2 + ): + continue + + compare_op = compare_arg.target + lhs, rhs = compare_arg.args + + def maybe_get_symint(x): + if ( + isinstance(x, torch.fx.Node) + and "val" in x.meta + and isinstance(x.meta["val"], torch.SymInt) + ): + return x.meta["val"].node.expr + return x + + lhs = maybe_get_symint(lhs) + rhs = maybe_get_symint(rhs) + + if compare_op is operator.ge: + lhs, rhs = rhs, lhs + + if isinstance(lhs, sympy.Symbol) and isinstance(rhs, int): + symint = lhs + scalar = rhs + elif isinstance(rhs, sympy.Symbol) and isinstance(lhs, int): + symint = rhs + scalar = lhs + else: + continue + + if symint not in range_constraints: + raise RuntimeError( + f"Unable to find symint {symint} in {range_constraints}" + ) + + previous_range = existing_inline_assertions.get( + symint, ValueRanges(-math.inf, math.inf) + ) + + if symint is lhs: + bounds = ValueRanges(-math.inf, scalar) + else: + bounds = ValueRanges(scalar, math.inf) + existing_inline_assertions[symint] = previous_range & bounds + + return existing_inline_assertions diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/collect_tracepoints_pass.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/collect_tracepoints_pass.py new file mode 100644 index 0000000000000000000000000000000000000000..ed43e3319ce79f80a9754231ad16d0751023851a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/collect_tracepoints_pass.py @@ -0,0 +1,158 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import operator +from typing import TYPE_CHECKING + +import torch +from torch.export.exported_program import ConstantArgument, TensorArgument +from torch.fx.passes.infra.pass_base import PassBase, PassResult + + +if TYPE_CHECKING: + from torch.export.exported_program import ModuleCallSignature + from torch.export.graph_signature import ExportGraphSignature + + +__all__ = ["CollectTracepointsPass"] + + +class CollectTracepointsPass(PassBase): + """ + Performs constant folding and constant propagation. + """ + + def __init__( + self, specs: dict[str, ModuleCallSignature], sig: ExportGraphSignature + ) -> None: + super().__init__() + self.specs = specs + self.sig = sig + + def call(self, gm: torch.fx.GraphModule) -> PassResult | None: + def get_arg_spec(arg) -> TensorArgument | ConstantArgument: + if isinstance(arg, torch.fx.Node): + if isinstance(arg.meta.get("val"), torch.Tensor): + return TensorArgument(name=arg.name) + else: + raise AssertionError( + "Symint input is not implemented yet for submodule call signature." + ) + else: + return ConstantArgument(name="", value=arg) + + for module in gm.modules(): + if not isinstance(module, torch.fx.GraphModule): + continue + nn_module_stack = None + for node in module.graph.nodes: + if node.op != "call_function": + continue + if node.target is torch.ops.higher_order._export_tracepoint: + kind = node.kwargs["kind"] + if kind == "module_call_outputs": + nn_module_stack = node.meta["nn_module_stack"] + elif kind == "module_call_inputs": + nn_module_stack = None + else: + raise AssertionError(f"Unknown tracepoint kind: {kind}") + elif node.meta["nn_module_stack"] == nn_module_stack: + node.meta["nn_module_stack"].popitem() + else: + nn_module_stack = None + nn_module_stack = None + for node in reversed(module.graph.nodes): + if node.op != "call_function": + continue + if node.target is torch.ops.higher_order._export_tracepoint: + kind = node.kwargs["kind"] + if kind == "module_call_inputs": + nn_module_stack = node.meta["nn_module_stack"] + elif kind == "module_call_outputs": + nn_module_stack = None + else: + raise AssertionError(f"Unknown tracepoint kind: {kind}") + elif node.meta["nn_module_stack"] == nn_module_stack: + node.meta["nn_module_stack"].popitem() + else: + nn_module_stack = None + + def copy_sig(sig) -> ModuleCallSignature: + from torch.export.exported_program import ModuleCallSignature + + return ModuleCallSignature( + inputs=[], + outputs=[], + in_spec=sig.in_spec, + out_spec=sig.out_spec, + forward_arg_names=None, + ) + + for module in gm.modules(): + if not isinstance(module, torch.fx.GraphModule): + continue + for node in module.graph.nodes: + if node.op != "call_function": + continue + if node.target is torch.ops.higher_order._export_tracepoint: + # There's some subtlety worth noting. Here fqn corresponds to + # the call name, whereas path corresponds to the module name. + # They are not necessarily the same! When a submodule is shared + # through different aliases, there are as many _export_tracepoint + # markers as there are aliases, since the shared submodule is + # wrapped once for each alias. + path = node.kwargs["path"] + fqn, _ = next(reversed(node.meta["nn_module_stack"].values())) + + module_key = next(reversed(node.meta["nn_module_stack"])) + if "@" in module_key: + suffix = module_key.split("@")[-1] + path = f"{path}@{suffix}" + + call_fqn = f"{fqn}@{suffix}" + if call_fqn not in self.specs: + self.specs[call_fqn] = copy_sig(self.specs[fqn]) + fqn = call_fqn + + kind = node.kwargs["kind"] + for i, arg in enumerate(node.args): + # We only update the signature of the alias used to call + # the submodule. Otherwise the signatures of all aliases + # would get conflated; the inputs/outputs of every call + # would be recorded in every other call as well. + if fqn == path: + if kind == "module_call_inputs": + self.specs[path].inputs.append(get_arg_spec(arg)) + elif kind == "module_call_outputs": + self.specs[path].outputs.append(get_arg_spec(arg)) + else: + raise AssertionError(f"Unknown tracepoint kind: {kind}") + if isinstance(arg, torch.fx.Node): + for user in node.users: + if user.op != "call_function": + raise AssertionError( + f"expected call_function, got {user.op}" + ) + if user.target is not operator.getitem: + raise AssertionError( + f"expected getitem target, got {user.target}" + ) + if not isinstance(user.args[1], int): + raise AssertionError( + f"expected int arg, got {type(user.args[1])}" + ) + if user.args[1] == i: + user.replace_all_uses_with(arg) + self.sig.replace_all_uses(user.name, arg.name) + break + users = list(node.users) + for user in users: + if len(user.users) != 0: + raise AssertionError( + f"expected no users, got {len(user.users)}" + ) + gm.graph.erase_node(user) + gm.graph.erase_node(node) + return PassResult(gm, True) + + return None diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/constant_folding.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/constant_folding.py new file mode 100644 index 0000000000000000000000000000000000000000..ab0f8c33f6f26d9c1dad718d0ef96d8e5b704895 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/constant_folding.py @@ -0,0 +1,304 @@ +# mypy: allow-untyped-defs +import collections +from collections import defaultdict +from collections.abc import Callable +from typing import Any + +import torch +import torch.utils._pytree as pytree + + +aten = torch.ops.aten + +# We would like to split modules into two subgraphs for runtime weight updates to work correctly. +# The use case and more information could be found at: +# https://docs.google.com/document/d/1inZC-8KarJ6gKB7G9egmYLx1V_dKX_apxon0w4zPC0Q/edit?usp=sharing +META_TAG = "MODULE_TYPE" +MODULE_TAG = "_MAIN_MODULE" +CONST_MODULE_TAG = "_CONST_MODULE" + + +def replace_node_with_constant(gm, node, constant, name=None): + g = gm.graph + + if name: + qualname = name + else: + if not hasattr(gm, "_frozen_param_count"): + gm._frozen_param_count = 0 + i = gm._frozen_param_count + + while True: + qualname = f"_frozen_param{i}" + if not hasattr(gm, qualname): + break + i += 1 + + gm._frozen_param_count = i + 1 + + with g.inserting_before(node): + new_input_node = g.create_node("get_attr", qualname, (), {}) + node.replace_all_uses_with(new_input_node) + new_input_node.meta.update(node.meta) + g.erase_node(node) + + # needed to suppress `does not reference an nn.Module, nn.Parameter, or buffer` warning + gm.register_buffer(qualname, constant) + setattr(gm, qualname, constant) + + +class ConstantFolder(torch.fx.Interpreter): + def __init__( + self, + gm: torch.fx.GraphModule, + skip_constructors: bool = False, + ): + super().__init__(gm) + self.node_replacements: dict[torch.fx.Node, Any] = {} + self.replaced_uses: dict[torch.fx.Node, int] = collections.Counter() + self.unknown_value = object() + self.skip_constructors: bool = skip_constructors + + # overwrite this to deallocate env values if their only remaining use + # is the output + self.user_to_last_uses = self.node_to_last_non_output_use() + + def is_impure(self, node: torch.fx.Node) -> bool: + if ( + node.target is torch.ops.prims.convert_element_type.default + and node.args[0].op == "get_attr" # type: ignore[union-attr] + and node.args[0].meta["val"].dtype == torch.int8 # type: ignore[union-attr] + and node.args[1] == torch.bfloat16 + ): + # For int8_weight -> dq -> bf16_weight + return True + if node.target in [ + torch.ops.quantized_decomposed.dequantize_per_channel.default, + torch.ops.quantized_decomposed.dequantize_per_tensor.default, + torch.ops.quantized_decomposed.dequantize_per_tensor.tensor, + torch.ops.pt2e_quant.dequantize_affine, + ]: + # For the pattern fp32_weight -> q -> dq + # We only folding fp32_weight -> q + # int8_weight and leave dq in graph to be fused + return True + return False + + def node_to_last_non_output_use(self): + last_non_output_use = collections.defaultdict(list) + seen_uses = set() + output_node = next(iter(reversed(self.module.graph.nodes))) # type: ignore[arg-type, union-attr] + + for node in reversed(self.module.graph.nodes): # type: ignore[arg-type, union-attr] + if node.target == "output": + continue + + def add_use(inp): + if inp in seen_uses: + return + + seen_uses.add(inp) + last_non_output_use[node].append(inp) + + # In-place is fine since we don't mutate + pytree.tree_map_only_(torch.fx.Node, add_use, (node.args, node.kwargs)) + + # if this node is only used in output, we want to gc it right away + if len(node.users) == 1 and output_node in node.users: + last_non_output_use[node].append(node) + + return last_non_output_use + + def run_node(self, node): + if node.target == "output": + # because we remove nodes from env on last non output use, + # re-define them now or we'll get error in interpreter + def set_env(arg): + self.env[arg] = self.unknown_value + + # In-place is fine since we don't mutate + pytree.tree_map_only_(torch.fx.Node, set_env, node.args) + return super().run_node(node) + + args, kwargs = self.fetch_args_kwargs_from_env(node) + flattened_inputs = pytree.arg_tree_leaves(*args, **kwargs) + + # We need to do this weird thing because in cases where flattened_inputs + # contains a ScriptObject, equality checking results in a type error if + # the types are different. + if any( + type(self.unknown_value) is type(input_) and self.unknown_value == input_ + for input_ in flattened_inputs + ): + return self.unknown_value + + # TODO - fix errors with this + if ( + node.op == "call_function" + and node.target is aten._efficientzerotensor.default + ): + return self.unknown_value + + # TODO - constant folding triton kernel returns the inputs -- fix this + if ( + node.op == "call_function" + and node.name == "triton_kernel_wrapper_functional_proxy" + ): + return self.unknown_value + + # skip constructors, since inductor generates optimal code for them already + # and turning into tensor would result in an additional global memory read + # TODO - more complicated strategy + if ( + self.skip_constructors + and node.op != "get_attr" + and not any(isinstance(e, torch.Tensor) for e in flattened_inputs) + ): + return self.unknown_value + + # All mutations should either be removed or on inputs which we did not make constant + if ( + isinstance(node.target, torch._ops.OpOverload) + and torch.Tag.nondeterministic_seeded in node.target.tags + ): + return self.unknown_value + + out = super().run_node(node) + + if node.op != "get_attr" and isinstance(out, torch.Tensor): + if out.device.type == "meta": + return out + + if not self.insertable_tensor_check(out): + return out + + if self.is_impure(node): + return self.unknown_value + + self.add_node_replacement(node, out) + + flattened_node_inps = pytree.arg_tree_leaves(*node.args, **node.kwargs) + + for n in flattened_node_inps: + if not isinstance(n, torch.fx.Node): + continue + + self.replaced_uses[n] += 1 + + for to_delete in self.user_to_last_uses.get(node, []): + if self.replaced_uses[to_delete] == len(to_delete.users): + self.node_replacements.pop(to_delete, None) + + return out + + def insertable_tensor_check(self, tensor: torch.Tensor) -> bool: + return True + + def add_node_replacement(self, node: torch.fx.Node, tensor: torch.Tensor) -> None: + self.node_replacements[node] = tensor + + def run(self): # type: ignore[override] + env = {} + for n in self.module.graph.find_nodes(op="placeholder"): # type: ignore[operator, union-attr] + env[n] = self.unknown_value + return super().run(initial_env=env) + + +def constant_fold( + gm: torch.fx.GraphModule, + constraint_fn: Callable[[torch.fx.Node], bool] | None = None, +): + with torch.utils._python_dispatch._disable_current_modes(): + cf = ConstantFolder(gm, skip_constructors=True) + cf.run() + + for node, constant in cf.node_replacements.items(): + if constraint_fn is not None and not constraint_fn(node): + continue + replace_node_with_constant(gm, node, constant) + + erased_params = [] + # Get all attr users by looking up the graph instead from node.users, because in this case + # _tensor_constant0 and _tensor_constant0_1 are actually refereing to the same tensor. + + # opcode name target args kwargs + # ------------- ------------------- ---------------- --------------------------- -------- + # placeholder arg0_1 arg0 () {} + # get_attr _tensor_constant0 state () {} + # call_function add aten.add.Tensor (arg0_1, _tensor_constant0) {} + # get_attr _tensor_constant0_1 state () {} + # call_function add_ aten.add_.Tensor (_tensor_constant0_1, 1) {} + # output output output ([add],) {} + + get_attr_node_users = defaultdict(list) + for node in gm.graph.nodes: + if node.op == "get_attr": + get_attr_node_users[node.target].extend(node.users.keys()) + for node in gm.graph.find_nodes(op="get_attr"): + if node.op == "get_attr" and len(get_attr_node_users[node.target]) == 0: + if hasattr(gm, node.target): + delattr(gm, node.target) + erased_params.append(node) + for node in erased_params: + gm.graph.erase_node(node) + + gm.graph.eliminate_dead_code() + gm.graph.lint() + gm.recompile() + + +def constant_graph_tag(gm: torch.fx.GraphModule) -> None: + with torch.utils._python_dispatch._disable_current_modes(): + cf = ConstantFolder(gm, skip_constructors=True) + cf.run() + + for node in gm.graph.nodes: + if ( + node.op == "get_attr" + or node in cf.node_replacements + or node in cf.replaced_uses + ): + node.meta[META_TAG] = CONST_MODULE_TAG + else: + node.meta[META_TAG] = MODULE_TAG + + +def run_and_get_constant_graph(gm: torch.fx.GraphModule) -> torch.fx.GraphModule: + """ + Construct a GraphModule which corresponds to the part which could be + constant folded in provided gm. + """ + + constant_graph_tag(gm) + # We rewrite the tags, if it's a constant being directly consumed, without + # any folding opportunity, we keep it in main gm. + for node in gm.graph.find_nodes(op="get_attr"): + used_to_fold = False + for u in node.users: + if u.meta[META_TAG] == CONST_MODULE_TAG: + used_to_fold = True + break + if not used_to_fold: + node.meta[META_TAG] = MODULE_TAG + + new_graph = torch.fx.Graph() + + node_remapping: dict[torch.fx.Node, torch.fx.Node] = {} + output_nodes = [] + for node in gm.graph.nodes: + if node.meta[META_TAG] == MODULE_TAG: + continue + + new_node = new_graph.node_copy(node, lambda x: node_remapping[x]) + node_remapping[node] = new_node + + for user in node.users: + if user.meta[META_TAG] == MODULE_TAG: + output_nodes.append(new_node) + break + + new_graph.output(tuple(output_nodes)) + new_graph.lint() + new_gm = torch.fx.GraphModule(gm, new_graph) + + return new_gm diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/functionalize_side_effectful_ops_pass.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/functionalize_side_effectful_ops_pass.py new file mode 100644 index 0000000000000000000000000000000000000000..c612aaf34888793b0953779c67e656d98eb6316f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/functionalize_side_effectful_ops_pass.py @@ -0,0 +1,100 @@ +import copy + +import torch +from torch._export.pass_base import ( + _ExportPassBaseDeprecatedDoNotUse, + Argument, + PassResult, +) +from torch._export.pass_infra.node_metadata import NodeMetadata +from torch._export.pass_infra.proxy_value import ProxyValue +from torch._ops import OpOverload + + +aten = torch.ops.aten + +_NON_FUNCTIONAL_TO_FUNCTIONAL_SIDE_EFFECTFUL_FUNCS: dict[OpOverload, OpOverload] = { + aten.sym_constrain_range.default: aten._functional_sym_constrain_range.default, + aten._assert_async.msg: aten._functional_assert_async.msg, +} + + +class _FunctionalizeSideEffectfulOpsPass(_ExportPassBaseDeprecatedDoNotUse): + """ + Functionalize ops with side effect in graph module by replacing the op with + functional version of it. A new dependency token (`dep_token`) will be + created and propagated through functional ops to output. + For example: + ``` + def f(x): + sym_constrain_range(x.shape[0], min=1, max=3) + return x.add(3) + ``` + Will be transformed to: + ``` + def f(x): + dep_token0 = _make_dep_token() + dep_token1 = _functional_sym_constrain_range( + x.shape[0], min=1, max=3, dep_token=dep_token0 + ) + + return x.add(3), dep_token1 + ``` + """ + + def __init__(self) -> None: + super().__init__() + self._dep_token: ProxyValue | None = None + self._next_dep_token_index: int | None = None + + def call(self, graph_module: torch.fx.GraphModule) -> PassResult: + # Early return if no non-functional assertions. + if not any( + n.target in _NON_FUNCTIONAL_TO_FUNCTIONAL_SIDE_EFFECTFUL_FUNCS + for n in graph_module.graph.nodes + ): + return PassResult(graph_module=graph_module, modified=False) + + gm = copy.deepcopy(graph_module) + self._dep_token = None + self._next_dep_token_index = None + return super().call(gm) + + def call_operator( + self, + op: OpOverload, + args: tuple[Argument, ...], + kwargs: dict[str, Argument], + meta: NodeMetadata, + ) -> ProxyValue: + if op not in _NON_FUNCTIONAL_TO_FUNCTIONAL_SIDE_EFFECTFUL_FUNCS: + return super().call_operator(op, args, kwargs, meta) + + if self._dep_token is None: + self._dep_token = super().call_operator( + aten._make_dep_token, + args=(), + kwargs={}, + meta=self._create_dummy_node_metadata(), + ) + self._dep_token.node.name = "dep_token0" + self._next_dep_token_index = 1 + + self._dep_token = super().call_operator( + _NON_FUNCTIONAL_TO_FUNCTIONAL_SIDE_EFFECTFUL_FUNCS[op], + args=args, + kwargs={**kwargs, "dep_token": self._dep_token}, + meta=meta, + ) + if self._next_dep_token_index is None: + raise AssertionError("_next_dep_token_index must not be None") + self._dep_token.node.name = f"dep_token{self._next_dep_token_index}" + self._next_dep_token_index += 1 + + return self._dep_token + + def output(self, results: list[Argument], meta: NodeMetadata) -> ProxyValue: + if self._dep_token is None: + raise AssertionError("_dep_token must not be None") + + return super().output(results=(*results, self._dep_token), meta=meta) # type: ignore[arg-type] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/insert_custom_op_guards.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/insert_custom_op_guards.py new file mode 100644 index 0000000000000000000000000000000000000000..8bbca86cf58908ffe31f976f3fa3fce152ade694 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/insert_custom_op_guards.py @@ -0,0 +1,84 @@ +import functools +from collections import defaultdict + +import torch +from torch._export.passes._node_metadata_hook import ( + _node_metadata_hook, + _set_node_metadata_hook, +) +from torch._library.fake_profile import OpProfile, TensorMetadata + + +def insert_custom_op_guards(gm: torch.fx.GraphModule, ops_to_guard: set[str]) -> None: + """ + This is used by draft_export to insert guards in front of calls to custom + operators which have a generated fake kernel. + """ + for node in gm.graph.nodes: + if node.op == "call_function" and str(node.target) in ops_to_guard: + with ( + _set_node_metadata_hook( + gm, + functools.partial( + _node_metadata_hook, + metadata={"stack_trace": node.meta.get("stack_trace")}, + ), + ), + gm.graph.inserting_before(node), + ): + for arg in (*node.args, *node.kwargs.values()): + if isinstance(arg, torch.fx.Node) and isinstance( + arg.meta.get("val"), torch.Tensor + ): + val = arg.meta["val"] + gm.graph.call_function( + torch.ops.aten._assert_tensor_metadata.default, + args=(arg,), + kwargs={ + "dtype": val.dtype, + "device": val.device, + "layout": val.layout, + }, + ) + + gm.recompile() + + +def get_op_profiles( + gm: torch.fx.GraphModule, ops_to_guard: set[str] +) -> dict[str, set[OpProfile]]: + """ + This is used by draft_export to get a list of custom operator profiles so + that we can generate fake kernels. + """ + + def _get_op_profile(node: torch.fx.Node) -> OpProfile: + args_profile = tuple( + TensorMetadata.maybe_from_tensor(arg.meta.get("val")) + if isinstance(arg, torch.fx.Node) + else None + for arg in (*node.args, *node.kwargs.values()) + ) + + out_profile = None + meta = node.meta.get("val") + if meta is None: + raise AssertionError("node.meta['val'] must not be None") + if isinstance(meta, torch.Tensor): + out_profile = TensorMetadata.maybe_from_tensor(meta) + elif isinstance(meta, (list, tuple)): + out_profile = tuple(TensorMetadata.maybe_from_tensor(m) for m in meta) # type: ignore[assignment] + if out_profile is None: + raise AssertionError( + f"out_profile must not be None for meta type {type(meta)}" + ) + + return OpProfile(args_profile, out_profile) # type: ignore[arg-type] + + op_profiles: dict[str, set[OpProfile]] = defaultdict(set) + + for node in gm.graph.nodes: + if node.op == "call_function" and str(node.target) in ops_to_guard: + op_profiles[str(node.target)].add(_get_op_profile(node)) + + return op_profiles diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/lift_constants_pass.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/lift_constants_pass.py new file mode 100644 index 0000000000000000000000000000000000000000..7c5690d7c6b4136c6c782d40f525678d8230f075 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/lift_constants_pass.py @@ -0,0 +1,449 @@ +# mypy: allow-untyped-defs +import collections +import logging +from typing import Any + +import torch +from torch._export.verifier import SpecViolationError +from torch._guards import detect_fake_mode +from torch._library.fake_class_registry import FakeScriptObject, maybe_to_fake_obj +from torch._library.opaque_object import ( + get_opaque_type_name, + is_opaque_reference_type, + is_opaque_type, +) +from torch._subclasses.fake_tensor import unset_fake_temporarily +from torch.export.exported_program import ( + ArgumentSpec, + CustomObjArgument, + ExportGraphSignature, + InputKind, + InputSpec, + TensorArgument, +) +from torch.fx._symbolic_trace import _ConstantAttributeType +from torch.fx.graph_module import _get_attr + + +log = logging.getLogger(__name__) + + +class ConstantAttrMap(collections.abc.MutableMapping): + """A mapping class that understands how to use module constants (tensors, + ScriptObjects, FakeScriptObjects, opaque objects) as keys. We store tensors, + FakeScriptObjects, and opaque objects normally, but ScriptObjects are stored + by hash, because different torch.ScriptObjects can point to the same + underlying value (but we guarantee that they will `hash()` to the same value + if that's the case). + """ + + def __init__(self) -> None: + # Underlying dict that we use to implement this mapping. + self._constant_attrs: dict[ + int | torch.Tensor | FakeScriptObject | torch.utils._pytree.TreeSpec, + list[Any], + ] = {} + # Map from the hash(ScriptObject) to the ScriptObject itself. Used for + # APIs like `__iter__` that should look like they're returning the + # original ScriptObjects. + self._script_object_map: dict[int, torch.ScriptObject] = {} + + def __getitem__(self, key: _ConstantAttributeType) -> Any: + real_key = hash(key) if isinstance(key, torch.ScriptObject) else key + if not isinstance( + real_key, (int, torch.Tensor, FakeScriptObject) + ) and not is_opaque_type(type(real_key)): + raise AssertionError( + f"expected int, Tensor, FakeScriptObject, or opaque type key, got {type(real_key)}" + ) + return self._constant_attrs[real_key] + + def __setitem__(self, key: _ConstantAttributeType, value): + # we shouldn't actually call this, should go to add() instead to handle aliasing + raise NotImplementedError( + """Directly setting values for ConstantAttrMap is not supported, please use add(key, value) instead. +The same key can be mapped to multiple values, for handling constant aliasing.""" + ) + + def add(self, key: _ConstantAttributeType, value: Any) -> None: + if isinstance(key, torch.ScriptObject): + if hash(key) not in self._constant_attrs: + self._constant_attrs[hash(key)] = [] + self._constant_attrs[hash(key)].append(value) + self._script_object_map[hash(key)] = key + elif isinstance(key, (torch.Tensor, FakeScriptObject)) or is_opaque_type( + type(key) + ): + if key not in self._constant_attrs: + self._constant_attrs[key] = [] + self._constant_attrs[key].append(value) + else: + raise TypeError( + f"Expected key to be a tensor or ScriptObject, got {type(key)}" + ) + + def __delitem__(self, key: _ConstantAttributeType): + real_key = hash(key) if isinstance(key, torch.ScriptObject) else key + + del self._constant_attrs[real_key] + + def __iter__(self): + for key in self._constant_attrs: + if isinstance(key, int): + yield self._script_object_map[key] + else: + yield key + + def __len__(self): + return len(self._constant_attrs) + + def __contains__(self, key: object) -> bool: + real_key = hash(key) if isinstance(key, torch.ScriptObject) else key + return real_key in self._constant_attrs + + +def get_constant_fqn(node: torch.fx.Node, constant_name: str) -> str: + # The FQN of the constant tensor in the state dict should + # correspond to the module where the constant tensor was + # originally used. + if len(node.meta["nn_module_stack"]) == 0: + return constant_name + parent_fqn = list(node.meta["nn_module_stack"].values())[-1][0] + if len(parent_fqn) > 0: + return f"{parent_fqn}.{constant_name}" + else: + return constant_name + + +def _get_first_fqn( + const_attrs: ConstantAttrMap, + key: _ConstantAttributeType, +) -> Any: + fqns = const_attrs.get(key) + return fqns[0] if fqns else None + + +def _unused_constant(node: torch.fx.Node) -> list[torch.fx.Node] | None: + """ + If there is a tensor constant created while tracing, here is how the graph + looks like: + + %_tensor_constant0 : [num_users=1] = get_attr[target=_tensor_constant0] + %lift_fresh_copy : [num_users=1] = call_function[target=torch.ops.aten.lift_fresh_copy.default](args = (%_tensor_constant0,)) + %detach_ : [num_users=?] = call_function[target=torch.ops.aten.detach_.default](args = (%lift_fresh_copy,)) + + To check to see if the tensor constant is being used, we want to traverse to + the detach node to see if it's actually being used. + + This function returns None if this constant is being used, otherwise it returns the + lift_fresh and detach node to be removed later. + """ # noqa: B950 + if len(node.users) > 1: + return None + + lift_fresh_node = next(iter(node.users.keys())) + if not ( + lift_fresh_node.op == "call_function" + and lift_fresh_node.target + in ( + torch.ops.aten.lift_fresh.default, + torch.ops.aten.lift_fresh_copy.default, + ) + ): + return None + + if len(lift_fresh_node.users) > 1: + return None + + # Case 1: lift node is not used anywhere + if len(lift_fresh_node.users) == 0: + return [lift_fresh_node, node] + + detach_node = next(iter(lift_fresh_node.users.keys())) + if not ( + detach_node.op == "call_function" + and detach_node.target + in ( + torch.ops.aten.detach_.default, + torch.ops.aten.detach.default, + ) + ): + return None + + if len(detach_node.users) > 0: + return None + else: + # Case 2: Lift node's child is not used anywhere + return [detach_node, lift_fresh_node, node] + + +def lift_constants_pass( + gm: torch.fx.GraphModule, + graph_signature: ExportGraphSignature, + constant_attrs: ConstantAttrMap, +) -> dict[str, _ConstantAttributeType]: + """ + Takes a graph module, graph signature, and modifies them inplace to lift any + constants (tensors or custom classes) as inputs to the graph. Returns a + dictionary of names to constants. + + Arguments: + gm (torch.fx.GraphModule): The graph module containing the graph and constants to lift. + graph_signature (ExportGraphSignature): This graph signature will be + mutated to add additional CONSTANT_TENSOR and CUSTOM_OBJ inputs. + constant_attrs (ConstantAttr): A mapping from a constant value to its + fully-qualified path in `gm`. This is used to maintain consistent + location of constants between the original module and the exported + version. + + Returns: + A dictionary of fqn => constant value. + """ + all_constants: dict[str, _ConstantAttributeType] = {} + + input_specs = graph_signature.input_specs + num_custom_obj = sum( + input_spec.kind == InputKind.CUSTOM_OBJ for input_spec in input_specs + ) + num_tensor_constants = sum( + input_spec.kind == InputKind.CONSTANT_TENSOR for input_spec in input_specs + ) + + fake_mode = detect_fake_mode( + tuple(node.meta["val"] for node in gm.graph.nodes if node.op == "placeholder") + ) + + first_user_input_loc, first_user_input = 0, next(iter(gm.graph.nodes)) + used_target_names = set() + + input_nodes = [node for node in gm.graph.nodes if node.op == "placeholder"] + if len(input_nodes) != len(input_specs): + raise AssertionError( + f"input nodes count {len(input_nodes)} != input specs count {len(input_specs)}" + ) + for i, (node, input_spec) in enumerate(zip(input_nodes, input_specs)): + used_target_names.add(input_spec.target) + if input_spec.kind == InputKind.USER_INPUT: + first_user_input = node + first_user_input_loc = i + break + + lifted_objs = ConstantAttrMap() + renamed_targets = {} + for node in list(gm.graph.nodes): + if node.op == "get_attr": + if nodes_to_remove := _unused_constant(node): + # Remove the node if it's not being used + for node_rm in nodes_to_remove: + gm.graph.erase_node(node_rm) + continue + + constant_val = _get_attr(gm, node.target) + # These are not hashable and not gonna be lifted + # so we can skip them earlier + if isinstance(constant_val, torch.fx.GraphModule): + continue + if "LoweredBackendModule" in type(constant_val).__name__: + continue + if "AOTInductorRunnerWrapper" in type(constant_val).__name__: + continue + if isinstance(constant_val, torch.utils._pytree.TreeSpec): + continue + + if constant_val in lifted_objs: + # We already lifted this constant elsewhere. Just rewrite uses + # of this get_attr to point to the already-existing placeholder + # node. + const_placeholder_node = _get_first_fqn(lifted_objs, constant_val) + node.replace_all_uses_with(const_placeholder_node) + gm.graph.erase_node(node) + renamed_targets[node.name] = const_placeholder_node.name + continue + + # For ScriptObject, Tensor and FakeScriptObject constants: + # First check if the constant was an attribute on some module by + # consulting `constant_attrs` map. If it is, use the fqn that keeps + # its location consistent with the eager module. + # + # If it's not in the `constant_attrs` map, that means it's an inline + # constant (e.g. x + torch.tensor(0)), and thus did not have a + # specific location in the eager module. In that case, just generate + # some name and attach it to the module in which it was used. + if isinstance( + constant_val, (torch.ScriptObject, FakeScriptObject) + ) or is_opaque_reference_type(type(constant_val)): + constant_kind = InputKind.CUSTOM_OBJ + constant_fqn = _get_first_fqn(constant_attrs, constant_val) + if constant_fqn is not None: + constant_name = constant_fqn.replace(".", "_") + else: + constant_name = f"lifted_custom_{num_custom_obj}" + constant_fqn = get_constant_fqn(node, constant_name) + while constant_fqn in used_target_names: + num_custom_obj += 1 + constant_name = f"lifted_custom_{num_custom_obj}" + constant_fqn = get_constant_fqn(node, constant_name) + num_custom_obj += 1 + elif isinstance(constant_val, torch.Tensor): + # Remove the parameterness of constant_val + if isinstance(constant_val, torch.nn.Parameter): + log.debug( + "%s created when tracing %s is a parameter. But " + "it's not registered with register_parameter(). export will treat it as a constant tensor", + str(node.target), + str(node.meta.get("stack_trace", "")), + ) + # We get the real data out of the parameter by disabling the surrounding fake mode. + with unset_fake_temporarily(): + constant_val = constant_val.data + constant_kind = InputKind.CONSTANT_TENSOR + constant_fqn = _get_first_fqn(constant_attrs, constant_val) + if constant_fqn is not None: + constant_name = constant_fqn.replace(".", "_") + else: + constant_name = f"lifted_tensor_{num_tensor_constants}" + constant_fqn = get_constant_fqn(node, constant_name) + while constant_fqn in used_target_names: + num_tensor_constants += 1 + constant_name = f"lifted_tensor_{num_tensor_constants}" + constant_fqn = get_constant_fqn(node, constant_name) + num_tensor_constants += 1 + else: + raise SpecViolationError( + f"getattr node {node} referencing unsupported type {type(constant_val)}" + ) + + with gm.graph.inserting_before(first_user_input): + # Insert the constant node before the first user input + const_placeholder_node = gm.graph.placeholder(constant_name) + # match target name with its node name in case there is name collision + # and suffix is added to node name in fx + const_placeholder_node.target = const_placeholder_node.name + + for k, v in node.meta.items(): + const_placeholder_node.meta[k] = v + + # Once the FQN has been used, remove nn_module_stack, stack_trace + const_placeholder_node.meta.pop("nn_module_stack") + const_placeholder_node.meta.pop("stack_trace", None) + + input_spec_arg: ArgumentSpec + if isinstance(constant_val, torch.Tensor): + if fake_mode is not None: + const_placeholder_node.meta["val"] = fake_mode.from_tensor( + constant_val, static_shapes=True + ) + const_placeholder_node.meta["val"].constant = constant_val + else: + const_placeholder_node.meta["val"] = constant_val + input_spec_arg = TensorArgument(name=const_placeholder_node.name) + elif isinstance(constant_val, torch._C.ScriptObject): + class_fqn = constant_val._type().qualified_name() # type: ignore[attr-defined] + const_placeholder_node.meta["val"] = CustomObjArgument( + constant_fqn, class_fqn + ) + input_spec_arg = CustomObjArgument( + name=const_placeholder_node.name, class_fqn=class_fqn + ) + elif isinstance(constant_val, FakeScriptObject): + class_fqn = constant_val.script_class_name + const_placeholder_node.meta["val"] = CustomObjArgument( + constant_fqn, class_fqn, constant_val + ) + input_spec_arg = CustomObjArgument( + name=const_placeholder_node.name, + class_fqn=class_fqn, + fake_val=constant_val, + ) + elif is_opaque_type(type(constant_val)): + class_fqn = get_opaque_type_name(type(constant_val)) + fake_val = ( + maybe_to_fake_obj(fake_mode, constant_val) + if fake_mode + else None + ) + const_placeholder_node.meta["val"] = CustomObjArgument( + constant_fqn, + class_fqn, + fake_val, # pyrefly: ignore[bad-argument-type] + ) + input_spec_arg = CustomObjArgument( + name=const_placeholder_node.name, + class_fqn=class_fqn, + fake_val=fake_val, # pyrefly: ignore[bad-argument-type] + ) + else: + raise SpecViolationError( + f"tried to lift unsupported type {type(constant_val)} from node {node.format_node()}" + ) + + lifted_objs.add(constant_val, const_placeholder_node) + node.replace_all_uses_with(const_placeholder_node) + gm.graph.erase_node(node) + + renamed_targets[node.name] = const_placeholder_node.name + + # Add the constant as a buffer to the graph signature + graph_signature.input_specs.insert( + first_user_input_loc, + InputSpec( + kind=constant_kind, + arg=input_spec_arg, + target=constant_fqn, + ), + ) + if constant_val in constant_attrs: + for fqn in constant_attrs[constant_val]: + all_constants[fqn] = constant_val + else: + all_constants[constant_fqn] = constant_val + first_user_input_loc += 1 + + for spec in graph_signature.output_specs: + if spec.arg.name in renamed_targets: + spec.arg.name = renamed_targets[spec.arg.name] + + return all_constants + + +def rewrite_script_object_meta( + gm: torch.fx.GraphModule, +) -> dict[str, _ConstantAttributeType]: + """When tracing, we produce a graph with FakeScriptObject in the + meta["val"]. + + For now, we rewrie meta["val"] to be a placeholder CustomObjArgument + """ + constants: dict[ + str, + _ConstantAttributeType, + ] = {} + for node in gm.graph.nodes: + if "val" not in node.meta: + continue + + old_meta = node.meta["val"] + + if isinstance(old_meta, torch.ScriptObject): + class_fqn = old_meta._type().qualified_name() # type: ignore[attr-defined] + new_meta = CustomObjArgument(node.name, class_fqn) + constants[node.name] = old_meta + node.meta["val"] = new_meta + + elif isinstance(old_meta, FakeScriptObject): + class_fqn = old_meta.script_class_name # type: ignore[attr-defined] + new_meta = CustomObjArgument(node.name, class_fqn, old_meta) + constants[node.name] = old_meta + node.meta["val"] = new_meta + + return constants + + +def _materialize_and_lift_constants( + gm: torch.fx.GraphModule, + export_graph_signature: ExportGraphSignature, + constant_attrs: ConstantAttrMap, +) -> dict[str, _ConstantAttributeType]: + constants = rewrite_script_object_meta(gm) + constants.update(lift_constants_pass(gm, export_graph_signature, constant_attrs)) + return constants diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/remove_runtime_assertions.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/remove_runtime_assertions.py new file mode 100644 index 0000000000000000000000000000000000000000..ceed7cd23aa0e953b99586052629668cc53c4bdd --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/remove_runtime_assertions.py @@ -0,0 +1,36 @@ +import torch +from torch.fx.passes.infra.pass_base import PassBase, PassResult + + +class _RemoveRuntimeAssertionsPass(PassBase): + """ + Remove runtime assertions inserted by the + _AddRuntimeAssertionsForInlineConstraintsPass. + """ + + def call(self, graph_module: torch.fx.GraphModule) -> PassResult: + modified = False + for module in graph_module.modules(): + if not isinstance(module, torch.fx.GraphModule): + continue + for node in module.graph.nodes: + if node.target in [ + torch.ops.aten._assert_async.msg, + torch.ops.aten._assert_scalar.default, + torch.ops.aten.sym_constrain_range_for_size.default, + torch.ops.aten.sym_constrain_range.default, + torch.ops.aten._assert_tensor_metadata.default, + ]: + assert_async_node = node + if len(assert_async_node.users) > 0: + continue + module.graph.erase_node(assert_async_node) + # the upstream scalar_tensor <- {le, ge} <- sym_size + # linear chain of nodes of nodes is removed by the + # downstream dead code elimination + modified = True + + # We don't necessarily want to run DCE here because it could affect + # nodes that are in the module_call_graph attribute of the exported + # program. We will leave it to the pass caller to call DCE. + return PassResult(graph_module, modified) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/replace_autocast_with_hop_pass.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/replace_autocast_with_hop_pass.py new file mode 100644 index 0000000000000000000000000000000000000000..a603565f502f83eff2bdaed145a850987a7eceda --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/replace_autocast_with_hop_pass.py @@ -0,0 +1,212 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch +from torch._higher_order_ops.wrap import wrap_with_autocast + +from ..utils import node_inline_, nodes_filter, nodes_first, sequential_split +from .replace_with_hop_pass_util import ( + _replace_with_hop_helper, + _replace_with_hop_pass_helper, + _sequential_split_and_maybe_inline_subgraphs_helper, +) + + +if TYPE_CHECKING: + from torch.export.graph_signature import ExportGraphSignature + + +def _is_autocast_node(node: torch.fx.Node) -> torch.fx.Node | bool: + return ( + node + and node.op == "call_function" + and node.target + in [ + torch.amp.autocast_mode._enter_autocast, + torch.amp.autocast_mode._exit_autocast, + ] + ) + + +def _is_enter_autocast_node(node: torch.fx.Node) -> torch.fx.Node | bool: + return ( + node + and node.op == "call_function" + and node.target is torch.amp.autocast_mode._enter_autocast + ) + + +def _is_exit_autocast_node(node: torch.fx.Node) -> torch.fx.Node | bool: + return ( + node + and node.op == "call_function" + and node.target is torch.amp.autocast_mode._exit_autocast + ) + + +def _is_autocast_sub_mod(node: torch.fx.Node) -> bool: + """ + Check if the first non-placeholder node is `torch.amp.autocast_mode._enter_autocast`. + """ + if node.op == "call_module": + if not isinstance(node.target, str): + raise AssertionError(f"expected str target, got {type(node.target)}") + subgm = getattr(node.graph.owning_module, node.target) + first_non_ph = nodes_first( + subgm.graph.nodes, lambda node: node.op != "placeholder" + ) + if ( + first_non_ph + and first_non_ph.op == "call_function" + and first_non_ph.target is torch.amp.autocast_mode._enter_autocast + ): + # TODO: check if current auto-cast type is the same as the args of + # _enter_autocast. If so, return False, i.e. do not create a submodule. + return True + return False + + +def _check_valid_autocast_block( + enter_autocast_node: torch.fx.Node, exit_autocast_node: torch.fx.Node +) -> None: + if not _is_enter_autocast_node(enter_autocast_node): + raise AssertionError( + f"expected enter_autocast node, got {enter_autocast_node.target}" + ) + if not _is_exit_autocast_node(exit_autocast_node): + raise AssertionError( + f"expected exit_autocast node, got {exit_autocast_node.target}" + ) + if exit_autocast_node.args[0] != enter_autocast_node: + raise AssertionError( + "exit_autocast_node.args[0] must match enter_autocast_node" + ) + + +def _replace_with_hop(node: torch.fx.Node) -> None: + if node.op != "call_module": + raise AssertionError(f"expected call_module op, got {node.op}") + graph: torch.fx.Graph = node.graph + if graph.owning_module is None: + raise AssertionError("graph.owning_module must not be None") + gm: torch.fx.GraphModule = graph.owning_module + if not isinstance(node.target, str): + raise AssertionError(f"expected str target, got {type(node.target)}") + sub_gm = getattr(gm, node.target) + sub_graph = sub_gm.graph + autocast_nodes = nodes_filter(sub_graph.nodes, _is_autocast_node) + if len(autocast_nodes) > 0: + if len(autocast_nodes) <= 1: + raise AssertionError( + f"need at least an enter node and an exit node, got {len(autocast_nodes)}" + ) + enter_autocast_node = autocast_nodes[0] + exit_autocast_node = autocast_nodes[-1] + _check_valid_autocast_block(enter_autocast_node, exit_autocast_node) + + _replace_with_hop_helper(node, enter_autocast_node, wrap_with_autocast) + sub_graph.erase_node(exit_autocast_node) + sub_graph.erase_node(enter_autocast_node) + + +def _split_autocast(gm: torch.fx.GraphModule) -> torch.fx.GraphModule: + """ + split_autocast creates a new graph module that splits the input graph module into multiple submodules + based on the `_enter_autocast` and `_exit_autocast` nodes. It doesn't mutate the input graph module. + + Nodes between the **outer-most** `_enter_autocast` and `_exit_autocast(_enter_autocast)` are split + into a submodule. Nested autocast regions are not split. + `_enter_autocast` and `_exit_autocast(_enter_autocast)` nodes are in the submodule as well. + + Below is an example of splitting. A, B, C, D, E are blocks of non-autocast nodes in the original graph + module. Nodes marked with the same number are grouped into the same submodule. + A # 0 + enter_autocast # 1 + B # 1 + exit_autocast # 1 + C # 2 + enter_autocast # 3 + D # 3 + exit_autocast # 3 + E # 4 + """ + enter_autocast_node_stack: list[torch.fx.Node] = [] + first_node_after_outer_most_exit: bool = False + + def node_call_back(node: torch.fx.Node) -> bool: + nonlocal enter_autocast_node_stack, first_node_after_outer_most_exit + increment_id = False + if first_node_after_outer_most_exit or ( + len(enter_autocast_node_stack) == 0 and _is_enter_autocast_node(node) + ): + if len(enter_autocast_node_stack) != 0: + raise AssertionError( + f"expected empty stack, got {len(enter_autocast_node_stack)} items" + ) + first_node_after_outer_most_exit = False + increment_id = True + if _is_enter_autocast_node(node): + enter_autocast_node_stack.append(node) + elif _is_exit_autocast_node(node): + if len(enter_autocast_node_stack) == 0: + raise AssertionError("enter_autocast_node_stack must not be empty") + last_enter_autocast_node = enter_autocast_node_stack.pop() + if node.args[0] != last_enter_autocast_node: + raise AssertionError("exit node args[0] must match last enter node") + if len(enter_autocast_node_stack) == 0: + # next node should be in the next submodule since + # autocast block ends + first_node_after_outer_most_exit = True + return increment_id + + return sequential_split(gm, node_call_back) + + +def _sequential_split_and_maybe_inline_subgraphs( + gm: torch.fx.GraphModule, graph_signature: ExportGraphSignature | None +) -> tuple[torch.fx.GraphModule, ExportGraphSignature | None]: + """ + Helper function for replace_autocast_with_hop_pass(). + Split the graph module into multiple subgraphs based on the autocast nodes. + For each subgraph, decides whether to construct a HOO subgraph, or inline the calls + back into the parent graph module. + Nodes between `_enter_autocast` and `_exit_autocast(_enter_autocast)` are considered + as a subgraph. + """ + need_replacing = any(_is_autocast_node(node) for node in gm.graph.nodes) + if not need_replacing: + return gm, graph_signature + + # split_autocast returns a new graph module that could have different output + # args names. We need to fix the graph signature in `_sequential_split_and_maybe_inline_subgraphs_helper`. + new_gm = _split_autocast(gm) + + def _maybe_inline_or_replace_with_hop(node: torch.fx.Node) -> None: + if _is_autocast_sub_mod(node): + _replace_with_hop(node) + else: + if node.op != "call_module": + raise AssertionError(f"expected call_module op, got {node.op}") + if not isinstance(node.target, str): + raise AssertionError(f"expected str target, got {type(node.target)}") + node_inline_(node) + + return _sequential_split_and_maybe_inline_subgraphs_helper( + new_gm, graph_signature, _maybe_inline_or_replace_with_hop + ) + + +def replace_autocast_with_hop_pass( + gm: torch.fx.GraphModule, graph_signature: ExportGraphSignature | None +) -> tuple[torch.fx.GraphModule, ExportGraphSignature | None]: + """ + Split gm into sub-graph-modules using `sequential_split_and_maybe_inline_subgraphs`, and + then recursively call itself on each of the submodules. + """ + return _replace_with_hop_pass_helper( + gm, + graph_signature, + _sequential_split_and_maybe_inline_subgraphs, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/replace_quantized_ops_with_standard_ops_pass.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/replace_quantized_ops_with_standard_ops_pass.py new file mode 100644 index 0000000000000000000000000000000000000000..9be342f0d188d1e00c6ceca772078e565153dab5 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/replace_quantized_ops_with_standard_ops_pass.py @@ -0,0 +1,700 @@ +# mypy: allow-untyped-defs +import logging +import operator + +import torch +import torch.export._trace +from torch._ops import OpOverload +from torch.ao.quantization.fx._decomposed import ( + dequantize_per_channel, + dequantize_per_tensor, + quantize_per_tensor, +) +from torch.ao.quantization.utils import calculate_qmin_qmax +from torch.fx.graph_module import _assign_attr + + +log = logging.getLogger(__name__) + +# Those values will need to be carried over multiple operators. +_INPUT_Q_DTYPE: torch.dtype | torch.fx.Node | None = None +_SCALE: float | torch.fx.Node | None = None +_ZERO_POINT: float | torch.fx.Node | None = None + + +def int_to_valid_dtype(val: int) -> torch.dtype: + from torch._export.converter import _TORCH_ENUM_TO_DTYPE # No circular import. + + if isinstance(val, torch.dtype): + return val + dtype = _TORCH_ENUM_TO_DTYPE[val] + if dtype == torch.quint8: + return torch.uint8 + elif dtype == torch.qint8: + return torch.int8 + return dtype + + +def fx_enum_to_dtype(gm: torch.fx.GraphModule, val: int) -> torch.fx.Node: + return gm.graph.call_function(int_to_valid_dtype, (val,)) + + +def insert_quantized_node( + gm: torch.fx.GraphModule, + val_node: torch.fx.Node, + scale_node: float | torch.fx.Node, + zero_point_node: float | torch.fx.Node, + qmin_node: float | int | torch.fx.Node, + qmax_node: float | int | torch.fx.Node, + dtype_node: torch.dtype | torch.fx.Node, + qscheme: torch.qscheme | None, +) -> torch.fx.Node: + return gm.graph.call_function( + quantize_per_tensor, + ( + val_node, + scale_node, + zero_point_node, + qmin_node, + qmax_node, + dtype_node, + ), + ) + + +def get_dequantized( + val: torch.Tensor, + scale: float | torch.Tensor, + zero_point: float | torch.Tensor, + qmin: float | int, + qmax: float | int, + dtype: torch.dtype, + axis: int | None, + qscheme: torch.qscheme | None, +) -> torch.Tensor: + if qscheme is torch.per_tensor_affine: + return dequantize_per_tensor( + val, + scale, # type: ignore[arg-type] + zero_point, # type: ignore[arg-type] + qmin, # type: ignore[arg-type] + qmax, # type: ignore[arg-type] + dtype, + ) + elif qscheme is torch.per_channel_affine: + return dequantize_per_channel( + val, + scale, # type: ignore[arg-type] + zero_point, # type: ignore[arg-type] + axis, # type: ignore[arg-type] + qmin, # type: ignore[arg-type] + qmax, # type: ignore[arg-type] + dtype, + ) + else: + raise RuntimeError(f"Unsupported dequantization scheme: {qscheme}") + + +def insert_dequantized_node( + gm: torch.fx.GraphModule, + val_node: torch.fx.Node, + scale_node: float | torch.fx.Node, + zero_point_node: float | torch.fx.Node, + qmin_node: float | int | torch.fx.Node, + qmax_node: float | int | torch.fx.Node, + dtype_node: torch.dtype | torch.fx.Node, + axis_node: int | torch.fx.Node | None, + qscheme: torch.qscheme | None, +) -> torch.fx.Node: + if qscheme is torch.per_tensor_affine: + return gm.graph.call_function( + dequantize_per_tensor, + ( + val_node, + scale_node, + zero_point_node, + qmin_node, + qmax_node, + dtype_node, + ), + ) + elif qscheme is torch.per_channel_affine: + return gm.graph.call_function( + dequantize_per_channel, + ( + val_node, + scale_node, + zero_point_node, + axis_node, + qmin_node, + qmax_node, + dtype_node, + ), + ) + else: + raise RuntimeError(f"Unsupported dequantization scheme: {qscheme}") + + +def get_qmin_qmax(dtype: torch.dtype) -> tuple[int | float, int | float]: + return calculate_qmin_qmax(None, None, False, dtype, False) # type: ignore[arg-type] + + +def insert_qmin_qmax_node( + gm: torch.fx.GraphModule, dtype_node: torch.dtype | torch.fx.Node +) -> tuple[torch.fx.Node, torch.fx.Node]: + q_min_max_node = gm.graph.call_function( + calculate_qmin_qmax, (None, None, False, dtype_node, False) + ) + qmin_node = gm.graph.call_function(operator.getitem, (q_min_max_node, 0)) + qmax_node = gm.graph.call_function(operator.getitem, (q_min_max_node, 1)) + return qmin_node, qmax_node + + +def get_script_object( + gm: torch.nn.Module, node: torch.fx.Node +) -> torch._C.ScriptObject: + if not isinstance(node, torch.fx.Node): + raise AssertionError(f"expected fx.Node, got {type(node).__name__}") + if node.op != "get_attr": + raise AssertionError(f"expected get_attr op, got {node.op}") + attr_name = node.target + if not isinstance(attr_name, str): + raise AssertionError(f"expected str target, got {type(attr_name).__name__}") + + mod = gm + for attr in attr_name.split("."): + mod = getattr(mod, attr) + if not isinstance(mod, torch._C.ScriptObject): + raise AssertionError(f"expected ScriptObject, got {type(mod).__name__}") + return mod + + +def insert_weight_and_bias_get_attr_node_from_get_attr_to_scriptobject( + gm: torch.fx.GraphModule, + param_node: torch.fx.Node, +) -> tuple[torch.fx.Node, torch.fx.Node | None]: + """Directly inline tensor from a get_attr fx node.""" + mod = get_script_object(gm, param_node) + w_qtensor, b_qtensor = mod.unpack() # type: ignore[attr-defined] + w_attr_name, b_attr_name = ( + f"dequantized_{param_node.target}_w", + f"dequantized_{param_node.target}_b", + ) + return insert_weight_and_bias_get_attr_node( + gm, w_qtensor, b_qtensor, w_attr_name, b_attr_name + ) + + +def insert_weight_and_bias_get_attr_node_from_get_attr_to_qtensor( + gm: torch.fx.GraphModule, + get_attr_to_weight_node: torch.fx.Node, + get_attr_to_bias_node: torch.fx.Node | None, +) -> tuple[torch.fx.Node, torch.fx.Node | None]: + if not isinstance(get_attr_to_weight_node.target, str): + raise AssertionError( + f"expected str target, got {type(get_attr_to_weight_node.target).__name__}" + ) + w_qtensor = getattr(gm, get_attr_to_weight_node.target) + w_attr_name = f"dequantized_{get_attr_to_weight_node.target}_w" + + if get_attr_to_bias_node is not None: + if not isinstance(get_attr_to_bias_node.target, str): + raise AssertionError( + f"expected str target, got {type(get_attr_to_bias_node.target).__name__}" + ) + b_qtensor = getattr(gm, get_attr_to_bias_node.target) + b_attr_name = f"dequantized_{get_attr_to_bias_node.target}_b" + else: + b_qtensor, b_attr_name = None, "" + + return insert_weight_and_bias_get_attr_node( + gm, w_qtensor, b_qtensor, w_attr_name, b_attr_name + ) + + +def insert_weight_and_bias_get_attr_node( + gm: torch.fx.GraphModule, + w_qtensor: torch.Tensor, + b_qtensor: torch.Tensor | None, + w_attr_name: str, + b_attr_name: str, +) -> tuple[torch.fx.Node, torch.fx.Node | None]: + w_tensor = get_tensor_from_qtensor(w_qtensor) + _assign_attr(w_tensor, gm, w_attr_name) + w_tensor_attr = gm.graph.get_attr(w_attr_name) + + if b_qtensor is not None: + b_tensor = get_tensor_from_qtensor(b_qtensor, dequant=False) + _assign_attr(b_tensor, gm, b_attr_name) + b_tensor_attr = gm.graph.get_attr(b_attr_name) + else: + b_tensor_attr = None + + return w_tensor_attr, b_tensor_attr + + +def get_tensor_from_qtensor( + qtensor: torch.Tensor, dequant: bool = True +) -> torch.Tensor: + # Manual conversion because qint8 is not used anymore. + if qtensor.dtype in [torch.qint8, torch.quint8]: + tensor = qtensor.int_repr() + else: + tensor = qtensor + + # Weights need dequantization with scaling and zero_point adjustment, but + # bias does not need that. + if dequant: + qscheme = qtensor.qscheme() + if qscheme == torch.per_channel_affine: + scale, zero_point, axis = ( + qtensor.q_per_channel_scales(), + qtensor.q_per_channel_zero_points(), + qtensor.q_per_channel_axis(), + ) + else: + scale, zero_point, axis = ( + qtensor.q_scale(), # type: ignore[assignment] + qtensor.q_zero_point(), # type: ignore[assignment] + None, + ) + dtype = tensor.dtype + qmin, qmax = get_qmin_qmax(dtype) + return get_dequantized( + tensor, scale, zero_point, qmin, qmax, dtype, axis, qscheme + ) + return tensor + + +def insert_fused_activation_node( + gm: torch.fx.GraphModule, opname: str, fx_node: torch.fx.Node +) -> torch.fx.Node: + if opname in ["conv1d_relu", "conv2d_relu", "linear_relu", "add_relu", "mul_relu"]: + fx_node = gm.graph.call_function(torch.ops.aten.relu, (fx_node,)) + return fx_node + + +def _conv1d_op_with_squeeze( + inp: torch.Tensor, + weight: torch.Tensor, + bias: torch.Tensor | None, + stride: list[int], + padding: list[int], + dilation: list[int], + groups: int, +) -> torch.Tensor: + # In quantized version, conv1d is emulated using conv2d with squeeze and unsqueeze + # operations before and after the conv2d operation to match the dimension of weights. + # Reference: https://github.com/pytorch/pytorch/blob/eca0cb0fbe84bb0a34fa94afe261bceecd52c436/aten/src/ATen/native/quantized/cpu/qconv.cpp#L1827 # noqa: B950 + s_inp = torch.ops.aten.unsqueeze(inp, 2) + conv1d_res = torch.ops.aten.conv2d( + s_inp, + weight, + bias, + stride, + padding, + dilation, + groups, + ) + uns_conv1d_res = torch.ops.aten.squeeze(conv1d_res, 2) + return uns_conv1d_res + + +def _transform_conv_with_packedparam(gm: torch.fx.GraphModule, node: torch.fx.Node): + """Conv specific transformation function.""" + if not isinstance(node.target, torch._ops.OpOverload): + raise AssertionError(f"expected OpOverload, got {type(node.target).__name__}") + opname = node.target._opname + scale_node, zero_point_node = node.args[2], node.args[3] + + op_f = ( + torch.ops.aten.conv2d + if opname in ["conv2d", "conv2d_relu"] + else _conv1d_op_with_squeeze + ) + + inp_node, param_node = node.args[0], node.args[1] + if not isinstance(inp_node, torch.fx.Node): + raise AssertionError(f"expected fx.Node for inp, got {type(inp_node)}") + if not isinstance(param_node, torch.fx.Node): + raise AssertionError(f"expected fx.Node for param, got {type(param_node)}") + + if param_node.op == "call_function": + # Using Conv2dPrepackParam from conv_prepack. + # We directly skip the packing call and inline weights and bias. + w_node, b_node = param_node.args[0], param_node.args[1] + if not isinstance(w_node, torch.fx.Node): + raise AssertionError(f"expected fx.Node for w, got {type(w_node)}") + if b_node is not None and not isinstance(b_node, torch.fx.Node): + raise AssertionError(f"expected fx.Node for b, got {type(b_node)}") + ( + param_0, + param_1, + ) = insert_weight_and_bias_get_attr_node_from_get_attr_to_qtensor( + gm, w_node, b_node + ) + op_res_node = gm.graph.call_function( + op_f, (inp_node, param_0, param_1, *param_node.args[2:]) + ) + else: + # Using ConvPrepackedParam. + param = get_script_object(gm, param_node) + ( + param_0, + param_1, + ) = insert_weight_and_bias_get_attr_node_from_get_attr_to_scriptobject( + gm, param_node + ) # type: ignore[assignment] + op_res_node = gm.graph.call_function( + op_f, + ( + inp_node, + param_0, + param_1, + param.stride(), # type: ignore[attr-defined] + param.padding(), # type: ignore[attr-defined] + param.dilation(), # type: ignore[attr-defined] + param.groups(), # type: ignore[attr-defined] + ), + ) + return op_res_node, scale_node, zero_point_node + + +def _transform_linear_with_packedparam(gm: torch.fx.GraphModule, node: torch.fx.Node): + """Linear specific transformation function.""" + scale_node, zero_point_node = node.args[2], node.args[3] + + inp_node, param_node = node.args[0], node.args[1] + if not isinstance(inp_node, torch.fx.Node): + raise AssertionError(f"expected fx.Node for inp, got {type(inp_node)}") + if not isinstance(param_node, torch.fx.Node): + raise AssertionError(f"expected fx.Node for param, got {type(param_node)}") + + if param_node.op == "call_function": + # Using LinearPrepackParam from linear_prepack. + # We directly skip the packing call and inline weights and bias. + w_node, b_node = param_node.args[0], param_node.args[1] + if not isinstance(w_node, torch.fx.Node): + raise AssertionError(f"expected fx.Node for w, got {type(w_node)}") + if b_node is not None and not isinstance(b_node, torch.fx.Node): + raise AssertionError(f"expected fx.Node for b, got {type(b_node)}") + ( + param_0, + param_1, + ) = insert_weight_and_bias_get_attr_node_from_get_attr_to_qtensor( + gm, w_node, b_node + ) + op_res_node = gm.graph.call_function( + torch.ops.aten.linear, (inp_node, param_0, param_1, *param_node.args[2:]) + ) + else: + # Using LinearPackedParams. + ( + param_0, + param_1, + ) = insert_weight_and_bias_get_attr_node_from_get_attr_to_scriptobject( + gm, param_node + ) # type: ignore[assignment] + op_res_node = gm.graph.call_function( + torch.ops.aten.linear, (inp_node, param_0, param_1) + ) + return op_res_node, scale_node, zero_point_node + + +def _transform_op_where_last_two_arguments_are_scale_and_zero_point( + gm: torch.fx.GraphModule, node: torch.fx.Node +): + """ + This transformation function can be used for function where the last two + parameters are scale and zero point. Additionally, the function's parameters + do not need any unpacking. + """ + to_standard_op = { + "mul": torch.ops.aten.mul, + "mul_relu": torch.ops.aten.mul, + "add": torch.ops.aten.add, + "add_relu": torch.ops.aten.add, + "softmax": torch.ops.aten.softmax, + "cat": torch.ops.aten.cat, + "hardswish": torch.ops.aten.hardswish, + } + + if not isinstance(node.target, torch._ops.OpOverload): + raise AssertionError(f"expected OpOverload, got {type(node.target).__name__}") + opname, args = node.target._opname, node.args + scale_node, zero_point_node = args[-2], args[-1] + op_res_node = gm.graph.call_function(to_standard_op[opname], tuple(args[:-2])) + return op_res_node, scale_node, zero_point_node + + +def _transform_scalar_arithmetic(gm: torch.fx.GraphModule, node: torch.fx.Node): + """Transform scalar overload for basic arithmetic.""" + to_standard_op = { + "mul": torch.ops.aten.mul.Scalar, + "add": torch.ops.aten.add.Scalar, + } + if not isinstance(node.target, torch._ops.OpOverload): + raise AssertionError(f"expected OpOverload, got {type(node.target).__name__}") + opname, args = node.target._opname, node.args + op_res_node = gm.graph.call_function(to_standard_op[opname], args) + return op_res_node, _SCALE, _ZERO_POINT + + +def _transform_prepacked_op(gm: torch.fx.GraphModule, node: torch.fx.Node): + """ + Transformation for functions under prepacked namespace, where they share + the same handling logic that [...]OpContext contains all parameters. + """ + if not isinstance(node.target, torch._ops.OpOverload): + raise AssertionError(f"expected OpOverload, got {type(node.target).__name__}") + opname, args = node.target._opname, node.args + op_f = None + if opname == "conv2d_clamp_run": + op_f = torch.ops.aten.conv2d + elif opname == "linear_clamp_run": + op_f = torch.ops.aten.linear + else: + raise RuntimeError(f"Invalid operator {opname}") + + if not isinstance(args[1], torch.fx.Node): + raise AssertionError(f"expected fx.Node for args[1], got {type(args[1])}") + so = get_script_object(gm, args[1]) + + func_args = [] + func_args += [args[0]] + func_args += so.unpack()[:2] # type: ignore[attr-defined] + if opname == "conv2d_clamp_run": + func_args += torch.ops.prepacked.unpack_prepacked_sizes_conv2d(so)[2:] + + op_res_node = gm.graph.call_function(op_f, tuple(func_args)) + return op_res_node + + +def _transform_batch_norm(gm: torch.fx.GraphModule, node: torch.fx.Node): + args = node.args + scale_node, zero_point_node = args[-2], args[-1] + op_res_node = gm.graph.call_function( + torch.ops.aten.native_batch_norm, (*args[:-3], False, 0.1, args[-3]) + ) + op_res_node = gm.graph.call_function(operator.getitem, (op_res_node, 0)) + return op_res_node, scale_node, zero_point_node + + +def fx_transform_quantized_op_to_standard_op( + gm: torch.fx.GraphModule, node: torch.fx.Node +) -> torch.fx.Node: + global _SCALE, _ZERO_POINT, _INPUT_Q_DTYPE + + if not isinstance(node.target, torch._ops.OpOverload): + raise AssertionError(f"expected OpOverload, got {type(node.target).__name__}") + opname, overload = node.target._opname, node.target._overloadname + + key = f"{opname}.{overload}" + opname_to_transform_f = { + "conv1d.new": _transform_conv_with_packedparam, + "conv1d_relu.new": _transform_conv_with_packedparam, + "conv1d.default": _transform_conv_with_packedparam, + "conv1d_relu.default": _transform_conv_with_packedparam, + "conv2d.new": _transform_conv_with_packedparam, + "conv2d_relu.new": _transform_conv_with_packedparam, + "conv2d.default": _transform_conv_with_packedparam, + "conv2d_relu.default": _transform_conv_with_packedparam, + "linear.default": _transform_linear_with_packedparam, + "linear_relu.default": _transform_linear_with_packedparam, + "add.default": _transform_op_where_last_two_arguments_are_scale_and_zero_point, + "add_relu.default": _transform_op_where_last_two_arguments_are_scale_and_zero_point, + "mul.default": _transform_op_where_last_two_arguments_are_scale_and_zero_point, + "mul_relu.default": _transform_op_where_last_two_arguments_are_scale_and_zero_point, + "softmax.default": _transform_op_where_last_two_arguments_are_scale_and_zero_point, + "cat.default": _transform_op_where_last_two_arguments_are_scale_and_zero_point, + "hardswish.default": _transform_op_where_last_two_arguments_are_scale_and_zero_point, + "batch_norm2d.default": _transform_batch_norm, + "mul.Scalar": _transform_scalar_arithmetic, + "add.Scalar": _transform_scalar_arithmetic, + } + + if f"{key}" not in opname_to_transform_f: + raise RuntimeError(f"Unsupported quantized op during transformation: {key}") + + op_res_node, scale_node, zero_point_node = opname_to_transform_f[f"{key}"](gm, node) + + # Add fused activation layer. + op_res_node = insert_fused_activation_node(gm, opname, op_res_node) + _SCALE, _ZERO_POINT = scale_node, zero_point_node + + if _INPUT_Q_DTYPE is None: + raise AssertionError("_INPUT_Q_DTYPE should not be None") + qmin_node, qmax_node = insert_qmin_qmax_node(gm, _INPUT_Q_DTYPE) + q_fx_node = insert_quantized_node( + gm, + op_res_node, + scale_node, + zero_point_node, + qmin_node, + qmax_node, + _INPUT_Q_DTYPE, + torch.per_tensor_affine, + ) + dq_fx_node = insert_dequantized_node( + gm, + q_fx_node, + scale_node, + zero_point_node, + qmin_node, + qmax_node, + _INPUT_Q_DTYPE, + None, + torch.per_tensor_affine, + ) + return dq_fx_node + + +def replace_quantized_ops_with_standard_ops(gm: torch.fx.GraphModule): + """ + Replace legacy quantized ops (aten.quantize_per_tensor, quantized.conv) with + PT2 ops (quantize_decomposed.quantize_per_tensor, aten.conv). + + Before: x || -> aten.q || -> quantized.conv2d || -> quantized.linear || -> aten.dq || -> y + + After: x || -> qd.q -> qd.dq || -> aten.conv2d -> qd.q -> qd.dq || aten.linear -> qd.q -> qd.dq || -> y + + (qd == quantized_decomposed library, q = quantize, dq = dequantize) + ^ + | + getattr(w), getattr(b) from Conv2dParamPrepack + + During each iteration, the transformation spits out the transformed operator, its quantized output, + and its dequantized value together. We did this because dequantization need to use the + scale and zero point parameters from the quantization to recover the approximate original value. After each + iteration, the new dequantization node will be used as the input to the next node (e.g., dq2 -> linear). + + For operators like conv2d and linear, their weights and bias are packed in a quantized format in the ScriptObject. + During the transformation, we unpack those objects, get their dequantized tensor, populate those + as attributes to the module, and use getattr to access them. + + One exception in the transformation is conv_prepack and linear_prepack. Those calls pack + weight and bias constant tensors into ScriptObject, which are then used by subsequent conv2d or linear calls. + During transformation, we directly skip transforming conv_prepack or linear_prepack. We check whether ScriptObject to the + quantized::conv2d or linear is from conv_prepack or linear_prepack. If it is, we then inline those parameters + to the operator by converting them to a getattr fx.node. + + For prepacked::conv2d_clamp_run and prepacked::linear_clamp_run, we directly convert them to aten.conv2d and aten.linear + without the need of doing de/quantization. + + Three global variables defined are _INPUT_Q_DTYPE, _SCALE, _ZERO_POINT. _INPUT_Q_DTYPE determines the de/quantization + data type, which is the same across the entire program, but it only shows up in the very first quantization + call. _SCALE and _ZERO_POINT are used only when operators do not have those specified. E.g., mul.Scalar. + """ + + global _INPUT_Q_DTYPE + + quantized = False + + last_quantized_node = None + # pyrefly: ignore [bad-assignment] + for node in gm.graph.nodes: + if isinstance(node.target, OpOverload): + with gm.graph.inserting_before(node): + namespace, opname = node.target.namespace, node.target._opname + if namespace == "quantized" and opname not in [ + "conv_prepack", + "linear_prepack", + ]: + quantized = True + fx_node = fx_transform_quantized_op_to_standard_op(gm, node) + node.replace_all_uses_with(fx_node) + last_quantized_node = fx_node + elif namespace == "prepacked": + quantized = True + fx_node = _transform_prepacked_op(gm, node) + node.replace_all_uses_with(fx_node) + last_quantized_node = fx_node + elif namespace == "aten" and opname == "quantize_per_tensor": + inp_node, scale_node, zero_point_node, dtype_node = node.args + dtype_node = fx_enum_to_dtype(gm, dtype_node) + _INPUT_Q_DTYPE = dtype_node + qmin_node, qmax_node = insert_qmin_qmax_node(gm, dtype_node) + q_fx_node = insert_quantized_node( + gm, + inp_node, + scale_node, + zero_point_node, + qmin_node, + qmax_node, + dtype_node, + torch.per_tensor_affine, + ) + dq_fx_node = insert_dequantized_node( + gm, + q_fx_node, + scale_node, + zero_point_node, + qmin_node, + qmax_node, + dtype_node, + None, + torch.per_tensor_affine, + ) + node.replace_all_uses_with(dq_fx_node) + last_quantized_node = dq_fx_node + elif namespace == "aten" and opname == "dequantize": + if last_quantized_node is None: + raise AssertionError("last_quantized_node should not be None") + node.replace_all_uses_with(last_quantized_node) + else: + last_quantized_node = node + + # Post-processing again to remove legacy ScriptObjects and quantizated tensors + # stored as attributes or in the buffer. This is used to clean up the GraphModule + # to not trigger tracing errors like missing __obj_flatten__ functions. + def _clean_attr(mod: torch.nn.Module): + for submod in mod.modules(): + attr_names_to_clean = set() + for k, v in submod.__dict__.items(): + if isinstance(v, torch.ScriptObject): + attr_names_to_clean.add(k) + if k == "_buffers": + buffer_name_to_clean = set() + + for b_name, b_value in v.items(): + if isinstance(b_value, torch.Tensor) and b_value.dtype in [ + torch.qint8, + torch.quint8, + ]: + buffer_name_to_clean.add(b_name) + for b_name in buffer_name_to_clean: + v.pop(b_name, None) + for attr_name in attr_names_to_clean: + delattr(submod, attr_name) + + if quantized: + """ + TODO: SetAttr + quantized ops will result incorrect program. This flag is used to temporarily + bypass test cases. + + The deadcode elimination pass is needed to remove legacy quantized ops. Otherwise, retracing + will throw errors. However, the current way of SetAttr does inplace update to attributes, so + this pass regard them as dead code and remove them. Below is an example of GraphModule before + and after the dead code elimination pass. + + class GraphModule(torch.nn.Module): + def forward(self, x_1): + # No stacktrace found for following nodes + data = self.data; data = None + data_1 = self.data + add_tensor = torch.ops.aten.add.Tensor(data_1, x_1, alpha = 1); data_1 = None + data_2 = self.data + copy_ = torch_Tensor_copy_(data_2, add_tensor); data_2 = add_tensor = copy_ = None + data_3 = self.data + add_tensor_1 = torch.ops.aten.add.Tensor(x_1, data_3, alpha = 1); x_1 = data_3 = None + return add_tensor_1 + + class GraphModule(torch.nn.Module): + def forward(self, x_1): + # No stacktrace found for following nodes + data_3 = self.data + add_tensor_1 = torch.ops.aten.add.Tensor(x_1, data_3, alpha = 1); x_1 = data_3 = None + return add_tensor_1 + """ + gm.graph.eliminate_dead_code() + _clean_attr(gm) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/replace_set_grad_with_hop_pass.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/replace_set_grad_with_hop_pass.py new file mode 100644 index 0000000000000000000000000000000000000000..c1c5f1a5703edd89ef1742a6856760971b3ada50 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/replace_set_grad_with_hop_pass.py @@ -0,0 +1,131 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch +from torch._higher_order_ops.wrap import wrap_with_set_grad_enabled + +from ..utils import node_inline_, nodes_filter, nodes_first, nodes_map, sequential_split +from .replace_with_hop_pass_util import ( + _replace_with_hop_helper, + _replace_with_hop_pass_helper, + _sequential_split_and_maybe_inline_subgraphs_helper, +) + + +if TYPE_CHECKING: + from torch.export.graph_signature import ExportGraphSignature + + +def _is_set_grad_enabled_node(node: torch.fx.Node) -> torch.fx.Node | bool: + return ( + node + and node.op == "call_function" + and node.target is torch._C._set_grad_enabled + ) + + +def _is_set_grad_enabled_sub_mod( + node: torch.fx.Node, omit_if_same_with_ambient: bool = False +) -> bool | torch.Tensor: + if node.op == "call_module": + if not isinstance(node.target, str): + raise AssertionError(f"expected str target, got {type(node.target)}") + subgm = getattr(node.graph.owning_module, node.target) + first_non_ph = nodes_first( + subgm.graph.nodes, lambda node: node.op != "placeholder" + ) + if ( + first_non_ph + and first_non_ph.op == "call_function" + and first_non_ph.target is torch._C._set_grad_enabled + ): + return ( + first_non_ph.args[0] != torch.is_grad_enabled() + if omit_if_same_with_ambient + else True + ) + return False + + +def _replace_with_hop(node: torch.fx.Node) -> None: + if node.op != "call_module": + raise AssertionError(f"expected call_module op, got {node.op}") + graph: torch.fx.Graph = node.graph + if graph.owning_module is None: + raise AssertionError("graph.owning_module must not be None") + gm: torch.fx.GraphModule = graph.owning_module + if not isinstance(node.target, str): + raise AssertionError(f"expected str target, got {type(node.target)}") + sub_gm = getattr(gm, node.target) + sub_graph = sub_gm.graph + set_grad_nodes = nodes_filter(sub_graph.nodes, _is_set_grad_enabled_node) + if len(set_grad_nodes) > 0: + if len(set_grad_nodes) != 1: + raise AssertionError( + f"expected exactly 1 set_grad node, got {len(set_grad_nodes)}" + ) + set_grad_node = set_grad_nodes[0] + _replace_with_hop_helper(node, set_grad_node, wrap_with_set_grad_enabled) + sub_graph.erase_node(set_grad_node) + + +def _remove_set_grad_and_inline(node: torch.fx.Node) -> None: + if node.op != "call_module": + raise AssertionError(f"expected call_module op, got {node.op}") + graph: torch.fx.Graph = node.graph + if graph.owning_module is None: + raise AssertionError("graph.owning_module must not be None") + gm: torch.fx.GraphModule = graph.owning_module + if not isinstance(node.target, str): + raise AssertionError(f"expected str target, got {type(node.target)}") + sub_gm = getattr(gm, node.target) + sub_graph = sub_gm.graph + nodes_map( + sub_graph.nodes, + lambda n: sub_graph.erase_node(n) if _is_set_grad_enabled_node(n) else n, + ) + node_inline_(node) + + +def _sequential_split_and_maybe_inline_subgraphs( + gm: torch.fx.GraphModule, graph_signature: ExportGraphSignature | None +) -> tuple[torch.fx.GraphModule, ExportGraphSignature | None]: + """ + Helper function for replace_set_grad_with_hop_pass(). + Split the graph module into multiple subgraphs based on the set_grad_enabled nodes. + For each subgraph, decides whether to construct a HOO subgraph, or inline the calls + back into the parent graph module. + """ + need_replacing = any(_is_set_grad_enabled_node(node) for node in gm.graph.nodes) + if not need_replacing: + return gm, graph_signature + + # sequential_split returns a new graph module that could have different output + # args names. We need to fix the graph signature. + new_gm = sequential_split(gm, _is_set_grad_enabled_node) + + def _maybe_inline_or_replace_with_hop(node: torch.fx.Node): + if _is_set_grad_enabled_sub_mod(node, omit_if_same_with_ambient=True): + _replace_with_hop(node) + else: + _remove_set_grad_and_inline(node) + + return _sequential_split_and_maybe_inline_subgraphs_helper( + new_gm, graph_signature, _maybe_inline_or_replace_with_hop + ) + + +def replace_set_grad_with_hop_pass( + gm: torch.fx.GraphModule, graph_signature: ExportGraphSignature | None +) -> tuple[torch.fx.GraphModule, ExportGraphSignature | None]: + """ + Split gm into sub-graph-modules using `sequential_split_and_maybe_inline_subgraphs`, and + then recursively call itself on each of the submodules. + """ + return _replace_with_hop_pass_helper( + gm, + graph_signature, + _sequential_split_and_maybe_inline_subgraphs, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/replace_view_ops_with_view_copy_ops_pass.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/replace_view_ops_with_view_copy_ops_pass.py new file mode 100644 index 0000000000000000000000000000000000000000..98d93e16559b77e9fc812096a010261cdaf05fde --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/replace_view_ops_with_view_copy_ops_pass.py @@ -0,0 +1,64 @@ +# mypy: allow-untyped-defs + +import torch +from torch._export.error import InternalError +from torch._export.pass_base import _ExportPassBaseDeprecatedDoNotUse +from torch._ops import HigherOrderOperator, OpOverload + + +__all__ = ["ReplaceViewOpsWithViewCopyOpsPass"] + + +_NON_FUNCTIONAL_OPS_TO_FUNCTIONAL_OPS: dict[OpOverload, OpOverload] = { + torch.ops.aten._unsafe_view.default: torch.ops.aten.view_copy.default, +} + + +def is_view_op(schema: torch._C.FunctionSchema) -> bool: + if len(schema.arguments) == 0: + return False + alias_info = schema.arguments[0].alias_info + return (alias_info is not None) and (not alias_info.is_write) + + +def get_view_copy_of_view_op(schema: torch._C.FunctionSchema) -> OpOverload | None: + if is_view_op(schema) and schema.name.startswith("aten::"): + view_op_name = schema.name.split("::")[1] + view_op_overload = ( + schema.overload_name if schema.overload_name != "" else "default" + ) + view_copy_op_name = view_op_name + "_copy" + if not hasattr(torch.ops.aten, view_copy_op_name): + raise InternalError(f"{schema.name} is missing a view_copy variant") + + view_copy_op_overload_packet = getattr(torch.ops.aten, view_copy_op_name) + + if not hasattr(view_copy_op_overload_packet, view_op_overload): + raise InternalError(f"{schema.name} is missing a view_copy variant") + + return getattr(view_copy_op_overload_packet, view_op_overload) + + return None + + +class ReplaceViewOpsWithViewCopyOpsPass(_ExportPassBaseDeprecatedDoNotUse): + """ + Our backend expects pure functional operators. For efficiency + purposes, we keep view ops around while functionalizing the exported + program. This pass replaces view ops with view copy ops for backends that + need AOT memory planning. + """ + + def call_operator(self, op, args, kwargs, meta): + if op in _NON_FUNCTIONAL_OPS_TO_FUNCTIONAL_OPS: + return super().call_operator( + (_NON_FUNCTIONAL_OPS_TO_FUNCTIONAL_OPS[op]), args, kwargs, meta + ) + + if isinstance(op, HigherOrderOperator): + return super().call_operator(op, args, kwargs, meta) + + if view_copy_op := get_view_copy_of_view_op(op._schema): + return super().call_operator(view_copy_op, args, kwargs, meta) + + return super().call_operator(op, args, kwargs, meta) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/replace_with_hop_pass_util.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/replace_with_hop_pass_util.py new file mode 100644 index 0000000000000000000000000000000000000000..9242ee8faf6f6a171d998df4f86135892ef9d97e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/passes/replace_with_hop_pass_util.py @@ -0,0 +1,202 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import contextlib +import copy +import operator +from typing import TYPE_CHECKING + +import torch + +from ..utils import node_replace_, nodes_map + + +if TYPE_CHECKING: + from collections.abc import Callable + + from torch._ops import HigherOrderOperator + from torch.export.graph_signature import ExportGraphSignature + + +def _replace_with_hop_helper( + node: torch.fx.Node, + enter_block_node: torch.fx.Node, + wrap_hoo: HigherOrderOperator, +) -> None: + graph: torch.fx.Graph = node.graph + if graph.owning_module is None: + raise AssertionError("graph.owning_module must not be None") + gm: torch.fx.GraphModule = graph.owning_module + if not isinstance(node.target, str): + raise AssertionError(f"expected str target, got {type(node.target)}") + sub_gm = getattr(gm, node.target) + + def set_hoo_node_meta(call_func_node): + call_func_node.meta["nn_module_stack"] = copy.copy( + enter_block_node.meta.get("nn_module_stack", {}) + ) + call_func_node.meta["torch_fn"] = ( + f"{wrap_hoo.__name__}", + # pyrefly: ignore [missing-attribute] + f"{wrap_hoo.__class__.__name__}.{wrap_hoo.__name__}", + ) + if isinstance(output_args, (tuple, list)): + call_func_node.meta["val"] = tuple(arg.meta["val"] for arg in output_args) + elif isinstance(output_args, torch.fx.Node): + call_func_node.meta["val"] = (output_args.meta["val"],) + + with graph.inserting_before(node): + get_attr_node = graph.get_attr(node.target) + get_attr_node.meta["nn_module_stack"] = copy.copy( + enter_block_node.meta.get("nn_module_stack", {}) + ) + output_node = next(iter(reversed(sub_gm.graph.nodes)), None) + # Split_module pass intentionally doesn't add output node + # if the graph doesn't return anything. + # TODO (tmanlaibaatar) Figure out if this is right behaviour + # for split_module + if isinstance(output_node, torch.fx.Node) and output_node.op != "output": + output_node = None + if output_node is not None: + if len(output_node.args) != 1: + raise AssertionError( + f"expected 1 output arg, got {len(output_node.args)}" + ) + output_args = output_node.args[0] + enter_block_node_args = enter_block_node.args + if isinstance(output_args, (tuple, list)): + call_func_node = graph.call_function( + wrap_hoo, + (*enter_block_node_args, get_attr_node, *node.args), + {}, + ) + # Create the metadata + set_hoo_node_meta(call_func_node) + node_replace_(node, call_func_node) + + # Rename the name of getitem nodes to the actual name of its contents + # for passing verifier and better readability, also propagate metadata + for get_item_node in call_func_node.users: + idx: int = get_item_node.args[1] # type: ignore[assignment] + output_node = output_args[idx] + get_item_node._rename(output_node.name) + get_item_node.meta = output_node.meta + + elif isinstance(output_args, torch.fx.Node): + call_func_node = graph.create_node( + "call_function", + wrap_hoo, + (*enter_block_node_args, get_attr_node, *node.args), + {}, + output_args.name, + ) + # Modify the subgraph to output a singleton list. + output_node.args = ((output_args,),) + # Add in an extra `getitem(wrap_hoo, 0)` node to the toplevel graph. + get_item_node = graph.create_node( + "call_function", + operator.getitem, + (call_func_node, 0), + {}, + ) + # Create the metadata + get_item_node.meta = output_args.meta + set_hoo_node_meta(call_func_node) + node_replace_(node, get_item_node) + else: + raise NotImplementedError( + f"replace_with_hop_pass doesn't support output type {type(output_args)}" + ) + else: + # TODO (shangdiy): remove this line, since the export graph can be non-functional + node.graph.erase_node(node) + + +def _sequential_split_and_maybe_inline_subgraphs_helper( + new_gm: torch.fx.GraphModule, + graph_signature: ExportGraphSignature | None, + maybe_inline_or_replace_with_hop: Callable[[torch.fx.Node], None], +) -> tuple[torch.fx.GraphModule, ExportGraphSignature | None]: + """ + Helper function for replacing graph nodse with higher order nodes. + For each subgraph in `new_gm`, decides whether to construct a HOO subgraph, or inline the calls + back into the parent graph module, depending on `maybe_inline_or_replace_with_hop`. + """ + # new_gm is a new graph module that could have different output args names. + # We need to fix the graph signature. + replace_ctx = contextlib.nullcontext() + new_signature = None + if graph_signature is not None: + # Cannot deep copy a real ScriptObject, which is referenced + # in the FakeScriptObject. Copy should be good enough to guard + # against accidental mutation to original graph_signature. + new_signature = copy.copy(graph_signature) + new_gm_out_node = next(reversed(new_gm.graph.find_nodes(op="output"))) + if new_gm_out_node.op != "output" or len(new_gm_out_node.args[0]) != len( + new_signature.output_specs + ): + raise AssertionError( + f"output node mismatch: {new_gm_out_node.op}, " + f"{len(new_gm_out_node.args[0])} vs {len(new_signature.output_specs)}" + ) + for arg_node, out_spec in zip( + new_gm_out_node.args[0], new_signature.output_specs + ): + if arg_node is None: + if out_spec.arg.value is not None: # type: ignore[union-attr] + raise AssertionError( + f"expected None out_spec.arg.value, got {out_spec.arg.value}" # type: ignore[union-attr] + ) + elif ( + isinstance(arg_node, torch.fx.Node) + and out_spec.arg.name != arg_node.name + ): + out_spec.arg.name = arg_node.name + + replace_ctx = new_gm._set_replace_hook(new_signature.get_replace_hook()) # type: ignore[assignment] + + with replace_ctx: + nodes_map( + list(new_gm.graph.nodes), + lambda node: ( + maybe_inline_or_replace_with_hop(node) + if node.op == "call_module" + else node + ), + ) + new_gm.recompile() + new_gm.graph.lint() + return new_gm, new_signature + + +def _replace_with_hop_pass_helper( + gm: torch.fx.GraphModule, + graph_signature: ExportGraphSignature | None, + sequential_split_and_maybe_inline_subgraphs: Callable[ + [torch.fx.GraphModule, ExportGraphSignature | None], + tuple[torch.fx.GraphModule, ExportGraphSignature | None], + ], +) -> tuple[torch.fx.GraphModule, ExportGraphSignature | None]: + """ + Split gm into sub-graph-modules using `sequential_split_and_maybe_inline_subgraphs`, and + then recursively call itself on each of the submodules. + """ + new_gm, new_signature = sequential_split_and_maybe_inline_subgraphs( + gm, graph_signature + ) + # recursively call + for node in new_gm.graph.nodes: + if node.op == "get_attr": + subgm = getattr(new_gm, node.target) + if not isinstance(subgm, torch.fx.GraphModule): + continue + new_subgm, _ = _replace_with_hop_pass_helper( + subgm, + None, + sequential_split_and_maybe_inline_subgraphs, + ) + setattr(new_gm, node.target, new_subgm) + + new_gm.recompile() + new_gm.graph.lint() + return new_gm, new_signature diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/dynamic_shapes.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/dynamic_shapes.py new file mode 100644 index 0000000000000000000000000000000000000000..32fe6014b04447ebe8f51ce8e855a737fbddd5d6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/dynamic_shapes.py @@ -0,0 +1,327 @@ +import dataclasses +from typing import Any + +import torch +from torch._dynamo.exc import UserError, UserErrorType +from torch.export.dynamic_shapes import ( + _check_dynamic_shapes, + _DerivedDim, + _DimHint, + _tree_map_with_path, + Dim, +) +from torch.utils._pytree import tree_map + +from .serialize import _dataclass_to_dict + + +@dataclasses.dataclass +class RootDim: + """ + This represents a Dim object. + """ + + min: int + max: int | None + derived: list[str] + + +@dataclasses.dataclass +class DynamicShapesSpec: + """ + This stores a dynamic_shapes spec for de/serialization. + """ + + dynamic_shapes: dict[str, Any] | tuple[Any] | list[Any] | None + dims: dict[str, RootDim] + + +def _postprocess_serialized_shapes( + dynamic_shapes: dict[str, Any] | tuple[Any] | list[Any] | None, + dims: dict[str, dict[str, int | list[str] | None]], + to_dict: bool | None = False, +) -> DynamicShapesSpec | dict[str, Any]: + """ + Sorts dims and dumps to dictionary format. + """ + from torch.utils._sympy.numbers import int_oo + + # pyrefly: ignore [bad-assignment] + dims = { + k: RootDim( + min=v["min"], # type: ignore[arg-type] + max=None if v["max"] is int_oo else v["max"], # type: ignore[arg-type] + derived=sorted(v["derived"]), # type: ignore[arg-type] + ) + for k, v in sorted(dims.items()) + } + # pyrefly: ignore [bad-argument-type] + spec = DynamicShapesSpec(dynamic_shapes=dynamic_shapes, dims=dims) + if to_dict: + return _dataclass_to_dict(spec) + else: + return spec + + +def _dump_dynamic_shapes( + dynamic_shapes: dict[str, Any] | tuple[Any] | list[Any] | None, + args: tuple[Any], + kwargs: dict[str, Any] | None = None, + to_dict: bool | None = False, +) -> DynamicShapesSpec | dict[str, Any]: + """ + Utility function for dynamic shapes serialization, serializing a dynamic_shapes spec. + Returns a DynamicShapesSpec dataclass containing 2 fields, "dynamic_shapes" and "dims". + Uses args & kwargs to distinguish between tensor-level and dim-level specs (only for Nones). + + dynamic_shapes: A pytree structure mirroring the dynamic_shapes input to export(): + - Each tensor input is represented with a list of values, non-tensor inputs with None. + - dynamic dimensions (i.e. symbols) in tensors and Dim enums are represented with strings. + - static dimensions are represented with ints. + + dims: A dictionary mapping each symbol name to the min/max range and derived dim names. + + For example: + ``` + dx = Dim("dx", min=4, max=16) + dy = dx + 1 + + inputs = ( + [ + torch.randn(4, 4), + torch.randn(5, 4), + ], + torch.randn(4), + torch.randn(4, 4), + "hello", + ) + dynamic_shapes = { + "a": [ + (dx, 4), + (dy, 4), + ], + "b": (Dim.STATIC,), + "c": None, + "d": None, + } + out = _dump_dynamic_shapes(dynamic_shapes, inputs, to_dict=True) + ``` + would generate the following output: + ``` + { + "dynamic_shapes": ( + [ + ["dx", 4], + ["dx + 1", 4], + ], + ["_DimHint.STATIC"], + ["_DimHint.STATIC", "_DimHint.STATIC"], + None, + ), + "dims": { + "dx": { + "min": 4, + "max": 16, + "derived": ["dx + 1"], + }, + }, + } + ``` + """ + dims: dict[str, dict[str, Any]] = {} + + def _standardize_shapes(path, tensor, shape): # type: ignore[no-untyped-def] + """ + Helps standardize the dynamic_shapes tree structure we serialize, + returning lists for each tensor shape, handling tensor-level Nones. + """ + if not isinstance(tensor, torch.Tensor): + return None + if shape is None: + return [Dim.STATIC] * len(tensor.shape) + + out = [] + if isinstance(shape, dict): + for i, s in enumerate(tensor.shape): + out.append(s if shape.get(i) is None else shape.get(i)) + else: + if not isinstance(shape, (tuple, list)): + raise AssertionError(f"expected tuple or list, got {type(shape)}") + for i, s in enumerate(tensor.shape): + out.append(s if shape[i] is None else shape[i]) + return out + + def _track_dim_from_dims( + val: None | int | _DimHint | Dim, + ) -> None | int | str: + """ + Tracks dims, ranges, derived dims from the standardized dynamic_shapes spec. + """ + if val is None or isinstance(val, int): # non-tensor input or static + return val + if isinstance(val, _DimHint): # store enum as string + return val.__class__.__name__ + "." + val.type.name + + if not isinstance(val, Dim): + raise AssertionError(f"expected Dim, got {type(val)}") + + # track root dim + root = val.root if isinstance(val, _DerivedDim) else val # type: ignore[attr-defined] + if root.__name__ not in dims: + dims[root.__name__] = { + "min": root.min, # type: ignore[attr-defined,union-attr] + "max": root.max, # type: ignore[attr-defined,union-attr] + "derived": set(), + } + + # track derived dims + if isinstance(val, _DerivedDim): + dims[root.__name__]["derived"].add(val.__name__) + + return val.__name__ + + if dynamic_shapes is None: + return {"dynamic_shapes": None, "dims": {}} + + # convert to tuple of specs, for each arg/kwarg + kwargs = kwargs or {} + if isinstance(dynamic_shapes, dict): + dynamic_shapes = dynamic_shapes.values() # type: ignore[assignment] + # pyrefly: ignore [bad-assignment, bad-argument-type] + dynamic_shapes = tuple(dynamic_shapes) + combined_args = tuple(args) + tuple(kwargs.values()) + + # run same check when we're processing shapes for export - is this too lazy? + _check_dynamic_shapes(dict(enumerate(combined_args)), dynamic_shapes) # type: ignore[arg-type] + + tree_shapes = _tree_map_with_path( + _standardize_shapes, combined_args, dynamic_shapes, tree_name="inputs" + ) + serialized_shapes = tree_map(_track_dim_from_dims, tree_shapes) + return _postprocess_serialized_shapes(serialized_shapes, dims, to_dict=to_dict) + + +def _load_dynamic_shapes( + spec: DynamicShapesSpec | dict[str, Any], + from_dict: bool | None = False, +) -> dict[str, Any] | tuple[Any] | list[Any] | None: + """ + Utility function for dynamic shapes serialization. + Deserializes a DynamicShapesSpec or corresponding dictionary into a dynamic_shapes input to export(). + """ + import sympy + + from torch.fx.experimental.symbolic_shapes import _is_supported_equivalence + + if from_dict: + if not isinstance(spec, dict): + raise UserError( + UserErrorType.INVALID_INPUT, + f"With from_dict=True, expected `spec` to be a dict, got {type(spec)}", + ) + if sorted(spec.keys()) != ["dims", "dynamic_shapes"]: + raise UserError( + UserErrorType.INVALID_INPUT, + "With from_dict=True, expected `spec` to have keys `dims` and `dynamic_shapes`, " + f"instead found {spec.keys()}", + ) + dims = {} + for k, v in spec["dims"].items(): + if not isinstance(k, str): + raise UserError( + UserErrorType.INVALID_INPUT, + f"Expected `spec['dims']` keys to be strings for symbols, got key {type(k)}", + ) + if sorted(v.keys()) != ["derived", "max", "min"]: + raise UserError( + UserErrorType.INVALID_INPUT, + f"Expected `spec['dims']` values to have keys `derived`, `max`, and `min`, " + f"instead found {v.keys()}", + ) + if not isinstance(v["min"], int): + raise UserError( + UserErrorType.INVALID_INPUT, + f"Expected dims in `spec['dims']` to map `min` to an int, got {k}: {v['min']}", + ) + if not isinstance(v["max"], int) or v["max"] is None: + raise UserError( + UserErrorType.INVALID_INPUT, + f"Expected dims in `spec['dims']` to map `max` to an int or None, got {k}: {v['max']}", + ) + if not isinstance(v["derived"], list) or any( + not isinstance(d, str) for d in v["derived"] + ): + raise UserError( + UserErrorType.INVALID_INPUT, + "Expected dims in `spec['dims']` to map `derived` to a list of derived expressions, " + f"got {k}: {v['derived']}", + ) + dims[k] = RootDim(**v) + dynamic_shapes = spec["dynamic_shapes"] + else: + if not isinstance(spec, DynamicShapesSpec): + raise UserError( + UserErrorType.INVALID_INPUT, + f"Expected `spec` to be a DynamicShapesSpec, got {type(spec)}", + ) + dims = spec.dims + dynamic_shapes = spec.dynamic_shapes + + if dynamic_shapes is None: + return None + + dim_cache = {} + for name, info in dims.items(): + symbol = sympy.sympify(name) + if not isinstance(symbol, sympy.Symbol): + raise UserError( + UserErrorType.INVALID_INPUT, + f"Expected `spec['dims']` keys to be symbols, got {name}", + ) + dim_cache[name] = Dim(name, min=info.min, max=info.max) # cache root dim + for _expr in info.derived: + expr = sympy.sympify(_expr) + if len(expr.free_symbols) != 1 or symbol not in expr.free_symbols: + raise UserError( + UserErrorType.INVALID_INPUT, + f"Expected derived expressions in to have {name} as the only free symbol, got {expr}", + ) + if not _is_supported_equivalence(expr): + raise UserError( + UserErrorType.INVALID_INPUT, + f"Expected derived expressions to be linear expressions, got {expr}", + ) + modulus, remainder = sympy.polys.polytools.div(expr, symbol) + ddim = dim_cache[name] + if modulus != 1: + ddim = int(modulus) * ddim # type: ignore[assignment, operator] + if remainder != 0: + ddim = ddim + int(remainder) # type: ignore[assignment, operator] + dim_cache[_expr] = ddim # cache derived dims + + def deserialize_shape( + val: None | int | str, + ) -> None | int | Dim | _DimHint: + if val is None or isinstance(val, int): + return val + elif val == "_DimHint.AUTO": + return _DimHint.AUTO() + elif val == "_DimHint.DYNAMIC": + return _DimHint.DYNAMIC() + elif val == "_DimHint.STATIC": + return _DimHint.STATIC() + if not isinstance(val, str): + raise UserError( + UserErrorType.INVALID_INPUT, + "Expected leaves in `spec['dynamic_shapes']` to be ints, None, Dim.AUTO/STATIC, symbols, " + f" or derived expressions, got {val}", + ) + if val not in dim_cache: + raise UserError( + UserErrorType.INVALID_INPUT, + "Expected dims in `spec['dynamic_shapes']` to be tracked in `spec['dims']`, " + f"got {val} which is not in {dims.keys()}", + ) + return dim_cache[val] # type: ignore[return-value] + + return tree_map(deserialize_shape, dynamic_shapes) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/export_schema.thrift b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/export_schema.thrift new file mode 100644 index 0000000000000000000000000000000000000000..5268efc883809b1b70c229e311ff90a5de7f50b5 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/export_schema.thrift @@ -0,0 +1,383 @@ +// @generated by update_schema.py +// checksum<> + +namespace py3 torch._export +namespace cpp2 torch._export.schema + +enum ArgumentKind { + UNKNOWN = 0, + POSITIONAL = 1, + KEYWORD = 2, +} + + +enum Layout { + Unknown = 0, + SparseCoo = 1, + SparseCsr = 2, + SparseCsc = 3, + SparseBsr = 4, + SparseBsc = 5, + _mkldnn = 6, + Strided = 7, +} + + +enum MemoryFormat { + Unknown = 0, + ContiguousFormat = 1, + ChannelsLast = 2, + ChannelsLast3d = 3, + PreserveFormat = 4, +} + + +enum ScalarType { + UNKNOWN = 0, + BYTE = 1, + CHAR = 2, + SHORT = 3, + INT = 4, + LONG = 5, + HALF = 6, + FLOAT = 7, + DOUBLE = 8, + COMPLEXHALF = 9, + COMPLEXFLOAT = 10, + COMPLEXDOUBLE = 11, + BOOL = 12, + BFLOAT16 = 13, + UINT16 = 28, + FLOAT8E4M3FN = 29, + FLOAT8E5M2 = 30, + FLOAT8E4M3FNUZ = 31, + FLOAT8E5M2FNUZ = 32, + FLOAT8E8M0FNU = 33, + UINT32 = 34, + UINT64 = 35, +} + + +struct Device { + 10: string type; + 20: optional i64 index; +} + +union SymExprHint { + 10: i64 as_int; + 20: bool as_bool; + 30: double as_float; +} + +struct SymExpr { + 10: string expr_str; + 20: optional SymExprHint hint; +} + +union SymInt { + 10: SymExpr as_expr; + 20: i64 as_int; +} + +union SymFloat { + 10: SymExpr as_expr; + 20: double as_float; +} + +union SymBool { + 10: SymExpr as_expr; + 20: bool as_bool; +} + +struct TensorMeta { + 10: ScalarType dtype; + 20: list sizes; + 30: bool requires_grad; + 40: Device device; + 50: list strides; + 60: SymInt storage_offset; + 70: Layout layout; +} + +union SymIntArgument { + 10: string as_name; + 20: i64 as_int; +} + +union SymFloatArgument { + 10: string as_name; + 20: double as_float; +} + +union SymBoolArgument { + 10: string as_name; + 20: bool as_bool; +} + +struct TensorArgument { + 10: string name; +} + +struct TokenArgument { + 10: string name; +} + +union OptionalTensorArgument { + 20: TensorArgument as_tensor; + 10: bool as_none; +} + +struct GraphArgument { + 10: string name; + 20: Graph graph; +} + +struct CustomObjArgument { + 10: string name; + 20: string class_fqn; +} + +struct ComplexValue { + 10: double real; + 20: double imag; +} + +union Argument { + 10: bool as_none; + 20: TensorArgument as_tensor; + 30: list as_tensors; + 50: i64 as_int; + 70: list as_ints; + 80: double as_float; + 90: list as_floats; + 100: string as_string; + 101: list as_strings; + 110: SymIntArgument as_sym_int; + 120: list as_sym_ints; + 130: ScalarType as_scalar_type; + 140: MemoryFormat as_memory_format; + 150: Layout as_layout; + 160: Device as_device; + 170: bool as_bool; + 180: list as_bools; + 182: SymBoolArgument as_sym_bool; + 184: list as_sym_bools; + 200: GraphArgument as_graph; + 190: list as_optional_tensors; + 210: CustomObjArgument as_custom_obj; + 220: string as_operator; + 230: SymFloatArgument as_sym_float; + 240: list as_sym_floats; + 250: OptionalTensorArgument as_optional_tensor; + 260: ComplexValue as_complex; + 270: list> as_nested_tensors; + 280: list> as_int_lists; + 290: map as_string_to_argument; + 300: list> as_float_lists; +} + +struct NamedArgument { + 10: string name; + 20: Argument arg; + 30: optional ArgumentKind kind; +} + +struct Node { + 10: string target; + 20: list inputs; + 30: list outputs; + 40: map metadata; + 50: optional bool is_hop_single_tensor_return; + 60: optional string name; +} + +struct Graph { + 10: list inputs; + 20: list outputs; + 30: list nodes; + 40: map tensor_values; + 50: map sym_int_values; + 60: map sym_bool_values; + 70: bool is_single_tensor_return; + 80: map custom_obj_values; + 90: map sym_float_values; +} + +struct UserInputSpec { + 10: Argument arg; +} + +union ConstantValue { + 10: bool as_none; + 20: i64 as_int; + 30: double as_float; + 40: string as_string; + 50: bool as_bool; +} + +struct InputToConstantInputSpec { + 10: string name; + 20: ConstantValue value; +} + +struct InputToParameterSpec { + 10: TensorArgument arg; + 20: string parameter_name; +} + +struct InputToBufferSpec { + 10: TensorArgument arg; + 20: string buffer_name; + 30: bool persistent; +} + +struct InputToTensorConstantSpec { + 10: TensorArgument arg; + 20: string tensor_constant_name; +} + +struct InputToCustomObjSpec { + 10: CustomObjArgument arg; + 20: string custom_obj_name; +} + +struct InputTokenSpec { + 10: TokenArgument arg; +} + +union InputSpec { + 10: UserInputSpec user_input; + 20: InputToParameterSpec parameter; + 30: InputToBufferSpec buffer; + 40: InputToTensorConstantSpec tensor_constant; + 50: InputToCustomObjSpec custom_obj; + 70: InputTokenSpec token; + 60: InputToConstantInputSpec constant_input; +} + +struct UserOutputSpec { + 10: Argument arg; +} + +struct LossOutputSpec { + 10: TensorArgument arg; +} + +struct BufferMutationSpec { + 10: TensorArgument arg; + 20: string buffer_name; +} + +struct ParameterMutationSpec { + 10: TensorArgument arg; + 20: string parameter_name; +} + +struct GradientToParameterSpec { + 10: TensorArgument arg; + 20: string parameter_name; +} + +struct GradientToUserInputSpec { + 10: TensorArgument arg; + 20: string user_input_name; +} + +struct UserInputMutationSpec { + 10: TensorArgument arg; + 20: string user_input_name; +} + +struct OutputTokenSpec { + 10: TokenArgument arg; +} + +union OutputSpec { + 10: UserOutputSpec user_output; + 20: LossOutputSpec loss_output; + 30: BufferMutationSpec buffer_mutation; + 40: GradientToParameterSpec gradient_to_parameter; + 50: GradientToUserInputSpec gradient_to_user_input; + 60: UserInputMutationSpec user_input_mutation; + 70: OutputTokenSpec token; + 80: ParameterMutationSpec parameter_mutation; +} + +struct GraphSignature { + 10: list input_specs; + 20: list output_specs; +} + +struct RangeConstraint { + 10: optional i64 min_val; + 20: optional i64 max_val; +} + +struct ModuleCallSignature { + 10: list inputs; + 20: list outputs; + 30: string in_spec; + 40: string out_spec; + 50: optional list forward_arg_names; +} + +struct ModuleCallEntry { + 10: string fqn; + 30: optional ModuleCallSignature signature; +} + +struct NamedTupleDef { + 10: list field_names; +} + +struct GraphModule { + 10: Graph graph; + 50: GraphSignature signature; + 60: list module_call_graph; + 40: map metadata; + 70: map treespec_namedtuple_fields; +} + +struct SchemaVersion { + 10: i64 major; + 20: i64 minor; +} + +struct ExportedProgram { + 10: GraphModule graph_module; + 20: map opset_version; + 30: map range_constraints; + 60: SchemaVersion schema_version; + 70: list verifiers; + 80: string torch_version; + 90: list guards_code; +} + +struct PayloadMeta { + 10: string path_name; + 20: bool is_param; + 30: bool use_pickle; + 40: optional TensorMeta tensor_meta; +} + +struct PayloadConfig { + 10: map config; +} + +struct AOTInductorModelPickleData { + 1: string library_basename; + 2: list input_names; + 3: list output_names; + 4: optional i64 floating_point_input_dtype; + 5: optional i64 floating_point_output_dtype; + 6: optional bool aot_inductor_model_is_cpu; +} + +struct ExternKernelNode { + 10: string name; + 20: Node node; +} + +struct ExternKernelNodes { + 10: list nodes; +} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/schema.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/schema.py new file mode 100644 index 0000000000000000000000000000000000000000..c7ee7b43495bd5f299cc880b08a5e86c3f35ba3e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/schema.py @@ -0,0 +1,574 @@ +# NOTE: This is a placeholder for iterating on export serialization schema design. +# Anything is subject to change and no guarantee is provided at this point. + +from dataclasses import dataclass, field +from enum import IntEnum +from typing import Annotated + +from torch._export.serde.union import _Union, _union_dataclass + + +# NOTE: Please update this value if any modifications are made to the schema +SCHEMA_VERSION = (8, 20) +TREESPEC_VERSION = 1 + + +# NOTE: If you updated the schema, please run `scripts/export/update_schema.py` +# to update the auto generated files. +# +# There are also mappings from serialized enum values to c10 enum member names. +# These are used by scripts/export/update_schema.py to generate C++ conversion +# functions for oss_proxy_executor.cpp. +# When adding new enum values, update both the enum AND the mapping. +class ScalarType(IntEnum): + UNKNOWN = 0 + BYTE = 1 + CHAR = 2 + SHORT = 3 + INT = 4 + LONG = 5 + HALF = 6 + FLOAT = 7 + DOUBLE = 8 + COMPLEXHALF = 9 + COMPLEXFLOAT = 10 + COMPLEXDOUBLE = 11 + BOOL = 12 + BFLOAT16 = 13 + UINT16 = 28 + FLOAT8E4M3FN = 29 + FLOAT8E5M2 = 30 + FLOAT8E4M3FNUZ = 31 + FLOAT8E5M2FNUZ = 32 + FLOAT8E8M0FNU = 33 + UINT32 = 34 + UINT64 = 35 + + +class Layout(IntEnum): + Unknown = 0 + SparseCoo = 1 + SparseCsr = 2 + SparseCsc = 3 + SparseBsr = 4 + SparseBsc = 5 + _mkldnn = 6 + Strided = 7 + + +class MemoryFormat(IntEnum): + Unknown = 0 + ContiguousFormat = 1 + ChannelsLast = 2 + ChannelsLast3d = 3 + PreserveFormat = 4 + + +SCALAR_TYPE_TO_C10: dict[int, str] = { + ScalarType.BYTE: "Byte", + ScalarType.CHAR: "Char", + ScalarType.SHORT: "Short", + ScalarType.INT: "Int", + ScalarType.LONG: "Long", + ScalarType.HALF: "Half", + ScalarType.FLOAT: "Float", + ScalarType.DOUBLE: "Double", + ScalarType.COMPLEXHALF: "ComplexHalf", + ScalarType.COMPLEXFLOAT: "ComplexFloat", + ScalarType.COMPLEXDOUBLE: "ComplexDouble", + ScalarType.BOOL: "Bool", + ScalarType.BFLOAT16: "BFloat16", + ScalarType.UINT16: "UInt16", + ScalarType.FLOAT8E4M3FN: "Float8_e4m3fn", + ScalarType.FLOAT8E5M2: "Float8_e5m2", + ScalarType.FLOAT8E4M3FNUZ: "Float8_e4m3fnuz", + ScalarType.FLOAT8E5M2FNUZ: "Float8_e5m2fnuz", + ScalarType.FLOAT8E8M0FNU: "Float8_e8m0fnu", + ScalarType.UINT32: "UInt32", + ScalarType.UINT64: "UInt64", +} + +LAYOUT_TO_C10: dict[int, str] = { + Layout.SparseCoo: "Sparse", + Layout.SparseCsr: "SparseCsr", + Layout.SparseCsc: "SparseCsc", + Layout.SparseBsr: "SparseBsr", + Layout.SparseBsc: "SparseBsc", + Layout._mkldnn: "Mkldnn", + Layout.Strided: "Strided", +} + +MEMORY_FORMAT_TO_C10: dict[int, str] = { + MemoryFormat.ContiguousFormat: "Contiguous", + MemoryFormat.ChannelsLast: "ChannelsLast", + MemoryFormat.ChannelsLast3d: "ChannelsLast3d", + MemoryFormat.PreserveFormat: "Preserve", +} + + +@dataclass +class Device: + type: Annotated[str, 10] + index: Annotated[int | None, 20] = None + + +@_union_dataclass +class SymExprHint(_Union): + as_int: Annotated[int, 10] + as_bool: Annotated[bool, 20] + as_float: Annotated[float, 30] + + +# This is for storing the symbolic expressions behind symints/symfloats/symbools +# For example, we can get something like +# SymExpr(expr_str="s0 + s1", hint=SymExprHint(as_int=4) +# if we also have the hint that s0 and s1 are both 2. +@dataclass +class SymExpr: + expr_str: Annotated[str, 10] + hint: Annotated[SymExprHint | None, 20] = None + + +@_union_dataclass +class SymInt(_Union): + as_expr: Annotated[SymExpr, 10] + as_int: Annotated[int, 20] + + +@_union_dataclass +class SymFloat(_Union): + as_expr: Annotated[SymExpr, 10] + as_float: Annotated[float, 20] + + +@_union_dataclass +class SymBool(_Union): + as_expr: Annotated[SymExpr, 10] + as_bool: Annotated[bool, 20] + + +@dataclass +class TensorMeta: + dtype: Annotated[ScalarType, 10] + sizes: Annotated[list[SymInt], 20] + requires_grad: Annotated[bool, 30] + device: Annotated[Device, 40] + strides: Annotated[list[SymInt], 50] + storage_offset: Annotated[SymInt, 60] + layout: Annotated[Layout, 70] + + +# In most cases we will use the "as_name" field to store arguments which are +# SymInts. +# The "as_int" field is used in the case where we have a list containing a mix +# of SymInt and ints (ex. [1, s0, ...]). We will serialize this type of list to +# be List[SymIntArgument] and map the SymInts to the "as_name" field, and ints +# to the "as_int" field. +@_union_dataclass +class SymIntArgument(_Union): + as_name: Annotated[str, 10] + as_int: Annotated[int, 20] + + +# In most cases we will use the "as_name" field to store arguments which are +# SymFloats. +# The "as_float" field is used in the case where we have a list containing a mix +# of SymFloat and float (ex. [1.0, s0, ...]). We will serialize this type of list to +# be List[SymFloatArgument] and map the SymFloats to the "as_name" field, and ints +# to the "as_float" field. +@_union_dataclass +class SymFloatArgument(_Union): + as_name: Annotated[str, 10] + as_float: Annotated[float, 20] + + +# In most cases we will use the "as_name" field to store arguments which are +# SymBools. +# The "as_bool" field is used in the case where we have a list containing a mix +# of SymBool and bools (ex. [True, i0, ...]). We will serialize this type of list to +# be List[SymboolArgument] and map the SymBools to the "as_name" field, and bools +# to the "as_bool" field. +@_union_dataclass +class SymBoolArgument(_Union): + as_name: Annotated[str, 10] + as_bool: Annotated[bool, 20] + + +@dataclass +class TensorArgument: + name: Annotated[str, 10] + + +@dataclass +class TokenArgument: + name: Annotated[str, 10] + + +# This is use for storing the contents of a list which contain optional tensors +# (Tensor?[], ex. [Tensor, None, ...]), where the list will be serialized to the +# type List[OptionalTensorArgument], with tensor values serialized to the +# "as_tensor" field, and None values serialized to the "as_none" field. +@_union_dataclass +class OptionalTensorArgument(_Union): + as_tensor: Annotated[TensorArgument, 20] + as_none: Annotated[bool, 10] + + +@dataclass +class GraphArgument: + name: Annotated[str, 10] + graph: Annotated["Graph", 20] + + +@dataclass +class CustomObjArgument: + name: Annotated[str, 10] + class_fqn: Annotated[str, 20] + + +@dataclass +class ComplexValue: + real: Annotated[float, 10] + imag: Annotated[float, 20] + + +# This is actually a union type +@_union_dataclass +class Argument(_Union): + as_none: Annotated[bool, 10] + as_tensor: Annotated[TensorArgument, 20] + as_tensors: Annotated[list[TensorArgument], 30] + as_int: Annotated[int, 50] + as_ints: Annotated[list[int], 70] + as_float: Annotated[float, 80] + as_floats: Annotated[list[float], 90] + as_string: Annotated[str, 100] + as_strings: Annotated[list[str], 101] + as_sym_int: Annotated[SymIntArgument, 110] + as_sym_ints: Annotated[list[SymIntArgument], 120] + as_scalar_type: Annotated[ScalarType, 130] + as_memory_format: Annotated[MemoryFormat, 140] + as_layout: Annotated[Layout, 150] + as_device: Annotated[Device, 160] + as_bool: Annotated[bool, 170] + as_bools: Annotated[list[bool], 180] + as_sym_bool: Annotated[SymBoolArgument, 182] + as_sym_bools: Annotated[list[SymBoolArgument], 184] + as_graph: Annotated[GraphArgument, 200] + as_optional_tensors: Annotated[list[OptionalTensorArgument], 190] + as_custom_obj: Annotated[CustomObjArgument, 210] + as_operator: Annotated[str, 220] + as_sym_float: Annotated[SymFloatArgument, 230] + as_sym_floats: Annotated[list[SymFloatArgument], 240] + as_optional_tensor: Annotated[OptionalTensorArgument, 250] + as_complex: Annotated[ComplexValue, 260] + as_nested_tensors: Annotated[list[list[TensorArgument]], 270] + as_int_lists: Annotated[list[list[int]], 280] + as_string_to_argument: Annotated[dict[str, "Argument"], 290] + as_float_lists: Annotated[list[list[float]], 300] + + +class ArgumentKind(IntEnum): + UNKNOWN = 0 + POSITIONAL = 1 + KEYWORD = 2 + + +@dataclass +class NamedArgument: + # Argument name from the operator schema + name: Annotated[str, 10] + arg: Annotated[Argument, 20] + kind: Annotated[ArgumentKind | None, 30] = None + + +@dataclass +class Node: + target: Annotated[str, 10] + inputs: Annotated[list[NamedArgument], 20] + outputs: Annotated[list[Argument], 30] + metadata: Annotated[dict[str, str], 40] + is_hop_single_tensor_return: Annotated[bool | None, 50] = None + # For BC, default is None so older serialized models without 'name' can be loaded. + name: Annotated[str | None, 60] = None + + +@dataclass +class Graph: + inputs: Annotated[list[Argument], 10] + outputs: Annotated[list[Argument], 20] + nodes: Annotated[list[Node], 30] + tensor_values: Annotated[dict[str, TensorMeta], 40] + sym_int_values: Annotated[dict[str, SymInt], 50] + sym_bool_values: Annotated[dict[str, SymBool], 60] + # This is for deserializing the submodule graphs from higher order ops + # (ex. cond, map) where single tensor returns will just return a single + # tensor, rather than following export schema and returning a singleton + # list. + is_single_tensor_return: Annotated[bool, 70] = False + custom_obj_values: Annotated[dict[str, CustomObjArgument], 80] = field( + default_factory=dict + ) + sym_float_values: Annotated[dict[str, SymFloat], 90] = field(default_factory=dict) + + +@dataclass +class UserInputSpec: + # Actually, only tensors and SymInts are allowed here + arg: Annotated[Argument, 10] + + +@_union_dataclass +class ConstantValue(_Union): + as_none: Annotated[bool, 10] + as_int: Annotated[int, 20] + as_float: Annotated[float, 30] + as_string: Annotated[str, 40] + as_bool: Annotated[bool, 50] + + +@dataclass +class InputToConstantInputSpec: + name: Annotated[str, 10] + value: Annotated[ConstantValue, 20] + + +@dataclass +class InputToParameterSpec: + arg: Annotated[TensorArgument, 10] + parameter_name: Annotated[str, 20] + + +@dataclass +class InputToBufferSpec: + arg: Annotated[TensorArgument, 10] + buffer_name: Annotated[str, 20] + persistent: Annotated[bool, 30] + + +@dataclass +class InputToTensorConstantSpec: + arg: Annotated[TensorArgument, 10] + tensor_constant_name: Annotated[str, 20] + + +@dataclass +class InputToCustomObjSpec: + arg: Annotated[CustomObjArgument, 10] + custom_obj_name: Annotated[str, 20] + + +@dataclass +class InputTokenSpec: + arg: Annotated[TokenArgument, 10] + + +@_union_dataclass +class InputSpec(_Union): + user_input: Annotated[UserInputSpec, 10] + parameter: Annotated[InputToParameterSpec, 20] + buffer: Annotated[InputToBufferSpec, 30] + tensor_constant: Annotated[InputToTensorConstantSpec, 40] + custom_obj: Annotated[InputToCustomObjSpec, 50] + token: Annotated[InputTokenSpec, 70] + constant_input: Annotated[InputToConstantInputSpec, 60] + + +@dataclass +class UserOutputSpec: + arg: Annotated[Argument, 10] + + +@dataclass +class LossOutputSpec: + arg: Annotated[TensorArgument, 10] + + +@dataclass +class BufferMutationSpec: + arg: Annotated[TensorArgument, 10] + buffer_name: Annotated[str, 20] + + +@dataclass +class ParameterMutationSpec: + arg: Annotated[TensorArgument, 10] + parameter_name: Annotated[str, 20] + + +@dataclass +class GradientToParameterSpec: + arg: Annotated[TensorArgument, 10] + parameter_name: Annotated[str, 20] + + +@dataclass +class GradientToUserInputSpec: + arg: Annotated[TensorArgument, 10] + user_input_name: Annotated[str, 20] + + +@dataclass +class UserInputMutationSpec: + arg: Annotated[TensorArgument, 10] + user_input_name: Annotated[str, 20] + + +@dataclass +class OutputTokenSpec: + arg: Annotated[TokenArgument, 10] + + +@_union_dataclass +class OutputSpec(_Union): + user_output: Annotated[UserOutputSpec, 10] + loss_output: Annotated[LossOutputSpec, 20] + buffer_mutation: Annotated[BufferMutationSpec, 30] + gradient_to_parameter: Annotated[GradientToParameterSpec, 40] + gradient_to_user_input: Annotated[GradientToUserInputSpec, 50] + user_input_mutation: Annotated[UserInputMutationSpec, 60] + token: Annotated[OutputTokenSpec, 70] + parameter_mutation: Annotated[ParameterMutationSpec, 80] + + +@dataclass +class GraphSignature: + input_specs: Annotated[list[InputSpec], 10] + output_specs: Annotated[list[OutputSpec], 20] + + +@dataclass +class RangeConstraint: + min_val: Annotated[int | None, 10] + max_val: Annotated[int | None, 20] + + +@dataclass +class ModuleCallSignature: + inputs: Annotated[list[Argument], 10] + outputs: Annotated[list[Argument], 20] + + # These are serialized by calling pytree.treespec_loads + # And deserialized by calling pytree.treespec_dumps + in_spec: Annotated[str, 30] + out_spec: Annotated[str, 40] + + # This field is used to prettify the graph placeholders + # after we Ser/Der and retrace + forward_arg_names: Annotated[list[str] | None, 50] = None + + +@dataclass +class ModuleCallEntry: + fqn: Annotated[str, 10] + signature: Annotated[ModuleCallSignature | None, 30] = None + + +@dataclass +class NamedTupleDef: + field_names: Annotated[list[str], 10] + + +@dataclass +class GraphModule: + graph: Annotated[Graph, 10] + signature: Annotated[GraphSignature, 50] + # This is used for unflattening, by tracking the calling structure of all of + # the modules in order to unflatten the modules back to the eager calling + # conventions. + module_call_graph: Annotated[list[ModuleCallEntry], 60] + metadata: Annotated[dict[str, str], 40] = field(default_factory=dict) + # Mapping of namedtuple types to namedtuple field names, used for BC + treespec_namedtuple_fields: Annotated[dict[str, NamedTupleDef], 70] = field( + default_factory=dict + ) + + +# Invariant: Every time a change is made to the schema, one of the versions +# should be updated. +@dataclass +class SchemaVersion: + major: Annotated[ + int, 10 + ] # Major version number is bumped every time a breaking change is made. + minor: Annotated[ + int, 20 + ] # Minor version number is bumped when a compatible change is made. + + +@dataclass +class ExportedProgram: + graph_module: Annotated[GraphModule, 10] + # Key is the opset namespace (ex. aten), and value is the version number + opset_version: Annotated[dict[str, int], 20] + range_constraints: Annotated[dict[str, RangeConstraint], 30] + schema_version: Annotated[SchemaVersion, 60] + verifiers: Annotated[list[str], 70] = field(default_factory=list) + torch_version: Annotated[str, 80] = "<=2.4" + guards_code: Annotated[list[str], 90] = field(default_factory=list) + + +######################################################################### +# Container types for inference tasks, not being used directly for export. +######################################################################### + + +# The metadata for payload saved in PT2 archive. +# payload includes params, buffers, tensor constants, and custom objects. +@dataclass +class PayloadMeta: + # the path of the payload in the archive file, e.g. "weight_0" + path_name: Annotated[str, 10] + is_param: Annotated[bool, 20] + # whether the payload is serialized using pickle. + # Only custom objects and tensor subclasses that are not fake tensors + # are serialized using pickle. + use_pickle: Annotated[bool, 30] + # Custom Objects don't have tensor_meta and will be serialized using pickle + tensor_meta: Annotated[TensorMeta | None, 40] + + +# The mapping from payload FQN to its metadata. +@dataclass +class PayloadConfig: + config: Annotated[dict[str, PayloadMeta], 10] + + +# +# The structure is used to serialize instances of AOTInductorModel to pass +# them from the publishing pipeline to the predictor. +# +# All new fields should be marked as optional. +# +@dataclass +class AOTInductorModelPickleData: + # Base name of an associated .so AOTInductor library. Typically looks like: + # "abc.so". + library_basename: Annotated[str, 1] + + # AOTInductor engine input names. + input_names: Annotated[list[str], 2] + + # AOTInductor engine output names. + output_names: Annotated[list[str], 3] + + # These fields tell whether floating point inputs/outputs should be converted to + # a certain type. If None, the dtypes that the AOTInductor engine inferred from the sample + # inputs are used. + floating_point_input_dtype: Annotated[int | None, 4] = None + floating_point_output_dtype: Annotated[int | None, 5] = None + + # Whether AOTInductor runtime is for CPU. + aot_inductor_model_is_cpu: Annotated[bool | None, 6] = None + + +@dataclass +class ExternKernelNode: + # name is not the unique identifier of the node + name: Annotated[str, 10] + node: Annotated[Node, 20] + + +@dataclass +class ExternKernelNodes: + nodes: Annotated[list[ExternKernelNode], 10] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/schema.yaml b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/schema.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3b9c4d3051a8779f77627b3e187c57e31fbf58be --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/schema.yaml @@ -0,0 +1,569 @@ +# @generated by update_schema.py +# checksum<> +AOTInductorModelPickleData: + kind: struct + fields: + library_basename: + type: str + input_names: + type: List[str] + output_names: + type: List[str] + floating_point_input_dtype: + type: Optional[int] + default: None + floating_point_output_dtype: + type: Optional[int] + default: None + aot_inductor_model_is_cpu: + type: Optional[bool] + default: None +Argument: + kind: union + fields: + as_none: + type: bool + as_tensor: + type: TensorArgument + as_tensors: + type: List[TensorArgument] + as_int: + type: int + as_ints: + type: List[int] + as_float: + type: float + as_floats: + type: List[float] + as_string: + type: str + as_strings: + type: List[str] + as_sym_int: + type: SymIntArgument + as_sym_ints: + type: List[SymIntArgument] + as_scalar_type: + type: ScalarType + as_memory_format: + type: MemoryFormat + as_layout: + type: Layout + as_device: + type: Device + as_bool: + type: bool + as_bools: + type: List[bool] + as_sym_bool: + type: SymBoolArgument + as_sym_bools: + type: List[SymBoolArgument] + as_graph: + type: GraphArgument + as_optional_tensors: + type: List[OptionalTensorArgument] + as_custom_obj: + type: CustomObjArgument + as_operator: + type: str + as_sym_float: + type: SymFloatArgument + as_sym_floats: + type: List[SymFloatArgument] + as_optional_tensor: + type: OptionalTensorArgument + as_complex: + type: ComplexValue + as_nested_tensors: + type: List[List[TensorArgument]] + as_int_lists: + type: List[List[int]] + as_string_to_argument: + type: Dict[str, Argument] + as_float_lists: + type: List[List[float]] +ArgumentKind: + kind: enum + fields: + UNKNOWN: 0 + POSITIONAL: 1 + KEYWORD: 2 +BufferMutationSpec: + kind: struct + fields: + arg: + type: TensorArgument + buffer_name: + type: str +ComplexValue: + kind: struct + fields: + real: + type: float + imag: + type: float +ConstantValue: + kind: union + fields: + as_none: + type: bool + as_int: + type: int + as_float: + type: float + as_string: + type: str + as_bool: + type: bool +CustomObjArgument: + kind: struct + fields: + name: + type: str + class_fqn: + type: str +Device: + kind: struct + fields: + type: + type: str + index: + type: Optional[int] + default: None +ExportedProgram: + kind: struct + fields: + graph_module: + type: GraphModule + opset_version: + type: Dict[str, int] + range_constraints: + type: Dict[str, RangeConstraint] + schema_version: + type: SchemaVersion + verifiers: + type: List[str] + default: '[]' + torch_version: + type: str + default: <=2.4 + guards_code: + type: List[str] + default: '[]' +ExternKernelNode: + kind: struct + fields: + name: + type: str + node: + type: Node +ExternKernelNodes: + kind: struct + fields: + nodes: + type: List[ExternKernelNode] +GradientToParameterSpec: + kind: struct + fields: + arg: + type: TensorArgument + parameter_name: + type: str +GradientToUserInputSpec: + kind: struct + fields: + arg: + type: TensorArgument + user_input_name: + type: str +Graph: + kind: struct + fields: + inputs: + type: List[Argument] + outputs: + type: List[Argument] + nodes: + type: List[Node] + tensor_values: + type: Dict[str, TensorMeta] + sym_int_values: + type: Dict[str, SymInt] + sym_bool_values: + type: Dict[str, SymBool] + is_single_tensor_return: + type: bool + default: 'False' + custom_obj_values: + type: Dict[str, CustomObjArgument] + default: '{}' + sym_float_values: + type: Dict[str, SymFloat] + default: '{}' +GraphArgument: + kind: struct + fields: + name: + type: str + graph: + type: Graph +GraphModule: + kind: struct + fields: + graph: + type: Graph + signature: + type: GraphSignature + module_call_graph: + type: List[ModuleCallEntry] + metadata: + type: Dict[str, str] + default: '{}' + treespec_namedtuple_fields: + type: Dict[str, NamedTupleDef] + default: '{}' +GraphSignature: + kind: struct + fields: + input_specs: + type: List[InputSpec] + output_specs: + type: List[OutputSpec] +InputSpec: + kind: union + fields: + user_input: + type: UserInputSpec + parameter: + type: InputToParameterSpec + buffer: + type: InputToBufferSpec + tensor_constant: + type: InputToTensorConstantSpec + custom_obj: + type: InputToCustomObjSpec + token: + type: InputTokenSpec + constant_input: + type: InputToConstantInputSpec +InputToBufferSpec: + kind: struct + fields: + arg: + type: TensorArgument + buffer_name: + type: str + persistent: + type: bool +InputToConstantInputSpec: + kind: struct + fields: + name: + type: str + value: + type: ConstantValue +InputToCustomObjSpec: + kind: struct + fields: + arg: + type: CustomObjArgument + custom_obj_name: + type: str +InputToParameterSpec: + kind: struct + fields: + arg: + type: TensorArgument + parameter_name: + type: str +InputToTensorConstantSpec: + kind: struct + fields: + arg: + type: TensorArgument + tensor_constant_name: + type: str +InputTokenSpec: + kind: struct + fields: + arg: + type: TokenArgument +Layout: + kind: enum + fields: + Unknown: 0 + SparseCoo: 1 + SparseCsr: 2 + SparseCsc: 3 + SparseBsr: 4 + SparseBsc: 5 + _mkldnn: 6 + Strided: 7 +LossOutputSpec: + kind: struct + fields: + arg: + type: TensorArgument +MemoryFormat: + kind: enum + fields: + Unknown: 0 + ContiguousFormat: 1 + ChannelsLast: 2 + ChannelsLast3d: 3 + PreserveFormat: 4 +ModuleCallEntry: + kind: struct + fields: + fqn: + type: str + signature: + type: Optional[ModuleCallSignature] + default: None +ModuleCallSignature: + kind: struct + fields: + inputs: + type: List[Argument] + outputs: + type: List[Argument] + in_spec: + type: str + out_spec: + type: str + forward_arg_names: + type: Optional[List[str]] + default: None +NamedArgument: + kind: struct + fields: + name: + type: str + arg: + type: Argument + kind: + type: Optional[ArgumentKind] + default: None +NamedTupleDef: + kind: struct + fields: + field_names: + type: List[str] +Node: + kind: struct + fields: + target: + type: str + inputs: + type: List[NamedArgument] + outputs: + type: List[Argument] + metadata: + type: Dict[str, str] + is_hop_single_tensor_return: + type: Optional[bool] + default: None + name: + type: Optional[str] + default: None +OptionalTensorArgument: + kind: union + fields: + as_tensor: + type: TensorArgument + as_none: + type: bool +OutputSpec: + kind: union + fields: + user_output: + type: UserOutputSpec + loss_output: + type: LossOutputSpec + buffer_mutation: + type: BufferMutationSpec + gradient_to_parameter: + type: GradientToParameterSpec + gradient_to_user_input: + type: GradientToUserInputSpec + user_input_mutation: + type: UserInputMutationSpec + token: + type: OutputTokenSpec + parameter_mutation: + type: ParameterMutationSpec +OutputTokenSpec: + kind: struct + fields: + arg: + type: TokenArgument +ParameterMutationSpec: + kind: struct + fields: + arg: + type: TensorArgument + parameter_name: + type: str +PayloadConfig: + kind: struct + fields: + config: + type: Dict[str, PayloadMeta] +PayloadMeta: + kind: struct + fields: + path_name: + type: str + is_param: + type: bool + use_pickle: + type: bool + tensor_meta: + type: Optional[TensorMeta] +RangeConstraint: + kind: struct + fields: + min_val: + type: Optional[int] + max_val: + type: Optional[int] +ScalarType: + kind: enum + fields: + UNKNOWN: 0 + BYTE: 1 + CHAR: 2 + SHORT: 3 + INT: 4 + LONG: 5 + HALF: 6 + FLOAT: 7 + DOUBLE: 8 + COMPLEXHALF: 9 + COMPLEXFLOAT: 10 + COMPLEXDOUBLE: 11 + BOOL: 12 + BFLOAT16: 13 + UINT16: 28 + FLOAT8E4M3FN: 29 + FLOAT8E5M2: 30 + FLOAT8E4M3FNUZ: 31 + FLOAT8E5M2FNUZ: 32 + FLOAT8E8M0FNU: 33 + UINT32: 34 + UINT64: 35 +SchemaVersion: + kind: struct + fields: + major: + type: int + minor: + type: int +SymBool: + kind: union + fields: + as_expr: + type: SymExpr + as_bool: + type: bool +SymBoolArgument: + kind: union + fields: + as_name: + type: str + as_bool: + type: bool +SymExpr: + kind: struct + fields: + expr_str: + type: str + hint: + type: Optional[SymExprHint] + default: None +SymExprHint: + kind: union + fields: + as_int: + type: int + as_bool: + type: bool + as_float: + type: float +SymFloat: + kind: union + fields: + as_expr: + type: SymExpr + as_float: + type: float +SymFloatArgument: + kind: union + fields: + as_name: + type: str + as_float: + type: float +SymInt: + kind: union + fields: + as_expr: + type: SymExpr + as_int: + type: int +SymIntArgument: + kind: union + fields: + as_name: + type: str + as_int: + type: int +TensorArgument: + kind: struct + fields: + name: + type: str +TensorMeta: + kind: struct + fields: + dtype: + type: ScalarType + sizes: + type: List[SymInt] + requires_grad: + type: bool + device: + type: Device + strides: + type: List[SymInt] + storage_offset: + type: SymInt + layout: + type: Layout +TokenArgument: + kind: struct + fields: + name: + type: str +UserInputMutationSpec: + kind: struct + fields: + arg: + type: TensorArgument + user_input_name: + type: str +UserInputSpec: + kind: struct + fields: + arg: + type: Argument +UserOutputSpec: + kind: struct + fields: + arg: + type: Argument +SCHEMA_VERSION: +- 8 +- 20 +TREESPEC_VERSION: 1 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/schema_check.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/schema_check.py new file mode 100644 index 0000000000000000000000000000000000000000..548ceb5435df8328b59d28adcb05c3873ad7e7d6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/schema_check.py @@ -0,0 +1,905 @@ +# mypy: allow-untyped-defs +import dataclasses +import hashlib +import inspect +import re +import types +import typing +from enum import IntEnum +from typing import Annotated, Any, ForwardRef, Union + +from torch._export.serde import schema +from torch._export.serde.union import _Union + + +class SchemaUpdateError(Exception): + pass + + +def _check(x, msg): + if not x: + raise SchemaUpdateError(msg) + + +_CPP_TYPE_MAP = { + str: "std::string", + int: "int64_t", + float: "F64", + bool: "bool", +} + +_THRIFT_TYPE_MAP = { + str: "string", + int: "i64", + float: "double", + bool: "bool", +} + + +def _staged_schema(): + yaml_ret: dict[str, Any] = {} + defs = {} + cpp_enum_defs: dict[str, str] = {} + cpp_class_defs: dict[str, str] = {} + cpp_type_decls: list[str] = [] + cpp_json_defs: list[str] = [] + thrift_enum_defs: list[str] = [] + thrift_type_defs: dict[str, str] = {} + + def _handle_aggregate(ty) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]: + def dump_type(t, level: int) -> tuple[str, str, str]: + if getattr(t, "__name__", None) in cpp_enum_defs: + return t.__name__, "int64_t", t.__name__ + elif t in _CPP_TYPE_MAP: + return (t.__name__, _CPP_TYPE_MAP[t], _THRIFT_TYPE_MAP[t]) + elif isinstance(t, str): + if t not in defs: + raise AssertionError(f"type {t} not in defs") + if t in cpp_enum_defs: + raise AssertionError(f"type {t} unexpectedly in cpp_enum_defs") + if "[" in t: + raise AssertionError(f"type {t} contains '[' which is not allowed") + return t, f"ForwardRef<{t}>", t + elif isinstance(t, ForwardRef): + return ( + t.__forward_arg__, + f"ForwardRef<{t.__forward_arg__}>", + t.__forward_arg__, + ) + elif o := typing.get_origin(t): + # Lemme know if there's a better way to do this. + if o is list: + yaml_head, cpp_head, thrift_head, thrift_tail = ( + "List", + "std::vector", + "list<", + ">", + ) + elif o is dict: + yaml_head, cpp_head, thrift_head, thrift_tail = ( + "Dict", + "std::unordered_map", + "map<", + ">", + ) + elif o is Union or o is types.UnionType: + if level != 0: + raise AssertionError( + f"Optional is only supported at the top level, got level={level}" + ) + args = typing.get_args(t) + if len(args) != 2 or args[1] is not type(None): + raise AssertionError( + f"expected Optional type with 2 args ending in None, got {args}" + ) + yaml_type, cpp_type, thrift_type = dump_type(args[0], level + 1) + return ( + f"Optional[{yaml_type}]", + f"std::optional<{cpp_type}>", + f"optional {thrift_type}", + ) + elif o is Annotated: + return dump_type(t.__origin__, level) + else: + raise AssertionError(f"Type {t} is not supported in export schema.") + yaml_arg_types, cpp_arg_types, thrift_arg_types = zip( + *[dump_type(x, level + 1) for x in typing.get_args(t)] + ) + return ( + (f"{yaml_head}[{', '.join(yaml_arg_types)}]"), + (f"{cpp_head}<{', '.join(cpp_arg_types)}>"), + f"{thrift_head}{', '.join(thrift_arg_types)}{thrift_tail}", + ) + elif isinstance(t, type): + return (t.__name__, t.__name__, t.__name__) + else: + raise AssertionError(f"Type {t} is not supported in export schema.") + + def dump_cpp_value(v) -> str: + if v is None: + return "std::nullopt" + elif v is True: + return "true" + elif v is False: + return "false" + elif v == {}: + return "{}" + elif v == []: + return "{}" + elif v == (): + return "{}" + elif isinstance(v, str): + return f'"{v}"' + else: + raise AssertionError( + f"Default value {v} is not supported yet in export schema." + ) + + def dump_field(f) -> tuple[dict[str, Any], str, str | None, str, int]: + t, cpp_type, thrift_type = dump_type(f.type, 0) + ret = {"type": t} + cpp_default: str | None = None + if typing.get_origin(f.type) is not Annotated: + raise AssertionError( + f"Field {f.name} must be annotated with an integer id." + ) + thrift_id = f.type.__metadata__[0] + if type(thrift_id) is not int: + raise AssertionError( + f"Field {f.name} must be annotated with an integer id, got {type(thrift_id)}" + ) + + value = dataclasses.MISSING + if f.default is not dataclasses.MISSING: + value = f.default + elif f.default_factory is not dataclasses.MISSING: + value = f.default_factory() + + if value is not dataclasses.MISSING: + default = str(value) + ret["default"] = default + cpp_default = dump_cpp_value(value) + + if t.startswith("Optional[") and value is not None: + raise AssertionError( + f"Optional field {ty.__name__}.{f.name} must have default value to be None." + ) + + return ret, cpp_type, cpp_default, thrift_type, thrift_id + + yaml_ret = {} + cpp_ret = {} + thrift_ret = {} + thrift_ids = set() + for f in dataclasses.fields(ty): + yaml_res, cpp_type, cpp_default, thrift_type, thrift_id = dump_field(f) + yaml_ret[f.name] = yaml_res + cpp_ret[f.name] = {"cpp_type": cpp_type, "cpp_default": cpp_default} + thrift_ret[f.name] = {"thrift_type": thrift_type, "thrift_id": thrift_id} + if thrift_id in thrift_ids: + raise AssertionError( + f"Duplicate thrift id {thrift_id} for field {f.name} in {ty.__name__}." + ) + thrift_ids.add(thrift_id) + return yaml_ret, cpp_ret, thrift_ret + + def _handle_int_enum(name, ty): + yaml_ret[name] = {"kind": "enum", "fields": {x.name: x.value for x in ty}} + cpp_enum_defs[name] = f""" +enum class {name} {{ +{chr(10).join([f" {x.name} = {x.value}," for x in ty])} +}}; + +inline std::string_view printEnum(const {name}& e) {{ + switch (e) {{ +{chr(10).join([f" case {name}::{x.name}: return {chr(34)}{x.name}{chr(34)};" for x in ty])} + default: + throw std::runtime_error("Unknown enum value"); + }} +}} + +inline void parseEnum(std::string_view s, {name}& t) {{ +{chr(10).join([f" if (s == {chr(34)}{x.name}{chr(34)}) {{ t = {name}::{x.name}; return; }}" for x in ty])} + throw std::runtime_error("Unknown enum value: " + std::string{{s}}); +}} +""" + thrift_enum_defs.append( + f""" +enum {name} {{ +{chr(10).join([f" {x.name} = {x.value}," for x in ty])} +}} +""" + ) + + def _handle_struct(name, ty): + fields, cpp_fields, thrift_fields = _handle_aggregate(ty) + yaml_ret[name] = {"kind": "struct", "fields": fields} + field_decls = "\n".join( + f" {f['cpp_type']} {name}{' = ' + f['cpp_default'] if f['cpp_default'] is not None else ''};" + for name, f in cpp_fields.items() + ) + + def accessor(name, ty): + type_name = fields[name]["type"] + if type_name in cpp_enum_defs: + return f""" + {type_name} get_{name}() const {{ + return static_cast<{type_name}>({name}); + }} + + void set_{name}({type_name} def) {{ + {name} = static_cast(def); + }} +""" + return f""" + const {ty}& get_{name}() const {{ + return {name}; + }} + + void set_{name}({ty} def) {{ + {name} = std::move(def); + }} +""" + + to_json_decl = f"void to_json(nlohmann::json& nlohmann_json_j, const {name}& nlohmann_json_t)" + to_json_def = f"""{{ +{chr(10).join([f' nlohmann_json_j["{name}"] = nlohmann_json_t.{name};' for name, f in cpp_fields.items()])} +}} +""" + from_json_decl = f"void from_json(const nlohmann::json& nlohmann_json_j, {name}& nlohmann_json_t)" + + from_json_def = f"""{{ + {name} nlohmann_json_default_obj; +{ + chr(10).join( + [ + f' nlohmann_json_t.{name} = nlohmann_json_j.value("{name}", nlohmann_json_default_obj.{name});' + for name, f in cpp_fields.items() + ] + ) + } +}} +""" + cpp_class_defs[name] = f""" +class {name} {{ + private: +{field_decls} + + public: +{"".join([accessor(name, f["cpp_type"]) for name, f in cpp_fields.items()])} + friend {to_json_decl}; + friend {from_json_decl}; +}}; +""" + cpp_json_defs.append(f"inline {to_json_decl} {to_json_def}") + cpp_json_defs.append(f"inline {from_json_decl} {from_json_def}") + cpp_type_decls.append(f"class {name};") + + thrift_type_defs[name] = f""" +struct {name} {{ +{chr(10).join(f" {f['thrift_id']}: {f['thrift_type']} {n};" for n, f in thrift_fields.items())} +}}""" + + def _handle_union(name, ty): + fields, cpp_fields, thrift_fields = _handle_aggregate(ty) + yaml_ret[name] = {"kind": "union", "fields": fields} + + def accessor(name, ty, idx): + return f""" + const {ty}& get_{name}() const {{ + return std::get<{idx + 1}>(variant_); + }} + + void set_{name}({ty} def) {{ + variant_.emplace<{idx + 1}>(std::move(def)); + tag_ = Tag::{name.upper()}; + }} +""" + + to_json_branches = "".join( + [ + f""" + if (nlohmann_json_t.tag_ == Tag::{name.upper()}) {{ + nlohmann_json_j["{name}"] = nlohmann_json_t.get_{name}(); + return; + }}""" + for idx, (name, f) in enumerate(cpp_fields.items()) + ] + ) + from_json_branches = "".join( + [ + f""" + if (nlohmann_json_j.contains("{name}")) {{ + nlohmann_json_t.variant_.emplace<{idx + 1}>(nlohmann_json_j.at("{name}").template get<{f["cpp_type"]}>()); + nlohmann_json_t.tag_ = Tag::{name.upper()}; + return; + }}""" + for idx, (name, f) in enumerate(cpp_fields.items()) + ] + ) + + cpp_class_defs[name] = f""" +class {name} {{ + struct Void {{}}; + + public: + enum class Tag {{ + {", ".join([name.upper() for name in cpp_fields])} + }}; + + private: + std::variant variant_; + Tag tag_; + + public: + Tag tag() const {{ + return tag_; + }} +{"".join([accessor(name, f["cpp_type"], idx) for idx, (name, f) in enumerate(cpp_fields.items())])} + friend void to_json(nlohmann::json& nlohmann_json_j, const {name}& nlohmann_json_t) {{ +{to_json_branches} + }} + + friend void from_json(const nlohmann::json& nlohmann_json_j, {name}& nlohmann_json_t) {{ +{from_json_branches} + }} +}}; + +inline std::string_view printEnum(const {name}::Tag& e) {{ + switch (e) {{ +{chr(10).join([f" case {name}::Tag::{x.upper()}: return {chr(34)}{x.upper()}{chr(34)};" for x in cpp_fields])} + default: + throw std::runtime_error("Unknown enum value"); + }} +}} + +inline void parseEnum(std::string_view s, {name}::Tag& t) {{ +{chr(10).join([f" if (s == {chr(34)}{x.upper()}{chr(34)}) {{ t = {name}::Tag::{x.upper()}; return; }}" for x in cpp_fields])} + throw std::runtime_error("Unknown enum value: " + std::string{{s}}); +}} + +""" + cpp_type_decls.append(f"class {name};") + + thrift_type_defs[name] = f""" +union {name} {{ +{chr(10).join(f" {f['thrift_id']}: {f['thrift_type']} {n};" for n, f in thrift_fields.items())} +}}""" + + for name in dir(schema): + if name.startswith("_"): + continue + + value = getattr(schema, name) + + if hasattr(value, "__module__") and value.__module__ != schema.__name__: + continue + + defs[name] = value + + class_ordering = {} + for name, value in defs.items(): + if isinstance(value, type): + if issubclass(value, IntEnum): + _handle_int_enum(name, value) + elif dataclasses.is_dataclass(value): + class_ordering[name] = inspect.findsource(value)[1] + if issubclass(value, _Union): + _handle_union(name, value) + else: + _handle_struct(name, value) + else: + raise AssertionError(f"Unknown schema type {name}: {value}") + elif isinstance(value, (int, tuple)): + if name not in ("SCHEMA_VERSION", "TREESPEC_VERSION"): + raise AssertionError( + f"expected SCHEMA_VERSION or TREESPEC_VERSION, got {name}" + ) + elif isinstance(value, dict): + # Skip mapping dictionaries used for codegen + pass + else: + raise AssertionError(f"Unknown variable {name}: {value}") + + yaml_ret["SCHEMA_VERSION"] = list(defs["SCHEMA_VERSION"]) + if not all(x > 0 for x in yaml_ret["SCHEMA_VERSION"]): + raise AssertionError( + f"all SCHEMA_VERSION values must be > 0, got {yaml_ret['SCHEMA_VERSION']}" + ) + yaml_ret["TREESPEC_VERSION"] = defs["TREESPEC_VERSION"] + if yaml_ret["TREESPEC_VERSION"] <= 0: + raise AssertionError( + f"TREESPEC_VERSION must be > 0, got {yaml_ret['TREESPEC_VERSION']}" + ) + + cpp_header = f""" +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#ifndef NLOHMANN_JSON_NAMESPACE_BEGIN +#define NLOHMANN_JSON_NAMESPACE_BEGIN namespace nlohmann {{ +#endif + +#ifndef NLOHMANN_JSON_NAMESPACE_END +#define NLOHMANN_JSON_NAMESPACE_END }} +#endif + +// https://github.com/nlohmann/json/pull/2117 +NLOHMANN_JSON_NAMESPACE_BEGIN +template +struct adl_serializer> {{ + static void to_json(json& j, const std::optional& opt) {{ + if (opt == std::nullopt) {{ + j = nullptr; + }} else {{ + j = *opt; // this will call adl_serializer::to_json which will + // find the free function to_json in T's namespace! + }} + }} + + static void from_json(const json& j, std::optional& opt) {{ + if (j.is_null()) {{ + opt = std::nullopt; + }} else {{ + opt = j.template get(); // same as above, but with + // adl_serializer::from_json + }} + }} +}}; +NLOHMANN_JSON_NAMESPACE_END + +namespace torch {{ +namespace _export {{ + +template +class ForwardRef {{ + static_assert(!std::is_reference_v, "ForwardRef cannot be a reference type"); + + public: + ForwardRef(): ptr_(std::make_unique()) {{}} + ForwardRef(ForwardRef&&); + ForwardRef(const ForwardRef& other): ptr_(std::make_unique(*other.ptr_)) {{}} + ForwardRef& operator=(ForwardRef&&); + ForwardRef& operator=(const ForwardRef& other) {{ + ptr_ = std::make_unique(*other.ptr_); + return *this; + }} + ~ForwardRef(); + const T& operator*() const {{ + return *ptr_; + }} + + const T* operator->() const {{ + return ptr_.get(); + }} + + void emplace(T&& t) {{ + ptr_ = std::make_unique(std::move(t)); + }} + + private: + std::unique_ptr ptr_; +}}; + +template +void to_json(nlohmann::json& j, const ForwardRef& p) {{ + j = *p; +}} + +template +void from_json(const nlohmann::json& j, ForwardRef& p) {{ + p.emplace(j.template get()); +}} + +class F64 {{ + public: + double get() const {{ + return value_; + }} + + void set(double value) {{ + value_ = value; + }} + + private: + double value_; +}}; + +inline void to_json(nlohmann::json& j, const F64& f) {{ + if (std::isinf(f.get())) {{ + j = "Infinity"; + }} else if (std::isinf(-f.get())) {{ + j = "-Infinity"; + }} else if (std::isnan(f.get())) {{ + j = "NaN"; + }} else {{ + j = f.get(); + }} +}} + +inline void from_json(const nlohmann::json& j, F64& f) {{ + if (j == "Infinity") {{ + f.set(std::numeric_limits::infinity()); + }} else if (j == "-Infinity") {{ + f.set(-std::numeric_limits::infinity()); + }} else if (j == "NaN") {{ + f.set(std::numeric_limits::quiet_NaN()); + }} else {{ + f.set(j.get()); + }} +}} + +{chr(10).join(cpp_type_decls)} +{"".join(cpp_enum_defs.values())} +{"".join(dict(sorted(cpp_class_defs.items(), key=lambda x: class_ordering[x[0]])).values())} +{chr(10).join(cpp_json_defs)} + +template ForwardRef::ForwardRef(ForwardRef&&) = default; +template ForwardRef& ForwardRef::operator=(ForwardRef&&) = default; +template ForwardRef::~ForwardRef() = default; +}} // namespace _export +}} // namespace torch +""" + thrift_schema = f""" +namespace py3 torch._export +namespace cpp2 torch._export.schema +{chr(10).join(thrift_enum_defs)} +{chr(10).join(dict(sorted(thrift_type_defs.items(), key=lambda x: class_ordering[x[0]])).values())} +""" + return yaml_ret, cpp_header, thrift_schema + + +def _diff_schema(dst, src): + additions = {key: src[key] for key in src.keys() - dst.keys()} + subtractions = {key: dst[key] for key in dst.keys() - src.keys()} + + common_keys = src.keys() & dst.keys() + + versions = {"SCHEMA_VERSION", "TREESPEC_VERSION"} + common_keys -= versions + + for key in common_keys: + src_kind = src[key]["kind"] + src_fields = src[key]["fields"] + dst_kind = dst[key]["kind"] + dst_fields = dst[key]["fields"] + _check( + src_kind == dst_kind, + f"Type {key} changed kind from {dst_kind} to {src_kind}", + ) + if not isinstance(src_fields, dict) or not isinstance(dst_fields, dict): + raise AssertionError( + f"expected dict fields, got src={type(src_fields)}, dst={type(dst_fields)}" + ) + added_fields = { + key: src_fields[key] for key in src_fields.keys() - dst_fields.keys() + } + subtracted_fields = { + key: dst_fields[key] for key in dst_fields.keys() - src_fields.keys() + } + common_fields = src_fields.keys() & dst_fields.keys() + + for field in common_fields: + src_field = src_fields[field] + dst_field = dst_fields[field] + if src_kind == "struct": + _check( + src_field["type"] == dst_field["type"], + f"Type of the field {key}.{field} changed from {dst_field['type']} to {src_field['type']}", + ) + if "default" in src_field and "default" not in dst_field: + added_fields[field] = {} + added_fields[field]["default"] = src_field["default"] + if "default" not in src_field and "default" in dst_field: + subtracted_fields[field] = {} + subtracted_fields[field]["default"] = dst_field["default"] + elif src_kind == "enum": + _check( + src_field == dst_field, + f"Value of the enum field {key}.{field} changed from {dst_field} to {src_field}", + ) + elif src_kind == "union": + _check( + src_field["type"] == dst_field["type"], + f"Type of the field {key}.{field} changed from {dst_field['type']} to {src_field['type']}", + ) + else: + raise AssertionError(f"Unknown kind {src_kind}: {key}") + if len(added_fields) > 0: + if key in additions: + raise AssertionError(f"key {key} already in additions") + additions[key] = {} + additions[key]["fields"] = added_fields + if len(subtracted_fields) > 0: + if key in subtractions: + raise AssertionError(f"key {key} already in subtractions") + subtractions[key] = {} + subtractions[key]["fields"] = subtracted_fields + + return additions, subtractions + + +def _hash_content(s: str): + return hashlib.sha256(s.strip().encode("utf-8")).hexdigest() + + +def _generate_enum_converters() -> str: + """Generate C++ converter functions from serialized enum values to c10 enums.""" + + def validate_mapping( + enum_class: type[IntEnum], + mapping: dict[int, str], + enum_name: str, + skip_values: set[int], + ) -> None: + """Validate that all enum values have corresponding c10 mappings.""" + for member in enum_class: + if member.value in skip_values: + continue + if member.value not in mapping: + raise SchemaUpdateError( + f"{enum_name}.{member.name} (value={member.value}) is missing " + f"from {enum_name.upper()}_TO_C10 mapping in schema.py. " + f"Please add the mapping to the c10 enum name." + ) + + # Validate that all enum values have mappings (except UNKNOWN values) + validate_mapping( + schema.ScalarType, + schema.SCALAR_TYPE_TO_C10, + "ScalarType", + {schema.ScalarType.UNKNOWN}, + ) + validate_mapping( + schema.Layout, + schema.LAYOUT_TO_C10, + "Layout", + {schema.Layout.Unknown}, + ) + validate_mapping( + schema.MemoryFormat, + schema.MEMORY_FORMAT_TO_C10, + "MemoryFormat", + {schema.MemoryFormat.Unknown}, + ) + + def generate_converter( + name: str, + c10_type: str, + mapping: dict[int, str], + max_value: int, + ) -> str: + lines: list[str] = [] + for i in range(max_value + 1): + if i in mapping: + lines.append( + f" static_cast(c10::{c10_type}::{mapping[i]}), // {i}" + ) + else: + lines.append(f" kInvalid, // {i}") + + return f""" +inline c10::{c10_type} convertSerialized{name}(int serialized_value) {{ + constexpr int kInvalid = -1; + constexpr int k{name}Map[] = {{ +{chr(10).join(lines)} + }}; + constexpr int kMapSize = sizeof(k{name}Map) / sizeof(k{name}Map[0]); + + TORCH_CHECK( + serialized_value >= 0 && serialized_value < kMapSize, + "Serialized {name} value out of range: ", + serialized_value); + int result = k{name}Map[serialized_value]; + TORCH_CHECK( + result != kInvalid, + "Invalid serialized {name} value: ", + serialized_value); + return static_cast(result); +}} +""" + + scalar_type_converter = generate_converter( + "ScalarType", + "ScalarType", + schema.SCALAR_TYPE_TO_C10, + max(schema.SCALAR_TYPE_TO_C10.keys()), + ) + layout_converter = generate_converter( + "Layout", + "Layout", + schema.LAYOUT_TO_C10, + max(schema.LAYOUT_TO_C10.keys()), + ) + memory_format_converter = generate_converter( + "MemoryFormat", + "MemoryFormat", + schema.MEMORY_FORMAT_TO_C10, + max(schema.MEMORY_FORMAT_TO_C10.keys()), + ) + + return f""" +#pragma once + +#include +#include +#include +#include + +// Converter functions from serialized enum values (torch._export.serde.schema) +// to c10 enums. The serialized format has different enum values than c10. + +namespace torch::aot_inductor {{ +{scalar_type_converter} +{layout_converter} +{memory_format_converter} +}} // namespace torch::aot_inductor +""" + + +@dataclasses.dataclass +class _Commit: + result: dict[str, Any] + checksum_next: str + yaml_path: str + additions: dict[str, Any] + subtractions: dict[str, Any] + base: dict[str, Any] + checksum_head: str | None + cpp_header: str + cpp_header_path: str + enum_converter_header: str + enum_converter_header_path: str + thrift_checksum_head: str | None + thrift_checksum_real: str | None + thrift_checksum_next: str + thrift_schema: str + thrift_schema_path: str + + +def update_schema(): + import importlib.resources + + # pyrefly: ignore [bad-argument-type] + if importlib.resources.is_resource(__package__, "schema.yaml"): + # pyrefly: ignore [bad-argument-type] + content = importlib.resources.read_text(__package__, "schema.yaml") + match = re.search("checksum<<([A-Fa-f0-9]{64})>>", content) + _check(match is not None, "checksum not found in schema.yaml") + if match is None: + raise AssertionError("checksum not found in schema.yaml") + checksum_head = match.group(1) + + thrift_content = importlib.resources.read_text( + # pyrefly: ignore [bad-argument-type] + __package__, + "export_schema.thrift", + ) + match = re.search("checksum<<([A-Fa-f0-9]{64})>>", thrift_content) + _check(match is not None, "checksum not found in export_schema.thrift") + if match is None: + raise AssertionError("checksum not found in export_schema.thrift") + thrift_checksum_head = match.group(1) + thrift_content = thrift_content.splitlines() + if not thrift_content[0].startswith("// @" + "generated"): + raise AssertionError( + f"expected first line to start with '// @generated', got {thrift_content[0]!r}" + ) + if not thrift_content[1].startswith("// checksum<<"): + raise AssertionError( + f"expected second line to start with '// checksum<<', got {thrift_content[1]!r}" + ) + thrift_checksum_real = _hash_content("\n".join(thrift_content[2:])) + + from yaml import load, Loader + + dst = load(content, Loader=Loader) + if not isinstance(dst, dict): + raise AssertionError(f"expected dict from yaml, got {type(dst)}") + else: + checksum_head = None + thrift_checksum_head = None + thrift_checksum_real = None + dst = {"SCHEMA_VERSION": None, "TREESPEC_VERSION": None} + + src, cpp_header, thrift_schema = _staged_schema() + enum_converter_header = _generate_enum_converters() + additions, subtractions = _diff_schema(dst, src) + # pyrefly: ignore [missing-attribute] + yaml_path = __package__.replace(".", "/") + "/schema.yaml" + # pyrefly: ignore [missing-attribute] + thrift_schema_path = __package__.replace(".", "/") + "/export_schema.thrift" + torch_prefix = "torch/" + if not yaml_path.startswith(torch_prefix): + raise AssertionError( + f"yaml_path must start with {torch_prefix}, got {yaml_path}" + ) + if not thrift_schema_path.startswith(torch_prefix): + raise AssertionError( + f"thrift_schema_path must start with {torch_prefix}, got {thrift_schema_path}" + ) + + return _Commit( + result=src, + checksum_next=_hash_content(repr(src)), + yaml_path=yaml_path, + additions=additions, + subtractions=subtractions, + base=dst, + checksum_head=checksum_head, + cpp_header=cpp_header, + cpp_header_path=torch_prefix + "csrc/utils/generated_serialization_types.h", + enum_converter_header=enum_converter_header, + enum_converter_header_path=torch_prefix + + "csrc/inductor/aoti_torch/generated_enum_converters.h", + thrift_checksum_head=thrift_checksum_head, + thrift_checksum_real=thrift_checksum_real, + thrift_checksum_next=_hash_content(thrift_schema), + thrift_schema=thrift_schema, + thrift_schema_path=thrift_schema_path, + ) + + +def check(commit: _Commit, force_unsafe: bool = False): + next_version = None + reason = "" + # Step 1: Detect major schema updates. + if len(commit.additions) > 0: + for k, v in commit.additions.items(): + if k not in commit.base: + continue + kind = commit.result[k]["kind"] + fields = v["fields"] + for f, d in fields.items(): + if kind == "struct" and "default" not in d: + reason += ( + f"Field {k}.{f} is added to schema.py without a default value as an incompatible change " + + "which requires major version bump.\n" + ) + next_version = [commit.base["SCHEMA_VERSION"][0] + 1, 1] + + if len(commit.subtractions) > 0: + for k, v in commit.subtractions.items(): + if k not in commit.result: + continue + for f in v["fields"]: + reason = f"Field {k}.{f} is removed from schema.py as an incompatible change which requires major version bump.\n" + next_version = [commit.base["SCHEMA_VERSION"][0] + 1, 1] + + if force_unsafe: + reason += "--force-unsafe is used." + next_version = commit.result["SCHEMA_VERSION"] + else: + # Step 2: Detect minor schema updates. + if next_version is None and len(commit.additions) > 0: + for k, v in commit.additions.items(): + for f in v["fields"]: + reason += ( + f"Field {k}.{f} is added to schema.py as an compatible change " + + "which still requires minor version bump.\n" + ) + next_version = [ + commit.base["SCHEMA_VERSION"][0], + commit.base["SCHEMA_VERSION"][1] + 1, + ] + if next_version is None and len(commit.subtractions) > 0: + for k, v in commit.subtractions.items(): + for f in v["fields"]: + reason += ( + f"Field {k}.{f} is removed from schema.py as an compatible change " + + "which still requires minor version bump.\n" + ) + next_version = [ + commit.base["SCHEMA_VERSION"][0], + commit.base["SCHEMA_VERSION"][1] + 1, + ] + + return next_version, reason diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/serialize.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/serialize.py new file mode 100644 index 0000000000000000000000000000000000000000..12d373c11c7a79663f7e3e3d744886aa721864ec --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/serialize.py @@ -0,0 +1,4370 @@ +# mypy: allow-untyped-defs +import base64 +import copy +import copyreg +import dataclasses +import heapq +import inspect +import io +import json +import keyword +import logging +import math +import operator +import re +import traceback +import types +import typing +from collections import namedtuple, OrderedDict +from collections.abc import Callable, Iterable, Iterator, Sequence +from contextlib import contextmanager +from dataclasses import dataclass, field +from enum import Enum +from typing import Annotated, Any, cast, final, Optional + +import sympy + +import torch +import torch.export.exported_program as ep +from torch._export.non_strict_utils import _enable_graph_inputs_of_type_nn_module +from torch._export.verifier import load_verifier +from torch._subclasses.fake_tensor import FakeTensor, FakeTensorMode +from torch.fx._symbolic_trace import _ConstantAttributeType +from torch.fx.experimental import symbolic_shapes +from torch.fx.traceback import NodeSource +from torch.utils import _pytree as pytree +from torch.utils._pytree import treespec_dumps, treespec_loads +from torch.utils._sympy.numbers import int_oo +from torch.utils._sympy.symbol import prefix_str, SymT +from torch.utils._sympy.value_ranges import ValueRanges +from torch.utils._traceback import CapturedTraceback +from torch.utils._triton import has_triton + +from ..utils import remove_proxy_from_state_dict +from . import schema +from .schema import ( # type: ignore[attr-defined] + Argument, + ArgumentKind, + BufferMutationSpec, + ComplexValue, + ConstantValue, + CustomObjArgument, + Device, + ExportedProgram, + GradientToParameterSpec, + GradientToUserInputSpec, + Graph, + GraphArgument, + GraphModule, + GraphSignature, + InputSpec, + InputToBufferSpec, + InputToConstantInputSpec, + InputToCustomObjSpec, + InputTokenSpec, + InputToParameterSpec, + InputToTensorConstantSpec, + Layout, + LossOutputSpec, + MemoryFormat, + ModuleCallEntry, + ModuleCallSignature, + NamedArgument, + NamedTupleDef, + Node, + OptionalTensorArgument, + OutputSpec, + OutputTokenSpec, + ParameterMutationSpec, + RangeConstraint, + ScalarType, + SCHEMA_VERSION, + SchemaVersion, + SymBool, + SymBoolArgument, + SymExpr, + SymExprHint, + SymFloat, + SymFloatArgument, + SymInt, + SymIntArgument, + TensorArgument, + TensorMeta, + TokenArgument, + TREESPEC_VERSION, + UserInputMutationSpec, + UserInputSpec, + UserOutputSpec, +) +from .union import _Union + + +__all__ = [ + "serialize", + "GraphModuleSerializer", + "ExportedProgramSerializer", + "GraphModuleDeserializer", + "ExportedProgramDeserializer", +] + +log = logging.getLogger(__name__) + + +class SerializeError(RuntimeError): + pass + + +def _reverse_map(d: dict[Any, Enum]): + return {v.value: k for k, v in d.items()} + + +MetaType = ( + FakeTensor + | int + | torch.SymInt + | float + | torch.SymFloat + | bool + | torch.SymBool + | ep.CustomObjArgument +) + +DEFAULT_PICKLE_PROTOCOL = 2 + +ST_DELIMITER = ";" + +_TORCH_TO_SERIALIZE_DTYPE = { + torch.uint8: ScalarType.BYTE, + torch.int8: ScalarType.CHAR, + torch.uint16: ScalarType.UINT16, + torch.int16: ScalarType.SHORT, + torch.int32: ScalarType.INT, + torch.int64: ScalarType.LONG, + torch.float16: ScalarType.HALF, + torch.float32: ScalarType.FLOAT, + torch.float64: ScalarType.DOUBLE, + torch.complex32: ScalarType.COMPLEXHALF, + torch.complex64: ScalarType.COMPLEXFLOAT, + torch.complex128: ScalarType.COMPLEXDOUBLE, + torch.bool: ScalarType.BOOL, + torch.bfloat16: ScalarType.BFLOAT16, + torch.float8_e4m3fn: ScalarType.FLOAT8E4M3FN, + torch.float8_e5m2: ScalarType.FLOAT8E5M2, + torch.float8_e4m3fnuz: ScalarType.FLOAT8E4M3FNUZ, + torch.float8_e5m2fnuz: ScalarType.FLOAT8E5M2FNUZ, + torch.float8_e8m0fnu: ScalarType.FLOAT8E8M0FNU, + torch.uint32: ScalarType.UINT32, + torch.uint64: ScalarType.UINT64, +} + + +_SERIALIZE_TO_TORCH_DTYPE = _reverse_map(_TORCH_TO_SERIALIZE_DTYPE) # type: ignore[arg-type] + + +_TORCH_TO_SERIALIZE_LAYOUT = { + torch.sparse_coo: Layout.SparseCoo, + torch.sparse_csr: Layout.SparseCsr, + torch.sparse_csc: Layout.SparseCsc, + torch.sparse_bsr: Layout.SparseBsr, + torch.sparse_bsc: Layout.SparseBsc, + torch._mkldnn: Layout._mkldnn, # type: ignore[attr-defined] + torch.strided: Layout.Strided, +} + + +_SERIALIZE_TO_TORCH_LAYOUT = _reverse_map(_TORCH_TO_SERIALIZE_LAYOUT) # type: ignore[arg-type] + + +_TORCH_TO_SERIALIZE_MEMORY_FORMAT = { + torch.contiguous_format: MemoryFormat.ContiguousFormat, + torch.channels_last: MemoryFormat.ChannelsLast, + torch.channels_last_3d: MemoryFormat.ChannelsLast3d, + torch.preserve_format: MemoryFormat.PreserveFormat, +} + + +_SERIALIZE_TO_TORCH_MEMORY_FORMAT = _reverse_map(_TORCH_TO_SERIALIZE_MEMORY_FORMAT) # type: ignore[arg-type] + +_SYM_OPS = { + operator.eq, + operator.ne, + operator.le, + operator.ge, + operator.lt, + operator.gt, + operator.neg, + operator.pos, + math.trunc, + torch.sym_not, + operator.mul, + operator.add, + operator.sub, + operator.floordiv, + operator.mod, + operator.pow, + torch.sym_int, + torch.sym_float, + torch.sym_ite, + torch.sym_max, + torch.sym_min, + torch.sym_sqrt, + operator.truediv, + operator.and_, + operator.or_, + operator.lshift, + operator.rshift, +} + + +if any(isinstance(op, torch._ops.OpOverload) for op in _SYM_OPS): + raise AssertionError("_SYM_OPS should not contain OpOverload instances") + + +@dataclass +class SerializedArtifact: + exported_program: bytes + state_dict: bytes + constants: bytes + example_inputs: bytes + + +@dataclass +class _SerializedProgram: + exported_program: ExportedProgram + state_dict: bytes + constants: bytes + example_inputs: bytes + + +class LazyMap(dict): + """ + Dictionary class for deferred instantiation of node metadata values. + Purpose is to avoid creation of symbolic-shape tensors before relevant shape guards are parsed. + """ + + def __init__(self): + self.map = {} + self.evaluated = set() + + def __setitem__(self, k, v): + self.map[k] = v + + def __getitem__(self, k): + out = self.map[k] + if k in self.evaluated: + return out + self.evaluated.add(k) + self.map[k] = out() + return self.map[k] + + def __repr__(self): + return self.map.__repr__() + + +def deserialize_device(d: Device) -> torch.device: + if d.index is None: + return torch.device(type=d.type) # type: ignore[call-overload] + return torch.device(type=d.type, index=d.index) + + +def deserialize_size(sizes: Sequence[SymInt]) -> tuple[int, ...]: + for sym_int_size in sizes: + if sym_int_size.type != "as_int": + raise AssertionError(f"Only as_int is supported, got {sym_int_size.type}") + return tuple(sym_int_size.as_int for sym_int_size in sizes) + + +def deserialize_stride(strides: Sequence[SymInt]) -> tuple[int, ...]: + for sym_int_stride in strides: + if sym_int_stride.type != "as_int": + raise AssertionError(f"Only as_int is supported, got {sym_int_stride.type}") + return tuple(sym_int_stride.as_int for sym_int_stride in strides) + + +def deserialize_scalar_type(st: ScalarType) -> torch.dtype: + return _SERIALIZE_TO_TORCH_DTYPE[st] + + +def deserialize_storage_offset(offset: SymInt) -> int: + if offset.type != "as_int": + raise AssertionError(f"Only as_int is supported, got {offset.type}") + return offset.as_int + + +def _print_sympy(s: torch.SymInt | torch.SymBool | torch.SymFloat | sympy.Expr): + if isinstance(s, (torch.SymInt, torch.SymBool, torch.SymFloat)): + s = s.node.expr + return sympy.printing.repr.srepr(s) + + +def serialize_sym_int(s: int | torch.SymInt) -> SymInt: + if isinstance(s, (torch.SymInt, sympy.Symbol, int)): + if symbolic_shapes.is_concrete_int(s): + return SymInt.create(as_int=int(s)) + else: + if not isinstance(s, (torch.SymInt, sympy.Symbol)): + raise AssertionError( + f"expected SymInt or Symbol, got {type(s).__name__}" + ) + if s.node.hint is None: + return SymInt.create(as_expr=SymExpr(_print_sympy(s))) + else: + return SymInt.create( + as_expr=SymExpr( + _print_sympy(s), + hint=SymExprHint.create(as_int=s.node.hint), + ) + ) + else: + raise SerializeError( + f"SymInt should be either symbol or int, got `{s}` of type `{type(s)}`" + ) + + +def serialize_sym_float(s: float | torch.SymFloat) -> SymFloat: + if isinstance(s, (torch.SymFloat, sympy.Symbol, float)): + if symbolic_shapes.is_concrete_float(s): + return SymFloat.create(as_float=float(s)) + else: + if not isinstance(s, (torch.SymFloat, sympy.Symbol)): + raise AssertionError( + f"expected SymFloat or Symbol, got {type(s).__name__}" + ) + if s.node.hint is None: + return SymFloat.create(as_expr=SymExpr(_print_sympy(s))) + else: + return SymFloat.create( + as_expr=SymExpr( + _print_sympy(s), + hint=SymExprHint.create(as_float=s.node.hint), + ) + ) + else: + raise SerializeError( + f"SymFloat should be either symbol or float, got `{s}` of type `{type(s)}`" + ) + + +def serialize_sym_bool(s: bool | torch.SymBool) -> SymBool: + if isinstance(s, (torch.SymBool, bool)): + if symbolic_shapes.is_concrete_bool(s): + return SymBool.create(as_bool=bool(s)) + else: + return SymBool.create(as_expr=SymExpr(expr_str=_print_sympy(s))) + else: + raise SerializeError( + f"SymBool should be either symbol or bool, got `{s}` of type `{type(s)}`" + ) + + +def serialize_tensor_meta(t: torch.Tensor) -> TensorMeta: + """ + Extract a TensorMeta describing `t`. + """ + return TensorMeta( + dtype=_TORCH_TO_SERIALIZE_DTYPE[t.dtype], + sizes=[serialize_sym_int(s) for s in t.shape], + requires_grad=t.requires_grad, + device=Device(type=t.device.type, index=t.device.index), + strides=[serialize_sym_int(s) for s in t.stride()], + storage_offset=serialize_sym_int(t.storage_offset()), + layout=_TORCH_TO_SERIALIZE_LAYOUT[t.layout], + ) + + +_CURRENT_DESERIALIZER: Optional["GraphModuleDeserializer"] = None + + +def _reduce_fake_tensor(fake_tensor: FakeTensor): + is_parameter = isinstance(fake_tensor, torch.nn.Parameter) + tensor_meta = serialize_tensor_meta(fake_tensor) + tensor_meta_bytes = json.dumps( + _dataclass_to_dict(tensor_meta), cls=EnumEncoder + ).encode("utf-8") + return _reconstruct_fake_tensor, (tensor_meta_bytes, is_parameter) + + +def _reconstruct_fake_tensor( + serialized_tensor_meta: bytes, is_parameter: bool +) -> FakeTensor: + # Deserialize the bytes into a TensorMeta + json_tensor_meta = json.loads(serialized_tensor_meta.decode("utf-8")) + tensor_meta = _dict_to_dataclass(TensorMeta, json_tensor_meta) + # Find the current fake mode + if _CURRENT_DESERIALIZER is None: + raise AssertionError("Need access to current deserializer state") + fake_tensor = _CURRENT_DESERIALIZER.deserialize_tensor_meta(tensor_meta) + if is_parameter: + fake_tensor = torch.nn.Parameter(fake_tensor) # type: ignore[assignment] + # pyrefly: ignore [bad-return] + return fake_tensor + + +def serialize_torch_artifact( + artifact: Any | None, pickle_protocol: int = DEFAULT_PICKLE_PROTOCOL +) -> bytes: + if artifact is None: + return b"" + + if FakeTensor in copyreg.dispatch_table: + raise AssertionError("Refusing to stomp on existing FakeTensor reducer") + try: + copyreg.pickle(FakeTensor, _reduce_fake_tensor) + buffer = io.BytesIO() + # This is a workaround for backend's tensor deserialization problem: + # unpickleTensor() always create a tensor on the device where it was originally saved + # This behavior is bad for multi-gpu training, as we wish to directly load the tensor + # on the designated device. + # For now, we simply move the tensor to cpu before saving. + # TODO: this should be fixed by deserialization instead. + torch.save(artifact, buffer, pickle_protocol=pickle_protocol) + return buffer.getvalue() + finally: + del copyreg.dispatch_table[FakeTensor] + + +def deserialize_torch_artifact( + serialized: dict[str, Any] | tuple[Any, ...] | bytes, +): + if isinstance(serialized, (dict, tuple)): + return serialized + if len(serialized) == 0: + return {} + buffer = io.BytesIO(serialized) + buffer.seek(0) + # weights_only=False as we want to load custom objects here (e.g. ScriptObject) + try: + artifact = torch.load(buffer, weights_only=True) + except Exception as e: + buffer.seek(0) + artifact = torch.load(buffer, weights_only=False) + log.warning( + "Fallback to weights_only=False succeeded. " + "Loaded object of type %s after initial failure: %s", + type(artifact), + exc_info=e, + ) + if not isinstance(artifact, (tuple, dict)): + raise AssertionError(f"expected tuple or dict, got {type(artifact).__name__}") + return artifact + + +def _sympy_int_to_int(val: sympy.Expr, adjust: str) -> int | None: + # Convert simple sympy Integers into concrete int + if val in (sympy.oo, int_oo): + return None + if val in (-sympy.oo, -int_oo): + return None + if isinstance(val, sympy.Integer): + return int(val) + + # TODO: Remove this adjustment when Ed gets rid of fractional ranges + log.warning( + "Export constraints cannot be non-integer expressions. Found " + "type %s, and value %s. We will attempt to %s " + "this value.", + type(val), + val, + adjust, + ) + + if adjust == "floor": + return math.floor(val) + elif adjust == "ceil": + return math.ceil(val) + else: + raise RuntimeError(f"Got invalid adjustment {adjust}") + + +def _int_to_sympy_int(val: int | None, default) -> sympy.Expr: + # Convert concrete int into simple sympy Integers + if val is None: + return default + if val in [-int_oo, int_oo]: + return val + if val == math.inf: + return int_oo + if val == -math.inf: + return -int_oo + return sympy.Integer(val) + + +def _symbol_index(sym: sympy.Symbol, sym_type: SymT): + return int(str(sym)[len(prefix_str[sym_type]) :]) + + +def serialize_range_constraints( + range_constraints: dict[sympy.Symbol, ValueRanges], +) -> dict[str, RangeConstraint]: + return { + str(k): RangeConstraint( + _sympy_int_to_int(v.lower, "ceil"), # type: ignore[arg-type] + _sympy_int_to_int(v.upper, "floor"), # type: ignore[arg-type] + ) + for k, v in range_constraints.items() + } + + +def _get_schema_from_target(target): + if isinstance(target, torch._ops.OpOverload): + return target._schema + elif type(target) in _serialization_registry: + return _serialization_registry[type(target)].op_schema(target) + raise RuntimeError(f"Cannot find schema for {type(target)}") + + +@dataclass +class GraphState: + inputs: list[Argument] = field(default_factory=list) + outputs: list[Argument] = field(default_factory=list) + nodes: list[Node] = field(default_factory=list) + tensor_values: dict[str, TensorMeta] = field(default_factory=dict) + sym_int_values: dict[str, SymInt] = field(default_factory=dict) + sym_bool_values: dict[str, SymBool] = field(default_factory=dict) + sym_float_values: dict[str, SymFloat] = field(default_factory=dict) + is_single_tensor_return: bool = False + custom_obj_values: dict[str, CustomObjArgument] = field(default_factory=dict) + + +class Final(type): + def __new__(metacls, name, bases, classdict): + for b in bases: + if isinstance(b, Final): + raise TypeError(f"type '{b.__name__}' is not an acceptable base type") + return type.__new__(metacls, name, bases, dict(classdict)) + + +def is_metadata_matched(config, entry_metadata): + metadata_attrs = ["num_cpu_threads", "num_warps", "num_stages", "num_ctas"] + for attr in metadata_attrs: + if hasattr(config, attr) and hasattr(entry_metadata, attr): + if getattr(config, attr) != getattr(entry_metadata, attr): + return False + return True + + +def get_triton_kernel_and_cache_entry(node: torch.fx.Node): + if ( + node.target + is not torch._higher_order_ops.triton_kernel_wrap.triton_kernel_wrapper_functional + ): + raise AssertionError( + f"expected triton_kernel_wrapper_functional, got {node.target}" + ) + + if not has_triton(): + raise AssertionError("triton required to serialize triton kernels") + from triton.runtime.autotuner import Autotuner + from triton.runtime.jit import JITFunction + + if not isinstance(node.kwargs["kernel_idx"], int): + raise AssertionError( + f"expected kernel_idx to be int, got {type(node.kwargs['kernel_idx'])}" + ) + kernel = torch._higher_order_ops.triton_kernel_wrap.kernel_side_table.get_kernel( + node.kwargs["kernel_idx"] + ) + + # For Autotuner, we need to look at the underlying JITFunction's cache + # since the Autotuner itself doesn't have a cache + is_autotuner = isinstance(kernel, Autotuner) + # pyrefly: ignore [missing-attribute] + actual_kernel = kernel.fn if is_autotuner else kernel + + if hasattr(actual_kernel, "device_caches"): + caches = actual_kernel.device_caches + if len(caches.keys()) != 1: + raise AssertionError( + f"expected exactly 1 device cache, got {len(caches.keys())}" + ) + cache = next(iter(caches.values()))[0] + elif hasattr(actual_kernel, "cache"): + # old path, still used for cpu triton builds + caches = actual_kernel.cache + if len(caches.keys()) != 1: + raise AssertionError( + f"expected exactly 1 cache key, got {len(caches.keys())}" + ) + cache = next(iter(caches.values())) + else: + raise AssertionError( + # pyrefly: ignore [missing-attribute] + f"kernel caches not found for kernel {actual_kernel.__name__}" + ) + + if len(cache.keys()) == 1: + return actual_kernel, next(iter(cache.values())) + + has_constexprs = ( + isinstance(actual_kernel, JITFunction) + and hasattr(actual_kernel, "constexprs") + and len(actual_kernel.constexprs) > 0 + ) + + if has_constexprs: + constexpr_vals = {} + # pyrefly: ignore [missing-attribute] + for constexpr_idx in actual_kernel.constexprs: + # pyrefly: ignore [missing-attribute] + if constexpr_idx < len(actual_kernel.arg_names): + # pyrefly: ignore [missing-attribute] + param_name = actual_kernel.arg_names[constexpr_idx] + kwargs_dict = node.kwargs.get("kwargs", {}) + if isinstance(kwargs_dict, dict): + if param_name in kwargs_dict: + constexpr_vals[param_name] = kwargs_dict[param_name] + + expected_values = [ + # pyrefly: ignore [missing-attribute] + constexpr_vals[actual_kernel.arg_names[idx]] + # pyrefly: ignore [missing-attribute] + for idx in actual_kernel.constexprs + # pyrefly: ignore [missing-attribute] + if actual_kernel.arg_names[idx] in constexpr_vals + ] + + # Normalize expected values for comparison with parsed constexpr values. + # The kernel signature key stores constexprs as strings (e.g., "True", "1.5", "42"), + # which we parse back to Python types. To ensure proper comparison, we normalize + # the expected values: booleans, ints, and floats are kept as-is since they can + # be compared directly with parsed values. Other types (like dtype or string + # constants) are converted to strings to match the parsed format. + normalized_expected = [] + for val in expected_values: + if isinstance(val, (bool, int, float)): + normalized_expected.append(val) + else: + # pyrefly: ignore [bad-argument-type] + normalized_expected.append(str(val)) + + matching_entries = [] + for sig_key, cache_entry in cache.items(): + constexpr_matches = re.findall(r"\('constexpr',\s*([^)]+)\)", sig_key) + if constexpr_matches: + # Parse constexpr string values back to Python types for comparison. + # Booleans are stored as "True"/"False" strings, numbers as their string + # representation. Values that can't be parsed as numbers are kept as strings + # (e.g., dtype names like "torch.float32"). + constexpr_values = [] + for match in constexpr_matches: + if match in ("True", "False"): + constexpr_values.append(match == "True") + else: + try: + constexpr_values.append(float(match)) + except ValueError: + try: + constexpr_values.append(int(match)) + except ValueError: + constexpr_values.append(match) + + if constexpr_values == normalized_expected: + matching_entries.append((sig_key, cache_entry)) + else: + matching_entries = list(cache.items()) + + if len(matching_entries) == 0: + raise AssertionError( + # pyrefly: ignore [missing-attribute] + f"couldn't find a kernel cache entry with metadata matching the autotuner configs for kernel {actual_kernel.__name__}. " + f"Available cache keys: {list(cache.keys())}" + ) + + if len(matching_entries) == 1: + return actual_kernel, matching_entries[0][1] + + if is_autotuner: + for _sig_key, cache_entry in matching_entries: + entry_metadata = cache_entry.metadata + # pyrefly: ignore [missing-attribute] + for config in kernel.configs: + if is_metadata_matched(config, entry_metadata): + return actual_kernel, cache_entry + + raise AssertionError( + # pyrefly: ignore [missing-attribute] + f"Multiple cache entries found for autotuned kernel {actual_kernel.__name__} " + f"{'with same constexpr values' if has_constexprs else 'with no constexpr'} " + f"and couldn't disambiguate using configs. " + ) + + raise AssertionError( + # pyrefly: ignore [missing-attribute] + f"Multiple cache entries found for non-autotuned kernel {actual_kernel.__name__} " + f"{'with same constexpr values' if has_constexprs else 'with no constexpr'}. " + f"This should not happen. Available cache keys: {[key for key, _ in matching_entries]}" + ) + + +@final +class GraphModuleSerializer(metaclass=Final): + def __init__( + self, + graph_signature: ep.ExportGraphSignature, + module_call_graph: list[ep.ModuleCallEntry], + ): + self.graph_state = GraphState() + self.graph_signature = graph_signature + self.module_call_graph = module_call_graph + self.custom_objs: dict[str, torch._C.ScriptObject] = {} + self.duplicate_getitem_nodes: dict[str, str] = {} + self.treespec_namedtuple_fields: dict[str, NamedTupleDef] = {} + + @contextmanager + def save_graph_state(self): + saved = self.graph_state + self.graph_state = GraphState() + try: + yield + finally: + self.graph_state = saved + + def handle_placeholder(self, node: torch.fx.Node): + if node.op != "placeholder": + raise AssertionError(f"expected placeholder op, got {node.op}") + val = node.meta["val"] + log.debug("[handle_placeholder] %s: %s", node.name, val) + if isinstance(val, torch.Tensor): + graph_input = Argument.create( + as_tensor=self.serialize_tensor_output(node.name, val) + ) + elif isinstance(val, torch.SymInt): + graph_input = Argument.create( + as_sym_int=self.serialize_sym_int_output(node.name, val) + ) + elif isinstance(val, torch.SymFloat): + raise AssertionError("SymFloat graph input is not implemented yet.") + elif isinstance(val, (int, bool, str, float, type(None))): + graph_input = self.serialize_input(val) + elif isinstance(val, ep.CustomObjArgument): + class_fqn = val.class_fqn + graph_input = Argument.create( + as_custom_obj=CustomObjArgument(name=node.name, class_fqn=class_fqn) + ) + self.graph_state.custom_obj_values[node.name] = ( + self.serialize_script_obj_meta(val) + ) + else: + raise AssertionError(f"Unimplemented graph input type: {node.meta['val']}") + self.graph_state.inputs.append(graph_input) + + def handle_output(self, node: torch.fx.Node): + if node.op != "output": + raise AssertionError(f"expected output op, got {node.op}") + if len(node.args) != 1: + raise AssertionError( + f"FX.Node's args should have one arg, got {len(node.args)}" + ) + node_args = node.args[0] + log.debug("[handle_output] %s: %s", node.name, node_args) + if isinstance(node_args, torch.fx.Node): + # For singleton tensor returns + self.graph_state.is_single_tensor_return = True + self.graph_state.outputs = [self.serialize_input(node_args)] + else: + if not isinstance(node_args, (tuple, list)): + raise AssertionError(f"expected tuple or list, got {type(node_args)}") + self.graph_state.outputs = [self.serialize_input(arg) for arg in node_args] + + def serialize_operator(self, target) -> str: + if isinstance(target, str): + return target + elif target.__module__.startswith("torch._ops"): + # TODO(zhxchen17) Maybe provide a function name helper in FX. + # From torch.fx.node._get_qualified_name + module = target.__module__.replace("torch._ops", "torch.ops") + return f"{module}.{target.__name__}" + else: # TODO(zhxchen17) Don't catch all here. + return f"{target.__module__}.{target.__name__}" + + def handle_call_function(self, node: torch.fx.Node): + if node.op != "call_function": + raise AssertionError(f"expected call_function op, got {node.op}") + meta_val = node.meta.get("val") + log.debug( + "[handle_call_function] %s: %s(%s, {%s}) -> %s", + node.name, + node.target, + node.args, + node.kwargs, + meta_val, + ) + + # getitem has been handled in the producer node, skip it here + if node.target is operator.getitem: + return + + if node.target in _SYM_OPS or ( + meta_val is not None + and isinstance(meta_val, (torch.SymInt, torch.SymBool, torch.SymFloat)) + ): + if len(node.kwargs) != 0: + raise AssertionError( + f"expected no kwargs for sym op, got {len(node.kwargs)}" + ) + ex_node = Node( + name=node.name, + target=self.serialize_operator(node.target), + inputs=self.serialize_sym_op_inputs(node.target, node.args), + outputs=[self.serialize_output(node.name, meta_val)], + metadata=self.serialize_metadata(node), + ) + elif isinstance(node.target, torch._ops.OpOverload): + ex_node = Node( + name=node.name, + target=self.serialize_operator(node.target), + inputs=self.serialize_inputs(node.target, node.args, node.kwargs), + outputs=self.serialize_outputs(node), + # TODO: create a new tensor_values here, meta might have faketensor info + metadata=self.serialize_metadata(node), + ) + elif isinstance(node.target, torch._ops.HigherOrderOperator): + + def _is_hop_single_tensor_return(node) -> bool: + if not isinstance(node.target, torch._ops.HigherOrderOperator): + raise AssertionError( + f"expected HigherOrderOperator, got {type(node.target).__name__}" + ) + # HOP schema is not always available, so we look at node.meta["val"] + meta_val = node.meta.get("val", None) + return meta_val is not None and isinstance(meta_val, torch.Tensor) + + # Special handle serialization for aoti_call_delegate + if node.target is torch._higher_order_ops.aoti_call_delegate: + serializable_args = list(node.args) + + # AOTI lowered module is not serializable, serialize the aoti_path instead + lowered_module_name: str = node.args[0].name # type: ignore[assignment, no-untyped-def, union-attr] + if not hasattr(node.graph.owning_module, lowered_module_name): + raise AssertionError( + f"owning_module does not have attribute {lowered_module_name}" + ) + lowered_module = getattr(node.graph.owning_module, lowered_module_name) # type: ignore[no-untyped-def] + serializable_args[0] = lowered_module.aoti_path + + # AOTI compiled graph module in node.args[0] is stateful, and will fail the verifier check + # Skip serializing original_gm as a workaround + serializable_args[1] = None + + serializable_weight_nodes = [] + if serializable_args[2] is not None and isinstance( + serializable_args[2], Iterable + ): + for weight_node in serializable_args[2]: + # skip passing custom obj into the weight arg as an hack + # The schema of weight input is a list of Tensors. + # Downstream runtime is not actively consuming the weighs arg for anything meaningful. + if isinstance(weight_node, torch.fx.Node) and isinstance( + weight_node.meta.get("val", None), ep.CustomObjArgument + ): + continue + serializable_weight_nodes.append(weight_node) + serializable_args[2] = serializable_weight_nodes + + def serialize_tensor_list_output(node): + meta_val = node.meta.get("val", None) + tensor_args = [] + for idx, meta in enumerate(meta_val): + name = self._output_node_name_at_index(node, idx) + tensor_args.append(self.serialize_tensor_output(name, meta)) + return [Argument.create(as_tensors=tensor_args)] + + ex_node = Node( + name=node.name, + target=self.serialize_operator(node.target), + inputs=self.serialize_hoo_inputs(serializable_args, node.kwargs), + outputs=serialize_tensor_list_output(node), + metadata=self.serialize_metadata(node), + is_hop_single_tensor_return=False, + ) + elif ( + node.target + is torch._higher_order_ops.triton_kernel_wrap.triton_kernel_wrapper_functional + ): + kernel, kernel_cache_entry = get_triton_kernel_and_cache_entry(node) + kernel_cache_metadata = kernel_cache_entry.metadata + + meta_val = node.meta["val"] + if not isinstance(meta_val, dict): + raise AssertionError( + f"expected meta_val to be dict, got {type(meta_val).__name__}" + ) + + output_keys = meta_val.keys() + output_indices = [] + + constexpr_keys = {p.name for p in kernel.params if p.is_constexpr} + found_constexpr = False + args_new = () + i = 0 + + if not isinstance(node.kwargs["kwargs"], dict): + raise AssertionError( + f"expected kwargs['kwargs'] to be dict, got {type(node.kwargs['kwargs'])}" + ) + for k, v in node.kwargs["kwargs"].items(): + # don't serialize constexpr since they will + # be embedded into the binary and don't + # need to be passed around as attributes + if k in constexpr_keys: + found_constexpr = True + continue + + if found_constexpr: + raise AssertionError( + "non-constexpr args found after constexpr arg(s)" + ) + + if k in output_keys: + output_indices.append(i) + args_new += (v,) # type: ignore[assignment] + i += 1 + + if not isinstance(node.kwargs["grid"], list): + raise AssertionError( + f"expected grid to be list, got {type(node.kwargs['grid'])}" + ) + + kernel_name_with_hash = ( + f"{kernel.fn.__name__}_{kernel_cache_metadata.hash}" + ) + kwargs_new = { + "name": kernel_name_with_hash, + "grid": node.kwargs["grid"][0], + "output_indices": output_indices, + "num_warps": kernel_cache_metadata.num_warps, + } + if hasattr(kernel_cache_metadata, "num_cpu_threads"): + kwargs_new["num_cpu_threads"] = ( + kernel_cache_metadata.num_cpu_threads + ) + + if hasattr(kernel_cache_metadata, "shared"): + if isinstance(kernel_cache_metadata.shared, bool): + kwargs_new["shared_memory_bytes"] = int( + kernel_cache_metadata.shared + ) + else: + kwargs_new["shared_memory_bytes"] = kernel_cache_metadata.shared + + # MTIA-specific parameters for triton kernel compilation + if hasattr(kernel_cache_metadata, "tile_width"): + kwargs_new["tile_width"] = kernel_cache_metadata.tile_width + if hasattr(kernel_cache_metadata, "tile_height"): + kwargs_new["tile_height"] = kernel_cache_metadata.tile_height + if hasattr(kernel_cache_metadata, "base_pe"): + kwargs_new["base_pe"] = kernel_cache_metadata.base_pe + + # Kernel parameter metadata for MTIA fatbin compilation + kwargs_new["kernel_param_names"] = [ + p.name for p in kernel.params if not p.is_constexpr + ] + # Use inferred signature types from the compiled kernel's ASTSource + # when available. The signature is populated at runtime with actual + # types like "i32", "*fp32" based on the values passed to the kernel + # (see specialize_impl in jit.py). Fall back to static annotations + # for architectures that don't rely on precise type information. + compiled_signature = getattr( + getattr(kernel_cache_entry, "src", None), "signature", None + ) + if compiled_signature is not None: + kwargs_new["kernel_param_types"] = [ + str(compiled_signature.get(p.name, p.annotation)) + for p in kernel.params + if not p.is_constexpr + ] + else: + # Default behavior: use static annotations (may be empty) + kwargs_new["kernel_param_types"] = [ + str(p.annotation) for p in kernel.params if not p.is_constexpr + ] + + ex_node = Node( + name=node.name, + target=self.serialize_operator(node.target), + inputs=self.serialize_hoo_inputs(args_new, kwargs_new), + outputs=self.serialize_hoo_outputs(node), + metadata=self.serialize_metadata(node), + is_hop_single_tensor_return=_is_hop_single_tensor_return(node), + ) + else: + ex_node = Node( + name=node.name, + target=self.serialize_operator(node.target), + inputs=self.serialize_hoo_inputs(node.args, node.kwargs), + outputs=self.serialize_hoo_outputs(node), + metadata=self.serialize_metadata(node), + is_hop_single_tensor_return=_is_hop_single_tensor_return(node), + ) + elif type(node.target) in _serialization_registry: + # Sanity check for unhandled serialization. + if type(node.target) not in _serialization_registry: + raise AssertionError( + f"{type(node.target)} is not supported in export serialization." + ) + + handler = _serialization_registry[type(node.target)] + namespace = handler.namespace() + op_name = handler.to_op_name(node.target) + if not isinstance(namespace, str) or not isinstance(op_name, str): + raise AssertionError( + f"expected namespace and op_name to be str, got {type(namespace).__name__} and {type(op_name).__name__}" + ) + if ":" in namespace or ":" in op_name: + raise AssertionError( + f"namespace and op_name should not contain ':', got {namespace!r} and {op_name!r}" + ) + ex_node = Node( + name=node.name, + target=f"#{namespace}:{op_name}", + inputs=self.serialize_inputs(node.target, node.args, node.kwargs), + outputs=self.serialize_outputs(node), + metadata=self.serialize_metadata(node), + ) + else: + raise SerializeError(f"Serializing {node.target} is not supported") + + self.graph_state.nodes.append(ex_node) + + def handle_get_attr(self, node): + log.debug("[handle_get_attr] %s", node.name) + + def _output_node_at_index(self, node, index) -> torch.fx.Node | None: + user_node = None + for user in node.users: + if user.target is not operator.getitem: + raise AssertionError(f"{user} is not a getitem node") + if index == user.args[1]: + if user_node is None: + user_node = user + else: + # We want to deduplicate getitem nodes that are trying to + # index to the same index + self.duplicate_getitem_nodes[user.name] = user_node.name + return user_node + + def _output_node_name_at_index(self, node, index) -> str: + user_node = self._output_node_at_index(node, index) + if user_node is None: + return f"{node.name}_unused_{index}" + else: + return user_node.name + + def serialize_metadata(self, node: torch.fx.Node) -> dict[str, str]: + ret = {} + + if stack_trace := node.meta.get("stack_trace"): + ret["stack_trace"] = stack_trace + + if nn_module_stack := node.meta.get("nn_module_stack"): + + def export_nn_module_stack(val): + if not isinstance(val, tuple) or len(val) != 2: + val_len = len(val) if isinstance(val, tuple) else "N/A" + raise AssertionError( + f"expected tuple of length 2, got {type(val).__name__} of length {val_len}" + ) + path, ty = val + + if not isinstance(path, str): + raise AssertionError( + f"expected path to be str, got {type(path).__name__}" + ) + if not isinstance(ty, str): + raise AssertionError( + f"expected ty to be str, got {type(ty).__name__}" + ) + + return path + "," + ty + + # Serialize to "key,orig_path,type_str" + nn_module_list = [ + f"{k},{export_nn_module_stack(v)}" for k, v in nn_module_stack.items() + ] + ret["nn_module_stack"] = ST_DELIMITER.join(nn_module_list) + + if source_fn_st := node.meta.get("source_fn_stack"): + source_fn_list = [ + f"{source_fn[0]},{self.serialize_operator(source_fn[1])}" + for source_fn in source_fn_st + ] + ret["source_fn_stack"] = ST_DELIMITER.join(source_fn_list) + + if torch_fn := node.meta.get("torch_fn"): + ret["torch_fn"] = ST_DELIMITER.join(list(torch_fn)) + + if custom := node.meta.get("custom"): + try: + ret["custom"] = json.dumps(custom) + except Exception as e: + raise SerializeError( + f"Failed to serialize custom metadata for node {node.name} with error {e}" + ) from e + + if "from_node" in node.meta: + from_node = node.meta["from_node"] + # Serialize from_node as JSON since it's a complex nested structure + ret["from_node"] = json.dumps(self._serialize_from_node(from_node)) + + return ret + + def _serialize_from_node( + self, from_node: list[NodeSource] | None + ) -> list[dict[str, Any]] | None: + """ + Serialize from_node metadata from a list of NodeSource objects to a list of dictionaries. + """ + if from_node is None: + return None + + return [ + node_source.to_dict() + for node_source in from_node + if isinstance(node_source, NodeSource) + ] + + def serialize_script_obj_meta( + self, script_obj_meta: ep.CustomObjArgument + ) -> CustomObjArgument: + log.debug("[serialize_script_obj_meta] %s", script_obj_meta) + return CustomObjArgument( + name=script_obj_meta.name, + class_fqn=script_obj_meta.class_fqn, + ) + + def serialize_sym_op_inputs(self, op, args) -> list[NamedArgument]: + if isinstance(op, torch._ops.OpOverload): + args_names = [arg.name for arg in op._schema.arguments] + else: + if op not in _SYM_OPS: + raise AssertionError(f"op {op} is not in _SYM_OPS") + args_names = list(inspect.signature(op).parameters.keys()) + serialized_args = [] + for args_name, arg in zip(args_names, args): + serialized_args.append( + NamedArgument( + name=args_name, + arg=self.serialize_input(arg), + kind=ArgumentKind.POSITIONAL, + ) + ) + return serialized_args + + def serialize_inputs( + self, + target: Any, # torch._ops.OpOverload and other custom operator types. + args, + kwargs=None, + ) -> list[NamedArgument]: + schema = None + serialized_args = [] + + if isinstance(target, torch._higher_order_ops.torchbind.CallTorchBind): + obj = args[0] + method = args[1] + schema = target.schema(obj, method) + else: + if not isinstance( + target, (torch._ops.OpOverload, *_registered_extension_types()) + ): + raise AssertionError( + f"expected OpOverload or registered extension type, got {type(target).__name__}" + ) + schema = _get_schema_from_target(target) + if schema is None: + raise AssertionError("schema should not be None") + kwargs = kwargs or {} + + for i, schema_arg in enumerate(schema.arguments): + if schema_arg.name in kwargs: + serialized_args.append( + NamedArgument( + name=schema_arg.name, + arg=self.serialize_input( + kwargs[schema_arg.name], schema_arg.type + ), + kind=ArgumentKind.KEYWORD, + ) + ) + elif not schema_arg.kwarg_only and i < len(args): + serialized_args.append( + NamedArgument( + name=schema_arg.name, + arg=self.serialize_input(args[i], schema_arg.type), + kind=ArgumentKind.POSITIONAL, + ) + ) + else: + # We intentionally don't serialize the missing arguments + # with default values + pass + + return serialized_args + + def serialize_hoo_inputs(self, args, kwargs) -> list[NamedArgument]: + """ + For serializing HOO inputs since HOOs do not have a schema. + """ + inputs = [ + NamedArgument( + name="", arg=self.serialize_input(a), kind=ArgumentKind.POSITIONAL + ) + for a in args + ] + inputs.extend( + [ + NamedArgument( + name=name, + arg=self.serialize_input(a), + kind=ArgumentKind.KEYWORD, + ) + for name, a in kwargs.items() + ] + ) + return inputs + + def is_inductor_sym_int_arg(self, arg) -> bool: + # This is a special branch for handling SymInt args in inductor's + # ExternalFallbackNode. + # For regular FX graph, SymInt arg should be a fx.Node and should be + # verified with is_sym_int_arg() + return type(arg) is int or isinstance(arg, torch.SymInt) + + def is_sym_int_arg(self, arg) -> bool: + return type(arg) is int or ( + isinstance(arg, torch.fx.Node) + and arg.name in self.graph_state.sym_int_values + ) + + def is_sym_float_arg(self, arg) -> bool: + return isinstance(arg, float) or ( + isinstance(arg, torch.fx.Node) + and arg.name in self.graph_state.sym_float_values + ) + + def is_sym_bool_arg(self, arg) -> bool: + return isinstance(arg, bool) or ( + isinstance(arg, torch.fx.Node) + and arg.name in self.graph_state.sym_bool_values + ) + + # should be torch._C.JitType but that annotation is busted + def serialize_input(self, arg, arg_type: Any | None = None) -> Argument: + import torch._inductor.ir as inductor_ir + + inductor_tensor_buffers = ( + inductor_ir.Buffer, + inductor_ir.ReinterpretView, + ) + + if isinstance(arg, torch.fx.Node): + if arg.op == "get_attr": + if not isinstance(arg.target, str): + raise AssertionError( + f"expected arg.target to be str, got {type(arg.target).__name__}" + ) + attr = getattr(arg.graph.owning_module, arg.target) + + if isinstance(attr, torch.Tensor): + raise SerializeError( + "getattr nodes containing tensors should not appear in the graph" + ) + elif isinstance(attr, torch.fx.GraphModule): + with self.save_graph_state(): + graph = self.serialize_graph(attr) + return Argument.create( + as_graph=GraphArgument(name=arg.target, graph=graph) + ) + elif type(attr).__name__ == "LoweredBackendModule": + # Special handling for executorch_call_delegate HOP + # It's first argument is a LoweredBackendModule, for which we + # serialize name and backend id of the lowered module + module_name = getattr(attr, "module_name", None) + backend_id = getattr(attr, "backend_id", None) + if module_name is None: + raise AssertionError("module_name should not be None") + if backend_id is None: + raise AssertionError("backend_id should not be None") + return Argument.create(as_string=f"{module_name}-{backend_id}") + else: + raise SerializeError( + f"Unsupported getattr attribute {arg.target} with type: {type(attr)}" + ) + elif self.is_sym_int_arg(arg): + return Argument.create( + as_sym_int=SymIntArgument.create(as_name=arg.name) + ) + elif self.is_sym_float_arg(arg): + return Argument.create( + as_sym_float=SymFloatArgument.create(as_name=arg.name) + ) + elif self.is_sym_bool_arg(arg): + return Argument.create( + as_sym_bool=SymBoolArgument.create(as_name=arg.name) + ) + elif isinstance(arg.meta["val"], ep.CustomObjArgument): + return Argument.create( + as_custom_obj=CustomObjArgument( + name=arg.name, class_fqn=arg.meta["val"].class_fqn + ) + ) + elif arg.name in self.duplicate_getitem_nodes: + dedup_name = self.duplicate_getitem_nodes[arg.name] + return Argument.create(as_tensor=TensorArgument(name=dedup_name)) + else: + return Argument.create(as_tensor=TensorArgument(name=arg.name)) + elif isinstance(arg, inductor_tensor_buffers): + # Other branches are for arguments in fx node. + # This is a special branch for handling buffers (representing tensor arguments) + # for inductor's ExternalFallbackNode + # export_extern_kernel_node() is using this function to serialize arguments + arg_name = arg.get_name() + if arg_name is None: + raise AssertionError("Buffer must have valid name") + return Argument.create(as_tensor=TensorArgument(name=arg_name)) + elif isinstance(arg, inductor_ir.TorchBindObject): + # This is a special branch for handling TorchBindObject + # for inductor's ExternalFallbackNode + # export_extern_kernel_node() is using this function to serialize arguments + arg_name = arg.get_name() + if arg_name is None: + raise AssertionError("Buffer must have valid name") + arg_val = arg.get_real_obj() + class_fqn = arg_val._type().qualified_name() + self.custom_objs[arg_name] = arg_val + return Argument.create(as_custom_obj=CustomObjArgument(arg_name, class_fqn)) + elif isinstance(arg, torch.SymInt): + # This is a special branch for handling SymInt args in inductor's + # ExternalFallbackNode. + # For regular FX graph, SymInt arg should be a fx.Node with + # self.is_sym_int_arg(arg) being true + return Argument.create(as_sym_int=SymIntArgument.create(as_name=str(arg))) + elif isinstance(arg, torch.SymFloat): + # This is a special branch for handling SymFloat args in inductor's + # ExternalFallbackNode. + # For regular FX graph, SymInt arg should be a fx.Node with + # self.is_sym_float_arg(arg) being true + return Argument.create( + as_sym_float=SymFloatArgument.create(as_name=str(arg)) + ) + elif type(arg) is bool: + return Argument.create(as_bool=arg) + elif type(arg) is str: + return Argument.create(as_string=arg) + elif type(arg) is int: + return Argument.create(as_int=arg) + elif type(arg) is float: + return Argument.create(as_float=arg) + elif type(arg) is complex: + return Argument.create( + as_complex=ComplexValue(real=arg.real, imag=arg.imag) + ) + elif arg is None: + return Argument.create(as_none=True) + elif isinstance(arg, dict): + serialized_dict = {} + for key, value in arg.items(): + if not isinstance(key, str): + raise SerializeError(f"Dict keys must be strings, got {type(key)}") + serialized_dict[key] = self.serialize_input(value) + return Argument.create(as_string_to_argument=serialized_dict) + elif isinstance(arg, (list, tuple)): + if len(arg) == 0: + if arg_type is not None: + if isinstance(arg_type, torch.OptionalType): + arg_type = arg_type.getElementType() # type: ignore[assignment] + if not isinstance(arg_type, torch.ListType): + raise AssertionError( + f"expected ListType, got {type(arg_type).__name__}" + ) + elem_type = arg_type.getElementType() + if isinstance(elem_type, torch.OptionalType): + elem_type = elem_type.getElementType() + + if isinstance(elem_type, torch.BoolType): + return Argument.create(as_bools=[]) + elif isinstance(elem_type, torch.IntType): + return Argument.create(as_ints=[]) + elif isinstance(elem_type, torch.FloatType): + return Argument.create(as_floats=[]) + elif isinstance(elem_type, torch.StringType): + return Argument.create(as_strings=[]) + elif isinstance(elem_type, torch.TensorType): + return Argument.create(as_tensors=[]) + else: + # I believe empty symint lists default to ints, but + # please file an issue if this is not the case + raise SerializeError(f"Empty list with type {elem_type} nyi.") + else: + # We could serialize this by default to a tensor list. This + # is needed in the HOO case + log.warning( + "Unsure how to serialize the given empty list, " + "as we don't know what is the type of this argument. " + "Serializing it as a tensor list by default." + ) + return Argument.create(as_tensors=[]) + + if all(type(a) is bool for a in arg): + return Argument.create(as_bools=list(arg)) + elif all(type(a) is int for a in arg): + return Argument.create(as_ints=list(arg)) + elif all(type(a) is float for a in arg): + return Argument.create(as_floats=list(arg)) + elif all(type(a) is str for a in arg): + return Argument.create(as_strings=list(arg)) + elif all(self.is_inductor_sym_int_arg(a) for a in arg): + # This is a special branch for handling SymInt args in inductor's + # ExternalFallbackNode. + # For regular FX graph, SymInt arg should be a fx.Node + values = [] + for a in arg: + if isinstance(a, torch.SymInt): + values.append(SymIntArgument.create(as_name=str(a))) + elif type(a) is int: + values.append(SymIntArgument.create(as_int=a)) + return Argument.create(as_sym_ints=values) + elif all(isinstance(a, torch.SymFloat) for a in arg): + return Argument.create( + as_sym_floats=[SymFloatArgument.create(as_name=str(a)) for a in arg] + ) + elif all(self.is_sym_int_arg(a) for a in arg): + # list of sym_ints + values = [] + for a in arg: + if isinstance(a, torch.fx.Node): + values.append(SymIntArgument.create(as_name=a.name)) + elif type(a) is int: + values.append(SymIntArgument.create(as_int=a)) + return Argument.create(as_sym_ints=values) + elif all(self.is_sym_float_arg(a) for a in arg): + # list of sym_float + values = [] + for a in arg: + if isinstance(a, torch.fx.Node): + values.append(SymFloatArgument.create(as_name=a.name)) + elif isinstance(a, float): + values.append(SymFloatArgument.create(as_float=a)) + return Argument.create(as_sym_floats=values) + elif all(self.is_sym_bool_arg(a) for a in arg): + # list of sym_bools + values = [] + for a in arg: + if isinstance(a, torch.fx.Node): + values.append(SymBoolArgument.create(as_name=a.name)) + elif isinstance(a, bool): + values.append(SymBoolArgument.create(as_bool=a)) + return Argument.create(as_sym_bools=values) + elif all(isinstance(a, torch.fx.Node) for a in arg): + # list of tensors + arguments = [] + for a in arg: + if a.op == "get_attr": + raise SerializeError( + "getattr nodes containing tensors should not appear in the graph" + ) + arguments.append(TensorArgument(name=a.name)) + return Argument.create(as_tensors=arguments) + elif all(isinstance(a, (list, tuple)) for a in arg) and all( + all(isinstance(t, torch.fx.Node) for t in inner) for inner in arg + ): + # nested list of tensors (List[List[Tensor]]) + nested_arguments = [] + for inner_list in arg: + inner_arguments = [] + for node in inner_list: + if node.op == "get_attr": + raise SerializeError( + "getattr nodes containing tensors should not appear in the graph" + ) + inner_arguments.append(TensorArgument(name=node.name)) + nested_arguments.append(inner_arguments) + return Argument.create(as_nested_tensors=nested_arguments) + elif all(isinstance(a, (torch.fx.Node, type(None))) for a in arg): + # list of optional tensors + def serialize_optional_tensor_args(a): + if a is None: + return OptionalTensorArgument.create(as_none=True) + elif isinstance(a, torch.fx.Node): + return OptionalTensorArgument.create( + as_tensor=TensorArgument(name=a.name) + ) + else: + raise SerializeError(f"Unsupported list/tuple argument: {a}") + + return Argument.create( + as_optional_tensors=list(map(serialize_optional_tensor_args, arg)) + ) + elif all(isinstance(a, inductor_tensor_buffers) for a in arg): + # list of inductor buffers + return Argument.create( + as_tensors=[TensorArgument(name=a.get_name()) for a in arg], + ) + elif all( + isinstance(a, (*inductor_tensor_buffers, type(None))) for a in arg + ): + # list of inductor buffers as optional tensors + def serialize_optional_tensor_args(a): + if a is None: + return OptionalTensorArgument.create(as_none=True) + elif isinstance(a, inductor_tensor_buffers): + return OptionalTensorArgument.create( + as_tensor=TensorArgument(name=a.get_name()) + ) + else: + raise SerializeError(f"Unsupported list/tuple argument: {a}") + + return Argument.create( + as_optional_tensors=list(map(serialize_optional_tensor_args, arg)) + ) + elif all( + isinstance(a, tuple) and all(type(x) is int for x in a) for a in arg + ): + # list of int tuples + return Argument.create(as_int_lists=[list(t) for t in arg]) + elif all( + isinstance(a, (list, tuple)) and all(isinstance(x, float) for x in a) + for a in arg + ): + # list of float lists (List[List[float]]) + return Argument.create(as_float_lists=[list(t) for t in arg]) + else: + raise SerializeError( + f"Unsupported list/tuple argument type: {[type(a) for a in arg]}" + ) + elif isinstance(arg, torch.dtype): + return Argument.create(as_scalar_type=_TORCH_TO_SERIALIZE_DTYPE[arg]) + elif isinstance(arg, torch.device): + return Argument.create(as_device=Device(type=arg.type, index=arg.index)) + elif isinstance(arg, torch.memory_format): + return Argument.create( + as_memory_format=_TORCH_TO_SERIALIZE_MEMORY_FORMAT[arg] + ) + elif isinstance(arg, torch.layout): + return Argument.create(as_layout=_TORCH_TO_SERIALIZE_LAYOUT[arg]) + elif isinstance(arg, torch._C.ScriptObject): + if not ( + arg._has_method("__getstate__") # type: ignore[attr-defined] + and arg._has_method("__setstate__") # type: ignore[attr-defined] + ): + raise SerializeError( + f"Unable to serialize custom class {arg}. Please define " + "serialization methods via def_pickle()." + ) + # Custom objects through torchind are serializable with pickle, + # through implementing the .def_pickle function. This should result + # in the object containing a __getstate__ and __setstate__ + # serialize/deserialize function. + custom_obj_name = f"_custom_obj_{len(self.custom_objs)}" + self.custom_objs[custom_obj_name] = arg + class_fqn = arg._type().qualified_name() # type: ignore[attr-defined] + return Argument.create( + as_custom_obj=CustomObjArgument(custom_obj_name, class_fqn) + ) + elif isinstance(arg, (torch._ops.OpOverload, torch._ops.HigherOrderOperator)): + return Argument.create(as_operator=self.serialize_operator(arg)) + else: + raise SerializeError( + f"Unsupported argument type: {type(arg)} with schema arg_type {arg_type}" + ) + + def serialize_tensor_output(self, name, meta_val) -> TensorArgument: + if name in self.graph_state.tensor_values: + raise AssertionError(f"name {name!r} already in tensor_values") + self.graph_state.tensor_values[name] = serialize_tensor_meta(meta_val) + return TensorArgument(name=name) + + def serialize_sym_int_output(self, name, meta_val) -> SymIntArgument: + if name in self.graph_state.sym_int_values: + raise AssertionError(f"name {name!r} already in sym_int_values") + self.graph_state.sym_int_values[name] = serialize_sym_int(meta_val) + return SymIntArgument.create(as_name=name) + + def serialize_sym_float_output(self, name, meta_val) -> SymFloatArgument: + if name in self.graph_state.sym_float_values: + raise AssertionError(f"name {name!r} already in sym_float_values") + self.graph_state.sym_float_values[name] = serialize_sym_float(meta_val) + return SymFloatArgument.create(as_name=name) + + def serialize_sym_bool_output(self, name, meta_val) -> SymIntArgument: + if name in self.graph_state.sym_bool_values: + raise AssertionError(f"name {name!r} already in sym_bool_values") + self.graph_state.sym_bool_values[name] = serialize_sym_bool(meta_val) + return SymBoolArgument.create(as_name=name) + + def serialize_input_spec(self, spec: ep.InputSpec) -> InputSpec: + log.debug("[serialize_input_spec] %s", spec) + if spec.kind == ep.InputKind.USER_INPUT: + if isinstance(spec.arg, ep.ConstantArgument): + if type(spec.arg.value) is int: + constant_spec = ConstantValue.create(as_int=spec.arg.value) + elif type(spec.arg.value) is bool: + constant_spec = ConstantValue.create(as_bool=spec.arg.value) + elif type(spec.arg.value) is str: + constant_spec = ConstantValue.create(as_string=spec.arg.value) + elif type(spec.arg.value) is float: + constant_spec = ConstantValue.create(as_float=spec.arg.value) + elif spec.arg.value is None: + constant_spec = ConstantValue.create(as_none=True) + else: + raise SerializeError( + f"Unhandled constant input {spec.arg.value} to serialize" + ) + return InputSpec.create( + constant_input=InputToConstantInputSpec( + name=spec.arg.name, value=constant_spec + ) + ) + else: + return InputSpec.create( + user_input=UserInputSpec(arg=self.serialize_argument_spec(spec.arg)) + ) + elif spec.kind == ep.InputKind.PARAMETER: + if spec.target is None: + raise AssertionError("spec.target should not be None for PARAMETER") + if not isinstance(spec.arg, ep.TensorArgument): + raise AssertionError( + f"expected TensorArgument, got {type(spec.arg).__name__}" + ) + return InputSpec.create( + parameter=InputToParameterSpec( + arg=TensorArgument(name=spec.arg.name), + parameter_name=spec.target, + ) + ) + elif spec.kind == ep.InputKind.BUFFER: + if spec.target is None: + raise AssertionError("spec.target should not be None for BUFFER") + if not isinstance(spec.arg, ep.TensorArgument): + raise AssertionError( + f"expected TensorArgument, got {type(spec.arg).__name__}" + ) + if spec.persistent is None: + raise AssertionError("spec.persistent should not be None for BUFFER") + return InputSpec.create( + buffer=InputToBufferSpec( + arg=TensorArgument(name=spec.arg.name), + buffer_name=spec.target, + persistent=spec.persistent, + ) + ) + elif spec.kind == ep.InputKind.CONSTANT_TENSOR: + if spec.target is None: + raise AssertionError( + "spec.target should not be None for CONSTANT_TENSOR" + ) + if not isinstance(spec.arg, ep.TensorArgument): + raise AssertionError( + f"expected TensorArgument, got {type(spec.arg).__name__}" + ) + return InputSpec.create( + tensor_constant=InputToTensorConstantSpec( + arg=TensorArgument(name=spec.arg.name), + tensor_constant_name=spec.target, + ) + ) + elif spec.kind == ep.InputKind.CUSTOM_OBJ: + if spec.target is None: + raise AssertionError("spec.target should not be None for CUSTOM_OBJ") + if not isinstance(spec.arg, ep.CustomObjArgument): + raise AssertionError( + f"expected CustomObjArgument, got {type(spec.arg).__name__}" + ) + return InputSpec.create( + custom_obj=InputToCustomObjSpec( + arg=CustomObjArgument( + name=spec.arg.name, class_fqn=spec.arg.class_fqn + ), + custom_obj_name=spec.target, + ) + ) + elif spec.kind == ep.InputKind.TOKEN: + if not isinstance(spec.arg, ep.TokenArgument): + raise AssertionError( + f"expected TokenArgument, got {type(spec.arg).__name__}" + ) + return InputSpec.create( + token=InputTokenSpec( + arg=TokenArgument(name=spec.arg.name), + ) + ) + else: + raise AssertionError(f"Unknown argument kind: {spec}") + + def serialize_output_spec(self, spec: ep.OutputSpec) -> OutputSpec: + log.debug("[serialize_output_spec] %s", spec) + if spec.kind == ep.OutputKind.USER_OUTPUT: + return OutputSpec.create( + user_output=UserOutputSpec(arg=self.serialize_argument_spec(spec.arg)) + ) + elif spec.kind == ep.OutputKind.LOSS_OUTPUT: + if not isinstance(spec.arg, ep.TensorArgument): + raise AssertionError( + f"expected TensorArgument, got {type(spec.arg).__name__}" + ) + return OutputSpec.create( + loss_output=LossOutputSpec(arg=TensorArgument(name=spec.arg.name)) + ) + elif spec.kind == ep.OutputKind.BUFFER_MUTATION: + if spec.target is None: + raise AssertionError( + "spec.target should not be None for BUFFER_MUTATION" + ) + if not isinstance(spec.arg, ep.TensorArgument): + raise AssertionError( + f"expected TensorArgument, got {type(spec.arg).__name__}" + ) + return OutputSpec.create( + buffer_mutation=BufferMutationSpec( + arg=TensorArgument(name=spec.arg.name), + buffer_name=spec.target, + ) + ) + elif spec.kind == ep.OutputKind.PARAMETER_MUTATION: + if spec.target is None: + raise AssertionError( + "spec.target should not be None for PARAMETER_MUTATION" + ) + if not isinstance(spec.arg, ep.TensorArgument): + raise AssertionError( + f"expected TensorArgument, got {type(spec.arg).__name__}" + ) + return OutputSpec.create( + parameter_mutation=ParameterMutationSpec( + arg=TensorArgument(name=spec.arg.name), + parameter_name=spec.target, + ) + ) + elif spec.kind == ep.OutputKind.GRADIENT_TO_PARAMETER: + if spec.target is None: + raise AssertionError( + "spec.target should not be None for GRADIENT_TO_PARAMETER" + ) + if not isinstance(spec.arg, ep.TensorArgument): + raise AssertionError( + f"expected TensorArgument, got {type(spec.arg).__name__}" + ) + return OutputSpec.create( + gradient_to_parameter=GradientToParameterSpec( + arg=TensorArgument(name=spec.arg.name), + parameter_name=spec.target, + ) + ) + elif spec.kind == ep.OutputKind.GRADIENT_TO_USER_INPUT: + if spec.target is None: + raise AssertionError( + "spec.target should not be None for GRADIENT_TO_USER_INPUT" + ) + if not isinstance(spec.arg, ep.TensorArgument): + raise AssertionError( + f"expected TensorArgument, got {type(spec.arg).__name__}" + ) + return OutputSpec.create( + gradient_to_user_input=GradientToUserInputSpec( + arg=TensorArgument(name=spec.arg.name), + user_input_name=spec.target, + ) + ) + elif spec.kind == ep.OutputKind.USER_INPUT_MUTATION: + if spec.target is None: + raise AssertionError( + "spec.target should not be None for USER_INPUT_MUTATION" + ) + if not isinstance(spec.arg, ep.TensorArgument): + raise AssertionError( + f"expected TensorArgument, got {type(spec.arg).__name__}" + ) + return OutputSpec.create( + user_input_mutation=UserInputMutationSpec( + arg=TensorArgument(name=spec.arg.name), + user_input_name=spec.target, + ) + ) + elif spec.kind == ep.OutputKind.TOKEN: + if not isinstance(spec.arg, ep.TokenArgument): + raise AssertionError( + f"expected TokenArgument, got {type(spec.arg).__name__}" + ) + return OutputSpec.create( + token=OutputTokenSpec( + arg=TokenArgument(name=spec.arg.name), + ) + ) + else: + raise AssertionError(f"Unknown argument kind: {spec}") + + def serialize_signature(self, sig: ep.ExportGraphSignature) -> GraphSignature: + log.debug("\n[serialize_signature]") + return GraphSignature( + input_specs=[self.serialize_input_spec(s) for s in sig.input_specs], + output_specs=[self.serialize_output_spec(s) for s in sig.output_specs], + ) + + def serialize_argument_spec(self, x: ep.ArgumentSpec) -> Argument: + if isinstance(x, ep.TensorArgument): + return Argument.create(as_tensor=TensorArgument(name=x.name)) + elif isinstance(x, ep.SymIntArgument): + return Argument.create(as_sym_int=SymIntArgument.create(as_name=x.name)) + elif isinstance(x, ep.SymFloatArgument): + return Argument.create(as_sym_float=SymFloatArgument.create(as_name=x.name)) + elif isinstance(x, ep.ConstantArgument): + return self.serialize_input(x.value) + elif isinstance(x, ep.CustomObjArgument): + return Argument.create( + as_custom_obj=CustomObjArgument(name=x.name, class_fqn=x.class_fqn) + ) + else: + raise AssertionError("TODO") + + def serialize_treespec(self, treespec: pytree.TreeSpec) -> str: + # We want to additionally save all the field names of the namedtuples in + # case users want to check that the treespec types are equivalent + def store_namedtuple_fields(ts: pytree.TreeSpec) -> None: + if ts.type is None: + return + if ts.type is namedtuple or pytree.is_namedtuple_class(ts.type): + serialized_type_name = pytree.SUPPORTED_SERIALIZED_TYPES[ + ts.context + ].serialized_type_name + if serialized_type_name in self.treespec_namedtuple_fields: + field_names = self.treespec_namedtuple_fields[ + serialized_type_name + ].field_names + if field_names != ts.context._fields: + raise SerializeError( + f"The given TreeSpec's namedtuple type {ts.context} " + f"was found to have field names {ts.context._fields} " + f"but somehow previously was found to have field names {field_names}." + ) + else: + self.treespec_namedtuple_fields[serialized_type_name] = ( + NamedTupleDef(field_names=ts.context._fields) + ) + + for child in ts.children(): + store_namedtuple_fields(child) + + serialized_treespec = treespec_dumps(treespec, TREESPEC_VERSION) + store_namedtuple_fields(treespec) + return serialized_treespec + + def serialize_module_call_signature( + self, module_call_signature: ep.ModuleCallSignature + ) -> ModuleCallSignature: + log.debug("[serialize_module_call_signature] %s", module_call_signature) + return ModuleCallSignature( + inputs=[ + self.serialize_argument_spec(x) for x in module_call_signature.inputs + ], + outputs=[ + self.serialize_argument_spec(x) for x in module_call_signature.outputs + ], + in_spec=self.serialize_treespec(module_call_signature.in_spec), + out_spec=self.serialize_treespec(module_call_signature.out_spec), + forward_arg_names=( + names if (names := module_call_signature.forward_arg_names) else None + ), + ) + + def serialize_module_call_graph( + self, module_call_graph: list[ep.ModuleCallEntry] + ) -> list[ModuleCallEntry]: + log.debug("\n[serialize_module_call_graph]") + return [ + ModuleCallEntry( + fqn=entry.fqn, + signature=( + self.serialize_module_call_signature(entry.signature) + if entry.signature + else None + ), + ) + for entry in module_call_graph + ] + + def serialize_outputs(self, node: torch.fx.Node) -> list[Argument]: + """For a given node, return the dataclass representing its output values. + + [NOTE: Multiple outputs] We handle aggregates differently than FX. For + FX, it looks like: + + x = call_function("multiple_return", ...) + element0 = call_function(getitem, x, 0) + foo = call_function("use_output", element0) + + We do not want the intermediate `getitem` call, so our serialized thing looks like: + + element0, element1, element2 = call_function("multiple_return", ...) + foo = call_function("use_output", element0) + + We want names to be consistent across these two schemes, so that we can + mostly reuse the names coming from FX. This function computes a mapping from + the FX representation to our representation, preserving the names. + """ + + def _is_single_tensor_list_return(target: Any) -> bool: + schema = _get_schema_from_target(target) + returns = schema.returns + + if len(returns) != 1: + return False + return_type = returns[0].real_type + return isinstance(return_type, torch.ListType) and isinstance( + return_type.getElementType(), torch.TensorType + ) + + if not ( + node.op == "call_function" + and isinstance( + node.target, (torch._ops.OpOverload, *_registered_extension_types()) + ) + ): + raise AssertionError( + f"expected call_function with OpOverload or registered extension type, " + f"got {node.op} with {type(node.target).__name__}" + ) + + schema = _get_schema_from_target(node.target) + returns = schema.returns + + if len(returns) == 0: + return [] + + meta_val = node.meta["val"] + + # Check single value return + if _is_single_tensor_list_return(node.target): + # e.g "-> Tensor[]" + tensor_args = [] + for idx, meta in enumerate(meta_val): + name = self._output_node_name_at_index(node, idx) + tensor_args.append(self.serialize_tensor_output(name, meta)) + return [Argument.create(as_tensors=tensor_args)] + elif len(returns) == 1: + return [self.serialize_output(node.name, meta_val)] + + # There are a two possibilities at this point: + # - This operator returns a tuple of Tensors, e.g. "-> (Tensor, Tensor)" + # - This operator returns a tuple of mixed of Tensor and Tensors, e.g. "-> (Tensor, Tensor[])" + # + # Either way, start by gathering a list of TensorArguments with the correct names. + # For consistent naming with FX, consult the downstream `getitem` node and + # make sure our outputs have the same name. + + output_arguments = [] + for idx, (meta, return_schema) in enumerate(zip(meta_val, returns)): + if meta is None: + if not isinstance( + return_schema.real_type, (torch.OptionalType, torch.TensorType) + ): + raise AssertionError( + f"expected OptionalType or TensorType, got {type(return_schema.real_type).__name__}" + ) + # When the return type is annotated as Tensor type, the op can also return an + # undefined Tensor which will be implicitly converted to None in Python. + output_arguments.append(Argument.create(as_none=True)) + elif isinstance(meta, FakeTensor): + if not isinstance( + return_schema.real_type, (torch.OptionalType, torch.TensorType) + ): + raise AssertionError( + f"expected OptionalType or TensorType, got {type(return_schema.real_type).__name__}" + ) + name = self._output_node_name_at_index(node, idx) + output_arguments.append(self.serialize_output(name, meta)) + elif isinstance(meta, list): + # for List[Tensor] return type + if not ( + isinstance(return_schema.real_type, torch.ListType) + and isinstance( + return_schema.real_type.getElementType(), torch.TensorType + ) + ): + raise AssertionError( + f"expected ListType with TensorType element, got {type(return_schema.real_type).__name__}" + ) + user_node = self._output_node_at_index(node, idx) + args = [] + for i, m in enumerate(meta): + if m is None: + continue + if user_node is None: + name = f"{node.name}_unused_{idx}_{i}" + else: + name = self._output_node_name_at_index(user_node, i) + args.append(self.serialize_tensor_output(name, m)) + output_arguments.append(Argument.create(as_tensors=args)) + elif isinstance(meta, (int, SymInt, float, SymFloat)): + user_node_name = self._output_node_name_at_index(node, idx) + output_arguments.append(self.serialize_output(user_node_name, meta)) + else: + raise ValueError( + f"Unhandled output type {type(meta)} from node {node.format_node()}" + ) + + return output_arguments + + def serialize_hoo_outputs(self, node: torch.fx.Node) -> list[Argument]: + """ + For serializing HOO outputs since HOOs do not have a schema. + """ + meta_val = node.meta["val"] + + if isinstance(meta_val, tuple): + outputs = [] + for i, element_meta_val in enumerate(meta_val): + user_node = self._output_node_at_index(node, i) + if isinstance(element_meta_val, list): + # e.g "-> Tensor[]" + tensors = [] + for j, m in enumerate(element_meta_val): + if not isinstance(m, torch.Tensor): + raise SerializeError( + f"Serialize list output with type {type(m)} nyi" + ) + + if user_node is None: + name = f"{node.name}_unused_{i}_{j}" + else: + name = self._output_node_name_at_index(user_node, j) + tensors.append(self.serialize_tensor_output(name, m)) + outputs.append(Argument.create(as_tensors=tensors)) + + else: + name = ( + user_node.name + if user_node is not None + else f"{node.name}_unused_{i}" + ) + + outputs.append(self.serialize_output(name, element_meta_val)) + + return outputs + elif isinstance(meta_val, dict): + tensor_args = [] + # use the dict key as the idx + for idx, meta in meta_val.items(): + if not isinstance(meta, torch.Tensor): + raise SerializeError( + f"Serialize list output with type {type(meta)} nyi" + ) + name = self._output_node_name_at_index(node, idx) + tensor_args.append(self.serialize_tensor_output(name, meta)) + return [Argument.create(as_tensors=tensor_args)] + else: + return [self.serialize_output(node.name, meta_val)] + + def serialize_output(self, name: str, meta_val: Any) -> Argument: + # Check single value return + if meta_val is None: + return Argument.create(as_none=True) + if isinstance(meta_val, torch.Tensor): + # e.g "-> Tensor" + return Argument.create( + as_tensor=self.serialize_tensor_output(name, meta_val) + ) + elif isinstance(meta_val, (bool, torch.SymBool)): + # e.g "-> SymBool" + return Argument.create( + as_sym_bool=self.serialize_sym_bool_output(name, meta_val) + ) + elif isinstance(meta_val, (int, torch.SymInt)): + # e.g "-> SymInt" + if isinstance(meta_val, bool): + raise AssertionError( + "meta_val should not be bool when returning SymInt" + ) + return Argument.create( + as_sym_int=self.serialize_sym_int_output(name, meta_val) + ) + elif isinstance(meta_val, (float, torch.SymFloat)): + # e.g "-> SymFloat" + return Argument.create( + as_sym_float=self.serialize_sym_float_output(name, meta_val) + ) + + # list outputs should've been handled earlier + raise SerializeError(f"Unable to serialize output {meta_val}") + + def _handle_getitem_users(self, node: torch.fx.Node) -> list[TensorArgument]: + meta_val = node.meta["val"] + + idx_to_name = {} + for user in node.users: + if user.target is not operator.getitem: + raise AssertionError(f"User node {user} of {node} is incorrect") + # pyrefly: ignore [unsupported-operation] + idx_to_name[user.args[1]] = user.name + + for idx, _ in enumerate(meta_val): + # FX does not emit a getitem node for any outputs that are unused. + # However, we need a name for them so that the number of outputs will + # correctly match the schema. Just assign a dummy name. + if idx not in idx_to_name: + # pyrefly: ignore [unsupported-operation] + idx_to_name[idx] = f"{node.name}_unused_{idx}" + + arg_list = [] + for i, element_meta_val in enumerate(meta_val): + arg_list.append( + # pyrefly: ignore [bad-index] + self.serialize_tensor_output(idx_to_name[i], element_meta_val) + ) + + return arg_list + + def serialize_graph(self, graph_module: torch.fx.GraphModule) -> Graph: + if not isinstance(graph_module, torch.fx.GraphModule): + raise AssertionError( + f"expected GraphModule, got {type(graph_module).__name__}" + ) + log.debug( + "[serialize_graph]\n\n%s", graph_module.print_readable(print_output=False) + ) + + for node in graph_module.graph.nodes: + try: + getattr(self, f"handle_{node.op}")(node) + except Exception as e: + raise SerializeError( + f"Failed serializing node {node} in graph: {node.format_node()}\n Original exception {traceback.format_exc()}" + ) from e + + return Graph( + inputs=self.graph_state.inputs, + nodes=self.graph_state.nodes, + tensor_values=self.graph_state.tensor_values, + sym_int_values=self.graph_state.sym_int_values, + sym_float_values=self.graph_state.sym_float_values, + sym_bool_values=self.graph_state.sym_bool_values, + custom_obj_values=self.graph_state.custom_obj_values, + outputs=self.graph_state.outputs, + is_single_tensor_return=self.graph_state.is_single_tensor_return, + ) + + def serialize_graph_module_metadata(self, meta: dict[str, Any]): + ret = {} + if custom := meta.get("custom"): + log.debug("\n[serialize_graph_module_metadata] %s", custom) + try: + ret["custom"] = json.dumps(custom) + except Exception as e: + raise SerializeError( + f"Failed to serialize custom metadata for graph with error {e}" + ) from e + + return ret + + def serialize(self, graph_module: torch.fx.GraphModule) -> GraphModule: + log.debug("\n[serialize]") + graph = self.serialize_graph(graph_module) + + return GraphModule( + graph=graph, + signature=self.serialize_signature(self.graph_signature), + module_call_graph=self.serialize_module_call_graph(self.module_call_graph), + metadata=self.serialize_graph_module_metadata(graph_module.meta), + treespec_namedtuple_fields=self.treespec_namedtuple_fields, + ) + + +@final +class ExportedProgramSerializer(metaclass=Final): + def __init__( + self, + opset_version: dict[str, int] | None = None, + pickle_protocol: int = DEFAULT_PICKLE_PROTOCOL, + ): + self.opset_version: dict[str, int] = {} + if opset_version: + self.opset_version.update(opset_version) + if "aten" not in self.opset_version: + self.opset_version["aten"] = torch._C._get_max_operator_version() + + self.pickle_protocol = pickle_protocol + + def serialize( + self, + exported_program: ep.ExportedProgram, + *, + serialize_state_dict: bool = True, + serialize_constants: bool = True, + serialize_example_inputs: bool = True, + ) -> _SerializedProgram: + """ + Args: + exported_program: Exported Program to serialize + """ + exported_program.validate() + + gm_serializer = GraphModuleSerializer( + exported_program.graph_signature, exported_program.module_call_graph + ) + serialized_graph_module = gm_serializer.serialize(exported_program.graph_module) + serialized_range_constraints = serialize_range_constraints( + exported_program.range_constraints + ) + + # TODO: Directly serialize exported_program.constants once + # CustomClassHolders get stored in the ExportedProgram rather than in + # the graph + constants: dict[str, Any] = gm_serializer.custom_objs.copy() + for n, t in exported_program.constants.items(): + if n in constants: + raise AssertionError(f"constant name {n!r} already exists in constants") + constants[n] = t + + serialized_ep = ExportedProgram( + graph_module=serialized_graph_module, + opset_version=self.opset_version, + range_constraints=serialized_range_constraints, + schema_version=SchemaVersion( + major=SCHEMA_VERSION[0], + minor=SCHEMA_VERSION[1], + ), + verifiers=[v.dialect for v in exported_program.verifiers], + torch_version=torch.__version__, + guards_code=exported_program._guards_code, + ) + + # Test canonical form is well defined. + canonicalize(serialized_ep, set(constants.keys())) + + # Proxy cannot be dumped, so we remove them. + new_state_dict = remove_proxy_from_state_dict( + exported_program.state_dict, in_place=False + ) + serialized_state_dict = b"" + if serialize_state_dict: + serialized_state_dict = serialize_torch_artifact( + new_state_dict, self.pickle_protocol + ) + + serialized_constants = b"" + if serialize_constants: + serialized_constants = serialize_torch_artifact( + constants, self.pickle_protocol + ) + + serialized_example_inputs = b"" + if serialize_example_inputs: + serialized_example_inputs = serialize_torch_artifact( + exported_program.example_inputs, self.pickle_protocol + ) + + return _SerializedProgram( + serialized_ep, + serialized_state_dict, + serialized_constants, + serialized_example_inputs, + ) + + +@final +class GraphModuleDeserializer(metaclass=Final): + @dataclasses.dataclass + class Result: + graph_module: torch.fx.GraphModule + signature: ep.ExportGraphSignature + module_call_graph: list[ep.ModuleCallEntry] + names_to_symbols: dict[str, sympy.Symbol] + state_dict: dict[str, torch.Tensor | torch.nn.Parameter] + constants: dict[str, _ConstantAttributeType] + example_inputs: tuple[tuple[torch.Tensor, ...], dict[str, Any]] | None + + def __init__(self) -> None: + self.serialized_name_to_node: dict[str, torch.fx.Node] = {} + self.serialized_name_to_meta: LazyMap = LazyMap() # str -> MetaType + self.graph = torch.fx.Graph() + self.module = torch.nn.Module() + + @contextmanager + def save_graph_module(self) -> Iterator[None]: + saved = ( + self.graph, + self.module, + self.serialized_name_to_node, + self.serialized_name_to_meta, + self.unbacked_symbols, + ) + self.graph = torch.fx.Graph() + self.module = torch.nn.Module() + self.serialized_name_to_node = {} + self.serialized_name_to_meta = LazyMap() + self.unbacked_symbols: set[sympy.Symbol] = set() + try: + yield + finally: + ( + self.graph, + self.module, + self.serialized_name_to_node, + self.serialized_name_to_meta, + self.unbacked_symbols, + ) = saved + + def deserialize_extension_operator(self, serialized_target: str): + namespace, op_name = serialized_target.split(":") + namespace = namespace[1:] # starting with # + handler = _deserialization_registry[namespace] + return handler.from_op_name(op_name) + + def deserialize_operator(self, serialized_target: str): + if serialized_target.startswith( + "_operator" + ): # TODO(zhxchen17) Follow up on this. + module = operator + serialized_target_names = serialized_target.split(".")[1:] + elif serialized_target.startswith("torch"): + module = torch # type: ignore[misc] + serialized_target_names = serialized_target.split(".")[1:] + elif serialized_target.startswith("math"): + module = math # type: ignore[misc] + serialized_target_names = serialized_target.split(".")[1:] + elif serialized_target.startswith("#"): + return self.deserialize_extension_operator(serialized_target) + else: # TODO(zhxchen17) Don't catch all here. + return serialized_target + + target = module + for name in serialized_target_names: + if not hasattr(target, name): + return serialized_target + else: + target = getattr(target, name) + return target + + def _parse_sym_expr( + self, expr_str: str, hint: int | bool | float | None = None + ) -> sympy.Expr: + """ + Parses and does bottom-up processing of sympy.Expr nodes, + populating ShapeEnv & caching symbols as needed. + """ + + def _process_sym_expr( + sym: sympy.Expr, hint: int | bool | float | None = None + ) -> sympy.Expr: + if sym.is_Integer or sym.is_Float or sym.is_Boolean: # base case + return sym + else: # recursive case + # important to use str(expr) and not _print_sympy(), + # str(expr) is key for self.symbol_name_to_range + expr_str = str(sym) + for arg in sym.args: + self._parse_sym_expr(arg) + # symbol caching + if expr_str in self.symbol_name_to_symbol: + sym = self.symbol_name_to_symbol[expr_str] + else: + self.symbol_name_to_symbol[expr_str] = sym + if isinstance(sym, sympy.Symbol) and symbolic_shapes.symbol_is_type( + sym, (SymT.UNBACKED_INT, SymT.UNBACKED_FLOAT) + ): + self.unbacked_symbols.add(sym) + # hints + if hint is not None and sym not in self.shape_env.backed_var_to_val: + self.shape_env.add_backed_var_to_val(sym, hint) # type: ignore[arg-type] + # ValueRanges + if vr := self.symbol_name_to_range.get(expr_str): + self.shape_env.constrain_symbol_range( + sym, + compiler_min=vr.lower, # type: ignore[arg-type] + compiler_max=vr.upper, # type: ignore[arg-type] + ) + # ShapeEnv meta + if isinstance(sym, sympy.Symbol): + self.shape_env.var_to_stack[sym] = CapturedTraceback.extract(skip=1) + return sym + + expr = sympy.sympify( + expr_str, + locals={**self.sympy_functions, **self.symbol_name_to_symbol}, + ) + return _process_sym_expr(expr, hint) + + def deserialize_sym_int(self, s: SymInt) -> int | torch.SymInt: + val = s.value + if s.type == "as_expr": + if val.hint is None: + hint = None + else: + if val.hint.type != "as_int": + raise AssertionError( + f"expected hint type 'as_int', got {val.hint.type!r}" + ) + hint = val.hint.value + + sym = self._parse_sym_expr(val.expr_str, hint) + return self.shape_env.create_symintnode(sym, hint=hint) + elif s.type == "as_int": + if type(val) is not int: + raise AssertionError(f"expected int, got {type(val).__name__}") + return val + else: + raise SerializeError( + f"SymInt has invalid field type {s.type} with value {s.value}" + ) + + def deserialize_sym_float(self, s: SymFloat) -> float | torch.SymFloat: + val = s.value + if s.type == "as_expr": + hint = val.hint.as_float if val.hint else None + sym = self._parse_sym_expr(val.expr_str, hint) + return self.shape_env.create_symfloatnode(sym, hint=hint) + elif s.type == "as_float": + if not isinstance(val, float): + raise AssertionError(f"expected float, got {type(val).__name__}") + return val + else: + raise SerializeError( + f"SymFloat has invalid field type {s.type} with value {s.value}" + ) + + def deserialize_sym_bool(self, s: SymBool) -> bool | torch.SymBool: + val = s.value + if s.type == "as_expr": + expr = self._parse_sym_expr(val.expr_str) + return self.shape_env.create_symboolnode(expr) + elif s.type == "as_bool": + if not isinstance(val, bool): + raise AssertionError(f"expected bool, got {type(val).__name__}") + return val + else: + raise SerializeError( + f"SymBool has invalid field type {s.type} with value {s.value}" + ) + + def deserialize_tensor_meta( + self, + tensor_meta: TensorMeta, + ) -> FakeTensor: + with self.fake_tensor_mode: + return cast( + FakeTensor, + torch.empty_strided( + tuple(self.deserialize_sym_int(val) for val in tensor_meta.sizes), # type: ignore[misc] + tuple(self.deserialize_sym_int(val) for val in tensor_meta.strides), # type: ignore[misc] + device=deserialize_device(tensor_meta.device), + dtype=_SERIALIZE_TO_TORCH_DTYPE[tensor_meta.dtype], + requires_grad=tensor_meta.requires_grad, + ), + ) + + def deserialize_script_obj_meta( + self, script_obj_meta: CustomObjArgument + ) -> ep.CustomObjArgument: + return ep.CustomObjArgument( + name=script_obj_meta.name, + class_fqn=script_obj_meta.class_fqn, + ) + + def deserialize_graph_output(self, output) -> torch.fx.Node | int | None: + if output.type == "as_tensor": + return self.serialized_name_to_node[output.as_tensor.name] + elif output.type == "as_sym_int": + return self.serialized_name_to_node[output.as_sym_int.as_name] + elif output.type == "as_sym_bool": + return self.serialized_name_to_node[output.as_sym_bool.as_name] + elif output.type == "as_sym_float": + return self.serialized_name_to_node[output.as_sym_float.as_name] + elif output.type == "as_int": + return output.as_int + elif output.type == "as_float": + return output.as_float + elif output.type == "as_bool": + return output.as_bool + elif output.type == "as_none": + return None + else: + raise SerializeError(f"Unable to deserialize output node {output}") + + def deserialize_graph(self, serialized_graph: Graph) -> torch.fx.Graph: + log.debug("\n[deserialize_graph]") + + # Handle the tensor metas. + for name, tensor_value in serialized_graph.tensor_values.items(): + log.debug("[deserialize_tensor_meta] %s (input): %s", name, tensor_value) + self.serialized_name_to_meta[name] = ( + lambda v=tensor_value: self.deserialize_tensor_meta(v) + ) + + for name, sym_int_value in serialized_graph.sym_int_values.items(): + log.debug("[deserialize_sym_int] %s (input): %s", name, sym_int_value) + self.serialized_name_to_meta[name] = ( + lambda v=sym_int_value: self.deserialize_sym_int(v) + ) + + for name, sym_float_value in serialized_graph.sym_float_values.items(): + log.debug("[deserialize_sym_float] %s (input): %s", name, sym_float_value) + self.serialized_name_to_meta[name] = ( + lambda v=sym_float_value: self.deserialize_sym_float(v) + ) + + for name, sym_bool_value in serialized_graph.sym_bool_values.items(): + log.debug("[deserialize_sym_bool] %s (input): %s", name, sym_bool_value) + self.serialized_name_to_meta[name] = ( + lambda v=sym_bool_value: self.deserialize_sym_bool(v) + ) + + for name, script_obj_meta in serialized_graph.custom_obj_values.items(): + log.debug("[deserialize_script_obj_meta] %s", script_obj_meta) + self.serialized_name_to_meta[name] = ( + lambda v=script_obj_meta: self.deserialize_script_obj_meta(v) + ) + + log.debug("\n[deserialize graph nodes]") + # Inputs: convert to placeholder nodes in FX. + for i, input_ in enumerate(serialized_graph.inputs): + log.debug("[deserialize input] %s", input_) + if input_.type in ("as_tensor", "as_custom_obj"): + node_name = input_.value.name + placeholder_node = self.graph.placeholder(node_name) + # FX might declare a name illegal (e.g. some nn.Modules use "input" as forward() arguments) + # we will overwrite it + placeholder_node.name = node_name + self.sync_fx_node(node_name, placeholder_node) + elif input_.type == "as_sym_int": + if input_.value.type == "as_name": + node_name = input_.value.as_name + placeholder_node = self.graph.placeholder(node_name) + # FX might declare a name illegal (e.g. some nn.Modules use "input" as forward() arguments) + # we will overwrite it + placeholder_node.name = node_name + self.sync_fx_node(node_name, placeholder_node) + else: + raise SerializeError( + f"Deserializing a constant symint {input_.value} as an input" + ) + elif input_.type in ( + "as_int", + "as_float", + "as_bool", + "as_none", + "as_string", + ): + node_name = self.signature.input_specs[i].arg.name or f"arg{i}" + placeholder_node = self.graph.placeholder(node_name) + placeholder_node.meta["val"] = self.deserialize_input(input_) + else: + raise SerializeError(f"Invalid input type {input_}") + + # Nodes: convert to call_function nodes. + for serialized_node in serialized_graph.nodes: + try: + target = self.deserialize_operator(serialized_node.target) + self.deserialize_node(serialized_node, target) + + except Exception as e: + raise SerializeError( + f"Failed deserializing node {serialized_node}\n Original exception {traceback.format_exc()}" + ) from e + + # Outputs: convert to a single `output` node. + outputs = [] + for output in serialized_graph.outputs: + log.debug("[deserialize output] %s", output) + outputs.append(self.deserialize_graph_output(output)) + + if serialized_graph.is_single_tensor_return: + if len(outputs) != 1: + raise AssertionError( + f"expected single output for single_tensor_return, got {len(outputs)}" + ) + outputs = outputs[0] # type: ignore[assignment] + else: + outputs = tuple(outputs) # type: ignore[assignment] + + output_node = self.graph.output(outputs) + + if serialized_graph.is_single_tensor_return: + output_node.meta["val"] = output_node.args[0].meta["val"] + else: + output_node.meta["val"] = tuple( + arg.meta["val"] if isinstance(arg, torch.fx.Node) else arg + for arg in output_node.args[0] + ) + + # recompute unbacked bindings + for node in self.graph.nodes: + if (val := node.meta.get("val")) is not None and ( + unbacked_bindings := symbolic_shapes._free_unbacked_symbols_with_path( + val, + (), + shape_env=self.shape_env, + pending=self.unbacked_symbols, + simplify=True, + ) + ): + node.meta["unbacked_bindings"] = unbacked_bindings + + if len(self.unbacked_symbols) != 0: + raise AssertionError( + f"expected no unbacked symbols, got {len(self.unbacked_symbols)}: {self.unbacked_symbols}" + ) + return self.graph + + def deserialize_node(self, serialized_node: Node, target: Callable) -> None: + def _is_single_tensor_return(target) -> bool: + schema = _get_schema_from_target(target) + returns = schema.returns + return len(returns) == 1 and isinstance( + returns[0].real_type, torch.TensorType + ) + + if ( + target in _SYM_OPS + or target + == torch.ops.aten.item.default # this can produce either SymInt or SymBool + ): + # BC: use serialized_node.name if available, otherwise fallback to original logic + name = ( + serialized_node.name + if serialized_node.name + else serialized_node.outputs[0].value.as_name + ) + args = self.deserialize_sym_op_inputs(serialized_node.inputs) + + fx_node = self.graph.create_node("call_function", target, args, {}, name) + self.deserialize_sym_op_outputs(serialized_node, fx_node) + elif ( + target + is torch._higher_order_ops.triton_kernel_wrap.triton_kernel_wrapper_functional + ): + raise SerializeError( + "deserialize nyi for torch._higher_order_ops.triton_kernel_wrap.triton_kernel_wrapper_functional" + ) + elif isinstance(target, torch._ops.HigherOrderOperator): + args, kwargs = self.deserialize_hoo_inputs(serialized_node.inputs) + metadata = self.deserialize_metadata(serialized_node.metadata) + for x in (*args, *kwargs.values()): + if isinstance(x, torch.fx.Node) and x.op == "get_attr": + # this means that we have deserialized a graph argument, but + # unfortunately the schema for it does not include metadata; + # so we reuse the metadata of the HOP call for such arguments + x.meta.update(metadata) + # If a serialized HOP node has a length=1 outputs of type `as_tensor``. + # There could be two cases: + # (1) The HOP node returns a single tensor + # (2) The HOP node returns a tuple containing a single tensor + # We distinguish (1) and (2) by the `is_single_tensor_return` + # field in the schema of Node + # For BC, getattr() will return True if `is_single_tensor_return` doesn't + # exist. This is because prior to adding `is_single_tensor_return`, + # only (1) could happen as we handle (2) with type `as_tensors` + # BC: use serialized_node.name if available, otherwise fallback to original logic + if serialized_node.name: + name = serialized_node.name + else: + name = ( + serialized_node.outputs[0].as_tensor.name + if len(serialized_node.outputs) == 1 + and hasattr(serialized_node.outputs[0], "as_tensor") + and getattr(serialized_node, "is_hop_single_tensor_return", True) + else None + ) + fx_node = self.graph.create_node( + "call_function", target, args, kwargs, name + ) + self.deserialize_outputs(serialized_node, fx_node) + fx_node.meta.update(metadata) + + elif isinstance( + target, (torch._ops.OpOverload, *_registered_extension_types()) + ): + # For convenience: if this node returns a single tensor, name the + # newly-created node after it. This ensures that these tensor values + # have names that are consistent with serialized. + # BC: use serialized_node.name if available, otherwise fallback to original logic + if serialized_node.name: + name = serialized_node.name + else: + name = ( + serialized_node.outputs[0].as_tensor.name + if _is_single_tensor_return(target) + else None # FX will generate a name for us. + ) + + args, kwargs = self.deserialize_inputs(target, serialized_node) + fx_node = self.graph.create_node( + "call_function", target, args, kwargs, name + ) + self.deserialize_outputs(serialized_node, fx_node) + else: + _additional_msg = ( + ( + f"We failed to resolve {target} to an operator. " + + "If it's a custom op/custom triton op, this is usually because the custom op is not registered" + + " when deserializing. Please import the custom op to register it before deserializing." + + " Otherwise, please file an issue on github." + ) + if isinstance(target, str) + else "" + ) + raise SerializeError( + _additional_msg + + f" Unsupported target type for node {serialized_node}: {type(target)}." + ) + + fx_node.meta.update(self.deserialize_metadata(serialized_node.metadata)) + log.debug( + "[deserialize_node] %s: %s(%s, {%s}) -> %s", + fx_node.name, + fx_node.target, + fx_node.args, + fx_node.kwargs, + fx_node.meta.get("val"), + ) + + # handle ShapeEnv asserts + if target is torch.ops.aten._assert_scalar.default: + if not isinstance((arg := fx_node.args[0]), bool): + expr = arg.meta["val"] # type: ignore[union-attr] + if isinstance(expr, torch.SymBool): + self.shape_env.guard_or_defer_runtime_assert( + expr.node.expr, "", fx_node + ) + elif target is torch.ops.aten.sym_constrain_range_for_size.default: + sym = fx_node.args[0].meta["val"] # type: ignore[union-attr] + if isinstance(sym, torch.SymInt): + self.shape_env._constrain_range_for_size(sym.node.expr) + + # handle nn_module_stack; serialization throws away empty dicts + if ( + fx_node.op not in ["placeholder", "output"] + and "nn_module_stack" not in fx_node.meta + ): + fx_node.meta["nn_module_stack"] = {} + + def deserialize_input_spec(self, i: InputSpec) -> ep.InputSpec: + log.debug("[deserialize_input_spec] %s", i) + if i.type == "user_input": + return ep.InputSpec( + kind=ep.InputKind.USER_INPUT, + arg=self.deserialize_argument_spec(i.user_input.arg), + target=None, + ) + elif i.type == "parameter": + return ep.InputSpec( + kind=ep.InputKind.PARAMETER, + arg=ep.TensorArgument(name=i.parameter.arg.name), + target=i.parameter.parameter_name, + ) + elif i.type == "buffer": + return ep.InputSpec( + kind=ep.InputKind.BUFFER, + arg=ep.TensorArgument(name=i.buffer.arg.name), + target=i.buffer.buffer_name, + persistent=i.buffer.persistent, + ) + elif i.type == "tensor_constant": + return ep.InputSpec( + kind=ep.InputKind.CONSTANT_TENSOR, + arg=ep.TensorArgument(name=i.tensor_constant.arg.name), + target=i.tensor_constant.tensor_constant_name, + ) + elif i.type == "custom_obj": + return ep.InputSpec( + kind=ep.InputKind.CUSTOM_OBJ, + arg=ep.CustomObjArgument( + name=i.custom_obj.arg.name, class_fqn=i.custom_obj.arg.class_fqn + ), + target=i.custom_obj.custom_obj_name, + ) + elif i.type == "token": + return ep.InputSpec( + kind=ep.InputKind.TOKEN, + arg=ep.TokenArgument(name=i.token.arg.name), + target=None, + ) + elif i.type == "constant_input": + return ep.InputSpec( + kind=ep.InputKind.USER_INPUT, + arg=ep.ConstantArgument( + name=i.constant_input.name, + value=self.deserialize_constant_input(i.constant_input.value), + ), + target=None, + ) + else: + raise AssertionError(f"Unknown input spec {i}") + + def deserialize_output_spec(self, o: OutputSpec) -> ep.OutputSpec: + log.debug("[deserialize_output_spec] %s", o) + if o.type == "user_output": + return ep.OutputSpec( + kind=ep.OutputKind.USER_OUTPUT, + arg=self.deserialize_argument_spec(o.user_output.arg), + target=None, + ) + elif o.type == "loss_output": + return ep.OutputSpec( + kind=ep.OutputKind.LOSS_OUTPUT, + arg=ep.TensorArgument(name=o.loss_output.arg.name), + target=None, + ) + elif o.type == "buffer_mutation": + return ep.OutputSpec( + kind=ep.OutputKind.BUFFER_MUTATION, + arg=ep.TensorArgument(name=o.buffer_mutation.arg.name), + target=o.buffer_mutation.buffer_name, + ) + elif o.type == "parameter_mutation": + return ep.OutputSpec( + kind=ep.OutputKind.PARAMETER_MUTATION, + arg=ep.TensorArgument(name=o.parameter_mutation.arg.name), + target=o.parameter_mutation.parameter_name, + ) + elif o.type == "gradient_to_parameter": + return ep.OutputSpec( + kind=ep.OutputKind.GRADIENT_TO_PARAMETER, + arg=ep.TensorArgument(name=o.gradient_to_parameter.arg.name), + target=o.gradient_to_parameter.parameter_name, + ) + elif o.type == "gradient_to_user_input": + return ep.OutputSpec( + kind=ep.OutputKind.GRADIENT_TO_USER_INPUT, + arg=ep.TensorArgument(name=o.gradient_to_user_input.arg.name), + target=o.gradient_to_user_input.user_input_name, + ) + elif o.type == "user_input_mutation": + return ep.OutputSpec( + kind=ep.OutputKind.USER_INPUT_MUTATION, + arg=ep.TensorArgument(name=o.user_input_mutation.arg.name), + target=o.user_input_mutation.user_input_name, + ) + elif o.type == "token": + return ep.OutputSpec( + kind=ep.OutputKind.TOKEN, + arg=ep.TokenArgument(name=o.token.arg.name), + target=None, + ) + else: + raise AssertionError(f"Unknown output spec {o}") + + def deserialize_signature(self, sig: GraphSignature) -> ep.ExportGraphSignature: + log.debug("\n[deserialize_signature]") + return ep.ExportGraphSignature( + input_specs=[self.deserialize_input_spec(i) for i in sig.input_specs], + output_specs=[self.deserialize_output_spec(o) for o in sig.output_specs], + ) + + def deserialize( + self, + serialized_graph_module: GraphModule, + serialized_state_dict: dict[str, torch.Tensor] | bytes, + constants: dict[str, Any] | bytes, + example_inputs: tuple[tuple[torch.Tensor, ...], dict[str, Any]] + | bytes + | None = None, + symbol_name_to_range: dict[str, symbolic_shapes.ValueRanges] | None = None, + ) -> Result: + global _CURRENT_DESERIALIZER + if _CURRENT_DESERIALIZER is not None: + raise AssertionError("_CURRENT_DESERIALIZER is already set") + _CURRENT_DESERIALIZER = self + try: + log.debug("\n[deserialize]") + self.shape_env = symbolic_shapes.ShapeEnv(assume_static_by_default=True) + self.fake_tensor_mode = FakeTensorMode( + allow_fallback_kernels=False, + allow_non_fake_inputs=True, + shape_env=self.shape_env, + ) + self.sympy_functions = { + # all torch.utils._sympy.functions should go here + # TODO(avik): find a better way to keep this collection in sync; + # e.g.., `exec('from torch.utils._sympy.functions import *', ...)` + # would work as long as the public API of that module is complete + "FloorDiv": torch.utils._sympy.functions.FloorDiv, + "ModularIndexing": torch.utils._sympy.functions.ModularIndexing, + "Where": torch.utils._sympy.functions.Where, + "PythonMod": torch.utils._sympy.functions.PythonMod, + "Mod": torch.utils._sympy.functions.Mod, + "CleanDiv": torch.utils._sympy.functions.CleanDiv, + "CeilToInt": torch.utils._sympy.functions.CeilToInt, + "FloorToInt": torch.utils._sympy.functions.FloorToInt, + "CeilDiv": torch.utils._sympy.functions.CeilDiv, + "LShift": torch.utils._sympy.functions.LShift, + "RShift": torch.utils._sympy.functions.RShift, + "PowByNatural": torch.utils._sympy.functions.PowByNatural, + "FloatPow": torch.utils._sympy.functions.FloatPow, + "FloatTrueDiv": torch.utils._sympy.functions.FloatTrueDiv, + "IntTrueDiv": torch.utils._sympy.functions.IntTrueDiv, + "IsNonOverlappingAndDenseIndicator": torch.utils._sympy.functions.IsNonOverlappingAndDenseIndicator, + "TruncToFloat": torch.utils._sympy.functions.TruncToFloat, + "TruncToInt": torch.utils._sympy.functions.TruncToInt, + "RoundToInt": torch.utils._sympy.functions.RoundToInt, + "RoundDecimal": torch.utils._sympy.functions.RoundDecimal, + "ToFloat": torch.utils._sympy.functions.ToFloat, + "Identity": torch.utils._sympy.functions.Identity, + } + self.symbol_name_to_symbol: dict[str, sympy.Symbol] = {} + self.constants = deserialize_torch_artifact(constants) + self.signature = self.deserialize_signature( + serialized_graph_module.signature + ) + + # deserialization does analysis with checks on 0/1, so we create fake range constraints and + # restore the original range constraints afterwards + self.symbol_name_to_range = {} + # we also need to bump unbacked sym[float,int] counters in the + # shape env to accommodate unbacked symbols in the exported program + self.unbacked_symbols = set() + count_unbacked_symfloat, count_unbacked_symint = -1, -1 + unbacked_symfloat_prefix, unbacked_symint_prefix = ( + prefix_str[t] for t in [SymT.UNBACKED_FLOAT, SymT.UNBACKED_INT] + ) + if symbol_name_to_range: + for k, vr in symbol_name_to_range.items(): + lower = vr.lower + self.symbol_name_to_range[k] = symbolic_shapes.ValueRanges( + _int_to_sympy_int(lower, -int_oo), vr.upper + ) + if k.startswith(unbacked_symfloat_prefix): + i = int(k[len(unbacked_symfloat_prefix) :]) + count_unbacked_symfloat = max(count_unbacked_symfloat, i) + elif k.startswith(unbacked_symint_prefix): + i = int(k[len(unbacked_symint_prefix) :]) + count_unbacked_symint = max(count_unbacked_symint, i) + + # TODO(pianpwk): if we can clean up unused symbols in range_constraints, + # then this logic can just be handled with self.unbacked_symbols alone + for _ in range(count_unbacked_symfloat + 1): + self.shape_env.unbacked_symfloat_counter += 1 + for _ in range(count_unbacked_symint + 1): + self.shape_env.unbacked_symint_counter += 1 + + if example_inputs is not None and len(example_inputs) > 0: + self.example_inputs = deserialize_torch_artifact(example_inputs) + else: + self.example_inputs = None + self.deserialize_graph(serialized_graph_module.graph) + + with _enable_graph_inputs_of_type_nn_module(self.example_inputs): + module_call_graph = self.deserialize_module_call_graph( + serialized_graph_module.module_call_graph + ) + graph_module = ep._create_graph_module_for_export(self.module, self.graph) + meta = {} + if custom := serialized_graph_module.metadata.get("custom"): + meta["custom"] = json.loads(custom) + if hasattr(serialized_graph_module, "treespec_namedtuple_fields"): + meta["treespec_namedtuple_fields"] = {} + for ( + type_, + fields, + ) in serialized_graph_module.treespec_namedtuple_fields.items(): + meta["treespec_namedtuple_fields"][type_] = fields.field_names + graph_module.meta = meta + return GraphModuleDeserializer.Result( + graph_module=graph_module, + signature=self.signature, + module_call_graph=module_call_graph, + names_to_symbols=self.symbol_name_to_symbol, + state_dict=deserialize_torch_artifact(serialized_state_dict), + constants=self.constants, + example_inputs=self.example_inputs, + ) + finally: + _CURRENT_DESERIALIZER = None + + def sync_fx_node(self, name: str, fx_node: torch.fx.Node): + if name in self.serialized_name_to_node: + raise SerializeError(f"Node {name} has already been deserialized before.") + # overwrite name + fx_node.name = name + self.serialized_name_to_node[name] = fx_node + if "val" in fx_node.meta: + raise AssertionError(f"fx_node {name} already has 'val' in meta") + fx_node.meta["val"] = self.serialized_name_to_meta[name] + + def deserialize_sym_op_inputs(self, inputs): + return tuple(self.deserialize_input(input.arg) for input in inputs) + + def deserialize_inputs(self, target, serialized_node: Node): + schema_args = _get_schema_from_target(target).arguments + argument_kinds = {input.name: input.kind for input in serialized_node.inputs} + actual_args = { + input.name: self.deserialize_input(input.arg) + for input in serialized_node.inputs + } + args = [] + kwargs: OrderedDict[str, Any] = OrderedDict() + for schema_arg in schema_args: + if schema_arg.name in actual_args: + arg = actual_args[schema_arg.name] + kind = argument_kinds[schema_arg.name] + if kind == ArgumentKind.POSITIONAL: + args.append(arg) + continue + elif kind == ArgumentKind.KEYWORD and not keyword.iskeyword( + schema_arg.name + ): + kwargs[schema_arg.name] = arg + continue + + # If there's no ArgumentKind found, fallback to the old cases. + is_positional = ( + not schema_arg.has_default_value() and not schema_arg.kwarg_only + ) + if is_positional: + args.append(actual_args[schema_arg.name]) + elif keyword.iskeyword(schema_arg.name): + if schema_arg.kwarg_only: + raise AssertionError( + f"schema_arg {schema_arg.name} should not be kwarg_only" + ) + if len(kwargs) > 0: + kwargs = OrderedDict() + args.extend(list(kwargs.values())) + args.append(actual_args[schema_arg.name]) + else: + if schema_arg.name in actual_args: + kwargs[schema_arg.name] = actual_args[schema_arg.name] + return tuple(args), kwargs + + def deserialize_hoo_inputs(self, inputs: list[NamedArgument]): + """ + For deserializing HOO inputs since HOOs do not have a schema. + """ + args = [] + kwargs = {} + for input_ in inputs: + if input_.name != "": + kwargs[input_.name] = self.deserialize_input(input_.arg) + else: + args.append(self.deserialize_input(input_.arg)) + return (tuple(args), kwargs) + + def deserialize_input(self, inp: Argument) -> Any: + value = inp.value + typ_ = inp.type + if typ_ == "as_none": + # None should converted as None, but is encoded as bool in serialized + # Convert serialized object to torch equivalent + return None + elif typ_ == "as_tensor": + return self.serialized_name_to_node[inp.as_tensor.name] + elif typ_ == "as_scalar_type": + return _SERIALIZE_TO_TORCH_DTYPE[inp.as_scalar_type] + elif typ_ == "as_memory_format": + return _SERIALIZE_TO_TORCH_MEMORY_FORMAT[inp.as_memory_format] + elif typ_ == "as_layout": + return _SERIALIZE_TO_TORCH_LAYOUT[inp.as_layout] + elif typ_ == "as_graph": + if not isinstance(value, GraphArgument): + raise AssertionError( + f"expected GraphArgument, got {type(value).__name__}" + ) + with self.save_graph_module(): + self.deserialize_graph(value.graph) + submodule = ep._create_graph_module_for_export(self.module, self.graph) + self.module.register_module(value.name, submodule) + return self.graph.create_node( + "get_attr", + value.name, + name=value.name, + ) + elif typ_ == "as_device": + return deserialize_device(inp.as_device) + elif typ_ == "as_int": + return inp.as_int + elif typ_ == "as_float": + return inp.as_float + elif typ_ == "as_bool": + return inp.as_bool + elif typ_ == "as_string": + return inp.as_string + elif typ_ == "as_complex": + return complex(inp.as_complex.real, inp.as_complex.imag) + elif typ_ == "as_sym_int": + return self.deserialize_sym_argument(inp.as_sym_int) + elif typ_ == "as_sym_float": + return self.deserialize_sym_argument(inp.as_sym_float) + elif typ_ == "as_sym_bool": + return self.deserialize_sym_argument(inp.as_sym_bool) + elif isinstance(value, dict): + if typ_ == "as_string_to_argument": + # Deserialize dict[str, Argument] recursively + return {k: self.deserialize_input(v) for k, v in value.items()} + else: + raise SerializeError(f"Unknown dict type: {typ_}") + elif isinstance(value, list): + if len(value) == 0: + return [] + elif typ_ == "as_tensors": + result = [self.serialized_name_to_node[arg.name] for arg in value] + return result + elif typ_ in ("as_ints", "as_floats", "as_bools", "as_strings"): + # convert from serialized.python.types.List to python list + return list(value) + elif typ_ == "as_int_lists": + # Convert list of lists back to list of tuples for Triton grids + return [tuple(dims) for dims in value] + elif typ_ == "as_float_lists": + return [list(floats) for floats in value] + elif typ_ == "as_nested_tensors": + # nested list of tensors (List[List[Tensor]]) + return [ + [self.serialized_name_to_node[arg.name] for arg in inner_list] + for inner_list in value + ] + elif typ_ in ("as_sym_ints", "as_sym_bools", "as_sym_floats"): + return [self.deserialize_sym_argument(arg) for arg in value] + elif typ_ == "as_optional_tensors": + + def deserialize_optional_tensor_args(a): + if a.type == "as_none": + return None + elif a.type == "as_tensor": + return self.serialized_name_to_node[a.value.name] + else: + raise SerializeError(f"Unhandled argument {inp}") + + return list(map(deserialize_optional_tensor_args, value)) + else: + raise SerializeError(f"Unhandled argument {inp}") + elif typ_ == "as_custom_obj": + if inp.as_custom_obj.name in self.serialized_name_to_node: + # Custom object has been lifted as an input + return self.serialized_name_to_node[inp.as_custom_obj.name] + return self.constants[inp.as_custom_obj.name] + elif typ_ == "as_operator": + return self.deserialize_operator(inp.as_operator) + else: + raise SerializeError(f"Unhandled argument {inp}") + + def deserialize_constant_input(self, inp: ConstantValue) -> Any: + if inp.type == "as_int": + return int(inp.as_int) + elif inp.type == "as_float": + return float(inp.as_float) + elif inp.type == "as_string": + return str(inp.as_string) + elif inp.type == "as_bool": + return bool(inp.as_bool) + elif inp.type == "as_none": + return None + else: + raise SerializeError(f"Unhandled constant argument {inp} to deserialize") + + def deserialize_sym_argument(self, sym_arg): + if isinstance(sym_arg, SymIntArgument): + if sym_arg.type == "as_int": + return sym_arg.as_int + elif sym_arg.type == "as_name": + return self.serialized_name_to_node[sym_arg.as_name] + elif isinstance(sym_arg, SymFloatArgument): + if sym_arg.type == "as_float": + return sym_arg.as_float + elif sym_arg.type == "as_name": + return self.serialized_name_to_node[sym_arg.as_name] + elif isinstance(sym_arg, SymBoolArgument): + if sym_arg.type == "as_bool": + return sym_arg.as_bool + elif sym_arg.type == "as_name": + return self.serialized_name_to_node[sym_arg.as_name] + raise SerializeError(f"Unknown symbolic argument type: {sym_arg}") + + def deserialize_sym_op_outputs(self, serialized_node: Node, fx_node: torch.fx.Node): + self.sync_fx_node(serialized_node.outputs[0].value.as_name, fx_node) + + def deserialize_outputs(self, serialized_node: Node, fx_node: torch.fx.Node): + # Check single value return + if len(serialized_node.outputs) == 0: + return + + if ( + len(serialized_node.outputs) == 1 + and "torch.ops.higher_order" in serialized_node.target + and not getattr(serialized_node, "is_hop_single_tensor_return", True) + and serialized_node.outputs[0].type != "as_none" + ): + + def _deserialize_hop_with_single_return(serialized_node, fx_node): + meta_val: list[Any] = [] + arg = None + if serialized_node.outputs[0].type == "as_tensor": + arg = serialized_node.outputs[0].as_tensor + elif isinstance( + serialized_node.outputs[0].value, + (SymIntArgument, SymBoolArgument, SymFloatArgument), + ): + arg = serialized_node.outputs[0].value + deserialized_metadata = self.deserialize_metadata( + serialized_node.metadata + ) + if arg is None: + raise AssertionError("arg should not be None") + # pyrefly: ignore [bad-argument-type] + self.generate_getitem(meta_val, fx_node, arg, 0, deserialized_metadata) + fx_node.meta["val"] = tuple(meta_val) + self.serialized_name_to_node[fx_node.name] = fx_node + return + + return _deserialize_hop_with_single_return(serialized_node, fx_node) + + if ( + len(serialized_node.outputs) == 1 + and serialized_node.outputs[0].type == "as_tensor" + ): + self.sync_fx_node(serialized_node.outputs[0].as_tensor.name, fx_node) + return + elif len(serialized_node.outputs) == 1 and isinstance( + serialized_node.outputs[0].value, + (SymIntArgument, SymBoolArgument, SymFloatArgument), + ): + self.sync_fx_node(serialized_node.outputs[0].value.as_name, fx_node) + return + elif ( + len(serialized_node.outputs) == 1 + and serialized_node.outputs[0].type == "as_none" + ): + # manually rename the node to a unused name to avoid naming conflicts + fx_node.meta["val"] = None + fx_node._rename(f"{self.graph._target_to_str(fx_node.target)}_unused") + return + + self.deserialize_multiple_outputs(serialized_node, fx_node) + + def generate_getitem( + self, + meta_val, + fx_node: torch.fx.Node, + arg: TensorArgument | SymIntArgument | SymFloatArgument, + idx: int, + deserialized_metadata: dict[str, Any], + ): + if isinstance(arg, TensorArgument): + name = arg.name + elif isinstance(arg, SymIntArgument): + name = arg.as_name + elif isinstance(arg, SymFloatArgument): + name = arg.as_name + else: + raise AssertionError( + f"generate_getitem got unknown argument type {type(arg)}" + ) + individual_output = self.graph.create_node( + "call_function", + operator.getitem, + (fx_node, idx), + name=name, + ) + self.sync_fx_node(name, individual_output) + meta_val.append(self.serialized_name_to_meta[name]) + # The derived `getitem` nodes should have the same stacktrace as the + # original `fx_node` + individual_output.meta.update(deserialized_metadata) + + def generate_getitems( + self, + meta_val, + fx_node: torch.fx.Node, + args, + deserialized_metadata: dict[str, Any], + ): + for idx, arg in enumerate(args): + if isinstance(arg, (TensorArgument, SymIntArgument, SymFloatArgument)): + self.generate_getitem( + meta_val, fx_node, arg, idx, deserialized_metadata + ) + continue + + if not isinstance(arg, Argument): + raise AssertionError(f"expected Argument, got {type(arg).__name__}") + if arg.type in ("as_tensor", "as_sym_int", "as_sym_float"): + self.generate_getitem( + meta_val, fx_node, arg.value, idx, deserialized_metadata + ) + elif arg.type in ( + "as_tensors", + "as_sym_ints", + "as_sym_floats", + "as_ints", + "as_floats", + "as_strings", + "as_bools", + "as_sym_bools", + ): + list_output = self.graph.create_node( + "call_function", + operator.getitem, + (fx_node, idx), + ) + meta_val.append([]) + self.generate_getitems( + meta_val[-1], list_output, arg.value, deserialized_metadata + ) + list_output.meta.update(deserialized_metadata) + list_output.meta["val"] = meta_val[-1] + elif arg.type == "as_none": + individual_output = self.graph.create_node( + "call_function", + operator.getitem, + (fx_node, idx), + name="as_none", + ) + meta_val.append(None) + individual_output.meta["val"] = None + individual_output.meta.update(deserialized_metadata) + else: + raise NotImplementedError(f"Unimplemented node output type: {arg}") + + def deserialize_multiple_outputs( + self, serialized_node: Node, fx_node: torch.fx.Node + ) -> None: + deserialized_metadata = self.deserialize_metadata(serialized_node.metadata) + + # Convert multiple return types to FX format. + # In FX, each node only returns one value. So in order to represent + # multiple return values, we have to emit a `getitem` node for each + # return value. + # This performs the inverse mapping of the `serialize_outputs` call in + # serialization, see [NOTE: Multiple outputs] + meta_val: list[Any] = [] + if len(serialized_node.outputs) == 1: + if not isinstance(serialized_node.outputs[0].value, list): + raise AssertionError( + f"expected list, got {type(serialized_node.outputs[0].value).__name__}" + ) + if not isinstance(serialized_node.outputs[0].value[0], TensorArgument): + raise AssertionError( + f"expected TensorArgument, got {type(serialized_node.outputs[0].value[0]).__name__}" + ) + self.generate_getitems( + meta_val, + fx_node, + serialized_node.outputs[0].as_tensors, + deserialized_metadata, + ) + else: + self.generate_getitems( + meta_val, fx_node, serialized_node.outputs, deserialized_metadata + ) + + # also update the metaval for `fx_node` to be a list(meta) + fx_node.meta["val"] = tuple(meta_val) + self.serialized_name_to_node[fx_node.name] = fx_node + + def deserialize_metadata(self, metadata: dict[str, str]) -> dict[str, Any]: + ret: dict[str, Any] = {} + if stack_trace := metadata.get("stack_trace"): + ret["stack_trace"] = stack_trace + + def deserialize_meta_func(serialized_target: str): + module = None + if serialized_target.startswith("torch.nn"): + module = torch.nn + serialized_target_names = serialized_target.split(".")[2:] + elif serialized_target.startswith("torch"): + module = torch + serialized_target_names = serialized_target.split(".")[1:] + else: + return self.deserialize_operator(serialized_target) + + target = module + for name in serialized_target_names: + if not hasattr(target, name): + return serialized_target + else: + target = getattr(target, name) + return target + + if nn_module_stack_str := metadata.get("nn_module_stack"): + # Originally serialized to "key,orig_path,type_str" + def import_nn_module_stack(key, path, ty): + return key, (path, ty) + + # Helper function to split string by commas, accounting for nested parentheses/brackets + def metadata_split(metadata): + out = [] + start, n = 0, 0 + a, b = "[(", ")]" + for end, c in enumerate(metadata): + if c in a: + n += 1 + elif c in b: + n -= 1 + elif c == "," and n == 0: + out.append(metadata[start:end]) + start = end + 1 + out.append(metadata[start:]) + if len(out) != 3: + raise AssertionError( + f"expected metadata_split to return 3 parts, got {len(out)}" + ) + return out + + nn_module_stack = dict( + import_nn_module_stack(*metadata_split(item)) + for item in nn_module_stack_str.split(ST_DELIMITER) + ) + ret["nn_module_stack"] = nn_module_stack + + if source_fn_st_str := metadata.get("source_fn_stack"): + # Originally serializes to "fx_node_name,op_str" + source_fn_st = [] + for source_fn_str in source_fn_st_str.split(ST_DELIMITER): + name, target_str = source_fn_str.split(",") + source_fn_st.append((name, deserialize_meta_func(target_str))) + ret["source_fn_stack"] = source_fn_st + + if torch_fn_str := metadata.get("torch_fn"): + ret["torch_fn"] = tuple(torch_fn_str.split(ST_DELIMITER)) + + if custom_str := metadata.get("custom"): + ret["custom"] = json.loads(custom_str) + + if from_node_str := metadata.get("from_node"): + ret["from_node"] = self._deserialize_from_node(json.loads(from_node_str)) + + return ret + + def _deserialize_from_node( + self, from_node_data: list[dict[str, Any]] | None + ) -> list[NodeSource] | None: + """ + Deserialize from_node metadata from JSON data. + """ + if from_node_data is None: + return None + + if not isinstance(from_node_data, list): + raise AssertionError(f"expected list, got {type(from_node_data).__name__}") + + return [ + node_source + for fn_dict in from_node_data + if (node_source := NodeSource._from_dict(fn_dict)) is not None + ] + + def deserialize_argument_spec(self, x: Argument) -> ep.ArgumentSpec: + log.debug("[deserialize_argument_spec] %s", x) + if x.type == "as_tensor": + return ep.TensorArgument(name=x.as_tensor.name) + elif x.type == "as_sym_int": + return ep.SymIntArgument(name=x.as_sym_int.as_name) + elif x.type == "as_sym_float": + return ep.SymFloatArgument(name=x.as_sym_float.as_name) + elif x.type == "as_custom_obj": + return ep.ConstantArgument( + name=x.as_custom_obj.name, value=self.deserialize_input(x) + ) + else: + return ep.ConstantArgument(name="", value=self.deserialize_input(x)) + + def deserialize_module_call_signature( + self, module_call_signature: ModuleCallSignature + ) -> ep.ModuleCallSignature: + return ep.ModuleCallSignature( + inputs=[ + self.deserialize_argument_spec(x) for x in module_call_signature.inputs + ], + outputs=[ + self.deserialize_argument_spec(x) for x in module_call_signature.outputs + ], + in_spec=treespec_loads(module_call_signature.in_spec), + out_spec=treespec_loads(module_call_signature.out_spec), + forward_arg_names=( + names if (names := module_call_signature.forward_arg_names) else None + ), + ) + + def deserialize_module_call_graph( + self, module_call_graph: list[ModuleCallEntry] + ) -> list[ep.ModuleCallEntry]: + log.debug("\n[deserialize_module_call_graph]") + return [ + ep.ModuleCallEntry( + fqn=entry.fqn, + signature=( + self.deserialize_module_call_signature(entry.signature) + if entry.signature + else None + ), + ) + for entry in module_call_graph + ] + + +@final +class ExportedProgramDeserializer(metaclass=Final): + def __init__(self, expected_opset_version: dict[str, int] | None = None): + self.expected_opset_version: dict[str, int] = {} + if expected_opset_version: + self.expected_opset_version.update(expected_opset_version) + if "aten" not in self.expected_opset_version: + self.expected_opset_version["aten"] = torch._C._get_max_operator_version() + + def deserialize_range_constraints( + self, + symbol_name_to_range: dict[str, symbolic_shapes.ValueRanges], + symbol_name_to_symbol: dict[str, sympy.Symbol], + ) -> dict[sympy.Symbol, ValueRanges]: + log.debug("\n[deserialize_range_constraints]") + range_constraints = {} + for k, v in symbol_name_to_range.items(): + if symbol := symbol_name_to_symbol.get(k): + log.debug("[deserialize_range_constraints] %s -> %s", k, v) + range_constraints[symbol] = v # type: ignore[arg-type] + else: + log.warning( + "Symbol %s did not appear in the graph that was deserialized", k + ) + return range_constraints + + def deserialize( + self, + exported_program: ExportedProgram, + state_dict: dict[str, torch.Tensor] | bytes, + constants: dict[str, torch.Tensor] | bytes, + example_inputs: tuple[tuple[torch.Tensor, ...], dict[str, Any]] + | bytes + | None = None, + *, + _unsafe_skip_version_check=False, + ) -> ep.ExportedProgram: + if not isinstance(exported_program, ExportedProgram): + raise AssertionError( + f"expected ExportedProgram, got {type(exported_program).__name__}" + ) + version = exported_program.schema_version + + # TODO(zhxchen17) blocked on thrift schema refactor + if version.major != SCHEMA_VERSION[0] and not ( + version.major == 0 and version.minor == 0 + ): + if not _unsafe_skip_version_check: + raise SerializeError( + f"Serialized schema version {exported_program.schema_version} " + f"does not match our current schema version {SCHEMA_VERSION}." + ) + + symbol_name_to_range = { + k: symbolic_shapes.ValueRanges( + _int_to_sympy_int(v.min_val, -int_oo), + _int_to_sympy_int(v.max_val, int_oo), + ) + for k, v in exported_program.range_constraints.items() + } + res = GraphModuleDeserializer().deserialize( + exported_program.graph_module, + state_dict, + constants, + example_inputs, + symbol_name_to_range, + ) + range_constraints = self.deserialize_range_constraints( + symbol_name_to_range, + res.names_to_symbols, + ) + + result = ep.ExportedProgram( + root=res.graph_module, + graph=res.graph_module.graph, + graph_signature=res.signature, + state_dict=res.state_dict, # type: ignore[arg-type] + range_constraints=range_constraints, + module_call_graph=res.module_call_graph, + example_inputs=res.example_inputs, + constants=res.constants, + verifiers=[load_verifier(v) for v in exported_program.verifiers], + ) + result._guards_code = exported_program.guards_code + log.debug("\n[deserialize]: %s", result) + return result + + +class EnumEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, Enum): + return obj.value + if isinstance(obj, bytes): + return base64.b64encode(obj).decode("utf-8") + return super().default(obj) + + +def _dataclass_to_dict(obj): + if isinstance(obj, _Union): + return {obj.type: _dataclass_to_dict(obj.value)} + elif dataclasses.is_dataclass(obj): + return { + f.name: _dataclass_to_dict(getattr(obj, f.name)) + for f in dataclasses.fields(obj) + } + elif isinstance(obj, list): + return [_dataclass_to_dict(x) for x in obj] + elif isinstance(obj, tuple): + return tuple(_dataclass_to_dict(x) for x in obj) + elif isinstance(obj, dict): + return {k: _dataclass_to_dict(v) for k, v in obj.items()} + elif isinstance(obj, float): + if obj == math.inf: + return "Infinity" + elif obj == -math.inf: + return "-Infinity" + elif math.isnan(obj): + return "NaN" + else: + return obj + else: + return obj + + +def _to_json_bytes(obj: Any) -> bytes: + return json.dumps(_dataclass_to_dict(obj), cls=EnumEncoder, allow_nan=False).encode( + "utf-8" + ) + + +def serialize( + exported_program: ep.ExportedProgram, + opset_version: dict[str, int] | None = None, + pickle_protocol: int = DEFAULT_PICKLE_PROTOCOL, + *, + serialize_state_dict: bool = True, + serialize_constants: bool = True, + serialize_example_inputs: bool = True, +) -> SerializedArtifact: + with _enable_graph_inputs_of_type_nn_module(exported_program.example_inputs): + serialized_program = ExportedProgramSerializer( + opset_version, pickle_protocol + ).serialize( + exported_program, + serialize_state_dict=serialize_state_dict, + serialize_constants=serialize_constants, + serialize_example_inputs=serialize_example_inputs, + ) + if not isinstance(serialized_program.exported_program, ExportedProgram): + raise AssertionError( + f"expected ExportedProgram, got {type(serialized_program.exported_program).__name__}" + ) + + json_bytes = _to_json_bytes(serialized_program.exported_program) + artifact = SerializedArtifact( + json_bytes, + serialized_program.state_dict, + serialized_program.constants, + serialized_program.example_inputs, + ) + return artifact + + +def _resolve_schema_cls(cls): + if isinstance(cls, str): + resolved = getattr(schema, cls, None) + if resolved is not None: + return resolved + if isinstance(cls, typing.ForwardRef): + return _resolve_schema_cls(cls.__forward_arg__) + return cls + + +def _dict_to_dataclass(cls, data): + cls = _resolve_schema_cls(cls) + if isinstance(cls, str): + raise AssertionError(f"Unresolved class type: '{cls}'.") + if typing.get_origin(cls) is Annotated: + return _dict_to_dataclass(cls.__origin__, data) + if typing.get_origin(cls) in (typing.Union, types.UnionType) and type( + None + ) in typing.get_args(cls): + if data is None: + return None + ty_args = typing.get_args(cls) + if len(ty_args) != 2: + raise AssertionError(f"expected 2 type args, got {len(ty_args)}") + return _dict_to_dataclass(ty_args[0], data) + elif isinstance(cls, type) and issubclass(cls, _Union): + if not isinstance(data, dict): + raise AssertionError(f"expected dict, got {type(data).__name__}") + if len(data) != 1: + raise AssertionError(f"expected dict with 1 key, got {len(data)}") + _type = next(iter(data.keys())) + _value = next(iter(data.values())) + if not isinstance(_type, str): + raise AssertionError(f"expected str key, got {type(_type).__name__}") + type_hints = typing.get_type_hints(cls, globalns=vars(schema)) + field_type = type_hints[_type] + # pyrefly: ignore [missing-attribute] + return cls.create(**{_type: _dict_to_dataclass(field_type, _value)}) + elif dataclasses.is_dataclass(cls): + fields = {} + type_hints = typing.get_type_hints(cls, globalns=vars(schema)) + # For forward compatibility consideration, we ignore all the keys + # that are not showing up in the dataclass definition. + for f in dataclasses.fields(cls): + name = f.name + if name not in data: + continue + new_field_obj = _dict_to_dataclass(type_hints[name], data[name]) + fields[name] = new_field_obj + return cls(**fields) # type: ignore[operator] + elif isinstance(data, list): + if len(data) == 0: + return data + d_type = typing.get_args(cls)[0] + return [_dict_to_dataclass(d_type, d) for d in data] + elif isinstance(data, dict): + v_type = typing.get_args(cls)[1] + return {k: _dict_to_dataclass(v_type, v) for k, v in data.items()} + elif cls is float: + return float(data) + return data + + +def _bytes_to_dataclass(cls: Any, artifact_bytes: bytes) -> Any: + artifact_str = artifact_bytes.decode("utf-8") + artifact_dict = json.loads(artifact_str) + artifact_dataclass = _dict_to_dataclass(cls, artifact_dict) + return artifact_dataclass + + +def deserialize( + artifact: SerializedArtifact, + expected_opset_version: dict[str, int] | None = None, + *, + _unsafe_skip_version_check=False, +) -> ep.ExportedProgram: + if not isinstance(artifact.exported_program, bytes): + raise AssertionError( + f"expected bytes, got {type(artifact.exported_program).__name__}" + ) + serialized_exported_program = _bytes_to_dataclass( + ExportedProgram, artifact.exported_program + ) + return ExportedProgramDeserializer(expected_opset_version).deserialize( + serialized_exported_program, + artifact.state_dict, + artifact.constants, + artifact.example_inputs, + _unsafe_skip_version_check=_unsafe_skip_version_check, + ) + + +def _canonicalize_graph( + sorted_inputs, sorted_outputs, graph, constants +) -> tuple[Graph, dict[str, str]]: + def _get_argument(a: Argument): + if a.type == "as_none": + return None + elif a.type == "as_tensor": + return a.as_tensor + elif a.type == "as_tensors": + return a.as_tensors + elif a.type == "as_int": + return None + elif a.type == "as_ints": + return None + elif a.type == "as_float": + return None + elif a.type == "as_floats": + return None + elif a.type == "as_string": + return None + elif a.type == "as_strings": + return None + elif a.type == "as_complex": + return None + elif a.type == "as_sym_int": + return a.as_sym_int + elif a.type == "as_sym_ints": + return a.as_sym_ints + elif a.type == "as_sym_float": + return a.as_sym_float + elif a.type == "as_sym_floats": + return a.as_sym_floats + elif a.type == "as_scalar_type": + return None + elif a.type == "as_memory_format": + return None + elif a.type == "as_layout": + return None + elif a.type == "as_device": + return None + elif a.type == "as_bool": + return None + elif a.type == "as_bools": + return None + elif a.type == "as_sym_bool": + return a.as_sym_bool + elif a.type == "as_sym_bools": + return a.as_sym_bools + elif a.type == "as_graph": + return None + elif a.type == "as_optional_tensors": + return a.as_optional_tensors + elif a.type == "as_custom_obj": + return a.as_custom_obj + elif a.type == "as_operator": + return None + elif a.type == "as_int_lists": + return None + elif a.type == "as_float_lists": + return None + elif a.type == "as_string_to_argument": + return None + elif a.type == "as_nested_tensors": + return a.as_nested_tensors + else: + raise AssertionError(f"Unknown input type to the ExportedProgram: {a}") + + # Stage 1: Reorder named items. + def for_args(f, a): + if not isinstance(a, Argument): + raise AssertionError(f"expected Argument, got {type(a).__name__}") + pytree.tree_map(f, _get_argument(a)) + + def sort_nodes(nodes): + @dataclass + class Edges: + outs: list[int] + ins: int + + graph_inputs: set[str] = set() + def_table: dict[str, int] = {} + edges: dict[int, Edges] = {} + candidates: list[tuple[str, list[tuple[str, list[int]]], int]] = [] + rank: dict[str, int] = {} + ret: list[Node] = [] + + def get_name(a) -> str | None: + if a is None: + return None + if isinstance(a, TensorArgument): + return a.name + elif isinstance(a, (SymIntArgument, SymBoolArgument, SymFloatArgument)): + if a.type == "as_name": + return a.as_name + elif a.type in ("as_int", "as_bool", "as_float"): + return None + else: + raise AssertionError(f"Unknown argument type: {a}") + elif isinstance(a, OptionalTensorArgument): + if a.type == "as_tensor": + return a.as_tensor.name + elif a.type == "as_none": + return None + else: + raise AssertionError(f"Unknown optional tensor type: {a}") + elif isinstance(a, CustomObjArgument): + return a.name + else: + raise AssertionError(f"Unknown argument type: {a}") + + for i in sorted_inputs: + + def add_input(a): + if s := get_name(a): + graph_inputs.add(s) + + for_args(add_input, i) + + for idx, node in enumerate(nodes): + + def add_def(a): + if s := get_name(a): + if s in def_table: + raise AssertionError(f"symbol {s!r} already in def_table") + def_table[s] = idx + + for o in node.outputs: + for_args(add_def, o) + + edges[idx] = Edges([], 0) + + for idx, user in enumerate(nodes): + + def add_edge(a): + if s := get_name(a): + if s in constants: + return + if s not in def_table: + if s not in graph_inputs: + raise AssertionError( + f"symbol {s!r} not in def_table or graph_inputs" + ) + return + src = def_table[s] + edges[src].outs.append(idx) + edges[idx].ins += 1 + + for i in user.inputs: + for_args(add_edge, i.arg) + + def add_rank(a): + if s := get_name(a): + if s in rank: + raise AssertionError(f"symbol {s!r} already in rank") + rank[s] = len(rank) + + def get_rank(a): + s = get_name(a) + if s and s not in constants: + return rank[s] + else: + return -1 + + for i in sorted_inputs: + for_args(add_rank, i) + + def add_candidate(idx: int): + def get_ranks(i): + ranks = [] + for_args(lambda x: ranks.append(get_rank(x)), i) + return ranks + + node = nodes[idx] + args_rank = [(a.name, get_ranks(a.arg)) for a in node.inputs] + heapq.heappush(candidates, (node.target, args_rank, idx)) + + for idx, e in edges.items(): + if e.ins == 0: + add_candidate(idx) + + while len(candidates) > 0: + _, _, idx = heapq.heappop(candidates) + node = nodes[idx] + for o in node.outputs: + for_args(add_rank, o) + ret.append(node) + if idx not in edges: + raise AssertionError(f"idx {idx} not in edges") + for user in edges[idx].outs: + e = edges[user] + if e.ins <= 0: + raise AssertionError(f"e.ins should be > 0, got {e.ins}") + e.ins -= 1 + if e.ins == 0: + add_candidate(user) + edges[idx].outs.clear() + + return ret + + sorted_nodes = sort_nodes(graph.nodes) + if len(sorted_nodes) != len(graph.nodes): + raise AssertionError( + f"expected {len(graph.nodes)} sorted nodes, got {len(sorted_nodes)}" + ) + + # Stage 2: Rename nodes. + name_table: dict[str, str] = {} + + def rename_def(a): + def _rename(arg_name, values): + new_name = f"_{len(name_table)}" + if arg_name in name_table: + raise AssertionError(f"arg_name {arg_name!r} already in name_table") + name_table[arg_name] = new_name + if arg_name not in values: + raise AssertionError(f"arg_name {arg_name!r} not in values") + values[new_name] = values.pop(arg_name) + return new_name + + if a is None: + return + if isinstance(a, TensorArgument): + a.name = _rename(a.name, graph.tensor_values) + elif isinstance(a, SymIntArgument): + if a.type == "as_name": + a.as_name = _rename(a.as_name, graph.sym_int_values) + elif isinstance(a, SymFloatArgument): + if a.type == "as_name": + a.as_name = _rename(a.as_name, graph.sym_float_values) + elif isinstance(a, SymBoolArgument): + if a.type == "as_name": + a.as_name = _rename(a.as_name, graph.sym_bool_values) + elif isinstance(a, CustomObjArgument): + a.name = _rename(a.name, graph.custom_obj_values) + else: + raise AssertionError(f"Unknown argument type: {a}") + + def replace_use(a): + if a is None: + return + if isinstance(a, TensorArgument): + a.name = name_table.get(a.name, a.name) + elif isinstance(a, (SymIntArgument, SymFloatArgument)): + if a.type == "as_name": + a.as_name = name_table.get(a.as_name, a.as_name) + elif isinstance(a, SymBoolArgument): + if a.type == "as_name": + a.as_name = name_table.get(a.as_name, a.as_name) + elif isinstance(a, OptionalTensorArgument): + if a.type == "as_tensor": + a.as_tensor.name = name_table.get(a.as_tensor.name, a.as_tensor.name) + elif isinstance(a, CustomObjArgument): + a.name = name_table.get(a.name, a.name) + else: + raise AssertionError(f"Unknown argument type: {a}") + + for i in sorted_inputs: + for_args(rename_def, i) + + for n in sorted_nodes: + for o in n.outputs: + for_args(rename_def, o) + + for n in sorted_nodes: + for i in n.inputs: + for_args(replace_use, i.arg) + + for o in sorted_outputs: + for_args(replace_use, o) + + # Stage 3: Remove unstable fields. + for n in sorted_nodes: + n.metadata.clear() + + # Stage 4: Aggregate values. + # pyrefly: ignore [no-matching-overload] + sorted_tensor_values = dict( + sorted(graph.tensor_values.items(), key=operator.itemgetter(0)) + ) + # pyrefly: ignore [no-matching-overload] + sorted_sym_int_values = dict( + sorted(graph.sym_int_values.items(), key=operator.itemgetter(0)) + ) + # pyrefly: ignore [no-matching-overload] + sorted_sym_float_values = dict( + sorted(graph.sym_float_values.items(), key=operator.itemgetter(0)) + ) + # pyrefly: ignore [no-matching-overload] + sorted_sym_bool_values = dict( + sorted(graph.sym_bool_values.items(), key=operator.itemgetter(0)) + ) + # pyrefly: ignore [no-matching-overload] + sorted_custom_obj_values = dict( + sorted(graph.custom_obj_values.items(), key=operator.itemgetter(0)) + ) + + # Stage 5: Recurse in subgraphs. + counter = 0 + for node in sorted_nodes: + for i in node.inputs: + a = i.arg + if a.type == "as_graph": + a.as_graph.graph, _ = _canonicalize_graph( + a.as_graph.graph.inputs, + a.as_graph.graph.outputs, + a.as_graph.graph, + constants, + ) + a.as_graph.name = f"_g{counter}" + counter += 1 + + graph = Graph( + inputs=sorted_inputs, + outputs=sorted_outputs, + nodes=sorted_nodes, + tensor_values=sorted_tensor_values, + sym_int_values=sorted_sym_int_values, + sym_float_values=sorted_sym_float_values, + sym_bool_values=sorted_sym_bool_values, + is_single_tensor_return=graph.is_single_tensor_return, + custom_obj_values=sorted_custom_obj_values, + ) + return graph, name_table + + +def canonicalize( + ep: ExportedProgram, constants: set[str] | None = None +) -> ExportedProgram: + """ + Normalize a serialized ExportedProgram, so that different eager program which + shares the same semantics can get a single representation on disk. + + This function canonicalizes an ExportedProgram by: + + 1. Sorting nodes in topological order. + 2. Rename nodes to have unique names. + 3. Remove unstable fields. + 4. Aggregate the above program fields. + 5. Recurse in subgraphs. + + Args: + ep (ExportedProgram): The ExportedProgram to canonicalize. + constants (Optional[set[str]]): Set of constants names + + Returns: + ExportedProgram: The canonicalized exported program. + """ + ep = copy.deepcopy(ep) + # pyrefly: ignore [annotation-mismatch, redefinition] + constants: set[str] = constants or set() + + opset_version = dict(sorted(ep.opset_version.items(), key=operator.itemgetter(0))) + range_constraints = dict( + sorted(ep.range_constraints.items(), key=operator.itemgetter(0)) + ) + guards_code = sorted(ep.guards_code) + module_call_graph = sorted(ep.graph_module.module_call_graph, key=lambda x: x.fqn) + signature = ep.graph_module.signature + graph = ep.graph_module.graph + + if len(graph.inputs) != len(signature.input_specs): + raise AssertionError( + f"graph.inputs length {len(graph.inputs)} != signature.input_specs length {len(signature.input_specs)}" + ) + if len(graph.outputs) != len(signature.output_specs): + raise AssertionError( + f"graph.outputs length {len(graph.outputs)} != signature.output_specs length {len(signature.output_specs)}" + ) + + def rank_input(inp) -> tuple[int, str | None, int]: + idx, (_arg, spec) = inp + if not isinstance(spec, InputSpec): + raise AssertionError(f"expected InputSpec, got {type(spec).__name__}") + if spec.type == "user_input": + return 5, None, idx + elif spec.type == "parameter": + return 1, spec.parameter.parameter_name, idx + elif spec.type == "buffer": + return 2, spec.buffer.buffer_name, idx + elif spec.type == "tensor_constant": + return 3, spec.tensor_constant.tensor_constant_name, idx + elif spec.type == "custom_obj": + return 4, spec.custom_obj.custom_obj_name, idx + elif spec.type == "token": + return 0, None, idx + elif spec.type == "constant_input": + return 6, spec.constant_input.name, idx + else: + raise AssertionError(f"Unknown input type: {spec}") + + def rank_output(out) -> tuple[int, str | None, int]: + idx, (_arg, spec) = out + if not isinstance(spec, OutputSpec): + raise AssertionError(f"expected OutputSpec, got {type(spec).__name__}") + if spec.type == "user_output": + return 4, None, idx + elif spec.type == "loss_output": + return 4, None, idx + elif spec.type == "parameter_mutation": + return 1, spec.parameter_mutation.parameter_name, idx + elif spec.type == "buffer_mutation": + return 2, spec.buffer_mutation.buffer_name, idx + elif spec.type == "gradient_to_parameter": + return 5, spec.gradient_to_parameter.parameter_name, idx + elif spec.type == "gradient_to_user_input": + return 6, None, idx + elif spec.type == "user_input_mutation": + return 3, None, idx + elif spec.type == "token": + return 0, None, idx + else: + raise AssertionError(f"Unknown output type: {spec}") + + sorted_ins = sorted( + enumerate(zip(graph.inputs, signature.input_specs)), key=rank_input + ) + + if len(sorted_ins) > 0: + sorted_inputs, input_specs = zip(*(i for idx, i in sorted_ins)) # type: ignore[assignment] + else: + sorted_inputs = () + input_specs = () + + sorted_outs = sorted( + enumerate(zip(graph.outputs, signature.output_specs)), key=rank_output + ) + sorted_outputs, output_specs = zip(*(i for idx, i in sorted_outs)) # type: ignore[assignment] + + sorted_graph, replace_table = _canonicalize_graph( + sorted_inputs, sorted_outputs, graph, constants + ) + + def replace_input(spec): + if not isinstance(spec, InputSpec): + raise AssertionError(f"expected InputSpec, got {type(spec).__name__}") + if spec.type == "user_input": + arg = spec.user_input.arg + if arg.type == "as_tensor": + t = arg.as_tensor + t.name = replace_table[t.name] + elif arg.type == "as_sym_int": + s = arg.as_sym_int + if s.type == "as_name": + s.as_name = replace_table[s.as_name] + elif s.type == "as_int": + pass + else: + raise AssertionError(f"Unknown sym_int type: {s}") + elif arg.type == "as_sym_float": + f = arg.as_sym_float + if f.type == "as_name": + f.as_name = replace_table[f.as_name] + elif f.type == "as_float": + pass + else: + raise AssertionError(f"Unknown sym_float type: {f}") + elif arg.type in ( + "as_none", + "as_bool", + "as_int", + "as_float", + "as_string", + "as_custom_obj", + ): + return + else: + raise AssertionError(f"Unknown input type: {arg}") + elif spec.type == "parameter": + t = spec.parameter.arg + t.name = replace_table[t.name] + elif spec.type == "buffer": + t = spec.buffer.arg + t.name = replace_table[t.name] + elif spec.type == "tensor_constant": + t = spec.tensor_constant.arg + t.name = replace_table[t.name] + elif spec.type == "custom_obj": + t_custom_obj = spec.custom_obj.arg + t_custom_obj.name = replace_table[t_custom_obj.name] + return + elif spec.type == "token": + tok = spec.token.arg + tok.name = replace_table[tok.name] + elif spec.type == "constant_input": + return + else: + raise AssertionError(f"Unknown input type: {spec}") + + def replace_output(out): + if not isinstance(spec, OutputSpec): + raise AssertionError(f"expected OutputSpec, got {type(spec).__name__}") + if spec.type == "user_output": + arg = spec.user_output.arg + if arg.type == "as_tensor": + t = arg.as_tensor + t.name = replace_table[t.name] + elif arg.type == "as_sym_int": + s = arg.as_sym_int + if s.type == "as_name": + s.as_name = replace_table[s.as_name] + elif s.type == "as_int": + pass + else: + raise AssertionError(f"Unknown sym_int type: {s}") + elif arg.type == "as_sym_float": + f = arg.as_sym_float + if f.type == "as_name": + f.as_name = replace_table[f.as_name] + elif f.type == "as_float": + pass + else: + raise AssertionError(f"Unknown sym_float type: {f}") + elif arg.type in ("as_none", "as_bool", "as_int", "as_float", "as_string"): + return + else: + raise AssertionError(f"Unknown input type: {arg}") + elif spec.type == "loss_output": + t = spec.loss_output.arg + t.name = replace_table[t.name] + elif spec.type == "buffer_mutation": + t = spec.buffer_mutation.arg + t.name = replace_table[t.name] + elif spec.type == "parameter_mutation": + t = spec.parameter_mutation.arg + t.name = replace_table[t.name] + elif spec.type == "gradient_to_parameter": + t = spec.gradient_to_parameter.arg + t.name = replace_table[t.name] + elif spec.type == "gradient_to_user_input": + g = spec.gradient_to_user_input + g.arg.name = replace_table[g.arg.name] + g.user_input_name = replace_table[g.user_input_name] + elif spec.type == "user_input_mutation": + u = spec.user_input_mutation + u.arg.name = replace_table[u.arg.name] + u.user_input_name = replace_table[u.user_input_name] + elif spec.type == "token": + tok = spec.token.arg + tok.name = replace_table[tok.name] + else: + raise AssertionError(f"Unknown output type: {spec}") + + for spec in input_specs: + replace_input(spec) + + for spec in output_specs: + replace_output(spec) + + return ExportedProgram( + graph_module=GraphModule( + graph=sorted_graph, + signature=GraphSignature( + input_specs=list(input_specs), + output_specs=list(output_specs), + ), + module_call_graph=module_call_graph, + ), + opset_version=opset_version, + range_constraints=range_constraints, + schema_version=ep.schema_version, + verifiers=ep.verifiers, + torch_version=ep.torch_version, + guards_code=guards_code, + ) + + +class ExtensionHandler: + """ + Base class for handling extension operators. + """ + + @classmethod + def namespace(cls) -> str: + raise NotImplementedError(f"{cls.__class__} namespace() must be implemented") + + @classmethod + def to_op_name(cls, op) -> str: + raise NotImplementedError(f"{cls.__class__} op_name() must be implemented") + + @classmethod + def from_op_name(cls, name: str): + raise NotImplementedError(f"{cls.__class__} op_name() must be implemented") + + @classmethod + def op_schema(cls, op) -> torch.FunctionSchema: + raise NotImplementedError(f"{cls.__class__} op_schema() must be implemented") + + +def register_extension( + op_type: type[Any], + extension_handler: type[ExtensionHandler], +): + """Register custom de/serialization method for a node with non-standard type.""" + if not issubclass(extension_handler, ExtensionHandler): + raise AssertionError(f"Expected ExtensionHandler, got {extension_handler}.") + if op_type in _serialization_registry: + raise AssertionError(f"{op_type} is already registered.") + if not isinstance(op_type, type): + raise AssertionError(f"op_type must be a type, got {type(op_type).__name__}") + if op_type.__module__.startswith("torch") or op_type.__module__.startswith( + "builtins" + ): + raise AssertionError( + f"op_type module {op_type.__module__} should not start with 'torch' or 'builtins'" + ) + if extension_handler.namespace() in _deserialization_registry: + raise AssertionError( + f"namespace {extension_handler.namespace()!r} is already registered" + ) + _serialization_registry[op_type] = extension_handler + _deserialization_registry[extension_handler.namespace()] = extension_handler + + +def _registered_extension_types(): + return tuple(_serialization_registry.keys()) + + +# Registry to store all custom serialization implementations. +# The registry maps a operation to its serialization function (a callable), in their own +# namespace to avoid conflicts. +# Serialization: Op type --> custom handler. +# De-serialization: Namespace --> custom handler. +_serialization_registry: dict[type[Any], type[ExtensionHandler]] = {} +_deserialization_registry: dict[str, type[ExtensionHandler]] = {} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/union.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/union.py new file mode 100644 index 0000000000000000000000000000000000000000..d694267ea28f253e29820026ecc6d95d00fd907d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/serde/union.py @@ -0,0 +1,104 @@ +# mypy: allow-untyped-defs +import functools +from collections.abc import Hashable +from dataclasses import dataclass, fields +from typing import TypeVar +from typing_extensions import dataclass_transform + + +T = TypeVar("T", bound="_Union") + + +class _UnionTag(str): + __slots__ = ("_cls",) + _cls: Hashable + + @staticmethod + def create(t, cls): + tag = _UnionTag(t) + if hasattr(tag, "_cls"): + raise AssertionError("tag already has _cls attribute") + tag._cls = cls + return tag + + def __eq__(self, cmp) -> bool: + if not isinstance(cmp, str): + raise AssertionError(f"expected str, got {type(cmp)}") + other = str(cmp) + if other not in _get_field_names(self._cls): + raise AssertionError( + f"{other} is not a valid tag for {self._cls}. Available tags: {_get_field_names(self._cls)}" + ) + return str(self) == other + + def __hash__(self): + return hash(str(self)) + + +@functools.cache +def _get_field_names(cls) -> set[str]: + return {f.name for f in fields(cls)} + + +# If you turn a schema class that inherits from union into a dataclass, please use +# this decorator to configure it. It's safe, faster and allows code sharing. +# +# For example, _union_dataclass customizes the __eq__ method to only check the type +# and value property instead of default implementation of dataclass which goes +# through every field in the dataclass. +@dataclass_transform(eq_default=False) +def _union_dataclass(cls: type[T]) -> type[T]: + if not issubclass(cls, _Union): + raise AssertionError(f"{cls} must inherit from {_Union}.") + return dataclass(repr=False, eq=False)(cls) + + +class _Union: + _type: _UnionTag + + @classmethod + def create(cls, **kwargs): + if len(kwargs) != 1: + raise AssertionError(f"expected exactly 1 kwarg, got {len(kwargs)}") + obj = cls(**{**{f.name: None for f in fields(cls)}, **kwargs}) # type: ignore[arg-type] + obj._type = _UnionTag.create(next(iter(kwargs.keys())), cls) + return obj + + def __post_init__(self): + if any( + f.name in ("type", "_type", "create", "value") + for f in fields(self) # type: ignore[arg-type, misc] + ): + raise AssertionError( + "field names 'type', '_type', 'create', 'value' are reserved" + ) + + @property + def type(self) -> str: + try: + return self._type + except AttributeError as e: + raise RuntimeError( + f"Please use {type(self).__name__}.create to instantiate the union type." + ) from e + + @property + def value(self): + return getattr(self, self.type) + + def __getattribute__(self, name): + attr = super().__getattribute__(name) + if attr is None and name in _get_field_names(type(self)) and name != self.type: # type: ignore[arg-type] + raise AttributeError(f"Field {name} is not set.") + return attr + + def __eq__(self, other: object) -> bool: + if not isinstance(other, _Union): + return False + return self.type == other.type and self.value == other.value + + def __str__(self): + return self.__repr__() + + def __repr__(self): + return f"{type(self).__name__}({self.type}={getattr(self, self.type)})" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/tools.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/tools.py new file mode 100644 index 0000000000000000000000000000000000000000..9e846de96c46545a07c9ee4481aee4f0255dc763 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/tools.py @@ -0,0 +1,148 @@ +# mypy: allow-untyped-defs +import logging +import warnings +from collections.abc import Iterable +from typing import Any + +import torch +import torch.export +import torch.export._trace +from torch._utils_internal import log_export_usage + + +log = logging.getLogger(__name__) + +__all__ = ["report_exportability"] + + +def _generate_inputs_for_submodules( + model: torch.nn.Module, + target_submodules: Iterable[str], + args: tuple[Any, ...], + kwargs: dict[str, Any] | None = None, +) -> dict[str, tuple[Any, Any]]: + """ + Generate inputs for targeting submdoules in the given model. Note that if two submodules refer to the same obj, this + function doesn't work. + + Args: + model: root model. + inputs: inputs to the root model. + target_submodules: submodules that we want to generate inputs for. + + Returns: + A dict that maps from submodule name to its inputs. + """ + kwargs = kwargs or {} + + handles = [] + results = {} + submodule_to_names = {mod: name for name, mod in model.named_modules()} + + def pre_forward(module, module_args, module_kwargs): + results[submodule_to_names[module]] = (module_args, module_kwargs) + + try: + for name, mod in model.named_modules(): + if name in target_submodules: + handles.append( + mod.register_forward_pre_hook(pre_forward, with_kwargs=True) + ) + model(*args, **kwargs) + except Exception as e: + warnings.warn( + f"Failed to generate submodule inputs because of the following error:\n{e}", + stacklevel=2, + ) + finally: + for h in handles: + h.remove() + return results + + +def report_exportability( + mod: torch.nn.Module, + args: tuple[Any, ...], + kwargs: dict[str, Any] | None = None, + *, + strict: bool = True, + pre_dispatch: bool = False, +) -> dict[str, Exception | None]: + """ + Report exportability issues for a module in one-shot. + + Args: + mod: root module. + args: args to the root module. + kwargs: kwargs to the root module. + Returns: + A dict that maps from submodule name to the exception that was raised when trying to export it. + `None` means the module is exportable without issue. + Sample output: + { + '': UnsupportedOperatorException(func=), + 'submod_1': UnsupportedOperatorException(func=), + 'submod_2': None + } + """ + + log_export_usage(event="export.report_exportability") + + kwargs = kwargs or {} + + all_submod_names = [name for name, _ in mod.named_modules() if name != ""] + submod_inputs = _generate_inputs_for_submodules(mod, all_submod_names, args, kwargs) + + tried_module_types = set() + report: dict[str, Exception | None] = {} + + def try_export(module, module_name, args, kwargs): + nonlocal submod_inputs, report, strict, pre_dispatch, tried_module_types + + if type(module) in tried_module_types: + return + tried_module_types.add(type(module)) + + if args is not None or kwargs is not None: + try: + torch.export._trace._export( + module, + args, + kwargs, + strict=strict, + pre_dispatch=pre_dispatch, + ) + report[module_name] = None + log.info("Successfully exported `%s`", module_name) + return + except Exception as e: + short_msg = repr(e).split("\n")[0] + log.warning( + "Failed exporting `%s` with exception: %s", module_name, short_msg + ) + report[module_name] = e + + for name, submod in module.named_children(): + sub_module_name = name if module_name == "" else f"{module_name}.{name}" + + submod_args, submod_kwargs = submod_inputs.get( + sub_module_name, (None, None) + ) + + try_export(submod, sub_module_name, submod_args, submod_kwargs) + + return + + try_export(mod, "", args, kwargs) + + unique_issues = set() + for exception in report.values(): + if exception is not None: + key = repr(exception).split("\\n")[0] + unique_issues.add(key) + + log.warning("Found %d export issues:", len(unique_issues)) + for issue in unique_issues: + log.warning(issue) + + return report diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..c0ebd5d5938fbc442d33350944b3007406f3ae18 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/utils.py @@ -0,0 +1,1638 @@ +# mypy: allow-untyped-defs +import ast +import copy +import dataclasses +import functools +import inspect +import json +import math +import operator +import re +from collections import defaultdict +from collections.abc import Callable, Iterable +from contextlib import contextmanager +from inspect import ismethod, Parameter +from typing import Any, TYPE_CHECKING + +import torch +from torch._guards import detect_fake_mode +from torch._subclasses.fake_tensor import FakeTensor, FakeTensorMode +from torch._subclasses.functional_tensor import FunctionalTensor +from torch.fx._utils import first_call_function_nn_module_stack +from torch.fx.experimental.proxy_tensor import PreDispatchTorchFunctionMode +from torch.fx.passes.runtime_assert import insert_deferred_runtime_asserts + + +if TYPE_CHECKING: + import sympy + + from torch._export.passes.lift_constants_pass import ConstantAttrMap + from torch._ops import OperatorBase + from torch.export import ExportedProgram + from torch.export.graph_signature import ExportGraphSignature + +from torch.export.graph_signature import CustomObjArgument, InputKind, OutputKind +from torch.fx._pytree import ( + _deregister_pytree_flatten_spec, + register_pytree_flatten_spec, +) +from torch.utils._pytree import ( + _deregister_pytree_node, + _register_pytree_node, + Context, + FlattenFn, + FromDumpableContextFn, + GetAttrKey, + KeyPath, + keystr, + MappingKey, + SequenceKey, + ToDumpableContextFn, + tree_flatten_with_path, + UnflattenFn, +) + + +placeholder_prefixes = { + InputKind.USER_INPUT: "", + InputKind.PARAMETER: "p_", + InputKind.BUFFER: "b_", + InputKind.CONSTANT_TENSOR: "c_", + InputKind.CUSTOM_OBJ: "obj_", + InputKind.TOKEN: "token", +} + +_DISABLE_ATEN_TO_ASSERTION_PASS = False + + +def _collect_and_set_constant_attrs( + graph_signature, constants, mod +) -> "ConstantAttrMap": + # the exported module will store constants & non-persistent buffers such that + # retracing treats them as persistent buffers, so we inform the constants lifting pass + # and overwrite the new graph signature using the previous program. This is intended to only be used + # in run_decompositions where we still have access to original EP. + from torch._export.passes.lift_constants_pass import ConstantAttrMap + + constant_attrs = ConstantAttrMap() + non_persistent_buffers = { + spec.target + for spec in graph_signature.input_specs + if spec.kind == InputKind.BUFFER and not spec.persistent + } + for name, value in constants.items(): + if name in non_persistent_buffers: + continue + # recursive getattr + _mod = mod + *atoms, attr = name.split(".") + for atom in atoms: + _mod = getattr(_mod, atom) + # remove as buffer, reassign as constant/non-persistent buffer + _mod._buffers.pop(attr, None) + setattr(_mod, attr, value) + constant_attrs.add(value, name) + return constant_attrs + + +def _register_constants_as_buffers( + mod: torch.fx.GraphModule, state_dict, non_persistent_buffers +): + # TODO some annoying circular dependency issue + from torch.export.unflatten import _assign_attr, _AttrKind + + temp_registered_constants = set() + + for node in mod.graph.nodes: + if node.op == "get_attr": + target = torch.fx.graph_module._get_attr(mod, node.target) + if isinstance(target, torch.Tensor): + # Make sure we also check if the original buffer is + # non persistent as well. + if (node.target not in state_dict) and ( + node.target not in non_persistent_buffers + ): + torch.fx.graph_module._del_attr(mod, node.target) + _assign_attr(target, mod, node.target, _AttrKind.BUFFER, False) + temp_registered_constants.add(node.target) + + mod.recompile() + + return temp_registered_constants + + +def _override_graph_signature_for_temp_registered_constants( + sig: "ExportGraphSignature", temp_registered_constants +): + for spec in sig.input_specs: + if spec.target in temp_registered_constants: + spec.kind = InputKind.CONSTANT_TENSOR + spec.persistent = None + + for spec in sig.output_specs: + if ( + spec.kind == OutputKind.BUFFER_MUTATION + and spec.target in temp_registered_constants + ): + raise RuntimeError( + f"Constant {spec.target} is mutated in the forward method. Pls register it as buffer" + ) + + return sig + + +def _overwrite_signature_for_non_persistent_buffers( + old_sig: "ExportGraphSignature", new_sig: "ExportGraphSignature" +): + # overwrite signature for non-persistent buffers + non_persistent_buffers = { + spec.target + for spec in old_sig.input_specs + if spec.kind == InputKind.BUFFER and not spec.persistent + } + + for spec in new_sig.input_specs: + if spec.kind == InputKind.BUFFER and spec.target in non_persistent_buffers: + spec.persistent = False + return new_sig + + +def _collect_param_buffer_metadata(mod: torch.fx.GraphModule) -> dict[str, Any]: + """ + Param/buffer metadata needs to be saved before lowering to aten IR + because aten IR lifts them, as a result, automatic preservation doesn't work. + This is intended to be called on the strict mode tracing right before lowering to + aten IR OR run_decomposition pass. + """ + params_buffers_to_node_meta = {} + + def _getattr(model: torch.fx.GraphModule, attr_name: str): + *prefix, field = attr_name.split(".") + t = model + for item in prefix: + t = getattr(t, item, None) # type: ignore[assignment] + if t is None: + raise AssertionError(f"attribute {item!r} not found in path") + + return getattr(t, field) + + for node in mod.graph.nodes: + target = node.target + meta = node.meta + if node.op == "call_module": + submodule = _getattr(mod, target) + if isinstance(submodule, torch.nn.Module): + for name, _ in submodule.named_parameters( + recurse=True, remove_duplicate=False + ): + params_buffers_to_node_meta[target + "." + name] = meta + + for name, _ in submodule.named_buffers( + recurse=True, remove_duplicate=False + ): + params_buffers_to_node_meta[target + "." + name] = meta + + if node.op == "get_attr": + submodule = _getattr(mod, target) + if not isinstance(submodule, torch.fx.GraphModule): + params_buffers_to_node_meta[target] = meta + + # If the call_function uses param as input, we also need to update params' meta + # with this call_function node's meta. + # This is basically the same flow as torch.fx.traceback.preserve_meta() + if node.op == "call_function" and not isinstance( + node.target, torch._ops.HigherOrderOperator + ): + for arg in node._input_nodes: + if arg.op == "get_attr": + for entry in torch.fx.proxy._COPY_META_FIELDS: + # the custom field should not be copied + if entry == "custom": + continue + if entry in meta: + params_buffers_to_node_meta[arg.target][entry] = meta[entry] + + return params_buffers_to_node_meta + + +def _maybe_find_pre_dispatch_tf_mode_for_export(): + if not torch._C._is_torch_function_mode_enabled(): + return None + + torch_function_mode_stack = torch.overrides._get_current_function_mode_stack() + + pre_dispatch_tf_modes = [ + mode + for mode in torch_function_mode_stack + if isinstance(mode, PreDispatchTorchFunctionMode) + ] + + if len(pre_dispatch_tf_modes) > 1: + raise AssertionError( + f"Expected only one PreDispatchTorchFunctionMode, found {len(pre_dispatch_tf_modes)}" + ) + + if len(pre_dispatch_tf_modes) == 0: + return None + + mode = pre_dispatch_tf_modes[0] + return mode + + +def _populate_param_buffer_metadata_to_new_gm( + params_buffers_to_node_meta: dict[str, Any], + gm: torch.fx.GraphModule, + new_sig: "ExportGraphSignature", +) -> None: + """ + Given that we collected param'buffer metadata before, we put them back in + newly traced graph module + """ + # Don't copy over nn_module_stack, stack_trace metadata for params/buffers nodes + for metadata in params_buffers_to_node_meta.values(): + metadata.pop("nn_module_stack", None) + metadata.pop("stack_trace", None) + + for node in gm.graph.nodes: + if node.op == "placeholder": + if node.target in new_sig.inputs_to_parameters: + param_name = new_sig.inputs_to_parameters[node.target] + if param_name in params_buffers_to_node_meta: + for k, v in params_buffers_to_node_meta[param_name].items(): + node.meta[k] = v + if node.target in new_sig.inputs_to_buffers: + buffer_name = new_sig.inputs_to_buffers[node.target] + if buffer_name in params_buffers_to_node_meta: + for k, v in params_buffers_to_node_meta[buffer_name].items(): + node.meta[k] = v + + +def _get_shape_env_from_gm(gm: torch.fx.GraphModule): + vals = [ + node.meta["val"] + for node in gm.graph.nodes + if node.meta.get("val", None) is not None + ] + + fake_mode = _detect_fake_mode_from_gm(gm) + if fake_mode is not None: + return fake_mode.shape_env + for v in vals: + if isinstance(v, torch.SymInt): + return v.node.shape_env + + +def _rename_without_collisions( + name_map: dict[str, str], + find_available: dict[str, int], + used_names: set[str], + orig_name: str, + name: str, + is_placeholder: bool = False, +): + """ + Renames nodes to avoid name collisions, with suffixing. + name_map: map from original name to new name + find_available: map prefix to available suffix + used_names: cache of used names + orig_name: mapping key + name: candidate name (potentially suffixed, e.g. mul_2) + is_placeholder: if the node is a placeholder, avoid detecting suffix + """ + match = re.match(r"(.*)_(\d+)", name) + key = name + + if match and not is_placeholder: + prefix, n = match.group(1), match.group(2) + key = prefix + + new_name = name + if new_name in used_names: + new_name = f"{key}_{find_available[key] + 1}" + + match = re.match(r"(.*)_(\d+)", new_name) + if match: + prefix, n = match.group(1), match.group(2) + if int(n) > find_available[prefix]: + find_available[prefix] = int(n) + + name_map[orig_name] = new_name + used_names.add(new_name) + + return name_map[orig_name] + + +def get_keystr(key_path: KeyPath) -> str: + """For a given index into the flat_args, return a human readable string + describing how to access it, e.g. "*args["foo"][0].bar" + """ + # Prefix the keypath with "*args" or "**kwargs" to make it clearer where + # the arguments come from. Ultimately we ought to serialize the + # original arg names for the best error message here. + args_kwargs_key_path = key_path[0] + if not isinstance(args_kwargs_key_path, SequenceKey): + raise AssertionError( + f"expected SequenceKey, got {type(args_kwargs_key_path).__name__}" + ) + if args_kwargs_key_path.idx == 0: + return f"*args{keystr(key_path[1:])}" + else: + kwarg_key = key_path[1] + if not isinstance(kwarg_key, (GetAttrKey, MappingKey)): + raise AssertionError( + f"expected GetAttrKey or MappingKey, got {type(kwarg_key).__name__}" + ) + name = str(kwarg_key)[1:-1] # get rid of the enclosed [] + return f"{name}{keystr(key_path[2:])}" + + +def _check_symint( + symint: int | torch.SymInt, + arg: int, + range_constraints, + unification_map, + keypath: KeyPath, + i: int | None = None, +) -> None: + from torch.export.dynamic_shapes import _IntWrapper + + if ( + isinstance(arg, torch.SymInt) + and not arg.node.expr.is_number + or isinstance(arg, _IntWrapper) + ): + # This can happen when, say, arg is a fake tensor. + # We do not run checks on symbolic shapes of fake inputs as + # such checks can affect the shape env. + return + + import sympy + + from torch._export.passes.add_runtime_assertions_for_constraints_pass import ( + _convert_range_to_int, + ) + from torch.utils._sympy.solve import try_solve + + if isinstance(symint, torch.SymInt) and len(symint.node.expr.free_symbols) == 1: + symbol = next(iter(symint.node.expr.free_symbols)) + if symbol in unification_map: + existing_dim = symint.node.expr.subs(unification_map) + if arg != existing_dim: + path = get_keystr(keypath) + if i is not None: + path += f".shape[{i}]" + raise RuntimeError( + f"Expected input at {path} to be equal to {existing_dim}, but got {arg}", + ) + else: + if isinstance(symint.node.expr, sympy.Symbol): + # Short cut for try_solve below. Also useful in cases where + # sympy.Eq(symint.node.expr, arg) would evaluate to False + # purely because symbol is constrained to be size-like, + # e.g., when symint.node.expr = symbol and arg = 0. + unification_map[symbol] = int(arg) + else: + solution = try_solve(sympy.Eq(symint.node.expr, arg), symbol) + if solution is None: + path = get_keystr(keypath) + if i is not None: + path += f".shape[{i}]" + raise RuntimeError( # noqa: B904 + f"Expected input {path} = {arg} to be " + f"of the form {symint.node.expr}, where {symbol} is an integer" + ) + else: + unification_map[symbol] = int(solution[1]) + + if symint.node.expr in range_constraints: + min_val, max_val = _convert_range_to_int( + range_constraints[symint.node.expr] + ) + # NOTE: we allow dimensions to be 0/1 at runtime + if min_val > 2: + if arg < min_val: + path = get_keystr(keypath) + if i is not None: + path += f".shape[{i}]" + raise RuntimeError( + f"Expected input at {path} to be >= {min_val}, but got {arg}", + ) + if max_val < math.inf: + if arg > max_val: + path = get_keystr(keypath) + if i is not None: + path += f".shape[{i}]" + raise RuntimeError( + f"Expected input at {path} to be <= {max_val}, but got {arg}", + ) + elif isinstance(symint, torch.SymInt) and not symint.node.expr.is_number: + # this means we deferred a guard from export analysis to runtime, let this pass + # we'll add a runtime assert checking equality to this replacement expression + pass + elif arg != int(symint): + path = get_keystr(keypath) + if i is not None: + path += f".shape[{i}]" + raise RuntimeError( + f"Expected input at {path} to be equal to {symint}, but got {arg}. " + "If you meant for this dimension to be dynamic, please re-export and specify dynamic_shapes " + "(e.g. with Dim.DYNAMIC)" + ) + + +def _check_input_constraints_for_graph( + input_placeholders: list[torch.fx.Node], flat_args_with_path, range_constraints +) -> None: + if len(flat_args_with_path) != len(input_placeholders): + raise RuntimeError( + "Unexpected number of inputs " + f"(expected {len(input_placeholders)}, got {len(flat_args_with_path)})" + ) + # NOTE: export already guarantees that the same symbol is used in metadata + # for all InputDims related by equality constraints, so we can just unify + # symbols with given input dimension values to check equality constraints. + unification_map: dict[sympy.Symbol, Any] = {} + for (key_path, arg), node in zip(flat_args_with_path, input_placeholders): + node_val = node.meta.get("val") + if isinstance(node_val, FakeTensor): + if not isinstance(arg, torch.Tensor): + raise RuntimeError( + f"Expected input at {get_keystr(key_path)} to be a tensor, but got {type(arg)}", + ) + + if len(node_val.shape) != len(arg.shape): + raise RuntimeError( + f"Unexpected number of dimensions in input at {get_keystr(key_path)}.shape " + f"(expected {node_val.shape}, got {arg.shape})" + ) + + for j, (arg_dim, node_dim) in enumerate(zip(arg.shape, node_val.shape)): + _check_symint( + node_dim, arg_dim, range_constraints, unification_map, key_path, j + ) + + elif isinstance(node_val, (int, float, str)): + if type(arg) is not type(node_val) or arg != node_val: + raise RuntimeError( + f"Expected input at {get_keystr(key_path)} to be equal to {node_val}, but got {arg}", + ) + elif isinstance(node_val, torch.SymInt): + _check_symint( + node_val, + arg, + range_constraints, + unification_map, + key_path, + None, + ) + + +def register_dataclass_as_pytree_node( + cls: type[Any], + flatten_fn: FlattenFn | None = None, + unflatten_fn: UnflattenFn | None = None, + *, + serialized_type_name: str | None = None, + to_dumpable_context: ToDumpableContextFn | None = None, + from_dumpable_context: FromDumpableContextFn | None = None, + return_none_fields: bool = False, +) -> None: + if not dataclasses.is_dataclass(cls): + raise AssertionError( + f"Only dataclasses can be registered with this function: {cls}" + ) + + @torch._dynamo.dont_skip_tracing + def default_flatten_fn(obj: Any) -> tuple[list[Any], Context]: + flattened = [] + flat_names = [] + none_names = [] + for f in dataclasses.fields(obj): + name, val = f.name, getattr(obj, f.name) + if val is not None or return_none_fields: + flattened.append(val) + flat_names.append(name) + else: + none_names.append(name) + return flattened, [flat_names, none_names] + + @torch._dynamo.dont_skip_tracing + def default_unflatten_fn(values: Iterable[Any], context: Context) -> Any: + flat_names, none_names = context + return cls(**dict(zip(flat_names, values)), **dict.fromkeys(none_names)) + + @torch._dynamo.dont_skip_tracing + def default_flatten_fn_with_keys(obj: Any) -> tuple[list[Any], Context]: + flattened, (flat_names, _none_names) = flatten_fn(obj) # type: ignore[misc] + return [(MappingKey(k), v) for k, v in zip(flat_names, flattened)], flat_names + + flatten_fn = flatten_fn if flatten_fn is not None else default_flatten_fn + unflatten_fn = unflatten_fn if unflatten_fn is not None else default_unflatten_fn + + if (to_dumpable_context is None) ^ (from_dumpable_context is None): + raise ValueError( + f"Both to_dumpable_context and from_dumpable_context for {cls} must " + "be None or registered." + ) + + _register_pytree_node( + cls, + flatten_fn, + unflatten_fn, + serialized_type_name=serialized_type_name, + flatten_with_keys_fn=default_flatten_fn_with_keys, + to_dumpable_context=to_dumpable_context, + from_dumpable_context=from_dumpable_context, + ) + + +def is_param(program: "ExportedProgram", node: torch.fx.Node) -> bool: + """ + Checks if the given node is a parameter within the exported program + """ + + return node.name in program.graph_signature.inputs_to_parameters + + +def get_param( + program: "ExportedProgram", + node: torch.fx.Node, +) -> torch.nn.Parameter | None: + """ + Returns the parameter associated with the given node in the exported program. + Returns None if the node is not a parameter within the exported program + """ + + if is_param(program, node): + parameter_name = program.graph_signature.inputs_to_parameters[node.name] + return program.state_dict[parameter_name] + + return None + + +def is_buffer(program: "ExportedProgram", node: torch.fx.Node) -> bool: + """ + Checks if the given node is a buffer within the exported program + """ + + return node.name in program.graph_signature.inputs_to_buffers + + +def get_buffer( + program: "ExportedProgram", + node: torch.fx.Node, +) -> torch.Tensor | None: + """ + Returns the buffer associated with the given node in the exported program. + Returns None if the node is not a buffer within the exported program + """ + + if is_buffer(program, node): + buffer_name = program.graph_signature.inputs_to_buffers[node.name] + if buffer_name in program.graph_signature.non_persistent_buffers: + return program.constants[buffer_name] + else: + return program.state_dict[buffer_name] + + return None + + +def is_lifted_tensor_constant( + program: "ExportedProgram", + node: torch.fx.Node, +) -> bool: + """ + Checks if the given node is a lifted tensor constant within the exported program + """ + + return node.name in program.graph_signature.inputs_to_lifted_tensor_constants + + +def get_lifted_tensor_constant( + program: "ExportedProgram", + node: torch.fx.Node, +) -> torch.Tensor | None: + """ + Returns the lifted tensor constant associated with the given node in the exported program. + Returns None if the node is not a lifted tensor constant within the exported program + """ + + if is_lifted_tensor_constant(program, node): + lifted_tensor_name = program.graph_signature.inputs_to_lifted_tensor_constants[ + node.name + ] + return program.constants[lifted_tensor_name] + + return None + + +def sequential_split( + gm: torch.fx.GraphModule, + node_call_back: Callable[[torch.fx.Node], torch.fx.Node | bool], +) -> torch.fx.GraphModule: + """ + sequential_split creates a new graph module that splits the input graph module into multiple submodules + based on the node_call_back. It doesn't mutate the input graph module. The node_call_back should return + True if the node is a delimiter. Delimiter will be the first node in the next submodule. + """ + from torch.fx.passes.split_module import split_module + + split_map = {} + split_id = 0 + for node in gm.graph.nodes: + if node_call_back(node): + split_id += 1 + split_map[node] = split_id + + new_gm = split_module( + gm, + gm, + lambda node: split_map[node], + keep_original_order=True, + keep_original_node_name=True, + ) + # Keep the codegen from original graph module to preserve e.g. pytree info. + new_gm.graph._codegen = gm.graph._codegen + new_gm.recompile() + return new_gm + + +def nodes_filter(nodes: list[torch.fx.Node], node_call_back) -> list[torch.fx.Node]: + """Returns the nodes that match the node_call_back as a list.""" + return [node for node in nodes if node_call_back(node)] + + +@contextmanager +def _disable_aten_to_metadata_assertions(): + global _DISABLE_ATEN_TO_ASSERTION_PASS + orig_val = _DISABLE_ATEN_TO_ASSERTION_PASS + _DISABLE_ATEN_TO_ASSERTION_PASS = True + try: + yield + finally: + _DISABLE_ATEN_TO_ASSERTION_PASS = orig_val + + +def _insert_aten_to_metadata_assert_pass(gm: torch.fx.GraphModule) -> None: + from torch._export.passes._node_metadata_hook import ( + _node_metadata_hook, + _set_node_metadata_hook, + ) + + if _DISABLE_ATEN_TO_ASSERTION_PASS: + return + + aten_to_variants = [ + torch.ops.aten.to.device, + torch.ops.aten.to.dtype, + torch.ops.aten.to.dtype_layout, + ] + for node in gm.graph.nodes: + if node.target in aten_to_variants: + if ( + node.prev.target is torch.ops.aten._assert_tensor_metadata.default + and node.args[0] == node.prev.args[0] + ): + # skip if already guarded + continue + + if (tensor_val := node.args[0].meta.get("val")) is not None: + with ( + gm.graph.inserting_before(node), + _set_node_metadata_hook( + gm, + functools.partial( + _node_metadata_hook, + metadata={ + "stack_trace": node.meta.get("stack_trace"), + "nn_module_stack": node.meta.get("nn_module_stack"), + }, + ), + ), + ): + gm.graph.call_function( + torch.ops.aten._assert_tensor_metadata.default, + args=(node.args[0],), + kwargs={ + "dtype": tensor_val.dtype, + "device": tensor_val.device, + "layout": tensor_val.layout, + }, + ) + + +def apply_runtime_assertion_pass(gm: torch.fx.GraphModule, graph_signature): + from torch._export.passes._node_metadata_hook import ( + _node_metadata_hook, + _set_node_metadata_hook, + ) + from torch._functorch._aot_autograd.input_output_analysis import _graph_output_names + + if not torch._dynamo.config.do_not_emit_runtime_asserts: + stack_trace = ( + 'File "torch/fx/passes/runtime_assert.py", line 24, ' + "in insert_deferred_runtime_asserts" + ) + with _set_node_metadata_hook( + gm, + functools.partial( + _node_metadata_hook, metadata={"stack_trace": stack_trace} + ), + ): + shape_env = _get_shape_env_from_gm(gm) + if shape_env: + insert_deferred_runtime_asserts( + gm, + shape_env, + f"exported program: {first_call_function_nn_module_stack(gm.graph)}", + export=True, + ) + + # insert runtime assertions for aten.to nodes + _insert_aten_to_metadata_assert_pass(gm) + + # update output specs + gm.recompile() + graph_signature.user_outputs = _graph_output_names(gm) + return gm, graph_signature + + +def nodes_first( + nodes: list[torch.fx.Node], node_call_back=None +) -> torch.fx.Node | None: + """ + Returns the first node that matches the node_call_back. If no node matches, returns None. + When node_call_back is None, returns the first node in the node list. + """ + ret = nodes_filter(nodes, node_call_back if node_call_back else lambda node: True) + if len(ret) > 0: + return ret[0] + return None + + +def nodes_count(nodes: list[torch.fx.Node], node_call_back) -> int: + """Returns the number of nodes that match the node_call_back.""" + return len(nodes_filter(nodes, node_call_back)) + + +def nodes_map(nodes: list[torch.fx.Node], node_call_back) -> list[torch.fx.Node]: + """ + Sequentially visit the nodes list and invoke node_call_back on each element. + Returns the nodes list after the node_call_back is invoked on each element. + """ + for node in nodes: + node_call_back(node) + return nodes + + +def node_replace_(old_node: torch.fx.Node, new_node: torch.fx.Node) -> None: + """ + Replace all uses of old_node with new_node. + """ + old_node.replace_all_uses_with(new_node) + old_node.users.clear() + old_node.graph.erase_node(old_node) + + +def _update_gm_meta_if_possible(gm: torch.fx.GraphModule, mod: torch.nn.Module) -> None: + if ( + isinstance(mod, torch.fx.GraphModule) + and hasattr(mod, "meta") + and "custom" in mod.meta + ): + gm.meta.update({"custom": mod.meta["custom"]}) + + +def node_inline_(call_mod_node: torch.fx.Node) -> torch.fx.GraphModule | None: + """ + Inline the submodule of the given node into the parent module. + Note: we only support the case where submodule takes tensors inputs. + """ + if call_mod_node.op != "call_module": + raise AssertionError(f"expected call_module op, got {call_mod_node.op}") + gm = call_mod_node.graph.owning_module + if gm is None: + raise AssertionError("owning_module should not be None") + + if not isinstance(call_mod_node.target, str): + raise AssertionError( + f"expected target to be str, got {type(call_mod_node.target).__name__}" + ) + sub_gm = getattr(gm, call_mod_node.target) + + phs = (node for node in sub_gm.graph.nodes if node.op == "placeholder") + body = ( + node for node in sub_gm.graph.nodes if node.op not in ("placeholder", "output") + ) + output = [node for node in sub_gm.graph.nodes if node.op == "output"] + + for ph, arg in zip(phs, call_mod_node.args): + if not isinstance(arg, torch.fx.Node): + raise AssertionError(f"expected fx.Node, got {type(arg)}") + node_replace_(ph, arg) + + with gm.graph.inserting_before(call_mod_node): + for node in body: + new_node = gm.graph.node_copy(node) + if node.op == "get_attr": + new_target_name = new_node.target + if hasattr(gm, new_target_name): + # Loop through and find the "submod_{i}" that have no name collision + i = 1 + new_target_name = f"submod_{i}" + while hasattr(gm, new_target_name): + i += 1 + new_target_name = f"submod_{i}" + new_node.target = new_target_name + setattr(gm, new_node.target, getattr(sub_gm, node.target)) + node_replace_(node, new_node) + + if len(output) > 0: + if len(output) != 1 or len(output[0].args) != 1: + raise AssertionError( + f"expected exactly 1 output with 1 arg, got {len(output)} outputs" + ) + new_output = output[0].args[0] + + if isinstance(new_output, torch.fx.Node): + # Clear the users of the output node and set + # the users to be the users of original call_module node. + new_output.users.clear() + node_replace_(call_mod_node, new_output) + elif isinstance(new_output, (list, tuple)): + # Pop subgraph output node from users. + for node in new_output: + node.users.pop(output[0]) + + # Inline the get_item calls for the output node. + get_item_users = nodes_filter( + list(call_mod_node.users.keys()), + lambda node: node.op == "call_function" + and node.target is operator.getitem, + ) + # get_item_node.args[1] is the idx referring to new_output[idx] + nodes_map( + get_item_users, + lambda get_item_node: node_replace_( + get_item_node, + new_output[get_item_node.args[1]], + ), + ) + call_mod_node.graph.erase_node(call_mod_node) + else: + raise NotImplementedError( + f"Unsupported output type {type(new_output)}. Expect it to be a Node or a list/tuple of Nodes." + ) + else: + call_mod_node.graph.erase_node(call_mod_node) + + gm.delete_all_unused_submodules() + gm.recompile() + return gm + + +def _get_torch_jit_trace_forward_signature(mod: torch.nn.Module) -> inspect.Signature: + """ + Get source code and parse argument names using AST. The function returns + a signature of the forward() function. + + # TODO: Directly provide inspect.signature compatible TS-d module. + """ + ast_mod = ast.parse(mod.code) # type: ignore[call-overload] + ast_func_def: ast.FunctionDef = ast_mod.body[0] + + # FIXME(jiashenc): TorchScript should only allow positional or keywords arguments. + arg_type_map = {"args": Parameter.POSITIONAL_OR_KEYWORD} + + # Traverse all argument types in AST tree and create associated parameters. + param_list = [] + for arg_type, param_type in arg_type_map.items(): + arg_name_list = [a.arg for a in getattr(ast_func_def.args, arg_type)] + for arg_name in arg_name_list: + if arg_name == "self": + continue # Skip self argument. + param_list.append(inspect.Parameter(arg_name, param_type)) + + return inspect.Signature(parameters=param_list) + + +def _bind_signature_to_inputs(mod, fake_args, fake_kwargs): + if isinstance(mod, (torch.jit.ScriptModule, torch.jit.TracedModule)): + sig = _get_torch_jit_trace_forward_signature(mod) + + # Sanity check for placeholder names coming from TorchScript. + if len(sig.parameters) != len(fake_args) + len(fake_kwargs): + raise AssertionError( + "Arguments other than POSITIONAL_OR_KEYWORD kinds in forward() " + "are not supported in _get_torch_jit_trace_forward_signature" + ) + else: + sig = inspect.signature(mod.forward) + + # Rather than binding both fake_args and fake_kwargs to sig names, we + # (partially) bind only fake_args, while reusing fake_kwarg names. This + # ensures that fake_kwargs do not get reordered, which is important to + # match flattened user inputs. + return {**sig.bind_partial(*fake_args).arguments, **fake_kwargs} + + +def _build_cache(name, find_available, used_names): + used_names.add(name) + match = re.match(r"(.*)_(\d+)", name) + if match: + prefix, n = match.group(1), match.group(2) + if int(n) > find_available[prefix]: + find_available[prefix] = int(n) + + +def _name_hoo_subgraph_placeholders(gm: torch.fx.GraphModule) -> None: + """ + Propagate placeholder names from the top-level graph into HigherOrderOp subgraphs, + and handle collisions with non-placeholders by count suffixing. + Different HOO subgraph types have different input schemas, so we first enumerate them + and gather the top-level named placeholder nodes. + """ + + # gather all HOO subgraphs and their top-level named placeholder nodes + subgraph_ph_tuples: list[tuple[torch.fx.GraphModule, list[torch.fx.Node]]] = [] + for node in gm.graph.nodes: + if node.op == "call_function" and isinstance( + node.target, torch._ops.HigherOrderOperator + ): + # HOO subgraphs have varying input schemas, so we enumerate them there + if node.target._name == "cond": + _, true_graph, false_graph, cond_args = node._args + subgraph_ph_tuples.append((getattr(gm, true_graph.target), cond_args)) + subgraph_ph_tuples.append((getattr(gm, false_graph.target), cond_args)) + elif node.target._name == "wrap_with_set_grad_enabled": + subgraph, phs = node._args[1], node._args[2:] + subgraph_ph_tuples.append((getattr(gm, subgraph.target), phs)) + elif node.target._name == "map_impl": + body_graph, array, args = node._args + subgraph_ph_tuples.append( + (getattr(gm, body_graph.target), array + args) + ) + + # propagate names + for subgraph, hoo_phs in subgraph_ph_tuples: + name_map: dict[str, str] = {} + find_available: dict[str, int] = defaultdict(int) + used_names: set[str] = set() + for i, node in enumerate(subgraph.graph.nodes): + if i < len(hoo_phs): # placeholder, retain name + name_map[node.name] = hoo_phs[i].name + node.name = node.target = hoo_phs[i].name + _build_cache(node.name, find_available, used_names) + else: # non-placeholder, check for collisions + node.name = _rename_without_collisions( + name_map, find_available, used_names, node.name, node.name + ) + + # recurse and recompile + _name_hoo_subgraph_placeholders(subgraph) + subgraph.recompile() + + +def _assign_new_node_names( + gm: torch.fx.GraphModule, + name_map: dict[str, str], + custom_meta: dict[str, Any], +) -> None: + """ + Assign new names to all nodes, in the graph module, from name map. + """ + for node in gm.graph.nodes: + if node.op == "placeholder": + if node.name not in name_map: + raise AssertionError(f"placeholder node {node.name!r} not in name_map") + node.name = node.target = name_map[node.name] + if node.name in custom_meta: + if node.meta.get("custom") is None: + node.meta["custom"] = {} + else: + # Assert if any existing key has different value + for k, v in node.meta["custom"].items(): + if ( + k in custom_meta[node.name] + and v != custom_meta[node.name][k] + ): + raise AssertionError( + f"Mismatch in custom metadata for key {k}. Value in " + f"node.meta is {v} and value in custom_meta is {custom_meta[node.name][k]}." + ) + node.meta["custom"].update(custom_meta[node.name]) + # if the constant obj is an input, we also need to update meta["val"] + # because this is created before the placeholder naming pass + if isinstance(node.meta["val"], CustomObjArgument): + node.meta["val"].name = node.name + elif node.name in name_map: + node.name = name_map[node.name] + + +def placeholder_naming_pass( + gm: torch.fx.GraphModule, + export_graph_signature: "ExportGraphSignature", + mod: torch.nn.Module, + fake_args, + fake_kwargs, + fake_params_buffers, + constants: dict[str, Any], +) -> None: + """ + This pass is run at the end of _export_non_strict() to assign better placeholder node names: + - User inputs: + These follow the signature of mod.forward(), e.g. forward(x, y) produces nodes x, y. + For nested inputs from dictionaries, lists, tuples, or dataclasses, + the names are a concatenation of the path to the tensor. + e.g. x = { + 'a': torch.randn(), + 'b': [torch.randn(), torch.randn()] + } + produces nodes x_a, x_b_0, x_b_1. + - Parameters/buffers/constants/custom objects: + These follow the FQN of the object, prefixed by "p", "b", "c", "obj" respectively. + e.g. self.bar.l0.weight produces "p_bar_l0_weight". + - Effect tokens: + These are named token, token_1, ... + """ + + custom_meta: dict[str, Any] = {} + if isinstance(mod, torch.fx.GraphModule): + for node in mod.graph.nodes: + if "custom" in node.meta: + custom_meta[node.name] = node.meta["custom"] + + def _strip_name(x): + if x.startswith("L__self___"): + x = x[len("L__self___") :] + elif x.startswith("self_"): + x = x[len("self_") :] + x = re.sub(r"[^a-zA-Z0-9]", "_", x) + return x + + def _extract_pytree_key(x): + if isinstance(x, MappingKey): + x = re.sub(r"[^a-zA-Z0-9]", "_", str(x.key)) + return x + elif isinstance(x, SequenceKey): + return str(x.idx) + elif isinstance(x, GetAttrKey): + return x.name + else: + raise RuntimeError(f"Pytree key of type {type(x)} not handled for {x}") + + name_map: dict[str, str] = {} + find_available: dict[str, int] = defaultdict(int) + used_names: set[str] = set() + + # map user input names with mod.forward() signature + combined_args = _bind_signature_to_inputs(mod, fake_args, fake_kwargs) + + flat_args_with_path, _ = tree_flatten_with_path(combined_args) + user_input_names = [ + spec.arg.name + for spec in export_graph_signature.input_specs + if spec.kind == InputKind.USER_INPUT + ] + + # use pytree path to name nested user inputs + for (arg_path, _arg), user_input_name in zip(flat_args_with_path, user_input_names): + if user_input_name: + _rename_without_collisions( + name_map, + find_available, + used_names, + user_input_name, + placeholder_prefixes[InputKind.USER_INPUT] + + "_".join(_extract_pytree_key(x).lower() for x in arg_path), + is_placeholder=True, + ) + + # use graph signature input specs to map param/buffer/constant names + # name effect tokens as token, token_1, ... (these aren't visible to user) + for spec in export_graph_signature.input_specs: + if spec.kind == InputKind.USER_INPUT: + continue + if spec.kind == InputKind.TOKEN: + base_name = "" + else: + base_name = _strip_name(spec.target).lower() + base_name = re.sub(r"[^a-zA-Z0-9]", "_", base_name) + + _rename_without_collisions( + name_map, + find_available, + used_names, + spec.arg.name, + placeholder_prefixes[spec.kind] + base_name, + is_placeholder=True, + ) + if base_name in custom_meta: + # the keys in custom_meta are node names from `mod`, + # which is the base_name here. + # we need the re-mapped name for lookup later + custom_meta[name_map[spec.arg.name]] = custom_meta[base_name] + del custom_meta[base_name] + + # handle naming collisions with call_function/get_attr inputs. + # here, we want to prioritize user input names over call_function names + # e.g. not have forward(self, mul): lead to a placeholder node called mul_13, + # so we increment the suffix of call_function nodes as needed + for node in gm.graph.nodes: + if node.op == "placeholder": + continue + _rename_without_collisions( + name_map, find_available, used_names, node.name, node.name + ) + + # assign new node names + _assign_new_node_names(gm, name_map, custom_meta) + + # propagate names to higher order op subgraphs + _name_hoo_subgraph_placeholders(gm) + + # re-generate graph module code + gm.recompile() + + # modify graph signature (input specs, output specs, user input mutations) + for spec in export_graph_signature.input_specs: + if spec.arg.name not in name_map: + raise AssertionError(f"input spec arg {spec.arg.name!r} not in name_map") + spec.arg.name = name_map[spec.arg.name] + if ( # handle targets for custom objects + spec.kind == InputKind.CUSTOM_OBJ and spec.target in name_map + ): + # pyrefly: ignore [bad-index, index-error] + spec.target = name_map[spec.target][4:] # strip obj_ prefix + + for spec in export_graph_signature.output_specs: + if spec.arg.name in name_map: + spec.arg.name = name_map[spec.arg.name] + if spec.kind == OutputKind.USER_INPUT_MUTATION and spec.target in name_map: + # pyrefly: ignore [bad-index, index-error] + spec.target = name_map[spec.target] + + # rename keys in constants dict for custom objects + for name in list(constants.keys()): + constant = constants[name] + if name in name_map and not isinstance( + constant, torch.Tensor + ): # rename custom objects with generic names + new_name = name_map[name] + if ( + new_name != name + and re.match(r"arg(\d+)_1", name) + and new_name != placeholder_prefixes[InputKind.CUSTOM_OBJ] + name + ): + constants[new_name] = constant + del constants[name] + + +def remove_proxy_from_state_dict(state_dict: dict, in_place: bool) -> dict: + """ + If `in_place` is false, return a new copy of `state_dict` with "proxy" removed from `v.__dict__`. + `v` is the values in the dictionary. + If `in_place` is true, modify `state_dict` in place. + """ + if in_place: + for k, v in state_dict.items(): + if hasattr(v, "proxy"): + delattr(state_dict[k], "proxy") + return state_dict + else: + new_state_dict = {} + for k, v in state_dict.items(): + if hasattr(v, "proxy"): + new_state_dict[k] = v.detach().clone() + else: + new_state_dict[k] = v + return new_state_dict + + +def _detect_fake_mode_from_gm( + gm: torch.fx.GraphModule, +) -> torch._subclasses.fake_tensor.FakeTensorMode | None: + """ + For a given graph module, we look at the "val" of placeholder nodes to find the fake inputs. + Additionally, if gm doesn't have placeholders, we further look at the "example_value" or "val" of other nodes. + If no fake mode is found, we return None for fake_mode. + """ + + fake_inps: list[torch.Tensor] = [] + fake_vals: list[torch.Tensor] = [] + for node in gm.graph.nodes: + if node.op == "placeholder" and "val" in node.meta: + fake_val = node.meta["val"] + if fake_val is not None and isinstance(fake_val, torch.Tensor): + fake_inps.append(fake_val) + elif len(fake_inps) == 0 and ( + "example_value" in node.meta or "val" in node.meta + ): + fake_val = None + if "example_value" in node.meta: + fake_val = node.meta["example_value"] + elif "val" in node.meta: + fake_val = node.meta["val"] + if fake_val is not None and isinstance(fake_val, torch.Tensor): + fake_vals.append(fake_val) + + return detect_fake_mode(fake_inps + fake_vals) + + +@contextmanager +def _disable_load_state_dict_hooks(mod: torch.nn.Module): + state_dict_hooks: dict[int, Callable] = dict(mod._state_dict_hooks) + state_dict_pre_hooks: dict[int, Callable] = dict(mod._state_dict_pre_hooks) + mod._state_dict_hooks.clear() + mod._state_dict_pre_hooks.clear() + try: + yield + finally: + mod._state_dict_hooks = state_dict_hooks + mod._state_dict_pre_hooks = state_dict_pre_hooks + + +def _is_cia_op(op: "OperatorBase") -> bool: + return ( + torch._C._dispatch_has_kernel_for_dispatch_key( + op.name(), torch._C.DispatchKey.CompositeImplicitAutograd + ) + or torch._C.DispatchKey.CompositeImplicitAutograd in op.py_kernels + ) + + +def _is_preservable_cia_op(op: "OperatorBase") -> bool: + return _check_valid_to_preserve(op) and _is_cia_op(op) + + +def _is_aten_op(op: "OperatorBase") -> bool: + return op.name().split("::")[0] == "aten" + + +def _is_custom_op(op: "OperatorBase") -> bool: + return not _is_aten_op(op) + + +# We can't cache this because custom op registry API in python can still +# add entries to the C++ dispatcher. +def _materialize_cpp_cia_ops() -> None: + """ + Utility function to query C++ dispatcher to get the all + possible CIA ops and populate them into torch.ops namespace + """ + cia_ops = torch._C._dispatch_get_registrations_for_dispatch_key( + "CompositeImplicitAutograd" + ) + + # Materialize all CIA ops + for op in cia_ops: + namespace, op_name = tuple(op.split("::")) + split_list = op_name.split(".") + # Sometime overload could be missing + if len(split_list) not in (1, 2): + raise AssertionError(f"expected 1 or 2 parts, got {len(split_list)}") + op_name = split_list[0] + op_overload_name = "default" + if len(split_list) == 2: + op_overload_name = split_list[1] + + _ = getattr(getattr(getattr(torch.ops, namespace), op_name), op_overload_name) + + +def _special_op_to_preserve_cia(*args, **kwargs): + """ + This is an special marker that tells our infra that we shouldn't decompose this op. + """ + return NotImplemented + + +# Our strategy for deciding if we can preserve a op is following: +# 1. The op should be known statically that it is functional +# 2. If it is maybe aliasing, we decompose because we must know if an op +# is mutating or aliasing. +def _check_valid_to_preserve(op_overload: "OperatorBase"): + from torch._decomp import _should_decompose_because_unsafe_op + + if _should_decompose_because_unsafe_op(op_overload): + return False + if op_overload in FunctionalTensor.metadata_fns: + return False + + if not hasattr(op_overload, "_schema"): + return False + + alias_info = len( + [i for i in op_overload._schema.arguments if i.alias_info is not None] + ) + + is_mutating_or_aliasing = alias_info != 0 or op_overload._schema.is_mutable + + if is_mutating_or_aliasing: + return False + + if not torch._C._dispatch_has_kernel(op_overload.name()): + return False + + return True + + +@functools.lru_cache(maxsize=1) +def _collect_all_valid_cia_ops_for_aten_namespace() -> set["OperatorBase"]: + return _collect_all_valid_cia_ops_for_namespace(torch.ops.aten) + + +def _collect_all_valid_cia_ops_for_namespace( + op_namespace: torch._ops._OpNamespace, +) -> set["OperatorBase"]: + # Step 1: Materialize all ops from C++ dispatcher + _materialize_cpp_cia_ops() + + # Step 2: Query all ops from python dispatcher + cia_ops = set() + for op in op_namespace: + op_packet = getattr(op_namespace, op) + for overload in op_packet.overloads(): + op_overload = getattr(op_packet, overload) + if _is_preservable_cia_op(op_overload): + cia_ops.add(op_overload) + return cia_ops + + +def _collect_all_valid_cia_ops() -> set["OperatorBase"]: + """ + This is an util function that gets the all CIA functional ops. + + The algorithm is in 2 steps: + 1. We first query C++ dispatcher to get the list of CIA ops + and then we call getattr on torch.ops.aten to lazily populate + them. + + 2. Sometimes, handful of ops have CIA registered in python dispatcher + but not on the C++ side, these can't be caught at the first step. + So we walk again to get the final list. + + Note that the output of this function should never be modified + """ + cia_ops = set() + for op_namespace_name in torch.ops._dir: + # The reason we split here is because aten ops are safe to cache. + if op_namespace_name != "aten": + if not hasattr(torch.ops, op_namespace_name): + raise AssertionError( + f"torch.ops does not have attribute {op_namespace_name!r}" + ) + op_namespace = getattr(torch.ops, op_namespace_name) + if isinstance(op_namespace, torch._ops._OpNamespace): + cia_ops |= _collect_all_valid_cia_ops_for_namespace(op_namespace) + else: + cia_ops |= _collect_all_valid_cia_ops_for_aten_namespace() + return cia_ops + + +def _get_decomp_for_cia(op: "OperatorBase"): + # [NOTE] Separating out func.decompose + # Ideally we should be able to just register func.decompose but + # we can't as this decomp is gonna be registered to the py_impl. + # As a result it will infinitely recurse. So we first check if the op + # has py_impl entry for CIA and if it is we use that first. If not, + # we register C++ query to py_impl. + dk = torch._C.DispatchKey.CompositeImplicitAutograd + if dk in op.py_kernels and not isinstance(op.py_kernels[dk], torch._C.DispatchKey): + return op.py_kernels[dk] + + def _special_op_to_decompose_cia(*args, **kwargs): + kernel = kwargs["kernel"] + del kwargs["kernel"] + # Can't call kernel.decompose due to infinite recursion as + # we register this kernel to py_impl directly + dk = torch._C.DispatchKey.CompositeImplicitAutograd + if torch._C._dispatch_has_kernel_for_dispatch_key( + kernel.name(), torch._C.DispatchKey.CompositeImplicitAutograd + ): + return kernel._op_dk(dk, *args, **kwargs) + else: + raise AssertionError( + f"Expected {kernel} to have CompositeImplicitAutograd kernel" + ) + + return functools.partial(_special_op_to_decompose_cia, kernel=op) + + +@contextmanager +def _compiling_state_context(): + old_compiling_flag = torch.compiler._is_compiling_flag + old_exporting_flag = torch.compiler._is_exporting_flag + try: + torch.compiler._is_compiling_flag = True + torch.compiler._is_exporting_flag = True + yield + finally: + torch.compiler._is_compiling_flag = old_compiling_flag + torch.compiler._is_exporting_flag = old_exporting_flag + + +def _fakify_params_buffers( + fake_mode: FakeTensorMode, + mod: torch.nn.Module, +) -> dict[str, torch.Tensor | torch.nn.Parameter]: + params_buffers = { + **dict(mod.named_parameters(remove_duplicate=False)), + **dict(mod.named_buffers(remove_duplicate=False)), + } + + faked_params_buffers = {} + memo: dict[int, FakeTensor] = {} + for key, value in params_buffers.items(): + if id(value) in memo: + fake_tensor = memo[id(value)] + else: + fake_tensor = fake_mode.from_tensor(value, static_shapes=True) + memo[id(value)] = fake_tensor + faked_params_buffers[key] = fake_tensor + return faked_params_buffers # type: ignore[return-value] + + +def register_module_as_pytree_input_node(cls: type[torch.nn.Module]) -> None: + """ + Registers a module as a valid input type for :func:`torch.export.export`. + + Args: + mod: the module instance + serialized_type_name: The serialized name for the module. This is + required if you want to serialize the pytree TreeSpec containing this + module. + + Example:: + + import torch + + + class Module(torch.nn.Module): + def __init__(self): + super().__init__() + self.linear = torch.nn.Linear(3, 3) + + def forward(self, x): + return self.linear(x) + + + torch._export.utils.register_module_as_pytree_node(InputDataClass) + + + class Mod(torch.nn.Module): + def forward(self, x, m): + return m(x) + x + + + ep = torch.export.export(Mod(), (torch.randn(3), Module())) + print(ep) + + """ + if not issubclass(cls, torch.nn.Module): + raise AssertionError(f"expected nn.Module subclass, got {cls}") + + import weakref + + class PrototypeModule(weakref.ref): + def __init__(self, m, *args, **kwargs): + super().__init__(m, *args, **kwargs) # type: ignore[call-arg] + if not isinstance(m, torch.nn.Module): + raise AssertionError(f"expected nn.Module, got {type(m).__name__}") + if hasattr(self, "_proto_cls"): + raise AssertionError("_proto_cls should not be set") + self._proto_cls = cls + + def __eq__(self, other): + return self._proto_cls == other._proto_cls + + def __deepcopy__(self, memo): + return PrototypeModule(self()) + + def default_flatten_fn(obj: Any) -> tuple[list[Any], Context]: + named_parameters = dict(obj.named_parameters()) + named_buffers = dict(obj.named_buffers()) + params_buffers = {**named_parameters, **named_buffers} + return list(params_buffers.values()), [ + list(params_buffers.keys()), + PrototypeModule(obj), + ] + + def default_unflatten_fn(values: Iterable[Any], context: Context) -> Any: + flat_names, ref = context + if ref is None or ref() is None: + raise RuntimeError("Module has been garbage collected") + obj = ref() + if flatten_fn is None: + raise AssertionError("flatten_fn should not be None") + flattened, _ = flatten_fn(obj) + + # NOTE: This helper function will replicate an nn.Module in the exactly same + # structure to be used together with _reparameterize_module. This will + # create a clone of the module with the new parameters and buffers without + # affecting the original module. + def copy_module(mod: torch.nn.Module): + ret = copy.copy(mod) + ret.__dict__ = {copy.copy(k): copy.copy(v) for k, v in mod.__dict__.items()} + for name, child in ret.named_children(): + setattr(ret, name, copy_module(child)) + return ret + + if any(v is not o for v, o in zip(values, flattened)): + with torch.nn.utils.stateless._reparametrize_module( + obj, dict(zip(flat_names, values)), tie_weights=True, strict=True + ): + ret = copy_module(obj) + else: + ret = obj + return ret + + def default_flatten_fn_with_keys(obj: Any) -> tuple[list[Any], Context]: + flattened, [flat_names, *args] = flatten_fn(obj) # type: ignore[misc] + return [(MappingKey(k), v) for k, v in zip(flat_names, flattened)], [ + flat_names, + *args, + ] + + flatten_fn = default_flatten_fn + unflatten_fn = default_unflatten_fn + + serialized_type_name = cls.__module__ + "." + cls.__qualname__ + + def to_dumpable_context(context): + keys, *_ = context + return json.dumps([keys, *([None] * len(_))]) + + def from_dumpable_context(dumpable): + s = json.loads(dumpable) + s[1] = PrototypeModule(torch.nn.Module()) + return s + + _register_pytree_node( + cls, + flatten_fn, + unflatten_fn, + serialized_type_name=serialized_type_name, + flatten_with_keys_fn=default_flatten_fn_with_keys, + to_dumpable_context=to_dumpable_context, + from_dumpable_context=from_dumpable_context, + ) + + def default_flatten_fn_spec(obj, spec) -> list[Any]: + flats, context = flatten_fn(obj) + if context != spec.context: + raise AssertionError(f"context mismatch: {context} != {spec.context}") + return flats + + register_pytree_flatten_spec( + cls, + default_flatten_fn_spec, + ) + + +def deregister_module_as_pytree_input_node(cls: type[torch.nn.Module]) -> None: + _deregister_pytree_node(cls) + _deregister_pytree_flatten_spec(cls) + + +def _sync_state(src, dst): + if not isinstance(src, torch.nn.Module): + raise AssertionError(f"Expected {src} to be a nn.Module") + if not isinstance(dst, torch.nn.Module): + raise AssertionError(f"Expected {dst} to be a nn.Module") + # Share state (params, buffers) between modules. + # This ensures that state mutations are visible across them. + # Since tensor constants are not mutable, copying (without sharing) is OK. + # Also, primitive constants are specialized, so copying (without sharing) is OK. + dst._parameters = src._parameters + dst._buffers = src._buffers + + +def sync_state(*wrapped_method_modules): + """ + Sync state between exported modules corresponding to wrapped methods. + This might be necessary after serializing/deserializing due to copying. + """ + if wrapped_method_modules: + m, *other_ms = wrapped_method_modules + for other_m in other_ms: + _sync_state(m, other_m) + + +class _WrappedMethod(torch.nn.Module): + def __init__(self, method): + super().__init__() + # share state of method's self module + _sync_state(method.__self__, self) + # redirect forward to method + self.forward = method + + +def wrap_method(method): + """ + Wrap a method as a module so that it can be exported. + The wrapped module's forward points to the method, and + the method's original module state is shared. + """ + if not ismethod(method): + raise AssertionError(f"Expected {method} to be a method") + return _WrappedMethod(method) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/verifier.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/verifier.py new file mode 100644 index 0000000000000000000000000000000000000000..9dbf6dc3a5c3c8fbd4c57c5ee04d93e1c118763e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/verifier.py @@ -0,0 +1,559 @@ +# mypy: allow-untyped-defs +import inspect +import math +import operator +from collections.abc import Iterable +from typing import Any, final, TYPE_CHECKING + +import torch +from torch._library.opaque_object import is_opaque_type +from torch._ops import HigherOrderOperator, OpOverload +from torch._subclasses.fake_tensor import FakeTensor +from torch.export.graph_signature import ( + CustomObjArgument, + InputKind, + SymBoolArgument, + SymFloatArgument, + SymIntArgument, + TensorArgument, + TokenArgument, +) +from torch.fx import GraphModule + + +if TYPE_CHECKING: + from torch.export.exported_program import ExportedProgram + + +class SpecViolationError(Exception): + pass + + +def is_functional(op: OpOverload) -> bool: + return not op._schema.is_mutable + + +def _check_has_fake_tensor(node: torch.fx.Node) -> None: + # TODO(angelayi): remove this in favor of _check_val + return _check_val(node) + + +def _check_val(node: torch.fx.Node) -> None: + from torch.fx.experimental.symbolic_shapes import SymBool, SymFloat, SymInt + + def _check_correct_val(val): + if val is None: + return True + elif isinstance(val, (int, bool, str, float)): + return True + elif isinstance( + val, (torch.memory_format, torch.dtype, torch.device, torch.layout) + ): + return True + elif isinstance( + val, (FakeTensor, torch.Tensor) + ): # TODO(zhxchen17) Remove Tensor. + return True + elif isinstance(val, (SymInt, SymFloat, SymBool)): + return True + elif isinstance(val, CustomObjArgument): + return True + elif isinstance(val, Iterable): + return all(_check_correct_val(x) for x in val) + elif is_opaque_type(type(val)): + return True + return False + + def _no_returns(op): + if not isinstance(op, OpOverload): + return False + return len(op._schema.returns) == 0 + + if "val" not in node.meta: + if node.op == "call_function" and _no_returns(node.target): + return + raise SpecViolationError(f"Node.meta {node.name} is missing val field.") + + val = node.meta["val"] + if not _check_correct_val(val): + raise SpecViolationError(f"Node.meta {node.name} has invalid val field {val}") + + +def _check_torch_fn(node: torch.fx.Node) -> None: + torch_fn = node.meta.get("torch_fn") + if torch_fn is None: + raise SpecViolationError( + f"Unable to find torch_fn metadata for node {node.name}" + ) + if ( + not isinstance(torch_fn, tuple) + and isinstance(torch_fn[0], str) + and isinstance(torch_fn[1], str) + ): + raise SpecViolationError( + f"Node.meta {node.name} has invalid torch_fn field {torch_fn}" + ) + + +class _VerifierMeta(type): + _registry: dict[str, type["Verifier"]] = {} + + def __new__(metacls, name, bases, attrs): + if bases: + if "check" in attrs or "_check_graph_module" in attrs: + raise SyntaxError("Overriding method check is not allowed.") + if "dialect" not in attrs or attrs["dialect"] == "ATEN": + raise AssertionError( + f"subclass must define dialect != 'ATEN', got {attrs.get('dialect')}" + ) + else: + if "check" not in attrs: + raise AssertionError("base class must define 'check' method") + if "_check_graph_module" not in attrs: + raise AssertionError( + "base class must define '_check_graph_module' method" + ) + if attrs["dialect"] != "ATEN": + raise AssertionError( + f"base class dialect must be 'ATEN', got {attrs['dialect']}" + ) + + if not isinstance(attrs["dialect"], str): + raise AssertionError(f"dialect must be str, got {type(attrs['dialect'])}") + ret = type.__new__(metacls, name, bases, attrs) + metacls._registry[attrs["dialect"]] = ret # type: ignore[assignment] + return ret + + +def getattr_recursive(obj: Any, target: str) -> Any: + target_atoms = target.split(".") + attr_itr = obj + for i, atom in enumerate(target_atoms): + if not hasattr(attr_itr, atom): + raise RuntimeError( + f"Node referenced nonexistent target {'.'.join(target_atoms[:i])}" + ) + attr_itr = getattr(attr_itr, atom) + return attr_itr + + +class Verifier(metaclass=_VerifierMeta): + dialect = "ATEN" + + def allowed_builtin_ops(self) -> list: + return [ + operator.getitem, + operator.add, + operator.mul, + operator.sub, + operator.truediv, + operator.ge, + operator.le, + operator.gt, + operator.lt, + operator.eq, + operator.ne, + operator.floordiv, + operator.mod, + operator.and_, + operator.or_, + operator.not_, + operator.pow, + operator.neg, + operator.abs, + operator.lshift, + operator.rshift, + math.ceil, + math.floor, + math.trunc, + round, + ] + + def allowed_op_types(self) -> tuple[type[Any], ...]: + return (OpOverload, HigherOrderOperator) + + def allowed_getattr_types(self) -> tuple[type[Any], ...]: + return (torch.fx.GraphModule, torch.utils._pytree.TreeSpec) + + def allowed_getattr_types_for_subgm(self) -> tuple[type[Any], ...]: + # subgm in HOP's argument could has have getattr(weight) nodes, thus stateful + return ( + torch.fx.GraphModule, + torch.nn.parameter.Parameter, + torch.Tensor, # for buffer and constant tensor + torch.utils._pytree.TreeSpec, + ) + + def check_valid_op(self, op): + pass + + def check_additional(self, gm: GraphModule) -> None: + """ + Additional checks that are specific to some dialects. + """ + + @final + def check(self, ep: "ExportedProgram") -> None: + self._check_graph_module(ep.graph_module) + _verify_exported_program_module_call_graph(ep) + _verify_exported_program_signature(ep) + + @final + def _check_graph_module(self, gm: torch.fx.GraphModule) -> None: + def _allowed_getattr_types(is_toplevel_gm) -> tuple[type[Any], ...]: + if is_toplevel_gm: + ret = self.allowed_getattr_types() + else: + ret = self.allowed_getattr_types_for_subgm() + if any(t is object for t in ret): + raise AssertionError("allowed_getattr_types must not contain 'object'") + return ret + + def _check_valid_op(op) -> None: + def _allowed_builtin_ops() -> list: + ret = self.allowed_builtin_ops() + if not all(inspect.isbuiltin(op) for op in ret): + raise AssertionError("allowed_builtin_ops must all be builtins") + return ret + + def _allowed_op_types() -> tuple[type[Any], ...]: + ret = self.allowed_op_types() + if any(t is object for t in ret): + raise AssertionError("allowed_op_types must not contain 'object'") + return ret + + # TODO Remove this allowlist. + _allowed_torch_functions = ( + torch.autograd.grad_mode.set_grad_enabled, + torch.sym_int, + torch.sym_float, + torch.sym_ite, + torch.sym_max, + torch.sym_min, + torch.sym_not, + torch.sym_sqrt, + torch.sym_sum, + torch.export.custom_ops._call_custom_autograd_function_in_pre_dispatch, + # TODO (tmanlaibaatar) + # Predispatch export is able to contain autograd ops. + # These will be modeled as HOO later + torch._C._set_grad_enabled, + torch.amp.autocast_mode._enter_autocast, + torch.amp.autocast_mode._exit_autocast, + torch.fx.experimental.symbolic_shapes.cast_symbool_to_symint_guardless, + torch._functorch.predispatch._add_batch_dim, + torch._functorch.predispatch._remove_batch_dim, + torch._functorch.predispatch._vmap_increment_nesting, + torch._functorch.predispatch._vmap_decrement_nesting, + torch._functorch.predispatch.lazy_load_decompositions, + ) + + if not isinstance(op, _allowed_op_types()): + if ( + op not in _allowed_builtin_ops() + and op not in _allowed_torch_functions + ): + raise SpecViolationError( + f"Operator '{op}' is not an allowed operator type: {_allowed_op_types()}\n" + f"Valid builtin ops: {_allowed_builtin_ops()}" + f"Valid torch functions: {_allowed_torch_functions}" + ) + + if isinstance(op, OpOverload): + # All ops functional + # TODO (tmanlaibaatar) more proper way is needed here + if self.dialect != "TRAINING" and not is_functional(op): + raise SpecViolationError(f"operator '{op}' is not functional") + self.check_valid_op(op) + + for mod in gm.modules(): + is_toplevel_gm = mod is gm + + if not isinstance(mod, torch.fx.GraphModule): + continue + + mod.graph.lint() + for node in mod.graph.nodes: + # TODO(T140410192): should have fake tensor for all dialects + if node.op in {"call_module", "call_method"}: + raise SpecViolationError( + f"call_module is not valid: got a class '{node.target}' ", + ) + + elif node.op == "call_function": + _check_val(node) + + _check_valid_op(node.target) + + elif node.op == "get_attr": + if not isinstance(node.target, str): + raise SpecViolationError( + f"Expected get_attr target to be string, but got {type(node.target)}" + ) + + attr = getattr_recursive(mod, node.target) + if isinstance(attr, torch.nn.Module): + + def _is_type(name, ty): + return isinstance(getattr(attr, name, None), ty) + + if type(attr).__name__ == "LoweredBackendModule": + if ( + _is_type("backend_id", str) + and hasattr(attr, "original_module") + and hasattr(attr, "module_name") + and getattr(attr, "backend_id", None) == "aoti" + ): + continue + if ( + _is_type("backend_id", str) + and _is_type("processed_bytes", bytes) + and _is_type("compile_specs", list) + and hasattr(attr, "original_module") + ): + continue + else: + backend_id = getattr(attr, "backend_id", None) + processed_bytes = getattr(attr, "processed_bytes", None) + compile_specs = getattr(attr, "compile_specs", None) + raise SpecViolationError( + f"Invalid get_attr type {type(attr)}. \n" + f"LoweredBackendModule fields: " + f"backend_id(str) : {type(backend_id)}, " + f"processed_bytes(bytes) : {type(processed_bytes)}, " + f"compile_specs(list) : {type(compile_specs)}" + ) + elif type(attr).__name__ == "AOTInductorEPModule": + continue + + elif type(attr).__name__ == "AOTInductorRunnerWrapper": + continue + + if not isinstance(attr, _allowed_getattr_types(is_toplevel_gm)): + raise SpecViolationError( + f"Invalid get_attr type {type(attr)} on target {node.target}. \n" + f"Valid get_attr types: {_allowed_getattr_types(is_toplevel_gm)}" + ) + + elif node.op == "placeholder": + _check_val(node) + # TODO(zhxchen17) + # elif node.op == "output": + # _check_flattened_outputs() + + self.check_additional(gm) + + +class TrainingIRVerifier(Verifier): + dialect = "TRAINING" + + +def _verify_exported_program_module_call_graph(exported_program) -> None: + module_call_graph = exported_program.module_call_graph + nodes = {node.name for node in exported_program.graph.nodes} + for entry in module_call_graph: + if entry.signature is not None: + for arg in entry.signature.inputs: + if arg.name and arg.name not in nodes: + raise SpecViolationError( + f"Input {arg.name} does not exist in the graph." + ) + for arg in entry.signature.outputs: + if arg.name and arg.name not in nodes: + raise SpecViolationError( + f"Output {arg.name} does not exist in the graph." + ) + + +def _verify_exported_program_signature(exported_program) -> None: + # Check ExportedProgram signature matches + gs = exported_program.graph_signature + + # Check every node in the signature exists in the graph + input_node_names = [ + node.name for node in exported_program.graph.nodes if node.op == "placeholder" + ] + + if len(input_node_names) != len(gs.input_specs): + input_spec_names = [ + spec.arg.name for spec in gs.input_specs if hasattr(spec.arg, "name") + ] + missing_in_specs = set(input_node_names) - set(input_spec_names) + missing_in_graph = set(input_spec_names) - set(input_node_names) + raise SpecViolationError( + f"Number of graph inputs ({len(input_node_names)}) " + f"does not match number of inputs in the graph signature ({len(gs.input_specs)})\n" + f"Placeholders missing input_specs: {missing_in_specs}\n" + f"Input_specs missing placeholders: {missing_in_graph}" + ) + + for input_spec, node in zip(gs.input_specs, input_node_names): + if isinstance( + input_spec.arg, + (TensorArgument, SymIntArgument, SymFloatArgument, SymBoolArgument), + ): + if input_spec.arg.name != node: + raise SpecViolationError( + f"Input spec name {input_spec.arg.name} does not match node name {node}" + ) + + if input_spec.kind == InputKind.USER_INPUT: + continue + + elif input_spec.kind == InputKind.PARAMETER: + if not isinstance(input_spec.arg, TensorArgument): + raise SpecViolationError( + f"Parameter {input_spec.name} is not a tensor argument. Found {input_spec.arg} instead." + ) + if input_spec.target is None: + raise SpecViolationError( + f"InputSpec for {input_spec.name} has no target." + ) + + param = input_spec.target + if param not in exported_program.state_dict: + raise SpecViolationError(f"Parameter {param} is not in the state dict.") + + if not isinstance(exported_program.state_dict[param], torch.nn.Parameter): + raise SpecViolationError( + f"State dict entry for parameter {param} is not an instance of torch.nn.Parameter." + ) + + elif input_spec.kind == InputKind.BUFFER: + if not isinstance(input_spec.arg, TensorArgument): + raise SpecViolationError( + f"Buffer {input_spec.name} is not a tensor argument. Found {input_spec.arg} instead." + ) + if input_spec.target is None: + raise SpecViolationError( + f"InputSpec for {input_spec.name} has no target." + ) + + buffer = input_spec.target + if input_spec.persistent is None: + raise SpecViolationError( + f"Buffer {buffer} is missing a persistence flag" + ) + + if ( + input_spec.persistent is True + and buffer not in exported_program.state_dict + ): + raise SpecViolationError(f"Buffer {buffer} is not in the state dict.") + + if input_spec.persistent is False and buffer in exported_program.state_dict: + raise SpecViolationError( + f"Non-persistent buffer {buffer} is in the state dict, it should not be." + ) + elif input_spec.kind == InputKind.CONSTANT_TENSOR: + if not isinstance(input_spec.arg, TensorArgument): + raise SpecViolationError( + f"Constant tensor {input_spec.name} is not a tensor argument. Found {input_spec.arg} instead." + ) + if input_spec.target is None: + raise SpecViolationError( + f"InputSpec for {input_spec.name} has no target." + ) + + tensor_const = input_spec.target + if tensor_const not in exported_program.constants: + raise SpecViolationError( + f"Constant tensor {tensor_const} is not in the constants dictionary." + ) + elif input_spec.kind == InputKind.CUSTOM_OBJ: + if not isinstance(input_spec.arg, CustomObjArgument): + raise SpecViolationError( + f"Custom object {input_spec.name} is not a custom object argument. Found {input_spec.arg} instead." + ) + if input_spec.target is None: + raise SpecViolationError( + f"InputSpec for {input_spec.name} has no target." + ) + + custom_obj = input_spec.target + if custom_obj not in exported_program.constants: + raise SpecViolationError( + f"Custom object {custom_obj} is not in the constants dictionary." + ) + elif input_spec.kind == InputKind.TOKEN: + if not isinstance(input_spec.arg, TokenArgument): + raise SpecViolationError( + f"Constant tensor {input_spec.name} is not a tensor argument. Found {input_spec.arg} instead." + ) + else: + raise SpecViolationError(f"Unknown InputKind {input_spec.kind}.") + + # Check outputs + output_node = list(exported_program.graph.nodes)[-1] + if output_node.op != "output": + raise AssertionError(f"last node must be output, got {output_node.op}") + output_nodes = [ + arg.name if isinstance(arg, torch.fx.Node) else arg + for arg in output_node.args[0] + ] + + if len(output_nodes) != len(gs.output_specs): + output_spec_names = [ + spec.arg.name if hasattr(spec.arg, "name") else str(spec.arg) + for spec in gs.output_specs + ] + missing_out_specs = set(output_nodes) - set(output_spec_names) + missing_out_graph = set(output_spec_names) - set(output_nodes) + raise SpecViolationError( + f"Number of output nodes {len(output_nodes)} is different " + f"Than the number of outputs specified by the graph signature: {len(gs.output_specs)}\n" + f"Nodes missing output_specs: {missing_out_specs}\n" + f"Output_specs missing nodes: {missing_out_graph}" + ) + + num_tokens = len(gs.output_tokens) + end = ( + len(gs.buffers_to_mutate) + + len(gs.parameters_to_mutate) + + len(gs.user_inputs_to_mutate) + + num_tokens + ) + mutate_nodes: list[str] = output_nodes[num_tokens:end] + user_output_nodes = output_nodes[end : end + len(gs.user_outputs)] + + for mutation_node in mutate_nodes: + if mutation_node in gs.buffers_to_mutate: + if gs.buffers_to_mutate[mutation_node] not in gs.buffers: + raise SpecViolationError( + f"Buffer output {mutation_node} does not point to a buffer that exists. \n" + f"Dict of buffers that are mutated, in order: {gs.buffers_to_mutate} \n" + f"Buffer nodes available: {gs.buffers} \n" + ) + elif mutation_node in gs.parameters_to_mutate: + if gs.parameters_to_mutate[mutation_node] not in gs.parameters: + raise SpecViolationError( + f"Parameter output {mutation_node} does not point to a parameter that exists. \n" + f"Dict of parameters that are mutated, in order: {gs.parameters_to_mutate} \n" + f"Parameter nodes available: {gs.parameters} \n" + ) + elif mutation_node in gs.user_inputs_to_mutate: + if gs.user_inputs_to_mutate[mutation_node] not in gs.user_inputs: + raise SpecViolationError( + f"User input output {mutation_node} does not point to a user input that exists. \n" + f"Dict of user inputs that are mutated, in order: {gs.user_inputs_to_mutate} \n" + f"User input nodes available: {gs.user_inputs} \n" + ) + else: + raise SpecViolationError( + f"Mutation node {mutation_node} is neither a buffer nor a user input. " + f"Buffers to mutate: {gs.buffers_to_mutate}, User inputs to mutate: {gs.user_inputs_to_mutate}" + ) + + for user_output_node, user_output_name in zip(user_output_nodes, gs.user_outputs): + if user_output_node != user_output_name: + raise SpecViolationError( + f"User output {user_output_node} is not in the correct " + "order or is not found in the " + f"exported program's user_output list: {gs.user_outputs}. " + ) + + +def load_verifier(dialect: str) -> type[Verifier]: + if dialect == "ATEN" or dialect == "": + return _VerifierMeta._registry.get(dialect, Verifier) + return _VerifierMeta._registry[dialect] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/wrappers.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/wrappers.py new file mode 100644 index 0000000000000000000000000000000000000000..671ce03560c6c6048d62bfd8bd1bd3631ac43cb8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_export/wrappers.py @@ -0,0 +1,354 @@ +# mypy: allow-untyped-defs +import inspect +from contextlib import contextmanager +from functools import wraps + +import torch +import torch._custom_ops +from torch._C import DispatchKey +from torch._export.utils import _maybe_find_pre_dispatch_tf_mode_for_export +from torch._higher_order_ops.flat_apply import ( + _ConstantFunction, + flat_apply, + to_graphable, +) +from torch._higher_order_ops.strict_mode import strict_mode +from torch._higher_order_ops.utils import autograd_not_implemented +from torch._ops import HigherOrderOperator +from torch._subclasses.fake_tensor import FakeTensorMode +from torch.fx.experimental.proxy_tensor import ( + PreDispatchTorchFunctionMode, + ProxyTorchDispatchMode, + track_tensor_tree, +) +from torch.utils import _pytree as pytree +from torch.utils._python_dispatch import is_traceable_wrapper_subclass_type + + +class ExportTracepoint(HigherOrderOperator): + def __init__(self): + super().__init__("_export_tracepoint") + + def __call__(self, *args, **kwargs): + # pyrefly: ignore [missing-attribute] + return super().__call__(*args, **kwargs) + + +_export_tracepoint = ExportTracepoint() + + +@_export_tracepoint.py_impl(ProxyTorchDispatchMode) +def export_tracepoint_dispatch_mode(mode, *args, **kwargs): + p_args, p_kwargs = pytree.tree_map(mode.tracer.unwrap_proxy, (args, kwargs)) + proxy = mode.tracer.create_proxy( + "call_function", _export_tracepoint, p_args, p_kwargs + ) + return track_tensor_tree(args, proxy, constant=None, tracer=mode.tracer) + + +@_export_tracepoint.py_impl(FakeTensorMode) +def export_tracepoint_fake_tensor_mode(mode, *args, **kwargs): + with mode: + return args + + +@_export_tracepoint.py_functionalize_impl +def export_tracepoint_functional(ctx, *args, **kwargs): + unwrapped_args = ctx.unwrap_tensors(args) + unwrapped_kwargs = ctx.unwrap_tensors(kwargs) + + with ctx.redispatch_to_next(): + _export_tracepoint(*unwrapped_args, **unwrapped_kwargs) + return args + + +_export_tracepoint.py_impl(DispatchKey.Autograd)( + autograd_not_implemented(_export_tracepoint, deferred_error=True) +) + + +@_export_tracepoint.py_impl(DispatchKey.CPU) +def export_tracepoint_cpu(*args, **kwargs): + return args + + +def _wrap_submodule(mod, path, module_call_specs): + if not isinstance(mod, torch.nn.Module): + raise AssertionError(f"expected torch.nn.Module, got {type(mod)}") + if path == "": + raise AssertionError("path must not be empty") + submodule = torch.fx.graph_module._get_attr(mod, path) + + def update_module_call_signatures(path, in_spec, out_spec): + if path in module_call_specs: + if module_call_specs[path]["in_spec"] != in_spec: + raise AssertionError( + f"in_spec mismatch for {path}: {module_call_specs[path]['in_spec']} != {in_spec}" + ) + if module_call_specs[path]["out_spec"] != out_spec: + raise AssertionError( + f"out_spec mismatch for {path}: {module_call_specs[path]['out_spec']} != {out_spec}" + ) + module_call_specs[path] = {"in_spec": in_spec, "out_spec": out_spec} + + def check_flattened(flat_args): + for a in flat_args: + if not (isinstance(a, (torch.Tensor, str, int, float, bool)) or a is None): + raise AssertionError( + f"Only Tensors or scalars are supported as pytree flattened inputs, got: {a}" + ) + + def pre_hook(module, args, kwargs): + flat_args, in_spec = pytree.tree_flatten((args, kwargs)) + check_flattened(flat_args) + flat_args = _export_tracepoint(*flat_args, kind="module_call_inputs", path=path) + args, kwargs = pytree.tree_unflatten(flat_args, in_spec) + return args, kwargs + + def post_hook(module, args, kwargs, res): + _, in_spec = pytree.tree_flatten((args, kwargs)) + flat_res, out_spec = pytree.tree_flatten(res) + check_flattened(flat_res) + flat_res = _export_tracepoint(*flat_res, kind="module_call_outputs", path=path) + update_module_call_signatures(path, in_spec, out_spec) + return pytree.tree_unflatten(flat_res, out_spec) + + pre_handle = submodule.register_forward_pre_hook(pre_hook, with_kwargs=True) + post_handle = submodule.register_forward_hook(post_hook, with_kwargs=True) + return pre_handle, post_handle + + +@contextmanager +def _wrap_submodules(f, preserve_signature, module_call_signatures): + handles = [] + + try: + for path in preserve_signature: + handles.extend(_wrap_submodule(f, path, module_call_signatures)) + yield + finally: + for handle in handles: + handle.remove() + + +def _mark_strict_experimental(cls): + def call(self, *args): + return strict_mode(self, args) + + cls.__call__ = call + return cls + + +def _register_func_spec_proxy_in_tracer(tracer, name, spec): + """ + This is a wrapper utility method on top of tracer to cache the + already registered subclass spec attribute. This is useful because + Subclass.__init__ will be same for each subclass. By default, fx will + create multiple attributes/proxies for given attribute. + """ + fx_name = name + "0" + if hasattr(tracer.root, fx_name): + if getattr(tracer.root, fx_name) != spec: + raise AssertionError(f"spec mismatch for {fx_name}") + return tracer.create_proxy("get_attr", fx_name, (), {}) + + qualname = tracer.get_fresh_qualname(name) + setattr(tracer.root, qualname, spec) + return tracer.create_proxy("get_attr", qualname, (), {}) + + +def _emit_flat_apply_call( + *, + tracer, + spec_name: str, + const_target_for_apply, + graphable_args, + track_value, + call_spec_cache_key: str, +): + # Flatten to graphable form and record the spec on the FX root + flat_args, in_spec = to_graphable(graphable_args) + qualname = tracer.get_fresh_qualname(spec_name) # type: ignore[union-attr] + setattr(tracer.root, qualname, in_spec) # type: ignore[union-attr] + spec_proxy = tracer.create_proxy("get_attr", qualname, (), {}) + + # Reuse/cached ConstantFunction spec on the root + _, func_spec = pytree.tree_flatten(_ConstantFunction(const_target_for_apply)) + func_spec_proxy = _register_func_spec_proxy_in_tracer( + tracer, f"{call_spec_cache_key}_const_func_spec", func_spec + ) + + # Map runtime args -> proxies (always via tracer.unwrap_proxy now) + flat_proxy_args = pytree.tree_map(tracer.unwrap_proxy, flat_args) + + # Emit flat_apply and track result structure + out_proxy = tracer.create_proxy( + "call_function", flat_apply, (func_spec_proxy, spec_proxy, *flat_proxy_args), {} + ) + track_tensor_tree(track_value, out_proxy, constant=None, tracer=tracer) + + +def _is_init(fn): + return callable(fn) and fn.__name__ == "__init__" + + +def mark_subclass_constructor_exportable_experimental(constructor_subclass): + """ + Experimental decorator that makes subclass to be traceable in export + with pre-dispatch IR. To make your subclass traceble in export, you need to: + 1. Implement __init__ method for your subclass (Look at DTensor implementation) + 2. Decorate your __init__ method with _mark_constructor_exportable_experimental + 3. Put torch._dynamo_disable decorator to prevent dynamo from peeking into its' impl + + Example: + + class FooTensor(torch.Tensor): + @staticmethod + def __new__(cls, elem, *, requires_grad=False): + # ... + return torch.Tensor._make_subclass(cls, elem, requires_grad=requires_grad) + + @torch._dynamo_disable + @mark_subclass_constructor_exportable_experimental + def __init__(self, elem, ...): + # ... + """ + if not _is_init(constructor_subclass): + raise RuntimeError( + f"torch._export.wrappers.mark_constructor_exportable_experimental can only be applied on subclass tensor.__init__" + f"But, you are adding it on {constructor_subclass.__name__} which is not supported. " + f"If __init__ doesn't exist on your subclass, please add it. Look at DTensor.__init__ implementation for example" + ) + + def wrapper(*args, **kwargs): + constructor_subclass(*args, **kwargs) + + if not torch.compiler.is_exporting(): + return + + if not is_traceable_wrapper_subclass_type(type(args[0])): + if not constructor_subclass.__qualname__.endswith("__init__"): + raise AssertionError( + f"expected __qualname__ to end with '__init__', got {constructor_subclass.__qualname__}" + ) + obj_name = constructor_subclass.__qualname__[: -len("__init__")] + raise RuntimeError( + f"Can't intercept {obj_name} in export because this object is not a traceable " + f"tensor subclass. Please look at DTensor.__init__ implementation as an example of proper usage of this API." + ) + + mode = _maybe_find_pre_dispatch_tf_mode_for_export() + if mode is None: + return + + if not isinstance(mode, PreDispatchTorchFunctionMode): + raise AssertionError( + f"expected PreDispatchTorchFunctionMode, got {type(mode)}" + ) + + tracer = mode.tracer + subclass = args[0] + graphable = (tuple(args[1:]), kwargs) + + spec_name = "_".join(constructor_subclass.__qualname__.lower().split(".")) + call_spec_cache_key = type(subclass).__name__.lower() + + _emit_flat_apply_call( + tracer=tracer, + spec_name=spec_name, + const_target_for_apply=type(subclass), + graphable_args=graphable, + track_value=subclass, # track the constructed subclass instance + call_spec_cache_key=call_spec_cache_key, + ) + return + + return wrapper + + +def allow_in_pre_dispatch_graph(func): + """ + Experimental decorator that adds user function to export pre-dispatch graph. Note that + we only support custom autograd function/subclass constructors today. To use this function: + 1. For subclasses: + 1. refer to instructions in mark_subclass_constructor_exportable_experimental + 2. Define apply method on your custom autograd function and apply this decorator. + + Example: + + class MyCoolCustomAutogradFunc(autograd.Function): + @classmethod + @torch._export.wrappers.allow_in_pre_dispatch_graph + def apply(cls, *args, **kwargs): + return super(MyCoolCustomAutogradFunc, cls).apply(*args, **kwargs) + + """ + if _is_init(func): + return mark_subclass_constructor_exportable_experimental(func) + + if not (_is_init(func) or func.__name__ == "apply"): + raise RuntimeError( + f"torch._export.wrappers.allow_in_pre_dispatch_graph can only be applied on subclass tensor.__init_ " + f"or custom_autograd_function.apply. " + f"But, you are adding it on {func.__name__} which is not supported. " + f"If __init__ doesn't exist on your subclass, please add it. Look at DTensor.__init__ implementation for example. " + f"If you are adding it on custom autograd function, please add it on apply method. " + f"If anything else, file an issue on github and we may consider extending our support. " + ) + + @wraps(func) + def wrapper(*args, **kwargs): + if not torch.compiler.is_exporting(): + return func(*args, **kwargs) + + if not inspect.isclass(args[0]): + return func(*args, **kwargs) + + if not issubclass(args[0], torch.autograd.Function): + return func(*args, **kwargs) + + from torch._ops import _get_dispatch_mode_pre_dispatch + + mode = _get_dispatch_mode_pre_dispatch(torch._C._TorchDispatchModeKey.PROXY) + if mode is None: + return func(*args, **kwargs) + + # Sometimes custom autograd functions can call into HOPs that don't have proxy impl + # at PreDispatch level, so we just dispatch it below to get the concrete result. + include_to_set = torch._C._dispatch_tls_local_include_set().remove( + torch._C.DispatchKey.PreDispatch + ) + exclude_to_set = ( + torch._C._dispatch_tls_local_exclude_set() + | torch._C.DispatchKeySet(torch._C.DispatchKey.PreDispatch) + ) + + with torch._C._ForceDispatchKeyGuard(include_to_set, exclude_to_set): + out = func(*args, **kwargs) + + if not mode.pre_dispatch: + raise AssertionError("Should only do this in predispatch") + tracer = mode.tracer + + function_cls_name = f"{args[0].__module__}.{args[0].__qualname__}" + graphable = ((function_cls_name, *args[1:]), kwargs) + + from torch.export.custom_ops import ( + _call_custom_autograd_function_in_pre_dispatch, + ) + + spec_name = "_".join(function_cls_name.split(".")) + call_spec_cache_key = type( + _call_custom_autograd_function_in_pre_dispatch + ).__name__.lower() + _emit_flat_apply_call( + tracer=tracer, + spec_name=spec_name, + const_target_for_apply=_call_custom_autograd_function_in_pre_dispatch, + graphable_args=graphable, + track_value=out, + call_spec_cache_key=call_spec_cache_key, + ) + return out + + return wrapper diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..10a55772ab58b21573a6eba0356ddd3080164ac7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_checkpointing/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_checkpointing/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..10a55772ab58b21573a6eba0356ddd3080164ac7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_checkpointing/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_checkpointing/ac_logging_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_checkpointing/ac_logging_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..c03cc923a31267f595fa5ca30f35ce4914e6fc98 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_checkpointing/ac_logging_utils.py @@ -0,0 +1,191 @@ +import json +import logging +from typing import Any + +from torch._logging import trace_structured +from torch.fx import Graph, Node + + +log: logging.Logger = logging.getLogger(__name__) + + +def create_joint_graph_node_information( + joint_graph: Graph, + recomputable_node_info: dict[str, int], +) -> dict[str, Any]: + joint_graph_node_information: dict[str, Any] = {} + + for i, joint_graph_node in enumerate(joint_graph.nodes): + is_recomputable_candidate: bool = ( + joint_graph_node.name in recomputable_node_info + ) + tensor_meta = joint_graph_node.meta.get("tensor_meta") + # pyrefly: ignore [implicit-any] + shape = getattr(tensor_meta, "shape", []) if tensor_meta else [] + + node_info: dict[str, Any] = { + "index": i, + "name": joint_graph_node.name, + "is_recomputable_candidate": is_recomputable_candidate, + "target": str(joint_graph_node.target), + "shape": str(shape), + "input_arguments": [inp.name for inp in joint_graph_node.all_input_nodes], + "stack_trace": joint_graph_node.meta.get("stack_trace", ""), + } + + if is_recomputable_candidate: + idx: int = recomputable_node_info[joint_graph_node.name] + node_info["recomputable_candidate_info"] = { + "recomputable_node_idx": idx, + } + + joint_graph_node_information[joint_graph_node.name] = node_info + + return joint_graph_node_information + + +def create_joint_graph_edges(joint_graph: Graph) -> list[tuple[str, str]]: + joint_graph_edges: list[tuple[str, str]] = [ + (inp.name, node.name) + for node in joint_graph.nodes + for inp in node.all_input_nodes + ] + return joint_graph_edges + + +def create_activation_checkpointing_logging_structure_payload( + joint_graph: Graph, + joint_graph_node_information: dict[str, Any], + joint_graph_edges: list[tuple[str, str]], + all_recomputable_banned_nodes: list[Node], + expected_runtime: float, + saved_node_idxs: list[int], + recomputable_node_idxs: list[int], + memories_banned_nodes: list[int], + normalized_memories_banned_nodes: list[float], + runtimes_banned_nodes: list[float], + min_cut_saved_values: list[Node], +) -> dict[str, Any]: + """ + Creates a structured payload for logging activation checkpointing information. + + Args: + joint_graph: The computational graph representing operations. + joint_graph_node_information: Dictionary containing information about nodes in the joint graph. + joint_graph_edges: List of edges in the joint graph represented as tuples of node names. + all_recomputable_banned_nodes: List of nodes that are banned from recomputation. + expected_runtime: Expected runtime of the computation. + saved_node_idxs: Indices of nodes that are saved (not recomputed). + recomputable_node_idxs: Indices of nodes that can be recomputed. + memories_banned_nodes: Memory usage values (in absolute units) for banned nodes. + normalized_memories_banned_nodes: Normalized memory usage values for banned nodes, + used as input to the knapsack algorithm. + runtimes_banned_nodes: Runtime values for banned nodes, used as input to the + knapsack algorithm. + min_cut_saved_values: List of nodes saved by the min-cut algorithm. + + Returns: + A dictionary containing structured logging information for activation checkpointing. + """ + activation_checkpointing_logging_structure_payload: dict[str, Any] = { + "Joint Graph Size": len(joint_graph.nodes), + "Joint Graph Edges": { + "Total": len(joint_graph_edges), + "Edges": joint_graph_edges, + }, + "Joint Graph Node Information": joint_graph_node_information, + "Recomputable Banned Nodes Order": [ + node.name for node in all_recomputable_banned_nodes + ], + "Expected Runtime": expected_runtime, + "Knapsack Saved Nodes": saved_node_idxs, + "Knapsack Recomputed Nodes": recomputable_node_idxs, + "Absolute Memories": memories_banned_nodes, + "Knapsack Input Memories": normalized_memories_banned_nodes, + "Knapsack Input Runtimes": runtimes_banned_nodes, + "Min Cut Solution Saved Values": [node.name for node in min_cut_saved_values], + } + return activation_checkpointing_logging_structure_payload + + +def create_structured_trace_for_min_cut_info( + joint_graph: Graph, + all_recomputable_banned_nodes: list[Node], + saved_node_idxs: list[int], + recomputable_node_idxs: list[int], + expected_runtime: float, + memories_banned_nodes: list[int], + normalized_memories_banned_nodes: list[float], + runtimes_banned_nodes: list[float], + min_cut_saved_values: list[Node], +) -> None: + """ + Creates a structured trace for minimum cut information in the graph. + + Args: + joint_graph: The computational graph representation. + all_recomputable_banned_nodes: List of nodes that can be recomputed. + saved_node_idxs: Indices of nodes that are saved in memory. + recomputable_node_idxs: Indices of nodes that are recomputed. + expected_runtime: Expected runtime for the computation. + memories_banned_nodes: Memory requirements for each banned node in bytes. + normalized_memories_banned_nodes: Normalized memory requirements for each banned node + (typically scaled between 0 and 1 for relative comparison). + runtimes_banned_nodes: Runtime costs associated with each banned node. + min_cut_saved_values: Nodes that are saved as part of the minimum cut solution. + """ + # Create a dictionary to store recomputable node information + recomputable_node_info: dict[str, int] = { + node.name: idx for idx, node in enumerate(all_recomputable_banned_nodes) + } + + # Create joint graph node information + joint_graph_node_information = create_joint_graph_node_information( + joint_graph, recomputable_node_info + ) + + # Update node information with recomputable candidate details + for node_name, node_info in joint_graph_node_information.items(): + if node_info["is_recomputable_candidate"]: + idx = recomputable_node_info[node_name] + node_info["recomputable_candidate_info"]["memory"] = memories_banned_nodes[ + idx + ] + node_info["recomputable_candidate_info"]["runtime"] = runtimes_banned_nodes[ + idx + ] + node_info["recomputable_candidate_info"]["is_saved"] = ( + idx in saved_node_idxs + ) + node_info["recomputable_candidate_info"]["is_recomputed"] = ( + idx in recomputable_node_idxs + ) + + # Create joint graph edges + joint_graph_edges = create_joint_graph_edges(joint_graph) + + # Create activation checkpointing logging structure payload + activation_checkpointing_logging_structure_payload = ( + create_activation_checkpointing_logging_structure_payload( + joint_graph=joint_graph, + joint_graph_node_information=joint_graph_node_information, + joint_graph_edges=joint_graph_edges, + all_recomputable_banned_nodes=all_recomputable_banned_nodes, + expected_runtime=expected_runtime, + saved_node_idxs=saved_node_idxs, + recomputable_node_idxs=recomputable_node_idxs, + memories_banned_nodes=memories_banned_nodes, + normalized_memories_banned_nodes=normalized_memories_banned_nodes, + runtimes_banned_nodes=runtimes_banned_nodes, + min_cut_saved_values=min_cut_saved_values, + ) + ) + + # Create structured trace + trace_structured( + "artifact", + metadata_fn=lambda: {"name": "min_cut_information", "encoding": "json"}, + payload_fn=lambda: json.dumps( + activation_checkpointing_logging_structure_payload + ), + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_checkpointing/graph_info_provider.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_checkpointing/graph_info_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..0294e32f292e5f9fe00eb6a1227c7c948757171b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_checkpointing/graph_info_provider.py @@ -0,0 +1,319 @@ +from typing import Any + +import networkx as nx + +from torch.fx import Graph, Node + + +class GraphInfoProvider: + """ + This class provides information about the graph, such as the nodes, edges, and their runtime and memory requirements. + It also provides methods to create graphs from the information provided. + """ + + __RECOMPUTABLE_NODE_ONLY_GRAPH = "recomputable_node_only_graph" + __RECOMPUTABLE_NODE_ONLY_GRAPH_WITH_LARGER_GRAPH_CONTEXT = ( + "recomputable_node_only_graph_with_larger_graph_context" + ) + __FULL_NX_JOINT_GRAPH = "full_nx_joint_graph" + __SIMPLIFIED_FX_JOINT_GRAPH = "fx_joint_graph" + + def __init__( + self, + graph_nodes_in_order: list[str], + graph_edges: list[tuple[str, str]], + all_recomputable_banned_nodes: list[str], + all_node_runtimes: dict[str, float] | None = None, + all_node_memories: dict[str, float] | None = None, + recorded_knapsack_input_memories: list[float] | None = None, + recorded_knapsack_input_runtimes: list[float] | None = None, + joint_graph: Graph | None = None, + ) -> None: + self.graph_nodes_in_order = graph_nodes_in_order + self.graph_edges = graph_edges + self.all_node_runtimes: dict[str, float] = dict() + if all_node_runtimes is None: + if recorded_knapsack_input_runtimes is None: + raise ValueError( + "Either all_node_runtimes or recorded_knapsack_input_runtimes must be provided." + ) + self.all_node_runtimes = { + node: recorded_knapsack_input_runtimes[i] + for i, node in enumerate(all_recomputable_banned_nodes) + } + else: + self.all_node_runtimes.update(all_node_runtimes) + self.all_node_memories: dict[str, float] = dict() + if all_node_memories is None: + if recorded_knapsack_input_memories is None: + raise ValueError( + "Either all_node_memories or recorded_knapsack_input_memories must be provided." + ) + self.all_node_memories = { + node: recorded_knapsack_input_memories[i] + for i, node in enumerate(all_recomputable_banned_nodes) + } + else: + self.all_node_memories.update(all_node_memories) + self.all_recomputable_banned_nodes = all_recomputable_banned_nodes + self.all_recomputable_banned_nodes_set = set(all_recomputable_banned_nodes) + self.recorded_knapsack_input_memories = recorded_knapsack_input_memories + self.recorded_knapsack_input_runtimes = recorded_knapsack_input_runtimes + self._lazily_initialized_graphs: dict[str, Any] = { + self.__RECOMPUTABLE_NODE_ONLY_GRAPH: None, + self.__RECOMPUTABLE_NODE_ONLY_GRAPH_WITH_LARGER_GRAPH_CONTEXT: None, + self.__FULL_NX_JOINT_GRAPH: None, + self.__SIMPLIFIED_FX_JOINT_GRAPH: None, + } + + @classmethod + def inialize_from_graph( + cls, + joint_graph: Graph, + all_recomputable_banned_nodes: list[Node], + recorded_knapsack_input_memories: list[float], + recorded_knapsack_input_runtimes: list[float], + ) -> "GraphInfoProvider": + """ + Enables initialization from a joint graph. + """ + graph_nodes_in_order = [node.name for node in joint_graph.nodes] + graph_edges = [ + (node.name, user.name) for node in joint_graph.nodes for user in node.users + ] + all_recomputable_banned_node_names = [ + node.name for node in all_recomputable_banned_nodes + ] + return cls( + graph_nodes_in_order=graph_nodes_in_order, + graph_edges=graph_edges, + all_recomputable_banned_nodes=all_recomputable_banned_node_names, + recorded_knapsack_input_memories=recorded_knapsack_input_memories, + recorded_knapsack_input_runtimes=recorded_knapsack_input_runtimes, + joint_graph=joint_graph, + ) + + @property + def recomputable_node_only_graph(self) -> nx.DiGraph: + if self._lazily_initialized_graphs[self.__RECOMPUTABLE_NODE_ONLY_GRAPH] is None: + self._lazily_initialized_graphs[self.__RECOMPUTABLE_NODE_ONLY_GRAPH] = ( + self._create_recomputable_node_only_graph() + ) + return self._lazily_initialized_graphs[self.__RECOMPUTABLE_NODE_ONLY_GRAPH] + + @property + def recomputable_node_only_graph_with_larger_graph_context(self) -> nx.DiGraph: + if ( + self._lazily_initialized_graphs[ + self.__RECOMPUTABLE_NODE_ONLY_GRAPH_WITH_LARGER_GRAPH_CONTEXT + ] + is None + ): + self._lazily_initialized_graphs[ + self.__RECOMPUTABLE_NODE_ONLY_GRAPH_WITH_LARGER_GRAPH_CONTEXT + ] = self._create_recomputable_node_only_graph_with_larger_graph_context() + return self._lazily_initialized_graphs[ + self.__RECOMPUTABLE_NODE_ONLY_GRAPH_WITH_LARGER_GRAPH_CONTEXT + ] + + @property + def full_joint_nx_graph(self) -> nx.DiGraph: + if self._lazily_initialized_graphs[self.__FULL_NX_JOINT_GRAPH] is None: + self._lazily_initialized_graphs[self.__FULL_NX_JOINT_GRAPH] = ( + self._create_full_joint_graph() + ) + return self._lazily_initialized_graphs[self.__FULL_NX_JOINT_GRAPH] + + @property + def simplified_fx_joint_graph(self) -> Graph: + if self._lazily_initialized_graphs[self.__SIMPLIFIED_FX_JOINT_GRAPH] is None: + self._lazily_initialized_graphs[self.__SIMPLIFIED_FX_JOINT_GRAPH] = ( + self._recreate_psuedo_joint_graph() + ) + return self._lazily_initialized_graphs[self.__SIMPLIFIED_FX_JOINT_GRAPH] + + def get_non_ac_peak_memory(self) -> float: + return sum( + self.all_node_memories[node_name] + for node_name in self.all_recomputable_banned_nodes_set + ) + + def get_theoretical_max_runtime(self) -> float: + return sum( + self.all_node_runtimes[node_name] + for node_name in self.all_recomputable_banned_nodes_set + ) + + def get_knapsack_memory_input(self) -> list[float]: + return ( + self.recorded_knapsack_input_memories + if self.recorded_knapsack_input_memories + else [ + self.all_node_memories[node_name] + for node_name in self.all_recomputable_banned_nodes + ] + ) + + def get_knapsack_runtime_input(self) -> list[float]: + return ( + self.recorded_knapsack_input_runtimes + if self.recorded_knapsack_input_runtimes + else [ + self.all_node_runtimes[node_name] + for node_name in self.all_recomputable_banned_nodes + ] + ) + + def _create_recomputable_node_only_graph(self) -> nx.DiGraph: + graph = nx.DiGraph() + for recomputable_node in self.all_recomputable_banned_nodes: + graph.add_node(recomputable_node) + + for a, b in self.graph_edges: + if ( + a in self.all_recomputable_banned_nodes_set + and b in self.all_recomputable_banned_nodes_set + ): + graph.add_edge(a, b) + return graph + + def _create_recomputable_node_only_graph_with_larger_graph_context( + self, + ) -> nx.DiGraph: + # Create a dictionary to store the reachable nodes for each node + all_recomputable_banned_nodes_set = set(self.all_recomputable_banned_nodes) + + reachable_nodes = {} + for node in all_recomputable_banned_nodes_set: + # Use BFS to find all reachable nodes + predecessors = dict(nx.bfs_predecessors(self.full_joint_nx_graph, node)) + reachable_recomputable_nodes = set(predecessors.keys()).intersection( + all_recomputable_banned_nodes_set + ) + reachable_nodes[node] = reachable_recomputable_nodes + # Create the candidate graph + candidate_graph = nx.DiGraph() + candidate_graph.add_nodes_from(all_recomputable_banned_nodes_set) + for node1 in all_recomputable_banned_nodes_set: + for node2 in reachable_nodes[node1]: + # Check if there is an overlapping path + overlapping_path = False + for intermediate_node in reachable_nodes[node1]: + if ( + intermediate_node != node2 + and node2 in reachable_nodes[intermediate_node] + ): + overlapping_path = True + break + if not overlapping_path: + candidate_graph.add_edge(node1, node2) + return candidate_graph + + def _create_full_joint_graph(self) -> nx.DiGraph: + graph = nx.DiGraph() + for node in self.graph_nodes_in_order: + if node == "output": + continue + graph.add_node(node) + + for a, b in self.graph_edges: + if a == "output" or b == "output": + continue + graph.add_edge(a, b) + return graph + + def _recreate_psuedo_joint_graph(self) -> Graph: + # Create a dictionary to store the dependencies of each node + node_dependencies: dict[str, list[str]] = { + node: [] for node in self.graph_nodes_in_order + } + for a, b in self.graph_edges: + if a not in node_dependencies or b not in node_dependencies: + raise ValueError(f"Edge ({a}, {b}) references a non-existent node.") + node_dependencies[b].append(a) + + joint_graph = Graph() + # Create nodes in the graph + nodes: dict[str, Node] = {} + for node_name in self.graph_nodes_in_order: + input_nodes = [nodes[dep] for dep in node_dependencies[node_name]] + if input_nodes: + node = joint_graph.call_function(lambda *x: x, tuple(input_nodes)) + node.name = node_name + else: + node = joint_graph.placeholder(node_name) + nodes[node_name] = node + return joint_graph + + def _visualize_recomputable_candidate_graph_with_larger_context( + self, + layout_k: float = 0.5, + layout_iterations: int = 30, + ) -> None: + """ + Visualize the recomputable candidate graph with larger context. + """ + from matplotlib import cm, colors as mcolors, pyplot as plt + + pos = nx.spring_layout( + self.recomputable_node_only_graph_with_larger_graph_context, + k=layout_k, + iterations=layout_iterations, + ) + # pos = nx.spectral_layout(graph_with_indirect_edges) + plt.figure(figsize=(20, 15)) + + # Create a dictionary for node labels using the index + labels = { + node: self.recomputable_node_only_graph_with_larger_graph_context.nodes[ + node + ].get("index", node) + for node in self.recomputable_node_only_graph_with_larger_graph_context.nodes + } + + # Extract memory values and normalize them + norm = mcolors.Normalize( + vmin=min(self.get_knapsack_memory_input()), + vmax=max(self.get_knapsack_memory_input()), + ) + cmap = cm.viridis # type: ignore[attr-defined] + + # Assign colors based on memory + node_colors = [ + cmap( + norm( + float( + self.recomputable_node_only_graph_with_larger_graph_context.nodes[ + node + ]["memory"] + ) + ) + ) + for node in self.recomputable_node_only_graph_with_larger_graph_context.nodes + ] + + # Draw the graph with parsed nodes only + nx.draw_networkx_nodes( + self.recomputable_node_only_graph_with_larger_graph_context, + pos, + node_color=node_colors, + node_size=300, + label="Parsed Nodes", + ) + nx.draw_networkx_edges( + self.recomputable_node_only_graph_with_larger_graph_context, + pos, + arrows=True, + arrowsize=10, + ) + nx.draw_networkx_labels( + self.recomputable_node_only_graph_with_larger_graph_context, + pos, + labels=labels, + font_size=8, + font_weight="bold", + ) + + plt.title("Memory Colour Coded Dependency Graph for Recomputable Nodes") + plt.colorbar(cm.ScalarMappable(norm=norm, cmap=cmap), label="Memory") + plt.show() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_checkpointing/knapsack.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_checkpointing/knapsack.py new file mode 100644 index 0000000000000000000000000000000000000000..b2f0a124c64c1ec7ec6651aa79ff62ebec557949 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_checkpointing/knapsack.py @@ -0,0 +1,267 @@ +import torch + + +def greedy_knapsack( + memory: list[float], runtimes: list[float], max_memory: float +) -> tuple[float, list[int], list[int]]: + n = len(runtimes) + items = list(range(n)) + + # Sort items based on the ratio of runtime to memory in descending order + items = sorted(items, key=lambda i: runtimes[i] / memory[i], reverse=True) + + total_memory = 0.0 + total_runtime = 0.0 + items_to_save = [] + items_to_allow_recomputing = [] + + for i in items: + if total_memory + memory[i] <= max_memory: + total_memory += memory[i] + total_runtime += runtimes[i] + items_to_save.append(i) + else: + items_to_allow_recomputing.append(i) + return total_runtime, items_to_save, items_to_allow_recomputing + + +def ilp_knapsack( + memory: list[float], runtimes: list[float], max_memory: float +) -> tuple[float, list[int], list[int]]: + import numpy as np + + try: + from scipy.optimize import Bounds, LinearConstraint, milp + except ImportError: + raise RuntimeError( + "To use the ILP for memory budget checkpointing you need to install scipy" + ) from None + + np_memory = np.array(memory) + np_runtimes = np.array(runtimes) + c = -np_runtimes # type: ignore[operator] + + memory_constraint = LinearConstraint(A=np_memory, ub=np.array(max_memory)) + constraints = [memory_constraint] + + integrality = np.ones_like(c) + res = milp( + c=c, constraints=constraints, integrality=integrality, bounds=Bounds(0, 1) + ) + if not res.success: + raise RuntimeError("Somehow scipy solving failed") + + items_to_save = [] + items_to_allow_recomputing = [] + for idx, i in enumerate(res.x): + if i == 1: + items_to_save.append(idx) + else: + items_to_allow_recomputing.append(idx) + return -res.fun, items_to_save, items_to_allow_recomputing + + +def dp_knapsack( + memory: list[float], runtime: list[float], max_memory: float +) -> tuple[float, list[int], list[int]]: + # Scaling factor to convert floating point weights to integers + S = 10000 + + # Quantize the memory weights + quantized_memory = torch.tensor( + [round(m * S) for m in memory], dtype=torch.long, device="cpu" + ) + runtimes = torch.tensor(runtime, dtype=torch.float32, device="cpu") + + # Quantized pseudopolynomial DP for 0-1 Knapsack + quantized_max_memory = round(max_memory * S) + + n = len(memory) + + # Initialize the DP table + # TODO(chilli): I think if needed, this memory can be optimized with sliding + # window trick + Hirschberg trick: + # https://codeforces.com/blog/entry/47247?#comment-316200 + dp = torch.zeros( + (n + 1, quantized_max_memory + 1), dtype=torch.float32, device="cpu" + ) + + for i in range(1, n + 1): + current_memory = quantized_memory[i - 1] + current_runtime = runtimes[i - 1] + + # Copy the previous row + dp[i, :] = dp[i - 1, :] + + # Update dp[i, j] for all j >= current_memory + if current_memory == 0: + dp[i, :] = dp[i - 1, :] + current_runtime + else: + dp[i, current_memory:] = torch.maximum( + dp[i - 1, current_memory:], + dp[i - 1, :-current_memory] + current_runtime, + ) + + # Backtrack to find the items included in the knapsack + saved_items = [] + recomputable_items = [] + j: int = quantized_max_memory + for i in range(n, 0, -1): + if dp[i][j] != dp[i - 1][j]: + saved_items.append(i - 1) # Include this item (indexing from 0) + j -= int(quantized_memory[i - 1].item()) + else: + recomputable_items.append(i - 1) + + saved_items.reverse() # To get items in the order they were added + + # The maximum runtime that can be achieved within the max_memory constraint + max_runtime = dp[n][quantized_max_memory].item() + + return max_runtime, saved_items, recomputable_items + + +def dp_knapsack_sliding_hirschberg( + memory: list[float], runtime: list[float], max_memory: float +) -> tuple[float, list[int], list[int]]: + # Scaling factor to convert floating point weights to integers + S = 10000 + + # q_ prefix stands for quantized + q_memory = [int(round(m * S)) for m in memory] + runtimes = [float(v) for v in runtime] + + q_max_memory = int(round(max_memory * S)) + + q_memory_length = len(q_memory) + if q_memory_length == 0: + return 0.0, [], [] + + item_indices = list(range(q_memory_length)) + dp_profile_size = q_max_memory + 1 + + # Current DP profile (row) + dp_profile = torch.zeros(dp_profile_size, dtype=torch.float32, device="cpu") + # Store a candidate for next dp_profile - current dp row + item + candidate_profile = torch.empty(dp_profile_size, dtype=torch.float32, device="cpu") + left_profile = torch.empty(dp_profile_size, dtype=torch.float32, device="cpu") + right_profile = torch.empty(dp_profile_size, dtype=torch.float32, device="cpu") + + saved_items: list[int] = [] + recomputable_items: list[int] = [] + + # Explicit stack to optimize memory and avoid recursion + # Stack stores segments as (start index, end index, capacity for segment) + stack: list[tuple[int, int, int]] = [(0, q_memory_length, q_max_memory)] + + # LIFO + while stack: + start, end, capacity = stack.pop() + length = end - start + if length == 0: + continue + + # Leaf + if length == 1: + index = item_indices[start] + memory_item = q_memory[index] + runtime_item = runtimes[index] + if memory_item <= capacity and runtime_item > 0.0: + saved_items.append(index) + else: + recomputable_items.append(index) + continue + + # Split the segment into two halves + middle = start + (length // 2) + left_start, left_end = middle, end + right_start, right_end = start, middle + + # Assign items to both halves + left_items = item_indices[left_start:left_end] + right_items = item_indices[right_start:right_end] + + # Working only on items allowed by segment's capacity + capacity = capacity + 1 + dp_view = dp_profile[:capacity] + candidate_view = candidate_profile[:capacity] + left_dp_local = left_profile[:capacity] + right_dp_local = right_profile[:capacity] + + # Left part + dp_view.zero_() + for index in left_items: + memory_item = q_memory[index] + runtime_item = runtimes[index] + + if memory_item == 0: + # Weight is 0, so add it to all capacities; a "free lunch", essentially + dp_view.add_(runtime_item) + continue + + # If item is too heavy, we skip it + if memory_item >= capacity: + continue + + # Add the current item so we can then pick the highest value + dp_view_candidate = candidate_view[: capacity - memory_item] + torch.add(dp_view[:-memory_item], runtime_item, out=dp_view_candidate) + # Take the highest - either previous (without current) or with current + torch.maximum( + dp_view[memory_item:], dp_view_candidate, out=dp_view[memory_item:] + ) + + # Store the left profile + left_dp_local.copy_(dp_view) + + # Right part + dp_view.zero_() + for index in right_items: + memory_item = q_memory[index] + runtime_item = runtimes[index] + + if memory_item == 0: + dp_view.add_(runtime_item) + continue + + if memory_item >= capacity: + continue + + dp_view_candidate = candidate_view[: capacity - memory_item] + torch.add(dp_view[:-memory_item], runtime_item, out=dp_view_candidate) + torch.maximum( + dp_view[memory_item:], dp_view_candidate, out=dp_view[memory_item:] + ) + + # Store the reversed right profile + right_dp_local.copy_(dp_view.flip(-1)) + + # In-place compute item-wise sum of left and right to pick the split point where the sum is highest + left_dp_local.add_(right_dp_local) + + # Pick the index of highest value of a pair, which we then use as a split point + best_split = int(torch.argmax(left_dp_local).item()) + + left_capacity = best_split + right_capacity = capacity - best_split + + # Clamp (might be removed if we're 100% sure that there is no edge case that will mess up the indices math) + if left_capacity < 0: + left_capacity = 0 + if right_capacity < 0: + right_capacity = 0 + if left_capacity > q_max_memory: + left_capacity = q_max_memory + if right_capacity > q_max_memory: + right_capacity = q_max_memory + + # Push right then left, so left is processed next + stack.append((right_start, right_end, right_capacity)) + stack.append((left_start, left_end, left_capacity)) + + saved_items = sorted(saved_items) + recomputable_items = sorted(recomputable_items) + + max_runtime = sum(runtime[i] for i in saved_items) + recomputable_items.reverse() + return max_runtime, saved_items, recomputable_items diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_checkpointing/knapsack_evaluator.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_checkpointing/knapsack_evaluator.py new file mode 100644 index 0000000000000000000000000000000000000000..86ad04ed2f5c767808b0a44967d63bcd176af6e3 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_checkpointing/knapsack_evaluator.py @@ -0,0 +1,282 @@ +import operator +from collections import deque +from collections.abc import Callable + +import networkx as nx + +from torch._functorch._activation_checkpointing.graph_info_provider import ( + GraphInfoProvider, +) + + +class KnapsackEvaluator: + """ + This class evaluates the theoretical runtime and peak memory usage of a given checkpointing strategy. + It takes in a graph and a list of nodes that are saved and recomputed, and then simulates the + backward pass to calculate the peak memory usage. + """ + + def __init__( + self, + graph_info_provider: GraphInfoProvider, + ) -> None: + self._graph_info_provider = graph_info_provider + + def _get_backward_memory_from_topologically_sorted_graph( + self, + node_graph: nx.DiGraph, + node_memories: dict[str, float], + saved_nodes_set: set[str], + peak_memory_after_forward_pass: float, + ) -> list[tuple[float, str]]: + """ + Simulates the backward pass and keeps track of the peak memory usage. + + High Level Steps: + 1. Set Initial Peak/Current Memory + Allows you to set the peak memory after the forward pass, but typically this is + the sum of the estimated memory of the saved nodes. + 2. Perform a reverse topological sort of the node_graph. + If full graph is defined then will sort the full graph and only process the subset + of nodes in the node_graph. + 3. Iterate through the sorted graph nodes. + If the node is saved then just drop it's memory from current memory. + If the node is not saved then add it's memory to current memory and then traverse it's + predecessors to simulate recomuptation chain. Will check if new peak memory after all + predecessors are processed. + + Args: + node_graph (nx.DiGraph): A directed graph representing the recomputable forward nodes. + saved_nodes_set (Set[str]): A set of node names that are saved. + peak_memory_after_forward_pass (float): The peak memory usage after the forward pass. + """ + current_memory = [ + (peak_memory_after_forward_pass, "Initial Peak/Current Memory") + ] + already_computed = set() + sorted_nodes = list(reversed(list(nx.topological_sort(node_graph)))) + dependencies_computed = set() + + for node in sorted_nodes: + if node in saved_nodes_set or node in already_computed: + current_memory.append( + ( + current_memory[-1][0] - node_memories[node], + f"Dropping Node(already saved): {node}", + ) + ) + continue + + already_computed.add(node) + current_memory.append( + ( + current_memory[-1][0] + node_memories[node], + f"Recomputing Node: {node}", + ) + ) + # Create a queue of dependencies required for recomputation + predecessor_queue = deque( + [ + dependency + # pyrefly: ignore [bad-unpacking] + for dependency, v in node_graph.in_edges(node) + if dependency not in already_computed + ] + ) + while predecessor_queue: + dep = predecessor_queue.popleft() + already_computed.add(dep) + dependencies_computed.add(dep) + current_memory.append( + ( + current_memory[-1][0] + node_memories[dep], + f"Recomputing Predecessor of {node}: {dep}", + ) + ) + # Add predecessors of the predecessor to the queue if they haven't been recomputed yet + # pyrefly: ignore [bad-unpacking] + for dependency_of_dependency, _ in node_graph.in_edges(dep): + if ( + dependency_of_dependency in already_computed + or dependency_of_dependency in saved_nodes_set + or dependency_of_dependency in predecessor_queue + ): + continue + predecessor_queue.append(dependency_of_dependency) + dependencies_computed.clear() + current_memory.append( + (current_memory[-1][0] - node_memories[node], f"Dropping Node: {node}") + ) + return current_memory + + def _validate_all_indexes_accounted_for_in_provided_output( + self, saved_nodes_idxs: list[int], recomputable_node_idxs: list[int] + ) -> None: + """ + Validate that all indexes are accounted for in the provided output. + This function checks that the union of saved nodes and recomputable nodes + covers all candidate nodes without any overlaps. + """ + recomputable_node_idxs_set = set(recomputable_node_idxs) + saved_nodes_idxs_set = set(saved_nodes_idxs) + all_candidate_nodes_idxs = set( + range(len(self._graph_info_provider.all_recomputable_banned_nodes)) + ) + # Check that there are no overlaps between saved nodes and recomputable nodes + if len(recomputable_node_idxs_set.intersection(saved_nodes_idxs_set)) != 0: + raise AssertionError( + f"Saved nodes and recomputable nodes cannot have any overlaps, " + f"but found overlap: {recomputable_node_idxs_set.intersection(saved_nodes_idxs_set)}" + ) + # Check that all candidate nodes are accounted for + if ( + recomputable_node_idxs_set.union(saved_nodes_idxs_set) + != all_candidate_nodes_idxs + ): + raise AssertionError( + f"All candidate nodes must be accounted for in the provided output, " + f"got union={recomputable_node_idxs_set.union(saved_nodes_idxs_set)}, " + f"expected={all_candidate_nodes_idxs}" + ) + + def evaluate_knapsack_output( + self, + saved_nodes_idxs: list[int], + recomputable_node_idxs: list[int], + account_for_backward_pass: bool = False, + ) -> dict[str, float]: + """ + Evaluate the theoretical runtime and peak memory usage of a given checkpointing strategy. + Args: + - saved_nodes_idxs (List[int]): The indices of nodes that are saved. + - recomputable_node_idxs (List[int]): The indices of nodes that need to be recomputed. + """ + self._validate_all_indexes_accounted_for_in_provided_output( + saved_nodes_idxs, recomputable_node_idxs + ) + recomputation_runtime = sum( + self._graph_info_provider.all_node_runtimes[ + self._graph_info_provider.all_recomputable_banned_nodes[node] + ] + for node in recomputable_node_idxs + ) + if account_for_backward_pass: + memory_list = self._get_backward_memory_from_topologically_sorted_graph( + node_graph=self._graph_info_provider.recomputable_node_only_graph_with_larger_graph_context, + saved_nodes_set={ + self._graph_info_provider.all_recomputable_banned_nodes[i] + for i in saved_nodes_idxs + }, + node_memories=self._graph_info_provider.all_node_memories, + peak_memory_after_forward_pass=sum( + self._graph_info_provider.all_node_memories[ + self._graph_info_provider.all_recomputable_banned_nodes[i] + ] + for i in saved_nodes_idxs + ), + ) + peak_memory = max(memory_list, key=operator.itemgetter(0))[0] + else: + peak_memory = sum( + self._graph_info_provider.all_node_memories[ + self._graph_info_provider.all_recomputable_banned_nodes[node] + ] + for node in saved_nodes_idxs + ) + return { + "peak_memory": peak_memory, + "recomputation_runtime": recomputation_runtime, + "non_ac_peak_memory": self._graph_info_provider.get_non_ac_peak_memory(), + "theoretical_max_runtime": self._graph_info_provider.get_theoretical_max_runtime(), + "percentage_of_theoretical_peak_memory": peak_memory + / self._graph_info_provider.get_non_ac_peak_memory(), + "percentage_of_theoretical_peak_runtime": recomputation_runtime + / self._graph_info_provider.get_theoretical_max_runtime(), + } + + def evaluate_distribution_of_results_for_knapsack_algo( + self, + knapsack_algo: Callable[ + [list[float], list[float], float], tuple[float, list[int], list[int]] + ], + memory_budget_values: list[float], + ) -> list[dict[str, float]]: + """ + Evaluates the distribution of results for a given knapsack algorithm. + Args: + knapsack_algo (Callable): The knapsack algorithm to use for evaluation. + memory_budget_values (List[float]): A list of memory budgets to evaluate. + """ + results = [] + for memory_budget in memory_budget_values: + _, saved_nodes, recomputed_nodes = knapsack_algo( + self._graph_info_provider.get_knapsack_memory_input(), + self._graph_info_provider.get_knapsack_runtime_input(), + memory_budget, + ) + result = self.evaluate_knapsack_output( + saved_nodes_idxs=saved_nodes, + recomputable_node_idxs=recomputed_nodes, + ) + result["memory_budget"] = memory_budget + results.append(result) + return results + + def get_knee_point_memory_budget( + self, + knapsack_algo: Callable[ + [list[float], list[float], float], tuple[float, list[int], list[int]] + ], + max_mem_budget: float = 0.1, + min_mem_budget: float = 0.001, + iterations: int = 100, + ) -> float: + """ + Finds the memory budget at the knee point in the Pareto frontier. + + The knee point is defined as the point where the trade-off between + runtime and memory usage is optimal. + + Args: + knapsack_algo (callable): Knapsack algorithm to use for evaluation. + max_mem_budget (float, optional): Maximum memory budget. Defaults to 0.1. + min_mem_budget (float, optional): Minimum memory budget. Defaults to 0.001. + iterations (int, optional): Number of memory budgets to evaluate. Defaults to 100. + + Returns: + float: Memory budget at the knee point. + """ + results = self.evaluate_distribution_of_results_for_knapsack_algo( + knapsack_algo=knapsack_algo, + memory_budget_values=[ + min_mem_budget + + i * (max_mem_budget - min_mem_budget) / (iterations - 1) + for i in range(iterations) + ], + ) + runtime_values = [ + result["percentage_of_theoretical_peak_runtime"] for result in results + ] + memory_values = [ + result["percentage_of_theoretical_peak_memory"] for result in results + ] + runtime_range = max(runtime_values) - min(runtime_values) + memory_range = max(memory_values) - min(memory_values) + if runtime_range == 0 or memory_range == 0: + return max_mem_budget + + # Normalize values + runtime_min = min(runtime_values) + memory_min = min(memory_values) + runtime_norm = [ + (value - runtime_min) / runtime_range for value in runtime_values + ] + memory_norm = [(value - memory_min) / memory_range for value in memory_values] + # Calculate Euclidean distance + distances = [ + (runtime_norm[i] ** 2 + memory_norm[i] ** 2) ** 0.5 + for i in range(len(runtime_norm)) + ] + # Find the knee point(shortest distance from the origin) + knee_index = distances.index(min(distances)) + return results[knee_index]["memory_budget"] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_checkpointing/remat_using_tags_for_fwd_loss_bwd_graph_pass.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_checkpointing/remat_using_tags_for_fwd_loss_bwd_graph_pass.py new file mode 100644 index 0000000000000000000000000000000000000000..82ef44a23ff0f162e81d43a2aefc677037aa9afb --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_checkpointing/remat_using_tags_for_fwd_loss_bwd_graph_pass.py @@ -0,0 +1,206 @@ +"""AC rematerialize pass: Duplicates recompute nodes for backward, then DCE removes unused forward versions.""" + +import itertools +import logging +from typing import Any, overload + +import torch +import torch.fx as fx +from torch._functorch.compile_utils import raise_getitems +from torch._functorch.partitioners import ( + cleanup_recompute_tags, + force_save_bw_mutation_src, + has_recomputable_ops, + has_recomputable_rng_ops, + is_not_collective, + must_recompute, +) + + +log = logging.getLogger(__name__) +_EMPTY_CUSTOM_META: dict[str, object] = {} + + +def is_impure_node_for_dce(node: fx.Node) -> bool: + # Check for special collectives that should be treated as pure + if not is_not_collective(node): + # It's a collective (wait_tensor, all_gather_into_tensor, etc.) + # Treat as pure - can be eliminated if unused + return False + + # For everything else, fall back to the DEFAULT logic + # This is what eliminate_dead_code() calls when is_impure_node=None + impure_random = True + if torch._guards.TracingContext.try_get(): + impure_random = torch._inductor.config.fallback_random + return node.is_impure(impure_random) + + +def _is_backward_node(node: fx.Node, use_phase: bool = False) -> bool: + """Check if node is in backward region. + + If use_phase is True, only checks custom["phase"] == "backward" + (user annotation). Otherwise falls back to node.meta["autograd_backward"], + which Dynamo adds when tracing torch.autograd.grad. + """ + custom = node.meta.get("custom", _EMPTY_CUSTOM_META) + if use_phase: + return custom.get("phase") == "backward" + return node.meta.get("autograd_backward", False) + + +def _has_user_phase_annotation(gm: fx.GraphModule) -> bool: + """Check if any node has the user-level phase: backward annotation.""" + return any( + node.meta.get("custom", _EMPTY_CUSTOM_META).get("phase") == "backward" + for node in gm.graph.nodes + ) + + +def _collect_backward_region_data( + gm: fx.GraphModule, +) -> tuple[bool, int | None, int | None, int]: + use_phase = _has_user_phase_annotation(gm) + bwd_start: int | None = None + bwd_end: int | None = None + num_regions = 0 + in_backward = False + + for idx, node in enumerate(gm.graph.nodes): + is_bwd = _is_backward_node(node, use_phase=use_phase) + if is_bwd: + if bwd_start is None: + bwd_start = idx + bwd_end = idx + 1 + if not in_backward: + num_regions += 1 + in_backward = is_bwd + + return use_phase, bwd_start, bwd_end, num_regions + + +def remat_using_tags_for_fwd_loss_bwd_graph(gm: fx.GraphModule) -> fx.GraphModule: + """ + Duplicate recompute nodes for backward use. DCE removes unused forward versions. + + Backward regions are identified by custom["phase"] == "backward" (user + annotation) or node.meta["autograd_backward"] == True (set automatically when + Dynamo traces torch.autograd.grad). When the user provides phase + annotations, only those annotated regions are used. + + Only a single contiguous backward region is supported. If multiple disjoint + backward regions are detected, an error is raised. Consecutive backward + operations without non-backward nodes between them are treated as a single + backward region. + """ + if not has_recomputable_ops(gm): + return gm + + use_phase, bwd_start, bwd_end, num_regions = _collect_backward_region_data(gm) + if num_regions > 1: + if use_phase: + raise RuntimeError( + f"Detected {num_regions} disjoint backward regions annotated with " + 'phase: "backward" but remat only supports a single backward region. ' + "Please ensure only one contiguous region is annotated." + ) + raise RuntimeError( + f"Detected {num_regions} disjoint backward regions in the graph but remat only supports " + "a single backward region. This can happen when non-backward computation appears " + "between backward sections. Please annotate the real backward with " + 'torch.fx.traceback.annotate({"phase": "backward"}).' + ) + + if bwd_start is None: + return gm + if bwd_end is None: + raise AssertionError( + "backward region should end somewhere when there was explicit backward region start." + ) + + order = {node: idx for idx, node in enumerate(gm.graph.nodes)} + + if has_recomputable_rng_ops(gm): + raise RuntimeError( + "Activation checkpoint rematerialization in `forward-loss-backward` graph does not support RNG ops " + "in recompute regions. Please move RNG operations outside " + "of recompute regions, or use joint graph mode (where partitioner handles RNG)." + ) + + # Use partitioner pass to normalize AC node tags. + gm = cleanup_recompute_tags(gm, is_default_partition=True) + + force_save_bw_mutation_src(gm) + + new_graph = fx.Graph() + env: dict[fx.Node, fx.Node] = {} + recomputed_nodes: dict[fx.Node, fx.Node] = {} + + # Insert forward nodes + for node in itertools.islice(gm.graph.nodes, 0, bwd_start): + env[node] = new_graph.node_copy(node, lambda x: env[x]) + + @overload + def remat_input(x: fx.Node) -> fx.Node: ... + @overload + def remat_input(x: Any) -> Any: ... + + def remat_input(x: object) -> object: + # fx.Node can have args that are primitive types (e.g. int, float, bool) + if not isinstance(x, fx.Node): + return x + return recomputed_nodes.get(x, env[x]) + + def gather_recompute_deps(node: fx.Node) -> set[fx.Node]: + deps: set[fx.Node] = set() + + def _gather(n: fx.Node) -> None: + if n in deps or n in recomputed_nodes or not must_recompute(n): + return + deps.add(n) + for inp in n.all_input_nodes: + _gather(inp) + + # Can't call _gather(node) directly: node itself may not be must_recompute + # (e.g. backward nodes), so _gather would return early without visiting inputs. + for inp in node.all_input_nodes: + _gather(inp) + return deps + + # Insert backward nodes + for node in itertools.islice(gm.graph.nodes, bwd_start, bwd_end): + # Gather all deps that need to be recomputed for this node + deps = gather_recompute_deps(node) + + # Insert deps in forward order (guaranteed disjoint from already-inserted) + # This is not as inefficient as it looks, because we only add fresh dependencies + # when they are not yet processed as recomputed nodes. + new_deps = sorted(deps, key=lambda n: order[n]) + if new_deps: + log.debug( + "To compute backward node %s, recomputing [%s]", + node.name, + ", ".join(dep.name for dep in new_deps), + ) + for dep in new_deps: + dup = new_graph.node_copy(dep, remat_input) + dup.name = dep.name + "_recomputed" + recomputed_nodes[dep] = dup + + env[node] = new_graph.node_copy(node, remat_input) + + for node in itertools.islice(gm.graph.nodes, bwd_end, None): + env[node] = new_graph.node_copy(node, lambda x: env[x]) + + new_gm = torch.fx.GraphModule(gm, new_graph) + + # DCE with custom is_impure_node (like default_partition) + # Treats certain collectives as pure while delegating to default impurity logic + new_gm.graph.eliminate_dead_code(is_impure_node=is_impure_node_for_dce) + + # raise_getitems pass for better memory (like default_partition) + new_gm = raise_getitems(new_gm) + + new_gm.recompile() + + return new_gm diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_offloading/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_offloading/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..10a55772ab58b21573a6eba0356ddd3080164ac7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_offloading/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_offloading/activation_offloading.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_offloading/activation_offloading.py new file mode 100644 index 0000000000000000000000000000000000000000..8f9cd2203d12fd8babe92317c898f67793276d19 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_offloading/activation_offloading.py @@ -0,0 +1,813 @@ +"""Activation offloading for memory optimization during compilation. + +This module provides functionality to offload activations to CPU during the forward +pass and reload them during the backward pass, reducing GPU memory usage. It can be +applied to graphs produced by both AOT Autograd partitioners and make_fx-based tracing. +""" + +import logging +import operator +from dataclasses import dataclass + +import torch +import torch.fx as fx +from torch._functorch._activation_offloading.offload_ops import ( # noqa: F401 -- registers ao::offload, ao::reload, ao::wait_tensor ops + offload, + reload, + wait_tensor, +) +from torch._inductor.fx_passes.overlap_scheduling import benchmark_node, is_compute_node +from torch._subclasses.fake_tensor import extract_tensor_metadata +from torch.utils._ordered_set import OrderedSet + +from .. import config +from ..partitioners import _size_of, get_default_op_list, OpTypes + + +log: logging.Logger = logging.getLogger(__name__) + + +# Node name prefixes for offload/reload operations +# NOTE: right now we are using these prefixes as identifiers for offload/reload +CPU_OFFLOAD_PREFIX = "cpu_offload_" +GPU_RELOAD_PREFIX = "gpu_reload_" + + +def _find_all_effective_users(node: fx.Node, op_types: OpTypes) -> OrderedSet[fx.Node]: + """Find all effective users of a node, where view ops extend the lifetime + of the original node. If a user is a view op, recursively find users of + the view.""" + effective_users: OrderedSet[fx.Node] = OrderedSet() + for user in node.users: + if user.op == "output": + continue + effective_users.add(user) + if op_types.is_view(user): + effective_users.update(_find_all_effective_users(user, op_types)) + return effective_users + + +@dataclass +class ReloadNodeInfo: + """ + Information about backward reload related nodes for each reload operation. + + Pattern: ao.reload → ao.wait_tensor + + - Reload group (ao.reload): Performs the actual asynchronous data transfer. + Can be moved earlier in the graph to overlap with computation. + - Wait node (ao.wait_tensor): Synchronization point that blocks until the data + transfer completes. Must remain at the point where the data is first needed. + """ + + reload_group_nodes: list[fx.Node] + wait_event_node: fx.Node + transfer_size_bytes: int + transfer_time_ms: float + + +@dataclass +class ReloadQueueEntry: + """ + Entry in the reload queue for prefetch scheduling. + + Attributes: + pattern: The reload pattern information + remaining_time_ms: Remaining overlap time needed in milliseconds + """ + + pattern: ReloadNodeInfo + remaining_time_ms: float + + +def offload_activation_fw(graph: fx.Graph) -> None: + """ + Insert CPU offload operations in the forward pass graph. + + Offload operations are placed after the last effective use of each tensor marked + for offloading. This ensures the tensor is no longer needed on the GPU before + transferring it to CPU memory. + + NOTE: An alternative approach would offload tensors immediately after generation + to maximize compute-communication overlap. However, this requires additional + synchronization to ensure tensor deletion (which occurs on the default stream) + waits for the asynchronous offload operation to complete. This would necessitate + more complex tracking to separate operation scheduling from memory cleanup. + + Args: + graph: The forward graph to modify + """ + + op_types: OpTypes = get_default_op_list() + + output_node: fx.Node = graph.find_nodes(op="output")[0] + # pyrefly: ignore [bad-assignment] + fwd_outputs: tuple[fx.Node, ...] = output_node.args[ + 0 + ] # pyrefly: ignore [bad-assignment] + node_to_offload: dict[fx.Node, fx.Node] = dict() + node_to_index: dict[fx.Node, int] = { + node: idx for idx, node in enumerate(graph.nodes) + } + + for node in fwd_outputs: + if node.meta.get("saved_for_offloading", False) is False: + continue + + # Find insertion point, which is the last use + if all_effective_users := _find_all_effective_users(node, op_types): + last_user = max(all_effective_users, key=lambda n: node_to_index[n]) + else: + last_user: fx.Node = node + + # Insert the CPU offload operation after the last user + with graph.inserting_after(last_user): + cpu_node: fx.Node = graph.call_function( + torch.ops.prims.device_put.default, + args=(node, torch.device("cpu")), + kwargs={"non_blocking": True}, + name=CPU_OFFLOAD_PREFIX + str(node.name), + ) + cpu_node.meta["val"] = node.meta["val"].to(torch.device("cpu")) + cpu_node.meta["tensor_meta"] = extract_tensor_metadata(cpu_node.meta["val"]) + + node_to_offload[node] = cpu_node + + # Update the return node args + output_node.update_arg( + 0, tuple(node_to_offload.get(node, node) for node in fwd_outputs) + ) + + +def reload_activation_bw(graph: fx.Graph) -> None: + """ + Insert GPU reload operations in the backward pass graph. + + Reload operations are placed before the first use of each offloaded tensor, + transferring it from CPU back to GPU memory before it's needed for computation. + + Args: + graph: The backward graph to modify + """ + + node_to_index: dict[fx.Node, int] = { + node: idx for idx, node in enumerate(graph.nodes) + } + output_node: fx.Node = graph.find_nodes(op="output")[0] + + for node in graph.find_nodes(op="placeholder"): + if node.meta.get("saved_for_offloading", False) is False: + continue + + # Find insertion point, which is the first use or output node if no users + # The later should not happen, but inserting before output node is safe + insert_point: fx.Node = ( + min(node.users.keys(), key=lambda n: node_to_index[n]) + if node.users + else output_node + ) + + # Insert the GPU reload operation before the first user + original_device: torch.Device = node.meta["original_device"] + with graph.inserting_before(insert_point): + gpu_node: fx.Node = graph.call_function( + torch.ops.prims.device_put.default, + args=(node, original_device), + kwargs={"non_blocking": True}, + name=str(node.name).replace(CPU_OFFLOAD_PREFIX, GPU_RELOAD_PREFIX), + ) + gpu_node.meta["val"] = node.meta["val"].to(original_device) + gpu_node.meta["tensor_meta"] = extract_tensor_metadata(gpu_node.meta["val"]) + + # Replace all uses of the CPU tensor with the GPU tensor + for user in list(node.users.keys()): + if user != gpu_node: + user.replace_input_with(node, gpu_node) + + +def offload_activation_fw_async(graph: fx.Graph) -> None: + """Insert async CPU offload operations in the forward pass graph. + + Uses ao.offload + ao.wait_tensor ops which encapsulate stream management + internally, producing a clean 2-node IR per offloaded tensor. + """ + + op_types: OpTypes = get_default_op_list() + + output_node: fx.Node = graph.find_nodes(op="output")[0] + # pyrefly: ignore [bad-assignment] + fwd_outputs: tuple[fx.Node, ...] = output_node.args[ + 0 + ] # pyrefly: ignore [bad-assignment] + node_to_offload: dict[fx.Node, fx.Node] = dict() + node_to_index: dict[fx.Node, int] = { + node: idx for idx, node in enumerate(graph.nodes) + } + + if not any(n.meta.get("saved_for_offloading", False) for n in fwd_outputs): + return + + for node in fwd_outputs: + if node.meta.get("saved_for_offloading", False) is False: + continue + + if all_effective_users := _find_all_effective_users(node, op_types): + last_user = max(all_effective_users, key=lambda n: node_to_index[n]) + else: + last_user: fx.Node = node + + with graph.inserting_after(last_user): + offload_node: fx.Node = graph.call_function( + torch.ops.ao.offload.default, + args=(node,), + name=f"async_{CPU_OFFLOAD_PREFIX}{node.name}", + ) + offload_node.meta["val"] = node.meta["val"].to(torch.device("cpu")) + offload_node.meta["tensor_meta"] = extract_tensor_metadata( + offload_node.meta["val"] + ) + # The keepalive=node arg extends the GPU tensor's lifetime in the + # graph so the allocator doesn't reclaim it before the async D2H + # copy completes. + with graph.inserting_after(offload_node): + wait_node: fx.Node = graph.call_function( + torch.ops.ao.wait_tensor.default, + args=(offload_node, node), + name=CPU_OFFLOAD_PREFIX + str(node.name), + ) + wait_node.meta["val"] = offload_node.meta["val"] + wait_node.meta["tensor_meta"] = offload_node.meta["tensor_meta"] + + node_to_offload[node] = wait_node + + output_node.update_arg( + 0, tuple(node_to_offload.get(node, node) for node in fwd_outputs) + ) + + +def reload_activation_bw_async(graph: fx.Graph) -> None: + """Insert async GPU reload operations in the backward pass graph. + + Uses ao.reload + ao.wait_tensor ops which encapsulate stream management internally, + producing a clean 2-node IR per reloaded tensor. + """ + + node_to_index: dict[fx.Node, int] = { + node: idx for idx, node in enumerate(graph.nodes) + } + + nodes_to_reload = [ + n + for n in graph.find_nodes(op="placeholder") + if n.meta.get("saved_for_offloading", False) + ] + if not nodes_to_reload: + return + + for node in nodes_to_reload: + if not node.users: + raise RuntimeError( + f"Offloaded tensor {node.name} has no users in the backward graph" + ) + insert_point: fx.Node = min(node.users.keys(), key=lambda n: node_to_index[n]) + + original_device: torch.device = node.meta["original_device"] + with graph.inserting_before(insert_point): + reload_node: fx.Node = graph.call_function( + torch.ops.ao.reload.default, + args=(node, original_device), + name=f"async_{str(node.name).replace(CPU_OFFLOAD_PREFIX, GPU_RELOAD_PREFIX)}", + ) + reload_node.meta["val"] = node.meta["val"].to(original_device) + reload_node.meta["tensor_meta"] = extract_tensor_metadata( + reload_node.meta["val"] + ) + wait_node: fx.Node = graph.call_function( + torch.ops.ao.wait_tensor.default, + args=(reload_node,), + name=str(node.name).replace(CPU_OFFLOAD_PREFIX, GPU_RELOAD_PREFIX), + ) + wait_node.meta["val"] = reload_node.meta["val"] + wait_node.meta["tensor_meta"] = reload_node.meta["tensor_meta"] + + for user in list(node.users.keys()): + if user != reload_node: + user.replace_input_with(node, wait_node) + + +def can_offload( + node: fx.Node, + fwd_outputs: OrderedSet[fx.Node], + model_outputs: OrderedSet[fx.Node], + static_lifetime_input_nodes: OrderedSet[fx.Node], +) -> bool: + """ + Determine if a node can be offloaded to CPU. + + Args: + node: The node to check + fwd_outputs: Forward module outputs, including model outputs and activations + model_outputs: Model outputs + + NOTE: Additional context for the logic behind these offloading checks: + + * fwd_outputs: Only saved intermediate tensors should be offloaded. + + * model_outputs / static_lifetime_input_nodes: Tensors that may be accessed outside + the compiled region (e.g., model outputs, static inputs) cannot be offloaded as + they must remain accessible beyond the scope of the compiled graph. + + * views / getitems: Offloading such nodes can lead to segmentation faults. + + * contiguous: Offloading non-contiguous tensors causes CPU-side stride changes + during both forward and backward passes when using the Inductor backend. While + these stride changes cancel each other out, they introduce significant compute + overhead. This is due to the contiguity check in ir.py (see link below). + TODO: This restriction could potentially be bypassed in the future. + Reference: https://github.com/pytorch/pytorch/blob/44ac69388a4a5eb463dbd2a13f00d1e3b924566c/torch/_inductor/ir.py#L3214 + + Additional criteria to consider for offloading optimization: + + * Tensor size: Small tensors may not fully utilize available bandwidth, reducing the + efficiency gains from offloading. + + * Position in forward/backward graph: Activations generated near the end of the forward + pass are typically consumed near the beginning of the backward pass. Offloading such + tensors may be counterproductive since they are quickly reloaded, not having sufficient + time to overlap the transfer with computation. + """ + + log.debug(f"Checking node {node.name} for offloading...") # noqa: G004 + + op_types: OpTypes = get_default_op_list() + + if node not in fwd_outputs: + log.debug("\tSkipped! Can only offload nodes in fwd_module_outputs.") + return False + if node in model_outputs: + log.debug("\tSkipped! Cannot offload model outputs.") + return False + if node in static_lifetime_input_nodes: + log.debug("\tSkipped! Cannot offload static input nodes.") + return False + if op_types.is_view(node): + log.debug("\tSkipped! Cannot offload views.") + return False + if node.target == operator.getitem: + log.debug("\tSkipped! Cannot offload getitems.") + return False + if hasattr(node, "meta") and "val" in node.meta: + if ( + isinstance(val := node.meta["val"], torch.Tensor) + and not val.is_contiguous() + ): + log.debug("\tSkipped! Cannot offload non-contiguous tensors.") + return False + + log.debug("\tGood!") + return True + + +def choose_offload_sets( + fwd_module: fx.GraphModule, + num_fwd_outputs: int, + static_lifetime_input_nodes: OrderedSet[fx.Node], +) -> bool: + """ + Decide which nodes will be offloaded based on the marked nodes and feasibility. + Marks nodes with "saved_for_offloading" if they should and can be offloaded. + + Args: + fwd_module: Forward graph module + bwd_module: Backward graph module + num_fwd_outputs: Number of forward outputs + + Returns: + bool: Whether activation offloading should be performed + """ + + fwd_outputs: OrderedSet[fx.Node] = OrderedSet( + fwd_module.graph.find_nodes(op="output")[0].args[0] + ) + model_outputs: OrderedSet[fx.Node] = OrderedSet( + fwd_module.graph.find_nodes(op="output")[0].args[0][:num_fwd_outputs] + ) + + should_perform_offloading = False + for node in fwd_module.graph.nodes: + if node.meta.get("should_offload", False) and can_offload( + node, fwd_outputs, model_outputs, static_lifetime_input_nodes + ): + node.meta["saved_for_offloading"] = True + node.meta["original_device"] = node.meta["val"].device + should_perform_offloading = True + + return should_perform_offloading + + +def offload_chosen_sets( + fwd_module: fx.GraphModule, + bwd_module: fx.GraphModule, +) -> None: + """ + Add offload and reload nodes to the forward and backward graphs. + This function adds device_put operations without any stream handling. + + Args: + fwd_module: Forward module graph + bwd_module: Backward module graph + """ + + # Add offload nodes in forward graph + offload_activation_fw(fwd_module.graph) + + # Update backward graph inputs to be offloaded tensors + bwd_inputs: dict[str, fx.Node] = { + node.name: node for node in bwd_module.graph.find_nodes(op="placeholder") + } + for fwd_node in fwd_module.graph.find_nodes(op="output")[0].args[0]: + if CPU_OFFLOAD_PREFIX not in fwd_node.name: + continue + + bwd_node: fx.Node = bwd_inputs[fwd_node.name.replace(CPU_OFFLOAD_PREFIX, "")] + with bwd_module.graph.inserting_after(bwd_node): + bwd_offload_node: fx.Node = bwd_module.graph.placeholder(name=fwd_node.name) + + bwd_offload_node.meta.update(fwd_node.meta) + bwd_offload_node.meta["saved_for_offloading"] = True + bwd_offload_node.meta["original_device"] = bwd_node.meta["val"].device + bwd_node.replace_all_uses_with(bwd_offload_node) + bwd_module.graph.erase_node(bwd_node) + + # Add reload nodes in backward graph + reload_activation_bw(bwd_module.graph) + + +def offload_chosen_sets_async( + fwd_module: fx.GraphModule, + bwd_module: fx.GraphModule, +) -> None: + """ + Add async offload and reload nodes using ao ops. + + Uses ao.offload/ao.reload + ao.wait_tensor which encapsulate stream management, + instead of device_put + explicit stream operations. Can be applied to + partitioned forward/backward graphs or to a joint graph produced by make_fx. + """ + + offload_activation_fw_async(fwd_module.graph) + + # Replace backward graph placeholders with their offloaded (CPU) counterparts. + # For each offloaded forward output, find the matching backward input and swap + # it with a new placeholder carrying the CPU tensor's metadata, then mark it + # for reloading. + bwd_inputs: dict[str, fx.Node] = { + node.name: node for node in bwd_module.graph.find_nodes(op="placeholder") + } + for fwd_node in fwd_module.graph.find_nodes(op="output")[0].args[0]: + if CPU_OFFLOAD_PREFIX not in fwd_node.name: + continue + + bwd_node: fx.Node = bwd_inputs[fwd_node.name.replace(CPU_OFFLOAD_PREFIX, "")] + with bwd_module.graph.inserting_after(bwd_node): + bwd_offload_node: fx.Node = bwd_module.graph.placeholder(name=fwd_node.name) + + bwd_offload_node.meta.update(fwd_node.meta) + bwd_offload_node.meta["saved_for_offloading"] = True + bwd_offload_node.meta["original_device"] = bwd_node.meta["val"].device + bwd_node.replace_all_uses_with(bwd_offload_node) + bwd_module.graph.erase_node(bwd_node) + + reload_activation_bw_async(bwd_module.graph) + + +def activation_offload_sink_wait_async(fwd_module: fx.GraphModule) -> None: + """Sink ao.wait_tensor operations for offload completion to the end of the graph. + + This allows computation to overlap with offload operations. + + NOTE: Sinking waits to the end delays GPU memory release of the source + tensor (kept alive via the wait's keepalive arg) until the end of the + compiled graph. For per-layer compile this is fine (one layer's worth of + memory), but for full-model compile this means offloaded GPU tensors are + not freed until the entire forward pass completes. + """ + graph: fx.Graph = fwd_module.graph + output_node: fx.Node = graph.find_nodes(op="output")[0] + + wait_nodes_to_sink: list[fx.Node] = [ + node + for node in graph.nodes + if node.op == "call_function" + and node.target == torch.ops.ao.wait_tensor.default + and isinstance(node.args[0], fx.Node) + and node.args[0].op == "call_function" + and node.args[0].target == torch.ops.ao.offload.default + ] + + # prepend moves the node from its current position (no manual removal needed) + for wait_node in wait_nodes_to_sink: + output_node.prepend(wait_node) + + +def activation_reload_prefetch_async(bwd_module: fx.GraphModule) -> None: + """ + Prefetch backward reload operations by moving ao.reload nodes earlier + in the graph to overlap data transfer with computation, while keeping + ao.wait_tensor at its original position. + """ + graph: fx.Graph = bwd_module.graph + nodes_list: list[fx.Node] = list(graph.nodes) + + # Identify reload + wait pairs + reload_patterns: dict[fx.Node, ReloadNodeInfo] = {} + for node in graph.nodes: + if not ( + node.op == "call_function" and node.target == torch.ops.ao.reload.default + ): + continue + wait_node = next( + (u for u in node.users if u.target == torch.ops.ao.wait_tensor.default), + None, + ) + if wait_node is None: + continue + transfer_size_bytes: int = _calculate_transfer_size(node) + transfer_time_ms: float = _estimate_transfer_time_in_ms(transfer_size_bytes) + reload_patterns[node] = ReloadNodeInfo( + reload_group_nodes=[node], + wait_event_node=wait_node, + transfer_size_bytes=transfer_size_bytes, + transfer_time_ms=transfer_time_ms, + ) + + reorder_for_prefetch(nodes_list, reload_patterns) + + +def _calculate_transfer_size(device_put_node: fx.Node) -> int: + """Calculate the size in bytes of data being transferred.""" + + # ao.offload(tensor) -> tensor at args[0] + # ao.reload(tensor, device) -> tensor at args[0] + if device_put_node.target in ( + torch.ops.ao.offload.default, + torch.ops.ao.reload.default, + ): + return _size_of(device_put_node.args[0]) # pyrefly: ignore [bad-argument-type] + raise ValueError(f"Unexpected transfer op: {device_put_node.target}") + + +def _estimate_transfer_time_in_ms(transfer_size_bytes: int) -> float: + """Estimate transfer time in milliseconds based on size and bandwidth. + + Uses config.activation_offload_cpu_gpu_bw (GB/s) which should be set by + the user to match their hardware. + """ + return ( + transfer_size_bytes / (1024**3) * 1_000 / config.activation_offload_cpu_gpu_bw + ) + + +def identify_reload_patterns( + graph: fx.Graph, nodes_list: list[fx.Node], node_to_idx: dict[fx.Node, int] +) -> dict[fx.Node, ReloadNodeInfo]: + """ + Identify backward reload patterns in the graph. + + Pattern: fork → wait_stream → device_put → record_event → join → wait_event + + This uses position-based matching since these nodes are inserted together in + add_backward_reload_stream_ops() in a specific order. Since stream operations + do not have data dependencies between them, they are unsuitable for subgroup + pattern matching type of checks. + + Returns a dict mapping device_put node to ReloadNodeInfo containing: + - reload_group_nodes: fork → wait_stream → device_put → record_event → join + - wait_event_node: the wait_event node + - transfer_size_bytes: size of data being transferred + - transfer_time_ms: estimated transfer time in milliseconds + """ + patterns: dict[fx.Node, ReloadNodeInfo] = {} + + # Find all GPU reload device_put nodes whose inputs are placeholder nodes + reload_nodes: list[fx.Node] = [ + node + for node in graph.find_nodes( + op="call_function", target=torch.ops.prims.device_put.default + ) + if GPU_RELOAD_PREFIX in node.name + and ( + node.args + and isinstance(node.args[0], fx.Node) + and node.args[0].op == "placeholder" + ) + ] + + # Extract patterns for each reload device_put node + for reload_node in reload_nodes: + reload_node_idx: int = node_to_idx[reload_node] + + fork_node: fx.Node = nodes_list[reload_node_idx - 2] + wait_stream_node: fx.Node = nodes_list[reload_node_idx - 1] + record_event_node: fx.Node = nodes_list[reload_node_idx + 1] + join_node: fx.Node = nodes_list[reload_node_idx + 2] + wait_event_node: fx.Node = nodes_list[reload_node_idx + 3] + + # Validate the nodes are what we expect + # Removed in follow-up commit + _validate_pattern_nodes( # noqa: F821 # pyrefly: ignore [unknown-name] + fork_node, + wait_stream_node, + record_event_node, + join_node, + wait_event_node, + ) + + # Calculate transfer size and time + transfer_size_bytes: int = _calculate_transfer_size(reload_node) + transfer_time_ms: float = _estimate_transfer_time_in_ms(transfer_size_bytes) + + patterns[reload_node] = ReloadNodeInfo( + reload_group_nodes=[ + fork_node, + wait_stream_node, + reload_node, + record_event_node, + join_node, + ], + wait_event_node=wait_event_node, + transfer_size_bytes=transfer_size_bytes, + transfer_time_ms=transfer_time_ms, + ) + + return patterns + + +def reorder_for_prefetch( + nodes_list: list[fx.Node], + reload_patterns: dict[fx.Node, ReloadNodeInfo], +) -> None: + """ + Reorder nodes to prefetch reload operations by directly manipulating the graph. + + This follows the algorithm as follows: + - Go through nodes in reverse order + - When encountering a reload pattern, add it to a queue with its transfer time + - When encountering a compute node, use its runtime to satisfy overlap requirements + - Place reload patterns when their overlap requirement is satisfied + - When encountering placeholder nodes, flush queue as reloads cannot move before inputs + """ + + # Build a set of all nodes in reload groups for quick lookup + reload_group_nodes_set: set[fx.Node] = set() + for pattern in reload_patterns.values(): + reload_group_nodes_set.update(pattern.reload_group_nodes) + + # Queue to hold reload group nodes waiting to be placed (FIFO) + reload_queue: list[ReloadQueueEntry] = [] + + # Loop through nodes in reverse + for node in reversed(nodes_list): + if node.op == "output": + continue + elif node.op == "placeholder": + # Flush queue - place all remaining reloads after the last placeholder + while reload_queue: + entry: ReloadQueueEntry = reload_queue.pop(0) + for reload_group_node in reversed(entry.pattern.reload_group_nodes): + node.append(reload_group_node) + break + elif node in reload_patterns: + pattern: ReloadNodeInfo = reload_patterns[node] + reload_queue.append( + ReloadQueueEntry( + pattern=pattern, remaining_time_ms=pattern.transfer_time_ms + ) + ) + elif node in reload_group_nodes_set: + continue + else: + if not reload_queue: + continue + compute_runtime_ms: float = ( + benchmark_node(node) if is_compute_node(node) else 0 + ) + reload_queue[0].remaining_time_ms -= compute_runtime_ms + + # Pop and place reload if its remaining time is satisfied (<= 0) + if reload_queue[0].remaining_time_ms <= 0: + entry: ReloadQueueEntry = reload_queue.pop(0) + for reload_group_node in entry.pattern.reload_group_nodes: + node.prepend(reload_group_node) + + +def activation_offload_sink_wait(fwd_module: fx.GraphModule) -> None: + """ + Sink wait_event operations for offload completion to the end of the graph. + + This function identifies wait_event nodes for offload completion and moves them + to the end of the graph, allowing computation to overlap with offload operations. + + Args: + fwd_module: Forward module graph + """ + graph: fx.Graph = fwd_module.graph + nodes_list: list[fx.Node] = list(graph.nodes) + node_to_idx: dict[fx.Node, int] = {node: idx for idx, node in enumerate(nodes_list)} + + # Find all CPU offload device_put nodes + offload_nodes: list[fx.Node] = [ + node + for node in graph.find_nodes( + op="call_function", target=torch.ops.prims.device_put.default + ) + if CPU_OFFLOAD_PREFIX in node.name + ] + + # Collect all wait_event nodes that need to be moved + wait_nodes_to_sink: list[fx.Node] = [] + for offload_node in offload_nodes: + offload_idx: int = node_to_idx[offload_node] + wait_event_node: fx.Node = nodes_list[offload_idx + 3] + + # Validate it's actually a wait_event node + if not ( + wait_event_node.op == "call_function" + and wait_event_node.target == torch.ops.streams.wait_event.default + ): + raise ValueError( + f"Expected wait_event node three positions after {offload_node.name}" + ) + + wait_nodes_to_sink.append(wait_event_node) + + # Find the output node, and move all wait_event nodes to just before the output node + output_node: fx.Node = graph.find_nodes(op="output")[0] + for wait_node in wait_nodes_to_sink: + output_node.prepend(wait_node) + + +def activation_reload_prefetch(bwd_module: fx.GraphModule) -> None: + """ + Prefetch backward reload operations by moving them earlier in the graph + to overlap communication with computation. + + This function identifies backward reload patterns (fork → wait_stream → device_put → + record_event → join) and moves them earlier in the execution order to overlap + the data transfer with computation, while keeping the wait_event at its original + position. + + Args: + bwd_module: Backward module graph + """ + graph: fx.Graph = bwd_module.graph + nodes_list: list[fx.Node] = list(graph.nodes) + node_to_idx: dict[fx.Node, int] = {node: idx for idx, node in enumerate(nodes_list)} + + # Step 1: Identify reload patterns + reload_patterns: dict[fx.Node, ReloadNodeInfo] = identify_reload_patterns( + graph, nodes_list, node_to_idx + ) + + # Step 2: Reorder nodes by directly manipulating the graph + reorder_for_prefetch(nodes_list, reload_patterns) + + +def enable_activation_offloading( + fwd_module: fx.GraphModule, + bwd_module: fx.GraphModule, + num_fwd_outputs: int, + static_lifetime_input_nodes: OrderedSet[fx.Node], +) -> None: + """ + Main entry point for activation offloading. + + Args: + fwd_module: Forward module graph + bwd_module: Backward module graph + num_fwd_outputs: Number of forward outputs + """ + + # Step 1: Decide which nodes to offload and mark them + should_perform_offloading: bool = choose_offload_sets( + fwd_module, + num_fwd_outputs, + static_lifetime_input_nodes, + ) + if not should_perform_offloading: + return + + # Step 2: Add offload and reload nodes to the graphs + if config.activation_offload_separate_stream: + # Use async ao ops (2 nodes each: offload/reload + wait_tensor) + offload_chosen_sets_async(fwd_module, bwd_module) + if config.activation_offload_sink_wait: + activation_offload_sink_wait_async(fwd_module) + if config.activation_reload_prefetch: + activation_reload_prefetch_async(bwd_module) + else: + # Use synchronous device_put (1 node each) + offload_chosen_sets(fwd_module, bwd_module) + + fwd_module.graph.lint() + bwd_module.graph.lint() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_offloading/offload_ops.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_offloading/offload_ops.py new file mode 100644 index 0000000000000000000000000000000000000000..5259b97adca3d6ea39b76e0f7f02306be7badeba --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_activation_offloading/offload_ops.py @@ -0,0 +1,180 @@ +"""Custom ops for async activation offloading between GPU and CPU. + +These ops encapsulate stream management internally, producing a clean 2-node +IR pattern (offload/reload + wait_tensor) similar to c10d functional collectives. + +A single dedicated transfer stream handles all D2H/H2D copies. +Completion events are keyed by output tensor data_ptr() and stored in a +module-level registry, so ``ao.wait_tensor`` takes only the tensor itself +(plus an optional keepalive). + +Offload pattern: + cpu_tensor = ao.offload(gpu_tensor) + cpu_tensor = ao.wait_tensor(cpu_tensor, gpu_tensor) + (keepalive arg extends gpu_tensor lifetime past the async D2H copy) + +Reload pattern: + gpu_tensor = ao.reload(cpu_tensor, device) + gpu_tensor = ao.wait_tensor(gpu_tensor) +""" + +import torch +from torch._library.custom_ops import custom_op +from torch.fx import has_side_effect + + +# --- Global transfer stream (one per device, lazily created) --- +_transfer_streams: dict[torch.device, torch.Stream] = {} + + +def _get_or_create_transfer_stream(device: torch.device) -> torch.Stream: + if device not in _transfer_streams: + _transfer_streams[device] = torch.Stream(device=device) + return _transfer_streams[device] + + +# --- Wait registry: maps data_ptr() -> (completion_event, device) --- +# Created by ao.offload / ao.reload, consumed (popped) by ao.wait_tensor. +# Not thread-safe — graph execution is single-threaded Python. +_wait_registry: dict[int, tuple[torch.Event, torch.device]] = {} + + +def _register_wait(tensor: torch.Tensor, device: torch.device) -> torch.Event: + """Create an event for an async transfer and register it for wait_tensor.""" + event = torch.Event() + _wait_registry[tensor.data_ptr()] = (event, device) + return event + + +def _pop_wait(tensor: torch.Tensor) -> tuple[torch.Event, torch.device]: + key = tensor.data_ptr() + try: + return _wait_registry.pop(key) + except KeyError: + raise RuntimeError( + f"ao.wait_tensor: no pending transfer for tensor with data_ptr={key}. " + "Every ao.wait_tensor must be paired with a preceding ao.offload or ao.reload." + ) from None + + +def _clear_wait_registry() -> None: + _wait_registry.clear() + + +@custom_op("ao::offload", mutates_args=()) +def offload(tensor: torch.Tensor) -> torch.Tensor: + """Async offload a GPU tensor to CPU on the dedicated transfer stream. + + Callers MUST pair this with an ``ao.wait_tensor`` that passes the source GPU + tensor as ``keepalive`` to extend its lifetime past the async D2H copy. + Do NOT use ``record_stream`` — it causes memory fragmentation and + unbounded memory growth. + + Uses pinned-memory allocation + copy_ so the transfer is compatible + with CUDA graph capture. + """ + device = tensor.device + transfer_stream = _get_or_create_transfer_stream(device) + current_stream = torch.accelerator.current_stream(device) + + transfer_stream.wait_stream(current_stream) + + torch.accelerator.set_stream(transfer_stream) + result = torch.empty_like(tensor, device="cpu", pin_memory=True) + completion_event = _register_wait(result, device) + result.copy_(tensor, non_blocking=True) + transfer_stream.record_event(completion_event) + torch.accelerator.set_stream(current_stream) + + return result + + +@offload.register_fake +def _(tensor: torch.Tensor) -> torch.Tensor: + return torch.empty_like(tensor, device="cpu") + + +@custom_op("ao::reload", mutates_args=()) +def reload( + tensor: torch.Tensor, + device: torch.device, +) -> torch.Tensor: + """Async reload a CPU tensor to GPU on the dedicated transfer stream. + + The GPU tensor is allocated on the compute stream to avoid cross-stream + allocator ownership issues. The H2D copy runs on the transfer stream. + The completion event is keyed by the output tensor's data_ptr. + """ + transfer_stream = _get_or_create_transfer_stream(device) + current_stream = torch.accelerator.current_stream(device) + + # Allocate on compute stream so the allocator tracks ownership correctly + result = torch.empty_like(tensor, device=device) + completion_event = _register_wait(result, device) + + transfer_stream.wait_stream(current_stream) + + torch.accelerator.set_stream(transfer_stream) + result.copy_(tensor, non_blocking=True) + transfer_stream.record_event(completion_event) + torch.accelerator.set_stream(current_stream) + + return result + + +@reload.register_fake +def _( + tensor: torch.Tensor, + device: torch.device, +) -> torch.Tensor: + return torch.empty_like(tensor, device=device) + + +# ao::wait_tensor is defined via torch.library with an aliasing schema so the +# output can alias the input (custom_op forbids this). +# +# Uses CompositeExplicitAutograd (single impl for all devices) because the +# offload case has mixed-device args: ``tensor`` is CPU (the offload result) +# while ``keepalive`` is CUDA (the source GPU tensor). A single impl avoids +# relying on device-priority dispatch ordering. +# +# Synchronization details (completion event, device) are looked up from +# ``_wait_registry`` keyed on ``tensor.data_ptr()``. +# +# ``keepalive`` is not read by the op — its sole purpose is to create a graph +# dependency that extends the tensor's lifetime in the FX graph. For offload, +# this keeps the source GPU tensor alive until the compute stream has waited +# on the D2H completion event, preventing the allocator from reclaiming it +# while the async copy is still in flight. +_lib = torch.library.Library("ao", "DEF") +_lib.define("wait_tensor(Tensor(a) tensor, Tensor? keepalive=None) -> Tensor(a)") + + +@torch.library.impl("ao::wait_tensor", "CompositeExplicitAutograd") +def _ao_wait_tensor( + tensor: torch.Tensor, + keepalive: torch.Tensor | None = None, +) -> torch.Tensor: + completion_event, device = _pop_wait(tensor) + current_stream = torch.accelerator.current_stream(device) + current_stream.wait_event(completion_event) + return tensor + + +@torch.library.register_fake("ao::wait_tensor") +def _ao_wait_tensor_fake( + tensor: torch.Tensor, + keepalive: torch.Tensor | None = None, +) -> torch.Tensor: + return tensor + + +has_side_effect(torch.ops.ao.wait_tensor.default) + + +def wait_tensor( + tensor: torch.Tensor, + keepalive: torch.Tensor | None = None, +) -> torch.Tensor: + """Callable wrapper so ``wait_tensor`` can be imported by name for op registration.""" + return torch.ops.ao.wait_tensor.default(tensor, keepalive) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..10a55772ab58b21573a6eba0356ddd3080164ac7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/aot_autograd_result.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/aot_autograd_result.py new file mode 100644 index 0000000000000000000000000000000000000000..83ab2240b01c9da6a3485c189be239efa0b6feb8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/aot_autograd_result.py @@ -0,0 +1,737 @@ +""" +This module provides result classes for AOT Autograd compilation. + +Similar to how torch._inductor.output_code provides OutputCode classes for inductor +compilation results, this module provides AOTAutogradResult classes that represent +the compiled artifacts produced by AOT Autograd. + +These results are: +- Serializable: can be saved/loaded from disk without recompilation +- Addressable: can be stored in caches with keys for later retrieval +- Reusable: can be used for both caching and ahead-of-time compilation (precompile) + +The main result types are: +- GenericAOTAutogradResult: Abstract base for all AOT Autograd results +- AOTAutogradResult: Regular result that references FxGraphCache entries +- BundledAOTAutogradResult: Result that bundles the entire compiled code directly +""" + +from __future__ import annotations + +import json +import logging +from abc import ABC, abstractmethod +from collections.abc import Callable, Sequence +from copy import copy +from dataclasses import dataclass +from typing import Any, Generic, TYPE_CHECKING, TypeVar + +import torch +from torch._dynamo.precompile_context import BackendCacheArtifact +from torch._inductor.codecache import FxGraphCache +from torch._inductor.output_code import ( + CompiledFxGraph, + CompiledFxGraphConstants, + OutputCode, +) +from torch._inductor.utils import should_use_remote_fx_graph_cache +from torch._logging import getArtifactLogger + +from .runtime_wrappers import ( + AOTDispatchAutograd, + AOTDispatchAutogradCompileSpec, + AOTDispatchSubclassWrapper, + CachedAutogradLazyBackwardCompileInfo, + CompilerWrapper, + FunctionalizedRngRuntimeWrapper, + post_compile, + RuntimeWrapper, + SerializableCompiledFunction, + SubclassMeta, +) +from .schemas import AOTAutogradCacheInfo # noqa: F401 +from .utils import simple_wraps + + +if TYPE_CHECKING: + from torch._inductor.compile_fx import _CompileFxKwargs + + from .schemas import AOTConfig, ViewAndMutationMeta + +log = logging.getLogger(__name__) +aot_graphs_log = getArtifactLogger(__name__, "aot_graphs") + + +TOut = TypeVar("TOut", bound=OutputCode) + + +class InductorOutput(ABC, Generic[TOut]): + """ + Class representing a single inductor output + """ + + @abstractmethod + def pre_save(self) -> None: ... + + @abstractmethod + def load(self, example_inputs: Sequence[Any]) -> TOut: ... + + @abstractmethod + def post_compile(self, result: TOut, fx_config: _CompileFxKwargs) -> TOut: ... + + +TOutputCode = TypeVar("TOutputCode", bound=OutputCode) + + +@dataclass +class BundledOutputCodeLoadable(InductorOutput[TOutputCode], Generic[TOutputCode]): + """ + A generic wrapper for OutputCode objects that are bundled directly in the cache + (rather than looked up via FxGraphCache). + + This works for any OutputCode subclass (CompiledFxGraph, RegionalOutputCode, etc.) + """ + + result: TOutputCode + + def pre_save(self) -> None: + disk_result = copy(self.result) + disk_result.prepare_for_serialization() + self.result = disk_result + return + + def load(self, example_inputs: Sequence[Any]) -> TOutputCode: + self.example_inputs = example_inputs + return self.result + + def post_compile( + self, result: TOutputCode, fx_config: _CompileFxKwargs + ) -> TOutputCode: + constants = CompiledFxGraphConstants() + + # Special handling for CompiledFxGraph - needs FxGraphCache.cache_hit_post_compile + if isinstance(result, CompiledFxGraph): + graph, cache_info = FxGraphCache.cache_hit_post_compile( + result, {}, constants + ) + if graph is None: + raise RuntimeError("Failed to reload cache entry from disk") + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "fx_graph_bundled_cache_hit", # always a hit + "encoding": "json", + }, + payload_fn=lambda: json.dumps(cache_info), + ) + result = graph # type: ignore[assignment] + result.compile_region_name = ( # pyrefly: ignore[missing-attribute] + fx_config.get("compile_region_name") + ) + + # Run normal post compile + result.post_compile(self.example_inputs, constants, fx_config) + + # Let the CUDAGraph policy do outer-level wrapping (e.g. wrapping + # an entire RegionalOutputCode as a single CUDA graph instead of + # per-inner-region). + import torch._inductor.config as _inductor_config + + policy = _inductor_config.cudagraph_policy + if policy is not None: + result = policy.wrap_output(result) + + return result + + +# Backwards compatibility alias +CompiledFxGraphLoadable: type[BundledOutputCodeLoadable[CompiledFxGraph]] = ( + BundledOutputCodeLoadable[CompiledFxGraph] +) + + +@dataclass +class FxGraphCacheLoadable(InductorOutput[CompiledFxGraph]): + fx_graph_cache_info: tuple[str, list[str]] + fx_graph_guard_expr: str | None + + def pre_save(self) -> None: + return + + def _is_backward(self) -> bool: + return False + + def load(self, example_inputs: Sequence[Any]) -> CompiledFxGraph: + from .autograd_cache import FXGraphCacheMiss + + # [Note: AOTAutogradCache and FXGraphCache Guard interactions] + # As mentioned, AOTAutograd takes in the symint inputs from dynamo's list of arguments. + # FXGraphCache serializes guards that are needed in the shape_env based on these symint inputs to the graph. + # The invariant that AOTAutograd uses here is that the sources for symints given to it by dynamo are exactly + # the same as the ones it passes to inductor, for both the forward and backward passes. + # (This does not mean that the tensor values passed in are the same: only that their symints are). + # That is, AOTAutograd and Inductor never create new guards based on symints with different sources + # than those passed to it by inductor. + # We pass the post compile function, which sets various fx_config boxed values, + # so we can call it only after we're sure both forward and backward have + # Clear CompiledTritonKernels before loading from FXGraphCache + torch._inductor.async_compile.CompiledTritonKernels.cache_clear() + remote_cache = None + constants = CompiledFxGraphConstants() + if should_use_remote_fx_graph_cache(): + remote_cache = FxGraphCache.get_remote_cache() + (cache_key, debug_lines) = self.fx_graph_cache_info + + def check_exact_guard_match(guard_expr: str, _hints: Any) -> bool: + """ + AOTAutogradCache tracks its own guards, so we just need to treat these guard expressions as a second + cache key of sorts: we just check for equality, i.e. the FXGraphCache entry with + the exact same guards as we originally saved into the cache. + """ + return guard_expr == self.fx_graph_guard_expr + + result, cache_info = FxGraphCache.load_with_key( + cache_key, + debug_lines, + example_inputs, + local=True, + remote_cache=remote_cache, + is_backward=self._is_backward(), + constants=constants, + evaluate_guards=check_exact_guard_match, + ) + if result is None: + log.info("FXGraphCache cache miss for key %s", self.fx_graph_cache_info) + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "fx_graph_cache_miss", # always a hit + "encoding": "json", + }, + payload_fn=lambda: json.dumps(cache_info), + ) + + raise FXGraphCacheMiss + + # No need to log chromium event because AOTAutograd will log that immediately for us + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "fx_graph_cache_hit", # always a hit + "encoding": "json", + }, + payload_fn=lambda: json.dumps(cache_info), + ) + self.example_inputs = example_inputs + self.constants = constants + return result + + def post_compile( + self, result: CompiledFxGraph, fx_config: _CompileFxKwargs + ) -> CompiledFxGraph: + """ + Called after FXGraphCacheLoadable.load, mutates fx_config + """ + result.compile_region_name = fx_config.get( # pyrefly: ignore[bad-assignment] + "compile_region_name" + ) + result.post_compile(self.example_inputs, self.constants, fx_config) + + import torch._inductor.config as _inductor_config + + policy = _inductor_config.cudagraph_policy + if policy is not None: + result = policy.wrap_output(result) + + return result + + +@dataclass +class CompiledForward(FxGraphCacheLoadable): + """ + Cacheable entry for a forward function + """ + + def _is_backward(self) -> bool: + return False + + +@dataclass +class GenericCompiledBackward(InductorOutput[TOut]): + # Used by AOTDispatchAutograd.post_compile + backward_state_indices: list[int] + num_symints_saved_for_bw_: int + + def post_compile(self, result: TOut, fx_config: _CompileFxKwargs) -> TOut: + # The concrete post_compile comes from the loadable mixin in each subclass MRO. + compiled_bw = super().post_compile( # pyrefly: ignore[missing-attribute] + result, fx_config + ) + # See note [Wrapping bw_compiler in disable] + # This is done by _wrapped_bw_compiler in torch/_dynamo/backends/common.py + # But since on cache hit we do not call the bw_compiler, we need to reapply the disable + return torch._dynamo.disable( # type: ignore[return-value] + compiled_bw, reason="do not trace generated backwards pass" + ) + + +@dataclass +class CompiledBackward(GenericCompiledBackward[CompiledFxGraph], FxGraphCacheLoadable): + """ + Cacheable entry for a backward function + """ + + def _is_backward(self) -> bool: + return True + + +# Generic bundled forward/backward classes that work with any OutputCode type +@dataclass +class BundledCompiledForward( + BundledOutputCodeLoadable[TOutputCode], Generic[TOutputCode] +): + """ + Generic forward function for bundled compilation. + Works with any OutputCode type (CompiledFxGraph, RegionalOutputCode, etc.) + """ + + +@dataclass +class BundledCompiledBackward( + GenericCompiledBackward[TOutputCode], + BundledOutputCodeLoadable[TOutputCode], + Generic[TOutputCode], +): + """ + Generic backward function for bundled compilation. + Works with any OutputCode type (CompiledFxGraph, RegionalOutputCode, etc.) + """ + + +@dataclass +class SerializedGraphModule: + fn: Callable[[dict[Any, Any], str], torch.nn.Module] + args: tuple[Any, ...] + + def __init__(self, gm: torch.fx.GraphModule) -> None: + self.fn, self.args = gm.__reduce__() + + def deserialize(self) -> torch.fx.GraphModule: + gm = self.fn(*self.args) + if not isinstance(gm, torch.fx.GraphModule): + raise AssertionError(f"expected fx.GraphModule, got {type(gm)}") + return gm + + +def serialize_graph_module(gm: torch.fx.GraphModule) -> SerializedGraphModule: + # NOTE: mutates the graph module + gm.meta = {} + for node in gm.graph.nodes: + # pyrefly: ignore [implicit-any] + node.meta = {} + return SerializedGraphModule(gm) + + +TForward = TypeVar("TForward", bound="InductorOutput[Any]") +TBackward = TypeVar("TBackward", bound="GenericCompiledBackward[Any]") + + +@dataclass +class GenericAOTAutogradResult(Generic[TForward, TBackward]): + """A single result from AOT Autograd compilation, genericized by Forward and Backward types. + + A TForward is always an InductorOutput of some sort, which represents the + forward graph of the compile. + A TBackward is an InductorOutput + metadata about the backward, useful for specific + backward-only wrappers. This type is encapsulated by GenericCompiledBackward. + + Each AOTAutogradResult is essentially parameterized by 1. the method of loading + from the cache (either Bundled or UnBundled), and 2. The type of the output. For now, + the only type of output we support is Python Wrapper output, i.e. OutputCode.CompiledFxGraph, + but the same technique works for C++ wrapper code; we'd just add an extra InductorOutput type. + """ + + # Forward and Backward info + compiled_fw: TForward + compiled_bw: TBackward | None + + # Code of the joint graph using print_readable() + # Used for logging purposes + aot_joint_graph_str: str | None + aot_forward_graph_str: str | None + aot_backward_graph_str: str | None + + # Runtime_metadata saved right before compilation + runtime_metadata: ViewAndMutationMeta + + # Wrappers that run after each aot_dispatch_* function + dispatch_wrappers: list[CompilerWrapper] + + # Used by AOTSubclassWrapper + maybe_subclass_meta: SubclassMeta | None + num_fw_outs_saved_for_bw: int | None + + # Used by RuntimeWrapper + indices_of_inps_to_detach: list[int] + + # Time taken to trace/compile the forward + # forward_time_taken includes AOTAutograd tracing time + inductor compilation time + # backward_time_taken is essentially just the time inductor took to compile + forward_time_taken_ns: int + backward_time_taken_ns: int + + # Used by standalone_compile + sanitized_aot_config: AOTConfig + + guards_expr: str | None + + # Used by Compiled Autograd + serialized_bw_module: SerializedGraphModule | None + + def pre_save(self) -> None: + """ + Perform any preparations to make the result ready for serialization. + """ + self.compiled_fw.pre_save() + if self.compiled_bw is not None: + self.compiled_bw.pre_save() + + def _log_cached_graphs(self, aot_config: AOTConfig) -> None: + if not aot_config.enable_log: + return + + if self.aot_joint_graph_str is not None: + torch._logging.trace_structured( + "aot_joint_graph", payload_fn=lambda: self.aot_joint_graph_str + ) + aot_graphs_log.info( + "Joint graph (from cache)\n\n%s", self.aot_joint_graph_str + ) + + if self.aot_forward_graph_str is not None: + from torchgen.utils import dataclass_repr + + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "aot_forward_graph_fw_metadata", + "encoding": "string", + }, + payload_fn=lambda: dataclass_repr(self.runtime_metadata), + ) + if self.maybe_subclass_meta is not None: + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "aot_forward_graph_fw_subclass_metadata", + "encoding": "string", + }, + payload_fn=lambda: dataclass_repr(self.maybe_subclass_meta), + ) + + # It's called an inference graph if not running with autograd + has_backward = self.aot_backward_graph_str is not None + torch._logging.trace_structured( + "aot_forward_graph" if has_backward else "aot_inference_graph", + payload_fn=lambda: self.aot_forward_graph_str, + ) + aot_graphs_log.info( + "Forward graph (from cache)\n\n%s", + self.aot_forward_graph_str, + ) + + if self.aot_backward_graph_str is not None: + torch._logging.trace_structured( + "aot_backward_graph", payload_fn=lambda: self.aot_backward_graph_str + ) + aot_graphs_log.info( + "Backward graph (from cache)\n\n%s", + self.aot_backward_graph_str, + ) + + def _load_and_post_compile( + self, + args: list[torch.Tensor], + fx_config: _CompileFxKwargs, + ) -> tuple[Callable[..., Any], Callable[..., Any] | None, bool]: + from torch._dynamo.utils import CompileEventLogger + + compiled_fw_func = self.compiled_fw.load(args) + if self.compiled_bw is not None: + compiled_bw_func = self.compiled_bw.load(args) + needs_autograd = True + CompileEventLogger.try_add_pt2_compile( + "backend_compile", dispatch_mode="autograd" + ) + # Now that we've loaded forward and backward, call post compile on both + # This avoids setting things like BoxedBools in fx_config until + # after both forward and backward cache hit + fw_fx_config: _CompileFxKwargs = { + **fx_config, + "is_backward": False, + } + bw_fx_config: _CompileFxKwargs = { + **fx_config, + "is_backward": True, + } + compiled_fw_func = self.compiled_fw.post_compile( + compiled_fw_func, fw_fx_config + ) + compiled_bw_func = self.compiled_bw.post_compile( + compiled_bw_func, bw_fx_config + ) + return compiled_fw_func, compiled_bw_func, needs_autograd + + inference_fx_config: _CompileFxKwargs = { + **fx_config, + "is_backward": False, + } + + needs_autograd = False + CompileEventLogger.try_add_pt2_compile( + "backend_compile", dispatch_mode="inference" + ) + compiled_fw_func = self.compiled_fw.post_compile( + compiled_fw_func, inference_fx_config + ) + return compiled_fw_func, None, needs_autograd + + def _apply_runtime_wrappers( + self, + compiled_fw_func: Callable[..., Any], + compiled_bw_func: Callable[..., Any] | None, + needs_autograd: bool, + aot_config: AOTConfig, + ) -> Callable[..., Any]: + from torch._dynamo.utils import CompileEventLogger + + compiled_fw_func = AOTDispatchSubclassWrapper( + trace_joint=needs_autograd, + fw_only=None, + maybe_subclass_meta=self.maybe_subclass_meta, + num_fw_outs_saved_for_bw=self.num_fw_outs_saved_for_bw, + ).post_compile( + compiled_fw_func, aot_config, runtime_metadata=self.runtime_metadata + ) + + req_subclass_dispatch = self.maybe_subclass_meta is not None + CompileEventLogger.try_add_pt2_compile( + "backend_compile", requires_subclass_dispatch=req_subclass_dispatch + ) + + # In autograd case, functionalizedRngWrapper should not modify outs + return_new_outs = not needs_autograd + compiled_fw_func = FunctionalizedRngRuntimeWrapper( + return_new_outs=return_new_outs + ).post_compile( + compiled_fw_func, aot_config, runtime_metadata=self.runtime_metadata + ) + # pyrefly: ignore [missing-attribute] + compiled_fw_func._boxed_call = True + disable_amp = torch._C._is_any_autocast_enabled() + + if needs_autograd: + if self.compiled_bw is None: + raise AssertionError("compiled_bw must not be None when needs_autograd") + + cached_lazy_backward = None + if self.serialized_bw_module is not None: + cached_lazy_backward = CachedAutogradLazyBackwardCompileInfo( + self.serialized_bw_module.deserialize + ) + # This function is run on both cache miss and cache hit, either here + # or in aot_dispatch_autograd. On a cache hit, + # 1. the bw is already compiled + # 2. we don't need to save to the cache again + # so those corresponding arguments are set to None. + compile_spec = AOTDispatchAutogradCompileSpec( + compiled_fw_func=compiled_fw_func, + compiled_bw_func=compiled_bw_func, + maybe_subclass_meta=self.maybe_subclass_meta, + num_symints_saved_for_bw=self.compiled_bw.num_symints_saved_for_bw_, + backward_state_indices=self.compiled_bw.backward_state_indices, + disable_amp=disable_amp, + indices_of_inps_to_detach=self.indices_of_inps_to_detach, + lazy_backward_info=cached_lazy_backward, + aot_config=aot_config, + fw_metadata=self.runtime_metadata, + try_save_cache_entry=None, + ) + compiled_function = AOTDispatchAutograd.post_compile(compile_spec) + + else: + compiled_function = RuntimeWrapper( + indices_of_inps_to_detach=self.indices_of_inps_to_detach, + trace_joint=False, + disable_amp=disable_amp, + ).post_compile( + compiled_fw_func, aot_config, runtime_metadata=self.runtime_metadata + ) + + # Add serialization function back onto object + compiled_function, _ = post_compile( + self.dispatch_wrappers, + compiled_function, + aot_config, + runtime_metadata=self.runtime_metadata, + ) + return compiled_function + + def _check_guards(self, args: list[torch.Tensor]) -> None: + if self.guards_expr: + from .autograd_cache import AOTAutogradCache + + symints = AOTAutogradCache._filter_backed_symints(args) + check = bool(AOTAutogradCache.evaluate_guards(self.guards_expr, symints)) + if check is not True: + raise AssertionError(f"guards check failed: {check}") + + # Turn result into the original callable + def wrap_post_compile( + self, + args: list[torch.Tensor], + aot_config: AOTConfig, + fx_config: _CompileFxKwargs, + ) -> Callable[..., Any]: + """ + This function takes a result and carefully reconstructs the original callable + that AOTAutograd returned the first time it was run. It does this by running the various + post compile steps that AOTAutograd runs on its compiled artifact after running the fw/bw compilers. + + In the inference path, this consists of the Subclass, FunctionalzedRngRuntime, and RuntimeWrappers. + In the autograd path, this consists of AOTAutogradDispatch.post_compile. + + The steps here should match exactly the steps that are run in aot_dispatch_base and aot_dispatch_autograd. + + Notably absent from the cached path are: + - DebugAssertWrapper + - FakifiedOutWrapper + + Which we'll handle separately later on, if necessary. + """ + from torch._dynamo.utils import dynamo_timed + + self._log_cached_graphs(aot_config) + with dynamo_timed("AOTAutogradCache.inductor_load"): + compiled_fw_func, compiled_bw_func, needs_autograd = ( + self._load_and_post_compile(args, fx_config) + ) + + compiled_function = self._apply_runtime_wrappers( + compiled_fw_func, compiled_bw_func, needs_autograd, aot_config + ) + # Now that we're pretty sure it's a successful load, add guards + # to the existing shape environment from the cache. + self._check_guards(args) + return compiled_function + + +class AOTAutogradResult(GenericAOTAutogradResult[CompiledForward, CompiledBackward]): + """ + Regular AOTAutogradResult: saves the forward/backward FxGraphCache keys + and looks them up in FxGraphCache on load + """ + + +class BundledAOTAutogradResult( + GenericAOTAutogradResult[ + BundledCompiledForward[TOutputCode], BundledCompiledBackward[TOutputCode] + ], + Generic[TOutputCode], +): + """ + Generic AOTAutogradResult where we bundle the entire OutputCode directly + (rather than looking it up via FxGraphCache). + + This works with any OutputCode type: + - CompiledFxGraph: Traditional inductor compilation + - RegionalOutputCode: Regional inductor compilation with GraphPickler serialization + - Any future OutputCode subclasses + + Type parameter: + TOutputCode: The OutputCode subclass (e.g., CompiledFxGraph, RegionalOutputCode) + + Usage with CompiledFxGraph: + entry = BundledAOTAutogradResult[CompiledFxGraph]( + compiled_fw=BundledCompiledForward(result=CompiledFxGraph(...)), + compiled_bw=BundledCompiledBackward( + result=CompiledFxGraph(...), + backward_state_indices=[...], + num_symints_saved_for_bw_=..., + ), + ... + ) + + Usage with RegionalOutputCode: + entry = BundledAOTAutogradResult[RegionalOutputCode]( + compiled_fw=BundledCompiledForward(result=RegionalOutputCode(gm)), + compiled_bw=BundledCompiledBackward( + result=RegionalOutputCode(gm), + backward_state_indices=[...], + num_symints_saved_for_bw_=..., + ), + ... + ) + """ + + +def deserialize_bundled_cache_entry( + entry: BundledAOTAutogradResult[Any], +) -> Callable[..., Any]: + from copy import deepcopy + + from torch._inductor.cudagraph_utils import BoxedDeviceIndex + from torch._inductor.utils import BoxedBool + + # In the precompile use case, guards are already serialized + # by dynamo, so we don't need to add them to the environment + entry.guards_expr = None + # TODO: this isn't exactly right, because cudagraphs needs to be a shared config + # which is set by compile_fx. But in precompile, we never actually call compile_fx + # so we don't have a place to track cudagraphs here. + cudagraphs = BoxedBool(torch._inductor.config.triton.cudagraphs) + boxed_forward_device_index = BoxedDeviceIndex(None) + # We need to make a clean copy of the cache entry + # in case it needs to be serialized again + serializable_copy = deepcopy(entry) + + from torch._subclasses import FakeTensorMode + from torch.fx.experimental.symbolic_shapes import ShapeEnv + + context = torch._guards.TracingContext.try_get() + if context is None: + # Create a clean environment when running fx graph post compile + # if one is not available + context = torch._guards.TracingContext(FakeTensorMode(shape_env=ShapeEnv())) + with torch._guards.tracing(context): + compiled_fn = entry.wrap_post_compile( + [], + entry.sanitized_aot_config, + { + "cudagraphs": cudagraphs, + "boxed_forward_device_index": boxed_forward_device_index, + }, + ) + # Ensure the deserialized cache entry is still serializable + + compiled_fn = SerializableCompiledFunction(compiled_fn, lambda: serializable_copy) + + # TODO: this ignores flat_params, which can exist + # if inline_builtin_nn_modules=False + @simple_wraps(compiled_fn) + def forward(*runtime_args: Any) -> Any: + return compiled_fn(list(runtime_args)) + + if not hasattr(compiled_fn, "serialize"): + raise AssertionError("compiled_fn must have serialize attribute") + forward.serialize = compiled_fn.serialize # type: ignore[attr-defined] + + return forward + + +@dataclass +# pyrefly: ignore [implicit-any] +class BundledAOTAutogradCacheArtifact(BackendCacheArtifact[Callable]): + # pyrefly: ignore [implicit-any] + def after_deserialization(self) -> Callable: + return deserialize_bundled_cache_entry(self.content) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/autograd_cache.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/autograd_cache.py new file mode 100644 index 0000000000000000000000000000000000000000..08843076fe01e377a9dc0430442b6f53bceb0461 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/autograd_cache.py @@ -0,0 +1,1395 @@ +""" +Utils for caching the outputs of AOTAutograd +""" + +from __future__ import annotations + +import base64 +import contextlib +import functools +import hashlib +import json +import logging +import os +import pickle +import random +import shutil +import time +import traceback +from copy import copy +from typing import Any, TYPE_CHECKING +from typing_extensions import override + +import torch +from torch._dynamo.precompile_context import PrecompileContext +from torch._dynamo.trace_rules import torch_non_c_binding_in_graph_functions +from torch._dynamo.utils import ( + chromium_event_log_active, + CompileEventLogger, + counters, + warn_once, +) +from torch._functorch import config +from torch._inductor.codecache import ( + _ident, + add_ephemeral_timeout_increase_for_distributed, + BypassFxGraphCache, + create_cache, + extract_tensor_metadata_for_cache_key, + FxGraphCache, + FxGraphCachePickler, + FxGraphHashDetails, + GuardedCache, + sha256_hash, + write_atomic, +) +from torch._inductor.custom_graph_pass import ( + CustomKnapsackSolver, + CustomRuntimeEstimator, +) +from torch._inductor.output_code import OutputCode +from torch._inductor.runtime.runtime_utils import cache_dir +from torch._inductor.utils import BoxedBool, should_use_remote_fx_graph_cache +from torch._library.fake_class_registry import FakeScriptObject +from torch._logging import LazyString +from torch._utils_internal import log_cache_bypass +from torch.compiler._cache import ( + CacheArtifact, + CacheArtifactFactory, + CacheArtifactManager, +) +from torch.fx.experimental.symbolic_shapes import guarding_hint_or_throw +from torch.fx.node import Node +from torch.utils._triton import has_triton_package + +from .aot_autograd_result import ( + AOTAutogradResult, + BundledAOTAutogradCacheArtifact, + BundledAOTAutogradResult, + BundledCompiledBackward, + BundledCompiledForward, + CompiledBackward, + CompiledForward, + GenericAOTAutogradResult, + SerializedGraphModule, +) +from .runtime_wrappers import ( + CompilerWrapper, + SerializableCompiledFunction, + SubclassMeta, +) +from .schemas import AOTAutogradCacheInfo, AOTConfig, ViewAndMutationMeta # noqa: F401 + + +if TYPE_CHECKING: + from collections.abc import Callable, Generator, Sequence + + from torch._inductor.compile_fx import _CompileFxKwargs, CompilerConfigExtra + from torch._inductor.remote_cache import JsonDataTy, RemoteCache + + +log = logging.getLogger(__name__) + + +class BypassAOTAutogradCache(Exception): + pass + + +# Used to signify when FXGraphCache missed when AOTAutogradCache uses it +class FXGraphCacheMiss(BypassAOTAutogradCache): + pass + + +def should_use_remote_autograd_cache() -> bool: + if torch.compiler.config.force_disable_caches: + return False + if config.enable_remote_autograd_cache is not None: + return config.enable_remote_autograd_cache + if not config.is_fbcode(): + return False + + if torch._utils_internal.is_fb_unit_test(): + return False + + try: + from torch._inductor.fb.remote_cache import REMOTE_CACHE_VERSION + except ModuleNotFoundError: + return False + + jk_name = "pytorch/remote_cache:aot_autograd_cache_version" + + return REMOTE_CACHE_VERSION >= torch._utils_internal.justknobs_getval_int(jk_name) + + +def should_use_local_autograd_cache() -> bool: + if torch.compiler.config.force_disable_caches: + return False + return config.enable_autograd_cache + + +def should_bundle_autograd_cache() -> bool: + return config.bundled_autograd_cache or torch._dynamo.config.caching_precompile + + +def check_node_safe(node: Node) -> None: + """ + Checks that the node only uses supported operators. We are starting with very + conservative cacheability constraints, and incrementally adding more support as we expand. + + [Note: AOTAutograd Cacheability checks] + - Our cache key is computed from the FX graph produced by Dynamo and the input example values + - A node is "safe" if the same cache key results in a compiled artifact that has the same behavior + (i.e, the set of inputs that go into our cache key is sufficient to distinguish its behavior) + + To accomplish this safety check, we consider the following functions to be safe: + - Public functions under modules torch, torch.functional, and torch.nn.functional: these are + allowed in the graph by dynamo, so we can assume they are safe to cache. + - method calls on base tensor types + - Any call_module that dynamo deemed safe to allow AOTAutograd to trace + - Non callable nodes, such as placeholder, output, get_attr + + The test suite test_aot_autograd_cache.py::AOTAutogradCachePicklerTests tries its best to fully cover/specify this behavior. + """ + SAFE_TORCH_MODULES = ("torch.functional", "torch.nn.functional") + SAFE_TORCH_FUNCTIONS = ( + "torch.Size", + "torch.Tensor", + "torch.sym_int", + "torch._sym_sqrt", + "torch.sym_float", + "torch.sym_sum", + "torch.autograd.grad", + "torch.distributed.tensor._api.from_local", + ) + SAFE_NON_TORCH_FUNCTIONS = ( + "einops.einops.rearrange", + "einops.einops.repeat", + ) + + def is_public_torch_api(target: Callable[..., Any]) -> bool: + # Don't blindly allow private functions in the torch namespace + is_private = target.__name__.startswith("_") + + return ( + getattr(target, "__module__", None) in SAFE_TORCH_MODULES and not is_private + ) + + def is_safe_torch_function(target: Callable[..., Any]) -> bool: + """Allowlisted torch functions""" + function_name = f"{target.__module__}.{target.__name__}" + # Allow torch.autograd.function.FunctionCtx if custom autograd functions are allowed + if function_name == "torch.autograd.function.FunctionCtx": + return ( + torch._functorch.config.autograd_cache_allow_custom_autograd_functions + ) + + # Functions in torch_non_c_binding_in_graph_functions + # are guaranteed to be cache safe. + # See NOTE: [Cacheability of in-graph torch functions] + return ( + function_name in torch_non_c_binding_in_graph_functions + or function_name in SAFE_TORCH_FUNCTIONS + or function_name in torch._inductor.config.unsafe_marked_cacheable_functions + ) + + def is_cacheable_function(target: Callable[..., Any]) -> bool: + if isinstance(target, (torch._ops.OpOverload, torch._ops.OpOverloadPacket)): + return True + if is_public_torch_api(target): + return True + # Technically, FXGraphCache._check_for_hop already checks this, + # but better to error earlier anyway + if isinstance(target, torch._ops.HigherOrderOperator): + return target.cacheable() + is_builtin_fun_or_type = type(target).__name__ == "builtin_function_or_method" + if is_builtin_fun_or_type: + return True + if is_safe_torch_function(target): + return True + function_name = f"{target.__module__}.{target.__name__}" + if function_name in SAFE_NON_TORCH_FUNCTIONS: + return True + return False + + def is_tensor(target: Node) -> bool: + # Tensors always have example values in meta field + return "example_value" in target.meta + + # I'd love to use a match statement here, but it wasn't introduced until py3.10 + if node.op == "call_function": + if node.meta and node.meta.get("is_wrapped", False): + # This is fx.wrap function + # By default we BypassAOTAutogradCache for unknown functions, + # But if user explicitly specified cache hash - allow to cache it. + if node.meta.get("user_cache_hash", None): + return + if isinstance(node.target, str): + raise AssertionError( + f"expected node.target to not be a string, got {node.target}" + ) + if not is_cacheable_function(node.target): + module = getattr(node.target, "__module__", None) + name = getattr(node.target, "__name__", None) + raise BypassAOTAutogradCache( + f"Unsupported call_function target {node.target}. \n Function module: {module}, \nFunction name: {name}" + ) + elif node.op == "call_method": + method_name = node.target + method_target = node.args[0] + # Only support method calls on base tensors + if not isinstance(method_target, Node): + raise AssertionError( + f"expected method_target to be Node, got {type(method_target)}" + ) + if not is_tensor(method_target): + module = getattr(method_target, "__module__", None) + name = getattr(method_target, "__name__", None) + raise BypassAOTAutogradCache( + f"Unsupported call_method target {method_target}. \nMethod module: {module}, \nMethod name: {name}" + ) + if ( + type(method_name) is not str + and type(method_name).__name__ != "method_descriptor" + ): + raise BypassAOTAutogradCache( + f"Unsupported call_method method {node.target}: {method_name}" + ) + # Cache safe + elif node.op in ("placeholder", "get_attr", "call_module", "output"): + # Assumption today for call_module being a safe op: + # (1) today the only call_module ops that can show up in a graph come from "built-in-nn-modules" + # that dynamo assumes are safe to trace. If dynamo assumes they are safely to blindly trace, then + # they should be safe to cache as well. + # (2) in the steady-state (some time in H2?) we shouldn't see these anymore, once inline builtin nn modules by default + # (3) We do not allow user made nn modules in the graph today, only function calls. + pass + else: + raise BypassAOTAutogradCache(f"Unsupported node op {node.op}") + + +def check_cacheable(gm: torch.fx.GraphModule) -> None: + """ + Checks that the graph module only uses supported operators + """ + nodes = gm.graph.nodes + if torch._inductor.config.freezing: + raise BypassAOTAutogradCache("Cannot cache a graph with freezing enabled") + + if not ( + torch._inductor.config.fx_graph_cache or should_use_remote_fx_graph_cache() + ): + raise BypassAOTAutogradCache("FX graph cache is not enabled") + + tracing_context = torch._guards.TracingContext.try_get() + if tracing_context and tracing_context.fakify_first_call: + raise BypassAOTAutogradCache( + "Won't cache a graph with fakify_first_call enabled" + ) + for node in nodes: + check_node_safe(node) + + # Saved tensors hooks are globally set subgraphs, + # that are not used explicitly in the main graph. + # They are inlined in aot_autograd graphs. + # Subgraphs are only used for caching logic. + if hasattr(gm, "saved_tensors_hooks_pack_0"): + check_cacheable(gm.saved_tensors_hooks_pack_0) # type: ignore[arg-type] + # We have guarantee of unpack sugraph existence if pack subgraph exists + check_cacheable(gm.saved_tensors_hooks_unpack_0) # type: ignore[arg-type] + + +def _get_context_fn_cache_hash(context_fn: Callable[..., Any]) -> str | None: + """ + Extract a cache hash from a context_fn used for selective activation checkpointing (SAC). + + The context_fn determines which ops are saved vs recomputed in the SAC region. + Since context_fn can be an arbitrary Python function, we cannot reliably pickle + it for cache key generation (pickle only captures the function name, not the code). + + Users must provide a stable hash by setting a `cache_hash` attribute on the context_fn. + For functools.partial objects, set the cache_hash on the partial object itself, not on + the underlying function. + + Returns: + The cache hash if found + None: If no hash is provided (caller should bypass caching) + """ + if hasattr(context_fn, "cache_hash"): + return context_fn.cache_hash + + return None + + +def _collect_context_fn_hashes(gm: torch.fx.GraphModule) -> list[str]: + """ + Collect cache hashes from all context_fn used in SAC HOPs within the graph module. + + Returns a list of hashes. Raises BypassAOTAutogradCache if any context_fn + lacks a cache_hash attribute. + """ + hashes = [] + for module in gm.modules(): + if not isinstance(module, torch.fx.GraphModule): + continue + context_fn = module.meta.get("_checkpoint_context_fn") + if context_fn is not None: + cache_hash = _get_context_fn_cache_hash(context_fn) + if cache_hash is None: + raise BypassAOTAutogradCache( + "SAC context_fn does not have a cache_hash attribute. " + "To enable caching with selective activation checkpointing, " + "add a 'cache_hash' attribute to your context_fn. This can be " + "a string or any hashable value that uniquely identifies the checkpointing " + "behavior (e.g., based on source code hash and closed-over globals). " + "For functools.partial objects, set cache_hash on the partial itself." + ) + hashes.append(cache_hash) + return hashes + + +def _get_custom_estimator_solver_uuids( + autograd_config: Any, +) -> tuple[object | None, object | None]: + """ + Extract uuid values from custom runtime estimator and solver configs if they have uuid() methods. + + Returns a tuple of (runtime_estimator_uuid, solver_uuid). + + Returns None for each component if: + - The config field value is None + - The config field value is a string (built-in option like "flops", "greedy") + + Raises BypassAOTAutogradCache if: + - The config field value is a raw callable without uuid() method (caching not supported) + - The CustomRuntimeEstimator/CustomKnapsackSolver's uuid() method returns None + (caching explicitly disabled by implementation) + """ + + runtime_estimator = getattr( + autograd_config, "activation_memory_budget_runtime_estimator", None + ) + solver = getattr(autograd_config, "activation_memory_budget_solver", None) + + runtime_estimator_uuid = None + solver_uuid = None + + if isinstance(runtime_estimator, CustomRuntimeEstimator): + runtime_estimator_uuid = runtime_estimator.uuid() + if runtime_estimator_uuid is None: + raise BypassAOTAutogradCache( + "CustomRuntimeEstimator.uuid() returned None, bypassing cache" + ) + elif callable(runtime_estimator) and not isinstance(runtime_estimator, str): + raise BypassAOTAutogradCache( + "activation_memory_budget_runtime_estimator is a raw callable without uuid() method, " + "bypassing cache. Use CustomRuntimeEstimator for cache support." + ) + + if isinstance(solver, CustomKnapsackSolver): + solver_uuid = solver.uuid() + if solver_uuid is None: + raise BypassAOTAutogradCache( + "CustomKnapsackSolver.uuid() returned None, bypassing cache" + ) + elif callable(solver) and not isinstance(solver, str): + raise BypassAOTAutogradCache( + "activation_memory_budget_solver is a raw callable without uuid() method, " + "bypassing cache. Use CustomKnapsackSolver for cache support." + ) + + return runtime_estimator_uuid, solver_uuid + + +class AOTAutogradCacheDetails(FxGraphHashDetails): + """ + Object to capture all the details for a dynamo graph module relevant to computing + a safe and stable cache key for AOTAutograd. + """ + + def get_triton_source_codes_from_gm( + self, + gm: torch.fx.GraphModule, + ) -> list[str]: + if not has_triton_package(): + raise AssertionError("Triton is not available") + + triton_kernels = [] + for module in gm.modules(): + if not isinstance(module, torch.fx.GraphModule): + continue + for node in module.graph.nodes: + if isinstance(node.target, torch._ops.OpOverloadPacket): + attrs = node.target._dir + for attr in attrs: + if custom_op := getattr(node.target, attr, None): + kernels = torch._library.triton.get_triton_kernels_for_op( + custom_op._name + ) + triton_kernels.extend(kernels) + elif isinstance(node.target, torch._ops.OpOverload): + kernels = torch._library.triton.get_triton_kernels_for_op( + node.target._name + ) + triton_kernels.extend(kernels) + + triton_kernel_source_codes = [] + from torch._inductor.codegen.wrapper import ( + user_defined_triton_kernel_transitive_closure_source_code, + ) + + for kernel in triton_kernels: + from triton.runtime.autotuner import Autotuner + + if isinstance(kernel, Autotuner): + # Grab the Inner JITFunction + kernel = kernel.fn + source_codes = user_defined_triton_kernel_transitive_closure_source_code( + kernel + ) + triton_kernel_source_codes.append(source_codes) + + return triton_kernel_source_codes + + def __init__( + self, + gm: torch.fx.GraphModule, + example_inputs: Sequence[Any], + aot_config: AOTConfig, + fx_config: _CompileFxKwargs, + ) -> None: + # FxGraphHashDetails contains all the keys related to inductor. Also includes some system info + self.aot_config = aot_config + self.grad_enabled = torch.is_grad_enabled() + self.disable_amp = torch._C._is_any_autocast_enabled() + self.deterministic_algorithms = torch.are_deterministic_algorithms_enabled() + self.autograd_config = config.save_config() + self.saved_tensors_hooks_fx_wrap_cache_hashes: tuple[list[str], list[str]] = ( + [], + [], + ) + if has_triton_package(): + self.triton_kernel_source_codes = self.get_triton_source_codes_from_gm(gm) + + if hasattr(gm, "saved_tensors_hooks_pack_0"): + + def _add_wrapped_user_cache_hashes( + _gm: torch.fx.GraphModule, _l: list[str] + ) -> None: + for node in _gm.graph.nodes: + if node.meta and node.meta.get("is_wrapped", False): + _l.append(node.meta["user_cache_hash"]) + + _add_wrapped_user_cache_hashes( + # pyrefly: ignore[bad-argument-type] + gm.saved_tensors_hooks_pack_0, + self.saved_tensors_hooks_fx_wrap_cache_hashes[0], + ) + _add_wrapped_user_cache_hashes( + # pyrefly: ignore[bad-argument-type] + gm.saved_tensors_hooks_unpack_0, + self.saved_tensors_hooks_fx_wrap_cache_hashes[1], + ) + + self.sac_context_fn_hashes: list[str] = _collect_context_fn_hashes(gm) + + # Note: We use the live config module, not self.autograd_config (the saved config), + # because activation_memory_budget_runtime_estimator and activation_memory_budget_solver + # are excluded from save_config (in _save_config_ignore) since they're not serializable. + # We must access the config module directly to get the patched runtime values. + self.custom_estimator_solver_uuids = _get_custom_estimator_solver_uuids(config) + + try: + # FXGraphCache has constraints on what can be pickled in its inductor + # config. Check that the gm is cacheable by inductor first, + # and if it raises an exception, also bypass on our end. + FxGraphCache._check_can_cache(gm) + super().__init__(gm, example_inputs, fx_config, []) + except BypassFxGraphCache as e: + # Sometimes inductor configs are unpickleable and can fail + raise BypassAOTAutogradCache(str(e)) from e + + +class AOTAutogradCachePickler(FxGraphCachePickler): + def __init__(self, gm: torch.fx.GraphModule) -> None: + super().__init__(gm) + # pyrefly: ignore[missing-attribute] + self.dispatch_table.update( + { + AOTConfig: functools.partial(self._reduce_aot_config), + torch.Tensor: functools.partial(self._reduce_tensor), + FakeScriptObject: functools.partial(self._reduce_fake_script_object), + } + ) + + # pyrefly: ignore [bad-override] + def reducer_override(self, obj: Any) -> Any: + """ + Override to handle tensor subclasses (like DTensor) that aren't caught + by the dispatch_table's exact type matching. + + The dispatch_table only matches exact types, so subclasses like DTensor + fall through to the default __reduce_ex__ which includes non-deterministic + storage addresses. This method catches those cases using isinstance checks. + """ + from torch.utils._python_dispatch import is_traceable_wrapper_subclass + + # Handle tensor subclasses that aren't exactly torch.Tensor + # dispatch_table already handles torch.Tensor exactly + if isinstance(obj, torch.Tensor) and type(obj) is not torch.Tensor: + if hasattr(obj, "_stable_hash_for_caching"): + return (_ident, (obj._stable_hash_for_caching(),)) + if is_traceable_wrapper_subclass(obj): + warn_once( + f"{type(obj).__name__} does not implement _stable_hash_for_caching. " + "For PT2-compatible tensor subclasses, it is recommended to implement " + "_stable_hash_for_caching(self) -> str for stable AOT autograd caching." + ) + return (_ident, (self._default_stable_hash_for_caching(obj),)) + return self._reduce_tensor(obj) + # Return NotImplemented to fall back to default behavior + return NotImplemented + + # [NOTE] Tensor subclass stable hashing for AOT autograd cache + # Python's hash() varies with PYTHONHASHSEED, making cache keys unstable + # across processes. We use blake2b for cross-process determinism. + # + # EXTENSION POINT: Traceable wrapper subclasses can override cache key + # generation by implementing _stable_hash_for_caching(self) -> str. + # This method should return a deterministic string that uniquely identifies + # the tensor's metadata for caching purposes. See DTensor for an example. + # + # We can't define a default method on subclasses because there is no abstract + # base subclass, and we don't want to pollute torch.Tensor. Instead, we provide + # a default implementation here that uses __tensor_flatten__ to recursively + # hash inner tensors and metadata. + + def _get_stable_hash(self, obj: Any) -> str: + """ + Get stable hash for a tensor or opaque object, dispatching to custom or default implementation. + """ + from torch._opaque_base import OpaqueBase + from torch.utils._python_dispatch import is_traceable_wrapper_subclass + + if hasattr(obj, "_stable_hash_for_caching"): + return obj._stable_hash_for_caching() + elif isinstance(obj, torch.Tensor) and is_traceable_wrapper_subclass(obj): + return self._default_stable_hash_for_caching(obj) + elif isinstance(obj, OpaqueBase): + # Opaque objects are runtime pass-throughs; only the type matters + # for cache key purposes, not the instance identity or value. + type_name = type(obj).__qualname__ + return hashlib.blake2b(type_name.encode(), digest_size=16).hexdigest() + elif isinstance(obj, torch.Tensor): + metadata = extract_tensor_metadata_for_cache_key(obj) + return hashlib.blake2b(pickle.dumps(metadata), digest_size=16).hexdigest() + else: + return hashlib.blake2b(pickle.dumps(obj), digest_size=16).hexdigest() + + def _default_stable_hash_for_caching(self, tensor: torch.Tensor) -> str: + """ + Default stable hash implementation for traceable wrapper subclasses. + """ + from torch._opaque_base import OpaqueBase + + inner_tensor_names, subclass_metadata = tensor.__tensor_flatten__() # type: ignore[attr-defined] + + # Recursively get hashes of inner tensors/opaque objects + inner_hashes: dict[str, str] = {} + for name in inner_tensor_names: + inner = getattr(tensor, name) + inner_hashes[name] = self._get_stable_hash(inner) + + # Stabilize metadata: replace OpaqueBase instances with their type name + # since their repr includes memory addresses + def _stabilize(obj: Any) -> Any: + if isinstance(obj, OpaqueBase): + return type(obj).__qualname__ + if isinstance(obj, tuple): + return tuple(_stabilize(x) for x in obj) + if isinstance(obj, list): + return [_stabilize(x) for x in obj] + if isinstance(obj, dict): + return {k: _stabilize(v) for k, v in obj.items()} + return obj + + cache_data = pickle.dumps( + ( + tensor.shape, + tensor.requires_grad, + _stabilize(subclass_metadata), + inner_hashes, + ) + ) + return hashlib.blake2b(cache_data, digest_size=16).hexdigest() + + def _reduce_aot_config( + self, aot_config: AOTConfig + ) -> tuple[Callable[..., Any], tuple[Any, ...]]: + """ + Reduce the config to a stable key for caching. + """ + return ( + _ident, + ( + aot_config.num_params_buffers, + aot_config.keep_inference_input_mutations, + aot_config.is_export, + aot_config.no_tangents, + aot_config.dynamic_shapes, + aot_config.aot_autograd_arg_pos_to_source, + aot_config.enable_log, + aot_config.pre_dispatch, + ), + ) + + def _reduce_tensor(self, t: torch.Tensor) -> tuple[Callable[..., Any], tuple[Any]]: + """ + Reduce the tensor to a stable key for caching. + """ + metadata = extract_tensor_metadata_for_cache_key(t) + return (_ident, (metadata,)) + + +@contextlib.contextmanager +def normalize_placeholder_names( + gm: torch.fx.GraphModule, +) -> Generator[None, None, None]: + """ + Context manager that normalizes the placeholder names in the graph module. + This is used while generating a cache key for AOTAutogradCache, so that two graphs + that are isomorphic when normalizing names can hit the same cache entry. + This is safe because nothing underneath AOTAutograd uses the node names on the + original dynamo graph: AOTAutograd re-traces with its own nodes, and guards are + in terms of original sources rather than placeholder names. + """ + # Standalone inductor: we're bypassing AOTAutogradCache anyway, so return the graph + # as-is + if not config.autograd_cache_normalize_inputs or not hasattr(gm, "graph"): + yield + return + + # Track all the old state of placeholders + old_placeholder_names = [] + old_used_names = copy(gm.graph._graph_namespace._used_names) + i = 0 + for n in gm.graph.find_nodes(op="placeholder", sort=True): + if n.type != torch.SymInt: + # _rename renames the node in the body of the function, + # but it doesn't change the raw name from node.target + # So we also set the raw_name of node.target to a new placeholder name + new_placeholder_name = f"p_{i}" + old_placeholder_names.append((n.name, n.target)) + n.target = new_placeholder_name + n._rename(new_placeholder_name) + i += 1 + gm.recompile() + try: + yield + finally: + # Used_names contains all our old placeholder names, + # so we clear it temporarily when we put them back + gm.graph._graph_namespace._used_names = set() + # Restore the placeholder names + i = 0 + for n in gm.graph.find_nodes(op="placeholder", sort=True): + if n.type != torch.SymInt: + (name, target) = old_placeholder_names[i] + n.target = target + n._rename(name) + i += 1 + if i != len(old_placeholder_names): + raise AssertionError( + f"i={i} != len(old_placeholder_names)={len(old_placeholder_names)}" + ) + # Now restore the old namespace's used names + gm.graph._graph_namespace._used_names = old_used_names + gm.recompile() + + +def create_fx_config( + compiler_config_extra: CompilerConfigExtra | None = None, + compile_region_name: str | None = None, +) -> _CompileFxKwargs: + if compiler_config_extra is None: + cudagraphs = BoxedBool(torch._inductor.config.triton.cudagraphs) + boxed_forward_device_index = None + else: + cudagraphs = compiler_config_extra.cudagraphs + boxed_forward_device_index = compiler_config_extra.forward_device + return { + "cudagraphs": cudagraphs, + "boxed_forward_device_index": boxed_forward_device_index, + "compile_region_name": compile_region_name, # pyrefly: ignore[bad-typed-dict-key] + } + + +def autograd_cache_key( + mod: torch.fx.GraphModule | torch._dynamo.utils.GmWrapper, + example_inputs: Sequence[Any], + config: AOTConfig, + compiler_config_extra: CompilerConfigExtra | None = None, + # TODO: add args and parameters +) -> tuple[str, list[str]]: + """ + Generate a unique hash of the FX graph for caching. + """ + + gm = mod.gm if isinstance(mod, torch._dynamo.utils.GmWrapper) else mod + with sanitize_gm_for_cache(gm): + try: + check_cacheable(gm) + if has_triton_package(): + # Due to https://github.com/triton-lang/triton/issues/3729, + # if triton is < 3.2.0, AOTAutogradCache may cause us to + # attempt to load a cache entry without initializing + # the CUDA context on the autograd thread. + + # Without caching, we naturally do this initialization when + # tracing through the graph with the autograd engine. + import triton + + if triton.__version__ < "3.2.0": + raise BypassAOTAutogradCache( + "AOTAutogradCache requires triton 3.2.0" + ) + details = AOTAutogradCacheDetails( + gm, example_inputs, config, create_fx_config(compiler_config_extra) + ) + pickler = AOTAutogradCachePickler(gm) + # The prefix distinguishes among the other kinds of objects we cache + key = "a" + pickler.get_hash(details) + # debug_lines re-hashes every attribute individually and is + # expensive. Only compute when debug logging is enabled. + if log.isEnabledFor(logging.DEBUG): + debug_lines = pickler.debug_lines(details) + log.debug( + "Autograd graph cache hash details for key %s:\n%s", + key, + LazyString(lambda: "\n".join(debug_lines)), + ) + else: + debug_lines: list[str] = [] + return key, debug_lines + except Exception: + # If enable_aot_compile is set, we're in AOT precompile mode where we always + # want to use fallback nonce keys. Unlike caching, it's fine if we can't generate + # a proper key because we are guaranteed in an AOT precompile world users are in + # complete control of distributing and loading artifacts. + if torch._functorch.config.bypass_autograd_cache_key: + log.info( + "Failed to generate AOTAutograd cache key; falling back to nonce due to enable_aot_compile", + exc_info=True, + ) + return str(random.random()), [] + else: + raise + + +@contextlib.contextmanager +def sanitize_gm_for_cache( + gm: torch.fx.GraphModule, +) -> Generator[None, None, None]: + """ + Clears a few fields in a dynamo supplied Graph Module that are not stable between graph inputs, but don't + affect inductor or aotdispatch correctness. + + These fields **can** be used by code calling into aotdispatch (namely, dynamo), so we can't null them out completely. + + To ensure that these fields are not accessed by inductor or aotdispatch, we clear them during AOTAutogradCache.load, + and then put them back before returning. This way, we generate a cache key based off of a canonical graph + without these fields, and also guarantee they aren't used to affect the cache's output. + """ + # Mapping from each field to a default value + IGNORED_FIELDS: dict[str, Any] = { + # pyrefly: ignore [implicit-any] + "meta": {}, # metadata used by export + "compile_subgraph_reason": None, # Used by dynamo only for logging, no change in inductor/autograd behavior + "_param_name_to_source": None, # Encapsulated by aot_config.aot_autograd_arg_pos_to_source + "_backend_id": None, + } + saved_fields = {} + for field, default_value in IGNORED_FIELDS.items(): + saved_fields[field] = getattr(gm, field, None) + # Clear the field + setattr(gm, field, default_value) + try: + with normalize_placeholder_names(gm): + yield + finally: + for field, value in saved_fields.items(): + setattr(gm, field, value) + + +@CacheArtifactFactory.register +class AOTAutogradCacheArtifact(CacheArtifact): + @override + def populate_cache(self) -> None: + AOTAutogradCache._write_to_local_cache(self.key, self.content) + + @override + @staticmethod + def type() -> str: + return "aot_autograd" + + +class AOTAutogradCache(GuardedCache[GenericAOTAutogradResult[Any, Any]]): + """ + Caches the results of running AOTAutograd. This class mostly handles the save and load logic, whereas + AOTAutogradResult handles the wrapping/unwrapping logic. + + Cache Inputs (AOTAutogradCacheDetails) + - AOTAutogradCache takes in the following inputs, which are analogous to inputs given + to AOTAutograd by dynamo: + - A fx graph module generated by dynamo + - A list of args, which consists of: + - Symint inputs to the graph, generated by dynamo + - The **real tensor** inputs, which inductor uses for cudagraphs + - Notably, the real tensor inputs don't have symints in their metadata. + AOTAutograd then retraces those real tensor arguments into FakeTensors later during execution. + - A set of global configurations that affect AOTAutograd or Inductor behavior. + + It then generates a cache key given these values. Notably, this means AOTAutogradCache currently + specializes on the sizes and strides of the real tensor inputs when dynamic shapes are turned on. + In a later PR, we'll likely generate the cache key based on the FakeTensors AOTAutograd generates + based on the real tensor inputs, which can contain symints. + + # Cache Outputs (AOTAutogradResult) + - AOTAutogradCache caches the following values: + - The compiled forward and backward functions from inductor, via keys to the FXGraphCache + - Metadata to reconstruct the AOTModule from the compiled inductor artifacts + - See AOTAutogradResult for more info + + [Note: Caching guards generated by AOTAutograd and Inductor] + AOTAutograd and inductor both can introduce new guards to the shape environment. FXGraphCache saves guards with each + compiled graph inductor generates. On a cache hit, AOTAutograd reloads the compiled forward and backward functions + from FXGraphCache, giving it new symint arguments from the input args. + FXGraphCache uses those symints and its saved guards to repopulate the ShapeEnv with guards. + **No new guards are generated into the shape env after inductor finishes compiling**, so the guards + saved by inductor are sufficient for correctness for both AOTAutograd and Inductor's caches. + """ + + @staticmethod + def clear() -> None: + """Clear the cache""" + try: + shutil.rmtree(AOTAutogradCache._get_tmp_dir()) + except FileNotFoundError: + pass + + @staticmethod + def try_load( + mod: torch.fx.GraphModule | torch._dynamo.utils.GmWrapper, + args: list[Any], + aot_config: AOTConfig, + compiler_config_extra: CompilerConfigExtra | None, + local: bool, + remote: bool, + compile_region_name: str | None = None, + ) -> Callable[..., Any] | None: + """ + Load a result from the cache, and reconstruct a runtime wrapper around the object + """ + compiled_fn = None + cache_info: dict[str, Any] = {} + cache_key = None + debug_lines: list[str] = [] + cache_event_time = time.time_ns() + cache_state = None + try: + cache_key, debug_lines = autograd_cache_key( + mod, args, aot_config, compiler_config_extra + ) + result: tuple[GenericAOTAutogradResult[Any, Any], bytes] | None = ( + AOTAutogradCache._lookup( + cache_key, local, remote, args, cache_info, aot_config + ) + ) + if result is not None: + (entry, pickled_content) = result + fx_config = create_fx_config(compiler_config_extra, compile_region_name) + compiled_fn = entry.wrap_post_compile(args, aot_config, fx_config) + # Make the compiled_fn serializable, where the serialize function just + # makes a copy of the original entry before post compile via the pickled content + compiled_fn = SerializableCompiledFunction( + compiled_fn, lambda: pickle.loads(pickled_content) + ) + log.info("AOTAutograd cache hit for key %s", cache_key) + + counters["aot_autograd"]["autograd_cache_hit"] += 1 + cache_state = "hit" + cache_event_time = time.time_ns() + forward_time_saved = entry.forward_time_taken_ns // 1e6 + backward_time_saved = entry.backward_time_taken_ns // 1e6 + cache_info.update( + { + "forward_time_saved_ms": forward_time_saved, + "backward_time_saved_ms": backward_time_saved, + "time_saved_ms": forward_time_saved + backward_time_saved, + } + ) + time_saved_ns = ( + entry.forward_time_taken_ns + entry.backward_time_taken_ns + ) + # TODO: should we use the same field for remote cache time saved for both + # FXGraphCache and AOTAutogradCache? + # get_metrics_context().increment(...) + if ( + ephemeral_increase + := add_ephemeral_timeout_increase_for_distributed(time_saved_ns) + ) != 0: + cache_info["ephemeral_timeout_increase"] = ephemeral_increase + + if compiled_fn is None: + log.info("AOTAutograd cache miss for key %s", cache_key) + counters["aot_autograd"]["autograd_cache_miss"] += 1 + cache_state = "miss" + cache_event_time = time.time_ns() + # Count missing the FXGraphCache as a miss not a bypass + except FXGraphCacheMiss as e: + counters["aot_autograd"]["autograd_cache_miss"] += 1 + cache_state = "miss" + if config.strict_autograd_cache or torch._dynamo.config.strict_precompile: + raise e + # Most often this is BypassAOTAutogradCache, but + # if there's ever different reason we can't cache, + # we still never want to hard throw an exception, since + # we can always fallback to a cache bypass. + # As an example, if the user calls autograd via + # standalone inductor, we will sometimes get a GraphModule + # that doesn't actually have a `.graph` on it. Instead + # of checking every single case, we safely catch the exception + # in those cases. + except Exception as e: + cache_key = None + counters["aot_autograd"]["autograd_cache_bypass"] += 1 + log.info("Bypassing autograd cache due to: %s", e) # noqa: G200 + cache_state = "bypass" + cache_event_time = time.time_ns() + cache_info["cache_bypass_reason"] = str(e) + cache_info["cache_bypass_exception_type"] = type(e).__name__ + cache_info["cache_bypass_traceback"] = traceback.format_exc().split("\n") + # TODO: this gets logged implicitly by cache_bypass_reason, + # and here we explicitly log it into tlparse. + # We may want to log this as an extra column in Scuba, though. + cache_info["cache_bypass_hard_exception"] = not isinstance( + e, BypassAOTAutogradCache + ) + if remote: + log_cache_bypass("bypass_aot_autograd", str(e)) + if config.strict_autograd_cache or torch._dynamo.config.strict_precompile: + raise e + if compiled_fn is None: + # Set the cache key so we can save a cache result later + symints = AOTAutogradCache._filter_backed_symints(args) + if cache_key is not None: + aot_config.cache_info = AOTAutogradCacheInfo( + cache_key, + time.time_ns(), + forward_symints=symints, + ) + + cache_info.update( + { + "key": cache_key, + "cache_state": cache_state, + "components": debug_lines, + } + ) + if chromium_event_log_active(): + CompileEventLogger.instant( + f"autograd_cache_{cache_state}", + metadata=cache_info, + time_ns=cache_event_time, + ) + CompileEventLogger.try_add_pt2_compile( + "backend_compile", + cache_state=cache_state, + cache_event_time=cache_event_time, + key=cache_info.get("key"), + components=cache_info.get("components"), + cache_bypass_reason=cache_info.get("cache_bypass_reason"), + remote_cache_enabled=remote, + local_cache_enabled=local, + ) + + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": f"aotautograd_cache_{cache_state}", + "encoding": "json", + }, + payload_fn=lambda: json.dumps(cache_info), + ) + + return compiled_fn + + @classmethod + def generate_guards_expression( + cls: type[AOTAutogradCache], cache_info: AOTAutogradCacheInfo + ) -> str | None: + shape_env = cls._get_shape_env() + + if shape_env is None: + return None + + symints = cache_info.forward_symints + guards = shape_env.get_pruned_guards(symints) + return shape_env.produce_guards_expression(placeholders=symints, guards=guards) + + @classmethod + def _get_tmp_dir(cls: type[AOTAutogradCache]) -> str: + """ + Get the toplevel temporary directory for storing compiled graphs. + """ + return os.path.join(cache_dir(), "aotautograd") + + @classmethod + def _get_tmp_dir_for_key(cls: type[AOTAutogradCache], _key: str) -> str: + """ + Get the toplevel temporary directory for storing compiled graphs. + """ + return os.path.join(cls._get_tmp_dir(), _key) + + @classmethod + def _record_result( + cls: type[AOTAutogradCache], + key: str, + local_hit: bool, + local_miss: bool, + remote_hit: bool, + remote_miss: bool, + ) -> None: + """ + Called by GuardedCache to record hit/miss statistics. + """ + if local_hit: + CompileEventLogger.try_( + CompileEventLogger.increment_toplevel, + "aotautograd_local_cache_hit_count", + ) + if remote_hit: + CompileEventLogger.try_( + CompileEventLogger.increment_toplevel, + "aotautograd_remote_cache_hit_count", + ) + if local_miss: + CompileEventLogger.try_( + CompileEventLogger.increment_toplevel, + "aotautograd_local_cache_miss_count", + ) + if remote_miss: + CompileEventLogger.try_( + CompileEventLogger.increment_toplevel, + "aotautograd_remote_cache_miss_count", + ) + + @staticmethod + def evaluate_guards(guard_expr: str, hints: list[int] | list[torch.SymInt]) -> bool: + if torch._inductor.config.unsafe_skip_cache_dynamic_shape_guards: + return True + shape_env = AOTAutogradCache._get_shape_env() + if shape_env is None: + raise AssertionError("shape_env must not be None") + result = shape_env.evaluate_guards_expression(guard_expr, hints) + return result + + @staticmethod + def _lookup( + key: str, + local: bool, + remote: bool, + args: list[Any], + cache_info: dict[str, Any], + aot_config: AOTConfig | None, + ) -> tuple[GenericAOTAutogradResult[Any, Any], bytes] | None: + """Given a key generated by AOTAutogradCachePickler, look up its location in the cache.""" + remote_cache: RemoteCache[JsonDataTy] | None = None + if remote: + remote_cache = AOTAutogradCache.get_remote_cache() + + symints = AOTAutogradCache._filter_backed_symints(args) + hints = [guarding_hint_or_throw(s) for s in symints] + entry = None + pickled_content = None + try: + ( + entry, + pickled_content, + guard_info, + ) = AOTAutogradCache.find_guarded_entry( + key, local, remote_cache, AOTAutogradCache.evaluate_guards, hints + ) + + if entry is None and guard_info["cache_status_detailed"] == "guard_miss": + counters["aot_autograd"]["autograd_cache_guard_miss"] += 1 + cache_info.update(guard_info) + if pickled_content is not None: + CacheArtifactManager.record_artifact( + AOTAutogradCacheArtifact.type(), key, pickled_content + ) + if ( + should_bundle_autograd_cache() + and aot_config is not None + and aot_config.precompile_backend_id is not None + ): + # NB: We don't want to use the cached aot_config.precompile_backend_id + # 1. because we set it to None on save 2. even if we didn't, this new run + # that cache hit has a *new* backend id associated with it. + PrecompileContext.record_artifact( + BundledAOTAutogradCacheArtifact( + aot_config.precompile_backend_id, entry + ), + ) + except Exception as e: + log.info("AOTAutograd cache unable to load compiled graph: %s", e) # noqa: G200 + if config.strict_autograd_cache: + raise e + if entry is not None: + if pickled_content is None: + raise AssertionError( + "pickled_content must not be None when entry is not None" + ) + return (entry, pickled_content) + else: + return None + + @staticmethod + def _write_to_local_cache(key: str, content: bytes) -> None: + """Write an entry to the local cache.""" + subdir = AOTAutogradCache._get_tmp_dir_for_key(key) + if not os.path.exists(subdir): + os.makedirs(subdir, exist_ok=True) + + # Use a hash of the serialized entry to get a unique file + # name. The specific name doesn't matter since a lookup involves + # iterating over all entries in the parent subdir. + path = os.path.join(subdir, sha256_hash(content)) + log.info("Writing AOTAutograd cache entry to %s", path) + write_atomic(path, content) + + @staticmethod + def _find_unpicklable_field( + entry: GenericAOTAutogradResult[Any, Any], + ) -> str | None: + """Find which field of entry is causing pickle to fail.""" + # pyrefly: ignore [implicit-any] + fields = [] + if hasattr(entry, "__dataclass_fields__"): + fields = list(entry.__dataclass_fields__.keys()) + elif hasattr(entry, "__dict__"): + fields = list(entry.__dict__.keys()) + + for name in fields: + try: + pickle.dumps(getattr(entry, name)) + except Exception: + return name + return None + + @staticmethod + def _pickle_entry( + entry: GenericAOTAutogradResult[Any, Any], remote: bool + ) -> bytes | None: + """Pickle entry, returning None on failure.""" + try: + with torch.utils._python_dispatch._disable_current_modes(): + return pickle.dumps(entry) + except (pickle.PicklingError, TypeError, AttributeError) as e: + bad_field = AOTAutogradCache._find_unpicklable_field(entry) + error_str = str(e) + log.warning("AOTAutograd cache unable to serialize compiled graph: %s", e) # noqa: G200 + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "aotautograd_cache_pickle_failure", + "encoding": "json", + }, + payload_fn=lambda: json.dumps({"error": error_str, "field": bad_field}), + ) + if remote: + log_cache_bypass( + "bypass_aot_autograd", "Unable to serialize: " + str(e) + ) + if config.strict_autograd_cache: + raise + return None + + @staticmethod + def _handle_save_error(e: Exception, remote: bool, is_bypass: bool) -> None: + """Handle exceptions during save, re-raising if strict mode is enabled.""" + if is_bypass: + counters["aot_autograd"]["autograd_cache_bypass"] += 1 + log.info("Bypassing autograd cache due to: %s", e) # noqa: G200 + bypass_reason = str(e) + else: + log.warning("AOTAutograd cache unable to serialize compiled graph: %s", e) # noqa: G200 + bypass_reason = "Unable to serialize: " + str(e) + if remote: + log_cache_bypass("bypass_aot_autograd", bypass_reason) + if config.strict_autograd_cache: + raise e + + @staticmethod + def save(key: str, entry: GenericAOTAutogradResult[Any, Any], remote: bool) -> None: + """Save a single entry into the cache.""" + content: bytes | None = None + try: + entry.pre_save() + content = AOTAutogradCache._pickle_entry(entry, remote) + if content is None: + return None + CacheArtifactManager.record_artifact( + AOTAutogradCacheArtifact.type(), key, content + ) + if ( + should_bundle_autograd_cache() + and entry.sanitized_aot_config.precompile_backend_id is not None + ): + precompile_key = entry.sanitized_aot_config.precompile_backend_id + artifact = BundledAOTAutogradCacheArtifact(precompile_key, entry) + entry.sanitized_aot_config.precompile_backend_id = None + PrecompileContext.record_artifact(artifact) + AOTAutogradCache._write_to_local_cache(key, content) + counters["aot_autograd"]["autograd_cache_saved"] += 1 + except BypassAOTAutogradCache as e: + AOTAutogradCache._handle_save_error(e, remote, is_bypass=True) + return None + except Exception as e: + AOTAutogradCache._handle_save_error(e, remote, is_bypass=False) + return None + + if remote: + remote_cache = AOTAutogradCache.get_remote_cache() + if remote_cache is not None: + time_taken_ms = int( + (entry.forward_time_taken_ns + entry.backward_time_taken_ns) // 1e6 + ) + cache_data: JsonDataTy = { + "data": base64.b64encode(content).decode("ascii"), + "time_taken_ms": time_taken_ms, + } + remote_cache.put(key, cache_data) + + @staticmethod + @functools.cache + def get_remote_cache() -> RemoteCache[JsonDataTy] | None: + """ + Attempts to load the remote cache, returns None on error. + """ + cache_id = "autograd-experimental" + return create_cache( + cache_id, + config.is_fbcode(), + "FbRemoteAOTAutogradCache", + "RemoteAOTAutogradCache", + ) + + @staticmethod + def make_entry( + compiled_fw_func: OutputCode, + compiled_bw_func: OutputCode | None, + aot_joint_graph_str: str | None, + aot_forward_graph_str: str | None, + aot_backward_graph_str: str | None, + runtime_metadata: ViewAndMutationMeta, + dispatch_wrappers: list[CompilerWrapper], + maybe_subclass_meta: SubclassMeta | None, + num_fw_outs_saved_for_bw: int | None, + indices_of_inps_to_detach: list[int], + forward_time_taken_ns: int, + backward_time_taken_ns: int, + sanitized_aot_config: AOTConfig, + guards_expr: str | None, + backward_state_indices: list[int] | None, + num_symints_saved_for_bw: int | None, + serialized_bw_module: SerializedGraphModule | None, + ) -> GenericAOTAutogradResult[Any, Any]: + if should_bundle_autograd_cache(): + # Helper function to unwrap all the wrappers we added during aotdispatch + # They get reapplied on cache load + def unwrap_output_code(obj: object) -> OutputCode: + while hasattr(obj, "__wrapped__"): + obj = obj.__wrapped__ + if not isinstance(obj, OutputCode): + raise AssertionError(f"expected OutputCode, got {type(obj)}") + return obj + + compiled_fw_graph = unwrap_output_code(compiled_fw_func) + bundled_compiled_forward = BundledCompiledForward(compiled_fw_graph) + bundled_compiled_backward = None + if compiled_bw_func is not None: + if backward_state_indices is None: + raise AssertionError("backward_state_indices must not be None") + if num_symints_saved_for_bw is None: + raise AssertionError("num_symints_saved_for_bw must not be None") + compiled_bw_graph = unwrap_output_code(compiled_bw_func) + bundled_compiled_backward = BundledCompiledBackward( + compiled_bw_graph, backward_state_indices, num_symints_saved_for_bw + ) + + return BundledAOTAutogradResult( + compiled_fw=bundled_compiled_forward, + compiled_bw=bundled_compiled_backward, + aot_joint_graph_str=aot_joint_graph_str, + aot_forward_graph_str=aot_forward_graph_str, + aot_backward_graph_str=aot_backward_graph_str, + runtime_metadata=runtime_metadata, + dispatch_wrappers=dispatch_wrappers, + maybe_subclass_meta=maybe_subclass_meta, + num_fw_outs_saved_for_bw=num_fw_outs_saved_for_bw, + indices_of_inps_to_detach=indices_of_inps_to_detach, + forward_time_taken_ns=forward_time_taken_ns, + backward_time_taken_ns=backward_time_taken_ns, + sanitized_aot_config=sanitized_aot_config, + guards_expr=guards_expr, + serialized_bw_module=serialized_bw_module, + ) + + else: + fw_key = getattr(compiled_fw_func, "_fx_graph_cache_key", None) + fw_debug_lines = getattr( + compiled_fw_func, "_fx_graph_cache_debug_lines", [] + ) + + if fw_key is None: + raise AssertionError("fw_key must not be None") + compiled_forward = CompiledForward( + fx_graph_cache_info=(fw_key, fw_debug_lines), + fx_graph_guard_expr=getattr(compiled_fw_func, "guards_expr", None), + ) + compiled_backward = None + if compiled_bw_func is not None: + bw_key = getattr(compiled_bw_func, "_fx_graph_cache_key", None) + bw_debug_lines = getattr( + compiled_bw_func, "_fx_graph_cache_debug_lines", [] + ) + if bw_key is None: + raise AssertionError("bw_key must not be None") + if backward_state_indices is None: + raise AssertionError("backward_state_indices must not be None") + if num_symints_saved_for_bw is None: + raise AssertionError("num_symints_saved_for_bw must not be None") + compiled_backward = CompiledBackward( + fx_graph_cache_info=(bw_key, bw_debug_lines), + fx_graph_guard_expr=getattr(compiled_bw_func, "guards_expr", None), + backward_state_indices=backward_state_indices, + num_symints_saved_for_bw_=num_symints_saved_for_bw, + ) + + return AOTAutogradResult( + compiled_fw=compiled_forward, + compiled_bw=compiled_backward, + aot_joint_graph_str=aot_joint_graph_str, + aot_forward_graph_str=aot_forward_graph_str, + aot_backward_graph_str=aot_backward_graph_str, + runtime_metadata=runtime_metadata, + dispatch_wrappers=dispatch_wrappers, + maybe_subclass_meta=maybe_subclass_meta, + num_fw_outs_saved_for_bw=num_fw_outs_saved_for_bw, + indices_of_inps_to_detach=indices_of_inps_to_detach, + forward_time_taken_ns=forward_time_taken_ns, + backward_time_taken_ns=backward_time_taken_ns, + sanitized_aot_config=sanitized_aot_config, + guards_expr=guards_expr, + serialized_bw_module=serialized_bw_module, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/collect_metadata_analysis.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/collect_metadata_analysis.py new file mode 100644 index 0000000000000000000000000000000000000000..d4d56919424eb992dd34b500853e41c1175e17bc --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/collect_metadata_analysis.py @@ -0,0 +1,883 @@ +from __future__ import annotations + + +""" +This module is one of the analysis modules - it takes as input a function or graph +and some preexisting properties, and returns some data that is useful for deciding +how to further proceed with compilation or construct runtime wrappers. + +In particular, the analysis here constructs view and mutation metadata from running +a functionalized version of the graph under compilation. +""" + +import collections +import contextlib +import logging +from typing import Any, TYPE_CHECKING + +import torch +import torch.utils._pytree as pytree +from torch import Tensor +from torch._guards import detect_fake_mode +from torch._library.opaque_object import is_opaque_type +from torch._logging import getArtifactLogger +from torch._subclasses.functional_tensor import FunctionalTensor, FunctionalTensorMode +from torch._subclasses.meta_utils import safe_is_leaf +from torch.fx.experimental.proxy_tensor import disable_autocast_cache +from torch.fx.experimental.symbolic_shapes import is_concrete_int +from torch.multiprocessing.reductions import StorageWeakRef +from torch.utils._python_dispatch import ( + is_traceable_wrapper_subclass, + transform_subclass, +) + +from .descriptors import ( + AOTInput, + AOTOutput, + InputMutationAOTOutput, + IntermediateBaseAOTOutput, + PlainAOTOutput, + TangentAOTInput, +) +from .functional_utils import ( + are_all_mutations_hidden_from_autograd, + are_all_mutations_under_no_grad_or_inference_mode, + from_fun, + has_data_mutation, + has_metadata_mutation, + MetadataKey, + to_fun, + ViewMetaSequence, + was_inductor_storage_resized, +) +from .schemas import ( + InputAliasInfo, + MemoryFormatMeta, + MutationType, + OutputAliasInfo, + OutputType, + ViewAndMutationMeta, +) +from .subclass_utils import create_subclass_meta +from .utils import _get_autocast_states, KNOWN_TYPES, simple_wraps, strict_zip + + +if TYPE_CHECKING: + from collections.abc import Callable + +zip = strict_zip + +log = logging.getLogger(__name__) +static_input_logger = getArtifactLogger("torch._dynamo", "cudagraph_static_inputs") + + +# Note [Tangents memory format] +# We assume tangents memory format to be similar to corresponding output's memory_format. +# The idea is that we are technically making a guess about the strides of our tangents, +# while we trace out the joint. +# If runtime specified tangents will not have the same memory format as predicted traced tangents, +# we coerce them at runtime to traced tangents memory format. + + +# Coercing and collecting traced tangents memory format in one recursive traversal +def coerce_tangent_and_suggest_memory_format( + x: Tensor, +) -> tuple[Any, MemoryFormatMeta | list[Any] | None, bool]: + updated = False + if not isinstance(x, Tensor): + return x, None, updated + + out = x.detach() + + is_subclass = is_traceable_wrapper_subclass(out) + + memory_format = MemoryFormatMeta.from_tensor(out) + + # pyrefly: ignore [missing-attribute] + if memory_format.memory_format is not None: + was = out + # pyrefly: ignore [bad-argument-type] + out = out.contiguous(memory_format=memory_format.memory_format) + updated = was is not out + + # For subclass we keep memory format of outer strides at the beginning of the list + out_memory_format = [memory_format] if is_subclass else memory_format + + # Note [Tangents memory format, Part 2] + # In the same way that "what strides do we assigns to our tangents" is a question + # that we can not answer (and therefore have to guess) as we trace the backward ahead-of-time, + # The same applies to any tensor subclass metadata, when we have tangents that are subclasses. + # To handle this situation, we have two new methods that a tensor subclass can implement: + # (1) __coerce_tangent_metadata__(self) + # Given a subclass with "non-standard" metadata, turn it into a new subclass with "normal" metadata. + # The main example here is a DTensor with the "_Partial" placement. + # If we have a forward output with a _Partial placement, and corresponding tangent + # with a Replicate/Shard placement, we have no way to convert the tangent "back" to a _Partial placement. + # This method lets us avoid the problem entirely by allowing subclasses to ensure that we can never + # have a tangent with "problematic" metadata, that we cannot convert to. + # (1) __coerce_same_metadata_as_tangent__(self, metadata) + # Given a subclass, and a target differing metadata, + # convert self to have the same metadata as the target. + # With DTensor being the main example, we can use this to convert a DTensor with a Replicate() + # placement into one with a Shard() placement, in the case that we "guessed wrong", + # and traced tangents with a Shard() placement at compile time. + # + if is_subclass and hasattr(out, "__coerce_tangent_metadata__"): + out = out.__coerce_tangent_metadata__() # type: ignore[attr-defined] + + if is_subclass: + # pyrefly: ignore [missing-attribute] + attrs = out.__tensor_flatten__()[0] + + for attr in attrs: + elem = getattr(out, attr) + ( + new_elem, + new_elem_memory_format, + elem_updated, + ) = coerce_tangent_and_suggest_memory_format(elem) + # pyrefly: ignore [missing-attribute] + out_memory_format.append(new_elem_memory_format) + if elem_updated: + setattr(out, attr, new_elem) + + return out, out_memory_format, updated + + +# This is a version of functionalization that is specifically designed +# for the AOTAutograd use case. +# +# Unlike functorch's variant, this doesn't use the functorch level system, +# instead it directly uses PyTorch's conventional dispatcher to hit the +# functionalization key. In particular, this means that FunctionalTensorWrapper +# can have autograd data stored directly on it. +# +# In typical AOTAutograd usage, the dispatch key order will look like: +# +# Autograd - Functionalization ~~~~> Proxy Mode - Fake Tensor +# outer tensor inner tensor +# +# Returns: +# - ViewAndMutationMeta, telling us metadata about the inputs and outputs, and +# The list of outputs from the forward, but **only** the outputs that we need +# to pass in as tangents into the backward. +# Specifically, aliased outputs from the forward get regenerated, and don't participate +# in the compiled backward function. +def run_functionalized_fw_and_collect_metadata( + f: Callable[..., Any], + *, + flat_args_descs: list[AOTInput], + keep_input_mutations: bool, + # Note: this is guaranteed to be set when running under dynamo + static_input_indices: list[int] | None = None, + pre_dispatch: bool = False, +) -> Callable[..., ViewAndMutationMeta]: + memo: dict[Tensor, Tensor] = {} + + # TODO: see if we can rewrite this to be more accurate using + # overload + def _to_fun(t: object) -> object: + if isinstance(t, Tensor): + if t in memo: + return memo[t] + r = to_fun(t) + memo[t] = r + return r + else: + return t + + @simple_wraps(f) + def inner(*flat_args: Any) -> ViewAndMutationMeta: + # This function is meant to be run with the forward, which expects a flat list of tensor/symint/other args. + if not all( + isinstance(a, tuple(KNOWN_TYPES)) or is_opaque_type(type(a)) + for a in flat_args + ): + raise AssertionError("all flat_args must be KNOWN_TYPES or opaque types") + + input_info: list[InputAliasInfo] = [] + output_info: list[OutputAliasInfo] = [] + + prior_grad_enabled = torch.is_grad_enabled() + prior_autocast_states = _get_autocast_states() + + # See Note [Disabling Functionalize TLS Above Python Functionalization] + disable_above = torch._C._ExcludeDispatchKeyGuard( + torch._C.DispatchKeySet(torch._C.DispatchKey.Functionalize) + ) + + # It doesn't matter if we run this under predispatch or not because it is + # only for figuring out metadata + mode = FunctionalTensorMode(_allow_token_discovery=True) + suppress_pending = contextlib.nullcontext() + fake_mode = detect_fake_mode() + if fake_mode and (shape_env := fake_mode.shape_env): + suppress_pending = shape_env.ignore_fresh_unbacked_symbols() + with disable_above, mode, suppress_pending, disable_autocast_cache(): + # precondition: The passed in function already handles unflattening inputs + flattening outputs + flat_f_args = pytree.tree_map(_to_fun, flat_args) + flat_f_args_descs = flat_args_descs + flat_f_outs = f(*flat_f_args) + + # Assert that f does NOT have an AOTOutputs in it, easy mistake to + # make! You need to drop the second output before calling this + # function + if pytree.tree_any(lambda x: isinstance(x, AOTOutput), flat_f_outs): + raise AssertionError( + f"{f} returned AOTOutput when it shouldn't. Did you remember to wrap the " + "function with without_output_descs before passing it here?" + ) + + # NB: this is just to setup the input descriptors, we will + # recreate these descriptors (with the same convention!) when we + # actually do the trace + flat_f_outs_descs = [PlainAOTOutput(i) for i in range(len(flat_f_outs))] + + # We didn't do any tracing, so we don't need to process the + # unbacked symbols, they will just disappear into the ether. + # Also, prevent memoization from applying. + if fake_mode: + fake_mode.epoch += 1 + fake_mode.reset_nt_tensor_id_counter() + + if prior_autocast_states != _get_autocast_states(): + raise RuntimeError( + "AOTAutograd does not support tracing graphs that mutate the autocast state. " + "Dynamo will only insert autocast context managers (e.g. with torch.autocast(..)) into the graph, " + "which will unwind all of their mutations to autocast state before the graph exits. " + "If you encounter this error while using torch.compile, please file a bug." + ) + + # Inspect the state of the input tensor functional wrapper to detect input mutation info + # If inp[i] has a metadata-only mutation, then maybe_inputs_with_mutated_metadata[i] contains the updated version + for arg, f_arg in zip(flat_args, flat_f_args): + # NB: Mutation of non-contiguous tensor subclass input can result in a mismatch in + # strides between the functionalized arg inner tensors and non-functionalized arg inner + # tensors. This is a problem as the inner tensor stride change may not be reflected + # correctly in the outer tensor, so disallow this for now. + mutates_data = has_data_mutation(f_arg) + mutates_metadata = has_metadata_mutation( + f_arg, arg, check_only_storage_mutation=False + ) + if mutates_metadata and is_traceable_wrapper_subclass(arg): + raise RuntimeError( + "Metadata mutations are currently not allowed on tensor subclasses" + ) + mutates_storage_metadata = has_metadata_mutation( + f_arg, arg, check_only_storage_mutation=True + ) + mutations_hidden_from_autograd = are_all_mutations_hidden_from_autograd( + f_arg + ) + mutations_under_no_grad_or_inference_mode = ( + mutates_data + and are_all_mutations_under_no_grad_or_inference_mode(f_arg) + ) + mutation_inductor_storage_resize = was_inductor_storage_resized(f_arg) + + if mutates_storage_metadata: + mutates_data = False + + requires_grad = isinstance(f_arg, torch.Tensor) and f_arg.requires_grad + + input_info.append( + InputAliasInfo( + is_leaf=isinstance(arg, Tensor) and safe_is_leaf(arg), + mutates_data=mutates_data, + mutates_metadata=mutates_metadata, + mutations_hidden_from_autograd=mutations_hidden_from_autograd, + mutates_storage_metadata=mutates_storage_metadata, + mutations_under_no_grad_or_inference_mode=mutations_under_no_grad_or_inference_mode, + mutation_inductor_storage_resize=mutation_inductor_storage_resize, + requires_grad=requires_grad, + keep_input_mutations=keep_input_mutations, + ) + ) + + # If a function involves creating a tensor, and returning a view of it, such that its _base is the intermediate, + # We need to make sure our graph returns the _base as a graph output, and we manually recreate the view + # to return to the user. Why? The backend compiler is free to (incorrectly) not set requires_grad + # on the base tensor, but we are obligated to properly set requires-gradness on the real output. + + inp_storage_refs = { + StorageWeakRef(inpt.untyped_storage()): idx + for idx, inpt in enumerate(flat_f_args) + if isinstance(inpt, Tensor) + } + + # We need inp tensor id's to be able to tell if an outputs **are** inputs. + inp_tensor_ids = {id(inpt) for inpt in flat_f_args if isinstance(inpt, Tensor)} + # We need output tensor id's to tell if any output._base` attributes **are** other outputs. + # (This is also a dict because we need to know that output's index, so we can regenerate + # the alias from it). + out_tensor_ids = {id(o): i for i, o in enumerate(flat_f_outs)} + + # Keep track of which outputs alias other outputs + out_tensor_alias_counts: collections.defaultdict[StorageWeakRef | None, int] = ( + collections.defaultdict(int) + ) + # This tells us, for a given group of outputs that alias each other, + # whether they e.g. all came from an unbind call + num_aliased_tensors_that_are_multi_output_views: collections.defaultdict[ + StorageWeakRef | None, int + ] = collections.defaultdict(int) + + out_storage_to_metadata_key_to_tensors: collections.defaultdict[ + StorageWeakRef | None, + collections.defaultdict[MetadataKey, set[torch.Tensor]], + ] = collections.defaultdict(lambda: collections.defaultdict(set)) + + curr_storage = None + for o in flat_f_outs: + if isinstance(o, torch.Tensor): + curr_storage = StorageWeakRef(o.untyped_storage()) + out_tensor_alias_counts[curr_storage] += 1 + # Note: [AOTAutograd: differentiable outputs that alias each other from a multi-output view call] + # This is an optimization on top of the "alias of intermediates" logic, + # which you can read more about under Note [AOT Autograd: outputs aliasing inputs or intermediates!] + # + # Before describing the optimization: this is important for AOTAutograd to have good + # perf around, multi-output views. HOWEVER: + # - There is a more generic change to AOTAutograd that we'd like to make, that subsumes this case, + # around using pre-dispatch tracing to partition out a graph so we can faithfully replay all + # views without having to regenerate them at runtime. + # - It's loosely described in this doc (more details will be added soon): + # https://docs.google.com/document/d/1DlfFq8TKbuAn2zyJxLfoW-X1qkkm5PLdHFtySo03QAk/edit + # - Once that change lands, we should just rip out this "optimization", since: + # (1) It will be fully unnecessary + # (2) Although it is only a few lines of code, it is a bit difficult to reason about + # its correctness with the autograd engine in all cases. + # + # + # What is this optimization? Consider the below case: + # def f(x): + # intermediate = x.mul(2) + # # x and intermediate here require grad + # o1, o2, ... o10 = intermediate.unbind(-1) + # return intermediate, o1, o2, ... o10 + # Now, the "intermediate base" handling in AOTAutograd implies that we must do the following: + # (1) return "intermediate as an extra output of the compiled graph + # (2) regenerate each aliased output off of "intermediate", **outside** of the autograd.Function. + # The reason AOTAutograd ordinarily does this is for safety: the autograd engine needs to know + # that o1 through o10 are all aliased, and if we blindly return o1 through o10 from the autograd.Function, + # this information will be hidden. + # In particular, mutating one alias might require autograd to update autograd metadata on the other aliases + # (like their grad_fn, for example, when the autograd engine needs to do view-replay). + # + # However, intermediate_base logic can be bad for backward performance (we sometimes generate + # as_strided calls during the intermediate base logic, which can have a slow backward formula). + # Is it possible to find a set of conditions where it is **safe** to hide the output aliasing from autograd? + # + # For a set of outputs of the graph that alias each other, o_1...o_k, consider: + # (1) They came from the same multi-output view op, e.g. o_1, ..., o_k = intermediate.unbind(0) + # (2) If there are any other aliases of o_1 through o_k (in the example above, intermediate), + # **at most** 1 can escape from the graph (e.g. there is not some other graph input/output + # o_other, that aliases these outputs) + # (3) o_1...o_k all require_grad, they all share the same ._base, and their ._base requires grad. + # This condition is important because it's what causes slowness in the intermediate_base + # codepath of aot_autograd. Ordinarily, o_1...o_k would all get a grad_fn, and + # aot_autograd's view-replay might give each output an AsStridedBackward as its grad_fn. + # "K" AsStridedBackward calls will be *much* slower than a single UnbindBackward. + # In this setup, is it possible to mutate one of the outputs o_i in a way that would affect the autograd meta + # of the other aliases? + # + # Claim: No! Consider a few example (which I'm pretty sure cover all cases of mutation w.r.t. autograd): + # (a) What happens if we mutate any of o_1 through o_k directly? + # Autograd raises an error: + # "RuntimeError: Output 0 of UnbindBackward0 is a view and is being modified inplace. This view is + # the output of a function that returns multiple views. Such functions do not allow the output + # views to be modified inplace. You should replace the inplace operation by an out-of-place one." + # (b) What if we take a view of o_k and mutate it, o_k.view(o_k.shape).mul_(2)? + # Autograd raises the same error- the "multi-output-view"ness of an alias propagates to future views. + # (c) What if we mutate o_k under no_grad? + # Autograd raises the same error + # (d) What if we detach and mutate, e.g. o_k.detach().mul_(2)? + # Autograd allows this, *but* autograd updates all alias's grad_fn's to be error functions when accessed. + # Autograd raises the same error + # (e) What if we try to mutate another alias of o_1...o_k, that was **not** created from a multi-output view? + # We promised that there is at most **one** such alias, e.g. intermediate in the example above. + # You can mutate intermediate, but in eager mode this will change the grad_fn of o_1...o_k + # to be error fn's. + # Since intermediate was the *only* non-multi-output-alias, there are no other aliases + # of `intermediate` around that were produced by the compiled fn and have a valid grad_fn. + # + # Coming back to this optimization: + # Given that it is not possible for mutating one of these aliases to affect the autograd metadata of another alias + # without causing an error in eager mode, we will simple hide the aliasing from autograd during torch.compile + # if all of the above conditions are met. + # This has the slight downside that it's possible to write some "bad" code that autograd will raise an error on + # in eager but fail to during torch.compile, but it has the benefit that this code has much better performance. + # NOTE: if and when we eventually update AOTAutograd to do the "view graph slicing" defined here: + # https://docs.google.com/document/d/1DlfFq8TKbuAn2zyJxLfoW-X1qkkm5PLdHFtySo03QAk/edit, + # then this optimization will probably matter less and might be ok to remove. + is_cur_tensor_multi_out_view = isinstance( + o, FunctionalTensor + ) and torch._functionalize_is_multi_output_view( # type: ignore[attr-defined] + o.elem + ) + if is_cur_tensor_multi_out_view: + num_aliased_tensors_that_are_multi_output_views[curr_storage] += 1 + if o.requires_grad: + out_storage_to_metadata_key_to_tensors[curr_storage][ + MetadataKey.make(o) + ].add(o) + + # maps the id of an intermediate base to its index in the output of the compiled forward + intermediate_base_tensor_id_to_output_idx: dict[int, int] = {} + intermediate_bases: list[torch.Tensor] = [] + intermediate_bases_descs: list[AOTInput] = [] + # Why Do We Care If Storage Changed? + # It's important to understand the implications of storage changes in complex scenarios. Take this example: + # + # def f(x): + # x_storage = x.untyped_storage() + # non_leaf_tensor = torch.ones(4, requires_grad=True).clone() + # + # # Using no_grad() and _unsafe_preserve_version_counter to simulate the .data = operation + # with torch.no_grad(), torch.autograd._unsafe_preserve_version_counter(x): + # x.set_(non_leaf_tensor.untyped_storage()) + # + # out = x.view(-1) + # + # # Restoring x to its original storage, again simulating .data = operation + # with torch.no_grad(), torch.autograd._unsafe_preserve_version_counter(x): + # x.set_(x_storage) + # + # return out + # + # In this scenario, 'x' and 'out' have different shapes and are stored at different memory addresses, aka no aliasing. + # However, due to how set_() and more specificlaly, set is functionalized, is defined to preserve eager semantics, + # the autograd engine mistakenly assumes that 'x' and 'out' are aliased, treating 'x' as 'out._base'. + # This misinterpretation leads to an 'alias_of_input' flag, causing an unnecessary as_strided() call to be generated, + # which could lead to issues later in the code. + for o, desc in zip(flat_f_outs, flat_f_outs_descs): + functional_tensor_storage_changed = isinstance( + o, FunctionalTensor + ) and torch._functionalize_was_storage_changed( # type: ignore[attr-defined] + o.elem + ) + curr_storage = ( + None + if not isinstance(o, torch.Tensor) + else StorageWeakRef(o.untyped_storage()) + ) + outs_with_identical_metadata_that_require_grad: list[torch.Tensor] = ( + [] + if not isinstance(o, Tensor) + else [ + curr + for curr in out_storage_to_metadata_key_to_tensors[curr_storage][ + MetadataKey.make(o) + ] + if o is not curr + ] + ) + + # See Note [Accessing .grad_fn on FunctionalTensor] + # In-place operations on views will trigger a lazy rebase of the autograd graph; + # this runs during access to the .grad_fn. The rebase logic will invoke view ops + # on FunctionalTensors, so we must enable a FunctionalTensorMode here to ensure + # these op calls succeed. + grad_fn = None + if isinstance(o, Tensor): + with FunctionalTensorMode(): + grad_fn = o.grad_fn + + is_result_of_custom_autograd_fn = False + # Need to check for both custom cpp (CppFunction) and python (BackwardCFunction) + # autograd fns + if type(grad_fn).__name__ == "CppFunction": + is_result_of_custom_autograd_fn = True + if isinstance(grad_fn, torch.autograd.function.BackwardCFunction): + is_result_of_custom_autograd_fn = True + + if not isinstance(o, Tensor): + output_type = OutputType.non_alias + base_idx = None + elif ( + curr_storage in inp_storage_refs + and grad_fn is not None + and is_result_of_custom_autograd_fn + ): + output_type = OutputType.custom_function_view + base_idx = None + elif ( + curr_storage in inp_storage_refs + and not functional_tensor_storage_changed + ): + # pyrefly: ignore [bad-index, index-error] + base_idx = inp_storage_refs[curr_storage] + is_input_tensor = id(o) in inp_tensor_ids + num_aliased_outs = out_tensor_alias_counts[curr_storage] + num_multi_output_view_outs = ( + num_aliased_tensors_that_are_multi_output_views[curr_storage] + ) + num_aliased_outs_that_are_not_multi_output_views = ( + num_aliased_outs - num_multi_output_view_outs + ) + if ( + grad_fn is not None + and num_aliased_outs_that_are_not_multi_output_views == 0 + ): + # See Note: [AOTAutograd: differentiable outputs that alias each other from a multi-output view call] + # In particular, given: + # def f(x): + # return list(x.unbind(0)) + # The main reason we ordinarily try to regenerate these output aliases outside of the + # compiled autograd.Function is because if any of the outputs are later mutated, + # autograd needs to perform view-replay to regenerate them. + # However, autograd does not allow users to mutate multi-output views + # in any way that can change the autograd metadata of other aliases. + # So we hide this aliasing from autograd here. + log.debug( + "Encountered AOTAutograd case: differentiable outputs that \ +alias each other from a multi-output view call" + ) + output_type = OutputType.non_alias + elif is_input_tensor: + output_type = OutputType.is_input + else: + output_type = OutputType.alias_of_input + elif functional_tensor_storage_changed and id(o) in inp_tensor_ids: + # When there is a set_() on an input, we cannot rely on checking storages + # to detect if we are returning an input (since the inputs storage is different) + if curr_storage is None: + raise AssertionError("curr_storage must not be None") + base_idx = inp_storage_refs[curr_storage] + output_type = OutputType.is_input + + # We only need to handle the intermediate base case when both + # the intermediate base and the output require gradients. + # See Note [AOT Autograd: outputs aliasing inputs or intermediates!] + elif o._base is not None and o.requires_grad and o._base.requires_grad: + num_aliased_outs = out_tensor_alias_counts[curr_storage] + num_multi_output_view_outs = ( + num_aliased_tensors_that_are_multi_output_views[curr_storage] + ) + num_aliased_outs_that_are_not_multi_output_views = ( + num_aliased_outs - num_multi_output_view_outs + ) + # Note: [AOTAutograd: differentiable outputs that alias each other from a multi-output view call] + if ( + out_tensor_alias_counts[curr_storage] == 1 + or num_aliased_outs_that_are_not_multi_output_views <= 1 + ): + # Note [Intermediate Bases Optimization] + # Normally if we have an output that aliases an intermediate, + # we need to add the extra "intermediate base" logic further down + # to prevent autograd from yelling at us if the user later tries to + # mutate that output. + # However, the common case here is if we have an output that aliases an intermediate, + # but doesn't alias any other outputs. + # In that case, autograd shouldn't have to worry about the aliasing at all + # (if that output is mutated, there are no other live aliases for autograd to worry about). + # The "intermediate bases" can hurt inductor perf by forcing more variables to become outputs. + # So as an optimization, we won't do intermediate base handling in this case. + # Instead, we'll hide the aliasing from autograd using aten._unsafe_view(). + if ( + out_tensor_alias_counts[curr_storage] != 1 + and num_aliased_outs_that_are_not_multi_output_views <= 1 + ): + log.debug( + "Encountered AOTAutograd case: differentiable outputs that alias each other \ +from a multi-output view call" + ) + output_type = OutputType.unsafe_view_alias + base_idx = None + else: + # First, check if o's ._base is an existing output + maybe_existing_out_idx = out_tensor_ids.get(id(o._base)) + if maybe_existing_out_idx is not None: + # Special case where the output is an alias of a graph intermediate, but that intermediate + # is itself also a user output. + output_type = ( + OutputType.alias_of_intermediate_base_is_user_output + ) + base_idx = maybe_existing_out_idx + else: + # Next, check if o's ._base is an intermediate base that we already returned + maybe_existing_base_output_idx = ( + intermediate_base_tensor_id_to_output_idx.get(id(o._base)) + ) + if maybe_existing_base_output_idx is not None: + output_type = OutputType.alias_of_intermediate + base_idx = maybe_existing_base_output_idx + else: + # Otherwise, take o._base and explicitly return it as an output in the compiled graph + new_out_idx = len(intermediate_bases) + base_idx = new_out_idx + # Indicate to the logic later on (when we trace the joint) + # that this particular output should get it's ._base appended to the forward graph outputs + output_type = ( + OutputType.alias_of_intermediate_save_as_output + ) + intermediate_base_tensor_id_to_output_idx[id(o._base)] = ( + new_out_idx + ) + intermediate_bases.append(o._base) + # NB: The desc we picked here is guaranteed to be + # synchronized with the one in + # graph_capture_wrappers.py because we + # SPECIFICALLY notated this output as + # alias_of_intermediate_save_as_output + intermediate_bases_descs.append( + TangentAOTInput(IntermediateBaseAOTOutput(desc)) + ) + elif ( + # See https://github.com/pytorch/pytorch/issues/100348 for this case. + # This protects against the specific case where a user fn returns (output, output.detach()) + out_tensor_alias_counts[curr_storage] > 1 + and len(outs_with_identical_metadata_that_require_grad) > 0 + and not o.requires_grad + ): + # In theory we could use any of these tensors to regenerate the aliased outputs from, + # since they all alias each other and have identical metadata + out_alias = outs_with_identical_metadata_that_require_grad[0] + existing_out_idx = out_tensor_ids[id(out_alias)] + output_type = OutputType.alias_of_intermediate_base_is_user_output + base_idx = existing_out_idx + else: + output_type = OutputType.non_alias + base_idx = None + + if isinstance(o, torch.Tensor): + dynamic_dims = { + i for i, s in enumerate(o.shape) if not is_concrete_int(s) + } + else: + dynamic_dims = None + + # Save the current FunctionalTensor output. + # + # This will be used at runtime for reconstructing output views from + # their respective base tensors. + # + # The FunctionalTensor will be saved if one of the 2 conditions below + # is true: + view_meta_sequence = None + if ( + # 1. If the output_type is either of: + # (i) alias_of_intermediate; + # (ii) alias_of_intermediate_save_as_output; or + # (iii) alias_of_intermediate_base_is_user_output. + # + # No need to worry about in-place view operations here, since + # this functionalization step elimitates mutations. + # + # i.e. we have access to the actual base tensor, before the + # in-place operation was applied. + output_type + in ( + OutputType.alias_of_intermediate, + OutputType.alias_of_intermediate_save_as_output, + OutputType.alias_of_intermediate_base_is_user_output, + ) + ) or ( + # 2. If the output_type is alias_of_input, and no in-place view + # operationthe was run on the input (base tensor). + # + # In this case, we need to check for metadata mutation because + # the runtime explicitly reconstructs the inputs, before actually + # reconstructing the outputs. Due to in-place view operations, the + # fully reconstructed input may not be this output base tensor + # anymore. + output_type == OutputType.alias_of_input + and base_idx is not None + and not input_info[base_idx].mutates_metadata + ): + if isinstance(o, FunctionalTensor): + view_meta_sequence = ViewMetaSequence(o) + + requires_grad = isinstance(o, torch.Tensor) and o.requires_grad + out_info = OutputAliasInfo( + output_type=output_type, + raw_type=type(o), + base_idx=base_idx, + dynamic_dims=dynamic_dims, + requires_grad=requires_grad, + # A view created under no_grad() inherits requires_grad from + # its base but has no grad_fn and does not participate in + # differentiation. + requires_grad_for_backward=requires_grad + and (o._base is None or grad_fn is not None), + view_meta_sequence=view_meta_sequence, + ) + output_info.append(out_info) + + # See Note [AOT Autograd: Views to avoid tangents aliasing inputs] + def view_avoid_dupes_with_primals(t: object) -> object: + if isinstance(t, Tensor) and is_traceable_wrapper_subclass(t): + return transform_subclass( + t, lambda _, inner_t: view_avoid_dupes_with_primals(inner_t) + ) + if isinstance(t, Tensor): + return t.view(t.shape) + return t + + # This analysis function returns *only* the outputs that are meant to be tangents to the backwards. + # Anything that aliases (inputs returned in the fw due to metadata mutations, or outputs that alias inputs/intermediates) + # are *regenerated* later, and not used directly in the autograd graph + def _plain_fake_tensor_like_subclass(x: Any) -> torch.Tensor: + # pyrefly: ignore [bad-context-manager] + with detect_fake_mode(): + return torch.empty( + x.shape, dtype=x.dtype, device=x.device, layout=x.layout + ) + + def _is_subclass_mutated_input_tangent_always_subclass(inp: object) -> bool: + return ( + isinstance(inp, torch.nested._internal.nested_tensor.NestedTensor) + or torch._functorch.config.disable_guess_zero_tangent_for_mutated_input_subclass + ) + + f_input_tangents_pairs = [ + # Note: [AOTAutograd Tangent Subclassness for mutated inputs] + # Generally when creating tangents to trace with, we assume that tangents will have + # the same subclass-ness as their forward outs + # however: for tangents that correspond to input mutations, in practice it is more likely + # that these tangents will be plain tensors of zeros at runtime, so we tweak our guess + # to assume that these tangents should always be plaint tensors. + # Example: + # def f(x): + # x.mul_(2) + # return x + 1 + # out = f(x) + # out.sum().backward() + # In the above code, we will have a tangent "x_updated_tangent", + # which will be a plain tensor of zeros, *unless* x is used in some compute after executing f + # + # However, there are exceptions to this logic. If a view is created from mutated input and is used in backward, + # The tangent for this subclass input will be a subclass tensor. + # Example: + # def f(a, b): + # a.mul_(2) + # b.mul_(3) + # return b.view(b.shape), a + b + # a_out, b_out = f(..., Subclass) + # (a * b).sum().backward() + # + # We can not deduce it easily now, so introducing a debug config to be able to turn off this for specific cases. + # NJT guarantees to have its tangent as NJT, because it has dedicated integration in Autograd + # See torch/csrc/autograd/python_function.cpp, use_zeros_like. + ( + ( + _plain_fake_tensor_like_subclass(inp) + if is_traceable_wrapper_subclass(inp) + and not _is_subclass_mutated_input_tangent_always_subclass(inp) + else inp + ), + TangentAOTInput(InputMutationAOTOutput(inp_desc)), + ) + for inp, inp_desc, info in zip(flat_f_args, flat_f_args_descs, input_info) + if info.mutation_type == MutationType.MUTATED_OUT_GRAPH + and info.mutates_data + and info.requires_grad + ] + f_input_tangents, f_input_tangents_descs = ( + [x[0] for x in f_input_tangents_pairs], + [x[1] for x in f_input_tangents_pairs], + ) + + f_output_tangents_pairs = [ + (o, TangentAOTInput(desc)) + for o, info, desc in zip(flat_f_outs, output_info, flat_f_outs_descs) + if info.output_type + in [ + OutputType.non_alias, + OutputType.unsafe_view_alias, + OutputType.custom_function_view, + ] + and issubclass(info.raw_type, torch.Tensor) + and info.requires_grad_for_backward + ] + f_output_tangents, f_output_tangents_descs = ( + [x[0] for x in f_output_tangents_pairs], + [x[1] for x in f_output_tangents_pairs], + ) + + # intermediate bases are also included in the backward graph + f_tangents = f_input_tangents + f_output_tangents + intermediate_bases + f_tangents_descs = ( + f_input_tangents_descs + f_output_tangents_descs + intermediate_bases_descs + ) + + # TODO: I'm pretty sure you don't need a tree_map here + traced_tangents = pytree.tree_map(from_fun, f_tangents) + traced_tangents = pytree.tree_map( + view_avoid_dupes_with_primals, traced_tangents + ) + traced_tangents = [ + coerce_tangent_and_suggest_memory_format(tt)[0] + for i, tt in enumerate(traced_tangents) + ] + # NB: update this if the maps above ever change structure. + # Also, it might be helpful to add coercion information to the tangent desc! + traced_tangents_descs = f_tangents_descs + + nonlocal static_input_indices + static_input_indices = static_input_indices or [] + if torch._dynamo.compiled_autograd.in_compiled_autograd_region: + passed_indices = set(static_input_indices) + static_input_indices = [ + i + for i, arg in enumerate(flat_args) + if (isinstance(arg, torch.nn.Parameter) or i in passed_indices) + ] + + static_input_logger.debug( + "static input indices metadata analysis: %s", static_input_indices + ) + + f_mutated_inputs = [ + inp + for inp, info in zip(flat_f_args, input_info) + if info.mutation_type == MutationType.MUTATED_OUT_GRAPH + ] + # Build the full list of forward graph outputs so the subclass wrapping + # code knows exactly which graph outputs to wrap back into subclasses. + # Including intermediate_bases unconditionally is safe: they are only + # populated when outputs require grad (line ~539), so they are naturally + # empty during pure inference. In the "downgrade from training to + # inference" path, num_intermediate_bases > 0 is already gated behind + # `assert not req_subclass_dispatch` (aot_autograd.py), so the subclass + # wrapping code that consumes subclass_fw_graph_out_meta never sees them. + f_fw_graph_outs = [*f_mutated_inputs, *flat_f_outs, *intermediate_bases] + fw_graph_outs = pytree.tree_map(from_fun, f_fw_graph_outs) + + grad_enabled_mutation = None + if torch.is_grad_enabled() != prior_grad_enabled: + grad_enabled_mutation = torch.is_grad_enabled() + torch.set_grad_enabled( + prior_grad_enabled + ) # Restore the prior state after tracing it + log.debug( + ( + "grad_mode mutation encountered in graph. " + "Will emit mutation epilogue, to set grad_mode=%s" + ), + grad_enabled_mutation, + ) + + subclass_inp_meta = create_subclass_meta(flat_args) + subclass_fw_graph_out_meta = create_subclass_meta(fw_graph_outs) + subclass_tangent_meta = create_subclass_meta( + traced_tangents, count_symints=False, with_memory_format=True + ) + + metadata = ViewAndMutationMeta( + input_info=input_info, + output_info=output_info, + num_intermediate_bases=len(intermediate_bases), + keep_input_mutations=keep_input_mutations, + traced_tangents=traced_tangents, + traced_tangents_descs=traced_tangents_descs, + subclass_inp_meta=subclass_inp_meta, + subclass_fw_graph_out_meta=subclass_fw_graph_out_meta, + subclass_tangent_meta=subclass_tangent_meta, + grad_enabled_mutation=grad_enabled_mutation, + static_input_indices=static_input_indices, + tokens=mode._tokens, + ) + return metadata + + return inner diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/descriptors.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/descriptors.py new file mode 100644 index 0000000000000000000000000000000000000000..5c55074637ffd7686af3c409721a6a03a9be780a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/descriptors.py @@ -0,0 +1,782 @@ +""" +AOTAutograd descriptors are a path-like data structure (similar to pytree +paths and sources) that describe the semantic meaning of an input/output to FX +graphs. Although you may know the input/output meaning at the top level of +the original function you traced, because we have many graph capture wrappers +that change the calling convention, it can be difficult to tell how these +correspond to the actual FX graph you get back, to say nothing about the extra +arguments/outputs for tangents, gradients, etc. Descriptors describe the meaning +of arguments. + +Examples +-------- + +Before we talk about the precise semantics, it's helpful to look at some +examples to get some intuition for the meaning of descriptors. Here are some +input descriptors you might find on the joint FX graph: + +* PlainAOTInput(idx=0) - the first input from the original callable, as is + +* ParamAOTInput(target="mod.weight") - the parameter with FQN mod.weight + +* TangentAOTInput(output=PlainAOTOutput(idx=1)) - the input tangent + corresponding to the gradients for the second output in the forward graph + +* ViewBaseAOTInput(base_of=PlainAOTInput(idx=0)) - it turned out the first + input was actually a (differentiable) view of a tensor which aliased with + another input tensor. We replaced this input with a single input for the + base of all of these inputs, replacing the original inputs (one of which is + mentioned in base_of). We would generate a GradAOTOutput for *this* input + (and not the original PlainAOTInputs!) If you have a joint graph where a + view base like this is undesirable, you can eliminate this by cloning + the views outside of the compiled region (assuming you aren't mutating this + tensor). + +* SubclassGetAttrAOTInput(base=AOTInput(idx=0), attr="inner") - this tensor + corresponds to the "inner" tensor from the tensor subclass that is at the + first index. In general, joint graphs from AOTAutograd never take tensor + subclasses as inputs; they are always unpacked into their constituent plain + tensor pieces; use the descriptors to identify the parts of the tensor that + are related. Note that this can be nested (if you have nested tensor + subclasses!) + +Here are some output descriptors you might find on the Joint FX graph: + +* PlainAOTOutput(idx=0) - the first output from the original forward function, + as is + +* GradAOTOutput(grad_of=PlainAOTInput(idx=1)) - the computed gradient for the + second input to the graph, an output of the backward graph + +* InputMutationAOTOutput(mutated_input=PlainAOTInput(idx=0)) - when the first + input is mutated, the new value to be copied into the first input of the + graph. Sometimes, these outputs can be elided and the ``copy_`` is done directly + in the graph (controlled by keep_input_mutations), but if the input + mutation must be differentiated through we always generate an output like this + +* IntermediateBaseAOTOutput(base_of=PlainAOTOutput(idx=0)) - if we return + multiple outputs which alias each other, we instead replace them with a single + output tensor representing the base of all the aliases. This output indicates + it is the base for /one/ of those original outputs. If this is undesirable in + the joint graph, clone all outputs before returning from the graph. + +* SubclassGetAttrAOTOutput(base=PlainAOTOutput(idx=0), idx="inner") - this + tensor correspondings to the inner tensor of the first original output which + is a tensor subclass. This and other subclass components of that output will + get repacked into a tensor subclass. + +High level semantics +-------------------- + +OK, let's formally define a descriptor. Intuitively, suppose we have:: + + def wrapped_graph(*args): + ret = graph(*in_transform(args)) + return out_transform(ret) + +Then the descriptor for input[i] to graph describes a function fin_i such that:: + + fin_i(args) == in_transform(args)[i] + +and the descriptor for output[j] from graph describes a function fout_j such that:: + + fout_j(out_transform(ret)) == ret[j] + +AKA input descriptors tell you how to get from outer inputs to inner inputs, +while output descriptors tell you how to get from outer outputs to inner +outputs (inverse data flow!) + +We haven't said anything about what these transformations actually do. There +are three major transformations AOTAutograd does (performed in this order): + +* View/mutation handling +* Autograd +* Subclasses + +So intuitively, descriptors are built like this: + +1. **PlainAOTInput, PlainAOTOutput.** + + We start off descriptors describing the exact inputs/outputs of the + original flattened user function. This user function is assumed to already + be flattened; you would chain on pytree KeyPaths to further describe where + in the pytree each input/output lived if you needed to deal with + unflattened functions: this can be done from userland on top of + descriptors, so the main descriptors mechanism doesn't handle it. + +2. **SyntheticBaseAOTInput, ViewBaseAOTInput, MetadataMutationAOTOutput, + InputMutationAOTOutput, IntermediateBaseAOTOutput** + + We deal with mutations and aliasing by removing duplicate PlainAOTInputs + and introduce some new artificial inputs/outputs. These inputs do not + have a straightforward correspondence to the original user inputs, but if + you are implementing a pass that doesn't care about the exact semantics of + inputs, you should handle all of these uniformly in the same way as regular + inputs. + +3. **TangentAOTInput, GradAOTOutput** + + We deal with autograd by introducing a tangent input for every + differentiable AOTOutput (including the new ones introduced above), and a + gradient output for every differentiable AOTInput (also including new ones + introduced above.) The arguments to these AOTInput/AOTOutput can ONLY be + the ones we already have above (from steps 1-2). As AOTAutograd does not + currently support double backwards, you never have tangents of grads or + vice versa (but in the future we could!) + +4. **SubclassGetAttrAOTInput, SubclassGetAttrAOTOutput, et al.** + + We deal with subclasses by introducing flattened inputs/outputs (including + potentially symbolic sizes/strides) for every AOTInput/AOTOutput that was a + subclass. As above, the arguments to these AOTInput/AOTOutput can ONLY be + the ones we have above (from steps 1-3). Recursive subclasses are + supported, so these descriptors can nest with each other (so descriptors + from step 4 are fair game as well.) + +5. **ForwardTokenAOTInput, ForwardTokenAOTOutput, BackwardTokenAOTInput, BackwardTokenAOTOutput.** + + Some extra token inputs/outputs get added, these are synthetic and are just here to + prevent DCE/reordering. + +The important thing about the pipeline is that descriptors can ONLY be +created from top-to-bottom. So for example, you can have:: + + SubclassGetAttrAOTInput(TangentAOTInput(PlainAOTOutput(...))) # OK + +As you can see that PlainAOTOutput -> TangentAOTInput -> +SubclassGetAttrAOTInput is consistent with the pipeline ordering), but you can +NEVER have:: + + TangentAOTInput(SubclassGetAttrAOTOutput(PlainAOTOutput(...)) # BAD + +This is inconsistent; we always do autograd BEFORE we process subclasses! + +Similarly, for example, this is illegal:: + + GradAOTOutput(SubclassGetAttrAOTInput(PlainAOTInput(...))) # BAD + +It is illegal because subclasses are handled *after* create joint during +wrapper construction. Instead, you would have:: + + SubclassGetAttrAOTOutput(GradAOTOutput(PlainAOTInput(...))) # OK + +This intuitively captures the fact that we always to autograd directly on the +subclass, rather than after desugaring the subclass into its inner tensors. + +Descriptor index +---------------- + +Here is a list of all AOTInput/AOTOutput, organized by how likely you need to +handle them: + +* AOTInput + + * Important: + + * PlainAOTInput (the primals!) + * ParamAOTInput + * TangentAOTInput + * SubclassGetAttrAOTInput et al. (if you use subclasses) + + * View related (can be eliminated by cloning inputs to graph; if you don't + eliminate them, make sure to handle pairing them with GradAOTOutput): + + * ViewBaseAOTInput + * SyntheticBaseAOTInput + + * Non-tensor, mostly just ignore them: + + * DummyAOTInput + * PhiloxForwardSeedAOTInput + * PhiloxForwardBaseOffsetAOTInput + * PhiloxBackwardSeedAOTInput + * PhiloxBackwardBaseOffsetAOTInput + * ForwardTokenAOTInput + * BackwardTokenAOTInput + +* AOTOutput + + * Important: + + * PlainAOTOutput + * GradAOTOutput + * SubclassGetAttrAOTOutput et al. (if you use subclasses) + + * More obscure (if not eliminated, make sure you handle pairing them with + TangentAOTInput): + + * InputMutationAOTOutput (can be eliminated if mutations are non-differentiable) + * IntermediateBaseAOTOutput (can be eliminated by cloning outputs of graph) + * MetadataMutationAOTOutput (uhh, just don't mutate metadata?) + + * Non-tensor, mostly just ignore them: + + * PhiloxUpdatedForwardOffsetAOTOutput + * PhiloxUpdatedBackwardOffsetAOTOutput + * ForwardTokenAOTOutput + * BackwardTokenAOTOutput + * DummyAOTOutput + +For convenience, we also have DifferentiableAOTInput and +DifferentiableAOTOutput to help you classify which inputs/outputs can be +wrapped by GradAOTOutput/TangentAOTInput (respectively), which are essentially +all tensor AOTInput/AOTOutput excluding the subclass descriptors. + +Implementation details +---------------------- + +The stylized view above is good for understanding how to interpret +descriptors, but the way that descriptors are generated in code is a bit more +complicated. Specifically, AOTAutograd is structured as a series of wrappers +on the original user function, which are composed together to form the final +function to trace. As a result of this, AOTAutograd ends up first building +the full AOTInputs for a function to be traced (as it builds the wrappers and +modifies the flat arguments to be compatible with the new input signature of +the wrapper), and then in reverse builds up the AOTOutput as it is tracing. + +There is one major exception to this general idea of "build AOTInput first", +and then "build AOTOutput second": when we create TangentAOTInput, we need to +reference AOTOutputs (which output we are the tangents of) which we generally +haven't created yet. There's two ways we deal with this: + +- After the precompile steps (dedup and synthetic base handling), we do an + initial pass to collect forward metadata that produces the initial set of + PlainAOTOutputs which we use to create the tangent inputs. + +- We also sometimes just violate causality and predict that an AOTOutput will + be created in a particular way at some later point in time when we build an + AOTInput. + +As of July 2025, here is an exhaustive description of how inputs/outputs +traverse the wrappers from AOTAutograd, and what descriptors can be introduced +at these phases. + +:: + + Build wrappers (FLOWS DOWN) Run trace (FLOWS UP) + ------------------------------------------------------------------------------------------------- + Begin PlainAOTInput (n/a) + ParamAOTInput + + Precompile dedupe (remove dupes) (nothing) + + Precompile synthetic base SyntheticBaseAOTInput MetadataMutationAOTOutput + ViewBaseAOTInput + + Forward metadata trace PlainAOTOutput (n/a) + MetadataMutationAOTOutput + + Prepare for autograd (nothing) InputMutationAOTOutput + IntermediateBaseAOTOutput + + Create joint TangentAOTInput GradAOTOutput + w/ InputMutationAOTOutput + w/ IntermediateBaseAOTOutput + + Precompile subclass SubclassGetAttrAOTInput et al. SubclassGetAttrAOTOutput et al. + + Effect tokens ForwardTokenAOTInput ForwardTokenAOTOutput + BackwardTokenAOTInput BackwardTokenAOTOutput + + End (n/a) PlainAOTOutput + +It can be helpful to separately write down the input flow and the output flow +for ease of understanding the data flow: + +* Input desc propagation (happens as we build wrappers) + + * [IN] Begin with original calling convention (PlainAOTInput, ParamAOTInput) + * [IN] Precompile dedupe: (removes duplicate AOTInputs) + * [IN] Precompile synthetic base: SyntheticBaseAOTInput, ViewBaseAOTInput + * Forward metadata trace (mini output desc propagation) + + * [OUT] Original output convention: PlainAOTOutput + * [OUT] Precompile synthetic base: MetadataMutationAOTOutput + + * [IN] Prepare for autograd: (nothing) + * [IN] Create joint: TangentAOTInput (potentially w/ + IntermediateBaseAOTOutput, InputMutationAOTOutput) + * [IN] Precompile subclass: SubclassGetAttrAOTInput et al. + * [IN] Effect tokens: ForwardTokenAOTInput, BackwardTokenAOTInput + (Note: BackwardTokenAOTInput is technically generated not by a wrapper but + actually done by token_discovery which implicitly adds extra arguments + to the FX trace on-the-fly.) + +* Trigger a trace with the modified inputs on the wrapper +* Output desc propagation (happens as we unwind from the user function call in trace) + + * [OUT] Begin with original calling convention: PlainAOTOutput + * [OUT] Effect tokens: ForwardTokenAOTOutput, BackwardTokenAOTOutput + * [OUT] Precompile subclass: SubclassGetAttrAOTOutput et al. + * [OUT] Create joint: GradAOTOutput + * [OUT] Prepare for autograd: InputMutationAOTOutput, IntermediateBaseAOTOutput + * [OUT] Precompile synthetic base: MetadataMutationAOTOutput + * [OUT] Precompile dedupe: (nothing) +""" + +import dataclasses + + +# TODO: the is_* predicates are a little suspicious because (1) they're not +# used by anything and (2) they always report False even when a parameter got +# swizzled into a view base or deduped with a non-parameter. It is pretty +# difficult to exercise these cases but it's not clear if you will write code +# that works correctly in those cases. + + +@dataclasses.dataclass(frozen=True) +class AOTInput: + """Describes where an input from an AOTAutograd produced FX graph comes from""" + + def expr(self) -> str: + raise NotImplementedError("Subclasses must implement expr()") + + def is_param(self) -> bool: + """True if this input is a parameter or derived from a parameter (e.g., subclass attr)""" + return False + + def is_buffer(self) -> bool: + """True if this input is a buffer or derived from a buffer (e.g., subclass attr)""" + return False + + def is_tangent(self) -> bool: + """True if this input is a tangent or derived from a tangent (e.g., subclass attr)""" + return False + + +# Note: Currently, our typing discipline for differentiable versus not is not +# very good, so feel free to rely on runtime tests instead. + + +@dataclasses.dataclass(frozen=True) +class DifferentiableAOTInput(AOTInput): + """A subclass that classifies AOTInput that can be wrapped by GradAOTOutput""" + + +@dataclasses.dataclass(frozen=True) +class AOTOutput: + """Describes where an output from an AOTAutograd produced FX graph will + eventually be bundled into the final output""" + + def expr(self) -> str: + raise NotImplementedError("Subclasses must implement expr()") + + def is_grad(self) -> bool: + """True if this output is a grad or derived from a grad (e.g., subclass attr)""" + return False + + +@dataclasses.dataclass(frozen=True) +class DifferentiableAOTOutput(AOTOutput): + """A subclass that classifies AOTOutput that can be wrapped by TangentAOTInput""" + + +# ------------ + +# AOTInput + +# ------------ + + +@dataclasses.dataclass(frozen=True) +class ParamAOTInput(DifferentiableAOTInput): + """The input is a parameter, whose FQN is target""" + + target: str + + def expr(self) -> str: + return f"self.get_parameter({self.target!r})" + + def is_param(self) -> bool: + return True + + def is_buffer(self) -> bool: + return False + + +@dataclasses.dataclass(frozen=True) +class BufferAOTInput(DifferentiableAOTInput): + """The input is a buffer, whose FQN is target""" + + target: str + + def expr(self) -> str: + return f"self.get_buffer({self.target!r})" + + def is_param(self) -> bool: + return False + + def is_buffer(self) -> bool: + return True + + +@dataclasses.dataclass(frozen=True) +class DummyAOTInput(AOTInput): + """In some circumstances, we want to call into a function that expects AOTInput, but + we don't actually care about that logic (most typically, because some code is being used + for both compile-time and run-time; AOTInput processing is not needed in this situation. + Pass a dummy in this situation; but it is better to just have a version of the function + that doesn't have this at all.""" + + idx: int + + def expr(self) -> str: + return f"__dummy{self.idx}" + + +@dataclasses.dataclass(frozen=True) +class PlainAOTInput(DifferentiableAOTInput): + """The input is a plain input, corresponding to a particular positional index. + + Note that AOTInput is always relative to a function with a *flat* calling convention, + e.g., as accepted by `aot_module_simplified`. There are some AOTAutograd APIs that + flatten pytrees, and we don't record PyTree key paths from the flattening (but we + could and should!) + """ + + idx: int + + def expr(self) -> str: + return f"args[{self.idx}]" + + +@dataclasses.dataclass(frozen=True) +class SubclassGetAttrAOTInput(AOTInput): + """Subclass inputs get unpacked into their constituent pieces before going into an FX + graph. This tells you which particular attribute of the subclass this particular + input corresponds to (of the 'base' originally subclass argument.) + """ + + base: AOTInput + attr: str + + def expr(self) -> str: + return f"{self.base.expr()}.{self.attr}" + + def is_param(self) -> bool: + return self.base.is_param() + + def is_buffer(self) -> bool: + return self.base.is_buffer() + + def is_tangent(self) -> bool: + return self.base.is_tangent() + + +@dataclasses.dataclass(frozen=True) +class SubclassSizeAOTInput(AOTInput): + """Which subclass this particular outer size SymInt input (at dim idx) came from.""" + + base: AOTInput + idx: int + + def expr(self) -> str: + return f"{self.base.expr()}.size({self.idx})" + + +@dataclasses.dataclass(frozen=True) +class SubclassStrideAOTInput(AOTInput): + """Which subclass this particular outer stride SymInt input (at dim idx) came from.""" + + base: AOTInput + idx: int + + def expr(self) -> str: + return f"{self.base.expr()}.stride({self.idx})" + + +@dataclasses.dataclass(frozen=True) +class ViewBaseAOTInput(DifferentiableAOTInput): + """ + When multiple differentiable inputs are views of the same input, AOTAutograd will replace all of these + views with a single input representing the base. If this is undesirable, you can clone the views + example inputs before passing them into AOTAutograd. + + TODO: In principle we could report ALL of the inputs who this is a base of. + """ + + base_of: AOTInput + + def expr(self) -> str: + return f"{self.base_of.expr()}._base" + + +@dataclasses.dataclass(frozen=True) +class SyntheticBaseAOTInput(DifferentiableAOTInput): + """This is similar to ViewBaseAOTInput, but this happens when none of the views were differentiable, so + we weren't able to get our hands on the true original view and constructed a synthetic one instead + for the sake of autograd. + """ + + base_of: AOTInput + + def expr(self) -> str: + return f"__make_synthetic_base({self.base_of.expr()})" + + +@dataclasses.dataclass(frozen=True) +class PhiloxForwardSeedAOTInput(AOTInput): + """The seed for functionalized Philox RNG calls, specifically for forward graph.""" + + def expr(self) -> str: + return "__philox_forward_seed" + + +@dataclasses.dataclass(frozen=True) +class PhiloxForwardBaseOffsetAOTInput(AOTInput): + """The offset for functionalized Philox RNG calls, specifically for forward graph.""" + + def expr(self) -> str: + return "__philox_forward_base_offset" + + +@dataclasses.dataclass(frozen=True) +class PhiloxBackwardSeedAOTInput(AOTInput): + """The seed for functionalized Philox RNG calls, specifically for backward graph.""" + + def expr(self) -> str: + return "__philox_backward_seed" + + +@dataclasses.dataclass(frozen=True) +class PhiloxBackwardBaseOffsetAOTInput(AOTInput): + """The offset for functionalized Philox RNG calls, specifically for backward graph.""" + + def expr(self) -> str: + return "__philox_backward_base_offset" + + +@dataclasses.dataclass(frozen=True) +class ForwardTokenAOTInput(AOTInput): + """The world token which is threaded through side-effectful operations""" + + idx: int + + def expr(self) -> str: + return f"__forward_token{self.idx}" + + +@dataclasses.dataclass(frozen=True) +class BackwardTokenAOTInput(AOTInput): + """The world token which is threaded through side-effectful operations, for backwards""" + + idx: int + + def expr(self) -> str: + return f"__backward_token{self.idx}" + + +# Technically the "output" here is redundant, tangents always correspond to +# outputs +# NB: this is marked differentiable as it /would/ be differentiable if we +# support double backwards, but we never generate this today because we +# don't support double backwards. +@dataclasses.dataclass(frozen=True) +class TangentAOTInput(DifferentiableAOTInput): + """An input to the joint graph representing the tangent of an output.""" + + output: DifferentiableAOTOutput + + def __post_init__(self) -> None: + if not isinstance(self.output, DifferentiableAOTOutput): + raise AssertionError( + f"expected output to be DifferentiableAOTOutput, got {type(self.output)}" + ) + + def expr(self) -> str: + return f"__output_tangent({self.output.expr()})" + + def is_tangent(self) -> bool: + return True + + +# ------------ + +# AOTOutput + +# ------------ + + +@dataclasses.dataclass(frozen=True) +class PlainAOTOutput(DifferentiableAOTOutput): + """A plain tensor output at position idx of the output tuple""" + + idx: int + + def expr(self) -> str: + return f"output[{self.idx}]" + + +@dataclasses.dataclass(frozen=True) +class InputMutationAOTOutput(DifferentiableAOTOutput): + """The mutated value of an input tensor, returned so we can appropriately propagate autograd.""" + + mutated_input: AOTInput + + def expr(self) -> str: + return f"__input_mutation({self.mutated_input.expr()})" + + +@dataclasses.dataclass(frozen=True) +class IntermediateBaseAOTOutput(DifferentiableAOTOutput): + """An intermediate base of multiple outputs which alias each other. We only report ONE of + the outputs that contributed to this base""" + + base_of: "AOTOutput" + + def expr(self) -> str: + return f"__intermediate_base({self.base_of.expr()})" + + +# TODO: it's a little dodgy this is differentiable lol, but we do generate +# these BEFORE autograd is handled +@dataclasses.dataclass(frozen=True) +class MetadataMutationAOTOutput(DifferentiableAOTOutput): + idx: int + + def expr(self) -> str: + return f"__aliased_arg_with_metadata_mutation{self.idx}" + + +# NB: this is marked differentiable as it /would/ be differentiable if we +# support double backwards, but we never generate this today because we +# don't support double backwards. +@dataclasses.dataclass(frozen=True) +class GradAOTOutput(DifferentiableAOTOutput): + """An output representing the computed gradient for a differentiable input, in the joint graph""" + + grad_of: DifferentiableAOTInput + + def __post_init__(self) -> None: + if not isinstance(self.grad_of, DifferentiableAOTInput): + raise AssertionError( + f"expected grad_of to be DifferentiableAOTInput, got {type(self.grad_of)}" + ) + + def expr(self) -> str: + return f"__grad({self.grad_of.expr()})" + + def is_grad(self) -> bool: + return True + + +@dataclasses.dataclass(frozen=True) +class PhiloxUpdatedForwardOffsetAOTOutput(AOTOutput): + """The final offset from the functionalized RNG calls, forward only""" + + def expr(self) -> str: + return "__philox_updated_forward_offset" + + +@dataclasses.dataclass(frozen=True) +class PhiloxUpdatedBackwardOffsetAOTOutput(AOTOutput): + """The final offset from the functionalized RNG calls, backward only""" + + def expr(self) -> str: + return "__philox_updated_backward_offset" + + +@dataclasses.dataclass(frozen=True) +class ForwardTokenAOTOutput(AOTOutput): + """The world token output for side-effectful calls, returned so we cannot DCE it, forward only""" + + idx: int + + def expr(self) -> str: + return f"__forward_token{self.idx}" + + +@dataclasses.dataclass(frozen=True) +class BackwardTokenAOTOutput(AOTOutput): + """The world token output for side-effectful calls, returned so we cannot DCE it, backward only""" + + idx: int + + def expr(self) -> str: + return f"__backward_token{self.idx}" + + +# These are seemingly symmetric with their AOTInput counterparts. The way to +# think about it is that a subclass could be an input or an output, and they +# get exploded into plain tensors on the way in and out. So we need +# descriptors for both. +@dataclasses.dataclass(frozen=True) +class SubclassGetAttrAOTOutput(AOTOutput): + """This output will be bundled into a subclass at this location""" + + base: AOTOutput + attr: str + + def expr(self) -> str: + return f"{self.base.expr()}.{self.attr}" + + def is_grad(self) -> bool: + return self.base.is_grad() + + +@dataclasses.dataclass(frozen=True) +class SubclassSizeAOTOutput(AOTOutput): + """This output size will be bundled into a subclass at this location""" + + base: AOTOutput + idx: int + + def expr(self) -> str: + return f"{self.base.expr()}.size({self.idx})" + + +@dataclasses.dataclass(frozen=True) +class SubclassStrideAOTOutput(AOTOutput): + """This output stride will be bundled into a subclass at this location""" + + base: AOTOutput + idx: int + + def expr(self) -> str: + return f"{self.base.expr()}.stride({self.idx})" + + +@dataclasses.dataclass(frozen=True) +class DummyAOTOutput(AOTOutput): + """For cases when you don't actually care about descriptor propagation, do not use under normal + circumstances.""" + + idx: int + + def expr(self) -> str: + return f"__dummy{self.idx}" + + +@dataclasses.dataclass(frozen=True) +class SavedForBackwardsAOTOutput(AOTOutput): + idx: int + + def expr(self) -> str: + return f"__saved_for_backwards_{self.idx}" + + +# Note [Activations with no version counter checks in eager] +# In eager, when a tensor is saved for backward, the autograd engine +# generally performs version counter checks when grabbing the activation +# at backward time. +# One exception is in a custom autograd.Function: if the user stashes a tensor +# for use in the backward using `ctx.foo = foo`, rather than +# `ctx.save_for_backward(foo)`, then the autograd engine will not know +# to perform any checks. +# In torch.compile, we handle autograd by tracing through it ahead of time, +# and wrapping the fw + bw graphs into a custom autograd.Function. +# In order to provide parity with eager around VC checks, though, we need +# to know which activations had gotten this "no VC checks" treatment in eager, +# so we can plumb this info into the AOTAutograd runtime, +# and avoid calling ctx.save_for_backward on these tensors. +# +# This dataclass tells us that a given AOTOutput corresponds to: +# - An activation output from the forward graph +# - that should *not* have its version counter checked at runtime in the backward. +# This is done by stashing this activation on the ctx object directly +@dataclasses.dataclass(frozen=True) +class SavedForBackwardsNoVcCheckAOTOutput(AOTOutput): + idx: int + + def expr(self) -> str: + return f"__saved_for_backwards_no_vc_check_{self.idx}" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/frontend_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/frontend_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..5691a2641a3017d6a22a6c1677da01cc25b0f30e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/frontend_utils.py @@ -0,0 +1,421 @@ +from __future__ import annotations + +import warnings +from contextlib import contextmanager +from typing import Any, cast, TYPE_CHECKING + +import torch +import torch.utils._pytree as pytree +from torch._guards import detect_fake_mode +from torch._library.opaque_object import is_opaque_type +from torch._opaque_base import OpaqueBase +from torch._subclasses import FakeTensor, FakeTensorMode +from torch.fx.experimental.proxy_tensor import _pytree_subclasses_that_lose_info +from torch.fx.experimental.symbolic_shapes import ShapeEnv +from torch.utils._python_dispatch import is_traceable_wrapper_subclass + +from .. import config +from .descriptors import BufferAOTInput, DifferentiableAOTInput, ParamAOTInput +from .schemas import AOTConfig, FakifiedFlatArgs + + +if TYPE_CHECKING: + from collections.abc import Generator, KeysView + + +static_inputs_log = torch._logging.getArtifactLogger( + __name__, "cudagraph_static_inputs" +) + + +def process_inputs( + flat_args: list[Any], + aot_config: AOTConfig, + fake_mode: FakeTensorMode, + shape_env: ShapeEnv | None, + ignore_shape_env: bool = False, +) -> tuple[FakifiedFlatArgs, list[int]]: + """Convert real tensor inputs into fake tensors for AOT autograd tracing. + + Called at compile time (not runtime) to produce the fake inputs that AOT + autograd traces through. Each real tensor is converted to a FakeTensor + via ``fake_mode.from_tensor``, preserving shape, dtype, device, and + symbolic shape information from the ShapeEnv. Non-tensor inputs (ints, + SymInts, ScriptObjects) are converted or passed through as appropriate. + + Tensor subclass inputs (DTensor, etc.) are fakified recursively by + walking their ``__tensor_flatten__`` attrs. AsyncCollectiveTensors are + resolved via ``trigger_wait()`` before fakification so they don't appear + in the traced metadata (see below). + + Called from ``aot_function``, ``aot_module_simplified``, and + ``aot_export_module`` — anywhere AOT autograd needs fake inputs before + graph capture. + + Returns: + A tuple of (fakified_args, act_input_indices) where act_input_indices + records which positions held AsyncCollectiveTensors. These indices are + stored on ViewAndMutationMeta so that the runtime wrapper can emit + direct trigger_wait() calls on those positions. + """ + # Resolve AsyncCollectiveTensors before tracing. ACTs are transient + # eager-mode wrappers for async collective overlap; if they leak into the + # traced graph as input types, AOT autograd records them in + # SubclassCreationMeta for output tangent metadata. At runtime, autograd + # produces plain tensor tangents, causing a type mismatch. Unwrapping + # here prevents ACT from appearing in the traced metadata. + try: + from torch.distributed._functional_collectives import AsyncCollectiveTensor + except ImportError: + AsyncCollectiveTensor = None + + act_input_indices: list[int] = [] + if AsyncCollectiveTensor is not None: + for i, a in enumerate(flat_args): + if isinstance(a, AsyncCollectiveTensor): + act_input_indices.append(i) + flat_args[i] = a.trigger_wait() + + with fake_mode: + + def convert(idx: int, x: Any) -> Any: + nonlocal ignore_shape_env + if shape_env is not None and not ignore_shape_env: + from torch._dynamo.source import ConstantSource + + if isinstance(x, int): + # We always specialize on scalar values in export. + if aot_config.is_export: + return x + source = ConstantSource(f"sym_{idx}") + return shape_env.create_symintnode( + shape_env.create_symbol(x, source, positive=x >= 0), + hint=x, + source=source, + ) + if isinstance(x, torch.ScriptObject) or is_opaque_type(type(x)): + return torch._library.fake_class_registry.maybe_to_fake_obj( + fake_mode, x + ) + if not isinstance(x, torch.Tensor): + return x + if isinstance(x, FakeTensor): + # In the case of cross compilation we will have example inputs + # with a different fake mode than our tracing fake mode. + # In these cases we want to clone the fake tensor into our + # inner fake mode. + if x.fake_mode is not fake_mode: + return fake_mode.from_tensor(x) + return x + if is_traceable_wrapper_subclass(x): + attrs, _ = x.__tensor_flatten__() + # See if all inner tensors are FakeTensors from this mode + all_this_fake = True + for a in attrs: + match getattr(x, a): + case FakeTensor() as v: + if v.fake_mode is not fake_mode: + # FakeTensor subclass from a different mode. + # Fall through to refakify. + all_this_fake = False + break + case torch.Tensor(): + all_this_fake = False + break + case OpaqueBase(): + pass + case unexpected: + raise AssertionError( + f"expected Tensor or OpaqueBase, got {type(unexpected)}" + ) + + if all_this_fake: + return x + + # see note [Tensor Fakification and Symbol Caching] + symbolic_context = None + source = None + trace = True + if tracing_context := torch._guards.TracingContext.try_get(): + if x in tracing_context.tensor_to_context: + symbolic_context = tracing_context.tensor_to_context[x] + source = symbolic_context.tensor_source + # We already fakeified this tensor in Dynamo, don't + # dump the trace for it again + trace = False + if ( + idx < aot_config.num_params_buffers + and config.static_weight_shapes + and not symbolic_context + ): + # TODO: Ensure that this codepath is never exercised from + # Dynamo + return fake_mode.from_tensor(x, static_shapes=True) + + result = fake_mode.from_tensor( + x, + static_shapes=ignore_shape_env, + symbolic_context=symbolic_context, + source=source, + trace=trace, + ) + return result + + return FakifiedFlatArgs( + [convert(idx, x) for idx, x in enumerate(flat_args)] + ), act_input_indices + + +def construct_fake_mode( + flat_args: list[Any], aot_config: AOTConfig +) -> tuple[FakeTensorMode, ShapeEnv | None]: + fake_mode = detect_fake_mode(flat_args) + if fake_mode is None: + shape_env = ShapeEnv() if aot_config.dynamic_shapes else None + fake_mode = FakeTensorMode(shape_env=shape_env) + else: + shape_env = fake_mode.shape_env + return (fake_mode, shape_env) + + +def _try_get_metadata_from_dynamo( + mod: torch.nn.Module, + param_keys: KeysView[str], + full_args_num: int, + full_args_descs: list[DifferentiableAOTInput], +) -> tuple[list[torch._guards.Source | None] | None, list[int]]: + """ + Metadata is forwarded from Dynamo to AOTDispatch via special fields on GraphModule. + We first verify that `mod` does come from Dynamo, then we handle cases where + metadata might be missing. + + Returns: + aot_autograd_arg_pos_to_source: used to dedup params and their guards + static_input_indices: used to identify static inputs for cudagraphs + """ + # Note [Assumption on Dynamo Metadata] + # This function assumes a graph module from dynamo provides `dynamo_compiled_id`, + # _param_name_to_source, and every placeholder node has `_dynamo_source` attributes. + # When gm is modified (e.g., DDPOptimizer via split_module), metadata needs to + # be propagated in order to be recognized as a dynamo graph + + if not (isinstance(mod, torch.fx.GraphModule) and "dynamo_compile_id" in mod.meta): + # graph was not captured by dynamo + return None, [] + + if not hasattr(mod, "_param_name_to_source"): + # is from export + static_input_indices = [ + i + for i, node in enumerate(full_args_descs) + if isinstance(node, (ParamAOTInput, BufferAOTInput)) + ] + return None, static_input_indices + + # We now know this came from dynamo, and (1) we care about guards, + # so setting up aot_autograd_arg_pos_to_source for downstream dedup guards + # can now be done safely. (2) Dynamo logic protects the 1:1 sizing below. + # Additionally, we mark static indices for cudagraphs. + param_name_to_source = cast( + dict[str, torch._guards.Source], mod._param_name_to_source + ) + seen_sources = set() + + aot_autograd_arg_pos_to_source: list[torch._guards.Source | None] = [] + static_input_indices = [] + # Collect the new inputs lifted by aotdispatch + for i, name in enumerate(param_keys): + if name not in param_name_to_source: + raise AssertionError(f"{name} not found in param_name_to_source") + source = param_name_to_source[name] + if source in seen_sources: + raise AssertionError(f"source {source} already in seen_sources") + if source is None: + raise AssertionError(f"source must not be None for {name}") + seen_sources.add(source) + aot_autograd_arg_pos_to_source.append(source) + + static_input_indices.append(i) + + # Collect the dynamo graph inputs + # TODO(mlazos): Revisit if this is still needed. With Dynamo install ID + # matched tensors back into the Fx graph, this might not be necessary. + for pos, node in enumerate(mod.graph.find_nodes(op="placeholder")): + if not hasattr(node, "_dynamo_source"): + raise AssertionError(f"node {node} must have _dynamo_source attribute") + source = node._dynamo_source + # `source`` specifies the source from user code. ddp optimizer may have + # intermediate values becoming submodule placeholders which does not + # have a source + if source is not None and source in seen_sources: + raise AssertionError(f"source {source} already in seen_sources") + seen_sources.add(source) + aot_autograd_arg_pos_to_source.append(source) + source_name = source.name if source else str(source) + + # input[i] in dynamo is now: + # input[i + len(extra_params)] in AOT, + # where extra_params are the params/buffers that dynamo baked into the + # OutputGraph + actual_pos = pos + len(param_keys) + + if "tensor_dict" in node.meta and node.meta["tensor_dict"].get( + "_dynamo_static_input_type", None + ): + static_inputs_log.debug( + "Adding static input pos %s for source %s", actual_pos, source_name + ) + static_input_indices.append(actual_pos) + else: + static_inputs_log.debug( + "Non-static input pos %s for source %s", actual_pos, source_name + ) + + if full_args_num != len(aot_autograd_arg_pos_to_source): + raise AssertionError( + f"full_args_num={full_args_num} != len(aot_autograd_arg_pos_to_source)={len(aot_autograd_arg_pos_to_source)}" + ) + return aot_autograd_arg_pos_to_source, static_input_indices + + +@contextmanager +def _detect_attribute_assignment(mod: torch.nn.Module) -> Generator[None, None, None]: + # Do not allow assignment of tensor attributes during export unless + # the attribute is registered as a buffer. + + NN_MODULE_STD_ATTRS = [ + "_backward_hooks", + "_backward_pre_hooks", + "_buffers", + "_forward_hooks", + "_forward_hooks_always_called", + "_forward_hooks_with_kwargs", + "_forward_pre_hooks", + "_forward_pre_hooks_with_kwargs", + "_is_full_backward_hook", + "_load_state_dict_post_hooks", + "_load_state_dict_pre_hooks", + "_modules", + "_non_persistent_buffers_set", + "_parameters", + "_state_dict_hooks", + "_state_dict_pre_hooks", + "training", + ] + NN_MODULE_LAZY_STD_ATTRS = [ + "_initialize_hook", + "_load_hook", + ] + STD_ATTRS = { + *NN_MODULE_STD_ATTRS, + *NN_MODULE_LAZY_STD_ATTRS, + } + + def _get_attributes(mod: torch.nn.Module) -> dict[str, Any]: + # return any attributes of a module that are not standard attributes + return {k: v for k, v in mod.__dict__.items() if k not in STD_ATTRS} + + def _get_all_module_attributes(mod: torch.nn.Module) -> dict[str, dict[str, Any]]: + # return attributes from all modules and submodules + result = {} + for name, submodule in mod.named_modules(): + result[name] = _get_attributes(submodule) + return result + + def _restore_all_module_attributes( + mod: torch.nn.Module, snapshot: dict[str, dict[str, Any]] + ) -> None: + # restore attributes to all modules and submodules + for name, submodule in mod.named_modules(): + if name in snapshot: + submodule.__dict__.update(snapshot[name]) + + # save state of attributes before enter + snapshot = pytree.tree_map( + lambda x: x, + _get_all_module_attributes(mod), + is_leaf=lambda x: type(x) in _pytree_subclasses_that_lose_info, + ) + try: + yield + finally: + # after exit, compare state of attributes with snapshot + # to detect which tensor attributes were assigned + + def _collect_assigned_tensor_attributes( + snapshot: dict[str, dict[str, Any]], new_attrs: dict[str, dict[str, Any]] + ) -> list[str]: + assigned_tensor_attributes = [] + + def _compare_values(path: str, old_val: Any, new_val: Any) -> None: + """Recursively compare values, handling containers.""" + # Same object, no change + if old_val is new_val: + return + + if old_val is None or new_val is None: + if isinstance(new_val, torch.Tensor): + assigned_tensor_attributes.append(path) + return + + # Check if it's a tensor that was reassigned + if isinstance(new_val, torch.Tensor): + assigned_tensor_attributes.append(path) + return + + # Handle dict containers + if isinstance(old_val, dict) and isinstance(new_val, dict): + all_keys = set(old_val.keys()) | set(new_val.keys()) + for key in all_keys: + old_item = old_val.get(key) + new_item = new_val.get(key) + _compare_values(f"{path}[{key!r}]", old_item, new_item) + return + + # Handle list/tuple containers + if isinstance(old_val, (list, tuple)) and isinstance( + new_val, (list, tuple) + ): + # Different lengths = mutation happened + max_len = max(len(old_val), len(new_val)) + for i in range(max_len): + old_item = old_val[i] if i < len(old_val) else None + new_item = new_val[i] if i < len(new_val) else None + _compare_values(f"{path}[{i}]", old_item, new_item) + return + + # For other types, just check if they're different objects + # (we don't care about non-tensor mutations) + + for module_name in snapshot.keys() | new_attrs.keys(): + old_module_attrs = snapshot.get(module_name, {}) + new_module_attrs = new_attrs.get(module_name, {}) + + for attr_name in old_module_attrs.keys() | new_module_attrs.keys(): + module_prefix = f"self.{module_name}." if module_name else "self." + full_path = f"{module_prefix}{attr_name}" + + old_val = old_module_attrs.get(attr_name) + new_val = new_module_attrs.get(attr_name) + _compare_values(full_path, old_val, new_val) + + return assigned_tensor_attributes + + new_attrs = _get_all_module_attributes(mod) + assigned_tensor_attributes = _collect_assigned_tensor_attributes( + snapshot, new_attrs + ) + # restore state of all attributes (including, e.g., of primitive types) + _restore_all_module_attributes(mod, snapshot) + + if assigned_tensor_attributes: + if len(assigned_tensor_attributes) > 1: + noun, verb = "attributes", "were" + else: + noun, verb = "attribute", "was" + warnings.warn( + f"The tensor {noun} {', '.join(assigned_tensor_attributes)} {verb} assigned during export. " + "Such attributes must be registered as buffers using the `register_buffer` API " + "(https://pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module.register_buffer).", + stacklevel=2, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/functional_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/functional_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..9770fc39566869ccff25b74cae9562cc30c82d4a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/functional_utils.py @@ -0,0 +1,658 @@ +""" +This file contains utilities related to functionalization in AOTAutograd: +1. converting to/from functional tensors +2. detecting Tensor mutations - both metadata and Tensor value +3. regenerating/replaying views from their base +4. checking if a graph is functional i.e. whether it contains any mutation ops +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, TypeGuard + +import torch +from torch import Tensor +from torch._C import _functionalization +from torch._logging import getArtifactLogger +from torch._opaque_base import OpaqueBase +from torch._subclasses.fake_tensor import FakeTensor +from torch._subclasses.functional_tensor import FunctionalTensor +from torch._subclasses.meta_utils import is_sparse_any +from torch.fx.experimental.symbolic_shapes import guard_or_false, sym_eq, SymIntEqByExpr +from torch.multiprocessing.reductions import StorageWeakRef +from torch.utils._python_dispatch import ( + is_traceable_wrapper_subclass, + transform_subclass, +) + + +aot_joint_log = getArtifactLogger(__name__, "aot_joint_graph") + + +def to_fun(t: object) -> Any: + if isinstance(t, Tensor): + if is_traceable_wrapper_subclass(t): + # See Note [Functionalization always runs last] + # This means that if we want to "functionalize" a subclass, we need to ensure that the functional wrapper + # goes at the bottom. + # recurse here, so we can support nested wrapper subclasses + out = transform_subclass(t, lambda _, inner_t: to_fun(inner_t)) + torch._mirror_autograd_meta_to(t, out) # type: ignore[attr-defined] + return out + else: + return FunctionalTensor.to_functional(t) + else: + return t + + +def sync_functional_tensor(t: torch.Tensor) -> None: + if is_traceable_wrapper_subclass(t): + attrs, _ctx = t.__tensor_flatten__() # type: ignore[attr-defined] + for attr in attrs: + match getattr(t, attr): + case Tensor() as inner: + sync_functional_tensor(inner) + case OpaqueBase(): + pass + case unexpected: + raise AssertionError( + f"expected Tensor or OpaqueBase, got {type(unexpected)}" + ) + else: + torch._sync(t) + + +# When subclasses are involved, t here will usually look something like: +# SubclassA(SubclassB(FunctionalTensor(_to_fun_tensor(FakeTensor)))) +def from_fun(t: object) -> object: + if isinstance(t, Tensor) and is_traceable_wrapper_subclass(t): + # See Note [Functionalization always runs last] + # This means that if we want to "functionalize" a subclass, we need to ensure that the functional wrapper + # goes at the bottom. + # recurse here, so we can support nested wrapper subclasses + out = transform_subclass(t, lambda _, inner_t: from_fun(inner_t)) + torch._mirror_autograd_meta_to(t, out) # type: ignore[attr-defined] + return out + + if not isinstance(t, FunctionalTensor): + # quick sanity assert + if isinstance(t, torch.Tensor): + if torch._is_functional_tensor(t): # type: ignore[attr-defined] + raise AssertionError("expected non-functional tensor") + return t + sync_functional_tensor(t) + return torch._from_functional_tensor(t.elem) + + +def is_fun(t: object) -> TypeGuard[FunctionalTensor | Tensor]: + if isinstance(t, Tensor) and is_traceable_wrapper_subclass(t): + # See Note [Functionalization always runs last] + # This means that if we want to "functionalize" a subclass, we need to ensure that the functional wrapper + # goes at the bottom. + # recurse here, so we can support nested wrapper subclasses + t_attrs, _ = t.__tensor_flatten__() # type: ignore[attr-defined] + got_fun: bool | None = None + for attr in t_attrs: + match getattr(t, attr): + case Tensor() as v: + fun = is_fun(v) + if got_fun is None: + got_fun = fun + elif got_fun != fun: + raise AssertionError( + "mixed functional/non-functional inner tensors" + ) + case OpaqueBase(): + pass + case unexpected: + raise AssertionError( + f"expected Tensor or OpaqueBase, got {type(unexpected)}" + ) + return got_fun or False + + return isinstance(t, FunctionalTensor) + + +# t here is either +# (1) A FunctionalTensor(_to_functional_tensor(FakeTensor)) +# (2) A traceable tensor subclass that holds a FunctionalTensor +# (3) Not a tensor +def has_data_mutation(t: object) -> bool: + if is_traceable_wrapper_subclass(t): + attrs, _ = t.__tensor_flatten__() + # A tensor subclass was updated if any of its inner elements were updated + for attr in attrs: + match getattr(t, attr): + case Tensor() as v: + if has_data_mutation(v): + return True + case OpaqueBase(): + pass + case unexpected: + raise AssertionError( + f"expected Tensor or OpaqueBase, got {type(unexpected)}" + ) + return False + else: + if isinstance(t, torch.Tensor): + if not isinstance(t, FunctionalTensor): + raise AssertionError(f"expected FunctionalTensor, got {type(t)}") + return torch._functionalize_has_data_mutation(t.elem) # type: ignore[attr-defined] + return False + + +def are_all_mutations_hidden_from_autograd(t: object) -> bool: + if is_traceable_wrapper_subclass(t): + attrs, _ = t.__tensor_flatten__() + # If all inner elements are mutations hidden from autograd, then it is a mutation hidden from autograd. + for attr in attrs: + match getattr(t, attr): + case Tensor() as v: + if not are_all_mutations_hidden_from_autograd(v): + return False + case OpaqueBase(): + pass + case unexpected: + raise AssertionError( + f"expected Tensor or OpaqueBase, got {type(unexpected)}" + ) + return True + elif isinstance(t, torch.Tensor): + if not isinstance(t, FunctionalTensor): + raise AssertionError(f"expected FunctionalTensor, got {type(t)}") + return torch._functionalize_are_all_mutations_hidden_from_autograd(t.elem) + else: + return False + + +def are_all_mutations_under_no_grad_or_inference_mode(t: torch.Tensor) -> bool: + if is_traceable_wrapper_subclass(t): + attrs, _ = t.__tensor_flatten__() + for attr in attrs: + match getattr(t, attr): + case Tensor() as v: + if not are_all_mutations_under_no_grad_or_inference_mode(v): + return False + case OpaqueBase(): + pass + case unexpected: + raise AssertionError( + f"expected Tensor or OpaqueBase, got {type(unexpected)}" + ) + return True + else: + if not isinstance(t, FunctionalTensor): + raise AssertionError(f"expected FunctionalTensor, got {type(t)}") + return torch._functionalize_are_all_mutations_under_no_grad_or_inference_mode( + t.elem + ) + + +def was_inductor_storage_resized(t: object) -> bool: + if is_traceable_wrapper_subclass(t): + attrs, _ = t.__tensor_flatten__() + for attr in attrs: + match getattr(t, attr): + case Tensor() as v: + if was_inductor_storage_resized(v): + raise RuntimeError( + f"storage resizing is not supported on tensor subclass: {type(t)}" + ) + case OpaqueBase(): + pass + case unexpected: + raise AssertionError( + f"expected Tensor or OpaqueBase, got {type(unexpected)}" + ) + return False + elif not isinstance(t, torch.Tensor): + return False + else: + if not isinstance(t, FunctionalTensor): + raise AssertionError(f"expected FunctionalTensor, got {type(t)}") + return torch._functionalize_was_inductor_storage_resized(t.elem) + + +# f_arg here is either +# (1) A FunctionalTensor(_to_functional_tensor(FakeTensor)) +# (2) A traceable tensor subclass that holds a FunctionalTensor +# (3) Not a tensor +# Assumption: arg promises to be the "original" tensor wrapped by f_arg +# Note: "storage mutations" coming from set_() are a type of metadata mutation. So: +# - check_only_storage_mutation=True: only return true if there was a storage mutation +# - check_only_storage_mutation=Flse: return true if there was any metadata mutation (including a storage mutation) +def has_metadata_mutation( + f_arg: object, arg: object, *, check_only_storage_mutation: bool +) -> bool: + if is_traceable_wrapper_subclass(f_arg): + attrs, _ = f_arg.__tensor_flatten__() + # A tensor subclass was updated if any of its inner elements were updated + for attr in attrs: + match getattr(f_arg, attr): + case Tensor(): + f_inner_t = getattr(f_arg, attr) + inner_t = getattr(arg, attr) + if has_metadata_mutation( + f_inner_t, + inner_t, + check_only_storage_mutation=check_only_storage_mutation, + ): + return True + case OpaqueBase(): + pass + case unexpected: + raise AssertionError( + f"expected Tensor or OpaqueBase, got {type(unexpected)}" + ) + return False + else: + if not isinstance(f_arg, torch.Tensor): + if isinstance(arg, torch.Tensor): + raise AssertionError( + f"f_arg is not a Tensor but arg is: {type(f_arg)} vs {type(arg)}" + ) + return False + if not isinstance(f_arg, FunctionalTensor): + raise AssertionError( + f"expected FunctionalTensor for f_arg, got {type(f_arg)}" + ) + if not isinstance(arg, FakeTensor): + raise AssertionError(f"expected FakeTensor for arg, got {type(arg)}") + + arg_after = torch._from_functional_tensor(f_arg.elem) + # This is true if the current tensor experienced at least one set_() call + maybe_storage_changed = torch._functionalize_was_storage_changed(f_arg.elem) # type: ignore[attr-defined] + # However, multiple set_() calls can cancel out. So we also check whether the + # storage of the tensor has changed. + # Note: if an input experienced two set_() calls that cancel out, **and** + # it experiences an data mutation, we pessimistically think that the set_() + # call is necessary here. We could in theory fix this, but this will + # hopefully never happen in user code, and is not needed for fsdp. + if is_sparse_any(arg): + # TODO:add sparse tensors support to functionalization + same_storages = False + else: + same_storages = StorageWeakRef(arg.untyped_storage()) == StorageWeakRef( + arg_after.untyped_storage() + ) + has_storage_metadata_mutation = maybe_storage_changed and not same_storages + if check_only_storage_mutation: + return has_storage_metadata_mutation + + # storage metadata mutation is a type of metadata mutation, so return true if we saw one + if has_storage_metadata_mutation: + return True + + maybe_metadata_mutated = torch._functionalize_has_metadata_mutation(f_arg.elem) # type: ignore[attr-defined] + # This is true if the current tensor experienced at least one metadata mutation. + # So if false, we know there was no metadata mutation + if not maybe_metadata_mutated: + return False + + # However, multi metadata mutations can cancel out. + # So we also check if the concrete sizes/strides on the tensor have changed. + same_sizes = arg.shape == arg_after.shape + same_strides = arg.stride() == arg_after.stride() + same_offsets = arg.storage_offset() == arg_after.storage_offset() + has_metadata_mutation_ = maybe_metadata_mutated and not ( + same_sizes and same_strides and same_offsets + ) + # We consider a tensor to have been metadata mutated if its storage was mutated through a set_() call. + return has_metadata_mutation_ + + +def gen_alias_from_base( + aliased_base_tensor: Tensor, + target_meta_tensor: Tensor, + target_requires_grad: bool, + target_view_meta_sequence: ViewMetaSequence | None = None, + *, + replay_views: bool, +) -> Tensor: + # Patch the correct requires_grad field of the output tensor, depending on whether: + # (i) the reconstructed output (out) was came from a tensor that requires grad or not; + # and (ii) the concrete returned output does require grad or not. + def patch_requires_grad(out: Tensor) -> Tensor: + if aliased_base_tensor.requires_grad and not target_requires_grad: + out = out.detach() + elif not aliased_base_tensor.requires_grad and target_requires_grad: + out.requires_grad_(True) + return out + + # If provided, use the target functional tensor for replaying the views. + # + # In summary, we use the fact that FunctionalTensorWrapper saves the view + # functions applied to itself (collected during functionalization) so as + # to replay them (view functions) on the aliased_base_tensor. + if ( + replay_views + and target_view_meta_sequence is not None + and not any(vm.has_symbolic_inputs for vm in target_view_meta_sequence.sequence) + ): + out = _functionalization.apply_view_meta_sequence( + aliased_base_tensor, target_view_meta_sequence.sequence + ) + # If re-applying the ViewMeta sequence succeeded, there should be no more + # problems going forward. We just check we got to the target shape and + # patch requires_grad flag. + if out.shape != target_meta_tensor.shape: + raise AssertionError( + "incorrect out shape after application of ViewMeta sequence: " + f"{tuple(out.shape)} (actual) vs {tuple(target_meta_tensor.shape)} (expected)" + ) + return patch_requires_grad(out) + + # Try to do view-replay if possible. + # fall back to .as_strided() if we can't. + if target_meta_tensor._base is not None: + # The base that we want to replay our view off of might have a different shape than the view's original base. + b = target_meta_tensor._base + abt = aliased_base_tensor + # Don't unnecessarily call as_strided if nothing changed; as_strided's + # backward is poorly implemented and slow + if abt is not b and ( + abt.size() != b.size() + or abt.stride() != b.stride() + or abt.storage_offset() != b.storage_offset() + ): + reshaped_base_tensor = aliased_base_tensor.as_strided( + b.size(), b.stride(), b.storage_offset() + ) + else: + reshaped_base_tensor = aliased_base_tensor + out = target_meta_tensor._view_func(reshaped_base_tensor) # type: ignore[attr-defined] + # This shape mismatch can happen due to a bug in inplace/view handling in autograd. + # Try putting a breakpoint here and running + # `test/functorch/test_aotdispatch TestAOTAutograd.test_output_all_alias_types` + # Also, https://github.com/pytorch/pytorch/issues/49825 + # + # As a stopgap, we'll fall back to as_strided. + if out is not None and out.shape == target_meta_tensor.shape: + return patch_requires_grad(out) + + size = target_meta_tensor.size() + stride = target_meta_tensor.stride() + storage_offset = target_meta_tensor.storage_offset() + if aliased_base_tensor.is_complex() and not target_meta_tensor.is_complex(): + aliased_out = torch.view_as_real(aliased_base_tensor).as_strided( + size, stride, storage_offset + ) + elif not aliased_base_tensor.is_complex() and target_meta_tensor.is_complex(): + aliased_out = torch.view_as_complex(aliased_base_tensor).as_strided( + size, stride, storage_offset + ) + else: + aliased_out = aliased_base_tensor.as_strided(size, stride, storage_offset) + # For outputs aliasing inputs, we need to check if the requires-gradness has changed. + aliased_out = patch_requires_grad(aliased_out) + # For outputs aliasing inputs, we need to check if the dtype has changed. + # as_strided() is the "most generic" view, but it does not cover cross-dtype views + if aliased_out.dtype != target_meta_tensor.dtype: + aliased_out = aliased_out.view(target_meta_tensor.dtype) + return aliased_out + + +def has_same_metadata(t1: Tensor, t2: Tensor) -> bool: + return ( + guard_or_false(sym_eq(t1.size(), t2.size())) + and guard_or_false(t1.layout == t2.layout) + and ( + is_sparse_any(t1) + or ( + guard_or_false(sym_eq(t1.stride(), t2.stride())) + and guard_or_false(t1.storage_offset() == t2.storage_offset()) + ) + ) + and t1.is_conj() == t2.is_conj() + and t1.is_neg() == t2.is_neg() + ) + + +@dataclass(frozen=True) +class MetadataKey: + """ + This should be equal whenever has_same_metadata would return True + """ + + size: tuple[SymIntEqByExpr, ...] + layout: torch.layout + is_sparse: bool + # these are empty when is_sparse + stride: tuple[SymIntEqByExpr, ...] | None + storage_offset: SymIntEqByExpr | None + is_conj: bool + is_neg: bool + + @staticmethod + def make(t: Tensor) -> MetadataKey: + is_sparse = is_sparse_any(t) + return MetadataKey( + size=tuple(SymIntEqByExpr(s) for s in t.size()), + layout=t.layout, + is_sparse=is_sparse, + stride=None if is_sparse else tuple(SymIntEqByExpr(s) for s in t.stride()), + storage_offset=None if is_sparse else SymIntEqByExpr(t.storage_offset()), + is_conj=t.is_conj(), + is_neg=t.is_neg(), + ) + + +# ViewMeta sequence wrapper for equality comparisons. +# +# Even though we can compare each ViewMeta instance, we compare the resulting +# tensor metadata, instead. That's because the creation of synthetic bases + the +# re-generation of input views might end-up creating a different sequence of +# ViewMeta that is semantically equivalent. i.e. gets to a tensor with the same +# metadata. +# +# Therefore, we store what the end result should look like as serializable +# metadata. +# +# When logging, this class should look like: +# +# ViewMetaSequence(view, select_int, slice_Tensor) +# +# i.e. a parenthesized list of view operations within that ViewMeta sequence. +class ViewMetaSequence: + def __init__(self, tensor: FunctionalTensor) -> None: + if not torch._is_functional_tensor(tensor.elem): + raise AssertionError("expected tensor.elem to be a functional tensor") + self.sequence = _functionalization.get_view_meta_sequence(tensor.elem) + self.metadata = MetadataKey.make(tensor) + + def __repr__(self) -> str: + suffix = len("_ViewMeta") + types = ", ".join(type(vm).__name__[:-suffix] for vm in self.sequence) + return f"ViewMetaSequence({types})" + + def __eq__(self, other: object) -> bool: + # If other is None, then it probably means that we weren't able to recreate + # the ViewMeta sequence. One example is when we update the view metadata by + # calling: create_synthetic_base_metadata. + if other is None: + return True + + # Comparison against any other type is not implemented. + if not isinstance(other, ViewMetaSequence): + return NotImplemented + + return self.metadata == other.metadata + + +# new_arg and arg here are either: +# (1) both a FakeTensor +# (2) both a traceable tensor subclass that holds a FakeTensor +# Pre-condition: the two args are the "old" and "new" inputs from running functionalization. +# When we run functionalization and wrap our inputs into FunctionalTensors, +# we can detect whether or not an input was mutated by checking to see if the inner tensor has changed +# +# Normally it would be enough just to check if arg is new_arg, which is normally enough for functionalization +# to confirm that inputs were not mutated when running the user's model with functionalization on. +# But when we have subclass inputs, we can't rely on that: +# `from_fun(to_fun(x)) is x` will return False, because the call to `from_fun` constructs +# a brand new subclass instance: we are calling __tensor_unflatten__, and going +# from Subclass(FakeTensor) to Subclass(FunctionalTensor(FakeTensor)) +def was_tensor_updated(arg: torch.Tensor, new_arg: torch.Tensor) -> bool: + if is_traceable_wrapper_subclass(arg): + if not is_traceable_wrapper_subclass(new_arg): + raise AssertionError( + f"expected new_arg to be traceable wrapper subclass, got {type(new_arg)}" + ) + attrs, _ = arg.__tensor_flatten__() + new_attrs, _ = new_arg.__tensor_flatten__() + if attrs != new_attrs: + raise AssertionError(f"attrs mismatch: {attrs} != {new_attrs}") + # A tensor subclass was updated if any of its inner elements were updated + for attr in attrs: + match getattr(arg, attr): + case Tensor() as v: + if was_tensor_updated(v, getattr(new_arg, attr)): + return True + case OpaqueBase(): + pass + case unexpected: + raise AssertionError( + f"expected Tensor or OpaqueBase, got {type(unexpected)}" + ) + return False + else: + return arg is not new_arg + + +# new_arg and arg here are either: +# (1) both a FakeTensor +# (2) both a traceable tensor subclass that holds a FakeTensor +# Pre-condition: the two args are the "old" and "new" inputs from running functionalization. +# When we run functionalization and wrap our inputs into FunctionalTensors, +# we can detect whether or not an input was mutated by checking to see if the inner tensor has changed, +# but shares storage with the old input +def was_tensor_metadata_updated(arg: Any, new_arg: Any) -> bool: + if is_traceable_wrapper_subclass(arg): + if not is_traceable_wrapper_subclass(new_arg): + raise AssertionError( + f"expected new_arg to be traceable wrapper subclass, got {type(new_arg)}" + ) + attrs, _ = arg.__tensor_flatten__() + new_attrs, _ = new_arg.__tensor_flatten__() + if attrs != new_attrs: + raise AssertionError(f"attrs mismatch: {attrs} != {new_attrs}") + # A tensor subclass was updated if any of its inner elements were updated + for attr in attrs: + match getattr(arg, attr): + case Tensor() as v: + if was_tensor_metadata_updated(v, getattr(new_arg, attr)): + return True + case OpaqueBase(): + pass + case unexpected: + raise AssertionError( + f"expected Tensor or OpaqueBase, got {type(unexpected)}" + ) + return False + else: + return arg is not new_arg and StorageWeakRef( + arg.untyped_storage() + ) == StorageWeakRef(new_arg.untyped_storage()) + + +# Returns the number of detected copy_ +def _is_functional_graph(fx_g: torch.fx.Graph) -> tuple[str | None, int]: + allowed_mutation_ops = [ + torch.ops.aten.copy_.default, + torch.ops.aten.set_.source_Tensor, + ] + if hasattr(torch.ops.fsdp, "copy_"): + allowed_mutation_ops.append(torch.ops.fsdp.copy_.default) + + placeholders = set() + mutation_count = 0 + # NB: It would also be nice to verify that the mutations all happen at the + # end, but we also do some administrative views after mutations so this + # isn't actually true. (TODO: Could this cause problems for Inductor?) + error = None + for n in fx_g.nodes: + if n.op == "placeholder": + placeholders.add(n) + if isinstance(n.target, torch._ops.OpOverload): + if n.target in allowed_mutation_ops: + # Can only copy_/set_ into an input + # this is mostly a hack to avoid failing XLA tests. + # See https://github.com/pytorch/pytorch/pull/122434#issuecomment-2101012113 + if "set_buffer_donor_" not in str(n.args[0]): + if n.args[0] not in placeholders: + error = f"n={str(n)}, n.args[0]={str(n.args[0])}, placeholders={str(placeholders)}, graph={str(fx_g)}" + mutation_count += 1 + else: + if n.target._schema.is_mutable: + error = f"aot_autograd expected to have an entirely functional graph, but found {n.format_node()}" + return error, mutation_count + + +def assert_functional_graph(fx_g: torch.fx.Graph) -> int: + error, mutation_count = _is_functional_graph(fx_g) + if error is not None: + raise AssertionError(error) + return mutation_count + + +def propagate_input_mutation_stacktraces(fx_g: torch.fx.Graph) -> None: + placeholders = set() + for n in fx_g.nodes: + if n.op == "placeholder": + placeholders.add(n) + if isinstance(n.target, torch._ops.OpOverload): + if n.target is torch.ops.aten.copy_.default: + # Can only copy_ into an input, and can only do so once + if "set_buffer_donor_" not in str(n.args[0]): + if n.args[0] not in placeholders: + raise AssertionError( + f"n={str(n)}, n.args[0]={str(n.args[0])}, placeholders={str(placeholders)}, graph={str(fx_g)}" + ) + placeholders.remove(n.args[0]) + copy_from_node = n.args[1] + # Pre-condition: every node has a "stack_trace" field in its meta, + # but copy_() nodes do not (since we manually added them during functionalization). + # Instead, we manually propagate here. + if "stack_trace" in copy_from_node.meta: + n.meta["stack_trace"] = copy_from_node.meta["stack_trace"] + + +def _check_if_mutation_can_be_in_graph( + keep_input_mutations: bool, + mutates_data: bool, + mutates_metadata: bool, + mutations_hidden_from_autograd: bool, + mutations_under_no_grad_or_inference_mode: bool, + mutates_storage_metadata: bool, + mutation_inductor_storage_resize: bool, + requires_grad: bool, +) -> bool: + if keep_input_mutations: + in_graph = ( + mutates_data or mutates_storage_metadata or mutation_inductor_storage_resize + ) and ( + (not mutates_metadata and not requires_grad) + or mutations_hidden_from_autograd + or mutations_under_no_grad_or_inference_mode + ) + else: + in_graph = False + # See Note [set_() Input Mutations in AOTAutograd] + # If there was a `set_()`, we require that all mutations were under no_grad, + # so we can (safely) emit the set_() in the graph at runtime + # resize_() gets the same treatment + if mutation_inductor_storage_resize or mutates_storage_metadata: + op_name = "resize_" if mutation_inductor_storage_resize else "set_" + if not in_graph: + raise AssertionError(f"""\ +Encountered a {op_name} on a graph input, but the input has other mutations that we cannot +keep in the graph. This is not supported today. Current state: + keep_input_mutations={keep_input_mutations} + mutates_data={mutates_data} + mutates_metadata={mutates_metadata} + mutations_hidden_from_autograd={mutations_hidden_from_autograd} + mutations_under_no_grad_or_inference_mode={mutations_under_no_grad_or_inference_mode} + mutation_inductor_storage_resize={mutation_inductor_storage_resize} + requires_grad={requires_grad}""") + return in_graph diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/fx_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/fx_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..fc97619de7d3fe597d49c928131ef10e3990c3ef --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/fx_utils.py @@ -0,0 +1,324 @@ +""" +This module contains utility functions for working with joint FX graphs with descriptors +that are produced by AOTAutograd. They will NOT work on generic FX graphs. See also +:func:`torch._functorch.aot_autograd.aot_export_joint_with_descriptors`. We also +recommend reading :mod:torch._functorch._aot_autograd.descriptors`. +""" + +from typing import NoReturn + +import torch.fx as fx + +from .descriptors import ( + AOTInput, + AOTOutput, + BufferAOTInput, + DifferentiableAOTInput, + DifferentiableAOTOutput, + GradAOTOutput, + ParamAOTInput, + PlainAOTInput, + PlainAOTOutput, + SubclassGetAttrAOTInput, + SubclassGetAttrAOTOutput, + TangentAOTInput, +) + + +def _raise_autograd_subclass_not_implemented( + n: fx.Node, desc: AOTInput | AOTOutput +) -> NoReturn: + raise RuntimeError( + "Subclasses are currently not supported by this function, but a desugared subclass input " + f"was found at {n} ({desc}). The problem is " + "that there may not necessarily be a 1-1 correspondence between primals/tangents/outputs/grads " + "when subclasses are involved: for example, the primal might be a plain tensor " + "but the tangent a tensor subclass that desugared into multiple plain tensors. " + "It is not clear what exactly you would like this function to do in this case " + "(Collect all nodes for the subclass together? Match up the inner nodes if " + "subclasses match exactly?) If you have a concrete use case, please file an " + "issue so we can understand it and design an API that works for your case." + ) + + +def get_all_input_and_grad_nodes( + g: fx.Graph, +) -> dict[DifferentiableAOTInput, tuple[fx.Node, fx.Node | None]]: + """ + Given a joint graph with descriptors (meta['desc'] on placeholders and + output), returns the node for every input and its corresponding grad + output node if it exists. These tuples are in a dict that is indexed by + the AOTInput descriptor that describes the input. + + NB: *all* forward tensor inputs are returned, including non-differentiable + inputs (which simply have a None grad), so it is safe to use this function + to perform operations on all inputs. (Non-tensor inputs like symbolic + integers, tokens or RNG state are NOT traversed by this function.) + + Args: + g: The FX joint graph with descriptors + + Returns: + A dictionary mapping each DifferentiableAOTInput descriptor to a tuple + containing: + - The input node itself + - The grad (output) node if it exists, None otherwise + + Raises: + RuntimeError: If the joint graph has subclass tensor inputs/outputs; this + is not supported by API as there is not necessarily a 1-1 correspondence + between inputs and grads when subclasses are involved. + """ + input_index: dict[DifferentiableAOTInput, tuple[fx.Node, fx.Node | None]] = {} + for n in g.nodes: + if n.op == "placeholder": + desc = n.meta["desc"] + # Skip inputs that cannot possibly be differentiable + if not isinstance(desc, DifferentiableAOTInput): + continue + if isinstance(desc, SubclassGetAttrAOTInput): + _raise_autograd_subclass_not_implemented(n, desc) + + input_index[desc] = (n, None) + elif n.op == "output": + if "desc" not in n.meta: + raise AssertionError(f"'desc' not in n.meta for {n}: {n.meta}") + desc = n.meta["desc"] + for sub_n, sub_desc in zip(n.args[0], desc): + if isinstance(sub_desc, SubclassGetAttrAOTOutput): + _raise_autograd_subclass_not_implemented(sub_n, sub_desc) + if isinstance(sub_desc, GradAOTOutput): + inp, grad = input_index[sub_desc.grad_of] + if grad is not None: + raise AssertionError( + f"grad already set for {sub_n}, {sub_desc}, {input_index}" + ) + input_index[sub_desc.grad_of] = (inp, sub_n) + return input_index + + +def get_all_output_and_tangent_nodes( + g: fx.Graph, +) -> dict[DifferentiableAOTOutput, tuple[fx.Node, fx.Node | None]]: + """Get all output nodes and their corresponding tangent nodes from a joint graph. + + Similar to get_all_input_and_grad_nodes, but returns output nodes paired with + their tangent nodes (if they exist). This function traverses the graph to find + all differentiable outputs and matches them with their corresponding tangent + inputs used in forward-mode autodiff. + + NB: *all* forward tensor output sare turned, including non-differentiable outputs, + so you can use this function to perform operations on all outputs. + + Args: + g: The FX joint graph with descriptors + + Returns: + A dictionary mapping each DifferentiableAOTOutput descriptor to a tuple + containing: + - The output node itself + - The tangent (input) node if it exists, None otherwise + + Raises: + RuntimeError: If the joint graph has subclass tensor inputs/outputs; this + is not supported by API as there is not necessarily a 1-1 correspondence + between outputs and tangents when subclasses are involved. + """ + output_index: dict[DifferentiableAOTOutput, tuple[fx.Node, fx.Node | None]] = {} + for n in g.nodes: + if n.op == "output": + desc = n.meta["desc"] + for sub_n, sub_d in zip(n.args[0], desc): + # Skip outputs that cannot possibly be differentiable + if not isinstance(sub_d, DifferentiableAOTOutput): + continue + if isinstance(sub_d, SubclassGetAttrAOTOutput): + _raise_autograd_subclass_not_implemented(sub_n, sub_d) + + output_index[sub_d] = (sub_n, None) + for n in g.nodes: + if n.op == "placeholder": + desc = n.meta["desc"] + if isinstance(desc, SubclassGetAttrAOTInput): + _raise_autograd_subclass_not_implemented(n, desc) + if isinstance(desc, TangentAOTInput): + out, tangent = output_index[desc.output] + if tangent is not None: + raise AssertionError( + f"tangent already set for {n}, {desc}, {output_index}" + ) + output_index[desc.output] = (out, n) + return output_index + + +def get_param_and_grad_nodes( + graph: fx.Graph, +) -> dict[ParamAOTInput, tuple[fx.Node, fx.Node | None]]: + """Get parameter nodes and their corresponding gradient nodes from a joint graph. + + Args: + graph: The FX joint graph with descriptors + + Returns: + A dictionary mapping each ParamAOTInput descriptor to a tuple containing: + - The parameter input node + - The gradient (output) node if it exists, None otherwise + """ + return { + desc: (n, g) + for desc, (n, g) in get_all_input_and_grad_nodes(graph).items() + if isinstance(desc, ParamAOTInput) + } + + +def get_plain_input_and_grad_nodes( + graph: fx.Graph, +) -> dict[PlainAOTInput, tuple[fx.Node, fx.Node | None]]: + """Get plain input nodes and their corresponding gradient nodes from a joint graph. + + Args: + graph: The FX joint graph with descriptors + + Returns: + A dictionary mapping each PlainAOTInput descriptor to a tuple containing: + - The plain input node + - The gradient (output) node if it exists, None otherwise + """ + return { + desc: (n, g) + for desc, (n, g) in get_all_input_and_grad_nodes(graph).items() + if isinstance(desc, PlainAOTInput) + } + + +def get_plain_output_and_tangent_nodes( + graph: fx.Graph, +) -> dict[PlainAOTOutput, tuple[fx.Node, fx.Node | None]]: + """Get plain output nodes and their corresponding tangent nodes from a joint graph. + + Args: + graph: The FX joint graph with descriptors + + Returns: + A dictionary mapping each PlainAOTOutput descriptor to a tuple containing: + - The plain output node + - The tangent (input) node if it exists, None otherwise + """ + return { + desc: (n, g) + for desc, (n, g) in get_all_output_and_tangent_nodes(graph).items() + if isinstance(desc, PlainAOTOutput) + } + + +def _raise_fqn_subclass_not_implemented( + n: fx.Node, desc: AOTInput | AOTOutput +) -> NoReturn: + raise RuntimeError( + "Subclasses are currently not supported by this function, but a desugared subclass input " + f"was found at {n} ({desc}). The problem is " + "that there may not necessarily be a 1-1 correspondence between a FQN and a plain tensor " + "when subclasses are involved: for example, a parameter that is a subclass " + "would desugar into multiple plain tensors, which we can't uniquely assign the " + "FQN to. It's not clear what you want the API to do in this case: do you want to " + "instead return a struct of nodes showing how to assemble the subclass? But you " + "don't (directly) have the metadata for the subclass? If you have a concrete use " + "case, please file an issue so we can understand it and design an API that works for your case." + ) + + +def get_named_param_nodes(graph: fx.Graph) -> dict[str, fx.Node]: + """Get parameter nodes mapped by their fully qualified names. + + This function traverses the graph to find all parameter input nodes and + returns them in a dictionary where keys are the parameter names (FQNs) + and values are the corresponding FX nodes. + + Args: + graph: The FX joint graph with descriptors + + Returns: + A dictionary mapping parameter names (str) to their corresponding FX nodes. + + Raises: + RuntimeError: If subclass tensors are encountered (not yet supported), as + with subclasses a FQN does not necessarily map to a single plain tensor. + """ + r = {} + for n in graph.nodes: + if n.op == "placeholder": + desc = n.meta["desc"] + if isinstance(desc, SubclassGetAttrAOTInput): + _raise_fqn_subclass_not_implemented(n, desc) + elif isinstance(desc, ParamAOTInput): + r[desc.target] = n + return r + + +def get_named_buffer_nodes(graph: fx.Graph) -> dict[str, fx.Node]: + """Get buffer nodes mapped by their fully qualified names. + + This function traverses the graph to find all buffer input nodes and + returns them in a dictionary where keys are the buffer names (FQNs) + and values are the corresponding FX nodes. + + Args: + graph: The FX joint graph with descriptors + + Returns: + A dictionary mapping buffer names (str) to their corresponding FX nodes. + + Raises: + RuntimeError: If subclass tensors are encountered (not yet supported), as + with subclasses a FQN does not necessarily map to a single plain tensor. + """ + r = {} + for n in graph.nodes: + if n.op == "placeholder": + desc = n.meta["desc"] + if isinstance(desc, SubclassGetAttrAOTInput): + _raise_fqn_subclass_not_implemented(n, desc) + elif isinstance(desc, BufferAOTInput): + r[desc.target] = n + return r + + +def get_param_nodes(graph: fx.Graph) -> list[fx.Node]: + """Get all parameter nodes from a graph as a list. + + You can rely on this providing the correct order of parameters you need + to feed into the joint graph (at the very beginning of the argument list, + before buffers). + + Args: + graph: The FX joint graph with descriptors + + Returns: + A list of FX nodes representing all parameters in the graph. + + Raises: + RuntimeError: If subclass tensors are encountered (not yet supported), as + it is not clear if you wanted each individual constituent piece of the + subclasses, or have them grouped up in some way. + """ + return list(get_named_param_nodes(graph).values()) + + +def get_buffer_nodes(graph: fx.Graph) -> list[fx.Node]: + """Get all buffer nodes from a graph as a list. + + You can rely on this providing the correct order of buffers you need + to feed into the joint graph (after parameters). + + Args: + graph: The FX joint graph with descriptors + + Returns: + A list of FX nodes representing all buffers in the graph. + + Raises: + RuntimeError: If subclass tensors are encountered (not yet supported), as + it is not clear if you wanted each individual constituent piece of the + subclasses, or have them grouped up in some way. + """ + return list(get_named_buffer_nodes(graph).values()) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/graph_capture.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/graph_capture.py new file mode 100644 index 0000000000000000000000000000000000000000..f1752bd4d13e7d2e3b683c340aa421c974a404fb --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/graph_capture.py @@ -0,0 +1,585 @@ +""" +This module dispatches the graphs to either the forward-only or joint compilation +pathways, taking into account the AOTConfig and the collected ViewAndMutationMetadata. +""" + +import contextlib +import dataclasses +from collections.abc import Callable +from typing import Any + +import torch +import torch.utils._pytree as pytree +import torch.utils.dlpack +from torch._dispatch.python import enable_python_dispatcher +from torch._dynamo.utils import detect_fake_mode, lazy_format_graph_code +from torch._logging import getArtifactLogger, trace_structured +from torch._subclasses.functional_tensor import FunctionalTensorMode +from torch.fx.experimental.proxy_tensor import make_fx +from torchgen.utils import dataclass_repr + +from .. import config +from .descriptors import AOTInput, BackwardTokenAOTInput +from .functional_utils import ( + assert_functional_graph, + propagate_input_mutation_stacktraces, +) +from .graph_capture_wrappers import ( + aot_dispatch_subclass, + create_functionalized_fn, + create_joint, + fn_input_mutations_to_outputs, + fn_prepped_for_autograd, + handle_effect_tokens_fn, +) +from .schemas import AOTConfig, FxValue, SubclassMeta, TraceFn, ViewAndMutationMeta +from .streams import ( + assign_backward_streams, + assign_epilogue_copy_streams, + insert_backward_syncs, + populate_fw_metadata_with_stream_indices, + sync_deallocations, + wrap_all_sync_nodes_with_control_deps, +) +from .utils import ( + call_and_expect_output_descs, + copy_fwd_metadata_to_bw_nodes, + fn_wrappers, + register_buffer_assignment_hook, + root_module_when_exporting_non_strict, + simple_wraps, + unlift_tokens, +) + + +aot_graphs_log = getArtifactLogger(__name__, "aot_graphs") + + +def _extract_tangent_source_stack_traces( + fx_g: torch.fx.GraphModule, + fw_metadata: ViewAndMutationMeta, +) -> None: + from .descriptors import PlainAOTOutput, TangentAOTInput + + if not fw_metadata.traced_tangents_descs: + return + + output_node = list(fx_g.graph.nodes)[-1] + all_outputs = output_node.args[0] + + stack_traces: list[str | None] = [] + got_one = False + + for desc in fw_metadata.traced_tangents_descs: + stack_trace = None + + if isinstance(desc, TangentAOTInput): + output_desc = desc.output + if isinstance(output_desc, PlainAOTOutput) and output_desc.idx < len( + all_outputs + ): + output_arg = all_outputs[output_desc.idx] + if isinstance(output_arg, torch.fx.Node): + stack_trace = output_arg.meta.get("stack_trace", None) + got_one = True + + stack_traces.append(stack_trace) + + if got_one: + fw_metadata.tangent_source_stack_traces = stack_traces + + +def _create_graph( + f: Callable[..., Any], + args: list[torch.Tensor], + args_descs: list[AOTInput] + | None = None, # keep compat with old clients; maybe we should split into two impls + *, + aot_config: AOTConfig, +) -> torch.fx.GraphModule: + # FunctionalTensorMode must be enabled here. + # See Note [Accessing .grad_fn on FunctionalTensor] + out_descs = None + + if args_descs is None: + inner_f = f + else: + + @simple_wraps(f) + def inner_f(*args: Any) -> Any: + nonlocal out_descs + if out_descs is not None: + raise AssertionError("out_descs must be None") + out, out_descs = call_and_expect_output_descs(f, args) + return out + + if aot_config.disable_functionalization: + ctx = contextlib.nullcontext() + else: + ctx = FunctionalTensorMode( # type: ignore[assignment] + pre_dispatch=aot_config.pre_dispatch, + export=aot_config.is_export, + # Allow token discovery for joint fn tracing as tokens can be used in backward. + _allow_token_discovery=True, + ) + + with ( + enable_python_dispatcher(), + ctx, + ): + fx_g = make_fx( + inner_f, + decomposition_table=aot_config.decompositions, + record_module_stack=True, + pre_dispatch=aot_config.pre_dispatch, + _disable_torch_fn_metadata_mode=aot_config._disable_torch_fn_metadata_mode, + )(*args) + + if args_descs is not None: + flat_args_descs, _ = pytree.tree_flatten(args_descs) + flat_out_descs, _ = pytree.tree_flatten(out_descs) + + # Unfortunately, flat_args_descs is not guaranteed to match the + # number of actual arguments that show up on the FX graph. + # Specifically, allow_token_discovery=True means that we will + # silently add extra token arguments to the backwards graph. + # + # Although there are a few ways to detect what these tokens are, + # we are going to settle for something dodgy but simple to + # implement: match tangents_token placeholders specifically, + # as these are the only placeholders that are created by token + # discovery (NB: there is NO other code that treats this name + # as load bearing, so this is a bit naughty!) + # + # I originally wanted to detect tokens in exactly the same way + # that they are detected at normal runtime, but to be honest + # the normal runtime detection is pretty strange: it seems the + # backward tokens are not reliably at the end of the argument list + # but *precede* the RNG arguments (I don't understand why this is + # the case). And in unlift_tokens, token arguments are detected + # by seeing if they feed into an effects call! Dastardly. Why + # didn't we just introduce a new type. + + i = 0 + j = 0 + for n in fx_g.graph.nodes: + if n.op == "placeholder": + if n.name.startswith("tangents_token"): + n.meta["desc"] = BackwardTokenAOTInput(j) + j += 1 + else: + if i >= len(flat_args_descs): + raise AssertionError( + f"i={i} >= len(flat_args_descs)={len(flat_args_descs)}: " + f"fn_wrappers={fn_wrappers(inner_f)}, " + f"placeholders={[n for n in fx_g.graph.nodes if n.op == 'placeholder']}" + ) + n.meta["desc"] = flat_args_descs[i] + i += 1 + elif n.op == "output": + n.meta["desc"] = flat_out_descs + + return fx_g + + +# TODO: Refactor the following code so detach() persists item_memo +def _detach_and_copy_item_memo(t: torch.Tensor) -> torch.Tensor: + detached_t = t.detach() + if hasattr(t, "item_memo"): + # pyrefly: ignore[missing-attribute] + detached_t.item_memo = t.item_memo + return detached_t + + +@dataclasses.dataclass +class _GraphCaptureTracingResult: + fn_to_trace: Callable[..., Any] + flat_args: Any + flat_args_descs: Any + maybe_subclass_meta: SubclassMeta | None + + +def _detach_traced_inputs(flat_args: Any) -> Any: + if detect_fake_mode(): + detach_tensor = _detach_and_copy_item_memo + else: + + def detach_tensor(t: torch.Tensor) -> torch.Tensor: + return t.detach() + + return pytree.tree_map_only(torch.Tensor, detach_tensor, flat_args) + + +def _prepare_graph_capture_tracing( + fn_to_trace: Callable[..., Any], + flat_args: Any, + flat_args_descs: Any, + flat_fn: TraceFn, + *, + fw_metadata: ViewAndMutationMeta, + aot_config: AOTConfig, + trace_joint: bool, + joint_fn_handle: Any | None = None, +) -> _GraphCaptureTracingResult: + if aot_config.disable_functionalization: + updated_flat_args, updated_flat_args_descs = flat_args, flat_args_descs + else: + fn_to_trace, updated_flat_args, updated_flat_args_descs = ( + create_functionalized_fn( + fn_to_trace, + flat_args, + flat_args_descs, + meta=fw_metadata, + aot_config=aot_config, + trace_joint=trace_joint, + joint_fn_handle=joint_fn_handle, + ) + ) + + subclass_tracing_info = aot_dispatch_subclass( + fn_to_trace, + updated_flat_args, + updated_flat_args_descs, + is_joint_structure=trace_joint, + meta=fw_metadata, + fw_only=flat_fn, + ) + fn_to_trace = subclass_tracing_info.plain_tensor_trace_fn + updated_flat_args = subclass_tracing_info.plain_tensor_args + updated_flat_args_descs = subclass_tracing_info.plain_tensor_args_descs + + if not aot_config.disable_functionalization: + fn_to_trace, updated_flat_args, updated_flat_args_descs = ( + handle_effect_tokens_fn( + fn_to_trace, + updated_flat_args, + updated_flat_args_descs, + meta=fw_metadata, + trace_joint=trace_joint, + ) + ) + + return _GraphCaptureTracingResult( + fn_to_trace=fn_to_trace, + flat_args=updated_flat_args, + flat_args_descs=updated_flat_args_descs, + maybe_subclass_meta=subclass_tracing_info.maybe_subclass_meta, + ) + + +def _create_graph_and_save_traced_inputs( + fn_to_trace: Callable[..., Any], + flat_args: Any, + flat_args_descs: Any, + *, + aot_config: AOTConfig, +) -> tuple[torch.fx.GraphModule, Any]: + saved_flat_args = _detach_traced_inputs(flat_args) + return ( + _create_graph(fn_to_trace, flat_args, flat_args_descs, aot_config=aot_config), + saved_flat_args, + ) + + +def aot_dispatch_base_graph( + flat_fn: TraceFn, + flat_args: list[FxValue], + flat_args_descs: list[AOTInput], + aot_config: AOTConfig, + *, + fw_metadata: ViewAndMutationMeta, +) -> tuple[torch.fx.GraphModule, list[FxValue], list[AOTInput], SubclassMeta | None]: + # aot_dispatch_base requires functionalization, but doesn't need to handle as many cases as the autograd case. + # The cases that aot_dispatch_base doesn't need to handle include: + # - outputs that are aliases of graph intermediates + # - outputs that are aliases of graph inputs + # While cases that it does need to handle include: + # - input mutations (including when inputs are aliases of each other) + # - input metadata mutations + fn_to_trace = fn_input_mutations_to_outputs( + flat_fn, + flat_args_descs, + fw_metadata, + keep_data_input_mutations=aot_config.keep_inference_input_mutations, + ) + # TODO: replace with AOTDispatchSubclassWrapper once we refactor + # fn_input_mutations_to_outputs and create_functionalized_fn + # into CompilerWrappers. + tracing_state = _prepare_graph_capture_tracing( + fn_to_trace, + flat_args, + flat_args_descs, + flat_fn, + fw_metadata=fw_metadata, + aot_config=aot_config, + trace_joint=False, + ) + fn_to_trace = tracing_state.fn_to_trace + updated_flat_args_subclasses_desugared = tracing_state.flat_args + updated_flat_args_subclasses_desugared_descs = tracing_state.flat_args_descs + maybe_subclass_meta = tracing_state.maybe_subclass_meta + + aot_graphs_log.debug( + "aot_config id: %s, fw_metadata=%s,subclass_metadata=%s", + aot_config.aot_id, + fw_metadata, + maybe_subclass_meta, + ) + + # We track buffer assignments when exporting in non-strict mode. + # (In contrast, strict mode errors on any attribute assignment.) + mod_when_exporting_non_strict = root_module_when_exporting_non_strict(flat_fn) + if aot_config.is_export and mod_when_exporting_non_strict is not None: + # For any buffer that is assigned, we want to associate it to the final proxy node + # that it is assigned to. This node can then be added as a buffer mutation output. + assigned_buffers: dict[str, str] = {} + hook = register_buffer_assignment_hook( + mod_when_exporting_non_strict, assigned_buffers + ) + + ( + fw_module, + saved_updated_flat_args_subclasses_desugared, + ) = _create_graph_and_save_traced_inputs( + fn_to_trace, + updated_flat_args_subclasses_desugared, + updated_flat_args_subclasses_desugared_descs, + aot_config=aot_config, + ) + saved_updated_flat_args_subclasses_desugared_descs = ( + updated_flat_args_subclasses_desugared_descs + ) + + if aot_config.is_export and mod_when_exporting_non_strict is not None: + # We update metadata to consider any assigned buffers as buffer mutations. + i = len(dict(mod_when_exporting_non_strict.named_parameters())) + for name, _ in mod_when_exporting_non_strict.named_buffers(): + if name in assigned_buffers and not fw_metadata.input_info[i].mutates_data: # type: ignore[possibly-undefined] + fw_metadata.input_info[i] = dataclasses.replace( + fw_metadata.input_info[i], mutates_data=True + ) + fw_metadata.num_mutated_inp_runtime_indices += 1 + i += 1 + + # We add nodes corresponding to buffer assignments as output nodes in the graph. + add_nodes = [] + output_node = list(fw_module.graph.nodes)[-1] + for name in assigned_buffers.values(): # type: ignore[possibly-undefined] + for node in fw_module.graph.nodes: + if node.name == name: + add_nodes.append(node) + node.users[output_node] = None + output_node.args = ((*add_nodes, *output_node.args[0]),) + + hook.remove() # type: ignore[possibly-undefined] + + # As long as we opted to remove input mutations, then + # there should be *NO* mutating ops in the graph at this point. + if not aot_config.disable_functionalization: + copy_count = assert_functional_graph(fw_module.graph) + assign_epilogue_copy_streams(fw_module) + # Wrap sync nodes with control_deps to prevent reordering + wrap_all_sync_nodes_with_control_deps(fw_module) + # Populate fw_metadata with stream indices from the compiled graph + populate_fw_metadata_with_stream_indices(fw_module, fw_metadata) + fw_module.graph.eliminate_dead_code() + fw_module.recompile() + copy_count2 = assert_functional_graph(fw_module.graph) + propagate_input_mutation_stacktraces(fw_module.graph) + if copy_count != copy_count2: + raise AssertionError( + f"copy_count={copy_count} != copy_count2={copy_count2}" + ) + else: + fw_module.graph.eliminate_dead_code() + + # See Note [Side-Effectful Tokens in AOTAutograd] + num_tokens = len(fw_metadata.tokens) + if num_tokens != 0 and config.unlift_effect_tokens: + unlift_tokens(fw_module, fw_metadata, aot_config) + saved_updated_flat_args_subclasses_desugared = ( + saved_updated_flat_args_subclasses_desugared[num_tokens:] + ) + saved_updated_flat_args_subclasses_desugared_descs = ( + saved_updated_flat_args_subclasses_desugared_descs[num_tokens:] + ) + + if aot_config.enable_log: + aot_graphs_log.info( + "%s", + lazy_format_graph_code( + "Forward graph", + fw_module, + aot_config.aot_id, + include_stride=True, + include_device=True, + colored=True, + # For more expanded output set this to True (but can't default + # to this because it affects tests): + expanded_def=False, + ), + ) + + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "aot_forward_graph_fw_metadata", + "encoding": "string", + }, + payload_fn=lambda: dataclass_repr(fw_metadata), + ) + if maybe_subclass_meta is not None: + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "aot_forward_graph_fw_subclass_metadata", + "encoding": "string", + }, + payload_fn=lambda: dataclass_repr(maybe_subclass_meta), + ) + + trace_structured( + "aot_inference_graph", + payload_fn=lambda: fw_module.print_readable( + print_output=False, + include_stride=True, + include_device=True, + expanded_def=True, + ), + ) + + # TODO: should factor this into a separate function for export that always only returns just the graph. + if aot_config.is_export and maybe_subclass_meta is not None: + raise AssertionError( + "aot_export_module does not support tensor subclass inputs for now." + ) + return ( + fw_module, + saved_updated_flat_args_subclasses_desugared, + saved_updated_flat_args_subclasses_desugared_descs, + maybe_subclass_meta, + ) + + +# Has the precondition that there +# are no duplicate arguments in flat_args (e.g., the same Tensor +# object never shows up twice. However, two tensor inputs MAY alias +# the same storage, so long as they have separate TensorImpls.) +def aot_dispatch_autograd_graph( + flat_fn: TraceFn, + flat_args: list[Any], + flat_args_descs: list[AOTInput], + aot_config: AOTConfig, + *, + fw_metadata: ViewAndMutationMeta, +) -> tuple[ + torch.fx.GraphModule, + tuple[list[Any], list[Any]], + tuple[list[AOTInput], list[AOTInput]], + SubclassMeta | None, +]: + # NB: flat_fn here is the original user function (as far as + # aot_module_simplified is concerned) + + # traced_tangents corresponds to the set of outputs in the traced forward that should get grad_outputs in the traced backward. + # It includes outputs of the original forward, *and* any updated inputs due to input mutations. + # However, it does *not* include any outputs that are aliases of inputs or intermediates, or any metadata-only input mutations. + joint_inputs = (flat_args, fw_metadata.traced_tangents) + joint_inputs_descs = (flat_args_descs, fw_metadata.traced_tangents_descs) + + fn_prepared_for_autograd = fn_prepped_for_autograd( + flat_fn, + flat_args_descs, + fw_metadata, + aot_config, + ) + joint_fn_to_trace = create_joint( + fn_prepared_for_autograd, flat_args_descs, aot_config=aot_config + ) + # pyrefly: ignore[missing-attribute] + joint_fn_handle = joint_fn_to_trace.handle + # TODO: replace with AOTDispatchSubclassWrapper once we refactor + # fn_input_mutations_to_outputs and create_functionalized_fn + # into CompilerWrappers. + tracing_state = _prepare_graph_capture_tracing( + joint_fn_to_trace, + joint_inputs, + joint_inputs_descs, + flat_fn, + fw_metadata=fw_metadata, + aot_config=aot_config, + trace_joint=True, + joint_fn_handle=joint_fn_handle, + ) + joint_fn_to_trace = tracing_state.fn_to_trace + updated_joint_inputs = tracing_state.flat_args + updated_joint_inputs_descs = tracing_state.flat_args_descs + maybe_subclass_meta = tracing_state.maybe_subclass_meta + + # When we call _create_graph, this may mutate the metadata of joint + # inputs. But callers are expecting to get the original joint inputs. So + # we make aliases of all the inputs to make sure we have a copy that + # doesn't get modified. + # + # This destroys requires_grad/grad_fn information. However, backends + # beneath AOTAutograd are indifferent to this information, so it doesn't + # matter. + fx_g, saved_updated_joint_inputs = _create_graph_and_save_traced_inputs( + joint_fn_to_trace, + updated_joint_inputs, + updated_joint_inputs_descs, + aot_config=aot_config, + ) + + # Redundant with the check above, but worth having in case tracing introduced + # a fake tensor. Unlikely. + # See Note: [Fake Modules and AOTAutograd] + torch._dynamo.utils.assert_no_fake_params_or_buffers(fx_g) + + # Have to copy before eliminate_dead_code otherwise the + # fw node match might be erased + copy_fwd_metadata_to_bw_nodes(fx_g) + + # After copying metadata, assign streams to gradient accumulation nodes + assign_backward_streams(fx_g) + + assign_epilogue_copy_streams(fx_g) + + # Insert syncs for newly assigned backward streams + insert_backward_syncs(fx_g) + + # Sync deallocations for tensors where the stream w/ their last usage + # is distinct from their allocation stream + sync_deallocations(fx_g) + + # Wrap sync nodes with control_deps to prevent reordering + # (must be after sync_deallocations which inserts additional sync nodes) + wrap_all_sync_nodes_with_control_deps(fx_g) + + # Populate fw_metadata with stream indices from the compiled graph + # NB: This needs to be done after the above stream assignments + populate_fw_metadata_with_stream_indices(fx_g, fw_metadata) + + # this helps users identify which forward output to call .detach() on. + _extract_tangent_source_stack_traces(fx_g, fw_metadata) + + fx_g.graph.eliminate_dead_code() + if not aot_config.disable_functionalization: + # There should be *NO* mutating ops in the graph at this point. + assert_functional_graph(fx_g.graph) + + fx_g.recompile() + + # TODO: in AOTAutograd, we create metadata like _indices_of_inps_to_detach to detect + # when we need to manually detach() some inputs in the forward. + # Higher order ops might eventually need to do the same. + if aot_config.is_export and maybe_subclass_meta is not None: + raise AssertionError( + "aot_export_module does not support tensor subclass inputs for now." + ) + return ( + fx_g, + saved_updated_joint_inputs, + updated_joint_inputs_descs, + maybe_subclass_meta, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/graph_capture_wrappers.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/graph_capture_wrappers.py new file mode 100644 index 0000000000000000000000000000000000000000..bb26cca92171c64af63d81d5d95898abe7c3f205 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/graph_capture_wrappers.py @@ -0,0 +1,1554 @@ +""" +This module is responsible for transforming functions to be traced into a form +that is easier for the downstream infra (e.g. Autograd, FX, AOTAutograd analysis) +to handle. + +It does so by: +1. functionalization (including RNG functionalzation) +2. creating a joint graph when required +3. transforming mutations into extra outputs +4. dispatching subclasses +""" + +import typing +import warnings +from collections.abc import Callable, Generator +from contextlib import AbstractContextManager, contextmanager, ExitStack, nullcontext +from dataclasses import dataclass +from typing import Any, TypeVar +from unittest.mock import patch + +import torch +import torch.fx.traceback as fx_traceback +import torch.utils._pytree as pytree +from torch import Tensor +from torch._decomp.decompositions_for_rng import PhiloxStateTracker +from torch._guards import detect_fake_mode +from torch._opaque_base import OpaqueBase +from torch._prims_common import CUDARngStateHelper +from torch.fx.experimental.proxy_tensor import ( + _proxy_tensor_disable_update_tensor_tracker, + get_proxy_mode, + maybe_disable_thunkify, + maybe_enable_thunkify, +) +from torch.fx.experimental.symbolic_shapes import ( + guard_or_true, + PropagateUnbackedSymInts, + sym_eq, +) +from torch.nn.utils import stateless +from torch.utils._python_dispatch import is_traceable_wrapper_subclass +from torch.utils._pytree import TreeSpec + +from .. import config +from .collect_metadata_analysis import run_functionalized_fw_and_collect_metadata +from .descriptors import ( + AOTInput, + AOTOutput, + BackwardTokenAOTOutput, + ForwardTokenAOTInput, + ForwardTokenAOTOutput, + GradAOTOutput, + InputMutationAOTOutput, + IntermediateBaseAOTOutput, + PhiloxBackwardBaseOffsetAOTInput, + PhiloxBackwardSeedAOTInput, + PhiloxForwardBaseOffsetAOTInput, + PhiloxForwardSeedAOTInput, + PhiloxUpdatedBackwardOffsetAOTOutput, + PhiloxUpdatedForwardOffsetAOTOutput, +) +from .functional_utils import ( + _check_if_mutation_can_be_in_graph, + are_all_mutations_hidden_from_autograd, + are_all_mutations_under_no_grad_or_inference_mode, + from_fun, + has_data_mutation, + has_metadata_mutation, + is_fun, + sync_functional_tensor, + to_fun, + was_inductor_storage_resized, +) +from .logging_utils import setup_stacktrace_preservation_hooks +from .schemas import ( + AOTConfig, + FxValue, + InputAliasInfo, + JointTraceFn, + MutationType, + OutputType, + PreppedForAutogradTraceFn, + SubclassMeta, + SubclassTracingInfo, + TraceFn, + ViewAndMutationMeta, +) +from .subclass_utils import ( + create_subclass_meta, + remap_unwrapped_subclass_arg_indices, + requires_subclass_dispatch, + unwrap_tensor_subclasses, + wrap_tensor_subclasses_maybe_joint, +) +from .utils import ( + _is_tangent, + call_and_expect_output_descs, + maybe_to_fresh_input, + simple_wraps, + without_output_descs, +) + + +# This function returns a new function that returns mutated inputs as outputs. +# if keep_data_input_mutations is set, then we assume that data-only mutations +# will be left in the graph, and we only return metadata-mutated inputs as outputs. +def fn_input_mutations_to_outputs( + fn: Callable[..., Any], + args_descs: list[AOTInput], + meta: ViewAndMutationMeta, + keep_data_input_mutations: bool, +) -> Any: + @simple_wraps(fn) + def inner_fn(*args: FxValue) -> tuple[tuple[Any, ...], tuple[Any, ...]]: + outs, outs_descs = call_and_expect_output_descs(fn, args) + if len(meta.output_info) != len(outs): + raise AssertionError( + f"output_info length ({len(meta.output_info)}) != outs length ({len(outs)})" + ) + # The compiled fw will return mutated input tensors, *including* metadata-only mutation. + # However, if keep_data_input_mutations is set, the compiled fw only needs to return metadata-mutated inputs. + # (because data-only input mutations are handled directly in the compiled graph) + mutated_input_pairs = [ + (x, InputMutationAOTOutput(src)) + for (i, (x, src)) in enumerate(zip(args, args_descs)) + if i in meta.mutated_inp_runtime_indices + ] + if mutated_input_pairs: + mutated_inputs_to_return, mutated_inputs_to_return_descs = zip( + *mutated_input_pairs + ) + else: + mutated_inputs_to_return, mutated_inputs_to_return_descs = (), () + return ( + (*mutated_inputs_to_return, *outs), + (*mutated_inputs_to_return_descs, *outs_descs), + ) + + return inner_fn + + +@contextmanager +def disable_autocast() -> Generator[None, None, None]: + with ExitStack() as stack: + autocast_enabled_devices = torch._C._autocast_supported_devices() + for device_type in autocast_enabled_devices: + if hasattr(torch, device_type): + stack.enter_context(torch.amp.autocast(device_type, enabled=False)) + yield + + +# This function takes in a fn with external aliasing and mutation, +# and returns a new fn with no external aliasing and mutation, +# as needed for autograd. +# The main transformations are: +# - Return mutated inputs as extra outputs +# - Clone mutated inputs that require gradients, +# because autograd will require us to pass the pre-mutated inputs into autograd.grad +# - Return intermediate bases of outputs as additional outputs, +# needed to appease autograd.Function +# The new function returns: +# (1) The updated outputs +# (2) A boolean mask of len(new_fn_outputs), +# that can be used to tell autograd.grad which outputs should get tangents +# if we trace the backward. +def fn_prepped_for_autograd( + fn: TraceFn, + args_descs: list[AOTInput], + meta: ViewAndMutationMeta, + aot_config: AOTConfig, +) -> PreppedForAutogradTraceFn: + @simple_wraps(fn) + def inner_fn( + *args: FxValue, + ) -> tuple[tuple[list[FxValue], list[bool]], list[AOTOutput]]: + args_maybe_cloned = [ + maybe_to_fresh_input(i, t, meta) for i, t in enumerate(args) + ] + + outs, outs_descs = call_and_expect_output_descs(fn, args_maybe_cloned) # type: ignore[arg-type] + if not isinstance(outs, (tuple, list)): + raise AssertionError(f"expected outs to be tuple or list, got {type(outs)}") + outs = list(outs) + if len(meta.output_info) != len(outs): + raise AssertionError( + f"output_info length ({len(meta.output_info)}) != outs length ({len(outs)})" + ) + + mutated_input_pairs = [ + (x, InputMutationAOTOutput(src)) + for (i, (x, src)) in enumerate(zip(args_maybe_cloned, args_descs)) + if i in meta.mutated_inp_runtime_indices + ] + if mutated_input_pairs: + mutated_inputs_to_return, mutated_inputs_to_return_descs = zip( + *mutated_input_pairs + ) + else: + mutated_inputs_to_return, mutated_inputs_to_return_descs = (), () + + intermediate_bases = [] + intermediate_bases_descs = [] + for o, info, o_desc in zip(outs, meta.output_info, outs_descs): + if info.output_type == OutputType.alias_of_intermediate_save_as_output: + if not isinstance(o, torch.Tensor): + raise AssertionError( + f"Expected tensor for intermediate base, got {type(o)}" + ) + intermediate_bases.append(o._base) + intermediate_bases_descs.append(IntermediateBaseAOTOutput(o_desc)) + + if meta.num_intermediate_bases != len(intermediate_bases): + raise AssertionError( + f"num_intermediate_bases ({meta.num_intermediate_bases}) != len(intermediate_bases) ({len(intermediate_bases)})" + ) + + # the compiled forward should return (mutated_inputs, user_outs, intermediate_bases) + fw_outs_to_return = *mutated_inputs_to_return, *outs, *intermediate_bases + fw_outs_to_return_descs = ( + *mutated_inputs_to_return_descs, + *outs_descs, + *intermediate_bases_descs, + ) + + # Also return a boolean mask specifying which outputs to this function will be used as tangents + mutated_inputs_grad_mask = [ + meta.input_info[meta.mutated_inp_runtime_indices[i]].mutates_data + and meta.input_info[meta.mutated_inp_runtime_indices[i]].requires_grad + for (i, x) in enumerate(mutated_inputs_to_return) + ] + + # Pass any (non-aliased) outputs in as tangents, since they'll be returned as outputs in the fw + # For outputs that are aliases of intermediates, we will have returned the output's _base as an output in the graph instead, + # which we *should* send to grad() + output_grad_mask = [ + meta.output_info[i].output_type + in [ + OutputType.non_alias, + OutputType.unsafe_view_alias, + OutputType.custom_function_view, + ] + # Also, only tensor outputs should participate in the backward + # (in particular, Symint outputs in the forward graph shouldn't get tangents) + and issubclass(meta.output_info[i].raw_type, Tensor) + and meta.output_info[i].requires_grad_for_backward + for (i, x) in enumerate(outs) + ] + + intermediate_base_grad_mask = [True for _ in range(len(intermediate_bases))] + + out_grad_mask = ( + mutated_inputs_grad_mask + output_grad_mask + intermediate_base_grad_mask + ) + if len(out_grad_mask) != len(fw_outs_to_return): + raise AssertionError( + f"out_grad_mask length ({len(out_grad_mask)}) != fw_outs_to_return length ({len(fw_outs_to_return)})" + ) + + # Take care to grab and sync the updated inputs from primals_after_cloning (the inputs we actually mutate!) + # and not primals (the preserved inputs, pre-mutation, that we pass to grad()) + # This is annoying: our joint function needs to be aware of functionalization + # (syncing mutated inputs before calling autograd.grad()) + # In theory, we could make the autograd engine do this automatically, although that probably isn't any cleaner. + if not aot_config.disable_functionalization: + for arg in args_maybe_cloned: + if not isinstance(arg, Tensor): + continue + sync_functional_tensor(arg) + + # pyrefly: ignore[bad-return] + return (fw_outs_to_return, out_grad_mask), ( + fw_outs_to_return_descs, + out_grad_mask, + ) + + return inner_fn + + +@dataclass +class JointFnHandle: + post_forward: Callable[..., Any] | None = None + + +# Given a fn, computes the joint. +# NOTE: fn is expects the following behavior: +# (1) fn() needs to return a tuple of (outs, mask), +# where `mask` tells us which outputs are meant to have tangents. +# we don't know this info automatically, because we don't actually want to blindly +# compute tangents for every output that requires grad. +# Specifically, outputs that alias inputs won't participate in the backward and get tangents. +# (2) fn() cannot mutate any inputs that require gradient. +# otherwise, when we compute autograd.grad(), we will not take those input mutations into account +# (the way this is handled is that we ensure any inputs that normally get mutated are cloned first) +def create_joint( + fn: Callable[..., Any], + primals_descs: list[AOTInput] | None = None, + *, + aot_config: AOTConfig, +) -> Callable[..., Any]: + joint_fn_handle = JointFnHandle() + + # post_forward + # NB: this type is inaccurate when primals_descs is None + @simple_wraps(fn) + def inner_fn( + primals: list[FxValue], tangents: list[FxValue] + ) -> tuple[ + tuple[list[FxValue], list[Tensor | None]], + tuple[list[AOTOutput], list[AOTOutput | None]], + ]: + outs_descs = None + if primals_descs is None: + outs, tangent_mask = fn(*primals) + if pytree.tree_any(lambda x: isinstance(x, AOTOutput), tangent_mask): + raise AssertionError( + "tangent_mask should not contain AOTOutput instances" + ) + else: + (outs, tangent_mask), (outs_descs, _) = call_and_expect_output_descs( + fn, + primals, # type: ignore[arg-type] + ) + mode = get_proxy_mode() + if mode is None: + raise AssertionError("Expected non-None proxy mode") + for node in mode.tracer.graph.nodes: + if _is_tangent(node): + node.meta["partitioner_tag"] = "is_backward" + else: + node.meta["partitioner_tag"] = "is_forward" + + # TODO: I think this hook can also be eliminated now + if joint_fn_handle and joint_fn_handle.post_forward: + joint_fn_handle.post_forward(primals) + + if len(tangent_mask) != len(outs): + raise AssertionError( + f"tangent_mask length ({len(tangent_mask)}) != outs length ({len(outs)})" + ) + outs_to_grad = [ + o for needs_tangent, o in zip(tangent_mask, outs) if needs_tangent + ] + if len(outs_to_grad) != len(tangents): + raise AssertionError( + f"outs_to_grad length ({len(outs_to_grad)}) != tangents length ({len(tangents)})" + ) + + # Get the inputs that need gradients + grad_primals: list[torch.Tensor] = [] + inputs_needs_grads = [] + # Note that we're not using primals here, + # being carefully not to pass any mutated inputs into autograd.grad() + for p in primals: + if isinstance(p, Tensor) and p.requires_grad: + inputs_needs_grads.append(True) + if not isinstance(p, torch.Tensor): # Help mypy understand the type + raise AssertionError(f"expected Tensor, got {type(p)}") + grad_primals.append(p) + else: + inputs_needs_grads.append(False) + + # Get the outputs that need gradients + needed_outs: list[Tensor] = [] + needed_tangents: list[Tensor] = [] + for out, tangent in zip(outs_to_grad, tangents): + if isinstance(out, Tensor) and out.requires_grad: + # A bit sketchy, but fixes e.g. test_aot_autograd_exhaustive_matmul_cpu_float32 + # The issue is that we are sensitive to decomps that don't accurately maintain + # their output's _base.shape compared to eager mode, and this helps mitigate a bit. + # The guard_or_true also sketchy; if unbacked + # symints are involved, we're just going to assume that the + # decomps setup the base shape correctly + + # Return out if the result of out.shape==tangent.shape is unknown or known to be true. + # otherwise if its a known false return out.view(tangent.shape). + # tangent should also be a tensor since it corresponds to a tensor output + if not isinstance(tangent, torch.Tensor): + raise AssertionError( + f"Expected tensor tangent, got {type(tangent)}" + ) + needed_outs.append( + out + if guard_or_true(sym_eq(out.shape, tangent.shape)) + else out.view(tangent.shape) + ) + needed_tangents.append(tangent) + + setup_stacktrace_preservation_hooks( + [out.grad_fn for out in needed_outs if out.grad_fn is not None] + ) + + if config.functionalize_rng_ops: + PhiloxStateTracker.mark_beginning_of_backward() + backward_out: tuple[Tensor, ...] = () + # Call the backwards pass + if grad_primals: + functional_tensor_mode = torch.utils._python_dispatch._detect_infra_mode( + torch._C._TorchDispatchModeKey.FUNCTIONAL + ) + if functional_tensor_mode is not None: + # Side-Effect Tokens: + # We want to have independent chains of tokens for forward and backward. + # functional_tensor_mode._tokens is used by both. + # We memoize the result tokens of forward in functional_tensor_mode._tokens_forward_output, + # to return them as joint graph outputs. + # We clean functional_tensor_mode._tokens before backward, to prevent reuse of forward tokens in backward. + # Joint graph tracing allows tokens discovery, + # So all the tokens in backward will be created and added as a graph inputs during tracing. + functional_tensor_mode._tokens_forward_output = ( + functional_tensor_mode._tokens + ) + functional_tensor_mode._tokens = {} # pyrefly: ignore[implicit-any] + + with ( + set_partitioner_tag_is_backward(), + fx_traceback.preserve_node_meta(), + ExitStack() as stack, + ): + backward_pass_autocast = torch._functorch.config.backward_pass_autocast + if backward_pass_autocast == "same_as_forward": + # Use the ambient autocast mode(s) + pass + elif backward_pass_autocast == "off": + stack.enter_context(disable_autocast()) + else: + # Disable autocast, then enable anything in `backward_pass_autocast`. + stack.enter_context(disable_autocast()) + if not isinstance(backward_pass_autocast, list): + raise AssertionError( + f"expected backward_pass_autocast to be a list, got {type(backward_pass_autocast)}" + ) + for kwargs in backward_pass_autocast: + if not isinstance(kwargs, dict): + raise AssertionError( + f"expected kwargs to be a dict, got {type(kwargs)}" + ) + stack.enter_context(torch.amp.autocast(**kwargs)) + + # for full graph export, we always export a joint graph where we assume no tangents are needed. + if aot_config.no_tangents: + if not ( + len(needed_tangents) == 1 and needed_tangents[0].numel() == 1 + ): + raise AssertionError( + f"expected single scalar tangent for no_tangents mode, got {len(needed_tangents)} tangents" + ) + backward_out = torch.autograd.grad( + needed_outs, + grad_primals, + allow_unused=True, + ) + else: + backward_out = torch.autograd.grad( + needed_outs, + grad_primals, + grad_outputs=needed_tangents, + allow_unused=True, + ) + backward_out_iter = iter(backward_out) + final_outs = ( + outs, + [next(backward_out_iter) if i else None for i in inputs_needs_grads], + ) + if primals_descs is None: + return final_outs # type: ignore[return-value] + if outs_descs is None: + raise AssertionError("outs_descs must not be None") + # pyrefly: ignore[bad-return] + return final_outs, ( + outs_descs, + [ + # TODO: ideally we do know this is DifferentiableAOTInput + # but this is quite an involved refactor + GradAOTOutput(desc) if i else None # type: ignore[arg-type] + for i, desc in zip(inputs_needs_grads, primals_descs) + ], + ) + + @simple_wraps(inner_fn) + def inner_fn_with_anomaly( + primals: list[FxValue], tangents: list[FxValue] + ) -> tuple[ + tuple[list[FxValue], list[Tensor | None]], + tuple[list[AOTOutput], list[AOTOutput | None]], + ]: + with fx_traceback.preserve_node_meta(), warnings.catch_warnings(): + warnings.filterwarnings("ignore", "Anomaly Detection has been enabled.") + with torch.autograd.detect_anomaly(check_nan=False): + return inner_fn(primals, tangents) + + def joint_helper( + primals: list[FxValue], tangents: list[FxValue] + ) -> tuple[ + tuple[list[FxValue], list[Tensor | None]], + tuple[list[AOTOutput], list[AOTOutput | None]], + ]: + return inner_fn_with_anomaly(primals, tangents) + + joint_helper.handle = joint_fn_handle # type: ignore[attr-defined] + + # pyrefly: ignore[bad-return] + return joint_helper + + +def create_functionalized_rng_ops_wrapper( + func: Callable[..., Any], + args: Any, + args_descs: list[AOTInput], + trace_joint: bool = True, +) -> Any: + # Functionalization of rng ops changes the calling convention of the joint graph. + # It goes from (primals, tangents) to (seed, offset, primals, tangents) + # At runtime, we pass on the current seed and offset. This is hidden from + # the user. + fake_mode_det = detect_fake_mode() + fake_mode: AbstractContextManager[Any] = nullcontext() + if fake_mode_det is not None: + fake_mode = fake_mode_det + + def override_get_rng_state( + device: int | str | torch.device = "cuda", + ) -> Tensor: + out = PhiloxStateTracker.get_state_as_tensor() + return out + + def override_set_rng_state( + x: Tensor, device: int | str | torch.device = "cuda" + ) -> None: + PhiloxStateTracker.set_state_from_tensor(x) + + def append_rng_offsets(outs: Any, outs_descs: Any) -> Any: + if trace_joint: + # outs signature before: Tuple(fwd_outputs), Tuple(bwd_outputs) + # outs signature after: Tuple(fwd_outputs, new_fwd_rng_offset), Tuple(bwd_offset, new_bwd_rng_offset) + return ( + ( + (*outs[0], PhiloxStateTracker.get_updated_fwd_offset()), + (*outs[1], PhiloxStateTracker.get_updated_bwd_offset()), + ), + ( + (*outs_descs[0], PhiloxUpdatedForwardOffsetAOTOutput()), + (*outs_descs[1], PhiloxUpdatedBackwardOffsetAOTOutput()), + ), + ) + else: + # outs signature before: Tuple(fwd_outputs) + # outs signature after: Tuple(fwd_outputs, new_fwd_rng_offset) + return ( + (*outs, PhiloxStateTracker.get_updated_fwd_offset()), + (*outs_descs, PhiloxUpdatedForwardOffsetAOTOutput()), + ) + + def traced_joint( + primals: list[FxValue], + tangents: list[FxValue], + fwd_seed: Tensor, + fwd_base_offset: Tensor, + bwd_seed: Tensor, + bwd_base_offset: Tensor, + ) -> tuple[ + tuple[tuple[FxValue, ...], tuple[FxValue, ...]], + tuple[tuple[AOTOutput, ...], tuple[AOTOutput, ...]], + ]: + with ( + patch("torch.cuda.get_rng_state", override_get_rng_state), + patch("torch.cuda.set_rng_state", override_set_rng_state), + ): + return append_rng_offsets(*func(primals, tangents)) + + def traced_forward(*primals_fwd_seed_fwd_base_offset: Any) -> Any: + # The signature is (*primals, seed, offset) + with ( + patch("torch.cuda.get_rng_state", override_get_rng_state), + patch("torch.cuda.set_rng_state", override_set_rng_state), + ): + return append_rng_offsets(*func(*primals_fwd_seed_fwd_base_offset[:-2])) + + if trace_joint: + # Get the current seed and offset to setup tracing. + fwd_seed, fwd_base_offset = CUDARngStateHelper.get_torch_state_as_tuple( + fake_mode + ) + bwd_seed, bwd_base_offset = CUDARngStateHelper.get_torch_state_as_tuple( + fake_mode + ) + PhiloxStateTracker.record_state(fwd_seed, fwd_base_offset, "forward") + PhiloxStateTracker.record_state(bwd_seed, bwd_base_offset, "backward") + return ( + traced_joint, + ( + *args, + fwd_seed, + fwd_base_offset, + bwd_seed, + bwd_base_offset, + ), + ( + *args_descs, + PhiloxForwardSeedAOTInput(), + PhiloxForwardBaseOffsetAOTInput(), + PhiloxBackwardSeedAOTInput(), + PhiloxBackwardBaseOffsetAOTInput(), + ), + ) + else: + # Get the current seed and offset to setup tracing. + fwd_seed, fwd_base_offset = CUDARngStateHelper.get_torch_state_as_tuple( + fake_mode + ) + PhiloxStateTracker.record_state(fwd_seed, fwd_base_offset, "forward") + return ( + traced_forward, + (*args, fwd_seed, fwd_base_offset), + ( + *args_descs, + PhiloxForwardSeedAOTInput(), + PhiloxForwardBaseOffsetAOTInput(), + ), + ) + + +@contextmanager +def set_partitioner_tag(tag: str) -> Generator[None, None, None]: + meta_key = "partitioner_tag" + if not fx_traceback.has_preserved_node_meta(): + raise AssertionError("expected preserved node meta") + + original_val = fx_traceback.current_meta.get(meta_key, None) + fx_traceback.current_meta[meta_key] = tag + try: + yield + finally: + fx_traceback.current_meta[meta_key] = original_val + + +def set_partitioner_tag_is_backward() -> AbstractContextManager[None]: + return set_partitioner_tag("is_backward") + + +def set_partitioner_tag_must_be_in_backward() -> AbstractContextManager[None]: + return set_partitioner_tag("must_be_in_backward") + + +def set_partitioner_tag_must_be_in_forward() -> AbstractContextManager[None]: + return set_partitioner_tag("must_be_in_forward") + + +@dataclass +class MutationCounters: + mc_data: int + mc_storage: int + mc_inductor_storage_resized: int + + +T = TypeVar("T") + + +def sc_visit( + t: torch.Tensor, + fn: Callable[[Tensor], T], + reduce_fn: Callable[[T, T], T], + accum_init: T, +) -> T: + if not is_traceable_wrapper_subclass(t): + return fn(t) + + accum = accum_init + + def visit(e: Any) -> None: + if not is_traceable_wrapper_subclass(e): + nonlocal accum + accum = reduce_fn(accum, fn(e)) + return + + for a in e.__tensor_flatten__()[0]: + match getattr(e, a): + case torch.Tensor() as inner: + visit(inner) + case OpaqueBase(): + pass + case unexpected: + raise AssertionError( + f"expected Tensor or OpaqueBase, got {type(unexpected)}" + ) + + visit(t) + return accum + + +def _get_mutation_counter(t: torch.Tensor) -> int: + return sc_visit( + t, + lambda t: torch._functionalize_mutation_counter(t.elem), # type: ignore[attr-defined] + lambda l, r: max(l, r), + -1, + ) + + +def _get_storage_changed_counter(t: torch.Tensor) -> int: + return sc_visit( + t, + lambda t: torch._functionalize_storage_changed_counter(t.elem), # type: ignore[attr-defined] + lambda l, r: max(l, r), + -1, + ) + + +def _get_inductor_storage_resized_counter(t: torch.Tensor) -> int: + return sc_visit( + t, + lambda t: torch._functionalize_inductor_storage_resized_counter(t.elem), # type: ignore[attr-defined] + lambda l, r: max(l, r), + -1, + ) + + +def _get_mutation_counters(t: torch.Tensor) -> MutationCounters: + return MutationCounters( + _get_mutation_counter(t), + _get_storage_changed_counter(t), + _get_inductor_storage_resized_counter(t), + ) + + +def apply_in_graph_mutations( + input_info: InputAliasInfo, + inpt_old: Tensor, + inpt_new: Tensor, + f_inpt: Tensor, + input_idx: int, + mcs: MutationCounters | None = None, + applied_mcs: MutationCounters | None = None, +) -> None: + if input_info.mutation_type != MutationType.MUTATED_IN_GRAPH: + raise AssertionError( + f"expected mutation_type MUTATED_IN_GRAPH, got {input_info.mutation_type}" + ) + # See Note [set_() Input Mutations in AOTAutograd] + # all mutations on the input must be under no_grad, so it is safe to put in the graph + # Here, we're saying that if an input experienced a set call, inp.set_(other), + # then we can effectively not have to worry about whether its data was mutated. + # There are 3 cases: + # (1) We mutate inp *after* the set_() call. other is a graph intermediate. + # In this case, we're not really mutating the input storage of "inp"; + # we're mutating the storage of an intermdiate value (other), + # and slamming that storage into the input tensor. So no data mutation is necessary. + # (2) We mutate inp *after* the set_() call. other is a graph *input*. + # In this case, the data mutation will be properly handled in the runtime + # epilogue during the processing of "other" + # (3) We mutate inp *before* the set_() call. + # This case is *not* currently handled. + if input_info.mutates_storage_metadata: + if mcs is None or mcs.mc_storage > applied_mcs.mc_storage: # type: ignore[union-attr] + with torch.no_grad(): + # pyrefly: ignore [bad-argument-type, no-matching-overload] + inpt_old.set_(inpt_new) + + # Note [Ordering of resize_() and set_()] + # Importantly: the common usage in FSDP is that we have a dummy parameter + # that sees a set_() and **Then** a resize_(). + # We must put those mutations into the graph in the same order, + # Since running them in the opposite order will have different behavior. + # We fully ban resize_() followed by set_() for now, although in principal + # we could support this + if input_info.mutation_inductor_storage_resize: + if ( + mcs is None + or mcs.mc_inductor_storage_resized > applied_mcs.mc_inductor_storage_resized # type: ignore[union-attr] + ): + # resizing is not supported on subclasses (we error earlier if this happens) + from torch._subclasses.functional_tensor import FunctionalTensor + + if not isinstance(f_inpt, FunctionalTensor): + raise AssertionError(f"expected FunctionalTensor, got {type(f_inpt)}") + old_storage_size = torch._functionalize_get_storage_size( # type: ignore[attr-defined] + f_inpt.elem, before=True + ) + new_storage_size = torch._functionalize_get_storage_size( # type: ignore[attr-defined] + f_inpt.elem, before=False + ) + if old_storage_size != new_storage_size: + if not (old_storage_size == 0 or new_storage_size == 0): + raise AssertionError(f"""\ + Encosize during tracing on input {input_idx}. Old nbytes={old_storage_size}, new nbytes={new_storage_size} + We oresizing on graph inputs as long as the input either starts or ends with a storage size of 0 + (thee for FSDP)""") + torch.ops.inductor.resize_storage_bytes_(inpt_old, new_storage_size) + if new_storage_size == 0: + # Even if we marked the input as having a data mutation (thus needing a copy_()), + # We should **ignore** it if our input has no storage + # (this can happen if, e.g. we temporarily resize our input, copy data into it, + # and resize it back down to zero) + return + + # Optimization: if the copy_() is a no-op then don't include it in the graph. + # In theory inductor could optimize this away, however in fsdp, we end up with + # param.copy_(param), where param is a zero-storage-size tensor, + # and running this op in eager mode (using the aot_eager backend) will result in a segfault. + # So we may as well optimize it away here. + if inpt_old is inpt_new: + # (This check needs to be done after putting resize_() in the graph, + # since a resize_(0) doesn't actually change the FunctionalTensor's inner tensor) + return + # We found an input that had a (data-only) mutation. + # Since keep_input_mutations is set, we need to faithfully apply a copy_() + # so the compiler will see the input mutation in the graph. + + if not input_info.mutates_data: + return + + if mcs is not None and mcs.mc_data <= applied_mcs.mc_data: # type: ignore[union-attr] + return + + if input_info.mutations_hidden_from_autograd: + # Hidden from autograd = run under no_grad, **and** don't bump VC + # (although if the tensor was created in inference mode, it has no VC) + if inpt_old.is_inference(): + maybe_preserve_vc = nullcontext() + else: + maybe_preserve_vc = torch.autograd._unsafe_preserve_version_counter( + inpt_old # type: ignore[assignment] + ) + with torch.no_grad(), maybe_preserve_vc: + inpt_old.copy_(inpt_new) + elif input_info.mutations_under_no_grad_or_inference_mode: + # Under no_grad = run under no_grad (we still bump the VC though) + # (inference_mode will also bump the VC, as long as the tensor in question + # was created outside of inference_mode) + + with torch.no_grad(): + inpt_old.copy_(inpt_new) + else: + inpt_old.copy_(inpt_new) + + +# This creates the final function that we want to trace using make_fx(), +# in both aot_dispatch_autograd and aot_dispatch_base. +# Preconditions: +# - fn corresponds to the user's fw function +# - fn arguments have been flattened, duplicate arguments have been handled +# - In the returned function, the "primals" arguments *includes* synthetic bases. +# This function does the work of functionalizing the input function, +# and performing copy_() calls at the end of the function if `keep_input_mutations` is set. +# The function returned has signature that is either: +# (1) "traced_fn(primals: List[Any])" if trace_joint is False +# (2) "traced_fn(primals: List[Any], tangents: List[Any])" if trace_joint is True +# Returns a new (functionalized) function, and updated arguments to call it with. +def create_functionalized_fn( + fn: Callable[..., Any], + args: Any, + args_descs: Any, + *, + meta: ViewAndMutationMeta, + aot_config: AOTConfig, + trace_joint: bool, + joint_fn_handle: JointFnHandle | None = None, +) -> Any: + primals_after_forward = None + f_args_after_forward = None + f_args_mutation_counters_after_forward: list[MutationCounters] | None = None + inputs_mutated_in_graph = [ + info.mutation_type == MutationType.MUTATED_IN_GRAPH for info in meta.input_info + ] + has_input_mutated_in_graph = any(inputs_mutated_in_graph) + + @simple_wraps(fn) + def _functionalized_f_helper( + *args: list[FxValue], + ) -> tuple[tuple[list[FxValue], list[Tensor]], list[AOTOutput | None]]: + with maybe_enable_thunkify(): + # See Note [Disabling Functionalize TLS Above Python Functionalization] + disable_above = torch._C._ExcludeDispatchKeyGuard( + torch._C.DispatchKeySet(torch._C.DispatchKey.Functionalize) + ) + + with disable_above: + # The functionalization code here can potentially trigger traces + # into the graph, but we'd prefer to NOT do this, because if we + # trace them now, we will end up with FX nodes that don't have + # module stack annotations, which makes unflattener unhappy. + # Wrap inputs into functional wrappers + f_args = pytree.tree_map(to_fun, args) + + if trace_joint and has_input_mutated_in_graph and joint_fn_handle: + # TODO(ivankobzarev): Support fw and bw mutations for subclasses + def _post_forward(primals: Any) -> None: + nonlocal primals_after_forward + primals_after_forward = pytree.tree_map(from_fun, primals) + nonlocal f_args_after_forward + f_args_after_forward = f_args[0] + nonlocal f_args_mutation_counters_after_forward + f_args_mutation_counters_after_forward = [ + MutationCounters(-1, -1, -1) + if not inputs_mutated_in_graph[i] + else _get_mutation_counters(f_arg) + for i, f_arg in enumerate(f_args_after_forward) + ] + + joint_fn_handle.post_forward = _post_forward + + # Run the joint + f_outs, f_outs_descs = call_and_expect_output_descs(fn, f_args) + + if trace_joint: + # We support a limited amount of mutation of graph inputs during the backward pass. + # (This is used e.g. by Float8, which needs to update buffers during the backward pass) + # Here, we perform extra checks for primals that were mutated in the **backward** + # We're doing the checks here instead of doing them with the rest of the input mutation handling because: + # - We need to detect inputs that were mutated in the backward **separately** from mutations that happened + # during the forward, because the handling is different: some input mutations from the forward + # can be only handled in a fw-only runtime epilogue, and in theory if we wanted to handle those same + # types of mutations in the backward we would need a bw-only runtime epilogue. + # - We could in theory have our analysis pass differentiate mutations in the fw from mutations in + # the bw by running our analysis first on the fw-only graph, and then on the joint graph. This would + # require an extra round of tracing though, so it's more efficient to do in-line here. + if not ( + isinstance(args, tuple) + and len(args) == 2 + and isinstance(args[0], (list, tuple)) + ): + raise AssertionError( + f"expected args to be tuple of (list/tuple, ...), got {type(args)}" + ) + # Only look at mutations that happened to forward inputs (e.g. fw buffers that were saved for bw) + primals_before = args[0] + primals_after = pytree.tree_map(from_fun, f_args[0]) + for idx, (f_inpt, before, after, inpt_info) in enumerate( + zip(f_args[0], primals_before, primals_after, meta.input_info) + ): + # Store information about mutations in joint(for backward analysis) + joint_mutates_data = has_data_mutation(f_inpt) + + joint_mutates_metadata = has_metadata_mutation( + f_inpt, before, check_only_storage_mutation=False + ) + + # Ban metadata mutations on fw inputs during the bw + if not inpt_info.mutates_metadata: + if joint_mutates_metadata: + raise AssertionError( + "Found a graph input that had its metadata mutated in the backward. This is not supported" + ) + + # Ban storage resizing on fw inputs during the bw + if not inpt_info.mutation_inductor_storage_resize: + if was_inductor_storage_resized(f_inpt): + raise AssertionError( + "Found a graph input that had storage resizing in the backward. This is not supported" + ) + + # Allow data mutations on fw inputs during the bw, but only if they do not require grad + # So we can guarantee that we can keep the mutations in the graph + if ( + joint_mutates_data + and not inpt_info.mutates_data + and not inpt_info.mutates_storage_metadata + ): + # Not banning here mutations on inpt_info.requires_grad - + # we'll check at runtime and fail only when backward is under torch.is_grad_enabled (create_graph) + # Add node meta for copy_ for partitioner that this node should be in backward graph. + with ( + torch.fx.traceback.preserve_node_meta(), + set_partitioner_tag_must_be_in_backward(), + ): + # before and after should be tensors if we're calling copy_ on them + if not ( + isinstance(before, torch.Tensor) + and isinstance(after, torch.Tensor) + ): + raise AssertionError( + f"expected both before and after to be Tensors, got {type(before)} and {type(after)}" + ) + # no_grad prevents the FakeTensor's requires_grad from + # triggering check_inplace during tracing. The + # requires_grad case is checked at runtime instead + with torch.no_grad(): + before.copy_(after) + meta.indices_of_inputs_that_requires_grad_with_mutations_in_bw.append( + idx + ) + # Now that we covered mutations to *forward* inputs during the backward, + # we also need to cover mutations to *backward-only* inputs during the backward (e.g. mutation to a grad_out). + # Today, we will just error in all cases of this happening unless someone needs us to support it. + tangents_before = args[1] + tangents_after = pytree.tree_map(from_fun, f_args[1]) + for f_inpt, before, after in zip( + f_args[1], tangents_before, tangents_after + ): + if has_metadata_mutation( + f_inpt, before, check_only_storage_mutation=False + ): + raise AssertionError( + "Found an input to the backward that had metadata mutated " + "during the backward pass. This is not supported" + ) + if has_data_mutation(f_inpt): + can_be_in_graph = _check_if_mutation_can_be_in_graph( + keep_input_mutations=True, + mutates_data=True, + mutates_metadata=False, + mutations_hidden_from_autograd=are_all_mutations_hidden_from_autograd( + f_inpt + ), + mutations_under_no_grad_or_inference_mode=are_all_mutations_under_no_grad_or_inference_mode( + f_inpt + ), + mutates_storage_metadata=False, + mutation_inductor_storage_resize=was_inductor_storage_resized( + f_inpt + ), + requires_grad=f_inpt.requires_grad, + ) + if not can_be_in_graph: + raise AssertionError( + "a backward input that had data mutated in an autograd-aware way. This is not supported" + ) + # Perform the input mutation + with torch.fx.traceback.preserve_node_meta(): + # before and after should be tensors if we're calling copy_ on them + if not ( + isinstance(before, torch.Tensor) + and isinstance(after, torch.Tensor) + ): + raise AssertionError( + f"expected both before and after to be Tensors, got {type(before)} and {type(after)}" + ) + before.copy_(after) + + if aot_config.keep_inference_input_mutations: + # Note: This is a bit annoying. There's a layering issue here, where: + # (1) functionalization needs to operate on **synthetic base** inputs, before unpacking them into the "real" inputs. + # (2) For keep_input_mutations, we support tracing a call to copy_() directly on mutated inputs. + # However, we **only** want to support this for inputs that have data-only (and no metadata) mutations, + # because inductor (and backends in generally) would prefer not to see these (e.g. as_strided_(), resize_()). + # This makes it pretty difficult for this logic to operate on synthetic bases. + # (3) In addition, there are cases where it's significantly cheaper to perform the copy on the individual + # (unpacked) input aliases, instead of the synthetic base. + # Example case where (3) could be important: + # + # def f(x, y): + # x.mul_(2) + # y.mul_(3) + # return x, y + # a = torch.ones(1'000'000) + # x, y = out(a[0:9], a[1:10]) + # + # It would be much better to add copy_() calls into the graph for the two tiny slices, instead of materializing + # a giant "updated synthetic base" and copying into a's entire storage. + # + # For now, we are pessimistically not performing the optimization from (3); + # we will materialize an "updated" synthetic base, and copy it back to the synthetic input base. + # This allows us to factor aot autograd much more nicely, since only one area of the code needs to worry + # about synthetic bases. + + # Apply in graph forward mutations only in joint case. + # Note: Mutations of primals in forward AND backward. + # If we have mutations of the same input in forward and in backward, + # we can not fuse them into one copy_ node. As in this case partitioner will put it + # either in forward or in backward. This will lead to incorrect state + # after forward and before backward. + # We have to emit two copy_ nodes, marking with additional meta each node, + # if it must be in forward or backward. + # We memorize mutation counter of the inputs after forward. + # Based on this after joint graph we check if backward also mutated input or not. + # We emit copy_ only in the end of joint tracing, to provide invariant for joint + # graph passes, that our graph is functional, except only some number of copy_ nodes + # in the end. + mcs_applied: list[MutationCounters] = [MutationCounters(0, 0, 0)] * len( + meta.input_info + ) + if f_args_mutation_counters_after_forward is not None: + primals_before = args[0] + for idx, (f_inpt, before, after, inpt_info) in enumerate( + # pyrefly: ignore [no-matching-overload] + zip( + f_args_after_forward, # type: ignore[arg-type] + primals_before, # type: ignore[arg-type] + primals_after_forward, # type: ignore[arg-type] + meta.input_info, + ) + ): + if inpt_info.mutation_type != MutationType.MUTATED_IN_GRAPH: + continue + + mcs_after_forward = f_args_mutation_counters_after_forward[idx] + with ( + torch.fx.traceback.preserve_node_meta(), + set_partitioner_tag_must_be_in_forward(), + _proxy_tensor_disable_update_tensor_tracker(), + ): + apply_in_graph_mutations( + inpt_info, + # pyrefly: ignore [bad-argument-type] + before, + after, + f_inpt, + idx, + mcs_after_forward, + mcs_applied[idx], + ) + mcs_applied[idx] = mcs_after_forward + + for idx, (inpt_old, f_inpt) in enumerate( + zip(args, f_args) if not trace_joint else zip(args[0], f_args[0]) # type: ignore[arg-type] + ): + if not isinstance(f_inpt, torch.Tensor): + continue + if not is_fun(f_inpt): + raise AssertionError( + f"expected functional tensor, got {type(f_inpt)}" + ) + inpt_new = from_fun(f_inpt) + if ( + meta.input_info[idx].mutation_type + != MutationType.MUTATED_IN_GRAPH + ): + continue + mcs: MutationCounters | None = None + if f_args_mutation_counters_after_forward is not None: + # This could happen for subclasses tracing + # Subclasses support for mutations in fw and bw is TBD. + mcs = _get_mutation_counters(f_inpt) + if mcs == mcs_applied[idx]: + # No mutation in backward; mutation was already applied. + continue + + with ( + torch.fx.traceback.preserve_node_meta(), + set_partitioner_tag_must_be_in_backward(), + ): + apply_in_graph_mutations( + meta.input_info[idx], + # pyrefly: ignore[bad-argument-type] + inpt_old, + # pyrefly: ignore[bad-argument-type] + inpt_new, + f_inpt, + idx, + mcs, + mcs_applied[idx], + ) + + # When an output tensor is a functionalized mutated input, and we + # were able to move the mutation in to the graph then we can return + # the mutated input directly. This prevents duplicating the + # tensors contents. + flat_outs, outs_spec = pytree.tree_flatten(f_outs) + flat_outs = [from_fun(o) for o in flat_outs] + num_outs = len(meta.output_info) + + for i in range(num_outs): + info = meta.output_info[i] + if info.output_type != OutputType.is_input: + continue + + if info.base_idx is None: + raise AssertionError("info.base_idx must not be None") + if ( + meta.input_info[info.base_idx].mutation_type + == MutationType.MUTATED_IN_GRAPH + ): + fw_args = args[0] if trace_joint else args + flat_outs[i] = fw_args[info.base_idx] + return pytree.tree_unflatten(flat_outs, outs_spec), f_outs_descs + + return pytree.tree_map(from_fun, f_outs), f_outs_descs + + # Kinda annoying, but needed to make sure that the fx graph we trace out has "primals" + # and "tangents" as its input names (which are special-cased by the partitioner) + # TODO (tmanlaibaatar) revisit this if we ever need to turn on non-strict joint graph export + def joint_helper(primals: list[FxValue], tangents: list[FxValue]) -> Any: + return _functionalized_f_helper(primals, tangents) + + helper = joint_helper if trace_joint else _functionalized_f_helper + if config.functionalize_rng_ops: + # Setup the wrapper for functionalization of rng ops + helper, args, args_descs = create_functionalized_rng_ops_wrapper( + helper, args, args_descs, trace_joint + ) + + return helper, args, args_descs + + +def handle_effect_tokens_fn( + fn: Callable[..., Any], + args: Any, + args_descs: list[AOTInput], + *, + meta: ViewAndMutationMeta, + trace_joint: bool, +) -> Any: + num_tokens = len(meta.tokens) + + @simple_wraps(fn) + def inner_fn(*args: Any) -> Any: + # See Note [Disabling Functionalize TLS Above Python Functionalization] + disable_above = torch._C._ExcludeDispatchKeyGuard( + torch._C.DispatchKeySet(torch._C.DispatchKey.Functionalize) + ) + + with disable_above: + # See Note [Side-Effectful Tokens in AOTAutograd] + if trace_joint: + if not (isinstance(args, tuple) and isinstance(args[0], (list, tuple))): + raise AssertionError( + f"expected args to be tuple with first element as list/tuple, got {type(args)}" + ) + tokens = args[0][:num_tokens] + if not all(token.numel() == 0 for token in tokens): + raise AssertionError("all tokens must have numel() == 0") + args = (args[0][num_tokens:], *args[1:]) + else: + tokens = args[:num_tokens] + if not all(token.numel() == 0 for token in tokens): + raise AssertionError("all tokens must have numel() == 0") + args = args[num_tokens:] + + # Populate the current FunctionalTensorMode with the tokens per + # operator. See Note [FunctionalTensorMode is Stateful] + functional_tensor_mode = torch.utils._python_dispatch._detect_infra_mode( + torch._C._TorchDispatchModeKey.FUNCTIONAL + ) + if functional_tensor_mode is None: + raise AssertionError("functional_tensor_mode must not be None") + f_tokens = pytree.tree_map(to_fun, tokens) + for i, k in enumerate(meta.tokens.keys()): + functional_tensor_mode._tokens[k] = f_tokens[i] + + # Run the joint + outs, outs_descs = call_and_expect_output_descs(fn, args) + + # Return both the tokens and the outputs + # See Note [Side-Effectful Tokens in AOTAutograd] + if trace_joint: + if len(outs) != 2: + raise AssertionError( + f"expected len(outs) == 2 for joint trace, got {len(outs)}" + ) + if len(functional_tensor_mode._tokens_forward_output) != num_tokens: + raise AssertionError( + f"expected {num_tokens} forward output tokens, got {len(functional_tensor_mode._tokens_forward_output)}" + ) + fwd_out_tokens = functional_tensor_mode._tokens_forward_output.values() + + bwd_out_tokens = functional_tensor_mode._tokens.values() + + f_fwd_out_tokens = [from_fun(t) for t in fwd_out_tokens] + f_bwd_out_tokens = [from_fun(t) for t in bwd_out_tokens] + f_fwd_out_tokens_descs = [ + ForwardTokenAOTOutput(i) for i in range(len(fwd_out_tokens)) + ] + f_bwd_out_tokens_descs = [ + BackwardTokenAOTOutput(i) for i in range(len(bwd_out_tokens)) + ] + + meta.num_backward_tokens = len(bwd_out_tokens) + return ( + ((*f_fwd_out_tokens, *outs[0]), (*outs[1], *f_bwd_out_tokens)), + ( + (*f_fwd_out_tokens_descs, *outs_descs[0]), + (*outs_descs[1], *f_bwd_out_tokens_descs), + ), + ) + + out_tokens = [from_fun(t) for t in functional_tensor_mode._tokens.values()] + # TODO: can probably do a little more resolution here + out_tokens_descs = [ + ForwardTokenAOTOutput(i) + for i in range(len(functional_tensor_mode._tokens.values())) + ] + return ((*out_tokens, *outs), (*out_tokens_descs, *outs_descs)) + + # Additionally pass in tokens as inputs + # See Note [Side-Effectful Tokens in AOTAutograd] + additional_fwd_token_inputs = [torch.tensor([])] * num_tokens + additional_fwd_token_inputs_descs = [ + ForwardTokenAOTInput(i) for i in range(num_tokens) + ] + + if trace_joint: + args = ([*additional_fwd_token_inputs, *args[0]], *args[1:]) + args_descs = ( # type: ignore[assignment] + [*additional_fwd_token_inputs_descs, *args_descs[0]], # type: ignore[misc] + *args_descs[1:], + ) + else: + args = [*additional_fwd_token_inputs, *args] + args_descs = [*additional_fwd_token_inputs_descs, *args_descs] + + if num_tokens > 0: + meta.static_input_indices = [ + idx + num_tokens for idx in meta.static_input_indices + ] + return inner_fn, args, args_descs + + +# Given a function operating on Subclass -> Subclass, returns an function that operates on Tensor -> Tensor +# Also returns: +# - the new set of arguments to pass into this function (now that tensor subclasses have been eliminated) +# - the updated ViewAndMutationMeta for this dense -> dense function. +# The other important arguments are: +# - flat_fn_maybe_joint: when is_joint_structure=True, this is the joint fw-bw function. +# when is_joint_structure=False, this is just the forward function. +# - fw_only: this is *always* the forward-only function. +# Why do we need this? We need to collect updated ViewAndMutationMeta on our new dense -> dense functions. +# In particular, we need this to tell the partitioner how many dense forward outputs there are. +def aot_dispatch_subclass( + flat_fn_maybe_joint: JointTraceFn | TraceFn, + args: list[FxValue] | tuple[list[FxValue], list[FxValue]], + args_descs: list[AOTInput] | tuple[list[AOTInput], list[AOTInput]], + *, + is_joint_structure: bool, + meta: ViewAndMutationMeta, + fw_only: Callable[..., Any], +) -> SubclassTracingInfo: + # Skip logic if we don't need to trace through any subclasses + req_subclass_dispatch = requires_subclass_dispatch(args, meta) # type: ignore[arg-type] + if not req_subclass_dispatch: + return SubclassTracingInfo( + plain_tensor_trace_fn=flat_fn_maybe_joint, + plain_tensor_args=args, + plain_tensor_args_descs=args_descs, + maybe_subclass_meta=None, + ) + + # TODO: add subclass guards (later PR). + + # What's going on here? We need to compute subclass metadata about the outputs of the joint (grad_inputs). + # Annoying: we don't know the grad input metas until we're in the middle of tracing the joint, + # so we set it later, while we're tracing the joint (see inner_fn() below). + # Another option would be to run our run_functionalized_fw_and_collect_metadata() function + # directly on the joint, but this would hurt compile time (adding yet another pass through the joint). + subclass_meta = SubclassMeta() + + # NB: doesn't take descs, this is going from the NEW flat_args to the + # subclasses, we don't need to do bookkeeping here + def inner_fn(fn: Callable[..., Any], args: Any, *, use_trace_joint: bool) -> Any: + # Step 1: wrap tensor inputs into subclasses if necessary + all_args = wrap_tensor_subclasses_maybe_joint( + args, is_joint_structure=use_trace_joint, meta=meta + ) + + # Step 2: call the inner function, with our (maybe subclass) inputs + wrapped_outs, wrapped_outs_descs = call_and_expect_output_descs(fn, all_args) # type: ignore[arg-type] + + if use_trace_joint: + # See Note: [Computing Subclass Metadata about grad_inputs] + # We also stash subclass info on our grad_inputs, if we're tracing the joint. + nonlocal subclass_meta + if not (isinstance(wrapped_outs, tuple) and len(wrapped_outs) == 2): + raise AssertionError( + f"expected wrapped_outs to be tuple of length 2, got {type(wrapped_outs)}, {wrapped_outs_descs}" + ) + # Don't need fw outs since we already have subclass metadata on them + grad_inputs = wrapped_outs[1] + subclass_meta.grad_input_metas = create_subclass_meta(grad_inputs) + + # Add extra symints as outputs to the forward/backward graphs + # ignore nested ints here + forward_outs, forward_outs_descs = unwrap_tensor_subclasses( + wrapped_outs[0], + wrapped_outs_descs[0], + append_symints=True, + ) + # ignore nested ints here + backward_outs, backward_outs_descs = unwrap_tensor_subclasses( + wrapped_outs[1], + wrapped_outs_descs[1], + append_symints=True, + ) + return ( + (forward_outs, backward_outs), + (forward_outs_descs, backward_outs_descs), + ) + + # Step 3: Unwrap any subclass outputs back into dense tensors + return unwrap_tensor_subclasses( + wrapped_outs, wrapped_outs_descs, append_symints=True + ) + + def joint_fn( + primals: list[FxValue], tangents: list[FxValue] + ) -> tuple[ + tuple[list[FxValue], list[FxValue]], tuple[list[AOTOutput], list[AOTOutput]] + ]: + with maybe_enable_thunkify(): + return inner_fn( + flat_fn_maybe_joint, (primals, tangents), use_trace_joint=True + ) + + def fw_fn(*primals: FxValue) -> tuple[list[FxValue], list[AOTOutput]]: + with maybe_enable_thunkify(): + return inner_fn(flat_fn_maybe_joint, primals, use_trace_joint=False) + + def metadata_fn(*primals: FxValue) -> tuple[list[FxValue], list[AOTOutput]]: + @simple_wraps(fw_only) + def inner_fw_only(*args: Any) -> Any: + return call_and_expect_output_descs(fw_only, args) + + return inner_fn(inner_fw_only, primals, use_trace_joint=False) + + if is_joint_structure: + primals_wrapped: list[FxValue] = typing.cast(list[FxValue], args[0]) + primals_wrapped_descs: list[AOTInput] = typing.cast( + list[AOTInput], args_descs[0] + ) + tangents_wrapped: list[FxValue] = typing.cast(list[FxValue], args[1]) + tangents_wrapped_descs: list[AOTInput] = typing.cast( + list[AOTInput], args_descs[1] + ) + + # Add extra symints (size/strides) as input to the forward graph + primals_unwrapped_pair = unwrap_tensor_subclasses( + primals_wrapped, + primals_wrapped_descs, + append_symints=True, + ) + # We pass append_symints=False here because the partitioner will + # capture and add any extra argument. + tangents_unwrapped_pair = unwrap_tensor_subclasses( + tangents_wrapped, + tangents_wrapped_descs, + append_symints=False, + ) + + args_unwrapped = (primals_unwrapped_pair[0], tangents_unwrapped_pair[0]) + args_descs_unwrapped = (primals_unwrapped_pair[1], tangents_unwrapped_pair[1]) + remapped_static_indices = remap_unwrapped_subclass_arg_indices( + primals_wrapped, + meta.static_input_indices, # type: ignore[arg-type] + ) + + primals_unwrapped = args_unwrapped[0] # type: ignore[assignment] + primals_unwrapped_descs = args_descs_unwrapped[0] # type: ignore[assignment] + fn_to_trace = joint_fn # type: ignore[assignment] + else: + primals_wrapped: list[FxValue] = typing.cast(list[FxValue], args) + primals_wrapped_descs: list[AOTInput] = typing.cast(list[AOTInput], args_descs) + + args_unwrapped, args_descs_unwrapped = unwrap_tensor_subclasses( # type: ignore[assignment] + primals_wrapped, + primals_wrapped_descs, + append_symints=True, + ) + remapped_static_indices = remap_unwrapped_subclass_arg_indices( + primals_wrapped, + meta.static_input_indices, # type: ignore[arg-type] + ) + + primals_unwrapped = args_unwrapped # type: ignore[assignment] + primals_unwrapped_descs = args_descs_unwrapped # type: ignore[assignment] + fn_to_trace = fw_fn # type: ignore[assignment] + + # Note: [Partitioner handling for Subclasses, Part 1] + # The way the partitioner works is that: + # (1) we pass is a single graph containing the joint fw/bw, + # where the # of graph outputs corresponds to # fw_outputs + # grad_inputs + # (2) The partitioner accepts an arguments, num_fwd_outputs, + # and assumes that the first "num_fwd_outputs" graph outputs correspond + # to outputs of the forward graph. + # How do tensor subclasses enter the picture? + # the num_fwd_outputs in the final graph is actually non-trivial to compute, + # because it can be influenced by input mutations and intermediate bases. + # So we compute it by inspecting the current ViewAndMutationMeta object. + # However, the original ViewAndMutationMeta that we computed was created + # on the subclass -> subclass graph, + # which can have a different number of outputs than the dense -> dense graph. + # That's why we created a fresh metadata object on the dense -> dense function here, + # and plumb it back up to the partitioner. + # See Note: [Partitioner handling for Subclasses, Part 2] for more info. + meta_updated = run_functionalized_fw_and_collect_metadata( + without_output_descs(metadata_fn), + # pyrefly: ignore [bad-argument-type] + flat_args_descs=primals_unwrapped_descs, + static_input_indices=remapped_static_indices, + keep_input_mutations=meta.keep_input_mutations, + # pyrefly: ignore [not-iterable] + )(*primals_unwrapped) + + subclass_meta.fw_metadata = meta_updated + + return SubclassTracingInfo( + plain_tensor_trace_fn=fn_to_trace, + plain_tensor_args=args_unwrapped, + plain_tensor_args_descs=args_descs_unwrapped, + maybe_subclass_meta=subclass_meta, + ) + + +def create_functional_call( + mod: Any, + params_spec: Any, + params_len: int, + store_orig_mod: bool = False, + strict_out_tuple: bool = True, +) -> Callable[..., Any]: + # Redundant with dynamo, but worth having in case this gets invoked elsewhere. + # https://github.com/pytorch/pytorch/issues/103569 + + @simple_wraps(mod) + def functional_call(*args: Any, **kwargs: Any) -> Any: + flat_params = args[:params_len] + if isinstance(params_spec, TreeSpec): + params = pytree.tree_unflatten(flat_params, params_spec) + else: + if not isinstance(params_spec, list): + raise AssertionError( + f"expected params_spec to be a list, got {type(params_spec)}" + ) + params = dict(zip(params_spec, flat_params)) + with ( + stateless._reparametrize_module(mod, params), + maybe_disable_thunkify(), + ): + if isinstance(mod, torch.fx.GraphModule): + if kwargs: + # Handle **kwargs. FX only natively supports positional + # arguments (through placeholders). + arg_list = list(args[params_len:]) + arg_list.extend(list(kwargs.values())) + args = tuple(arg_list) + else: + args = args[params_len:] + + with fx_traceback.preserve_node_meta(), warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", "Anomaly Detection has been enabled." + ) + with torch.autograd.detect_anomaly(check_nan=False): + fake_mode = detect_fake_mode() + if fake_mode is None: + raise AssertionError("fake_mode must not be None") + fake_mode.epoch += 1 + out = PropagateUnbackedSymInts(mod).run(*args) + else: + out = mod(*args[params_len:], **kwargs) + + if strict_out_tuple and not isinstance(out, (tuple, list)): + raise RuntimeError( + "Graph output must be a (). This is so that we can avoid " + "pytree processing of the outputs. Please change the module to " + "have tuple outputs or use aot_module instead." + ) + return out + + # Note [Preserving the nn module stack metadata during export non-strict mode] + # This path is currently only used by the non-strict export flow, + # where we cannot rely on dynamo to preserve nn stack metadata in our captured graph. + # Instead, we stash the original user nn module here, and rely on `make_fx` to grab + # this stashed module and use it to track nn module stack metadata + if store_orig_mod and not hasattr(functional_call, "_orig_mod"): + functional_call._orig_mod = mod # type: ignore[attr-defined] + + return functional_call diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/graph_compile.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/graph_compile.py new file mode 100644 index 0000000000000000000000000000000000000000..10a6a429184483ee31905b7c11c974232d871a58 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/graph_compile.py @@ -0,0 +1,2641 @@ +""" +Functions in this module do most of the "work" of AOTAutograd. +An aot_dispatch_* function: +- Takes in the input flat_fn, flat_args, and some metadata +- Runs a set of pre compile wrappers (e.g. argument deduping) +- Runs the actual compiler +- Wraps the returned callable in a set of post compile wrappers +- Returns the wrapped callable and metadata. +""" + +import copy +import dataclasses +import itertools +import logging +import operator +import threading +import time +import traceback +from collections import defaultdict +from collections.abc import Callable, Generator +from contextlib import contextmanager, nullcontext +from typing import Any + +import torch +import torch.utils._pytree as pytree +import torch.utils.dlpack +from torch import Tensor +from torch._dynamo.utils import ( + CompileEventLogger, + detect_fake_mode, + dynamo_timed, + lazy_format_graph_code, +) +from torch._guards import CompileContext, TracingContext +from torch._library.fake_class_registry import FakeScriptObject +from torch._library.opaque_object import is_opaque_value +from torch._logging import getArtifactLogger, trace_structured +from torch._opaque_base import OpaqueBase +from torch._subclasses import FakeTensor +from torch._subclasses.meta_utils import is_sparse_any +from torch.fx.experimental._backward_state import BackwardState +from torch.fx.experimental.proxy_tensor import is_sym_node +from torch.fx.experimental.symbolic_shapes import fx_placeholder_vals, guard_or_true +from torch.fx.graph_module import GraphModule +from torch.fx.passes._tensorify_python_scalars import tensorify_python_scalars +from torch.multiprocessing.reductions import StorageWeakRef +from torch.types import py_sym_types +from torch.utils._python_dispatch import is_traceable_wrapper_subclass +from torchgen.utils import dataclass_repr + +from .. import config +from .aot_autograd_result import GenericAOTAutogradResult, serialize_graph_module +from .autograd_cache import ( + AOTAutogradCache, + should_bundle_autograd_cache, + should_use_remote_autograd_cache, +) +from .descriptors import AOTOutput, PlainAOTOutput +from .graph_capture import aot_dispatch_autograd_graph, aot_dispatch_base_graph +from .logging_utils import track_graph_compiling +from .runtime_wrappers import ( + AOTDedupeWrapper, + AOTDispatchAutograd, + AOTDispatchAutogradCompileSpec, + AOTDispatchSubclassWrapper, + AOTSyntheticBaseWrapper, + AutogradLazyBackwardCompileInfo, + CompilerWrapper, + DebugAssertWrapper, + EffectTokensWrapper, + FakifiedOutWrapper, + FunctionalizedRngRuntimeWrapper, + make_runtime_safe, + post_compile, + pre_compile, + RuntimeWrapper, + SerializableCompiledFunction, +) +from .schemas import ( + AOTConfig, + AOTGraphCapture, + AOTState, + FlatFn, + FxValue, + MutationType, + SubclassMeta, + ViewAndMutationMeta, +) +from .subclass_utils import compute_inner_mutated_inp_indices_from_subclass_meta +from .utils import ( + contain_metadata_mutation_ops, + get_cuda_generator_meta_val, + make_boxed_func, + simple_wraps, + strict_zip, + unlift_tokens, +) + + +def is_opaque_node(node: Any) -> bool: + """Check if a node contains an opaque or non-tensor value (e.g., ProcessGroup).""" + from torch._library.fake_class_registry import FakeScriptObject + + if not isinstance(node, torch.fx.Node): + return False + if "val" not in getattr(node, "meta", {}): + return False + val = node.meta["val"] + if is_opaque_value(val): + return True + if isinstance(val, (torch.ScriptObject, FakeScriptObject)): + return True + return False + + +_thread_local = threading.local() + + +@contextmanager +def maybe_skip_decompose(aot_config: AOTConfig) -> Generator[None, None, None]: + old_decomp = aot_config.decompositions + try: + if config.selective_decompose: + aot_config.decompositions = {} + yield + finally: + aot_config.decompositions = old_decomp + + +# Saved tensor hooks context +# Compiled saved tensor hooks are convenient way to inline some logic in the graphs +# for saved nodes from forward to backward. (E.g. activations quantization) +# In base implementation user does not have any additional information about saved value +# in the hook, except FakeTensor shape, dtype, device etc. +# _get_saved_tensor_hook_context gives additional graph information about that saved value, +# that can be used to make a decisions which pack/unpack to apply for particular saved value. +# This allows user to reuse saved tensors hooks api to apply selective pack/unpack in +# graph aware way. +# Alternative to this will be making user to write a custom pass that mucks with forward outputs, +# backward input metadata, which requires significantly more effort. +# +# As for now in context we expose forward graph, backward graph and current saved node, +# which contains node.meta with additional information about that fx.Node. +# Warning: This API may change without backward compatibility. +@contextmanager +def _saved_tensor_hook_context(state: dict[str, Any]) -> Generator[None, None, None]: + previous_state = getattr(_thread_local, "state", None) + try: + _thread_local.state = state + yield + finally: + # Clean up: restore previous state or remove attribute + if previous_state is not None: + _thread_local.state = previous_state + else: + if hasattr(_thread_local, "state"): + delattr(_thread_local, "state") + + +def _get_saved_tensor_hook_context() -> dict[str, Any] | None: + return getattr(_thread_local, "state", None) + + +zip = strict_zip + +log = logging.getLogger(__name__) +aot_joint_log = getArtifactLogger(__name__, "aot_joint_graph") +aot_graphs_log = getArtifactLogger(__name__, "aot_graphs") + +aten = torch.ops.aten + +# Returns a Callable and a ViewAndMutationMeta. +# Currently, only export needs the ViewAndMutationMeta after this function. +# TODO: Refactor this +DispatchReturn = tuple[Callable[..., Any], ViewAndMutationMeta] + + +def _create_wrappers_for_dispatch(needs_autograd: bool) -> list[CompilerWrapper]: + """ + Wrappers that run on every dispatch function + """ + return [AOTDedupeWrapper(), AOTSyntheticBaseWrapper(trace_joint=needs_autograd)] + + +def aot_stage1_graph_capture( + aot_state: AOTState, + orig_flat_fn: FlatFn, +) -> AOTGraphCapture: + # NB: flat_fn at this point coincides with the initial info from forward + # metadata collection returning a list[Tensor]. We are now going to + # augment the output to return a tuple[list[Tensor], list[AOTOutput]] and + # then preserve this convention through the rest of the passes. + + # TODO: We could test for consistency with fw_metadata, but this is not a + # big deal + @simple_wraps(orig_flat_fn) + def orig_flat_fn2(*args: FxValue) -> tuple[list[FxValue], list[AOTOutput]]: + out = orig_flat_fn(*args) + out_descs: list[AOTOutput] = type(out)( # type: ignore[assignment] + PlainAOTOutput(i) # type: ignore[misc] + for i in range(len(out)) # type: ignore[misc] + ) + return out, out_descs + + aot_config = aot_state.aot_config + + wrappers = _create_wrappers_for_dispatch(aot_state.needs_autograd) + flat_fn, aot_state.flat_args, aot_state.flat_args_descs, aot_state.fw_metadata = ( + pre_compile( + wrappers, + orig_flat_fn2, + aot_state.flat_args, + aot_state.flat_args_descs, + aot_config, + fw_metadata=aot_state.fw_metadata, + ) + ) + + # NB: This is currently only used for backwards, where fwd/bwd + # deterministic TLS can be different + aot_state.fw_metadata.deterministic = torch.are_deterministic_algorithms_enabled() + updated_flat_args: list[Any] | tuple[list[Any], list[Any]] + + with maybe_skip_decompose(aot_config): + # if config.selective_decompose, skip decomposition and apply selective_decompose + # after we get the joint graph. See [Note: Selective Decomposition] for details. + if aot_state.needs_autograd and not aot_config.pre_dispatch: + # FYI: this being moved to trigger in export is new, seems fine! + with dynamo_timed("aot_trace_joint_graph", log_pt2_compile_event=True): + ( + graph, + updated_flat_args, + updated_flat_args_descs, + maybe_subclass_meta, + ) = aot_dispatch_autograd_graph( + flat_fn, + aot_state.flat_args, + aot_state.flat_args_descs, + aot_config, + fw_metadata=aot_state.fw_metadata, + ) + else: + graph, updated_flat_args, updated_flat_args_descs, maybe_subclass_meta = ( + aot_dispatch_base_graph( + flat_fn, + aot_state.flat_args, + aot_state.flat_args_descs, + aot_config, + fw_metadata=aot_state.fw_metadata, + ) + ) + # Apply AC rematerialization to forward+loss+bwd graph + if torch._functorch.config.remat_using_tags_for_fwd_loss_bwd_graph: + from torch._functorch._activation_checkpointing.remat_using_tags_for_fwd_loss_bwd_graph_pass import ( + remat_using_tags_for_fwd_loss_bwd_graph, + ) + + graph = remat_using_tags_for_fwd_loss_bwd_graph(graph) + + if config.selective_decompose: + from torch.fx.experimental.proxy_tensor import selective_decompose + from torch.fx.passes.regional_inductor import _needs_inductor_compile + + graph = selective_decompose( + graph, + *updated_flat_args, + decomposition=aot_config.decompositions, + should_decompose=_needs_inductor_compile, + trace_joint_graph=aot_state.needs_autograd and not aot_config.pre_dispatch, + ) + + return AOTGraphCapture( + wrappers=wrappers, + graph_module=graph, + updated_flat_args=updated_flat_args, + updated_flat_args_descs=updated_flat_args_descs, + maybe_subclass_meta=maybe_subclass_meta, + ) + + +def aot_stage2_export( + aot_state: AOTState, aot_graph_capture: AOTGraphCapture +) -> DispatchReturn: + graph = aot_graph_capture.graph_module + aot_config = aot_state.aot_config + wrappers = aot_graph_capture.wrappers + + CompileEventLogger.try_add_pt2_compile("backend_compile", dispatch_mode="export") + + # NB: the wrappers that run in pre_compile for export are + # either a no-op, because they're not needed, or will raise a runtime error, + # since they don't support export. + # We still run these wrappers to make sure that they're not needed pre compile, + # but we technically don't need to run them post compile at all here. + compiled_fn, aot_state.fw_metadata = post_compile( + wrappers, + graph, # pyrefly: ignore [bad-argument-type] + aot_config, + runtime_metadata=aot_state.fw_metadata, + ) + + # Therefore, since no wrapperes run, we don't get back a callable - we get back the raw fx graph + # (either a joint or an inference-only graph) + if not isinstance(compiled_fn, torch.fx.GraphModule): + raise AssertionError( + f"expected compiled_fn to be GraphModule, got {type(compiled_fn)}" + ) + return compiled_fn, aot_state.fw_metadata + + +def sanitize_aot_config(input: AOTConfig) -> AOTConfig: + return AOTConfig( + fw_compiler=None, + bw_compiler=None, + partition_fn=None, + decompositions={}, + inference_compiler=None, + num_params_buffers=input.num_params_buffers, + aot_id=input.aot_id, + keep_inference_input_mutations=input.keep_inference_input_mutations, + is_export=input.is_export, + no_tangents=input.no_tangents, + aot_autograd_arg_pos_to_source=input.aot_autograd_arg_pos_to_source, + dynamic_shapes=input.dynamic_shapes, + enable_log=input.enable_log, + static_input_indices=input.static_input_indices, + pre_dispatch=input.pre_dispatch, + cache_info=None, + precompile_backend_id=input.precompile_backend_id, + ) + + +def _get_inner_meta( + maybe_subclass_meta: SubclassMeta | None, + fw_metadata: ViewAndMutationMeta, +) -> ViewAndMutationMeta: + """ + Util to get view and mutation metadata. + """ + return ( + fw_metadata if maybe_subclass_meta is None else maybe_subclass_meta.fw_metadata + ) + + +def _apply_tensorify_python_scalars(module: torch.fx.GraphModule) -> None: + """ + Util to apply tensorify_python_scalars. + """ + # TODO(anijain2305) - Add tensorify_python_scalars to the HOP graph passes. + fake_mode = detect_fake_mode() + if fake_mode is not None and fake_mode.shape_env is not None: + tensorify_python_scalars(module, fake_mode.shape_env, fake_mode) + + +def aot_stage2_compile( + aot_state: AOTState, + aot_graph_capture: AOTGraphCapture, + # pyrefly: ignore [implicit-any] + partition_fn: Callable, + # pyrefly: ignore [implicit-any] + fw_compiler: Callable, + # pyrefly: ignore [implicit-any] + bw_compiler: Callable | None = None, + # pyrefly: ignore [implicit-any] + inference_compiler: Callable | None = None, +) -> DispatchReturn: + if bw_compiler is None: + bw_compiler = fw_compiler + if inference_compiler is None: + inference_compiler = fw_compiler + # Update the AOTState with the provided compilers + aot_state.aot_config.partition_fn = partition_fn + aot_state.aot_config.fw_compiler = fw_compiler + aot_state.aot_config.bw_compiler = bw_compiler + aot_state.aot_config.inference_compiler = inference_compiler + + if aot_state.needs_autograd and not aot_state.aot_config.pre_dispatch: + return aot_stage2_autograd(aot_state, aot_graph_capture) + else: + return aot_stage2_inference(aot_state, aot_graph_capture) + + +def _log_inference_graph( + fw_module: torch.fx.GraphModule, + aot_config: AOTConfig, +) -> str | None: + """ + Log the inference graph to the structured logger. + Return a str representation of the graph. + """ + if aot_config.enable_log: + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "torch._functorch.config", + "encoding": "string", + }, + payload_fn=lambda: torch._functorch.config.get_serializable_config_copy(), + ) + + # Save the forward_graph_str right after aot_dispatch_base_graph, + # to save in the cache + aot_forward_graph_str = None + if aot_config.cache_info is not None: + aot_forward_graph_str = fw_module.print_readable( + print_output=False, + include_stride=True, + include_device=True, + fast_sympy_print=True, + expanded_def=True, + ) + + return aot_forward_graph_str + + +def _aot_stage2b_inference_compile( + fw_module: torch.fx.GraphModule, + updated_flat_args: list[Any], + maybe_subclass_meta: SubclassMeta | None, + fw_metadata: ViewAndMutationMeta, + aot_config: AOTConfig, + # pyrefly: ignore [implicit-any] +) -> Callable: + return _aot_stage2b_compile_forward_or_inference( + fw_module, + updated_flat_args, # type: ignore[arg-type] + maybe_subclass_meta, + fw_metadata, + aot_config, + is_inference=True, + )[1] + + +def aot_stage2_inference( + aot_state: AOTState, + aot_graph_capture: AOTGraphCapture, +) -> DispatchReturn: + """ + Handles functions that don't need autograd. Runs wrappers and compiles with fw_compiler. + """ + + aot_config = aot_state.aot_config + fw_metadata = aot_state.fw_metadata + fw_module = aot_graph_capture.graph_module + wrappers = aot_graph_capture.wrappers + updated_flat_args = aot_graph_capture.updated_flat_args + maybe_subclass_meta = aot_graph_capture.maybe_subclass_meta + + CompileEventLogger.try_add_pt2_compile("backend_compile", dispatch_mode="inference") + aot_forward_graph_str = _log_inference_graph(fw_module, aot_config) + + if not isinstance(fw_module, GraphModule): + raise AssertionError( + f"expected fw_module to be GraphModule, got {type(fw_module)}" + ) + _apply_tensorify_python_scalars(fw_module) + + compiled_fw = _aot_stage2b_inference_compile( + fw_module, + updated_flat_args, # type: ignore[arg-type] + maybe_subclass_meta, + fw_metadata, + aot_config, + ) + + entry = _cache_inference_info( + aot_config, + fw_metadata, + maybe_subclass_meta, + compiled_fw, + aot_forward_graph_str, + wrappers, + ) + + return _aot_stage2c_make_inference_function( + aot_config, + fw_metadata, + compiled_fw, + wrappers, + entry, + ) + + +def _cache_inference_info( + aot_config: AOTConfig, + fw_metadata: ViewAndMutationMeta, + maybe_subclass_meta: SubclassMeta | None, + compiled_fw: Callable[..., Any], + aot_forward_graph_str: str | None, + wrappers: list[CompilerWrapper], +) -> GenericAOTAutogradResult[Any, Any] | None: + make_runtime_safe(fw_metadata, maybe_subclass_meta) + + cache_info = aot_config.cache_info + + def should_save_cache() -> bool: + if should_bundle_autograd_cache(): + return True + else: + return hasattr(compiled_fw, "_fx_graph_cache_key") + + entry: GenericAOTAutogradResult[Any, Any] | None = None + if cache_info is not None and should_save_cache(): + time_taken_ns = time.time_ns() - cache_info.start_time_ns + guards_expr = AOTAutogradCache.generate_guards_expression(cache_info) + entry = AOTAutogradCache.make_entry( + compiled_fw_func=compiled_fw, # type: ignore[arg-type] + compiled_bw_func=None, + aot_joint_graph_str=None, + aot_forward_graph_str=aot_forward_graph_str, + aot_backward_graph_str=None, + runtime_metadata=fw_metadata, + dispatch_wrappers=wrappers, + maybe_subclass_meta=maybe_subclass_meta, + num_fw_outs_saved_for_bw=None, + indices_of_inps_to_detach=[], + forward_time_taken_ns=time_taken_ns, + backward_time_taken_ns=0, + sanitized_aot_config=sanitize_aot_config(aot_config), + guards_expr=guards_expr, + backward_state_indices=None, + num_symints_saved_for_bw=None, + serialized_bw_module=None, + ) + AOTAutogradCache.save( + cache_info.cache_key, + entry, + remote=should_use_remote_autograd_cache(), + ) + + return entry + + +def _aot_stage2c_make_inference_function( + aot_config: AOTConfig, + fw_metadata: ViewAndMutationMeta, + compiled_fw: Callable[..., Any], + wrappers: list[CompilerWrapper], + entry: GenericAOTAutogradResult[Any, Any] | None, +) -> DispatchReturn: + if entry is not None: + compiled_fw = SerializableCompiledFunction(compiled_fw, lambda: entry) + + disable_amp = torch._C._is_any_autocast_enabled() + compiled_fn = RuntimeWrapper( + indices_of_inps_to_detach=[], + trace_joint=False, + disable_amp=disable_amp, + ).post_compile( + compiled_fw, + aot_config, + runtime_metadata=fw_metadata, + ) + + compiled_fn = post_compile( + wrappers, compiled_fn, aot_config, runtime_metadata=fw_metadata + ) + return compiled_fn + + +def collect_fw_donated_buffer_idxs( + fw_ins: list[FakeTensor | None], + user_fw_outs: list[FakeTensor | None], + bw_outs: list[FakeTensor | None], + saved_tensors: list[FakeTensor | None], +) -> list[int]: + """ + Checks if the saved tensors are donated buffers, which means a saved tensor is not + an alias of any tensors in fw_ins, user_fw_outs, and bw_outs. + """ + + storage_refs = set() + + for t in itertools.chain(fw_ins, user_fw_outs, bw_outs): + # Only access storage if a tensor has storage (not sparse) + if t is not None and isinstance(t, FakeTensor) and not is_sparse_any(t): + storage_refs.add(StorageWeakRef(t.untyped_storage())) + + num_saved_tensor = len(saved_tensors) + donated_buffer_idxs = [] + for i in range(num_saved_tensor): + t = saved_tensors[i] + if ( + t is not None + and isinstance(t, FakeTensor) + and not is_sparse_any(t) + and StorageWeakRef(t.untyped_storage()) not in storage_refs + ): + donated_buffer_idxs.append(i) + + return donated_buffer_idxs + + +def collect_bw_donated_buffer_idxs( + fw_module: torch.fx.GraphModule, + bw_module: torch.fx.GraphModule, + fw_metadata: ViewAndMutationMeta, +) -> list[int]: + """ + Collects backward donated buffer indexes from fw_module and bw_module. + """ + + # [Note: Metadata mutation in proxy tracing] + # node.meta["val"] is a snapshot of the tensor value when tracing a graph, + # instead of the final state after the graph has run. node.meta["val"] is + # not updated even if later there is a metadata mutation op. + # See: https://github.com/pytorch/pytorch/pull/141308#issuecomment-2495798947 + # + # Currently, metadata mutation op happens only for sacrificial parameter + # specifically the `set_` op. This motivates banning metadata mutation from + # proxy tracing. + # + # Since node.meta["val"] is used to detect donated buffer, we return an empty + # list if there exists metadata mutation op. + if contain_metadata_mutation_ops(fw_module) or contain_metadata_mutation_ops( + bw_module + ): + return [] + + fw_ins = fw_module.graph.find_nodes(op="placeholder") + bw_outs = next(reversed(bw_module.graph.find_nodes(op="output"))).args[0] + fw_outs = next(reversed(fw_module.graph.find_nodes(op="output"))).args[0] + + fw_ins = [ + n.meta["val"] if (hasattr(n, "meta") and "val" in n.meta) else None + for n in fw_ins + ] + fw_outs = [ + n.meta["val"] if (hasattr(n, "meta") and "val" in n.meta) else None + for n in fw_outs + ] + bw_outs = [ + n.meta["val"] if (hasattr(n, "meta") and "val" in n.meta) else None + for n in bw_outs + ] + + user_fw_outs = fw_outs[: fw_metadata.num_forward] + saved_tensors = fw_outs[fw_metadata.tensors_saved_for_backwards_slice] + + fw_donated_buffer = collect_fw_donated_buffer_idxs( + fw_ins, + user_fw_outs, + bw_outs, + saved_tensors, + ) + + if fw_metadata.num_symints_saved_for_bw is None: + raise AssertionError("fw_metadata.num_symints_saved_for_bw must not be None") + return [fw_metadata.num_symints_saved_for_bw + i for i in fw_donated_buffer] + + +@dataclasses.dataclass +class InvokeSubgraphHopGraphs: + """ + A data structure to hold all the information needed to partition the + `joint_hop_gm` and joint graph and the restitch the `new_fw_hop_gm` and + `new_bw_hop_gm` into the bigger `joint_gm`. + """ + + # To avoid re-partitioning subgraphs + partitioning_done: bool = False + old_num_fw_outputs: int | None = None + old_num_fw_inputs: int | None = None + + new_fw_hop_gm: torch.fx.GraphModule | None = None + new_bw_hop_gm: torch.fx.GraphModule | None = None + new_num_sym_nodes: int | None = None + new_num_saved_nodes: int | None = None + + +def prepare_for_partitioner( + mod: torch.fx.GraphModule, num_primals: int, num_fw_outputs: int +) -> torch.fx.GraphModule: + # min-cut partitioner requires the placeholders to have primals and + # tangents string in the node.name. The signature of the joint graph is + # (*primals, *tangents) + + # We also have to update the output signature which is right now + # (*grads, *fw_outs) and we have to change to (*fw_outs, *grads) for the + # partitioner to work. + new_graph = torch.fx.Graph() + env = {} + + primals_counter = itertools.count(0) + tangents_counter = itertools.count(0) + + for idx, node in enumerate(mod.graph.nodes): + if node.op == "placeholder": + if idx < num_primals: + env[node] = new_graph.placeholder(f"primals_{next(primals_counter)}") + else: + env[node] = new_graph.placeholder(f"tangents_{next(tangents_counter)}") + env[node].meta = copy.copy(node.meta) + elif node.op == "output": + # Reverse the (*grads, *fw_outs) to (*fw_outs, *grads) + # The reason for having the reversed signature in the first + # place is to simplify step 3. + old_outputs = node.args[0] + new_outputs = ( + *old_outputs[-num_fw_outputs:], + *old_outputs[:-num_fw_outputs], + ) + new_outputs = [env[n] if n else None for n in new_outputs] + new_graph.output(tuple(new_outputs)) + else: + env[node] = new_graph.node_copy(node, lambda n: env[n]) + env[node].meta = copy.copy(node.meta) + + new_graph.lint() + + out = torch.fx.GraphModule(mod, new_graph) + return out + + +def _get_partition_fn( + fw_hop_node: torch.fx.Node, aot_config: AOTConfig +) -> tuple[bool, Callable[..., tuple[torch.fx.GraphModule, torch.fx.GraphModule]]]: + """ + Return either the default `partition_fn` in aot_config or a HOP specific partition + function. + + If a HOP specific partition function is returned, used_hop_custom_partition is True. + + See Note [InvokeSubgraphHOP Partitioner] + """ + used_hop_custom_partition = False + if aot_config.partition_fn is None: + raise AssertionError("aot_config.partition_fn must not be None") + partition_fn: Callable[..., tuple[torch.fx.GraphModule, torch.fx.GraphModule]] = ( + aot_config.partition_fn + ) + if ( + fw_hop_node.target == torch._higher_order_ops.invoke_subgraph + and "custom" in fw_hop_node.meta + and "nested_region_config" in fw_hop_node.meta["custom"] + ): + hop_partition_fn = fw_hop_node.meta["custom"][ + "nested_region_config" + ].partitioner + if hop_partition_fn is None: + # inherit the parent paritioner + return used_hop_custom_partition, partition_fn + + if callable(hop_partition_fn): + partition_fn = hop_partition_fn # pyrefly: ignore [bad-assignment] + used_hop_custom_partition = True + else: + if not isinstance(hop_partition_fn, str): + raise AssertionError( + f"expected hop_partition_fn to be str, got {type(hop_partition_fn)}" + ) + match hop_partition_fn: + case "default_partition": + partition_fn = torch._functorch.partitioners.default_partition + case "min_cut_rematerialization_partition": + partition_fn = torch._functorch.partitioners.min_cut_rematerialization_partition + case _: + raise ValueError( + f"Unknown HOP partitioner config: {hop_partition_fn}" + ) + return used_hop_custom_partition, partition_fn + + +def run_joint_graph_passes_on_hops( + joint_gm: torch.fx.GraphModule, + joint_inputs: Any, + aot_config: AOTConfig, +) -> torch.fx.GraphModule: + """ + This pass runs the joint graph passes on the HOP graph. In torch.compile, we + typically have many passes which work on the joint graph and then end with a + partitioner. + + + The partitioner part is quite mechanical to handle. HOP have their own + forward and backward graph. The process can be broken into following steps + + 1) Get a `joint_hop_gm` from the `fw_hop_gm` and `bw_hop_gm` + 2) Run joint graph passes on the `joint_hop_gm` to get `new_fw_hop_gm` and `new_bw_hop_gm` + 3) Stitch the `new_fw_hop_gm` and `new_bw_hop_gm` back into the `joint_gm`. + + The terminology used in the code is + `joint_graph/joint_gm` : Refers to the main graph. This may contain many HOPs which have their own `hop_graph` + `fw_hop_graph/fw_hop_gm` : Refers to the forward graph associated with a HOP. + `bw_hop_graph/bw_hop_gm` : Refers to the backward graph associated with a HOP. + `joint_hop_graph/joint_hop_gm` : Refers to the subgraph associated with the HOP like invoke_subgraph. + `new_fw_hop_graph/new_fw_hop_gm` : Refers to the forward graph after partitioning is applied to `joint_hop_gm`. + `new_bw_hop_graph/new_bw_hop_gm` : Refers to the backward graph after partitioning is applied to `joint_hop_gm`. + + NB: This pass works for invoke_subgraph today because we took extra care in + the Autograd.Dispatch key of invoke_subgraph to vastly simplify Step 1. + """ + from torch._higher_order_ops import invoke_subgraph + + def num_outputs(mod: torch.fx.GraphModule) -> int: + return len(mod.graph.find_nodes(op="output")[0].args[0]) + + def num_inputs(mod: torch.fx.GraphModule) -> int: + return len(mod.graph.find_nodes(op="placeholder")) + + new_hop_graphs: dict[str, InvokeSubgraphHopGraphs] = defaultdict( + lambda: InvokeSubgraphHopGraphs() + ) + + # Step 1 - Get a `joint_hop_gm` from the `fw_hop_gm` and `bw_hop_gm` This is + # easy to do for `invoke_subgraph` HOP. During the Autograd dispatch key + # tracing, we have put the joint_hop_graph in the backward hop graph itself. + # So to recover the joint_hop_gm, we just have to look at the backward + # HOP graphs. + # So we will merge step 1 and step 2 in this next section + + # Save the fw and bwd hop nodes. We will later in-place modify the graph + # using these nodes. + # pyrefly: ignore [implicit-any] + fw_hop_nodes = [] + # pyrefly: ignore [implicit-any] + bw_hop_nodes = [] + for node in joint_gm.graph.nodes: + if ( + node.op == "call_function" + and node.target is invoke_subgraph + and isinstance(node.args[1], str) + ): + if node.args[1].startswith("fw"): + fw_hop_nodes.append(node) + elif node.args[1].startswith("bw"): + bw_hop_nodes.append(node) + + if not bw_hop_nodes: + return joint_gm + + if len(fw_hop_nodes) != len(bw_hop_nodes): + raise AssertionError( + f"expected len(fw_hop_nodes) == len(bw_hop_nodes), " + f"got {len(fw_hop_nodes)} != {len(bw_hop_nodes)}" + ) + + # Create a bw to hop node mapping. This helps us in identifying the bw and + # fw subgraph pairs without relying on the identifier. This is important + # because we can have different subgraphs for bwd for same subgraph in the + # fwd because of differing strides in the backward. + bw_to_fw_hop_node = dict(zip(list(reversed(bw_hop_nodes)), fw_hop_nodes)) + + for node in bw_hop_nodes: + identifier = node.args[1].removeprefix("bw") + + # If partitioning already done for this identifier, skip. This saves + # redundant joint graph passes for same subgraphs. + if new_hop_graphs[identifier].partitioning_done: + continue + + # Collect some information from the forward hop graph + fw_hop_node = bw_to_fw_hop_node[node] + fw_hop_gm = getattr(joint_gm, fw_hop_node.args[0].target) + if not isinstance(fw_hop_gm, torch.fx.GraphModule): + raise AssertionError( + f"expected fw_hop_gm to be GraphModule, got {type(fw_hop_gm)}" + ) + num_fw_inputs = num_inputs(fw_hop_gm) + num_fw_outputs = num_outputs(fw_hop_gm) + new_hop_graphs[identifier].old_num_fw_inputs = num_fw_inputs + new_hop_graphs[identifier].old_num_fw_outputs = num_fw_outputs + + # Step 1) - Get the `joint_hop_gm`. As mentioned earlier, the + # backward graph is the joint graph. + joint_hop_gm = getattr(joint_gm, node.args[0].target) + if not isinstance(joint_hop_gm, torch.fx.GraphModule): + raise AssertionError( + f"expected joint_hop_gm to be GraphModule, got {type(joint_hop_gm)}" + ) + + # Prepare the graph for the partitioner + joint_hop_gm = prepare_for_partitioner( + joint_hop_gm, num_fw_inputs, num_fw_outputs + ) + + # TODO: invoke_subgraph should track which of its inputs static indices + # so it can propagate them to the partitioner (and use in cudagraphs) + static_lifetime_input_indices: list[int] = [] + + used_hop_custom_partition, partition_fn = _get_partition_fn( + fw_hop_node, aot_config + ) + + # Step 2) and 3) - Run joint graph passes and partitioner + try: + new_fw_hop_gm, new_bw_hop_gm = partition_fn( + joint_hop_gm, + [], + num_fwd_outputs=num_fw_outputs, + static_lifetime_input_indices=static_lifetime_input_indices, + ) + except Exception as e: + if used_hop_custom_partition: + raise RuntimeError( + f"Error in custom partition function for invoke_subgraph node {fw_hop_node.name}: {e}" + ) from e + else: + raise + + # Save the new forward and backward graph modules + new_hop_graphs[identifier].new_fw_hop_gm = new_fw_hop_gm + new_hop_graphs[identifier].new_bw_hop_gm = new_bw_hop_gm + + # Save the number of symints and saved tensors + new_fw_out_nodes = new_fw_hop_gm.graph.find_nodes(op="output")[0].args[0] + extra_outputs = new_fw_out_nodes[num_fw_outputs:] + symint_outputs = [n for n in extra_outputs if is_sym_node(n)] + + new_hop_graphs[identifier].new_num_sym_nodes = len(symint_outputs) + new_hop_graphs[identifier].new_num_saved_nodes = len(extra_outputs) - len( + symint_outputs + ) + + new_hop_graphs[identifier].partitioning_done = True + + # Step 3) Restitch the new fw and bw graphs back into the main graph. + # + # This is a very mechanical process. There are a quite a few pieces that we + # need to connect together to make it work. Lets try to understand the + # problem statement first. + # + # For the forward graph, the signature of the old_fw_hop_gm is + # inputs - (*primals) + # outputs - (*fw_outs) + # Now the signature of the new_fw_hop_gm is + # inputs - (*primals) -- This is same + # outputs - (*fw_outs, *saved_tensors) - This is different + # At a high level, this is an easy transformation, in the new graph we just + # have to replace the old_fw_hop_gm with the new_fw_hop_gm. Everything else + # falls into place, because the input signature (i.e. args) is same. And + # even though output signature is different, fw_outs are still at the same + # indexes as before. So the forward of the `joint_gm` works nicely. + # + # Now, lets look at the backward hop graph. Old signature + # inputs - (*primals, *tangents) + # outputs - (*grad_outs, *fw_outs) + # New signature + # inputs - (*saved_tensors, *tangents) -- Different + # outputs - (*grad_outs) -- Different + # Here both input and output signature change. The output signature handling + # is quite easy because the grads_out are sitting at the right place, so we + # dont have to do anything. + # + # For the input signature, we have to collect the saved tensors from the + # corresponding forward graph output. We collect all saved_tensors when we + # see the forward graph, and save it into a map and then later use it during + # the backward. + + # The stack of fw_nodes for invoke_subgraph HOP. There is an implicit + # assumption about the graph structure, i.e., if we have hop1, hop2, hop3, + # ... in the forward part of the joint graph, we will have .., hop3, hop2, + # hop1 order for the backward. This structure allows us to just use a stack + # to collect all the information that we need to pass from the forward hop + # node to the corresponding backward node. + + already_added_new_hop_mods = set() + + def add_new_hop_gm(new_subgraph_mod: torch.fx.GraphModule, name: str) -> str: + new_subgraph_attr_name = f"partitioned_{name}" + if new_subgraph_attr_name in already_added_new_hop_mods: + return new_subgraph_attr_name + + joint_gm.register_module(new_subgraph_attr_name, new_subgraph_mod) + already_added_new_hop_mods.add(new_subgraph_attr_name) + return new_subgraph_attr_name + + def propagate_meta_info( + new_hop_gm: torch.fx.GraphModule, + new_call_function_node: torch.fx.Node, + old_call_function_node: torch.fx.Node, + ) -> None: + # Copy all the fields from the old call_function node. And then override + # the `val` meta field with the outputs of new_hop_gm. + new_call_function_node.meta = copy.copy(old_call_function_node.meta) + + output = new_hop_gm.graph.find_nodes(op="output")[0] + out_example_vals = [n.meta["val"] if n else None for n in output.args[0]] + new_call_function_node.meta["val"] = tuple(out_example_vals) + + for bw_node in reversed(bw_hop_nodes): + identifier = bw_node.args[1].removeprefix("bw") + + # Make changes to the corresponding fw and bw node pair simultaneously. + # The removes the need of any bookkeeping. + + # Fw node changes + # Insert the new_fw_hop_gm. This is straightforward. Get the + # new_fw_hop_gm, insert the hop_gm as a get_attr fw_node, and then + # add a call_function fw_node. Additionally, also use getitem + # call_functions to collect the saved_tensor nodes + + fw_node = bw_to_fw_hop_node[bw_node] + new_fw_hop_gm = new_hop_graphs[identifier].new_fw_hop_gm + if new_fw_hop_gm is None: + raise AssertionError( + f"new_fw_hop_gm for identifier {identifier} must not be None" + ) + + old_num_fw_outputs = new_hop_graphs[identifier].old_num_fw_outputs + new_num_sym_nodes = new_hop_graphs[identifier].new_num_sym_nodes + new_num_saved_nodes = new_hop_graphs[identifier].new_num_saved_nodes + if old_num_fw_outputs is None: + raise AssertionError( + f"old_num_fw_outputs for identifier {identifier} must not be None" + ) + if new_num_sym_nodes is None: + raise AssertionError( + f"new_num_sym_nodes for identifier {identifier} must not be None" + ) + if new_num_saved_nodes is None: + raise AssertionError( + f"new_num_saved_nodes for identifier {identifier} must not be None" + ) + total_outputs = old_num_fw_outputs + new_num_saved_nodes + new_num_sym_nodes + + extra_fw_outputs = [] + + # Insert the new_fw_hop_gm into the joint_gm + with joint_gm.graph.inserting_after(fw_node): + new_fw_mod_attr_name = add_new_hop_gm(new_fw_hop_gm, f"fw{identifier}") + new_fw_mod_attr = joint_gm.graph.get_attr(new_fw_mod_attr_name) + new_fw_mod_attr.meta = copy.copy(fw_node.args[0].meta) + + # new_hop_fw_gm output signature is (*fw_outs, *saved_tensors) + with joint_gm.graph.inserting_after(new_fw_mod_attr): + new_fw_node = joint_gm.graph.call_function( + the_function=invoke_subgraph, + args=( + new_fw_mod_attr, + new_fw_mod_attr_name, + *fw_node.args[2:], + ), + ) + propagate_meta_info(new_fw_hop_gm, new_fw_node, fw_node) + + # old_num_fw_outputs = (*fw_outs) + # new_num_fw_outputs = (*fw_outs, *saved_tensors, *sym_nodes) + with joint_gm.graph.inserting_after(new_fw_node): + for fw_out_idx in range(old_num_fw_outputs, total_outputs): + saved_tensor_node = joint_gm.graph.call_function( + the_function=operator.getitem, args=(new_fw_node, fw_out_idx) + ) + saved_tensor_node.meta = copy.copy(new_fw_node.meta) + saved_tensor_node.meta["val"] = new_fw_node.meta["val"][fw_out_idx] + extra_fw_outputs.append(saved_tensor_node) + + fw_node.replace_all_uses_with(new_fw_node) + joint_gm.graph.erase_node(fw_node) + + # Bw node changes + # Prepare the operands for the bwd graph + # Old bw graph signature : (*primals, *tangents) + # New signature will be : (*sym_nodes, *saved_tensors, *tangents) + # We have already collected the saved_tensors in the forward hop processing. + + # extra_fw_outputs are in the order (*saved_nodes, *sym_nodes). + # Partitioner has this quirk where the backward wants sym_nodes + # first. So extract the sym and saved nodes. + + new_bw_hop_gm = new_hop_graphs[identifier].new_bw_hop_gm + if new_bw_hop_gm is None: + raise AssertionError( + f"new_bw_hop_gm for identifier {identifier} must not be None" + ) + + saved_tensor_nodes = extra_fw_outputs[:new_num_saved_nodes] + sym_nodes = extra_fw_outputs[new_num_saved_nodes:] + + num_primals = new_hop_graphs[identifier].old_num_fw_inputs + if num_primals is None: + raise AssertionError( + f"num_primals for identifier {identifier} must not be None" + ) + tangents = list(bw_node.args[2 + num_primals :]) + operands = sym_nodes + saved_tensor_nodes + tangents + + # Insert the new_bw_hop_gm into the joint_gm + with joint_gm.graph.inserting_after(bw_node): + new_bw_mod_attr_name = add_new_hop_gm(new_bw_hop_gm, bw_node.args[1]) + new_bw_mod_attr = joint_gm.graph.get_attr(new_bw_mod_attr_name) + new_bw_mod_attr.meta = copy.copy(bw_node.args[0].meta) + + with joint_gm.graph.inserting_after(new_bw_mod_attr): + new_bw_node = joint_gm.graph.call_function( + the_function=invoke_subgraph, + args=( + new_bw_mod_attr, + new_bw_mod_attr_name, + *operands, + ), + ) + propagate_meta_info(new_bw_hop_gm, new_bw_node, bw_node) + # Since the partitioner is run after the graph passes, we have lost + # the eager information and cannot faithfully extract the eager + # inputs for the new partitioned backward graph. For the forward + # graph, it was fine because the input signature remains same. + new_bw_node.meta.pop("eager_input_vals", None) + + bw_node.replace_all_uses_with(new_bw_node) + joint_gm.graph.erase_node(bw_node) + + joint_gm.graph.eliminate_dead_code() + joint_gm.graph.lint() + joint_gm.recompile() + return joint_gm + + +def maybe_log_graph( + gm: torch.fx.GraphModule, + graph_name: str, + aot_config: AOTConfig, + structured_log_prefix_fn: Callable[[], str], + out_structured_logs: list[str] | None = None, +) -> None: + if not aot_config.enable_log: + return + aot_graphs_log.debug( + "%s", + lazy_format_graph_code( + f"{graph_name}", + gm, + aot_config.aot_id, + include_stride=True, + include_device=True, + colored=True, + ), + ) + + def gm_str_fn() -> str: + return gm.print_readable( + print_output=False, + include_stride=True, + include_device=True, + expanded_def=True, + ) + + if out_structured_logs is not None: + out_structured_logs.append(f"{structured_log_prefix_fn()}:{gm_str_fn()}") + else: + trace_structured( + f"{structured_log_prefix_fn()}", + payload_fn=lambda: gm_str_fn(), + ) + + +def create_wrap_fn( + fn: Callable[..., Any], args: tuple[Any, ...] +) -> tuple[Callable[..., Any], tuple[Any, ...]]: + from torch.fx.experimental.proxy_tensor import maybe_enable_thunkify + + from .functional_utils import from_fun, has_data_mutation, to_fun + + def assert_no_mutation(t: Any) -> None: + if has_data_mutation(t): + raise AssertionError( + "Saved tensors hooks with inputs mutations are not allowed" + ) + + @simple_wraps(fn) + def _wrapper(*args: Any) -> Any: + with maybe_enable_thunkify(): + disable_above = torch._C._ExcludeDispatchKeyGuard( + torch._C.DispatchKeySet(torch._C.DispatchKey.Functionalize) + ) + + with disable_above: + f_args = pytree.tree_map(to_fun, args) + f_outs = fn(*f_args) + pytree.tree_map(assert_no_mutation, f_args) + return pytree.tree_map(from_fun, f_outs) + + return _wrapper, args + + +def prepare_hook_gm( + aot_config: AOTConfig, fn: Callable[..., Any], args: tuple[Any, ...] +) -> torch.fx.GraphModule: + from torch._functorch._aot_autograd.graph_capture import _create_graph + + fn, args = create_wrap_fn(fn, args) + gm = _create_graph(fn, args, aot_config=aot_config) # type: ignore[arg-type] + return gm + + +# Inline Autograd saved_tensors_hooks into epilogue of forward graph +# and prologue of backward graph. +# This changes forward graph outputs and inputs. +# Pack hook can return tensors, sym scalars, constants. +# All tensors to save for backward will be grouped together at front. +# Sym scalars grouped on another end. Constants are inlined in the graph. +def maybe_inline_graph_saved_tensors_hooks( + fw_module: torch.fx.GraphModule, + bw_module: torch.fx.GraphModule, + num_inner_fwd_outputs: int, + inner_meta: ViewAndMutationMeta, + aot_config: AOTConfig, + static_input_indices: list[int], +) -> None: + if torch._dynamo.compiled_autograd.in_compiled_autograd_region: + return + + get_hooks = torch._functorch._aot_autograd.utils.top_saved_tensors_hooks + are_inline_hooks = ( + torch._functorch._aot_autograd.utils.saved_tensors_hooks_are_inlineable + ) + + hooks = get_hooks() + if not are_inline_hooks(hooks): + return + + pack_hook_gm, unpack_hook_gm = hooks + + structured_logs: list[str] = [] + maybe_log_graph( + fw_module, + "Forward graph pre saved_tensors_hooks inlining", + aot_config, + lambda: "aot_forward_graph_pre_saved_tensors_hooks", + structured_logs, + ) + maybe_log_graph( + bw_module, + "Backward graph pre saved_tensors_hooks inlining", + aot_config, + lambda: "aot_backward_graph_pre_saved_tensors_hooks", + structured_logs, + ) + fw_g = fw_module.graph + bw_g = bw_module.graph + + fw_g_names = {node.name for node in fw_g.nodes} + bw_g_names = {node.name for node in bw_g.nodes} + + def _gen_unused_name(candidate: str) -> str: + c = candidate + i = 0 + while c in fw_g_names or c in bw_g_names: + c = f"{candidate}_{i}" + i = i + 1 + return c + + bw_g_inputs = bw_g.find_nodes(op="placeholder") + + fw_out_n = fw_g.output_node() + fw_outs = fw_out_n.args[0] # type: ignore[var-annotated] + fw_outs_inner_set = set(fw_outs[:num_inner_fwd_outputs]) # type: ignore[index] + fw_outs_saved_for_bw = fw_outs[num_inner_fwd_outputs:] # type: ignore[index] + fw_outs_packed_tensors = [] # type: ignore[var-annotated] + fw_outs_packed_syms = [] # type: ignore[var-annotated] + + # The main use case for saved_tensors_hooks is activation quantization, + # for memory usage optimization. + # Desired behavior is to quantize saved activations to free the original saved tensor. + # Saved nodes may include forward inputs, outputs, parameters. + # They may be held by something else and will not be deallocated after quantization. + # Donated buffers are intermediates in the graph invisible for the user, + # this guarantees that they can be deallocated. + # Using this as a default behavior to select saved nodes to apply hooks. + # There is also a config to apply hooks for all saved nodes without any filtering. + # The plan is to propagate meta about the source of the saved node to the user hook function. + mode = torch._functorch.config.saved_tensors_hooks_filtering_mode + allow_set = None + exclude_set = None + + if mode == "donated": + # collect_bw_donated_buffer_idxs requires inner_meta to have num_symints_saved_for_bw + inner_meta.num_symints_saved_for_bw = len( + [n for n in fw_outs_saved_for_bw if is_sym_node(n)] # type: ignore[arg-type] + ) + # Count tensors with no version counter check (used in tensors_saved_for_backwards_slice) + inner_meta.num_tensors_saved_with_no_vc_check = len( + [ + n + # pyrefly: ignore [not-iterable] + for n in fw_outs_saved_for_bw + if isinstance(n, torch.fx.Node) + and n.meta.get("saved_tensor_with_no_vc_check", False) + ] + ) + bw_donated_idxs = collect_bw_donated_buffer_idxs( + fw_module, + bw_module, + inner_meta, + ) + fw_donated_idxs = [ + i - inner_meta.num_symints_saved_for_bw for i in bw_donated_idxs + ] + allow_set = {fw_outs_saved_for_bw[i].name for i in fw_donated_idxs} # type: ignore[union-attr] + elif mode == "no_static": + fw_g_inputs = fw_g.find_nodes(op="placeholder") + exclude_set = {fw_g_inputs[i].name for i in static_input_indices} + + if (allow_set is not None) and (not allow_set): + # This means we have empty whitelist, + # No donated (intermediate) saved. + # Do not do anything in this case + return + + if aot_config.enable_log: + structured_logs.append(f"fw_outs_saved_for_bw:{fw_outs_saved_for_bw}") + structured_logs.append(f"mode:{mode}") + structured_logs.append(f"allow_set:{allow_set}") + structured_logs.append(f"exclude_set:{exclude_set}") + + # pyrefly: ignore [not-iterable] + for saved in fw_outs_saved_for_bw: + if ((allow_set is not None) and (saved.name not in allow_set)) or ( # type: ignore[union-attr] + (exclude_set is not None) and (saved.name in exclude_set) # type: ignore[union-attr] + ): + if isinstance(saved.meta["val"], torch.Tensor): # type: ignore[union-attr] + fw_outs_packed_tensors.append(saved) + continue + + val = saved.meta["val"] # type: ignore[union-attr] + if not isinstance(val, torch.Tensor): + continue + + def _get_extra_info() -> dict[str, Any]: + return {"_fw_graph": fw_g, "_bw_graph": bw_g, "_node": saved} + + with _saved_tensor_hook_context(_get_extra_info()): + pack_out_val = pack_hook_gm(val) + + requires_sc_handling = any( + is_traceable_wrapper_subclass(x) for x in pytree.tree_leaves(pack_out_val) + ) + if requires_sc_handling: + raise NotImplementedError( + "Tensor subclasses in GraphModule saved tensors hooks are not supported" + "You can workaround it by manually returning subclass's inner tensors" + " in the pack hook, and reconstructing the subclass in the unpack hook" + ) + + with _saved_tensor_hook_context(_get_extra_info()): + pack_gm = prepare_hook_gm(aot_config, pack_hook_gm, (val,)) + pack_g = pack_gm.graph + maybe_log_graph( + pack_gm, + f"saved_tensors_pack_hook {saved.name}", # type: ignore[union-attr] + aot_config, + lambda: f"aot_saved_tensors_hooks_pack {saved.name}", # type: ignore[union-attr] + structured_logs, + ) + pack_out_val = pack_gm(val) + + # Install pack hook graph as eiplogue of fw_module. + # Saved tensor output becomes input of pack hook graph. + # Replace saved tensor output with pack hook graph output. + # Outputs symbolic scalars, tensors are accumulated separately. + # Then in forward outputs and backward inputs installed in order + # sym_scalars, packed_saved_tensors. + # Keeping all tensors together allows to preserve + # the same identification at runtime, + # updating only number of saved sym_scalars and tensors. + pack_g_inputs = pack_g.find_nodes(op="placeholder") + if len(pack_g_inputs) != 1: + raise AssertionError( + f"expected exactly 1 pack_g_input, got {len(pack_g_inputs)}" + ) + env = {pack_g_inputs[0]: saved} + fw_pack_out_args = None + with fw_g.inserting_before(fw_out_n): + for node in pack_g.nodes: + if node.op == "placeholder": + continue + new_n = fw_g.node_copy(node, lambda n: env[n]) + fw_g_names.add(new_n.name) + env[node] = new_n + # Output node is temporarily copied to have remapped arguments. + # Removed in the end. + if node.op == "output": + fw_pack_out_args = new_n.args[0] + fw_g.erase_node(new_n) + + env.clear() + if not fw_pack_out_args: + raise AssertionError("fw_pack_out_args must not be empty") + fw_outs_bw_ins_node_names = [] + for out_idx, _n in enumerate(pytree.tree_leaves(fw_pack_out_args)): + if not isinstance(_n, torch.fx.Node): + fw_outs_bw_ins_node_names.append("") + continue + + # This happens when hook is noop and it is either user input or user output. + # Do not do anything with this node. + if _n.op == "placeholder" or _n in fw_outs_inner_set: + # This means the hook returned input primals unchanged + # Do not rename in this case. + n = _n + new_node_name = _n.name + fw_outs_bw_ins_node_names.append(new_node_name) + else: + # We can not specify desired name in node_copy. + # Copying node manually to set specific name, + # to have matching fw_outs, bw_inputs names. + new_node_name = _gen_unused_name(f"{saved.name}_hook_{out_idx}") # type: ignore[union-attr] + with fw_g.inserting_before(_n): + n = fw_g.create_node( + _n.op, + _n.target, + _n.args, + _n.kwargs, + name=new_node_name, + ) + if n.name != new_node_name: + raise AssertionError( + f"expected n.name == {new_node_name}, got {n.name}" + ) + fw_outs_bw_ins_node_names.append(new_node_name) + n.meta = copy.copy(_n.meta) + _n.replace_all_uses_with(n) + fw_g.erase_node(_n) + if isinstance(n.meta["val"], torch.Tensor): + fw_outs_packed_tensors.append(n) + elif is_sym_node(n): + fw_outs_packed_syms.append(n) + + # Install unpack hook graph as a prologue of backward graph + # Saved tensors inputs are replaced with packed tensors and packed sym scalars. + # The saved tensors inputs usages in the graph are replaced with unpack hook graph outputs. + with _saved_tensor_hook_context(_get_extra_info()): + unpack_gm = prepare_hook_gm(aot_config, unpack_hook_gm, (pack_out_val,)) + unpack_g = unpack_gm.graph + maybe_log_graph( + unpack_gm, + f"saved_tensors_unpack_hook {saved.name}", # type: ignore[union-attr] + aot_config, + lambda: f"aot_saved_tensors_hooks_unpack {saved.name}", # type: ignore[union-attr] + structured_logs, + ) + + def find_saved_in_bw_inputs( + bw_inputs: list[torch.fx.Node], + ) -> torch.fx.Node | None: + for n in bw_inputs: + if n.name == saved.name: # type: ignore[union-attr] + return n + + bw_g_input = find_saved_in_bw_inputs(bw_g_inputs) + if not bw_g_input: + raise AssertionError( + f"could not find saved tensor {saved.name} in bw_g_inputs" # type: ignore[union-attr] + ) + original_bw_g_input_users = list(bw_g_input.users.keys()) + bw_g_input_used_directly = False + + # Replace backward graph saved tensor input with copy of pack graph outputs + # All non-Tensor, non-symscalars outputs are constanted. + + unpack_g_inputs = unpack_g.find_nodes(op="placeholder") + env = {} + for out_idx, (unp_in_n, out_n, val) in enumerate( + zip( + unpack_g_inputs, + pytree.tree_leaves(fw_pack_out_args), + pytree.tree_leaves(pack_out_val), + ) + ): + is_sym = isinstance(val, py_sym_types) + if isinstance(val, torch.Tensor) or is_sym: + # We want forward_outputs names to match backward_inputs, + # Potentially backward may already have "{saved.name}_hook_{idx}", + # In this case fx.Graph will add suffix. + new_node_name = fw_outs_bw_ins_node_names[out_idx] + if bw_g_input.name == new_node_name: + env[unp_in_n] = bw_g_input + bw_g_input_used_directly = True + else: + # Backward calling convention: ctx_symints,ctx_saved_tensors + # Inserting packed sym scalars before first saved tensor input. + # Inserting packed tensors before last saved tensor input. + # Saved tensor inputs between them will be removed. + with ( + bw_g.inserting_before(bw_g_inputs[0]) + if is_sym + else bw_g.inserting_before(bw_g_input) + ): + new_n = bw_g.placeholder(new_node_name) + if new_n.name != new_node_name: + raise AssertionError( + f"expected new_n.name == {new_node_name}, got {new_n.name}" + ) + new_n.meta = copy.copy(out_n.meta) + env[unp_in_n] = new_n + else: + # Inline values of non-Tensor, non-SymScalars + env[unp_in_n] = val + + # Inserting unpack hook after placeholders. + bw_unpack_out_n = None + with bw_g.inserting_before(bw_g_inputs[-1].next): + for node in unpack_g.nodes: + if node.op == "placeholder": + continue + new_n = bw_g.node_copy(node, lambda n: env[n]) + bw_g_names.add(new_n.name) + env[node] = new_n + # Temporary insert output, to have remapped by node_copy args. + # Removed in the end. + if node.op == "output": + bw_unpack_out_n = new_n + + if not bw_unpack_out_n: + raise AssertionError("bw_unpack_out_n must not be None") + _leaves = pytree.tree_leaves(bw_unpack_out_n.args) + if len(_leaves) != 1: + raise AssertionError(f"expected exactly 1 leaf, got {len(_leaves)}") + unpack_saved_tensor_n = _leaves[0] + + if not bw_g_input_used_directly: + bw_g_input.replace_all_uses_with(unpack_saved_tensor_n) + bw_g.erase_node(bw_g_input) + else: + # Keep usages of bw_g_input in inserted unpacked hook graph. + # Replace other usages of bw_g_input with unpack_saved_tensor_n. + for use_node in original_bw_g_input_users: + use_node._replace_input_with(bw_g_input, unpack_saved_tensor_n) + bw_g.erase_node(bw_unpack_out_n) + + # Changing forward graph outputs, + # Inserting packed_tensors and packed_syms on the place of saved tensors. + # Packed sym_scalars are together with saved symints + symint_outs_saved_for_bw = [n for n in fw_outs_saved_for_bw if is_sym_node(n)] # type: ignore[arg-type] + fw_new_outs = pytree.tree_leaves( + ( + fw_outs[:num_inner_fwd_outputs], # type: ignore[index] + fw_outs_packed_tensors, + fw_outs_packed_syms, + symint_outs_saved_for_bw, + ) + ) + fw_out_n.args = (tuple(fw_new_outs),) + + # Assert that saved tensors and symints in forward outputs are aligned with backward inputs + _fw_n = num_inner_fwd_outputs + _fw_num_t = len(fw_outs_packed_tensors) + _fw_num_s = len(fw_outs_packed_syms) + len(symint_outs_saved_for_bw) + fw_outs_saved_tensors = fw_new_outs[_fw_n : _fw_n + _fw_num_t] + fw_outs_saved_syms = fw_new_outs[_fw_n + _fw_num_t :] + bw_new_ins = list(bw_g.find_nodes(op="placeholder")) + bw_ins_saved_syms = bw_new_ins[:_fw_num_s] + bw_ins_saved_tensors = bw_new_ins[_fw_num_s : _fw_num_s + _fw_num_t] + + fw_t_names = [n.name for n in fw_outs_saved_tensors] + bw_t_names = [n.name for n in bw_ins_saved_tensors] + fw_s_names = [n.name for n in fw_outs_saved_syms] + bw_s_names = [n.name for n in bw_ins_saved_syms] + + def _log_structured_logs() -> None: + if not aot_config.enable_log: + return + + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "aot_saved_tensors_hooks_graphs", + "encoding": "string", + }, + payload_fn=lambda: "\n".join(structured_logs), + ) + + if aot_config.enable_log: + structured_logs.append( + f"fw_outs[:num_inner_fwd_outputs]:{fw_outs[:num_inner_fwd_outputs]}" # type: ignore[index] + ) + structured_logs.append(f"fw_outs_packed_tensors:{fw_outs_packed_tensors}") + structured_logs.append(f"fw_t_names:{fw_t_names}") + structured_logs.append(f"bw_t_names:{bw_t_names}") + structured_logs.append(f"fw_s_names:{fw_s_names}") + structured_logs.append(f"bw_s_names:{bw_s_names}") + structured_logs.append(f"\nfw_g_pre_assert:{fw_g}") + structured_logs.append(f"\nbw_g_pre_assert:{bw_g}") + maybe_log_graph( + fw_module, + "Forward graph after transform pre-assert", + aot_config, + lambda: "aot_forward_graph_pre_assert_saved_tensors_hooks", + structured_logs, + ) + maybe_log_graph( + bw_module, + "Backward graph after transform pre-assert", + aot_config, + lambda: "aot_backward_graph_pre_assert_saved_tensors_hooks", + structured_logs, + ) + _log_structured_logs() + + if fw_t_names != bw_t_names: + raise AssertionError( + f"expected fw_t_names == bw_t_names, got {fw_t_names} != {bw_t_names}" + ) + if fw_s_names != bw_s_names: + raise AssertionError( + f"expected fw_s_names == bw_s_names, got {fw_s_names} != {bw_s_names}" + ) + + fw_g.lint() + bw_g.lint() + fw_module.recompile() + bw_module.recompile() + + +def _log_joint_graph( + fx_g: torch.fx.GraphModule, + aot_config: AOTConfig, +) -> str | None: + """ + Log the joint graph to the structured logger. + Return a str representation of the graph. + """ + joint_graph_str = None + if aot_config.enable_log: + aot_joint_log.info( + "%s", + lazy_format_graph_code( + "Joint graph", + fx_g, + aot_config.aot_id, + include_stride=True, + include_device=True, + colored=True, + ), + ) + joint_graph_str = fx_g.print_readable( + print_output=False, + include_stride=True, + include_device=True, + expanded_def=True, + ) + trace_structured( + "aot_joint_graph", + payload_fn=lambda: joint_graph_str, + ) + return joint_graph_str + + +def _log_fw_bw_graphs( + fw_module: torch.fx.GraphModule, + bw_module: torch.fx.GraphModule, + maybe_subclass_meta: SubclassMeta | None, + fw_metadata: ViewAndMutationMeta, + aot_config: AOTConfig, +) -> tuple[str | None, str | None]: + """ + Log the fw and bw graphs to the structured logger. + Return str representations of the graphs. + """ + fw_module_str = None + bw_module_str = None + if aot_config.enable_log: + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "torch._functorch.config", + "encoding": "string", + }, + payload_fn=lambda: torch._functorch.config.get_serializable_config_copy(), + ) + aot_graphs_log.info( + "aot_config id: %s, fw_metadata=%s, inner_meta=%s", + aot_config.aot_id, + fw_metadata, + _get_inner_meta(maybe_subclass_meta, fw_metadata), + ) + + aot_graphs_log.info( + "%s", + lazy_format_graph_code( + "Forward graph", + fw_module, + aot_config.aot_id, + include_stride=True, + include_device=True, + colored=True, + ), + ) + aot_graphs_log.info( + "%s", + lazy_format_graph_code( + "Backward graph", + bw_module, + aot_config.aot_id, + include_stride=True, + include_device=True, + colored=True, + ), + ) + fw_module_str = fw_module.print_readable( + print_output=False, + include_stride=True, + include_device=True, + expanded_def=True, + ) + bw_module_str = bw_module.print_readable( + print_output=False, + include_stride=True, + include_device=True, + expanded_def=True, + ) + + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "aot_forward_graph_fw_metadata", + "encoding": "string", + }, + payload_fn=lambda: dataclass_repr(fw_metadata), + ) + if maybe_subclass_meta is not None: + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "aot_forward_graph_fw_subclass_metadata", + "encoding": "string", + }, + payload_fn=lambda: dataclass_repr(maybe_subclass_meta), + ) + + trace_structured( + "aot_forward_graph", + payload_fn=lambda: fw_module_str, + ) + trace_structured( + "aot_backward_graph", + payload_fn=lambda: bw_module_str, + ) + return fw_module_str, bw_module_str + + +def _partition_joint_graph_into_fw_bw( + fx_g: torch.fx.GraphModule, + joint_inputs: list[Any] | tuple[list[Any], list[Any]], + inner_meta: ViewAndMutationMeta, + fw_metadata: ViewAndMutationMeta, + aot_config: AOTConfig, +) -> tuple[torch.fx.GraphModule, torch.fx.GraphModule, int]: + # See Note: [Partitioner handling for Subclasses, Part 1] + # See Note: [Recomputing subclass mutation handling] + mutated_inp_runtime_indices = compute_inner_mutated_inp_indices_from_subclass_meta( + fw_metadata, inner_meta + ) + num_tokens = len(fw_metadata.tokens) + num_inner_fwd_outputs = ( + len(mutated_inp_runtime_indices) + + inner_meta.num_outputs + + inner_meta.num_intermediate_bases + + inner_meta.num_outputs_rng_offset + + num_tokens # See Note [Side-Effectful Tokens in AOTAutograd] + ) + + fx_g = run_joint_graph_passes_on_hops(fx_g, joint_inputs, aot_config) + + # apply joint_gm callback here + if callable(torch._functorch.config.joint_custom_pass): + # pyrefly: ignore [bad-assignment] + fx_g = torch._functorch.config.joint_custom_pass(fx_g, joint_inputs) + + if aot_config.partition_fn is None: + raise AssertionError("aot_config.partition_fn must not be None") + fw_module, bw_module = aot_config.partition_fn( + fx_g, + joint_inputs, + num_fwd_outputs=num_inner_fwd_outputs, + static_lifetime_input_indices=fw_metadata.static_input_indices, + ) + + rng_states = [ + n + for n in fw_module.graph.find_nodes(op="placeholder") + if "fwd_rng_state" in n.name + ] + fw_metadata.num_graphsafe_rng_states = len(rng_states) + if rng_states: + fw_metadata.graphsafe_rng_state_index = rng_states[0].meta["val"].device.index + + return fw_module, bw_module, num_inner_fwd_outputs + + +def _joint_inputs_for_forward( + joint_inputs: list[Any] | tuple[list[Any], list[Any]], +) -> list[Any]: + return joint_inputs[0] if isinstance(joint_inputs, tuple) else joint_inputs + + +def _maybe_unlift_partitioned_effect_tokens( + fw_module: torch.fx.GraphModule, + bw_module: torch.fx.GraphModule, + joint_inputs: list[Any] | tuple[list[Any], list[Any]], + fw_metadata: ViewAndMutationMeta, + aot_config: AOTConfig, + num_inner_fwd_outputs: int, +) -> tuple[int, list[Any] | tuple[list[Any], list[Any]]]: + num_tokens = len(fw_metadata.tokens) + + # See Note [Side-Effectful Tokens in AOTAutograd] + if config.unlift_effect_tokens and ( + num_tokens > 0 or fw_metadata.num_backward_tokens > 0 + ): + unlift_tokens(fw_module, fw_metadata, aot_config, bw_module) + num_inner_fwd_outputs -= num_tokens + if isinstance(joint_inputs, tuple): + joint_inputs = ( + _joint_inputs_for_forward(joint_inputs)[num_tokens:], + joint_inputs[1], + ) + else: + joint_inputs = joint_inputs[num_tokens:] + + return num_inner_fwd_outputs, joint_inputs + + +def _categorize_saved_tensors_for_backward( + fw_module: torch.fx.GraphModule, + bw_module: torch.fx.GraphModule, + inner_meta: ViewAndMutationMeta, + fw_metadata: ViewAndMutationMeta, + num_inner_fwd_outputs: int, +) -> tuple[int, int]: + fw_outs = next(iter(fw_module.graph.find_nodes(op="output"))).args[0] + # we only need to bookkeep the symints that are saved for bw, not any symints + # the user forward might have returned in its own output + fw_outs_saved_for_bw = fw_outs[num_inner_fwd_outputs:] + num_fw_outs_saved_for_bw = len(fw_outs_saved_for_bw) + + num_symints_saved_for_bw = 0 + num_opaque_objects_saved_for_bw = 0 + for idx, node in enumerate(fw_outs_saved_for_bw): + if is_sym_node(node): + num_symints_saved_for_bw += 1 + elif is_opaque_node(node): + num_opaque_objects_saved_for_bw += 1 + elif isinstance(node, torch.fx.Node) and "val" in getattr(node, "meta", {}): + if isinstance(node.meta["val"], FakeTensor): + # record dynamic tensor activations + dynamic_dims: set[int] = { + dim + for dim, size in enumerate(node.meta["val"].shape) + if not isinstance(size, int) + } + if dynamic_dims: + fw_metadata.dynamic_saved_tensors_idxs[idx] = dynamic_dims + elif isinstance(node.meta["val"], (FakeScriptObject, OpaqueBase)): + num_opaque_objects_saved_for_bw += 1 + + fw_metadata.num_symints_saved_for_bw = num_symints_saved_for_bw + fw_metadata.num_opaque_objects_saved_for_bw = num_opaque_objects_saved_for_bw + inner_meta.num_symints_saved_for_bw = num_symints_saved_for_bw + inner_meta.num_opaque_objects_saved_for_bw = num_opaque_objects_saved_for_bw + + # See Note [Activations with no version counter checks in eager] + # Count tensors saved with no version counter check. + # These are tensors that were stashed on ctx (e.g., ctx.x = x) rather than + # via save_for_backward in an autograd.Function. + # The partitioner sorts these to be at the end of saved_values. + num_tensors_saved_with_no_vc_check = sum( + 1 + for node in fw_outs_saved_for_bw + if isinstance(node, torch.fx.Node) + and node.meta.get("saved_tensor_with_no_vc_check", False) + ) + fw_metadata.num_tensors_saved_with_no_vc_check = num_tensors_saved_with_no_vc_check + inner_meta.num_tensors_saved_with_no_vc_check = num_tensors_saved_with_no_vc_check + + if torch._functorch.config.donated_buffer: + fw_metadata.bw_donated_idxs = collect_bw_donated_buffer_idxs( + fw_module, + bw_module, + inner_meta, + ) + inner_meta.bw_donated_idxs = fw_metadata.bw_donated_idxs + + return num_fw_outs_saved_for_bw, num_symints_saved_for_bw + + +# Note [Detaching inputs that never need gradients] +# See https://github.com/pytorch/pytorch/issues/97745 +# Suppose we have a function like this that we want to compile: +# +# def f(x, y): +# return torch.mul(x, y.detach()) +# +# What gradients should we compute for x and y? +# By default, AOTAutograd will compute a gradient for **every** input that requires gradients, +# and so we'll compute: +# x_grad_input = y +# y_grad_input = None +# Does this preserve the semantics of eager mode? +# Unfortunately, no. +# Doing the above will cause autograd to **continue** to backprop the autograd tape +# that was generated from constructing y. +# +# This is **different** from what would have happened in eager mode. +# In eager mode, if we backprop through the output of this function, autograd will only traverse +# the bit of the autograd tape corresponding to "x". +# In particular, if a user had previously backpropped through y's autograd tape, +# And then they try to backprop through the output of the above function, +# then we'll hit the dreaded "Trying to backward through the graph a second time" error. +# +# You might think: If autograd sees that a gradient is None, shouldn't it stop early, +# instead of continuing the backprop through the ancestors of that node in the graph? +# +# Autograd has two passes: +# (1) a first pass that traverses the autograd graph and figures out which nodes need to be executed +# (2) a second pass that actually goes ahead and executes each node when it becomes ready, +# propagating gradients +# By the time we're executing a node and we see that it produces a None, the set of nodes to execute +# is already locked-in. +# +# The fix: instead, we can recognize statically that the graph we're compiling will never contribute +# gradients to y, and prevent autograd from trying to traverse y's autograd tape at all. +# We can do this by manually detach'ing y before sending it through the `CompiledFunction`. +# +# Note that this solution is not bulletproof. +# It's possible to construct a case where eager may or may not have have tried to autograd through y, +# depending on the actual grad_outputs that were passed in during the backward. +# There is no easy fix for this: the simplest fix would be to run with `retain_graph=True`, +# allowing autograd to reuse the graph. +# +# An example of this case is: +# def f(x): +# return x.detach() * 2, x * 3 +# If we were to only backprop through outs[0], in eager, we would stop +# If we backward only on the first output, we shouldn't send a grad through x. +# But the custom autograd function doesn't know that: it will materialize zero grads for x * 3 +# and we will end up with a zero grad at x. +# If we later backprop through the second output, this will also require backprop'ing through x. +# Meaning we'll need to use `retain_graph=True` to be able to backprop through x the second time. +def _compute_indices_of_inps_to_detach( + bw_module: torch.fx.GraphModule, + maybe_subclass_meta: SubclassMeta | None, + inner_meta: ViewAndMutationMeta, + fw_metadata: ViewAndMutationMeta, +) -> list[int]: + # TODO: we should apply the below "detach inputs if their gradients are statically known to be None" + # optimization even if we have subclass inputs/outputs (we do not handle this today). + # Computing which our our inputs get None gradients is a bit more complicated, + # if any of our inputs are subclasses. Why? + # (a) we need to make sure that we call .detach() on the input subclasses, since autograd sees subclasses. + # (b) The grad_outputs that we AOT computed in our backward graph are the desugared tensor tensors, + # so we need to figure out which subclass fw inputs they map to. + if maybe_subclass_meta is not None: + return [] + + indices_of_inps_to_detach: list[int] = [] + + # reversed() since we expect output at end of graph + bw_output = next(reversed(bw_module.graph.find_nodes(op="output"))) + bw_outs = bw_output.args[0] + + num_backward_tokens = inner_meta.num_backward_tokens + expected_bw_outs = ( + len(fw_metadata.input_info) + + inner_meta.num_outputs_rng_offset + + num_backward_tokens + ) + if len(bw_outs) != expected_bw_outs: + raise AssertionError( + f"expected len(bw_outs) == {expected_bw_outs}, got {len(bw_outs)}" + ) + + bw_outs_no_rng_no_tokens = bw_outs + if (inner_meta.num_outputs_rng_offset + num_backward_tokens) > 0: + bw_outs_no_rng_no_tokens = bw_outs[ + : -(inner_meta.num_outputs_rng_offset + num_backward_tokens) + ] + if len(bw_outs_no_rng_no_tokens) != len(fw_metadata.input_info): + raise AssertionError( + f"expected len(bw_outs_no_rng_no_tokens) == {len(fw_metadata.input_info)}, " + f"got {len(bw_outs_no_rng_no_tokens)}" + ) + + for i, bw_out in enumerate(bw_outs_no_rng_no_tokens): + # If our input experiences a metadata mutation inside the graph (e.g. set_()), + # we *must* not detach, otherwise it will be the detach'd input that gets the metadata mutation + metadata_mutation_in_graph = ( + fw_metadata.input_info[i].mutation_type == MutationType.MUTATED_IN_GRAPH + and fw_metadata.input_info[i].mutates_storage_metadata + ) + is_non_leaf = ( + fw_metadata.input_info[i].requires_grad + and not fw_metadata.input_info[i].is_leaf + ) + if bw_out is None and not metadata_mutation_in_graph and is_non_leaf: + indices_of_inps_to_detach.append(i) + + return indices_of_inps_to_detach + + +def _aot_stage2a_partition( + fx_g: torch.fx.GraphModule, + joint_inputs: list[Any] | tuple[list[Any], list[Any]], + maybe_subclass_meta: SubclassMeta | None, + fw_metadata: ViewAndMutationMeta, + aot_config: AOTConfig, +) -> tuple[torch.fx.GraphModule, torch.fx.GraphModule, int, int, list[int], list[Any]]: + """ + Partition the joint graph into a forward graph and a backward graph. Returns: + - the forward and backward graphs + - the number of forward outputs and the number of symints saved for backward + - indices of inputs to detach + - adjusted inputs to forward + """ + disable_amp = torch._C._is_any_autocast_enabled() + inner_meta = _get_inner_meta(maybe_subclass_meta, fw_metadata) + + with torch.no_grad(): + context = torch._C._DisableAutocast if disable_amp else nullcontext + with context(), track_graph_compiling(aot_config, "joint"): + fw_module, bw_module, num_inner_fwd_outputs = ( + _partition_joint_graph_into_fw_bw( + fx_g, + joint_inputs, + inner_meta, + fw_metadata, + aot_config, + ) + ) + num_inner_fwd_outputs, joint_inputs = ( + _maybe_unlift_partitioned_effect_tokens( + fw_module, + bw_module, + joint_inputs, + fw_metadata, + aot_config, + num_inner_fwd_outputs, + ) + ) + + maybe_inline_graph_saved_tensors_hooks( + fw_module, + bw_module, + num_inner_fwd_outputs, + inner_meta, + aot_config, + fw_metadata.static_input_indices, + ) + num_fw_outs_saved_for_bw, num_symints_saved_for_bw = ( + _categorize_saved_tensors_for_backward( + fw_module, + bw_module, + inner_meta, + fw_metadata, + num_inner_fwd_outputs, + ) + ) + + _indices_of_inps_to_detach = _compute_indices_of_inps_to_detach( + bw_module, + maybe_subclass_meta, + inner_meta, + fw_metadata, + ) + + return ( + fw_module, + bw_module, + num_fw_outs_saved_for_bw, + num_symints_saved_for_bw, + _indices_of_inps_to_detach, + _joint_inputs_for_forward(joint_inputs), + ) + + +def _aot_stage2b_fw_compile( + fw_module: torch.fx.GraphModule, + adjusted_flat_args: list[Any], + maybe_subclass_meta: SubclassMeta | None, + fw_metadata: ViewAndMutationMeta, + num_fw_outs_saved_for_bw: int, + aot_config: AOTConfig, + # pyrefly: ignore [implicit-any] +) -> tuple[list[tuple[int, ...] | None] | None, Callable]: + return _aot_stage2b_compile_forward_or_inference( + fw_module, + adjusted_flat_args, + maybe_subclass_meta, + fw_metadata, + aot_config, + is_inference=False, + num_fw_outs_saved_for_bw=num_fw_outs_saved_for_bw, + ) + + +def _aot_stage2b_bw_compile( + bw_module: torch.fx.GraphModule, + maybe_subclass_meta: SubclassMeta | None, + fw_metadata: ViewAndMutationMeta, + fwd_output_strides: list[tuple[int, ...] | None] | None, + num_symints_saved_for_bw: int, + aot_config: AOTConfig, + # pyrefly: ignore [implicit-any] +) -> tuple[AutogradLazyBackwardCompileInfo, Callable | None]: + """ + Compile the backward graph. Returns: + - the placeholder list for the backward graph + - the compiled backward function + """ + with torch.no_grad(): + # NB: It's important to compile backwards ahead of time, as this may + # add extra guards which we need to apply to the Dynamo cache at + # forwards + with track_graph_compiling(aot_config, "backward"), torch._C._DisableAutocast(): + placeholder_list = fx_placeholder_vals(bw_module) + + forward_saved_for_backwards_strides = None + if fwd_output_strides is not None: + inner_meta = _get_inner_meta(maybe_subclass_meta, fw_metadata) + forward_saved_for_backwards_strides = fwd_output_strides[ + inner_meta.tensors_saved_for_backwards_slice + ] + + # saved activations can have different stride to eager if + # the compiler does layout optimization. We should restride the + # tensor passed in for compiling the backward graph using the + # saved tensor's stride. + for i in range(len(placeholder_list)): + ph_arg = placeholder_list[i] + if not isinstance(ph_arg, torch.Tensor): + continue + + if forward_saved_for_backwards_strides is None: + continue + + real_stride = None + # Per all_args calling convention + j = i - num_symints_saved_for_bw + if 0 <= j < len(forward_saved_for_backwards_strides): + real_stride = forward_saved_for_backwards_strides[j] + if real_stride is None: + continue + + # Comparing ph_arg.stride() with real_stride directly may + # cause dynamic dimensions in ph_arg being specialized to static + # value. Using suppress_guards and guard_or_true to avoid that. + + stride_different = False + fake_mode = detect_fake_mode() + suppress_ctx = ( + fake_mode.shape_env.suppress_guards() + if fake_mode is not None and fake_mode.shape_env is not None + else nullcontext() + ) + + # Inductor can choose different strides for activations than + # what backward graph has. if we can't statically tell that + # strides are the same, we assume they are not. + with suppress_ctx: + for k in range(len(ph_arg.stride())): + # real_stride can't be symbolic. + + if guard_or_true(ph_arg.stride()[k] != int(real_stride[k])): + stride_different = True + break + + if stride_different: + # Note that here we use the stride of the real tensor to + # restride a FakeTensor. This does not cause trouble + # for dynamic shape since this code path only get + # executed if layout optimization is enabled. And we + # disable layout optimization for dynamic shape right + # now. + # + # A solution that decide stride order based on real + # tensor's stride and then apply that stride order to + # the FakeTensor does not work smoothly since some + # tensor's layout is not 'dense'. E.g. mixnet_l has a + # tensor with size [8, 64, 112, 112] and strides + # (2408448, 1, 21504, 192). The solution mentioned will + # decide a stride of (802816, 1, 7168, 64) for this + # tensor which is wrong. + + ph_size = ph_arg.size() + + placeholder_list[i] = ph_arg.as_strided(ph_size, real_stride) + compiled_bw_func = None + if ( + num_symints_saved_for_bw > 0 + or aot_config.force_non_lazy_backward_lowering + ): + try: + # See Note: [Backward graph lazy lowering] + with torch._subclasses.fake_tensor.unset_fake_temporarily(): + # If bw_module contains lifted constants, they will be real tensors stored as + # GraphModule. Deepcopying tensors under fake mode is not supported and will + # raise when attempting to set storage. + bw_module_copy = copy.deepcopy(bw_module) + if aot_config.bw_compiler is None: + raise AssertionError("aot_config.bw_compiler must not be None") + compiled_bw_func = aot_config.bw_compiler( + bw_module_copy, placeholder_list + ) + del bw_module_copy + except Exception as e: + if aot_config.force_non_lazy_backward_lowering: + raise + exc = e + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "eager_compile_backwards_failure", + "encoding": "string", + }, + payload_fn=lambda: "\n".join( + traceback.format_exception( + type(exc), exc, exc.__traceback__ + ) + ), + ) + log.warning( + "failed to eagerly compile backwards for dynamic, suppressing in case backwards not needed", + exc_info=True, + ) + # Compiled autograd will run the bw_module in the backward pass, + # so recompilation need happen anyway if the backward pass is ever + # called. + # + # The reason we do the GraphModule recompilation here is because + # the lazy recompilation will cause issue in the backward pass + # with compiled autograd. + # + # Do the _LazyGraphModule.force_recompile here rather than when + # bw_module is first generated by the partitioner because the bw_module.recompile + # may be called in some code path later and cause the _LazyGraphModule.forward + # becomes the lazy version again. One example is when dynamic shape is enabled + # upfront, the bw_compiler will be called above which can cause extra + # graph module recompilation on bw_module. + if torch._dynamo.compiled_autograd.in_compiled_autograd_region: + from torch.fx._lazy_graph_module import _LazyGraphModule + + _LazyGraphModule.force_recompile(bw_module) + + saved_context = TracingContext.try_get() + saved_compile_context = CompileContext.try_get() + + lazy_backward_info = AutogradLazyBackwardCompileInfo( + # pyrefly: ignore [bad-argument-type] + bw_module, + placeholder_list, + saved_context, + saved_compile_context, + ) + + return lazy_backward_info, compiled_bw_func + + +def aot_stage2_autograd( + aot_state: AOTState, + aot_graph_capture: AOTGraphCapture, +) -> DispatchReturn: + """ + Autograd logic. Generates a joint graph, partitions it, manipulates the input with various wrappers, + and returns a wrapped torch.autograd.Function with a forward and backward. + """ + + fx_g = aot_graph_capture.graph_module + maybe_subclass_meta = aot_graph_capture.maybe_subclass_meta + fw_metadata = aot_state.fw_metadata + aot_config = aot_state.aot_config + + CompileEventLogger.try_add_pt2_compile("backend_compile", dispatch_mode="autograd") + joint_graph_str = _log_joint_graph(fx_g, aot_config) + + _apply_tensorify_python_scalars(fx_g) + + ( + fw_module, + bw_module, + num_fw_outs_saved_for_bw, + num_symints_saved_for_bw, + _indices_of_inps_to_detach, + adjusted_flat_args, + ) = _aot_stage2a_partition( + fx_g, + aot_graph_capture.updated_flat_args, + maybe_subclass_meta, + fw_metadata, + aot_config, + ) + + fw_module_str, bw_module_str = _log_fw_bw_graphs( + fw_module, bw_module, maybe_subclass_meta, fw_metadata, aot_config + ) + + fwd_output_strides, compiled_fw_func = _aot_stage2b_fw_compile( + fw_module, + adjusted_flat_args, + maybe_subclass_meta, + fw_metadata, + num_fw_outs_saved_for_bw, + aot_config, + ) + + lazy_backward_info, compiled_bw_func = _aot_stage2b_bw_compile( + bw_module, + maybe_subclass_meta, + fw_metadata, + fwd_output_strides, + num_symints_saved_for_bw, + aot_config, + ) + + try_save_cache_entry, entry = _cache_autograd_info( + aot_config, + aot_state.flat_args, + compiled_fw_func, + compiled_bw_func, + fw_module_str, + bw_module_str, + joint_graph_str, + aot_graph_capture.wrappers, + maybe_subclass_meta, + fw_metadata, + num_fw_outs_saved_for_bw, + _indices_of_inps_to_detach, + num_symints_saved_for_bw, + bw_module, + ) + + return _aot_stage2c_make_autograd_function( + aot_config, + aot_state.flat_args, + fw_metadata, + maybe_subclass_meta, + aot_graph_capture.wrappers, + compiled_fw_func, + compiled_bw_func, + lazy_backward_info, + try_save_cache_entry, # type: ignore[arg-type] + entry, # type: ignore[arg-type] + _indices_of_inps_to_detach, + num_symints_saved_for_bw, + ) + + +def _aot_stage2c_make_autograd_function( + aot_config: AOTConfig, + flat_args: list[Any], + fw_metadata: ViewAndMutationMeta, + maybe_subclass_meta: SubclassMeta | None, + wrappers: list[CompilerWrapper], + compiled_fw_func: Callable[..., Any], + compiled_bw_func: Callable[..., Any] | None, + lazy_backward_info: AutogradLazyBackwardCompileInfo | None, + try_save_cache_entry: Callable[..., Any], + entry: GenericAOTAutogradResult[Any, Any] | None, + _indices_of_inps_to_detach: list[int], + num_symints_saved_for_bw: int, +) -> DispatchReturn: + backward_state_indices = [ + idx for idx, x in enumerate(flat_args) if isinstance(x, BackwardState) + ] + if len(backward_state_indices) > 1: + raise AssertionError( + f"expected at most 1 backward_state_index, got {len(backward_state_indices)}" + ) + + disable_amp = torch._C._is_any_autocast_enabled() + compile_spec = AOTDispatchAutogradCompileSpec( + compiled_fw_func=compiled_fw_func, + compiled_bw_func=compiled_bw_func, + maybe_subclass_meta=maybe_subclass_meta, + num_symints_saved_for_bw=num_symints_saved_for_bw, + backward_state_indices=backward_state_indices, + disable_amp=disable_amp, + indices_of_inps_to_detach=_indices_of_inps_to_detach, + lazy_backward_info=lazy_backward_info, + aot_config=aot_config, + fw_metadata=fw_metadata, + try_save_cache_entry=try_save_cache_entry, + ) + compiled_fn = AOTDispatchAutograd.post_compile(compile_spec) + + if entry is not None: + compiled_fn = SerializableCompiledFunction(compiled_fn, lambda: entry) + + if config.debug_assert: + flat_requires_grad: list[bool | None] = [ + a.requires_grad if isinstance(a, Tensor) else None for a in flat_args + ] + compiled_fn = DebugAssertWrapper( + flat_requires_grad=flat_requires_grad + ).post_compile(compiled_fn, aot_config, runtime_metadata=fw_metadata) + + compiled_fn = post_compile( + wrappers, + compiled_fn, + aot_config, + runtime_metadata=fw_metadata, + ) + return compiled_fn + + +def _cache_autograd_info( + aot_config: AOTConfig, + flat_args: list[Any], + compiled_fw_func: Callable[..., Any], + compiled_bw_func: Callable[..., Any] | None, + fw_module_str: str | None, + bw_module_str: str | None, + joint_graph_str: str | None, + wrappers: list[CompilerWrapper], + maybe_subclass_meta: SubclassMeta | None, + fw_metadata: ViewAndMutationMeta, + num_fw_outs_saved_for_bw: int, + _indices_of_inps_to_detach: list[int], + num_symints_saved_for_bw: int, + bw_module: torch.fx.GraphModule | None, +) -> tuple[ + GenericAOTAutogradResult[Any, Any] | None, + Callable[..., Any], +]: + backward_state_indices = [ + idx for idx, x in enumerate(flat_args) if isinstance(x, BackwardState) + ] + if len(backward_state_indices) > 1: + raise AssertionError( + f"expected at most 1 backward_state_index, got {len(backward_state_indices)}" + ) + + make_runtime_safe(fw_metadata, maybe_subclass_meta) + + try_save_cache_entry: Callable[..., Any] | None = None + entry: GenericAOTAutogradResult[Any, Any] | None = None + + if aot_config.cache_info is not None: + forward_time_taken_ns = time.time_ns() - aot_config.cache_info.start_time_ns + + # NB: aot_config here is technically not needed as an argument: we could just + # close over aot_config.cache_info, since aot_config never changes. + # But closing over random variables is confusing IMO, so I'm leaving it. + def try_save_cache_entry( # noqa: F811 + compiled_bw_func: Callable[..., Any], + bw_module: torch.fx.GraphModule, + _fw_metadata: ViewAndMutationMeta, + aot_config: AOTConfig, + ) -> GenericAOTAutogradResult[Any, Any] | None: + cache_info = aot_config.cache_info + + def should_save_cache() -> bool: + if should_bundle_autograd_cache(): + return True + else: + return hasattr(compiled_fw_func, "_fx_graph_cache_key") and hasattr( + compiled_bw_func, "_fx_graph_cache_key" + ) + + if cache_info is not None and should_save_cache(): + if forward_time_taken_ns is None: + raise AssertionError("forward_time_taken_ns must not be None") + # TODO: technically, AOTAutograd does a *little* bit of post processing work + # in the backward that isn't measured here. But it's small enough that it's not worth + # the complexity of threading a bunch of times through the code, so we + # use the compiled_bw_func's inductor compile time instead. + # It's possible this changes in the future, in which case we should + # update backward_time_taken_ns to be more inclusive + backward_time_taken_ns = getattr(compiled_bw_func, "_time_taken_ns", 0) + + aot_forward_graph_str: str | None = fw_module_str + aot_backward_graph_str: str | None = bw_module_str + aot_joint_graph_str: str | None = joint_graph_str + guards_expr = AOTAutogradCache.generate_guards_expression(cache_info) + + entry = AOTAutogradCache.make_entry( + compiled_fw_func, # type: ignore[arg-type] + compiled_bw_func, # type: ignore[arg-type] + aot_joint_graph_str, + aot_forward_graph_str, + aot_backward_graph_str, + _fw_metadata, + wrappers, + maybe_subclass_meta, + num_fw_outs_saved_for_bw, + _indices_of_inps_to_detach, + forward_time_taken_ns, + backward_time_taken_ns, + sanitized_aot_config=sanitize_aot_config(aot_config), + guards_expr=guards_expr, + backward_state_indices=backward_state_indices, + num_symints_saved_for_bw=num_symints_saved_for_bw, + serialized_bw_module=serialize_graph_module(bw_module), + ) + AOTAutogradCache.save( + cache_info.cache_key, + entry, + remote=should_use_remote_autograd_cache(), + ) + return entry + return None + + if compiled_bw_func is not None: + # If we already compiled the backward, we save its cache entry now + if bw_module is None: + raise AssertionError( + "bw_module must not be None when compiled_bw_func is not None" + ) + entry = try_save_cache_entry( + compiled_bw_func, + bw_module, + fw_metadata, + aot_config, # type: ignore[arg-type] + ) + try_save_cache_entry = None + + return try_save_cache_entry, entry # type: ignore[return-value] + + +def _aot_stage2b_compile_forward_or_inference( + fw_module: torch.fx.GraphModule, + adjusted_flat_args: list[Any], + maybe_subclass_meta: SubclassMeta | None, + fw_metadata: ViewAndMutationMeta, + aot_config: AOTConfig, + *, + is_inference: bool, + num_fw_outs_saved_for_bw: int | None = None, + # pyrefly: ignore [implicit-any] +) -> tuple[list[tuple[int, ...] | None] | None, Callable]: + """ + Compile the forward or inference graph. Returns: + - the output strides of the forward graph + - the compiled forward/inference function + + Args: + fw_module: The forward graph module to compile + adjusted_flat_args: Flattened arguments after adjustments + maybe_subclass_meta: Metadata for tensor subclasses + fw_metadata: View and mutation metadata + aot_config: AOT configuration + is_inference: If True, compile for inference; if False, compile for forward (autograd) + num_fw_outs_saved_for_bw: Number of forward outputs saved for backward (required if not is_inference) + + Before compiling, we run pre_compile for the following wrappers: + - FakifiedOutWrapper + - FunctionalizedRngRuntimeWrapper + After compiling, we run post_compile for the following wrappers: + - EffectTokensWrapper + - AOTDispatchSubclassWrapper + - FunctionalizedRngRuntimeWrapper + - FakifiedOutWrapper + """ + + # Validation + if not is_inference and num_fw_outs_saved_for_bw is None: + raise ValueError( + "num_fw_outs_saved_for_bw must be provided when is_inference=False" + ) + + # Determine grad context, autocast context, tracking mode, compiler + if is_inference: + grad_ctx: Any = nullcontext + autocast_ctx: Any = ( + torch._C._DisableAutocast + if torch._C._is_any_autocast_enabled() + else nullcontext + ) + tracking_mode: str = "inference" + compiler: Any = aot_config.inference_compiler + else: + grad_ctx = torch.no_grad + autocast_ctx = torch._C._DisableAutocast + tracking_mode = "forward" + compiler = aot_config.fw_compiler + + with grad_ctx(), autocast_ctx(), track_graph_compiling(aot_config, tracking_mode): + # Setup wrappers + fakified_out_wrapper = FakifiedOutWrapper() + fakified_out_wrapper.pre_compile( + fw_module, adjusted_flat_args, aot_config, fw_metadata=fw_metadata + ) + + # Initialize RNG wrapper based on mode + functionalized_rng_wrapper = FunctionalizedRngRuntimeWrapper( + return_new_outs=is_inference + ) + + # Add RNG states for forward mode only + if not is_inference and fw_metadata.num_graphsafe_rng_states > 0: + index = fw_metadata.graphsafe_rng_state_index + if index is None: + raise AssertionError( + "fw_metadata.graphsafe_rng_state_index must not be None when num_graphsafe_rng_states > 0" + ) + rng_states = [ + get_cuda_generator_meta_val(index) + for _ in range(fw_metadata.num_graphsafe_rng_states) + ] + adjusted_flat_args.extend(rng_states) # type: ignore[arg-type] + + functionalized_rng_wrapper.pre_compile( + fw_module, adjusted_flat_args, aot_config, fw_metadata=fw_metadata + ) + + # Set tracing context + if tracing_context := torch._guards.TracingContext.try_get(): + tracing_context.fw_metadata = _get_inner_meta( + maybe_subclass_meta, fw_metadata + ) + + with TracingContext.report_output_strides() as fwd_output_strides: + # pyrefly: ignore[not-callable] + compiled_fw_func = compiler(fw_module, adjusted_flat_args) + + # Make boxed if needed + if not getattr(compiled_fw_func, "_boxed_call", False): + compiled_fw_func = make_boxed_func(compiled_fw_func) + + # Set forward output strides if needed + if fakified_out_wrapper.needs_post_compile: + fakified_out_wrapper.set_fwd_output_strides(fwd_output_strides) # type: ignore[arg-type] + + # Apply post-compile wrappers + compiled_fw_func = EffectTokensWrapper().post_compile( + compiled_fw_func, + aot_config, + runtime_metadata=fw_metadata, + ) + + compiled_fw_func = AOTDispatchSubclassWrapper( + fw_only=None, + trace_joint=False, + maybe_subclass_meta=maybe_subclass_meta, + num_fw_outs_saved_for_bw=num_fw_outs_saved_for_bw, + ).post_compile( + compiled_fw_func, + aot_config, + runtime_metadata=fw_metadata, + ) + + compiled_fw_func = functionalized_rng_wrapper.post_compile( + compiled_fw_func, aot_config, runtime_metadata=fw_metadata + ) + + compiled_fw_func = fakified_out_wrapper.post_compile( + compiled_fw_func, + aot_config, + runtime_metadata=fw_metadata, + ) + + return fwd_output_strides, compiled_fw_func diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/indexed_dict.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/indexed_dict.py new file mode 100644 index 0000000000000000000000000000000000000000..748818ff4b16ccf6651a9bdfe7b8064e67ff922e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/indexed_dict.py @@ -0,0 +1,54 @@ +from collections.abc import Iterator, MutableMapping +from typing import Generic, TypeVar + + +K = TypeVar("K") +V = TypeVar("V") + + +# Used for fast next key access (using the fact that the dict is ordered) +# Note: doesn't support deletion but we don't need it! +class IndexedDict(MutableMapping[K, V], Generic[K, V]): + """A dict that maintains insertion order with O(1) index access.""" + + __slots__ = ("_dict", "_keys", "_key_to_index") + + def __init__(self) -> None: + self._dict: dict[K, V] = {} + self._keys: list[K] = [] # typing: ignore[bad-override] + self._key_to_index: dict[K, int] = {} + + def __setitem__(self, key: K, value: V) -> None: + if key not in self._dict: + self._key_to_index[key] = len(self._keys) + self._keys.append(key) + self._dict[key] = value + + def __getitem__(self, key: K) -> V: + return self._dict[key] + + def __delitem__(self, key: K) -> None: + raise NotImplementedError("Deletion not supported for IndexedDict") + + def __len__(self) -> int: + return len(self._dict) + + def __iter__(self) -> Iterator[K]: + return iter(self._keys) + + def __contains__(self, key: object) -> bool: + return key in self._dict + + def next_key(self, key: K) -> K | None: + """Get the next key in insertion order. O(1).""" + idx = self._key_to_index.get(key) + if idx is not None and idx + 1 < len(self._keys): + return self._keys[idx + 1] + return None + + def prev_key(self, key: K) -> K | None: + """Get the previous key in insertion order. O(1).""" + idx = self._key_to_index.get(key) + if idx is not None and idx > 0: + return self._keys[idx - 1] + return None diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/input_output_analysis.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/input_output_analysis.py new file mode 100644 index 0000000000000000000000000000000000000000..0c0721fcb73f5e31650bff9379b56a4ad49e9055 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/input_output_analysis.py @@ -0,0 +1,496 @@ +""" +This module is one of the analysis modules - it takes as input a function or graph +and some preexisting properties, and returns some data that is useful for deciding +how to further proceed with compilation or construct runtime wrappers. + +In particular, the following analyses are provided: +1. Refine the view and mutation metadata collected previously - removing duplicate + inputs or mapping views to their bases. +2. We also analyze the function signature for export graphs. +""" + +import contextlib +import itertools +from typing import Any + +import torch +import torch.utils._pytree as pytree +from torch import Tensor +from torch._C._dynamo.guards import compute_overlapping_tensors +from torch._functorch._aot_autograd.schemas import PlainTensorMeta +from torch._guards import StorageOverlap +from torch._subclasses.functional_tensor import FunctionalTensor +from torch.fx.experimental.symbolic_shapes import is_concrete_int + +from .collect_metadata_analysis import coerce_tangent_and_suggest_memory_format +from .descriptors import AOTInput, InputMutationAOTOutput, TangentAOTInput +from .schemas import ( + AOTConfig, + BackwardSignature, + GraphSignature, + InputAliasInfo, + MemoryFormatMeta, + OutputAliasInfo, + OutputType, + ViewAndMutationMeta, +) +from .utils import strict_zip + + +zip = strict_zip + + +def remove_dupe_metadata( + m: ViewAndMutationMeta, + keep_arg_mask: list[bool], + add_dupe_map: list[int], +) -> ViewAndMutationMeta: + if len(m.input_info) != len(keep_arg_mask): + raise AssertionError( + f"len(m.input_info)={len(m.input_info)} != len(keep_arg_mask)={len(keep_arg_mask)}" + ) + # Easy invariant: the first argument should never be a dupe (it will be kept) + if len(keep_arg_mask) == 0 or not keep_arg_mask[0]: + raise AssertionError( + "keep_arg_mask must be non-empty and keep_arg_mask[0] must be True" + ) + + # Filter dupe'd mutated inputs out of traced_tangents + num_data_mutations = len([x for x in m.input_info if x.mutates_data]) + other_traced_tangents = m.traced_tangents[num_data_mutations:] + inp_traced_tangents = m.traced_tangents[:num_data_mutations] + other_traced_tangents_descs = m.traced_tangents_descs[num_data_mutations:] + inp_traced_tangents_descs = m.traced_tangents_descs[:num_data_mutations] + filtered_inp_traced_tangents = [ + # See Note [Tangents memory format] + x + for i, x in enumerate(inp_traced_tangents) + if keep_arg_mask[m.mutated_inp_runtime_indices[i]] + ] + filtered_inp_traced_tangents_descs = [ + x_desc + for i, x_desc in enumerate(inp_traced_tangents_descs) + if keep_arg_mask[m.mutated_inp_runtime_indices[i]] + ] + traced_tangents = filtered_inp_traced_tangents + other_traced_tangents + traced_tangents_descs = ( + filtered_inp_traced_tangents_descs + other_traced_tangents_descs + ) + + if m.subclass_tangent_meta is None: + raise AssertionError("m.subclass_tangent_meta must not be None") + subclass_tangent_meta = [ + PlainTensorMeta( + 0, memory_format=MemoryFormatMeta(memory_format=torch.contiguous_format) + ) + ] * len(filtered_inp_traced_tangents) + m.subclass_tangent_meta[num_data_mutations:] + + return ViewAndMutationMeta( + input_info=[x for i, x in enumerate(m.input_info) if keep_arg_mask[i]], + # For outputs that are views of inputs, we store the index of the input that the output + # was generated from. Need to update that index to account for removed dupes. + output_info=[ + OutputAliasInfo( + output_type=o.output_type, + raw_type=o.raw_type, + dynamic_dims=o.dynamic_dims, + base_idx=None if o.base_idx is None else add_dupe_map[o.base_idx], + requires_grad=o.requires_grad, + requires_grad_for_backward=o.requires_grad_for_backward, + view_meta_sequence=o.view_meta_sequence, + ) + for o in m.output_info + ], + num_intermediate_bases=m.num_intermediate_bases, + keep_input_mutations=m.keep_input_mutations, + traced_tangents=traced_tangents, + traced_tangents_descs=traced_tangents_descs, + # We are guaranteed not to get here, since dupes are not supported today with subclass inputs. + subclass_inp_meta=[], + subclass_fw_graph_out_meta=[], + subclass_tangent_meta=subclass_tangent_meta, + ) + + +# Given our ViewAndMutation metadata, this fn constructs a new set of metadata, +# after adding synthetic base arguments to the function. +# Most of the work in this fn is slogging through all of the metadata corresponding to inputs, +# and updating it with our synthetic base calling convention. +# +# When config.debug_assert is set, we automatically regenerate the metadata +# and compare it to this output for sanity. +# +# In addition to the updated metadata, also return the list of input indices +# that will need to be updated in the synthetic base epilogue +def create_synthetic_base_metadata( + m: ViewAndMutationMeta, + # Maps each outer argument idx to its inner idx (or, if this outer arg is generated from a + # synthetic base, you get a tuple of (i, TensorMeta), telling you the base tensor idx, and view metadata) + synthetic_base_info: list[int | tuple[int, torch.Tensor]], + outer_args: list[Any], + inner_args: list[Any], + inner_args_desc: list[AOTInput], +) -> tuple[ViewAndMutationMeta, list[int]]: + # maps inner arg indices to outer arg indices + synthetic_base_to_indices: dict[int, list[int]] = {} + for inner_idx in range(len(inner_args)): + outer_aliased_indices_of_current_base_arg = [ + outer_idx + for outer_idx, inner_idx_or_tuple in enumerate(synthetic_base_info) + if (isinstance(inner_idx_or_tuple, int) and inner_idx_or_tuple == inner_idx) + or ( + isinstance(inner_idx_or_tuple, tuple) + and inner_idx_or_tuple[0] == inner_idx + ) + ] + synthetic_base_to_indices[inner_idx] = outer_aliased_indices_of_current_base_arg + + # given the requires_grad info on mutated inputs, + # generate the requires_grad info on those same mutated inputs, but after constructing synthetic bases. + # pyrefly: ignore [implicit-any] + input_infos = [] + for outer_indices in synthetic_base_to_indices.values(): + # leaf-ness should be all-or-nothing for aliased tensor. + # (aka if "a" and "b" are views, then a.is_leaf == b.is_leaf) + any_leaf = any(m.input_info[x].is_leaf for x in outer_indices) + all_leaf = all(m.input_info[x].is_leaf for x in outer_indices) + if any_leaf != all_leaf: + raise AssertionError( + f"any_leaf={any_leaf} != all_leaf={all_leaf} for outer_indices={outer_indices}" + ) + + mutates_data = ( + True + if len(outer_indices) > 1 + else m.input_info[outer_indices[0]].mutates_data + ) + mutates_metadata = ( + False + if len(outer_indices) > 1 + else m.input_info[outer_indices[0]].mutates_metadata + ) + requires_grad = any(m.input_info[x].requires_grad for x in outer_indices) + mutations_under_no_grad_or_inference_mode = all( + m.input_info[x].mutations_under_no_grad_or_inference_mode + for x in outer_indices + ) + + mutation_inductor_storage_resize = all( + m.input_info[x].mutation_inductor_storage_resize for x in outer_indices + ) + + inpt_info = InputAliasInfo( + # If len(outer_indices) > 1, then this input is a synthetic base. + # The invariant is that to the rest of aot autograd, synthetic bases only show up if + # one of their aliases gets a data mutation. And if any of their aliases get metadata + # mutations, they will be hidden from the rest of aot autograd. + mutates_data=mutates_data, + mutates_metadata=mutates_metadata, + mutations_hidden_from_autograd=all( + m.input_info[x].mutations_hidden_from_autograd for x in outer_indices + ), + mutates_storage_metadata=( + False + if len(outer_indices) > 1 + else m.input_info[outer_indices[0]].mutates_storage_metadata + ), + mutations_under_no_grad_or_inference_mode=mutations_under_no_grad_or_inference_mode, + mutation_inductor_storage_resize=mutation_inductor_storage_resize, + is_leaf=any_leaf, + requires_grad=requires_grad, + keep_input_mutations=m.keep_input_mutations, + ) + input_infos.append(inpt_info) + + # Find any inputs that fulfill the following criteria: + # (1) They are part of a synthetic base (because they alias another input, + # and at least one input experiences a data mutation) + # (2) They experience a metadata mutation + outer_aliased_arg_idx_with_metadata_mutations = [ + outer_idx + for outer_idx, inpt_info in enumerate(m.input_info) + if inpt_info.mutates_metadata + and not isinstance(synthetic_base_info[outer_idx], int) + ] + + # grab the original requires grad info on the outputs, except the ones from the mutated inputs + input_metadata_output_info = [ + OutputAliasInfo( + output_type=OutputType.alias_of_input, + raw_type=FunctionalTensor, + dynamic_dims={ + i + for i, s in enumerate(outer_args[outer_idx].shape) + if not is_concrete_int(s) + }, + base_idx=synthetic_base_info[outer_idx][0], # type: ignore[index] + requires_grad=(requires_grad := outer_args[outer_idx].requires_grad), + requires_grad_for_backward=requires_grad, + ) + for outer_idx in outer_aliased_arg_idx_with_metadata_mutations + ] + existing_output_infos = [] + for o in m.output_info: + new_base_idx = ( + None + if o.base_idx is None + else ( + synthetic_base_info[o.base_idx] + if isinstance(synthetic_base_info[o.base_idx], int) + else synthetic_base_info[o.base_idx][0] # type: ignore[index] + ) + ) + # If base_idx is changed for OutputType.is_input, we need to update the output type to reflect the change + new_output_type = ( + OutputType.alias_of_input + if o.output_type == OutputType.is_input and o.base_idx != new_base_idx + else o.output_type + ) + existing_output_infos.append( + OutputAliasInfo( + output_type=new_output_type, + raw_type=o.raw_type, + dynamic_dims=o.dynamic_dims, + # Map the input idx pre-synthetic-bases to the new idx post-synthetic-bases + base_idx=new_base_idx, # type: ignore[arg-type] + requires_grad=o.requires_grad, + requires_grad_for_backward=o.requires_grad_for_backward, + view_meta_sequence=o.view_meta_sequence, + ) + ) + + inner_mutated_tangents_and_memory_formats = [ + # See Note [Tangents memory format] + ( + coerce_tangent_and_suggest_memory_format(x), + TangentAOTInput(InputMutationAOTOutput(x_desc)), + ) + for inner_idx, (x, x_desc) in enumerate(zip(inner_args, inner_args_desc)) + if input_infos[inner_idx].mutates_data and input_infos[inner_idx].requires_grad + ] + inner_mutated_tangents = [ + x[0][0] for x in inner_mutated_tangents_and_memory_formats + ] + inner_mutated_tangents_descs = [ + x[1] for x in inner_mutated_tangents_and_memory_formats + ] + inner_mutated_tangents_memory_formats = [ + x[0][1] for x in inner_mutated_tangents_and_memory_formats + ] + + output_info = existing_output_infos + input_metadata_output_info + # Regenerate traced tangents to include mutated inputs including synthetic bases + traced_tangents = ( + inner_mutated_tangents + m.traced_tangents[len(inner_mutated_tangents) :] + ) + traced_tangents_descs = ( + inner_mutated_tangents_descs + + m.traced_tangents_descs[len(inner_mutated_tangents) :] + ) + if m.subclass_tangent_meta is None: + raise AssertionError("m.subclass_tangent_meta must not be None") + subclass_tangent_meta = [ + # pyrefly: ignore[bad-argument-type] + PlainTensorMeta(0, memory_format=x) + for x in inner_mutated_tangents_memory_formats + ] + m.subclass_tangent_meta[len(inner_mutated_tangents) :] + + return ( + ViewAndMutationMeta( + input_info=input_infos, + output_info=output_info, + num_intermediate_bases=m.num_intermediate_bases, + keep_input_mutations=m.keep_input_mutations, + traced_tangents=traced_tangents, + traced_tangents_descs=traced_tangents_descs, + # We are guaranteed not to get here, since synthetic_base codepaths are not supported today with subclass inputs. + subclass_inp_meta=[], + subclass_fw_graph_out_meta=[], + subclass_tangent_meta=subclass_tangent_meta, + ), + outer_aliased_arg_idx_with_metadata_mutations, + ) + + +def compute_overlapping_inputs( + aot_config: AOTConfig, fwd_inputs: list[Any], aliased_input_indices: list[int] +) -> set[int]: + num_aliases = len(aliased_input_indices) + + shape_env = None + maybe_suppress_guards = contextlib.nullcontext + tracing_context = torch._guards.TracingContext.try_get() + + if tracing_context is not None: + if tracing_context.fake_mode is None: + raise AssertionError("tracing_context.fake_mode must not be None") + shape_env = tracing_context.fake_mode.shape_env + + # Check whether we can actually get the dynamo sources from within AOTAutograd. + if aot_config.aot_autograd_arg_pos_to_source and shape_env is not None: + maybe_suppress_guards = shape_env.suppress_guards # type: ignore[assignment] + + # Check whether there are any symbolic values being used. + # We do this for 2 reasons: + # 1. StorageOverlap guard is only issued whenever dynamic shapes is turned on + # 2. Triggers the fast-path for computing storage overlapping + symbolic = any( + isinstance(x, torch.SymInt) + for i in aliased_input_indices + for x in [ + *fwd_inputs[i].shape, + *fwd_inputs[i].stride(), + fwd_inputs[i].storage_offset(), + ] + ) + + if torch._inductor.config.is_fbcode(): + if symbolic and num_aliases > 400: + from torch._subclasses.fake_tensor import ( + UnsupportedMutationAliasingException, + ) + from torch._utils_internal import justknobs_check + + msg = f"Encountered {num_aliases} dynamic, aliased/mutated inputs, consider setting dynamic=False" + + if justknobs_check( + "pytorch/compiler:aliased_inputs_with_mutation_and_dyn_shapes_killswitch", + False, + ): + raise UnsupportedMutationAliasingException(msg) + + with maybe_suppress_guards(): + aliased_fwd_inputs = [fwd_inputs[i] for i in aliased_input_indices] + actual_aliased_indices = { + aliased_input_indices[i] + for i in compute_overlapping_tensors(aliased_fwd_inputs, symbolic=symbolic) + } + + # Add the StorageOverlap AOTAutograd guard only if we are actually keeping track of + # dynamo sources inside AOTAutograd. + if ( + tracing_context is not None + # Make sure dynamic shapes is currently being used. + and symbolic + # We check that we have more than 1 aliased tensor, which should be true at + # this point, anyway. + and num_aliases > 1 + and aot_config.aot_autograd_arg_pos_to_source + ): + no_overlap_indices = list(set(aliased_input_indices) - actual_aliased_indices) + + overlapping_sources = [ + aot_config.aot_autograd_arg_pos_to_source[i] for i in actual_aliased_indices + ] + non_overlapping_sources = [ + aot_config.aot_autograd_arg_pos_to_source[i] for i in no_overlap_indices + ] + + tracing_context.guards_context.aotautograd_guards.append( + StorageOverlap(overlapping_sources, non_overlapping_sources) + ) + + return actual_aliased_indices + + +def _graph_input_names(gm: torch.fx.GraphModule) -> list[str]: + return [node.name for node in gm.graph.find_nodes(op="placeholder")] + + +def _graph_output_names(gm: torch.fx.GraphModule) -> list[Any]: + output_node = next(iter(reversed(gm.graph.nodes))) + if output_node.op != "output" or len(output_node.args) != 1: + raise AssertionError( + f"expected output node with 1 arg, got op={output_node.op}, args={len(output_node.args)}" + ) + return_args = output_node.args[0] + return [getattr(return_arg, "name", None) for return_arg in return_args] + + +def create_graph_signature( + fx_g: torch.fx.GraphModule, + fw_metadata: ViewAndMutationMeta, + in_spec: pytree.TreeSpec, + out_spec: pytree.TreeSpec, + *, + user_args_flat: list[Tensor], + params_and_buffers_flat: list[Tensor], + param_names: list[str], + buffer_names: list[str], + trace_joint: bool, + num_user_fw_outs: int | None, + loss_index: int | None, +) -> GraphSignature: + # Retrieve graph input names + graph_input_names = _graph_input_names(fx_g) + # Retrieve graph output names + graph_output_names = _graph_output_names(fx_g) + + num_params_buffers = len(param_names) + len(buffer_names) + num_tokens = len(fw_metadata.tokens) + # We have enough restrictions on the graph (no de-duping, synthetic bases, etc), + # Such that # graph inps = # user inps + # params + # buffers + num_user_args = len(graph_input_names) - num_params_buffers - num_tokens + + if trace_joint: + if num_user_fw_outs is None: + raise AssertionError( + "num_user_fw_outs must not be None when trace_joint=True" + ) + num_fw_outs = num_user_fw_outs + fw_metadata.num_mutated_inp_runtime_indices + backward_output_names = graph_output_names[num_fw_outs:] + + grad_index = itertools.count(0) + gradients_to_parameters = { + backward_output_names[next(grad_index)]: param_names[i] + for i, param in enumerate(params_and_buffers_flat) + if param.requires_grad + } + + gradients_to_user_inputs = { + backward_output_names[next(grad_index)]: graph_input_names[ + i + len(params_and_buffers_flat) + ] + for i, user_input in enumerate(user_args_flat) + if user_input.requires_grad + } + + if len(gradients_to_parameters) + len(gradients_to_user_inputs) != len( + backward_output_names + ): + raise AssertionError( + f"len(gradients_to_parameters)={len(gradients_to_parameters)} + " + f"len(gradients_to_user_inputs)={len(gradients_to_user_inputs)} != " + f"len(backward_output_names)={len(backward_output_names)}" + ) + + # Check that we have fully accounted for all graph outputs + if loss_index is None: + raise AssertionError("loss_index must not be None") + backward_signature = BackwardSignature( + gradients_to_parameters, + gradients_to_user_inputs, + graph_output_names[loss_index], + ) + else: + backward_signature = None + num_user_fw_outs = ( + len(graph_output_names) + - fw_metadata.num_mutated_inp_runtime_indices + - num_tokens + ) + + return GraphSignature.from_tracing_metadata( + in_spec=in_spec, + out_spec=out_spec, + graph_input_names=graph_input_names, + graph_output_names=graph_output_names, + view_mutation_metadata=fw_metadata, + named_parameters=param_names, + named_buffers=buffer_names, + num_user_inputs=num_user_args, + num_user_outputs=num_user_fw_outs, + trace_joint=trace_joint, + loss_index=loss_index, + backward_signature=backward_signature, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/logging_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/logging_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..a5dda6c67dfa7804170374e7c737b3bc3ec5eb2e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/logging_utils.py @@ -0,0 +1,166 @@ +""" +Contains utils for logging in AOTAutograd, including managing the names of the graphs under +compilation, capturing user-friendly tracebacks, and debug messages. +""" + +import collections +from collections.abc import Callable, Generator, Iterator +from contextlib import contextmanager +from typing import Any + +import torch +import torch.fx.traceback as fx_traceback + +from .schemas import AOTConfig + + +# This is a list since looking forward, we can have this arbitrarily nested. +graph_being_compiled: list[str] = [] +# TODO: It would be nice to reset the numbering every time aot_id goes +# up, but this is annoying to do right now (because we don't know if +# an aot_id will come back from the dead), so right now this also happens +# to be a globally unique number too (at the cost of wobbling if you change +# how the graphs compile) +nth_graph: int = 0 +model_name: str = "model" + + +def set_model_name(name: str) -> None: + global model_name + model_name = name + + +def get_aot_compilation_context() -> tuple[list[str], str, int]: + return list(graph_being_compiled), model_name, nth_graph + + +def get_aot_graph_name() -> str: + """ + Returns the name of the graph being compiled. + """ + global model_name, graph_being_compiled, nth_graph + return f"{model_name}__{'_'.join(graph_being_compiled)}_{nth_graph}" + + +get_graph_being_compiled = get_aot_graph_name + + +@contextmanager +def track_graph_compiling( + aot_config: AOTConfig, graph_name: str +) -> Generator[None, None, None]: + global graph_being_compiled + # TODO: Don't shove the aot_id in here; set it in the context + graph_being_compiled = [f"{aot_config.aot_id}_{graph_name}"] + old_name = None + if tracing_context := torch._guards.TracingContext.try_get(): + old_name = tracing_context.aot_graph_name + tracing_context.aot_graph_name = graph_being_compiled + has_tracing_context = True + else: + has_tracing_context = False + try: + yield + finally: + global nth_graph + nth_graph += 1 + graph_being_compiled = [] + if has_tracing_context: + if tracing_context := torch._guards.TracingContext.try_get(): + tracing_context.aot_graph_name = old_name + + +# Set up hooks so that during backward the fx's stack_trace is properly set +callback_set = False + + +def setup_stacktrace_preservation_hooks(roots: list[torch.autograd.graph.Node]) -> None: + def iter_graph( + roots: list[torch.autograd.graph.Node], + ) -> Iterator[torch.autograd.graph.Node]: + if not roots: + return + seen = set() + q = collections.deque() + for node in roots: + if node is not None and node not in seen: + seen.add(node) + q.append(node) + + while q: + node = q.popleft() + for fn, _idx in node.next_functions: + if fn in seen or fn is None: + continue + seen.add(fn) + q.append(fn) + + yield node + + def get_callback(saved_stack_: list[str]) -> Callable[[], None]: + def callback() -> None: + global callback_set + fx_traceback.set_stack_trace(saved_stack_) + callback_set = False + + return callback + + def get_prehook(stack_: list[str], seq_nr: int) -> Callable[[Any], None]: + def prehook(grad_output: Any) -> None: + global callback_set + + if not callback_set: + torch.autograd.variable.Variable._execution_engine.queue_callback( # type: ignore[attr-defined] + get_callback(fx_traceback.format_stack()) + ) + callback_set = True + + fx_traceback.set_stack_trace(stack_) + fx_traceback.set_grad_fn_seq_nr(seq_nr) + fx_traceback._mark_autograd_backward() + + return prehook + + def get_posthook( + special_stack_: list[str], seq_nr: int + ) -> Callable[[Any, Any], None]: + def posthook(grad_input: Any, grad_output: Any) -> None: + fx_traceback.set_stack_trace(special_stack_) + fx_traceback.reset_grad_fn_seq_nr() + fx_traceback._reset_autograd_backward() + + return posthook + + for node in iter_graph(roots): + # pyrefly: ignore[missing-attribute] + forward_node_stack = node.metadata.get("traceback_", []) + node.register_prehook(get_prehook(forward_node_stack, node._sequence_nr())) + + special_stack = forward_node_stack.copy() + special_stack.append(fx_traceback.GRADIENT_ACC_SPECIAL_STACK) + node.register_hook(get_posthook(special_stack, node._sequence_nr())) + + +def setup_stacktrace_preservation_hooks_from_tensors(outputs: Any) -> None: + roots = [ + t.grad_fn + for t in (outputs if isinstance(outputs, (list, tuple)) else (outputs,)) + if isinstance(t, torch.Tensor) and t.grad_fn is not None + ] + if roots: + setup_stacktrace_preservation_hooks(roots) + + +def describe_input(i: int, aot_config: AOTConfig) -> str: + if i < aot_config.num_params_buffers: + return f"parameter/buffer {i}" + else: + return f"input {i - aot_config.num_params_buffers}" + + +def format_guard_bug_msg(aot_config: AOTConfig, expected: str) -> str: + return ( + f"At compilation time, graph {aot_config.aot_id} was compiled under the " + f"assumption that {expected}, but at runtime this was not the case. " + "This indicates a guard bug in AOTAutograd or Dynamo, please file a bug to PyTorch." + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/runtime_wrappers.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/runtime_wrappers.py new file mode 100644 index 0000000000000000000000000000000000000000..069ebf615db68eb90d9eae5350b4b269bf418e84 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/runtime_wrappers.py @@ -0,0 +1,3254 @@ +""" +This module defines runtime wrappers, which, based on previous analysis attempts to: +1. process the inputs and outputs +2. apply mutations +3. handle functionalized randomness +4. deduplicate inputs and consolidate views into their bases (see input_output_analysis) +""" + +import builtins +import collections +import contextlib +import copy +import functools +import itertools +import pprint +import typing +import warnings +from collections.abc import Callable, Generator, Sequence +from contextlib import AbstractContextManager, nullcontext +from dataclasses import dataclass, field +from functools import wraps +from typing import Any + +import torch +import torch.fx as fx +import torch.utils.dlpack +from torch import Tensor +from torch._dynamo import config as dynamo_config +from torch._dynamo.callback import callback_handler, CallbackTrigger +from torch._dynamo.utils import CompileEventLogger, dynamo_timed, get_metrics_context +from torch._guards import ( + compile_context, + CompileContext, + detect_fake_mode, + DuplicateInputs, + tracing, + TracingContext, +) +from torch._library.fake_class_registry import FakeScriptObject +from torch._library.opaque_object import is_opaque_type +from torch._library.utils import is_builtin +from torch._logging import getArtifactLogger +from torch._opaque_base import OpaqueBase +from torch._ops import OpOverload +from torch._prims_common import CUDARngStateHelper +from torch._subclasses import FakeTensor +from torch.fx.experimental._backward_state import BackwardState +from torch.fx.experimental.proxy_tensor import HANDLED_TYPES +from torch.multiprocessing.reductions import StorageWeakRef +from torch.types import IntLikeType +from torch.utils._python_dispatch import ( + is_traceable_wrapper_subclass, + TorchDispatchMode, +) +from torch.utils._pytree import tree_flatten + +from .. import config +from .collect_metadata_analysis import run_functionalized_fw_and_collect_metadata +from .descriptors import ( + AOTInput, + AOTOutput, + DummyAOTInput, + MetadataMutationAOTOutput, + SyntheticBaseAOTInput, + ViewBaseAOTInput, +) +from .functional_utils import gen_alias_from_base +from .graph_capture_wrappers import aot_dispatch_subclass +from .input_output_analysis import ( + compute_overlapping_inputs, + create_synthetic_base_metadata, + remove_dupe_metadata, +) +from .logging_utils import describe_input, format_guard_bug_msg, track_graph_compiling +from .schemas import ( + AOTConfig, + CompilerWrapper, + FxValue, + InductorWrapper, + InputAliasInfo, + MemoryFormatMeta, + MutationType, + OpaqueMeta, + OutputType, + PlainTensorMeta, + SubclassCreationMeta, + SubclassMeta, + TensorAlias, + TraceFn, + ViewAndMutationMeta, +) +from .subclass_utils import ( + requires_subclass_dispatch, + runtime_unwrap_tensor_subclasses, + wrap_tensor_subclasses, +) +from .utils import ( + call_and_expect_output_descs, + call_func_at_runtime_with_args, + make_boxed_func, + partial_flatten_asdict, + simple_wraps, + strict_zip, + without_output_descs, +) + + +def _unwrap_tensor_subclasses_no_symints( + args: list[Any], +) -> list[Any]: + return runtime_unwrap_tensor_subclasses(args, append_symints=False) # type: ignore[arg-type] + + +zip = strict_zip + +aot_graphs_log = getArtifactLogger(__name__, "aot_graphs") + + +def _unwrap_no_symints(args: list[Any]) -> list[Any]: + return runtime_unwrap_tensor_subclasses(args, append_symints=False) + + +def _describe_arg_for_logging(arg: object) -> str: + from torch._library import opaque_object + + try: + is_dtensor = isinstance(arg, torch.distributed.tensor.DTensor) + except AttributeError: + is_dtensor = False + + if is_dtensor: + arg = typing.cast(torch.distributed.tensor.DTensor, arg) + mesh = arg.device_mesh + return ( + f"DTensor(shape={arg.shape}, dtype={arg.dtype}, " + f"device={arg.device}, mesh_shape={mesh.shape}, " + f"placements={arg.placements})" + ) + elif isinstance(arg, torch.Tensor): + return f"Tensor(shape={arg.shape}, dtype={arg.dtype}, device={arg.device})" + elif opaque_object.is_opaque_type(type(arg)): + return f"Opaque: {type(arg).__name__}" + else: + return f"{type(arg).__name__}: {arg}" + + +def _log_input_metadata(runtime_metadata: ViewAndMutationMeta) -> None: + aot_graphs_log.debug( + "Expected input metadata (count=%s):", len(runtime_metadata.subclass_inp_meta) + ) + for i, meta in enumerate(runtime_metadata.subclass_inp_meta): + aot_graphs_log.debug(" [%s] %s", i, meta) + + +def _log_args_list(args: Sequence[object], label: str) -> None: + aot_graphs_log.debug("%s (count=%s):", label, len(args)) + for i, arg in enumerate(args): + aot_graphs_log.debug(" [%s] %s", i, _describe_arg_for_logging(arg)) + + +def _log_args_maybe_list(arg: object, label: str) -> None: + if isinstance(arg, (list, tuple)): + _log_args_list(arg, label) + else: + aot_graphs_log.debug("%s: %s", label, _describe_arg_for_logging(arg)) + + +# The wrapper created by this function handles all of the runtime aliasing and mutation "epilogue" logic +# that needs to run after the compiled function. +# +# This function accepts a trace_joint flag, indicating whether or not we're generating the runtime +# epilogue for a forward-only inference graph, or for an autograd.Function.apply function. +# This is because there are some minor differences in how we treat these cases at runtime: +# - resize_() is currently handled in the inference case, but not fully handled in the autograd case. +# - the autograd cases inserts TensorAlias wrapper objects for outputs that alias inputs +@dataclass +class RuntimeWrapper(CompilerWrapper): + indices_of_inps_to_detach: list[int] + trace_joint: bool + disable_amp: bool + + def post_compile( + self, + compiled_fn: Callable[..., Any], + aot_config: AOTConfig, + *, + runtime_metadata: ViewAndMutationMeta, + ) -> Callable[..., Any]: + return _create_runtime_wrapper( + compiled_fn, + runtime_metadata=runtime_metadata, + indices_of_inps_to_detach=self.indices_of_inps_to_detach, + trace_joint=self.trace_joint, + keep_input_mutations=aot_config.keep_inference_input_mutations, + disable_amp=self.disable_amp, + ) + + +class NoopAliasHandler: + def __init__( + self, info: Any, runtime_metadata: ViewAndMutationMeta, trace_joint: bool + ) -> None: + pass + + def __call__(self, orig_inputs: list[Any], fw_outs: list[Any], out: Any) -> Any: + return out + + +def _unwrap_tensoralias(x: TensorAlias) -> torch.Tensor: + if not isinstance(x, TensorAlias): + raise AssertionError(f"expected TensorAlias, got {type(x)}") + return x.alias + + +def _identity(x: Any) -> Any: + return x + + +class AliasOfInputHandler: + def __init__( + self, info: Any, runtime_metadata: ViewAndMutationMeta, trace_joint: bool + ) -> None: + self.base_idx = info.base_idx + self.unwrap_out = _unwrap_tensoralias if trace_joint else _identity + self.requires_grad = info.requires_grad + self.view_meta_sequence = info.view_meta_sequence + self.replay_views = config.view_replay_for_aliased_outputs + + def __call__( + self, orig_inputs: list[Any], fw_outs: list[Any], out: Any + ) -> torch.Tensor: + aliased_base_tensor = orig_inputs[self.base_idx] + return gen_alias_from_base( + aliased_base_tensor, + self.unwrap_out(out), + self.requires_grad, + self.view_meta_sequence, + replay_views=self.replay_views, + ) + + +class IsInputHandler: + def __init__( + self, info: Any, runtime_metadata: ViewAndMutationMeta, trace_joint: bool + ) -> None: + self.base_idx = info.base_idx + self.unwrap_out = _unwrap_tensoralias if trace_joint else _identity + + def __call__( + self, orig_inputs: list[Any], fw_outs: list[Any], out: Any + ) -> torch.Tensor: + aliased_base_tensor = orig_inputs[self.base_idx] + return aliased_base_tensor + + +class AliasOfIntermediateHandler: + def __init__( + self, info: Any, runtime_metadata: ViewAndMutationMeta, trace_joint: bool + ) -> None: + self._unwrap_aliased_base_tensor = _identity + if info.output_type in ( + OutputType.alias_of_intermediate, + OutputType.alias_of_intermediate_save_as_output, + ): + num_user_outputs = len(runtime_metadata.output_info) + self.base_idx = info.base_idx + num_user_outputs + else: + self.base_idx = info.base_idx + if self.base_idx in runtime_metadata.aliased_out_indices: + self._unwrap_aliased_base_tensor = _unwrap_tensoralias + + self.unwrap_out = _unwrap_tensoralias if trace_joint else _identity + self.requires_grad = info.requires_grad + self.view_meta_sequence = info.view_meta_sequence + self.replay_views = config.view_replay_for_aliased_outputs + + def __call__( + self, orig_inputs: list[Any], fw_outs: list[Any], out: Any + ) -> torch.Tensor: + aliased_base_tensor = fw_outs[self.base_idx] + return gen_alias_from_base( + self._unwrap_aliased_base_tensor(aliased_base_tensor), + self.unwrap_out(out), + self.requires_grad, + self.view_meta_sequence, + replay_views=self.replay_views, + ) + + +_HANDLER_MAP = { + OutputType.non_alias: NoopAliasHandler, + OutputType.unsafe_view_alias: NoopAliasHandler, + OutputType.custom_function_view: NoopAliasHandler, + OutputType.alias_of_input: AliasOfInputHandler, + OutputType.is_input: IsInputHandler, + OutputType.alias_of_intermediate: AliasOfIntermediateHandler, + OutputType.alias_of_intermediate_save_as_output: AliasOfIntermediateHandler, + OutputType.alias_of_intermediate_base_is_user_output: AliasOfIntermediateHandler, +} + + +def make_output_handler( + info: Any, runtime_metadata: ViewAndMutationMeta, trace_joint: bool +) -> Any: + handler_type = _HANDLER_MAP[info.output_type] + return handler_type(info, runtime_metadata, trace_joint) + + +# not sure why AOTDispatcher needs to manually set this +def maybe_mark_dynamic_helper(t: torch.Tensor, dims: set[int]) -> None: + if hasattr(t, "_dynamo_weak_dynamic_indices"): + # pyrefly: ignore [missing-attribute] + t._dynamo_weak_dynamic_indices |= dims + else: + t._dynamo_weak_dynamic_indices = dims.copy() # type: ignore[attr-defined] + + +def _should_disable_saved_tensors_hooks() -> bool: + # Compiled autograd is not supported yet, to be added in future. + if torch._dynamo.compiled_autograd.in_compiled_autograd_region: + return False + + get_hooks = torch._functorch._aot_autograd.utils.top_saved_tensors_hooks + are_inline_hooks = ( + torch._functorch._aot_autograd.utils.saved_tensors_hooks_are_inlineable + ) + + hooks = get_hooks() + if are_inline_hooks(hooks): + return True + + return False + + +def _schema_allows_aliasing(func: Any) -> bool: + schema = func._schema + # View ops have non-write aliases declared in arguments + if schema._is_view_op(): + return True + # Handles cases like mkldnn::_convolution_pointwise_.binary + # where the schema is Tensor(a!) other -> Tensor(a!) Y + for ret in schema.returns: + if ret.alias_info is not None: + return True + return False + + +def _check_custom_op_aliasing( + name: str, args: tuple[Any, ...], kwargs: dict[str, Any], result: Any +) -> None: + """ + Check if custom op outputs alias inputs or other outputs. + If config.error_on_custom_op_aliasing is True, raises RuntimeError. + Otherwise, emits a warning. + """ + try: + torch._library.utils._c_check_aliasing_constraint( + name, + args, + kwargs, + result, + ) + except RuntimeError as e: + if config.error_on_custom_op_aliasing: + raise + else: + msg = f"{e} This is deprecated and will become an error in PyTorch 2.12." + warnings.warn(msg, UserWarning, stacklevel=3) + + +@functools.lru_cache(None) +def _is_fsdp_all_gather_copy_in(func: Any) -> bool: + """ + Check if func is torch.ops.fsdp.all_gather_copy_in.default by comparing + namespace and name strings. This avoids accessing torch.ops.fsdp directly, + which would fail on platforms where FSDP ops aren't registered (e.g., macOS + builds with USE_DISTRIBUTED=0). + """ + return ( + hasattr(func, "namespace") + and func.namespace == "fsdp" + and hasattr(func, "__name__") + and func.__name__ == "all_gather_copy_in.default" + ) + + +class _AnalyzeCustomOpInputOutputMode(TorchDispatchMode): + """ + Checks if inp/out of custom ops alias each other. + If config.error_on_custom_op_aliasing is True, violations raise errors. + Otherwise, violations emit warnings. + """ + + def __init__(self) -> None: + super().__init__() + self.supports_higher_order_operators = True + + def __torch_dispatch__( + self, + func: OpOverload, + types: Any, + args: tuple[Any, ...] = (), + kwargs: dict[str, Any] | None = None, + ) -> Any: + if not kwargs: + kwargs = {} + + flat_tensor_args = filter( + lambda x: isinstance(x, torch.Tensor), tree_flatten((args, kwargs))[0] + ) + + # Defer this to subclass torchdispatch modes (probably shouldn't have fake tensor here tho) + # For Parameters, we need to check the underlying tensor type, not the Parameter itself + for tensor in flat_tensor_args: + underlying_tensor = tensor + if isinstance(tensor, torch.nn.Parameter): + underlying_tensor = tensor.data + if type(underlying_tensor) not in HANDLED_TYPES: + return NotImplemented + + res = func(*args, **kwargs) + # Only check aliasing for custom ops (non-aten/prim/prims/_c10d_functional/c10d) + # that claim to be functional + # Skip ops whose schema declares aliasing is allowed + if ( + not isinstance(func, torch._ops.HigherOrderOperator) + and not is_builtin(func) + # TODO (https://github.com/pytorch/pytorch/issues/170986) + and func.namespace not in ("_c10d_functional", "c10d", "onednn") + # This op is quite important but has wrong schema, so lets skip for now + and not _is_fsdp_all_gather_copy_in(func) + and not _schema_allows_aliasing(func) + ): + _check_custom_op_aliasing( + func.name(), + args, + kwargs, + res, + ) + return res + + @classmethod + def ignore_compile_internals(cls) -> bool: + return True + + +class _FirstInvocationContext: + """ + Context manager that tracks first invocation and conditionally enables _AnalyzeCustomOpInputOutputMode. + This is useful when we have a custom op where we want to analyze its' input + and output during cold start. + """ + + def __init__(self) -> None: + self._is_first = True + + def __call__(self) -> AbstractContextManager[Any]: + """ + Returns a context manager: _AnalyzeCustomOpInputOutputMode on first invocation, nullcontext thereafter. + Automatically updates state after first use. + """ + # NB: Don't run the analyzer when you're forcing compile during FX + # tracing, as the analyzer doesn't play nicely when it's being + # make_fx'ed through + if ( + self._is_first + and config.check_custom_op_aliasing + and not torch._dynamo.config.force_compile_during_fx_trace + ): + self._is_first = False + return _AnalyzeCustomOpInputOutputMode() + return nullcontext() + + +@dataclass +class _RuntimeCompiledFnInvoker: + compiled_fn: Callable[..., Any] + indices_of_inps_to_detach: list[int] + trace_joint: bool + disable_amp: bool + first_invocation_ctx: _FirstInvocationContext = field( + default_factory=_FirstInvocationContext + ) + + def __post_init__(self) -> None: + if not getattr(self.compiled_fn, "_boxed_call", False): + self.compiled_fn = make_boxed_func(self.compiled_fn) + + def run(self, args: list[Any], *, on_before_call: Callable[[], None]) -> list[Any]: + with self.first_invocation_ctx(): + if self.trace_joint: + args_ = list(args) + # See Note [Detaching inputs that never need gradients] + for idx in self.indices_of_inps_to_detach: + if isinstance(args_[idx], torch.Tensor): + args_[idx] = args_[idx].detach() + + # It's possible to have trace_joint inside user specified with no_grad() region, + # if there is a nested with enable_grad(), that forces some outputs to require gradients. + # Therefore, we unconditionally turn on enable_grad() for compiled_fn execution. + with ( + torch.autograd._force_original_view_tracking(True), + torch.enable_grad(), + ): + on_before_call() + return call_func_at_runtime_with_args( + self.compiled_fn, + args_, + disable_amp=self.disable_amp, + steal_args=True, + ) + + # When we have an inference graph, we run with grad disabled. + # It's possible to get an inference graph with inputs that require grad, + # in which case we want to make sure autograd is disabled + # (since e.g., inductor will generate aten.addmm.out calls which autograd will complain on) + # NOTE: We use _set_grad_enabled directly to reduce runtime overhead + grad_enabled = torch.is_grad_enabled() + try: + if grad_enabled: + torch._C._set_grad_enabled(False) + on_before_call() + return call_func_at_runtime_with_args( + self.compiled_fn, + args, + disable_amp=self.disable_amp, + steal_args=True, + ) + finally: + if grad_enabled: + torch._C._set_grad_enabled(True) + + +@dataclass +class _RuntimeForwardEpilogue: + runtime_metadata: ViewAndMutationMeta + trace_joint: bool + keep_input_mutations: bool + epilogue_args_idx: tuple[int, ...] = field(init=False) + output_handlers: tuple[Any, ...] = field(init=False) + + def __post_init__(self) -> None: + epilogue_args_idx = list(self.runtime_metadata.mutated_inp_runtime_indices) + for info in self.runtime_metadata.output_info: + if ( + info.output_type == OutputType.alias_of_input + or info.output_type == OutputType.is_input + ): + if not isinstance(info.base_idx, int): + raise AssertionError( + f"expected info.base_idx to be int, got {type(info.base_idx)}" + ) + epilogue_args_idx.append(info.base_idx) + self.epilogue_args_idx = tuple(epilogue_args_idx) + + if config.unlift_effect_tokens: + if len(self.runtime_metadata.tokens) != 0: + raise AssertionError( + "expected no tokens when unlift_effect_tokens is True, " + f"got {len(self.runtime_metadata.tokens)}" + ) + + if self.runtime_metadata.num_outputs_aliased > 0: + self.output_handlers = tuple( + make_output_handler(info, self.runtime_metadata, self.trace_joint) + for info in self.runtime_metadata.output_info + ) + else: + self.output_handlers = () + + def capture_orig_inputs(self, args: list[Any]) -> dict[int, Any]: + return {i: args[i] for i in self.epilogue_args_idx} + + def increment_mutation_versions(self, args: list[Any]) -> None: + if self.keep_input_mutations: + mutated_args = ( + args[i] + for i in self.runtime_metadata.mutated_graph_handled_indices_seen_by_autograd + ) + torch.autograd.graph.increment_version(mutated_args) + + def finalize(self, orig_inputs: dict[int, Any], all_outs: list[Any]) -> Any: + self._validate_compiled_output_arity(all_outs) + updated_inputs, fw_outs = self._split_mutated_inputs(all_outs) + if updated_inputs is not None: + self._apply_input_mutations(orig_inputs, updated_inputs) + + ret_outs = self._replay_output_aliases(orig_inputs, fw_outs) + if self.runtime_metadata.dynamic_outputs: + for t, o in zip(ret_outs, self.runtime_metadata.output_info): + if o.dynamic_dims is None: + continue + maybe_mark_dynamic_helper(t, o.dynamic_dims) + if self.runtime_metadata.grad_enabled_mutation is not None: + torch._C._set_grad_enabled(self.runtime_metadata.grad_enabled_mutation) + return ret_outs + + def _validate_compiled_output_arity(self, all_outs: list[Any]) -> None: + expected_outs = ( + self.runtime_metadata.num_mutated_inp_runtime_indices + + self.runtime_metadata.num_outputs + + self.runtime_metadata.num_intermediate_bases + ) + if len(all_outs) != expected_outs: + raise AssertionError( + f"expected {expected_outs} outputs, got {len(all_outs)}" + ) + + def _split_mutated_inputs( + self, all_outs: list[Any] + ) -> tuple[list[Any] | None, list[Any]]: + num_mutated_runtime_inps = self.runtime_metadata.num_mutated_inp_runtime_indices + if num_mutated_runtime_inps == 0: + return None, all_outs + return ( + all_outs[:num_mutated_runtime_inps], + all_outs[num_mutated_runtime_inps:], + ) + + def _apply_input_mutations( + self, orig_inputs: dict[int, Any], updated_inputs: list[Any] + ) -> None: + for i, inpt_idx in enumerate(self.runtime_metadata.mutated_inp_runtime_indices): + meta = self.runtime_metadata.input_info[inpt_idx] + if not meta.mutates_data and not meta.mutates_metadata: + continue + original_inpt = orig_inputs[inpt_idx] + updated_inpt = updated_inputs[i] + if meta.mutates_storage_metadata: + # See Note [set_() Input Mutations in AOTAutograd] + # mutates_storage_metadata means our input saw a x.set_(y) call. + # What if x **also** saw a data and/or a metadata mutation? + # (1) If the [meta]data mutation occurred after the set_(), + # then there is no need to copy_() the data. + # When we perform x.set_(x_updated), we are guaranteed that + # x_updated already has the final version of the data/metadata + # (2) If a data mutation occurred before the set_(). + # This case seems very difficult to support. + # TODO: discuss on the PR and decide if we want to tr to + # either support it, or detect and ban it. + if self.trace_joint: + if not isinstance(updated_inpt, TensorAlias): + raise AssertionError( + f"expected TensorAlias for updated_inpt, got {type(updated_inpt)}" + ) + updated_inpt = updated_inpt.alias + with torch.no_grad(): + original_inpt.set_(updated_inpt) + continue + if meta.mutates_metadata and not meta.mutates_data: + if self.trace_joint: + if not isinstance(updated_inpt, TensorAlias): + raise AssertionError( + f"expected TensorAlias for updated_inpt, got {type(updated_inpt)}" + ) + updated_inpt = updated_inpt.alias + # We need to grab the size/stride/storage_offset from the compiled forward, + # and use that to mutate the metadata of the input + original_inpt.as_strided_( + updated_inpt.size(), + updated_inpt.stride(), + updated_inpt.storage_offset(), + ) + else: + if meta.mutates_data and meta.mutates_metadata: + original_inpt.as_strided_( + updated_inpt.size(), + updated_inpt.stride(), + updated_inpt.storage_offset(), + ) + else: + if not meta.mutates_data: + raise AssertionError("expected meta.mutates_data to be True") + if meta.is_leaf and original_inpt.requires_grad: + # We can hit this situation in this case: + # def f(x): + # x.detach().mul_(2) + # return x + 1 + # AOTAutograd will see a mutation in the above case, and try to + # apply a copy_() here, in the epilogue. + # But if x required gradients, and is a leaf, then autograd + # will yell at us for trying to mutate it. + # However, it's only possible to end up in this scenario (like the above) + # if all of the mutations to the leaf input were non-autograd-tracking mutations + # (aka mutations under no_grad(), or on detached views). + # In that case, we fully want to hide the mutation from autograd, so detaching is ok. + original_inpt.detach().copy_(updated_inpt) + else: + # Check if we have stream index information for this mutated input + if ( + self.runtime_metadata.mutated_inp_stream_indices is not None + and i < len(self.runtime_metadata.mutated_inp_stream_indices) + and self.runtime_metadata.mutated_inp_stream_indices[i] + is not None + ): + raise RuntimeError( + "Mutations on inputs with user-specified streams are not yet supported. " + "See: https://github.com/pytorch/pytorch/issues/172522" + ) + original_inpt.copy_(updated_inpt) + + def _replay_output_aliases( + self, orig_inputs: dict[int, Any], fw_outs: list[Any] + ) -> Any: + if self.runtime_metadata.num_outputs_aliased == 0: + return fw_outs + + # The compiled forward also returned intermediate bases. We don't want to return them to the user. + expect_num_outputs = ( + len(self.output_handlers) + self.runtime_metadata.num_intermediate_bases + ) + if len(fw_outs) != expect_num_outputs: + raise AssertionError( + f"expected {expect_num_outputs} fw_outs, got {len(fw_outs)}" + ) + return [ + handler(orig_inputs, fw_outs, out) + for out, handler in builtins.zip(fw_outs, self.output_handlers) + ] + + +def _create_runtime_wrapper( + compiled_fn: Callable[..., Any], + *, + runtime_metadata: ViewAndMutationMeta, + indices_of_inps_to_detach: list[int], + trace_joint: bool, + keep_input_mutations: bool, + disable_amp: bool, +) -> Callable[..., Any]: + compiled_invoker = _RuntimeCompiledFnInvoker( + compiled_fn=compiled_fn, + indices_of_inps_to_detach=indices_of_inps_to_detach, + trace_joint=trace_joint, + disable_amp=disable_amp, + ) + runtime_epilogue = _RuntimeForwardEpilogue( + runtime_metadata=runtime_metadata, + trace_joint=trace_joint, + keep_input_mutations=keep_input_mutations, + ) + + def record_runtime_wrapper_prologue_enter() -> AbstractContextManager[None] | None: + if ( + torch.autograd.profiler._is_profiler_enabled + and dynamo_config.record_runtime_overhead + ): + cm = torch._C._profiler._RecordFunctionFast( + "AOTDispatcher Runtime Wrapper Prologue" + ) + cm.__enter__() + return cm + return None + + def record_runtime_wrapper_prologue_exit( + cm: AbstractContextManager[None] | None, + ) -> None: + if cm is not None: + cm.__exit__(None, None, None) + + @simple_wraps(compiled_invoker.compiled_fn) + def runtime_wrapper(args: list[Any]) -> Any: + # Create context manager for profiler + cm = record_runtime_wrapper_prologue_enter() + prologue_exited = False + + def exit_prologue() -> None: + nonlocal prologue_exited + if not prologue_exited: + record_runtime_wrapper_prologue_exit(cm) + prologue_exited = True + + try: + # stash a ref to each input tensor we plan to use after the compiled function + orig_inputs = runtime_epilogue.capture_orig_inputs(args) + runtime_epilogue.increment_mutation_versions(args) + all_outs = compiled_invoker.run(args, on_before_call=exit_prologue) + finally: + exit_prologue() + + del args + return runtime_epilogue.finalize(orig_inputs, all_outs) + + if not (trace_joint and _should_disable_saved_tensors_hooks()): + return runtime_wrapper + + # Disabling saved tensors hooks + @simple_wraps(runtime_wrapper) + def _runtime_wrapper(*args: Any, **kwargs: Any) -> Any: + with _disable_saved_tensors_hooks(): + return runtime_wrapper(*args, **kwargs) + + return _runtime_wrapper + + +# WARNING: this does NOT operate on TraceFn +@dataclass +class FunctionalizedRngRuntimeWrapper(InductorWrapper): + # TODO: I would love to get rid of this argument, but it's + # Wrapped pretty tightly around our aot_dispatch_autograd logic. + # Specifically, tensors_saved_for_backwards_slice's value is both used for calculating indices + # for setting placeholder strides(which is done before runtime, before this wrapper runs) + # and for saving tensors for backward (which is done during runtime, after this wrapper runs) + # So in aot_dispatch_autograd, this wrapper can't edit the set of outs without making one + # of those two indices incorrect. + return_new_outs: bool = True + + def pre_compile( + self, + fw_module: torch.fx.GraphModule, + flat_args: list[Any], + aot_config: AOTConfig, + *, + fw_metadata: ViewAndMutationMeta, + ) -> None: + if config.functionalize_rng_ops: + # Update example inputs for the fw_compiler + fake_mode = detect_fake_mode() + if fake_mode is None: + raise AssertionError( + "fake_mode must not be None when functionalize_rng_ops is True" + ) + seed, offset = CUDARngStateHelper.get_torch_state_as_tuple(fake_mode) + flat_args.extend([seed, offset]) + # We are not clearing flat_args here because + # 1) There is a check in the debug compiler at the end + # 2) It does not matter as these are fake tensors + + def post_compile( + self, + compiled_fn: Callable[..., Any], + aot_config: AOTConfig, + *, + runtime_metadata: ViewAndMutationMeta, + ) -> Callable[..., Any]: + @wraps(compiled_fn) + def wrapper(runtime_args: list[Any]) -> Any: + if runtime_metadata.is_rng_op_functionalized: + # Add the seed and offset to args + seed, offset = CUDARngStateHelper.get_torch_state_as_tuple() + runtime_args.extend([seed, offset]) + out = compiled_fn(runtime_args) + out = self._functionalized_rng_runtime_epilogue( + runtime_metadata, + out, + # TODO: this won't be right for the backward when we convert the call_compiled_backward to use the wrapper + runtime_metadata.num_forward_returns, + ) + return out + return compiled_fn(runtime_args) + + return wrapper + + # Calling convention: If we are running functionalized RNG, then outs consists + # of (user_outs, rng_offset) + def _functionalized_rng_runtime_epilogue( + self, + metadata: ViewAndMutationMeta, + outs: Any, + offset_index: int, + ) -> Any: + if metadata.is_rng_op_functionalized: + if metadata.num_outputs_rng_offset != 1: + raise AssertionError( + f"expected num_outputs_rng_offset == 1, got {metadata.num_outputs_rng_offset}" + ) + new_rng_offset = outs[offset_index] + CUDARngStateHelper.set_new_offset(new_rng_offset) + if self.return_new_outs: + user_outs = outs[:offset_index] + outs[offset_index + 1 :] + return user_outs + else: + return outs + + return outs + + +# WARNING: this does NOT operate on TraceFn +@dataclass +class FakifiedOutWrapper(InductorWrapper): + out_metas: list[torch.Tensor] = field(default_factory=list) + # TracingContext.fwd_output_strides + # Generated from actually doing compile + # NB: an entry is None if it's not a Tensor + fwd_output_strides: list[list[int] | None] | None = None + needs_post_compile: bool = True + + def pre_compile( + self, + fw_module: fx.GraphModule, # Must be fw_module from aot_dispatch_*_graph + flat_args: list[Any], + aot_config: AOTConfig, + *, + fw_metadata: ViewAndMutationMeta, + ) -> None: + tracing_context = torch._guards.TracingContext.try_get() + if tracing_context and tracing_context.fakify_first_call: + self.out_metas = [ + n.meta["val"] for n in (list(fw_module.graph.nodes)[-1].args[0]) + ] + else: + self.needs_post_compile = False + + def _compute_output_meta_with_inductor_strides(self) -> list[torch.Tensor]: + out = self.out_metas + fwd_output_strides = self.fwd_output_strides + if not fwd_output_strides: + return out + + from torch.fx.experimental.symbolic_shapes import statically_known_true + + for i in range(len(out)): + if not isinstance(out[i], Tensor): + continue + strides = fwd_output_strides[i] + # fwd_output_strides is best effort by Inductor. When an output + # Tensor has unbacked SymInts, Inductor may sometimes be unable + # to compute what the output stride would be. If Inductor doesn't + # have any clear direction on the layout, we don't have to run + # as_strided. To repro without this, run: + # + # python test/distributed/test_dynamo_distributed.py + # TestFakeDistributedSingleProc.test_unbacked_symbol_splitting_no_binding + if strides is None: + continue + if all( + statically_known_true(s1 == s2) + for s1, s2 in zip(out[i].stride(), strides) + ): + continue + out[i] = out[i].as_strided(out[i].shape, strides) + return out + + # To be called post compile + def set_fwd_output_strides( + self, fwd_output_strides: list[list[int] | None] + ) -> None: + self.fwd_output_strides = fwd_output_strides + + def post_compile( + self, + compiled_fn: Callable[..., Any], + aot_config: AOTConfig, + *, + runtime_metadata: ViewAndMutationMeta, + ) -> Callable[..., Any]: + if self.needs_post_compile: + if self.fwd_output_strides is None: + raise AssertionError( + "fwd_output_strides must not be None when needs_post_compile is True" + ) + fakified_out = self._compute_output_meta_with_inductor_strides() + + @wraps(compiled_fn) + def wrapper(runtime_args: list[Any]) -> Any: + nonlocal fakified_out + if fakified_out is not None: + out = fakified_out + fakified_out = None + return out + return compiled_fn(runtime_args) + + return wrapper + # If we don't need to fakify, we can just return the original compiled function + return compiled_fn + + +# This wrapper handles the AOTDispatch runtime logic for tensor subclasses. +# At runtime, we have a compiled function that knows how to operate on the domain of DenseTensor -> DenseTensor, +# But the user might have passed us some tensor subclass inputs (or expect some subclass tensor outputs). +# This function handles the wrapping and unwrapping of tensor subclasses at runtime. +@dataclass +class AOTDispatchSubclassWrapper(CompilerWrapper): + trace_joint: bool + fw_only: Callable[..., Any] | None # Not cached, only used in pre_compile + maybe_subclass_meta: SubclassMeta | None + num_fw_outs_saved_for_bw: int | None + + def pre_compile( + self, + flat_fn: TraceFn, + flat_args: list[FxValue], + flat_args_descs: list[AOTInput], + aot_config: AOTConfig, + *, + fw_metadata: ViewAndMutationMeta, + ) -> tuple[TraceFn, list[FxValue], list[AOTInput], ViewAndMutationMeta]: + (new_flat_fn, new_flat_args, new_flat_args_descs, subclass_meta) = ( + aot_dispatch_subclass( + flat_fn, + flat_args, + flat_args_descs, + is_joint_structure=self.trace_joint, + meta=fw_metadata, + fw_only=self.fw_only, # type: ignore[arg-type] + ) + ) + self.maybe_subclass_meta = subclass_meta + return new_flat_fn, new_flat_args, new_flat_args_descs, fw_metadata + + @staticmethod + def _get_frozen_inp_indices() -> frozenset[int]: + # fw_compiler_freezing (compile_fx.py) bakes frozen params into the + # graph and sets their TracingContext.params_flat entries to None + # before post_compile runs. We pass these indices to codegen so it + # can emit straight-line code instead of a runtime None check. + tc = TracingContext.try_get() + if tc is None or tc.params_flat is None: + return frozenset() + return frozenset(i for i, p in enumerate(tc.params_flat) if p is None) + + def post_compile( + self, + compiled_fn: Callable[..., Any], + aot_config: AOTConfig, + *, + runtime_metadata: ViewAndMutationMeta, + ) -> Callable[..., Any]: + if self.maybe_subclass_meta is None and not runtime_metadata.act_input_indices: + return compiled_fn + + from .subclass_codegen import codegen_subclass_wrapper + + inner_fn = codegen_subclass_wrapper( + compiled_fn=compiled_fn, + inp_metas=runtime_metadata.subclass_inp_meta, + out_metas=runtime_metadata.subclass_fw_graph_out_meta, + num_fw_outs_saved_for_bw=self.num_fw_outs_saved_for_bw, + frozen_inp_indices=self._get_frozen_inp_indices(), + act_input_indices=runtime_metadata.act_input_indices, + ) + inner_fn._boxed_call = True # type: ignore[attr-defined] + return inner_fn + + +@dataclass +class EffectTokensWrapper(CompilerWrapper): + def post_compile( + self, + compiled_fn: Callable[..., Any], + aot_config: AOTConfig, + *, + runtime_metadata: ViewAndMutationMeta, + ) -> Callable[..., Any]: + num_tokens = len(runtime_metadata.tokens) + + @wraps(compiled_fn) + def inner_fn(args: list[Any]) -> Any: + if num_tokens > 0: + # Pass in forward effect tokens (See Note [Side-Effectful Tokens in AOTAutograd]) + old_args = args + args = [*([None] * num_tokens), *args] + old_args.clear() + + outs = compiled_fn(args) + + # Inductor cache DummyModule can return None + if outs is None: + return None + # Toss out the effect tokens (See Note [Side-Effectful Tokens in AOTAutograd]) + return outs[num_tokens:] if num_tokens != 0 else outs + + # box it + inner_fn._boxed_call = True # type: ignore[attr-defined] + return inner_fn + + +# MOTIVATION: +# +# When tracing functions for future execution, one must be careful not to pass +# in the same input tensor multiple times (e.g., f(x, x), as this can result +# in graphs that are ONLY valid if you later pass a new tensor in exactly the +# same way (e.g., f(y, y)). (NB: we really mean duplicate; two distinct +# tensors that alias each other is a different situation that is covered by +# aot_dispatch_deduplicated_autograd). Here are two examples: +# +# (1) Suppose you have a function: +# +# def f(x, y): +# return x + y +# +# If you make_fx(f)(x, x), you will trace out: +# +# def f(x, y): +# return y + y +# +# Oops! +# +# (2) For most tensors x and y, you can compute f's gradient with respect to +# these to inputs by saying torch.autograd.grad(f(x, y), (x, y)). However, +# if x is y, you will trace out a program that gets incorrect gradients: +# +# >>> x = torch.randn(1, requires_grad=True) +# >>> torch.autograd.grad(x + x, (x, x)) +# (tensor([2.]), tensor([2.])) +# +# In other words, the gradient is double-counted. Deduplicating the arguments +# gives you an appropriate gradient: +# +# >>> y = torch.randn(1, requires_grad=True) +# >>> torch.autograd.grad(x + y, (x, y)) +# (tensor([1.]), tensor([1.])) +# +# HOW TO DEDUPLICATE: +# +# There are a few strategies, in order of preference: +# +# 1. For every duplicate argument to the function, detach it into +# a separate leaf tensor, so that it is no longer duplicated. +# +# PRO: The resulting compiled graph works for any configuration +# of duplicated arguments. +# +# CON: It does not (naively) work if you mutate the metadata of inputs: +# +# def f(x, y): +# x.transpose_(0, 1) +# y.transpose_(0, 2) +# +# x = torch.randn(2, 3, 4) +# f(x, x) +# +# The ordering of the transposes inside f dictates whether or not +# you get [4, 2, 3] or [3, 4, 2]. This means that you cannot precompute +# what metadata mutations should get applied to each input; you need to +# assume they aren't duplicates (what we do today) or preserve +# the original metadata mutations exactly in order, so that they work +# for any duplicate configuration. +# +# CON: It does not (naively) work if you mutate the data of inputs. +# In particular, leaf tensors that require grad cannot be mutated, +# this makes it impossible to differentiate with respect to the original +# base. +# +# 2. For every duplicate argument to the function, remove it, so it is +# no longer part of the "true" signature: +# +# PRO: Implemented naively, it still works for metadata/data mutation. +# +# CON: The resulting compiled graph is duplicate-specialized: it only +# works if future calls duplicate arguments in exactly the same way. +# Horribly, Dynamo doesn't guard on this at the moment. But even if +# it did, you could still end up recompiling a bunch of each duplicate. +# +# Our strategy is to do (1) if we can, and do (2) otherwise, erroring if +# Dynamo's guards are not enough. In practice, this seems to cover +# everything. +# +@dataclass +class AOTDedupeWrapper(CompilerWrapper): + keep_arg_mask: list[bool] = field(default_factory=list) + add_dupe_map: list[int] = field(default_factory=list) + old_input_metadata: list[InputAliasInfo] = field(default_factory=list) + needs_post_compile: bool = True + + # NB: Hot path, avoid set lookups here + # TODO: Can avoid the zip here too, probably + def remove_dupe_args(self, args: list[Any]) -> list[Any]: + return [t for t, keep in zip(args, self.keep_arg_mask) if keep] + + def add_dupe_args(self, args: list[Any]) -> list[Any]: + return [args[i] for i in self.add_dupe_map] + + def pre_compile( + self, + flat_fn: TraceFn, + flat_args: list[FxValue], + flat_args_descs: list[AOTInput], + aot_config: AOTConfig, + *, + fw_metadata: ViewAndMutationMeta, + ) -> tuple[TraceFn, list[FxValue], list[AOTInput], ViewAndMutationMeta]: + # Use information about whether or not flat_fn mutates its arguments + # or not to handle dupe args + + # Strategy 1: For any input that is not mutated, we can leafify it if we + # need to remove a duplicate. + leaf_flat_args: list[FxValue] = [] + leaf_flat_args_descs: list[AOTInput] = [] + args_set = set() + ok = True + + for i, (a, a_desc) in enumerate(zip(flat_args, flat_args_descs)): + if not isinstance(a, torch.Tensor): + leaf_flat_args.append(a) + leaf_flat_args_descs.append(a_desc) + elif a not in args_set: + args_set.add(a) + leaf_flat_args.append(a) + leaf_flat_args_descs.append(a_desc) + elif ( + not fw_metadata.input_info[i].mutates_data + and not fw_metadata.input_info[i].mutates_metadata + ): + leaf_flat_args.append(a.detach().requires_grad_(a.requires_grad)) + leaf_flat_args_descs.append(a_desc) + else: + ok = False + break + + if ok: + self.needs_post_compile = False + return flat_fn, leaf_flat_args, leaf_flat_args_descs, fw_metadata + + if requires_subclass_dispatch(leaf_flat_args, fw_metadata): # type: ignore[arg-type] + raise RuntimeError( + """\ + Encountered duplicate inputs that are mutated in the graph, but at least one input/output + to the graph is a tensor subclass. This is not supported today. You can try to + remove the aliasing yourself as a workaround, or otherwise file an issue on github.""" + ) + + # export path: ban duplicate inputs for now, add later if requested. + if aot_config.is_export: + raise RuntimeError( + f"""\ + Encountered duplicated inputs that are mutated in the graph you are trying to export. + This functionality is currently not supported. If needed, please file a github issue. + + fw_metadata={str(fw_metadata)} + """ + ) + + # Strategy 2: Duplicate specialization + # + # When we have duplicate arguments in a function call, we need to handle them specially. + # For example, if we have a function call f(a, b, a, c), we need to: + # + # 1. Remove duplicates to get a deduplicated list [a, b, c] + # 2. Compile our function to work with this deduplicated list + # 3. At runtime, convert incoming arguments with duplicates to the deduplicated form + # 4. Pass the deduplicated arguments to our compiled function + # + # To do this, we need two helper functions: + # + # - remove_dupe_args: Converts [a, b, a, c] -> [a, b, c] + # - add_dupe_args: Converts [a, b, c] -> [a, b, a, c] + # + # For our example [a, b, a, c], we track: + # + # - seen_args = {a: 0, b: 1, c: 2} (maps each unique arg to its first position) + # - add_dupe_map = [0, 1, 0, 2] (tells us how to reconstruct the original list) + # - keep_arg_mask = [True, True, False, True] (tells us which args to keep when deduplicating) + + seen_args: dict[Tensor, int] = {} + # Implicitly map duped arg position (list index) to de-duped arg position + keep_arg_mask: list[bool] = [] + add_dupe_map: list[int] = [] + duped_arg_len = len(flat_args) + + j = 0 # index into deduped_flat_args + for t in flat_args: + if isinstance(t, torch.Tensor): + if t in seen_args: + keep_arg_mask.append(False) + add_dupe_map.append(seen_args[t]) + continue + seen_args[t] = j + + keep_arg_mask.append(True) + add_dupe_map.append(j) + j += 1 + if len(add_dupe_map) != duped_arg_len: + raise AssertionError( + f"Expects add_dupe_map to have length {duped_arg_len} but got {len(add_dupe_map)}" + ) + + self.keep_arg_mask = keep_arg_mask + self.add_dupe_map = add_dupe_map + + deduped_flat_args = self.remove_dupe_args(flat_args) + # TODO: instead of arbitrarily removing args, it might be useful to + # have a record that these were duped, perhaps as a mutable attribute + # on the kept arg? Do this if someone needs it + deduped_flat_args_descs = self.remove_dupe_args(flat_args_descs) + + # Update our input metadata to remove duped input metadata. + updated_fw_metadata = remove_dupe_metadata( + fw_metadata, keep_arg_mask, add_dupe_map + ) + + if ( + tracing_context := TracingContext.try_get() + and aot_config.aot_autograd_arg_pos_to_source + ): + # TODO(voz): This structure is 1:1, we could consider an alternate structure like + # kept_pos:[dupe_arg_pos], however, add_dupe_map is 1:1 so we would need a new structure there, + # which feels like needless complexity for a tiny bit of efficiency at this point. + for dupe_arg_pos, (kept_pos, keep_arg) in enumerate( + zip(add_dupe_map, keep_arg_mask) + ): + if not keep_arg: + dupe_arg_source = aot_config.aot_autograd_arg_pos_to_source[ + dupe_arg_pos + ] + kept_arg_source = aot_config.aot_autograd_arg_pos_to_source[ + kept_pos + ] + tracing_context.guards_context.aotautograd_guards.append( # type: ignore[attr-defined] + DuplicateInputs(kept_arg_source, dupe_arg_source) + ) + + @simple_wraps(flat_fn) + def wrapped_flat_fn( + *args: FxValue, + ) -> tuple[list[FxValue], list[AOTOutput]]: + outs, out_descs = call_and_expect_output_descs( + flat_fn, + self.add_dupe_args(args), # type: ignore[arg-type] + ) + return outs, out_descs + + if config.debug_assert: + ref_fw_metadata = run_functionalized_fw_and_collect_metadata( + without_output_descs(wrapped_flat_fn), + flat_args_descs=deduped_flat_args_descs, + static_input_indices=aot_config.static_input_indices, + keep_input_mutations=fw_metadata.keep_input_mutations, + )(*deduped_flat_args) + if ref_fw_metadata != updated_fw_metadata: + raise AssertionError( + f"ref_metadata={str(ref_fw_metadata)}, actual_metadata={str(updated_fw_metadata)}" + ) + + return ( + wrapped_flat_fn, + deduped_flat_args, + deduped_flat_args_descs, + updated_fw_metadata, + ) + + def post_compile( + self, + compiled_fn: Callable[..., Any], + aot_config: AOTConfig, + *, + runtime_metadata: ViewAndMutationMeta, + ) -> Callable[..., Any]: + if not self.needs_post_compile: + return compiled_fn + + keep_indices = [i for i, keep in enumerate(self.keep_arg_mask) if keep] + idx_list = ", ".join(f"args[{i}]" for i in keep_indices) + source = ( + f"def inner_fn(args):\n" + f" deduped_args = [{idx_list}]\n" + f" args.clear()\n" + f" return compiled_fn(deduped_args)\n" + ) + from .subclass_codegen import _compile_and_exec_source + + wrapped_compiled_fn: Callable[..., Any] = _compile_and_exec_source( # type: ignore[assignment] + source, + {"compiled_fn": compiled_fn}, + "inner_fn", + "dedup_wrapper", + wrapped_fn=compiled_fn, + ) + + wrapped_compiled_fn._boxed_call = True # type: ignore[attr-defined] + + # This can be uncommented when we properly guard for duplicates, + # but right now we must not do it. + # if not config.debug_assert: + # return wrapped_compiled_fn + + @wraps(wrapped_compiled_fn) + def debugged_compiled_fn(args: list[Any]) -> Any: + # Test that the computed remove/add arg functions are an inverse + new_args = self.add_dupe_args(self.remove_dupe_args(args)) + seen: dict[Any, None] = {} + for i, (x, y) in enumerate(zip(new_args, args)): + seen[y] = None + if x is not y: + raise AssertionError( + format_guard_bug_msg( + aot_config, + f"{describe_input(i, aot_config)} would be a duplicate of " + f"{describe_input(self.add_dupe_map[i], aot_config)}", + ) + ) + # This is only an error if there is metadata mutation on both of + # the duped arguments; in this case, we need to know what order + # the metadata mutation applies in. You'll get the correct result + # otherwise, because a graph that assumes distinct inputs works if + # you dupe the inputs (the gradient contributions from each input + # will get summed up appropriately.) + # + # TODO: work out how to setup this assert correctly + """ + assert len(seen) == unique_args, format_guard_bug_msg(aot_config, + f"there would be {unique_args} distinct arguments" + ) + """ + return wrapped_compiled_fn(args) + + debugged_compiled_fn._boxed_call = True # type: ignore[attr-defined] + + return debugged_compiled_fn + + +# This layer handles the situation where you have two inputs that alias each other, +# and one of the inputs is mutated. +# We need to take special care to ensure that the mutation is applied to the other aliases in the graph. +# +# pre-condition: AOTDedupWrapper has already run. +# (This function will in theory work if there are duplicate args. +# However, the synthetic base code path is a bit sub-optimal, and running with dupe'd inputs +# would cause us to hit that path more frequently). +@dataclass +class AOTSyntheticBaseWrapper(CompilerWrapper): + # Currently, the only reason we need to plumb this bool is because + # the synthetic base code prohibits more cases in the autograd case than the inference case. + trace_joint: bool # TODO: refactor trace_joint + needs_post_compile: bool = True + aliased_arg_idx_with_metadata_mutations: list[int] = field(default_factory=list) + + def pre_compile( + self, + flat_fn: TraceFn, + flat_args: list[FxValue], + flat_args_descs: list[AOTInput], + aot_config: AOTConfig, + *, + fw_metadata: ViewAndMutationMeta, + ) -> tuple[Callable[..., Any], list[FxValue], list[AOTInput], ViewAndMutationMeta]: + is_inference = not self.trace_joint + ( + flat_args_with_synthetic_bases, + flat_args_descs_with_synthetic_bases, + synthetic_base_info, + ) = merge_view_inputs( + aot_config, + flat_args, + flat_args_descs, + fw_metadata.input_info, + is_inference=is_inference, + ) + + # Happy path: we don't need synthetic bases + if synthetic_base_info is None: + self.needs_post_compile = False + return flat_fn, flat_args, flat_args_descs, fw_metadata + + # export path: ban synthetic bases for now, add later if requested. + if requires_subclass_dispatch(flat_args, fw_metadata): # type: ignore[arg-type] + raise RuntimeError( + """\ + Encountered aliased inputs that are mutated in the graph, but at least one input/output + to the graph is a tensor subclass. This is not supported today. You can try to + remove the aliasing yourself as a workaround, or otherwise file an issue on github.""" + ) + + if aot_config.is_export: + raise RuntimeError( + f"""\ + Encountered aliased inputs that are mutated in the graph you are trying to export. + This functionality is currently not supported. If needed, please file a github issue. + + synthetic_base_info={str(synthetic_base_info)} + + fw_metadata={str(fw_metadata)} + """ + ) + + if len(fw_metadata.input_info) != len(synthetic_base_info): + raise AssertionError( + f"expected len(fw_metadata.input_info) == len(synthetic_base_info), " + f"got {len(fw_metadata.input_info)} != {len(synthetic_base_info)}" + ) + + # Update our forward metadata to take synthetic bases into account + ( + fw_metadata_updated, + aliased_arg_idx_with_metadata_mutations, + ) = create_synthetic_base_metadata( + fw_metadata, + synthetic_base_info, + flat_args, + flat_args_with_synthetic_bases, + flat_args_descs_with_synthetic_bases, + ) + # Save old input args for post-compile + self.old_input_info = fw_metadata.input_info + + self.aliased_arg_idx_with_metadata_mutations = ( + aliased_arg_idx_with_metadata_mutations + ) + replay_views = config.view_replay_for_aliased_outputs + + def _unpack_synthetic_bases(primals: tuple[Any, ...]) -> list[Any]: + f_args_inner = [] + # pyrefly: ignore [not-iterable] + for inner_idx_or_tuple in synthetic_base_info: + if isinstance(inner_idx_or_tuple, int): + f_args_inner.append(primals[inner_idx_or_tuple]) + else: + inner_base_idx, view_tensor = inner_idx_or_tuple + base = primals[inner_base_idx] + view_arg = gen_alias_from_base( + base, + view_tensor, + view_tensor.requires_grad, + replay_views=replay_views, + ) + f_args_inner.append(view_arg) + return f_args_inner + + @simple_wraps(flat_fn) + def wrapped_flat_fn(*args: Any) -> Any: + unpacked_args = _unpack_synthetic_bases(args) + # This is a bit subtle. The goal of this entire function (aot_dispatch_synthetic_bases) + # is to relieve the downstream logic from having to reason about mutations on inputs that alias + # each other, by replacing aliased inputs with a synthetic base. + # One area where this breaks down a bit however is if one of those aliased inputs + # experienced a metadata mutation. + # We are now obligated to reapply the metadata mutation directly to the user's input; + # it isn't enough to apply mutations back to the synthetic base in the downstream logic. + # + # The way we handle this is by pretending that those aliased inputs that experience metadata mutations + # are additional outputs in the user's forward function. + # The downstream logic will just treat these as "user outputs that alias inputs". + # However, we will manually grab them at runtime here, use them to reapply the metadata mutation + # to the user inputs, and not return them to the user. + aliased_args_with_metadata_mutations = [ + x + for i, x in enumerate(unpacked_args) + if i in self.aliased_arg_idx_with_metadata_mutations + ] + out, out_descs = call_and_expect_output_descs(flat_fn, unpacked_args) # type: ignore[arg-type] + if len(aliased_args_with_metadata_mutations) > 0: + # TODO: record more detailed desc information here + return (*out, *aliased_args_with_metadata_mutations), ( + *out_descs, + *( + [ + MetadataMutationAOTOutput(i) + for i in range( + len(self.aliased_arg_idx_with_metadata_mutations) + ) + ] + ), + ) + else: + return out, out_descs + + if config.debug_assert: + ref_fw_metadata = run_functionalized_fw_and_collect_metadata( + without_output_descs(wrapped_flat_fn), + flat_args_descs=flat_args_descs_with_synthetic_bases, + static_input_indices=aot_config.static_input_indices, + keep_input_mutations=fw_metadata.keep_input_mutations, + )(*flat_args_with_synthetic_bases) + if ref_fw_metadata != fw_metadata_updated: + raise AssertionError( + f"ref_metadata={pprint.pformat(partial_flatten_asdict(ref_fw_metadata))}, " + f"\nactual_metadata={pprint.pformat(partial_flatten_asdict(fw_metadata_updated))}" + ) + return ( + wrapped_flat_fn, + flat_args_with_synthetic_bases, + flat_args_descs_with_synthetic_bases, + fw_metadata_updated, + ) + + def post_compile( + self, + compiled_fn: Callable[..., Any], + aot_config: AOTConfig, + *, + runtime_metadata: ViewAndMutationMeta, + ) -> Callable[..., Any]: + if not self.needs_post_compile: + return compiled_fn + + is_inference = not self.trace_joint + + @wraps(compiled_fn) + def wrapped_compiled_fn(args: list[Any]) -> Any: + # TODO: this sure seems expensive to run at runtime (which + # post_compile seems to imply it does?!) + args_with_synthetic_bases, _, synthetic_base_info = merge_view_inputs( + aot_config, args, None, self.old_input_info, is_inference=is_inference + ) + if synthetic_base_info is None: + raise AssertionError("synthetic_base_info must not be None") + aliased_args_w_metadata_mutations = [ + args[i] for i in self.aliased_arg_idx_with_metadata_mutations + ] + num_aliased_args_with_metadata_mutations = len( + aliased_args_w_metadata_mutations + ) + args.clear() + outs = compiled_fn(args_with_synthetic_bases) + if num_aliased_args_with_metadata_mutations > 0: + # This code does not handle **all** input metadata mutations. + # Instead, it only handles metadata mutations on inputs that were converted into synthetic bases + # (which only happens if at least one aliased input experienced a data mutation). + # e.g: + # def f(a, b): + # a.mul_(2) + # b.t_(1, 0) + # f(x.view(2, 2), x.view(2, 2)) + mutated_metadata_inps = outs[-num_aliased_args_with_metadata_mutations:] + user_outs = outs[:-num_aliased_args_with_metadata_mutations] + for inp, mutated_inp in zip( + aliased_args_w_metadata_mutations, mutated_metadata_inps + ): + inp.as_strided_( + mutated_inp.size(), + mutated_inp.stride(), + mutated_inp.storage_offset(), + ) + return user_outs + return outs + + return wrapped_compiled_fn + + +# Note [Handling mutations on an input that aliases other inputs] +# The easiest example to show-case this edge case is here: +# +# def f(a, b): +# a.mul_(2) +# out = a + b +# return out +# b = torch.ones(...) +# a = b.view(-1) +# f(a, b) +# +# In this situation, if a and b happened to be aliased, we need to trace something different! +# Suppose we had b = a.view(-1) +# (In this case, that means that `a._base is b`) +# +# We need to ensure that the aliasing relationship between a and b is preserved. +# We do that detecting the specific situation above (mutate an input that aliases another input), +# and when we do that, we create a synthetic base argument. Then inside of the traced forward, +# we regenerate a and b off of that base. +# The complete example of the transformed function looks like this: +# +# // The traced forward takes in a synthetic base, and regenerates the aliased inputs as views +# // We could consider getting view-replay support here to minimize as_strided_scatter ops in the graph +# def traced_forward(base): +# a = base.as_strided(...) +# b = base.as_strided(...) +# a_updated = a.mul(2) +# base_updated = torch.as_strided_scatter(base, a_updated, ...) +# b_updated = base_updated.as_strided(...) +# out = a_updated + b_updated +# return a_updated, out +# +# def compiled_fn(a, b): +# // we detect that a is the "differentiable base" here +# base = a +# // In other situations, we might do either: +# // (1) a and b are both views off of some larger differentiable base +# // assert a._base is b._base and a._base is not None +# // base = a._base +# // (2) a and b both don't require gradients. Create a base from the storage +# // assert a._base is None and b._base is None +# // base = torch.Tensor(a.storage()) +# a_updated, out = traced_forward(base) +# a.copy_(a_updated) +# return out +# +# This function: +# (1) Merges input views into a synthetic base argument, when any of those input views are mutated +# (2) Returns metadata telling the autograd.Function how to modify their arguments properly, +# to respect the new calling convention. +# +# The calling convention is as follows. +# Any inputs that were originally views of one another get yanked, and replaced with a synthetic base. +# The argument list ordering goes [base1, ..., baseN], [arg1, ..., argN], +# Where the ordering of the bases is determined from the ordering of the original view args. +# baseA will come before baseB if the earliest original argument coming from baseA +# showed up earlier in the argument list than the earliest original argument coming from baseB. +# +# Example, given some tensors a, b, c, d +# call site: +# f(a, c.view(-1), b.view(-1), b, c, d) +# Modified argument list: +# c_base comes first because the first c view came earlier in arg list than the first b view +# a and d still show up in the modified arg list, but b and c don't- they're regenerated from their bases +# b_base = torch.Tensor(b.storage()) +# c_base = torch.Tensor(c.storage()) +# f(c_base, b_base, a, d) +def merge_view_inputs( + aot_config: AOTConfig, + fwd_inputs: list[Any], + # This is None when called at runtime from post_compile closure + fwd_inputs_descs: list[AOTInput] | None, + mutated_input_info: list[InputAliasInfo], + *, + # The autograd case currently has more restrictions than the inference case. + is_inference: bool, +) -> tuple[list[Any], list[AOTInput], list[int | tuple[int, torch.Tensor]] | None]: + if fwd_inputs_descs is None: + fwd_inputs_descs = [DummyAOTInput(i) for i in range(len(fwd_inputs))] + + def _are_differentiable_views(view1: torch.Tensor, view2: torch.Tensor) -> bool: + if view1 is view2: + return True + if view1._base is None and view2._base is None: + return False + if view1._base is view2._base or view1._base is view2 or view1 is view2._base: + return True + return False + + def _same_dtype_views(view1: torch.Tensor, view2: torch.Tensor) -> bool: + if view1.dtype != view2.dtype: + return False + if view1._base is not None and view1.dtype != view1._base.dtype: + return False + if view2._base is not None and view2.dtype != view2._base.dtype: + return False + return True + + if len(fwd_inputs) != len(mutated_input_info): + raise AssertionError( + f"expected len(fwd_inputs) == len(mutated_input_info), " + f"got {len(fwd_inputs)} != {len(mutated_input_info)}" + ) + if not [info for info in mutated_input_info if info.mutates_data]: + # Return early when there are no mutations. + return fwd_inputs, fwd_inputs_descs, None + + storage_ref_to_idx: dict[StorageWeakRef, list[int]] = collections.defaultdict(list) + # pyrefly: ignore [implicit-any] + base_args = [] + # pyrefly: ignore [implicit-any] + other_args = [] + base_args_descs = [] + other_args_descs = [] + for i, (inpt, source) in enumerate(zip(fwd_inputs, fwd_inputs_descs)): + if isinstance(inpt, Tensor): + storage_ref = StorageWeakRef(inpt.untyped_storage()) + storage_ref_to_idx[storage_ref].append(i) + else: + other_args.append(inpt) + other_args_descs.append(source) + # Note [Synthetic Base Info Metadata] + # This list contains metadata that tells you what the i'th argument in the inner calling convention should be. + # It's either: + # - another int (corresponding to the index in the argument list of the element from the outer calling convention) + # - idx, view_tensor, where we can generate the new output with view_tensor._view_func(old_args[idx]) + # idx corresponds to which synthetic base from the outer calling context to view + inner_calling_convention_meta: dict[int, int | tuple[int, torch.Tensor]] = {} + for aliased_input_indices in storage_ref_to_idx.values(): + if len(aliased_input_indices) <= 1 or not any( + # We only care about mutations that affect all aliases, + # so metadata mutations on an input doesn't require us to do synthetic base handling. + mutated_input_info[inpt_idx].mutates_data + for inpt_idx in aliased_input_indices + ): + other_args.extend( + fwd_inputs[curr_idx] for curr_idx in aliased_input_indices + ) + other_args_descs.extend( + fwd_inputs_descs[curr_idx] for curr_idx in aliased_input_indices + ) + continue + + # Here, we attempt to do a more complicated check to detect false aliasing + # (e.g. if all the tensors have the same storage, but don't actually overlap) + # In theory, we could have a large group of tensors that all share storages, where only *some* of them + # have overlapping memory. + # I don't bother with that case for now: here, we only bail out earlier if we detect that **every** pair + # of tensors in the current group that shares a storage is non-overlapping. + aliased_input_indices_no_false_sharing = compute_overlapping_inputs( + aot_config, fwd_inputs, aliased_input_indices + ) + if len(aliased_input_indices_no_false_sharing) <= 1: + other_args.extend( + fwd_inputs[curr_idx] for curr_idx in aliased_input_indices + ) + other_args_descs.extend( + fwd_inputs_descs[curr_idx] for curr_idx in aliased_input_indices + ) + continue + + # We detected an input that was mutated, AND aliases with another input. + # we need to replace this set of aliased inputs with a single synthetic base. + # For now, I'm banning a bunch of cases. We expect dynamo to properly detect these cases + # and error out. We can fix them later. + # These checks are transitive, so we don't need to check every pair. + for idx1, idx2 in zip( + aliased_input_indices, aliased_input_indices[1:], strict=False + ): + view1 = fwd_inputs[idx1] + view2 = fwd_inputs[idx2] + # The "inputs that are aliased but have different differentiable bases" case + # is more complicated and hopefully pretty rare. Not currently handled. + if not is_inference: + if not _are_differentiable_views(view1, view2): + raise AssertionError( + "aot_autograd() does not yet handle non-differentiable view input mutations." + ) + # Regenerating views when reinterpreting complex / real tensors seems non-trivial, + # not handling for now + if not _same_dtype_views(view1, view2): + raise AssertionError( + "aot_autograd() does not yet handle input mutations on views with different dtypes." + ) + non_none_bases = [ + (i, fwd_inputs[i]._base) + for i in aliased_input_indices + if fwd_inputs[i]._base is not None + ] + aliases_with_none_bases = [ + fwd_inputs[i] for i in aliased_input_indices if fwd_inputs[i]._base is None + ] + synthetic_base_desc: AOTInput + if len(non_none_bases) == 0: + # Case where none of the aliases have a ._base + # we generate a synthetic base without gradients, and generate views off of it + # We hit this case when we have input tensors to the graph that share a storage, + # but do not have a ._base field. + # Wondering when we hit this case? + # The _base field simply says that autograd knows about the aliasing relationship, + # but sometimes we create tensors which are aliased out of the same storage but guaranteed + # to be disjoint. In these cases, we will skip setting up the _base relationship + # for performance reasons (because the fact that the tensors share the same storage + # is unobservable unless you (1) do naughty things with resize_/as_strided + # or (2) look at the storage--as we are doing here.) + # One particular example of this is optimizer steps on the LSTM module: + # LSTM parameters are packed into a contiguous storage for efficiency reasons when + # calling cuDNN kernels, so when these parameters get passed to the optimizer we will + # find they share the same storage, but do not have _base set since they are all disjoint. + # + # NOTE: There is one case where this is unsafe: + # torch.Tensor(storage) will ALWAYS create a 1D tensor, which is not necessarily + # the same shape as the "actual" base that the tensor came from. + # For the most part this is fine, because we always use as_strided() + # to generate the original aliased inputs again. + # If we were to use view-replay though, this could cause the aliased views + # to have incorrect sizes. + example_idx = aliased_input_indices[0] + example_alias = fwd_inputs[example_idx] + # Note that this function is reused at both trace time and runtime. + # At trace time, we're under a FakeMode so synthetic_base becomes a FakeTensor. + synthetic_base = torch.empty( + (0,), dtype=example_alias.dtype, device=example_alias.device + ) + # We don't actually have a convenient way of going from storage -> tensor, + # So using set_() here (we suffer some minor overhead, but this case is rare). + synthetic_base.set_(example_alias.untyped_storage()) + synthetic_base_desc = SyntheticBaseAOTInput(fwd_inputs_descs[example_idx]) + else: + # Case where all of the aliases require gradients, and have the same _base. + i, synthetic_base = non_none_bases[0] + synthetic_base_desc = ViewBaseAOTInput(fwd_inputs_descs[i]) + for _, other_base in non_none_bases[1:]: + if other_base is not synthetic_base: + raise AssertionError( + "aot_autograd() does not yet handle non-differentiable view input mutations." + ) + for alias in aliases_with_none_bases: + if alias is not synthetic_base: + raise AssertionError( + "aot_autograd() does not yet handle non-differentiable view input mutations." + ) + base_args.append(synthetic_base) + base_args_descs.append(synthetic_base_desc) + for curr_view_idx in aliased_input_indices: + curr_view = fwd_inputs[curr_view_idx] + base_idx = len(base_args) - 1 + # We store just enough info here so that we can regenerate the view later. + # Regeneration: curr_view._view_func(args[base_idx]) + inner_calling_convention_meta[curr_view_idx] = (base_idx, curr_view) + if len(base_args) == 0: + if len(other_args) != len(fwd_inputs): + raise AssertionError( + f"expected len(other_args) == len(fwd_inputs), " + f"got {len(other_args)} != {len(fwd_inputs)}" + ) + # If no synthetic bases are necessary, just return the original inputs. + return fwd_inputs, fwd_inputs_descs, None + else: + from torch.fx.experimental.symbolic_shapes import SymIntEqByExpr + + def make_hashable(arg: Any) -> Any: + if isinstance(arg, torch.SymInt): + # Since only nested SymInt objects can be hashed, we wrap them with + # SymIntEqByExpr, which is a hashable wrapper of SymInts. + return SymIntEqByExpr(arg) + return arg + + # Otherwise, return: + # (1) The new args according to the updated calling convention: (synthetic_bases, other_args) + # (2) Metadata telling functionalization how to generate the inner argument list given the outer calling convention. + # We post-process it into a list, where meta[i] tells you info about the i'th argument in the inner calling convention. + args_to_functionalization = base_args + other_args + args_to_functionalization_descs = base_args_descs + other_args_descs + + # Map each argument into its old index. + # There may be some repeated arguments, so we collect their indices in a list. + arg_to_old_idx_map = collections.defaultdict(list) + for i, arg in enumerate(fwd_inputs): + arg_to_old_idx_map[make_hashable(arg)].append(i) + # Reverse the list of each argument, so that we can easily pop them one-after-the-other in order. + for hashable_arg in arg_to_old_idx_map: + arg_to_old_idx_map[hashable_arg] = list( + reversed(arg_to_old_idx_map[hashable_arg]) + ) + + for i, other_arg in enumerate(other_args): + new_idx = len(base_args) + i + old_idx = arg_to_old_idx_map[make_hashable(other_arg)].pop() + inner_calling_convention_meta[old_idx] = new_idx + + # post process into a list + post_processed_calling_convention_meta: list[int | tuple[int, torch.Tensor]] = [ + -1 for _ in range(len(inner_calling_convention_meta)) + ] + for k, v in inner_calling_convention_meta.items(): + post_processed_calling_convention_meta[k] = v + # Quick assert: every argument in the inner calling convention should be accounted for. + for x in post_processed_calling_convention_meta: + if x == -1: + raise AssertionError( + "every argument in the inner calling convention should be accounted for" + ) + # pyrefly: ignore [bad-return] + return ( + args_to_functionalization, + args_to_functionalization_descs, + post_processed_calling_convention_meta, + ) + + +# Note: [Backward graph lazy lowering] +# After AOTDispatch traces the backward for graphs requiring autograd, we will lower the graph lazily, +# unless we suspect that inductor might specialize and insert additional guards. When we do lazy +# lowering, we stash the AOT backward graph (bw_module) in this class. +# +# Lowering passes are performed on a deepcopy of this bw_module due to compatibility +# with compiled autograd. See: https://github.com/pytorch/pytorch/pull/149229#discussion_r2002122645. +@dataclass +class AutogradLazyBackwardCompileInfo: + bw_module: Callable[..., Any] + placeholder_list: list[Any] + saved_context: TracingContext | None + saved_compile_context: CompileContext | None + + +# On an AOT Autograd cache hit, we already have a lowered backward, so there is usually +# no need to keep information around for a new lazy compilation. Except for compiled autograd, +# which wants to retrace this backward into a larger graph, and it needs the graph module to do so. +@dataclass +class CachedAutogradLazyBackwardCompileInfo: + bw_module_fn: Callable[..., Any] + + +def _raise_if_functorch_active() -> None: + # not ideal but prevent the user from seeing a nasty traceback - See #138422 + stack = torch._C._functorch.peek_interpreter_stack() + torch._check( + stack is None, + lambda: ( + "It looks like you're trying to call a compiled backward function within vmap/grad/vjp, " + "which isn't supported. Try wrapping vmap inside torch.compile, or skip compiling the " + "backward function." + ), + ) + + +# NOTE: this function must be torch._dynamo.allow_in_graph-able. Non tensor/symnode inputs must be constants. +def _backward_prologue_functional( + ctx_saved_tensors: Sequence[torch.Tensor], + ctx_symints: Sequence[IntLikeType], + ctx_opaque_objects: Sequence[Any], + metadata: ViewAndMutationMeta, + maybe_subclass_metadata: SubclassMeta | None, + *flat_args: Any, + codegen_unwrap_fn: Callable[..., Any] | None = None, +) -> list[Any]: + # Calling convention: we expect a grad_out passed to the backward: + # - for every output of the fw that does *not* alias an input or graph intermediate + # - for every updated_input generated by the fw that does *not* alias an input (aka only data-mutations) + # - for every graph intermediate that we need to use to generate an output later. + # The other outputs in the autograd.Function.forward that do *not* show up in the backward include: + # - outputs that alias inputs or graph intermediates + # - updated inputs due to metadata-only mutations. + # We need to return them in the forward, but ensure that they all do not get gradients in the backward, + # and we filter them out here before passing the remaining grad_outputs into the compiled backward. + _raise_if_functorch_active() + + num_intermediate_bases = metadata.num_intermediate_bases + num_mutated_runtime_inps = metadata.num_mutated_inp_runtime_indices + expected_grad_outs = ( + metadata.num_outputs + num_mutated_runtime_inps + num_intermediate_bases + ) + deterministic = metadata.deterministic + global_deterministic = torch.are_deterministic_algorithms_enabled() + if deterministic is not None: + torch._check( + not (not deterministic and global_deterministic), + lambda: ( + "This compiled backward function is being run with " + "torch.use_deterministic_algorithms(True), " + "but it was previously generated during the forward function while " + "torch.use_deterministic_algorithms(False) was set." + ), + ) + + if len(flat_args) != expected_grad_outs: + raise AssertionError( + f"expected {expected_grad_outs} grad_outs, got {len(flat_args)}" + ) + out_info = metadata.output_info + + inp_tangents, out_tangents, intermediate_base_tangents = ( + flat_args[:num_mutated_runtime_inps], + flat_args[ + num_mutated_runtime_inps : num_mutated_runtime_inps + metadata.num_outputs + ], + flat_args[num_mutated_runtime_inps + metadata.num_outputs :], + ) + # input_info contains info on *every* input, + # But in the backward(), we are only given grad outputs for every mutated input + # We then need to filter out the grad outputs that correspond to metadata-only mutations or don't require grad + input_info = metadata.input_info + inp_tangents_filtered = [ + x + for x, info_idx in zip( + inp_tangents, + metadata.mutated_inp_runtime_indices, + ) + if input_info[info_idx].mutates_data and input_info[info_idx].requires_grad + ] + # We also need to filter out grad outputs that correspond to outputs aliasing inputs/intermediates + out_tangents_filtered = [ + x + for x, info in zip(out_tangents, out_info) + if info.output_type + in [ + OutputType.non_alias, + OutputType.unsafe_view_alias, + OutputType.custom_function_view, + ] + and issubclass(info.raw_type, torch.Tensor) + and info.requires_grad_for_backward + ] + # intermediate bases always require gradients, and always participate in the backward graph. + flat_bw_args_with_grads = [ + *inp_tangents_filtered, + *out_tangents_filtered, + *intermediate_base_tangents, + ] + num_flat_bw_args_with_grads = len(flat_bw_args_with_grads) + + # sanity asserts + # metadata_only_inps = [ + # x for x, info_idx in zip(inp_tangents, mutated_inp_indices) + # if not input_info[info_idx].mutates_data + # ] + # aliased_outputs = [ + # x for x, info in zip(out_tangents, out_info) if info.output_type != OutputType.non_alias] + # assert all(x is None for x in metadata_only_inps) + # assert all(x is None for x in aliased_outputs) + # TODO: replace this with FunctionalizedRngRuntimeWrapper + # pyrefly: ignore [implicit-any] + rng_args = [] + if metadata.is_rng_op_functionalized: + # Add the seed and offset to args + rng_args = CUDARngStateHelper.get_torch_state_as_tuple() + + bw_tokens = [None] * metadata.num_backward_tokens + + # - note: donated buffer logic requires (*ctx.symints, *ctx.saved_tensors, *ctx.opaques) showing up first + # in the bw output order. + + # Every dereference of ctx.saved_tensors incurs saved_tensors_hooks calls + # There are tests that count these calls, saving to var. + num_ctx_saved_tensors = len(ctx_saved_tensors) + all_args = [ + *ctx_symints, + *ctx_saved_tensors, + *ctx_opaque_objects, + *flat_bw_args_with_grads, + *bw_tokens, + *rng_args, + ] + del ctx_saved_tensors + + # Note: [AOTAutograd Backward Guards] + # During AOTDispatch, we eagerly create and trace out a joint fw-bw graph. + # Doing so requires us to "guess" about some of the metadata of our grad_outputs. + # + # In particular: if an output to the forward is a plain tensor or a subclass, + # its corresponding grad_output in the backward **may or may not** be + # a plain tensor or a subclass. The main cases are: + # (1) If an output is a plain tensor, its grad_out will also be a plain tensor, + # *unless* the output is used in some subclass compute later in the forward graph, + # which will cause its grad_output to become a subclass + # (2) If an output is a subclass, its grad_out will also be a subclass, + # *unless* the output of the forward did not actually participate in the gradient computation, + # in which case autograd will insert a plain tensor of zeros for the grad_output. + # We could avoid this case with `torch.autograd.Function.set_materialize_grads`, + # although this is not turned on today in AOTAutgrad and would require more work. + # + # Today, we make a guess on subclass-ness based on the above examples, + # and hard-error in the backward if we guessed wrong. + # + # In the future, we should add backward guards that would allow us to + # properly handle this case instead of erroring: we would need to retrace the backward graph, + # since we might produce an entirely different trace if our grad_outputs are subclass or not. + del flat_bw_args_with_grads + + tangents_start_idx = ( + len(all_args) - num_flat_bw_args_with_grads - len(rng_args) - len(bw_tokens) + ) + expected_tangents_start = ( + len(ctx_symints) + num_ctx_saved_tensors + len(ctx_opaque_objects) + ) + if tangents_start_idx != expected_tangents_start: + raise AssertionError( + f"expected tangents_start_idx == {expected_tangents_start}, got {tangents_start_idx}" + ) + tangents_end_idx = len(all_args) - len(rng_args) - len(bw_tokens) + + # TODO: figure out how to refactor the backward properly + # so I can use aot_dispatch_subclass_wrapper() here. + if maybe_subclass_metadata is not None: + tangents = all_args[tangents_start_idx:tangents_end_idx] + + if len(tangents) != len(metadata.subclass_tangent_meta): + raise RuntimeError( + "The grad inputs should be same number as forward output tangents" + ) + + stack_traces = metadata.tangent_source_stack_traces or () + + flat_processed_tangents = list( + itertools.chain.from_iterable( + ( + AOTDispatchAutograd.process_runtime_tangent( + t, + m, + tangent_idx=idx, + tangent_desc=desc, + compile_id_str=metadata.compile_id_str, + tangent_stack_trace=stack_traces[idx] if stack_traces else None, + )[1] + ) + for idx, (t, m, desc) in enumerate( + zip( + tangents, + metadata.subclass_tangent_meta, + metadata.traced_tangents_descs, + ) + ) + ) + ) + + if codegen_unwrap_fn is not None: + unwrap = codegen_unwrap_fn + else: + unwrap = _unwrap_no_symints + all_args = ( + unwrap(all_args[:tangents_start_idx]) + + flat_processed_tangents + + unwrap(all_args[tangents_end_idx:]) + ) + else: + stack_traces = metadata.tangent_source_stack_traces or () + + all_args = [ + ( + AOTDispatchAutograd.process_runtime_tangent( + t, + metadata.subclass_tangent_meta[i - tangents_start_idx], + tangent_idx=i - tangents_start_idx, + tangent_desc=metadata.traced_tangents_descs[i - tangents_start_idx], + compile_id_str=metadata.compile_id_str, + tangent_stack_trace=( + stack_traces[i - tangents_start_idx] if stack_traces else None + ), + )[0] + if (tangents_start_idx <= i < tangents_end_idx) + else t + ) + for i, t in enumerate(all_args) + ] + + # Backward with forward inputs mutations is not supported in double backward. + if ( + torch.is_grad_enabled() + and metadata.indices_of_inputs_that_requires_grad_with_mutations_in_bw + ): + raise RuntimeError( + "aot_autograd does not support input mutations with requires_grad in backward for create_graph=True" + ) + + return all_args + + +def initialize_rng_states( + num_rng: int, + graphsafe_idx: int, + fwd_rng_states: list[torch.Generator], + bwd_rng_states: list[torch.Generator], +) -> None: + """ + Initialize the cudagraph safe rng states. + + Initialization of rng states should have a few properties: + - the initialization for each rng state should be independent + - the initialization should be deterministic + - the initialization should be based off current rng state, so that independent graphs do not + have equal rng behavior + + We defer initialization of rng states until runtime because compilation is wrapped + with preserve_rng_states. Seed initialization should advance the rng states so consecutive compilations + do not give equal randomness. + """ + with torch.utils._python_dispatch._disable_current_modes(): + seeds = torch.randint(0, torch.iinfo(torch.int64).max, (num_rng,), device="cpu") + fwd_rng_states.extend( + [ + torch.cuda.default_generators[graphsafe_idx] + .clone_state() + .manual_seed(int(seeds[i])) + for i in range(num_rng) + ] + ) + bwd_rng_states.extend( + [ + torch.cuda.default_generators[graphsafe_idx] + .clone_state() + .manual_seed(int(seeds[i])) + for i in range(num_rng) + ] + ) + + +# NOTE: this function must be torch._dynamo.allow_in_graph-able. Non tensor/symnode inputs must be constants. +def _backward_epilogue_functional( + metadata: ViewAndMutationMeta, + maybe_subclass_metadata: SubclassMeta | None, + out: Any, + *, + ctx_opaque_objects: Sequence[Any] = (), + make_subclass_override: Callable[..., Any] | None = None, + codegen_wrap_fn: Callable[..., Any] | None = None, +) -> tuple[Any, ...]: + # Toss out the backward output tokens + num_bw_tokens = metadata.num_backward_tokens + if num_bw_tokens > 0: + out = out[:-num_bw_tokens] + + # TODO: replace this with FunctionalizedRngRuntimeWrapper.post_compile + out = FunctionalizedRngRuntimeWrapper()._functionalized_rng_runtime_epilogue( + metadata, out, offset_index=len(out) - 1 + ) + out = tuple(out) + + # Replace compile-time opaque constants in the backward output with the + # real runtime opaques saved from the forward pass. During joint graph + # tracing, backward output opaques come from tangent constants (baked at + # compile time). At runtime we need the actual opaque objects that were + # saved for backward from the forward pass. + if ctx_opaque_objects: + opaque_iter = iter(ctx_opaque_objects) + out = tuple( + next(opaque_iter) if isinstance(v, FakeScriptObject) else v for v in out + ) + remaining = list(opaque_iter) + if remaining: + raise AssertionError( + f"ctx_opaque_objects had {len(remaining)} leftover entries " + "(expected all to be consumed by FakeScriptObject slots in backward output)" + ) + + # TODO: figure out how to refactor the backward properly so I can use aot_dispatch_subclass_wrapper() here. + if maybe_subclass_metadata is not None: + if maybe_subclass_metadata.grad_input_metas is None: + raise AssertionError("grad_input_metas must not be None") + if codegen_wrap_fn is not None and make_subclass_override is None: + return codegen_wrap_fn(out) + outs_wrapped = wrap_tensor_subclasses( + out, + subclass_metas=maybe_subclass_metadata.grad_input_metas, + included_subclass_symints=True, + is_runtime=True, + make_subclass_override=make_subclass_override, + ) + return outs_wrapped + return out + + +def coerce_to_expected_memory_format( + x: torch.Tensor, memory_format: MemoryFormatMeta +) -> torch.Tensor: + if memory_format.memory_format is not None: + # Coerce to torch.memory_format + if not x.is_contiguous(memory_format=memory_format.memory_format): + x = x.contiguous(memory_format=memory_format.memory_format) + return x + + expected_size = memory_format.size + if expected_size is None: + raise AssertionError("memory_format.size must not be None") + expected_stride = memory_format.stride + if expected_stride is None: + raise AssertionError("memory_format.stride must not be None") + # Expected size and stride are static ints + # ok to use == to compare runtime tensor strides and shapes + + if x.shape == expected_size and x.stride() == expected_stride: + # Runtime tangent size and stride are the same as expected, no need to coerce + return x + + # Empty_strided creates a raw Tensor. + # We are guaranteed that only raw Tensors has expected size and stride. + # Subclasses have only expected memory_format. + restrided = torch.empty_strided( + size=expected_size, + stride=expected_stride, + dtype=x.dtype, + device=x.device, + layout=x.layout, + requires_grad=x.requires_grad, + ) + restrided.copy_(x) + return restrided + + +@contextlib.contextmanager +def _disable_saved_tensors_hooks() -> Generator[None, None, None]: + error_message = ( + "Saved tensors hooks were specialized as GraphModules." + "In this case aot_autograd inlines them in forward and backward graph " + "and disables them during runtime of aot_autograd compiled region." + "If you see this error, that means that there is some unexpected push or pop manipulation " + "during aot_autograd compiled region runtime." + "Compilation with different hooks must result in recompilation." + ) + fail_if_non_empty = False + maybe_prev_message = None + try: + maybe_prev_message = ( + torch._C._autograd._saved_tensors_hooks_get_disabled_error_message() + ) + torch._C._autograd._saved_tensors_hooks_disable( + error_message, fail_if_non_empty + ) + yield + finally: + if maybe_prev_message is None: + torch._C._autograd._saved_tensors_hooks_enable() + else: + torch._C._autograd._saved_tensors_hooks_disable( + maybe_prev_message, fail_if_non_empty + ) + + +@dataclass +class SerializableCompiledFunction: + """ + Represents a result of AOTDispatch after calling the inner compiler + that can be serialized + """ + + compiled_fn: Callable[..., Any] + serialize_fn: Callable[[], Any] + + def __init__( + self, compiled_fn: Callable[..., Any], serialize_fn: Callable[[], Any] + ) -> None: + self.compiled_fn = compiled_fn + self.serialize_fn = serialize_fn + # Equivalent to functools.wraps + functools.update_wrapper( + self, + compiled_fn, + assigned=("__doc__", "__annotations__", "__type_params__"), + ) + + def serialize(self) -> Any: + return self.serialize_fn() + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + return self.compiled_fn(*args, **kwargs) + + +@dataclass +class AOTDispatchAutogradCompileSpec: + compiled_fw_func: Callable[..., Any] + compiled_bw_func: Callable[..., Any] | None + maybe_subclass_meta: SubclassMeta | None + num_symints_saved_for_bw: int + backward_state_indices: list[int] + disable_amp: bool + indices_of_inps_to_detach: list[int] + lazy_backward_info: ( + AutogradLazyBackwardCompileInfo | CachedAutogradLazyBackwardCompileInfo | None + ) + aot_config: AOTConfig + fw_metadata: ViewAndMutationMeta + try_save_cache_entry: Callable[..., Any] | None + + +@dataclass +class _AutogradSavedState: + metadata: ViewAndMutationMeta + + def save_from_forward(self, ctx: Any, fw_outs: Sequence[Any]) -> None: + tensors_saved_with_vc_check = fw_outs[ + self.metadata.tensors_saved_for_backwards_with_vc_check_slice + ] + tensors_saved_no_vc_check = fw_outs[ + self.metadata.tensors_saved_for_backwards_no_vc_check_slice + ] + if not all(isinstance(x, torch.Tensor) for x in tensors_saved_with_vc_check): + raise AssertionError( + "expected all tensors_saved_with_vc_check to be Tensors, " + f"got types: {[type(x) for x in tensors_saved_with_vc_check]}" + ) + if not all(isinstance(x, torch.Tensor) for x in tensors_saved_no_vc_check): + raise AssertionError( + "expected all tensors_saved_no_vc_check to be Tensors, " + f"got types: {[type(x) for x in tensors_saved_no_vc_check]}" + ) + + # See Note [Detaching saved tensors in AOTAutograd] + num_vc_check = len(tensors_saved_with_vc_check) + tensors_to_save = [ + x.detach() if x._is_view() else x for x in tensors_saved_with_vc_check + ] + tensors_no_vc_check = [ + x.detach() if x._is_view() else x for x in tensors_saved_no_vc_check + ] + + # dynamic_saved_tensors_idxs has indices relative to all saved tensors + # (vc_check + no_vc_check combined). Mark dynamics on the detached tensors. + for idx, dims in self.metadata.dynamic_saved_tensors_idxs.items(): + if idx < num_vc_check: + maybe_mark_dynamic_helper(tensors_to_save[idx], dims) + else: + maybe_mark_dynamic_helper(tensors_no_vc_check[idx - num_vc_check], dims) + + ctx.save_for_backward(*tensors_to_save) + ctx._tensors_no_vc_check = tensors_no_vc_check + + symint_outs = fw_outs[self.metadata.symints_saved_for_backwards_slice] + if not all( + isinstance(x, (int, float, torch.SymInt, torch.SymFloat)) + for x in symint_outs + ): + raise AssertionError( + "expected all symint_outs to be int/float/SymInt/SymFloat, " + f"got types: {[type(x) for x in symint_outs]}" + ) + ctx.symints = symint_outs + + opaque_object_outs = fw_outs[ + self.metadata.opaque_objects_saved_for_backwards_slice + ] + if not all( + is_opaque_type(type(obj)) or isinstance(obj, OpaqueBase) + for obj in opaque_object_outs + ): + raise AssertionError( + "expected all opaque_object_outs to be opaque types, " + f"got types: {[type(obj) for obj in opaque_object_outs]}" + ) + ctx.opaque_objects = opaque_object_outs + + def load_tensors(self, ctx: Any) -> Sequence[torch.Tensor]: + if len(ctx._tensors_no_vc_check) > 0: + return list(ctx.saved_tensors) + ctx._tensors_no_vc_check + return ctx.saved_tensors + + +@dataclass +class _AutogradForwardEpilogue: + metadata: ViewAndMutationMeta + + def finalize(self, ctx: Any, fw_outs: Sequence[Any]) -> tuple[Any, ...]: + num_outputs = self.metadata.num_outputs + num_outputs_aliased = self.metadata.num_outputs_aliased + num_mutated_runtime_inps = self.metadata.num_mutated_inp_runtime_indices + num_forward_returns = self.metadata.num_forward_returns + + raw_returns = list(fw_outs[:num_forward_returns]) + + # Wrap all autograd.Function.forward() outputs that are aliases + # so that autograd.Function doesn't treat them as tensors + if num_mutated_runtime_inps > 0: + for i, idx in enumerate(self.metadata.mutated_inp_runtime_indices): + # We could make this faster by only looping over inputs with metadata-only mutations + # (instead of looping over inputs with either data or metadata mutations), but there shouldn't be many. + info = self.metadata.input_info[idx] + if info.mutates_metadata and not info.mutates_data: + raw_returns[i] = TensorAlias(raw_returns[i]) + + if config.debug_assert: + user_mutated_inputs_raw = raw_returns[0:num_mutated_runtime_inps] + mut_inp_infos = [ + x + for x in self.metadata.input_info + if x.mutates_data or x.mutates_metadata + ] + if len(user_mutated_inputs_raw) != len(mut_inp_infos): + raise AssertionError( + "expected len(user_mutated_inputs_raw) == len(mut_inp_infos), " + f"got {len(user_mutated_inputs_raw)} != {len(mut_inp_infos)}" + ) + + if self.metadata.num_unsafe_view_outputs > 0: + for idx in self.metadata.unsafe_view_out_indices: + raw_return_idx = num_mutated_runtime_inps + idx + o = raw_returns[raw_return_idx] + raw_returns[raw_return_idx] = torch.ops.aten._unsafe_view(o, o.shape) + + if num_outputs_aliased > 0: + for idx in self.metadata.aliased_out_indices: + raw_return_idx = num_mutated_runtime_inps + idx + raw_returns[raw_return_idx] = TensorAlias(raw_returns[raw_return_idx]) + + if config.debug_assert: + intermediates_raw = raw_returns[ + num_mutated_runtime_inps + num_outputs : + ] + if any(isinstance(x, TensorAlias) for x in intermediates_raw): + raise AssertionError("expected no TensorAlias in intermediates_raw") + + # invariant: intermediate bases always require gradients, so we don't have to + # consider marking them as non-differentiable. + raw_returns_not_including_intermediate_bases = raw_returns[ + : num_mutated_runtime_inps + num_outputs + ] + raw_returns_meta = [ + x + for x in self.metadata.input_info + if x.mutation_type == MutationType.MUTATED_OUT_GRAPH + ] + self.metadata.output_info + + fw_outs_not_requiring_grad = [ + x + for (i, x) in enumerate(raw_returns_not_including_intermediate_bases) + if isinstance(x, torch.Tensor) and not raw_returns_meta[i].requires_grad + ] + ctx.mark_non_differentiable(*fw_outs_not_requiring_grad) + ctx._materialize_non_diff_grads = False + return tuple(raw_returns) + + +@dataclass +class _AutogradRngStateTracker: + num_rng: int + graphsafe_idx: int | None + fwd_rng_states: list[torch.Generator] = field(default_factory=list) + bwd_rng_states: list[torch.Generator] = field(default_factory=list) + curr_fwd_iter: Any = field(default_factory=lambda: itertools.count(0)) + backward_state_position: int = 0 + pending_forwards: set[int] = field(default_factory=set) + saved_backward_tensor_states: dict[int, list[torch.Tensor]] = field( + default_factory=dict + ) + + def add_forward_args(self, ctx: Any, args: tuple[Any, ...]) -> tuple[Any, ...]: + if self.num_rng == 0: + return args + + if len(self.fwd_rng_states) == 0: + if self.graphsafe_idx is None: + raise AssertionError("graphsafe_idx must not be None when num_rng > 0") + initialize_rng_states( + self.num_rng, + self.graphsafe_idx, + self.fwd_rng_states, + self.bwd_rng_states, + ) + + curr_iter = next(self.curr_fwd_iter) + ctx._curr_iter = curr_iter + + # if this state is not contained in the backward, + # we need to save it for when its backward pass happens + if curr_iter != self.backward_state_position: + self.saved_backward_tensor_states[curr_iter] = [ + rng_state.get_state() for rng_state in self.fwd_rng_states + ] + + self.pending_forwards.add(curr_iter) + return (*args, *self.fwd_rng_states) + + def add_backward_args(self, ctx: Any, all_args: list[Any]) -> None: + if self.num_rng == 0: + return + + curr_backward_iter = ctx._curr_iter + retain_graph = torch._C._autograd._get_current_graph_task_keep_graph() + + # Save current state if we have a pending forward that needs this state + # or this state may be needed again because of retain graph + if ( + self.backward_state_position in self.pending_forwards + and self.backward_state_position not in self.saved_backward_tensor_states + and (self.backward_state_position != curr_backward_iter or retain_graph) + ): + self.saved_backward_tensor_states[self.backward_state_position] = [ + rng_state.get_state() for rng_state in self.bwd_rng_states + ] + + # Restore saved states if needed + if curr_backward_iter in self.saved_backward_tensor_states: + if self.backward_state_position != curr_backward_iter: + for bwd_state, saved_state in zip( + self.bwd_rng_states, + self.saved_backward_tensor_states[curr_backward_iter], + ): + bwd_state.set_state(saved_state) + if not retain_graph: + del self.saved_backward_tensor_states[curr_backward_iter] + else: + if self.backward_state_position != curr_backward_iter: + raise AssertionError( + "expected backward_state_position == curr_backward_iter, " + f"got {self.backward_state_position} != {curr_backward_iter}" + ) + + self.backward_state_position = curr_backward_iter + 1 + if not retain_graph: + self.pending_forwards.remove(curr_backward_iter) + all_args.extend(self.bwd_rng_states) + + +@dataclass +class _AutogradBackwardCompiler: + compiled_bw: Callable[..., Any] | None + lazy_backward_info: ( + AutogradLazyBackwardCompileInfo | CachedAutogradLazyBackwardCompileInfo | None + ) + disable_amp: bool + aot_config: AOTConfig + fw_metadata: ViewAndMutationMeta + try_save_cache_entry: Callable[..., Any] | None + + def get_or_compile(self, *, saved_tensors_use_once: bool) -> Callable[..., Any]: + if self.compiled_bw is not None: + return self.compiled_bw + + if self.lazy_backward_info is None: + raise AssertionError("lazy_backward_info must not be None") + if not isinstance(self.lazy_backward_info, AutogradLazyBackwardCompileInfo): + raise AssertionError( + "expected AutogradLazyBackwardCompileInfo, " + f"got {type(self.lazy_backward_info)}" + ) + + self._prepare_lazy_backward_context(saved_tensors_use_once) + + bw_module = self.lazy_backward_info.bw_module + placeholder_list = self.lazy_backward_info.placeholder_list + saved_context = self.lazy_backward_info.saved_context + saved_compile_context = self.lazy_backward_info.saved_compile_context + + context = torch._C._DisableAutocast if self.disable_amp else nullcontext + metrics_context = get_metrics_context() + with ( + tracing(saved_context), + compile_context(saved_compile_context), + context(), + track_graph_compiling(self.aot_config, "backward"), + metrics_context, + dynamo_timed( + "backward._backward_impl", + phase_name="entire_backward_compile", + log_pt2_compile_event=True, + dynamo_compile_column_us="backward_cumulative_compile_time_us", + log_waitcounter=True, + waitcounter_name_override="entire_backward_compile", + ), + callback_handler.install_callbacks( + CallbackTrigger.LAZY_BACKWARD, + str(CompileContext.current_compile_id()), + ), + ): + CompileEventLogger.compilation_metric(is_forward=False) + # See Note: [Backward graph lazy lowering] + if self.aot_config.bw_compiler is None: + raise AssertionError("aot_config.bw_compiler must not be None") + self.compiled_bw = self.aot_config.bw_compiler( + copy.deepcopy(bw_module), placeholder_list + ) + # Maybe save cache entry + if self.try_save_cache_entry is not None: + self.try_save_cache_entry( + self.compiled_bw, + bw_module, + self.fw_metadata, + self.aot_config, + ) + + return self.compiled_bw + + def _prepare_lazy_backward_context(self, saved_tensors_use_once: bool) -> None: + if self.lazy_backward_info is None: + raise AssertionError("lazy_backward_info must not be None") + if not isinstance(self.lazy_backward_info, AutogradLazyBackwardCompileInfo): + raise AssertionError( + "expected AutogradLazyBackwardCompileInfo, " + f"got {type(self.lazy_backward_info)}" + ) + + if ( + hasattr(self.lazy_backward_info, "saved_context") + and self.lazy_backward_info.saved_context is not None + ): + if not isinstance(self.lazy_backward_info.saved_context, TracingContext): + raise AssertionError( + f"expected TracingContext, got {type(self.lazy_backward_info.saved_context)}" + ) + ddp_ctx = self.lazy_backward_info.saved_context.ddp_optimizer_ctx + if ddp_ctx is not None: + if ddp_ctx.curr_bucket < 0: + raise AssertionError( + "expected same # of fw and bw compiles, " + f"but found bucket {ddp_ctx.curr_bucket}" + ) + curr_fw_meta = ddp_ctx.metadata_per_bucket[ddp_ctx.curr_bucket] + # Note [DDPOptimizer and fw_metadata] + # When using the DDPOptimizer, we have a single dynamo graph (and TracingContext), + # but multiple AOTDispatcher graph. + # + # One consequence is that there will be **multiple** fw_metadata objects, one per AOT graph, + # which we stash the fw_metadata on the TracingContext. + # + # Normally what happens is that as we compile AOT graphs 1...N, we clobber the fw_metadata + # for graph i-1 when we start running AOT for graph i. + # Ordinarily this is fine, because inductor no longer needs the metadata from graph i-1. + # + # However, this is a problem for lazy compilation of the backward. During backward compilation, + # we compile the backward lazily at backward runtime, meaning that we will first compile + # backward graph N, N-1, ..., 1. + # We need to ensure that at the time inductor compiles bw graph N-1, it can access + # the corresponding fw_metadta for graph N-1. + # + # We do this by stashing a DDPOptimizerContext, which tracks: + # - the metadata of all N graphs + # - the graph we are currently compiling in our DDPOptimizer region. + ddp_ctx.curr_bucket -= 1 + self.lazy_backward_info.saved_context.fw_metadata = curr_fw_meta + + if not saved_tensors_use_once: + self.fw_metadata.bw_donated_idxs = [] + # Update bw_donated_idxs if using lazy_backward_info from `aot_dispatch_autograd` + if ( + hasattr(self.lazy_backward_info, "saved_context") + and hasattr(self.lazy_backward_info.saved_context, "fw_metadata") + and hasattr( + self.lazy_backward_info.saved_context.fw_metadata, # type: ignore[union-attr] + "bw_donated_idxs", + ) + ): + self.lazy_backward_info.saved_context.fw_metadata.bw_donated_idxs = ( # type: ignore[union-attr] + # pyrefly: ignore [implicit-any] + [] + ) + + +@dataclass +class _AOTDispatchAutogradFunctionFactory: + spec: AOTDispatchAutogradCompileSpec + + def build(self) -> type[torch.autograd.Function]: + compile_id = CompileContext.current_compile_id() + compile_id_str = str(compile_id) if compile_id is not None else None + self.spec.fw_metadata.compile_id_str = compile_id_str + + saved_state = _AutogradSavedState(self.spec.fw_metadata) + forward_epilogue = _AutogradForwardEpilogue(self.spec.fw_metadata) + rng_state = _AutogradRngStateTracker( + num_rng=self.spec.fw_metadata.num_graphsafe_rng_states, + graphsafe_idx=self.spec.fw_metadata.graphsafe_rng_state_index, + ) + backward_compiler = _AutogradBackwardCompiler( + compiled_bw=self.spec.compiled_bw_func, + lazy_backward_info=self.spec.lazy_backward_info, + disable_amp=self.spec.disable_amp, + aot_config=self.spec.aot_config, + fw_metadata=self.spec.fw_metadata, + try_save_cache_entry=self.spec.try_save_cache_entry, + ) + + compiled_fw_func = self.spec.compiled_fw_func + compiled_bw_func = self.spec.compiled_bw_func + maybe_subclass_meta = self.spec.maybe_subclass_meta + num_symints_saved_for_bw_ = self.spec.num_symints_saved_for_bw + backward_state_indices = self.spec.backward_state_indices + disable_amp = self.spec.disable_amp + lazy_backward_info = self.spec.lazy_backward_info + aot_config = self.spec.aot_config + fw_metadata = self.spec.fw_metadata + + _codegen_bw_unwrap_fn = None + _codegen_bw_wrap_fn = None + if maybe_subclass_meta is not None: + from .subclass_codegen import codegen_backward_subclass_fns + + _codegen_bw_unwrap_fn, _codegen_bw_wrap_fn = codegen_backward_subclass_fns( + grad_input_metas=maybe_subclass_meta.grad_input_metas, + ) + + class CompiledFunction(torch.autograd.Function): + compiled_fw = compiled_fw_func + compiled_bw = compiled_bw_func + metadata: ViewAndMutationMeta = fw_metadata # type: ignore[assignment] + maybe_subclass_metadata: SubclassMeta | None = maybe_subclass_meta + num_symints_saved_for_bw = num_symints_saved_for_bw_ + _aot_id = aot_config.aot_id + _lazy_backward_info = lazy_backward_info + _bw_epilogue_wrap_fn = _codegen_bw_wrap_fn + _bw_prologue_unwrap_fn = _codegen_bw_unwrap_fn + + @staticmethod + def _compiled_autograd_key(ctx: Any) -> tuple[Any, ...]: + return (ctx._autograd_function_id, *ctx.symints) + + @staticmethod + # pyrefly: ignore [bad-override] + def forward(ctx: Any, *deduped_flat_tensor_args: Any) -> Any: + args = deduped_flat_tensor_args + if backward_state_indices: + bw_state = args[backward_state_indices[0]] + if not isinstance(bw_state, BackwardState): + raise AssertionError( + f"expected BackwardState, got {type(bw_state)}" + ) + ctx._compiled_autograd_backward_state = bw_state + + args = rng_state.add_forward_args(ctx, args) + + # There is a pretty complicated calling convention around what the compiled fw returns. + # The full list of outputs and their relative order is: + # (*tokens, *mutated_inputs, *fw_outs, *fw_intermediate_bases, *saved_tensors, *saved_symints) + # - Note that in the synthetic bases case, mutated_inputs will correspond to an updated version + # of the original view, and not the synthetic base + # - Note that donated buffer logic requires (*saved_tensors, *saved_symints) showing up last + # in the fw output order. + fw_outs = call_func_at_runtime_with_args( + CompiledFunction.compiled_fw, + # pyrefly: ignore [bad-argument-type] + args, + disable_amp=disable_amp, + ) + + saved_state.save_from_forward(ctx, fw_outs) + return forward_epilogue.finalize(ctx, fw_outs) + + @staticmethod + def backward(ctx: Any, *flat_args: Any) -> tuple[Any, ...]: + all_args = _backward_prologue_functional( + saved_state.load_tensors(ctx), + ctx.symints, + ctx.opaque_objects, + CompiledFunction.metadata, + CompiledFunction.maybe_subclass_metadata, + *flat_args, + codegen_unwrap_fn=CompiledFunction._bw_prologue_unwrap_fn, + ) + rng_state.add_backward_args(ctx, all_args) + + def impl_fn(double_ctx: Any = None) -> Any: + out = CompiledFunction._backward_impl(ctx, all_args) + return _backward_epilogue_functional( + CompiledFunction.metadata, + CompiledFunction.maybe_subclass_metadata, + out, + codegen_wrap_fn=CompiledFunction._bw_epilogue_wrap_fn, + ) + + if ( + torch._C._is_key_in_tls("context") + and (config_ctx := torch._C._get_obj_in_tls("context")) is not None + ): + impl_fn = functools.partial(config_ctx.run, impl_fn) + + needs_grad = torch.is_grad_enabled() and any( + t.requires_grad for t in all_args if isinstance(t, torch.Tensor) + ) + if needs_grad: + # double backward + return CompiledFunction._double_backward(ctx, impl_fn, all_args) + return impl_fn() + + @staticmethod + def _double_backward( + ctx: Any, impl_fn: Callable[..., Any], all_args: list[Any] + ) -> Any: + # Ensure that the graph is connected, and error if double backward is performed. + # See comment for why once_differentiable is not sufficient: + # https://github.com/pytorch/pytorch/pull/92348/files#r1072962107 + class CompiledFunctionBackward(torch.autograd.Function): + # CompiledFunctionBackward is not yet supported in dynamo skipfiles + _aot_id = aot_config.aot_id + + @staticmethod + # pyrefly: ignore [bad-override] + def forward(double_ctx: Any, *unused_args: Any) -> Any: + return impl_fn(double_ctx) + + @staticmethod + def backward(ctx: Any, *args: Any) -> None: + raise RuntimeError( + "torch.compile with aot_autograd does not currently support double backward" + ) + + CompiledFunctionBackward._compiled_autograd_key = ( # type: ignore[method-assign] + CompiledFunction._compiled_autograd_key + ) + + return CompiledFunctionBackward.apply(*all_args) + + @staticmethod + def _backward_impl(ctx: Any, all_args: list[Any]) -> Any: + # compiled autograd reimplements this function at proxy_call_aot_backward + if backward_state_indices: + raise AssertionError("BackwardState requires CompiledAutograd") + ctx.maybe_clear_saved_tensors() + + saved_tensors_use_once = ( + not torch._C._autograd._get_current_graph_task_keep_graph() + ) + compiled_bw = backward_compiler.get_or_compile( + saved_tensors_use_once=saved_tensors_use_once + ) + CompiledFunction.compiled_bw = compiled_bw + + if ( + torch._functorch.config.donated_buffer + and not saved_tensors_use_once + and fw_metadata.bw_donated_idxs != [] + ): + torch._check( + False, + lambda: ( + "This backward function was compiled with non-empty donated " + "buffers which requires create_graph=False and retain_graph=False. " + "Please keep backward(create_graph=False, retain_graph=False) " + "across all backward() function calls, or set " + "torch._functorch.config.donated_buffer=False to disable " + "donated buffer." + ), + ) + + return call_func_at_runtime_with_args( + compiled_bw, + all_args, + steal_args=True, + disable_amp=disable_amp, + ) + + return CompiledFunction + + +# This is wrapped in a class just for namespacing purposes +# No need to make it into an actual CompilerWrapper because it doesn't fit the abstract as cleanly +class AOTDispatchAutograd: + @staticmethod + def _raise_tangent_metadata_error( + expected_type: type | None, + expected_meta: Any, + runtime_type: type, + runtime_meta: Any, + orig_x: torch.Tensor, + tangent_idx: int | None, + tangent_desc: Any | None, + compile_id_str: str | None, + tangent_stack_trace: str | None, + ) -> RuntimeError: + expected_subclass_got_plain_tensor = ( + expected_type is not None + and expected_type is not torch.Tensor + and runtime_type is torch.Tensor + ) + if expected_subclass_got_plain_tensor: + tangent_msg = "" + if tangent_idx is not None: + tangent_msg = f" (tangent index: {tangent_idx})" + + output_hint = "" + if tangent_desc is not None: + from .descriptors import PlainAOTOutput, TangentAOTInput + + if isinstance(tangent_desc, TangentAOTInput) and isinstance( + tangent_desc.output, PlainAOTOutput + ): + idx = tangent_desc.output.idx + output_hint = f"\n\nThe problematic output is: forward output at index {idx} (0-indexed)" + else: + output_hint = ( + f"\n\nThe problematic output is: {tangent_desc.expr()}" + ) + + graph_hint = "" + if compile_id_str is not None: + graph_hint = ( + f"\n\nThis error occurred in compiled graph [{compile_id_str}]." + ) + + stack_trace_hint = "" + if tangent_stack_trace is not None: + stack_trace_hint = ( + f"\n\nThe forward output was created here:\n{tangent_stack_trace}" + ) + + return RuntimeError( + f""" +During the backward, we encountered a tensor subclass where we guessed its +metadata incorrectly. +Expected a {expected_type.__name__} tangent but got a plain Tensor{tangent_msg}. +This happens when a compiled function returns multiple outputs that +require gradients, but .backward() is only called on some of them. +To fix: call .detach() on forward outputs you don't need gradients for.{output_hint}{graph_hint}{stack_trace_hint} + +This error is also more likely to occur if your compiled model is suffering +from a large number of graph breaks. For more advice on finding and fixing +graph breaks, see: +https://docs.pytorch.org/docs/stable/user_guide/torch_compiler/compile/programming_model.graph_breaks_index.html + +For more info about this error, see: +https://github.com/pytorch/pytorch/issues/172556""" + ) + else: + return RuntimeError( + f""" +During the backward, we encountered a tensor subclass where we guessed its +metadata incorrectly. +Expected: {expected_meta} (type {expected_type}), +got: {runtime_meta} (type {runtime_type}), shape: {orig_x.shape}. +Your tensor subclass must implement __coerce_same_metadata_as_tangent__.""" + ) + + @staticmethod + def process_runtime_tangent( + x: Any, + meta: PlainTensorMeta | SubclassCreationMeta, + tangent_idx: int | None = None, + tangent_desc: Any | None = None, + compile_id_str: str | None = None, + tangent_stack_trace: str | None = None, + ) -> tuple[Any, list[Any]]: + if not isinstance(x, torch.Tensor): + return x, [x] + + if isinstance(x, FakeTensor): + if not meta.memory_format: + raise AssertionError( + "meta.memory_format must not be None for FakeTensor" + ) + x = coerce_to_expected_memory_format(x, meta.memory_format) + return x, [x] + + expected_type: type | None = torch.Tensor + expected_meta = None + if isinstance(meta, SubclassCreationMeta): + expected_type = meta.original_subclass_type + expected_meta = meta.meta + + runtime_type = type(x) + # When we're inside compiled autograd's AOTDispatcher step, + # regular Tensors look like FunctionalTensors. + # Tensor subclasses still look like Tensor subclasses though. + if isinstance(x, torch._subclasses.functional_tensor.FunctionalTensor): + runtime_type = torch.Tensor + + runtime_meta = None + runtime_subclass_keys: Sequence[str] = [] + + if is_traceable_wrapper_subclass(x): + runtime_subclass_keys, runtime_meta = x.__tensor_flatten__() + + def maybe_coerce(x: torch.Tensor) -> torch.Tensor | None: + same_type: bool = expected_type == runtime_type + same_meta: bool = expected_meta == runtime_meta + + if same_type and same_meta: + return x + + if not hasattr(x, "__coerce_same_metadata_as_tangent__"): + return None + + if same_type: + # Backward Compatibility, as some Subclass impls can have original 1-arg function. + return x.__coerce_same_metadata_as_tangent__(expected_meta) + + return x.__coerce_same_metadata_as_tangent__(expected_meta, expected_type) + + # Coerce to expected type and metadata + orig_x = x + x = maybe_coerce(x) + if x is None: + raise AOTDispatchAutograd._raise_tangent_metadata_error( + expected_type, + expected_meta, + runtime_type, + runtime_meta, + orig_x, + tangent_idx, + tangent_desc, + compile_id_str, + tangent_stack_trace, + ) + + # Coerce to expected memory format + if not meta.memory_format: + raise AssertionError("meta.memory_format must not be None") + x = coerce_to_expected_memory_format(x, meta.memory_format) + + if not is_traceable_wrapper_subclass(x): + return x, [x] + + if not isinstance(meta, SubclassCreationMeta): + raise AssertionError(f"expected SubclassCreationMeta, got {type(meta)}") + if orig_x is not x: + runtime_subclass_keys = x.__tensor_flatten__()[0] + + if len(meta.attrs) != len(runtime_subclass_keys): + raise AssertionError( + f"expected len(meta.attrs) == len(runtime_subclass_keys), " + f"got {len(meta.attrs)} != {len(runtime_subclass_keys)}" + ) + leaves = [] + for attr, attr_meta in meta.attrs.items(): + if isinstance(attr_meta, OpaqueMeta): + # Opaques aren't differentiable but occupy a flat arg slot. + leaves.append(getattr(x, attr)) + continue + elem = getattr(x, attr) + new_elem, elem_leaves = AOTDispatchAutograd.process_runtime_tangent( + elem, attr_meta + ) + if new_elem is not elem: + setattr(x, attr, new_elem) + leaves.extend(elem_leaves) + + return x, leaves + + @staticmethod + def post_compile(spec: AOTDispatchAutogradCompileSpec) -> Callable[..., Any]: + compiled_function_cls = _AOTDispatchAutogradFunctionFactory(spec).build() + return RuntimeWrapper( + indices_of_inps_to_detach=spec.indices_of_inps_to_detach, + trace_joint=True, + disable_amp=spec.disable_amp, + ).post_compile( + compiled_function_cls.apply, + spec.aot_config, + runtime_metadata=spec.fw_metadata, + ) + + +@dataclass +class DebugAssertWrapper(CompilerWrapper): + flat_requires_grad: list[bool | None] = field(default_factory=list) + + def post_compile( + self, + compiled_fn: Callable[..., Any], + aot_config: AOTConfig, + *, + runtime_metadata: ViewAndMutationMeta, + ) -> Callable[..., Any]: + lines = ["def inner_fn(args):"] + globals_dict: dict[str, object] = {"compiled_fn": compiled_fn} + for i, can_require_grad in enumerate(self.flat_requires_grad): + if can_require_grad is None: + lines.append( + f" if isinstance(args[{i}], Tensor):" + f" raise AssertionError(" + f"'expected non-Tensor for arg {i}, got Tensor')" + ) + elif not can_require_grad: + msg_name = f"_msg_{i}" + globals_dict[msg_name] = format_guard_bug_msg( + aot_config, + f"{describe_input(i, aot_config)} would not require grad", + ) + lines.append( + f" if args[{i}].requires_grad: raise AssertionError({msg_name})" + ) + lines.append(" return compiled_fn(args)") + + source = "\n".join(lines) + globals_dict["Tensor"] = Tensor + + from .subclass_codegen import _compile_and_exec_source + + return _compile_and_exec_source( + source, + globals_dict, + "inner_fn", + "debug_assert_wrapper", + wrapped_fn=compiled_fn, + ) + + +def pre_compile( + wrappers: list[CompilerWrapper], + flat_fn: TraceFn, + flat_args: list[FxValue], + flat_args_descs: list[AOTInput], + aot_config: AOTConfig, + *, + fw_metadata: ViewAndMutationMeta, +) -> tuple[TraceFn, list[FxValue], list[AOTInput], ViewAndMutationMeta]: + """ + Runs a sequence of wrappers on the given function and arguments. + Mutates wrappers in place. + """ + for wrapper in wrappers: + flat_fn, flat_args, flat_args_descs, fw_metadata = wrapper.pre_compile( + flat_fn, flat_args, flat_args_descs, aot_config, fw_metadata=fw_metadata + ) + return flat_fn, flat_args, flat_args_descs, fw_metadata + + +def post_compile( + wrappers: list[CompilerWrapper], + compiled_fn: Callable[..., Any], + aot_config: AOTConfig, + *, + runtime_metadata: ViewAndMutationMeta, +) -> tuple[Callable[..., Any], ViewAndMutationMeta]: + """ + Runs a sequence of wrappers on the given function. Should be called after pre_compile() + """ + for wrapper in reversed(wrappers): + compiled_fn = wrapper.post_compile( + compiled_fn, aot_config, runtime_metadata=runtime_metadata + ) + return compiled_fn, runtime_metadata + + +def make_runtime_safe( + fw_metadata: ViewAndMutationMeta, + maybe_subclass_meta: SubclassMeta | None, +) -> None: + """ + Calls make_runtime_safe on all ViewAndMutationMetas. + Modifies both arguments. Allows ViewAndMutationMetas to + be safely cached in AOTAutogradCache. + """ + fw_metadata.make_runtime_safe() + if maybe_subclass_meta is not None: + maybe_subclass_meta.fw_metadata.make_runtime_safe() + if maybe_subclass_meta.grad_input_metas: + for meta in maybe_subclass_meta.grad_input_metas: + if isinstance(meta, SubclassCreationMeta): + meta.make_runtime_safe() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/schemas.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/schemas.py new file mode 100644 index 0000000000000000000000000000000000000000..809f389b3ed5619fb5bc9b3d24677fa005bb8757 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/schemas.py @@ -0,0 +1,1484 @@ +""" +The various dataclasses, Enums, namedtuples etc used in AOTAutograd. This includes +input/output types, metadata, config, function signatures etc. +""" + +from __future__ import annotations + +import collections +import functools +from dataclasses import dataclass, field, replace +from enum import Enum +from typing import Any, NewType, Protocol, TYPE_CHECKING, TypeVar +from typing_extensions import ParamSpec + +import torch +import torch.utils._pytree as pytree +from torch import SymInt, Tensor +from torch._opaque_base import OpaqueBase +from torch._subclasses import FakeTensor, FakeTensorMode +from torch._subclasses.fake_tensor import is_fake +from torch.fx.experimental._backward_state import BackwardState +from torch.utils._python_dispatch import is_traceable_wrapper_subclass + +from .. import config +from .functional_utils import _check_if_mutation_can_be_in_graph, ViewMetaSequence +from .utils import strict_zip + + +if TYPE_CHECKING: + import contextlib + from collections.abc import Callable, Iterable, Sequence + + from torch._guards import Source + from torch._inductor.output_code import OutputCode + from torch._inductor.utils import InputType + from torch._ops import OpOverload + from torch.types import IntLikeType + + from .descriptors import AOTInput, AOTOutput + from .graph_capture_wrappers import JointFnHandle + +_P = ParamSpec("_P") +_R = TypeVar("_R") +zip = strict_zip + + +OutputType = Enum( + "OutputType", + ( + # output is not an alias + "non_alias", + # output aliases an input + "alias_of_input", + # output **is** an input tensor + "is_input", + # output has a ._base tensor, which is a graph intermediate. + # We need to return its ._base as a graph output, + # so its requires_grad info is populated correctly. + # Instructs the runtime code to regenerate the current output + # from a base tensor, graph_intermediates[base_idx] + "alias_of_intermediate_save_as_output", + # Same as above; but we don't need to explicitly add its ._base + # as a graph output, because it already **is** a graph output. + "alias_of_intermediate", + # Same as above; but the output's ._base is **already** a user output. + # Instructs the runtime code to regenerate the current output from + # a base tensor, user_outputs[base_idx] + "alias_of_intermediate_base_is_user_output", + # See Note [Intermediate Bases Optimization] + "unsafe_view_alias", + # output is an alias, but has a custom autograd.Function backward. + # In this case, we don't want to do view-replay, since we won't be able to replay the custom function. + # Instead, we'll treat this output "normally", and trace its backward into the graph. + "custom_function_view", + ), +) + + +# This class stores info about every user output. +@dataclass(frozen=True) +class OutputAliasInfo: + # Tells us if this output is: + # (1) a regular (non-aliased) output + # (2) an alias of a forward input + # (3) **is** a forward input (special case of "alias_of_input") + # (4) an alias of an intermediate (aka an alias of an output of the inner traced forward) + # (5) an alias of an intermediate, that explicitly requires returning the intermediate + # as a graph output + # (6) an alias of an intermediate, where that intermediate is also a user output + output_type: OutputType + # The raw type of the output (torch.Tensor, SymInt, etc) + raw_type: type + # If (1) above, then + # - base_idx is None + # If (2) or (3) above, then + # - Tells us that the base of this alias is user_fwd_input[base_idx] + # (This is an index into the inputs *before* we make synthetic bases) + # If (4) or (5) above, then + # - Tells us that the base of this alias is output_graph_intermediates[base_idx] + # here, this refers to the index of the *direct* traced + # If (6) above, then: + # - Tells us that the base of this alias is output_user_fwds[base_idx] + # here, this refers to the index of the *direct* traced + base_idx: int | None + # If it is a Tensor, what the dynamic dims are (otherwise is None) + dynamic_dims: set[int] | None + # Whether the output tensor has requires_grad=True. Use for + # preserving output properties (mark_non_differentiable, alias reconstruction). + requires_grad: bool + # Whether this output participates in backward as + # a tangent. Normally the same as requires_grad, but False for views created + # under no_grad() that inherit requires_grad from their base without having a + # grad_fn. Use when constructing tangent lists for torch.autograd.grad(). + requires_grad_for_backward: bool + # Sequence of ViewMeta objects. + # + # Provides us the means to re-run view functions on other tensors. + # + # We need to wrap the actual list of ViewMeta with this class so that + # we compare the ViewMeta elements appropriately, i.e. their type and + # the elements returned by the `as_tuple()` call. + view_meta_sequence: ViewMetaSequence | None = None + + +class MutationType(Enum): + NOT_MUTATED = 1 + MUTATED_IN_GRAPH = 2 + MUTATED_OUT_GRAPH = 3 + + +# This class tells us info about user inputs. +@dataclass(frozen=True) +class InputAliasInfo: + is_leaf: bool + mutates_data: bool + mutates_metadata: bool + mutations_hidden_from_autograd: bool + mutations_under_no_grad_or_inference_mode: bool + mutation_inductor_storage_resize: bool + mutates_storage_metadata: bool + requires_grad: bool + keep_input_mutations: bool + + def __post_init__(self) -> None: + if self.mutates_storage_metadata: + # For convenience, we guarantee that this is always true. + # In practice, If we call .set_(), then at runtime there is no need + # to additionally fix up the tensor metadata, since our runtime + # call to inp.set_(updated_inp) will already have the right metadata + if not self.mutates_metadata: + raise AssertionError( + "mutates_storage_metadata requires mutates_metadata to be True" + ) + + @functools.cached_property + def mutation_type(self) -> MutationType: + if ( + (not self.mutates_data) + and (not self.mutates_metadata) + and not (self.mutation_inductor_storage_resize) + ): + return MutationType.NOT_MUTATED + + if _check_if_mutation_can_be_in_graph( + self.keep_input_mutations, + self.mutates_data, + self.mutates_metadata, + self.mutations_hidden_from_autograd, + self.mutations_under_no_grad_or_inference_mode, + self.mutates_storage_metadata, + self.mutation_inductor_storage_resize, + self.requires_grad, + ): + return MutationType.MUTATED_IN_GRAPH + + return MutationType.MUTATED_OUT_GRAPH + + +@dataclass +class MemoryFormatMeta: + # For static shapes we assume tangents have the same strideness as outputs + size: Sequence[int] | None = None + stride: Sequence[int] | None = None + + # For dynamic shapes we assume the same memory format: contiguous, channels_last etc. + memory_format: torch.memory_format | None = None + + @staticmethod + def from_tensor( + t: torch.Tensor, force_use_memory_format: bool = False + ) -> MemoryFormatMeta | None: + # We only memorize expected memory format for + # 1. Traceable wrapper subclasses + # We can not create restrided subclass tensor, as torch.empty_strided works only with dense tensors. + # 2. Dynamic shape tensors + # Support for symbolic shapes is not implemented yet. + # 3. force_use_memory_format=True (e.g., local_map where shapes change) + use_memory_format: bool = ( + force_use_memory_format + or not torch._functorch.config.guess_tangent_strides_as_outputs + or is_traceable_wrapper_subclass(t) + ) + if not use_memory_format: + use_memory_format = t._has_symbolic_sizes_strides + + if use_memory_format: + return MemoryFormatMeta( + # pyrefly: ignore [unbound-name] + memory_format=torch._prims_common.suggest_memory_format(t), + ) + + return MemoryFormatMeta( + size=t.size(), + stride=t.stride(), + ) + + +@dataclass +class PlainTensorMeta: + unwrapped_idx: int + memory_format: MemoryFormatMeta | None = None + + +@dataclass +class OpaqueMeta: + pass + + +@dataclass +class SubclassCreationMeta: + """ + Used for AOTDispatch. + This dataclass gives us the information we need to reconstruct a tensor subclass + from our flat inputs. + Why is this important? The graph that we'd like to trace out contains flat tensor inputs, + But the user's original model may have subclass inputs and outputs. + So we need to wrap/unwrap subclasses as necessary to translate between the user's + view (subclass inps/outs), and the backend compiler's view (graph with no subclass args). + + Complications arise mostly from the fact that a subclass can hold more than one inner tensor; + So for a given subclass input/output, we need to carefully track which indices map + to the subclass tensor in the corresponding "dense-tensor-only" graph. + """ + + # In the inner graph that only takes in dense tensor inputs, + # this maps to the first index of "tensors that should go in this subclass wrapper" + flat_tensor_start_idx: int + # arg_count is inclusive of the arg_counts of any + # inner tensor subclasses: If I have a TwoTensor and + # both of its inner elements are TwoTensors, then the + # arg_count of the outer-most subclass will be 4 + arg_count: int + # Mark where or not symints were included. This flag is only used in one assertion + # in "wrap_tensor_subclasses" + included_subclass_symints: bool + # meta and attrs are produced by the subclass's __tensor_flatten__. + # We need to keep them around along with outer_size / outer_stride to plumb them + # into __tensor_unflatten__ + attrs: dict[str, SubclassCreationMeta | PlainTensorMeta | OpaqueMeta] + outer_size: Iterable[IntLikeType | None] + outer_stride: Iterable[IntLikeType | None] + meta: Any + # Stores the original subclass itself. + # This is needed because we need the autograd metadata on the original subclass + # (this is guaranteed to be a wrapper subclass that holds a fake tensor, + # so holding onto this at runtime shouldn't leak memory) + # This field is nulled out after calling make_runtime_safe() + original_subclass: torch.Tensor | None + + # Used at runtime to determine the subclass type, so we don't need to save the original subclass + original_subclass_type: type | None = None + memory_format: MemoryFormatMeta | None = None + + def compute_outer_size_and_stride( + self, + all_args: Sequence[torch.Tensor | IntLikeType | OpaqueBase], + *, + curr_start_idx: int, + ) -> tuple[ + Iterable[IntLikeType | None], + Iterable[IntLikeType | None], + ]: + from .subclass_utils import compute_symint_placeholders + + def compute( + outer: Iterable[IntLikeType | None], start_idx: int + ) -> tuple[Any, int | Any]: + placeholders = compute_symint_placeholders(outer) + has_symbolic = any(placeholders) + + if has_symbolic: + end = start_idx + sum(placeholders) + it_args = iter(all_args[start_idx:end]) + it_placeholders = iter(placeholders) + return pytree.tree_map_only( + lambda _: next(it_placeholders), lambda _: next(it_args), outer + ), end + else: + return outer, start_idx + + outer_size, next_idx = compute(self.outer_size, curr_start_idx) + outer_stride, _ = compute(self.outer_stride, next_idx) + return outer_size, outer_stride + + def creation_fn( + self, + all_args: Sequence[torch.Tensor | IntLikeType | OpaqueBase], + *, + is_runtime: bool, + ) -> torch.Tensor: + inner_tensors: dict[str, torch.Tensor | OpaqueBase] = {} + + curr_start_idx = self.flat_tensor_start_idx + for attr, creation_meta in self.attrs.items(): + if isinstance(creation_meta, OpaqueMeta): + opaque = all_args[curr_start_idx] + if not isinstance(opaque, OpaqueBase): + raise AssertionError(f"OpaqueBase expected, got {type(opaque)}") + inner_tensors[attr] = opaque + curr_start_idx += 1 + continue + if isinstance(creation_meta, PlainTensorMeta): + subclass = all_args[curr_start_idx] + if not isinstance(subclass, Tensor): + raise AssertionError("Tensor expected") + curr_start_idx += 1 + else: + subclass = creation_meta.creation_fn( + all_args, + is_runtime=is_runtime, + ) + curr_start_idx += creation_meta.arg_count + inner_tensors[attr] = subclass + + if is_runtime: + if self.original_subclass_type is None: + raise AssertionError( + "original_subclass_type must not be None at runtime" + ) + original_subclass_type = self.original_subclass_type + else: + original_subclass_type = type(self.original_subclass) + + if is_runtime: + outer_size, outer_stride = self.compute_outer_size_and_stride( + all_args, + curr_start_idx=curr_start_idx, + ) + else: + outer_size, outer_stride = self.outer_size, self.outer_stride + + rebuilt = original_subclass_type.__tensor_unflatten__( # type: ignore[attr-defined] + inner_tensors, self.meta, outer_size, outer_stride + ) + + if not is_runtime: + # After wrapping up the inner dense tensors into a subclass, we need to make sure that our new wrapper + # has correct autograd metadata, since we'll be tracing through the autograd engine with the subclass. + # We don't trace through the autograd engine at runtime though, so no need + # to compute this extra metadata then! + torch._mirror_autograd_meta_to(self.original_subclass, rebuilt) # type: ignore[attr-defined] + + return rebuilt + + def make_runtime_safe(self) -> None: + def _make_size_runtime_safe(x: None | int | torch.SymInt) -> int | None: + dummy = -1 + if isinstance(x, torch.SymInt): + # Replace nested ints by a dummy value (-1) as NJT ignores + # the outer_size/outer_stride at runtime. + return dummy if x.node.is_nested_int() else None + return x + + if self.original_subclass is None: + raise AssertionError( + "original_subclass must not be None before calling make_runtime_safe" + ) + self.original_subclass_type = type(self.original_subclass) + self.original_subclass = None + + # Note: NJT outer_size in AOTDispatcher + # `_make_size_runtime_safe` replaces any nested int with a dummy value (-1) + # to prevent serializing a SymInt at runtime. Internally, nested tensor __tensor_unflatten__ + # is designed to safely ignore this dummy value. + # For more details, see: https://github.com/pytorch/pytorch/blob/5141ade8e30c64e873e14dcc8de233da45d15025/torch/nested/_internal/nested_tensor.py#L266-L299 # noqa: B950 + self.outer_size = tuple(map(_make_size_runtime_safe, self.outer_size)) + self.outer_stride = tuple(map(_make_size_runtime_safe, self.outer_stride)) + + # Recurse on nested subclass info + for creation_meta in self.attrs.values(): + if isinstance(creation_meta, SubclassCreationMeta): + creation_meta.make_runtime_safe() + + def __post_init__(self) -> None: + # sanity assert to make sure we don't leak memory + if not is_fake(self.original_subclass): + raise AssertionError( + f"original_subclass must be a fake tensor to avoid memory leaks, got {type(self.original_subclass)}" + ) + + +# This class encapsulates all aliasing + mutation info we need about the forward graph +# See a more detailed overview of the edge case handling at +# https://docs.google.com/document/d/19UoIh_SVrMy_b2Sx5ZaeOJttm6P0Qmyss2rdBuyfoic/edit +# NOTE: This class is saved in AOTAutogradCache, If you are adding elements, make sure +# they are covered by warm cache tests. +@dataclass(eq=False) +class ViewAndMutationMeta: + # length = # user inputs + # This gives us info about every input, and what sort of mutation happened to it (if any) + input_info: list[InputAliasInfo] + + # length = # user outputs + # This gives us info about every output (mostly around whether it aliases other tensors) + output_info: list[OutputAliasInfo] + + # length = the number of intermediate bases appended as outputs to the end of the forward graph. + # Note: this is not necessarily the same thing as: + # len([x for x in output_info if x.output_type == OutputType.alias_of_intermediate]) + # Because outputs might share a ._base, or an output's ._base might itself be + # another user output (in both cases, we won't redundantly append bases to the end of the graph) + num_intermediate_bases: int + + # For inference only: instructs us to keep data-only input mutations directly in the graph + keep_input_mutations: bool + + # length = (# inputs w data mutations) + (# user outputs that are non_aliasing tensors) + # + (# intermediate bases) + # These are the FakeTensor (or potential SymInt) outputs that we traced from our + # metadata pass of the user's forward function. + # Their only use today is to pass them as a best-guess for tangents when tracing the joint. + # Stashing them as part of our "metadata" makes it simpler if we want to run our analysis + # pass once, and reuse the output throughout AOTAutograd + traced_tangents: list[Any] + + # TODO doc + traced_tangents_descs: list[AOTInput] + + # Each of these is a list telling us about subclasses for the inputs/outputs/grad_outs + # They are used throughout AOTDispatch to tell us how to generate a list of subclass tensors, + # Given a (potentially larger) list of plain torch tensors. + + # Taking subclass_inp_meta as an example: + # subclass_inp_meta[i] = j (an int) tells us: + # "The i'th user input is not a subclass, and corresponds to inputs[j] of the plain-tensor graph." + # subclass_inp_meta[i] = SubclassCreationMeta(flat_tensor_start_idx=3, arg_count=2) + # "The i'th user input is subclass holding two inner tensors, which are + # inputs[3] and inputs[4] of the plain-tensor graph". + + # length = # user inputs + subclass_inp_meta: list[PlainTensorMeta | SubclassCreationMeta] + # So, the full set of outputs to the forward graph looks something like: + # (*mutated_inps, *user_outs, *intermediate_bases, *saved_for_bw_tensors) + # where the first 3 of those 4 can be subclasses + # (but not saved_for_bw tensors, since these are internal to the compiler + # and not user visible, so there's no point in wrapping/unwrapping them at runtime). + # This list contains subclass information on all of the fw graph outputs + # except for saved_for_bw_tensors. + subclass_fw_graph_out_meta: list[PlainTensorMeta | SubclassCreationMeta] + # length = # backward graph inputs + subclass_tangent_meta: list[PlainTensorMeta | SubclassCreationMeta] + + # length = (# inputs w data mutations) + (# user outputs that are non_aliasing tensors) + # + (# intermediate bases) + # At runtime, we don't keep the traced_tangents around since they're not serializable. + # Instead, we keep any necessary subclass metadata necessary about each traced_tangent. + # This list is generated after calling make_runtime_safe(). + traced_tangent_metas: list[Any] | None = None + + num_symints_saved_for_bw: int | None = None + + # See Note [Activations with no version counter checks in eager] + # Number of tensors saved for backward that were stashed on ctx (e.g., ctx.x = x) + # rather than via save_for_backward in an autograd.Function. + # These tensors are placed at the end of the saved tensors and should skip + # version counter checks at runtime. + num_tensors_saved_with_no_vc_check: int | None = None + + # Number of opaque objects saved for backward + num_opaque_objects_saved_for_bw: int | None = None + # The grad_enabled mutation that will be emitted in the runtime_wrapper epilogue + # NOTE: AOTAutograd will assume that the ambient `is_grad_enabled` is the grad mode + # that is intended to be in effect prior to running the graph, in keeping with + # equivalence to eager mode. It is the responsibility of upstream graph acquisition + # to reset the grad mode to its pre-graph value prior to calling aot_autograd. + grad_enabled_mutation: bool | None = None + + # Keeps track of whether `torch.use_deterministic_algorithms` was turned on + # when the forward was run. If deterministic mode was turned off during the + # forward, but is turned on during the backward call, then an error is + # raised + deterministic: bool | None = None + + # Keeps track of which input indices store parameters (which we will treat as static) + static_input_indices: list[int] = field(default_factory=list) + + # Input indices that held AsyncCollectiveTensors at compile time. + # Used to emit direct trigger_wait() calls at runtime instead of + # scanning every arg on every graph invocation. + act_input_indices: list[int] = field(default_factory=list) + + # Map of effect type (ex. _EffectType.ORDERED) to token. If there are + # side-effectful operators, FunctionalTensorMode will populate this + # dictionary telling us how many tokens we will need during tracing. + tokens: dict[Any, torch.Tensor] = field(default_factory=dict) + + # Only filled in if/when we trace the joint function + # If an input requires grad and is mutated in the backward, it is only safe to keep the mutation + # in the graph if gradients are disabled while the backward runs + # (grad mode is disabled by default when users run the backward, but can be turned on with create_graph=True) + # At runtime during the backward, we use this list of indices to error properly if we find out + # that it was not safe to include a backward mutation in the graph. + indices_of_inputs_that_requires_grad_with_mutations_in_bw: list[int] = field( + default_factory=list + ) + + # Indexes of saved tensors which are donated buffer. + # Donated buffer means the tensor is not alias of any forward user input, forward user output, + # and backward output. + bw_donated_idxs: list[int] | None = None + + # Number of tokens used in backward, appended at the end of backward outputs. + # Filled after tracing joint function. + num_backward_tokens: int = 0 + + # Number of rng states that will get thread into the forward and backward for + # cudagraph compatible run_and_save_rng + num_graphsafe_rng_states: int = 0 + + graphsafe_rng_state_index: int | None = None + + # Stream indices for mutated inputs in the epilogue + # Maps from index in mutated_inp_runtime_indices to the stream index that last touched + # the storage of the tensor that will be copied back into the original input + # None means use the current/default stream + # This is populated during graph compilation when stream assignments are made + mutated_inp_stream_indices: list[int | None] | None = None + + # compile ID string (e.g., "1/0") for error messages + compile_id_str: str | None = None + + # help users identify where to add .detach() in their code + tangent_source_stack_traces: list[str | None] | None = None + + def __post_init__(self) -> None: + # pre-compute the indices of the inputs that are mutated. + # When keep_input_mutations is set, we don't need to worry about our epilogue + # handling data-only mutations, because we keep them directly in the graph. + mutated_inp_runtime_indices = [ + i + for i, m in enumerate(self.input_info) + if (m.mutation_type == MutationType.MUTATED_OUT_GRAPH) + ] + + mutated_graph_handled_indices = [ + i + for i, m in enumerate(self.input_info) + if m.mutation_type == MutationType.MUTATED_IN_GRAPH + ] + self.mutated_graph_handled_indices = mutated_graph_handled_indices + self.num_mutated_graph_handled_indices = len(self.mutated_graph_handled_indices) + + mutated_graph_handled_indices_seen_by_autograd = [ + i + for i in mutated_graph_handled_indices + if not self.input_info[i].mutations_hidden_from_autograd + ] + + self.mutated_graph_handled_indices_seen_by_autograd = ( + mutated_graph_handled_indices_seen_by_autograd + ) + self.num_mutated_graph_handled_indices_seen_by_autograd = len( + self.mutated_graph_handled_indices_seen_by_autograd + ) + + aliased_out_indices = [ + i + for i, m in enumerate(self.output_info) + if m.output_type + not in [ + OutputType.non_alias, + OutputType.unsafe_view_alias, + OutputType.custom_function_view, + ] + ] + unsafe_view_out_indices = [ + i + for i, m in enumerate(self.output_info) + if m.output_type is OutputType.unsafe_view_alias + ] + + # This is pre-computed in post_init for perf. + # It contains the index of every element + # of input_info that corresponds to a mutation (data or metadata or both) + self.mutated_inp_runtime_indices = mutated_inp_runtime_indices + self.num_mutated_inp_runtime_indices = len(self.mutated_inp_runtime_indices) + + # This is pre-computed for perf. + # It contains the index of every element + # of output_info that corresponds to an alias (either of an input or intermediate) + self.aliased_out_indices = aliased_out_indices + self.unsafe_view_out_indices = unsafe_view_out_indices + self.num_outputs = len(self.output_info) + self.num_outputs_non_aliased = len( + [ + x + for x in self.output_info + if x.output_type + in [ + OutputType.non_alias, + OutputType.unsafe_view_alias, + OutputType.custom_function_view, + ] + ] + ) + self.num_outputs_aliased_to_inputs = len( + [ + x + for x in self.output_info + if x.output_type + in [ + OutputType.alias_of_input, + OutputType.is_input, + ] + ] + ) + self.num_unsafe_view_outputs = len(self.unsafe_view_out_indices) + self.num_outputs_aliased_to_intermediates = len( + [ + x + for x in self.output_info + if x.output_type + in [ + OutputType.alias_of_intermediate, + OutputType.alias_of_intermediate_save_as_output, + OutputType.alias_of_intermediate_base_is_user_output, + ] + ] + ) + self.num_outputs_aliased = ( + self.num_outputs_aliased_to_inputs + + self.num_outputs_aliased_to_intermediates + ) + + # Record dynamic outputs of the Dynamo traced forward graph + # Mark them as dynamic at the end of the runtime wrapper + self.dynamic_outputs = any(o.dynamic_dims for o in self.output_info) + + # Record the indices of dynamic outputs in the partitioned forward graph + # Mark them as dynamic in the runtime wrapper + # activation index -> dynamic dims indices + self.dynamic_saved_tensors_idxs: dict[int, set[int]] = {} + + # See Note: [AOTAutograd Backward Guards] + # This is pre-computed for fast asserts on the types of our grad_outputs in the backward. + # Eventually, we should kill this and replace with real backward guards. + # (we want to precompute the "runtime" types, so replace FakeTensor with torch.Tensor) + self.output_types = [ + torch.Tensor if isinstance(x, FakeTensor) else type(x) + for x in self.traced_tangents + ] + + self.is_rng_op_functionalized = config.functionalize_rng_ops + # All of the above metadata is collected by tracing the fw function. + # However, extra outputs for rng offsets behave differently. Both fwd + # and bwd graphs have their own outputs for the total consumed offsets. + # Unlike mutated inputs, we don't have to worry about sending the right + # set of tensors between fwd and bwd. Fwd and bwd offsets are + # independent and simpler to handle. Therefore, we track them + # separately. + self.num_outputs_rng_offset = 1 if self.is_rng_op_functionalized else 0 + + # Our forward() returns both (tokens, mutated_inputs, outputs, output_intermediate_bases, saved_tensors, saved_symints) + # Tokens will be split out before mutations/view handling and we do not count them here. + self.num_forward_returns = ( + self.num_mutated_inp_runtime_indices + + self.num_outputs + + self.num_intermediate_bases + ) + # In case of functionalization of rng ops, the fw_module returns one + # additional output for rng offset. This rng offset is used right + # away to advance the rng state, and is not passed on to the raw + # outputs. However, we need to know the exact boundary to identify + # which tensors to be saved for the bwd graph. num_forward captures + # this information. + self.num_forward = self.num_forward_returns + self.num_outputs_rng_offset + + def make_runtime_safe(self) -> None: + """ + There are various fields in ViewAndMutationMeta that aren't serializable. This function is called after all tracing + is completed to simplify certain fields in the metadata so that they can be safely cached. + + Doing so may lose information (in the case of traced_tangents), but none of the information is needed at runtime. + """ + # TODO: This function is only a best effort: there are other fields that may not be cache safe + # (i.e., there's no guarantee that tensor_flatten() returns a serializable result), or that + # SubclassCreationMeta is cache safe. + if self.traced_tangent_metas is not None: + raise AssertionError( + "traced_tangent_metas should be None before calling make_runtime_safe" + ) + + def extract_metadata(t: object) -> tuple[Sequence[str], object] | None: + if isinstance(t, torch.Tensor) and is_traceable_wrapper_subclass(t): + (inner_tensors, flatten_spec) = t.__tensor_flatten__() # type: ignore[attr-defined] + # Technically, we only need the flatten_spec, not the inner tensors. + # However, some Tensor subclasses (like TwoTensor) may have flatten_spec = None. + # And we want to be able to assert that this metadata is non-None, + # to distinguish between "this was a tensor subclass with no metadata" vs. + # "this wasn't a tensor subclass at all". + return (inner_tensors, flatten_spec) + else: + return None + + self.traced_tangent_metas = [extract_metadata(t) for t in self.traced_tangents] + # Clear traced tangents at runtime + self.traced_tangents = [] + for inp_meta in self.subclass_inp_meta: + if isinstance(inp_meta, SubclassCreationMeta): + inp_meta.make_runtime_safe() + for inp_meta in self.subclass_fw_graph_out_meta: + if isinstance(inp_meta, SubclassCreationMeta): + inp_meta.make_runtime_safe() + for inp_meta in self.subclass_tangent_meta: + if isinstance(inp_meta, SubclassCreationMeta): + inp_meta.make_runtime_safe() + + # Clear view_meta_sequence when it has symbolic inputs, since it won't + # be used at runtime anyway (gen_alias_from_base skips view replay for + # symbolic inputs) and the SymInt references make it unpicklable. + for i, out_info in enumerate(self.output_info): + if out_info.view_meta_sequence is not None and any( + vm.has_symbolic_inputs for vm in out_info.view_meta_sequence.sequence + ): + self.output_info[i] = replace(out_info, view_meta_sequence=None) + + @property + def tensors_saved_for_backwards_slice(self) -> slice: + if self.num_symints_saved_for_bw is None: + raise AssertionError("num_symints_saved_for_bw must not be None") + if self.num_tensors_saved_with_no_vc_check is None: + raise AssertionError("num_tensors_saved_with_no_vc_check must not be None") + # Fast-path: if no tensors without VC check, just return the VC check slice + if self.num_tensors_saved_with_no_vc_check == 0: + return self.tensors_saved_for_backwards_with_vc_check_slice + # Invariant: total tensors activations = (acts_with_vc_check, acts_no_vc_check) + vc_slice = self.tensors_saved_for_backwards_with_vc_check_slice + no_vc_slice = self.tensors_saved_for_backwards_no_vc_check_slice + # Start should be the same (self.num_forward) + if vc_slice.start != self.num_forward: + raise AssertionError( + f"vc_slice.start ({vc_slice.start}) != self.num_forward ({self.num_forward})" + ) + # End is the end of the no_vc_check slice + return slice(vc_slice.start, no_vc_slice.stop) + + @property + def tensors_saved_for_backwards_with_vc_check_slice(self) -> slice: + """ + Slice of forward outputs that are tensors saved for backward that + require version counter checks (i.e., were saved via save_for_backward). + """ + # See Note [Activations with no version counter checks in eager] + if self.num_symints_saved_for_bw is None: + raise AssertionError("num_symints_saved_for_bw must not be None") + if self.num_tensors_saved_with_no_vc_check is None: + raise AssertionError("num_tensors_saved_with_no_vc_check must not be None") + # The tensors with VC check come first, followed by tensors without VC check + num_no_vc_check = self.num_tensors_saved_with_no_vc_check + num_opaque = self.num_opaque_objects_saved_for_bw or 0 + num_symints = self.num_symints_saved_for_bw + num_trailing = num_no_vc_check + num_opaque + num_symints + if num_trailing > 0: + return slice(self.num_forward, -num_trailing if num_trailing != 0 else None) + else: + return slice(self.num_forward, None) + + @property + def tensors_saved_for_backwards_no_vc_check_slice(self) -> slice: + """ + Slice of forward outputs that are tensors saved for backward that + do NOT require version counter checks (i.e., were stashed on ctx + rather than via save_for_backward in an autograd.Function). + """ + if self.num_symints_saved_for_bw is None: + raise AssertionError("num_symints_saved_for_bw must not be None") + if self.num_tensors_saved_with_no_vc_check is None: + raise AssertionError("num_tensors_saved_with_no_vc_check must not be None") + num_no_vc_check = self.num_tensors_saved_with_no_vc_check + num_opaque = self.num_opaque_objects_saved_for_bw or 0 + num_symints = self.num_symints_saved_for_bw + if num_no_vc_check == 0: + return slice(0, 0) # empty slice + num_trailing = num_opaque + num_symints + if num_trailing > 0: + return slice(-num_no_vc_check - num_trailing, -num_trailing) + else: + return slice(-num_no_vc_check, None) + + @property + def opaque_objects_saved_for_backwards_slice(self) -> slice: + num_opaque = self.num_opaque_objects_saved_for_bw or 0 + num_symints = self.num_symints_saved_for_bw or 0 + if num_opaque > 0: + if num_symints > 0: + return slice(-num_opaque - num_symints, -num_symints) + else: + return slice(-num_opaque, None) + else: + return slice(0, 0) # empty slice + + @property + def symints_saved_for_backwards_slice(self) -> slice: + if self.num_symints_saved_for_bw is None: + raise AssertionError("num_symints_saved_for_bw must not be None") + if self.num_symints_saved_for_bw > 0: + return slice(-self.num_symints_saved_for_bw, None) + else: + return slice(0, 0) # empty slice + + def __eq__(self, other: object) -> bool: + if not isinstance(other, ViewAndMutationMeta): + return NotImplemented + return ( + self.input_info == other.input_info + and self.output_info == other.output_info + and self.num_intermediate_bases == other.num_intermediate_bases + and self.keep_input_mutations == other.keep_input_mutations + and self.is_rng_op_functionalized == other.is_rng_op_functionalized + and self.num_outputs_rng_offset == other.num_outputs_rng_offset + and len(self.traced_tangents) == len(other.traced_tangents) + and all( + x.shape == y.shape and x.dtype == y.dtype + for x, y in zip(self.traced_tangents, other.traced_tangents) + ) + and self.num_backward_tokens == other.num_backward_tokens + ) + + +@dataclass(eq=False) +class SubclassMeta: + # A copy of all forward metadata, but computed on the *dense* tensor forward (after desugaring subclasses) + # So for example, if the user had a model containing two `TwoTensor` inputs, + # Then `SubclassMeta.fw_metadata.input_infos` would have length 4 here. + fw_metadata: ViewAndMutationMeta + + # Note: [Computing Subclass Metadata about grad_inputs] + # Given a list of flattened, plain tensor grad_inputs, this tells us how to reconstruct the grad_input subclasses + # + # You might think: why not just assume that all grad_inputs will have the same subclass-ness as the original inputs? + # (AOTAutograd generally assumes other properties, e.g. that grad_outputs are contiguous) + # + # This doesn't really work though. take this example: + # + # def f(DoubleTensor, DenseTensor): + # return DoubleTensor * DenseTensor + # + # In the above example, the .grad field of *both* DoubleTensor and DenseTensor will be a DoubleTensor. + # When we trace out a joint fw-bw graph, we'll end up returning two subclasses for the two grad_inputs. + # This means that our backward graph will return 4 outputs (two dense tensors for each DoubleTensor grad_input) + # and we need to properly store the metadata that tells us how to turn these 4 outputs back into DoubleTensors. + # + # Note that this info **cannot** easily be figured out from ViewAndMutationMeta. + # We can only compute this info by tracing the entire joint and examining the grad_inputs that we computed. + # + # See Note: [AOTAutograd Backward Guards] + # This will also eventually require us to install backward guards, + # in case we made incorrect assumptions about the subclass-ness of our grad_outputs + # + # Optional field because we don't compute for inference graphs + grad_input_metas: list[PlainTensorMeta | SubclassCreationMeta] | None = None + + def __init__(self) -> None: + # The fields in this class get set after its construction. + pass + + +# This class exists because: +# - the autograd.Function.forward() in aot autograd returns outputs that might alias inputs +# - we only care about the metadata on those aliases, so we can regenerate them. +# We do not want them to participate in the autograd.Function. +# We do that by wrapping them in an opaque class, so the autograd.Function +# does not know to treat them as tensors. +@dataclass(frozen=True) +class TensorAlias: + alias: torch.Tensor + + +@dataclass +class BackwardSignature: + """ + Provides information about the backward section of an exported + joint forward-backward graph. + For a particular fx GraphModule, this class contains information on: + (1) A mapping from each gradient (backwards output) to the parameter + it corresponds to (forward input) + (2) A mapping from each gradient (backwards output) to the user input + it corresponds to (forward input) + (3) Which of the forward outputs corresponds to the loss, that we backprop on. + + Each string name is the `node.name` of the corresponding node in the fx graph. + """ + + gradients_to_parameters: dict[str, str] + gradients_to_user_inputs: dict[str, str] + loss_output: str + + +GraphOutputName = NewType("GraphOutputName", str) +GraphInputName = NewType("GraphInputName", str) +FQN = NewType("FQN", str) + + +@dataclass +class GraphSignature: + """ + Provides information about an exported module. + For a particular fx GraphModule, this class contains information on: + (1) Which graph inputs are parameters, buffers, or user inputs + (2) (for params/buffers) a mapping from the name of each graph argument + to its parameter/buffer FQN in the original nn.Module. + (3) If there are input mutations, these are represented as extra outputs + in the fx GraphModule. We provide a mapping from these + extra output names to the names of the actual inputs. + (4) The pytree metadata on how to flatten/unflatten inputs and outputs. + The corresponding FX GraphModule only accepts and returns + pytree-flattened inputs/outputs. + (5) (Optionally) if the FX is a joint forward-backward graph, we provide + a signature on the backward section of the joint graph. + """ + + parameters: list[FQN] + buffers: list[FQN] + + user_inputs: list[GraphInputName] + user_outputs: list[GraphOutputName] + inputs_to_parameters: dict[GraphInputName, FQN] + inputs_to_buffers: dict[GraphInputName, FQN] + + # If the user's module mutates a buffer, + # it's represented in the graph as an extra graph output. + # This dict is a mapping from + # "graph outputs that correspond to updated buffers" + # to the FQN names of those mutated buffers. + buffers_to_mutate: dict[GraphOutputName, FQN] + parameters_to_mutate: dict[GraphOutputName, FQN] + user_inputs_to_mutate: dict[GraphOutputName, GraphInputName] + + in_spec: pytree.TreeSpec + out_spec: pytree.TreeSpec + + backward_signature: BackwardSignature | None + + input_tokens: list[GraphInputName] + output_tokens: list[GraphOutputName] + + @classmethod + def from_tracing_metadata( + cls, + *, + in_spec: pytree.TreeSpec, + out_spec: pytree.TreeSpec, + graph_input_names: list[str], + graph_output_names: list[str], + view_mutation_metadata: ViewAndMutationMeta, + named_parameters: list[str], + named_buffers: list[str], + num_user_inputs: int, + num_user_outputs: int, + trace_joint: bool, + loss_index: int | None, + backward_signature: BackwardSignature | None, + ) -> GraphSignature: + graph_inputs = graph_input_names + graph_outputs = graph_output_names + parameters = list(named_parameters) + buffers = list(named_buffers) + num_tokens = len(view_mutation_metadata.tokens) + + # Calling convention assumptions: + # (1) graph inputs = (input_tokens, params, buffers, user_inputs) + # (2) graph outputs = (output_tokens, mutated_inputs, user_outs, param_gradients) + # (If we are capturing an inference graph, this convention is identical + # except that param_gradients is empty) + # See Note [Side-Effectful Tokens in AOTAutograd] for information on tokens + + # Address input calling conventions: + start, stop = 0, num_tokens + input_tokens = graph_inputs[start:stop] + + start, stop = stop, stop + len(parameters) + inputs_to_parameters = dict(zip(graph_inputs[start:stop], parameters)) + + start, stop = stop, stop + len(buffers) + inputs_to_buffers = dict( + zip( + graph_inputs[start:stop], + buffers, + ) + ) + + start, stop = stop, stop + num_user_inputs + user_inputs = graph_inputs[start:stop] + + # We should've gone through all the inputs now + if len(graph_inputs) - stop != 0: + raise AssertionError( + f"expected all graph_inputs consumed, but {len(graph_inputs) - stop} remain" + ) + + # Address output calling conventions: + start, stop = 0, num_tokens + output_tokens = graph_outputs[start:stop] + + names = [*input_tokens, *parameters, *buffers, *user_inputs] + mutations: list[str] = [] + for idx, input_info in enumerate(view_mutation_metadata.input_info): + if input_info.mutates_data: + if trace_joint: + # Only buffers can be mutated, not parameters + if idx < len(parameters): + raise AssertionError( + f"expected idx ({idx}) >= len(parameters) ({len(parameters)}) when tracing joint" + ) + mutations.append(names[idx + num_tokens]) + + if len(mutations) != view_mutation_metadata.num_mutated_inp_runtime_indices: + raise AssertionError( + f"len(mutations) ({len(mutations)}) != " + f"num_mutated_inp_runtime_indices ({view_mutation_metadata.num_mutated_inp_runtime_indices})" + ) + + start, stop = ( + stop, + stop + view_mutation_metadata.num_mutated_inp_runtime_indices, + ) + outputs_to_mutations = dict(zip(graph_outputs[start:stop], mutations)) + + user_inputs_to_mutate: dict[GraphOutputName, GraphInputName] = {} + buffers_to_mutate: dict[GraphOutputName, FQN] = {} + parameters_to_mutate: dict[GraphOutputName, FQN] = {} + for output_name, mutation_name in outputs_to_mutations.items(): + if mutation_name in user_inputs: + # pyrefly: ignore [unsupported-operation] + user_inputs_to_mutate[output_name] = mutation_name + else: + if mutation_name not in buffers and mutation_name not in parameters: + raise AssertionError( + f"mutation_name '{mutation_name}' not found in buffers or parameters" + ) + if mutation_name in buffers: + # pyrefly: ignore [unsupported-operation] + buffers_to_mutate[output_name] = mutation_name + else: + # pyrefly: ignore [unsupported-operation] + parameters_to_mutate[output_name] = mutation_name + + start, stop = stop, stop + num_user_outputs + user_outputs = graph_outputs[start:stop] + + unused_outputs = len(graph_outputs) - stop + if backward_signature is not None: + unused_outputs -= len(backward_signature.gradients_to_parameters) + len( + backward_signature.gradients_to_user_inputs + ) + if unused_outputs != 0: + raise AssertionError(f"expected unused_outputs == 0, got {unused_outputs}") + + return GraphSignature( + parameters=parameters, # type: ignore[arg-type] + buffers=buffers, # type: ignore[arg-type] + user_inputs=user_inputs, # type: ignore[arg-type] + user_outputs=user_outputs, # type: ignore[arg-type] + inputs_to_buffers=inputs_to_buffers, # type: ignore[arg-type] + inputs_to_parameters=inputs_to_parameters, # type: ignore[arg-type] + user_inputs_to_mutate=user_inputs_to_mutate, + buffers_to_mutate=buffers_to_mutate, # type: ignore[arg-type] + parameters_to_mutate=parameters_to_mutate, # type: ignore[arg-type] + in_spec=in_spec, + out_spec=out_spec, + backward_signature=backward_signature, + input_tokens=input_tokens, # type: ignore[arg-type] + output_tokens=output_tokens, # type: ignore[arg-type] + ) + + +@dataclass +class AOTAutogradCacheInfo: + cache_key: str + start_time_ns: int + forward_symints: list[torch.SymInt] + + +@dataclass +class AOTConfig: + """ + Configuration for AOTDispatcher + """ + + fw_compiler: Callable[..., Any] | None + bw_compiler: Callable[..., Any] | None + partition_fn: Callable[..., Any] | None + decompositions: dict[OpOverload, Callable[..., Any]] | None + num_params_buffers: int + aot_id: int + keep_inference_input_mutations: bool + is_export: bool = False + no_tangents: bool = False + dynamic_shapes: bool = False + aot_autograd_arg_pos_to_source: list[Source] | None = None + static_input_indices: list[int] | None = None + inference_compiler: Callable[..., Any] | None = None + enable_log: bool = True + # this is always false outside of export. + pre_dispatch: bool = False + # Key to use for AOTAutogradCache + cache_info: AOTAutogradCacheInfo | None = None + # If we should ignore the shape_env in the ambient tracing_context. + # The net effect is that if dynamic shapes are on, we end up + # specializing on example_inputs. + # Used only by standalone_compile. + ignore_shape_env: bool = False + precompile_backend_id: str | None = None + force_non_lazy_backward_lowering: bool = False + # This config makes sure to check certain things like + # mutating input with req_grad in export joint tracing. + export_trace_joint: bool = False + disable_functionalization: bool = False + # If True, disable TorchFunctionMetadataMode during make_fx tracing. + # This mode is used to track torch_fn metadata but can interfere with + # certain tracing scenarios. + _disable_torch_fn_metadata_mode: bool = False + + def __post_init__(self) -> None: + if self.pre_dispatch: + if not self.is_export: + raise AssertionError("Can only have pre_dispatch IR for export.") + + +# TODO: types here +# plain_tensor_trace_fn, when it is joint, has tuple structure on the trace +# info too! +# TODO: this needs to be generic, parameterized on AOTDescriptor +SubclassTracingInfo = collections.namedtuple( + "SubclassTracingInfo", + [ + "plain_tensor_trace_fn", + "plain_tensor_args", + "plain_tensor_args_descs", + "maybe_subclass_meta", + ], +) + + +@dataclass +class AOTState: + """ + When we run AOTAutograd, this class encapsulates the state in the compiler which + must be preserved across stages. This is state in the traditional sense (not an + environment) because some values in this structure change as we progress through + pipelines in AOTAutograd. + """ + + # Whether or not we need to handle autograd when doing graph capture and + # compilation. Although the calling convention for non-autograd graph + # capture in AOTAutograd is simple and can be relied upon, the autograph + # capture calling convention is quite complicated and in general you are + # only expected to pass to aot_stage2_compile to process. + needs_autograd: bool + + # The FAKE flat arguments which we will do tracing with. Although you + # might naively expect this to be immutable, it's not: when we perform + # tracing, we may execute code that modifies the metadata of inputs, + # causing the args to become "invalid". It's also nontrivial to have a + # "golden" set of fake values and deepcopy them just in time when you + # might destructively mutate them (Voz and I tried very hard to do this). + # So we just periodically renew this field. Don't worry too much about + # this unless you're specifically trying to track down an input metadata + # mutation bug. + # + # (By the way, this is NEVER the joint inputs! Those only ever go in + # AOTGraphCapture) + flat_args: list[FxValue] + + # The descriptor for each argument in flat_args. + flat_args_descs: list[AOTInput] + + # This contains view and mutation information about the function, which we + # detected by doing an initial trace when we created this state. + fw_metadata: ViewAndMutationMeta + + # Top-level configuration + # This is morally immutable but sometimes we are naughty and mutate it. + aot_config: AOTConfig + + # When performing AOTAutograd traces and other passes, we typically + # require a lot of active context managers; most typically these either + # (1) ensure we are faithfully replicating the original PyTorch context + # managers or (2) toggle some behaviors in PyTorch to make it more + # suitable for tracing. When you use AOTState, you're expected to have + # created an ExitStack, entered it; then while we are running AOTAutograd + # we will add things onto the stack as necessary. When you're all done + # with processing AOTAutograd, you can exit this stack. All functions + # that take AOTState expect the ExitStack to not have been exited yet. + # + # TODO: We potentially could offer a resumable context manager, where you + # can cancel it and reenable it later when you need it. + stack: contextlib.ExitStack + + # The fake tensor mode used during tracing. This is useful for later + # operations that need to create new fake tensors consistent with the + # original trace. + fake_mode: FakeTensorMode + + +FxValue = Tensor | int | SymInt | BackwardState | OpaqueBase + + +class CompilerWrapper: + """ + AOTAutograd needs to do many transformations to the calling convention of the user function + it is tracing, e.g., deduplicating inputs, unpacking subclasses, etc. CompilerWrapper lets + us factor these into compositional stages so we can handle each transformation incrementally + instead of having to do it all at once. + + Since there is a calling convention change, there are two parts to the wrpaper: + + 1. The prologue, which is about compile-time behavior: given this original function, what + is the new function with modified calling convention that we should trace with AOTAutograd + to get the FX graph we will do joint passes, partitioning and ultimate Inductor compilation on? + We get (flat_fn, flat_args), the original function under trace and inputs we were + going to feed it, and produce a new function and new inputs to feed it. + + 2. The epilogue, which is about run-time behavior: we have now compiled the modified calling + convention function, we need to wrap it so that we have a new function that has the + original calling convention of the original function, so that our users can call it + at the old signature they expected. We get (compiled_fn, real arguments), the newly + compiled function we need to wrap. + + Note about caching: we do NOT directly serialize the runtime wrappers; instead, they + are reapplied to compiled_fn after we have finished deserializing the compiled_fn. + + Extra metadata that is needed to compute pre or post compile can be passed in via attributes. + """ + + def pre_compile( + self, + flat_fn: TraceFn, + flat_args: list[FxValue], + flat_args_descs: list[AOTInput], + aot_config: AOTConfig, + *, + fw_metadata: ViewAndMutationMeta, + ) -> tuple[TraceFn, list[FxValue], list[AOTInput], ViewAndMutationMeta]: + """ + Process the inputs to the compiler_fn. You can pass in extra metadata via kwargs. + Args: + flat_fn: The function to compile + flat_args: Metadata from example inputs of the function to compile + aot_config: AOTConfig passed in at compile time + fw_metadata: ViewAndMutationMeta generated from flat_fn and flat_args + """ + return flat_fn, flat_args, flat_args_descs, fw_metadata + + def post_compile( + self, + compiled_fn: Callable[_P, _R], + aot_config: AOTConfig, + *, + runtime_metadata: ViewAndMutationMeta, + ) -> Callable[_P, _R]: + """ + Given an output of the compiler, wrap it with information received from prologue. + Args: + compiled_fn: Callable after calling compiler_fn + aot_config: AOTConfig after calling prologue + runtime_metadata: ViewAndMutationMeta after calling all wrappers's pre_compile steps. + Example: + + def wrapped_compiled_fn(args): + # do something with args, aot_config, fw_metadata + return compiled_fn(args) + + return wrapped_compiled_fn + """ + return compiled_fn + + +class InductorWrapper: + """ + This is sort of like CompilerWrapper, but it happens at a different part of the lifecycle: + it talks about transformations we do to the traced and partitioned FX graph before we + send it to the Inductor compiler. + + Once again, there are two parts: + + 1. The prologue, which "modifies" the FX graph before we send it to + Inductor. I say "modifies" because... we don't really actually do + anything nontrivial in either of our two implementations. + 2. The epilogue, which modifies the compiled function produced by Inductor + + Although hypothetically these wrappers could be used compositionally in a centralized + wrappers list, in practice they seem to just be invoked manually when needed. + + NB: The flat_args input is sometimes mutated. This is probably naughty but whatever. + """ + + def pre_compile( + self, + fw_module: torch.fx.GraphModule, + flat_args: list[Tensor], + aot_config: AOTConfig, + *, + fw_metadata: ViewAndMutationMeta, + ) -> None: + """ + Process the inputs to the compiler_fn. You can pass in extra metadata via kwargs. + Args: + flat_fn: The function to compile + flat_args: Metadata from example inputs of the function to compile + aot_config: AOTConfig passed in at compile time + fw_metadata: ViewAndMutationMeta generated from flat_fn and flat_args + """ + return + + def post_compile( + self, + compiled_fn: Callable[_P, _R], + aot_config: AOTConfig, + *, + runtime_metadata: ViewAndMutationMeta, + ) -> Callable[_P, _R]: + """ + Given an output of the compiler, wrap it with information received from prologue. + Args: + compiled_fn: Callable after calling compiler_fn + aot_config: AOTConfig after calling prologue + runtime_metadata: ViewAndMutationMeta after calling all wrappers's pre_compile steps. + Example: + + def wrapped_compiled_fn(args): + # do something with args, aot_config, fw_metadata + return compiled_fn(args) + + return wrapped_compiled_fn + """ + return compiled_fn + + +@dataclass +class AOTGraphCapture: # Produced by aot_stage1_graph_capture + # AOTAutograd typically operates by taking complicated graphs and + # desugaring them into simpler graphs that use PyTorch features. These + # wrappers establish invariants so that when we actually do tracing we can + # assume these invariants hold, leading to a simpler tracing + # implementation. However, this means that we have to keep track of how + # to enter/exit these wrappers when passing inputs into the compiled + # graph, among other things! + wrappers: list[CompilerWrapper] + + # The actual captured graph module. In some circumstances (export) this + # graph has a specific calling convention that can be relied upon by + # external callers. In other situations, the calling convention is + # unspecified and only aot_stage2_compile knows how to deal with them. + graph_module: torch.fx.GraphModule + + # When compiling with autograd support, this is the joint_inputs, which is + # larger than the original flat_args as all tangents get inputs. The + # tuple organizes into primals and tangents. When not autograd it's just + # a plain list. + updated_flat_args: list[Any] | tuple[list[Any], list[Any]] + + updated_flat_args_descs: list[AOTInput] | tuple[list[AOTInput], list[AOTInput]] + + # Metadata about subclass inputs/outputs in the graph trace. + maybe_subclass_meta: Any + + +FakifiedFlatArgs = NewType("FakifiedFlatArgs", list[Any]) + + +TOutputCode = TypeVar("TOutputCode", bound="OutputCode") + + +class AOTDispatchCompiler(Protocol): + """ + Represents a fw or bw_compiler passed to AOTAutograd. + """ + + def __call__( + self, + gm: torch.fx.GraphModule, + example_inputs: Sequence[InputType], + ) -> Any: ... + + +# TODO: bikeshed on this name +class SerializableAOTDispatchCompiler(AOTDispatchCompiler): + """ + Represents an AOTDispatchCompiler that returns an OutputCode, and is + therefore cacheable. SerializableAOTDispatchCompiler always return an OutputCode. + A _CompileFxCallable usually gets converted into an AOTDispatchCompiler after binding all of + the kwargs in _CompileFxKwargs. + """ + + def __init__( + self, + output_code_ty: type[TOutputCode], + compiler_fn: Callable[[torch.fx.GraphModule, Sequence[InputType]], TOutputCode], + ) -> None: + # pyrefly: ignore [invalid-type-var] + self.output_code_ty = output_code_ty + # pyrefly: ignore [invalid-type-var] + self.compiler_fn = compiler_fn + + def __call__( + self, + gm: torch.fx.GraphModule, + example_inputs: Sequence[InputType], + ) -> OutputCode: + output_code = self.compiler_fn(gm, example_inputs) + return output_code + + +class FlatFn(Protocol): + def __call__(self, *args: FxValue) -> list[FxValue]: ... + + +class TraceFn(Protocol): + def __call__(self, *args: FxValue) -> tuple[list[FxValue], list[AOTOutput]]: ... + + +class PreppedForAutogradTraceFn(Protocol): + def __call__( + self, + *args: FxValue, + ) -> tuple[tuple[list[FxValue], list[bool]], list[AOTOutput]]: ... + + +class JointTraceFn(Protocol): + handle: JointFnHandle + + def __call__( + self, primals: list[FxValue], tangents: list[FxValue] + ) -> tuple[ + tuple[list[FxValue], list[Tensor | None]], + tuple[list[AOTOutput], list[AOTOutput | None]], + ]: ... + + +@dataclass +class JointWithDescriptors: + _aot_state: AOTState + _aot_graph_capture: AOTGraphCapture + + # The exact order parameters and buffers are expected to be passed into + # the final compiled function. Parameters before buffers. + params_spec: list[str] + buffers_spec: list[str] + + in_spec: pytree.TreeSpec + out_spec: pytree.TreeSpec + + @property + def graph_module(self) -> torch.fx.GraphModule: + return self._aot_graph_capture.graph_module + + @graph_module.setter + def graph_module(self, value: torch.fx.GraphModule) -> None: + self._aot_graph_capture.graph_module = value + + @property + def fake_mode(self) -> FakeTensorMode: + return self._aot_state.fake_mode + + def cache_hash(self) -> str: + """ + Return a hash string suitable for use as a cache key. This method + exists to decouple cache key generation from __str__/__repr__, so + that display-oriented changes don't accidentally affect caching. + """ + from hashlib import sha256 + + return sha256(str(self).encode("utf-8")).hexdigest() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/streams.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/streams.py new file mode 100644 index 0000000000000000000000000000000000000000..8e94bef9370def0228cf56236d8cf101f712ff4a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/streams.py @@ -0,0 +1,596 @@ +import operator +from typing import Any, TYPE_CHECKING, TypeAlias + +import torch.fx +import torch.fx.traceback +import torch.utils._pytree as pytree +from torch._dynamo.graph_utils import _get_flat_args +from torch._dynamo.variables.streams import get_current_stream, new_event +from torch.fx.node import map_arg +from torch.utils._runtime_estimation import ( + _FLOAT_TYPES, + _IGNORE_OPS, + get_compute_time, + get_transfer_time, +) + + +if TYPE_CHECKING: + from .schemas import ViewAndMutationMeta # noqa: TC004 + +from .indexed_dict import IndexedDict + + +aten = torch.ops.aten + +Node: TypeAlias = torch.fx.Node +Graph: TypeAlias = torch.fx.Graph + +_SYNC_OPS = ( + torch.ops.streams.record_event.default, + torch.ops.streams.wait_event.default, + torch.ops.streams.synchronize_event.default, + torch.ops.streams.synchronize_device.default, + torch.ops.streams.synchronize_stream.default, +) + + +def get_roofline_estimate(node: Node) -> float: + if node.op != "call_function": + raise AssertionError(f"non-func node in roofline estimate: {node.op}") + + def map_value(x: Any) -> Any: + return x.meta.get("value", x) if isinstance(x, Node) else x + + func = node.target + if func in _IGNORE_OPS: + return 0.0 + + mapped_args = torch.fx.map_arg(node.args, map_value) + mapped_kwargs = torch.fx.map_arg(node.kwargs, map_value) + flat_args_kwargs = [map_value(x) for x in _get_flat_args(node, {})] + flat_outs, _ = pytree.tree_flatten(node.meta.get("value", node)) + out = node.meta.get("value", node) + out_dtypes = { + t.dtype + for t in flat_outs + if isinstance(t, torch.Tensor) and t.dtype in _FLOAT_TYPES + } + + return ( + max( + get_transfer_time(flat_args_kwargs, flat_outs), + get_compute_time(func, mapped_args, mapped_kwargs, out, out_dtypes), + ) + / 1e6 + ) + + +def is_gradient_acc(node: Node) -> bool: + return node.meta.get("is_gradient_acc", False) + + +def is_bwd_node(node: Node) -> bool: + tag = node.meta.get("partitioner_tag") + return tag == "is_backward" or tag == "must_be_in_backward" + + +def get_device(node: Node) -> torch.device: + return node.meta["val"].device + + +def get_stream(node: Node) -> int | None: + maybe_annotation = node.meta.get("custom", None) + if maybe_annotation is not None: + return node.meta["custom"].get("stream", None) + else: + return None + + +def get_stream_or_current_stream(node: Node) -> int: + ind = get_stream(node) + if ind is None: + ind = get_current_stream(get_device(node)) + return ind + + +def set_stream(node: Node, ind: int) -> None: + if "custom" in node.meta: + node.meta["custom"].update({"stream": ind}) + else: + node.meta["custom"] = {"stream": ind} + + +def insert_record_event_after_node(graph: Graph, node: Node, event_ind: int) -> Node: + with graph.inserting_after(node): + node = graph.call_function( + torch.ops.streams.record_event.default, + ( + event_ind, + get_stream_or_current_stream(node), + ), + ) + node.meta["partitioner_tag"] = "must_be_in_backward" + + return node + + +def insert_wait_event_before_node(graph: Graph, node: Node, event_ind: int) -> Node: + with graph.inserting_before(node): + node = graph.call_function( + torch.ops.streams.wait_event.default, + ( + event_ind, + get_stream_or_current_stream(node), + ), + ) + node.meta["partitioner_tag"] = "must_be_in_backward" + + return node + + +def populate_stream_timeline( + stream_to_timeline: dict[int | None, IndexedDict[Node, float]], + graph: Graph, + stream_index: int | None, +) -> IndexedDict[Node, float]: + if stream_index not in stream_to_timeline: + stream_to_timeline[stream_index] = IndexedDict() + total_time = 0.0 + for node in graph.nodes: + # mlazos: not sure if we should include forward here too but don't think it matters + if ( + node.op == "call_function" + and is_bwd_node(node) + and get_stream(node) == stream_index + ): + total_time += get_roofline_estimate(node) + stream_to_timeline[stream_index][node] = ( + total_time # NB: total time includes the node's runtime + ) + + return stream_to_timeline[stream_index] + + +# NB: we start all estimates at 0, estimating the total runtime of each stream with timestamps at each node +# we then try and use these timestamps to estimate when to deallocate tensors used in side streams +# See https://docs.pytorch.org/docs/stable/generated/torch.Tensor.record_stream.html#torch.Tensor.record_stream +# for details on the problem being addressed. Rather than using the automatic memory management approach of record_stream +# we attempt to find the point which to deallocate based on the estimated timestamps. +def handle_synced_deallocation( + graph: Graph, + stream_to_exec_trace: dict[int | None, IndexedDict[Node, float]], + node: Node, + last_usage: Node, +) -> None: + if not is_bwd_node(node): + raise AssertionError( + "synced allocations should only be handled on backward nodes" + ) + if not is_bwd_node(last_usage): + raise AssertionError( + "synced allocations should only be handled on backward nodes" + ) + allocating_stream = get_stream(node) + side_stream = get_stream(last_usage) + if allocating_stream == side_stream: + raise AssertionError( + "allocating and side stream should be different for synced deallocations" + ) + if not torch.cuda.is_available(): + # fallback to record_stream in this case + with graph.inserting_after(node): + graph.call_function( + torch.ops.streams.record_stream.default, + ( + node, + get_stream_or_current_stream(last_usage), + ), + {}, + ) + node.meta["partitioner_tag"] = "must_be_in_backward" + + allocating_stream_trace = populate_stream_timeline( + stream_to_exec_trace, graph, allocating_stream + ) + side_stream_trace = populate_stream_timeline( + stream_to_exec_trace, graph, side_stream + ) + + alloc_ptr = node + target_side_stream_time = side_stream_trace[last_usage] + # linear search from first usage of tensor to a point in time after the side stream has finished + while alloc_ptr is not None: + alloc_time = allocating_stream_trace[alloc_ptr] + + if alloc_time >= target_side_stream_time: + break + elif alloc_time < target_side_stream_time: + next_ptr = allocating_stream_trace.next_key(alloc_ptr) + if next_ptr is not None: + alloc_ptr = next_ptr + else: + break + + wait_event = new_event() + record_node = insert_record_event_after_node(graph, last_usage, wait_event) + with graph.inserting_after(max(alloc_ptr, record_node)): + graph.call_function( + torch.ops.streams.sync_dealloc.default, + (wait_event, get_stream_or_current_stream(alloc_ptr), node), + {}, + ) + node.meta["partitioner_tag"] = "must_be_in_backward" + + +def insert_sync( + graph: Graph, + consumer: Node, + producer: Node, + node_to_wait_event_ind: dict[Node, int], +) -> None: + if producer not in node_to_wait_event_ind: + node_to_wait_event_ind[producer] = new_event() + + insert_record_event_after_node( + graph, producer, node_to_wait_event_ind[producer] + ) + insert_wait_event_before_node(graph, consumer, node_to_wait_event_ind[producer]) + + +def assign_backward_streams(gm: torch.fx.GraphModule) -> None: + """Assigns backward streams to gradient accumulation nodes""" + + # NB: iterate in reverse order to more closely match eager + # the user node stream will be populated first + for node in reversed(list(gm.graph.nodes)): + if is_gradient_acc(node): + # Accumulation stream selection. Follow the rules from top to bottom to determine the accumulation stream: + # 1. Match first stream assignment of the first user with a stream + # 2. Match first stream assignment encountered in the args from left to right + # This differs from eager in some cases: + # Specifically the eager code uses the autograd node to determine the stream, + # crucially this does not necessarily correspond to the FX graph node. For example, + # in the backward for an add node with a constant we will passthrough and during backward tracing, + # no op will be added to the FX graph, so our stream assignment will differ in this case. + gradients = _get_flat_args(node, {}) + users = list(node.users.keys()) + + # All gradients will be on same device, they will be coerced if they were not with a .to() node + for neighbor in users + gradients: + ind = get_stream(neighbor) + if ind is not None: + set_stream(node, ind) + break + + +def insert_backward_syncs(gm: torch.fx.GraphModule) -> None: + """Inserts stream syncs for backward nodes if consumer and producer are on different streams""" + node_to_wait_event_ind: dict[Node, int] = {} + for node in gm.graph.nodes: + if node.op == "call_function" and is_bwd_node(node): + flat_args = _get_flat_args(node, {}) + cur_node_stream = get_stream(node) + + for arg in flat_args: + if arg.op == "call_function" and is_bwd_node(arg): + arg_stream = get_stream(arg) + if arg_stream != cur_node_stream and get_device(arg).type != "cpu": + insert_sync(gm.graph, node, arg, node_to_wait_event_ind) + + +def sync_deallocations(gm: torch.fx.GraphModule) -> None: + """Handles https://docs.pytorch.org/docs/stable/generated/torch.Tensor.record_stream.html#torch.Tensor.record_stream""" + # Note: this is only needed if the last usage of a tensor is on a stream other than + # the stream the tensor was allocated on + + # an estimated timestamp from the beginning of graph execution (assuming 0 CPU overhead) + # I think this is fine because you should have large tensors if you're using streams + # although perhaps I could add a constant 10us per op ahead of the first stream op? + # a trace of all the nodes running in a given stream + stream_to_exec_trace: dict[int | None, IndexedDict[Node, float]] = {} + for node in gm.graph.nodes: + if node.op == "call_function" and is_bwd_node(node): + allocating_stream = get_stream(node) + users = list(node.users.keys()) + if not users: + continue + last_user = max(user for user in users) + if last_user.op == "output": + continue + side_stream = get_stream(last_user) + if allocating_stream != side_stream: + handle_synced_deallocation( + gm.graph, stream_to_exec_trace, node, last_user + ) + + +def assign_epilogue_copy_streams(gm: torch.fx.GraphModule) -> None: + for epi_copy in gm.graph.find_nodes(op="call_function", target=aten.copy_.default): + arg_stream = get_stream(epi_copy.args[1]) + copy_stream = get_stream(epi_copy) + if arg_stream != copy_stream: + set_stream(epi_copy, get_stream_or_current_stream(epi_copy.args[1])) + + +def populate_fw_metadata_with_stream_indices( + gm: torch.fx.GraphModule, fw_metadata: "ViewAndMutationMeta" +) -> None: + """ + Populates fw_metadata.mutated_inp_stream_indices with stream indices from the compiled graph. + + The forward graph outputs are structured as: + (*mutated_inputs, *user_outputs, *intermediate_bases, *saved_tensors, *saved_symints) + + We extract the stream index for each mutated input from the graph's output node. + """ + + num_mutated_inps = fw_metadata.num_mutated_inp_runtime_indices + if num_mutated_inps == 0: + fw_metadata.mutated_inp_stream_indices = [] + return + + # Find the output node in the graph + output_node = None + for node in gm.graph.find_nodes(op="output"): + output_node = node + break + + if output_node is None: + raise AssertionError( + "No output node found in the graph when extracting stream indices" + ) + + # The output node's args[0] is a tuple/list of all outputs + output_args = output_node.args[0] + + # Extract stream indices for the first num_mutated_inps outputs + stream_indices = [] + for i in range(num_mutated_inps): + if i < len(output_args): + output_arg = output_args[i] + # Get the stream index from the node metadata + stream_idx = ( + get_stream(output_arg) + if isinstance(output_arg, torch.fx.Node) + else None + ) + stream_indices.append(stream_idx) + else: + stream_indices.append(None) + + fw_metadata.mutated_inp_stream_indices = stream_indices + + +def _wrap_sync_node( + gm: torch.fx.GraphModule, + sync_node: Node, + deps_before_sync: list[Node], + visited: set[Node], +) -> tuple[Node, list[Node]]: + """ + Core logic: wrap a single sync node in control_deps. + + Returns (control_deps_node, passthrough_getitems) where passthrough_getitems + are the getitem nodes that thread dependencies through the control_deps node. + ``visited`` is the set of nodes at or before the sync node in graph order, + used to distinguish pre-sync vs post-sync users. + """ + from torch._inductor.fx_passes.control_dependencies import ( + _create_subgraph_for_node, + control_deps, + get_subgraph_name, + ) + + graph = gm.graph + + # Use dep.users to find deps with uses after the sync — avoids a forward walk. + deps_with_uses_after_sync = [ + dep + for dep in deps_before_sync + if any(user not in visited for user in dep.users) + ] + + # Create subgraph that executes sync and passes through only used dependencies + subgraph_module = _create_subgraph_for_node( + graph, sync_node, deps_with_uses_after_sync + ) + subgraph_attr_name = get_subgraph_name(gm, sync_node.name) + setattr(gm, subgraph_attr_name, subgraph_module) + + # Create control_deps call + # Note: sync nodes (record_event/wait_event) only take int args, no Node args. + with graph.inserting_before(sync_node): + get_subgraph = graph.get_attr(subgraph_attr_name) + control_deps_node = graph.call_function( + control_deps, + args=( + tuple(deps_before_sync), # additional_deps (all deps for ordering) + get_subgraph, # subgraph + *deps_with_uses_after_sync, # only pass through deps that are used + ), + kwargs={}, + ) + + # Mark newly created nodes as visited so subsequent syncs don't + # misclassify them as "after the sync" during replacement. + visited.add(get_subgraph) + visited.add(control_deps_node) + + # The output is (sync_result, *deps_with_uses_after_sync) + # Create getitem nodes only for dependencies that have uses after sync + replacements: dict[Node, Node] = {} + with graph.inserting_after(control_deps_node): + for i, dep in enumerate(deps_with_uses_after_sync): + getitem_node = graph.call_function( + operator.getitem, + args=(control_deps_node, i + 1), # +1 because index 0 is sync result + ) + getitem_node.meta.update(dep.meta) + replacements[dep] = getitem_node + visited.add(getitem_node) + + # Replace uses of dependencies that come after sync_node. + # Use map_arg to handle nested structures (e.g. output node's list args). + for dep, getitem_node in replacements.items(): + for user in list(dep.users.keys()): + if user is control_deps_node: + continue + if user in visited: + continue + # Don't replace forward outputs in the output node — they belong + # to the forward partition and must not reference backward nodes. + if user.op == "output" and not is_bwd_node(dep): + continue + + def _replace(n: Node) -> Node: + return getitem_node if n is dep else n + + user.args = map_arg(user.args, _replace) + user.kwargs = map_arg(user.kwargs, _replace) + + # Remove original sync node + sync_node.replace_all_uses_with(control_deps_node) + graph.erase_node(sync_node) + return control_deps_node, list(replacements.values()) + + +def wrap_all_sync_nodes_with_control_deps(gm: torch.fx.GraphModule) -> None: + """ + Single-pass wrap of all sync nodes in control_deps. + + Iterates through the graph once, accumulating per-stream node lists. + When a sync node is encountered, it is wrapped using the accumulated deps + for that stream, then the deps are reset to the control_deps node + (maintaining the ordering chain for subsequent syncs on the same stream). + """ + graph = gm.graph + if len(graph.nodes) == 0: + raise RuntimeError("Expected a non-empty graph") + stream_to_nodes: dict[int | None, list[Node]] = {} + # Maps event_index -> control_deps node that wrapped its record_event, + # so the corresponding wait_event/synchronize_event can depend on the record. + event_to_ctrl: dict[int, Node] = {} + # Maps event_index -> getitem nodes threaded through record_event's control_deps, + # so synchronize_event can thread them through to subsequent ops. + event_to_passthrough: dict[int, list[Node]] = {} + # Maps event_index -> stream that the event was recorded on, + # so synchronize_event can infer its stream. + event_to_stream: dict[int, int | None] = {} + visited: set[Node] = set() + found_sync = False + + # Walk the node linked-list manually so we can mutate the graph + # (wrapping sync nodes inserts/erases nodes) without losing our place. + node = next(iter(graph.nodes)) + while node.op != "root": + next_node = node.next + visited.add(node) + + if node.op == "call_function": + if node.target in _SYNC_OPS: + # synchronize_device and synchronize_stream block the CPU, + # so all subsequent kernel launches are host-ordered after + # them. Treat both as full barriers across all streams. + if node.target in ( + torch.ops.streams.synchronize_device.default, + torch.ops.streams.synchronize_stream.default, + ): + all_stream_deps: list[Node] = [ + n for nodes in stream_to_nodes.values() for n in nodes + ] + if all_stream_deps: + found_sync = True + _wrap_sync_node(gm, node, all_stream_deps, visited) + stream_to_nodes.clear() + node = next_node + continue + + event_index: int = node.args[0] # type: ignore[assignment] + + # synchronize_event blocks the CPU thread, so it acts + # as a barrier across all streams. Collect deps from every + # stream and reset them all afterward. If the event was + # recorded externally, thread the graph inputs through so + # that any post-sync uses depend on the synchronize. + if node.target is torch.ops.streams.synchronize_event.default: + sync_stream: int | None = event_to_stream.get(event_index) + all_stream_deps: list[Node] = [ + n for nodes in stream_to_nodes.values() for n in nodes + ] + if event_index not in event_to_stream: + placeholders = [n for n in graph.nodes if n.op == "placeholder"] + deps_before_sync = [*placeholders, *all_stream_deps] + else: + deps_before_sync = all_stream_deps + else: + sync_stream = node.args[1] # type: ignore[assignment] + deps_before_sync = list(stream_to_nodes.get(sync_stream, ())) + # Nodes without explicit stream annotation (custom.stream=None) + # run on the current/default stream. Include them when the sync + # op references a stream, since the unannotated nodes are + # implicitly on that stream. + if None in stream_to_nodes and sync_stream is not None: + deps_before_sync.extend(stream_to_nodes[None]) + + # For wait_event and synchronize_event, add a cross-event + # dependency on the matching record_event's control_deps node + # so they cannot be reordered before the record. + if ( + node.target + in ( + torch.ops.streams.wait_event.default, + torch.ops.streams.synchronize_event.default, + ) + and event_index in event_to_ctrl + ): + deps_before_sync = [ + event_to_ctrl[event_index], + *deps_before_sync, + ] + + # For synchronize_event, also include the getitem nodes + # threaded through record_event's control_deps. This ensures + # subsequent ops that depend on recorded values get rewired + # through synchronize_event. + if ( + node.target is torch.ops.streams.synchronize_event.default + and event_index in event_to_passthrough + ): + deps_before_sync = [ + *deps_before_sync, + *event_to_passthrough[event_index], + ] + + if deps_before_sync: + found_sync = True + ctrl_node, passthrough = _wrap_sync_node( + gm, node, deps_before_sync, visited + ) + else: + ctrl_node = None + passthrough: list[torch.fx.Node] = [] + + if node.target is torch.ops.streams.record_event.default: + event_to_stream[event_index] = sync_stream + if ctrl_node is not None: + event_to_ctrl[event_index] = ctrl_node + event_to_passthrough[event_index] = passthrough + + # Reset: ops between this sync and the next will accumulate + # fresh. Ordering with prior ops is already enforced because + # their uses were rewired through getitems from control_deps. + if node.target is torch.ops.streams.synchronize_event.default: + stream_to_nodes.clear() + else: + stream_to_nodes[sync_stream] = [] + if None in stream_to_nodes: + stream_to_nodes[None] = [] + elif "val" in node.meta: + stream = get_stream(node) + stream_to_nodes.setdefault(stream, []).append(node) + + node = next_node + + if found_sync: + gm.recompile() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/subclass_codegen.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/subclass_codegen.py new file mode 100644 index 0000000000000000000000000000000000000000..f6a47790569adf8f74c92d42c6171e513f25aaeb --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/subclass_codegen.py @@ -0,0 +1,397 @@ +""" +Codegen for AOTDispatchSubclassWrapper. + +Generates a Python function that replaces the data-driven +runtime_unwrap_tensor_subclasses / wrap_tensor_subclasses loop with +a straight-line function where all metadata (indices, attr names, +subclass types, symint positions) is baked in at compile time. +""" + +import functools +import keyword +import logging +from collections.abc import Callable, Iterable + +import torch +from torch import SymInt + +from .schemas import OpaqueMeta, PlainTensorMeta, SubclassCreationMeta + + +log = logging.getLogger(__name__) + + +def _is_symint_placeholder(x: None | int | SymInt) -> bool: + """Check whether a size/stride entry is symbolic and needs a runtime value. + + Works both before make_runtime_safe() (entries are SymInt) and after + (symbolic entries replaced with None, nested ints with -1). + """ + if x is None: + return True + if isinstance(x, SymInt) and not x.node.is_nested_int(): + return True + return False + + +def _compute_placeholders(outer: Iterable[None | int | SymInt]) -> list[bool]: + return [_is_symint_placeholder(s) for s in outer] + + +def _safe_attr_access(var: str, attr: str) -> str: + if attr.isidentifier() and not keyword.iskeyword(attr): + return f"{var}.{attr}" + return f"getattr({var}, {attr!r})" + + +class _CodegenState: + """Accumulates lines of generated source and global bindings.""" + + def __init__(self) -> None: + self.lines: list[str] = [] + self.globals: dict[str, object] = {} + self._name_counter: int = 0 + + def emit(self, line: str, indent: int = 1) -> None: + self.lines.append(" " * indent + line) + + def fresh_name(self, prefix: str) -> str: + name = f"{prefix}_{self._name_counter}" + self._name_counter += 1 + return name + + def add_global(self, name: str, value: object) -> str: + self.globals[name] = value + return name + + +def _codegen_unwrap_subclass( + state: _CodegenState, + meta: SubclassCreationMeta, + var: str, + indent: int = 1, + include_symints: bool = True, +) -> None: + """Emit code to recursively unwrap a single subclass input.""" + for attr, attr_meta in meta.attrs.items(): + match attr_meta: + case PlainTensorMeta() | OpaqueMeta(): + state.emit( + f"unwrapped_args.append({_safe_attr_access(var, attr)})", + indent=indent, + ) + case SubclassCreationMeta(): + inner_var = state.fresh_name("_inner") + state.emit( + f"{inner_var} = {_safe_attr_access(var, attr)}", indent=indent + ) + _codegen_unwrap_subclass( + state, + attr_meta, + inner_var, + indent=indent, + include_symints=include_symints, + ) + + # Emit symint extraction + if include_symints: + size_placeholders = _compute_placeholders(meta.outer_size) + stride_placeholders = _compute_placeholders(meta.outer_stride) + has_size_symints = any(size_placeholders) + has_stride_symints = any(stride_placeholders) + + if has_size_symints or has_stride_symints: + size_var = state.fresh_name("_size") + state.emit(f"{size_var} = {var}.size()", indent=indent) + for i, is_sym in enumerate(size_placeholders): + if is_sym: + state.emit(f"unwrapped_args.append({size_var}[{i}])", indent=indent) + + stride_var = state.fresh_name("_stride") + state.emit(f"{stride_var} = {var}.stride()", indent=indent) + for i, is_sym in enumerate(stride_placeholders): + if is_sym: + state.emit( + f"unwrapped_args.append({stride_var}[{i}])", indent=indent + ) + + +def _concrete_value(val: None | int | SymInt) -> int: + """Get the concrete int value for a non-symbolic size/stride entry. + + Used for entries that are NOT symbolic placeholders, meaning they are + concrete ints or nested ints (represented as -1 after make_runtime_safe). + """ + if isinstance(val, int): + return val + # Before make_runtime_safe: nested ints are SymInts; use -1 as dummy. + # After make_runtime_safe: they're already -1. + if isinstance(val, SymInt) and val.node.is_nested_int(): + return -1 + raise AssertionError(f"Expected concrete int, got {type(val)}: {val}") + + +def _codegen_wrap_subclass( + state: _CodegenState, + meta: SubclassCreationMeta, + out_idx_ref: list[int], +) -> str: + """Emit code to reconstruct one subclass output. Returns the variable name.""" + inner_dict_var = state.fresh_name("_out_inner") + entries: list[str] = [] + + for attr, attr_meta in meta.attrs.items(): + match attr_meta: + case PlainTensorMeta() | OpaqueMeta(): + idx = out_idx_ref[0] + out_idx_ref[0] += 1 + entries.append(f"{attr!r}: unwrapped_outs[{idx}]") + case SubclassCreationMeta(): + nested_var = _codegen_wrap_subclass(state, attr_meta, out_idx_ref) + entries.append(f"{attr!r}: {nested_var}") + + state.emit(f"{inner_dict_var} = {{{', '.join(entries)}}}") + + # Reconstruct outer_size and outer_stride + size_placeholders = _compute_placeholders(meta.outer_size) + stride_placeholders = _compute_placeholders(meta.outer_stride) + + def _build_tuple( + outer: Iterable[None | int | SymInt], placeholders: list[bool] + ) -> str: + parts: list[str] = [] + for val, is_sym in zip(outer, placeholders): + if is_sym: + idx = out_idx_ref[0] + out_idx_ref[0] += 1 + parts.append(f"unwrapped_outs[{idx}]") + else: + parts.append(repr(_concrete_value(val))) + if len(parts) == 1: + return f"({parts[0]},)" + return f"({', '.join(parts)})" + + size_expr = _build_tuple(meta.outer_size, size_placeholders) + stride_expr = _build_tuple(meta.outer_stride, stride_placeholders) + + type_name = state.add_global( + state.fresh_name("_subclass_type"), + meta.original_subclass_type or type(meta.original_subclass), + ) + meta_name = state.add_global(state.fresh_name("_meta"), meta.meta) + + result_var = state.fresh_name("_out") + state.emit( + f"{result_var} = {type_name}.__tensor_unflatten__(" + f"{inner_dict_var}, {meta_name}, {size_expr}, {stride_expr})" + ) + return result_var + + +def _emit_output_wrapping( + state: _CodegenState, + out_metas: list[PlainTensorMeta | SubclassCreationMeta], +) -> tuple[list[str], int]: + """Emit wrapping code for output metas. + + Returns (result_exprs, num_args_tallied) where result_exprs are Python + expression strings referencing each wrapped output. + """ + out_idx_ref = [0] + result_exprs: list[str] = [] + num_args_tallied = 0 + + for meta in out_metas: + if isinstance(meta, PlainTensorMeta): + result_exprs.append(f"unwrapped_outs[{meta.unwrapped_idx}]") + num_args_tallied += 1 + out_idx_ref[0] = max(out_idx_ref[0], meta.unwrapped_idx + 1) + else: + result_var = _codegen_wrap_subclass(state, meta, out_idx_ref) + result_exprs.append(result_var) + num_args_tallied += meta.arg_count + + return result_exprs, num_args_tallied + + +def _emit_input_unwrapping( + state: _CodegenState, + inp_metas: list[PlainTensorMeta | SubclassCreationMeta], + frozen_inp_indices: frozenset[int] = frozenset(), + include_symints: bool = True, +) -> None: + """Emit unwrapping code for input metas into unwrapped_args. + + Caller must have already emitted ``unwrapped_args = []``. + """ + for i, meta in enumerate(inp_metas): + if isinstance(meta, PlainTensorMeta): + state.emit(f"unwrapped_args.append(args[{i}])") + elif i in frozen_inp_indices: + # Frozen by inductor freezing: constant already baked into graph. + state.emit("unwrapped_args.append(None)") + else: + inp_var = state.fresh_name("_inp") + type_name = state.add_global( + state.fresh_name("_expected_type"), + meta.original_subclass_type or type(meta.original_subclass), + ) + state.emit(f"{inp_var} = args[{i}]") + state.emit( + f"assert type({inp_var}) is {type_name}, " + f"f'expected {{{type_name}}}, got {{type({inp_var})}}'", + ) + _codegen_unwrap_subclass( + state, meta, inp_var, indent=1, include_symints=include_symints + ) + + +def _codegen_subclass_wrapper_source( + inp_metas: list[PlainTensorMeta | SubclassCreationMeta], + out_metas: list[PlainTensorMeta | SubclassCreationMeta], + num_fw_outs_saved_for_bw: int | None, + frozen_inp_indices: frozenset[int] = frozenset(), + act_input_indices: list[int] | None = None, +) -> tuple[str, dict[str, object]]: + """Generate source and globals for a subclass wrapper. + + Returns (source, globals_dict). The globals_dict will NOT contain + ``compiled_fn`` — the caller is responsible for adding it before exec. + """ + state = _CodegenState() + + state.emit("def inner_fn(args):", indent=0) + + # --- Resolve AsyncCollectiveTensors --- + # ACTs are transient eager-mode wrappers for async collective overlap. + # Inductor triton kernels bypass __torch_dispatch__, so we must call + # trigger_wait() before the compiled graph uses the data. + if act_input_indices: + for i in act_input_indices: + state.emit(f"args[{i}] = args[{i}].trigger_wait()") + + # --- Input unwrapping --- + state.emit("unwrapped_args = []") + _emit_input_unwrapping(state, inp_metas, frozen_inp_indices=frozen_inp_indices) + + # Pass through any trailing args not covered by inp_metas + # (e.g. rng seed/offset added by FunctionalizedRngRuntimeWrapper). + num_inp_metas = len(inp_metas) + state.emit(f"unwrapped_args.extend(args[{num_inp_metas}:])") + state.emit("args.clear()") + + # --- Call compiled function --- + state.emit("unwrapped_outs = compiled_fn(unwrapped_args)") + + # --- Output wrapping --- + result_exprs, num_args_tallied = _emit_output_wrapping(state, out_metas) + result_tuple = f"({', '.join(result_exprs)},)" if result_exprs else "()" + if num_fw_outs_saved_for_bw is not None: + state.emit( + f"return {result_tuple} + tuple(unwrapped_outs[{num_args_tallied}:])" + ) + else: + state.emit(f"return {result_tuple}") + + source = "\n".join(state.lines) + return source, state.globals + + +def _codegen_subclass_wrap_source( + out_metas: list[PlainTensorMeta | SubclassCreationMeta], +) -> tuple[str, dict[str, object]]: + """Generate source for wrapping flat outputs into subclasses. + + Used for the backward epilogue. Shares output-wrapping logic with + _codegen_subclass_wrapper_source via _emit_output_wrapping. + """ + state = _CodegenState() + state.emit("def wrap_fn(unwrapped_outs):", indent=0) + result_exprs, _ = _emit_output_wrapping(state, out_metas) + result_tuple = f"({', '.join(result_exprs)},)" if result_exprs else "()" + state.emit(f"return {result_tuple}") + source = "\n".join(state.lines) + return source, state.globals + + +def _compile_and_exec_source( + source: str, + globals_dict: dict[str, object], + fn_name: str, + artifact_name: str, + wrapped_fn: Callable[..., object] | None = None, +) -> Callable[..., object]: + """Compile generated source, exec it, and return the named function. + + If wrapped_fn is provided, applies functools.update_wrapper so that + __wrapped__ and __dict__ (e.g. _fx_graph_cache_key) propagate to the + generated function. + """ + if log.isEnabledFor(logging.DEBUG): + log.debug("Generated %s:\n%s", artifact_name, source) + + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": artifact_name, + "encoding": "string", + }, + payload_fn=lambda: source, + ) + + code = compile(source, f"<{artifact_name}>", "exec") + local_dict: dict[str, object] = {} + exec(code, globals_dict, local_dict) # noqa: S102 + fn = local_dict[fn_name] + if wrapped_fn is not None: + functools.update_wrapper(fn, wrapped_fn) # type: ignore[arg-type] + return fn # type: ignore[return-value] + + +def codegen_backward_subclass_fns( + grad_input_metas: list[PlainTensorMeta | SubclassCreationMeta] | None = None, +) -> tuple[Callable[..., object], Callable[..., object] | None]: + """Generate codegen'd unwrap and wrap functions for the backward pass. + + Returns (unwrap_fn, wrap_fn). unwrap_fn is used by the backward prologue + to unwrap non-tangent subclass inputs (always an identity in AOT dispatch + since the compiled forward operates on unwrapped inner tensors). wrap_fn + is used by the backward epilogue to wrap flat grad inputs back into + subclasses; it is None when grad_input_metas is None. + """ + source = "def unwrap_fn(args):\n return list(args)" + globals_dict: dict[str, object] = {} + unwrap_fn = _compile_and_exec_source( + source, globals_dict, "unwrap_fn", "backward_subclass_unwrap" + ) + + wrap_fn = None + if grad_input_metas is not None: + wrap_source, wrap_globals = _codegen_subclass_wrap_source(grad_input_metas) + wrap_fn = _compile_and_exec_source( + wrap_source, wrap_globals, "wrap_fn", "backward_subclass_wrapper" + ) + + return unwrap_fn, wrap_fn + + +def codegen_subclass_wrapper( + compiled_fn: Callable[..., object], + inp_metas: list[PlainTensorMeta | SubclassCreationMeta], + out_metas: list[PlainTensorMeta | SubclassCreationMeta], + num_fw_outs_saved_for_bw: int | None, + frozen_inp_indices: frozenset[int] = frozenset(), + act_input_indices: list[int] | None = None, +) -> Callable[..., object]: + """Generate a specialized wrapper function for subclass unwrap/wrap.""" + source, globals_dict = _codegen_subclass_wrapper_source( + inp_metas, + out_metas, + num_fw_outs_saved_for_bw, + frozen_inp_indices, + act_input_indices=act_input_indices, + ) + globals_dict["compiled_fn"] = compiled_fn + return _compile_and_exec_source( + source, globals_dict, "inner_fn", "subclass_wrapper", wrapped_fn=compiled_fn + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/subclass_parametrization.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/subclass_parametrization.py new file mode 100644 index 0000000000000000000000000000000000000000..0911526b91242798463f402fb83cd8c671de795a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/subclass_parametrization.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +import dataclasses +import itertools +from typing import Any, TYPE_CHECKING + +import torch +from torch._library.opaque_object import is_opaque_reference_type +from torch._opaque_base import OpaqueBase +from torch.utils._python_dispatch import is_traceable_wrapper_subclass + +from .schemas import OpaqueMeta + + +if TYPE_CHECKING: + from collections.abc import Iterable + + +# This is technically very similar to SubclassCreatingMeta +# in aot_autograd, but we don't need all the stuff in there +# so just recreated a new dataclass. +@dataclasses.dataclass +class SubclassCreationMeta: + start_idx: int + num_tensors: int + class_type: Any + # None means the attr is a plain tensor (base case of recursion) + attrs: dict[str, SubclassCreationMeta | OpaqueMeta | None] + metadata: Any + outer_size: Iterable[None | int | torch.SymInt] + outer_stride: Iterable[None | int | torch.SymInt] + + +class UnwrapTensorSubclass(torch.nn.Module): + def forward(self, *tensors) -> torch.Tensor: # type: ignore[no-untyped-def] + todo: list[torch.Tensor | OpaqueBase] = list(tensors) + + def _unwrap_tensor_subclasses(subclass_meta, tensors, offset): # type: ignore[no-untyped-def] + if subclass_meta is None: + return tensors[offset], offset + 1 + inner_tensors = {} + for attr, meta in subclass_meta.attrs.items(): + if isinstance(meta, OpaqueMeta): + inner_tensors[attr] = tensors[offset] + offset += 1 + else: + built_tensor, offset = _unwrap_tensor_subclasses( + meta, tensors, offset + ) + inner_tensors[attr] = built_tensor + rebuilt = subclass_meta.class_type.__tensor_unflatten__( + inner_tensors, + subclass_meta.metadata, + subclass_meta.outer_size, + subclass_meta.outer_stride, + ) + return rebuilt, offset + + return _unwrap_tensor_subclasses(self.subclass_meta, todo, 0)[0] + + def right_inverse(self, tensor: torch.Tensor) -> list[torch.Tensor | OpaqueBase]: + if type(tensor) is torch.Tensor: + raise AssertionError("tensor must be a subclass, not torch.Tensor") + plain_tensors: list[torch.Tensor | OpaqueBase] = [] + + def _create_subclass_meta(tensor, idx, plain_tensor_container): # type: ignore[no-untyped-def] + if type(tensor) is torch.Tensor: + plain_tensor_container.append(tensor) + return None, idx + 1 + inner_tensors_attrnames, metadata = tensor.__tensor_flatten__() # type: ignore[attr-defined] + new_idx = idx + attr_to_meta: dict[str, SubclassCreationMeta | OpaqueMeta | None] = {} + for attr in inner_tensors_attrnames: + val = getattr(tensor, attr) + match val: + case OpaqueBase(): + if not is_opaque_reference_type(type(val)): + raise ValueError( + f"{type(val).__name__!r} found in tensor attrs of " + f"{type(tensor).__name__}.__tensor_flatten__(). " + "Only tensors and reference-type opaques are allowed " + "in tensor attrs." + ) + attr_to_meta[attr] = OpaqueMeta() + plain_tensor_container.append(val) + new_idx += 1 + case torch.Tensor(): + subclass_meta, new_idx = _create_subclass_meta( + val, new_idx, plain_tensor_container + ) + attr_to_meta[attr] = subclass_meta + case _: + raise AssertionError( + f"expected Tensor or OpaqueBase, got {type(val)}" + ) + return ( + SubclassCreationMeta( + start_idx=idx, + num_tensors=new_idx - idx, + class_type=type(tensor), + attrs=attr_to_meta, + metadata=metadata, + outer_size=tensor.size(), + outer_stride=tensor.stride(), + ), + new_idx, + ) + + self.subclass_meta = _create_subclass_meta(tensor, 0, plain_tensors)[0] + return plain_tensors + + +def unwrap_tensor_subclass_parameters(module: torch.nn.Module) -> torch.nn.Module: + """ + Model transformation that replaces all the parameters that are subclasses to plain tensors. + This reduces runtime overhead of flattening/unflattening the parameters. + + This transformation adds parametrization with `torch.nn.utils.parametrize`. + The FQNs of the subclass parameters will be changed and state_dict will become incompatible with the original model. + E.g. + Original model state_dict: {"p1": torch.testing._internal.TwoTensor} + becomes: {"parametrizations.p2.original0": torch.Tensor, "parametrizations.p2.original1": torch.Tensor} + + """ + for name, tensor in itertools.chain( + list(module.named_parameters(recurse=False)), + # pyrefly: ignore [bad-argument-type, no-matching-overload] + list(module.named_buffers(recurse=False)), + ): + if is_traceable_wrapper_subclass(tensor): + torch.nn.utils.parametrize.register_parametrization( + module, name, UnwrapTensorSubclass() + ) + + for child in module.children(): + unwrap_tensor_subclass_parameters(child) + + return module diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/subclass_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/subclass_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..d019d9f5649e6ea12edbe836b8f55b4b6151ae66 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/subclass_utils.py @@ -0,0 +1,655 @@ +""" +This file contains utilities for tracing through __torch_dispatch__ based tensor subclasses and modes. +AOTAutograd's responsibility is to trace through all pytorch capabilities that live in the pytorch dispatcher, +and this includes tensor subclasses that implement __torch_dispatch__. +""" + +import collections +import typing +from collections.abc import Callable, Iterable, Sequence +from typing import Any, TypeGuard, TypeVar + +import torch +import torch.utils._pytree as pytree +from torch import SymInt, Tensor +from torch._library.fake_class_registry import maybe_unwrap_fake_script_object +from torch._library.opaque_object import is_opaque_reference_type +from torch._opaque_base import OpaqueBase +from torch._subclasses.fake_tensor import get_plain_tensors +from torch.types import IntLikeType +from torch.utils._python_dispatch import is_traceable_wrapper_subclass + +from .descriptors import ( + AOTInput, + AOTOutput, + DummyAOTInput, + SubclassGetAttrAOTInput, + SubclassGetAttrAOTOutput, + SubclassSizeAOTInput, + SubclassSizeAOTOutput, + SubclassStrideAOTInput, + SubclassStrideAOTOutput, +) +from .schemas import ( + FakifiedFlatArgs, + FxValue, + MutationType, + OpaqueMeta, + PlainTensorMeta, + SubclassCreationMeta, + ViewAndMutationMeta, +) +from .utils import strict_zip + + +zip = strict_zip + +T = TypeVar("T", bound=torch.Tensor) + + +def requires_subclass_dispatch( + args: FakifiedFlatArgs, fw_metadata: ViewAndMutationMeta +) -> bool: + args_flattened = pytree.arg_tree_leaves(*args) + any_subclass_args = any( + is_traceable_wrapper_subclass(x) + for x in args_flattened + if isinstance(x, Tensor) + ) + from torch._functorch._aot_autograd.schemas import SubclassCreationMeta + + any_subclass_outputs = any( + type(x) is SubclassCreationMeta for x in fw_metadata.subclass_fw_graph_out_meta + ) + # This tells us whether or not we need to perform any unwrapping/wrapping of tensor subclasses at runtime. + return bool(any_subclass_args or any_subclass_outputs) + + +from .schemas import MemoryFormatMeta + + +def maybe_suggest_memory_format( + t: Tensor, with_memory_format: bool +) -> MemoryFormatMeta | None: + if not with_memory_format: + return None + + return MemoryFormatMeta.from_tensor(t) + + +def get_subclass_typing_container( + tensor_subclass: torch.Tensor, +) -> dict[type[torch.Tensor], list[type[torch.Tensor]]]: + """ + Given a subclass, returns a recursive dictionary mapping each + inner tensors to its' subclass types. + """ + + def _get_types_for_subclass(tensor_subclass: torch.Tensor) -> None: + if not is_traceable_wrapper_subclass(tensor_subclass): + return + tracker[type(tensor_subclass)].append(tensor_subclass) + inner_keys, _ = tensor_subclass.__tensor_flatten__() + for key in inner_keys: + match getattr(tensor_subclass, key): + case torch.Tensor() as inner_value: + _get_types_for_subclass(inner_value) + case OpaqueBase(): + pass + case unexpected: + raise AssertionError( + f"expected Tensor or OpaqueBase, got {type(unexpected)}" + ) + + tracker: dict[Any, list[Any]] = collections.defaultdict(list) + _get_types_for_subclass(tensor_subclass) + return tracker + + +def create_subclass_metadata( + a: Any, + start_idx: int, + count_symints: bool, + with_memory_format: bool = False, +) -> tuple[Any, int]: + if not is_traceable_wrapper_subclass(a): + idx = start_idx + 1 + return ( + PlainTensorMeta( + idx, + memory_format=maybe_suggest_memory_format(a, with_memory_format), + ), + idx, + ) + + inner_keys, metadata = a.__tensor_flatten__() + new_start_idx = start_idx + attrs: dict[str, SubclassCreationMeta | PlainTensorMeta | OpaqueMeta] = {} + + for key in inner_keys: + inner_value = getattr(a, key) + match inner_value: + case OpaqueBase(): + # During tracing, opaques are wrapped in FakeScriptObject; + # unwrap to check the real type. + real_type = type(maybe_unwrap_fake_script_object(inner_value)) + if not is_opaque_reference_type(real_type): + raise RuntimeError( + f"{real_type.__name__!r} found in tensor attrs of " + f"{type(a).__name__}.__tensor_flatten__(). " + "Only tensors and reference-type opaques are allowed " + "in tensor attrs." + ) + attrs[key] = OpaqueMeta() + new_start_idx += 1 + case Tensor(): + new_subclass_meta, new_start_idx = create_subclass_metadata( + inner_value, + new_start_idx, + count_symints=count_symints, + with_memory_format=with_memory_format, + ) + attrs[key] = new_subclass_meta + case _: + raise AssertionError( + f"expected Tensor or OpaqueBase, got {type(inner_value)}" + ) + + # It *must* be because is_traceable_wrapper_subclass() - but mypy is not smart. + if not isinstance(a, Tensor): + raise AssertionError(f"expected Tensor, got {type(a)}") + + new_start_idx = ( + new_start_idx + + count_symints * len(enumerate_filter_symints(a.size())) + + count_symints * len(enumerate_filter_symints(a.stride())) + ) + + return ( + SubclassCreationMeta( + flat_tensor_start_idx=start_idx, + arg_count=new_start_idx - start_idx, + included_subclass_symints=count_symints, + attrs=attrs, + meta=metadata, + outer_size=a.size(), # type: ignore[attr-defined, arg-type] + outer_stride=a.stride(), # type: ignore[arg-type] + original_subclass=a, + memory_format=maybe_suggest_memory_format(a, with_memory_format), + ), + new_start_idx, + ) + + +# Given a flat list of arguments, some of which may be tensor subclasses, +# computes metadata about "how to reconstruct the current list of subclasses, +# if we were given their flattened dense tensors instead" +def create_subclass_meta( + curr_args: list[Any] | tuple[Any, ...], + *, + count_symints: bool = True, + with_memory_format: bool = False, +) -> list[PlainTensorMeta | SubclassCreationMeta]: + idx = 0 + infos: list[PlainTensorMeta | SubclassCreationMeta] = [] + for a in curr_args: + if is_traceable_wrapper_subclass(a): + if not isinstance(a, Tensor): + raise AssertionError( + f"expected Tensor for traceable wrapper subclass, got {type(a)}" + ) + start_idx = idx + subclass_meta, _ = create_subclass_metadata( + a, + start_idx, + count_symints=count_symints, + with_memory_format=with_memory_format, + ) + infos.append(subclass_meta) + cnt = subclass_meta.arg_count + else: + infos.append( + PlainTensorMeta( + idx, + memory_format=maybe_suggest_memory_format(a, with_memory_format), + ) + ) + cnt = 1 + idx += cnt + return infos + + +def enumerate_filter_symints(lst: Iterable[IntLikeType]) -> list[tuple[int, SymInt]]: + # Capture all SymInts from the iterable. + def symint_check(s: IntLikeType) -> TypeGuard[SymInt]: + return isinstance(s, SymInt) and not s.node.is_nested_int() + + return [(i, s) for i, s in enumerate(lst) if symint_check(s)] + + +def compute_symint_placeholders(lst: Iterable[None | int | SymInt]) -> list[bool]: + # Non-nested symints are replaced with None in `make_runtime_safe()` + return [s is None for s in lst] + + +# Intended to make it easier to define function that is +# either (AOTInput -> AOTInput) or (AOTOutput -> AOTOutput) +# but not the other combos +AOTDescriptor = TypeVar("AOTDescriptor", AOTInput, AOTOutput) + + +# This function takes in a pytree of arguments and unwraps any tensor +# subclasses. +# +# NOTE: The reason for "append_symints": +# +# * At compile time: we append extra symint args when unwrapping primals +# (but not tangents, because they should always share symints with primals). +# We also append extra symints when unwrapping the subclass outputs of the +# traced function, so we can return them as extra outputs +# +# * At runtime: we similarly append subclass sizes when we unwrap subclass +# primals (but not tangents) on entry to the forward. See the runtime version of +# this function below. +def unwrap_tensor_subclasses( + wrapped_args: list[FxValue], + wrapped_args_descs: Sequence[AOTDescriptor], + *, + append_symints: bool, +) -> tuple[list[FxValue], list[AOTDescriptor]]: + def _maybe_fakeify_opaque(v: Any) -> Any: + # Registered opaque types need to be wrapped as FakeScriptObject for + # compile-time FX tracing (proxy slot tracking, hashability, etc.). + if isinstance(v, OpaqueBase): + from torch._guards import detect_fake_mode + from torch._library.fake_class_registry import maybe_to_fake_obj + from torch._library.opaque_object import is_opaque_type + + fake_mode = detect_fake_mode() + if fake_mode is not None and is_opaque_type(type(v)): + return maybe_to_fake_obj(fake_mode, v) + return v + + def flatten_subclass( + t: FxValue, + desc: AOTDescriptor, + *, + out: tuple[list[FxValue], list[AOTDescriptor]], + ) -> None: + # unwrap a subclass into plain tensors and their size/stride if "append_symint" + # is True + if not is_traceable_wrapper_subclass(t): + out[0].append(_maybe_fakeify_opaque(t)) + out[1].append(desc) + return + + attrs, _ = t.__tensor_flatten__() + + SubclassGetAttr: Callable[[AOTInput | AOTOutput, str], AOTDescriptor] + SubclassSize: Callable[[AOTInput | AOTOutput, int], AOTDescriptor] + SubclassStride: Callable[[AOTInput | AOTOutput, int], AOTDescriptor] + if isinstance(desc, AOTInput): + SubclassGetAttr = SubclassGetAttrAOTInput # type: ignore[bad-assignment] + SubclassSize = SubclassSizeAOTInput # type: ignore[bad-assignment] + SubclassStride = SubclassStrideAOTInput # type: ignore[bad-assignment] + else: + SubclassGetAttr = SubclassGetAttrAOTOutput # type: ignore[bad-assignment] + SubclassSize = SubclassSizeAOTOutput # type: ignore[bad-assignment] + SubclassStride = SubclassStrideAOTOutput # type: ignore[bad-assignment] + + for attr in attrs: + inner_value = getattr(t, attr) + n_desc: Any = SubclassGetAttr(desc, attr) + flatten_subclass(inner_value, n_desc, out=out) + + if append_symints: + sizes = enumerate_filter_symints(t.size()) + strides = enumerate_filter_symints(t.stride()) + out[0].extend(s for _, s in sizes) + out[0].extend(s for _, s in strides) + out[1].extend(SubclassSize(desc, i) for i, _ in sizes) + out[1].extend(SubclassStride(desc, i) for i, _ in strides) + + xs_inner: list[FxValue] = [] + descs_inner: list[AOTDescriptor] = [] + + for x, desc in zip(wrapped_args, wrapped_args_descs): + flatten_subclass(x, desc, out=(xs_inner, descs_inner)) + + return xs_inner, descs_inner + + +# subclass_metas is needed at runtime to compute which indices are symints in +# the outer_size/outer_stride +def runtime_unwrap_tensor_subclasses( + wrapped_args: list[Tensor | int], + *, + append_symints: bool, + subclass_metas: list[PlainTensorMeta | SubclassCreationMeta] | None = None, +) -> list[int | Tensor | SymInt | OpaqueBase]: + def flatten_subclass( + x: Tensor, + subclass_meta: PlainTensorMeta | SubclassCreationMeta | OpaqueMeta | None, + *, + out: list[OpaqueBase | SymInt | Tensor | int], + ) -> list[OpaqueBase | SymInt | Tensor | int]: + if not is_traceable_wrapper_subclass(x): + out.append(x) + return out + + if not isinstance(x, Tensor): + raise AssertionError(f"expected Tensor, got {type(x)}") + if not isinstance(subclass_meta, SubclassCreationMeta): + raise AssertionError("subclass_meta should be a SubclassCreationMeta") + + attrs, _ = x.__tensor_flatten__() + + for attr in attrs: + inner_value = getattr(x, attr) + match inner_value: + case OpaqueBase(): + out.append(inner_value) + case Tensor(): + inner_meta = subclass_meta.attrs.get(attr) + flatten_subclass(inner_value, inner_meta, out=out) + case _: + raise AssertionError( + f"expected Tensor or OpaqueBase, got {type(inner_value)}" + ) + + if append_symints: + # outer_size + size = x.size() + symint_placeholders = compute_symint_placeholders(subclass_meta.outer_size) + if len(size) != len(symint_placeholders): + raise AssertionError( + f"size length mismatch: {len(size)} != {len(symint_placeholders)}" + ) + out.extend( + [r for (r, is_symint) in zip(size, symint_placeholders) if is_symint] + ) + + # outer_stride + stride = x.stride() + symint_placeholders = compute_symint_placeholders( + subclass_meta.outer_stride + ) + if len(stride) != len(symint_placeholders): + raise AssertionError( + f"stride length mismatch: {len(stride)} != {len(symint_placeholders)}" + ) + out.extend( + [r for (r, is_symint) in zip(stride, symint_placeholders) if is_symint] + ) + return out + + xs_inner: list[int | Tensor | SymInt | OpaqueBase] = [] + + if append_symints: + if subclass_metas is None: + raise AssertionError( + "subclass_metas must not be None when append_symints is True" + ) + + for idx, x in enumerate(wrapped_args): + if not is_traceable_wrapper_subclass(x): + xs_inner.append(x) + continue + + if subclass_metas is None: + get_plain_tensors(typing.cast(Tensor, x), out=xs_inner) + else: + subclass_meta = subclass_metas[idx] + if not isinstance(subclass_meta, SubclassCreationMeta): + raise AssertionError( + f"expected SubclassCreationMeta, got {type(subclass_meta)}" + ) + flatten_subclass(typing.cast(Tensor, x), subclass_meta, out=xs_inner) + + return xs_inner + + +def unwrap_tensor_subclasses_with_indices_to_original( + wrapped_args: list[Any], +) -> tuple[list[Any], list[int]]: + ret_unwrapped = [] + ret_indices_to_original = [] + for i, a in enumerate(wrapped_args): + a_unwrapped, _ = unwrap_tensor_subclasses( + [a], [DummyAOTInput(9999)], append_symints=False + ) + ret_unwrapped.extend(a_unwrapped) + n = len(a_unwrapped) + ret_indices_to_original.extend([i] * n) + + return ret_unwrapped, ret_indices_to_original + + +def remap_unwrapped_subclass_arg_indices( + wrapped_args: list[Any], static_input_indices: list[int] +) -> list[int]: + static_input_indices_set = set(static_input_indices) + new_ind = 0 + remapped_static_indices = [] + for i, arg in enumerate(wrapped_args): + num_indices = 1 + if is_traceable_wrapper_subclass(arg): + num_indices = ( + len(get_plain_tensors(typing.cast(Tensor, arg), out=[])) + + len(enumerate_filter_symints(arg.size())) + + len(enumerate_filter_symints(arg.stride())) + ) + + for _ in range(num_indices): + if i in static_input_indices_set: + remapped_static_indices.append(new_ind) + + new_ind += 1 + + return remapped_static_indices + + +# Turns a flattened list of tensor arguments into (maybe) subclass tensors. +# This function is used both at trace time and runtime, so we have an is_runtime flag telling us which context we're in. +def wrap_tensor_subclasses( + unwrapped_args: Sequence[Any], + *, + subclass_metas: list[PlainTensorMeta | SubclassCreationMeta], + num_fw_outs_saved_for_bw: int | None = None, + included_subclass_symints: bool = False, + is_runtime: bool = False, + make_subclass_override: Callable[..., Any] | None = None, +) -> tuple[Any, ...]: + # pyrefly: ignore [implicit-any] + wrapped_args = [] + num_args_tallied = 0 + for subclass_meta in subclass_metas: + if isinstance(subclass_meta, PlainTensorMeta): + wrapped_args.append(unwrapped_args[subclass_meta.unwrapped_idx]) + num_args_tallied += 1 + else: + if not isinstance(subclass_meta, SubclassCreationMeta): + raise AssertionError( + f"expected SubclassCreationMeta, got {type(subclass_meta)}" + ) + if subclass_meta.included_subclass_symints != included_subclass_symints: + raise AssertionError( + f"included_subclass_symints mismatch: {subclass_meta.included_subclass_symints} != {included_subclass_symints}" + ) + + if make_subclass_override: + wrapped_args.append( + make_subclass_override(subclass_meta, is_runtime, unwrapped_args) + ) + else: + wrapped_args.append( + subclass_meta.creation_fn( + unwrapped_args, + is_runtime=is_runtime, + ) + ) + num_args_tallied += subclass_meta.arg_count + + # Note: [Partitioner handling for Subclasses, Part 2] + # At the beginning of AOTAutograd, we collect metadata on the inputs and outputs of the user fw, + # to figure out which inputs/outputs are subclasses, and how to reconstruct the subclasses after flattening them. + # + # When this function is called at runtime in the forward, + # we have been passed a list of (flattened) dense-tensor fw-outs, and need to reconstruct any subclass fw outs. + # + # One reasonable question that you should ask: when should the dense_tensor -> subclass_tensor wrapping happen? + # Answer: we do it **inside of our compiled autograd.Function**. + # This seems like morally the right place: autograd happens above subclass desugaring, + # so autograd should see actual tensor subclasses at runtime, and not flattened dense tensors. + # + # This causes a tricky interaction though: when we run the min-cut partitioner to divvy up the joint graph + # into a forward and backward graph, we end up with some activations that show up as extra outputs + # in the compiled forward graph, that are **not** user outputs. + # These activations are not visible to the user, and so there's no need for us to wrap them back into subclasses. + # + # On top of that, when we first computed subclass metadata (in `run_functionalized_fw_and_collect_metadata`), + # we computed subclass metadata on every forward output, but this did **not** include activations + # created by the partitioner. + # as a result, `unwrapped_args` here will correspond to (*unwrapped_user_fw_outs, *activations), + # but `subclass_metas` will only correspond to subclass metadata on `user_fw_outs`. + # We then need to make sure that we return (*wrapped_user_fw_outs, *activations). + if num_fw_outs_saved_for_bw is not None: + if len(unwrapped_args) != num_args_tallied + num_fw_outs_saved_for_bw: + raise AssertionError( + f"Expected the number actual unwrapped-subclass outputs {len(unwrapped_args)} to equal " + f"the number of args calculated from subclasses ({num_args_tallied}) plus the number of " + f"additional activations saved for the backward pass ({num_fw_outs_saved_for_bw})" + ) + activations = unwrapped_args[num_args_tallied:] + if isinstance(wrapped_args, tuple) and isinstance(activations, tuple): + return wrapped_args + activations + return tuple(list(wrapped_args) + list(activations)) + else: + if len(unwrapped_args) != num_args_tallied: + raise AssertionError( + f"Expected {len(unwrapped_args)} == {num_args_tallied}" + ) + return tuple(wrapped_args) + + +# Given a bunch of "dense" tensor arguments, this function (potentially) wraps them into tensor subclasses. +# This function carefully handles the inference vs. joint cases: +# - when is_joint_structure is True, args is (primals, tangents) +# - when is_joint_structure is False, args is [*primals] +def wrap_tensor_subclasses_maybe_joint( + unwrapped_args: Sequence[Any], + *, + is_joint_structure: bool, + meta: ViewAndMutationMeta, +) -> tuple[Any, ...]: + # Since this function is reused for both inference and joint graphs, + if is_joint_structure: + if not (isinstance(unwrapped_args, tuple) and len(unwrapped_args) == 2): + unwrapped_len = ( + len(unwrapped_args) + if isinstance(unwrapped_args, (tuple, list)) + else "N/A" + ) + raise AssertionError( + f"expected tuple of length 2 for joint structure, " + f"got {type(unwrapped_args)} with length {unwrapped_len}" + ) + if not ( + isinstance(unwrapped_args[0], (tuple, list)) + and isinstance(unwrapped_args[1], (tuple, list)) + ): + raise AssertionError( + f"expected primals and tangents to be tuple or list, got {type(unwrapped_args[0])} and {type(unwrapped_args[1])}" + ) + primals, tangents = unwrapped_args[0], unwrapped_args[1] + wrapped_primals = wrap_tensor_subclasses( + primals, + subclass_metas=meta.subclass_inp_meta, + included_subclass_symints=True, + ) + wrapped_tangents = wrap_tensor_subclasses( + tangents, + subclass_metas=meta.subclass_tangent_meta, + included_subclass_symints=False, + ) + return (wrapped_primals, wrapped_tangents) + else: + wrapped_args = wrap_tensor_subclasses( + unwrapped_args, + subclass_metas=meta.subclass_inp_meta, + included_subclass_symints=True, + ) + return wrapped_args + + +def compute_inner_mutated_inp_indices_from_subclass_meta( + fw_metadata: ViewAndMutationMeta, + inner_metadata: ViewAndMutationMeta, +) -> list[int]: + # Note: [Recomputing subclass mutation handling] + # + # Generally, if a subclass requires grad, its components will not require grad. + # But for the purposes of tracking returned tensors, we should treat those component + # tensors as if they require grad. + # + # For example, if the subclass tensor requires grad and will be mutated in a way that + # requires us to handle the mutation outside of the graph, we need to return it + # from the forward graph. The inner_meta data won't consider the component tensors + # as if they need to be returned, because they don't require grad; but really, we + # should handle those tensors the same way we handle the subclass tensor itself; i.e. + # if we'd include the subclass tensor as part of the outputs, then we should also + # include the component tensors. + # + # To do this, we patch num_mutated_inp_runtime_indices below by expanding the inputs + # from the outer subclass tensors and propagating + + updated_input_info = [] + inner_idx = 0 + if not fw_metadata.subclass_inp_meta: + # Sometimes we don't have subclass info, e.g. synthetic_base codepaths + return inner_metadata.mutated_inp_runtime_indices + if len(fw_metadata.subclass_inp_meta) != len(fw_metadata.input_info): + raise AssertionError( + f"subclass_inp_meta length ({len(fw_metadata.subclass_inp_meta)}) != input_info length ({len(fw_metadata.input_info)})" + ) + for outer_idx, inp_meta in enumerate(fw_metadata.subclass_inp_meta): + if isinstance(inp_meta, PlainTensorMeta): + if outer_idx >= len(fw_metadata.input_info): + raise AssertionError( + f"outer_idx ({outer_idx}) >= len(fw_metadata.input_info) ({len(fw_metadata.input_info)})" + ) + if inner_metadata is not None: + if inner_idx >= len(inner_metadata.input_info): + raise AssertionError( + f"inner_idx ({inner_idx}) >= len(inner_metadata.input_info) ({len(inner_metadata.input_info)})" + ) + if ( + inner_metadata.input_info[inner_idx] + != fw_metadata.input_info[outer_idx] + ): + raise AssertionError( + f"input_info mismatch at inner_idx={inner_idx}, outer_idx={outer_idx}: " + f"{inner_metadata.input_info[inner_idx]} != {fw_metadata.input_info[outer_idx]}" + ) + updated_input_info.append(fw_metadata.input_info[outer_idx]) + inner_idx += 1 + else: + if inp_meta.original_subclass is None: + raise AssertionError( + "inp_meta.original_subclass must not be None for SubclassCreationMeta" + ) + for _ in range(inp_meta.arg_count): + updated_input_info.append(fw_metadata.input_info[outer_idx]) + inner_idx += 1 + if inner_metadata is not None: + if len(inner_metadata.input_info) != len(updated_input_info): + raise AssertionError( + f"inner_metadata.input_info length ({len(inner_metadata.input_info)}) " + f"!= updated_input_info length ({len(updated_input_info)})" + ) + + return [ + i + for i, inp in enumerate(updated_input_info) + if inp.mutation_type == MutationType.MUTATED_OUT_GRAPH + ] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..a06c82307ffdf57b12b3b248541a42e326a25365 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/_aot_autograd/utils.py @@ -0,0 +1,835 @@ +""" +Contains various utils for AOTAutograd, including those for handling collections. +""" + +import copy +import dataclasses +import logging +import operator +import warnings +from collections.abc import Callable, Sequence +from contextlib import nullcontext +from functools import partial, wraps +from typing import Any, overload, TYPE_CHECKING +from typing_extensions import ParamSpec, TypeVar, TypeVarTuple, Unpack + +import torch +import torch.utils._pytree as pytree +from torch._library.fake_class_registry import FakeScriptObject +from torch._library.opaque_object import is_opaque_value +from torch._logging import getArtifactLogger +from torch._subclasses.fake_tensor import FakeTensor +from torch._subclasses.functional_tensor import FunctionalTensor +from torch.fx.experimental._backward_state import BackwardState +from torch.fx.experimental.proxy_tensor import py_sym_types + + +_T = TypeVar("_T") +if TYPE_CHECKING: + from .schemas import AOTConfig, ViewAndMutationMeta + + +KNOWN_TYPES = [ + torch.Tensor, + BackwardState, + int, + str, + float, + bool, + type(None), + *py_sym_types, + FakeScriptObject, + torch.ScriptObject, +] + +aot_graphs_effects_log = getArtifactLogger(__name__, "aot_graphs_effects") +annotation_log = getArtifactLogger(__name__, "annotation") + +strict_zip = partial(zip, strict=True) + + +def partial_flatten_asdict(obj: object) -> Any: + if dataclasses.is_dataclass(obj): + return { + field.name: getattr(obj, field.name) for field in dataclasses.fields(obj) + } + elif isinstance(obj, (list, tuple)): + return obj.__class__([partial_flatten_asdict(item) for item in obj]) + elif isinstance(obj, dict): + return {k: partial_flatten_asdict(v) for k, v in obj.items()} + else: + return obj + + +@overload +def normalize_as_list(x: _T) -> list[_T]: ... +@overload +def normalize_as_list(x: tuple[_T, ...]) -> list[_T]: ... +@overload +def normalize_as_list(x: list[_T]) -> list[_T]: ... + + +def normalize_as_list(x: object) -> list[object]: + if isinstance(x, tuple): + return list(x) + elif isinstance(x, list): + return x + return [x] + + +def _get_autocast_states() -> list[Any]: + return [ + torch.is_autocast_enabled("cuda"), + torch.is_autocast_enabled("cpu"), + torch.get_autocast_dtype("cuda"), + torch.get_autocast_dtype("cpu"), + torch.is_autocast_cache_enabled(), + ] + + +def make_boxed_func(f: Callable[..., Any]) -> Callable[[list[Any]], Any]: + @simple_wraps(f) + def g(args: list[Any]) -> Any: + return f(*args) + + # pyrefly: ignore[missing-attribute] + g._boxed_call = True + return g + + +def make_boxed_compiler( + compiler: Callable[..., Any], +) -> Callable[..., Any]: + @wraps(compiler) + def f(fx_g: Any, inps: Any) -> Any: + out_f = compiler(fx_g, inps) + fx_g = make_boxed_func(out_f) + return fx_g + + return f + + +def call_func_at_runtime_with_args( + f: Callable[..., Any], + args: Sequence[Any], + steal_args: bool = False, + disable_amp: bool = False, +) -> list[Any]: + if not steal_args: + args = list(args) + if not isinstance(args, list): + raise AssertionError(f"args must be a list, got {type(args)}") + + context = torch._C._DisableAutocast if disable_amp else nullcontext + with context(): + if getattr(f, "_boxed_call", False): + out = normalize_as_list(f(args)) + else: + # TODO: Please remove soon + # https://github.com/pytorch/pytorch/pull/83137#issuecomment-1211320670 + warnings.warn( + "Your compiler for AOTAutograd is returning a function that doesn't take boxed arguments. " + "Please wrap it with functorch.compile.make_boxed_func or handle the boxed arguments yourself. " + "See https://github.com/pytorch/pytorch/pull/83137#issuecomment-1211320670 for rationale.", + stacklevel=2, + ) + out = normalize_as_list(f(*args)) + return out + + +# Inspired by autodidax (thanks!) +class PytreeThunk: + spec: pytree.TreeSpec | None = None + # These are some kinda dumb microoptimizations that save about 3-4 us of overhead. + is_simple: bool | None = ( + None # if the output spec is a tuple/list, we won't bother unflattening it. + ) + is_really_simple: bool | None = None # if the output spec is a LeafSpec + + def set(self, spec: pytree.TreeSpec) -> None: + if not (self.spec is None or self.spec == spec): + raise AssertionError(f"spec mismatch: existing={self.spec}, new={spec}") + if spec is None: + raise AssertionError("spec must not be None") + self.spec: pytree.TreeSpec = spec + if self.spec.type in {tuple, list} and all( + child.is_leaf() for child in spec.children() + ): + self.is_simple = True + if self.spec.is_leaf(): + self.is_really_simple = True + + def unflatten(self, x: Sequence[Any]) -> Any: + if self.is_really_simple: + return x[0] + if self.is_simple: + return x + if self.spec is None: + raise AssertionError("spec must be set before calling unflatten") + return pytree.tree_unflatten(x, self.spec) + + +# Creates a function that returns flattened inputs and outputs +# Also returns the output tree spec, which is needed to recover the "unflattened" +# output tree structure later. +def create_tree_flattened_fn( + fn: Callable[..., Any], + args: Sequence[Any], + kwargs: dict[str, Any] | None = None, +) -> tuple[Callable[..., list[Any]], PytreeThunk]: + if kwargs is None: + kwargs = {} + # Save the args_spec for flat_tensor_args to unflatten while tracing + _, tensor_args_spec = pytree.tree_flatten((args, kwargs)) + out_spec = PytreeThunk() + + def flat_fn(*flat_args: Any) -> list[Any]: + # The input are flattened tensor args. Prepare the args in the + # order that original function expects. Add static args as well. + # They will appear as tensor constants in the traced graph. + nonlocal out_spec + args, kwargs = pytree.tree_unflatten(flat_args, tensor_args_spec) + tree_out = fn(*args, **kwargs) + flat_out, spec = pytree.tree_flatten(tree_out) + for i in flat_out: + is_known_type = isinstance(i, tuple(KNOWN_TYPES)) or is_opaque_value(i) + if not is_known_type: + raise RuntimeError( + f"Found {type(i)} in output, which is not a known type. " + "If this type holds tensors, you need to register a pytree for it. " + "See https://github.com/pytorch/functorch/issues/475 for a brief " + "explanation why. If you don't need to register a pytree, please " + "leave a comment explaining your use case and we'll make this more " + "ergonomic to deal with" + ) + out_spec.set(spec) + return flat_out + + # Can't use functools.wraps here because the wrapper has different + # calling convention + if hasattr(fn, "_orig_mod"): + # pyrefly: ignore[missing-attribute] + flat_fn._orig_mod = fn._orig_mod + + return flat_fn, out_spec + + +# This function takes in a tensor t, and returns one of t, t.view(), or t.clone(). +# When tracing the joint forward + backward, for any inputs in the graph that are mutated, +# we need to clone them first (and similarly for metadata-only mutations, we need to view them first). +# The idea is that when we trace the backward, we need to pass in the *original* primals +# to autograd.grad(), before they were mutated. +# Note: when we have synthetic base inputs, we need to clone them *before* creating views off of them. +# This means that "idx" here represents the index of the (potentially) synthetic base. +# What we need to do is: +# (1) map the current (post-synthetic-base calling convention) input argument index +# to int index pre-synthetic-base-calling-convention. +# (2) There could be multiple, if this index corresponds to a synthetic base +# that has multiple input aliases. +# (3) If any of those corresponding inputs get metadata mutations, then we clone the base. +def maybe_to_fresh_input(idx: int, t: Any, meta: "ViewAndMutationMeta") -> Any: + if not isinstance(t, torch.Tensor): + return t + if idx in meta.mutated_inp_runtime_indices: + # We only need to bother cloning mutated inputs that participate in autograd. + if meta.input_info[idx].requires_grad and meta.input_info[idx].mutates_data: + # Make sure the primal we pass to autograd.grad() + # sees the tensor before the mutation + return t.clone() + if meta.input_info[idx] and meta.input_info[idx].mutates_metadata: + # Make sure the primal we pass to autograd.grad() + # sees the tensor before the metadata mutation + return t.view(t.shape) + return t + + +def is_with_effects(node: torch.fx.Node) -> bool: + if ( + node.op == "call_function" + and node.target is torch.ops.higher_order.with_effects + ): + return True + elif ( + node.op == "call_function" + and node.target is torch.ops.higher_order.invoke_subgraph + ): + # Check if subgraph has effects by looking in the cache + from torch._guards import InvokeSubgraphCache, TracingContext + + tracing_ctx = TracingContext.try_get() + if tracing_ctx: + invoke_subgraph_cache = tracing_ctx.hop_dispatch_set_cache.get_cache( + torch.ops.higher_order.invoke_subgraph + ) + if invoke_subgraph_cache: + if not isinstance(invoke_subgraph_cache, InvokeSubgraphCache): + raise AssertionError( + f"expected InvokeSubgraphCache, got {type(invoke_subgraph_cache)}" + ) + # pyrefly: ignore[bad-argument-type] + effects = invoke_subgraph_cache.get_effects(node.args[1]) + return effects is not None + return False + + +def unlift_tokens( + fw_module: torch.fx.GraphModule, + fw_metadata: "ViewAndMutationMeta", + aot_config: "AOTConfig", + bw_module: torch.fx.GraphModule | None = None, +) -> None: + # Remove the tokens from the inputs/outputs of the graph since inductor does + # not want these extra inputs/outputs, and replace them with + # _make_token() to create a token, and _sink_tokens() to collect the + # tokens. See Note [Side-Effectful Tokens in AOTAutograd] + # Logic: + # 1. In the case of with_effects: + # Before: + # ``` + # def forward(self, token, arg1_1): + # with_effects = torch.ops.higher_order.with_effects(token, ...) + # getitem = with_effects[0] + # getitem_1 = with_effects[0] + # return (getitem, getitem_1) + # ``` + # + # After: + # ``` + # def forward(self, arg1_1): + # _make_token_default = torch.ops.prims._make_token.default() + # with_effects = torch.ops.higher_order.with_effects(_make_token_default, ...) + # getitem = with_effects[0] + # getitem_1 = with_effects[0] + # _sink_tokens_default = torch.ops.prims._sink_tokens.default([getitem]); + # return (getitem_1,) + # ``` + # + # 2. In the case of an invoke_subgraph node, we will use the + # InvokeSubgraphCache to determine if the subgraph has effects. Then we will + # turn it into a `with_effects` node. This is so that at the toplevel graph, + # the nodes will have the correct with_effects threading. We will apply this + # pass recursively to submodules so the tokens will be removed from the + # subgraph's inputs. + # + # Before: + # ``` + # def forward(self, token, arg1_1): + # repeated_subgraph0 = self.repeated_subgraph0 + # invoke_subgraph = torch.ops.higher_order.invoke_subgraph( + # repeated_subgraph0, 'subgraph_0', token, x, arg1_1) + # getitem = invoke_subgraph[0] + # getitem_1 = invoke_subgraph[1] + # return (getitem, getitem1) + # ``` + # + # After: + # ``` + # def forward(self, arg1_1): + # _make_token_default = torch.ops.prims._make_token.default() + # repeated_subgraph0 = self.repeated_subgraph0 + # with_effects_1 = torch.ops.higher_order.with_effects( + # _make_token_default, torch.ops.higher_order.invoke_subgraph, + # repeated_subgraph0, 'subgraph_0', arg1_1) + # getitem = with_effects_1[0] + # getitem_1 = with_effects_1[1]; with_effects_1 = None + # _sink_tokens_default = torch.ops.prims._sink_tokens.default([getitem]) + # return (getitem_1,) + # ``` + # + # 3. The toplevel module should have the following invariants: + # forward: + # expected_num_erased_inputs == len(fw_metadata.tokens) + # expected_num_erased_outputs == len(fw_metadata.tokens) + # backward: + # expected_num_erased_inputs == fw_metadata.num_backward_tokens + # expected_num_erased_outputs == fw_metadata.num_backward_tokens + num_forward_tokens = len(fw_metadata.tokens) + num_backward_tokens = fw_metadata.num_backward_tokens + + def replace_input_token_with_make_token( + module: torch.fx.GraphModule, node: torch.fx.Node + ) -> None: + with module.graph.inserting_before(node): + new_token_node = module.graph.call_function( + torch.ops.prims._make_token.default, () + ) + new_token_node.meta["val"] = torch.tensor([]) + new_token_node.meta["tensor_meta"] = torch.tensor([]) + node.replace_all_uses_with(new_token_node) + module.graph.erase_node(node) + + def get_output_tokens(node: torch.fx.Node) -> set[torch.fx.Node]: + output_tokens = set() + for user in list(node.users.keys()): + # Check if this is a getitem accessing index 0 (the token) + if ( + user.op == "call_function" + and user.target is operator.getitem + and len(user.args) > 1 + and user.args[1] == 0 + ): + # Check if this getitem is used in an output + for user_user in list(user.users.keys()): + if user_user.op == "output": + output_tokens.add(user) + return output_tokens + + def _unlift_tokens_from_module_helper( + module: torch.fx.GraphModule, + subgraph_str: str, + expected_num_erased: int | None, + ) -> None: + input_token_nodes = set() + output_token_nodes = set() + + for node in module.graph.nodes: + if ( + node.op == "call_function" + and node.target is torch.ops.higher_order.with_effects + ): + if node.args[0].op == "placeholder": + input_token_nodes.add(node.args[0]) + replace_input_token_with_make_token(module, node.args[0]) + + tokens_from_with_effects = get_output_tokens(node) + output_token_nodes = output_token_nodes | tokens_from_with_effects + + elif ( + node.op == "call_function" + and node.target is torch.ops.higher_order.invoke_subgraph + ): + subgraph_node, identifier, *operands = node.args + + # Check if subgraph has effects by looking in the cache + from torch._guards import InvokeSubgraphCache, TracingContext + + effects = None + tracing_ctx = TracingContext.try_get() + if tracing_ctx: + invoke_subgraph_cache = ( + tracing_ctx.hop_dispatch_set_cache.get_cache( + torch.ops.higher_order.invoke_subgraph + ) + ) + if invoke_subgraph_cache: + if not isinstance(invoke_subgraph_cache, InvokeSubgraphCache): + raise AssertionError( + f"expected InvokeSubgraphCache, got {type(invoke_subgraph_cache)}" + ) + effects = invoke_subgraph_cache.get_effects(identifier) + + if effects is not None: + # Wrap invoke_subgraph with with_effects + # Before: invoke_subgraph(subgraph, id, token, *args) -> (token_out, result) + # After: with_effects(token, invoke_subgraph, subgraph, id, *args) -> (token_out, result) + # + # Note: The subgraph itself will be unlifted separately when we iterate + # through named_modules() below. + + num_tokens = len(effects) + if num_tokens != 1: + raise AssertionError( + f"Multiple token subgraph NYI, got {num_tokens} tokens" + ) + token_args = operands[:num_tokens] + non_token_args = operands[num_tokens:] + + # Create with_effects wrapper around invoke_subgraph + # with_effects(token, op, *args) where op is invoke_subgraph + # Pass the subgraph and non-token args to invoke_subgraph + with module.graph.inserting_before(node): + new_node = module.graph.call_function( + torch.ops.higher_order.with_effects, + # pyrefly: ignore [bad-argument-type] + ( + token_args[0], # pyrefly: ignore[bad-argument-type] + torch.ops.higher_order.invoke_subgraph, + subgraph_node, + identifier, + *tuple(non_token_args), + ), + ) + node.replace_all_uses_with(new_node) + new_node.meta = node.meta + module.graph.erase_node(node) + + for token in token_args: + if token.op == "placeholder": + input_token_nodes.add(token) + replace_input_token_with_make_token(module, token) + + # Get output tokens from the new with_effects node + tokens_from_invoke_subgraph = get_output_tokens(new_node) + output_token_nodes = ( + output_token_nodes | tokens_from_invoke_subgraph + ) + + if not output_token_nodes and not input_token_nodes: + return + + output_node = next(reversed(module.graph.find_nodes(op="output"))) + if output_node is None: + raise AssertionError("output node not found in graph") + with module.graph.inserting_before(output_node): + module.graph.call_function( + torch.ops.prims._sink_tokens.default, + (list(output_token_nodes),), + ) + new_out_args = tuple( + [out for out in output_node.args[0] if out not in output_token_nodes] + ) + output_node.args = (new_out_args,) + + if expected_num_erased: + if len(input_token_nodes) != expected_num_erased: + raise AssertionError( + f"{subgraph_str} num_erased_inputs:{len(input_token_nodes)} " + f"{input_token_nodes} != expected {expected_num_erased} \n" + f"{fw_module.print_readable(print_output=False)}" + ) + if len(output_token_nodes) != expected_num_erased: + raise AssertionError( + f"{subgraph_str} num_erased_outs:{len(output_token_nodes)} " + f"{output_token_nodes} != expected {expected_num_erased} \n" + f"{fw_module.print_readable(print_output=False)}" + ) + + module.recompile() + + def unlift_tokens_from_module( + module: torch.fx.GraphModule, subgraph_str: str, expected_num_erased: int + ) -> None: + for name, m in module.named_modules(): + if isinstance(m, torch.fx.GraphModule): + if name == "": + _unlift_tokens_from_module_helper( + m, subgraph_str, expected_num_erased + ) + else: + # Subgraph -- we may or may not have effects applied + _unlift_tokens_from_module_helper(m, f"{subgraph_str}_{name}", None) + + if num_forward_tokens > 0: + if aot_config.enable_log: + from torch._dynamo.utils import lazy_format_graph_code + + aot_graphs_effects_log.debug( + "%s", + lazy_format_graph_code( + "Forward graph before unlifting tokens", + fw_module, + aot_config.aot_id, + include_stride=True, + include_device=True, + colored=True, + ), + ) + unlift_tokens_from_module( + fw_module, + "forward", + num_forward_tokens, + ) + + if bw_module is not None and num_backward_tokens > 0: + if aot_config.enable_log: + from torch._dynamo.utils import lazy_format_graph_code + + aot_graphs_effects_log.debug( + "%s", + lazy_format_graph_code( + "Backward graph before unlifting tokens", + bw_module, + aot_config.aot_id, + include_stride=True, + include_device=True, + colored=True, + ), + ) + unlift_tokens_from_module(bw_module, "backward", num_backward_tokens) + + # This is sad, but we need to update the metadata to get rid of + # the tokens. + fw_metadata.tokens = {} + fw_metadata.num_backward_tokens = 0 + + +def root_module_when_exporting_non_strict( + flat_fn: Callable[..., Any], +) -> torch.nn.Module | None: + # When exporting in non-strict mode, we wrap the root module in a specific pattern. + # See `_aot_export_non_strict` in torch.export._trace.py. + # We look for that wrapping pattern here. + if hasattr(flat_fn, "_orig_mod") and hasattr(flat_fn._orig_mod, "_export_root"): + return flat_fn._orig_mod._export_root + else: + return None + + +def _is_forward_node_with_seq_nr(node: torch.fx.Node) -> bool: + # For now, assume that if nn_module_stack_metadata is populated, this + # node is from the forward. Ignore nodes without `seq_nr`. + # TODO(future): there is likely a less brittle way to do this by walking + # the descendants of graph inputs corresponding to fwd inputs, didn't + # seem obvious at first glance on how to partition graph inputs into + # fwd vs bwd without relying on string names. + return node.meta.get("partitioner_tag") != "is_backward" and "seq_nr" in node.meta + + +def _is_backward_node_with_seq_nr(node: torch.fx.Node) -> bool: + # For now, assume that if nn_module_stack_metadata is not populated, + # this node is from the backward. Ignore nodes without `seq_nr`. + # TODO(future): there is likely a less brittle way to do this, same + # as with the forward. + return node.meta.get("partitioner_tag") == "is_backward" and "seq_nr" in node.meta + + +def _collect_fwd_nodes_from_subgraph( + fx_g: torch.fx.GraphModule, fwd_seq_nr_to_node: dict[str, torch.fx.Node] +) -> None: + """Collect forward nodes from a single subgraph into the global mapping.""" + for node in fx_g.graph.nodes: + if not _is_forward_node_with_seq_nr(node): + continue + seq_nr = node.meta["seq_nr"] + if seq_nr in fwd_seq_nr_to_node: + # If we already saw an op with the current `seq_nr`, that means + # that the current op did not create an autograd node, and there + # is no corresponding backward node, so we skip. + continue + fwd_seq_nr_to_node[seq_nr] = node + + +def _copy_metadata_to_bw_nodes_in_subgraph( + fx_g: torch.fx.GraphModule, fwd_seq_nr_to_node: dict[str, torch.fx.Node] +) -> None: + """Copy metadata from forward nodes to backward nodes in a single subgraph.""" + for node in fx_g.graph.nodes: + annotation_log.debug("node: %s", node.name) + seq_nr = node.meta.get("seq_nr") + annotation_log.debug("seq_nr: %s", seq_nr) + + if not _is_backward_node_with_seq_nr(node): + continue + + # We exclude gradient accumulation nodes from copying tags + if node.meta.get("is_gradient_acc", False): + annotation_log.debug("is_gradient_acc") + continue + + # fwd_node should always exist, but handle non-existence just in case + fwd_node = fwd_seq_nr_to_node.get(node.meta["seq_nr"]) + if fwd_node is not None: + node.meta["fwd_nn_module_stack"] = fwd_node.meta.get("nn_module_stack") + node.meta["fwd_source_fn_stack"] = fwd_node.meta.get("source_fn_stack") + # TODO: better to change to a specific field of custom? + custom = fwd_node.meta.get("custom") + if custom is not None: + node.meta["custom"] = copy.deepcopy(custom) + + +def copy_fwd_metadata_to_bw_nodes(fx_g: torch.fx.GraphModule) -> None: + """ + Input: `fx_g` which contains the joint fwd+bwd FX graph created by + aot_autograd. + + This function walks the graph and copies over metadata from forward nodes + to backward nodes, using the `seq_nr` field as a one-to-many mapping + from forward node to backward node. This metadata is useful for performance + profiling and debugging. + + This function supports matching forward and backward nodes across different + subgraphs (e.g., in recursive submodules from HOPs), enabling backward nodes + in any submodule to match forward nodes in any submodule. + """ + + # Build a global mapping of seq_nr to forward nodes across all subgraphs + fwd_seq_nr_to_node: dict[str, torch.fx.Node] = {} + + # First pass: collect all forward nodes from all subgraphs + for submod in fx_g.modules(): + if isinstance(submod, torch.fx.GraphModule): + _collect_fwd_nodes_from_subgraph(submod, fwd_seq_nr_to_node) + + if annotation_log.isEnabledFor(logging.DEBUG): + for k, v in fwd_seq_nr_to_node.items(): + annotation_log.debug("forward:: key: %s, value: %s", k, v) + + # Second pass: copy metadata to backward nodes in all subgraphs + # using the global forward mapping + for submod in fx_g.modules(): + if isinstance(submod, torch.fx.GraphModule): + _copy_metadata_to_bw_nodes_in_subgraph(submod, fwd_seq_nr_to_node) + + +def register_buffer_assignment_hook( + mod: torch.nn.Module, assigned_buffers: dict[str, str] +) -> Any: + """ + Register a hook that intercepts buffer assignments. + This is used to detect when a buffer is assigned to, and then we can + map that buffer to the corresponding proxy node in the graph. + """ + + def _map_assigned_buffer_to_proxy( + _mod: torch.nn.Module, name: str, buffer: Any + ) -> Any: + # We intercept buffer assignments on the root module through this hook. + if _mod._buffers is mod._buffers: + # either buffer is a functional tensor, which wraps a fake tensor + if isinstance(buffer, FunctionalTensor): + buffer = buffer.from_functional() + # or buffer is a fake tensor + if not isinstance(buffer, FakeTensor): + raise AssertionError(f"expected FakeTensor, got {type(buffer)}") + # The fake tensor in turn is associated with a proxy node. + proxy_mode = torch.fx.experimental.proxy_tensor.get_proxy_mode() + if proxy_mode is None: + raise AssertionError("proxy_mode must not be None") + proxy = torch.fx.experimental.proxy_tensor.get_proxy_slot( + buffer, proxy_mode.tracer + ).proxy.node + # We map the assigned buffer to this proxy node. + assigned_buffers[name] = proxy.name + return buffer + + return torch.nn.modules.module.register_module_buffer_registration_hook( + _map_assigned_buffer_to_proxy + ) + + +def contain_metadata_mutation_ops(module: torch.fx.GraphModule) -> bool: + """ + Checks if the module contains any metadata mutation ops. + """ + for node in module.graph.nodes: + if ( + node.op == "call_function" + and hasattr(node.target, "tags") + and torch.Tag.inplace_view in node.target.tags + ): + return True + return False + + +def get_cuda_generator_meta_val(device_idx: int) -> Any: + """ + Get a generator value to use as a meta val + + newly cloned generator will not contain tensors. it is only Generators that are + registered to a CUDAGraph that contain tensors. since this does not contain Tensor + it is fine to use in the meta. + """ + return torch.cuda.default_generators[device_idx].clone_state() + + +def top_saved_tensors_hooks() -> Any: + return torch._C._autograd._top_saved_tensors_default_hooks(True) + + +def saved_tensors_hooks_are_inlineable(hooks: Any) -> bool: + if not hooks: + return False + pack, unpack = hooks + return isinstance(pack, torch.fx.GraphModule) and isinstance( + unpack, torch.fx.GraphModule + ) + + +_P = ParamSpec("_P") +_T = TypeVar("_T") +_S = TypeVar("_S") + + +def without_output_descs(f: Callable[_P, tuple[_T, _S]]) -> Callable[_P, _T]: + @wraps(f) + @simple_wraps(f) + def inner(*args: _P.args, **kwargs: _P.kwargs) -> _T: + return f(*args, **kwargs)[0] + + return inner + + +_P2 = ParamSpec("_P2") +_R = TypeVar("_R") +_R2 = TypeVar("_R2") + + +def simple_wraps( + f: Callable[_P, _R], +) -> Callable[[Callable[_P2, _R2]], Callable[_P2, _R2]]: + # NB: omit ('__module__', '__name__', '__qualname__') for ease of + # debugging + return wraps(f, assigned=("__doc__", "__annotations__", "__type_params__")) + + +_Ts = TypeVarTuple("_Ts") + + +def call_and_expect_output_descs( + fn: Callable[[*_Ts], tuple[Any, Any]], args: tuple[Unpack[_Ts]] +) -> tuple[Any, Any]: + from .descriptors import AOTOutput + + outs_pair = fn(*args) + if not (isinstance(outs_pair, tuple) and len(outs_pair) == 2): + raise AssertionError( + f"expected tuple of length 2, got {type(outs_pair)} with value {outs_pair}" + ) + outs, outs_descs = outs_pair + # The Tensor tests protects against the test when there are no outputs + out_vals, out_spec = pytree.tree_flatten(outs) + out_desc_vals, out_desc_spec = pytree.tree_flatten(outs_descs) + if out_spec != out_desc_spec: + raise AssertionError( + f"output spec mismatch: {fn_wrappers(fn)}, outs={outs}, outs_descs={outs_descs}, " + f"out_spec={out_spec}, out_desc_spec={out_desc_spec}" + ) + if any(isinstance(x, AOTOutput) for x in out_vals): + raise AssertionError( + f"unexpected AOTOutput in out_vals: {fn_wrappers(fn)}, outs={outs}, " + f"outs_descs={outs_descs}, out_vals={out_vals}" + ) + if not all( + isinstance(d, AOTOutput) + for (x, d) in zip(out_vals, out_desc_vals) + if isinstance(x, (torch.Tensor, torch.SymInt)) or type(x) is int + ): + raise AssertionError( + f"expected all descriptors to be AOTOutput: {fn_wrappers(fn)}, outs={outs}, " + f"outs_descs={outs_descs}, out_vals={out_vals}, out_desc_vals={out_desc_vals}" + ) + return outs_pair + + +def fn_wrappers(fn: Callable[..., Any]) -> list[Callable[..., Any]]: + fns = [fn] + f = fn + while hasattr(f, "__wrapped__"): + f = f.__wrapped__ + fns.append(f) + return fns + + +def _is_primal(node: torch.fx.Node) -> bool: + return ( + node.op == "placeholder" + and "tangents" not in str(node.target) + and not _is_bwd_seed_offset(node) + and not _is_fwd_seed_offset(node) + ) + + +def _is_tangent(node: torch.fx.Node) -> bool: + return node.op == "placeholder" and "tangents" in str(node.target) + + +def _is_bwd_seed_offset(node: torch.fx.Node) -> bool: + return node.op == "placeholder" and ( + "bwd_seed" in str(node.target) or "bwd_base_offset" in str(node.target) + ) + + +def _is_fwd_seed_offset(node: torch.fx.Node) -> bool: + return node.op == "placeholder" and ( + "fwd_seed" in str(node.target) or "fwd_base_offset" in str(node.target) + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/aot_autograd.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/aot_autograd.py new file mode 100644 index 0000000000000000000000000000000000000000..278bbf812cbb05a5c84c52b7eb375fc7a34c74d7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/aot_autograd.py @@ -0,0 +1,1952 @@ +from __future__ import annotations + +import contextlib +import itertools +import time +from contextlib import nullcontext +from functools import wraps +from typing import Any, Literal, TYPE_CHECKING +from typing_extensions import ParamSpec, TypeVar +from unittest.mock import patch + +import torch +import torch._dynamo.logging +import torch.nn as nn +import torch.utils._pytree as pytree +import torch.utils.dlpack +from torch import Tensor +from torch._decomp.decompositions_for_rng import PhiloxStateTracker, rng_decompositions +from torch._dispatch.python import enable_python_dispatcher +from torch._dynamo import compiled_autograd +from torch._dynamo.utils import ( + CompileEventLogger, + dynamo_timed, + preserve_rng_state, + set_feature_use, +) +from torch._guards import detect_fake_mode +from torch._inductor.codecache import resolve_pre_grad_pass_timing + +# Runtime annotation consumers still resolve BoxedBool from module globals. +from torch._subclasses import FakeTensor, FakeTensorMode +from torch.export._tree_utils import reorder_kwargs +from torch.fx.experimental.proxy_tensor import make_fx + +from . import config +from ._aot_autograd import autograd_cache +from ._aot_autograd.autograd_cache import ( # noqa: F401 + AOTAutogradCache, + should_use_local_autograd_cache, + should_use_remote_autograd_cache, +) +from ._aot_autograd.collect_metadata_analysis import ( # noqa: F401 + run_functionalized_fw_and_collect_metadata, +) +from ._aot_autograd.descriptors import ( + AOTInput, + BufferAOTInput, + DifferentiableAOTInput, + ParamAOTInput, + PlainAOTInput, +) +from ._aot_autograd.frontend_utils import ( + _detect_attribute_assignment, + _try_get_metadata_from_dynamo, + construct_fake_mode, + process_inputs, +) +from ._aot_autograd.functional_utils import ( # noqa: F401 + _check_if_mutation_can_be_in_graph, + are_all_mutations_hidden_from_autograd, + are_all_mutations_under_no_grad_or_inference_mode, + assert_functional_graph, + from_fun, + gen_alias_from_base, + has_data_mutation, + has_metadata_mutation, + is_fun, + sync_functional_tensor, + to_fun, +) +from ._aot_autograd.graph_capture_wrappers import ( # noqa: F401 + aot_dispatch_subclass, + create_functional_call, + create_functionalized_fn, + create_functionalized_rng_ops_wrapper, + create_joint, + fn_input_mutations_to_outputs, + fn_prepped_for_autograd, +) +from ._aot_autograd.graph_compile import ( # noqa: F401 + aot_stage1_graph_capture, + aot_stage2_compile, + aot_stage2_export, +) +from ._aot_autograd.input_output_analysis import ( # noqa: F401 + compute_overlapping_inputs, + create_graph_signature, + create_synthetic_base_metadata, + remove_dupe_metadata, +) +from ._aot_autograd.logging_utils import ( # noqa: F401 + callback_set, + describe_input, + format_guard_bug_msg, + get_aot_compilation_context, + get_aot_graph_name, + get_graph_being_compiled, + graph_being_compiled, + model_name, + nth_graph, + set_model_name, + setup_stacktrace_preservation_hooks, + track_graph_compiling, +) +from ._aot_autograd.runtime_wrappers import ( # noqa: F401 + AOTDedupeWrapper, + AOTSyntheticBaseWrapper, + SerializableCompiledFunction, +) +from ._aot_autograd.schemas import ( # noqa: F401 + AOTConfig, + AOTDispatchCompiler, + AOTGraphCapture, + AOTState, + BackwardSignature, + FakifiedFlatArgs, + FQN, + GraphInputName, + GraphOutputName, + GraphSignature, + InputAliasInfo, + JointWithDescriptors, + MutationType, + OpaqueMeta, + OutputAliasInfo, + OutputType, + SerializableAOTDispatchCompiler, + SubclassCreationMeta, + SubclassMeta, + TensorAlias, + ViewAndMutationMeta, +) +from ._aot_autograd.subclass_utils import ( # noqa: F401 + requires_subclass_dispatch, + unwrap_tensor_subclasses, + unwrap_tensor_subclasses_with_indices_to_original, + wrap_tensor_subclasses, + wrap_tensor_subclasses_maybe_joint, +) +from ._aot_autograd.utils import ( # noqa: F401 + _get_autocast_states, + call_func_at_runtime_with_args, + create_tree_flattened_fn, + KNOWN_TYPES, + make_boxed_compiler, + make_boxed_func, + maybe_to_fresh_input, + normalize_as_list, + partial_flatten_asdict, + PytreeThunk, + root_module_when_exporting_non_strict, + simple_wraps, + strict_zip, +) +from .partitioners import default_partition + + +if TYPE_CHECKING: + from collections.abc import Callable, Iterable, Sequence + + from torch._inductor.compile_fx import CompilerConfigExtra + from torch._inductor.output_code import OutputCode + from torch._inductor.utils import InputType + from torch._ops import OpOverload + from torch.fx.experimental.symbolic_shapes import ShapeEnv + +_P = ParamSpec("_P") +_R = TypeVar("_R") + +zip = strict_zip + +# This global counter increments every time we compile a graph with +# AOTAutograd. You can use this to correlate runtime error messages +# with compile time (e.g., if you get an error at runtime saying +# compiled graph 3 failed, you can set a breakpoint at compile time +# for this graph number to investigate further at compile time.) +# +# NB: this is different from get_aot_compilation_context, which tracks +# each underlying graph that is compiled. In contrast, AOT_COUNTER +# corresponds to top-level invocations of aot_module/aot_function; +# one counter is allocated per entire compiled block (but this block +# may involve compiling multiple subgraphs; e.g., for forwards/backwards) +AOT_COUNTER = itertools.count() + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# +# AOT Autograd contains a pretty non-trivial amount of logic to handle edge cases around aliasing and mutation +# that are external to the graph (they show up as side effects in some way when you run the graph). +# +# Take a look at `test_aotdispatch.py TestAOTAutograd.test_input_mutation*` tests for some examples functions +# and what they're compiled graphs looks like. +# Below is a very long comment detailing several edge cases, and showing how AOT Autograd handles them. +# +# Note [AOT Autograd: input data mutations] +# +# If we compile a function that mutates inputs, then those input mutations are real side effects +# that a user expects to see after running the compiled graph. +# However, the graph that we want to send to a backend needs to be *entirely* functional. +# The way we reconcile this difference is that we remove the mutations completely from the graph that we compile +# but we update the graph to return (updated_inputs, user_outputs). +# In the epilogue that runs after the compiled graph is executed, we copy the updated inputs back to the originals. +# +# Example: original user code: +# def f(x): +# x.mul_(2) +# out = x.mul(3) +# return out +# +# After AOT Autograd compiles, we end up with a: +# (a) compiled graph +# (b) autograd.Function.forward() method, that executes the compiled graph +# (c) wrapper function, that calls the autograd.Function.forward() and performs the epilogue +# +# The output of (a, b, c) are all written below. +# +# def compiled_forward_graph(x): +# x_updated = x.mul(2) +# out = x_updated.mul(3) +# return x_updated, out +# +# # x_updated gets a gradient in the compiled backward +# def compiled_backward_graph(grad_x_updated, grad_out): +# grad_x = ... +# return grad_x +# +# def autograd.Function.forward(x): +# x_updated, out = compiled_forward_graph(x) +# return x_updated, out +# +# def compiled_wrapper(x): +# x_updated, out = autograd.Function.apply(x) +# x.copy_(x_updated) +# return out +# +# Another important thing to note is that updated inputs (due to data mutations) *do* participate +# in the compiled backward graph! Since the compiled forward graph gets N extra outputs +# (due to updated inputs showing up as graph outputs), +# The compiled backward gets an additional N inputs. +# That way, during the x.copy_(x_updated) bit in the epilogue, gradients will flow from the updated input +# back to the original input. + + +# Note [AOT Autograd: input metadata mutations] +# +# For the same reason as input mutations, we also don't put input metadata mutations in the graph. +# Instead, we return the updated version of the input (a view), and mutate the input's metadata outside of the graph +# +# Example: original user code: +# def f(x): +# x.t_() +# out = x.mul(3) +# return out +# +# AOT Autograd output (compiled graph, autograd.Function.forward(), wrapper function): +# def compiled_forward_graph(x): +# x_updated = x.t() +# out = x_updated.mul(3) +# return x_updated, out +# +# # x_updated does *not* get a gradient in the compiled backward +# def compiled_backward_graph(grad_out): +# grad_x = ... +# return grad_x +# +# def autograd.Function.forward(x): +# x_updated, out = compiled_forward_graph(x) +# return x_updated, out +# +# def compiled_wrapper(x): +# x_updated, out = autograd.Function.apply(x) +# x.as_strided_(x_updated) +# return out + + +# Note [AOT Autograd: outputs aliasing inputs or intermediates!] +# +# AOT Autograd needs special handling for outputs that alias graph inputs or intermediates! +# Why? +# (1) autograd.Function.forward() has a limitation, where views that returned in the forward cannot later be mutated. +# (2) views don't need to be compiled in the graph anyway - it's cheap to generate them outside of the compiled graph, +# in an epilogue. +# For outputs that alias inputs, we do the following: +# (a) *still* return the aliased output as a graph output +# (b) In the AOT Autograd wrapper/epilogue, we don't return that aliased output. Instead, we use it to regenerate the output. +# +# For outputs that alias *intermediates*, we do the following: +# (a) Return the output in the compiled forward, **and** return it's ._base (a graph intermediates) as an output in the forward +# (b) Use (output, graph_intermediate) to regenerate the alias, and return that to the user (instead of the compiled fw output). +# You might wonder why we return the aliased output directly in the graph (and making the graph compute it), +# only to not return it and instead generate a fresh alias off of the intermediate, +# instead of (say) just storing metadata about the size/stride of the output somewhere to generate the alias. There are two reasons: +# (1) Getting the actual alias tensor allows us to use view-replay to generate the alias, instead of an as_strided() call +# (2) Inductor (and other backends) are free to change the memory format of graph outputs, if it results in better performance. +# This can result in problems if a user later tries to .view() that output expecting it to have one set of strides, +# when it has a different set of strides. +# By including the view op directly in the graph, inductor takes that into account when deciding what memory format +# the graph intermediate should be. +# +# Another important thing to note is how our traced backward() graph handles aliases. +# (this applies to outputs aliasing inputs, outputs aliasing intermediates, +# *and* updated inputs returned in the compiled forward due to metadata-only mutations). +# Any outputs that alias (either inputs or intermediates) do NOT participate in the compiled backward graph +# It would be wasteful to include them in the compiled backward(), because we regenerate them eagerly +# at the end of the forward. +# +# Example: original user code: +# def f(x): +# out1 = x.t() +# intermediate = x.mul(2) +# out2 = intermediate.view(-1) +# return out1, out2 +# +# AOT Autograd output (compiled graph, autograd.Function.forward(), wrapper function): +# def compiled_forward_graph(x): +# out1 = x.t() +# intermediate = x.mul(2) +# out2 = intermediate.view(-1) +# # the compiled graph also returns the intermediate +# return out1, out2, intermediate +# +# # intermediate gets a gradient in the compiled backward. +# # both output aliases (out1 and out2) do not. +# def compiled_backward_graph(grad_intermediate): +# grad_x = ... +# return grad_x +# +# def autograd.Function.forward(x): +# out1, out2, intermediate = compiled_forward_graph(x) +# return out1, out2, intermediate +# +# def compiled_wrapper(x): +# out1, out2, intermediate = autograd.Function.apply(x) +# # regenerate out1 from the input +# out1_regenerated = out1._view_func(x) +# # regenerate out1 from the intermediate +# out2_regenerated = out2._view_func(intermediate) +# return out1_regenerated, out2_regenerated + + +# Note [AOT Autograd: mutations to inputs that alias other inputs] +# +# Another edge case that is (only partially) handled today is when an input is mutated, but itself aliases another input. +# AOT Autograd needs to **ensure** that functionalization knows that the two inputs are aliased to each other. +# That way, when the aliased input is accessed later in the graph, functionalization knows to "update" the alias +# given the mutation that occurred. +# +# This is handled by updating the calling convention: we create a "synthetic base" that becomes a new input +# in the compiled function, and we regenerate the original (aliased) inputs directly off of the base +# inside of the compiled function. +# +# This logic is fully encapsulated in aot_wrapper_synthetic_base() +# +# Example: original user code: +# def f(x, x_view): +# x.mul_(2) +# out = x * x_view +# return out +# f(x, x.view(-1)) +# +# AOT Autograd output (compiled graph, autograd.Function.forward(), wrapper function): +# def compiled_forward_graph(base) +# x = generate_x(base) +# x_view = generate_x_view(base) +# x_updated = x.mul(2) +# x_view_updated = x_updated.view(-1) +# out = x_updated * x_view_updated +# return x_updated, out +# +# # The calling convention change from (aliases) -> (base) happens +# # *outside* of the autograd.Function.forward(). +# # That means the forward() only has 1 input (base), +# # and the backward() only has 1 output (grad_base) +# def compiled_backward_graph(grad_out): +# grad_base = ... +# return grad_base +# +# def autograd.Function.forward(base): +# x_updated, out = compiled_forward_graph(base) +# return x_updated, out +# +# # The compiled wrapper is where we create synthetic bases. +# # The info on which inputs are mutated is also tracked *before* synthetic base creation. +# def compiled_wrapper(x, x_view): +# base = merge_view_inputs(x, x_view) +# x_updated, out = autograd.Function.apply(base) +# # x and x_view are aliased in eager mode, so this mutation to x will automatically affect x_view. +# x.copy_(x_updated) +# return out + + +# Note [AOT Autograd: Views to avoid tangents aliasing inputs] +# +# We view every forward output when creating out tangent tensors to handle the problematic +# case in which a subclass does extra aliasing between graph outputs/inputs in a way that +# is not visible above the subclass. +# +# Ordinarily, when constructing the joint function that we want to trace in AOTAutograd, +# we're guaranteed that the tangent tensors that we pass +# into the joint are distinct tensors from the primals. This is because when +# decide which forward outputs to create tangents for, we only create tangents +# for forward outputs that are not aliases of inputs (See Note +# [AOT Autograd: outputs aliasing inputs or intermediates!]). +# +# However, when wrapper tensor subclasses enter the picture, it is possible +# to have an output of the forward that is a subclass that is not an +# input / alias of an input, but one of its inner tensors is an alias! +# NestedTensor is an example: Performing an out-of-place pointwise op on a +# NestedTensor constructs a fresh NestedTensor that holds onto the input's +# offsets tensor directly. +# +# Having tangent tensors that are the same as the (primal) forward inputs, +# can cause problems during tracing as make_fx() will specialize on our +# duplicate inputs: If we passed in the same tensor for primals_1 and +# tangents_1 during tracing, make_fx() will happily sub out all usages of +# tangents_1 with primals_1 in the graph, which is not what we want. +# +# To work around this, we view every forward output when creating out tangent +# tensors so that tangents can never be the same as forward inputs even if +# forward inputs alias forward outputs. + +# Note [Side-Effectful Tokens in AOTAutograd] +# +# We allow some some side-effectful operators in +# the post-AOTAutograd (functional) graph, such as prints and torchbind operations. +# To ensure that these side-effects are compatible to future graph passes that +# assume that the graph is functional, we will thread "effect tokens" to show +# data dependence between these side-effectful operators. Practically speaking, +# effect tokens are just dummy values (torch.tensor([])). The graph would look +# like the following: +# +# def gm(self, token0, reader): +# token1, frame = with_token(ordered_effect_op, (reader,), token0) +# frame = frame * 2 +# token2, frame2 = with_token(ordered_effect_op, (reader,), token1) +# frame2 = frame2 * 2 +# return token2, frame, frame2 +# +# We will pass the token as an input to the graph, thread it through +# side-effectful operators using the `with_effects` high order operator, and then +# return the updated token as an output. +# So the signature of the graph input would look something like +# (*tokens, *params_buffers, *user_inputs), and the signature of the graph +# output would look something like (*tokens, *outputs). +# +# However, Inductor does not want the concept of tokens in the final generated +# code's input and output. Since changing the graph signature inside of inductor +# is difficult, after generating the forward graph, we will run a pass to +# remove the tokens from the inputgenerate the following graph for Inductor, where +# the tokens are created and sunk within the graph, rather than as inputs and +# outputs: +# +# def gm(self, reader): +# token0 = torch.ops.prims._make_token() +# token1, frame = with_token(ordered_effect_op, (reader,), token0) +# frame = frame * 2 +# token2, frame2 = with_token(ordered_effect_op, (reader,), token1) +# frame2 = frame2 * 2 +# sink_token = torch.ops.prims._sink_tokens([token2]) +# return frame, frame2 + +# +# +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +aot_autograd_decompositions: dict[OpOverload, Callable[..., Any]] = {} + + +def create_aot_state( + stack: contextlib.ExitStack, + flat_fn: Callable[_P, _R], + fake_flat_args: FakifiedFlatArgs, + flat_args_descs: list[AOTInput], + aot_config: AOTConfig, + fake_mode: FakeTensorMode, + shape_env: ShapeEnv | None, +) -> AOTState: + """ + Traces the forward and backward graphs of the attr:`flat_fn` to generate a + joint graph. The joint graph is an Fx graph with Aten ops. Please refer to + the tracing mechanism to understand the graph capturing details. + + The joint graph is then passed through attr:`partition_fn` to isolate the + forward and backward portions, which are then respectively compiled via the + provided attr:`fw_compiler` and attr:`bw_compiler`. + + The resulting compiled forward and backward graphs are then wrapped up in a + ``torch.autograd.Function`` object. + + The calling convention here is that the first aot_config.num_params_buffers + inputs in flat_args are parameters and buffers, and the rest are inputs. + + We use this to assume that parameters/buffer's shapes don't change. + """ + + # Old name for now to avoid messing with stats. Also, note this is pushed + # on the stack, so it extends BEYOND this function + stack.enter_context( + dynamo_timed("create_aot_dispatcher_function", log_pt2_compile_event=True) + ) + + # This is the main entry point. + # TODO: Chillee argues that dynamo itself should pass in fake tensors to + # the list of arguments when compiling; at the moment we do not do this + + if aot_config.decompositions is None: + aot_config.decompositions = {} + + aot_config.decompositions = { + **aot_autograd_decompositions, + **aot_config.decompositions, + } + + if config.functionalize_rng_ops: + # Update the decompositions with functionalized random decompositions + aot_config.decompositions = { # type: ignore[assignment] + **rng_decompositions, + **aot_config.decompositions, + } + + # Check flat_args to see if they're already fake. If so, use that fake + # mode instead. + + python_dispatcher_mode = ( + enable_python_dispatcher() if shape_env is not None else nullcontext() + ) + + # See NOTE: [Deferring tensor pack/unpack hooks until runtime] + # If any saved tensor hooks are active, we **don't** want to trace them. + # Instead, we'll let them run at runtime, around the custom autograd.Function + # that we generate in torch.compile. + stack.enter_context(torch.autograd.set_multithreading_enabled(False)) + stack.enter_context(preserve_rng_state()) + stack.enter_context(fake_mode) + stack.enter_context(python_dispatcher_mode) + stack.enter_context(PhiloxStateTracker()) + stack.enter_context( + torch._dynamo.utils._disable_saved_tensors_hooks_during_tracing() + ) + + from torch._library.fake_class_registry import FakeScriptObject, maybe_to_fake_obj + from torch._library.opaque_object import is_opaque_type + + # Tracing may mutate the states the fake script object, + # so we need to duplicate the fake script objects so that subsequent tracing + # won't be affected. + def _dup_fake_script_obj(fake_flat_args: FakifiedFlatArgs) -> list[Any]: + return [ + maybe_to_fake_obj(detect_fake_mode(fake_flat_args), arg.real_obj) + if isinstance(arg, FakeScriptObject) or is_opaque_type(type(arg)) + else arg + for arg in fake_flat_args + ] + + needs_autograd = any( + x.requires_grad for x in fake_flat_args if isinstance(x, Tensor) + ) + + with enable_python_dispatcher(): + # Patch set_rng_state as set_rng_state with fake tensors is + # nonsensical. This does not affect the collection of metadata. + with patch("torch.cuda.set_rng_state", lambda *args: None): + mod = root_module_when_exporting_non_strict(flat_fn) + if mod is not None: + ctx = _detect_attribute_assignment(mod) + else: + ctx = nullcontext() + + if torch._functorch.config.fake_tensor_propagate_real_tensors: + # Running dynamo_timed causes fake tensor issues when + # propagate real tensor is switched on. + dynamo_timed_ctx = nullcontext() + else: + dynamo_timed_ctx = dynamo_timed( + "aot_collect_metadata", log_pt2_compile_event=True + ) + + with dynamo_timed_ctx, ctx: + fw_metadata = run_functionalized_fw_and_collect_metadata( + flat_fn, + flat_args_descs=flat_args_descs, + static_input_indices=aot_config.static_input_indices, + keep_input_mutations=aot_config.keep_inference_input_mutations, + pre_dispatch=aot_config.pre_dispatch, + )(*_dup_fake_script_obj(fake_flat_args)) + + req_subclass_dispatch = requires_subclass_dispatch( + fake_flat_args, fw_metadata + ) + CompileEventLogger.try_add_pt2_compile( + "backend_compile", requires_subclass_dispatch=req_subclass_dispatch + ) + + output_and_mutation_safe = not any( + x.requires_grad + # view-type operations preserve requires_grad even in no_grad. + # Do not count aliases of inputs with requires_grad as reason to make a training graph, + # as AOTAutograd will perform view-replay to regenerate the view outputs at runtime, + # setting their grad_fn properly. + and not ( + x.output_type in (OutputType.alias_of_input, OutputType.is_input) + and x.base_idx is not None + and fw_metadata.input_info[x.base_idx].requires_grad + ) + for x in fw_metadata.output_info + ) and not any( + x.requires_grad + and x.mutates_data + and not x.mutations_under_no_grad_or_inference_mode + and not x.mutations_hidden_from_autograd + for x in fw_metadata.input_info + ) + + if needs_autograd and output_and_mutation_safe: + # We realized that none of the outputs require grad, + # and none of the inputs that require grad are mutated. + # so we actually have an inference graph. + needs_autograd = False + + if fw_metadata.num_intermediate_bases > 0: + if req_subclass_dispatch: + raise AssertionError(f"""\ +torch.compile is currently being used with tensor subclass inputs. +We are attempting to a compile a graph with two graph outputs +that alias one another, specifically output indices: + + {[i for i, x in enumerate(fw_metadata.output_info) if x.output_type == OutputType.alias_of_intermediate]} + +ANY output aliasing (even for regular tensors) is currently unsupported if +there are any subclass outputs. If you run into this, please file a github +issue""") + + if aot_config.is_export: + # aot_export: ban input metadata mutations for now to keep shared code paths simpler. + # Keeping .resize_() in the graph will require some work + # Allowing it but keeping the graph functional will require some calling convention changes. + if len([x for x in fw_metadata.input_info if x.mutates_metadata]) != 0: + raise RuntimeError( + f"""\ +Found an input that received a metadata mutation, through e.g. a call to `.resize_()` or `.transpose_()`. +This is currently banned in the aot_export workflow. If you need this functionality, please file a github issue. + +fw_metadata={str(fw_metadata)}""" + ) + # In export, banning data mutations on inputs that require grad for now. + # This should be rare, and is tricky to get right. When we trace the backward, + # we currently trace with autograd.grad instead of .backward(), which makes it difficult + # to ensure that we run autograd all the way through the input **before** it saw the mutation. + if ( + len( + [ + x + for x in fw_metadata.input_info + if x.requires_grad and x.mutates_data + ] + ) + != 0 + and aot_config.export_trace_joint + ): + raise RuntimeError( + f"""\ +Found a graph input that requires gradients, and received a mutation. +This is currently banned in the aot_export workflow. If you need this functionality, please file a github issue. + +fw_metadata={str(fw_metadata)}""" + ) + if req_subclass_dispatch: + raise RuntimeError( + """\ +aot_export is not currently supported with traceable tensor subclass. +If you need this feature, please comment on """ + ) + + # Need to decide on a strategy for functionalized RNG: toggling via global config seems bad, + # and turning it on will require a non-trivial calling convention change for any export runtime. + if config.functionalize_rng_ops: + raise RuntimeError( + """\ +Functionalized RNG is not currently supported in the aot_export workflow. Please file a github issue, +or otherwise set torch._functorch.config.functionalize_rng_ops = False.""" + ) + + return AOTState( + needs_autograd=needs_autograd, + flat_args=_dup_fake_script_obj(fake_flat_args), + flat_args_descs=flat_args_descs, + fw_metadata=fw_metadata, + # Packaging this just for later use + aot_config=aot_config, + stack=stack, + fake_mode=fake_mode, + ) + + +def aot_function( + fn: Callable[_P, _R], + fw_compiler: AOTDispatchCompiler, + bw_compiler: AOTDispatchCompiler | None = None, + partition_fn: Callable[..., Any] = default_partition, + decompositions: dict[OpOverload, Callable[..., Any]] | None = None, + num_params_buffers: int = 0, + keep_inference_input_mutations: bool = False, + inference_compiler: AOTDispatchCompiler | None = None, + *, + # Whether or not to trace with dynamic shapes + dynamic: bool = False, + enable_log: bool = True, + disable_functionalization: bool = False, + _disable_torch_fn_metadata_mode: bool = False, +) -> Callable[_P, Any]: + """ + Traces the forward and backward graph of :attr:`fn` using torch dispatch + mechanism, and then compiles the generated forward and backward graphs + through :attr:`fw_compiler` and :attr:`bw_compiler`. + + :func:`aot_function` traces the forward and backward graph ahead of time, + and generates a joint forward and backward graph. :attr:`partition_fn` is + then used to separate out forward and backward graphs. The partitioner + function can be used to perform optimizations such as recomputation. One can + set `decompositions` dictionary to decompose the operators into a sequence + of core or simpler operators supported by the backend compilers. + + .. warning:: + This API is experimental and likely to change. + + Args: + fn (Callable): A Python function that takes one or more arguments. Must + return one or more Tensors. + fw_compiler (Callable): A Python function that accepts an Fx graph with + Aten ops and input args, and returns a Callable that semantically is + equivalent to the input Fx graph. + bw_compiler (Optional[Callable]): A Python function that accepts an + Fx graph with Aten ops and input args, and returns a Callable that + semantically is equivalent to the input Fx graph. Default: None + (when None, it defaults to the :attr:`fw_compiler`) + partition_fn (Callable): A Python function that takes a joint forward + and backward graph, and partitions it into separate forward and + backward graphs. + decompositions (Dict): A dictionary to define the decomposition of + larger Aten ops into simpler or core Aten ops. + inference_compiler (Optional[Callable]): A Python function that accepts an + Fx graph with Aten ops and input args, and returns a Callable that + semantically is equivalent to the input Fx graph. inference_compiler is invoked + if no autograd is needed. Default: None + (when None, it defaults to the :attr:`fw_compiler`) + Returns: + Returns a ``Callable`` that retains the eager behavior of the original + :attr:`fn`, but with forward and backward graph compiled via + :attr:`fw_compile` and :attr:`bw_compile`. + + A simple example usage of :func:`aot_function` is as follows. This example + will print the forward and backward graphs of the function ``fn`` + + >>> fn = lambda x: x.sin().cos() + >>> def print_compile_fn(fx_module, args): + >>> print(fx_module) + >>> return fx_module + >>> aot_fn = aot_function(fn, print_compile_fn) + >>> x = torch.randn(4, 5, requires_grad=True) + >>> aot_fn(x) + """ + + aot_config = AOTConfig( + fw_compiler=None, + bw_compiler=None, + inference_compiler=None, + partition_fn=None, + decompositions=decompositions, + num_params_buffers=num_params_buffers, + aot_id=next(AOT_COUNTER), + keep_inference_input_mutations=keep_inference_input_mutations, + dynamic_shapes=dynamic, + aot_autograd_arg_pos_to_source=None, + is_export=False, + no_tangents=False, + enable_log=enable_log, + disable_functionalization=disable_functionalization, + _disable_torch_fn_metadata_mode=_disable_torch_fn_metadata_mode, + ) + cached_res = None + + @wraps(fn) + def returned_function(*args: _P.args, **kwargs: _P.kwargs) -> Any: + nonlocal cached_res + # Now flatten the tensor args + flat_args = pytree.arg_tree_leaves(*args, **kwargs) + + # Compile the function and save it in the cache + if cached_res is None: + flat_fn, out_spec = create_tree_flattened_fn(fn, args, kwargs) + (fake_mode, shape_env) = construct_fake_mode(flat_args, aot_config) + fake_flat_args: FakifiedFlatArgs + fake_flat_args, act_input_indices = process_inputs( + flat_args, aot_config, fake_mode, shape_env + ) + # TODO: We actually could use the pytree path to make better descs. + # Also, the descs here are bad if you do aot_module. + fake_flat_args_descs: list[AOTInput] = [ + PlainAOTInput(i) for i in range(len(fake_flat_args)) + ] + with contextlib.ExitStack() as stack: + aot_state = create_aot_state( + stack, + flat_fn, + fake_flat_args, + fake_flat_args_descs, + aot_config, + fake_mode, + shape_env, + ) + aot_state.fw_metadata.act_input_indices = act_input_indices + aot_graph_capture = aot_stage1_graph_capture(aot_state, flat_fn) + compiled_fn, _ = aot_stage2_compile( + aot_state, + aot_graph_capture, + partition_fn, + fw_compiler, + bw_compiler, + inference_compiler, + ) + cached_res = (compiled_fn, out_spec) + + cached_fn, out_spec = cached_res + out = cached_fn(flat_args) + return out_spec.unflatten(out) + + return returned_function + + +def aot_module(mod: nn.Module, *args: Any, **kwargs: Any) -> nn.Module: + """ + Traces the forward and backward graph of :attr:`mod` using torch dispatch + tracing mechanism. It is wrapper function, that underneath uses + :func:`aot_function` to perform tracing and compilation. + + :func:`aot_module` lifts the parameters and buffers of ``nn.Module`` as inputs + to a new callable which is then compiled through :func:`aot_function`. + + .. warning:: + This API is experimental and likely to change. + + Args: + mod (Callable): A ``nn.Module`` module. + args : args to be passed to :func:`aot_function` + kwargs : kwargs to be passed to :func:`aot_function` + + Returns: + Returns a ``nn.Module`` that retains the eager behavior of the original + :attr:`mod`, but with forward and backward graph compiled. + + """ + # See Note: [Fake Modules and AOTAutograd] + torch._dynamo.utils.assert_no_fake_params_or_buffers(mod) + + def functional_call( + named_params: dict[str, torch.nn.Parameter], + named_buffers: dict[str, torch.Tensor], + *args: Any, + **kwargs: Any, + ) -> Any: + params_and_buffers = {**named_params, **named_buffers} + return torch.func.functional_call(mod, params_and_buffers, args, kwargs) + + named_params = dict(mod.named_parameters(remove_duplicate=False)) + named_buffers = dict(mod.named_buffers(remove_duplicate=False)) + num_params_buffers = len(named_params) + len(named_buffers) + compiled_f = aot_function( + functional_call, + *args, + # pyrefly: ignore[bad-keyword-argument] + num_params_buffers=num_params_buffers, + **kwargs, + ) + + class AOTModule(nn.Module): + def __init__(self) -> None: + super().__init__() + self.orig_module = mod + + def forward(self, *args: Any, **kwargs: Any) -> Any: + return compiled_f( + named_params, + named_buffers, + *args, + **kwargs, + ) + + return AOTModule() + + +def autograd_cache_key( + graph, + example_inputs, + ignore_shape_env: bool, + decompositions, + compiler_config_extra: CompilerConfigExtra, + keep_inference_input_mutations: bool = False, + disable_functionalization: bool = False, +): + ( + _params_buffers_flat, + _params_spec, + _buffers_spec, + full_args, + _full_args_descs, + aot_config, + ) = prepare_aot_config( + graph, + example_inputs, + decompositions, + keep_inference_input_mutations, + ignore_shape_env, + force_non_lazy_backward_lowering=config.force_non_lazy_backward_lowering, + disable_functionalization=disable_functionalization, + ) + + fake_mode, shape_env = construct_fake_mode(full_args, aot_config) + fake_flat_args, _act_input_indices = process_inputs( + full_args, aot_config, fake_mode, shape_env, ignore_shape_env + ) + + return autograd_cache.autograd_cache_key( + graph, fake_flat_args, aot_config, compiler_config_extra + ) + + +def prepare_aot_config( + mod: nn.Module, + args: Iterable[Any], + decompositions: dict[OpOverload, Callable[..., Any]] | None, + keep_inference_input_mutations: bool, + ignore_shape_env: bool, + *, + force_non_lazy_backward_lowering: bool = False, + disable_functionalization: bool = False, + _disable_torch_fn_metadata_mode: bool = False, +) -> tuple[ + list[torch.nn.Parameter | Tensor], + list[str], + list[str], + list[Any], + list[Any], + AOTConfig, +]: + # TODO: There's something a bit suspicious here; typically simplified + # module shouldn't actually have any parameters... + params = dict(mod.named_parameters(remove_duplicate=False)) + buffers = dict(dict(mod.named_buffers(remove_duplicate=False))) + + params_flat, params_spec = list(params.values()), list(params.keys()) + params_len = len(params_flat) + + buffers_flat, buffers_spec = list(buffers.values()), list(buffers.keys()) + buffers_len = len(buffers_flat) + + params_buffers = {**params, **buffers} + params_buffers_flat = params_flat + buffers_flat + + full_args = [*params_flat, *buffers_flat, *args] + + # OK, set up the descs + + full_args_descs: list[DifferentiableAOTInput] = [] + full_args_descs.extend(ParamAOTInput(fqn) for fqn in params_spec) + full_args_descs.extend(BufferAOTInput(fqn) for fqn in buffers_spec) + + # TODO: These tracing_context fields should become unnecessary once we + # always maintain sources on all arguments + if tracing_context := torch._guards.TracingContext.try_get(): + # NB: TracingContext misnames this, the "params" here also contains + # buffers + tracing_context.params_flat = params_buffers_flat + ( + tracing_context.params_flat_unwrap_subclasses, + tracing_context.params_unwrapped_to_flat_index, + ) = unwrap_tensor_subclasses_with_indices_to_original(params_buffers_flat) + + # TODO: Might be nice to hold on to the Dynamo source here in full_args_descs! + ( + aot_autograd_arg_pos_to_source, + static_input_indices, + ) = _try_get_metadata_from_dynamo( + mod, params_buffers.keys(), len(full_args), full_args_descs + ) + + dynamic_shapes = False + for x in full_args: + if isinstance(x, FakeTensor): + dynamic_shapes = x.fake_mode.shape_env is not None + break + + aot_config = AOTConfig( + fw_compiler=None, + bw_compiler=None, + inference_compiler=None, + partition_fn=None, + decompositions=decompositions, + num_params_buffers=params_len + buffers_len, + aot_id=next(AOT_COUNTER), + keep_inference_input_mutations=keep_inference_input_mutations, + dynamic_shapes=dynamic_shapes, + # pyrefly: ignore[bad-argument-type] + aot_autograd_arg_pos_to_source=aot_autograd_arg_pos_to_source, + static_input_indices=static_input_indices, + is_export=False, + no_tangents=False, + cache_info=None, + ignore_shape_env=ignore_shape_env, + precompile_backend_id=getattr(mod, "_backend_id", None), + force_non_lazy_backward_lowering=force_non_lazy_backward_lowering, + disable_functionalization=disable_functionalization, + _disable_torch_fn_metadata_mode=_disable_torch_fn_metadata_mode, + ) + + return ( + params_buffers_flat, + params_spec, + buffers_spec, + full_args, + full_args_descs, + aot_config, + ) + + +def prepare_aot_module_simplified( + mod: nn.Module, + args: Iterable[Any], + kwargs: dict[str, Any] | None, + decompositions: dict[OpOverload, Callable[..., Any]] | None, + keep_inference_input_mutations: bool, + ignore_shape_env: bool, + flatten: bool, + *, + force_non_lazy_backward_lowering: bool = False, + disable_functionalization: bool = False, + _record_nn_module_stack: bool = False, + _disable_torch_fn_metadata_mode: bool = False, +) -> tuple[ + Any, + list[torch.nn.Parameter | Tensor], + list[str], + list[str], + FakifiedFlatArgs, + list[Any], + AOTConfig, + FakeTensorMode, + ShapeEnv | None, + pytree.TreeSpec | None, + PytreeThunk | None, + list[int], +]: + if not flatten: + if kwargs is not None: + raise AssertionError("kwargs must be None when flatten=False") + elif kwargs is None: + kwargs = {} + + ( + params_buffers_flat, + params_spec, + buffers_spec, + full_args, + full_args_descs, + aot_config, + ) = prepare_aot_config( + mod, + args, + decompositions, + keep_inference_input_mutations, + ignore_shape_env, + force_non_lazy_backward_lowering=force_non_lazy_backward_lowering, + disable_functionalization=disable_functionalization, + _disable_torch_fn_metadata_mode=_disable_torch_fn_metadata_mode, + ) + + params_buffers_spec = params_spec + buffers_spec + + # NB: This doesn't change the in/out convention, except adding the + # parameters as explicit arguments + functional_call = create_functional_call( + mod, + params_buffers_spec, + aot_config.num_params_buffers, + strict_out_tuple=not flatten, + # We need this for export to run ModuleStackTracer + # instead of PythonKeyTracer + store_orig_mod=_record_nn_module_stack, + ) + + in_spec, out_spec = None, None + if flatten: + functional_call, out_spec = create_tree_flattened_fn( + functional_call, full_args, kwargs + ) + full_args, in_spec = pytree.tree_flatten((full_args, kwargs)) + + # TODO: it would be better to put pytree information in here + full_args_descs.extend( + PlainAOTInput(i) for i in range(len(full_args) - len(full_args_descs)) + ) + + fake_mode, shape_env = construct_fake_mode(full_args, aot_config) + # NB: full_args_descs not needed here, fake_flat_args is 1:1 with full_args + fake_flat_args, act_input_indices = process_inputs( + full_args, aot_config, fake_mode, shape_env, ignore_shape_env + ) + + return ( + functional_call, + params_buffers_flat, + params_spec, + buffers_spec, + fake_flat_args, + full_args_descs, + aot_config, + fake_mode, + shape_env, + in_spec, + out_spec, + act_input_indices, + ) + + +def aot_module_simplified( + mod: torch.fx.GraphModule | torch._dynamo.utils.GmWrapper, + args: Sequence[Any], + fw_compiler: AOTDispatchCompiler, + bw_compiler: AOTDispatchCompiler | None = None, + partition_fn: Callable[..., Any] = default_partition, + decompositions: dict[OpOverload, Callable[..., Any]] | None = None, + keep_inference_input_mutations: bool = False, + inference_compiler: AOTDispatchCompiler | None = None, + # TODO: This doesn't seem to be used in any nontrivial way, check if it's + # actually needed + compiler_config_extra: CompilerConfigExtra | None = None, + ignore_shape_env: bool = False, + disable_functionalization: bool = False, + # Optional callback to run passes on the module at the start of AOT autograd. + pre_grad_passes: Callable[ + [torch.fx.GraphModule, Sequence[InputType]], torch.fx.GraphModule + ] + | None = None, + compile_region_name: str | None = None, +) -> Callable[..., Any]: + """ + This is the simplified or low overhead version of aot_module. For frontends + like TorchDynamo, the input functions/modules to AOT are static and have + unpacked inputs/outputs. This gives us an opportunity to remove the + (1) pytree overhead to parse inputs/outputs, + (2) AOT Autograd cache, + (3) Reading of params/buffers in every forward call + + :func:`aot_module_simplified` removes these overheads. + """ + + pre_grad_pass_timing: Literal["early", "late"] = resolve_pre_grad_pass_timing() + + if ( + pre_grad_pass_timing == "early" + and pre_grad_passes + and isinstance(mod, torch.fx.GraphModule) + ): + mod = pre_grad_passes(mod, args) + + with contextlib.ExitStack() as stack: + ( + functional_call, + params_buffers_flat, + _params_spec, + _buffers_spec, + fake_flat_args, + full_args_descs, + aot_config, + fake_mode, + shape_env, + _in_spec, + _out_spec, + act_input_indices, + ) = prepare_aot_module_simplified( + mod, + args, + None, + decompositions, + keep_inference_input_mutations, + ignore_shape_env, + flatten=False, + force_non_lazy_backward_lowering=config.force_non_lazy_backward_lowering, + disable_functionalization=disable_functionalization, + ) + + compiled_fn = None + + if ( + isinstance(fw_compiler, SerializableAOTDispatchCompiler) + or torch._functorch.config.force_autograd_cache + ): + local = should_use_local_autograd_cache() + remote = should_use_remote_autograd_cache() + if local or remote: + set_feature_use("aot_autograd_remote_cache", remote) + compiled_fn = AOTAutogradCache.try_load( + mod, + fake_flat_args, + aot_config, + compiler_config_extra, + local, + remote, + compile_region_name=compile_region_name, + ) + + if compiled_fn is None: + if ( + pre_grad_pass_timing == "late" + and pre_grad_passes + and isinstance(mod, torch.fx.GraphModule) + ): + mod = pre_grad_passes(mod, args) + + stack.enter_context(compiled_autograd._disable()) + aot_state = create_aot_state( + stack, + functional_call, + fake_flat_args, + full_args_descs, + aot_config, + fake_mode, + shape_env, + ) + aot_state.fw_metadata.act_input_indices = act_input_indices + aot_graph_capture = aot_stage1_graph_capture(aot_state, functional_call) + compiled_fn, _ = aot_stage2_compile( + aot_state, + aot_graph_capture, + partition_fn, + fw_compiler, + bw_compiler, + inference_compiler, + ) + if compiled_fn is None: + raise AssertionError("compiled_fn must not be None") + if isinstance(mod, torch._dynamo.utils.GmWrapper): + # This function is called by the flatten_graph_inputs wrapper, which boxes + # the inputs so that they can be freed before the end of this scope. + # For overhead reasons, this is not the default wrapper, see comment: + # https://github.com/pytorch/pytorch/pull/122535/files#r1560096481 + @simple_wraps(compiled_fn) + def forward(runtime_args: list[Any]) -> Any: + flat_args = [] + flat_args.extend(params_buffers_flat) + flat_args.extend(runtime_args) + runtime_args.clear() + if compiled_fn is None: + raise AssertionError("compiled_fn must not be None") + return compiled_fn(flat_args) + + else: + # TODO: There is something deeply wrong here; compiled_fn running with + # the boxed calling convention, but aot_module_simplified somehow + # historically returned a function that was not the boxed calling + # convention. This should get fixed... + # NB: GraphModule/nn.Module rely on the non-boxed calling convention here + @simple_wraps(compiled_fn) + def forward(*runtime_args: tuple[Any]) -> Any: + full_args = [] + full_args.extend(params_buffers_flat) + # pyrefly: ignore[bad-argument-type] + full_args.extend(runtime_args) + if compiled_fn is None: + raise AssertionError("compiled_fn must not be None") + return compiled_fn(full_args) + + # Just for convenience + forward.zero_grad = mod.zero_grad # type: ignore[attr-defined] + forward.named_parameters = mod.named_parameters # type: ignore[attr-defined] + forward.named_buffers = mod.named_buffers # type: ignore[attr-defined] + + # Add a serialize function + def grab_serialize_fn(fn: Any) -> Callable[..., Any] | None: + if isinstance(fn, SerializableCompiledFunction): + return fn.serialize_fn + elif hasattr(fn, "__wrapped__"): + return grab_serialize_fn(fn.__wrapped__) + else: + return None + + forward.serialize = grab_serialize_fn(forward) # type: ignore[attr-defined] + return forward + + +def boxed_nop_preserve_node_meta( + gm: torch.fx.GraphModule, example_inputs: Sequence[InputType] +) -> Any: + def run(args: list[Any]) -> OutputCode: + with torch.fx.traceback.preserve_node_meta(): + return torch.fx.Interpreter(gm).boxed_run(args) + + run._boxed_call = True # type: ignore[attr-defined] + return run + + +def aot_export_joint_with_descriptors( + stack: contextlib.ExitStack, + mod: nn.Module, + args: tuple[Any, ...], + kwargs: dict[str, Any] | None = None, + *, + decompositions: dict[OpOverload, Callable[..., Any]] | None = None, + keep_inference_input_mutations: bool = False, + ignore_shape_env: bool = False, + disable_functionalization: bool = False, + _record_nn_module_stack: bool = False, + _disable_torch_fn_metadata_mode: bool = False, +) -> JointWithDescriptors: + """ + This API captures the joint graph for an nn.Module. However, unlike + aot_export_joint_simple or aot_export_module(trace_joint=True), the + calling convention of the produced joint graph follows no fixed positional + schema; for example, you cannot rely on the second argument of the traced + joint graph to correspond to the second argument of the module you traced. + However, the inputs and outputs of the traced graph are schematized + with **descriptors**, annotated on meta['desc'] on the placeholder and + return FX nodes, which you can use to determine the meaning of arguments. + + The major benefit of using this export rather than aot_export_joint_simple + is that we have feature parity with all situations that torch.compile + supports (via aot_module_simplified), including handling for more + complicated cases such as multiple differentiable outputs, input mutations + that must be handled outside of the graph, tensor subclasses, etc. + + What can you do with one of these joint graphs with descriptors? The + motivating use case (autoparallel) involves taking the joint graph, doing + optimizations on it, and then turning it back into a callable so it can be + torch.compile'd at a later point in time. This cannot be done as a + traditional torch.compile joint graph pass for two reasons: + + 1. The sharding of parameters must be decided before parameter + initialization / checkpoint load, far before torch.compile would + ordinarily run. + + 2. We need to change the meaning of parameters (e.g., we might replace + a replicated parameter with a sharded version of it, changing its + input size). torch.compile is ordinarily semantics preserving, and + not allowed to change the meaning of inputs. + + Some descriptors can be quite exotic, so we recommend thinking carefully + if there is a safe fallback you can apply to descriptors you don't understand. + For example, you should have some way to handle not finding a particular + input exactly as is in the final FX graph inputs. + + Note: When using this API, you must create and enter an ExitStack context + manager, which will be passed into this function. This context manager + must remain active if you call the compile function to finish compilation. + (TODO: We may relax this requirement by having AOTAutograd keep track of + how to reconstruct all the context managers at a later point in time.) + + NB: You're not obligated to do a /full/ compile in stage2; instead you can + leave the forward/backward compilers unspecified in which case the + partitioned FX graphs will directly run. The overall autograd Function + can be allowed in graph so you can reprocess it in the context of a + (potentially larger) compiled region later. + + NB: These APIs do NOT hit cache, as we only ever cache the final compile results, + not the intermediate export result. + + NB: If the passed nn.Module has parameters and buffers on it, we will + generate extra implicit parameter/buffer arguments and assign ParamAOTInput + and BufferAOTInput descriptors to them. However, if you generate the input + nn.Module from a mechanism like Dynamo, you will NOT get these descriptors + (because Dynamo will already have taken care of lifting the parameters/buffers + into arguments!) In that case, it would be necessary to analyze the Sources + of the inputs to determine if inputs are parameters and their FQNs. + """ + + ( + functional_call, + _params_buffers_flat, + params_spec, + buffers_spec, + fake_flat_args, + full_args_descs, + aot_config, + fake_mode, + shape_env, + in_spec, + out_spec, + act_input_indices, + ) = prepare_aot_module_simplified( + mod, + args, + kwargs, + # In contrast, decompositions are needed at this stage. + decompositions, + keep_inference_input_mutations, + ignore_shape_env, + flatten=True, + # Without this, we will attempt to "compile" the backward lazily + # at runtime, but this is pointless because it's just boxed_nop, + # it's trivial. But this will get Inductor confused about scoping + # Metric(s) {'is_forward'} have already been set in the current + # context. + force_non_lazy_backward_lowering=True, + disable_functionalization=disable_functionalization, + _record_nn_module_stack=_record_nn_module_stack, + _disable_torch_fn_metadata_mode=_disable_torch_fn_metadata_mode, + ) + + # TODO: Maybe this should be in create_aot_state? Not sure, that would + # increase its scope + stack.enter_context(compiled_autograd._disable()) + + aot_state = create_aot_state( + stack, + functional_call, + fake_flat_args, + full_args_descs, + aot_config, + fake_mode, + shape_env, + ) + aot_state.fw_metadata.act_input_indices = act_input_indices + + # NB: no cache lookup! + aot_graph_capture = aot_stage1_graph_capture(aot_state, functional_call) + + if out_spec is None or out_spec.spec is None: + raise AssertionError("out_spec and out_spec.spec must not be None") + if in_spec is None: + raise AssertionError("in_spec must not be None") + return JointWithDescriptors( + _aot_state=aot_state, + _aot_graph_capture=aot_graph_capture, + params_spec=params_spec, + buffers_spec=buffers_spec, + in_spec=in_spec, + out_spec=out_spec.spec, + ) + + +def aot_compile_joint_with_descriptors( + jd: JointWithDescriptors, + *, + partition_fn: Callable[..., Any] = default_partition, + fw_compiler: AOTDispatchCompiler = boxed_nop_preserve_node_meta, + bw_compiler: AOTDispatchCompiler | None = boxed_nop_preserve_node_meta, + serializable: bool = False, +) -> Callable[..., Any]: + """ + Companion function for aot_export_joint_with_descriptors which compiles the joint + graph into a callable function that follows a standard calling convention. + params_flat all are arguments. + + Note: We do NOT instantiate the module; this gives you the flexibility to subclass it and + customize its behavior without having to worry about FQN rebinding. + + Args: + serializable: If True, configures the compilation to produce a serializable + callable by leveraging AOTAutogradCache machinery. This sets up the + necessary cache_info and patches config options required for serialization. + When True, this function will always return a BundledAOTAutogradSerializableCallable. + """ + # TODO: Consider if we should allow_in_graph the result by default. + from torch._dynamo.aot_compile_types import ( + BundledAOTAutogradSerializableCallable, + SerializableCallable, + ) + from torch._functorch._aot_autograd.schemas import AOTAutogradCacheInfo + from torch._guards import detect_fake_mode + from torch._inductor.output_code import OutputCode + + fw_compiler = SerializableAOTDispatchCompiler(OutputCode, fw_compiler) + + cache_ctx = nullcontext() + if serializable: + jd._aot_state.aot_config.cache_info = AOTAutogradCacheInfo( + jd.cache_hash(), + time.time_ns(), + forward_symints=[], + ) + cache_ctx = torch._functorch.config.patch( + { + "strict_autograd_cache": True, + "bundled_autograd_cache": True, + "force_non_lazy_backward_lowering": True, + "bypass_autograd_cache_key": True, + } + ) + + # Set up a TracingContext if one isn't already available. + # This is needed for compilers (like regional_inductor) that call + # standalone_compile with dynamic_shapes="from_tracing_context". + tracing_context = torch._guards.TracingContext.try_get() + if tracing_context is None: + fake_mode = detect_fake_mode(jd._aot_state.flat_args) + if fake_mode is not None: + tracing_context = torch._guards.TracingContext(fake_mode) + tracing_ctx = torch._guards.tracing(tracing_context) + else: + tracing_ctx = nullcontext() + else: + tracing_ctx = nullcontext() + + with cache_ctx, tracing_ctx: + compiled_fn, _ = aot_stage2_compile( + jd._aot_state, + jd._aot_graph_capture, + partition_fn, + fw_compiler, + bw_compiler, + ) + + if not isinstance(compiled_fn, SerializableCallable) and hasattr( + compiled_fn, "serialize" + ): + compiled_fn = BundledAOTAutogradSerializableCallable(compiled_fn) + + # Cribbed from torch/export/pt2_archive/_package.py + @simple_wraps(compiled_fn) + @torch._dynamo.nonstrict_trace # allow recursive compilation + def unflattened_compiled_fn(*args: Any, **kwargs: Any) -> Any: + flat_inputs = pytree.tree_flatten((args, reorder_kwargs(kwargs, jd.in_spec)))[0] + # TODO: do I need to filter? I hope not! + flat_outputs = compiled_fn(flat_inputs) + return pytree.tree_unflatten(flat_outputs, jd.out_spec) + + return unflattened_compiled_fn + + +def aot_export_module( + mod: nn.Module, + args: Iterable[Any], + *, + decompositions: dict[OpOverload, Callable[..., Any]] | None = None, + # If true, we'll return a joint forward-backward graph, + # As well as metadata on the loss + gradients in the backward. + trace_joint: bool, + # If trace_joint is True, we expect your module to return a scalar loss. + # Your module can return multiple outputs, so you must specify which output the loss is. + output_loss_index: int | None = None, + pre_dispatch: bool = False, + # If None, will be inferred from inputs and mod.graph.nodes if mod is a graph module, but the inferred result might be wrong. + dynamic_shapes: bool | None = None, + kwargs: dict[str, Any] | None = None, +) -> tuple[Callable[..., Any] | torch.fx.GraphModule, GraphSignature]: + """ + This function takes in a module, and returns: + (1) an FX graph that can be exported + (2) some metadata about the graph + + If `trace_joint=True` we will return a joint graph of the forward + backward. + + The traced FX graph will have the following properties compared to the original module: + (1) Inputs and outputs to the module will be pytree-flattened + (2) Parameters and buffers on the module will be lifted into graph inputs, + graph_inputs = (*parameters, *buffers, *user_inputs) + (3) The graph will be fully functionalized + (4) Any input mutations will be converted into additional outputs in the graph, + meaning whoever calls this graph is responsible for applying the mutations + back to the original inputs. + (5) If is_joint is provided the graph will return parameter gradients in addition to user outputs. + The graph output will look like: + graph_outputs = (*updated_inputs, *user_outputs, *param_gradients) + + There are also several restrictions on what modules can use this API. In particular: + (1) If trace_joint is specified, we expect the loss function to be **fused** + into the module forward. One of the outputs to the forward must be a scalar loss, + which is specified with `output_loss_index`. + All other outputs to the forward are presumed to not require gradients. + (2) This API cannot capture optimizers (although in theory we could build an API for this). + (3) Metadata mutations on params/buffers/inputs are banned. + (4) Data mutations on anything that requires gradients are banned (parameters) + (5) If an input is mutated, it is not allowed to alias any other inputs. + (6) Parameters must not be duplicated. + """ + if pre_dispatch and trace_joint: + raise RuntimeError("pre_dispatch is not supported when trace_joint is True.") + named_parameters = dict(mod.named_parameters(remove_duplicate=False)) + named_buffers = dict(mod.named_buffers(remove_duplicate=False)) + + params_and_buffers = { + **dict(named_parameters), + **dict(named_buffers), + } + params_and_buffers_flat, params_spec = pytree.tree_flatten(params_and_buffers) + params_and_buffers_flat = tuple(params_and_buffers_flat) + params_len = len(params_and_buffers_flat) + + kwargs = kwargs or {} + + functional_call = create_functional_call( + mod, params_spec, params_len, store_orig_mod=True + ) + + num_fw_outs = None + + if trace_joint: + # This helper effectively just adds some extra asserts about what the backward will look like: + # Outputs must include a scalar loss, that we compute gradients w.r.t. + # We don't compute gradients w.r.t. anything else: so just in case we detach() + # and other output tensors. + def fn_to_trace(*args: Any) -> Any: + nonlocal num_fw_outs + out = functional_call(*args) + if output_loss_index is None: + raise RuntimeError( + """\ +If trace_joint=Trueit is required that one of your forward outputs must be a scalar loss. +You must specify the which (index) output is the loss with output_loss_index.""" + ) + if isinstance(out, (torch.Tensor)): + out = (out,) + if not isinstance(out, (tuple, list)): + raise RuntimeError( + f"Expected forward output to be either a tensor or a list/tuple of tensors. found {type(out)}" + ) + + for i, o in enumerate(out): + # We only want to create a backward graph w.r.t. the loss that the user passed in. + # This implies that every other output should not require gradients. + # Instead of making this an error (and forcing the user to detach all other outputs + # of their forward), + # we'll automatically detach them here. + if o.requires_grad and i != output_loss_index: + raise RuntimeError( + f"""\ +Found an output of the forward that requires gradients, that was not the scalar loss. +We require all outputs to the forward that are not the scalar loss to not require gradient, +because we will only compute a backward graph against the scalar loss. +You can fix this by calling .detach() on each of your forward outputs that is not the loss. +You specified that output index {output_loss_index} is the loss, but we found that +the output at index {i} requires gradients.""" + ) + out_loss = out[output_loss_index] + num_fw_outs = len(out) + if not out_loss.requires_grad: + raise RuntimeError( + f"""\ +The output at index {output_loss_index} was marked as the loss, but it does not require gradients""" + ) + if out_loss.numel() != 1: + raise RuntimeError( + f"""\ +We require the output marked as the loss (at index {output_loss_index}) to be a scalar, but it has shape {out_loss.shape}""" + ) + return out + + ctx = nullcontext + else: + # Run under no_grad, so our tracing machinery only traces an inference graph. + # However if pre_dispatch=True, we want to correctly trace set_grad_enabled calls for training. + ctx = nullcontext if pre_dispatch else torch.no_grad + fn_to_trace = functional_call + + full_args = [] + # First, the params + # NB: It is REQUIRED that parameters come first, Inductor infers "fixed" + # parameters by looking at the difference in parameter count outside + # and inside AOTAutograd, and assumes the prefix of arguments are fixed + # arguments + full_args.extend(params_and_buffers_flat) + # Next, the input args + full_args.extend(args) + + with ctx(): + fx_g, metadata, in_spec, out_spec = _aot_export_function( + fn_to_trace, + tuple(full_args), + decompositions=decompositions, + num_params_buffers=params_len, + no_tangents=True, + pre_dispatch=pre_dispatch, + dynamic_shapes=dynamic_shapes, + trace_joint=trace_joint, + kwargs=kwargs, + ) + + # TODO: subsume this path with the aot_stage2_graph_capture path + if trace_joint: + + @wraps(functional_call) + def flattened_joint(*args: Any) -> Any: + # The idea here is that the joint graph that AOTAutograd creates has some strict properties: + # (1) It accepts two arguments (primals, tangents), and pytree_flattens them + # (2) It returns a tuple of (fw_outs, gradients) + # This is a very useful convention for anyone who wants to partition the joint graph + # into a separate forward and backward graph. + # However, + # (1) for people exporting a single joint graph, it would be preferable not to have + # any pytrees in the graph. + # (2) We are guaranteed in the aot_export_module case that the forward outputs a loss, + # and there are therefore no tangents that are needed to run the joint graph. + # (3) AOTAutograd creates a grad_input for every input in the forward, + # including None's for inputs that are not grad-requiring tensors. + # we don't want these in our export graph. + # and there are therefore no tangents that are needed to run the joint graph. + # This function "fixes" both of the above by removing any tangent inputs, + # and removing pytrees from the original FX graph. + fake_tangents = [ + None + for _ in range( + metadata.num_outputs + metadata.num_mutated_inp_runtime_indices + ) + ] + fw_outs, gradients = fx_g(args, fake_tangents) + if len(gradients) != len(args): + raise AssertionError( + f"len(gradients)={len(gradients)} != len(args)={len(args)}" + ) + output_gradients = [] + for a, grad in zip(args, gradients): + if isinstance(a, torch.Tensor) and a.requires_grad: + if grad is None: + raise AssertionError("""\ +Found a parameter that did not receive a gradient. +"This is most likely a bug, but if this needs to be supported please comment on this Github issue: +https://github.com/pytorch/pytorch/issues/101192 +""") + output_gradients.append(grad) + else: + if grad is not None: + raise AssertionError( + f"expected grad to be None for non-tensor or non-requires_grad input, got {type(grad)}" + ) + return *fw_outs, *output_gradients + + fx_g = make_fx(flattened_joint, record_module_stack=True)(*full_args) + + user_args_flat = pytree.arg_tree_leaves(*args, **kwargs) + if out_spec is None: + raise AssertionError("out_spec must not be None") + return fx_g, create_graph_signature( + # type: ignore[bad-argument-type] + fx_g, + metadata, + in_spec, + out_spec, + user_args_flat=user_args_flat, + params_and_buffers_flat=list(params_and_buffers_flat), + param_names=list(named_parameters.keys()), + buffer_names=list(named_buffers.keys()), + trace_joint=trace_joint, + num_user_fw_outs=num_fw_outs, + loss_index=output_loss_index, + ) + + +def aot_export_joint_simple( + func: Callable[..., Any], + args: tuple[Any, ...], + *, + trace_joint: bool, + # It looks like the main consequence of this API is that for dynamic shapes, + # it will assume that params/buffers are static. + # With the new inferred dynamic shapes API, maybe this doesn't matter? + num_params_buffers: int = 0, + decompositions: dict[OpOverload, Callable[..., Any]] | None = None, +) -> Callable[..., Any]: + """ + A simplified version of export. Used by higher order operators. + + This function makes a high-level "no calling convention changes" guarantee: + - If no inputs require grad (so we export an inference graph), + there are *no* calling convention change between the exported graph, and "func". + - If at least one input requires grad (so we trace out and export a joint fw-bw graph), + Then if you were partition the graph into a separate forward and backward graph, + The forward graph will have no calling convention changes compared to "func". + + The above also relies on some strong restrictions around which functions this API accepts: + (1) `args` cannot contain any pytrees (they must have been pytree_flattened already) + (2) `func` cannot mutate any inputs + (3) The outputs of `func` cannot alias any inputs. + + Note: this function is only lightly tested today. It will probably be tested more heavily by higher order ops. + """ + if trace_joint: + ctx = nullcontext + else: + # Run under no_grad, so our tracing machinery only traces an inference graph. + ctx = torch.no_grad + + with ctx(): + fx_g, metadata, in_spec, out_spec = _aot_export_function( + func, + args, + decompositions=decompositions, + trace_joint=trace_joint, + ) + in_spec, _kw_in_spec = in_spec.children() + # At this point, we can just directly return the (joint or inference graph) that we traced. + # First though: a bunch of assertions to make sure that our graph doesn't require + # any calling convention changes compared to the original function. + # These restrictions are *in addition to* the general restrictions on export. + + # No input mutations + if ( + len([x for x in metadata.input_info if x.mutates_data or x.mutates_metadata]) + != 0 + ): + raise RuntimeError( + f"aot_export_joint_simple does not support input mutations. {str(metadata)}" + ) + # No output aliasing + if ( + len([x for x in metadata.output_info if x.output_type != OutputType.non_alias]) + != 0 + ): + raise RuntimeError( + f"aot_export_joint_simple does not support outputs that alias inputs. {str(metadata)}" + ) + # No pytrees + if in_spec.is_leaf(): + raise RuntimeError( + f"aot_export_joint_simple requires inputs to be a single list/tuple. in_spec={str(in_spec)}" + ) + if not all(child.is_leaf() for child in in_spec.children()): + raise RuntimeError( + f"aot_export_joint_simple requires individual inputs not to be pytrees. in_spec={str(in_spec)}" + ) + + if out_spec is None: + raise AssertionError("out_spec must not be None") + if out_spec.is_leaf(): + raise RuntimeError( + f"aot_export_joint_simple requires outputs to be a single list/tuple. out_spec={str(out_spec)}" + ) + if not all(child.is_leaf() for child in out_spec.children()): + raise RuntimeError( + f"aot_export_joint_simple requires individual outputs not to be pytrees. out_spec={str(out_spec)}" + ) + # TODO: we might have to temporarily patch config.functionalize_rng + # so that it doesn't run when we're exporting a higher order op. + + if config.debug_assert: + # Smoke test that after partitioning, we can run the forward without any calling convention changes. + fw_module, _bw_module = default_partition( # noqa: F821 + # type: ignore[bad-argument-type] + fx_g, + args, + # type: ignore[unknown-name] + num_fwd_outputs=len(fw_metadata.output_infos), # noqa: F821 + ) + # Attempt to run the fw_module with the original user inputs + fake_mode = detect_fake_mode(args) + if fake_mode is None: + fake_mode = FakeTensorMode() + with fake_mode: + fw_module(*args) + return fx_g + + +# Private for now because we aren't providing a contract on what to return +# for joint graphs (we could when there's a clearer use case) +# In the future, we may need to add more export API's that provide their own strong guarantees. +# This is meant as a general helper function for handling various export-y use cases. +def _aot_export_function( + func: Callable[..., Any], + args: tuple[Any, ...], + *, + num_params_buffers: int = 0, + decompositions: dict[OpOverload, Callable[..., Any]] | None = None, + # If we're exporting a joint graph and we don't want any tangent inputs in the graph + # (because we are backpropping through a scalar 1 loss), + # we need to explicitly specify not to include tangents in the graph. + # It's not enough just to check that our tangent is a scalar, since we also + # need to know if it is a 1 (no need to make it a graph input), or something else + # (requiring it to be a graph input). + # We don't know this info at trace time though, so we need to make it an explicit config. + no_tangents: bool = False, + pre_dispatch: bool = False, + # If None, `dynamic_shapes` will be inferred from inputs, but the inferred result might be wrong. + dynamic_shapes: bool | None = None, + keep_input_mutations: bool = False, + # Under export, configures whether we are getting inference or training IR + trace_joint: bool = False, + kwargs: dict[str, Any] | None = None, +) -> tuple[ + Callable[..., Any], ViewAndMutationMeta, pytree.TreeSpec, pytree.TreeSpec | None +]: + kwargs = kwargs or {} + + flat_fn, out_spec = create_tree_flattened_fn(func, args, kwargs) + flat_args, in_spec = pytree.tree_flatten((args, kwargs)) + + fake_mode = None + if dynamic_shapes is None: + # Try to infer `dynamic_shapes from inputs and graph nodes + fake_mode = detect_fake_mode(flat_args) + if ( + fake_mode is None + and hasattr(func, "_orig_mod") + and isinstance(func._orig_mod, torch.fx.GraphModule) + ): + vals = [ + node.meta["val"] + for node in func._orig_mod.graph.nodes + if "val" in node.meta + ] + fake_mode = detect_fake_mode(vals) + dynamic_shapes = fake_mode is not None and fake_mode.shape_env is not None + + # The export use case doesn't care about several bits of AOTConfig + # (1) compilers (we just export the graph) + # (2) partitioners (export is only full graph, user can partition themselves) + aot_config = AOTConfig( # type: ignore[arg-type] + fw_compiler=None, + bw_compiler=None, + inference_compiler=None, + partition_fn=None, + decompositions=decompositions, + num_params_buffers=num_params_buffers, + aot_id=next(AOT_COUNTER), + keep_inference_input_mutations=keep_input_mutations, + dynamic_shapes=dynamic_shapes, + aot_autograd_arg_pos_to_source=None, + is_export=True, + no_tangents=no_tangents, + pre_dispatch=pre_dispatch, + export_trace_joint=trace_joint, + ) + if fake_mode is None: + fake_mode, shape_env = construct_fake_mode(flat_args, aot_config) + else: + shape_env = fake_mode.shape_env + fake_flat_args, act_input_indices = process_inputs( + flat_args, aot_config, fake_mode, shape_env + ) + # TODO: Improve the descs here with pytree information + fake_flat_args_descs: list[AOTInput] = [ + PlainAOTInput(i) for i in range(len(fake_flat_args)) + ] + + with contextlib.ExitStack() as stack: + aot_state = create_aot_state( + stack, + flat_fn, + fake_flat_args, + fake_flat_args_descs, + aot_config, + fake_mode, + shape_env, + ) + aot_state.fw_metadata.act_input_indices = act_input_indices + aot_graph_capture = aot_stage1_graph_capture(aot_state, flat_fn) + fx_g, meta = aot_stage2_export(aot_state, aot_graph_capture) + + return fx_g, meta, in_spec, out_spec.spec + + +compiled_function = aot_function +compiled_module = aot_module diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/apis.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/apis.py new file mode 100644 index 0000000000000000000000000000000000000000..600f6f14da393574857895da6a242a93668893a8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/apis.py @@ -0,0 +1,484 @@ +# NOTE: We allow Dynamo to see this file (via torch/_dynamo/trace_rules.py) so that it can +# trace through functorch transforms. +# Currently, we can't allow Dynamo to see `eager_transforms.py`/`vmap.py` as that break a lot of thing +# and there isn't a mechanism to selectively expose only some functions (eg. grad) from a file +# to Dynamo. + +from __future__ import annotations + +import functools +from typing import Any, TYPE_CHECKING +from typing_extensions import ParamSpec, TypeVar + +from torch._functorch.utils import argnums_t, exposed_in +from torch._functorch.vmap import ( + _check_out_dims_is_int_or_int_pytree, + _check_randomness_arg, + _chunked_vmap, + _process_batched_inputs, + Callable, + in_dims_t, + out_dims_t, + vmap_impl, +) + + +if TYPE_CHECKING: + from collections.abc import Iterable + + import torch + +_P = ParamSpec("_P") +_R = TypeVar("_R") +# vmap(func)(inputs) wraps all Tensor inputs to be batched in BatchedTensors, +# sends those into func, and then unwraps the output BatchedTensors. Operations +# on BatchedTensors perform the batched operations that the user is asking for. +# +# vmap's randomness behavior differs from JAX's, which would require a PRNG key +# to be passed everywhere. + + +@exposed_in("torch.func") +def vmap( + func: Callable[_P, _R], + in_dims: in_dims_t = 0, + out_dims: out_dims_t = 0, + randomness: str = "error", + *, + chunk_size: int | None = None, +) -> Callable[_P, _R]: + """ + vmap is the vectorizing map; ``vmap(func)`` returns a new function that + maps ``func`` over some dimension of the inputs. Semantically, vmap + pushes the map into PyTorch operations called by ``func``, effectively + vectorizing those operations. + + vmap is useful for handling batch dimensions: one can write a function + ``func`` that runs on examples and then lift it to a function that can + take batches of examples with ``vmap(func)``. vmap can also be used to + compute batched gradients when composed with autograd. + + .. note:: + :func:`torch.vmap` is aliased to :func:`torch.func.vmap` for + convenience. Use whichever one you'd like. + + Args: + func (function): A Python function that takes one or more arguments. + Must return one or more Tensors. + in_dims (int or nested structure): Specifies which dimension of the + inputs should be mapped over. ``in_dims`` should have a + structure like the inputs. If the ``in_dim`` for a particular + input is None, then that indicates there is no map dimension. + Default: 0. + out_dims (int or Tuple[int]): Specifies where the mapped dimension + should appear in the outputs. If ``out_dims`` is a Tuple, then + it should have one element per output. Default: 0. + randomness (str): Specifies whether the randomness in this + vmap should be the same or different across batches. If 'different', + the randomness for each batch will be different. If 'same', the + randomness will be the same across batches. If 'error', any calls to + random functions will error. Default: 'error'. WARNING: this flag + only applies to random PyTorch operations and does not apply to + Python's random module or numpy randomness. + chunk_size (None or int): If None (default), apply a single vmap over inputs. + If not None, then compute the vmap :attr:`chunk_size` samples at a time. + Note that :attr:`chunk_size=1` is equivalent to computing the vmap with a for-loop. + If you run into memory issues computing the vmap, please try a non-None chunk_size. + + Returns: + Returns a new "batched" function. It takes the same inputs as + ``func``, except each input has an extra dimension at the index + specified by ``in_dims``. It takes returns the same outputs as + ``func``, except each output has an extra dimension at the index + specified by ``out_dims``. + + .. warning: + :func:`vmap` works best with functional-style code. Please do not + perform any side-effects in ``func``, with the exception of + in-place PyTorch operations. Examples of side-effects include mutating + Python data structures and assigning values to variables not captured + in ``func``. + + One example of using :func:`vmap` is to compute batched dot products. PyTorch + doesn't provide a batched ``torch.dot`` API; instead of unsuccessfully + rummaging through docs, use :func:`vmap` to construct a new function. + + >>> torch.dot # [D], [D] -> [] + >>> batched_dot = torch.func.vmap(torch.dot) # [N, D], [N, D] -> [N] + >>> x, y = torch.randn(2, 5), torch.randn(2, 5) + >>> batched_dot(x, y) + + :func:`vmap` can be helpful in hiding batch dimensions, leading to a simpler + model authoring experience. + + >>> batch_size, feature_size = 3, 5 + >>> weights = torch.randn(feature_size, requires_grad=True) + >>> + >>> def model(feature_vec): + >>> # Very simple linear model with activation + >>> return feature_vec.dot(weights).relu() + >>> + >>> examples = torch.randn(batch_size, feature_size) + >>> result = torch.vmap(model)(examples) + + :func:`vmap` can also help vectorize computations that were previously difficult + or impossible to batch. One example is higher-order gradient computation. + The PyTorch autograd engine computes vjps (vector-Jacobian products). + Computing a full Jacobian matrix for some function f: R^N -> R^N usually + requires N calls to ``autograd.grad``, one per Jacobian row. Using :func:`vmap`, + we can vectorize the whole computation, computing the Jacobian in a single + call to ``autograd.grad``. + + >>> # Setup + >>> N = 5 + >>> f = lambda x: x**2 + >>> x = torch.randn(N, requires_grad=True) + >>> y = f(x) + >>> I_N = torch.eye(N) + >>> + >>> # Sequential approach + >>> jacobian_rows = [torch.autograd.grad(y, x, v, retain_graph=True)[0] + >>> for v in I_N.unbind()] + >>> jacobian = torch.stack(jacobian_rows) + >>> + >>> # vectorized gradient computation + >>> def get_vjp(v): + >>> return torch.autograd.grad(y, x, v) + >>> jacobian = torch.vmap(get_vjp)(I_N) + + :func:`vmap` can also be nested, producing an output with multiple batched dimensions + + >>> torch.dot # [D], [D] -> [] + >>> batched_dot = torch.vmap( + ... torch.vmap(torch.dot) + ... ) # [N1, N0, D], [N1, N0, D] -> [N1, N0] + >>> x, y = torch.randn(2, 3, 5), torch.randn(2, 3, 5) + >>> batched_dot(x, y) # tensor of size [2, 3] + + If the inputs are not batched along the first dimension, ``in_dims`` specifies + the dimension that each inputs are batched along as + + >>> torch.dot # [N], [N] -> [] + >>> batched_dot = torch.vmap(torch.dot, in_dims=1) # [N, D], [N, D] -> [D] + >>> x, y = torch.randn(2, 5), torch.randn(2, 5) + >>> batched_dot( + ... x, y + ... ) # output is [5] instead of [2] if batched along the 0th dimension + + If there are multiple inputs each of which is batched along different dimensions, + ``in_dims`` must be a tuple with the batch dimension for each input as + + >>> torch.dot # [D], [D] -> [] + >>> batched_dot = torch.vmap(torch.dot, in_dims=(0, None)) # [N, D], [D] -> [N] + >>> x, y = torch.randn(2, 5), torch.randn(5) + >>> batched_dot( + ... x, y + ... ) # second arg doesn't have a batch dim because in_dim[1] was None + + If the input is a Python struct, ``in_dims`` must be a tuple containing a struct + matching the shape of the input: + + >>> f = lambda dict: torch.dot(dict["x"], dict["y"]) + >>> x, y = torch.randn(2, 5), torch.randn(5) + >>> input = {"x": x, "y": y} + >>> batched_dot = torch.vmap(f, in_dims=({"x": 0, "y": None},)) + >>> batched_dot(input) + + By default, the output is batched along the first dimension. However, it can be batched + along any dimension by using ``out_dims`` + + >>> f = lambda x: x**2 + >>> x = torch.randn(2, 5) + >>> batched_pow = torch.vmap(f, out_dims=1) + >>> batched_pow(x) # [5, 2] + + For any function that uses kwargs, the returned function will not batch the kwargs but will + accept kwargs + + >>> x = torch.randn([2, 5]) + >>> def fn(x, scale=4.): + >>> return x * scale + >>> + >>> batched_pow = torch.vmap(fn) + >>> assert torch.allclose(batched_pow(x), x * 4) + >>> batched_pow(x, scale=x) # scale is not batched, output has shape [2, 2, 5] + + .. note:: + vmap does not provide general autobatching or handle variable-length + sequences out of the box. + """ + from torch.compiler import is_compiling + + _check_randomness_arg(randomness) + if not (chunk_size is None or chunk_size > 0): + raise ValueError( + f"vmap: chunk_size should be None or greater than 0. (got {chunk_size})" + ) + + def wrapped(*args: _P.args, **kwargs: _P.kwargs) -> _R: + # pyrefly: ignore[bad-argument-type] + return vmap_impl( + # pyrefly: ignore[bad-argument-type] + func, + in_dims, + out_dims, + randomness, + chunk_size, + *args, + **kwargs, + ) + + if not is_compiling(): + wrapped = functools.wraps(func)(wrapped) + + return wrapped + + +def chunk_vmap( + func: Callable[_P, _R], + in_dims: in_dims_t = 0, + out_dims: out_dims_t = 0, + randomness: str = "error", + chunks: int = 2, +) -> Callable[_P, _R]: + """ + chunk_vmap is the vectorizing map (vmap) using chunks of input data. It is a mix of vmap (which vectorizes + everything) and map (which executes things sequentially). ``chunk_vmap`` vectorizes the input with number of + chunks at a time. For more details about vectorizing map, see :func:`vmap`. + + .. note:: + Please use :func:`vmap` with ``chunk_size`` argument instead of this API. + + Args: + func (function): A Python function that takes one or more arguments. + Must return one or more Tensors. + in_dims (int or nested structure): Specifies which dimension of the + inputs should be mapped over. ``in_dims`` should have a + structure like the inputs. If the ``in_dim`` for a particular + input is None, then that indicates there is no map dimension. + Default: 0. + out_dims (int or Tuple[int]): Specifies where the mapped dimension + should appear in the outputs. If ``out_dims`` is a Tuple, then + it should have one element per output. Default: 0. + randomness (str): Specifies whether the randomness in this + vmap should be the same or different across batches. If 'different', + the randomness for each batch will be different. If 'same', the + randomness will be the same across batches. If 'error', any calls to + random functions will error. Default: 'error'. WARNING: this flag + only applies to random PyTorch operations and does not apply to + Python's random module or numpy randomness. + chunks (int): Number of chunks to use to split the input data. Default is 2. + If equals to 1 then :func:`vmap` is called. + + Returns: + Returns a new "batched" function. It takes the same inputs as + ``func``, except each input has an extra dimension at the index + specified by ``in_dims``. It takes returns the same outputs as + ``func``, except each output has an extra dimension at the index + specified by ``out_dims``. + """ + _check_randomness_arg(randomness) + + if chunks == 1: + return vmap(func, in_dims=in_dims, out_dims=out_dims, randomness=randomness) + + def _get_chunk_flat_args( + flat_args_: Iterable[Any], + flat_in_dims_: Iterable[int | None], + chunks_: int, + ) -> Iterable[Any]: + flat_args_chunks = tuple( + t.chunk(chunks_, dim=in_dim) + if in_dim is not None + else [ + t, + ] + * chunks_ + for t, in_dim in zip(flat_args_, flat_in_dims_) + ) + # transpose chunk dim and flatten structure + # chunks_flat_args is a list of flatten args + chunks_flat_args = zip(*flat_args_chunks) + return chunks_flat_args + + @functools.wraps(func) + def wrapped_with_chunks(*args: _P.args, **kwargs: _P.kwargs) -> _R: + _check_out_dims_is_int_or_int_pytree(out_dims, func) + _, flat_in_dims, flat_args, args_spec = _process_batched_inputs( + in_dims, args, func + ) + # Chunk flat arguments + chunks_flat_args = _get_chunk_flat_args(flat_args, flat_in_dims, chunks) + + # Apply vmap on chunks + return _chunked_vmap( + # pyrefly: ignore[bad-argument-type] + func, + flat_in_dims, + chunks_flat_args, + args_spec, + out_dims, + randomness, + **kwargs, + ) + + return wrapped_with_chunks + + +# TODO: Improve the return type of this function +@exposed_in("torch.func") +def grad( + func: Callable[_P, Any], argnums: argnums_t = 0, has_aux: bool = False +) -> Callable[_P, Any]: + """``grad`` operator helps computing gradients of ``func`` with respect to the + input(s) specified by ``argnums``. This operator can be nested to + compute higher-order gradients. + + Args: + func (Callable): A Python function that takes one or more arguments. + Must return a single-element Tensor. If specified ``has_aux`` equals ``True``, + function can return a tuple of single-element Tensor and other auxiliary objects: + ``(output, aux)``. + argnums (int or Tuple[int]): Specifies arguments to compute gradients with respect to. + ``argnums`` can be single integer or tuple of integers. Default: 0. + has_aux (bool): Flag indicating that ``func`` returns a tensor and other + auxiliary objects: ``(output, aux)``. Default: False. + + Returns: + Function to compute gradients with respect to its inputs. By default, the output of + the function is the gradient tensor(s) with respect to the first argument. + If specified ``has_aux`` equals ``True``, tuple of gradients and output auxiliary objects + is returned. If ``argnums`` is a tuple of integers, a tuple of output gradients with + respect to each ``argnums`` value is returned. + + Example of using ``grad``: + + >>> # xdoctest: +SKIP + >>> from torch.func import grad + >>> x = torch.randn([]) + >>> cos_x = grad(lambda x: torch.sin(x))(x) + >>> assert torch.allclose(cos_x, x.cos()) + >>> + >>> # Second-order gradients + >>> neg_sin_x = grad(grad(lambda x: torch.sin(x)))(x) + >>> assert torch.allclose(neg_sin_x, -x.sin()) + + When composed with ``vmap``, ``grad`` can be used to compute per-sample-gradients: + + >>> # xdoctest: +SKIP + >>> from torch.func import grad, vmap + >>> batch_size, feature_size = 3, 5 + >>> + >>> def model(weights, feature_vec): + >>> # Very simple linear model with activation + >>> assert feature_vec.dim() == 1 + >>> return feature_vec.dot(weights).relu() + >>> + >>> def compute_loss(weights, example, target): + >>> y = model(weights, example) + >>> return ((y - target) ** 2).mean() # MSELoss + >>> + >>> weights = torch.randn(feature_size, requires_grad=True) + >>> examples = torch.randn(batch_size, feature_size) + >>> targets = torch.randn(batch_size) + >>> inputs = (weights, examples, targets) + >>> grad_weight_per_example = vmap(grad(compute_loss), in_dims=(None, 0, 0))( + ... *inputs + ... ) + + Example of using ``grad`` with ``has_aux`` and ``argnums``: + + >>> # xdoctest: +SKIP + >>> from torch.func import grad + >>> def my_loss_func(y, y_pred): + >>> loss_per_sample = (0.5 * y_pred - y) ** 2 + >>> loss = loss_per_sample.mean() + >>> return loss, (y_pred, loss_per_sample) + >>> + >>> fn = grad(my_loss_func, argnums=(0, 1), has_aux=True) + >>> y_true = torch.rand(4) + >>> y_preds = torch.rand(4, requires_grad=True) + >>> out = fn(y_true, y_preds) + >>> # > output is ((grads w.r.t y_true, grads w.r.t y_preds), (y_pred, loss_per_sample)) + + .. note:: + Using PyTorch ``torch.no_grad`` together with ``grad``. + + Case 1: Using ``torch.no_grad`` inside a function: + + >>> # xdoctest: +SKIP + >>> def f(x): + >>> with torch.no_grad(): + >>> c = x ** 2 + >>> return x - c + + In this case, ``grad(f)(x)`` will respect the inner ``torch.no_grad``. + + Case 2: Using ``grad`` inside ``torch.no_grad`` context manager: + + >>> # xdoctest: +SKIP + >>> with torch.no_grad(): + >>> grad(f)(x) + + In this case, ``grad`` will respect the inner ``torch.no_grad``, but not the + outer one. This is because ``grad`` is a "function transform": its result + should not depend on the result of a context manager outside of ``f``. + + """ + # To avoid cyclical dependency. + import torch._functorch.eager_transforms as eager_transforms + from torch.compiler import is_compiling + + def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> tuple[Any, torch.Tensor]: + return eager_transforms.grad_impl(func, argnums, has_aux, args, kwargs) + + if not is_compiling(): + wrapper = functools.wraps(func)(wrapper) + + return wrapper + + +# TODO: Improve the return type of this function +@exposed_in("torch.func") +def grad_and_value( + func: Callable[_P, Any], argnums: argnums_t = 0, has_aux: bool = False +) -> Callable[_P, tuple[Any, Any]]: + """ + Returns a function to compute a tuple of the gradient and primal, or + forward, computation. + + Args: + func (Callable): A Python function that takes one or more arguments. + Must return a single-element Tensor. If specified ``has_aux`` + equals ``True``, function can return a tuple of single-element + Tensor and other auxiliary objects: ``(output, aux)``. + argnums (int or Tuple[int]): Specifies arguments to compute gradients + with respect to. ``argnums`` can be single integer or tuple of + integers. Default: 0. + has_aux (bool): Flag indicating that ``func`` returns a tensor and + other auxiliary objects: ``(output, aux)``. Default: False. + + Returns: + Function to compute a tuple of gradients with respect to its inputs + and the forward computation. By default, the output of the function is + a tuple of the gradient tensor(s) with respect to the first argument + and the primal computation. If specified ``has_aux`` equals + ``True``, tuple of gradients and tuple of the forward computation with + output auxiliary objects is returned. If ``argnums`` is a tuple of + integers, a tuple of a tuple of the output gradients with respect to + each ``argnums`` value and the forward computation is returned. + + See :func:`grad` for examples + """ + from torch._functorch import eager_transforms + from torch.compiler import is_compiling + + def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> tuple[Any, torch.Tensor]: + return eager_transforms.grad_and_value_impl( + func, argnums, has_aux, args, kwargs + ) + + if not is_compiling(): + wrapper = functools.wraps(func)(wrapper) + + return wrapper diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/autograd_function.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/autograd_function.py new file mode 100644 index 0000000000000000000000000000000000000000..2ae0262cceff1756a417c8d69dfc0327ea8a93f7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/autograd_function.py @@ -0,0 +1,909 @@ +from __future__ import annotations + +from typing import Any, NamedTuple, TYPE_CHECKING +from typing_extensions import ParamSpec, TypeVar + +import torch +import torch.utils._pytree as pytree +from torch._C._functorch import ( + _unwrap_for_grad, + _wrap_for_grad, + current_level, + TransformType, +) +from torch._functorch.apis import vmap +from torch._functorch.utils import enable_single_level_autograd_function +from torch._functorch.vmap import ( + _add_batch_dim, + _broadcast_to_and_flatten, + restore_vmap, + unwrap_batched, + wrap_batched, +) +from torch._ops import HigherOrderOperator +from torch.autograd.forward_ad import _set_fwd_grad_enabled + + +if TYPE_CHECKING: + from collections.abc import Callable, Iterable, Sequence + + from torch._functorch.pyfunctorch import FuncTorchInterpreter, VmapInterpreter + +_P = ParamSpec("_P") +_R = TypeVar("_R") + + +# autograd.Function technically runs before the regular PyTorch dispatcher. +# This is how features like autocast and torch_dispatch (e.g. PythonTLSSnapshot) +# work with it. One day we might decide to change this, but until then, +# we need to give the illusion that autograd.Function runs before those things. +# +# We do this by using creating a custom HigherOrderOperator that only functorch +# dispatches specially. +class CustomFunctionHigherOrderOperator(HigherOrderOperator): + def __init__(self) -> None: + super().__init__("custom_function_call") + + def __call__( + self, + autograd_function: type[torch.autograd.Function], + *args: _P.args, + **kwargs: _P.kwargs, + ) -> Any: + # When custom_function_call is done dispatching through functorch, + # it should just invoke the autograd.Function. This is consistent + # with the autograd.Function behavior of being invoked before the + # PyTorch dispatcher. + # + # This will lead us into trouble later down the line, but this is + # pre-existing. There is an invariant that a function traced by + # make_fx should have the same behavior when provided the same + # Tensor. However, make_fx sees autograd.Function as a composite + # (because autograd.Function happens before the Python dispatch key) + # and only traces the forward pass. + if torch._C._are_functorch_transforms_active(): + # pyrefly: ignore [missing-attribute] + return super().__call__(autograd_function, *args, **kwargs) + return autograd_function.apply(*args, **kwargs) + + +# "custom_function_call" +# This is the mechanism for an autograd.Function that works with functorch transforms. +# It wraps an autograd.Function; interactions with functorch transforms are defined +# via PyDispatcher and HigherOrderOperator rather than through the traditional PyTorch +# dispatcher. +custom_function_call = CustomFunctionHigherOrderOperator() + + +# The grad rule for custom_function_call is to construct a new _SingleLevelFunction +# (autograd.Function that only works with a single layer (level) of functorch) that: +# - unwraps the inputs +# - redispatches to custom_function_call +# - wraps the outputs +# and whose backward pass calls the original autograd.Function's backward. +# +# Why do we need to redispatch to custom_function_call? +# ----------------------------------------------------- +# This is consistent with how ATen operators work with functorch's grad transform: +# they always redispatch to the original operator. +# Consider torch.sin, and let's say we do grad0(grad1(torch.sin))(x) +# +# grad1 will: +# - set up the autograd graph +# - unwrap the inputs +# - redispatch to at::sin (*) +# - rewrap the outputs on the return +# +# On the redispatch in (*), grad0 will: +# - set up the autograd graph +# - unwrap the inputs +# - redispatch to at::sin +# - rewrap the outputs on the return +# +# To "set up the autograd graph", we generate a _SingleLevelFunction +# and apply it. +@custom_function_call.py_impl(TransformType.Grad) +@custom_function_call.py_impl(TransformType.Jvp) +def custom_function_call_grad( + interpreter: FuncTorchInterpreter, + autograd_function: type[torch.autograd.Function], + *operands: Any, +) -> Any: + Generated = generate_single_level_function(interpreter, autograd_function) + with enable_single_level_autograd_function(): + # pyrefly: ignore [missing-attribute] + flat_out = Generated.apply(*operands) + return flat_out + + +def generate_single_level_function( + interpreter: FuncTorchInterpreter, + autograd_function: type[torch.autograd.Function], +) -> type[torch.autograd.function._SingleLevelFunction]: + level = interpreter.level() + + def forward(*operands: Any) -> Any: + unwrapped_operands = pytree.tree_map_only( + torch.Tensor, lambda x: _unwrap_for_grad(x, level), operands + ) + # Both enable_grad() and _set_fwd_grad_enabled() are necessary no matter + # the transform. _SingleLevelFunction will turn off both fwd and bwd + # gradient computation and we need to turn it back on here. + with torch.enable_grad(), _set_fwd_grad_enabled(True), interpreter.lower(): + unwrapped_output = custom_function_call( + autograd_function, *unwrapped_operands + ) + + # See NOTE [mark_dirty object identity check] + def wrap_fn(output: torch.Tensor) -> torch.Tensor: + return _wrap_for_grad(output, level) + + return wrap_outputs_maintaining_identity( + unwrapped_output, unwrapped_operands, operands, wrap_fn + ) + + def setup_context(ctx: Any, inputs: Any, output: Any) -> Any: + return autograd_function.setup_context(ctx, inputs, output) + + # backward is only used if the transform is TransformType.Grad + def backward(ctx: Any, *grads: Any) -> Any: + result = autograd_function.backward(ctx, *grads) + return result + + # jvp is only used if the transform is TransformType.Jvp + def jvp(ctx: Any, *tangents: Any) -> Any: + result = autograd_function.jvp(ctx, *tangents) + return result + + # This is the sequence of magic words to dynamically generate a Subclass with + # a given name. A Tensor's .grad_fn field has a class name that is the original + # autograd.Function's name + Backward, so we do this to generate some + # meaningful name. + name = f"{autograd_function.__name__}Generated" + Generated = type( + name, + (torch.autograd.function._SingleLevelFunction,), + { + "forward": staticmethod(forward), + "backward": staticmethod(backward), + "jvp": staticmethod(jvp), + "setup_context": staticmethod(setup_context), + }, + ) + return Generated + + +# wrap_outputs_maintaining_identity handles outputs from the vmap, +# backward (vjp), and jvp staticmethod. The way it distinguishes +# between the vmap case and the {backward, jvp} case is if the out_dims +# are specified or not. +# +# NB: we cannot use out_dims=None as the deciding factor. This because +# out_dims=None can still happen in the vmap staticmethod! What the +# user is saying in that case is that their output does not have a +# dimension that is being vmapped over, which is valid. +NO_OUT_DIMS = "not specified" + + +# NOTE [mark_dirty object identity check] +# autograd.Function's ctx.mark_dirty expect a returned input +# to have the same object identity as the input. +# Mode-only functorch will greatly simplify this logic. +def wrap_outputs_maintaining_identity( + outputs: Any, + unwrapped_inputs: Any, + orig_inputs: Any, + wrap_fn: Callable[..., Any], + out_dims: Any = NO_OUT_DIMS, +) -> Any: + flat_unwrapped_inputs = pytree.arg_tree_leaves(*unwrapped_inputs) + flat_orig_inputs = pytree.arg_tree_leaves(*orig_inputs) + + unwrapped_input_to_orig_input = { + id(unwrapped): orig + for unwrapped, orig in zip(flat_unwrapped_inputs, flat_orig_inputs) + } + + flat_outputs, spec = pytree.tree_flatten(outputs) + result = [] + + out_dims_specified = out_dims != NO_OUT_DIMS + + flat_out_dims = None + if out_dims_specified: + flat_out_dims = _broadcast_to_and_flatten(out_dims, spec) + # _broadcast_to_and_flatten returns None if it is unable to broadcast. + # TODO: update following link from master to stable once that's out + if flat_out_dims is None: + raise RuntimeError( + f"The autograd.Function's vmap staticmethod returned an " + f"incompatible (output, out_dims) tuple. " + f"Expected out_dims={out_dims} " + f"to be compatible with the structure of `output`. " + f"out_dims has structure {pytree.tree_flatten(out_dims)[1]} " + f"but output has structure {spec}. " + f"For more details, please see " + f"https://pytorch.org/docs/main/notes/extending.func.html" + ) + + for i, output in enumerate(flat_outputs): + if not isinstance(output, torch.Tensor): + result.append(output) + continue + if id(output) in unwrapped_input_to_orig_input: + result.append(unwrapped_input_to_orig_input[id(output)]) + continue + if out_dims_specified: + if flat_out_dims is None: + raise AssertionError( + "flat_out_dims must not be None when out_dims is specified" + ) + result.append(wrap_fn(output, flat_out_dims[i])) + else: + result.append(wrap_fn(output)) + + return pytree.tree_unflatten(result, spec) + + +# NOTE: [functorch vjp and autograd interaction] +# There's an edge case with the functorch vjp and autograd interaction +# that will eventually be fixed by mode-only functorch. +# The TL;DR is that there's no way to unwrap a dead GradTensorWrapper, +# so we (the framework) need to do it manually. Regular PyTorch operators +# automatically do so this is consistent. +# +# class MyExp(torch.autograd.Function): +# @staticmethod +# def forward(x): +# return x.exp() +# +# @staticmethod +# def setup_context(ctx, inputs, output): +# y = output +# ctx.save_for_backward(y) +# +# @staticmethod +# def backward(gy): +# y, = ctx.saved_tensors() +# return MyMul.apply(gy, y) +# +# x = torch.randn([], requires_grad=True) +# gy = torch.randn([], requires_grad=True) +# _, vjp_fn = vjp(MySin.apply, x) +# result = vjp_fn(gy) +# +# MyMul is an autograd.Function that is not shown here. +# It saves a `y` for backward (since gy requires grad). +# +# in vjp_fn(gy), we get: +# > MyMul.apply(gy, GradTensorWrapper(y, level=dead)) +# Because the y that is saved for backward by MyExp is a GradTensorWrapper +# but is now dead since we are outside the vjp context. +# +# PyTorch dispatcher operations, upon seeing a dead GradTensorWrapper, +# will automatically unwrap the GradTensorWrapper when applied. +# But since autograd.Function technically sits above the regular PyTorch +# dispatcher, it doesn't get this treatment. So we manually do +# the unwrapping to be consistent with regular PyTorch dispatcher operations. + + +class VmapInfo(NamedTuple): + batch_size: int + randomness: str + + +def has_overridden_vmap_rule( + autograd_function: type[torch.autograd.Function], +) -> bool: + return autograd_function.vmap is not torch.autograd.Function.vmap + + +def validate_vmap_returns_tuple_of_two_elements(result: Any) -> None: + base_error_msg = ( + "Expected the vmap staticmethod to have two returns, an output " + "and out_dims with pytree structure compatible with the output. " + ) + if not isinstance(result, tuple): + raise RuntimeError(base_error_msg + f"Got a {type(result)} instead") + if not len(result) == 2: + raise RuntimeError(base_error_msg + f"Got {len(result)} returns instead") + + +@custom_function_call.py_impl(TransformType.Vmap) +def custom_function_call_vmap( + interpreter: VmapInterpreter, + autograd_function: type[torch.autograd.Function], + *operands: Any, + **kwargs: Any, +) -> Any: + if any( + isinstance(val, torch.Tensor) + for val in torch.utils._pytree.tree_flatten(kwargs)[0] + ): + raise NotImplementedError( + f"Run vmap on autograd.Function with kwarg-only Tensor args. " + f"Please do not pass kwarg-only Tensors to autograd.Function. " + f"Got: {kwargs}" + ) + + if autograd_function.generate_vmap_rule: + if has_overridden_vmap_rule(autograd_function): + # TODO: Update link to stable once that's out + # https://github.com/pytorch/pytorch/issues/92029 + raise RuntimeError( + f"You tried to vmap over {autograd_function.__name__}, but " + f"it has both generate_vmap_rule=True and an overridden vmap " + f"staticmethod. Please set generate_vmap_rule=False or delete " + f"the overridden vmap staticmethod to avoid ambiguity. " + f"For more details, please see " + f"https://pytorch.org/docs/main/notes/extending.func.html" + ) + return custom_function_call_vmap_generate_rule( + interpreter, autograd_function, *operands + ) + + if not has_overridden_vmap_rule(autograd_function): + # TODO: Update link to stable once that's out + # https://github.com/pytorch/pytorch/issues/92029 + raise RuntimeError( + f"You tried to vmap over {autograd_function.__name__}, but " + f"it does not have vmap support. Please override and implement the " + f"vmap staticmethod or set generate_vmap_rule=True. " + f"For more details, please see " + f"https://pytorch.org/docs/main/notes/extending.func.html" + ) + + return custom_function_call_vmap_helper( + interpreter, autograd_function.vmap, autograd_function, *operands, **kwargs + ) + + +def custom_function_call_vmap_helper( + interpreter: VmapInterpreter, + vmap_function: Callable[..., Any], + op: Any, + *operands: Any, + **kwargs: Any, +) -> Any: + current_level = interpreter.level() + info = VmapInfo( + batch_size=interpreter.batch_size(), + randomness=interpreter.randomness(), + ) + # We're either in the autograd.Function case (vmap staticmethod) + # or the torch.library.register_vmap case. + autograd_function_case = isinstance(op, torch.autograd.function.FunctionMeta) + + def lower_to_next() -> Any: + if autograd_function_case: + return interpreter.lower() + else: + return torch._C._ExcludeDispatchKeyGuard( + torch._C.DispatchKeySet(torch._C.DispatchKey.FuncTorchBatched) + ) + + unwrapped_operands, in_dims = unwrap_batched(operands, current_level) + # If none of the tensors are batched at the current level, then we skip the + # current level. This saves the user from needing to handle this case in + # their vmap staticmethod (and is consistent with our C++ batching rule API) + if pytree.tree_all(lambda dim: dim is None, in_dims): + with lower_to_next(): + if autograd_function_case: + return custom_function_call(op, *operands) + else: + return op(*operands, **kwargs) + + with lower_to_next(): + result = vmap_function(info, in_dims, *unwrapped_operands, **kwargs) + validate_vmap_returns_tuple_of_two_elements(result) + unwrapped_output, out_dims = result + + # See NOTE [mark_dirty object identity check] + def wrap_fn(output: torch.Tensor, out_dim: int | None) -> torch.Tensor: + return ( + output + if out_dim is None + else _add_batch_dim(output, out_dim, current_level) + ) + + return wrap_outputs_maintaining_identity( + unwrapped_output, unwrapped_operands, operands, wrap_fn, out_dims=out_dims + ) + + +def unpack_outputs(outputs: tuple[Any, ...]) -> tuple[Any, Any]: + out_dims = outputs[-1] + if isinstance(out_dims, tuple): + outputs = outputs[:-1] + else: + outputs = outputs[0] + return outputs, out_dims + + +def custom_function_call_vmap_generate_rule( + interpreter: VmapInterpreter, + autograd_function: type[torch.autograd.Function], + *operands: Any, +) -> Any: + unwrapped_operands, in_dims = unwrap_batched(operands, interpreter.level()) + vmapped_function = vmapify_autograd_function( + autograd_function, + in_dims, + interpreter.batch_size(), + interpreter.randomness(), + ) + with interpreter.lower(): + outputs = custom_function_call(vmapped_function, *unwrapped_operands) + + if not isinstance(outputs, tuple): + raise AssertionError(f"expected outputs to be a tuple, got {type(outputs)}") + outputs, out_dims = unpack_outputs(outputs) + return wrap_batched(outputs, out_dims, interpreter.level()) + + +@custom_function_call.py_impl(TransformType.Functionalize) +def custom_function_call_functionalize( + interpreter: FuncTorchInterpreter, + autograd_function: type[torch.autograd.Function], + generate_vmap_rule: bool, + *operands: Any, +) -> Any: + raise RuntimeError("NYI: Functionalize rule for custom_function_call") + + +def vmapify_autograd_function( + autograd_function: type[torch.autograd.Function], + in_dims: Any, + batch_size: int, + randomness: str, +) -> type[torch.autograd.Function]: + def forward(*operands: Any) -> Any: + outputs, out_dims = restore_vmap( + autograd_function.forward, in_dims, batch_size, randomness + )(*operands) + if isinstance(outputs, torch.Tensor): + return outputs, out_dims + else: + return *outputs, out_dims + + def setup_context(ctx: Any, inputs: Any, outputs: Any) -> None: + outputs, out_dims = unpack_outputs(outputs) + key = id(Generated) + + def inner(inputs: Any, outputs: Any) -> None: + # wrapped_ctx.save_for_backward will: + # - unwrap batchedtensors into (tensor, bdim) + # - save_for_backward(*unwrapped_tensors) + # - assign the bdims to wrapped_ctx._pt_saved_tensors_bdims + wrapped_ctx = CtxCustomSave(ctx, current_level()) + autograd_function.setup_context(wrapped_ctx, inputs, outputs) + + # input_shapes are used for reductify later to reduce expanded gradients + # to the correct shape. + # See NOTE: [Why can't we rely on autograd to reduce expanded gradients?] + # for more details + input_shapes = tuple( + inp.shape if isinstance(inp, torch.Tensor) else None for inp in inputs + ) + if not hasattr(ctx, "_pt_input_shapes"): + # pyrefly: ignore [implicit-any] + ctx._pt_input_shapes = {} + ctx._pt_input_shapes.update({key: input_shapes}) + + if not hasattr(ctx, "_pt_saved_tensors_bdims_stack"): + # pyrefly: ignore [implicit-any] + ctx._pt_saved_tensors_bdims_stack = {} + ctx._pt_saved_tensors_bdims_stack.update( + {key: (wrapped_ctx._pt_saved_tensors_bdims)} + ) + + # See NOTE: [Why do we need to run setup_context under a vmap?] + restore_vmap( + inner, + (in_dims, out_dims), + batch_size, + randomness, + )(inputs, outputs) + + if not hasattr(ctx, "_pt_out_dims"): + # pyrefly: ignore [implicit-any] + ctx._pt_out_dims = {} + ctx._pt_out_dims.update({key: out_dims}) + + def jvp(ctx: Any, *tangents: Any) -> Any: + key = id(Generated) + + def jvp_no_context(saved_tensors: Any, tangents: Any) -> Any: + wrapped_ctx = CtxWithSavedTensors(ctx, saved_tensors) + return autograd_function.jvp(wrapped_ctx, *tangents) + + tangent_in_dims = get_tangents_in_dims(in_dims, tangents) + out_tangents, out_tangents_dims = restore_vmap( + jvp_no_context, + (ctx._pt_saved_tensors_bdims_stack[key], tangent_in_dims), + batch_size, + randomness, + )(ctx.saved_tensors, tangents) + + result = reductify( + out_tangents, out_tangents_dims, ctx._pt_out_dims[key], batch_size + ) + if isinstance(result, torch.Tensor): + return result, None + else: + return *result, None + + def backward(ctx: Any, *grad_outputs: Any) -> Any: + key = id(Generated) + grad_outputs_ = grad_outputs[:-1] + grad_outputs_in_dims = ctx._pt_out_dims[key] + + if not isinstance(grad_outputs_in_dims, tuple): + grad_outputs_in_dims = (grad_outputs_in_dims,) + + grad_outputs_in_dims = tuple( + in_dim if grad_output is not None else None + for grad_output, in_dim in zip(grad_outputs_, grad_outputs_in_dims) + ) + + def backward_no_context(inputs: Any) -> Any: + saved_tensors, grad_outputs = inputs + wrapped_ctx = CtxWithSavedTensors(ctx, saved_tensors) + return autograd_function.backward(wrapped_ctx, *grad_outputs) + + grad_ins, grad_ins_dims = restore_vmap( + backward_no_context, + ((ctx._pt_saved_tensors_bdims_stack[key], grad_outputs_in_dims),), + batch_size, + randomness, + )((ctx.saved_tensors, grad_outputs_)) + result = reductify( + grad_ins, grad_ins_dims, in_dims, batch_size, ctx._pt_input_shapes[key] + ) + return result + + name = f"Vmapped{autograd_function.__name__}" + Generated = type( + name, + (torch.autograd.Function,), + { + "forward": staticmethod(forward), + "backward": staticmethod(backward), + "jvp": staticmethod(jvp), + "setup_context": staticmethod(setup_context), + "generate_vmap_rule": True, + }, + ) + + return Generated + + +# tangents might be None, so we need to replace +# the corresponding in_dims with None. +def get_tangents_in_dims(input_dims: Any, tangents: tuple[Any, ...]) -> Any: + flat_in_dims, spec = pytree.tree_flatten(input_dims) + flat_tangents = pytree.arg_tree_leaves(*tangents) + result = [ + None if tangent is None else in_dim + for in_dim, tangent in zip(flat_in_dims, flat_tangents) + ] + return pytree.tree_unflatten(result, spec) + + +# NOTE: [Why do we need to run setup_context under a vmap?] +# Consider the following autograd.Function +# +# class Sum(torch.autograd.Function): +# @staticmethod +# def forward(x): +# return x.sum() +# @staticmethod +# def setup_context(ctx, inputs, outputs): +# ctx.x_shape = inputs[0] +# @staticmethod +# def backward(ctx, gy): +# return gy.expand(ctx.x_shape) +# +# x = torch.randn(B, 4) +# in_dims = 0 +# vmap(Sum.apply, in_dims)(x) +# +# Let's assume for a moment that we didn't vmap setup_context in VmappedSum: +# +# class VmappedSum(torch.autograd.Function): +# @staticmethod +# def forward(x): +# return vmap(Sum.forward, in_dims)(x) +# +# @staticmethod +# def setup_context(ctx, inputs, outputs): +# Sum.setup_context(ctx, inputs, outputs) +# +# @staticmethod +# def backward(ctx, gy): +# def backward_no_context(gy): +# return gy.expand(ctx.x_shape) +# +# dims = (0,) +# gx = vmap(backward_no_context, dims)(gy) +# return gx +# +# We end up saving [B, 4] as x_shape. In the backward, gy has shape [B], +# and we're doing: +# +# def backward_no_context(gy): +# return gy.expand([B, 4]) +# +# gx = vmap(backward_no_context, dims)(gy: "Tensor[B]") +# +# This gives us the wrong result (gx has shape [B, B, 4], but it should +# have shape [4]). Performing vmap over setup_context means the shape +# saved has shape [4] and leads to a correct result shape for gx. + + +# Wraps a ctx object. Forwards all attr accesses to the underlying object +# except for the attrs in _pt_attrs +class WrappedCtx: + _pt_reserved_attrs: tuple[str, ...] = ("_pt_reserved_attrs", "_pt_inner_ctx") + + def __init__(self, ctx: Any) -> None: + if not isinstance(ctx, WrappedCtx): + reserved_attrs = type(self)._pt_reserved_attrs + for name in reserved_attrs: + if not hasattr(ctx, name): + continue + raise RuntimeError( + f"PyTorch reserves the {reserved_attrs} field on ctx. " + "Please name your fields on ctx something else to avoid name " + "collision." + ) + self._pt_inner_ctx = ctx + + def __getattr__(self, name: str) -> Any: + return getattr(self._pt_inner_ctx, name) + + def __setattr__(self, name: str, value: Any) -> None: + if name in type(self)._pt_reserved_attrs: + self.__dict__[name] = value + return + return setattr(self._pt_inner_ctx, name, value) + + +# Wraps ctx to create a new ctx object that overrides saved_tensors. +class CtxWithSavedTensors(WrappedCtx): + _pt_reserved_attrs = ("_pt_new_saved_tensors", *WrappedCtx._pt_reserved_attrs) + + def __init__(self, ctx: Any, new_saved_tensors: Sequence[torch.Tensor]) -> None: + super().__init__(ctx) + self._pt_new_saved_tensors = new_saved_tensors + + @property + def saved_tensors(self) -> Sequence[torch.Tensor]: + return self._pt_new_saved_tensors + + +class CtxCustomSave(WrappedCtx): + _pt_reserved_attrs = ( + "_pt_saved_tensors_bdims", + "_pt_current_level", + *WrappedCtx._pt_reserved_attrs, + ) + + def __init__(self, ctx: Any, current_level: int) -> None: + super().__init__(ctx) + self._pt_saved_tensors_bdims: tuple[Any, ...] = () + self._pt_current_level = current_level + + def save_for_backward(self, *tensors: torch.Tensor) -> None: + unwrapped_tensors, bdims = unwrap_batched(tensors, self._pt_current_level) + self._pt_inner_ctx.save_for_backward(*unwrapped_tensors) + self._pt_saved_tensors_bdims = bdims + + def save_for_forward(self, *tensors: torch.Tensor) -> None: + unwrapped_tensors, bdims = unwrap_batched(tensors, self._pt_current_level) + self._pt_inner_ctx.save_for_forward(*unwrapped_tensors) + self._pt_saved_tensors_bdims = bdims + + +def reductify( + grad_input: torch.Tensor | tuple[torch.Tensor, ...], + grad_input_bdim: int | tuple[int, ...], + input_bdim: int | tuple[int, ...], + batch_size: int, + target_shape_without_bdim_to_reduce_to: Any = None, +) -> tuple[Any, ...]: + if not isinstance(grad_input, tuple): + grad_input = (grad_input,) + if not isinstance(grad_input_bdim, tuple): + grad_input_bdim = (grad_input_bdim,) + if not isinstance(input_bdim, tuple): + input_bdim = (input_bdim,) + + if target_shape_without_bdim_to_reduce_to is None: + target_shape_without_bdim_to_reduce_to = len(grad_input) * (None,) + result = tuple( + reductify_leaf(gi, gi_bdim, i_bdim, batch_size, maybe_ishape) + for gi, gi_bdim, i_bdim, maybe_ishape in zip( + grad_input, + grad_input_bdim, + input_bdim, + target_shape_without_bdim_to_reduce_to, + ) + ) + return result + + +def reductify_leaf( + grad_input: torch.Tensor | None, + grad_input_bdim: int | None, + input_bdim: int | None, + batch_size: int, + target_shape_without_bdim_to_reduce_to: Any = None, +) -> torch.Tensor | None: + if grad_input is None: + return None + + if grad_input_bdim is None and input_bdim is None: + return grad_input + + if grad_input_bdim is not None and input_bdim is None: + return grad_input.sum(grad_input_bdim) + + # NOTE: [Why can't we rely on autograd to reduce expanded gradients?] + # For reverse-mode AD, + # given a grad_input and input, it is valid for the user to return a + # grad_input that has a broadcasted shape when compared to the input. + # In this situation, autograd automatically reduces the grad_input to + # the shape of the input. + # + # However, when input_bdim is not None, we have problems. + # + # [example 1] + # grad_input: Tensor[3, 4], input: Tensor[B, 4] + # We can expand grad_input to Tensor[B, 3, 4], but that isn't broadcastable + # from [B, 4]. + # + # [example 2] + # grad_input: Tensor[3, B, 4], input: Tensor[B, 4] + # We can swizzle grad_input to Tensor[B, 3, 4], but that isn't broadcastable + # from [B, 4]. + # + # This means that we need to also reduce the grad_input to the shape of the + # input. This behavior is controlled by the `target_shape_without_bdim_to_reduce_to` flag; + # if not-None then we do the reducing manually, otherwise, we do not do a reduction. + if input_bdim is None: + raise AssertionError("input_bdim must not be None") + + if grad_input_bdim is None: + grad_input = grad_input.unsqueeze(input_bdim) + new_shape = list(grad_input.shape) + new_shape[input_bdim] = batch_size + grad_input = grad_input.expand(new_shape) + grad_input_bdim = input_bdim + + if target_shape_without_bdim_to_reduce_to is not None: + return vmap( + torch.Tensor.sum_to_size, + in_dims=(grad_input_bdim, None), + out_dims=input_bdim, + )(grad_input, target_shape_without_bdim_to_reduce_to) + + if input_bdim != grad_input_bdim: + grad_input = grad_input.movedim(grad_input_bdim, input_bdim) + return grad_input + + +def autograd_function_forward_rewritten( + original_forward: Callable[_P, _R], + original_setup_context: Callable[..., Any], +) -> Callable[..., _R]: + def new_forward(ctx: Any, *args: _P.args, **kwargs: _P.kwargs) -> _R: + output = original_forward(*args, **kwargs) + original_setup_context(ctx, args, output) + return output + + return new_forward + + +class AutogradFunctionApply(HigherOrderOperator): + def __init__(self) -> None: + super().__init__("autograd_function_apply") + + def __call__( + self, + fwd: torch.fx.GraphModule, + bwd: torch.fx.GraphModule, + *fwd_args: Any, + **fwd_kwargs: Any, + ) -> Any: + saved_values: Iterable[Any] | None = None + non_differentiable_idx = fwd_kwargs["non_differentiable_idx"] + saved_for_backward_idx = fwd_kwargs["saved_for_backward_idx"] + + class ApplyTemplate(torch.autograd.Function): + @staticmethod + def forward(*args: Any, **kwargs: Any) -> Any: + nonlocal saved_values + + # The Interpreter here is required to propagate metadata + # from the dynamo graph body to the local_map graph body. + # This is required for fx_traceback.annotate for work. + output, saved_values = torch.fx.Interpreter(fwd).run(*args) + + # See Note [Activations with no version counter checks in eager] + # Mark tensors that came from ctx.save_for_backward with metadata. + # This allows AOT autograd to distinguish between tensors saved via + # save_for_backward vs those stashed directly on ctx (e.g., ctx.x = x). + from torch.fx.experimental.proxy_tensor import _get_proxies + + for idx, t in enumerate(saved_values): + if idx not in saved_for_backward_idx: + for proxy in _get_proxies(t): + proxy.node.meta["saved_tensor_with_no_vc_check"] = True + + return output + + @staticmethod + def setup_context(ctx: Any, inputs: tuple[Any, ...], output: Any) -> None: + # If users call ctx.mark_non_differentiable() in the original fwd function. + if len(non_differentiable_idx) > 0: + non_differentiable_output = [] + for i, x in enumerate(output): + if i in non_differentiable_idx: + non_differentiable_output.append(x) + ctx.mark_non_differentiable(*non_differentiable_output) + + @staticmethod + def backward(ctx: Any, *grad: Any) -> Any: + # The Interpreter here is required to propagate metadata + # from the dynamo graph body to the local_map graph body. + # This is required for fx_traceback.annotate for work. + + if saved_values is None: + raise AssertionError("saved_values must not be None") + return torch.fx.Interpreter(bwd).run(*grad, *saved_values) + + return ApplyTemplate.apply(*fwd_args) + + +autograd_function_apply = AutogradFunctionApply() + + +class DynamoAutogradFunctionTraceHelper: + @staticmethod + def fwd_trace_helper(orig_fwd: Callable[_P, Any]) -> Callable[_P, Any]: + # autograd.Function forward does more than just running the forward method. Most + # of this logic is in C++. Here, we rewrite that functionality in python and let + # Dynamo trace it. + def inner(*args: _P.args, **kwargs: _P.kwargs) -> Any: + with torch.no_grad(): + outs = orig_fwd(*args, **kwargs) + + # Handle the case where if the input is passed on directly to the output, we call view_as + # Refer to https://github.com/pytorch/pytorch/blob/main/torch/csrc/autograd/custom_function.cpp#L254 + tensor_args = {arg for arg in args if isinstance(arg, torch.Tensor)} + if isinstance(outs, torch.Tensor): + if outs in tensor_args: + return outs.view_as(outs) + else: + return outs + + new_outs = [] + for out in outs: + if isinstance(out, torch.Tensor): + if out in tensor_args: + new_outs.append(out.view_as(out)) + else: + new_outs.append(out) + else: + new_outs.append(out) + return tuple(new_outs) + + # TODO - there is missing functionality here, where + # autograd.Function overwrites the requires_grad_ of the output + # tensors depending on the `mark_non_differentiable`. Currently, + # this is handled hackily in Dynamo, where we just overwrite the + # variable trackers requires_grad. Refer to the function - + # overwrite_tensor_vt_requires_grad + + return inner diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/batch_norm_replacement.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/batch_norm_replacement.py new file mode 100644 index 0000000000000000000000000000000000000000..77aa9b9c2d7c78647d2c850b550754e43c4fe592 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/batch_norm_replacement.py @@ -0,0 +1,27 @@ +import torch.nn as nn +from torch._functorch.utils import exposed_in + + +def batch_norm_without_running_stats(module: nn.Module) -> None: + if ( + isinstance(module, nn.modules.batchnorm._BatchNorm) + and module.track_running_stats + ): + module.running_mean = None + module.running_var = None + module.num_batches_tracked = None + module.track_running_stats = False + + +@exposed_in("torch.func") +def replace_all_batch_norm_modules_(root: nn.Module) -> nn.Module: + """ + In place updates :attr:`root` by setting the ``running_mean`` and ``running_var`` to be None and + setting track_running_stats to be False for any nn.BatchNorm module in :attr:`root` + """ + # base case + batch_norm_without_running_stats(root) + + for obj in root.modules(): + batch_norm_without_running_stats(obj) + return root diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/benchmark_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/benchmark_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..32cf348a222047823d5ebacb345df01f91be0567 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/benchmark_utils.py @@ -0,0 +1,245 @@ +from __future__ import annotations + +import contextlib +import json +import operator +import os +import time +from contextlib import AbstractContextManager +from typing import Any, TYPE_CHECKING +from typing_extensions import TypeVar + +import torch +from torch.profiler import profile, ProfilerActivity + + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + + +_R = TypeVar("_R") + + +def synchronize() -> None: + pass + + +def dump_chrome_trace( + f: Callable[[tuple[Any, ...]], _R], + input_: tuple[Any, ...], + trace_filename: str, + optimize_ctx: AbstractContextManager[Any], + activities: Sequence[ProfilerActivity], + num_runs: int = 1, + devices: list[str] | None = None, + kwargs_for_f: dict[str, Any] | None = None, + kwargs_for_profiler: dict[str, Any] | None = None, +) -> float: + """ + Output the chrome trace of running f(input_, **kwargs_for_f) with [optimize_ctx] + [num_runs] times to [trace_filename]. + + [activities] are the activities that the profiler will record, e.g. ProfilerActivity.CUDA. + Return total runtime without the profiler + + Outputs to trace_filename + """ + + if devices is None: + devices = ["cuda"] + + global synchronize + if devices != ["cpu"] and torch.cuda.is_available(): + synchronize = torch.cuda.synchronize + + if kwargs_for_f is None: + kwargs_for_f = {} + if kwargs_for_profiler is None: + kwargs_for_profiler = {} + + with optimize_ctx: + torch.manual_seed(1337) + for _ in range(5): # warmup runs + f(input_, **kwargs_for_f) + synchronize() + torch.manual_seed(1337) + t0 = time.perf_counter() + for _ in range(num_runs): + f(input_, **kwargs_for_f) + synchronize() + t1 = time.perf_counter() + timing = t1 - t0 + + with profile(activities=activities, **kwargs_for_profiler) as prof: + with optimize_ctx: + synchronize() + torch.manual_seed(1337) + for _ in range(num_runs): + f(input_, **kwargs_for_f) + synchronize() + prof.export_chrome_trace(trace_filename) + + return timing + + +def get_chrome_trace_events(filename: str) -> list[dict[str, Any]]: + with open(filename) as f: + data = json.load(f) + events = data["traceEvents"] + return events + + +def is_gpu_compute_event(event: dict[str, Any]) -> bool: + global gpu_pids + return ( + "pid" in event + and event["pid"] in gpu_pids + and "ph" in event + and event["ph"] == "X" + ) + + +def get_sorted_gpu_events(events: list[dict[str, Any]]) -> list[dict[str, Any]]: + sorted_gpu_events: list[dict[str, Any]] = [] + for event in events: + if not is_gpu_compute_event(event): + continue + sorted_gpu_events.append(event) + return sorted(sorted_gpu_events, key=operator.itemgetter("ts")) + + +def get_duration(sorted_gpu_events: list[dict[str, Any]]) -> int: + if len(sorted_gpu_events) == 0: + return 0 + event = sorted_gpu_events[0] + current_end_time = event["ts"] + event["dur"] + total_duration = event["dur"] + for event in sorted_gpu_events[1:]: + start_time = max(event["ts"], current_end_time) + end_time = event["ts"] + event["dur"] + total_duration = total_duration + max(end_time - start_time, 0) + current_end_time = max(current_end_time, end_time) + return total_duration + + +def get_sorted_gpu_mm_conv_events(events: list[dict[str, Any]]) -> list[dict[str, Any]]: + def is_mm_conv_event(event: dict[str, Any]) -> bool: + return "name" in event and ( + "gemm" in event["name"] + or "conv" in event["name"] + or "cutlass" in event["name"] + or "wgrad" in event["name"] + ) + + gpu_events = get_sorted_gpu_events(events) + sorted_events: list[dict[str, Any]] = [] + for event in gpu_events: + if not is_mm_conv_event(event): + continue + sorted_events.append(event) + return sorted_events + + +gpu_pids: list[Any] = [] + + +def compute_utilization(filename: str, total_length: float) -> tuple[float, float]: + """ + Process the chrome traces outputs by the pytorch profiler to compute GPU Utilization + and percent of times spent on matmul and convolution + + Args: + filename(str): Name of chrome traces file produced by pytorch profiler + + total_length(float): total length of the process without profiler in second + + Return: + tuple: (GPU Utilization, percent of time spent on matmul and convolution) + """ + events = get_chrome_trace_events(filename) + + # get pids of GPU events + global gpu_pids + gpu_pids = [] + for event in events: + if "name" not in event: + continue + if event["name"] == "process_labels" and "GPU" in event["args"]["labels"]: + gpu_pids.append(event["pid"]) + + total_length = total_length * 1e6 + sorted_gpu_events = get_sorted_gpu_events(events) + utilization = get_duration(sorted_gpu_events) / total_length + + sorted_gpu_mm_conv_events = get_sorted_gpu_mm_conv_events(events) + mm_conv_utilization = get_duration(sorted_gpu_mm_conv_events) / total_length + + return utilization, mm_conv_utilization + + +def benchmark_utilization( + f: Callable[[tuple[Any, ...]], _R], + input_: tuple[Any, ...], + trace_folder: str, + optimize_ctx: AbstractContextManager[Any] | None = None, + trace_file_name: str = "tmp_chrome_trace", + num_runs: int = 1, +) -> tuple[float, float]: + """ + Benchmark the GPU Utilization and percent of time spent on matmul and convolution operations of + running f(input_, **kwargs_for_f) with [optimize_ctx] [num_runs] times. + It will produce a chrome trace file in trace_folder/trace_file_name.json + + Example: + + ``` + def f(a): + return a.sum() + + + a = torch.rand(2**20, device="cuda") + utilization, mm_conv_utilization = benchmark_utilization( + f, a, "tmp", trace_file_name="tmp_chrome_trace" + ) + ``` + + Args: + f: function to benchmark + + input_: input to :attr:`f` + + trace_folder: name of the folder to store the chrome trace + + optimize_ctx: the context in which f will run + + trace_file_name: name of the dumped chrome trace file, default to "tmp_chrome_trace" + + num_runs: number of times to run f, excluding the warm-up runs, default to 1. + + Return: + tuple: (GPU Utilization, percent of time spent on matmul and convolution) + + """ + isExist = os.path.exists(trace_folder) + if not isExist: + os.makedirs(trace_folder) + print("create folder " + trace_folder) + + if optimize_ctx is None: + optimize_ctx = contextlib.nullcontext() + + chrome_trace_file_name = os.path.join(trace_folder, trace_file_name + ".json") + total_length = dump_chrome_trace( + f, + input_, + chrome_trace_file_name, + optimize_ctx, + [ProfilerActivity.CUDA], + num_runs=num_runs, + devices=["cuda"], + ) + utilization, mm_conv_utilization = compute_utilization( + chrome_trace_file_name, total_length + ) + + return utilization, mm_conv_utilization diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/compile_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/compile_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f80d81b2f2958444800db176e496224bb9b90579 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/compile_utils.py @@ -0,0 +1,229 @@ +from __future__ import annotations + +import operator +from typing import Any, TYPE_CHECKING + +import sympy + +import torch +import torch.fx as fx +from torch.fx.experimental.symbolic_shapes import free_unbacked_symbols +from torch.multiprocessing.reductions import StorageWeakRef +from torch.utils import _pytree as pytree +from torch.utils._pytree import tree_flatten + + +if TYPE_CHECKING: + from collections.abc import Callable + + from torch._ops import OpOverloadPacket + from torch.utils._pytree import TreeSpec + +aten = torch.ops.aten + + +def get_aten_target(node: fx.Node) -> OpOverloadPacket | Callable[..., Any] | str: + if hasattr(node.target, "overloadpacket"): + return node.target.overloadpacket + return node.target + + +rand_ops = [ + aten.dropout, + aten._fused_dropout, + aten._standard_gamma, + aten.bernoulli, + aten.multinomial, + aten.native_dropout, + aten.normal, + aten.poisson, + aten.binomial, + aten.rrelu, + aten.rand_like, + aten.rand, + aten.randint, + aten.randn, + aten.randperm, +] + + +# return a new copy of torch.fx.graph.Graph with CSE applied to the input graph +def fx_graph_cse(fx_g: torch.fx.graph.Graph) -> fx.Graph: + new_graph = fx.Graph() + env: dict[ + fx.Node, fx.Node + ] = {} # map from node in the old graph to node in the new graph + hash_env: dict[ + tuple[str, int], fx.Node + ] = {} # map from hash to a node in the new graph + token_map: dict[tuple[str, int], dict[str, Any]] = {} # map from hash to token + + from torch._inductor.pattern_matcher import ( + compute_mutation_region_ids, + same_mutation_regions, + ) + + compute_mutation_region_ids(fx_g) # type: ignore[arg-type] + + # Make a set of separate storages returned from the output, which will be preserved + # when pruning. This prevents us from deduplicating returned tensors which have + # experienced identical operations, but are separate data structures in eager mode. + output_node: fx.Node = list(fx_g.nodes)[-1] + if output_node.op != "output": + raise AssertionError( + f"expected output_node.op to be 'output', got '{output_node.op}'" + ) + + def checkable_node(node: fx.Node) -> bool: + """We can evaluate only nodes that represent tensors with defined storage.""" + if "val" not in node.meta or not isinstance(node.meta["val"], torch.Tensor): + return False + + try: + node.meta["val"].untyped_storage() + except NotImplementedError: + return False + + return True + + output_storages = { + StorageWeakRef(n.meta["val"].untyped_storage()) + for n in output_node.all_input_nodes + if checkable_node(n) + } + nodes_that_alias_outputs = { + n + for n in fx_g.nodes + if checkable_node(n) + and StorageWeakRef(n.meta["val"].untyped_storage()) in output_storages + } + + for n in fx_g.nodes: + # The placeholder, output, and get_attr nodes are copied to the new graph without change + # do not CSE away random operations + if ( + n.op == "placeholder" + or n.op == "output" + or n.op == "get_attr" + or get_aten_target(n) in rand_ops + # aten.empty is non-deterministic, so don't CSE it. + # Also, aten.empty is almost always fusible into its consumer, + # so it's not worth CSEing. + or get_aten_target(n) is aten.empty + or n in nodes_that_alias_outputs + # This CSE pass currently doesn't handle re-propagation of unbacked + # meta where it'll sometimes eliminate a _local_scalar_dense but not + # replace the meta of downstream users. eg. one bug we've seen is: + # + # _local_scalar_dense_11: "Sym(u14)" = torch.ops.aten._local_scalar_dense.default(select_10); + # sym_sum_2: "Sym(u19 + u20 + u21)" = torch.sym_sum((_local_scalar_dense_11, _local_scalar_dense_12, _local_scalar_dense_13)) # noqa: B950 + # + # Notice how _local_scalar_dense_11 is u14 but sym_sum_2's meta is incorrectly the old + # pre-cse value of u19. + or ( + "val" in n.meta + and isinstance(n.meta["val"], sympy.Symbol) + and free_unbacked_symbols(n.meta["val"]) + ) + ): + new_node = new_graph.node_copy(n, lambda x: env[x]) + env[n] = new_node + else: # n.op == 'call_function', should never see n.op == 'call_module' or 'call_method' + # substitute args and kwargs members to their mapping in env if exists + # specs can be used to reconstruct nested list/dictionaries + def substitute( + arg_list: list[Any] | tuple[Any, ...], + ) -> tuple[tuple[Any, ...], TreeSpec]: + arg_list, spec = tree_flatten(arg_list) + for i in range(len(arg_list)): + v = arg_list[i] + if isinstance(v, torch.fx.node.Node) and v in env: + arg_list[i] = env[v] + if isinstance(v, (torch.SymBool, torch.SymInt, torch.SymFloat)): + arg_list[i] = v.node + return tuple(arg_list), spec + + args, args_spec = substitute(n.args) + kwargs, kwargs_spec = substitute(n.kwargs) + + # each token corresponds to a unique node + # nodes with the same token can be substituted + token = { + "target": n.target, + "args": args, + "args_spec": args_spec, + "kwargs": kwargs, + "kwargs_spec": kwargs_spec, + } + + # hash substituted args to a number, do not hash specs because specs are not hashable + # We need to add type into hash to avoid situations like: + # hash((primals_2, 1.0)) == hash((primals_2, 1)) + hash_arg = hash( + (tuple((a, type(a)) for a in args), tuple((a, type(a)) for a in kwargs)) + ) + hash_val = (n.target, hash_arg) + + # check if a node has a substitute and can be eliminated + hash_val_in_hash_env = hash_val in hash_env + overwrite_due_to_mutation = False + if hash_val_in_hash_env and token_map[hash_val] == token: + duplicate_n_prev = hash_env[hash_val] + if same_mutation_regions(n, duplicate_n_prev): + env[n] = duplicate_n_prev + continue + else: + # any futures duplicates should replace with n, not duplicate_n_prev + overwrite_due_to_mutation = True + + new_node = new_graph.node_copy(n, lambda x: env[x]) + env[n] = new_node + if overwrite_due_to_mutation or not hash_val_in_hash_env: + hash_env[hash_val] = new_node + token_map[hash_val] = token + + return new_graph + + +def raise_getitems(gm: fx.GraphModule) -> fx.GraphModule: + # Pre-create a list of nodes to iterate over, as modifying the node order + # during the loop can lead to infinite loops if not handled properly. + getitem_nodes = list( + gm.graph.find_nodes(op="call_function", target=operator.getitem) + ) + + # loop through getitem nodes in the graph and raise them to the parent node + # in reverse order to preserve their original relative order + for node in reversed(getitem_nodes): + if len(node.all_input_nodes) != 1: + raise AssertionError( + f"expected node {node.name} to have 1 input node, got {len(node.all_input_nodes)}" + ) + parent = node.all_input_nodes[0] + parent.append(node) + + gm.recompile() + return gm + + +def strip_overloads(gm: fx.GraphModule) -> None: + """ + Modifies the target of graph nodes in :attr:`gm` to strip overloads. + + Args: + gm(fx.GraphModule): The input Fx graph module to be modified + """ + for node in gm.graph.nodes: + if isinstance(node.target, torch._ops.OpOverload): + node.target = node.target.overloadpacket + gm.recompile() + + +def get_placeholders(graph: fx.Graph) -> fx.graph._node_list: + return graph.find_nodes(op="placeholder") + + +def get_outputs(graph: fx.Graph) -> list[fx.Node]: + for node in graph.find_nodes(op="output"): + return pytree.tree_leaves(node.args[0]) + raise AssertionError("No output node found") diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/compilers.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/compilers.py new file mode 100644 index 0000000000000000000000000000000000000000..687aabbb9c9cf1a0a4c5c8122d46a3da3eacace3 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/compilers.py @@ -0,0 +1,501 @@ +from __future__ import annotations + +import copy +import logging +import os +import pickle +import random +from contextlib import contextmanager +from functools import partial +from typing import Any, TYPE_CHECKING +from typing_extensions import ParamSpec, TypeVar + +import sympy + +import torch +import torch.fx as fx +import torch.nn as nn +import torch.utils._pytree as pytree +from torch import SymInt +from torch._decomp import get_decompositions +from torch.fx.experimental.symbolic_shapes import bind_symbols + +from .aot_autograd import aot_function, aot_module, make_boxed_compiler +from .compile_utils import strip_overloads +from .partitioners import ( + default_partition, + draw_graph, + min_cut_rematerialization_partition, +) + + +if TYPE_CHECKING: + from collections.abc import Callable, Generator, Sequence + + from torch.fx.node import Node + from torch.types import IntLikeType + + +_P = ParamSpec("_P") +_R = TypeVar("_R") + +log = logging.getLogger(__name__) + + +# These canonicalization are needed here (and not decompositions), as the ops +# we're trying to canonicalize to CompositeImplicitAutograd. +def _canonicalize(fx_g: fx.GraphModule) -> fx.GraphModule: + for node in fx_g.graph.find_nodes( + op="call_function", target=torch.ops.aten._to_copy + ): + node.target = torch.ops.aten.to + fx_g.recompile() + return fx_g + + +@contextmanager +def _disable_jit_autocast() -> Generator[None, None, None]: + # pyrefly: ignore [missing-attribute] + old_jit_autocast_flag = torch._C._jit_set_autocast_mode(False) + try: + yield + finally: + # pyrefly: ignore [missing-attribute] + torch._C._jit_set_autocast_mode(old_jit_autocast_flag) + + +@make_boxed_compiler +def ts_compile(fx_g: fx.GraphModule, inps: Sequence[Any]) -> torch.jit.ScriptModule: + """ + Compiles the :attr:`fx_g` with Torchscript compiler. + + .. warning:: + This API is experimental and likely to change. + + Args: + fx_g(fx.GraphModule): The input Fx graph module to be compiled. + + Returns: + Torch scripted model. + """ + + with _disable_jit_autocast(): + strip_overloads(fx_g) + + for node in fx_g.graph.find_nodes( + op="call_function", target=torch.ops.aten._to_copy + ): + if len(node.args) == 1 and len(node.kwargs) == 1 and "dtype" in node.kwargs: + node.target = torch.ops.aten.to + + for node in fx_g.graph.nodes: + new_kwargs = {} + for k, v in node.kwargs.items(): + if isinstance(v, torch.device): + v = v.type + new_kwargs[k] = v + node.kwargs = new_kwargs + + fx_g.graph.lint() + + fx_g.recompile() + + f = torch.jit.script(fx_g) + + # pyrefly: ignore [missing-attribute] + torch._C._jit_pass_remove_mutation(f.graph) + + f = torch.jit.freeze(f.eval()) + f = torch.jit.optimize_for_inference(f) + if not any(isinstance(t, torch._subclasses.FakeTensor) for t in inps): + f(*inps) + return f + + +def _draw_graph_compile( + fx_g: fx.GraphModule, _: Any, name: str, clear_meta: bool = True +) -> fx.GraphModule: + print(fx_g.code) + draw_graph(fx_g, name, clear_meta=clear_meta) + return fx_g + + +def draw_graph_compile( + name: str, +) -> Callable[[fx.GraphModule, list[Any]], fx.GraphModule]: + return make_boxed_compiler(partial(_draw_graph_compile, name=name)) + + +@make_boxed_compiler +def nop(fx_g: fx.GraphModule, _: Any) -> fx.GraphModule: + """ + Returns the :attr:`fx_g` Fx graph module as it is. This is a no-op compiler + and can be used to check accuracy. + + .. warning:: + This API is experimental and likely to change. + + """ + return fx_g + + +class DebugInterpreter(fx.Interpreter): + def run( + self, + *args: Any, + initial_env: dict[Node, Any] | None = None, + enable_io_processing: bool = True, + ) -> Any: + self.symbol_mapping = bind_symbols( + # pyrefly: ignore[bad-argument-type] + self.module, + *args, + ) + return super().run( + *args, initial_env=initial_env, enable_io_processing=enable_io_processing + ) + + def run_node(self, n: Node) -> Any: + def subst_symint(ni: IntLikeType) -> int: + if not isinstance(ni, SymInt): + return ni + r = sympy.expand(ni.node.expr.xreplace(self.symbol_mapping)) + if not r.is_number: + raise AssertionError(f"expected r to be a number, got {r}") + return int(r) + + def subst_symint_tuple(nis: tuple[IntLikeType, ...]) -> tuple[int, ...]: + return tuple(subst_symint(ni) for ni in nis) + + def check_significant_strides(a: torch.Tensor, b: torch.Tensor) -> bool: + if subst_symint(a.numel()) > 0: + for idx in range(a.ndim): + if ( + subst_symint(a.stride(idx)) != b.stride(idx) + and subst_symint(a.size(idx)) > 1 + ): + return False + return True + + def check(nv: torch.Tensor, rv: torch.Tensor, desc: Callable[[], str]) -> None: + if not callable(desc): + raise AssertionError(f"expected desc to be callable, got {type(desc)}") + if nv.dtype != rv.dtype: + raise AssertionError(f"{desc()}: {nv.dtype} != {rv.dtype}") + if subst_symint_tuple(nv.size()) != rv.size(): + raise AssertionError( + f"{desc()}: {nv.size()} aka {subst_symint_tuple(nv.size())} != {rv.size()}" + ) + same_strides = check_significant_strides(nv, rv) + if not same_strides: + raise AssertionError( + f"{desc()}: {nv.stride()} aka {subst_symint_tuple(nv.stride())} != {rv.stride()}" + ) + + r = super().run_node(n) + if "val" in n.meta: + n_vals, _n_spec = pytree.tree_flatten(n.meta["val"]) + r_vals, _r_spec = pytree.tree_flatten(r) + # TODO: There is some sort of problem where we record that an + # operator returned a tuple/list, and then later it turns out the + # real version of the operator returned a list/tuple. Need to + # figure out what's actually going on here, the error itself is + # harmless enough as we only getitem out the outputs. + # assert n_spec == r_spec, f"{n_spec} != {r_spec}" + if len(n_vals) != len(r_vals): + raise AssertionError(f"{len(n_vals)} != {len(r_vals)}") + for i, nv, rv in zip(range(len(n_vals)), n_vals, r_vals): + if not isinstance(rv, torch.Tensor): + continue + check(nv, rv, lambda: f"output {i} where {self.symbol_mapping}") + return r + + +@make_boxed_compiler +def debug_nop( + fx_g: fx.GraphModule, _: Any +) -> Callable[[DebugInterpreter, Any, dict[Node, Any] | None, bool], Any]: + """ + Returns a (slow) interpreter over the FX graph module that also checks + various debugging properties (e.g., that tracing strides matched real + strides.) + """ + return DebugInterpreter(fx_g).run + + +@make_boxed_compiler +def simple_ts_compile(fx_g: fx.GraphModule, _: Any) -> torch.jit.ScriptModule: + strip_overloads(fx_g) + f = torch.jit.script(fx_g) + f = torch.jit.freeze(f.eval()) + return f + + +def nnc_jit(f: Callable[..., Any]) -> Callable[..., Any]: + return aot_function(f, simple_ts_compile) + + +aten = torch.ops.aten +default_decompositions = { + aten.detach, + aten.gelu_backward, + aten.leaky_relu_backward, + aten.sigmoid_backward, + aten.threshold_backward, + aten.hardtanh_backward, + aten.hardsigmoid_backward, + aten.hardswish_backward, + aten.tanh_backward, + aten.silu_backward, + aten.elu_backward, + aten.cudnn_batch_norm, + aten.cudnn_batch_norm_backward, + aten.masked_fill.Scalar, + aten.masked_fill.Tensor, + aten.elu, + aten.leaky_relu, + aten.hardtanh, + aten.hardswish, + aten.hardsigmoid, + aten.conj_physical, + aten.is_same_size, +} + +# pyrefly: ignore[bad-argument-type] +default_decompositions = get_decompositions(default_decompositions) + + +@make_boxed_compiler +def print_compile(fx_g: fx.GraphModule, _: Any) -> fx.GraphModule: + print(fx_g.code) + return fx_g + + +def memory_efficient_fusion( + fn: Callable[_P, _R] | nn.Module, + **kwargs: Any, +) -> Callable[_P, _R] | nn.Module: + """ + Wrapper function over :func:`aot_function` and :func:`aot_module` to perform + memory efficient fusion. It uses the + :func:`min_cut_rematerialization_partition` partitioner to perform efficient + recomputation. It uses NVFuser to compile the generated forward and backward + graphs. + + .. warning:: + This API is experimental and likely to change. + + Args: + fn (Union[Callable, nn.Module]): A Python function or a ``nn.Module`` + that takes one or more arguments. Must return one or more Tensors. + **kwargs: Any other overrides you want to make to the settings + + Returns: + Returns a ``Callable`` or ``nn.Module`` that retains the eager behavior + of the original :attr:`fn`, but whose forward and backward graphs have + gone through recomputation optimizations, and the graphs have been + compiled with nvfuser. + + """ + config = { + "fw_compiler": ts_compile, + "bw_compiler": ts_compile, + "partition_fn": min_cut_rematerialization_partition, + "decompositions": default_decompositions, + } + config.update(kwargs) + if isinstance(fn, torch.nn.Module): + return aot_module(fn, **config) # pyrefly: ignore[bad-argument-type] + else: + return aot_function(fn, **config) # pyrefly: ignore[bad-argument-type] + + +def debug_compile( + fx_g: fx.GraphModule, inps: Sequence[torch.Tensor] +) -> torch.jit.ScriptModule: + fx_g.to_folder("foo") + print( + f""" +############################################################## +# To minimize FX graph, copy and paste the below and run it # +############################################################## + +import torch +import torch.fx as fx +from functorch.compile import minifier, check_nvfuser_subprocess, check_nvfuser_correctness_subprocess + +inps = {[(i.shape, i.dtype) for i in inps]} +inps = [torch.ones(shape, dtype=dtype, device='cuda') for (shape, dtype) in inps] +from foo import FxModule +mod = FxModule().cuda() + +with torch.jit.fuser("fuser2"): + # check_nvfuser_subprocess can be replaced with check_nvfuser_correctness_subprocess + minifier(fx.symbolic_trace(mod), inps, check_nvfuser_subprocess) +""" + ) + # pyrefly: ignore[missing-import, missing-module-attribute] + from foo import FxModule + + FxModule().cuda()(*inps) + + return ts_compile(fx_g, inps) + + +graph_index: int = 0 + + +def get_inputs(input_data_path: str) -> list[torch.Tensor]: + """ + Return a random input for the given inputs meta generated from _save_fx_default. + """ + inputs: list[torch.Tensor] = [] + with open(input_data_path, "rb") as f: + inputs_meta = pickle.load(f) + inputs = [] + for meta in inputs_meta: + if len(meta) == 1: + type = meta + input_ = type(random.random()) + else: + type, shape, _stride, dtype, device = meta + if dtype in { + torch.int, + torch.int32, + torch.int64, + torch.bool, + torch.int, + torch.uint8, + int, + float, + }: + input_ = torch.randint(0, 1, shape, dtype=dtype, device=device) + else: + input_ = torch.rand(shape, dtype=dtype, device=device) + inputs.append(input_) + return inputs + + +def _save_fx_default( + current_name: str, + folder_name: str, + dump_example_input: bool, + gm: torch.fx.GraphModule, + example_inputs: list[torch.Tensor], +) -> nn.Module: + """ + The forward, backward, and joint computation graph will be stored in + {folder_name}/{current_name}/{current_name}_forward_{graph_index}, + {folder_name}/{current_name}/{current_name}_backward_{graph_index}, and + {folder_name}/{current_name}/{current_name}_joint_{graph_index} respectively. + The input shape of the graphs will be stored in the .input files. + These files can be loaded with pickle, + and is a list of format (type, shape, stride, dtype, device). + In the case of type = int or float, it is just (type,). + For joint graph input, it is a nested list [[],[]] + where the two inner lists have the same format. + If dump_example_input is True, example_inputs will be stored in .pt file. + Since each function might produce multiple graphs, + the graph_index is used to distinguish difference graphs + """ + from functorch.compile import aot_module_simplified + + def get_input_meta(args: Any) -> list[Any]: + input_meta = [] + if len(args) > 0 and isinstance(args[0], tuple): # joint input + input_meta += get_input_meta(args[0]) + input_meta += get_input_meta(args[1]) + return input_meta + for arg in args: + if type(arg) is int or type(arg) is float: + input_meta.append((type(arg),)) + else: + input_meta.append( + (type(arg), arg.shape, arg.stride(), arg.dtype, arg.device) + ) + return input_meta + + def graph_saver_helper( + gm_to_save: fx.GraphModule, args: Any, type_name: str + ) -> None: + global graph_index + if len(gm_to_save.graph.nodes) == 0: + log.log( + logging.WARNING, + "No nodes in graph {%s}_{%s}_{%s}.", + current_name, + type_name, + graph_index, + ) + return + + gm = copy.deepcopy(gm_to_save) + gm.graph.set_codegen(torch.fx.graph.CodeGen()) # remove codegen + gm.recompile() + + input_meta = get_input_meta(args) + + os.makedirs(f"{folder_name}/{current_name}", exist_ok=True) + gm.to_folder( + f"{folder_name}/{current_name}/{current_name}_{type_name}_{graph_index}" + ) + with open( + f"{folder_name}/{current_name}/{current_name}_{type_name}_{graph_index}/{current_name}_{type_name}_{graph_index}.input", + "wb", + ) as f: + pickle.dump(input_meta, f) + if dump_example_input: + torch.save( + args, + f"{folder_name}/{current_name}/{current_name}_{type_name}_{graph_index}/{current_name}_{type_name}_{graph_index}.pt", # noqa: B950 + ) # noqa: E501 + + def graph_saver_forward( + gm: fx.GraphModule, example_inputs: list[torch.Tensor] + ) -> fx.GraphModule: + graph_saver_helper(gm, example_inputs, "forward") + return gm + + def graph_saver_backward( + gm: fx.GraphModule, example_inputs: list[torch.Tensor] + ) -> fx.GraphModule: + graph_saver_helper(gm, example_inputs, "backward") + global graph_index + graph_index += 1 + return gm + + def graph_saver_joint( + gm: fx.GraphModule, joint_args: list[torch.Tensor] + ) -> tuple[fx.GraphModule, fx.GraphModule]: + graph_saver_helper(gm, joint_args, "joint") + return default_partition(gm, joint_args) # pyrefly: ignore[missing-argument] + + # pyrefly: ignore[bad-return] + return aot_module_simplified( + gm, + example_inputs, + fw_compiler=graph_saver_forward, # pyrefly: ignore[bad-argument-type] + bw_compiler=graph_saver_backward, # pyrefly: ignore[bad-argument-type] + partition_fn=graph_saver_joint, + decompositions=default_decompositions, # pyrefly: ignore[bad-argument-type] + ) + + +# WARNING: This isn't tested anywhere!! +def graph_dumper_aot( + current_name: str, folder_name: str, dump_example_input: bool = False +) -> Callable[[bool, nn.Module], Any]: + """ + Dump the forward, backward, and joint computation graph. + Example Usage: + save_fx_func = graph_dumper_aot(current_name, folder_name, dump_example_input = False) + optimize_ctx = torchdynamo.optimize( + save_fx_func + ) + with torch.enable_grad(): + with optimize_ctx: + result = forward_and_backward_pass(model, example_inputs) + """ + global graph_index + graph_index = 0 + return partial(_save_fx_default, current_name, folder_name, dump_example_input) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/config.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/config.py new file mode 100644 index 0000000000000000000000000000000000000000..7f834556485d660cbc062311e2b6834fec048431 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/config.py @@ -0,0 +1,444 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from collections.abc import Callable + + +""" +Global flags for aot autograd +""" + +import os +import sys +from typing import Literal, TYPE_CHECKING + +from torch.utils._config_module import Config, install_config_module + + +# [@compile_ignored: debug] +_save_config_ignore = [ + # callable not serializable + "joint_custom_pass", + # callable configs with uuid() for caching, or raw callables + "activation_memory_budget_runtime_estimator", + "activation_memory_budget_solver", +] + + +# Converts torch rng ops to their functional philox rng equivalents. Note that +# we functionalize only CUDA rng ops today. +functionalize_rng_ops = False + +# can be useful for debugging if we are incorrectly creating meta fake tensors +fake_tensor_allow_meta = os.environ.get("FAKE_ALLOW_META", "1") != "0" + +# Enables optional asserts in hotpath code to check for errors. If +# you are seeing weird accuracy problems, try turning this on. +# This is currently off by default as it will harm tracing time, +# but it is on by default for aot_eager. +debug_assert = False + +debug_partitioner = os.environ.get("AOT_PARTITIONER_DEBUG", "0") != "0" + +# See # NOTE [Export custom triton op] +decompose_custom_triton_ops = True + +static_weight_shapes = True + +# See https://github.com/pytorch/pytorch/issues/141881 +# Tells partitioner that parameters are free to save for backward. +treat_parameters_as_free_to_save = True + +# Applies CSE to the graph before partitioning +cse = True + +from torch._environment import is_fbcode + + +enable_autograd_cache: bool = Config( + justknob="pytorch/remote_cache:enable_local_autograd_cache", + env_name_force="TORCHINDUCTOR_AUTOGRAD_CACHE", + default=True, +) + +autograd_cache_allow_custom_autograd_functions: bool = Config( + env_name_force="TORCHINDUCTOR_AUTOGRAD_CACHE_ALLOW_CUSTOM_AUTOGRAD", default=False +) + +# For now, this is just for enabling unit testing in test_aot_autograd_cache.py +# We will either make this the default with AOTAutogradCache, or +# we'll just use it in the precompile flow. So there's no +# need to add env vars or make it configurable +bundled_autograd_cache: bool = False + +bypass_autograd_cache_key: bool = False + +# Whether or not to normalize placeholder names in graphs +# from dynamo in AOTAutogradCache +autograd_cache_normalize_inputs = not is_fbcode() + +# Enable debug mode at first invocation to check if custom ops are valid. +# When enabled, this checks that custom operators don't violate aliasing constraints. +# +# check_custom_op_aliasing: Controls whether to run the custom op aliasing check at all. +# - When True: The check runs on first invocation of compiled functions. +# - When False: The check is skipped entirely. +# +# error_on_custom_op_aliasing: Controls behavior when a violation is detected. +# Only has effect when check_custom_op_aliasing is True. +# - When True: Raises RuntimeError on aliasing violations. +# - When False: Emits UserWarning on aliasing violations. +# +# Deprecated: Custom ops returning aliased outputs is deprecated and will +# become an error in PyTorch 2.12. Currently error_on_custom_op_aliasing +# is True only in CI. +check_custom_op_aliasing = True +error_on_custom_op_aliasing = bool(os.getenv("CI")) + + +def remote_autograd_cache_default() -> bool | None: + if os.environ.get("TORCHINDUCTOR_AUTOGRAD_REMOTE_CACHE") == "1": + return True + if os.environ.get("TORCHINDUCTOR_AUTOGRAD_REMOTE_CACHE") == "0": + return False + return None + + +enable_remote_autograd_cache = remote_autograd_cache_default() + + +# When AOTAutograd regenerates aliased graph outputs, +# attempt to use functionalization's view-replay logic +# before falling back to the autograd engine's view replay or as_strided. +# This can have some perf implications +# (although for many models this will not matter). +# (1) If you have many view ops chained together, replaying all of them +# at runtime can have more overhead compared to a single as_strided call +# (2) If you are doing training, AsStridedBackward is quite slow, +# and the individual view op backward formulas will likely be faster. +# (3) Some backends like XLA do not support as_strided + +# Temporary hack: disable this flag for internal +# (needed to fix an internal issue while avoiding bumping XLA pin) +# eventually: either default this config to false completely +# once XLA pin update works, +# or default config to true and fix relevant bugs + + +# View replay is currently not compatible with AOTAutogradCache, since +# FunctionalTensors are not serializable. We'll need to make them +# serializable before enabling warm cache with this config turned on. +view_replay_for_aliased_outputs = not is_fbcode() + +# Restricts the amount of computation AOTAutograd can do. +# NB: We have essentially disabled this heuristic now. However, this is kept +# here for now in case it's useful. Setting it low can artificially reduce the +# amount of recomputation AOTAutograd performs, although not in any kind of +# principled way. +max_dist_from_bw = 1000 + + +# Bans recomputation of nodes that are reading from nodes that are far before +# the current node +ban_recompute_used_far_apart = True +# Breaks up long chain of fusible ops, as otherwise we can have an arbitrarily +# long chain of recomputation in the backwards pass. +ban_recompute_long_fusible_chains = True +# Bans recomputation of nodes that must be materialized in the backwards pass +# (used by a non-fusible node) +ban_recompute_materialized_backward = True +# Chooses to ban recomputation of nodes based off an allowlist. Setting it to +# False changes it to use a denylist. Main change is on operators like +# sort/pool/stuff that isn't cheap enough to be fusible for free but also isn't +# that expensive +ban_recompute_not_in_allowlist = True +# Chooses to ban recomputation of reductions. This is generally a good idea, as +# the result of reductions is generally very small but recomputing reductions in +# a fusion can be expensive. +ban_recompute_reductions = True +# Prevents the partitioner from ever saving views (i.e. always recompute them). +# Generally a good idea since views are free to recompute. +recompute_views = False +# Set this flag to enable considering non-built-in ops, including triton and custom +# ops, for recomputation during the knapsack optimization solver. +is_non_builtin_to_include = False + +# Rematerialize AC nodes for graphs with forward+loss+backward in one graph. +# This optimization minimizes activation checkpoint node lifetimes by computing them +# just-in-time. For AC nodes only used in backward, they are deferred to backward region +# instead of being computed and saved in forward. This reduces peak memory usage. +# Note: This only applies to forward+loss+backward graphs where torch.autograd.grad is allowed +# in the graph. Joint graphs (standard AOTAutograd) use the partitioner instead. +remat_using_tags_for_fwd_loss_bwd_graph = True + +# By default, the partitioner is purely trying to optimize for runtime (although +# it should always use less memory than eager) +# This knob controls the partitioner to make that tradeoff for you, choosing the +# fastest option that saves less activations than the memory budget. +# Specifically, 0.0 corresponds to the activation memory from applying +# activation checkpointing to the full compiled region, and 1.0 corresponds to +# the activation memory from the default runtime-optimized strategy. So, 0.4 +# would result in a strategy that saves 40% of the activations compared to the +# default strategy. +# It solves a 0-1 knapsack to find the minimum recompute necessary to stay below +# the activation memory budget. +# NOTE: This *cannot* be treated as +activation_memory_budget = 1.0 + +# This controls how we estimate the runtime when deciding what the cheapest +# operators to recompute are. The 3 options are +# "flops": Bases it off of the flop count provided by torch.utils.flop_counter +# "profile": Benchmarks each operator to come up with a runtime +# "testing": Returns 1 for everything +activation_memory_budget_runtime_estimator = "flops" + +# This controls the solver used for the 0-1 knapsack. By default we use a +# quantized DP solution ("dp"). The other approaches are a "greedy", an "ilp" +# (which has a scipy dependency) and "dp_knapsack_sliding_hirschberg", which +# used memory-efficient quantized DP solution +activation_memory_budget_solver = "dp" + +# This dumps out a SVG visualization of the expected runtime vs. activation +# memory tradeoffs for all memory budget values from 0 to 1 in increments of +# 0.5. See an example here: +# https://github.com/pytorch/pytorch/pull/126320#discussion_r1625104015 +visualize_memory_budget_pareto = ( + os.environ.get("PARTITIONER_MEMORY_BUDGET_PARETO", "0") == "1" +) + +# This controls the directory in which to dump the SVG plot with the pareto +# frontier of the activation checkpointing memory-vs-runtime tradeoffs. +memory_budget_pareto_dir = os.environ.get("PARTITIONER_MEMORY_BUDGET_PARETO_DIR") + +# Sets all of the ban_recompute heuristics to False except ban_recompute_reductions +# Generally, this will probably result in some memory improvement, but at the +# cost of some performance +aggressive_recomputation = False + +# activation offloading enablement (testing purpose) +enable_activation_offloading = False + +# activation offloading with separate CUDA stream +activation_offload_separate_stream = False + +# activation offloading wait sinking when using separate stream (fwd graph) +activation_offload_sink_wait = False + +# activation reloading with prefetching when using separate streams (bwd graph) +activation_reload_prefetch = False + +# CPU ↔ GPU bandwidth in GB/s, used to estimate transfer times for prefetch +# scheduling. This is hardware-specific and should be set by the user. +activation_offload_cpu_gpu_bw: float = 50.0 + +# If FakeTensor.data_ptr() should error. +# This option is independent of AOTAutograd and torch.compile, but our policy +# is to turn it off during torch.compile. +fake_tensor_allow_unsafe_data_ptr_access = True + +# Unlifts effect tokens from the inputs/outputs in the traced graph and instead +# inserts make_token/sink_token calls in the graph to create tokens and then +# sink them at the end. Note that this means the graph is no longer functional +# which may lead to silent errors unless the backend knows how to handle the +# tokens. +unlift_effect_tokens = False + +# NOTE: [The default layout constraint for custom operators.] +# This must be the name of one of the layout constraint tags +# (that is, one of {"needs_fixed_stride_order", "flexible_layout"}), +# If the custom op does not have a layout constraint tag already +# then we assume the following applies. +# +# This config is respected by Inductor and we recommend other backends also +# respect it. +# This config is in torch._functorch and not torch._inductor because it affects +# ProxyTensor tracing. +custom_op_default_layout_constraint: Literal[ + "needs_exact_strides", "needs_fixed_stride_order", "flexible_layout" +] = "needs_exact_strides" + + +# Run aot eager decomp partition with CrossRefFakeMode +# options = False, "all", "custom_ops" +fake_tensor_crossref = False + +# This mode specifies that we should also keep track of the real +# tensor along with the fake tensor, and do real compute. While +# seemingly this eliminates the whole point of fake tensors, there are +# two obvious use cases for it: +# +# 1. When users call item()/other data dependent operations, +# if we propagate_real_tensors we are able to determine what +# the true value is and keep going. +# +# 2. It can be useful for testing, when you want to see if the fake +# and real tensors agree with each other. (Note that there are +# currently known inaccuracies in how we clone real tensors, that +# would have to be tightened up for this to be useful in this +# case.) +# +# Note that fake tensors are typically understood to be cheap to store +# indefinitely, so we tend to hold on to them longer than we would +# hold onto the real tensors. So we also support you explicitly +# deallocating the real tensor associated with a fake tensor, at which +# point we will stop propagating real tensors. +# +# One more thing: when you provide a real tensor to fakeify, we will +# clone it, so that we can safely perform mutations on it if necessary. +# This will increase live memory usage. This could potentially be +# optimized by using COW. We also currently do not faithfully +# maintain autograd metadata on the real tensor; this is fine because +# AOTAutograd will only use the fake tensor to determine leafness/etc +# of tensors in question. +fake_tensor_propagate_real_tensors = False + +# AOTDispatcher traces out a backward graph at the time of the forward pass. +# This flag controls whether or not that backward graph gets autocast behavior +# applied to it. +# +# The options are either: +# - "same_as_forward". We assume that the backward of the torch.compile'ed region +# will be run under the same autocast context manager that the region was run +# under. This is equivalent to running the following code in eager: +# +# with torch.amp.autocast(...): +# y = region(x) +# ... +# z.backward() +# +# - "off". We assume that the backward of the torch.compile'd region will +# not be run under any autocast context managers. +# This is equivalent to running the following code in eager: +# +# with torch.amp.autocast(...): +# y = region(x) +# ... +# z.backward() +# +# - or a list of kwargs dicts that represent an autocast context manager to turn +# on during the backward pass. +# +# e.g. [{"device_type": "cuda"}] is equivalent to running the following code in eager: +# +# y = region(x) +# ... +# with torch.amp.autocast(device="cuda"): +# z.backward() +backward_pass_autocast = "same_as_forward" + +# This controls whether we collect donated buffers. This flag must be set +# False if a user wants to retain_graph=True for backward. +donated_buffer = not is_fbcode() + +# Controls the default graph output format used by draw_graph +# Supported formats are defined here https://graphviz.org/docs/outputs/ +torch_compile_graph_format = os.environ.get("TORCH_COMPILE_GRAPH_FORMAT", "svg") + +# Valid only if fake_tensor_propagate_real_tensors = True; if a fake-real +# kernel mismatch is detected, bypasses by making a fake kernel from the +# real tensor outputs. +generate_fake_kernels_from_real_mismatches = False + +# When there are device mismatches in FakeTensor device propagation, +# prefer a specific device type over others. This is particularly useful +# in full compiled mode where intermediate tensors with device mismatches +# represent only logical differences during compilation - these intermediate +# tensors will never physically materialize in the binary execution, so the +# device mismatch is not a real runtime concern. Enabling this allows the +# compiler to proceed with compilation by choosing the preferred device type +# for consistency. For example, set to "mtia" to prefer MTIA devices over +# CPU, or "cuda" to prefer CUDA devices over CPU. +fake_tensor_prefer_device_type: str | None = None + +# CUDAGraph safe run_with_rng functionalization. +# TODO: turn on by default +graphsafe_rng_functionalization = True + +# Whether or not to eagerly compile the backward +# used by AOT compile and other settings +# TODO: once AOT compile calls aot autograd directly instead of +# through compile_fx, we can remove this +force_non_lazy_backward_lowering = False + +# only for testing, used to turn functionalization off in AOTDispatcher +_test_disable_functionalization = True + +# Error on BypassAOTAutogradCache instead of just a warning +# Used for tests +strict_autograd_cache = False + +# Note [Recomputing collectives in the partitioner] +# The purpose of this config is as follows: +# - We have many passes in the compiler (min-cut partitioning, DCE, etc) +# which can reorder or delete duplicate nodes in the graph +# - If any of these passes reorder/delete/duplicate a collective +# in a setting where the compiler is being run independently on multiple +# ranks, we run the risk that the compiler will make a different decision on +# different ranks, resulting in a NCCL hang when using torch.compile +# To handle this, we will (by default) ensure that collectives are not modified +# by the compiler. +# +# A few examples: +# - don't dead-code-eliminate collectives +# (in case they are dead on rank i but not rank j) +# - don't recompute collectives in partitioning +# (in case we recompute on rank i but not rank j) +# +# Today this flag **must** be set to false, but eventually +# we want the option to set it to true. +# In order to potentially optimize collectives, we'll need the compiler +# to broadcast information across ranks at compile time to ensure +# that any decisions on collectives are made consistently. +unsafe_allow_optimization_of_collectives = False + +# See Note [AOTAutograd Tangent Subclassness for mutated inputs] +# TODO(ivankobzarev): Remove this config, being able to deduce it compile time. +disable_guess_zero_tangent_for_mutated_input_subclass = False + +# See Note [Tangents memory format] +# By default tangents strideness is guessed to be contiguous, +# At runtime non contiguous tangents will be coerced to be contiguous. +# This config changes this guess for tangents strides to be the same as outputs. +# TODO(ivankobzarev): Remove this config once extra memory usage is investigated. +guess_tangent_strides_as_outputs = not is_fbcode() + + +# This is a temporary config to ensure all ranks take the same decision in the partitioner +# it will ultimately be removed once we share size_hints across ranks through compiler collectives +_sync_decision_cross_ranks = False + +# By default apply inlined saved_tensors_hooks only for "donated" buffers. +# "donated" buffers are invisible to the user, they are intermediates of the forward graph. +# Applying saved tensors hooks for memory optimizations only for intermediates +# guarantees that original saved tensors could be deallocated. +# This config enables saved_tensors_hooks are applied for **all** saved tensors, +# that could include inputs, parameters, outputs. +# "donated" - applied only to saved intermediates of the graph +# "no_static" - applied to all saved but not "static" +# (this includes parameters and user marked as static) +# "all" - no filtering, everything saved for backward. +saved_tensors_hooks_filtering_mode = "donated" + + +# This callback is invoked on the joint graph before partitioning +joint_custom_pass: Callable = None # type: ignore[assignment] + +force_autograd_cache = False + +# Note [Selective Decomposition] +# This config allows selective decomposition of certain operators in the graph. +# When True, it does NOT decompose any nodes, except those nodes that users explicitly +# annotated with regional inductor compile. Please read torch.fx.passes.regional_inductor +# on to explicitly annotate. This is currently only used by inductor lite mode. +selective_decompose: bool = False + + +if TYPE_CHECKING: + from torch.utils._config_typing import * # noqa: F401, F403 + + +# adds patch, save_config, invalid config checks, etc +install_config_module(sys.modules[__name__]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/deprecated.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/deprecated.py new file mode 100644 index 0000000000000000000000000000000000000000..0e66a55bb6cbe18feb8ad87a5b51ce1e38e22b2a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/deprecated.py @@ -0,0 +1,188 @@ +""" +The APIs in this file are exposed as `functorch.*`. They are thin wrappers +around the torch.func.* APIs that have deprecation warnings -- we're trying +to move people to the torch.func.* equivalents. + +NB: We don't use *args, **kwargs in the signatures because that changes the +documentation. +""" + +from __future__ import annotations + +import textwrap +import warnings +from typing import Any, TYPE_CHECKING + +import torch._functorch.apis as apis +import torch._functorch.eager_transforms as _impl +import torch._functorch.make_functional as _nn_impl +import torch.nn as nn + + +if TYPE_CHECKING: + from collections.abc import Callable + + from torch._functorch.eager_transforms import argnums_t + from torch._functorch.vmap import in_dims_t, out_dims_t + + +def get_warning( + api: str, new_api: str | None = None, replace_newlines: bool = False +) -> str: + if new_api is None: + new_api = f"torch.func.{api}" + warning = ( + f"We've integrated functorch into PyTorch. As the final step of the \n" + f"integration, `functorch.{api}` is deprecated as of PyTorch \n" + f"2.0 and will be deleted in a future version of PyTorch >= 2.3. \n" + f"Please use `{new_api}` instead; see the PyTorch 2.0 release notes \n" + f"and/or the `torch.func` migration guide for more details \n" + f"https://pytorch.org/docs/main/func.migrating.html" + ) + if replace_newlines: + warning = warning.replace("\n", "") + return warning + + +def warn_deprecated(api: str, new_api: str | None = None) -> None: + warning = get_warning(api, new_api, replace_newlines=True) + warnings.warn(warning, FutureWarning, stacklevel=3) + + +def setup_docs( + functorch_api: Callable[..., Any], + torch_func_api: Callable[..., Any] | None = None, + new_api_name: str | None = None, +) -> None: + api_name = functorch_api.__name__ + if torch_func_api is None: + torch_func_api = getattr(_impl, api_name) + # See https://docs.python.org/3/using/cmdline.html#cmdoption-OO + if torch_func_api.__doc__ is None: + return + + warning = get_warning(api_name, new_api_name) + warning_note = "\n.. warning::\n\n" + textwrap.indent(warning, " ") + warning_note = textwrap.indent(warning_note, " ") + functorch_api.__doc__ = torch_func_api.__doc__ + warning_note + + +def vmap( + func: Callable[..., Any], + in_dims: in_dims_t = 0, + out_dims: out_dims_t = 0, + randomness: str = "error", + *, + chunk_size: int | None = None, +) -> Callable[..., Any]: + warn_deprecated("vmap", "torch.vmap") + return apis.vmap(func, in_dims, out_dims, randomness, chunk_size=chunk_size) + + +def grad( + func: Callable[..., Any], argnums: argnums_t = 0, has_aux: bool = False +) -> Callable[..., Any]: + warn_deprecated("grad") + return apis.grad(func, argnums, has_aux) + + +def grad_and_value( + func: Callable[..., Any], argnums: argnums_t = 0, has_aux: bool = False +) -> Callable[..., Any]: + warn_deprecated("grad_and_value") + return apis.grad_and_value(func, argnums, has_aux) + + +def vjp(func: Callable[..., Any], *primals: Any, has_aux: bool = False) -> Any: + warn_deprecated("vjp") + return _impl.vjp(func, *primals, has_aux=has_aux) + + +def jvp( + func: Callable[..., Any], + primals: Any, + tangents: Any, + *, + strict: bool = False, + has_aux: bool = False, +) -> Any: + warn_deprecated("jvp") + return _impl.jvp(func, primals, tangents, strict=strict, has_aux=has_aux) + + +def jacrev( + func: Callable[..., Any], + argnums: int | tuple[int, ...] = 0, + *, + has_aux: bool = False, + chunk_size: int | None = None, + _preallocate_and_copy: bool = False, +) -> Callable[..., Any]: + warn_deprecated("jacrev") + return _impl.jacrev( + func, + argnums, + has_aux=has_aux, + chunk_size=chunk_size, + _preallocate_and_copy=_preallocate_and_copy, + ) + + +def jacfwd( + func: Callable[..., Any], + argnums: argnums_t = 0, + has_aux: bool = False, + *, + randomness: str = "error", +) -> Callable[..., Any]: + warn_deprecated("jacfwd") + return _impl.jacfwd(func, argnums, has_aux, randomness=randomness) + + +def hessian(func: Callable[..., Any], argnums: int = 0) -> Callable[..., Any]: + warn_deprecated("hessian") + return _impl.hessian(func, argnums=argnums) + + +def functionalize( + func: Callable[..., Any], *, remove: str = "mutations" +) -> Callable[..., Any]: + warn_deprecated("functionalize") + return _impl.functionalize(func, remove=remove) + + +def make_functional(model: nn.Module, disable_autograd_tracking: bool = False) -> Any: + warn_deprecated("make_functional", "torch.func.functional_call") + return _nn_impl.make_functional(model, disable_autograd_tracking) + + +def make_functional_with_buffers( + model: nn.Module, disable_autograd_tracking: bool = False +) -> Any: + warn_deprecated("make_functional_with_buffers", "torch.func.functional_call") + return _nn_impl.make_functional_with_buffers(model, disable_autograd_tracking) + + +def combine_state_for_ensemble(models: list[nn.Module]) -> Any: + warn_deprecated("combine_state_for_ensemble", "torch.func.stack_module_state") + return _nn_impl.combine_state_for_ensemble(models) + + +setup_docs(vmap, apis.vmap, "torch.vmap") +setup_docs(grad, apis.grad) +setup_docs(grad_and_value, apis.grad_and_value) +setup_docs(vjp) +setup_docs(jvp) +setup_docs(jacrev) +setup_docs(jacfwd) +setup_docs(hessian) +setup_docs(functionalize) +setup_docs(make_functional, _nn_impl.make_functional, "torch.func.functional_call") +setup_docs( + make_functional_with_buffers, _nn_impl.make_functional, "torch.func.functional_call" +) +setup_docs( + combine_state_for_ensemble, + _nn_impl.combine_state_for_ensemble, + "torch.func.stack_module_state", +) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/eager_transforms.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/eager_transforms.py new file mode 100644 index 0000000000000000000000000000000000000000..b3bf0f078f8e61b12f2d5883098230338212486e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/eager_transforms.py @@ -0,0 +1,1951 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from __future__ import annotations + +import contextlib +from functools import partial, wraps +from typing import Any, overload, TYPE_CHECKING +from typing_extensions import ParamSpec, TypeVar + +import torch +import torch.autograd.forward_ad as fwAD +from torch._C._functorch import ( + _assert_wrapped_functional, # type: ignore[attr-defined] + _func_decrement_nesting, # type: ignore[attr-defined] + _func_increment_nesting, # type: ignore[attr-defined] + _grad_decrement_nesting, + _grad_increment_nesting, + _jvp_decrement_nesting, + _jvp_increment_nesting, + _propagate_functional_input_mutation, # type: ignore[attr-defined] + _unwrap_for_grad, + _unwrap_functional_tensor, + _wrap_for_grad, + _wrap_functional_tensor, + get_inplace_requires_grad_allowed, + get_unwrapped, + is_functorch_wrapped_tensor, + set_inplace_requires_grad_allowed, +) +from torch._functorch.utils import argnums_t, exposed_in +from torch._subclasses.functional_tensor import FunctionalTensor +from torch.fx.experimental import const_fold +from torch.fx.experimental.proxy_tensor import make_fx +from torch.utils import _pytree as pytree +from torch.utils._pytree import ( + tree_flatten, + tree_map, + tree_map_, + tree_map_only, + tree_unflatten, + treespec_pprint, +) + +from .apis import vmap +from .vmap import doesnt_support_saved_tensors_hooks, get_chunk_sizes + + +if TYPE_CHECKING: + from collections.abc import Callable, Generator, Sequence + + +_P = ParamSpec("_P") +_R = TypeVar("_R") +_T = TypeVar("_T") + + +def lazy_dynamo_disallow(func: Callable[_P, _R]) -> Callable[_P, _R]: + import torch._dynamo + + return torch._dynamo.disallow_in_graph(func) + + +@contextlib.contextmanager +def enable_inplace_requires_grad(enabled: bool) -> Generator[None, None, None]: + prev_state = get_inplace_requires_grad_allowed() + set_inplace_requires_grad_allowed(enabled) + try: + yield + finally: + set_inplace_requires_grad_allowed(prev_state) + + +def _set_tensor_requires_grad(x: torch.Tensor) -> torch.Tensor: + # avoid graph-break on x.requires_grad_() + # https://github.com/pytorch/pytorch/pull/110053 + return x.requires_grad_() + + +def _create_differentiable(inps: Any, level: int | None = None) -> Any: + def create_differentiable(x: torch.Tensor | Any) -> torch.Tensor: + if isinstance(x, torch.Tensor): + with enable_inplace_requires_grad(True): + return _set_tensor_requires_grad(x) + raise ValueError(f"Thing passed to transform API must be Tensor, got {type(x)}") + + return tree_map(create_differentiable, inps) + + +_Tensors = torch.Tensor | tuple[torch.Tensor, ...] +_TensorsT = TypeVar("_TensorsT", bound=_Tensors) +_TensorsU = TypeVar("_TensorsU", bound=_Tensors) + + +def _undo_create_differentiable(inps: _TensorsT, level: int | None = None) -> _TensorsT: + def unwrap_tensors(x: _TensorsU) -> _TensorsU: + if isinstance(x, torch.Tensor): + if level is None: + raise AssertionError("level must not be None when unwrapping tensors") + # pyrefly: ignore[bad-return] + return _unwrap_for_grad(x, level) + # TODO: Remove the following hack for namedtuples + if isinstance(x, tuple): + return tree_map(unwrap_tensors, tuple(x)) + + raise RuntimeError(f"Expected tensors, got unsupported type {type(x)}") + + return tree_map(unwrap_tensors, inps) + + +def _is_differentiable(maybe_tensor: object) -> bool: + if not isinstance(maybe_tensor, torch.Tensor): + return False + return maybe_tensor.requires_grad + + +def _any_differentiable( + tensor_or_tuple_of_tensors: _Tensors, +) -> bool: + flat_args, _ = tree_flatten(tensor_or_tuple_of_tensors) + return any(tuple(map(_is_differentiable, flat_args))) + + +def _wrap_tensor_for_grad(maybe_tensor: _T, level: int) -> _T: + if not isinstance(maybe_tensor, torch.Tensor): + return maybe_tensor + # pyrefly: ignore[bad-return] + return _wrap_for_grad(maybe_tensor, level) + + +def _wrap_all_tensors(tensor_pytree: _T, level: int) -> _T: + return tree_map(partial(_wrap_tensor_for_grad, level=level), tensor_pytree) + + +# TODO: this is more accurate - enable in 3.11 +# _Ts = TypeVarTuple("_Ts") +# @overload +# def _as_tuple(val: tuple[*_Ts]) -> tuple[*_Ts]: ... +@overload +def _as_tuple(val: tuple[_R, ...]) -> tuple[_R]: ... +@overload +def _as_tuple(val: _R) -> tuple[_R]: ... +def _as_tuple(val: object) -> object: + if isinstance(val, tuple): + return val + return (val,) + + +# Version of autograd.grad that handles outputs that don't depend on inputs + + +def _autograd_grad( + outputs: Sequence[torch.Tensor], + inputs: Sequence[torch.Tensor], + grad_outputs: Sequence[torch.Tensor] | None = None, + retain_graph: bool = False, + create_graph: bool = True, +) -> tuple[torch.Tensor, ...]: + if grad_outputs is None: + diff_outputs = tuple(out for out in outputs if out.requires_grad) + else: + result = tuple( + (out, go) for out, go in zip(outputs, grad_outputs) if out.requires_grad + ) + if len(result) == 0: + diff_outputs, grad_outputs = (), () + else: + diff_outputs, grad_outputs = zip(*result) + if len(diff_outputs) == 0: + return tuple(torch.zeros_like(inp) for inp in inputs) + with torch._dynamo.compiled_autograd._disable(): + grad_inputs = torch.autograd.grad( + diff_outputs, + inputs, + grad_outputs, + retain_graph=retain_graph, + create_graph=create_graph, + allow_unused=True, + ) + grad_inputs = tuple( + torch.zeros_like(inp) if gi is None else gi + for gi, inp in zip(grad_inputs, inputs) + ) + return grad_inputs + + +# NOTE [grad and vjp interaction with no_grad] +# +# def f(x): +# with torch.no_grad(): +# c = x ** 2 +# return x - c +# +# The thing to consider is if enable_grad is on/off before grad gets called. +# +# Case 1: enable_grad is on. +# grad(f)(x) +# In this case, `grad` should respect the inner torch.no_grad. +# +# Case 2: enable_grad is off +# with torch.no_grad(): +# grad(f)(x) +# In this case, `grad` should respect the inner torch.no_grad, but not the +# outer one. This is because `grad` is a "function transform": its result +# should not depend on the result of a context manager outside of `f`. +# +# This gives us the following desired behavior: +# - (nested) grad transforms must obey torch.no_grad inside them +# - (nested) grad transforms should not obey torch.no_grad outside them +# +# To achieve this behavior, upon entering grad/vjp: +# - we save the current ("previous") is_grad_enabled (*) +# - we unconditionally enable grad. +# +# Inside DynamicLayerBackFallback, when we're temporarily popping `grad` layer +# off the stack: +# - if grad_mode is disabled, then we do nothing. (there is a torch.no_grad +# active, all subsequent grad transforms must obey it). +# - if grad_mode is enabled, and the previous is_grad_enabled (*) is False, +# then we temporarily restore the previous `is_grad_enabled`. This is +# because we're crossing the boundary from a `grad` outside the +# no_grad to a `grad` inside the no_grad. +# +# NB: vjp has some interesting behavior because the vjp's Callable[..., Any] can be called +# under a different grad_mode than the forward computation... +# +# NB: forward-mode AD: forward-mode AD doesn't respect torch.no_grad, but +# it respects c10::AutoFwGradMode. We've implemented the same logic for +# our jvp transform (it will have special handling if FwGradMode is disabled). + + +# How do we increment and decrement the nesting? I don't think we can. +@exposed_in("torch.func") +def vjp( + func: Callable[..., Any], *primals: Any, has_aux: bool = False +) -> tuple[Any, Callable[..., Any]] | tuple[Any, Callable[..., Any], Any]: + """ + Standing for the vector-Jacobian product, returns a tuple containing the + results of ``func`` applied to ``primals`` and a function that, when + given ``cotangents``, computes the reverse-mode Jacobian of ``func`` with + respect to ``primals`` times ``cotangents``. + + Args: + func (Callable[..., Any]): A Python function that takes one or more arguments. Must + return one or more Tensors. + primals (Tensors): Positional arguments to ``func`` that must all be + Tensors. The returned function will also be computing the + derivative with respect to these arguments + has_aux (bool): Flag indicating that ``func`` returns a + ``(output, aux)`` tuple where the first element is the output of + the function to be differentiated and the second element is + other auxiliary objects that will not be differentiated. + Default: False. + + Returns: + Returns a ``(output, vjp_fn)`` tuple containing the output of ``func`` + applied to ``primals`` and a function that computes the vjp of + ``func`` with respect to all ``primals`` using the cotangents passed + to the returned function. If ``has_aux is True``, then instead returns a + ``(output, vjp_fn, aux)`` tuple. + The returned ``vjp_fn`` function will return a tuple of each VJP. + + When used in simple cases, :func:`vjp` behaves the same as :func:`grad` + + >>> x = torch.randn([5]) + >>> f = lambda x: x.sin().sum() + >>> (_, vjpfunc) = torch.func.vjp(f, x) + >>> grad = vjpfunc(torch.tensor(1.0))[0] + >>> assert torch.allclose(grad, torch.func.grad(f)(x)) + + However, :func:`vjp` can support functions with multiple outputs by + passing in the cotangents for each of the outputs + + >>> x = torch.randn([5]) + >>> f = lambda x: (x.sin(), x.cos()) + >>> (_, vjpfunc) = torch.func.vjp(f, x) + >>> vjps = vjpfunc((torch.ones([5]), torch.ones([5]))) + >>> assert torch.allclose(vjps[0], x.cos() + -x.sin()) + + :func:`vjp` can even support outputs being Python structs + + >>> x = torch.randn([5]) + >>> f = lambda x: {"first": x.sin(), "second": x.cos()} + >>> (_, vjpfunc) = torch.func.vjp(f, x) + >>> cotangents = {"first": torch.ones([5]), "second": torch.ones([5])} + >>> vjps = vjpfunc(cotangents) + >>> assert torch.allclose(vjps[0], x.cos() + -x.sin()) + + The function returned by :func:`vjp` will compute the partials with + respect to each of the ``primals`` + + >>> x, y = torch.randn([5, 4]), torch.randn([4, 5]) + >>> (_, vjpfunc) = torch.func.vjp(torch.matmul, x, y) + >>> cotangents = torch.randn([5, 5]) + >>> vjps = vjpfunc(cotangents) + >>> assert len(vjps) == 2 + >>> assert torch.allclose(vjps[0], torch.matmul(cotangents, y.transpose(0, 1))) + >>> assert torch.allclose(vjps[1], torch.matmul(x.transpose(0, 1), cotangents)) + + ``primals`` are the positional arguments for ``f``. All kwargs use their + default value + + >>> x = torch.randn([5]) + >>> def f(x, scale=4.): + >>> return x * scale + >>> + >>> (_, vjpfunc) = torch.func.vjp(f, x) + >>> vjps = vjpfunc(torch.ones_like(x)) + >>> assert torch.allclose(vjps[0], torch.full(x.shape, 4.0)) + + .. note:: + Using PyTorch ``torch.no_grad`` together with ``vjp``. + Case 1: Using ``torch.no_grad`` inside a function: + + >>> def f(x): + >>> with torch.no_grad(): + >>> c = x ** 2 + >>> return x - c + + In this case, ``vjp(f)(x)`` will respect the inner ``torch.no_grad``. + + Case 2: Using ``vjp`` inside ``torch.no_grad`` context manager: + + >>> # xdoctest: +SKIP(failing) + >>> with torch.no_grad(): + >>> vjp(f)(x) + + In this case, ``vjp`` will respect the inner ``torch.no_grad``, but not the + outer one. This is because ``vjp`` is a "function transform": its result + should not depend on the result of a context manager outside of ``f``. + """ + return _vjp_with_argnums(func, *primals, has_aux=has_aux) + + +@contextlib.contextmanager +def _disable_inference_mode() -> Generator[None, None, None]: + # Disable inference_mode without clobbering grad_mode / fw_grad_mode. + # torch.inference_mode(False) unconditionally sets grad_mode=True and + # fw_grad_mode=True; we save and restore those to avoid that. + # No-op when inference_mode is already off. + if not torch.is_inference_mode_enabled(): + yield + return + prev_grad = torch.is_grad_enabled() + prev_fw_grad = torch._C._is_fwd_grad_enabled() + with torch.inference_mode(False): + torch._C._set_grad_enabled(prev_grad) + torch._C._set_fwd_grad_enabled(prev_fw_grad) + yield + + +@contextlib.contextmanager +def grad_increment_nesting() -> Generator[int, None, None]: + try: + grad_level = _grad_increment_nesting() + yield grad_level + finally: + _grad_decrement_nesting() + + +def enter_jvp_nesting() -> int: + global JVP_NESTING + jvp_level = _jvp_increment_nesting() + JVP_NESTING += 1 + return jvp_level + + +def exit_jvp_nesting() -> None: + global JVP_NESTING + _jvp_decrement_nesting() + JVP_NESTING -= 1 + + +@contextlib.contextmanager +def jvp_increment_nesting() -> Generator[int, None, None]: + try: + yield enter_jvp_nesting() + finally: + exit_jvp_nesting() + + +@doesnt_support_saved_tensors_hooks +def _vjp_with_argnums( + func: Callable[..., Any], + *primals: Any, + argnums: argnums_t | None = None, + has_aux: bool = False, +) -> tuple[Any, Callable[..., Any]] | tuple[Any, Callable[..., Any], Any]: + # This is the same function as vjp but also accepts an argnums argument + # All args are the same as vjp except for the added argument + # argnums (int or tuple[int,...] | None): Optional, specifies the argument(s) to compute gradients with respect to. + # If None, computes the gradients with respect to all inputs (used for vjp). Default: None + # + # WARN: Users should NOT call this function directly and should just be calling vjp. + # It is only separated so that inputs passed to jacrev but not differentiated get the correct wrappers. + # + # NOTE: All error messages are produced as if vjp was being called, even if this was called by jacrev + # + # Returns the same two elements as :func:`vjp` but the function returned, vjp_fn, returns a tuple of VJPs + # for only the primal elements given by argnums. + with grad_increment_nesting() as level: + # See NOTE [grad and vjp interaction with no_grad] + with torch.enable_grad(): + primals = _wrap_all_tensors(primals, level) + if argnums is None: + diff_primals = _create_differentiable(primals, level) + else: + diff_primals = _slice_argnums(primals, argnums, as_tuple=False) + tree_map_(partial(_create_differentiable, level=level), diff_primals) + primals_out = func(*primals) + + aux: Any = None + if has_aux: + if not (isinstance(primals_out, tuple) and len(primals_out) == 2): + raise RuntimeError( + "vjp(f, *primals): output of function f should be a tuple: (output, aux) " + "if has_aux is True" + ) + primals_out, aux = primals_out + aux = _undo_create_differentiable(aux, level) + + flat_primals_out, primals_out_spec = tree_flatten(primals_out) + assert_non_empty_tensor_output(flat_primals_out, "vjp(f, *primals)") + flat_diff_primals, primals_spec = tree_flatten(diff_primals) + results = _undo_create_differentiable(primals_out, level) + + for primal_out in flat_primals_out: + if not isinstance(primal_out, torch.Tensor): + raise AssertionError( + f"expected primal_out to be a Tensor, got {type(primal_out)}" + ) + if primal_out.is_floating_point() or primal_out.is_complex(): + continue + raise RuntimeError( + "vjp(f, ...): All outputs of f must be " + "floating-point or complex Tensors, got Tensor " + f"with dtype {primal_out.dtype}" + ) + + def wrapper( + cotangents: Any, + retain_graph: bool = True, + create_graph: bool | None = None, + ) -> Any: + if create_graph is None: + create_graph = torch.is_grad_enabled() + flat_cotangents, cotangents_spec = tree_flatten(cotangents) + if primals_out_spec != cotangents_spec: + raise RuntimeError( + f"Expected pytree structure of cotangents to be the same " + f"as pytree structure of outputs to the function. " + f"cotangents: {treespec_pprint(cotangents_spec)}, " + f"primal output: {treespec_pprint(primals_out_spec)}" + ) + # This closure runs after grad_increment_nesting exits, so + # inference_mode may have been restored. Disable it for autograd. + # Skip under Dynamo — tracing through the generator CM emits + # spurious _enter_inference_mode nodes. + ctx = ( + contextlib.nullcontext() + if torch.compiler.is_compiling() + else _disable_inference_mode() + ) + with ctx: + result = _autograd_grad( + flat_primals_out, + flat_diff_primals, + flat_cotangents, + retain_graph=retain_graph, + create_graph=create_graph, + ) + return tree_unflatten(result, primals_spec) + + if has_aux: + return results, wrapper, aux # type: ignore[possibly-unbound] + else: + return results, wrapper + + +def _safe_zero_index(x: tuple[_R, ...]) -> _R: + if len(x) != 1: + raise AssertionError(f"expected tuple of length 1, got length {len(x)}") + return x[0] + + +# jacrev and jacfwd don't support complex functions +# Helper function to throw appropriate error. +def error_if_complex(func_name: str, args: Any, is_input: bool) -> None: + flat_args = pytree.tree_leaves(args) + for idx, arg in enumerate(flat_args): + if isinstance(arg, torch.Tensor) and arg.dtype.is_complex: + input_or_output = "inputs" if is_input else "outputs" + err_msg = ( + f"{func_name}: Expected all {input_or_output} " + f"to be real but received complex tensor at flattened input idx: {idx}" + ) + raise RuntimeError(err_msg) + + +@exposed_in("torch.func") +def jacrev( + func: Callable[..., Any], + argnums: int | tuple[int, ...] = 0, + *, + has_aux: bool = False, + chunk_size: int | None = None, + _preallocate_and_copy: bool = False, +) -> Callable[..., Any]: + """ + Computes the Jacobian of ``func`` with respect to the arg(s) at index + ``argnum`` using reverse mode autodiff + + .. note:: + Using :attr:`chunk_size=1` is equivalent to computing the jacobian + row-by-row with a for-loop i.e. the constraints of :func:`vmap` are + not applicable. + + Args: + func (function): A Python function that takes one or more arguments, + one of which must be a Tensor, and returns one or more Tensors + argnums (int or tuple[int, ...]): Optional, integer or tuple of integers, + saying which arguments to get the Jacobian with respect to. + Default: 0. + has_aux (bool): Flag indicating that ``func`` returns a + ``(output, aux)`` tuple where the first element is the output of + the function to be differentiated and the second element is + auxiliary objects that will not be differentiated. + Default: False. + chunk_size (None or int): If None (default), use the maximum chunk size + (equivalent to doing a single vmap over vjp to compute the jacobian). + If 1, then compute the jacobian row-by-row with a for-loop. + If not None, then compute the jacobian :attr:`chunk_size` rows at a time + (equivalent to doing multiple vmap over vjp). If you run into memory issues computing + the jacobian, please try to specify a non-None chunk_size. + + Returns: + Returns a function that takes in the same inputs as ``func`` and + returns the Jacobian of ``func`` with respect to the arg(s) at + ``argnums``. If ``has_aux is True``, then the returned function + instead returns a ``(jacobian, aux)`` tuple where ``jacobian`` + is the Jacobian and ``aux`` is auxiliary objects returned by ``func``. + + A basic usage with a pointwise, unary operation will give a diagonal array + as the Jacobian + + >>> from torch.func import jacrev + >>> x = torch.randn(5) + >>> jacobian = jacrev(torch.sin)(x) + >>> expected = torch.diag(torch.cos(x)) + >>> assert torch.allclose(jacobian, expected) + + If you would like to compute the output of the function as well as the + jacobian of the function, use the ``has_aux`` flag to return the output + as an auxiliary object: + + >>> from torch.func import jacrev + >>> x = torch.randn(5) + >>> + >>> def f(x): + >>> return x.sin() + >>> + >>> def g(x): + >>> result = f(x) + >>> return result, result + >>> + >>> jacobian_f, f_x = jacrev(g, has_aux=True)(x) + >>> assert torch.allclose(f_x, f(x)) + + :func:`jacrev` can be composed with vmap to produce batched + Jacobians: + + >>> from torch.func import jacrev, vmap + >>> x = torch.randn(64, 5) + >>> jacobian = vmap(jacrev(torch.sin))(x) + >>> assert jacobian.shape == (64, 5, 5) + + Additionally, :func:`jacrev` can be composed with itself to produce + Hessians + + >>> from torch.func import jacrev + >>> def f(x): + >>> return x.sin().sum() + >>> + >>> x = torch.randn(5) + >>> hessian = jacrev(jacrev(f))(x) + >>> assert torch.allclose(hessian, torch.diag(-x.sin())) + + By default, :func:`jacrev` computes the Jacobian with respect to the first + input. However, it can compute the Jacboian with respect to a different + argument by using ``argnums``: + + >>> from torch.func import jacrev + >>> def f(x, y): + >>> return x + y ** 2 + >>> + >>> x, y = torch.randn(5), torch.randn(5) + >>> jacobian = jacrev(f, argnums=1)(x, y) + >>> expected = torch.diag(2 * y) + >>> assert torch.allclose(jacobian, expected) + + Additionally, passing a tuple to ``argnums`` will compute the Jacobian + with respect to multiple arguments + + >>> from torch.func import jacrev + >>> def f(x, y): + >>> return x + y ** 2 + >>> + >>> x, y = torch.randn(5), torch.randn(5) + >>> jacobian = jacrev(f, argnums=(0, 1))(x, y) + >>> expectedX = torch.diag(torch.ones_like(x)) + >>> expectedY = torch.diag(2 * y) + >>> assert torch.allclose(jacobian[0], expectedX) + >>> assert torch.allclose(jacobian[1], expectedY) + + .. note:: + Using PyTorch ``torch.no_grad`` together with ``jacrev``. + Case 1: Using ``torch.no_grad`` inside a function: + + >>> def f(x): + >>> with torch.no_grad(): + >>> c = x ** 2 + >>> return x - c + + In this case, ``jacrev(f)(x)`` will respect the inner ``torch.no_grad``. + + Case 2: Using ``jacrev`` inside ``torch.no_grad`` context manager: + + >>> with torch.no_grad(): + >>> jacrev(f)(x) + + In this case, ``jacrev`` will respect the inner ``torch.no_grad``, but not the + outer one. This is because ``jacrev`` is a "function transform": its result + should not depend on the result of a context manager outside of ``f``. + """ + if not (chunk_size is None or chunk_size > 0): + raise ValueError("jacrev: `chunk_size` should be greater than 0.") + + @wraps(func) + def wrapper_fn(*args: Any) -> Any: + error_if_complex("jacrev", args, is_input=True) + vjp_out = _vjp_with_argnums(func, *args, argnums=argnums, has_aux=has_aux) + aux: Any = None + if has_aux: + # pyrefly: ignore[bad-unpacking] + output, vjp_fn, aux = vjp_out + else: + # pyrefly: ignore[bad-unpacking] + output, vjp_fn = vjp_out + + # See NOTE: [Computing jacobian with vmap and vjp for multiple outputs] + flat_output, output_spec = tree_flatten(output) + + error_if_complex("jacrev", flat_output, is_input=False) + + # NB: vjp already checks that all outputs are tensors + # Step 1: Construct grad_outputs by splitting the standard basis + flat_output_numels = tuple(out.numel() for out in flat_output) + + primals = _slice_argnums(args, argnums) + flat_primals, primals_spec = tree_flatten(primals) + + def compute_jacobian_stacked() -> list[Any]: + # Helper function to compute chunked Jacobian + # The intermediate chunked calculation are only + # scoped at this function level. + chunked_results: list[list[Any]] = [] + for flat_basis_chunk in _chunked_standard_basis_for_( + flat_output, flat_output_numels, chunk_size=chunk_size + ): + if chunk_size == 1: + # sanity check. + for t in flat_basis_chunk: + if t.size(0) != 1: + raise AssertionError( + f"expected t.size(0) to be 1, got {t.size(0)}" + ) + + flat_basis_chunk = tree_map( + lambda t: torch.squeeze(t, 0), flat_basis_chunk + ) + + basis = tree_unflatten(flat_basis_chunk, output_spec) + + if chunk_size == 1: + # Behaviour with `chunk_size=1` is same as `for-loop` + # i.e. user shouldn't deal with the limitations of vmap. + chunked_result = vjp_fn(basis) + else: # chunk_size is None or chunk_size != 1 + chunked_result = vmap(vjp_fn)(basis) + + flat_results = pytree.tree_leaves(chunked_result) + + if chunk_size == 1: + flat_results = tree_map( + lambda t: torch.unsqueeze(t, 0), flat_results + ) + + chunked_results.append(flat_results) + + if len(chunked_results) == 1: + # Short-circuit if we used a single chunk + return chunked_results[0] + + # Concatenate chunks. + flat_results = [] + # Iterate and concat the jacobians of different + # inputs. + for idx in range(len(flat_primals)): + r = tuple(r_[idx] for r_ in chunked_results) + flat_results.append(torch.cat(r, 0)) + + return flat_results + + def compute_jacobian_preallocate_and_copy() -> list[Any]: + # Helper function to compute chunked Jacobian + # The intermediate chunked calculation are only + # scoped at this function level. + out_vec_size = sum(flat_output_numels) + + # Don't pre-allocate if we have a single chunk. + stacked_results: list[torch.Tensor] = [] + if not (chunk_size is None or chunk_size >= out_vec_size): + stacked_results = [ + primal.new_zeros(out_vec_size, *primal.shape) + for primal in flat_primals + ] + + for idx, flat_basis_chunk in enumerate( + _chunked_standard_basis_for_( + flat_output, flat_output_numels, chunk_size=chunk_size + ) + ): + if chunk_size == 1: + # sanity check. + for t in flat_basis_chunk: + if t.size(0) != 1: + raise AssertionError( + f"expected t.size(0) to be 1, got {t.size(0)}" + ) + + flat_basis_chunk = [torch.squeeze(t, 0) for t in flat_basis_chunk] + + basis = tree_unflatten(flat_basis_chunk, output_spec) + + if chunk_size == 1: + # Behaviour with `chunk_size=1` is same as `for-loop` + # i.e. user shouldn't deal with the limitations of vmap. + chunked_result = vjp_fn(basis) + else: # chunk_size is None or chunk_size != 1 + chunked_result = vmap(vjp_fn)(basis) + + flat_results = pytree.tree_leaves(chunked_result) + + # Short-circuit if we have a single chunk. + if chunk_size is None or chunk_size >= out_vec_size: + if chunk_size == 1: # and out_vec_size == 1 + # Since we squeezed the output dim + flat_results = tree_map( + lambda t: torch.unsqueeze(t, 0), flat_results + ) + return flat_results + + for r, sr in zip(flat_results, stacked_results): # type: ignore[possibly-unbound] + sr[idx * chunk_size : (idx + 1) * chunk_size].copy_(r) + + return stacked_results # type: ignore[possibly-unbound] + + if _preallocate_and_copy: + flat_jacobians_per_input = compute_jacobian_preallocate_and_copy() + else: + flat_jacobians_per_input = compute_jacobian_stacked() + + # Step 2: The returned jacobian is one big tensor per input. In this step, + # we split each Tensor by output. + flat_jacobians_per_input = [ + result.split(flat_output_numels, dim=0) + for result in flat_jacobians_per_input + ] + flat_input_flat_output = [ + tuple( + split.view(out.shape + primal.shape) + for split, out in zip(splits, flat_output) + ) + for splits, primal in zip(flat_jacobians_per_input, flat_primals) + ] + + # Step 3: Right now, `jacobian` is a List[List[Tensor]]. + # The outer List corresponds to the number of primals, + # the inner List corresponds to the number of outputs. + # We need to: + # a. Exchange the order of the outer List and inner List + # b. tree_unflatten the inner Lists (which correspond to the primals) + # c. handle the argnums=int case + # d. tree_unflatten the outer List (which corresponds to the outputs) + flat_output_flat_input = tuple(zip(*flat_input_flat_output)) + + flat_output_input = tuple( + tree_unflatten(flat_input, primals_spec) + for flat_input in flat_output_flat_input + ) + + if isinstance(argnums, int): + flat_output_input = tuple( + _safe_zero_index(flat_input) for flat_input in flat_output_input + ) + output_input = tree_unflatten(flat_output_input, output_spec) + if has_aux: + return output_input, aux # type: ignore[possibly-unbound] + return output_input + + return wrapper_fn + + +# NOTE: [Computing jacobian with vmap and vjp for multiple outputs] +# +# Let's consider f(x) = (x**2, x.sum()) and let x = torch.randn(3). +# It turns out we can compute the jacobian of this function with a single +# call to autograd.grad by using vmap over the correct grad_outputs. +# +# Firstly, one way to compute the jacobian is to stack x**2 and x.sum() +# into a 4D vector. E.g., use g(x) = torch.stack([x**2, x.sum()]) +# +# To get the first row of the jacobian, we call +# >>> autograd.grad(g(x), x, grad_outputs=torch.tensor([1, 0, 0, 0])) +# To get the 2nd row of the jacobian, we call +# >>> autograd.grad(g(x), x, grad_outputs=torch.tensor([0, 1, 0, 0])) +# and so on. +# +# Using vmap, we can vectorize all 4 of these computations into one by +# passing the standard basis for R^4 as the grad_output. +# vmap(partial(autograd.grad, g(x), x))(torch.eye(4)). +# +# Now, how do we compute the jacobian *without stacking the output*? +# We can just split the standard basis across the outputs. So to +# compute the jacobian of f(x), we'd use +# >>> autograd.grad(f(x), x, grad_outputs=_construct_standard_basis_for(...)) +# The grad_outputs looks like the following: +# ( torch.tensor([[1, 0, 0], +# [0, 1, 0], +# [0, 0, 1], +# [0, 0, 0]]), +# torch.tensor([[0], +# [0], +# [0], +# [1]]) ) +# +# But we're not done yet! +# >>> vmap(partial(autograd.grad(f(x), x, grad_outputs=...))) +# returns a Tensor of shape [4, 3]. We have to remember to split the +# jacobian of shape [4, 3] into two: +# - one of shape [3, 3] for the first output +# - one of shape [ 3] for the second output + + +def _chunked_standard_basis_for_( + tensors: Sequence[torch.Tensor], + tensor_numels: Sequence[int], + chunk_size: int | None = None, +) -> Generator[tuple[torch.Tensor, ...], None, None]: + # This function: + # - constructs a N=sum(tensor_numels) standard basis. i.e. an NxN identity matrix. + # - Splits the identity matrix into chunks with each chunk size determined by `tensor_numels`. + # - Each chunk corresponds to one tensor. The chunk has the same dtype and + # device as the tensor + # + # For example, with tensor_numels = [1, 2, 1], this function returns: + # ( tensor([[1], tensor([[0, 0], tensor([[0], + # [0], [1, 0], [0], + # [0], [0, 1], [0], + # [0]]) , [0, 0]]) , [1]]) ) + # + # Precondition: tensor_numels == tuple(tensor.numel() for tensor in tensors) + # Precondition: tensors always has at least one element. + # + # See NOTE: [Computing jacobian with vmap and grad for multiple tensors] + # for context behind this function. + # NOTE: Argument `chunk_size` is used to generate chunked basis instead of + # one huge basis matrix. `chunk_size` dictates the maximum size of the + # basis matrix along dim=0. + if len(tensors) != len(tensor_numels): + raise AssertionError( + f"len(tensors)={len(tensors)} != len(tensor_numels)={len(tensor_numels)}" + ) + if len(tensors) == 0: + raise AssertionError("tensors must have at least one element") + if chunk_size is not None and chunk_size <= 0: + raise AssertionError(f"chunk_size must be > 0 or None, got {chunk_size}") + total_numel = sum(tensor_numels) + if chunk_size and chunk_size < total_numel: + chunk_numels = get_chunk_sizes(total_numel, chunk_size) + else: # chunk_size is None or chunk_size >= total_numel + chunk_size = total_numel + chunk_numels = [total_numel] + + diag_start_indices = ( + 0, + *torch.tensor(tensor_numels).cumsum(dim=0)[:-1].neg().unbind(), + ) + + for chunk_idx, total_numel in enumerate(chunk_numels): + chunks = tuple( + tensor.new_zeros(total_numel, tensor_numel) + for tensor, tensor_numel in zip(tensors, tensor_numels) + ) + + for chunk, diag_start_idx in zip(chunks, diag_start_indices): + offset = int(diag_start_idx) + chunk_idx * chunk_size + chunk.diagonal(offset).fill_(1) + chunks = tuple( + chunk.view(total_numel, *tensor.shape) + for chunk, tensor in zip(chunks, tensors) + ) + yield chunks + + +def _construct_standard_basis_for( + tensors: Sequence[torch.Tensor], tensor_numels: Sequence[int] +) -> tuple[torch.Tensor, ...] | None: + for basis in _chunked_standard_basis_for_(tensors, tensor_numels, chunk_size=None): + return basis + return None + + +def _validate_and_wrap_argnum(argnum: int, num_args: int) -> int: + if not isinstance(argnum, int): + raise RuntimeError(f"argnum must be int, got: {type(argnum)}") + if argnum >= 0 and argnum < num_args: + return argnum + if argnum < 0 and argnum >= -num_args: + return argnum + num_args + raise RuntimeError(f"Got argnum={argnum}, but only {num_args} positional inputs") + + +def _check_unique_non_empty(argnums: argnums_t) -> None: + if isinstance(argnums, tuple): + if len(argnums) == 0: + raise RuntimeError("argnums must be non-empty") + if len(set(argnums)) != len(argnums): + raise RuntimeError(f"argnums elements must be unique, got {argnums}") + + +def _replace_args( + old_args: tuple[Any, ...], new_args: tuple[Any, ...], argnums: argnums_t +) -> tuple[Any, ...]: + if isinstance(argnums, int): + if len(new_args) != 1: + raise RuntimeError( + f"new_args should be of size 1, was of size {len(new_args)}" + ) + return tuple( + new_args[0] if i == argnums else old_args[i] for i in range(len(old_args)) + ) + if isinstance(argnums, tuple): + if len(new_args) != len(argnums): + raise RuntimeError( + "new_args should have the same size as argnums. " + f"Argnums size {len(argnums)}, new_args size {len(new_args)}" + ) + + argnums_tuple = argnums + + def get_right_elem(i: int) -> Any: + return ( + new_args[argnums_tuple.index(i)] if i in argnums_tuple else old_args[i] + ) + + return tuple(get_right_elem(i) for i in range(len(old_args))) + raise RuntimeError(f"argnums must be int or Tuple[int, ...], got: {type(argnums)}") + + +def _validate_and_wrap_argnums(argnums: argnums_t, num_args: int) -> argnums_t: + if isinstance(argnums, int): + return _validate_and_wrap_argnum(argnums, num_args) + if isinstance(argnums, tuple): + return tuple(_validate_and_wrap_argnum(argnum, num_args) for argnum in argnums) + raise AssertionError("Should never get here") + + +def _slice_argnums( + args: tuple[Any, ...], argnums: argnums_t, as_tuple: bool = True +) -> Any: + if not isinstance(argnums, int) and not isinstance(argnums, tuple): + raise RuntimeError( + f"argnums must be int or Tuple[int, ...], got: {type(argnums)}" + ) + argnums = _validate_and_wrap_argnums(argnums, len(args)) + _check_unique_non_empty(argnums) + if isinstance(argnums, int): + if as_tuple: + return (args[argnums],) + else: + return args[argnums] + return tuple(args[i] for i in argnums) + + +JVP_NESTING = 0 + + +def assert_flat_tuple_of_tensors(elts: Any, api: str, argname: str) -> None: + if not isinstance(elts, tuple): + raise RuntimeError( + f"{api}: Expected {argname} to be a tuple of Tensors, got {type(elts)}" + ) + for elt in elts: + if isinstance(elt, torch.Tensor): + continue + raise RuntimeError( + f"{api}: Expected {argname} to be a tuple of Tensors, got " + f"a tuple with an element of type {type(elt)}" + ) + if len(elts) == 0: + raise RuntimeError( + f"{api}: Expected {argname} to be a non-empty tuple of Tensors." + ) + + +def assert_non_empty_tensor_output(output: list[Any], api: str) -> None: + if (len(output) == 1 and output[0] is None) or len(output) < 1: + raise RuntimeError( + f"{api}: Expected f to be a function that has non-empty output (got output = {output})" + ) + for o in output: + if not isinstance(o, torch.Tensor): + raise RuntimeError( + f"{api}: expected f(*primals) to return only tensors" + f", got unsupported type {type(o)}" + ) + + +def assert_output_is_tensor_or_tensors(output: Any, api: str) -> None: + if isinstance(output, torch.Tensor): + return + if not isinstance(output, tuple): + raise RuntimeError( + f"{api}: Expected output of f to be a Tensor or Tensors, got {type(output)}" + ) + if len(output) == 0: + raise RuntimeError( + f"{api}: Expected output of f to be a non-empty tuple of Tensors." + ) + for out in output: + if isinstance(out, torch.Tensor): + continue + raise RuntimeError( + f"{api}: Expected output of f to be a Tensor or Tensors, got " + f"{type(out)} as an output" + ) + + +def assert_non_empty_list_of_tensors( + output: list[torch.Tensor], api: str, argname: str +) -> None: + if len(output) == 0: + raise RuntimeError(f"{api}: Expected {argname} to contain at least one Tensor.") + for out in output: + if isinstance(out, torch.Tensor): + continue + raise RuntimeError( + f"{api}: Expected {argname} to only contain Tensors, got {type(out)}" + ) + + +jvp_str = "jvp(f, primals, tangents)" + + +def safe_unpack_dual( + dual: torch.Tensor, strict: bool +) -> tuple[torch.Tensor, torch.Tensor]: + if not isinstance(dual, torch.Tensor): + raise RuntimeError( + f"{jvp_str}: expected f(*args) to return only tensors" + f", got unsupported type {type(dual)}" + ) + + primal, tangent = fwAD.unpack_dual(dual) + if tangent is None: + if strict: + raise RuntimeError( + "jvp(f, primals, tangents, strict=True): " + "The output of f is independent of " + "the inputs. This is not allowed with strict=True." + ) + tangent = torch.zeros_like(primal) + return primal, tangent + + +@exposed_in("torch.func") +def jvp( + func: Callable[..., Any], + primals: Any, + tangents: Any, + *, + strict: bool = False, + has_aux: bool = False, +) -> tuple[Any, Any] | tuple[Any, Any, Any]: + """ + Standing for the Jacobian-vector product, returns a tuple containing + the output of `func(*primals)` and the "Jacobian of ``func`` evaluated at + ``primals``" times ``tangents``. This is also known as forward-mode autodiff. + + Args: + func (function): A Python function that takes one or more arguments, + one of which must be a Tensor, and returns one or more Tensors + primals (Tensors): Positional arguments to ``func`` that must all be + Tensors. The returned function will also be computing the + derivative with respect to these arguments + tangents (Tensors): The "vector" for which Jacobian-vector-product is + computed. Must be the same structure and sizes as the inputs to + ``func``. + has_aux (bool): Flag indicating that ``func`` returns a + ``(output, aux)`` tuple where the first element is the output of + the function to be differentiated and the second element is + other auxiliary objects that will not be differentiated. + Default: False. + + Returns: + Returns a ``(output, jvp_out)`` tuple containing the output of ``func`` + evaluated at ``primals`` and the Jacobian-vector product. + If ``has_aux is True``, then instead returns a ``(output, jvp_out, aux)`` tuple. + + .. note:: + You may see this API error out with "forward-mode AD not implemented + for operator X". If so, please file a bug report and we will prioritize it. + + jvp is useful when you wish to compute gradients of a function R^1 -> R^N + + >>> from torch.func import jvp + >>> x = torch.randn([]) + >>> f = lambda x: x * torch.tensor([1.0, 2.0, 3]) + >>> value, grad = jvp(f, (x,), (torch.tensor(1.0),)) + >>> assert torch.allclose(value, f(x)) + >>> assert torch.allclose(grad, torch.tensor([1.0, 2, 3])) + + :func:`jvp` can support functions with multiple inputs by passing in the + tangents for each of the inputs + + >>> from torch.func import jvp + >>> x = torch.randn(5) + >>> y = torch.randn(5) + >>> f = lambda x, y: (x * y) + >>> _, output = jvp(f, (x, y), (torch.ones(5), torch.ones(5))) + >>> assert torch.allclose(output, x + y) + + """ + + return _jvp_with_argnums( + func, primals, tangents, argnums=None, strict=strict, has_aux=has_aux + ) + + +def _jvp_with_argnums( + func: Callable[..., Any], + primals: Any, + tangents: Any, + argnums: argnums_t | None, + *, + strict: bool = False, + has_aux: bool, +) -> tuple[Any, Any] | tuple[Any, Any, Any]: + # This is the same function as jvp but also accepts an argnums argument + # Most args are the same as jvp except for the added argument + # argnums (int or tuple[int, ...]): Optional, specifies the argument(s) to compute gradients with respect to. + # If None, computes the gradients with respect to all inputs (used for jvp). Default: None + # Because of this, tangents must be of length argnums and matches up to the corresponding primal whose index is + # given by argnums + # + # WARN: Users should NOT call this function directly and should just be calling jvp. + # It is only separated so that inputs passed to jacfwd but not differentiated get the correct wrappers. + # + # NOTE: All error messages are produced as if jvp was being called, even if this was called by jacfwd + # + # Returns the same two elements as :func:`jvp` but the returned tuple, ``jvp_out``, only has JVPs with respect to + # the primals given by argnums + if not isinstance(primals, tuple): + raise RuntimeError( + f"{jvp_str}: Expected primals to be a tuple. " + f"E.g. it should be valid to call f(*primals)." + ) + diff_args = primals if argnums is None else _slice_argnums(primals, argnums) + flat_primals, primals_spec = tree_flatten(diff_args) + flat_tangents, tangents_spec = tree_flatten(tangents) + if primals_spec != tangents_spec: + raise RuntimeError( + f"{jvp_str}: Expected primals and tangents to have the same python " + f"structure. For example, if primals is a tuple of 3 tensors, " + f"tangents also must be. Got primals with structure {primals_spec} " + f"and tangents with structure {tangents_spec}" + ) + assert_non_empty_list_of_tensors(flat_primals, jvp_str, "primals") + assert_non_empty_list_of_tensors(flat_tangents, jvp_str, "tangents") + + global JVP_NESTING + + with jvp_increment_nesting() as level: + with fwAD._set_fwd_grad_enabled(True): + ctx = fwAD.dual_level if JVP_NESTING == 1 else contextlib.nullcontext + with ctx(): + flat_duals = tuple( + fwAD.make_dual(p, t) for p, t in zip(flat_primals, flat_tangents) + ) + duals = tree_unflatten(flat_duals, primals_spec) + if argnums is not None: + primals = _wrap_all_tensors(primals, level) + duals = _replace_args(primals, duals, argnums) + result_duals = func(*duals) + aux: Any = None + if has_aux: + if not (isinstance(result_duals, tuple) and len(result_duals) == 2): + raise RuntimeError( + f"{jvp_str}: output of function f should be a tuple: (output, aux) " + "if has_aux is True" + ) + result_duals, aux = result_duals + aux = _undo_create_differentiable(aux, level) + + result_duals, spec = tree_flatten(result_duals) + assert_non_empty_tensor_output(result_duals, jvp_str) + + primals_out, tangents_out = zip( + *[safe_unpack_dual(dual, strict) for dual in result_duals] + ) + primals_out = tree_map( + partial(_undo_create_differentiable, level=level), primals_out + ) + tangents_out = tree_map( + partial(_undo_create_differentiable, level=level), tangents_out + ) + + primals_out_unflatten = tree_unflatten(primals_out, spec) + tangents_out_unflatten = tree_unflatten(tangents_out, spec) + if has_aux: + return primals_out_unflatten, tangents_out_unflatten, aux # type: ignore[possibly-unbound] + + return primals_out_unflatten, tangents_out_unflatten + + +def safe_unflatten(tensor: torch.Tensor, dim: int, shape: torch.Size) -> torch.Tensor: + if len(shape) == 0: + if tensor.shape[dim] != 1: + raise AssertionError( + f"expected tensor.shape[{dim}] to be 1, got {tensor.shape[dim]}" + ) + return tensor.squeeze(dim) + return tensor.unflatten(dim, shape) + + +@exposed_in("torch.func") +def jacfwd( + func: Callable[..., Any], + argnums: argnums_t = 0, + has_aux: bool = False, + *, + randomness: str = "error", +) -> Callable[..., Any]: + """ + Computes the Jacobian of ``func`` with respect to the arg(s) at index + ``argnum`` using forward-mode autodiff + + Args: + func (function): A Python function that takes one or more arguments, + one of which must be a Tensor, and returns one or more Tensors + argnums (int or tuple[int, ...]): Optional, integer or tuple of integers, + saying which arguments to get the Jacobian with respect to. + Default: 0. + has_aux (bool): Flag indicating that ``func`` returns a + ``(output, aux)`` tuple where the first element is the output of + the function to be differentiated and the second element is + auxiliary objects that will not be differentiated. + Default: False. + randomness(str): Flag indicating what type of randomness to use. + See :func:`vmap` for more detail. Allowed: "different", "same", "error". + Default: "error" + + Returns: + Returns a function that takes in the same inputs as ``func`` and + returns the Jacobian of ``func`` with respect to the arg(s) at + ``argnums``. If ``has_aux is True``, then the returned function + instead returns a ``(jacobian, aux)`` tuple where ``jacobian`` + is the Jacobian and ``aux`` is auxiliary objects returned by ``func``. + + .. note:: + You may see this API error out with "forward-mode AD not implemented + for operator X". If so, please file a bug report and we will prioritize it. + An alternative is to use :func:`jacrev`, which has better operator coverage. + + A basic usage with a pointwise, unary operation will give a diagonal array + as the Jacobian + + >>> from torch.func import jacfwd + >>> x = torch.randn(5) + >>> jacobian = jacfwd(torch.sin)(x) + >>> expected = torch.diag(torch.cos(x)) + >>> assert torch.allclose(jacobian, expected) + + :func:`jacfwd` can be composed with vmap to produce batched + Jacobians: + + >>> from torch.func import jacfwd, vmap + >>> x = torch.randn(64, 5) + >>> jacobian = vmap(jacfwd(torch.sin))(x) + >>> assert jacobian.shape == (64, 5, 5) + + If you would like to compute the output of the function as well as the + jacobian of the function, use the ``has_aux`` flag to return the output + as an auxiliary object: + + >>> from torch.func import jacfwd + >>> x = torch.randn(5) + >>> + >>> def f(x): + >>> return x.sin() + >>> + >>> def g(x): + >>> result = f(x) + >>> return result, result + >>> + >>> jacobian_f, f_x = jacfwd(g, has_aux=True)(x) + >>> assert torch.allclose(f_x, f(x)) + + Additionally, :func:`jacrev` can be composed with itself or :func:`jacrev` + to produce Hessians + + >>> from torch.func import jacfwd, jacrev + >>> def f(x): + >>> return x.sin().sum() + >>> + >>> x = torch.randn(5) + >>> hessian = jacfwd(jacrev(f))(x) + >>> assert torch.allclose(hessian, torch.diag(-x.sin())) + + By default, :func:`jacfwd` computes the Jacobian with respect to the first + input. However, it can compute the Jacboian with respect to a different + argument by using ``argnums``: + + >>> from torch.func import jacfwd + >>> def f(x, y): + >>> return x + y ** 2 + >>> + >>> x, y = torch.randn(5), torch.randn(5) + >>> jacobian = jacfwd(f, argnums=1)(x, y) + >>> expected = torch.diag(2 * y) + >>> assert torch.allclose(jacobian, expected) + + Additionally, passing a tuple to ``argnums`` will compute the Jacobian + with respect to multiple arguments + + >>> from torch.func import jacfwd + >>> def f(x, y): + >>> return x + y ** 2 + >>> + >>> x, y = torch.randn(5), torch.randn(5) + >>> jacobian = jacfwd(f, argnums=(0, 1))(x, y) + >>> expectedX = torch.diag(torch.ones_like(x)) + >>> expectedY = torch.diag(2 * y) + >>> assert torch.allclose(jacobian[0], expectedX) + >>> assert torch.allclose(jacobian[1], expectedY) + + """ + + @wraps(func) + def wrapper_fn(*args: Any) -> Any: + error_if_complex("jacfwd", args, is_input=True) + primals = args if argnums is None else _slice_argnums(args, argnums) + flat_primals, primals_spec = tree_flatten(primals) + flat_primals_numels = tuple(p.numel() for p in flat_primals) + flat_basis = _construct_standard_basis_for(flat_primals, flat_primals_numels) + if flat_basis is None: + raise AssertionError("flat_basis must not be None") + basis = tree_unflatten(flat_basis, primals_spec) + + def push_jvp(basis: Any) -> Any: + output = _jvp_with_argnums( + func, args, basis, argnums=argnums, has_aux=has_aux + ) + # output[0] is the output of `func(*args)` + error_if_complex("jacfwd", output[0], is_input=False) + if has_aux: + _, jvp_out, aux = output # pyrefly: ignore[bad-unpacking] + return jvp_out, aux + _, jvp_out = output # pyrefly: ignore[bad-unpacking] + return jvp_out + + results = vmap(push_jvp, randomness=randomness)(basis) + aux: Any = None + if has_aux: + results, aux = results + # aux is in the standard basis format, e.g. NxN matrix + # We need to fetch the first element as original `func` output + flat_aux, aux_spec = tree_flatten(aux) + flat_aux = [value[0] for value in flat_aux] + aux = tree_unflatten(flat_aux, aux_spec) + + jac_outs, spec = tree_flatten(results) + # Most probably below output check can never raise an error + # as jvp should test the output before + # assert_non_empty_output(jac_outs, 'jacfwd(f, ...)(*args)') + + jac_outs_ins = tuple( + tuple( + safe_unflatten(jac_out_in, -1, primal.shape) + for primal, jac_out_in in zip( + flat_primals, + jac_out.movedim(0, -1).split(flat_primals_numels, dim=-1), + ) + ) + for jac_out in jac_outs + ) + jac_outs_ins = tuple( + tree_unflatten(jac_ins, primals_spec) for jac_ins in jac_outs_ins + ) + + if isinstance(argnums, int): + jac_outs_ins = tuple(jac_ins[0] for jac_ins in jac_outs_ins) + if has_aux: + return tree_unflatten(jac_outs_ins, spec), aux # type: ignore[possibly-unbound] + return tree_unflatten(jac_outs_ins, spec) + + return wrapper_fn + + +@exposed_in("torch.func") +def hessian(func: Callable[..., Any], argnums: argnums_t = 0) -> Callable[..., Any]: + """ + Computes the Hessian of ``func`` with respect to the arg(s) at index + ``argnum`` via a forward-over-reverse strategy. + + The forward-over-reverse strategy (composing ``jacfwd(jacrev(func))``) is + a good default for good performance. It is possible to compute Hessians + through other compositions of :func:`jacfwd` and :func:`jacrev` like + ``jacfwd(jacfwd(func))`` or ``jacrev(jacrev(func))``. + + Args: + func (function): A Python function that takes one or more arguments, + one of which must be a Tensor, and returns one or more Tensors + argnums (int or tuple[int, ...]): Optional, integer or tuple of integers, + saying which arguments to get the Hessian with respect to. + Default: 0. + + Returns: + Returns a function that takes in the same inputs as ``func`` and + returns the Hessian of ``func`` with respect to the arg(s) at + ``argnums``. + + .. note:: + You may see this API error out with "forward-mode AD not implemented + for operator X". If so, please file a bug report and we will prioritize it. + An alternative is to use ``jacrev(jacrev(func))``, which has better + operator coverage. + + A basic usage with a R^N -> R^1 function gives a N x N Hessian: + + >>> from torch.func import hessian + >>> def f(x): + >>> return x.sin().sum() + >>> + >>> x = torch.randn(5) + >>> hess = hessian(f)(x) # equivalent to jacfwd(jacrev(f))(x) + >>> assert torch.allclose(hess, torch.diag(-x.sin())) + + """ + return jacfwd(jacrev(func, argnums), argnums) + + +@doesnt_support_saved_tensors_hooks +def grad_and_value_impl( + func: Callable[..., Any], + argnums: argnums_t, + has_aux: bool, + args: tuple[Any, ...], + kwargs: dict[str, Any], +) -> tuple[Any, Any]: + with grad_increment_nesting() as level: + output, aux, grad_input = None, None, None + # See NOTE [grad and vjp interaction with no_grad] + with torch.enable_grad(): + args = _wrap_all_tensors(args, level) + kwargs = _wrap_all_tensors(kwargs, level) + diff_args = _slice_argnums(args, argnums, as_tuple=False) + tree_map_(partial(_create_differentiable, level=level), diff_args) + + output = func(*args, **kwargs) + if has_aux: + if not (isinstance(output, tuple) and len(output) == 2): + raise RuntimeError( + "grad_and_value(f)(*args): output of function f should be a tuple: (output, aux) " + "if has_aux is True" + ) + output, aux = output + + if not isinstance(output, torch.Tensor): + raise RuntimeError( + "grad_and_value(f)(*args): Expected f(*args) " + f"to return a Tensor, got {type(output)}" + ) + if output.dim() != 0: + raise RuntimeError( + "grad_and_value(f)(*args): Expected f(*args) " + "to return a scalar Tensor, got tensor with " + f"{output.dim()} dims. Maybe you wanted to " + "use the vjp or jacrev APIs instead?" + ) + + flat_diff_args, spec = tree_flatten(diff_args) + + # NB: need create_graph so that backward pass isn't run in no_grad mode + flat_outputs = _as_tuple(output) + flat_grad_input = _autograd_grad( + flat_outputs, flat_diff_args, create_graph=True + ) + grad_input = tree_unflatten(flat_grad_input, spec) + + grad_input = _undo_create_differentiable(grad_input, level) + output = _undo_create_differentiable(output, level) + if has_aux: + # pyrefly: ignore[bad-specialization] + aux = _undo_create_differentiable(aux, level) + + if has_aux: + return grad_input, (output, aux) + return grad_input, output + + +def grad_impl( + func: Callable[..., Any], + argnums: argnums_t, + has_aux: bool, + args: tuple[Any, ...], + kwargs: dict[str, Any], +) -> Any: + results = grad_and_value_impl(func, argnums, has_aux, args, kwargs) + if has_aux: + grad, (_, aux) = results + return grad, aux + grad, _ = results + return grad + + +def _maybe_wrap_functional_tensor( + maybe_tensor: Any, level: int, *, _python_functionalize: bool = False +) -> Any: + if not isinstance(maybe_tensor, torch.Tensor): + return maybe_tensor + wrapped = _wrap_functional_tensor(maybe_tensor, level) + _assert_wrapped_functional(maybe_tensor, wrapped) + if _python_functionalize: + # pyrefly: ignore[missing-argument] + out = FunctionalTensor(wrapped) + # pyrefly: ignore[missing-attribute] + torch._mirror_autograd_meta_to(maybe_tensor, out) + return out + return wrapped + + +def _wrap_all_tensors_to_functional( + tensor_pytree: Any, level: int, *, _python_functionalize: bool = False +) -> Any: + return tree_map( + partial( + lambda x: _maybe_wrap_functional_tensor( + x, level, _python_functionalize=_python_functionalize + ) + ), + tensor_pytree, + ) + + +def _maybe_unwrap_functional_tensor(maybe_tensor: Any, *, reapply_views: bool) -> Any: + if not isinstance(maybe_tensor, torch.Tensor): + return maybe_tensor + if isinstance(maybe_tensor, FunctionalTensor): + maybe_tensor = maybe_tensor.elem + + if not torch._is_functional_tensor(maybe_tensor): + # If it's not a functional tensor, just return it. + # This can happen if we functionalize a fn that returns a global, + # which was never wrapped properly. + return maybe_tensor + # Sync any pending updates on the output tensor + torch._sync(maybe_tensor) + return _unwrap_functional_tensor(maybe_tensor, reapply_views) + + +def _unwrap_all_tensors_from_functional( + tensor_pytree: Any, *, reapply_views: bool +) -> Any: + return tree_map( + lambda t: _maybe_unwrap_functional_tensor(t, reapply_views=reapply_views), + tensor_pytree, + ) + + +@exposed_in("torch.func") +def functionalize( + func: Callable[..., Any], *, remove: str = "mutations" +) -> Callable[..., Any]: + """ + functionalize is a transform that can be used to remove (intermediate) + mutations and aliasing from a function, while preserving the function's + semantics. + + ``functionalize(func)`` returns a new function with the same semantics + as ``func``, but with all intermediate mutations removed. + Every inplace operation performed on an intermediate tensor: + ``intermediate.foo_()`` + gets replaced by its out-of-place equivalent: + ``intermediate_updated = intermediate.foo()``. + + functionalize is useful for shipping a pytorch program off to + backends or compilers that aren't able to easily represent + mutations or aliasing operators. + + Args: + func (Callable[..., Any]): A Python function that takes one or more arguments. + remove (str): An optional string argument, that takes on either + the value 'mutations' or 'mutations_and_views'. + If 'mutations' is passed in then all mutating operators + will be replaced with their non-mutating equivalents. + If 'mutations_and_views' is passed in, then additionally, all aliasing + operators will be replaced with their non-aliasing equivalents. + Default: 'mutations'. + + Returns: + Returns a new "functionalized" function. It takes the same inputs as + ``func``, and has the same behavior, but any mutations + (and optionally aliasing) performed on intermediate tensors + in the function will be removed. + + functionalize will also remove mutations (and views) that were performed on function inputs. + However to preserve semantics, functionalize will "fix up" the mutations after + the transform has finished running, by detecting if any tensor inputs "should have" + been mutated, and copying the new data back to the inputs if necessary. + + + Example:: + + >>> # xdoctest: +SKIP + >>> import torch + >>> from torch.fx.experimental.proxy_tensor import make_fx + >>> from torch.func import functionalize + >>> + >>> # A function that uses mutations and views, but only on intermediate tensors. + >>> def f(a): + ... b = a + 1 + ... c = b.view(-1) + ... c.add_(1) + ... return b + ... + >>> inpt = torch.randn(2) + >>> + >>> out1 = f(inpt) + >>> out2 = functionalize(f)(inpt) + >>> + >>> # semantics are the same (outputs are equivalent) + >>> print(torch.allclose(out1, out2)) + True + >>> + >>> f_traced = make_fx(f)(inpt) + >>> f_no_mutations_traced = make_fx(functionalize(f))(inpt) + >>> f_no_mutations_and_views_traced = make_fx(functionalize(f, remove='mutations_and_views'))(inpt) + >>> + >>> print(f_traced.code) + + + + def forward(self, a_1): + add = torch.ops.aten.add(a_1, 1); a_1 = None + view = torch.ops.aten.view(add, [-1]) + add_ = torch.ops.aten.add_(view, 1); view = None + return add + + >>> print(f_no_mutations_traced.code) + + + + def forward(self, a_1): + add = torch.ops.aten.add(a_1, 1); a_1 = None + view = torch.ops.aten.view(add, [-1]); add = None + add_1 = torch.ops.aten.add(view, 1); view = None + view_1 = torch.ops.aten.view(add_1, [2]); add_1 = None + return view_1 + + >>> print(f_no_mutations_and_views_traced.code) + + + + def forward(self, a_1): + add = torch.ops.aten.add(a_1, 1); a_1 = None + view_copy = torch.ops.aten.view_copy(add, [-1]); add = None + add_1 = torch.ops.aten.add(view_copy, 1); view_copy = None + view_copy_1 = torch.ops.aten.view_copy(add_1, [2]); add_1 = None + return view_copy_1 + + + >>> # A function that mutates its input tensor + >>> def f(a): + ... b = a.view(-1) + ... b.add_(1) + ... return a + ... + >>> f_no_mutations_and_views_traced = make_fx(functionalize(f, remove='mutations_and_views'))(inpt) + >>> # + >>> # All mutations and views have been removed, + >>> # but there is an extra copy_ in the graph to correctly apply the mutation to the input + >>> # after the function has completed. + >>> print(f_no_mutations_and_views_traced.code) + + + + def forward(self, a_1): + view_copy = torch.ops.aten.view_copy(a_1, [-1]) + add = torch.ops.aten.add(view_copy, 1); view_copy = None + view_copy_1 = torch.ops.aten.view_copy(add, [2]); add = None + copy_ = torch.ops.aten.copy_(a_1, view_copy_1); a_1 = None + return view_copy_1 + + + There are a few "failure modes" for functionalize that are worth calling out: + (1) Like other torch.func transforms, `functionalize()` doesn't work with functions + that directly use `.backward()`. The same is true for torch.autograd.grad. + If you want to use autograd, you can compute gradients directly + with `functionalize(grad(f))`. + (2) Like other torch.func transforms, `functionalize()` doesn't work with global state. + If you call `functionalize(f)` on a function that takes views / mutations of + non-local state, functionalization will simply no-op and pass the view/mutation + calls directly to the backend. + One way to work around this is to ensure that any non-local state creation + is wrapped into a larger function, which you then call functionalize on. + (3) `resize_()` has some limitations: functionalize will only work on programs + that use resize_()` as long as the tensor being resized is not a view. + (4) `as_strided()` has some limitations: functionalize will not work on + `as_strided()` calls that result in tensors with overlapping memory. + + + Finally, a helpful mental model for understanding functionalization is that + most user pytorch programs are writing with the public torch API. + When executed, torch operators are generally decomposed into + our internal C++ "ATen" API. + The logic for functionalization happens entirely at the level of ATen. + Functionalization knows how to take every aliasing operator in ATen, + and map it to its non-aliasing equivalent + (e.g. ``tensor.view({-1})`` -> ``at::view_copy(tensor, {-1})``), + and how to take every mutating operator in ATen, + and map it to its non-mutating equivalent + (e.g. ``tensor.add_(1)`` -> ``at::add(tensor, -1)``), + while tracking aliases and mutations out-of-line to know when to fix things up. + Information about which ATen operators are aliasing or mutating all comes from + https://github.com/pytorch/pytorch/blob/master/aten/src/ATen/native/native_functions.yaml. + """ + if remove == "mutations": + reapply_views = True + elif remove == "mutations_and_views": + reapply_views = False + else: + raise RuntimeError( + f"functionalize(f, remove='mutations'): received invalid argument for remove={remove}." + " Valid options are:\n" + " remove='mutations': all inplace and out= operators will be removed from the program, and replaced" + " with their out-of-place equivalents.\n" + " remove='mutations_and_views': In addition to the above, all aliasing operators {view} will be" + " replaced with their non-aliasing counterparts, {view}_copy.\n" + ) + + @wraps(func) + def wrapped(*args: Any, **kwargs: Any) -> Any: + try: + func_level = _func_increment_nesting(reapply_views) + func_args = _wrap_all_tensors_to_functional(args, func_level) + func_kwargs = _wrap_all_tensors_to_functional(kwargs, func_level) + + flattened_unwrapped_args = pytree.arg_tree_leaves(*args) + flattened_wrapped_args = pytree.arg_tree_leaves(*func_args) + flattened_unwrapped_kwargs = pytree.arg_tree_leaves(**kwargs) + flattened_wrapped_kwargs = pytree.arg_tree_leaves(**func_kwargs) + + func_outputs = func(*func_args, **func_kwargs) + outputs = _unwrap_all_tensors_from_functional( + func_outputs, reapply_views=reapply_views + ) + + for a in flattened_wrapped_args + flattened_wrapped_kwargs: + if isinstance(a, torch.Tensor): + # Call sync_() on the inputs, to ensure that any pending mutations have been applied. + torch._sync(a) + + # And if any mutations were applied to the inputs, we need to propagate them back to the user. + for unwrapped, wrapped in zip( + flattened_unwrapped_args, flattened_wrapped_args + ): + if isinstance(unwrapped, torch.Tensor) and isinstance( + wrapped, torch.Tensor + ): + _propagate_functional_input_mutation(unwrapped, wrapped) + for unwrapped, wrapped in zip( + flattened_unwrapped_kwargs, flattened_wrapped_kwargs + ): + if isinstance(unwrapped, torch.Tensor) and isinstance( + wrapped, torch.Tensor + ): + _propagate_functional_input_mutation(unwrapped, wrapped) + + return outputs + finally: + _func_decrement_nesting() + + return wrapped + + +@exposed_in("torch.func") +def linearize( + func: Callable[..., Any], *primals: Any +) -> tuple[Any, Callable[..., Any]]: + """ + Returns the value of ``func`` at ``primals`` and linear approximation + at ``primals``. + + Args: + func (Callable[..., Any]): A Python function that takes one or more arguments. + primals (Tensors): Positional arguments to ``func`` that must all be + Tensors. These are the values at which the function is linearly approximated. + + Returns: + Returns a ``(output, jvp_fn)`` tuple containing the output of ``func`` + applied to ``primals`` and a function that computes the jvp of + ``func`` evaluated at ``primals``. + + linearize is useful if jvp is to be computed multiple times at ``primals``. However, + to achieve this, linearize saves intermediate computation and has higher memory requirements + than directly applying `jvp`. So, if all the ``tangents`` are known, it maybe more efficient + to compute vmap(jvp) instead of using linearize. + + .. note:: + linearize evaluates ``func`` twice. Please file an issue for an implementation + with a single evaluation. + + Example:: + + >>> import torch + >>> from torch.func import linearize + >>> def fn(x): + ... return x.sin() + ... + >>> output, jvp_fn = linearize(fn, torch.zeros(3, 3)) + >>> jvp_fn(torch.ones(3, 3)) + tensor([[1., 1., 1.], + [1., 1., 1.], + [1., 1., 1.]]) + >>> + + """ + # Note: We evaluate `fn` twice. + # Once for returning the output and other while + # tracing the graph. + # If this becomes a bottle-neck, we should update + # make_fx such that it also returns the output. + + output = func(*primals) + _, output_spec = tree_flatten(output) + + flat_primals, primals_argspec = tree_flatten(primals) + + # tangents for tracing + flat_tangents = tuple(p.new_empty(()).expand_as(p) for p in flat_primals) + + # function to trace + def trace_fn(flat_tangents: tuple[torch.Tensor, ...]) -> Any: + with fwAD.dual_level(): + flat_duals = tuple( + fwAD.make_dual(p, t) for p, t in zip(flat_primals, flat_tangents) + ) + duals = tree_unflatten(flat_duals, primals_argspec) + output = func(*duals) + tangents = tree_map_only( + torch.Tensor, lambda dual: safe_unpack_dual(dual, False)[1], output + ) + + return tangents + + jvp_graph = lazy_dynamo_disallow(make_fx)(trace_fn)(flat_tangents) + const_folded_jvp_graph = lazy_dynamo_disallow(const_fold.split_const_subgraphs)( + jvp_graph + ) + + # Hold only the meta-data regarding the primals. + flat_primals_shape = tuple(p.shape for p in flat_primals) + flat_primals_device = tuple(p.device for p in flat_primals) + flat_primals_dtype = tuple(p.dtype for p in flat_primals) + + def forward_ad_checks(flat_tangents: Sequence[torch.Tensor]) -> None: + for idx, t in enumerate(flat_tangents): + if t.shape != flat_primals_shape[idx]: + msg = ( + f"tangent:{idx} with shape {t.shape} in flattened " + f"pytree doesn't match the shape {flat_primals_shape[idx]} " + "of the corresponding primal." + ) + raise RuntimeError(msg) + + if t.device != flat_primals_device[idx]: + msg = ( + f"tangent:{idx} with device {t.device} in flattened " + f"pytree doesn't match the device {flat_primals_device[idx]} " + "of the corresponding primal." + ) + raise RuntimeError(msg) + + if t.dtype != flat_primals_dtype[idx]: + msg = ( + f"tangent:{idx} with dtype {t.dtype} in flattened " + f"pytree doesn't match the dtype {flat_primals_dtype[idx]} " + "of the corresponding primal." + ) + raise RuntimeError(msg) + + # jvp_fn : Callable[..., Any] to return + # It takes care of checking the argspec of tangents, + # calling the folded fx graph and unflattening fx graph output + def jvp_fn(*tangents: Any) -> Any: + flat_tangents, tangent_argspec = tree_flatten(tangents) + if tangent_argspec != primals_argspec: + raise RuntimeError( + f"Expected the tangents {tangent_argspec} to have " + f"the same argspec as the primals {primals_argspec}" + ) + + forward_ad_checks(flat_tangents) # type: ignore[arg-type] + + flat_output = const_folded_jvp_graph(*flat_tangents) + # const folded graph can return flat output, + # so transform output. + return tree_unflatten(flat_output, output_spec) + + return output, jvp_fn + + +@exposed_in("torch.func") +def debug_unwrap(tensor: torch.Tensor, *, recurse: bool = True) -> torch.Tensor: + """Unwraps a functorch tensor (e.g. BatchedTensor, GradTrackingTensor) to its underlying tensor. + + This function should only be used in a debug setting (e.g. trying to print the + value of a Tensor in a debugger). Otherwise, using the result of function + inside of a function being transformed will lead to undefined behavior. + """ + if not is_functorch_wrapped_tensor(tensor): + return tensor + result = get_unwrapped(tensor) + if recurse: + return debug_unwrap(result) + return result diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/functional_call.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/functional_call.py new file mode 100644 index 0000000000000000000000000000000000000000..051681a044f0b3f35b2a9fd142f37977be8f26c6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/functional_call.py @@ -0,0 +1,264 @@ +from __future__ import annotations + +from collections.abc import Sequence +from typing import Any + +import torch +import torch.nn as nn +from torch import Tensor +from torch._functorch.utils import exposed_in + + +@exposed_in("torch.func") +def functional_call( + module: torch.nn.Module, + parameter_and_buffer_dicts: dict[str, Tensor] | Sequence[dict[str, Tensor]], + args: Any = None, + kwargs: dict[str, Any] | None = None, + *, + tie_weights: bool = True, + strict: bool = False, +) -> Any: + r"""Performs a functional call on the module by replacing the module parameters + and buffers with the provided ones. + + .. note:: If the module has active parametrizations, passing a value in the + :attr:`parameter_and_buffer_dicts` argument with the name set to the regular parameter + name will completely disable the parametrization. + If you want to apply the parametrization function to the value passed + please set the key as ``{submodule_name}.parametrizations.{parameter_name}.original``. + + .. note:: If the module performs in-place operations on parameters/buffers, these will be reflected + in the ``parameter_and_buffer_dicts`` input. + + + Example:: + + >>> a = {'foo': torch.zeros(())} + >>> # xdoctest: +SKIP + >>> mod = Foo() # does self.foo = self.foo + 1 + >>> print(mod.foo) # tensor(0.) + >>> functional_call(mod, a, torch.ones(())) + >>> print(mod.foo) # tensor(0.) + >>> print(a['foo']) # tensor(1.) + + .. note:: If the module has tied weights, whether or not functional_call respects the tying is determined by the + tie_weights flag. + + Example:: + + >>> a = {'foo': torch.zeros(())} + >>> # xdoctest: +SKIP + >>> mod = Foo() # has both self.foo and self.foo_tied which are tied. Returns x + self.foo + self.foo_tied + >>> print(mod.foo) # tensor(1.) + >>> mod(torch.zeros(())) # tensor(2.) + >>> functional_call(mod, a, torch.zeros(())) # tensor(0.) since it will change self.foo_tied too + >>> functional_call(mod, a, torch.zeros(()), tie_weights=False) # tensor(1.)--self.foo_tied is not updated + >>> new_a = {'foo': torch.zeros(()), 'foo_tied': torch.zeros(())} + >>> functional_call(mod, new_a, torch.zeros()) # tensor(0.) + + An example of passing multiple dictionaries + + .. code-block:: python + + a = ( + {"weight": torch.ones(1, 1)}, + {"buffer": torch.zeros(1)}, + ) # two separate dictionaries + mod = nn.Bar(1, 1) # return self.weight @ x + self.buffer + print(mod.weight) # tensor(...) + print(mod.buffer) # tensor(...) + x = torch.randn((1, 1)) + print(x) + functional_call(mod, a, x) # same as x + print(mod.weight) # same as before functional_call + + + And here is an example of applying the grad transform over the parameters + of a model. + + .. code-block:: python + + import torch + import torch.nn as nn + from torch.func import functional_call, grad + + x = torch.randn(4, 3) + t = torch.randn(4, 3) + model = nn.Linear(3, 3) + + + def compute_loss(params, x, t): + y = functional_call(model, params, x) + return nn.functional.mse_loss(y, t) + + + grad_weights = grad(compute_loss)(dict(model.named_parameters()), x, t) + + .. note:: If the user does not need grad tracking outside of grad transforms, they can detach all of the + parameters for better performance and memory usage + + Example:: + + >>> detached_params = {k: v.detach() for k, v in model.named_parameters()} + >>> grad_weights = grad(compute_loss)(detached_params, x, t) + >>> grad_weights.grad_fn # None--it's not tracking gradients outside of grad + + This means that the user cannot call ``grad_weight.backward()``. However, if they don't need autograd tracking + outside of the transforms, this will result in less memory usage and faster speeds. + + Args: + module (torch.nn.Module): the module to call + parameters_and_buffer_dicts (Dict[str, Tensor] or tuple of Dict[str, Tensor]): the parameters that will be used in + the module call. If given a tuple of dictionaries, they must have distinct keys so that all dictionaries can + be used together + args (Any or tuple): arguments to be passed to the module call. If not a tuple, considered a single argument. + kwargs (dict): keyword arguments to be passed to the module call + tie_weights (bool, optional): If True, then parameters and buffers tied in the original model will be treated as + tied in the reparameterized version. Therefore, if True and different values are passed for the tied + parameters and buffers, it will error. If False, it will not respect the originally tied parameters and + buffers unless the values passed for both weights are the same. Default: True. + strict (bool, optional): If True, then the parameters and buffers passed in must match the parameters and + buffers in the original module. Therefore, if True and there are any missing or unexpected keys, it will + error. Default: False. + + Returns: + Any: the result of calling ``module``. + """ + if isinstance(parameter_and_buffer_dicts, dict): + parameters_and_buffers = parameter_and_buffer_dicts + elif isinstance(parameter_and_buffer_dicts, Sequence): + if not all(isinstance(d, dict) for d in parameter_and_buffer_dicts): + raise ValueError( + "Expected all elements of parameter_and_buffer_dicts to be dictionaries" + ) + all_keys = [k for d in parameter_and_buffer_dicts for k in d] + all_keys_counter: dict[str, int] = {} + for k in all_keys: + v = all_keys_counter.get(k, 0) + all_keys_counter[k] = v + 1 + repeated_keys = [key for key, n in all_keys_counter.items() if n > 1] + if len(repeated_keys) > 0: + raise ValueError( + f"{repeated_keys} appeared in multiple dictionaries; behavior of functional call is ambiguous" + ) + parameters_and_buffers = { + k: v for d in parameter_and_buffer_dicts for k, v in d.items() + } + else: + raise ValueError( + f"Expected parameter_and_buffer_dicts to be a dict, or a list/tuple of dicts, " + f"but got {type(parameter_and_buffer_dicts)}" + ) + + return nn.utils.stateless._functional_call( + module, + parameters_and_buffers, + args, + kwargs, + tie_weights=tie_weights, + strict=strict, + ) + + +@exposed_in("torch.func") +def stack_module_state( + models: Sequence[nn.Module] | nn.ModuleList, +) -> tuple[dict[str, Any], dict[str, Any]]: + """stack_module_state(models) -> params, buffers + + Prepares a list of torch.nn.Modules for ensembling with :func:`vmap`. + + Given a list of ``M`` ``nn.Modules`` of the same class, returns two dictionaries + that stack all of their parameters and buffers together, indexed by name. + The stacked parameters are optimizable (i.e. they are new leaf nodes in the + autograd history that are unrelated to the original parameters and can be + passed directly to an optimizer). + + Here's an example of how to ensemble over a very simple model: + + .. code-block:: python + + num_models = 5 + batch_size = 64 + in_features, out_features = 3, 3 + models = [torch.nn.Linear(in_features, out_features) for i in range(num_models)] + data = torch.randn(batch_size, 3) + + + def wrapper(params, buffers, data): + return torch.func.functional_call(models[0], (params, buffers), data) + + + params, buffers = stack_module_state(models) + output = vmap(wrapper, (0, 0, None))(params, buffers, data) + + assert output.shape == (num_models, batch_size, out_features) + + When there's submodules, this follows state dict naming conventions + + .. code-block:: python + + import torch.nn as nn + + + class Foo(nn.Module): + def __init__(self, in_features, out_features): + super().__init__() + hidden = 4 + self.l1 = nn.Linear(in_features, hidden) + self.l2 = nn.Linear(hidden, out_features) + + def forward(self, x): + return self.l2(self.l1(x)) + + + num_models = 5 + in_features, out_features = 3, 3 + models = [Foo(in_features, out_features) for i in range(num_models)] + params, buffers = stack_module_state(models) + print(list(params.keys())) # "l1.weight", "l1.bias", "l2.weight", "l2.bias" + + .. warning:: + All of the modules being stacked together must be the same (except for + the values of their parameters/buffers). For example, they should be in the + same mode (training vs eval). + """ + if len(models) == 0: + raise RuntimeError("stack_module_state: Expected at least one model, got 0.") + if not (all(m.training for m in models) or all(not m.training for m in models)): + raise RuntimeError( + "stack_module_state: Expected all models to have the same training/eval mode." + ) + model0_typ = type(models[0]) + if not all(type(m) is model0_typ for m in models): + raise RuntimeError( + "stack_module_state: Expected all models to be of the same class." + ) + all_params = [dict(model.named_parameters()) for model in models] + params = { + k: construct_stacked_leaf(tuple(params[k] for params in all_params), k) + for k in all_params[0] + } + all_buffers = [dict(model.named_buffers()) for model in models] + buffers = { + k: construct_stacked_leaf(tuple(buffers[k] for buffers in all_buffers), k) + for k in all_buffers[0] + } + + return params, buffers + + +def construct_stacked_leaf( + tensors: tuple[Tensor, ...] | list[Tensor], name: str +) -> Tensor: + all_requires_grad = all(t.requires_grad for t in tensors) + none_requires_grad = all(not t.requires_grad for t in tensors) + if not all_requires_grad and not none_requires_grad: + raise RuntimeError( + f"Expected {name} from each model to have the same .requires_grad" + ) + result = torch.stack(tensors) + if all_requires_grad: + result = result.detach().requires_grad_() + return result diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/fx_minifier.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/fx_minifier.py new file mode 100644 index 0000000000000000000000000000000000000000..d40cb473f54fe930925acfde5b01fcc58d4914f0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/fx_minifier.py @@ -0,0 +1,562 @@ +from __future__ import annotations + +import copy +import math +import os +import sys +from dataclasses import dataclass +from functools import partial, wraps +from typing import Any, TYPE_CHECKING + +import torch +import torch.fx as fx +from torch.hub import tqdm +from torch.multiprocessing.reductions import StorageWeakRef +from torch.utils._content_store import ContentStoreWriter + +from .compile_utils import get_outputs, get_placeholders + + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + + +is_tuple = object() + + +@dataclass +class LoadTensorMeta: + size: tuple[int, ...] + stride: tuple[int, ...] + dtype: torch.dtype + device: torch.device + + +class ConcreteProp(torch.fx.Interpreter): + def __init__( + self, + mod: fx.GraphModule, + *, + writer: ContentStoreWriter | None = None, + skip_offload: bool = False, + ) -> None: + super().__init__(mod) + self.writer = writer + self.skip_offload = skip_offload + self.seen_storages: set[StorageWeakRef] = set() + self.pbar: Any = None + + def run_node(self, n: fx.Node) -> Any: + self.pbar.update(1) + r = super().run_node(n) + name = n.name + + if isinstance(r, torch.Tensor): + if self.writer is None: + n.meta["concrete_value"] = r + else: + if StorageWeakRef(r.untyped_storage()) in self.seen_storages: + # Refuse to offload tensors which alias other live + # tensors, because this will violate operator contracts + n.meta["concrete_value"] = None + else: + if not self.skip_offload: + self.writer.write_tensor(os.path.join("eager", name), r) + n.meta["concrete_value"] = LoadTensorMeta( + r.size(), r.stride(), r.dtype, r.device + ) + self.seen_storages.add(StorageWeakRef(r.untyped_storage())) + else: + n.meta["concrete_value"] = is_tuple + + return r + + def propagate(self, *args: Any) -> Any: + mod = self.module + if not isinstance(mod, fx.GraphModule): + raise AssertionError(f"expected fx.GraphModule, got {type(mod)}") + with tqdm( + desc="Saving intermediates for delta debugging", + total=len(mod.graph.nodes), + disable=self.writer is None, + ) as pbar: + self.pbar = pbar + r = super().run(*args) + if not self.skip_offload: + pbar.set_description( + "Saved! To skip next time, run with --skip-saving-eager-intermediates" + ) + return r + + +def is_load_tensor_node(node: fx.Node) -> bool: + return ( + node.op == "call_function" + and node.target is torch.ops.debugprims.load_tensor.default + ) + + +# inplace modifies node/inps +def _convert_node_to_placeholder( + graph: fx.Graph, node: fx.Node, inps: list[torch.Tensor] +) -> bool: + if node.op == "output" or node.op == "placeholder": + return False + + if is_load_tensor_node(node): + return False + + concrete_val = node.meta.get("concrete_value", None) + + if isinstance(concrete_val, torch.Tensor): + node.op = "placeholder" + node.target = node.name + node.args = () + node.kwargs = {} + + inps.append(concrete_val) + return True + + elif concrete_val is None: + return False + + elif concrete_val is is_tuple: + r = False + for tuple_user in list(node.users): + r = _convert_node_to_placeholder(graph, tuple_user, inps) or r + # NB: We must not erase the node at this point, because + # we are iterating over the nodes and this would change + # the iteration order + # graph.erase_node(node) + return r + + elif isinstance(concrete_val, LoadTensorMeta): + node.op = "call_function" + node.target = torch.ops.debugprims.load_tensor.default + node.args = ( + os.path.join("eager", node.name), + concrete_val.size, + concrete_val.stride, + ) + node.kwargs = { + "device": concrete_val.device, + "dtype": concrete_val.dtype, + } + return True + + return False + + +def create_minified_hlo_graph( + minified_fx_graph: fx.GraphModule, inputs: Sequence[torch.Tensor] +) -> None: + """ + Takes minified FX graph as primary input, and ports it to HLO via StableHLO + Provides minified HLO graph as output, and archive them to local directory + """ + hlo_dir = f"{os.getcwd()}/hlo_files" + os.makedirs(hlo_dir, exist_ok=True) + + from torch_xla.stablehlo import save_torch_model_as_stablehlo + + save_torch_model_as_stablehlo(minified_fx_graph, inputs, hlo_dir) + + +def dump_state(fx_g: fx.GraphModule, inps: Sequence[torch.Tensor]) -> None: + print( + f""" +# Working Repro with {len(fx_g.graph.nodes)} nodes +inps = {[(i.shape, i.dtype, i.device.type) for i in inps]} +inps = [torch.zeros(())] + [torch.ones(shape, dtype=dtype, device=device) for (shape, dtype, device) in inps] +{fx_g.code} +""" + ) + + +def is_power_of_two(n: int) -> bool: + if n == 0: + return False + return (n & (n - 1)) == 0 + + +@dataclass +class ReproState: + graph: fx.Graph + inps: Sequence[torch.Tensor] + + def __post_init__(self) -> None: + ph_nodes = get_placeholders(self.graph) + if len(ph_nodes) != len(self.inps): + raise AssertionError( + f"len(ph_nodes)={len(ph_nodes)} != len(self.inps)={len(self.inps)}" + ) + + +def minifier( + fail_f: fx.GraphModule, + inps: Sequence[torch.Tensor], + module_fails: Callable[[fx.GraphModule, Sequence[torch.Tensor]], bool], + dump_state: Callable[[fx.GraphModule, Sequence[torch.Tensor]], None] = dump_state, + *, + save_dir: str | None = None, + offload_to_disk: bool = False, + skip_offload: bool = False, + skip_sanity: bool = False, + max_granularity: int | None = None, +) -> tuple[fx.GraphModule, Sequence[torch.Tensor]]: + """ + Minimizes a FX graph with given inputs, such that the resulting FX graph still returns True for module_fails. + + Does 2 main strategies: + 1. Truncates suffix: Removes some suffix from the graph and sets a new output. + 2. Delta Debugging: Tries replacing half of the graph with inputs. If fails, + tries replacing quarter of the graph, etc. + + >>> # xdoctest: +SKIP(failing) + >>> failing_function = fx.symbolic_trace(f) + >>> minimize(failing_function, [torch.randn(5)], lambda fx_g, inps: fx_g(*inps)) + + note: module_fails returns True if it fails. + """ + + failing_graph = fail_f.graph + cur_size = len(failing_graph.nodes) + + if max_granularity is not None and not is_power_of_two(max_granularity): + raise RuntimeError(f"max_granularity {max_granularity} not power of two") + + num_queries = 0 + + def deepcopy_fx_graph(fx_graph: fx.Graph) -> fx.Graph: + return fx.GraphModule(fail_f, copy.deepcopy(fx_graph)).graph + + def graph_fails(graph: fx.Graph, inps: Sequence[torch.Tensor]) -> bool: + nonlocal num_queries + graph = copy.deepcopy(graph) + num_queries += 1 + mod = fx.GraphModule(fail_f, graph) + mod.graph.lint() + return module_fails(mod, inps) + + writer = None + if offload_to_disk: + if save_dir is None: + raise AssertionError("save_dir must not be None when offload_to_disk=True") + writer = ContentStoreWriter(save_dir) + + ConcreteProp(fail_f, writer=writer, skip_offload=skip_offload).propagate(*inps) + if not skip_sanity and not graph_fails(failing_graph, inps): + raise RuntimeError("Input graph did not fail the tester") + print(f"Started off with {cur_size} nodes", file=sys.stderr) + + def _register_strategy( + strategy: Callable[[fx.Graph, Sequence[torch.Tensor], int], ReproState | None], + name: str, + ) -> Callable[[ReproState, int], ReproState | None]: + @wraps(strategy) + def new_func(old_state: ReproState, granularity: int = 1) -> ReproState | None: + print(file=sys.stderr) + print( + f"Strategy: {name} (G: {granularity}) " + f"({len(old_state.graph.nodes)} nodes, {len(old_state.inps)} inputs)", + file=sys.stderr, + ) + new_state = strategy( + deepcopy_fx_graph(old_state.graph), list(old_state.inps), granularity + ) + if new_state is not None: + new_nodes = len(new_state.graph.nodes) + old_nodes = len(old_state.graph.nodes) + new_inps = len(new_state.inps) + old_inps = len(old_state.inps) + new_outs = len(get_outputs(new_state.graph)) + old_outs = len(get_outputs(old_state.graph)) + progress_made = False + if new_nodes < old_nodes: + progress_made = True + print( + f"SUCCESS: Went from {old_nodes} to {new_nodes} nodes", + file=sys.stderr, + ) + if new_inps > old_inps: + progress_made = True + print( + f"SUCCESS: Went from {old_inps} to {new_inps} inputs", + file=sys.stderr, + ) + if new_outs < old_outs: + progress_made = True + print( + f"SUCCESS: Went from {old_outs} to {new_outs} outputs", + file=sys.stderr, + ) + + if not progress_made: + raise RuntimeError("Success raised but no progress made?") + + if not graph_fails(new_state.graph, new_state.inps): + print( + "WARNING: Something went wrong, not applying this minification", + file=sys.stderr, + ) + return None + return new_state + else: + print(f"FAIL: {name}", file=sys.stderr) + return None + + return new_func + + def register_strategy( + name: str, + ) -> Callable[ + [Callable[[fx.Graph, Sequence[torch.Tensor], int], ReproState | None]], + Callable[[ReproState, int], ReproState | None], + ]: + return partial(_register_strategy, name=name) + + @register_strategy("Truncate suffix") + def remove_suffix( + cur_graph: fx.Graph, cur_inps: Sequence[torch.Tensor], granularity: int + ) -> ReproState | None: + tested: set[int] = set() + new_graph = fx.Graph() + env: dict[fx.Node, fx.Node] = {} + for idx, node in enumerate(cur_graph.nodes): + new_node = new_graph.node_copy(node, lambda x: env[x]) + if node.op not in ["placeholder", "output"]: + # If idx is divisible by (granularity * 2), it would have been checked already. + if ( + idx % granularity == 0 + and (idx % (granularity * 2) != 0) + and idx not in tested + ): + output_node = new_graph.output((new_node,)) + if len(new_graph.nodes) < len(cur_graph.nodes) and graph_fails( + new_graph, cur_inps + ): + return ReproState(new_graph, cur_inps) + else: + tested.add(idx) + new_graph.erase_node(output_node) + env[node] = new_node + return None + + @register_strategy("Remove outputs") + def remove_outputs( + cur_graph: fx.Graph, cur_inps: Sequence[torch.Tensor], granularity: int + ) -> ReproState | None: + granularity = max(1, granularity // 2) + output: fx.Node | None = None + for idx, node in enumerate(cur_graph.nodes): + node.idx = idx # type: ignore[attr-defined] + if node.op == "output": + output = node + break + + if output is None: + return None + + if isinstance(output.args[0], fx.Node): + return None + + # output.args[0] is a tuple/list of nodes when returning multiple outputs + output_args_raw = output.args[0] + if not isinstance(output_args_raw, (list, tuple)): + raise AssertionError( + f"expected output_args_raw to be list or tuple, got {type(output_args_raw)}" + ) + output_args = sorted( + output_args_raw, + key=lambda x: x.idx if isinstance(x, fx.Node) else int(1e9), # type: ignore[attr-defined] + ) + if len(output_args) == 1: + return None + + for idx in range(0, len(output_args), granularity): + output.args = (output_args[:idx] + output_args[idx + granularity :],) + if graph_fails(cur_graph, cur_inps): + return ReproState(cur_graph, cur_inps) + return None + + def remove_unused_inputs_unchecked(cur_state: ReproState) -> ReproState | None: + cur_graph = cur_state.graph + cur_inps = cur_state.inps + ph_nodes = list(get_placeholders(cur_graph)) + if len(ph_nodes) != len(cur_inps): + raise AssertionError( + f"len(ph_nodes)={len(ph_nodes)} != len(cur_inps)={len(cur_inps)}" + ) + + new_inps: list[torch.Tensor] = [] + for idx in range(len(ph_nodes)): + if len(ph_nodes[idx].users) == 0: + cur_graph.erase_node(ph_nodes[idx]) + else: + new_inps.append(cur_inps[idx]) + if len(new_inps) < len(cur_inps): + return ReproState(cur_graph, new_inps) + return None + + def remove_unused_inputs_checked(cur_state: ReproState) -> ReproState | None: + new_state = remove_unused_inputs_unchecked(cur_state) + if new_state is not None and graph_fails(new_state.graph, new_state.inps): + return new_state + return None + + def _remove_unused_wrapper( + cur_graph: fx.Graph, cur_inps: Sequence[torch.Tensor], granularity: int + ) -> ReproState | None: + return remove_unused_inputs_checked(ReproState(cur_graph, cur_inps)) + + remove_unused_inputs = register_strategy("Remove unused inputs")( + _remove_unused_wrapper + ) + + @register_strategy("Eliminate dead code") + def eliminate_dead_code( + cur_graph: fx.Graph, cur_inps: Sequence[torch.Tensor], granularity: int + ) -> ReproState | None: + if cur_graph.eliminate_dead_code() and graph_fails(cur_graph, cur_inps): + return ReproState(cur_graph, cur_inps) + return None + + def _consolidate_placeholders( + cur_graph: fx.Graph, inps: list[torch.Tensor] + ) -> fx.Graph: + new_graph = fx.Graph() + env = {} + seen_non_placeholder = False + + # Move all placeholders to the front; also, if any load_tensor + # is at the front, convert it into an input (because it can be live + # all the time) + for node in cur_graph.nodes: + if node.op == "placeholder": + new_node = new_graph.node_copy(node, lambda x: env[x]) + env[node] = new_node + elif not seen_non_placeholder and is_load_tensor_node(node): + new_node = new_graph.placeholder(node.name) + env[node] = new_node + inps.append( + torch.ops.debugprims.load_tensor.default(*node.args, **node.kwargs) + ) + else: + seen_non_placeholder = True + + # Move everyone else + for node in cur_graph.nodes: + if node not in env: + new_node = new_graph.node_copy(node, lambda x: env[x]) + env[node] = new_node + return new_graph + + @register_strategy("Delta Debugging") + def delta_debugging( + cur_graph: fx.Graph, cur_inps: Sequence[torch.Tensor], granularity: int + ) -> ReproState | None: + num_nodes = len(cur_graph.nodes) + for start_range in range(0, num_nodes, granularity): + is_removing = False + new_graph = deepcopy_fx_graph(cur_graph) + new_inps = list(cur_inps[:]) + end_range = min(num_nodes, start_range + granularity) + for idx in range(start_range, end_range): + new_node = list(new_graph.nodes)[idx] + if _convert_node_to_placeholder(new_graph, new_node, new_inps): + is_removing = True + if not is_removing: + continue + new_graph.eliminate_dead_code() + new_graph = _consolidate_placeholders(new_graph, new_inps) + new_state = remove_unused_inputs_unchecked(ReproState(new_graph, new_inps)) + if new_state is None: + new_state = ReproState(new_graph, new_inps) + if graph_fails(new_state.graph, new_state.inps): + return ReproState(new_state.graph, new_state.inps) + + return None + + @register_strategy("Consolidate Inputs") + def consolidate_inputs( + cur_graph: fx.Graph, cur_inps: Sequence[torch.Tensor], granularity: int + ) -> ReproState | None: + old_len = len(cur_inps) + new_inps = list(cur_inps[:]) + cur_graph = _consolidate_placeholders(cur_graph, new_inps) + if len(cur_inps) > old_len and graph_fails(cur_graph, new_inps): + return ReproState(cur_graph, new_inps) + return None + + failing_state = ReproState(failing_graph, inps) + + def try_granularity( + failing_state: ReproState, granularity: int, use_non_granular: bool + ) -> ReproState | None: + print(f"Trying granularity {granularity}", file=sys.stderr) + + strategies = [] + num_nodes = len(failing_state.graph.nodes) + num_outputs = len(get_outputs(failing_state.graph)) + if num_outputs > num_nodes // 2: + strategies += [remove_outputs] + + if use_non_granular: + strategies += [ + eliminate_dead_code, + remove_unused_inputs, + consolidate_inputs, + ] + + strategies += [remove_suffix, delta_debugging] + + for strategy in strategies: + new_state = strategy(failing_state, granularity) + if new_state is not None: + return new_state + return None + + while True: + dump_state(fx.GraphModule(fail_f, failing_state.graph), failing_state.inps) + granularity = int(2 ** (math.floor(math.log2(len(failing_state.graph.nodes))))) + if max_granularity is not None: + granularity = min(max_granularity, granularity) + new_state = try_granularity(failing_state, granularity, use_non_granular=True) + if new_state is not None: + failing_state = new_state + continue + + granularity //= 2 + has_progress = False + while granularity >= 1: + new_state = try_granularity( + failing_state, granularity, use_non_granular=False + ) + if new_state is not None: + failing_state = new_state + has_progress = True + break + granularity //= 2 + if has_progress: + continue + + new_state = remove_outputs(failing_state, 1) + if new_state is not None: + failing_state = new_state + continue + + break + + if not graph_fails(failing_state.graph, failing_state.inps): + raise RuntimeError("Uh oh, something went wrong :( Final graph is not failing") + + print(f"Made {num_queries} queries", file=sys.stderr) + failing_fx = fx.GraphModule(fail_f, failing_state.graph) + + # If XLA debugging environment is enabled, create minified HLO graph as well + if "XLA_HLO_DEBUG" in os.environ: + create_minified_hlo_graph(failing_fx, failing_state.inps) + + dump_state(failing_fx, failing_state.inps) + print("Wrote minimal repro out to repro.py", file=sys.stderr) + return failing_fx, failing_state.inps diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/make_functional.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/make_functional.py new file mode 100644 index 0000000000000000000000000000000000000000..65b77627b1dc8f922d1c511a345859ae9eb60346 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/make_functional.py @@ -0,0 +1,669 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from __future__ import annotations + +import copy +from typing import Any, NoReturn, TYPE_CHECKING + +import torch +import torch.nn as nn +from torch import Tensor +from torch.nn.utils._named_member_accessor import NamedMemberAccessor + + +if TYPE_CHECKING: + from collections.abc import Callable, Iterable, Sequence + +# Utilities to make nn.Module "functional" +# In particular the goal is to be able to provide a function that takes as input +# the parameters and evaluate the nn.Module using fixed inputs. + + +def raise_parameter_tying_error() -> NoReturn: + raise RuntimeError( + "make_functional(module): we don't yet support models that " + "do parameter tying (also sometimes known as weight sharing). " + "Please try to rewrite your model by replacing all instances of the " + "tied parameter with another and/or comment your support in " + "https://github.com/pytorch/functorch/issues/446" + ) + + +def create_names_map( + named_params: dict[str, Tensor] | Iterable[tuple[str, Tensor]], + tied_named_params: dict[str, Tensor] | Iterable[tuple[str, Tensor]], +) -> dict[str, list[str]]: + """ + named_params is a dictionary of tensors: {'A': A, 'B': B} + tied_named_params is another dictionary of tensors {'A': A, 'B': B, 'B_tied': B} + with potentially tied (or 'duplicated') tensors + + This function creates a mapping from the names in named_params to the + names in tied_named_params: {'A': ['A'], 'B': ['B', 'B_tied']}. + """ + named_params_dict = dict(named_params) + tied_named_params_dict = dict(tied_named_params) + + tensors_dict_keys = set(named_params_dict.keys()) + tied_tensors_dict_keys = set(tied_named_params_dict.keys()) + if not tensors_dict_keys.issubset(tied_tensors_dict_keys): + raise AssertionError( + f"tensors_dict_keys {tensors_dict_keys} is not a subset of " + f"tied_tensors_dict_keys {tied_tensors_dict_keys}" + ) + + tensor_to_mapping: dict[Tensor, tuple[str, list[str]]] = {} + for key, tensor in named_params_dict.items(): + tensor_to_mapping[tensor] = (key, []) + for key, tensor in tied_named_params_dict.items(): + if tensor not in tensor_to_mapping: + raise AssertionError( + f"tensor for key '{key}' not found in tensor_to_mapping" + ) + tensor_to_mapping[tensor][1].append(key) + return dict(tensor_to_mapping.values()) + + +def _extract_members( + mod: nn.Module, + named_members: Callable[..., Iterable[tuple[str, Tensor]]], + subclass: Callable[[Tensor], Tensor], +) -> tuple[tuple[Tensor, ...], tuple[str, ...], dict[str, list[str]]]: + all_named_members = tuple(named_members(remove_duplicate=False)) + unique_named_members = tuple(named_members(remove_duplicate=True)) + names_map = create_names_map(unique_named_members, all_named_members) + + # Remove all the members in the model + # pyrefly: ignore [implicit-any] + memo = {} + accessor = NamedMemberAccessor(mod) + for name, p in all_named_members: + if p not in memo: + memo[p] = subclass(torch.empty_like(p, device="meta")) + replacement = memo[p] + accessor.set_tensor(name, replacement) + + if len(unique_named_members) == 0: + names, params = (), () + else: + names, params = zip(*unique_named_members) # type: ignore[assignment] + return params, names, names_map + + +def extract_weights( + mod: nn.Module, +) -> tuple[tuple[Tensor, ...], tuple[str, ...], dict[str, list[str]]]: + """ + This function removes all the Parameters from the model and + return them as a tuple as well as their original attribute names. + The weights must be re-loaded with `load_weights` before the model + can be used again. + Note that this function modifies the model in place and after this + call, mod.parameters() will be empty. + """ + return _extract_members(mod, mod.named_parameters, nn.Parameter) + + +def extract_buffers( + mod: nn.Module, +) -> tuple[tuple[Tensor, ...], tuple[str, ...], dict[str, list[str]]]: + return _extract_members(mod, mod.named_buffers, lambda x: x) + + +def load_weights( + mod: nn.Module, + names: Sequence[str], + params: Sequence[Tensor], + as_params: bool = False, +) -> None: + """ + Reload a set of weights so that `mod` can be used again to perform a forward pass. + Note that the `params` are regular Tensors (that can have history) and so are left + as Tensors. This means that mod.parameters() will still be empty after this call. + """ + accessor = NamedMemberAccessor(mod) + if as_params: + params = [nn.Parameter(p) for p in params] + accessor.set_tensors(names, params) + + +def _swap_state( + mod: nn.Module, names_map: dict[str, list[str]], elems: Iterable[Tensor] +) -> list[Tensor]: + result: list[Tensor] = [] + accessor = NamedMemberAccessor(mod) + for (_, attr_names), elem in zip(names_map.items(), elems): + for i, attr_name in enumerate(attr_names): + if i == 0: + result.append(accessor.swap_tensor(attr_name, elem)) + else: + accessor.set_tensor(attr_name, elem) + return result + + +def load_buffers( + mod: nn.Module, + names: Sequence[str], + buffers: Sequence[Tensor], + as_params: bool = False, +) -> None: + accessor = NamedMemberAccessor(mod) + accessor.set_tensors(names, buffers) + + +def load_state( + model: nn.Module, + weights: Sequence[Tensor], + weight_names: Sequence[str], + buffers: Sequence[Tensor] = (), + buffer_names: Sequence[str] = (), +) -> nn.Module: + """load_state(model, weights, weight_names, buffers=(), buffer_names=()) -> model + + load_state takes `weights` and `buffers` and assigns them to the model. + This is the inverse operation of `make_functional_deprecated_v1`. + """ + if len(weight_names) != len(weights): + raise AssertionError( + f"len(weight_names)={len(weight_names)} != len(weights)={len(weights)}" + ) + load_weights(model, weight_names, weights) + if len(buffers) > 0: + if len(buffer_names) != len(buffers): + raise AssertionError( + f"len(buffer_names)={len(buffer_names)} != len(buffers)={len(buffers)}" + ) + load_buffers(model, buffer_names, buffers) + return model + + +def make_functional_deprecated_v1( + model: nn.Module, +) -> tuple[tuple[Tensor, ...], Callable[..., Any], tuple[str, ...]]: + """make_functional_deprecated_v1(model) -> weights, func, weight_names + + Given an nn.Module, make_functional_deprecated_v1 extracts the state (weights) + and returns a functional version of the model, `func`. This makes + it so that it is possible use transforms over the parameters of + `model`. + + `func` can be invoked as follows: + ``` + x = torch.randn(4, 3) + model = nn.Linear(3, 3) + weights, func, _ = make_functional_deprecated_v1(model) + func(weights, (x,)) + ``` + + And here is an example of applying the grad transform: + ``` + x = torch.randn(4, 3) + model = nn.Linear(3, 3) + weights, _, func = make_functional_deprecated_v1(model) + grad_weights = grad(func)(weights, (x,)) + ``` + + To put the state back into a model, use `load_state`. + """ + buffers = list(model.buffers()) + if len(buffers) > 0: + raise RuntimeError( + "make_functional_deprecated_v1(model): `model` has buffers. Please use " + "make_functional_with_buffers_deprecated_v1(model) instead." + ) + weights, descriptors, _ = extract_weights(model) + + def fun(weights: tuple[Tensor, ...], data: tuple[Any, ...]) -> Any: + mutable_model = copy.deepcopy(model) + load_weights(mutable_model, descriptors, weights) + return mutable_model(*data) + + return weights, fun, descriptors + + +def make_functional_with_buffers_deprecated_v1( + model: nn.Module, +) -> tuple[ + tuple[Tensor, ...], + tuple[Tensor, ...], + Callable[..., Any], + tuple[str, ...], + tuple[str, ...], +]: + """make_functional_with_buffers_deprecated_v1(model) -> weights, buffers, func, weight_names, buffer_names + + Given an nn.Module, make_functional_with_buffers_deprecated_v1 extracts the state (weights and buffers) + and returns a functional version of the model, `func`. + + `func` can be invoked as follows: + ``` + x = torch.randn(4, 3) + model = nn.Linear(3, 3) + weights, buffers, func, _, _ = make_functional_with_buffers_deprecated_v1(model) + func(weights, buffers, (x,)) + ``` + + And here is an example of applying the grad transform: + ``` + x = torch.randn(4, 3) + model = nn.Linear(3, 3) + weights, buffers, func, _, _ = make_functional_with_buffers_deprecated_v1(model) + func(weights, buffers, (x,)) + grad_weights = grad(func)(weights, buffers, (x,)) + ``` + + To put the state back into a model, use `load_state`. + """ + weights, weight_descriptors, _ = extract_weights(model) + buffers, buf_descriptors, _ = extract_buffers(model) + + def fun( + weights: tuple[Tensor, ...], + buffers: tuple[Tensor, ...], + data: tuple[Any, ...], + ) -> Any: + mutable_model = copy.deepcopy(model) + load_weights(mutable_model, weight_descriptors, weights) + load_buffers(mutable_model, buf_descriptors, buffers) + return mutable_model(*data) + + return weights, buffers, fun, weight_descriptors, buf_descriptors + + +class FunctionalModuleWithBuffers(nn.Module): + """ + This is the callable object returned by :func:`make_functional_with_buffers`. + """ + + def __init__( + self, + stateless_model: nn.Module, + param_names: tuple[str, ...], + buffer_names: tuple[str, ...], + param_names_map: dict[str, list[str]], + buffer_names_map: dict[str, list[str]], + ) -> None: + super().__init__() + self.stateless_model = stateless_model + self.param_names = param_names + self.buffer_names = buffer_names + + self.all_names_map = dict(param_names_map) + self.all_names_map.update(buffer_names_map) + + @staticmethod + def _create_from( + model: nn.Module, disable_autograd_tracking: bool = False + ) -> tuple[FunctionalModuleWithBuffers, tuple[Tensor, ...], tuple[Tensor, ...]]: + # TODO: We don't need to copy the model to create a stateless copy + model_copy = copy.deepcopy(model) + params, param_names, param_names_map = extract_weights(model_copy) + buffers, buffer_names, buffer_names_map = extract_buffers(model_copy) + if disable_autograd_tracking: + for param in params: + param.requires_grad_(False) + return ( + FunctionalModuleWithBuffers( + model_copy, param_names, buffer_names, param_names_map, buffer_names_map + ), + params, + buffers, + ) + + def forward( + self, + params: Iterable[Tensor], + buffers: Iterable[Tensor], + *args: Any, + **kwargs: Any, + ) -> Any: + # Temporarily load the state back onto self.stateless_model + old_state = _swap_state( + self.stateless_model, + self.all_names_map, + tuple(params) + tuple(buffers), + ) + try: + return self.stateless_model(*args, **kwargs) + finally: + # Remove the loaded state on self.stateless_model + _swap_state(self.stateless_model, self.all_names_map, old_state) + + +class FunctionalModule(nn.Module): + """ + This is the callable object returned by :func:`make_functional`. + """ + + def __init__( + self, + stateless_model: nn.Module, + param_names: tuple[str, ...], + names_map: dict[str, list[str]], + ) -> None: + super().__init__() + self.stateless_model = stateless_model + self.param_names = param_names + self.names_map = names_map + + @staticmethod + def _create_from( + model: nn.Module, disable_autograd_tracking: bool = False + ) -> tuple[FunctionalModule, tuple[Tensor, ...]]: + # TODO: We don't need to copy the model to create a stateless copy + model_copy = copy.deepcopy(model) + params, param_names, names_map = extract_weights(model_copy) + if disable_autograd_tracking: + for param in params: + param.requires_grad_(False) + return FunctionalModule(model_copy, param_names, names_map), params + + def forward(self, params: Iterable[Tensor], *args: Any, **kwargs: Any) -> Any: + # Temporarily load the state back onto self.stateless_model + old_state = _swap_state(self.stateless_model, self.names_map, params) + try: + return self.stateless_model(*args, **kwargs) + finally: + # Remove the loaded state on self.stateless_model + _swap_state(self.stateless_model, self.names_map, old_state) + + +def make_functional( + model: nn.Module, disable_autograd_tracking: bool = False +) -> tuple[FunctionalModule, tuple[Tensor, ...]]: + """make_functional(model, disable_autograd_tracking=False) -> func, params + + Given a ``torch.nn.Module``, :func:`make_functional` extracts the state + (params) and returns a functional version of the model, ``func``. This + makes it so that it is possible use transforms over the parameters of + ``model``. + + ``func`` can be invoked as follows: + + .. code-block:: python + + import torch + import torch.nn as nn + from functorch import make_functional + + x = torch.randn(4, 3) + model = nn.Linear(3, 3) + func, params = make_functional(model) + func(params, x) + + And here is an example of applying the grad transform over the parameters + of a model. + + .. code-block:: python + + import torch + import torch.nn as nn + from functorch import make_functional, grad + + x = torch.randn(4, 3) + t = torch.randn(4, 3) + model = nn.Linear(3, 3) + func, params = make_functional(model) + + + def compute_loss(params, x, t): + y = func(params, x) + return nn.functional.mse_loss(y, t) + + + grad_weights = grad(compute_loss)(params, x, t) + + If the model has any buffers, please use :func:`make_functional_with_buffers` instead. + + Args: + model (torch.nn.Module): Input model. + disable_autograd_tracking (bool): Flag to disable gradients tracking for output parameters. + The returned params are unrelated to the set of params from the original model. If False (default), + the params will have ``requires_grad=True`` on them (aka they will be trackable with regular + PyTorch autograd), matching the requires_grad-ness of the params from the original model. + Otherwise, the returned params will have ``requires_grad=False``. Default, False. + If you plan on using regular PyTorch autograd (e.g., if you want to call ``.backward()`` or + ``torch.autograd.grad()``, then set ``disable_autograd_tracking=False``. + Otherwise, if you're only planning on using functorch's gradient transforms, + then please set ``disable_autograd_tracking=True`` to avoid unnecessarily tracking + history with PyTorch autograd. + + """ + buffers = list(model.buffers()) + if len(buffers) > 0: + raise RuntimeError( + "make_functional(model): `model` has buffers. Please use " + "make_functional_with_buffers(model) instead." + ) + return FunctionalModule._create_from( + model, disable_autograd_tracking=disable_autograd_tracking + ) + + +def make_functional_with_buffers( + model: nn.Module, disable_autograd_tracking: bool = False +) -> tuple[FunctionalModuleWithBuffers, tuple[Tensor, ...], tuple[Tensor, ...]]: + """make_functional_with_buffers(model, disable_autograd_tracking=False) -> func, params, buffers + + Given a ``torch.nn.Module``, make_functional_with_buffers extracts the + state (params and buffers) and returns a functional version of the model + ``func`` that can be invoked like a function. + + ``func`` can be invoked as follows: + + .. code-block:: python + + import torch + import torch.nn as nn + from functorch import make_functional_with_buffers + + x = torch.randn(4, 3) + model = nn.Linear(3, 3) + func, params, buffers = make_functional_with_buffers(model) + func(params, buffers, x) + + And here is an example of applying the grad transform over the parameters + of a model: + + .. code-block:: python + + import torch + import torch.nn as nn + from functorch import make_functional_with_buffers, grad + + x = torch.randn(4, 3) + t = torch.randn(4, 3) + model = nn.Linear(3, 3) + func, params, buffers = make_functional_with_buffers(model) + + + def compute_loss(params, buffers, x, t): + y = func(params, buffers, x) + return nn.functional.mse_loss(y, t) + + + grad_weights = grad(compute_loss)(params, buffers, x, t) + + Args: + model (torch.nn.Module): Input model. + disable_autograd_tracking (bool): Flag to disable gradients tracking for output parameters. + The returned params are unrelated to the set of params from the original model. If False (default), + the params will have ``requires_grad=True`` on them (aka they will be trackable with regular + PyTorch autograd), matching the requires_grad-ness of the params from the original model. + Otherwise, the returned params will have ``requires_grad=False``. Default, False. + If you plan on using regular PyTorch autograd (e.g., if you want to call ``.backward()`` or + ``torch.autograd.grad()``, then set ``disable_autograd_tracking=False``. + Otherwise, if you're only planning on using functorch's gradient transforms, + then please set ``disable_autograd_tracking=True`` to avoid unnecessarily tracking + history with PyTorch autograd. + + """ + return FunctionalModuleWithBuffers._create_from( + model, disable_autograd_tracking=disable_autograd_tracking + ) + + +def transpose_stack( + tuple_of_tuple_of_tensors: tuple[tuple[Tensor, ...], ...], +) -> tuple[Tensor, ...]: + tuple_of_tuple_of_tensors = tuple(zip(*tuple_of_tuple_of_tensors)) + results = tuple( + torch.stack(shards).detach() for shards in tuple_of_tuple_of_tensors + ) + return results + + +def combine_state_for_ensemble( + models: Sequence[nn.Module], +) -> tuple[FunctionalModuleWithBuffers, tuple[Tensor, ...], tuple[Tensor, ...]]: + """combine_state_for_ensemble(models) -> func, params, buffers + + Prepares a list of torch.nn.Modules for ensembling with :func:`vmap`. + + Given a list of ``M`` ``nn.Modules`` of the same class, stacks all of their + parameters and buffers together to make ``params`` and ``buffers``. + Each parameter and buffer in the result will have an additional dimension + of size ``M``. + + :func:`combine_state_for_ensemble` also returns ``func``, a functional + version of one of the models in :attr:`models`. One cannot directly run + ``func(params, buffers, *args, **kwargs)`` directly, you probably want to + use ``vmap(func, ...)(params, buffers, *args, **kwargs)`` + + Here's an example of how to ensemble over a very simple model: + + .. code-block:: python + + num_models = 5 + batch_size = 64 + in_features, out_features = 3, 3 + models = [torch.nn.Linear(in_features, out_features) for i in range(num_models)] + data = torch.randn(batch_size, 3) + + fmodel, params, buffers = combine_state_for_ensemble(models) + output = vmap(fmodel, (0, 0, None))(params, buffers, data) + + assert output.shape == (num_models, batch_size, out_features) + + .. warning:: + All of the modules being stacked together must be the same (except for + the values of their parameters/buffers). For example, they should be in the + same mode (training vs eval). + + This API is subject to change -- we're investigating better ways to + create ensembles and would love your feedback how to improve this. + """ + if len(models) == 0: + raise RuntimeError( + "combine_state_for_ensemble: Expected at least one model, got 0." + ) + if not (all(m.training for m in models) or all(not m.training for m in models)): + raise RuntimeError( + "combine_state_for_ensemble: Expected all models to " + "have the same training/eval mode." + ) + model0_typ = type(models[0]) + if not all(type(m) is model0_typ for m in models): + raise RuntimeError( + "combine_state_for_ensemble: Expected all models to be of the same class." + ) + funcs, params, buffers = zip( + *[make_functional_with_buffers(model) for model in models] + ) + params = transpose_stack(params) + buffers = transpose_stack(buffers) + return funcs[0], params, buffers + + +def functional_init( + model_class: type[nn.Module], + ensemble_shape: tuple[()] | tuple[int, ...] = (), + device: torch.types.Device = "cpu", +) -> Callable[..., tuple[tuple[Tensor, ...], Callable[..., Any], tuple[str, ...]]]: + def wrapped( + *args: Any, **kwargs: Any + ) -> tuple[tuple[Tensor, ...], Callable[..., Any], tuple[str, ...]]: + if len(ensemble_shape) >= 2: + raise ValueError("NYI: ensemble_shape with more than 1 element") + if len(ensemble_shape) == 0: + model = model_class(*args, **kwargs).to(device) + return make_functional_deprecated_v1(model) + num_models = ensemble_shape[0] # type: ignore[misc] + if num_models <= 0: + raise ValueError(f"num_models {num_models} should be > 0") + # NB: Not very efficient, more of a POC + models = tuple( + model_class(*args, **kwargs).to(device) for _ in range(num_models) + ) + _, fn, names = make_functional_deprecated_v1(model_class(*args, **kwargs)) + weights = tuple(make_functional_deprecated_v1(model)[0] for model in models) + weights = tuple(zip(*weights)) + weights = tuple(torch.stack(shards).detach() for shards in weights) + return weights, fn, names + + return wrapped + + +def functional_init_with_buffers( + model_class: type[nn.Module], + ensemble_shape: tuple[()] | tuple[int, ...] = (), + device: torch.types.Device = "cpu", +) -> Callable[ + ..., + tuple[ + tuple[Tensor, ...], + tuple[Tensor, ...], + Callable[..., Any], + tuple[str, ...], + tuple[str, ...], + ] + | tuple[tuple[Tensor, ...], Callable[..., Any], tuple[str, ...]], +]: + def wrapped( + *args: Any, **kwargs: Any + ) -> ( + tuple[ + tuple[Tensor, ...], + tuple[Tensor, ...], + Callable[..., Any], + tuple[str, ...], + tuple[str, ...], + ] + | tuple[tuple[Tensor, ...], Callable[..., Any], tuple[str, ...]] + ): + if len(ensemble_shape) >= 2: + raise ValueError("NYI: ensemble_shape with more than 1 element") + if len(ensemble_shape) == 0: + model = model_class(*args, **kwargs).to(device) + return make_functional_deprecated_v1(model) + num_models = ensemble_shape[0] # type: ignore[misc] + if num_models <= 0: + raise ValueError(f"num_models {num_models} should be > 0") + # NB: Not very efficient, more of a POC + models = tuple( + model_class(*args, **kwargs).to(device) for _ in range(num_models) + ) + ( + _, + _, + fn, + weight_names, + buffer_names, + ) = make_functional_with_buffers_deprecated_v1(model_class(*args, **kwargs)) + weights, buffers = zip( + *tuple( + make_functional_with_buffers_deprecated_v1(model)[:2] + for model in models + ) + ) + weights = tuple(zip(*weights)) + weights = tuple(torch.stack(shards).detach() for shards in weights) + buffers = tuple(zip(*buffers)) + buffers = tuple(torch.stack(shards).detach() for shards in buffers) + return weights, buffers, fn, weight_names, buffer_names + + return wrapped diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/partitioners.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/partitioners.py new file mode 100644 index 0000000000000000000000000000000000000000..6042049bebe1244236e02eef7c462bff95cf961c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/partitioners.py @@ -0,0 +1,3771 @@ +from __future__ import annotations + +import copy +import functools +import hashlib +import heapq +import itertools +import logging +import math +import operator +import os +import os.path +import re +import warnings +from collections import defaultdict, deque +from collections.abc import Callable +from dataclasses import dataclass, replace +from typing import Any, TYPE_CHECKING + +import torch +import torch._inductor.inductor_prims +import torch.distributed +import torch.fx as fx +import torch.utils._pytree as pytree +from torch._dynamo.utils import counters, is_node_meta_valid +from torch._functorch._activation_checkpointing.ac_logging_utils import ( + create_structured_trace_for_min_cut_info, +) +from torch._functorch._aot_autograd.utils import is_with_effects +from torch._inductor import config as inductor_config +from torch._inductor.custom_graph_pass import ( + CustomKnapsackSolver, + CustomRuntimeEstimator, +) +from torch._library.fake_class_registry import FakeScriptObject +from torch._library.utils import is_builtin +from torch._logging import LazyString, trace_structured +from torch._logging._internal import trace_log +from torch._subclasses.fake_tensor import extract_tensor_metadata +from torch.fx.experimental._backward_state import BackwardState +from torch.fx.experimental.proxy_tensor import is_sym_node, py_sym_types +from torch.fx.experimental.sym_node import magic_methods, method_to_operator +from torch.fx.experimental.symbolic_shapes import ( + find_symbol_binding_fx_nodes, + free_symbols, + is_symbol_binding_fx_node, + optimization_hint, + statically_known_false, + statically_known_true, +) +from torch.fx.passes import graph_drawer +from torch.utils._ordered_set import OrderedSet +from torch.utils.checkpoint import CheckpointPolicy + +from . import config +from ._activation_checkpointing.graph_info_provider import GraphInfoProvider +from ._activation_checkpointing.knapsack import ( + dp_knapsack, + dp_knapsack_sliding_hirschberg, + greedy_knapsack, + ilp_knapsack, +) +from ._activation_checkpointing.knapsack_evaluator import KnapsackEvaluator +from ._aot_autograd.descriptors import ( + AOTOutput, + SavedForBackwardsAOTOutput, + SavedForBackwardsNoVcCheckAOTOutput, +) +from ._aot_autograd.functional_utils import _is_functional_graph +from ._aot_autograd.graph_compile import is_opaque_node +from ._aot_autograd.logging_utils import get_aot_graph_name +from ._aot_autograd.utils import ( + _is_bwd_seed_offset, + _is_fwd_seed_offset, + _is_primal, + _is_tangent, + get_cuda_generator_meta_val, +) +from .compile_utils import fx_graph_cse, get_aten_target, raise_getitems + + +if TYPE_CHECKING: + import networkx as nx + import sympy + + +AOT_PARTITIONER_DEBUG: bool = config.debug_partitioner +log: logging.Logger = logging.getLogger(__name__) + +aten = torch.ops.aten +prims = torch.ops.prims + + +@dataclass +class OpTypes: + """Class for keeping track of different operator categories""" + + fusible_ops: OrderedSet[Callable[..., Any]] + compute_intensive_ops: OrderedSet[Callable[..., Any]] + random_ops: OrderedSet[Callable[..., Any]] + view_ops: OrderedSet[Callable[..., Any]] + recomputable_ops: OrderedSet[Callable[..., Any]] + + def is_fusible(self, node: fx.Node) -> bool: + return get_aten_target(node) in self.fusible_ops + + def is_compute_intensive(self, node: fx.Node) -> bool: + return get_aten_target(node) in self.compute_intensive_ops + + def is_random(self, node: fx.Node) -> bool: + return get_aten_target(node) in self.random_ops + + def is_view(self, node: fx.Node) -> bool: + return get_aten_target(node) in self.view_ops + + def is_recomputable(self, node: fx.Node) -> bool: + return get_aten_target(node) in self.recomputable_ops + + +@dataclass +class NodeInfo: + # Be careful about iterating over these explicitly, as their order may not + # be deterministic + inputs: list[fx.Node] + _required_fw_nodes: OrderedSet[fx.Node] + required_bw_nodes: OrderedSet[fx.Node] + tangents_closure: OrderedSet[fx.Node] + unclaimed_nodes: OrderedSet[fx.Node] + fw_order: dict[fx.Node, int] + # Effectively maps to which of our primals are parameters + static_lifetime_input_nodes: OrderedSet[fx.Node] + + @functools.cached_property + def required_fw_nodes(self) -> list[fx.Node]: + return sorted( + (n for n in self._required_fw_nodes), key=lambda n: self.fw_order[n] + ) + + def is_required_fw(self, n: fx.Node) -> bool: + return n in self._required_fw_nodes + + def is_required_bw(self, n: fx.Node) -> bool: + return n in self.required_bw_nodes + + def is_unclaimed(self, n: fx.Node) -> bool: + return n in self.unclaimed_nodes + + def get_fw_order(self, n: fx.Node) -> int: + if n not in self._required_fw_nodes: + raise AssertionError(f"Node {n} not in fw nodes!") + return self.fw_order[n] + + +@dataclass +class MinCutOptions: + ban_if_used_far_apart: bool + ban_if_long_fusible_chains: bool + ban_if_materialized_backward: bool + ban_if_not_in_allowlist: bool + ban_if_reduction: bool + + +def must_recompute(node: fx.Node) -> bool: + return node.meta.get("recompute", None) in [ + CheckpointPolicy.MUST_RECOMPUTE, + CheckpointPolicy.PREFER_RECOMPUTE, + ] + + +def has_recomputable_ops(fx_g: fx.GraphModule) -> bool: + for node in fx_g.graph.nodes: + if must_recompute(node): + return True + return False + + +def has_recomputable_rng_ops(fx_g: fx.GraphModule) -> bool: + for node in fx_g.graph.nodes: + if ( + must_recompute(node) + and hasattr(node.target, "tags") + and torch.Tag.nondeterministic_seeded in node.target.tags + ): + return True + return False + + +def sym_node_size(node: fx.Node) -> int: + if isinstance(node.meta["val"], (torch.SymInt, torch.SymBool)): + return 1 + if not isinstance(node.meta["val"], torch.SymFloat): + raise AssertionError( + f"expected node.meta['val'] to be SymFloat, got {type(node.meta['val'])}" + ) + return 4 + + +class InvalidNodeBase: + def __repr__(self) -> str: + return "Invalid Node" + + +# Run DCE while overriding the definition of is_impure_node +def is_not_collective(node: fx.Node) -> bool: + return getattr(node.target, "namespace", None) != "_c10d_functional" + + +InvalidNode = InvalidNodeBase() + + +def _get_ho_op_original_input(getitem_node: fx.Node) -> fx.Node | None: + """Given a getitem node, check if it extracts from a higher-order op + that has kwargs mapping the key back to an original input. + + Returns the original input node if found, None otherwise. + """ + if getitem_node.target != operator.getitem: + return None + ho_result = getitem_node.args[0] + key = getitem_node.args[1] + if not isinstance(ho_result, fx.Node) or ho_result.op != "call_function": + return None + if "kwargs" not in ho_result.kwargs: + return None + kwargs = ho_result.kwargs["kwargs"] + # pyrefly: ignore [not-iterable, unsupported-operation] + if key not in kwargs: + return None + # pyrefly: ignore [bad-index, unsupported-operation] + original_input = kwargs[key] + if isinstance(original_input, fx.Node): + return original_input + return None + + +def _is_copy_node_bw_only(node: fx.Node) -> fx.Node | None: + """Check if node is a view/reshape of a higher-order op output that aliases an input. + + Returns the original input node from the higher-order op's kwargs if the pattern + matches, None otherwise. + """ + if node.target not in (torch.ops.aten.view.default, torch.ops.aten.reshape.default): + return None + source = node.args[0] + if not isinstance(source, fx.Node): + return None + return _get_ho_op_original_input(source) + + +def _find_input_for_invalid_output( + node: fx.Node, + env: dict[fx.Node, Any], +) -> fx.Node | None: + """Try to find a valid input replacement for an invalid forward output. + + This handles cases where a forward output depends on backward nodes but + semantically aliases an input. For example, a view of a getitem from a + triton kernel that mutates a buffer in backward, or a direct getitem from + such a higher-order op. The original input may be a primal or a valid + intermediate node already present in the forward graph. + """ + # Pattern 1: view/reshape(getitem(ho_op, key)) -> ho_op.kwargs["kwargs"][key] + original_input = _is_copy_node_bw_only(node) + if ( + original_input is not None + and original_input in env + and not isinstance(env[original_input], InvalidNodeBase) + ): + return env[original_input] + # Pattern 2: getitem(ho_op, key) -> ho_op.kwargs["kwargs"][key] + original_input = _get_ho_op_original_input(node) + if ( + original_input is not None + and original_input in env + and not isinstance(env[original_input], InvalidNodeBase) + ): + return env[original_input] + return None + + +def _extract_graph_with_inputs_outputs( + joint_graph: fx.Graph, + inputs: list[fx.Node], + outputs: list[fx.Node], + outputs_descs: list[AOTOutput], + subgraph: str | None = None, + ignore_must_be_in_fw_bw: bool = False, +) -> fx.Graph: + """ + Given a graph, extracts out a subgraph that takes the specified nodes as + inputs and returns the specified outputs. + + This includes specifying non-placeholder nodes as inputs. + + The general strategy is to initialize all inputs with proxies as we + encounter them, and trace through the graph, only keeping values which take + in valid proxies. Then, all dead code is eliminated. + """ + new_graph = fx.Graph() + env: dict[fx.Node, fx.Node] = {} + + # Add new placeholder nodes in the order specified by the inputs + for node in inputs: + new_node = new_graph.placeholder(node.name) + # Can't use node_copy here as we may be turning previous call_function into placeholders + new_node.meta = node.meta + # pyrefly: ignore [unsupported-operation] + env[node] = new_node + + for node in joint_graph.nodes: + if not ignore_must_be_in_fw_bw: + if ( + _must_be_in_backward(node) + and subgraph != "backward" + and node not in inputs + ): + env[node] = InvalidNode # type: ignore[assignment] + continue + + if ( + _must_be_in_forward(node) + and subgraph != "forward" + and node not in inputs + ): + env[node] = InvalidNode # type: ignore[assignment] + continue + + if node in env: + # Node must be one of our inputs. (Any member of env which wasn't an + # input to start must have been created by this loop and won't be in + # joint_graph.nodes). + continue + elif node.op == "placeholder": + env[node] = InvalidNode # type: ignore[assignment] + elif node.op == "call_function": + all_args = pytree.arg_tree_leaves(*node.args, **node.kwargs) + all_args = [ + isinstance(env[x], InvalidNodeBase) + for x in all_args + if isinstance(x, fx.Node) + ] + if any(all_args): + env[node] = InvalidNode # type: ignore[assignment] + continue + # pyrefly: ignore [unsupported-operation, bad-argument-type] + env[node] = new_graph.node_copy(node, lambda x: env[x]) + elif node.op == "get_attr": + # pyrefly: ignore [unsupported-operation, bad-argument-type] + env[node] = new_graph.node_copy(node, lambda x: env[x]) + elif node.op == "output": + pass + output_values = [] + for x, x_desc in zip(outputs, outputs_descs): + if isinstance(x, fx.Node): + if x not in env: + raise RuntimeError(f"Node {x} couldn't be found in env") + if isinstance(env[x], InvalidNodeBase): + # For forward outputs that are invalid (depend on backward), try + # to find a valid replacement. + replacement = None + # For copy_ nodes that are backward-only, use the destination + # (first arg) which is the original input. + if ( + x.target is torch.ops.aten.copy_.default + and _must_be_in_backward(x) + and len(x.args) >= 1 + and isinstance(x.args[0], fx.Node) + and x.args[0] in env + and not isinstance(env[x.args[0]], InvalidNodeBase) + ): + replacement = env[x.args[0]] + # For view/reshape outputs that trace back to a getitem of a + # higher-order op that mutates an input, find that input. + # This handles custom_function_view outputs from triton kernels. + if replacement is None: + replacement = _find_input_for_invalid_output(x, env) + if replacement is not None: + output_values.append(replacement) + continue + raise AssertionError(f"Node {x} was invalid, but is output") + output_values.append(env[x]) + else: + output_values.append(x) + out = new_graph.output(tuple(output_values)) + out.meta["desc"] = outputs_descs + # Snapshot stack traces on the output node before passes run, + # as later passes may strip stack_trace from individual nodes. + out.meta["output_stack_traces"] = [ + v.meta.get("stack_trace") if isinstance(v, fx.Node) else None + for v in output_values + ] + + new_graph.eliminate_dead_code() + new_graph.lint() + return new_graph + + +def is_non_builtin_to_include(node: fx.Node) -> bool: + return config.is_non_builtin_to_include and ( + (isinstance(node.target, torch._ops.OpOverload) and not is_builtin(node.target)) + or node.target == torch.ops.higher_order.triton_kernel_wrapper_functional + ) + + +def _is_backward_state(node: fx.Node) -> bool: + return node.op == "placeholder" and isinstance(node.meta.get("val"), BackwardState) + + +def _has_tag_is_backward(node: fx.Node) -> bool: + return node.meta.get("partitioner_tag", None) == "is_backward" + + +def _has_tag_is_forward(node: fx.Node) -> bool: + return node.meta.get("partitioner_tag", None) == "is_forward" + + +def _has_tag_must_be_in_forward(node: fx.Node) -> bool: + return node.meta.get("partitioner_tag", None) == "must_be_in_forward" + + +def _has_tag_must_be_in_backward(node: fx.Node) -> bool: + return node.meta.get("partitioner_tag", None) == "must_be_in_backward" + + +def _must_be_in_forward(node: fx.Node) -> bool: + if _has_tag_must_be_in_forward(node): + return True + + is_mutable = ( + isinstance(node.target, torch._ops.OpOverload) + and node.target._schema.is_mutable + ) + return ( + not _has_tag_is_backward(node) + and not _has_tag_must_be_in_backward(node) + and is_mutable + ) + + +def _must_be_in_backward(node: fx.Node) -> bool: + if _has_tag_must_be_in_backward(node): + return True + is_mutable = ( + isinstance(node.target, torch._ops.OpOverload) + and node.target._schema.is_mutable + ) + return _has_tag_is_backward(node) and is_mutable + + +def _extract_fwd_bwd_outputs( + joint_module: fx.GraphModule, *, num_fwd_outputs: int +) -> tuple[list[fx.Node], list[fx.Node], list[AOTOutput], list[AOTOutput]]: + outputs = pytree.arg_tree_leaves( + *(node.args for node in joint_module.graph.find_nodes(op="output")) + ) + outputs_descs = pytree.arg_tree_leaves( + next(iter(joint_module.graph.find_nodes(op="output"))).meta.get( + "desc", [None] * len(outputs) + ) + ) + fwd_outputs = outputs[:num_fwd_outputs] + bwd_outputs = outputs[num_fwd_outputs:] + fwd_outputs_descs = outputs_descs[:num_fwd_outputs] + bwd_outputs_descs = outputs_descs[num_fwd_outputs:] + return fwd_outputs, bwd_outputs, fwd_outputs_descs, bwd_outputs_descs + + +def _remove_by_name(saved_values: list[fx.Node], name: str) -> None: + for saved_value in saved_values: + if saved_value.name == name: + saved_values.remove(saved_value) + break + + +def find_first_sym_node( + fwd_module_outputs: list[fx.Node] | tuple[fx.Node, ...], +) -> int: + idx = len(fwd_module_outputs) + for i in range(len(fwd_module_outputs) - 1, -1, -1): + if not is_sym_node(fwd_module_outputs[i]): + idx = i + 1 + break + return idx + + +def calculate_quantization_scaling( + graph: torch.fx.Graph, + node: torch.fx.Node, + max: float = 57344.0, + min: float = 1e-12, + position: int = 0, +) -> torch.fx.Node: + with graph.inserting_after(node): + abs_node = graph.call_function( + torch.ops.aten.abs.default, + args=(node,), + ) + abs_node.meta["val"] = torch.ops.aten.abs.default(node.meta["val"]) + abs_node.meta["tensor_meta"] = extract_tensor_metadata(abs_node.meta["val"]) + with graph.inserting_after(abs_node): + amax_node = graph.call_function( + torch.ops.aten.amax.default, + args=(abs_node, [-1], True), + ) + amax_node.meta["val"] = torch.ops.aten.amax.default( + abs_node.meta["val"], [-1], True + ) + amax_node.meta["tensor_meta"] = extract_tensor_metadata(amax_node.meta["val"]) + with graph.inserting_after(amax_node): + amax_64_node = graph.call_function( + torch.ops.prims.convert_element_type.default, + args=(amax_node, torch.float64), + ) + amax_64_node.meta["val"] = torch.ops.prims.convert_element_type.default( + amax_node.meta["val"], torch.float64 + ) + amax_64_node.meta["tensor_meta"] = extract_tensor_metadata( + amax_64_node.meta["val"] + ) + with graph.inserting_after(amax_64_node): + clamp_min_node = graph.call_function( + torch.ops.aten.clamp_min.default, + args=(amax_64_node, min), + ) + clamp_min_node.meta["val"] = torch.ops.aten.clamp_min.default( + amax_64_node.meta["val"], min + ) + clamp_min_node.meta["tensor_meta"] = extract_tensor_metadata( + clamp_min_node.meta["val"] + ) + with graph.inserting_after(clamp_min_node): + reciprocal_node = graph.call_function( + torch.ops.aten.reciprocal.default, + args=(clamp_min_node,), + ) + reciprocal_node.meta["val"] = torch.ops.aten.reciprocal.default( + clamp_min_node.meta["val"] + ) + reciprocal_node.meta["tensor_meta"] = extract_tensor_metadata( + reciprocal_node.meta["val"] + ) + with graph.inserting_after(reciprocal_node): + mul_node = graph.call_function( + torch.ops.aten.mul.Tensor, + args=(reciprocal_node, max), + ) + mul_node.meta["val"] = torch.ops.aten.mul.Tensor( + reciprocal_node.meta["val"], max + ) + mul_node.meta["tensor_meta"] = extract_tensor_metadata(mul_node.meta["val"]) + with graph.inserting_after(mul_node): + scale_node = graph.call_function( + torch.ops.prims.convert_element_type.default, + args=(mul_node, torch.float32), + name=f"fp8_scale_pos_{position}_{node.name}", + ) + scale_node.meta["val"] = torch.ops.prims.convert_element_type.default( + mul_node.meta["val"], torch.float32 + ) + scale_node.meta["tensor_meta"] = extract_tensor_metadata(scale_node.meta["val"]) + return scale_node + + +def perform_quantization( + graph: torch.fx.Graph, + node: torch.fx.Node, + scale_node: torch.fx.Node, + quant_type: torch.dtype, + clamp_min: float, + clamp_max: float, + position: int, +) -> torch.fx.Node: + with graph.inserting_after(scale_node): + target_node_32 = graph.call_function( + torch.ops.prims.convert_element_type.default, + args=(node, torch.float32), + ) + target_node_32.meta["val"] = torch.ops.prims.convert_element_type.default( + node.meta["val"], torch.float32 + ) + target_node_32.meta["tensor_meta"] = extract_tensor_metadata( + target_node_32.meta["val"] + ) + with graph.inserting_after(target_node_32): + scaled_target_node = graph.call_function( + torch.ops.aten.mul.Tensor, + args=(target_node_32, scale_node), + ) + scaled_target_node.meta["val"] = torch.ops.aten.mul.Tensor( + target_node_32.meta["val"], scale_node.meta["val"] + ) + scaled_target_node.meta["tensor_meta"] = extract_tensor_metadata( + scaled_target_node.meta["val"] + ) + with graph.inserting_after(scaled_target_node): + clamp_min_scaled_node = graph.call_function( + torch.ops.aten.clamp_min.default, + args=(scaled_target_node, clamp_min), + ) + clamp_min_scaled_node.meta["val"] = torch.ops.aten.clamp_min.default( + scaled_target_node.meta["val"], clamp_min + ) + clamp_min_scaled_node.meta["tensor_meta"] = extract_tensor_metadata( + clamp_min_scaled_node.meta["val"] + ) + with graph.inserting_after(clamp_min_scaled_node): + clamp_max_scaled_node = graph.call_function( + torch.ops.aten.clamp_max.default, + args=(clamp_min_scaled_node, clamp_max), + ) + clamp_max_scaled_node.meta["val"] = torch.ops.aten.clamp_max.default( + clamp_min_scaled_node.meta["val"], clamp_max + ) + clamp_max_scaled_node.meta["tensor_meta"] = extract_tensor_metadata( + clamp_max_scaled_node.meta["val"] + ) + with graph.inserting_after(clamp_max_scaled_node): + quant_activation_node = graph.call_function( + torch.ops.prims.convert_element_type.default, + args=(clamp_max_scaled_node, quant_type), + name=f"fp8_quant_pos_{position}_{node.name}", + ) + quant_activation_node.meta["val"] = ( + torch.ops.prims.convert_element_type.default( + clamp_max_scaled_node.meta["val"], quant_type + ) + ) + quant_activation_node.meta["tensor_meta"] = extract_tensor_metadata( + quant_activation_node.meta["val"] + ) + return quant_activation_node + + +def calculate_tensor_size(tensor: torch.Tensor) -> float: + """ + Calculate the size of a PyTorch tensor in megabytes (MB). + + Args: + tensor (torch.Tensor): Input tensor + + Returns: + float: Memory size in MB + """ + # Get number of elements and size per element + num_elements = tensor.numel() + element_size = tensor.element_size() + + return (num_elements * element_size) / (1024 * 1024) + + +def get_allowed_dtypes() -> list[torch.dtype]: + allowed_dtypes = torch._inductor.config.post_grad_fusion_options[ + "activation_quantization_aten_pass" + ].get("allowed_dtypes", "torch.bfloat16") + allowed_dtypes = [ + getattr(torch, dtype.split(".")[-1]) for dtype in allowed_dtypes.split(";") + ] + return allowed_dtypes + + +def should_quantize(node: torch.fx.Node) -> bool: + allowed_dtypes = get_allowed_dtypes() + if not is_node_meta_valid(node) or node.meta["val"].dtype not in allowed_dtypes: + return False + size_threshold = torch._inductor.config.post_grad_fusion_options[ + "activation_quantization_aten_pass" + ].get("size_in_mb", 100) + # calculate the size of the node + size_in_mb = calculate_tensor_size(node.meta["val"]) + if not torch._inductor.config.post_grad_fusion_options[ + "activation_quantization_aten_pass" + ].get("skip_dynamo_guards", False): + return size_in_mb >= size_threshold + else: + # case 1: we always quantize tensors with dynamic shapes + if torch._inductor.config.post_grad_fusion_options[ + "activation_quantization_aten_pass" + ].get("quantize_dynamic_shape", False): + return statically_known_true( + size_in_mb >= size_threshold + ) or not statically_known_false(size_in_mb >= size_threshold) + else: + # case 2: we always not quantize tensors with dynamic shapes + return statically_known_true(size_in_mb >= size_threshold) + + +def get_quant_type() -> torch.dtype: + quant_type = torch._inductor.config.post_grad_fusion_options[ + "activation_quantization_aten_pass" + ].get("quant_type", "torch.float8_e5m2") + + return getattr(torch, quant_type.split(".")[-1]) + + +def calculate_range(dtype: torch.dtype) -> tuple[float, float]: + """ + Calculate the range of values for a given torch.dtype. + Args: + dtype (torch.dtype): The input dtype. + Returns: + tuple: A tuple containing the minimum and maximum values. + """ + info = torch.finfo(dtype) + return info.min, info.max + + +def quantize_activation_fw(graph: torch.fx.Graph, num_fwd_outputs: int = 0) -> None: + output = graph.find_nodes(op="output")[0] + fwd_outputs = output.args[0] + quant_type = get_quant_type() + clamp_min, clamp_max = calculate_range(quant_type) + position_to_quant = dict() + tensor_scale_nodes: list[fx.Node] = [] + sym_scale_nodes: list[fx.Node] = [] + for position, node in enumerate(fwd_outputs): + # Don't quantize user-visible forward outputs. A tensor may appear as + # both a user output and a saved-for-backward activation (same FX node + # at two positions). Quantizing the user output position would: + # 1. Return fp8 to the user instead of the original precision + # 2. Create duplicate fp8_quant/fp8_scale backward placeholders that + # shift the stride mapping in _aot_stage2b_bw_compile (T264303372) + if position < num_fwd_outputs: + continue + # check if the activation node is the node saved for quantization + if node.meta.get("saved_for_quantization", False): + # case: use scaling + if torch._inductor.config.post_grad_fusion_options[ + "activation_quantization_aten_pass" + ].get("use_scaling", True): + # calculating the scale + scale_node = calculate_quantization_scaling( + graph, node, clamp_max, 1e-12, position + ) + + # converting to fp8 + quant_node = perform_quantization( + graph, node, scale_node, quant_type, clamp_min, clamp_max, position + ) + if not is_sym_node(scale_node): + tensor_scale_nodes.append(scale_node) + else: + sym_scale_nodes.append(scale_node) + else: + # case: do not use scaling + with graph.inserting_after(node): + quant_node = graph.call_function( + torch.ops.prims.convert_element_type.default, + args=(node, quant_type), + name=f"fp8_quant_pos_{position}_{node.name}", + ) + quant_node.meta["val"] = ( + torch.ops.prims.convert_element_type.default( + node.meta["val"], quant_type + ) + ) + quant_node.meta["tensor_meta"] = extract_tensor_metadata( + quant_node.meta["val"] + ) + + position_to_quant[position] = quant_node + + # Use position-based lookup for building output + # only update the return node args, and remain all other users unchanged + output_updated_args = [ + position_to_quant.get(i, node) for i, node in enumerate(fwd_outputs) + ] + # add the scale nodes to the output find the first sym_node in the output + # pyrefly: ignore [bad-argument-type] + idx = find_first_sym_node(output_updated_args) + scale_nodes = tensor_scale_nodes + sym_scale_nodes + if scale_nodes: + output_updated_args = ( + output_updated_args[:idx] + scale_nodes + output_updated_args[idx:] + ) + + output.update_arg(0, tuple(output_updated_args)) + counters["inductor"]["activation_quantization_fwd_aten_pass"] += 1 + + +def quantize_activation_bw(graph: torch.fx.Graph) -> None: + bw_inputs = [node for node in graph.nodes if node.op == "placeholder"] + activation_node = None + for node in bw_inputs: + if node.meta.get("saved_for_quantization", False): + node.meta.pop("saved_for_quantization") + dequant_type = node.meta.pop("dequant_type") + # dequantize the node + if torch._inductor.config.post_grad_fusion_options[ + "activation_quantization_aten_pass" + ].get("use_scaling", False): + # case: use scaling + with graph.inserting_after(node): + # find corresponding scale node + scale_name = "fp8_scale_" + node.name.replace("fp8_quant_", "") + scale_node = next( + bwd_input + for bwd_input in bw_inputs + if bwd_input.name == scale_name + ) + with graph.inserting_after(scale_node): + activation_node = graph.call_function( + torch.ops.prims.convert_element_type.default, + args=(node, dequant_type), + ) + activation_node.meta["val"] = ( + torch.ops.prims.convert_element_type.default( + node.meta["val"], dequant_type + ) + ) + activation_node.meta["tensor_meta"] = extract_tensor_metadata( + activation_node.meta["val"] + ) + with graph.inserting_after(activation_node): + divided_target_node_32 = graph.call_function( + torch.ops.aten.div.Tensor, + args=(activation_node, scale_node), + ) + divided_target_node_32.meta["val"] = torch.ops.aten.div.Tensor( + activation_node.meta["val"], scale_node.meta["val"] + ) + divided_target_node_32.meta["tensor_meta"] = ( + extract_tensor_metadata(divided_target_node_32.meta["val"]) + ) + with graph.inserting_after(divided_target_node_32): + dequant_node = graph.call_function( + torch.ops.prims.convert_element_type.default, + args=(divided_target_node_32, dequant_type), + ) + dequant_node.meta["val"] = ( + torch.ops.prims.convert_element_type.default( + divided_target_node_32.meta["val"], dequant_type + ) + ) + dequant_node.meta["tensor_meta"] = extract_tensor_metadata( + dequant_node.meta["val"] + ) + else: + with graph.inserting_after(node): + dequant_node = graph.call_function( + torch.ops.prims.convert_element_type.default, + args=(node, dequant_type), + name="dequant_" + str(node.name), + ) + dequant_node.meta["val"] = ( + torch.ops.prims.convert_element_type.default( + node.meta["val"], dequant_type + ) + ) + dequant_node.meta["tensor_meta"] = extract_tensor_metadata( + dequant_node.meta["val"] + ) + # find the users of the node and replace them with the new node except the dequant_node + for user in list(node.users.keys()): + if user != dequant_node and user != activation_node: + user.replace_input_with(node, dequant_node) + + counters["inductor"]["activation_quantization_bwd_aten_pass"] += 1 + + +def perform_fp8_activation_quantization( + fwd_module: fx.GraphModule, + bwd_module: fx.GraphModule, + bwd_module_inputs: dict[str, fx.Node], + num_fwd_outputs: int = 0, +) -> None: + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "before_activation_quantization_fwd_aten_pass", + "encoding": "string", + }, + payload_fn=lambda: fwd_module.print_readable( + print_output=False, include_stride=True, include_device=True + ), + ) + + quantize_activation_fw(fwd_module.graph, num_fwd_outputs) + + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "after_activation_quantization_fwd_aten_pass", + "encoding": "string", + }, + payload_fn=lambda: fwd_module.print_readable( + print_output=False, include_stride=True, include_device=True + ), + ) + + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "before_activation_quantization_bwd_aten_pass", + "encoding": "string", + }, + payload_fn=lambda: bwd_module.print_readable( + print_output=False, include_stride=True, include_device=True + ), + ) + + quant_fwd_module_outputs = fwd_module.graph.find_nodes(op="output")[0].args[0] + # update the corresponding bwd_inputs due to the fwd_outputs quantization + for fwd_node in quant_fwd_module_outputs: + if "fp8_quant_" in fwd_node.name: + bwd_input = bwd_module_inputs[ + re.sub(r"^fp8_quant_pos_\d+_", "", fwd_node.name) + ] + with bwd_module.graph.inserting_after(bwd_input): + quant_bwd_input = bwd_module.graph.placeholder(name=fwd_node.name) + dequant_type = bwd_input.meta["dequant_type"] + quant_bwd_input.meta.update(fwd_node.meta) + quant_bwd_input.meta["saved_for_quantization"] = True + quant_bwd_input.meta["dequant_type"] = dequant_type + bwd_input.replace_all_uses_with(quant_bwd_input) + bwd_module.graph.erase_node(bwd_input) + # update the bwd_inputs if quantization with scaling is used + if torch._inductor.config.post_grad_fusion_options[ + "activation_quantization_aten_pass" + ].get("use_scaling", True): + quant_bwd_module_inputs = list(bwd_module.graph.find_nodes(op="placeholder")) + # update the corresponding bwd input nodes find the last non-tangent node + bwd_input_loc = quant_bwd_module_inputs[-1] + for bw_input in reversed(quant_bwd_module_inputs): + if not _is_tangent(bw_input): + bwd_input_loc = bw_input + break + + scaled_fwd_module_outputs = fwd_module.graph.find_nodes(op="output")[0].args[0] + for fwd_node in scaled_fwd_module_outputs: + if "fp8_scale_" in fwd_node.name: + # fwd node is a scale node + with bwd_module.graph.inserting_after(bwd_input_loc): + scale_bwd_input = bwd_module.graph.placeholder(name=fwd_node.name) + scale_bwd_input.meta.update(fwd_node.meta) + bwd_input_loc = scale_bwd_input + + quantize_activation_bw(bwd_module.graph) + + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "after_activation_quantization_bwd_aten_pass", + "encoding": "string", + }, + payload_fn=lambda: bwd_module.print_readable( + print_output=False, include_stride=True, include_device=True + ), + ) + + +def enable_activation_quantization( + saved_values: list[fx.Node], + fwd_module: fx.GraphModule, + bwd_module: fx.GraphModule, + static_lifetime_input_nodes: OrderedSet[fx.Node] | None = None, + num_fwd_outputs: int = 0, +) -> None: + static_input_names: list[str] = ( + [node.name for node in static_lifetime_input_nodes] + if static_lifetime_input_nodes + else [] + ) + saved_values_names = {node.name: node for node in saved_values} + if torch._inductor.config.post_grad_fusion_options[ + "activation_quantization_aten_pass" + ].get("exclude_primals", False): + saved_values_names = { + node.name: node for node in saved_values if "primals" not in node.name + } + fwd_module_outputs = fwd_module.graph.find_nodes(op="output")[0].args[0] + bwd_module_inputs = { + node.name: node for node in bwd_module.graph.find_nodes(op="placeholder") + } + should_perform_fp8_quant = False + for node in fwd_module_outputs: + if node.name in saved_values_names and should_quantize(node): + if node.name in static_input_names: + log.debug("Skipping quantization of static input %s: ", node.name) + continue + node.meta["saved_for_quantization"] = True + node.meta["dequant_type"] = node.meta["val"].dtype + # some of the fwd outputs and bwd inputs are not share the same object + bwd_module_inputs[node.name].meta["saved_for_quantization"] = True + bwd_module_inputs[node.name].meta["dequant_type"] = node.meta["val"].dtype + should_perform_fp8_quant = True + + if should_perform_fp8_quant: + perform_fp8_activation_quantization( + fwd_module, bwd_module, bwd_module_inputs, num_fwd_outputs + ) + + +def _extract_fwd_bwd_modules( + joint_module: fx.GraphModule, + saved_values: list[fx.Node], + saved_sym_nodes: list[fx.Node], + saved_opaque_nodes: list[fx.Node] | None = None, + *, + num_fwd_outputs: int, + static_lifetime_input_nodes: OrderedSet[fx.Node] | None = None, + ignore_must_be_in_fw_bw: bool = False, + omit_aot_autograd_runtime: bool = False, +) -> tuple[fx.GraphModule, fx.GraphModule]: + """Extract forward and backward graph modules from a joint graph. + + Args: + ignore_must_be_in_fw_bw: When True, disables forward/backward placement + enforcement in _extract_graph_with_inputs_outputs. Needed when the + joint_module is not an original fwd+bwd joint graph (e.g. a backward + graph being re-partitioned for dI/dW splitting). + omit_aot_autograd_runtime: When True, skips postprocessing that is + only needed when the resulting modules will be wrapped in a custom + autograd.Function (the AOTAutograd path). This includes: tangent input + handling, version-counter check sorting of saved tensors, opaque object + (FakeScriptObject) separation, and fp8 activation quantization. Set this + to True when the fwd/bwd modules will be executed directly without autograd. + """ + fwd_outputs, bwd_outputs, fwd_outputs_descs, bwd_outputs_descs = ( + _extract_fwd_bwd_outputs(joint_module, num_fwd_outputs=num_fwd_outputs) + ) + placeholders = joint_module.graph.find_nodes(op="placeholder") + primal_inputs = [*filter(_is_primal, placeholders)] + tangent_inputs = ( + [] if omit_aot_autograd_runtime else [*filter(_is_tangent, placeholders)] + ) + fwd_seed_offset_inputs = [*filter(_is_fwd_seed_offset, placeholders)] + bwd_seed_offset_inputs = [*filter(_is_bwd_seed_offset, placeholders)] + backward_state_inputs = [*filter(_is_backward_state, placeholders)] + + if saved_opaque_nodes is None: + saved_opaque_nodes = [] + + bwd_graph = _extract_graph_with_inputs_outputs( + joint_module.graph, + saved_sym_nodes + + saved_opaque_nodes + + saved_values + + tangent_inputs + + bwd_seed_offset_inputs, + bwd_outputs, + bwd_outputs_descs, + "backward", + ignore_must_be_in_fw_bw=ignore_must_be_in_fw_bw, + ) + + distributed_enabled = torch.distributed.is_available() + + for node in bwd_graph.find_nodes(op="placeholder"): + # This is to filter out saved values that don't actually end up being used by the backwards pass + if not node.users: + _remove_by_name(saved_values, node.name) + _remove_by_name(saved_sym_nodes, node.name) + _remove_by_name(saved_opaque_nodes, node.name) + # wait_tensor is a bit special: if we have a "dead activation" that is not used in the bw, + # but this dead activation is actually a collective, + # then the collective will generally by followed by a wait_tensor() call. + # we need to peak one node further to see if this wait_tensor is dead as well. + elif distributed_enabled and all( + n.target is torch.ops._c10d_functional.wait_tensor.default + and len(n.users) == 0 + for n in node.users + ): + _remove_by_name(saved_values, node.name) + _remove_by_name(saved_sym_nodes, node.name) + _remove_by_name(saved_opaque_nodes, node.name) + elif _is_backward_state(node): + # BackwardState is saved directly + _remove_by_name(saved_values, node.name) + if not backward_state_inputs: + raise AssertionError("backward_state_inputs must not be empty") + + # Now that we have the finalized list of saved values, we need to ensure + # we propagate all symbols which are referenced by backwards inputs. + # These are not directly used in the graph but are required for downstream + # sizevar assignment + saved_symbols: OrderedSet[sympy.Symbol] = OrderedSet() + saved_sym_nodes_binding = [] + saved_sym_nodes_derived = [] + + # Some symbols may already be bound in the directly saved_sym_nodes, + # keep track of them so we don't re-bind them + for node in saved_sym_nodes: + symbol = is_symbol_binding_fx_node(node) + if symbol: + saved_symbols.add(symbol) + saved_sym_nodes_binding.append(node) + else: + saved_sym_nodes_derived.append(node) + + # Now go through all of the prospective backward inputs and track any + # other symbols we need to bind + symbol_bindings = find_symbol_binding_fx_nodes(joint_module.graph) + for node in itertools.chain(saved_sym_nodes_derived, saved_values, tangent_inputs): + if "val" not in node.meta: + continue + new_symbols = free_symbols(node.meta["val"]) - saved_symbols + # NB: Deterministic order please! + for s in sorted(new_symbols, key=lambda s: s.name): + # NB: For well formed graphs, the symbol should always be present, + # but we also have ways to produce ill-formed graphs, e.g., direct + # make_fx usages, so don't choke in this case + if s not in symbol_bindings: + continue + saved_sym_nodes_binding.append(symbol_bindings[s]) + saved_symbols |= new_symbols + + # Update saved_sym_nodes that are now reordered to have all bindings at + # front. This can also be used later on to figure out the position of saved + # sym nodes in the output of fwd graph. + saved_sym_nodes.clear() + saved_sym_nodes.extend(saved_sym_nodes_binding + saved_sym_nodes_derived) + + if not omit_aot_autograd_runtime: + # See Note [Activations with no version counter checks in eager] + # Sort saved_values so that tensors with saved_tensor_with_no_vc_check=True + # are at the end. This allows us to have two consecutive slices: + # 1. tensors_saved_with_vc_check_slice - tensors saved via save_for_backward + # 2. tensors_saved_with_no_vc_check_slice - tensors stashed on ctx without save_for_backward + # The sort is stable, so the relative order within each group is preserved. + # + # Additionally, separate out opaque objects (FakeScriptObject) from tensors. + # Opaque objects should be placed after tensors in the forward outputs. + saved_values_with_vc_check = [] + saved_values_no_vc_check = [] + saved_opaque_objects = [] + for node in saved_values: + # Check if this is an opaque object + if isinstance(node.meta.get("val"), FakeScriptObject): + saved_opaque_objects.append(node) + elif node.meta.get("saved_tensor_with_no_vc_check", False): + saved_values_no_vc_check.append(node) + else: + saved_values_with_vc_check.append(node) + saved_values.clear() + saved_values.extend(saved_values_with_vc_check + saved_values_no_vc_check) + no_vc_check_start_idx = len(saved_values_with_vc_check) + + # debug assert: given saved_values where the last k of them are expected to not + # require VC checks, they should all have node metadata indicating so. + for i, node in enumerate(saved_values): + if i >= no_vc_check_start_idx: + if not node.meta.get("saved_tensor_with_no_vc_check", False): + raise AssertionError( + f"i={i}, no_vc_check_start_idx={no_vc_check_start_idx}, len(saved_values)={len(saved_values)}" + ) + + # Now, we re-generate the fwd/bwd graphs. + # NB: This might increase compilation time, but I doubt it matters + # Convention for saved acts is (tensors_with_vc_check, tensors_no_vc_check, opaque_objects, symints, opaque_nodes) + fwd_graph = _extract_graph_with_inputs_outputs( + joint_module.graph, + primal_inputs + fwd_seed_offset_inputs, + fwd_outputs + + saved_values + + saved_opaque_objects + + saved_opaque_nodes + + saved_sym_nodes, + fwd_outputs_descs + + [ + SavedForBackwardsNoVcCheckAOTOutput(i) + if i >= no_vc_check_start_idx and i < len(saved_values) + else SavedForBackwardsAOTOutput(i) + for i in range( + len(saved_values) + + len(saved_opaque_objects) + + len(saved_opaque_nodes) + + len(saved_sym_nodes) + ) + ], + "forward", + ignore_must_be_in_fw_bw=ignore_must_be_in_fw_bw, + ) + bwd_graph = _extract_graph_with_inputs_outputs( + joint_module.graph, + saved_sym_nodes + + saved_values + + saved_opaque_objects + + saved_opaque_nodes + + tangent_inputs + + bwd_seed_offset_inputs + + backward_state_inputs, + bwd_outputs, + bwd_outputs_descs, + "backward", + ignore_must_be_in_fw_bw=ignore_must_be_in_fw_bw, + ) + else: + # Raw fwd/bwd split for direct execution without autograd + fwd_graph = _extract_graph_with_inputs_outputs( + joint_module.graph, + primal_inputs + fwd_seed_offset_inputs, + fwd_outputs + saved_values + saved_sym_nodes, + fwd_outputs_descs + + [ + SavedForBackwardsAOTOutput(i) + for i in range(len(saved_values) + len(saved_sym_nodes)) + ], + "forward", + ignore_must_be_in_fw_bw=ignore_must_be_in_fw_bw, + ) + bwd_graph = _extract_graph_with_inputs_outputs( + joint_module.graph, + saved_values + + saved_sym_nodes + + bwd_seed_offset_inputs + + backward_state_inputs, + bwd_outputs, + bwd_outputs_descs, + "backward", + ignore_must_be_in_fw_bw=ignore_must_be_in_fw_bw, + ) + + fwd_module = fx._lazy_graph_module._make_graph_module(joint_module, fwd_graph) + bwd_module = fx._lazy_graph_module._make_graph_module(joint_module, bwd_graph) + if ( + inductor_config.post_grad_fusion_options.get( + "activation_quantization_aten_pass", None + ) + is not None + ): + enable_activation_quantization( + saved_values, + fwd_module, + bwd_module, + static_lifetime_input_nodes, + num_fwd_outputs, + ) + return fwd_module, bwd_module + + +def default_partition( + joint_module: fx.GraphModule, + _joint_inputs: Any, + *, + num_fwd_outputs: int, + static_lifetime_input_indices: list[int] | None = None, + static_lifetime_input_nodes: OrderedSet[fx.Node] | None = None, +) -> tuple[fx.GraphModule, fx.GraphModule]: + """ + Partitions the :attr:`joint_module` in a manner that closely resembles the + behavior observed in the original ``.forward()`` and ``.backward()`` of the + callable, i.e., the resulting forward graph contains those operators that + are executed in the original ``.forward()`` callable passed to + :func:`aot_function`. + + The default partitioner collects the operators that are between the forward + inputs and the forward outputs. This helps in finding the tensors which have + to be stashed for the backward pass. These stashed tensors become the output + of the generated forward graph. The remaining operators are then placed in + the backward graph. + + .. warning:: + This API is experimental and likely to change. + + Args: + joint_module(fx.GraphModule): The joint forward and backward graph. This + is the result of AOT Autograd tracing. + + Returns: + Returns the generated forward and backward Fx graph modules. + """ + # Respect the original placement of ops rather than rely on dataflow. + forward_nodes = [] + last_node = None + for node in joint_module.graph.nodes: + if _has_tag_is_forward(node) or _is_primal(node) or _is_fwd_seed_offset(node): + last_node = node + if last_node is None: + raise AssertionError("last_node must not be None") + for node in joint_module.graph.nodes: + if not _is_tangent(node): + forward_nodes.append(node) + if node is last_node: + break + forward_node_names = OrderedSet( + node.name for node in forward_nodes if node.op != "output" + ) + graph_has_recomputable_ops = has_recomputable_ops(joint_module) + graph_has_recomputable_rng_ops = has_recomputable_rng_ops(joint_module) + if graph_has_recomputable_ops: + if _is_functional_graph(joint_module.graph)[0] is not None: + # Fall-back to previous behavior to avoid bc-breaking, although can + # eventually flip the switch to make this a hard error. + warnings.warn( + "Trying to unsafely apply AC to a non-functional graph with the " + "default partitioner. Falling back to min-cut partitioner." + ) + return min_cut_rematerialization_partition( + joint_module, + _joint_inputs, + num_fwd_outputs=num_fwd_outputs, + static_lifetime_input_indices=static_lifetime_input_indices, + ) + + joint_module = cleanup_recompute_tags(joint_module, is_default_partition=True) + + if not config.unsafe_allow_optimization_of_collectives: + force_save_collectives(joint_module) + + force_save_effectful_ops(joint_module) + force_save_bw_mutation_src(joint_module) + + if static_lifetime_input_indices is None: + static_lifetime_input_indices = [] + node_info = classify_nodes( + joint_module, static_lifetime_input_indices, num_fwd_outputs + ) + + saved_values = [] + saved_sym_nodes = [] + saved_opaque_nodes = [] + + distributed_enabled = torch.distributed.is_available() + + def is_tensor(node: fx.Node) -> bool: + return "tensor_meta" in node.meta or isinstance( + node.meta.get("val"), torch._subclasses.FakeTensor + ) + + def is_multi_output(node: fx.Node) -> bool: + return ( + all(user.target == operator.getitem for user in node.users) + and len(node.users) > 0 + ) + + def is_impure(node: fx.Node) -> bool: + # wait tensor is an "impure" op according to DCE's definition of impure + # (see is_impure in torch/fx/node.py), but it survives past + # functionalization and can be safely dup'd and reordered under the + # assumption SPMD. + return ( + node.is_impure(impure_random=False) + and node.op + not in ( + "placeholder", + "output", + ) + and ( + not distributed_enabled + or node.target is not torch.ops._c10d_functional.wait_tensor.default + ) + ) + + for node in joint_module.graph.nodes: + if node.name not in forward_node_names: + continue + if node.op == "get_attr" and node.name in ( + k for k, v in joint_module.named_modules() + ): + continue + if node.target in ( + torch.ops.aten._assert_scalar.default, + # Profiler record_function ops are technically impure (they set up + # profiling spans), but they're safe to duplicate during AC recompute. + # We skip both enter and exit to keep profiling spans balanced. + torch.ops.profiler._record_function_enter_new.default, + torch.ops.profiler._record_function_enter.default, + torch.ops.profiler._record_function_exit.default, + torch.ops.profiler._record_function_exit._RecordFunction, + ): + continue + if is_sym_node(node): + # Symints must be kept separate from tensors so that PythonFunction only calls + # save_for_backward on tensors and stashes symints in autograd .ctx + saved_sym_nodes.append(node) + continue + if is_multi_output(node): + # Must be ordered before MUST_SAVE tags to avoid saving tuples marked MUST_SAVE. + continue + if node.meta.get("recompute") == CheckpointPolicy.MUST_SAVE: + if is_opaque_node(node): + saved_opaque_nodes.append(node) + else: + saved_values.append(node) + continue + if is_impure(node): + if graph_has_recomputable_ops: + raise AssertionError( + f"Trying to apply AC on a graph with impure op: {node}, {node.target}" + ) + if is_opaque_node(node): + saved_opaque_nodes.append(node) + else: + saved_values.append(node) + continue + if is_opaque_node(node): + saved_opaque_nodes.append(node) + continue + if not is_tensor(node) and node.op == "call_function": + raise AssertionError(f"Expected {node} to be a tensor") + backward_usages = [n for n in node.users if n.name not in forward_node_names] + if all(is_sym_node(n) for n in backward_usages): + # If we have a tensor in the forward, where only its sizes/strides are needed in the backward, + # and not the actual tensor data, + # then it will be a lot cheaper to save only the sizes/strides, and not the actual tensor. + # + # Note that saving the tensor could also cause compilation problems: + # If the user mutated an input in the forward and uses its sizes/strides in the backward, + # then we would be obligated to clone the input before saving it to appease autograd. + # (This is how we originally found this bug). + saved_sym_nodes.extend(backward_usages) + continue + if not must_recompute(node): + saved_values.append(node) + + saved_values = list(dict.fromkeys(saved_values).keys()) + saved_sym_nodes = list(dict.fromkeys(saved_sym_nodes).keys()) + saved_opaque_nodes = list(dict.fromkeys(saved_opaque_nodes).keys()) + + if config._sync_decision_cross_ranks: + saved_values = _sync_decision_cross_ranks(joint_module.graph, saved_values) + + if static_lifetime_input_nodes is None: + static_lifetime_input_nodes = node_info.static_lifetime_input_nodes + fw_module, bw_module = _extract_fwd_bwd_modules( + joint_module, + saved_values, + saved_sym_nodes=saved_sym_nodes, + saved_opaque_nodes=saved_opaque_nodes, + num_fwd_outputs=num_fwd_outputs, + static_lifetime_input_nodes=static_lifetime_input_nodes, + ) + + # Run DCE while overriding the definition of is_impure_node + fw_module.graph.eliminate_dead_code(is_impure_node=is_not_collective) + bw_module.graph.eliminate_dead_code(is_impure_node=is_not_collective) + + if graph_has_recomputable_ops: + if graph_has_recomputable_rng_ops: + fw_module, bw_module = functionalize_rng_ops( + joint_module, fw_module, bw_module, len(saved_sym_nodes) + ) + bw_module = reordering_to_mimic_autograd_engine(bw_module) + + # pyrefly: ignore [unbound-name] + if config.enable_activation_offloading: + from ._activation_offloading.activation_offloading import ( + enable_activation_offloading, + ) + + enable_activation_offloading( + fw_module, + bw_module, + num_fwd_outputs, + static_lifetime_input_nodes, + ) + + # raise all getitem ops to as early as possible + # this is helpful for memory, especially in the case of aot_eager backend + fw_module = raise_getitems(fw_module) + bw_module = raise_getitems(bw_module) + + fw_module = thread_graphsafe_rng_from_hops(fw_module, is_backward=False) + if len(node_info.required_bw_nodes) > 0: + bw_module = thread_graphsafe_rng_from_hops(bw_module, is_backward=True) + + return fw_module, bw_module + + +INT_INF = int(1e6) + + +def _tensor_nbytes(numel: int, dtype: torch.dtype) -> int: + return numel * dtype.itemsize + + +def _size_of(node: fx.Node) -> int: + def object_nbytes(x: object) -> int: + if not isinstance(x, torch.Tensor): + return 0 + return _tensor_nbytes(optimization_hint(x.numel(), fallback=4096), x.dtype) + + if "val" in node.meta: + val = node.meta["val"] + if isinstance(val, py_sym_types): + return 1 + # NB: The fallback values here are meaningless, maybe we should respect + # torch._inductor.config.unbacked_symint_fallback (but this is a + # layering violation) + elif isinstance(val, (list, tuple)): + return sum(object_nbytes(n) for n in val) + elif isinstance(val, dict): + return sum(object_nbytes(n) for _, n in val.items()) + elif isinstance(val, torch.Tensor): + return object_nbytes(val) + + raise RuntimeError(f"Unknown metadata type {type(val)} on node {node}") + if node.op == "get_attr" or node.target is torch.ops.aten._assert_scalar.default: + return 0 + raise RuntimeError( + f"Node {node} didn't have `val` metadata; we should always have `val` metadata on the nodes." + ) + + +# Used for some investigative purposes +def _count_ops(graph: fx.Graph) -> None: + from collections import defaultdict + + cnt: dict[str, int] = defaultdict(int) + for node in graph.nodes: + if node.op == "call_function": + cnt[node.target.__name__] += 1 + log.info("%s", sorted(cnt.items(), key=operator.itemgetter(1), reverse=True)) + + +@functools.cache +def pointwise_ops() -> list[torch._ops.OpOverloadPacket]: + ops: list[torch._ops.OpOverloadPacket] = [] + for attr_name in dir(torch.ops.aten): + opoverloadpacket = getattr(torch.ops.aten, attr_name) + if not isinstance(opoverloadpacket, torch._ops.OpOverloadPacket): + continue + + for overload in opoverloadpacket.overloads(): + op_overload = getattr(opoverloadpacket, overload) + if torch.Tag.pointwise in op_overload.tags: + # currently aot autograd uses packet not overload + ops.append(opoverloadpacket) + break + + return ops + + +def sort_depths( + args: tuple[Any, ...], depth_map: dict[fx.Node, int] +) -> list[tuple[fx.Node, int]]: + arg_depths = { + arg: depth_map[arg] for arg in args if isinstance(arg, torch.fx.node.Node) + } + return sorted(arg_depths.items(), key=operator.itemgetter(1), reverse=True) + + +def reordering_to_mimic_autograd_engine(gm: fx.GraphModule) -> fx.GraphModule: + """ + This pass finds the first bwd node in the graph (by looking at users of + tangents) and then reorders the graph by walking from this node to all the + way to the end of the graph. At each op in this traversal, we insert this op + in a new graph and try to bring only the relevant subgraph from the other + non-bwd edges relevant for this op. This closely mimics the behavior of + autograd engine. + + Why is this pass required in the first place? + + This is an artifact of how partitioners work today. The starting point of + partitioner is a joint graph, which is fwd and then bwd graph. In the case + of checkpointing, we keep portions of fwd graph in their original place in + the joint graph, while obtaining a bwd graph. As a result, the resulting bwd + graph has copies of recomputed fwd subgraphs followed by the original bwd + graph. If we run this naively, this leads to bad memory footprint, because + the fwd subgraphs are live for way longer duration than necessary. This pass + reorders the operations such that we prioritize the ops for the original bwd + graph while only realizing those ops from the fwd graph that are necessary + at any given point in the graph. + """ + + new_graph = fx.Graph() + env: dict[fx.Node, fx.Node] = {} + + # Add new placeholder nodes in the order specified by the inputs + for node in gm.graph.find_nodes(op="placeholder"): + env[node] = new_graph.node_copy(node, lambda x: env[x]) + + order = {node: idx for idx, node in enumerate(gm.graph.nodes)} + + def insert_node_in_graph(node: fx.Node) -> None: + cur_nodes = [node] + insertable_nodes: OrderedSet[fx.Node] = OrderedSet() + while len(cur_nodes) > 0: + node = cur_nodes.pop() + if node in insertable_nodes or node in env: + continue + insertable_nodes.add(node) + + # Bias traversal towards the nodes that have higher depth - prioritizes + # critical path first. + cur_nodes += node.all_input_nodes + + # pyrefly: ignore [bad-assignment] + insertable_nodes = sorted(insertable_nodes, key=lambda n: order[n]) + for node in insertable_nodes: + env[node] = new_graph.node_copy(node, lambda x: env[x]) + + # Find first bwd node in the graph + tangent_inputs = list(filter(_is_tangent, gm.graph.nodes)) + first_node_in_bwd = None + minimum_order = math.inf + for tangent in tangent_inputs: + for user in tangent.users: + if order[user] < minimum_order: + minimum_order = order[user] + first_node_in_bwd = user + + # If gradInp does not depend upon gradOut, we may not find any nodes in the "backwards pass" + if first_node_in_bwd is None: + return gm + + # Build the graph op-by-op by starting from the node all the way to the end + # copy_ can be not using tangents at all, we must copy it. + for node in list(gm.graph.nodes)[: order[first_node_in_bwd]]: + if node.op == "call_function" and node.target is torch.ops.aten.copy_.default: + insert_node_in_graph(node) + + for node in list(gm.graph.nodes)[order[first_node_in_bwd] :]: + insert_node_in_graph(node) + + # The output node is already built by the traversal. + new_gm = torch.fx.GraphModule(gm, new_graph) + return new_gm + + +def apply_graphsafe_rng_functionalization( + fw_module: torch.fx.GraphModule, + bw_module: torch.fx.GraphModule, + fw_node: torch.fx.Node, + bw_node: torch.fx.Node, + device: torch.device, + rng_count: int, + last_fwd_input: torch.fx.Node, + last_bwd_input: torch.fx.Node, +) -> tuple[torch.fx.Node, torch.fx.Node]: + """ + Note [CUDA Graph Safe RNG Functionalization] + + CUDA Graph capture doesn't work with get_rng_state and set_rng_state because these functions operate on CPU values, + while CUDA Graph RNG capture uses on-device CUDA tensors. To solve this, we use graphsafe_set_state with a + CUDA Generator registered to the CUDA Graph before capture begins. graphsafe_set_state updates the generator's pointer + to reference a different GeneratorImpl, ensuring subsequent calls are correctly forwarded to the desired generator + (and its cuda-tensor RNG state during graph capture). + + For each RNG operation's forward/backward pair: + + - We create two generators initialized with identical values + - Each forward and backward call advances its respective generator equally + - This keeps generators synchronized so forward and backward operations use matching RNG values + + When forward is called multiple times before backward (causing desynchronization): + + - We save the forward RNG state + - We update the backward Generator's state before executing backward + + Before each CUDA Graph replay, replay_prologue updates captured RNG pointers with current states, ensuring backward Generator + changes are reflected during replay. + + This function modifies both forward and backward computation graphs by: + + Creating RNG state placeholders for both passes + Updating the forward node to use graph-safe RNG state + Updating the backward node to use graph-safe RNG state + + For more details: https://github.com/pytorch/pytorch/issues/113541 + """ + device_idx = device.index + if device_idx is None: + raise AssertionError("device_idx must not be None") + fw_graph = fw_module.graph + bw_graph = bw_module.graph + graphsafe_run_with_rng_state = torch._prims.rng_prims.graphsafe_run_with_rng_state + + # Handle forward pass + + # Note: [Generator arguments in AOTDispatcher] + # Generator arguments in AOTDispatcher are added to support graphsafe rng + # functionalization. See note above [CUDA Graph Safe RNG Functionalization] + with fw_module.graph.inserting_after(last_fwd_input): + fwd_rng_state = fw_module.graph.placeholder(f"fwd_rng_state_{rng_count}") + fwd_rng_state.meta["val"] = get_cuda_generator_meta_val(device_idx) + last_fwd_input = fwd_rng_state + + # Handle backward pass + with bw_module.graph.inserting_after(last_bwd_input): + bwd_rng_state = bw_module.graph.placeholder(f"bwd_rng_state_{rng_count}") + # as above, clone so that meta val generator will not contain tensors + bwd_rng_state.meta["val"] = get_cuda_generator_meta_val(device_idx) + last_bwd_input = bwd_rng_state + + # Update forward node + fw_kwargs = dict(fw_node.kwargs) + fw_kwargs["rng_state"] = fwd_rng_state + with fw_module.graph.inserting_after(fw_node): + functional_fw_node = fw_graph.create_node( + "call_function", + graphsafe_run_with_rng_state, + args=(fw_node.target, *fw_node.args), # type: ignore[arg-type] + kwargs=fw_kwargs, + ) + fw_node.replace_all_uses_with(functional_fw_node) + fw_graph.erase_node(fw_node) + + # Update backward node + bwd_kwargs = dict(bw_node.kwargs) + bwd_kwargs["rng_state"] = bwd_rng_state + with bw_graph.inserting_before(bw_node): + rng_output = bw_graph.create_node( + "call_function", + graphsafe_run_with_rng_state, + args=(bw_node.target, *bw_node.args), # type: ignore[arg-type] + kwargs=bwd_kwargs, + ) + bw_node.replace_all_uses_with(rng_output) + bw_graph.erase_node(bw_node) + + return last_fwd_input, last_bwd_input + + +def functionalize_rng_ops( + joint_module: fx.GraphModule, + fw_module: fx.GraphModule, + bw_module: fx.GraphModule, + num_sym_nodes: int, +) -> tuple[fx.GraphModule, fx.GraphModule]: + # During user-driven activation checkpointing, we have to ensure that a rng + # op in fwd yields the same output as the recomputed rng op in the bwd. To + # do this, we use functionalize wrappers to wrap the random ops and share + # rng state between the fwd and bwd graphs. + + # There are 3 main steps to do this + # Step 1 - Construct a mapping of rng node between the fwd and its counterpart in bwd. + # Step 2 - Modify the fwd pass such that + # 1) Replace rand with run_and_save_rng_state wrapper + # 2) Replace the users of the original op with the output[1] of this op. + # 3) Collect all the rng_state - output[0] of each op, and make them + # output nodes. Special care needs to be taken here because fwd outputs + # has symints at the very end. + # Step 3 - Modify the bwd pass such that + # 1) Add the input nodes just before the tangents for the stashed rng states + # 2) Replace rand with run_with_save_rng_state wrappers + # 3) Use the stashed states as inputs to these ops + + # Unique id to generate name + uid = itertools.count() + + def get_rng_ops(gmod: fx.GraphModule) -> dict[str, fx.Node]: + random_nodes: dict[str, fx.Node] = {} + for node in gmod.graph.nodes: + if ( + node.op == "call_function" + and hasattr(node.target, "tags") + and torch.Tag.nondeterministic_seeded in node.target.tags + ): + random_nodes[node.name] = node + return random_nodes + + def get_device(node: fx.Node) -> torch.device | None: + """ + Check the example value of the node outputs to find the device type. + """ + if "val" not in node.meta: + return None + + candidates = node.meta["val"] + if not isinstance(candidates, tuple): + candidates = (candidates,) + + for candidate in candidates: + if isinstance(candidate, torch.Tensor): + if candidate.device.type == "cuda": + return candidate.device + + return torch.device("cpu") + + def get_sample_rng_state(device: torch.device | None) -> torch.Tensor: + from torch._guards import detect_fake_mode # noqa: F401 + + fake_mode = detect_fake_mode() + if fake_mode is None: + raise AssertionError("fake_mode must not be None") + with fake_mode: + if device is not None and device.type == "cuda": + return fake_mode.from_tensor(torch.cuda.get_rng_state()) + return fake_mode.from_tensor(torch.get_rng_state()) + + # Step 1 - Construct a mapping of rng node between the fwd and its counterpart in bwd. + joint_graph_rng_ops = get_rng_ops(joint_module) + fw_graph_rng_ops = get_rng_ops(fw_module) + bw_graph_rng_ops = get_rng_ops(bw_module) + recomputable_rng_ops_map = {} + for node in joint_module.graph.nodes: + if ( + must_recompute(node) + and hasattr(node.target, "tags") + and torch.Tag.nondeterministic_seeded in node.target.tags + ): + # Skip if the node doesn't exist in both forward and backward graphs. + # This can happen when the RNG op's output is not needed for gradient + # computation and gets eliminated by dead code elimination. + if node.name not in fw_graph_rng_ops or node.name not in bw_graph_rng_ops: + continue + base_node = joint_graph_rng_ops[node.name] + fw_node = fw_graph_rng_ops[node.name] + bw_node = bw_graph_rng_ops[node.name] + recomputable_rng_ops_map[base_node] = {"fwd": fw_node, "bwd": bw_node} + + run_and_save_rng = torch._prims.rng_prims.run_and_save_rng_state + run_with_rng_state = torch._prims.rng_prims.run_with_rng_state + + bw_tangent_start_node = None + for node in bw_module.graph.find_nodes(op="placeholder"): + if "tangent" in node.name: + bw_tangent_start_node = node + break + if bw_tangent_start_node is None: + raise RuntimeError( + "Couldn't find tangent node in graph inputs. This is unexpected, please file a bug if you see this" + ) + + fw_rng_state_outputs: list[fx.Node] = [] + + last_fwd_input = next(reversed(fw_module.graph.find_nodes(op="placeholder"))) + last_bwd_input = next(reversed(bw_module.graph.find_nodes(op="placeholder"))) + + devices = OrderedSet( + get_device(node_pair["fwd"]) for node_pair in recomputable_rng_ops_map.values() + ) + # pyrefly: ignore [unbound-name] + devices.discard(torch.device("cpu")) + # multiple cuda devices won't work with cudagraphs anyway, + # fallback to non graphsafe rng checkpointing + multi_cuda_devices = len(devices) > 1 + + # this changes numerics, so if fallback_random is set we will not use it + # pyrefly: ignore [unbound-name] + ind_config = torch._inductor.config + use_rng_graphsafe_rng_functionalization = ( + config.graphsafe_rng_functionalization + and not multi_cuda_devices + and ( + not ind_config.fallback_random + or ind_config.test_configs.graphsafe_rng_func_ignores_fallback_random + ) + ) + + for rng_count, node_pair in enumerate(recomputable_rng_ops_map.values()): + # Step 2 - Modify the fwd pass such that + fw_node = node_pair["fwd"] + bw_node = node_pair["bwd"] + device = get_device(fw_node) + + fw_graph = fw_module.graph + bw_graph = bw_module.graph + + if ( + use_rng_graphsafe_rng_functionalization + and device is not None + and device.type == "cuda" + ): + last_fwd_input, last_bwd_input = apply_graphsafe_rng_functionalization( + fw_module, + bw_module, + fw_node, + bw_node, + device, + rng_count, + last_fwd_input, + last_bwd_input, + ) + else: + with fw_graph.inserting_before(fw_node): + functional_fw_node = fw_graph.create_node( + "call_function", + run_and_save_rng, + # pyrefly: ignore [bad-argument-type] + args=( + fw_node.target, + *fw_node.args, + ), # pyrefly: ignore[bad-argument-type] + kwargs=fw_node.kwargs, + ) + state = fw_graph.create_node( + "call_function", + operator.getitem, + args=(functional_fw_node, 0), + kwargs={}, + ) + state.meta["val"] = get_sample_rng_state(device) + + rng_output = fw_graph.create_node( + "call_function", + operator.getitem, + args=( + functional_fw_node, + 1, + ), + kwargs={}, + ) + # Copy the meta data from the original node + rng_output.meta = copy.copy(fw_node.meta) + + fw_node.replace_all_uses_with(rng_output) + fw_graph.erase_node(fw_node) + fw_rng_state_outputs.append(state) + + # Step 3 - Modify the bwd pass such that + with bw_graph.inserting_before(bw_tangent_start_node): + state_name = f"rng_state_output_{next(uid)}" + bw_rng_state_node = bw_graph.placeholder(state_name) + bw_rng_state_node.meta["val"] = get_sample_rng_state(device) + + with bw_graph.inserting_before(bw_node): + rng_output = bw_graph.create_node( + "call_function", + run_with_rng_state, + # pyrefly: ignore [bad-argument-type] + args=( + bw_rng_state_node, + bw_node.target, + *bw_node.args, + ), # pyrefly: ignore[bad-argument-type] + kwargs=bw_node.kwargs, + ) + + bw_node.replace_all_uses_with(rng_output) + bw_graph.erase_node(bw_node) + + # Add the rng states in the output of the fwd graph. AOT Autograd assumes + # that symints are at the end of forward graph outputs. So, insert the new + # rng states accordingly. + if fw_rng_state_outputs: + fw_output_node = next(iter(fw_module.graph.find_nodes(op="output"))) + fw_outputs = fw_output_node.args[0] + sym_node_start_idx = len(fw_outputs) - num_sym_nodes + outputs = ( + fw_outputs[:sym_node_start_idx] + + tuple(fw_rng_state_outputs) + + fw_outputs[sym_node_start_idx:] + ) + fw_module.graph.output(outputs) + fw_module.graph.erase_node(fw_output_node) + fw_module.recompile() + bw_module.recompile() + return fw_module, bw_module + + +def force_save_collectives(joint_module: fx.GraphModule) -> None: + """ + By default, the partitioner is not allowed to recompute collectives + unless they come from a user-annotated AC region. + See Note [Recomputing collectives in the partitioner] + """ + for node in joint_module.graph.nodes: + if ( + isinstance(node.target, torch._ops.OpOverload) + and node.target.namespace == "_c10d_functional" + and not must_recompute(node) + ): + node.meta["recompute"] = CheckpointPolicy.MUST_SAVE + + +def force_save_effectful_ops(joint_module: fx.GraphModule) -> None: + """ + Force save outputs from with_effects nodes wrapping effectful ops. + + Effectful ops (registered via _register_effectful_op) should not be recomputed + because they may have arbitrary global side effects (I/O, RNG state, collectives, + etc.). We mark the tensor outputs of with_effects as MUST_SAVE to prevent + recomputation of the effectful op. + + The with_effects node returns a tuple (token, result). We recursively find all + leaf outputs extracted via getitem and mark them as MUST_SAVE. Since these are + saved, the with_effects op doesn't need to be recomputed in backward. + """ + + def mark_getitem_outputs(node: fx.Node) -> None: + for user in node.users: + if user.target is operator.getitem: + mark_getitem_outputs(user) + if not isinstance(user.meta.get("val"), (tuple, list)): + user.meta["recompute"] = CheckpointPolicy.MUST_SAVE + + for node in joint_module.graph.nodes: + if ( + is_with_effects(node) + and not must_recompute(node) + and not _has_tag_is_backward(node) + ): + mark_getitem_outputs(node) + + +def force_save_bw_mutation_src(joint_module: fx.GraphModule) -> None: + # If we have mutations of the same primal in forward and backward, + # We must not recompute the source of mutation to not apply twice. + has_mutation_in_bw: OrderedSet[torch.fx.Node] = OrderedSet() + for node in reversed(joint_module.graph.nodes): + if node.op == "output": + continue + + is_copy_ = node.target is torch.ops.aten.copy_.default + if is_copy_: + if _has_tag_must_be_in_backward(node): + has_mutation_in_bw.add(node.args[0]) + + if _has_tag_must_be_in_forward(node) and node.args[0] in has_mutation_in_bw: + node.args[1].meta["recompute"] = CheckpointPolicy.MUST_SAVE + else: + # We use invariant of aotdispatch joint graph, + # That we emit copy_ only in the end of it. + # We do not want to iterate through all the joint graph, + # so break at the first non-output, non-copy_ node. + break + + +def is_getitem_of_multi_output(node: fx.Node) -> bool: + if node.target != operator.getitem: + return False + parent = node.args[0] + if type(parent) is not fx.Node: + raise AssertionError(f"expected parent to be fx.Node, got {type(parent)}") + return "tensor_meta" not in parent.meta and node.op == "call_function" + + +def cleanup_recompute_tags( + joint_module: fx.GraphModule, *, is_default_partition: bool +) -> fx.GraphModule: + """ + If there are two consecutive checkpointed blocks with no operator in + between, we would still want to stash the tensor at the boundary of + checkpointed blocks. The following pass makes the last output node + non-recomputable to allow for that. + """ + for node in joint_module.graph.nodes: + if must_recompute(node): + for user in node.users: + if ( + must_recompute(user) + and "ac_graph_id" in user.meta + and "ac_graph_id" in node.meta + and user.meta["ac_graph_id"] > node.meta["ac_graph_id"] + ): + node.meta["recompute"] = CheckpointPolicy.MUST_SAVE + if node.meta.get("has_backward_hook", False) and not any( + must_recompute(user) for user in node.users + ): + # If node is AC region output and has a backward hook on it, we intentionally choose to save it. + # This is to work around circular dependencies in Traceable FSDP2+AC. + # Example: + # ``` + # out = fully_shard(utils.checkpoint(module))(x) + # norm_out = layer_norm(out) + # ``` + # Here there is a circular dependency: + # 1. In backward, grad_input of layer_norm aka. `out_grad` is actually dependent on `out`. + # 2. `out` depends on `out`'s backward hook created by FSDP2 (which does all-gather for `module` weights) + # in order to be recomputed. + # 3. `out`'s backward hook, as is the case for all eager backward hooks, depends on `out_grad` + # -> circular dependency with (1)! + # + # Solution: check whether `out` has a backward hook, and if so, intentionally save `out` + # in forward graph outputs. With this, we can break the above circular dependency. + node.meta["recompute"] = CheckpointPolicy.MUST_SAVE + elif ( + "ac_graph_id" not in node.meta + and any(must_recompute(user) for user in node.users) + and not ( + # Avoid saving getitem nodes which are not labeled with "ac_graph_id" + is_getitem_of_multi_output(node) and "ac_graph_id" in node.args[0].meta + ) + and is_default_partition + ): + # This node is not part of the AC region and a user is marked as recompute. + # This means it's an input to the AC region and we should save it. + # For ease of landing, gate this to default partitioner only, but we should think + # about flipping the switch in general as well. + node.meta["recompute"] = CheckpointPolicy.MUST_SAVE + return joint_module + + +def solve_min_cut( + joint_graph: fx.Graph, + node_info: NodeInfo, + min_cut_options: MinCutOptions, + dont_ban: OrderedSet[fx.Node] | None = None, +) -> tuple[list[fx.Node], OrderedSet[fx.Node]]: + if dont_ban is None: + dont_ban = OrderedSet() + op_types = get_default_op_list() + + if AOT_PARTITIONER_DEBUG: + joint_module_ops = OrderedSet( + str(node.target._overloadpacket) + for node in joint_graph.nodes + if node.op == "call_function" and hasattr(node.target, "_overloadpacket") + ) + ops_ignored = joint_module_ops - OrderedSet( + str(i) for i in op_types.recomputable_ops + ) + log.info("Ops banned from re-materialization: %s", ops_ignored) + + def can_fuse_into_auto_functionalized(a: fx.Node, b: fx.Node) -> bool: + if b.target != torch.ops.higher_order.auto_functionalized: + return False + mutable_op = b.args[0] + ( + mutable_arg_names, + _, + ) = torch._higher_order_ops.auto_functionalize.get_mutable_args( + # pyrefly: ignore[bad-argument-type] + mutable_op + ) + for name in mutable_arg_names: # pyrefly: ignore [not-iterable] + arg = b.kwargs[name] + if a is arg: + return True + if isinstance(arg, list): + if a in arg: + return True + return False + + def can_fuse_into_triton_kernel_wrapper_functional(a: fx.Node, b: fx.Node) -> bool: + if b.target != torch.ops.higher_order.triton_kernel_wrapper_functional: + return False + mutable_arg_names = b.kwargs["tensors_to_clone"] + for name in mutable_arg_names: # pyrefly: ignore [not-iterable] + kwargs: Any = b.kwargs["kwargs"] + if kwargs is None: + raise AssertionError("kwargs must not be None") + arg = kwargs[name] + if a is arg: + return True + return False + + def is_fusible(a: fx.Node, b: fx.Node) -> bool: + # We can perform "memory fusion" into a cat, but cat cannot be a + # producer to a fusion + if get_aten_target(b) == aten.cat: + return True + if can_fuse_into_auto_functionalized(a, b): + return True + if can_fuse_into_triton_kernel_wrapper_functional(a, b): + return True + if ( + a.target is operator.getitem + and a.args[0].target # pyrefly: ignore [missing-attribute] + is torch.ops.higher_order.triton_kernel_wrapper_functional + ): + # if a is the output of a user triton kernel, + # then (by default) we will not be able to fuse b into it + return False + return op_types.is_fusible(a) and op_types.is_fusible(b) + + try: + import networkx as nx + except ImportError as e: + raise RuntimeError( + "Need networkx installed to perform smart recomputation heuristics" + ) from e + + def is_materialized_backwards(node: fx.Node) -> bool: + if op_types.is_view(node): + return False + cur_nodes = OrderedSet([node]) + while len(cur_nodes) > 0: + cur = cur_nodes.pop() + for user in cur.users: + if not node_info.is_required_fw(user) and not is_fusible(cur, user): + return True + if op_types.is_view(user): + cur_nodes.add(user) + + return False + + def should_ban_recomputation(node: fx.Node) -> str | None: + """Returns reason string if node should be banned from recomputation, None otherwise.""" + if node.op != "call_function": + return None + if node.target is operator.getitem: + return None + if node.meta.get("recompute", None) == CheckpointPolicy.MUST_SAVE: + return "marked MUST_SAVE" + if config.recompute_views and op_types.is_view(node): + return None + if node.target in [aten.lift_fresh_copy.default, aten.lift_fresh.default]: + return None + + if min_cut_options.ban_if_not_in_allowlist: + if not op_types.is_recomputable(node): + return "not in recomputable allowlist" + else: + if op_types.is_random(node): + return "random op" + if op_types.is_compute_intensive(node): + return "compute intensive op" + if is_non_builtin_to_include(node): + return "non-builtin op" + + # If a node *must* be materialized in the backwards pass, then we + # should never recompute it. This is a pretty subtle point. In + # general, the assumption we make is that recomputing a node in the + # backwards pass is "free". However, if a node must be materialized + # in the backwards pass, then recomputing it is never free. + if min_cut_options.ban_if_materialized_backward and is_materialized_backwards( + node + ): + log.debug("materialized backwards: %s %s", node, tuple(node.users)) + return "materialized in backward" + + # Arbitrary hack that sometimes seems to help things. The above + # modification appears to have made this heuristic a lot less critical + # for performance. + # NB: As of PR #121692, this hack no longer seems necessary. + if ( + # pyrefly: ignore [missing-attribute] + node.dist_from_bw < 1000 and node.dist_from_bw > config.max_dist_from_bw + ): + return "too far from backward" + + # If the output of an op is 4x smaller (arbitrary choice), + # then we don't allow recomputation. The idea here is that for + # things like reductions, saving the output of the reduction is very + # cheap/small, and it makes sure we don't do things like recompute + # normalizations in the backwards. + if min_cut_options.ban_if_reduction: + input_tensors_size = sum( + _size_of(i) for i in node.args if isinstance(i, fx.Node) + ) + output_size = _size_of(node) + if output_size * 4 < input_tensors_size: + return "reduction op" + return None + + def is_materialized(node: fx.Node) -> bool: + if node.op == "placeholder": + return True + + return not all(is_fusible(node, user) for user in node.users) + + def get_node_weight( + node: fx.Node, static_lifetime_input_nodes: OrderedSet[fx.Node] + ) -> tuple[float, str | None]: + """Returns (weight, cannot_save_reason). + + cannot_save_reason is None for finite weights, or a string explaining + why the node cannot be saved for infinite weights. + """ + if ( + config.treat_parameters_as_free_to_save + and node in static_lifetime_input_nodes + ): + return 0, None + mem_sz = _size_of(node) + if config.recompute_views and op_types.is_view(node): + # If `config.recompute_views=True`, we don't save views. This is generally + # a good idea since views are free to recompute, and it makes it a bit simpler + # to analyze. + # NB: If they're not free to recompute (e.g. nested tensors)... I + # think we should modify checks for view_ops to `is_view` and check + # that. Basically, with nested tensors, `aten.view` is not a "view + # op". + return math.inf, "view op (recompute_views=True)" + + if isinstance(node.meta["val"], py_sym_types): + # We never want to save symfloats + if not isinstance(node.meta["val"], torch.SymInt): + return INT_INF, "SymFloat (non-SymInt symbolic value)" + + # Heuristic to bias towards nodes closer to the backwards pass + # Complete guess about current value + mem_sz = int( + # pyrefly: ignore [missing-attribute] + mem_sz * (1.1 ** max(min(node.dist_from_bw, 100), 1)) + ) + if is_materialized(node): + return mem_sz, None + else: + return mem_sz * 2, None + + nx_graph = nx.DiGraph() + banned_nodes: OrderedSet[fx.Node] = OrderedSet() + + def ban_recomputation_if_allowed(node: fx.Node, reason: str = "") -> bool: + if op_types.is_view(node): + return False + if node in dont_ban: + # collectives are *always* banned from recompute, overriding `dont_ban` + # (in particular, the activation memory budget logic is not allowed to recompute collectives) + is_collective = ( + isinstance(node.target, torch._ops.OpOverload) + and node.target.namespace == "_c10d_functional" + ) + if config.unsafe_allow_optimization_of_collectives or not is_collective: + return False + # This bans recomputation of the node unless we've been forced not to by + # user annotation + if must_recompute(node): + return False + + if "val" in node.meta and isinstance(node.meta["val"], torch.SymFloat): + return False + banned_nodes.add(node) + # A node will only ever be recomputed if there is a path from an + # ancestor of this node to the backwards path through this node that + # doesn't go through any saved value. If this node is saved, then that + # condition is not possible. + nx_graph.add_edge( + "source", + node.name + "_in", + capacity=math.inf, + reason=f"cannot recompute: {reason}" if reason else "cannot recompute", + ) + return True + + for node in joint_graph.nodes: + if node.op == "output": + continue + + if node in node_info.required_bw_nodes: + # See Note: [tangents_closure vs required_bw_nodes] + if node not in node_info.tangents_closure: + nx_graph.add_edge( + node.name + "_out", + "sink", + capacity=math.inf, + reason="must be available for backward: input required for gradient", + ) + else: + nx_graph.add_edge( + node.name + "_in", + "sink", + capacity=math.inf, + reason="must be computed in backward: required for gradient", + ) + continue + + if must_recompute(node): + # If user explicitly says they want to recompute a node, we honor it + # by adding an inf-capacity edge from X_in to the sink. + # This way, X_in node is guaranteed to be part of the subgraph that contains "sink" + # after the cut, thus guaranteeing that X op will be recomputed. + nx_graph.add_edge( + node.name + "_in", + "sink", + capacity=math.inf, + reason="must recompute: marked by checkpoint policy", + ) + continue + + if _is_primal(node): + ban_recomputation_if_allowed(node, "primal input") + elif _is_fwd_seed_offset(node): + ban_recomputation_if_allowed(node, "forward RNG seed") + + # If a node can't be recomputed (too expensive or involves randomness), + # we prevent it from being recomputed by adding an inf edge to the source + # We only need to ban nodes in the fw pass, as those are the only ones that would be recomputed. + ban_reason = should_ban_recomputation(node) + if node_info.is_required_fw(node) and ban_reason: + ban_recomputation_if_allowed(node, ban_reason) + + # Checks if a node is actually a tuple. Can be simplified to just an isinstance check if we always use faketensors. + is_non_tensor_node = ( + "val" not in node.meta and "tensor_meta" not in node.meta + ) or ("val" in node.meta and not isinstance(node.meta["val"], torch.Tensor)) + + if is_sym_node(node): + weight = float(sym_node_size(node)) + cannot_save_reason = None + elif is_non_tensor_node: + # FakeScriptObjects and opaque objects should have weight 0.0 + # so they can be properly partitioned between forward and + # backward, like BackwardState. + if isinstance( + node.meta.get("val"), (BackwardState, FakeScriptObject) + ) or is_opaque_node(node): + weight = 0.0 + cannot_save_reason = None + else: + weight = math.inf + cannot_save_reason = "non-tensor output" + else: + weight, cannot_save_reason = get_node_weight( + node, node_info.static_lifetime_input_nodes + ) + + # Creates the weights on the "node" edge + if cannot_save_reason and (weight == math.inf or weight == INT_INF): + nx_graph.add_edge( + node.name + "_in", + node.name + "_out", + capacity=weight, + reason=f"cannot save: {cannot_save_reason}", + ) + else: + nx_graph.add_edge(node.name + "_in", node.name + "_out", capacity=weight) + + for user in node.users: + nx_graph.add_edge( + node.name + "_out", + user.name + "_in", + capacity=math.inf, + reason="data dependency", + ) + + # todo(chilli): This is the most questionable of the 3 heuristics for banning recompute. + # Some example models to look at where this helps perf: poolformer_m36, + # mixer_b16_224, cait_m36_384 + + # The "rough" idea here is that if you have some node that is used by both a + # node nearby downstream as well as a node far downstream, if we recompute + # both of the downstream nodes, we're unlikely to be able to fuse both + # downstream nodes together. + + # Thus, we shouldn't aim to recompute far downstream nodes that depend on + # this node. That intuition of "far downstream" is captured by whether + # there's an unfusible op along the chain somewhere + + # It could probably be improved by properly analyzing what's going on in the + # backwards pass instead of only relying on whether it's unfusible in the + # forwards. + + def find_first_unfusible(start_nodes: list[fx.Node], max_range: int) -> int: + """ + Finds the first unfusible node in the chain of nodes starting from + `start_nodes` and returns its position. + """ + sorted_nodes: list[tuple[int, fx.Node, bool]] = [] + for n in start_nodes: + heapq.heappush(sorted_nodes, (node_info.get_fw_order(n), n, True)) + + while len(sorted_nodes) > 0: + _, node, node_is_fusible = heapq.heappop(sorted_nodes) + if not node_is_fusible: + return node_info.get_fw_order(node) + for user in node.users: + if node_info.is_required_fw(user): + if node_info.get_fw_order(user) > max_range: + continue + val: tuple[int, fx.Node, bool] = ( + node_info.get_fw_order(user), + user, + is_fusible(node, user), + ) + if val not in sorted_nodes: + heapq.heappush(sorted_nodes, val) + return max_range + + if min_cut_options.ban_if_used_far_apart: + for used_node in node_info.required_fw_nodes: + orders = [ + node_info.get_fw_order(user) + for user in used_node.users + if node_info.is_required_fw(user) + ] + fw_users = [ + user for user in used_node.users if node_info.is_required_fw(user) + ] + if len(orders) > 0: + first_unfusible_use = find_first_unfusible(fw_users, max(orders)) + for user in tuple(used_node.users): + if ( + node_info.is_required_fw(user) + and node_info.get_fw_order(user) > first_unfusible_use + and is_fusible(used_node, user) + ): + if user in banned_nodes: + continue + log.info( + "used above/below fusible %s:(%s) -> %s -> %s:(%s)", + used_node, + node_info.get_fw_order(used_node), + first_unfusible_use, + user, + node_info.get_fw_order(user), + ) + ban_recomputation_if_allowed(user) + + # This heuristic is fairly straightforward. The idea is that although it is + # cheap to recompute bandwidth-bound ops, we don't want to end up in a situation + # where we have a long chain of pointwise ops from the beginning to the end + # of the model (like say, residual connections) + + # todo: I'm not totally sure why this heuristic matters. It's possible that this is + # working around Inductor fusion decisions, or that it's a patch over + # suboptimal partitioning decisions + + # Some models it improves perf on are cait_m36_384, mixer_b16_224, poolformer_m36 + + if min_cut_options.ban_if_long_fusible_chains: + visited: OrderedSet[fx.Node] = OrderedSet() + for start_node in joint_graph.nodes: + if not node_info.is_required_fw(start_node): + continue + fusible: list[tuple[int, fx.Node]] = [ + (node_info.get_fw_order(start_node), start_node) + ] + start_order = node_info.get_fw_order(start_node) + while len(fusible) > 0: + _, cur = heapq.heappop(fusible) + if cur in visited: + continue + visited.add(cur) + # 100 is arbitrary choice to try and prevent degenerate cases + if ( + node_info.get_fw_order(cur) > start_order + 100 + and len(fusible) == 0 + ): + log.info( + "too long %s %s %s %s", + cur, + start_node, + node_info.get_fw_order(cur), + node_info.get_fw_order(start_node), + ) + ban_recomputation_if_allowed(cur) + break + + for user in cur.users: + if ( + node_info.is_required_fw(user) + and is_fusible(cur, user) + and user not in banned_nodes + ): + heapq.heappush(fusible, (node_info.get_fw_order(user), user)) + + try: + cut_value, partition = nx.minimum_cut(nx_graph, "source", "sink") + except nx.NetworkXUnbounded as unbounded_exc: + # Check if structured tracing is enabled (for production job debugging via tlparse) + structured_tracing_enabled = bool(trace_log.handlers) + + # Dump the FX graph for debugging + fx_graph_file: str | None = None + fx_graph_str: str | None = None + joint_module = joint_graph.owning_module + try: + fx_graph_str = ( + joint_module.print_readable( + print_output=False, include_stride=True, include_device=True + ) + if joint_module + else str(joint_graph) + ) + # Always log to structured trace for production debugging + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "min_cut_failed_fx_graph", + "encoding": "string", + }, + payload_fn=lambda: fx_graph_str, + ) + # Also write to local file for local debugging + fx_graph_file = _get_unique_path("min_cut_failed_graph", ".txt") + with open(fx_graph_file, "w") as f: + f.write(fx_graph_str) + except Exception as e: + fx_graph_file = f"(failed to write: {e})" + + # Dump the min-cut edge list to structured trace + edge_list_str = "\n".join(nx.readwrite.edgelist.generate_edgelist(nx_graph)) + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "min_cut_failed_edge_list", + "encoding": "string", + }, + payload_fn=lambda: edge_list_str, + ) + + # Find and report the infinite-capacity path + inf_path = _find_infinite_capacity_path(nx_graph) + if inf_path: + # Group edges by FX node and format for user understanding + # inf_path is a list of (from_node, to_node, reason) tuples + # + # Edge types and what they mean: + # - source -> X_in: X cannot be recomputed + # - X_in -> X_out (inf): X's output cannot be saved + # - X_out -> Y_in: Y depends on X (data flow) + # - X_in -> sink: X must be computed in backward + # - X_out -> sink: X's output must be available for backward + + # Build a user-friendly explanation grouped by FX node + node_constraints: dict[str, list[str]] = {} + raw_path_nodes = ["source"] + + def get_base_name(node_name: str) -> str: + for suffix in ("_in", "_out"): + if node_name.endswith(suffix): + return node_name[: -len(suffix)] + return node_name + + for from_node, to_node, reason in inf_path: + raw_path_nodes.append(to_node) + + # Skip source/sink, focus on FX nodes + if from_node == "source": + base = get_base_name(to_node) + node_constraints.setdefault(base, []).append(reason) + elif to_node == "sink": + base = get_base_name(from_node) + node_constraints.setdefault(base, []).append(reason) + elif get_base_name(from_node) == get_base_name(to_node): + # Internal edge (X_in -> X_out) + base = get_base_name(from_node) + node_constraints.setdefault(base, []).append(reason) + else: + # Data dependency edge (X_out -> Y_in) + from_base = get_base_name(from_node) + to_base = get_base_name(to_node) + node_constraints.setdefault(to_base, []).append( + f"depends on {from_base}" + ) + + # Format the constraints nicely + constraint_lines: list[str] = [] + for node_name, constraints in node_constraints.items(): + constraint_lines.append(f" {node_name}:") + for c in constraints: + constraint_lines.append(f" - {c}") + + constraints_str = "\n".join(constraint_lines) + raw_path_str = " -> ".join(raw_path_nodes) + + # Try to visualize (logs to structured trace and writes local file) + svg_path, svg_content = visualize_min_cut_graph(nx_graph) + if svg_content: + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "min_cut_failed_svg", + "encoding": "string", + }, + payload_fn=lambda: svg_content, + ) + + # Build file location messages + local_files_msg = ( + f"FX graph dump: {fx_graph_file}\n" if fx_graph_file else "" + ) + if svg_path: + local_files_msg += f"Min-cut graph visualization: {svg_path}\n" + + # Suggest tlparse if structured tracing is enabled + tlparse_msg = "" + if structured_tracing_enabled: + tlparse_msg = ( + "[Production debugging: Use tlparse to extract debug artifacts " + "(min_cut_failed_fx_graph, min_cut_failed_edge_list, min_cut_failed_svg)]\n" + ) + + raise RuntimeError( + f"AOT Autograd failed to partition the joint forward-backward graph.\n\n" + f"The partitioner determines which intermediate values to save from the " + f"forward pass vs recompute in the backward pass. This error means a value " + f"is required for backward, but cannot be saved AND cannot be recomputed.\n\n" + f"This is a bug in PyTorch. Please file an issue at " + f"https://github.com/pytorch/pytorch/issues\n\n" + f"Nodes involved in the conflict:\n" + f"{constraints_str}\n\n" + f"[For PyTorch developers: one of the above constraints is wrong. " + f"Either the node should be recomputable, saveable, or not required for backward.]\n\n" + f"[Debug: min-cut path] {raw_path_str}\n" + f"{local_files_msg}" + f"{tlparse_msg}" + ) from unbounded_exc + + # Fallback if we couldn't find the path + log.info("Failed to compute min-cut on following graph:") + log.info( + "%s", + LazyString( + lambda: "\n".join(nx.readwrite.edgelist.generate_edgelist(nx_graph)) + ), + ) + visualize_min_cut_graph(nx_graph) + raise + except Exception: + log.info("Failed to compute min-cut on following graph:") + log.info( + "%s", + LazyString( + lambda: "\n".join(nx.readwrite.edgelist.generate_edgelist(nx_graph)) + ), + ) + visualize_min_cut_graph(nx_graph) + raise + + reachable, non_reachable = partition + cutset: OrderedSet[tuple[str, str]] = OrderedSet() + for u, nbrs in ((n, nx_graph[n]) for n in reachable): + cutset.update((u, v) for v in nbrs if v in non_reachable) + + cut_nodes: OrderedSet[str] = OrderedSet() + for node_in, node_out in cutset: + if node_in[:-3] != node_out[:-4]: + raise AssertionError( + f"node_in[:-3]={node_in[:-3]} != node_out[:-4]={node_out[:-4]}" + ) + node_name = node_in[:-3] + cut_nodes.add(node_name) + + name_to_node = get_name_to_node(joint_graph) + # To make this stuff deterministic + node_idx = {node: idx for idx, node in enumerate(joint_graph.nodes)} + saved_values = sorted( + (name_to_node[node] for node in cut_nodes), key=lambda x: node_idx[x] + ) + return saved_values, banned_nodes + + +def _find_infinite_capacity_path( + nx_graph: nx.DiGraph[str, dict[str, Any]], +) -> list[tuple[str, str, str]] | None: + """BFS from source to sink following only infinite-capacity edges. + + Returns a list of (from_node, to_node, reason) tuples representing the path, + or None if no such path exists. + """ + + visited = OrderedSet(["source"]) + # Each queue item: (current_node, path_of_edges) + # where path_of_edges is a list of (from_node, to_node, reason) tuples + queue: deque[tuple[str, list[tuple[str, str, str]]]] = deque([("source", [])]) + + while queue: + node, edge_path = queue.popleft() + for neighbor in nx_graph.successors(node): + if neighbor in visited: + continue + edge_data = nx_graph[node][neighbor] + capacity = edge_data.get("capacity", 0) + # Check for infinite capacity (either math.inf or INT_INF) + if capacity == math.inf or capacity == INT_INF: + reason = edge_data.get("reason", "unknown") + new_edge = (node, neighbor, reason) + new_path = edge_path + [new_edge] + if neighbor == "sink": + return new_path + visited.add(neighbor) + queue.append((neighbor, new_path)) + return None + + +def _get_unique_path(base_name: str, extension: str) -> str: + """Get a unique file path, appending a counter if the file already exists. + + For example, if "min_cut_failed.svg" exists, returns "min_cut_failed_1.svg". + """ + path = f"{base_name}{extension}" + if not os.path.exists(path): + return path + + counter = 1 + while os.path.exists(f"{base_name}_{counter}{extension}"): + counter += 1 + return f"{base_name}_{counter}{extension}" + + +def visualize_min_cut_graph( + nx_graph: nx.DiGraph[str, dict[str, Any]], +) -> tuple[str | None, str | None]: + """Visualize the min-cut graph to an SVG file. + + Returns (path_to_svg, svg_content) tuple. Both are None if pydot is unavailable. + """ + import networkx as nx + + try: + import pydot + except ImportError: + log.info( + "Install pydot to visualize the min-cut graph for debugging: pip install pydot", + exc_info=True, + ) + return None, None + + dot_format = nx.nx_pydot.to_pydot(nx_graph).to_string() + dot_graph = pydot.graph_from_dot_data(dot_format)[0] # type: ignore[index] + for edge in dot_graph.get_edges(): + weight = nx_graph[edge.get_source()][edge.get_destination()]["capacity"] + # Set edge label to weight + edge.set_label(str(weight)) # type: ignore[union-attr] + # Color edges with weight 'inf' as red + if weight == float("inf"): + edge.set_color("red") # type: ignore[union-attr] + + # Generate SVG content + svg_content = dot_graph.create_svg().decode("utf-8") # type: ignore[union-attr] + + # Write to local file + svg_path = _get_unique_path("min_cut_failed", ".svg") + with open(svg_path, "w") as f: + f.write(svg_content) + + return svg_path, svg_content + + +def get_default_op_list() -> OpTypes: + default_recomputable_ops: list[Callable[..., Any]] = [ + aten.add, + aten.sub, + aten.div, + aten.atan2, + aten.mul, + aten.max, + aten.min, + aten.pow, + aten.remainder, + aten.fmod, + aten.__and__, + aten.__or__, + aten.__xor__, + aten.__lshift__, + aten.__rshift__, + aten.eq, + aten.ne, + aten.ge, + aten.gt, + aten.le, + aten.lt, + aten.abs, + aten.bitwise_not, + aten.ceil, + aten.floor, + aten.frac, + aten.neg, + aten.relu, + aten.round, + aten.silu, + aten.trunc, + aten.log, + aten.log10, + aten.log1p, + aten.log2, + aten.lgamma, + aten.exp, + aten.expm1, + aten.erf, + aten.erfc, + aten.cos, + aten.acos, + aten.cosh, + aten.sin, + aten.asin, + aten.sinh, + aten.tan, + aten.atan, + aten.tanh, + aten.atanh, + aten.sqrt, + aten.rsqrt, + aten.reciprocal, + aten.sigmoid, + aten.softplus, + aten.threshold, + aten.threshold_backward, + aten.clamp, + aten.where, + aten.lerp, + aten.addcmul, + aten.gelu, + aten.gelu_backward, + aten.sum, + aten.mean, + aten._grad_sum_to_size, + aten.sum_to_size, + aten.amax, + aten.to, + aten.type_as, + operator.getitem, + aten.squeeze, + aten.unsqueeze, + aten.rsub, + aten._to_copy, + ] # noqa: E501,B950 + recomputable_view_ops = [aten.squeeze, aten.unsqueeze, aten.alias] + recomputable_view_ops += [ + aten.view, + aten.slice, + aten.t, + prims.broadcast_in_dim, + aten.expand, + aten.as_strided, + aten.permute, + aten.select, + aten.split, + ] + view_ops = recomputable_view_ops + default_recomputable_ops += [ + prims.div, + prims.convert_element_type, + aten.clone, + aten._to_copy, + aten.full_like, + prims.var, + prims.sum, + aten.var, + aten.std, + prims.broadcast_in_dim, + aten.select, + aten._unsafe_view, + aten.view, + aten.expand, + aten.slice, + aten.reshape, + aten.broadcast_tensors, + aten.scalar_tensor, + aten.ones, + aten.new_zeros, + aten.lift_fresh_copy, + aten.arange, + aten.triu, + aten.var_mean, + aten.isinf, + aten.any, + aten.full, + aten.as_strided, + aten.zeros, + aten.empty, + aten.empty_like, + aten.argmax, + aten.maximum, + prims.iota, + prims._low_memory_max_pool_offsets_to_indices, + ] # noqa: E501,B950 + # Natalia said that we should allow recomputing indexing :) + default_recomputable_ops += [aten.index, aten.gather] + default_recomputable_ops += view_ops + + default_recomputable_ops += pointwise_ops() + + default_recomputable_ops += [ + aten.zeros_like, + ] + + default_recomputable_ops += [method_to_operator(m) for m in magic_methods] + recomputable_ops = OrderedSet(default_recomputable_ops) + + random_ops = OrderedSet[Callable[..., Any]]( + [aten.native_dropout, aten.rand_like, aten.randn_like] + ) + compute_intensive_ops = [ + aten.mm, + aten.convolution, + aten.convolution_backward, + aten.bmm, + aten.addmm, + aten._scaled_dot_product_flash_attention, + aten._scaled_dot_product_efficient_attention, + aten._flash_attention_forward, + aten._efficient_attention_forward, + aten.upsample_bilinear2d, + aten._scaled_mm, + ] # noqa: E501,B950 + + fusible_ops = recomputable_ops | random_ops + return OpTypes( + fusible_ops, + OrderedSet(compute_intensive_ops), + random_ops, + OrderedSet(view_ops), + recomputable_ops, + ) + + +def get_name_to_node(graph: fx.Graph) -> dict[str, fx.Node]: + name_to_node: dict[str, fx.Node] = {} + for node in graph.nodes: + name_to_node[node.name] = node + return name_to_node + + +def _optimize_runtime_with_given_memory( + joint_graph: fx.Graph, + memory: list[float], + runtimes: list[float], + max_memory: float, + node_info: NodeInfo, + all_recomputable_banned_nodes: list[fx.Node], +) -> tuple[float, list[int], list[int]]: + SOLVER = config.activation_memory_budget_solver + if SOLVER == "greedy": + return greedy_knapsack(memory, runtimes, max_memory) + elif SOLVER == "ilp": + return ilp_knapsack(memory, runtimes, max_memory) + elif SOLVER == "dp": + return dp_knapsack(memory, runtimes, max_memory) + elif SOLVER == "dp_knapsack_sliding_hirschberg": + return dp_knapsack_sliding_hirschberg(memory, runtimes, max_memory) + elif SOLVER == "dynamic_memory_budget_dp": + log.warning( + "dynamic_memory_budget_dp is an experimental solver. " + "It does not guarantee performance improvements. " + "Additionally, it is not guaranteed to be stable." + ) + graph_info_provider = GraphInfoProvider.inialize_from_graph( + joint_graph=joint_graph, + all_recomputable_banned_nodes=all_recomputable_banned_nodes, + recorded_knapsack_input_memories=memory, + recorded_knapsack_input_runtimes=runtimes, + ) + return dp_knapsack( + memory, + runtimes, + KnapsackEvaluator( + graph_info_provider=graph_info_provider, + ).get_knee_point_memory_budget( + knapsack_algo=dp_knapsack, + max_mem_budget=max_memory, + ), + ) + elif isinstance(SOLVER, CustomKnapsackSolver): + saved_node_idx, recomp_node_idx = SOLVER( + memory, joint_graph, max_memory, node_info, all_recomputable_banned_nodes + ) + return (0.0, saved_node_idx, recomp_node_idx) + else: + raise RuntimeError(f"Not aware of memory budget knapsack solver: {SOLVER}") + + +from torch.utils._mode_utils import no_dispatch + + +# replace symbols in size and strides with their hints without guarding. +def _remove_symbols_without_guarding(x: torch.Tensor, fallback: int) -> torch.Tensor: + shape = list(x.shape) + + def realize_symbol(d: torch.SymInt | int) -> int: + return optimization_hint(d, fallback=fallback) + + shape = [realize_symbol(s) for s in shape] + stride = [realize_symbol(s) for s in x.stride()] + return x.new_empty_strided(shape, stride=stride) + + +def estimate_runtime(node: fx.Node) -> float: + RUNTIME_MODE = config.activation_memory_budget_runtime_estimator + + def materialize_arg(x: Any) -> Any: + if isinstance(x, fx.Node) and isinstance(x.meta["val"], torch.Tensor): + return _remove_symbols_without_guarding(x.meta["val"], fallback=4096) + elif isinstance(x, fx.Node) and isinstance(x.meta["val"], torch.SymInt): + return optimization_hint(x.meta["val"], fallback=4096) + elif isinstance(x, fx.Node) and isinstance(x.meta["val"], torch.SymFloat): + return 1.0 + elif isinstance(x, fx.Node) and isinstance(x.meta["val"], torch.SymBool): + return True + else: + return x + + if RUNTIME_MODE == "testing": + return 1 + + elif RUNTIME_MODE == "profile": + with no_dispatch(): + from torch._inductor.runtime.benchmarking import benchmarker + + args, kwargs = pytree.tree_map(materialize_arg, (node.args, node.kwargs)) + # pyrefly: ignore[not-callable] + ms = benchmarker.benchmark_gpu(lambda: node.target(*args, **kwargs)) + return ms + + elif RUNTIME_MODE == "flops": + # todo(chilli): Normalize this to also return ms + from torch.utils.flop_counter import FlopCounterMode + + args, kwargs = pytree.tree_map(materialize_arg, (node.args, node.kwargs)) + with FlopCounterMode(display=False) as mode: + # pyrefly: ignore[not-callable] + node.target(*args, **kwargs) + counted_flops = mode.get_total_flops() + return max(counted_flops, 1) + + elif isinstance(RUNTIME_MODE, CustomRuntimeEstimator): + return RUNTIME_MODE(node) + + else: + raise RuntimeError(f"Not aware of runtime estimator: {RUNTIME_MODE}") + + +def choose_saved_values_set( + joint_graph: fx.Graph, + node_info: NodeInfo, + memory_budget: float = 1, +) -> list[fx.Node]: + if memory_budget > 1 or memory_budget < 0: + raise RuntimeError( + f"The valid ranges for memory budget are 0 <= m <= 1. The provided value is {memory_budget}" + ) + min_cut_options = MinCutOptions( + ban_if_used_far_apart=config.ban_recompute_used_far_apart, + ban_if_long_fusible_chains=config.ban_recompute_long_fusible_chains, + ban_if_materialized_backward=config.ban_recompute_materialized_backward, + ban_if_not_in_allowlist=config.ban_recompute_not_in_allowlist, + ban_if_reduction=config.ban_recompute_reductions, + ) + + if config.aggressive_recomputation: + min_cut_options = replace( + min_cut_options, + ban_if_used_far_apart=False, + ban_if_long_fusible_chains=False, + ban_if_materialized_backward=False, + ban_if_not_in_allowlist=False, + ) + if memory_budget == 0: + return node_info.inputs + + runtime_optimized_saved_values, _ = solve_min_cut( + joint_graph, + node_info, + min_cut_options, + ) + # return runtime_optimized_saved_values + if memory_budget == 1: + return runtime_optimized_saved_values + + def estimate_activations_size(saved_values: list[fx.Node]) -> float: + return sum(map(_size_of, saved_values)) / 1e9 + + min_act_size = estimate_activations_size(node_info.inputs) + max_act_size = estimate_activations_size(runtime_optimized_saved_values) + # The optimized choice is smaller than the inputs anyways + if max_act_size <= min_act_size: + return runtime_optimized_saved_values + + def get_normalized_size(sz: float) -> float: + return (sz / 1e9) / (max_act_size - min_act_size) + + def get_mem_ratio(activations: list[fx.Node]) -> float: + return (estimate_activations_size(activations) - min_act_size) / ( + max_act_size - min_act_size + ) + + more_aggressive_options = replace( + min_cut_options, + ban_if_used_far_apart=False, + ban_if_long_fusible_chains=False, + ban_if_materialized_backward=False, + ) + more_aggressive_saved_values, _ = solve_min_cut( + joint_graph, node_info, more_aggressive_options + ) + if get_mem_ratio(more_aggressive_saved_values) < memory_budget: + return more_aggressive_saved_values + + aggressive_options = replace( + more_aggressive_options, + ban_if_not_in_allowlist=False, + ) + aggressive_recomputation_saved_values, banned_nodes = solve_min_cut( + joint_graph, node_info, aggressive_options + ) + + if get_mem_ratio(aggressive_recomputation_saved_values) < memory_budget: + return aggressive_recomputation_saved_values + + from torch._inductor.fx_utils import get_node_storage + + input_storages = OrderedSet(get_node_storage(node) for node in node_info.inputs) + + def get_recomputable_banned_nodes( + banned_nodes: OrderedSet[fx.Node], + ) -> list[fx.Node]: + return [ + i + for i in banned_nodes + if ( + # Only allow recomputing nodes that are actually required for BW + i.dist_from_bw < int(1e9) # type: ignore[attr-defined] + and ( + get_node_storage(i) not in input_storages + or is_non_builtin_to_include(i) + ) + ) + ] + + recomputable_banned_nodes = get_recomputable_banned_nodes(banned_nodes) + must_save_nodes = [ + i + for i in recomputable_banned_nodes + if i.meta.get("recompute", False) == CheckpointPolicy.MUST_SAVE + ] + recomputable_banned_nodes = [ + i for i in recomputable_banned_nodes if i not in must_save_nodes + ] + + # default: runtime_optimized_saved_values + # more aggressive: more_aggressive_saved_values + # full aggressive: aggressive_recomputation_saved_values + + all_recomputable_banned_nodes = sorted( + recomputable_banned_nodes, key=_size_of, reverse=True + ) + if len(all_recomputable_banned_nodes) == 0: + return node_info.inputs + must_save_nodes + memories_banned_nodes = [ + get_normalized_size(_size_of(i)) for i in all_recomputable_banned_nodes + ] + runtimes_banned_nodes = [ + estimate_runtime(node) for node in all_recomputable_banned_nodes + ] + from torch.utils._mode_utils import no_dispatch + + def get_saved_values_knapsack( + memory_budget: float, node_info: NodeInfo, joint_graph: fx.Graph + ) -> tuple[list[fx.Node], float]: + with no_dispatch(): + ( + expected_runtime, + saved_node_idxs, + recomputable_node_idxs, + ) = _optimize_runtime_with_given_memory( + joint_graph, + memories_banned_nodes, + runtimes_banned_nodes, + max(memory_budget, 0), + node_info, + all_recomputable_banned_nodes, + ) + dont_ban: OrderedSet[fx.Node] = OrderedSet() + for idx in recomputable_node_idxs: + # if idx in all_recomputable_banned_nodes: + try: + dont_ban.add(all_recomputable_banned_nodes[idx]) + except BaseException: # noqa: B036 + pass + + if not dont_ban.issubset(all_recomputable_banned_nodes): + raise AssertionError( + "dont_ban must be a subset of all_recomputable_banned_nodes" + ) + + saved_values, _ = solve_min_cut( + joint_graph, + node_info, + aggressive_options, + dont_ban, + ) + if AOT_PARTITIONER_DEBUG: + create_structured_trace_for_min_cut_info( + joint_graph=joint_graph, + all_recomputable_banned_nodes=all_recomputable_banned_nodes, + saved_node_idxs=saved_node_idxs, + recomputable_node_idxs=recomputable_node_idxs, + expected_runtime=expected_runtime, + memories_banned_nodes=[ + _size_of(i) for i in all_recomputable_banned_nodes + ], + normalized_memories_banned_nodes=memories_banned_nodes, + runtimes_banned_nodes=runtimes_banned_nodes, + min_cut_saved_values=saved_values, + ) + return saved_values, expected_runtime + + if config.visualize_memory_budget_pareto: + + def estimate_for_budget(b: float) -> tuple[float, float, float]: + saved_values, expected_runtime = get_saved_values_knapsack( + b, node_info=node_info, joint_graph=joint_graph + ) + return ( + b, + sum(runtimes_banned_nodes) - expected_runtime, + get_mem_ratio(saved_values), + ) + + options = [estimate_for_budget(0.0), estimate_for_budget(1.0)] + + if options[0][1:] != options[1][1:]: + bisects = [(options[0], options[1])] + while bisects: + lhs, rhs = bisects.pop() + if rhs[0] - lhs[0] < 1e-3: + options.append(lhs) + options.append(rhs) + continue + mid = estimate_for_budget((lhs[0] + rhs[0]) / 2) + if mid[1:] != lhs[1:]: + bisects.append((lhs, mid)) + if mid[1:] != rhs[1:]: + bisects.append((mid, rhs)) + options.sort() + + import matplotlib.pyplot as plt + + x_values = [item[2] for item in options] + y_values = [item[1] for item in options] + + # Plotting the values with updated axis labels and chart title + plt.figure(figsize=(10, 6)) + plt.plot(x_values, y_values, marker="o") + + # Adding labels for each point + for i, txt in enumerate(x_values): + plt.annotate( + f"{txt:.4f}", + (txt, y_values[i]), + textcoords="offset points", + xytext=(0, 10), + ha="center", + ) + + plt.xlabel("Memory Budget") + plt.ylabel("Runtime of Recomputed Components") + plt.title("Pareto Frontier of Memory Budget vs. Recomputation Runtime") + plt.grid(True) + fig = plt.gcf() + plt.show() + fig_dir = os.getcwd() + if config.memory_budget_pareto_dir is not None: + fig_dir = config.memory_budget_pareto_dir + os.makedirs(fig_dir, exist_ok=True) + rank_suffix = "" + if torch.distributed.is_available() and torch.distributed.is_initialized(): + rank_suffix = f"_rank_{torch.distributed.get_rank()}" + fig_name = os.path.join( + fig_dir, f"memory_budget_pareto{rank_suffix}_{get_aot_graph_name()}.svg" + ) + fig.savefig(fig_name) + log.warning("Generated Pareto frontier curve at %s", fig_name) + + # todo(chilli): Estimated doesn't align exactly with actual - actual is + # usually less memory than estimated. i'm guessing (actually quite + # unsure about this) that's because estimated is just only including + # tensors we actually banned from recompute, but there may be other + # tensors that we choose to save. + + return get_saved_values_knapsack( + memory_budget=memory_budget, node_info=node_info, joint_graph=joint_graph + )[0] + + +def _sync_decision_cross_ranks( + joint_graph: torch.fx.Graph, saved_values: list[torch.fx.Node] +) -> list[torch.fx.Node]: + # use the same policy across different GPUs + from torch._subclasses.fake_tensor import unset_fake_temporarily + + def has_collectives(joint_graph: torch.fx.Graph) -> bool: + for node in joint_graph.nodes: + if isinstance( + node.target, torch._ops.OpOverload + ) and node.target.namespace in {"_c10d_functional", "c10d_functional"}: + return True + return False + + def has_same_nodes(joint_graph: torch.fx.Graph) -> bool: + # proxy to check if the graph is the same across different GPUs. + # We only consider the name and order of nodes. A more robust way + # would be to check the hash of the whole graph (disregarding input shapes), + # this is a reasonable first-order approximation. + node_str = "/".join(x.name for x in joint_graph.nodes) + inputs = hashlib.sha256(node_str.encode("utf-8")).hexdigest() + all_inputs = [None for _ in range(torch.distributed.get_world_size())] + with no_dispatch(), unset_fake_temporarily(): + # TODO: maybe use a different process group? + torch.distributed.all_gather_object(all_inputs, inputs) + return all(all_inputs[0] == x for x in all_inputs) + + if ( + torch.distributed.is_available() + and torch.distributed.is_initialized() + and torch.distributed.get_world_size() > 1 + and has_collectives(joint_graph) + and has_same_nodes(joint_graph) + ): + with no_dispatch(), unset_fake_temporarily(): + objects = [[x.name for x in saved_values]] + saved_ops_names_all_ranks: list[list[str]] = [ + [] for _ in range(torch.distributed.get_world_size()) + ] + torch.distributed.all_gather_object(saved_ops_names_all_ranks, objects[0]) + name_to_node = get_name_to_node(joint_graph) + saved_sizes: list[int] = [] + saved_ops_with_sizes: dict[str, int] = {} + + for idx, saved_ops_names in enumerate(saved_ops_names_all_ranks): + saved_nodes = [name_to_node[op_name] for op_name in saved_ops_names] + saved_size = 0 + for node in saved_nodes: + size_of_node = _size_of(node) + saved_size += size_of_node + if idx == torch.distributed.get_rank(): + saved_ops_with_sizes[node.name] = size_of_node + saved_ops_with_sizes["total size"] = saved_size + saved_sizes.append(saved_size) + + saved_sizes_tensor = torch.tensor( + saved_sizes, + device=torch.distributed.distributed_c10d._get_object_coll_device(), + ) + torch.distributed.all_reduce( + saved_sizes_tensor, op=torch.distributed.distributed_c10d.ReduceOp.MAX + ) + + picked_rank_idx = int(torch.argmin(saved_sizes_tensor).item()) + sync_decision_cross_ranks_str = f"picked_rank_idx={picked_rank_idx}, saved_nodes of current rank={saved_ops_with_sizes}" + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "aot_joint_graph_sync_decision_cross_ranks", + "encoding": "string", + }, + payload_fn=lambda: sync_decision_cross_ranks_str, + ) + + saved_values = [ + name_to_node[n] for n in saved_ops_names_all_ranks[picked_rank_idx] + ] + + return saved_values + + +def thread_graphsafe_rng_from_hops( + module: fx.GraphModule, is_backward: bool +) -> fx.GraphModule: + """ + Graph-safe RNG lets torch.compile use CUDA Graphs for graphs with RNG ops. + For graphs without HOPs, the partitioner adds placeholder nodes + fwd_rng_state_* and bw_rng_state_* to the forward and backward graphs. At + runtime, the AOTDispatcher retrieves these RNG states and passes them to the + compiled graphs. + + This works well for no-HOP graphs. With HOPs, the partitioner runs + recursively: it first partitions the HOP (producing forward/backward HOP + subgraphs) and then stitches them back into the outer joint graph. For HOPs + that contain RNG ops, the outer joint graph now includes HOP subgraph + modules with extra RNG placeholders. We must thread these placeholders + through the outer module partitioned forward and backward graphs—this + function does exactly that. It collects the RNG placeholder nodes from the + HOPs and creates corresponding placeholders in the outer forward and + backward graphs. + + There is a catch: for a short period, the joint graph is in a “bad” state. + The HOP subgraphs expect additional inputs (because of the new + placeholders), but the outer graph call sites don't yet provide them. We + can't fix this in the joint graph because the joint graph's input signature + is fixed (primals, tangents). As a compromise, we keep the joint graph in + somewhat of a bad state for some time and, once the outer forward and + backward graphs are partitioned, insert the corresponding RNG placeholders + and wire up the calls. + """ + + rng_count = 0 + rng_string = "bwd_rng_state" if is_backward else "fwd_rng_state" + last_input = next(reversed(module.graph.find_nodes(op="placeholder"))) + for hop_node in module.graph.find_nodes( + op="call_function", target=torch.ops.higher_order.invoke_subgraph + ): + subgraph = getattr(module, hop_node.args[0].target) + if isinstance(subgraph, fx.GraphModule): + new_rng_inputs: list[fx.Node] = [] + for placeholder_node in subgraph.graph.find_nodes(op="placeholder"): + if rng_string in placeholder_node.name: + # Found a rng state placeholder in the hop graph, lets add + # the corresponding node in the outer graph + with module.graph.inserting_after(last_input): + rng_state = module.graph.placeholder( + f"{rng_string}_{rng_count}" + ) + rng_count += 1 + rng_state.meta["val"] = placeholder_node.meta["val"] + last_input = rng_state + new_rng_inputs.append(rng_state) + + if new_rng_inputs: + # Pass on the new args that include the new_rng_inputs + with module.graph.inserting_after(hop_node): + new_hop_node_with_fixed_args = module.graph.create_node( + "call_function", + torch.ops.higher_order.invoke_subgraph, + (*hop_node.args, *new_rng_inputs), # type: ignore[arg-type] + {}, + ) + hop_node.replace_all_uses_with( + new_hop_node_with_fixed_args, propagate_meta=True + ) + + # Setup the eager_input_vals + eager_vals = hop_node.meta.get("eager_input_vals") + if eager_vals: + eager_args, eager_kwargs = eager_vals + new_eager_args = ( + *eager_args, + *[inp.meta["val"] for inp in new_rng_inputs], + ) + new_hop_node_with_fixed_args.meta["eager_input_vals"] = ( + new_eager_args, + eager_kwargs, + ) + module.graph.erase_node(hop_node) + + return module + + +def classify_nodes( + joint_module: fx.GraphModule, + static_lifetime_input_indices: list[int], + num_fwd_outputs: int, +) -> NodeInfo: + name_to_node = get_name_to_node(joint_module.graph) + required_bw_nodes: OrderedSet[fx.Node] = OrderedSet() + for node in joint_module.graph.nodes: + if node.op == "placeholder" and "tangents" in node.target: + required_bw_nodes.add(node) + elif _must_be_in_backward(node): + required_bw_nodes.add(node) + + if node in required_bw_nodes: + required_bw_nodes.update(node.users) + + primal_inputs = list(filter(_is_primal, joint_module.graph.nodes)) + fwd_seed_offset_inputs = list(filter(_is_fwd_seed_offset, joint_module.graph.nodes)) + inputs = primal_inputs + fwd_seed_offset_inputs + fwd_outputs, bwd_outputs, fwd_outputs_descs, bwd_outputs_descs = ( + _extract_fwd_bwd_outputs(joint_module, num_fwd_outputs=num_fwd_outputs) + ) + # Note: [tangents_closure vs required_bw_nodes] + # + # required_bw_nodes is used to determine which nodes need edges to + # the sink. It is important to also track tangents closure because + # that determines whether you can save that tensor, i.e., whether you + # want to connect x_in or x_out to the sink. + tangents_closure = required_bw_nodes.copy() + required_bw_nodes.update( + o for o in bwd_outputs if o is not None and o.op != "output" + ) + forward_only_graph = _extract_graph_with_inputs_outputs( + joint_module.graph, inputs, fwd_outputs, fwd_outputs_descs, "forward" + ) + required_fw_nodes: OrderedSet[fx.Node] = OrderedSet( + name_to_node[node.name] + for node in forward_only_graph.nodes + if node.op != "output" + ) + unclaimed_nodes: OrderedSet[fx.Node] = OrderedSet( + node + for node in joint_module.graph.nodes + if node not in required_fw_nodes and node not in required_bw_nodes + ) + static_lifetime_input_nodes = OrderedSet( + p for i, p in enumerate(primal_inputs) if i in static_lifetime_input_indices + ) + fw_cnt = 0 + fw_order = {} + for node in joint_module.graph.nodes: + if node in required_fw_nodes: + fw_order[node] = fw_cnt + fw_cnt += 1 + return NodeInfo( + inputs, + required_fw_nodes, + required_bw_nodes, + tangents_closure, + unclaimed_nodes, + fw_order, + static_lifetime_input_nodes, + ) + + +def min_cut_rematerialization_partition( + joint_module: fx.GraphModule, + _joint_inputs: Any, + compiler: str = "inductor", + *, + num_fwd_outputs: int, + static_lifetime_input_indices: list[int] | None = None, +) -> tuple[fx.GraphModule, fx.GraphModule]: + """ + Partitions the joint graph such that the backward recomputes the forward. + Recomputing helps in trading off memory bandwidth with computation. + + To create the fwd and bwd graph, we copy the joint graph, manually set the + outputs to just original forward or backward outputs. And then we run the + resulting graphs through dead code elimination. + + .. warning:: + This API is experimental and likely to change. + + Args: + joint_module(fx.GraphModule): The joint forward and backward graph. This + is the result of AOT Autograd tracing. + _joint_inputs: The inputs to the joint graph. This is unused. + compiler: This option determines the default set of recomputable ops. + Currently, there are two options: ``nvfuser`` and ``inductor``. + recomputable_ops: This is an optional set of recomputable ops. If this + is not None, then this set of ops will be used instead of the + default set of ops. + num_fwd_outputs: The number of outputs from the forward graph. + + Returns: + Returns the generated forward and backward Fx graph modules. + """ + + joint_module.graph.eliminate_dead_code() + joint_module.recompile() + + fx_g = joint_module.graph + + # add the CSE pass + if config.cse: + cse_graph = fx_graph_cse(fx_g) + joint_module.graph = cse_graph + joint_graph = joint_module.graph + + graph_has_recomputable_ops = has_recomputable_ops(joint_module) + graph_has_recomputable_rng_ops = has_recomputable_rng_ops(joint_module) + if graph_has_recomputable_ops: + joint_module = cleanup_recompute_tags(joint_module, is_default_partition=False) + if not config.unsafe_allow_optimization_of_collectives: + force_save_collectives(joint_module) + + force_save_effectful_ops(joint_module) + force_save_bw_mutation_src(joint_module) + + if static_lifetime_input_indices is None: + static_lifetime_input_indices = [] + node_info = classify_nodes( + joint_module, static_lifetime_input_indices, num_fwd_outputs + ) + + # networkx blows up on graphs with no required backward nodes + # Since there's nothing to partition anyway, and the default partitioner can "handle" + # this case, send our graph over to the default partitioner. + if len(node_info.required_bw_nodes) == 0: + return default_partition( + joint_module, + _joint_inputs, + num_fwd_outputs=num_fwd_outputs, + static_lifetime_input_indices=static_lifetime_input_indices, + static_lifetime_input_nodes=node_info.static_lifetime_input_nodes, + ) + + for node in reversed(joint_module.graph.nodes): + if node.op == "output": + node.dist_from_bw = int(1e9) + elif not node_info.is_required_fw(node): + node.dist_from_bw = 0 + else: + node.dist_from_bw = int(1e9) + for user in node.users: + node.dist_from_bw = min(node.dist_from_bw, user.dist_from_bw + 1) + + memory_budget = config.activation_memory_budget + for node in joint_graph.nodes: + if isinstance(node.meta.get("memory_budget", None), float): + memory_budget = node.meta["memory_budget"] + break + saved_values = choose_saved_values_set( + joint_graph, + node_info, + memory_budget=memory_budget, + ) + # pyrefly: ignore [unbound-name] + if config._sync_decision_cross_ranks: + saved_values = _sync_decision_cross_ranks(joint_graph, saved_values) + + # save_for_backward on tensors and stashes symints in autograd .ctx + # Skip SymBool nodes whose only consumers are _assert_scalar calls. + # These are runtime assertion intermediates and are not needed in backward + # for any real computation. + def _is_assert_only_symbool(n: fx.Node) -> bool: + return ( + isinstance(n.meta.get("val"), torch.SymBool) + and len(n.users) > 0 + and all(u.target is torch.ops.aten._assert_scalar.default for u in n.users) + ) + + saved_sym_nodes = list( + filter( + lambda n: is_sym_node(n) and not _is_assert_only_symbool(n), saved_values + ) + ) + saved_opaque_nodes = list(filter(is_opaque_node, saved_values)) + saved_values = list( + filter(lambda n: not is_sym_node(n) and not is_opaque_node(n), saved_values) + ) + + # NB: saved_sym_nodes will be mutated to reflect the actual saved symbols + fw_module, bw_module = _extract_fwd_bwd_modules( + joint_module, + saved_values, + # pyrefly: ignore [bad-argument-type] + saved_sym_nodes=saved_sym_nodes, + saved_opaque_nodes=saved_opaque_nodes, + num_fwd_outputs=num_fwd_outputs, + static_lifetime_input_nodes=node_info.static_lifetime_input_nodes, + ) + if graph_has_recomputable_ops: + if graph_has_recomputable_rng_ops: + fw_module, bw_module = functionalize_rng_ops( + joint_module, fw_module, bw_module, len(saved_sym_nodes) + ) + bw_module = reordering_to_mimic_autograd_engine(bw_module) + + # pyrefly: ignore [unbound-name] + if config.enable_activation_offloading: + from ._activation_offloading.activation_offloading import ( + enable_activation_offloading, + ) + + enable_activation_offloading( + fw_module, + bw_module, + num_fwd_outputs, + node_info.static_lifetime_input_nodes, + ) + + # raise all getitem ops to as early as possible + # this is helpful for memory, especially in the case of aot_eager backend + fw_module = raise_getitems(fw_module) + bw_module = raise_getitems(bw_module) + + fw_module = thread_graphsafe_rng_from_hops(fw_module, is_backward=False) + bw_module = thread_graphsafe_rng_from_hops(bw_module, is_backward=True) + + if AOT_PARTITIONER_DEBUG: + # Calculate sorted sizes of saved values + sorted_sizes = sorted([(_size_of(i), str(i)) for i in saved_values]) + + # Log total theoretical activations stored + total_activations_size_gb = sum(_size_of(i) for i in saved_values) / 1e9 + log.info("Theoretical Activations Stored: %.2f GB", total_activations_size_gb) + + # Log theoretical per activation storage sizes + log.info("Theoretical Per Activation Storage Sizes: %s", sorted_sizes) + fw_module_nodes = OrderedSet( + node.name for node in fw_module.graph.nodes if node.op == "call_function" + ) + bw_module_nodes = OrderedSet( + node.name for node in bw_module.graph.nodes if node.op == "call_function" + ) + remat_nodes = fw_module_nodes & bw_module_nodes + + counts: dict[str, int] = defaultdict(int) + for node in fw_module.graph.nodes: + if node.name in remat_nodes and hasattr(node.target, "_overloadpacket"): + counts[str(node.target._overloadpacket)] += 1 + log.info( + "# remat/fw/bw: %d/%d/%d", + len(remat_nodes), + len(fw_module_nodes), + len(bw_module_nodes), + ) + rematerialized_ops = sorted( + counts.items(), key=operator.itemgetter(1), reverse=True + ) + log.info("Count of Ops Rematerialized: %s", rematerialized_ops) + return fw_module, bw_module + + +def draw_graph( + traced: torch.fx.GraphModule, + fname: str, + figname: str = "fx_graph", + clear_meta: bool = True, + prog: str | list[str] | None = None, + parse_stack_trace: bool = False, + dot_graph_shape: str | None = None, +) -> None: + if clear_meta: + new_graph = copy.deepcopy(traced.graph) + traced = fx.GraphModule(traced, new_graph) + for node in traced.graph.nodes: + node.meta = {} # pyrefly: ignore[implicit-any] + base, ext = os.path.splitext(fname) + if not ext: + ext = "." + config.torch_compile_graph_format + log.info("Writing FX graph to file: %s%s", base, ext) + g = graph_drawer.FxGraphDrawer( + traced, + figname, + parse_stack_trace=parse_stack_trace, + dot_graph_shape=dot_graph_shape, + ) + x = g.get_main_dot_graph() + write_method = getattr(x, "write_" + ext.lstrip(".")) + fname = f"{base}{ext}" + if prog is None: + write_method(fname) + else: + write_method(fname, prog=prog) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/predispatch.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/predispatch.py new file mode 100644 index 0000000000000000000000000000000000000000..ffa66ad8c2b74251fd5d5814919628a56d1420cd --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/predispatch.py @@ -0,0 +1,169 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +""" +This module contains pre-dispatch wrappers for functorch operations +that enable proper tracing in PT2 non-strict export/compile fx graph. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch +from torch._C._functorch import ( + _add_batch_dim as _add_batch_dim_impl, + _remove_batch_dim as _remove_batch_dim_impl, + _vmap_decrement_nesting as _vmap_decrement_nesting_impl, + _vmap_increment_nesting as _vmap_increment_nesting_impl, +) + + +if TYPE_CHECKING: + import threading + + +def _add_batch_dim(self: torch.Tensor, batch_dim: int, level: int) -> torch.Tensor: + """ + Thin wrapper around torch._C._add_batch_dim that is used to proxy in + PT2 export/compile fx graph + """ + from torch._export.utils import _maybe_find_pre_dispatch_tf_mode_for_export + + mode = _maybe_find_pre_dispatch_tf_mode_for_export() + batch_dim = self.ndim + batch_dim if batch_dim < 0 else batch_dim + + if mode: + return torch.overrides.handle_torch_function( + _add_batch_dim, (self,), self, batch_dim, level + ) + + res = _add_batch_dim_impl(self, batch_dim, level) + return res + + +def _remove_batch_dim( + self: torch.Tensor, level: int, batch_size: int, out_dim: int +) -> torch.Tensor: + """ + Thin wrapper around torch._C._remove_batch_dim that is used to proxy in + PT2 export/compile fx graph + """ + from torch._export.utils import _maybe_find_pre_dispatch_tf_mode_for_export + + mode = _maybe_find_pre_dispatch_tf_mode_for_export() + + if mode: + return torch.overrides.handle_torch_function( + _remove_batch_dim, (self,), self, level, batch_size, out_dim + ) + + res = _remove_batch_dim_impl(self, level, batch_size, out_dim) + return res + + +def _vmap_increment_nesting(batch_size: int, randomness: str) -> int: + """ + Thin wrapper around torch._C._vmap_increment_nesting that is used + to proxy in export/compile graph + """ + from torch._export.utils import _maybe_find_pre_dispatch_tf_mode_for_export + + mode = _maybe_find_pre_dispatch_tf_mode_for_export() + + if mode: + return torch.overrides.handle_torch_function( + _vmap_increment_nesting, (batch_size,), batch_size, randomness + ) + res = _vmap_increment_nesting_impl(batch_size, randomness) + return res + + +def _vmap_decrement_nesting() -> int: + """ + Thin wrapper around torch._C._vmap_increment_nesting that is used + to proxy in export/compile graph + """ + from torch._export.utils import _maybe_find_pre_dispatch_tf_mode_for_export + + mode = _maybe_find_pre_dispatch_tf_mode_for_export() + + if mode: + return torch.overrides.handle_torch_function( + _vmap_decrement_nesting, + (), + ) + return _vmap_decrement_nesting_impl() + + +# Global variables for lazy_load_decompositions +DECOMPOSITIONS_LOADED: bool = False +DECOMPOSITIONS_LOCK: threading.Lock | None = None +VMAP_DECOMPOSITIONS_LIB: torch.library.Library | None = None + + +def lazy_load_decompositions() -> None: + """ + Lazy loading of vmap decompositions with pre-dispatch support. + """ + from torch._export.utils import _maybe_find_pre_dispatch_tf_mode_for_export + + mode = _maybe_find_pre_dispatch_tf_mode_for_export() + + if mode: + return torch.overrides.handle_torch_function(lazy_load_decompositions, ()) + + global DECOMPOSITIONS_LOADED, DECOMPOSITIONS_LOCK, VMAP_DECOMPOSITIONS_LIB + + if DECOMPOSITIONS_LOADED: + return + + # Initialize lock if needed + if DECOMPOSITIONS_LOCK is None: + import threading + + DECOMPOSITIONS_LOCK = threading.Lock() + + with DECOMPOSITIONS_LOCK: + if DECOMPOSITIONS_LOADED: + return + + import os + + if not (os.environ.get("PYTORCH_JIT", "1") == "1" and __debug__): + DECOMPOSITIONS_LOADED = True + return + + # use an alternate way to register an operator into the decomposition table + # _register_jit_decomposition doesn't work for some operators, e.g. addr, + # because the Tensor types generated cannot be unioned by torchscript + # decomp should be type OpOverload + VMAP_DECOMPOSITIONS_LIB = torch.library.Library( + "aten", "IMPL", "FuncTorchBatched" + ) + + from torch._decomp import decomposition_table + + def _register_python_decomposition_vmap(decomp: torch._ops.OpOverload) -> None: + if VMAP_DECOMPOSITIONS_LIB is None: + raise AssertionError("VMAP_DECOMPOSITIONS_LIB must not be None") + if decomp in decomposition_table: + VMAP_DECOMPOSITIONS_LIB.impl(decomp, decomposition_table[decomp]) + else: + raise RuntimeError(f"could not find decomposition for {decomp}") + + _register_python_decomposition_vmap(torch.ops.aten.mse_loss_backward.default) + _register_python_decomposition_vmap( + torch.ops.aten.smooth_l1_loss_backward.default + ) + _register_python_decomposition_vmap(torch.ops.aten.huber_loss_backward.default) + _register_python_decomposition_vmap(torch.ops.aten.nll_loss_forward.default) + _register_python_decomposition_vmap(torch.ops.aten.nll_loss2d_forward.default) + _register_python_decomposition_vmap(torch.ops.aten.nll_loss_backward.default) + _register_python_decomposition_vmap(torch.ops.aten.nll_loss2d_backward.default) + _register_python_decomposition_vmap(torch.ops.aten.addr.default) + + DECOMPOSITIONS_LOADED = True diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/pyfunctorch.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/pyfunctorch.py new file mode 100644 index 0000000000000000000000000000000000000000..f3b09a6adb06bd1a50ce44fafaa76afc83f21f9e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/pyfunctorch.py @@ -0,0 +1,338 @@ +from __future__ import annotations + +import contextlib +from abc import ABC, abstractmethod +from functools import cached_property +from typing import Any, TYPE_CHECKING + +import torch +import torch.utils._pytree as pytree +from torch._C._functorch import ( + CFunctionalizeInterpreterPtr, + CGradInterpreterPtr, + CInterpreter, + CJvpInterpreterPtr, + CVmapInterpreterPtr, + pop_dynamic_layer_stack, + push_dynamic_layer_stack, + RandomnessType, + TransformType, +) +from torch.autograd.forward_ad import _set_fwd_grad_enabled + + +if TYPE_CHECKING: + from collections.abc import Generator + + +""" +This file contains the functorch integration with PyDispatcher. + +PyDispatcher does not understand functorch's DynamicLayerStack dispatching +logic because it is entirely implemented in C++ in the fallbacks for two +dispatch keys, FuncTorchDynamicLayer{Front, Back}Mode (PyDispatcher is unable +to directly reuse C++ boxed fallbacks). + +Instead of trying to hammer PyDispatcher into understanding those fallbacks, +we re-implement the logic of peeking the top of the stack for an interpreter, +selecting the interpreter to dispatch on, etc, in Python. This leads to a +simpler design. + +The main difference between C++ functorch and PyDispatcher's functorch logic +is that: +- C++ functorch needs to manually tweak dispatch keys to ping-pong between + DynamicLayerFrontMode and DynamicLayerBackMode. +- PyDispatcher's functorch logic pops an Interpreter from the top of the stack + and asks it to execute the rule associated with the Interpreter. + +In C++ we do the ping-pong because e.g. vmap rules are associated with the +batched DispatchKey, but in PyDispatcher we are able to avoid this by asking +the user to register a batching rule directly to a transform that an +interpreter then invokes. +""" + + +# FuncTorchInterpreter is the Python version of Interpreter (recall that +# the DynamicLayerStack is a stack of interpreters). +# It is a wrapper around the actual C++ Interpreter object. +# +# Keep the methods in sync with aten/src/ATen/functorch/Interpreter.h +class FuncTorchInterpreter(ABC): + def __init__(self, cptr: Any) -> None: + self._cptr = cptr + + # Process an operation. eg for vmap, this is invoking a batching rule. + # Conceptually this is analogous to Interpreter::process in C++ + @abstractmethod + def process(self, op: Any, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: + pass + + # lower an operation from this Interpreter to the next Interpreter on the stack. + # Concretely, this involves temporarily popping the current Interpreter. + # Conceptually this is analogous to Interpreter::sendToNextInterpreter in C++ + def lower(self) -> contextlib.AbstractContextManager[Any]: + return temporarily_pop_interpreter_stack() + + def level(self) -> int: + return self._cptr.level() + + def key(self) -> TransformType: + return self._cptr.key() + + def get_state(self) -> tuple[Any, ...]: + raise NotImplementedError + + def check_state(self, state: tuple[Any, ...]) -> bool: + return state == self.get_state() + + def __getstate__(self) -> dict[str, Any]: + state = self.__dict__.copy() + state.pop("_cptr", None) + return state + + +@contextlib.contextmanager +def temporarily_pop_interpreter_stack() -> Generator[None, None, None]: + try: + saved = pop_dynamic_layer_stack() + yield + finally: + push_dynamic_layer_stack(saved) + + +@contextlib.contextmanager +def temporarily_clear_interpreter_stack() -> Generator[list[Any], None, None]: + stack: list[Any] = [] + try: + while torch._C._functorch.peek_interpreter_stack() is not None: + stack.append(pop_dynamic_layer_stack()) + yield list(stack) + finally: + while stack: + push_dynamic_layer_stack(stack.pop()) + + +@contextlib.contextmanager +def temporarily_restore_interpreter_stack( + stack: list[Any] | None, +) -> Generator[None, None, None]: + pushed: list[Any] = [] + if stack is None: + return + try: + for s in reversed(stack): + push_dynamic_layer_stack(s) + pushed.append(s) + yield + finally: + for _ in reversed(pushed): + # TODO: would be nice to assert that the layers are the same, but + # Python object identity is not preserved + pop_dynamic_layer_stack() + + +class VmapInterpreter(FuncTorchInterpreter): + def __init__(self, cdata: CInterpreter) -> None: + if cdata.key() != TransformType.Vmap: + raise AssertionError(f"expected TransformType.Vmap, got {cdata.key()}") + # NOTE: [Interpreter cdata vs cptr] + # cdata is a generic CInterpreter. We wrap it in a CVmapInterpreterPtr + # so that we can access methods specific to the vmap interpreter + self._cdata = cdata + + @cached_property + # pyrefly: ignore [bad-override] + def _cptr(self) -> CVmapInterpreterPtr: + return CVmapInterpreterPtr(self._cdata) + + def process(self, op: Any, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: + kernel = op.functorch_table[TransformType.Vmap] + return kernel(self, *args, **kwargs) + + def batch_size(self) -> int: + return self._cptr.batchSize() + + def randomness(self) -> str: + typ = self._cptr.randomness() + if typ == RandomnessType.Error: + return "error" + elif typ == RandomnessType.Same: + return "same" + elif typ == RandomnessType.Different: + return "different" + raise RuntimeError(f"Unknown RandomnessType: {typ}") + + def get_state(self) -> tuple[Any, ...]: + return (self.key().name, self.level(), self.randomness()) + + +@contextlib.contextmanager +def nested( + *contexts: contextlib.AbstractContextManager[Any], +) -> Generator[tuple[contextlib.AbstractContextManager[Any], ...], None, None]: + with contextlib.ExitStack() as stack: + for ctx in contexts: + stack.enter_context(ctx) + yield contexts + + +class GradInterpreter(FuncTorchInterpreter): + def __init__(self, cdata: CInterpreter) -> None: + if cdata.key() != TransformType.Grad: + raise AssertionError(f"expected TransformType.Grad, got {cdata.key()}") + # See NOTE: [Interpreter cdata vs cptr] + self._cdata = cdata + + @cached_property + # pyrefly: ignore [bad-override] + def _cptr(self) -> CGradInterpreterPtr: + return CGradInterpreterPtr(self._cdata) + + def lift( + self, args: tuple[Any, ...], kwargs: dict[str, Any] + ) -> tuple[tuple[Any, ...], dict[str, Any]]: + args, kwargs = pytree.tree_map_only( + torch.Tensor, self._cptr.lift, [args, kwargs] + ) + return args, kwargs + + def process(self, op: Any, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: + kernel = op.functorch_table[TransformType.Grad] + args, kwargs = self.lift(args, kwargs) + return kernel(self, *args, **kwargs) + + # GradInterpreter has custom lower because of the no_grad interaction + # See NOTE [grad and vjp interaction with no_grad] + # This logic is mirrored from C++ GradInterpreterPtr::sendToNextInterpreter + def lower(self) -> contextlib.AbstractContextManager[Any]: + prev_grad_mode = self.prev_grad_mode() + if not prev_grad_mode: + return nested(torch.no_grad(), super().lower()) + return super().lower() + + def prev_grad_mode(self) -> bool: + return self._cptr.prevGradMode() + + def get_state(self) -> tuple[Any, ...]: + return (self.key().name, self.level(), self.prev_grad_mode()) + + +class JvpInterpreter(FuncTorchInterpreter): + def __init__(self, cdata: CInterpreter) -> None: + if cdata.key() != TransformType.Jvp: + raise AssertionError(f"expected TransformType.Jvp, got {cdata.key()}") + # See NOTE: [Interpreter cdata vs cptr] + self._cdata = cdata + + @cached_property + # pyrefly: ignore [bad-override] + def _cptr(self) -> CJvpInterpreterPtr: + return CJvpInterpreterPtr(self._cdata) + + def lift( + self, args: tuple[Any, ...], kwargs: dict[str, Any] + ) -> tuple[tuple[Any, ...], dict[str, Any]]: + args, kwargs = pytree.tree_map_only( + torch.Tensor, self._cptr.lift, [args, kwargs] + ) + return args, kwargs + + def process(self, op: Any, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: + kernel = op.functorch_table[TransformType.Jvp] + args, kwargs = self.lift(args, kwargs) + return kernel(self, *args, **kwargs) + + # Jvp has custom lower because of the no_fwd_grad interaction + # See NOTE [grad and vjp interaction with no_grad] for related info. + # This logic is mirrored from C++ JvpInterpreterPtr::sendToNextInterpreter + def lower(self) -> contextlib.AbstractContextManager[Any]: + prev_fwd_grad_mode = self.prev_fwd_grad_mode() + if not prev_fwd_grad_mode: + return nested(_set_fwd_grad_enabled(False), super().lower()) + return super().lower() + + def prev_fwd_grad_mode(self) -> bool: + return self._cptr.prevFwdGradMode() + + def get_state(self) -> tuple[Any, ...]: + return (self.key().name, self.level(), self.prev_fwd_grad_mode()) + + +class FunctionalizeInterpreter(FuncTorchInterpreter): + def __init__(self, cdata: CInterpreter) -> None: + if cdata.key() != TransformType.Functionalize: + raise AssertionError( + f"expected TransformType.Functionalize, got {cdata.key()}" + ) + self._cdata = cdata + + @cached_property + # pyrefly: ignore [bad-override] + def _cptr(self) -> CFunctionalizeInterpreterPtr: + return CFunctionalizeInterpreterPtr(self._cdata) + + def process(self, op: Any, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: + kernel = op.functorch_table[TransformType.Functionalize] + return kernel(self, *args, **kwargs) + + def functionalize_add_back_views(self) -> bool: + return self._cptr.functionalizeAddBackViews() + + def get_state(self) -> tuple[Any, ...]: + return (self.key().name, self.level()) + + +def coerce_cinterpreter(cinterpreter: CInterpreter) -> FuncTorchInterpreter: + key = cinterpreter.key() + if key == TransformType.Grad: + return GradInterpreter(cinterpreter) + if key == TransformType.Vmap: + return VmapInterpreter(cinterpreter) + if key == TransformType.Jvp: + return JvpInterpreter(cinterpreter) + if key == TransformType.Functionalize: + return FunctionalizeInterpreter(cinterpreter) + raise RuntimeError(f"NYI: PyDispatcher has not implemented support for {key}") + + +def retrieve_current_functorch_interpreter() -> FuncTorchInterpreter: + interpreter = torch._C._functorch.peek_interpreter_stack() + if interpreter is None: + raise AssertionError("interpreter must not be None") + return coerce_cinterpreter(interpreter) + + +def retrieve_all_functorch_interpreters() -> list[FuncTorchInterpreter]: + cis = torch._C._functorch.get_interpreter_stack() + if cis is None: + return [] + return [coerce_cinterpreter(ci) for ci in cis] + + +def compare_functorch_state(states: list[tuple[Any, ...]]) -> bool: + # There are four possible cases covered here: + # 1. Current stack empty AND stack when generated not empty -> Invalidate + # 2. Current stack not empty AND stack when generated empty -> Invalidate + # 3. Current stack and generated stack empty -> Valid FX graph + # 4. Current stack and generated stack not empty -> Valid if both states match + peek = torch._C._functorch.peek_interpreter_stack() + if (peek is None and len(states) != 0) or (peek is not None and len(states) == 0): + return False + + cis = retrieve_all_functorch_interpreters() + return len(cis) == len(states) and all( + ci.check_state(state) for ci, state in zip(cis, states) + ) + + +def dispatch_functorch(op: Any, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: + interpreter = retrieve_current_functorch_interpreter() + # In traditional PyTorch operators, DispatchKey::FuncTorchTensorWrapper's + # unwrap_dead_tensors fallback handles unwrapping dead tensor wrappers. + # PyDispatcher sidesteps the PyTorch dispatcher when dealing with functorch + # transforms, so we manually unwrap the dead tensors here. + # This logic won't need to exist when we have mode-only functorch. + args, kwargs = pytree.tree_map_only( + torch.Tensor, torch._C._functorch.unwrap_if_dead, (args, kwargs) + ) + return interpreter.process(op, args, kwargs) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/python_key.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/python_key.py new file mode 100644 index 0000000000000000000000000000000000000000..557334f68928a057a6a9e036c904c8c0bd3231c1 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/python_key.py @@ -0,0 +1,15 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +__all__ = ["make_fx", "dispatch_trace", "PythonKeyTracer", "pythonkey_decompose"] +from torch.fx.experimental.proxy_tensor import ( + decompose, + dispatch_trace, + make_fx, + PythonKeyTracer, +) + + +pythonkey_decompose = decompose diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/pytree_hacks.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/pytree_hacks.py new file mode 100644 index 0000000000000000000000000000000000000000..96dea7ad100705ae53139aa5ae729fd2206182af --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/pytree_hacks.py @@ -0,0 +1,23 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +import warnings + +# TODO: remove this file when the migration of the pytree utility is done +from torch.utils._pytree import tree_map_, treespec_pprint + + +__all__ = ["tree_map_", "treespec_pprint"] + + +with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "`torch._functorch.pytree_hacks` is deprecated and will be removed in a future release. " + "Please `use torch.utils._pytree` instead.", + DeprecationWarning, + stacklevel=2, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/top_operators_github_usage.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/top_operators_github_usage.py new file mode 100644 index 0000000000000000000000000000000000000000..0b7eb7449e8a554774049f2c70f73e1a7ed21793 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/top_operators_github_usage.py @@ -0,0 +1,631 @@ +from __future__ import annotations + + +""" +From https://docs.google.com/spreadsheets/d/12R3nCOLskxPYjjiNkdqy4OdQ65eQp_htebXGODsjSeA/edit#gid=0 +Try to keep this list in sync with that. +""" + +import operator + + +top_torch: list[tuple[str, int]] = [ + ("t", 6837449), + ("tensor", 585786), + ("mode", 462182), + ("cat", 394818), + ("max", 368038), + ("zeros", 329495), + ("load", 327756), + ("no_grad", 294694), + ("save", 265130), + ("from_numpy", 243063), + ("manual_seed", 165044), + ("ones", 153696), + ("randn", 150796), + ("stack", 133358), + ("sum", 130772), + ("arange", 98087), + ("rand", 94715), + ("mean", 88546), + ("exp", 73883), + ("zeros_like", 72831), + ("min", 72248), + ("sigmoid", 66798), + ("log", 62135), + ("matmul", 47811), + ("clamp", 45304), + ("sqrt", 44911), + ("abs", 43535), + ("tanh", 42793), + ("empty", 40311), + ("argmax", 38435), + ("bmm", 33984), + ("pow", 33571), + ("norm", 31125), + ("mm", 30995), + ("is_tensor", 29546), + ("ones_like", 29512), + ("nonzero", 28681), + ("full", 28373), + ("unsqueeze", 27911), + ("where", 26585), + ("randperm", 26450), + ("eye", 24342), + ("mul", 23236), + ("topk", 22537), + ("as_tensor", 21967), + ("sort", 21412), + ("squeeze", 20863), + ("randint", 20771), + ("linspace", 20041), + ("add", 19201), + ("transpose", 18663), + ("split", 18325), + ("gather", 17904), + ("set_grad_enabled", 16013), + ("sin", 15669), + ("cos", 15562), + ("div", 15513), + ("index_select", 14866), + ("multinomial", 14331), + ("flatten", 14267), + ("isnan", 14170), + ("randn_like", 13096), + ("eq", 12680), + ("einsum", 12480), + ("round", 12367), + ("floor", 11628), + ("allclose", 11000), + ("reshape", 10605), + ("diag", 10167), + ("chunk", 9581), + ("std", 9379), + ("set_default_tensor_type", 9281), + ("triu", 8559), + ("meshgrid", 8292), + ("set_num_threads", 8126), + ("unique", 7964), + ("full_like", 7780), + ("tril", 7538), + ("dot", 7275), + ("sign", 6943), + ("equal", 6916), + ("normal", 6750), + ("cumsum", 6556), + ("dist", 6058), + ("isfinite", 6030), + ("gt", 5935), + ("set_printoptions", 5888), + ("range", 5491), + ("empty_like", 5351), + ("flip", 5342), + ("masked_select", 5341), + ("bernoulli", 5262), + ("atan", 5253), + ("var", 5247), + ("prod", 5200), + ("erf", 5088), + ("inverse", 5072), + ("addmm", 4854), + ("logsumexp", 4582), + ("fft", 4436), + ("lt", 4421), + ("log2", 4316), + ("enable_grad", 4238), + ("rand_like", 4187), + ("argsort", 3972), + ("seed", 3932), + ("mv", 3547), + ("ger", 3309), + ("ge", 3248), + ("atan2", 3210), + ("ceil", 3202), + ("ne", 3075), + ("bincount", 3063), + ("acos", 3055), + ("rsqrt", 3031), + ("svd", 3029), + ("numel", 3003), + ("log1p", 2840), + ("unbind", 2808), + ("le", 2714), + ("isinf", 2707), + ("cross", 2646), + ("set_default_dtype", 2536), + ("argmin", 2535), + ("sparse_coo_tensor", 2489), + ("log10", 2304), + ("kthvalue", 2192), + ("set_rng_state", 2158), + ("get_rng_state", 1996), + ("get_default_dtype", 1879), + ("det", 1868), + ("qr", 1864), + ("histc", 1852), + ("symeig", 1832), + ("trace", 1801), + ("median", 1795), + ("addcmul", 1751), + ("remainder", 1717), + ("baddbmm", 1693), + ("lgamma", 1665), + ("repeat_interleave", 1598), + ("fmod", 1576), + ("reciprocal", 1575), + ("tan", 1560), + ("initial_seed", 1532), + ("take", 1529), + ("stft", 1487), + ("get_num_threads", 1477), + ("real", 1459), + ("cholesky", 1406), + ("quantize_per_tensor", 1392), + ("diag_embed", 1364), + ("lerp", 1363), + ("asin", 1345), + ("eig", 1333), + ("trunc", 1290), + ("diagonal", 1287), + ("cosh", 1279), + ("rfft", 1269), + ("cumprod", 1260), + ("addr", 1211), + ("roll", 1198), + ("narrow", 1188), + ("digamma", 1172), + ("square", 1163), + ("sinh", 1131), + ("logspace", 1084), + ("broadcast_tensors", 1070), + ("irfft", 1013), + ("frac", 997), + ("hann_window", 994), + ("solve", 989), + ("logdet", 977), + ("expm1", 968), + ("cdist", 946), + ("addmv", 903), + ("randint_like", 888), + ("tensordot", 888), + ("ifft", 877), + ("true_divide", 854), + ("erfinv", 830), + ("addcdiv", 819), + ("addbmm", 813), + ("renorm", 781), + ("pinverse", 753), + ("isclose", 740), + ("erfc", 729), + ("is_storage", 725), + ("triangular_solve", 723), + ("rot90", 709), + ("logical_not", 686), + ("geqrf", 681), + ("slogdet", 677), + ("lu", 665), + ("hamming_window", 659), + ("orgqr", 651), + ("ormqr", 622), + ("is_floating_point", 602), + ("diagflat", 562), + ("cholesky_solve", 559), + ("tril_indices", 552), + ("chain_matmul", 551), + ("triu_indices", 548), + ("angle", 522), + ("poisson", 505), + ("matrix_power", 485), + ("unique_consecutive", 471), + ("quantize_per_channel", 465), + ("std_mean", 458), + ("bartlett_window", 447), + ("var_mean", 428), + ("lstsq", 421), + ("logical_and", 419), + ("mvlgamma", 411), + ("blackman_window", 400), + ("bitwise_not", 395), + ("cholesky_inverse", 388), + ("as_strided", 384), + ("floor_divide", 353), + ("cartesian_prod", 321), + ("lu_solve", 317), + ("set_flush_denormal", 310), + ("empty_strided", 283), + ("logical_xor", 282), + ("polygamma", 282), + ("logical_or", 280), + ("set_num_interop_threads", 278), + ("combinations", 274), + ("trapz", 270), + ("matrix_rank", 260), + ("lu_unpack", 255), + ("result_type", 244), + ("conj", 231), + ("cummax", 230), + ("lobpcg", 229), + ("bitwise_xor", 217), + ("promote_types", 213), + ("get_num_interop_threads", 211), + ("cummin", 205), + ("bitwise_and", 198), + ("dequantize", 192), + ("bitwise_or", 191), + ("imag", 191), + ("can_cast", 184), + ("istft", 180), + ("compiled_with_cxx11_abi", 159), + ("is_complex", 151), + ("block_diag", 136), + ("pca_lowrank", 124), + ("absolute", 122), + ("svd_lowrank", 108), + ("neg", 2), +] + +top_nn_functional: list[tuple[str, int]] = [ + ("nn.functional.softmax", 10522), + ("nn.functional.relu", 8572), + ("nn.functional.interpolate", 7277), + ("nn.functional.pad", 5207), + ("nn.functional.log_softmax", 4699), + ("nn.functional.normalize", 2338), + ("nn.functional.cross_entropy", 2083), + ("nn.functional.grid_sample", 1970), + ("nn.functional.one_hot", 1967), + ("nn.functional.mse_loss", 1920), + ("nn.functional.conv2d", 1593), + ("nn.functional.dropout", 1516), + ("nn.functional.softplus", 1385), + ("nn.functional.sigmoid", 1128), + ("nn.functional.linear", 1036), + ("nn.functional.gelu", 930), + ("nn.functional.avg_pool2d", 899), + ("nn.functional.max_pool2d", 876), + ("nn.functional.nll_loss", 863), + ("nn.functional.embedding", 737), + ("nn.functional.tanh", 664), + ("nn.functional.leaky_relu", 640), + ("nn.functional.adaptive_avg_pool2d", 633), + ("nn.functional.cosine_similarity", 627), + ("nn.functional.unfold", 609), + ("nn.functional.conv1d", 596), + ("nn.functional.binary_cross_entropy_with_logits", 591), + ("nn.functional.l1_loss", 571), + ("nn.functional.binary_cross_entropy", 492), + ("nn.functional.elu", 416), + ("nn.functional.batch_norm", 413), + ("nn.functional.upsample", 413), + ("nn.functional.fold", 305), + ("nn.functional.affine_grid", 298), + ("nn.functional.max_pool1d", 297), + ("nn.functional.torch", 294), + ("nn.functional.threshold", 263), + ("nn.functional.smooth_l1_loss", 262), + ("nn.functional.pairwise_distance", 253), + ("nn.functional.logsigmoid", 243), + ("nn.functional.adaptive_max_pool2d", 235), + ("nn.functional.relu6", 213), + ("nn.functional.pixel_shuffle", 209), + ("nn.functional.avg_pool3d", 203), + ("nn.functional.bilinear", 203), + ("nn.functional.conv_transpose2d", 201), + ("nn.functional.gumbel_softmax", 197), + ("nn.functional.max_unpool2d", 196), + ("nn.functional.kl_div", 191), + ("nn.functional.hardtanh", 189), + ("nn.functional.ctc_loss", 185), + ("nn.functional.layer_norm", 178), + ("nn.functional.conv3d", 172), + ("nn.functional.max_unpool3d", 167), + ("nn.functional.hardshrink", 165), + ("nn.functional.hardswish", 156), + ("nn.functional.selu", 156), + ("nn.functional.glu", 155), + ("nn.functional.assert_int_or_pair", 150), + ("nn.functional.hardsigmoid", 146), + ("nn.functional.upsample_bilinear", 146), + ("nn.functional.max_pool3d", 140), + ("nn.functional.adaptive_avg_pool3d", 139), + ("nn.functional.instance_norm", 124), + ("nn.functional.embedding_bag", 122), + ("nn.functional.upsample_nearest", 110), + ("nn.functional.avg_pool1d", 105), + ("nn.functional.prelu", 102), + ("nn.functional.celu", 92), + ("nn.functional.dropout2d", 86), + ("nn.functional.hinge_embedding_loss", 82), + ("nn.functional.softsign", 81), + ("nn.functional.max_unpool1d", 74), + ("nn.functional.silu", 74), + ("nn.functional.softshrink", 70), + ("nn.functional.leaky_relu_", 68), + ("nn.functional.softmin", 67), + ("nn.functional.channel_shuffle", 66), + ("nn.functional.multilabel_margin_loss", 66), + ("nn.functional.dropout3d", 65), + ("nn.functional.multi_margin_loss", 65), + ("nn.functional.lp_pool2d", 64), + ("nn.functional.conv_transpose1d", 62), + ("nn.functional.triplet_margin_loss", 62), + ("nn.functional.tanhshrink", 61), + ("nn.functional.adaptive_max_pool1d", 59), + ("nn.functional.cosine_embedding_loss", 58), + ("nn.functional.multi_head_attention_forward", 58), + ("nn.functional.max_pool1d_with_indices", 53), + ("nn.functional.poisson_nll_loss", 53), + ("nn.functional.margin_ranking_loss", 52), + ("nn.functional.soft_margin_loss", 52), + ("nn.functional.adaptive_max_pool3d", 51), + ("nn.functional.group_norm", 51), + ("nn.functional.local_response_norm", 51), + ("nn.functional.multilabel_soft_margin_loss", 51), + ("nn.functional.relu_", 50), + ("nn.functional.alpha_dropout", 49), + ("nn.functional.feature_alpha_dropout", 49), + ("nn.functional.lp_pool1d", 49), + ("nn.functional.adaptive_max_pool1d_with_indices", 48), + ("nn.functional.adaptive_max_pool2d_with_indices", 48), + ("nn.functional.adaptive_max_pool3d_with_indices", 48), + ("nn.functional.fractional_max_pool2d", 48), + ("nn.functional.fractional_max_pool2d_with_indices", 48), + ("nn.functional.fractional_max_pool3d", 48), + ("nn.functional.fractional_max_pool3d_with_indices", 48), + ("nn.functional.max_pool2d_with_indices", 48), + ("nn.functional.max_pool3d_with_indices", 48), + ("nn.functional.handle_torch_function", 47), + ("nn.functional.has_torch_function", 47), + ("nn.functional.adaptive_avg_pool1d", 43), + ("nn.functional.pdist", 43), + ("nn.functional.rrelu_", 37), + ("nn.functional.elu_", 34), + ("nn.functional.boolean_dispatch", 33), + ("nn.functional.hardtanh_", 26), + ("nn.functional.triplet_margin_with_distance_loss", 23), + ("nn.functional.selu_", 20), + ("nn.functional.pixel_unshuffle", 19), + ("nn.functional.conv_transpose3d", 18), + ("nn.functional.gaussian_nll_loss", 15), + ("nn.functional.has_torch_function_unary", 15), + ("nn.functional.has_torch_function_variadic", 15), + ("nn.functional.celu_", 13), + ("nn.functional.huber_loss", 7), + ("nn.functional.mish", 4), + ("nn.functional.threshold_", 3), + ("nn.functional.grad", 2), + ("nn.functional.conv_tbc", 1), + ("nn.functional.math", 1), +] + +top_nn_module: list[tuple[str, int, str | None]] = [ + ("nn.Module", 927129, None), + ("nn.Linear", 530688, "nn.functional.linear"), + ("nn.Sequential", 384968, None), + ("nn.Conv2d", 383320, "nn.functional.conv2d"), + ("nn.ReLU", 318877, "nn.functional.relu"), + ("nn.BatchNorm2d", 233265, "nn.functional.batch_norm"), + ("nn.Dropout", 179268, "nn.functional.dropout"), + ("nn.ModuleList", 171225, None), + ("nn.Parameter", 153291, None), + ("nn.CrossEntropyLoss", 152696, "nn.functional.cross_entropy"), + ("nn.MaxPool2d", 138619, "nn.functional.max_pool2d"), + ("nn.Embedding", 111844, "nn.functional.embedding"), + ("nn.DataParallel", 104238, None), + ("nn.MSELoss", 82954, "nn.functional.mse_loss"), + ("nn.Sigmoid", 75810, "nn.functional.sigmoid"), + ("nn.LeakyReLU", 65632, "nn.functional.leaky_relu"), + ("nn.BatchNorm1d", 65374, "nn.functional.batch_norm"), + ("nn.Softmax", 65114, "nn.functional.softmax"), + ("nn.Tanh", 59445, "nn.functional.tanh"), + ("nn.AdaptiveAvgPool2d", 59071, "nn.functional.adaptive_avg_pool2d"), + ("nn.AvgPool2d", 58377, "nn.functional.avg_pool2d"), + ("nn.ConvTranspose2d", 57524, "nn.functional.conv_transpose2d"), + ("nn.LSTM", 57411, None), + ("nn.Conv1d", 41108, "nn.functional.conv1d"), + ("nn.LayerNorm", 36089, "nn.functional.layer_norm"), + ("nn.BCELoss", 34005, "nn.functional.binary_cross_entropy"), + ("nn.Upsample", 32527, "nn.functional.interpolate"), + ("nn.BCEWithLogitsLoss", 29944, "nn.functional.binary_cross_entropy_with_logits"), + ("nn.GRU", 25421, None), + ("nn.Dropout2d", 23512, "nn.functional.dropout2d"), + ("nn.LogSoftmax", 22897, "nn.functional.log_softmax"), + ("nn.L1Loss", 22778, "nn.functional.l1_loss"), + ("nn.GroupNorm", 22183, "nn.functional.group_norm"), + ("nn.NLLLoss", 21751, "nn.functional.nll_loss"), + ("nn.Conv3d", 20874, "nn.functional.conv3d"), + ("nn.Identity", 17911, None), + ("nn.InstanceNorm2d", 16426, "nn.functional.instance_norm"), + ("nn.BatchNorm3d", 16378, "nn.functional.batch_norm"), + ("nn.PReLU", 13472, "nn.functional.prelu"), + ("nn.ReLU6", 12622, "nn.functional.relu6"), + ("nn.ELU", 12508, "nn.functional.elu"), + ("nn.LSTMCell", 10885, None), + ("nn.Flatten", 10384, "torch.flatten"), + ("nn.ModuleDict", 10255, None), + ("nn.ReflectionPad2d", 9954, "nn.functional.pad"), + ("nn.MaxPool3d", 9526, "nn.functional.max_pool3d"), + ("nn.MaxPool1d", 9154, "nn.functional.max_pool1d"), + ("nn.RNN", 9154, None), + ("nn.ZeroPad2d", 8847, "nn.functional.pad"), + ("nn.ParameterList", 7702, None), + ("nn.SyncBatchNorm", 6814, None), + ("nn.PixelShuffle", 6571, "nn.functional.pixel_shuffle"), + ("nn.SmoothL1Loss", 6517, "nn.functional.smooth_l1_loss"), + ("nn.Hardswish", 6458, "nn.functional.hardswish"), + ("nn.AdaptiveMaxPool2d", 6071, "nn.functional.adaptive_max_pool2d"), + ("nn.SELU", 6043, "nn.functional.selu"), + ("nn.ConvTranspose3d", 6039, "nn.functional.conv_transpose3d"), + ("nn.GRUCell", 5840, None), + ("nn.ReplicationPad2d", 5600, "nn.functional.pad"), + ("nn.KLDivLoss", 5541, "nn.functional.kl_div"), + ("nn.ConvTranspose1d", 5183, "nn.functional.conv_transpose1d"), + ("nn.Softplus", 5120, "nn.functional.softplus"), + ("nn.SiLU", 4895, "nn.functional.silu"), + ("nn.AvgPool3d", 4523, "nn.functional.avg_pool3d"), + ("nn.CosineSimilarity", 4058, "nn.functional.cosine_similarity"), + ("nn.GELU", 3932, "nn.functional.gelu"), + ("nn.UpsamplingBilinear2d", 3673, "nn.functional.interpolate"), + ("nn.InstanceNorm1d", 3658, "nn.functional.instance_norm"), + ("nn.Transformer", 3604, None), + ("nn.MultiheadAttention", 3435, "nn.functional.multi_head_attention_forward"), + ("nn.AvgPool1d", 3195, "nn.functional.avg_pool1d"), + ("nn.Dropout3d", 2964, "nn.functional.dropout3d"), + ("nn.AdaptiveAvgPool3d", 2915, "nn.functional.adaptive_avg_pool3d"), + ("nn.InstanceNorm3d", 2893, "nn.functional.instance_norm"), + ("nn.Hardtanh", 2613, "nn.functional.hardtanh"), + ("nn.MarginRankingLoss", 2568, "nn.functional.margin_ranking_loss"), + ("nn.GLU", 2526, "nn.functional.glu"), + ("nn.AdaptiveAvgPool1d", 2481, "nn.functional.adaptive_avg_pool1d"), + ("nn.EmbeddingBag", 2344, "nn.functional.embedding_bag"), + ("nn.TransformerEncoderLayer", 2292, None), + ("nn.TransformerEncoder", 2091, None), + ("nn.MaxUnpool2d", 2031, "nn.functional.max_unpool2d"), + ("nn.UpsamplingNearest2d", 2004, "nn.functional.interpolate"), + ("nn.ConstantPad1d", 1904, "nn.functional.pad"), + ("nn.ConstantPad2d", 1791, "nn.functional.pad"), + ("nn.CTCLoss", 1789, "nn.functional.ctc_loss"), + ("nn.AdaptiveMaxPool1d", 1713, "nn.functional.adaptive_max_pool1d"), + ("nn.AdaptiveLogSoftmaxWithLoss", 1665, None), + ("nn.Bilinear", 1664, "nn.functional.bilinear"), + ("nn.RNNCell", 1653, None), + ("nn.MultiLabelSoftMarginLoss", 1624, "nn.functional.multilabel_soft_margin_loss"), + ("nn.Unfold", 1452, "nn.functional.unfold"), + ("nn.RReLU", 1431, "nn.functional.rrelu"), + ("nn.CosineEmbeddingLoss", 1357, "nn.functional.cosine_embedding_loss"), + ("nn.LocalResponseNorm", 1331, "nn.functional.local_response_norm"), + ("nn.Softmax2d", 1300, "nn.functional.softmax"), + ("nn.PairwiseDistance", 1241, "nn.functional.pairwise_distance"), + ("nn.LogSigmoid", 1235, "nn.functional.logsigmoid"), + ("nn.TripletMarginLoss", 1230, "nn.functional.triplet_margin_loss"), + ("nn.RNNBase", 1133, None), + ("nn.Threshold", 1043, "nn.functional.threshold"), + ("nn.AdaptiveMaxPool3d", 1025, "nn.functional.adaptive_max_pool3d"), + ("nn.CELU", 1018, "nn.functional.celu"), + ("nn.NLLLoss2d", 966, "nn.functional.nll_loss"), + ("nn.Softsign", 877, "nn.functional.softsign"), + ("nn.ReplicationPad1d", 862, "nn.functional.pad"), + ("nn.SoftMarginLoss", 856, "nn.functional.soft_margin_loss"), + ("nn.ParameterDict", 742, None), + ("nn.ReflectionPad1d", 731, "nn.functional.pad"), + ("nn.Softshrink", 713, "nn.functional.softshrink"), + ("nn.AlphaDropout", 710, "nn.functional.alpha_dropout"), + ("nn.Tanhshrink", 681, "nn.functional.tanhshrink"), + ("nn.PoissonNLLLoss", 676, "nn.functional.poisson_nll_loss"), + ("nn.MaxUnpool3d", 660, "nn.functional.max_unpool3d"), + ("nn.Fold", 630, "nn.functional.fold"), + ("nn.MultiMarginLoss", 622, "nn.functional.multi_margin_loss"), + ("nn.TransformerDecoderLayer", 614, None), + ("nn.TransformerDecoder", 607, None), + ("nn.Hardshrink", 592, "nn.functional.hardshrink"), + ("nn.ConstantPad3d", 582, "nn.functional.pad"), + ("nn.MultiLabelMarginLoss", 580, "nn.functional.multilabel_margin_loss"), + ("nn.LPPool2d", 550, "nn.functional.lp_pool2d"), + ("nn.Softmin", 537, "nn.functional.softmin"), + ("nn.MaxUnpool1d", 518, "nn.functional.max_unpool1d"), + ("nn.FractionalMaxPool2d", 484, "nn.functional.fractional_max_pool2d"), + ("nn.Hardsigmoid", 477, "nn.functional.hardsigmoid"), + ("nn.ReplicationPad3d", 470, "nn.functional.pad"), + ("nn.HingeEmbeddingLoss", 442, "nn.functional.hinge_embedding_loss"), + ("nn.LPPool1d", 386, "nn.functional.lp_pool1d"), + ("nn.FractionalMaxPool3d", 252, "nn.functional.fractional_max_pool3d"), + ("nn.Container", 217, None), + ("nn.Unflatten", 206, "nn.functional.unflatten"), + ("nn.FeatureAlphaDropout", 136, "nn.functional.feature_alpha_dropout"), + ( + "nn.TripletMarginWithDistanceLoss", + 107, + "nn.functional.triplet_margin_with_distance_loss", + ), + ("nn.ChannelShuffle", 90, "nn.functional.channel_shuffle"), + ("nn.RNNCellBase", 88, None), + ("nn.LazyLinear", 81, "nn.functional.linear"), + ("nn.UninitializedParameter", 60, None), + ("nn.CrossMapLRN2d", 59, None), + ("nn.GaussianNLLLoss", 55, "nn.functional.gaussian_nll_loss"), + ("nn.PixelUnshuffle", 45, "nn.functional.pixel_unshuffle"), + ("nn.Mish", 31, "nn.functional.mish"), + ("nn.ReflectionPad3d", 22, "nn.functional.pad"), + ("nn.HuberLoss", 18, "nn.functional.huber_loss"), + ("nn.LazyConv2d", 15, None), + ("nn.LazyConv1d", 9, None), + ("nn.LazyConv3d", 8, None), + ("nn.LazyConvTranspose1d", 8, None), + ("nn.LazyConvTranspose2d", 8, None), + ("nn.LazyConvTranspose3d", 8, None), + ("nn.LazyBatchNorm1d", 3, None), + ("nn.LazyBatchNorm2d", 3, None), + ("nn.LazyBatchNorm3d", 3, None), + ("nn.UninitializedBuffer", 3, None), +] + +# No rankings because these are a little hard to get rankings for +method_only_ops: list[str] = [ + "bfloat16", + "bool", + "byte", + "char", + "contiguous", + "cpu", + "cuda", + "detach", + "double", + "expand", + "expand_as", + "float", + "get_device", + "half", + "hardshrink", + "index_add", + "index_copy", + "index_fill", + "index_put", + "int", + "is_contiguous", + "is_pinned", + "is_set_to", + "is_shared", + "is_signed", + "item", + "long", + "masked_scatter", + "masked_fill", + "narrow_copy", + "numpy", + "pin_memory", + "repeat", + "reshape_as", + "select", + "short", + "storage_offset", + "sum_to_size", + "to", + "to_mkldnn", + "tolist", + "type", + "type_as", + "unfold", + "view", + "view_as", +] + + +def get_nn_functional_top_list() -> list[tuple[str, int]]: + top_nn_functional_: dict[str, int] = dict(top_nn_functional) + for _, count, functional_name in top_nn_module: + if functional_name is None: + continue + if functional_name == "torch.flatten": + continue + if functional_name not in top_nn_functional_: + top_nn_functional_[functional_name] = count + else: + top_nn_functional_[functional_name] += count + + top_nn_functional_list = list(top_nn_functional_.items()) + top_nn_functional_list.sort(key=operator.itemgetter(1), reverse=True) + return top_nn_functional_list + + +usage_count: dict[str, int] = dict(get_nn_functional_top_list()) +usage_count.update(top_torch) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f7f00384c6013d15ae6cfb37c5f68c82d9a758a2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/utils.py @@ -0,0 +1,40 @@ +import contextlib +from collections.abc import Generator +from typing import Any + +import torch +from torch._C._functorch import ( + get_single_level_autograd_function_allowed, + set_single_level_autograd_function_allowed, + unwrap_if_dead, +) +from torch.utils._exposed_in import exposed_in + + +__all__ = [ + "exposed_in", + "argnums_t", + "enable_single_level_autograd_function", + "unwrap_dead_wrappers", +] + + +@contextlib.contextmanager +def enable_single_level_autograd_function() -> Generator[None, None, None]: + try: + prev_state = get_single_level_autograd_function_allowed() + set_single_level_autograd_function_allowed(True) + yield + finally: + set_single_level_autograd_function_allowed(prev_state) + + +def unwrap_dead_wrappers(args: tuple[Any, ...]) -> tuple[Any, ...]: + # NB: doesn't use tree_map_only for performance reasons + result = tuple( + unwrap_if_dead(arg) if isinstance(arg, torch.Tensor) else arg for arg in args + ) + return result + + +argnums_t = int | tuple[int, ...] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/vmap.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/vmap.py new file mode 100644 index 0000000000000000000000000000000000000000..50af5a2e5ac2c333fdab8d022a3b3ed87fb82633 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_functorch/vmap.py @@ -0,0 +1,567 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from __future__ import annotations + +import contextlib +import functools +import itertools +from collections.abc import Callable # noqa: TC003 +from functools import partial +from typing import Any, cast, NoReturn, TYPE_CHECKING +from typing_extensions import ParamSpec, TypeVar + +import torch +from torch import Tensor +from torch._C._functorch import is_batchedtensor +from torch._functorch.predispatch import ( + _add_batch_dim, + _remove_batch_dim, + _vmap_decrement_nesting, + _vmap_increment_nesting, + lazy_load_decompositions, +) +from torch.utils._pytree import ( + _broadcast_to_and_flatten, + tree_flatten, + tree_map_, + tree_unflatten, + TreeSpec, +) + + +if TYPE_CHECKING: + from collections.abc import Generator, Iterable + + +_P = ParamSpec("_P") +_R = TypeVar("_R") + +in_dims_t = int | tuple[Any, ...] +out_dims_t = int | tuple[int, ...] | None + + +def doesnt_support_saved_tensors_hooks(f: Callable[_P, _R]) -> Callable[_P, _R]: + message = ( + "torch.func.{grad, vjp, jacrev, hessian} don't yet support saved tensor hooks. " + "Please open an issue with your use case." + ) + + @functools.wraps(f) + def fn(*args: _P.args, **kwargs: _P.kwargs) -> _R: + with torch.autograd.graph.disable_saved_tensors_hooks(message): + return f(*args, **kwargs) + + return fn + + +# Checks that all args-to-be-batched have the same batch dim size +def _validate_and_get_batch_size( + flat_in_dims: list[int | None], flat_args: list[Any] +) -> int: + batch_sizes = [ + arg.size(in_dim) + for in_dim, arg in zip(flat_in_dims, flat_args) + if in_dim is not None + ] + if len(batch_sizes) == 0: + raise ValueError("vmap: Expected at least one Tensor to vmap over") + if batch_sizes and any(size != batch_sizes[0] for size in batch_sizes): + raise ValueError( + f"vmap: Expected all tensors to have the same size in the mapped " + f"dimension, got sizes {batch_sizes} for the mapped dimension" + ) + return batch_sizes[0] + + +def _num_outputs(batched_outputs: Tensor | tuple[Tensor, ...]) -> int: + if isinstance(batched_outputs, tuple): + return len(batched_outputs) + return 1 + + +# If value is a tuple, check it has length `num_elements`. +# If value is not a tuple, make a tuple with `value` repeated `num_elements` times + + +def _as_tuple( + value: tuple[_R, ...] | _R, + num_elements: int, + error_message_lambda: Callable[[], str], +) -> tuple[_R, ...]: + if not isinstance(value, tuple): + return (value,) * num_elements + if len(value) != num_elements: + raise ValueError(error_message_lambda()) + return value + + +def _process_batched_inputs( + in_dims: in_dims_t, args: tuple[Any, ...], func: Callable[..., Any] +) -> tuple[int, list[int | None], list[Any], TreeSpec]: + if not isinstance(in_dims, int) and not isinstance(in_dims, tuple): + raise ValueError( + f"vmap({_get_name(func)}, in_dims={in_dims}, ...)(): " + f"expected `in_dims` to be int or a (potentially nested) tuple " + f"matching the structure of inputs, got: {type(in_dims)}." + ) + if len(args) == 0: + raise ValueError( + f"vmap({_get_name(func)})(): got no inputs. Maybe you forgot to add " + f"inputs, or you are trying to vmap over a function with no inputs. " + f"The latter is unsupported." + ) + + flat_args, args_spec = tree_flatten(args) + flat_in_dims = _broadcast_to_and_flatten(in_dims, args_spec) + if flat_in_dims is None: + raise ValueError( + f"vmap({_get_name(func)}, in_dims={in_dims}, ...)(): " + f"in_dims is not compatible with the structure of `inputs`. " + f"in_dims has structure {tree_flatten(in_dims)[1]} but inputs " + f"has structure {args_spec}." + ) + + for i, (arg, in_dim) in enumerate(zip(flat_args, flat_in_dims)): + if not isinstance(in_dim, int) and in_dim is not None: + raise ValueError( + f"vmap({_get_name(func)}, in_dims={in_dims}, ...)(): " + f"Got in_dim={in_dim} for an input but in_dim must be either " + f"an integer dimension or None." + ) + if isinstance(in_dim, int) and not isinstance(arg, Tensor): + raise ValueError( + f"vmap({_get_name(func)}, in_dims={in_dims}, ...)(): " + f"Got in_dim={in_dim} for an input but the input is of type " + f"{type(arg)}. We cannot vmap over non-Tensor arguments, " + f"please use None as the respective in_dim" + ) + if in_dim is not None and (in_dim < -arg.dim() or in_dim >= arg.dim()): + raise ValueError( + f"vmap({_get_name(func)}, in_dims={in_dims}, ...)(): " + f"Got in_dim={in_dim} for some input, but that input is a Tensor " + f"of dimensionality {arg.dim()} so expected in_dim to satisfy " + f"-{arg.dim()} <= in_dim < {arg.dim()}." + ) + if in_dim is not None and in_dim < 0: + flat_in_dims[i] = in_dim % arg.dim() + + return ( + _validate_and_get_batch_size(flat_in_dims, flat_args), + flat_in_dims, + flat_args, + args_spec, + ) + + +# Creates BatchedTensors for every Tensor in arg that should be batched. +# Returns the (potentially) batched arguments and the batch_size. + + +# TODO: See if we can explain how flat works to the type checker +def _create_batched_inputs( + flat_in_dims: list[int | None], + flat_args: list[Any], + vmap_level: int, + args_spec: TreeSpec, +) -> tuple[Any, ...]: + # See NOTE [Ignored _remove_batch_dim, _add_batch_dim] + batched_inputs = [ + arg if in_dim is None else _add_batch_dim(arg, in_dim, vmap_level) + for in_dim, arg in zip(flat_in_dims, flat_args) + ] + return tree_unflatten(batched_inputs, args_spec) + + +def _maybe_remove_batch_dim( + name: str, + batched_output: Any, + vmap_level: int, + batch_size: int, + out_dim: int | None, +) -> torch.Tensor: + if out_dim is None: + if isinstance(batched_output, torch.Tensor) and is_batchedtensor( + batched_output + ): + raise ValueError( + f"vmap({name}, ...): `{name}` can not return a " + f"BatchedTensor when out_dim is None" + ) + return batched_output + + # out_dim is non None + if not isinstance(batched_output, torch.Tensor): + raise ValueError( + f"vmap({name}, ...): `{name}` must only return " + f"Tensors, got type {type(batched_output)}. " + "Did you mean to set out_dims= to None for output?" + ) + + return _remove_batch_dim(batched_output, vmap_level, batch_size, out_dim) + + +# Undos the batching (and any batch dimensions) associated with the `vmap_level`. +def _unwrap_batched( + batched_outputs: Tensor | tuple[Tensor, ...], + out_dims: out_dims_t, + vmap_level: int, + batch_size: int, + func: Callable[..., Any], +) -> tuple[Any, ...]: + flat_batched_outputs, output_spec = tree_flatten(batched_outputs) + + def incompatible_error() -> NoReturn: + raise ValueError( + f"vmap({_get_name(func)}, ..., out_dims={out_dims})(): " + f"out_dims is not compatible with the structure of `outputs`. " + f"out_dims has structure {tree_flatten(out_dims)[1]} but outputs " + f"has structure {output_spec}." + ) + + flat_out_dims: list[int | None] = [] + if isinstance(batched_outputs, torch.Tensor): + # Some weird edge case requires us to spell out the following + # see test_out_dims_edge_case + if isinstance(out_dims, int): + flat_out_dims = [out_dims] + elif isinstance(out_dims, tuple) and len(out_dims) == 1: + flat_out_dims = list(out_dims) + elif out_dims is None: + flat_out_dims = [out_dims] + else: + incompatible_error() + else: + broadcast_result = _broadcast_to_and_flatten(out_dims, output_spec) + if broadcast_result is None: + incompatible_error() + else: + flat_out_dims = broadcast_result + + flat_outputs = [ + _maybe_remove_batch_dim( + _get_name(func), batched_output, vmap_level, batch_size, out_dim + ) + for batched_output, out_dim in zip(flat_batched_outputs, flat_out_dims) + ] + return tree_unflatten(flat_outputs, output_spec) + + +def _check_int_or_none(x: Any, func: Callable[..., Any], out_dims: out_dims_t) -> None: + if isinstance(x, int): + return + if x is None: + return + raise ValueError( + f"vmap({_get_name(func)}, ..., out_dims={out_dims}): `out_dims` must be " + f"an int, None or a python collection of ints representing where in the outputs the " + f"vmapped dimension should appear." + ) + + +def _check_out_dims_is_int_or_int_pytree( + out_dims: out_dims_t, func: Callable[..., Any] +) -> None: + if isinstance(out_dims, int): + return + tree_map_(partial(_check_int_or_none, func=func, out_dims=out_dims), out_dims) + + +def _get_name(func: Callable[..., Any]) -> str: + if hasattr(func, "__name__"): + return func.__name__ + + if isinstance(func, functools.partial): + return f"functools.partial({_get_name(func.func)}, ...)" + + # Not all callables have __name__, in fact, only static functions/methods + # do. A callable created via nn.Module, to name one example, doesn't have a + # __name__. + return repr(func) + + +def vmap_impl( + func: Callable[_P, Tensor | tuple[Tensor, ...]], + in_dims: in_dims_t, + out_dims: out_dims_t, + randomness: str, + chunk_size: int | None, + *args: _P.args, + **kwargs: _P.kwargs, +) -> Any: + lazy_load_decompositions() + _check_out_dims_is_int_or_int_pytree(out_dims, func) + batch_size, flat_in_dims, flat_args, args_spec = _process_batched_inputs( + in_dims, args, func + ) + + if chunk_size is not None: + chunks_flat_args = _get_chunked_inputs( + flat_args, flat_in_dims, batch_size, chunk_size + ) + return _chunked_vmap( + func, + flat_in_dims, + chunks_flat_args, + args_spec, + out_dims, + randomness, + **kwargs, + ) + + # If chunk_size is not specified. + return _flat_vmap( + func, + batch_size, + flat_in_dims, + flat_args, + args_spec, + out_dims, + randomness, + **kwargs, + ) + + +def get_chunk_sizes(total_elems: int, chunk_size: int) -> list[int]: + n_chunks = total_elems // chunk_size + chunk_sizes = [chunk_size] * n_chunks + # remainder chunk + remainder = total_elems % chunk_size + if remainder != 0: + chunk_sizes.append(remainder) + return chunk_sizes + + +def _get_chunked_inputs( + flat_args: list[Any], + flat_in_dims: list[int | None], + batch_size: int, + chunk_size: int | None, +) -> Iterable[tuple[Any, ...]]: + split_idxs = (batch_size,) + if chunk_size is not None: + chunk_sizes = get_chunk_sizes(batch_size, chunk_size) + split_idxs = tuple(itertools.accumulate(chunk_sizes)) + + flat_args_chunks = tuple( + ( + t.tensor_split(split_idxs, dim=in_dim) + if in_dim is not None + else [ + t, + ] + * len(split_idxs) + ) + for t, in_dim in zip(flat_args, flat_in_dims) + ) + + # transpose chunk dim and flatten structure + # chunks_flat_args is a list of flatten args + chunks_flat_args = zip(*flat_args_chunks) + return chunks_flat_args + + +def _flatten_chunks_output( + chunks_output_: list[Any], +) -> tuple[list[tuple[Any, ...]], TreeSpec]: + # chunks_output is a list of chunked outputs + # flatten chunked outputs: + flat_chunks_output: list[list[Any]] = [] + arg_spec: TreeSpec | None = None + for output in chunks_output_: + flat_output, arg_specs = tree_flatten(output) + flat_chunks_output.append(flat_output) + if arg_spec is None: + arg_spec = arg_specs + + # transpose chunk dim and flatten structure + # flat_output_chunks is flat list of chunks + flat_output_chunks = list(zip(*flat_chunks_output)) + if arg_spec is None: + raise AssertionError("arg_spec must not be None") + return flat_output_chunks, arg_spec + + +def _concat_chunked_outputs( + out_dims: out_dims_t, + arg_spec: TreeSpec, + flat_output_chunks: list[tuple[Any, ...] | None], +) -> list[Tensor]: + # concat chunks on out_dim + flat_out_dims = _broadcast_to_and_flatten(out_dims, arg_spec) + if flat_out_dims is None: + raise AssertionError("flat_out_dims must not be None") + if len(flat_out_dims) != len(flat_output_chunks): + raise AssertionError( + f"len(flat_out_dims)={len(flat_out_dims)} != len(flat_output_chunks)={len(flat_output_chunks)}" + ) + flat_output: list[Tensor] = [] + for idx, out_dim in enumerate(flat_out_dims): + chunk = flat_output_chunks[idx] + if chunk is None: + raise AssertionError(f"chunk at index {idx} must not be None") + flat_output.append(torch.cat(chunk, dim=out_dim)) + # release tensors + flat_output_chunks[idx] = None + + return flat_output + + +# Applies vmap on chunked_input and returns concatenated output over the chunks. +def _chunked_vmap( + func: Callable[_P, Tensor | tuple[Tensor, ...]], + flat_in_dims: list[int | None], + chunks_flat_args: Iterable[tuple[Any, ...]], + args_spec: TreeSpec, + out_dims: out_dims_t, + randomness: str, + **kwargs: Any, +) -> Any: + chunks_output: list[Any] = [] + rs = torch.get_rng_state() if randomness == "same" else None + for flat_args_tuple in chunks_flat_args: + flat_args = list(flat_args_tuple) + batch_size = _validate_and_get_batch_size(flat_in_dims, flat_args) + + # The way we compute split the input in `_get_chunked_inputs`, + # we may get a tensor with `0` batch-size. We skip any computation + # in that case. + # Eg. + # >>> chunk_size = 1 + # >>> batch_size = 6 + # >>> t = torch.zeros(batch_size, 1) + # >>> t.tensor_split([1, 2, 3, 4, 5, 6]) + # (tensor([[0.]]), tensor([[0.]]), tensor([[0.]]), tensor([[0.]]), + # tensor([[0.]]), tensor([[0.]]), tensor([], size=(0, 1))) + if batch_size == 0: + continue + + if rs is not None: + torch.set_rng_state(rs) + chunks_output.append( + _flat_vmap( + func, + batch_size, + flat_in_dims, + flat_args, + args_spec, + out_dims, + randomness, + **kwargs, + ) + ) + + flat_output_chunks, arg_spec = _flatten_chunks_output(chunks_output) + + # chunked output tensors are held by both `flat_output_chunks` and `chunks_output`. + # eagerly remove the reference from `chunks_output`. + del chunks_output + + # concat chunks on out_dim + # Note: We use cast since flat_output_chunks is modified in _concat_chunked_outputs + # to set elements to None after processing + flat_output = _concat_chunked_outputs( + out_dims, arg_spec, cast(list[tuple[Any, ...] | None], flat_output_chunks) + ) + + # finally unflatten the output + return tree_unflatten(flat_output, arg_spec) + + +# Vmap refactored helper functions: +def _check_randomness_arg(randomness: str) -> None: + if randomness not in ["error", "different", "same"]: + raise RuntimeError( + f"Only allowed values for randomness are 'error', 'different', or 'same'. Got {randomness}" + ) + + +@contextlib.contextmanager +def vmap_increment_nesting( + batch_size: int, randomness: str +) -> Generator[int, None, None]: + try: + vmap_level = _vmap_increment_nesting(batch_size, randomness) + yield vmap_level + finally: + _vmap_decrement_nesting() + + +def _flat_vmap( + func: Callable[..., Tensor | tuple[Tensor, ...]], + batch_size: int, + flat_in_dims: list[int | None], + flat_args: list[Any], + args_spec: TreeSpec, + out_dims: out_dims_t, + randomness: str, + **kwargs: Any, +) -> Any: + with vmap_increment_nesting(batch_size, randomness) as vmap_level: + batched_inputs = _create_batched_inputs( + flat_in_dims, flat_args, vmap_level, args_spec + ) + batched_outputs = func(*batched_inputs, **kwargs) + return _unwrap_batched(batched_outputs, out_dims, vmap_level, batch_size, func) + + +# `restore_vmap` is a private helper function. It is vmap but has the following +# differences: +# - instead of returning outputs, it returns an (outputs, out_dims) tuple. +# out_dims is a pytree of same shape as outputs and contains Optional[int] +# specifying where the vmapped dimension, if it exists, is in the corresponding output. +# - does no validation on in_dims or inputs (vmap expects at least one Tensor to be vmapped). +# restore_vmap allows for no inputs to have the vmap dimension +# - does no validation on outputs (vmap expects only Tensor outputs) +# restore_vmap allows for return of arbitrary outputs (not just Tensors) +# +# The TL;DR is that restore_vmap is more general than vmap and has a slightly +# different API. The relaxations are so that we can "pause" vmap in the middle +# of its execution and then "restore" it later (this is what we do in +# the generate_vmap_rule=True implementation of autograd.Function). +# +# restore_vmap can be technically used in the implementation of vmap, but doing +# that refactor is a bit technically challenging because: +# - vmap couples the tensor-wrapping code with error checking +# - vmap's tensor unwrapping code is in C++; we would need to rewrite part of it +# in python because it overlaps with unwrap_batched +def restore_vmap( + func: Callable[..., _R], in_dims: in_dims_t, batch_size: int, randomness: str +) -> Callable[..., tuple[Any, Any]]: + def inner(*args: Any, **kwargs: Any) -> tuple[Any, Any]: + with vmap_increment_nesting(batch_size, randomness) as vmap_level: + batched_inputs = wrap_batched(args, in_dims, vmap_level) + batched_outputs = func(*batched_inputs, **kwargs) + return unwrap_batched(batched_outputs, vmap_level) + + return inner + + +def wrap_batched( + args: tuple[Any, ...], bdims: in_dims_t, level: int +) -> tuple[Any, ...]: + flat_args, spec = tree_flatten(args) + flat_bdims = _broadcast_to_and_flatten(bdims, spec) + if flat_bdims is None: + raise AssertionError("flat_bdims must not be None") + result = _create_batched_inputs(flat_bdims, flat_args, level, spec) + return result + + +def unwrap_batched(args: Any, level: int) -> tuple[Any, Any]: + flat_args, spec = tree_flatten(args) + if len(flat_args) == 0: + return args, () + result = [ + ( + torch._C._functorch._unwrap_batched(arg, level) + if isinstance(arg, torch.Tensor) + else (arg, None) + ) + for arg in flat_args + ] + output, bdims = zip(*result) + return tree_unflatten(output, spec), tree_unflatten(bdims, spec) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_guards.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_guards.py new file mode 100644 index 0000000000000000000000000000000000000000..1ae422d999c6ee534a2ae7d2f735112f705b2c37 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_guards.py @@ -0,0 +1,1536 @@ +from __future__ import annotations + +import contextlib +import dataclasses +import enum +import functools +import logging +import re +import threading +import traceback +import unittest.mock +import weakref +from abc import abstractmethod +from collections import defaultdict +from contextlib import contextmanager +from dataclasses import dataclass +from typing import Any, Generic, NamedTuple, overload, TYPE_CHECKING, TypeVar +from typing_extensions import dataclass_transform + +import torch +from torch.utils import _pytree as pytree +from torch.utils._ordered_set import OrderedSet +from torch.utils._python_dispatch import is_traceable_wrapper_subclass +from torch.utils._traceback import CapturedTraceback, format_frame +from torch.utils.weak import WeakTensorKeyDictionary + + +log = logging.getLogger(__name__) + + +if TYPE_CHECKING: + from collections.abc import Callable, Generator, Iterator + from types import CodeType + + import sympy + + from torch._dynamo.backends.distributed import DDPOptimizerContext + from torch._dynamo.codegen import PyCodegen + from torch._dynamo.guards import GuardCheckSpec + from torch._functorch._aot_autograd.schemas import ViewAndMutationMeta + from torch._higher_order_ops.invoke_subgraph import NestedCompileRegionOptions + from torch._subclasses.fake_tensor import FakeTensorMode + + +""" +torch._guards is the definitional source of truth for general purpose guard structures. + +An important thing to keep in mind here is the preservation of layering. There should be no dynamo notions, +and no guard installation notions here. +""" + +COMPILE_ID_PATTERN = re.compile(r"^(?P\d+)/(?P\d+)$") +CA_COMPILE_ID_PATTERN = re.compile( + r"^!(?P\d+)(?:/(?P\d+)/(?P\d+))?$" +) + +# [Note: Updating CompiledId] +# +# CompiledId represents a unique program-level identifier, and we want to keep that +# property as the codebase evolves. This property is relied on even outside of the pytorch +# repo, e.g. tlparse or other internal tooling. The in-memory format can be freely changed, +# as those dependencies only consume the string serialization. +# +# The string form should be: +# 1. Program-level uid: CompileId can uniquely identify a compiled graph. +# 2. Storage efficient: This object is logged in nearly every entry. We should elide symbols when possible. +# 3. Compact: The string form is directly displayed by some tools. Special symbols are okay. + + +@dataclass(frozen=True, kw_only=True, slots=True) +class CompileId: + frame_id: int | None + # This id is per-frame, and counts how many times we've compiled this + # frame. This could have been a global id but having this be per-frame + # gives you a better intuitive sense for how many recompiles have occurred + # so far. + frame_compile_id: int | None + + # torch.compiling a compiled autograd graph + compiled_autograd_id: int | None = None + + # TODO: consider also tracking the recompilation count + # See Note: Updating CompileId + + def __str__(self) -> str: + # NOTE: Keep this in sync with both from_string and the tlparse repo + if self.compiled_autograd_id is not None: + if (self.frame_id is None) != (self.frame_compile_id is None): + raise AssertionError( + f"frame_id and frame_compile_id must both be None or both be set, " + f"got frame_id={self.frame_id}, frame_compile_id={self.frame_compile_id}" + ) + frame_str = "" + if self.frame_id is not None: + frame_str = f"/{self.frame_id}/{self.frame_compile_id}" + + return f"!{self.compiled_autograd_id}{frame_str}" + else: + if self.frame_id is None or self.frame_compile_id is None: + raise AssertionError( + f"frame_id and frame_compile_id must not be None, " + f"got frame_id={self.frame_id}, frame_compile_id={self.frame_compile_id}" + ) + return f"{self.frame_id}/{self.frame_compile_id}" + + @classmethod + def from_string(cls, compile_id: str | None) -> CompileId | None: + """ + Factory method that creates a CompileId from its string representation. + Keep this in sync with the __str__ method. + """ + if compile_id is None: + return None + try: + for pattern in (COMPILE_ID_PATTERN, CA_COMPILE_ID_PATTERN): + if match := pattern.match(compile_id): + groups = match.groupdict() + for k, v in groups.items(): + if v is not None: + groups[k] = int(v) + return cls(**groups) # type: ignore[arg-type] + else: + raise ValueError + + except Exception as e: + raise ValueError(f"Invalid compile_id '{compile_id}'") from e + + +class TraceId(NamedTuple): + compile_id: CompileId + # This starts off as 0, and every time we restart analysis it goes + # up by one + attempt: int + + def __str__(self) -> str: + # Keep this in sync with tlparse repo + if self.attempt == 0: + return str(self.compile_id) + else: + return f"{self.compile_id}_{self.attempt}" + + +class GuardSource(enum.Enum): + LOCAL = 0 + GLOBAL = 1 + LOCAL_SPECIALIZED_NN_MODULE = 2 + GLOBAL_SPECIALIZED_NN_MODULE = 3 + CONSTANT = 4 + RANDOM_VALUE = 5 + SHAPE_ENV = 6 + LOCAL_FSDP_MODULE = 7 + GLOBAL_FSDP_MODULE = 8 + BACKWARD_STATE = 9 + EPHEMERAL = 10 + SYNTHETIC_LOCAL = 11 + LOCAL_UNSPECIALIZED_NN_MODULE = 12 + GLOBAL_UNSPECIALIZED_NN_MODULE = 13 + LOCAL_UNSPECIALIZED_BUILTIN_NN_MODULE = 14 + GLOBAL_UNSPECIALIZED_BUILTIN_NN_MODULE = 15 + TEMP_LOCAL = 16 + + def is_fsdp_module(self) -> bool: + return self in (GuardSource.GLOBAL_FSDP_MODULE, GuardSource.LOCAL_FSDP_MODULE) + + def is_specialized_nn_module(self) -> bool: + import torch._dynamo.config as config + + if config._unsafe_skip_fsdp_module_guards: + return ( + self + in ( + GuardSource.GLOBAL_SPECIALIZED_NN_MODULE, + GuardSource.LOCAL_SPECIALIZED_NN_MODULE, + ) + or self.is_fsdp_module() + ) + return self in ( + GuardSource.GLOBAL_SPECIALIZED_NN_MODULE, + GuardSource.LOCAL_SPECIALIZED_NN_MODULE, + ) + + def is_unspecialized_nn_module(self) -> bool: + return self in ( + GuardSource.GLOBAL_UNSPECIALIZED_NN_MODULE, + GuardSource.LOCAL_UNSPECIALIZED_NN_MODULE, + GuardSource.GLOBAL_UNSPECIALIZED_BUILTIN_NN_MODULE, + GuardSource.LOCAL_UNSPECIALIZED_BUILTIN_NN_MODULE, + ) + + def is_unspecialized_builtin_nn_module(self) -> bool: + return self in ( + GuardSource.GLOBAL_UNSPECIALIZED_BUILTIN_NN_MODULE, + GuardSource.LOCAL_UNSPECIALIZED_BUILTIN_NN_MODULE, + ) + + def is_local(self) -> bool: + return self in ( + GuardSource.LOCAL, + GuardSource.LOCAL_SPECIALIZED_NN_MODULE, + GuardSource.LOCAL_FSDP_MODULE, + GuardSource.LOCAL_UNSPECIALIZED_NN_MODULE, + GuardSource.LOCAL_UNSPECIALIZED_BUILTIN_NN_MODULE, + ) + + +""" +Base class for a "GuardBuilder" role. + +The GuardBuilderBase role is to represent a scope within which to build a guard. The name is a little +confusing, as its not a builder, but for the sake of avoiding a lot of renames and keeping the original reference +to torchdynamo's GuardBuilder. + +Note: create_fn is invoked with a GuardBuilderBase and a Guard. A GuardBuilder is chosen based +on GuardSource's select function. + +There is value in keeping this GuardBuilderBase empty to keep layering clean. +""" + + +class GuardBuilderBase: + pass + + +@dataclasses.dataclass(frozen=True) +class SLoc: + framework_loc: traceback.FrameSummary | str | None + maybe_user_loc: str | None + + def __str__(self) -> str: + floc = ( + self.framework_loc + if isinstance(self.framework_loc, str) + else format_frame(self.framework_loc) + ) + if self.maybe_user_loc is not None: + return f"{self.maybe_user_loc} ({floc})" + else: + return f"({floc})" + + +class ShapeGuard(NamedTuple): + expr: sympy.logic.boolalg.Boolean + sloc: SLoc + size_oblivious: bool + + +@dataclasses.dataclass(slots=True) +class Guard: + # originating_source is the source that called the make_guard method to + # construct this guard object. The property name specifies what exactly it + # is the guard is guarding on. The meaning of the name is dependent on the + # create_fn; you must look at the use-site inside create_fn to know what + # name means. + # + # That being said, although you might think this is just a "name", name is + # usually an arbitrary Python expression that will be evaluated with all + # globals (and locals, if you create a LOCAL guard) to extract the Python + # object that we want to perform guard tests on. This evaluation + # typically happens in GuardBuilder.eval. In these cases, name is + # typically produced by originating_source.name (not to be confused with + # GuardSource - the property source). + # + # Occasionally, name is not a valid Python expression; sometimes + # it is meaningless. Example create_fns that are like this include + # GRAD_MODE and SHAPE_ENV. + originating_source: Source + create_fn: Callable[[GuardBuilderBase, Guard], None] + + # Export only. These values are written to at time of guard check_fn creation. + guard_types: list[str] | None = None + code_list: list[str] | None = None + obj_weakref: object | None = None + guarded_class_weakref: weakref.ReferenceType[Any] | None = None + + stack: CapturedTraceback | None = None + user_stack: traceback.StackSummary | None = None + _hash: int | None = None + _unserializable: bool = False + + def __hash__(self) -> int: + if self._hash is None: + self._hash = hash((self.name, self.source, id(self.create_fn))) + return self._hash + + def sort_key(self) -> tuple[bool, int, int, str, int]: + # Put the duplicate input guards at the end. The duplicate guards have + # two sources while guard.name only considers one source. + + is_duplicate_input = ( + isinstance(self.create_fn, functools.partial) + and self.create_fn.func is torch._dynamo.guards.GuardBuilder.DUPLICATE_INPUT + ) + return ( + is_duplicate_input, + self.source.value if self.source else -1, + len(self.name), + self.name, + self.inner_create_fn().__code__.co_firstlineno, + ) + + def __lt__(self, other: Guard) -> bool: + return self.sort_key() < other.sort_key() + + def inner_create_fn(self) -> Callable[[GuardBuilderBase, Guard], Any]: + if isinstance(self.create_fn, functools.partial): + return self.create_fn.func + else: + return self.create_fn + + @property + def name(self) -> str: + return self.originating_source.name + + @property + def source(self) -> GuardSource: + return self.originating_source.guard_source + + @staticmethod + def weakref_to_str(obj_weakref: object) -> str: + """ + This is a workaround of a Python weakref bug. + + `obj_weakref` is instance returned by `weakref.ref`, + `str(obj_weakref)` is buggy if the original obj overrides __getattr__, e.g: + + class MyConfig(dict): + def __getattr__(self, x): + return self[x] + + obj = MyConfig(offset=5) + obj_weakref = weakref.ref(obj) + str(obj_weakref) # raise error: KeyError: '__name__' + """ + if isinstance(obj_weakref, weakref.ReferenceType): + obj = obj_weakref() + if obj is not None: + return f"" + else: + return f"" + else: + return str(obj_weakref) + + def __repr__(self) -> str: + s = f""" + {self.source.name.lower() if self.source else ""} {repr(self.name)} {self.inner_create_fn().__name__} + {{ + 'guard_types': {self.guard_types}, + 'code': {self.code_list}, + 'obj_weakref': {self.weakref_to_str(self.obj_weakref)} + 'guarded_class': {self.guarded_class_weakref} + }} + """ + return s + + def __str__(self) -> str: + output = f"Name: {repr(self.name)}\n" + source = self.source.name.lower() if self.source else "" + output += f" Source: {source}\n" + output += f" Create Function: {self.inner_create_fn().__name__}\n" + output += f" Guard Types: {self.guard_types}\n" + output += f" Code List: {self.code_list}\n" + output += f" Object Weakref: {self.weakref_to_str(self.obj_weakref)}\n" + output += f" Guarded Class Weakref: {self.guarded_class_weakref}\n" + return output + + def create(self, builder: GuardBuilderBase) -> Any: + try: + return self.create_fn(builder, self) + except Exception: + log.exception("Error while creating guard:\n%s", str(self).rstrip()) + if self.stack: + log.error("Created at:\n%s", "".join(self.stack.format()[-4:]).rstrip()) + raise + + def is_specialized_nn_module(self) -> bool: + return self.source.is_specialized_nn_module() + + def is_fsdp_module(self) -> bool: + return self.source.is_fsdp_module() + + def is_local(self) -> bool: + return self.source.is_local() + + def create_fn_name(self) -> str: + if isinstance(self.create_fn, functools.partial): + create_fn = self.create_fn.func # type: ignore[attr-defined] + else: + create_fn = self.create_fn + return create_fn.__name__ + + def set_export_info( + self, + guard_type: str, + guarded_class: weakref.ReferenceType[Any] | None, + code_list: list[str], + obj_weakref: object, + ) -> None: + if not self.guard_types: + self.guard_types = [] + + self.guard_types.append(guard_type) + + if self.guarded_class_weakref not in (guarded_class, None): + raise AssertionError( + f"Guarded class id must be identical, or None, " + f"got {self.guarded_class_weakref} vs {guarded_class}" + ) + self.guarded_class_weakref = guarded_class + + if not self.code_list: + self.code_list = code_list + else: + self.code_list.extend(code_list) + + # Some objects are ephemeral, e.g., list[slice(1, 2)]. If we have + # multiple guards on the same object, the weakref can die between the + # invocation of set_export_info calls. So a dead weakref is also + # acceptable. + is_valid = ( + self.obj_weakref in (obj_weakref, None) + or callable(self.obj_weakref) + and self.obj_weakref() is None + ) + if not is_valid: + raise AssertionError( + f"Guarded object must be identical, None or ephemeral (dead weakref), " + f"got {self.obj_weakref} vs {obj_weakref}" + ) + self.obj_weakref = obj_weakref + + +T = TypeVar("T") + +""" +Parent structure for guard env expressions. +A GuardEnvExpr can have any subtype. +Note: All subtypes must be handled exhaustively in +torch._dynamo.guards._parse_guard_env_guards to avoid a RuntimeError. +""" + + +@dataclasses.dataclass(frozen=True) +class GuardEnvExpr: + pass + + +""" +A class representing a pair of duplicate inputs. +input_pos_a and input_pos_b are input positions we have deduped. +""" + + +@dataclasses.dataclass(frozen=True) +class DuplicateInputs(GuardEnvExpr): + input_source_a: Source + input_source_b: Source + + def __post_init__(self) -> None: + if self.input_source_a == self.input_source_b: + raise AssertionError( + f"input_source_a and input_source_b must be different, " + f"got {self.input_source_a}" + ) + + +""" +A class representing storage overlap relations among inputs that aliases the same storage. + +Given that a set of tensors alias the same storage, this guard checks whether they actually +have overlapping storages. + +While non_overlapping_sources represent input tensors that definitely don't have any storage +overlapping with any other input, overlapping_sources represent tensors that either: + +1. Do overlap some other input tensor +2. Might not overlap some other input tensor, but we are not sure +""" + + +@dataclasses.dataclass(frozen=True) +class StorageOverlap(GuardEnvExpr): + overlapping_sources: list[Source] + non_overlapping_sources: list[Source] + + +""" +Checkpointable is an interface for driving state snapshotting, left purposely vague for now. + +copy_graphstate() -> T, a somewhat legacy name, is expected to emit a snapshot of any type that +can also be taken in at restore_graphstate(T) calls. + +When to snapshot, is, at the moment, an implementation detail of upstream callers. Checkpointable +does not provide any guarantees around consistency, idempotency, or safety of calling its APIs, yet. + +In the future, it will have a closer coupling to a generic Checkpoint management system. +""" + + +class Checkpointable(Generic[T]): + @abstractmethod + def copy_graphstate(self) -> T: ... + + @abstractmethod + def restore_graphstate(self, state: T) -> None: ... + + +class GuardsCheckpointState: + """ + The GuardCheckpointState - it is the T of Checkpointable[T] for GuardsContext + """ + + dynamo_guards: OrderedSet[Guard] + + def __init__(self, dynamo_guards: OrderedSet[Guard]) -> None: + self.dynamo_guards = dynamo_guards + + def diff(self, other: GuardsCheckpointState) -> OrderedSet[Guard] | None: + """ + Produces a delta against another GuardsCheckpointState. + + Returns None if no delta is found, otherwise, return an OrderedSet() of mismatched + Guard type objects. + """ + r = self.dynamo_guards.difference(other.dynamo_guards) + if len(r) == 0: + return None + return r + + def __eq__(self, other: object) -> bool: + if not isinstance(other, GuardsCheckpointState): + return False + return self.diff(other) is None + + +class ModuleContextCheckpointState: + nn_modules: dict[str, torch.nn.Module] = {} + + def __init__(self, nn_modules: dict[str, torch.nn.Module]) -> None: + self.nn_modules = nn_modules + + def diff(self, other: ModuleContextCheckpointState) -> set[str] | None: + """ + Produces a delta against another ModuleContextCheckpointState. + + Returns None if no delta is found, otherwise, return a set() of mismatched + module key names. + """ + r = set(self.nn_modules.keys()).difference(set(other.nn_modules.keys())) + if len(r) == 0: + return None + return r + + def __eq__(self, other: object) -> bool: + if not isinstance(other, ModuleContextCheckpointState): + return False + return self.diff(other) is None + + +class ModuleContext(Checkpointable[ModuleContextCheckpointState]): + def __init__(self) -> None: + self.nn_modules: dict[str, Any] = {} + + def copy_graphstate(self) -> ModuleContextCheckpointState: + return ModuleContextCheckpointState(dict(self.nn_modules)) + + def restore_graphstate(self, state: ModuleContextCheckpointState) -> None: + if not isinstance(state, ModuleContextCheckpointState): + raise AssertionError( + f"expected ModuleContextCheckpointState, got {type(state)}" + ) + self.nn_modules = state.nn_modules + + +class GlobalContextCheckpointState: + global_state: dict[str, tuple[Callable, Any]] = {} + + def __init__(self, global_states: dict[str, tuple[Callable, Any]]) -> None: + self.global_state = global_states + + def diff(self, other: GlobalContextCheckpointState) -> set[str] | None: + """ + Produces a delta against another GlobalContextCheckpointState. + + Returns None if no delta is found, otherwise, return a set() of mismatched + global key names. + """ + r = set(self.global_state.keys()).difference(set(other.global_state.keys())) + if len(r) == 0: + return None + return r + + def __eq__(self, other: object) -> bool: + if not isinstance(other, GlobalContextCheckpointState): + return False + return self.diff(other) is None + + +class GlobalContext(Checkpointable[GlobalContextCheckpointState]): + """ + This keeps track of the global torch state during tracing of a function. + For example, torch.is_grad_enabled. + """ + + _supported_global_states = { + "grad_enabled", + "autocast_enabled", + "autocast_cpu_enabled", + "autocast_gpu_dtype", + "autocast_cpu_dtype", + "autocast_cache_enabled", + } + + def __init__(self) -> None: + self.global_state: dict[str, tuple[Callable, Any]] = {} + + def copy_graphstate(self) -> GlobalContextCheckpointState: + return GlobalContextCheckpointState(self.global_state) + + def restore_graphstate(self, state: GlobalContextCheckpointState) -> None: + if not isinstance(state, GlobalContextCheckpointState): + raise AssertionError( + f"expected GlobalContextCheckpointState, got {type(state)}" + ) + self.global_state = state.global_state + if not ( + len(self.global_state) == len(self._supported_global_states) + and set(self.global_state.keys()) == self._supported_global_states + ): + raise AssertionError( + f"Global state mismatch: got keys {set(self.global_state.keys())}, " + f"expected {self._supported_global_states}" + ) + for func, args in self.global_state.values(): + func(args) + + +# Like a Set[Guard] but will record the user stack on all guards at the +# time they were installed at their destination +class GuardsSet: + def __init__(self, inner: OrderedSet[Guard] | None = None) -> None: + if inner is None: + self.inner: OrderedSet[Guard] = OrderedSet() + else: + self.inner = inner + # Map from source to list of guards with that source. Incrementally + # built in `add` method. + self.source_to_guards: defaultdict[Source, list[Guard]] = defaultdict(list) + for guard in self.inner: + self.track_guard_by_source(guard) + + def track_guard_by_source(self, guard: Guard) -> None: + source = guard.originating_source + self.source_to_guards[source].append(guard) + + def __iter__(self) -> Iterator[Guard]: + return iter(self.inner) + + def __len__(self) -> int: + return len(self.inner) + + # Subtraction along with bool is typically used to determine the delta of + # added guards between checkpoints for higher order ops + def __sub__(self, other: GuardsSet) -> GuardsSet: + return GuardsSet(self.inner - other.inner) + + def __bool__(self) -> bool: + return bool(self.inner) + + def clear(self) -> None: + self.inner = OrderedSet() + self.source_to_guards = defaultdict(list) + + def add( + self, guard: Guard, *, collect_debug_stack: bool = True, skip: int = 0 + ) -> None: + if guard in self.inner: + return + if collect_debug_stack: + if guard.stack is None: + guard.stack = CapturedTraceback.extract(skip=1 + skip) + if guard.user_stack is None: + guard.user_stack = TracingContext.extract_stack() + self.inner.add(guard) + self.track_guard_by_source(guard) + + def update(self, *others: set[Guard]) -> None: + for o in others: + for g in o: + self.add(g, skip=1) + + def get_guards_for_source(self, source: Source) -> list[Guard]: + """Return all guards with the given originating_source.""" + return list(self.source_to_guards[source]) + + def remove_guards_with_source(self, source: Source) -> None: + """Delete all guards that contains a given source""" + from ._dynamo.source import is_from_source + + self.inner = OrderedSet( + g for g in self.inner if not is_from_source(g.originating_source, source) + ) + # Rebuild the index since is_from_source walks the chain, so + # multiple source keys may need removal. + self.source_to_guards = defaultdict(list) + for guard in self.inner: + self.track_guard_by_source(guard) + + +""" +A GuardsContext is a checkpointable representation of all the guards in the current tracing +context. It's lifecycle is bound 1:1 to the tracing context, and it should never be instantiated +directly outside of it. For passing around internal state representations of this object, +prefer to extract them with copy_graphstate to produce a GuardsCheckpointState. +""" + + +class GuardsContext(Checkpointable[GuardsCheckpointState]): + def __init__(self) -> None: + self.dynamo_guards: GuardsSet = GuardsSet() + self.aotautograd_guards: list[GuardEnvExpr] = [] + self.skip_install: bool = False + + @contextlib.contextmanager + def skip_guard_install(self) -> Generator[None, None, None]: + old = self.skip_install + self.skip_install = True + try: + yield + finally: + self.skip_install = old + + def copy_graphstate(self) -> GuardsCheckpointState: + return GuardsCheckpointState(OrderedSet(self.dynamo_guards.inner)) + + def restore_graphstate(self, state: GuardsCheckpointState) -> None: + # NB: "steals" the passed in state + if not isinstance(state, GuardsCheckpointState): + raise AssertionError(f"expected GuardsCheckpointState, got {type(state)}") + self.dynamo_guards = GuardsSet(state.dynamo_guards) + + +class HopSubgraphCache: + @abstractmethod + def add_dynamo_installed_submodule( + self, fn_code: CodeType, identifier: str + ) -> None: ... + + @abstractmethod + def get_dynamo_installed_submodules(self, fn_code: CodeType) -> list[str]: ... + + @abstractmethod + def add_autograd_key_entry(self, identifier: str, key: Callable) -> None: ... + + @abstractmethod + def get_autograd_key_entry(self, identifier: str) -> Callable | None: ... + + @abstractmethod + def add_proxy_dispatch_entry(self, identifier: str, key: Callable) -> None: ... + + @abstractmethod + def get_proxy_dispatch_entry(self, identifier: str) -> Callable | None: ... + + @abstractmethod + def add_lazy_bwd_entry( + self, + identifier: str, + tangent_metadata: tuple[object], + gmod: torch.fx.GraphModule, + ) -> int: ... + + @abstractmethod + def get_lazy_bwd_entry( + self, identifier: str, tangent_metadata: tuple[object] + ) -> tuple[torch.fx.GraphModule | None, int | None]: ... + + +@dataclass +class InvokeSubgraphReuseEntry: + body_name: str + body_gmod: torch.fx.GraphModule + config: NestedCompileRegionOptions | None + subgraph_input_mapping: list[ + Any + ] # list[LiftedArgOrigin] - defined in invoke_subgraph.py + single_tensor_output: bool + # Per-output tensor metadata (shape, stride, dtype, device, requires_grad) + # cached from the first trace so we can construct fresh FakeTensors on + # cache hit without re-running body_gmod. + output_metadata: list[ + tuple[torch.Size, tuple[int, ...], torch.dtype, torch.device, bool] + ] + # 1-1 mapping to flat_vts: source for each flattened arg/kwarg, or None if + # the VT has no source. On cache hit, we build a source replacement mapping + # (old arg sources → new arg sources) to rewrite captured variable sources + # for the current invocation. + arg_sources: list[Source | None] + # Number of user-visible outputs (from the function return value). + # The graph may have additional outputs from side-effect intermediates; + # stamp_out_subgraph uses this to return only the user-visible slice. + num_user_outputs: int = 0 + + +@dataclass +class InvokeSubgraphReuseCondition: + # Per flattened input VT: (InputTag, metadata). + # (InputTag.TENSOR, TensorMetadata) + # (InputTag.SYMNODE, sym_num — same object implies same symbol) + # (InputTag.CONSTANT, value) + # (InputTag.MODULE, None) + # Tensor metadata is checked here because TENSOR_MATCH guards for + # subgraph inputs may already exist before tracing and thus won't + # appear in the guard delta. + input_checks: list[tuple[Any, object]] # list[tuple[InputTag, object]] + + # Guards captured during the trace (delta + source-mapped). + # Each entry: (source, handler, expected_value, guard) + # handler is a pre-resolved GuardCheckSpec from GUARD_VALUE_DISPATCH. + guards: list[tuple[Source, GuardCheckSpec, object, Guard]] + + # TreeSpec from pytree.tree_flatten of the (args, kwargs) structure. + # On cache hit, we verify the new call has the same treespec. + treespec: pytree.TreeSpec | None = None + + # All sources accessed via VariableBuilder during the subgraph trace. + # On cache hit, we check if any modified VT's source is a base of one + # of these to detect mutations on captured variables. + traced_sources: OrderedSet[Source] = dataclasses.field(default_factory=OrderedSet) + + +class InvokeSubgraphCache(HopSubgraphCache): + def __init__(self) -> None: + self.autograd_cache: dict[str, Callable] = {} + self.proxy_dispatch_cache: dict[str, Callable] = {} + self.dynamo_installed_submodules: dict[CodeType, list[str]] = defaultdict(list) + self.lazy_bwd_cache: dict[ + str, dict[tuple[object], tuple[torch.fx.GraphModule, int]] + ] = defaultdict(dict) + self.effects_cache: dict[ + str, set + ] = {} # Maps identifier -> set of effect types + # fn.__code__ → list of (condition, cache_entry) pairs. Walked linearly + # on lookup; first matching condition wins. + self.subgraph_reuse_cache: dict[ + CodeType, + list[tuple[InvokeSubgraphReuseCondition, InvokeSubgraphReuseEntry]], + ] = defaultdict(list) + # fn_code → {hash_key → cache_entry}. Used by user-provided + # reuse_hash_fn for O(1) subgraph reuse lookup. + self.subgraph_reuse_key_cache: dict[ + CodeType, dict[int, InvokeSubgraphReuseEntry] + ] = defaultdict(dict) + + def add_dynamo_installed_submodule( + self, fn_code: CodeType, identifier: str + ) -> None: + self.dynamo_installed_submodules[fn_code].append(identifier) + + def get_dynamo_installed_submodules(self, fn_code: CodeType) -> list[str]: + return self.dynamo_installed_submodules.get(fn_code, []) + + def add_autograd_key_entry(self, identifier: str, key: Callable) -> None: + self.autograd_cache[identifier] = key + + def get_autograd_key_entry(self, identifier: str) -> Callable | None: + return self.autograd_cache.get(identifier, None) + + def add_proxy_dispatch_entry(self, identifier: str, key: Callable) -> None: + self.proxy_dispatch_cache[identifier] = key + + def get_proxy_dispatch_entry(self, identifier: str) -> Callable | None: + return self.proxy_dispatch_cache.get(identifier, None) + + def add_lazy_bwd_entry( + self, + identifier: str, + tangent_metadata: tuple[object], + gmod: torch.fx.GraphModule, + ) -> int: + # Save the number of existing graph modules in the dictionary to get the suffix + num_gmods = len(self.lazy_bwd_cache[identifier]) + self.lazy_bwd_cache[identifier][tangent_metadata] = (gmod, num_gmods) + return num_gmods + + def get_lazy_bwd_entry( + self, identifier: str, tangent_metadata: tuple[object] + ) -> tuple[torch.fx.GraphModule | None, int | None]: + if identifier not in self.lazy_bwd_cache: + return (None, None) + + return self.lazy_bwd_cache[identifier].get(tangent_metadata, (None, None)) + + def add_effects(self, identifier: str, effects: set) -> None: + """Store the effect types for a given invoke_subgraph identifier.""" + if prev_effects := self.effects_cache.get(identifier, None): + if effects != prev_effects: + raise AssertionError( + "Different number of effects were found for invoke_subgraph " + f"call with identifier {identifier}. \n" + f"Previously we had the following effects: {prev_effects}.\n" + f"But now we have: {effects}." + ) + self.effects_cache[identifier] = effects + + def get_effects(self, identifier: str) -> set | None: + """Retrieve the effect types for a given invoke_subgraph identifier.""" + return self.effects_cache.get(identifier, None) + + def add_reuse_entry( + self, + fn_code: CodeType, + condition: InvokeSubgraphReuseCondition, + entry: InvokeSubgraphReuseEntry, + max_reuse_entries: int = 8, + ) -> None: + entries = self.subgraph_reuse_cache[fn_code] + if len(entries) >= max_reuse_entries: + raise RuntimeError( + f"invoke_subgraph: exceeded maximum reuse entries " + f"({max_reuse_entries}) for function code {fn_code}. " + f"This most likely means a guard keeps failing on every " + f"invocation, preventing subgraph reuse. " + f"Set TORCH_LOGS='+hierarchical_compile' to identify which " + f"guard is failing. If reuse is genuinely not possible and " + f"you need more cache entries, increase the limit via the " + f"max_reuse_entries argument to nested_compile_region()." + ) + entries.append((condition, entry)) + + def find_reuse_entry( + self, + fn_code: CodeType, + evaluator: Callable[ + [InvokeSubgraphReuseCondition, InvokeSubgraphReuseEntry], bool + ], + ) -> InvokeSubgraphReuseEntry | None: + entries = self.subgraph_reuse_cache.get(fn_code, []) + for i, (condition, entry) in enumerate(entries): + if evaluator(condition, entry): + # MRU: move the hit entry to the front for faster future lookups + if i > 0: + entries.insert(0, entries.pop(i)) + return entry + return None + + def find_reuse_entry_by_key( + self, fn_code: CodeType, hash_key: int + ) -> InvokeSubgraphReuseEntry | None: + return self.subgraph_reuse_key_cache.get(fn_code, {}).get(hash_key) + + def add_reuse_entry_by_key( + self, + fn_code: CodeType, + hash_key: int, + entry: InvokeSubgraphReuseEntry, + max_reuse_entries: int = 8, + ) -> None: + key_cache = self.subgraph_reuse_key_cache[fn_code] + if len(key_cache) >= max_reuse_entries and hash_key not in key_cache: + raise RuntimeError( + f"invoke_subgraph: exceeded maximum reuse entries " + f"({max_reuse_entries}) for function code {fn_code} (hash-key path). " + f"Increase the limit via the max_reuse_entries argument to " + f"nested_compile_region()." + ) + key_cache[hash_key] = entry + + +class HopDispatchSetCache: + def __init__(self) -> None: + # Delayed import to avoid circular dependency + from torch._higher_order_ops.invoke_subgraph import invoke_subgraph + + self.hop_cache_map = {invoke_subgraph: InvokeSubgraphCache()} + + def get_cache(self, op: torch._ops.HigherOrderOperator) -> HopSubgraphCache | None: + if op not in self.hop_cache_map: + return None + return self.hop_cache_map[op] # type: ignore[index] + + +_TLS = threading.local() + +""" +TracingContext is the source of truth for all currently accumulated information +needed to trace. Its lifecycle is kept 1:1 when using TorchDynamo, but other systems +are open to managing their own TracingContext with that in mind. + +The purpose of TracingContext is not to be a dumping ground, or god object, but rather to avoid +having to plumb complex subsystems across multiple verticals. + +Ex: A common example is guard accumulation between dynamo, shape_env, aot_autograd, and inductor. +Accessing the current tracing context via +TracingContext.get() allows users to accumulate their own guards for processing, without needing to know how +to plumb objects back up to where frame interpretation happened. + +Note that you can end up with multiple TracingContext for a single compilation +of a frame, as we reset the TracingContext whenever we restart analysis. +CompileContext is a more overarching context that encompasses multiple restarts. +""" + + +class CompileContext: + @staticmethod + def get() -> CompileContext: + if _TLS.compile_context is None: + raise AssertionError("compile_context is not set") + return _TLS.compile_context + + @staticmethod + def try_get() -> CompileContext | None: + return getattr(_TLS, "compile_context", None) + + def __init__(self, compile_id: CompileId | None) -> None: + if compile_id is not None and not isinstance(compile_id, CompileId): + raise AssertionError( + f"compile_id must be None or CompileId, got {type(compile_id)}" + ) + self.compile_id: CompileId | None = compile_id + self.attempt = 0 + # Verbose ShapeEnv guards produced. + self.shape_env_guards: list[str] = [] + + @staticmethod + def current_compile_id() -> CompileId | None: + self = CompileContext.try_get() + if self is None: + return None + return self.compile_id + + @staticmethod + def current_trace_id() -> TraceId | None: + self = CompileContext.try_get() + if self is None: + return None + if self.compile_id is None: + return None + return TraceId(self.compile_id, self.attempt) + + +@dataclass +class InlinedCodeCache: + """Cache for code-object-derived data used during inlining.""" + + instructions: list[Any] + indexof: dict[Any, int] + code_options: dict[str, Any] + + +class TracingContext: + """ + Provides the currently installed TracingContext, or None. + + Note that it is a staticmethod, and invocations outside of `with tracing()` (see below), are valid but + will return None. + """ + + @staticmethod + def try_get() -> TracingContext | None: + return getattr(_TLS, "tracing_context", None) + + @staticmethod + def get() -> TracingContext: + if ctx := TracingContext.try_get(): + return ctx + raise RuntimeError( + "TracingContext.get() must be called within an ongoing trace." + ) + + def __init__(self, fake_mode: FakeTensorMode | None) -> None: + self.guards_context = GuardsContext() + self.module_context = ModuleContext() + self.global_context = GlobalContext() + self.previously_inlined_functions: dict[Any, Any] = dict() + self.previously_cleaned_instructions: dict[Any, Any] = dict() + # Combined cache for inlined code data (instructions, indexof, code_options) + self.inlined_code_cache: dict[Any, InlinedCodeCache] = dict() + self.fake_mode: FakeTensorMode | None = fake_mode + self.frame_summary_stack: list[traceback.FrameSummary] = [] + # This is morally part of frame_summary_stack, but it is kept separate + # for clarity. As we process a frame, this variable gets updated + # to keep track of what line we are in the function. We make a + # function call, this gets cleared and the frame location is pushed + # to frame_summary_stack (prepping this variable for the inner frame's + # progress) + self.loc_in_frame: tuple[str, int, str] | None = None + # this is only set after aot_autograd + self.fw_metadata: ViewAndMutationMeta | None = None + # this is only set when the DDPOptimizer is used + self.ddp_optimizer_ctx: DDPOptimizerContext | None = None + # this is only set after aot_autograd + self.aot_graph_name: list[str] | None = None + self.params_flat: list[Any] | None = None + self.params_flat_unwrap_subclasses: list[Any] | None = None + self.params_unwrapped_to_flat_index: list[Any] | None = None + # this is for extended return calling convention from backend + # compiler to aot_autograd + # Per output, what the compiler specified stride of the output is, + # or None if no stride is known. This is always the HINT, it + # is never a SymInt (it would be better if it was a SymInt, but + # I can't conveniently get this from Inductor atm. Also, be + # careful not to accidentally induce guards on the SymInt if + # you ever do change this in aot_autograd.py; you should check + # on permutations preferentially.) + self.output_strides: list[tuple[int, ...] | None] | None = None + # When this is True, whenever we encounter an int in Dynamo tracing, + # we will (1) force unspec it and (2) force it as a size-like unbacked + # integer. This is currently used when processing certain lists of + # ints that are known to be size-like and may have 0/1 entries that we + # must not specialize on. + self.force_unspec_int_unbacked_size_like = False + # See note [Tensor Fakification and Symbol Caching] + self.tensor_to_context = WeakTensorKeyDictionary() + + # If this true, Aot Autograd will return output Fake Tensors with appropriate + # meta on the first invocation + # see note: [Returning Fake Tensors on First AOT Autograd Call] + self.fakify_first_call = False + self.hop_dispatch_set_cache = HopDispatchSetCache() + # list of code objects for inlined functions + self.traced_code: list[CodeType] = [] + self.cudagraph_annotation: Any = None + + def clear(self) -> None: + # Look at the note in output_graph.py in function `save_global_state` + # for the context on clearing global context. + self.global_context.global_state = {} + self.previously_inlined_functions.clear() + self.previously_cleaned_instructions.clear() + self.inlined_code_cache.clear() + + @staticmethod + @contextmanager + def patch(**kwargs: Any) -> Generator[None, None, None]: + prior = {} + ctx = TracingContext.get() + + for key in kwargs: + # KeyError on invalid entry + prior[key] = getattr(ctx, key) + for key, val in kwargs.items(): + setattr(ctx, key, val) + try: + yield + finally: + for key, val in prior.items(): + setattr(ctx, key, val) + + @staticmethod + def extract_stack() -> traceback.StackSummary: + self = TracingContext.try_get() + if self is None: + return traceback.StackSummary() + stack = self.frame_summary_stack + if self.loc_in_frame is not None: + stack = stack + [self._populate_loc_in_frame_summary()] + return traceback.StackSummary.from_list(stack) + + def _populate_loc_in_frame_summary(self) -> traceback.FrameSummary: + if self.loc_in_frame is None: + raise AssertionError("loc_in_frame must not be None") + filename, lineno, frame_name = self.loc_in_frame + return traceback.FrameSummary(filename, lineno, frame_name, lookup_line=False) + + # Call this when you want to call into some code that isn't necessarily + # associated with the current frame state + @staticmethod + @contextlib.contextmanager + def clear_frame() -> Generator[None, None, None]: + tc = TracingContext.get() + with ( + unittest.mock.patch.object(tc, "frame_summary_stack", []), + unittest.mock.patch.object(tc, "loc_in_frame", None), + ): + try: + yield + except Exception as e: + # Prevent real_stack from getting attached + # + # The invariant is that if an Exception as real_stack, we've + # appropriately attached a user stack and we no longer need to + # attach anything. Because we cannot conveniently interpose + # when an exception is thrown, we instead interpose everywhere + # we set what the user stack is set (using the context + # manager). However, our compiler stack does "tail calls" + # (when it calls into user compiler), at which point the + # parent exception frames would incorrectly attach an + # incorrect frame. + # + # However, if, somehow, someone raised an exception with this + # scope that had a stack (for example, because they are + # restoring the user stack state appropriately as they process + # node by node), we should respect it. Thus, we cannot + # unconditionally set None. + if not hasattr(e, "real_stack"): + e.real_stack = None # type: ignore[attr-defined] + raise + + @staticmethod + @contextlib.contextmanager + def current_frame( + frame_summary: traceback.FrameSummary | None, + ) -> Generator[None, None, None]: + # frame_summary can be None to solely take advantage of real_stack + # attachment to thrown exceptions + tc = TracingContext.get() + if frame_summary is not None: + tc.frame_summary_stack.append(frame_summary) + old = tc.loc_in_frame + tc.loc_in_frame = None + try: + yield + except Exception as e: + if not hasattr(e, "real_stack"): + e.real_stack = tc.extract_stack() # type: ignore[attr-defined] + raise + finally: + if frame_summary is not None: + tc.frame_summary_stack.pop() + tc.loc_in_frame = old + + @staticmethod + @contextlib.contextmanager + def report_output_strides() -> Generator[ + list[tuple[int, ...] | None] | None, None, None + ]: + tc = TracingContext.try_get() + if tc is None: + yield None + return + old_output_strides = tc.output_strides + tc.output_strides = [] + try: + yield tc.output_strides + finally: + tc.output_strides = old_output_strides + + @staticmethod + def set_current_loc(filename: str, lineno: int, frame_name: str) -> None: + # Save the current location in the frame. Lazily generate the + # framesummary. + TracingContext.get().loc_in_frame = (filename, lineno, frame_name) + + @staticmethod + def get_traced_code() -> list[CodeType] | None: + tc = TracingContext.try_get() + if tc is None: + return None + return tc.traced_code + + +@contextmanager +def compile_context( + context: CompileContext | None, +) -> Generator[CompileContext | None, None, None]: + old_context = getattr(_TLS, "compile_context", None) + _TLS.compile_context = context + try: + yield context + finally: + _TLS.compile_context = old_context + + +@contextmanager +def tracing( + context: TracingContext | None, +) -> Generator[TracingContext | None, None, None]: + """ + This function installs the passed in tracing context as a dynamic scoped + global variable. + + Calls to TracingContext.get() while not under a `with tracing()` context + will return None. + """ + old_context = getattr(_TLS, "tracing_context", None) + _TLS.tracing_context = context + try: + yield context + except Exception as e: + if not hasattr(e, "real_stack") and context is not None: + e.real_stack = context.extract_stack() # type: ignore[attr-defined] + raise + finally: + if ( + context is not None + and context.fake_mode is not None + and context.fake_mode.shape_env is not None + ): + context.fake_mode.shape_env.cleanup() + _TLS.tracing_context = old_context + + +@overload +def dataclass_with_cached_hash(cls: type[T], **kwargs: Any) -> type[T]: ... + + +@overload +def dataclass_with_cached_hash( + cls: None = None, **kwargs: Any +) -> Callable[[type[T]], type[T]]: ... + + +@dataclass_transform() +def dataclass_with_cached_hash( + cls: type[T] | None = None, **kwargs: Any +) -> type[T] | Callable[[type[T]], type[T]]: + def wrap(cls_inner: type[T]) -> type[T]: + new_cls = dataclasses.dataclass(cls_inner, **kwargs) + old_hash = cls_inner.__hash__ + + def __hash__(self) -> int: + if not hasattr(self, "_hash"): + object.__setattr__(self, "_hash", old_hash(self)) + return self._hash + + def __reduce__(self): + # Exclude _hash from pickling to ensure deterministic cache keys. + # The _hash is a cached value that can be nondeterministically computed + # (e.g., based on id() of objects), so it should not affect pickling. + fields = dataclasses.fields(self) + field_values = tuple(getattr(self, f.name) for f in fields if f.init) + return (self.__class__, field_values) + + new_cls.__hash__ = __hash__ + new_cls.__reduce__ = __reduce__ + return new_cls # type: ignore[return-value] + + if cls is None: + return wrap + + return wrap(cls) + + +# Subclasses can be found in torch/_dynamo/source.py +# TODO(voz): Consider a toplevel torch/_source.py +@dataclass_with_cached_hash(frozen=True) +class Source: + def is_dict_key(self) -> bool: + return False + + def is_ephemeral(self) -> bool: + return False + + def reconstruct(self, codegen: PyCodegen) -> None: + raise NotImplementedError + + @functools.cached_property + def guard_source(self) -> GuardSource: + raise NotImplementedError + + @property + def _name_template(self) -> str: + """ + A template for the name of the source. Used to prevent code duplication between + `name` and `get_value`. + + For non-ChainedSources, `name` and `get_value` use the returned string directly. + + For ChainedSources, `name` and `get_value` expect the return to be a format string + with `{0}` present - `name` and `get_value` will apply different values to this function's + returned format string. + """ + raise NotImplementedError + + @functools.cached_property + def name(self) -> str: + return self._name_template + + def get_value( + self, + globals: dict[str, Any], + locals: dict[str, Any], + cache: dict[Source, Any], + ) -> Any: + if self in cache: + return cache[self] + value = eval(self._name_template, globals, locals) + cache[self] = value + return value + + def make_guard(self, fn: Callable[..., Any]) -> Guard: + if self.guard_source is GuardSource.CONSTANT: + raise NotImplementedError + return Guard(self, fn) + + def is_specialized_nn_module(self) -> bool: + return self.guard_source.is_specialized_nn_module() + + def subguards_allowed(self) -> bool: + """True if you can guard on attributes of this""" + return self.guard_source != GuardSource.SYNTHETIC_LOCAL + + def clone(self, transform_fn: Callable[[Source], Source] | None = None) -> Source: + # Frozen dataclass, so returning self is effectively a clone. + # Subclasses with mutable state should override. + if transform_fn is not None: + return transform_fn(self) + return self + + +# Subclasses can be found in torch/_dynamo/source.py +@dataclass_with_cached_hash(frozen=True) +class ChainedSource(Source): + base: Source + + def is_dict_key(self) -> bool: + # Recurse until you either hit a ConstDictKey or a Source + return self.base.is_dict_key() + + def is_ephemeral(self) -> bool: + return self.base.is_ephemeral() + + @functools.cached_property + def guard_source(self) -> GuardSource: + return self.base.guard_source + + def get_base(self) -> Source: + current: Source = self + while isinstance(current, ChainedSource): + current = current.base + return current + + @functools.cached_property + def name(self) -> str: + return self._name_template.format(self.base.name) + + def get_value( + self, + globals: dict[str, Any], + locals: dict[str, Any], + cache: dict[Source, Any], + ) -> Any: + if self in cache: + return cache[self] + tmpvar = "tmp" + counter = 0 + while tmpvar in locals: + tmpvar = f"tmp{counter}" + counter += 1 + locals[tmpvar] = self.base.get_value(globals, locals, cache) + value = eval(self._name_template.format(tmpvar), globals, locals) + del locals[tmpvar] + cache[self] = value + return value + + def clone(self, transform_fn: Callable[[Source], Source] | None = None) -> Source: + cloned_fields: dict[str, Any] = {"base": self.base.clone(transform_fn)} + for f in dataclasses.fields(self): + if f.name == "base": + continue + val = getattr(self, f.name) + if isinstance(val, Source): + cloned_fields[f.name] = val.clone(transform_fn) + result = dataclasses.replace(self, **cloned_fields) + if transform_fn is not None: + result = transform_fn(result) + return result + + +def detect_fake_mode(inputs: Any = None) -> FakeTensorMode | None: + """ + Attempts to "detect" what the current fake mode is. If there is one ambiently + available from TracingContext, we preferentially use that. Otherwise, we + heuristically detect the fake mode via the following sources, in order of + priority: + + - Currently active fake mode on stack + - Fake mode associated with passed in tensors (inputs does not + have to be flattened) + """ + from torch._subclasses.fake_tensor import ( + FakeTensor, + FakeTensorMode, + get_plain_tensors, + ) + + # If TracingContext has a fake_mode, use it authoritatively. + # This is the case when Dynamo is driving compilation - any fake tensors + # from other modes in the inputs will be refakified by the caller. + if context := TracingContext.try_get(): + fake_mode = context.fake_mode + if fake_mode is not None: + return fake_mode + + fake_modes = [] + + from torch.utils._python_dispatch import _get_current_dispatch_mode_stack + + for i, m in enumerate(reversed(_get_current_dispatch_mode_stack())): + if isinstance(m, FakeTensorMode): + fake_modes.append((m, "active fake mode", i)) + + flat_inputs = pytree.tree_leaves(inputs) + for i, flat_input in enumerate(flat_inputs): + if isinstance(flat_input, FakeTensor): + fake_modes.append((flat_input.fake_mode, "fake tensor input", i)) + if is_traceable_wrapper_subclass(flat_input): + out: list[torch.Tensor | int | torch.SymInt] = [] + get_plain_tensors(flat_input, out=out) # type: ignore[arg-type] + fake_tensors: list[FakeTensor] = [ + x for x in out if isinstance(x, FakeTensor) + ] + fake_modes.extend( + [ + (tensor.fake_mode, f"subclass input {i}", ix) + for ix, tensor in enumerate(fake_tensors) + ] + ) + + if fake_modes: + fake_mode, desc1, i1 = fake_modes[0] + for m, desc2, i2 in fake_modes[1:]: + if fake_mode is not m: + raise AssertionError( + f"fake mode ({fake_mode}) from {desc1} {i1} doesn't match mode ({m}) from {desc2} {i2}\n\n" + f"fake mode from {desc1} {i1} allocated at:\n{fake_mode.stack}\n" + f"fake mode from {desc2} {i2} allocated at:\n{m.stack}" + ) + + return fake_mode + else: + return None + + +def active_fake_mode() -> FakeTensorMode | None: + """ + Inspects the dispatch mode stack for an active fake mode and returns it. + Returns None if no fake mode is active. + """ + from torch._subclasses.fake_tensor import FakeTensorMode + from torch.utils._python_dispatch import _get_current_dispatch_mode_stack + + for _, m in enumerate(reversed(_get_current_dispatch_mode_stack())): + if isinstance(m, FakeTensorMode): + return m + + return None diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..09bd8d188112e1b27ee2bbfabd1ffcd614bf7290 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/__init__.py @@ -0,0 +1,86 @@ +from torch._higher_order_ops._invoke_quant import ( + invoke_quant, + invoke_quant_packed, + InvokeQuant, +) +from torch._higher_order_ops.aoti_call_delegate import aoti_call_delegate +from torch._higher_order_ops.associative_scan import associative_scan +from torch._higher_order_ops.auto_functionalize import ( + auto_functionalized, + auto_functionalized_v2, +) +from torch._higher_order_ops.base_hop import BaseHOP +from torch._higher_order_ops.cond import cond +from torch._higher_order_ops.effects import with_effects +from torch._higher_order_ops.executorch_call_delegate import executorch_call_delegate +from torch._higher_order_ops.flat_apply import flat_apply +from torch._higher_order_ops.flex_attention import ( + flex_attention, + flex_attention_backward, +) +from torch._higher_order_ops.foreach_map import _foreach_map, foreach_map +from torch._higher_order_ops.hints_wrap import hints_wrapper +from torch._higher_order_ops.inline_asm_elementwise import inline_asm_elementwise +from torch._higher_order_ops.invoke_leaf_function import invoke_leaf_function +from torch._higher_order_ops.invoke_subgraph import invoke_subgraph +from torch._higher_order_ops.local_map import local_map_hop +from torch._higher_order_ops.map import map +from torch._higher_order_ops.out_dtype import out_dtype +from torch._higher_order_ops.print import print +from torch._higher_order_ops.run_const_graph import run_const_graph +from torch._higher_order_ops.scan import scan +from torch._higher_order_ops.strict_mode import strict_mode +from torch._higher_order_ops.torchbind import call_torchbind +from torch._higher_order_ops.while_loop import ( + while_loop, + while_loop_stack_output_op as while_loop_stack_output, +) +from torch._higher_order_ops.wrap import ( + dynamo_bypassing_wrapper, + inductor_compiled_code, + tag_activation_checkpoint, + wrap_activation_checkpoint, + wrap_with_autocast, + wrap_with_set_grad_enabled, +) + + +__all__ = [ + "cond", + "while_loop", + "invoke_subgraph", + "scan", + "map", + "flex_attention", + "flex_attention_backward", + "hints_wrapper", + "BaseHOP", + "flat_apply", + "foreach_map", + "_foreach_map", + "with_effects", + "tag_activation_checkpoint", + "auto_functionalized", + "auto_functionalized_v2", + "associative_scan", + "out_dtype", + "executorch_call_delegate", + "call_torchbind", + "run_const_graph", + "InvokeQuant", + "invoke_leaf_function", + "invoke_quant", + "invoke_quant_packed", + "wrap_with_set_grad_enabled", + "wrap_with_autocast", + "wrap_activation_checkpoint", + "dynamo_bypassing_wrapper", + "strict_mode", + "aoti_call_delegate", + "map", + "while_loop_stack_output", + "local_map_hop", + "print", + "inductor_compiled_code", + "inline_asm_elementwise", +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/_invoke_quant.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/_invoke_quant.py new file mode 100644 index 0000000000000000000000000000000000000000..4e125e68fa4f4e0b19af801301d9d04e7d1d0e41 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/_invoke_quant.py @@ -0,0 +1,61 @@ +# mypy: allow-untyped-defs +# need to fix prim_hop_base type annotations first + +import dataclasses + +import torch +from torch._higher_order_ops.base_hop import BaseHOP, FunctionWithNoFreeVars + + +class InvokeQuantTracer(BaseHOP): + def __init__(self) -> None: + super().__init__("invoke_quant_packed") + + def __call__(self, subgraph, *operands, scheme=None, quant_options=None): + subgraph = FunctionWithNoFreeVars(subgraph) + return super().__call__( + subgraph, *operands, scheme=scheme, quant_options=quant_options + ) + + +invoke_quant_packed = InvokeQuantTracer() + + +class InvokeQuantUnpacked(BaseHOP): + def __init__(self) -> None: + super().__init__("invoke_quant") + + +invoke_quant = InvokeQuantUnpacked() + + +@dataclasses.dataclass(frozen=True, repr=True) +class InvokeQuant: + """ + Invoke a quantization function that will be preserved as a single operator. Preservation + as a single operator aids in pattern matching and custom lowerings. + + The operation appears as: + torch.ops.higher_order.invoke_quant(subgraph, *args, scheme=scheme) + + Args: + codegen_low_precision: Use observed subgraph dtypes for codegen instead of + upcasting to fp32. Can improve performance for prologue fusion but + requires careful testing of numerics. + """ + + codegen_low_precision: bool = True + + def __call__( + self, + *args, + scheme: str | None = None, + **kwargs, + ): + if not torch.compiler.is_compiling(): + return args[0](*args[1:], **kwargs) + + if scheme is not None: + kwargs["scheme"] = scheme + + return invoke_quant_packed(*args, **kwargs, quant_options=self) # type: ignore[call-arg] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/aoti_call_delegate.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/aoti_call_delegate.py new file mode 100644 index 0000000000000000000000000000000000000000..4de05fe5080551c6741939a86de043e4fa82f7be --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/aoti_call_delegate.py @@ -0,0 +1,165 @@ +# mypy: allow-untyped-defs + +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from __future__ import annotations + +import torch +import torch.utils._pytree as pytree +from torch._ops import HigherOrderOperator +from torch._subclasses.fake_tensor import FakeTensor, FakeTensorMode +from torch.fx.experimental.proxy_tensor import ( + disable_proxy_modes_tracing, + ProxyTorchDispatchMode, + track_tensor_tree, +) + + +AOTI_LOWERED_MODULE = "AOTInductorEPModule/AOTInductorRunnerWrapper" + + +class AOTICallDelegate(HigherOrderOperator): + """aoti_call_delegate is a HOP for calling AOTInductor lowered submodule in ExportedProgram. + + It has the following signature: + aoti_call_delegate( + lowered_module: Union[AOTInductorEPModule, AOTInductorRunnerWrapper] + original_gm:fx.GraphModule, + weight_args: List[Tensor], + input_args: List[Tensor], + ) -> outputs: List[Tensor] + + where, + - lowered_module is the AOTInductor lowered submodule, backed by compiled .so file, supporting real tensor inputs + - original_gm is the stateless version of the original GraphModule before lowering, allowing FakeTensor propagation + - weight_args is the list of weights in original GraphModule, including parameters and buffers + - input_args is the list of flatten inputs + """ + + def __init__(self) -> None: + super().__init__("aoti_call_delegate") + + def __call__( + self, + lowered_module: AOTI_LOWERED_MODULE, # type: ignore[valid-type] + original_gm: torch.fx.GraphModule, + weight_args: list[torch.Tensor], + input_args: list[torch.Tensor], + ) -> list[torch.Tensor]: + # pyrefly: ignore [missing-attribute] + return super().__call__(lowered_module, original_gm, weight_args, input_args) + + +aoti_call_delegate = AOTICallDelegate() +aoti_call_delegate.fallthrough(torch._C.DispatchKey.PythonDispatcher) +aoti_call_delegate.fallthrough(torch._C.DispatchKey.PythonTLSSnapshot) +aoti_call_delegate.fallthrough(torch._C.DispatchKey.ADInplaceOrView) +aoti_call_delegate.fallthrough(torch._C.DispatchKey.AutocastCPU) + + +@aoti_call_delegate.py_impl(torch._C.DispatchKey.CompositeExplicitAutograd) +def call_delegate_cpu( + lowered_module: AOTI_LOWERED_MODULE, # type: ignore[valid-type] + original_gm: torch.fx.GraphModule, + weight_args: list[torch.Tensor], + input_args: list[torch.Tensor], +) -> list[torch.Tensor]: + # FX creates this immutable_dict/list concept. Get rid of this. + map_types: dict[type, type] = { + torch.fx.immutable_collections.immutable_dict: dict, + torch.fx.immutable_collections.immutable_list: list, + } + new_args = pytree.tree_map_only( + tuple(map_types.keys()), + lambda a: map_types[type(a)](a), + weight_args + input_args, + lambda a: isinstance(a, tuple(map_types.keys())), + ) + has_fake_args = any(isinstance(arg, FakeTensor) for arg in new_args) + if has_fake_args: + # use stateless original_gm for tracing with fake tensors + fake_out = original_gm(*new_args) + return fake_out + else: + # use AOTI Runner for real tensors + new_input_args = new_args[len(weight_args) :] + if type(lowered_module).__name__ == "AOTInductorRunnerWrapper": + return lowered_module(*new_input_args) # type: ignore[misc] + elif type(lowered_module).__name__ == "AOTInductorEPModule": + return lowered_module(new_input_args) # type: ignore[misc] + else: + raise RuntimeError( + f"Unexpected lowered_module type: {type(lowered_module)}." + ) + + +def trace_aoti_call_delegate( + proxy_mode, func_overload, lowered_module, original_gm, weight_args, input_args +): + proxy_mode.tracer.root.register_module("lowered_module", lowered_module) + proxy_mode.tracer.root.register_module("original_gm", original_gm) + + node_args = (lowered_module, original_gm, weight_args, input_args) + proxy_args = pytree.tree_map(proxy_mode.tracer.unwrap_proxy, node_args) + + out_proxy = proxy_mode.tracer.create_proxy( + "call_function", func_overload, proxy_args, {}, name="aoti_call_delegate" + ) + with disable_proxy_modes_tracing(): + out = call_delegate_cpu(lowered_module, original_gm, weight_args, input_args) + + return track_tensor_tree(out, out_proxy, constant=None, tracer=proxy_mode.tracer) + + +@aoti_call_delegate.py_impl(ProxyTorchDispatchMode) +def call_delegate_proxy_torch_dispatch_mode( + mode: ProxyTorchDispatchMode, + lowered_module: AOTI_LOWERED_MODULE, # type: ignore[valid-type] + original_gm: torch.fx.GraphModule, + weight_args: list[torch.Tensor], + input_args: list[torch.Tensor], +): + res = trace_aoti_call_delegate( + mode, aoti_call_delegate, lowered_module, original_gm, weight_args, input_args + ) + return res + + +@aoti_call_delegate.py_impl(FakeTensorMode) +def call_delegate_fake_tensor_mode( + mode: FakeTensorMode, + lowered_module: AOTI_LOWERED_MODULE, # type: ignore[valid-type] + original_gm: torch.fx.GraphModule, + weight_args: list[torch.Tensor], + input_args: list[torch.Tensor], +) -> list[torch.Tensor]: + with mode: + return call_delegate_cpu(lowered_module, original_gm, weight_args, input_args) + + +@aoti_call_delegate.py_functionalize_impl +def call_delegate_functionalize( + ctx, + lowered_module: AOTI_LOWERED_MODULE, # type: ignore[valid-type] + original_gm: torch.fx.GraphModule, + weight_args: list[torch.Tensor], + input_args: list[torch.Tensor], +): + unwrapped_weight_args = tuple( + ctx.unwrap_tensors(weight_arg) for weight_arg in weight_args + ) + unwrapped_input_args = tuple( + ctx.unwrap_tensors(input_arg) for input_arg in input_args + ) + with ctx.redispatch_to_next(): + res = aoti_call_delegate( + lowered_module, + original_gm, + unwrapped_weight_args, # type: ignore[arg-type] + unwrapped_input_args, # type: ignore[arg-type] + ) + return ctx.wrap_tensors(res) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/associative_scan.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/associative_scan.py new file mode 100644 index 0000000000000000000000000000000000000000..5876525ce433d7da42693d043ca91206226f694f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/associative_scan.py @@ -0,0 +1,923 @@ +# mypy: allow-untyped-defs +import functools +import itertools +from collections.abc import Callable +from typing import Any + +import torch +import torch._prims_common as utils +import torch.utils._pytree as pytree +from torch._C import DispatchKey +from torch._higher_order_ops.utils import ( + _maybe_compile_and_run_fn, + _maybe_run_with_interpreter, + check_input_alias_and_mutation_return_outputs, + check_meta_consistency, + create_bw_fn, + first_slice_copy, + first_slice_copy_with_grad, + materialize_as_graph, + reenter_make_fx, + save_values_for_backward, + saved_values, + split_into_chunks, + unique_graph_id, + validate_subgraph_args_types, +) +from torch._ops import HigherOrderOperator +from torch._subclasses.fake_tensor import FakeTensorMode +from torch.fx.experimental.proxy_tensor import ( + disable_proxy_modes_tracing, + ProxyTorchDispatchMode, + track_tensor_tree, +) + + +aten = torch._ops.ops.aten + + +def wrap_combine_fn_flat(*args, combine_fn, spec, num_leaves): + if len(args) != 2 * num_leaves: + raise AssertionError( + f"Combine_fn received wrong number of arguments, expected {2 * num_leaves}, but got {len(args)}" + ) + lhs = pytree.tree_unflatten(args[:num_leaves], spec) + rhs = pytree.tree_unflatten(args[num_leaves:], spec) + return combine_fn(lhs, rhs) + + +def _interleave(a, b, dim=0): + # https://stackoverflow.com/questions/60869537/how-can-i-interleave-5-pytorch-tensors + if b_trunc := (a.shape[dim] == b.shape[dim] + 1): + pad = ( + [0] * ((b.ndim - dim - 1) * 2 + 1) + + [1] + + [0] * (b.ndim * 2 - ((b.ndim - dim - 1) * 2 + 2)) + ) + b = torch.nn.functional.pad(b, pad) + + stacked = torch.stack([a, b], dim=dim + 1) + interleaved = torch.flatten(stacked, start_dim=dim, end_dim=dim + 1) + # pyrefly: ignore [unbound-name] + if b_trunc: + # TODO: find torch alternative for slice_along dim for torch.jit.script to work + interleaved = aten.slice(interleaved, dim, 0, b.shape[dim] + a.shape[dim] - 1) + return interleaved + + +def safe_map(f, *args): + args = list(map(list, args)) + n = len(args[0]) + for arg in args[1:]: + if len(arg) != n: + raise ValueError("length mismatch: {list(map(len, args))}") + + def nf(a): + return f(*a) + + return list(map(nf, zip(*args))) + + +class AssociativeScanOp(HigherOrderOperator): + def __init__(self): + super().__init__("associative_scan") + + def __call__(self, combine_fn, xs, additional_inputs): + # There is currently an issue that the ScanOp is sometimes called with + # the additional_inputs being a list. See https://github.com/pytorch/pytorch/issues/145785 + # Once this issue is resolved, the assertion should only allow tuples + # and the tuple cast should be removed + if not isinstance(additional_inputs, (tuple, list)): + raise AssertionError( + f"additional_inputs must be a tuple or list, got {type(additional_inputs)}" + ) + additional_inputs = ( + tuple(additional_inputs) + if isinstance(additional_inputs, list) + else additional_inputs + ) + validate_subgraph_args_types(additional_inputs) + # pyrefly: ignore [missing-attribute] + return super().__call__(combine_fn, xs, additional_inputs) + + # pyrefly: ignore [bad-override] + def gen_schema(self, combine_fn, xs, additional_inputs): + from torch._higher_order_ops.schema import HopSchemaGenerator + from torch._higher_order_ops.utils import materialize_as_graph + + # For associative scan, we need two copies of xs for the combine function + # The combine function takes two elements and returns one element + xs_slice1 = [first_slice_copy(x) for x in xs] + xs_slice2 = [first_slice_copy(x) for x in xs] + all_inputs = tuple(xs_slice1 + xs_slice2 + list(additional_inputs)) + + combine_gm: torch.fx.GraphModule = materialize_as_graph(combine_fn, all_inputs) + ( + _, + _, + _, + mutated_inputs, + outputs, + ) = check_input_alias_and_mutation_return_outputs(combine_gm) + if len(mutated_inputs) > 0: + raise RuntimeError( + "For associative_scan, combine_fn cannot have in-place mutations but found " + f"{mutated_inputs}-th inputs are mutated." + ) + + schema_gen = HopSchemaGenerator(self) + schema_gen.add_arg("combine_fn", combine_gm) + + for idx, x in enumerate(xs): + schema_gen.add_arg(f"xs{idx}", x) + + for idx, arg in enumerate(additional_inputs): + schema_gen.add_arg( + f"additional_input{idx}", + arg, + ) + + for out in outputs: + schema_gen.add_output(out) + + schema_gen.add_schema_tree_spec(combine_fn, xs, additional_inputs) + return schema_gen.gen_schema() + + +associative_scan_op = AssociativeScanOp() + + +def associative_scan( + combine_fn: Callable[[pytree.PyTree, pytree.PyTree], pytree.PyTree], + xs: pytree.PyTree, + dim: int, + reverse: bool = False, + combine_mode: str = "pointwise", +) -> torch.Tensor: + r""" + Performs an inclusive scan with an associative combine function. + + .. warning:: + + ``torch.associative_scan`` is a prototype feature in PyTorch. It currently + does not support autograd and you may run into miscompiles. + Read more about feature classification at: + https://pytorch.org/blog/pytorch-feature-classification-changes/#prototype + + This operator requires runtime code generation and so requires support for + ``torch.compile``. Further, only CUDA device codegen is supported at the moment. + + Args: + combine_fn (Callable): A binary callable with type ``(Tensor, Tensor) -> Tensor``, + or if input is a pytree ``(pytree, pytree) -> pytree``. + This function must be pure, i.e., no lifted arguments are supported at the moment, + satisfy the associative property and have no side-effects. + xs (torch.Tensor): The input tensor, or nested pytree of tensors. + All inputs are expected to have the same shape. + dim (int): the dimension to scan over + reverse (bool): A boolean stating if the scan should be reversed with respect to ``dim``, default ``False``. + combine_mode (str): A string indicating whether the ``combine_fn`` is ``pointwise`` or ``generic``, default ``pointwise``. + If ``combine_mode=pointwise``, ``combine_fn`` must be pure, may only contain pointwise operations + and ``xs`` must be CUDA tensors. + In all other cases ``combine_mode=generic`` should be used. + Note: ``combine_mode=pointwise`` is more efficient than ``combine_mode=generic``. + + + Example:: + + def add(x: torch.Tensor, y: torch.Tensor): + return x + y + + + cumsum = associative_scan(add, x, dim) + + """ + # TODO: Support lifted arguments in inductor for associative_scan + # TODO: Support autograd for cases with lifted arguments for combine_mode=pointwise + + # The reason we flatten xs before calling into dynamo is that + # we want to create a consistent input ordering for combine_fn + # and we also want to the input ordering matches the output ordering. + leaves_xs_orig, spec_xs = pytree.tree_flatten(xs) + + def _validate_input(cfn, lxs, d, r, cm): + # Basic arguments check + if not callable(cfn): + raise ValueError(f"Combine_fn must be a callable, but got {cfn}") + if not isinstance(d, int): + raise ValueError("Dim must be an int, but got " + str(type(d))) + if not isinstance(r, bool): + raise RuntimeError("Reverse must be a bool, but got " + str(type(r))) + if cm not in ["pointwise", "generic"]: + raise ValueError( + f"Combine_mode must either 'pointwise' or 'generic', but got {cm}" + ) + if cm == "pointwise" and not all(l.device.type in ("cuda", "xpu") for l in lxs): + raise ValueError( + "For combine_mode='pointwise', all input tensors need to be on CUDA or XPU" + ) + + # Checks for xs + if len(lxs) == 0: + raise ValueError("Expected at least 1 xs leaf") + if any(not isinstance(x, torch.Tensor) for x in lxs): + raise ValueError("xs leaves must be a Tensor") + if any(x.is_sparse for x in lxs): + raise ValueError( + "xs leaves must dense Tensors, consider using `to_dense()`" + ) + if any(x.ndim <= d for x in lxs): + raise ValueError( + "All xs leaves must at least have 'dim' number of dimensions and scan dimension > 0" + ) + if any(x.shape[d] == 0 for x in lxs): + raise ValueError( + "All xs leaves must at least have 'dim' number of dimensions and scan dimension > 0" + ) + + ndim = leaves_xs_orig[0].ndim + dim = utils.canonicalize_dim(ndim, dim) + + _validate_input(combine_fn, leaves_xs_orig, dim, reverse, combine_mode) + + # Move scan dim to 0 and always perform scan on dim 0 + leaves_xs = [torch.movedim(elem, dim, 0) for elem in leaves_xs_orig] + + if reverse: + leaves_xs = [torch.flip(elem, [0]) for elem in leaves_xs] + + if combine_mode == "generic": + # The generic_associative_scan implementation calls the combine_fn with a `batch` along the scan dimension + # For example, consider: + # def add(x: torch.Tensor, y: torch.Tensor): + # return x + y + # leaves = torch.tensor([[0.0, 1.0, 2.0, 3.0] + # [0.0, 1.0, 2.0, 3.0]]) + # which has shape 2 x 4; + # dim = 1; + # In the first iteration of `_scan` the combine_fn gets invoked with + # combine_fn([torch.tensor([[0.0, 2.0], + # [0.0, 2.0]])], + # [torch.tensor([[1.0, 3.0], + # [1.0, 3.0]])]) + # The arguments are of shape 2 x 2, but can be evaluated in parallel along the scan dimension. + combine_fn = functools.partial( + wrap_combine_fn_flat, + combine_fn=torch.vmap( + combine_fn, + in_dims=( + pytree.tree_unflatten([0] * len(leaves_xs), spec_xs), + pytree.tree_unflatten([0] * len(leaves_xs), spec_xs), + ), + out_dims=0, + ), + spec=spec_xs, + num_leaves=len(leaves_xs), + ) + out = generic_associative_scan(combine_fn, leaves_xs, additional_inputs=()) + out = pytree.tree_unflatten(out, spec_xs) + else: + combine_fn = functools.partial( + wrap_combine_fn_flat, + combine_fn=combine_fn, + spec=spec_xs, + num_leaves=len(leaves_xs), + ) + + def run_flattened_associative_scan(combine_fn, leaves_xs): + return associative_scan_op(combine_fn, leaves_xs, additional_inputs=()) + + out = _maybe_compile_and_run_fn( + run_flattened_associative_scan, + combine_fn, + leaves_xs, + ) + + if reverse: + out = pytree.tree_map(lambda elem: elem.flip([0]), out) + + out = pytree.tree_map(lambda elem: torch.movedim(elem, 0, dim), out) + + return out + + +def generic_associative_scan(operator, leaves, dim=0, additional_inputs=()): + r""" + This function performs the associative_scan operation. + The algorithm works by recursively collecting neighbours of ``leaves`` and subsequently + applying the ``operator`` on all pairs in parallel along ``dim``. + The results of the recursive calls are later combined. + + Args: + operator (Callable): A binary callable with type ``(Tensor, Tensor) -> Tensor``, + or if input is a pytree ``(pytree, pytree) -> pytree``. + This function must be pure, pointwise, and satisfy the associative property. + leaves (torch.Tensor): A list of torch.Tensors converted from the pytree of + ``xs`` provided to ``associative_scan``. + All inputs are expected to have the same shape. + dim (int): the dimension to scan over + additional_inputs (Tuple of tensors): A tuple of lifted parameters from the global scope. + This parameter will be populated internally. + + Example:: + + def add(x: torch.Tensor, y: torch.Tensor): + return x + y + + leaves = torch.tensor([0.0, 1.0, 2.0, 3.0]) + + First iteration of _scan -> + # odd_elems -> apply operator on all neighbours + # odd_elems = operator([torch.tensor([0.0, 2.0])], + # [torch.tensor([1.0, 3.0])]) + odd_elems = torch.tensor([1.0, 5.0]) + Second iteration of _scan -> + # odd_elems = operator([torch.tensor([1.0])], + # [torch.tensor([5.0])]) + odd_elems = torch.tensor([6.0]) + # even_elems -> apply operator on all odd_elems and + # every second element of ``elems``, starting from the second element. + # even_elems is expanded with the first element of ``elems`` + even_elems = [1.0] + # Merges odd_elems and even_elems + res = torch.tensor([1.0, 6.0]) + # even_elems -> apply operator on all odd_elems and + # every second element of ``elems``, starting from the second element. + # even_elems is expanded with the first element of ``elems`` + even_elems = [0.0, 3.0] + # Merges odd_elems and even_elems + res = torch.tensor([0.0, 1.0, 3.0, 6.0]) + + """ + + def call_operator(*args): + return pytree.tree_leaves(operator(*args)) + + def _scan(elems): + """Perform the actual recursive scan on ``elems``.""" + num_elems = elems[0].shape[dim] + + if num_elems < 2: + return elems + + reduced_elems = call_operator( + *[aten.slice(elem, dim, 0, -1, 2) for elem in elems], + *[aten.slice(elem, dim, 1, None, 2) for elem in elems], + *additional_inputs, + ) + + # Recursively compute scan for partially reduced tensors. + odd_elems = _scan(reduced_elems) + + if num_elems % 2 == 0: + even_elems = call_operator( + *[aten.slice(e, dim, 0, -1) for e in odd_elems], + *[aten.slice(e, dim, 2, None, 2) for e in elems], + *additional_inputs, + ) + else: + even_elems = call_operator( + *odd_elems, + *[aten.slice(e, dim, 2, None, 2) for e in elems], + *additional_inputs, + ) + + # The first element of a scan is the same as the first element + # of the original `elems`. + even_elems = [ + torch.cat([aten.slice(elem, dim, 0, 1), result], dim=dim) + if result.shape.numel() > 0 and elem.shape[dim] > 0 + else result + if result.shape.numel() > 0 + else aten.slice( + elem, dim, 0, 1 + ) # Jax allows/ignores concat with 0-dim, Pytorch does not + for (elem, result) in zip(elems, even_elems) + ] + + return list( + safe_map(functools.partial(_interleave, dim=dim), even_elems, odd_elems) + ) + + scans = _scan(leaves) + + return scans + + +def trace_associative_scan( + proxy_mode, + func_overload, + combine_fn: Callable, + xs: list[torch.Tensor], + additional_inputs: tuple[torch.Tensor], +): + from torch._dynamo.utils import clone_input + + with disable_proxy_modes_tracing(): + sample_xs = [first_slice_copy(x) for x in itertools.chain(xs, xs)] + sample_additional_inputs = [ + clone_input(x) if isinstance(x, torch.Tensor) else x + for x in additional_inputs + ] + combine_graph = reenter_make_fx(combine_fn)( + *sample_xs, *sample_additional_inputs + ) + + outputs = None + for node in combine_graph.graph.nodes: + if node.op == "output": + if outputs is not None: + raise AssertionError("found multiple output nodes in combine_graph") + if len(node.args) != 1: + raise AssertionError( + f"expected output node to have 1 arg, got {len(node.args)}" + ) + outputs = node.args[0] + + if outputs is None: + raise AssertionError("no output node found in combine_graph") + outputs = pytree.tree_leaves(outputs) + if len(outputs) != len(xs): + raise AssertionError( + f"expected combine_fn to return {len(xs)} results but got {len(outputs)}" + ) + + xs_fake_tensors: list[torch.Tensor | torch.SymInt | int] = [ + first_slice_copy(x) for x in xs + ] + output_fake_tensors: list[torch.Tensor | torch.SymInt | int] = [ + c.meta["val"] for c in outputs + ] + check_meta_consistency( + xs_fake_tensors, output_fake_tensors, "init", "carry", include_contiguity=False + ) + + _, combine_graph_name = unique_graph_id( + proxy_mode, prefix="associative_scan_combine_graph" + ) + + proxy_mode.tracer.root.register_module(combine_graph_name, combine_graph) + + args = (combine_graph, xs, additional_inputs) + proxy_args = pytree.tree_map(proxy_mode.tracer.unwrap_proxy, args) + out_proxy = proxy_mode.tracer.create_proxy( + "call_function", func_overload, proxy_args, {}, name="associative_scan" + ) + + with disable_proxy_modes_tracing(): + out = tuple(aten.clone(x) for x in xs) + + return track_tensor_tree(out, out_proxy, constant=None, tracer=proxy_mode.tracer) + + +@associative_scan_op.py_impl(DispatchKey.CompositeExplicitAutograd) +def associative_scan_op_dense(combine_fn, xs, additional_inputs): + return generic_associative_scan(combine_fn, xs, additional_inputs=additional_inputs) + + +class AssociativeScanAutogradOp(torch.autograd.Function): + r""" associative_scan + Example:: + xs = torch.arange(1, 5) = [1, 2, 3, 4] + + def combine_fn(a: torch.Tensor, b: torch.Tensor): + return a * b + + ys = associative_scan(comine_fn, xs), + which can be unpacked as: + ys0 = xs0 = 1 + ys1 = combine_fn(ys0, xs1) = combine_fn(1, 2) = 2 + ... + ysT = combine_fn(ys(T-1), xsT) = combine_fn(6, 4) = 24 + ys = [1, 2, 6, 24] + + This creates a recursive data dependency structure where each output yst + depends on all prior inputs xs0 through xst. The dependency can be visualized as: + + Level 0 (Input): xs0 xs1 xs2 xs3 xs4 + \ / | | | + \ / | | | + Level 1: ys1 ───────┘ | | + \ / | + \ / | + Level 2: ys2 ────────┘ | + \ / + \ / + Level 3: ys3 ────────────┘ + \ + \ + Level 4: ys4 + + + We could get the following backward gradient graph: + + + Level 0 (output): g_xs0 g_xs1 g_xs2 g_xs3 g_xs4 + \ / | | | + \ / | | | + Level 1: gl_ys1 ─> g_ys1 ──────┘ | | + \ / | + \ / | + Level 2: gl_ys2 ─> g_ys2 ────────┘ | + \ / + \ / + Level 3: gl_ys3 ─> g_ys3 ────────────┘ + \ + \ + Level 4: gl_ys4 ─> g_ys4, + + where gl_y1 is the gradient of the loss with respect to ys1 and the input of backward. + + To calculate the gradients of the inputs, the chain rule suggests: + + g_xs0 = g_ys1 + g_xs1 = g_ys1 * bw(ys0, xs1) = g_ys1 * bwxs01 + g_xs2 = g_ys2 * bw(ys1, xs2) = g_ys2 * bwxs12 + g_xs3 = g_ys3 * bw(ys2, xs3) = g_ys3 * bwxs23 + g_xs4 = g_ys4 * bw(ys3, xs4) = g_ys4 * bwxs34 + + Notice the bw(...) is just the single step bw (instantaneous gradients), whose formula can be computed from combine_fn. + For example bw(ys3, xs4) (also abbreviated with bwxs34) computes the gradients ∂/∂xs4 combine_fn(ys3, xs4). + Similarly, bw(ys4, ys3) (also abbreviated with bwys43) computes the gradients ∂/∂ys3 combine_fn(ys3, xs4). + + Let's break down how to calculate g_ys by recursively substituting the unknowns: + + g_ys1 = gl_ys1 + g_ys2 * bw(ys2, ys1) + = gl_ys1 + (gl_ys2 + g_ys3 * bw(ys3, ys2)) * bw(ys2, ys1) + = gl_ys1 + gl_ys2 * bw(ys2, ys1) + g_ys3 * bw(ys3, ys2) * bw(y2, y1) + = gl_ys1 + gl_ys2 * bw(ys2, ys1) + gl_ys3 * bw(ys3, ys2) * bw(y2, y1) \ + + g_ys4 * bw(ys4, ys3) * bw(ys3, ys2) * bw(ys2, ys1) + = gl_ys1 + gl_ys2 * bw(ys2, ys1) + gl_ys3 * bw(ys3, ys2) * bw(y2, y1) \ + + gl_ys4 * bw(ys4, ys3) * bw(ys3, ys2) * bw(ys2, ys1) + + Let's do the same for all the g_ys: + g_ys2 = gl_ys2 + gl_ys3 * bw(ys3, ys2) + gl_y4 * bw(ys4, ys3) * bw(ys3, ys2) + g_ys3 = gl_ys3 + gl_ys4 * bw(ys4, ys3) + g_ys4 = gl_ys4 + + Notice that the above can be re-written as columnwise multiplication of y_mat and gl_ys: + + g_ys1 1, bwys21, bwys321, bwys4321 gl_ys1 + g_ys2 = 0, 1 , bwys321, bwys4321 . gl_ys2 + g_ys3 0, 0 , 1 , bwys4321 gl_ys3 + g_ys4 0, 0 , 0 , 1 gl_ys4, + + where bwys21 is an abbreviation for bw(ys2, ys1), + bwys321 is an abbreviation for bw(ys3, ys2) * bw(ys2, ys1) so on and so forth. + + We could effectively compute the upper triangular matrix y_mat with: + cumprod([1, bwys21, bwys32, bwys43]) then masking out the values as needed. + Thus, only [1, bwys21, bwys32, bwys43] are required to compute the y_mat. + + + References: https://justintchiu.com/blog/pscan_diff/ + + NOTE: [associative_scan autograd implementation] + + The forward of associative_scan can be computed with the following steps: + + 1.) Compute the forward output of the associative_scan + ys = associative_scan(combine_fn, xs, additional_inputs) + + The backward of associative_scan can be computed with the following steps: + + 2.) Prepare the backward graph + We prepare the backward graph to be used in the backward function. + We utilize ``create_bw_fn`` to generate the joint function: + combine_fn_bw = create_bw_fn(combine_fn, operands) + where operands = [ys{t-1}, xst, additional_inputs] + + 3.) Materialize the ``combine_fn_bw`` + This is required because torch.compile and torch.autograd.grad + cannot trace through the joint backward function dynamically. + + 4.) Compute the single step bw (instantaneous gradients) at every step t + bwys{t-1}, bwxst = combine_fn_bw(ys{t-1}, xst, 1.) + Here we pass 1 as the upstream gradient to obtain the local partial derivatives. + + This gives: + bwys = [bw(ys1, ys0), bw(ys2, ys1), ..., bw(ysT, ys{T-1})] + bwxs = [bw(ys1, xs0), bw(ys2, xs1), ..., bw(ys{T-1}, xsT)] + + 5.) Compute the gradient transition matrix y_mat + + As shown in the example above, each input xst affects all later outputs ysi for i ≥ t. + According to the chain rule, each such path contributes a product of local gradients g_ysk. + + For example: + ∂ysT/∂xst = ∂ysT/∂ys{T-1} * ∂ys{T-1}/∂ys{T-2} * ... * ∂ys{t+1}/∂yst * ∂yst/∂xst + = bw(ysT, ys{T-1}) * bw(ys{T-1}, ys{T-2}) * ... * bw(ys{t+1}, yst) * bw(ys{t-1}, xst) + + This motivates the use of a cumulative product over bwys to compute all such paths efficiently. + + We now construct the matrix of gradient transition paths: + + 5.1 Repeat g_y values to form the base matrix + y_mat = [[1, bwys21, bwys32, bwys43], + [1, bwys21, bwys32, bwys43], + [1, bwys21, bwys32, bwys43], + [1, bwys21, bwys32, bwys43]] + + 5.2 Mask the lower triangle (inclusive) with 1s + y_mat = [[1, bwys21, bwys32, bwys43], + [1, 1 , bwys32, bwys43], + [1, 1 , 1 , bwys43], + [1, 1 , 1 , 1 ]] + + 5.3 Apply cumulative product row-wise + y_mat = cumprod(y_mat, dim=1) + Resulting in: + y_mat = [[1, bwys21, bwys32 * bwys21, bwys43 * bwys32 * bwys21], + [1, 1 , bwys32 , bwys43 * bwys32 ], + [1, 1 , 1 , bwys43 ], + [1, 1 , 1 , 1 ]] + + 5.4 Zero out the lower triangle (exclusive) + Final y_mat: + y_mat = [[1, bwys21, bwys32 * bwys21, bwys43 * bwys32 * bwys21], + [0, 1 , bwys32 , bwys43 * bwys32 ], + [0, 0 , 1 , bwys43 ], + [0, 0 , 0 , 1 ]] + + 6.) Scale the y_mat with the upstream gradients gl_ys + scaled_y_mat = y_mat * gl_ys + Each entry now holds the full contribution of ∂L/∂ysj to ∂L/∂xsi via the path through ysj. + + 7.) Reduce the scaled_y_mat with a row-wise sum + summed_y_mat = scaled_y_mat.sum(dim=1) + This accumulates all downstream contributions for each xst. + + 8.) Scale with the instantaneous input gradients bwxs + g_xs = summed_y_mat * bwxs + + This gives the final input gradients: + g_xs = [∂L/∂xs0, ∂L/∂xs1, ..., ∂L/∂xsT] + + NOTE: [scan partial grad handling] + If any element of xs or of the outputs does not require gradients + (i.e., requires_grad=False), then the corresponding gradients will be returned + as tensors of zeros with the same shape as the element. + """ + + @staticmethod + # pyrefly: ignore [bad-override] + def forward( + ctx, + combine_fn, + num_xs, + num_additional_inputs, + *operands, + ): + ctx._num_xs = num_xs + ctx._num_additional_inputs = num_additional_inputs + ctx._combine_fn = combine_fn + xs, additional_inputs = split_into_chunks( + operands, [num_xs, num_additional_inputs] + ) + + scan_length = xs[0].shape[0] + ctx._scan_length = scan_length + + # We snapshot the dispatch keys in forward for materializing the + # the bw_graph in backward. + ctx._fw_include_key_set = torch._C._dispatch_tls_local_include_set() + ctx._fw_exclude_key_set = torch._C._dispatch_tls_local_exclude_set() + + with torch._C._AutoDispatchBelowAutograd(): + # 1.) Compute the forward output of the associative_scan + ys = associative_scan_op(combine_fn, xs, additional_inputs) + save_values_for_backward(ctx, list(operands) + list(ys)) + + return (*ys,) + + @staticmethod + def backward(ctx, *gl_ys): + r""" + This function computes the gradients of the scan operation. + For a detailed description see the document above. + + Args: + flat_grads (torch.Tensor): The tensor of upstream gradients, or a nested pytree of tensors. + E.g.: Gradient of the loss with respect to the forward output ys + """ + + # The backward of associative_scan is always performed on the first dimension + dim = 0 + scan_length = ctx._scan_length + num_xs = ctx._num_xs + num_additional_inputs = ctx._num_additional_inputs + + # Extract the inputs to the forward path and outputs from the forward path + flat_args = saved_values(ctx) + xs, additional_inputs, outs = split_into_chunks( + flat_args, [num_xs, num_additional_inputs, num_xs] + ) + ndim = outs[0].ndim + + # First_slice_copy does not keep the original requires_grad flag, + # but we need it here in order to compute the correcte gradients + xs_slices = first_slice_copy_with_grad(itertools.chain(xs, xs)) + + # Construct the operands from the forward, fw_operands + # and the operands for a single event t of the forward, fw_operands_slice + fw_operands = (*xs, *additional_inputs) + fw_operands_slice = (*xs_slices, *additional_inputs) + + # 2.) Prepare the backward graph + combine_fn_bw = create_bw_fn(ctx._combine_fn, fw_operands_slice) + + # 3.) Materialize the ``combine_fn_bw`` + # TODO: we need to materialize the bw graphs because dynamo is unable to + # trace through the joint function when torch.compile torch.autograd.grad. + combine_fn_bw_gm = materialize_as_graph( + combine_fn_bw, + ( + *fw_operands_slice, + *[first_slice_copy(o) for o in outs], + ), + ctx._fw_include_key_set, + ctx._fw_exclude_key_set, + force_enable_grad=True, + ) + + # vmap joint graph over scan dimension to compute the individual + # gradients for each time slice ``t`` in parallel. + # This computation can be parallelized, as these are just the instantaneous gradients and not the full chain-rule + # pyrefly: ignore [bad-argument-type] + mapped_combine_fn_bw_gm = torch.vmap(combine_fn_bw_gm, 0, 0) + + # 4.) Compute the single step bw (instantaneous gradients) at every step ``t`` + # Use a ones_like tensor in order not to scale the bwyst and bwxst, + # with the upstream gradients yet. + # Note: All bwyst and bwxst are computed in parallel, thus the tensors bwys and bwxs are the result. + dummy_upstream_grad = (torch.ones_like(x) for x in gl_ys) + grads = mapped_combine_fn_bw_gm( + *(o.roll(1, dim) for o in outs), *fw_operands, *dummy_upstream_grad + ) + bwys, bwxs = split_into_chunks(grads, [num_xs, num_xs]) + + def compute_y_mat(bwys: torch.Tensor) -> torch.Tensor: + # Prepare a ones and a zeros helper mask in order to easily compute the y_mat + def compute_helper_tril_mask(diagonal): + def expand_masks(mask): + for _ in range(ndim - 1): + mask = mask.unsqueeze(-1) + return mask + + tril_mask = torch.tril( + torch.ones( + scan_length, scan_length, device=bwys.device, dtype=torch.bool + ), + diagonal=diagonal, + ) + tril_mask = expand_masks(tril_mask) + tril_mask = tril_mask.expand(-1, -1, *bwys.shape[1:]) + return tril_mask + + # The ones mask is used to fill the main diagonal and all elements below it with 1s + ones_mask = compute_helper_tril_mask(0) + + # The zero mask is used to set all elements below the main diagonal to 0 + zeros_mask = compute_helper_tril_mask(-1) + + # 5.1) Repeat the elements of bwys to form the square matrix + y_mat = bwys.unsqueeze(dim).repeat_interleave(scan_length, dim) + + # 5.2) Fill the lower triangular part, including the diagonal, + # of the h_mat with 1s. I.e., use the ones_mask to fill with 1s. + y_mat.masked_fill_(ones_mask, 1.0) + + # 5.3) Compute the cumulative products across dim + 1 + y_mat = y_mat.cumprod(dim=dim + 1) + + # 5.4) Replace the elements we filled with 1s before with 0s + y_mat.masked_fill_(zeros_mask, 0.0) + + return y_mat + + def compute_grad(bwxs, bwys, gl_ys): + # Set the first gradient component of bwxs to 1.0, per definition. + torch.select(bwxs, dim, 0).fill_(1.0) + + # 5.) Compute the gradient transition matrix + y_mat = compute_y_mat(bwys) + + # 6.) scale the y_mat with the upstream gradients gl_ys + scaled_y_mat = y_mat * gl_ys + + # 7.) Reduce the y_mat with sum along the columns to get the total contributions for xs_t + summed_y_mat = scaled_y_mat.sum(dim + 1) + + # 8.) Scale with the bwxs to obtain the final gradients g_xs + g_xs = summed_y_mat * bwxs + + return g_xs + + # Stack all leaves of the gradients along the first dimension. + # This is useful as later the gradients of those leaves can be computed in parallel. + bwxs_stacked_leaves = torch.stack(bwxs) + bwys_stacked_leaves = torch.stack(bwys) + gl_ys_stacked_leaves = torch.stack(gl_ys) + + # The compute_grad function is parallelized across all individual leaves of xs + # as these gradients can be computed independently from each other + # TODO: torch.vmap may create composability issues + compute_grad_mapped = torch.vmap(compute_grad, 0, 0) + + g_xs = compute_grad_mapped( + bwxs_stacked_leaves, bwys_stacked_leaves, gl_ys_stacked_leaves + ) + + # TODO: Currently the gradients for the additional_inputs are not computed properly + return *[None] * 3, *g_xs, *[None] * num_additional_inputs + + +@associative_scan_op.py_autograd_impl +def associative_scan_autograd(combine_fn, xs, additional_inputs): + num_xs = len(xs) + num_additional_inputs = len(additional_inputs) + + if num_additional_inputs > 0: + raise RuntimeError( + "Associative_scan does currently not support gradients for lifted parameters!" + ) + + flat_out = AssociativeScanAutogradOp.apply( + combine_fn, + num_xs, + num_additional_inputs, + *(tuple(xs) + tuple(additional_inputs)), + ) + return (*flat_out,) + + +@associative_scan_op.py_impl(ProxyTorchDispatchMode) +def associative_scan_proxy_mode(mode, combine_fn, xs, additional_inputs): + return trace_associative_scan( + mode, associative_scan_op, combine_fn, xs, additional_inputs + ) + + +@associative_scan_op.py_impl(FakeTensorMode) +def assoiciative_scan_fake_tensor_mode(mode, combine_fn, xs, additional_inputs): + with mode: + return tuple(x.clone() for x in xs) + + +@associative_scan_op.py_functionalize_impl +def associative_scan_functionalize(ctx, combine_fn, xs, additional_inputs): + from torch._higher_order_ops.utils import _check_alias_and_mutation + + unwrapped_xs = ctx.unwrap_tensors(xs) + unwrapped_additional_inputs = ctx.unwrap_tensors(additional_inputs) + with ctx.redispatch_to_next(): + functional_combine_fn = ctx.functionalize( + _maybe_run_with_interpreter(combine_fn) + ) + pre_dispatch = hasattr(ctx, "mode") and ctx.mode.pre_dispatch + sample_unwrapped_xs_sliced = [ + first_slice_copy(inp) for inp in itertools.chain(unwrapped_xs, unwrapped_xs) + ] + sample_inputs = list( + itertools.chain( + sample_unwrapped_xs_sliced, + unwrapped_additional_inputs, + ) + ) + _check_alias_and_mutation( + combine_fn, sample_inputs, "associative_scan", pre_dispatch + ) + ret = associative_scan_op( + functional_combine_fn, + unwrapped_xs, + unwrapped_additional_inputs, + ) + return ctx.wrap_tensors(ret) + + +def _fake_associative_scan(combine_fn, xs, dim, reverse=False): + inp_leaves, spec = pytree.tree_flatten(xs) + result_flat: list[Any] = [] + num_leaves = len(inp_leaves) + op = reversed if reverse else lambda x: x + + for ind in op(range(inp_leaves[0].size(dim))): + r = [ + inp_leaves[leave_ind][(slice(None),) * dim + (ind,)] + for leave_ind in range(num_leaves) + ] + if (ind > 0 and not reverse) or ( + ind < (inp_leaves[0].size(dim) - 1) and reverse + ): + r = combine_fn( + pytree.tree_unflatten(result_flat[-1], spec), + pytree.tree_unflatten(r, spec), + ) + r_flat, _ = pytree.tree_flatten(r) + result_flat.append(r_flat) + + results = [ + torch.stack([e[leave_ind] for e in op(result_flat)], dim) + for leave_ind in range(num_leaves) + ] + return pytree.tree_unflatten(results, spec) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/auto_functionalize.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/auto_functionalize.py new file mode 100644 index 0000000000000000000000000000000000000000..7c2d8d9828f8769e685b2543a0eda2650bdb84e9 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/auto_functionalize.py @@ -0,0 +1,1090 @@ +# mypy: allow-untyped-defs +import warnings +from abc import ABC, abstractmethod +from collections.abc import Callable, Sequence +from dataclasses import dataclass +from typing import Any, get_args + +import torch +import torch._library.utils as library_utils +import torch.utils._pytree as pytree +from torch import Tensor +from torch._C import DispatchKey +from torch._higher_order_ops.utils import ( + _has_gen_schema, + call_op, + HopInstance, + HopSchema, + materialize_callable_in_args, + unique_graph_id, +) +from torch._ops import HigherOrderOperator, OperatorBase, OpOverload +from torch._prims_common import clone_preserve_strides +from torch._subclasses.fake_tensor import FakeTensorMode +from torch.fx.experimental.proxy_tensor import ( + disable_proxy_modes_tracing, + ProxyTorchDispatchMode, + track_tensor_tree, +) + + +class SchemaHolder: + def __init__(self, schema: torch.FunctionSchema): + self.schema = schema + + def __eq__(self, other): + return self.schema == other.schema + + def __hash__(self) -> int: + return hash(self.schema) + + @classmethod + def from_tree_spec(cls, tree_spec: pytree.TreeSpec): + if tree_spec is None: + raise AssertionError("tree_spec cannot be None") + return cls(pytree.tree_unflatten([], tree_spec).schema) + + +# register_constant allows us to get a tree_spec from pytree.tree_flatten(SchemaHolder(FunctionSchema)). +# The tree_spec is proxable in the graph and we can get back the schema via +# schema = pytree.tree_unflatten([], tree_spec).schema +pytree.register_constant(SchemaHolder) + + +def get_base(tensor): + if torch.is_inference_mode_enabled(): + return tensor._inference_mode_base + else: + return tensor._base + + +class ViewInfo(ABC): + base_index: int + + def __init__(self, base_index): + self.base_index = base_index + + @abstractmethod + def regenerate_view(self, bases_list: list[Tensor]): + pass + + +@dataclass +class AsStridedViewInfo(ViewInfo): + size: Sequence[int | torch.SymInt] + stride: Sequence[int | torch.SymInt] + storage_offset: int + + def __init__(self, base_index, size, stride, storage_offset): + super().__init__(base_index) + self.size = size + self.stride = stride + self.storage_offset = storage_offset + + def regenerate_view(self, bases_list: list[Tensor]): + return torch.as_strided( + bases_list[self.base_index], + self.size, + self.stride, + self.storage_offset, + ) + + +@dataclass +class SliceViewInfo(ViewInfo): + dim: int | torch.SymInt + start: int | torch.SymInt + end: int | torch.SymInt + + def __init__(self, base_index, dim, start, end): + super().__init__(base_index) + self.dim = dim + self.start = start + self.end = end + + def regenerate_view(self, bases_list: list[Tensor]): + return torch.ops.aten.slice.Tensor( + bases_list[self.base_index], self.dim, self.start, self.end + ) + + +@dataclass +class AliasViewInfo(ViewInfo): + def __init__(self, base_index): + super().__init__(base_index) + + def regenerate_view(self, bases_list: list[Tensor]): + return torch.ops.aten.alias.default(bases_list[self.base_index]) + + +@dataclass +class NotView(ViewInfo): + def __init__(self, base_index): + super().__init__(base_index) + + def regenerate_view(self, bases_list: list[Tensor]): + return bases_list[self.base_index] + + +def is_alias(base, tensor): + from torch.fx.experimental.symbolic_shapes import statically_known_true, sym_eq + + return all( + statically_known_true(a) + for a in [ + sym_eq(base.storage_offset(), tensor.storage_offset()), + sym_eq(base.stride(), tensor.stride()), + sym_eq(base.size(), tensor.size()), + ] + ) + + +# return None or (dim, start, end) +def try_use_slice(base, tensor): + from torch.fx.experimental.symbolic_shapes import statically_known_true, sym_eq + + # This condition should never be triggered. + if is_alias(base, tensor): + return (0, 0, base.size()[0]) + + # TODO is there cases can we use slice even if stride or len(sizes) are not equal? + if not statically_known_true(sym_eq(tensor.stride(), base.stride())): + return None + if not statically_known_true(sym_eq(len(tensor.size()), len(base.size()))): + return None + + dim = None + count = 0 + for i in range(len(tensor.size())): + if base.size()[i] != tensor.size()[i]: + dim = i + count = count + 1 + if count != 1: + return None + + if tensor.storage_offset() % tensor.stride()[dim] != 0: + return None + start = tensor.storage_offset() // tensor.stride()[dim] + end = start + tensor.size()[dim] + return (dim, start, end) + + +def write_view_information_to_args( + mutable_arg_names: list[str], + mutable_arg_types: list[torch.Type], + kwargs: dict[str, Any], + arg_to_base_index: dict[str, Any], +): + """ + This function writes the view information into kwargs. It reads mutable_args from kwargs. + and uses arg_to_base_index and tensor information to write ViewInfo into kwargs. + mutable_arg_names: mutable custom operator arg names. + mutable_arg_types: mutable custom operator arg types. + kwargs: the original custom operator args. + arg_to_base_index: maps mutable_arg_name to int | [int] that refers to the base tensor that + corresponds to the input tensor + """ + + def write_single_view(prefix: str, tensor: Tensor, base_index: int): + if f"{prefix}_base_index" in kwargs: + raise AssertionError(f"{prefix}_base_index already in kwargs") + if f"{prefix}_size" in kwargs: + raise AssertionError(f"{prefix}_size already in kwargs") + if f"{prefix}_stride" in kwargs: + raise AssertionError(f"{prefix}_stride already in kwargs") + if f"{prefix}_storage_offset" in kwargs: + raise AssertionError(f"{prefix}_storage_offset already in kwargs") + + if f"{prefix}_slice_dim" in kwargs: + raise AssertionError(f"{prefix}_slice_dim already in kwargs") + if f"{prefix}_slice_start" in kwargs: + raise AssertionError(f"{prefix}_slice_start already in kwargs") + if f"{prefix}_slice_end" in kwargs: + raise AssertionError(f"{prefix}_slice_end already in kwargs") + + def use_as_strided(tensor): + kwargs[f"{prefix}_size"] = tensor.size() + kwargs[f"{prefix}_stride"] = tensor.stride() + kwargs[f"{prefix}_storage_offset"] = tensor.storage_offset() + + def use_slice(dim, start, end): + kwargs[f"{prefix}_slice_dim"] = dim + kwargs[f"{prefix}_slice_start"] = start + kwargs[f"{prefix}_slice_end"] = end + + def use_alias(): + kwargs[f"{prefix}_alias"] = True + + # The start if the function + if tensor is None: + kwargs[f"{prefix}_base_index"] = None + else: + base = get_base(tensor) + kwargs[f"{prefix}_base_index"] = base_index + if base is None: + # no need to add anything else other than _base_index + return + elif is_alias(base, tensor): + use_alias() + elif (slice_info := try_use_slice(base, tensor)) is not None: + use_slice(*slice_info) + else: + use_as_strided(tensor) + + for arg_name, arg_type in zip(mutable_arg_names, mutable_arg_types): + arg = kwargs[arg_name] + if library_utils.is_tensorlist_like_type(arg_type): + if arg is None: + kwargs[f"_{arg_name}_length"] = None + else: + kwargs[f"_{arg_name}_length"] = len(arg) + for i, elem in enumerate(arg): + write_single_view( + f"_{arg_name}_{i}", elem, arg_to_base_index[arg_name][i] + ) + + elif library_utils.is_tensor_like_type(arg_type): + write_single_view( + f"_{arg_name}", + kwargs[arg_name], + arg_to_base_index.get(arg_name), # type: ignore[arg-type] + ) + else: + raise RuntimeError(f"Unsupported type {arg_type}") + + +# Returns a dict of arg_name -> ViewInfo | [ViewInfo] +def read_view_information_from_args( + mutable_arg_names: list[str], + mutable_arg_types: list[torch.Type], + kwargs: dict[str, Any], + all_bases: list[Tensor], +): + """ + This reads the view information added by `write_view_information_to_args` from kwargs, pop them, + and returns a dict arg_name -> ViewInfo | [ViewInfo](if the input is list). that maps each mutable arg + to its view information. + mutable_arg_names: mutable custom operator arg names. + mutable_arg_types: mutable custom operator arg types. + kwargs : args of auto_functionalize(custom_op, kwargs) + """ + + def get_arg(name): + return kwargs.pop(name) + + def read_single_view(prefix): + base_index = get_arg(f"{prefix}_base_index") + if base_index is None: + return None + elif f"{prefix}_alias" in kwargs: + get_arg(f"{prefix}_alias") + return AliasViewInfo(base_index) + elif f"{prefix}_storage_offset" in kwargs: + # The view is regenerated using as_strided. + size = get_arg(f"{prefix}_size") + stride = get_arg(f"{prefix}_stride") + storage_offset = get_arg(f"{prefix}_storage_offset") + return AsStridedViewInfo(base_index, size, stride, storage_offset) + elif f"{prefix}_slice_dim" in kwargs: + dim = get_arg(f"{prefix}_slice_dim") + start = get_arg(f"{prefix}_slice_start") + end = get_arg(f"{prefix}_slice_end") + return SliceViewInfo(base_index, dim, start, end) + else: + # This means that the argument is the base tensor + return NotView(base_index) + + args_view_info: dict[str, Any] = {} + for arg_name, arg_type in zip(mutable_arg_names, mutable_arg_types): + if library_utils.is_tensorlist_like_type(arg_type): + length = get_arg(f"_{arg_name}_length") + if length is None: + # The whole list is None. + args_view_info[arg_name] = None + else: + args_view_info[arg_name] = [ + read_single_view(f"_{arg_name}_{i}") for i in range(length) + ] + + elif library_utils.is_tensor_like_type(arg_type): + args_view_info[arg_name] = read_single_view(f"_{arg_name}") + else: + raise RuntimeError(f"Unsupported type {arg_type}") + return args_view_info + + +# NOTE: [auto-functionalizing custom ops] +# Users may wish to torch.compile custom ops that mutate their inputs. +# torch.compile will automatically support this op without anyone needing +# to provide a functionalization kernel for it. Here's how. +# +# Let's say we have a hypothetical mylib::sin_(Tensor(a!) x) -> () +# op. First, when FakeTensor sees this op: +# - If the schema says it returns nothing, we can generate a trivial +# FakeTensor rule for it (that returns nothing). +# - Otherwise, the user needs to provide a FakeTensor impl (fake impl) +# +# Next, when Python FunctionalTensor sees the op, it will functionalize +# it by emitting a call to an auto_functionalize(op, ["x"], {"x": ...}) +# HOP and replacing the mutated inputs with corresponding outputs of this HOP. +# This HOP effectively runs the functional version of the op when +# called: it clones inputs that will be mutated, runs the op, and +# then returns (output, Tensors with the new values) +# +# auto_functionalize_v2 is an improved version of auto_functionalize that better handle +# re-inplacing views. + + +class AutoFunctionalized(HigherOrderOperator): + """auto_functionalized(_mutable_op, **kwargs) + + This HOP runs a "functional" version of _mutable_op. + + Concretely, it looks at all the arguments that are mutable through + _mutable_op's operator schema, clones those kwargs, runs + `out = _mutable_op(**kwargs)` with the cloned values, and then returns the + operator output concatenated with the cloned values that were mutated. + + We have some restrictions on `_mutable_op`. + See `can_auto_functionalize` for the restrictions. We can likely lift + many of these if users request it. + + The reason why _mutable_op is prefixed with an + underscore is to prevent collisions with kwarg names in **kwargs. + """ + + def __init__(self) -> None: + super().__init__("auto_functionalized", cacheable=True) + + def __call__( + self, + /, + _mutable_op: OpOverload, + **kwargs: Any, + ) -> tuple[Any, tuple[Tensor, ...]]: + if not can_auto_functionalize(_mutable_op): + raise AssertionError(f"Cannot auto-functionalize op {_mutable_op}") + if not isinstance(kwargs, dict): + raise AssertionError(f"kwargs must be a dict, got {type(kwargs)}") + # pyrefly: ignore [missing-attribute] + return super().__call__(_mutable_op, **kwargs) + + +auto_functionalized = AutoFunctionalized() +auto_functionalized.__module__ = "torch.ops.higher_order" + +auto_functionalized.fallthrough(DispatchKey.AutogradCPU) +auto_functionalized.fallthrough(DispatchKey.AutogradCUDA) + + +_MutableOpType = OpOverload | HigherOrderOperator + + +class AutoFunctionalizedV2(HigherOrderOperator): + """auto_functionalized_v2(_mutable_op, **kwargs) + + This HOP runs a "functional" version of _mutable_op. + Unlike AutoFunctionalized, this version is improved to better handle + view tensors. This version is only used in non export mode. + """ + + def __init__(self) -> None: + super().__init__("auto_functionalized_v2", cacheable=True) + + def __call__( + self, + /, + _mutable_op: _MutableOpType, + **kwargs: Any, + ) -> tuple[Any, tuple[Tensor, ...]]: + _op_to_check: OpOverload | HopInstance | None = None + if isinstance(_mutable_op, HigherOrderOperator): + _op_to_check = HopInstance( + _mutable_op, + SchemaHolder.from_tree_spec(kwargs.get("_op_schema")).schema, # type: ignore[arg-type] + ) + else: + _op_to_check = _mutable_op + + if _op_to_check is None: + raise AssertionError("_op_to_check cannot be None") + if not can_auto_functionalize(_op_to_check): + raise AssertionError(f"Cannot auto-functionalize op {_op_to_check}") + if not isinstance(kwargs, dict): + raise AssertionError(f"kwargs must be a dict, got {type(kwargs)}") + # pyrefly: ignore [missing-attribute] + return super().__call__(_mutable_op, **kwargs) + + +auto_functionalized_v2 = AutoFunctionalizedV2() +auto_functionalized_v2.__module__ = "torch.ops.higher_order" + +auto_functionalized_v2.fallthrough(DispatchKey.AutogradCPU) +auto_functionalized_v2.fallthrough(DispatchKey.AutogradCUDA) + + +def can_auto_functionalize( + op: OperatorBase | HopInstance, +) -> bool: + if isinstance(op, HopInstance): + # HOPs that implement gen_schema and schema is not functional are auto_functionalizable. + if not _has_gen_schema(op._op): + return False + + else: + if not isinstance(op, OpOverload): + return False + + if torch._library.utils.is_builtin(op): + # We control the built-ins. These may (in rare cases) + # do input metadata mutation (which we have banned on custom ops) + return False + + schema = op._schema + if not schema.is_mutable: + return False + schema = op._schema + + for arg in schema.arguments: + if arg.alias_info is None: + continue + if not arg.alias_info.is_write: + continue + if torch._library.utils.is_tensor_like_type(arg.type): + continue + if torch._library.utils.is_tensorlist_like_type(arg.type): + continue + return False + + if len(schema.returns) == 1 and isinstance(schema.returns[0].type, torch.NoneType): + # Skip schema returns -> None + return True + if isinstance(op, OpOverload): + # The returns of OpOverload must not alias anything + for ret in schema.returns: + if ret.alias_info is None and type(ret.type) is torch.TensorType: + continue + # Not yet supported: List[Tensor] return. + return False + if torch._C._dispatch_has_kernel_for_dispatch_key(op.name(), "Functionalize"): + return False + return True + + +def get_mutable_args_from_schema( + schema: torch.FunctionSchema, +) -> tuple[list[str], list[torch.Type]]: + """ + Returns the list of argument names that get mutated according to the + schema and their types. + """ + mutable_args_names = [ + arg.name + for arg in schema.arguments + if arg.alias_info is not None and arg.alias_info.is_write + ] + + mutable_args_types = [ + arg.type + for arg in schema.arguments + if arg.alias_info is not None and arg.alias_info.is_write + ] + return mutable_args_names, mutable_args_types # type: ignore[return-value] + + +def get_mutable_args(op: OpOverload) -> tuple[list[str], list[torch.Type]]: + return get_mutable_args_from_schema(op._schema) + + +def do_auto_functionalize( + mode: "torch._subclasses.functional_tensor.FunctionalTensorMode", + op: OpOverload, + args: tuple[Any, ...], + kwargs: dict[str, Any], +) -> Any: + """Functionalizes a call to op(*args, **kwargs) by emitting a call to + `outs = auto_functionalized(op, normalized_kwargs)` + and replacing the mutated (args, kwargs) with the corresponding outputs. + + The normalized_kwargs are just the (args, kwargs), but all in kwarg form. + This makes handling easier for the auto_functionalized HOP. + """ + from torch._subclasses.functional_tensor import PythonFunctionalizeAPI + + ctx = PythonFunctionalizeAPI(mode=mode) + + # All of the (args, kwargs), but all as kwargs. The names for the + # args come from the schema. This makes it easier for us to work with them. + normalized_kwargs = {} + schema = op._schema + for idx, arg in enumerate(schema.arguments): + # NB: torch_dispatch kwargs are the args defined as kwarg-only in the schema + if arg.name in kwargs: + normalized_kwargs[arg.name] = kwargs[arg.name] + elif idx < len(args): + # if its out of bounds we don't need to do anything + # as it means the optional arg was passed with its default + # value + normalized_kwargs[arg.name] = args[idx] + else: + normalized_kwargs[arg.name] = arg.default_value + + unwrapped_kwargs = ctx.unwrap_tensors(normalized_kwargs) # type: ignore[arg-type] + if "self" in unwrapped_kwargs or "self_" in unwrapped_kwargs: + warnings.warn( + "Using `self` or `self_` as an argument in the definition of custom ops may lead to ambiguous parsing. " + "Please consider using a different name for this argument to avoid potential issues.", + stacklevel=2, + ) + with ctx.redispatch_to_next(): + unwrapped_outs = auto_functionalized( + op, + **unwrapped_kwargs, # type: ignore[arg-type] + ) + + # List of the name of args that get mutated (according to the schema) + mutable_args_names, _ = get_mutable_args(op) + + unwrapped_actual_out: Any | tuple[Any] = unwrapped_outs[: -len(mutable_args_names)] + unwrapped_mutable_out = unwrapped_outs[-len(mutable_args_names) :] + + if len(op._schema.returns) == 0: + if unwrapped_actual_out[0] is not None: + raise AssertionError( + f"Expected None for op with no returns, got {unwrapped_actual_out[0]}" + ) + unwrapped_actual_out = None + elif len(op._schema.returns) == 1: + if len(unwrapped_actual_out) != 1: + raise AssertionError(f"Expected 1 output, got {len(unwrapped_actual_out)}") + unwrapped_actual_out = unwrapped_actual_out[0] + else: + if len(unwrapped_actual_out) != len(op._schema.returns): + raise AssertionError( + f"Expected {len(op._schema.returns)} outputs, got {len(unwrapped_actual_out)}" + ) + + for name, unwrapped_out in zip(mutable_args_names, unwrapped_mutable_out): + # Can be None if input was `Tensor(a!)?` + if unwrapped_out is None: + continue + + # We only handle Tensor or List[Tensor] here for now. + def sync_update(o, orig_arg): + ctx.replace(orig_arg, o) + ctx.commit_update(orig_arg) + ctx.sync(orig_arg) + + orig_arg = normalized_kwargs[name] + + if isinstance(unwrapped_out, torch.Tensor): + sync_update(unwrapped_out, orig_arg) + elif isinstance(unwrapped_out, list) and all( + isinstance(o, torch.Tensor) for o in unwrapped_out + ): + if len(orig_arg) != len(unwrapped_out): + raise AssertionError( + f"orig_arg length ({len(orig_arg)}) != unwrapped_out length ({len(unwrapped_out)})" + ) + for orig_a, o in zip(orig_arg, unwrapped_out): + sync_update(o, orig_a) + else: + raise RuntimeError( + f"unsupported type for auto-functionalization: {unwrapped_out}" + ) + + return ctx.wrap_tensors(unwrapped_actual_out) # type: ignore[arg-type] + + +# Wrapper for GraphModule that applies functionalization during execution to enable +# epilogue graph inlining and better fusion opportunities in subgraphs +# When tracing this wrapper, we'll get a graph module with epilogue. +# +# We want to hash it according to the original graph module, so that when we go +# from Functional mode -> fake mode for multiple invoke_subgraph calls that share, +# the same inner graph module, we can hit the cache. +class FunctionalCallableWithEpilogue: + def __init__(self, orig_callable: Callable): + self.orig_callable = orig_callable + # Propagate so callers pass inputs as a list, enabling input deallocation. + self._boxed_call = getattr(orig_callable, "_boxed_call", False) + + def __call__(self, *args, **kwargs): + # Functionalize to inline the epilogue graph (copy_ ops) for better fusion. + functionalized = torch.func.functionalize(self.orig_callable) + if self._boxed_call: + # Not all callers respect _boxed_call (e.g. reenter_make_fx + # always calls f(*args)). Detect which convention was used. + if len(args) == 1 and isinstance(args[0], list): + return tuple(functionalized(args[0])) + return tuple(functionalized(list(args))) + return tuple(functionalized(*args, **kwargs)) + + def __hash__(self): + return id(self.orig_callable) + + +def do_auto_functionalize_v2( + mode: "torch._subclasses.functional_tensor.FunctionalTensorMode", + op: OpOverload | HopInstance, + args: tuple[Any, ...], + kwargs: dict[str, Any], +) -> Any: + from torch._subclasses.functional_tensor import PythonFunctionalizeAPI + + ctx = PythonFunctionalizeAPI(mode=mode) + + # All of the (args, kwargs), but all as kwargs. The names for the + # args come from the schema. This makes it easier for us to work with them. + normalized_kwargs = {} + + schema = op._schema + # pyrefly: ignore [bad-assignment] + op = op._op if isinstance(op, HopInstance) else op + if not isinstance(op, get_args(_MutableOpType)): + raise AssertionError(f"Expected _MutableOpType, got {type(op)}") + + subgraph_arg_names = { + arg_info.name + for arg_info in schema.arguments + if isinstance(arg_info.type, torch._C.AnyType) + } + + def _maybe_functionalize(name: str, arg: Any) -> Any: + if name in subgraph_arg_names and callable(arg): + return FunctionalCallableWithEpilogue(arg) + return arg + + args = tuple( + _maybe_functionalize(schema.arguments[i].name, a) + if i < len(schema.arguments) + else a + for i, a in enumerate(args) + ) + kwargs = {k: _maybe_functionalize(k, v) for k, v in kwargs.items()} + + for idx, arg in enumerate(schema.arguments): + # NB: torch_dispatch kwargs are the args defined as kwarg-only in the schema + if arg.name in kwargs: + normalized_kwargs[arg.name] = kwargs[arg.name] + elif idx < len(args): + # if its out of bounds we don't need to do anything + # as it means the optional arg was passed with its default + # value + normalized_kwargs[arg.name] = args[idx] + else: + normalized_kwargs[arg.name] = arg.default_value + + # List of the name of args that get mutated (according to the schema) + mutable_args_names, mutable_args_types = get_mutable_args_from_schema(schema) + + # A list of all bases of mutable args without duplication + all_bases = [] + all_bases_addresses: list[int] = [] + + # Map arg_name to the index of its base in all_bases. + arg_to_base_index: dict[str, Any] = {} + + def update_dict(tensor, arg_name, index=None): + base = tensor if get_base(tensor) is None else get_base(tensor) + + def set_result(base_index): + if index is None: + arg_to_base_index[arg_name] = base_index + else: + arg_to_base_index[arg_name][index] = base_index + + if not all_bases_addresses.__contains__(base._cdata): + all_bases_addresses.append(base._cdata) + all_bases.append(base) + set_result(len(all_bases) - 1) + else: + set_result(all_bases_addresses.index(base._cdata)) + + for arg_name in mutable_args_names: + arg = normalized_kwargs[arg_name] + if arg is None: + continue + + if isinstance(arg, list): + arg_to_base_index[arg_name] = {} + for i, tensor in enumerate(arg): + if tensor is None: + arg_to_base_index[arg_name].append(None) + continue + + update_dict(tensor, arg_name, i) + + else: + update_dict(arg, arg_name) + + # add view_meta for each args into unwrapped_kwargs. + write_view_information_to_args( + mutable_args_names, + mutable_args_types, + normalized_kwargs, + arg_to_base_index, + ) + + # remove mutated args from the kwargs (its a function of _all_bases now) + for arg_name in mutable_args_names: + del normalized_kwargs[arg_name] # type: ignore[arg-type] + + unwrapped_kwargs = ctx.unwrap_tensors(normalized_kwargs) # type: ignore[arg-type] + if "self" in unwrapped_kwargs or "self_" in unwrapped_kwargs: + warnings.warn( + "Using `self` or `self_` as an argument in the definition of custom ops may lead to ambiguous parsing. " + "Please consider using a different name for this argument to avoid potential issues.", + stacklevel=2, + ) + all_basis_unwrapped = ctx.unwrap_tensors(all_bases) + + if "_all_bases" in unwrapped_kwargs: + raise AssertionError(f"_all_bases already in unwrapped_kwargs for {op}") + auto_func_kwargs = dict(unwrapped_kwargs, _all_bases=all_basis_unwrapped) + if isinstance(op, HigherOrderOperator): + if "_ops_schema" in unwrapped_kwargs: + raise AssertionError(f"_ops_schema already in unwrapped_kwargs for {op}") + # We pass in the tree_spec of tree_flatten(SchemaHolder) to make it proxable + auto_func_kwargs.update( + {"_op_schema": pytree.tree_flatten(SchemaHolder(schema))[1]} + ) + + with ctx.redispatch_to_next(): + unwrapped_outs = auto_functionalized_v2( + op, + **auto_func_kwargs, # type: ignore[arg-type] + ) + + unwrapped_actual_out: Any | tuple[Any] = ( + unwrapped_outs if len(all_bases) == 0 else unwrapped_outs[: -len(all_bases)] + ) + + unwrapped_mutable_out = ( + [] if len(all_bases) == 0 else unwrapped_outs[-len(all_bases) :] + ) + + if isinstance(op, HigherOrderOperator): + if len(schema.returns) <= 0: + raise AssertionError( + f"hop is expected to return at least one output {schema}." + ) + if len(unwrapped_actual_out) != len(schema.returns): + raise AssertionError( + f"Expected {len(schema.returns)} outputs, got {len(unwrapped_actual_out)}" + ) + else: + if len(schema.returns) == 0: + if unwrapped_actual_out[0] is not None: + raise AssertionError( + f"Expected None for op with no returns, got {unwrapped_actual_out[0]}" + ) + unwrapped_actual_out = None + elif len(schema.returns) == 1: + if len(unwrapped_actual_out) != 1: + raise AssertionError( + f"Expected 1 output, got {len(unwrapped_actual_out)}" + ) + unwrapped_actual_out = unwrapped_actual_out[0] + else: + if len(unwrapped_actual_out) != len(schema.returns): + raise AssertionError( + f"Expected {len(schema.returns)} outputs, got {len(unwrapped_actual_out)}" + ) + + for orig_arg, unwrapped_out in zip(all_bases, unwrapped_mutable_out): + # Can be None if input was `Tensor(a!)?` + if unwrapped_out is None: + continue + + # We only handle Tensor or List[Tensor] here for now. + def sync_update(o, orig_arg): + ctx.replace(orig_arg, o) + ctx.commit_update(orig_arg) + ctx.sync(orig_arg) + + if isinstance(unwrapped_out, torch.Tensor): + sync_update(unwrapped_out, orig_arg) + elif isinstance(unwrapped_out, list) and all( + isinstance(o, torch.Tensor) for o in unwrapped_out + ): + if len(orig_arg) != len(unwrapped_out): + raise AssertionError( + f"orig_arg length ({len(orig_arg)}) != unwrapped_out length ({len(unwrapped_out)})" + ) + for orig_a, o in zip(orig_arg, unwrapped_out): + sync_update(o, orig_a) + else: + raise RuntimeError( + f"unsupported type for auto-functionalization: {unwrapped_out}" + ) + + return ctx.wrap_tensors(unwrapped_actual_out) # type: ignore[arg-type] + + +# auto_functionalize functions +@auto_functionalized.py_impl(DispatchKey.CompositeExplicitAutograd) +def auto_functionalized_dense( + _mutable_op: OpOverload, + _only_clone_these_tensors: tuple[str, ...] | None = None, + **kwargs: Any, +) -> tuple[Any, tuple[Tensor, ...]]: + new_kwargs = dict(**kwargs) + result = [] + + _mutable_args_names, _ = get_mutable_args(_mutable_op) + for name in _mutable_args_names: + if ( + _only_clone_these_tensors is not None + and name not in _only_clone_these_tensors + ): + new_kwargs[name] = kwargs[name] + else: + new_kwargs[name] = ( + [clone_preserve_strides(x) for x in kwargs[name]] + if kwargs[name] is not None and isinstance(kwargs[name], list) + else ( + clone_preserve_strides(kwargs[name]) + if kwargs[name] is not None + else None + ) + ) + result.append(new_kwargs[name]) + out = _mutable_op(**new_kwargs) + + if isinstance(out, tuple): + return (*out, *result) # type: ignore[return-value] + else: + return (out, *result) # type: ignore[return-value] + + +@auto_functionalized.py_impl(FakeTensorMode) +def auto_functionalized_fake( + mode, + _mutable_op: OpOverload, + **kwargs: Any, +) -> tuple[Any, tuple[Tensor, ...]]: + with mode: + result = auto_functionalized_dense( + _mutable_op, _only_clone_these_tensors=None, **kwargs + ) + return result + + +@auto_functionalized.py_impl(ProxyTorchDispatchMode) +def auto_functionalized_proxy( + mode, + _mutable_op: OpOverload, + **kwargs: Any, +) -> tuple[Any, tuple[Tensor, ...]]: + with disable_proxy_modes_tracing(): + out = auto_functionalized(_mutable_op, **kwargs) + + proxy_kwargs = pytree.tree_map(mode.tracer.unwrap_proxy, kwargs) + out_proxy = mode.tracer.create_proxy( + "call_function", + auto_functionalized, + (_mutable_op,), + proxy_kwargs, + ) + result = track_tensor_tree(out, out_proxy, constant=None, tracer=mode.tracer) + return result + + +@auto_functionalized.py_functionalize_impl +def auto_functionalized_func(ctx, _mutable_op, **kwargs): + unwrapped_kwargs = ctx.unwrap_tensors(kwargs) + with ctx.redispatch_to_next(): + result = auto_functionalized(_mutable_op, **unwrapped_kwargs) + return ctx.wrap_tensors(result) + + +# auto_functionalized_v2 functions +@auto_functionalized_v2.py_impl(DispatchKey.CompositeExplicitAutograd) +def auto_functionalized_v2_dense( + _mutable_op: _MutableOpType, + _only_clone_these_bases: tuple[int, ...] | None = None, + **kwargs: Any, +) -> tuple[Any, tuple[Tensor, ...]]: + _all_bases: list[Tensor] = kwargs.pop("_all_bases", []) + if _only_clone_these_bases is None: + _only_clone_these_bases = tuple(range(len(_all_bases))) + + if isinstance(_mutable_op, OpOverload): + schema: torch._C.FunctionSchema = _mutable_op._schema + else: + schema = pytree.tree_unflatten([], kwargs.pop("_op_schema")).schema + + if isinstance(_mutable_op, OpOverload): + _callable_op: HopInstance | OpOverload = _mutable_op + else: + if not isinstance(schema, HopSchema): + raise AssertionError(f"Expected HopSchema, got {type(schema)}") + _callable_op = HopInstance(_mutable_op, schema) + + op_kwargs_new, all_bases_new = _generate_new_op_kwargs_from_bases( + schema, + kwargs, + _all_bases, + _only_clone_these_bases, + ) + + out = call_op( + _callable_op, + tuple(), + op_kwargs_new, + ) + + if isinstance(out, tuple): + return (*out, *all_bases_new) # type: ignore[return-value] + else: + return (out, *all_bases_new) # type: ignore[return-value] + + +def _generate_new_op_kwargs_from_bases( + schema, kwargs, all_bases, _only_clone_these_bases +): + mutable_args_names, mutable_args_types = get_mutable_args_from_schema(schema) + args_view_info = read_view_information_from_args( + mutable_args_names, mutable_args_types, kwargs, all_bases + ) + + def maybe_copy(i, t): + if t is None: + return None + if i in _only_clone_these_bases: + return clone_preserve_strides(t) + else: + return t + + all_bases_new = [maybe_copy(i, t) for i, t in enumerate(all_bases)] + + # create new args + new_kwargs = dict(**kwargs) + + # re-generate all inputs from all_bases_new using args_view_info and add them to new_kwargs. + for arg_name in mutable_args_names: + if args_view_info[arg_name] is None: + new_kwargs[arg_name] = None + elif isinstance(args_view_info[arg_name], list): + new_kwargs[arg_name] = [] + for i, elem in enumerate(args_view_info[arg_name]): + if elem is None: + new_kwargs[arg_name].append(None) + else: + view_info = args_view_info[arg_name][i] + new_kwargs[arg_name].append( + view_info.regenerate_view(all_bases_new) + ) + else: + new_kwargs[arg_name] = args_view_info[arg_name].regenerate_view( + all_bases_new + ) + + return new_kwargs, all_bases_new + + +@auto_functionalized_v2.py_impl(FakeTensorMode) +def auto_functionalized_v2_fake( + mode, + _mutable_op: _MutableOpType, + **kwargs: dict[str, Any], +) -> tuple[Any, tuple[Tensor, ...]]: + with mode: + result = auto_functionalized_v2_dense( + _mutable_op, _only_clone_these_bases=None, **kwargs + ) + return result + + +@auto_functionalized_v2.py_impl(ProxyTorchDispatchMode) +def auto_functionalized_v2_proxy( + mode, + _mutable_op: _MutableOpType, + **kwargs: Any, +) -> tuple[Any, tuple[Tensor, ...]]: + if isinstance(_mutable_op, HigherOrderOperator): + # Note [materialize callable inputs as graph] + # Below code materializes the callable inputs to the hop as graph modules. + # kwargs may contain general callables, that are not proxable e.g. FunctionWithNoFreeVars + # this could happen when we auto_functionalize the backward of the hop, + # where backward fn is a callablle that wraps forward graph module. + # This function materialize the callable args according to the schema of the hop. + + # We cannot materialize the callables in kwargs directly because the inputs to callable + # vary from hops to hop. To make the materialiation process generic to all hops, + # we trace a function that wraps the hop and let each hop itself figure out how to trace + # its callable inputs. Then we look at the schema of the traced hop node and replace the + # callable in original kwarg with the traced subgraphs. + # + # Specifically, we first trace a wrapped_fn that calls into the hop. Then we look for the + # hop node in the traced graph and graph module inputs to the hop. Finally, we replace the + # original kwarg's callable with the graph module. + all_bases = kwargs.get("_all_bases", []) + _only_clone_these_bases = kwargs.get("_only_clone_these_bases") + if _only_clone_these_bases is None: + _only_clone_these_bases = tuple(range(len(all_bases))) + + schema = pytree.tree_unflatten([], kwargs.get("_op_schema")).schema # type: ignore[arg-type] + new_kwargs, _ = _generate_new_op_kwargs_from_bases( + schema, + {k: v for k, v in kwargs.items() if k not in ("_all_bases", "_op_schema")}, + all_bases, + _only_clone_these_bases, + ) + + _, materialized_kwargs = materialize_callable_in_args( + HopInstance(_mutable_op, schema), tuple(), new_kwargs + ) + + # Only replace the callables in kwargs with the materialized subgraphs. + # The rest of the kwargs are kept unchanged. + subgraph_arg_names = { + arg_info.name + for arg_info in schema.arguments + if isinstance(arg_info.type, torch._C.AnyType) + } + for k, v in kwargs.items(): + if k in subgraph_arg_names and callable(v): + if k not in materialized_kwargs or not isinstance( + materialized_kwargs[k], torch.fx.GraphModule + ): + raise AssertionError( + f"Expected {k} to be in materialized_kwargs as a GraphModule" + ) + kwargs[k] = materialized_kwargs[k] + + with disable_proxy_modes_tracing(): + out = auto_functionalized_v2(_mutable_op, **kwargs) + + proxy_kwargs = pytree.tree_map(mode.tracer.unwrap_proxy, kwargs) + + if isinstance(_mutable_op, HigherOrderOperator): + + def _maybe_register_subgraph(val: Any): + if isinstance(val, torch.fx.GraphModule): + _, graph_name = unique_graph_id( + mode, prefix="auto_functionalized_subgraph" + ) + mode.tracer.root.register_module(graph_name, val) + return val + return val + + proxy_kwargs = pytree.tree_map(_maybe_register_subgraph, proxy_kwargs) + + out_proxy = mode.tracer.create_proxy( + "call_function", + auto_functionalized_v2, + (_mutable_op,), + proxy_kwargs, + ) + result = track_tensor_tree(out, out_proxy, constant=None, tracer=mode.tracer) + return result + + +@auto_functionalized_v2.py_functionalize_impl +def auto_functionalized_v2_func(ctx, _mutable_op, **kwargs): + unwrapped_kwargs = ctx.unwrap_tensors(kwargs) + with ctx.redispatch_to_next(): + result = auto_functionalized_v2(_mutable_op, **unwrapped_kwargs) + return ctx.wrap_tensors(result) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/base_hop.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/base_hop.py new file mode 100644 index 0000000000000000000000000000000000000000..60b14e03147f786a83520c717b9d77f438e981fd --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/base_hop.py @@ -0,0 +1,281 @@ +# mypy: allow-untyped-defs + +import abc + +import torch +import torch.utils._pytree as pytree +from torch._C import DispatchKey +from torch._dispatch.python import suspend_functionalization +from torch._higher_order_ops.auto_functionalize import FunctionalCallableWithEpilogue +from torch._higher_order_ops.utils import ( + check_input_alias_and_mutation_return_outputs, + HopInstance, + materialize_as_graph, + reenter_make_fx, +) +from torch._ops import HigherOrderOperator +from torch._subclasses import FakeTensorMode +from torch._subclasses.functional_tensor import disable_functional_mode +from torch.fx.experimental.proxy_tensor import ( + disable_proxy_modes_tracing, + ProxyTorchDispatchMode, + track_tensor_tree, +) + + +class BaseHOP(HigherOrderOperator, abc.ABC): + """ + This is the "Base" HOP implementation for a HOP that looks like: + + call_subgraph_hop(subgraph, *operands, **kwargs) + + That is: + 1) the HOP stays alive until Inductor + 2) the HOP's semantics are subgraph(*operands) + 3) kwargs may be some config options but aren't passed directly to the subgraph. + + To use this, please subclass this class and override methods as necessary: + ``` + class InvokeQuant(BaseHOP): + def __init__(self): + return super().__init__("invoke_quant") + + + invoke_quant = InvokeQuant() + + + def g(x): + return x.sin().cos() + + + @torch.compile(backend="aot_eager") + def f(x): + return invoke_quant(g, x, scheme="nf4") + ``` + + NOTE: don't subclass BaseHOP out of tree! That is not allowed. All + usages must be in tree. + """ + + def __init__(self, hop_name) -> None: + super().__init__(hop_name) + + # Set up the registrations + # If you want to override any of these, override them in your subclass. + self.py_autograd_impl(self._call_Autograd) + self.py_functionalize_impl(self._call_Functionalize) + self.py_impl(ProxyTorchDispatchMode)(self._call_ProxyTorchDispatchMode) + self.py_impl(FakeTensorMode)(self._call_FakeTensorMode) + self.py_impl(DispatchKey.CompositeExplicitAutograd)( + self._call_CompositeExplicitAutograd + ) + + def __call__(self, subgraph, *operands, **kwargs): + if not isinstance( + subgraph, + ( + torch.fx.GraphModule, + FunctionWithNoFreeVars, + FunctionalCallableWithEpilogue, + ), + ): + raise RuntimeError( + f"{self._name}: when calling this API without torch.compile, " + f"we require that the subgraph be a torch.fx.GraphModule (or " + f"a function we know doesn't have free variables)." + ) + # pyrefly: ignore [missing-attribute] + return super().__call__(subgraph, *operands, **kwargs) + + def _call_Autograd(self, subgraph, *operands, **kwargs): + if isinstance(subgraph, torch.fx.GraphModule): + pass + + # We assume the subgraph doesn't mutate inputs and there is no aliasing. + # In the PT2 stack, this is Dynamo's responsibility to figure out. + return BaseHOPFunction.apply(self, subgraph, kwargs, *operands) + + def _call_CompositeExplicitAutograd(self, subgraph, *operands, **kwargs): + from torch.utils._python_dispatch import _get_current_dispatch_mode + + mode = _get_current_dispatch_mode() + if mode is not None: + raise AssertionError("Mode should never be enabled for CPU/CUDA key") + return subgraph(*operands) + + def _call_ProxyTorchDispatchMode(self, proxy_mode, subgraph, *operands, **kwargs): + traced_graph = reenter_make_fx(subgraph)(*operands) + if not isinstance(proxy_mode.tracer, torch.fx.Tracer): + raise AssertionError( + f"expected proxy_mode.tracer to be torch.fx.Tracer, got {type(proxy_mode.tracer)}" + ) + qualname = proxy_mode.tracer.get_fresh_qualname("subgraph") + proxy_mode.tracer.root.register_module(qualname, traced_graph) + + node_args = (traced_graph, *operands) + proxy_args = pytree.tree_map(proxy_mode.tracer.unwrap_proxy, node_args) # type: ignore[attr-defined] + proxy_kwargs = pytree.tree_map(proxy_mode.tracer.unwrap_proxy, kwargs) # type: ignore[attr-defined] + out_proxy = proxy_mode.tracer.create_proxy( + "call_function", self, proxy_args, proxy_kwargs + ) + + out = self(subgraph, *operands, **kwargs) + return track_tensor_tree( + out, + out_proxy, + constant=None, + tracer=proxy_mode.tracer, # type: ignore[arg-type] + ) + + def _call_FakeTensorMode(self, mode, subgraph, *operands, **kwargs): + # TODO: this should probably route through FakeTensorMode to reuse caching + with mode: + return subgraph(*operands) + + # NOTE [Support input mutation of hops] + # To support input mutation, hop's subgraph must be functionalized because many inductor passes are + # applied to subgraph recursively and only work on functional graph. However, we could inline an + # epilogue graph (i.e. the copy_) into the subgraph because this is how input mutation + # is implemented in the top-level graph when no hop is presented. All passes must have been and will be + # aware of the epilogue graph. + # + # Since we've supported input mutation for custom op with auto_functionalized, we share the infra for hops + # The plan is: + # 1. In hop's Functionalization key, it calls do_auto_functionalize_v2 if subgraph mutates input + # 2. In do_auto_functionalize_v2: + # a. we functionalize the callables in hop's argument. This is to make the subgraphs functional so we + # could recursively run passes on them. Also the epilogue graph is inlined at the end. + # b. we call auto_functionalized_v2 and pass in an additional schema in order to properly invoke + # the hop with normalized kwargs. + # 3. In inductor, we decompose the auto_functionalized hop by callilng into the dense implementation, which + # copies the mutated inputs to the hop if necessary and call the hop. + # After these steps, the rest of the inductor stack knows how to fuse the copy_ in subgraph with other ops. + def _call_Functionalize(self, ctx, subgraph, *operands, **kwargs): + from torch._higher_order_ops.auto_functionalize import ( + can_auto_functionalize, + do_auto_functionalize_v2, + ) + + # invoke_quant has non-proxable argument of type InvokeQuant that + # we cannot generate schema for. + if self is not torch.ops.higher_order.invoke_quant_packed: + hop_instance = HopInstance.create(self, subgraph, *operands, **kwargs) + if can_auto_functionalize(hop_instance): + return do_auto_functionalize_v2( + ctx.mode, hop_instance, (subgraph, *operands), kwargs + ) + + unwrapped_operands = ctx.unwrap_tensors(operands) + with ctx.redispatch_to_next(): + # We assume the subgraph doesn't mutate inputs and there is no aliasing. + # In the PT2 stack, this is Dynamo's responsibility to figure out. + functionalized_subgraph = FunctionWithNoFreeVars( + ctx.functionalize(subgraph) + ) + out = self(functionalized_subgraph, *unwrapped_operands, **kwargs) + return ctx.wrap_tensors(out) + + # pyrefly: ignore [bad-override] + def gen_schema(self, subgraph, *operands, **kwargs): + from .schema import HopSchemaGenerator + + subgraph = materialize_as_graph(subgraph, operands) + ( + inp_inp_alias, + inp_out_alias, + out_out_alias, + mutated_inp_idx, + output, + ) = check_input_alias_and_mutation_return_outputs(subgraph) + + if not ( + len(inp_inp_alias) == 0 + and len(inp_out_alias) == 0 + and len(out_out_alias) == 0 + ): + # TODO: turn this into an error. + # test_foreach_map_backward_binary_foreach_map_addrecip_op fails the alias test. + import warnings + + warnings.warn( + "Aliasing is not supported for HOP subgraph.\n" + f"{subgraph.print_readable(print_output=False)}\n" + f"Alias info: inp-inp alias: {inp_inp_alias}, inp-out alias: {inp_out_alias}, out-out alias{out_out_alias}" + f"This may lead to silent incorrectness.", + stacklevel=2, + ) + + schema_gen = HopSchemaGenerator(self) + schema_gen.add_arg("subgraph", subgraph) + for idx, arg in enumerate(operands): + schema_gen.add_arg(f"arg{idx}", arg, is_mutated=idx in mutated_inp_idx) + + for name, arg in kwargs.items(): + schema_gen.add_arg(name, arg, default_value=arg, kw_only=True) + + for out in output: + schema_gen.add_output(out) + + return schema_gen.gen_schema() + + +class BaseHOPFunction(torch.autograd.Function): + @staticmethod + # pyrefly: ignore [bad-override] + def forward(ctx, hop, subgraph, kwargs, *operands): + ctx.hop = hop + ctx.operands = operands + ctx.subgraph = subgraph + ctx.kwargs = kwargs + + with torch._C._AutoDispatchBelowAutograd(): + return hop(subgraph, *operands, **kwargs) + + @staticmethod + def backward(ctx, *grad_outputs): + subgraph = ctx.subgraph + operands = ctx.operands + kwargs = ctx.kwargs + + # TODO: Something special needs to happen with min cut partitioner + with ( + suspend_functionalization(), + disable_functional_mode(), + torch.enable_grad(), + ): + with disable_proxy_modes_tracing(): + from .invoke_subgraph import create_fw_bw_graph + from .utils import _from_fun + + fw_inputs = pytree.tree_map(_from_fun, operands) + ( + _, + joint_graph, + _, + ) = create_fw_bw_graph(subgraph, fw_inputs, grad_outputs) + + # The joint graph returns (*grad_inputs, *fwd_outputs). + # We only need the grad_inputs. + def bwd_fn(*args): + operands = args[: -len(grad_outputs)] + grad_outs = args[-len(grad_outputs) :] + result = joint_graph(*operands, *grad_outs) + grad_inputs = result[: -len(grad_outputs)] + return grad_inputs + + return ( + None, + None, + None, + *ctx.hop( + FunctionWithNoFreeVars(bwd_fn), *operands, *grad_outputs, **kwargs + ), + ) + + +class FunctionWithNoFreeVars: + def __init__(self, fn): + self.fn = fn + + def __call__(self, *args, **kwargs): + return self.fn(*args, **kwargs) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/cond.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/cond.py new file mode 100644 index 0000000000000000000000000000000000000000..4f05618d38806b84bee09f192d1c468269e70100 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/cond.py @@ -0,0 +1,793 @@ +# mypy: allow-untyped-decorators +# mypy: allow-untyped-defs +import contextlib +import functools +import logging +import warnings +from collections.abc import Callable +from typing import Any + +import torch +import torch.utils._pytree as pytree +from torch._C import DispatchKey +from torch._C._functorch import ( + _add_batch_dim, + get_unwrapped, + is_batchedtensor, + maybe_get_bdim, +) +from torch._functorch.utils import exposed_in +from torch._higher_order_ops.utils import ( + _maybe_run_with_interpreter, + check_input_alias_and_mutation_return_outputs, + create_bw_fn, + fill_none_with_masks, + filter_with_masks, + materialize_as_graph, + reenter_make_fx, + save_values_for_backward, + saved_values, + unique_graph_id, + validate_subgraph_args_types, +) +from torch._ops import HigherOrderOperator +from torch._subclasses.fake_tensor import FakeTensor, FakeTensorMode +from torch.fx.experimental.proxy_tensor import ProxyTorchDispatchMode, track_tensor_tree +from torch.utils._python_dispatch import _get_current_dispatch_mode + + +log = logging.getLogger(__name__) + +""" +We're going to define a `cond_op` operation. +In order to do this, we need implementations for each of the dispatch keys. +""" + + +class CondOp(HigherOrderOperator): + def __init__(self): + super().__init__("cond") + + def __call__(self, pred, true_fn, false_fn, operands): + validate_subgraph_args_types(operands) + # pyrefly: ignore [missing-attribute] + return super().__call__(pred, true_fn, false_fn, operands) + + # pyrefly: ignore [bad-override] + def gen_schema(self, pred, true_fn, false_fn, operands): + from torch._higher_order_ops.schema import HopSchemaGenerator + from torch._higher_order_ops.utils import materialize_as_graph + + then_gm: torch.fx.GraphModule = materialize_as_graph(true_fn, operands) + else_gm: torch.fx.GraphModule = materialize_as_graph(false_fn, operands) + ( + _, + _, + _, + then_mutated_inputs, + then_outputs, + ) = check_input_alias_and_mutation_return_outputs(then_gm) + ( + _, + _, + _, + else_mutated_inputs, + else_outputs, + ) = check_input_alias_and_mutation_return_outputs(else_gm) + mutated_inputs = set(then_mutated_inputs) | set(else_mutated_inputs) + + schema_gen = HopSchemaGenerator(self) + schema_gen.add_arg("pred", pred) + schema_gen.add_arg("true_fn", then_gm) + schema_gen.add_arg("false_fn", else_gm) + for idx, arg in enumerate(operands): + schema_gen.add_arg(f"operand{idx}", arg, is_mutated=idx in mutated_inputs) + + for out in then_outputs: + schema_gen.add_output(out) + schema_gen.add_schema_tree_spec(pred, true_fn, false_fn, operands) + return schema_gen.gen_schema() + + +cond_op = CondOp() + + +@exposed_in("torch") +def cond( + pred: bool | int | float | torch.Tensor, + true_fn: Callable, + false_fn: Callable, + operands: tuple | list = (), +) -> Any: + r""" + Conditionally applies `true_fn` or `false_fn`. + + .. warning:: + + `torch.cond` is a prototype feature in PyTorch. It has limited support for input and output types. + Please look forward to a more stable implementation in a future version of PyTorch. + Read more about feature classification at: https://pytorch.org/blog/pytorch-feature-classification-changes/#prototype + + `cond` is structured control flow operator. That is, it is like a Python if-statement, + but has restrictions on `true_fn`, `false_fn`, and `operands` that enable it to be + capturable using torch.compile and torch.export. + + Assuming the constraints on `cond`'s arguments are met, `cond` is equivalent to the following:: + + def cond(pred, true_branch, false_branch, operands): + if pred: + return true_branch(*operands) + else: + return false_branch(*operands) + + Args: + pred (Union[bool, torch.Tensor]): A boolean expression or a tensor with one element, + indicating which branch function to apply. + + true_fn (Callable): A callable function (a -> b) that is within the + scope that is being traced. + + false_fn (Callable): A callable function (a -> b) that is within the + scope that is being traced. The true branch and false branch must + have consistent input and outputs, meaning the inputs have to be + the same, and the outputs have to be the same type and shape. Int + output is also allowed. We'll make the output dynamic by turning it + into a symint. + + operands (Tuple of possibly nested dict/list/tuple of torch.Tensor): A tuple of inputs to the + true/false functions. It can be empty if true_fn/false_fn doesn't require input. Defaults to (). + + Example:: + + def true_fn(x: torch.Tensor): + return x.cos() + + + def false_fn(x: torch.Tensor): + return x.sin() + + + return cond(x.shape[0] > 4, true_fn, false_fn, (x,)) + + Restrictions: + - The conditional statement (aka `pred`) must meet one of the following constraints: + + - It's a `torch.Tensor` with only one element, and torch.bool dtype + + - It's a boolean expression, e.g. `x.shape[0] > 10` or `x.dim() > 1 and x.shape[1] > 10` + + - The branch function (aka `true_fn`/`false_fn`) must meet all of the following constraints: + + - The function signature must match with operands. + + - The function must return a tensor with the same metadata, e.g. shape, + dtype, etc. + + - The function cannot have in-place mutations on global variables. + (Note: in-place tensor operations such as `add_` for intermediate results + are allowed in a branch) + + - The function can perform in-place mutations on its input tensors during inference (i.e., + when `torch.is_grad_enabled()` is False). + Note: When using `torch.compile()` with a non-constant predicate, the outputs will always + be new tensors that do not share object identity with the original inputs. + + Example:: + + def true_fn(x): + return x.sin_() + + + def false_fn(x): + return x + 1 + + + def f(x): + return cond(x.sum() > 0, true_fn, false_fn, (x,)) + + + x = torch.ones(4) + with torch.no_grad(): + result = torch.compile(f)(x) + assert result is not x # result is a new tensor, not the original x + + """ + if torch.compiler.is_dynamo_compiling(): + return cond_op(pred, true_fn, false_fn, operands) + + if isinstance(pred, (bool, int, float)): + # This is the non-strict export case. Strict export and torch.compile are + # handled above in dynamo. + if torch.compiler.is_compiling(): + warnings.warn( + "Pred is a Python constant. When used with torch.cond, it specializes on one of the branches." + " If you want torch.cond to preserve two branches, please make the predicate a boolean tensor or a SymBool.", + UserWarning, + stacklevel=2, + ) + # This is the eager case. We can just run the true or false branch. + if pred: + return true_fn(*operands) + else: + return false_fn(*operands) + + def _validate_input(pred, true_fn, false_fn, operands): + if not isinstance(pred, (bool, torch.Tensor, torch.SymBool)): + raise RuntimeError(f"Expected pred to be bool or tensor, but got {pred}.") + + if isinstance(pred, torch.Tensor) and pred.numel() != 1: + raise RuntimeError( + f"Expected pred to be bool or single-element tensor, but got {pred}." + ) + + if not callable(true_fn) or not callable(false_fn): + raise RuntimeError("Expect both branches to be callable.") + + if not isinstance(operands, (tuple, list)) or pytree.tree_any( + lambda t: not isinstance(t, torch.Tensor), operands + ): + raise RuntimeError( + "Expect operands to be a tuple of possibly nested dict/list/tuple that only " + f"consists of tensor leaves, but got {operands}." + ) + + _validate_input(pred, true_fn, false_fn, operands) + + if not torch._dynamo.is_dynamo_supported(): + raise RuntimeError("torch.cond requires dynamo support.") + + # Dynamo is expecting a callable with "__code__" attribute. + # We cannot directly pass cond_op to it. So we wrap it in a dummy function. + def _cond_op_wrapper(*args, **kwargs): + return cond_op(*args, **kwargs) + + from torch._higher_order_ops.utils import _hop_compile_and_call + + return _hop_compile_and_call(_cond_op_wrapper, (pred, true_fn, false_fn, operands)) + + +def trace_cond(proxy_mode, func_overload, pred, true_fn, false_fn, operands): + if not isinstance(operands, (list, tuple)): + raise AssertionError( + f"Cond operands must be a list or tuple of tensors and SymInts {operands}" + ) + + true_graph = reenter_make_fx(true_fn)(*operands) + false_graph = reenter_make_fx(false_fn)(*operands) + + true_outs = [] + false_outs = [] + for node in true_graph.graph.nodes: + if node.op == "output": + true_outs.extend(node.args) + + for node in false_graph.graph.nodes: + if node.op == "output": + false_outs.extend(node.args) + + flat_true_outs = pytree.arg_tree_leaves(*true_outs) + flat_false_outs = pytree.arg_tree_leaves(*false_outs) + if len(flat_true_outs) != len(flat_false_outs): + raise torch._dynamo.exc.CondOpArgsMismatchError( + f"Expected to return same number of outputs but got:" + f"\n true branch returns {len(flat_true_outs)} item(s)" + f"\n false branch returns {len(flat_false_outs)} item(s)" + ) + + i, true_name = unique_graph_id(proxy_mode, prefix="true_graph") + + false_name = f"false_graph_{i}" + if hasattr(proxy_mode.tracer.root, false_name): + raise AssertionError( + f"proxy_mode.tracer.root already has attribute {false_name}" + ) + + proxy_mode.tracer.root.register_module(true_name, true_graph) + proxy_mode.tracer.root.register_module(false_name, false_graph) + + args = (pred, true_graph, false_graph, operands) + + proxy_args = pytree.tree_map(proxy_mode.tracer.unwrap_proxy, args) + + out_proxy = proxy_mode.tracer.create_proxy( + "call_function", func_overload, proxy_args, {} + ) + + out = func_overload(pred, true_graph, false_graph, operands) + + return track_tensor_tree(out, out_proxy, constant=None, tracer=proxy_mode.tracer) + + +@cond_op.py_impl(DispatchKey.CompositeExplicitAutograd) +def cond_op_dense(pred, true_fn, false_fn, operands): + if not all(isinstance(o, (torch.Tensor, int)) for o in operands): + raise AssertionError( + f"Dense implementation operands must be a list of tensors and ints {operands}" + ) + mode = _get_current_dispatch_mode() + if mode is not None: + raise AssertionError("Mode should never be enabled for CPU/CUDA key") + if pred: + return true_fn(*operands) + else: + return false_fn(*operands) + + +class CondAutogradOp(torch.autograd.Function): + @staticmethod + # pyrefly: ignore [bad-override] + def forward( + ctx, + pred, + true_fn, + false_fn, + *operands, + ): + ctx._pred = pred + ctx._true_bw_fn = create_bw_fn( + true_fn, + operands, + ) + ctx._false_bw_fn = create_bw_fn( + false_fn, + operands, + ) + # We snapshot the dispatch keys in forward for materializing the + # the bw_graph in backward. + ctx._fw_include_key_set = torch._C._dispatch_tls_local_include_set() + ctx._fw_exclude_key_set = torch._C._dispatch_tls_local_exclude_set() + save_values_for_backward(ctx, operands) + + with torch._C._AutoDispatchBelowAutograd(): + return cond_op(pred, true_fn, false_fn, operands) + + @staticmethod + def backward(ctx, *flat_grads): + operands = saved_values(ctx) + args = operands + flat_grads + # TODO: we need to materialize the bw graphs because dynamo is unable to + # trace through the joint function when torch.compile torch.autograd.grad. + + grads_tensor_masks = [] + + def create_fn_remove_none(fn): + @functools.wraps(fn) + def wrapped(*args): + nonlocal grads_tensor_masks + + true_outputs = fn(*args) + grads_tensor_masks = [ + bool(isinstance(out, torch.Tensor)) for out in true_outputs + ] + return filter_with_masks(true_outputs, grads_tensor_masks) + + return wrapped + + true_bw_gm = materialize_as_graph( + create_fn_remove_none(ctx._true_bw_fn), + args, + ctx._fw_include_key_set, + ctx._fw_exclude_key_set, + force_enable_grad=True, + ) + false_bw_gm = materialize_as_graph( + create_fn_remove_none(ctx._false_bw_fn), + args, + ctx._fw_include_key_set, + ctx._fw_exclude_key_set, + force_enable_grad=True, + ) + grads = cond_op( + ctx._pred, + true_bw_gm, + false_bw_gm, + args, + ) + return None, None, None, *fill_none_with_masks(grads, grads_tensor_masks) + + +# Note: +# As long as one of the tensors in pred or operands requires grad, +# all the output would require grad with backward fn set to be the CondAutogradOp. +# This is consistent with autograd.Function's semantic. +@cond_op.py_autograd_impl +def cond_autograd(pred, true_fn, false_fn, operands): + return CondAutogradOp.apply( + pred, + true_fn, + false_fn, + *operands, + ) + + +@cond_op.py_impl(ProxyTorchDispatchMode) +def inner(mode, pred, true_fn, false_fn, operands): + return trace_cond(mode, cond_op, pred, true_fn, false_fn, operands) + + +@cond_op.py_impl(FakeTensorMode) +def cond_fake_tensor_mode(mode, pred, true_fn, false_fn, operands): + # Ignore here, because if you've gotten here but you're not manually + # tracing the inner graphs, that means that you intend to reuse the graph + # directly. Which means the old unbacked symbol bindings are appropriate. + # This strategy will not work if unbacked symbols can escape. + ignore_fresh_unbacked = contextlib.nullcontext() + if mode.shape_env: + ignore_fresh_unbacked = mode.shape_env.ignore_fresh_unbacked_symbols() + + with mode, ignore_fresh_unbacked: + flat_true_outs, true_out_spec = pytree.tree_flatten(true_fn(*operands)) + flat_false_outs, false_out_spec = pytree.tree_flatten(false_fn(*operands)) + if true_out_spec != false_out_spec: + raise RuntimeError( + "Unmatched output spec from torch.cond branches: " + f"true branch tree_spec {true_out_spec} vs false branch tree_spec {false_out_spec}." + ) + + merged_outs = [] + for true_out, false_out in zip(flat_true_outs, flat_false_outs): + merged_outs.append(_merge_output(true_out, false_out, mode)) + return pytree.tree_unflatten(merged_outs, true_out_spec) + + +def check_tensor_meta_match( + t1: torch.Tensor, t2: torch.Tensor, attr_names: tuple[str, ...], msg_prefix: str +) -> None: + def _get_attr_maybe_call(t: torch.Tensor, attr_name: str) -> Any: + attr = getattr(t, attr_name) + if callable(attr): + return attr() + return attr + + for attr_name in attr_names: + lattr = _get_attr_maybe_call(t1, attr_name) + rattr = _get_attr_maybe_call(t2, attr_name) + torch._check( + lattr == rattr, + lambda: f"{msg_prefix} expected same {attr_name} but got {lattr} and {rattr}.", + ) + + +def _merge_output( + a: torch.Tensor | int | None, + b: torch.Tensor | int | None, + mode: FakeTensorMode, +): + from torch.fx.experimental.symbolic_shapes import ( + has_free_unbacked_symbols, + SymIntEqByExpr, + ) + + if a is None or b is None: + if not (a is None and b is None): + raise AssertionError(f"expected both a and b to be None, got a={a}, b={b}") + return None + + def min_max(s0, s1): + def _bound(s0, lower_bound: bool): + if isinstance(s0, int): + return s0 + r = mode.shape_env.var_to_range.get( # type: ignore[union-attr] + s0.node.expr, + torch.utils._sympy.value_ranges.ValueRanges.unknown(), + ) + return r.lower if lower_bound else r.upper + + return min(_bound(s0, True), _bound(s1, True)), max( + _bound(s0, False), _bound(s1, False) + ) + + if type(a) is int and type(b) is int: + if a == b: + return a + if mode.shape_env is None: + raise AssertionError("mode.shape_env is None") + merged_out = mode.shape_env.create_unbacked_symint() + mode.shape_env.constrain_symbol_range(merged_out.node.expr, *min_max(a, b)) + return merged_out + + if not (type(a) is FakeTensor and type(b) is FakeTensor): + raise AssertionError( + f"expected both a and b to be FakeTensor, got a={type(a)}, b={type(b)}" + ) + + # Note: we don't check size, stride because + # they'll be merged with unbacked symints if they differ. + _meta_to_check = { + "dtype", + "device", + "layout", + "dim", + "is_quantized", + "is_conj", + "is_sparse", + "storage_offset", + } + check_tensor_meta_match( + a, + b, + tuple(_meta_to_check), + msg_prefix="When merging two branches' output in torch.cond, ", + ) + # NYI + if a.is_quantized or b.is_quantized: + raise AssertionError("quantized tensors not yet implemented") + if a.is_sparse or b.is_sparse: + raise AssertionError("sparse tensors not yet implemented") + if a.is_conj() or b.is_conj(): + raise AssertionError("conjugate tensors not yet implemented") + + """ + Step 1: create unbacked symints for sizes that are different + along the same axis. For example: + a.size is [s0, 4, s0, 5, 4, 5] + b.size is [s1, 4, s2, 8, 4, 7] + merged_size will be [u0, 4, u1, u2, 4, u3], where + u0 has range [min(s0, s1), max(s0, s1)] + u1 has range [min(s0, s2), max(s0, s2)] + u2 has range [5, 8] + u3 has range [5, 7] + """ + merged_size: list[int | torch.SymInt] = [] + + def _has_unbacked_symbols(s: int | torch.SymInt) -> bool: + if isinstance(s, int): + return False + else: + return has_free_unbacked_symbols(s.node.expr) + + for s0, s1 in zip(a.size(), b.size()): + # If there are unbacked symbols leaked out of true_branch or false_branch + # we need to merge them with a new unbacked symbol and track in parent graph. + if ( + not _has_unbacked_symbols(s0) + and not _has_unbacked_symbols(s1) + and SymIntEqByExpr(s0) == SymIntEqByExpr(s1) + ): + merged_size.append(s0) + else: + if mode.shape_env is None: + raise AssertionError("mode.shape_env is None") + new_size = mode.shape_env.create_unbacked_symint() + mode.shape_env.constrain_symbol_range(new_size.node.expr, *min_max(s0, s1)) + merged_size.append(new_size) + + """ + This follows the logic in symbolic_shapes._compute_symbolic_stride + Step 2: Since tensor stride is an accumulative multiplication of the sizes, which is a permutated + (due to view ops) non-descending sequence. + + Case 1: No size is 1. In this case, strides have unique values. + For example, suppose we have a tensor with: + size [3, 4, 3, 5, 4, 5], + stride (1200, 300, 1, 12, 3, 60), + merged_size [u0, u1, u2, u3, u4, u5]. + + We visit the strides in ascending order: 1, 3, 12, 60, 300, 1200. In each step, we check whether + the current stride is bounded or not and bound next stride by setting. + stride_expr[next_stride] = current_stride_expr * current_size_expr + 1st round: + current_stride is 1, current_size is 3, so next_stride is 1 * 3 = 3, + current_stride_expr is set to 1, current_size_expr is u2, so stride_expr[3] is therefore 1 * u2 = u2 + 2nd round: + current_stride is 3, current_size is 4, so next_stride is 3 * 4 = 12, + current_stride_expr is stride_expr[3] i.e. u2, current_size_expr is u4, so stride_expr[12] = u2 * u4 + ... + + Case 2: At least one dimension has size 1, which can produce duplicates in strides. + In this case, theoretically, we cannot uniquely determine the expr of strides because + the accessing stride_expr with same key in different order causes the final stride expression + to be different. + + Suppose we have: + size: (3, 1) + stride: (1, 1) + merged_size: (u0, u1) + + The stride expr could either be (u1, 1) or (1, u0) depending on whether we start with u1 or u0. + For this reason, we try to break tie by sorting via descending index so we always get (u1, 1). + + Note that backend might optimize the strides anyway so this is usually not a problem as long + as two branches matches. See relevant discussions in https://github.com/pytorch/pytorch/issues/142024. + + Case 3: Dim has 0 stride. 0 stride doesn't participate in the accumulative multiplication of + sizes. So they're always treated as constant even if their corresponding size is turned into unbacked symint. + + Suppose we have: + size: (3, 3) + stride: (0, 1) + merged_size: (u0, u1) + + The merged stride would be (0, 1) + """ + + def _bound_stride( + a_ex_size: torch.Size, + b_ex_size: torch.Size, + a_ex_stride: tuple[int, ...], + b_ex_stride: tuple[int, ...], + merged_size: list[int | torch.SymInt], + ) -> list[int | torch.SymInt]: + from torch._inductor.ir import get_stride_order + + a_sorted_stride_idx = get_stride_order(a_ex_stride, mode.shape_env) + b_sorted_stride_idx = get_stride_order(b_ex_stride, mode.shape_env) + + a_stride_li: list[tuple[int | torch.SymInt, int] | None] = [None] * len( + a_ex_stride + ) + b_stride_li: list[tuple[int | torch.SymInt, int] | None] = [None] * len( + b_ex_stride + ) + for i, idx in enumerate(a_sorted_stride_idx): + a_stride_li[idx] = (a_ex_stride[i], -i) + for i, idx in enumerate(b_sorted_stride_idx): + b_stride_li[idx] = (b_ex_stride[i], -i) + + for a_pair, b_pair in zip(a_stride_li, b_stride_li): + if a_pair is None or b_pair is None: + raise AssertionError( + f"expected a_pair and b_pair to be non-None, got a_pair={a_pair}, b_pair={b_pair}" + ) + _, a_idx = a_pair + _, b_idx = b_pair + + if a_idx != b_idx: + raise RuntimeError( + f"The sorted order of strides of the two branches' output doesn't match." + f"this indicates the contiguousness of the two branches are different. " + f"True branch has stride {a_ex_stride} but false branch has stride {b_ex_stride}." + f"Consider using contiguous() to make the two branches have the same contiguousness." + ) + + def _maybe_expr(s: int | torch.SymInt): + if isinstance(s, int): + return s + return s.node.expr + + a_stride_expr: dict[Any, int | torch.SymInt] = {} + b_stride_expr: dict[Any, int | torch.SymInt] = {} + merged_strides: list[int | torch.SymInt] = [None] * len(a_ex_stride) # type: ignore[list-item] + for a_pair, b_pair in zip(a_stride_li, b_stride_li): + if a_pair is None or b_pair is None: + raise AssertionError( + f"expected a_pair and b_pair to be non-None, got a_pair={a_pair}, b_pair={b_pair}" + ) + a_val, neg_i = a_pair + b_val, _ = b_pair + + i = -neg_i + if a_val == 0: + if b_val != 0: + raise AssertionError( + f"expected b_val == 0 when a_val == 0, got a_val={a_val}, b_val={b_val}" + ) + merged_strides[i] = 0 + continue + + if _maybe_expr(a_val) in a_stride_expr: + a_expr = a_stride_expr[_maybe_expr(a_val)] + if b_stride_expr[_maybe_expr(b_val)] != a_expr: + raise AssertionError( + f"a_stride_expr:{a_stride_expr}, b_stride_expr:{b_stride_expr}" + ) + merged_strides[i] = a_expr + else: + if a_val == 1: + if b_val != 1: + raise AssertionError( + f"expected b_val == 1 when a_val == 1, got b_val={b_val}" + ) + a_stride_expr[_maybe_expr(a_val)] = 1 + b_stride_expr[_maybe_expr(b_val)] = 1 + merged_strides[i] = 1 + else: + # If we cannot find the expr of a_val in a_stride_expr, it means + # the strides is not a simple accumulative multiplication of sizes. + # In this case, we cannot determine the expr of strides from the new + # shapes so we error out and hint users to call contiguous(). + raise RuntimeError( + f"It seems one of cond's output stride is not a simple accumulative multiplication of sizes. " + f"This could be because cond returns a slice of a tensor, which is not dense in memory. " + f"True branch has size {a_ex_size}, stride {a_ex_stride} and false branch has size {b_ex_size} " + f"stride {b_ex_stride}. Hint: can call t.contiguous(). " + ) + nxt_merged_stride_expr = merged_strides[i] * merged_size[i] + a_stride_expr[_maybe_expr(a_val * a_ex_size[i])] = nxt_merged_stride_expr + b_stride_expr[_maybe_expr(b_val * b_ex_size[i])] = nxt_merged_stride_expr + return merged_strides + + merged_stride: list[int | torch.SymInt] = _bound_stride( + a.size(), b.size(), a.stride(), b.stride(), merged_size + ) + + with mode: + return torch.empty_strided( + merged_size, merged_stride, dtype=a.dtype, device=a.device + ) + + +@cond_op.py_functionalize_impl +def cond_func(ctx, pred, true_fn, false_fn, inputs): + from torch._higher_order_ops.auto_functionalize import ( + can_auto_functionalize, + do_auto_functionalize_v2, + ) + from torch._higher_order_ops.utils import _check_alias_and_mutation, HopInstance + + hop_instance = HopInstance.create(cond_op, pred, true_fn, false_fn, inputs) + # For now, we only support auto-functionalization for cond when using python + # functionalization mode + if can_auto_functionalize(hop_instance) and hasattr(ctx, "mode"): + return do_auto_functionalize_v2( + ctx.mode, + hop_instance, + tuple(pytree.tree_flatten((pred, true_fn, false_fn, inputs))[0]), + {}, + ) + + unwrapped_inputs = ctx.unwrap_tensors(inputs) + unwrapped_pred = ctx.unwrap_tensors(pred) + with ctx.redispatch_to_next(): + functional_true = ctx.functionalize(_maybe_run_with_interpreter(true_fn)) + functional_false = ctx.functionalize(_maybe_run_with_interpreter(false_fn)) + pre_dispatch = hasattr(ctx, "mode") and ctx.mode.pre_dispatch + for branch, branch_name in [(true_fn, "cond_true"), (false_fn, "cond_false")]: + _check_alias_and_mutation( + branch, unwrapped_inputs, branch_name, pre_dispatch + ) + + cond_return = cond_op( + unwrapped_pred, functional_true, functional_false, unwrapped_inputs + ) + return ctx.wrap_tensors(cond_return) + + +@cond_op.py_impl(torch._C._functorch.TransformType.Vmap) +def cond_batch_rule(interpreter, pred, true_fn, false_fn, inputs): + if not isinstance(inputs, (list, tuple)): + raise AssertionError( + f"Cond inputs must be a list or tuple of tensors, got {type(inputs)}" + ) + if not all(isinstance(i, torch.Tensor) for i in inputs): + raise AssertionError( + f"Cond inputs must be a list of tensors, got {[type(i) for i in inputs]}" + ) + + pred_is_batched = isinstance(pred, torch.Tensor) and is_batchedtensor(pred) + pred_ = get_unwrapped(pred) if pred_is_batched else pred + + # unbatched tensors are not vmapped + tensors, in_dims = zip( + *[ + (get_unwrapped(t), maybe_get_bdim(t)) if is_batchedtensor(t) else (t, None) + for t in inputs + ] + ) + + if pred_is_batched: + # prepend "pred" and vmap everything + tensors = (pred_,) + tensors + in_dims = (0,) + in_dims + + def fn(p, *args): + t = true_fn(*args) + f = false_fn(*args) + return torch.where(p, t[0], f[0]) + + with interpreter.lower(): + result = torch.vmap(fn, in_dims=in_dims)(*tensors) + + else: + # predicate is known at this stage and it is a boolean expression or a + # tensor with one element. + true_fn = torch.vmap(true_fn, in_dims=in_dims) + false_fn = torch.vmap(false_fn, in_dims=in_dims) + + with interpreter.lower(): + result = cond_op(pred, true_fn, false_fn, tensors) + + if not isinstance(result, tuple): + result = (result,) + lvl = interpreter.level() + return tuple(_add_batch_dim(r, 0, lvl) for r in result) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/cudagraph_conditional_nodes.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/cudagraph_conditional_nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..393119c740162aff86f8ee89c632a8ea7527f1d9 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/cudagraph_conditional_nodes.py @@ -0,0 +1,130 @@ +# mypy: allow-untyped-defs +from collections.abc import Generator +from contextlib import contextmanager + +import torch +import torch.utils._pytree as pytree +from torch.utils._python_dispatch import TorchDispatchMode + + +class CUDAGraphCaptureControlFlowOpDispatchMode(TorchDispatchMode): + @classmethod + def ignore_compile_internals(cls) -> bool: + return True + + def __init__( + self, + ) -> None: + self.supports_higher_order_operators = True + super().__init__() + + def __torch_dispatch__( + self, + func, + types, + args=(), + kwargs=None, + ): + if func is torch.ops.higher_order.cond: + # Re-enter the mode to support nested conditionals + with self: + return if_else_node(*args) + kwargs = {} if kwargs is None else kwargs + return func(*args, **kwargs) + + +class ControlFlowOpWarmupDispatchMode(TorchDispatchMode): + """The purpose of this TodchDispatchMode is to "warm up" both sides of a torch.cond() statement. + + For data-dependent control flow code, only one side will be + executed. Therefore, it is not safe to stream capture a + torch.cond() statement naively, since we don't have a guarantee + that all ops will have been "warmed up". The clever workaround is + to use a "relaxed" stream capture whose final cuda graph we throw + away. This works because stream capture does not actually execute + any GPU code, and because true_fn and false_fn are both fxgraphs, + which do not have any CPU side effects. + """ + + @classmethod + def ignore_compile_internals(cls) -> bool: + return True + + def __init__( + self, + ) -> None: + super().__init__() + self.supports_higher_order_operators = True + self.capture_stream = torch.cuda.Stream() + + def __torch_dispatch__( + self, + func, + types, + args=(), + kwargs=None, + ): + kwargs = {} if kwargs is None else kwargs + + # Warm up both sides of this torch.cond() + if func is torch.ops.higher_order.cond: + if torch.cuda.is_current_stream_capturing(): + # This is a call to torch.cond() nested within another + # torch.cond() function. + with self: + # We re-enter the mode in case of nested calls to torch.cond() + return if_else_node(*args) + else: + with ( + torch.cuda.graph( + torch.cuda.CUDAGraph(), + pool=None, + stream=self.capture_stream, + capture_error_mode="relaxed", + ), + self, + ): + if_else_node(*args) + + return func(*args, **kwargs) + else: + return func(*args, **kwargs) + + +@contextmanager +def _if_body(pred: torch.Tensor) -> Generator[None, None, None]: + current_cuda_graph = torch.cuda.CUDAGraph.get_currently_capturing_graph() + current_cuda_graph.begin_capture_to_if_node(pred) + try: + yield + finally: + current_cuda_graph.end_capture_to_conditional_node() + + +def if_else_node(pred: torch.Tensor, true_fn, false_fn, operands): + if not pred.is_cuda: + raise ValueError( + "Conditions must be on a cuda device to use conditional node in cuda graphs" + ) + # if-else is not supported until CUDA 12.8. Therefore, we use two + # if conditions, where one evaluates !pred + outs = [] + + for lazy_pred, fn in [ + (lambda: pred, true_fn), + (lambda: torch.logical_not(pred), false_fn), + ]: + with _if_body(lazy_pred()): + outs.append(fn(*operands)) + + # The output of the else branch gets copied into the + # output of the if branch. This is done because the rest + # of the cudagraph after the conditional node has fixed + # inputs, so we need to merge the two outputs into a + # single output. + if len(outs) == 2: + for if_out, else_out in zip( + pytree.tree_iter(outs[0]), pytree.tree_iter(outs[1]) + ): + if_out.copy_(else_out) + return outs[0] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/effects.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/effects.py new file mode 100644 index 0000000000000000000000000000000000000000..7f2d203df50c7561d9886be2880d2b57b8b484c7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/effects.py @@ -0,0 +1,340 @@ +# mypy: allow-untyped-defs +from typing import Any, Union + +import torch +import torch.utils._pytree as pytree +from torch._C import DispatchKey +from torch._higher_order_ops.invoke_leaf_function import invoke_leaf_function +from torch._higher_order_ops.print import print as hop_print +from torch._higher_order_ops.schema import HopSchema +from torch._higher_order_ops.torchbind import call_torchbind +from torch._library.custom_ops import CustomOpDef +from torch._library.effects import EffectType +from torch._library.utils import RegistrationHandle +from torch._ops import HigherOrderOperator +from torch._subclasses.fake_tensor import FakeTensorMode +from torch.fx.experimental.proxy_tensor import ( + disable_proxy_modes_tracing, + ProxyTorchDispatchMode, + track_tensor_tree, +) + + +_op_identifier = Union[ + str, + "torch._ops.OpOverload", + "torch._library.custom_ops.CustomOpDef", + "torch._ops.HigherOrderOperator", +] +OpType = Union["torch._ops.HigherOrderOperator", "torch._ops.OpOverload"] + +_EffectType = EffectType + + +def _get_op_qualname(op: _op_identifier) -> str: + """Convert an op identifier to a qualified string key.""" + if isinstance(op, torch._ops.OpOverload): + return op._name + elif isinstance(op, torch._ops.HigherOrderOperator): + return f"{op.namespace}::{op.name()}" + elif isinstance(op, CustomOpDef): + return op._qualname + elif isinstance(op, str): + return op + + raise ValueError(f"Invalid operator input {op}") + + +def _register_effectful_op( + op: _op_identifier, effect: EffectType | None +) -> RegistrationHandle: + qualname = _get_op_qualname(op) + entry = torch._library.simple_registry.singleton.find(qualname) + handle = entry.effect.register(effect) + return handle + + +def _get_effect(op: _op_identifier) -> _EffectType | None: + qualname = _get_op_qualname(op) + entry = torch._library.simple_registry.singleton.find(qualname) + return entry.effect.effect + + +_register_effectful_op("aten::_print", _EffectType.ORDERED) +_register_effectful_op("profiler::_record_function_exit._RecordFunction", None) +_register_effectful_op(call_torchbind, _EffectType.ORDERED) +_register_effectful_op(hop_print, _EffectType.ORDERED) +_register_effectful_op(invoke_leaf_function, _EffectType.ORDERED) + + +class WithEffects(HigherOrderOperator): + """ + with_effects(token, op, args, kwargs) -> (new_token, op_results) + + This HOP helps ensure ordering between side effectful ops like prints or ops + using torchbind objects. This is needed to ensure a traced graph from + AOTAutograd is functional so that future optimization passes do not reorder + these operators. This is done through threading "effect tokens" through the + graph to enforce data dependence between side effectful ops. + + The tokens are basically dummy values (torch.tensor([])). We create a token + per "effect type", which are enumerated in the _EffectType enum. + """ + + def __init__(self) -> None: + super().__init__("with_effects") + + def __call__( + self, + token, + op: OpType, + *args: tuple[Any, ...], + **kwargs: dict[str, Any], + ) -> tuple[Any, ...]: + if not isinstance(op, (torch._ops.HigherOrderOperator, torch._ops.OpOverload)): + raise AssertionError( + f"op must be HigherOrderOperator or OpOverload, got {type(op)}" + ) + if has_aliasing(op): + raise AssertionError("Ops with aliasing is not supported") + if not isinstance(kwargs, dict): + raise AssertionError(f"kwargs must be a dict, got {type(kwargs)}") + # pyrefly: ignore [missing-attribute] + return super().__call__(token, op, *args, **kwargs) + + +with_effects = WithEffects() + + +def has_aliasing(op: OpType): + # NOT FOR PUBLIC USE + if isinstance(op, torch._ops.HigherOrderOperator): + return False + + for arg in op._schema.arguments: + if arg.alias_info is not None: + return True + for arg in op._schema.returns: + if arg.alias_info is not None: + return True + return False + + +def has_effects(op) -> bool: + return ( + isinstance(op, (torch._ops.HigherOrderOperator, torch._ops.OpOverload)) + and not has_aliasing(op) + and _get_effect(op) is not None + ) + + +def new_token_tensor() -> torch.Tensor: + return torch.tensor([]) + + +@with_effects.py_impl(DispatchKey.CompositeExplicitAutograd) +def with_effects_dense( + token: torch.Tensor, + op: torch._ops.OpOverload, + *args: tuple[Any, ...], + **kwargs: dict[str, Any], +) -> tuple[torch.Tensor, ...]: + out = op(*args, **kwargs) + new_token = new_token_tensor() + # [NOTE: with_effects return type] + # Note that we should only do *out for tuple type, but not list type. + # This is to match the schema of the op. + # For tuple output, the length of schema output is the same as the length of out. + # For list output, the length of schema output is 1 (e.g. Tensor[]) regardless of the + # length of the list. + if isinstance(out, tuple): + return (new_token, *out) + return (new_token, out) + + +@with_effects.py_impl(FakeTensorMode) +def with_effects_fake( + mode, + token: torch.Tensor, + op: torch._ops.OpOverload, + *args: tuple[Any, ...], + **kwargs: dict[str, Any], +) -> tuple[torch.Tensor, ...]: + with mode: + result = with_effects_dense(token, op, *args, **kwargs) + return result + + +@with_effects.py_impl(ProxyTorchDispatchMode) +def with_effects_proxy( + mode, + token: torch.Tensor, + op: torch._ops.OpOverload, + *args: tuple[Any, ...], + **kwargs: dict[str, Any], +) -> tuple[torch.Tensor, ...]: + with disable_proxy_modes_tracing(): + out = with_effects(token, op, *args, **kwargs) + + proxy_token = mode.tracer.unwrap_proxy(token) + proxy_args = pytree.tree_map(mode.tracer.unwrap_proxy, args) + proxy_kwargs = pytree.tree_map(mode.tracer.unwrap_proxy, kwargs) + + from torch.fx.node import has_side_effect + + # To avoid the being DCEed by graph.eliminate_dead_code if they. + # don't have output or their outputs are not used. + has_side_effect(op) + + out_proxy = mode.tracer.create_proxy( + "call_function", + with_effects, + (proxy_token, op, *proxy_args), + proxy_kwargs, + ) + result = track_tensor_tree(out, out_proxy, constant=None, tracer=mode.tracer) + return result + + +with_effects.fallthrough(DispatchKey.AutogradCPU) +with_effects.fallthrough(DispatchKey.AutogradCUDA) + + +@with_effects.py_functionalize_impl +def with_effects_functional( + ctx, + token: torch.Tensor, + op: torch._ops.OpOverload, + *args: tuple[Any, ...], + **kwargs: dict[str, Any], +) -> tuple[torch.Tensor, ...]: + # with_effects is already functional, so just re-emit it. + unwrapped_token, unwrapped_args, unwrapped_kwargs = ctx.unwrap_tensors( + [token, args, kwargs] + ) + with ctx.redispatch_to_next(): + result = with_effects(unwrapped_token, op, *unwrapped_args, **unwrapped_kwargs) + return ctx.wrap_tensors(result) + + +_EFFECTFUL_HOPS_WITH_SCHEMA = {hop_print, invoke_leaf_function} + + +def _get_schema(op, args, kwargs: dict | None = None) -> torch.FunctionSchema: + if isinstance(op, torch._ops.OpOverload): + return op._schema + elif op == call_torchbind: + return getattr(args[0], args[1]).schema + elif op in _EFFECTFUL_HOPS_WITH_SCHEMA: + extra_kwargs = kwargs or {} + return op.gen_schema(*args, **extra_kwargs) + else: + raise RuntimeError(f"Unable to get schema for op {op}") + + +def handle_effects( + allow_token_discovery: bool, + tokens: dict[_EffectType, torch.Tensor], + op: OpType, + args: tuple[Any, ...], + kwargs: dict[str, Any], +) -> Any: + """ + Args: + allow_token_discovery: Whether or not we are discovering tokens. If this + is true, we will create a token for every side effect type seen that + does not have a token assigned yet. If this is false, the tokens + should've all been created ahead of time, so we will error if there is + no token mapping to every effect type. + + tokens: Map of effect type to tokens. This is to chain operators of the + same effects together so that they do not get reordered in later + optimization passes. + """ + + # Get a token. We can't do `tokens.get(op, torch.tensor([]))` because + # this will create an empty tensor during proxy mode tracing if the token + # doesn't exist. But the tokens should always exist during proxy mode tracing. + key = _get_effect(op) + if key is None: + raise AssertionError(f"effect key must not be None for op {op}") + if key not in tokens: + if not allow_token_discovery: + raise AssertionError( + f"Could not find a token for effect {key} which came from the function {op}" + ) + proxy_tensor_mode = torch._C._get_dispatch_mode( + torch._C._TorchDispatchModeKey.PROXY + ) + if proxy_tensor_mode is not None: + # If we discovered a new token during tracing, we are in backward. + # Then we patch the graph, adding additional tangents_token as input to the joint graph. + tracer = proxy_tensor_mode.tracer + + from torch.fx.experimental.proxy_tensor import ( + disable_proxy_modes_tracing, + track_tensor_tree, + ) + + with disable_proxy_modes_tracing(): + token_tensor = new_token_tensor() + + token_proxy = proxy_tensor_mode.tracer.create_proxy( + "placeholder", "tangents_token", (), {}, name="tangents_token" + ) + track_tensor_tree(token_tensor, token_proxy, constant=None, tracer=tracer) + + tokens[key] = token_tensor + else: + tokens[key] = new_token_tensor() + + token = tokens[key] + + from torch._subclasses.functional_tensor import PythonFunctionalizeAPI + + ctx = PythonFunctionalizeAPI() + + unwrapped_token = ctx.unwrap_tensors([token])[0] + unwrapped_args = ctx.unwrap_tensors(args) + unwrapped_kwargs = ctx.unwrap_tensors(kwargs) # type: ignore[arg-type] + with ctx.redispatch_to_next(): + (new_token, *unwrapped_outs) = with_effects( + unwrapped_token, op, *unwrapped_args, **unwrapped_kwargs + ) + + schema = _get_schema(op, unwrapped_args, unwrapped_kwargs) + + if isinstance(schema, HopSchema): + if len(schema.returns) == 0: + unwrapped_outs = () + else: + if len(unwrapped_outs) != len(schema.returns): + raise AssertionError( + f"expected {len(schema.returns)} outputs but got {len(unwrapped_outs)}" + ) + unwrapped_outs = tuple(unwrapped_outs) + elif len(schema.returns) == 0: + if unwrapped_outs[0] is not None: + raise AssertionError(f"expected no outputs but got {unwrapped_outs[0]}") + unwrapped_outs = None # type: ignore[assignment] + elif len(schema.returns) == 1: + if len(unwrapped_outs) != 1: + raise AssertionError(f"expected 1 output but got {len(unwrapped_outs)}") + unwrapped_outs = unwrapped_outs[0] + else: + if len(unwrapped_outs) != len(schema.returns): + raise AssertionError( + f"expected {len(schema.returns)} outputs but got {len(unwrapped_outs)}" + ) + + # Add the newly created token into the tokens map for a following call to + # use this token. + wrapped_token = ctx.wrap_tensors(new_token) + if not isinstance(wrapped_token, torch.Tensor): + raise AssertionError( + f"expected wrapped_token to be torch.Tensor, got {type(wrapped_token)}" + ) + tokens[key] = wrapped_token + + # pyrefly: ignore [bad-argument-type] + return ctx.wrap_tensors(unwrapped_outs) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/executorch_call_delegate.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/executorch_call_delegate.py new file mode 100644 index 0000000000000000000000000000000000000000..1e639b99c6a58c16916ece0504bbdcafcc5f50ed --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/executorch_call_delegate.py @@ -0,0 +1,182 @@ +# mypy: allow-untyped-defs + +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +# pyre-strict + +from __future__ import annotations + +from typing import Any, cast + +import torch +import torch.utils._pytree as pytree +from torch._ops import HigherOrderOperator +from torch._subclasses.fake_tensor import FakeTensorMode +from torch.fx.experimental.proxy_tensor import ( + disable_proxy_modes_tracing, + get_proxy_slot, + ProxyTorchDispatchMode, + track_tensor_tree, +) +from torch.utils._pytree import tree_flatten + + +class ExecutorchCallDelegate(HigherOrderOperator): + def __init__(self): + super().__init__("executorch_call_delegate") + + def __call__(self, lowered_module, *args): + # pyrefly: ignore [missing-attribute] + return super().__call__(lowered_module, *args) + + +executorch_call_delegate = ExecutorchCallDelegate() +executorch_call_delegate.fallthrough(torch._C.DispatchKey.PythonDispatcher) +executorch_call_delegate.fallthrough(torch._C.DispatchKey.PythonTLSSnapshot) +executorch_call_delegate.fallthrough(torch._C.DispatchKey.ADInplaceOrView) +executorch_call_delegate.fallthrough(torch._C.DispatchKey.AutocastCPU) + +LOWERED_BACKEND_MODULE_TYPE = "LoweredBackendModule" + + +# pyre-ignore +def trace_call_delegate(proxy_mode, func_overload, lowered_module, *args): + # pyre-ignore + def _unwrap_proxy(e): + if not isinstance(e, (torch.Tensor, torch.SymInt, torch.SymFloat)): + return e + return get_proxy_slot( + cast(torch.Tensor, e), + proxy_mode.tracer, + e, + lambda e: e.proxy, # type: ignore[attr-defined] + ) + + if not is_lowered_module(lowered_module): + raise ValueError( + "executorch_call_delegate()'s first argument must be a LoweredBackendModule" + ) + + with disable_proxy_modes_tracing(): + out = call_delegate_cpu(lowered_module, *args) + + get_lowered_module_name(proxy_mode.tracer.root, lowered_module) + + node_args = (lowered_module, *args) + proxy_args = pytree.tree_map(_unwrap_proxy, node_args) + out_proxy = proxy_mode.tracer.create_proxy( + "call_function", func_overload, proxy_args, {}, name="executorch_call_delegate" + ) + return track_tensor_tree(out, out_proxy, constant=None, tracer=proxy_mode.tracer) + + +@executorch_call_delegate.py_impl(torch._C.DispatchKey.CompositeExplicitAutograd) +# pyre-ignore +def call_delegate_cpu(lowered_module, *args): + # FX creates this immutable_dict/list concept. Get rid of this. + map_types: dict[type, type] = { + torch.fx.immutable_collections.immutable_dict: dict, + torch.fx.immutable_collections.immutable_list: list, + } + new_args = pytree.tree_map_only( + tuple(map_types.keys()), + lambda a: map_types[type(a)](a), + args, + lambda a: isinstance(a, tuple(map_types.keys())), + ) + return lowered_module.original_module.module()(*new_args) + + +@executorch_call_delegate.py_autograd_impl +# pyre-ignore +def call_delegate_autograd(lowered_module, *args): + # TODO: support autograd + flat_operands, _ = tree_flatten([lowered_module, *args]) + requires_grad = any( + f.requires_grad for f in flat_operands if isinstance(f, torch.Tensor) + ) + + with torch._C._ExcludeDispatchKeyGuard( + torch._C.DispatchKeySet(torch._C.DispatchKey.AutogradCPU) + ): + res = executorch_call_delegate(lowered_module, *args) + + if requires_grad: + # Create aliases of the output that has requires_grad=True. We need + # at least one of the inputs to err_fn to require grad so that the + # output will have a grad_fn. + + # pyre-ignore + def fake_requires_grad(var): + if var is not None: + var = var.detach() + if torch.is_floating_point(var) or torch.is_complex(var): + var.requires_grad = True + return var + + return pytree.tree_map_only(torch.Tensor, fake_requires_grad, res) + + return res + + +@executorch_call_delegate.py_impl(ProxyTorchDispatchMode) +# pyre-ignore +def call_delegate_proxy_torch_dispatch_mode(mode, lowered_module, *args): + res = trace_call_delegate(mode, executorch_call_delegate, lowered_module, *args) + return res + + +@executorch_call_delegate.py_impl(FakeTensorMode) +# pyre-ignore +def call_delegate_fake_tensor_mode(mode, lowered_module, *args): + with mode: + return call_delegate_cpu(lowered_module, *args) + + +@executorch_call_delegate.py_functionalize_impl +# pyre-ignore +def call_delegate_functionalize(ctx, lowered_module, *args): + unwrapped_args = tuple(ctx.unwrap_tensors(arg) for arg in args) + with ctx.redispatch_to_next(): + res = executorch_call_delegate(lowered_module, *unwrapped_args) + return ctx.wrap_tensors(res) + + +# pyre-ignore: Missing parameter annotation [2]: Parameter `obj` must have a type other than `Any`.Pyre +def is_lowered_module(obj: Any) -> bool: + """ + This function is added to avoid using isinstance(obj, + LoweredBackendModule) as it will import LoweredBackendModule, which may + cause a circular import. + """ + return type(obj).__name__ == LOWERED_BACKEND_MODULE_TYPE + + +def get_lowered_module_name( + root: torch.nn.Module, + # pyre-ignore: Undefined or invalid type [11]: Annotation `LoweredBackendModule` is not defined as a type. + lowered_module: LOWERED_BACKEND_MODULE_TYPE, # type: ignore[valid-type] +) -> str: + """ + Adds the given lowered_module into the given root module and returns the + name of the module added. + """ + # Find a qualifying name for the lowered submodule + qualname = None + i = 0 + while True: + qualname = f"lowered_module_{i}" + if not hasattr(root, qualname): + break + i += 1 + if qualname is None: + raise AssertionError( + "qualname must not be None after finding unused module slot" + ) + + root.add_module(qualname, lowered_module) + return qualname diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/flat_apply.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/flat_apply.py new file mode 100644 index 0000000000000000000000000000000000000000..5f93ec46a19da92a637490983defa11221474285 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/flat_apply.py @@ -0,0 +1,183 @@ +import typing +from collections.abc import Callable, Sequence +from dataclasses import dataclass +from typing import Generic, overload, TypeAlias, TypeVar +from typing_extensions import ParamSpec, TypeIs, TypeVarTuple, Unpack + +import torch +import torch.fx.node +import torch.utils._pytree as pytree +from torch._library.fake_class_registry import FakeScriptObject +from torch._library.opaque_object import is_opaque_type +from torch._ops import HigherOrderOperator + + +_R = TypeVar("_R") +_P = ParamSpec("_P") +_Ts = TypeVarTuple("_Ts") + + +def is_graphable(val: object) -> TypeIs[torch.fx.node.BaseArgumentTypes]: + """Definition: a graphable type is a type that is an acceptable input/output type to a FX node.""" + return isinstance( + val, (*torch.fx.node.base_types, FakeScriptObject) + ) or is_opaque_type(type(val)) + + +def is_graphable_type(typ: type[object]) -> bool: + """Return whether the given type is graphable.""" + return ( + issubclass(typ, torch.fx.node.base_types) + or is_opaque_type(typ) + or issubclass(typ, FakeScriptObject) + ) + + +def to_graphable(stuff: pytree.PyTree) -> tuple[list[object], pytree.TreeSpec]: + """Flattens stuff into a flat list of graphable types.""" + # We can consider preserving things like List[int] to improve + # perf and readability (right now that is all flattened out) + flat_args, spec = pytree.tree_flatten(stuff) + for arg in flat_args: + if not is_graphable(arg): + raise RuntimeError( + f"Expected all pytree.tree_leaves of (args, kwargs) to be graphable types, but found " + f"non-fx-graphable type {type(arg)}. If this type is meant to be constant, mark it as " + f"via pytree.register_constant; otherwise, register it as a pytree." + ) + return flat_args, spec + + +def from_graphable( + flat_args: tuple[Unpack[_Ts]], spec: pytree.TreeSpec +) -> pytree.PyTree: + """The inverse of to_graphable.""" + stuff = pytree.tree_unflatten(flat_args, spec) + return stuff + + +def func_to_graphable( + func: Callable[..., object], +) -> tuple[list[object], pytree.TreeSpec]: + """ + Pack and flatten a function type into graphable types. + This is useful for legalizing the function argument of `flat_apply`. + """ + return pytree.tree_flatten(_ConstantFunction(func)) + + +@dataclass(frozen=True, slots=True) +class _ConstantFunction(Generic[_P, _R]): + func: Callable[_P, _R] + + def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R: + return self.func(*args, **kwargs) + + +pytree.register_constant(_ConstantFunction) + + +_OpTypes = ( + torch._ops.OpOverload | torch._ops.OpOverloadPacket | torch._ops.HigherOrderOperator +) +_op_types = typing.get_args(_OpTypes) + + +_Base: TypeAlias = torch.fx.node.BaseArgumentTypes +# pyrefly bug: pyrefly is complaining: Expected a type form, got instance of `Literal['_FXOutput'] +# pyrefly: ignore[not-a-type] +_FXOutput = _Base | Sequence["_FXOutput"] + + +class FlatApply(HigherOrderOperator): + def __init__(self) -> None: + super().__init__("flat_apply") + + def __call__( + self, + func: _OpTypes | pytree.TreeSpec, + in_spec: pytree.TreeSpec, + *flat_args: Unpack[_Ts], + # If True then the output is checked to be valid. If False then it is up + # to the caller to ensure the output is appropriate. + checked_output: bool = True, + **_unused: object, + ) -> object: + """ + Functions that take in non-graphable types cannot directly be put into FX graph. + + Given func(*args, **kwargs), if all of the non-graphable types are pytrees, + then we're able to store a call to flat_apply(func, in_spec, *flat_args) in the FX graph. + + The semantics of flat_apply(func, in_spec, *flat_args) are roughly equivalent to: + + >>> def flat_apply_impl(func, in_spec, *flat_args): + >>> args, kwargs = pytree.tree_unflatten(flat_args, in_spec) + >>> output = func(*args, **kwargs) + >>> return output + + flat_apply supports the following two cases: + - an input type is a container type (e.g. of tensors) registered as a pytree. + We'll tree_flatten the input type and store the spec. + - an input type is a constant type (i.e. torch.compile will specialize on it) + registered with pytree.register_constant. The constant type goes directly + into the spec. + """ + if not ( + isinstance(func, _op_types) + or is_opaque_type(type(func)) + or pytree._is_constant_holder(func) + ): + raise AssertionError( + f"func must be an op type, constant holder, or opaque callable, got {type(func)}" + ) + if len(_unused) != 0: + raise AssertionError(f"unexpected keyword arguments: {_unused}") + # pyrefly: ignore[bad-argument-type] # pyrefly bug? + return impl(func, in_spec, flat_args, checked_output) + + +@overload +def is_valid_output(x: tuple[object, ...]) -> TypeIs[tuple[_FXOutput, ...]]: ... + + +@overload +def is_valid_output(x: Sequence[object]) -> TypeIs[Sequence[_FXOutput]]: ... + + +def is_valid_output(x: object) -> bool: + if isinstance(x, (tuple, list)): + return all(map(is_valid_output, x)) + return is_graphable(x) + + +def impl( + func: _OpTypes | pytree.TreeSpec, + in_spec: pytree.TreeSpec, + flat_args: tuple[Unpack[_Ts]], + checked_output: bool, +) -> _FXOutput: + if isinstance(func, pytree.TreeSpec): + # assume _ConstantFunction + func = pytree._retrieve_constant(func) + if not isinstance(func, _ConstantFunction): + raise AssertionError( + f"expected retrieved constant to be _ConstantFunction, got {type(func)}" + ) + + from torch._higher_order_ops.invoke_leaf_function import unflatten_args_with_modules + + with unflatten_args_with_modules(flat_args, in_spec) as (args, kwargs): + out = func(*args, **kwargs) + + if checked_output: + # For "normal" usage all outputs must either be graphable or + # lists/tuples of graphables. + if not is_valid_output(out): + raise AssertionError( + f"output must be graphable or nested list/tuple of graphables, got {type(out)}" + ) + return out + + +flat_apply = FlatApply() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/flex_attention.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/flex_attention.py new file mode 100644 index 0000000000000000000000000000000000000000..9949aba8b69b218e5d69582802c8e6443dd23653 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/flex_attention.py @@ -0,0 +1,1464 @@ +import math +from collections.abc import Callable, Sequence +from typing import Any + +import torch +import torch.utils._pytree as pytree +from torch import Tensor +from torch._C import DispatchKey +from torch._higher_order_ops.utils import ( + _has_potential_branch_input_mutation, + _maybe_reenter_make_fx, + autograd_not_implemented, + has_user_subclass, + redirect_to_mode, + reenter_make_fx, + register_fake, + save_values_for_backward, + saved_values, + UnsupportedAliasMutationException, + validate_subgraph_args_types, +) +from torch._ops import HigherOrderOperator +from torch._subclasses import FakeTensor +from torch.amp.autocast_mode import _cast as _autocast_cast +from torch.fx.experimental.proxy_tensor import ( + make_fx, + ProxyTorchDispatchMode, + track_tensor_tree, +) +from torch.fx.graph_module import GraphModule +from torch.utils.checkpoint import _CachedTorchDispatchMode, _CachingTorchDispatchMode + + +# Duplicate of _inductor/kernel/flex_attention.py to avoid circular import +def _construct_strides( + sizes: Sequence[int], + fill_order: Sequence[int], +) -> Sequence[int]: + """From a list of sizes and a fill order, construct the strides of the permuted tensor.""" + # Initialize strides + if len(sizes) != len(fill_order): + raise AssertionError( + f"Length of sizes must match the length of the fill order, got {len(sizes)} vs {len(fill_order)}" + ) + strides = [0] * len(sizes) + + # Start with stride 1 for the innermost dimension + current_stride = 1 + + # Iterate through the fill order populating strides + for dim in fill_order: + strides[dim] = current_stride + current_stride *= sizes[dim] + + return strides + + +def _permute_strides(out: torch.Tensor, query_strides: tuple[int, ...]) -> torch.Tensor: + """ + Create a new tensor with the same data and shape as the input, + but with strides permuted based on the input tensor's stride order. + + Args: + out (torch.Tensor): The output tensor of attention. + query_strides (List[int]): The stride order of the input query tensor + + Returns: + torch.Tensor: A new tensor with same shape and data as the input, + but with strides permuted based on the query tensor's stride order. + """ + from torch._inductor.ir import get_fill_order + + fill_order = get_fill_order(query_strides) + if out.storage_offset() != 0: + raise AssertionError( + f"Only support storage_offset == 0, got {out.storage_offset()}" + ) + out_strides = _construct_strides(out.shape, fill_order) + + # Attention kernels require stride[-1]=1 for efficient memory access. + # Ensure this by moving last dim to front of fill_order if needed. + if out_strides[-1] != 1: + last_dim = len(out.shape) - 1 + fill_order = list(fill_order) + fill_order.remove(last_dim) + fill_order = [last_dim] + fill_order + out_strides = _construct_strides(out.shape, fill_order) + + new_out = out.new_empty_strided(out.shape, out_strides) + new_out.copy_(out) + return new_out + + +class FlexAttentionHOP(HigherOrderOperator): + def __init__(self) -> None: + super().__init__("flex_attention", cacheable=True) + + def __call__( + self, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + score_mod: Callable, + block_mask: tuple, + scale: float, + kernel_options: dict[str, Any], + score_mod_other_buffers: tuple = (), + mask_mod_other_buffers: tuple = (), + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + validate_subgraph_args_types(score_mod_other_buffers + mask_mod_other_buffers) + # pyrefly: ignore [missing-attribute] + return super().__call__( + query, + key, + value, + score_mod, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ) + + +flex_attention = FlexAttentionHOP() + + +class FlexAttentionBackwardHOP(HigherOrderOperator): + def __init__(self) -> None: + super().__init__("flex_attention_backward", cacheable=True) + + def __call__( + self, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + out: torch.Tensor, + logsumexp: torch.Tensor, + grad_out: torch.Tensor | None, + grad_logsumexp: torch.Tensor | None, + fw_graph: Callable | GraphModule, + joint_graph: GraphModule, + block_mask: tuple, + scale: float, + kernel_options: dict[str, Any], + score_mod_other_buffers: tuple = (), + mask_mod_other_buffers: tuple = (), + ) -> tuple[ + torch.Tensor, torch.Tensor, torch.Tensor, tuple[torch.Tensor | None, ...] + ]: + validate_subgraph_args_types(score_mod_other_buffers + mask_mod_other_buffers) + + # pyrefly: ignore [missing-attribute] + return super().__call__( + query, + key, + value, + out, + logsumexp, + grad_out, + grad_logsumexp, + fw_graph, + joint_graph, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ) + + +flex_attention_backward = FlexAttentionBackwardHOP() + + +def _math_attention_inner( + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + score_mod: Callable, + block_mask: tuple, + scale: float, + kernel_options: dict[str, Any], + score_mod_other_buffers: tuple = (), + mask_mod_other_buffers: tuple = (), +) -> tuple[torch.Tensor, torch.Tensor]: + from torch._dynamo._trace_wrapped_higher_order_op import TransformGetItemToIndex + + working_precision = torch.float64 if query.dtype == torch.float64 else torch.float32 + + scores = query.to(working_precision) @ key.to(working_precision).transpose(-2, -1) + + b = torch.arange(0, scores.size(0), device=scores.device) + h = torch.arange(0, scores.size(1), device=scores.device) + m = torch.arange(0, scores.size(2), device=scores.device) + n = torch.arange(0, scores.size(3), device=scores.device) + + captured_buffers_in_dim = (None,) * len(score_mod_other_buffers) + from torch.nn.attention.flex_attention import _vmap_for_bhqkv + + # first input is score + score_mod = _vmap_for_bhqkv(score_mod, prefix=(0,), suffix=captured_buffers_in_dim) + + mask_mod = block_mask[-1] + mask_mod_in_dim_buffers = (None,) * len(mask_mod_other_buffers) + mask_mod = _vmap_for_bhqkv(mask_mod, prefix=(), suffix=mask_mod_in_dim_buffers) + + with TransformGetItemToIndex(): + scores = (scores * scale).to(working_precision) + post_mod_scores = torch.where( + mask_mod(b, h, m, n, *mask_mod_other_buffers), + score_mod(scores, b, h, m, n, *score_mod_other_buffers), + torch.tensor(-float("inf"), dtype=working_precision, device=scores.device), + ) + + return scores, post_mod_scores + + +def math_attention( + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + score_mod: Callable, + block_mask: tuple, + scale: float, + kernel_options: dict[str, Any], + score_mod_other_buffers: tuple = (), + mask_mod_other_buffers: tuple = (), +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Eager implementation + + This implementation uses vmap to vectorize the score_mod function over the batch, head, m, and n dimensions. + We then apply the vectorized score_mod function to the scores matrix. Each wrap of vmap applies one of the + batch, head, m, or n dimensions. We need to apply vmap 4 times to vectorized over all 4 dimensions. + + Args: + query: The query tensor + key: The key tensor + value: The value tensor + score_mod: The score_mod function + other_buffers: Other buffers that are passed to the score_mod function + + Notes: + Query and Keys are dtype cast up to float64 (if query.dtype is float64) and float32 otherwise. + Scores and Values are dtype cast to input query.dtype at the end. + """ + # broadcast query & key along head dim for GQA + G = query.size(1) // key.size(1) + value = torch.repeat_interleave(value, G, dim=1) + key = torch.repeat_interleave(key, G, dim=1) + + Bq, Bkv = query.size(0), key.size(0) + if not ((Bq == Bkv) or (Bq > 1 and Bkv == 1)): + raise RuntimeError(f"Bq and Bkv must broadcast. Got Bq={Bq} and Bkv={Bkv}") + + key = key.expand((Bq, *key.size()[1:])) + value = value.expand((Bq, *value.size()[1:])) + + _, post_mod_scores = _math_attention_inner( + query, + key, + value, + score_mod, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ) + + # Set fully masked rows' sumexp to 0.0 + logsumexp = post_mod_scores.logsumexp(dim=-1) + masked_rows = torch.all(post_mod_scores == -float("inf"), dim=-1) + logsumexp = torch.where(masked_rows, -float("inf"), logsumexp) + + # working precision will be used so no need to cast to fp32 + max_scores = torch.max(post_mod_scores, dim=-1)[0] + + post_mod_scores = torch._safe_softmax(post_mod_scores, dim=-1) + + # NB: kernel computes in ln2 space, we always convert back at the top level op, so + # for math impl we divide by log(2) because we will multiply by log(2) + + return ( + post_mod_scores.to(query.dtype) @ value.to(query.dtype), + logsumexp / math.log(2), + max_scores / math.log(2), + ) + + +def _flex_attention_autocast_impl( + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + score_mod: Callable, + block_mask: tuple, + scale: float, + kernel_options: dict[str, Any], + score_mod_other_buffers: tuple, + mask_mod_other_buffers: tuple, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """ + Forward-only autocast shim: cast Q/K/V to the active autocast dtype, then + redispatch with Autocast keys excluded so we hit the normal implementation. + """ + device_type = query.device.type + autocast_dtype = torch.get_autocast_dtype(device_type) + + query = _autocast_cast(query, device_type, autocast_dtype) + key = _autocast_cast(key, device_type, autocast_dtype) + value = _autocast_cast(value, device_type, autocast_dtype) + + autocast_keyset = torch._C.DispatchKeySet( + DispatchKey.AutocastCPU + ) | torch._C.DispatchKeySet(DispatchKey.AutocastCUDA) + with torch._C._ExcludeDispatchKeyGuard(autocast_keyset): + return flex_attention( + query, + key, + value, + score_mod, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ) + + +@flex_attention.py_impl(DispatchKey.AutocastCUDA) +def flex_attention_autocast_cuda( + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + score_mod: Callable, + block_mask: tuple, + scale: float, + kernel_options: dict[str, Any], + score_mod_other_buffers: tuple = (), + mask_mod_other_buffers: tuple = (), +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + return _flex_attention_autocast_impl( + query, + key, + value, + score_mod, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ) + + +@flex_attention.py_impl(DispatchKey.AutocastCPU) +def flex_attention_autocast_cpu( + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + score_mod: Callable, + block_mask: tuple, + scale: float, + kernel_options: dict[str, Any], + score_mod_other_buffers: tuple = (), + mask_mod_other_buffers: tuple = (), +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + return _flex_attention_autocast_impl( + query, + key, + value, + score_mod, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ) + + +@flex_attention.py_impl(DispatchKey.CompositeExplicitAutograd) +def sdpa_dense( + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + score_mod: Callable, + block_mask: tuple, + scale: float, + kernel_options: dict[str, Any], + score_mod_other_buffers: tuple = (), + mask_mod_other_buffers: tuple = (), +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + out, lse, max_scores = math_attention( + query, + key, + value, + score_mod, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ) + out = _permute_strides(out, query.stride()) + return out, lse, max_scores + + +def trace_flex_attention( + proxy_mode: ProxyTorchDispatchMode, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + score_mod: Callable, + block_mask: tuple, + scale: float, + kernel_options: dict[str, Any], + score_mod_other_buffers: tuple = (), + mask_mod_other_buffers: tuple = (), +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Traces the flex_attention operator with the given score_mod function and other_buffers. + + Trace SDPA will call make_fx with "fake" example vals and then trace the score_mod function + This will produce a GraphModule that will be stored on the root tracer as "sdpa_score". We + access this graph module in inductor to inline the score_mod function to the triton template. + """ + from torch._dynamo._trace_wrapped_higher_order_op import TransformGetItemToIndex + + example_out = flex_attention( + query, + key, + value, + score_mod, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ) + example_vals = [query.new_zeros((), requires_grad=query.requires_grad)] + [ + query.new_zeros((), dtype=torch.int) for _ in range(4) + ] + mask_example_vals = [query.new_zeros((), dtype=torch.int) for _ in range(4)] + mask_mod = block_mask[-1] + with TransformGetItemToIndex(): + score_graph = reenter_make_fx(score_mod)( + *example_vals, *score_mod_other_buffers + ) + mask_graph = reenter_make_fx(mask_mod)( + *mask_example_vals, *mask_mod_other_buffers + ) + if not isinstance(proxy_mode.tracer, torch.fx.Tracer): + raise AssertionError( + f"expected proxy_mode.tracer to be torch.fx.Tracer, got {type(proxy_mode.tracer)}" + ) + block_mask = block_mask[:-1] + (mask_graph,) + qualname = proxy_mode.tracer.get_fresh_qualname("sdpa_score") + proxy_mode.tracer.root.register_module(qualname, score_graph) + mask_qualname = proxy_mode.tracer.get_fresh_qualname("sdpa_mask") + proxy_mode.tracer.root.register_module(mask_qualname, mask_graph) + node_args = ( + query, + key, + value, + score_graph, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ) + # pyrefly: ignore [missing-attribute] + proxy_args = pytree.tree_map(proxy_mode.tracer.unwrap_proxy, node_args) + with torch.fx.experimental.proxy_tensor.set_original_aten_op(flex_attention): + out_proxy = proxy_mode.tracer.create_proxy( + "call_function", flex_attention, proxy_args, {} + ) + return track_tensor_tree( + example_out, + out_proxy, + constant=None, + tracer=proxy_mode.tracer, + ) + + +@flex_attention.py_impl(ProxyTorchDispatchMode) +def flex_attention_proxy_torch_dispatch_mode( + mode: ProxyTorchDispatchMode, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + score_mod: Callable, + block_mask: tuple, + scale: float, + kernel_options: dict[str, Any], + score_mod_other_buffers: tuple = (), + mask_mod_other_buffers: tuple = (), +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + if mode is None: + raise AssertionError("Mode should always be enabled for python fallback key") + return trace_flex_attention( + mode, + query, + key, + value, + score_mod, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ) + + +@flex_attention.py_functionalize_impl +def flex_attention_functionalize( + ctx: torch._subclasses.functional_tensor.BaseFunctionalizeAPI, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + score_mod: Callable, + block_mask: tuple, + scale: float, + kernel_options: dict[str, Any], + score_mod_other_buffers: tuple = (), + mask_mod_other_buffers: tuple = (), +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Defines the functionalization rules for the flex_attention operator. + + Write now we are unwrapping each tensor and then redispatching to the next, however we want to + guard against any mutations in the score_mod function, to the other_buffers since those + are free variables. + """ + from torch._dynamo._trace_wrapped_higher_order_op import TransformGetItemToIndex + + query_unwrapped = ctx.unwrap_tensors(query) + key_unwrapped = ctx.unwrap_tensors(key) + value_unwrapped = ctx.unwrap_tensors(value) + block_mask_unwrapped = ctx.unwrap_tensors(block_mask) + score_mod_other_buffers_unwrapped = ctx.unwrap_tensors(score_mod_other_buffers) + mask_mod_other_buffers_unwrapped = ctx.unwrap_tensors(mask_mod_other_buffers) + + # Appease the mypy overlords + if not isinstance(query_unwrapped, torch.Tensor): + raise AssertionError( + f"expected query_unwrapped to be torch.Tensor, got {type(query_unwrapped)}" + ) + if not isinstance(key_unwrapped, torch.Tensor): + raise AssertionError( + f"expected key_unwrapped to be torch.Tensor, got {type(key_unwrapped)}" + ) + if not isinstance(value_unwrapped, torch.Tensor): + raise AssertionError( + f"expected value_unwrapped to be torch.Tensor, got {type(value_unwrapped)}" + ) + if not isinstance(block_mask_unwrapped, tuple): + raise AssertionError( + f"expected block_mask_unwrapped to be tuple, got {type(block_mask_unwrapped)}" + ) + if not isinstance(score_mod_other_buffers_unwrapped, tuple): + raise AssertionError( + f"expected score_mod_other_buffers_unwrapped to be tuple, got {type(score_mod_other_buffers_unwrapped)}" + ) + if not isinstance(mask_mod_other_buffers_unwrapped, tuple): + raise AssertionError( + f"expected mask_mod_other_buffers_unwrapped to be tuple, got {type(mask_mod_other_buffers_unwrapped)}" + ) + + example_vals = ( + [query_unwrapped.new_zeros(())] + + [query_unwrapped.new_zeros((), dtype=torch.int) for _ in range(4)] + + list(score_mod_other_buffers_unwrapped) + ) + with ctx.redispatch_to_next(): + functional_score_mod = ctx.functionalize(score_mod) + pre_dispatch = hasattr(ctx, "mode") and ctx.mode.pre_dispatch + with TransformGetItemToIndex(): + # TODO: So far only the input mutations are checked + # In the other HOPs, also aliases are checked which is + # omitted here + mutates = _has_potential_branch_input_mutation( + score_mod, example_vals, pre_dispatch + ) + # The only care about mutations of existing buffers since we can't replay these. + # However, we can just error if anything is detected + if mutates: + raise UnsupportedAliasMutationException("Mutations detected in score_mod") + + out = flex_attention( + query_unwrapped, + key_unwrapped, + value_unwrapped, + functional_score_mod, + block_mask_unwrapped, + scale, + kernel_options, + score_mod_other_buffers_unwrapped, + mask_mod_other_buffers_unwrapped, + ) + return ctx.wrap_tensors(out) # type: ignore[return-value, arg-type] + + +@register_fake(flex_attention) +def flex_attention_fake_impl( + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + score_mod: Callable, + block_mask: tuple, + scale: float, + kernel_options: dict[str, Any], + score_mod_other_buffers: tuple = (), + mask_mod_other_buffers: tuple = (), +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + if has_user_subclass( + ( + query, + key, + value, + score_mod, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ), + allowed_subclasses=(FakeTensor,), + ): + return NotImplemented + + v_head_dim = value.size(-1) + batch_size, num_heads, seq_len_q, _q_head_dim = query.shape + logsumexp = query.new_empty(batch_size, num_heads, seq_len_q, dtype=torch.float32) + max_scores = query.new_empty(batch_size, num_heads, seq_len_q, dtype=torch.float32) + out_shape = (batch_size, num_heads, seq_len_q, v_head_dim) + out = query.new_empty(out_shape) + out = _permute_strides(out, query.stride()) + return out, logsumexp, max_scores + + +# Registers dispatches for SAC +redirect_to_mode(flex_attention, _CachingTorchDispatchMode) +redirect_to_mode(flex_attention, _CachedTorchDispatchMode) + + +# ---------------------------- Autograd Implementation ---------------------------- +def create_fw_bw_graph( + score_mod: Callable, + index_values: tuple[Tensor, Tensor, Tensor, Tensor, Tensor], + other_buffers: tuple[Tensor, ...], +) -> tuple[Callable, Callable]: + # See Note:[HOP create fw_bw graph] + + # All of these imports need to be here in order to avoid circular dependencies + from torch._dispatch.python import suspend_functionalization + from torch._functorch.aot_autograd import AOTConfig, create_joint + from torch._subclasses.fake_tensor import FakeTensor, FakeTensorMode + from torch._subclasses.functional_tensor import disable_functional_mode + from torch.fx.experimental.proxy_tensor import disable_proxy_modes_tracing + + dummy_aot_config = AOTConfig( + fw_compiler=None, # type: ignore[arg-type] + bw_compiler=None, # type: ignore[arg-type] + partition_fn=None, # type: ignore[arg-type] + decompositions={}, + num_params_buffers=0, + aot_id=0, + keep_inference_input_mutations=False, + ) + + with suspend_functionalization(), disable_functional_mode(): + with disable_proxy_modes_tracing(): + + def _from_fun( + t: Tensor | torch.SymInt | int, + ) -> Tensor | torch.SymInt | int: + if isinstance(t, torch.Tensor): + return torch.empty_strided( + t.size(), + t.stride(), + device=t.device, + dtype=t.dtype, + requires_grad=t.requires_grad, + ) + return t + + # If someone runs this hop under the default compiler backend ("eager") + # Then this path will be run with the actual user inputs. We convert them + # to fake tensors in order to not perform any actual compute. + from torch._guards import detect_fake_mode + + fake_mode = detect_fake_mode(index_values) + if fake_mode is None: + fake_mode = FakeTensorMode(allow_non_fake_inputs=True) + + with fake_mode: + unwrapped_score_mod_indexes = pytree.tree_map(_from_fun, index_values) + unwrapped_other_buffers = pytree.tree_map(_from_fun, other_buffers) + + if not all( + isinstance(t, (FakeTensor, int, torch.SymInt)) + for t in unwrapped_score_mod_indexes + unwrapped_other_buffers + ): + raise AssertionError( + f"all unwrapped values must be FakeTensor, int, or SymInt, got " + f"{[type(t) for t in unwrapped_score_mod_indexes + unwrapped_other_buffers]}" + ) + + example_flat_out = pytree.tree_map( + _from_fun, + score_mod(*unwrapped_score_mod_indexes, *unwrapped_other_buffers), + ) + if not isinstance(example_flat_out, torch.Tensor): + raise RuntimeError( + "Expected output of score_mod to be a tensor." + f"Got type {type(example_flat_out)}." + ) + example_grad = _from_fun(example_flat_out) + + def joint_f( + score: Tensor, + b: Tensor, + h: Tensor, + m: Tensor, + n: Tensor, + example_grad: Tensor, + *other_buffers: tuple[Tensor, ...], + ) -> tuple[Tensor, ...]: + def fw_with_masks( + *args: tuple[Tensor, ...], + ) -> tuple[tuple[Tensor], tuple[bool]]: + fw_out = score_mod(*args) + out_requires_grad = fw_out.requires_grad + return ((fw_out,), (out_requires_grad,)) + + joint = create_joint(fw_with_masks, aot_config=dummy_aot_config) + args = [score, b, h, m, n] + list(other_buffers) + optional_grad = [example_grad] if example_grad.requires_grad else [] + _, grads = joint(args, optional_grad) + + return grads + + joint_graph = make_fx(joint_f)( + *unwrapped_score_mod_indexes, example_grad, *unwrapped_other_buffers + ) + # pyrefly: ignore [bad-return] + return score_mod, joint_graph + + +class FlexAttentionAutogradOp(torch.autograd.Function): + @staticmethod + # pyrefly: ignore [bad-override] + def forward( + ctx: Any, + query: Tensor, + key: Tensor, + value: Tensor, + fw_graph: Callable, + joint_graph: Callable, + block_mask: tuple[Any, ...], + scale: float, + kernel_options: dict[str, Any], + mask_mod_other_buffers: tuple[Any, ...], + *score_mod_other_buffers: tuple[Any, ...], + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + ctx.set_materialize_grads(False) + any_buffer_requires_grad = any( + buffer.requires_grad + for buffer in mask_mod_other_buffers + if isinstance(buffer, torch.Tensor) + ) + if any_buffer_requires_grad: + raise AssertionError( + "Captured buffers from mask mod that require grad are not supported." + ) + ctx._fw_graph = fw_graph + ctx._joint_graph = joint_graph + ctx._mask_graph = block_mask[-1] + ctx.scale = scale + ctx.kernel_options = kernel_options + ctx._score_mod_other_buffers_len = len(score_mod_other_buffers) + with torch._C._AutoDispatchBelowAutograd(): + out, logsumexp, max_scores = flex_attention( + query, + key, + value, + fw_graph, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ) + # no grads for you sir + ctx.mark_non_differentiable(max_scores) + save_values_for_backward( + ctx, + ( + query, + key, + value, + out, + logsumexp, + max_scores, + *block_mask[:-1], + *score_mod_other_buffers, + *mask_mod_other_buffers, + ), + ) + return out, logsumexp, max_scores + + @staticmethod + def backward( # type: ignore[override] + ctx: Any, + grad_out: Tensor, + grad_logsumexp: Tensor, + grad_max_scores: Tensor, + ) -> tuple[Tensor | None, ...]: + fw_args = saved_values(ctx) + ( + query, + key, + value, + out, + logsumexp, + max_scores, + query_lengths, + kv_lengths, + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + q_num_blocks, + q_indices, + full_q_num_blocks, + full_q_indices, + Q_BLOCK_SIZE, + KV_BLOCK_SIZE, + *other_buffers, + ) = fw_args + fw_graph = ctx._fw_graph + joint_graph = ctx._joint_graph + mask_graph = ctx._mask_graph + scale = ctx.scale + kernel_options = ctx.kernel_options + score_mod_other_buffers = tuple( + other_buffers[: ctx._score_mod_other_buffers_len] + ) + mask_mod_other_buffers = tuple( + other_buffers[ctx._score_mod_other_buffers_len :] + ) + # We have asserted that mask_mod_other_buffers do not require grad, + # but score_mod_other_buffers can require grad. + none_grads = [None] * 6 + ( + grad_query, + grad_key, + grad_value, + grad_score_mod_captured, + ) = flex_attention_backward( + query, + key, + value, + out, + logsumexp, + grad_out, + grad_logsumexp, + fw_graph, + joint_graph, + ( + query_lengths, + kv_lengths, + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + q_num_blocks, + q_indices, + full_q_num_blocks, + full_q_indices, + Q_BLOCK_SIZE, + KV_BLOCK_SIZE, + mask_graph, + ), + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ) + return grad_query, grad_key, grad_value, *none_grads, *grad_score_mod_captured + + +# TODO: Rework DispatchKey.Autograd to py_autograd_impl +@flex_attention.py_impl(DispatchKey.Autograd) +def flex_attention_autograd( + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + score_mod: Callable, + block_mask: tuple, + scale: float, + kernel_options: dict[str, Any], + score_mod_other_buffers: tuple[Tensor, ...] = (), + mask_mod_other_buffers: tuple[Tensor, ...] = (), +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + from torch._dynamo._trace_wrapped_higher_order_op import TransformGetItemToIndex + + with TransformGetItemToIndex(): + input_requires_grad = any( + isinstance(t, torch.Tensor) and t.requires_grad + for t in (query, key, value, *score_mod_other_buffers) + ) + if torch.is_grad_enabled() and input_requires_grad: + if block_mask[7] is None: + raise RuntimeError( + "BlockMask q_indices is None. Backward pass requires q_indices to be computed. " + "Please create the BlockMask with compute_q_blocks=True" + ) + example_vals = ( + query.new_zeros((), requires_grad=input_requires_grad), + query.new_zeros((), dtype=torch.int), + query.new_zeros((), dtype=torch.int), + query.new_zeros((), dtype=torch.int), + query.new_zeros((), dtype=torch.int), + ) + fw_graph, bw_graph = create_fw_bw_graph( + score_mod, example_vals, score_mod_other_buffers + ) + else: + fw_graph, bw_graph = score_mod, None + out, logsumexp, max_scores = FlexAttentionAutogradOp.apply( + query, + key, + value, + fw_graph, + bw_graph, + block_mask, + scale, + kernel_options, + mask_mod_other_buffers, + *score_mod_other_buffers, + ) + return out, logsumexp, max_scores + + +# ---------------------------- Backward HOP Implementation ---------------------------- + + +@flex_attention_backward.py_impl(DispatchKey.CompositeExplicitAutograd) +def sdpa_dense_backward( + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + out: torch.Tensor, + logsumexp: torch.Tensor, + grad_out: torch.Tensor | None, + grad_logsumexp: torch.Tensor | None, + fw_graph: Callable, # GraphModule type hint? + joint_graph: Callable, + block_mask: tuple, + scale: float, + kernel_options: dict[str, Any], + score_mod_other_buffers: tuple, + mask_mod_other_buffers: tuple, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, tuple[torch.Tensor | None, ...]]: + if query.dtype != key.dtype or query.dtype != value.dtype: + raise ValueError( + f"Backward pass with mixed query, key, and value dtype is not supported, " + f"got query.dtype={query.dtype}, key.dtype={key.dtype}, " + f"and value.dtype={value.dtype}" + ) + if joint_graph is None: + example_vals = ( + query.new_zeros((), requires_grad=True), + query.new_zeros((), dtype=torch.int), + query.new_zeros((), dtype=torch.int), + query.new_zeros((), dtype=torch.int), + query.new_zeros((), dtype=torch.int), + ) + _, joint_graph = create_fw_bw_graph( + fw_graph, example_vals, score_mod_other_buffers + ) + from torch._dynamo._trace_wrapped_higher_order_op import TransformGetItemToIndex + + Bq, Hq, seq_len_q, qk_head_dim = query.shape + Bkv, Hkv, seq_len_kv, v_head_dim = value.shape + + # Get outputs before calling repeat interleave and permute to input stride orders + actual_grad_query = query.new_empty((Bq, Hq, seq_len_q, qk_head_dim)) + actual_grad_query = _permute_strides(actual_grad_query, query.stride()) + + actual_grad_key = key.new_empty((Bq, Hkv, seq_len_kv, qk_head_dim)) + actual_grad_key = _permute_strides(actual_grad_key, key.stride()) + + actual_grad_value = value.new_empty((Bq, Hkv, seq_len_kv, v_head_dim)) + actual_grad_value = _permute_strides(actual_grad_value, value.stride()) + + def _maybe_new_buffer( + buffer: torch.Tensor | torch.SymInt | int, + ) -> torch.Tensor | torch.SymInt | int | None: + if isinstance(buffer, torch.Tensor): + return ( + torch.empty_like(buffer, memory_format=torch.contiguous_format) + if buffer.requires_grad + else None + ) + return buffer + + actual_grad_score_mod_captured = [ + _maybe_new_buffer(buffer) for buffer in score_mod_other_buffers + ] + + Bq, Bkv = query.size(0), key.size(0) + if not ((Bq == Bkv) or (Bq > 1 and Bkv == 1)): + raise RuntimeError(f"Bq and Bkv must broadcast. Got Bq={Bq} and Bkv={Bkv}") + + key = key.expand((Bq, *key.size()[1:])) + value = value.expand((Bq, *value.size()[1:])) + + G = query.size(1) // key.size(1) + key = torch.repeat_interleave(key, G, dim=1) + value = torch.repeat_interleave(value, G, dim=1) + + if grad_out is None: + grad_out = torch.zeros_like(out) + if grad_logsumexp is None: + grad_logsumexp = torch.zeros_like(logsumexp) + + # logsumexp is expected in log2 scale (as returned by the forward HOP). + # The public flex_attention API converts lse to natural log before returning, + # so callers using the public API must not pass that value here directly. + logsumexp = logsumexp * math.log(2) + # The backwards formula for the log -> log2 change of base in the forwards + grad_logsumexp = grad_logsumexp / math.log(2) + scores, post_mod_scores = _math_attention_inner( + query, + key, + value, + fw_graph, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ) + masked_out_rows = logsumexp == -float("inf") + softmax_scores = torch.exp(post_mod_scores - logsumexp.unsqueeze(-1)) + softmax_scores = torch.where(masked_out_rows.unsqueeze(-1), 0, softmax_scores) + + grad_value = softmax_scores.to(query.dtype).transpose(-2, -1) @ grad_out + + grad_softmax_scores = grad_out.to(dtype=softmax_scores.dtype) @ value.to( + dtype=softmax_scores.dtype + ).transpose(-2, -1) + + sum_scores = torch.sum( + out.to(dtype=softmax_scores.dtype) * grad_out.to(dtype=softmax_scores.dtype), + -1, + keepdim=True, + ) + grad_score_mod = softmax_scores * ( + grad_softmax_scores - sum_scores + grad_logsumexp.unsqueeze(-1) + ) + + b = torch.arange(0, scores.size(0), device=scores.device) + h = torch.arange(0, scores.size(1), device=scores.device) + m = torch.arange(0, scores.size(2), device=scores.device) + n = torch.arange(0, scores.size(3), device=scores.device) + + mask_graph = block_mask[-1] + # Gradient of the inline score_mod function, with respect to the scores + captured_buffers_in_dim = (None,) * len(score_mod_other_buffers) + out_dims = [0, None, None, None, None] + [None] * len(score_mod_other_buffers) + from torch.nn.attention.flex_attention import _vmap_for_bhqkv + + # inputs are [score, b, h, q_idx, kv_idx, gradOut, ...] + # score and gradOut are "fully" batched + joint_score_mod = _vmap_for_bhqkv( + joint_graph, + prefix=(0,), + suffix=(0,) + captured_buffers_in_dim, + out_dims=out_dims, + ) + with TransformGetItemToIndex(): + grad_scores, _, _, _, _, *grad_score_mod_captured = joint_score_mod( + scores, b, h, m, n, grad_score_mod, *score_mod_other_buffers + ) + grad_scores = grad_scores * scale + grad_scores = grad_scores.to(query.dtype) + + mask_mod = _vmap_for_bhqkv( + mask_graph, prefix=(), suffix=(None,) * len(mask_mod_other_buffers) + ) + with TransformGetItemToIndex(): + mask_scores = mask_mod(b, h, m, n, *mask_mod_other_buffers) + grad_scores = torch.where( + mask_scores, grad_scores, torch.tensor(0, dtype=query.dtype) + ) + + grad_query = grad_scores @ key + grad_key = grad_scores.transpose(-2, -1) @ query + + # Reduce DK, DV along broadcasted heads. + grad_key = grad_key.view( + grad_key.size(0), -1, G, grad_key.size(-2), grad_key.size(-1) + ) + grad_value = grad_value.view( + grad_value.size(0), -1, G, grad_value.size(-2), grad_value.size(-1) + ) + + grad_key = torch.sum(grad_key, 2, keepdim=False) + grad_value = torch.sum(grad_value, 2, keepdim=False) + + # Fill to correctly strided outputs + actual_grad_query.copy_(grad_query) + actual_grad_key.copy_(grad_key) + actual_grad_value.copy_(grad_value) + + if Bq != Bkv: + if not (Bq > 1 and Bkv == 1): + raise AssertionError( + f"Bq and Bkv must broadcast. Got Bq={Bq} and Bkv={Bkv}" + ) + + actual_grad_key = torch.sum(actual_grad_key, 0, keepdim=True) + actual_grad_value = torch.sum(actual_grad_value, 0, keepdim=True) + + score_mod_other_buffer_grads = [ + actual_grad.copy_(grad) if isinstance(actual_grad, torch.Tensor) else None + for actual_grad, grad in zip( + actual_grad_score_mod_captured, grad_score_mod_captured + ) + ] + + return ( + actual_grad_query, + actual_grad_key, + actual_grad_value, + tuple(score_mod_other_buffer_grads), + ) + + +def trace_flex_attention_backward( + proxy_mode: ProxyTorchDispatchMode, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + out: torch.Tensor, + logsumexp: torch.Tensor, + grad_out: torch.Tensor, + grad_logsumexp: torch.Tensor, + fw_graph: Callable | GraphModule, + joint_graph: GraphModule, + block_mask: tuple, + scale: float, + kernel_options: dict[str, Any], + score_mod_other_buffers: tuple = (), + mask_mod_other_buffers: tuple = (), +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, tuple[torch.Tensor | None, ...]]: + """We already have the forward graph and joint graph from the forward pass, so we create a proxy attach both graphs""" + from torch._dynamo._trace_wrapped_higher_order_op import TransformGetItemToIndex + + example_out = flex_attention_backward( + query, + key, + value, + out, + logsumexp, + grad_out, + grad_logsumexp, + fw_graph, + joint_graph, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ) + + requires_grad = any(pytree.tree_map(lambda x: x.requires_grad, (query, key))) + fw_example_vals = [query.new_zeros((), requires_grad=requires_grad)] + [ + query.new_zeros((), dtype=torch.int) for _ in range(4) + ] + bw_example_vals = fw_example_vals + [query.new_zeros(())] + mask_example_vals = [query.new_zeros((), dtype=torch.int) for _ in range(4)] + mask_graph = block_mask[-1] + with TransformGetItemToIndex(): + # There's no active make_fx during the compiled autograd graph's initial capture + fw_graph = _maybe_reenter_make_fx(fw_graph)( + *fw_example_vals, *score_mod_other_buffers + ) + joint_graph = _maybe_reenter_make_fx(joint_graph)( + *bw_example_vals, *score_mod_other_buffers + ) + mask_graph = _maybe_reenter_make_fx(mask_graph)( + *mask_example_vals, *mask_mod_other_buffers + ) + if not isinstance(proxy_mode.tracer, torch.fx.Tracer): + raise AssertionError( + f"expected proxy_mode.tracer to be torch.fx.Tracer, got {type(proxy_mode.tracer)}" + ) + block_mask = block_mask[:-1] + (mask_graph,) + + qualname = proxy_mode.tracer.get_fresh_qualname("fw_graph") + proxy_mode.tracer.root.register_module(qualname, fw_graph) # type: ignore[arg-type] + qualname = proxy_mode.tracer.get_fresh_qualname("joint_graph") + proxy_mode.tracer.root.register_module(qualname, joint_graph) + qualname = proxy_mode.tracer.get_fresh_qualname("mask_graph") + proxy_mode.tracer.root.register_module(qualname, mask_graph) + + node_args = ( + query, + key, + value, + out, + logsumexp, + grad_out, + grad_logsumexp, + fw_graph, + joint_graph, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ) + # pyrefly: ignore [missing-attribute] + proxy_args = pytree.tree_map(proxy_mode.tracer.unwrap_proxy, node_args) + out_proxy = proxy_mode.tracer.create_proxy( + "call_function", + flex_attention_backward, + proxy_args, + {}, + name="flex_attention_backward", + ) + return track_tensor_tree( + example_out, + out_proxy, + constant=None, + tracer=proxy_mode.tracer, + ) + + +@flex_attention_backward.py_impl(ProxyTorchDispatchMode) +def flex_attention_backward_proxy_torch_dispatch_mode( + mode: ProxyTorchDispatchMode, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + out: torch.Tensor, + logsumexp: torch.Tensor, + grad_out: torch.Tensor, + grad_logsumexp: torch.Tensor, + fw_graph: Callable | GraphModule, + joint_graph: GraphModule, + block_mask: tuple, + scale: float, + kernel_options: dict[str, Any], + score_mod_other_buffers: tuple = (), + mask_mod_other_buffers: tuple = (), +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, tuple[torch.Tensor | None, ...]]: + if mode is None: + raise AssertionError("Mode should always be enabled for python fallback key") + with torch.fx.experimental.proxy_tensor.set_original_aten_op( + flex_attention_backward + ): + return trace_flex_attention_backward( + mode, + query, + key, + value, + out, + logsumexp, + grad_out, + grad_logsumexp, + fw_graph, + joint_graph, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ) + + +@flex_attention_backward.py_functionalize_impl +def flex_attention_backward_functionalize( + ctx: torch._subclasses.functional_tensor.BaseFunctionalizeAPI, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + out: torch.Tensor, + logsumexp: torch.Tensor, + grad_out: torch.Tensor, + grad_logsumexp: torch.Tensor, + fw_graph: Callable | GraphModule, + joint_graph: GraphModule, + block_mask: tuple, + scale: float, + kernel_options: dict[str, Any], + score_mod_other_buffers: tuple = (), + mask_mod_other_buffers: tuple = (), +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, tuple[torch.Tensor | None, ...]]: + """Defines the functionalization rules for the flex_attention operator. + + Write now we are unwrapping each tensor and then redispatching to the next, + since we know that the forward score mod function is assured to be free of mutations + to the other_buffers, we skip that mutate check and go straight to redispatching. + """ + + query_unwrapped = ctx.unwrap_tensors(query) + key_unwrapped = ctx.unwrap_tensors(key) + value_unwrapped = ctx.unwrap_tensors(value) + out_unwrapped = ctx.unwrap_tensors(out) + logsumexp_unwrapped = ctx.unwrap_tensors(logsumexp) + grad_out_unwrapped = ctx.unwrap_tensors(grad_out) + grad_logsumexp_unwrapped = ctx.unwrap_tensors(grad_logsumexp) + block_mask_unwrapped = ctx.unwrap_tensors(block_mask) + score_mod_other_buffers_unwrapped = ctx.unwrap_tensors(score_mod_other_buffers) + mask_mod_other_buffers_unwrapped = ctx.unwrap_tensors(mask_mod_other_buffers) + + # Appease the mypy overlords + if not isinstance(query_unwrapped, torch.Tensor): + raise AssertionError( + f"expected query_unwrapped to be torch.Tensor, got {type(query_unwrapped)}" + ) + if not isinstance(key_unwrapped, torch.Tensor): + raise AssertionError( + f"expected key_unwrapped to be torch.Tensor, got {type(key_unwrapped)}" + ) + if not isinstance(value_unwrapped, torch.Tensor): + raise AssertionError( + f"expected value_unwrapped to be torch.Tensor, got {type(value_unwrapped)}" + ) + if not isinstance(out_unwrapped, torch.Tensor): + raise AssertionError( + f"expected out_unwrapped to be torch.Tensor, got {type(out_unwrapped)}" + ) + if not isinstance(logsumexp_unwrapped, torch.Tensor): + raise AssertionError( + f"expected logsumexp_unwrapped to be torch.Tensor, got {type(logsumexp_unwrapped)}" + ) + if grad_out_unwrapped is not None and not isinstance( + grad_out_unwrapped, torch.Tensor + ): + raise AssertionError( + f"expected grad_out_unwrapped to be torch.Tensor or None, got {type(grad_out_unwrapped)}" + ) + if grad_logsumexp_unwrapped is not None and not isinstance( + grad_logsumexp_unwrapped, torch.Tensor + ): + raise AssertionError( + f"expected grad_logsumexp_unwrapped to be torch.Tensor or None, got {type(grad_logsumexp_unwrapped)}" + ) + if not isinstance(block_mask_unwrapped, tuple): + raise AssertionError( + f"expected block_mask_unwrapped to be tuple, got {type(block_mask_unwrapped)}" + ) + if not isinstance(score_mod_other_buffers_unwrapped, tuple): + raise AssertionError( + f"expected score_mod_other_buffers_unwrapped to be tuple, got {type(score_mod_other_buffers_unwrapped)}" + ) + if not isinstance(mask_mod_other_buffers_unwrapped, tuple): + raise AssertionError( + f"expected mask_mod_other_buffers_unwrapped to be tuple, got {type(mask_mod_other_buffers_unwrapped)}" + ) + + with ctx.redispatch_to_next(): + # pyrefly: ignore [bad-argument-type] + functional_fw_graph = ctx.functionalize(fw_graph) + # pyrefly: ignore [bad-argument-type] + functional_joint_graph = ctx.functionalize(joint_graph) + + ( + grad_query, + grad_key, + grad_value, + grad_score_mod_captured, + ) = flex_attention_backward( + query_unwrapped, + key_unwrapped, + value_unwrapped, + out_unwrapped, + logsumexp_unwrapped, + grad_out_unwrapped, + grad_logsumexp_unwrapped, + functional_fw_graph, # type: ignore[arg-type] + functional_joint_graph, # type: ignore[arg-type] + block_mask_unwrapped, + scale, + kernel_options, + score_mod_other_buffers_unwrapped, + mask_mod_other_buffers_unwrapped, + ) + + return ctx.wrap_tensors((grad_query, grad_key, grad_value, grad_score_mod_captured)) # type: ignore[return-value,arg-type] + + +@register_fake(flex_attention_backward) +def flex_attention_backward_fake_tensor_mode( + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + out: torch.Tensor, + logsumexp: torch.Tensor, + grad_out: torch.Tensor, + grad_logsumexp: torch.Tensor, + fw_graph: Callable | GraphModule, + joint_graph: GraphModule, + block_mask: tuple, + scale: float, + kernel_options: dict[str, Any], + score_mod_other_buffers: tuple = (), + mask_mod_other_buffers: tuple = (), +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, tuple[torch.Tensor | None, ...]]: + if has_user_subclass( + ( + query, + key, + value, + out, + logsumexp, + grad_out, + grad_logsumexp, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ), + allowed_subclasses=(FakeTensor,), + ): + return NotImplemented + Bq, _, _, qk_head_dim = query.shape + Bkv, Hkv, seq_len_kv, v_head_dim = value.shape + + grad_query = query.new_empty(query.shape) + grad_query = _permute_strides(grad_query, query.stride()) + # zeros_and_scatter creates a contiguous zeros tensor -> contiguous_format + grad_score_mod_captured = tuple( + ( + torch.empty_like(buffer, memory_format=torch.contiguous_format) + if isinstance(buffer, torch.Tensor) + else None + ) + for buffer in score_mod_other_buffers + ) + + broadcasted_grad_key = key.new_empty((Bq, Hkv, seq_len_kv, qk_head_dim)) + broadcasted_grad_key = _permute_strides(broadcasted_grad_key, key.stride()) + + broadcasted_grad_value = value.new_empty((Bq, Hkv, seq_len_kv, v_head_dim)) + broadcasted_grad_value = _permute_strides(broadcasted_grad_value, value.stride()) + + if Bq > 1 and Bkv == 1: + grad_key = torch.sum(broadcasted_grad_key, dim=0, keepdim=True) + grad_value = torch.sum(broadcasted_grad_value, dim=0, keepdim=True) + else: + grad_key = broadcasted_grad_key + grad_value = broadcasted_grad_value + + return grad_query, grad_key, grad_value, grad_score_mod_captured + + +flex_attention_backward.py_autograd_impl( + autograd_not_implemented(flex_attention_backward, deferred_error=True) +) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/foreach_map.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/foreach_map.py new file mode 100644 index 0000000000000000000000000000000000000000..0d02515d555d3be737011bd4cab32393acbe5b07 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/foreach_map.py @@ -0,0 +1,24 @@ +# mypy: allow-untyped-decorators +# mypy: allow-untyped-defs +from collections.abc import Callable +from typing import Any + +from torch._higher_order_ops.base_hop import BaseHOP, FunctionWithNoFreeVars + + +class ForeachMap(BaseHOP): + def __init__(self): + super().__init__("foreach_map") + + def __call__(self, fn, *operands, **kwargs): # type: ignore[override] + fn = FunctionWithNoFreeVars(fn) + return super().__call__(fn, *operands, **kwargs) + + +_foreach_map = ForeachMap() + + +def foreach_map(op: Callable, *operands: Any, **kwargs: dict[str, Any]): + from torch._dynamo.polyfills import foreach_map_fn + + return _foreach_map(foreach_map_fn, op, *operands, **kwargs) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/hints_wrap.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/hints_wrap.py new file mode 100644 index 0000000000000000000000000000000000000000..508be03643b872f9c37fd75c6b960f255f0f303b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/hints_wrap.py @@ -0,0 +1,142 @@ +# mypy: allow-untyped-defs +import torch +import torch.utils._pytree as pytree +from torch._C import DispatchKey +from torch._higher_order_ops.utils import ( + autograd_not_implemented, + reenter_make_fx, + unique_graph_id, +) +from torch._ops import HigherOrderOperator +from torch._subclasses.fake_tensor import FakeTensorMode +from torch.fx.experimental.proxy_tensor import ProxyTorchDispatchMode, track_tensor_tree + + +# used for wrapping a function/op with context hints +class HintsWrapper(HigherOrderOperator): + def __init__(self): + super().__init__("hints_wrapper") + + def __call__(self, body_fn, args, kwargs, hints): + r""" + Call implementation of hints_wrapper + + Args: + body_fn (Callable): A callable function that is within the scope + that is being traced. + + args (Tuple of torch.Tensor/int/float/bool): A tuple of inputs to + body_fn. + + kwargs (dict): Keyword argument to the body_fn. + + hints (dict): A dict of context hints which could be passed to + backend compiler. + """ + if not isinstance(args, tuple): + args = tuple(args) + + if not all(isinstance(t, (torch.Tensor, int, float, bool)) for t in args): + raise RuntimeError( + f"args must be a tuple of tensors, ints, floats, or bools, got {args}" + ) + + if not isinstance(kwargs, dict): + raise RuntimeError(f"kwargs must be a dict, got {type(kwargs)}") + + if len(kwargs) > 0: + raise RuntimeError( + f"kwargs except for hints are not supported, got {kwargs}" + ) + + if not isinstance(hints, dict): + raise RuntimeError(f"hints must be a dict, got {type(hints)}") + + for k, v in hints.items(): + if not isinstance(k, str): + raise RuntimeError(f"hints key must be a str, got {k}.") + + if not isinstance(v, (int, float, bool, str)): + raise RuntimeError( + "hints must be a dict containing int, float, bool or str " + f"value, got value {v} for key {k}." + ) + + # pyrefly: ignore [missing-attribute] + return super().__call__(body_fn, args, kwargs, hints) + + +hints_wrapper = HintsWrapper() + + +@hints_wrapper.py_impl(DispatchKey.CompositeExplicitAutograd) +def hints_wrapper_dense(body_fn, args, kwargs, hints): + return body_fn(*args, **kwargs) + + +hints_wrapper.py_autograd_impl( + autograd_not_implemented(hints_wrapper, deferred_error=True) +) + + +@hints_wrapper.py_impl(FakeTensorMode) +def hints_wrapper_fake_tensor_mode(mode, body_func, args, kwargs, hints): + flat_args = pytree.tree_leaves(args) + with mode: + return body_func(*flat_args, **kwargs) + + +@hints_wrapper.py_functionalize_impl +def hints_wrapper_functionalize(ctx, body_fn, args, kwargs, hints): + from torch._higher_order_ops.utils import _check_alias_and_mutation + + unwrapped_args = ctx.unwrap_tensors(args) + unwrapped_kwargs = ctx.unwrap_tensors(kwargs) + unwrapped_hints = ctx.unwrap_tensors(hints) + with ctx.redispatch_to_next(): + functional_body_fn = ctx.functionalize(body_fn) + pre_dispatch = hasattr(ctx, "mode") and ctx.mode.pre_dispatch + _check_alias_and_mutation( + body_fn, unwrapped_args, "hints_wrapper", pre_dispatch + ) + + outputs = hints_wrapper( + functional_body_fn, + unwrapped_args, + unwrapped_kwargs, + unwrapped_hints, + ) + return ctx.wrap_tensors(outputs) + + +def trace_hints_wrapper(proxy_mode, hints_wrapper, body_fn, args, kwargs, hints): + flat_args = tuple(pytree.tree_leaves(args)) + body_graph = reenter_make_fx(body_fn)(*flat_args, **kwargs) + + _, body_graph_name = unique_graph_id(proxy_mode, prefix="hints_wrapper_body_graph") + proxy_mode.tracer.root.register_module(body_graph_name, body_graph) + + new_args: tuple = (body_graph, flat_args, {}) + # merge hints into kwargs + new_kwargs = {} + new_kwargs["hints"] = hints + + proxy_args = pytree.tree_map(proxy_mode.tracer.unwrap_proxy, new_args) + proxy_kwargs = pytree.tree_map(proxy_mode.tracer.unwrap_proxy, new_kwargs) + + out_proxy = proxy_mode.tracer.create_proxy( + "call_function", hints_wrapper, proxy_args, proxy_kwargs, name="hints_wrapper" + ) + + out = body_fn(*flat_args, **kwargs) + return track_tensor_tree(out, out_proxy, constant=None, tracer=proxy_mode.tracer) + + +@hints_wrapper.py_impl(ProxyTorchDispatchMode) +def inner(proxy_mode, body_fn, args, kwargs, hints): + if proxy_mode.enable_tracing: + return trace_hints_wrapper( + proxy_mode, hints_wrapper, body_fn, args, kwargs, hints + ) + else: + return hints_wrapper(body_fn, args, kwargs, hints) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/inline_asm_elementwise.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/inline_asm_elementwise.py new file mode 100644 index 0000000000000000000000000000000000000000..f23f8dc626503880281772c87599189a0da630f5 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/inline_asm_elementwise.py @@ -0,0 +1,299 @@ +# mypy: allow-untyped-defs +import functools +import re + +import torch +import torch.utils._pytree as pytree +from torch._C import DispatchKey +from torch._higher_order_ops.utils import autograd_not_implemented +from torch._ops import HigherOrderOperator +from torch._subclasses.fake_tensor import FakeTensorMode +from torch.fx.experimental.proxy_tensor import ProxyTorchDispatchMode, track_tensor_tree + + +__all__ = ["inline_asm_elementwise"] + + +class InlineAsmElementwiseOp(HigherOrderOperator): + """Execute inline PTX assembly elementwise over tensors. + + This is an elementwise map where the function body is inline assembly. + Input tensors are implicitly broadcast to the same shape. + + Each invocation of the inline asm processes ``pack`` elements at a time. + Exactly which set of inputs a given invocation receives is unspecified. + + Output strides follow PyTorch's standard pointwise striding propagation + rules. + + In eager mode, the assembly is executed via the CUDA Jiterator. Under + ``torch.compile`` the assembly is lowered to Triton's + ``tl.inline_asm_elementwise`` via Inductor, which allows fusion with + surrounding operators. + + Args: + *inputs: Input tensors whose values are passed to the asm block. + asm_str: PTX assembly string. Operands use ``$N`` syntax + (e.g. ``$0`` for the first output, ``$1`` for the first input). + constraints: Inline-asm constraints in LLVM format. Output constraints + are prefixed with ``=`` (e.g. ``"=f,f,f"`` for one float output + and two float inputs). + dtype: Element type of the returned tensor. + is_pure: Must be ``True``. If true, the compiler may assume the asm + block has no side-effects. + pack: Number of elements processed per asm invocation. When + ``pack > 1``, the constraint string must list ``pack`` outputs + and ``pack`` copies of each input. Requires ``torch.compile``. + + Returns: + A tensor with the broadcast shape of the inputs and the given dtype. + + Example:: + + >>> # xdoctest: +SKIP(requires CUDA) + >>> # Float32 fused multiply-add via PTX + >>> result = inline_asm_elementwise( + ... a, b, c, + ... asm_str="fma.rn.f32 $0, $1, $2, $3;", + ... constraints="=f,f,f,f", + ... dtype=torch.float32, + ... ) + + >>> # xdoctest: +SKIP(requires CUDA) + >>> # pack=2: each asm invocation processes two elements + >>> result = inline_asm_elementwise( + ... x, + ... asm_str="mov.b32 $0, $2; mov.b32 $1, $3;", + ... constraints="=r,=r,r,r", + ... dtype=torch.float32, + ... pack=2, + ... ) + """ + + def __init__(self): + super().__init__("inline_asm_elementwise") + + def __call__( + self, + *inputs: torch.Tensor, + asm_str: str, + constraints: str, + dtype: torch.dtype, + is_pure: bool = True, + pack: int = 1, + ) -> torch.Tensor: + if not is_pure: + raise ValueError("inline_asm_elementwise only supports is_pure=True") + # pyrefly: ignore [missing-attribute] + return super().__call__( + *inputs, + asm_str=asm_str, + constraints=constraints, + dtype=dtype, + is_pure=True, + pack=pack, + ) + + +inline_asm_elementwise = InlineAsmElementwiseOp() + + +def _parse_constraints(constraints: str) -> tuple[int, int]: + parts = [p.strip() for p in constraints.split(",")] + n_outputs = sum(1 for p in parts if p.startswith("=")) + n_inputs = len(parts) - n_outputs + return n_outputs, n_inputs + + +_DTYPE_TO_CUDA_TYPE = { + torch.float32: "float", + torch.float64: "double", + torch.float16: "__half", + torch.bfloat16: "__nv_bfloat16", + torch.int32: "int", + torch.int64: "long long", + torch.int16: "short", + torch.int8: "signed char", + torch.uint8: "unsigned char", + torch.uint16: "unsigned short", + torch.uint32: "unsigned int", + torch.bool: "bool", +} + + +_TRITON_ARG_RE = re.compile(r"\$(\d+)") + + +def _triton_asm_to_cuda_asm(asm_str: str) -> str: + return _TRITON_ARG_RE.sub(r"%\1", asm_str) + + +@functools.lru_cache +def _get_jiterator_fn( + asm_str: str, + constraints: str, + n_inputs: int, + input_dtype: torch.dtype, + output_dtype: torch.dtype, +): + from torch.cuda.jiterator import _create_jit_fn + + cuda_asm = _triton_asm_to_cuda_asm(asm_str) + + constraint_parts = [p.strip() for p in constraints.split(",")] + output_constraints = [p.lstrip("=") for p in constraint_parts if p.startswith("=")] + input_constraints = [p for p in constraint_parts if not p.startswith("=")] + + if input_dtype not in _DTYPE_TO_CUDA_TYPE: + raise ValueError(f"Unsupported input dtype for inline asm: {input_dtype}") + if output_dtype not in _DTYPE_TO_CUDA_TYPE: + raise ValueError(f"Unsupported output dtype for inline asm: {output_dtype}") + + input_type = _DTYPE_TO_CUDA_TYPE[input_dtype] + output_type = _DTYPE_TO_CUDA_TYPE[output_dtype] + + input_params = ", ".join(f"{input_type} in{i}" for i in range(n_inputs)) + out_constraints_str = ", ".join(f'"={c}"(result)' for c in output_constraints) + in_constraints_str = ", ".join( + f'"{c}"(in{i})' for i, c in enumerate(input_constraints) + ) + escaped_asm = ( + cuda_asm.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") + ) + + code = f""" +template +{output_type} inline_asm_kernel({input_params}) {{ + {output_type} result; + asm volatile ( + "{escaped_asm}" + : {out_constraints_str} + : {in_constraints_str} + ); + return result; +}} +""" + + return _create_jit_fn(code) + + +def _inline_asm_dense(*inputs, asm_str, constraints, dtype, is_pure, pack): + if not inputs: + raise ValueError("inline_asm_elementwise requires at least one input tensor") + + inputs = torch.broadcast_tensors(*inputs) + + if not inputs[0].is_cuda: + raise RuntimeError("inline_asm_elementwise only supports CUDA tensors") + + if pack > 1: + raise RuntimeError( + "inline_asm_elementwise with pack > 1 requires torch.compile" + ) + + n_outputs, n_inputs = _parse_constraints(constraints) + + if n_outputs != 1: + raise ValueError(f"Expected 1 output constraint, got {n_outputs}") + + if n_inputs != len(inputs): + raise ValueError( + f"Constraint string specifies {n_inputs} inputs but got " + f"{len(inputs)} tensor(s)" + ) + + # Jiterator generates a single input type for all inputs — mixed dtypes + # would produce incorrect CUDA code. + input_dtypes = {inp.dtype for inp in inputs} + if len(input_dtypes) > 1: + raise ValueError( + f"All inputs must have the same dtype for eager execution, " + f"got {sorted(str(d) for d in input_dtypes)}" + ) + + jit_fn = _get_jiterator_fn( + asm_str=asm_str, + constraints=constraints, + n_inputs=len(inputs), + input_dtype=inputs[0].dtype, + output_dtype=dtype, + ) + + return jit_fn(*inputs) + + +@inline_asm_elementwise.py_impl(DispatchKey.CompositeExplicitAutograd) +def _(*inputs, asm_str, constraints, dtype, is_pure=True, pack=1): + return _inline_asm_dense( + *inputs, + asm_str=asm_str, + constraints=constraints, + dtype=dtype, + is_pure=is_pure, + pack=pack, + ) + + +inline_asm_elementwise.py_autograd_impl( + autograd_not_implemented(inline_asm_elementwise, deferred_error=True) +) + + +def _elementwise_output_like(*inputs, dtype): + from torch._prims_common import compute_elementwise_output_logical_to_physical_perm + + broadcasted = torch.broadcast_tensors(*inputs) + l2p_perm, _ = compute_elementwise_output_logical_to_physical_perm(*broadcasted) + return torch.empty_permuted( + broadcasted[0].shape, l2p_perm, dtype=dtype, device=broadcasted[0].device + ) + + +@inline_asm_elementwise.py_impl(FakeTensorMode) +def _(mode, *inputs, asm_str, constraints, dtype, is_pure=True, pack=1): + with mode: + return _elementwise_output_like(*inputs, dtype=dtype) + + +@inline_asm_elementwise.py_impl(ProxyTorchDispatchMode) +def _(mode, *inputs, asm_str, constraints, dtype, is_pure=True, pack=1): + proxy_args = pytree.tree_map(mode.tracer.unwrap_proxy, inputs) + + out_proxy = mode.tracer.create_proxy( + "call_function", + inline_asm_elementwise, + proxy_args, + { + "asm_str": asm_str, + "constraints": constraints, + "dtype": dtype, + "is_pure": is_pure, + "pack": pack, + }, + name="inline_asm_elementwise", + ) + + out = inline_asm_elementwise( + *inputs, + asm_str=asm_str, + constraints=constraints, + dtype=dtype, + is_pure=is_pure, + pack=pack, + ) + return track_tensor_tree(out, out_proxy, constant=None, tracer=mode.tracer) + + +@inline_asm_elementwise.py_functionalize_impl +def _(ctx, *inputs, asm_str, constraints, dtype, is_pure=True, pack=1): + unwrapped_inputs = ctx.unwrap_tensors(inputs) + + with ctx.redispatch_to_next(): + res = inline_asm_elementwise( + *unwrapped_inputs, + asm_str=asm_str, + constraints=constraints, + dtype=dtype, + pack=pack, + ) + return ctx.wrap_tensors(res) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/invoke_leaf_function.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/invoke_leaf_function.py new file mode 100644 index 0000000000000000000000000000000000000000..9746e4696cfb81b58db3e13f80f17c36507ea893 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/invoke_leaf_function.py @@ -0,0 +1,897 @@ +import contextlib +import functools +from collections.abc import Callable, Generator, Sequence +from dataclasses import dataclass +from typing import Any, NamedTuple + +import torch +import torch.utils._pytree as pytree +from torch._C import DispatchKey, DispatchKeySet +from torch._higher_order_ops.utils import register_fake +from torch._library.opaque_object import OpaqueBase, register_opaque_type +from torch._ops import HigherOrderOperator +from torch.autograd.graph import get_gradient_edge +from torch.fx.experimental.proxy_tensor import ProxyTorchDispatchMode, track_tensor_tree +from torch.nn.utils.stateless import _reparametrize_module + + +_leaf_function_module_retriever: Callable[[int], Any] | None = None + +# Separate storage for the make_fx / _invoke_leaf_function_python path. +# We can't use Dynamo's register_user_object because it requires a Source +# (for bytecode generation) and adds entries to index_to_bytecode_constructor. +# In the make_fx path we have neither a Source nor bytecode generation, so we +# maintain our own dict keyed by negative indices to avoid collisions with +# Dynamo's non-negative indices. +_makefx_module_storage: dict[int, torch.nn.Module] = {} +_makefx_next_index = 0 + + +def store_makefx_modules(modules: list[torch.nn.Module]) -> tuple[int, ...]: + """Store modules for the make_fx path and return their assigned indices. + + Uses negative indices to avoid collisions with Dynamo's register_user_object + which uses non-negative indices. + """ + global _makefx_next_index + indices = [] + for mod in modules: + _makefx_next_index -= 1 + _makefx_module_storage[_makefx_next_index] = mod + indices.append(_makefx_next_index) + return tuple(indices) + + +def reset_makefx_module_storage() -> None: + global _makefx_next_index + _makefx_next_index = 0 + _makefx_module_storage.clear() + + +class _LeafCallable(OpaqueBase): + def __init__(self, fn: Callable) -> None: + self._fn = fn + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + return self._fn(*args, **kwargs) + + +register_opaque_type(_LeafCallable, typ="reference") + + +def set_leaf_function_module_retriever(retriever: Callable[[int], Any]) -> None: + global _leaf_function_module_retriever + _leaf_function_module_retriever = retriever + + +class LeafModuleState(NamedTuple): + """ + In dynamo, nn.Module arguments to leaf functions are converted to this + pytree format (index, parameters, buffers). This structure is then + flattened to produce the actual inputs to invoke_leaf_function. + + At runtime, the original module is reconstructed from it. + """ + + nn_module_index: int + named_parameters: dict[str, torch.nn.Parameter] + named_buffers: dict[str, torch.Tensor] + + +def convert_modules_to_states(values: Any, module_to_index: dict[int, int]) -> Any: + """Replace nn.Module instances in a pytree with LeafModuleState objects. + + Args: + values: A pytree of values that may contain nn.Module instances. + module_to_index: Mapping from id(module) to its integer index. + """ + + def module_to_state(val: Any) -> Any: + if isinstance(val, torch.nn.Module): + return LeafModuleState( + nn_module_index=module_to_index[id(val)], + named_parameters=dict(val.named_parameters()), + named_buffers=dict(val.named_buffers()), + ) + return val + + return pytree.tree_map(module_to_state, values) + + +def _resolve_mutated_flat_indices( + fn: Callable, + mutates_args: frozenset[str], + num_flat_args: int, + input_spec: pytree.TreeSpec, +) -> str: + """Resolve mutates_args expressions to a comma-separated string of flat-arg indices. + + Each expression in mutates_args (e.g. "x", "model.running_mean") is evaluated + against sentinel values to determine which flat-arg positions are mutated. + + Example: for ``def fn(x, model)`` where model is an nn.Module with parameters + ``weight`` and ``bias``, the flat args are ``[x, nn_module_index, weight, bias]``. + Given ``mutates_args={"model.weight"}``, this assigns sentinels ``[0, 1, 2, 3]`` + to the flat args, evaluates ``model.weight`` to ``2``, and returns ``"2"``. + """ + import inspect + + class _AttrDict: + pass + + def _set_nested_attr(obj: _AttrDict, fqn: str, value: Any) -> None: + parts = fqn.split(".") + for part in parts[:-1]: + if not hasattr(obj, part): + setattr(obj, part, _AttrDict()) + obj = getattr(obj, part) + setattr(obj, parts[-1], value) + + def _lms_to_attr_dict(val: Any) -> Any: + if isinstance(val, LeafModuleState): + target = _AttrDict() + for fqn, sentinel in val.named_parameters.items(): + _set_nested_attr(target, fqn, sentinel) + for fqn, sentinel in val.named_buffers.items(): + _set_nested_attr(target, fqn, sentinel) + return target + return val + + sig = inspect.signature(fn) + sentinels = list(range(num_flat_args)) + args_struct, kwargs_struct = pytree.tree_unflatten(sentinels, input_spec) + args_eval, kwargs_eval = pytree.tree_map( + _lms_to_attr_dict, + (args_struct, kwargs_struct), + is_leaf=lambda x: isinstance(x, LeafModuleState), + ) + namespace = dict(sig.bind(*args_eval, **kwargs_eval).arguments) + + indices: list[int] = [] + for expr in mutates_args: + # Empty __builtins__ prevents access to builtins like __import__, open, exec. + result = eval(expr, {"__builtins__": {}}, namespace) # noqa: S307 + leaves = pytree.tree_leaves(result) + for sentinel in leaves: + if not isinstance(sentinel, int): + raise ValueError( + f"mutates_args expression '{expr}' resolved to a non-leaf value " + f"of type {type(sentinel).__name__}. Expressions must resolve to " + f"individual tensor positions, e.g. 'model.weight' not 'model'." + ) + indices.append(sentinel) + indices.sort() + return ",".join(str(i) for i in indices) + + +@dataclass +class GradientInfo: + """ + We need the gradient edge to trigger the autograd engine backward. + + We need the tensor metadata (size, stride, dtype, device) to create zeros when + gradient is None at runtime but required by the backward graph (because the graph + is traced with fake implementation). + """ + + edge: torch.autograd.graph.GradientEdge + size: torch.Size + stride: tuple[int, ...] + dtype: torch.dtype + device: torch.device + + +def _retrieve_module_by_index(nn_module_index: int) -> torch.nn.Module: + # Check make_fx storage first (used by _invoke_leaf_function_python). + # Fall back to the Dynamo retriever (used by the compiled path). + if nn_module_index in _makefx_module_storage: + if nn_module_index >= 0: + raise RuntimeError( + f"Expected negative nn_module_index for non-strict trace over leaf_function, but got {nn_module_index}." + ) + return _makefx_module_storage[nn_module_index] + + if _leaf_function_module_retriever is None: + raise RuntimeError("Leaf function module retriever not set.") + + mod = _leaf_function_module_retriever(nn_module_index) + if not isinstance(mod, torch.nn.Module): + raise TypeError( + f"Expected nn.Module at index {nn_module_index} for leaf function invocation, " + f"but got {type(mod).__name__}." + ) + return mod + + +def check_escaped_gradients( + outputs: Any, + inputs: Sequence[Any], + requires_grad_indices: set[int], +) -> None: + """ + Check if autograd graph depends on tensors that not passed as explicit inputs. + + Controlled by torch._dynamo.config.leaf_function_check_escaped_gradients. + """ + if not requires_grad_indices: + return + + import torch._dynamo.config as config + + if not config.leaf_function_check_escaped_gradients: + return + + input_nodes: set[torch.autograd.graph.Node] = set() + for i, inp in enumerate(inputs): + if ( + isinstance(inp, torch.Tensor) + and i in requires_grad_indices + and inp.requires_grad + ): + edge = get_gradient_edge(inp) + if edge.node is not None: + input_nodes.add(edge.node) + + flat_outputs = outputs if isinstance(outputs, tuple) else (outputs,) + start_nodes: set[torch.autograd.graph.Node] = { + out.grad_fn + for out in flat_outputs + if isinstance(out, torch.Tensor) + and out.requires_grad + and out.grad_fn is not None + } + if not start_nodes: + return + + escaped: set[torch.autograd.graph.Node] = set() + visited: set[torch.autograd.graph.Node] = set() + stack = list(start_nodes) + + while stack: + node = stack.pop() + if node in visited or node in input_nodes: + continue + visited.add(node) + + for next_node, _ in node.next_functions: + if next_node is None or next_node in input_nodes: + continue + if not next_node.next_functions: + escaped.add(next_node) + else: + stack.append(next_node) + + if escaped: + tensor_info = [] + for node in escaped: + if hasattr(node, "variable"): + t = node.variable + tensor_info.append( + f" - Tensor(shape={list(t.shape)}, dtype={t.dtype})" + ) + tensor_details = ( + "\n".join(tensor_info) if tensor_info else " (tensor details unavailable)" + ) + + raise RuntimeError( + f"@leaf_function detected {len(escaped)} tensor(s) with requires_grad=True " + f"that are not passed as explicit inputs:\n{tensor_details}\n" + f"Gradients will not flow back to closure-captured or global tensors. " + f"Pass them as explicit arguments to the leaf function." + ) + + +@contextlib.contextmanager +def unflatten_args_with_modules( + flat_args: tuple[Any, ...], input_spec: pytree.TreeSpec +) -> Generator[tuple[list[Any] | tuple[Any, ...], dict[str, Any]], None, None]: + args, kwargs = pytree.tree_unflatten(flat_args, input_spec) + + with contextlib.ExitStack() as stack: + + def process_module_state(state: LeafModuleState) -> torch.nn.Module: + orig_module = _retrieve_module_by_index(state.nn_module_index) + stack.enter_context( + _reparametrize_module( + orig_module, + {**state.named_parameters, **state.named_buffers}, + ) + ) + return orig_module + + new_args, new_kwargs = pytree.tree_map_only( + LeafModuleState, + process_module_state, + (args, kwargs), + is_leaf=lambda x: isinstance(x, LeafModuleState), + ) + yield new_args, new_kwargs + + +def flatten_args_with_modules( + args_kwargs: tuple[Any, ...], +) -> list[Any]: + def expand_module(x: Any) -> Any: + if isinstance(x, torch.nn.Module): + return LeafModuleState( + nn_module_index=-1, + named_parameters=dict(x.named_parameters()), + named_buffers=dict(x.named_buffers()), + ) + return x + + expanded = pytree.tree_map(expand_module, args_kwargs) + return pytree.tree_leaves(expanded) + + +def make_leaf_function_wrappers( + real_fn: Callable[..., Any], + fake_fn: Callable[..., Any], + captured_out_spec: list[pytree.TreeSpec | None], +) -> tuple[Callable[..., tuple[Any, ...]], Callable[..., tuple[Any, ...]]]: + """Wrap real_fn and fake_fn to flatten outputs and capture the output TreeSpec. + + Both wrappers share the same captured output spec: the first call (typically + fake_fn during tracing) records it, and subsequent calls verify consistency. + The caller passes in a single-element list and reads captured_out_spec[0] + after the wrappers have been called. + + Used by both the Dynamo path (_call_leaf_function in torch.py) and the + make_fx path (_invoke_leaf_function_python in decorators.py). + """ + + def _wrap(fn: Callable[..., Any]) -> Callable[..., tuple[Any, ...]]: + if len(captured_out_spec) != 1: + raise RuntimeError( + f"captured_out_spec must be a single-element list, got length {len(captured_out_spec)}" + ) + + def wrapper(*args: Any, **kwargs: Any) -> tuple[Any, ...]: + out = fn(*args, **kwargs) + + flat_out, out_spec = pytree.tree_flatten(out) + if captured_out_spec[0] is None: + captured_out_spec[0] = out_spec + elif captured_out_spec[0] != out_spec: + raise AssertionError( + f"leaf_function output structure mismatch: " + f"expected {captured_out_spec[0]}, got {out_spec}. " + f"This can happen if the real function and fake function return " + f"different pytree structures (e.g., dict vs tuple, different number " + f"of elements). Ensure both functions return the same structure." + ) + return tuple(flat_out) + + return wrapper + + return _wrap(real_fn), _wrap(fake_fn) + + +def autograd_grad_with_gradient_info( + output_infos: Sequence[GradientInfo | None], + input_infos: Sequence[GradientInfo | None], + grad_outputs: Sequence[Any] | None = None, + retain_graph: bool | None = None, + create_graph: bool = False, + allow_unused: bool = False, +) -> tuple[torch.Tensor | None, ...]: + """ + Compute gradients using GradientInfo, it additionally handles the case + where input and output infos are None. + + Args: + output_infos: GradientInfo for each output (None if no grad_fn), + input_infos: GradientInfo for each input (None if not requires_grad) + grad_outputs: Gradients w.r.t. outputs + + Returns a tuple of gradients with None for inputs that don't require grad. + """ + filtered_output_edges = [] + filtered_grad_outputs = [] + for i, info in enumerate(output_infos): + if info is not None and info.edge.node is not None: + filtered_output_edges.append(info.edge) + if grad_outputs is not None: + filtered_grad_outputs.append(grad_outputs[i]) + + filtered_input_edges = [] + filtered_input_infos = [] + input_indices = [] + for i, info in enumerate(input_infos): + if info is not None: + filtered_input_edges.append(info.edge) + filtered_input_infos.append(info) + input_indices.append(i) + + if not filtered_output_edges or not filtered_input_edges: + return tuple(None for _ in input_infos) + + grads = torch.autograd.grad( + outputs=filtered_output_edges, + inputs=filtered_input_edges, + grad_outputs=filtered_grad_outputs if grad_outputs is not None else None, + retain_graph=retain_graph, + create_graph=create_graph, + allow_unused=allow_unused, + ) + + # Reconstruct full gradient tuple with Nones at proper positions. + # + # NB: For unused inputs that require grad, we return zeros instead of None. + # This is necessary because during AOT tracing, we use fake_impl to determine + # the backward graph structure. fake_impl may not accurately reflect which + # inputs are truly used for gradients in real_impl. For example, users often + # write fake_impl by returning torch.empty_like(input) to get the right shape + # without actually using the input in a differentiable computation. So we must + # be permissive and assume all inputs with requires_grad=True could need + # gradients. When multiple invoke_leaf_function calls share an input (e.g., + # a module's forward + hook both receiving the same tensor), the traced + # backward graph generates explicit add operations to accumulate their + # gradients. At runtime, if one leaf function doesn't actually use the input, + # autograd.grad returns None for it, and the traced add(grad1, grad2) fails + # because one operand is None. Returning zeros ensures the add always works. + result: list[torch.Tensor | None] = [None] * len(input_infos) + for filtered_idx, original_idx in enumerate(input_indices): + grad = grads[filtered_idx] + if grad is None: + info = filtered_input_infos[filtered_idx] + grad = torch.empty_strided( + info.size, info.stride, dtype=info.dtype, device=info.device + ).zero_() + result[original_idx] = grad + + return tuple(result) + + +def _make_forward( + fn: Callable, + include_keys: DispatchKeySet, + exclude_keys: DispatchKeySet, +) -> tuple[Callable, dict[str, Any]]: + state: dict[str, Any] = {"inputs": None, "outputs": None} + + @functools.wraps(fn) + def forward(*args, **kwargs): + effective_keys = include_keys + if include_keys.has(DispatchKey.PythonDispatcher): + effective_keys = effective_keys.remove(DispatchKey.PythonDispatcher) + if effective_keys.has(DispatchKey.Python): + effective_keys = effective_keys.remove(DispatchKey.Python) + with torch._C._ForceDispatchKeyGuard(effective_keys, exclude_keys): + with torch.enable_grad(): + outputs = fn(*args, **kwargs) + + flat_inputs = flatten_args_with_modules((args, kwargs)) + requires_grad_indices = { + i + for i, inp in enumerate(flat_inputs) + if isinstance(inp, torch.Tensor) and inp.requires_grad + } + check_escaped_gradients(outputs, flat_inputs, requires_grad_indices) + + state["inputs"] = tuple( + GradientInfo( + edge=get_gradient_edge(inp), + size=inp.size(), + stride=inp.stride(), + dtype=inp.dtype, + device=inp.device, + ) + if isinstance(inp, torch.Tensor) and inp.requires_grad + else None + for inp in flat_inputs + ) + + if outputs is None: + state["outputs"] = () + else: + state["outputs"] = tuple( + GradientInfo( + edge=get_gradient_edge(out), + size=out.size(), + stride=out.stride(), + dtype=out.dtype, + device=out.device, + ) + if isinstance(out, torch.Tensor) + and out.requires_grad + and out.grad_fn is not None + else None + for out in outputs + ) + + return pytree.tree_map_only( + torch.Tensor, + lambda t: t.detach().requires_grad_(t.requires_grad), + outputs, + ) + + return forward, state + + +class InvokeLeafFunction(HigherOrderOperator): + def __init__(self): + super().__init__("invoke_leaf_function", supports_training_input_mutation=True) + + def __call__( + self, + real_fn_callable, + fake_fn_callable, + input_spec, + mutated_arg_indices, + *flat_args, + requires_grad_indices="", + ): + """ + real_fn_callable: _LeafCallable wrapping the real function + fake_fn_callable: _LeafCallable wrapping the fake function + input_spec: pytree.TreeSpec for unflattening flat_args back to (args, kwargs) + mutated_arg_indices: comma-separated string of flat-arg indices that are + declared as mutated (e.g. "1,2"), or "" for no mutations. Encoded as a + string so it is a pytree leaf for the HOP schema infrastructure. + requires_grad_indices: comma-separated string of flat-arg indices that + require grad (e.g. "0,1"), or "" for none. + """ + return super().__call__( # type: ignore[attr-defined] + real_fn_callable, + fake_fn_callable, + input_spec, + mutated_arg_indices, + *flat_args, + requires_grad_indices=requires_grad_indices, + ) + + # pyrefly: ignore [bad-override] + def gen_schema( + self, + real_fn_callable, + fake_fn_callable, + input_spec, + mutated_arg_indices, + *flat_args, + requires_grad_indices="", + ): + from torch._higher_order_ops.schema import HopSchemaGenerator + from torch._higher_order_ops.utils import _maybe_fake_prop_ignore_unbacked + from torch.fx.experimental.proxy_tensor import disable_proxy_modes_tracing + + mutated_set = _parse_mutated_arg_indices(mutated_arg_indices) + + with disable_proxy_modes_tracing(): + if mutated_set: + schema_flat_args = tuple( + arg.detach().clone() + if isinstance(arg, torch.Tensor) and i in mutated_set + else arg + for i, arg in enumerate(flat_args) + ) + else: + schema_flat_args = flat_args + + def run_fake(*unfunc_flat_args): + with unflatten_args_with_modules(unfunc_flat_args, input_spec) as ( + args, + kwargs, + ): + return fake_fn_callable(*args, **kwargs) + + fake_outputs = _maybe_fake_prop_ignore_unbacked(run_fake, schema_flat_args) + + gen = HopSchemaGenerator(self) + gen.add_arg("real_fn_callable", real_fn_callable) + gen.add_arg("fake_fn_callable", fake_fn_callable) + gen.add_arg("input_spec", input_spec) + gen.add_arg("mutated_arg_indices", mutated_arg_indices) + for i, arg in enumerate(flat_args): + gen.add_arg(f"arg{i}", arg, is_mutated=i in mutated_set) + gen.add_arg( + "requires_grad_indices", + requires_grad_indices, + default_value="", + kw_only=True, + ) + + if isinstance(fake_outputs, tuple): + for out in fake_outputs: + gen.add_output(out) + else: + if fake_outputs is not None: + raise AssertionError( + f"Expected fake_outputs to be a tuple or None, got {type(fake_outputs)}" + ) + gen.add_output(fake_outputs) + + return gen.gen_schema() + + +invoke_leaf_function = InvokeLeafFunction() + + +# NOTE: [Autograd support for invoke_leaf_function] +# +# The overall idea is that when the real forward executes, we are going to build an autograd graph +# and save it. When the real backward executes, we are going to invoke the autograd graph. +# We need to build these "real_forward" and "real_backward" functions from the "real_fn". +# +# Inputs: +# real_fn_callable/fake_fn_callable are _LeafCallable objects that wrap real_fn and fake_fn. +# These functions were created in dynamo by wrapping the user's original leaf function and fake function: +# - They accept *flat_args (flattened LeafModuleState objects + other args) +# - They unflatten flat_args and convert LeafModuleState back to nn.Modules +# - They call the user's original leaf function with the reconstructed args +# - They return the user function's outputs +# +# We wrap real_fn (via _make_forward) to handle autograd properly: +# 1. Detach inputs and outputs to isolate the leaf function's autograd graph from the +# outer graph (gradients flow through our backward, not internal ops) +# 2. Real function is invoked at backend dispatch keys (i.e. CompositeExplicitAutograd) +# where autograd is disabled. We re-enable grad and restore dispatch keys for the autograd engine to work. +# 3. Store GradientInfo (gradient edges + tensor metadata) instead of full tensors for backward, +# which is sufficient for autograd.grad and avoids keeping tensors alive. +# +# We automatically generate backward functions: +# - real_backward uses the stored input/output GradientInfo and invokes the autograd engine +# - fake_backward creates empty gradients for inputs that requires grad + + +class InvokeLeafFunctionAutogradOp(torch.autograd.Function): + @staticmethod + # pyrefly: ignore [bad-override] + def forward( + ctx, + real_fn_callable, + fake_fn_callable, + input_spec, + mutated_arg_indices, + *flat_args, + ): + include_keys = torch._C._dispatch_tls_local_include_set() + exclude_keys = torch._C._dispatch_tls_local_exclude_set() + + requires_grad_indices = ",".join( + str(i) + for i, arg in enumerate(flat_args) + if isinstance(arg, torch.Tensor) and arg.requires_grad + ) + + real_forward, real_state = _make_forward( + real_fn_callable, include_keys, exclude_keys + ) + + def real_backward(*grads): + if real_state["inputs"] is None or real_state["outputs"] is None: + raise RuntimeError( + "invoke_leaf_function backward expects inputs/outputs to be set in forward." + ) + return autograd_grad_with_gradient_info( + output_infos=real_state["outputs"], + input_infos=real_state["inputs"], + grad_outputs=grads, + allow_unused=True, + ) + + input_infos_for_fake = tuple( + GradientInfo( + edge=None, # type: ignore[arg-type] + size=arg.size(), + stride=arg.stride(), + dtype=arg.dtype, + device=arg.device, + ) + if isinstance(arg, torch.Tensor) and arg.requires_grad + else None + for arg in flat_args + ) + + def fake_backward(*grads): + return tuple( + torch.empty_strided( + info.size, info.stride, dtype=info.dtype, device=info.device + ) + if info is not None + else None + for info in input_infos_for_fake + ) + + new_real_fn_callable = _LeafCallable(real_forward) + + with torch._C._AutoDispatchBelowAutograd(): + fw_outputs = invoke_leaf_function( + new_real_fn_callable, + fake_fn_callable, + input_spec, + mutated_arg_indices, + *flat_args, + requires_grad_indices=requires_grad_indices, + ) + + ctx.real_backward = real_backward + ctx.fake_backward = fake_backward + + return fw_outputs + + @staticmethod + # pyrefly: ignore [bad-override] + def backward(ctx, *grads): + real_bw_callable = _LeafCallable(ctx.real_backward) + fake_bw_callable = _LeafCallable(ctx.fake_backward) + _, bw_input_spec = pytree.tree_flatten((grads, {})) + fw_grads = invoke_leaf_function( + real_bw_callable, fake_bw_callable, bw_input_spec, "", *grads + ) + return None, None, None, None, *fw_grads + + +@invoke_leaf_function.py_autograd_impl +def invoke_leaf_function_autograd( + real_fn_callable, + fake_fn_callable, + input_spec, + mutated_arg_indices, + *flat_args, + requires_grad_indices="", +): + return InvokeLeafFunctionAutogradOp.apply( + real_fn_callable, fake_fn_callable, input_spec, mutated_arg_indices, *flat_args + ) + + +# TODO: aliasing is not allowed +@invoke_leaf_function.py_functionalize_impl +def invoke_leaf_function_functionalization(ctx, *all_args, **kwargs): + from torch._higher_order_ops.auto_functionalize import ( + can_auto_functionalize, + do_auto_functionalize_v2, + ) + from torch._higher_order_ops.utils import HopInstance + + unwrapped_args = ctx.unwrap_tensors(all_args) + hop_instance = HopInstance.create(invoke_leaf_function, *unwrapped_args, **kwargs) + if can_auto_functionalize(hop_instance): + return do_auto_functionalize_v2(ctx.mode, hop_instance, all_args, kwargs) + + from torch._higher_order_ops.effects import handle_effects + + return handle_effects( + ctx.mode._allow_token_discovery, + ctx.mode._tokens, + invoke_leaf_function, + all_args, + kwargs, + ) + + +@invoke_leaf_function.py_impl(ProxyTorchDispatchMode) +def invoke_leaf_function_proxy_mode(proxy_mode, *all_args, **kwargs): + out = invoke_leaf_function(*all_args, **kwargs) + proxies = pytree.tree_map(proxy_mode.tracer.unwrap_proxy, all_args) + proxy = proxy_mode.tracer.create_proxy( + "call_function", invoke_leaf_function, proxies, kwargs + ) + return track_tensor_tree(out, proxy, constant=None, tracer=proxy_mode.tracer) + + +def _validate_outputs_match( + fake_output: Any, + real_output: Any, +) -> None: + """ + Validate that fake_fn and real_fn outputs have matching pytree structure, + shapes, and dtypes. + + Raises: + RuntimeError: If outputs don't match with detailed error message. + """ + fake_flat, fake_spec = pytree.tree_flatten(fake_output) + real_flat, real_spec = pytree.tree_flatten(real_output) + + if fake_spec != real_spec: + raise RuntimeError( + f"Output structure mismatch in @leaf_function decorator.\n" + f"fake_impl returned structure: {fake_spec}\n" + f"real_impl returned structure: {real_spec}\n" + f"The fake_impl must return outputs with the same pytree structure as real_impl." + ) + + if len(fake_flat) != len(real_flat): + raise RuntimeError( + f"Output count mismatch in @leaf_function decorator.\n" + f"fake_impl returned {len(fake_flat)} values\n" + f"real_impl returned {len(real_flat)} values" + ) + + for i, (fake_val, real_val) in enumerate(zip(fake_flat, real_flat)): + fake_is_tensor = isinstance(fake_val, torch.Tensor) + real_is_tensor = isinstance(real_val, torch.Tensor) + + if fake_is_tensor != real_is_tensor: + raise RuntimeError( + f"Output type mismatch at position {i} in @leaf_function decorator.\n" + f"fake_impl returned: {type(fake_val).__name__}\n" + f"real_impl returned: {type(real_val).__name__}" + ) + + if fake_is_tensor: + if fake_val.shape != real_val.shape: + raise RuntimeError( + f"Shape mismatch at output position {i} in @leaf_function decorator.\n" + f"fake_impl output shape: {list(fake_val.shape)}\n" + f"real_impl output shape: {list(real_val.shape)}\n" + f"The fake_impl must produce tensors with the same shapes as real_impl." + ) + + if fake_val.dtype != real_val.dtype: + raise RuntimeError( + f"Dtype mismatch at output position {i} in @leaf_function decorator.\n" + f"fake_impl output dtype: {fake_val.dtype}\n" + f"real_impl output dtype: {real_val.dtype}\n" + f"The fake_impl must produce tensors with the same dtypes as real_impl." + ) + + +def _parse_mutated_arg_indices(s: str) -> set[int]: + return {int(x) for x in s.split(",") if x} + + +def _check_no_input_mutation( + flat_args: tuple[Any, ...], + version_before: list[int], + mutated_arg_indices: str = "", +) -> None: + mutated_set = _parse_mutated_arg_indices(mutated_arg_indices) + for i, arg in enumerate(flat_args): + if isinstance(arg, torch.Tensor) and arg._version != version_before[i]: + if i not in mutated_set: + raise RuntimeError( + f"Undeclared in-place mutation on input tensor at position {i}. " + f"Declare it in @leaf_function(mutates_args=...) or avoid mutating inputs." + ) + + +@register_fake(invoke_leaf_function) +def invoke_leaf_function_fake( + real_fn_callable, + fake_fn_callable, + input_spec, + mutated_arg_indices, + *flat_args, + requires_grad_indices="", +): + with unflatten_args_with_modules(flat_args, input_spec) as (args, kwargs): + return fake_fn_callable(*args, **kwargs) + + +@invoke_leaf_function.py_impl(DispatchKey.CompositeExplicitAutograd) +def invoke_leaf_function_dense( + real_fn_callable, + fake_fn_callable, + input_spec, + mutated_arg_indices, + *flat_args, + requires_grad_indices="", +): + from torch._dynamo import config as dynamo_config + + version_before = [ + arg._version if isinstance(arg, torch.Tensor) else 0 for arg in flat_args + ] + + flat_args = tuple( + arg.detach() if isinstance(arg, torch.Tensor) else arg for arg in flat_args + ) + requires_grad_indices_set = _parse_mutated_arg_indices(requires_grad_indices) + flat_args = tuple( + arg.requires_grad_(True) if idx in requires_grad_indices_set else arg + for idx, arg in enumerate(flat_args) + ) + + with unflatten_args_with_modules(flat_args, input_spec) as (args, kwargs): + real_output = real_fn_callable(*args, **kwargs) + + _check_no_input_mutation(flat_args, version_before, mutated_arg_indices) + + if dynamo_config.leaf_function_validate_outputs: + fake_output = fake_fn_callable(*args, **kwargs) + _validate_outputs_match(fake_output, real_output) + + return real_output diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/invoke_subgraph.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/invoke_subgraph.py new file mode 100644 index 0000000000000000000000000000000000000000..6a79caeb6315a28827ddf5f015419351119e8cf0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/invoke_subgraph.py @@ -0,0 +1,1070 @@ +# mypy: allow-untyped-defs + +import contextlib +import copy +import functools +from collections import defaultdict +from collections.abc import Callable +from contextlib import nullcontext +from dataclasses import dataclass, field +from typing import Any + +import torch +import torch.utils._pytree as pytree +from torch._C import DispatchKey +from torch._dispatch.python import suspend_functionalization +from torch._higher_order_ops.utils import ( + _from_fun, + _maybe_reenter_make_fx, + clone_outputs_aliasing_inputs, + FunctionalizeCtxWrapper, + get_dummy_aot_autograd_config, + HopInstance, + prepare_fw_with_masks, + redirect_to_mode, + reenter_make_fx, + register_fake, + save_values_for_backward, + saved_values, +) +from torch._library.fake_class_registry import FakeScriptObject +from torch._library.opaque_object import is_opaque_type +from torch._ops import HigherOrderOperator +from torch._subclasses.functional_tensor import disable_functional_mode +from torch.fx.experimental.proxy_tensor import ( + disable_proxy_modes_tracing, + ProxyTorchDispatchMode, + track_tensor_tree, +) +from torch.fx.graph_module import GraphModule +from torch.fx.passes.runtime_assert import insert_deferred_runtime_asserts +from torch.utils._debug_mode import DebugMode +from torch.utils.checkpoint import _CachedTorchDispatchMode, _CachingTorchDispatchMode + + +invoke_subgraph_counter = 0 + + +# During the tracing of the joint graph, we construct this information. This is +# used to filter out grad_outs/tangents in the `backward` method of +# InvokeSubgraphAutogradOp. +@dataclass +class OutputMetadata: + num_fw_outs: int | None = None + indexes_with_symint: set[int] = field(default_factory=set) + indexes_with_no_grad: set[int] = field(default_factory=set) + + +# This config will be stored in invoke_subgraph HOP node.meta["custom"]["nested_region_config"] +# as well as the subgraph's gm.meta["nested_region_config"]. +@dataclass +class NestedCompileRegionOptions: + # A Callable that takes (gm, example_inputs, decompositions=None, **kwargs) as inputs. + # Returns AOTCompiledArtifact + fw_compiler: Callable | None = None + bw_compiler: Callable | None = None + + # Note: [InvokeSubgraphHOP Partitioner] + # If not None, add "partitioner" to HOP node meta. + # If Callable, directly assign the callable, but the callable cannot be pickled + # If str, the options are "default_partition" and "min_cut_rematerialization_partition". + # The HOP joint graph will be partitioned using the corresponding functions in + # torch/_functorch/partitioners.py + partitioner: Callable | str | None = None + + # If it's None, we'll inherit the parent call's decompositions. + # Otherwise, the nested region will use this decompositions. + decompositions: dict[str, Any] | None = None + + +def _extract_nested_region_config(fn): + """ + Extract the NestedCompileRegionOptions from the HOP subgraph gm.meta["nested_region_config"] + """ + gm_to_compile = None + if isinstance(fn, torch.fx.GraphModule): + gm_to_compile = fn + elif isinstance(fn, FunctionalizeCtxWrapper): + gm_to_compile = fn.subgraph + + if ( + isinstance(gm_to_compile, torch.fx.GraphModule) + and hasattr(gm_to_compile, "meta") + and "nested_region_config" in gm_to_compile.meta + ): + if isinstance( + gm_to_compile.meta["nested_region_config"], NestedCompileRegionOptions + ): + return gm_to_compile.meta["nested_region_config"].decompositions + return None + + +class InvokeSubgraphHOP(HigherOrderOperator): + def __init__(self) -> None: + # Invoke subgraph does not have any state, it is just a wrapper over a + # subgraph, so we can safely cache the HOP. + super().__init__("invoke_subgraph", cacheable=True) + # This is used by the fake tensor cache key validator to extract the + # subgraph and iterate over the nodes to find if all nodes are fake + # tensor cacheable. + self.subgraph_indexes = [ + 0, + ] + + # identifier is setup by upper part of the stack. This helps us in + # identifying two invoke_subgraph calls have same subgraph. + def __call__( + self, + subgraph: GraphModule | FunctionalizeCtxWrapper, + identifier: str | None, + *operands, + ): + if identifier is not None and not isinstance(identifier, str): + raise AssertionError( + f"identifier must be None or a string, got {type(identifier)}" + ) + + if not all( + isinstance( + o, (torch.Tensor, int, torch.SymInt, torch.Generator, FakeScriptObject) + ) + or is_opaque_type(type(o)) + for o in operands + if o is not None + ): + raise AssertionError( + f"invoke_subgraph operands must be a list of tensors/ints/SymInts/Generator, got {operands}" + ) + + # pyrefly: ignore [missing-attribute] + return super().__call__(subgraph, identifier, *operands) + + # pyrefly: ignore [bad-override] + def gen_schema(self, subgraph, identifier, *operands): + from torch._higher_order_ops.schema import HopSchemaGenerator + from torch._higher_order_ops.utils import ( + check_input_alias_and_mutation_return_outputs, + materialize_as_graph, + ) + + subgraph_decomp_table = _extract_nested_region_config(subgraph) + gm: torch.fx.GraphModule = materialize_as_graph( + subgraph, operands, subgraph_decomp_table=subgraph_decomp_table + ) + + schema_gen = HopSchemaGenerator(self) + schema_gen.add_arg("subgraph", gm) + schema_gen.add_arg("identifier", identifier) + ( + _, + _, + _, + mutated_inputs, + outputs, + ) = check_input_alias_and_mutation_return_outputs(gm) + for idx, arg in enumerate(operands): + schema_gen.add_arg(f"arg{idx}", arg, is_mutated=idx in mutated_inputs) + for out in outputs: + schema_gen.add_output(out) + + return schema_gen.gen_schema() + + +invoke_subgraph = InvokeSubgraphHOP() + + +def invoke_subgraph_infer( + subgraph: GraphModule | FunctionalizeCtxWrapper, + *operands, +): + """Inference-only entrypoint for invoke_subgraph that auto-generates identifier. + + This is intended for use cases where we are building an inference graph and + don't need the forward/backward caching that requires a stable identifier. + The identifier is automatically computed based on the current proxy mode's + tracer state. + + If no proxy mode is active, the subgraph is called directly. + """ + from torch.fx.experimental.proxy_tensor import get_proxy_mode + + proxy_mode = get_proxy_mode() + if proxy_mode is None: + # No tracing active, just call the subgraph directly + if getattr(subgraph, "_boxed_call", False): + return subgraph(list(operands)) + else: + return subgraph(*operands) + + from torch._dynamo.utils import get_unique_name_wrt + + # How exactly should we allocate names for the HOP invoke_subgraph we + # are going to put into the graph? This is a bit tricky. In the + # original design of invoke_subgraph, this HOP never shows up in the + # wild: it is only generated Dynamo, so Dynamo can take sure of + # ensuring it picks unique names in the context of the particular + # Dynamo compilation. However, these invoke_subgraph are different: + # they live as Dynamo compiled code that can potentially get traced + # multiple times! If they get retraced several times in the same + # trace, deduplication occurs; but if I make_fx a function f once, + # and then do a separate new trace, there's no relationship between + # these. Additionally, we also want the name we put in the graph to + # be deterministic, and for it to be indifferent to how many + # unrelated invoke_subgraphs/make_fxs we've done, prior to THIS + # particular make_fx. + # + # To satisfy all of these constraints, it's impossible to preallocate + # a name before tracing actually goes through us (since those names + # would have to all be unique even if a subgraph never gets used.) + # So we allocate the subgraph a fresh name PER proxy mode, and then + # consistently reuse it if it hits again. + # + # Note we do NOT do equality comparison subgraph, since it has + # reference equality semantics. + + if subgraph in proxy_mode._invoke_subgraph_cache: + name = proxy_mode._invoke_subgraph_cache[subgraph] + else: + name = get_unique_name_wrt( + "invoke_subgraph", + proxy_mode._invoke_subgraph_names, + requires_suffix=True, + ) + proxy_mode._invoke_subgraph_names.add(name) + proxy_mode._invoke_subgraph_cache[subgraph] = name + + return invoke_subgraph(subgraph, name, *operands) + + +# Registers dispatches for SAC +redirect_to_mode(invoke_subgraph, _CachingTorchDispatchMode) +redirect_to_mode(invoke_subgraph, _CachedTorchDispatchMode) + + +def invoke_subgraph_placeholder(func, *args, **kwargs): + if torch.compiler.is_dynamo_compiling(): + # This is just a placeholder for Dynamo to replace with invoke_subgraph + raise RuntimeError("invoke_subgraph should not be called directly in Dynamo") + + if torch.compiler.is_compiling(): + # For non-strict export tracing, we still want to go through Dynamo + + def _invoke_subgraph_placeholder_wrapper(func, args): + return invoke_subgraph_placeholder(func, *args) + + from torch._higher_order_ops.utils import _hop_compile_and_call + + return _hop_compile_and_call(_invoke_subgraph_placeholder_wrapper, (func, args)) + + return func(*args, **kwargs) + + +def mark_compile_region( + fn=None, + options: NestedCompileRegionOptions | None = None, + max_reuse_entries: int = 8, + reuse_hash_fn=None, +): + """ + This wrapper instructs torch.compile to compile the wrapped region once and + reuse the compiled artifact, instead of the usual way of aggressively + inlining the function. + + Under the hood, it tells TorchDynamo to use InvokeSubgraph HOP for the + region. For PyTorch eager, this is a no-op. + + Args: + fn: The function to wrap + options: Optional config to use for compiling the subgraph. + Warning: this is an experimental feature under development and + not ready for use yet. + max_reuse_entries: Maximum number of reuse cache entries per function + before raising an error. If this limit is hit, guards keep failing + across invocations and hierarchical compilation is not effective. + """ + + def wrap(func): + def inner(*args, **kwargs): + # Get the innermost function to avoid nested compile regions + inner_func = func + while hasattr(inner_func, "__marked_compile_region_fn__"): + inner_func = inner_func.__marked_compile_region_fn__ + return invoke_subgraph_placeholder(inner_func, *args, **kwargs) + + inner.__marked_compile_region_fn__ = func # type: ignore[attr-defined] + func.__marked_compile_region_config__ = options # type: ignore[attr-defined] + func.__marked_compile_region_max_reuse_entries__ = max_reuse_entries # type: ignore[attr-defined] + func.__marked_compile_region_reuse_hash_fn__ = reuse_hash_fn # type: ignore[attr-defined] + + return inner + + if fn: + return wrap(fn) + else: + return wrap + + +def get_invoke_subgraph_cache(): + cache = None + if tracing_ctx := torch._guards.TracingContext.try_get(): + cache = tracing_ctx.hop_dispatch_set_cache.get_cache(invoke_subgraph) + return cache + + +# TODO (@anijain2305) - Delete this function when base_hop uses invoke_subgraph infra +def trace_joint_graph(fn, fw_inputs, fw_outputs): + """ + Naively trace out a joint graph. This simplifies the reconstruction of joint + graph in the min-cut partitioner later on. + """ + from torch._functorch.aot_autograd import create_joint + + dummy_aot_config = get_dummy_aot_autograd_config() + + # This joint_fn is inserted as the backward graph as is. This simplifies the + # min-cut partitioner work later on. + # Input signature - (*primals, *tangents) + # Output signature - (*grads, *fw_outs) + # The output signature is deliberately kept grads first and fw_outs second. + # Having grads first makes the min-cut partitioner HOP graph stitching + # easier. + def joint_fn(*primals_and_tangents): + primals = primals_and_tangents[: len(fw_inputs)] + tangents = primals_and_tangents[len(fw_inputs) :] + + fw_outs, grads = create_joint( + prepare_fw_with_masks(fn), aot_config=dummy_aot_config + )(primals, tangents) + + maybe_clone = clone_outputs_aliasing_inputs(primals_and_tangents) + + # return signature is deliberately kept (*grads, *fw_outs). This + # simplifies partitioning work later on. + return pytree.tree_map(maybe_clone, tuple(grads + list(fw_outs))) + + primals = list(fw_inputs) + # This assumes that the tangent strides match fw_outputs strides. Check the + # InvokeSubgraphAutogradOp backward op for the contiguous call. + tangents = [_from_fun(out) for out in fw_outputs] + + joint_operands = primals + tangents + + return _maybe_reenter_make_fx(joint_fn)(*joint_operands) + + +# TODO (@anijain2305) - Delete this function when base_hop uses invoke_subgraph infra +def create_fw_bw_graph(subgraph, operands, grad_outputs=None): + with suspend_functionalization(), disable_functional_mode(): + with disable_proxy_modes_tracing(): + # args are functional tensors, generate some example tensors + fw_inputs = pytree.tree_map(_from_fun, operands) + + from torch._guards import detect_fake_mode + + fake_mode = detect_fake_mode(fw_inputs) + context = ( + nullcontext() + if fake_mode is None or fake_mode.shape_env is None + else fake_mode.shape_env.ignore_fresh_unbacked_symbols() + ) + + with context: + fw_outs = pytree.tree_map(_from_fun, subgraph(*fw_inputs)) + + num_fw_outs = len(fw_outs) + + # Collect the indexes of none in the output to check that the grad + # is None at the corresponding index in the backward. This check is + # performed in the autograd.Function - InvokeSubgraphAutogradOp. + # Also collect the indexes of no_grad in the output to filter out + # the grad_outs in the `backward` method. + output_metadata = OutputMetadata() + + output_metadata.num_fw_outs = num_fw_outs + for idx, fw_out in enumerate(fw_outs): + if isinstance(fw_out, torch.SymInt): + output_metadata.indexes_with_symint.add(idx) + elif not fw_out.requires_grad: + output_metadata.indexes_with_no_grad.add(idx) + + if grad_outputs is None: + # Infer grad_outputs to be the same properties as the fw_outputs + # if they're not passed in + # Although fw_outs are equivalent to grad_outputs for tracing + # purposes, we have to carefully handle the None and fw_out that do + # not have require_grad. At those indexes, we will have None in the + # backward graph. + grad_outputs = fw_outs + grad_outputs = [grad for grad in grad_outputs if grad is not None] + grad_outputs = [grad for grad in grad_outputs if grad.requires_grad] + + # Force grad_out to be contiguous. This is because at runtime, + # grad_out could have different strides than fw_outs. So, we + # force the grad_outs to be contiguous for both tracing and + # runtime. + grad_outputs = [grad.contiguous() for grad in grad_outputs] + + if any( + not isinstance(out, torch.Tensor) + for out in grad_outputs + if out is not None + ): + raise RuntimeError( + "Expect outputs of invoke_subgraph to only contains tensors or None. " + f"Got types {[type(out) for out in grad_outputs]}." + ) + + # Trace the forward subgraph + fw_graph = _maybe_reenter_make_fx(subgraph)(*fw_inputs) + + # Trace the joint graph and assign it to the bwd graph + bw_graph = trace_joint_graph( + subgraph, + fw_inputs, + grad_outputs, + ) + return fw_graph, bw_graph, output_metadata + + +def get_output_metadata(subgraph, *operands): + """ + Extract metadata about the subgraph outputs WITHOUT executing the subgraph. + This avoids running side-effectful operations twice (once here, once in forward). + We analyze the graph structure statically to extract metadata. + """ + # Unwrap FunctionalizeCtxWrapper if present + if isinstance(subgraph, FunctionalizeCtxWrapper): + subgraph = subgraph.subgraph + + # If not a GraphModule, fall back to execution-based metadata extraction + if not isinstance(subgraph, torch.fx.GraphModule): + return _get_output_metadata_by_execution(subgraph, *operands) + + output_metadata = OutputMetadata() + + # Extract output arguments from the output node + # The output node has args=(output_values,) where output_values is a tuple/list + output_node = next(reversed(subgraph.graph.find_nodes(op="output"))) + output_metadata.num_fw_outs = len(output_node.args[0]) + + for idx, output_arg in enumerate(output_node.args[0]): + if not isinstance(output_arg, torch.fx.Node): + if isinstance(output_arg, int): + output_metadata.indexes_with_symint.add(idx) + output_metadata.indexes_with_no_grad.add(idx) + continue + + # Check node metadata for type information + if output_arg.meta.get("val") is None: + # If we don't have complete metadata for all outputs, fall back to execution + # This is important for correctness (e.g., detecting SymInts) even though it + # runs side-effectful operations + return _get_output_metadata_by_execution(subgraph, *operands) + + val = output_arg.meta["val"] + if isinstance(val, torch.SymInt): + output_metadata.indexes_with_symint.add(idx) + output_metadata.indexes_with_no_grad.add(idx) + elif isinstance(val, torch.Tensor): + # Check if tensor requires grad from metadata + if hasattr(val, "requires_grad") and not val.requires_grad: + output_metadata.indexes_with_no_grad.add(idx) + else: + # Non-tensor, non-symint (shouldn't happen but be safe) + output_metadata.indexes_with_no_grad.add(idx) + + return output_metadata + + +def _get_output_metadata_by_execution(subgraph, *operands): + """ + Fallback: Extract metadata by executing the subgraph. + This should only be used when static analysis fails. + WARNING: This will run side-effectful operations! + """ + + with suspend_functionalization(), disable_functional_mode(): + with disable_proxy_modes_tracing(): + # args are functional tensors, generate some example tensors + fw_inputs = pytree.tree_map(_from_fun, operands) + + from torch._guards import detect_fake_mode + + fake_mode = detect_fake_mode(fw_inputs) + context = ( + nullcontext() + if fake_mode is None or fake_mode.shape_env is None + else fake_mode.shape_env.ignore_fresh_unbacked_symbols() + ) + + with context: + fw_outs = pytree.tree_map(_from_fun, subgraph(*fw_inputs)) + + num_fw_outs = len(fw_outs) + + output_metadata = OutputMetadata() + output_metadata.num_fw_outs = num_fw_outs + + for idx, fw_out in enumerate(fw_outs): + if isinstance(fw_out, torch.SymInt): + output_metadata.indexes_with_symint.add(idx) + elif not fw_out.requires_grad: + output_metadata.indexes_with_no_grad.add(idx) + + return output_metadata + + +def trace_joint_graph_as_bwd( + subgraph, num_primals, joint_operands, include_key_set, exclude_key_set +): + """ + Naively trace out a joint graph. This simplifies the reconstruction of joint + graph in the min-cut partitioner later on. + """ + from torch._functorch.aot_autograd import create_joint + + dummy_aot_config = get_dummy_aot_autograd_config() + + if isinstance(subgraph, torch.fx.GraphModule): + + def graph_with_interpreter(*args): + # Running graph with interpreter is needed for propagating the stack_trace + with torch.fx.traceback.preserve_node_meta(): + return torch.fx.Interpreter(subgraph).run(*args) + + fn = graph_with_interpreter + else: + fn = subgraph + + # This joint_fn is inserted as the backward graph as is. This simplifies the + # min-cut partitioner work later on. + # Input signature - (*primals, *tangents) + # Output signature - (*grads, *fw_outs) + # The output signature is deliberately kept grads first and fw_outs second. + # Having grads first makes the min-cut partitioner HOP graph stitching + # easier. + def joint_fn(*primals_and_tangents): + primals = primals_and_tangents[:num_primals] + tangents = primals_and_tangents[num_primals:] + + fw_outs, grads = create_joint( + prepare_fw_with_masks(fn), aot_config=dummy_aot_config + )(primals, tangents) + + maybe_clone = clone_outputs_aliasing_inputs(primals_and_tangents) + + # return signature is deliberately kept (*grads, *fw_outs). This + # simplifies partitioning work later on. + return pytree.tree_map(maybe_clone, tuple(grads + list(fw_outs))) + + with suspend_functionalization(), disable_functional_mode(): + with disable_proxy_modes_tracing(): + joint_operands = [_from_fun(arg) for arg in joint_operands] + with contextlib.ExitStack() as stack: + stack.enter_context( + torch._C._ForceDispatchKeyGuard(include_key_set, exclude_key_set), + ) + subgraph_decomp_table = _extract_nested_region_config(subgraph) + with torch.enable_grad(): + return _maybe_reenter_make_fx( + joint_fn, subgraph_decomp_table=subgraph_decomp_table + )(*joint_operands) + + +class InvokeSubgraphAutogradOp(torch.autograd.Function): + """ + Saves the subgraph, i.e. original callable, in the forward method. And then + traces out a joint graph in the backward. This delaying of tracing in + backward, also called as lazy backward, ensures that the assumptions about + the grad_out strides and tensor-subclass-ness are already accounted for. + """ + + @staticmethod + # pyrefly: ignore [bad-override] + def forward( + ctx, + subgraph, + identifier, + output_metadata, + *operands, + ): + # We want to delay the backward graph construction until the backward. + # So in forward, we just run the fw callable as is. And save all the + # information necessary to construct the backward graph in the ctx. + ctx._subgraph = subgraph + ctx._identifier = identifier + ctx._output_metadata = output_metadata + # We snapshot the dispatch keys in forward for materializing the + # the bw_graph in backward. + ctx._fw_include_key_set = torch._C._dispatch_tls_local_include_set() + ctx._fw_exclude_key_set = torch._C._dispatch_tls_local_exclude_set() + + save_values_for_backward(ctx, operands) + + with torch._C._AutoDispatchBelowAutograd(): + out = invoke_subgraph( + subgraph, + f"fw_{identifier}", + *operands, + ) + + # Check that int (coming from symint) is at expected indexes. + for idx, o in enumerate(out): + if isinstance(o, int): + if idx not in output_metadata.indexes_with_symint: + raise AssertionError( + f"unexpected int output at index {idx}, not in indexes_with_symint" + ) + + return out + + @staticmethod + def backward( + ctx, + *grad_outs, + ): + from torch._dynamo.utils import dynamo_timed + + subgraph = ctx._subgraph + identifier = ctx._identifier + output_metadata = ctx._output_metadata + primals = saved_values(ctx) + + # Filter out grads that are None or do not require_grad. This was + # the assumption we made during the tracing of joint_graph. + filtered_grad_outs = [] + for idx, o in enumerate(grad_outs): + if o is None: + if idx not in output_metadata.indexes_with_symint: + raise AssertionError( + f"unexpected None grad_out at index {idx}, not in indexes_with_symint" + ) + elif idx in output_metadata.indexes_with_no_grad: + # Deliberately skip over the grad_outs which we know should be + # None because the corresponding fwd_out does not require_grad. + pass + else: + filtered_grad_outs.append(o) + filtered_grad_outs = tuple(filtered_grad_outs) + + # Important note - Even though the forward graph can be same for + # different invoke_subgraphs, the backward graph can be different + # because the tangent strides can be different. So, here we cache on + # tangent_metadata in addition to identifier + from torch._guards import detect_fake_mode + from torch._subclasses._fake_tensor_utils import _CacheKeyState + from torch._subclasses.fake_tensor import extract_tensor_metadata + + fake_mode = detect_fake_mode(primals + filtered_grad_outs) + if fake_mode is None: + raise AssertionError("fake_mode should be enabled for HOPs") + state = _CacheKeyState(fake_mode.shape_env) + + tangent_metadata: list[object] = [] + for tangent in filtered_grad_outs: + metadata = extract_tensor_metadata(tangent) + metadata._flatten_into(tangent_metadata, fake_mode, state) + + # Add aliasing information to tangent_metadata + # Two tangents are aliased if they are the same tensor object (using id()) + # We create a tuple of tuples where each inner tuple contains indices of aliased tensors + # e.g. ((0, 1),) would mean there is one aliasing group, and the first and second tangents are aliased + # e.g. () would mean there is no aliasing between tangents + tensor_to_indices: dict[int, list[int]] = defaultdict(list) + for i, tangent in enumerate(filtered_grad_outs): + if isinstance(tangent, torch.Tensor): + tensor_to_indices[id(tangent)].append(i) + + aliasing_groups = tuple( + sorted( + tuple(indices) + for indices in tensor_to_indices.values() + if len(indices) > 1 + ) + ) + tangent_metadata.append(aliasing_groups) + + # pyrefly: ignore [bad-assignment] + tangent_metadata = tuple(tangent_metadata) + + # bw_graph is a joint graph with signature (*primals_and_tangents) and + # returns (*grads_and_fw_outs). To get the grads, we use the num_fw_outs + # to extract the grads. + primals_and_tangents = primals + filtered_grad_outs + + # Check if we have already traced the bwd subgraph. + bw_graph = None + suffix = None + invoke_subgraph_cache = get_invoke_subgraph_cache() + cache_hit = False + if invoke_subgraph_cache: + bw_graph, suffix = invoke_subgraph_cache.get_lazy_bwd_entry( + identifier, tangent_metadata + ) + cache_hit = bw_graph is not None + + if bw_graph is None: + if suffix is not None: + raise AssertionError( + f"suffix should be None when bw_graph is None, got {suffix}" + ) + with dynamo_timed( + "invoke_subgraph_trace_joint_graph", log_pt2_compile_event=True + ): + bw_graph = trace_joint_graph_as_bwd( + subgraph, + len(primals), + primals_and_tangents, + ctx._fw_include_key_set, + ctx._fw_exclude_key_set, + ) + if ( + hasattr(subgraph, "meta") + and "nested_region_config" in subgraph.meta + ): + bw_graph.meta["nested_region_config"] = subgraph.meta[ + "nested_region_config" + ] + + if invoke_subgraph_cache and not cache_hit: + suffix = invoke_subgraph_cache.add_lazy_bwd_entry( + identifier, tangent_metadata, bw_graph + ) + + grads = invoke_subgraph( + bw_graph, f"bw_{identifier}_{suffix}", *primals_and_tangents + )[: -output_metadata.num_fw_outs] + return None, None, None, *grads + + +@invoke_subgraph.py_autograd_impl +def _(subgraph, identifier, *operands): + # Check if we have already traced the subgraph. + invoke_subgraph_cache = get_invoke_subgraph_cache() + if invoke_subgraph_cache: + if saved_autograd_fn := invoke_subgraph_cache.get_autograd_key_entry( + identifier + ): + return saved_autograd_fn(*operands) + + output_metadata = get_output_metadata(subgraph, *operands) + + def autograd_fn_callable(*args): + return InvokeSubgraphAutogradOp.apply( + subgraph, identifier, output_metadata, *args + ) + + # Save the autograd_fn_callable in the dispatch set cache. + if invoke_subgraph_cache: + invoke_subgraph_cache.add_autograd_key_entry(identifier, autograd_fn_callable) + + return autograd_fn_callable(*operands) + + +@invoke_subgraph.py_impl(DebugMode) +def _(debug_mode, subgraph, identifier, *operands): + # record HOP call + call = torch.utils._debug_mode._OpCall( + invoke_subgraph, + (identifier, *operands), + kwargs={}, + call_depth=debug_mode.call_depth + 1, + stack=debug_mode.record_stack_trace, + ) + debug_mode._record_call(call) + + debug_mode.call_depth += 1 + debug_mode._handle_annotate(f"[enter InvokeSubgraph HOP] {identifier}") + + # If the HOP is dispatched from DebugMode, we should enable debug_mode + # for the subgraph call. + with debug_mode: + if getattr(subgraph, "_boxed_call", False): + result = subgraph(list(operands)) + else: + result = subgraph(*operands) + debug_mode._handle_annotate(f"[exit InvokeSubgraph HOP] {identifier}") + debug_mode.call_depth -= 1 + # record output of HOP + debug_mode._record_call_output(call, result) + return result + + +@invoke_subgraph.py_impl(DispatchKey.CompositeExplicitAutograd) +def _(subgraph, identifier, *operands): + from torch.utils._python_dispatch import _get_current_dispatch_mode + + mode = _get_current_dispatch_mode() + + if mode is not None: + raise AssertionError("Mode should never be enabled for CPU/CUDA key") + + if getattr(subgraph, "_boxed_call", False): + return subgraph(list(operands)) + else: + return subgraph(*operands) + + +@invoke_subgraph.py_functionalize_impl +def _(ctx, subgraph, identifier, *operands): + from torch._higher_order_ops.auto_functionalize import ( + can_auto_functionalize, + do_auto_functionalize_v2, + ) + + # (in the functionalization metadata phase) Capture tokens before + tokens_before = dict(ctx.mode._tokens) + + # Check if this subgraph has effects stored in the cache + invoke_subgraph_cache = get_invoke_subgraph_cache() + effects = None + if invoke_subgraph_cache: + effects = invoke_subgraph_cache.get_effects(identifier) + + if effects: + if len(effects) != 1: + raise AssertionError( + f"Multiple effects within a subgraph NYI, got {len(effects)} effects" + ) + tokens = ctx.mode._tokens + effects = next(iter(effects)) + token_input = tokens[effects] + + operands = (token_input, *operands) + + def wrap_subgraph(subgraph): + def wrapped_subgraph(token, *args): + res = subgraph(*args) + return ctx.unwrap_tensors(ctx.mode._tokens[effects]), *res + + return wrapped_subgraph + + subgraph = wrap_subgraph(subgraph) + + unwrapped_operands = ctx.unwrap_tensors(operands) + + hop_instance = HopInstance.create(invoke_subgraph, subgraph, identifier, *operands) + if can_auto_functionalize(hop_instance): + # NOTE: [auto_functionalize x invoke_subgraph caching] + # We call auto_functionalized_v2 to support input mutation of invoke_subgraph. + # See NOTE [Support input mutation of hops] for the overall design. + # + # invoke_subgraph is special because of its identifier based caching mechanism. + # In invoke_subgraph's functionalization key implementation, we create a new + # identifier because the subgraph is replaced by FunctionWithNoFreeVars in a + # functional + epilogue form. + if not isinstance(identifier, str): + raise AssertionError( + f"identifier must be a string for auto_functionalize, got {type(identifier)}" + ) + return do_auto_functionalize_v2( + ctx.mode, + hop_instance, + (subgraph, "auto_functionalized_" + identifier, *operands), + {}, + ) + + with ctx.redispatch_to_next(): + # NB: There is an assumption that subgraph does not mutate inputs and + # there is no aliasing. It's Dynamo's responsibility to prevent formation + # of invoke_subgraph ops if input aliasing/mutation is detected. + functionalized_subgraph = FunctionalizeCtxWrapper(ctx, subgraph) + out = invoke_subgraph(functionalized_subgraph, identifier, *unwrapped_operands) + + if effects: + (new_token, *out) = out + ctx.mode._tokens[effects] = new_token + + # (in the functionalization metadata phase) Capture tokens after and see if + # there are any differences (there are new effects or the token value for an + # effect type has changed) + tokens_after = dict(ctx.mode._tokens) + discovered_effects = set() + for effect_type, token in tokens_after.items(): + if effect_type not in tokens_before or tokens_before[effect_type] is not token: + discovered_effects.add(effect_type) + + if discovered_effects: + if not ctx.mode._allow_token_discovery: + raise AssertionError( + f"Number of tokens changed by {len(discovered_effects)} when tracing subgraph {subgraph}." + ) + # Store discovered effects in the cache by identifier + if invoke_subgraph_cache: + invoke_subgraph_cache.add_effects(identifier, discovered_effects) + + return ctx.wrap_tensors(out) + + +# Register the hop fake fn. This will be called in the fake_tensor _dispatch_impl. +@register_fake(invoke_subgraph) +def _(subgraph, identifier, *operands): + from torch._dynamo.utils import dynamo_timed + + with dynamo_timed("invoke_subgraph_fake_tensor", log_pt2_compile_event=True): + if getattr(subgraph, "_boxed_call", False): + return subgraph(list(operands)) + return subgraph(*operands) + + +@invoke_subgraph.py_impl(ProxyTorchDispatchMode) +def _(proxy_mode: ProxyTorchDispatchMode, subgraph, identifier, *operands): + # Check if we have already traced the subgraph. + graph = None + invoke_subgraph_cache = get_invoke_subgraph_cache() + if invoke_subgraph_cache: + graph = invoke_subgraph_cache.get_proxy_dispatch_entry(identifier) + + if graph is None: + from torch._dynamo.utils import dynamo_timed + + with dynamo_timed("invoke_subgraph_proxy_tensor", log_pt2_compile_event=True): + subgraph_decomp_table = _extract_nested_region_config(subgraph) + + # NB: invoke_subgraph subgraph re-trace seq_nr + # The joint graph seq_nr will get wrong in the subsequent re-trace (all nodes will have the same seq_nr), + # so we preserve the original graph's seq_nr here. + with torch.fx.traceback._preserve_node_seq_nr(): + graph = reenter_make_fx( + subgraph, subgraph_decomp_table=subgraph_decomp_table + )(*operands) + + from torch._guards import detect_fake_mode + + fake_mode = detect_fake_mode(operands) + # Only insert deferred runtime asserts when we have dynamic shapes. + # When shape_env is None (static shapes), there are no deferred asserts to insert. + if fake_mode is not None and fake_mode.shape_env is not None: + insert_deferred_runtime_asserts( + graph, + fake_mode.shape_env, + "invoke_subgraph_proxy_torch_dispatch_mode", + export=True, + ) + graph.recompile() + + if not isinstance(proxy_mode.tracer, torch.fx.Tracer): + raise AssertionError( + f"expected proxy_mode.tracer to be torch.fx.Tracer, got {type(proxy_mode.tracer)}" + ) + if invoke_subgraph_cache: + invoke_subgraph_cache.add_proxy_dispatch_entry(identifier, graph) + + node_args = (graph, identifier, *operands) + + def _unwrap_proxy(arg): + if isinstance(arg, torch.fx.GraphModule): + # NOTE: [invoke_subgraph proxy_mode x auto_functionalize] + # Previously, we assumed that `invoke_subgraph` would always be traced with the same tracer. + # This allowed us to cache modules by their identifiers, assuming they were already registered. + # + # However, this assumption no longer holds when we auto-functionalize `invoke_subgraph`. + # auto_functionalize functionalizes the subgraph and wrap it with `FunctionWithNoFreeVars`. + # In the proxy mode implementation of `auto_functionalized_v2`, we need to materialize `FunctionWithNoFreeVars` + # input as a graph module. To do this, we re-trace the `invoke_subgraph` hop, which starts a new sub-tracer + # (see NOTE [materialize callable inputs as graph]). # When the new sub-tracer traces the `invoke_subgraph` + # with a previously cached identifier, the corresponding graph module might not + # exist as a submodule in the new tracer's root. Therefore, we register it as a submodule below. + # + # The alternative is to give a new identifier when we re-trace the invoke_subgraph but this will increase + # the compilation time, which defeats the purpose of caching. + registered_before = False + for ( + _, + submod, + ) in proxy_mode.tracer.root.named_modules(): # type: ignore[union-attr] + if arg is submod: + registered_before = True + + if not registered_before: + qualname = proxy_mode.tracer.get_fresh_qualname("repeated_subgraph") # type: ignore[union-attr] + proxy_mode.tracer.root.register_module(qualname, arg) # type: ignore[union-attr] + return proxy_mode.tracer.unwrap_proxy(arg) # type: ignore[union-attr] + + proxy_args = pytree.tree_map(_unwrap_proxy, node_args) # type: ignore[union-attr] + out_proxy = proxy_mode.tracer.create_proxy( + "call_function", invoke_subgraph, proxy_args, {} + ) + + example_out = invoke_subgraph(graph, identifier, *operands) + return track_tensor_tree( + example_out, out_proxy, constant=None, tracer=proxy_mode.tracer + ) + + +def invoke_subgraph_inductor_compile( + gm, example_inputs, inductor_config_patches=None, **kwargs +): + from torch._functorch._aot_autograd.runtime_wrappers import ( + SerializableCompiledFunction, + ) + from torch._functorch._aot_autograd.utils import simple_wraps + from torch._inductor import config + from torch._inductor.compile_fx import compile_fx_inner + from torch._inductor.standalone_compile import AOTCompiledArtifact + + # Used for testing only, should only be changed via _testing_capture_invoke_subgraph_inductor_compile_gms() + if ( + torch._dynamo.testing._testing_invoke_subgraph_inductor_compile_captured_gms + is not None + ): + torch._dynamo.testing._testing_invoke_subgraph_inductor_compile_captured_gms.append( + copy.deepcopy(gm) + ) + + if inductor_config_patches is None: + inductor_config_patches = {} + compile_fn = config.patch(inductor_config_patches)(compile_fx_inner) + compiled_fn_inner = compile_fn(gm, example_inputs) + if not compiled_fn_inner._boxed_call: + raise AssertionError( + "compiled_fn_inner must have _boxed_call attribute set to True" + ) + + # Follow boxed calling convention + @simple_wraps(compiled_fn_inner) + def forward(*runtime_args: tuple[Any]): + full_args = [] + full_args.extend(runtime_args) + return compiled_fn_inner(full_args) + + # Just for convenience + forward.zero_grad = gm.zero_grad # type: ignore[attr-defined] + forward.named_parameters = gm.named_parameters # type: ignore[attr-defined] + forward.named_buffers = gm.named_buffers # type: ignore[attr-defined] + + # TODO: Do we need the post compile passes in _aot_stage2b_compile_forward_or_inference? + # TODO: add a real serialize function for SerializableCompiledFunction like _cache_inference_info + forward.serialize = SerializableCompiledFunction(forward, lambda: None) # type: ignore[attr-defined] + return AOTCompiledArtifact(forward) + + +def get_invoke_subgraph_compile_options( + inductor_config_patches=None, + decompositions=None, + partitioner="min_cut_rematerialization_partition", +): + if inductor_config_patches is None: + inductor_config_patches = {"triton.autotune_at_compile_time": True} + inductor_compile = functools.partial( + invoke_subgraph_inductor_compile, + inductor_config_patches=inductor_config_patches, + ) + + if inductor_config_patches: + from torch._inductor import config as inductor_config + + # Validate that all config keys exist + for key in inductor_config_patches: + if not hasattr(inductor_config, key): + raise ValueError( + f"Invalid inductor config key '{key}' in get_invoke_subgraph_compile_options. " + f"Available config keys can be found in torch._inductor.config" + ) + + return NestedCompileRegionOptions( + fw_compiler=inductor_compile, + bw_compiler=inductor_compile, + partitioner=partitioner, + decompositions=decompositions, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/local_map.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/local_map.py new file mode 100644 index 0000000000000000000000000000000000000000..e691eda36b057e3cc3660300915b19eb8999ba7b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/local_map.py @@ -0,0 +1,645 @@ +# Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +# +# This source code is licensed under the BSD license found in the +# LICENSE file in the root directory of this source tree. + +# NOTE: this file may be removed once we move to a dynamo frontend + +import contextlib +import functools +from collections.abc import Callable, Generator, Sequence +from contextlib import contextmanager +from typing import Any, TypeAlias + +import torch +import torch.utils._pytree as pytree +from torch._C import DispatchKey +from torch._higher_order_ops.utils import ( + clone_outputs_aliasing_inputs, + redirect_to_mode, + save_values_for_backward, + saved_values, +) +from torch._ops import HigherOrderOperator +from torch._subclasses.fake_tensor import FakeTensor, FakeTensorMode +from torch._subclasses.functional_tensor import FunctionalTensor +from torch.fx import GraphModule +from torch.fx.experimental.proxy_tensor import ProxyTorchDispatchMode, track_tensor_tree +from torch.utils.checkpoint import _CachedTorchDispatchMode, _CachingTorchDispatchMode + + +# Proxy the HOP instead of inlining into it +# And trace it with local shapes for AP +_DEFER_INLINING = False + +GraphArg: TypeAlias = tuple[torch.Tensor, int, torch.SymInt, None] + + +@contextmanager +def defer_inlining() -> Generator[None, None, None]: + global _DEFER_INLINING + prior = _DEFER_INLINING + try: + _DEFER_INLINING = True + yield + finally: + _DEFER_INLINING = prior + + +# Used to unwrap tensors classes like FunctionalTensor and Parameter +def _new_tensor( + t: Any, + new_shape: Sequence[int] | None = None, + new_stride: Sequence[int] | None = None, +) -> Any: + if isinstance(t, torch.Tensor): + if type(t) not in (FunctionalTensor, FakeTensor, torch.Tensor): + raise AssertionError(f"No subclasses support for now, found {type(t)}") + return torch.empty_strided( + t.size() if new_shape is None else new_shape, + t.stride() if new_stride is None else new_stride, + device=t.device, + dtype=t.dtype, + requires_grad=t.requires_grad, + ) + return t + + +# Autoparallel specific, we want to treat plain tensors as DTensors +def _redistribute( + args: Any, + all_placements: tuple[Any], + mesh: Any, + shape_stride_fn: Callable[[torch.Tensor, Any, Any], tuple[list[int], list[int]]], +) -> GraphArg: + from torch._dispatch.python import suspend_functionalization + from torch._guards import detect_fake_mode + from torch._subclasses.functional_tensor import disable_functional_mode + from torch.fx.experimental.proxy_tensor import disable_proxy_modes_tracing + + with ( + suspend_functionalization(), + disable_functional_mode(), + disable_proxy_modes_tracing(), + ): + fake_mode = detect_fake_mode(args) + if fake_mode is None: + raise AssertionError("defer_inlining() is only supported for FakeTensors") + + with fake_mode: + new_args = list(pytree.tree_map(_new_tensor, args)) + for i, (tensor, placements) in enumerate(zip(new_args, all_placements)): + if tensor is None: + # Sometimes gradients can be None + continue + + new_shape, new_stride = shape_stride_fn( + tensor, + mesh, + placements, + ) + new_args[i] = _new_tensor( + tensor, new_shape=new_shape, new_stride=new_stride + ) + + new_args = tuple(new_args) + if not all( + isinstance(t, (FakeTensor, int, torch.SymInt, type(None))) + for t in new_args + ): + raise AssertionError(f"Unexpected element in {args=}") + + return new_args + + +def redistribute_fw_inputs( + global_args: Any, all_placements: Any, mesh: Any, _: int | None = None +) -> GraphArg: + if len(global_args) != len(all_placements): + raise AssertionError( + f"global_args length ({len(global_args)}) != all_placements length ({len(all_placements)})" + ) + return _redistribute( + global_args, + all_placements, + mesh, + torch.distributed.tensor._utils.compute_local_tensor_info, + ) + + +def redistribute_fw_outputs( + local_outs: Any, all_placements: Any, mesh: Any, num_activations: int +) -> GraphArg: + if len(local_outs) != len(all_placements) + num_activations: + raise AssertionError( + f"local_outs length ({len(local_outs)}) != " + f"all_placements length ({len(all_placements)}) + num_activations ({num_activations})" + ) + num_fw_outs = len(local_outs) - num_activations + if num_fw_outs <= 0: + raise AssertionError(f"num_fw_outs must be > 0, got {num_fw_outs}") + outs, activations = local_outs[:num_fw_outs], local_outs[num_fw_outs:] + return ( + *_redistribute( + outs, + all_placements, + mesh, + torch.distributed.tensor._utils.compute_global_tensor_info, + ), + *activations, + ) + + +def redistribute_bw_inputs( + global_args: Any, all_placements: Any, mesh: Any, num_activations: int +) -> GraphArg: + if len(global_args) != len(all_placements) + num_activations: + raise AssertionError( + f"global_args length ({len(global_args)}) != " + f"all_placements length ({len(all_placements)}) + num_activations ({num_activations})" + ) + activations, inputs = global_args[:num_activations], global_args[num_activations:] + if len(inputs) <= 0: + raise AssertionError("inputs must not be empty") + local_inputs = _redistribute( + inputs, + all_placements, + mesh, + torch.distributed.tensor._utils.compute_local_tensor_info, + ) + return ( + *activations, + *local_inputs, + ) + + +def redistribute_bw_outputs( + local_outs: Any, all_placements: Any, mesh: Any, _: int | None = None +) -> GraphArg: + if len(local_outs) != len(all_placements): + raise AssertionError( + f"local_outs length ({len(local_outs)}) != all_placements length ({len(all_placements)})" + ) + return _redistribute( + local_outs, + all_placements, + mesh, + torch.distributed.tensor._utils.compute_global_tensor_info, + ) + + +class LocalMapHOP(HigherOrderOperator): + def __init__(self) -> None: + super().__init__("local_map_hop") + + def __call__(self, gm: GraphModule, *args: Any, **kwargs: Any) -> Any: + # pyrefly: ignore [missing-attribute] + return super().__call__(gm, *args, **kwargs) + + +local_map_hop = LocalMapHOP() + +# Registers dispatches for SAC +redirect_to_mode(local_map_hop, _CachingTorchDispatchMode) +redirect_to_mode(local_map_hop, _CachedTorchDispatchMode) + + +def create_hop_fw_bw( + fw_gm: GraphModule, + *_args: Any, +) -> tuple[GraphModule, GraphModule, int, int, set[int]]: + """ + Traces a joint, applies passes and partitions it + """ + # Keeping these imports here + # Avoid circular dependencies once we upstream with dynamo frontend + from torch._dispatch.python import suspend_functionalization + from torch._functorch.aot_autograd import AOTConfig, create_joint + from torch._guards import detect_fake_mode + from torch._subclasses.fake_tensor import FakeTensor, FakeTensorMode + from torch._subclasses.functional_tensor import disable_functional_mode + from torch.fx.experimental.proxy_tensor import disable_proxy_modes_tracing, make_fx + + local_map_kwargs = fw_gm.meta["local_map_kwargs"] # type: ignore[attr-defined] + if "in_placements" not in local_map_kwargs: + raise AssertionError("'in_placements' not found in local_map_kwargs") + if "out_placements" not in local_map_kwargs: + raise AssertionError("'out_placements' not found in local_map_kwargs") + if "device_mesh" not in local_map_kwargs: + raise AssertionError("'device_mesh' not found in local_map_kwargs") + if len(local_map_kwargs["in_placements"]) != len(_args): + raise AssertionError( + f"in_placements length ({len(local_map_kwargs['in_placements'])}) != _args length ({len(_args)})" + ) + + dummy_aot_config = AOTConfig( + fw_compiler=None, # type: ignore[arg-type] + bw_compiler=None, # type: ignore[arg-type] + partition_fn=None, # type: ignore[arg-type] + decompositions={}, + num_params_buffers=0, + aot_id=0, + keep_inference_input_mutations=False, + ) + + with suspend_functionalization(), disable_functional_mode(): + with disable_proxy_modes_tracing(): + # If someone runs this hop under the default compiler backend ("eager") + # Then this path will be run with the actual user inputs. We convert them + # to fake tensors in order to not perform any actual compute. + + fake_mode = detect_fake_mode(_args) + if fake_mode is None: + fake_mode = FakeTensorMode(allow_non_fake_inputs=True) + + with fake_mode: + fw_inputs = redistribute_fw_inputs( + _args, + local_map_kwargs["in_placements"], + local_map_kwargs["device_mesh"], + ) + if len(fw_inputs) != len(local_map_kwargs["in_placements"]): + raise AssertionError( + f"fw_inputs length ({len(fw_inputs)}) != " + f"in_placements length ({len(local_map_kwargs['in_placements'])})" + ) + + if not all( + isinstance(t, (FakeTensor, int, torch.SymInt)) for t in fw_inputs + ): + raise AssertionError(f"Unexpected element in {fw_inputs=}") + + ctx = ( + fake_mode.shape_env.ignore_fresh_unbacked_symbols + if fake_mode.shape_env is not None + else contextlib.nullcontext + ) + with ctx(): + fw_outs = fw_gm(*fw_inputs) + + example_grads = pytree.tree_map( + _new_tensor, + fw_outs, + ) + if not isinstance(example_grads, (list, tuple)): + example_grads = [example_grads] + + num_fw_inputs = len(fw_inputs) + num_fw_outputs = len(example_grads) + + def joint_f( + *primals_and_tangents: list[torch.Tensor], + ) -> Any: + primals = primals_and_tangents[:num_fw_inputs] + tangents = primals_and_tangents[num_fw_inputs:] + + def prepare_fw_with_masks( + fw_gm: torch.fx.GraphModule, + ) -> Callable[..., Any]: + def fw_with_masks(*args: Any) -> tuple[tuple[Any], list[bool]]: + # The Interpreter here is required to propagate metadata + # from the dynamo graph body to the local_map graph body. + # This is required for fx_traceback.annotate for work. + fw_out = torch.fx.Interpreter(fw_gm).run(*args) + if not isinstance(fw_out, tuple): + raise AssertionError( + "Dynamo traced submodule should return tuple" + ) + return fw_out, [ + bool(isinstance(ret, torch.Tensor) and ret.requires_grad) + for ret in fw_out + ] + + return fw_with_masks + + fw_outs, grads = create_joint( + prepare_fw_with_masks(fw_gm), aot_config=dummy_aot_config + )(primals, tangents) + from torch.fx.experimental.symbolic_shapes import has_free_unbacked_symbols + + if has_free_unbacked_symbols((*fw_outs, *grads)): + raise AssertionError( + "Unbacked symints leaking outside of the joint graph is not yet supported." + ) + + maybe_clone = clone_outputs_aliasing_inputs(primals_and_tangents) + # put grads first to work with existing hop utils + return pytree.tree_map(maybe_clone, (*grads, *fw_outs)) + + filtered_grads_idx = set() + for i, example_grad in enumerate(example_grads): + # Filter out grads that are None or do not require_grad. + # The AOTAutograd utils we rely on force this assumption. + # We must also filter the runtime tangents too. + if example_grad is not None and ( + isinstance(example_grad, torch.Tensor) and example_grad.requires_grad + ): + filtered_grads_idx.add(i) + + primals_and_tangents = [ + *fw_inputs, + *[example_grads[i] for i in filtered_grads_idx], + ] + joint_hop_gm = make_fx(joint_f)(*primals_and_tangents) + from torch._functorch._aot_autograd.graph_capture import ( + copy_fwd_metadata_to_bw_nodes, + ) + + copy_fwd_metadata_to_bw_nodes(joint_hop_gm) + + from torch._functorch._aot_autograd.graph_compile import prepare_for_partitioner + from torch._inductor.compile_fx import partition_fn + + # Match partitioner convention + prepped_joint_hop_gm = prepare_for_partitioner( + joint_hop_gm, num_fw_inputs, num_fw_outputs + ) + with disable_proxy_modes_tracing(): + # Also runs joint passes + new_fw_gm, new_bw_gm = partition_fn( + prepped_joint_hop_gm, + [], + num_fwd_outputs=num_fw_outputs, + static_lifetime_input_indices=[], + ) + + # Fix tags because min-cut does not respect fw/bw boundary, breaking + # default partitioner's assumptions. + for node in new_fw_gm.graph.nodes: + node.meta["partitioner_tag"] = "is_forward" + for node in new_bw_gm.graph.nodes: + node.meta["partitioner_tag"] = "is_backward" + + # Propagate meta onto fw/bw graphs, later will be set on proxied nodes + new_fw_gm.meta["local_map_kwargs"] = local_map_kwargs + new_bw_gm.meta["local_map_kwargs"] = {**local_map_kwargs} + # Okay because Autoparallel assumes same sharding between param and grads + new_bw_gm.meta["local_map_kwargs"]["in_placements"] = tuple( + [local_map_kwargs["out_placements"][i] for i in filtered_grads_idx] + ) + new_bw_gm.meta["local_map_kwargs"]["out_placements"] = local_map_kwargs[ + "in_placements" + ] + + # Validate Forward + fw_kwargs = new_fw_gm.meta["local_map_kwargs"] + expected_fw_inputs = len(fw_kwargs["in_placements"]) + expected_fw_outputs = len(fw_kwargs["out_placements"]) + actual_fw_inputs = len(new_fw_gm.graph.find_nodes(op="placeholder")) + actual_fw_outputs = num_fw_outputs + if expected_fw_inputs != actual_fw_inputs: + raise AssertionError( + f"expected_fw_inputs ({expected_fw_inputs}) != actual_fw_inputs ({actual_fw_inputs})" + ) + if expected_fw_outputs != actual_fw_outputs: + raise AssertionError( + f"expected_fw_outputs ({expected_fw_outputs}) != actual_fw_outputs ({actual_fw_outputs})" + ) + + # Validate Activations + if len(new_fw_gm.graph.find_nodes(op="output")) != 1: + raise AssertionError( + f"Expected exactly 1 output node, got {len(new_fw_gm.graph.find_nodes(op='output'))}" + ) + num_activations = ( + len(new_fw_gm.graph.find_nodes(op="output")[0].args[0]) - num_fw_outputs + ) + # tensors first, then symints + if num_activations < 0: + raise AssertionError(f"num_activations must be >= 0, got {num_activations}") + + # Validate Backward + bw_kwargs = new_bw_gm.meta["local_map_kwargs"] + expected_bw_inputs = len(bw_kwargs["in_placements"]) + expected_bw_outputs = len(bw_kwargs["out_placements"]) + actual_bw_inputs = ( + len(new_bw_gm.graph.find_nodes(op="placeholder")) - num_activations + ) + if actual_bw_inputs <= 0: + raise AssertionError( + f"actual_bw_inputs must be > 0, got {actual_bw_inputs}" + ) + if expected_fw_inputs + expected_bw_inputs != len(primals_and_tangents): + raise AssertionError( + f"expected_fw_inputs ({expected_fw_inputs}) + expected_bw_inputs ({expected_bw_inputs}) " + f"!= primals_and_tangents length ({len(primals_and_tangents)})" + ) + if actual_fw_inputs + actual_bw_inputs != len(primals_and_tangents): + raise AssertionError( + f"actual_fw_inputs ({actual_fw_inputs}) + actual_bw_inputs ({actual_bw_inputs}) " + f"!= primals_and_tangents length ({len(primals_and_tangents)})" + ) + if len(new_bw_gm.graph.find_nodes(op="output")) != 1: + raise AssertionError( + f"Expected exactly 1 bw output node, got {len(new_bw_gm.graph.find_nodes(op='output'))}" + ) + actual_bw_outputs = len(new_bw_gm.graph.find_nodes(op="output")[0].args[0]) + if expected_bw_inputs != actual_bw_inputs: + raise AssertionError( + f"expected_bw_inputs ({expected_bw_inputs}) != actual_bw_inputs ({actual_bw_inputs})" + ) + if expected_bw_outputs != actual_bw_outputs: + raise AssertionError( + f"expected_bw_outputs ({expected_bw_outputs}) != actual_bw_outputs ({actual_bw_outputs})" + ) + + new_fw_gm.meta["num_activations"] = num_activations + new_fw_gm.meta["is_backward"] = False + new_bw_gm.meta["num_activations"] = num_activations + new_bw_gm.meta["is_backward"] = True + + return new_fw_gm, new_bw_gm, num_fw_inputs, num_fw_outputs, filtered_grads_idx + + +class LocalMapAutogradOp(torch.autograd.Function): + @staticmethod + # pyrefly: ignore [bad-override] + def forward( + ctx: Any, + fw_gm: GraphModule, + bw_gm: GraphModule, + num_fw_ins: int, + num_fw_outs: int, + filtered_grads_idx: set[int], + *args: Any, + **kwargs: Any, + ) -> tuple[torch.Tensor | None, ...]: + from torch._functorch._aot_autograd.schemas import MemoryFormatMeta + + ctx.bw_gm = bw_gm + ctx.num_fw_ins = num_fw_ins + ctx.filtered_grads_idx = filtered_grads_idx + + with torch._C._AutoDispatchBelowAutograd(): + fw_outs_with_saved_activations = local_map_hop(fw_gm, *args, **kwargs) + + fw_outs = fw_outs_with_saved_activations[:num_fw_outs] + saved_activations = fw_outs_with_saved_activations[num_fw_outs:] + save_values_for_backward(ctx, saved_activations) + + # Force memory_format path (not exact size/stride) because local_map forward + # operates on local shapes but backward receives global-shaped tangents. + # TODO(ivankobzarev): Support exact size/stride by converting between local/global shapes. + ctx.expected_tangent_metadata = { + i: MemoryFormatMeta.from_tensor(fw_outs[i], force_use_memory_format=True) + for i in filtered_grads_idx + } + return fw_outs + + @staticmethod + def backward( + ctx: Any, *_grads: tuple[torch.Tensor] + ) -> tuple[torch.Tensor | None, ...]: + from torch._functorch._aot_autograd.runtime_wrappers import ( + coerce_to_expected_memory_format, + ) + + if ctx.pos != sorted(ctx.pos): + raise AssertionError( + "Interleaving saved tensor activations and symints is not expected from min-cut partitioner." + ) + ctx.pos = list(reversed(ctx.pos)) # make saved_values return symints first + saved_activations = saved_values(ctx) + with torch._C._AutoDispatchBelowAutograd(): + # Filter out grads that are None or do not require_grad. + # The AOTAutograd utils we rely on force this assumption. + grads = [_grads[i] for i in ctx.filtered_grads_idx] + if len(grads) != len(ctx.expected_tangent_metadata): + raise AssertionError( + f"{len(grads)=} vs {len(ctx.expected_tangent_metadata)}" + ) + + for i, meta in ctx.expected_tangent_metadata.items(): + grads[i] = coerce_to_expected_memory_format(grads[i], meta) + + grad_ins = local_map_hop(ctx.bw_gm, *saved_activations, *grads) + if len(grad_ins) != ctx.num_fw_ins: + raise RuntimeError( + f"Expected {ctx.num_fw_ins} grad_ins, got {len(grad_ins)}" + ) + return None, None, None, None, None, *grad_ins + + +@local_map_hop.py_impl(torch._C.DispatchKey.Autograd) +def autograd_key( + fw_gm: GraphModule, + *args: Any, + **kwargs: Any, +) -> Any: + local_map_kwargs = fw_gm.meta["local_map_kwargs"] # type: ignore[attr-defined] + if local_map_kwargs.get("in_grad_placements", None) is not None: + raise AssertionError("local_map in_grad_placements are not yet supported.") + if _DEFER_INLINING: + fw_gm, bw_gm, num_fw_ins, num_fw_outs, filtered_grads_idx = create_hop_fw_bw( + fw_gm, *args + ) + return LocalMapAutogradOp.apply( + fw_gm, bw_gm, num_fw_ins, num_fw_outs, filtered_grads_idx, *args, **kwargs + ) + + # TODO: get rid of this when we can install as a subgraph + return torch.fx.Interpreter(fw_gm).run(*args, **kwargs) + + +@local_map_hop.py_functionalize_impl +def functional_mode_key( + ctx: Any, gm: GraphModule, *args: Any, **kwargs: Any +) -> tuple[torch.Tensor]: + if kwargs: + raise AssertionError(f"kwargs must be empty, got {kwargs}") + + unwrapped_inputs = ctx.unwrap_tensors(args) + with ctx.redispatch_to_next(): + out = local_map_hop(gm, *unwrapped_inputs) + return ctx.wrap_tensors(out) + + +@local_map_hop.py_impl(FakeTensorMode) +def fake_mode_key( + mode: FakeTensorMode, + gm: GraphModule, + *args: Any, + **kwargs: Any, +) -> GraphArg: + with mode: + if not _DEFER_INLINING: + return gm(*args, **kwargs) + + # otherwise, we need to convert to local shapes for AP + is_backward = gm.meta["is_backward"] + redistribute_inputs = ( + redistribute_bw_inputs if is_backward else redistribute_fw_inputs + ) + local_args = redistribute_inputs( + args, + gm.meta["local_map_kwargs"]["in_placements"], + gm.meta["local_map_kwargs"]["device_mesh"], + gm.meta["num_activations"], + ) + local_outs = gm(*local_args) + redistribute_outputs = ( + redistribute_bw_outputs if is_backward else redistribute_fw_outputs + ) + global_outs = redistribute_outputs( + local_outs, + gm.meta["local_map_kwargs"]["out_placements"], + gm.meta["local_map_kwargs"]["device_mesh"], + gm.meta["num_activations"], + ) + return global_outs + + +def proxy_mode_key_common( + call_hop: Callable[..., Any], + proxy_mode: ProxyTorchDispatchMode, + gm: GraphModule, + *args: Any, + **kwargs: Any, +) -> tuple[torch.Tensor]: + if proxy_mode is None: + raise AssertionError("Mode should always be enabled for python fallback key") + if len(kwargs) != 0: + raise AssertionError(f"kwargs must be empty, got {kwargs}") + + example_out = call_hop(*args, **kwargs) + proxy_args = pytree.tree_map(proxy_mode.tracer.unwrap_proxy, args) # type: ignore[union-attr] + + out_proxy = proxy_mode.tracer.create_proxy( + "call_function", call_hop, proxy_args, {} + ) + + # extract local_map args, post-dispatch operates on GraphModules + if not gm.meta["local_map_kwargs"]: + raise AssertionError("gm.meta['local_map_kwargs'] must be set") + local_map_kwargs = gm.meta["local_map_kwargs"] + + # propagate local_map args to the call_function node + out_proxy.node.meta["local_map_kwargs"] = local_map_kwargs + + return track_tensor_tree( + example_out, out_proxy, constant=None, tracer=proxy_mode.tracer + ) + + +@local_map_hop.py_impl(ProxyTorchDispatchMode) +def proxy_mode_key( + proxy_mode: ProxyTorchDispatchMode, + gm: GraphModule, + *args: Any, + **kwargs: Any, +) -> tuple[torch.Tensor]: + # TODO: get rid of this when we can install as a subgraph + def call_local_map(*_args: Any, **_kwargs: Any) -> Any: + return functools.partial(local_map_hop, gm)(*_args, **_kwargs) + + return proxy_mode_key_common(call_local_map, proxy_mode, gm, *args, **kwargs) + + +# Running HOP in eager with real tensors +@local_map_hop.py_impl(DispatchKey.CompositeExplicitAutograd) +def real_impl( + gm: GraphModule, + *args: Any, + **kwargs: Any, +) -> tuple[torch.Tensor]: + return gm(*args, **kwargs) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/map.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/map.py new file mode 100644 index 0000000000000000000000000000000000000000..cb54eddfb7a13da1bba3406f1f6d9f03e2b252ec --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/map.py @@ -0,0 +1,337 @@ +# mypy: allow-untyped-defs +import functools +from collections.abc import Callable +from typing_extensions import TypeVarTuple + +import torch +import torch.utils._pytree as pytree +from torch._C import DispatchKey +from torch._dispatch.python import suspend_functionalization +from torch._higher_order_ops.utils import _maybe_run_with_interpreter, reenter_make_fx +from torch._ops import HigherOrderOperator +from torch._subclasses.fake_tensor import FakeTensorMode +from torch._subclasses.functional_tensor import disable_functional_mode +from torch.fx.experimental.proxy_tensor import ( + disable_proxy_modes_tracing, + ProxyTorchDispatchMode, + track_tensor_tree, +) + +from .utils import ( + _from_fun, + _stack_pytree, + _unstack_pytree, + create_bw_fn, + fill_none_with_masks, + filter_with_masks, + first_slice_copy, + materialize_as_graph, + save_values_for_backward, + saved_values, + split_into_chunks, +) + + +class MapImpl(HigherOrderOperator): + def __init__(self): + super().__init__("map_impl") + + def __call__(self, *args, **kwargs): + # pyrefly: ignore [missing-attribute] + return super().__call__(*args, **kwargs) + + +map_impl = MapImpl() + + +def map( + f: Callable[[pytree.PyTree, tuple[pytree.PyTree, ...]], pytree.PyTree], + xs: pytree.PyTree | torch.Tensor, + *args: TypeVarTuple, +): + r""" + Performs a map of f with xs. Intuitively, you can think of the semantic being:: + + out = [] + for idx in len(xs.size(0)): + xs_sliced = xs.select(0, idx) + out.append(f(xs_sliced, *args)) + torch.stack(out) + + .. warning:: + + ``torch._higher_order_ops.map`` is a prototype feature in PyTorch. It currently + does not support autograd and you may run into miscompiles. + Read more about feature classification at: + https://pytorch.org/blog/pytorch-feature-classification-changes/#prototype + + + Args: + f (Callable): a callable that takes an input x, that could either be a single Tensor + or a nested dict, list of tensors and some additional inputs + xs: the inputs that're to be mapped over. We'll iterate over the first dim of each x + and perform f on each slice. + + *args: additional arguments provided to each step of f. They could also be omitted and + map is able to automatically figure out the read dependency. + + Return: + the stacked output for each step of f + + Example:: + + def f(xs): + return xs[0] + xs[1] + const1 + const2 + + + xs = [torch.randn(2, 3), torch.randn(2, 3)] + const1 = torch.randn(2, 3) + const2 = torch.randn(2, 3) + # returns a tensor of shape [2, 2, 3] + torch._higher_order_ops.map(f, xs) + + """ + flat_xs, xs_spec = pytree.tree_flatten(xs) + flat_args, args_spec = pytree.tree_flatten(args) + if not all(isinstance(t, torch.Tensor) for t in flat_xs): + raise RuntimeError(f"Mapped xs can only consist of tensors. Got xs {flat_xs}.") + + shapes = [xs.shape for xs in flat_xs] + leading_dim_size = shapes[0][0] + if leading_dim_size == 0: + raise RuntimeError("Leading dimensions of mapped xs cannot be 0.") + + if any(cur_shape[0] != leading_dim_size for cur_shape in shapes): + raise RuntimeError( + f"Leading dimensions of mapped xs must be consistent. Got shapes {shapes}." + ) + + def run_flattened_map(f, flat_xs, flat_args): + def wrapped_fn(*flat_args, f, xs_tree_spec, args_tree_spec, num_xs): + xs = pytree.tree_unflatten(flat_args[:num_xs], xs_tree_spec) + args = pytree.tree_unflatten(flat_args[num_xs:], args_tree_spec) + return f(xs, *args) + + inner_f = functools.partial( + wrapped_fn, + f=f, + xs_tree_spec=xs_spec, + args_tree_spec=args_spec, + num_xs=len(flat_xs), + ) + return map_impl(inner_f, flat_xs, flat_args) + + from torch._higher_order_ops.utils import _maybe_compile_and_run_fn + + return _maybe_compile_and_run_fn(run_flattened_map, f, flat_xs, flat_args) + + +class MapAutogradOp(torch.autograd.Function): + @staticmethod + # pyrefly: ignore [bad-override] + def forward(ctx, f, num_mapped_args, *flat_args): + ctx._f = f + ctx._num_mapped_args = num_mapped_args + ctx._num_pos_args = len(flat_args) - num_mapped_args + + # We snapshot the dispatch keys in forward for materializing the + # the bw_graph in backward. + ctx._fw_include_key_set = torch._C._dispatch_tls_local_include_set() + ctx._fw_exclude_key_set = torch._C._dispatch_tls_local_exclude_set() + save_values_for_backward(ctx, flat_args) + with torch._C._AutoDispatchBelowAutograd(): + return ( + *map_impl(f, flat_args[:num_mapped_args], flat_args[num_mapped_args:]), + ) + + @staticmethod + def backward(ctx, *flat_grads): + fw_args = saved_values(ctx) + num_mapped_args = ctx._num_mapped_args + num_pos_args = ctx._num_pos_args + num_grads = len(flat_grads) + + fw_mapped_args, pos_args = split_into_chunks( + fw_args, + [ + num_mapped_args, + num_pos_args, + ], + ) + + bw_f = create_bw_fn(ctx._f, fw_args) + + grads_tensor_masks = [] + + # Create a wrapper around thefor the bw_f + def bw_f_wrapper(*args): + nonlocal grads_tensor_masks + + # Dissect args and re-order them for the ``ctx._bw_f`` + # args provided to the wrapper are composed of [*fw_mapped_args, *flat_grads, *pos_args] + # The content of ``bw_f_tangents`` are the upstream gradients, i.e. flat_grads + # The content of ``bw_f_primals`` are the fw_args, i.e., [*fw_mapped_args, *pos_args] + # The bw_f requires *bw_f_primals, *bw_f_tangents + fw_m_args, bw_f_tangents, pos_args = split_into_chunks( + args, [num_mapped_args, num_grads, num_pos_args] + ) + bw_f_primals = *fw_m_args, *pos_args + gradients = bw_f(*bw_f_primals, *bw_f_tangents) + grads_tensor_masks = [ + True if isinstance(out, torch.Tensor) else out for out in gradients + ] + return filter_with_masks(gradients, grads_tensor_masks) + + def construct_args_single_step_bw(): + unwrapped_mapped_xs = pytree.tree_map(_from_fun, fw_mapped_args) + # Use first_slice_copy instead of _unstack_pytree to avoid + # iterating over batch dim, which would guard on symbolic sizes. + example_xs = [ + first_slice_copy(x).requires_grad_(x.requires_grad) + for x in unwrapped_mapped_xs + ] + unwrapped_grads = pytree.tree_map(_from_fun, flat_grads) + example_grads = [ + first_slice_copy(x).requires_grad_(x.requires_grad) + for x in unwrapped_grads + ] + example_pos_args = [ + _from_fun(arg) if isinstance(arg, torch.Tensor) else arg + for arg in pos_args + ] + return *example_xs, *example_grads, *example_pos_args + + with suspend_functionalization(), disable_functional_mode(): + with disable_proxy_modes_tracing(): + args_single_step_bw = construct_args_single_step_bw() + + # TODO: we need to materialize the bw graphs because dynamo is unable to + # trace through the joint function when torch.compile torch.autograd.grad. + fn_bw_gm = materialize_as_graph( + bw_f_wrapper, + args_single_step_bw, + ctx._fw_include_key_set, + ctx._fw_exclude_key_set, + force_enable_grad=True, + ) + + grads = map_impl(fn_bw_gm, fw_mapped_args + flat_grads, pos_args) + + return None, None, *fill_none_with_masks(grads, grads_tensor_masks) + + +def _broadcast_to_batch(output, batch_size): + """Expand each tensor in output pytree to include batch dimension. + + Note: Although outputs are flattened in the compiled path (via torch.compile), + users may call map_impl directly with pytree outputs, so we support pytrees + for backward compatibility. + """ + + def expand_with_batch(t): + if isinstance(t, torch.Tensor): + # Use contiguous_format to match torch.stack behavior + return ( + t.unsqueeze(0) + .expand(batch_size, *t.shape) + .clone(memory_format=torch.contiguous_format) + ) + return t + + return pytree.tree_map(expand_with_batch, output) + + +def trace_map(proxy_mode, func_overload, f, xs, pos_args): + from torch._higher_order_ops.utils import first_slice_copy + + with disable_proxy_modes_tracing(): + # Use first_slice_copy instead of _unstack_pytree to avoid + # iterating over batch dim, which would guard on symbolic sizes. + example_input = pytree.tree_map(first_slice_copy, xs) + + body_graph = f + + body_graph = reenter_make_fx(body_graph)(*example_input, *pos_args) + + next_name = proxy_mode.tracer.get_fresh_qualname("body_graph_") + + proxy_mode.tracer.root.register_module(next_name, body_graph) + + fake_outs = map_impl(body_graph, xs, pos_args) + + node_args = (body_graph, list(xs), list(pos_args)) + proxy_args = pytree.tree_map(proxy_mode.tracer.unwrap_proxy, node_args) + out_proxy = proxy_mode.tracer.create_proxy( + "call_function", func_overload, proxy_args, {}, name="map_impl" + ) + return track_tensor_tree( + fake_outs, out_proxy, constant=None, tracer=proxy_mode.tracer + ) + + +@map_impl.py_impl(DispatchKey.CompositeExplicitAutograd) +def map_dense(f, xs, pos_args): + pytrees = [f(*inp, *pos_args) for inp in _unstack_pytree(xs)] + return _stack_pytree(pytrees) + + +@map_impl.py_autograd_impl +def map_autograd(f, xs, pos_args): + num_mapped_args = len(xs) + flat_out = MapAutogradOp.apply(f, num_mapped_args, *xs, *pos_args) + return flat_out + + +@map_impl.py_impl(ProxyTorchDispatchMode) +def map_proxy_torch_dispatch_mode(mode, f, xs, args): + return trace_map(mode, map_impl, f, xs, args) + + +@map_impl.py_impl(FakeTensorMode) +def map_fake_tensor_mode(mode, f, xs, args): + from torch._higher_order_ops.utils import first_slice_copy + + with mode: + # Use first_slice_copy instead of _unstack_pytree to avoid + # iterating over batch dim, which would guard on symbolic sizes. + first_row = pytree.tree_map(first_slice_copy, xs) + example_output = f(*first_row, *args) + + flat_xs, _ = pytree.tree_flatten(xs) + batch_size = flat_xs[0].shape[0] + + return _broadcast_to_batch(example_output, batch_size) + + +@map_impl.py_functionalize_impl +def map_functionalize(ctx, f, xs, pos_args): + from torch._higher_order_ops.utils import ( + _check_alias_and_mutation, + first_slice_copy, + ) + + unwrapped_xs = ctx.unwrap_tensors(xs) + unwrapped_args = ctx.unwrap_tensors(pos_args) + wrapped_fn = ctx.functionalize(_maybe_run_with_interpreter(f)) + + with ctx.redispatch_to_next(): + # Use first_slice_copy instead of _unstack_pytree to avoid + # iterating over batch dim, which would guard on symbolic sizes. + example_inputs = ( + *pytree.tree_map(first_slice_copy, unwrapped_xs), + *unwrapped_args, + ) + pre_dispatch = hasattr(ctx, "mode") and ctx.mode.pre_dispatch + _check_alias_and_mutation(f, example_inputs, "map", pre_dispatch) + map_return = map_impl(wrapped_fn, unwrapped_xs, unwrapped_args) + return ctx.wrap_tensors(map_return) + + +def _fake_map(f, x, *args): + from functorch.experimental.control_flow import _stack_pytree, _unstack_pytree + + x_pytrees = _unstack_pytree(x) + zs = [] + for xp in x_pytrees: + zs.append(f(xp, *args)) + return _stack_pytree(zs) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/out_dtype.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/out_dtype.py new file mode 100644 index 0000000000000000000000000000000000000000..bf6af1948eca3e0a16a07f731fa838f87404fdd5 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/out_dtype.py @@ -0,0 +1,164 @@ +# mypy: allow-untyped-defs + +import torch +import torch.utils._pytree as pytree +from torch._C import DispatchKey +from torch._higher_order_ops.utils import autograd_not_implemented +from torch._ops import HigherOrderOperator +from torch._prims_common import elementwise_dtypes, ELEMENTWISE_TYPE_PROMOTION_KIND +from torch._subclasses.fake_tensor import FakeTensorMode +from torch.fx.experimental.proxy_tensor import ( + disable_proxy_modes_tracing, + maybe_handle_decomp, + ProxyTorchDispatchMode, + track_tensor_tree, +) + + +# TODO to figure out a more generic approach +ALLOWABLE_OPS = [ + torch.ops.aten.linear.default, + torch.ops.aten.mm.default, + torch.ops.aten.conv2d.default, + torch.ops.aten.convolution.default, + torch.ops.aten.mul.Tensor, + torch.ops.aten.mul.Scalar, + torch.ops.aten.div.Tensor, + torch.ops.aten.div.Scalar, +] + + +class OutDtypeOperator(HigherOrderOperator): + """ + The out_dtype operator takes an existing ATen functional operator, an + `out_dtype` argument, and arguments to the original operator, and executes + the original operator and returns a Tensor with the `out_dtype` precision. + This operator does not mandate a compute precision so it allows the + representation to not be opinionated about the exact implementation. + + The general implementation for all operators will be the following: + 1. Promote inputs dtypes based on default PyTorch dtype promotion rules, + using the dtypes of all input Tensors/Scalars and the `out_dtype` + arugument. + 2. Execute the operator + 3. Cast the output to `out_dtype` + """ + + def __init__(self) -> None: + super().__init__("out_dtype") + + def __call__(self, op, output_dtype, *args): + if not isinstance(op, torch._ops.OpOverload): + raise ValueError("out_dtype's first argument must be an OpOverload") + if op._schema.is_mutable: + raise ValueError( + "out_dtype's first argument needs to be a functional operator" + ) + if not ( + len(op._schema.returns) == 1 + and isinstance(op._schema.returns[0].type, torch.TensorType) + ): + raise ValueError( + "out_dtype's can only apply to ops that return a single tensor" + f"Instead got {[r.type for r in op._schema.returns]}" + ) + + if op not in ALLOWABLE_OPS: + raise ValueError( + f"out_dtype only allows the following operators: {ALLOWABLE_OPS}." + ) + + # pyrefly: ignore [missing-attribute] + res = super().__call__(op, output_dtype, *args) + + return res + + +out_dtype = OutDtypeOperator() + + +def trace_out_dtype(proxy_mode, func_overload, op, output_dtype, *args): + # NB: Long-term we should put the decomposition logic into + # ProxyTorchDispatchMode so that people do not need to call maybe_handle_decomp + # in all HigherOrderOp proxy implementations. + r = maybe_handle_decomp(proxy_mode, func_overload, (op, output_dtype, *args), {}) + if r is not NotImplemented: + return r + + with disable_proxy_modes_tracing(): + # This is a simplified implementation of this operator just for tracing. + # Actual implementation may also first promote the arguments + out = op(*args).to(dtype=output_dtype) + + node_args = (op, output_dtype, *args) + proxy_args = pytree.tree_map(proxy_mode.tracer.unwrap_proxy, node_args) + out_proxy = proxy_mode.tracer.create_proxy( + "call_function", func_overload, proxy_args, {}, name="out_dtype" + ) + return track_tensor_tree(out, out_proxy, constant=None, tracer=proxy_mode.tracer) + + +@out_dtype.py_impl(DispatchKey.CompositeExplicitAutograd) +def out_dtype_dense(op: torch._ops.OpOverload, output_dtype: torch.dtype, *args): + if is_int_mm(op, output_dtype, args): + return torch._int_mm(*args) + return out_dtype_fallback(op, output_dtype, *args) + + +def is_int_mm(op, output_dtype, args): + return ( + op is torch.ops.aten.mm.default + and output_dtype == torch.int32 + and len(args) == 2 + and args[0].dtype == torch.int8 + and args[1].dtype == torch.int8 + and (args[0].is_cuda or args[0].is_xpu) + and (args[1].is_cuda or args[1].is_xpu) + ) + + +def out_dtype_fallback(op, output_dtype, *args): + flat_inputs = pytree.arg_tree_leaves(*args) + [torch.ones(1, dtype=output_dtype)] + promote_dtype: torch.dtype = elementwise_dtypes( + *flat_inputs, + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.DEFAULT, + )[0] + + casted_args = pytree.tree_map_only( + torch.Tensor, lambda arg: arg.to(dtype=promote_dtype), args + ) + res = op(*casted_args).to(dtype=output_dtype) + return res + + +out_dtype.py_autograd_impl(autograd_not_implemented(out_dtype, deferred_error=True)) + + +@out_dtype.py_impl(ProxyTorchDispatchMode) +def out_dtype_proxy( + mode: ProxyTorchDispatchMode, + op: torch._ops.OpOverload, + output_dtype: torch.dtype, + *args, +): + return trace_out_dtype(mode, out_dtype, op, output_dtype, *args) + + +@out_dtype.py_impl(FakeTensorMode) +def out_dtype_fake_tensor_mode( + mode: FakeTensorMode, + op: torch._ops.OpOverload, + output_dtype: torch.dtype, + *args, +): + with mode: + return out_dtype_dense(op, output_dtype, *args) + + +@out_dtype.py_functionalize_impl +def out_dtype_func(ctx, op, output_dtype, *args): + unwrapped_args = tuple(ctx.unwrap_tensors(arg) for arg in args) + + with ctx.redispatch_to_next(): + res = out_dtype(op, output_dtype, *unwrapped_args) + return ctx.wrap_tensors(res) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/partitioner.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/partitioner.py new file mode 100644 index 0000000000000000000000000000000000000000..3517ca2b35127db7297e0136745b448a56f60760 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/partitioner.py @@ -0,0 +1,380 @@ +import logging +from collections.abc import Callable +from typing import Any + +import torch +from torch._higher_order_ops.utils import create_bw_fn, materialize_as_graph + + +logger: logging.Logger = logging.getLogger(__name__) + +logger.setLevel(logging.DEBUG) + + +def _find_hop_subgraph_outputs(gm: torch.fx.GraphModule) -> tuple[torch.fx.Node]: + output_node_args = gm.graph.find_nodes(op="output")[0].args + if not isinstance(output_node_args, tuple): + raise AssertionError( + f"expected output_node_args to be tuple, got {type(output_node_args)}" + ) + return output_node_args[0] + + +def is_complex_expr(expr: Any) -> bool: + return not expr.is_symbol and not expr.is_constant() + + +class HopPartitionedGraph: + def __init__( + self, + fw_gm: torch.fx.GraphModule, + bw_gm: torch.fx.GraphModule, + n_fw_outputs: int, + n_intermediates: int, + no_complex_exprs_at_boundary: bool, + ): + self.fw_gm = fw_gm + self.bw_gm = bw_gm + self.n_fw_outputs = n_fw_outputs + self.n_intermediates = n_intermediates + self.no_complex_exprs_at_boundary = no_complex_exprs_at_boundary + self._reorder_fw_output() + self._check_partition_boundary() + + def _check_partition_boundary(self) -> None: + """check partitioned graph is in valid state.""" + invalid_reasons = [] + fw_outputs = _find_hop_subgraph_outputs(self.fw_gm) + for i, out in enumerate(fw_outputs): + if "val" not in out.meta: + invalid_reasons.append(f"fw_gm output[{i}] doesn't have a 'val' meta.") + elif not isinstance(out.meta["val"], (torch.SymInt, torch.Tensor)): + invalid_reasons.append( + f"fw_gm output[{i}] is of type {type(out.meta['val'])} but only SymInt or Tensor are allowed." + ) + + elif ( + isinstance(out.meta["val"], torch.SymInt) + and is_complex_expr(out.meta["val"].node.expr) + and self.no_complex_exprs_at_boundary + ): + invalid_reasons.append( + f"fw_gm output[{i}] must be of type SymInt with basic symbols or " + f"Tensor but got {type(out.meta['val'])} {out.meta['val']}" + ) + + if len(fw_outputs) != self.n_fw_outputs + self.n_intermediates: + invalid_reasons.append( + f"len(fw_outputs) ({len(fw_outputs)}) != n_fw_outputs ({self.n_fw_outputs}) + n_intermediates ({self.n_intermediates})" # noqa: B950 + ) + + bw_phs = list(self.bw_gm.graph.find_nodes(op="placeholder")) + + if len(fw_outputs) != len(bw_phs): + invalid_reasons.append( + f"Expect number of fw_gm's output to be the same as bw_gm's input but " + f"fw_gm has {len(fw_outputs)} outputs, bw_gm takes {len(bw_phs)} inputs." + ) + + original_forward_outputs = fw_outputs[: self.n_fw_outputs] + fw_intermediates = fw_outputs[self.n_fw_outputs :] + + bw_intermediates = bw_phs[: -self.n_fw_outputs] + bw_grads = bw_phs[-self.n_fw_outputs :] + + def _match_size_or_expr( + val1: torch.SymInt | torch.Tensor, + val2: torch.SymInt | torch.Tensor, + ) -> bool: + if type(val1) is not type(val2): + return False + + if isinstance(val1, torch.SymInt) and isinstance(val2, torch.SymInt): + return val1.node.expr == val2.node.expr + elif isinstance(val1, torch.Tensor) and isinstance(val2, torch.Tensor): + return val1.size() == val2.size() + + return False + + for fw, bw in zip(fw_intermediates, bw_intermediates): + if fw.name != bw.name or not _match_size_or_expr( + fw.meta["val"], bw.meta["val"] + ): + invalid_reasons.append("fw intermediates don't match bw intermediates") + + for fw_out, bw_grad in zip(original_forward_outputs, bw_grads): + if not _match_size_or_expr(fw_out.meta["val"], bw_grad.meta["val"]): + invalid_reasons.append("fw outputs don't match bw gradients") + + if len(invalid_reasons) > 0: + newline = "\n" + raise RuntimeError( + f"Invalid HopPartitionedGraph. Reasons:\n{newline.join(invalid_reasons)}" + ) + + def _reorder_fw_output(self) -> None: + """ + Before the pass, fw_gm returns (*fw_outputs, *intermediates1) + and bw_gm takes (*intermediates2, *grad_fw_outputs) as input. + intermediates1 and intermediates2 share the same node names but + they might be in different order. E.g. this could happen if there + are inputs that contain symints. + + To simplify downstream processing, this graph pass normalizes the output of fw_gm + to be consistent with the bacwkard inputs: + + fw_gm: + - input: fw_args + - output: (*fw_outputs, *intermediates) + + bw_gm: + - input: (*intermediates, *grad_fw_outputs) + - output: grad_fw_args + + Example: + + def fw_gm(x, y, z): + a, b, c = f(x), g(y), k(z) + return a, b, c, f_tmp, g_tmp, k_tmp + + , where a, b, c are fw_outputs, f_tmp, g_tmp, k_tmp are intermediates + + The corresponding bw_gm has the following signature: + + def bw_gm(f_tmp, g_tmp, k_tmp, grad_a, grad_b, grac): + return grad_x, grad_y, grad_z + """ + fw_gm_output_nodes = _find_hop_subgraph_outputs(self.fw_gm) + fw_outputs_nodes = fw_gm_output_nodes[: self.n_fw_outputs] + fw_intermediates_nodes = fw_gm_output_nodes[self.n_fw_outputs :] + if len(fw_intermediates_nodes) > 0: + fw_intermediates_name_to_node = {n.name: n for n in fw_intermediates_nodes} + + # First n_intermediates placeholders + bw_names: list[str] = [ + ph.name + for ph in list(self.bw_gm.graph.find_nodes(op="placeholder"))[ + : self.n_intermediates + ] + ] + new_fw_outputs = list(fw_outputs_nodes) + [ + fw_intermediates_name_to_node[name] for name in bw_names + ] + + output_node = self.fw_gm.graph.find_nodes(op="output")[0] + output_node.args = (tuple(new_fw_outputs),) + + self.fw_gm.graph.lint() + self.fw_gm.recompile() + + +class HopJointGraph: + def __init__( + self, + joint_gm: torch.fx.GraphModule, + n_primals: int, + n_fw_outputs: int, + *, + functionalized: bool, + ): + self.joint_gm = joint_gm + self.n_primals = n_primals + self.n_fw_outputs = n_fw_outputs + self.functionalized = functionalized + + self._rename_phs() + self._remove_redundant_sym_size_ops() + + def _rename_phs(self) -> None: + """ + Rename the placeholders for joint_gm so that the partitioner + could recognize which inputs are primals and which are tangents. + """ + self.n_tangents = 0 + for i, ph in enumerate(self.joint_gm.graph.find_nodes(op="placeholder")): + if i < self.n_primals: + ph.target = f"primals_{i}" + ph.name = f"primals_{i}" + else: + self.n_tangents += 1 + ph.target = f"tangents_{i - self.n_primals}" + ph.name = f"tangents_{i - self.n_primals}" + + self.joint_gm.graph.lint() + self.joint_gm.compile() + + def _remove_redundant_sym_size_ops(self) -> None: + """ + Deletes torch.ops.sym_size.int operators whose output is a + corresponding placeholder that holds the same symbol, and replace all usage + of the sym_size node to be directly using the placeholders. + + This is to make sure all basic symbols come from inputs. + """ + placeholder_exprs = {} + for node in self.joint_gm.graph.nodes: + if ( + isinstance(node, torch.fx.Node) + and node.op == "placeholder" + and hasattr(node, "meta") + and "val" in node.meta + ): + val = node.meta["val"] + if isinstance(val, torch.SymInt): + placeholder_exprs[val.node.expr] = node + + nodes_to_remove = [] + for node in self.joint_gm.graph.find_nodes( + op="call_function", target=torch.ops.aten.sym_size.int + ): + if not (hasattr(node, "meta") and "val" in node.meta): + raise AssertionError( + f"node {node} must have 'meta' attribute with 'val' key" + ) + val = node.meta["val"] + expr = val.node.expr + if expr in placeholder_exprs: + placeholder_node = placeholder_exprs[expr] + node.replace_all_uses_with(placeholder_node) + nodes_to_remove.append(node) + + for node in nodes_to_remove: + self.joint_gm.graph.erase_node(node) + + self.joint_gm.graph.lint() + self.joint_gm.recompile() + + def _mark_complex_exprs_as_must_recompute(self) -> None: + """ + For control flow operators such as scan, we don't want to + have symint in the partitioning boundaries because otherwise we would need to support stacking + the symints up, which causes more entropy in the stack. + + By marking the recompute polify for complex nodes as MUST_RECOMPUTE, the partitioning boundary + no longer contains complex expressions. + + Note that this pass doesn't exclude basic symbols from partitioning boundary + and it's up to the downstream to decide whether to return the basic symbol + or have a separate graph pass to remove them. + """ + + from torch._functorch.partitioners import CheckpointPolicy + + for n in ( + node for node in self.joint_gm.graph.nodes if node.op == "call_function" + ): + if "val" not in n.meta: + continue + val = n.meta["val"] + if isinstance(val, torch.SymInt) and is_complex_expr(val.node.expr): + if n.meta.get("recompute", None) is not None: + raise AssertionError( + f"node {n} with complex SymInt expression should not have recompute policy set" + ) + + n.meta["recompute"] = CheckpointPolicy.MUST_RECOMPUTE + + self.joint_gm.graph.lint() + self.joint_gm.recompile() + + def partition( + self, partition_fn: Callable, always_recompute_complex_exprs: bool + ) -> HopPartitionedGraph: + if logger.isEnabledFor(logging.DEBUG): + logger.debug( + "before min_cut_partition:\n%s", + self.joint_gm.print_readable(print_output=False), + ) + + if always_recompute_complex_exprs: + self._mark_complex_exprs_as_must_recompute() + + fw_gm, bw_gm = partition_fn( + self.joint_gm, None, num_fwd_outputs=self.n_fw_outputs + ) + + if logger.isEnabledFor(logging.DEBUG): + logger.debug("after partition_fn:") + logger.debug("fw_gm:\n%s", fw_gm.print_readable(print_output=False)) + logger.debug("bw_gm:\n%s", bw_gm.print_readable(print_output=False)) + + n_intermediates = len(_find_hop_subgraph_outputs(fw_gm)) - self.n_fw_outputs + + return HopPartitionedGraph( + fw_gm, + bw_gm, + self.n_fw_outputs, + n_intermediates, + always_recompute_complex_exprs, + ) + + +def create_hop_joint_graph( + fw_fn: Callable, + fw_args: tuple[torch.Tensor | torch.SymInt, ...], + functionalize: bool, +) -> HopJointGraph: + fw_gm = materialize_as_graph(fw_fn, fw_args, force_enable_grad=True) + fw_gm_output_nodes = _find_hop_subgraph_outputs(fw_gm) + + if not all( + isinstance(n, torch.fx.Node) and "val" in n.meta for n in fw_gm_output_nodes + ): + raise AssertionError( + "all fw_gm output nodes must be torch.fx.Node with 'val' in meta" + ) + fw_gm_output_vals = tuple(n.meta["val"] for n in fw_gm_output_nodes) # type: ignore[arg-type] + + if not all(isinstance(val, torch.Tensor) for val in fw_gm_output_vals): + raise AssertionError( + f"all fw_gm output values must be torch.Tensor, got {[type(v) for v in fw_gm_output_vals]}" + ) + example_grads = tuple(torch.zeros_like(val) for val in fw_gm_output_vals) + + joint_fn = create_bw_fn(fw_fn, fw_args, return_fw_outputs=True) + joint_gm = materialize_as_graph( + joint_fn, fw_args + example_grads, force_enable_grad=True + ) + if functionalize: + # Need to first trace out the joint_fn with autograd info on + # then functionalize the graph otherwise the grad information is lost + joint_gm = materialize_as_graph( + # pyrefly: ignore [bad-argument-type] + torch.func.functionalize(joint_gm, remove="mutations_and_views"), + fw_args + example_grads, + ) + + return HopJointGraph( + joint_gm, + len(fw_args), + len(fw_gm_output_nodes), + functionalized=functionalize, + ) + + +class HopGraphMinCutPartitioner: + @staticmethod + def create_partitioned_graph( + fw_fn: Callable, + fw_args: tuple[torch.Tensor | torch.SymInt, ...], + *, + always_recompute_complex_exprs: bool = False, + ) -> HopPartitionedGraph: + """ + Inputs: + - fw_fn: the forward function that we'll use to create a joint graph and partition + - fw_args: the flat_args to fw_fn + - always_recompute_complex_exprs: when set to True, the bw_gm will do a re-compute + for inputs that are complex expressions such that the partitioning boundary + only consists of basic symbols and tensors. + + Returns a HopPartitionedGraph + """ + from torch._functorch.partitioners import min_cut_rematerialization_partition + + joint_graph: HopJointGraph = create_hop_joint_graph( + fw_fn, fw_args, functionalize=True + ) + return joint_graph.partition( + min_cut_rematerialization_partition, always_recompute_complex_exprs + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/passes/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/passes/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/passes/inline_invoke_subgraph.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/passes/inline_invoke_subgraph.py new file mode 100644 index 0000000000000000000000000000000000000000..54b2c4ab81b3178d5e677be6c771c2df071edf5b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/passes/inline_invoke_subgraph.py @@ -0,0 +1,69 @@ +import operator +from typing import Any, TYPE_CHECKING + +import torch +from torch.fx.graph_module import GraphModule + + +if TYPE_CHECKING: + from torch.fx.node import Node + + +def inline_invoke_subgraph(gm: GraphModule) -> GraphModule: + """Inline all invoke_subgraph HOPs, producing a flat FX graph. + + This is useful when downstream compilers (like vllm-compile) don't support + HOPs or prefer a flat graph, but we still want the Dynamo tracing-time + benefits of auto-caching (trace once, stamp out cached calls). + """ + invoke_nodes = list( + gm.graph.find_nodes( + op="call_function", target=torch.ops.higher_order.invoke_subgraph + ) + ) + if not invoke_nodes: + return gm + + # Recursively inline any nested invoke_subgraph calls inside + # subgraph modules themselves. + for name, mod in gm.named_modules(): + if name and isinstance(mod, GraphModule): + inline_invoke_subgraph(mod) + + for node in invoke_nodes: + get_attr_node = node.args[0] + # args[1] is the identifier string, args[2:] are operands + operands = node.args[2:] + + subgraph: GraphModule = getattr(gm, get_attr_node.target) + + # Build mapping from subgraph placeholder nodes -> parent operands + env: dict[Node, Any] = dict( + zip(subgraph.graph.find_nodes(op="placeholder"), operands) + ) + + # Copy subgraph nodes into parent graph, inserting before the + # invoke_subgraph node. + with gm.graph.inserting_before(node): + for sub_node in subgraph.graph.nodes: + if sub_node.op in ("placeholder", "output"): + continue + env[sub_node] = gm.graph.node_copy(sub_node, lambda n: env[n]) + + output_values = subgraph.graph.output_node().args[0] + + # Replace getitem users of the invoke_subgraph result with the + # corresponding inlined output. + for user in list(node.users): + if user.op == "call_function" and user.target is operator.getitem: + idx = user.args[1] + user.replace_all_uses_with(env[output_values[idx]]) # pyrefly: ignore + gm.graph.erase_node(user) + + gm.graph.erase_node(node) + + # Remove the get_attr node if it has no other users. + if not get_attr_node.users: + gm.graph.erase_node(get_attr_node) + + return gm diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/print.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/print.py new file mode 100644 index 0000000000000000000000000000000000000000..0399400acb4bbae664599bfdf78f58dcfcf57765 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/print.py @@ -0,0 +1,153 @@ +import builtins + +import torch +import torch.utils._pytree as pytree +from torch._ops import HigherOrderOperator +from torch._subclasses.fake_tensor import FakeTensorMode +from torch.fx.experimental.proxy_tensor import ProxyTorchDispatchMode + + +class Print(HigherOrderOperator): + """ + print(format_str, *args, **kwargs) -> None + + This Higher Order Operator (HOP) provides a functional version of print for use in PyTorch graphs. + It supports the calling conventions of print(format_str.format(*args, **kwargs)): + + 1. Format string with keyword arguments (named placeholders): + torch._higher_order_ops.print("moo {x} {y}", x=1, y=2) + Output: "moo 1 2" + + 2. Format string with positional arguments (positional placeholders): + torch._higher_order_ops.print("moo {} {}", 1, 2) + Output: "moo 1 2" + + 3. Mixed positional and keyword arguments: + torch._higher_order_ops.print("moo {} {y}", 1, y=2) + Output: "moo 1 2" + + 4. DTensor support: + DTensor args are unwrapped to local tensors via to_local() (no collective). + Each rank prints its own local view, prefixed with [rank N]. + For the global view of a sharded tensor, call full_tensor() before + passing to print. + + dt = DTensor.from_local(local_shard, mesh, [Shard(0)]) + torch._higher_order_ops.print("activations: {}", dt) + # Output: [rank 0] activations: tensor([0., 1.]) + + This HOP enables printing without causing graph break. + """ + + def __init__(self) -> None: + super().__init__("print") + + def __call__(self, format_str: str, *args: object, **kwargs: object) -> None: + if not isinstance(format_str, str): + raise AssertionError(f"format_str must be a string, got {type(format_str)}") + # pyrefly: ignore [missing-attribute] + return super().__call__(format_str, *args, **kwargs) + + # pyrefly: ignore [bad-override] + def gen_schema( + self, format_str: str, *args: object, **kwargs: object + ) -> torch.FunctionSchema: + from torch._higher_order_ops.schema import HopSchemaGenerator + + schema_gen = HopSchemaGenerator(self) + schema_gen.add_arg("format_str", format_str[0]) + + # Add each positional arg + for i, value in enumerate(args): + schema_gen.add_arg(f"arg{i}", value) + + # Add each kwarg as a keyword-only argument + for key, value in kwargs.items(): + schema_gen.add_arg(key, value, kw_only=True) + + schema_gen.add_schema_tree_spec(format_str, *args, **kwargs) + + return schema_gen.gen_schema() + + +print = Print() + + +@print.py_impl(ProxyTorchDispatchMode) +# pyre-ignore +def print_proxy_torch_dispatch_mode( + mode: ProxyTorchDispatchMode, format_str: str, *args: object, **kwargs: object +) -> None: + proxy_args = pytree.tree_map(mode.tracer.unwrap_proxy, args) # type: ignore[union-attr] + proxy_kwargs = pytree.tree_map(mode.tracer.unwrap_proxy, kwargs) # type: ignore[union-attr] + mode.tracer.create_proxy( + "call_function", print, (format_str, *proxy_args), proxy_kwargs + ) + + +@print.py_impl(FakeTensorMode) +# pyre-ignore +def print_fake_tensor_mode(mode, format_str: str, *args: object, **kwargs: object): + return None + + +@print.py_impl(torch._C.DispatchKey.CompositeExplicitAutograd) +# pyre-ignore +def print_impl(format_str: str, *args: object, **kwargs: object) -> None: + # Ensure all immutable_dict/list in args and kwargs are converted to regular dict/list + map_types: dict[type, type] = { + torch.fx.immutable_collections.immutable_dict: dict, + torch.fx.immutable_collections.immutable_list: list, + } + new_args, new_kwargs = pytree.tree_map_only( + tuple(map_types.keys()), + lambda a: map_types[type(a)](a), + (args, kwargs), + lambda a: isinstance(a, tuple(map_types.keys())), + ) + # Use built-in print to avoid recursion with the HOP print + builtins.print(format_str.format(*new_args, **new_kwargs)) + + +print.fallthrough(torch._C.DispatchKey.AutogradCPU) +print.fallthrough(torch._C.DispatchKey.AutogradCUDA) + + +def _register_dtensor_impl() -> None: + from torch.distributed.tensor import DTensor + + @print.py_impl(DTensor) # pyrefly: ignore [missing-attribute] + # pyre-ignore + def print_dtensor(format_str: str, *args: object, **kwargs: object) -> None: + # Unwrap DTensors to local tensors via to_local() — no collective is + # introduced so there is no OOM or performance risk. Every rank prints + # its own local view (including Replicate, where to_local() already + # holds the full tensor). + # + # The output is prefixed with [rank N] so users can identify which rank + # produced each line. + # + # If the user needs the global view of a sharded tensor, they can call + # full_tensor() explicitly before passing it to print. + import torch.distributed as dist + + local_args = pytree.tree_map_only(DTensor, DTensor.to_local, args) + local_kwargs = pytree.tree_map_only(DTensor, DTensor.to_local, kwargs) + if dist.is_initialized() and dist.get_world_size() > 1: + format_str = f"[rank {dist.get_rank()}] {format_str}" + print( # pyrefly: ignore [no-matching-overload] + format_str, *local_args, **local_kwargs + ) + + +@print.py_functionalize_impl +def print_func(ctx, format_str: str, *args: object, **kwargs: object): + from torch._higher_order_ops.effects import handle_effects + + return handle_effects( + ctx.mode._allow_token_discovery, + ctx.mode._tokens, + print, # type: ignore[arg-type] + (format_str, *args), + kwargs, # type: ignore[arg-type] + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/run_const_graph.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/run_const_graph.py new file mode 100644 index 0000000000000000000000000000000000000000..7ac135859f2f8bb9f0727a663c268055bdc1c43c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/run_const_graph.py @@ -0,0 +1,85 @@ +from typing import Any, TYPE_CHECKING + +import torch +from torch._C import DispatchKey +from torch._higher_order_ops.utils import autograd_not_implemented +from torch._ops import HigherOrderOperator +from torch._subclasses.fake_tensor import FakeTensorMode + + +if TYPE_CHECKING: + from torch._subclasses.functional_tensor import BaseFunctionalizeAPI + +from torch.fx.experimental.proxy_tensor import ProxyTorchDispatchMode, track_tensor_tree +from torch.utils import _pytree as pytree + + +class RunConstGraph(HigherOrderOperator): + def __init__(self) -> None: + super().__init__("run_const_graph") + + def __call__(self, graph: torch.fx.GraphModule, args: tuple[object, ...]) -> object: + # pyrefly: ignore [missing-attribute] + return super().__call__(graph, args) + + +run_const_graph = RunConstGraph() + + +@run_const_graph.py_impl(ProxyTorchDispatchMode) +def run_const_graph_dispatch_mode( + mode: ProxyTorchDispatchMode, graph: torch.fx.GraphModule, args: tuple[object, ...] +) -> object: + const_gm, weights = graph, args + p_args = pytree.tree_map(mode.tracer.unwrap_proxy, (graph, args)) # type: ignore[union-attr] + if not isinstance(const_gm, torch.fx.GraphModule): + raise AssertionError( + f"expected const_gm to be torch.fx.GraphModule, got {type(const_gm)}" + ) + if hasattr(mode.tracer.root, "_const_graph"): # type: ignore[union-attr] + raise AssertionError("mode.tracer.root already has _const_graph attribute") + mode.tracer.root.register_module("_const_graph", const_gm) # type: ignore[union-attr] + + proxy = mode.tracer.create_proxy("call_function", run_const_graph, p_args, {}) + + out = const_gm(*weights) + return track_tensor_tree(out, proxy, constant=None, tracer=mode.tracer) + + +@run_const_graph.py_functionalize_impl +def run_const_graph_functional( + ctx: "BaseFunctionalizeAPI", graph: torch.fx.GraphModule, args: tuple[Any, ...] +) -> Any: + unwrapped_args = ctx.unwrap_tensors(args) + + with ctx.redispatch_to_next(): + out = run_const_graph(graph, unwrapped_args) + return ctx.wrap_tensors(out) # type: ignore[arg-type] + + +run_const_graph.py_autograd_impl( + autograd_not_implemented(run_const_graph, deferred_error=True) +) + + +@run_const_graph.py_impl(FakeTensorMode) +def run_const_graph_fake_tensor_mode( + mode: FakeTensorMode, graph: torch.fx.GraphModule, args: tuple[object, ...] +) -> object: + if not isinstance(graph, torch.fx.GraphModule): + raise AssertionError( + f"expected graph to be torch.fx.GraphModule, got {type(graph)}" + ) + with mode: + return graph(*args) + + +@run_const_graph.py_impl(DispatchKey.CPU) +def run_const_graph_cpu( + graph: torch.fx.GraphModule, args: tuple[object, ...] +) -> object: + if not isinstance(graph, torch.fx.GraphModule): + raise AssertionError( + f"expected graph to be torch.fx.GraphModule, got {type(graph)}" + ) + return graph(*args) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/scan.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/scan.py new file mode 100644 index 0000000000000000000000000000000000000000..0d7015f2663169be60f2c3847df11f9202587a56 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/scan.py @@ -0,0 +1,994 @@ +# mypy: allow-untyped-defs +import enum +import functools +import itertools +import logging +from collections.abc import Callable +from typing import Any + +import torch +import torch._prims_common as utils +import torch.utils._pytree as pytree +from torch._C import DispatchKey +from torch._higher_order_ops.partitioner import ( + _find_hop_subgraph_outputs, + HopGraphMinCutPartitioner, + HopPartitionedGraph, +) +from torch._higher_order_ops.utils import ( + _maybe_compile_and_run_fn, + check_input_alias_and_mutation_return_outputs, + check_meta_consistency, + fill_none_with_masks, + filter_with_masks, + first_slice_copy, + get_tensor_mask, + mask_list, + materialize_as_graph, + reenter_make_fx, + split_into_chunks, + unique_graph_id, + validate_subgraph_args_types, +) +from torch._ops import HigherOrderOperator +from torch._subclasses.fake_tensor import FakeTensorMode +from torch.fx.experimental.proxy_tensor import ( + disable_proxy_modes_tracing, + ProxyTorchDispatchMode, + track_tensor_tree, +) +from torch.utils._python_dispatch import _get_current_dispatch_mode + + +logger: logging.Logger = logging.getLogger(__name__) +aten = torch._ops.ops.aten + + +def wrap_combine_fn_flat( + *args, combine_fn, spec_init, spec_xs, num_init_leaves, num_inp_leaves +): + if len(args) != (num_init_leaves + num_inp_leaves): + raise AssertionError( + f"combine_fn received wrong number of arguments, expected {num_init_leaves + num_inp_leaves}, but got {len(args)}" + ) + carry = pytree.tree_unflatten(args[:num_init_leaves], spec_init) + xs = pytree.tree_unflatten(args[num_init_leaves:], spec_xs) + return combine_fn(carry, xs) + + +def _extract_carry_and_out(flat_out: list[Any], num_carry: int): + return split_into_chunks(flat_out, [num_carry, len(flat_out) - num_carry]) + + +# We also do a clone with contiguous_format. This is to be consistent with +# eager semantic of scan, which stacks the outputs. The result is contiguous +# as a result of the stack operation. +def stack_y(y: torch.Tensor, scan_length: int) -> torch.Tensor: + return ( + y.unsqueeze(0) + .repeat(*([scan_length] + [1] * y.ndim)) + .clone(memory_format=torch.contiguous_format) + ) + + +def call_operator(operator, *args): + return pytree.tree_leaves(operator(*args)) + + +def scan( + combine_fn: Callable[ + [pytree.PyTree, pytree.PyTree], tuple[pytree.PyTree, pytree.PyTree] + ], + init: pytree.PyTree, + xs: pytree.PyTree, + *, + dim: int = 0, + reverse: bool = False, +) -> tuple[pytree.PyTree, pytree.PyTree]: + r""" + Performs an inclusive scan with a combine function. + + .. warning:: + + ``torch.scan`` is a prototype feature in PyTorch. You may run into miscompiles. + Read more about feature classification at: + https://pytorch.org/blog/pytorch-feature-classification-changes/#prototype + + Args: + combine_fn (Callable): A binary callable with type ``(Tensor, Tensor) -> (Tensor, Tensor)``, + or if xs is a pytree ``(pytree, pytree) -> (pytree, pytree)``. + The first input to ``combine_fn`` is the previous or initial scan carry + and the second input element to ``combine_fn`` is a slice of the input along dim. + The first output element of ``combine_fn`` is the next scan carry + and the second output of ``combine_fn`` represents a slice of the output. + This function must be pure, i.e., no lifted arguments are supported at the moment + and may not have any side effects. + init (torch.Tensor or pytree with tensor leaves): The initial scan carry, a tensor, or nested pytree of tensors. + The ``init`` is expected to have the same pytree structure as the first output element (i.e. carry) + of ``combine_fn``. + xs (torch.Tensor or pytree with tensor leaves): The input tensor, or nested pytree of tensors. + + Kwargs: + dim (int): the dimension to scan over, default 0. + reverse (bool): A boolean stating if the scan should be reversed with respect to ``dim``, default ``False``. + + Returns: + final_carry (torch.Tensor or pytree with tensor leaves), + the final carry of the scan operation with same pytree structure as init. + out (torch.Tensor or pytree with tensor leaves), + each tensor leaf is a stacked output along first dim, where each slice is the output of a scan iteration. + + Restrictions: + - The combine_fn shouldn't have any aliasing between input-input, input-output, and output-output. E.g. return a view + or the same tensor as input is not supported. As a workaround, can clone the output to avoid aliasing. + + - The combine_fn shouldn't mutate any inputs. We'll remove the mutation restriction for inference soon. Please file an issue + if you input mutation support for training is needed. + + - The combine_fn's init carry should match the next_carry in pytree structure and in tensor metadata. + + Example:: + + def add(x: torch.Tensor, y: torch.Tensor): + next_carry = y = x + y + # clone the output to avoid output-output aliasing + return next_carry, y.clone() + + + i0 = torch.zeros(1) + xs = torch.arange(5) + # returns torch.tensor([10.]), torch.tensor([[0], [1.], [3.], [6.], [10.]]) + last_carry, cumsum = scan(add, init=i0, xs=xs) + + + """ + # The reason we flatten init and xs before calling into dynamo is that + # we want to create a consistent input ordering for combine_fn + # and we also want to the input ordering matches the output ordering. + leaves_init, spec_init = pytree.tree_flatten(init) + leaves_xs_orig, spec_xs = pytree.tree_flatten(xs) + + # Shortcut if no xs is provided + if len(leaves_xs_orig) == 0: + return init, [] + + def _validate_input(cfn, lxs, linit, d, r): + # Basic arguments check + if not callable(cfn): + raise RuntimeError(f"Combine_fn must be a callable, but got {cfn}") + if not isinstance(d, int): + raise RuntimeError("Dim must be an int, but got " + str(type(d))) + if not isinstance(r, bool): + raise RuntimeError("Reverse must be a bool, but got " + str(type(r))) + + # Checks for init + if len(linit) == 0: + raise RuntimeError("scan() operator requires init leaves.") + for x in linit: + if not isinstance(x, torch.Tensor): + raise RuntimeError(f"All init leaves must be a Tensor but got {x}") + + # Checks for xs + for x in lxs: + if not isinstance(x, torch.Tensor): + raise RuntimeError(f"All xs leaves must be a Tensor but got {x}") + if any(x.ndim <= d for x in lxs): + raise RuntimeError( + "All xs leaves must at least have 'dim' number of dimensions and scan dimension > 0" + ) + if any(x.shape[d] == 0 for x in lxs): + raise RuntimeError( + "All xs leaves must at least have 'dim' number of dimensions and scan dimension > 0" + ) + + ndim = leaves_xs_orig[0].ndim + dim = utils.canonicalize_dim(ndim, dim) + + _validate_input(combine_fn, leaves_xs_orig, leaves_init, dim, reverse) + + # Move scan dim to 0 and always perform scan on dim 0 + leaves_xs = [] + for elem in leaves_xs_orig: + leaves_xs.append(torch.movedim(elem, dim, 0) if dim != 0 else elem) + + if reverse: + leaves_xs = [torch.flip(elem, [0]) for elem in leaves_xs] + + # TODO: Support _inductor lowering + # TODO: Unify handling of pytrees for control flow ops, such as cond, while_loop, etc. + + combine_fn = functools.partial( + wrap_combine_fn_flat, + combine_fn=combine_fn, + spec_init=spec_init, + spec_xs=spec_xs, + num_init_leaves=len(leaves_init), + num_inp_leaves=len(leaves_xs), + ) + + def run_flattened_scan(combine_fn, leaves_init, leaves_xs): + return scan_op(combine_fn, leaves_init, leaves_xs, additional_inputs=()) + + carry, out = _maybe_compile_and_run_fn( + run_flattened_scan, + combine_fn, + leaves_init, + leaves_xs, + ) + + if reverse: + out = pytree.tree_map(lambda elem: elem.flip([0]), out) + + return carry, out + + +class ScanOp(HigherOrderOperator): + def __init__(self): + super().__init__("scan") + + def __call__(self, combine_fn, init, xs, additional_inputs): + # There is currently an issue that the ScanOp is sometimes called with + # the additional_inputs being a list. See https://github.com/pytorch/pytorch/issues/145785 + # Once this issue is resolved, the assertion should only allow tuples + # and the tuple cast should be removed + if not isinstance(additional_inputs, (tuple, list)): + raise AssertionError( + f"additional_inputs must be a tuple or list, got {type(additional_inputs)}" + ) + additional_inputs = ( + tuple(additional_inputs) + if isinstance(additional_inputs, list) + else additional_inputs + ) + validate_subgraph_args_types(additional_inputs) + # pyrefly: ignore [missing-attribute] + return super().__call__(combine_fn, init, xs, additional_inputs) + + # pyrefly: ignore [bad-override] + def gen_schema(self, combine_fn, init, xs, additional_inputs): + from torch._higher_order_ops.schema import HopSchemaGenerator + from torch._higher_order_ops.utils import materialize_as_graph + + all_inputs = tuple( + list(init) + [first_slice_copy(x) for x in xs] + list(additional_inputs) + ) + + combine_gm: torch.fx.GraphModule = materialize_as_graph(combine_fn, all_inputs) + + ( + _, + _, + _, + mutated_inputs, + outputs, + ) = check_input_alias_and_mutation_return_outputs(combine_gm) + if len(mutated_inputs) > 0: + raise RuntimeError( + "For scan, combine_fn cannot have in-place mutations but found " + f"{mutated_inputs}-th inputs are mutated." + ) + + schema_gen = HopSchemaGenerator(self) + schema_gen.add_arg("combine_fn", combine_gm) + + for idx, arg in enumerate(init): + schema_gen.add_arg(f"init{idx}", arg) + + for idx, arg in enumerate(xs): + schema_gen.add_arg(f"xs{idx}", arg) + + for idx, arg in enumerate(additional_inputs): + schema_gen.add_arg(f"additional_input{idx}", arg) + + for out in outputs: + schema_gen.add_output(out) + + schema_gen.add_schema_tree_spec(combine_fn, init, xs, additional_inputs) + return schema_gen.gen_schema() + + +scan_op = ScanOp() + + +def generic_scan(operator, init, xs, dim=0, additional_inputs=()): + def _scan(init, xs): + """Perform scan on `elems` using `elems_init.""" + carry = init + if len(xs) == 0: + return carry, [] + + num_elems = xs[0].shape[dim] + num_init_leaves = len(init) + + # Process element 0 to infer output shapes for pre-allocation + # AND produce the first real result in a single call. The previous + # approach used first_slice_copy() for shape inference and then + # re-processed element 0 in the main loop, calling the operator + # num_elems+1 times. That extra invocation is incorrect for + # operators with side effects. + carry, out_0 = _extract_carry_and_out( + call_operator( + operator, + *carry, + *[elem.select(dim, 0) for elem in xs], + *additional_inputs, + ), + num_init_leaves, + ) + + out_tensor_mask = get_tensor_mask(out_0) + out_0_masked = mask_list(out_tensor_mask, out_0) + + # Pre-allocate + # outs -> Output matrix + # idxs -> Index matrix for scatter_ + # out: (num_elems, M, N, ...) + # idx: (1, M, N) + outs = [ + torch.empty( + [num_elems] + list(e.size()), + dtype=e.dtype, + device=e.device, + ) + for e in out_0_masked + ] + idxs = [ + torch.ones_like(e, dtype=torch.int64).unsqueeze(0) for e in out_0_masked + ] + + def store_out_in_outs(out, ind): + # Store the intermediate out in the outs matrix + for o, x, idx in zip(outs, out, idxs): + # o: (num_elems, M, N ...) + # x: (M, N, ...) -> (1, M, N) + # ind * idx: (1, M, N,) with values to be ind + # essentially: o[ind][n][k] = x[0][n][k] + o.scatter_(0, ind * idx, x.unsqueeze(0)) + + # Store element 0's result, then continue from element 1. + store_out_in_outs(out_0_masked, 0) + + for i in range(1, num_elems): + carry, out = _extract_carry_and_out( + call_operator( + operator, + *carry, + *[elem.select(dim, i) for elem in xs], + *additional_inputs, + ), + num_init_leaves, + ) + + store_out_in_outs(mask_list(out_tensor_mask, out), i) + + # Expand outs with None depending on the tensor mask of the output + outs_expanded = [outs.pop(0) if out_m else None for out_m in out_tensor_mask] + + return (*carry, *outs_expanded) + + scans = _scan(init, xs) + return scans + + +def trace_scan( + proxy_mode, + func_overload, + combine_fn: Callable, + init: list[torch.Tensor], + xs: list[torch.Tensor], + additional_inputs: tuple[torch.Tensor], +): + from torch._dynamo.utils import clone_input + + with disable_proxy_modes_tracing(): + sample_inits = [clone_input(x_init) for x_init in init] + sample_inputs = [first_slice_copy(x) for x in xs] + sample_additional_inputs = [ + clone_input(x) if isinstance(x, torch.Tensor) else x + for x in additional_inputs + ] + combine_graph = reenter_make_fx(combine_fn)( + *sample_inits, *sample_inputs, *sample_additional_inputs + ) + + outputs = None + for node in combine_graph.graph.nodes: + if node.op == "output": + if outputs is not None: + raise AssertionError("found multiple output nodes in combine_graph") + if len(node.args) != 1: + raise AssertionError( + f"expected output node to have 1 arg, got {len(node.args)}" + ) + outputs = node.args[0] + + if outputs is None: + raise AssertionError("no output node found in combine_graph") + + carry, output = _extract_carry_and_out(outputs, len(init)) + init_fake_tensors: list[torch.Tensor | torch.SymInt | int] = [ + i.clone() for i in init + ] + carry_fake_tensors: list[torch.Tensor | torch.SymInt | int] = [ + c.meta["val"] for c in carry + ] + check_meta_consistency( + init_fake_tensors, carry_fake_tensors, "init", "carry", include_contiguity=False + ) + + _, combine_graph_name = unique_graph_id(proxy_mode, prefix="scan_combine_graph") + + proxy_mode.tracer.root.register_module(combine_graph_name, combine_graph) + + args = (combine_graph, init, xs, additional_inputs) + proxy_args = pytree.tree_map(proxy_mode.tracer.unwrap_proxy, args) + out_proxy = proxy_mode.tracer.create_proxy( + "call_function", func_overload, proxy_args, {}, name="scan" + ) + + with disable_proxy_modes_tracing(): + scan_length = xs[0].shape[0] + fake_carry, fake_outputs = _extract_carry_and_out( + [o.meta["val"] for o in outputs], len(init) + ) + out = ( + *fake_carry, + *(stack_y(t, scan_length) for t in fake_outputs), + ) + + return track_tensor_tree(out, out_proxy, constant=None, tracer=proxy_mode.tracer) + + +@scan_op.py_impl(DispatchKey.CompositeExplicitAutograd) +def scan_op_dense(combine_fn, init, xs, additional_inputs): + mode = _get_current_dispatch_mode() + if mode is not None: + raise AssertionError("Mode should never be enabled for CPU/CUDA key") + return generic_scan(combine_fn, init, xs, additional_inputs=additional_inputs) + + +class ScanAutogradOp(torch.autograd.Function): + """ + NOTE: [scan partial grad handling] + If any element of init, of xs, of the outputs or of the additional_inputs does not require gradients, + i.e., requires_grad=False, there will be still gradients returned for those elements, + but those gradients will be a tensor filled with zeros of the same shape as the element itself. + + A special case are additional_inputs that are not tensors. Such inputs can occur for example with symbolic tracing, + where the shape symbol (SymInt) becomes an additional_input. + For such cases, we compute a ``additional_inputs_tensor_mask``, which is True for elements of additional_inputs + that are tensors and False otherwise. Gradients of additional_inputs are only accumulated if this mask is True, + otherwise, the value of initial_g_additional_inputs is passed, which is None for non-Tensor values. + """ + + @staticmethod + # pyrefly: ignore [bad-override] + def forward( + ctx, + hop_partitioned_graph, + n_init, + n_xs, + n_additional_inputs, + *operands, + ): + init, xs, additional_inputs = split_into_chunks( + operands, [n_init, n_xs, n_additional_inputs] + ) + ctx._scan_impl = ScanAutogradImpl( + hop_partitioned_graph, init, xs, additional_inputs + ) + with torch._C._AutoDispatchBelowAutograd(): + return ctx._scan_impl.call_forward() + + @staticmethod + def backward(ctx, *grad_fw_outputs): + return ( + None, + None, + None, + None, + *ctx._scan_impl.call_backward(*grad_fw_outputs), + ) + + +class ScanForwardIntermediatesHandlingPolicy(enum.Enum): + """ + Partitioner can add interemdiates to the output of original graph. + These intermediates fall into 4 categories and we want to have different policies for handling them by + modifying the graph: + + CLONE: we clone the intermediate when it is a carried input (i.e. init). In this case, this carry will be + replaced with new values at each forward step so we need to clone the carry as part of return (i.e. ys) + so as to remove the aliasing and that each step's intermediate will be stacked together and saved in bacwkard. + + REMOVE_XS: we remove the intermediate from output when it is part of xs. Since xs is read-only, in this case, + we can directly save them for backward to use. + + REMOVE_ADDITIONAL_INPUTS: we remove the intermediate from output when it is part of additinonal_inputs. additional_inputs + are also read-only in each step, we can directly save them for bacwkard to use. We differentiate XS and ADDITIONAL_INPUTS + so that we could have different treatment for them in backward. In backward, we need to put xs intermediates in carry but + put additional_inputs as backward scan's additional_inputs. + + KEEP: this corresponds to a real intermediate tensor operations' output. It varies at each forward step, we could just keep + it as part of ys. + + """ + + KEEP = 0 + CLONE = 1 + REMOVE_XS = 2 + REMOVE_ADDITIONAL_INPUTS = 3 + + +class ScanAutogradImpl: + """ + Wraps over partitioned graph and encapsulates scan-specific implementation details + """ + + def __init__( + self, hop_partitioned_graph: HopPartitionedGraph, init, xs, additional_inputs + ): + self.hop_partitioned_graph = hop_partitioned_graph + self.init = init + self.xs = xs + self.additional_inputs = additional_inputs + self.forward_intermediates_handling_policies: list[ + ScanForwardIntermediatesHandlingPolicy + ] = [] + self.saved_fw_xs: list[Any] = [] + self.saved_fw_additional_inputs: list[Any] = [] + self.saved_intermediates: list[Any] = [] + self.fw_spec = pytree.tree_flatten((init, xs, additional_inputs))[1] + self._optimize_forward_intermediates() + + def _insert_clone( + self, need_copy_node: torch.fx.Node, output_node: torch.fx.Node + ) -> torch.fx.Node: + graph: torch.fx.Graph = output_node.graph + with graph.inserting_before(output_node): + clone_node = graph.call_function( + torch.ops.aten.clone.default, + args=(need_copy_node,), + ) + clone_node.meta = ( + need_copy_node.meta.copy() if hasattr(need_copy_node, "meta") else {} + ) + return clone_node + + def _optimize_forward_intermediates(self): + """ + We optimize the forward intermediates by categorize forward intermediates into categories + and construct a ScanForwardIntermediatesHandlingPolicy for them + + """ + if logger.isEnabledFor(logging.DEBUG): + logger.debug( + "Need remove aliasing in fw_gm:\n%s", + self.hop_partitioned_graph.fw_gm.print_readable(print_output=False), + ) + + fw_gm = self.hop_partitioned_graph.fw_gm + fw_all_outputs = _find_hop_subgraph_outputs(fw_gm) + phs = list(fw_gm.graph.find_nodes(op="placeholder")) + fw_outputs = fw_all_outputs[: self.hop_partitioned_graph.n_fw_outputs] + fw_intermediates = fw_all_outputs[self.hop_partitioned_graph.n_fw_outputs :] + + init_phs, xs_phs, additional_inputs_phs = pytree.tree_unflatten( + phs, self.fw_spec + ) + init_node_set, xs_node_set, addi_node_set = ( + set(init_phs), + set(xs_phs), + set(additional_inputs_phs), + ) + + if len(self.forward_intermediates_handling_policies) != 0: + raise AssertionError( + "forward_intermediates_handling_policies should be empty" + ) + if len(self.saved_fw_xs) != 0: + raise AssertionError("saved_fw_xs should be empty") + if len(self.saved_fw_additional_inputs) != 0: + raise AssertionError("saved_fw_additional_inputs should be empty") + intermediate_idx_to_ph_idx = {} + ph_idx = {ph: i for i, ph in enumerate(phs)} + for i, out in enumerate(fw_intermediates): + if out in init_node_set: + self.forward_intermediates_handling_policies.append( + ScanForwardIntermediatesHandlingPolicy.CLONE + ) + intermediate_idx_to_ph_idx[i] = ph_idx[out] + elif out in xs_node_set: + self.forward_intermediates_handling_policies.append( + ScanForwardIntermediatesHandlingPolicy.REMOVE_XS + ) + intermediate_idx_to_ph_idx[i] = ph_idx[out] + elif out in addi_node_set: + self.forward_intermediates_handling_policies.append( + ScanForwardIntermediatesHandlingPolicy.REMOVE_ADDITIONAL_INPUTS + ) + intermediate_idx_to_ph_idx[i] = ph_idx[out] + else: + self.forward_intermediates_handling_policies.append( + ScanForwardIntermediatesHandlingPolicy.KEEP + ) + + new_output_node = [] + real_graph_inputs = ( + list(self.init) + list(self.xs) + list(self.additional_inputs) + ) + fw_output_node = next(iter(fw_gm.graph.find_nodes(op="output"))) + for intermediate_idx, (node, policy) in enumerate( + zip(fw_intermediates, self.forward_intermediates_handling_policies) + ): + if policy == ScanForwardIntermediatesHandlingPolicy.CLONE: + new_output_node.append(self._insert_clone(node, fw_output_node)) + elif policy == ScanForwardIntermediatesHandlingPolicy.REMOVE_XS: + if intermediate_idx not in intermediate_idx_to_ph_idx: + raise AssertionError( + f"intermediate_idx {intermediate_idx} not in intermediate_idx_to_ph_idx" + ) + inp_idx = intermediate_idx_to_ph_idx[intermediate_idx] + self.saved_fw_xs.append(real_graph_inputs[inp_idx]) + elif ( + policy + == ScanForwardIntermediatesHandlingPolicy.REMOVE_ADDITIONAL_INPUTS + ): + if intermediate_idx not in intermediate_idx_to_ph_idx: + raise AssertionError( + f"intermediate_idx {intermediate_idx} not in intermediate_idx_to_ph_idx for REMOVE_ADDITIONAL_INPUTS" + ) + inp_idx = intermediate_idx_to_ph_idx[intermediate_idx] + self.saved_fw_additional_inputs.append(real_graph_inputs[inp_idx]) + else: + new_output_node.append(node) + + fw_output_node.args = (tuple(fw_outputs) + tuple(new_output_node),) + fw_gm.graph.lint() + fw_gm.recompile() + + if logger.isEnabledFor(logging.DEBUG): + logger.debug( + "after removing aliasing:\n%s", fw_gm.print_readable(print_output=False) + ) + + def call_forward(self): + fw_outputs_and_intermediates: tuple[Any] = scan_op( + self.hop_partitioned_graph.fw_gm, self.init, self.xs, self.additional_inputs + ) # type: ignore[return-type] + fw_outs = fw_outputs_and_intermediates[ + : self.hop_partitioned_graph.n_fw_outputs + ] + saved_intermediates = fw_outputs_and_intermediates[ + self.hop_partitioned_graph.n_fw_outputs : + ] + if len(self.saved_intermediates) != 0: + raise AssertionError( + "saved_intermediates should be empty before call_forward" + ) + self.saved_intermediates.extend(saved_intermediates) + return tuple(fw_outs) + + def call_backward(self, *grad_fw_outputs): + """ + Recall that fw_outputs = (*carry, *ys), bw_gm takes in (*fw_intermediates, *grad_carry, *grad_ys) + and returns (*grad_init, *grad_xs, *grad_additional_inputs) + The bacwkard is a reversed scan that can be constructed as follows: + + grad_additonal_inputs = torch.zeros_like(additional_inputs) + bw_init = (grad_carry, grad_additional_inputs) + bw_xs = (fw_intermediates, grad_ys) + grad_init, grad_additional_inputs, grad_xs = scan( + combine_fn, + bw_init, + bw_xs, + reverse = True + ) + , where combine_fn is defined as follows: + + def combine_fn(bw_init, bw_xs): + grad_carry, grad_additional_inputs = bw_init + fw_intermediates, grad_y = bw_xs + nxt_grad_carry, grad_x, nxt_grad_additional_inputs = bw_gm(*fw_intermediates, *grad_carry, *grad_y) + return (nxt_grad_carry, grad_additional_inputs + nxt_grad_additional_inputs), grad_x + + Note that grad_additional_inputs is accumulated with add, grad_carry is carried over to next iteration and + grad_x is the ys output, which will be stacked together after the loop and will have the same shape as xs. + """ + fw_policy = self.forward_intermediates_handling_policies + saved_intermediates = self.saved_intermediates + saved_fw_xs = self.saved_fw_xs + saved_fw_additional_inputs = self.saved_fw_additional_inputs + + n_carry = len(self.init) + + grad_carry, grad_ys = grad_fw_outputs[:n_carry], grad_fw_outputs[n_carry:] + additional_inputs_tensor_masks = [ + bool(isinstance(t, torch.Tensor)) for t in self.additional_inputs + ] + grad_additional_inputs = [ + torch.zeros_like(t) + for t in filter_with_masks( + self.additional_inputs, additional_inputs_tensor_masks + ) + ] + + bw_init = [grad_carry, grad_additional_inputs] + bw_xs = [ + grad_ys, + saved_fw_xs, + saved_intermediates, + ] + bw_additional_inputs = saved_fw_additional_inputs + + _, flat_spec = pytree.tree_flatten((bw_init, bw_xs, bw_additional_inputs)) + + grad_spec = None + + def bw_single_step_wrapper(*args): + bw_init, bw_xs, bw_additional_inputs = pytree.tree_unflatten( + args, flat_spec + ) + grad_carry, grad_additional_inputs = bw_init + grad_y, saved_fw_xs, saved_intermediates = bw_xs + saved_fw_additional_inputs = bw_additional_inputs + + fw_intermediates = [] + xs_it = iter(saved_fw_xs) + carry_it = iter(saved_intermediates) + addi_it = iter(saved_fw_additional_inputs) + for policy in fw_policy: + if policy in ( + ScanForwardIntermediatesHandlingPolicy.CLONE, + ScanForwardIntermediatesHandlingPolicy.KEEP, + ): + fw_intermediates.append(next(carry_it)) + elif policy == ScanForwardIntermediatesHandlingPolicy.REMOVE_XS: + fw_intermediates.append(next(xs_it)) + elif ( + policy + == ScanForwardIntermediatesHandlingPolicy.REMOVE_ADDITIONAL_INPUTS + ): + fw_intermediates.append(next(addi_it)) + else: + raise RuntimeError(f"Unknown policy: {policy}") + + grad_fw_outputs = (*grad_carry, *grad_y) + + flat_out = self.hop_partitioned_graph.bw_gm( + *fw_intermediates, + *grad_fw_outputs, + ) + + next_grad_carry, grad_xs, grad_addi = split_into_chunks( + flat_out, # type: ignore[arg-type] + [len(self.init), len(self.xs), len(self.additional_inputs)], + ) + + nonlocal grad_spec + flat_grads, grad_spec = pytree.tree_flatten( + ( + next_grad_carry, + [ + prev + cur + for prev, cur in zip( + grad_additional_inputs, + filter_with_masks( + grad_addi, additional_inputs_tensor_masks + ), + ) + ], + grad_xs, + ) + ) + return flat_grads + + single_step_bw_xs = pytree.tree_map(lambda t: t[0], bw_xs) + bw_single_step_gm = materialize_as_graph( + bw_single_step_wrapper, + tuple( + pytree.tree_flatten((bw_init, single_step_bw_xs, bw_additional_inputs))[ + 0 + ] + ), + ) + + flat_grads = scan_op( + bw_single_step_gm, + pytree.tree_flatten(bw_init)[0], + # TODO: torch.flip copies the tensor, we should optimize it away + [torch.flip(x, (0,)) for x in pytree.tree_flatten(bw_xs)[0]], + pytree.tree_flatten(bw_additional_inputs)[0], + ) + if grad_spec is None: + raise AssertionError("grad_spec must not be None after scan_op") + grad_init, grad_additional_inputs, grad_xs = pytree.tree_unflatten( + flat_grads, grad_spec + ) + return ( + *grad_init, + *[torch.flip(elem, (0,)) for elem in grad_xs], + *fill_none_with_masks( + grad_additional_inputs, additional_inputs_tensor_masks + ), + ) + + +@scan_op.py_autograd_impl +def scan_autograd(combine_fn, init, xs, additional_inputs): + with disable_proxy_modes_tracing(): + hop_partitioned_graph: HopPartitionedGraph = ( + HopGraphMinCutPartitioner.create_partitioned_graph( + combine_fn, + (*init, *[x[0] for x in xs], *additional_inputs), + always_recompute_complex_exprs=True, + ) + ) + + return ScanAutogradOp.apply( + hop_partitioned_graph, + len(init), + len(xs), + len(additional_inputs), + *init, + *xs, + *additional_inputs, + ) + + +@scan_op.py_impl(ProxyTorchDispatchMode) +def scan_proxy_mode(mode, combine_fn, init, xs, additional_inputs): + return trace_scan(mode, scan_op, combine_fn, init, xs, additional_inputs) + + +@scan_op.py_impl(FakeTensorMode) +def scan_fake_tensor_mode(mode, combine_fn, init, xs, additional_inputs): + with mode: + scan_length = xs[0].shape[0] + carry, outputs = _extract_carry_and_out( + combine_fn( + *init, + *[first_slice_copy(inp) for inp in xs], + *additional_inputs, + ), + len(init), + ) + out = ( + *carry, + *(stack_y(t, scan_length) for t in outputs), + ) + return out + + +@scan_op.py_functionalize_impl +def scan_functionalize(ctx, combine_fn, init, xs, additional_inputs): + from torch._higher_order_ops.utils import ( + _check_alias_and_mutation, + _maybe_run_with_interpreter, + ) + + unwrapped_xs = ctx.unwrap_tensors(xs) + unwrapped_init = ctx.unwrap_tensors(init) + unwrapped_additional_inputs = ctx.unwrap_tensors(additional_inputs) + + with ctx.redispatch_to_next(): + functional_combine_fn = ctx.functionalize( + _maybe_run_with_interpreter(combine_fn) + ) + sample_unwrapped_xs_sliced = [first_slice_copy(inp) for inp in unwrapped_xs] + sample_inputs = list( + itertools.chain( + unwrapped_init, + sample_unwrapped_xs_sliced, + unwrapped_additional_inputs, + ) + ) + pre_dispatch = hasattr(ctx, "mode") and ctx.mode.pre_dispatch + _check_alias_and_mutation(combine_fn, sample_inputs, "scan", pre_dispatch) + ret = scan_op( + functional_combine_fn, + unwrapped_init, + unwrapped_xs, + unwrapped_additional_inputs, + ) + return ctx.wrap_tensors(ret) + + +@scan_op.py_impl(torch._C._functorch.TransformType.Vmap) +def scan_batch_rule(interpreter, combine_fn, init, xs, additional_inputs): + from torch._functorch.vmap import restore_vmap, unwrap_batched, wrap_batched + + unbatched_args, in_dims = unwrap_batched( + (init, xs, additional_inputs), interpreter.level() + ) + # move to last dim to not interfere with scan's batching + unbatched_init, unbatched_xs, unbatched_additional_inputs = pytree.tree_map( + lambda x, bdim: x.movedim(bdim, -1) if bdim is not None else x, + unbatched_args, + in_dims, + ) + after_move_dims = tuple( + pytree.tree_flatten( + pytree.tree_map(lambda x: -1 if x is not None else None, in_dims) + )[0] + ) + + with interpreter.lower(): + out_dims = None + + def wrapper(*args): + nonlocal out_dims + outputs, per_slice_out_dims = restore_vmap( + combine_fn, + after_move_dims, + interpreter.batch_size(), + interpreter.randomness(), + )(*args) + # Note: outputs are not batched, we just move the batch dim to the end + # this is to avoid it interfering with scan's batching + outputs = tuple( + pytree.tree_map( + lambda out, out_bdim: out.movedim(out_bdim, -1) + if out_bdim is not None + else out, + outputs, + per_slice_out_dims, + ) + ) + out_dims = tuple( + pytree.tree_map( + lambda out_bdim: -1 if out_bdim is not None else None, + per_slice_out_dims, + ) + ) + return outputs + + unwrapped_out = scan_op( + wrapper, unbatched_init, unbatched_xs, unbatched_additional_inputs + ) + + if out_dims is None: + raise AssertionError("out_dims must not be None after scan_op") + batched_out = wrap_batched(unwrapped_out, out_dims, interpreter.level()) + return batched_out + + +# dense implementation for scan. Used for testing only. +def _fake_scan(combine_fn, init, xs=None, dim=0, reverse=False): + carry_leaves, carry_spec = pytree.tree_flatten(init) + inp_leaves, inp_spec = pytree.tree_flatten(xs) + if xs is None or len(inp_leaves) == 0: + return init, [] + result_flat = [] + carry = carry_leaves + op = reversed if reverse else lambda x: x + + dummy_carry, dummy_out = combine_fn( + pytree.tree_unflatten(carry, carry_spec), + pytree.tree_unflatten( + [first_slice_copy(elem, dim) for elem in inp_leaves], + inp_spec, + ), + ) + dummy_out_leaves, dummy_out_spec = pytree.tree_flatten(dummy_out) + num_leaves = len(dummy_out_leaves) + + for ind in op(range(inp_leaves[0].size(dim))): + xs = [elem.select(dim, ind) for elem in inp_leaves] + + carry, y = combine_fn( + pytree.tree_unflatten(carry, carry_spec), + pytree.tree_unflatten(xs, inp_spec), + ) + carry, _ = pytree.tree_flatten(carry) + y, _ = pytree.tree_flatten(y) + result_flat.append(y) + + results = [ + torch.stack([e[leave_ind] for e in op(result_flat)]) + for leave_ind in range(num_leaves) + ] + return ( + pytree.tree_unflatten(carry, carry_spec), + pytree.tree_unflatten(results, dummy_out_spec), + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/schema.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/schema.py new file mode 100644 index 0000000000000000000000000000000000000000..550a64b9e82491327cf4168d23bf53d867822138 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/schema.py @@ -0,0 +1,323 @@ +import copy +from dataclasses import dataclass +from typing import Any + +import torch +import torch.utils._pytree as pytree +from torch._library.fake_class_registry import FakeScriptObject +from torch._library.opaque_object import is_opaque_type +from torch.fx.node import Target + + +# Below is an implementation of generating FunctionSchema from example values. +# This is helpful for generating FunctionSchema for HigherOrderOperator, where +# we don't have a function to inspect and each call of the higher order operator +# would have different schema. +@dataclass(frozen=True, slots=True) +class HopArgumentInfo: + # Could give a name to the operand by default it's empty string. + name: str + example_value: Any + # Provide an default_value + default_value: Any + # Whether this argument gets mutated in the hop subgraph. + # For output, this should always be False + is_mutated: bool + kw_only: bool + + +class HopArgumentInfoGen: + @staticmethod + def from_example( + example_value: Any, + *, + name: str = "", + default_value: Any | None = None, + is_mutated: bool = False, + kw_only: bool = False, + ) -> HopArgumentInfo: + if default_value is not None: + if type(example_value) is not type(default_value): + raise AssertionError( + f"example_value type {type(example_value)} doesn't match default_value type: {type(default_value)}" + ) + + return HopArgumentInfo( + name=name, + example_value=example_value, + default_value=default_value, + is_mutated=is_mutated, + kw_only=kw_only, + ) + + +class CTypeGen: + convert_to_base_ty = { + int: torch._C.IntType.get(), + float: torch._C.FloatType.get(), + str: torch._C.StringType.get(), + bool: torch._C.BoolType.get(), + } + + # should return torch._C.JitType but that annotation is busted + @staticmethod + def from_example(obj: Any) -> Any: + import torch + + if isinstance(obj, torch.fx.GraphModule): + return torch._C.AnyType.get() + elif isinstance(obj, torch.SymInt): + return torch._C.SymIntType.get() + elif isinstance(obj, torch.SymBool): + return torch._C.SymBoolType.get() + elif isinstance(obj, torch.SymFloat): + return torch._C.FloatType.get() + elif isinstance(obj, (FakeScriptObject, pytree.TreeSpec)) or is_opaque_type( + type(obj) + ): + return torch._C.PyObjectType.get() # pyrefly: ignore[missing-attribute] + return torch._C._jit_try_infer_type(obj).type() + + +class CArgumentGen: + @staticmethod + def from_hop_argument_info( + arg_idx: int, arg_info: HopArgumentInfo, is_output: bool = False + ) -> Any: + typ = CTypeGen.from_example(arg_info.example_value) + if is_output: + return torch._C.Argument("", typ, None, None, False, None) + + alias_set = set({f"alias::a{arg_idx}"}) if arg_info.is_mutated else set() + alias_info = torch._C._AliasInfo(arg_info.is_mutated, alias_set, alias_set) # type: ignore[attr-defined] + return torch._C.Argument( + arg_info.name, + typ, + None, + arg_info.default_value, + arg_info.kw_only, + alias_info, + ) + + +class HopSchemaGenerator: + def __init__(self, hop: torch._ops.HigherOrderOperator): + self.arg_infos: list[HopArgumentInfo] = [] + self.example_outputs: list[Any] = [] + self.schema_tree_spec: pytree.TreeSpec | None = None + self.hop = hop + + def add_arg( + self, + name: str, + example_value: Any, + default_value: Any | None = None, + is_mutated: bool = False, + kw_only: bool = False, + ) -> None: + if callable(example_value) and not is_opaque_type(type(example_value)): + if not isinstance( + example_value, (torch.fx.GraphModule, torch._ops.OperatorBase) + ): + raise AssertionError( + "Expect callable to be a GraphModule or an OperatorBase. Please call materialize_as_graph first " + f"to turn callable arguments {example_value} into a GraphModule." + ) + _, flat_spec = pytree.tree_flatten(example_value) + if not flat_spec.is_leaf(): + raise RuntimeError( + f"example_value {example_value} is not a leaf node. " + "Please only add flattened inputs to the hop schema. " + "If you need some structure in the arguments, please" + "add_arg for flattened args one by one then " + "call add_schema_tree_spec to register the original pytree " + " spec of the args." + ) + + arg_info = HopArgumentInfoGen.from_example( + example_value=example_value, + name=name, + default_value=default_value, + is_mutated=is_mutated, + kw_only=kw_only, + ) + self.arg_infos.append(arg_info) + + def add_output(self, output: Any) -> None: + self.example_outputs.append(output) + + def add_schema_tree_spec(self, *args: Any, **kwargs: Any) -> None: + """schema tree spec is the tree spec from flattening all inputs to the hop with pytree.tree_flatten + Since torch.FunctionSchema only have proper mutation/alias support for flattened inputs, we need + to store the tree spec in order to reconstruct the inputs to the hop. + """ + self.schema_tree_spec = pytree.tree_flatten((args, kwargs))[1] + + def gen_schema(self) -> torch._C.FunctionSchema: + for i, arg_info in enumerate(self.arg_infos): + arg_spec = pytree.tree_flatten(arg_info.example_value)[1] + if not arg_spec.is_leaf() and self.schema_tree_spec is None: + raise RuntimeError( + f"example_value of arg_infos[{i}] is {arg_info.example_value}, which is not a leaf node. " + "Please call add_schema_tree_spec to add a schema tree spec first. " + "Or consider changing the hop's signature to only take flattened arguments." + ) + + return CFunctionSchemaGen.from_hop_argument_info( + str(self.hop), + self.arg_infos, + HopArgumentInfoGen.from_example(tuple(self.example_outputs), name="out"), + self.schema_tree_spec, + ) + + +class CFunctionSchemaGen: + """ + Note: [HigherOrderOperator schema generation] + Each invocation of a HigherOrderOperator will have a different schema. + For example, the schema of torch.cond varies depending on the true_fn and + false_fn. So we need a way to generate the schema for each invocation of a HOP. + + We want to enforce the following invariants for HOP's schema: + 1. Flattened inputs. There should be no pytree structure in it. + 2. Flattened outputs. Note even if the hop returns a single value, it should be wrapped as a tuple. + 3. No aliasing. This includes inp-inp aliasing, inp-out aliasing and out-out aliasing. + + By enforcing these invariants, we could make HOP's schema meets the requirement of schema parser + and makes hop easier to handle downstream. For example, suppose we have an invoke_quant_test HOP: + + class GraphModule(torch.nn.Module): + def forward(self, l_x_, l_y_): + subgraph_0 = self.subgraph_0 + invoke_quant_test = torch.ops.higher_order.invoke_quant_test(subgraph_0, l_x_, l_y_, scheme = 'nf4'); + + class subgraph_0(torch.nn.Module): + def forward(self, l_x_, l_y_): + add_ = l_x_.add_(1) + matmul = l_x_ @ l_y_ + sin = matmul.sin() + child = sin.cos() + child_1 = l_x_ + l_y_ + child_2 = l_x_ - l_y_ + child_3 = l_x_ @ l_y_ + return (child, child_1, child_2, child_3) + + By encoding the inputs of hop into a list of HopArgumentInfo and output as a single HopArgumentInfo, + we would get the following schema: + invoke_quant_test(Any arg0, Tensor(!) arg1, Tensor arg2, str scheme="\\"nf4\\"") -> (Tensor, Tensor, Tensor, Tensor) + """ + + @staticmethod + def from_hop_argument_info( + op_name: str, + inp_argument_info: list[HopArgumentInfo], + out_argument_info: HopArgumentInfo, + schema_tree_spec: pytree.TreeSpec | None, + ) -> Any: + args = [] + for i, arg_info in enumerate(inp_argument_info): + args.append(CArgumentGen.from_hop_argument_info(i, arg_info)) + + # NOTE: we want the output to always be a single argument with torch._C.TupleType. + if not isinstance(out_argument_info.example_value, tuple): + raise AssertionError( + f"expect out_argument_info's example_value to be a tuple but got {out_argument_info.example_value}" + ) + if out_argument_info.is_mutated: + raise AssertionError( + "out_argument_info.is_mutated should always be set to False." + ) + rets = None + if len(out_argument_info.example_value) == 1: + rets = [CArgumentGen.from_hop_argument_info(0, out_argument_info, True)] + else: + rets = [ + CArgumentGen.from_hop_argument_info( + i, + HopArgumentInfoGen.from_example( + name=f"out{i}", + example_value=val, + default_value=None, + is_mutated=False, + ), + is_output=True, + ) + for i, val in enumerate(out_argument_info.example_value) + ] + + return HopSchema( + op_name, + "", + args, + rets, + False, + False, + schema_tree_spec, + ) + + +class HopSchema(torch._C.FunctionSchema): + def __init__( + self, + name: str, + overload_name: str, + arguments: list[torch._C.Argument], + returns: list[torch._C.Argument], + is_vararg: bool, + is_varret: bool, + schema_tree_spec: pytree.TreeSpec | None, + ): + self.tree_spec = schema_tree_spec + self.is_vararg = is_vararg + self.is_varret = is_varret + super().__init__( + name, + overload_name, + arguments, + returns, + self.is_vararg, + self.is_varret, + ) + + def __deepcopy__(self, memo: Any) -> "HopSchema": + # Need to additionally copy the tree_spec since + # it's not a member of torch._C.FunctionSchema + return HopSchema( + self.name, + self.overload_name, + self.arguments, + self.returns, + self.is_vararg, + self.is_varret, + copy.deepcopy(self.tree_spec), + ) + + +def find_hop_schema( + gm: torch.fx.GraphModule, target: Target +) -> list[torch._C.FunctionSchema]: + schemas = [] + for node in gm.graph.find_nodes(op="call_function", target=target): + + def _get_example_value(node: torch.fx.Node) -> Any: + if node.op == "get_attr": + if not isinstance(node.target, str): + raise AssertionError( + f"expected node.target to be str for get_attr, got {type(node.target)}" + ) + return getattr(gm, node.target) + else: + return ( + node.meta["example_value"] + if "example_value" in node.meta + else node.meta["val"] + ) + + fake_args, fake_kwargs = pytree.tree_map_only( + torch.fx.Node, + _get_example_value, + (node.args, node.kwargs), + ) + schema = node.target.gen_schema(*fake_args, **fake_kwargs) + schemas.append(schema) + return schemas diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/strict_mode.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/strict_mode.py new file mode 100644 index 0000000000000000000000000000000000000000..ac0f2952f2c236c0ff62dc40c6f9d784c417929f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/strict_mode.py @@ -0,0 +1,95 @@ +# mypy: allow-untyped-defs + +import torch +import torch._subclasses.functional_tensor +import torch.utils._pytree as pytree +from torch._C import DispatchKey +from torch._functorch.utils import exposed_in +from torch._higher_order_ops.utils import autograd_not_implemented +from torch._ops import HigherOrderOperator +from torch._subclasses.fake_tensor import FakeTensorMode +from torch.fx.experimental.proxy_tensor import ( + disable_proxy_modes_tracing, + make_fx, + ProxyTorchDispatchMode, + track_tensor_tree, +) +from torch.utils._python_dispatch import _get_current_dispatch_mode + + +@exposed_in("torch") +def strict_mode(callable, operands): + if torch.compiler.is_dynamo_compiling(): + return strict_mode_op(callable, operands) + + from torch._higher_order_ops.utils import _hop_compile_and_call + + return _hop_compile_and_call(strict_mode_op, (callable, operands)) + + +class StrictMode(HigherOrderOperator): + def __init__(self): + super().__init__("strict_mode") + + def __call__(self, callable, operands): + # pyrefly: ignore [missing-attribute] + return super().__call__(callable, operands) + + +strict_mode_op = StrictMode() + + +@strict_mode_op.py_impl(DispatchKey.CompositeExplicitAutograd) +def strict_mode_op_dense(callable, operands): + mode = _get_current_dispatch_mode() + if mode is not None: + raise AssertionError("Mode should never be enabled for CPU/CUDA key") + return callable(*operands) + + +strict_mode_op.py_autograd_impl( + autograd_not_implemented(strict_mode_op, deferred_error=True) +) + + +@strict_mode_op.py_impl(ProxyTorchDispatchMode) +def inner(mode, callable, operands): + return trace_strict_mode(mode, strict_mode_op, callable, operands) + + +def trace_strict_mode(mode, strict_mode_op, callable, operands): + pre_dispatch = getattr(mode, "pre_dispatch", False) + + with disable_proxy_modes_tracing(): + graph = make_fx(callable, pre_dispatch=pre_dispatch)(*operands) + + graph_name = mode.tracer.get_fresh_qualname("strict_graph_") + mode.tracer.root.register_module(graph_name, graph) + + args = (graph, operands) + + proxy_args = pytree.tree_map(mode.tracer.unwrap_proxy, args) + + out_proxy = mode.tracer.create_proxy( + "call_function", strict_mode_op, proxy_args, {}, name="strict_mode" + ) + + out = graph(*operands) + return track_tensor_tree(out, out_proxy, constant=None, tracer=mode.tracer) + + +@strict_mode_op.py_impl(FakeTensorMode) +def strict_mode_fake_tensor_mode(mode, callable, operands): + with mode: + true_outs = callable(*operands) + return true_outs + + +@strict_mode_op.py_functionalize_impl +def strict_mode_func(ctx, callable, inputs): + unwrapped_inputs = ctx.unwrap_tensors(inputs) + with ctx.redispatch_to_next(): + functional_callable = ctx.functionalize(callable) + + cond_return = strict_mode_op(functional_callable, unwrapped_inputs) + return ctx.wrap_tensors(cond_return) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/torchbind.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/torchbind.py new file mode 100644 index 0000000000000000000000000000000000000000..e5ad7551c73d939de21c6c2320d66a47eca618c0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/torchbind.py @@ -0,0 +1,168 @@ +# mypy: allow-untyped-defs +import logging +from contextlib import contextmanager + +import torch +from torch._C import DispatchKey # @manual +from torch._functorch._aot_autograd.utils import KNOWN_TYPES +from torch._higher_order_ops.utils import autograd_not_implemented +from torch._library.fake_class_registry import ( + _is_script_object, + _ns_and_class_name, + FakeScriptObject, +) +from torch._ops import HigherOrderOperator +from torch._subclasses.fake_tensor import FakeTensorMode +from torch.fx.experimental.proxy_tensor import ProxyTorchDispatchMode, track_tensor_tree +from torch.fx.node import has_side_effect +from torch.utils import _pytree as pytree + + +log = logging.getLogger(__name__) + + +# The call_torchbind operator represents a method invocation on a torchbind +# object. The calling convention is: +# call_torchbind(self: ScriptObject, method_name: str, *method_args, **method_kwargs) +# We do not expect users to write this operator directly. Instead it will be +# emitted by Dynamo when tracing encounters a torchbind object. +class CallTorchBind(HigherOrderOperator): + def __init__(self): + super().__init__("call_torchbind") + + def __call__(self, obj, method, *args, **kwargs): + # pyrefly: ignore [missing-attribute] + return super().__call__(obj, method, *args, **kwargs) + + @staticmethod + def schema(obj, method) -> torch.FunctionSchema: + """ + Returns the schema of ``CallTorchbind.__call__``. + """ + if not isinstance(obj, torch._inductor.ir.TorchBindObject): + raise AssertionError(f"expected obj to be TorchBindObject, got {type(obj)}") + val = obj.get_real_obj() + schema = val._get_method(method).schema + schema_str = str(schema) + new_schema_str = f"call_torchbind({str(schema.arguments[0].real_type)} {schema.arguments[0].name}," + first_comma_index = schema_str.find(",") + if first_comma_index == -1: + # If no comma is found, find the last closing parenthesis + first_comma_index = schema_str.rfind(") ->") + new_schema_str = new_schema_str + " str method" + schema_str[first_comma_index:] + new_schema = torch._C.parse_schema(new_schema_str) + return new_schema + + +call_torchbind = CallTorchBind() + +# Register this operator as side-effectful with FX. +# TODO: this is not really sufficient. While passes (hopefully) check +# Node.is_impure() and make good decisions, we also assume we can execute the +# graph as many times as we want without changing behavior, which is NOT true of +# ops that mutate torchbind object state. +has_side_effect(call_torchbind) + +_orig_scriptmethod_call = torch.ScriptMethod.__call__ + + +def torchbind_method_redispatch(self, *args, **kwargs): + if _is_script_object(self.raw_owner): + return call_torchbind(self.raw_owner, self.name, *args, **kwargs) + return _orig_scriptmethod_call(self, *args, **kwargs) + + +@contextmanager +def enable_torchbind_tracing(): + """Context manager that acts as a feature flag to enable torchbind tracing + behavior. Once torchbind tracing has been stabilized, we can remove this and + turn it always on. + """ + try: + KNOWN_TYPES.append(torch.ScriptObject) + torch.ScriptMethod.__call__ = torchbind_method_redispatch # type: ignore[method-assign] + yield + finally: + if KNOWN_TYPES.pop() is not torch.ScriptObject: + raise AssertionError( + "Someone else messed with KNOWN_TYPES during tracing, exploding." + ) + torch.ScriptMethod.__call__ = _orig_scriptmethod_call # type: ignore[method-assign] + + +@call_torchbind.py_impl(DispatchKey.CompositeExplicitAutograd) +def call_torchbind_impl(obj, method, *args, **kwargs): + if isinstance(obj, torch.ScriptObject): + return _orig_scriptmethod_call(getattr(obj, method), *args, **kwargs) + elif isinstance(obj, FakeScriptObject): + return getattr(obj.wrapped_obj, method)(*args, **kwargs) + else: + raise RuntimeError(f"Unsupported first arg type {type(obj)} for call_torchbind") + + +@call_torchbind.py_impl(ProxyTorchDispatchMode) +def inner(mode, *args, **kwargs): + proxy_args = pytree.tree_map(mode.tracer.unwrap_proxy, args) + proxy_kwargs = pytree.tree_map(mode.tracer.unwrap_proxy, kwargs) + + out_proxy = mode.tracer.create_proxy( + "call_function", + call_torchbind, + proxy_args, + proxy_kwargs, + ) + out = call_torchbind(*args, **kwargs) + + obj, method, *_rest_args = args + if isinstance(obj, torch.ScriptObject): + ns, class_name = _ns_and_class_name( + obj._type().qualified_name() # type: ignore[attr-defined] + ) + log.warning( + "Tracing torchbind method %s.%s with real ScriptObject. This may" + " cause the original object being mutated. If this is not intended," + ' You can register a fake class with torch._library.register_fake_class("%s::%s").', + class_name, + method, + ns, + class_name, + ) + + ret = track_tensor_tree(out, out_proxy, constant=None, tracer=mode.tracer) + if "val" not in out_proxy.node.meta: + if out is not None and not isinstance(out, (int, float, bool)): + raise AssertionError( + f"Currently, only these constant dtypes are supported to be returned from torchbind methods, got {type(out)}" + ) + out_proxy.node.meta["val"] = out + return ret + + +# When tracing with fake script object, the call_torchbind op will return a fake tensor +# When tracing with real script object, the call_torchbind op may return a real tensor, +# we need to convert it to fake tensor manually. Dynamic shape is supported. +@call_torchbind.py_impl(FakeTensorMode) +def call_torchbind_fake(mode, *args, **kwargs): + with mode: + out = call_torchbind_impl(*args, **kwargs) + return pytree.tree_map_only( + torch.Tensor, + lambda x: mode.from_tensor(x, static_shapes=True) + if not isinstance(x, torch._subclasses.fake_tensor.FakeTensor) + else x, + out, + ) + + +call_torchbind.py_autograd_impl( + autograd_not_implemented(call_torchbind, deferred_error=True) +) + + +@call_torchbind.py_functionalize_impl +def call_torchbind_func(ctx, *args, **kwargs): + from torch._higher_order_ops.effects import handle_effects + + return handle_effects( + ctx.mode._allow_token_discovery, ctx.mode._tokens, call_torchbind, args, kwargs + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/triton_kernel_wrap.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/triton_kernel_wrap.py new file mode 100644 index 0000000000000000000000000000000000000000..bb884b3445b8d0c9ba8e20a1cb26a664d705b7df --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/triton_kernel_wrap.py @@ -0,0 +1,2457 @@ +import ast +import collections +import copy +import dataclasses +import functools +import inspect +import itertools +import logging +import operator +import threading +import typing +from collections import defaultdict +from collections.abc import Callable, Sequence +from typing import Any, Optional, TYPE_CHECKING, Union +from typing_extensions import Never + +import sympy + +import torch.fx as fx +import torch.utils._pytree as pytree +from torch import SymInt, Tensor +from torch._C import DispatchKey +from torch._higher_order_ops.utils import redirect_to_mode +from torch._ops import HigherOrderOperator +from torch._prims_common import clone_preserve_strides +from torch._subclasses.fake_tensor import FakeTensorMode +from torch.fx.experimental.proxy_tensor import ( + disable_proxy_modes_tracing, + ProxyTorchDispatchMode, + track_tensor_tree, +) +from torch.fx.experimental.symbolic_shapes import guard_scalar +from torch.types import IntLikeType +from torch.utils._ordered_set import OrderedSet +from torch.utils.checkpoint import _CachedTorchDispatchMode, _CachingTorchDispatchMode + + +if TYPE_CHECKING: + from triton._C.libtriton.ir import ( + module as TritonIRModule, + operation as TritonIROperation, + ) + + from torch._dynamo.symbolic_convert import InstructionTranslator + from torch._dynamo.variables.constant import ConstantVariable + from torch._dynamo.variables.functions import TritonKernelVariable + from torch._guards import Source + from torch._inductor.dependencies import ReadWrites + from torch._subclasses.functional_tensor import BaseFunctionalizeAPI + from torch.fx.proxy import Proxy + from torch.utils._triton import has_triton + + TritonMetaParamsType = dict[str, int] + TritonGridTupleType = tuple[int | sympy.Expr | SymInt, ...] + TritonGridCallableType = Callable[[TritonMetaParamsType], tuple[int, ...]] + TritonGridType = TritonGridTupleType | TritonGridCallableType + + if has_triton(): + from triton.runtime.autotuner import Autotuner, Config as TritonConfig + from triton.runtime.jit import JITFunction + else: + + class Autotuner: # type: ignore[no-redef] + pass + + class JITFunction: # type: ignore[no-redef] + pass + + TritonKernelType = Autotuner | JITFunction + # mypy specifically complains that TritonAutotunerType is not a valid type if Autotuner is not inside of a Union. + TritonAutotunerType = Union[Autotuner] # noqa: UP007 + +log = logging.getLogger("torch._dynamo") + +# e.g. for a host-side Triton TMA API call ``create_2d_tma_descriptor(ptr, 50, 60, 32, 15, 4)``, +# the metadata will look like ``("experimental", ([50, 60], [32, 15], 4))`` +TMAExperimentalMetadata = tuple[ + str, # type of TMA (should be "experimental") + tuple[ + list[IntLikeType], # dims + list[IntLikeType], # block_dims + IntLikeType, # element_size + ], +] + +# e.g. for host-side Triton TMA API call ``TensorDescriptor.from_tensor(ptr, [32, 64])`` +# the metadata will look like ``("stable", ([32, 64],))`` +TMAStableMetadata = tuple[ + str, # type of TMA ("experimental" or "stable") + tuple[list[IntLikeType],], # block_shape +] + + +def create_tma_experimental_metadata( + dims: list[IntLikeType], + block_dims: list[IntLikeType], + element_size: IntLikeType, +) -> TMAExperimentalMetadata: + return ("experimental", (dims, block_dims, element_size)) + + +def maybe_unpack_tma_experimental_metadata( + tma_meta: TMAExperimentalMetadata | TMAStableMetadata, +) -> tuple[list[IntLikeType], list[IntLikeType], IntLikeType] | None: + if not tma_meta or len(tma_meta) != 2: + return None + if tma_meta[0] == "experimental": + return tma_meta[1] # type: ignore[return-value] + return None + + +def create_tma_stable_metadata( + block_shape: list[IntLikeType], +) -> TMAStableMetadata: + return ("stable", (block_shape,)) + + +def maybe_unpack_tma_stable_metadata( + tma_meta: TMAExperimentalMetadata | TMAStableMetadata, +) -> tuple[list[IntLikeType]] | None: + if not tma_meta or len(tma_meta) != 2: + return None + if tma_meta[0] == "stable": + return tma_meta[1] # type: ignore[return-value] + return None + + +# TMADescriptorMetadata maps kernel parameter names to the metadata that allows +# reconstructing TMA descriptors from the underlying tensors (passed as kernel +# arguments in the fx graph, instead of the TMA descriptors). +# +# Since there are two TMA APIs (the old "experimental" API and the new "stable" API), +# each entry in the dict is a tuple that starts with a string, either "experimental" +# or "stable". The second entry in the tuple is another tuple, with data that depends +# on the API type (see TMAExperimentalMetadata and TMAStableMetadata above). +# +# These are stored as raw tuples (instead of classes) for ease of serialization. +TMADescriptorMetadata = dict[ + str, # kernel parameter name + TMAExperimentalMetadata | TMAStableMetadata, +] + + +############################################################################### +# Kernel Side Table + + +# We cannot put Triton Kernels into the FX graph as the graph nodes +# do not support arbitrary functions. +# Use a side table. +# We use two dicts so that fetching both the kernel and id are O(1) +class KernelSideTable: + id_to_kernel: dict[int, "TritonKernelType"] = {} + kernel_to_id: dict["TritonKernelType", int] = {} + constant_args: dict[int, dict[str, Any]] = {} + lock = threading.Lock() + + # Returns index on the table + def add_kernel(self, kernel: "TritonKernelType") -> int: + with self.lock: + if kernel in self.kernel_to_id: + return self.kernel_to_id[kernel] + + idx = len(self.id_to_kernel) + self.id_to_kernel[idx] = kernel + self.kernel_to_id[kernel] = idx + return idx + + # Returns the triton kernel at the given index + def get_kernel(self, idx: int) -> "TritonKernelType": + # No need to lock here as fetching from dict is atomic + if idx not in self.id_to_kernel: + raise AssertionError(f"Kernel index {idx} not found in id_to_kernel") + return self.id_to_kernel[idx] + + # Not every constant arg can be added to the graph. Use this side table + # for constant args. + def add_constant_args(self, args: dict[str, Any]) -> int: + with self.lock: + idx = len(self.constant_args) + self.constant_args[idx] = args + return idx + + # Returns the constant args + def get_constant_args(self, idx: int) -> dict[str, Any]: + # No need to lock here as fetching from dict is atomic + if idx not in self.constant_args: + raise AssertionError( + f"Constant args index {idx} not found in constant_args" + ) + return self.constant_args[idx] + + # Resets the table (only meant to be used in unit tests) + # This is only safe assuming single threaded execution + def reset_table(self) -> None: + self.id_to_kernel = {} + self.kernel_to_id = {} + self.constant_args = {} + + +kernel_side_table = KernelSideTable() + + +############################################################################### +# Mutation Tracker + + +@dataclasses.dataclass(frozen=True, slots=True) +class Param: + idx: int + + +@dataclasses.dataclass(frozen=True, slots=True) +class Intermediate: + idx: int + + def fake(self) -> bool: + return self.idx < 0 + + +@dataclasses.dataclass(frozen=True, slots=True) +class Op: + name: str + fn_call_name: str | None + args: list[Param | Intermediate] + ret: Intermediate = dataclasses.field(repr=False) + # used for scf.yield: see [Note: scf.yield fix-up] + sub_idx: int | None = None + # used for tt.elementwise_inline_asm + # `is_pure = True` assumes the asm block has no side-effects + is_pure: bool = False + + def __post_init__(self) -> None: + if self.name == "tt.call": + if self.fn_call_name is None: + raise AssertionError("fn_call_name must not be None for tt.call op") + else: + if self.fn_call_name is not None: + raise AssertionError( + f"fn_call_name must be None for non-tt.call op, got {self.fn_call_name}" + ) + + +def generate_ttir( + kernel: "TritonKernelType", + kwargs: dict[str, Any], + tma_descriptor_metadata: TMADescriptorMetadata, +) -> tuple["TritonIRModule", list[str]]: + """ + Uses Triton's internal code generation to create TTIR + """ + import sympy + import triton + import triton.runtime.jit + from triton.compiler.compiler import ASTSource + from triton.runtime.autotuner import Autotuner + from triton.runtime.jit import JITFunction + + from torch._inductor.utils import ( + get_triton_attrs_descriptor_version, + triton_version_uses_attrs_dict, + TritonAttrsDescriptorVersion, + ) + from torch.utils._triton import has_triton_tensor_descriptor_host_tma + + triton_version = get_triton_attrs_descriptor_version() + + import torch._inductor.ir + from torch._subclasses.fake_tensor import FakeTensor + + if isinstance(kernel, Autotuner): + if len(kernel.configs) > 0: + # If we are autotuning, then it doesn't matter which version gets + # picked for tracing purposes, so lets pick the first one + kwargs = {**kwargs, **kernel.configs[0].kwargs} + kernel = kernel.fn + + if not isinstance(kernel, JITFunction): + raise AssertionError(f"Expected kernel to be a JITFunction, got {type(kernel)}") + + context = triton._C.libtriton.ir.context() + target = triton.runtime.driver.active.get_current_target() + backend = triton.compiler.compiler.make_backend(target) + options = backend.parse_options({}) + + # ignore backend-specific kwargs same way as in the native Triton code + # https://github.com/triton-lang/triton/blob/a6bb57d6285e723c58e87dd7cba263db6efff789/python/triton/runtime/jit.py#L594-L596 + # why this is important for user-defined Triton kernels on AMD: https://github.com/pytorch/pytorch/issues/140800 + for name in list(kwargs): + if name not in kernel.arg_names and name in options.__dict__: + kwargs.pop(name) + + if len(kwargs) != len(kernel.arg_names): + raise ValueError( + "Incorrect number of arguments passed to kernel: " + f"passed {list(kwargs.keys())}, expected {kernel.arg_names}." + ) + + # Replace all SymExprs with a regular value for TTIR generation + # Replace all FakeTensor/TensorBox with real tensors + # These replacements are needed for triton's type, key and config functions + ordered_args: dict[str, Any] = {} + for name in kernel.arg_names: + a = kwargs[name] + if isinstance(a, (torch.SymInt, torch.SymFloat, torch.SymBool, sympy.Expr)): + ordered_args[name] = 2 + elif ( + stable_meta := maybe_unpack_tma_stable_metadata( + # pyrefly: ignore [bad-argument-type] + tma_descriptor_metadata.get(name, None) + ) + ) is not None: + from triton.tools.tensor_descriptor import TensorDescriptor + + block_shape = stable_meta[0] + with torch._C._DisableTorchDispatch(): + # need 16-byte aligned strides + elements_per_dim = max(1, 16 // a.dtype.itemsize) + base_tensor = torch.empty( + [elements_per_dim] * len(block_shape), dtype=a.dtype + ) + + ordered_args[name] = TensorDescriptor.from_tensor(base_tensor, block_shape) + elif isinstance(a, (FakeTensor, torch._inductor.ir.TensorBox)): + with torch._C._DisableTorchDispatch(): + ordered_args[name] = torch.empty(2, dtype=a.dtype) + else: + ordered_args[name] = a + + def is_stable_tensor_descriptor_arg(arg: Any) -> bool: + if has_triton_tensor_descriptor_host_tma(): + from triton.tools.tensor_descriptor import TensorDescriptor + + if isinstance(arg, TensorDescriptor): + return True + return False + + def _is_constexpr_or_none(name: str, arg: Any) -> bool: + param_idx = kernel.arg_names.index(name) + return kernel.params[param_idx].is_constexpr or arg is None + + # Note: one would expect that each input to the triton kernel maps to + # one input parameter in the TTIR. This is _not_ true for TMA descriptors: + # one TMA descriptor gets converted into: + # * one TMA descriptor input + # * N strides, for a rank-N tensor + # * N sizes, for a rank-N tensor + # To account for this, we inject some fake arg names as placeholders for + # the stride and size parameters. + # + # Additionally, tensors and scalars are both included as TTIR parameters, + # whereas `constexpr` are inlined, and None are excluded. We both preserve + # scalars and tensors as this matters for "odd" ordering, + # eg. [tensor, scalar, tensor]. + def get_arg_names(name: str, arg: Any) -> list[str]: + if _is_constexpr_or_none(name, arg): + return [] + + if is_stable_tensor_descriptor_arg(arg): + stable_meta = maybe_unpack_tma_stable_metadata( + tma_descriptor_metadata[name] + ) + if stable_meta is None: + raise AssertionError(f"Failed to unpack stable TMA metadata for {name}") + block_shape = stable_meta[0] + tensor_rank = len(block_shape) + names = [name] + names.extend(name + f" STRIDE PLACEHOLDER {i}" for i in range(tensor_rank)) + names.extend(name + f" SIZE PLACEHOLDER {i}" for i in range(tensor_rank)) + return names + + return [name] + + ordered_arg_names = list( + itertools.chain.from_iterable( + get_arg_names(name, arg) for name, arg in ordered_args.items() + ) + ) + + def _get_specialization(args): # type: ignore[no-untyped-def] + # Support multiple triton versions. + # This code basically copies JITFunction.run() logic to get the attrs to construct an ASTSource. + if triton_version == TritonAttrsDescriptorVersion.V1_COMPILER: + return kernel._get_config(*args) + elif triton_version in { + TritonAttrsDescriptorVersion.V2_BACKENDS, + TritonAttrsDescriptorVersion.V3_BACKENDS_TUPLE, + }: + from triton.backends.compiler import AttrsDescriptor # noqa: F401 + + target = triton.runtime.driver.active.get_current_target() + backend_ = triton.compiler.compiler.make_backend(target) + + return backend_.get_attrs_descriptor(args, kernel.params) + else: + if ( + get_triton_attrs_descriptor_version() + != TritonAttrsDescriptorVersion.V4_DICT + ): + raise AssertionError( + f"Expected Triton attrs descriptor version V4_DICT, " + f"got {get_triton_attrs_descriptor_version()}" + ) + # specialize_impl switched to create_specialize_impl in https://github.com/triton-lang/triton/pull/6099 + if hasattr(triton.runtime.jit, "create_specialize_impl"): + try: + # Latest versions of Triton take specialize_extra as an arg to create_specialize_impl + specialize_impl = triton.runtime.jit.create_specialize_impl( + specialize_extra=backend.get_arg_specialization + ) + except TypeError: # Unknown arg `specialize_extra` + # Older versions of Triton take specialize_extra as an arg to specialize_impl + specialize_impl = functools.partial( + triton.runtime.jit.create_specialize_impl(), + specialize_extra=backend.get_arg_specialization, + ) + # create_specialize_impl is removed in https://github.com/triton-lang/triton/pull/7771 + # switch to native_specialize_impl instead + elif hasattr(triton.runtime.jit, "native_specialize_impl"): + from triton.backends import BaseBackend + from triton.runtime.jit import native_specialize_impl + + def _native_specialize_impl( + arg: Any, + is_const: bool = False, + specialize_value: bool = True, + align: bool = True, + ) -> Callable: + return native_specialize_impl( + BaseBackend, arg, is_const, specialize_value, align + ) + + specialize_impl = _native_specialize_impl + else: + from triton.runtime.jit import specialize_impl as specialize_impl_orig + + specialize_impl = functools.partial( + specialize_impl_orig, + specialize_extra=backend.get_arg_specialization, + ) + + from triton._utils import find_paths_if, get_iterable_path + + # logic is copied from: binder = create_function_from_signature(self.signature, self.params, backend) + attrvals = [] + for arg, kp in zip(args, kernel.params): + if kp.is_constexpr: + attrvals.append(arg) + else: + spec = specialize_impl( + arg, + is_const=kp.is_const, + specialize_value=not kp.do_not_specialize, + align=not kp.do_not_specialize_on_alignment, + ) + # pyrefly: ignore [unsupported-operation] + attrvals.append(spec[1]) + + attrs = find_paths_if(attrvals, lambda _, x: isinstance(x, str)) + attrs = { + k: backend.parse_attr(get_iterable_path(attrvals, k)) for k in attrs + } + return attrs + + specialization = _get_specialization(ordered_args.values()) + # Triton explicitly interprets ASTSource.constants entries as constexpr + # Thus, only None and arguments marked `is_constexpr` should be treated as such. + constants = { + name: arg + for name, arg in ordered_args.items() + if _is_constexpr_or_none(name, arg) + } + + if (mangle_type := getattr(triton.runtime.jit, "mangle_type", None)) is not None: + + def get_signature_value(idx: int, arg: Any) -> str: + if kernel.params[idx].is_constexpr: + return "constexpr" + # pyrefly: ignore [not-callable] + result = mangle_type(arg) + # Workaround for Triton i1/u1 AOTI bug: PyTorch stores bool + # tensors as uint8 (1 byte per element), but *i1/*u1 causes + # the compiled kernel to generate bit-packed loads. Use *u8 + # so loads correctly read 1 byte per element. + if result in ("*i1", "*u1"): + result = "*u8" + return result + + else: + + def get_signature_value(idx: int, arg: Any) -> str: + return kernel._type_of(kernel.key_of(arg)) + + if triton_version_uses_attrs_dict(): + # In newer versions of Triton, the signature includes constexpr args + signature = { + name: get_signature_value(i, arg) + for i, (name, arg) in enumerate(ordered_args.items()) + } + else: + # In older versions of Triton, the signature does not include constexpr args + constexprs = [p.num for p in kernel.params if p.is_constexpr] + signature = { + name: get_signature_value(i, arg) + for i, (name, arg) in enumerate(ordered_args.items()) + if i not in constexprs + } + + triton._C.libtriton.ir.load_dialects(context) + backend.load_dialects(context) + + src = ASTSource(kernel, signature, constants, specialization) + + # Triton changes ASTSource.make_ir to take 3/4 arguments. Handle + # backward compatibility here. + make_ir_sig_params = len(inspect.signature(src.make_ir).parameters) + get_codegen_implementation_sig_params = len( + inspect.signature(backend.get_codegen_implementation).parameters + ) + if make_ir_sig_params == 2: + ttir_module = src.make_ir(options, context) + elif make_ir_sig_params == 3: + codegen_fns = backend.get_codegen_implementation() + + ttir_module = src.make_ir(options, codegen_fns, context) + elif make_ir_sig_params == 4: + codegen_args = [options] if get_codegen_implementation_sig_params == 1 else [] + + codegen_fns = backend.get_codegen_implementation(*codegen_args) + module_map = backend.get_module_map() + + ttir_module = src.make_ir(options, codegen_fns, module_map, context) + else: + codegen_args = [options] if get_codegen_implementation_sig_params == 1 else [] + + codegen_fns = backend.get_codegen_implementation(*codegen_args) + module_map = backend.get_module_map() + + ttir_module = src.make_ir(target, options, codegen_fns, module_map, context) + if not ttir_module.verify(): + raise RuntimeError("Verification for TTIR module has failed") + + return ttir_module, ordered_arg_names + + +def ttir_to_functions( + ttir_module: "TritonIRModule", +) -> dict[str, dict[Intermediate, list[Op]]]: + """ + Walk the `ttir_module` bottom up to mine the `functions` from + the structured MLIR entities representing the Triton kernel + (mlir::Operation, mlir::Block, mlir::Region). + """ + functions: dict[str, dict[Intermediate, list[Op]]] = {} + + # block id --> op result (Intermediate) --> one or more ops + op_stack: dict[int, dict[Intermediate, list[Op]]] = defaultdict( + lambda: defaultdict(list) + ) + region_id_to_block_ids: dict[int, list[int]] = defaultdict(list) + block_id_to_block_arg_ids: dict[int, list[int]] = {} + replacements: dict[int, Intermediate | Param] = {} + reindex_map: dict[int, int] = {} + next_fake_intermediate = 0 + + def reindex(idx: int) -> int: + if idx not in reindex_map: + reindex_map[idx] = len(reindex_map) + return reindex_map[idx] + + def mlir_to_functions(op: "TritonIROperation") -> None: + name: str = op.get_name() + if name == "builtin.module": + # this wraps all tt.func ops + return + + operand_ids: list[int] = [ + reindex(op.get_operand(i).id()) for i in range(op.get_num_operands()) + ] + result_ids: list[int] = [ + reindex(op.get_result(i).id()) for i in range(op.get_num_results()) + ] + + child_block_ids: list[int] = [] + for i in [op.get_region(i).id() for i in range(op.get_num_regions())]: + # as the walk is bottom-up, the region_id_to_block_ids[i] + # must be populated by the time we process the enclosing op + child_block_ids.extend(region_id_to_block_ids[i]) + + parent_block_id = -1 + parent_block = op.get_block() + if parent_block is not None: + parent_block_id = parent_block.id() + if parent_block_id not in block_id_to_block_arg_ids: + block_id_to_block_arg_ids[parent_block_id] = [] + for i in range(parent_block.get_num_arguments()): + block_id_to_block_arg_ids[parent_block_id].append( + reindex(parent_block.get_argument(i).id()), + ) + # the region info is collected via ops' parent blocks to be + # used later when the region's encloding op is traversed + parent_region = parent_block.get_parent() + if parent_region is not None: + region_id_to_block_ids[parent_region.id()].append(parent_block_id) + + nonlocal next_fake_intermediate + + if name == "tt.func": + # for function ops: gather and inline + # the ops from all child blocks + fn_ops = defaultdict(list) + for child_block_id in child_block_ids: + for result, block_fn_ops in op_stack.pop(child_block_id).items(): + for block_fn_op in block_fn_ops: + fn_ops[result].append(block_fn_op) + + # replace the corresponding Intermediates in the + # child op args with the function args (Params) + for i, idx in enumerate(block_id_to_block_arg_ids[child_block_ids[0]]): + replacements[idx] = Param(i) + + for fn_op_list in fn_ops.values(): + for fn_op in fn_op_list: + for i in range(len(fn_op.args)): + arg = fn_op.args[i] + seen = set() # to break cycles + # there can be transitive replacements, but likely + # no cycles (we keep the `seen` set just in case) + while ( + isinstance(arg, Intermediate) + and arg.idx in replacements + and arg.idx not in seen + ): + seen.add(arg.idx) + arg = fn_op.args[i] = replacements[arg.idx] + + # next function capture starts + # with empty replacements + replacements.clear() + + fn_name = op.get_str_attr("sym_name") + functions[fn_name] = fn_ops + elif child_block_ids: + if name in {"scf.if", "scf.for", "scf.while", "tt.reduce", "tt.scan"}: + # for blocked ops: inline the enclosed ops into + # the parent block + rewire the last op in each + # child block to return the block result + return_ops = [] + for block_id in child_block_ids: + if name == "scf.for": + # example: + # %result = scf.for %iv = %lb to %ub step %step iter_args(%arg = %init) -> (i32) ... + # block args: 2 (%iv, %arg) + # op operands: 4 (%lb, %ub, %step, %init) + # `%arg` is mapping to `%init` + for i, idx in enumerate(block_id_to_block_arg_ids[block_id]): + if i == 0: + next_fake_intermediate -= 1 + replacements[idx] = Intermediate(next_fake_intermediate) + else: + replacements[idx] = Intermediate(operand_ids[i + 2]) + elif name == "scf.while": + # example: + # %3:3 = scf.while (%arg2 = %1, %arg3 = %2, %arg4 = %c0_i32_8) ... + # block args: 3 (%arg2, %arg3, %arg4) + # op operands: 3 (%1, %2, %c0_i32_8) + # `%arg2` is mapping to `%1`, `%arg3` is mapping to `%2`, ... + for i, idx in enumerate(block_id_to_block_arg_ids[block_id]): + replacements[idx] = Intermediate(operand_ids[i]) + elif name == "scf.if": + # the scf block args are ignored by the pass. but, as they + # may be used as operands of the ops inside the block + # (and nested blocks inlined in the current block by now), + # they are replaced by new fake Intermediates to avoid "this + # operand is not returned by any other op in the fn" error + # in the downstream analysis + for idx in block_id_to_block_arg_ids[block_id]: + next_fake_intermediate -= 1 + replacements[idx] = Intermediate(next_fake_intermediate) + else: + if name not in ("tt.reduce", "tt.scan"): + raise AssertionError( + f"Expected op name to be 'tt.reduce' or 'tt.scan', got {name}" + ) + # wire the block arguments to the op arguments + num_operands = len(operand_ids) + block_arg_ids = block_id_to_block_arg_ids[block_id] + if len(block_arg_ids) != 2 * num_operands: + raise AssertionError( + f"{name} is expected to have twice as " + "many block arguments as op arguments: " + f"{operand_ids=}, {block_arg_ids=}." + ) + for i, idx in enumerate(block_arg_ids): + # for a tt.reduce/tt.scan op with N arguments, the block + # arguments comprise N reduced values followed by + # N current values corresponding to the N op args + replacements[idx] = Intermediate( + operand_ids[i % num_operands] + ) + + if block_id in op_stack: + block_ops = op_stack.pop(block_id) + if not block_ops: + continue + last_ret, last_ops = block_ops.popitem() + if all( + op.name + in ("scf.yield", "tt.reduce.return", "tt.scan.return") + for op in last_ops + ): + # if last_ops are all return ops, treat them separately + return_ops.extend(last_ops) + else: + # otherwise, return last_ops to the block + block_ops[last_ret] = last_ops + for op_result, child_ops in block_ops.items(): + op_stack[parent_block_id][op_result].extend(child_ops) + + scf_results = [Intermediate(idx) for idx in result_ids] + + if return_ops and all( + (op.name == "scf.yield" and len(result_ids) == len(op.args)) + for op in return_ops + ): + # [Note: scf.yield fix-up] + # + # TL;DR: if our scf.yield takes N args, then we'll create N scf.yield ops to handle each of the + # args. + # + # **Context**: + # During mutation analysis, the analysis pass will identify mutating ops (e.g. tt.store) + # and then DFS upwards towards the parameters of the function. Specifically, the analysis pass + # looks at the mutated arg in tt.store; then looks for its source ops; and then recurses on the + # arguments to each of the source ops. + # + # In the case of scf.if/scf.for, we may have multiple return ops, each passed as an arg + # to scf.yield: + # + # %18:2 = scf.if %... -> (!tt.ptr, !tt.ptr) { + # ... + # scf.yield %1, %2 + # } else { + # scf.yield %3, %4 + # } + # + # And for each of the returns of the scf.if, we'd naively assign the source op of each of the + # return values to be the scf.yields. But the scf.yields take _all_ the returns as arguments. + # Therefore, if _any_ of the return values of the scf.if are mutated, then the analysis pass + # would mark _all_ of the yield args as mutated. + # + # **Solution**: + # For the purposes of this analysis pass, we create N yield ops - one for each + # return-val/yield-arg. In the example above, we'll have two scf.yield's for each branch of the + # scf.if. + + for return_op in return_ops: + for i, (scf_result, yield_arg) in enumerate( + zip(scf_results, return_op.args) + ): + sub_yield_op = Op( + return_op.name, + return_op.fn_call_name, + [yield_arg], + return_op.ret, + sub_idx=i, + ) + op_stack[parent_block_id][scf_result].append(sub_yield_op) + + else: + for scf_result in scf_results: + for return_op in return_ops: + op_stack[parent_block_id][scf_result].append(return_op) + else: + raise RuntimeError( + f"Unknown blocked function: {name}. Can't capture the TTIR." + ) + else: + callee = None + if name == "tt.call": + callee = op.get_flat_symbol_ref_attr("callee") + args: list[Param | Intermediate] = [ + Intermediate(operand) for operand in operand_ids + ] + block_ops = op_stack[parent_block_id] + + is_pure = False + # Handle the case for tt.elementwise_inline_asm to set `is_pure` for mutation analysis + if name == "tt.elementwise_inline_asm": + is_pure = op.get_bool_attr("pure") + + if result_ids: + for result_id in result_ids: + res = Intermediate(result_id) + block_ops[res].append(Op(name, callee, args, res, is_pure=is_pure)) + else: + next_fake_intermediate -= 1 + fake_res = Intermediate(next_fake_intermediate) + block_ops[fake_res].append( + Op(name, callee, args, fake_res, is_pure=is_pure) + ) + + ttir_module.walk(mlir_to_functions) + + return functions + + +class MemoizeWithCycleCheck: + fn: Callable[..., Any] + cache: dict[tuple[Any], Any] + + def __init__(self, fn: Callable[..., Any]) -> None: + self.fn = fn + self.reset() + + def __call__( + self, + functions: dict[str, dict[Intermediate, list[Op]]], + fn_name: str, + *args: Any, + ) -> Any: + key: tuple[Any, ...] = (fn_name, *args) + if key not in self.cache: + self.cache[key] = None + self.cache[key] = self.fn(functions, fn_name, *args) + if self.cache[key] is None: + raise RuntimeError("Recursion is not supported") + return self.cache[key] + + def reset(self) -> None: + self.cache = {} + + +@MemoizeWithCycleCheck +def get_tma_stores( + functions: dict[str, dict[Intermediate, list[Op]]], fn_name: str +) -> set[Intermediate | Param]: + """ + Identifies all intermediates and parameters that are written to by a + `tt.experimental_descriptor_store`. It tracks only the specific values + written to via experimental_descriptor_store and the input values to + `tt.reinterpret_tensor_descriptor` used to construct the direct inputs + to tt.experimental_descriptor_store - not any recursive values + used to construct those values. + + For example: for + tt.reinterpret_tensor_descriptor(Intermediate(idx=0), ...) + Intermediate(idx=1) = tt.experimental_descriptor_store(Intermediate(idx=0), ...) + this function will return [Intermediate(idx=0), Intermediate(idx=1)], + + However + Intermediate(idx=4) = arith.addptr(Intermediate(idx=2), Intermediate(idx=3)) + Intermediate(idx=5) = tt.experimental_descriptor_store(Intermediate(idx=4), ...) + tt.experimental_descriptor_store(Intermediate(idx=5), ...) + this function will mark only idx=4 and idx=5 (but not idx=2 or idx=3) + + If an intermediate/parameter is passed into a function and is written to + via experimental_descriptor_store within that function, the argument to the + function will also be marked. + """ + + result: set[Intermediate | Param] = set() + + ops = functions[fn_name] + for op_list in ops.values(): + for op in op_list: + if op.name == "tt.call": + if op.fn_call_name not in functions: + raise AssertionError( + f"Function {op.fn_call_name} not found in functions for TMA stores" + ) + # pyrefly: ignore [bad-argument-type] + tma_stores = get_tma_stores(functions, op.fn_call_name) + for i, inp in enumerate(op.args): + if Param(idx=i) in tma_stores: + result.add(inp) + elif op.name == "tt.experimental_descriptor_store": + if len(op.args) < 1: + raise AssertionError( + f"tt.experimental_descriptor_store expected at least 1 arg, got {len(op.args)}" + ) + result.add(op.args[0]) + elif op.name == "tt.descriptor_store": + if len(op.args) < 1: + raise AssertionError( + f"tt.descriptor_store expected at least 1 arg, got {len(op.args)}" + ) + result.add(op.args[0]) + + for val in list(result): + if val in ops: + if not isinstance(val, Intermediate): + continue + for op in ops[val]: + if op.name == "tt.reinterpret_tensor_descriptor": + if len(op.args) < 1: + raise AssertionError( + "tt.reinterpret_tensor_descriptor expected at least 1 arg, " + f"got {len(op.args)}" + ) + result.add(op.args[0]) + + return result + + +@dataclasses.dataclass +class TensorAccesses: + read_writes: "ReadWrites" + can_fuse_epilogue: bool + + +@MemoizeWithCycleCheck +def analyze_kernel_access( + functions: dict[str, dict[Intermediate, list[Op]]], + fn_name: str, + num_args: int, + tensor_names: tuple[str, ...], + tensor_arg_indices: frozenset[int] | None, +) -> TensorAccesses: + """ + Analyzes the graph to detect which arguments are written to and which are read. + + For writes: traverses from write sinks (tt.store, tt.atomic_cas, etc.) backwards + to identify input pointers that are written to. + + For reads: traverses from read operations (tt.load) backwards to identify + input pointers that are read from. + + Returns ReadWrites with StarDep objects for each accessed tensor. + """ + from torch._inductor.dependencies import Dep, ReadWrites, StarDep + + # Name of mutation op to mutated parameter indices + # List from Triton Github include/triton/Dialect/Triton/IR/TritonOps.td + # All the OPs that have MemWrite trait. + # What if Triton exposed this? + WRITE_OPS = { + "tt.store": [0], + "tt.atomic_cas": [0], + "tt.atomic_rmw": [0], + "tt.experimental_descriptor_store": [0], + "tt.experimental_tensormap_create": [0], + "tt.descriptor_store": [0], + } + READ_OPS = { + "tt.load": [0], + "tt.load_tensor_descriptor": [0], + "tt.descriptor_load": [0], + } + UNKNOWN_OPS = {"tt.elementwise_inline_asm"} + + write_stack: list[Param | Intermediate] = [] + read_stack: list[Param | Intermediate] = [] + + ops = functions[fn_name] + tma_stores = get_tma_stores(functions, fn_name) + + for op_list in ops.values(): + for op in op_list: + # If we encounter an operation with effects that cannot be reliably analyzed + # (e.g. `tt.elementwise_inline_asm`), we assume it does not mutate any input parameters. + if op.name in UNKNOWN_OPS: + if op.name == "tt.elementwise_inline_asm" and op.is_pure: + continue + raise RuntimeError( + f"ttir analysis hit an op we do not know how to analyze: {op.name}" + ) + + if op.name == "tt.experimental_tensormap_create": + # Note: this is how we implement experimental_descriptor_store mutation analysis. + # for on-device TMA. + # experimental_tensormap_store(a, b, ...) stores b to the location specified + # by descriptor in the memory of a. + # To track this, we first find all the intermediates/params to which we store via + # experimental_tensormap_store (get_tma_stores, called above). Then, during this + # analysis we wait to find the corresponding experimental_tensormap_create (if it + # exists), at which point we will mark the global_ptr as mutated (as done below). + if len(op.args) < 2: + raise AssertionError( + f"tt.experimental_tensormap_create expected at least 2 args, " + f"got {len(op.args)}" + ) + if op.args[0] in tma_stores: + write_stack.append(op.args[1]) + + if op.name == "tt.call": + if op.fn_call_name not in functions: + raise AssertionError( + f"Function {op.fn_call_name} not found in functions dict" + ) + # Create placeholder names for nested function arguments + nested_names = tuple(f"_arg{i}" for i in range(len(op.args))) + + # Do not pass tensor_arg_indices, most outer call of + # analyze_kernel_access will filter Param nodes. + accesses = analyze_kernel_access( + functions, + # pyrefly: ignore [bad-argument-type] + op.fn_call_name, + len(op.args), + nested_names, + None, + ) + # Map back from StarDep names to args + written_set = {dep.name for dep in accesses.read_writes.writes} + read_set = {dep.name for dep in accesses.read_writes.reads} + for arg, name in zip(op.args, nested_names): + if name in written_set: + write_stack.append(arg) + if name in read_set: + read_stack.append(arg) + else: + write_stack.extend(op.args[idx] for idx in WRITE_OPS.get(op.name, [])) + read_stack.extend(op.args[idx] for idx in READ_OPS.get(op.name, [])) + + # For these ops, only the first argument (base pointer) refers to actual + # memory. The remaining arguments are shape/stride/offset metadata and + # should not be traced during mutation analysis. + POINTER_ONLY_OPS = { + "tt.make_tensor_ptr", + "tt.advance", + "tt.make_tensor_descriptor", + } + + def _find_arg_access_count( + initial_stack: list[Param | Intermediate], + skip_loads: bool, + ) -> dict[int, int]: + """DFS traversal to find argument indices that are accessed (and how many times they are accessed).""" + access_count = dict() + stack = initial_stack[:] + + while stack: + arg = stack.pop() + + if isinstance(arg, Param): + if arg.idx >= num_args: + continue + if tensor_arg_indices is not None and arg.idx not in tensor_arg_indices: + continue + if arg.idx not in access_count: + access_count[arg.idx] = 1 + else: + access_count[arg.idx] += 1 + elif isinstance(arg, Intermediate) and not arg.fake(): + for op in ops[arg]: + if skip_loads and op.name == "tt.load": + continue + if op.name in POINTER_ONLY_OPS: + stack.append(op.args[0]) + else: + stack.extend(op.args) + + return access_count + + write_count = _find_arg_access_count(write_stack, skip_loads=True) + read_count = _find_arg_access_count(read_stack, skip_loads=False) + + writes: OrderedSet[Dep] = OrderedSet( + StarDep(tensor_names[i]) for i in sorted(write_count.keys()) + ) + reads: OrderedSet[Dep] = OrderedSet( + StarDep(tensor_names[i]) for i in sorted(read_count.keys()) + ) + + read_writes = ReadWrites( + reads=reads, + writes=writes, + index_exprs=OrderedSet(), + ) + + def _decide_can_fuse_epilogue(): + # only do epilogue fusion if the kernel has a single output tensor + if len(write_count) != 1: + return False + + written_arg_index = next(iter(write_count)) + # only do epilogue fusion if the written tensor is written exactly once + if write_count[written_arg_index] != 1: + return False + + written_arg_name = next(iter(writes)).name + # cannot fuse if the kernel also reads from the output buffer + if any(read_dep.name == written_arg_name for read_dep in reads): + return False + + return True + + can_fuse_epilogue = _decide_can_fuse_epilogue() + + return TensorAccesses(read_writes=read_writes, can_fuse_epilogue=can_fuse_epilogue) + + +def identify_accessed_tensors( + kernel: "TritonKernelType", + kwargs: dict[str, Any], + tma_descriptor_metadata: TMADescriptorMetadata, +) -> TensorAccesses: + """ + Given a triton kernel and the arguments for this kernel, this function + 1) Retrieves the TTIR converted version of the kernel from Triton's API. + 2) Parses the TTIR and creates a control flow graph + 3) Analyzes the graph to detect which input tensors are read and/or written + """ + + from torch._inductor.dependencies import Dep, ReadWrites, StarDep + from torch._inductor.ir import TensorBox + + ttir_module = None + functions = None + try: + ttir_module, ordered_arg_names = generate_ttir( + kernel, kwargs, tma_descriptor_metadata + ) + + # extract functions from TTIR using MLIR bindings exposed by Triton code + functions = ttir_to_functions(ttir_module) + + if functions is None: + raise AssertionError("ttir_to_functions returned None") + kernel_name = next(iter(functions.keys())) + # Triton codegen modifies the name + # pyrefly: ignore [missing-attribute] + kernel_fn_name = kernel.fn.__name__ + if kernel_fn_name not in kernel_name: + raise AssertionError( + f"Kernel name {kernel_fn_name} not found in TTIR kernel name {kernel_name}" + ) + # Reset the cache between top level invocations + # The cache for analyze kernel access is mainly used for cycle + # detection, so each top level invocation needs a clean cache + analyze_kernel_access.reset() + get_tma_stores.reset() + + # Build frozenset of indices corresponding to tensor args only. + # Used to filter out scalars which are transitively captured as mutated + # during traversal. + tensor_arg_indices = frozenset( + i + for i, name in enumerate(ordered_arg_names) + if isinstance(kwargs.get(name), (Tensor, TensorBox)) + ) + + return analyze_kernel_access( + functions, + kernel_name, + len(ordered_arg_names), + tuple(ordered_arg_names), + tensor_arg_indices, + ) + except Exception: + log.warning( + "Encountered an exception in identify_accessed_tensors, assuming every input is mutated", + exc_info=True, + ) + if ttir_module is not None: + log.debug("TTIR:\n%s", ttir_module) + if functions is not None: + log.debug("functions:") + for name, fn in functions.items(): + log.debug("===\t%s\t===", name) + for ret, ops in fn.items(): + log.debug("%s\t=>\t%s", ret, ops) + + all_tensor_names = [ + key + for key, value in kwargs.items() + if isinstance(value, (Tensor, TensorBox)) + ] + all_deps = OrderedSet(StarDep(name) for name in all_tensor_names) + all_deps = typing.cast(OrderedSet[Dep], all_deps) + return TensorAccesses( + ReadWrites( + reads=all_deps, + writes=all_deps, + index_exprs=OrderedSet(), + ), + can_fuse_epilogue=False, + ) + + +@dataclasses.dataclass +class TritonStore: + store_node: ast.Call + store_pointer_node: ast.Expr + store_value_node: ast.Expr + + +@dataclasses.dataclass +class TritonStores: + stores: list[TritonStore] + + +@functools.cache +def identify_triton_stores(source_code: str) -> TritonStores: + """ + Parse Python source code of triton kernel and find all tl.store calls. + Returns a TritonStores object containing information about pointer, value, and mask. + + tl.store signature: store(pointer, value, mask=None, boundary_check=(), ...) + """ + return identify_triton_stores_from_ast(ast.parse(source_code)) + + +def identify_triton_stores_from_ast(tree: ast.Module) -> TritonStores: + stores = [] + + def _extract_arg(node, arg_name, positional_index): + """ + Extract an argument from a Call node, checking both positional and keyword args. + Returns the AST node for the argument, or None if not found. + """ + # Check positional args first + if len(node.args) > positional_index: + return node.args[positional_index] + + # Check keyword args + for keyword in node.keywords: + if keyword.arg == arg_name: + return keyword.value + + return None + + for node in ast.walk(tree): + if isinstance(node, ast.Call): + # Check if this is a tl.store call + if ( + isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) + and node.func.value.id == "tl" + and node.func.attr == "store" + ): + # Extract required arguments + pointer_node = _extract_arg(node, "pointer", 0) + value_node = _extract_arg(node, "value", 1) + + if pointer_node is None or value_node is None: + continue + + stores.append(TritonStore(node, pointer_node, value_node)) + + return TritonStores(stores=stores) + + +############################################################################### +# Triton Kernel Wrappers + + +# Used for wrapping a Triton Kernel +class TritonKernelWrapperMutation(HigherOrderOperator): + def __init__(self) -> None: + super().__init__("triton_kernel_wrapper_mutation", cacheable=True) + + def __call__( + self, + kernel_idx: int, + constant_args_idx: int, + grid: list["TritonGridType"], + tma_descriptor_metadata: TMADescriptorMetadata, + kwargs: dict[str, Any], + ) -> Any: + # pyrefly: ignore [missing-attribute] + return super().__call__( + kernel_idx=kernel_idx, + constant_args_idx=constant_args_idx, + grid=grid, + tma_descriptor_metadata=tma_descriptor_metadata, + kwargs=kwargs, + ) + + +triton_kernel_wrapper_mutation = TritonKernelWrapperMutation() + + +# Used for wrapping a Triton Kernel in a functional manner +class TritonKernelWrapperFunctional(HigherOrderOperator): + def __init__(self) -> None: + super().__init__("triton_kernel_wrapper_functional", cacheable=True) + + def __call__( + self, + kernel_idx: int, + constant_args_idx: int, + grid: list["TritonGridType"], + tma_descriptor_metadata: TMADescriptorMetadata, + kwargs: dict[str, Any], + tensors_to_clone: list[str], + ) -> dict[str, Any]: + # pyrefly: ignore [missing-attribute] + return super().__call__( + kernel_idx=kernel_idx, + constant_args_idx=constant_args_idx, + grid=grid, + tma_descriptor_metadata=tma_descriptor_metadata, + kwargs=kwargs, + tensors_to_clone=tensors_to_clone, + ) + + +triton_kernel_wrapper_functional = TritonKernelWrapperFunctional() + + +def get_kernel(kernel_idx: int) -> "TritonKernelType": + return kernel_side_table.get_kernel(kernel_idx) + + +@triton_kernel_wrapper_mutation.py_impl(DispatchKey.CompositeExplicitAutograd) +def triton_kernel_wrapper_mutation_dense( + *, + kernel_idx: int, + constant_args_idx: int, + grid: list["TritonGridType"], + tma_descriptor_metadata: TMADescriptorMetadata, + kwargs: dict[str, Any], +) -> None: + from torch._inductor.codegen.wrapper import user_defined_kernel_grid_fn_code + + kernel = kernel_side_table.get_kernel(kernel_idx) + constant_args = kernel_side_table.get_constant_args(constant_args_idx) + + if len(grid) == 1: + grid_fn = grid[0] + else: + fn_name, code = user_defined_kernel_grid_fn_code( + # pyrefly: ignore [missing-attribute] + kernel.fn.__name__, + # pyrefly: ignore [missing-attribute] + kernel.configs, + grid, + ) + namespace: dict[str, Any] = {} + exec(code, namespace) + grid_fn = namespace[fn_name] + + if tma_descriptor_metadata: + # as we need to launch the kernel here, we "unwrap" the + # tma_descriptor_metadata, create the TMA descriptors + # from it, and replace the tensors in the kwargs by the + # corresponding TMA descriptors before launching + kwargs = kwargs.copy() + for k, v in tma_descriptor_metadata.items(): + tensor = kwargs[k] + if (exp_meta := maybe_unpack_tma_experimental_metadata(v)) is not None: + from triton.tools.experimental_descriptor import ( # noqa: F401 + create_1d_tma_descriptor, + create_2d_tma_descriptor, + ) + + dims, block_dims, element_size = exp_meta + create_tma_descriptor = ( + create_1d_tma_descriptor + if len(dims) == 1 + else create_2d_tma_descriptor + ) + kwargs[k] = create_tma_descriptor( + tensor.data_ptr(), + *dims, + *block_dims, + element_size, + ) + else: + stable_meta = maybe_unpack_tma_stable_metadata(v) + if stable_meta is None: + raise AssertionError( + f"Failed to unpack stable TMA metadata for key {k}" + ) + from triton.tools.tensor_descriptor import TensorDescriptor + + block_shape = stable_meta[0] + + kwargs[k] = TensorDescriptor.from_tensor(tensor, block_shape) + + # move as many positional arguments from dicts to args as we + # can to circumvent the bug with the kwargs and pre_/post_hook: + # https://github.com/triton-lang/triton/issues/5082 + # TODO: remove this when the Triton issue above is fixed + args = [] + # copy kwargs and constant_args here to + # avoid mutating the original inputs + kwargs = kwargs.copy() + constant_args = constant_args.copy() + # pyrefly: ignore [missing-attribute] + for name in kernel.arg_names: + if name in kwargs: + args.append(kwargs.pop(name)) + elif name in constant_args: + args.append(constant_args.pop(name)) + else: + break + + # pyrefly: ignore [bad-index, index-error] + kernel[grid_fn](*args, **kwargs, **constant_args) + + +@triton_kernel_wrapper_mutation.py_impl(FakeTensorMode) +def triton_kernel_wrapper_mutation_fake_tensor_mode( + mode: FakeTensorMode, + *, + kernel_idx: int, + constant_args_idx: int, + grid: list["TritonGridType"], + tma_descriptor_metadata: TMADescriptorMetadata, + kwargs: dict[str, Any], +) -> None: + with mode: + return None + + +@triton_kernel_wrapper_mutation.py_impl(DispatchKey.Meta) +def _( + *, + kernel_idx: int, + constant_args_idx: int, + grid: list["TritonGridType"], + tma_descriptor_metadata: TMADescriptorMetadata, + kwargs: dict[str, Any], +) -> None: + return None + + +def trace_triton_kernel_wrapper( + proxy_mode: ProxyTorchDispatchMode, + func_overload: Callable[..., Any], + node_args: dict[str, Any], +) -> dict[str, Any] | None: + with disable_proxy_modes_tracing(): + out = func_overload(**node_args) + + proxy_args = pytree.tree_map( + proxy_mode.tracer.unwrap_proxy, # type: ignore[union-attr] + node_args, + ) + out_proxy = proxy_mode.tracer.create_proxy( + "call_function", + func_overload, + (), + proxy_args, + name=func_overload.__name__ + "_proxy", + ) + + ret = track_tensor_tree(out, out_proxy, constant=None, tracer=proxy_mode.tracer) + return ret + + +@triton_kernel_wrapper_mutation.py_impl(ProxyTorchDispatchMode) +def triton_kernel_wrapper_mutation_proxy_torch_dispatch_mode( + mode: ProxyTorchDispatchMode, + *, + kernel_idx: int, + constant_args_idx: int, + grid: list["TritonGridType"], + tma_descriptor_metadata: TMADescriptorMetadata, + kwargs: dict[str, Any], +) -> None: + trace_triton_kernel_wrapper( + mode, + triton_kernel_wrapper_mutation, + { + "kernel_idx": kernel_idx, + "constant_args_idx": constant_args_idx, + "grid": grid, + "tma_descriptor_metadata": tma_descriptor_metadata, + "kwargs": kwargs, + }, + ) + + return None + + +def get_mutated_tensors( + kernel_idx: int, + constant_args_idx: int, + kwargs: dict[str, Any], + tma_descriptor_metadata: TMADescriptorMetadata, +) -> list[str]: + kernel = kernel_side_table.get_kernel(kernel_idx) + constant_args = kernel_side_table.get_constant_args(constant_args_idx) + tensor_accesses = identify_accessed_tensors( + kernel, {**kwargs, **constant_args}, tma_descriptor_metadata + ) + # Filter to only tensor kwargs: with Triton 3.7+, ordered_arg_names + # includes scalars, so writes may reference non-tensor args like SymInts. + return [ + dep.name + for dep in tensor_accesses.read_writes.writes + if isinstance(kwargs.get(dep.name), Tensor) + ] + + +@triton_kernel_wrapper_mutation.py_functionalize_impl +def triton_kernel_wrapper_mutation_functionalize( + ctx: "BaseFunctionalizeAPI", + kernel_idx: int, + constant_args_idx: int, + grid: list["TritonGridType"], + tma_descriptor_metadata: TMADescriptorMetadata, + kwargs: dict[str, Any], +) -> None: + unwrapped_kwargs = ctx.unwrap_tensors(kwargs) # type: ignore[arg-type] + # TODO(oulgen): Preexisting bug, if two kernel inputs are views of each + # other, and one gets mutated in kernel, and later another gets mutated, + # they are no longer equal. Fix this by graph breaking on this condition + # earlier in dynamo. + tensors_to_clone = get_mutated_tensors( + kernel_idx, constant_args_idx, unwrapped_kwargs, tma_descriptor_metadata + ) + with ctx.redispatch_to_next(): + unwrapped_outputs = triton_kernel_wrapper_functional( + kernel_idx=kernel_idx, + constant_args_idx=constant_args_idx, + grid=grid, + tma_descriptor_metadata=tma_descriptor_metadata, + kwargs=unwrapped_kwargs, + tensors_to_clone=tensors_to_clone, + ) + + if not set(unwrapped_outputs.keys()).issubset(set(kwargs.keys())): + raise AssertionError( + f"Output keys {set(unwrapped_outputs.keys())} not subset of input keys {set(kwargs.keys())}" + ) + for key, output_arg in unwrapped_outputs.items(): + if not isinstance(output_arg, Tensor): + continue + input_arg = kwargs[key] + if not isinstance(input_arg, Tensor): + raise AssertionError( + f"Expected input_arg for key {key} to be a Tensor, got {type(input_arg)}" + ) + + ctx.replace(input_arg, output_arg) + # indicate that above replace is hidden from autograd + ctx.mark_mutation_hidden_from_autograd(input_arg) + ctx.commit_update(input_arg) + ctx.sync(input_arg) + return None + + +@triton_kernel_wrapper_functional.py_impl(DispatchKey.CompositeExplicitAutograd) +def triton_kernel_wrapper_functional_dense( + *, + kernel_idx: int, + constant_args_idx: int, + grid: list["TritonGridType"], + tma_descriptor_metadata: TMADescriptorMetadata, + kwargs: dict[str, Any], + tensors_to_clone: list[str], +) -> dict[str, Any]: + # TODO(oulgen): For performance reasons, we want to ensure that these + # `clone_preserve_strides` calls are never executed at runtime + # (inductor should always optimize them away). + # Requires https://github.com/pytorch/pytorch/issues/109240 + kwargs = { + key: (clone_preserve_strides(val) if key in tensors_to_clone else val) + for key, val in kwargs.items() + } + triton_kernel_wrapper_mutation( + kernel_idx=kernel_idx, + constant_args_idx=constant_args_idx, + grid=grid, + tma_descriptor_metadata=tma_descriptor_metadata, + kwargs=kwargs, + ) + return {key: val for key, val in kwargs.items() if key in tensors_to_clone} + + +@triton_kernel_wrapper_functional.py_impl(FakeTensorMode) +def triton_kernel_wrapper_functional_fake_tensor_mode( + mode: FakeTensorMode, + *, + kernel_idx: int, + constant_args_idx: int, + grid: list["TritonGridType"], + tma_descriptor_metadata: TMADescriptorMetadata, + kwargs: dict[str, Any], + tensors_to_clone: list[str], +) -> dict[str, Any]: + # TODO(oulgen): For performance reasons, we want to ensure that these + # `clone_preserve_strides` calls are never executed at runtime + # (inductor should always optimize them away). + # Requires https://github.com/pytorch/pytorch/issues/109240 + with mode: + return { + key: clone_preserve_strides(val) + for key, val in kwargs.items() + if key in tensors_to_clone + } + + +@triton_kernel_wrapper_functional.py_impl(ProxyTorchDispatchMode) +def triton_kernel_wrapper_functional_proxy_torch_dispatch_mode( + mode: ProxyTorchDispatchMode, + *, + kernel_idx: int, + constant_args_idx: int, + grid: list["TritonGridType"], + tma_descriptor_metadata: TMADescriptorMetadata, + kwargs: dict[str, Any], + tensors_to_clone: list[str], +) -> dict[str, Any]: + ret = trace_triton_kernel_wrapper( + mode, + triton_kernel_wrapper_functional, + { + "kernel_idx": kernel_idx, + "constant_args_idx": constant_args_idx, + "grid": grid, + "tma_descriptor_metadata": tma_descriptor_metadata, + "kwargs": kwargs, + "tensors_to_clone": tensors_to_clone, + }, + ) + if ret is None: + raise AssertionError("trace_triton_kernel_wrapper returned None") + return ret + + +@triton_kernel_wrapper_functional.py_functionalize_impl +def triton_kernel_wrapper_functional_functionalize( + ctx: "BaseFunctionalizeAPI", + kernel_idx: int, + constant_args_idx: int, + grid: list["TritonGridType"], + tma_descriptor_metadata: TMADescriptorMetadata, + kwargs: dict[str, Any], + tensors_to_clone: list[str], +) -> dict[str, Any]: + unwrapped_kwargs = ctx.unwrap_tensors(kwargs) # type: ignore[arg-type] + with ctx.redispatch_to_next(): + outputs = triton_kernel_wrapper_functional( + kernel_idx=kernel_idx, + constant_args_idx=constant_args_idx, + grid=grid, + tma_descriptor_metadata=tma_descriptor_metadata, + kwargs=unwrapped_kwargs, + tensors_to_clone=tensors_to_clone, + ) + return ctx.wrap_tensors(outputs) # type: ignore[return-value,arg-type] + + +triton_kernel_wrapper_mutation.fallthrough(DispatchKey.PythonDispatcher) # type: ignore[attr-defined] +triton_kernel_wrapper_mutation.fallthrough(DispatchKey.PythonTLSSnapshot) # type: ignore[attr-defined] +triton_kernel_wrapper_mutation.fallthrough(DispatchKey.ADInplaceOrView) +triton_kernel_wrapper_mutation.fallthrough(DispatchKey.BackendSelect) +triton_kernel_wrapper_mutation.fallthrough(DispatchKey.AutocastCPU) # type: ignore[attr-defined] +triton_kernel_wrapper_mutation.fallthrough(DispatchKey.AutocastCUDA) # type: ignore[attr-defined] +triton_kernel_wrapper_mutation.fallthrough(DispatchKey.AutogradCUDA) +triton_kernel_wrapper_mutation.fallthrough(DispatchKey.AutogradCPU) + +triton_kernel_wrapper_functional.fallthrough(DispatchKey.PythonDispatcher) # type: ignore[attr-defined] +triton_kernel_wrapper_functional.fallthrough(DispatchKey.PythonTLSSnapshot) # type: ignore[attr-defined] +triton_kernel_wrapper_functional.fallthrough(DispatchKey.ADInplaceOrView) +triton_kernel_wrapper_functional.fallthrough(DispatchKey.BackendSelect) +triton_kernel_wrapper_functional.fallthrough(DispatchKey.AutocastCPU) # type: ignore[attr-defined] +triton_kernel_wrapper_functional.fallthrough(DispatchKey.AutocastCUDA) # type: ignore[attr-defined] +triton_kernel_wrapper_functional.fallthrough(DispatchKey.AutogradCUDA) +triton_kernel_wrapper_functional.fallthrough(DispatchKey.AutogradCUDA) +triton_kernel_wrapper_functional.fallthrough(DispatchKey.AutogradCPU) + +# Adds SAC support for triton ops +redirect_to_mode(triton_kernel_wrapper_mutation, _CachingTorchDispatchMode) +redirect_to_mode(triton_kernel_wrapper_mutation, _CachedTorchDispatchMode) + +############################################################################### +# The "TritonHOPifier": a class that transforms a call to a triton kernel into +# a call to the triton_kernel_wrapper_mutation HOP. + + +class TritonHOPifier: + """Orchestrator for converting a user-defined triton kernel into a call + to the triton_kernel_wrapper_mutation HOP. + + It has two main use cases. + + 1. When Dynamo sees a triton kernel, it wraps it into a TritonKernelVariable + and uses the TritonHOPifier to convert calls to the TritonKernelVariable + into a call to the HOP. + + 2. In order to capture a user-defined triton kernel while performing + tracing (via make_fx or non-strict export), a user must annotate their + triton kernel with the `wrap_triton` decorator. The decorator uses + TritonHOPifier to convert calls to the triton kernel into a call + to the HOP (which can then be traced). + + Because Dynamo has its own calling conventions for e.g. invoking a user-defined function + TritonHOPifier is an abstract class that can be overridden by its subclasses. + """ + + def raise_unsupported(self, msg: str) -> Never: + raise NotImplementedError("abstract method") + + def is_callable(self, maybe_callable: Any) -> bool: + raise NotImplementedError("abstract method") + + def get_value(self, val: Any) -> Any: + raise NotImplementedError("abstract method") + + def call_grid( # type: ignore[no-untyped-def] + self, + grid, + meta, + tx, + ) -> tuple[int | sympy.Expr | SymInt, ...] | tuple["Proxy", ...]: + raise NotImplementedError("abstract method") + + def wrap_user_defined_obj( + self, + user_obj: Any, + tx: Optional["InstructionTranslator"], + variable: Union["TritonKernelVariable", "TraceableTritonKernelWrapper"] | None, + name: str, + ) -> Any: + raise NotImplementedError("abstract method") + + def call_user_defined_fn( + self, + user_fn: Callable[..., Any], + args: list, + kwargs: dict, + tx: Optional["InstructionTranslator"], + variable: Union["TritonKernelVariable", "TraceableTritonKernelWrapper"] | None, + ) -> Any: + raise NotImplementedError("abstract method") + + def maybe_unpack_configs( + self, configs: list["TritonConfig"], tx: Optional["InstructionTranslator"] + ) -> list["TritonConfig"]: + raise NotImplementedError("abstract method") + + def maybe_unpack_heuristic_result(self, result: Any) -> Any: + raise NotImplementedError("abstract method") + + @staticmethod + def do_prune_configs( # type: ignore[no-untyped-def] + autotuner: "TritonAutotunerType", + early_config_prune: Callable | None, + perf_model: Callable | None, + top_k: float, + configs: list, + named_args: dict, + kwargs: dict, + ) -> list["TritonConfig"]: + # Reimplement autotuner.prune_configs(...) here + # see: https://github.com/triton-lang/triton/blob/e57b46897191b3b3061c78d0d60e58e94be565b6/python/triton/runtime/autotuner.py # noqa: E501,B950 + # We do this to avoid calling prune_configs, which in turn calls early_config_prune and perf_model + # These are both user-defined functions which can contain side effects, so we want to sandbox them in Dynamo + + if early_config_prune: + configs = early_config_prune(configs, named_args, **kwargs) + + if perf_model: + # we assert top_k is a float before calling this + if isinstance(top_k, float) and top_k <= 1.0: + top_k = int(len(configs) * top_k) + elif not isinstance(top_k, int): + """ + Slice index must be an integer, SupportsIndex or None + """ + raise TypeError( + "Error while pruning configs, top_k must be either 1) a float <= 1.0 or 2) an int" + ) + if len(configs) > top_k: + est_timing = [ + ( + config, + float( + perf_model(**named_args, **kwargs, **config.all_kwargs()) + ), + ) + for config in configs + ] + configs = [ + config[0] + for config in sorted(est_timing, key=operator.itemgetter(1))[:top_k] + ] + return configs + + def call_HOP( # type: ignore[no-untyped-def] + self, + variable, + grids, + combined_args: dict[str, Any], + tx, + ) -> Optional["ConstantVariable"]: + raise NotImplementedError("abstract method") + + def check_grid( # type: ignore[no-untyped-def] + self, grid + ) -> tuple[int | sympy.Expr | SymInt, ...] | tuple["Proxy", ...]: + raise NotImplementedError("abstract method") + + def init_variable( + self, + variable: Union["TraceableTritonKernelWrapper", "TritonKernelVariable"], + kernel: "TritonKernelType", + kernel_idx: int | None, + grid: Optional["TritonGridType"], + ) -> None: + from triton.runtime.autotuner import Autotuner + + if kernel is None: + raise AssertionError("kernel cannot be None") + + variable.kernel = kernel + variable.kernel_idx = kernel_side_table.add_kernel(kernel) + + if kernel_idx is not None and variable.kernel_idx != kernel_idx: + raise AssertionError( + f"kernel_idx mismatch: expected {kernel_idx}, got {variable.kernel_idx}" + ) + + # pyrefly: ignore [bad-assignment] + variable.grid = grid + + if isinstance(kernel, Autotuner): + import torch + import torch._dynamo + + # We only support configs, keys, and restore_value arguments + # of triton.autotune. Make sure other arguments are defaulted. + defaults = inspect.signature(Autotuner.__init__).parameters + # Newer version of triton change attribute name from warmup to num_warmup and rep to num_rep. + # The call to get_first_attr is to maintain backward-compatibility. + + def defaults_ok( + attr: str, alternates: tuple[str, ...], values: tuple[Any, ...] + ) -> bool: + if attr not in defaults: + return True + value = torch._dynamo.utils.get_first_attr(kernel, attr, *alternates) + if value == defaults[attr].default: + return True + return value in values + + if ( + not torch._inductor.config.unsafe_ignore_unsupported_triton_autotune_args + and ( + not defaults_ok("num_warmups", ("warmup",), (25, None)) + or not defaults_ok("num_reps", ("rep",), (100, None)) + or not defaults_ok("use_cuda_graph", (), (False,)) + ) + ): + self.raise_unsupported( + "Only configs, keys, restore_value, and reset_to_zero are supported for triton.autotune" + ) + if ( + not torch._inductor.config.unsafe_ignore_unsupported_triton_autotune_args + and ( + # pre_hook requires running arbitrary code at runtime, which we cannot handle at this time + # https://github.com/pytorch/pytorch/issues/139059 + # we can't support pre_hook or post_hook in user defined triton kernels at the moment, + # as they require the ability to execute code at runtime (AOTI can't support this) + ( + hasattr(kernel, "user_defined_pre_hook") + and kernel.user_defined_pre_hook is not False + ) + or ( + hasattr(kernel, "user_defined_post_hook") + and kernel.user_defined_post_hook is not False + ) + or ( + # Check Config passed to autotuner in configs + any(cfg.pre_hook is not None for cfg in kernel.configs) + ) + ) + ): + self.raise_unsupported( + "pre_hook and post_hook are not supported in triton.Autotune or triton.Config" + ) + + @staticmethod + def get_kernel_source( + variable: "TritonKernelVariable | TraceableTritonKernelWrapper", + ) -> "Source | None": + kernel_source = getattr(variable, "kernel_source", None) + if kernel_source is None: + kernel_source = getattr(variable, "source", None) + return kernel_source + + def recreate_variable( + self, + variable: "TritonKernelVariable | TraceableTritonKernelWrapper", + *, + kernel: "TritonKernelType", + kernel_idx: int | None, + grid: "TritonGridType | None", + ) -> "TritonKernelVariable | TraceableTritonKernelWrapper": + return type(variable)( + kernel=kernel, + kernel_idx=kernel_idx, + grid=grid, + kernel_source=self.get_kernel_source(variable), + ) + + def call_getitem( + self, + variable: Union["TritonKernelVariable", "TraceableTritonKernelWrapper"], + args: Sequence[Any], + ) -> Union["TritonKernelVariable", "TraceableTritonKernelWrapper"]: + # __getitem__ should only be called if we don't already have a grid + # Only grid needs to be passed + if variable.grid is not None or len(args) != 1: + self.raise_unsupported( + "Triton kernels should be called with only a single grid" + ) + + return self.recreate_variable( + variable, + kernel=variable.kernel, + kernel_idx=variable.kernel_idx, + grid=args[0], + ) + + def call_run( + self, + variable: Union["TritonKernelVariable", "TraceableTritonKernelWrapper"], + args: Sequence[Any], + kwargs: dict[str, Any], + tx: Optional["InstructionTranslator"], + ) -> Optional["ConstantVariable"]: + if "grid" not in kwargs: + self.raise_unsupported("Triton kernel requires to be called with a grid") + grid = kwargs.pop("grid") + kwargs.pop("warmup", None) + # rewrite kernel.run(*args, grid=grid) to kernel[grid](*args) + return self.call_triton_kernel( + self.recreate_variable( + variable, + kernel=variable.kernel, + kernel_idx=variable.kernel_idx, + grid=grid, + ), + args, + kwargs, + tx, + ) + + def call_triton_kernel( + self, + variable: Union["TritonKernelVariable", "TraceableTritonKernelWrapper"], + args: Sequence[Any], + kwargs: dict[str, Any], + tx: Optional["InstructionTranslator"], + ) -> Optional["ConstantVariable"]: + from triton import JITFunction + from triton.runtime.autotuner import autotune, Autotuner, Config, Heuristics + + # Check if num_ctas is in kwargs + if "num_ctas" in kwargs: + self.raise_unsupported( + "Passing num_ctas directly to the Triton kernel is not supported. " + "Please use a Config in @triton.autotune instead." + ) + + # Make sure the kernel has a grid + if variable.grid is None: + self.raise_unsupported("Triton kernels should always be called with a grid") + + # raise an exception if there are multiple @triton.autotune decorators + iter_kernel = variable.kernel + autotuner_count = 0 + while not isinstance(iter_kernel, JITFunction): + if isinstance(iter_kernel, Autotuner): + autotuner_count += 1 + if autotuner_count > 1: + self.raise_unsupported( + "Passing multiple @triton.autotune decorators is not supported. " + "Please use a single @triton.autotune decorator instead." + ) + + iter_kernel = iter_kernel.fn + + # Process the @triton.heuristics decorator: + # - We know there is only 1 autotuner decorator here + # - We can apply the heuristic to all triton.Configs in the order that the decorators appear + # This way, when the config is selected, the heuristics have already been applied. + # - Decorators that appear *before* the autotuner are already processed correctly + if isinstance(variable.kernel, Autotuner) and isinstance( + variable.kernel.fn, Heuristics + ): + # unwrap the heuristics decorator, we don't need it anymore + # variable.kernel ==> Autotuner + # variable.kernel.fn ==> Heuristics + # ... + # There can be arbitrarily many heuristics wrappers here! + # ... + # variable.kernel.fn ==> JITFunction + + # Copy the configs, we are going to be modifying them + new_configs = copy.deepcopy(variable.kernel.configs) + + named_args = dict(zip(variable.kernel.arg_names, args)) + + # Iterate through all of the heuristics wrappers that come after the autotune wrapper + iter_kernel = variable.kernel.fn + while isinstance(iter_kernel, Heuristics): + # For each config, apply the heuristic fn(s) + for config_idx in range(len(new_configs)): + for kwarg_key, heuristic_fn in iter_kernel.values.items(): + # Run heuristics on the combined configs + kwargs + heuristic_result = self.call_user_defined_fn( + heuristic_fn, + [ + { + **named_args, + **kwargs, + **new_configs[config_idx].__dict__["kwargs"], + }, + ], + {}, + tx, + variable, + ) + + # Update the kwargs in each config + # maybe_unpack_heuristic_result raises unsupported if the value is non-constant + new_configs[config_idx].__dict__["kwargs"][kwarg_key] = ( + self.maybe_unpack_heuristic_result(heuristic_result) + ) + + iter_kernel = iter_kernel.fn + if not isinstance(iter_kernel, JITFunction): + raise AssertionError( + f"Expected iter_kernel to be a JITFunction, got {type(iter_kernel)}" + ) + prune_configs_by = { + "perf_model": variable.kernel.perf_model, + "early_config_prune": variable.kernel.early_config_prune, + "configs_top_k": variable.kernel.configs_top_k, + } + new_kernel = autotune( + configs=new_configs, key=[], prune_configs_by=prune_configs_by + )(iter_kernel) + # create a new variable to contain the new (wrapped) kernel; + # skip kernel_idx to get a new record in the kernel side table + new_var = self.recreate_variable( + variable, + kernel=new_kernel, + kernel_idx=None, + grid=variable.grid, + ) + return self.call_triton_kernel(new_var, args, kwargs, tx) + + SPECIAL_CONFIG_NAMES = { + "num_warps", + "num_stages", + "num_ctas", + "num_consumer_groups", + "num_buffers_warp_spec", + "num_cpu_threads", + } + + # move special config names to configs out of kwargs + special_kwargs = {} + for name in SPECIAL_CONFIG_NAMES: + if name in kwargs: + # remove special kwargs from `kwargs` + val = kwargs.pop(name) + special_kwargs[name] = self.get_value(val) + + if special_kwargs: + if isinstance(variable.kernel, Autotuner): + # if there is Autotuner already, set + # special kwargs to each of its configs + new_configs = copy.deepcopy(variable.kernel.configs) + for config in new_configs: + config.__dict__.update(special_kwargs) + prune_configs_by = { + "perf_model": variable.kernel.perf_model, + "early_config_prune": variable.kernel.early_config_prune, + "configs_top_k": variable.kernel.configs_top_k, + } + + new_kernel = autotune( + configs=new_configs, key=[], prune_configs_by=prune_configs_by + )(variable.kernel.fn) + else: + # if there is no Autotuner, wrap the kernel into a + # new one with a single config with special kwargs + new_config = Config(kwargs={}, **special_kwargs) + + new_kernel = autotune(configs=[new_config], key=[])(variable.kernel) + + # create a new variable to contain the new (wrapped) kernel; + # skip kernel_idx to get a new record in the kernel side table + new_var = self.recreate_variable( + variable, + kernel=new_kernel, + kernel_idx=None, + grid=variable.grid, + ) + return self.call_triton_kernel(new_var, args, kwargs, tx) + + if isinstance(variable.kernel, Autotuner): + special_param_names = [] + for name in SPECIAL_CONFIG_NAMES: + if name in variable.kernel.fn.arg_names: + special_param_names.append(name) + + if special_param_names: + # If the Triton kernel has SPECIAL_CONFIG_NAMES in parameters, those should + # be passed from the kernel configs: the behavior of Triton runtime is that + # those values get folded into the kernel arguments iff there are parameters + # with the same name. Normally the values of those parameters are defined + # outside the `kwargs` part of the autotuning configs. Here we move them to + # the `kwargs` part (if they're absent there) to facilitate passing them as + # arguments to the kernel downstream. + updated = False + new_configs = copy.deepcopy(variable.kernel.configs) + for config in new_configs: + for name in special_param_names: + if name not in config.__dict__["kwargs"]: + if name not in config.__dict__: + raise AssertionError( + f"{name} must be in autotuning configs to be used " + "as a kernel parameter" + ) + config.__dict__["kwargs"][name] = config.__dict__[name] + updated = True + + if updated: + prune_configs_by = { + "perf_model": variable.kernel.perf_model, + "early_config_prune": variable.kernel.early_config_prune, + "configs_top_k": variable.kernel.configs_top_k, + } + + new_kernel = autotune( + configs=new_configs, prune_configs_by=prune_configs_by, key=[] + )(variable.kernel.fn) + new_var = self.recreate_variable( + variable, + kernel=new_kernel, + kernel_idx=None, + grid=variable.grid, + ) + return self.call_triton_kernel(new_var, args, kwargs, tx) + + # These are the default values in upstream Triton + # see: https://github.com/triton-lang/triton/blob/e57b46897191b3b3061c78d0d60e58e94be565b6/python/triton/runtime/autotuner.py # noqa: E501,B950 + default_perf_model = None + default_early_config_prune = None + + # run prune_configs_by + if isinstance(variable.kernel, Autotuner) and ( + variable.kernel.perf_model != default_perf_model + or variable.kernel.early_config_prune != default_early_config_prune + ): + # Prune the configs + named_args = dict(zip(variable.kernel.arg_names, args)) + + # The source information is important here so the guards are installed correctly + + wrapped_early_configs_prune = self.wrap_user_defined_obj( + variable.kernel.early_config_prune, + tx, + variable, + "early_config_prune", + ) + + wrapped_perf_model = self.wrap_user_defined_obj( + variable.kernel.perf_model, tx, variable, "perf_model" + ) + + wrapped_configs_top_k = self.wrap_user_defined_obj( + variable.kernel.configs_top_k, tx, variable, "configs_top_k" + ) + + wrapped_configs = self.wrap_user_defined_obj( + variable.kernel.configs, tx, variable, "configs" + ) + + pruned_configs = self.call_user_defined_fn( + self.do_prune_configs, + [ + variable, + wrapped_early_configs_prune, + wrapped_perf_model, + wrapped_configs_top_k, + wrapped_configs, + named_args, + kwargs, + ], + {}, + tx, + variable, + ) + + pruned_configs = self.maybe_unpack_configs(pruned_configs, tx) + + # after pruning the configs, create a new autotuner object with + # these configs and recurse. + new_kernel = autotune(configs=pruned_configs, key=[])(variable.kernel.fn) + # create a new variable to contain the new (wrapped) kernel; + # skip kernel_idx to get a new record in the kernel side table + new_var = self.recreate_variable( + variable, + kernel=new_kernel, + kernel_idx=None, + grid=variable.grid, + ) + return self.call_triton_kernel(new_var, args, kwargs, tx) + + # Both for grid's meta as well as for the kernel, we need combined + # args and kwargs combined and normalized + + combined_args_raw = {**dict(zip(variable.kernel.arg_names, args)), **kwargs} + + # precompute the grid for the kernel + configs = ( + [config.kwargs for config in variable.kernel.configs] + if isinstance(variable.kernel, Autotuner) + else [{}] + ) + grids = [] + for config_args in configs: + # If the grid is a function, then lets execute it and convert it to + # a list + grid = variable.grid + if grid is None: + raise AssertionError("grid cannot be None at this point") + if self.is_callable(grid): + # Populate the special "meta" argument to call the grid function + meta = {**combined_args_raw, **config_args} + grid = self.call_grid(grid, meta, tx) # type: ignore[arg-type] + grids.append(self.check_grid(grid)) + + for i in range(len(grids)): + if not isinstance(grids[i], tuple): + self.raise_unsupported("Only tuple grids are supported") + # inductor expects all grids to be 3-tuple so lets make it + if len(grids[i]) == 1: + grids[i] = (grids[i][0], 1, 1) + elif len(grids[i]) == 2: + grids[i] = (grids[i][0], grids[i][1], 1) + elif len(grids[i]) > 3: + self.raise_unsupported("Grid can have at most rank 3") + + if len(grids) == 0: + raise AssertionError("grids cannot be empty") + if isinstance(variable.kernel, JITFunction): + constexprs = [p.num for p in variable.kernel.params if p.is_constexpr] + arg_names = [p.name for p in variable.kernel.params] + else: + # If we are looking at an @triton.autotune decorator, the nested function should be a JITFunction + # This is because we don't support @triton.heuristics or nested @triton.autotune decorators yet + if not isinstance(variable.kernel, Autotuner): + raise AssertionError( + f"Expected variable.kernel to be an Autotuner, got {type(variable.kernel)}" + ) + constexprs = [p.num for p in variable.kernel.fn.params if p.is_constexpr] + arg_names = [p.name for p in variable.kernel.fn.params] + + for idx, arg_name in enumerate(arg_names): + if idx in constexprs: + if arg_name in combined_args_raw: + # [Note: Specialize tl.constexpr args in user-defined triton kernels] + # This arg is marked as tl.constexpr. That means that triton will recompile every time + # this value changes. + # https://github.com/pytorch/pytorch/issues/136504 + # One option is to correctly pass the symints in so that the symbolic expressions are defined + # when the triton code is being executed. + # But since triton will have to recompile either way, we instead just specialize on the value. + # + # Depending on the type of `variable` we might expect different types for the symbolic args: + # either SymNodeVariables (for TritonKernelVariables) or SymInts (TracingTritonKernelWrapper) + combined_args_raw[arg_name] = variable.specialize_symbolic( + combined_args_raw[arg_name] + ) + return self.call_HOP(variable, grids, combined_args_raw, tx) + + +############################################################################### +# Helpers for wrap_triton API that makes a user-defined triton kernel traceable into +# a graph via make_fx or non-strict export (coming soon) + + +class TracingTritonHOPifier(TritonHOPifier): + def raise_unsupported(self, msg: str) -> Never: + raise RuntimeError(msg) + + def is_callable(self, maybe_callable: Any) -> bool: + return callable(maybe_callable) + + def get_value(self, val: Any) -> Any: + return val + + def call_grid( + self, + grid: "TritonGridCallableType", + meta: "TritonMetaParamsType", + tx: None, + ) -> tuple[int | sympy.Expr | SymInt, ...]: + if tx is not None: + raise AssertionError("tx must be None for TracingTritonHOPifier") + if not isinstance(meta, dict): + raise AssertionError(f"meta must be a dict, got {type(meta)}") + if not callable(grid): + raise AssertionError(f"grid must be callable, got {type(grid)}") + return grid(meta) + + def wrap_user_defined_obj( + self, + user_obj: Any, + tx: Optional["InstructionTranslator"], + variable: Union["TritonKernelVariable", "TraceableTritonKernelWrapper"] | None, + name: str, + ) -> Any: + if tx is not None: + raise AssertionError("tx must be None for TracingTritonHOPifier") + return user_obj + + def call_user_defined_fn( + self, + user_fn: Callable[..., Any], + args: list, + kwargs: dict, + tx: Optional["InstructionTranslator"], + variable: Union["TritonKernelVariable", "TraceableTritonKernelWrapper"] | None, + ) -> Any: + if not isinstance(args, list): + raise AssertionError(f"args must be a list, got {type(args)}") + if not isinstance(kwargs, dict): + raise AssertionError(f"kwargs must be a dict, got {type(kwargs)}") + if not callable(user_fn): + raise AssertionError(f"user_fn must be callable, got {type(user_fn)}") + return user_fn(*args, **kwargs) + + def maybe_unpack_configs( + self, configs: list["TritonConfig"], tx: Optional["InstructionTranslator"] + ) -> list["TritonConfig"]: + if not isinstance(configs, list): + raise AssertionError(f"configs must be a list, got {type(configs)}") + return configs + + def maybe_unpack_heuristic_result(self, result: Any) -> Any: + return result + + def check_grid( + self, + grid: "TritonGridType", + ) -> tuple[int | sympy.Expr | SymInt, ...]: + if not isinstance(grid, collections.abc.Sequence): + raise RuntimeError( + "wrap_triton can only handle grids that resolve to Sequence[int]." + ) + # normalize to tuple + return tuple(grid) + + def store_non_graphable_args( + self, + combined_args: dict[str, Any], + ) -> tuple[dict, int]: + """ + Some args cannot be stored in the FX graph. + Put them in the side table. + """ + + def is_graphable(val: Any) -> bool: + return isinstance(val, (fx.node.base_types, fx.Node)) + + non_graphable_args = { + k: v for k, v in combined_args.items() if not is_graphable(v) + } + graphable_args = {k: v for k, v in combined_args.items() if is_graphable(v)} + + constant_args_idx = kernel_side_table.add_constant_args(non_graphable_args) + + return graphable_args, constant_args_idx + + def call_HOP( + self, + variable: "TraceableTritonKernelWrapper", + grids: list["TritonGridTupleType"], + combined_args: dict[str, Any], + tx: None, + ) -> None: + if tx is not None: + raise AssertionError("tx must be None for TracingTritonHOPifier") + if not isinstance(variable, TraceableTritonKernelWrapper): + raise AssertionError( + f"Expected TraceableTritonKernelWrapper, got {type(variable)}" + ) + + graphable_args, constant_args_idx = self.store_non_graphable_args(combined_args) + + if not isinstance(variable.kernel_idx, int): + raise AssertionError( + f"kernel_idx must be an int, got {type(variable.kernel_idx)}" + ) + return triton_kernel_wrapper_mutation( + kernel_idx=variable.kernel_idx, + constant_args_idx=constant_args_idx, + grid=grids, # type: ignore[arg-type] + # TMA descriptor capturing not yet + # supported in non-dynamo tracing + tma_descriptor_metadata={}, + kwargs=graphable_args, + ) + + +tracing_triton_hopifier_singleton = TracingTritonHOPifier() + + +class TraceableTritonKernelWrapper: + kernel: "TritonKernelType" + kernel_idx: int | None + grid: Optional["TritonGridType"] + kernel_source: "Source | None" + + def __init__( + self, + kernel: "TritonKernelType", + kernel_idx: int | None, + grid: Optional["TritonGridType"], + kernel_source: "Source | None" = None, + ) -> None: + self.kernel = None + self.grid = None + self.kernel_source = kernel_source + tracing_triton_hopifier_singleton.init_variable(self, kernel, kernel_idx, grid) + if self.kernel is None: + raise AssertionError("kernel was not initialized properly") + + def __getitem__(self, *args: Sequence[Any]) -> "TraceableTritonKernelWrapper": + return tracing_triton_hopifier_singleton.call_getitem(self, args) # type: ignore[return-value] + + def run(self, *args: Sequence[Any], **kwargs: dict[str, Any]) -> Any: + from torch._library.triton import is_wrap_triton_enabled + + if is_wrap_triton_enabled(): + return tracing_triton_hopifier_singleton.call_run(self, args, kwargs, None) + else: + if self.kernel is None: + raise AssertionError("kernel cannot be None") + # pyrefly: ignore [missing-attribute] + return self.kernel.run(*args, **kwargs) + + def __call__(self, *args: Sequence[Any], **kwargs: dict[str, Any]) -> Any: + from torch._library.triton import is_wrap_triton_enabled + + if is_wrap_triton_enabled(): + return tracing_triton_hopifier_singleton.call_triton_kernel( + self, args, kwargs, None + ) + else: + if self.kernel is None: + raise AssertionError("kernel cannot be None") + # pyrefly: ignore [bad-index, index-error] + return self.kernel[self.grid](*args, **kwargs) + + def specialize_symbolic(self, arg: Sequence[Any]) -> Any: + import torch + + # See [Note: Specialize tl.constexpr args in user-defined triton kernels] + if isinstance(arg, (torch.SymInt, torch.SymBool, torch.SymFloat)): + return guard_scalar(arg) + return arg diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..a4baf0be6f2bbcd8ec6f9fd58744ace44c33418a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/utils.py @@ -0,0 +1,1391 @@ +# mypy: allow-untyped-defs +import contextlib +import functools +from collections.abc import Callable, Iterable, Mapping, Sequence +from contextlib import AbstractContextManager, contextmanager, ExitStack, nullcontext +from dataclasses import dataclass +from typing import Any, overload, TypeVar + +import torch +import torch.fx.traceback as fx_traceback +import torch.utils._pytree as pytree +from torch._dispatch.python import suspend_functionalization +from torch._guards import detect_fake_mode +from torch._higher_order_ops.schema import HopSchema +from torch._library.fake_class_registry import FakeScriptObject +from torch._library.opaque_object import is_opaque_type +from torch._ops import HigherOrderOperator, OperatorBase, OpOverload +from torch._subclasses.fake_tensor import FakeTensor +from torch._subclasses.functional_tensor import ( + disable_functional_mode, + FunctionalTensor, +) +from torch.fx.experimental.proxy_tensor import ( + _temp_remove_metadata_torch_function_mode, + disable_proxy_modes_tracing, + make_fx, +) +from torch.fx.passes.runtime_assert import insert_deferred_runtime_asserts +from torch.fx.passes.shape_prop import _extract_tensor_metadata, TensorMetadata +from torch.multiprocessing.reductions import StorageWeakRef + + +@dataclass +class UnsupportedAliasMutationException(RuntimeError): + reason: str + + +def autograd_not_implemented_inner( + operator: OperatorBase, delayed_error: bool, *args: Any, **kwargs: Any +) -> Any: + """If autograd is enabled and any of the arguments require grad this will either + raise an error or return a DelayedError depending on the value of delayed. + + Args: + operator: The Operator to call with the *args and **kwargs with + op_name: The name of the Operator + delayed_error: If True, return a DelayedError instead of raising an error + args: The flattened operands to the Operator + kwargs: The keyword arguments to the Operator + + Raises: + RuntimeError: If autograd is enabled and any of the arguments to the Operator + """ + with torch._C._AutoDispatchBelowAutograd(): + result = operator(*args, **kwargs) + flat_operands = pytree.arg_tree_leaves(*args) + if torch.is_grad_enabled() and any( + f.requires_grad for f in flat_operands if isinstance(f, torch.Tensor) + ): + if delayed_error: + err_fn = torch._C._functions.DelayedError( + f"Autograd not implemented for {str(operator)}", + 1, + ) + + def fake_requires_grad(tensor): + if torch.is_floating_point(tensor) or torch.is_complex(tensor): + tensor = tensor.detach() + tensor.requires_grad = True + return tensor + + return pytree.tree_map_only( + torch.Tensor, lambda x: err_fn(fake_requires_grad(x)), result + ) + else: + raise RuntimeError(f"Autograd not implemented for {str(operator)}") + return result + + +def autograd_not_implemented(op: OperatorBase, deferred_error: bool) -> Callable: + def inner(*args, **kwargs): + return autograd_not_implemented_inner(op, deferred_error, *args, **kwargs) + + return inner + + +def _maybe_run_with_interpreter(fn): + maybe_interpreted_fn = fn + if isinstance(fn, torch.fx.GraphModule) and fx_traceback.has_preserved_node_meta(): + # Running graph with interpreter is needed for propagating the stack_trace + def graph_with_interpreter(*args): + with fx_traceback.preserve_node_meta(): + return torch.fx.Interpreter(fn).run(*args) + + maybe_interpreted_fn = graph_with_interpreter + return maybe_interpreted_fn + + +def _hop_compile_and_call(fn, args, kwargs=None): + """Compile and call fn with fullgraph=True for HOP eager execution. + + Pre-activates the fullgraph counter so that compile_wrapper treats this as + a nested compile. This avoids erroring when a non-infra dispatch mode + causes the frame to be skipped — the function still executes eagerly within + compile_wrapper and returns normally. + """ + from torch._dynamo.eval_frame import set_fullgraph_compiled_frame_count + + with setup_compilation_env() as backend: + old_count = set_fullgraph_compiled_frame_count(0) + try: + return torch.compile(fn, backend=backend, fullgraph=True)( + *args, **(kwargs or {}) + ) + finally: + set_fullgraph_compiled_frame_count(old_count) + + +def _maybe_compile_and_run_fn(fn, *args): + if not torch.compiler.is_dynamo_compiling(): + return _hop_compile_and_call(fn, args) + else: + return fn(*args) + + +def reenter_make_fx(fn, subgraph_decomp_table=None): + from torch.fx.experimental.proxy_tensor import _CURRENT_MAKE_FX_TRACER + + @functools.wraps(fn) + def wrapped(*args): + if _CURRENT_MAKE_FX_TRACER is None: + raise AssertionError( + "Cannot reenter make_fx when we're not under a make_fx tracing session" + ) + if subgraph_decomp_table is None: + gm = _CURRENT_MAKE_FX_TRACER.trace_subgraph( + _maybe_run_with_interpreter(fn), *args + ) + else: + gm = _CURRENT_MAKE_FX_TRACER.trace_subgraph_custom_decomp( + _maybe_run_with_interpreter(fn), subgraph_decomp_table, *args + ) + + return gm + + return wrapped + + +def _maybe_reenter_make_fx(fn, subgraph_decomp_table=None): + from torch.fx.experimental.proxy_tensor import _CURRENT_MAKE_FX_TRACER + + if _CURRENT_MAKE_FX_TRACER is not None: + return reenter_make_fx(fn, subgraph_decomp_table=subgraph_decomp_table) + else: + + def _maybe_make_fx_with_fake_mode(fn): + @functools.wraps(fn) + def wrapped(*args): + from torch._guards import detect_fake_mode + + fake_mode = detect_fake_mode(args) + if fake_mode is None: + # we creaeta a fake_mode here to make sure we could + # trace the graph with data-dependent calls e.g. .item() + return make_fx( + fn, + tracing_mode="fake", + decomposition_table=subgraph_decomp_table, + )(*args) + # Tracing with real if all inputs have been fakfied + return make_fx(fn, decomposition_table=subgraph_decomp_table)(*args) + + return wrapped + + return _maybe_make_fx_with_fake_mode(fn) + + +def check_meta_consistency( + lhs_list: list[torch.Tensor | torch.SymInt | int], + rhs_list: list[torch.Tensor | torch.SymInt | int], + lhs_name: str, + rhs_name: str, + include_contiguity: bool = True, +) -> None: + def diff_meta_pairs( + lhs_list: list[torch.Tensor | torch.SymInt | int], + rhs_list: list[torch.Tensor | torch.SymInt | int], + ) -> list[str]: + def diff_meta( + lhs: torch.Tensor | torch.SymInt | int, + rhs: torch.Tensor | torch.SymInt | int, + ) -> str: + if isinstance(lhs, torch.Tensor) and isinstance(rhs, torch.Tensor): + return ", ".join( + diff_tensor_meta( + _extract_tensor_metadata( + lhs, include_contiguity=include_contiguity + ), + _extract_tensor_metadata( + rhs, include_contiguity=include_contiguity + ), + check_grad=False, + ) + ) + else: + + def _both_int_types(lhs, rhs): + return isinstance(lhs, (int, torch.SymInt)) and isinstance( + rhs, (int, torch.SymInt) + ) + + def _both_tensor(lhs, rhs): + return isinstance(lhs, torch.Tensor) and isinstance( + rhs, torch.Tensor + ) + + if not _both_int_types(lhs, rhs) and not _both_tensor(lhs, rhs): + return f"type: {lhs} vs {rhs}" + + return "" + + # Manually check the device of lhs and rhs as this field is currently not part of TensorMetadata + def diff_device( + lhs: torch.Tensor | torch.SymInt | int, + rhs: torch.Tensor | torch.SymInt | int, + ) -> str: + if isinstance(lhs, torch.Tensor) and isinstance(rhs, torch.Tensor): + if ( + rhs.device.type == lhs.device.type + and rhs.device.index == lhs.device.index + ): + return "" + else: + return "device" + return "" + + if len(lhs_list) != len(rhs_list): + raise torch._dynamo.exc.UncapturedHigherOrderOpError( + f"Expected {lhs_name} and {rhs_name} to have same number of outputs but got lhs:{lhs_list} and rhs:{rhs_list}" + ) + all_diffs = [] + for i, (lhs, rhs) in enumerate(zip(lhs_list, rhs_list)): + if diff := diff_meta(lhs, rhs): + all_diffs.append( + f"pair[{i}] differ in {diff}, where lhs is {lhs} and rhs is {rhs}" + ) + if diff := diff_device(lhs, rhs): + all_diffs.append( + f"pair[{i}] differ in {diff}, where lhs is {lhs} and rhs is {rhs}" + ) + return all_diffs + + if all_diffs := diff_meta_pairs(lhs_list, rhs_list): + diff_str = "\n".join(all_diffs) + raise torch._dynamo.exc.UncapturedHigherOrderOpError( + f"Expected {lhs_name} and {rhs_name} to have same metadata but found:\n{diff_str}" + ) + + +# Thread-local flag to indicate we're inside HOP internal compilation +import threading + + +_hop_compile_tls = threading.local() + + +def _in_hop_compile() -> bool: + return getattr(_hop_compile_tls, "in_hop_compile", False) + + +@contextmanager +def setup_compilation_env(): + """ + Context manager that sets up proper environment and backend when invoking torch.compile + inside torch.export region or inside HOP. + """ + from torch._dynamo.backends.debugging import ( + make_eager_backend_with_torch_function_modes, + ) + from torch._dynamo.backends.registry import lookup_backend + from torch.fx.experimental.proxy_tensor import ( + _temp_remove_pre_dispatch_torch_function_mode, + ) + + old_in_hop_compile = getattr(_hop_compile_tls, "in_hop_compile", False) + _hop_compile_tls.in_hop_compile = True + try: + with ( + _set_compilation_env(), + torch._dynamo.utils.disable_cache_limit(), + _temp_remove_pre_dispatch_torch_function_mode() as pre_dispatch_mode, + _temp_remove_metadata_torch_function_mode() as metadata_mode, + ): + modes = [ + mode for mode in (pre_dispatch_mode, metadata_mode) if mode is not None + ] + if modes: + yield make_eager_backend_with_torch_function_modes(modes) + else: + yield lookup_backend("eager") + finally: + _hop_compile_tls.in_hop_compile = old_in_hop_compile + + +@contextmanager +def _set_compilation_env(): + _old_is_tracing = torch.fx._symbolic_trace._is_fx_tracing_flag + _old_allow_empty_graphs = torch._dynamo.config.allow_empty_graphs + _old_capture_scalar_outputs = torch._dynamo.config.capture_scalar_outputs + # The issue is tracked in https://github.com/pytorch/pytorch/issues/144360: when dynamo finds + # the top-level frame produces no graph, the default behavior is to fallback to eager. + # Then when it encounters an inner function, it will try to trace that function again, which is unnecessary. + # For while_loop, during inspecting the inner call, we trace into the python dispathcer + # logic, which is not tracable as of today. So the proper fix can be either 1. allow dispatch + # logic to be dynamo tracable or 2. fixing https://github.com/pytorch/pytorch/issues/144360. + # but it exposes some bugs in existing tests so we have to have a temporary flag to control + # the behavior, which allows dynamo to store an empty graph for a frame without falling back to eager + try: + # We need to turn off the is_fx_tracing_flag. Remove this flag check from dyanmo + # once we are confident fx tracing works with dynamo. + torch.fx._symbolic_trace._is_fx_tracing_flag = False + # pyrefly: ignore [bad-assignment] + torch._dynamo.config.allow_empty_graphs = True + torch._dynamo.config.capture_scalar_outputs = True + yield + finally: + torch.fx._symbolic_trace._is_fx_tracing_flag = _old_is_tracing + torch._dynamo.config.allow_empty_graphs = _old_allow_empty_graphs + torch._dynamo.config.capture_scalar_outputs = _old_capture_scalar_outputs + + +# The invariant here is that we always trace the branch with fake tensor +def _maybe_fake_tracing(fn, inputs: list[Any], pre_dispatch): + fake_mode_det = None + for inp in pytree.tree_leaves(inputs): + if isinstance(inp, FakeTensor): + fake_mode_det = inp.fake_mode + break + + fake_mode: AbstractContextManager = nullcontext() + tracing_mode = "fake" + if fake_mode_det is not None: + fake_mode = fake_mode_det + tracing_mode = "real" + + # Note: we need to turn off proxy tensor mode to avoid tracing infra + # code that happens in make_fx e.g. we now call as_strided when wrapping tensor + # as fake tensor. + with fake_mode, disable_proxy_modes_tracing(): + gm = make_fx( + fn, + tracing_mode=tracing_mode, + pre_dispatch=pre_dispatch, + _error_on_data_dependent_ops=False, + )(*inputs) + if not isinstance(fake_mode, nullcontext) and fake_mode.shape_env is not None: # type: ignore[attr-defined] + insert_deferred_runtime_asserts( + gm, + fake_mode.shape_env, # type: ignore[attr-defined] + "hoo_maybe_fake_tracing", + export=True, # type: ignore[attr-defined] + ) + return gm + + +def potential_input_alias_or_mutation(gm, inputs, pre_dispatch=False): + try: + gm = _maybe_fake_tracing(gm, inputs, pre_dispatch) + except UnsupportedAliasMutationException: + # this can happen when nested cond_op is + # functionalized + return True + except Exception as e: + raise e + + example_inputs = [ + ph.meta.get("val", None) for ph in gm.graph.find_nodes(op="placeholder") + ] + ( + inp_inp_alias_map, + inp_out_alias_map, + out_out_alias_map, + inp_mutation, + ) = check_input_alias_and_mutation(gm, example_inputs) + return (inp_inp_alias_map, inp_out_alias_map, out_out_alias_map), inp_mutation + + +def analyze_potential_input_alias_or_mutation(name, aliases, input_mutations): + if any(len(a) > 0 for a in aliases): + # TODO: Investigate here further which node is exactly aliasing + raise RuntimeError( + f"{name} where aliases appear. " + + f"In particular, these inputs \ + {set(el for el_map in aliases if len(el_map.keys()) > 0 for el in el_map)} " # noqa: C401 + + "get aliased. Please ensure that this doesn't happen." + ) + if len(input_mutations): + # TODO: Investigate here further which node is exactly mutating the inputs + raise RuntimeError( + f"{name} where the inputs are mutated. " + + f"In particular, these nodes are mutating the inputs \ + {set(el for el in input_mutations)}." # noqa: C401 + + "Please ensure that this doesn't happen." + ) + + +def _has_potential_branch_input_mutation(gm, inputs, pre_dispatch=False): + ( + (_, _, _), + inp_mutation, + ) = potential_input_alias_or_mutation(gm, inputs, pre_dispatch) + + return len(inp_mutation) > 0 + + +def has_potential_input_alias_or_mutation(gm, inputs, pre_dispatch=False): + ( + ( + inp_inp_alias_map, + inp_out_alias_map, + out_out_alias_map, + ), + inp_mutation, + ) = potential_input_alias_or_mutation(gm, inputs, pre_dispatch) + return ( + any( + ( + len(inp_inp_alias_map) > 0, + len(inp_out_alias_map) > 0, + len(out_out_alias_map) > 0, + ) + ), + len(inp_mutation) > 0, + ) + + +def _collect_fake_inputs(inputs): + from torch._subclasses.fake_tensor import FakeTensor + + # Get the example values of the inputs. + inputs_fake: list[FakeTensor | torch.Tensor | int] = [] + for inp in inputs: + if isinstance(inp, (torch.fx.proxy.Proxy, torch.fx.node.Node)): + inp = inp.node if isinstance(inp, torch.fx.proxy.Proxy) else inp + if hasattr(inp, "meta"): + val = inp.meta["example_value"] + if isinstance(val, torch.Tensor): + if torch._C._functorch.is_batchedtensor( + val + ) or torch._C._functorch.is_functionaltensor(val): + # This case is for batched or functional tensors + # Unwrap the tensors + while torch._C._functorch.is_batchedtensor( + val + ) or torch._C._functorch.is_functionaltensor(val): + val = torch._C._functorch.get_unwrapped(val) + if not isinstance(val, FakeTensor): + raise AssertionError( + f"Expected FakeTensor after unwrapping, got {type(val)}" + ) + inputs_fake.append(val) + else: + # This is the standard case of a TensorVariable + if not isinstance(val, FakeTensor): + raise AssertionError( + f"Expected FakeTensor, got {type(val)}" + ) + inputs_fake.append(val) + else: + # This case is for SymInts and other non-Tensor elements + if isinstance(val, torch.Tensor): + raise AssertionError(f"Expected non-Tensor, got {type(val)}") + inputs_fake.append(val) + else: + # This case is for ints + if not isinstance(inp, int): + raise AssertionError(f"Expected int, got {type(inp)}") + inputs_fake.append(inp) + + return inputs_fake + + +def _check_alias_and_mutation(graph_module, inputs_fake, name, pre_dispatch): + aliases, inp_mutation = has_potential_input_alias_or_mutation( + graph_module, inputs_fake, pre_dispatch=pre_dispatch + ) + if aliases: + raise RuntimeError(f"{name} might be aliasing the input or the output!") # noqa: F541 + if inp_mutation: + raise RuntimeError(f"{name} might be modifying the input!") # noqa: F541 + + +def unique_graph_id(proxy_mode, prefix): + """Returns a unique name and id for a graph to be added to a proxy_mode tracer""" + # There are probably better ways - I know that create_arg has some self incrementing name + # magic to it, but since we explicitly have to get the name for register_module, + # I was not sure how to do that. This kinda simulates it. + return unique_graph_name_with_root(proxy_mode.tracer.root, prefix) + + +def unique_graph_name_with_root( + root: torch.fx.GraphModule, prefix: str +) -> tuple[int, str]: + next_name = None + i = 0 + # pyrefly: ignore [bad-assignment] + while not next_name: + candidate = f"{prefix}_{i}" + if hasattr(root, candidate): + i += 1 + else: + next_name = candidate + return i, next_name + + +def _from_fun(t): + from torch._functorch.aot_autograd import from_fun + + if isinstance(t, torch.Tensor): + if t.dtype != torch.bool: + return torch.empty_strided( + t.size(), + t.stride(), + dtype=t.dtype, + requires_grad=t.requires_grad, + device=t.device, + ) + else: + # clone of a functional tensor produces a functional tensor + # but we want to avoid it so we clone a non-functional version + maybe_unfunc_t = t + if isinstance(t, FunctionalTensor): + torch._sync(t) + maybe_unfunc_t = from_fun(t) + elif torch._is_functional_tensor(t): + # need to handle both types of functionalization here: + # these are the tensors that came from the user, + # which could be either FunctionalTensorWrapper or FunctionalTensor + torch._sync(t) + maybe_unfunc_t = torch._from_functional_tensor(t) + # pyrefly: ignore[missing-attribute] + return maybe_unfunc_t.clone() + return t + + +def clone_outputs_aliasing_inputs(args): + input_storage = { + StorageWeakRef(arg._typed_storage()) + for arg in args + if isinstance(arg, torch.Tensor) + } + + def maybe_clone(t): + if ( + isinstance(t, torch.Tensor) + and StorageWeakRef(t._typed_storage()) in input_storage + ): + return t.clone() + return t + + return maybe_clone + + +def prepare_fw_with_masks(fn): + def fw_with_masks(*args): + fw_out = fn(*args) + return fw_out, [ + bool(isinstance(ret, torch.Tensor) and ret.requires_grad) for ret in fw_out + ] + + return fw_with_masks + + +def prepare_fw_with_masks_all_requires_grad(fn): + def fw_with_masks(*args): + fw_out = fn(*args) + # Note [force all outputs to be require grad] + # Instead of using the original fn, we set the output of original + # fn to all require grad. This is consistent with the behavior + # of autograd.Function, where if any one of the inputs requires grad + # all output will be require grad. This also makes the downstream + # require_gradness reasoning much easier. + if pytree.tree_any_only(torch.Tensor, lambda t: t.requires_grad, args): + fw_out = pytree.tree_map_only( + torch.Tensor, + lambda x: x.requires_grad_(True) if x.dtype.is_floating_point else x, + fw_out, + ) + + def _query_requires_grad(t: torch.Tensor) -> bool: + if torch._is_functional_tensor(t): + t = torch._from_functional_tensor(t) + return t.requires_grad + + return fw_out, pytree.tree_map_only(torch.Tensor, _query_requires_grad, fw_out) + + return fw_with_masks + + +# This function replaces None gradients with all-zero gradients. +# `None` gradients are problematic for CUDA graphs. Those gradients are +# replaced with an all-zero tensor for better optimization +def unmask_none_gradients(grads, operands): + allowed_types = (torch.Tensor, int, torch.SymInt) + if not all(isinstance(o, allowed_types) for o in operands): + raise AssertionError( + f"operands can only be of {allowed_types} but got {[type(o) for o in operands]}" + ) + + unmasked_grads = [] + for g, o in zip(grads, operands): + if g is not None: + unmasked_grads.append(g) + else: + # In case the operand is an int or a torch.SymInt, return None + # This can happen for lifted_arguments. E.g., the shapes of a dynamic tensor are lifted and passed + # as additional arguments + unmasked_grads.append( + torch.zeros_like(o) if isinstance(o, torch.Tensor) else None + ) + + return unmasked_grads + + +def _maybe_fake_prop_ignore_unbacked(fn, args): + with suspend_functionalization(), disable_functional_mode(): + with disable_proxy_modes_tracing(): + unfunc_args = [_from_fun(arg) for arg in args] + with ExitStack() as ctx_stack: + ctx_stack.enter_context( + torch.utils._python_dispatch._disable_current_modes() + ) + if (fake_mode := detect_fake_mode(unfunc_args)) is not None: + ctx_stack.enter_context(fake_mode) + if fake_mode.shape_env is not None: + ctx_stack.enter_context( + fake_mode.shape_env.ignore_fresh_unbacked_symbols() + ) + return fn(*unfunc_args) + + +def redirect_to_mode(hop: OperatorBase, mode): + """Utility for redispatching HOP to underlying mode + + Args: + hop: The HOP to redispatch + mode: The mode to redispatch to + + Returns: + A decorated function that implements the HOP for the given mode + """ + + @hop.py_impl(mode) + def impl(mode, *args, **kwargs): + return mode.__torch_dispatch__(hop, [], args, kwargs) + + return impl + + +# TODO: The parameter use_output_and_grad_bw is required because some operations +# that utilize this function, such as the while_loop, may require (grad, fwd_outputs) +def create_fw_bw_graph(fn, use_output_and_grad_bw, fw_inputs, fw_outputs): + from torch._functorch.aot_autograd import AOTConfig, create_joint + + # Note:[HOP create fw_bw graph] We create "clean" environments for make_fx by suspending all dispatch keys + # between Autograd and Python key. Currently, we only suspend functionalization but more can be + # added when required. Will encounter two problems if we don't suspend functionalization: + # + # 1. make_fx fails to capture operations on input: the inputs are wrapped as _to_functional_tensor_wrapper, + # but they will be unwrapped before entering ProxyTorchDispatchMode as part of the dispatching. + # However, it's the outside wrapper that tracer creates proxies for. This casuses tracer fail to + # fetch the proxy for the inputs and fail to capture any operations on them. + # + # 2. make_fx fails to capture output: the outputs after ProxyTorchDispatchMode are further + # wrapped as FunctionalTensorWrapper in Functionalize key after return. However, the tracer + # only associates the inner tensor with proxy in ProxyTorchDispatchMode. Therefore, + # when creating the output node, it fails to associate the wrapped tensor with its proxy. + # Instead, it will create _tensor_constant as output. + + dummy_aot_config = AOTConfig( + fw_compiler=None, # type: ignore[arg-type] + bw_compiler=None, # type: ignore[arg-type] + partition_fn=None, # type: ignore[arg-type] + decompositions={}, + num_params_buffers=0, + aot_id=0, + keep_inference_input_mutations=False, + ) + + example_grad = [_from_fun(out) for out in fw_outputs] + num_grads = len(example_grad) + fw_graph = _maybe_reenter_make_fx(fn)(*fw_inputs) + + def joint_fn(*joint_operands_grads): + if use_output_and_grad_bw: + grads = joint_operands_grads[0] + inputs = joint_operands_grads[1][-1:] + else: + grads = joint_operands_grads[:num_grads] + inputs = joint_operands_grads[num_grads:] + + joint = create_joint(prepare_fw_with_masks(fn), aot_config=dummy_aot_config) + _, grads = joint( + list(inputs), + [grad for grad in grads if grad is not None and grad.requires_grad], + ) + + # Unmask None gradients to all-zero gradients + unmasked_grads = unmask_none_gradients(grads, inputs) + + # In order to keep map functional for backward graph, + # we clone outputs that are aliasing inputs + maybe_clone = clone_outputs_aliasing_inputs(joint_operands_grads) + + return pytree.tree_map(maybe_clone, unmasked_grads) + + if use_output_and_grad_bw: + example_xs_out = list(fw_inputs) + list(fw_outputs) + joint_graph = _maybe_reenter_make_fx(joint_fn)( + (list(example_grad), list(example_xs_out)) + ) + else: + example_xs_out = list(fw_inputs) + joint_graph = _maybe_reenter_make_fx(joint_fn)( + *(list(example_grad) + list(example_xs_out)) + ) + + return fw_graph, joint_graph + + +def _unstack_pytree(xs): + flat_xs, inspec = pytree.tree_flatten(xs) + if not all(isinstance(xs, torch.Tensor) for xs in flat_xs): + raise RuntimeError(f"Leaves of xs must be Tensor {flat_xs}") + + if not all(xs.shape[0] == flat_xs[0].shape[0] for xs in flat_xs): + raise RuntimeError( + f"Leaves of xs must have same leading dimension size {[xs.shape for xs in flat_xs]}" + ) + + a = zip(*flat_xs) + + pytrees = [pytree.tree_unflatten(tuple, inspec) for tuple in a] + return pytrees + + +def _stack_pytree(pytrees): + flat_out = [] + out_spec = None + for pt in pytrees: + flat_pt, out_spec = pytree.tree_flatten(pt) + flat_out.append(flat_pt) + if out_spec is None: + raise AssertionError("out_spec cannot be None") + b = zip(*flat_out) + stacked_out = [] + for leaves in b: + if all(isinstance(leaf, torch.Tensor) for leaf in leaves): + stacked_out.append(torch.stack(leaves)) + elif all(leaf is None for leaf in leaves): + # Backward graph can return None output when forward inputs doesn't require grad. + # When we eagerly execute backward graph, we need to call _stack_pytree on its output, + # therefore we need to deal with None output. + stacked_out.append(None) # type: ignore[arg-type] + else: + raise RuntimeError(f"Cannot stack {leaves}.") + return pytree.tree_unflatten(stacked_out, out_spec) + + +# We cannot call save_for_backward for symints. This helper function +# can be used to save symints as direct attributes of ctx in autograd.Function. +# +# For example, if args = (x, y, s0, z, s1), +# save_values_for_backward will partition the args into two lists, and a bookkeeping list pos: +# partitioned_args[0] = (x, y, z) +# partitioned_args[1] = (s0, s1) +# pos = (0, 0, 1, 0, 1) +# pos list keeps track of which partition the args +# is partitioned into in order to recover it in saved_values. +# +# In saved_values, we can recover the original args by: +# iterating over the pos list and pop one item from the front of partitioned_args[pos[i]]. +# We use t_idx and s_idx to keep track of the next index of the item we are going to pop for the two lists. +def save_values_for_backward(ctx, args): + if not all( + isinstance(arg, (torch.Tensor, torch.SymInt, int, type(None), FakeScriptObject)) + or is_opaque_type(type(arg)) + for arg in args + ): + raise AssertionError(f"Invalid arg types in {args}") + partitioned_args: list[Any] = [[], []] + pos = [] + for arg in args: + idx = 0 if isinstance(arg, torch.Tensor) else 1 + partitioned_args[idx].append(arg) + pos.append(idx) + + if hasattr(ctx, "non_tensor_args"): + raise AssertionError("ctx already has non_tensor_args attribute.") + if hasattr(ctx, "pos"): + raise AssertionError("ctx already has pos attribute.") + ctx.save_for_backward(*partitioned_args[0]) + ctx.non_tensor_args = partitioned_args[1] + ctx.pos = pos + + +def saved_values(ctx): + args = [] + t_idx = 0 + s_idx = 0 + saved_tensors = ctx.saved_tensors + for p in ctx.pos: + if p == 0: + args.append(saved_tensors[t_idx]) + t_idx += 1 + else: + args.append(ctx.non_tensor_args[s_idx]) + s_idx += 1 + if t_idx + s_idx != len(ctx.pos): + raise AssertionError( + f"t_idx ({t_idx}) + s_idx ({s_idx}) != len(ctx.pos) ({len(ctx.pos)})" + ) + return tuple(args) + + +def split_into_chunks(iterable: Sequence[Any], chunk_sizes: list[int]) -> list[Any]: + if sum(chunk_sizes) != len(iterable): + raise AssertionError( + f"the sum of all chunks ({sum(chunk_sizes)}) needs to match the length of the iterable ({len(iterable)})." + ) + elements = [] + idx = 0 + for size in chunk_sizes: + elements.append(iterable[idx : idx + size]) + idx += size + return elements + + +def _clone_aliasing_output(inputs: Sequence[Any], outputs: Sequence[Any]): + # For tensors whose grad is None, create zero tensors as gradients + # This invariant is useful for cudagraph. + + # Elimitate input-output, output-output aliasing + seen_input_storages = { + StorageWeakRef(t._typed_storage()) + for t in inputs + if isinstance(t, torch.Tensor) + } + seen_output_storages = set() + final_outputs = [] + for out in outputs: + if isinstance(out, torch.Tensor): + out_storage = StorageWeakRef(out._typed_storage()) + if ( + out_storage in seen_input_storages + or out_storage in seen_output_storages + ): + out = out.clone() + seen_output_storages.add(StorageWeakRef(out._typed_storage())) + final_outputs.append(out) + return final_outputs + + +def create_bw_fn( + fn: Callable, args: tuple[Any, ...], return_fw_outputs: bool = False +) -> Callable: + """ + For a fn that accepts flat inputs and returns flat outputs: + fw_out = fn(*args), + this function returns: + grad_args = bw_fn(*args_and_grad_output) + with the following invariants: + 1. args + fw_out has an 1-1 correspondence to args_and_grad_output + 2. grad_args has an 1-1 corresponsence to args + 3. for tensor arg whose requires_grad is False, its corresponding grad in + grad_args will be a zero tensor with the same shape. + """ + + from torch._functorch.aot_autograd import AOTConfig, create_joint + + # pyrefly: ignore [missing-module-attribute] + from torch._higher_order_ops.utils import prepare_fw_with_masks_all_requires_grad + + dummy_aot_config = AOTConfig( + fw_compiler=None, # type: ignore[arg-type] + bw_compiler=None, # type: ignore[arg-type] + partition_fn=None, # type: ignore[arg-type] + decompositions={}, + num_params_buffers=0, + aot_id=0, + keep_inference_input_mutations=False, + ) + n_primals = len(args) + + bw_fn = create_joint( + prepare_fw_with_masks_all_requires_grad(fn), aot_config=dummy_aot_config + ) + + def flat_fn(*args_and_grad_outs): + primals = args_and_grad_outs[:n_primals] + tangents = args_and_grad_outs[n_primals:] + fw_outs, grad_args = bw_fn(primals, tangents) + if len(args) != len(grad_args): + raise AssertionError( + f"Expected {len(args)} grad_args, got {len(grad_args)}" + ) + + # For tensors whose grad is None, create zero tensors as gradients + # This invariant is useful for cudagraph. + grad_args = [ + torch.zeros_like(arg) + if isinstance(arg, torch.Tensor) and grad is None + else grad + for grad, arg in zip(grad_args, primals) + ] + + final_grads = _clone_aliasing_output(args_and_grad_outs, grad_args) + if return_fw_outputs: + return *fw_outs, *final_grads + return final_grads + + return flat_fn + + +def get_dummy_aot_autograd_config(): + from torch._functorch.aot_autograd import AOTConfig + + return AOTConfig( + fw_compiler=None, # type: ignore[arg-type] + bw_compiler=None, # type: ignore[arg-type] + partition_fn=None, # type: ignore[arg-type] + decompositions={}, + num_params_buffers=0, + aot_id=0, + keep_inference_input_mutations=False, + ) + + +# Slices off the first element of a given dimension +def first_slice_copy(t: torch.Tensor, dim: int = 0) -> torch.Tensor: + return torch.select_copy(t, dim, 0) + + +# Returns a mask whether a list element is a tensor or not +def get_tensor_mask(tensor_list: Iterable[Any]) -> list[bool]: + return [bool(isinstance(v, torch.Tensor)) for v in tensor_list] + + +def mask_list( + mask: list[bool], inp: list[Any], other: list[Any] | None = None +) -> list[Any]: + # Masks elements on an `inp` list. + # If other is None, then the elements of the `inp` list where the mask is False are removed + # If other is not None, then the elements of the `inp` list where the mask is False are + # replaced with the elements of the `other` list + if len(mask) != len(inp): + raise AssertionError( + f"The length of the mask ({len(mask)}) needs to be identical to the length of the input ({len(inp)})" + ) + if other is not None: + if len(inp) != len(other): + raise AssertionError( + f"If an input and an other list is provided, they need to have the same length ({len(inp)} != {len(other)})" + ) + return [i if m else o for m, i, o in zip(mask, inp, other)] + else: + return [i for m, i in zip(mask, inp) if m] + + +def first_slice_copy_with_grad(li: Iterable[Any]) -> list[Any]: + # First_slice_copy does not keep the original requires_grad flag, + # but we need it for materialize_as_graph + # in order to compute the correct gradients + # The reason why first_slice_copy doesn't keep requires_grad flag is + # because it's called in torch.autograd.Function.backward/forward. + slc = [first_slice_copy(x).requires_grad_(x.requires_grad) for x in li] + return slc + + +# Reports the difference between meta of two tensors in a string +def diff_tensor_meta( + meta1: TensorMetadata, meta2: TensorMetadata, check_grad=True +) -> list[str]: + from torch.fx.experimental.symbolic_shapes import GuardOnDataDependentSymNode + + pair_diffs = [] + for meta_name in TensorMetadata._fields: + if not check_grad and meta_name == "requires_grad": + continue + val1 = getattr(meta1, meta_name) + val2 = getattr(meta2, meta_name) + try: + if val1 != val2: + pair_diffs.append(f"'{meta_name}: {val1} vs {val2}'") + except GuardOnDataDependentSymNode: + pair_diffs.append(f"'{meta_name}: {val1} vs {val2}'") + continue + return pair_diffs + + +# Note [lifted arg types in hop] +# For dynamoed hops, we automatically lift the free symbols in tensors as arguments. +# This has implications for the types of lifted args for different dispatch keys: +# 1. functionalization, FakeTensorMode, ProxyTorchDispatchMode, Autograd need to support torch.Symint +# lifted args because it's on the path of torch.compile(dynamic=True). +# 2. functionalization, FakeTensorMode, ProxyTorchDispatchMode, Autograd, CompositeExplicitAutograd need +# to support int arguments. In the eager run case, we re-trace the subgraph in AutogradKey, so inner +# hops may receive int inputs from the shape of outer tensor inputs. +# However, CompositeExplicitAutograd won't receive SymInt inputs because it only accepts real tensor inputs. +def validate_subgraph_args_types(lifted_args: tuple[Any, ...] | list[Any]): + allowed_types = (torch.Tensor, int, torch.SymInt) + if not all( + isinstance(arg, (torch.Tensor, int, torch.SymInt)) for arg in lifted_args + ): + raise AssertionError( + f"{lifted_args} can only be of {allowed_types} but got {tuple(type(arg) for arg in lifted_args)}" + ) + + +# TODO: Return a more detailed information as to which node +# causes a mutation or an alias. This may requires a per operator tensor version checking +def check_input_alias_and_mutation( + gm: torch.fx.GraphModule, + fake_args: list[FakeTensor], +) -> tuple[dict[int, int], dict[int, int], dict[int, int], list[int]]: + ( + inp_inp_alias_map, + inp_out_alias_map, + out_out_alias_map, + mutated_inputs, + ) = check_input_alias_and_mutation_return_outputs(gm)[:-1] + # pyrefly: ignore [bad-return] + return inp_inp_alias_map, inp_out_alias_map, out_out_alias_map, mutated_inputs + + +def _tensor_storage(t) -> StorageWeakRef: + return StorageWeakRef(t._typed_storage()) + + +def check_input_alias_and_mutation_return_outputs( + gm: torch.fx.GraphModule, +) -> tuple[ + dict[int, int], + dict[int, int], + dict[int, int], + list[int], + tuple[Any, ...] | list[Any], +]: + def _get_example_value(n): + if not isinstance(n, torch.fx.Node): + return n + if "val" in n.meta: + return n.meta["val"] + if "example_value" in n.meta: + return n.meta["example_value"] + return None + + fake_args = [ + _get_example_value(n) + for n in gm.graph.find_nodes(op="placeholder") + if isinstance(n, torch.fx.Node) and "val" in n.meta + ] + outputs = [ + _get_example_value(n) + for n in pytree.tree_flatten(gm.graph.find_nodes(op="output")[0].args[0])[0] + ] + + # We need to analyze the original fake_args to detect + # inp-inp alias. + inp_storage_map = { + _tensor_storage(inp): i + for i, inp in enumerate(fake_args) + if isinstance(inp, torch.Tensor) + } + out_storage_map = { + _tensor_storage(out): i + for i, out in enumerate(outputs) + if isinstance(out, torch.Tensor) + } + inp_inp_alias_map = { + i: inp_storage_map[_tensor_storage(inp)] + for i, inp in enumerate(fake_args) + if isinstance(inp, torch.Tensor) and inp_storage_map[_tensor_storage(inp)] != i + } + out_out_alias_map = { + i: out_storage_map[_tensor_storage(out)] + for i, out in enumerate(outputs) + if isinstance(out, torch.Tensor) and out_storage_map[_tensor_storage(out)] != i + } + inp_out_alias_map = { + i: out_storage_map[_tensor_storage(inp)] + for i, inp in enumerate(fake_args) + if isinstance(inp, torch.Tensor) and _tensor_storage(inp) in out_storage_map + } + mutated_inputs = [] + for node in gm.graph.nodes: + if node.op == "call_function" and isinstance( + node.target, torch._ops.OpOverload + ): + for arg_node, arg_schema in zip(node.args, node.target._schema.arguments): + if arg_schema.is_write: + arg_val = _get_example_value(arg_node) + if not isinstance(arg_val, torch.Tensor): + raise AssertionError( + f"Expected arg_val to be a Tensor, got {type(arg_val)}" + ) + if _tensor_storage(arg_val) in inp_storage_map: + mutated_inputs.append(inp_storage_map[_tensor_storage(arg_val)]) + + return ( + inp_inp_alias_map, + inp_out_alias_map, + out_out_alias_map, + mutated_inputs, + outputs, + ) + + +registered_hop_fake_fns: dict[torch._ops.OpOverload, Callable] = {} + + +F = TypeVar("F", bound=Callable) + + +@overload +def register_fake(hop, fn: None = None) -> Callable[[F], F]: ... + + +@overload +def register_fake(hop, fn: F) -> F: ... + + +def register_fake(hop, fn=None): + """ + Register a fake function for a HOP. This is conceptually equivalent of the + register_fake utility for the custom ops. The registered function is called + inside the fake_tensor _dispatch_impl. + """ + if hop in registered_hop_fake_fns: + raise AssertionError(f"hop {hop} already registered in registered_hop_fake_fns") + + def register(func: F) -> F: + from torch._subclasses.fake_tensor import FakeTensorMode + + redirect_to_mode(hop, FakeTensorMode) + + registered_hop_fake_fns[hop] = func + return func + + if fn is None: + return register + return register(fn) + + +class FunctionalizeCtxWrapper: + """ + This is a dummy wrapper to facilitate fake tensor caching. + + For AOT Dispatcher metadata collection pass, HOPs go from functionalization + key to fake tensor key. The functionalization key wraps the subgraphs in a + function, which changes from call to call even though the subgraph might + still be same. + + To enable fake tensor caching, we just wrap the ctx and subgraph in this + class and then use the subgraph as the hash. + """ + + # Prevents PYTORCH_TEST_WITH_DYNAMO=1 test failures + @torch._disable_dynamo + def __init__(self, ctx, subgraph): + self.ctx = ctx + self.subgraph = subgraph + # Propagate so callers pass inputs as a list, enabling input deallocation. + self._boxed_call = getattr(subgraph, "_boxed_call", False) + + def __hash__(self): + return id(self.subgraph) + + def __repr__(self): + return f"FunctionalizeCtxWrapper on subgraph {self.subgraph})" + + def __call__(self, *args, **kwargs): + if isinstance(self.subgraph, torch.fx.GraphModule): + if self._boxed_call: + # Not all callers respect _boxed_call (e.g. reenter_make_fx). + if len(args) == 1 and isinstance(args[0], list): + return self.ctx.functionalize(self.subgraph)(args[0]) + return self.ctx.functionalize(self.subgraph)(list(args)) + else: + # Running graph with interpreter is needed for propagating the stack_trace + with fx_traceback.preserve_node_meta(): + return self.ctx.functionalize( + torch.fx.Interpreter(self.subgraph).run + )(*args, **kwargs) + functionalized = self.ctx.functionalize(self.subgraph) + if self._boxed_call: + if len(args) == 1 and isinstance(args[0], list): + return functionalized(args[0]) + return functionalized(list(args)) + return functionalized(*args, **kwargs) + + +# A wrapper over HigherOrderOperator that also carries its schema +class HopInstance: + def __init__(self, op: HigherOrderOperator, schema: HopSchema): + if not isinstance(op, HigherOrderOperator): + raise AssertionError(f"Expected HigherOrderOperator, got {type(op)}") + self._op = op + # Using "_" to be consistent with how we access _schema of OpOverload + self._schema = schema + + def __call__(self, *args, **kwargs): + return self._op(*args, **kwargs) + + @staticmethod + def create(hop: HigherOrderOperator, *args, **kwargs): + return HopInstance(hop, hop.gen_schema(*args, **kwargs)) + + +# This call_op can be used to call a HopInstance with +# flat args and kwargs. We need to make use of the hop's schema's tree_spec +# to unflatten the args and kwargs before calling the hop. +def call_op(op: OpOverload | HopInstance, args, kwargs): + if isinstance(op, OpOverload): + return op(*args, **kwargs) + + if not isinstance(op, HopInstance): + raise AssertionError(f"Expected HopInstance, got {type(op)}") + schema = op._schema + bound_args = list(args) + bound_kwargs = {} + for arg in schema.arguments[len(bound_args) :]: + if arg.name not in kwargs: + raise AssertionError(f"arg {arg.name} not in kwargs: {kwargs}") + val = kwargs[arg.name] + if not arg.kwarg_only: + bound_args.append(val) + else: + bound_kwargs[arg.name] = val + + if schema.tree_spec is not None: + if len(bound_args) != len(schema.arguments) or len(bound_kwargs) != 0: + raise AssertionError( + f"Expected {len(schema.arguments)} bound_args and 0 bound_kwargs, " + f"got {len(bound_args)} and {len(bound_kwargs)}" + ) + args, kwargs = pytree.tree_unflatten(bound_args, schema.tree_spec) + return op(*args, **kwargs) + else: + if len(bound_args) + len(bound_kwargs) != len(schema.arguments): + raise AssertionError( + f"Expected {len(schema.arguments)} total args, " + f"got {len(bound_args)} + {len(bound_kwargs)}" + ) + return op(*bound_args, **bound_kwargs) + + +def materialize_as_graph( + fn: Callable, + args: tuple[Any, ...], + include_key_set: torch._C.DispatchKeySet | None = None, + exclude_key_set: torch._C.DispatchKeySet | None = None, + force_enable_grad=False, + subgraph_decomp_table: Mapping[OpOverload, Callable] | None = None, +) -> torch.fx.GraphModule: + if include_key_set is None: + include_key_set = torch._C._dispatch_tls_local_include_set() + if exclude_key_set is None: + exclude_key_set = torch._C._dispatch_tls_local_exclude_set() + + @torch._dynamo.disable(recursive=True, reason=None) + def _materialize_as_graph_inner(): + from torch._guards import active_fake_mode + from torch.fx.experimental.proxy_tensor import _CURRENT_MAKE_FX_TRACER + + with suspend_functionalization(), disable_functional_mode(): + fake_mode = None + if _CURRENT_MAKE_FX_TRACER is not None: + fake_mode = _CURRENT_MAKE_FX_TRACER.fake_tensor_mode + if fake_mode is None: + fake_mode = active_fake_mode() + + with contextlib.ExitStack() as stack: + stack.enter_context( + torch.utils._python_dispatch._disable_current_modes() + ) + if fake_mode is not None: + stack.enter_context(fake_mode) + + with disable_proxy_modes_tracing(): + unfunc_t = [_from_fun(arg) for arg in args] + + stack.enter_context( + torch._C._ForceDispatchKeyGuard(include_key_set, exclude_key_set), + ) + if force_enable_grad: + stack.enter_context(torch.enable_grad()) + return _maybe_reenter_make_fx( + fn, subgraph_decomp_table=subgraph_decomp_table + )(*unfunc_t) + + gm = _materialize_as_graph_inner() + if gm is None: + raise AssertionError("materialize_as_graph returned None") + return gm + + +def materialize_callable_in_args(op: HopInstance, args, kwargs): + schema = op._schema + hop = op._op + flat_args, flat_spec = pytree.tree_flatten((args, kwargs)) + + def wrapped_fn(*flat_args): + return call_op(op, args, kwargs) + + # We need to trace the higher order op in order to materilaize the callable inputs that + # are a callable (e.g. after functionalization key) + gm = reenter_make_fx(wrapped_fn)(*flat_args) + hop_node = gm.graph.find_nodes(op="call_function", target=hop)[0] + arg_proxies = pytree.tree_leaves((hop_node.args, hop_node.kwargs)) + if not isinstance(schema, torch._C.FunctionSchema) or len(arg_proxies) != len( + schema.arguments + ): + raise AssertionError( + f"Expected FunctionSchema with {len(arg_proxies)} arguments" + ) + + # call_op preserves ordering of proxies via schema + materialized_args = [] + for i, proxy in enumerate(arg_proxies): + if ( + isinstance(proxy, torch.fx.Node) + and proxy.op == "get_attr" + and isinstance(getattr(gm, proxy.target), torch.fx.GraphModule) # type: ignore[arg-type] + ): + if not callable(flat_args[i]): + raise AssertionError( + f"Expected flat_args[{i}] to be callable for {schema}" + ) + materialized_args.append(getattr(gm, proxy.target)) # type: ignore[arg-type] + else: + materialized_args.append(flat_args[i]) + + return pytree.tree_unflatten(materialized_args, flat_spec) + + +def has_user_subclass(args, allowed_subclasses): + """Check if any tensor arguments are user subclasses. + + This is used to determine if tensor subclasses should get a chance to run + their own implementation first before falling back to the default implementation. + + Args: + args: Arguments to check (will be flattened with pytree) + allowed_subclasses: Tuple of allowed subclass types + + Returns: + True if user tensor subclasses are found, False otherwise + """ + flat_args, _ = pytree.tree_flatten(args) + + val = any( + isinstance(a, torch.Tensor) + and type(a) is not torch.Tensor + and not isinstance(a, allowed_subclasses) + for a in flat_args + ) + return val + + +def _has_gen_schema(op: HigherOrderOperator): + # There is an InvokeQuant argument we cannot gen_schema. + if op is torch.ops.higher_order.invoke_quant_packed: + return False + method = "gen_schema" + return hasattr(type(op), method) and getattr(type(op), method) is not getattr( + HigherOrderOperator, method + ) + + +def filter_with_masks(data: list[torch.Tensor | None], masks: list[bool]): + if len(data) != len(masks): + raise AssertionError( + f"data length ({len(data)}) != masks length ({len(masks)})" + ) + return [item for item, keep in zip(data, masks) if keep] + + +def fill_none_with_masks(data: list[torch.Tensor | None], masks: list[bool]): + data_iter = iter(data) + return [next(data_iter) if kept else None for kept in masks] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/while_loop.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/while_loop.py new file mode 100644 index 0000000000000000000000000000000000000000..5484c7ab6e48621d3544c83bff99acfd2b9dcfaa --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/while_loop.py @@ -0,0 +1,953 @@ +# mypy: allow-untyped-defs +import contextlib +import functools +from collections.abc import Callable + +import torch +import torch.utils._pytree as pytree +from torch._C import DispatchKey +from torch._higher_order_ops.utils import ( + _maybe_run_with_interpreter, + autograd_not_implemented, + check_input_alias_and_mutation_return_outputs, + check_meta_consistency, + fill_none_with_masks, + filter_with_masks, + materialize_as_graph, + reenter_make_fx, + validate_subgraph_args_types, +) +from torch._ops import HigherOrderOperator +from torch._subclasses.fake_tensor import FakeTensorMode +from torch.fx.experimental.proxy_tensor import ( + disable_proxy_modes_tracing, + ProxyTorchDispatchMode, + track_tensor_tree, +) + + +class WhileLoopOp(HigherOrderOperator): + def __init__(self) -> None: + super().__init__("while_loop") + + def __call__( + self, + cond_fn: Callable, + body_fn: Callable, + carried_inputs: tuple[torch.Tensor | int | float | bool], + additional_inputs: tuple[torch.Tensor | torch.SymInt | int, ...], + /, + ): + if not isinstance(carried_inputs, (tuple, list)): + raise RuntimeError( + f"carried_inputs must be a tuple or list, got {type(carried_inputs)}" + ) + if not isinstance(additional_inputs, (tuple, list)): + raise RuntimeError( + f"additional_inputs must be a tuple or list, got {type(additional_inputs)}" + ) + + validate_subgraph_args_types(carried_inputs) + validate_subgraph_args_types(additional_inputs) + # pyrefly: ignore [missing-attribute] + return super().__call__(cond_fn, body_fn, carried_inputs, additional_inputs) + + # pyrefly: ignore [bad-override] + def gen_schema(self, cond_fn, body_fn, carried_inputs, additional_inputs): + from torch._higher_order_ops.schema import HopSchemaGenerator + from torch._higher_order_ops.utils import materialize_as_graph + + all_inputs = carried_inputs + additional_inputs + + cond_gm: torch.fx.GraphModule = ( + cond_fn + if isinstance(cond_fn, torch.fx.GraphModule) + else materialize_as_graph(cond_fn, all_inputs) + ) + body_gm: torch.fx.GraphModule = ( + body_fn + if isinstance(body_fn, torch.fx.GraphModule) + else materialize_as_graph(body_fn, all_inputs) + ) + + def _find_example_value(n, real_inp): + if "val" in n.meta: + return n.meta["val"] + elif "example_value" in n.meta: + return n.meta["example_value"] + else: + if isinstance(real_inp, torch.Tensor): + raise AssertionError( + "expected non-Tensor real_inp when no val/example_value in meta, got Tensor" + ) + return real_inp + + ( + _, + _, + _, + body_mutated_inputs, + body_outputs, + ) = check_input_alias_and_mutation_return_outputs(body_gm) + + ( + _, + _, + _, + cond_mutated_inputs, + _, + ) = check_input_alias_and_mutation_return_outputs(cond_gm) + + mutated_inputs = set(body_mutated_inputs) | set(cond_mutated_inputs) + + schema_gen = HopSchemaGenerator(self) + schema_gen.add_arg("cond_fn", cond_gm) + schema_gen.add_arg("body_fn", body_gm) + + for idx, arg in enumerate(carried_inputs): + schema_gen.add_arg( + f"carried_input{idx}", arg, is_mutated=idx in mutated_inputs + ) + + for idx, arg in enumerate(additional_inputs): + additional_idx = len(carried_inputs) + idx + schema_gen.add_arg( + f"additional_input{idx}", + arg, + is_mutated=additional_idx in mutated_inputs, + ) + + for out in body_outputs: + schema_gen.add_output(out) + + schema_gen.add_schema_tree_spec( + cond_fn, body_fn, carried_inputs, additional_inputs + ) + return schema_gen.gen_schema() + + +while_loop_op = WhileLoopOp() + + +def while_loop(cond_fn, body_fn, carried_inputs): + r""" + Run ``body_fn(*carried_inputs)`` while ``cond_fn(*carried_inputs)`` returns + a True scalar tensor. Returns the output of body_fn or initial + carried_inputs. + + .. warning:: + + `torch.while_loop` is a prototype feature in PyTorch. It has limited support for input and output types and + doesn't support training currently. Please look forward to a more stable implementation in a future version of PyTorch. + Read more about feature classification at: https://pytorch.org/blog/pytorch-feature-classification-changes/#prototype + + `while_loop` is a structured control flow operator. It preserves the loop semantic across the torch.compile and torch.export. + + `while_loop` is equivalent to the following:: + + def while_loop(cond_fn, body_fn, carried_inputs): + val = carried_inputs + while cond_fn(*val): + val = body_fn(*val) + return val + + Args: + cond_fn (Callable): A callable function that returns a boolean Scalar tensor or a python boolean. + + body_fn (Callable): A callable function that takes the same inputs as `cond_fn` and returns a tuple of tensors or ints + + carried_inputs (Tuple of possibly nested dict/list/tuple of tensors or ints): A tuple of inputs to cond_fn and body_fn. + It's also the initial value of states that are carried across iterations. Note that when pass an integer as carry, + the corresponding return of while_loop will be another int with unknown values because we don't know how many + iterations while_loop will run. + + Example 1:: + + def cond_fn(iter, x): + return iter.sum() < 10 + + + def body_fn(iter, x): + return iter + 1, x.sin() + + + while_loop(cond_fn, body_fn, (torch.zeros(1), torch.randn(3, 4))) + + Example 2:: + + def cond_fn(int_iter, x): + return 2 * int_iter < x.shape[0] + + + def body_fn(int_iter, x): + return int_iter + 1, x + int_iter + + + while_loop(cond_fn, body_fn, (0, torch.randn(3, 4))) + + Restrictions: + + - body_fn must return tensors or int with the same metadata (e.g.shape, dtype) as inputs. + + - body_fn and cond_fn must not in-place mutate the carried_inputs. A clone before the mutation is required. + + - body_fn and cond_fn must not mutate python variables (e.g. list/dict) created outside of the body_fn. + + - body_fn and cond_fn's output cannot alias any of the inputs. A clone is required. + + .. warning:: + + Temporal Limitations: + + - 'while_loop' only supports **inference** right now. Autograd will be supported in the future. + + """ + + # Currently, additional_inputs is not a user-facing input. It will be automatically set in dynamo. + # parameters and buffers accessed in cond_fn or body_fn or tensor closures will become additional_inputs. + additional_inputs: tuple = () + + # The reason we flatten the output before calling into dynamo is that + # we want to create a consistent input ordering for cond_fn and body_fn. + # and we also want to the input ordering matches the output ordering. + # Also see NOTE: [why we cannot use "automatic" for while_loop] + # Construct flat cond_fn and flat_body_fn, which takes flattened inputs + flat_inputs, in_spec = pytree.tree_flatten((carried_inputs, additional_inputs)) + + def flat_cond_fn(*flat_args): + carried, additional = pytree.tree_unflatten(flat_args, in_spec) + return cond_fn(*carried, *additional) + + def flat_body_fn(*flat_args): + carried, additional = pytree.tree_unflatten(flat_args, in_spec) + return body_fn(*carried, *additional) + + if torch.compiler.is_dynamo_compiling(): + return while_loop_op(flat_cond_fn, flat_body_fn, tuple(flat_inputs), tuple()) + + def _validate_input(cond_fn, body_fn, carried_inputs): + from torch._higher_order_ops.utils import validate_subgraph_args_types + + if not callable(cond_fn) or not callable(body_fn): + raise RuntimeError("Expect cond_fn and body_fn to be callable.") + + validate_subgraph_args_types(flat_inputs) + + if not pytree.tree_all( + lambda t: isinstance(t, (torch.Tensor, torch.SymInt, int)), carried_inputs + ): + raise RuntimeError( + "Expect carried_inputs to be a tuple of possibly nested dict/list/tuple that only" + f"consists of tensor or int leaves, but got {carried_inputs}." + ) + + _validate_input(cond_fn, body_fn, carried_inputs) + + # Dynamo is expecting a callable with "__code__" attribute. + # We cannot directly pass cond_op to it. So we wrap it in a dummy function. + def _while_loop_op_wrapper(*args, **kwargs): + return while_loop_op(*args, **kwargs) + + from torch._higher_order_ops.utils import _hop_compile_and_call + + return _hop_compile_and_call( + _while_loop_op_wrapper, + (flat_cond_fn, flat_body_fn, tuple(flat_inputs), tuple()), + ) + + +@while_loop_op.py_impl(DispatchKey.CompositeExplicitAutograd) +def while_loop_dense( + cond_fn, body_fn, carried_inputs, additional_inputs, stack_output=False +): + carried_vals = carried_inputs + + def _validate_cond_output(pred): + if ( + isinstance(pred, torch.Tensor) + and pred.size() == torch.Size([]) + and pred.dtype == torch.bool + ) or isinstance(pred, bool): + return + else: + raise RuntimeError( + f"cond_fn must return a boolean scalar tensor or a boolean but got {pred}" + ) + + if not isinstance(carried_inputs, (tuple, list)): + raise RuntimeError( + f"carried_inputs must be a tuple or list but got {type(carried_inputs)}" + ) + + # Check condition and set up flag + should_loop = cond_fn(*carried_vals, *additional_inputs) + _validate_cond_output(should_loop) + + if not should_loop: + if stack_output: + return tuple( + val.unsqueeze(0).clone() if isinstance(val, torch.Tensor) else val + for val in carried_vals + ) + else: + return tuple( + val.clone() if isinstance(val, torch.Tensor) else val + for val in carried_vals + ) + + outputs: list[list[torch.Tensor]] = [[] for _ in carried_vals] + + while should_loop: + out = body_fn(*carried_vals, *additional_inputs) + if stack_output: + for i, o in enumerate(out): + outputs[i].append(o) + + if not isinstance(out, tuple): + raise AssertionError(f"body_fn should return a tuple but got {type(out)}") + if len(out) != len(carried_inputs): + raise AssertionError( + f"body_fn should return the same number of elements as carried_inputs, got {len(out)} vs {len(carried_inputs)}" + ) + carried_vals = out + + should_loop = cond_fn(*carried_vals, *additional_inputs) + + if stack_output: + outs: list[torch.Tensor] = [] + for out in outputs: + outs.append(torch.stack(out, dim=0)) + return tuple(outs) + + return carried_vals + + +@while_loop_op.py_autograd_impl +def while_loop_autograd(cond_fn, body_fn, operands, additional_inputs): + return WhileLoopAutogradOp.apply( + cond_fn, + body_fn, + len(operands), + len(additional_inputs), + *operands, + *additional_inputs, + ) + + +def _find_or_create_fake_mode() -> FakeTensorMode: + from torch.fx.experimental.symbolic_shapes import ShapeEnv + + fake_mode = torch._guards.detect_fake_mode() + if fake_mode is None: + fake_mode = FakeTensorMode(shape_env=ShapeEnv()) + + return fake_mode + + +def _create_unbacked_symint( + fake_mode: FakeTensorMode, ignore_fresh_unbacked_symbols: bool +) -> torch.SymInt: + if fake_mode is None or fake_mode.shape_env is None: + raise AssertionError("Must provide a fake_mode with shape_env.") + ctx = ( + contextlib.nullcontext() + if not ignore_fresh_unbacked_symbols + else fake_mode.shape_env.ignore_fresh_unbacked_symbols() + ) + with ctx: + return fake_mode.shape_env.create_unbacked_symint() + + +@while_loop_op.py_impl(ProxyTorchDispatchMode) +def while_loop_tracing( + mode, + cond_fn, + body_fn, + carried_inputs, + additional_inputs, + stack_output=False, +): + op = while_loop_stack_output_op if stack_output else while_loop_op + + def _trace_while_loop( + proxy_mode, op, cond_fn, body_fn, carried_inputs, additional_inputs + ): + # NOTE [unspecialize int carry with unbacked symints] + # When we support int carry, we'll also need to support int output of body_fn because. + # previous iteration's output is next iteration's input and they must match. + # For carries, when we start tracing while_loop, they can be + # - constants e.g. (0, [1, 3]) + # - backed symints (x.shape[0], [x.shape[1] + x.stride[1], x.shape[2]]) + # - unbacked symints e.g. (u0, [u0 + u1, u2]) + # We choose the most conservative design: in all cases, we create new unbacked symints to trace the + # subgraph. It's possible to do some analysis on initial carry and the output of first + # iteration to determine a better range for the output unbacked symbol e.g. when input is an unbacked + # symint >= 0 before the while_loop but in general this is difficult because we don't know + # the number of iterations. Users would have to re-constrain the unbacked symint in subgraph if needed. + # + # For output of fake cond_fn, it could be constant bool or SymBool (e.g. return x.shape[0] < 4, + # where x.shape[0] can be either static of dynamic). In the case of constant bool, we should do a + # specialization (NYI). + + # For output of fake body_fn, it could be all three types though from user's point of view, + # they're all integers e.g. + + # init_carry = (0, s0, u1, t) + # def body_fn(u0, s0, u1, t): + # ... + # return (t.shape[0], t.shape[1], t.shape[2], y + 1) + # + # It may seem that a constant output isn't possible: users shouldn't write a while_loop + # that always return 0. But it could be that a shape is not set as dynamic properly (e.g. + # automatic dynamic hasn't been triggered). + # + # For this reason, we treat int, symint outputs in the same way: + # - they can match against any of int, symint carry + # - we unspecialize them with new unbacked symints in fake while_loop + # Similarly, we could do some analysis to refine the output ranges but it's easier to start with + # fresh unbacked symints. One surprising case can be: an input unbacked symint is constrained by + # users to be >= 0 (either before while_loop or inside body_fn) and it increments by 1 in each + # iteration. Ideally, we should know that the final output is >= 0 but we didn't constrain the + # unbacked symint output of subgraph as of today because this requires a smart range analysis. + fake_mode: FakeTensorMode = _find_or_create_fake_mode() + + def _unspecialize_carried_inputs(x): + if isinstance(x, (int, torch.SymInt)): + return _create_unbacked_symint( + fake_mode, ignore_fresh_unbacked_symbols=True + ) + # Note: [unspecialize constant tensor carry] + # We need to disable constant specialization for tensor inputs that become loop carries. + # Here's the problem: when a user creates a constant tensor e.g. torch.tensor(0), PyTorch calls aten.lift_fresh_copy + # to create a safe copy (avoiding aliasing issues), which creates a FakeTensor with constant=True. + # But when this FakeTensor becomes a loop carry, we have a problem: + # - Operations like .item() will read the constant value and bake it into the traced code + # - This is incorrect because carry variables change between loop iterations + # - The traced code would use the wrong constant value for all iterations + # Solution: We clone the constant tensors and mark the cloned tensor as non-constant so they won't + # be specialized to fixed values during tracing body_fn or cond_fn. + elif isinstance(x, torch.Tensor): + x = x.clone() + if hasattr(x, "constant") and x.constant is not None: + # pyrefly: ignore [missing-attribute] + x.constant = None + return x + + with disable_proxy_modes_tracing(): + unspecialized_carried_inputs = pytree.tree_map_only( + (int, torch.SymInt, torch.Tensor), + # For temporarily created unbacked symints, we don't need to bind them to any proxy + lambda x: _unspecialize_carried_inputs(x), + carried_inputs, + ) + + def produce_graph(fn): + cloned_carried_inputs = pytree.tree_map_only( + torch.Tensor, lambda x: x.clone(), unspecialized_carried_inputs + ) + return reenter_make_fx(fn)(*cloned_carried_inputs, *additional_inputs) + + cond_graph = produce_graph(cond_fn) + body_graph = produce_graph(body_fn) + + next_name = None + i = 0 + # pyrefly: ignore [bad-assignment] + while not next_name: + candidate = f"while_loop_cond_graph_{i}" + if hasattr(proxy_mode.tracer.root, candidate): + i += 1 + else: + next_name = candidate + cond_graph_name = next_name + body_graph_name = f"while_loop_body_graph_{i}" + if hasattr(proxy_mode.tracer.root, body_graph_name): + raise AssertionError( + f"proxy_mode.tracer.root already has attribute {body_graph_name}" + ) + + proxy_mode.tracer.root.register_module(cond_graph_name, cond_graph) + proxy_mode.tracer.root.register_module(body_graph_name, body_graph) + + args = (cond_graph, body_graph, carried_inputs, additional_inputs) + + proxy_args = pytree.tree_map(proxy_mode.tracer.unwrap_proxy, args) + + out_proxy = proxy_mode.tracer.create_proxy( + "call_function", op, proxy_args, {}, name=op._name + ) + + out = op( + cond_graph, body_graph, unspecialized_carried_inputs, additional_inputs + ) + return track_tensor_tree( + out, out_proxy, constant=None, tracer=proxy_mode.tracer + ) + + return _trace_while_loop( + mode, + op, + cond_fn, + body_fn, + carried_inputs, + additional_inputs, + ) + + +@while_loop_op.py_impl(FakeTensorMode) +def while_loop_fake_tensor_mode( + mode, cond_fn, body_fn, carried_inputs, additional_inputs, stack_output=False +): + with mode: + # NOTE: [Handling unback symints in subgraph of while_loop] + # The idea is that the scope of unbacked symints are limited to the subgraph. + # + # We're implementing the fake tensor mode of while_loop operator. + # and we run body_fn once to get an fake output. + # Let's first consider the case that unbacked symints are tensor shapes: + # + # Case 1: + # if the unbacked symints is local to the subgraph e.g. + # def body_fn(it, x): + # nz = x.nonzero() + # return it+1. nz.sum() + # we can just ignore the newly created unbacked symints because it has + # no effect on the output of while_loop and it's tracked when we tracing. + # the subgraph. + # + # Case 2: + # if the unbacked symints are shape of output of while_loop e.g. + # def body_fn(it, x): + # nz = x.nonzero() + # return it+1, nz + # This will fail the shape check because in each iteration, the carried_input's shape + # must match the output shape as nz.shape contains newly allocated unbacked symint, this + # won't match the carried_input's shape. + # + # Case 3: + # if the unbacked symints are shape of carried_inputs e.g. + # nz = a.nonzero() + # body_fn(it, nz): + # return it+1. nz.sin() + 1, + # There's no new unbacked symints allocated in subgraph, so we're safe. + with mode.shape_env.ignore_fresh_unbacked_symbols(): + # body_fn return output with the same pytree and tensor meta data as carried_inputs + # so we could just return the output after one iteration. + body_outs = body_fn(*carried_inputs, *additional_inputs) + check_meta_consistency( + carried_inputs, + body_outs, + "carried_inputs", + "body_output", + include_contiguity=False, + ) + + if stack_output: + n_iter = _create_unbacked_symint(mode, ignore_fresh_unbacked_symbols=False) + if not all(isinstance(x, torch.Tensor) for x in carried_inputs): + raise AssertionError( + f"all carried_inputs must be tensors for stack_output, got {[type(x) for x in carried_inputs]}" + ) + fake_outputs = tuple( + out.clone() + .unsqueeze(0) + .repeat((n_iter,) + tuple(1 for _ in range(out.dim()))) + for out in body_outs + ) + return pytree.tree_map_only( + (int, torch.SymInt), + # For while_loop's unbacked symint output, we want them to be bound + # to the proxy of while_loop's output. + lambda _: _create_unbacked_symint( + mode, ignore_fresh_unbacked_symbols=False + ), + fake_outputs, + ) + + # See NOTE [unspecialize int carry with unbacked symints] + return pytree.tree_map_only( + (int, torch.SymInt), + # For while_loop's unbacked symint output, we want them to be bound + # to the proxy of while_loop's output. + lambda _: _create_unbacked_symint( + mode, ignore_fresh_unbacked_symbols=False + ), + body_outs, + ) + + +@while_loop_op.py_functionalize_impl +def while_loop_func( + ctx, cond_fn, body_fn, carried_inputs, additional_inputs, stack_output=False +): + from torch._higher_order_ops.utils import _check_alias_and_mutation + + op = while_loop_stack_output_op if stack_output else while_loop_op + + unwrapped_carried_inputs = ctx.unwrap_tensors(carried_inputs) + unwrapped_additional_inputs = ctx.unwrap_tensors(additional_inputs) + unwrapped_inputs = unwrapped_carried_inputs + unwrapped_additional_inputs + with ctx.redispatch_to_next(): + functional_cond_fn = ctx.functionalize(_maybe_run_with_interpreter(cond_fn)) + functional_body_fn = ctx.functionalize(_maybe_run_with_interpreter(body_fn)) + pre_dispatch = hasattr(ctx, "mode") and ctx.mode.pre_dispatch + for fn, fn_name in [ + (cond_fn, "cond_fn"), + (body_fn, "body_fn"), + ]: + _check_alias_and_mutation(fn, unwrapped_inputs, fn_name, pre_dispatch) + ret = op( + functional_cond_fn, + functional_body_fn, + unwrapped_carried_inputs, + unwrapped_additional_inputs, + ) + return ctx.wrap_tensors(ret) + + +class WhileLoopStackOutputOp(HigherOrderOperator): + """ + while_loop_stack_output is a variant of while_loop that returns a stack of outputs. + Its semantic can be illurated using python code as: + def while_loop_stack_output(cond_fn, body_fn, carried_inputs, additional_inputs): + outs = [] + while cond_fn(*carried_inputs, *additional_inputs): + out = body_fn(*carried_inputs, *additional_inputs) + outs.append(out) + return torch.stack(outs) + + It's useful for supporting autograd of while_loop. + """ + + def __init__(self) -> None: + super().__init__("while_loop_stack_output") + + def __call__( + self, + cond_fn: Callable, + body_fn: Callable, + carried_inputs: tuple[torch.Tensor | int | float | bool], + additional_inputs: tuple[torch.Tensor | torch.SymInt | int, ...], + /, + ): + if not isinstance(carried_inputs, (tuple, list)): + raise RuntimeError( + f"carried_inputs must be a tuple or list, got {type(carried_inputs)}" + ) + if not isinstance(additional_inputs, (tuple, list)): + raise RuntimeError( + f"additional_inputs must be a tuple or list, got {type(additional_inputs)}" + ) + + validate_subgraph_args_types(carried_inputs) + validate_subgraph_args_types(additional_inputs) + # pyrefly: ignore [missing-attribute] + return super().__call__(cond_fn, body_fn, carried_inputs, additional_inputs) + + +# Note [while_loop autograd] +# Consider wthe following while_loop that can be visualized as: +# additional_inputs +# ┌─────┬─────┼─────┬─────┐ +# | | | | | +# ↓ ↓ ↓ ↓ ↓ +# x ──→ y0 ─→ y1 ─→ y2 ─→ y3 ─→ y4 +# +# The bacwkard can be visualized as follows: +# +# g_additional_inputs +# ┌──────┬──────┼──────┬──────┐ +# | | | | | +# | | | | | +# gx <── gy0 <─ gy1 <─ gy2 <─ gy3 <─ gy4 +# +# We can compute gx using chain rule: +# +# gx = gy0 * bw(y0, x), +# +# where gy0 denotes the gradient of loss with respect to y0, and bw(y0, x) denotes the gradient of y0 with +# respect to x. Note that bw can be computed from forward body_fn easily using torch.autograd.grad. +# We could substitute the unknowns gy0, gy1, ..., with chain rule until gy4: +# +# gx = gy1 * bw(y1, y0) * bw(y0, x) +# = gy2 * bw(y2, y1) * bw(y1, y0) * bw(y0, x) +# = ... +# = gy4 * bw(y4, y3) * bw(y3, y2) * bw(y2, y1) * bw(y1, y0) * bw(y0, x) +# +# since gy4 is the graient of the final output, which is given as the backward input, we've got a formula +# to compute gx. A abbr for the formula is: gy4 * bw43210x +# +# In a similar way, we can compute g_additional_inputs using chain rule: +# +# g_additional_inputs = gy0 * bw(y0, addi) + gy1 * bw(y1, addi) + gy2 * bw(y2, addi) + ... + gy4 * bw(y4, addi) +# +# Notice that gy0 = gy4 * bw43210, gy1 = gy4 * bw4321 etc, we now also get a formula for g_additional_inputs. +# +# Implementation: +# The idea of implementation is to construct a while_loop to calculate both gx and g_additional_inputs. +# Specifically, we can implement the backward of while_loop with as follows: +# +# def cond_fn(idx, grad_carries, grad_additional_inputs, fw_additional_inputs, fw_inps): +# return idx < fw_inps.size(0) +# +# def body_fn(idx, grad_carries, grad_additional_inputs, fw_additional_inputs, fw_inps): +# reversed_idx = fw_inps.size(0) - 1 - idx +# next_grad_carry, next_grad_additional_inputs = bw(fw_inps[reversed_idx], fw_additional_inputs, grad_carries) +# return idx + 1, next_grad_carry, next_grad_additional_inputs + grad_additional_inputs +# +# idx = 0 +# init_grad_carries = grads +# init_grad_additional_inputs = torch.zeros_like(g_additional_inputs) +# fw_inps = torch.cat([ctx.fw_carried_inputs, fw_outputs[:-1]]) +# while_loop(cond_fn, body_fn, (idx, init_grad_carries, init_grad_additional_inputs,), (fw_additional_inputs, fw_inps)) + + +class WhileLoopAutogradOp(torch.autograd.Function): + @staticmethod + # pyrefly: ignore [bad-override] + def forward( + ctx, + cond_fn, + body_fn, + num_carried_inputs, + num_additional_inputs, + *carries_and_inputs, + ): + from torch._higher_order_ops.scan import split_into_chunks + + carries, additional_inputs = split_into_chunks( + carries_and_inputs, [num_carried_inputs, num_additional_inputs] + ) + with torch._C._AutoDispatchBelowAutograd(): + fw_outputs = while_loop_stack_output_op( + cond_fn, body_fn, carries, additional_inputs + ) + + if hasattr(ctx, "fw_cond_fn"): + raise AssertionError("ctx already has fw_cond_fn attribute") + if hasattr(ctx, "fw_body_fn"): + raise AssertionError("ctx already has fw_body_fn attribute") + if hasattr(ctx, "carries"): + raise AssertionError("ctx already has carries attribute") + if hasattr(ctx, "additional_inputs"): + raise AssertionError("ctx already has additional_inputs attribute") + if hasattr(ctx, "fw_outputs"): + raise AssertionError("ctx already has fw_outputs attribute") + ctx.fw_cond_fn = cond_fn + ctx.fw_body_fn = body_fn + ctx.carries = carries + ctx.additional_inputs = additional_inputs + ctx.fw_outputs = fw_outputs + loop_count = None + # pyrefly: ignore [bad-assignment] + for out in fw_outputs: + if isinstance(out, torch.Tensor): + if loop_count is not None: + if out.size(0) != loop_count: + raise AssertionError( + f"inconsistent loop_count: expected {loop_count}, got {out.size(0)}" + ) + else: + loop_count = out.size(0) + if loop_count is None: + raise AssertionError( + "loop_count must not be None after processing fw_outputs" + ) + + # Remove the loop_count from pending_fresh_unbacked_symbols + # because it's not part of forward output and it's impossible + # to bind it to a proxy in forward graph anyways. + if ( + isinstance(loop_count, torch.SymInt) + and (shape_env := loop_count.node.shape_env) + and loop_count in shape_env.pending_fresh_unbacked_symbols + ): + shape_env.pending_fresh_unbacked_symbols.remove(loop_count) + + # Even when body function is not executed, we clone and unsqueeze the input + # to avoid the aliasing, therefore loop_count is always >= 1 + torch._check(loop_count >= 1) + # We snapshot the dispatch keys in forward for materializing the + # the bw_graph in backward. + ctx._fw_include_key_set = torch._C._dispatch_tls_local_include_set() + ctx._fw_exclude_key_set = torch._C._dispatch_tls_local_exclude_set() + if len(fw_outputs) <= 0: + raise AssertionError("fw_outputs shouldn't be empty") + # Only the last of the output fw_outputs need to be returned + return tuple(ckp[-1] for ckp in fw_outputs) + + @staticmethod + def backward(ctx, *grads): + from torch._higher_order_ops.cond import create_bw_fn + from torch._higher_order_ops.scan import split_into_chunks + + # set up single step bw fn + bw_body_fn = create_bw_fn(ctx.fw_body_fn, ctx.carries + ctx.additional_inputs) + # Note [Handle inputs that're not differentiable] + # When a forward input is non-differentiable e.g. a symint or an integer tensor, their gradients + # will be None. However, we don't want to return None in the subgraph because this complicates the + # inductor codegen, where we need to do a non-uniform treatment for None and tensors. + # So we set up masks and filter the None gradients so that only tensors are returned from each step. + carries_tensor_masks = [ + bool(isinstance(t, torch.Tensor) and t.dtype.is_floating_point) + for t in ctx.carries + ] + additional_inputs_tensor_masks = [ + bool(isinstance(t, torch.Tensor) and t.dtype.is_floating_point) + for t in ctx.additional_inputs + ] + + init_idx = torch.zeros((), dtype=torch.int64) + init_grad_carries = filter_with_masks(grads, carries_tensor_masks) # type: ignore[arg-type] + init_grad_additional_inputs = tuple( + torch.zeros_like(t) + for need_keep, t in zip( + additional_inputs_tensor_masks, ctx.additional_inputs + ) + if need_keep + ) + # We need to the forward inputs to each iteration to compute the backward + # which is the concatenation of first iteraiton input i.e. ctx.carries and all iterations's + # output except the last iteration. + fw_carries = [ + torch.cat([carry.unsqueeze(0), carries[:-1]]) + for carry, carries in zip(ctx.carries, ctx.fw_outputs) + ] + for fw_carry, carry in zip(fw_carries, ctx.carries): + fw_carry.requires_grad_(carry.requires_grad) + + _, spec = pytree.tree_flatten( + ( + init_idx, + init_grad_carries, + init_grad_additional_inputs, + ctx.fw_outputs, + ctx.additional_inputs, + ) + ) + + def cond_fn(*flat_args): + ( + idx, + grad_carries, + grad_additional_inputs, + fw_carries, + additional_inputs, + ) = pytree.tree_unflatten(flat_args, spec) + if not isinstance(fw_carries[0], torch.Tensor): + raise AssertionError( + f"expected fw_carries[0] to be torch.Tensor, got {type(fw_carries[0])}" + ) + # excluding the last iteration's output + return idx < fw_carries[0].size(0) + + def body_fn(*flat_args): + ( + idx, + grad_carries, + grad_additional_inputs, + fw_carries, + additional_inputs, + ) = pytree.tree_unflatten(flat_args, spec) + reversed_idx = fw_carries[0].size(0) - idx - 1 + selected_fw_carries = [ + ckp.select(0, reversed_idx.item()) for ckp in fw_carries + ] + cur_grad_carries, cur_grad_additional_inputs = split_into_chunks( + bw_body_fn(*selected_fw_carries, *additional_inputs, *grad_carries), + [len(ctx.carries), len(ctx.additional_inputs)], + ) + if not all(isinstance(t, torch.Tensor) for t in cur_grad_carries): + raise AssertionError( + f"all cur_grad_carries must be tensors, got {[type(t) for t in cur_grad_carries]}" + ) + cur_grad_carries_tensors = filter_with_masks( + cur_grad_carries, carries_tensor_masks + ) + cur_grad_additional_inputs_tensors = filter_with_masks( + cur_grad_additional_inputs, additional_inputs_tensor_masks + ) + return ( + idx + 1, + *cur_grad_carries_tensors, + *( + cur_grad + grad + for cur_grad, grad in zip( + cur_grad_additional_inputs_tensors, grad_additional_inputs + ) + ), + ) + + args_single_step_bw = ( + init_idx, + *init_grad_carries, + *init_grad_additional_inputs, + *fw_carries, + *ctx.additional_inputs, + ) + + cond_gm = materialize_as_graph( + cond_fn, + args_single_step_bw, + ctx._fw_include_key_set, + ctx._fw_exclude_key_set, + force_enable_grad=True, + ) + + body_gm = materialize_as_graph( + body_fn, + args_single_step_bw, + ctx._fw_include_key_set, + ctx._fw_exclude_key_set, + force_enable_grad=True, + ) + + _, final_grad_carries, final_grad_additional_inputs = split_into_chunks( + while_loop_op( + # pyrefly: ignore [bad-argument-type] + cond_gm, + # pyrefly: ignore [bad-argument-type] + body_gm, + # pyrefly: ignore [bad-argument-type] + ( + init_idx, + *init_grad_carries, + *init_grad_additional_inputs, + ), + (*fw_carries, *ctx.additional_inputs), + ), + [1, len(init_grad_carries), len(init_grad_additional_inputs)], + ) + return ( + None, + None, + None, + None, + *fill_none_with_masks(final_grad_carries, carries_tensor_masks), + *fill_none_with_masks( + final_grad_additional_inputs, additional_inputs_tensor_masks + ), + ) + + +while_loop_stack_output_op = WhileLoopStackOutputOp() + +while_loop_stack_output_op.py_impl(DispatchKey.CompositeExplicitAutograd)( + functools.partial(while_loop_dense, stack_output=True) +) + +while_loop_stack_output_op.py_impl(ProxyTorchDispatchMode)( + functools.partial(while_loop_tracing, stack_output=True) +) + +while_loop_stack_output_op.py_impl(FakeTensorMode)( + functools.partial(while_loop_fake_tensor_mode, stack_output=True) +) + +while_loop_stack_output_op.py_functionalize_impl( + functools.partial(while_loop_func, stack_output=True) +) + +while_loop_stack_output_op.py_autograd_impl( + autograd_not_implemented(while_loop_stack_output_op, deferred_error=True) +) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/wrap.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/wrap.py new file mode 100644 index 0000000000000000000000000000000000000000..37a7cb6cb993f38ca07f198b6bbd4a5a4af16c01 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_higher_order_ops/wrap.py @@ -0,0 +1,546 @@ +# mypy: allow-untyped-defs +import inspect +import itertools +import logging +import weakref +from collections.abc import Callable +from typing import Any +from typing_extensions import ParamSpec, TypeVar + +import torch +import torch.utils._pytree as pytree +from torch._C import DispatchKey +from torch._higher_order_ops.utils import ( + redirect_to_mode, + reenter_make_fx, + register_fake, +) +from torch._logging import warning_once +from torch._ops import HigherOrderOperator +from torch.fx import GraphModule +from torch.fx.experimental.proxy_tensor import ProxyTorchDispatchMode, track_tensor_tree +from torch.types import _dtype +from torch.utils._debug_mode import DebugMode +from torch.utils.checkpoint import _CachedTorchDispatchMode, _CachingTorchDispatchMode + + +_P = ParamSpec("_P") +_R = TypeVar("_R") + + +log = logging.getLogger(__name__) + + +uid = itertools.count(1) + + +# Used for testing the HigherOrderOperator mechanism +class Wrap(HigherOrderOperator): + def __init__(self) -> None: + super().__init__("wrap") + + def __call__( + self, func: Callable[_P, _R], *args: _P.args, **kwargs: _P.kwargs + ) -> _R: + # Dynamo already traces the body of HigherOrderOp beforehand when it + # so no need to trace into it. + import torch._dynamo # noqa: F401 + from torch._dynamo import disable + + @disable + def wrapper(): + result = func(*args, **kwargs) + return result + + return wrapper() + + +wrap = Wrap() + + +class InductorCompiledCode(HigherOrderOperator): + """ + Defines a HOP for wrapping inductor compiled functions as a callable. + When used with torch.compile via "wrap_inductor_compiled_regions", + this HOP will automatically be wrapped and redirect various torch dispatch modes. + """ + + def __init__(self) -> None: + super().__init__("inductor_compiled_code") + + def __call__(self, func, inputs, *, name: str | None = None): + # pyrefly: ignore [missing-attribute] + return super().__call__(func, inputs, name=name) + + +inductor_compiled_code = InductorCompiledCode() +inductor_compiled_code.fallthrough(DispatchKey.AutogradCPU) +inductor_compiled_code.fallthrough(DispatchKey.AutogradCUDA) + + +_inductor_compiled_callable_id = itertools.count() + + +class InductorCompiledCallable: + """ + A wrapper class that holds both the Inductor-compiled callable and the + original FX graph for fake tensor propagation. + Each instance gets a globally unique idx at creation (via atomic itertools.count). + """ + + def __init__( + self, + compiled_callable, + original_gm=None, + compile_region_name: str | None = None, + ): + self.idx = next(_inductor_compiled_callable_id) + self.compiled_callable = compiled_callable + self.original_gm = original_gm + self.compile_region_name = compile_region_name + # AOT autograd needs this to know inputs are passed as a list + self._boxed_call = True + + def __call__(self, inputs): + return self.compiled_callable(inputs) + + +class InductorCodeSideTable: + """ + Side table for storing InductorCompiledCallable objects. + + We cannot put InductorCompiledCallable objects directly into the FX graph + as graph nodes do not support arbitrary objects. We use this side table + and pass callable.idx instead. + + Uses WeakValueDictionary so entries are automatically removed when + the InductorCompiledCallable is no longer referenced elsewhere + (e.g. after dynamo.reset() drops the compiled code cache). + """ + + def __init__(self): + self.id_to_callable: weakref.WeakValueDictionary[ + int, InductorCompiledCallable + ] = weakref.WeakValueDictionary() + + def add_callable(self, callable_obj: InductorCompiledCallable) -> int: + """Register a callable and return its idx.""" + self.id_to_callable[callable_obj.idx] = callable_obj + return callable_obj.idx + + def get_callable(self, idx: int) -> InductorCompiledCallable: + """Get the callable at the given index.""" + assert idx in self.id_to_callable, f"Invalid inductor code index: {idx}" # noqa: S101 + return self.id_to_callable[idx] + + def __getstate__(self): + # Convert WeakValueDictionary to regular dict for pickling + return {"id_to_callable": dict(self.id_to_callable)} + + def __setstate__(self, state): + self.id_to_callable = weakref.WeakValueDictionary(state["id_to_callable"]) + + def reset_table(self) -> None: + """Reset the table.""" + self.id_to_callable = weakref.WeakValueDictionary() + + +inductor_code_side_table = InductorCodeSideTable() + + +def _resolve_inductor_callable( + func: int | InductorCompiledCallable, +) -> InductorCompiledCallable: + """ + Resolve func to an InductorCompiledCallable. + + func is either an InductorCompiledCallable directly (from post_compile) + or an int index into the side table (from a traced FX graph node). + """ + if isinstance(func, int): + return inductor_code_side_table.get_callable(func) + assert isinstance(func, InductorCompiledCallable), ( # noqa: S101 + f"Unexpected func type: {type(func)}" + ) + return func + + +@inductor_compiled_code.py_impl(DispatchKey.CompositeExplicitAutograd) +def inductor_compiled_code_impl(func, inputs, *, name=None): + resolved = _resolve_inductor_callable(func) + return resolved.compiled_callable(inputs) + + +redirect_to_mode(inductor_compiled_code, DebugMode) +redirect_to_mode(inductor_compiled_code, _CachingTorchDispatchMode) +redirect_to_mode(inductor_compiled_code, _CachedTorchDispatchMode) + + +@register_fake(inductor_compiled_code) +def inductor_compiled_code_fake(func, inputs, *, name=None): + resolved = _resolve_inductor_callable(func) + if resolved.original_gm is None: + raise RuntimeError( + "inductor_compiled_code original_gm is None — the compiled graph may " + "have been serialized without it. Recompile to restore." + ) + # Run the original FX graph under FakeTensorMode to re-derive output + # shapes, dtypes, and aliasing from the input fake tensors. + return tuple(resolved.original_gm(*inputs)) + + +@inductor_compiled_code.py_functionalize_impl +def inductor_compiled_code_functionalize(ctx, func, inputs, *, name=None): + # Unwrap the functional tensors to get the underlying tensors + unwrapped_inputs = ctx.unwrap_tensors(inputs) + + # Redispatch to the next handler in the dispatch chain + with ctx.redispatch_to_next(): + kwargs = {"name": name} if name is not None else {} + result = inductor_compiled_code(func, unwrapped_inputs, **kwargs) + return ctx.wrap_tensors(result) + + +@inductor_compiled_code.py_impl(ProxyTorchDispatchMode) +def inductor_compiled_code_proxy(mode, func, inputs, *, name=None): + resolved = _resolve_inductor_callable(func) + + # Run the fake impl to get example outputs for tracing + kwargs = {"name": name} if name is not None else {} + example_out = inductor_compiled_code(func, inputs, **kwargs) + + # Register in side table so the FX node stores a serializable int + callable_idx = inductor_code_side_table.add_callable(resolved) + + proxy_inputs = pytree.tree_map(mode.tracer.unwrap_proxy, inputs) + + out_proxy = mode.tracer.create_proxy( + "call_function", + inductor_compiled_code, + (callable_idx, proxy_inputs), + kwargs, + ) + + return track_tensor_tree(example_out, out_proxy, constant=None, tracer=mode.tracer) + + +class WrapWithSetGradEnabled(HigherOrderOperator): + def __init__(self) -> None: + super().__init__("wrap_with_set_grad_enabled") + + def __call__( + self, + enable_grad: bool, + wrapped_func: Callable[_P, _R], + *args: _P.args, + **kwargs: _P.kwargs, + ) -> _R: + # Dynamo already traces the body of HigherOrderOp beforehand when it + # so no need to trace into it. + import torch._dynamo # noqa: F401 + from torch._dynamo import disable + + @disable + def wrapper(): + prev = torch.is_grad_enabled() + torch.set_grad_enabled(enable_grad) + res = wrapped_func(*args, **kwargs) + torch.set_grad_enabled(prev) + return res + + return wrapper() + + +wrap_with_set_grad_enabled = WrapWithSetGradEnabled() + + +class WrapWithAutocast(HigherOrderOperator): + def __init__(self): + super().__init__("wrap_with_autocast") + + def __call__( + self, + device_type: str, + dtype: _dtype | None, + enabled: bool, + cache_enabled: bool | None, + wrapped_func: Callable[_P, _R], + *args: _P.args, + **kwargs: _P.kwargs, + ) -> _R: + # Dynamo already traces the body of HigherOrderOp beforehand when it + # so no need to trace into it. + import torch._dynamo # noqa: F401 + from torch._dynamo import disable + + @disable + def wrapper(): + with torch.autocast(device_type, dtype, enabled, cache_enabled): + return wrapped_func(*args, **kwargs) + + return wrapper() + + +wrap_with_autocast = WrapWithAutocast() + + +# This HOP allows you to bypass dynamo tracing of the wrapper function while +# still tracing the inner function. +# Takes two callables: The first, `wrapper_fn`, accepts `inner_fn` and returns a +# callable with the same signature. The second is the `inner_fn` itself. Any +# extra *args and **kwargs are forwarded to `wrapper_fn(inner_fn)` when it is +# executed. +class DynamoBypassingWrapper(HigherOrderOperator): + def __init__(self): + super().__init__("dynamo_bypassing_wrapper") + + def __call__( + self, + wrapper_fn_or_key, + inner_fn, + *args, + **kwargs, + ): + # Dynamo already traces the body of HigherOrderOp beforehand when it + # so no need to trace into it. + import torch._dynamo # noqa: F401 + from torch._dynamo import disable + + is_compiling = isinstance(wrapper_fn_or_key, str) + if is_compiling: + if not isinstance(inner_fn, torch.fx.GraphModule): + raise AssertionError( + f"expected inner_fn to be torch.fx.GraphModule, got {type(inner_fn)}" + ) + wrapper_fn = inner_fn.meta[wrapper_fn_or_key] + else: + wrapper_fn = wrapper_fn_or_key + + @disable + def wrapper(): + return wrapper_fn(inner_fn)(*args, **kwargs) + + return wrapper() + + +dynamo_bypassing_wrapper = DynamoBypassingWrapper() + + +class WrapActivationCheckpoint(HigherOrderOperator): + """ + This operator is used to wrap torch.utils.checkpoint. This avoids + TorchDynamo to look into saved tensor hooks and directly passes the control + to AOT Autograd, which is ok with tracing saved tensor hooks. As a result of + AOT tracing torch.utils.checkpoint code, we have a backward graph with + recomputed forward nodes. + + However, we might deprecate this operator soon. The difficulty arises in the + functionalization of rng ops. Today, there are two different + functionalization of rng ops - one at AOT autograd and other at Inductor. + And they are difficult to map to each other. The rng states also complicate + pattern matching in Inductor. Due to the ease of implementation, we are + currently inclined towards functionalization at Inductor level, which means + that duplication/recomputation is done as a compiler pass in the + partitioners. See TagActivationCheckpoint for more information. + """ + + def __init__(self) -> None: + super().__init__("wrap_activation_checkpoint", cacheable=False) + + def __call__(self, function: GraphModule, *args: Any, **kwargs: Any) -> Any: + # use_reentrant is set to False because this op is going to be traced. + # And we ensure that AOT Autograd traces through the non reentrant + # version of checkpointing. + import torch.fx.traceback as fx_traceback + from torch.fx import Interpreter + + kwargs["use_reentrant"] = False + kwargs["preserve_rng_state"] = False + # Using interpreter allows preservation of metadata through torch.compile stack. + with fx_traceback.preserve_node_meta(): + from torch.utils.checkpoint import checkpoint + + return checkpoint(Interpreter(function).run, *args, **kwargs) + + +wrap_activation_checkpoint = WrapActivationCheckpoint() + + +class TagActivationCheckpoint(HigherOrderOperator): + """ + This operator is supposed to be used only with torch.compile stack. This + accepts a Fx graph module which needs to be checkpointed. This operator adds + "recomputable" tag to the nodes of the Fx graph that should be recomputed. + + The goal is to: + 1. Avoid using Dynamo to trace through saved tensor hooks. + 2. For selective checkpointing case, let AOTAutograd trace through + saved tensor hooks but has special logic with TorchDispatchMode to override + the usual saved_tensor_hooks fn logic in order to tag the nodes. + 3. Rely on the partitioners to actually duplicate the nodes. + This sits well in the torch.compile stack, because by the time graph + reaches partitioner, inductor has already run its functionalization of rng + ops (by setting fixed seed for each random op, see `replace_random_passes`). + Therefore, the duplication of nodes, by design, respects the rng states in + the forward and recomputed forward in backward. + """ + + def __init__(self) -> None: + super().__init__("tag_activation_checkpoint", cacheable=True) + + @staticmethod + def divide_kwargs(kwargs): + """ + checkpoint fn can have mixed kwargs between checkpointed fn and + checkpoint fn itself. For example + >> def gn(x, y, z=None): + >> a = torch.matmul(x, y) + >> if z is not None: + >> return torch.matmul(a, z) + >> return a + >> def fn(x, y, z): + >> return torch.cos(checkpoint(gn, x, y, use_reentrant=False, z=z)) + In the above case, z belongs to checkpointed function gn, but + use_reentrant belongs to the checkpoint function. This function splits + the kwargs into checkpoint_kwargs and gmod_kwargs (or + checkpointed_fn_kwargs). + We do sorting to ensure same graph from run to run for better + debuggability. It is not required for correctness. + """ + from torch.utils.checkpoint import checkpoint + + ckpt_signature = inspect.signature(checkpoint) + checkpoint_keys = set() + for name in ckpt_signature.parameters: + if name in ("function", "args", "kwargs"): + continue + checkpoint_keys.add(name) + + # `preserve_rng_state` is not a regular kwarg + checkpoint_keys.add("preserve_rng_state") + + checkpoint_kwargs = { + name: kwargs[name] for name in kwargs if name in checkpoint_keys + } + gmod_kwargs = { + name: kwargs[name] for name in kwargs if name not in checkpoint_keys + } + return checkpoint_kwargs, gmod_kwargs + + def __call__(self, gmod, *args, **kwargs): + dispatch_key_set = torch._ops._compute_keyset( + args, kwargs, self.non_fallthrough_keys + ) + dispatch_key = dispatch_key_set.highestPriorityTypeId() + if dispatch_key == torch._C.DispatchKey.PreDispatch: + # pyrefly: ignore [missing-attribute] + return super().__call__(gmod, *args, **kwargs) + + return tag_activation_checkpoint_impl(gmod, *args, **kwargs) + + +tag_activation_checkpoint = TagActivationCheckpoint() + + +def _always_prefer_recompute(ctx, op, *args, **kwargs): + from torch.utils.checkpoint import CheckpointPolicy + + return CheckpointPolicy.PREFER_RECOMPUTE + + +def tag_activation_checkpoint_impl(gmod, *args, **kwargs): + import functools + + import torch.fx.traceback as fx_traceback + from torch.fx import Interpreter + from torch.utils.checkpoint import create_selective_checkpoint_contexts + + unique_graph_id = next(uid) + if "_checkpoint_context_fn" in gmod.meta: + context_fn = gmod.meta["_checkpoint_context_fn"] + warning_once( + log, + """ +Detected that context_fn is passed to torch.utils.checkpoint under torch.compile. +Please make sure the checkpointed region does not contain in-place ops (e.g. torch.relu_). +""", + ) + else: + # Vanilla AC: use the SAC path with a policy that always recomputes. + context_fn = functools.partial( + create_selective_checkpoint_contexts, _always_prefer_recompute + ) + + def context_fn_with_graph_id(): + fwd_ctx, recomp_ctx = context_fn() + # Plumb ac_graph_id so _CachingTorchDispatchMode tags all nodes + # (including ops from desugared HOPs like custom autograd.Function). + fwd_ctx.ac_graph_id = unique_graph_id + return fwd_ctx, recomp_ctx + + # use_reentrant is set to False because this op is going to be traced. + # And we ensure that AOT Autograd traces through the non reentrant + # version of checkpointing. + kwargs["use_reentrant"] = False + # preserve_rng_state is set to False because we want to prevent AOTAutograd from tracing through + # `torch.random.fork_rng` op (which is not supported yet under CUDA). + # This doesn't mean that we don't preserve RNG state. Instead, we will always preserve RNG state + # regardless of this flag (by doing RNG functionalization via `replace_random_passes` in Inductor + # instead of in AOTAutograd). + kwargs["preserve_rng_state"] = False + kwargs["context_fn"] = context_fn_with_graph_id + # Disable early stop to prevent _StopRecomputationError from interrupting + # recomputation between _vmap_increment_nesting and _vmap_decrement_nesting, + # which would leak a functorch dynamic layer. + kwargs["early_stop"] = False + # Using interpreter allows preservation of metadata through torch.compile stack. + # We use a wrapper instead of passing Interpreter(gmod).run directly because + # checkpoint's recompute_fn captures the function in a closure. A bound method + # reference would keep the Interpreter alive, whose env dict retains the output + # tensors and prevents the autograd graph from being freed. + + def run_with_interpreter(*args): + return Interpreter(gmod).run(*args) + + with fx_traceback.preserve_node_meta(): + from torch.utils.checkpoint import checkpoint + + return checkpoint(run_with_interpreter, *args, **kwargs) + + +@tag_activation_checkpoint.py_impl(ProxyTorchDispatchMode) +def proxy_mode_key( + proxy_mode: ProxyTorchDispatchMode, + gmod: GraphModule, + *args: Any, + **kwargs: Any, +) -> tuple[torch.Tensor]: + import torch.fx.traceback as fx_traceback + from torch.fx import Interpreter + + if not proxy_mode.pre_dispatch: + raise AssertionError( + "post-dispatch mode should have inlined in the Autograd key" + ) + example_out = tag_activation_checkpoint(gmod, *args, **kwargs) + proxy_args = pytree.tree_map(proxy_mode.tracer.unwrap_proxy, args) # type: ignore[union-attr] + proxy_kwargs = pytree.tree_map(proxy_mode.tracer.unwrap_proxy, kwargs) # type: ignore[union-attr] + qualname = proxy_mode.tracer.get_fresh_qualname("wrap_body") # type: ignore[union-attr] + + # TODO (tmanlaibaatar) don't we need flat_apply here?? + # Dynamo already traced the gmod body without kwargs + flat_args, _ = pytree.tree_flatten(args) + with fx_traceback.preserve_node_meta(): + gmod_aten = reenter_make_fx(Interpreter(gmod).run)(*flat_args) + gmod_aten.meta["_checkpoint_context_fn"] = gmod.meta["_checkpoint_context_fn"] + proxy_mode.tracer.root.register_module(qualname, gmod_aten) # type: ignore[union-attr] + proxy_gmod = proxy_mode.tracer.unwrap_proxy(gmod_aten) # type: ignore[union-attr, call-overload] + out_proxy = proxy_mode.tracer.create_proxy( + "call_function", + tag_activation_checkpoint, + (proxy_gmod, *proxy_args), + proxy_kwargs, + ) + return track_tensor_tree( + example_out, out_proxy, constant=None, tracer=proxy_mode.tracer + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/__autotune_main__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/__autotune_main__.py new file mode 100644 index 0000000000000000000000000000000000000000..1eb5ca86e8c185e9c355e6dea152b53a3f181519 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/__autotune_main__.py @@ -0,0 +1,33 @@ +import argparse +import logging +import os + +from torch._inductor.autotune_process import TuningProcess +from torch._inductor.compile_worker.utils import _async_compile_initializer + + +log = logging.getLogger(__name__) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--parent", type=int) + parser.add_argument("--read-fd", type=int) + parser.add_argument("--write-fd", type=int) + args = parser.parse_args() + read_pipe = os.fdopen(args.read_fd, "rb") + write_pipe = os.fdopen(args.write_fd, "wb") + + try: + # Ensures the subprocess exits if the parent crashes: + _async_compile_initializer(args.parent) + TuningProcess.process_main(read_pipe, write_pipe) + except Exception: + log.exception("Uncaught exception in autotune subprocess") + finally: + read_pipe.close() + write_pipe.close() + + +if __name__ == "__main__": + main() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..09a0de086adbc77eb73a24daa5f50035743baf83 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/__init__.py @@ -0,0 +1,462 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import dataclasses +import io +import logging +import os +from typing import Any, IO, Literal, Optional, TYPE_CHECKING, Union + +import torch.fx + +from .standalone_compile import CompiledArtifact # noqa: TC001 + + +if TYPE_CHECKING: + from torch._inductor.utils import InputType + from torch.export import ExportedProgram + from torch.export.pt2_archive._package import AOTICompiledModel + from torch.export.pt2_archive._package_weights import Weights + from torch.types import FileLike + +__all__ = [ + "compile", + "list_mode_options", + "list_options", + "cudagraph_mark_step_begin", + "standalone_compile", +] + + +log = logging.getLogger(__name__) + + +def compile( + gm: torch.fx.GraphModule, + example_inputs: list[InputType], + options: dict[str, Any] | None = None, +): + """ + Compile a given FX graph with TorchInductor. This allows compiling + FX graphs captured without using TorchDynamo. + + Args: + gm: The FX graph to compile. + example_inputs: List of tensor inputs. + options: Optional dict of config options. See `torch._inductor.config`. + + Returns: + Callable with same behavior as gm but faster. + """ + from .compile_fx import compile_fx + + return compile_fx(gm, example_inputs, config_patches=options) + + +def aoti_compile_and_package( + exported_program: ExportedProgram, + _deprecated_unused_args=None, + _deprecated_unused_kwargs=None, + *, + package_path: FileLike | None = None, + inductor_configs: dict[str, Any] | None = None, +) -> str: + """ + Compiles the exported program with AOTInductor, and packages it into a .pt2 + artifact specified by the input package_path. To load the package, you can + call ``torch._inductor.aoti_load_package(package_path)``. + + An example usage is as follows: + + .. code-block:: python + + ep = torch.export.export(M(), ...) + aoti_file = torch._inductor.aoti_compile_and_package( + ep, package_path="my_package.pt2" + ) + compiled_model = torch._inductor.aoti_load_package("my_package.pt2") + + To compile and save multiple models into a single ``.pt2`` artifact, you can do + the following: + + .. code-block:: python + + ep1 = torch.export.export(M1(), ...) + aoti_file1 = torch._inductor.aot_compile( + ep1, ..., options={"aot_inductor.package": True} + ) + ep2 = torch.export.export(M2(), ...) + aoti_file2 = torch._inductor.aot_compile( + ep2, ..., options={"aot_inductor.package": True} + ) + + from torch._inductor.package import package_aoti, load_package + + package_aoti("my_package.pt2", {"model1": aoti_file1, "model2": aoti_file2}) + + compiled_model1 = load_package("my_package.pt2", "model1") + compiled_model2 = load_package("my_package.pt2", "model2") + + Args: + exported_program: An exported program created through a call from torch.export + package_path: Optional specified path to the generated .pt2 artifact. + inductor_configs: Optional dictionary of configs to control inductor. + + Returns: + Path to the generated artifact + """ + from torch.export import ExportedProgram + + from .debug import aot_inductor_minifier_wrapper + + if not isinstance(exported_program, ExportedProgram): + raise ValueError("Only ExportedProgram is supported") + + if exported_program.example_inputs is None: + raise RuntimeError( + "exported_program.example_inputs is required to be set in order " + "for AOTInductor compilation." + ) + + if _deprecated_unused_args is not None or _deprecated_unused_kwargs is not None: + log.warning( + "You no longer need to specify args/kwargs to aoti_compile_and_package " + "as we can get this information from exported_program.example_inputs." + ) + + assert ( + package_path is None + or ( + isinstance(package_path, (io.IOBase, IO)) + and package_path.writable() + and package_path.seekable() + ) + or ( + isinstance(package_path, (str, os.PathLike)) + and os.fspath(package_path).endswith(".pt2") + ) + ), ( + f"Expect package path to be a file ending in .pt2, is None, or is a buffer. Instead got {package_path}" + ) + + inductor_configs = inductor_configs or {} + inductor_configs["aot_inductor.package"] = True + + if inductor_configs.get("aot_inductor.output_path"): + raise RuntimeError( + "Please pass in a package path to aot_inductor_compile() instead " + "of setting the aot_inductor.output_path config." + ) + + # a wrapper around aoti_compile_and_package_inner. + return aot_inductor_minifier_wrapper( + _aoti_compile_and_package_inner, + exported_program, + package_path=package_path, + inductor_configs=inductor_configs, + ) + + +def _aoti_compile_and_package_inner( + gm: torch.nn.Module, + # flat_example_inputs: List[Any], + args: tuple[Any], + kwargs: dict[str, Any] | None = None, + *, + load_and_run: bool = False, + check_accuracy: str | None = None, + package_path: str | io.BytesIO | None = None, + inductor_configs: dict[str, Any] | None = None, +): + """ + See docstring for aoti_compile_and_package. + + If `load_and_run` is True, this function will load the compiled model and run it. + This is for the minifier to check the correctness of the compiled model. + + If `check_accuracy` is set, this function will check the accuracy of the compiled + model against gm. kwargs must be None if check_accuracy is set. + "strict_accuracy" means "we will minify any time we see anything that + diverges", whereas "accuracy" is more conservative, and will only minify if there + is a meaningful fp64 divergence + """ + + if check_accuracy: + assert kwargs is None or len(kwargs) == 0, ( + "when checking for accuracy, the inputs must have been flattened and kwargs is None" + ) + + from .package import package_aoti + + assert isinstance(gm, torch.fx.GraphModule) + + kwargs = kwargs or {} + + aoti_files = aot_compile(gm, args, kwargs, options=inductor_configs) + assert isinstance(aoti_files, list) + + if package_path is None: + path = [ + os.path.splitext(file)[0] + for file in aoti_files + if isinstance(file, str) and os.path.splitext(file)[1] == ".so" + ] + if len(path) == 0: + path = [ + os.path.splitext(file)[0] + for file in aoti_files + if isinstance(file, str) and os.path.splitext(file)[1] == ".cpp" + ] + package_path = path[0] + ".pt2" + + res = package_aoti(package_path, aoti_files) + assert res == package_path + + if load_and_run or check_accuracy: + compiled_model = aoti_load_package(package_path) + if check_accuracy: + from torch._dynamo.debug_utils import AccuracyError, same_two_models + + # This might look inverted but it's not. strict_accuracy means "we will + # minify any time we see anything that diverges", whereas accuracy is more + # conservative, and will only minify if there is a meaningful fp64 + # divergence + not_strict_accuracy = check_accuracy == "accuracy" + if not same_two_models( + gm, + compiled_model, # type: ignore[arg-type] + args, + only_fwd=True, + require_fp64=not_strict_accuracy, + ignore_non_fp=not_strict_accuracy, + ): + raise AccuracyError("Bad accuracy detected") + else: + compiled_model(*args, **kwargs) + + return package_path + + +def aoti_load_package( + path: FileLike, run_single_threaded: bool = False, device_index: int = -1 +) -> AOTICompiledModel: + """ + Loads the model from the PT2 package. + + If multiple models were packaged into the PT2, this will load the default + model. To load a specific model, you can directly call the load API + + .. code-block:: python + + from torch._inductor.package import load_package + + compiled_model1 = load_package("my_package.pt2", "model1") + compiled_model2 = load_package("my_package.pt2", "model2") + + Args: + path: Path to the .pt2 package + run_single_threaded (bool): Whether the model should be run without + thread synchronization logic. This is useful to avoid conflicts with + CUDAGraphs. + device_index (int): The index of the device to which the PT2 package is + to be loaded. By default, `device_index=-1` is used, which corresponds + to the device `cuda` when using CUDA. Passing `device_index=1` would + load the package to `cuda:1`, for example. + """ + from torch._inductor.package import load_package + + return load_package( + path, run_single_threaded=run_single_threaded, device_index=device_index + ) + + +def aot_compile( + gm: torch.fx.GraphModule, + args: tuple[Any, ...], + kwargs: dict[str, Any] | None = None, + *, + options: dict[str, Any] | None = None, +) -> str | list[str | Weights] | torch.fx.GraphModule: + """ + Ahead-of-time compile a given FX graph with TorchInductor into a shared library. + + Args: + gm: The FX graph to compile. + args: Example arguments + kwargs: Example keyword arguments + options: Optional dict of config options. See `torch._inductor.config`. + + Returns: + Path to the generated shared library, or a list of files generated by + AOTI if aot_inductor.package=True. + TODO: make it return a list by default + """ + from .compile_fx import _aoti_flatten_inputs, compile_fx_aot + + if hasattr(gm, "_guards_fn"): + # Do not compile the guards function, since it may contain checks + # that are not currently supported by AOTI. In particular, non-Tensor + # arguments are converted to None and will fail specialization checks. + node = next(iter(gm.graph.find_nodes(op="call_module", target="_guards_fn"))) + gm.graph.erase_node(node) + delattr(gm, "_guards_fn") + gm.recompile() + + flat_example_inputs, options = _aoti_flatten_inputs( + gm, args, kwargs, options=options + ) + from torch._export.utils import _compiling_state_context + + with _compiling_state_context(): + return compile_fx_aot( + gm, + flat_example_inputs, # type: ignore[arg-type] + config_patches=options, + ) + + +lite_mode_options = { + # Fallback by default unless users explicitly annotated with + # regional inductor compile. + "fallback_by_default": True, + "selective_decompose": True, + # Disable reorder optimizations + "reorder_for_peak_memory": False, + "reorder_for_compute_comm_overlap": False, + "triton.reorder_for_reducing_graph_partitions": False, + # Disable pre-, joint-, post-grad passes + "use_pre_grad_passes": False, + "use_joint_graph_passes": False, + "use_post_grad_passes": False, + # Disable dead code elimination (dce) and buffer reuse + "use_dce": False, + "allow_buffer_reuse": False, +} + + +def list_mode_options( + mode: str | None = None, dynamic: bool | None = None +) -> dict[str, Any]: + r"""Returns a dictionary describing the optimizations that each of the available + modes passed to `torch.compile()` performs. + + Args: + mode (str, optional): The mode to return the optimizations for. + If None, returns optimizations for all modes + dynamic (bool, optional): Whether dynamic shape is enabled. + + Example:: + >>> torch._inductor.list_mode_options() + """ + + mode_options: dict[str, dict[str, bool]] = { + "default": {}, + # lite backend for opt-in optimizations + "lite": lite_mode_options, + # enable cudagraphs + "reduce-overhead": { + "triton.cudagraphs": True, + }, + # enable max-autotune + "max-autotune-no-cudagraphs": { + "max_autotune": True, + "coordinate_descent_tuning": True, + }, + # enable max-autotune + # enable cudagraphs + "max-autotune": { + "max_autotune": True, + "triton.cudagraphs": True, + "coordinate_descent_tuning": True, + }, + } + try: + return mode_options[mode] if mode else mode_options + except KeyError as e: + raise RuntimeError( + f"Unrecognized mode={mode}, should be one of: {', '.join(mode_options.keys())}" + ) from e + + +def list_options() -> list[str]: + r"""Returns a dictionary describing the optimizations and debug configurations + that are available to `torch.compile()`. + + The options are documented in `torch._inductor.config`. + + Example:: + + >>> torch._inductor.list_options() + """ + + from torch._inductor import config + + current_config: dict[str, Any] = config.get_config_copy() + + return list(current_config.keys()) + + +def cudagraph_mark_step_begin(): + "Indicates that a new iteration of inference or training is about to begin." + from .cudagraph_trees import mark_step_begin + + mark_step_begin() + + +def standalone_compile( + gm: torch.fx.GraphModule, + example_inputs: list[InputType], + *, + dynamic_shapes: Literal[ + "from_example_inputs", "from_tracing_context", "from_graph" + ] = "from_graph", + options: dict[str, Any] | None = None, + aot: bool = False, # AOT mode, which uses BundledAOTAutogradCache + donate_graph_module: bool = False, +) -> CompiledArtifact: + """ + Precompilation API for inductor. + + .. code-block:: python + + compiled_artifact = torch._inductor.standalone_compile(gm, args) + compiled_artifact.save(path=path, format="binary") + + # Later on a new process + loaded = torch._inductor.CompiledArtifact.load(path=path, format="binary") + compiled_out = loaded(*args) + + Args: + gm: Graph Module + example_inputs: Inputs for the graph module + dynamic_shapes: If "from_graph" (default), we will use the dynamic + shapes in the passed-in graph module. + If "from_tracing_context", we use the dynamic shape info in the + ambient tracing context. + If "from_example_inputs", we will specialize the graph on the + example_inputs. + options: Inductor compilation options + donate_graph_module: If True, standalone_compile takes ownership of + the graph module and may mutate it, avoiding an internal deepcopy. + Defaults to False for backwards compatibility. + + Returns: + CompiledArtifact that can be saved to disk or invoked directly. + """ + from .standalone_compile import standalone_compile + + options = options if options else {} + return standalone_compile( + gm, + example_inputs, + dynamic_shapes=dynamic_shapes, + options=options, + aot=aot, + donate_graph_module=donate_graph_module, + ) + + +@dataclasses.dataclass +class _CudagraphAnnotation: + fwd: bool | None + bwd: bool | None diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/analysis/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/analysis/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/analysis/device_info.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/analysis/device_info.py new file mode 100644 index 0000000000000000000000000000000000000000..6b8dcfc2e385cbbc31921b83a4489ba26d65ccfc --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/analysis/device_info.py @@ -0,0 +1,215 @@ +import logging +from dataclasses import dataclass + +import torch + + +log = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class DeviceInfo: + """ + Theoretical Numbers from data sheet. If two numbers are given, Tensor/Matrix Core vs not, + then the higher number is reported. Sparsity is not considered. + + + Bandwidth numbers are tricky, because there are platform differences that may not show up in the profiler trace. + For example, + """ + + tops: dict[torch.dtype | str, float] + dram_bw_gbs: float + dram_gb: float + + +# Indexing is based on `torch.cuda.get_device_name()` +# TODO investigate profiler support for tf32 and allow device to report correct number when it's turned on. +_device_mapping: dict[str, DeviceInfo] = { + # Source: + # @lint-ignore https://www.nvidia.com/en-us/data-center/h100/ + "NVIDIA H100": DeviceInfo( + tops={ + torch.float64: 67.0, + torch.float32: 67.5, + "torch.tf32": 156.0, + torch.bfloat16: 1979.0, + torch.float16: 1979.0, + torch.float8_e8m0fnu: 3958.0, + torch.float8_e8m0fnu: 3958.0, + torch.float8_e4m3fnuz: 3958.0, + torch.float8_e5m2: 3958.0, + torch.float8_e5m2fnuz: 3958.0, + torch.float8_e8m0fnu: 3958.0, + torch.int8: 3958.0, + }, + dram_bw_gbs=3350, + dram_gb=80, + ), + # Source: + # @lint-ignore https://www.nvidia.com/content/dam/en-zz/Solutions/Data-Center/a100/pdf/ + # nvidia-a100-datasheet-us-nvidia-1758950-r4-web.pdf + "NVIDIA A100": DeviceInfo( + tops={ + torch.float64: 19.5, + torch.float32: 19.5, + torch.bfloat16: 312.5, + torch.float16: 312.5, + # Not in datasheet: float8 + torch.int8: 624.0, + "torch.tf32": 156.0, + }, + dram_bw_gbs=2039.0, + dram_gb=80.0, + ), + # Source: + # @lint-ignore https://resources.nvidia.com/en-us-gpu-resources/l4-tensor-datasheet + "NVIDIA L4": DeviceInfo( + tops={ + # This is a guess, not in datasheet + torch.float64: 15.1, + torch.float32: 30.3, + "torch.tf32": 120.0, + torch.bfloat16: 242.0, + torch.float16: 242.0, + torch.float8_e8m0fnu: 485.0, + torch.float8_e8m0fnu: 485.0, + torch.float8_e4m3fnuz: 485.0, + torch.float8_e5m2: 485.0, + torch.float8_e5m2fnuz: 485.0, + torch.float8_e8m0fnu: 485.0, + torch.int8: 485.0, + }, + dram_bw_gbs=3350, + dram_gb=24, + ), + # Source: + # @lint-ignore https://www.amd.com/content/dam/amd/en/documents\ + # /instinct-tech-docs/product-briefs/amd-instinct-mi350x-gpu-brochure.pdf + "AMD MI350X": DeviceInfo( + tops={ + torch.float64: 72.1, + torch.float32: 144.2, + # not specified, fall back to float32 numbers + "torch.tf32": 144.2, + torch.bfloat16: 2309.6, + torch.float16: 2309.6, + torch.float8_e8m0fnu: 4614.0, + torch.float8_e8m0fnu: 4614.0, + torch.float8_e4m3fnuz: 4614.0, + torch.float8_e5m2: 4614.0, + torch.float8_e5m2fnuz: 4614.0, + torch.float8_e8m0fnu: 4614.0, + torch.int8: 4614.0, + }, + dram_bw_gbs=8000.0, + dram_gb=288.0, + ), + # Source: + # @lint-ignore https://www.amd.com/content/dam/amd/en/documents\ + # /instinct-tech-docs/data-sheets/amd-instinct-mi300a-data-sheet.pdf + "AMD MI300A": DeviceInfo( + tops={ + torch.float64: 122.6, + torch.float32: 122.6, + "torch.tf32": 490.3, + torch.bfloat16: 980.6, + torch.float16: 980.6, + torch.float8_e8m0fnu: 1961.2, + torch.float8_e8m0fnu: 1961.2, + torch.float8_e4m3fnuz: 1961.2, + torch.float8_e5m2: 1961.2, + torch.float8_e5m2fnuz: 1961.2, + torch.float8_e8m0fnu: 1961.2, + torch.int8: 1961.2, + }, + dram_bw_gbs=5300.0, + dram_gb=128.0, + ), + # Source: + # @lint-ignore https://www.amd.com/content/dam/amd/en/documents/\ + # instinct-tech-docs/data-sheets/amd-instinct-mi300x-data-sheet.pdf + "AMD MI300X": DeviceInfo( + tops={ + torch.float64: 163.4, + torch.float32: 163.4, + "torch.tf32": 653.7, + torch.bfloat16: 1307.4, + torch.float16: 1307.4, + torch.float8_e8m0fnu: 2614.9, + torch.float8_e8m0fnu: 2614.9, + torch.float8_e4m3fnuz: 2614.9, + torch.float8_e5m2: 2614.9, + torch.float8_e5m2fnuz: 2614.9, + torch.float8_e8m0fnu: 2614.9, + torch.int8: 2614.9, + }, + dram_bw_gbs=5300.0, + dram_gb=192.0, + ), + # Source: + # @lint-ignore https://www.amd.com/content/dam/amd/\ + # en/documents/instinct-business-docs/product-briefs/instinct-mi210-brochure.pdf + "AMD MI210X": DeviceInfo( + tops={ + torch.float64: 45.3, + torch.float32: 45.3, + # not specified, fall back to float32 numbers + "torch.tf32": 45.3, + torch.bfloat16: 181.0, + torch.float16: 181.0, + # not specified, fall back to float16 numbers + torch.float8_e8m0fnu: 181.0, + torch.float8_e8m0fnu: 181.0, + torch.float8_e4m3fnuz: 181.0, + torch.float8_e5m2: 181.0, + torch.float8_e5m2fnuz: 181.0, + torch.float8_e8m0fnu: 181.0, + torch.int8: 181.0, + }, + # pcie4.0x16 + dram_bw_gbs=1600.0, + dram_gb=64.0, + ), +} +_device_mapping["AMD INSTINCT MI350X"] = _device_mapping["AMD MI350X"] +_device_mapping["AMD INSTINCT MI300X"] = _device_mapping["AMD MI300X"] +_device_mapping["AMD INSTINCT MI210X"] = _device_mapping["AMD MI210X"] + + +def lookup_device_info(name: str) -> DeviceInfo | None: + """ + Problem: when diffing profiles between amd and nvidia, we don't have access to the device information + of the other one. Also, since the analysis is static, we should be able to do it on another device unrelated + to the recorded device. Therefore, _device_mapping statically contains the information for lots of devices. + If one is missing, please run DeviceInfo.get_device_info() and add it to _device_mapping. + name (str): name of the device to lookup. Should map onto torch.cuda.get_device_name(). + """ + return _device_mapping.get(name) + + +def datasheet_tops(dtype: torch.dtype, is_tf32: bool = False) -> float | None: + """ + Get the theoretical TFLOPS of the device for a given dtype. This can throw an exception if the device + is not in the datasheet list above. + """ + name: str | None = torch.cuda.get_device_name() + if name is None: + log.info("No device found, returning None") + return None + device_info = lookup_device_info(name) + if device_info is None: + log_str = f"Device {name} not in datasheet, returning None" + log.info(log_str) + return None + if dtype not in device_info.tops: + log.info( + "Device %s does not have a datasheet entry for %s, returning None", + name, + dtype, + ) + return None + + return device_info.tops[ + "torch.tf32" if dtype == torch.float32 and is_tf32 else dtype + ] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/analysis/profile_analysis.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/analysis/profile_analysis.py new file mode 100644 index 0000000000000000000000000000000000000000..3dd20fba88a92975bf04f15ba67f94991d8387de --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/analysis/profile_analysis.py @@ -0,0 +1,823 @@ +import json +import logging +import math +from collections import defaultdict +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any, Optional + +import torch +from torch._inductor.analysis.device_info import DeviceInfo, lookup_device_info +from torch._inductor.utils import tabulate_2d, zip_dicts +from torch.utils import _pytree as pytree +from torch.utils._ordered_set import OrderedSet +from torch.utils.flop_counter import flop_registry + + +log = logging.getLogger(__name__) + + +ATEN_PREFIX = "aten::" + + +@dataclass +class ProfileEvent: + category: str + key: str + self_device_time_ms: float + # the benchmark is run multiple times and we average the count across all the + # runs. It should be an integer but define a float just in case. + count: float + + +# adapters convert the json trace into a format that works with flops_counter +ArgsType = tuple[tuple[Any, ...], dict[Any, Any]] +AdapterType = Callable[[tuple[Any, ...], tuple[Any, ...]], ArgsType] +adapters_map: dict[str, AdapterType] = {} + + +def parse_list(lst: str) -> list[int]: + lst = lst.replace("[", "").replace("]", "") + substrings = lst.split(",") + + return [int(substring.strip()) for substring in substrings] + + +def register_adapter( + aten: str | list[str], +) -> Callable[ + [AdapterType], + AdapterType, +]: + def decorator(func: AdapterType) -> AdapterType: + # pyrefly: ignore [unknown-name] + global _adapters_map + + if isinstance(aten, str): + adapters_map[aten] = func + else: + for at in aten: + adapters_map[at] = func + return func + + return decorator + + +@register_adapter(["_slow_conv2d_forward"]) +def _slow_conv2d_adapter( + shapes: tuple[Any, ...], concrete: tuple[Any, ...] +) -> tuple[tuple[Any], dict[Any, Any]]: + tmp = list(shapes) + tmp.append(False) + tmp2 = list(concrete) + if len(tmp2) < 5: + raise ParseException("slow conv2d has less than 5 concrete inputs") + tmp2[3] = tmp2[4] + return conv_adapter(tuple(tmp), tuple(tmp2)) + + +@register_adapter( + ["convolution", "_convolution", "cudnn_convolution", "convolution_overrideable"] +) +def conv_adapter( + shapes: tuple[Any, ...], concrete: tuple[Any, ...] +) -> tuple[tuple[Any], dict[Any, Any]]: + tmp = list(shapes) + if len(tmp) == 4: + transposed = False + elif len(tmp) > 6: + transposed = bool(tmp[6]) + tmp[6] = transposed + else: + raise ParseException(f"Convolution has the wrong number of inputs: {len(tmp)}") + + kwargs: dict[Any, Any] = {} + if not transposed: + # calculate output shape if not transposed. + def conv_out_dims(x: int, kernel: int, stride: int) -> int: + return (x - kernel) // stride + 1 + + stride = parse_list(concrete[3]) + inp = shapes[0] + w = shapes[1] + out_x_y = [conv_out_dims(*args) for args in zip(inp[2:], w[2:], stride)] + out = [inp[0], w[0]] + out_x_y # we only need the xy values + kwargs["out_val"] = out + + return tuple(tmp), kwargs + + +def default_adapter( + shapes: tuple[Any], concrete: tuple[Any] +) -> tuple[tuple[Any], dict[Any, Any]]: + return shapes, {} + + +@register_adapter("addmm") +def addmm_adapter( + shapes: tuple[Any], concrete: tuple[Any] +) -> tuple[tuple[Any], dict[Any, Any]]: + tmp = list(shapes)[:3] + return tuple(tmp), {} + + +@register_adapter("bmm") +def bmm_adapter( + shapes: tuple[Any], concrete: tuple[Any] +) -> tuple[tuple[Any], dict[Any, Any]]: + tmp = list(shapes) + return tuple(tmp[:2]), {} + + +@register_adapter("baddbmm") +def baddbmm_adapter( + shapes: tuple[Any], concrete: tuple[Any] +) -> tuple[tuple[Any], dict[Any, Any]]: + tmp = list(shapes)[:3] + return tuple(tmp), {} + + +@register_adapter("mm") +def mm_adapter( + shapes: tuple[Any], concrete: tuple[Any] +) -> tuple[tuple[Any], dict[Any, Any]]: + return shapes, {} + + +def _parse_kernel_name(name: str) -> str | None: + """ + parse the name of the kernel from the event name. + """ + if name.startswith(ATEN_PREFIX): + return name[len(ATEN_PREFIX) :] + elif "conv" in name: + return "convolution" + elif "addmm" in name: + return "addmm" + elif "bmm" in name: + return "bmm" + elif "baddbmm" in name: + return "baddbmm" + elif "_mm" in name: + return "mm" + else: + return None + + +def _calculate_flops(event: dict[str, Any]) -> int: + """ + This function has to parse the kernel name, which is error prone. There doesn't seem to be another solution that + will support all the different backends that can generate kernels, so make sure to update this function when new + ops and backends are desired. + """ + name = event["name"] + if "kernel_flop" in event["args"] and event["args"]["kernel_flop"] != 0: + return event["args"]["kernel_flop"] + op_name = _parse_kernel_name(name) + if op_name is None: + return 0 + + op_obj = getattr(torch.ops.aten, op_name, None) + if op_obj is None or op_obj not in flop_registry: + return 0 + + flop_function = flop_registry[op_obj] + + if "Input Dims" not in event["args"] or "Concrete Inputs" not in event["args"]: + return 0 + input_shapes = event["args"]["Input Dims"] + concrete = event["args"]["Concrete Inputs"] + if op_name in adapters_map: + try: + args, kwargs = adapters_map[op_name](input_shapes, concrete) + except ParseException as e: + msg = f"Failed to parse {op_name} with {e}" + log.warning(msg) + return 0 + else: + try: + args, kwargs = default_adapter(input_shapes, concrete) + except ParseException as e: + msg = f"Failed to parse {op_name} with {e}" + log.warning(msg) + return 0 + return flop_function(*args, **kwargs) + + +def _get_size_from_string(type_string: str) -> int: + if not hasattr(torch, type_string): + return 1 + else: + return getattr(torch, type_string).itemsize + + +def _default_estimate_gb(event: dict[str, Any]) -> float: + sizes_and_types = zip(event["args"]["Input Dims"], event["args"]["Input type"]) + bw = 0 + for size, typ in sizes_and_types: + isize = _get_size_from_string(typ) + bw += isize * math.prod(pytree.tree_flatten(size)[0]) + return bw / 1e9 + + +def _estimate_gb(event: dict[str, Any]) -> float: + """ + Our best effort to estimate the gb, should be refactored soon with MemoryCounter. + """ + name = event["name"] + if "kernel_num_gb" in event["args"] and event["args"]["kernel_num_gb"] != 0: + return event["args"]["kernel_num_gb"] + if "Input type" not in event["args"] or "Input Dims" not in event["args"]: + return 0 + op_name = _parse_kernel_name(name) + if op_name is None: + return _default_estimate_gb(event) + + op_obj = getattr(torch.ops.aten, op_name, None) + if op_obj is None: + return _default_estimate_gb(event) + + if "Input Dims" not in event["args"] or "Concrete Inputs" not in event["args"]: + return _default_estimate_gb(event) + input_shapes = event["args"]["Input Dims"] + + # NOTE these will be refactored into a similar object to FlopCounter soon + def mm_formula(M: int, N: int, K: int, size: int) -> int: + return 2 * (M * K + N * K + M * N) * size + + if op_name == "addmm": + add_in_size = math.prod(pytree.tree_flatten(input_shapes[0])[0]) + add_type_size = _get_size_from_string(event["args"]["Input type"][0]) + M = input_shapes[1][0] + N = input_shapes[1][1] + assert input_shapes[1][1] == input_shapes[2][0] + K = input_shapes[2][1] + mul_type_size = _get_size_from_string(event["args"]["Input type"][1]) + return (mm_formula(M, N, K, mul_type_size) + add_in_size * add_type_size) / 1e9 + elif op_name == "mm": + M = input_shapes[0][0] + N = input_shapes[0][1] + assert input_shapes[0][1] == input_shapes[1][0] + K = input_shapes[1][1] + type_size = _get_size_from_string(event["args"]["Input type"][0]) + return mm_formula(M, N, K, type_size) / 1e9 + elif op_name == "baddbmm": + add_in_size = math.prod(pytree.tree_flatten(input_shapes[0])[0]) + add_type_size = _get_size_from_string(event["args"]["Input type"][0]) + B = input_shapes[0][0] + M = input_shapes[1][1] + N = input_shapes[1][2] + K = input_shapes[2][2] + mul_type_size = _get_size_from_string(event["args"]["Input type"][1]) + return ( + B * mm_formula(M, N, K, mul_type_size) + add_in_size * add_type_size + ) / 1e9 + elif op_name == "bmm": + add_in_size = math.prod(pytree.tree_flatten(input_shapes[0])[0]) + add_type_size = _get_size_from_string(event["args"]["Input type"][0]) + B = input_shapes[0][0] + M = input_shapes[0][1] + N = input_shapes[0][2] + K = input_shapes[1][2] + mul_type_size = _get_size_from_string(event["args"]["Input type"][1]) + return ( + B * mm_formula(M, N, K, mul_type_size) + add_in_size * add_type_size + ) / 1e9 + elif op_name in [ + "convolution", + "_convolution", + "cudnn_convolution", + "_slow_conv2d_forward", + ]: + concrete = event["args"]["Concrete Inputs"] + + def conv_out_dim(x: int, kernel: int, stride: int) -> int: + return (x - kernel) // stride + 1 + + stride = parse_list( + concrete[3] if op_name != "_slow_conv2d_forward" else concrete[4] + ) + inp = input_shapes[0] + w = input_shapes[1] + out_x_y = [conv_out_dim(*args) for args in zip(inp[2:], w[2:], stride)] + out = [inp[0], w[0]] + out_x_y + # each output element reads in * w * w chunk + input_reads = out[0] * out[1] * out[2] * out[3] * inp[1] * w[2] * w[3] + # Assume weights are in cache, so only read once + weight_reads = w[0] * w[1] * w[2] * w[3] + return (input_reads + weight_reads) / 1e9 + + return _default_estimate_gb(event) + + +def _create_extern_mapping( + data: dict[str, Any], +) -> defaultdict[int, list[dict[str, Any]]]: + """ + compute a mapping from external ids to non kernels, which contain the information we need to estimate flops etc + """ + extern_mapping: defaultdict[int, list[dict[str, Any]]] = defaultdict(list) + for event in data["traceEvents"]: + if ( + "args" not in event + or "External id" not in event["args"] + or event["cat"] != "cpu_op" + ): + continue + if len(extern_mapping[event["args"]["External id"]]) > 0: + raise ParseException("duplicate external id in event") + extern_mapping[event["args"]["External id"]].append(event) + return extern_mapping + + +def _augment_trace_helper(data: dict[str, Any]) -> dict[str, Any]: + extern_mapping = _create_extern_mapping(data) + + for event in data["traceEvents"]: + if "cat" not in event or event["cat"] != "kernel": + continue + if "args" not in event: + raise ParseException(f"kernel has no args: {event}") + if "External id" not in event["args"]: + event_str = f"kernel has no External id: {event}" + log.info(event_str) + continue + + external_op = extern_mapping[event["args"]["External id"]][0] + flops = _calculate_flops(external_op) + if flops == 0: + flops = _calculate_flops(event) + external_op["args"]["kernel_flop"] = flops + external_op["args"]["kernel_num_gb"] = _estimate_gb(external_op) + event["args"]["kernel_flop"] = external_op["args"]["kernel_flop"] + event["args"]["kernel_num_gb"] = external_op["args"]["kernel_num_gb"] + return data + + +_dtype_map = { + "float": torch.float, + "float32": torch.float, + "int": torch.int, + "int8": torch.int8, + "int16": torch.int16, + "int32": torch.int, + "long": torch.long, + "long int": torch.long, + "bfloat16": torch.bfloat16, + "float16": torch.float16, + "float64": torch.double, +} + + +@dataclass(frozen=True) +class KernelStats: + flops: int + bw: float + latency: float # us + achieved_flops: float + achieved_bandwidth: float + + +KernelNameMap = defaultdict[str, OrderedSet[KernelStats]] + + +@dataclass(frozen=False) +class Device: + name: str + index: int + info: DeviceInfo | None + stats: KernelNameMap + + def __repr__(self) -> str: + return f"Device({self.name}, {self.index}): {self.info}" + + +DeviceMap = dict[int, Device] +Table = tuple[list[str], dict[str, list[str]]] + + +class JsonProfile: + _devices: DeviceMap + + def __init__( + self, + path: str, + benchmark_name: str | None = None, + dtype: torch.dtype | str | None = None, + ): + """ + Convenience class for running common operations on chrome/perfetto json traces. + """ + self.path = path + with open(path) as f: + self.data = json.load(f) + self.events = self.data["traceEvents"] + self.benchmark_name = benchmark_name + if dtype is None: + self.dtype = None + elif isinstance(dtype, torch.dtype): + # pyrefly: ignore [bad-assignment] + self.dtype = dtype + else: + # pyrefly: ignore [bad-assignment] + self.dtype = _dtype_map.get(dtype) + self._create_devices() + + def convert_dtype(self, event: dict[str, Any]) -> torch.dtype | None: + """ + Each op has a list of dtypes for each input arg. We need to convert these into a single dtype for flop estimation. + Issues: + - converting the strings to concrete torch.dtypes + - What if we have float32, float, float16 all in the inputs? Our choice is to use the largest buffer dtype. + """ + + if ( + "Input Dims" not in event["args"] + or "Input type" not in event["args"] + or "Concrete Inputs" not in event["args"] + ): + if "bfloat16" in event["name"]: + return torch.bfloat16 + elif "float16" in event["name"]: + return torch.float16 + else: + return None + + input_sizes = event["args"]["Input Dims"] + input_types = event["args"]["Input type"] + concrete_inputs = event["args"]["Concrete Inputs"] + assert len(input_sizes) == len(input_types) + assert len(input_types) == len(concrete_inputs) + + if len(input_sizes) == 0: + raise RuntimeError("Empty input_sizes and input_types") + + biggest_size = 0 + biggest_index = 0 + for i in range(len(input_sizes)): + if concrete_inputs[i] != "": + # concrete inputs are usually small tensors, so we can just skip + continue + my_size = input_sizes[i] + total_size = sum(parse_list(my_size)) + if total_size > biggest_size: + biggest_size = total_size + biggest_index = i + ret_type = input_types[biggest_index] + if ret_type in _dtype_map: + return _dtype_map[ret_type] + raise RuntimeError(f"Unknown type: {ret_type}. Please add to _dtype_map.") + + def _create_devices(self) -> None: + self._devices = {} + for dev in self.data["deviceProperties"]: + name = dev["name"] + device_info = lookup_device_info(name) + + if device_info is None: + log.info( + "Unsupported device in profile: %s, please consider contributing to _device_mapping.", + name, + ) + self._devices[dev["id"]] = Device( + name, dev["id"], device_info, defaultdict(OrderedSet) + ) + + def calculate_flops(self, event: dict[str, Any]) -> int: + return _calculate_flops(event) + + def estimate_gb(self, event: dict[str, Any]) -> float: + return _estimate_gb(event) + + def augment_trace(self) -> None: + self.data = _augment_trace_helper(self.data) + + def _compute_stats(self) -> None: + """populates the name -> stats map""" + for event in self.events: + if "cat" not in event or "args" not in event or event["cat"] != "kernel": + continue + if "device" not in event["args"]: + continue + dev_tmp = event["args"]["device"] + if dev_tmp not in self._devices: + continue + dev = self._devices[event["args"]["device"]] + + dur = event["dur"] # us + if "kernel_flop" in event["args"]: + assert dur != 0 + # 1,000,000us/s * flop / us + op_flops = event["args"]["kernel_flop"] / (dur / 1e6) + else: + op_flops = 0 + + if "kernel_num_gb" in event["args"]: + assert dur != 0 + # 1,000,000us/s * gb = gb/s + op_gbps = event["args"]["kernel_num_gb"] / (dur / 1e6) + else: + op_gbps = 0 + + if dev.info is not None: + dtype = self.convert_dtype(event) or self.dtype + if dtype is None: + raise RuntimeError( + "dtype is not found on tensor and default dtype is not set" + ) + achieved_flops = 100 * op_flops / (1e12 * dev.info.tops[dtype]) + achieved_bandwidth = 100 * op_gbps / dev.info.dram_bw_gbs + else: + achieved_flops = 0 + achieved_bandwidth = 0 + + if "name" not in event["args"]: + continue + dev.stats[event["name"]].add( + KernelStats( + flops=op_flops, + bw=op_gbps, + latency=dur, + achieved_bandwidth=achieved_bandwidth, + achieved_flops=achieved_flops, + ) + ) + + def _create_single_table(self, dev: Device) -> Table: + """Create a table with the devices mapped to indices.""" + headers = [ + "Kernel Name", + "Kernel Count", + "FLOPS", + "Kernel Reads (GB)", + "Dur (us)", + "Achieved FLOPS %", + "Achieved Bandwidth %", + ] + rows: dict[str, list[str]] = {} + + def safe_div_format(x: float, y: float) -> str: + if y == 0: + return "0.0" + return f"{x / y:.4f}" + + for kernel_name, stats_set in dev.stats.items(): + ker_count = 0 + flops = 0 + flops_count = 0 + achieved_flops = 0.0 + bw = 0.0 + bw_count = 0 + achieved_bandwidth = 0.0 + latency = 0.0 + for stats in stats_set: + if stats.flops != 0: + flops += stats.flops + achieved_flops += stats.achieved_flops + flops_count += 1 + if stats.bw != 0: + bw += stats.bw + achieved_bandwidth += stats.achieved_bandwidth + bw_count += 1 + latency += stats.latency + ker_count += 1 + assert ker_count != 0 + rows[kernel_name] = [ + str(ker_count), + safe_div_format(flops, flops_count), + safe_div_format(bw, bw_count), + safe_div_format(latency, ker_count), + safe_div_format(achieved_flops, flops_count), + safe_div_format(achieved_bandwidth, bw_count), + ] + + return headers, rows + + def _create_tables(self, devs: DeviceMap) -> dict[int, Table]: + return {idx: self._create_single_table(dev) for idx, dev in devs.items()} + + def _combine_tables( + self, table1: Table, table1_name: str, table2: Table, table2_name: str + ) -> Table: + new_headers = ( + ["Kernel Name"] + + [f"{table1_name} {head}" for head in table1[0][1:]] + + [f"{table2_name} {head}" for head in table2[0][1:]] + ) + t1_length = len(table1[0][1:]) + t2_length = len(table2[0][1:]) + new_rows = {} + + for key, row1, row2 in zip_dicts( + table1[1], + table2[1], + d1_default=["Empty"] * t1_length, + d2_default=["Empty"] * t2_length, + ): + assert row1 is not None + assert row2 is not None + new_rows[key] = row1 + row2 + return new_headers, new_rows + + def report( + self, other: Optional["JsonProfile"] = None, name_limit: int = 40 + ) -> str: + def create_ret( + table_headers: list[str], table_rows: dict[str, list[str]] + ) -> str: + table_flattened = [ + [kernel_name[:name_limit], *kernel_vals] + for kernel_name, kernel_vals in table_rows.items() + ] + return tabulate_2d(table_flattened, headers=table_headers) + + if other is not None: + self._compute_stats() + other._compute_stats() + + self_tables = self._create_tables(self._devices) + other_tables = self._create_tables(other._devices) + + self_name = ( + self.benchmark_name if self.benchmark_name is not None else "Table 1" + ) + other_name = ( + other.benchmark_name if other.benchmark_name is not None else "Table 2" + ) + + ret = [] + assert self._devices.keys() == other._devices.keys() + for device_idx, t1, t2 in zip_dicts( + self_tables, other_tables, d1_default=None, d2_default=None + ): + assert t1 is not None + assert t2 is not None + table_headers, table_rows = self._combine_tables( + t1, self_name, t2, other_name + ) + tab_string = create_ret(table_headers, table_rows) + # pyrefly: ignore [bad-argument-type] + ret.append(f"{self._devices[device_idx]}:\n{tab_string}") + return "\n".join(ret) + self._compute_stats() + + self_tables = self._create_tables(self._devices) + + ret = [] + for idx, table in self_tables.items(): + table_headers, table_rows = table + tab_string = create_ret(table_headers, table_rows) + # pyrefly: ignore [bad-argument-type] + ret.append(f"{self._devices[idx]}:\n{tab_string}") + return "\n".join(ret) + + def dump(self, out: str) -> None: + with open(out, "w") as f: + json.dump(self.data, f) + + def combine_with(self, other: "JsonProfile") -> "JsonProfile": + """ + Combine this profile with another profile by merging their trace events. + Returns a new JsonProfile object with combined data. + """ + # Create a new combined data structure + combined_data = { + "traceEvents": self.data["traceEvents"] + other.data["traceEvents"], + "deviceProperties": self.data.get("deviceProperties", []), + } + + # Merge device properties, avoiding duplicates + other_device_props = other.data.get("deviceProperties", []) + existing_device_ids = OrderedSet( + [dev["id"] for dev in combined_data["deviceProperties"]] + ) + + for device_prop in other_device_props: + if device_prop["id"] not in existing_device_ids: + combined_data["deviceProperties"].append(device_prop) + + # Copy any other top-level properties from the first profile + for key, value in self.data.items(): + if key not in combined_data: + combined_data[key] = value + + import os + + # Create a temporary file to write the combined data + import tempfile + + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False + ) as tmp_file: + json.dump(combined_data, tmp_file) + tmp_path = tmp_file.name + + try: + # Create new JsonProfile from the combined data + combined_profile = JsonProfile( + tmp_path, + benchmark_name=f"{self.benchmark_name or 'Profile1'}_+_{other.benchmark_name or 'Profile2'}", + dtype=self.dtype or other.dtype, + ) + return combined_profile + finally: + # Clean up temporary file + os.unlink(tmp_path) + + +class ParseException(RuntimeError): + pass + + +def main() -> None: + """ + Main function for the profile analysis script. + """ + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument( + "--diff", + nargs=5, + metavar=( + "input_file1", + "name1", + "input_file2", + "name2", + "dtype", + ), + help="Two json traces to compare with, specified as ", + ) + parser.add_argument( + "--name_limit", + type=int, + help="the maximum name size in the final report", + ) + parser.add_argument( + "--augment_trace", + "-a", + nargs=3, + metavar=("input_file", "output_file", "dtype"), + help="Augment a trace with inductor meta information. Provide input and output file paths.", + ) + parser.add_argument( + "--analysis", + nargs=2, + metavar=("input_file", "dtype"), + help="Run analysis on a single trace, specified as ", + ) + parser.add_argument( + "--combine", + nargs="+", + metavar=("input_files", "output_file"), + help="Combine multiple profiles into a single profile by merging trace events. Specify as \ + [input_file3 ...] . The last argument is the output file, all preceding arguments are \ +input files to combine.", + ) + args = parser.parse_args() + + if args.diff: + p1 = JsonProfile(args.diff[0], args.diff[1], dtype=args.diff[4]) + p1.augment_trace() + p2 = JsonProfile(args.diff[2], args.diff[3], dtype=args.diff[4]) + p2.augment_trace() + if args.name_limit: + print(p1.report(p2, name_limit=args.name_limit)) + else: + print(p1.report(p2)) + if args.analysis: + p1 = JsonProfile( + args.analysis[0], + dtype=args.analysis[1], + ) + p1.augment_trace() + if args.name_limit: + print(p1.report(name_limit=args.name_limit)) + else: + print(p1.report()) + if args.augment_trace: + p = JsonProfile(args.augment_trace[0], dtype=args.augment_trace[2]) + p.augment_trace() + p.dump(args.augment_trace[1]) + if args.combine: + input_files = args.combine[:-1] # All arguments except the last one + output_file = args.combine[-1] # Last argument is the output file + + if len(input_files) < 2: + print("Error: At least 2 input files are required for combining") + return + + # Load the first profile + combined = JsonProfile(input_files[0], dtype=None) + + # Iteratively combine with all other profiles + for input_file in input_files[1:]: + profile = JsonProfile(input_file, dtype=None) + combined = combined.combine_with(profile) + + combined.dump(output_file) + print(f"Successfully combined {', '.join(input_files)} into {output_file}") + + +if __name__ == "__main__": + main() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/analyze_preserves_zero_mask.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/analyze_preserves_zero_mask.py new file mode 100644 index 0000000000000000000000000000000000000000..b1721a4f8290f3bc3ff8f1002fa2020bb5d95b62 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/analyze_preserves_zero_mask.py @@ -0,0 +1,166 @@ +import dataclasses +import itertools +from typing import Any, TYPE_CHECKING + +import sympy + +import torch +from torch._inductor import config +from torch._inductor.dtype_propagation import DtypePropagationOpsHandler +from torch._inductor.index_propagation import SymPyOps, TypedExpr + +from .ops_handler import DefaultHandler +from .virtualized import StoreMode, V + + +if TYPE_CHECKING: + from torch._inductor.scheduler import SchedulerNode + + +def construct_symbol(count: int, dtype: torch.dtype) -> sympy.Symbol: + return sympy.Symbol(f"unknown_{count}") + + +class PreservesZeros(SymPyOps, DefaultHandler): + """ + For prologue kernels where the loads are masked, does the final store of this kernel preserve + the zeros. + """ + + def __init__(self) -> None: + self.count = itertools.count(0) + self.store_preserves_zeros: bool | None = None + self.dtype_prop = DtypePropagationOpsHandler() + + def load(self, name: str, index: sympy.Expr) -> TypedExpr: + # In prologue fusion, all loads get broadcasted + dtype = self.dtype_prop.load(name, index) + return TypedExpr( + sympy.Float(0) if dtype.is_floating_point else sympy.Integer(0), dtype + ) + + def store( + self, name: str, index: sympy.Expr, value: TypedExpr, mode: "StoreMode" = None + ) -> None: + assert isinstance(self, PreservesZeros) + # should only have a single store in prologue + assert self.store_preserves_zeros is None + self.store_preserves_zeros = value.is_constant() and value.expr == 0 + + def indirect_indexing(self, *args: Any, **kwargs: Any) -> sympy.Expr: + return construct_symbol(next(self.count), torch.int32) + + def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: + from torch._inductor.codegen.common import OpDecompositions + + if hasattr(OpDecompositions, name): + return getattr(OpDecompositions, name)(*args, **kwargs).value + + dtype = getattr(self.dtype_prop, name)(*args, **kwargs) + return TypedExpr(construct_symbol(next(self.count), dtype), dtype) + + +def prologue_preserves_zero_mask(prologue: "SchedulerNode") -> bool: + """ + Does this prologue preserve zero masks + """ + preserves_zeros = PreservesZeros() + with V.set_ops_handler(preserves_zeros): + prologue._body(*prologue.get_ranges()) + + store_preserves_zeros = preserves_zeros.store_preserves_zeros + assert isinstance(store_preserves_zeros, bool) + + return store_preserves_zeros + + +@dataclasses.dataclass +class DTypeContainer: + dtype: torch.dtype + is_scalar: bool = False + + +class RecordLowPrecisionOps(DefaultHandler): + def __init__(self, disallow_fp32_ops: bool = False) -> None: + self.disallow_fp32_ops = disallow_fp32_ops + self.low_precision_numeric_op = False + self.dtype_prop = DtypePropagationOpsHandler() + self.non_numeric_ops = ( + "to_dtype", + "constant", + "where", + ) + + def load(self, name: str, index: sympy.Expr) -> DTypeContainer: + return DTypeContainer(self.dtype_prop.load(name, index)) + + @staticmethod + def store( + name: str, index: sympy.Expr, value: TypedExpr, mode: "StoreMode" = None + ) -> None: + pass + + def check_bounds( + self, expr: sympy.Expr, size: sympy.Expr, lower: bool, upper: bool + ) -> None: + pass + + @staticmethod + # pyrefly: ignore [bad-override] + def indirect_indexing(*args: Any, **kwargs: Any) -> sympy.Expr: + return sympy.S.Zero + + def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: + out_dtype = getattr(self.dtype_prop, name)(*args, **kwargs) + out = DTypeContainer(out_dtype, is_scalar=(name == "constant")) + if name == "constant": + return DTypeContainer(torch.float, is_scalar=True) + + uses_low_prec = any( + isinstance(dtype_cont, DTypeContainer) + and dtype_cont.dtype is not None + and low_prec_float(dtype_cont.dtype) + for dtype_cont in itertools.chain((out,), args, kwargs.values()) + ) + + if uses_low_prec and name not in self.non_numeric_ops: + self.low_precision_numeric_op = True + + if ( + self.disallow_fp32_ops + and out.dtype in (torch.float32, torch.float64) + and not out.is_scalar + ): + self.low_precision_numeric_op = True + + return out + + +def low_prec_float(dtype: torch.dtype) -> bool: + return dtype.is_floating_point and dtype.itemsize < 4 + + +def can_codegen_without_upcasts( + prologue: "SchedulerNode", + disallow_fp32_ops: bool = False, +) -> bool: + """ + Can this prologue be run without `upcast_to_fp32` while preserving numerics. + + This is only true if the node only contains dtype conversions, indexing, and other non-arithmetic operators. + + If disallow_fp32_ops is True, then we also disallow ops that are explicitly computed in fp32 or fp64. + """ + if prologue.get_operation_names() <= V.graph.low_precision_codegen_ops: + return True + + low_prec_analysis = RecordLowPrecisionOps(disallow_fp32_ops) + + # Need to turn off upcasting to do analysis of whether we can turn it off + with ( + config.patch("triton.codegen_upcast_to_fp32", False), + V.set_ops_handler(low_prec_analysis), + ): + prologue._body(*prologue.get_ranges()) + + return not low_prec_analysis.low_precision_numeric_op diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/aoti_eager.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/aoti_eager.py new file mode 100644 index 0000000000000000000000000000000000000000..69f6a45afa5769fcb2ddad1014d1b6b985e855fb --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/aoti_eager.py @@ -0,0 +1,340 @@ +import json +import logging +import os +from collections.abc import Callable +from dataclasses import dataclass +from pathlib import Path +from typing import Any +from unittest import mock + +import torch +import torch._export +from torch._inductor.utils import is_cpu_device + +from .runtime.runtime_utils import cache_dir + + +log = logging.getLogger(__name__) + + +@dataclass +class AOTICompileBackend: + compile_fn: Callable[..., str] + load_fn: Callable[[str, str, str], list[dict[str, Any] | None]] + + +_aoti_compile_backends: dict[str, AOTICompileBackend] = {} + + +def register_aoti_compile_backend( + device_type: str, + compile_fn: Callable[..., str], + load_fn: Callable[[str, str, str], list[dict[str, Any] | None]], +) -> None: + _aoti_compile_backends[device_type] = AOTICompileBackend( + compile_fn=compile_fn, + load_fn=load_fn, + ) + + +def aoti_eager_cache_dir(namespace: str, device: str) -> Path: + return Path(cache_dir()) / "aoti_eager" / namespace / device + + +def aoti_eager_op_conf_lock(op_func_name_with_overload: str) -> Any: + # Avoid circular import + from torch._inductor.codecache import get_lock_dir, LOCK_TIMEOUT + from torch.utils._filelock import FileLock + + op_conf_lock_file = f"{op_func_name_with_overload}.lock" + lock_dir = get_lock_dir() + return FileLock(os.path.join(lock_dir, op_conf_lock_file), timeout=LOCK_TIMEOUT) + + +def load_aoti_eager_cache( + ns: str, op_func_name_with_overload: str, device_type: str +) -> list[dict[str, Any] | None]: + backend = _aoti_compile_backends.get(device_type) + if backend: + return backend.load_fn(ns, op_func_name_with_overload, device_type) + + device_kernel_cache = aoti_eager_cache_dir(ns, device_type) + op_conf = device_kernel_cache / f"{op_func_name_with_overload}.json" + if not op_conf.exists(): + return [] + + try: + with aoti_eager_op_conf_lock(op_func_name_with_overload): + with open(op_conf) as f: + json_data = json.load(f) + for item in json_data: + # Get absolution path for kernel library + kernel_lib_abs_path = device_kernel_cache / item["kernel_path"] + item["kernel_path"] = kernel_lib_abs_path.as_posix() + + # Check if the kernel library exists + if not kernel_lib_abs_path.exists(): + return [] + + for metadata in item["meta_info"]: + if metadata.get("is_dynamic"): + raise NotImplementedError( + "Only support static shape for now" + ) + if ( + "device_type" in metadata + and metadata["device_type"] == "cpu" + ): + metadata["device_index"] = -1 + for dtype_key in ["dtype", "dtype_value"]: + if dtype_key in metadata: + metadata[dtype_key] = getattr( + torch, metadata[dtype_key].split(".")[-1] + ) + if "layout_value" in metadata: + metadata["layout_value"] = getattr( + torch, metadata["layout_value"].split(".")[-1] + ) + if "memory_format_value" in metadata: + metadata["memory_format_value"] = getattr( + torch, metadata["memory_format_value"].split(".")[-1] + ) + + return json_data + except Exception as e: + err_msg = f"Failed to load aoti eager cache: {e}" + log.exception(err_msg) + return [] + + +def supported_builtin_dtype_torch_dtype() -> dict[type, torch.dtype]: + return {int: torch.int32, float: torch.float, bool: torch.bool} + + +def supported_scalar_types() -> tuple[type, ...]: + type_to_torch_dtype = supported_builtin_dtype_torch_dtype() + return tuple(type_to_torch_dtype.keys()) + + +def extract_tensor_metadata(dynamic: bool, input: torch.Tensor) -> dict[str, Any]: + metadata: dict[str, Any] = {} + metadata["is_dynamic"] = dynamic + + assert isinstance(input, torch.Tensor) + metadata["device_type"] = f"{input.device.type}" + if is_cpu_device([input]): + metadata["device_index"] = -1 + else: + metadata["device_index"] = input.device.index + metadata["dtype"] = f"{input.dtype}" + metadata["sizes"] = list(input.size()) + metadata["strides"] = list(input.stride()) + metadata["requires_grad"] = input.requires_grad + metadata["dispatch_key_set"] = torch._C._dispatch_keys(input).raw_repr() + return metadata + + +def extract_tensor_list_metadata( + dynamic: bool, + input: list[torch.Tensor], +) -> dict[str, Any]: + metadata_list = [] + for item in input: + assert isinstance(item, torch.Tensor) + metadata_list.append(extract_tensor_metadata(dynamic, item)) + + metadata: dict[str, Any] = {} + metadata["tensor_list"] = metadata_list + return metadata + + +def extract_scalar_metadata(device_type: str, input: Any) -> dict[str, Any]: + assert isinstance(input, supported_scalar_types()) + metadata: dict[str, Any] = {} + metadata["is_dynamic"] = False + # Scalar tensor + metadata["device_type"] = device_type + metadata["device_index"] = -1 if device_type == "cpu" else 0 + type_to_torch_dtype = supported_builtin_dtype_torch_dtype() + metadata["dtype"] = f"{type_to_torch_dtype[type(input)]}" + metadata["scalar_value"] = input + return metadata + + +def extract_string_metadata(input: str) -> dict[str, Any]: + assert isinstance(input, str) + metadata: dict[str, Any] = {} + metadata["string_value"] = input + return metadata + + +def extract_dtype_metadata(input: torch.dtype) -> dict[str, Any]: + assert isinstance(input, torch.dtype) + metadata: dict[str, Any] = {} + metadata["dtype_value"] = f"{input}" + return metadata + + +def extract_device_metadata(input: torch.device) -> dict[str, Any]: + assert isinstance(input, torch.device) + metadata: dict[str, Any] = {} + metadata["device_type_value"] = f"{input.type}" + metadata["device_index_value"] = input.index + return metadata + + +def extract_layout_metadata(input: torch.layout) -> dict[str, Any]: + assert isinstance(input, torch.layout) + metadata: dict[str, Any] = {} + metadata["layout_value"] = f"{input}" + return metadata + + +def aoti_compile_with_persistent_cache( + ns: str, + op_func_name_with_overload: str, + device_type: str, + dynamic: bool, + f: Callable[..., Any], + args: tuple[Any], + kwargs: dict[str, Any], + *, + dynamic_shapes: dict[str, Any] | None = None, + options: dict[str, Any] | None = None, + remove_runtime_assertions: bool = False, + disable_constraint_solver: bool = False, +) -> str: + """ + Compile the given function with persistent cache for AOTI eager mode. + """ + backend = _aoti_compile_backends.get(device_type) + if backend: + return backend.compile_fn( + ns, + op_func_name_with_overload, + device_type, + dynamic, + f, + args, + kwargs, + dynamic_shapes=dynamic_shapes, + options=options, + remove_runtime_assertions=remove_runtime_assertions, + disable_constraint_solver=disable_constraint_solver, + ) + + assert not dynamic, "Only support static shape for now" + flattened_inputs = list(args) + list(kwargs.values()) + if not all( + isinstance( + input, + ( + supported_scalar_types(), + torch.Tensor, + list, + str, + torch.dtype, + torch.device, + torch.layout, + ), + ) + for input in flattened_inputs + ): + err_msg = f"Unsupported input types: {flattened_inputs}" + log.exception(err_msg) + raise NotImplementedError(err_msg) + + for input in flattened_inputs: + if isinstance(input, list) and not all( + isinstance(item, torch.Tensor) for item in input + ): + err_msg = f"_impl_with_aoti_compile encounters unsupported input types: {flattened_inputs}" + log.exception(err_msg) + raise NotImplementedError(err_msg) + + persistent_cache = aoti_eager_cache_dir(ns, device_type) + if not persistent_cache.exists(): + persistent_cache.mkdir(parents=True) + + persistent_cache_lib = persistent_cache / "lib" + if not persistent_cache_lib.exists(): + persistent_cache_lib.mkdir() + + with mock.patch.dict( + os.environ, + {"TORCHINDUCTOR_CACHE_DIR": persistent_cache_lib.absolute().as_posix()}, + ): + try: + kernel_lib_path = torch._export.aot_compile( + f, + args, + kwargs, + dynamic_shapes=dynamic_shapes, + remove_runtime_assertions=remove_runtime_assertions, + disable_constraint_solver=disable_constraint_solver, + # Some operations may have non-Tensor parameters like int, float, bool. These + # non-Tensor parameters will not be the input of the graph. Therefore, we do + # need to keep the same signature. + same_signature=False, + ) + assert isinstance(kernel_lib_path, str) + + kernel_metadata_items = [] + + for idx, input in enumerate(flattened_inputs): + if isinstance(input, torch.Tensor): + metadata = extract_tensor_metadata(dynamic, input) + elif isinstance(input, list): + assert all(isinstance(item, torch.Tensor) for item in input) + metadata = extract_tensor_list_metadata(dynamic, input) + elif isinstance(input, supported_scalar_types()): + metadata = extract_scalar_metadata(device_type, input) + elif isinstance(input, str): + metadata = extract_string_metadata(input) + elif isinstance(input, torch.dtype): + metadata = extract_dtype_metadata(input) + elif isinstance(input, torch.device): + metadata = extract_device_metadata(input) + elif isinstance(input, torch.layout): + metadata = extract_layout_metadata(input) + else: + raise NotImplementedError(f"Unsupported input type: {type(input)}") + + metadata["arg_order"] = idx + kernel_metadata_items.append(metadata) + + kernel_meta_info: dict[str, Any] = {} + kernel_meta_info["meta_info"] = kernel_metadata_items + kernel_meta_info["kernel_path"] = ( + Path(kernel_lib_path).relative_to(persistent_cache).as_posix() + ) + + json_data = [] + update_json = True + op_conf = persistent_cache / f"{op_func_name_with_overload}.json" + mode = "r" if op_conf.exists() else "w" + with aoti_eager_op_conf_lock(op_func_name_with_overload): + with open(op_conf, mode) as op_conf_file: + try: + json_data = json.load(op_conf_file) + except Exception: + json_data = [] + + assert isinstance(json_data, list) + for item in json_data: + assert isinstance(item, dict) + # Same kernel meta info already exists in the json file + if item["meta_info"] == kernel_metadata_items: + update_json = False + break + + if update_json: + json_data.append(kernel_meta_info) + with open(op_conf, "w") as op_conf_file: + json.dump(json_data, op_conf_file, indent=4) + + return kernel_lib_path + except Exception as e: + err_msg = f"Failed to compile {op_func_name_with_overload}: {e}" + log.exception(err_msg) + return "" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/async_compile.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/async_compile.py new file mode 100644 index 0000000000000000000000000000000000000000..1e6dcbece6b6350b42401d5d15b75218b8d866b6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/async_compile.py @@ -0,0 +1,783 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import atexit +import functools +import json +import logging +import multiprocessing +import os +import re +import sys +from concurrent.futures import Future, ThreadPoolExecutor +from concurrent.futures.process import BrokenProcessPool +from functools import partial +from time import time, time_ns +from typing import Any, TYPE_CHECKING + +import torch +from torch._dynamo.device_interface import get_registered_device_interfaces +from torch._dynamo.utils import ( + counters, + dynamo_timed, + get_metrics_context, + set_feature_use, +) +from torch._inductor import config +from torch._inductor.codecache import ( + _load_triton_kernel_from_source, + code_hash, + CodeCacheFuture, + CppCodeCache, + CppPythonBindingsCodeCache, + CUDACodeCache, + HalideCodeCache, + LambdaFuture, + ROCmCodeCache, + StaticAutotunerFuture, + torch_key, + XPUCodeCache, +) +from torch._inductor.compile_worker.subproc_pool import ( + AnyPool, + SubprocException, + SubprocPool, +) +from torch._inductor.compile_worker.tracked_process_pool import ( + TrackedProcessPoolExecutor, +) +from torch._inductor.compile_worker.utils import _async_compile_initializer +from torch._inductor.runtime.compile_tasks import ( + _set_triton_libdevice_path, + _set_triton_ptxas_path, + _worker_compile_triton, +) +from torch._inductor.utils import clear_on_fresh_cache +from torch._inductor.virtualized import V +from torch._utils_internal import log_triton_builds +from torch.hub import _Faketqdm, tqdm +from torch.utils._ordered_set import OrderedSet +from torch.utils._triton import has_triton_package + + +if TYPE_CHECKING: + from collections.abc import Callable + + from torch._inductor.runtime.hints import HalideMeta + from torch._inductor.runtime.triton_heuristics import CachingAutotuner + +# timing metrics for time spent in the compilation +_cumulative_compile_time = 0.0 +_t0: float | None = None + +kernel_code_log = torch._logging.getArtifactLogger(__name__, "kernel_code") + +log = logging.getLogger(__name__) + +_triton_kernel_metrics: dict[str, dict[str, Any]] | None = None + +size_hints_regex = re.compile( + r"size_hints=(\{.*?\})", +) + + +def pre_fork_setup(): + """ + Setup that must be done prior to forking with a process pool. + """ + # ensure properties have been calculated before processes + # are forked + caching_device_properties() + + # Computing the triton key can be slow. If we call it before fork, + # it will be cached for the forked subprocesses. + from torch._inductor.runtime.triton_compat import HAS_TRITON, triton_key + + if HAS_TRITON: + triton_key() + + +def caching_device_properties(): + for _, device_interface in get_registered_device_interfaces(): + if device_interface.is_available(): + device_interface.Worker.get_device_properties() + + +def _compile_start() -> None: + global _t0, _triton_kernel_metrics + if _t0 is None: + _t0 = time() + if _triton_kernel_metrics is None: + _triton_kernel_metrics = {} + + +def _compile_end() -> None: + global _cumulative_compile_time, _t0, _triton_kernel_metrics + if _t0 is not None: + t1 = time() + _cumulative_compile_time += t1 - _t0 + _t0 = None + # print("CUMULATIVE COMPILE TIME", _cumulative_compile_time) + if _triton_kernel_metrics: + # Log triton kernel info + sorted_info = dict(sorted(_triton_kernel_metrics.items())) + torch._logging.trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "triton_kernel_info", + "encoding": "json", + }, + payload_fn=lambda: json.dumps(sorted_info), + ) + _triton_kernel_metrics = None + + +def _add_triton_kernel_info(kernel_name: str, info: dict[str, Any]): + global _triton_kernel_metrics + # Must be called between _compile_start and _compile_end + if _triton_kernel_metrics is not None: + _triton_kernel_metrics[kernel_name] = info + + +_IS_WINDOWS = sys.platform == "win32" + +log = logging.getLogger(__name__) + +# Used to keep track of all process pools invoked so far. +_pool_set = OrderedSet[AnyPool]() + + +def shutdown_compile_workers() -> None: + """Shut down all outstanding compile-worker pools.""" + for pool in _pool_set: + pool.shutdown() + AsyncCompile._ready_future = None + after_fork() + + +def after_fork(): + """Reset pools to initial state without shutting them down""" + _pool_set.clear() + AsyncCompile.process_pool.cache_clear() + + +try: + os.register_at_fork(after_in_child=after_fork) +except AttributeError: + pass # register_at_fork does not exists on windows + + +def get_compile_threads() -> int: + """ + Temporary for internal rollout. Assign config.compile_threads lazily and return it. + TODO: remove after rollout. + """ + if config.compile_threads is None: + config.compile_threads = config.decide_compile_threads() + return config.compile_threads + + +@clear_on_fresh_cache +class CompiledTritonKernels: + """ + In memory cache for storing compiled triton kernels. + + Each triton kernel is keyed by the hash of its source code. Each value stored + in the cache is a return value of AsyncCompile.triton(). + + Currently, the cache stores Future objects, but it should be generalizable for any kernels. + """ + + _cache: dict[str, CodeCacheFuture] = {} + + @staticmethod + def key(kernel_src: str): + """ + Generates a cache key given a triton kernel's full source code. + This source includes the inductor meta, compilation metadata, the kernel itself, etc. + `kernel_src` should be the exact string passed to async_compile.triton()'s first argument. + """ + # Hashes the kernel source with torch_key into a single hash key + return code_hash(kernel_src, extra=torch_key()) + + @staticmethod + def save(kernel_src: str, future: CodeCacheFuture): + """ + Saves a compiled triton kernel to the cache. + TODO: We store a LambdaFuture as that's the callable returned by async_compile.triton, + but the real type we want to return here is actually an abstract triton kernel. + + TODO: Source code here is not just the kernel's source code, but also includes the inductor preamble, etc. + so it could be less strict. + """ + key = CompiledTritonKernels.key(kernel_src) + CompiledTritonKernels._cache[key] = future + + @staticmethod + def get(kernel_src: str) -> CodeCacheFuture | None: + key = CompiledTritonKernels.key(kernel_src) + return CompiledTritonKernels._cache.get(key, None) + + @staticmethod + def cache_clear(): + CompiledTritonKernels._cache = {} + + @staticmethod + def remove_future(kernel_src: str) -> None: + key = CompiledTritonKernels.key(kernel_src) + + # Delete the LambdaFuture if there is one + if key in CompiledTritonKernels._cache: + del CompiledTritonKernels._cache[key] + + +class AsyncCompile: + """ + Utilities to compile in thread pools or subprocess pools (in the case of Triton). + """ + + _ready_future: Future[Any] | None = None + _metal_sources: list[tuple[str, str, list[str]]] | None = None + + def __init__(self) -> None: + pass + + @staticmethod + @functools.lru_cache(1) + def pool() -> ThreadPoolExecutor: + assert get_compile_threads() > 1 + return ThreadPoolExecutor(get_compile_threads()) + + @staticmethod + def _get_ready(): + """No-op function to help mark when the subprocess pool is ready.""" + return "ready" + + @staticmethod + @functools.lru_cache(1) + def process_pool() -> AnyPool: + assert get_compile_threads() > 1 + AsyncCompile._ready_future = None + log.info( + "Creating '%s' pool with %d workers", + config.worker_start_method, + get_compile_threads(), + ) + + pool: AnyPool + if config.worker_start_method == "subprocess": + # Wrapper around ProcessPoolExecutor forks in a new process we control + pool = SubprocPool( + get_compile_threads(), quiesce=config.quiesce_async_compile_pool + ) + else: + if config.worker_start_method == "spawn": + # Avoid creating pools in the spawned subprocs themselves: + os.environ["TORCH_WARM_POOL"] = "0" + pre_fork_setup() + ctx = multiprocessing.get_context(config.worker_start_method) + pool = TrackedProcessPoolExecutor( + get_compile_threads(), + mp_context=ctx, + initializer=partial(_async_compile_initializer, os.getpid()), + ) + # when this pool is created in a subprocess object, the normal exit handler + # doesn't run, and we need to register our own handler. + # exitpriority has to be high, because another one of the finalizers will + # kill the worker thread that sends the shutdown message to the workers... + multiprocessing.util.Finalize(None, pool.shutdown, exitpriority=sys.maxsize) + + _pool_set.add(pool) + return pool + + @classmethod + def warm_pool(cls) -> None: + if get_compile_threads() <= 1: + return + _compile_start() + # Pool is created on first access. Note for a SubprocPool, the sidecar process starts, + # but its ProcessPoolExecutor does not initialize until a wakeup() call or the first + # job is submitted. + cls.process_pool() + _compile_end() + + @classmethod + def wait_pool_ready(cls, timeout=120) -> None: + cls.use_process_pool() + if cls._ready_future is not None: + cls._ready_future.result(timeout=timeout) + + @classmethod + def submit(cls, task: Callable[..., Any]) -> Any: + if get_compile_threads() <= 1: + return task() + return cls.pool().submit(task) + + @classmethod + def use_process_pool(cls): + if get_compile_threads() <= 1: + return False + + # Proton instrumentation backend requires compilation to happen in the main + # process so it can instrument the Triton IR during JIT compilation. + # Force synchronous compilation when proton profiling is enabled. + if config.triton.proton_profiling: + return False + + # Create a dummy job to check if the pool is ready. Submit it here instead of at + # pool creation so we don't launch the full pool of worker subprocesses until + # we're sure they're needed. + if not cls._ready_future: + cls._ready_future = cls.process_pool().submit(cls._get_ready) + return cls._ready_future.done() + + @classmethod + def wakeup(cls) -> None: + """ + If using a SubprocPool, signal the sidecar process to start up its + ProcessPoolExecutor. + """ + if not cls.use_process_pool(): + return + pool = cls.process_pool() + if isinstance(pool, SubprocPool): + pool.wakeup() + + def triton(self, kernel_name: str, source_code: str, device_str: str = "cuda"): + """ + Async_compile.triton is more complicated than the other backends because + we're trying to optimize compile time as much as possible for this hot callsite. + + First of all, the function is cached by CompiledTritonKernels; if there's a kernel + already compiled, we grab it directly from the cache and return. + + Otherwise, if we have multiple compile threads, we kick off triton compilations on each + worker process by giving it a kernel and source code to compile. The worker initializes + a CachingAutotuner, runs triton compilation, and pickles the kernel back to us. + We use TritonCompileResult to represent the objects being pickled back to us by each + worker. + + Some maybe not obvious things that are pickled back to us: + - Most of the time, we can avoid sending back CachingAutotuner.fn and other metadata + and do not have to pay the cost of loading the triton kernel on the parent. But certain + cases, like coordesc tuning and dynamic_scale_rblock, require us to reload the function + in the parent lazily when we require it. + - The AutotuneCache, if enabled, is constructed on each worker per triton config + and pickled by to us via `CachingAutotuner.save_cache_hook`. + """ + load_kernel = functools.partial( + _load_triton_kernel_from_source, kernel_name, source_code + ) + + def reload_kernel_in_parent(): + # Benchmark how often this happens + with dynamo_timed("reload_kernel_in_parent"): + return load_kernel() + + counters["inductor"]["async_compile_cache_miss"] += 1 + + kernel_code_log.info("Triton Kernel:\n%s", source_code) + _compile_start() + + if os.environ.get("TRITON_INTERPRET", "0") == "1": + return getattr( + torch._inductor.codecache.PyCodeCache.load(source_code), kernel_name + ) + + is_parallel = self.use_process_pool() + set_feature_use("parallel_compile_post_warmup", is_parallel) + + compile_id = torch._guards.CompileContext.current_compile_id() + is_backward = getattr(V.graph, "is_backward", False) + + if (future := CompiledTritonKernels.get(source_code)) is not None: + counters["inductor"]["async_compile_cache_hit"] += 1 + # Set reload_kernel_from_src properly based on source_code + if isinstance(future, StaticAutotunerFuture): + # Remove the future now that we've cache hit + CompiledTritonKernels.remove_future(source_code) + future.reload_kernel_from_src = reload_kernel_in_parent + if is_parallel: + return future + else: + return future.result() + + # Cache miss + if is_parallel: + # Ensure libdevice path is set in os.environ before passing to workers + _set_triton_libdevice_path() + # We want to support changing these env vars after (and while) the + # process pool is running, so pass them to the subprocess to reset. + env_vars = [ + "TORCHINDUCTOR_CACHE_DIR", + "TRITON_CACHE_DIR", + "TRITON_LIBDEVICE_PATH", + ] + extra_env = {v: os.environ[v] for v in env_vars if v in os.environ} + extra_config = { + "use_static_triton_launcher": torch._inductor.config.use_static_triton_launcher + } + + if len(torch._inductor.config.autotune_lookup_table) > 0: + m = size_hints_regex.search(source_code) + if m: + size_hints_str = m.group(1) + else: + size_hints_str = str(None) + + triton_src = source_code.split("@triton.jit\n")[1] + from torch._inductor.runtime.triton_heuristics import ( + generate_lookup_hash_from_source_code, + ) + + fn_hash = generate_lookup_hash_from_source_code( + size_hints_str, triton_src + ) + + if fn_hash in torch._inductor.config.autotune_lookup_table: + extra_config["autotune_lookup_table"] = { # type: ignore[assignment] + fn_hash: torch._inductor.config.autotune_lookup_table[fn_hash] + } + + task = self.process_pool().submit( + _worker_compile_triton, + load_kernel, + extra_env, + extra_config, + ) + + def get_result() -> CachingAutotuner: + try: + kernel, elapsed_us = task.result() + except SubprocException as e: + raise e.with_name(kernel_name) from e + + # Now that we've compiled, we should clear the future + # so it can't be used again + kernel.set_compile_info(compile_id, is_backward) + CompiledTritonKernels.remove_future(source_code) + + kernel.restore_after_unpickle(old_values=None) + + kernel.precompile( + warm_cache_only=False, + reload_kernel=reload_kernel_in_parent, + static_triton_bundle_key=CompiledTritonKernels.key(source_code), + ) + info = kernel.autotune_cache_info or {} + info["compile_time_us"] = elapsed_us + _add_triton_kernel_info(kernel_name, info) + get_metrics_context().add_top_n( + "triton_kernel_compile_times_us", kernel_name, elapsed_us + ) + return kernel + + future = LambdaFuture(get_result, future=task) + CompiledTritonKernels.save(source_code, future) + return future + else: + with dynamo_timed( + "async_compile.precompile", + log_pt2_compile_event=True, + dynamo_compile_column_us="triton_compile_time_us", + log_waitcounter=True, + waitcounter_name_override="compile_triton", + ): + fail = None + try: + start_ns = time_ns() + _set_triton_ptxas_path() + _set_triton_libdevice_path() + kernel = load_kernel() + kernel.set_compile_info(compile_id, is_backward) + kernel.precompile( + warm_cache_only=False, + static_triton_bundle_key=CompiledTritonKernels.key(source_code), + ) + elapsed_us = (time_ns() - start_ns) // 1000 + get_metrics_context().add_top_n( + "triton_kernel_compile_times_us", kernel_name, elapsed_us + ) + info = kernel.autotune_cache_info or {} + info["compile_time_us"] = elapsed_us + _add_triton_kernel_info(kernel_name, info) + return kernel + except Exception as e: + fail = str(e) + raise + finally: + log_triton_builds(fail=fail) + + def multi_kernel(self, *args, **kwargs) -> Any: + from torch._inductor.codegen.multi_kernel import MultiKernelCall + + # no need to call this in parallel since the sub-kernels are already parallel tasks + return MultiKernelCall(*args, **kwargs) + + def size_hint_multi_kernel(self, *args, **kwargs) -> Any: + from torch._inductor.codegen.multi_kernel import SizeHintMultiKernelCall + + return SizeHintMultiKernelCall(*args, **kwargs) + + def cpp(self, source_code: str): + kernel_code_log.info("CPP Kernel:\n%s", source_code) + if get_compile_threads() <= 1: + return CppCodeCache.load(source_code).kernel + else: + get_result = CppCodeCache.load_async(source_code, submit_fn=self.submit) + return LambdaFuture(lambda: get_result().kernel) + + def cpp_pybinding(self, argtypes: list[str], source_code: str): + kernel_code_log.info("CPP+Bindings Kernel:\n%s", source_code) + if get_compile_threads() <= 1: + return CppPythonBindingsCodeCache.load_pybinding(argtypes, source_code) + else: + get_result = CppPythonBindingsCodeCache.load_pybinding_async( + argtypes, source_code, submit_fn=self.submit + ) + return LambdaFuture(get_result) + + def cutlass(self, cache_cls, source_code, dst_file_ext, aot_compile=False): + def task(): + if aot_compile: + # We rely on JITInductor to compile the CUDA code, + # so that we can load it into AOTInductor. + output_path, *_ = cache_cls.compile(source_code, "o") + cache_cls.aot_kernels_o.append(output_path) + return cache_cls.load(source_code, dst_file_ext)[0] + + return self.submit(task) + + def cuda(self, source_code, dst_file_ext, aot_compile=False): + kernel_code_log.info("CUDA Kernel:\n%s", source_code) + return self.cutlass(CUDACodeCache, source_code, dst_file_ext, aot_compile) + + def xpu(self, source_code, dst_file_ext, aot_compile=False): + kernel_code_log.info("XPU Kernel:\n%s", source_code) + return self.cutlass(XPUCodeCache, source_code, dst_file_ext, aot_compile) + + def rocm( + self, + source_code, + dst_file_ext, + aot_compile=False, + ): + kernel_code_log.info("ROCm Kernel:\n%s", source_code) + + def task(): + if aot_compile: + output_path, *_ = ROCmCodeCache.compile(source_code, dst_file_ext="o") + ROCmCodeCache.aot_kernels_o.append(output_path) + if config.rocm.generate_test_runner: + _ = ROCmCodeCache.compile(source_code, dst_file_ext="exe") + return ROCmCodeCache.load(source_code, dst_file_ext)[0] + + return self.submit(task) + + def halide(self, meta: HalideMeta, source_code: str): + kernel_code_log.info("Halide Kernel:\n%r\n%s", meta, source_code) + if get_compile_threads() <= 1: + return HalideCodeCache.generate_halide(meta, source_code) + else: + get_result = HalideCodeCache.generate_halide_async( + meta, source_code, submit_fn=self.submit + ) + return LambdaFuture(get_result) + + def cutedsl(self, kernel_name: str, source_code: str): + """ + Compile CuteDSL (CUTLASS Python DSL) kernels. + + Args: + kernel_name: Name of the kernel to be defined + source_code: Source code of the CuteDSL kernel, as a string + + Note: + CuteDSL currently requires source files to do its compilation, there we + use the PyCodeCache to write the source code to a file and load it. + """ + from torch._inductor.codegen.cutedsl.cutedsl_kernel import ( + CuteDSLKernelWrapper, + MAIN_SUFFIX, + ) + + kernel_code_log.info("CuteDSL Kernel:\n%s", source_code) + + def task(): + key, path = torch._inductor.codecache.PyCodeCache.write(source_code) + mod = torch._inductor.codecache.PyCodeCache.load_by_key_path(key, path) + + # Find our special entry point named function + main_func_name = f"{kernel_name}_{MAIN_SUFFIX}" + if not hasattr(mod, main_func_name): + available = [name for name in dir(mod) if callable(getattr(mod, name))] + raise RuntimeError( + f"Could not find CuteDSL main kernel function '{main_func_name}'. Available callables: {available}" + ) + + return CuteDSLKernelWrapper(getattr(mod, main_func_name), kernel_path=path) + + if get_compile_threads() <= 1: + return task() + else: + future = self.submit(task) + return LambdaFuture(lambda: future.result()) + + def pallas(self, kernel_name: str, source_code: str): + """ + Compile Pallas (JAX experimental) kernels. + + Args: + kernel_name: Name of the kernel to be defined + source_code: Source code of the Pallas kernel, as a string + + Note: + Pallas kernels are Python code that uses JAX and Pallas APIs. + We use the PyCodeCache to write the source code to a file and load it. + """ + from torch._inductor.codegen.pallas import MAIN_SUFFIX, PallasKernelWrapper + + kernel_code_log.info("Pallas Kernel:\n%s", source_code) + + def task(): + key, path = torch._inductor.codecache.PyCodeCache.write(source_code) + mod = torch._inductor.codecache.PyCodeCache.load_by_key_path(key, path) + + # Find our special entry point named function + main_func_name = f"{kernel_name}_{MAIN_SUFFIX}" + if not hasattr(mod, main_func_name): + available = [name for name in dir(mod) if callable(getattr(mod, name))] + raise RuntimeError( + f"Could not find Pallas main kernel function '{main_func_name}'. Available callables: {available}" + ) + + return PallasKernelWrapper(getattr(mod, main_func_name), kernel_path=path) + + if get_compile_threads() <= 1: + return task() + else: + future = self.submit(task) + return LambdaFuture(lambda: future.result()) + + def nv_universal_gemm(self, kernel_name: str, source_code: str): + """ + Compile NVIDIA Universal GEMM kernels. + + Args: + kernel_name: Name of the kernel to be defined + source_code: Source code of the kernel, as a string + + Note: + NVIDIA Universal GEMM kernels are Python code that calls the cutlass_api library. + We use the PyCodeCache to write the source code to a file and load it. + """ + from torch._inductor.codegen.nv_universal_gemm.nv_universal_gemm_kernel import ( + NVUniversalGemmKernelWrapper, + ) + from torch._inductor.codegen.nv_universal_gemm.nv_universal_gemm_scheduling import ( + MAIN_SUFFIX, + ) + + kernel_code_log.info("NVIDIA Universal GEMM Kernel:\n%s", source_code) + + def task(): + key, path = torch._inductor.codecache.PyCodeCache.write(source_code) + mod = torch._inductor.codecache.PyCodeCache.load_by_key_path(key, path) + + # Find our special entry point named function + main_func_name = f"{kernel_name}_{MAIN_SUFFIX}" + if not hasattr(mod, main_func_name): + available = [name for name in dir(mod) if callable(getattr(mod, name))] + raise RuntimeError( + f"Could not find NVIDIA Universal GEMM main kernel function " + f"'{main_func_name}'. Available callables: {available}" + ) + + return NVUniversalGemmKernelWrapper( + getattr(mod, main_func_name), kernel_path=path + ) + + if get_compile_threads() <= 1: + return task() + else: + future = self.submit(task) + return LambdaFuture(lambda: future.result()) + + def metal(self, kernel_name: str, source: str, headers: list[str]) -> None: + """Register a Metal kernel body; wait() compiles all registered kernels into one library.""" + if self._metal_sources is None: + self._metal_sources = [] + self._metal_sources.append((kernel_name, source, headers)) + + def wait(self, scope: dict[str, Any]) -> None: + if get_compile_threads() > 1: + with dynamo_timed( + "async_compile.wait", + log_pt2_compile_event=True, + dynamo_compile_column_us="triton_compile_time_us", + log_waitcounter=True, + waitcounter_name_override="compile_triton", + ): + self._wait_futures(scope) + + if self._metal_sources: + from torch._inductor.runtime.runtime_utils import compile_mps_shaders + + scope.update(compile_mps_shaders(self._metal_sources)) + self._metal_sources.clear() + + _compile_end() + + def _wait_futures(self, scope: dict[str, Any]) -> None: + kernels = { + key: value + for key, value in scope.items() + if isinstance(value, (Future, CodeCacheFuture)) + } + pbar = tqdm( + total=len(kernels), + desc="Inductor Compilation", + disable=config.disable_progress, + delay=0, + ) + for key, result in kernels.items(): + if config.verbose_progress and not isinstance(pbar, _Faketqdm): + pbar.set_postfix_str(key) + try: + kernel = result.result() + scope[key] = kernel + except BrokenProcessPool as e: + raise RuntimeError( + "A compilation subprocess exited unexpectedly. This " + "is likely due to a crash. To facilitate debugging, " + "you can re-run with TORCHINDUCTOR_COMPILE_THREADS=1 " + "to cause compilation to occur in the main process." + ) from e + pbar.update(1) + + +def maybe_warm_pool() -> None: + if ( + os.environ.get("TORCH_TNT_IN_USE", "0") == "1" + or os.environ.get("TORCH_WARM_POOL", "1") != "1" + # The subprocess pool is only used for the Triton backend + or not has_triton_package() + # Skip for fbcode. We have internal reports of usages inside multiprocessing + # pools that lead a multiplicative number of compile subprocesses. + or config.is_fbcode() + ): + return + + AsyncCompile.warm_pool() + # TODO: This starts the SubprocPool's internal process pool as early as possible at + # the expense of creating a bunch of worker processes that might not be needed. We + # could start them lazily if we're willing to lose a small amount of compile time. + AsyncCompile.wakeup() + + +# On exit give the workers a chance to clean themselves up. Without this the +# resource_tracker can complain about leaked semaphores coming from the +# ProcessPoolExecutor: +# UserWarning: resource_tracker: There appear to be 5 leaked semaphore objects +# to clean up at shutdown +atexit.register(shutdown_compile_workers) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/augmented_graph_helper.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/augmented_graph_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..e6ff7cbe01e41899d9a5f72f7d033413fae059b4 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/augmented_graph_helper.py @@ -0,0 +1,276 @@ +import logging +from collections import defaultdict + +import torch +import torch.fx as fx +from torch._logging import trace_structured +from torch.utils._ordered_set import OrderedSet + + +log = logging.getLogger(__name__) + + +class AugmentedGraphHelper: + """ + Graph helper that augments the original graph with additional + dependencies and uses, plus tracks node equivalences for coalescing. + + TODO: if this becomes too large of compile time, consider binding + graphcycles.cc + """ + + def __init__( + self, + graph: fx.Graph, + node_ancestors: dict[fx.Node, OrderedSet[fx.Node]] | None = None, + ): + # Each node starts in its own singleton set + self.graph = graph + self.merge_sets = {node: OrderedSet([node]) for node in graph.nodes} + + # Extra dependencies: node depends on dep (dep must come before node) + self.extra_deps: dict[fx.Node, OrderedSet[fx.Node]] = defaultdict(OrderedSet) + # Extra uses: reverse of extra_deps (node is used by user) + self.extra_uses: dict[fx.Node, OrderedSet[fx.Node]] = defaultdict(OrderedSet) + # Note: only reflect original ancestors, not maintained through additional deps + # or merge sets + self.node_ancestors = node_ancestors + + def add_extra_dep(self, *, n: fx.Node, dep: fx.Node) -> None: + """Add extra dependency: node depends on dep.""" + self.extra_deps[n].add(dep) + self.extra_uses[dep].add(n) + + def remove_extra_dep(self, *, n: fx.Node, dep: fx.Node) -> None: + if dep in self.extra_deps[n]: + self.extra_deps[n].discard(dep) + self.extra_uses[dep].discard(n) + + def merge_to_set(self, existing_node: fx.Node, new_node: fx.Node) -> None: + """ + Merge new_node into existing_node's set. The new node must be a singleton set. + """ + existing_set = self.merge_sets[existing_node] + new_set = self.merge_sets[new_node] + assert len(new_set) == 1 + + # Add all nodes from new_set to existing_set + existing_set.update(new_set) + + # Update all nodes from new_set to point to existing_set + for node in new_set: + self.merge_sets[node] = existing_set + + def unmerge_node(self, node: fx.Node) -> None: + """Remove a node from its merge set, making it singleton.""" + old_set = self.merge_sets[node] + + # If already singleton, nothing to do + if len(old_set) == 1: + return + + # Remove from old set + old_set.remove(node) + + # Make node singleton + self.merge_sets[node] = OrderedSet([node]) + + def get_merged_deps(self, node: fx.Node) -> OrderedSet[fx.Node]: + """ + Get all dependencies of a node considering merges and extra deps. + Combines: + 1. Direct deps (all_input_nodes) of node and its merge equivalents + 2. Extra deps of node and its merge equivalents + """ + deps: OrderedSet[fx.Node] = OrderedSet() + + # For each node in the merge set + for merged_node in self.merge_sets[node]: + # Add direct dependencies from all_input_nodes + deps.update(merged_node.all_input_nodes) + # Add extra dependencies + deps.update(self.extra_deps[merged_node]) + + return deps + + def has_cycle(self) -> bool: + return torch._dynamo.graph_deduplication._has_cycle( + self.graph, self.get_all_extra_deps() + ) + + def _get_all_ancestors(self, node: fx.Node) -> OrderedSet[fx.Node]: + """Transitive ancestors through both data deps and extra deps.""" + ancestors: OrderedSet[fx.Node] = OrderedSet() + stack: list[fx.Node] = list(node.all_input_nodes) + stack.extend(self.extra_deps.get(node, ())) + while stack: + n = stack.pop() + if n not in ancestors: + ancestors.add(n) + stack.extend(n.all_input_nodes) + stack.extend(self.extra_deps.get(n, ())) + return ancestors + + def has_path(self, source: fx.Node, target: fx.Node) -> bool: + """Check if there's a path from source to target.""" + # we should not be checking path from node to itself + assert self.merge_sets[source] is not self.merge_sets[target] + + # search backwards from target to source + visited: OrderedSet[fx.Node] = OrderedSet() + queue = [target] + visited.add(target) + + while queue: + current = queue.pop() + + for dep in self.get_merged_deps(current): + # Check if we reached source or its equivalent + if dep in self.merge_sets[source]: + return True + + if dep in visited: + continue + + # We are searching from target, so this node is necessarily an ancestor + # of target. + # If dep is an ancestor of source, any path through dep to source would imply a cycle + if self.node_ancestors: + source_set = self.merge_sets[source] + is_ancestor_of_source = any( + dep in self.node_ancestors[s] for s in source_set + ) + # Add to visited to avoid recomputing this check if we see dep again + if is_ancestor_of_source: + visited.add(dep) + continue + + visited.add(dep) + queue.append(dep) + + return False + + def transfer_erased_node_deps( + self, erased_to_new: dict[fx.Node, fx.Node | None] + ) -> None: + """ + Transfer all extra dependencies from erased nodes to their replacements, handling + cross-dependencies between erased nodes correctly. + + Skips deps where both endpoints resolve to replacement nodes from the + same erasure batch — these are intra-bucket deps that would create + cycles (e.g. new_start <-> new_wait within the same bucket). + """ + erased_merge_sets: dict[fx.Node, fx.Node | None] = {} + + for replaced, new in erased_to_new.items(): + for equiv in self.merge_sets[replaced]: + erased_merge_sets[equiv] = new + + # Transfer dependencies + for old_node, new_node in erased_merge_sets.items(): + if new_node is None: + # Clean up references to removed node + for extra_use in list(self.extra_uses[old_node]): + updated_use = erased_merge_sets.get(extra_use, extra_use) + if updated_use is not None: + self.extra_deps[updated_use].discard(old_node) + for extra_dep in list(self.extra_deps[old_node]): + updated_dep = erased_merge_sets.get(extra_dep, extra_dep) + if updated_dep is not None: + self.extra_uses[updated_dep].discard(old_node) + else: + # Transfer dependencies FROM old_node (what old_node depended on) + for extra_dep in self.extra_deps[old_node]: + updated_dep = erased_merge_sets.get(extra_dep, extra_dep) + if updated_dep is not None and updated_dep != new_node: + # Skip if reverse dep already exists (extra or data) + if new_node in self.extra_deps.get( + updated_dep, () + ) or new_node in OrderedSet(updated_dep.all_input_nodes): + continue + self.extra_deps[new_node].add(updated_dep) + self.extra_uses[updated_dep].discard(old_node) + self.extra_uses[updated_dep].add(new_node) + + # Transfer dependencies TO old_node (what depended on old_node) + for extra_use in self.extra_uses[old_node]: + updated_use = erased_merge_sets.get(extra_use, extra_use) + if updated_use is not None and updated_use != new_node: + # Skip if reverse dep already exists (extra or data) + if updated_use in self.extra_deps.get( + new_node, () + ) or updated_use in OrderedSet(new_node.all_input_nodes): + continue + self.extra_deps[updated_use].discard(old_node) + self.extra_deps[updated_use].add(new_node) + self.extra_uses[new_node].add(updated_use) + + # Clean up erased nodes + for old_node in erased_merge_sets: + self.extra_deps[old_node].clear() + self.extra_uses[old_node].clear() + del self.merge_sets[old_node] + + def remove_erased_extra_deps(self) -> None: + """Remove extra deps referencing erased nodes.""" + for node in list(self.extra_deps): + if node._erased: + for dep in list(self.extra_deps[node]): + self.remove_extra_dep(n=node, dep=dep) + continue + for dep in list(self.extra_deps[node]): + if dep._erased: + self.remove_extra_dep(n=node, dep=dep) + + def check_and_maybe_autofix_cyclic_extra_deps( + self, *, autofix: bool = False + ) -> None: + """Check for and optionally remove extra deps that create cycles. + + Args: + autofix: If True, silently remove cyclic deps. If False (default), + raise an error so the root cause gets investigated. + """ + if not self.has_cycle(): + return + removed = [] + for node in list(self.extra_deps): + for dep in list(self.extra_deps[node]): + ancestors = self._get_all_ancestors(dep) + if node in ancestors: + removed.append((node.name, dep.name)) + self.remove_extra_dep(n=node, dep=dep) + if not removed: + return + msg = ( + f"Overlap scheduling: detected {len(removed)} cyclic extra " + f"dep(s): {removed}. Please report this to the overlap " + f"scheduling developers." + ) + log.warning(msg) + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "inductor_overlap_cyclic_extra_deps", + "encoding": "string", + }, + payload_fn=lambda: msg, + ) + if not autofix: + raise RuntimeError( + f"{msg}\nTo unblock, set " + f"torch._inductor.config.aten_distributed_optimizations" + f".overlap_scheduling_autofix_cycles = True" + ) + + def get_all_extra_deps(self) -> dict[fx.Node, OrderedSet[fx.Node]]: + """ + Get all extra dependencies in a format suitable for topological sort. + Returns a copy to avoid external modifications. + """ + return { + node: OrderedSet(deps) + for node, deps in self.extra_deps.items() + if deps # Only include nodes with non-empty deps + } diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/artifacts/_MMRankingA100.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/artifacts/_MMRankingA100.py new file mode 100644 index 0000000000000000000000000000000000000000..c87561d6eb3e40874dc10a2928c22b6aa8636345 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/artifacts/_MMRankingA100.py @@ -0,0 +1,296 @@ +# flake8: noqa: B950 +# fmt: off +# This file was generated by AutoHeuristic. Do not modify it manually! +# To regenerate this file, take a look at the steps in the README.md file inside torchgen/_autoheuristic/mm/ +from typing import List, Optional, Tuple + +from torch._inductor.autoheuristic.autoheuristic_utils import ( + AHContext, + AHMetadata, + Choice, +) +from torch._inductor.autoheuristic.learnedheuristic_interface import ( + LearnedHeuristicDecision, +) + + +class MMRankingA100(LearnedHeuristicDecision): + + def __init__(self) -> None: + self.choices: list[Choice] = [] + self.fill_choices() + + def check_precondition(self, metadata: AHMetadata, context: AHContext,) -> bool: + return ( + metadata.name == self.get_name() + and metadata.shared_memory == 166912 + and str(metadata.device_capa) == "(8, 0)" + ) + + def get_confidence_threshold(self) -> float: + return 0.0 + + def get_choice(self, idx: int) -> str | None: + if idx < len(self.choices): + return self.choices[idx] + return None + + def fill_choices(self) -> None: + self.choices.append('extern_mm') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=128_BLOCK-N=16_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=128_BLOCK-N=32_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=128_BLOCK-N=64_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=128_numstages=2_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=128_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=128_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=128_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=16_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=16_numstages=2_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=16_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=16_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=16_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=16_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=16_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=16_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=32_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=32_numstages=2_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=32_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=32_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=32_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=32_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=32_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=64_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=64_numstages=2_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=64_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=64_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=64_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=64_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=128_numstages=2_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=128_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=128_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=128_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=16_numstages=2_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=16_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=16_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=16_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=16_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=16_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=16_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=32_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=32_numstages=2_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=32_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=32_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=32_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=32_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=32_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=64_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=64_numstages=2_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=64_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=64_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=64_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=64_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=128_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=128_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=128_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=16_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=16_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=16_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=16_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=16_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=32_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=32_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=32_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=32_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=64_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=64_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=64_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=128_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=128_BLOCK-N=32_numstages=5_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=128_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=128_BLOCK-N=64_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=128_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=128_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=128_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=128_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=16_numstages=5_numwarps=1') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=32_numstages=1_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=32_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=32_numstages=3_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=32_numstages=4_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=32_numstages=5_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=64_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=64_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=32_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=32_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=32_BLOCK-N=16_numstages=5_numwarps=1') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=32_BLOCK-N=32_numstages=5_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=32_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=32_BLOCK-N=64_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=32_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=64_BLOCK-N=128_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=64_BLOCK-N=128_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=64_BLOCK-N=32_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=64_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=64_BLOCK-N=64_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=64_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=128_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=128_BLOCK-N=16_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=128_BLOCK-N=32_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=128_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=128_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=128_BLOCK-N=64_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=128_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=16_BLOCK-N=16_numstages=1_numwarps=2') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=16_BLOCK-N=16_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=16_BLOCK-N=16_numstages=5_numwarps=2') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=16_BLOCK-N=32_numstages=1_numwarps=2') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=16_BLOCK-N=32_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=16_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=16_BLOCK-N=64_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=32_BLOCK-N=16_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=32_BLOCK-N=16_numstages=5_numwarps=2') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=32_BLOCK-N=64_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=64_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=64_BLOCK-N=128_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=64_BLOCK-N=32_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=16_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=16_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=16_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=32_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=32_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=64_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=64_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=128_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=16_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=16_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=16_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=16_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=32_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=32_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=32_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=32_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=32_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=32_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=64_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=64_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=64_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=64_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=128_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=16_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=16_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=16_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=16_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=32_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=32_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=32_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=32_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=32_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=32_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=64_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=64_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=64_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=64_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=16_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=16_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=16_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=32_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=32_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=32_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=64_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=64_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=64_numstages=5_numwarps=4') + + def get_name(self) -> str: + return 'mm' + + def get_best_choices(self, context: AHContext) -> list[tuple[float, int]] | None: + if context.get_value('arith_intensity') <= 52.6245059967041: + if context.get_value('n') <= 34.0: + if context.get_value('n') <= 18.0: + if context.get_value('k*n') <= 312.0: + return [(0.093, 12), (0.081, 16), (0.081, 148), (0.070, 10), (0.070, 17), (0.070, 149), (0.070, 151), (0.070, 150), (0.070, 14), (0.058, 11), (0.058, 15), (0.058, 13), (0.058, 122), (0.047, 121), (0.035, 123), (0.012, 92)] + else: + if context.get_value('k') <= 40.0: + return [(0.083, 42), (0.083, 46), (0.083, 44), (0.083, 40), (0.083, 128), (0.067, 45), (0.067, 43), (0.067, 41), (0.067, 169), (0.067, 171), (0.067, 168), (0.067, 129), (0.067, 170), (0.033, 103), (0.017, 121)] + else: + return [(0.112, 137), (0.104, 136), (0.101, 0), (0.081, 1), (0.073, 135), (0.069, 67), (0.066, 187), (0.058, 41), (0.050, 71), (0.046, 68), (0.046, 70), (0.031, 44), (0.027, 43), (0.027, 170), (0.019, 189), (0.019, 188), (0.015, 169), (0.015, 171), (0.012, 115), (0.012, 168), (0.012, 69), (0.004, 103)] + else: + if context.get_value('mat1_stride_0') <= 20.0: + return [(0.069, 0), (0.059, 157), (0.059, 22), (0.059, 153), (0.059, 155), (0.059, 25), (0.059, 23), (0.059, 19), (0.044, 21), (0.044, 18), (0.044, 152), (0.044, 158), (0.044, 154), (0.044, 156), (0.044, 20), (0.044, 124), (0.044, 24), (0.030, 125), (0.029, 126), (0.015, 97), (0.015, 95), (0.015, 96), (0.010, 2), (0.010, 75)] + else: + if context.get_value('k') <= 68.0: + return [(0.087, 72), (0.087, 74), (0.087, 73), (0.086, 76), (0.077, 75), (0.067, 192), (0.058, 190), (0.048, 47), (0.048, 193), (0.048, 49), (0.048, 51), (0.048, 191), (0.038, 53), (0.019, 133), (0.019, 50), (0.019, 175), (0.019, 172), (0.019, 48), (0.019, 174), (0.010, 173), (0.010, 177), (0.010, 52), (0.010, 54), (0.010, 178), (0.010, 176)] + else: + return [(0.154, 52), (0.154, 72), (0.102, 75), (0.087, 49), (0.087, 73), (0.086, 51), (0.057, 176), (0.045, 2), (0.038, 191), (0.038, 178), (0.038, 190), (0.029, 173), (0.029, 76), (0.026, 138), (0.013, 139), (0.013, 140), (0.003, 0)] + else: + if context.get_value('k') <= 35.0: + if context.get_value('k') <= 18.0: + if context.get_value('m*n') <= 19505152.0: + return [(0.151, 159), (0.140, 160), (0.129, 164), (0.055, 127), (0.051, 29), (0.044, 161), (0.044, 147), (0.040, 146), (0.040, 31), (0.037, 145), (0.026, 28), (0.022, 90), (0.022, 93), (0.022, 94), (0.022, 100), (0.022, 125), (0.022, 158), (0.022, 157), (0.011, 87), (0.011, 88), (0.011, 89), (0.011, 91), (0.011, 95), (0.011, 96), (0.011, 98), (0.011, 99)] + else: + return [(0.069, 7), (0.069, 5), (0.067, 147), (0.066, 8), (0.061, 145), (0.058, 146), (0.052, 124), (0.049, 29), (0.049, 159), (0.046, 31), (0.043, 157), (0.041, 9), (0.041, 4), (0.040, 6), (0.035, 164), (0.035, 160), (0.026, 158), (0.017, 125), (0.017, 28), (0.017, 32), (0.017, 162), (0.017, 27), (0.017, 30), (0.017, 161), (0.009, 33), (0.009, 26), (0.009, 163), (0.006, 0)] + else: + if context.get_value('n') <= 68.0: + return [(0.101, 182), (0.101, 59), (0.088, 57), (0.076, 184), (0.076, 61), (0.076, 179), (0.076, 62), (0.076, 58), (0.063, 180), (0.063, 60), (0.051, 56), (0.050, 181), (0.025, 130), (0.025, 177), (0.025, 183), (0.013, 178), (0.013, 55)] + else: + return [(0.089, 180), (0.079, 60), (0.066, 35), (0.066, 181), (0.066, 38), (0.066, 58), (0.066, 179), (0.066, 57), (0.062, 184), (0.053, 37), (0.044, 166), (0.040, 55), (0.040, 39), (0.040, 36), (0.040, 165), (0.040, 167), (0.027, 177), (0.027, 34), (0.022, 159)] + else: + if context.get_value('m*n') <= 309760.0: + return [(0.298, 0), (0.097, 140), (0.080, 83), (0.072, 86), (0.044, 84), (0.036, 178), (0.036, 117), (0.036, 82), (0.032, 120), (0.032, 85), (0.028, 119), (0.024, 130), (0.024, 109), (0.020, 108), (0.020, 118), (0.012, 104), (0.012, 116), (0.012, 141), (0.012, 144), (0.008, 105), (0.008, 106), (0.008, 111), (0.008, 114), (0.008, 107), (0.008, 132), (0.004, 101), (0.004, 102), (0.004, 110), (0.004, 112), (0.004, 113), (0.004, 131)] + else: + if context.get_value('n') <= 72.0: + return [(0.227, 77), (0.118, 78), (0.102, 194), (0.086, 80), (0.059, 57), (0.054, 81), (0.049, 196), (0.048, 197), (0.048, 59), (0.043, 79), (0.032, 195), (0.027, 180), (0.022, 3), (0.021, 141), (0.016, 60), (0.016, 142), (0.011, 183), (0.011, 0), (0.011, 144)] + else: + return [(0.140, 186), (0.132, 185), (0.109, 63), (0.085, 65), (0.078, 37), (0.077, 35), (0.062, 197), (0.047, 194), (0.046, 165), (0.046, 57), (0.039, 78), (0.039, 79), (0.039, 66), (0.039, 64), (0.016, 195), (0.008, 159)] + else: + if str(context.get_value('using_tf32')) != 'False': + if context.get_value('m*n') <= 815360.0: + if context.get_value('k') <= 1184.0: + return [(0.218, 140), (0.205, 0), (0.154, 144), (0.115, 141), (0.051, 185), (0.051, 104), (0.039, 78), (0.038, 116), (0.026, 165), (0.026, 130), (0.026, 178), (0.013, 57), (0.013, 195), (0.013, 167), (0.013, 186)] + else: + return [(0.901, 0), (0.030, 144), (0.030, 134), (0.016, 3), (0.006, 78), (0.006, 77), (0.002, 57), (0.002, 194), (0.002, 59), (0.002, 60), (0.002, 143)] + else: + if context.get_value('arith_intensity') <= 187.23922729492188: + if context.get_value('mat1_stride_0') <= 198.0: + return [(0.273, 63), (0.158, 37), (0.152, 35), (0.127, 57), (0.097, 165), (0.053, 185), (0.031, 0), (0.028, 64), (0.014, 60), (0.014, 78), (0.009, 55), (0.008, 134), (0.005, 34), (0.005, 167), (0.005, 179), (0.005, 65), (0.005, 66), (0.005, 186), (0.005, 194), (0.002, 166)] + else: + return [(0.296, 63), (0.235, 0), (0.132, 64), (0.074, 37), (0.069, 78), (0.051, 185), (0.051, 35), (0.030, 57), (0.020, 77), (0.016, 194), (0.008, 66), (0.007, 65), (0.003, 3), (0.003, 165), (0.003, 141), (0.001, 134), (0.001, 166)] + else: + return [(0.405, 0), (0.246, 37), (0.177, 63), (0.145, 35), (0.005, 185), (0.005, 65), (0.005, 64), (0.004, 57), (0.003, 66), (0.002, 165), (0.001, 78), (0.001, 55)] + else: + return [(0.357, 0), (0.112, 165), (0.101, 57), (0.094, 179), (0.086, 64), (0.074, 167), (0.067, 60), (0.064, 159), (0.033, 35), (0.007, 195), (0.002, 180), (0.001, 34), (0.001, 166), (0.001, 78)] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/artifacts/_MMRankingH100.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/artifacts/_MMRankingH100.py new file mode 100644 index 0000000000000000000000000000000000000000..74812d9f0b404bbb5d7040d298e2bd597f9bba8e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/artifacts/_MMRankingH100.py @@ -0,0 +1,321 @@ +# flake8: noqa: B950 +# fmt: off +# This file was generated by AutoHeuristic. Do not modify it manually! +# To regenerate this file, take a look at the steps in the README.md file inside torchgen/_autoheuristic/mm/ +from typing import List, Optional, Tuple + +from torch._inductor.autoheuristic.autoheuristic_utils import ( + AHContext, + AHMetadata, + Choice, +) +from torch._inductor.autoheuristic.learnedheuristic_interface import ( + LearnedHeuristicDecision, +) + + +class MMRankingH100(LearnedHeuristicDecision): + + def __init__(self) -> None: + self.choices: list[Choice] = [] + self.fill_choices() + + def check_precondition(self, metadata: AHMetadata, context: AHContext,) -> bool: + return ( + metadata.name == self.get_name() + and metadata.shared_memory == 232448 + and str(metadata.device_capa) == "(9, 0)" + ) + + def get_confidence_threshold(self) -> float: + return 0.0 + + def get_choice(self, idx: int) -> str | None: + if idx < len(self.choices): + return self.choices[idx] + return None + + def fill_choices(self) -> None: + self.choices.append('extern_mm') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=128_BLOCK-N=16_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=128_BLOCK-N=32_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=128_BLOCK-N=64_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=128_numstages=2_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=128_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=128_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=128_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=16_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=16_numstages=2_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=16_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=16_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=16_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=16_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=16_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=16_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=32_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=32_numstages=2_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=32_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=32_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=32_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=64_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=64_numstages=2_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=64_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=64_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=64_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=16_BLOCK-N=64_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=128_numstages=2_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=128_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=128_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=128_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=16_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=16_numstages=2_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=16_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=16_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=16_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=16_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=16_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=16_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=32_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=32_numstages=2_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=32_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=32_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=32_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=32_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=32_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=64_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=64_numstages=2_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=64_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=64_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=64_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=64_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=128_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=128_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=128_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=16_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=16_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=16_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=16_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=16_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=32_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=32_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=32_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=32_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=64_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=64_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=64_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=128_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=128_BLOCK-N=32_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=128_BLOCK-N=32_numstages=5_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=128_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=128_BLOCK-N=64_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=128_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=128_numstages=2_numwarps=8') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=128_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=128_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=128_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=128_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=16_numstages=3_numwarps=1') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=16_numstages=4_numwarps=1') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=16_numstages=5_numwarps=1') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=32_numstages=1_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=32_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=32_numstages=3_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=32_numstages=4_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=32_numstages=5_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=64_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=64_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=64_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=16_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=32_BLOCK-N=128_numstages=2_numwarps=8') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=32_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=32_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=32_BLOCK-N=128_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=32_BLOCK-N=16_numstages=4_numwarps=1') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=32_BLOCK-N=16_numstages=5_numwarps=1') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=32_BLOCK-N=32_numstages=4_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=32_BLOCK-N=32_numstages=5_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=32_BLOCK-N=64_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=32_BLOCK-N=64_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=32_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=64_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=64_BLOCK-N=128_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=64_BLOCK-N=128_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=64_BLOCK-N=128_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=64_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=128_BLOCK-N=16_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=128_BLOCK-N=32_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=128_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=128_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=16_BLOCK-N=16_numstages=1_numwarps=2') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=16_BLOCK-N=16_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=16_BLOCK-N=16_numstages=5_numwarps=2') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=16_BLOCK-N=32_numstages=1_numwarps=2') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=16_BLOCK-N=32_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=16_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=16_BLOCK-N=64_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=32_BLOCK-N=16_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=32_BLOCK-N=16_numstages=5_numwarps=2') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=32_BLOCK-N=32_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=32_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=32_BLOCK-N=64_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=64_BLOCK-N=16_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=64_BLOCK-N=32_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=16_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=16_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=16_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=32_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=32_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=64_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=128_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=16_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=16_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=16_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=16_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=32_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=32_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=32_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=32_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=64_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=64_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=64_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=64_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=16_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=128_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=16_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=16_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=16_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=16_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=32_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=32_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=32_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=32_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=32_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=32_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=64_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=64_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=64_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=64_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=16_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=16_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=16_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=32_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=32_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=32_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=64_numstages=3_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=64_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=64_numstages=5_numwarps=4') + + def get_name(self) -> str: + return 'mm' + + def get_best_choices(self, context: AHContext) -> list[tuple[float, int]] | None: + if context.get_value('arith_intensity') <= 29.89772129058838: + if context.get_value('n') <= 34.0: + if context.get_value('n') <= 18.0: + if context.get_value('k*n') <= 432.0: + if context.get_value('arith_intensity') <= 7.8700292110443115: + return [(0.098, 128), (0.098, 129), (0.098, 127), (0.073, 14), (0.073, 16), (0.073, 12), (0.073, 154), (0.073, 156), (0.073, 157), (0.073, 155), (0.049, 10), (0.049, 94), (0.049, 95), (0.048, 96)] + else: + return [(0.091, 154), (0.073, 10), (0.073, 15), (0.073, 13), (0.073, 11), (0.073, 17), (0.073, 16), (0.073, 14), (0.073, 12), (0.055, 127), (0.054, 157), (0.054, 156), (0.054, 155), (0.036, 129), (0.036, 128), (0.018, 41), (0.018, 43)] + else: + if context.get_value('k') <= 40.0: + return [(0.070, 39), (0.069, 45), (0.069, 41), (0.069, 43), (0.069, 111), (0.069, 112), (0.056, 38), (0.056, 40), (0.056, 42), (0.056, 44), (0.056, 174), (0.056, 173), (0.056, 175), (0.056, 134), (0.056, 172), (0.056, 135), (0.014, 154), (0.014, 127)] + else: + return [(0.147, 144), (0.119, 143), (0.087, 142), (0.083, 0), (0.073, 191), (0.059, 69), (0.050, 67), (0.046, 70), (0.041, 1), (0.036, 174), (0.032, 43), (0.032, 123), (0.028, 40), (0.027, 42), (0.027, 173), (0.023, 175), (0.018, 66), (0.014, 192), (0.014, 193), (0.014, 139), (0.014, 68), (0.014, 127)] + else: + if context.get_value('mat1_stride_0') <= 40.0: + if context.get_value('mat1_stride_0') <= 20.0: + return [(0.109, 23), (0.109, 21), (0.109, 20), (0.088, 0), (0.087, 131), (0.066, 18), (0.065, 130), (0.065, 132), (0.065, 159), (0.065, 160), (0.065, 161), (0.065, 158), (0.022, 22), (0.022, 19)] + else: + return [(0.065, 46), (0.064, 52), (0.064, 50), (0.064, 48), (0.064, 51), (0.064, 49), (0.064, 47), (0.064, 53), (0.064, 181), (0.064, 177), (0.064, 179), (0.064, 176), (0.038, 130), (0.038, 136), (0.026, 182), (0.026, 178), (0.026, 180), (0.026, 137), (0.025, 158), (0.013, 114), (0.013, 113)] + else: + if context.get_value('mat1_stride_0') <= 68.0: + return [(0.138, 140), (0.125, 195), (0.100, 71), (0.100, 74), (0.100, 196), (0.100, 194), (0.100, 197), (0.075, 75), (0.062, 72), (0.062, 73), (0.012, 180), (0.012, 51), (0.012, 182)] + else: + return [(0.124, 180), (0.124, 182), (0.114, 75), (0.103, 74), (0.093, 51), (0.093, 71), (0.072, 72), (0.062, 194), (0.052, 145), (0.052, 195), (0.021, 48), (0.021, 50), (0.021, 47), (0.020, 124), (0.010, 147), (0.010, 146), (0.010, 46)] + else: + if context.get_value('k') <= 18.0: + if context.get_value('m*k') <= 528.0: + return [(0.097, 88), (0.087, 92), (0.077, 90), (0.058, 105), (0.058, 103), (0.058, 104), (0.058, 99), (0.058, 100), (0.058, 106), (0.058, 93), (0.057, 91), (0.057, 97), (0.057, 98), (0.057, 101), (0.048, 102), (0.029, 87), (0.029, 89)] + else: + if context.get_value('n') <= 80.0: + return [(0.057, 161), (0.057, 130), (0.057, 24), (0.056, 164), (0.056, 163), (0.056, 166), (0.056, 168), (0.056, 30), (0.056, 28), (0.056, 26), (0.056, 25), (0.056, 27), (0.056, 29), (0.056, 31), (0.042, 131), (0.028, 99), (0.028, 101), (0.028, 100), (0.028, 167), (0.028, 165), (0.028, 133)] + else: + return [(0.110, 164), (0.108, 163), (0.106, 168), (0.069, 161), (0.066, 151), (0.060, 152), (0.055, 165), (0.050, 27), (0.050, 29), (0.048, 131), (0.043, 153), (0.037, 133), (0.037, 130), (0.028, 8), (0.028, 5), (0.027, 7), (0.026, 26), (0.016, 162), (0.012, 9), (0.007, 4), (0.005, 100), (0.005, 6), (0.005, 24)] + else: + if context.get_value('k') <= 36.0: + if context.get_value('n') <= 68.0: + return [(0.097, 184), (0.097, 56), (0.086, 186), (0.086, 183), (0.086, 188), (0.086, 58), (0.086, 60), (0.065, 54), (0.043, 187), (0.043, 185), (0.043, 57), (0.043, 61), (0.032, 55), (0.032, 130), (0.032, 59), (0.011, 181), (0.011, 163), (0.011, 136), (0.011, 138)] + else: + return [(0.117, 184), (0.117, 170), (0.117, 169), (0.107, 183), (0.106, 188), (0.075, 181), (0.064, 130), (0.064, 56), (0.053, 171), (0.032, 57), (0.032, 59), (0.032, 185), (0.011, 163), (0.011, 32), (0.011, 37), (0.011, 34), (0.011, 33), (0.011, 35), (0.011, 36), (0.011, 54)] + else: + if context.get_value('mat2_stride_0') <= 384.0: + return [(0.244, 0), (0.061, 76), (0.061, 79), (0.030, 3), (0.030, 183), (0.030, 189), (0.030, 187), (0.030, 64), (0.030, 190), (0.030, 62), (0.030, 198), (0.030, 201), (0.030, 77), (0.030, 200), (0.030, 80), (0.030, 199), (0.030, 78), (0.030, 184), (0.020, 86), (0.020, 84), (0.020, 120), (0.020, 81), (0.020, 121), (0.020, 85), (0.020, 122), (0.010, 83), (0.010, 118), (0.010, 119), (0.010, 82)] + else: + return [(0.274, 83), (0.171, 86), (0.152, 0), (0.071, 85), (0.061, 125), (0.050, 84), (0.020, 109), (0.020, 117), (0.020, 81), (0.020, 118), (0.020, 121), (0.020, 108), (0.020, 115), (0.020, 116), (0.010, 110), (0.010, 120), (0.010, 103), (0.010, 107), (0.010, 119), (0.010, 122)] + else: + if context.get_value('arith_intensity') <= 56.995582580566406: + if context.get_value('n') <= 68.0: + if context.get_value('k*n') <= 4448.0: + if context.get_value('m*n') <= 29626368.0: + return [(0.107, 198), (0.107, 200), (0.107, 201), (0.107, 199), (0.106, 76), (0.106, 79), (0.064, 197), (0.063, 56), (0.043, 184), (0.043, 187), (0.042, 80), (0.042, 77), (0.042, 183), (0.021, 78)] + else: + return [(0.073, 201), (0.073, 198), (0.073, 200), (0.073, 199), (0.073, 197), (0.073, 56), (0.073, 58), (0.073, 79), (0.073, 76), (0.072, 59), (0.072, 78), (0.072, 77), (0.072, 80), (0.018, 184), (0.018, 55), (0.018, 54)] + else: + if context.get_value('k') <= 348.0: + return [(0.206, 76), (0.183, 77), (0.169, 198), (0.160, 199), (0.053, 59), (0.046, 56), (0.038, 3), (0.030, 148), (0.030, 58), (0.030, 187), (0.023, 184), (0.015, 0), (0.008, 55), (0.008, 54)] + else: + return [(0.146, 198), (0.145, 199), (0.145, 148), (0.126, 0), (0.084, 76), (0.084, 77), (0.042, 80), (0.042, 79), (0.021, 149), (0.021, 150), (0.021, 3), (0.014, 46), (0.014, 74), (0.014, 75), (0.014, 124), (0.014, 194), (0.014, 195), (0.007, 145), (0.007, 146), (0.007, 2), (0.007, 72), (0.007, 147), (0.007, 71)] + else: + if context.get_value('m') <= 3264.0: + return [(0.247, 147), (0.115, 197), (0.066, 199), (0.066, 201), (0.066, 198), (0.049, 0), (0.049, 169), (0.049, 171), (0.033, 140), (0.033, 125), (0.033, 114), (0.016, 126), (0.016, 183), (0.016, 184), (0.016, 185), (0.016, 182), (0.016, 188), (0.016, 78), (0.016, 148), (0.016, 138), (0.016, 77), (0.016, 56), (0.016, 59)] + else: + if context.get_value('k') <= 62.5: + return [(0.226, 190), (0.226, 189), (0.122, 62), (0.122, 64), (0.055, 77), (0.055, 78), (0.037, 198), (0.036, 201), (0.036, 33), (0.024, 163), (0.018, 56), (0.018, 35), (0.018, 169), (0.006, 171)] + else: + return [(0.162, 35), (0.118, 33), (0.096, 189), (0.096, 190), (0.088, 169), (0.074, 62), (0.073, 56), (0.066, 171), (0.051, 198), (0.051, 201), (0.044, 59), (0.037, 64), (0.029, 63), (0.007, 0), (0.007, 77)] + else: + if context.get_value('m*n') <= 1097728.0: + return [(0.403, 0), (0.179, 141), (0.134, 150), (0.086, 147), (0.051, 148), (0.048, 3), (0.024, 189), (0.020, 199), (0.017, 64), (0.010, 65), (0.010, 77), (0.007, 114), (0.003, 138), (0.003, 59), (0.003, 182)] + else: + if context.get_value('m*n') <= 3244032.0: + return [(0.295, 189), (0.176, 64), (0.157, 65), (0.090, 0), (0.069, 62), (0.059, 63), (0.046, 77), (0.039, 169), (0.023, 199), (0.020, 35), (0.013, 33), (0.010, 171), (0.003, 141)] + else: + if context.get_value('n') <= 136.0: + return [(0.197, 189), (0.197, 63), (0.161, 77), (0.157, 62), (0.061, 33), (0.044, 65), (0.039, 35), (0.039, 64), (0.030, 169), (0.026, 0), (0.017, 199), (0.017, 148), (0.009, 56), (0.004, 3)] + else: + return [(0.460, 0), (0.145, 62), (0.138, 63), (0.081, 35), (0.047, 33), (0.043, 189), (0.023, 64), (0.018, 77), (0.013, 169), (0.009, 65), (0.009, 56), (0.005, 32), (0.005, 59), (0.002, 183), (0.002, 163)] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/artifacts/_MixedMMA100.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/artifacts/_MixedMMA100.py new file mode 100644 index 0000000000000000000000000000000000000000..a781767a3ce520a073f9630b834154f156a5fe5f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/artifacts/_MixedMMA100.py @@ -0,0 +1,150 @@ +# flake8: noqa: B950 +# fmt: off +# This file was generated by AutoHeuristic. Do not modify it manually! +# To regenerate this file, take a look at the steps in the README.md file inside torchgen/_autoheuristic/mixed_mm/ +from typing import List, Optional, Tuple + +from torch._inductor.autoheuristic.autoheuristic_utils import ( + AHContext, + AHMetadata, + Choice, +) +from torch._inductor.autoheuristic.learnedheuristic_interface import ( + LearnedHeuristicDecision, +) + + +class MixedMMA100(LearnedHeuristicDecision): + + def __init__(self) -> None: + self.choices: list[Choice] = [] + self.fill_choices() + + def check_precondition(self, metadata: AHMetadata, context: AHContext,) -> bool: + return ( + metadata.name == self.get_name() + and metadata.shared_memory == 166912 + and str(metadata.device_capa) == "(8, 0)" + ) + + def get_confidence_threshold(self) -> float: + return 0.0 + + def get_choice(self, idx: int) -> str | None: + if idx < len(self.choices): + return self.choices[idx] + return None + + def fill_choices(self) -> None: + self.choices.append('extern_fallback_mixed_mm') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=128_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=128_BLOCK-N=32_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=128_BLOCK-N=32_numstages=5_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=128_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=256_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=256_BLOCK-N=128_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=64_BLOCK-N=128_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=64_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=128_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=128_BLOCK-N=32_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=128_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=128_numstages=4_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=32_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=128_numstages=5_numwarps=8') + + def get_name(self) -> str: + return 'mixed_mm' + + def get_best_choices(self, context: AHContext) -> list[tuple[float, int]] | None: + if str(context.get_value('1LEQmLEQ16')) != 'True': + if context.get_value('m') <= 32.5: + if context.get_value('n') <= 6976.0: + if context.get_value('n') <= 3520.0: + if context.get_value('m*n') <= 37632.0: + return None + else: + return [(1.000, 13)] + else: + if context.get_value('m*k') <= 452352.0: + return [(0.590, 13), (0.256, 8), (0.103, 7), (0.051, 11)] + else: + return [(0.778, 8), (0.222, 13)] + else: + if context.get_value('k*n') <= 102776832.0: + if context.get_value('n') <= 14656.0: + return [(1.000, 11)] + else: + return [(0.889, 11), (0.111, 13)] + else: + return [(1.000, 11)] + else: + if context.get_value('m*n') <= 446464.0: + if context.get_value('m*n') <= 223424.0: + if context.get_value('mat1_stride_0') <= 3968.0: + return None + else: + return None + else: + if context.get_value('m*n') <= 346112.0: + return [(0.960, 16), (0.040, 7)] + else: + return [(0.750, 16), (0.136, 14), (0.114, 7)] + else: + if str(context.get_value('33LEQmLEQ64')) != 'True': + if context.get_value('n') <= 6976.0: + return [(1.000, 14)] + else: + return [(0.753, 2), (0.222, 1), (0.015, 7), (0.007, 16), (0.004, 12)] + else: + if context.get_value('n') <= 13888.0: + return [(0.710, 14), (0.275, 21), (0.014, 12)] + else: + return [(0.374, 19), (0.339, 20), (0.106, 21), (0.101, 16), (0.066, 17), (0.009, 14), (0.004, 18)] + else: + if context.get_value('n') <= 3520.0: + if context.get_value('arith_intensity') <= 3.994754433631897: + if str(context.get_value('mat2_dtype')) != 'torch.uint8': + if context.get_value('m*k') <= 18944.0: + return [(0.577, 5), (0.423, 6)] + else: + return [(0.988, 5), (0.012, 6)] + else: + if context.get_value('arith_intensity') <= 2.9899919033050537: + return None + else: + return None + else: + if context.get_value('arith_intensity') <= 7.956453561782837: + if context.get_value('k*n') <= 9244032.0: + return [(0.822, 5), (0.178, 6)] + else: + return [(0.977, 5), (0.023, 0)] + else: + if context.get_value('m*k') <= 978944.0: + return [(1.000, 5)] + else: + return [(0.971, 5), (0.029, 0)] + else: + if context.get_value('n') <= 13632.0: + if context.get_value('n') <= 6976.0: + return [(1.000, 6)] + else: + if context.get_value('k') <= 3968.0: + return [(0.617, 3), (0.111, 5), (0.099, 7), (0.086, 9), (0.062, 6), (0.025, 8)] + else: + return [(0.779, 8), (0.119, 5), (0.053, 7), (0.035, 6), (0.013, 3)] + else: + if context.get_value('k*n') <= 39518208.0: + return [(0.385, 4), (0.327, 3), (0.192, 6), (0.038, 7), (0.038, 10), (0.019, 5)] + else: + if context.get_value('n') <= 20800.0: + return [(0.821, 6), (0.121, 7), (0.029, 4), (0.014, 5), (0.007, 3), (0.007, 8)] + else: + return [(0.530, 7), (0.386, 6), (0.046, 8), (0.021, 3), (0.015, 4), (0.002, 5)] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/artifacts/_MixedMMH100.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/artifacts/_MixedMMH100.py new file mode 100644 index 0000000000000000000000000000000000000000..a149cc39d7f6c5c98c76961a2b250a667d1220f1 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/artifacts/_MixedMMH100.py @@ -0,0 +1,149 @@ +# flake8: noqa: B950 +# fmt: off +# This file was generated by AutoHeuristic. Do not modify it manually! +# To regenerate this file, take a look at the steps in the README.md file inside torchgen/_autoheuristic/mixed_mm/ +from typing import Optional + +from torch._inductor.autoheuristic.autoheuristic_utils import ( + AHContext, + AHMetadata, + Choice, +) +from torch._inductor.autoheuristic.learnedheuristic_interface import ( + LearnedHeuristicDecision, +) + + +class MixedMMH100(LearnedHeuristicDecision): + + def __init__(self) -> None: + self.choices: list[Choice] = [] + self.fill_choices() + + def check_precondition(self, metadata: AHMetadata, context: AHContext,) -> bool: + return ( + metadata.name == self.get_name() + and metadata.shared_memory == 232448 + and str(metadata.device_capa) == "(9, 0)" + ) + + def get_confidence_threshold(self) -> float: + return 0.0 + + def get_choice(self, idx: int) -> str | None: + if idx < len(self.choices): + return self.choices[idx] + return None + + def fill_choices(self) -> None: + self.choices.append('extern_fallback_mixed_mm') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=32_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=128_BLOCK-K=64_BLOCK-N=128_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=128_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=128_BLOCK-N=32_numstages=2_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=128_BLOCK-N=32_numstages=5_numwarps=2') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=128_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=256_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=256_BLOCK-N=128_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=64_BLOCK-N=128_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=16_BLOCK-K=64_BLOCK-N=64_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=128_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=128_BLOCK-N=32_numstages=2_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=128_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=32_BLOCK-K=32_BLOCK-N=64_numstages=5_numwarps=8') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=128_numstages=4_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=32_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=128_BLOCK-N=64_numstages=5_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=128_numstages=3_numwarps=4') + self.choices.append('type=triton_BLOCK-M=64_BLOCK-K=64_BLOCK-N=64_numstages=3_numwarps=8') + + def get_name(self) -> str: + return 'mixed_mm' + + def get_best_choices(self, context: AHContext) -> list[tuple[float, int]] | None: + if context.get_value('arith_intensity') <= 15.988086223602295: + if context.get_value('n') <= 25280.0: + if context.get_value('n') <= 1344.0: + if context.get_value('mat1_stride_0') <= 7808.0: + return [(0.581, 7), (0.419, 6)] + else: + if context.get_value('m*n') <= 7680.0: + return [(0.875, 0), (0.125, 6)] + else: + return [(0.833, 0), (0.167, 7)] + else: + if context.get_value('n') <= 8512.0: + if str(context.get_value('mat2_dtype')) != 'torch.int8': + return [(0.763, 6), (0.237, 7)] + else: + return [(0.725, 7), (0.275, 6)] + else: + if str(context.get_value('mat1_dtype')) != 'torch.bfloat16': + return [(0.736, 7), (0.197, 9), (0.048, 6), (0.014, 8), (0.005, 10)] + else: + return [(0.473, 7), (0.398, 6), (0.097, 9), (0.032, 10)] + else: + if context.get_value('n') <= 42254.0: + if context.get_value('n') <= 33856.0: + if context.get_value('k*n') <= 68157440.0: + return [(0.370, 4), (0.370, 5), (0.074, 7), (0.074, 8), (0.074, 11), (0.037, 6)] + else: + return [(0.916, 8), (0.036, 7), (0.036, 9), (0.012, 4)] + else: + return [(0.659, 5), (0.341, 6)] + else: + if context.get_value('k*n') <= 326052992.0: + if context.get_value('n') <= 55232.0: + return [(0.571, 6), (0.321, 7), (0.036, 4), (0.036, 8), (0.036, 9)] + else: + return [(0.506, 6), (0.325, 8), (0.104, 7), (0.039, 5), (0.026, 9)] + else: + if context.get_value('n') <= 57024.0: + return [(0.462, 9), (0.385, 7), (0.115, 6), (0.038, 8)] + else: + return [(0.598, 8), (0.223, 9), (0.107, 6), (0.071, 7)] + else: + if context.get_value('m*n') <= 543936.0: + if str(context.get_value('17LEQmLEQ32')) != 'True': + if context.get_value('m*n') <= 262272.0: + if context.get_value('n') <= 1592.5: + return [(0.860, 0), (0.140, 9)] + else: + return None + else: + if context.get_value('m*k') <= 1294336.0: + return [(0.833, 17), (0.150, 18), (0.017, 15)] + else: + return [(0.917, 17), (0.083, 8)] + else: + if context.get_value('n') <= 12416.0: + if context.get_value('m*n') <= 43008.0: + return None + else: + return [(0.853, 14), (0.147, 9)] + else: + return [(0.625, 12), (0.375, 14)] + else: + if context.get_value('m') <= 32.5: + if context.get_value('mat2_stride_1') <= 6656.0: + if context.get_value('n') <= 69184.0: + return [(0.611, 12), (0.361, 14), (0.028, 13)] + else: + return [(1.000, 12)] + else: + if context.get_value('mat2_stride_1') <= 20864.0: + return [(1.000, 12)] + else: + return [(0.958, 12), (0.042, 9)] + else: + if context.get_value('m*n') <= 1085440.0: + if context.get_value('n') <= 9152.0: + return [(1.000, 18)] + else: + return [(0.780, 18), (0.160, 16), (0.060, 20)] + else: + if context.get_value('m') <= 67.0: + return [(0.650, 16), (0.203, 19), (0.122, 18), (0.016, 20), (0.008, 1)] + else: + return [(0.561, 3), (0.185, 16), (0.096, 20), (0.083, 19), (0.076, 2)] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/artifacts/_PadMMA100.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/artifacts/_PadMMA100.py new file mode 100644 index 0000000000000000000000000000000000000000..c19817f98484acb745a1fa600e25e1e3f3586307 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/artifacts/_PadMMA100.py @@ -0,0 +1,265 @@ +# flake8: noqa: B950 +# fmt: off +# This file was generated by AutoHeuristic. Do not modify it manually! +# To regenerate this file, take a look at the steps in the README.md file inside torchgen/_autoheuristic/pad_mm/ +from typing import Optional + +from torch._inductor.autoheuristic.autoheuristic_utils import ( + AHContext, + AHMetadata, + Choice, +) +from torch._inductor.autoheuristic.learnedheuristic_interface import ( + LearnedHeuristicDecision, +) + + +class PadMMA100(LearnedHeuristicDecision): + + def __init__(self) -> None: + self.choices: list[Choice] = [] + self.fill_choices() + + def check_precondition(self, metadata: AHMetadata, context: AHContext,) -> bool: + return ( + metadata.name == self.get_name() + and metadata.shared_memory == 166912 + and str(metadata.device_capa) == "(8, 0)" + ) + + def get_confidence_threshold(self) -> float: + return 0.9294871794871795 + + def get_choice(self, idx: int) -> Optional[str]: + if idx < len(self.choices): + return self.choices[idx] + return None + + def fill_choices(self) -> None: + self.choices.append('orig') + self.choices.append('pad') + + def get_name(self) -> str: + return 'pad_mm' + + def get_best_choices(self, context: AHContext) -> Optional[list[tuple[float, int]]]: + if str(context.get_value('mat1_innermost_needs_padding')) != 'False': + if context.get_value('arith_intensity') <= 880.0238037109375: + if str(context.get_value('m_multiple_2')) != 'True': + if context.get_value('n') <= 652.0: + if context.get_value('m') <= 2022.0: + if str(context.get_value('using_tf32')) != 'False': + return [(0.579, 1), (0.421, 0)] + else: + return [(1.000, 0)] + else: + return [(1.000, 0)] + else: + if context.get_value('m*k') <= 107278336.0: + if str(context.get_value('using_tf32')) != 'False': + if context.get_value('m') <= 23691.0: + return [(0.993, 1), (0.007, 0)] + else: + return [(0.840, 1), (0.160, 0)] + else: + if context.get_value('arith_intensity') <= 793.3185424804688: + return [(0.958, 0), (0.042, 1)] + else: + return [(0.792, 1), (0.208, 0)] + else: + if context.get_value('arith_intensity') <= 795.6242370605469: + if context.get_value('mat2_stride_1') <= 2048.5: + return [(0.929, 0), (0.071, 1)] + else: + return [(1.000, 1)] + else: + if context.get_value('arith_intensity') <= 796.3460388183594: + return [(0.957, 1), (0.043, 0)] + else: + return [(0.778, 0), (0.222, 1)] + else: + if context.get_value('mat2_stride_0') <= 2432.0: + if str(context.get_value('k_multiple_2')) != 'False': + if context.get_value('n') <= 1024.5: + if str(context.get_value('prepadded_mat1')) != 'False': + return [(0.580, 0), (0.420, 1)] + else: + return [(0.986, 0), (0.014, 1)] + else: + if context.get_value('mat1_stride_0') <= 5125.0: + return [(0.551, 0), (0.449, 1)] + else: + return [(0.916, 0), (0.084, 1)] + else: + if context.get_value('mat2_align_size') <= 6.0: + if str(context.get_value('using_tf32')) != 'True': + return [(1.000, 0)] + else: + return [(0.800, 0), (0.200, 1)] + else: + if context.get_value('mat2_stride_1') <= 3820.0: + return [(0.986, 1), (0.014, 0)] + else: + return [(0.532, 0), (0.468, 1)] + else: + if str(context.get_value('using_tf32')) != 'False': + if context.get_value('m*n') <= 5244928.0: + if str(context.get_value('k_multiple_2')) != 'True': + return [(0.971, 1), (0.029, 0)] + else: + return [(0.646, 1), (0.354, 0)] + else: + if context.get_value('k/(m*n)') <= 9.468618827668251e-06: + return [(0.800, 1), (0.200, 0)] + else: + return [(1.000, 1)] + else: + if context.get_value('mat1_stride_1') <= 1288.0: + if context.get_value('k') <= 5717.0: + return [(0.983, 0), (0.017, 1)] + else: + return [(0.800, 0), (0.200, 1)] + else: + return [(0.588, 0), (0.412, 1)] + else: + if str(context.get_value('using_tf32')) != 'False': + if context.get_value('n') <= 2640.0: + if context.get_value('m_padded_length') <= 1.5: + if context.get_value('mat2_stride_1') <= 6021.0: + return [(1.000, 1)] + else: + if context.get_value('mat2_stride_1') <= 6069.0: + return [(0.900, 1), (0.100, 0)] + else: + return [(1.000, 1)] + else: + if str(context.get_value('m_multiple_2')) != 'False': + if context.get_value('m*k') <= 24444928.0: + return [(0.593, 0), (0.407, 1)] + else: + return [(0.923, 0), (0.077, 1)] + else: + return [(1.000, 1)] + else: + if context.get_value('m*k') <= 404182016.0: + if str(context.get_value('mat2_innermost_needs_padding')) != 'False': + if context.get_value('m*k') <= 12328960.0: + return [(0.732, 1), (0.268, 0)] + else: + return [(0.989, 1), (0.011, 0)] + else: + if context.get_value('m*k') <= 389028864.0: + return [(0.998, 1), (0.002, 0)] + else: + return [(0.922, 1), (0.078, 0)] + else: + if context.get_value('m*n') <= 137631744.0: + if context.get_value('m*k') <= 405715968.0: + return [(0.611, 1), (0.389, 0)] + else: + return [(0.946, 1), (0.054, 0)] + else: + return [(0.714, 0), (0.286, 1)] + else: + if context.get_value('mat2_stride_0') <= 3902.5: + return [(0.941, 0), (0.059, 1)] + else: + return [(0.583, 1), (0.417, 0)] + else: + if context.get_value('n_padded_length') <= 0.5: + if str(context.get_value('mat2_innermost_needs_padding')) != 'False': + if str(context.get_value('k_multiple_2')) != 'True': + if context.get_value('arith_intensity') <= 884.5185852050781: + if context.get_value('arith_intensity') <= 743.931884765625: + return [(1.000, 0)] + else: + return [(0.583, 0), (0.417, 1)] + else: + return [(1.000, 1)] + else: + if context.get_value('k/(m*n)') <= 0.00023481580865336582: + return [(0.900, 0), (0.100, 1)] + else: + return [(1.000, 0)] + else: + if str(context.get_value('using_tf32')) != 'False': + if context.get_value('m*k') <= 93734912.0: + if context.get_value('mat1_stride_0') <= 1344.0: + if context.get_value('n') <= 7168.0: + return [(1.000, 0)] + else: + return [(0.970, 0), (0.030, 1)] + else: + if context.get_value('m') <= 22883.5: + return [(0.977, 0), (0.023, 1)] + else: + return [(0.800, 0), (0.200, 1)] + else: + if context.get_value('arith_intensity') <= 1914.3681030273438: + if str(context.get_value('prepadded_mat1')) != 'False': + return [(0.981, 0), (0.019, 1)] + else: + return [(0.995, 0), (0.005, 1)] + else: + return [(1.000, 0)] + else: + if str(context.get_value('prepadded_mat1')) != 'False': + if context.get_value('mat2_stride_1') <= 256.5: + if context.get_value('m') <= 5880.5: + return [(1.000, 0)] + else: + return [(0.800, 0), (0.200, 1)] + else: + if context.get_value('m*k') <= 6318080.0: + return [(0.618, 1), (0.382, 0)] + else: + return [(0.880, 0), (0.120, 1)] + else: + if context.get_value('k/(m*n)') <= 0.0009747986623551697: + if context.get_value('k/(m*n)') <= 0.0006397514371201396: + return [(0.951, 0), (0.049, 1)] + else: + return [(0.857, 0), (0.143, 1)] + else: + return [(1.000, 0)] + else: + if str(context.get_value('using_tf32')) != 'False': + if str(context.get_value('n_multiple_2')) != 'False': + if context.get_value('m') <= 2024.0: + if context.get_value('mat2_stride_0') <= 1629.0: + if context.get_value('k*n') <= 1288704.0: + return [(0.600, 0), (0.400, 1)] + else: + return [(0.982, 0), (0.018, 1)] + else: + if context.get_value('mat1_stride_0') <= 768.0: + return [(0.619, 0), (0.381, 1)] + else: + return [(0.812, 1), (0.188, 0)] + else: + if context.get_value('m*n') <= 5803008.0: + return [(0.500, 0), (0.500, 1)] + else: + if context.get_value('mat2_stride_1') <= 896.0: + return [(1.000, 1)] + else: + return [(0.818, 1), (0.182, 0)] + else: + if context.get_value('mat2_stride_1') <= 2560.0: + if context.get_value('num_dims_needs_padding') <= 1.5: + if context.get_value('mat2_stride_1') <= 896.0: + return [(1.000, 1)] + else: + return [(0.857, 1), (0.143, 0)] + else: + return [(0.727, 1), (0.273, 0)] + else: + return [(0.667, 1), (0.333, 0)] + else: + if context.get_value('k/(m*n)') <= 0.00015462777810171247: + return [(0.857, 0), (0.143, 1)] + else: + if context.get_value('k/(m*n)') <= 0.0019917909521609545: + return [(1.000, 0)] + else: + return [(0.900, 0), (0.100, 1)] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/artifacts/_PadMMH200.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/artifacts/_PadMMH200.py new file mode 100644 index 0000000000000000000000000000000000000000..1527b02319c105579be9b5aae81716142a9bf2ee --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/artifacts/_PadMMH200.py @@ -0,0 +1,175 @@ +# flake8: noqa: B950 +# fmt: off +# This file was generated by AutoHeuristic. Do not modify it manually! +# To regenerate this file, take a look at the steps in the README.md file inside torchgen/_autoheuristic/pad_mm/ +from typing import Optional + +from torch._inductor.autoheuristic.autoheuristic_utils import ( + AHContext, + AHMetadata, + Choice, +) +from torch._inductor.autoheuristic.learnedheuristic_interface import ( + LearnedHeuristicDecision, +) + + +class PadMMH200(LearnedHeuristicDecision): + + def __init__(self) -> None: + self.choices: list[Choice] = [] + self.fill_choices() + + def check_precondition(self, metadata: AHMetadata, context: AHContext,) -> bool: + return ( + metadata.name == self.get_name() + and metadata.shared_memory == 232448 + and str(metadata.device_capa) == "(9, 0)" + ) + + def get_confidence_threshold(self) -> float: + return 0.7710651828298887 + + def get_choice(self, idx: int) -> Optional[str]: + if idx < len(self.choices): + return self.choices[idx] + return None + + def fill_choices(self) -> None: + self.choices.append('orig') + self.choices.append('pad') + + def get_name(self) -> str: + return 'pad_mm' + + def get_best_choices(self, context: AHContext) -> Optional[list[tuple[float, int]]]: + if str(context.get_value('mat1_innermost_needs_padding')) != 'True': + if str(context.get_value('mat2_innermost_needs_padding')) != 'True': + if context.get_value('n_padded_length') <= 0.5: + if str(context.get_value('prepadded_mat1')) != 'True': + if str(context.get_value('using_tf32')) != 'False': + return [(1.000, 0)] + else: + if context.get_value('mat1_stride_0') <= 3584.0: + return [(1.000, 0)] + else: + if context.get_value('mat2_stride_0') <= 3584.0: + return [(1.000, 0)] + else: + return [(0.528, 0), (0.472, 1)] + else: + if context.get_value('n') <= 2304.0: + if context.get_value('m*k') <= 25198592.0: + if context.get_value('arith_intensity') <= 1103.9319458007812: + return [(1.000, 0)] + else: + return [(0.885, 0), (0.115, 1)] + else: + if context.get_value('m*k') <= 25688064.0: + return [(1.000, 1)] + else: + return [(0.771, 0), (0.229, 1)] + else: + if str(context.get_value('using_tf32')) != 'False': + if context.get_value('m') <= 27825.0: + return [(0.948, 0), (0.052, 1)] + else: + return [(0.855, 0), (0.145, 1)] + else: + if context.get_value('mat2_stride_0') <= 3584.0: + return [(1.000, 0)] + else: + return [(0.917, 1), (0.083, 0)] + else: + if context.get_value('m') <= 1823.5: + if str(context.get_value('n_multiple_2')) != 'False': + if context.get_value('k*n') <= 7859200.0: + return [(0.600, 0), (0.400, 1)] + else: + return [(1.000, 0)] + else: + if context.get_value('k/(m*n)') <= 0.00040277576772496104: + return [(1.000, 1)] + else: + return [(0.800, 1), (0.200, 0)] + else: + if context.get_value('n') <= 3602.0: + return [(0.800, 1), (0.200, 0)] + else: + return [(1.000, 1)] + else: + if str(context.get_value('using_tf32')) != 'False': + if str(context.get_value('n_multiple_16')) != 'False': + if str(context.get_value('k_multiple_2')) != 'True': + if context.get_value('arith_intensity') <= 744.8332214355469: + return [(0.600, 0), (0.400, 1)] + else: + return [(1.000, 1)] + else: + if context.get_value('m*n') <= 8912896.0: + if context.get_value('m*k') <= 5934080.0: + return [(0.800, 0), (0.200, 1)] + else: + return [(1.000, 0)] + else: + return [(1.000, 1)] + else: + return [(1.000, 1)] + else: + return [(1.000, 0)] + else: + if context.get_value('arith_intensity') <= 895.8767395019531: + if str(context.get_value('m_multiple_2')) != 'False': + if context.get_value('mat1_stride_1') <= 3421.0: + if str(context.get_value('using_tf32')) != 'False': + if context.get_value('mat2_stride_1') <= 10706.5: + if context.get_value('mat2_stride_0') <= 1024.5: + return [(0.816, 1), (0.184, 0)] + else: + return [(1.000, 1)] + else: + if str(context.get_value('k_multiple_2')) != 'True': + return [(0.905, 1), (0.095, 0)] + else: + return [(1.000, 0)] + else: + if str(context.get_value('prepadded_mat2')) != 'True': + if str(context.get_value('mat2_innermost_needs_padding')) != 'False': + return [(1.000, 0)] + else: + return [(0.932, 0), (0.068, 1)] + else: + if context.get_value('arith_intensity') <= 742.1241760253906: + return [(0.889, 0), (0.111, 1)] + else: + return [(0.765, 1), (0.235, 0)] + else: + if context.get_value('n') <= 1216.0: + if str(context.get_value('using_tf32')) != 'True': + if context.get_value('mat1_stride_1') <= 5567.0: + return [(0.896, 0), (0.104, 1)] + else: + return [(0.999, 0), (0.001, 1)] + else: + return [(1.000, 1)] + else: + if str(context.get_value('using_tf32')) != 'False': + return [(1.000, 1)] + else: + return [(1.000, 0)] + else: + if str(context.get_value('using_tf32')) != 'False': + return [(1.000, 1)] + else: + if context.get_value('mat2_stride_1') <= 2688.0: + return [(1.000, 0)] + else: + return [(0.500, 0), (0.500, 1)] + else: + if str(context.get_value('using_tf32')) != 'False': + return [(1.000, 1)] + else: + if str(context.get_value('mat2_innermost_needs_padding')) != 'True': + return [(1.000, 0)] + else: + return [(0.800, 0), (0.200, 1)] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/artifacts/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/artifacts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/autoheuristic.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/autoheuristic.py new file mode 100644 index 0000000000000000000000000000000000000000..d0072dd64310af116a43e39fe2b0c26b10c284d3 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/autoheuristic.py @@ -0,0 +1,318 @@ +import json +import os +from collections.abc import Callable +from functools import partial +from typing import Any + +import torch +from torch._inductor.autoheuristic.autoheuristic_utils import ( + AHContext, + AHMetadata, + AHOperation, + Choice, + CHOICE_COL, + Feedback, + FEEDBACK_COL, + get_metadata_str_from_log, +) +from torch._inductor.autoheuristic.learned_heuristic_controller import ( + LearnedHeuristicController, +) +from torch._inductor.ir import ChoiceCaller +from torch._inductor.runtime.runtime_utils import cache_dir +from torch._inductor.utils import get_gpu_shared_memory + + +class LocalFeedback: + """ + To be able to collect data for a choice, a function providing feedback given a choice has to be provided. + LocalFeedback can be used when AutoHeuristic should immediately run the function to collect feedback for each choice + (see pad_mm.py, where the autotuning happens locally, for an example). + """ + + def __init__(self, feedback_fn: Callable[[Choice], Feedback]) -> None: + self.feedback_fn = feedback_fn + + def __call__(self, choice: Choice) -> Feedback: + return self.feedback_fn(choice) + + +class InconsistentMetadata(Exception): + """ + Exception that is thrown when AutoHeuristic tries to log data to a file where the metadata stored in the file does + not match the metadata it would store if the file didn't exist. + """ + + +class AutoHeuristic: + """ + AutoHeuristic is a framework that allows one to collect data, learn a heuristic (i.e. a regression tree) and + generate the heuristic to code. This class allows one to collect data. The collected data can then be used to train + a heuristic (see torchgen/autoheuristic/). + """ + + collected_feedback: dict[Choice, Feedback] + + def __init__( + self, + fallback: Callable[[], Choice], + choices: list[Choice], + feedback: LocalFeedback | None, + context: AHContext, + name: str, + augment_context: list[AHOperation] | None = None, + precondition: Callable[[AHMetadata, AHContext], bool] | None = None, + ) -> None: + """ + Initializes an instance of the AutoHeuristic class. + + Args: + fallback: A callable that returns a Choice when the heuristic is unsure which choice to make, or + AutoHeuristic is in data collection mode. + choices: A list of possible choices the heuristic can make. + feedback: An instance of LocalFeedback that provides feedback for a given choice. + context: Context to store with each choice and feedback. + name: A string that identifies the heuristic. + augment_context: An optional list of AHOperation instances that augment the context. + precondition: A callable that returns a boolean indicating whether AutoHeuristic should run. + """ + self.fallback = fallback + self.choices = choices + self.feedback = feedback + self.context = context + self.name = name + self.collected_feedback = {} + self.augment_context = augment_context + self.metadata = AHMetadata( + get_gpu_shared_memory(), + torch.cuda.get_device_capability(), + self.choices, + self.name, + ) + self.precondition = precondition + + if not self.satisfies_precondition(): + return + + if torch._inductor.config.autoheuristic_log_path == "DEFAULT": + self.log_path = self.get_default_log_path() + else: + self.log_path = torch._inductor.config.autoheuristic_log_path + + if torch._inductor.config.collect_autoheuristic(self.name): + if self.feedback is not None: + for choice in self.choices: + feedback_val = self.feedback(choice) + self.save_data(choice, feedback_val) + + def satisfies_precondition(self) -> bool: + return self.precondition is None or self.precondition( + self.metadata, self.context + ) + + def get_choice(self) -> Choice: + """ + Returns the chosen option based on the value of autoheuristic_use. + If self.name is one of the comma separated strings in autoheuristic_use, + it queries a learned heuristic to make a decision. Otherwise, it returns the fallback option. + """ + + if not self.satisfies_precondition(): + return self.fallback() + + if torch._inductor.config.use_autoheuristic(self.name): + if self.augment_context is not None: + self.context.apply_operations(self.augment_context) + controller = LearnedHeuristicController( + self.metadata, + self.context, + ) + decision = controller.get_decision() + if decision not in self.choices: + # TODO(AlnisM): We might want to allow this in the future + return self.fallback() + if decision is not None: + return decision + return self.fallback() + + def get_top_k_choices( + self, top_k: int, always_included: list[str] | None = None + ) -> list[Choice] | None: + if not self.satisfies_precondition(): + return None + if torch._inductor.config.use_autoheuristic(self.name): + if self.augment_context is not None: + self.context.apply_operations(self.augment_context) + controller = LearnedHeuristicController( + self.metadata, + self.context, + ) + choices = controller.get_decisions_ranked(top_k) + if choices is None: + return None + if always_included is not None: + for choice in always_included: + if choice not in choices: + choices.append(choice) + return choices + return None + + def get_collected_feedback(self, choice: Choice) -> Any: + return self.collected_feedback.get(choice, None) + + @staticmethod + def get_device_identifier() -> str: + # a heuristic might work well for one GPU, but not for another + # we store the collected data per GPU model and learn a heuristic per GPU model + + # TODO(AlnisM): just using the device name for now, but the same GPU model can have different names + device_name = torch.cuda.get_device_name().replace(" ", "_") + return device_name + + def get_default_log_path(self) -> str: + device_name = self.get_device_identifier() + path = f"{cache_dir()}/autoheuristic/{device_name}/" + os.makedirs(path, exist_ok=True) + path += f"{self.name}.txt" + return path + + def serialize_metadata(self) -> str: + metadata_dict = self.metadata.to_dict() + ( + num_features, + cat_features, + ) = self.context.get_numerical_and_categorical_features() + metadata_dict["numerical_features"] = num_features + metadata_dict["categorical_features"] = cat_features + return json.dumps(metadata_dict) + + def save_data(self, choice: Choice, feedback_val: Feedback) -> None: + self.collected_feedback[choice] = feedback_val + log_path = self.log_path + + lines = [] + log_exists = os.path.exists(log_path) + if log_exists: + # if log already exists, make sure it is consistent + metadata = self.serialize_metadata() + existing_metadata = get_metadata_str_from_log(self.log_path) + if existing_metadata != metadata: + raise InconsistentMetadata( + "Given metadata does not match existing metadata" + ) + else: + lines.append(self.serialize_metadata()) + feature_header = self.context.get_feature_names_csv() + header = feature_header + "," + CHOICE_COL + "," + FEEDBACK_COL + lines.append(header) + + line = "" + feature_values = self.context.get_feature_values_csv() + line += feature_values + "," + choice + "," + str(feedback_val) + lines.append(line) + + with open(log_path, "a") as f: + f.write("\n".join(lines) + "\n") + + +class AutoHeuristicSelectAlgorithm(AutoHeuristic): + """ + AutoHeuristicSelectAlgorithm is a subclass of AutoHeuristic that allows one to collect data and learn a heuristic + when one wants to use AutoHeuristic for kernel choice selection. + """ + + def __init__( + self, + fallback: Callable[[], ChoiceCaller | None], + choices: list[ChoiceCaller], + input_nodes: list[Any], + context: AHContext, + name: str, + augment_context: list[AHOperation] | None = None, + precondition: Callable[[AHMetadata, AHContext], bool] | None = None, + ) -> None: + """ + The arguments choices, input_nodes and name have to match the ones used in the call to + autotune_select_algorithm(), e.g. if the following call is made + autotune_select_algorithm(name, choices, input_nodes, layout), the same name, choices and input_nodes + have to be used here. + """ + self.input_nodes = input_nodes + self.choicestr2choice: dict[str, ChoiceCaller] = {} + for choice in choices: + self.choicestr2choice[choice.autoheuristic_id()] = choice + choices_str = list(self.choicestr2choice.keys()) + + def fallback_str() -> str: + fallback_choice = fallback() + if fallback_choice is None: + # TODO: Find a nicer way to handle this + return "unsure" + return fallback_choice.autoheuristic_id() + + super().__init__( + fallback_str, + choices_str, + None, + context, + name, + augment_context, + precondition, + ) + + if ( + torch._inductor.config.collect_autoheuristic(self.name) + and self.satisfies_precondition() + ): + self.register_global_feedback(input_nodes, choices) + + def register_global_feedback( + self, input_nodes: list[Any], choices: list[ChoiceCaller] + ) -> None: + """ + Registers a callback in select_algorithm, which is called with the timing of each choice. + """ + + from torch._inductor.select_algorithm import ( + add_feedback_saver, + create_inputs_key, + create_precompile_key, + ) + + def store_global_feedback( + ah_inputs_key: str, + ah_precompile_key: str, + timings: dict[ChoiceCaller, float], + name: str, + input_nodes: list[Any], + choices: list[ChoiceCaller], + profiled_time: Callable[[], dict[ChoiceCaller, float]], + precompile_times: dict[ChoiceCaller, float], + ) -> None: + current_inputs_key = create_inputs_key(input_nodes) + if current_inputs_key != ah_inputs_key: + return + current_precompile_key = create_precompile_key( + name, current_inputs_key, choices + ) + if current_precompile_key != ah_precompile_key: + return + for choice, time in timings.items(): + self.save_data(choice.autoheuristic_id(), time) + + inputs_key = create_inputs_key(input_nodes) + precompile_key = create_precompile_key(self.name, inputs_key, choices) + feedback_saver = partial(store_global_feedback, inputs_key, precompile_key) + add_feedback_saver(feedback_saver) + + def get_choice_caller(self) -> ChoiceCaller | None: + choice = self.get_choice() + return self.choicestr2choice.get(choice, None) + + def get_top_k_choices_caller( + self, top_k: int, always_included: list[str] | None = None + ) -> list[ChoiceCaller] | None: + choices = self.get_top_k_choices(top_k, always_included) + if choices is None: + return None + return [self.choicestr2choice[choice] for choice in choices] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/autoheuristic_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/autoheuristic_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..e275405dbccc1acc0e7ead8466dedd63daa3bd48 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/autoheuristic_utils.py @@ -0,0 +1,340 @@ +import functools +from collections.abc import Callable +from typing import Any + +import torch + + +Feedback = float +Choice = str +Value = Any + +CHOICE_COL = "choice" +FEEDBACK_COL = "feedback" + + +class AHFeature: + """ + The context, that AutoHeuristic stores, is a list of features. AutoHeuristic needs to know whether a feature is + categorical (i.e., not a continuous variable) to learn a machine learning model. + """ + + def __init__(self, name: str, value: Value, is_categorical: bool = False) -> None: + self.name = name + self.value = value + self.is_categorical = is_categorical + + +class AHOperation: + """ + AHOperation can be used to augment the data collected by AutoHeuristic. + One might for example store features like m, k, n, but also want to use + features like m*n, or k*n, to learn a heuristic. Instead of storing features + that can be created from the collected data, one can use AHOperation to + create new features from the collected data. + """ + + def __init__( + self, name: str, func: Callable[[Any], Value], is_categorical: bool = False + ) -> None: + self.name = name + self.func = func + self.is_categorical = is_categorical + + def apply_operation(self, data: Any) -> None: + data[self.name] = self.func(data) + + +class AHContext: + """ + This class is used to specify which information AutoHeuristic should store. For each choice, AutoHeursitic will + store the context and the collected feedback. The context could be something like the shape of a tensor, i.e., + information that will help to learn a heuristic. + """ + + features: list[AHFeature] + context_dict: dict[str, Value] + + def __init__(self) -> None: + self.features = [] + self.context_dict = {} + + def add_feature( + self, name: str, value: Value, is_categorical: bool = False + ) -> None: + self.features.append(AHFeature(name, value, is_categorical=is_categorical)) + self.context_dict[name] = value + + def get_numerical_and_categorical_features(self) -> tuple[list[str], list[str]]: + numerical_features = [] + categorical_features = [] + for feature in self.features: + if feature.is_categorical: + categorical_features.append(feature.name) + else: + numerical_features.append(feature.name) + + return numerical_features, categorical_features + + def get_feature_names_csv(self) -> str: + return ",".join(feature.name for feature in self.features) + + def get_feature_values_csv(self) -> str: + return ",".join(str(feature.value) for feature in self.features) + + def get_value(self, name: str) -> Value: + return self.context_dict[name] + + def apply_operations(self, operations: list[AHOperation]) -> None: + for op in operations: + op.apply_operation(self.context_dict) + + +class AHMetadata: + def __init__( + self, + shared_memory: Any, + device_capa: tuple[int, int], + choices: list[Choice], + name: str, + ) -> None: + # use amount of shared_memory and device_capability to identify GPU + # TODO(AlnisM): there might be a better way to do this + self.shared_memory = shared_memory + self.device_capa = device_capa + self.choices = choices + self.name = name + + def to_dict(self) -> dict[str, Value]: + return { + "shared_memory": self.shared_memory, + "device_capa": self.device_capa, + "name": self.name, + } + + +def get_metadata_str_from_log(log_path: str) -> str: + with open(log_path, newline="") as file: + json_string = file.readline().strip() + return json_string + + +def check_minsize(context: AHContext, minsize: int) -> bool: + return ( + context.get_value("m") >= minsize + and context.get_value("k") >= minsize + and context.get_value("n") >= minsize + ) + + +def pad_mm_precondition(metadata: AHMetadata, context: AHContext) -> bool: + if metadata.shared_memory == 166912 and metadata.device_capa == (8, 0): + # A100 precondition + return check_minsize(context, 512) + elif metadata.shared_memory == 232448 and metadata.device_capa == (9, 0): + # H100 precondition + return check_minsize(context, 768) + return True + + +def get_mixedmm_precondition(metadata: AHMetadata, context: AHContext) -> bool: + m = context.get_value("m") + k = context.get_value("k") + n = context.get_value("n") + if m > 128 or k < 1024 or n < 1024: + return False + mat1_iscontig = context.get_value("mat1_iscontig") + mat2_iscontig = context.get_value("mat2_iscontig") + return mat1_iscontig and not mat2_iscontig + + +def get_mult_dims_ops() -> list[AHOperation]: + m_times_k_op = AHOperation("m*k", lambda data: data["m"] * data["k"]) + m_times_n_op = AHOperation("m*n", lambda data: data["m"] * data["n"]) + k_times_n_op = AHOperation("k*n", lambda data: data["k"] * data["n"]) + return [m_times_k_op, m_times_n_op, k_times_n_op] + + +def get_arith_intensity(data: Any) -> float: + m = data["m"] + k = data["k"] + n = data["n"] + if m == 0 or k == 0 or n == 0: + return 0.0 + return m * k * n / (m * k + k * n + m * n) + + +def pad_mm_operations() -> list[AHOperation]: + mult_dims_ops = get_mult_dims_ops() + k_div_m_times_n_op = AHOperation( + "k/(m*n)", lambda data: data["k"] / (data["m"] * data["n"]) + ) + + def bfloat_perf_hit(data: Any) -> bool: + m = data["m"] + k = data["k"] + n = data["n"] + is_bfloat = str(data["mat1_dtype"]) == "torch.bfloat16" + return k > (m * 1024) and k > (n * 1024) and is_bfloat + + bfloat_perf_hit_op = AHOperation( + "bfloat_perf_hit", bfloat_perf_hit, is_categorical=True + ) + + arith_intensity_op = AHOperation("arith_intensity", get_arith_intensity) + dims_need_padding_ops = get_dims_need_padding_ops() + dims_multiple_ops = get_dims_multiple_ops() + is_contig_ops = get_is_contig_ops() + + ah_operations = mult_dims_ops + [ + k_div_m_times_n_op, + bfloat_perf_hit_op, + arith_intensity_op, + ] + ah_operations.extend(dims_need_padding_ops) + ah_operations.extend(dims_multiple_ops) + ah_operations.extend(is_contig_ops) + return ah_operations + + +def between_op(data: Any, dim: str, lower: int, upper: int) -> bool: + return data[dim] >= lower and data[dim] <= upper + + +def between_ops() -> list[AHOperation]: + dims = ["m", "k", "n"] + limits = [(1, 16), (17, 32), (33, 64), (65, 128), (129, 256)] + ah_operations = [] + for dim in dims: + for lower, upper in limits: + between_op_fn = functools.partial( + between_op, dim=dim, lower=lower, upper=upper + ) + # using 'LEQ' instead of '<=' because '<=' cannot be exported to dot + between_op_name = f"{lower}LEQ{dim}LEQ{upper}" + ah_operations.append( + AHOperation(between_op_name, between_op_fn, is_categorical=True) + ) + return ah_operations + + +def pow2_op(data: Any, dim: str, exponent: int) -> bool: + return data[dim] == 2**exponent + + +def mm_operations() -> list[AHOperation]: + mult_dims_ops = get_mult_dims_ops() + arith_intensity_op = AHOperation("arith_intensity", get_arith_intensity) + return mult_dims_ops + [arith_intensity_op] + + +def mixed_mm_operations() -> list[AHOperation]: + return mm_operations() + between_ops() + + +def is_multiple(data: Any, dim: str, mult: int) -> bool: + return data[dim] % mult == 0 + + +def get_dims_multiple_ops() -> list[AHOperation]: + multiples = [2, 4, 8, 16, 32] + dims = ["m", "k", "n"] + dims_multiple_ops = [] + for dim in dims: + for mult in multiples: + is_multiple_fn = functools.partial(is_multiple, dim=dim, mult=mult) + dims_multiple_op = AHOperation( + f"{dim}_multiple_{mult}", is_multiple_fn, is_categorical=True + ) + dims_multiple_ops.append(dims_multiple_op) + return dims_multiple_ops + + +def get_dims_need_padding_ops() -> list[AHOperation]: + def mat1_innermost_needs_padding_fn(data: Any) -> bool: + mat1_stride_0 = data["mat1_stride_0"] + mat1_stride_1 = data["mat1_stride_1"] + m_padded_length = data["m_padded_length"] + k_padded_length = data["k_padded_length"] + mat1_innermost_needs_padding = False + if mat1_stride_0 == 1 and m_padded_length != 0: + mat1_innermost_needs_padding = True + if mat1_stride_1 == 1 and k_padded_length != 0: + mat1_innermost_needs_padding = True + return mat1_innermost_needs_padding + + mat1_innermost_op = AHOperation( + "mat1_innermost_needs_padding", + mat1_innermost_needs_padding_fn, + is_categorical=True, + ) + + def mat2_innermost_needs_padding_fn(data: Any) -> bool: + mat2_stride_0 = data["mat2_stride_0"] + mat2_stride_1 = data["mat2_stride_1"] + k_padded_length = data["k_padded_length"] + n_padded_length = data["n_padded_length"] + mat2_innermost_needs_padding = False + if mat2_stride_0 == 1 and k_padded_length != 0: + mat2_innermost_needs_padding = True + if mat2_stride_1 == 1 and n_padded_length != 0: + mat2_innermost_needs_padding = True + return mat2_innermost_needs_padding + + mat2_innermost_op = AHOperation( + "mat2_innermost_needs_padding", + mat2_innermost_needs_padding_fn, + is_categorical=True, + ) + + def num_dims_needs_padding_fn(data: Any) -> int: + m_padded_length = data["m_padded_length"] + k_padded_length = data["k_padded_length"] + n_padded_length = data["n_padded_length"] + num_dims_needs_padding = 0 + if m_padded_length != 0: + num_dims_needs_padding += 1 + if k_padded_length != 0: + num_dims_needs_padding += 1 + if n_padded_length != 0: + num_dims_needs_padding += 1 + return num_dims_needs_padding + + num_dims_op = AHOperation("num_dims_needs_padding", num_dims_needs_padding_fn) + return [mat1_innermost_op, mat2_innermost_op, num_dims_op] + + +def get_is_contig_ops() -> list[AHOperation]: + def mat1_is_contig_fn(data: Any) -> bool: + stride_0 = data["mat1_stride_0"] + stride_1 = data["mat1_stride_1"] + k = data["k"] + return stride_0 == k and stride_1 == 1 + + mat1_is_contig_op = AHOperation( + "mat1_iscontig", mat1_is_contig_fn, is_categorical=True + ) + + def mat2_is_contig_fn(data: Any) -> bool: + stride_0 = data["mat2_stride_0"] + stride_1 = data["mat2_stride_1"] + n = data["n"] + return stride_0 == n and stride_1 == 1 + + mat2_is_contig_op = AHOperation( + "mat2_iscontig", mat2_is_contig_fn, is_categorical=True + ) + + return [mat1_is_contig_op, mat2_is_contig_op] + + +def context_add_strides(context: AHContext, name: str, stride: tuple[int, ...]) -> None: + for i, s in enumerate(stride): + context.add_feature(f"{name}_stride_{i}", s) + + +def context_add_using_tf32(context: AHContext, dtype: torch.dtype) -> None: + using_tf32 = "not_float_32" + if dtype == torch.float32: + using_tf32 = torch.backends.cuda.matmul.fp32_precision == "tf32" + context.add_feature("using_tf32", using_tf32, is_categorical=True) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/learned_heuristic_controller.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/learned_heuristic_controller.py new file mode 100644 index 0000000000000000000000000000000000000000..26f03e3838c33b3a2bdf3b1a325ffa6bce19ff2e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/learned_heuristic_controller.py @@ -0,0 +1,119 @@ +import importlib +import inspect +import pkgutil +from collections import defaultdict +from typing import Any + +from torch._inductor.autoheuristic.autoheuristic_utils import ( + AHContext, + AHMetadata, + Choice, +) +from torch._inductor.autoheuristic.learnedheuristic_interface import LearnedHeuristic + + +def find_and_instantiate_subclasses( + package_name: str, base_class: Any +) -> list[LearnedHeuristic]: + instances = [] + + package = importlib.import_module(package_name) + for _, module_name, _ in pkgutil.walk_packages( + package.__path__, package.__name__ + "." + ): + try: + module_basename = module_name.split(".")[-1] + if not module_basename.startswith("_"): + # learned heuristics start with an underscore + continue + module = importlib.import_module(module_name) + + # look for classes that are subclasses of base_class + for _name, obj in inspect.getmembers(module): + if ( + inspect.isclass(obj) + and issubclass(obj, base_class) + and obj != base_class + ): + instance = obj() + instances.append(instance) + except Exception as e: + print(f"Error processing module {module_name}: {e}") + + return instances + + +class LearnedHeuristicController: + """ + Class that finds and instantiates all learned heuristics. It also provides + a way to get the decision of a learned heuristic. + """ + + existing_heuristics: dict[str, list[LearnedHeuristic]] = defaultdict(list) + """ + A dictionary that stores all the learned heuristics for each optimization. + The key is the optimization name, and the value is a list of LearnedHeuristic objects. + """ + + heuristics_initialized: bool = False + """ + A flag that indicates whether the learned heuristics have been initialized. + Set to true when the get_decision() function is called for the first time. + """ + + def __init__( + self, + metadata: AHMetadata, + context: AHContext, + ) -> None: + self.metadata = metadata + self.context = context + + def get_heuristics(self, name: str) -> list[LearnedHeuristic]: + """ + Returns a list of learned heuristics for the given optimization name. + """ + + if not LearnedHeuristicController.heuristics_initialized: + # learned heuristics are generated into the following package + learned_heuristics_package = "torch._inductor.autoheuristic.artifacts" + + # learned heuristics have to be of type LearnedHeuristic + base_class = LearnedHeuristic + found_heuristics = find_and_instantiate_subclasses( + learned_heuristics_package, base_class + ) + + for learned_heuristic in found_heuristics: + opt_name = learned_heuristic.get_name() + LearnedHeuristicController.existing_heuristics[opt_name].append( + learned_heuristic + ) + LearnedHeuristicController.heuristics_initialized = True + + return LearnedHeuristicController.existing_heuristics[name] + + def get_decision(self) -> Choice | None: + """ + Returns the decision made by the learned heuristic or None if no heuristic was found or the heuristic is unsure + which choice to make. + """ + + heuristics = self.get_heuristics(self.metadata.name) + for heuristic in heuristics: + if heuristic.check_precondition(self.metadata, self.context): + return heuristic.get_decision(self.context, self.metadata.choices) + return None + + def get_decisions_ranked(self, top_k: int) -> list[Choice] | None: + heuristics = self.get_heuristics(self.metadata.name) + for heuristic in heuristics: + if heuristic.check_precondition(self.metadata, self.context): + choices = heuristic.get_decisions_ranked(self.context) + if choices is None: + return None + avail_choices = [ + choice for choice in choices if choice in self.metadata.choices + ] + return avail_choices[:top_k] + return None diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/learnedheuristic_interface.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/learnedheuristic_interface.py new file mode 100644 index 0000000000000000000000000000000000000000..85558cf8e488c7f56fa99c4f59b6150eb37ff592 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autoheuristic/learnedheuristic_interface.py @@ -0,0 +1,82 @@ +import operator + +from torch._inductor.autoheuristic.autoheuristic_utils import ( + AHContext, + AHMetadata, + Choice, +) + + +class LearnedHeuristic: + """ + LearnedHeuristic is a base class for all learned heuristics. + """ + + def __init__(self) -> None: + pass + + def check_precondition( + self, + metadata: AHMetadata, + context: AHContext, + ) -> bool: + return True + + def get_decision(self, context: AHContext, choices: list[Choice]) -> Choice | None: + return None + + def get_confidence_threshold(self) -> float: + return 1.0 + + def get_name(self) -> str: + return "" + + def get_decisions_ranked(self, context: AHContext) -> list[str] | None: + return None + + +class LearnedHeuristicRegression(LearnedHeuristic): + def get_feedback(self, context: AHContext, choice: Choice) -> float: + return 1.0 + + def get_decision(self, context: AHContext, choices: list[Choice]) -> Choice | None: + choice2feedback = {} + for choice in choices: + predicted_feedback = self.get_feedback(context, choice) + choice2feedback[choice] = predicted_feedback + sorted_choices_feedback = sorted( + choice2feedback.items(), key=operator.itemgetter(1) + ) + highest_feedback = sorted_choices_feedback[-1][1] + second_highest_feedback = sorted_choices_feedback[-2][1] + if highest_feedback / second_highest_feedback > self.get_confidence_threshold(): + return sorted_choices_feedback[-1][0] + # We are not sure which choice is the best one + return None + + +class LearnedHeuristicDecision(LearnedHeuristic): + def get_choice(self, idx: int) -> str | None: + return None + + def get_decision(self, context: AHContext, choices: list[Choice]) -> Choice | None: + best_choices = self.get_best_choices(context) + if not best_choices: + return None + (best_choice_proba, best_choice_idx) = best_choices[0] + if best_choice_proba <= self.get_confidence_threshold(): + return None + return self.get_choice(best_choice_idx) + + def get_decisions_ranked(self, context: AHContext) -> list[str] | None: + feedback_idx_list = self.get_best_choices(context) + if feedback_idx_list is None: + return None + choices = [ + self.get_choice(feedback_idx[1]) for feedback_idx in feedback_idx_list + ] + choices = [choice for choice in choices if choice is not None] + return choices + + def get_best_choices(self, context: AHContext) -> list[tuple[float, int]] | None: + return [] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autotune_process.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autotune_process.py new file mode 100644 index 0000000000000000000000000000000000000000..9ddb8a9e8a841687b787b2240d345d9eb87bacc8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/autotune_process.py @@ -0,0 +1,1474 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import atexit +import contextvars +import ctypes +import dataclasses +import functools +import logging +import multiprocessing as mp +import os +import pickle +import queue +import selectors +import subprocess +import sys +import threading +import time +import warnings +from collections.abc import Callable, Iterable, Sequence +from concurrent.futures import Future, ProcessPoolExecutor, ThreadPoolExecutor +from ctypes import byref, c_size_t, c_void_p, CDLL +from typing import Any, IO, TYPE_CHECKING + +import torch +import torch._inductor.async_compile # noqa: F401 required to warm up AsyncCompile pools +from torch._dynamo.device_interface import get_interface_for_device +from torch._dynamo.testing import rand_strided +from torch._inductor import ir +from torch._inductor.codecache import ( + CppCodeCache, + CUDACodeCache, + DLLWrapper, + get_hash, + PyCodeCache, + XPUCodeCache, +) +from torch._inductor.compile_worker.timer import Timer +from torch._inductor.utils import ( + do_bench_using_profiling, + get_gpu_type, + get_ld_library_path, + is_gpu, + python_subprocess_env, +) +from torch._logging import getArtifactLogger +from torch.utils._ordered_set import OrderedSet + + +# Inactivity timeout for AutotuneProcessPool in seconds. +# Default: 600 seconds (10 minutes). Set to 0 to disable. +AUTOTUNE_POOL_INACTIVITY_TIMEOUT = int( + os.environ.get("TORCHINDUCTOR_AUTOTUNE_POOL_INACTIVITY_TIMEOUT", "600") +) + +if TYPE_CHECKING: + from types import ModuleType + + from torch._inductor.select_algorithm import ( + ChoiceCaller, + PartialRender, + TritonTemplateCaller, + ) + +from . import config +from .runtime.benchmarking import benchmarker +from .virtualized import V + + +CUDA_VISIBLE_DEVICES = "CUDA_VISIBLE_DEVICES" + +autotuning_log = getArtifactLogger(__name__, "autotuning") + + +class NonzeroWorkspaceNotSupportedError(Exception): + pass + + +class TuningProcess: + """ + Class to launch and interact with a benchmarking subprocess. + """ + + @staticmethod + def process_main(read_pipe: IO[bytes], write_pipe: IO[bytes]) -> None: + """ + Entry point for the child process. + """ + autotuning_log.debug( + "Started autotune subprocess %s. Visible devices: %s", + os.getpid(), + os.environ.get(CUDA_VISIBLE_DEVICES), + ) + + def workloop(): + while True: + job, extra_env = TuningProcess.recv(read_pipe) + if job is None: + # None is a sentinel for the child to shut down + break + try: + if extra_env: + os.environ.update(extra_env) + result = job() + except Exception as e: + result = e + TuningProcess.send(result, write_pipe) + + try: + workloop() + except EOFError: + # The parent closed the pipe + pass + + @staticmethod + def send( + obj: Any, write_pipe: IO[bytes], extra_env: dict[str, str] | None = None + ) -> None: + pickle.dump((obj, extra_env), write_pipe) + write_pipe.flush() + + @staticmethod + def recv(read_pipe: IO[bytes]) -> Any: + return pickle.load(read_pipe) + + def __init__(self, device: int | None): + self.device = device + self.start() + + def start(self): + """ + Start the benchmarking subprocess. + """ + entry = os.path.join(os.path.dirname(__file__), "__autotune_main__.py") + + subproc_read_fd, write_fd = os.pipe() + read_fd, subproc_write_fd = os.pipe() + self.write_pipe = os.fdopen(write_fd, "wb") + self.read_pipe = os.fdopen(read_fd, "rb") + + self.selector = selectors.DefaultSelector() + self.selector.register(self.read_pipe, selectors.EVENT_READ) + + cmd = [ + sys.executable, + entry, + f"--parent={os.getpid()}", + f"--read-fd={str(subproc_read_fd)}", + f"--write-fd={str(subproc_write_fd)}", + ] + env = { + **python_subprocess_env(), + # We shouldn't be using the Triton async compile subprocess pool, + # but as a precaution set the env var that disables its creation. + "TORCH_WARM_POOL": "0", + # Some internal usages need a modified LD_LIBRARY_PATH. + "LD_LIBRARY_PATH": get_ld_library_path(), + # This will cause the subprocs to profile using the profiler. + "TORCHINDUCTOR_PROFILE_WITH_DO_BENCH_USING_PROFILING": "1" + if config.profile_bandwidth_with_do_bench_using_profiling + else "0", + } + if self.device is not None: + env[CUDA_VISIBLE_DEVICES] = str(self.device) + self.process = subprocess.Popen( + cmd, + env=env, + pass_fds=(subproc_read_fd, subproc_write_fd), + ) + os.close(subproc_read_fd) + os.close(subproc_write_fd) + + self.running = True + + def alive(self) -> bool: + """ + True if the subprocess is still running. + """ + return self.running and self.process.poll() is None + + def put(self, req: Any, extra_env: dict[str, str] | None = None) -> None: + """ + Push a work item to the child process. + """ + if not self.alive(): + self.start() + TuningProcess.send(req, self.write_pipe, extra_env=extra_env) + + def get(self, timeout: float = 120.0) -> Any: + """ + Get a response from the child process. Raises TimeoutError on timeout; + raises EOFError if the subprocess crashes. + """ + try: + if not self.selector.select(timeout): + raise TimeoutError(f"Timeout in autotune subprocess {self.process.pid}") + result, _ = TuningProcess.recv(self.read_pipe) + except TimeoutError: + self.kill() + raise + except EOFError: + # The subprocess crashed + self.close() + raise + except Exception: + autotuning_log.exception( + "Unexpected exception in autotune subprocess %s", self.process.pid + ) + self.kill() + raise + + if isinstance(result, Exception): + raise result + return result + + def shutdown(self, wait: bool = True) -> None: + """ + Signal the child process to shut down gracefully. + """ + if self.alive(): + TuningProcess.send(None, self.write_pipe) + if wait: + self.wait() + + def wait(self) -> None: + """ + Wait for the child process to exit. + """ + if self.alive(): + self.process.wait() + self.close() + + def close(self) -> None: + """ + Close resources. + """ + self.selector.close() + self.read_pipe.close() + self.write_pipe.close() + self.running = False + + def kill(self) -> None: + """ + Send a SIGKILL to the child process. + """ + if self.alive(): + autotuning_log.error( + "Sending SIGKILL to autotune subprocess %d", + self.process.pid, + ) + self.process.kill() + self.close() + + def restart(self) -> None: + """ + Gracefully restarts the child process. + """ + self.shutdown(wait=True) + self.start() + + +class TuningProcessPool: + """ + Maintains a pool of TuningProcesses to benchmark kernels in parallel + across devices. By default, we create one TuningProcess per device and + set the sub-process environment to make only that device visible. + """ + + def __init__(self) -> None: + """ + Start the child processes. + """ + devices = self.get_device_list() + autotuning_log.debug("Sub-process autotune device list: %s", devices) + + # Launch the child processes. + self.processes = [TuningProcess(device=device) for device in devices] + + self.process_queue: queue.Queue[TuningProcess] = queue.Queue() + for p in self.processes: + self.process_queue.put(p) + + # Use a thread pool to manage distributing work to the subprocesses. + # Threads block on an available process, so it makes sense to match + # the number of threads with the number of devices. + self.executor = ThreadPoolExecutor(max_workers=len(devices)) + + @staticmethod + def get_device_list() -> Sequence[int | None]: + """ + Gather the list of devices to be used in the pool. + """ + if not config.autotune_multi_device: + # Don't use multiple devices + return [None] + + gpu_type = get_gpu_type() + device_interface = get_interface_for_device(gpu_type) + count = device_interface.device_count() + + # If the user specified the visible devices in the env, use those. + if CUDA_VISIBLE_DEVICES in os.environ: + devices = [int(d) for d in os.environ[CUDA_VISIBLE_DEVICES].split(",")] + assert len(devices) <= count + return devices + + return list(range(count)) + + def shutdown(self) -> None: + """ + Signal all child processes to exit. + """ + self.executor.shutdown() + + for p in self.processes: + p.shutdown(wait=False) + for p in self.processes: + p.wait() + + def target(self, choice: TritonTemplateCaller) -> float: + """ + Entry point for the thread-pool helper threads: Wait for an open TuningProcess, + remove it from the queue, execute the benchmark in that subprocess, and return + the TuningProcess to the queue. + """ + assert choice.bmreq is not None + + env_vars = ["TORCHINDUCTOR_CACHE_DIR", "TRITON_CACHE_DIR"] + extra_env = {v: os.environ[v] for v in env_vars if v in os.environ} + process = self.process_queue.get() + process.put(choice.bmreq.benchmark, extra_env=extra_env) + try: + return process.get( + config.max_autotune_subproc_result_timeout_seconds, + ) + except TimeoutError: + warnings.warn( + f"Timed out benchmarking choice '{choice}'. It will be ignored. " + "Please debug the root cause in case the choice can bring perf gains." + ) + # Set to INF so this choice will be ignored + return float("inf") + except Exception as process_exception: + warnings.warn( + f"Failed to benchmark choice '{choice}'. It will be ignored. " + "Please debug the root cause in case the choice can bring perf gains." + ) + # Sticky CUDA errors corrupt the context, making it unrecoverable. + # The process must be restarted to restore CUDA functionality. + error_msg = str(process_exception) + if ( + "cudaErrorLaunchFailure" in error_msg + or "cudaErrorIllegalAddress" in error_msg + ): + process.restart() + # Set to INF so this choice will be ignored + return float("inf") + finally: + self.process_queue.put(process) + + def benchmark( + self, + choices: list[TritonTemplateCaller], + ) -> dict[TritonTemplateCaller, float]: + """ + Benchmark each choice in a separate process. + """ + + # Use a ThreadExecutorPool to spread the work across the subprocesses and + # to grab subprocesses as soon as they're free. + results = dict(zip(choices, self.executor.map(self.target, choices))) + + return results + + +LayoutOrBuffer = ir.Layout | ir.Buffer + + +@dataclasses.dataclass +class TensorMeta: + device: torch.device + dtype: torch.dtype + sizes: torch._prims_common.ShapeType + strides: torch._prims_common.StrideType + offset: int + name: str | None = None + + @classmethod + def from_irnodes( + cls, irnodes: LayoutOrBuffer | Sequence[LayoutOrBuffer] + ) -> TensorMeta | list[TensorMeta]: + from torch._inductor.select_algorithm import get_strides_with_layout_constraints + + if isinstance(irnodes, Sequence): + result: list[Any] = [cls.from_irnodes(x) for x in irnodes] + assert all(isinstance(x, TensorMeta) for x in result) + return result + + node = irnodes + if isinstance(node, ir.Layout): + node = ir.Buffer(name="fake", layout=node) + + dtype = node.get_dtype() + assert dtype is not None + device = node.get_device() + assert device is not None + + return TensorMeta( + device=device, + dtype=dtype, + sizes=V.graph.sizevars.optimization_hints(node.get_size()), + strides=V.graph.sizevars.optimization_hints( + get_strides_with_layout_constraints(node) + ), + offset=V.graph.sizevars.optimization_hint(node.get_layout().offset), + name=node.get_name(), + ) + + def to_tensor(self) -> torch.Tensor: + return rand_strided( + self.sizes, + self.strides, + device=self.device, + dtype=self.dtype, + extra_size=self.offset, + ) + + +@dataclasses.dataclass +class BenchmarkRequest: + """ + Only handle triton template benchmark for now. The extern kernel benchmark + can be done inside the same process since they usually don't cause crash. + + Important: Instances of this class and subclasses have to be serializable + across process boundaries. Do not put CUDA Tensors in here! + """ + + def __init__( + self, + kernel_name: str, + input_tensor_meta: TensorMeta | list[TensorMeta], + output_tensor_meta: TensorMeta | list[TensorMeta], + extra_args: Iterable[Any], + ) -> None: + # the kernel name defined in the module + self.kernel_name = kernel_name + + if isinstance(input_tensor_meta, TensorMeta): + self.input_tensor_meta: list[TensorMeta] = [input_tensor_meta] + else: + self.input_tensor_meta: list[TensorMeta] = input_tensor_meta + + if output_tensor_meta and isinstance(output_tensor_meta, (tuple, list)): + if len(output_tensor_meta) > 1: + # Each output with same meta for Grouped GEMM + assert all( + getattr(output_tensor_meta[0], attr) == getattr(x, attr) + for x in output_tensor_meta + for attr in ["device", "dtype", "sizes", "strides", "offset"] + ) + self.output_tensor_meta = output_tensor_meta[0] + else: + # pyrefly: ignore [bad-assignment] + self.output_tensor_meta: TensorMeta = output_tensor_meta + + self.extra_args = extra_args + self.benchmark_with_cudagraphs = False + + def make_run_fn( + self, *input_tensors: torch.Tensor, out: torch.Tensor + ) -> Callable[[], None]: + raise NotImplementedError + + def cleanup_run_fn(self) -> None: + pass + + def do_bench( + self, + fn, + *input_tensors: torch.Tensor, + out: torch.Tensor | None = None, + ) -> float: + raise NotImplementedError + + def benchmark( + self, + *input_tensors: torch.Tensor, + out: torch.Tensor | None = None, + ) -> float: + debug = autotuning_log.isEnabledFor(logging.DEBUG) + if debug: + start_ts = time.time() + + # create args and out tensor + if out is None: + assert self.input_tensor_meta and self.output_tensor_meta, ( + "Input and output tensor meta must be populated when input_tensors is empty" + ) + assert len(input_tensors) == 0 + input_tensors = tuple(x.to_tensor() for x in self.input_tensor_meta) + out = self.output_tensor_meta.to_tensor() + + if debug: + create_tensor_elapse = time.time() - start_ts # type: ignore[possibly-undefined] + start_ts = time.time() + try: + fn = self.make_run_fn(*input_tensors, out=out) + except NonzeroWorkspaceNotSupportedError: + # Skipping all ops with nonzero workspace requirements + autotuning_log.info("Skipping op due to nonzero workspace requirement") + return float("inf") + + if debug: + load_elapse = time.time() - start_ts # type: ignore[possibly-undefined] + start_ts = time.time() + + if self.benchmark_with_cudagraphs: + res = benchmarker.benchmark_gpu_with_cuda_graph(fn) + else: + res = self.do_bench(fn, *input_tensors, out) + + if debug: + bench_elapse = time.time() - start_ts # type: ignore[possibly-undefined] + autotuning_log.debug( + "InChildProcess %s: load %f, create tensor %f, bench %f", + self, + load_elapse, # type: ignore[possibly-undefined] + create_tensor_elapse, # type: ignore[possibly-undefined] + bench_elapse, + ) + self.cleanup_run_fn() + return res + + +class _TestBenchmarkRequest(BenchmarkRequest): + """ + Supports unit testing. Defined in this file instead of the test file so the + TuningProcess sub-process can unpickle these objects. + """ + + def __init__( + self, + result: float = 0.0, + device: int | None = None, + sleep: float | None = None, + exc: Exception | None = None, + crash: bool = False, + ): + self.result = result + self.device = device + self.sleep = sleep + self.exc = exc + self.crash = crash + + def benchmark( + self, *input_tensors: torch.Tensor, out: torch.Tensor | None = None + ) -> float: + if self.device is not None: + assert os.environ.get(CUDA_VISIBLE_DEVICES, None) == str(self.device) + if self.sleep: + time.sleep(self.sleep) + if self.exc: + raise self.exc + if self.crash: + sys.exit(1) + return self.result + + +class GPUDeviceBenchmarkMixin: + def do_bench( + self, + fn, + *input_tensors: torch.Tensor, + out: torch.Tensor | None = None, + ) -> float: + device_idx_set = OrderedSet( + tensor.device.index + for tensor in [*input_tensors, out] + if isinstance(tensor, torch.Tensor) + and is_gpu(tensor.device.type) + and tensor.device.index is not None + ) + assert len(device_idx_set) <= 1, f"Can not mix devices {device_idx_set}" + device_type = next( + ( + tensor.device.type + for tensor in input_tensors + if is_gpu(tensor.device.type) + ), + "cuda", + ) + device_interface = get_interface_for_device(device_type) + if len(device_idx_set) == 1: + device_idx = next(iter(device_idx_set)) + else: + device_idx = device_interface.current_device() + with device_interface.device(device_idx): # type: ignore[attr-defined] + res = benchmarker.benchmark(fn, device=device_type) + device_interface.synchronize() # shake out any CUDA errors + + return res + + +class CPUDeviceBenchmarkMixin: + def do_bench( + self, + fn, + *input_tensors: torch.Tensor, + out: torch.Tensor | None = None, + ) -> float: + return benchmarker.benchmark_cpu(fn) + + +class TritonBenchmarkRequest(BenchmarkRequest): + """ + Represents a standalone benchmark request for a Triton Template. + + Important: Instances of this class have to be serializable + across process boundaries. Do not put CUDA Tensors in here! + """ + + def __init__( + self, + kernel_name: str, + input_tensor_meta: TensorMeta | list[TensorMeta], + output_tensor_meta: TensorMeta | list[TensorMeta], + extra_args: Iterable[Any], + module_path: str, # the path of the module defining the triton kernel + module_cache_key: str, + num_stages: int, + num_warps: int, + num_consumer_groups: int = 0, + num_buffers_warp_spec: int = 0, + matrix_instr_nonkdim: int = 0, # only used for hip to choose the shape of mfma instruction. + waves_per_eu: int = 0, # only used for hip to schedule waves per execution unit + kpack: int = 0, # ROCm specific gemm parameter + workspace_size: int | None = None, # size of workspace buffer in bytes + workspace_zero_fill: bool = False, # whether to zero-fill workspace + ) -> None: + super().__init__(kernel_name, input_tensor_meta, output_tensor_meta, extra_args) + self.module_path = module_path + self.module_cache_key = module_cache_key + self.num_stages = num_stages + self.num_warps = num_warps + self.num_consumer_groups = num_consumer_groups + self.num_buffers_warp_spec = num_buffers_warp_spec + self.matrix_instr_nonkdim = matrix_instr_nonkdim + self.waves_per_eu = waves_per_eu + self.kpack = kpack + self.workspace_size = workspace_size + self.workspace_zero_fill = workspace_zero_fill + + def make_run_fn( + self, *input_tensors: torch.Tensor, out: torch.Tensor + ) -> Callable[[], None]: + mod = PyCodeCache.load_by_key_path(self.module_cache_key, self.module_path) + autotuning_log.debug( + "benchmark module key: %s, path: %s", + self.module_cache_key, + self.module_path, + ) + + run_method = getattr(mod, self.kernel_name).run + extra_args = list(self.extra_args) + + # Recreate workspace tensor if needed (for TMA templates) + # The workspace tensor couldn't be pickled, so we recreate it here + # It should be inserted before the grid values (last 3 elements) + if self.workspace_size is not None: + from torch._inductor.select_algorithm import WORKSPACE_ARG_PLACEHOLDER + + workspace_tensor = torch.empty( + (self.workspace_size,), + dtype=torch.uint8, + device=out.device, + ) + if self.workspace_zero_fill: + workspace_tensor.zero_() + workspace_index = extra_args.index(WORKSPACE_ARG_PLACEHOLDER) + extra_args[workspace_index] = workspace_tensor + + run_method.__self__.with_bandwidth_info = False + + # Newer version of triton add warmup argument to JITFunction.run. + # This code handles backward-compatibility. + warmup_arg = {} + import inspect + + if "warmup" in inspect.signature(run_method).parameters: + warmup_arg["warmup"] = False + + if out.device.type == "cpu": + stream = 0 + else: + device_type = out.device.type + device_interface = get_interface_for_device(device_type) + stream = device_interface.get_raw_stream( + self.output_tensor_meta.device.index + ) + + if isinstance( + getattr(mod, self.kernel_name), + torch._inductor.runtime.triton_heuristics.DebugAutotuner, + ): + return functools.partial( + run_method, + *input_tensors, + out, + *extra_args, + **warmup_arg, + stream=stream, + ) + else: + return functools.partial( + run_method, + *input_tensors, + out, + *extra_args, + **warmup_arg, + stream=stream, + benchmark_run=True, + ) + + def precompile(self): + mod = PyCodeCache.load_by_key_path(self.module_cache_key, self.module_path) + kernel = getattr(mod, self.kernel_name) + kernel.precompile() + + self.n_regs = kernel.launchers[0].n_regs + + def __str__(self) -> str: + return f"{self.kernel_name=}, {self.module_path=}, {self.module_cache_key=}" + + +class TritonGPUBenchmarkRequest(GPUDeviceBenchmarkMixin, TritonBenchmarkRequest): + pass + + +class TritonCPUBenchmarkRequest(CPUDeviceBenchmarkMixin, TritonBenchmarkRequest): + pass + + +class ExternKernelBenchmarkRequest(BenchmarkRequest): + """ + A class to handle extern kernel benchmark requests. This allows extern kernels + (like aten::mm) to be benchmarked in a subprocess, similar to Triton kernels. + + Important: Instances of this class have to be serializable across + process boundaries. Do not put CUDA Tensors in here! + """ + + def __init__( + self, + kernel_name: str, + input_tensor_meta: TensorMeta | list[TensorMeta], + output_tensor_meta: TensorMeta | list[TensorMeta], + extra_args: Iterable[Any], + callable_path: str, # Module path to the callable (e.g., "extern_kernels.mm") + kwargs: dict[str, Any] | None = None, + has_out_variant: bool = True, + ) -> None: + super().__init__(kernel_name, input_tensor_meta, output_tensor_meta, extra_args) + self.callable_path = callable_path + self.kwargs = kwargs or {} + self.has_out_variant = has_out_variant + + def make_run_fn( + self, *input_tensors: torch.Tensor, out: torch.Tensor + ) -> Callable[[], None]: + fn = self.to_callable() + if self.has_out_variant: + # For out=variant, pass output as keyword arg + return functools.partial(fn, *input_tensors, out=out) + else: + # For non-out variant, just call with inputs + return functools.partial(fn, *input_tensors) + + def benchmark(self, *input_tensors: torch.Tensor, out: torch.Tensor | None = None): + if out is not None and out.numel() == 0: + # no need to run the kernel of do benchmarking + return 0.0 + if self.has_out_variant or len(input_tensors) == 0: + return super().benchmark(*input_tensors, out=out) + else: + algo = self.to_callable() + out_new = algo(*input_tensors) + if out is not None: + torch._C._dynamo.guards.assert_size_stride( + out_new, tuple(out.size()), tuple(out.stride()) + ) + out.copy_(out_new) # for correctness checking + if self.benchmark_with_cudagraphs: + return benchmarker.benchmark_gpu_with_cuda_graph( + lambda: algo(*input_tensors) + ) + if config.profile_bandwidth_with_do_bench_using_profiling: + return do_bench_using_profiling(lambda: algo(*input_tensors)) + return benchmarker.benchmark(algo, input_tensors, {}) + + def precompile(self) -> None: + # Extern kernels don't need precompilation - they're already compiled + pass + + def to_callable(self): + # While ExternKernelChoice also has a to_callable method, + # we avoid calling the ExternKernelChoice version here to make sure + # this is picklable + from torch._inductor.select_algorithm import extern_kernels + + fn = getattr(extern_kernels, self.kernel_name) + if self.kwargs: + return functools.partial(fn, **self.kwargs) + + return fn + + def __str__(self) -> str: + return f"ExternKernelBenchmarkRequest({self.callable_path})" + + +class ExternKernelGPUBenchmarkRequest( + GPUDeviceBenchmarkMixin, ExternKernelBenchmarkRequest +): + pass + + +class ExternKernelCPUBenchmarkRequest( + CPUDeviceBenchmarkMixin, ExternKernelBenchmarkRequest +): + pass + + +class SubgraphBenchmarkRequest(BenchmarkRequest): + """ + Benchmark request for subgraph choices. + + Pre-compiles the subgraph in the main process and stores + the module path/cache key for loading in subprocess. + """ + + def __init__( + self, + kernel_name: str, + input_tensor_meta: TensorMeta | list[TensorMeta], + output_tensor_meta: TensorMeta | list[TensorMeta], + extra_args: Iterable[Any], + module_path: str, + module_cache_key: str, + sym_input_values: list[int], + ) -> None: + super().__init__(kernel_name, input_tensor_meta, output_tensor_meta, extra_args) + self.module_path = module_path + self.module_cache_key = module_cache_key + self.sym_input_values = sym_input_values + + def make_run_fn( + self, *input_tensors: torch.Tensor, out: torch.Tensor + ) -> Callable[[], None]: + mod = PyCodeCache.load_by_key_path(self.module_cache_key, self.module_path) + sym_input_values = self.sym_input_values + # Create a new list each call since mod.call does args.clear() + return lambda: mod.call([*sym_input_values, *input_tensors]) + + def precompile(self) -> None: + # Module is already compiled in main process, no precompilation needed + pass + + def __str__(self) -> str: + return f"SubgraphBenchmarkRequest({self.kernel_name}, {self.module_path})" + + +class SubgraphGPUBenchmarkRequest(GPUDeviceBenchmarkMixin, SubgraphBenchmarkRequest): + pass + + +class SubgraphCPUBenchmarkRequest(CPUDeviceBenchmarkMixin, SubgraphBenchmarkRequest): + pass + + +class CUTLASSBenchmarkRequest(GPUDeviceBenchmarkMixin, BenchmarkRequest): + """ + A class to handle CUDA (CUTLASS) benchmark requests. This class is for + managing the lifecycle of a CUDA kernel benchmark, including compiling + the source code, managing workspace memory, and executing the kernel. + + Important: Instances of this class have to be serializable across + process boundaries. Do not put CUDA Tensors in here! + """ + + def __init__( + self, + kernel_name: str, + input_tensor_meta: TensorMeta | list[TensorMeta], + output_tensor_meta: TensorMeta | list[TensorMeta], + extra_args: Iterable[Any], + source_code: str, + device_type: str = "cuda", + ) -> None: + super().__init__(kernel_name, input_tensor_meta, output_tensor_meta, extra_args) + self.source_code = source_code + self.workspace_size: int = 0 + self.workspace: torch.Tensor | None = None + self.DLL: DLLWrapper | None = None + self._workspace_size_updated = False + self.hash_key: str = "" + self.source_file: str = "" + self.device_type = device_type + self.codecache_cls = XPUCodeCache if device_type == "xpu" else CUDACodeCache + self.device_interface = get_interface_for_device(device_type) + self.hash_key, self.source_file = self.codecache_cls.write( + self.source_code, "so" + ) + + def precompile(self): + """ + Precompile the CUDA source code to populate the CUDACodeCache. + This may happen in a separate thread pool. + """ + autotuning_log.debug("Precompiling %s", self) + self.codecache_cls.compile(self.source_code, "so") + autotuning_log.debug("Done precompiling %s", self) + + def make_run_fn( + self, *input_tensors: torch.Tensor, out: torch.Tensor + ) -> Callable[[], None]: + """ + Create a function to run the CUDA/XPU kernel with the given input and output tensors. + """ + + self.ensure_dll_loaded() + self.update_workspace_size() + args = [c_void_p(tensor.data_ptr()) for tensor in list(input_tensors) + [out]] + autotuning_log.debug( + "make_run_fn: self.kernel_name=%s, self.source_file=%s, self.hash_key=%s, self.DLL=%s, args=%s, self.extra_args=%s", + self.kernel_name, + self.source_file, + self.hash_key, + self.DLL, + args, + self.extra_args, + ) + stream_ptr = c_void_p( + self.device_interface.get_raw_stream(self.device_interface.current_device()) + ) + run_method = getattr(self.DLL, self.kernel_name) + workspace_ptr = c_void_p(0) + if self.workspace_size > 0: + self.workspace = torch.zeros( + (self.workspace_size + 7) // 8, + dtype=torch.float64, + device=out.device, + ) + workspace_ptr = c_void_p(self.workspace.data_ptr()) + + # Generate partial function. + ret = functools.partial( + run_method, + *args, + *self.extra_args, + None, # null workspace size ptr + workspace_ptr, # set workspace ptr, + stream_ptr, + ) + + # sanity check to make sure we cleanup run fn properly + try: + ret() + except RuntimeError as e: + err_msg = str(e) + + def raise_runtime_error(): + raise RuntimeError(err_msg) + + self.cleanup_run_fn() + return raise_runtime_error + + return ret + + def update_workspace_size(self) -> None: + if self._workspace_size_updated: + return + self.ensure_dll_loaded() + unique_input_count = len( + dict.fromkeys(meta.name for meta in self.input_tensor_meta) + ) + args = [c_void_p(None) for _ in range(unique_input_count + 1)] + stream_ptr = c_void_p( + self.device_interface.get_raw_stream(self.device_interface.current_device()) + ) + + run_method = getattr(self.DLL, self.kernel_name) + # Retrieve workspace_size and initialize workspace. + c_workspace_size = c_size_t() + run_method( + *args, # input ptrs and output ptrs + *self.extra_args, + byref( + c_workspace_size + ), # set workspace size ptr to retrieve workspace size + None, # null workspace ptr + stream_ptr, + ) + self.device_interface.synchronize() # shake out any device errors + self.workspace_size = c_workspace_size.value + autotuning_log.debug( + "update_workspace_size called: new workspace size=%d, self.kernel_name=%s, self.source_file=%s, self.hash_key=%s, self.DLL=%s, args=%s, self.extra_args=%s", # noqa: B950 + self.workspace_size, + self.kernel_name, + self.source_file, + self.hash_key, + self.DLL, + args, + self.extra_args, + ) + self._workspace_size_updated = True + + def ensure_dll_loaded(self): + if self.DLL is None: + self.DLL, self.hash_key, self.source_file = self.codecache_cls.load( + self.source_code, "so" + ) + self.DLL.open() + + def cleanup_run_fn(self) -> None: + if self.DLL is not None: + self.DLL.close() + self.DLL = None + self.workspace = None + + def __str__(self) -> str: + return f"{self.kernel_name=}, {self.source_file=}, {self.hash_key=}" + + def __getstate__(self) -> dict[str, Any]: + state = self.__dict__.copy() + state["DLL"] = None + state["workspace"] = None + state["_workspace_size_updated"] = False + return state + + def __setstate__(self, state: dict[str, Any]) -> None: + self.__dict__.update(state) + + +class CppBenchmarkRequest(CPUDeviceBenchmarkMixin, BenchmarkRequest): + # Important: Instances of this class have to be serializable + # across process boundaries. Do not put Tensors in here! + + def __init__( + self, + kernel_name: str, + input_tensor_meta: TensorMeta | list[TensorMeta], + output_tensor_meta: TensorMeta | list[TensorMeta], + extra_args: Iterable[Any], + source_code: str, + ) -> None: + super().__init__(kernel_name, input_tensor_meta, output_tensor_meta, extra_args) + self.source_code = source_code + self.hash_key = get_hash(source_code) + self.DLL: CDLL | ModuleType | None = None + + def precompile(self): + # Prepopulate CppCodeCache + # may happen in separate Threadpool + autotuning_log.debug("Precompiling %s", self) + CppCodeCache.load(self.source_code, device_type="cpu") + autotuning_log.debug("Done precompiling %s", self) + + def make_run_fn( + self, *input_tensors: torch.Tensor, out: torch.Tensor + ) -> Callable[[], None]: + # TODO(jgong5): use CppPythonBindingsCodeCache for better binding perf + self.DLL = CppCodeCache.load(self.source_code, device_type="cpu") + args = [tensor.data_ptr() for tensor in list(input_tensors) + [out]] + autotuning_log.debug( + "make_run_fn: self.kernel_name=%s, self.DLL=%s, args=%s, self.extra_args=%s", + self.kernel_name, + self.DLL, + args, + self.extra_args, + ) + run_method = getattr(self.DLL, self.kernel_name) + # Assume only size with type ctypes.c_ulonglong in extra_args + assert all(isinstance(arg, ctypes.c_ulonglong) for arg in self.extra_args) + run_method.argtypes = [ctypes.c_ulonglong] * ( + len(args) + len(list(self.extra_args)) + ) + + # Generate partial function. + return functools.partial( + run_method, + *args, + *self.extra_args, + ) + + def __str__(self) -> str: + return f"{self.kernel_name=}" + + +class CuteDSLBenchmarkRequest(GPUDeviceBenchmarkMixin, BenchmarkRequest): + """Benchmark request for CuteDSL (CUTLASS Python DSL) kernels.""" + + def __init__( + self, + kernel_name: str, + input_tensor_meta: TensorMeta | list[TensorMeta], + output_tensor_meta: TensorMeta | list[TensorMeta], + extra_args: tuple[Any, ...], + source_code: PartialRender, + ) -> None: + super().__init__(kernel_name, input_tensor_meta, output_tensor_meta, extra_args) + + finalized_code = source_code.finalize_all() + self.module_cache_key, self.module_path = PyCodeCache.write(finalized_code) + + def make_run_fn( + self, *input_tensors: torch.Tensor, out: torch.Tensor + ) -> Callable[[], None]: + """ + Create a function to run the CuteDSL kernel with the given input and output tensors. + Similar to TritonBenchmarkRequest.make_run_fn but for CuteDSL kernels. + """ + mod = PyCodeCache.load_by_key_path(self.module_cache_key, self.module_path) + + # Logic replicated async_compile + from .codegen.cutedsl.cutedsl_kernel import MAIN_SUFFIX + + main_func_name = f"{self.kernel_name}_{MAIN_SUFFIX}" + + if not hasattr(mod, main_func_name): + available = [name for name in dir(mod) if callable(getattr(mod, name))] + raise RuntimeError( + f"Could not find CuteDSL main kernel function '{main_func_name}'. Available callables: {available}" + ) + + kernel_func = getattr(mod, main_func_name) + + def run_kernel(): + device_interface = get_interface_for_device("cuda") + stream = device_interface.get_raw_stream(out.device.index) + return kernel_func(*input_tensors, out, stream=stream) + + return run_kernel + + +@functools.cache +def get_tuning_process_pool() -> TuningProcessPool: + pool = TuningProcessPool() + atexit.register(pool.shutdown) + return pool + + +def benchmark_in_sub_process( + choices: list[TritonTemplateCaller], +) -> dict[TritonTemplateCaller, float]: + """ + Do benchmarking in a subprocess and return the perf number (latency). + """ + return get_tuning_process_pool().benchmark(choices) + + +class AutotuneProcessPool: + """ + Singleton pool manager for running autotuning (precompilation + benchmarking) + in a separate process. + """ + + _instance: AutotuneProcessPool | None = None + _lock: threading.Lock = threading.Lock() + _shutdown_for_inactivity: bool = False + + def __init__(self): + self._pool: ProcessPoolExecutor | None = self._init_pool() + self._warmup_future: Future[Any] | None = None + self._warmup_start_time: float | None = None + self._timer: Timer | None = self._init_timer() + + @classmethod + def get_instance(cls): + """Get or create the singleton pool instance.""" + if cls._instance is None: + with cls._lock: + if cls._instance is None: + # num_workers=1 to avoid GPU contention during benchmarking + cls._instance = cls() + return cls._instance + + @property + def pool(self): + """Get the process pool.""" + assert config.pipeline_max_autotune_gemm, ( + "To use AutotuneProcessPool, pipeline_max_autotune_gemm must be enabled" + ) + if self._pool is None: + self._pool = self._init_pool() + self._timer = self._init_timer() + return self._pool + + def _init_timer(self) -> Timer | None: + if AUTOTUNE_POOL_INACTIVITY_TIMEOUT > 0: + return Timer(AUTOTUNE_POOL_INACTIVITY_TIMEOUT, self._on_inactivity_timeout) + return None + + def _record_activity(self) -> None: + if self._timer is not None: + self._timer.record_call() + + def _on_inactivity_timeout(self) -> None: + autotuning_log.info( + "AutotuneProcessPool shutting down due to inactivity (timeout=%ds)", + AUTOTUNE_POOL_INACTIVITY_TIMEOUT, + ) + + with self._lock: + if self._pool is not None: + self._pool.shutdown(wait=False) + self._pool = None + self._timer = None + + # Mark that the pool was shut down for inactivity. + # This prevents the pool from being recreated on recompiles + # which likely do not require large amounts of autotuning. + AutotuneProcessPool._shutdown_for_inactivity = True + + def _init_pool(self): + """ + Get or create the process pool. + + Uses ProcessPoolExecutor with 'spawn' context for CUDA safety. + ProcessPoolExecutor is lazily initialized - workers are not spawned + until the first submit() call, making this property non-blocking. + """ + # Use 'spawn' context to avoid CUDA fork issues + # Workers are spawned lazily on first submit(), not here + ctx = mp.get_context("spawn") + pool = ProcessPoolExecutor( + max_workers=1, + mp_context=ctx, + ) + atexit.register(self._shutdown) + autotuning_log.info("AutotuneProcessPool created (workers spawn lazily)") + + return pool + + def warm_up(self) -> Future[Any]: + """ + Submit a warmup job to eagerly spawn workers and initialize CUDA. + + This is optional - call it early to hide spawn latency. + Returns the warmup future which can be ignored or awaited. + """ + if self._warmup_future is None: + with self._lock: + if self._warmup_future is None: + self._warmup_start_time = time.perf_counter() + self._warmup_future = self.pool.submit( + _init_autotune_subprocess, + fp32_precision=torch.backends.cuda.matmul.fp32_precision, + ) + self._warmup_future.add_done_callback(self._on_warmup_complete) + autotuning_log.info("Warmup job submitted") + # pyrefly: ignore[bad-return] + return self._warmup_future + + def _on_warmup_complete(self, future: Future[Any]) -> None: + """Callback invoked when the warmup job completes.""" + warmup_elapsed_time = None + if self._warmup_start_time is not None: + warmup_elapsed_time = time.perf_counter() - self._warmup_start_time + + try: + result = future.result() + autotuning_log.info( + "AutotuneProcessPool warmup completed successfully in %.4f seconds: %s", + warmup_elapsed_time, + result, + ) + self._record_activity() + except Exception as e: + autotuning_log.error( + "AutotuneProcessPool warmup failed after %.4f seconds", + warmup_elapsed_time, + ) + raise e + + def submit(self, fn, *args, **kwargs) -> Future[Any]: + """Submit a job to the pool and return a Future.""" + future = self.pool.submit(fn, *args, **kwargs) + if self._timer is not None: + future.add_done_callback(lambda _: self._record_activity()) + return future + + def _shutdown(self): + """Shutdown the pool on exit.""" + if self._timer is not None: + self._timer.quit() + self._timer = None + if self._pool is not None: + self._pool.shutdown(wait=False) + self._pool = None + + @classmethod + def shutdown_instance(cls): + """Explicitly shutdown the singleton instance.""" + if cls._instance is not None: + with cls._lock: + if cls._instance is not None: + cls._instance._shutdown() + cls._instance = None + + +def use_pipelined_autotuning() -> bool: + return ( + config.pipeline_max_autotune_gemm + and not AutotuneProcessPool._shutdown_for_inactivity + ) + + +def _init_autotune_subprocess(fp32_precision: str) -> bool: + """ + Warmup function run in the autotune subprocess. + """ + import torch + + # Initialize dummy tensor for CUDA context + if torch.cuda.is_available(): + torch.zeros(1, device="cuda") + + torch.backends.cuda.matmul.fp32_precision = fp32_precision + + return True + + +def run_autotune_in_subprocess( + benchmark_request: BenchmarkRequest, +) -> float: + """ + Run autotuning benchmarks in a subprocess. + + This function is submitted to AutotuneProcessPool and runs in isolation + to prevent GPU contention with the main compilation process. + + Args: + picklable_choices: List of picklable choice information + + Returns: + timing + """ + + try: + # Run the benchmark directly - bmreq is already a BenchmarkRequest + timing = benchmark_request.benchmark() + + return timing + + except Exception: + autotuning_log.warning( + "Failed to benchmark choice %s", + benchmark_request, + exc_info=True, + ) + # Use infinity for failed benchmarks so they're not selected + return float("inf") + + +class PrecompileThreadPool: + """ + Thread pool for running precompilation asynchronously. + + This allows the main compilation process to continue while + precompilation happens in background threads. + """ + + _instance: PrecompileThreadPool | None = None + _lock = threading.Lock() + + def __init__(self, max_workers: int = 4): + self._executor = ThreadPoolExecutor(max_workers=max_workers) + + @classmethod + def get_instance(cls) -> PrecompileThreadPool: + from torch._inductor.select_algorithm import get_num_workers + + if cls._instance is None: + with cls._lock: + if cls._instance is None: + cls._instance = cls(get_num_workers()) + return cls._instance + + def submit(self, fn, *args, **kwargs): + ctx = contextvars.copy_context() + # Need to copy context so workers have access to the correct config settings + fn = functools.partial(ctx.run, fn) + return self._executor.submit(fn, *args, **kwargs) + + def _shutdown(self, wait: bool = False): + return self._executor.shutdown(wait=wait) + + @classmethod + def shutdown_instance(cls) -> None: + if cls._instance is not None: + with cls._lock: + if cls._instance is not None: + cls._instance._shutdown(wait=False) + cls._instance = None + + +class AsyncAutotuner: + """ + Handles asynchronous autotuning of kernel choices in a separate process. + + This class manages the lifecycle of autotuning: + 1. Accepts precompiled choices from the main process + 2. Submits benchmarking work to AutotuneProcessPool + 3. Returns results via a Future + + Usage: + autotuner = AsyncAutotuner(choices) + autotuner.start() # Kicks off async benchmarking + timings = autotuner.get_results() # Blocks until complete + """ + + choice_hash_to_future = {} + + @staticmethod + def get_choice_hash(choice: ChoiceCaller, inputs_key: str) -> str: + return choice.hash_key() + inputs_key + + @classmethod + def start(cls, choices: list[ChoiceCaller], inputs_key: str): + """ + Start asynchronous autotuning in a subprocess. + + This method: + 1. Extracts picklable benchmark requests from choices + 2. Submits benchmarking work to AutotuneProcessPool + 3. Returns immediately (non-blocking) + """ + + for choice in choices: + choice_hash = AsyncAutotuner.get_choice_hash(choice, inputs_key) + + if choice_hash in AsyncAutotuner.choice_hash_to_future: + continue + + assert getattr(choice, "bmreq", None) is not None, ( + "bmreq is None for choice" + ) + + autotune_future = AutotuneProcessPool.get_instance().submit( + run_autotune_in_subprocess, + choice.bmreq, + ) + + AsyncAutotuner.choice_hash_to_future[choice_hash] = autotune_future + + @classmethod + def get_results( + cls, choices: list[ChoiceCaller], inputs_key: str + ) -> dict[ChoiceCaller, float]: + """ + Get autotuning results, blocking until complete. + + Args: + timeout: Maximum time to wait in seconds. None means wait forever. + + Returns: + Dict mapping ChoiceCaller to benchmark timing + """ + + timings = {} + for choice in choices: + choice_hash = AsyncAutotuner.get_choice_hash(choice, inputs_key) + timings[choice] = AsyncAutotuner.choice_hash_to_future[choice_hash].result() + return timings diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/await_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/await_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..fcb4549066eddb64b80947179bbf4016cbfa483a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/await_utils.py @@ -0,0 +1,178 @@ +import asyncio +import sys +import weakref +from asyncio import AbstractEventLoop, Future +from collections.abc import Awaitable, Callable, Coroutine, Generator, Iterator +from contextlib import contextmanager, ExitStack +from contextvars import Context +from typing import Any, Protocol, TypeVar + +from torch.utils._ordered_set import OrderedSet + + +T = TypeVar("T") +TCoro = Generator[Any, None, T] + +if sys.version_info >= (3, 11): + + class TaskFactory(Protocol): + def __call__( + self, + __loop: AbstractEventLoop, + __factory: Coroutine[None, None, object] | Generator[None, None, object], + __context: Context | None = None, + /, + ) -> asyncio.futures.Future[object]: ... + + TaskFactoryType = TaskFactory +else: + TaskFactoryType = Callable[[AbstractEventLoop, Generator[TCoro, None, T]], Future] # type: ignore[valid-type] + + +def await_sync(awaitable: Awaitable[T]) -> T: + with get_loop() as loop: + return loop.run_until_complete(awaitable) + + +@contextmanager +def get_loop( + always_create_new_loop: bool = False, +) -> Iterator[AbstractEventLoop]: + try: + loop = asyncio.get_event_loop() + except RuntimeError as re: + if "There is no current event loop in thread" in str(re): + with _new_loop() as loop: + yield loop + return + else: + raise + + @contextmanager + def _restore_loop( + loop: asyncio.AbstractEventLoop, + ) -> Iterator[None]: + try: + yield + finally: + asyncio.set_event_loop(loop) + + @contextmanager + def _restore_running_loop() -> Iterator[None]: + loop_from_events = asyncio.events._get_running_loop() + asyncio.events._set_running_loop(None) + try: + yield + finally: + asyncio.events._set_running_loop(loop_from_events) + + with ExitStack() as stack: + if loop.is_running(): + stack.enter_context(_restore_running_loop()) + stack.enter_context(_restore_loop(loop=loop)) + loop = stack.enter_context(_new_loop(loop.get_task_factory())) # type: ignore[arg-type] + elif loop.is_closed(): + loop = stack.enter_context(_new_loop()) # type: ignore[arg-type] + elif always_create_new_loop: + stack.enter_context(_restore_loop(loop=loop)) + loop = stack.enter_context(_new_loop()) # type: ignore[arg-type] + yield loop + + +@contextmanager +def _new_loop( + task_factory: TaskFactoryType | None = None, +) -> Iterator[asyncio.AbstractEventLoop]: + loop = asyncio.new_event_loop() + tasks = _patch_loop(loop) + + if task_factory: + # pyre-ignore[6] + loop.set_task_factory(task_factory) # type: ignore[arg-type] + + asyncio.set_event_loop(loop) + try: + yield loop + finally: + try: + _cancel_all_tasks(loop, tasks) + finally: + asyncio.set_event_loop(None) + loop.close() + + +def _cancel_all_tasks( + loop: AbstractEventLoop, + tasks: OrderedSet[Future], # type: ignore[type-arg] +) -> None: + to_cancel = [task for task in tasks if not task.done()] + + if not to_cancel: + return + + # pyre-fixme[1001]: Awaitable assigned to `task` is never awaited. + for task in to_cancel: + task.cancel() + + # pyrefly: ignore [bad-argument-type] + loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True)) + + for task in to_cancel: + if task.cancelled(): + continue + if task.exception() is not None: + loop.call_exception_handler( + { + "message": "unhandled exception during asyncio.run() shutdown", + "exception": task.exception(), + "task": task, + } + ) + + +def _patch_loop(loop: AbstractEventLoop) -> OrderedSet[Future]: # type: ignore[type-arg] + tasks: weakref.WeakSet[Future] = weakref.WeakSet() # type: ignore[type-arg] + + task_factories: list[TaskFactoryType | None] = [None] + + def _set_task_factory(factory: TaskFactoryType | None) -> None: + task_factories[0] = factory + + def _get_task_factory() -> TaskFactoryType | None: + return task_factories[0] + + def _safe_task_factory( + loop: AbstractEventLoop, + coro: TCoro, # type: ignore[type-arg] + *, + context: Context | None = None, + ) -> asyncio.Future: # type: ignore[valid-type, type-arg] + task_factory = task_factories[0] + if task_factory is None: + if sys.version_info >= (3, 11): + # pyrefly: ignore [bad-argument-type] + task = asyncio.Task(coro, loop=loop, context=context) + else: + task = asyncio.Task(coro, loop=loop) + # pyre-ignore[16]: `Task` has no attribute `_source_traceback`. + if task._source_traceback: # type: ignore[attr-defined] + del task._source_traceback[ # type: ignore[attr-defined] + -1 + ] # pragma: no cover # type: ignore[attr-defined] + else: + if sys.version_info >= (3, 11): + task = task_factory(loop, coro, context=context) # type: ignore[arg-type, call-arg, assignment] + else: + task = task_factory(loop, coro) # type: ignore[arg-type] + # `Union[Task[Any], Future[Any]]`. + tasks.add(task) + return task + + # pyre-ignore[6] + loop.set_task_factory(_safe_task_factory) # type: ignore[method-assign, arg-type] + # pyre-ignore[8] + loop.set_task_factory = _set_task_factory # type: ignore[method-assign, assignment] + # pyre-ignore[8] + loop.get_task_factory = _get_task_factory # type: ignore[method-assign, assignment] + + return tasks # type: ignore[return-value] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/bounds.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/bounds.py new file mode 100644 index 0000000000000000000000000000000000000000..50b0fbaf96c124682d9bcb469f42e0ab9a637150 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/bounds.py @@ -0,0 +1,263 @@ +import logging +import operator +from collections.abc import Callable +from functools import partial +from typing import Any + +import sympy +from sympy import Expr + +import torch +from torch.utils._sympy.value_ranges import ( + bound_sympy, + SymPyValueRangeAnalysis, + ValueRanges, +) + +from ..utils._sympy.functions import PowByNatural +from ..utils._sympy.numbers import int_oo +from .loop_body import InterpreterShim, LoopBody, LoopBodyBlock +from .ops_handler import DefaultHandler, ReductionType, StoreMode +from .utils import cache_on_self, dominated_nodes +from .virtualized import V + + +log = logging.getLogger(__name__) + + +class BoundVars: + """ + Performs Value Range Analysis on LoopBody's fx graph by calling BoundVars.run() + It exposes the ranges of the nodes in the `bounds` variable + + Note. A current limitation of this analysis is that it just works on a per-loop basis. + We should be able to propagate the bounds between across the whole graph. This may benefit + the case a bounded variable is returned by a kernel and fed into another. + """ + + def __init__(self, loop_body: LoopBody) -> None: + def upper_bound(v: Expr | int) -> int: + return bound_sympy(v).upper if isinstance(v, Expr) else v + + self.loop_body = loop_body + self.replacement_vals = { + k: ValueRanges[Expr](0, upper_bound(v) - 1) + for k, v in loop_body.var_ranges.items() + } + # avoid computing these values, pessimistically assume that they are unbounded + self.unbounded_vars = dominated_nodes( + node + for node in self.loop_body.get_nodes() + if node.target in ["load", "reduction", operator.getitem] + or "masked_subblock" in node.target + ) + # To access this variable call `get_bounds()` + self._bounds: dict[torch.fx.Node, ValueRanges[Expr]] = {} + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"loop_body={self.loop_body},\n " + f"replacement_vals={self.replacement_vals}, \n" + f"unbounded_vars={self.unbounded_vars}, \n" + f"_bounds={self._bounds})" + ) + + @cache_on_self + def get_bounds(self) -> dict[torch.fx.Node, ValueRanges[Expr]]: + submodules = self.swap_submodules(self.loop_body.submodules) + + # Initialize the environment with the unbounded variables + for node in self.unbounded_vars: + # we need to evaluate masked_subblock to recurse, and we need to set indirect values + if not isinstance(node.target, str) or ( + "masked_subblock" not in node.target + and "set_indirect" not in node.target + ): + self._bounds[node] = ValueRanges[Expr].unknown() + + with V.set_ops_handler(ValueRangeAnalysis()): + interpreter = InterpreterShim(self.loop_body.root_block.graph, submodules) + log.debug("get_bounds:\n%s", self.loop_body.root_block.graph) + interpreter.run(V.get_ops_handler(), initial_env=self._bounds) + return self._bounds + + def swap_submodules( + self, submodules: dict[str, Callable[..., Any]] + ) -> dict[str, Callable[..., ValueRanges[Expr]]]: + result: dict[str, Callable[..., ValueRanges[Expr]]] = {} + for key in submodules: + if key == "get_index": + result[key] = self.get_index + elif "masked_subblock" in key: + subblock = self.loop_body.subblocks[key] + # The result within the lambda will reference to the final + # set of modules at the end of the for-loop as it stores a reference to it + + # bind subblock in a function because python lambdas close over by reference + # moving the lambda out of make_fn would close over the reference to subblock, + # so all lambdas would have the same subblock reference that is the final + # subblock in the loop + def make_fn( + subblock: LoopBodyBlock, + ) -> Callable[[Any, Any], ValueRanges[Expr]]: + return lambda mask, value: self.masked_subblock( + subblock, self._bounds, mask, value, result + ) + + result[key] = make_fn(subblock) + elif "set_indirect" in key: + idx = int(key[len("set_indirect") :]) + var = self.loop_body.indirect_vars[idx] + indirect = partial(self.set_indirect, var) + result[key] = indirect + else: + assert "scan" in key + result[key] = submodules[key] + + return result + + def masked_subblock( + self, + subblock: LoopBodyBlock, + env: dict[torch.fx.Node, ValueRanges[Expr]], + mask: Any, + value: Any, + submodules: dict[str, Callable[..., Any]], + ) -> ValueRanges[Expr]: + interp = InterpreterShim(subblock.graph, submodules) + interp.run(V.get_ops_handler(), initial_env=env) + output = [node for node in subblock.graph.nodes if node.target == "output"] + assert len(output) == 1 + # dont bother unioning with value since the load from buffer will be + # pessimistically assumed to be inf anyway + return interp.env[output[0]] + + def set_indirect(self, old: Expr, new: ValueRanges[Expr]) -> ValueRanges[Expr]: + assert isinstance(new, ValueRanges) + self.replacement_vals[old] = new + return new + + def get_index(self, name: str) -> ValueRanges[Expr]: + expr = self.loop_body.indexing_exprs[name] + bound = self.replacement_vals.get(expr) + if bound is None: + bound = bound_sympy(expr, self.replacement_vals) + # The following assertion is true at the time of this writing + # We don't assert is as to not execute bound_sympy when bound is not None + # assert bound is None or bound == bound_sympy(expr, self.replacement_vals) + self.replacement_vals[name] = bound + return bound + + +class ValueRangeAnalysis(SymPyValueRangeAnalysis, DefaultHandler): + def __init__(self) -> None: + self.name = "ValueRangeAnalysis" + boolean_operators = ( + "xor", + "logical_and", + "logical_or", + "logical_not", + ) + for op in boolean_operators: + setattr(self, op, self.bool_handler) + + @staticmethod + def bool_handler(*args: Any, **kwargs: Any) -> ValueRanges[Any]: + # just assuming bools can have both values + return ValueRanges(sympy.false, sympy.true) # type: ignore[arg-type] + + def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: + # many ops are unlikely to show up in optimizable indexing compute, + # so we dont have full coverage + return ValueRanges.unknown() + + def load(self, name: str, index: sympy.Expr) -> ValueRanges[Any]: + return ValueRanges.unknown() + + def store( + self, name: str, index: sympy.Expr, value: Any, mode: StoreMode = None + ) -> None: + return + + def reduction( + self, + dtype: torch.dtype, + src_dtype: torch.dtype, + reduction_type: ReductionType, + value: Any, + ) -> ValueRanges[Any]: + return ValueRanges.unknown() + + @classmethod + def index_expr(cls, index: Any, dtype: torch.dtype) -> ValueRanges[Any]: + assert isinstance(index, ValueRanges) + return cls.to_dtype(index, dtype) + + @staticmethod + # pyrefly: ignore [bad-override] + def to_dtype( + x: Any, + dtype: torch.dtype, + src_dtype: torch.dtype | None = None, + use_compute_types: bool = True, + ) -> ValueRanges[Any]: + x = ValueRanges.wrap(x) + + if dtype == torch.bool: + if x.is_singleton(): + return ValueRanges.wrap(x.lower != 0) + elif x.is_bool: + return x + elif 0 not in x: + return ValueRanges.wrap(sympy.true) + else: + return ValueRanges(sympy.false, sympy.true) + + def cast(x: Any, dtype: torch.dtype) -> sympy.Expr: + # dtype is int or float + if dtype.is_floating_point: + return sympy.Float(x) + else: + if x in (int_oo, -int_oo): + return x + try: + return sympy.Integer(x) + except TypeError: + # inf cannot be cast to Integer + return x + + if x.is_bool: + if x.is_singleton(): + val = 1 if x.lower else 0 + return ValueRanges.wrap(cast(val, dtype)) + else: + return ValueRanges(cast(0, dtype), cast(1, dtype)) + else: + # int to float or float to int + return ValueRanges(cast(x.lower, dtype), cast(x.upper, dtype)) + + @staticmethod + # pyrefly: ignore [bad-override] + def square(x: Any) -> ValueRanges[Any]: + return ValueRanges.convex_min_zero_map(x, lambda y: PowByNatural(y, 2)) + + @staticmethod + # pyrefly: ignore [bad-override] + def neg(x: Any) -> ValueRanges[Any]: + return ValueRanges.decreasing_map(x, operator.neg) + + # TODO: this is slightly inaccurate because truncdiv operates at integer + # precision, but we're going through float truediv which means we can + # potentially lose precision on the bounds + @classmethod + def truncdiv(cls, a: Any, b: Any) -> ValueRanges[Any]: + x = cls.truediv(a, b) + if x == ValueRanges.unknown(): + return x + + return cls.trunc(x) + + @classmethod + def sub(cls, a: Any, b: Any) -> ValueRanges[Any]: + return cls.add(a, cls.neg(b)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/cache.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/cache.py new file mode 100644 index 0000000000000000000000000000000000000000..118bbf2828799d8fd63a96427b5e88573d6adf19 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/cache.py @@ -0,0 +1,419 @@ +from __future__ import annotations + +import pickle +from abc import ABC, abstractmethod +from ast import literal_eval +from functools import cached_property +from hashlib import sha256 +from os import getenv +from pathlib import Path +from tempfile import gettempdir +from threading import Lock +from typing import Any, Generic, TYPE_CHECKING, TypeVar +from typing_extensions import assert_never, override, Self + +from torch.utils._filelock import FileLock + + +if TYPE_CHECKING: + from concurrent.futures import Future, ThreadPoolExecutor + + +# TypeVars can't be recursive, so generic types that fall within +# Key or Value can't be bound properly; for example, Key should +# only take tuples of other Key types: tuple[Key, ...]. this is +# a known shortcoming of torch's typing +Key = TypeVar("Key", str, int, tuple[Any, ...]) +Value = TypeVar("Value", str, int, tuple[Any, ...], bytes, dict[Any, Any], list[Any]) + + +class CacheError(ValueError): + """ + Exception raised for errors encountered during cache operations. + """ + + +class Cache(ABC, Generic[Key, Value]): + """ + Abstract base class for cache implementations. + Provides the interface for cache operations. + """ + + @abstractmethod + def get(self: Self, key: Key) -> Value | None: + """ + Retrieve a value from the cache. + Args: + key (Key): The key to look up. + Returns: + Value | None: The cached value if present, else None. + """ + + @abstractmethod + def insert(self: Self, key: Key, value: Value) -> bool: + """ + Insert a value into the cache. + Args: + key (Key): The key to insert. + value (Value): The value to associate with the key. + Returns: + bool: True if the value was inserted, False if the key already exists. + """ + + +class InMemoryCache(Cache[Key, Value]): + """ + In-memory cache implementation using a dictionary and thread lock. + """ + + def __init__(self: Self) -> None: + """ + Initialize an empty in-memory cache. + """ + self._cache: dict[Key, Value] = {} + self._lock: Lock = Lock() + + def get(self: Self, key: Key) -> Value | None: + """ + Retrieve a value from the cache. + Args: + key (Key): The key to look up. + Returns: + Value | None: The cached value if present, else None. + """ + with self._lock: + if (value := self._cache.get(key)) is not None: + return value + return None + + def insert(self: Self, key: Key, value: Value) -> bool: + """ + Insert a value into the cache. + Args: + key (Key): The key to insert. + value (Value): The value to associate with the key. + Returns: + bool: True if the value was inserted, False if the key already exists. + """ + with self._lock: + if key in self._cache: + # no overwrites for insert! + return False + self._cache[key] = value + return True + + @classmethod + def from_env_var(cls, env_var: str) -> Self: + """ + Create an in-memory cache from an environment variable. + Args: + env_var (str): Name of the environment variable containing cache data. + Returns: + InMemoryCache: An instance populated from the environment variable. + Raises: + CacheError: If the environment variable is malformed or contains invalid data. + """ + cache = cls() + + if (env_val := getenv(env_var)) is None: + # env_var doesn't exist = empty cache + return cache + + for kv_pair in env_val.split(";"): + # ignore whitespace prefix/suffix + kv_pair = kv_pair.strip() + + if not kv_pair: + # kv_pair could be '' if env_val is '' or has ; suffix + continue + + try: + # keys and values should be comma separated + key_bytes_repr, value_bytes_repr = kv_pair.split(",", 1) + except ValueError as err: + raise CacheError( + f"Malformed kv_pair {kv_pair!r} from env_var {env_var!r}, likely missing comma separator." + ) from err + + # ignore whitespace prefix/suffix, again + key_bytes_repr, value_bytes_repr = ( + key_bytes_repr.strip(), + value_bytes_repr.strip(), + ) + + try: + # check that key_bytes_str is an actual, legitimate encoding + key_bytes = literal_eval(key_bytes_repr) + except (ValueError, SyntaxError) as err: + raise CacheError( + f"Malformed key_bytes_repr {key_bytes_repr!r} in kv_pair {kv_pair!r}, encoding is invalid." + ) from err + try: + # check that value_bytes_str is an actual, legitimate encoding + value_bytes = literal_eval(value_bytes_repr) + except (ValueError, SyntaxError) as err: + raise CacheError( + f"Malformed value_bytes_repr {value_bytes_repr!r} in kv_pair {kv_pair!r}, encoding is invalid." + ) from err + + try: + key = pickle.loads(key_bytes) + except pickle.UnpicklingError as err: + raise CacheError( + f"Malformed key_bytes_repr {key_bytes_repr!r} in kv_pair {kv_pair!r}, not un-pickle-able." + ) from err + try: + value = pickle.loads(value_bytes) + except pickle.UnpicklingError as err: + raise CacheError( + f"Malformed value_bytes_repr {value_bytes_repr!r} in kv_pair {kv_pair!r}, not un-pickle-able." + ) from err + + # true duplicates, i.e. multiple occurrences of the same key => value + # mapping are ok and treated as a no-op; key duplicates with differing + # values, i.e. key => value_1 and key => value_2 where value_1 != value_2, + # are not okay since we don't allow overwriting cached values (it's bad regardless) + if (not cache.insert(key, value)) and (cache.get(key) != value): + raise CacheError( + f"Multiple values for key {key!r} found, got {cache.get(key)!r} and {value!r}." + ) + + return cache + + @classmethod + def from_file_path(cls, fpath: Path) -> Self: + """ + Create an in-memory cache from a file path. + Args: + fpath (Path): Path to the file containing pickled cache data. + Returns: + InMemoryCache: An instance populated from the file. + Raises: + CacheError: If the file is not a valid pickled dictionary. + """ + cache = cls() + + if not fpath.is_file(): + # fpath doesn't exit = empty cache + return cache + + try: + with open(fpath, "rb") as fp: + cache._cache = pickle.load(fp) + except pickle.UnpicklingError as err: + raise CacheError( + f"Failed to create cache from file path {fpath}, file contents are un-pickle-able." + ) from err + + if not isinstance(cache._cache, dict): + raise CacheError( + f"Failed to create cache from file path {fpath}, file contents not pickled dict[Key, Value]." + ) + + return cache + + +class AsyncCache(Cache[Key, Value]): + """ + Asynchronous cache implementation using ThreadPoolExecutor. + """ + + def get_async( + self: Self, key: Key, executor: ThreadPoolExecutor + ) -> Future[Value | None]: + """ + Retrieve a value from the cache asynchronously. + Args: + key (Key): The key to look up. + executor (ThreadPoolExecutor): Executor for async execution. + Returns: + Future[Value | None]: Future for the cached value or None. + """ + return executor.submit(self.get, key) + + def insert_async( + self: Self, key: Key, value: Value, executor: ThreadPoolExecutor + ) -> Future[bool]: + """ + Insert a value into the cache asynchronously. + Args: + key (Key): The key to insert. + value (Value): The value to associate with the key. + executor (ThreadPoolExecutor): Executor for async execution. + Returns: + Future[bool]: Future for the result of insertion. + """ + return executor.submit(self.insert, key, value) + + +class OnDiskCache(AsyncCache[Key, Value]): + """ + On-disk cache implementation using files and file locks. + Stores cache data in files on disk, with atomic operations and versioning. + Supports custom cache directory names. + Attributes: + version (int): The version used for cache versioning. + name (str): The name of the cache directory. + """ + + version: int = 0 + + def __init__(self: Self, name: str | None = None) -> None: + """ + Initialize an on-disk cache instance. + Args: + name (str | None, optional): The name of the cache directory. If None, + defaults to "on_disk_cache". + """ + self.name = name or "on_disk_cache" + + @cached_property + def base_dir(self: Self) -> Path: + """ + Get the base directory for the cache. + Returns: + Path: The base directory path for storing cache files. + """ + return Path(gettempdir()) / "cache" / self.name + + def _fpath_from_key(self: Self, key: Key) -> Path: + """ + Get the file path for a given key. + Args: + key (Key): The key to convert to a file path. + Returns: + Path: The file path for the key. + Raises: + CacheError: If the key is not pickle-able. + """ + try: + return self.base_dir / sha256(pickle.dumps(key)).hexdigest()[:32] + except (AttributeError, pickle.PicklingError) as err: + raise CacheError( + f"Failed to get fpath for key {key!r}, key is not pickle-able." + ) from err + # pyrefly: ignore [bad-argument-type] + assert_never(key) + + def _flock_from_fpath(self: Self, fpath: Path) -> FileLock: + """ + Get a file lock for a given file path. + Args: + fpath (Path): The file path. + Returns: + FileLock: The file lock for the path. + """ + # fpath.name is a hex digest, meaning there are 16^4 potential values + # for fpath.name[:4]; this is more than enough unique locks to not + # cause additional overhead from shared locks and it also saves our + # cache dir from becoming 50 percent locks + # pyrefly: ignore [bad-return] + return FileLock(str(fpath.parent / "locks" / fpath.name[:4]) + ".lock") + + @property + def version_prefix(self: Self) -> bytes: + """ + Get the version prefix for the cache. + Returns: + bytes: The version prefix as bytes, derived from the cache version string. + """ + return sha256(str(OnDiskCache.version).encode()).digest()[:4] + + @override + def get(self: Self, key: Key) -> Value | None: + """ + Retrieve a value from the cache. + Args: + key (Key): The key to look up. + Returns: + Value | None: The cached value if present and version matches, else None. + Raises: + CacheError: If the value is corrupted or cannot be unpickled. + Side Effects: + Removes stale cache files if the version prefix does not match. + """ + fpath = self._fpath_from_key(key) + flock = self._flock_from_fpath(fpath) + + with flock: + if not fpath.is_file(): + return None + + value_bytes = None + prefix_length = len(self.version_prefix) + with open(fpath, "rb") as fp: + if fp.read(prefix_length) == self.version_prefix: + value_bytes = fp.read() + + if value_bytes is None: + # version_prefix did not match, so we can't read the stale + # cached value; we should also remove the stale cached value, + # so that key can be re-cached by the newer version + fpath.unlink() + return None + + try: + value = pickle.loads(value_bytes) + except pickle.UnpicklingError as err: + raise CacheError( + f"Failed to get key {key!r}, value is potentially corrupted (value is not un-pickle-able)." + ) from err + + return value + + @override + def insert(self: Self, key: Key, value: Value) -> bool: + """ + Insert a value into the cache. + Args: + key (Key): The key to insert. + value (Value): The value to associate with the key. + Returns: + bool: True if the value was inserted, False if the key already exists. + Raises: + CacheError: If the value is not pickle-able. + Side Effects: + Creates the cache directory if it does not exist. + """ + fpath = self._fpath_from_key(key) + flock = self._flock_from_fpath(fpath) + fpath.parent.mkdir(parents=True, exist_ok=True) + try: + # "x" mode is exclusive creation, meaning the file will be created + # iff the file does not already exist (atomic w/o overwrite); use + # flock for added atomicity guarantee and to prevent partial writes + with flock as _, open(fpath, "xb") as fp: + fp.write(self.version_prefix) + pickle.dump(value, fp) + except pickle.PicklingError as err: + raise CacheError( + f"Failed to insert key {key!r} with value {value!r}, value is not pickle-able." + ) from err + except FileExistsError: + return False + return True + + +class InductorOnDiskCache(OnDiskCache[Key, Value]): + """ + Inductor-specific on-disk cache implementation. + Uses a custom base directory for Inductor cache files. + """ + + def __init__(self: Self) -> None: + """ + Initialize an inductor on-disk cache instance. + Sets the cache directory name to "inductor_on_disk_cache". + """ + super().__init__("inductor_on_disk_cache") + + @cached_property + def base_dir(self: Self) -> Path: + """ + Get the base directory for the Inductor cache. + Returns: + Path: The base directory path for Inductor cache files. + """ + from torch._inductor.runtime.runtime_utils import default_cache_dir + + return Path(default_cache_dir(), "cache", self.name) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/choices.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/choices.py new file mode 100644 index 0000000000000000000000000000000000000000..deccd38641cc48be74de4c562b1e1bb38caeb4e1 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/choices.py @@ -0,0 +1,689 @@ +from __future__ import annotations + +import dataclasses +import typing +from typing import Any, TYPE_CHECKING + +import sympy + +import torch +from torch._inductor.runtime.runtime_utils import next_power_of_2 +from torch._inductor.scheduler import MixOrderReduction +from torch.utils._sympy.value_ranges import bound_sympy + +from . import config +from .codecache import write_text +from .kernel_inputs import KernelInputs # noqa: TC001 +from .kernel_template_choice import make_ktc_generator +from .metrics import get_metric_table, is_metric_table_enabled +from .runtime.hints import DeviceProperties, ReductionHint +from .scheduler import BaseSchedulerNode, Scheduler, WhyNoFuse +from .select_algorithm import ExternKernelChoice +from .template_heuristics import get_template_heuristic +from .template_heuristics.triton import ( + BaseConfigHeuristic, + CPUConfigHeuristic, + CUDAConfigHeuristic, + MTIAConfigHeuristic, + ROCmConfigHeuristic, + XPUConfigHeuristic, +) +from .utils import _use_autotune_backend +from .virtualized import V + + +if TYPE_CHECKING: + from collections.abc import Generator + from functools import partial + + from triton import Config as TritonConfig + + from .codegen.common import KernelTemplate + from .codegen.simd_kernel_features import SIMDKernelFeatures + from .codegen.triton import TritonKernel + from .ir import ChoiceCaller + from .kernel_template_choice import KernelTemplateChoice + + from torch.utils._ordered_set import OrderedSet # isort: skip + + +class Sortable(typing.Protocol): + """Anything that can be used as a list.sort() key (int/tuple/etc)""" + + def __lt__(self, other: typing.Self) -> bool: ... + + +@dataclasses.dataclass +class FusionScore: + template_score: int + node_type_score: bool + memory_score: int + buffer_overlap_score: int + proximity_score: int + + def __lt__(self, other): + """ + node_type_score has higher priority than memory_score unless + the memory_score differs too much. + + buffer_overlap_score is prioritized below memory_score so that + strict global memory savings (exact dep matches) are preferred + over buffer overlap scoring (same buffer, different indexing). + """ + threshold = 16 + if self.template_score != other.template_score: + return self.template_score < other.template_score + + if ( + max(self.memory_score, other.memory_score) + > min(self.memory_score, other.memory_score) * threshold + ): + return self.memory_score < other.memory_score + + return ( + self.node_type_score, + self.memory_score, + self.buffer_overlap_score, + self.proximity_score, + ) < ( + other.node_type_score, + other.memory_score, + other.buffer_overlap_score, + other.proximity_score, + ) + + +class InductorChoices: + """ + This class contains a collection of default heuristics that affect performance of our generated + code. We try to not put correctness requirements in this file. + + You can override the choices made here by doing: + + class MyHeuristics(InductorChoices): + ... + + torch._inductor.virtualized.V.set_choices_handler(MyHeuristics()) + + Subclasses used with inductor_choices_class must implement uuid() for + cache key computation. + """ + + def get_config_heuristics( + self, device_type: str | None = "cuda" + ) -> BaseConfigHeuristic: + if device_type == "cuda": + if torch.version.hip is None: + return CUDAConfigHeuristic() + else: + return ROCmConfigHeuristic() + elif device_type == "xpu": + return XPUConfigHeuristic() + elif device_type == "cpu": + return CPUConfigHeuristic() + elif device_type == "mtia": + return MTIAConfigHeuristic() + else: + return BaseConfigHeuristic() + + # Conv configs + def get_conv_configs( + self, device_type: str | None = "cuda" + ) -> partial[Generator[TritonConfig, None, None]]: + conv_heuristics = self.get_config_heuristics(device_type) + return conv_heuristics.get_conv_configs() + + def get_depthwise_conv_configs(self, device_type: str | None = "cuda") -> list[Any]: + heuristics = self.get_config_heuristics(device_type) + return heuristics.get_depthwise_conv_configs() + + # Flex attention configs + # TODO(coconutruben): break out flexattention/decode configs into the new retrieval mechanism + def get_flex_attention_fwd_configs( + self, head_dim: int, dtype: torch.dtype, device_type: str | None = "cuda" + ) -> list[Any]: + flex_heuristics = self.get_config_heuristics(device_type) + return flex_heuristics.get_flex_attn_fwd_configs(head_dim, dtype) + + def get_flex_attention_bwd_configs( + self, head_dim: int, dtype: torch.dtype, device_type: str | None = "cuda" + ) -> list[Any]: + flex_heuristics = self.get_config_heuristics(device_type) + return flex_heuristics.get_flex_attn_bwd_configs(head_dim, dtype) + + def get_flex_decode_configs( + self, head_dim: int, dtype: torch.dtype, device_type: str | None = "cuda" + ) -> list[Any]: + flex_heuristics = self.get_config_heuristics(device_type) + return flex_heuristics.get_flex_decode_configs(head_dim, dtype) + + def _finalize_template_configs( + self, + template_choices: dict[str, Generator[KernelTemplateChoice, None, None]], + kernel_inputs: KernelInputs, + templates: list[KernelTemplate | ExternKernelChoice], + op_name: str, + kwarg_overrides: dict[str, dict[str, Any]] | None = None, + ) -> list[KernelTemplateChoice]: + """ + This method can be subclassed to perform any override/modification of the choices. + The incoming parameters are cheap (generators), so you can do any overrides without + incurring too much cost. Override this method to customize the kernel template choices + before they are converted to ChoiceCaller objects, which is expensive on template codegen. + + The full list of arguments are here to facilitate any overrides you may want to do, + as they can be used to start from scratch for each template if so desired. + + Args: + template_choices: Dictionary mapping template UIDs to generators of KernelTemplateChoice objects + kernel_inputs: MMKernelInputs containing input tensor nodes and matrix indices + templates: List of template objects (KernelTemplate or ExternKernelChoice) in use + op_name: Operation name (e.g., "bmm", "baddbmm", "addmm") + kwarg_overrides: Optional dict of kwargs to override for each template heuristic + + Returns: + Flattened list of KernelTemplateChoice objects across all templates + """ + choices: list[KernelTemplateChoice] = [] + for choice_gen in template_choices.values(): + choices.extend(choice_gen) + return choices + + def get_ktc( + self, + kernel_inputs: KernelInputs, + template: KernelTemplate | ExternKernelChoice, + op_name: str, + kwarg_overrides: dict[str, Any] | None = None, + ) -> Generator[KernelTemplateChoice, None, None]: + """ + Utility to get the KernelTemplateChoice generator for a specific input. + + This is a per template/op call, whereas get_template_configs is an op wide call (all templates). + Consider when overriding/using at which level you need to make decisions + """ + # Extract device_type from kernel_inputs + device_type = kernel_inputs.device_type + assert device_type is not None, "get_ktc requires a valid device type" + # Extract template_name from the template object + template_name = template.uid + + # Get the appropriate template-specific heuristic + heuristic = get_template_heuristic(template_name, device_type, op_name) + cs = heuristic.get_template_configs( + kernel_inputs, + op_name, + ) + # adjust the kernel inputs to the template-specific heuristic, if needed + # default here is to just return the kernel_inputs as is + inputs_val = heuristic.adjust_kernel_inputs(kernel_inputs, op_name) + extra_kwargs = heuristic.get_extra_kwargs(kernel_inputs, op_name) + # Create KernelTemplateChoice generator using the moved function + overrides = kwarg_overrides or {} + return make_ktc_generator( + template=template, + cs=cs, + extra_kwargs=extra_kwargs, + overrides=overrides, + layout=kernel_inputs.output_layout(), + inputs=inputs_val, + ) + + def _need_to_fix_layout( + self, + adjusted_choices: list[KernelTemplateChoice], + op_name: str, + ) -> bool: + """ + Check if we need to fix the layout instead of keeping it flexible + + Args: + ktc: KernelTemplateChoice object + + Returns: + True if we need to fix the layout, False otherwise + """ + # TODO: debug and fix + # NOTE: on mps, we see issues with flexible layouts on baddmm. This check just makes sure + # that for mps, everything stays as it was before this optimization + if len(adjusted_choices) > 0: + if adjusted_choices[0].inputs.device_type == "mps" and op_name not in [ + "mm", + "addmm", + ]: + return True + + # Since the following backends are not using get_mm_configs yet through the singular call, + if not (config.max_autotune or config.max_autotune_gemm): + # no danger of using other backends than ATEN + if not config.max_autotune_allow_flexible_layouts and op_name not in [ + # The historical implementation for mm and addmm allowed had flexible layouts in the + # not max-autotune world + "mm", + "addmm", + ]: + # TODO: deprecate this by migrating users to the new behavior + return True + return False + + if not config.max_autotune_allow_flexible_layouts: + # we always need to fix the layout + return True + + # Since the following backends are not using get_template_configs yet through the singular call, + # we don't know if they are a valid choice or not. Instead, just skip the optimization + # defensively. + # TODO(coconutruben): remove this once CPP,CK,CUTLASS are supported + if _use_autotune_backend("CUTLASS"): + return True + if _use_autotune_backend("CK") or _use_autotune_backend("CKTILE"): + return True + if _use_autotune_backend("CPP"): + return True + return any( + not isinstance(ktc.template, ExternKernelChoice) for ktc in adjusted_choices + ) + + def get_template_configs( + self, + kernel_inputs: KernelInputs, + templates: list[KernelTemplate | ExternKernelChoice], + op_name: str, + kwarg_overrides: dict[str, dict[str, Any]] | None = None, + ) -> list[ChoiceCaller]: + """ + Get list of ChoiceCallers for MM templates using template-specific heuristics. + + Args: + kernel_inputs: MMKernelInputs containing input tensor nodes and matrix indices + layout: Output layout + templates: List of template objects (KernelTemplate or ExternKernelChoice) + op_name: Operation name (e.g., "bmm", "baddbmm", "addmm", "mm_plus_mm") + kwarg_overrides: Optional dict of kwargs to override for each template heuristic, + indexed by template.uid. These only override the per config kwargs, not the extra kwargs + Returns: + List of ChoiceCaller objects from the templates + """ + if kwarg_overrides is None: + kwarg_overrides = {} + input_tensors = kernel_inputs.nodes() + if len(input_tensors) < 2: + raise ValueError(f"Need at least 2 input tensors, got {len(input_tensors)}") + layout = kernel_inputs.output_layout() + # First pass: Create dict of template.uid to generator of KernelTemplateChoice objects + template_choices = {} + for template in templates: + template_choices[template.uid] = self.get_ktc( + kernel_inputs, + template, + op_name, + kwarg_overrides.get(template.uid, {}), + ) + + # Second pass: Adjust the template choices + adjusted_choices = self._finalize_template_configs( + template_choices, + kernel_inputs, + templates, + op_name, + kwarg_overrides, + ) + # Layout optimization: if all choices are ExternKernelChoice and layout is FixedLayout, convert to FlexibleLayout + if self._need_to_fix_layout(adjusted_choices, op_name): + layout = kernel_inputs.output_layout(flexible=False) + for ktc in adjusted_choices: + ktc.layout = layout + # for good measure, delete the cached ChoiceCaller from the ktc if it existed. + # ExternKernelChoice are cheap to generate + if hasattr(ktc, "_choice"): + del ktc._choice + # Third pass: Convert to ChoiceCaller objects + return [ktc.choice for ktc in adjusted_choices if ktc.choice is not None] + + def triton_kernel_kwargs( + self, + kernel_cls: type[TritonKernel], + features: SIMDKernelFeatures, + groups: list[sympy.Expr], + kernel_kwargs: dict[str, Any], + ) -> dict[str, Any]: + """Hook to change the kwargs passed to TritonKernel, used to apply fixed configurations""" + return kernel_kwargs + + def override_best_choice( + self, + best_choice: ChoiceCaller, + timings: dict[ChoiceCaller, float], + ) -> ChoiceCaller: + """Hook to override the autotuning best choice after benchmarking.""" + return best_choice + + def customize_fused_kernel_name(self, fused_name: str, src_code: str) -> str: + """Hook to transform fused kernel names during codegen""" + return fused_name + + @staticmethod + def should_use_cooperative_reduction(features: SIMDKernelFeatures) -> bool: + """Heuristic to decide if a cooperative reduction should be used.""" + if config.triton.force_cooperative_reductions: + return True + if ( + not config.triton.cooperative_reductions + or V.graph.get_current_device_or_throw().type == "cpu" + ): + return False + + xhint = V.graph.sizevars.optimization_hint(features.numel, fallback=2) + if xhint <= 8: + threshold = 32768 * xhint + elif xhint <= 16: + threshold = 2097152 + else: + return False + # TODO(jansel): should this default on for dynamic shapes? + # TODO(laith) What if hint(features.reduction_numel) >= threshold ? + # shall we compare hints instead + return V.graph.sizevars.statically_known_geq( + features.reduction_numel, threshold + ) + + @staticmethod + def should_use_persistent_reduction( + features: SIMDKernelFeatures, cooperative_reduction: bool + ) -> bool: + """ + Heuristic to decide if a persistent reduction should be used. + """ + if not config.triton.persistent_reductions: + return False + threshold = { + ReductionHint.INNER: 1024, + }.get(features.get_reduction_hint(), 64) + + if features.get_reduction_hint() not in ( + ReductionHint.INNER, + ReductionHint.OUTER_TINY, + ): + bounds = bound_sympy(features.reduction_numel) + lower = bounds.lower + upper = bounds.upper + + if not all( + ( + (isinstance(bound, int) or bound.is_constant()) + and not torch.utils._sympy.numbers.is_infinite(bound) + ) + for bound in (lower, upper) + ): + return False + + lower = next_power_of_2(int(lower)) + upper = next_power_of_2(int(upper)) + + # If we are are coalescing on xblock (not ReductionHint.INNER) and this is not a tiny kernel + # (not ReductionHint.OUTER_TINY), do not use persistent reduction if it induces tile + # quantization. Persistent reduction forces rblock == rnumel, if the bounds between lower + # and upper are large, for the lower values we will be masking off large % of read/writes, + # when we could expand the coalescing xblock instead. + if lower != upper: + return False + + if cooperative_reduction: + # The RSPLIT of cooperative reductions means each thread block is operating on fewer elements + # The default fallback will be used if optimizations hint is not provided. The default fallback + # is >> 32. + threshold *= 32 // min( + V.graph.sizevars.optimization_hint(features.numel), 32 + ) + + # If multi_kernel is enabled, we do more aggressive persistent reduction. + # This may result in some persistent reductions slower than the + # corresponding non-persistent reductions. MultiKernel will do benchmarking + # to pick the faster one. + if config.triton.multi_kernel: + threshold *= 16 + + return V.graph.sizevars.statically_known_leq( + features.reduction_numel, threshold + ) # type: ignore[arg-types] + + @staticmethod + def reduction_split_factor( + device: torch.device, + reduction_numel_hint: int, + numel_hint: int, + inner_reduction: bool, + ) -> int: + """Heuristic to decide the RSPLIT used for split reductions. + When a reduction has a small number of outputs there is not enough parallelism, + so we will do the reduction in two phases.""" + props = DeviceProperties.create(device) + num_sm = props.multi_processor_count + warp_size = props.warp_size if props.warp_size is not None else 32 + max_threads_per_sm = ( + props.max_threads_per_multi_processor + if props.max_threads_per_multi_processor is not None + else 2048 + ) + min_elements_per_thread = warp_size + max_elements_per_thread = 512 + threads_per_sm = max_threads_per_sm + min_elements_per_device = min_elements_per_thread * num_sm * threads_per_sm + max_elements_per_device = max_elements_per_thread * num_sm * threads_per_sm + num_warps = 8 + num_threads = warp_size * num_warps + + if inner_reduction: + # do heuristics that's close to eager mode for split inner reduction + # we leak reduction autotune configs here, and will need to refactor to avoid this later + if numel_hint >= 2 * num_sm: # don't split if there are enough outputs + return 1 + if reduction_numel_hint <= 8192: + return 1 + if reduction_numel_hint * numel_hint <= min_elements_per_device: + split_size = min_elements_per_thread + elif reduction_numel_hint * numel_hint < max_elements_per_device: + target_blocks = num_sm * threads_per_sm // (2 * num_threads) + blocks_per_output = (target_blocks + numel_hint - 1) // numel_hint + tmp_split_size = ( + reduction_numel_hint + num_threads * blocks_per_output - 1 + ) // (num_threads * blocks_per_output) + divisors = sympy.divisors(reduction_numel_hint) + closest = min(divisors, key=lambda x: abs(x - tmp_split_size)) + if abs(closest - tmp_split_size) < 30: + # prefer even splits, but never smalle than min_elements_per_thread + split_size = max(closest, min_elements_per_thread) + else: + split_size = tmp_split_size + else: + divisors = sympy.divisors(reduction_numel_hint) + closest = min(divisors, key=lambda x: abs(x - max_elements_per_thread)) + if abs(closest - max_elements_per_thread) < 50: + # prefer even splits + split_size = closest + else: + split_size = max_elements_per_thread + return (reduction_numel_hint + split_size * num_threads - 1) // ( + split_size * num_threads + ) + else: + # TODO the best heuristic currently has XBLOCK (corresponding to numel_hint) 128 + # extend to even smaller number of outputs + rvals_per_thread = 4 # comes from heuristics, refactor to not leak here + xvals_per_block = 128 + xblocks = (numel_hint + xvals_per_block - 1) // xvals_per_block + if reduction_numel_hint * numel_hint < min_elements_per_device: + split_size = min_elements_per_thread + elif reduction_numel_hint * numel_hint < max_elements_per_device: + target_blocks = num_sm * threads_per_sm // (num_threads) + target_blocks = (target_blocks + xblocks - 1) // xblocks + tmp_split_size = ( + reduction_numel_hint + rvals_per_thread * target_blocks - 1 + ) // (rvals_per_thread * target_blocks) + divisors = sympy.divisors(reduction_numel_hint) + closest = min(divisors, key=lambda x: abs(x - tmp_split_size)) + if abs(tmp_split_size - closest) < 20: + split_size = max(closest, min_elements_per_thread) + else: + split_size = tmp_split_size + else: + divisors = sympy.divisors(reduction_numel_hint) + closest = min(divisors, key=lambda x: abs(x - max_elements_per_thread)) + if abs(closest - max_elements_per_thread) < 50: + # prefer even splits + split_size = closest + else: + split_size = max_elements_per_thread + + return (reduction_numel_hint + rvals_per_thread * split_size - 1) // ( + rvals_per_thread * split_size + ) + + @staticmethod + def can_fuse( + scheduler: Scheduler, + node1: BaseSchedulerNode, + node2: BaseSchedulerNode, + shared_data_score: int, + ) -> bool: + """ + Heuristics to prevent fusion applied to both horizontal and vertical fusions. Heuristics here should not + be needed for correctness and tweaking them may yield additional performance. + + See also some related heuristics that can be changed via config: + - config.triton.tiling_prevents_pointwise_fusion + - config.triton.tiling_prevents_reduction_fusion + - config.aggressive_fusion (will cause this function to be called more times) + """ + if shared_data_score == 0 and ( + not config.aggressive_fusion or node1.is_reduction() or node2.is_reduction() + ): + if is_metric_table_enabled("fusion_failure_due_to_indexing_mismatch"): + common_buf_names: OrderedSet[str] = ( + node1.read_writes.buffer_names() & node2.read_writes.buffer_names() + ) + if len(common_buf_names) > 0: + get_metric_table("fusion_failure_due_to_indexing_mismatch").add_row( + # pyrefly: ignore [bad-argument-type] + lambda: { + "pre_grad_graph_id": V.graph.graph_id, + "post_grad_graph_id": V.graph.post_grad_graph_id, + "node1_name": node1.get_name(), + "node2_name": node2.get_name(), + "node1_debug_str": write_text(node1.debug_str()), + "node2_debug_str": write_text(node2.debug_str()), + "common_buffer_names": list(common_buf_names), # type: ignore[dict-item] + "failure_reason": scheduler.decide_fusion_fail_reason( + node1, node2, common_buf_names + ), + } + ) + + WhyNoFuse(node1, node2)("no shared data due to indexing mismatch") + return False + WhyNoFuse(node1, node2)("no shared data") + return False # heuristic not needed for correctness + + if ( + not node1.is_foreach() + and not node2.is_foreach() + and len(node1.get_nodes()) + len(node2.get_nodes()) > config.max_fusion_size + ): + WhyNoFuse(node1, node2)("exceeds max fusion") + return False # heuristic not needed for correctness + + if scheduler.can_fusion_increase_peak_memory(node1, node2): + WhyNoFuse(node1, node2)("Fusion will increase peak memory") + return False + + if ( + config.max_fusion_unique_io_buffers is not None + and scheduler.fusion_prevent_too_many_reads_and_writes( + node1, + node2, + config.max_fusion_unique_io_buffers, + ) + ): + WhyNoFuse(node1, node2)("fusion_prevent_too_many_reads_and_writes") + return False + + return True + + @staticmethod + def can_fuse_vertical( + scheduler: Scheduler, + node1: BaseSchedulerNode, + node2: BaseSchedulerNode, + shared_data_score: int, + ) -> bool: + """Hook for heuristics to prevent vertical (producer/consumer) fusions""" + return True + + @staticmethod + def can_fuse_horizontal( + scheduler: Scheduler, + node1: BaseSchedulerNode, + node2: BaseSchedulerNode, + shared_data_score: int, + ) -> bool: + """Hook for heuristics to prevent horizontal (consumer/consumer) fusions""" + if MixOrderReduction.can_fuse(node1, node2): + # For mix order reduction, we disregard shared data or + # distance. + return True + if shared_data_score < config.score_fusion_memory_threshold: + WhyNoFuse(node1, node2)("score_fusion_memory_threshold") + return False + if scheduler.are_long_distant_nodes(node1, node2): + WhyNoFuse(node1, node2)( + "Nodes are too far away. Fusing them may increase peak memory." + ) + return False + return True + + @staticmethod + def score_fusion( + scheduler: Scheduler, + node1: BaseSchedulerNode, + node2: BaseSchedulerNode, + ) -> Sortable: + """ + Assign a score (higher comes first) to the fusion of node1 and node2. + When different fusions conflict with each other, this is the way we + decide what order to run them in. + + Our current score is based on: + - The type of fusion (template/reduction/etc) + - Estimate of the saved memory operations + - Fusions closer together in original graph order + """ + + memory_score, buffer_overlap_score, is_mix_order_reduction = typing.cast( + tuple[int, int, bool], + scheduler.score_fusion_memory( + node1, node2, return_is_mix_order_reduction=True + ), + ) + proximity_score = -max( + abs(node1.min_order - node2.max_order), + abs(node2.min_order - node1.max_order), + ) + + # prologue fusion always last + if node2.is_template(): + template_score = 0 + else: + template_score = 1 + ( + (node1.is_template() == config.epilogue_fusion_first) + and memory_score > 0 + ) + + type_score = node1.is_reduction() == node2.is_reduction() and memory_score > 0 + + return FusionScore( + template_score, + type_score, + memory_score, + buffer_overlap_score, + proximity_score, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codecache.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codecache.py new file mode 100644 index 0000000000000000000000000000000000000000..75f3c6316be661ca48a362fdda9a1164b4c75e78 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codecache.py @@ -0,0 +1,4672 @@ +from __future__ import annotations + +import base64 +import copyreg +import dataclasses +import functools +import hashlib +import importlib +import importlib.resources +import io +import itertools +import json +import logging +import os +import pickle +import pkgutil +import platform +import re +import shlex +import shutil +import struct +import subprocess +import sys +import tempfile +import textwrap +import threading +import warnings +from bisect import bisect_right +from copy import copy +from ctypes import c_void_p, CDLL, cdll +from datetime import timedelta +from functools import lru_cache, partial +from pathlib import Path +from tempfile import _TemporaryFileWrapper +from time import time, time_ns +from types import ModuleType +from typing import Any, cast, Generic, Literal, NoReturn, TYPE_CHECKING, TypeVar +from typing_extensions import override, Self + +import torch +import torch._library.opaque_object as opaque_object +import torch.distributed as dist +from torch import SymInt, Tensor +from torch._dynamo.device_interface import get_interface_for_device +from torch._dynamo.exc import SkipFrame +from torch._dynamo.utils import ( + CompileEventLogger, + counters, + dynamo_timed, + get_metrics_context, +) +from torch._inductor import config, exc, metrics +from torch._inductor.codegen.common import ( + custom_backend_codegen_configs, + custom_backend_passes, + init_backend_registration, +) +from torch._inductor.codegen.cuda import compile_utils as cuda_compile_utils +from torch._inductor.codegen.rocm.compile_command import ( + rocm_compile_command, + rocm_compiler, +) +from torch._inductor.compile_worker.utils import in_toplevel_process +from torch._inductor.cpp_builder import ( + _LINKER_SCRIPT, + _set_gpu_runtime_env, + _TORCH_PATH, + batch_convert_cubins_to_obj, + convert_cubin_to_obj, + CppBuilder, + CppOptions, + CppTorchDeviceOptions, + get_compiler_version_info, + get_cpp_compiler, + get_ld_and_objcopy, + get_name_and_dir_from_output_file_path, + normalize_path_separator, + run_asm_build_object, +) +from torch._inductor.cpu_vec_isa import pick_vec_isa +from torch._inductor.custom_graph_pass import ( + CustomGraphModulePass, + CustomGraphPass, + CustomGraphPassType, + CustomPartitionerFn, + CustomPartitionerFnType, +) +from torch._inductor.freezing_utils import has_frozen_params, is_frozen_param +from torch._inductor.runtime.compile_tasks import _reload_python_module +from torch._inductor.runtime.runtime_utils import cache_dir, default_cache_dir +from torch._inductor.utils import ( + ALIGN_BYTES, + clear_on_fresh_cache, + determine_aoti_mmap_flags, + is_linux, + is_windows, + XPU_KERNEL_FORMAT, +) +from torch._library.fake_class_registry import FakeScriptObject +from torch._library.opaque_object import is_opaque_reference_type +from torch._logging import trace_structured +from torch._subclasses.fake_tensor import ( + extract_tensor_metadata, + FakeTensor, + TensorMetadata, +) +from torch._utils_internal import log_cache_bypass +from torch.compiler import config as cconfig +from torch.compiler._cache import ( + CacheArtifact, + CacheArtifactFactory, + CacheArtifactManager, +) +from torch.export.pt2_archive._package_weights import TensorProperties, Weights +from torch.export.pt2_archive.constants import CUSTOM_OBJ_FILENAME_PREFIX +from torch.fx.experimental.symbolic_shapes import ( + guarding_hint_or_throw, + has_guarding_hint, + ShapeEnv, +) +from torch.utils._ordered_set import OrderedSet + +from .output_code import CompiledFxGraph +from .remote_cache import create_cache +from .runtime import autotune_cache +from .runtime.autotune_cache import AutotuneCacheBundler +from .triton_bundler import TritonBundler +from .virtualized import V + + +T = TypeVar("T") + +if TYPE_CHECKING: + from collections.abc import Callable, Generator, KeysView, Sequence + from concurrent.futures import Future + + from .compile_fx import _CompileFxKwargs + from .cpp_builder import BuildOptionsBase + from .graph import GraphLowering + from .ir import ChoiceCaller + from .output_code import CompiledFxGraphConstants, OutputCode + from .remote_cache import JsonDataTy, RemoteCache + from .runtime.hints import HalideInputSpec, HalideMeta + from .runtime.triton_heuristics import CachingAutotuner + from .utils import InputType + + +_IS_WINDOWS = sys.platform == "win32" +LOCK_TIMEOUT = config.file_lock_timeout + +output_code_log = torch._logging.getArtifactLogger(__name__, "output_code") +autotuning_log = torch._logging.getArtifactLogger(__name__, "autotuning") +log = logging.getLogger(__name__) + + +def get_cpp_wrapper_cubin_path_name() -> str: + return "cubin_path" if torch.version.hip is None else "hsaco_path" + + +def get_kernel_bin_format(device: str) -> str: + if device == "cuda": + return "cubin" if torch.version.hip is None else "hsaco" + elif device == "xpu": + return XPU_KERNEL_FORMAT + else: + return "" + + +def get_device_information(device_type: str) -> dict[str, str]: + """ + Gets all the current device information used to compile the .so. + """ + metadata: dict[str, str] = { + "AOTI_PLATFORM": sys.platform, + "AOTI_MACHINE": platform.machine(), + "AOTI_CPU_ISA": str(torch._inductor.cpu_vec_isa.pick_vec_isa()).upper(), + "AOTI_COMPUTE_CAPABILITY": str( + get_interface_for_device(device_type).get_compute_capability() + ), + } + return metadata + + +class CacheBase: + @staticmethod + @functools.cache + def get_system() -> dict[str, Any]: + from torch._inductor.runtime.triton_compat import HAS_TRITON, triton_key + + if HAS_TRITON: + # Use triton_key instead of triton.__version__ as the version + # is not updated with each code change + triton_version = triton_key() + else: + triton_version = None + + try: + system: dict[str, Any] = { + "device": {"name": None}, + "version": { + "triton": triton_version, + }, + } + device_properties = torch.cuda.get_device_properties( + torch.cuda.current_device() + ) + if torch.version.cuda is not None: + system["device"]["name"] = device_properties.name + system["version"]["cuda"] = torch.version.cuda + else: + system["device"]["name"] = device_properties.gcnArchName + system["version"]["hip"] = torch.version.hip + except (AssertionError, RuntimeError): + # If cuda is not installed, none of the above config is relevant. + system = {} + + system["hash"] = hashlib.sha256( + json.dumps(system, sort_keys=True).encode("utf-8") + ).hexdigest() + + return system + + @staticmethod + @clear_on_fresh_cache + @functools.cache + def get_local_cache_path() -> Path: + return Path(os.path.join(cache_dir(), "cache", CacheBase.get_system()["hash"])) + + def __init__(self) -> None: + self.system = CacheBase.get_system() + + def get_local_cache(self) -> dict[str, Any]: + local_cache_path = self.get_local_cache_path() + if not local_cache_path.is_file(): + return {} + with open(local_cache_path) as local_cache_fp: + local_cache = json.load(local_cache_fp) + return local_cache["cache"] + + def update_local_cache(self, local_cache: dict[str, Any]) -> None: + local_cache_path = self.get_local_cache_path() + write_atomic( + str(local_cache_path), + json.dumps({"system": self.system, "cache": local_cache}, indent=4), + make_dirs=True, + ) + + +class LocalCache(CacheBase): + def lookup(self, *keys: str) -> dict[str, Any] | None: + cache = self.get_local_cache() + + sub_cache = cache + for key in keys: + if key in cache: + sub_cache = cache[key] + else: + return None + + return sub_cache + + def set_value(self, *keys: str, value: Any) -> None: + cache = self.get_local_cache() + + sub_cache = cache + for key in keys[0:-1]: + sub_cache.setdefault(key, {}) + sub_cache = sub_cache[key] + sub_cache[keys[-1]] = value + + self.update_local_cache(cache) + + +class PersistentCache(CacheBase): + def lookup( + self, + choices: list[ChoiceCaller], + op: str, + inputs: str, + benchmark: Callable[[Any], dict[ChoiceCaller, float]] | None, + hint_override: int | None = None, + ) -> dict[ChoiceCaller, float]: + """ + Check to see if we have benchmarked the given choice callers. For each + choice caller: + + 1. Check local_cache[op][inputs][choice][precision], return benchmark if cached. + 2. If benchmark is not None: + a. `max_autotune_gemm=True`: benchmark the choice, update + local_cache[op][inputs][choice], and return the benchmark. + b. `max_autotune_gemm=False`: don't benchmark the choice, return nothing. + """ + precision = torch.get_float32_matmul_precision() + cache_key = f"{inputs}_{hint_override}" if hint_override is not None else inputs + + timings = {} + + def check_cache(cache: dict[str, Any]) -> bool: + """Check if `cache` contains data for all the choices""" + hit = True + for choice in choices: + choice_hash = choice.hash_key() + if choice_hash in cache.get(op, {}).get(cache_key, {}).get( + precision, {} + ): + # cache hit + timings[choice] = cache[op][cache_key][precision][choice_hash] + else: + # cache miss + hit = False + break + return hit + + local_cache = self.get_local_cache() if config.autotune_local_cache else {} + if (not check_cache(local_cache)) and (benchmark is not None): + # re-benchmark everything to try to get consistent numbers from the same machine + timings = benchmark(choices) + assert all(choice in timings for choice in choices) + local_cache.setdefault(op, {}) + local_cache[op].setdefault(cache_key, {}).setdefault(precision, {}) + for choice, timing in timings.items(): + local_cache[op][cache_key][precision][choice.hash_key()] = timing + + self.update_local_cache(local_cache) + + return timings + + +def get_lock_dir() -> str: + lock_dir = os.path.join(cache_dir(), "locks") + if not os.path.exists(lock_dir): + os.makedirs(lock_dir, exist_ok=True) + return lock_dir + + +def sha256_hash(data: bytes) -> str: + # [:51] to strip off the "Q====" suffix common to every hash value. + return base64.b32encode(hashlib.sha256(data).digest())[:51].decode("utf-8").lower() + + +def code_hash(code: str | bytes, extra: str | bytes = "") -> str: + hashing_str = code if isinstance(code, bytes) else code.encode("utf-8") + if extra: + extra_b = extra if isinstance(extra, bytes) else extra.encode("utf-8") + hashing_str = hashing_str + b"||" + extra_b + return "c" + sha256_hash(hashing_str) + + +def get_path( + basename: str, extension: str, specified_dir: str = "" +) -> tuple[str, str, str]: + if specified_dir: + if os.path.isabs(specified_dir): + subdir = specified_dir + else: + subdir = os.path.join(cache_dir(), specified_dir) + else: + subdir = os.path.join(cache_dir(), basename[1:3]) + path = os.path.join(subdir, f"{basename}.{extension}") + return basename, subdir, path + + +def get_hash(content: str | bytes, extra: str = "", hash_type: str = "code") -> str: + if hash_type in {"amdgcn", "code", "ptx", "spv"}: + return code_hash(content, extra) + if hash_type in {"cubin", "hsaco", XPU_KERNEL_FORMAT}: + return code_hash(repr(content)) + raise AssertionError(f"Unknown hash type {hash_type}") + + +class WritableTempFile: + """ + Avoid "Permission denied error" on Windows: + with tempfile.NamedTemporaryFile("w", suffix=".gv") as temp_file: + # Not writable on Windows: + # https://docs.python.org/3/library/tempfile.html#tempfile.NamedTemporaryFile + + Example: + with WritableTempFile("w", suffix=".gv") as temp_file: + tree.to_dotfile(temp_file.name) + """ + + def __init__( + self, mode: str = "w", *, encoding: Any = None, suffix: Any = None + ) -> None: + self.mode = mode + self.encoding = encoding + self.suffix = suffix + + def __enter__(self) -> _TemporaryFileWrapper[Any]: + self.temp_file = tempfile.NamedTemporaryFile( + self.mode, encoding=self.encoding, suffix=self.suffix, delete=False + ) + return self.temp_file + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self.temp_file.close() + try: + os.unlink(self.temp_file.name) + except OSError as e: + if _IS_WINDOWS: + # On Windows, some case temp file is opened and fail to unlink. Need to ignore it. + pass + else: + raise e + + +def write( + content: str | bytes, + extension: str, + extra: str = "", + hash_type: str = "code", + specified_dir: str = "", + key: str | None = None, +) -> tuple[str, str]: + if key is None: + # use striped content to compute hash so we don't end up with different + # hashes just because the content begins/ends with different number of + # spaces. + key = get_hash(content.strip(), extra, hash_type) + basename, _subdir, path = get_path(key, extension, specified_dir) + if not os.path.exists(path): + write_atomic(path, content, make_dirs=True) + return basename, path + + +def write_text(text: str) -> str: + """ + Write the `text` to a file and return the path computed based on the hash. + """ + return write(text, "txt")[1] + + +def write_atomic( + path_: str, + content: str | bytes, + make_dirs: bool = False, + encode_utf_8: bool = False, +) -> None: + # Write into temporary file first to avoid conflicts between threads + # Avoid using a named temporary file, as those have restricted permissions + assert isinstance(content, (str, bytes)), ( + "Only strings and byte arrays can be saved in the cache" + ) + path = Path(path_) + if make_dirs: + path.parent.mkdir(parents=True, exist_ok=True) + tmp_path = path.parent / f".{os.getpid()}.{threading.get_ident()}.tmp" + write_mode = "w" if isinstance(content, str) else "wb" + with tmp_path.open(write_mode, encoding="utf-8" if encode_utf_8 else None) as f: + f.write(content) + try: + tmp_path.rename(target=path) + except FileExistsError: + if not _IS_WINDOWS: + raise + # On Windows file exist is expected: https://docs.python.org/3/library/pathlib.html#pathlib.Path.rename + # Below two lines code is equal to `tmp_path.rename(path)` on non-Windows OS. + # 1. Copy tmp_file to Target(Dst) file. + shutil.copy2(src=tmp_path, dst=path) + # 2. Delete tmp_file. + os.remove(tmp_path) + + +@dataclasses.dataclass +class TensorMetadataAndValues: + """ + TensorMetadata plus the elements as a list of raw values. + Used for hashing inlined constants. + """ + + tensor_metadata: TensorMetadata + values: list[Any] + + +def _ident(x: T) -> T: + return x + + +def extract_tensor_metadata_for_cache_key(t: Tensor) -> TensorMetadata: + """ + Extracts the tensor metadata and removes fields of the TensorMetadata + that are not needed for caching + """ + meta = extract_tensor_metadata(t) + if not hasattr(t, "_is_inductor_static"): + meta = dataclasses.replace(meta, storage_offset=0, storage_bytes=None) + + return meta + + +class FxGraphCachePickler(pickle.Pickler): + """ + Custom pickler to customize the pickling of some objects (Tensors), only for the + purpose of computing a hash for keying into the FxGraphCache. Tensors contain + objects that don't pickle and/or vary between runs, and we want to capture the + data that allow us to compute a stable, but safe hash. + """ + + def __init__( + self, + gm: torch.fx.GraphModule, + has_user_defined_triton_kernels: bool = False, + device_id_agnostic: bool = False, + ) -> None: + """ + Create an FX graph pickler. If include_non_inlined=True, then pickling will + include the _values_ for all Tensors. (Note that any tensors are constants + attached as attributes to the GraphModule). Otherwise, pickling will include + only the metadata for these tensors. + + If device_id_agnostic=True, device indices in TensorMetadata are normalized + to 0, so that the same graph on different GPUs produces identical bytes. + """ + self._stream = io.BytesIO() + self._device_id_agnostic = device_id_agnostic + super().__init__(self._stream) + + self.dispatch_table = copyreg.dispatch_table.copy() + self.dispatch_table.update( + { + FakeTensor: functools.partial(self._reduce_fake_tensor), + torch.Tensor: functools.partial(self._reduce_tensor), + torch.nn.parameter.Parameter: functools.partial(self._reduce_tensor), + torch.SymInt: functools.partial(self._reduce_symint), + torch.SymBool: functools.partial(self._reduce_symbool), + torch.fx.experimental._backward_state.BackwardState: functools.partial( + self._reduce_unsupported + ), + FakeScriptObject: functools.partial(self._reduce_fake_script_object), + } + ) + if has_user_defined_triton_kernels: + # Need to use runtime type as GraphModule generates a singleton in __new__ function + self.dispatch_table[gm.__class__] = functools.partial( + self._reduce_graph_module + ) + + # Run with pickler.fast so it doesn't intern strings, making the hash result more predictable + # TODO: pickler.fast is technically deprecated. Will this work on new python versions? + self.fast = True + + def _reduce_fake_tensor( + self, t: Tensor + ) -> tuple[Callable[[T], T], tuple[TensorMetadata]]: + """ + Custom reducer to pickle FakeTensors. + """ + metadata = extract_tensor_metadata_for_cache_key(t) + if self._device_id_agnostic: + metadata = dataclasses.replace( + metadata, device=torch.device(metadata.device.type, 0) + ) + return (_ident, (metadata,)) + + def _reduce_tensor( + self, t: Tensor + ) -> tuple[Callable[[T], T], tuple[TensorMetadata | TensorMetadataAndValues]]: + """ + Custom reducer to pickle Tensors. If we see tensors, we know they're constants + stored as attributes on the GraphModule. + """ + from .graph import GraphLowering + + if t.is_mkldnn: + # TODO: These tensors don't currently pickle, so we can't cache a compiled + # graph containing them. Just fail now. If mkldnn tensors get pickling + # support, we can remove this. + raise BypassFxGraphCache("mkldnn tensors unpickleable") + + metadata = extract_tensor_metadata_for_cache_key(t) + if self._device_id_agnostic: + metadata = dataclasses.replace( + metadata, device=torch.device(metadata.device.type, 0) + ) + + # If this is a non-inlined frozen parameter, we consider the metadata only. + if is_frozen_param(t) and not GraphLowering.can_inline_constant(t): + return (_ident, (metadata,)) + + # Very large tensors will be expensive to copy to cpu and hash. Let's at least + # report any slowness. + start = time() + values = t.tolist() + elapsed = time() - start + if elapsed > 1.0: + warnings.warn( + f"FX graph cache copying of a large constant took {elapsed:.1}s. " + "Please file an issue." + ) + + return (_ident, (TensorMetadataAndValues(metadata, values),)) + + def _reduce_symint(self, s: SymInt) -> tuple[Callable[[T], T], tuple[str]]: + """ + Custom reducer to pickle SymInts. + """ + # For hashing purposes, we only care about the name of the symbol and not the + # backed value. We evaluate guards stored with a cached graph to ensure a cached + # entity with SymInt args is safe to reuse. + return (_ident, (str(s),)) + + def _reduce_symbool(self, s: torch.SymBool) -> tuple[Callable[[T], T], tuple[str]]: + """ + Custom reducer to pickle SymBools. + """ + # Same approach as _reduce_symint: use the string representation for + # hashing. Guards ensure correctness on cache reload. + return (_ident, (str(s),)) + + def _reduce_unsupported(self, s: Any) -> NoReturn: + """ + Custom reducer to handle any objects that we don't support and therefore + raise to bypass caching. + """ + raise BypassFxGraphCache("Reduce unsupported") + + def _reduce_graph_module( + self, gm: torch.fx.GraphModule + ) -> tuple[Any, tuple[dict[str, Any], str]]: + """ + Custom reducer for graph module to handle irrelevant data for user + defined triton kernels + Essentially what we are doing here is a huge hack where user defined + triton kernel contain a dynamo time side table and the arguments to the + call_function are indices into this side table. These arguments are not + for hashing purposes since we included the source code into the cache + key and the numbers are prone to give false negatives due to ordering. + """ + fn, (data, imports) = gm.__reduce__() + code = data["_code"] + code = re.sub(r"kernel_idx = \d+", "", code) + code = re.sub(r"constant_args_idx = \d+", "", code) + data["_code"] = code + return fn, (data, imports) + + def _reduce_fake_script_object( + self, t: FakeScriptObject + ) -> tuple[Callable[..., Any], tuple[Any, ...]]: + if t.real_obj is not None: + cls = type(t.real_obj) + # This is the only case where I'm sure it's cache safe. + # I have not worked out the details for everything else + # but I'm sure we could + if ( + opaque_object.is_opaque_type(cls) + and opaque_object.should_hoist(cls) + and not opaque_object.has_members(cls) + ): + return (_ident, (t.script_class_name,)) + return (_ident, (t.wrapped_obj, t.script_class_name, t.real_obj)) + + def dumps(self, obj: Any) -> bytes: + """ + Pickle an object and return a byte string. + """ + try: + self.dump(obj) + return self._stream.getvalue() + except (TypeError, AttributeError, pickle.PicklingError, ValueError) as e: + # Some configs options may not pickle. + log.warning("Failed to pickle cache key", exc_info=True) + raise BypassFxGraphCache("Failed to pickle cache key") from e + except RuntimeError as e: + # pybind11 raises RuntimeError with message like: + # " is not pickleable." + if "pybind11" in str(e) and "is not pickleable" in str(e): + log.warning("Failed to pickle cache key", exc_info=True) + raise BypassFxGraphCache("Failed to pickle cache key") from e + raise + finally: + # Reset our stream for the next dump. + self._stream.seek(0) + self._stream.truncate(0) + + def get_hash(self, obj: Any) -> str: + """ + Serialize an object and return a hash of the bytes. + """ + serialized_data = self.dumps(obj) + return sha256_hash(serialized_data) + + def debug_lines(self, inp: FxGraphHashDetails) -> list[str]: + """ + Get a printable string describing in more detail all the attributes + comprising an object. Useful for debugging when one graph hashes + to a different value than another. + """ + + def get_str(obj: Any) -> str: + if isinstance(obj, torch.Tensor): + return str(extract_tensor_metadata_for_cache_key(obj)) + elif isinstance(obj, bytes): + val = obj.decode("utf-8", errors="replace") + return val if len(val) <= 1024 else val[:1024] + "..." + elif type(obj) in self.dispatch_table: + # Run the reducer on the object + return str(self.dispatch_table[type(obj)](obj)[1]) + else: + return str(obj) + + lines = [] + for attr, obj in vars(inp).items(): + if isinstance(obj, list): + for ii in range(len(obj)): + h = self.get_hash(obj[ii]) + lines.append(f"[{h}] {attr}[{ii}]: {get_str(obj[ii])}") + elif isinstance(obj, dict): + for k, v in obj.items(): + h = self.get_hash(v) + lines.append(f"[{h}] {attr}[{k}]: {get_str(v)}") + else: + h = self.get_hash(obj) + lines.append(f"[{h}] {attr}: {get_str(obj)}") + return lines + + +def build_code_hash( + roots: list[str] | None, prefix: str, hasher: hashlib._Hash +) -> None: + for lib in sorted(pkgutil.iter_modules(roots, prefix), key=lambda x: x.name): + spec = lib.module_finder.find_spec(lib.name, None) + assert spec is not None + module = spec.origin + assert module is not None + with open(module, "rb") as f: + hasher.update(spec.name.encode("utf-8")) + hasher.update(f.read()) + if lib.ispkg: + # need to also hash submodules + build_code_hash(spec.submodule_search_locations, f"{spec.name}.", hasher) + + +def torch_key_cache(func: Callable[[], bytes]) -> Callable[[], bytes]: + """ + This function is a reimplementation of functools.lru_cache with a + set function that allows prepopulating the cache. + """ + # Use list for reference semantics + _cache: list[bytes] = [] + + def wrapper() -> bytes: + if len(_cache) == 0: + _cache.append(func()) + return _cache[0] + + def set_val(val: bytes) -> None: + assert len(_cache) == 0 + _cache.append(val) + + def clear() -> None: + _cache.clear() + + wrapper.set = set_val # type: ignore[attr-defined] + wrapper.clear = clear # type: ignore[attr-defined] + return wrapper + + +@torch_key_cache +def torch_key() -> bytes: + """ + Compute a key that contains relevant information about torch source files + """ + with dynamo_timed("inductor_codecache_torch_key", log_pt2_compile_event=False): + if not config.is_fbcode(): + + def get_code_hash(root: str) -> bytes: + # This function isn't meant to be used outside of torch_key, just a + # helper for clarity. Instead, use torch_key() directly when you need + # a hash representing the state of the source code. + extra_files = ( + "codegen/aoti_runtime/interface.cpp", + "script.ld", + ) + inductor_root = os.path.dirname(__file__) + extra_files = [os.path.join(inductor_root, x) for x in extra_files] + hasher = hashlib.sha256() + hasher.update(torch.__version__.encode("utf-8")) + build_code_hash([root], "", hasher) + for path in extra_files: + if os.path.exists(path): + with open(path, "rb") as f: + hasher.update(f.read()) + return hasher.digest() + + return get_code_hash(_TORCH_PATH) + + from libfb.py import parutil + + return parutil.get_file_contents("torch/src_hash.txt").rstrip().encode("ascii") + + +def get_inductor_root() -> str: + return os.path.dirname(__file__) + + +@dataclasses.dataclass +class OrderedSetHolder: + """ + See FxGraphHashDetails. Holds a sorted list to support stable hashing + of set kwargs. + """ + + items: list[Any] + + +class BypassFxGraphCache(Exception): + """ + Exception to indicate that the FxGraphCache should be bypassed. + """ + + +_warned_pre_grad_pass_missing_uuid: OrderedSet[str] = OrderedSet() + + +def resolve_pre_grad_pass_timing() -> Literal["early", "late"]: + """Resolve the effective pre-grad pass timing from the config. + + "default" is resolved based on whether the custom pass provides a UUID: + passes with a UUID (or no custom pass) run "late" (after cache lookup), + passes without a UUID run "early" (before cache lookup). + + Raises RuntimeError if a custom pass without a UUID is explicitly set to + run "late", since the cache key cannot account for it. + """ + timing: Literal["early", "late", "default"] = config.pre_grad_pass_timing + custom_pass = config.pre_grad_custom_pass + has_uuid = ( + custom_pass + and isinstance(custom_pass, CustomGraphPass) + and custom_pass.uuid() is not None + ) + + if timing == "default": + supports_late = custom_pass is None or has_uuid + timing = "late" if supports_late else "early" + if timing == "early" and custom_pass: + pass_name = type(custom_pass).__qualname__ + if pass_name not in _warned_pre_grad_pass_missing_uuid: + _warned_pre_grad_pass_missing_uuid.add(pass_name) + log.warning( + "pre_grad_custom_pass %s does not implement uuid(); " + "falling back to early timing (pre-grad pass cache will be bypassed). " + "Implement uuid() on your CustomGraphPass to enable caching.", + pass_name, + ) + CompileEventLogger.try_add_pt2_compile( + "backend_compile", + pre_grad_pass_missing_uuid=True, + pre_grad_pass_name=pass_name, + ) + + if timing == "late" and custom_pass and not has_uuid: + raise RuntimeError( + "pre_grad_custom_pass must implement uuid() to run late " + "(after cache lookup). Either implement uuid() or set " + "pre_grad_pass_timing to 'early'." + ) + + return timing + + +@dataclasses.dataclass +class HashableOpaqueValue: + ordinal: int + + +class FxGraphHashDetails: + """ + Object to capture all the details for a compiled FX graph relevant to computing + a safe and stable cache key. + """ + + # Excluded kwargs param that are not stable between runs or that + # don't affect compiled output (like compile_region_name which is + # just a debug label). + EXCLUDED_KWARGS = ["graph_id", "compile_region_name"] + + def __init__( + self, + gm: torch.fx.GraphModule, + example_inputs: Sequence[InputType], + fx_kwargs: _CompileFxKwargs, + inputs_to_check: Sequence[int], + ) -> None: + self.gm = gm + # Replace opaque references with hashable ordinals. What's important + # is that if the same reference appears twice then it's the same hash + # value for each. + processed_inputs: list[InputType | HashableOpaqueValue] = [] + seen_opaques: dict[int, HashableOpaqueValue] = {} + for inp in example_inputs: + if is_opaque_reference_type(type(inp)): + if id(inp) not in seen_opaques: + seen_opaques[id(inp)] = HashableOpaqueValue(len(seen_opaques)) + processed_inputs.append(seen_opaques[id(inp)]) + else: + processed_inputs.append(inp) + self.example_inputs = processed_inputs + self.cache_key_tag = cconfig.cache_key_tag + + # Order kwargs so hashing is stable to changes in kwarg order. Although + # it's technically a _CompileFxKwargs we don't actually need it typed as + # such since we're just using it to generate a hash. + self.fx_kwargs: dict[str, object] = {} + for k, v in sorted(fx_kwargs.items()): + if k not in self.EXCLUDED_KWARGS: + if type(v) in (set, OrderedSet): # noqa: set_linter + # Special case to handle set params. Python sets can't be + # ordered, so sort the elements and store them in a proxy. + self.fx_kwargs[k] = OrderedSetHolder(sorted(v)) # type: ignore[call-overload] + else: + self.fx_kwargs[k] = v + + from torch._higher_order_ops.triton_kernel_wrap import ( + kernel_side_table, + triton_kernel_wrapper_functional, + triton_kernel_wrapper_mutation, + ) + from torch._inductor.codegen.wrapper import ( + user_defined_triton_kernel_transitive_closure_source_code, + ) + + # Node meta will not be part of gm's reduce function, so lets remember + # the kernel source code separately + self.user_defined_triton_source: list[Any] = [] + if gm is not None: + for module in gm.modules(): + if not isinstance(module, torch.fx.GraphModule): + continue + for node in itertools.chain( + module.graph.find_nodes( + op="call_function", target=triton_kernel_wrapper_functional + ), + module.graph.find_nodes( + op="call_function", target=triton_kernel_wrapper_mutation + ), + ): + from triton.runtime.autotuner import Autotuner + + kernel = kernel_side_table.get_kernel(node.kwargs["kernel_idx"]) + configs = None + if isinstance(kernel, Autotuner): + if kernel.configs: + configs = str( + sorted( + sorted(str(kv) for kv in c.all_kwargs().items()) + for c in kernel.configs + ) + ) + kernel = kernel.fn + + kernel_source = ( + user_defined_triton_kernel_transitive_closure_source_code( + kernel + ) + ) + constant_args = kernel_side_table.get_constant_args( + node.kwargs["constant_args_idx"] + ) + self.user_defined_triton_source.append( + (kernel_source, constant_args, configs) + ) + + # Alignment checks + self.inputs_to_check = inputs_to_check + + no_tensor_inputs = not any(isinstance(x, torch.Tensor) for x in example_inputs) + # This device index is usually already encoded by the device of the inputs + # but fx graphs don't necessarily have tensor inputs. If there aren't any, + # we need to guard on the device index in case we allocate cuda tensors + if no_tensor_inputs and torch.accelerator.is_available(): + self.default_cuda_device_index = torch.accelerator.current_device_index() + + # 'Deterministic algorithms' can affect codegen via lowering to cuda kernels. + self.deterministic_algorithms_settings = ( + torch.are_deterministic_algorithms_enabled(), + torch.is_deterministic_algorithms_warn_only_enabled(), + torch.utils.deterministic.fill_uninitialized_memory, # type: ignore[attr-defined] + ) + + # Provenance tracking level affects whether provenance data is stored + # in the CompiledFxGraph, so it must be part of the cache key. + # Note: the "trace" prefix is excluded from _cache_config_ignore_prefix, + # so we add this explicitly. + self.provenance_tracking_level = config.trace.provenance_tracking_level + + # Global settings affecting matmul codegen. + self.cuda_matmul_settings = ( + torch.backends.cuda.matmul.fp32_precision, + torch.backends.cuda.matmul.allow_fp16_reduced_precision_reduction, + torch.backends.cuda.matmul.allow_bf16_reduced_precision_reduction, + ) + + # Include cudagraph annotation in cache key only when it changes + # behavior. When both fwd and bwd are overridden to the same value, + # normalize to a simple boolean (equivalent to flipping the config). + # When fwd and bwd differ, include the full annotation. + if gm is not None: + annotation = gm.meta.get("cudagraph_annotation") + if annotation is not None: + default = config.triton.cudagraphs + if annotation.fwd == annotation.bwd and annotation.fwd is not None: + if annotation.fwd != default: + self.cudagraph_override = annotation.fwd + elif (annotation.fwd is not None and annotation.fwd != default) or ( + annotation.bwd is not None and annotation.bwd != default + ): + self.cudagraph_annotation = annotation + + # Also hash on various system info (including the triton compiler version). + self.torch_version = torch_key() + self.system_info = CacheBase.get_system() + self.inductor_config = config.save_config_portable(ignore_private_configs=False) + # Custom passes should provide an ID to hash when they run late (after cache lookup). + if resolve_pre_grad_pass_timing() != "early": + self.pre_grad_custom_pass = self._get_custom_pass_detail( + config.pre_grad_custom_pass + ) + self.post_grad_custom_pre_pass = self._get_custom_pass_detail( + config.post_grad_custom_pre_pass + ) + # TODO: change to more holistic config rather than bundled_autograd_cache + self.precompile_enabled = torch._functorch.config.bundled_autograd_cache + self.post_grad_custom_post_pass = self._get_custom_pass_detail( + config.post_grad_custom_post_pass + ) + self.joint_custom_pre_pass = self._get_custom_pass_detail( + config.joint_custom_pre_pass + ) + self.joint_custom_post_pass = self._get_custom_pass_detail( + config.joint_custom_post_pass + ) + self._pre_fusion_custom_pass = self._get_custom_pass_detail_unsafe( + config._pre_fusion_custom_pass + ) + self._fuse_ddp_communication_passes = self._get_custom_pass_detail_unsafe( + config._fuse_ddp_communication_passes + ) + + # Register indcutor backends and custom passes and get their UUIDs. + init_backend_registration() + self.custom_backend_passes = tuple( + map(self._get_custom_pass_detail, custom_backend_passes.values()) + ) + + # Save custom inductor codegen configs + self.custom_backend_codegen_configs = { + device: custom_config.save_config_portable(ignore_private_configs=False) + for device, custom_config in custom_backend_codegen_configs.items() + if custom_config is not None + } + + # Register the custom partitioner function + self._custom_partitioner_fn = self._get_custom_partitioner_fn_detail( + config.custom_partitioner_fn + ) + + # Include hint overrides in the cache key because _reduce_symint + # only hashes symbol names, not hint values. + self.var_to_hint_override: dict[str, int] = {} + shape_env = FxGraphCache._get_shape_env() + if shape_env is not None and shape_env.var_to_hint_override: + self.var_to_hint_override = { + str(sym): val + for sym, val in sorted( + shape_env.var_to_hint_override.items(), key=lambda x: str(x[0]) + ) + } + + # This is mainly added to handle these two inductor configs, which are (unfortunately) + # sometimes cache safe: + # - _pre_fusion_custom_pass + # - _fuse_ddp_communication_passes + # Their types can be found in `torch/_inductor/config.py`, but: + # - if they are string names, we can cache them safely (one is by default) + # - if any of them are set to custom callables, we will need to cache miss + # Future work is for someone to find any places where these functions are used + # and force them to be of type CustomGraphPass, so we can guarantee serialization. + def _get_custom_pass_detail_unsafe(self, custom_pass: Any) -> Any | None: + if not custom_pass: + return None + if isinstance(custom_pass, list): + return [self._get_custom_pass_detail_unsafe(x) for x in custom_pass] + if isinstance(custom_pass, str): + return custom_pass + if isinstance(custom_pass, CustomGraphPass): + return custom_pass.uuid() + if callable(custom_pass): + # Returning None is safe here because we raise an explicit bypass error + # later if we detect these passes are set to callables + return None + raise AssertionError(f"unknown config type: {str(type(custom_pass))}") + + def _get_custom_pass_detail( + self, custom_pass: CustomGraphPassType | CustomGraphModulePass + ) -> Any | None: + if not custom_pass: + return None + assert isinstance(custom_pass, (CustomGraphPass, CustomGraphModulePass)) + return custom_pass.uuid() + + def _get_custom_partitioner_fn_detail( + self, custom_partitioner_fn: CustomPartitionerFnType + ) -> Any | None: + if not custom_partitioner_fn: + return None + assert isinstance(custom_partitioner_fn, CustomPartitionerFn) + return custom_partitioner_fn.uuid() + + +def compiled_fx_graph_hash( + gm: torch.fx.GraphModule, + example_inputs: Sequence[InputType], + fx_kwargs: _CompileFxKwargs, + inputs_to_check: Sequence[int], +) -> tuple[str, list[str]]: + """ + Generate a unique hash of the FX graph for caching. + """ + details = FxGraphHashDetails(gm, example_inputs, fx_kwargs, inputs_to_check) + has_user_defined_triton_kernels = len(details.user_defined_triton_source) != 0 + pickler = FxGraphCachePickler(gm, has_user_defined_triton_kernels) + + # The prefix distinguishes among the other kinds of objects we + # cache in this module. + key = "f" + pickler.get_hash(details) + debug_lines = pickler.debug_lines(details) + debug_str = "\n".join(debug_lines) + log.debug(f"FX graph cache hash details for key {key}:\n{debug_str}") # noqa: G004 + return key, debug_lines + + +def add_ephemeral_timeout_increase_for_distributed(time_saved_ns: int) -> int: + """ + Ephemerally increases the NCCL timeout when compiling for a distributed job + Returns amount of seconds increased + """ + if not torch.distributed.is_available() or not torch.distributed.is_initialized(): + return 0 + + increased_timeout_sec = int(time_saved_ns // 1e9) # convert to seconds + + if config.is_fbcode(): + fudge_factor = torch._utils_internal.justknobs_getval_int( + "pytorch/remote_cache:ephemeral_timeout_fudge_factor_percentage" + ) + log.info( + "Ephemeral NCCL timeout increase fudge factor %d and original increase value %d", + fudge_factor, + increased_timeout_sec, + ) + increased_timeout_sec += int(increased_timeout_sec * fudge_factor / 100) + + log.info("Increasing NCCL timeout by %d", increased_timeout_sec) + dist.distributed_c10d._add_ephemeral_timeout_for_all_pgs( + timedelta(seconds=increased_timeout_sec) + ) + return increased_timeout_sec + + +class GuardedCache(Generic[T]): + """ + Mixin for caches that have guards associated with their entries. + """ + + @classmethod + def _get_tmp_dir_for_key(cls: type[GuardedCache[T]], _key: str) -> str: + raise NotImplementedError("Implement _get_tmp_dir_for_key on parent class") + + @classmethod + def _record_result( + cls: type[GuardedCache[T]], + key: str, + local_hit: bool, + local_miss: bool, + remote_hit: bool, + remote_miss: bool, + ) -> None: + raise NotImplementedError("Implement _record_result on parent class") + + @classmethod + def iterate_over_candidates( + cls: type[GuardedCache[T]], + local: bool, + remote_cache: RemoteCache[JsonDataTy] | None, + key: str, + ) -> Generator[tuple[T, bytes, bool], None, None]: + if local: + subdir = cls._get_tmp_dir_for_key(key) + if os.path.exists(subdir): + for path in sorted(os.listdir(subdir)): + if path.startswith("."): + continue # Skip temp files from concurrent write_atomic() calls + try: + with open(os.path.join(subdir, path), "rb") as f: + content = f.read() + yield pickle.loads(content), content, True + except Exception: + log.warning( + "fx graph cache unable to load compiled graph", + exc_info=True, + ) + + if remote_cache: + try: + if (cache_data := remote_cache.get(key)) is not None: + assert isinstance(cache_data, dict) + data = cache_data["data"] + assert isinstance(data, (str, bytes)) + content = base64.b64decode(data) + yield pickle.loads(content), content, False + except Exception: + log.warning( + "%s unable to load compiled graph", cls.__name__, exc_info=True + ) + + @classmethod + def find_guarded_entry( + cls: type[GuardedCache[T]], + key: str, + local: bool, + remote_cache: RemoteCache[JsonDataTy] | None, + evaluate_guards: Callable[[str, list[int] | list[torch.SymInt]], bool], + hints: list[int], + ) -> tuple[T | None, bytes | None, dict[str, str]]: + """ + Find the first cache entry in iterate_over_candidates that passes `evaluate_guards`. + + Args: + key: The cache key to look up + local: Whether to check the local cache + remote_cache: The remote cache to check, if any + evaluate_guards: Function that evaluates whether a guard passes the check, + given a list of hint values and the guard expression. + hints: List of symint hints paired with evaluate_guards + + Returns: + A tuple of (graph, pickled_content) if found, or (None, None) if not found + """ + graph = None + pickled_content = None + result_status = "full_miss" + sample_guards_expr = None + in_local = False + + # Iterate over any entries in the subdir for this key and evaluate + # guards to determine whether there's a hit. + + for candidate, content, in_local in cls.iterate_over_candidates( + local, remote_cache, key + ): + assert hasattr(candidate, "guards_expr") + if not candidate.guards_expr: # type: ignore[attr-defined] + # No guards to evaluate, so this is a hit. + graph = candidate + pickled_content = content + result_status = "hit" + break + + # Evaluate the guard expression in the current context. + # If there's not a cache hit, we don't want the evaluation to + # affect the current env, e.g., cause the creation of new guards, + # so we evaluate with the hints instead of the symbols. + hit = bool(evaluate_guards(candidate.guards_expr, hints)) # type: ignore[attr-defined] + if hit: + graph = candidate + pickled_content = content + result_status = "hit" + sample_guards_expr = candidate.guards_expr + break + else: + # At least one guard missed, log this + result_status = "guard_miss" + sample_guards_expr = candidate.guards_expr + + info = {"cache_status_detailed": result_status} + if sample_guards_expr is not None: + info["cache_status_guard_expr"] = sample_guards_expr + + # Record hits/misses for compilation event logging. The tricky part is that a + # remote hit would imply a local miss (if local caching is enabled). + local_hit = graph is not None and in_local + remote_hit = graph is not None and not in_local + local_miss = (graph is None or remote_hit) and local + remote_miss = graph is None and remote_cache is not None + cls._record_result( + key, + local_hit=local_hit, + local_miss=local_miss, + remote_hit=remote_hit, + remote_miss=remote_miss, + ) + + return graph, pickled_content, info + + @classmethod + def _filter_backed_symints( + cls: type[GuardedCache[T]], inputs: Sequence[InputType] + ) -> list[torch.SymInt]: + """ + Get the backed SymInt objects from the input list. Note that we can never + have guards that depend on unbacked symint. + """ + return [ + s for s in inputs if isinstance(s, torch.SymInt) and has_guarding_hint(s) + ] + + @classmethod + def _get_shape_env(cls: type[GuardedCache[T]]) -> ShapeEnv | None: + """ + Helper to get the shape env from the tracing context. + """ + ctx = torch._guards.TracingContext.try_get() + if not ctx or not ctx.fake_mode: + return None + return ctx.fake_mode.shape_env + + +@CacheArtifactFactory.register +class InductorCacheArtifact(CacheArtifact): + @override + def populate_cache(self) -> None: + FxGraphCache._write_to_local_cache(self.key, self.content) + + @override + @staticmethod + def type() -> str: + return "inductor" + + +class FxGraphCache(GuardedCache[CompiledFxGraph]): + """ + Supports caching and reusing compiled Fx graphs. + + The overall strategy is as follows: + - This cache stores entries on disk. When saving an entry, we can't + serialize callables (that could be C++, Triton, etc.), so we serialize + their own disk cache location. We then recreate the compiled artifact + after fetching from disk. + - For indexing the cache, we gather the fields relevant to identifying an + FxGraph (the graph module, graph inputs, system settings etc.) into an + FxGraphCacheDetails object, pickle it, and compute a hash for the key. + See FxGraphCachePickler. + - Among the metadata we store, we also include a guards expression that's + appropriate for validating any symbols for Tensor arguments that have + symbolic bounds. On cache lookup then, we evaluate those guards in the + current context to validate that a cached entry can be served. + - A given graph could have multiple compiled versions, corresponding to + different sets of guards. Therefore, we store cache entries in the form: + // + - On lookup, we compute the key from the graph details, iterate over all + leaf files in the corresponding subdirectory, deserialize the entry, and + evaluate its guards expression. If the evaluation succeeds, we have a + cache hit. If it fails, we compile the graph and store a new entry. + - Finally, on a cache hit, we need to make sure any guards that would + have been created during compilation are added to the current context. + """ + + # TODO(masnesral): Investigate whether it's beneficial to store compiled graphs + # in an in-memory cache after loading from disk. + @staticmethod + def _get_tmp_dir() -> str: + """ + Get the toplevel temporary directory for storing compiled graphs. + """ + return os.path.join(cache_dir(), "fxgraph") + + @classmethod + def _get_tmp_dir_for_key(cls: type[FxGraphCache], key: str) -> str: + """ + Return the disk location for a given cache key. + """ + return os.path.join(FxGraphCache._get_tmp_dir(), key[1:3], key) + + @classmethod + def _record_result( + cls: type[FxGraphCache], + key: str, + local_hit: bool, + local_miss: bool, + remote_hit: bool, + remote_miss: bool, + ) -> None: + """ + Called by GuardedCache to record hit/miss statistics. + """ + if local_hit: + CompileEventLogger.try_( + CompileEventLogger.increment_toplevel, + "inductor_fx_local_cache_hit_count", + ) + if remote_hit: + CompileEventLogger.try_( + CompileEventLogger.increment_toplevel, + "inductor_fx_remote_cache_hit_count", + ) + CompileEventLogger.try_( + CompileEventLogger.add_to_set_toplevel, + "inductor_fx_remote_cache_hit_keys", + key, + ) + if local_miss: + CompileEventLogger.try_( + CompileEventLogger.increment_toplevel, + "inductor_fx_local_cache_miss_count", + ) + if remote_miss: + CompileEventLogger.try_( + CompileEventLogger.increment_toplevel, + "inductor_fx_remote_cache_miss_count", + ) + CompileEventLogger.try_( + CompileEventLogger.add_to_set_toplevel, + "inductor_fx_remote_cache_miss_keys", + key, + ) + + @staticmethod + def cache_hit_post_compile( + graph: CompiledFxGraph, + cache_info: dict[str, Any], + constants: CompiledFxGraphConstants, + ) -> tuple[CompiledFxGraph | None, dict[str, Any]]: + """ + Cache specific post compile steps that need to run if we find a graph in the cache + This includes putting bundled triton artifacts in the right place, + reloading the PyCodeCache artifact, etc. + + These don't always happen (i.e. on a cache miss, so they are in a separate function from + CompiledFxGraph.post_compile) + """ + if bundle := graph._triton_bundle: + triton_bundler_meta = TritonBundler.read_and_emit(bundle) + if (meta := triton_bundler_meta) is not None: + cache_info["triton_bundler_meta"] = str(meta) + CompileEventLogger.try_add_pt2_compile( + "inductor_compile", cached_kernel_names=meta.cached_kernel_names + ) + CompileEventLogger.try_add_pt2_compile( + "AOTAutogradCache.inductor_load", + cached_kernel_names=meta.cached_kernel_names, + ) + if len(meta.cached_kernel_names) > 0: + CompileEventLogger.try_( + CompileEventLogger.increment_toplevel, "num_triton_bundles" + ) + + try: + artifact_path = graph.after_deserialization(constants) + + from .graph import GraphLowering + + # This is used by tests to check the output for specific details. + if GraphLowering.save_output_code is not None: + GraphLowering.save_output_code(graph.source_code) + + except OSError: + # Not expected, but in case the PyCodeCache entry is removed from + # underneath us, treat it as a cache miss and recompile. + return None, cache_info + + inductor_meta = autotune_cache.inductor_meta_from_config() + code = graph.source_code + AutotuneCacheBundler.begin_compile(inductor_meta, code=code) + + # Increment the cached metrics/counters by the amounts recorded when the FX + # graph was compiled for this cache entry. Pretending these counters + # were incremented normally is useful for testing with the cache enabled. + metrics.CachedMetricsHelper.apply_deltas(graph.metrics_deltas) + counters["inductor"] += graph.counter_deltas + + output_code_log.debug("Output code: \n%s", code) + output_code_log.debug("Output code written to: %s", artifact_path) + # On cache hit, use artifact path as filename + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "fx_graph_runnable", + "encoding": "string", + }, + payload_fn=lambda: graph.runnable_graph_str, + ) + trace_structured( + "inductor_post_grad_graph", + payload_fn=lambda: graph.inductor_post_grad_graph_str, + ) + trace_structured( + "inductor_output_code", + lambda: { + "filename": artifact_path, + "file_path": os.path.abspath(artifact_path), + }, + payload_fn=lambda: code, + ) + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "inductor_provenance_tracking_node_mappings", + "encoding": "json", + }, + payload_fn=lambda: graph.inductor_provenance_mapping_str, + ) + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "inductor_provenance_tracking_kernel_stack_traces", + "encoding": "json", + }, + payload_fn=lambda: graph.inductor_provenance_stack_traces_str, + ) + if ( + get_metrics_context().in_progress() + and graph.inductor_provenance_stack_traces_str + ): + get_metrics_context().add_to_set( + "inductor_provenance", graph.inductor_provenance_stack_traces_str + ) + return graph, cache_info + + @staticmethod + def _lookup_graph( + key: str, + example_inputs: Sequence[InputType], + local: bool, + remote_cache: RemoteCache[JsonDataTy] | None, + constants: CompiledFxGraphConstants, + evaluate_guards: Callable[[str, list[int] | list[torch.SymInt]], bool] + | None = None, + ) -> tuple[CompiledFxGraph | None, dict[str, Any]]: + """ + Lookup a compiled graph in the cache by key. On a hit, return the + deserialized CompiledFxGraph object. On a miss, return None. + `constants` tracks a list of constants, or a way to obtain the list of constants + associated with a given cache entry + `evaluate_guards` allows AOTAutogradCache and other callers to customize + what constitutes a guard success. Normally, a guard hit happens if + `shape_env.evaluate_guards_expression` returns True. + """ + shape_env = FxGraphCache._get_shape_env() + assert shape_env is not None + + symints = FxGraphCache._filter_backed_symints(example_inputs) + hints = [guarding_hint_or_throw(s) for s in symints] + + # If this config is turned on, everything is a guard hit and we check nothing + if config.unsafe_skip_cache_dynamic_shape_guards: + # This also makes it so we don't add anything to the dynamic + # shape environment + evaluate_guards = lambda x, y: True # noqa: E731 + + if evaluate_guards is None: + evaluate_guards = shape_env.evaluate_guards_expression + + cache_info: dict[str, Any] = dict() + + # Use the find_graph_for_key method to find a graph for the given key + graph, pickled_content, guard_info = FxGraphCache.find_guarded_entry( + key, local, remote_cache, evaluate_guards, hints + ) + cache_info.update(guard_info) + if graph is None: + return None, cache_info + + # Validate extern_libs (e.g. libdevice) match the current env. + if graph.extern_libs_key is not None: + try: + backend = torch.utils._triton.triton_backend() + current = torch.utils._triton._extern_libs_key(backend) + except Exception: + current = None + if current != graph.extern_libs_key: + cache_info["cache_status_detailed"] = "guard_miss" + return None, cache_info + + if pickled_content is not None: + CacheArtifactManager.record_artifact( + InductorCacheArtifact.type(), key, pickled_content + ) + + # Now re-evaluate with the symints to add any guards to the current env. + if graph.guards_expr: + check = bool(evaluate_guards(graph.guards_expr, symints)) + assert check is True + log.debug( + "fx graph cache key %s post-load guards: %s", key, shape_env.guards + ) + + return FxGraphCache.cache_hit_post_compile(graph, cache_info, constants) + + @staticmethod + def _write_to_local_cache(key: str, content: bytes) -> None: + subdir = FxGraphCache._get_tmp_dir_for_key(key) + if not os.path.exists(subdir): + os.makedirs(subdir, exist_ok=True) + + # Use a hash of the serialized CompiledFxGraph to get a unique file + # name. The specific name doesn't matter since a lookup involves + # iterating over all entries in the parent subdir. + path = os.path.join(subdir, sha256_hash(content)) + write_atomic(path, content, make_dirs=True) + + @staticmethod + def _save_graph( + key: str, + compiled_graph: OutputCode, + example_inputs: Sequence[InputType], + local: bool, + remote_cache: RemoteCache[JsonDataTy] | None, + ) -> None: + """ + Store a serialized CompiledFxGraph on disk. + """ + from .compile_fx import CompiledFxGraph + + assert isinstance(compiled_graph, CompiledFxGraph), ( + f"serialization for {type(compiled_graph)} NYI" + ) + + # Before serializing, compute the guard expression that will be used to + # ensure that a CompiledFxGraph is valid when loaded from the cache. It's + # sufficient to consider only the SymInt args to the fx graph since the + # Tensor shapes are already captured in the hash for the cache key. Any + # Tensor arg with a symbolic shape will have a SymInt arg for the graph. + shape_env = FxGraphCache._get_shape_env() + assert shape_env is not None + symints = FxGraphCache._filter_backed_symints(example_inputs) + guards = shape_env.get_pruned_guards(symints) + compiled_graph.guards_expr = shape_env.produce_guards_expression( + placeholders=symints, guards=guards + ) + try: + backend = torch.utils._triton.triton_backend() + compiled_graph.extern_libs_key = torch.utils._triton._extern_libs_key( + backend + ) + except Exception: + pass + disk_compiled_graph = copy(compiled_graph) + disk_compiled_graph.prepare_for_serialization() + + try: + content = pickle.dumps(disk_compiled_graph) + except Exception: + log.warning( + "fx graph cache unable to serialize compiled graph", exc_info=True + ) + counters["inductor"]["fxgraph_cache_pickle_error"] += 1 + return + + try: + CacheArtifactManager.record_artifact( + InductorCacheArtifact.type(), key, content + ) + if local: + FxGraphCache._write_to_local_cache(key, content) + + if remote_cache: + time_taken_ms = int((disk_compiled_graph._time_taken_ns or 0) // 1e6) + cache_data: JsonDataTy = { + "data": base64.b64encode(content).decode("ascii"), + "time_taken_ms": time_taken_ms, + } + remote_cache.put(key, cache_data) + except Exception: + log.warning("fx graph unable to write to cache", exc_info=True) + counters["inductor"]["fxgraph_cache_write_error"] += 1 + + @staticmethod + def _check_for_hop(gm: torch.fx.GraphModule) -> None: + for module in gm.modules(): + if not isinstance(module, torch.fx.GraphModule): + continue + for node in module.graph.nodes: + if ( + isinstance(node.target, torch._ops.HigherOrderOperator) + and not node.target.cacheable() + ): + raise BypassFxGraphCache( + f"Can't cache HigherOrderOperator: {node.target.name()}" + ) + # TODO: this check is broken in two ways: + # 1. FX uses "get_attr" (with underscore), not "getattr" + # 2. It only checks for ScriptObject, not FakeScriptObject + # Fixing it would also bypass AOTAutogradCache (which calls + # _check_can_cache), so we'd need to decouple the two first. + if node.op == "getattr" and isinstance( + getattr(gm, node.target), torch._C.ScriptObject + ): + raise BypassFxGraphCache("Can't cache torchbind objects") + + @staticmethod + def _check_can_cache(gm: torch.fx.GraphModule) -> None: + """ + Check some conditions that would preclude caching and raise BypassFxGraphCache + to bypass in case caching is not possible. + """ + # Custom passes must implement the CustomGraphPass or we don't + # know how to include them in the cache key calculation. + # When timing is EARLY, pre-grad passes already ran before the cache + # lookup so there's nothing to validate here. + if resolve_pre_grad_pass_timing() != "early": + assert not config.pre_grad_custom_pass or ( + isinstance(config.pre_grad_custom_pass, CustomGraphPass) + and config.pre_grad_custom_pass.uuid() + ), "Unsupported pre grad custom pass" + for p in (config.post_grad_custom_pre_pass, config.post_grad_custom_post_pass): + if p and (not isinstance(p, CustomGraphPass) or not p.uuid()): + raise BypassFxGraphCache("Unsupported post grad custom pass") + # Same with the joint custom passes + for p in (config.joint_custom_pre_pass, config.joint_custom_post_pass): + if p and (not isinstance(p, CustomGraphPass) or not p.uuid()): + raise BypassFxGraphCache("Unsupported joint custom pass") + # We should find any users of _pre_fusion_custom_pass and _fuse_ddp_communication_passes + # and ensure they are not passing us raw callables + if config._pre_fusion_custom_pass is not None: + if not isinstance(config._pre_fusion_custom_pass, CustomGraphPass): + raise BypassFxGraphCache("Unsupported _pre_fusion_custom_pass") + for p in config._fuse_ddp_communication_passes: + if callable(p) and not isinstance(p, CustomGraphPass): + raise BypassFxGraphCache("Unsupported _fuse_ddp_communication_pass") + + # Freezing can embed constants that wouldn't be static across runs. + if has_frozen_params(gm) and not torch._utils_internal.justknobs_check( + "pytorch/inductor:allow_freezing_with_caching" + ): + raise BypassFxGraphCache("Skipping graph with frozen constants") + + if config.aot_inductor.use_runtime_constant_folding: + raise BypassFxGraphCache( + "Runtime constant folding can introduce constants that aren't " + "static across runs" + ) + + from torch._inductor.compiler_bisector import CompilerBisector + + if CompilerBisector.bisection_enabled: + log.debug("dont cache graph when bisect enabled") + raise BypassFxGraphCache + + # The treatment of guards in the caching implementation requires that + # we have a shape env. + if FxGraphCache._get_shape_env() is None: + log.debug("fx graph cache no shape env") + raise BypassFxGraphCache("No shape env") + + # We skip caching if there are any HOPs or torchbind objects. + FxGraphCache._check_for_hop(gm) + + @staticmethod + def prepare_key( + gm: torch.fx.GraphModule, + example_inputs: Sequence[InputType], + fx_kwargs: _CompileFxKwargs, + inputs_to_check: Sequence[int], + remote: bool, + ) -> tuple[tuple[str, list[str]] | None, dict[str, Any]]: + """ + Checks that the inductor input is cacheable, then computes + and returns the cache key for the input. + Returns (key_info, cache_info) where: + - key_info is (hash_key, debug_lines), and + - cache_info will contain debug info in the event of BypassFxGraphCache. + + NB: It is possible to have this function return a union instead. But + I personally believe it is more annoying/difficult to read in that format. + """ + try: + FxGraphCache._check_can_cache(gm) + key, debug_lines = compiled_fx_graph_hash( + gm, example_inputs, fx_kwargs, inputs_to_check + ) + except BypassFxGraphCache as e: + counters["inductor"]["fxgraph_cache_bypass"] += 1 + log.info("Bypassing FX Graph Cache because '%s'", e) # noqa: G200 + if remote: + log_cache_bypass("bypass_fx_graph", str(e)) + cache_info = { + "cache_state": "bypass", + "cache_bypass_reason": str(e), + "cache_event_time": time_ns(), + } + return None, cache_info + # If key exists, then cache_info will come from load_with_key + return (key, debug_lines), {} + + @staticmethod + def get_remote_cache() -> RemoteCache[JsonDataTy] | None: + """ + Attempts to load the remote cache, returns None on error. + """ + cache_id = "fx-graph-v1" + return create_cache( + cache_id, + config.is_fbcode(), + "FbRemoteFxGraphCache", + "RemoteFxGraphCache", + ) + + @staticmethod + def load_with_key( + key: str, + debug_lines: list[str], + example_inputs: Sequence[InputType], + local: bool, + remote_cache: RemoteCache[JsonDataTy] | None, + is_backward: bool, + constants: CompiledFxGraphConstants, + evaluate_guards: Callable[[str, list[int] | list[torch.SymInt]], bool] + | None = None, + ) -> tuple[CompiledFxGraph | None, dict[str, Any]]: + """ + Lookup the graph with the given key, and return results and metadata. + Doesn't do any logging on its own, because AOTAutograd handles a cache miss + differently from FXGraphCache. + """ + compiled_graph, cache_info = FxGraphCache._lookup_graph( + key, example_inputs, local, remote_cache, constants, evaluate_guards + ) + cache_info = { + **cache_info, + "key": key, + "components": debug_lines, + "cache_event_time": time_ns(), + } + if compiled_graph is not None: + log.info("fx graph cache hit for key %s", key) + counters["inductor"]["fxgraph_cache_hit"] += 1 + cache_info["cache_state"] = "hit" + + if (time_saved_ns := compiled_graph._time_taken_ns) is not None: + cache_info["time_saved_ns"] = time_saved_ns + CompileEventLogger.try_( + CompileEventLogger.increment_toplevel, + "distributed_ephemeral_timeout_us", + time_saved_ns // 1000, + ) + if ( + ephemeral_increase + := add_ephemeral_timeout_increase_for_distributed(time_saved_ns) + ) != 0: + cache_info["ephemeral_timeout_increase"] = ephemeral_increase + else: + log.info("fx graph cache miss for key %s", key) + counters["inductor"]["fxgraph_cache_miss"] += 1 + cache_info["cache_state"] = "miss" + + return compiled_graph, cache_info + + @staticmethod + def clear() -> None: + """ + Clear out the on-disk cache. + """ + try: + shutil.rmtree(FxGraphCache._get_tmp_dir()) + except FileNotFoundError: + pass + + +@functools.cache +def split_aot_inductor_output_path(path: str) -> tuple[str, str]: + def get_module_ext_type() -> str: + if _IS_WINDOWS: + return ".pyd" + else: + return ".so" + + """Returns the path where the AOT Inductor compiled kernels are stored.""" + if path.endswith(get_module_ext_type()): + return os.path.split(path) + elif path.endswith(".pt2"): + return os.path.split(path) + else: + return path, "" + + +@clear_on_fresh_cache +class CudaKernelParamCache: + cache: dict[str, dict[str, Any]] = {} + cache_clear = staticmethod(cache.clear) + + @classmethod + def set( + cls, + key: str, + params: dict[str, str | None], + cubin: str, + bin_type: str, + asm: str | None = None, + asm_type: str | None = None, + ) -> None: + basename = None + if config.aot_inductor.package_cpp_only: + assert config.triton.unique_kernel_names, ( + "package_cpp_only requires triton kernel names to be unique" + ) + assert params["mangled_name"], "Missing kernel name" + basename = params["mangled_name"] + + _, bin_path = write( + cubin, + bin_type, + hash_type=bin_type, + specified_dir=split_aot_inductor_output_path( + config.aot_inductor.output_path + )[0], + key=basename, + ) + # Retrieve the basename again in case it is a generated hashcode + basename, _ = get_name_and_dir_from_output_file_path(bin_path) + + if config.aot_inductor.emit_multi_arch_kernel: + bin_type_to_ext = { + "cubin": ".fatbin", + XPU_KERNEL_FORMAT: ".spv", + "hsaco": ".hsaco", + } + assert bin_type in bin_type_to_ext, ( + "multi_arch_kernel_binary only supported in CUDA/XPU/ROCm" + ) + base_path, _ = os.path.splitext(bin_path) + bin_path = base_path + bin_type_to_ext[bin_type] + + asm_path: str = "" + + # Kernel assembly/IR requirements for AOT Inductor: + # - CUDA/XPU: Always require PTX/SPV + # - ROCm multi-arch: Require LLVM IR (.ll) for bundle compilation + if ( + config.aot_inductor.emit_multi_arch_kernel + or config.aot_inductor.package_cpp_only + ): + # Allow ROCm single-arch to skip (asm=None OK), require for everything else + if torch.version.hip is None or (asm and asm_type): + assert asm, "Missing kernel assembly code" + assert asm_type, "Missing kernel assembly type" + + # Cache directory mapping: asm_type → hash_type + # Problem: LLVM IR extension ".ll" isn't a recognized cache category + # Solution: Map to "code" (generic category for non-standard formats) + # Recognized categories: "ptx", "amdgcn", "spv", "code" + hash_kind = asm_type if asm_type in {"amdgcn", "ptx", "spv"} else "code" + + _, asm_path = write( + asm, + asm_type, + hash_type=hash_kind, + specified_dir=split_aot_inductor_output_path( + config.aot_inductor.output_path + )[0], + key=basename, + ) + + params[get_cpp_wrapper_cubin_path_name()] = bin_path + params["asm"] = asm_path + cls.cache[key] = params + + @classmethod + def get(cls, key: str) -> dict[str, Any] | None: + return cls.cache.get(key, None) + + @classmethod + def get_keys(cls) -> KeysView[str]: + return cls.cache.keys() + + +class AotCodeCompiler: + """ + Compile AOT Inductor generated code. + """ + + @classmethod + def compile( + cls, + graph: GraphLowering, + wrapper_code: str, + kernel_code: str, + serialized_extern_kernel_nodes: str | None, + *, + device_type: str, + additional_files: list[str], + ) -> list[str | Weights] | str: + """ + Returns the .so path, or returns a list of files that were generated if + config.aot_inductor.package=True. + """ + generated_files: list[str | Weights] = additional_files # type: ignore[assignment] + + _set_gpu_runtime_env() # cpp_extension consults the env + + picked_vec_isa = pick_vec_isa() + vec_isa_cmd_gen = CppBuilder( + name="o", + sources="i", + BuildOption=CppTorchDeviceOptions( + vec_isa=picked_vec_isa, + device_type=device_type, + aot_mode=graph.aot_mode, + ), + ) + # write function will calc source_code hash, the same source code with different + # ISA level should be generate different hash. + # So we need get a command_line which contains isa related parameter as a part of hash key. + # And then pass the command_line to below write function as extra parameter to + # guarantee the source code hash contains ISA difference. + cpp_command = repr(vec_isa_cmd_gen.get_command_line()) + + # Meta internal AOTInductor CPU + use_relative_path = ( + config.is_fbcode() and device_type == "cpu" and graph.aot_mode + ) + + ( + specified_output_path, + specified_artifact_name, + ) = split_aot_inductor_output_path(config.aot_inductor.output_path) + + # TODO (benjaminglass1): the CMake packaging path doesn't support linking files + # built with different flags. Until that's implemented, append the kernel code + # to the wrapper and build everything at max optimization. + if config.aot_inductor.package_cpp_only: + wrapper_code = "\n".join((wrapper_code, kernel_code)) + kernel_code = "" + + wrapper_key, wrapper_path = write( + wrapper_code, + "wrapper.cpp", + extra=cpp_command, + specified_dir=specified_output_path, + key=config.aot_inductor.model_name_for_generated_files, + ) + kernel_code = ( + f"// Triton kernels are embedded as comments in {wrapper_path}\n" + + kernel_code + ) + _, kernel_path = write( + kernel_code, + "kernel.cpp", + extra=cpp_command, + specified_dir=specified_output_path, + key=config.aot_inductor.model_name_for_generated_files, + ) + + header_code = "" + header_path = "" + if not config.aot_inductor.dynamic_linkage: + # to link statically, we also need a header file + with open( + os.path.join( + os.path.dirname(os.path.dirname(__file__)), + "csrc", + "inductor", + "aoti_runtime", + "model.h", + ) + ) as f: + # model_name_for_generated_files is guaranteed to be non-empty when compile_standalone + model_class_name = config.aot_inductor.model_name_for_generated_files + class_name = f"AOTInductorModel{model_class_name}" + header_code = f.read() + + # we replace like this to avoid replacing + # AOTInductorModelBase and AOTInductorModelKernelsBase + header_code = ( + header_code.replace("", f"<{class_name}>") + .replace("AOTInductorModel(", f"{class_name}(") + .replace("AOTInductorModel :", f"{class_name} :") + ) + _, header_path = write( + header_code, + "h", + specified_dir=specified_output_path, + key=model_class_name, + ) + + # Log the AOTInductor wrapper and kernel code, if needed. + with WritableTempFile("w+") as t: + """ + Avoid "Permission denied error" on Windows: + with tempfile.NamedTemporaryFile("w", suffix=".gv") as temp_file: + # Not writable on Windows: + # https://docs.python.org/3/library/tempfile.html#tempfile.NamedTemporaryFile + + Example: + with WritableTempFile("w", suffix=".gv") as temp_file: + tree.to_dotfile(temp_file.name) + """ + t.writelines((wrapper_code, "\n", kernel_code, "\n")) + t.flush() + V.debug.output_code(t.name, extension="cpp") + + if config.aot_inductor.package: + generated_files.append(wrapper_path) + if not config.aot_inductor.package_cpp_only: + generated_files.append(kernel_path) + if not config.aot_inductor.dynamic_linkage: + generated_files.append(header_path) + + output_code_log.info("Wrapper code written to: %s", wrapper_path) + output_code_log.info("Kernel code written to: %s", kernel_path) + trace_structured( + "graph_dump", + lambda: { + "name": "inductor_aot_wrapper_code", + "type": "cpp", + "filename": wrapper_path, + }, + payload_fn=lambda: wrapper_code, + ) + trace_structured( + "graph_dump", + lambda: { + "name": "inductor_aot_kernel_code", + "type": "cpp", + "filename": kernel_path, + }, + payload_fn=lambda: kernel_code, + ) + if not config.aot_inductor.dynamic_linkage: + output_code_log.info("Header code written to: %s", header_path) + trace_structured( + "graph_dump", + lambda: { + "name": "inductor_aot_header_code", + "type": "cpp", + "filename": header_path, + }, + payload_fn=lambda: header_code, + ) + + # We use a file lock below to protect FS operations. The lock file + # is scoped to the 'key', so make sure the consts_s is protected + # by the same lock: + wrapper_path_operator = Path(wrapper_path) + kernel_path_operator = Path(kernel_path) + specified_sub_dir = wrapper_path_operator.parent / wrapper_key + if not specified_sub_dir.exists(): + specified_sub_dir.mkdir(exist_ok=True) + cmake_path = str(Path(specified_sub_dir) / "CMakeLists.txt") + + def _compile_consts(consts: bytes, platform: str) -> str: + # Load from aot_inductor, and update the value on demand. + use_asm_build: bool = config.aot_inductor.use_consts_asm_build + + if platform == "linux": + if graph.mutated_buffers & OrderedSet(graph.constants.keys()): + # .data section is between .text and .bss. When the size of .data is large, + # during the linking, the relocation of .text against .bss may overflow. + # Rename it to .ldata so that it won't be in between the .text and .bss section + if len(consts) > 2_000_000_000: + raise ValueError( + "Models with buffer mutation included doesn't support constants greater than 2GB!" + ) + section_attr = '.ldata, "aw"' + else: + section_attr = '.lrodata, "a"' + symbol_prefix = "" + elif platform == "darwin": + section_attr = "__DATA,__data" + symbol_prefix = "_" + elif platform == "win32": + symbol_prefix = "" + # ASM build is not supported on Windows, force use CPP build. + use_asm_build = False + else: + raise RuntimeError(f"Unsupported platform: {platform}") + + # Intel compiler failed to compile this manually constructed assembly file. + # Switch XPU to use consts cpp build. + if device_type == "xpu": + use_asm_build = False + + is_large_consts = len(consts) > 1024 + is_zero_size_consts = len(consts) == 0 + + def format_consts_to_gnu_asm( + consts: bytes, + align_bytes: int, + symbol_prefix: str, + is_large_consts: bool, + ) -> tuple[str, str]: + consts_asm = f"\t.section\t{section_attr}\n" + consts_asm += f"\t.balign {align_bytes}\n" + consts_asm += f"\t.globl\t{symbol_prefix}_binary_constants_bin_start\n" + consts_asm += f"{symbol_prefix}_binary_constants_bin_start:\n" + if not is_large_consts: + for c in consts: + consts_asm += f"\t.byte {c}\n" + # Add one element even if constants are empty + # Otherwise assembler will not put them in data section + if not consts: + consts_asm += "\t.space 1\n" + else: + consts_asm += "\t.quad 0x1234567899abcdef\n" + consts_asm += f"\t.space {len(consts) - 8}\n" + consts_asm += f".globl\t{symbol_prefix}_binary_constants_bin_end\n" + consts_asm += f"{symbol_prefix}_binary_constants_bin_end:\n" + return consts_asm, "weights.S" + + # Use c++ to convert consts to object file can support more compilers, such as msvc and icx. + def format_consts_to_cpp( + consts: bytes, align_bytes: int, symbol_prefix: str + ) -> tuple[str, str]: + consts_size = len(consts) + asan_attr = """#if defined(__clang__) || defined (__GNUC__)\t\n\ +#define ATTRIBUTE_NO_SANITIZE_ADDRESS __attribute__((no_sanitize("address")))\t\n\ +#else\t\n\ +#define ATTRIBUTE_NO_SANITIZE_ADDRESS\t\n\ +#endif\t\n\ +\t\n\ +ATTRIBUTE_NO_SANITIZE_ADDRESS\t\n""" + const_cpp = asan_attr + const_cpp += f"alignas({align_bytes}) extern " + const_cpp += f"unsigned char {symbol_prefix}_binary_constants_bin_start[{consts_size}] = {{\t\n" + count_bytes = 0 + for c in consts: + const_cpp += f"{c}, " + count_bytes = count_bytes + 1 + if count_bytes % 16 == 0: + const_cpp += "\t\n" + const_cpp += "};\t\n" + const_cpp += f"alignas({align_bytes}) extern unsigned char * {symbol_prefix}_binary_constants_bin_end;\t\n" + return const_cpp, "weights.cpp" + + def get_zero_consts_asm_code( + align_bytes: int, + symbol_prefix: str, + ) -> tuple[str, str]: + """ + This function handles zero-sized constants because the C++ standard prohibits zero-length arrays: + https://stackoverflow.com/questions/9722632/what-happens-if-i-define-a-0-size-array-in-c-c + + On Windows (MSVC): + The compiler reports error C2466 for zero-sized arrays: + https://learn.microsoft.com/en-us/cpp/error-messages/compiler-errors-1/compiler-error-c2466 + Solution: Use assembly compilation to handle this case. + + Why not use Win32 assembly for all paths? + ml64 only supports alignment up to 16 bytes, which isn't optimal for performance. + + Cross-platform implementation: + Linux: Added '-pedantic' to disable zero-sized arrays in C++ compiler + Windows: MSVC naturally rejects zero-sized arrays by default + """ + if _IS_WINDOWS: + # Windows ml64 is max support align to 16, but it is no effect to zero size data. + asm_code = """ +option casemap:none +.data +?_binary_constants_bin_start@@3PAEA: +align 16 +?_binary_constants_bin_end@@3PAEA: +align 16 +public ?_binary_constants_bin_start@@3PAEA +public ?_binary_constants_bin_end@@3PAEA +end +""" + asm_ext = "asm" + else: + asm_code = f"\t.section\t{section_attr}\n" + asm_code += f"\t.balign {align_bytes}\n" + asm_code += ( + f"\t.globl\t{symbol_prefix}_binary_constants_bin_start\n" + ) + asm_code += f"{symbol_prefix}_binary_constants_bin_start:\n" + asm_code += f".globl\t{symbol_prefix}_binary_constants_bin_end\n" + asm_code += f"{symbol_prefix}_binary_constants_bin_end:\n" + asm_ext = "S" + return asm_code, asm_ext + + if use_asm_build: + consts_code, code_ext = format_consts_to_gnu_asm( + consts, ALIGN_BYTES, symbol_prefix, is_large_consts + ) + else: + if is_zero_size_consts: + consts_code, code_ext = get_zero_consts_asm_code( + ALIGN_BYTES, symbol_prefix + ) + else: + consts_code, code_ext = format_consts_to_cpp( + consts, ALIGN_BYTES, symbol_prefix + ) + + _, consts_s = write( + consts_code, + code_ext, + specified_dir=str(specified_sub_dir), + key=config.aot_inductor.model_name_for_generated_files, + ) + consts_s = Path(consts_s) + object_build_options = CppTorchDeviceOptions( + device_type=device_type, + aot_mode=graph.aot_mode, + compile_only=True, + use_relative_path=use_relative_path, + ) + object_builder = CppBuilder( + name=str(consts_s.stem), + sources=str(consts_s), + output_dir=str(consts_s.parent), + BuildOption=object_build_options, + ) + consts_o = object_builder.get_target_file_path() + if use_asm_build is False and is_zero_size_consts: + run_asm_build_object(str(consts_s), consts_o, str(consts_s.parent)) + else: + object_builder.build() + + if is_large_consts and use_asm_build: + with open(consts_o, "r+b") as f: + f.seek(0) + hdr = f.read(1024) + # Search for magic number and write the actual data over it + start_idx = ( + hdr.find(b"\xef\xcd\xab\x99\x78\x56\x34\x12") + if sys.byteorder == "little" + else hdr.find(b"\x12\x34\x56\x78\x99\xab\xcd\xef") + ) + assert start_idx != -1 + f.seek(start_idx) + pos = 0 + while pos < len(consts): + rc = f.write(consts[pos:]) + pos += rc + + # Remove the .S file to save space + os.remove(consts_s) + + return consts_o + + from torch.utils._filelock import FileLock + + lock_dir = get_lock_dir() + lock = FileLock( + os.path.join(lock_dir, wrapper_key + ".lock"), timeout=LOCK_TIMEOUT + ) + with lock: + if serialized_extern_kernel_nodes: + extern_kernel_nodes_json = str( + wrapper_path_operator.with_suffix(".json") + ) + with open(extern_kernel_nodes_json, "w") as f: + f.write(serialized_extern_kernel_nodes) + + if config.aot_inductor.package: + generated_files.append(extern_kernel_nodes_json) + + metadata = config.aot_inductor.metadata + metadata["AOTI_DEVICE_KEY"] = device_type + + # Add environment information to ensure .so compatibility + metadata.update(get_device_information(device_type)) + + # Save user provided metadata + meta_json = str( + wrapper_path_operator.with_name( + f"{wrapper_path_operator.stem}_metadata.json" + ) + ) + for k, v in config.aot_inductor.metadata.items(): + assert isinstance(k, str) and isinstance(v, (str)), ( + "Metadata must only contain strings" + ) + + with open(meta_json, "w") as f: + f.write(json.dumps(config.aot_inductor.metadata)) + + kernel_meta_json = str( + kernel_path_operator.with_name( + f"{kernel_path_operator.stem}_metadata.json" + ) + ) + shutil.copy(meta_json, kernel_meta_json) + + if config.aot_inductor.package: + generated_files.append(meta_json) + if not config.aot_inductor.package_cpp_only: + generated_files.append(kernel_meta_json) + + output_so = ( + config.aot_inductor.output_path + if specified_artifact_name + else str(wrapper_path_operator.with_suffix(".so")) + ) + all_cuda = all( + graph.get_original_value_of_constant(name).is_cuda + for name in graph.constants + if name not in graph.folded_constants + ) + + def _to_bytes(t: torch.Tensor, all_cuda: bool) -> bytes: + def _pad_to_alignment(raw_bytes: bytes) -> bytes: + padded_bytes = raw_bytes.ljust( + (len(raw_bytes) + ALIGN_BYTES - 1) // ALIGN_BYTES * ALIGN_BYTES, + b"\x00", + ) + return padded_bytes + + # This serializes the tensor's untyped_storage to bytes by accessing + # the raw data of the underlying structure. + import ctypes + + if t.numel() == 0: + return b"" + + if t.is_mkldnn: + data_ptr = torch.ops.mkldnn.data_ptr(t) + nbytes = torch.ops.mkldnn._nbytes(t) + else: + t_cpu = t.untyped_storage().cpu() + data_ptr = t_cpu.data_ptr() + nbytes = t_cpu.nbytes() + + raw_array = ctypes.cast( + data_ptr, + ctypes.POINTER(ctypes.c_ubyte * nbytes), + ) + # pyrefly: ignore [missing-attribute] + raw_bytes = bytes(raw_array.contents) + return raw_bytes if all_cuda else _pad_to_alignment(raw_bytes) + + if ( + config.aot_inductor.package_constants_in_so + or config.aot_inductor.package_constants_on_disk_format == "binary_blob" + ): + serialized_weights = b"".join( + _to_bytes(graph.get_original_value_of_constant(name), all_cuda) + for name in graph.constants + if name not in graph.folded_constants + ) + else: + serialized_weights = b"" + + if config.aot_inductor.package_constants_on_disk_format == "pickle_weights": + # We need to return a storage key here because the original value tensor might be a clone + weights_dict = Weights( + { + graph.allocated_constant_name[name]: ( + graph.get_original_value_of_constant(name), + TensorProperties(graph.constants[name]), + ) + for name in graph.constants + if name not in graph.folded_constants + } + ) + generated_files.append(weights_dict) + + consts_size = len(serialized_weights) + + use_external_weights, use_mmap_weights = determine_aoti_mmap_flags( + consts_size + ) + if use_external_weights and use_mmap_weights: + # Should never reach here, just a check for sanity + raise RuntimeError( + "use_external_weights and use_mmap_weights cannot both be True." + ) + + external_weights_path = None + if use_external_weights: + external_weights_filename = f"{wrapper_path_operator.stem}_weights.blob" + external_weights_path = str( + wrapper_path_operator.with_name(external_weights_filename) + ) + + compile_command: dict[str, Any] = { + "aot_mode": graph.aot_mode, + "device_type": device_type, + "use_mmap_weights": use_mmap_weights, + "use_mmap_weights_external": use_external_weights, + "use_relative_path": use_relative_path, + "vec_isa": picked_vec_isa, + } + # If we're packaging via CMake, we build the whole code at max optimization. + wrapper_build_options = CppTorchDeviceOptions( + compile_only=True, + min_optimize=not config.aot_inductor.package_cpp_only, + **compile_command, + ) + kernel_build_options = CppTorchDeviceOptions( + compile_only=True, + **compile_command, + ) + + # potentially, precompile the AOT header for this device + if config.aot_inductor.precompile_headers and not _IS_WINDOWS: + header_file = _get_cpp_wrapper_header( + device_type, aot_mode=graph.aot_mode + ) + wrapper_build_options.precompiled_header = _precompile_header( + header_file, + cpp_command, + min_optimize=not config.aot_inductor.package_cpp_only, + **compile_command, + ) + if cpp_prefix := _get_cpp_prefix_header(device_type): + kernel_build_options.precompiled_header = _precompile_header( + cpp_prefix, + cpp_command, + **compile_command, + ) + + wrapper_builder = CppBuilder( + name=str(wrapper_path_operator.stem), + sources=wrapper_path, + output_dir=str(wrapper_path_operator.parent), + BuildOption=wrapper_build_options, + ) + wrapper_compile_cmd = wrapper_builder.get_command_line() + wrapper_o = wrapper_builder.get_target_file_path() + + kernel_builder = CppBuilder( + name=str(kernel_path_operator.stem), + sources=kernel_path, + output_dir=str(wrapper_path_operator.parent), + BuildOption=kernel_build_options, + ) + kernel_compile_cmd = kernel_builder.get_command_line() + kernel_o = kernel_builder.get_target_file_path() + + log.debug("aot wrapper compilation command: %s", wrapper_compile_cmd) + log.debug("aot kernel compilation command: %s", kernel_compile_cmd) + if config.aot_inductor.package_cpp_only: + # Not doing the actual compilation here + compile_flags = str( + wrapper_path_operator.with_name( + f"{wrapper_path_operator.stem}_compile_flags.json" + ) + ) + wrapper_build_options.save_flags_to_json(compile_flags) + generated_files.append(compile_flags) + wrapper_builder.save_compile_cmd_to_cmake(cmake_path, device_type) + wrapper_builder.save_src_to_cmake(cmake_path, wrapper_path) + generated_files.append(cmake_path) + else: + try: + wrapper_builder.build() + except (exc.CppCompileError, SkipFrame) as e: + if " is too big to optimize" in str(e): + raise RuntimeError( + "Please use torch._inductor.config.aot_inductor.compile_wrapper_opt_level = 'O0' flag." + ) from e + raise e + kernel_builder.build() + + if not use_mmap_weights: + aot_constants = serialized_weights + magic_number = 0 + if use_external_weights: + aot_constants = struct.pack("q", consts_size) + assert external_weights_path is not None + # For external weights, write weights to separate file and embed minimal placeholder + with open(external_weights_path, "wb") as f_weights: + f_weights.write(serialized_weights) + generated_files.append(external_weights_path) + else: + # we'll append weights binary to the end of .so file and mmap it when loading + magic_number = cast( + int, torch.randint(0, torch.iinfo(torch.int64).max, (1,)).item() + ) + aot_constants = struct.pack("qq", consts_size + 8, magic_number) + + consts_o = _compile_consts(aot_constants, sys.platform) + custom_obj_idx = 0 + # Note that custom_objs_config.json file is different from the model_constants_config.json file produced + # in package_sigmoid(). The keys in custom_objs_config.json directly correspond to the arg name in extern + # nodes json. The key in model_constants_config.json produced by package_sigmoid is the attribute name in the + # user model code. + + qual_name_to_id = {} # Map from constant name to its name in constants folder + for custom_obj_idx, (name, constant) in enumerate( + graph.torchbind_constants.items() + ): + if isinstance( + constant, torch._library.fake_class_registry.FakeScriptObject + ): + constant = constant.real_obj + assert isinstance(constant, torch._C.ScriptObject) + custom_obj_name = f"{CUSTOM_OBJ_FILENAME_PREFIX}{custom_obj_idx}" + + log.debug("saving script object %s as %s", name, custom_obj_name) + + qual_name_to_id[name] = custom_obj_name + custom_obj_bytes = torch._C._pickle_save(constant) + custom_obj_path = os.path.join( + wrapper_path_operator.parent, custom_obj_name + ) + + write_atomic(custom_obj_path, custom_obj_bytes, True) + generated_files.append(custom_obj_path) + + if qual_name_to_id: + constants_config_json = os.path.join( + wrapper_path_operator.parent, "custom_objs_config.json" + ) + with open(constants_config_json, "w") as f: + f.write(json.dumps(qual_name_to_id)) + generated_files.append(constants_config_json) + + cache_cls = { + "rocm": ROCmCodeCache, + "cuda": CUDACodeCache, + "xpu": XPUCodeCache, + }.get("rocm" if torch.version.hip else device_type, CUDACodeCache) + + gpu_codecache = cache_cls() + gpu_kernels_o = gpu_codecache.aot_kernels_o.copy() + # clear the list of aot kernels after each linking + gpu_codecache.aot_kernels_o.clear() + + if gpu_kernels_o: + assert not config.aot_inductor.emit_multi_arch_kernel, ( + "TODO: add emit_multi_arch_kernel support for cutlass kernels" + ) + + cubins_o = [] + asm_files = [] + fatbin_cmds: list[tuple[str, str]] = [] + if not _IS_WINDOWS: + cubins_to_embed: list[tuple[str, str]] = [] + ld, objcopy = get_ld_and_objcopy(use_relative_path) + kernels = getattr(V.graph.wrapper_code, "_kernel_name_to_body", {}) + for kernel_name, value in CudaKernelParamCache.cache.items(): + if kernel_name not in kernels: + # It is possible that CudaKernelParamCache contains more Triton kernels + # than what the current graph uses + continue + + if asm_file := value["asm"]: + asm_files.append(asm_file) + + cubin_file = value[get_cpp_wrapper_cubin_path_name()] + if ( + config.aot_inductor.emit_multi_arch_kernel + and device_type == "cuda" + ): + if torch.version.hip is None: + fatbin_cmds.append((asm_file, cubin_file)) + + else: + # ROCm multi-arch: compile LLVM IR to multi-arch bundle + from torch._inductor.rocm_multiarch_utils import ( + compile_multiarch_bundle_from_llvm_ir, + ) + + # pyrefly: ignore [unbound-name] + if not os.path.exists(asm_file): + raise RuntimeError( + f"Multi-arch ROCm compilation requires LLVM IR file, " + # pyrefly: ignore [unbound-name] + f"but {asm_file} not found. " + f"Ensure asm_type='ll' is captured in triton_heuristics.py" + ) + + # Compile for multiple archs and bundle them + success = compile_multiarch_bundle_from_llvm_ir( + # pyrefly: ignore [unbound-name] + llvm_ir_path=asm_file, + output_bundle_path=cubin_file, + target_archs=None, + ) + + if not success: + raise RuntimeError( + f"Failed to compile multi-arch bundle for kernel {kernel_name}. " + f"Check that ROCm toolchain is available and LLVM IR is valid." + ) + + log.info("Created multi-arch bundle: %s", cubin_file) + + if config.aot_inductor.embed_kernel_binary: + cubins_to_embed.append((cubin_file, kernel_name)) + + # Compile PTX → fatbin in parallel (each nvcc call is independent). + # Must complete before cubin embedding below. + if fatbin_cmds: + from concurrent.futures import ThreadPoolExecutor + + current_arch = cuda_compile_utils._nvcc_arch_as_compile_option() + nvcc = cuda_compile_utils._cuda_compiler() + + def _compile_fatbin(asm_and_cubin: tuple[str, str]) -> None: + asm_f, cubin_f = asm_and_cubin + cmd = ( + f"{nvcc} -fatbin {asm_f} -o {cubin_f} " + f"-gencode arch=compute_{current_arch},code=compute_{current_arch} " + f"-gencode arch=compute_{current_arch},code=sm_{current_arch} " + ) + try: + subprocess.run( + cmd.split(), capture_output=True, text=True, check=True + ) + except subprocess.CalledProcessError as e: + print( + f"{cmd} failed with:\nstdout:\n{e.stdout}\nstderr:\n{e.stderr}", + file=sys.stderr, + ) + raise + + with ThreadPoolExecutor() as pool: + list(pool.map(_compile_fatbin, fatbin_cmds)) + + if cubins_to_embed: + # Batch all cubins into a single .o using .incbin assembly. + # This replaces N * 3 subprocess calls (ld + 2x objcopy per + # cubin) with a single compiler invocation. + try: + combined_obj = batch_convert_cubins_to_obj( + cubins_to_embed, + os.path.dirname(output_so), + cpp_compiler=get_cpp_compiler(), + ) + cubins_o.append(combined_obj) + except subprocess.CalledProcessError: + log.warning( + "Batched cubin embedding failed, " + "falling back to per-cubin objcopy" + ) + for cubin_file, kernel_name in cubins_to_embed: + cubins_o.append( + convert_cubin_to_obj( + cubin_file, kernel_name, ld, objcopy + ) + ) + + output_name, output_dir = get_name_and_dir_from_output_file_path(output_so) + so_build_options = CppTorchDeviceOptions( + vec_isa=picked_vec_isa, + device_type=device_type, + aot_mode=graph.aot_mode, + use_relative_path=use_relative_path, + ) + + if gpu_kernels_o and device_type == "xpu": + so_build_options = CppTorchDeviceOptions( + compiler="icpx", + vec_isa=picked_vec_isa, + device_type=device_type, + aot_mode=graph.aot_mode, + use_relative_path=use_relative_path, + extra_flags=[ + "-fsycl", + "-fsycl-targets=intel_gpu_pvc", + "-Xspirv-translator", + ( + "-spirv-ext=" + "+SPV_INTEL_split_barrier," + "+SPV_INTEL_2d_block_io," + "+SPV_INTEL_subgroup_matrix_multiply_accumulate" + ), + ], + ) + + obj_srcs = [wrapper_o, kernel_o, consts_o, *gpu_kernels_o, *cubins_o] + so_builder = CppBuilder( + name=output_name, + sources=obj_srcs, + output_dir=output_dir, + BuildOption=so_build_options, + ) + link_cmd = so_builder.get_command_line() + output_so = so_builder.get_target_file_path() + + log.debug("aot linkage command: %s", link_cmd) + + # Append cmds to the end of codegen-ed wrapper file + with open(wrapper_path, "a") as f: + f.write("\n") + f.write(f"// Compile cmd\n// {wrapper_compile_cmd}\n") + f.write(f"// Link cmd\n// {link_cmd}\n") + + with open(kernel_path, "a") as f: + f.write("\n") + f.write(f"// Compile cmd\n// {kernel_compile_cmd}\n") + f.write(f"// Link cmd\n// {link_cmd}\n") + + if config.aot_inductor.package_cpp_only: + linker_flags = str( + wrapper_path_operator.with_name( + f"{wrapper_path_operator.stem}_linker_flags.json" + ) + ) + so_build_options.save_flags_to_json(linker_flags) + generated_files.append(linker_flags) + generated_files.append(_LINKER_SCRIPT) + + # If we only want to package the cpp, then we need to save the + # weights separately into a bin, and we also need to prevent compiling the so + if use_mmap_weights: + weight_file = str( + wrapper_path_operator.with_name( + f"{wrapper_path_operator.stem}_serialized_weights.bin" + ) + ) + with open(weight_file, "wb") as f_weights: + f_weights.write(serialized_weights) + f_weights.write(struct.pack("q", magic_number)) + + generated_files.append(weight_file) + else: + # TODO: unify to always use mmap_weights + generated_files.append(consts_o) + so_builder.save_src_to_cmake(cmake_path, consts_o) + + # Different CMake strategies for CUDA vs ROCm: + # - CUDA: Save asm for CMake to recompile (user has nvcc) + # - ROCm: Link pre-compiled bundle (user may lack dev tools) + if ( + config.aot_inductor.emit_multi_arch_kernel + and torch.version.hip is None + ): + so_builder.save_kernel_asm_to_cmake(cmake_path, asm_files) + generated_files.extend(asm_files) + else: + # ROCm multi-arch + all single-arch: Link pre-compiled objects + # Bundle already embedded in .o files - just link into .so + obj_srcs = [*gpu_kernels_o, *cubins_o] + generated_files.extend(obj_srcs) + for obj in obj_srcs: + so_builder.save_src_to_cmake(cmake_path, obj) + + so_builder.save_link_cmd_to_cmake(cmake_path) + else: + so_builder.build() + for o_file in obj_srcs: + if o_file in gpu_kernels_o: + continue + # Remove these as they are not needed anymore + os.remove(o_file) + + if use_mmap_weights: + if config.aot_inductor.cross_target_platform == "windows": + raise RuntimeError( + "when cross_target_platform is windows, use_mmap_weights should not be true." + ) + + def get_page_size() -> int: + # Don't use resource.getpagesize() on Windows, as it is a Unix specific package + # as seen in https://docs.python.org/2/library/resource.html + if _IS_WINDOWS: + from ctypes import ( + byref, + Structure, + windll, # pyrefly: ignore [missing-module-attribute] + ) + from ctypes.wintypes import DWORD, LPVOID, WORD + + class SYSTEM_INFO(Structure): + _fields_ = [ + ("wProcessorArchitecture", WORD), + ("wReserved", WORD), + ("dwPageSize", DWORD), + ("lpMinimumApplicationAddress", LPVOID), + ("lpMaximumApplicationAddress", LPVOID), + ("dwActiveProcessorMask", DWORD), + ("dwNumberOfProcessors", DWORD), + ("dwProcessorType", DWORD), + ("dwAllocationGranularity", DWORD), + ("wProcessorLevel", WORD), + ("wProcessorRevision", WORD), + ] + + si = SYSTEM_INFO() + windll.kernel32.GetSystemInfo(byref(si)) + sys_page_size = si.dwPageSize + else: + import resource + + sys_page_size = resource.getpagesize() + + return sys_page_size + + page_size_ = get_page_size() + page_size = max(16384, page_size_) + + with open(output_so, "a+b") as f_so: + so_size = f_so.tell() + # Page align the weights + f_so.write(b" " * (page_size - so_size % page_size)) + f_so.write(serialized_weights) + f_so.write(struct.pack("q", magic_number)) + + if config.aot_inductor.package: + generated_files.append(output_so) + + if config.trace.provenance_tracking_level != 0: + kernel_info = torch._inductor.debug.create_kernel_information_json() + kernel_info_json = os.path.join( + wrapper_path_operator.parent, "kernel_information.json" + ) + with open(kernel_info_json, "w") as f: + f.write(json.dumps(kernel_info, indent=4)) + generated_files.append(kernel_info_json) + + if config.aot_inductor.package: + # We want to return the directory that contains all the AOTI + # generated files, not just the so + # return os.path.split(output_so)[0] + return generated_files + + return output_so + + +_libgomp: CDLL | None = None + + +def custom_op_wrapper(op: str, *args: Any) -> list[c_void_p] | c_void_p | None: + # This function will be called from generated cpp wrapper code in the JIT mode. + # Because tensors will be passed in as AtenTensorHandle, we need to explicitly convert them. + def convert_arg(arg: Any) -> Any: + if str(type(arg)) == "": + # No easy way to do isinstance check on PyCapsule + return torch._C._aoti.alloc_tensor_by_stealing_from_void_ptr(arg) + elif isinstance(arg, (list, tuple)): + return type(arg)(convert_arg(a) for a in arg) + else: + return arg + + converted_args = [convert_arg(arg) for arg in args] + + assert op.startswith("torch.ops."), ( + op + " can not be called through custom_op_wrapper" + ) + func = None + for i, s in enumerate(op.split(".")): + if i == 0: + func = importlib.import_module(s) + func = getattr(func, s) + + assert callable(func), op + " can not be loaded through custom_op_wrapper" + + # convert any kwarg-only arguments to kwargs + kwargs = dict() + # pyrefly: ignore [missing-attribute] + for func_arg, conv_arg in zip(func._schema.arguments, converted_args): + if func_arg.kwarg_only: + kwargs[func_arg.name] = conv_arg + if kwargs: + del converted_args[-len(kwargs) :] + + result = func(*converted_args, **kwargs) + if result is None: + return None + + if isinstance(result, (list, tuple)): + # unsafe_alloc_void_ptrs_from_tensors expects result contains tensor only + result = [torch.tensor([]) if r is None else r for r in result] + for r in result: + assert isinstance(r, torch.Tensor), op + " returns a list of non-tensors" + return torch._C._aoti.unsafe_alloc_void_ptrs_from_tensors(result) # type: ignore[arg-type] + + assert isinstance(result, torch.Tensor), op + " returns a non-tensor" + return torch._C._aoti.unsafe_alloc_void_ptr_from_tensor(result) + + +# Precompiled headers are persistent past program runtime, but associated with one +# specific compiler version and set of flags. We explicitly use default_cache_dir here +# because these headers need to be global, rather than ignored by fresh_cache. +_HEADER_DIR = os.path.join(default_cache_dir(), "precompiled_headers") +_HEADER_LOCK_DIR = os.path.join(_HEADER_DIR, "locks") + + +@functools.cache +def _precompile_header( + header: str, + hashable_cmd_line: str, + **compile_command: Any, +) -> str: + assert not _IS_WINDOWS, ( + "CppBuilder does not currently support precompiling on Windows!" + ) + + # Get the preprocessed output from the header file to be precompiled. This allows + # us to properly invalidate the file cache when any header dependency changes. This + # is thread-safe, as each thread will get its own temporary directory. + # + # N.B. we can't use NamedTemporaryFile here because Windows errors out on attempts + # to read from a file with an open write handle. + with tempfile.TemporaryDirectory() as preprocessing_dir: + preprocessing_header = Path(preprocessing_dir) / "header.hpp" + preprocessing_header.write_text(f"#include <{header}>\n") + preprocessor = CppBuilder( + name=str(preprocessing_header)[:-4], # strip off the .hpp extension + sources=str(preprocessing_header), + BuildOption=CppTorchDeviceOptions(**compile_command, preprocessing=True), + ) + preprocessor.build() + + def _get_file_checksum(filename: str) -> str: + """Reading the whole preprocessed header in for hashing is very expensive, + but calling a fast hashing utility in a subprocess is cheap.""" + # If Windows support needs to be added here, use certutil -hashfile. + cmd_output = subprocess.run( + ("openssl", "sha512", filename), capture_output=True, text=True + ) + return cmd_output.stdout.split()[-1] + + preprocessor_hash = _get_file_checksum(preprocessor.get_target_file_path()) + + header_build_option = CppTorchDeviceOptions(**compile_command, precompiling=True) + header_hash, header_full_path = write( + content=f"#include <{header}>\n", + extension="h", + extra=( + hashable_cmd_line + + preprocessor_hash + + get_compiler_version_info(header_build_option.get_compiler()) + ), + specified_dir=_HEADER_DIR, + ) + cpp_builder = CppBuilder( + name=header_full_path, + sources=header_full_path, + BuildOption=header_build_option, + ) + # _worker_compile_cpp will automatically ignore any compilation whose result already + # exists, so this is always safe. + os.makedirs(_HEADER_LOCK_DIR, exist_ok=True) + _worker_compile_cpp( + os.path.join(_HEADER_LOCK_DIR, f"{header_hash}.lock"), + (cpp_builder,), + ) + + return header_full_path + + +def _get_cpp_prefix_header(device: str) -> str | None: + if device.startswith("cpu"): + return "torch/csrc/inductor/cpp_prefix.h" + return None + + +def _get_cpp_wrapper_header(device: str, aot_mode: bool = False) -> str: + """Given a device type (and optionally whether we're in AOT Inductor mode), returns + the path to the cpp_wrapper header file to be precompiled.""" + base_device = device.split(":", maxsplit=1)[0] + is_array_ref = config.aot_inductor.allow_stack_allocation and base_device == "cpu" + return ( + "torch/csrc/inductor/" + f"{'aoti_include' if aot_mode else 'cpp_wrapper'}/" + f"{'array_ref' if is_array_ref else base_device}.h" + ) + + +@clear_on_fresh_cache +class CppCodeCache: + """Compiles and caches C++ libraries. Users of this class supply the source code to + be compiled, while compilation flags are set by CppBuilder.""" + + cache: dict[str, Callable[[], CDLL | ModuleType]] = {} + cache_clear = staticmethod(cache.clear) + cpp_compile_command_flags: dict[str, Any] = {} + + @staticmethod + def _load_library_inner(path: str, key: str) -> CDLL | ModuleType: + return cdll.LoadLibrary(path) + + @classmethod + def _load_library(cls, path: str, key: str) -> CDLL | ModuleType: + try: + result = cls._load_library_inner(path, key) + result.key = key # type: ignore[union-attr] + return result + except (ImportError, OSError) as e: + if "gomp" in str(e) and os.path.exists("/usr/lib64/libgomp.so.1"): + # hacky workaround for fbcode/buck + global _libgomp + _libgomp = cdll.LoadLibrary("/usr/lib64/libgomp.so.1") + result = cls._load_library_inner(path, key) + result.key = key # type: ignore[union-attr] + return result + if "failed to map segment from shared object" in str(e): + raise OSError( + f"{e}. The most common reason this may occur is if the {tempfile.gettempdir()} folder " + "is mounted with noexec (e.g., by default Docker mounts tmp file systems " + f"as noexec). Please remount {tempfile.gettempdir()} with exec enabled, or set another " + "temporary directory with TORCHINDUCTOR_CACHE_DIR environment variable." + ) from e + raise + + @classmethod + def _get_uncompiled_header(cls, device: str) -> str | None: + """ + Given a device type, returns the path to a CPP header file to be precompiled. + """ + return None + + @classmethod + def load_async( + cls, + main_code: str, + device_type: str = "cpu", + submit_fn: Any = None, + extra_flags: Sequence[str] = (), + optimized_code: str | None = None, + ) -> Any: + """Compile and load a C++ library. Returns a callable that returns the loaded + library.""" + compile_command = { + **cls.cpp_compile_command_flags, + "device_type": device_type, + "extra_flags": extra_flags, + "use_relative_path": config.is_fbcode(), + "vec_isa": pick_vec_isa(), + } + + _set_gpu_runtime_env() # cpp_extension consults the env + + # Note the distinction between the two booleans. We do minimal optimization if + # the optimized_code argument is present at all, since that's how the user of + # this function opts in, but we do compilation and linking in one step if the + # optimized_code argument is empty (as a micro-optimization). + # On GPU the C++ wrapper is just glue — the real kernels are compiled + # separately by Triton/CUDA. Always use -O1 to cut compile time. + min_optimize = optimized_code is not None or device_type != "cpu" + main_build_option = CppTorchDeviceOptions( + compile_only=bool(optimized_code), + min_optimize=min_optimize, + # pyrefly: ignore [bad-argument-type] + **compile_command, + ) + optimized_build_option = CppTorchDeviceOptions( + # pyrefly: ignore [bad-argument-type] + compile_only=True, + # pyrefly: ignore [bad-argument-type] + **compile_command, + ) + + def get_hashable_command_line(build_option: BuildOptionsBase) -> str: + """Writing the code to file will calculate a hash, which we need to vary if + the command line flags change. This implements a mostly-generic way of + validating that.""" + return CppBuilder( + name="o", sources="i", BuildOption=build_option + ).get_command_line() + + main_cmd_line = get_hashable_command_line(main_build_option) + optimized_cmd_line = get_hashable_command_line(optimized_build_option) + + key, main_path = write( + main_code, "main.cpp", extra=f"{optimized_code} {main_cmd_line}" + ) + + # Don't bother writing if the argument is empty. + if optimized_code: + _, optimized_path = write( + optimized_code, "optimized.cpp", extra=optimized_cmd_line + ) + else: + # Unused, but makes type checkers happy. + optimized_path = os.devnull + + if key not in cls.cache: + from torch.utils._filelock import FileLock + + lock_path = os.path.join(get_lock_dir(), key + ".lock") + future: Future[Any] | None = None + lib = None + + # if requested, pre-compile any headers + if config.cpp_cache_precompile_headers and not _IS_WINDOWS: + if header := cls._get_uncompiled_header(device_type): + main_build_option.precompiled_header = _precompile_header( + header, + main_cmd_line, + min_optimize=min_optimize, + **compile_command, + ) + + # Currently, the optimized_code field is only used for cpp kernel code, + # so go ahead and precompile the relevant header here. Revisit this + # decision if that ever changes. + if optimized_code and (header := _get_cpp_prefix_header(device_type)): + optimized_build_option.precompiled_header = _precompile_header( + # pyrefly: ignore [unbound-name] + header, + optimized_cmd_line, + **compile_command, + ) + + main_name, output_dir = get_name_and_dir_from_output_file_path(main_path) + main_builder = CppBuilder( + name=main_name, + sources=main_path, + BuildOption=main_build_option, + output_dir=output_dir, + ) + + if optimized_code: + optimized_name, _ = get_name_and_dir_from_output_file_path( + optimized_path + ) + optimized_builder = CppBuilder( + name=optimized_name, + sources=optimized_path, + BuildOption=optimized_build_option, + output_dir=output_dir, + ) + + linker = CppBuilder( + name=main_name, + sources=[ + main_builder.get_target_file_path(), + optimized_builder.get_target_file_path(), + ], + # pyrefly: ignore [bad-argument-type] + BuildOption=CppTorchDeviceOptions(**compile_command), + output_dir=output_dir, + ) + + worker_fn = functools.partial( + _worker_compile_cpp, + lock_path, + (main_builder, optimized_builder, linker), + ) + binary_path = normalize_path_separator(linker.get_target_file_path()) + else: + worker_fn = functools.partial( + _worker_compile_cpp, lock_path, (main_builder,) + ) + binary_path = normalize_path_separator( + main_builder.get_target_file_path() + ) + + def load_fn() -> Any: + nonlocal lib + if lib is None: + if future is not None: + future.result() + result = worker_fn() + assert result is None + lib = cls._load_library(binary_path, key) + assert lib is not None + return lib + + if submit_fn is not None: + with FileLock(lock_path, timeout=LOCK_TIMEOUT): + if not os.path.exists(binary_path): + future = submit_fn(worker_fn) + + cls.cache[key] = load_fn + + return cls.cache[key] + + @classmethod + def load(cls, *args: Any, **kwargs: Any) -> Any: + return cls.load_async(*args, **kwargs)() + + +def _worker_compile_cpp( + lock_path: str, + cpp_builders: Sequence[CppBuilder], +) -> None: + from torch.utils._filelock import FileLock + + with FileLock(lock_path, timeout=LOCK_TIMEOUT): + for builder in cpp_builders: + if not os.path.exists(builder.get_target_file_path()): + builder.build() + + +# Customized Python binding for cpp kernels +@clear_on_fresh_cache +class CppPythonBindingsCodeCache(CppCodeCache): + cache: dict[str, Callable[[], CDLL | ModuleType]] = {} + _loaded_module_names: OrderedSet[str] = OrderedSet() + + @staticmethod + def cache_clear() -> None: + CppPythonBindingsCodeCache.cache.clear() + for name in CppPythonBindingsCodeCache._loaded_module_names: + sys.modules.pop(name, None) + CppPythonBindingsCodeCache._loaded_module_names.clear() + + cpp_compile_command_flags = { + # kernels have no dependency on libtorch + "include_pytorch": False, + "shared": True, + } + entry_function = "kernel" + call_entry_function = "kernel({}); Py_RETURN_NONE;" + extra_parse_arg = "" + suffix_template = textwrap.dedent( + """ + // Python bindings to call {entry_func}(): + #define PY_SSIZE_T_CLEAN + #include + #include + #include + #include + + // This is defined in guards.cpp so we don't need to import PyTorch headers that are slooow. + // We manually link it below to workaround issues with fbcode build. + static void* (*_torchinductor_pyobject_tensor_data_ptr)(PyObject* obj); + + template static inline T parse_arg(PyObject* args, size_t n) {{ + static_assert(std::is_pointer_v, "arg type must be pointer or long"); + return static_cast(_torchinductor_pyobject_tensor_data_ptr(PyTuple_GET_ITEM(args, n))); + }} + template <> inline int64_t parse_arg(PyObject* args, size_t n) {{ + auto result = PyLong_AsSsize_t(PyTuple_GET_ITEM(args, n)); + if(result == -1 && PyErr_Occurred()) [[unlikely]] + throw std::runtime_error("expected int arg"); + return result; + }} + template <> inline uintptr_t parse_arg(PyObject* args, size_t n) {{ + auto result = PyLong_AsVoidPtr(PyTuple_GET_ITEM(args, n)); + if(result == reinterpret_cast(-1) && PyErr_Occurred()) [[unlikely]] + throw std::runtime_error("expected int arg"); + return reinterpret_cast(result); + }} + template <> inline float parse_arg(PyObject* args, size_t n) {{ + auto result = PyFloat_AsDouble(PyTuple_GET_ITEM(args, n)); + if(result == -1.0 && PyErr_Occurred()) [[unlikely]] + throw std::runtime_error("expected float arg"); + return static_cast(result); + }} + + {extra_parse_arg} + + static PyObject* {entry_func}_py(PyObject* self, PyObject* args) {{ + try {{ + if(!PyTuple_CheckExact(args)) [[unlikely]] + throw std::runtime_error("tuple args required"); + if(PyTuple_GET_SIZE(args) != {arg_len}) [[unlikely]] + throw std::runtime_error("requires {arg_len} args"); + {call_entry_func} + }} catch(std::exception const& e) {{ + PyErr_SetString(PyExc_RuntimeError, e.what()); + return nullptr; + }} catch(...) {{ + PyErr_SetString(PyExc_RuntimeError, "unhandled error"); + return nullptr; + }} + }} + + static PyMethodDef py_methods[] = {{ + {{"{entry_func}", {entry_func}_py, METH_VARARGS, ""}}, + {{NULL, NULL, 0, NULL}}}}; + + static struct PyModuleDef py_module = + {{PyModuleDef_HEAD_INIT, "{entry_func}", NULL, -1, py_methods}}; + + PyMODINIT_FUNC PyInit_{entry_func}(void) {{ + const char* str_addr = std::getenv("_TORCHINDUCTOR_PYOBJECT_TENSOR_DATA_PTR"); + if(!str_addr) {{ + PyErr_SetString(PyExc_RuntimeError, "_TORCHINDUCTOR_PYOBJECT_TENSOR_DATA_PTR must be set"); + return nullptr; + }} + + char* endptr = nullptr; + errno = 0; + uintptr_t addr = std::strtoull(str_addr, &endptr, 10); + if(errno != 0 || endptr == str_addr || addr == 0) {{ + PyErr_SetString(PyExc_RuntimeError, "Failed to parse _TORCHINDUCTOR_PYOBJECT_TENSOR_DATA_PTR"); + return nullptr; + }} + _torchinductor_pyobject_tensor_data_ptr = + reinterpret_cast(addr); + PyObject* module = PyModule_Create(&py_module); + if (module == NULL) {{ + return NULL; + }} + #ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED); + #endif + return module; + }} + """ + ) + + @classmethod + # pyrefly: ignore [bad-override] + def _load_library_inner(cls, path: str, key: str) -> ModuleType: + os.environ["_TORCHINDUCTOR_PYOBJECT_TENSOR_DATA_PTR"] = str( + torch._C._dynamo.guards._torchinductor_pyobject_tensor_data_ptr # type: ignore[attr-defined] + ) + module_name = f"{key}.{cls.entry_function}" + try: + return sys.modules[module_name] + except KeyError: + pass + spec = importlib.util.spec_from_file_location(module_name, path) + assert spec is not None + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + CppPythonBindingsCodeCache._loaded_module_names.add(module_name) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + @classmethod + def _get_uncompiled_header(cls, device: str) -> str | None: + return _get_cpp_prefix_header(device) + + @classmethod + def load_pybinding_async( + cls, + argtypes: Sequence[str], + main_code: str, + device_type: str = "cpu", + num_outputs: int = -1, + submit_fn: Any = None, + extra_flags: Sequence[str] = (), + kernel_code: str | None = None, + ) -> Any: + """ + Wrap a C++ function in fast Python bindings. + + Args: + argtypes: The types of args to ENTRY_FUNCTION(), e.g. ["float*", "long"] + main_code: C++ source code containing ENTRY_FUNCTION(). Will be built at + -O3 if kernel_code is None (to maximize performance in any kernels that + are present), or -O1 otherwise (to minimize compile time). + kernel_code: If present, C++ source code that will be built at -O3 and + linked to main_code. + + Returns: + A python version of ENTRY_FUNCTION() + """ + parseargs = ", ".join( + f"parse_arg<{argtype.replace('const ', '')}>(args, {n})" + for n, argtype in enumerate(argtypes) + ) + suffix = cls.suffix_template.format( + arg_len=len(argtypes), + call_entry_func=cls.call_entry_function.format(parseargs), + entry_func=cls.entry_function, + extra_parse_arg=cls.extra_parse_arg.format(array_len=num_outputs), + ) + get_result = cls.load_async( + main_code + suffix, + device_type, + submit_fn=submit_fn, + extra_flags=extra_flags, + optimized_code=kernel_code, + ) + result = None + + def future() -> Any: + nonlocal result + if result is None: + result = get_result() + assert isinstance(result, ModuleType) + return getattr(result, cls.entry_function) + + return future + + @classmethod + def load_pybinding(cls, *args: Any, **kwargs: Any) -> Any: + return cls.load_pybinding_async(*args, **kwargs)() + + +@clear_on_fresh_cache +class CppWrapperCodeCache(CppPythonBindingsCodeCache): + cache: dict[str, Callable[[], CDLL | ModuleType]] = {} + + @staticmethod + def cache_clear() -> None: + CppWrapperCodeCache.cache.clear() + + cpp_compile_command_flags = { + "include_pytorch": True, + "shared": True, + } + entry_function = "inductor_entry_cpp" + call_entry_function = "return inductor_entry_cpp({});" + extra_parse_arg = textwrap.dedent( + """ + #include + + static inline std::vector unpack_tensor_handle_list(PyObject* pyvec) {{ + std::vector result; + size_t result_len = PyList_GET_SIZE(pyvec); + result.reserve(result_len); + for (size_t i = 0; i < result_len; i++) {{ + // AtenTensorHandle is essentially a pointer + void* elem = PyCapsule_GetPointer(PyList_GET_ITEM(pyvec, i), NULL); + result.push_back(reinterpret_cast(elem)); + }} + return result; + }} + + static inline PyObject* pack_tensor_handle_list(const std::array& arr) {{ + PyObject* result = PyList_New({array_len}); + for (size_t i = 0; i < {array_len}; i++) {{ + PyObject *elem = + arr[i] == nullptr + ? Py_None + // Store AtenTensorHandle as PyCapsulate + : PyCapsule_New(reinterpret_cast(arr[i]), NULL, NULL); + PyList_SET_ITEM(result, i, elem); + }} + return result; + }} + + template <> inline std::vector parse_arg>(PyObject* args, size_t n) {{ + return unpack_tensor_handle_list(PyTuple_GET_ITEM(args, n)); + }} + + PyObject* inductor_entry_cpp(std::vector&& input_handles) {{ + // For outputs, we only allocate an array to hold returned tensor handles, + // not the actual output tensor storage. + std::array output_handles{{}}; + try {{ + inductor_entry_impl(input_handles.data(), output_handles.data()); + if (PyErr_Occurred()) {{ + return nullptr; + }} + return pack_tensor_handle_list(output_handles); + }} catch(std::exception const& e) {{ + PyErr_SetString(PyExc_RuntimeError, e.what()); + return nullptr; + }} catch(...) {{ + PyErr_SetString(PyExc_RuntimeError, "unhandled error"); + return nullptr; + }} + }} + """ + ) + + @classmethod + def _get_uncompiled_header(cls, device: str) -> str | None: + return _get_cpp_wrapper_header(device) + + +@clear_on_fresh_cache +class HalideCodeCache(CppPythonBindingsCodeCache): + cache: dict[str, Callable[[], ModuleType | CDLL]] = {} + cache_clear = staticmethod(cache.clear) + _standalone_runtime_path: str | None = None + prefix = textwrap.dedent( + """ + #include "{halideruntime_h}" + #include "{headerfile}" + #include + #include + + namespace c10 {{ + inline long div_floor_integer(long a, long b) {{ + if ((a<0) != (b<0)) {{ + const auto quot = a / b; + const auto rem = a % b; + return rem ? quot - 1 : quot; + }} + return a / b; + }} + }} + """ + ) + glue_template_cpp = prefix + textwrap.dedent( + """ + void kernel({argdefs}) {{ + {buffers} + int err = halide_kernel({buffer_names}); + if(err != 0) throw std::runtime_error("halide_kernel failed"); + }} + """ + ) + glue_template_cuda = prefix + textwrap.dedent( + """ + #include + static const halide_device_interface_t* cuda_interface = halide_cuda_device_interface(); + + void kernel({argdefs}, uintptr_t stream) {{ + {buffers} + int err = halide_kernel(reinterpret_cast(stream), {buffer_names}); + if(err != 0) throw std::runtime_error("halide_kernel failed"); + }} + """ + ) + standalone_runtime_cuda_init = textwrap.dedent( + """ + #include "{}" + #include + + static int acquire_context(void* user_context, + void** cuda_context_out, + bool create) {{ + return cuCtxGetCurrent(reinterpret_cast(cuda_context_out)); + }} + + static int release_context(void* user_context) {{ + return 0; + }} + + static int get_stream(void* user_context, + void* cuda_context, + void** stream_out) {{ + *stream_out = user_context; + return 0; + }} + + static int register_halide_hooks() {{ + halide_set_cuda_acquire_context(&acquire_context); + halide_set_cuda_release_context(&release_context); + halide_set_cuda_get_stream(&get_stream); + return 0; + }} + + int inductor_register_halide_hooks_result = register_halide_hooks(); + """ + ) + + @classmethod + def _codegen_buffer(cls, name: str, arg: HalideInputSpec, cuda: bool) -> list[str]: + assert arg.shape is not None + assert arg.stride is not None and len(arg.shape) == len(arg.stride) + assert arg.offset is not None + data_ptr = f"{arg.alias_of or arg.name} + {arg.offset}" + if cuda: + device = f"reinterpret_cast({data_ptr})" + device_interface = "cuda_interface" + host = "nullptr" + flags = "halide_buffer_flag_device_dirty" + else: + device = "0" + device_interface = "nullptr" + host = f"reinterpret_cast({data_ptr})" + flags = "halide_buffer_flag_host_dirty" + + dims = [] + for size, stride in zip(arg.shape, arg.stride): + dims.append(f"halide_dimension_t(0, {size}, {stride})") + + return [ + f"halide_buffer_t {name};", + f"halide_dimension_t {name}_dims[] = {{{', '.join(dims)}}};" + if len(dims) > 0 + else f"halide_dimension_t * {name}_dims = nullptr;", + f"{name}.device = {device};", + f"{name}.device_interface = {device_interface};", + f"{name}.host = {host};", + f"{name}.flags = {flags};", + f"{name}.type = {arg.halide_type()};", + f"{name}.dimensions = {len(dims)};", + f"{name}.dim = {name}_dims;", + f"{name}.padding = nullptr;", + ] + + @classmethod + def _codegen_glue(cls, meta: HalideMeta, headerfile: object) -> str: + is_cuda = meta.is_cuda() + assert is_cuda is ("user_context" in meta.target) + assert "no_runtime" in meta.target + buffers = [] + buffer_names = [] + for i, arg in enumerate(meta.argtypes): + if arg.is_buffer(): + # pyrefly: ignore [bad-argument-type] + buffer_names.append(f"&hl_buf_{i}") + buffers.extend(cls._codegen_buffer(f"hl_buf_{i}", arg, is_cuda)) + else: + assert "*" not in arg.ctype + # pyrefly: ignore [bad-argument-type] + buffer_names.append(arg.name) + buffers = "\n".join([f" {line}" for line in buffers]).lstrip() + + glue_template = cls.glue_template_cuda if is_cuda else cls.glue_template_cpp + glue_code = glue_template.format( + halideruntime_h=cls.find_header( + "HalideRuntimeCuda.h" if is_cuda else "HalideRuntime.h" + ), + headerfile=headerfile, + argdefs=", ".join( + f"{a.bindings_type()} {a.name}" + for a in meta.argtypes + if a.alias_of is None + ), + buffers=buffers, + buffer_names=", ".join(buffer_names), + ) + return glue_code + + @classmethod + @functools.cache + def config_hash(cls) -> str: + command_gen = CppBuilder( + name="O", + sources="I", + BuildOption=CppOptions(), + ) + command_line = command_gen.get_command_line() + return sha256_hash( + "\n".join( + [ + cls.glue_template_cpp, + cls.glue_template_cuda, + cls.standalone_runtime_cuda_init, + command_line, + ] + ).encode("utf-8") + ) + + @staticmethod + def _search_for_file(suffix: str, errmsg: str) -> str: + spec = importlib.machinery.PathFinder.find_spec("halide") + if spec is None or not spec.submodule_search_locations: + raise RuntimeError("halide python bindings not installed") + try: + search = spec.submodule_search_locations[0] + for file in os.listdir(search): + if file.endswith(".so"): + try: + out = subprocess.check_output( + ["ldd", os.path.join(search, file)] + ) + except subprocess.SubprocessError: + continue + m = re.search(r"(/.*)/libHalide.so", out.decode("utf-8")) + if m: + path = os.path.join(os.path.abspath(m.group(1)), suffix) + if os.path.exists(path): + return os.path.abspath(path) + except Exception as e: + raise RuntimeError(errmsg) from e + raise RuntimeError(errmsg) + + @staticmethod + @functools.cache + def find_libautoschedule(name: str) -> str: + sofile = f"libautoschedule_{name.lower()}.so" + if "HALIDE_LIB" in os.environ: + path = os.path.join(os.environ["HALIDE_LIB"], sofile) + if os.path.exists(path): + return path + errmsg = ( + f"Can't find {sofile}, set env HALIDE_LIB to the directory containing it" + ) + return HalideCodeCache._search_for_file(sofile, errmsg) + + @staticmethod + @functools.cache + def find_header(name: str) -> str: + if "HALIDE_INCLUDE" in os.environ: + path = os.path.join(os.environ["HALIDE_INCLUDE"], name) + if os.path.exists(path): + return path + if "HALIDE_LIB" in os.environ: + path = os.path.abspath( + os.path.join(os.environ["HALIDE_LIB"], f"../include/{name}") + ) + if os.path.exists(path): + return path + errmsg = ( + f"Can't find {name}, set env HALIDE_INCLUDE to the directory containing it" + ) + return HalideCodeCache._search_for_file(f"../include/{name}", errmsg) + + @classmethod + def generate_halide_async( + cls, meta: HalideMeta, source_code: str, submit_fn: Any = None + ) -> Callable[[], Any]: + dirpath = Path( + get_path( + code_hash( + source_code, + extra=repr((cls.config_hash(), meta)), + ), + "halide", + )[2] + ) + os.makedirs(dirpath, exist_ok=True) + wait_for_compile = None + genfile = str(dirpath / "generate_kernel.py") + libfile = str(dirpath / "halide_kernel.a") + headerfile = str(dirpath / "halide_kernel.h") + donefile = str(dirpath / "done") + lockfile = str(dirpath / "lock") + need_compile = not os.path.exists(donefile) + jobs: list[Any] = [] + if need_compile: + write_atomic(genfile, source_code) + cmd = [ + sys.executable, + genfile, + "-g", + "kernel", + "-o", + f"{dirpath}", + "-f", + "halide_kernel", + "-e", + "static_library,h,schedule", + ] + if meta.scheduler: + cmd.extend(["-p", cls.find_libautoschedule(meta.scheduler)]) + cmd.extend(meta.args()) + jobs.append(functools.partial(subprocess.check_call, cmd)) + + binding_types = [ + arg.bindings_type() for arg in meta.argtypes if arg.alias_of is None + ] + if meta.is_cuda(): + binding_types.append("uintptr_t") # stream + bindings_future = cls.load_pybinding_async( + binding_types, + cls._codegen_glue(meta, headerfile), + extra_flags=(libfile, cls.build_standalone_runtime()), + submit_fn=jobs.append if need_compile else None, + device_type="cuda" if meta.is_cuda() else "cpu", + ) + + if need_compile: + jobs.append(functools.partial(touch, donefile)) + task = functools.partial(_worker_task_halide, lockfile, jobs) + if submit_fn: + wait_for_compile = submit_fn(task).result + else: + task() + + def load() -> Callable[[], Any]: + if wait_for_compile: + wait_for_compile() + return bindings_future() + + return load + + @classmethod + def generate_halide(cls, *args: Any, **kwargs: Any) -> Callable[[], Any]: + return cls.generate_halide_async(*args, **kwargs)() + + @classmethod + def build_standalone_runtime(cls) -> str: + if cls._standalone_runtime_path and os.path.exists( + cls._standalone_runtime_path + ): + return cls._standalone_runtime_path + device_type = "cuda" if torch.cuda.is_available() else "cpu" + libname = "libStandaloneHalideRuntime.so" + target = "host-cuda" if device_type == "cuda" else "host" + if cls._standalone_runtime_path: + assert not os.path.exists(cls._standalone_runtime_path) + # We hit this case in unittests when we run with fresh_cache() + # Generating a fresh runtime over and over causes errors because we initialize + # cuda hundreds of times in the same process and run out of file descriptors. + # Workaround by jail breaking the current fresh_cache(). + base = default_cache_dir() + else: + base = cache_dir() + dirpath = Path(base) / f"halide-runtime-{target}-{cls.config_hash()}" + os.makedirs(dirpath, exist_ok=True) + done_file = str(dirpath / "done") + lock_file = str(dirpath / "lock") + hook_file = str(dirpath / "hooks.cpp") + a_file = str(dirpath / "standalone_halide_runtime.a") + so_file = str(dirpath / libname) + if not os.path.exists(done_file): + import halide as hl # type: ignore[import-untyped,import-not-found] + + from torch.utils._filelock import FileLock + + with FileLock(lock_file, LOCK_TIMEOUT): + if not os.path.exists(done_file): + with open(hook_file, "w") as f: + if device_type == "cuda": + f.write( + cls.standalone_runtime_cuda_init.format( + cls.find_header("HalideRuntimeCuda.h") + ) + ) + hl.compile_standalone_runtime(a_file, hl.Target(target)) + + name, output_dir = get_name_and_dir_from_output_file_path(so_file) + halide_cmd_gen = CppBuilder( + name=name, + sources=[hook_file, a_file], + output_dir=output_dir, + BuildOption=CppTorchDeviceOptions( + device_type=device_type, + ), + ) + + subprocess.check_call( + shlex.split(halide_cmd_gen.get_command_line()) + ) + touch(done_file) + assert os.path.exists(so_file) + cls._standalone_runtime_path = so_file + return so_file + + @classmethod + def _get_uncompiled_header(cls, device: str) -> str | None: + """Header precompiling is currently disabled for halide.""" + return None + + +def _worker_task_halide(lockfile: str, jobs: list[partial[Any]]) -> None: + from torch.utils._filelock import FileLock + + try: + with FileLock(lockfile, LOCK_TIMEOUT): + for job in jobs: + job() + except subprocess.SubprocessError as e: + if os.environ.get("HALIDE_REPRO") == "1": + cmd: list[Any] + python, script, *cmd = getattr(e, "cmd", ("", "", "")) + if os.path.basename(python).startswith("python"): + code = Path(script).read_text() + main = " hl.main()" + assert code.count(main) == 1 + + class Out: + def __repr__(self) -> str: + return "out" + + ci = cmd.index("-o") + assert isinstance(ci, int) + # pyrefly: ignore [unsupported-operation] + cmd[ci + 1] = Out() + repl = textwrap.indent( + textwrap.dedent( + f"""\ + import sys, tempfile + with tempfile.TemporaryDirectory() as out: + sys.argv = {["repro.py", *cmd]!r} + hl.main() + """ + ), + " ", + ) + code = code.replace(main, repl) + with open("repro.py", "w") as fd: + fd.write(code.lstrip()) + raise RuntimeError(f"wrote repro.py: {e}") from e + raise + + +def touch(filename: str) -> None: + with open(filename, "a"): + pass + + +@clear_on_fresh_cache +class PyCodeCache: + # Track the loaded modules so we can remove the on-disk artifacts when + # clearing the cache. Note also that we may load the same path more + # than once, but attach different attributes, i.e., due to different + # constant values. + modules: list[ModuleType] = [] + + # Modules loaded without extra attributes are stored here, those do not + # need to be re-loaded. + modules_no_attr: dict[str, ModuleType] = {} + + linemaps: dict[str, list[tuple[Any, ...]]] = {} + + @classmethod + def write(cls, source_code: str, extra: str = "") -> tuple[str, str]: + return write(source_code, "py", extra=extra) + + @classmethod + def load(cls, source_code: str, extra: str = "") -> ModuleType: + key, path = write(source_code, "py", extra=extra) + return cls.load_by_key_path(key, path) + + @classmethod + def load_by_key_path( + cls, + key: str, + path: str, + linemap: list[tuple[int, str]] | None = None, + attrs: dict[str, Any] | None = None, + ) -> ModuleType: + if linemap is None: + linemap = [] + + # we only cache when attrs is None + if attrs is None and path in cls.modules_no_attr: + return cls.modules_no_attr[path] + + in_toplevel = in_toplevel_process() + mod = _reload_python_module(key, path, set_sys_modules=in_toplevel) + + # unzip into separate lines/nodes lists + if in_toplevel: + cls.linemaps[path] = list(zip(*linemap)) + + if attrs is not None: + for k, v in attrs.items(): + setattr(mod, k, v) + + if in_toplevel: + # we only cache when attrs is None + if attrs is None: + cls.modules_no_attr[path] = mod + + cls.modules.append(mod) + return mod + + @classmethod + def cache_clear(cls, purge: bool = False) -> None: + """ + Clear the in-memory module cache. If purge=True, also delete all the + corresponding on-disk source files. + """ + if purge: + for mod in cls.modules: + try: + assert mod.__file__ + os.remove(mod.__file__) + except FileNotFoundError: + pass + cls.modules.clear() + cls.modules_no_attr.clear() + + @classmethod + @functools.cache + def stack_frames_for_code( + cls, path: str, lineno: int + ) -> list[dict[str, Any]] | None: + if path not in cls.linemaps: + return None + if len(cls.linemaps[path]) == 0: + return None + # [(starting_line, ), ...] + lines, nodes = cls.linemaps[path] + p = bisect_right(lines, lineno) + if p == 0: + return None + entry = nodes[p - 1] + if not entry: + return None + + def parse_stack_trace(stack_trace: str) -> list[dict[str, Any]]: + # ideally fx stores stack traces as data rather than a string + # but this is not along a performance critical path + regex = r'File "(.+)", line (\d+), in (.+)\n' + matches = re.findall(regex, stack_trace) + return [ + {"filename": f, "line": int(l), "name": n} + for f, l, n in reversed(matches) + ] + + return parse_stack_trace(entry) + + +def _load_triton_kernel_from_source( + kernel_name: str, source_code: str +) -> CachingAutotuner: + return getattr(PyCodeCache.load(source_code), kernel_name) + + +@torch_key_cache +def cutlass_key() -> bytes: + """ + Compute a key representing the state of the CUTLASS library. + + Note: OSS and fbcode will have different keys. + """ + if config.is_fbcode(): + with ( + importlib.resources.path( + "cutlass_library", "src_hash.txt" + ) as resource_path, + open(resource_path) as resource_file, + ): + return resource_file.read().encode() + + combined_hash = hashlib.sha256() + build_code_hash([config.cutlass.cutlass_dir], "", combined_hash) + return combined_hash.digest() + + +class DLLWrapper: + """A wrapper for a dynamic library.""" + + def __init__( + self, + lib_path: str, + ) -> None: + self.lib_path = lib_path + self.is_open = False + self.open() + + def open(self) -> None: + if not self.is_open: + self.DLL = cdll.LoadLibrary(self.lib_path) + self.is_open = True + + def close(self) -> None: + if self.is_open: + self._dlclose() + self.is_open = False + + def _dlclose(self) -> None: + f_dlclose = None + # During Python interpreter shutdown, importing modules or calling + # dlclose is unsafe. Silently skip cleanup in that case. + try: + import sys + + if sys.is_finalizing(): + return + except Exception: + # import machinery may already be torn down + return + if is_linux(): + syms = CDLL(None) + if not hasattr(syms, "dlclose"): + # Apline Linux + syms = CDLL("libc.so") + + if hasattr(syms, "dlclose"): + f_dlclose = syms.dlclose + elif is_windows(): + import ctypes + + kernel32 = ctypes.CDLL("kernel32", use_last_error=True) + + f_dlclose = kernel32.FreeLibrary + else: + raise NotImplementedError("Unsupported env, failed to do dlclose!") + + if f_dlclose is not None: + if is_linux(): + f_dlclose.argtypes = [c_void_p] + f_dlclose(self.DLL._handle) + elif is_windows(): + import ctypes + from ctypes import wintypes + + f_dlclose.argtypes = [wintypes.HMODULE] + f_dlclose(self.DLL._handle) + else: + log.warning( + "dll unloading function was not found, library may not be unloaded properly!" + ) + + def __getattr__(self, name: str) -> Callable[..., None]: + if not self.is_open: + raise RuntimeError(f"Cannot use closed DLL library: {self.lib_path}") + + method = getattr(self.DLL, name) + + def _wrapped_func(*args: Any) -> None: + err = method(*args) + if err: + raise RuntimeError(f"Error in function: {method.__name__}") + + return _wrapped_func + + def __enter__(self) -> Self: + return self + + def __exit__(self, *args: Any) -> None: + self.close() + + def __del__(self) -> None: + self.close() + + +@lru_cache +def binary_error_path(output_path: str) -> str: + """ + standard format for the error path + """ + return output_path + ".error" + + +class CUTLASSCodeCache: + """ + A cache for managing the compilation and loading source code specifically for CUTLASS. + This class handles writing source code to files, compiling them into shared objects, and caching + the results to avoid redundant compilations. It also manages error handling and logging for the + compilation process. + """ + + @dataclasses.dataclass + class CacheEntry: + input_path: str + output_path: str + error_json: str | None = None + + cache: dict[str, CacheEntry] = {} + aot_kernels_o: list[str] = [] + + _SOURCE_CODE_SUFFIX: str = "" + _BACKEND: str = "" + + @classmethod + def cache_clear(cls) -> None: + cls.cache.clear() + cls.aot_kernels_o.clear() + cls.write.cache_clear() + + @staticmethod + @lru_cache(maxsize=4) + def get_kernel_binary_remote_cache( + caching_enabled: bool, caching_available: bool + ) -> Any | None: + """ + Get or create the class instance of the CUTLASSKernelBinaryRemoteCache. + + Args: + caching_enabled: Whether binary remote caching is enabled + caching_available: Whether we're in fbcode environment + + Returns: + CUTLASSKernelBinaryRemoteCache: The class instance of the kernel binary remote cache + """ + if not caching_enabled: + log.debug("CUTLASSKernelBinaryRemoteCache not requested, skipping") + return None + if not caching_available: + return None + + try: + from torch._inductor.fb.kernel_binary_remote_cache import ( + CUTLASSKernelBinaryRemoteCache, + ) + + return CUTLASSKernelBinaryRemoteCache() + except ImportError: + log.debug( + "CUTLASSKernelBinaryRemoteCache not available, remote caching disabled" + ) + return None + + @classmethod + def _use_re_build(cls) -> bool: + raise NotImplementedError + + @classmethod + def _compile_command( + cls, + src_files: list[str], + dst_file: str, + dst_file_ext: str, + extra_args: list[str] | None = None, + ) -> str: + raise NotImplementedError + + @classmethod + def _source_code_extra(cls) -> str: + raise NotImplementedError + + @classmethod + @lru_cache(None) + def write(cls, source_code: str, dst_file_ext: str) -> tuple[str, str]: + """ + Writes source code into a file with dst_file_ext as the file extension. + Returns the hash key of source code, and the path to the file. + """ + + if config.cutlass.cutlass_hash_with_compile_cmd: + compile_command = repr( + cls._compile_command(["dummy_input"], "dummy_output", dst_file_ext) + ) + extra = compile_command + else: + extra = cls._source_code_extra() + + key, input_path = write(source_code, cls._SOURCE_CODE_SUFFIX, extra=extra) + return key, input_path + + @classmethod + def compile( + cls, source_code: str, dst_file_ext: str, extra_args: list[str] | None = None + ) -> tuple[str, str, str]: + """ + Compiles CUDA source_code into a file with dst_file_ext extension. + If dst_file_ext is "so", first compiles to ".o" and then links to ".so". + Returns a tuple of dst_file_path, hash_key, source_code_path + """ + if dst_file_ext == "so": + # Two-step compilation: first compile to .o, then link to .so + obj_path, _, _ = cls.compile(source_code, "o", extra_args) + key, input_path = cls.write(source_code, dst_file_ext) + src_files, operation_name = [obj_path], "Linking" + else: + # Regular compilation for non-.so files + key, input_path = cls.write(source_code, dst_file_ext) + src_files, operation_name = [input_path], "Compilation" + + key_with_ext = key + dst_file_ext + if key_with_ext not in cls.cache: + from torch.utils._filelock import FileLock + + lock_dir = get_lock_dir() + lock = FileLock(os.path.join(lock_dir, key + ".lock"), timeout=LOCK_TIMEOUT) + with lock: + output_path = input_path[: -len(cls._SOURCE_CODE_SUFFIX)] + dst_file_ext + error_path = binary_error_path(output_path) + binary_remote_cache = cls.get_kernel_binary_remote_cache( + caching_enabled=config.cutlass.use_binary_remote_cache + and not config.force_disable_caches, + caching_available=config.is_fbcode(), + ) + if binary_remote_cache is not None: + # The remote cache implementation will only download if the file does + # not already exist locally + binary_remote_cache.get(output_path, error_path) + + if os.path.exists(error_path): + with open(error_path, encoding="utf-8") as fh: + error_json = fh.read() + cmd_parts, error_output = json.loads(error_json) + if ( + binary_remote_cache is not None + and config.cutlass.upload_to_binary_remote_cache + ): + # This ensures that a local error is uploaded to the remote cache, + # as we make no assumptions about the remote cache having the same + # information as the local cache + binary_remote_cache.put( + error_path, config.cutlass.binary_remote_cache_force_write + ) + cls.cache[key_with_ext] = cls.CacheEntry( + input_path, output_path, error_json + ) + raise exc.CUDACompileError(cmd_parts, error_output) + if not os.path.exists(output_path): + cmd = cls._compile_command( + src_files, output_path, dst_file_ext, extra_args + ) + with open(input_path, "a") as f: + f.write("\n") + f.write(f"// {cls._BACKEND} {operation_name} cmd\n// {cmd}\n") + start_time = time() + log.debug("%s %s: %s", cls._BACKEND, operation_name, cmd) + cmd_parts = cmd.split(" ") + try: + if cls._use_re_build(): + from triton.fb.re_build_helper import run_build_command + + run_build_command( + cmd_parts, + os.path.dirname(input_path), + os.path.basename(output_path), + ) + else: + subprocess.check_output( + cmd_parts, stderr=subprocess.STDOUT, env=os.environ + ) + except subprocess.CalledProcessError as error: + cls._record_compile_error( + error.output.decode("utf-8"), + key_with_ext, + cmd_parts, + input_path, + output_path, + binary_remote_cache, + ) + raise exc.CUDACompileError(cmd_parts, error.output) from error + except Exception as error: + if "COMPILE FAILED WITH" in str(error): + cls._record_compile_error( + str(error), + key_with_ext, + cmd_parts, + input_path, + output_path, + binary_remote_cache, + ) + raise exc.CUDACompileError(cmd_parts, str(error)) from error + raise error + end_time = time() + log_duration_msg = f"{cls._BACKEND} {operation_name} took {end_time - start_time} seconds. Command: {cmd}" + log.info(log_duration_msg) + + else: + log.debug( + "%s %s skipped: %s since output already exists", + cls._BACKEND, + operation_name, + output_path, + ) + # Upload to remote cache if enabled + if ( + binary_remote_cache is not None + and config.cutlass.upload_to_binary_remote_cache + ): + # will log on errors, but not fail out + binary_remote_cache.put( + output_path, config.cutlass.binary_remote_cache_force_write + ) + cls.cache[key_with_ext] = cls.CacheEntry(input_path, output_path, None) + + cache_entry: CUTLASSCodeCache.CacheEntry = cls.cache[key_with_ext] + if cache_entry.error_json is not None: + # Restore cached Exception and raise it as if we had compiled + cmd_parts, error_output = json.loads(cache_entry.error_json) + raise exc.CUDACompileError(cmd_parts, error_output.encode("utf-8")) + return (cls.cache[key_with_ext].output_path, key, input_path) + + @classmethod + def load(cls, source_code: str, dst_file_ext: str) -> tuple[DLLWrapper, str, str]: + """ + Compiles source code and loads the generated .so file. + Returns a tuple of DLLWrapper, hash_key, source_code_path + """ + + if dst_file_ext != "so": + raise RuntimeError( + f"Only support loading a .so file for now. " + f"Requested file extension: {dst_file_ext}. Source code: {source_code}" + ) + dst_file_path, hash_key, source_code_path = cls.compile( + source_code, dst_file_ext + ) + return (DLLWrapper(dst_file_path), hash_key, source_code_path) + + @classmethod + def _record_compile_error( + cls, + error_str: str, + key_with_ext: str, + cmd_parts: list[str], + input_path: str, + output_path: str, + # Any here, as the import and type will only work in fbcode + # TODO: Make the typing hint strong here + binary_remote_cache: Any = None, + ) -> None: + error_json = json.dumps([cmd_parts, error_str]) + cls.cache[key_with_ext] = cls.CacheEntry(input_path, output_path, error_json) + error_path = binary_error_path(output_path) + with open(error_path, "w", encoding="utf-8") as fh: + fh.write(error_json) + + # Upload to remote cache directly from memory if enabled + if ( + binary_remote_cache is not None + and config.cutlass.upload_to_binary_remote_cache + ): + binary_remote_cache.put( + error_path, config.cutlass.binary_remote_cache_force_write + ) + + +@clear_on_fresh_cache +class CUDACodeCache(CUTLASSCodeCache): + _SOURCE_CODE_SUFFIX = "cu" + _BACKEND = "CUDA" + + @classmethod + def _use_re_build(cls) -> bool: + return cuda_compile_utils.use_re_build() + + @classmethod + def _compile_command( + cls, + src_files: list[str], + dst_file: str, + dst_file_ext: str, + extra_args: list[str] | None = None, + ) -> str: + return cuda_compile_utils.cuda_compile_command( + src_files, dst_file, dst_file_ext, extra_args=extra_args + ) + + @classmethod + def _source_code_extra(cls) -> str: + extra = repr( + [ + # nvcc and cuda hash + cuda_compile_utils._cuda_compiler(), + # cutlass flags and gcc hash + cuda_compile_utils._nvcc_compiler_options(), + # flags + cuda_compile_utils._nvcc_host_compiler_options(), + # cutlass key + cutlass_key(), + # hack to deal with AOTI .o compilation + ] + ) + return extra + + +from torch._inductor.codegen.xpu import compile_utils as xpu_compile_utils + + +@clear_on_fresh_cache +class XPUCodeCache(CUTLASSCodeCache): + _SOURCE_CODE_SUFFIX = "cpp" + _BACKEND = "XPU" + dll_cache: dict[str, DLLWrapper] = {} + + @classmethod + def _use_re_build(cls) -> bool: + return False + + @classmethod + def _compile_command( + cls, + src_files: list[str], + dst_file: str, + dst_file_ext: str, + extra_args: list[str] | None = None, + ) -> str: + return xpu_compile_utils.xpu_compile_command( + src_files, dst_file, dst_file_ext, extra_args=extra_args + ) + + @classmethod + def _source_code_extra(cls) -> str: + extra = repr( + [ + xpu_compile_utils._sycl_compiler(), + xpu_compile_utils._sycl_compiler_options(), + cutlass_key(), + ] + ) + return extra + + @classmethod + def load(cls, source_code: str, dst_file_ext: str) -> tuple[DLLWrapper, str, str]: + """ + Compiles source code and loads the generated .so file. + Returns a tuple of DLLWrapper, hash_key, source_code_path + """ + + if dst_file_ext != "so": + raise RuntimeError( + f"Only support loading a .so file for now. " + f"Requested file extension: {dst_file_ext}. Source code: {source_code}" + ) + dst_file_path, hash_key, source_code_path = cls.compile( + source_code, dst_file_ext + ) + if dst_file_path not in cls.dll_cache: + cls.dll_cache[dst_file_path] = DLLWrapper(dst_file_path) + + return (cls.dll_cache[dst_file_path], hash_key, source_code_path) + + +@clear_on_fresh_cache +class ROCmCodeCache: + @dataclasses.dataclass + class CacheEntry: + input_path: str + output_path: str + + cache: dict[str, CacheEntry] = {} + aot_kernels_o: list[str] = [] + _SOURCE_CODE_SUFFIX = "cpp" + _logged_compiler_version = False + + @staticmethod + def cache_clear() -> None: + ROCmCodeCache.cache.clear() + ROCmCodeCache.aot_kernels_o.clear() + + @classmethod + def write(cls, source_code: str, dst_file_ext: str) -> tuple[str, str]: + """ + Writes source code into a file with dst_file_ext as the file extension. + Returns the hash key of source code, and the path to the file. + """ + + cuda_command = repr( + rocm_compile_command(["dummy_input"], "dummy_output", dst_file_ext) + ) + key, input_path = write( + source_code, cls._SOURCE_CODE_SUFFIX, extra=cuda_command + ) + return key, input_path + + @classmethod + def compile( + cls, source_code: str, dst_file_ext: str, extra_args: list[str] | None = None + ) -> tuple[str, str, str]: + """ + Compiles source_code into a file with dst_file_ext extension, + using the compile command specific for the ROCm platform. + Returns a tuple of dst_file_path, hash_key, source_code_path + """ + if not cls._logged_compiler_version: + cls._logged_compiler_version = True + log.debug(get_compiler_version_info(str(rocm_compiler()))) + + key, input_path = cls.write(source_code, dst_file_ext) + if key not in cls.cache: + from torch.utils._filelock import FileLock + + lock_dir = get_lock_dir() + lock = FileLock(os.path.join(lock_dir, key + ".lock"), timeout=LOCK_TIMEOUT) + with lock: + output_path = input_path[: -len(cls._SOURCE_CODE_SUFFIX)] + dst_file_ext + if not os.path.exists(output_path): + cmd = rocm_compile_command( + [input_path], output_path, dst_file_ext, extra_args + ) + start_time = time() + cmd_parts = cmd.split(" ") + try: + output = subprocess.check_output( + cmd_parts, + stderr=subprocess.STDOUT, + text=True, + env=os.environ, + ) + log.debug("Compilation output: %s", output) + except subprocess.CalledProcessError as error: + raise exc.CUDACompileError(cmd_parts, error.output) from error + end_time = time() + log_duration_msg = f"Compilation took {end_time - start_time} seconds. Compile command: {cmd}" + log.info(log_duration_msg) + else: + log.debug( + "Skip compiling %s: output %s already exists", + input_path, + output_path, + ) + cls.cache[key] = ROCmCodeCache.CacheEntry(input_path, output_path) + + return (cls.cache[key].output_path, key, input_path) + + @classmethod + def load(cls, source_code: str, dst_file_ext: str) -> tuple[DLLWrapper, str, str]: + """ + Compiles source code and loads the generated .so file. + Returns a tuple of DLLWrapper, hash_key, source_code_path + """ + + if dst_file_ext != "so": + raise RuntimeError( + f"Only support loading a .so file for now. " + f"Requested file extension: {dst_file_ext}. Source code: {source_code}" + ) + dst_file_path, hash_key, source_code_path = cls.compile( + source_code, dst_file_ext + ) + return (DLLWrapper(dst_file_path), hash_key, source_code_path) + + +class CodeCacheFuture: + def result(self) -> Callable[..., Any]: + raise NotImplementedError + + +class LambdaFuture(CodeCacheFuture): + def __init__( + self, result_fn: Callable[..., Any], future: Future[Any] | None = None + ) -> None: + self.result_fn = result_fn + self.future = future + + def result(self) -> Callable[..., Any]: + return self.result_fn() + + +class StaticAutotunerFuture(CodeCacheFuture): + """ + A statically launchable CachingAutotuner, loaded from TritonBundler + """ + + def __init__(self, static_autotuner: CachingAutotuner) -> None: + # Pickled version of CachingAutotuner + self.static_autotuner = static_autotuner + # This needs to be set in AsyncCompile.triton, in case + # we need to reload the CachingAutotuner from its source code + # We don't store the source code on the CachingAutotuner itself + # since it can be very large. + self.reload_kernel_from_src: Callable[[], Any] | None = None + + def result(self) -> CachingAutotuner: + assert self.reload_kernel_from_src is not None + with dynamo_timed("StaticAutotunerFuture.warm_precompile"): + self.static_autotuner.recheck_autotune_cache( + reload_kernel_from_src=self.reload_kernel_from_src + ) + self.static_autotuner.precompile( # type: ignore[union-attr] + warm_cache_only=False, + reload_kernel=self.reload_kernel_from_src, + static_triton_bundle_key=None, # no need to save again + ) + return self.static_autotuner diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/aoti_hipify_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/aoti_hipify_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..eca4f85ced9260e9122db73366ab4a136b7cc4ab --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/aoti_hipify_utils.py @@ -0,0 +1,36 @@ +import re + +import torch + + +# It is not a good idea to directly apply hipify_torch to codegen, which will be vulnerable to cases like: +# "... +# from ..codecache import CudaKernelParamCache +# ..." +# In such cases, we do not need to hipify_torch the original class/file name in codegen/codecache + + +def maybe_hipify_code_wrapper(source_codes: str, force_hipify: bool = False) -> str: + if torch.version.hip is None and not force_hipify: + return source_codes + + try: + from torch.utils.hipify.hipify_python import PYTORCH_MAP, PYTORCH_TRIE + except ImportError: + # hipify not available for non-AMD builds + return source_codes + + def c2_repl(m: re.Match[str]) -> object: + return PYTORCH_MAP[m.group(0)] + + # We need to redefine RE_PYTORCH_PREPROCESSOR here since in hipify_torch, + # it will apply positive lookbehind (?<=\W) to the pattern to avoid matching + # keyword at the beginning of code line. However, this can happen in codegen, + # which will cause the pattern to not match. + + # Note that lookahead (?=\W) is still needed to keep hipification idomponent, for example + # we need to skip replacing "getStreamFromExternal" in "getStreamFromExternalMasqueradingAsCUDA" + RE_PYTORCH_PREPROCESSOR = re.compile(rf"({PYTORCH_TRIE.export_to_regex()})(?=\W)") + + source_codes = RE_PYTORCH_PREPROCESSOR.sub(c2_repl, source_codes) # type: ignore[arg-type] + return source_codes diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/aoti_runtime/interface.cpp b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/aoti_runtime/interface.cpp new file mode 100644 index 0000000000000000000000000000000000000000..515ab89d1f2d1187fe2855733ecbbc523fbeae0a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/aoti_runtime/interface.cpp @@ -0,0 +1,488 @@ +// Definition of AOTI runtime interface functions + +#include +#include + +#include +#include + +#define CONVERT_EXCEPTION_TO_ERROR_CODE(...) \ + try { \ + __VA_ARGS__ \ + } catch (const std::exception& e) { \ + std::cerr << "Error: " << e.what() << '\n'; \ + return AOTI_RUNTIME_FAILURE; \ + } catch (...) { \ + std::cerr << "Unknown exception occurred.\n"; \ + return AOTI_RUNTIME_FAILURE; \ + } \ + return AOTI_RUNTIME_SUCCESS; + +#define AOTI_VECTOR_SIZE_CHECK(actual_size, expected_size, name) \ + do { \ + AOTI_RUNTIME_CHECK( \ + actual_size == expected_size, \ + "expected " + std::string(name) + " vector size to be " + \ + std::to_string(expected_size) + ", but got " + \ + std::to_string(actual_size)); \ + } while (0) + +// AOTInductor uses at::addmm_out, which doesn't supports +// arguments that requires gradient. For this reason, we +// enforce no_grad context for run APIs. +// +// A RAII, thread local (!) guard that enables or disables grad mode upon +// construction, and sets it back to the original value upon destruction. +struct AOTINoGradGuard { + AOTINoGradGuard() { + aoti_torch_grad_mode_set_enabled(false); + } + AOTINoGradGuard(const AOTINoGradGuard&) = delete; + AOTINoGradGuard(AOTINoGradGuard&&) noexcept = delete; + ~AOTINoGradGuard() { + aoti_torch_grad_mode_set_enabled(prev_mode); + } + AOTINoGradGuard& operator=(const AOTINoGradGuard&) = delete; + AOTINoGradGuard& operator=(AOTINoGradGuard&&) noexcept = delete; + bool prev_mode{aoti_torch_grad_mode_is_enabled()}; +}; + +extern "C" { + +AOTIRuntimeError AOTInductorModelContainerCreate( + AOTInductorModelContainerHandle* container_handle, + size_t num_models, + bool is_cpu, + const char* cubin_dir) { + return AOTInductorModelContainerCreateWithDevice( + container_handle, + num_models, + is_cpu ? "cpu" : "cuda", + cubin_dir); +} + +AOTIRuntimeError AOTInductorModelContainerCreateWithDevice( + AOTInductorModelContainerHandle* container_handle, + size_t num_models, + const char* device_str, + const char* cubin_dir) { + + if (num_models == 0) { + std::cerr << "Error: num_models must be positive, but got 0\n"; + return AOTI_RUNTIME_FAILURE; + } + CONVERT_EXCEPTION_TO_ERROR_CODE({ + std::optional cubin_dir_opt; + if (cubin_dir != nullptr) { + cubin_dir_opt.emplace(cubin_dir); + } + auto* container = new torch::aot_inductor::AOTInductorModelContainer( + num_models, std::string(device_str), cubin_dir_opt); + *container_handle = + reinterpret_cast(container); + }) +} + + +AOTIRuntimeError AOTInductorModelContainerDelete( + AOTInductorModelContainerHandle container_handle) { + CONVERT_EXCEPTION_TO_ERROR_CODE({ + auto* container = + reinterpret_cast( + container_handle); + delete container; + }); +} + +AOTIRuntimeError AOTInductorModelContainerRun( + AOTInductorModelContainerHandle container_handle, + AtenTensorHandle* input_handles, // array of input AtenTensorHandle; handles + // are stolen; the array itself is borrowed + size_t num_inputs, + AtenTensorHandle* + output_handles, // array for writing output AtenTensorHandle; handles + // will be stolen by the caller; the array itself is + // borrowed + size_t num_outputs, + AOTInductorStreamHandle stream_handle, + AOTIProxyExecutorHandle proxy_executor_handle) { + auto* container = + reinterpret_cast( + container_handle); + AOTI_VECTOR_SIZE_CHECK(num_inputs, container->num_inputs(), "inputs"); + AOTI_VECTOR_SIZE_CHECK(num_outputs, container->num_outputs(), "outputs"); + + auto stream = + reinterpret_cast(stream_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE({ + AOTINoGradGuard guard; + container->run( + input_handles, output_handles, stream, proxy_executor_handle); + }) +} + +AOTIRuntimeError AOTInductorModelContainerRunSingleThreaded( + AOTInductorModelContainerHandle container_handle, + AtenTensorHandle* input_handles, // array of input AtenTensorHandle; handles + // are stolen; the array itself is borrowed + size_t num_inputs, + AtenTensorHandle* + output_handles, // array for writing output AtenTensorHandle; handles + // will be stolen by the caller; the array itself is + // borrowed + size_t num_outputs, + AOTInductorStreamHandle stream_handle, + AOTIProxyExecutorHandle proxy_executor_handle) { + auto* container = + reinterpret_cast( + container_handle); + AOTI_VECTOR_SIZE_CHECK(num_inputs, container->num_inputs(), "inputs"); + AOTI_VECTOR_SIZE_CHECK(num_outputs, container->num_outputs(), "outputs"); + + auto stream = + reinterpret_cast(stream_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE({ + AOTINoGradGuard guard; + container->run_single_threaded( + input_handles, output_handles, stream, proxy_executor_handle); + }) +} + +AOTIRuntimeError AOTInductorModelContainerGetNumConstants( + AOTInductorModelContainerHandle container_handle, + size_t* num_constants) { + auto* container = + reinterpret_cast( + container_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE( + { *num_constants = container->num_constants(); }) +} + +AOTIRuntimeError AOTInductorModelContainerGetConstantName( + AOTInductorModelContainerHandle container_handle, + size_t idx, + const char** name) { + auto* container = + reinterpret_cast( + container_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE( + { *name = container->constant_name(idx); }) +} + +AOTIRuntimeError AOTInductorModelContainerGetConstantOriginalFQN( + AOTInductorModelContainerHandle container_handle, + size_t idx, + const char** original_fqn) { + auto* container = + reinterpret_cast( + container_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE( + { *original_fqn = container->constant_original_fqn(idx); }) +} + +AOTIRuntimeError AOTInductorModelContainerGetConstantFromFolded( + AOTInductorModelContainerHandle container_handle, + size_t idx, + bool* from_folded) { + auto* container = + reinterpret_cast(container_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE({ *from_folded = container->constant_from_folded(idx); }) +} + +AOTIRuntimeError AOTInductorModelContainerGetConstantType( + AOTInductorModelContainerHandle container_handle, + size_t idx, + int32_t* type) { + auto* container = + reinterpret_cast(container_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE({ *type = container->constant_type(idx); }) +} + +AOTIRuntimeError AOTInductorModelContainerGetConstantDtype( + AOTInductorModelContainerHandle container_handle, + size_t idx, + int32_t* dtype) { + auto* container = + reinterpret_cast( + container_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE( + { *dtype = container->constant_dtype(idx); }) +} + +AOTIRuntimeError AOTInductorModelContainerGetConstantDataSize( + AOTInductorModelContainerHandle container_handle, + size_t idx, + size_t* data_size) { + auto* container = + reinterpret_cast( + container_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE( + { *data_size = container->constant_data_size(idx); }) +} + +AOTIRuntimeError AOTInductorModelContainerExtractConstantsMap( + AOTInductorModelContainerHandle container_handle, + AOTInductorConstantMapHandle constant_map_handle, + bool use_inactive) { + auto* container = + reinterpret_cast( + container_handle); + auto constants_map = reinterpret_cast*>(constant_map_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE( + { const auto ret = container->extract_constants_map(use_inactive); + for (const auto& pair: ret) { + constants_map->emplace(pair.first, pair.second); + } + }) +} + +AOTIRuntimeError AOTInductorModelContainerUpdateUserManagedConstantBuffer( + AOTInductorModelContainerHandle container_handle, + AOTInductorConstantMapHandle constant_map_handle, + bool use_inactive, + bool validate_full_update) { + auto* container = + reinterpret_cast( + container_handle); + auto input_map = reinterpret_cast*>(constant_map_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE({ + container->update_constant_buffer( + *input_map, use_inactive, validate_full_update, /* user_managed = */ true); + }) +} + +AOTIRuntimeError AOTInductorModelContainerUpdateUserManagedConstantBufferPairs( + AOTInductorModelContainerHandle container_handle, + const AOTInductorConstantMapEntry* pairs, + size_t num_pairs, + bool use_inactive, + bool validate_full_update) { + auto* container = + reinterpret_cast(container_handle); + // Build a local unordered_map inside + std::unordered_map input_map; + input_map.reserve(num_pairs); + for (size_t i = 0; i < num_pairs; ++i) { + input_map.emplace(pairs[i].name, pairs[i].handle); + } + CONVERT_EXCEPTION_TO_ERROR_CODE({ + container->update_constant_buffer( + input_map, use_inactive, validate_full_update, /*user_managed=*/true); + }) +} + +AOTIRuntimeError AOTInductorModelContainerUpdateConstantBuffer( + AOTInductorModelContainerHandle container_handle, + AOTInductorConstantMapHandle constant_map_handle, + bool use_inactive, + bool validate_full_update) { + auto* container = + reinterpret_cast( + container_handle); + auto input_map = reinterpret_cast*>(constant_map_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE({ + container->update_constant_buffer( + *input_map, use_inactive, validate_full_update); + }) +} + +AOTIRuntimeError AOTInductorModelContainerUpdateInactiveConstantBuffer( + AOTInductorModelContainerHandle container_handle, + AOTInductorConstantMapHandle constant_map_handle) { + return AOTInductorModelContainerUpdateConstantBuffer(container_handle, + constant_map_handle, + /*use_inactive*/ true, + /*validate_full_update*/ true); +} + +AOTIRuntimeError AOTInductorModelContainerFreeInactiveConstantBuffer( + AOTInductorModelContainerHandle container_handle) { + auto* container = + reinterpret_cast( + container_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE({ + container->free_inactive_constant_buffer(); + }) +} + +AOTIRuntimeError AOTInductorModelContainerRunConstantFolding( + AOTInductorModelContainerHandle container_handle, + bool use_inactive, + AOTInductorStreamHandle stream_handle, + AOTIProxyExecutorHandle proxy_executor_handle) { + auto* container = + reinterpret_cast( + container_handle); + auto stream = + reinterpret_cast(stream_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE({ + AOTINoGradGuard guard; + container->run_const_fold(use_inactive, stream, proxy_executor_handle); + }) +} + +AOTIRuntimeError AOTInductorModelContainerSwapConstantBuffer( + AOTInductorModelContainerHandle container_handle) { + auto* container = + reinterpret_cast( + container_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE({ + container->swap_constant_buffer(); + }) +} + +AOTIRuntimeError AOTInductorModelContainerGetNumInputs( + AOTInductorModelContainerHandle container_handle, + size_t* ret_num_inputs) { + auto* container = + reinterpret_cast( + container_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE( + { *ret_num_inputs = container->num_inputs(); }) +} + +AOTIRuntimeError AOTInductorModelContainerGetInputName( + AOTInductorModelContainerHandle container_handle, + size_t input_idx, + const char** ret_input_names) { + auto* container = + reinterpret_cast( + container_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE( + { *ret_input_names = container->input_name(input_idx); }) +} + +AOTIRuntimeError AOTInductorModelContainerGetNumOutputs( + AOTInductorModelContainerHandle container_handle, + size_t* ret_num_outputs) { + auto* container = + reinterpret_cast( + container_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE( + { *ret_num_outputs = container->num_outputs(); }) +} + +AOTIRuntimeError AOTInductorModelContainerGetOutputName( + AOTInductorModelContainerHandle container_handle, + size_t output_idx, + const char** ret_output_names) { + auto* container = + reinterpret_cast( + container_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE( + { *ret_output_names = container->output_name(output_idx); }) +} + +AOTIRuntimeError AOTInductorModelContainerGetCallSpec( + AOTInductorModelContainerHandle container_handle, + const char** in_spec, + const char** out_spec) { + auto* container = + reinterpret_cast( + container_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE({ + *in_spec = container->get_in_spec(); + *out_spec = container->get_out_spec(); + }) +} + +AOTIRuntimeError AOTInductorModelCreate( + AOTInductorModelHandle* model_handle, + AOTInductorConstantMapHandle constant_map_handle){ + CONVERT_EXCEPTION_TO_ERROR_CODE({ + auto constant_map = std::make_shared(); + auto constant_array = std::make_shared>(); + auto input_map = reinterpret_cast*>(constant_map_handle); + + auto model = new torch::aot_inductor::AOTInductorModel( + constant_map, + constant_array, + "cpu", // device_str is hardcoded, as AOTInductorModelCreate is only use for CPU models + "" + ); + + if (input_map) { + for (auto const& kv : *input_map) { + constant_map->emplace(kv.first, kv.second); + } + } else { + model->load_constants(); + } + + *model_handle = reinterpret_cast(model); + })} + +AOTIRuntimeError AOTInductorModelRun( + AOTInductorModelHandle model_handle, + AtenTensorHandle* input_handles, + AtenTensorHandle* output_handles) { + auto model = + reinterpret_cast(model_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE({ + AOTINoGradGuard guard; + model->run_impl( + input_handles, + output_handles, + (torch::aot_inductor::DeviceStreamType) nullptr, + nullptr); + }) +} + +AOTIRuntimeError AOTInductorModelDelete(AOTInductorModelHandle model_handle){ + CONVERT_EXCEPTION_TO_ERROR_CODE({ + auto model = reinterpret_cast( + model_handle); + delete model; + })} + +AOTIRuntimeError AOTInductorModelGetNumOutputs( + AOTInductorModelHandle model_handle, + size_t* ret_num_outputs) { + CONVERT_EXCEPTION_TO_ERROR_CODE({ + auto model = reinterpret_cast(model_handle); + *ret_num_outputs = model->num_outputs(); + }) +} + +AOTIRuntimeError AOTInductorModelUpdateConstantsMap( + AOTInductorModelHandle model_handle, + AOTInductorConstantMapHandle constant_map_handle) { + auto model = + reinterpret_cast(model_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE({ + auto constant_map = std::make_shared(); + auto input_map = + reinterpret_cast*>( + constant_map_handle); + + for (auto const& kv : *input_map) { + constant_map->emplace(kv.first, kv.second); + } + model->update_constants_map(std::move(constant_map)); + }) +} + +AOTIRuntimeError AOTInductorModelContainerGetConstantsBlobSize( + AOTInductorModelContainerHandle container_handle, + uint64_t* ret_size) { + auto* container = + reinterpret_cast( + container_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE( + { *ret_size = container->constant_blob_size(); }) +} + + +// Load weights from a single blob in weight_blob_ptr +AOTIRuntimeError AOTInductorModelUpdateConstantsFromBlob( + AOTInductorModelContainerHandle container_handle, + const uint8_t* weight_blob_ptr){ + auto* container = + reinterpret_cast( + container_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE( + {container->update_constants_from_blob(weight_blob_ptr); }) + } + + +} // extern "C" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/block_analysis.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/block_analysis.py new file mode 100644 index 0000000000000000000000000000000000000000..a62c219b2c5a00015005a5d5824b7b333c87d396 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/block_analysis.py @@ -0,0 +1,218 @@ +import collections +import functools +import textwrap + +import sympy +from sympy import Expr, Symbol + +from torch.utils._ordered_set import OrderedSet +from torch.utils._sympy.functions import FloorDiv, ModularIndexing + +from ..utils import sympy_dot, sympy_subs +from ..virtualized import V + + +class BlockPatternMatcher: + """ + Matches block indexing expressions. + """ + + _indexing_wild_signed_int = functools.partial( + sympy.Wild, properties=[lambda x: x.is_integer] + ) + _indexing_wild_unsigned_int = functools.partial( + sympy.Wild, properties=[lambda x: x.is_integer and x.is_nonnegative] + ) + + @classmethod + def get_subexpr_involving_symbol(cls, expr: Expr, symbol: Symbol) -> Expr: + """ + Given a sympy expression, return the subexpression comprised only of terms + involving the specified symbol. + + For example, if `expr` is `x * 5 + x ** 2 + y * 2 + 5`, and `symbol` is `x`, + this returns `x * 5 + x ** 2`. + """ + expr = cls._preprocess(expr) + return sympy.S.Zero + sum( + term for term in sympy.Add.make_args(expr) if symbol in term.free_symbols + ) + + @staticmethod + def factor_index_expr(expr: sympy.Expr, index_var: Symbol) -> sympy.Expr: + """ + Given an index expression, factor the expression around + - FloorDiv(index_var, ...) + - ModularIndexing(index_var, ...) + - xindex + + e.g. FloorDiv(index_var, d0)*s0 + FloorDiv(index_var, d0)*s1 -> + FloorDiv(index_var, d0) * (s0 + s1) + """ + centres = OrderedSet() + for sub in sympy.preorder_traversal(expr): + if isinstance(sub, FloorDiv) and sub.args[0] == index_var: + centres.add(sub) + elif isinstance(sub, ModularIndexing) and sub.args[0] == index_var: + centres.add(sub) + centres.add(index_var) + + expr_out = expr + for c in centres: + expr_out = sympy.collect(expr_out, c) + return expr_out + + @staticmethod + def get_slice_numels(dims: list[Expr]) -> list[Expr]: + """ + Compute the cumulative size of each dimension's slice. + This proceeds from the last dim up to the second. + """ + numels = collections.deque([sympy.S.One]) + for dim in dims[:0:-1]: + numel = dim * numels[0] + numels.appendleft(numel) + return [*numels] + + @staticmethod + def _preprocess(expr: Expr) -> Expr: + # Remove any Identity nodes, e.g. expand x + (5 * y) to x + 5 * y. + # Disable mul and multinomial as those expansions affect op trees: + # e.g. sympy expects to match Mul(a, b), but expansion has simplified + # to Add(...). Even though they may be algebraically equivalent, + # sympy `match` performs structural pattern matching + return expr.expand(mul=False, multinomial=False, identity=True) + + @classmethod + def match_mod_div_block_expr( + cls, + index: Expr, + index_var: Symbol, + numel: Expr, + num_dims: int, + ) -> tuple[list[Expr], list[Expr], list[Expr]] | None: + """ + Matches modular indexing expressions, converting them to implied block dimensions and strides. + See triton.py for more information. + + Warning: this function requires that `index`, `numel` and any other sympy + expression does not have precomputed replacements since otherwise block + pattern matching may fail. + See [Note: Precomputed replacements with BlockPatternMatch] + """ + index = cls._preprocess(index) + + # Pattern match to find the strides and offset. + wild_unsigned_int = functools.partial( + cls._indexing_wild_unsigned_int, exclude=[index_var] + ) + wild_signed_int = functools.partial( + cls._indexing_wild_signed_int, exclude=[index_var] + ) + dims: list[Expr] = [ + wild_unsigned_int(f"dim_mod{idx}") for idx in range(num_dims) + ] + strides: list[Expr] = [ + wild_signed_int(f"stride_mod{idx}") for idx in range(num_dims) + ] + + # The first dimension's index is computed by division. + # The remaining are computed by modulo. + slice_numels = cls.get_slice_numels(dims[:num_dims]) + block_index_exprs = [FloorDiv(index_var, slice_numels[0])] + [ + ModularIndexing(index_var, numel, dim) + for dim, numel in zip(dims[1:], slice_numels[1:]) + ] + + # Calculate a linear index from block indices. + match_expr = sympy_dot(strides, block_index_exprs) + + # Heuristic: if the number of dimensions is high, check that the minimum requirements + # are met before attempting an expensive full match. see triton.py:match_mod_div_block + # for more details. In short, here we check that each subexpression in sympy.Add contains + # only FloorDiv or ModularIndexing expressions. + if num_dims >= 5: + stride = sympy.symbols("stride", cls=wild_signed_int) + denom, other = sympy.symbols("denominator other", cls=wild_unsigned_int) + mod_div_pattern = stride * ModularIndexing(index_var, denom, other) + floor_div_pattern = stride * FloorDiv(index_var, denom) + first_dim_floor_div_matched = False + match_failed = False + for arg in sympy.Add.make_args(index): + if arg.match(floor_div_pattern): + # There should only be a single FloorDiv(index, denom) expression + # corresponding to the first dimension + if first_dim_floor_div_matched: + match_failed = True + break + first_dim_floor_div_matched = True + elif arg.match(mod_div_pattern): + continue + else: + match_failed = True + break + + if match_failed: + return None + + # Pattern match. + match = index.match(match_expr) + if match is None: + return None + + # Provide default values for unmatched dims and strides. + for dim in dims[1:]: + if dim not in match: + match[dim] = sympy.S.One + for stride in strides[1:]: + if stride not in match: + match[stride] = sympy.S.Zero + + # Replace wildcards with matched expressions. + dims = [dims[0]] + [match[dim] for dim in dims[1:]] + strides = [match[stride] for stride in strides] + slice_numels = cls.get_slice_numels(dims) + block_index_exprs = [sympy_subs(expr, match) for expr in block_index_exprs] + + sizevars = V.graph.sizevars + + # The leading dimension is not directly matched in our expression. + # We solve for it by dividing the range tree numel by the product of + # all other dimensions. We quit if they are not known to be divisible. + assert dims[0] not in match, "Expected not to match the leading dimension!" + if not sizevars.statically_known_multiple_of(numel, slice_numels[0]): + return None + dims[0] = numel / slice_numels[0] + + # Sanity check that we can recover the index from the matched subexpressions. + matched_index = sympy_dot(strides, block_index_exprs) + assert sizevars.statically_known_equals( + matched_index, + index, + ), textwrap.dedent( + f""" + Invalid match! + Index: {index} + Matched expression: {matched_index} + """ + ) + + return dims, strides, block_index_exprs + + @classmethod + def match_affine_block_expr( + cls, + index: Expr, + index_var: Symbol, + ) -> Expr | None: + """ + Matches simple expressions of the form stride * index, returning the + stride. + """ + index = cls._preprocess(index) + stride = cls._indexing_wild_signed_int(name="stride", exclude=[index_var]) + m = index.match(index_var * stride) + if m is None: + return None + + return m[stride] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/common.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/common.py new file mode 100644 index 0000000000000000000000000000000000000000..16b7b03ecc6aae9d558fb7db1b989cba5bbae821 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/common.py @@ -0,0 +1,2998 @@ +from __future__ import annotations + +import atexit +import contextlib +import dataclasses +import enum +import functools +import itertools +import logging +import math +import operator +import os +import re +import tempfile +from abc import ABC, abstractmethod +from enum import auto, Enum +from itertools import chain +from typing import Any, cast, ClassVar, Generic, NamedTuple, TYPE_CHECKING +from typing_extensions import Self, TypeVar + +import sympy + +import torch +import torch.fx +from torch._prims_common import ELEMENTWISE_TYPE_PROMOTION_KIND +from torch.utils import _pytree as pytree +from torch.utils._config_module import ConfigModule +from torch.utils._ordered_set import OrderedSet +from torch.utils._sympy.numbers import int_oo +from torch.utils._sympy.printers import PythonPrinter as _PythonPrinter +from torch.utils._sympy.symbol import free_symbol_is_type, symbol_is_type, SymT +from torch.utils._sympy.value_ranges import bound_sympy, ValueRanges + +from .. import config, metrics +from ..dtype_propagation import DtypePropagationOpsHandler +from ..ops_handler import BasicMathOpsMixin, DefaultHandler +from ..shape_propagation import ShapePropagationOpsHandler +from ..utils import ( + boolean_ops, + DeferredLineBase, + generate_assert, + get_current_backend, + IndentedBuffer, + ir_dataclass, + ScopedDict, + sympy_dot, + sympy_index_symbol, + sympy_subs, + triton_type, + unique, +) +from ..virtualized import ( + NullHandler, + ops, + OpsHandler, + OpsValue, + ReductionType, + StoreMode, + V, +) + + +if TYPE_CHECKING: + from collections.abc import Callable, Iterator, MutableMapping, Sequence + + from torch.fx import GraphModule + + from ..custom_graph_pass import CustomGraphModulePass + from ..ir import Buffer, ChoiceCaller, FixedLayout, IRNode + from ..loop_body import LoopBody + from ..scheduler import BaseScheduling, Scheduler, SchedulerNode + from ..shape_propagation import BlockShapeType + from .wrapper import PythonWrapperCodegen + + _T = TypeVar("_T") + SchedulingConstructor = Callable[[Scheduler | None], BaseScheduling] + WrapperConstructor = type[PythonWrapperCodegen] + SymbolLike = str | sympy.Symbol + + # OpVarT should really be Union[CSEVariable, str], however this + # causes typing errors in subclasses (defined in other files). + OpVarT = str + +schedule_log = torch._logging.getArtifactLogger(__name__, "schedule") +log = logging.getLogger(__name__) + + +def data_type_logger(msg: str) -> None: + if schedule_log.isEnabledFor(logging.DEBUG): + schedule_log.debug("Data type propagation: %s", msg) + + +@dataclasses.dataclass +class FileBackedGraphModule: + """ + Output of FX wrapper codegen. Exposes the same methods as ModuleType, but these + map back to a GraphModule instead of Python source. + """ + + gm: GraphModule + compiled_fn: Callable[..., Any] + + def __post_init__(self) -> None: + # Write the code to a file for compatibility with debugging utilities. + # The file is deleted upon program termination. + self.tempfile = tempfile.NamedTemporaryFile( # noqa: SIM115 + mode="w+", suffix=".py", delete=False + ) + atexit.register(os.remove, self.tempfile.name) + with self.tempfile as f: + f.write(self.value) + + @property + def __file__(self) -> str: + return self.tempfile.name + + def call(self, args: list[Any]) -> Any: + return self.compiled_fn(*args) + + @property + def value(self) -> str: + return self.gm.code + + +class WorkspaceZeroMode(enum.Enum): + UNINITIALIZED = 0 + ZERO_ON_CALL = 1 # kernel may leave workspace dirty + ZERO_PER_GRAPH = 2 # must be re-zeroed by kernel + + @staticmethod + def combine(a: WorkspaceZeroMode, b: WorkspaceZeroMode) -> WorkspaceZeroMode: + if a == b or b == WorkspaceZeroMode.UNINITIALIZED: + return a + if a == WorkspaceZeroMode.UNINITIALIZED: + return b + raise NotImplementedError(f"WorkspaceZeroMode.combine({a!r}, {b!r})") + + @staticmethod + def from_bool(zero_fill: bool) -> WorkspaceZeroMode: + if zero_fill: + return WorkspaceZeroMode.ZERO_ON_CALL + return WorkspaceZeroMode.UNINITIALIZED + + +class CodegenSymbol(ABC): + """ + An IR object possibly corresponding to a variable in the wrapper code. + """ + + @abstractmethod + def get_name(self) -> str: + pass + + @abstractmethod + def get_example(self) -> torch.Tensor | sympy.Symbol: + pass + + +@ir_dataclass(frozen=True) +class WorkspaceArg(CodegenSymbol): + """A temporary buffer used for a single kernel, then discarded. + + Not registered as a traditional buffer since there are no users, + so it would be dead code eliminated. + + Args: + nbytes: The size of the buffer in bytes. + zero_fill: Whether the buffer should be initialized to zero. + + """ + + count: sympy.Expr + zero_mode: WorkspaceZeroMode + device: torch.device + outer_name: str + inner_name: str = "ws_ptr" + dtype: torch.dtype = torch.uint8 + + @staticmethod + def unique_name(prefix: str = "workspace_") -> str: + return f"{prefix}{next(V.graph.workspace_id)}" + + @staticmethod + def can_join(a: WorkspaceArg, b: WorkspaceArg) -> bool: + return ( + a.inner_name == b.inner_name and a.dtype == b.dtype and a.device == b.device + ) + + @staticmethod + def join(a: WorkspaceArg, b: WorkspaceArg) -> WorkspaceArg: + return WorkspaceArg( + count=a.count + b.count, + zero_mode=WorkspaceZeroMode.combine(a.zero_mode, b.zero_mode), + dtype=a.dtype, + device=a.device, + inner_name=a.inner_name, + outer_name=a.outer_name, + ) + + @staticmethod + def maximum(a: WorkspaceArg, b: WorkspaceArg) -> WorkspaceArg: + assert ( + a.dtype == b.dtype and a.device == b.device and a.inner_name == b.inner_name + ) + return WorkspaceArg( + count=sympy.Max(a.count, b.count), + zero_mode=WorkspaceZeroMode.combine(a.zero_mode, b.zero_mode), + dtype=a.dtype, + device=a.device, + inner_name=a.inner_name, + outer_name=a.outer_name, + ) + + # These methods let WorkspaceArg pretend it is a buffer to reuse allocation code + def get_device(self) -> torch.device: + return self.device + + get_device_or_error = get_device + + def get_dtype(self) -> torch.dtype: + return self.dtype + + def get_example(self) -> torch.Tensor | sympy.Symbol: + return self.get_layout().get_example() + + def get_layout(self) -> FixedLayout: + from ..ir import FixedLayout + + return FixedLayout( + device=self.device, + dtype=self.dtype, + size=[self.count], + stride=[1], + ) + + @property + def layout(self) -> FixedLayout: + return self.get_layout() + + get_output_spec = get_layout + maybe_get_output_spec = get_layout + maybe_get_layout = get_layout + + def get_offset(self) -> sympy.Expr: + return sympy.S.Zero + + def get_size(self) -> list[sympy.Expr]: + return [self.count] + + def get_stride(self) -> list[sympy.Expr]: + return [sympy.S.One] + + def get_name(self) -> str: + return self.outer_name + + def get_is_pinned(self) -> bool: + return False + + def get_inputs_that_alias_output(self) -> list[str]: + return [] + + +class TritonScratchWorkspace: + def __init__(self, size: int, generate_dtype_str: Callable[..., str]): + self.size = size + self._generate_dtype_str = generate_dtype_str + + def generate_dtype_str(self) -> str: + return self._generate_dtype_str() + + +@dataclasses.dataclass +class TensorArg: + name: str + buffer: str + dtype: torch.dtype + offset: sympy.Expr = sympy.S.Zero # c++ only + alias_of: str | None = None # halide only + + +@dataclasses.dataclass +class SizeArg: + name: str + expr: sympy.Expr + + @property + def alias_of(self) -> str | None: + return None + + +@dataclasses.dataclass +class ConstexprArg: + name: str + + +@dataclasses.dataclass +class TMADescriptorArg: + name: str + api_type: str # "experimental" or "stable" + block_shape: list[sympy.Expr] | None # only needed for "stable" + dtype: torch.dtype | None # only needed for "stable" + + +@dataclasses.dataclass +class DeviceCodegen: + scheduling: SchedulingConstructor + wrapper_codegen: WrapperConstructor + cpp_wrapper_codegen: WrapperConstructor | None = None + fx_wrapper_codegen: WrapperConstructor | None = None + + +KernelArgType = WorkspaceArg | TensorArg | SizeArg | TMADescriptorArg | ConstexprArg + +device_codegens: dict[str, DeviceCodegen] = {} + + +class DeviceOpOverrides: + def import_get_raw_stream_as(self, name: str) -> str: + raise NotImplementedError + + def set_device(self, device_idx: int) -> str: + raise NotImplementedError + + def synchronize(self) -> str: + raise NotImplementedError + + def device_guard(self, device_idx: int) -> str: + raise NotImplementedError + + def cpp_device_guard(self) -> str: + raise NotImplementedError + + def cpp_aoti_device_guard(self) -> str: + raise NotImplementedError + + def cpp_stream_guard(self) -> str: + raise NotImplementedError + + def cpp_aoti_stream_guard(self) -> str: + raise NotImplementedError + + def cpp_getStreamFromExternal(self) -> str: + raise NotImplementedError + + def kernel_header(self) -> str: + raise NotImplementedError + + def kernel_driver(self) -> str: + raise NotImplementedError + + def cpp_stream_type(self) -> str: + raise NotImplementedError + + def aoti_get_stream(self) -> str: + raise NotImplementedError + + def cpp_kernel_type(self) -> str: + raise NotImplementedError + + def cpp_device_ptr(self) -> str: + raise NotImplementedError + + def tma_descriptor_helpers(self) -> str: + raise NotImplementedError + + def cpp_scratch( + self, idx: int, workspace: TritonScratchWorkspace, prefix: str | None = None + ) -> tuple[list[str], str] | None: + # optionally return (scratch definition, arg name) + raise NotImplementedError + + +device_op_overrides_dict: dict[str, DeviceOpOverrides] = {} +_device_op_overrides_initialized = False +custom_backend_passes: dict[str, CustomGraphModulePass | None] = {} +custom_backend_codegen_configs: dict[str, ConfigModule | None] = {} + + +# The code generated by Inductor consists of two main parts: kernel code and wrapper code. +# For any new backend looking to integrate with Inductor, customization of these two main +# parts are necessary to generate its specific code. +# +# Kernel code generation is determined by different Scheduling. Consequently, a new +# backend needs to provide a custom Scheduling for its unique kernel code generation. Currently, +# CppScheduling and TritonScheduling serve the C++/OpenMP and Triton backends, respectively. +# +# For the Wrapper, Inductor provides a PythonWrapperCodegen class to generate the Python wrapper code +# that bridges kernels. This allows out-of-tree backends to inherit from PythonWrapperCodegen, +# and override specific member functions to create backend-specific Python wrapper code. +# +# Other classes, such as CppKernel and TritonKernel, used for code generation, typically form part +# of the logic for either Scheduling or PythonWrapperCodegen. So the Scheduling and PythonWrapperCodegen interfaces +# provide flexibility to the backend. A backend can choose to implement these classes from scratch, +# or reuse them by extending and overriding as necessary. And Inductor provides the registration API, +# register_backend_for_device, to equip a new backend at runtime. +# +# Intel has developed a new backend on top of Triton to support Intel GPUs, leveraging these interfaces. +# This backend can be used as a reference: +# https://github.com/intel/intel-extension-for-pytorch/blob/5dcc9d57e5422cf295e1a1ee97896d6b6a554a85/intel_extension_for_pytorch/_inductor/__init__.py#L9 +def register_backend_for_device( + device: str, + device_scheduling: SchedulingConstructor, + device_wrapper_codegen: WrapperConstructor, + device_cpp_wrapper_codegen: WrapperConstructor | None = None, + device_fx_wrapper_codegen: WrapperConstructor | None = None, + device_custom_pass: CustomGraphModulePass | None = None, + device_custom_config: ConfigModule | None = None, +) -> None: + device_codegens[device] = DeviceCodegen( + device_scheduling, + device_wrapper_codegen, + device_cpp_wrapper_codegen, + device_fx_wrapper_codegen, + ) + custom_backend_passes[device] = device_custom_pass + if device_custom_config: + assert ( + isinstance(device_custom_config, ConfigModule) + and device_custom_config is not config + ), ( + f"{device_custom_config=} cannot be the same as the default inductor config {config=}" + ) + custom_backend_codegen_configs[device] = device_custom_config + + +class BackendFeature(Enum): + FOREACH = auto() + BUCKETIZE = auto() + INPLACE_BUFFERS = auto() + MASKED_SCATTER_WITH_INDEX = auto() + SCAN = auto() + SORT = auto() + TUPLE_REDUCTION = auto() + PREFER_STORE_LOOP_ORDER = auto() + TRITON_TEMPLATES = auto() + REDUCE_TO_SINGLE_ELEMENT = auto() + + +def get_backend_features( + device: torch.device | str | None, +) -> OrderedSet[BackendFeature]: + if device is None: + return OrderedSet() + init_backend_registration() + if isinstance(device, torch.device): + device_type = device.type + else: + assert isinstance(device, str), type(device) + device_type = device + device = torch.device(device_type) + scheduling_ctor = get_scheduling_for_device(device_type) + assert scheduling_ctor + scheduling = scheduling_ctor(None) + return scheduling.get_backend_features(device) + + +def has_backend_feature( + device: torch.device | str | None, feature: BackendFeature +) -> bool: + """See also V.graph.has_feature""" + assert isinstance(feature, BackendFeature) + return feature in get_backend_features(device) + + +def get_scheduling_for_device(device: str) -> SchedulingConstructor | None: + return device_codegens[device].scheduling if device in device_codegens else None + + +def get_wrapper_codegen_for_device( + device: str, cpp_wrapper: bool = False, fx_wrapper: bool = False +) -> WrapperConstructor | None: + if device in device_codegens: + wrapper_codegen_obj: DeviceCodegen = device_codegens[device] + if fx_wrapper: + return wrapper_codegen_obj.fx_wrapper_codegen + elif cpp_wrapper: + return wrapper_codegen_obj.cpp_wrapper_codegen + else: + return wrapper_codegen_obj.wrapper_codegen + return None + + +def get_custom_backend_pass_for_device(device: str) -> CustomGraphModulePass | None: + return custom_backend_passes.get(device) + + +def get_custom_backend_config_for_device(device: str) -> ConfigModule | None: + return custom_backend_codegen_configs.get(device) + + +@functools.cache +def init_backend_registration() -> None: + """ + Register the backend for different devices, including the scheduling + for kernel code generation and the host side wrapper code generation. + """ + from .cpp import CppScheduling + from .cpp_wrapper_cpu import CppWrapperCpu + from .cpp_wrapper_cpu_array_ref import CppWrapperCpuArrayRef + from .cpp_wrapper_gpu import CppWrapperGpu + from .cpp_wrapper_mps import CppWrapperMps + from .cuda_combined_scheduling import CUDACombinedScheduling + from .halide import HalideScheduling + from .mps import MetalScheduling + from .pallas import PallasScheduling + from .python_wrapper_mtia import PythonWrapperMtia + from .triton import TritonScheduling + from .wrapper import PythonWrapperCodegen + from .wrapper_fxir import WrapperFxCodegen + from .xpu.xpu_combined_scheduling import XPUCombinedScheduling + + if get_scheduling_for_device("cpu") is None: + cpu_backends = { + "cpp": CppScheduling, + "halide": HalideScheduling, + "triton": TritonScheduling, + "pallas": PallasScheduling, + } + register_backend_for_device( + "cpu", + lambda scheduling: cpu_backends[config.cpu_backend](scheduling), + PythonWrapperCodegen, + CppWrapperCpuArrayRef + if config.aot_inductor.allow_stack_allocation + else CppWrapperCpu, + WrapperFxCodegen, + ) + + if get_scheduling_for_device("cuda") is None: + # CUDACombinedScheduling combines Triton and CUDA C++ scheduling for CUDA devices via delegation + cuda_backends = { + "triton": CUDACombinedScheduling, + "halide": HalideScheduling, + "pallas": PallasScheduling, + } + register_backend_for_device( + "cuda", + lambda scheduling: cuda_backends[config.cuda_backend](scheduling), + PythonWrapperCodegen, + CppWrapperGpu, + WrapperFxCodegen, + ) + + if get_scheduling_for_device("tpu") is None: + register_backend_for_device( + "tpu", + PallasScheduling, + PythonWrapperCodegen, + # CppWrapperGpu, + # WrapperFxCodegen, + ) + + if get_scheduling_for_device("xpu") is None: + register_backend_for_device( + "xpu", + XPUCombinedScheduling, + PythonWrapperCodegen, + CppWrapperGpu, + WrapperFxCodegen, + ) + + if get_scheduling_for_device("tpu") is None: + tpu_backends = { + "pallas": PallasScheduling, + } + register_backend_for_device( + "tpu", + lambda scheduling: tpu_backends[config.tpu_backend](scheduling), + PythonWrapperCodegen, + ) + + if get_scheduling_for_device("mps") is None: + register_backend_for_device( + "mps", + MetalScheduling, + PythonWrapperCodegen, + CppWrapperMps, + WrapperFxCodegen, + ) + + if get_scheduling_for_device("mtia") is None: + register_backend_for_device( + "mtia", + TritonScheduling, + PythonWrapperMtia, + CppWrapperGpu, + WrapperFxCodegen, + ) + + private_backend = torch._C._get_privateuse1_backend_name() + if ( + private_backend != "privateuseone" + and get_scheduling_for_device(private_backend) is None + ): + from torch.utils.backend_registration import _get_custom_mod_func + + try: + device_scheduling = _get_custom_mod_func("Scheduling") + wrapper_codegen = _get_custom_mod_func("PythonWrapperCodegen") + cpp_wrapper_codegen = _get_custom_mod_func("CppWrapperCodegen") + fx_wrapper_codegen = _get_custom_mod_func("WrapperFxCodegen") + if device_scheduling and wrapper_codegen and cpp_wrapper_codegen: + register_backend_for_device( + private_backend, + device_scheduling, + wrapper_codegen, + cpp_wrapper_codegen, + fx_wrapper_codegen, + ) + except RuntimeError: + pass + + +def index_prevent_reordering( + index: Sequence[sympy.Expr], + index_vars: Sequence[sympy.Expr], + sizes: Sequence[sympy.Expr], +) -> list[sympy.Expr]: + from ..ir import FlexibleLayout + + # added contiguous index prevents reordering + return [*index, sympy_dot(index_vars, FlexibleLayout.contiguous_strides(sizes))] + + +def register_device_op_overrides( + device: str, device_op_overrides: DeviceOpOverrides +) -> None: + device_op_overrides_dict[device] = device_op_overrides + + +def _initialize_device_op_overrides(): + # Use a flag rather than checking device_op_overrides_dict, since external/test + # code may partially populate it before we are called. + global _device_op_overrides_initialized + if _device_op_overrides_initialized: + return + + from . import mps_device_op_overrides # noqa: F401 + from .cpu_device_op_overrides import CpuDeviceOpOverrides + from .cuda import device_op_overrides # noqa: F401 + from .mtia import device_op_overrides as mtia_op_overrides # noqa: F401 + from .xpu import device_op_overrides as xpu_op_overrides # noqa: F401 + + # TPU uses Pallas for codegen and only needs no-op overrides + register_device_op_overrides("tpu", CpuDeviceOpOverrides()) + + _device_op_overrides_initialized = True + + +def get_device_op_overrides(device: str) -> DeviceOpOverrides: + assert isinstance(device, str), type(device) + _initialize_device_op_overrides() + return device_op_overrides_dict[device] + + +DTYPE_TO_COMPUTATION_DTYPE: dict[torch.dtype, torch.dtype] = { + torch.bfloat16: torch.float, + torch.float16: torch.float, + **{ + dtype: dtype + for dtype in [ + torch.bool, + torch.float32, + torch.float64, + torch.int8, + torch.int16, + torch.int32, + torch.int64, + torch.uint8, + torch.uint16, + torch.uint32, + torch.uint64, + ] + }, +} + + +def deduce_output_dtype_by_name( + op_name: str, + *args: Any, + **kwargs: Any, +) -> torch.dtype | None: + """ + Given op name and a list of input dtypes, deduce the output dtype + """ + if op_name in boolean_ops(): + return torch.bool + elif op_name in ( + "to_dtype", + "index_expr", + ): + return kwargs["dtype"] if "dtype" in kwargs else args[-1] + elif op_name in ( + "rand", + "randn", + ): + return torch.float + elif op_name in ( + "get_index", + "randint64", + "load_seed", + ): + return torch.int64 + elif op_name == "reduction": + return kwargs["dtype"] if "dtype" in kwargs else args[1] + elif op_name == "constant": + return kwargs["dtype"] if "dtype" in kwargs else args[-1] + elif op_name in ( + "load", + "store", + "store_reduction", + ): + buf_name = args[1] + return V.graph.get_dtype(buf_name) # type: ignore[arg-type] + elif op_name == "to_dtype_bitcast": + return kwargs["dtype"] if "dtype" in kwargs else args[-2] + return None + + +def check_dtype( + buffer: IndentedBuffer, var: CSEVariableType, dtype: torch.dtype +) -> None: + backend = get_current_backend() + if config.test_configs.runtime_triton_dtype_assert and backend == "triton": + buffer.writeline(f"tl.static_assert({var}.dtype == {triton_type(dtype)})") + elif config.test_configs.static_cpp_dtype_assert and backend == "cpp": + from .cpp_utils import CppCSEVariable, DTYPE_TO_CPP + + assert isinstance(var, CppCSEVariable), type(var) + if dtype == torch.bool: + if var.is_vec: + is_same_dt = f"IsVecMaskType::value" + else: + # operator&(bool, bool) returns int and it can be used as boolean in C++ + is_same_dt = f"std::is_same_v || std::is_same_v" + else: + c_var_type = f"decltype({var})" + if var.is_vec: + c_var_type = f"typename {c_var_type}::value_type" + is_same_dt = f"std::is_same_v<{c_var_type}, {DTYPE_TO_CPP[dtype]}>" + + buffer.writeline(f"static_assert({is_same_dt});") + + +def check_shape( + buffer: IndentedBuffer, var: CSEVariableType, shape: BlockShapeType +) -> None: + backend = get_current_backend() + assert shape is not None + if config.test_configs.runtime_triton_shape_assert and backend == "triton": + shape_str = ( + ", ".join(str(d) for d in shape) if len(shape) != 1 else f"{shape[0]}," + ) + buffer.writeline(f"tl.static_assert({var}.shape == ({shape_str}))") + + +def check_nan(buffer: IndentedBuffer, var: CSEVariableType) -> None: + backend = get_current_backend() + if backend == "triton": + msg = "NaN or Inf found" + buffer.writeline( + f"tl.device_assert(({var} == {var}) & ({var} != float('inf')) & ({var} != float('-inf')), '{msg}')" + ) + + +class DataTypePropagation: + def __init__(self, body: LoopBody) -> None: + self.body = body + self.graphs: dict[Callable[..., Any] | str, Any] = { + "root": body.root_block.graph + } + for k, v in body.subblocks.items(): + self.graphs[k] = v.graph + + def deduce_node_dtype_by_inputs(self, node: torch.fx.Node) -> torch.dtype | None: + inputs = node.all_input_nodes + input_nodes = [ + n for n in inputs if isinstance(n, torch.fx.Node) and n.op != "placeholder" + ] + if len(input_nodes) == 0: + return None + + all_input_nodes_propagated = all( + OptimizationContext.key in n.meta + and n.meta[OptimizationContext.key].dtype is not None + for n in input_nodes + ) + if not all_input_nodes_propagated: + return None + + return functools.reduce( + torch.promote_types, + [n.meta[OptimizationContext.key].dtype for n in input_nodes], + ) + + def deduce_node_dtype_by_subgraph(self, node: torch.fx.Node) -> torch.dtype: + sub_graph = self.graphs[node.target] + dtype = self.propagate_graph(sub_graph) + assert dtype + return dtype + + def deduce_node_dtype(self, node: torch.fx.Node) -> torch.dtype | None: + if node.op == "placeholder": + return None + + if node.target == "output" and len(node.args) != 1: + # we can infer output node if it only have 1 arg + return None + + if node.target is operator.getitem: + node_arg = node.args[0] + assert isinstance(node_arg, torch.fx.Node), type(node_arg) + return self.deduce_node_dtype(node_arg) + + assert isinstance(node.target, str), type(node.target) + + if node.target.startswith("masked_subblock"): + return self.deduce_node_dtype_by_subgraph(node) + + if ( + output_dtype := deduce_output_dtype_by_name( + node.target, + *node.args, + **node.kwargs, + ) + ) is not None: + return output_dtype + + return self.deduce_node_dtype_by_inputs(node) + + def propagate_graph(self, graph: torch.fx.Graph) -> torch.dtype | None: + assert graph.nodes + graph_dtype: torch.dtype | None = None + # For masked_subblock, we use output's dtype to represent + # the dtype of this subgraph. For other cases, graph_dtype + # might be None + for node in graph.nodes: + if OptimizationContext.key in node.meta: + opt_ctx = node.meta[OptimizationContext.key] + else: + opt_ctx = OptimizationContext() + + opt_ctx.dtype = self.deduce_node_dtype(node) + node.meta[OptimizationContext.key] = opt_ctx + if node.target == "output": + graph_dtype = opt_ctx.dtype + return graph_dtype + + def propagate(self) -> torch.dtype | None: + return self.propagate_graph(self.graphs["root"]) + + @classmethod + def propagate_loopbody(cls, body: LoopBody) -> torch.dtype | None: + return cls(body).propagate() + + @classmethod + def propagate_scheduler_node(cls, node: SchedulerNode) -> torch.dtype | None: + from ..loop_body import LoopBody + from ..scheduler import SchedulerNode + + assert isinstance(node, SchedulerNode), type(node) + assert isinstance(node._body, LoopBody), type(node._body) + return DataTypePropagation.propagate_loopbody(node._body) + + +class PythonPrinter(_PythonPrinter): + def doprint( + self, expr: sympy.Expr, *, simplify: bool = True, p: bool = True + ) -> str: + # TODO: why are people passing strings to the printer here :think: + if simplify and isinstance(expr, sympy.Expr) and hasattr(V.graph, "sizevars"): + expr = V.graph.sizevars.simplify(expr) + return super().doprint(expr) + + def parenthesize(self, item: sympy.Expr, level: int, strict: bool = False) -> str: + if isinstance(item, sympy.Mod): + # use parenthesis to enforce precedence. + # in sympy 1.13.3, -2*Mod(x,y) becomes -2*x%y, which is wrong. + # pyrefly: ignore [missing-attribute] + return f"({self._print(item)})" + else: + return super().parenthesize(item, level, strict) + + +class OpDecompositions: + """ + Decomposes inductor ops + """ + + @staticmethod + def identity(value: OpVarT) -> OpVarT: + # used to trigger cse + return value + + @staticmethod + def reciprocal(x: OpVarT) -> OpVarT: + # Use float32 constant so that div_rn can be applied when + # eager_numerics.division_rounding is enabled + return ops.truediv(ops.constant(1.0, torch.float32), x) + + @staticmethod + def square(x: OpVarT) -> OpVarT: + return ops.mul(x, x) + + @staticmethod + def erfc(x: OpVarT) -> OpVarT: + return ops.sub(ops.constant(1, torch.float32), ops.erf(x)) + + @staticmethod + def erfcx(x: OpVarT) -> OpVarT: + return ops.mul(ops.exp(ops.square(x)), ops.erfc(x)) + + @staticmethod + def expm1(x: OpVarT) -> OpVarT: + return ops.sub(ops.exp(x), ops.constant(1, torch.float32)) + + @staticmethod + def log10(x: OpVarT) -> OpVarT: + return ops.mul(ops.log(x), ops.constant(1 / math.log(10), torch.float32)) + + @staticmethod + def log2(x: OpVarT) -> OpVarT: + return ops.mul(ops.log(x), ops.constant(1 / math.log(2), torch.float32)) + + @staticmethod + def exp2(x: OpVarT) -> OpVarT: + return ops.exp(ops.mul(x, ops.constant(math.log(2), torch.float32))) + + @staticmethod + def log1p(x: OpVarT) -> OpVarT: + return ops.log(ops.add(x, ops.constant(1, torch.int32))) + + @staticmethod + def sigmoid(x: OpVarT) -> OpVarT: + one = ops.constant(1, torch.int32) + return ops.truediv(one, ops.add(one, ops.exp(ops.neg(x)))) + + @staticmethod + def relu(x: OpVarT) -> OpVarT: + return ops.maximum(x, ops.constant(0, torch.int32)) + + @staticmethod + def fma(x: OpVarT, y: OpVarT, z: OpVarT) -> OpVarT: + # for backends that don't override this (halide) + return ops.add(ops.mul(x, y), z) + + @staticmethod + def mul_rn(x: OpVarT, y: OpVarT) -> OpVarT: + # for backends that don't override this, just use regular mul + return ops.mul(x, y) + + @staticmethod + def div_rn(x: OpVarT, y: OpVarT) -> OpVarT: + # for backends that don't override this, just use regular div + return ops.truediv(x, y) + + @staticmethod + def floor_to_int(a: OpVarT, dtype: torch.dtype) -> OpVarT: + return ops.to_dtype(ops.floor(a), dtype) + + @staticmethod + def ceil_to_int(a: OpVarT, dtype: torch.dtype) -> OpVarT: + return ops.to_dtype(ops.ceil(a), dtype) + + @staticmethod + def trunc_to_int(a: OpVarT, dtype: torch.dtype) -> OpVarT: + return ops.to_dtype(ops.trunc(a), dtype) + + @staticmethod + def remainder(a: OpVarT, b: OpVarT) -> OpVarT: + r = ops.mod(a, b) + cond = ops.and_( + ops.ne(r, ops.constant(0, torch.int32)), + ops.ne(ops.signbit(r), ops.signbit(b)), + ) + return ops.where(cond, ops.add(r, b), r) + + @staticmethod + def round_to_int(a: OpVarT, dtype: torch.dtype) -> OpVarT: + return ops.to_dtype(ops.round(a), dtype) + + +_RE_PAREN_NOT_NEEDED = re.compile(r"[a-z0-9_.]+|\([^)]*\)|", flags=re.IGNORECASE) + + +def _all_in_parens(string: str) -> bool: + if string[0] != "(" or len(string) < 2: + return False + count = 1 + for i, char in enumerate(string[1:]): + if char == "(": + count += 1 + elif char == ")": + count -= 1 + if count == 0 and i != len(string) - 2: + return False + assert count == 0 + return True + + +# pyrefly: ignore [inconsistent-inheritance] +class OpOverrides(BasicMathOpsMixin, OpDecompositions, OpsHandler[Any]): + @staticmethod + def paren(string: OpVarT) -> OpVarT: + if ( + isinstance(string, CSEVariable) + or _RE_PAREN_NOT_NEEDED.fullmatch(string) + or _all_in_parens(string) + ): + # don't put extra parens for strings that are already wrapped in parens + + return string + return f"({string})" + + @staticmethod + def constant(value: bool | float | int, dtype: torch.dtype) -> OpVarT: + return repr(value) + + @staticmethod + # pyrefly: ignore [bad-override] + def bitwise_not(x: OpVarT) -> OpVarT: + return f"~{OpOverrides.paren(x)}" + + @staticmethod + # pyrefly: ignore [bad-override] + def logical_not(a: OpVarT) -> OpVarT: + return f"{OpOverrides.paren(a)} == 0" + + @staticmethod + # pyrefly: ignore [bad-override] + def bitwise_and(x: OpVarT, y: OpVarT) -> OpVarT: + return f"{OpOverrides.paren(x)} & {OpOverrides.paren(y)}" + + @staticmethod + # pyrefly: ignore [bad-override] + def bitwise_or(x: OpVarT, y: OpVarT) -> OpVarT: + return f"{OpOverrides.paren(x)} | {OpOverrides.paren(y)}" + + @staticmethod + # pyrefly: ignore [bad-override] + def bitwise_xor(x: OpVarT, y: OpVarT) -> OpVarT: + return f"{OpOverrides.paren(x)} ^ {OpOverrides.paren(y)}" + + @staticmethod + # pyrefly: ignore [bad-override] + def bitwise_left_shift(x: OpVarT, y: OpVarT) -> OpVarT: + return f"{OpOverrides.paren(x)} << {OpOverrides.paren(y)}" + + @staticmethod + # pyrefly: ignore [bad-override] + def bitwise_right_shift(x: OpVarT, y: OpVarT) -> OpVarT: + return f"{OpOverrides.paren(x)} >> {OpOverrides.paren(y)}" + + @staticmethod + # pyrefly: ignore [bad-override] + def int_truediv(a: OpVarT, b: OpVarT) -> OpVarT: + # TODO: this is wrong + # TODO: an easy bandaid is to generate runtime asserts that it's + # <= 2**53, which is when this equation is correct + return ops.truediv(a, b) + + @staticmethod + def load_seed(name: str, offset: OpVarT) -> OpVarT: + return ops.load(name, sympy.Integer(offset)) + + def indirect_indexing( + self, + var: OpVarT, + size: sympy.Expr | int, + check: bool = True, + wrap_neg: bool = True, + ) -> sympy.Symbol: + return sympy_index_symbol(str(var)) + + def check_bounds( + self, expr: sympy.Expr, size: sympy.Expr, lower: bool, upper: bool + ) -> None: + raise NotImplementedError( + f"{type(self).__name__}: check_bounds should be handled by CSEProxy" + ) + + def load(self, name: str, index: sympy.Expr) -> OpVarT: + raise NotImplementedError( + f"{type(self).__name__}: load should be handled by CSEProxy" + ) + + def store( + self, name: str, index: sympy.Expr, value: OpVarT, mode: StoreMode = None + ) -> None: + raise NotImplementedError( + f"{type(self).__name__}: store should be handled by CSEProxy" + ) + + def device_assert_async(self, cond: CSEVariable, msg: str) -> None: + raise NotImplementedError( + f"{type(self).__name__}: device_assert_async should be handled by CSEProxy" + ) + + def store_reduction(self, name: str, index: sympy.Expr, value: OpVarT) -> None: + raise NotImplementedError( + f"{type(self).__name__}: store_reduction should be handled by CSEProxy" + ) + + def reduction( + self, + dtype: torch.dtype, + src_dtype: torch.dtype, + reduction_type: ReductionType, + value: OpVarT | tuple[OpVarT, ...], + ) -> OpVarT | tuple[OpVarT, ...]: + raise NotImplementedError( + f"{type(self).__name__}: reduction should be handled by CSEProxy" + ) + + def scan( + self, + dtypes: tuple[torch.dtype, ...], + combine_fn: Callable[ + [tuple[OpVarT, ...], tuple[OpVarT, ...]], + tuple[OpVarT, ...], + ], + values: tuple[OpVarT, ...], + ) -> tuple[OpVarT, ...]: + raise NotImplementedError( + f"{type(self).__name__}: scan should be handled by CSEProxy" + ) + + def sort( + self, + dtypes: tuple[torch.dtype, ...], + values: tuple[OpVarT, ...], + stable: bool, + descending: bool, + ) -> tuple[OpVarT, ...]: + raise NotImplementedError( + f"{type(self).__name__}: sort should be handled by CSEProxy" + ) + + def bucketize( + self, + values: OpVarT, + boundaries: tuple[str, sympy.Expr, sympy.Expr, sympy.Expr], + boundary_indices: OpVarT, + indexing_dtype: torch.dtype, + right: bool, + sorter: tuple[str, sympy.Expr] | None = None, + sorter_indices: OpVarT | None = None, + ) -> OpVarT: + raise NotImplementedError( + f"{type(self).__name__}: bucketize should be handled by CSEProxy" + ) + + def halide_clamp(self, value: OpVarT, size: sympy.Expr, check: bool) -> OpVarT: + raise NotImplementedError( + f"{type(self).__name__}: halide_clamp only implemented for Halide backend" + ) + + def dot(self, x: OpVarT, y: OpVarT) -> OpVarT: + raise NotImplementedError( + f"{type(self).__name__}: dot only implemented for Triton backend" + ) + + def inline_asm_elementwise( + self, + *inputs: OpVarT, + asm: str, + constraints: str | None = None, + dtype: torch.dtype = torch.float32, + is_pure: bool = True, + pack: int = 1, + input_dtypes: tuple[torch.dtype, ...] | None = None, + ) -> OpVarT: + raise NotImplementedError( + f"{type(self).__name__}: inline_asm_elementwise only implemented for Triton backend" + ) + + def output(self, *args: OpVarT) -> None: + raise AssertionError( + f"{type(self).__name__}: ops.output should not appear at codegen time" + ) + + def placeholder(self, index: int) -> OpVarT: + raise AssertionError( + f"{type(self).__name__}: ops.placeholder should not appear at codegen time" + ) + + @staticmethod + def _unimplemented(name: str) -> Callable[..., OpVarT]: + def unimplemented(self: OpOverrides, *args: Any, **kwargs: Any) -> OpVarT: + raise NotImplementedError( + f"{type(self).__name__} does not implement ops.{name}" + ) + + unimplemented.__name__ = name + unimplemented.is_unimplemented = True # type: ignore[attr-defined] + return unimplemented + + @classmethod + def _is_unimplemented(cls, name: str) -> bool: + fn = getattr(cls, name, None) + default_fn = getattr(OpsHandler, name, None) + return not fn or fn == default_fn or getattr(fn, "is_unimplemented", False) + + @classmethod + def _initialize_pointwise_overrides(cls, target: str) -> None: + assert target in ("triton", "cpp", "cppvec", "halide", "mps"), target + + for funcname, data in pointwise_overrides_data.items(): + impl = getattr(data, target) + if impl is None: + if cls._is_unimplemented(funcname): + setattr(cls, funcname, cls._unimplemented(funcname)) + else: + assert funcname not in cls.__dict__, ( + f"multiple definitions of {funcname} on {cls.__name__}" + ) + impl.__name__ = funcname + setattr(cls, funcname, staticmethod(impl)) + + +@dataclasses.dataclass +class OverridesData: + name: str + cpp: Callable[..., str] + # None when not impl in libdevice/triton + triton: Callable[..., str] | None = None + # None when not impl in aten/.../vec + cppvec: Callable[..., str] | None = None + type_promotion_kind: ELEMENTWISE_TYPE_PROMOTION_KIND = ( + ELEMENTWISE_TYPE_PROMOTION_KIND.DEFAULT + ) + halide: Callable[..., str] | None = None + mps: Callable[..., str] | None = None + + +# NB: if you add a new special function, don't forget to update +# torch._inductor.ops_handler too +pointwise_overrides_data: dict[str, OverridesData] = dict( + airy_ai=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x: f"airy_ai_forward({x})", + name="special_airy_ai", + ), + bessel_j0=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x: f"bessel_j0_forward({x})", + triton=lambda x: f"libdevice.j0({x})", + name="special_bessel_j0", + ), + bessel_j1=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x: f"bessel_j1_forward({x})", + triton=lambda x: f"libdevice.j1({x})", + name="special_bessel_j1", + ), + bessel_y0=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x: f"bessel_y0_forward({x})", + triton=lambda x: f"libdevice.y0({x})", + name="special_bessel_y0", + ), + bessel_y1=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x: f"bessel_y1_forward({x})", + triton=lambda x: f"libdevice.y1({x})", + name="special_bessel_y1", + ), + digamma=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x: f"calc_digamma({x})", + cppvec=lambda x: f"{x}.digamma()", + name="digamma", + ), + # no cpp nor triton implementation for entr, it is defined as decomposition + # erf, erfc + erfcx=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x: f"calc_erfcx({x})", + triton=lambda x: f"libdevice.erfcx({x})", + name="special_erfcx", + ), + fma=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x, y, z: f"std::fma({x}, {y}, {z})", + cppvec=lambda x, y, z: f"fmadd({x}, {y}, {z})", + # NOTE: We use tl.fma instead of libdevice.fma because libdevice.fma + # has Flush-To-Zero (FTZ) behavior for subnormal values, which causes + # numerical differences compared to eager CUDA execution. tl.fma + # preserves subnormals and matches eager's FMA precision. + triton=lambda x, y, z: f"tl.fma({x}, {y}, {z})", + name="fma", + ), + # mul_rn: Multiplication with round-to-nearest. This prevents Triton's + # compiler from fusing the multiplication with subsequent operations, + # which is needed to match eager's rounding behavior in operations like + # addcmul where the product must be rounded before use. + # Note: libdevice.mul_rn is not supported on ROCm, so we fall back to + # regular multiplication there. + mul_rn=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.DEFAULT, + cpp=lambda x, y: f"({x}) * ({y})", # C++ doesn't need special handling + triton=lambda x, y: f"({x}) * ({y})" + if torch.version.hip + else f"libdevice.mul_rn({x}, {y})", + name="mul_rn", + ), + # erfinv, exp2, expit, gammaln + igamma=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x, y: f"calc_igamma({x}, {y})", + name="igamma", + ), + igammac=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x, y: f"calc_igammac({x}, {y})", + name="igammac", + ), + gammainc=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x, y: f"calc_igamma({x}, {y})", + name="special_gammainc", + ), + gammaincc=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x, y: f"calc_igammac({x}, {y})", + name="special_gammaincc", + ), + i0=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x: f"calc_i0({x})", + triton=lambda x: f"libdevice.cyl_bessel_i0({x})", + cppvec=lambda x: f"{x}.i0()", + name="i0", + ), + i0e=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x: f"calc_i0e({x})", + cppvec=lambda x: f"{x}.i0e()", + name="special_i0e", + ), + i1=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x: f"calc_i1({x})", + triton=lambda x: f"libdevice.cyl_bessel_i1({x})", + name="special_i1", + ), + i1e=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x: f"calc_i1e({x})", + name="special_i1e", + ), + log_ndtr=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x: f"calc_log_ndtr({x})", + name="special_log_ndtr", + ), + # logit + modified_bessel_i0=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x: f"modified_bessel_i0_forward({x})", + triton=lambda x: f"libdevice.cyl_bessel_i0({x})", + name="special_modified_bessel_i0", + ), + modified_bessel_i1=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x: f"modified_bessel_i1_forward({x})", + triton=lambda x: f"libdevice.cyl_bessel_i1({x})", + name="special_modified_bessel_i1", + ), + modified_bessel_k0=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x: f"modified_bessel_k0_forward({x})", + name="special_modified_bessel_k0", + ), + modified_bessel_k1=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x: f"modified_bessel_k1_forward({x})", + name="special_modified_bessel_k1", + ), + # multigamma + ndtr=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x: f"calc_ndtr({x})", + name="special_ndtr", + ), + ndtri=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x: f"calc_ndtri({x})", + name="special_ndtri", + ), + polygamma=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x, + y: f"{x} == 0 ? calc_digamma({y}) : ({x} == 1 ? trigamma({y}) : calc_polygamma({y}, {x}))", + name="polygamma", + ), + # psi - alias to digamma + # round + scaled_modified_bessel_k0=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x: f"scaled_modified_bessel_k0_forward({x})", + name="special_scaled_modified_bessel_k0", + ), + scaled_modified_bessel_k1=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x: f"scaled_modified_bessel_k1_forward({x})", + name="special_scaled_modified_bessel_k1", + ), + # sinc + spherical_bessel_j0=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x: f"spherical_bessel_j0_forward({x})", + name="special_spherical_bessel_j0", + ), + zeta=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x, y: f"zeta({x}, {y})", + name="special_zeta", + ), + chebyshev_polynomial_t=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x, y: f"chebyshev_polynomial_t_forward({x}, {y})", + name="special_chebyshev_polynomial_t", + ), + chebyshev_polynomial_u=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x, y: f"chebyshev_polynomial_u_forward({x}, {y})", + name="special_chebyshev_polynomial_u", + ), + chebyshev_polynomial_v=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x, y: f"chebyshev_polynomial_v_forward({x}, {y})", + name="special_chebyshev_polynomial_v", + ), + chebyshev_polynomial_w=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x, y: f"chebyshev_polynomial_w_forward({x}, {y})", + name="special_chebyshev_polynomial_w", + ), + legendre_polynomial_p=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x, y: f"legendre_polynomial_p_forward({x}, {y})", + name="special_legendre_polynomial_p", + ), + shifted_chebyshev_polynomial_t=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x, y: f"shifted_chebyshev_polynomial_t_forward({x}, {y})", + name="special_shifted_chebyshev_polynomial_t", + ), + shifted_chebyshev_polynomial_u=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x, y: f"shifted_chebyshev_polynomial_u_forward({x}, {y})", + name="special_shifted_chebyshev_polynomial_u", + ), + shifted_chebyshev_polynomial_v=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x, y: f"shifted_chebyshev_polynomial_v_forward({x}, {y})", + name="special_shifted_chebyshev_polynomial_v", + ), + shifted_chebyshev_polynomial_w=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x, y: f"shifted_chebyshev_polynomial_w_forward({x}, {y})", + name="special_shifted_chebyshev_polynomial_w", + ), + hermite_polynomial_h=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x, y: f"hermite_polynomial_h_forward({x}, {y})", + name="special_hermite_polynomial_h", + ), + hermite_polynomial_he=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x, y: f"hermite_polynomial_he_forward({x}, {y})", + name="special_hermite_polynomial_he", + ), + laguerre_polynomial_l=OverridesData( + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + cpp=lambda x, y: f"laguerre_polynomial_l_forward({x}, {y})", + name="special_laguerre_polynomial_l", + ), +) + + +def is_buffer_removed(name: str) -> bool: + return any( + name in x + for x in ( + V.graph.removed_buffers, + V.kernel.removed_buffers, + V.graph.inplaced_to_remove, + V.kernel.inplaced_to_remove, + ) + ) + + +class DeferredLine(DeferredLineBase): + """A line that can be 'unwritten' by adding name to V.graph.removed_buffers""" + + def __init__(self, name: str, line: str): + super().__init__(line) + self.name = name + assert not isinstance(line, DeferredLineBase) + + def __call__(self) -> str | None: + if not is_buffer_removed(self.name): + return self.line + return None + + def _new_line(self, line: str) -> DeferredLine: + return DeferredLine(self.name, line) + + +class BracesBuffer(IndentedBuffer): + def indent(self, offset: int = 1) -> contextlib.AbstractContextManager[None]: + @contextlib.contextmanager + def ctx() -> Iterator[None]: + for _ in range(offset): + self.writeline("{") + self._indent += 1 + for _ in range(-offset): + self._indent -= 1 + self.writeline("}") + yield + for _ in range(-offset): + self.writeline("{") + self._indent += 1 + for _ in range(offset): + self._indent -= 1 + self.writeline("}") + + return ctx() + + +class InplacedBuffer(NamedTuple): + inner_name: str + other_names: list[str] + + +@dataclasses.dataclass +class ArgName: + name: str + # is_constexpr=True is used to attach a " : tl.constexpr" into the argument list + is_constexpr: bool = False + + def full_name(self) -> str: + return f"{self.name}{' : tl.constexpr' if self.is_constexpr else ''}" + + +class RemovedArg: + def __str__(self) -> str: + return "REMOVED" + + +REMOVED = RemovedArg() + + +class KernelArgs: + @staticmethod + def _lookup( + prefix: str, + odict: dict[_T, str | RemovedArg] | dict[_T, str], + name: _T, + ) -> str: + result: str | RemovedArg = odict.get(name, REMOVED) + if isinstance(result, RemovedArg): + odict[name] = new_result = f"{prefix}{len(odict)}" + return new_result + return result + + def __init__(self) -> None: + self.input_buffers: dict[str, str] = {} + self.output_buffers: dict[str, str | RemovedArg] = {} + self.inplace_buffers: dict[str, InplacedBuffer | RemovedArg] = {} + self.sizevars: dict[sympy.Expr, str] = {} + self.workspace_args: list[WorkspaceArg] = [] + + def __repr__(self) -> str: + return "KernelArgs({})".format( + ", ".join( + map( + repr, + [ + self.input_buffers, + self.output_buffers, + self.inplace_buffers, + self.sizevars, + ], + ) + ) + ) + + @staticmethod + def _buffer_is_marked_removed(name: Any) -> bool: + # this function is needed by MTIA + return isinstance(name, RemovedArg) + + def input(self, name: str) -> str: + if V.graph.scheduler: + name = V.graph.scheduler.mutation_real_name.get(name, name) + assert name not in V.graph.removed_buffers, name + if name in self.output_buffers: + return cast(str, self.output_buffers[name]) + if name in self.inplace_buffers: + return cast(InplacedBuffer, self.inplace_buffers[name]).inner_name + if name.startswith("seed"): + return self._lookup("seed", self.input_buffers, name) + return self._lookup("in_ptr", self.input_buffers, name) + + def output(self, name: str) -> str: + if V.graph.scheduler: + name = V.graph.scheduler.mutation_real_name.get(name, name) + assert name not in V.graph.removed_buffers, name + if name in self.inplace_buffers: + return cast(InplacedBuffer, self.inplace_buffers[name]).inner_name + return self._lookup("out_ptr", self.output_buffers, name) + + def make_inplace(self, input_name: str, output_name: str) -> None: + if input_name in V.graph.unaligned_buffers: + V.graph.unaligned_buffers.add(output_name) + assert output_name not in self.inplace_buffers, output_name + if input_name in self.inplace_buffers: + buf = self.inplace_buffers[input_name] + assert not isinstance(buf, RemovedArg) + buf.other_names.append(output_name) + self.inplace_buffers[output_name] = buf + else: + alive_buffers = [ + val + for val in self.inplace_buffers.values() + if not isinstance(val, RemovedArg) + ] + removed_buffers = [ + val + for val in self.inplace_buffers.values() + if isinstance(val, RemovedArg) + ] + inplace_buffer_idx = len(unique(alive_buffers)) + len(removed_buffers) + buf = InplacedBuffer( + f"in_out_ptr{inplace_buffer_idx}", + [input_name, output_name], + ) + self.inplace_buffers[input_name] = buf + self.inplace_buffers[output_name] = buf + + def workspace( + self, nelem: sympy.Expr, zero_fill: bool, dtype: torch.dtype = torch.uint8 + ) -> tuple[str, str, int]: + """ + Allocate or extend a workspace buffer of nelem elements. + + This function manages the allocation of a workspace buffer. It either creates + a new WorkspaceArg or extends an existing one. + + Note: + - Calling this function will in-place mutate the args by adding or updating + a WorkspaceArg. + - The codegen for generating the Python argdefs and call_defs will check + this field and allocate the buffer accordingly. + - A new argument "ws_ptr" will be present in the generated code. + + Args: + nelem (sympy.Expr): The number of elements to allocate. + zero_fill (bool): Whether to initialize the buffer to zero. + dtype (torch.dtype): the dtype of the workspace tensor + + Returns: + Tuple[str, str, int]: A tuple containing: + - "ws_ptr": A string identifier for the workspace pointer. + - "workspace_{i}": agraph level unique identifier for + the workspace tensor. + - offset: An integer representing the item offset in the workspace. + """ + arg = WorkspaceArg( + count=nelem, + zero_mode=WorkspaceZeroMode.from_bool(zero_fill), + device=V.graph.get_current_device_or_throw(), + outer_name=WorkspaceArg.unique_name(), + dtype=dtype, + ) + for i, existing_arg in enumerate(self.workspace_args): + if WorkspaceArg.can_join(existing_arg, arg): + offset = existing_arg.count + self.workspace_args[i] = WorkspaceArg.join(existing_arg, arg) + return existing_arg.inner_name, existing_arg.outer_name, offset + assert ( + existing_arg.inner_name != arg.inner_name + and existing_arg.outer_name != arg.outer_name + ), existing_arg + self.workspace_args.append(arg) + return arg.inner_name, arg.outer_name, 0 + + def semaphores(self, min_size: sympy.Expr) -> str: + """ + Lazily allocate a graph-wide semaphores buffer with at least min_size. This is a single buffer shared by + all kernels and zero initialized once at graph start. Each kernel must leave the buffer zeroed on exit. + + Warning: multiple calls to this function will return the same buffer. + + Args: + min_size: the number of int32 semaphores required + + Returns: + name of the semaphores buffer + """ + current_device = V.graph.get_current_device_or_throw() + arg = WorkspaceArg( + count=min_size, + zero_mode=WorkspaceZeroMode.ZERO_PER_GRAPH, + dtype=torch.uint32, + inner_name="sem_ptr", + outer_name=f"semaphores_{current_device.type}_{current_device.index}", + device=current_device, + ) + for existing_arg in self.workspace_args: + if existing_arg.inner_name == arg.inner_name: + assert arg == existing_arg, (arg, existing_arg) + self.workspace_args.append(arg) + return arg.inner_name + + def seed_offset(self, name: str, value: int) -> str: + assert isinstance(value, int), (type(value), value) + # here we are lifting a constant integer into an arg to the kernel to try to get additional cache hits + value = sympy.Integer(value) + if value in self.sizevars: + return self.sizevars[value] + if name in self.sizevars.values(): + name = ( + f"{name}{sum(1 for v in self.sizevars.values() if v.startswith(name))}" + ) + self.sizevars[value] = name + return name + + def size(self, name: sympy.Symbol) -> str: + assert isinstance(name, sympy.Symbol), (type(name), name) + if name.name == "seed": + self.sizevars[name] = "seed" # don't manage the name of seeds + return "seed" + return self._lookup("ks", self.sizevars, name) + + def call_names(self) -> Iterator[str]: + return chain( + self.input_buffers.keys(), self.output_buffers.keys(), self.sizevars.keys() + ) + + def arg_name(self, name: str) -> str | None: + """ + Returns inner name of a given outer name. + """ + inplaced = self.inplace_buffers.get(name, None) + if inplaced is not None and not isinstance(inplaced, RemovedArg): + return inplaced.inner_name + output_name = self.output_buffers.get(name, None) + if output_name is not None and not isinstance(output_name, RemovedArg): + return output_name + return self.input_buffers.get(name, None) + + def wrap_ptr_arg(self, buf: str, dtype: torch.dtype) -> str: + return buf + + def wrap_size_arg(self, size: SymbolLike) -> str: + return str(size) + + def cpp_argdefs( + self, dtype_to_cpp_type: dict[torch.dtype, str] | None = None + ) -> tuple[list[str], list[str], list[str]]: + from .cpp_utils import INDEX_TYPE + + if dtype_to_cpp_type is None: + from .cpp_utils import DTYPE_TO_CPP + + dtype_to_cpp_type = DTYPE_TO_CPP + + call_args = [] + arg_defs = [] + arg_types = [] + for inplaced in unique(self.inplace_buffers.values()): + if isinstance(inplaced, RemovedArg): + continue + outer = inplaced.other_names[-1] + inner = inplaced.inner_name + dtype = V.graph.get_dtype(outer) + cpp_dtype = dtype_to_cpp_type[dtype] + arg_defs.append(f"{cpp_dtype}* {inner}") + call_args.append(self.wrap_ptr_arg(outer, dtype)) + arg_types.append(f"{cpp_dtype}*") + for outer, inner in self.input_buffers.items(): + if outer in self.inplace_buffers: + continue + dtype = V.graph.get_dtype(outer) + cpp_dtype = dtype_to_cpp_type[dtype] + arg_defs.append(f"const {cpp_dtype}* {inner}") + call_args.append(self.wrap_ptr_arg(outer, dtype)) + arg_types.append(f"const {cpp_dtype}*") + for outer, maybe_inner in self.output_buffers.items(): + if outer in self.inplace_buffers or isinstance(maybe_inner, RemovedArg): + continue + dtype = V.graph.get_dtype(outer) + cpp_dtype = dtype_to_cpp_type[dtype] + arg_defs.append(f"{cpp_dtype}* {maybe_inner}") + call_args.append(self.wrap_ptr_arg(outer, dtype)) + arg_types.append(f"{cpp_dtype}*") + for outer, inner in self.sizevars.items(): + if isinstance(outer, sympy.Symbol) and symbol_is_type( + outer, (SymT.UNBACKED_FLOAT) + ): + arg_defs.append(f"const float {inner}") + arg_types.append("const float") + else: + arg_defs.append(f"const {INDEX_TYPE} {inner}") + arg_types.append(f"const {INDEX_TYPE}") + call_args.append(self.wrap_size_arg(outer)) + if V.graph.wrapper_code: + V.graph.wrapper_code.ensure_size_computed(outer) + assert not self.workspace_args, "Workspace not supported on CPU " + return arg_defs, call_args, arg_types + + def python_argdefs( + self, + ) -> tuple[list[ArgName], list[str], list[KernelArgType], list[Any]]: + arg_defs: list[ArgName] = [] + call_args: list[str] = [] + arg_types: list[Any] = [] + precompile_args: list[KernelArgType] = [] + for inplaced in unique(self.inplace_buffers.values()): + if isinstance(inplaced, RemovedArg): + continue + arg_defs.append(ArgName(inplaced.inner_name)) + call_args.append(inplaced.other_names[-1]) + arg_types.append(V.graph.get_dtype(inplaced.other_names[-1])) + precompile_args.append( + TensorArg( + name=inplaced.inner_name, + buffer=inplaced.other_names[-1], + dtype=V.graph.get_dtype(inplaced.other_names[-1]), + ) + ) + for outer, inner in chain( + self.input_buffers.items(), + # pyrefly: ignore [bad-argument-type] + self.output_buffers.items(), + ): + if outer in self.inplace_buffers or isinstance(inner, RemovedArg): + continue + arg_defs.append(ArgName(inner)) + call_args.append(outer) + arg_types.append(V.graph.get_dtype(outer)) + precompile_args.append( + TensorArg( + name=inner, + buffer=outer, + dtype=V.graph.get_dtype(outer), + ) + ) + for outer, inner in self.sizevars.items(): + arg_defs.append(ArgName(inner)) + call_args.append(outer) + arg_types.append(type(outer)) + precompile_args.append(SizeArg(inner, outer)) + if V.graph.wrapper_code: + V.graph.wrapper_code.ensure_size_computed(outer) + for arg in self.workspace_args: + arg_defs.append(ArgName(arg.inner_name)) + call_args.append(arg.outer_name) + precompile_args.append(arg) + arg_types.append(arg.dtype) + return arg_defs, call_args, precompile_args, arg_types + + def aliases(self) -> Iterator[tuple[str, str]]: + for inplaced in unique(self.inplace_buffers.values()): + if isinstance(inplaced, RemovedArg): + continue + for other in inplaced.other_names: + if ( + other in V.graph.inplaced_to_remove + or other in V.kernel.inplaced_to_remove + ): + continue + if other in self.input_buffers: + yield self.input_buffers[other], inplaced.inner_name + if other in self.output_buffers: + yield cast(str, self.output_buffers[other]), inplaced.inner_name + + def is_removed(self, name: str) -> bool: + return isinstance( + self.output_buffers.get(name, REMOVED), RemovedArg + ) and isinstance(self.inplace_buffers.get(name, REMOVED), RemovedArg) + + # Includes inplace buffers, excludes removed buffers. Essentially, + # after you do a call into this kernel, which buffers actually contain + # updated data? Modeled off of python_argdefs. + def live_output_buffers(self) -> OrderedSet[str]: + live_outs: OrderedSet[str] = OrderedSet() + for inplaced in unique(self.inplace_buffers.values()): + if isinstance(inplaced, RemovedArg): + continue + live_outs.add(inplaced.other_names[-1]) + for outer, inner in self.output_buffers.items(): + if outer in self.inplace_buffers or isinstance(inner, RemovedArg): + continue + live_outs.add(outer) + return live_outs + + +class CSEVariable: + """A CSEVariable is just a name for an expression but it is useful to be able to annotate them on a backend dependent basis. + To do so, the backends can simply overload `Kernel.create_cse_var` + The "CSEVariable.update_on_args" method gives you a hook for annotations + See example of TritonCSEVariable in triton.py + """ + + def __init__( + self, + name: str, + bounds: ValueRanges[Any], + dtype: torch.dtype | None = None, + shape: BlockShapeType = None, + ): + super().__init__() + assert isinstance(bounds, ValueRanges), type(bounds) + self.name = name + self.bounds = bounds + self.use_count = 1 # track how many times this expression is used + self.dtype = dtype + self.shape = shape + + def __str__(self) -> str: + return self.name + + def __hash__(self) -> int: + return hash(self.name) + + def __eq__(self, other: object) -> bool: + return isinstance(other, CSEVariable) and other.name == self.name + + def update_on_args(self, name: str, args: Any, kwargs: Any) -> None: + pass + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.name!r})" + + +AugmentedKeyT = TypeVar("AugmentedKeyT", default=str) +CSEVariableType = TypeVar("CSEVariableType", bound=CSEVariable, default=CSEVariable) + +if TYPE_CHECKING: + ReductionCacheKey = tuple[ + torch.dtype, + ReductionType, + CSEVariable | tuple[CSEVariable, ...], + ] + + +class CSE(Generic[CSEVariableType, AugmentedKeyT]): + """Common subexpression elimination""" + + def __init__( + self, + prefix: str = "", + suffix: str = "", + name_prefix: str = "tmp", + iter_buffers: itertools.count[int] | None = None, + store_cache: MutableMapping[str, CSEVariableType] | None = None, + reduction_cache: MutableMapping[ReductionCacheKey, CSEVariableType] + | None = None, + varname_map: dict[str, CSEVariableType] | None = None, + ): + self.prefix = prefix + self.suffix = suffix + self._cache: MutableMapping[AugmentedKeyT, CSEVariableType] = {} + self.name_prefix = name_prefix + self.store_cache: MutableMapping[str, CSEVariableType] = store_cache or {} + self.reduction_cache: MutableMapping[ReductionCacheKey, CSEVariableType] = ( + reduction_cache or {} + ) + self.iter_buffer_ids: itertools.count[int] = iter_buffers or itertools.count() + self.invalidated_stores: OrderedSet[str] = OrderedSet() + self.varname_map: dict[str, CSEVariableType] = varname_map or {} + + def invalidate(self, keep_vars: OrderedSet[CSEVariable]) -> None: + for name, tmp in [*self.store_cache.items()]: + if tmp not in keep_vars: + del self.store_cache[name] + self.invalidated_stores.add(name) + if keep_vars: + self._cache = {k: v for k, v in self._cache.items() if v in keep_vars} + else: + self._cache = {} + + def clone(self) -> Self: + return type(self)( + prefix=self.prefix, + suffix=self.suffix, + name_prefix=self.name_prefix, + iter_buffers=self.iter_buffer_ids, + store_cache=self.store_cache, + varname_map=self.varname_map, + reduction_cache=self.reduction_cache, + ) + + def scoped_copy(self) -> Self: + """Return a copy of using ScopedDict so changes to *_cache aren't visible in self""" + new_cse = self.clone() + new_cse._cache = ScopedDict(self._cache) + new_cse.reduction_cache = ScopedDict(self.reduction_cache) + new_cse.store_cache = ScopedDict(self.store_cache) + return new_cse + + def augment_key(self, cache_key: str) -> AugmentedKeyT: + "Override this method to augment cache key with backend specifics" + return cast(AugmentedKeyT, cache_key) + + def put(self, cache_key: str, val: CSEVariableType) -> None: + self._cache[self.augment_key(cache_key)] = val + + def contains(self, cache_key: str) -> bool: + return self.augment_key(cache_key) in self._cache + + def try_get(self, cache_key: str) -> CSEVariableType | None: + return self._cache.get(self.augment_key(cache_key), None) + + def get(self, cache_key: str) -> CSEVariableType: + return self._cache[self.augment_key(cache_key)] + + def generate( + self, + buffer: IndentedBuffer, + expr: str | CSEVariable | OpsValue | IndentedBuffer | DeferredLineBase, + *, + bounds: ValueRanges[Any] = ValueRanges.unknown(), + write: bool = True, + assignment: bool = True, + dtype: torch.dtype | None = None, + shape: BlockShapeType = None, + ) -> CSEVariableType: + if isinstance(expr, OpsValue): + expr = expr.value + + assert write or assignment + if isinstance(expr, CSEVariable): + # If the expressions were always created with all the information, we could + # assert expr.bounds == bounds, but sometimes the expression is created + # with the loose ValueRanges.unknown(), so we need to tighten the bounds + expr.bounds = expr.bounds.tighten(bounds) + expr.use_count += 1 + return cast(CSEVariableType, expr) + elif isinstance(expr, IndentedBuffer): + cache_key = expr.getvalue() + elif isinstance(expr, DeferredLineBase): + cache_key = expr.line + else: + assert isinstance(expr, str) + cache_key = expr + var = self.try_get(cache_key) + if shape is None and not assignment: + # since there's no assignment to a variable, use any shape here + # other than None to avoid the unknown shape failures + shape = () + if not var: + var = self.newvar(bounds, dtype, shape) + self.put(cache_key, var) + if write: + if V.kernel.current_node: + V.kernel.current_node.codegen_originating_info( + buffer, only_once=True + ) + if isinstance(expr, IndentedBuffer): + if assignment: + buffer.writeline(f"{self.prefix}{var} =") + buffer.splice(expr) + buffer.writeline(self.suffix) + elif isinstance(expr, DeferredLineBase): + assert assignment + buffer.writeline( + expr._new_line(f"{self.prefix}{var} = {expr.line}{self.suffix}") + ) + else: + if assignment: + line = f"{self.prefix}{var} = {expr}{self.suffix}" + else: + line = f"{expr}{self.suffix}" + buffer.writeline(line) + + # cpp backend cannot determine is_vec at this point + if ( + assignment + and ( + config.test_configs.runtime_triton_dtype_assert + or config.test_configs.static_cpp_dtype_assert + ) + and dtype is not None + and get_current_backend() != "cpp" + ): + check_dtype(buffer, var, dtype) + + else: + var.bounds = var.bounds.tighten(bounds) + var.use_count += 1 + + return var + + def newvar( + self, + bounds: ValueRanges[Any] = ValueRanges.unknown(), + dtype: torch.dtype | None = None, + shape: BlockShapeType = None, + ) -> CSEVariableType: + var_name = f"{self.name_prefix}{next(self.iter_buffer_ids)}" + var = V.kernel.create_cse_var(var_name, bounds, dtype, shape) + self.varname_map[var_name] = var + return var + + def namedvar( + self, + name: str, + bounds: ValueRanges[Any] = ValueRanges.unknown(), + dtype: torch.dtype | None = None, + shape: BlockShapeType = None, + ) -> CSEVariableType: + torch._check_value( + name not in self.varname_map, lambda: f"duplicate name: {name}" + ) + var = V.kernel.create_cse_var(name, bounds, dtype, shape) + self.varname_map[name] = var + return var + + +class CodeGen: + def __init__(self) -> None: + super().__init__() + self.exit_stack = contextlib.ExitStack() + + def __enter__(self) -> Self: + self.exit_stack.__enter__() + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self.exit_stack.__exit__(exc_type, exc_val, exc_tb) + + +class Kernel(CodeGen, Generic[CSEVariableType]): + newvar_prefix: str = "" + suffix: str = "" + overrides: Callable[[], OpsHandler[Any]] | None = None + + def __init__( + self, args: KernelArgs | None = None, increase_kernel_count: bool = True + ) -> None: + super().__init__() + if increase_kernel_count: + # pyrefly: ignore [bad-assignment] + metrics.generated_kernel_count += 1 + self.args = args or KernelArgs() + self.loads = IndentedBuffer() + self.compute = IndentedBuffer() + self.stores = IndentedBuffer() + + self.atomic_add_found = False + self.num_load = 0 + self.num_store = 0 + self.num_reduction = 0 + + self.cse: CSE[CSEVariableType, Any] = CSE(self.newvar_prefix, self.suffix) + self.must_keep_buffers: OrderedSet[str] = OrderedSet() + self.store_buffer_names: OrderedSet[str] = OrderedSet() + self._load_mask: str | None = None + self._load_other: None | int | float = None + # OrderedSet in set_current_node + self.current_node: SchedulerNode | None = None + self.node_to_bounds: dict[torch.fx.Node, ValueRanges[Any]] | None = None + + self.removed_buffers: OrderedSet[str] = OrderedSet() + self.inplaced_to_remove: OrderedSet[str] = OrderedSet() + + # key: the buffer to write + # value: the buffer to read and whose memory can be reused for + # the buffer specified by key + self.inplace_update_buffers: dict[str, str] = {} + # Set minimum number of elements processed per thread. + self.min_elem_per_thread = 1 + self.kernel_name: str | None = None + + @contextlib.contextmanager + def set_current_node(self, node: SchedulerNode) -> Iterator[None]: + prior = self.current_node + self.current_node = node + self.node_to_bounds = node._body.bounds().get_bounds() + try: + yield + finally: + self.current_node = prior + + @contextlib.contextmanager + def swap_buffers( + self, + lb: IndentedBuffer, + cb: IndentedBuffer | None = None, + sb: IndentedBuffer | None = None, + ) -> Iterator[None]: + if cb is None: + cb = lb + if disallow_stores := sb is None: + sb = IndentedBuffer() + loads = self.loads + compute = self.compute + stores = self.stores + cse = self.cse + self.loads = lb + self.compute = cb + self.stores = sb + self.cse = cse.scoped_copy() + try: + yield + finally: + self.loads = loads + self.compute = compute + self.stores = stores + self.cse = cse + # pyrefly: ignore [unbound-name] + if disallow_stores: + assert not sb, "unexpected store inside swap_buffers" + + def emit_kernel_override( + self, + wrapper, + src_code: str, + kernel_name: str, + node_schedule, + kernel_path: str, + get_kernel_metadata, + ) -> bool: + """Override kernel emission. Return True if overridden, False to use default. + + External template handlers (e.g. Helion) can override this method + to implement custom kernel emission to the wrapper. + """ + return False + + def load(self, name: str, index: sympy.Expr) -> CSEVariable: + raise NotImplementedError + + def indirect_load(self, name: str, index: sympy.Expr) -> CSEVariable: + """A load the depends on an index we have read""" + prior = self.loads + try: + # put the load in the compute section as it might have deps + self.loads = self.compute + return self.load(name, index) + finally: + self.loads = prior + + def store_reduction(self, name: str, index: sympy.Expr, value: CSEVariable) -> None: + raise NotImplementedError + + def store( + self, name: str, index: sympy.Expr, value: CSEVariable, mode: StoreMode = None + ) -> None: + raise NotImplementedError + + def device_assert_async(self, cond: CSEVariable, msg: str) -> None: + raise NotImplementedError( + f"{type(self).__name__}: device_assert_async should be handled by CSEProxy" + ) + + def reduction( + self, + dtype: torch.dtype, + src_dtype: torch.dtype, + reduction_type: ReductionType, + value: CSEVariable | tuple[CSEVariable, ...], + ) -> CSEVariable | tuple[CSEVariable, ...]: + raise NotImplementedError + + def partial_accumulate( + self, + name: str, + reduction_type: ReductionType, + value: CSEVariable, + extra_meta: dict[str, Any], + ) -> None: + raise NotImplementedError + + def scan( + self, + dtypes: tuple[torch.dtype, ...], + combine_fn: Callable[ + [tuple[CSEVariable, ...], tuple[CSEVariable, ...]], tuple[CSEVariable, ...] + ], + values: tuple[CSEVariable, ...], + ) -> tuple[CSEVariable, ...]: + raise NotImplementedError + + def sort( + self, + dtypes: tuple[torch.dtype, ...], + values: tuple[CSEVariable, ...], + stable: bool, + descending: bool, + ) -> tuple[CSEVariable, ...]: + raise NotImplementedError + + def var_ranges(self) -> dict[sympy.Symbol, sympy.Expr]: + raise NotImplementedError + + def bucketize( + self, + values: CSEVariable, + boundaries: tuple[str, sympy.Expr, sympy.Expr, sympy.Expr], + boundary_indices: CSEVariable, + indexing_dtype: torch.dtype, + right: bool, + sorter: tuple[str, sympy.Expr] | None = None, + sorter_indices: CSEVariable | None = None, + ) -> CSEVariable: + """ + See [Note: Inductor bucketize op] + """ + raise NotImplementedError + + @property + def assert_function(self) -> str: + raise NotImplementedError + + def indirect_assert( + self, + var: CSEVariable | str, + lower: str | None, + upper: str | None, + mask: CSEVariable | str | None = None, + ) -> str: + if isinstance(var, CSEVariable): + var = str(var) + assert isinstance(var, str), type(var) + assert lower is None or isinstance(lower, str) + assert upper is None or isinstance(upper, str) + if lower and upper: + # The conditions need to be in parens because of Python's operator precedence. + # It'd be less error-prone to use and/or/not, which is supported by triton + cond = f"({lower} <= {var}) & ({var} < {upper})" + cond_print = f"{lower} <= {var} < {upper}" + elif lower: + cond = f"{lower} <= {var}" + cond_print = cond + else: + assert upper + cond = f"{var} < {upper}" + cond_print = cond + + if mask: + cond = f"({cond}) | ~({mask})" + + return f'{self.assert_function}({cond}, "index out of bounds: {cond_print}")' + + def check_bounds( + self, expr: sympy.Expr, size: sympy.Expr, lower: bool, upper: bool + ) -> None: + raise NotImplementedError + + def index_to_str(self, index: sympy.Expr) -> str: + raise NotImplementedError + + def __enter__(self) -> Self: + super().__enter__() + assert self.overrides + self.exit_stack.enter_context( + V.set_ops_handler(CSEProxy(self, self.overrides())) + ) + self.exit_stack.enter_context(V.set_kernel_handler(self)) + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self.remove_kernel_local_buffers() + super().__exit__(exc_type, exc_val, exc_tb) + + def remove_kernel_local_buffers(self) -> None: + """ + Any buffers that are both created and have a last use in the + same kernel can be removed. + + Note that V.graph.scheduler can be None when codegening triton template + kernels. + """ + scheduler = V.graph.scheduler + if not scheduler: + return + fused_node_names = OrderedSet( + scheduler.name_to_buf[buf].defining_op_name() + for buf in self.store_buffer_names + if buf in scheduler.name_to_buf + ) + names_to_remove: OrderedSet[str] = OrderedSet() + for name in self.store_buffer_names: + if ( + name not in self.must_keep_buffers + and name not in self.args.input_buffers + and scheduler.can_buffer_be_removed_through_fusion( + name, fused_node_names + ) + ): + self.num_store -= 1 + names_to_remove.add(name) + + for name in names_to_remove: + if name in self.args.inplace_buffers: + buf = self.args.inplace_buffers[name] + if isinstance(buf, RemovedArg): + continue + remove = all(n in names_to_remove for n in buf.other_names) + if remove: + self.remove_inplace_buffer(name) + self.inplaced_to_remove.add(name) + else: + self.remove_buffer(name) + + def remove_buffer(self, name: str) -> None: + # Assign a special value instead of deleting the entry + # because we still rely on output_buffers's length to + # generate unique arg name. + log.debug("remove_buffer(%r)", name) + self.args.output_buffers[name] = REMOVED + self.removed_buffers.add(name) + + def remove_inplace_buffer(self, name: str) -> None: + log.debug("removing_inplace_buffer(%r)", name) + self.args.inplace_buffers[name] = REMOVED + self.removed_buffers.add(name) + + def rename_indexing( + self, index: list[sympy.Expr] | tuple[sympy.Expr, ...] | sympy.Expr + ) -> sympy.Expr: + # adds the necessary kernel args for index expressions + # and renames variables in index expressions to kernel arg names + if isinstance(index, (list, tuple)): + return [self.rename_indexing(x) for x in index] + index = V.graph.sizevars.simplify(index) + sorted_symbols = sorted(index.free_symbols, key=lambda s: s.name) + replacements = { + x: self.args.size(x) + for x in sorted_symbols + if symbol_is_type( + x, + ( + SymT.UNBACKED_INT, + SymT.SIZE, + SymT.PRECOMPUTED_SIZE, + SymT.UNBACKED_FLOAT, + ), + ) + } + return sympy_subs(index, replacements) + + def create_cse_var(self, *args: Any, **kwargs: Any) -> CSEVariable: + return CSEVariable(*args, **kwargs) + + def arg_name(self, node: IRNode) -> str | None: + """ + Returns arg name of a given input or output node. + """ + if node is None: + return None + return self.args.arg_name(node.get_name()) + + +@dataclasses.dataclass +class OptimizationContext: + key: ClassVar[str] = "opt_ctx" + + dtype: torch.dtype | None = None + ops_name: str = "" + + +@functools.cache +def jinja2_env() -> Any: + try: + import jinja2 + + return jinja2.Environment( + undefined=jinja2.StrictUndefined, + ) + except ImportError: + return None + + +class KernelTemplate: + """ + Base class for defining kernel templates. + + Children classes: TritonTemplate, CUTLASSTemplate + """ + + @staticmethod + def indent_except_first( + source: str, num_indents: int, indents_spacing: int = 4 + ) -> str: + lines = source.splitlines(True) + if len(lines) > 1: + lines[1:] = [ + (" " * indents_spacing * num_indents) + line for line in lines[1:] + ] + return "".join(lines) + + @staticmethod + def _template_from_string(source: str) -> Any: + env = jinja2_env() + if env is None: + return None + env.filters["indent_except_first"] = KernelTemplate.indent_except_first + from jinja2 import TemplateSyntaxError + + try: + return env.from_string(source) + except TemplateSyntaxError as e: + + class DetailedTemplateSyntaxError(TemplateSyntaxError): + def __init__(self, original_error: TemplateSyntaxError) -> None: + super().__init__( + original_error.message, + original_error.lineno, + original_error.name, + original_error.filename, + ) + self.original_error = original_error + + def __str__(self) -> str: + error_info = f"Error in template at line {self.lineno}\n" + error_info += f"Error message: {self.message}\n" + if hasattr(self.original_error, "source"): + lines = self.original_error.source.split("\n") + error_info += "Context:\n" + start = max(0, self.lineno - 2) + end = min(len(lines), self.lineno + 2) + for i in range(start, end): + if i == self.lineno - 1: + error_info += f"{i + 1}: --> {lines[i]}\n" + if hasattr(self.original_error, "column"): + error_info += ( + " " + + " " * (self.original_error.column - 1) + + "^\n" + ) + else: + error_info += f"{i + 1}: {lines[i]}\n" + return error_info + + raise DetailedTemplateSyntaxError(e) from e + + @staticmethod + def _fake_get_dtype( + fake_outs: list[Buffer] | Buffer, + ) -> Callable[[str], torch.dtype]: + _get_dtype_real = V.graph.get_dtype + if isinstance(fake_outs, (list, tuple)): + lookup = {buf.get_name(): buf.get_dtype() for buf in fake_outs} + else: + lookup = {fake_outs.get_name(): fake_outs.get_dtype()} + + def get_dtype(name: str) -> torch.dtype: + result = lookup.get(name) + if result is not None: + return result + return _get_dtype_real(name) + + return get_dtype + + def __init__(self, name: str, hash: str | None = None) -> None: + self.name = name + self._hash = hash + + @property + def uid(self) -> str: + """ + entry point to override for templates to ensure a uid e.g. through a prefix + + the purpose of this is that every KernelTemplate/ExternKernelChoice is unique + in the system, but reproducible e.g. restarting pytorch should yield the same id + """ + # TODO(coconutruben): add some central registration to assert on global uniqueness + return self.name + + @property + def src_hash(self) -> str | None: + """ + source hash for a Template. + + Templates can optionally provide a src hash to make it easier to cache/validate that + a template has not changed from one version to another. Override this if that detection + is different for your specific Template + """ + return self._hash + + def choice_or_none(self, **kwargs: Any) -> ChoiceCaller | None: + """ + Maybe generates a new ChoiceCaller and returns it, or None if generation fails. + + kwargs: Additional kwargs to be passed to self.generate() to generate a new ChoiceCaller. + """ + temp_choices: list[Any] = [] + result = self.maybe_append_choice(temp_choices, **kwargs) + if result is None and len(temp_choices) == 1: + return temp_choices[0] + return None + + def maybe_append_choice( + self, choices: list[Any], **kwargs: Any + ) -> NotImplementedError | None: + """ + Maybe generates a new ChoiceCaller and appends it into existing choices. + Returns None if success, otherwise returns the error. + + choices: A list of ChoiceCallers. + kwargs: Additional kwargs to be passed to self.generate() to generate a new ChoiceCaller. + """ + + try: + choices.append(self.generate(**kwargs)) + return None + except NotImplementedError as e: + log.info( # noqa: G200 + "Cannot Append Choice: %s. KernelTemplate type is %s", + e, + type(self), + stack_info=log.getEffectiveLevel() < logging.INFO, + ) + return e + + def generate(self, **kwargs: Any) -> ChoiceCaller: + """ + Generates a ChoiceCaller instance from the given arguments. + """ + + raise NotImplementedError + + +class CSEProxy(DefaultHandler): + """A ops handler that proxies calls to `kernel` and its + handler and returns `CSEVariable`s with correct shape and dtype. + """ + + name = "CSEProxy" + + def __init__(self, kernel: Kernel[Any], parent_handler: OpsHandler[Any]): + super().__init__() + from ..bounds import ValueRangeAnalysis + + self.vr_analysis = ValueRangeAnalysis() + self.kernel = kernel + self.parent_handler = parent_handler + + def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: + bounds = self._bound_variable(name, *args, **kwargs) + + value = getattr(self.parent_handler, name)(*args, **kwargs) + dtype_handler = DtypePropagationOpsHandler() + shape_handler = ShapePropagationOpsHandler() + + backend = get_current_backend() + + shape_op = getattr(shape_handler, name) + output_dtype = None + output_shape = None + + if name == "masked" and backend == "triton": + output_dtype = value.dtype + output_shape = value.shape + elif name == "masked" and backend == "cpp": + output_dtype = V.interpreter.current_node.meta.get( + OptimizationContext.key, None + ).dtype + # TODO: fix me + output_shape = None + elif backend in ("triton", "cpp", "mps"): + dtype_op = getattr(dtype_handler, name) + output_dtype = dtype_op(*args, **kwargs) + output_shape = shape_op(*args, **kwargs) + + if backend in ("triton", "cpp"): + # maybe there are some exceptions on mps? + assert output_dtype is not None + + output_idx = 0 + + def do_cse(v: str | CSEVariable) -> CSEVariable: + # we tree_map over the output, so we need to fetch corresponding dtype + nonlocal output_idx + var_dtype: torch.dtype | None = ( + output_dtype[output_idx] + if isinstance(output_dtype, (list, tuple)) + else output_dtype + ) + var_shape: BlockShapeType = ( + output_shape[output_idx] # type: ignore[assignment] + if isinstance(output_shape, (list, tuple)) + and len(output_shape) > 0 + and isinstance(output_shape[0], (list, tuple)) + else output_shape + ) + output_idx += 1 + + # some cpp op implementations don't set the dtype + if isinstance(v, CSEVariable): + if backend == "cpp" and v.dtype is None: + v.dtype = var_dtype + if v.shape is None: + v.shape = var_shape + + csevar = V.kernel.cse.generate( + V.kernel.compute, + v, + bounds=bounds, + dtype=output_dtype, + shape=output_shape, + ) + + csevar.update_on_args(name, args, kwargs) + + if ( + config.test_configs.runtime_triton_dtype_assert + or config.test_configs.static_cpp_dtype_assert + ): + assert var_dtype is not None + check_dtype(V.kernel.compute, csevar, var_dtype) + + if config.test_configs.runtime_triton_shape_assert: + assert output_shape is not None + check_shape(V.kernel.compute, csevar, output_shape) + + if config.runtime_triton_nan_asserts: + check_nan(V.kernel.compute, csevar) + + return csevar + + return pytree.tree_map(do_cse, value) + + def _bound_variable(self, name: str, *args: Any, **kwargs: Any) -> ValueRanges[Any]: + """ + If the variable comes from an FX node, we forward the bound we have already computed + Else, if the variable when codegen'ing another op, we try to compute its bounds + """ + from ..bounds import ValueRangeAnalysis + from ..select_algorithm import TritonTemplateKernel + from .cutlass.kernel import CUTLASSTemplateKernel + + if isinstance(V.kernel, TritonTemplateKernel): + return ValueRanges.unknown() + + if isinstance(V.kernel, CUTLASSTemplateKernel): + return ValueRanges.unknown() + + if isinstance(V.interpreter, NullHandler): + return ValueRanges.unknown() + + fx_node = V.interpreter.current_node + if fx_node.target == name and self.kernel.node_to_bounds is not None: + assert isinstance(self.kernel.node_to_bounds, dict), type( + self.kernel.node_to_bounds + ) + return self.kernel.node_to_bounds.get(fx_node, ValueRanges.unknown()) + elif config.compute_all_bounds and hasattr(ValueRangeAnalysis, name): + # These create lots of inner strings. We would need to compute the bounds at the ops + # We will also likely not get much from computing VRs on these nodes + if any(s in fx_node.target for s in ("set_indirect", "reduction", "scan")): + return ValueRanges.unknown() + + # We assume that the inputs come from `ops.` and are not strings. If you want to generate + # intermediary strings, wrap them in CSE variables with properly initialised bounds. + + # If there is no FX bound but we know how to compute one we do so + assert not kwargs + + def arg_to_bound(x: Any) -> Any: + if isinstance(x, CSEVariable): + return x.bounds + elif isinstance(x, sympy.Expr): + return bound_sympy(x) + else: + return x + + arg_bounds = list(map(arg_to_bound, args)) + return getattr(self.vr_analysis, name)(*arg_bounds) + return ValueRanges.unknown() + + def indirect_indexing( + self, + var: CSEVariable, + size: sympy.Expr | int, + check: bool = True, + wrap_neg: bool = True, + ) -> sympy.Symbol: + if isinstance(size, int): + size = sympy.Integer(size) + assert isinstance(size, sympy.Expr), (type(size), size) + # Skip CSE since this doesn't return an expression + + if var.bounds.lower < 0: + if wrap_neg: + stm = ops.add(var, ops.index_expr(size, torch.long)) + # Mixed negative and non-negative + if var.bounds.upper >= 0: + lt = ops.lt(var, 0) + stm = ops.where(lt, stm, var) + else: + stm = var + + # Propagate bounds as we know how to compute them properly + new_bounds = ValueRanges.unknown() + if var.bounds != ValueRanges.unknown() and isinstance(size, sympy.Number): + # Take the negative part of the bound and add size to it + # Then take union of that and the positive part + # This is a tighter bound than that of a generic ops.where, as we have info on the cond + neg_bounds = var.bounds & ValueRanges(-int_oo, -1) + new_bounds = ValueRanges( + neg_bounds.lower + size, neg_bounds.upper + size + ) + # We don't have a good way of representing the empty range + if var.bounds.upper >= 0: + pos = var.bounds & ValueRanges(0, int_oo) + new_bounds = new_bounds | pos + + var = self.kernel.cse.generate( + self.kernel.compute, + stm, + bounds=new_bounds, + dtype=var.dtype, + shape=var.shape, + ) + + sympy_var = self.parent_handler.indirect_indexing(var, size, check) + if generate_assert(check): + assert_lower = not (var.bounds.lower >= 0) + # value ranges cannot x < s when x and s are symbols + assert_upper = not isinstance(size, sympy.Number) or not ( + var.bounds.upper < size + ) + self.kernel.check_bounds(sympy_var, size, assert_lower, assert_upper) + return sympy_var + + def check_bounds( + self, expr: sympy.Expr, size: sympy.Expr, lower: bool, upper: bool + ) -> None: + return self.kernel.check_bounds(expr, size, lower, upper) + + def load(self, name: str, index: sympy.Expr) -> CSEVariable: + if name in self.kernel.cse.invalidated_stores: + # A load from an invalidated store requires us to + # keep the actual buffer around + V.kernel.must_keep_buffers.add(name) + if free_symbol_is_type(index, SymT.TMP): + return self.kernel.indirect_load(name, index) + store_cache = self.kernel.cse.store_cache + if name in store_cache: + return store_cache[name] + out = self.kernel.load(name, index) + # count load that is not in the store_cache, and also not in the + # cse cache. + if out.use_count == 1: + self.kernel.num_load += 1 + return out + + def _update_store_cache(self, name: str, value: CSEVariable) -> None: + self.kernel.cse.store_cache[name] = value + if self.kernel.current_node and name in V.graph.name_to_buffer: + buf = self.kernel.current_node.get_output(name) + for other_name in buf.get_mutations(): + self.kernel.cse.store_cache[other_name] = value + + def store( + self, name: str, index: sympy.Expr, value: CSEVariable, mode: StoreMode = None + ) -> None: + self.kernel.store_buffer_names.add(name) + # Update store cache when mode is None or "tma" + if mode != "atomic_add": + self._update_store_cache(name, value) + if name not in V.graph.removed_buffers: + self.kernel.store(name, index, value, mode=mode) + self.kernel.num_store += 1 + + def device_assert_async(self, cond: CSEVariable, msg: str) -> None: + self.kernel.device_assert_async(cond, msg) + + # pyrefly: ignore [bad-override] + def partial_accumulate(self, *args: Any) -> None: + self.kernel.partial_accumulate(*args) + + def store_reduction(self, name: str, index: sympy.Expr, value: CSEVariable) -> None: + self.kernel.store_buffer_names.add(name) + self._update_store_cache(name, value) + + if name not in V.graph.removed_buffers: + self.kernel.num_store += 1 + return self.kernel.store_reduction(name, index, value) + + def reduction( + self, + dtype: torch.dtype, + src_dtype: torch.dtype, + reduction_type: ReductionType, + value: CSEVariable | tuple[CSEVariable, ...], + ) -> CSEVariable | tuple[CSEVariable, ...]: + self.kernel.num_reduction += 1 + return self.kernel.reduction(dtype, src_dtype, reduction_type, value) + + def scan( + self, + dtypes: tuple[torch.dtype, ...], + combine_fn: Callable[ + [tuple[CSEVariable, ...], tuple[CSEVariable, ...]], + tuple[CSEVariable, ...], + ], + values: tuple[CSEVariable, ...], + ) -> tuple[CSEVariable, ...]: + return self.kernel.scan(dtypes, combine_fn, values) + + def sort( + self, + dtypes: tuple[torch.dtype, ...], + values: tuple[CSEVariable, ...], + stable: bool, + descending: bool, + ) -> tuple[CSEVariable, ...]: + return self.kernel.sort(dtypes, values, stable, descending) + + def bucketize( + self, + values: CSEVariable, + boundaries: tuple[str, sympy.Expr, sympy.Expr, sympy.Expr], + boundary_indices: CSEVariable, + indexing_dtype: torch.dtype, + right: bool, + sorter: tuple[str, sympy.Expr] | None = None, + sorter_indices: CSEVariable | None = None, + ) -> CSEVariable: + """ + [Note: Inductor bucketize op] + + Inputs: + ------- + values: the values to be bucketized. + boundaries: a tuple containing + (a) the name of the boundaries tensor (which must be sorted, unless + the sorting tensor is present), + (b) the length of the tensor in the last dimension (i.e. the length of + one set of boundaries), + (c) the number of elements in the underlying storage (i.e. the length + of the flattened tensor, ignoring striding), and + (d) the stride of the tensor in the last dimension. + boundary_indices: indices into a flattened version of the boundaries + tensor, of the same size and shape as "values". Each index points to + the first element in the set of boundaries to be used for the + corresponding value. + indexing_dtype: the dtype to use when indexing into the boundaries + tensor. This must be int64 or int32. This additionally specifies the + dtype of the return value. + right: see "Details" below. + sorter: an optional tuple containing + (a) the name of an optional sorting tensor, used to access unsorted + boundaries without reordering the boundaries tensor, and + (b) the stride of the tensor in the last dimension. + The values in the sorting tensor are used as indices into the *last* + dimension of the boundaries tensor, with all other indices matching. + The size of the sorting and boundaries tensors must be equivalent. + sorter_indices: must be present if the sorting array is present; see + "boundary_indices" for the equivalent definition for the boundaries + tensor. + + Output: + ------- + The buckets each value belongs in, within a given set of boundaries. 0 + indicates a position before the first boundary, and len(boundaries_set) + represents a position after the last boundary. + + Details: + -------- + Given a value and a set of boundaries, calculate the bucket that each + value belongs to. This works differently in 1-D and N-D cases. + + for values [[-1, 0, 1, 2], [3, 4, 5, 9]], boundaries [0, 4, 4, 8], right=True + return = [[ 0, 1, 1, 1], [1, 3, 3, 4]]. + + for values [[-1, 0, 1, 2], [3, 4, 5, 9]], boundaries [[0, 4], [4, 8]], right=True + return = [[ 0, 1, 1, 1], [0, 1, 1, 2]] + + Note that in the N-D boundaries case, the shape of "values" and + "boundaries" must match in every dimension _except_ the last. + + When right == False, bucket i refers to range (boundaries[i], boundaries[i+1]]. + When right == True, bucket i refers to range [boundaries[i], boundaries[i+1]). + + Boundaries must be non-decreasing, or a sorter must be provided which + would re-index offsets in a non-decreasing order (e.g. the second output + of torch.sort(offsets)). Otherwise, the result is undefined. + """ + return self.kernel.bucketize( + values, + boundaries, + boundary_indices, + indexing_dtype, + right, + sorter, + sorter_indices, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp.py new file mode 100644 index 0000000000000000000000000000000000000000..a392c0c184576a7d4cbff804793a65d0b8409511 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp.py @@ -0,0 +1,5966 @@ +# mypy: allow-untyped-defs +import contextlib +import dataclasses +import functools +import itertools +import math +import operator +import os +import re +import sys +import warnings +from collections.abc import Callable, Sequence +from enum import Enum +from typing import Any, cast, Optional + +import sympy + +import torch +import torch.fx +from torch._inductor import dependencies +from torch._prims_common import is_float_dtype, is_integer_dtype +from torch.utils._ordered_set import OrderedSet +from torch.utils._sympy.functions import CeilDiv, FloorDiv, ModularIndexing +from torch.utils._sympy.symbol import free_symbol_is_type, symbol_is_type, SymT + +from ..._dynamo.utils import counters +from .. import config, cpp_builder, cpu_vec_isa, ir, metrics +from ..debug import set_kernel_post_grad_provenance_tracing +from ..loop_body import LoopBody +from ..scheduler import ( + BaseSchedulerNode, + BaseScheduling, + ExternKernelSchedulerNode, + ForeachKernelSchedulerNode, + FusedSchedulerNode, + Scheduler, + SchedulerNode, +) +from ..utils import ( + cache_on_self, + get_bounds_index_expr, + get_fused_kernel_name, + has_free_symbols, + is_multi_outputs_template, + is_welford_reduction, + parallel_num_threads, + Placeholder, + sympy_index_symbol, + sympy_index_symbol_with_prefix, + sympy_product, + sympy_subs, +) +from ..virtualized import NullKernelHandler, ops, OpsValue, V +from .common import ( + BackendFeature, + BracesBuffer, + CSE, + CSEVariable, + DataTypePropagation, + DeferredLine, + DTYPE_TO_COMPUTATION_DTYPE, + IndentedBuffer, + Kernel, + KernelArgs, + OpOverrides, + OptimizationContext, +) +from .cpp_utils import ( + _get_dtype_from_loopbodies, + _get_loop_body, + cexpr, + cexpr_index, + codegen_rand, + CppCSEVariable, + DTYPE_TO_CPP, + get_promote_dtype, + INDEX_TYPE, + LocalBufferContext, + may_unify_binary_op_mask_type, + promote_args, + template_fusion_with_epilogues_supported, + unify_mask_base_type, + value_to_cpp, +) + + +_IS_WINDOWS = sys.platform == "win32" + + +@functools.cache +def get_export_declaration(): + return "__declspec(dllexport)" if _IS_WINDOWS else "" + + +schedule_log = torch._logging.getArtifactLogger(__name__, "schedule") + +NATIVE_OMP_RTYPES = OrderedSet(["+", "*", "^", "||", "min", "max"]) +RTYPE_TO_CPP = { + "sum": "+", + "prod": "*", + "xor_sum": "^", + "min": "min", + "max": "max", + "argmin": "argmin", + "argmax": "argmax", + "any": "||", + "welford_reduce": "welford", + "welford_combine": "welford", +} +VECTORIZABLE_RTYPES = OrderedSet( + [ + "max", + "min", + "sum", + "prod", + "xor_sum", + "welford_reduce", + "welford_combine", + "argmin", + "argmax", + "any", + ] +) + +PYTHON_TO_CPP = { + "Tensor": "at::Tensor", + "int": "long", + "float": "double", + "bool": "bool", + "str": "std::string", + "ScalarType": "c10::ScalarType", + "MemoryFormat": "at::MemoryFormat", + "Layout": "at::Layout", + "Device": "at::Device", + "number": "at::Scalar", +} + +CONTAINER_PYTHON_TO_CPP = { + "List": "std::vector", + "Optional": "std::optional", +} + +DTYPE_LOWP_FP = [ + torch.bfloat16, + torch.float16, +] + +VECTORIZABLE_DTYPES: list[torch.dtype] = [ + torch.float64, + torch.float, + torch.bfloat16, + torch.float16, + torch.bool, + torch.uint8, + torch.int8, + torch.int32, + torch.int64, + torch.float8_e4m3fn, + torch.float8_e5m2, +] + + +def reduction_init(reduction_type, dtype): + if dtype in DTYPE_LOWP_FP: + # Since load promotes all half-precision inputs to float, the initial + # constant for reduction must be promoted as well + dtype = torch.float32 + if reduction_type in ("xor_sum", "sum", "any"): + return 0 + if reduction_type == "prod": + return 1 + if reduction_type in ("max", "argmax", "min", "argmin"): + cdtype = DTYPE_TO_CPP[dtype] + if dtype == torch.bool and reduction_type in ("argmin", "argmax"): + cdtype = DTYPE_TO_CPP[torch.float] + min_var = ( + f"-std::numeric_limits<{cdtype}>::infinity()" + if is_float_dtype(dtype) + else f"std::numeric_limits<{cdtype}>::min()" + ) + max_var = ( + f"std::numeric_limits<{cdtype}>::infinity()" + if is_float_dtype(dtype) + else f"std::numeric_limits<{cdtype}>::max()" + ) + init_var = min_var if reduction_type in ("max", "argmax") else max_var + return ( + init_var + if reduction_type in ("max", "min") + else f"IndexValue<{cdtype}>{{0, {init_var}}}" + ) + if is_welford_reduction(reduction_type): + return f"Welford<{DTYPE_TO_CPP[dtype]}>()" + raise AssertionError(reduction_type) + + +def reduction_acc_type(reduction_type, dtype): + scalar_type = DTYPE_TO_CPP[DTYPE_TO_COMPUTATION_DTYPE[dtype]] + if is_welford_reduction(reduction_type): + return f"Welford<{scalar_type}>" + if reduction_type in ("argmin", "argmax"): + if dtype == torch.bool: + scalar_type = DTYPE_TO_CPP[torch.float] + return f"IndexValue<{scalar_type}>" + return scalar_type + + +def reduction_combine( + reduction_type, + var, + next_value, + helper_val=None, + index: sympy.Symbol | None = None, + src_dtype=None, +): + is_bool = src_dtype == torch.bool + if reduction_type == "sum": + if helper_val: + return f"cascade_sum_combine({next_value}, &{helper_val})" + else: + conjunction = "|" if is_bool else "+" + return f"{var} {conjunction} {next_value}" + if reduction_type == "prod": + return f"{var} * {next_value}" + if reduction_type == "xor_sum": + return f"{var} ^ {next_value}" + if reduction_type == "any": + return f"{var} || {next_value}" + if reduction_type in ("min", "max"): + return f"{reduction_type}_propagate_nan({var}, {next_value})" + if reduction_type == "welford_reduce": + if helper_val: + return f"welford_combine({var}, {next_value}, &{helper_val})" + else: + return f"welford_combine({var}, {next_value})" + if reduction_type == "welford_combine": + if isinstance(next_value, tuple): + mean, m2, weight = next_value + else: + mean, m2, weight = reduction_project(reduction_type, next_value) + return f"welford_combine({var}, {{{mean}, {m2}, {weight}}})" + if reduction_type in ("argmin", "argmax"): + if ( + hasattr(next_value, "dtype") + and next_value.dtype == torch.bool + and not next_value.is_vec + ): + if index is not None: + return f"{reduction_type}_combine({var}, static_cast({next_value}), {index})" + else: + return ( + f"{reduction_type}_combine({var}, static_cast({next_value}))" + ) + if index is not None: + return f"{reduction_type}_combine({var}, {next_value}, {index})" + else: + return f"{reduction_type}_combine({var}, {next_value})" + raise AssertionError(reduction_type) + + +def reduction_project(reduction_type, acc): + if is_welford_reduction(reduction_type): + return f"{acc}.mean", f"{acc}.m2", f"{acc}.weight" + elif reduction_type in ("argmin", "argmax"): + return f"{acc}.index" + return acc + + +def move_code_under_inner_loop( + code: IndentedBuffer, + iter_var: sympy.Expr, + new_iter_var: str, + loop_start: sympy.Expr, + loop_end: sympy.Expr, +) -> BracesBuffer: + r""" + f(iter_var) is transformed to f(new_iter_var) under the inner loop + \/ + for (new_iter_var = loop_start; new_iter_var < loop_end; new_iter_var++) { + f(new_iter_var) + } + Please be careful while using this function, + as the variable defined in f(iter_var) will be invalid outside the for loop. + For example: + auto tmp0 = in_ptr[x0]; -> + for (new_x0 = start; new_x0 < end; new_x0++){ + auto tmp0 = in_ptr[new_x0]; + } + The tmp0 is invalid outside the loop. + """ + transformed_code = BracesBuffer() + with contextlib.ExitStack() as stack: + transformed_code.writeline( + f"for ({INDEX_TYPE} {new_iter_var} = {cexpr_index(loop_start)};" + + f"{new_iter_var} < {cexpr_index(loop_end)}; {new_iter_var}++)" + ) + stack.enter_context(transformed_code.indent()) + for _, line in enumerate(code._lines): + assert isinstance( + line, + ( + str, + DeferredLine, + ), + ) + deferred_name = None + if isinstance(line, DeferredLine): + deferred_name = line.name + line = line.line + new_line = re.sub(r"\b" + f"{iter_var}" + r"\b", f"{new_iter_var}", line) + if deferred_name: + new_line = DeferredLine(deferred_name, new_line) # type: ignore[assignment] + transformed_code.writeline(new_line) + return transformed_code + + +def reduction_prefix_array( + acc_var: str | CSEVariable, + acc_type: str, + reduction_type: str, + dtype: torch.dtype, + len: str | int, + init_fn, +): + """ + MSVC don't support dynamic array(VLA). So we use std::unique_ptr here. + Ref: https://stackoverflow.com/questions/56555406/creating-dynamic-sized-array-using-msvc-c-compiler + MSVC is the only one compiler without VLA. support. Since MSVC can't get good performance here. + We just use unique_ptr make it works on MSVC. + For other compilers, we continue to use VLA to get best performance. + """ + code_buffer = IndentedBuffer() + acc_decl = ( + f"auto {acc_var}_arr = std::make_unique<{acc_type}[]>({len});" + if cpp_builder.is_msvc_cl() + else f"{acc_type} {acc_var}_arr[{len}];" + ) + code_buffer.writeline(f"{acc_decl}") + code_buffer.writelines( + [ + f"for (int i = 0; i < {len}; i++)", + "{", + f" {acc_var}_arr[i] = {init_fn(reduction_type, dtype)};", + "}", + ], + ) + return code_buffer + + +def replace_acc_name(buffer: IndentedBuffer, name: str, new_name: str): + for i, line in enumerate(buffer._lines): + assert isinstance( + line, + ( + str, + DeferredLine, + ), + ) + if isinstance(line, DeferredLine): + line.line = re.sub(r"\b" + f"{name}" + r"\b", f"{new_name}", line.line) + else: + buffer._lines[i] = re.sub(r"\b" + f"{name}" + r"\b", f"{new_name}", line) + + +def replace_cascade_sum_with_add(buffer: IndentedBuffer): + """ + Replaces `acc = cascade_sum_combine(value, ...)` with `acc = acc + value;` + """ + + pattern = r"(.*?)\s*=\s*cascade_sum_combine\(([^,]+),.*?\);" + for i, line in enumerate(buffer._lines): + assert isinstance( + line, + ( + str, + DeferredLine, + ), + ) + content = line.line if isinstance(line, DeferredLine) else line + match = re.search(pattern, content) + if match: + acc, value = match.groups() + new_content = re.sub(pattern, f"{acc} = {acc} + {value};", content) + if isinstance(line, DeferredLine): + line.line = new_content + else: + buffer._lines[i] = new_content + + +@functools.lru_cache +def stride_at(index: sympy.Expr, var: sympy.Symbol): + if not index.has(var): + # see test_torchinductor_dynamic_shapes.py::test_full_boolean_dynamic_shapes_cpu + # which has tmp0 = ops.index_expr(s0 >= 1024, torch.bool) and fails below calculation. + # in this case, there is no dependencies between index and var. + return sympy.S.Zero + replacement = {var: var + 1} + new_index = sympy_subs(index, replacement) # type: ignore[arg-type] + return sympy.simplify(new_index - index) + + +@functools.lru_cache +def simplify_index_in_vec_range(index: sympy.Expr, var: sympy.Expr, vec_length: int): + """ + Simplifies the index expression within the range of a vectorized loop. + Given a vectorized loop variable `var` in the range of a loop with `vec_length`, + this function transforms the `index` into an equivalent form. It handles + simplifications for cases where `var` can be expressed as `vec_length * a + b`, + where `b` ranges from 0 to `vec_length - 1`. The function reduces occurrences + of `FloorDiv` and `ModularIndexing` in the `index` with best-effort optimizations. + + NOTE: + The simplified index expression is intended for analysis purposes only, not + for code generation. It replaces `FloorDiv` and `ModularIndexing` with free variables + which are not dependent on the loop variable `var` in the vectorized range. Check + https://github.com/pytorch/pytorch/pull/117221#discussion_r1449746217 for more details. + + Examples: + 1. If `var` is `x3` and `vec_length` is 16, and `x3 = 16*a + b`, then + `FloorDiv(x3, div)` or `ModularIndexing(x3, div, mod)` becomes a free variable + when `div` is divisible by 16. + 2. `ModularIndexing(x3, 1, mod)` can be simplified to `x3 + c` where `c` is a free + variable when `mod` is divisible by 16. + """ + + div_freevar_id = 0 + mod_freevar_id = 0 + + def visit_indexing_div(divisor): + nonlocal div_freevar_id + result = FloorDiv(var, divisor) + if sympy.gcd(divisor, vec_length) == vec_length: + result = sympy.Symbol(f"{var}_div_c{div_freevar_id}") + div_freevar_id += 1 + return result + + def visit_modular_indexing(divisor, modulus): + nonlocal mod_freevar_id + result = ModularIndexing(var, divisor, modulus) + if sympy.gcd(divisor, vec_length) == vec_length: + result = sympy.Symbol(f"{var}_mod_c{mod_freevar_id}") + mod_freevar_id += 1 + elif divisor == 1 and sympy.gcd(modulus, vec_length) == vec_length: + result = var + sympy.Symbol(f"{var}_mod_c{mod_freevar_id}") + mod_freevar_id += 1 + return result + + original_index = index + + div = sympy.Wild("divisor", integer=True) + if index.has(FloorDiv): + index = index.replace(FloorDiv(var, div), visit_indexing_div) + + mod = sympy.Wild("modulus", integer=True) + if index.has(ModularIndexing): + index = index.replace(ModularIndexing(var, div, mod), visit_modular_indexing) + + index = sympy.simplify(index) + if index != original_index: + return simplify_index_in_vec_range(index, var, vec_length) + + return index + + +@functools.lru_cache +def stride_at_vec_range( + index: sympy.Expr, var: sympy.Symbol, vec_length: int | None = None +): + if vec_length: + index = simplify_index_in_vec_range(index, var, vec_length) + return stride_at(index, var) + + +@dataclasses.dataclass +class ParallelDepth: + """ + A class representing parallel depth. + Includes the starting depth of parallelism and the depth of parallelism. + """ + + parallel_depth: int + start_depth: int + + +class OuterLoopFusedSchedulerNode(FusedSchedulerNode): + @classmethod + def fuse( # type: ignore[override] + cls, node1: BaseSchedulerNode, node2: BaseSchedulerNode, outer_loop_fusion_depth + ): + assert node1.scheduler is node2.scheduler + assert all( + type(node) + in ( + OuterLoopFusedSchedulerNode, + SchedulerNode, + FusedSchedulerNode, + ) + for node in (node1, node2) + ) + if any(type(node) is OuterLoopFusedSchedulerNode for node in (node1, node2)): + return cls( + node1.scheduler, + # pyrefly: ignore [bad-argument-type] + ( + list(node1.get_outer_nodes()) + if type(node1) is OuterLoopFusedSchedulerNode + else [ + node1, + ] + ) + + ( + list(node2.get_outer_nodes()) + if type(node2) is OuterLoopFusedSchedulerNode + else [ + node2, + ] + ), + outer_loop_fusion_depth, + ) + else: + return cls(node1.scheduler, [node1, node2], outer_loop_fusion_depth) # type: ignore[list-item] + + def __init__( + self, + scheduler: "Scheduler", + outer_fused_nodes: list[FusedSchedulerNode | SchedulerNode], + outer_loop_fusion_depth, + ): + self.outer_fused_nodes: list[FusedSchedulerNode | SchedulerNode] = ( + outer_fused_nodes + ) + self.outer_loop_fusion_depth = outer_loop_fusion_depth + flatten_snodes = [] + for _node in self.outer_fused_nodes: + assert isinstance(_node, (SchedulerNode, FusedSchedulerNode)) + flatten_snodes.extend(list(_node.get_nodes())) + super().__init__(scheduler, flatten_snodes) # type: ignore[arg-type] + + def get_outer_nodes(self): + return self.outer_fused_nodes + + def check_outer_fusion_loop_level_attr( + self, cpp_kernel_proxy_list, outer_loop_fusion_depth + ): + # This function ensures that the same tiling split is applied at each loop level within the outer loop fusion depth. + # In the fusion stage, we only examine nodes with same vars and reduce. + # However, for nodes with same vars and reduce, the loops may still have different tile splits. + # For example (test_expr_vec_non_contiguous in test_cpu_repro.py): + # * buf0 tiling along the 2nd loop level, buf1 tiling along the 3rd loop level. + # If the check failed, we should fall back to standard loop codegen. + def _inner( + left_loop_nest: LoopNest, + right_loop_nest: LoopNest, + loop_fusion_depth: int, + current_checking_depth: int, + ) -> bool: + assert left_loop_nest.loops + assert right_loop_nest.loops + left_loop_level = left_loop_nest.loops[current_checking_depth] + right_loop_level = right_loop_nest.loops[current_checking_depth] + # Check if same loop level attr + outer_loops_attr_compare_list = [ + "var", + "size", + "offset", + "steps", + ] + if not ( + all( + getattr(left_loop_level, attr_compare) + == getattr(right_loop_level, attr_compare) + for attr_compare in outer_loops_attr_compare_list + ) + ): + return False + + assert loop_fusion_depth >= 1 + if (loop_fusion_depth := loop_fusion_depth - 1) > 0: + # Check next loop level attr + current_checking_depth = current_checking_depth + 1 + assert current_checking_depth < len(left_loop_nest.loops) + assert current_checking_depth < len(right_loop_nest.loops) + if not _inner( + left_loop_nest, + right_loop_nest, + loop_fusion_depth, + current_checking_depth, + ): + return False + + return True + + for idx in range(len(cpp_kernel_proxy_list) - 1): + left_loop_nest = cpp_kernel_proxy_list[idx].loop_nest + right_loop_nest = cpp_kernel_proxy_list[idx + 1].loop_nest + if not _inner( + left_loop_nest, + right_loop_nest, + outer_loop_fusion_depth, + 0, + ): + return False + + for cpp_kernel_proxy in cpp_kernel_proxy_list: + outer_ranges = functools.reduce( + operator.mul, + cpp_kernel_proxy.ranges[:outer_loop_fusion_depth], + ) + # When the range of the first inner loop is much larger than the range of + # all outer loops, do not fuse outer loop and fallback to standard loop codegen, + # so that the inner loops with larger range have a chance to be parallelized. + # We set a conservative threshold here: + # First inner loop range / all outer loops range > 300. + if ( + len(cpp_kernel_proxy.ranges) > outer_loop_fusion_depth + and isinstance(outer_ranges, sympy.Integer) + and isinstance( + cpp_kernel_proxy.ranges[outer_loop_fusion_depth], + sympy.Integer, + ) + and outer_ranges * 300 + < cpp_kernel_proxy.ranges[outer_loop_fusion_depth] + ): + return False + + return True + + def merge_outer_fusion_kernels( + self, + cpp_kernel_proxy_list, + ): + kernel_group = cpp_kernel_proxy_list[0].kernel_group + outer_loop_fused_kernel = OuterLoopFusedKernel(kernel_group) + outer_loop_fused_kernel.inner = [ + proxy.loop_nest.from_loop_level(self.outer_loop_fusion_depth) + for proxy in cpp_kernel_proxy_list + ] + outer_fused_proxy = cpp_kernel_proxy_list[0] + outer_fused_proxy.loop_nest.kernel = outer_loop_fused_kernel + outer_fused_proxy.loop_nest.loops = outer_fused_proxy.loop_nest.loops[ + : self.outer_loop_fusion_depth + ] + return outer_fused_proxy + + +class RecordOptimizationContext: + def __init__(self, func_name: str = ""): + self.func_name = func_name + self.current_node: torch.fx.Node | None = None + self.opt_ctx: OptimizationContext | None = None + + def __enter__(self): + assert V.interpreter + assert V.interpreter.current_node + + self.current_node = V.interpreter.current_node + assert self.current_node is not None + if OptimizationContext.key in self.current_node.meta: + self.opt_ctx = self.current_node.meta[OptimizationContext.key] + else: + self.opt_ctx = OptimizationContext() + assert self.opt_ctx is not None + self.opt_ctx.ops_name = self.func_name + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + assert self.current_node + assert self.opt_ctx + self.current_node.meta[OptimizationContext.key] = self.opt_ctx + + def get_opt_ctx(self): + return self.opt_ctx + + def get_fx_node(self): + assert self.current_node + return self.current_node + + +def decltype_promoted(*args): + assert not any(isinstance(arg, CppCSEVariable) and arg.is_vec for arg in args), ( + "Promotion of vector types is not supported" + ) + + if (dt := get_promote_dtype(args)) is not None: + return DTYPE_TO_CPP[dt] + else: + return f"decltype({args[0]})" + + +class CppOverrides(OpOverrides): + """Map element-wise ops to C++""" + + @staticmethod + def add(a, b): + return f"{decltype_promoted(a, b)}({a} + {b})" + + @staticmethod + def sub(a, b): + return f"{decltype_promoted(a, b)}({a} - {b})" + + @staticmethod + def mul(a, b): + return f"{decltype_promoted(a, b)}({a} * {b})" + + @staticmethod + def to_dtype(x, dtype, src_dtype=None, use_compute_types=True): + assert isinstance(x, CppCSEVariable) + if src_dtype is None: + src_dtype = x.dtype + expr = V.kernel.get_to_dtype_expr(x, dtype, src_dtype) + csevar = V.kernel.cse.generate(V.kernel.compute, expr) + csevar.update_on_args("to_dtype", (x, dtype), {"src_dtype": src_dtype}) + if dtype in DTYPE_LOWP_FP and src_dtype == torch.float: + """ + https://github.com/pytorch/pytorch/issues/115260 + For FusedSchedulerNode[node1, node2], the node2 loads what node1 stores and the buffer is + in low-precision floating point data type. When the output of node1 also serves as the output of the + kernel, the result of nodes would be different from the case when output of node1 is not the output + of the kernel (where we don't need to insert `to_dtype` for legalization). To address the problem, on + storing the lowp node1 output, we also add the inverse dtype conversion to high precision data type + to the cse cache. + + Example (pseudo code): + node1_output = ... + node1_output_lowp = to_dtype(node1_output, dtype=torch.bfloat16) + store(buf, node1_output_lowp) + node2_input_lowp = load(buf) + node2_input = to_dtype(node2_input_lowp, dtype=torch.float) + + Without cse cache trick: + node1_output = ... + node1_output_lowp = to_dtype(node1_output, dtype=torch.bfloat16) + store(buf, node1_output_lowp) + node2_input_lowp = node_output_lowp # hit store cache + node2_input = to_dtype(node2_input_lowp, dtype=torch.float) + + With cse cache trick: + node1_output = ... + node1_output_lowp = to_dtype(node1_output, dtype=torch.bfloat16) + # also add `to_dtype(node1_input_lowp, dtype=torch.float)` -> `node1_output` to cse cache + store(buf, node1_output_lowp) + node2_input_lowp = node_output_lowp # hit store cache + node2_input = node1_output # hit cse cache + """ + V.kernel.cache_dtype_convert(x, src_dtype, csevar, dtype) + return csevar + + @staticmethod + # pyrefly: ignore [bad-override] + def round_to_int(x, dtype, src_dtype=None, use_compute_types=True): + assert isinstance(x, CppCSEVariable) + if src_dtype is None: + src_dtype = x.dtype + expr = V.kernel.get_to_dtype_expr(x, dtype, src_dtype, rounding=True) + csevar = V.kernel.cse.generate(V.kernel.compute, expr) + csevar.update_on_args("round_to_int", (x, dtype), {"src_dtype": src_dtype}) + if dtype in DTYPE_LOWP_FP and src_dtype == torch.float: + V.kernel.cache_dtype_convert(x, src_dtype, csevar, dtype) + return csevar + + @staticmethod + def to_dtype_bitcast(x, dtype, src_dtype): + assert dtype in DTYPE_TO_CPP, f"{dtype} missing from {__name__}.DTYPE_TO_CPP" + return f"c10::bit_cast<{DTYPE_TO_CPP[dtype]}>({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def abs(x): + return f"std::abs({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def sin(x): + return f"std::sin({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def cos(x): + return f"std::cos({x})" + + @staticmethod + def neg(x): + return f"decltype({x})(-{x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def exp(x): + # return f"Sleef_expf_u10({x})" + return f"std::exp({x})" + + @staticmethod + def exp2(x): + return f"std::exp2({x})" + + @staticmethod + def expm1(x): + return f"std::expm1({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def erf(x): + return f"std::erf({x})" + + @staticmethod + def erfc(x): + return f"std::erfc({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def erfinv(x): + return f"calc_erfinv({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def sqrt(x): + return f"std::sqrt({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def rsqrt(x): + return f"1 / std::sqrt({x})" + + @staticmethod + def log1p(x): + bug = config.cpp.inject_log1p_bug_TESTING_ONLY + if bug == "accuracy": + return f"{x} + decltype({x})(1)" + elif bug is None: + return f"std::log1p({x})" + else: + raise AssertionError( + f"unrecognized config cpp.inject_log1p_bug_TESTING_ONLY = {bug!r}" + ) + + @staticmethod + # pyrefly: ignore [bad-override] + def tan(x): + return f"std::tan({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def tanh(x): + return f"std::tanh({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def signbit(x): + """ + On windows std::signbit only support float type. + Ref: https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/signbit?view=msvc-170 + """ + return ( + f"std::signbit(static_cast({x}))" + if _IS_WINDOWS + else f"std::signbit({x})" + ) + + @staticmethod + def pow(a, b): + return f"std::pow({a}, {b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def log(x): + return f"std::log({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def round(x): + return f"std::nearbyint({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def floor(x): + return f"std::floor({x})" + + @staticmethod + def floordiv(a, b): + # a and b are integer type + quot = f"{a} / {b}" + rem = f"{a} % {b}" + return f"(({a} < 0) != ({b} < 0) ? ({rem} != 0 ? {quot} - 1 : {quot}) : {quot})" + + @staticmethod + # pyrefly: ignore [bad-override] + def ceil(x): + return f"std::ceil({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def trunc(x): + return f"std::trunc({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def truncdiv(a, b): + # a and b are integer type + return f"{a} / {b}" + + @staticmethod + # pyrefly: ignore [bad-override] + def fmod(a, b): + return f"std::fmod({a}, {b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def isinf(x): + return f"std::isinf({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def isnan(x): + return f"std::isnan({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def lgamma(x): + return f"std::lgamma({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def acos(x): + return f"std::acos({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def acosh(x): + return f"std::acosh({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def cosh(x): + return f"std::cosh({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def sinh(x): + return f"std::sinh({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def asin(x): + return f"std::asin({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def asinh(x): + return f"std::asinh({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def atan2(x, y): + return f"std::atan2({x}, {y})" + + @staticmethod + # pyrefly: ignore [bad-override] + def atan(x): + return f"std::atan({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def atanh(x): + return f"std::atanh({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def copysign(x, y): + return f"std::copysign({x}, {y})" + + @staticmethod + # pyrefly: ignore [bad-override] + def frexp(x): + cache_keys = f"frexp({x})[0]", f"frexp({x})[1]" + if all(V.kernel.cse.try_get(cache_key) is not None for cache_key in cache_keys): + return tuple(V.kernel.cse.try_get(cache_key) for cache_key in cache_keys) + + code = BracesBuffer() + exponent = V.kernel.cse.newvar(dtype=torch.int32, shape=x.shape) + mantissa = V.kernel.cse.newvar(dtype=x.dtype, shape=x.shape) + code.writeline(f"int32_t {exponent};") + code.writeline(f"auto {mantissa} = std::frexp({x}, &{exponent});") + V.kernel.compute.splice(code) + cse_vars = (mantissa, exponent) + for cache_key, cse_var in zip(cache_keys, cse_vars): + V.kernel.cse.put(cache_key, cse_var) + return mantissa, exponent + + @staticmethod + # pyrefly: ignore [bad-override] + def hypot(x, y): + return f"std::hypot({x}, {y})" + + @staticmethod + def log10(x): + return f"std::log10({x})" + + @staticmethod + def log2(x): + return f"std::log2({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def ldexp(x, n): + return f"std::ldexp({x}, {n})" + + @staticmethod + # pyrefly: ignore [bad-override] + def nextafter(x, y): + return f"std::nextafter({x}, {y})" + + @staticmethod + def relu(x): + bug = config.cpp.inject_relu_bug_TESTING_ONLY + if bug == "compile_error": + return "compile error!" + elif bug == "runtime_error": + return f"{x}; throw 1" + elif bug == "accuracy": + return f"{x} + decltype({x})(1)" + elif bug is None: + return f"std::max({x}, decltype({x})(0))" + else: + raise AssertionError( + f"unrecognized config cpp.inject_relu_bug_TESTING_ONLY = {bug!r}" + ) + + @staticmethod + # pyrefly: ignore [bad-override] + def minimum(a, b): + return f"min_propagate_nan({a}, {b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def maximum(a, b): + return f"max_propagate_nan({a}, {b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def where(a, b, c): + return f"{a} ? {b} : {c}" + + @staticmethod + def mod(a, b): + return f"mod({a}, {b})" + + @staticmethod + def constant(val, dtype): + return value_to_cpp(val, DTYPE_TO_CPP[dtype]) + + @staticmethod + def index_expr(expr, dtype): + idx_str = cexpr(V.kernel.rename_indexing(expr)) + var = V.kernel.cse.generate( + V.kernel.compute, idx_str, bounds=get_bounds_index_expr(expr) + ) + return ops.to_dtype(var, dtype) + + @staticmethod + def masked(mask, body, other): + code = BracesBuffer() + + # Write masked operation into a lambda + body_var = V.kernel.cse.newvar() + code.writeline(f"auto {body_var} = [&]") + with V.kernel.swap_buffers(code), code.indent(): + result = body() + code.writeline(f"return {result};") + code.writeline(";") + V.kernel.compute.splice(code) + + # Use the lambda's return type as the type of other + other_code = value_to_cpp(other, f"decltype({body_var}())") + return f"{mask} ? {body_var}() : {other_code}" + + @staticmethod + # pyrefly: ignore [bad-override] + def logical_and(a, b): + return f"{a} && {b}" + + @staticmethod + def logical_not(a): + return f"!{a}" + + @staticmethod + # pyrefly: ignore [bad-override] + def logical_or(a, b): + return f"{a} || {b}" + + @staticmethod + # pyrefly: ignore [bad-override] + def logical_xor(a, b): + return f"{a} != {b}" + + @staticmethod + def bitwise_and(a, b): + return f"decltype({a})({a} & {b})" + + @staticmethod + def bitwise_not(a): + return f"decltype({a})(~{a})" + + @staticmethod + def bitwise_or(a, b): + return f"decltype({a})({a} | {b})" + + @staticmethod + def bitwise_xor(a, b): + return f"decltype({a})({a} ^ {b})" + + @staticmethod + def bitwise_left_shift(a, b): + code = BracesBuffer() + code.writeline("[&]()") + with code.indent(): + scalar_t = DTYPE_TO_CPP[a.dtype] + code.writeline( + f"constexpr decltype({b}) max_shift = sizeof({scalar_t}) * CHAR_BIT;" + ) + code.writeline( + f"if ((static_cast>({b}) < 0) || ({b} >= max_shift))" + ) + with code.indent(): + code.writeline(f"return decltype({a})(0);") + code.writeline( + f"return decltype({a})(static_cast>({a}) << {b});" + ) + code.writeline("()") + return code + + @staticmethod + def bitwise_right_shift(a, b): + code = BracesBuffer() + code.writeline("[&]()") + with code.indent(): + scalar_t = DTYPE_TO_CPP[a.dtype] + code.writeline( + f"constexpr decltype({b}) max_shift = sizeof({scalar_t}) * CHAR_BIT - std::is_signed_v<{scalar_t}>;" + ) + code.writeline( + f"if ((static_cast>({b}) < 0) || ({b} >= max_shift))" + ) + with code.indent(): + code.writeline(f"return decltype({a})({a} >> max_shift);") + code.writeline(f"return decltype({a})({a} >> {b});") + code.writeline("()") + return code + + @staticmethod + def rand(seed: sympy.Expr, offset: sympy.Expr): + return f"normalized_rand_cpu({seed}, {offset})" + + @staticmethod + def rand_eager( + seed: sympy.Expr, + base_offset: sympy.Expr, + threads_per_round: sympy.Expr, + tid: sympy.Expr, + vec: sympy.Expr, + ): + # NOTE: This is a codegen fallback used by the C++ backend for eager random. + # It is not intended to provide CPU parity; parity target is CUDA eager vs compiled. + # Keep this hook to satisfy codegen paths and CI. + return f"normalized_rand_cpu({seed}, {base_offset})" + + @staticmethod + def randn(seed: sympy.Expr, offset: sympy.Expr): + return f"randn_cpu({seed}, {offset})" + + @staticmethod + def randint64(seed: sympy.Expr, offset: sympy.Expr, low, high): + return f"randint64_cpu({seed}, {offset}, {low}, {high})" + + @staticmethod + def sigmoid(x): + return f"decltype({x})(1) / (decltype({x})(1) + std::exp(-{x}))" + + @staticmethod + # pyrefly: ignore [bad-override] + def sign(x): + code = BracesBuffer() + scalar_zero = f"decltype({x})(0)" + scalar_one = f"decltype({x})(1)" + code.writeline("[&]()") + with code.indent(): + code.writeline(f"auto left = {x} > 0 ? {scalar_one} : {scalar_zero};") + code.writeline(f"auto right = {x} < 0 ? {scalar_one} : {scalar_zero};") + code.writeline("return left - right;") + code.writeline("()") + return code + + def partial_accumulate( + self, + name: str, + reduction_type: str, + value: CSEVariable, + extra_meta: dict[str, Any], + ) -> None: + raise NotImplementedError + + +CppOverrides._initialize_pointwise_overrides("cpp") + + +class CppVecOverrides(CppOverrides): + """Map element-wise ops to aten vectorization C++""" + + def __new__(cls, *args, **kargs): + self = super().__new__(cls) + + def wrap(func): + # `CppVecKernel` generates both scalar ops and vector ops according to + # whether the inputs are scalars or vectors while all ops in `CppVecOverrides` + # (except for some ops explained below) assume the inputs are vectors. We wrap the ops in + # `CppVecOverrides` to broadcast scalar inputs to vectors if needed or fallback to + # `CppOverrides` when all inputs are scalars. + # + # Notes on ops handled separately in their own functions: + # `ops.masked`: + # needs recursive handling of masked body. + # `ops.index_expr`: + # needs to further analyze the dependency of the index expression on + # the tiling itervar. + def wrapper(*args, **kwargs): + scalars = [ + arg + for arg in args + if isinstance(arg, (int, sympy.Expr)) + or (isinstance(arg, CppCSEVariable) and not arg.is_vec) + ] + vectors = [ + arg + for arg in args + if isinstance(arg, CppCSEVariable) and arg.is_vec + ] + new_args = list(args) + if scalars and vectors: + new_args = [] + for arg in args: + if isinstance(arg, (int, sympy.Expr)): + if isinstance(arg, sympy.Expr) and not arg.is_number: + arg = ops.index_expr(arg, torch.int64) + else: + arg = ops.constant(arg, torch.int64) + arg = arg.value if isinstance(arg, OpsValue) else arg + new_args.append(arg) + + # DType Promotion + if vectors: + # We have saw several data type mismatch issues related with index_expr in + # the lowering phase of torch.int8. torch.int32, torch.int64. + # 1. int32 and int64 in test_torchinductor.py::test_max_pool2d_with_indices_backward3_cpu + # 2. int8 and int32 in test_torchinductor.py::test_max_pool2d5_cpu + # 3. int32 and fp32 in test_torchinductor_dynamic_shapes.py::test_avg_pool2d8_dynamic_shapes_cpu + if len(new_args) == 2: + new_args = promote_args(new_args) + elif func is CppVecOverrides.where: + new_args[1:] = promote_args(new_args[1:]) + + # Broadcast scalar args to vector + if scalars and vectors: + assert isinstance(V.kernel, CppVecKernel) + new_args = [ + ( + V.kernel.broadcast(new_arg) + if ( + isinstance(new_arg, CppCSEVariable) + and not new_arg.is_vec + and func + not in [ + CppVecOverrides.rand, + CppVecOverrides.randn, + CppVecOverrides.randint64, + ] + ) + else new_arg + ) + for new_arg in new_args + ] + + if vectors: + return func(*new_args, **kwargs) + else: + # fallback to scalar ops + scalar_ops = super(CppVecOverrides, self) + scalar_func = getattr(scalar_ops, func.__name__) + assert scalar_func is not None + return scalar_func(*args, **kwargs) + + return wrapper + + for name, method in vars(CppVecOverrides).items(): + if getattr(method, "__class__", None) is staticmethod and name not in [ + "masked", + "index_expr", + ]: + setattr(self, name, wrap(method.__func__)) + + return self + + @staticmethod + def add(a, b): + return f"{a} + {b}" + + @staticmethod + def sub(a, b): + return f"{a} - {b}" + + @staticmethod + def mul(a, b): + return f"{a} * {b}" + + @staticmethod + def truediv(a, b): + return f"{a} / {b}" + + @staticmethod + def abs(x): + return f"{x}.abs()" + + @staticmethod + def sin(x): + return f"{x}.sin()" + + @staticmethod + def cos(x): + return f"{x}.cos()" + + @staticmethod + def exp(x): + return f"{x}.exp()" + + @staticmethod + def exp2(x): + return f"{x}.exp2()" + + @staticmethod + def expm1(x): + # decompose for a better performance + vec_one = f"decltype({x})(1)" + return f"{x}.exp() - {vec_one}" + + @staticmethod + def erf(x): + return f"{x}.erf()" + + @staticmethod + def erfc(x): + return f"{x}.erfc()" + + @staticmethod + def erfinv(x): + return f"{x}.erfinv()" + + @staticmethod + def sqrt(x): + return f"{x}.sqrt()" + + @staticmethod + def eq(x, y): + assert isinstance(V.kernel, CppVecKernel) + assert isinstance(x, CppCSEVariable) + assert x.dtype is not None + return f"{V.kernel._get_mask_type(x.dtype)}({x} == {y})" + + @staticmethod + def ne(x, y): + assert isinstance(V.kernel, CppVecKernel) + assert isinstance(x, CppCSEVariable) + if x.dtype == torch.bool: + assert y.dtype == torch.bool + x_cast, y_cast = unify_mask_base_type(V.kernel.compute, (x, y)) + return f"{x_cast} != {y_cast}" + else: + assert x.dtype is not None + return f"{V.kernel._get_mask_type(x.dtype)}({x} != {y})" + + @staticmethod + def lt(x, y): + assert isinstance(V.kernel, CppVecKernel) + assert isinstance(x, CppCSEVariable) + assert x.dtype is not None + return f"{V.kernel._get_mask_type(x.dtype)}({x} < {y})" + + @staticmethod + def gt(x, y): + assert isinstance(V.kernel, CppVecKernel) + assert isinstance(x, CppCSEVariable) + assert x.dtype is not None + return f"{V.kernel._get_mask_type(x.dtype)}({x} > {y})" + + @staticmethod + def le(x, y): + assert isinstance(V.kernel, CppVecKernel) + assert isinstance(x, CppCSEVariable) + assert x.dtype is not None + return f"{V.kernel._get_mask_type(x.dtype)}({x} <= {y})" + + @staticmethod + def ge(x, y): + assert isinstance(V.kernel, CppVecKernel) + assert isinstance(x, CppCSEVariable) + assert x.dtype is not None + return f"{V.kernel._get_mask_type(x.dtype)}({x} >= {y})" + + @staticmethod + def and_(x, y): + return f"{x} & {y}" + + @staticmethod + def rsqrt(x): + return f"{x}.rsqrt()" + + @staticmethod + def pow(a, b): + return f"{a}.pow({b})" + + @staticmethod + def log(x): + return f"{x}.log()" + + @staticmethod + def round(x): + return f"{x}.round()" + + @staticmethod + def floor(x): + return f"{x}.floor()" + + @staticmethod + def ceil(x): + return f"{x}.ceil()" + + @staticmethod + def trunc(x): + return f"{x}.trunc()" + + @staticmethod + def fmod(a, b): + return f"{a}.fmod({b})" + + @staticmethod + def lgamma(x): + return f"{x}.lgamma()" + + @staticmethod + def logical_and(a, b): + a, b = may_unify_binary_op_mask_type(a, b) + return f"{a} & {b}" + + @staticmethod + def logical_not(a): + return f"~{a}" + + @staticmethod + def logical_or(a, b): + a, b = may_unify_binary_op_mask_type(a, b) + return f"{a} | {b}" + + @staticmethod + def logical_xor(a, b): + a, b = may_unify_binary_op_mask_type(a, b) + return f"{a} ^ {b}" + + @staticmethod + def bitwise_and(a, b): + a, b = may_unify_binary_op_mask_type(a, b) + return f"{a} & {b}" + + @staticmethod + def bitwise_not(a): + return f"~{a}" + + @staticmethod + def bitwise_or(a, b): + a, b = may_unify_binary_op_mask_type(a, b) + return f"{a} | {b}" + + @staticmethod + def bitwise_xor(a, b): + a, b = may_unify_binary_op_mask_type(a, b) + return f"{a} ^ {b}" + + @staticmethod + def bitwise_left_shift(a, b): + return f"{a} << {b}" + + @staticmethod + def bitwise_right_shift(a, b): + return f"{a} >> {b}" + + @staticmethod + def load_seed(name, offset): + assert isinstance(V.kernel, CppVecKernel) + return f"{V.kernel.load(name, offset)}" + + @staticmethod + def rand(seed, offset): + assert isinstance(V.kernel, CppVecKernel) + code = BracesBuffer() + rand_function = ( + f"result[offset_idx] = normalized_rand_cpu({seed}, offset[offset_idx]);" + ) + return codegen_rand(offset, code, rand_function) + + @staticmethod + def randn(seed, offset): + assert isinstance(V.kernel, CppVecKernel) + code = BracesBuffer() + rand_function = f"result[offset_idx] = randn_cpu({seed}, offset[offset_idx]);" + return codegen_rand(offset, code, rand_function) + + @staticmethod + def randint64(seed, offset, low, high): + assert isinstance(V.kernel, CppVecKernel) + code = BracesBuffer() + rand_function = f"result[offset_idx] = randint64_cpu({seed}, offset[offset_idx], {low}, {high});" + return codegen_rand(offset, code, rand_function, torch.int64) + + @staticmethod + def remainder(a, b): + assert a.dtype == b.dtype, ( + "remainder vec implementation expect the same inputs' dtype." + ) + return f"{a} - ({CppVecOverrides.floordiv(a, b)}) * {b}" + + @staticmethod + def tan(a): + return f"{a}.tan()" + + @staticmethod + def tanh(a): + if config.cpp.use_decompose_tanh: + vec_one = f"decltype({a})(1)" + vec_two = f"decltype({a})(2)" + vec_minus_two = f"decltype({a})(-2)" + return ( + f"{vec_two} / ({vec_one} + ({vec_minus_two} * {a}).exp()) - {vec_one}" + ) + else: + return f"{a}.tanh()" + + @staticmethod + def reciprocal(a): + return f"{a}.reciprocal()" + + @staticmethod + def atan(x): + return f"{x}.atan()" + + @staticmethod + def acos(x): + return f"{x}.acos()" + + @staticmethod + def asin(x): + return f"{x}.asin()" + + @staticmethod + def cosh(x): + return f"{x}.cosh()" + + @staticmethod + def sinh(x): + return f"{x}.sinh()" + + @staticmethod + def log10(x): + return f"{x}.log10()" + + @staticmethod + def log2(x): + return f"{x}.log2()" + + @staticmethod + def nextafter(x, y): + return f"{x}.nextafter({y})" + + @staticmethod + def copysign(a, b): + return f"{a}.copysign({b})" + + @staticmethod + def atan2(a, b): + return f"{a}.atan2({b})" + + @staticmethod + def hypot(a, b): + return f"{a}.hypot({b})" + + @staticmethod + def atanh(x): + # For real x, atanh(x) = 1/2 * log((1+x)/(1-x)) + vec_one = f"decltype({x})(1)" + vec_one_half = f"decltype({x})(0.5)" + return f"{vec_one_half} * (({vec_one} + {x})/({vec_one} - {x})).log()" + + @staticmethod + def asinh(x): + return f"{x}.asinh()" + + @staticmethod + def acosh(x): + return f"{x}.acosh()" + + @staticmethod + def relu(x): + bug = config.cpp.inject_relu_bug_TESTING_ONLY + if bug == "compile_error": + return "compile error!" + elif bug == "runtime_error": + return f"{x}; throw 1" + elif bug == "accuracy": + return f"{x} + decltype({x})(1)" + elif bug is None: + return f"at::vec::clamp_min({x}, decltype({x})(0))" + else: + raise AssertionError( + f"unrecognized config cpp.inject_relu_bug_TESTING_ONLY = {bug!r}" + ) + + # TODO: this seems to be dead + @staticmethod + def sigmoid(x): + return f"decltype({x})(1)/(decltype({x})(1) + {x}.neg().exp())" + + @staticmethod + def neg(x): + return f"{x}.neg()" + + @staticmethod + def floordiv(a, b): + if is_float_dtype(a.dtype): + assert a.dtype == b.dtype, ( + "div_floor_floating_vec implementation expect the same inputs' dtype." + ) + return f"div_floor_floating_vec({a}, {b})" + else: + assert all(is_integer_dtype(item.dtype) for item in [a, b]) + # a and b are integer type + _t = f"decltype({a})" + if V.kernel._get_raw_num_vectors(b.dtype) < 1: + # Doing blend to set the remaining bits of b to non-zero + b = f"{_t}::blend<{(1 << V.kernel.tiling_factor) - 1}>({_t}(1), {b})" + quot = f"{a} / {b}" + has_rem = f"({a} % {b} != {_t}(0))" + is_neg = f"(({a} < {_t}(0)) != ({b} < {_t}(0)))" + return f"{_t}::blendv({quot}, {quot} - {_t}(1), {has_rem} & {is_neg})" + + @staticmethod + def truncdiv(a, b): + # a and b are integer type + if V.kernel._get_raw_num_vectors(b.dtype) < 1: + # Doing blend to set the remaining bits of b to non-zero + _t = f"decltype({b})" + b = f"{_t}::blend<{(1 << V.kernel.tiling_factor) - 1}>({_t}(1), {b})" + return f"{a} / {b}" + + @staticmethod + def minimum(a, b): + if a.dtype == torch.bool: + assert b.dtype == torch.bool + a_cast, b_cast = unify_mask_base_type(V.kernel.compute, (a, b)) + return f"{a_cast} & {b_cast}" + else: + return f"at::vec::minimum({a}, {b})" + + @staticmethod + def maximum(a, b): + if a.dtype == torch.bool: + assert b.dtype == torch.bool + a_cast, b_cast = unify_mask_base_type(V.kernel.compute, (a, b)) + return f"{a_cast} | {b_cast}" + else: + return f"at::vec::maximum({a}, {b})" + + @staticmethod + def square(a): + return f"{a} * {a}" + + @staticmethod + def where(a, b, c): + assert isinstance(V.kernel, CppVecKernel) + if b.dtype == torch.bool: + assert c.dtype == torch.bool + blendv_a, blendv_b, blendv_c = unify_mask_base_type( + V.kernel.compute, (a, b, c) + ) + return f"decltype({blendv_b})::blendv({blendv_c}, {blendv_b}, {blendv_a})" + else: + return f"decltype({b})::blendv({c}, {b}, {V.kernel._get_mask_cast(a, b.dtype)})" + + @staticmethod + def sign(x): + code = BracesBuffer() + vec_zero = f"decltype({x})(0)" + vec_one = f"decltype({x})(1)" + blendv_l = f"decltype({x})::blendv({vec_zero}, {vec_one}, {vec_zero} < {x})" + blendv_r = f"decltype({x})::blendv({vec_zero}, {vec_one}, {x} < {vec_zero})" + code.writeline("[&]()") + with code.indent(): + code.writeline(f"auto left = {blendv_l};") + code.writeline(f"auto right = {blendv_r};") + code.writeline("return left - right;") + code.writeline("()") + return code + + @staticmethod + def to_dtype(x, dtype, src_dtype=None, use_compute_types=True): + assert dtype in [ + torch.bool, + torch.float64, + torch.float, + torch.bfloat16, + torch.float16, + torch.uint8, + torch.int8, + torch.int32, + torch.int64, + torch.float8_e4m3fn, + torch.float8_e5m2, + ], f"{__name__} does not support {dtype}" + assert isinstance(x, CppCSEVariable) + src_dtype = x.dtype + expr = V.kernel.get_to_dtype_expr(x, dtype, src_dtype) + csevar = V.kernel.cse.generate(V.kernel.compute, expr) + csevar.update_on_args("to_dtype", (x, dtype), {"src_dtype": src_dtype}) + if dtype in DTYPE_LOWP_FP and src_dtype == torch.float: + V.kernel.cache_dtype_convert(x, src_dtype, csevar, dtype) + return csevar + + @staticmethod + def round_to_int(x, dtype, src_dtype=None, use_compute_types=True): + assert dtype in [ + torch.uint8, + torch.int8, + torch.int32, + ], f"{__name__} does not support {dtype}" + assert isinstance(x, CppCSEVariable) + src_dtype = x.dtype + expr = V.kernel.get_to_dtype_expr(x, dtype, src_dtype, rounding=True) + csevar = V.kernel.cse.generate(V.kernel.compute, expr) + csevar.update_on_args("round_to_int", (x, dtype), {"src_dtype": src_dtype}) + if dtype in DTYPE_LOWP_FP and src_dtype == torch.float: + V.kernel.cache_dtype_convert(x, src_dtype, csevar, dtype) + return csevar + + @staticmethod + def log1p(x): + bug = config.cpp.inject_log1p_bug_TESTING_ONLY + if bug == "accuracy": + return f"{x} + decltype({x})(1)" + elif bug is None: + return f"{x}.log1p()" + else: + raise AssertionError( + f"unrecognized config cpp.inject_log1p_bug_TESTING_ONLY = {bug!r}" + ) + + @staticmethod + def masked(mask, body, other): + assert isinstance(V.kernel, CppVecKernel) + code = BracesBuffer() + var = V.kernel.cse.newvar() + with V.kernel.masked(mask) as new_mask: + code.writeline(f"auto {var} = [&]") + with V.kernel.swap_buffers(code), code.indent(): + result = body() + code.writeline(f"return {result};") + code.writeline(";") + V.kernel.compute.splice(code) + + dtype = result.dtype + body_code = f"{var}()" + + def maskify_or_vecify(code): + return ( + f"{V.kernel._get_mask_type()}::from({code})" + if dtype == torch.bool + else f"{V.kernel._get_vec_type(dtype)}({code})" + ) + + if result.is_vec: + body_code_vec = body_code + else: + body_code_vec = maskify_or_vecify(body_code) + other_code = value_to_cpp(other, DTYPE_TO_CPP[dtype]) + # loading bool as VecMask + other_code_vec = maskify_or_vecify(other_code) + assert isinstance(new_mask, CppCSEVariable), new_mask + if new_mask.is_vec: + code = BracesBuffer() + code.writeline("[&]") + with V.kernel.swap_buffers(code), code.indent(): + code.writeline(f"if ({new_mask}.all_zero())") + with code.indent(): + code.writeline(f"return {other_code_vec};") + code.writeline("else") + with code.indent(): + # Create cse variable to reuse kernel.overrides.where + body_vec_var = V.kernel.cse.generate( + V.kernel.compute, + body_code_vec, + ) + other_vec_var = V.kernel.cse.generate( + V.kernel.compute, + other_code_vec, + ) + assert isinstance(body_vec_var, CppCSEVariable), body_vec_var + assert isinstance(other_vec_var, CppCSEVariable), other_vec_var + body_vec_var.dtype = dtype + other_vec_var.dtype = dtype + overrides: type[CppOverrides | CppVecOverrides] = ( + # pyrefly: ignore [bad-assignment] + V.kernel.overrides + ) # type: ignore[has-type] + code.writeline( + f"return {overrides.where(new_mask, body_vec_var, other_vec_var)};" + ) + code.writeline("()") + csevar = V.kernel.cse.generate( + V.kernel.compute, + code, + ) + assert isinstance(csevar, CppCSEVariable) + csevar.is_vec = True + elif result.is_vec: + csevar = V.kernel.cse.generate( + V.kernel.compute, f"{mask} ? {body_code_vec} : {other_code_vec}" + ) + else: + csevar = V.kernel.cse.generate( + V.kernel.compute, f"{mask} ? {body_code} : {other_code}" + ) + # `result` is explicitly added to the args for correct propagation + # of relevant itervars and vectorization status. + csevar.update_on_args("masked", (mask, body, other, result), {}) + return csevar + + @staticmethod + def index_expr(expr, dtype): + assert isinstance(V.kernel, CppVecKernel) + index = V.kernel.rename_indexing(expr) + tiling_var = V.kernel.itervars[V.kernel.tiling_idx] + stride = V.kernel._try_get_const_stride(index, tiling_var) + if stride == 0: + return CppOverrides.index_expr(expr, dtype) + elif stride is not None: + idx = V.kernel.cse.generate( + V.kernel.compute, cexpr(index), bounds=get_bounds_index_expr(expr) + ) + value = ops.to_dtype(idx, dtype) + if isinstance(value, OpsValue): + value = value.value + csevar = V.kernel.arange(value, stride) + else: + csevar = V.kernel._load_or_store_non_contiguous( # type: ignore[assignment] + None, index, dtype, V.kernel.compute + ) + # pyrefly: ignore [missing-attribute] + csevar.update_on_args("index_expr", (expr, dtype), {}) + return csevar + + @staticmethod + def frexp(x): + cache_keys = f"frexp({x})[0]", f"frexp({x})[1]" + if all(V.kernel.cse.try_get(cache_key) is not None for cache_key in cache_keys): + return tuple(V.kernel.cse.try_get(cache_key) for cache_key in cache_keys) + + cdtype = DTYPE_TO_CPP[x.dtype] + size = V.kernel.tail_size if V.kernel.tail_size else V.kernel.tiling_factor + code = BracesBuffer() + exponent = V.kernel.cse.newvar(dtype=torch.int32) + mantissa = V.kernel.cse.newvar(dtype=x.dtype) + exponent.update_on_args("frexp", (x,), kwargs={}) + mantissa.update_on_args("frexp", (x,), kwargs={}) + n_vec = V.kernel._get_num_vectors(x.dtype) + mantissa_t = ( + f"at::vec::Vectorized<{cdtype}>" + if n_vec == 1 + else f"at::vec::VectorizedN<{cdtype}, {n_vec}>" + ) + code.writeline( + f"at::vec::Vectorized {exponent};" + if n_vec == 1 + else f"at::vec::VectorizedN {exponent};" + ) + code.writeline(f"{mantissa_t} {mantissa};") + code.writeline("[&]()") + with code.indent(): + code.writeline( + f"__at_align__ std::array<{cdtype}, {V.kernel.tiling_factor}> tmpbuf;" + ) + code.writeline(f"{x}.store(tmpbuf.data(), {cexpr_index(size)});") + code.writeline( + f"__at_align__ std::array tmpbuf_exponent;" + ) + code.writeline( + f"__at_align__ std::array<{cdtype}, {V.kernel.tiling_factor}> tmpbuf_mantissa;" + ) + code.writeline(f"for (int i = 0; i < {cexpr_index(size)}; i++)") + with code.indent(): + code.writeline( + "tmpbuf_mantissa[i] = std::frexp(tmpbuf[i], &tmpbuf_exponent[i]);" + ) + code.writeline( + f"{exponent} = at::vec::Vectorized::loadu(tmpbuf_exponent.data(), {cexpr_index(size)});" + if n_vec == 1 + else f"{exponent} = at::vec::VectorizedN::loadu(tmpbuf_exponent.data(), {cexpr_index(size)});" + ) + code.writeline( + f"{mantissa} = {mantissa_t}::loadu(tmpbuf_mantissa.data(), {cexpr_index(size)});" + ) + code.writeline("();") + V.kernel.compute.splice(code) + cse_vars = (mantissa, exponent) + for cache_key, cse_var in zip(cache_keys, cse_vars): + V.kernel.cse.put(cache_key, cse_var) + return mantissa, exponent + + @classmethod + def _scalarize(cls, scalar_func): + def inner(*args, **kwargs): + assert not kwargs + kernel = V.kernel + assert isinstance(kernel, CppVecKernel) + code = BracesBuffer() + code.writeline("[&]()") + vec_dtype = args[0].dtype + n_vec = kernel._get_num_vectors(vec_dtype) + size = kernel.tail_size if kernel.tail_size else kernel.tiling_factor + scalar_args = [] + cdtype = DTYPE_TO_CPP[vec_dtype] + output_mask = scalar_func.__name__ in ( + "isinf", + "isnan", + "signbit", + ) + octype = "bool" if output_mask else cdtype + octype = ( + DTYPE_TO_CPP[args[-2]] + if (scalar_func.__name__ == "to_dtype_bitcast") + else octype + ) + with code.indent(): + for argidx, arg in enumerate(args): + if isinstance(arg, CppCSEVariable): + assert arg.is_vec + assert arg.dtype == vec_dtype + code.writeline( + f"__at_align__ std::array<{cdtype}, {kernel.tiling_factor}> tmpbuf{argidx};" + ) + code.writeline( + f"{arg}.store(tmpbuf{argidx}.data(), {cexpr_index(size)});" + ) + scalar_args.append(f"tmpbuf{argidx}[i]") + else: + scalar_args.append(arg) + code.writeline( + f"__at_align__ std::array<{octype}, {kernel.tiling_factor}> tmpbuf_out;" + ) + res = scalar_func(*scalar_args) + code.writeline(f"for (int i = 0; i < {cexpr_index(size)}; i++)") + with code.indent(): + code.writeline(f"tmpbuf_out[i] = {res};") + load_args = f"tmpbuf_out.data(), {cexpr_index(size)}" + if output_mask: + load_fn = f"at::vec::VecMask<{cdtype},{n_vec}>::from" + elif n_vec == 1: + load_fn = f"at::vec::Vectorized<{octype}>::loadu" + else: + load_fn = f" at::vec::VectorizedN<{octype}, {n_vec}>::loadu" + code.writeline(f"return {load_fn}({load_args});") + code.writeline("()") + return code + + return inner + + @classmethod + def _initialize_scalarize(cls): + vec_vars = vars(CppVecOverrides) + for name, method in vars(CppOverrides).items(): + if isinstance(method, staticmethod) and name not in vec_vars: + func = cls._scalarize(method.__func__) + func.__name__ = name + setattr(cls, name, staticmethod(func)) + + +CppVecOverrides._initialize_pointwise_overrides("cppvec") +CppVecOverrides._initialize_scalarize() + + +class CppTile2DOverrides(CppVecOverrides): + @staticmethod + def index_expr(expr, dtype): + assert isinstance(V.kernel, CppTile2DKernel) + expr = V.kernel.transform_indexing(expr) + return CppVecOverrides.index_expr(expr, dtype) + + +class CppKernel(Kernel): + """ + Base class for C++ kernel code generation in PyTorch Inductor. + This class is responsible for generating C++ code from the intermediate representation. + + Args: + args: Kernel arguments used for code generation + num_threads: Number of threads for parallel execution + """ + + overrides = CppOverrides # type: ignore[assignment] + sexpr = cexpr + newvar_prefix = "auto " + suffix = ";" + + def __init__(self, args, num_threads): + super().__init__(args) + # Indicate when this kernel is active, for example + # {x0, {24, 26}} -> this kernel is active when x0 >= 24 and x0 < 26 + self.active_ranges: dict[sympy.Expr, tuple[sympy.Expr, ...]] = {} + # Indicate this kernel will be moved under the inner for-loop + # See move_code_under_inner_loop + self.inner_itervars: list[sympy.Symbol] = [] + self.call_ranges: tuple[sympy.Expr, ...] | None = None + self.ranges: list[sympy.Expr] = [] + self.itervars: list[sympy.Symbol] = [] + self.reduction_depth = None + self.reduction_prefix = IndentedBuffer() + # We need this because when we run "reduction" nodes here, we lack + # "loop" information to decide whether we need a scalar init or an array init + # in the reduction prefix. Meanwhile, we have other information like + # reduction types and dtype to generate the reduction prefix. We record the information + # with a callable lambda function, and when we have enough information to finalize + # the reduction prefix, we can invoke the functions here with additional information. + self.reduction_prefix_generators: list[Callable] = [] # type: ignore[type-arg] + self.reduction_suffix = IndentedBuffer() + self.parallel_reduction_prefix = IndentedBuffer() + self.parallel_reduction_suffix = IndentedBuffer() + self.local_reduction_init = IndentedBuffer() + self.local_reduction_stores = IndentedBuffer() + self.is_reduction = False + self.non_parallel_reduction_prefix = IndentedBuffer() + self.non_parallel_reduction_suffix = IndentedBuffer() + self.reduction_cse = CSE(self.newvar_prefix, self.suffix, name_prefix="tmp_acc") + self.welford_helper_cse = CSE( + self.newvar_prefix, self.suffix, name_prefix="welford_helper" + ) + self.cascade_helper_cse = CSE( + self.newvar_prefix, self.suffix, name_prefix="cascade_helper" + ) + self.preloads = IndentedBuffer() + self.poststores = IndentedBuffer() + self.num_threads = num_threads # num_threads the kernel specialized for + self.reduction_omp_dec: dict[tuple[str, str], str] = {} + self.reduction_var_names: list[str] = [] + + def _gen_parallel_reduction_buffers( + self, + acc, + acc_type, + reduction_type, + dtype, + reduction_combine_fn=reduction_combine, + reduction_init_fn=reduction_init, + ): + if config.cpp.dynamic_threads and not self.parallel_reduction_prefix: + self.parallel_reduction_prefix.writeline( + "int max_threads = omp_get_max_threads();" + ) + acc_local = f"{acc}_local" + num_threads = ( + "max_threads" if config.cpp.dynamic_threads else parallel_num_threads() + ) + acc_local_in_array = f"{acc}_arr[tid]" + self.local_reduction_init.writeline( + f"{acc_type} {acc_local} = {reduction_init_fn(reduction_type, dtype)};" + ) + self.parallel_reduction_prefix.splice( + reduction_prefix_array( + acc, + acc_type, + reduction_type, + dtype, + num_threads, + reduction_init_fn, + ) + ) + self.local_reduction_stores.writeline(f"{acc_local_in_array} = {acc_local};") + self.parallel_reduction_suffix.writelines( + [ + f"for (int tid = 0; tid < {num_threads}; tid++)", + "{", + f" {acc} = {reduction_combine_fn(reduction_type, acc, acc_local_in_array, src_dtype=dtype)};", + "}", + ], + ) + + def update_stores_with_parallel_reduction(self): + for var_name in self.reduction_var_names: + replace_acc_name(self.stores, var_name, f"{var_name}_local") + + def gen_body(self, code: BracesBuffer | None = None): + assert code is None + code = BracesBuffer() + with contextlib.ExitStack() as stack: + if hasattr(self, "codegen_inner_loops"): + code.splice(self.preloads) + self.codegen_inner_loops(code) + stack.enter_context(code.indent()) + code.splice(self.loads) + code.splice(self.compute) + code.splice(self.stores) + if hasattr(self, "codegen_inner_loops"): + code.splice(self.poststores) + + if self.inner_itervars: + for idx in self.inner_itervars: + start, end = self.active_ranges[idx] + code = move_code_under_inner_loop(code, idx, f"{idx}_tail", start, end) + return code + + @contextlib.contextmanager + def masked(self, mask): + """Context manager to add an additional mask to loads and stores.""" + prior = self._load_mask + if prior: + mask = ops.and_(mask, prior) + if isinstance(mask, OpsValue): + mask = mask.value + assert isinstance(mask, CppCSEVariable) + # see NOTE [dtype of CppCSEVariable] + # mask's dtype should be bool + mask.dtype = torch.bool + + self._load_mask = mask + try: + yield mask + finally: + self._load_mask = prior + + def scale_index_with_offset( + self, index: sympy.Expr, scale=1, itervar_idx=-1, offset=0 + ): + var = self.itervars[itervar_idx] + replacement = {var: var * scale + offset} + new_index = sympy_subs(index, replacement) + return new_index + + def index_to_str(self, index: sympy.Expr) -> str: + """ + Convert an index expr to a string that can be used in cpp code. + e.g. a sympy expression "s2" may actually appear as "ks1" in the cpp kernel. + """ + return cexpr(self.rename_indexing(index)) + + def index_indirect_depends_on(self, index: sympy.Expr, itervar: sympy.Symbol): + """ + Check if an index has free symbol CppCSEVariable that depends on `itervar`. + """ + return any( + self.cse.varname_map[s.name].depends_on(itervar) # type: ignore[attr-defined] + for s in index.free_symbols + if s.name in self.cse.varname_map # type: ignore[attr-defined] + and isinstance(self.cse.varname_map[s.name], CppCSEVariable) # type: ignore[attr-defined] + ) + + def index_depends_on(self, index: sympy.Expr, itervar: sympy.Symbol): + return itervar in index.free_symbols or self.index_indirect_depends_on( + index, itervar + ) + + def var_ranges(self): + return dict(zip(self.itervars, self.ranges)) + + def check_bounds( + self, + expr: sympy.Expr, + size: sympy.Expr, + lower: bool, + upper: bool, + ): + if not (lower or upper): + return + + indirect = free_symbol_is_type(expr, SymT.TMP) + if indirect: + # indexing in compute + csevar = ops.index_expr(expr, torch.int64).value + buffer = V.kernel.compute + else: + # indexing in loads + prior_compute = V.kernel.compute + try: + V.kernel.compute = self.loads + csevar = ops.index_expr(expr, torch.int64).value + finally: + V.kernel.compute = prior_compute + buffer = self.loads + + size_str = V.kernel.sexpr(self.rename_indexing(size)) if upper else None + + line = self.indirect_assert( + csevar, "0" if lower else None, size_str, self._load_mask + ) + self.cse.generate(buffer, line, assignment=False) + + def load(self, name: str, index: sympy.Expr): + var = self.args.input(name) + index = self.rename_indexing(index) + line = f"{var}[{cexpr_index(index)}]" + csevar = self.cse.generate(self.loads, line, dtype=V.graph.get_dtype(name)) + csevar.update_on_args("load", (self, name, index), {}) + return csevar + + def store(self, name, index, value, mode=None): + assert "buf" in name + var = self.args.output(name) + index = self.rename_indexing(index) + if mode is None: + line = f"{var}[{cexpr_index(index)}] = {value};" + elif mode == "atomic_add": + if not config.cpp.dynamic_threads and self.num_threads == 1: + line = f"{var}[{cexpr_index(index)}] += {value};" + else: + dtype = V.graph.get_dtype(name) + # mirroring static_cast(...) in load: + value = f"static_cast<{DTYPE_TO_CPP[dtype]}>({value})" + line = f"atomic_add(&{var}[{cexpr_index(index)}], {value});" + else: + raise NotImplementedError(f"store mode={mode}") + self.stores.writeline(DeferredLine(name, line)) + + def device_assert_async(self, cond, msg): + self.compute.writeline( + f'({cond} ? 0 : (throw std::runtime_error("{msg}"), 0));' + ) + + def _gen_reduction_prefix( + self, + acc: CSEVariable | str, + acc_type: str, + rtype: str, + dtype: torch.dtype, + init_fn, + ): + # Generate reduction prefix + # If size is None, we will define and initialize a single reduction variable + # => float tmp_acc0 = 0; + # Otherwise, we will define and initialize a reduction array + # => float tmp_acc0_arr[size]; + # => for (int i = 0; i < size; i++) tmp_acc0_arr[i] = 0; + def inner(size: int | None = None): + if size is None: + return f"{acc_type} {acc} = {init_fn(rtype, dtype)};" + else: + return reduction_prefix_array( + acc, + acc_type, + rtype, + dtype, + size, + init_fn, + ) + + return inner + + def finalize_reduction_prefix(self, size: int | None = None): + for gen_fn in self.reduction_prefix_generators: + self.reduction_prefix.splice(gen_fn(size)) + + def need_use_acc_helper(self, reduction_type, dtype, use_scalar): + # Check if we need accumulate helper for the reduction operation. + # using accumulate helper generates the necessary code to improve precision for + # sum and welford + # Note: using helper has non-negligible impact on performance + + if reduction_type == "welford_reduce": + return True + + # TODO add supports for more data types when needed + if reduction_type == "sum" and dtype == torch.float: + assert self.call_ranges is not None + reduction_size = functools.reduce( + operator.mul, self.call_ranges[self.reduction_depth :] + ) + + # chunk size to balance accuracy and performance + chunk_size = 4096 + + return V.graph.sizevars.guard_or_false(sympy.Gt(reduction_size, chunk_size)) + return False + + def _acc_helper_init( + self, + reduction_type, + helper_val, + helper_range, + dtype, + num_threads=None, + use_scalar=False, + ): + num_range_thread = ( + CeilDiv(helper_range, num_threads) if num_threads else helper_range + ) + num_range_thread_expr = cexpr_index(num_range_thread) + assert reduction_type in ["welford_reduce", "sum"] + chunk_size = 4096 + num_chunks = CeilDiv(num_range_thread, chunk_size) + helper_type = ( + "WelfordHelper" + if reduction_type == "welford_reduce" + else "CascadeSumHelper" + ) + if use_scalar: + h_type = DTYPE_TO_CPP[dtype] + else: + h_type = ( + self._get_vec_type(dtype) + if hasattr(self, "_get_vec_type") + else DTYPE_TO_CPP[dtype] + ) + helper_init_line = ( + f"{helper_type}<{h_type}, {chunk_size}> {helper_val}" + f"(" + f"{num_range_thread_expr}" + f");" + ) + if reduction_type == "sum": + return helper_init_line + if isinstance(num_chunks, sympy.Integer) and num_chunks <= 1: + # When the number of chunks <= 1, there is no need to use cascade summation to improve + # reduction accuracy. We can initialize a static WelfordHelper to improve performance. + return f"static {helper_init_line}" + else: + return helper_init_line + + def _use_acc_helper( + self, reduction_type, acc, helper_val, helper_range, dtype, use_scalar=False + ): + num_threads = ( + "max_threads" if config.cpp.dynamic_threads else parallel_num_threads() + ) + self.non_parallel_reduction_prefix.writeline( + self._acc_helper_init( + reduction_type, helper_val, helper_range, dtype, None, use_scalar + ) + ) + self.local_reduction_init.writeline( + self._acc_helper_init( + reduction_type, helper_val, helper_range, dtype, num_threads, use_scalar + ) + ) + result = acc if use_scalar else f"{acc}_vec" + if reduction_type == "welford_reduce": + self.non_parallel_reduction_suffix.writeline( + f"{result} = welford_combine({result}, &{helper_val});" + ) + self.local_reduction_stores.writeline( + f"{result}_local = welford_combine({result}_local, &{helper_val});" + ) + else: + self.non_parallel_reduction_suffix.writeline( + f"{result} = cascade_sum_final(&{helper_val});" + ) + self.local_reduction_stores.writeline( + f"{result}_local = cascade_sum_final(&{helper_val});" + ) + + def reduction(self, dtype, src_dtype, reduction_type, value): + argmax_or_argmin = reduction_type in ("argmax", "argmin") + reduction_key = src_dtype, reduction_type, value + if reduction_key in self.reduction_cse.reduction_cache: + return self.reduction_cse.reduction_cache[reduction_key] + + acc = self.reduction_cse.generate( + self.loads, f"reduction {reduction_key}", write=False + ) + self.reduction_var_names.append(f"{acc}") + self.is_reduction = True + init_dtype = src_dtype if argmax_or_argmin else dtype + acc_type = reduction_acc_type(reduction_type, init_dtype) + self.reduction_prefix_generators.append( + self._gen_reduction_prefix( + acc, acc_type, reduction_type, init_dtype, reduction_init + ) + ) + + if self.need_use_acc_helper(reduction_type, dtype, True): + # use cascade_helper for vec kernel + reduction_size = functools.reduce( + operator.mul, self.ranges[self.reduction_depth :] + ) + # use welford_helper/cascade_helper for vec kernel + if reduction_type == "welford_reduce": + helper_val = self.welford_helper_cse.generate( + self.compute, f"reduction {reduction_key}", write=False + ) + else: + helper_val = self.cascade_helper_cse.generate( + self.compute, f"reduction {reduction_key}", write=False + ) + # rename the helper variable to distinguish it from vectorized version + scalar_helper_val = f"scalar_{helper_val}" + self._use_acc_helper( + reduction_type, + acc, + scalar_helper_val, + reduction_size, + dtype, + use_scalar=True, + ) + self.stores.writeline( + f"{acc} = {reduction_combine(reduction_type, acc, value, scalar_helper_val)};" + ) + else: + assert self.reduction_depth is not None + index = self.itervars[self.reduction_depth] + for i in range(self.reduction_depth + 1, len(self.itervars)): + index = index * self.ranges[i] + self.itervars[i] + self.stores.writeline( + f"{acc} = {reduction_combine(reduction_type, acc, value, index=index)};" + ) + + self._gen_parallel_reduction_buffers(acc, acc_type, reduction_type, init_dtype) + result = reduction_project(reduction_type, acc) + self.reduction_cse.reduction_cache[reduction_key] = result + return result + + def store_reduction(self, name, index, value): + index = self.rename_indexing(index) + var = self.args.output(name) + self.reduction_suffix.writeline( + DeferredLine(name, f"{var}[{cexpr_index(index)}] = {value};") + ) + + def set_ranges(self, lengths, reduction_lengths): + if self.call_ranges: + assert self.call_ranges == tuple(lengths) + tuple(reduction_lengths), ( + f"{self.call_ranges} == {tuple(lengths)} + {tuple(reduction_lengths)}" + ) + assert self.reduction_depth == len(lengths) + else: + self.call_ranges = tuple(lengths) + tuple(reduction_lengths) + self.ranges = [self.rename_indexing(x) for x in self.call_ranges] + self.itervars = [ + sympy_index_symbol_with_prefix(SymT.XBLOCK, n) + for n in range(len(self.ranges)) + ] + # pyrefly: ignore [bad-assignment] + self.reduction_depth = len(lengths) + return ( + self.itervars[: self.reduction_depth], + self.itervars[self.reduction_depth :], + ) + + def size_hint(self): + assert self.call_ranges is not None + expr = sympy_product(self.call_ranges) + return V.graph.sizevars.optimization_hint(expr) + + def codegen_loops_impl(self, loop_nest, code, worksharing): + assert isinstance(self, CppKernelProxy) + threads = parallel_num_threads() + assert self.call_ranges is not None + if isinstance(loop_nest.kernel, OuterLoopFusedKernel): + par_depth = loop_nest.kernel.decide_parallel_depth( + loop_nest.max_parallel_depth(), threads + ) + else: + par_depth = self.decide_parallel_depth( + loop_nest.max_parallel_depth(), threads + ) + + is_reduction_loop = ( + loop_nest.loops is not None + and loop_nest.loops[par_depth.start_depth].is_reduction + ) + with contextlib.ExitStack() as stack: + if par_depth.parallel_depth: + if is_reduction_loop: + # need to close the worksharing scope to define reduction vars outside it + worksharing.close() + else: + worksharing.parallel(threads) + loop_nest.mark_parallel(par_depth) + elif threads > 1: + if worksharing.single(): + stack.enter_context(code.indent()) + + def gen_kernel(_loop_nest: LoopNest): + def is_parallel_reduction(): + assert _loop_nest.loops + root = _loop_nest.loops[par_depth.start_depth] + return root.is_reduction and root.parallel + + kernel = _loop_nest.get_kernel() + if isinstance(kernel, OuterLoopFusedKernel): + for _loop_nest in kernel.inner: + gen_loop_nest(_loop_nest) + else: + assert isinstance(kernel, CppKernelProxy) + if _loop_nest.loops is not None and is_parallel_reduction(): + kernel.update_stores_with_parallel_reduction() + with contextlib.ExitStack() as stack: + stack.enter_context(code.indent()) + kernel.gen_body(code) + + def get_reduction_prefix_suffix(kernel, parallel=False, is_suffix=False): + if is_suffix: + suffix = kernel.reduction_suffix + if parallel: + suffix = kernel.parallel_reduction_suffix + suffix + else: + suffix = kernel.non_parallel_reduction_suffix + suffix + return suffix + else: + prefix = kernel.reduction_prefix + if parallel: + prefix = prefix + kernel.parallel_reduction_prefix + else: + prefix = prefix + kernel.non_parallel_reduction_prefix + return prefix + + def gen_loop_with_reduction( + _loop_nest: LoopNest, depth: int = 0, in_reduction=False + ): + kernel = _loop_nest.get_kernel() + assert _loop_nest.loops + loop = _loop_nest.loops[depth] + with contextlib.ExitStack() as stack_outer: + if loop.is_reduction and not in_reduction: + reduction_prefix = get_reduction_prefix_suffix( + kernel, loop.parallel, is_suffix=False + ) + if reduction_prefix: + stack_outer.enter_context(code.indent()) + code.splice(reduction_prefix) + if is_reduction_loop and loop.parallel: + worksharing.parallel(threads) + if kernel.local_reduction_init: + assert kernel.local_reduction_stores + code.splice(kernel.local_reduction_init) + + gen_loop_at(_loop_nest, depth) + + if is_reduction_loop and loop.parallel: + if kernel.local_reduction_stores: + code.splice(kernel.local_reduction_stores) + worksharing.close() + if loop.is_reduction and not in_reduction: + code.splice( + get_reduction_prefix_suffix( + kernel, loop.parallel, is_suffix=True + ) + ) + + def gen_loop_at(_loop_nest: LoopNest, depth: int = 0): + with contextlib.ExitStack() as stack: + assert _loop_nest.loops + loop = _loop_nest.loops[depth] + loop_lines = loop.lines() + if loop_lines is None: + return + code.writelines(loop_lines) + stack.enter_context(code.indent()) + gen_loop_nest(_loop_nest, depth + 1, loop.is_reduction) + + def gen_loop_nest( + _loop_nest: LoopNest, + depth: int = 0, + in_reduction: bool = False, + ): + if _loop_nest.loops is None or depth == len(_loop_nest.loops): # type: ignore[arg-type] + gen_kernel(_loop_nest) + else: + gen_loop_with_reduction(_loop_nest, depth, in_reduction) + + stack.enter_context(code.indent()) + + if ( + isinstance(loop_nest.kernel, OuterLoopFusedKernel) + and isinstance(V.local_buffer_context, LocalBufferContext) + and V.local_buffer_context.local_buffers + ): + # Allocate local buffer + local_buffers = V.local_buffer_context.local_buffers + for local_buffer in local_buffers.values(): + # For dynamic size, rename s to ks + local_buf_size = sympy_product( + [ + self.rename_indexing(size_val) + for size_val in local_buffer.get_layout().size + ] + ) + local_buf_dtype = DTYPE_TO_CPP[local_buffer.get_layout().dtype] + allocate = f"std::make_unique<{local_buf_dtype} []>({cexpr(local_buf_size)})" + local_buffer_name = local_buffer.get_name() + code.splice( + f"std::unique_ptr<{local_buf_dtype} []> buf_{local_buffer_name} = {allocate};" + ) + code.splice( + f"{local_buf_dtype}* {local_buffer_name} = buf_{local_buffer_name}.get();" + ) + gen_loop_nest(loop_nest) + + def codegen_loops(self, code, worksharing): + loop_nest = LoopNest.build(self) + self.codegen_loops_impl(loop_nest, code, worksharing) + + @property + def assert_function(self) -> str: + if V.graph.aot_mode: + return "AOTI_TORCH_CHECK" + else: + return "TORCH_CHECK" + + def decide_parallel_depth(self, max_parallel_depth, threads): + assert self.call_ranges is not None + ranges = self.call_ranges[ + max_parallel_depth.start_depth : ( + max_parallel_depth.start_depth + max_parallel_depth.parallel_depth + ) + ] + seq = self.size_hint() + par = 1 + depth = 0 + for expr in ranges: + hint = V.graph.sizevars.optimization_hint(expr, fallback=8192) + if par >= 2 * threads or par == threads: + break + if seq // threads < config.cpp.min_chunk_size: + # not enough work + break + depth += 1 + par *= hint + seq /= hint + # if we assume thread number is dynamic, make sure we + # have at least one parallel scope and let OMP runtime + # to manage the serial vs. parallel. + if config.cpp.dynamic_threads and depth == 0 and len(ranges) > 0: + depth = 1 + return ParallelDepth( + parallel_depth=depth, start_depth=max_parallel_depth.start_depth + ) + + @contextlib.contextmanager + def write_to_suffix(self): + prior = (self.loads, self.compute, self.stores, self.cse) + self.loads = IndentedBuffer() + self.compute = IndentedBuffer() + self.stores = IndentedBuffer() + self.cse = self.cse.clone() + yield + self.reduction_suffix.splice(self.loads) + self.reduction_suffix.splice(self.compute) + self.reduction_suffix.splice(self.stores) + (self.loads, self.compute, self.stores, self.cse) = prior + + def create_cse_var(self, *args, **kwargs): + return CppCSEVariable(*args, **kwargs) + + def get_to_dtype_expr(self, src, dtype, src_dtype, rounding=False): + if ( + rounding + and dtype in [torch.int8, torch.uint8] + and src_dtype in [torch.float, torch.double] + ): + return f"c10::convert<{DTYPE_TO_CPP[dtype]}>(std::round({src}))" + return f"c10::convert<{DTYPE_TO_CPP[dtype]}>({src})" + + def cache_dtype_convert(self, dst, dst_dtype, src, src_dtype): + expr = self.get_to_dtype_expr(src, dst_dtype, src_dtype) + self.cse.put(expr, dst) + + def codegen_conditions( + self, + code: BracesBuffer, + prefix: str | None = None, + var: sympy.Symbol | None = None, + ): + if prefix is None: + prefix = "" + if not self.active_ranges: + return True + conditions = [] + + def gen(start, end, var): + if start == end: + return False + var_id = None + for i, _var in enumerate(self.itervars): + if var == _var: + var_id = i + break + if ( + type(self) is CppKernel + and var_id + and start == 0 + and end == self.ranges[var_id] + ): + end = 1 + # pyrefly: ignore [bad-argument-type] + conditions.append(f"{var} >= {cexpr_index(start)}") + # pyrefly: ignore [bad-argument-type] + conditions.append(f"{var} < {cexpr_index(end)}") + return True + + if var is not None: + assert var in self.active_ranges + start, end = self.active_ranges[var] + if not gen(start, end, var): + return False + else: + for _var, _range in self.active_ranges.items(): + start, end = _range + if not gen(start, end, _var): + return False + joined_conditions = " && ".join(conditions) + if joined_conditions: + code.writeline(f"if({prefix}({joined_conditions}))") + return True + else: + return False + + +class CppVecKernel(CppKernel): + overrides = CppVecOverrides # type: ignore[assignment] + + def __init__( + self, + args, + num_threads, + tiling_factor, + tiling_idx, + tail_size=None, + ): + super().__init__(args, num_threads) + self.vec_isa = cpu_vec_isa.pick_vec_isa() + assert self.vec_isa + assert tiling_factor > 0, "Expect pass in Non-Zero tiling_factor explicitly" + self.tiling_factor = tiling_factor + self.tiling_idx = tiling_idx + self.tail_size = tail_size + self.num_elems = tail_size if tail_size else tiling_factor + + def _try_get_const_stride(self, index: sympy.Expr, itervar: sympy.Symbol): + if self.index_indirect_depends_on(index, itervar): + return None + for indirect_var in ( + self.cse.varname_map[s.name] # type: ignore[attr-defined] + for s in index.free_symbols + if symbol_is_type(s, SymT.TMP) + ): + assert isinstance(indirect_var, CppCSEVariable) + if indirect_var.is_vec: + return None + stride = stride_at_vec_range(index, itervar, self.tiling_factor) + return stride if stride.is_number else None + + def _get_num_vectors(self, dtype: torch.dtype) -> int: + num_vectors = math.ceil( + self.tiling_factor * dtype.itemsize * 8 / self.vec_isa.bit_width() + ) + assert num_vectors >= 1 + return num_vectors + + def _get_raw_num_vectors(self, dtype: torch.dtype) -> float: + # This utility function is used to check if the vector lanes has been + # fully utilized. For example, uint8 will only use 1/4 of the vector lanes. + return self.tiling_factor * dtype.itemsize * 8 / self.vec_isa.bit_width() + + def _get_vec_type(self, dtype: torch.dtype) -> str: + num_vectors = self._get_num_vectors(dtype) + if num_vectors == 1: + return f"at::vec::Vectorized<{DTYPE_TO_CPP[dtype]}>" + else: + return f"at::vec::VectorizedN<{DTYPE_TO_CPP[dtype]},{num_vectors}>" + + def _get_mask_type(self, dtype: torch.dtype = torch.float) -> str: + if dtype == torch.bool: + return "" + num_vectors = self._get_num_vectors(dtype) + return f"at::vec::VecMask<{DTYPE_TO_CPP[dtype]},{num_vectors}>" + + def _get_mask_cast(self, mask: CppCSEVariable, dtype: torch.dtype) -> str: + assert mask.dtype == torch.bool, repr(mask) + num_vectors = self._get_num_vectors(dtype) + return f"{mask}.template cast<{DTYPE_TO_CPP[dtype]},{num_vectors}>()" + + def _get_vec_load_line( + self, + var: str, + index: sympy.Expr, + dtype: torch.dtype, + load_mask: CppCSEVariable | None = None, + ): + """ + Get a load line str that loads a vector from `var` at `index` of type `dtype`. + If `load_mask` is not None, we do a masked load accordingly. + Notes on the `dtype`: + 1. We always load `self.tiling_factor` number of elements regardless of the `dtype`. + It means we load half of the vector lanes for 16-bit data types and quarter of the + vector lanes for 8-bit data types. + 2. `torch.bool` and `torch.uint8` could mean masks and we load them as float mask vectors. + """ + cpp_type = DTYPE_TO_CPP[dtype] + num_vectors = self._get_num_vectors(dtype) + load_mask_str = None + if load_mask: + if not load_mask.is_vec: + # TODO: avoid hard-code torch.float + load_mask_str = f"{self._get_mask_type(torch.float)}::from({load_mask})" + else: + load_mask_str = f"{self._get_mask_cast(load_mask, torch.float)}" + loadbuf = f"{var} + {cexpr_index(index)}" if index != 0 else var + if dtype == torch.bool: + # TODO: should we consider load mask here? + line = f"{self._get_mask_type()}::from({loadbuf}, {cexpr_index(self.num_elems)})" + else: + line = ( + f"{load_mask_str}.template loadu<{cpp_type},{num_vectors}>({loadbuf})" + if load_mask_str + else f"{self._get_vec_type(dtype)}::loadu({loadbuf}, {cexpr_index(self.num_elems)})" + ) + return line + + def _load_or_store_non_contiguous( + self, + var: str | None, + index: sympy.Expr, + dtype: torch.dtype, + buffer: IndentedBuffer | None = None, + store_value: str | CppCSEVariable | None = None, + accu_store: bool = False, + ) -> CppCSEVariable | None: + """ + Load or store a vector in a non-contiguous way. The vector is initialized from an array that is + filled in an inner loop over the tiling factor. + :param var: buffer to load from or store to, i.e. `var[transformed(index)]`. If None, we load the index + as index expression, i.e. `transformed(index)`. + :param index: index into the `var` or the index expression by its own if `var` is None. + The `index` could contain indirect indexing or the tiling itervar. When used in + the inner loop, the index is transformed as follows: + 1. the index is linearized along the tiling dim. + 2. the indirect indexing vector variables are transformed into arrays over the tiling dim. + :param dtype: data type of `var` or `index` if `var` is None. + :param buffer: the code buffer to write the generated code to. If None, we write to `self.loads`. + :param store_value: the value to store. If None, we load the vector. + :param accu_store: whether accumulate the store_value to store_ptr. If True, a store_value should be provided + :return: a CppCSEVariable that represents the loaded vector or None if it is a store. + """ + assert not store_value or var is not None, "store var must be provided" + if accu_store: + assert store_value + if buffer is None: + buffer = self.loads + + def get_result_size(dtype: torch.dtype) -> int: + if dtype.itemsize < 4: + return self.num_elems * (4 // dtype.itemsize) + else: + return self.num_elems + + def get_tiling_size(dtype: torch.dtype) -> int: + if dtype.itemsize < 4: + return self.tiling_factor * (4 // dtype.itemsize) + else: + return self.tiling_factor + + def vec_to_array(vec_var: CppCSEVariable) -> CppCSEVariable: + assert vec_var.is_vec + code = BracesBuffer() + code.writeline("[&]") + with code.indent(): + vec_dtype = vec_var.dtype + assert vec_dtype is not None + if vec_dtype == torch.bool: + vec_dtype = torch.float + result_size = get_result_size(vec_dtype) + tiling_size = get_tiling_size(vec_dtype) + code.writeline( + f"__at_align__ std::array<{DTYPE_TO_CPP[vec_dtype]}, {tiling_size}> tmpbuf;" + ) + line = f"{vec_var}.store(tmpbuf.data(), {cexpr_index(result_size)});" + code.writeline(line) + code.writeline("return tmpbuf;") + code.writeline("()") + csevar = self.cse.generate(buffer, code) + assert isinstance(csevar, CppCSEVariable) + return csevar + + code = BracesBuffer() + code.writeline("[&]") + with code.indent(): + result_size = get_result_size(dtype) + tiling_size = get_tiling_size(dtype) + result_declare = ( + f"__at_align__ std::array<{DTYPE_TO_CPP[dtype]}, {tiling_size}> tmpbuf;" + ) + code.writeline(result_declare) + if store_value: + code.writeline( + f"{store_value}.store(tmpbuf.data(), {cexpr_index(result_size)});" + ) + itervar_inner = sympy_index_symbol( + f"{self.itervars[self.tiling_idx]}_inner" + ) + replacements = {} + for indirect_var in ( + self.cse.varname_map[s.name] # type: ignore[attr-defined] + for s in index.free_symbols + if symbol_is_type(s, SymT.TMP) + ): + assert isinstance(indirect_var, CppCSEVariable) + if indirect_var.is_vec: + array_var = vec_to_array(indirect_var) + replacements[indirect_var] = f"{array_var}[{itervar_inner}]" + index = self.scale_index_with_offset( + index, itervar_idx=self.tiling_idx, offset=itervar_inner + ) + load_mask = None + if self._load_mask is not None: + assert not store_value, "unexpected store with load mask" + assert isinstance(self._load_mask, CppCSEVariable), self._load_mask + if self._load_mask.is_vec: + load_mask = f"{self._load_mask}.is_masked({itervar_inner})" + else: + load_mask = f"{self._load_mask} != 0" + if cpp_builder.is_gcc(): + code.writeline(f"#pragma GCC unroll {self.tiling_factor}") + else: + code.writeline(f"#pragma unroll {self.tiling_factor}") + code.writeline( + f"for (long {itervar_inner} = 0; " + + f"{itervar_inner} < {cexpr_index(self.num_elems)}; " + + f"{itervar_inner}++)" + ) + with code.indent(), contextlib.ExitStack() as stack: + index_c = cexpr_index(index) + # pyrefly: ignore [bad-assignment] + for indirect_var in replacements: + index_c = re.sub( + r"\b" + f"{indirect_var}" + r"\b", + replacements[indirect_var], + index_c, + ) + rhs = f"{var}[{index_c}]" if var is not None else f"{index_c}" + if load_mask: + code.writeline(f"if ({load_mask})") + stack.enter_context(code.indent()) + if store_value: + conjunction = "+=" if accu_store else "=" + code.writeline(f"{rhs} {conjunction} tmpbuf[{itervar_inner}];") + else: + code.writeline(f"tmpbuf[{itervar_inner}] = {rhs};") + if not store_value: + load_line = self._get_vec_load_line("tmpbuf.data()", 0, dtype) # type: ignore[arg-type] + code.writeline(f"return {load_line};") + code.writeline("()") + if store_value: + code.writeline(";") + buffer.splice(code) + return None + else: + csevar = self.cse.generate(buffer, code, dtype=dtype) + assert isinstance(csevar, CppCSEVariable) + csevar.is_vec = True + return csevar + + def load(self, name: str, index: sympy.Expr): + var = self.args.input(name) + index = self.rename_indexing(index) + dtype = V.graph.get_dtype(name) + tiling_var = self.itervars[self.tiling_idx] + stride = self._try_get_const_stride(index, tiling_var) + if stride == 0: + # load scalar and lazily broadcast it on demand + return super().load(name, index) + elif stride == 1: + # load contiguously + line = self._get_vec_load_line(var, index, dtype, self._load_mask) # type: ignore[arg-type] + csevar = self.cse.generate(self.loads, line, dtype=dtype) # type: ignore[assignment] + else: + csevar = self._load_or_store_non_contiguous(var, index, dtype) # type: ignore[assignment] + assert isinstance(csevar, CppCSEVariable) + csevar.update_on_args("load", (self, name, index), {}) + csevar.is_vec = True + return csevar + + def _get_store_line( + self, + value: str | CppCSEVariable, + var: str, + index: sympy.Expr, + dtype: torch.dtype, + accu_store: bool = False, + ): + """ + Get a store line buffer that stores `value` into `var` at `index` of `dtype`. It handles + both contiguous and non-contiguous store cases. + :param value: Vectorized type templaterized on `dtype`. + :param var: buffer to store into. + :index: index into the `var`. + """ + # when value's type is str (e.g., welford reduction), caller should make sure + # it is a vector + assert isinstance(value, str) or ( + isinstance(value, CppCSEVariable) and value.is_vec + ), value + tiling_var = self.itervars[self.tiling_idx] + var_expr = f"{var} + {cexpr_index(index)}" + stride = self._try_get_const_stride(index, tiling_var) + code = IndentedBuffer() + if stride == 1: + if accu_store: + load = ( + f"{self._get_vec_type(dtype)}::loadu({var_expr})" + if dtype == torch.float and self.tail_size is None + else f"{self._get_vec_type(dtype)}::loadu({var_expr}, {cexpr_index(self.num_elems)})" + ) + value = f"({value} + {load})" + if dtype == torch.float and self.tail_size is None: + code.writeline(f"{value}.store({var_expr});") + else: + code.writeline( + f"{value}.store({var_expr}, {cexpr_index(self.num_elems)});" + ) + else: + self._load_or_store_non_contiguous( + var, index, dtype, buffer=code, store_value=value, accu_store=accu_store + ) + return code + + def store(self, name, index, value, mode=None): + assert "buf" in name + assert isinstance(value, CppCSEVariable), value + if not value.is_vec: + # this happens when we store a scalar into a vectorized buffer like "fill" + value = self.broadcast(value) + var = self.args.output(name) + index = self.rename_indexing(index) + dtype = V.graph.get_dtype(name) + if mode is None: + code = self._get_store_line(value, var, index, dtype) + self.stores.splice(code.map(lambda x: DeferredLine(name, x))) + elif mode == "atomic_add": + if not config.cpp.dynamic_threads and self.num_threads == 1: + code = self._get_store_line( + f"{value}", + var, + index, + dtype, + accu_store=True, + ) + self.stores.splice(code.map(lambda x: DeferredLine(name, x))) + else: + n_src = self._get_num_vectors(dtype) + n_idx = self._get_num_vectors(torch.int64) + cdtype = DTYPE_TO_CPP[dtype] + index = ops.index_expr(index, torch.int64).value + assert isinstance(index, CppCSEVariable) and index.is_vec + if self.tail_size: + line = f"atomic_add_vec<{cdtype}, {n_idx}, {n_src}>({var}, {index}, {value}, {cexpr_index(self.tail_size)});" + else: + line = f"atomic_add_vec<{cdtype}, {n_idx}, {n_src}>({var}, {index}, {value});" + self.stores.writeline(DeferredLine(name, line)) + else: + raise NotImplementedError(f"store mode={mode}") + + def reduction(self, dtype, src_dtype, reduction_type, value): + """ + Perform vectorized reduction operation. + + This method handles vectorized reduction for different reduction types. + It manages special cases for low-precision floating point types and + employs precision improvement techniques for certain reduction operations. + + Args: + dtype: The output data type for the reduction result + src_dtype: The source data type of the input value + reduction_type: Type of reduction operation (sum, min, max, etc.) + value: The input value to reduce + + Returns: + The result of the reduction operation + """ + # Note: For argmax and argmin on bool type, we always convert bool to float. + # Fix issue: https://github.com/pytorch/pytorch/issues/143568 + assert reduction_type in VECTORIZABLE_RTYPES + argmax_or_argmin = reduction_type in ("argmax", "argmin") + horizontal_reduction = self.tiling_idx >= self.reduction_depth + init_dtype = src_dtype if argmax_or_argmin else dtype + assert isinstance(value, CppCSEVariable), value + + if not value.is_vec: + value = self.broadcast(value) + + reduction_key = src_dtype, reduction_type, value + if reduction_key in self.reduction_cse.reduction_cache: + return self.reduction_cse.reduction_cache[reduction_key] + + vec_ns = "at::vec" + vec = f"{vec_ns}::Vectorized<{DTYPE_TO_CPP[dtype]}>" + acc_type = reduction_acc_type(reduction_type, init_dtype) + acc_type_vec = self.reduction_acc_type_vec(reduction_type, init_dtype) + + acc = self.reduction_cse.generate( + self.loads, f"reduction {reduction_key}", write=False + ) + assert isinstance(acc, CppCSEVariable) + acc_vec = f"{acc}_vec" + masked_acc = f"masked_{acc}" + masked_acc_vec = f"masked_{acc_vec}" + self.reduction_var_names += [f"{acc}", acc_vec, masked_acc_vec] + self.is_reduction = True + self.reduction_prefix_generators.append( + self._gen_reduction_prefix( + acc, acc_type, reduction_type, init_dtype, reduction_init + ) + ) + self.reduction_prefix_generators.append( + self._gen_reduction_prefix( + acc_vec, + acc_type_vec, + reduction_type, + init_dtype, + self.reduction_init_vec, + ) + ) + + use_acc_helper = self.need_use_acc_helper(reduction_type, dtype, False) + if use_acc_helper: + # use masked acc_vec for tail vec kernel + self.reduction_prefix_generators.append( + self._gen_reduction_prefix( + masked_acc_vec, + acc_type_vec, + reduction_type, + dtype, + self.reduction_init_vec, + ) + ) + + # use welford_helper/cascade_helper for vec kernel + assert self.reduction_depth is not None + reduction_size = functools.reduce( + operator.mul, self.ranges[self.reduction_depth :] + ) + if reduction_type == "welford_reduce": + helper_val = self.welford_helper_cse.generate( + self.compute, f"reduction {reduction_key}", write=False + ) + else: + helper_val = self.cascade_helper_cse.generate( + self.compute, f"reduction {reduction_key}", write=False + ) + masked_helper_val = f"masked_{helper_val}" + helper_vec_range = ( + ( + FloorDiv(reduction_size, self.ranges[self.tiling_idx]) + * FloorDiv(self.ranges[self.tiling_idx], self.tiling_factor) + if self.tiling_idx >= self.reduction_depth + else reduction_size + ) + if FloorDiv(self.ranges[self.tiling_idx], self.tiling_factor) + else sympy.Integer(0) + ) + masked_helper_vec_range = ( + ( + FloorDiv(reduction_size, self.ranges[self.tiling_idx]) + if self.tiling_idx >= self.reduction_depth + else reduction_size + ) + if self.ranges[self.tiling_idx] % self.tiling_factor + else sympy.Integer(0) + ) + # scalar helper for scalar welford_reduce/sum is also needed when vec kernel is included + scalar_helper_val = f"scalar_{helper_val}" + self._use_acc_helper( + reduction_type, + acc, + scalar_helper_val, + reduction_size, + dtype, + use_scalar=True, + ) + self._use_acc_helper( + reduction_type, acc, helper_val, helper_vec_range, dtype + ) + self._use_acc_helper( + reduction_type, + masked_acc, + masked_helper_val, + masked_helper_vec_range, + dtype, + ) + + # use masked acc_vec for tail vec kernel + acc_vec_ = masked_acc_vec if self.tail_size else acc_vec + helper_val_ = masked_helper_val if self.tail_size else helper_val + if reduction_type == "sum": + self.stores.writeline( + f"{acc_vec_} = {self.reduction_combine_vec(reduction_type, acc_vec_, value, helper_val_)};" + ) + else: + self.stores.writeline( + f"{acc_vec_} = {self.reduction_combine_vec(reduction_type, acc_vec_, value, helper_val_)};" + ) + else: + assert self.reduction_depth is not None + index = self.itervars[self.reduction_depth] + for i in range(self.reduction_depth + 1, len(self.itervars)): + index = index * self.ranges[i] + self.itervars[i] + kwargs = { + "next_value": value, + "index": index, + "horizontal_reduction": horizontal_reduction, + "src_dtype": src_dtype, + } + self.stores.writeline( + f"{acc_vec} = {self.reduction_combine_vec(reduction_type, acc_vec, **kwargs)};" + ) + self._gen_parallel_reduction_buffers( + acc_vec, + acc_type_vec, + reduction_type, + init_dtype, + reduction_combine_fn=self.reduction_combine_vec, + reduction_init_fn=self.reduction_init_vec, + ) + self._gen_parallel_reduction_buffers( + acc, + acc_type, + reduction_type, + init_dtype, + reduction_combine_fn=reduction_combine, + reduction_init_fn=reduction_init, + ) + if use_acc_helper: + # use masked acc_vec for tail vec kernel + self._gen_parallel_reduction_buffers( + masked_acc_vec, + acc_type_vec, + reduction_type, + dtype, + reduction_combine_fn=self.reduction_combine_vec, + reduction_init_fn=self.reduction_init_vec, + ) + tmpvar: str | CSEVariable + is_bool = dtype == torch.bool + if horizontal_reduction: + # Horizontal reduction + if is_welford_reduction(reduction_type): + assert self._get_num_vectors(dtype) in [ + 1, + 2, + ], "Welford reduction does not support VectorizedN (N>2)" + next_value = f"welford_vec_reduce_all({acc_vec})" + masked_next_value = f"welford_vec_reduce_all({masked_acc_vec})" + self.reduction_suffix.writeline( + f"{acc} = {reduction_combine(reduction_type, acc, masked_next_value)};" + ) + elif argmax_or_argmin: + next_value = f"{reduction_type}_vec_reduce_all({acc_vec})" + elif is_bool: + if reduction_type in ( + "any", + "sum", + "max", + ): + next_value = f"!{acc_vec}.all_zero()" + else: + assert reduction_type == "min" + next_value = f"{acc_vec}.all_masked()" + else: + reduce_all_body = ( + "{ return " + + self.reduction_combine_vec(reduction_type, "x", "y") + + "; }" + ) + is_bool = dtype == torch.bool + # we are using at::vec::VecMask for bool + vec_dtype = torch.float if is_bool else dtype + vec = f"at::vec::Vectorized<{DTYPE_TO_CPP[vec_dtype]}>" + vec_reduce_all_func = f"at::vec::vec_reduce_all<{DTYPE_TO_CPP[vec_dtype]}, {self._get_num_vectors(vec_dtype)}>" + result_vec = f"{acc_vec}" + if use_acc_helper: + assert reduction_type == "sum" + result_vec = f"{acc_vec} + {masked_acc_vec}" + next_value = f"{vec_reduce_all_func}([]({vec}& x, {vec}& y) {reduce_all_body}, {result_vec})" + + self.reduction_suffix.writeline( + f"{acc} = {reduction_combine(reduction_type, acc, next_value, src_dtype=src_dtype)};" + ) + tmpvar = acc + else: + tmpvar = acc_vec + if is_welford_reduction(reduction_type): + masked_tmpvar = f"masked_{tmpvar}" + self.reduction_suffix.writeline( + f"{tmpvar} = {reduction_combine(reduction_type, tmpvar, masked_tmpvar)};" + ) + elif use_acc_helper: + assert reduction_type == "sum" + masked_tmpvar = f"masked_{tmpvar}" + self.reduction_suffix.writeline( + f"{tmpvar} = {tmpvar} + {masked_tmpvar};" + ) + + result = reduction_project(reduction_type, tmpvar) + self.reduction_cse.reduction_cache[reduction_key] = result + return result + + def store_reduction(self, name, index, value): + index = self.rename_indexing(index) + var = self.args.output(name) + out_dtype = V.graph.get_dtype(name) + if out_dtype.is_floating_point and out_dtype != torch.double: + dtype = torch.float + else: + dtype = out_dtype + out_num_vectors = V.kernel._get_num_vectors(out_dtype) + src_num_vectors = V.kernel._get_num_vectors(dtype) + code = IndentedBuffer() + if self.tiling_idx >= self.reduction_depth: + # Horizontal reduction + code.writeline( + f"{var}[{cexpr_index(index)}] = static_cast<{DTYPE_TO_CPP[out_dtype]}>({value});" + ) + else: + # Vertical reduction + if out_dtype != dtype: + converted_value = ( + f"{DTYPE_TO_CPP[out_dtype].replace('::', '_')}_{value}" + ) + if out_dtype == torch.bool: + convert = f"{value}.template cast()" + else: + if src_num_vectors == out_num_vectors == 1: + convert = ( + f"at::vec::convert<{DTYPE_TO_CPP[out_dtype]}>({value})" + ) + else: + convert = ( + f"at::vec::convert<{DTYPE_TO_CPP[out_dtype]}," + f"{out_num_vectors},{DTYPE_TO_CPP[dtype]},{src_num_vectors}>({value})" + ) + code.writeline(f"auto {converted_value} = {convert};") + value = converted_value + code.splice(self._get_store_line(value, var, index, out_dtype)) + self.reduction_suffix.splice(code.map(lambda x: DeferredLine(name, x))) + + def broadcast(self, scalar_var: CppCSEVariable) -> CppCSEVariable: + assert not scalar_var.is_vec + if scalar_var.dtype == torch.bool: + vec_var = self.cse.generate( + self.compute, f"{self._get_mask_type()}::from({scalar_var.name})" + ) + else: + assert scalar_var.dtype is not None + vec_var = self.cse.generate( + self.compute, + f"{self._get_vec_type(scalar_var.dtype)}({scalar_var.name})", + ) + assert isinstance(vec_var, CppCSEVariable) + vec_var.dtype = scalar_var.dtype + vec_var.dependent_itervars = scalar_var.dependent_itervars + vec_var.is_vec = True + return vec_var + + def arange(self, index: CppCSEVariable, stride: sympy.Symbol) -> CppCSEVariable: + assert not index.is_vec + assert index.dtype is not None + csevar = self.cse.generate( + self.compute, + f"{self._get_vec_type(index.dtype)}::arange({index}, {stride})", + ) + assert isinstance(csevar, CppCSEVariable) + csevar.dtype = index.dtype + csevar.is_vec = True + return csevar + + def reduction_init_vec(self, reduction_type, dtype): + scalar_type = DTYPE_TO_COMPUTATION_DTYPE[dtype] + vec_type = self._get_vec_type(scalar_type) + + if is_welford_reduction(reduction_type): + return f"Welford<{vec_type}>()" + + if reduction_type in ("argmin", "argmax"): + # For bool argmin/argmax, we use float for computations + compute_dtype = torch.float if dtype == torch.bool else scalar_type + cdtype = DTYPE_TO_CPP[compute_dtype] + acc_type = self.reduction_acc_type_vec(reduction_type, dtype) + if reduction_type == "argmin": + val = ( + f"std::numeric_limits<{cdtype}>::infinity()" + if is_float_dtype(dtype) or dtype == torch.bool + else f"std::numeric_limits<{cdtype}>::max()" + ) + else: + val = ( + f"-std::numeric_limits<{cdtype}>::infinity()" + if is_float_dtype(dtype) or dtype == torch.bool + else f"std::numeric_limits<{cdtype}>::min()" + ) + return f"{acc_type}({val})" + + if reduction_type == "any": + return f"{self._get_mask_type()}::from(0)" + + scalar_init = reduction_init(reduction_type, dtype) + vec_init = f"{vec_type}({scalar_init})" + if dtype == torch.bool: + assert reduction_type in ("min", "max", "sum") + return f"{self._get_mask_type()}::from({scalar_init})" + return vec_init + + def reduction_acc_type_vec(self, reduction_type, dtype): + scalar_type = DTYPE_TO_COMPUTATION_DTYPE[dtype] + vec_type = self._get_vec_type(scalar_type) + if is_welford_reduction(reduction_type): + return f"Welford<{vec_type}>" + if reduction_type in ("argmin", "argmax"): + n_idx = self._get_num_vectors(torch.int64) + if dtype == torch.bool: + # For bool argmin/argmax, we use float for computations + # so n_src must be computed from float, not bool + n_src = self._get_num_vectors(torch.float) + return f"IndexValueVec<{DTYPE_TO_CPP[torch.float]}, {n_src}, {n_idx}>" + n_src = self._get_num_vectors(scalar_type) + return f"IndexValueVec<{DTYPE_TO_CPP[scalar_type]}, {n_src}, {n_idx}>" + if dtype == torch.bool: + assert reduction_type in ("min", "max", "any", "sum") + return f"{self._get_mask_type()}" + return vec_type + + def reduction_combine_vec( + self, + reduction_type, + var, + next_value, + helper_val=None, + index: sympy.Symbol | None = None, + horizontal_reduction: bool | None = None, + src_dtype: torch.dtype | None = torch.float32, + ): + is_bool = src_dtype == torch.bool + if reduction_type == "max": + if self.tail_size: + return f"max_masked_reduce({var}, {next_value}, {cexpr_index(self.tail_size)})" + else: + return ( + f"{var} | {next_value}" + if is_bool + else f"at::vec::maximum({var}, {next_value})" + ) + elif reduction_type == "min": + if self.tail_size: + return f"min_masked_reduce({var}, {next_value}, {cexpr_index(self.tail_size)})" + else: + return ( + f"{var} & {next_value}" + if is_bool + else f"at::vec::minimum({var}, {next_value})" + ) + elif reduction_type == "sum": + if helper_val: + if self.tail_size: + return f"cascade_sum_combine({next_value}, {cexpr_index(self.tail_size)}, &{helper_val})" + else: + return f"cascade_sum_combine({next_value}, &{helper_val})" + else: + if self.tail_size: + return f"sum_masked_reduce({var}, {next_value}, {cexpr_index(self.tail_size)})" + else: + conjunction = "|" if is_bool else "+" + return f"{var} {conjunction} {next_value}" + elif reduction_type == "prod": + if self.tail_size: + return f"prod_masked_reduce({var}, {next_value}, {cexpr_index(self.tail_size)})" + else: + return f"{var} * {next_value}" + elif reduction_type == "xor_sum": + if self.tail_size: + return f"xor_sum_masked_reduce({var}, {next_value}, {cexpr_index(self.tail_size)})" + else: + return f"{var} ^ {next_value}" + elif reduction_type == "welford_reduce": + if helper_val: + if self.tail_size: + return f"welford_combine({var}, {next_value}, {cexpr_index(self.tail_size)}, &{helper_val})" + else: + return f"welford_combine({var}, {next_value}, &{helper_val})" + else: + if self.tail_size: + return f"welford_combine({var}, {next_value}, {cexpr_index(self.tail_size)})" + else: + return f"welford_combine({var}, {next_value})" + elif reduction_type == "welford_combine": + if isinstance(next_value, tuple): + # When reading a value from Inductor IR we have a tuple of variable names + mean, m2, weight = next_value + else: + # When combining intermediate accumulators we have a Welford struct + mean, m2, weight = reduction_project(reduction_type, next_value) + if self.tail_size: + return f"welford_combine({var}, {{{mean}, {m2}, {weight}}}, {cexpr_index(self.tail_size)})" + else: + return f"welford_combine({var}, {{{mean}, {m2}, {weight}}})" + elif reduction_type in ("argmin", "argmax"): + assert src_dtype is not None + cdtype = DTYPE_TO_CPP[src_dtype] + compute_dtype = src_dtype + if src_dtype == torch.bool: + # For bool argmin/argmax, we use float for computations + cdtype = DTYPE_TO_CPP[torch.float] + compute_dtype = torch.float + # Convert bool VecMask to float vector for argmax_combine_vec + if isinstance(next_value, CppCSEVariable) and next_value.is_vec: + (next_value,) = unify_mask_base_type(self.compute, (next_value,)) + n_src = self._get_num_vectors(compute_dtype) + n_idx = self._get_num_vectors(torch.int64) + t_extra = "" + arg_extra = "" + if index is not None: + assert horizontal_reduction is not None + t_extra = f", {str(horizontal_reduction).lower()}" + arg_extra = f", {index}" + if self.tail_size: + return ( + f"{reduction_type}_combine_vec<{cdtype}, {n_src}, {n_idx}{t_extra}>" + f"({var}, {next_value}{arg_extra}, {cexpr_index(self.tail_size)})" + ) + else: + return f"{reduction_type}_combine_vec<{cdtype}, {n_src}, {n_idx}{t_extra}>({var}, {next_value}{arg_extra})" + elif reduction_type == "any": + if isinstance(next_value, CppCSEVariable): + assert next_value.dtype == torch.bool + (next_value,) = unify_mask_base_type(V.kernel.compute, (next_value,)) + if self.tail_size: + return f"any_masked_reduce({var}, {next_value}, {cexpr_index(self.tail_size)})" + else: + return f"{var} | {next_value}" + else: + raise NotImplementedError + + def indirect_assert(self, var, lower, upper, mask=None): + assert isinstance(var, CppCSEVariable) + assert var.dtype is not None + if not var.is_vec: + if isinstance(mask, CppCSEVariable) and mask.is_vec: + mask = f"({mask}).all_masked()" + return super().indirect_assert(var, lower, upper, mask) + lower_scalar = lower + upper_scalar = upper + if lower: + lower = f"{self._get_vec_type(var.dtype)}({lower})" + if upper: + upper = f"{self._get_vec_type(var.dtype)}({upper})" + if lower and upper: + cond = f"({lower} <= {var}) & ({var} < {upper})" + cond_print = f"{lower_scalar} <= {var} < {upper_scalar}" + elif lower: + cond = f"{lower} <= {var}" + cond_print = f"{lower_scalar} <= {var}" + else: + assert upper + cond = f"{var} < {upper}" + cond_print = f"{var} < {upper_scalar}" + cond = f"{self._get_mask_type(var.dtype)}({cond})" + if mask: + if not mask.is_vec: + mask = f"{self._get_mask_type(var.dtype)}::from({mask})" + # We need not check when the mask is False + cond = f"({cond}) | ~({mask})" + if self.tail_size: + cond = ( + f"{self._get_mask_type(var.dtype)}::set({self._get_mask_type(var.dtype)}::from(1)" + f", ({cond}), {cexpr_index(self.tail_size)})" + ) + cond = f"({cond}).all_masked()" + return f'{self.assert_function}({cond}, "index out of bounds: {cond_print}")' + + def get_to_dtype_expr(self, src, dtype, src_dtype, rounding=False): + assert isinstance(src, CppCSEVariable) + if not src.is_vec: + return super().get_to_dtype_expr(src, dtype, src_dtype, rounding) + src_cpp_type = DTYPE_TO_CPP[src_dtype] + src_num_vectors = self._get_num_vectors(src_dtype) + dst_cpp_type = DTYPE_TO_CPP[dtype] + dst_num_vectors = self._get_num_vectors(dtype) + expr = f"({src})" + if src_dtype != torch.bool and dtype == torch.bool: + expr = f"{self._get_mask_type(src_dtype)}::from<{src_cpp_type},{src_num_vectors}>({src})" + elif src_dtype == torch.bool and dtype != torch.bool: + expr = f"{src}.to<{dst_cpp_type},{dst_num_vectors}>()" + elif src_dtype != dtype: + expr = "" + if ( + rounding + and src_dtype in [torch.float, torch.double] + and dtype in [torch.int8, torch.uint8] + ): + expr = "at::vec::round_convert" + else: + expr = "at::vec::convert" + if src_num_vectors == dst_num_vectors == 1: + expr = expr + f"<{dst_cpp_type}>({src})" + else: + expr = ( + expr + + f"<{dst_cpp_type},{dst_num_vectors},{src_cpp_type},{src_num_vectors}>({src})" + ) + return expr + + +class CppTile2DKernel(CppVecKernel): + """ + A vector kernel that handles the 2d tiles with the tile size defined in `tiling_factor` on + the inner-most loop level and one of the outer loop level (`outer_tiling_idx`). When the data + tile is accessed in a contiguous way from the outer loop axis, a transposition is applied on the + tile to make the access contiguous from the inner-most loop axis. Then, the same vectorization + logic from its parent `CppVecKernel` is leveraged for load/store/compute. The transposed tile load + and store are generated into kernel.preloads and kernel.poststores buffers. + + The loop structure looks like below: + for ... + for i_outer ... + for ... + for inner_most ... + // generated by CppTile2DKernel + float tmp0[16*16]; at::vec::transpose_mxn<...>(tmp0, in_ptr0 + ..., ...); // into kernel.preloads + float tmp1[16*16]; // into kernel.preloads + for i_inner ... { // the kernel inner loop + vectorized loads/compute/stores (e.g., load tmp0, store tmp1) // into kernel.loads/compute/stores + } + at::vec::transpose_mxn(out_ptr0 + ..., tmp1, ...) // into kernel.poststores + for inner_most ... (tail) + // generated by CppVecKernel + ... + for i_outer ... (tail) + for ... + for ... + // generated by CppKernel + ... + """ + + overrides = CppTile2DOverrides # type: ignore[assignment] + + def __init__( + self, + args, + num_threads, + tiling_factor, + tiling_indices, + inner_tail_size=None, + outer_tail_size=None, + ): + super().__init__( + args, + num_threads, + tiling_factor, + tiling_indices[1], + inner_tail_size, + ) + self.tiling_indices = tiling_indices + self.inner_tail_size = inner_tail_size + self.outer_tail_size = outer_tail_size + self.inner_num_elems = inner_tail_size if inner_tail_size else tiling_factor + self.outer_num_elems = outer_tail_size if outer_tail_size else tiling_factor + self.inner_is_tiling_idx = True + + def inner_itervar(self): + return sympy_index_symbol(f"{self.itervars[self.outer_idx]}_inner") + + def need_vec_transpose(self, index): + outer_var = self.itervars[self.outer_idx] + inner_var = self.itervars[self.tiling_idx] + outer_stride = stride_at_vec_range(index, outer_var, self.tiling_factor) + inner_stride = stride_at_vec_range(index, inner_var, self.tiling_factor) + return ( + self._load_mask is None # TODO: support transposition with mask + and outer_stride == 1 + and index.has(inner_var) + and not inner_stride.has(inner_var) + and not inner_stride.has(outer_var) + ) + + def gen_transposed_tile_load_store( + self, name, var, index, is_store, store_mode=None + ): + # transposed tile load/store outside the kernel inner loop + dtype = V.graph.get_dtype(name) + factor = self.tiling_factor + src = f"{var} + {cexpr_index(index)}" + dst = "__place_holder__" + ld_src = f"{cexpr_index(stride_at_vec_range(index, self.itervars[self.tiling_idx], self.tiling_factor))}" + ld_dst = f"{cexpr_index(self.num_elems)}" + if is_store: + src, dst = dst, src + ld_src, ld_dst = ld_dst, ld_src + + need_define = True + if self.inner_is_tiling_idx ^ is_store: + M, N = self.inner_num_elems, self.outer_num_elems + else: + M, N = ( + self.outer_num_elems, + self.inner_num_elems, + ) + atomic_add = "true" if (is_store and (store_mode == "atomic_add")) else "false" + if (isinstance(M, sympy.Expr) and not M.is_number) or ( + isinstance(N, sympy.Expr) and not N.is_number + ): + load_or_store = ( + f"transpose_mxn<{DTYPE_TO_CPP[dtype]},{atomic_add}>" + f"({src}, {ld_src}, {dst}, {ld_dst}, {cexpr_index(M)}, {cexpr_index(N)});" + ) + else: + load_or_store = ( + f"transpose_mxn<{DTYPE_TO_CPP[dtype]},{cexpr_index(M)},{cexpr_index(N)},{atomic_add}>" + f"({src}, {ld_src}, {dst}, {ld_dst});" + ) + if is_store: + tile_var = self.cse.newvar() + elif not self.cse.contains(load_or_store): + tile_var = self.cse.generate(self.preloads, load_or_store, write=False) + else: + need_define = False + tile_var = self.cse.get(load_or_store) + + if need_define: + cpp_dtype = DTYPE_TO_CPP[dtype] + # tiling_factor might be smaller than the alignment of cpp_dtype, such as + # with a vector that only holds 4 elements due to NEON 128-bit vectors and + # cpp_dtype being a 64-bit integer. + alignas = f"alignas(std::max(std::size_t({factor}), alignof({cpp_dtype})))" + define_line = f"{alignas} {cpp_dtype} {tile_var}[{factor}*{factor}];" + self.preloads.writeline(define_line) + + load_or_store = load_or_store.replace("__place_holder__", str(tile_var)) + if is_store: + self.poststores.writeline(DeferredLine(name, load_or_store)) + else: + self.preloads.writeline(load_or_store) + + return tile_var + + def load(self, name: str, index: sympy.Expr): + var = self.args.input(name) + index = self.rename_indexing(index) + + inner = self.inner_itervar() + if self.need_vec_transpose(index): + tile_var = self.gen_transposed_tile_load_store( + name, var, index, is_store=False + ) + # vector load inside the kernel inner loop + loadbuf = f"{tile_var} + {cexpr_index(inner * self.num_elems)}" + dtype = V.graph.get_dtype(name) + line = self._get_vec_load_line(loadbuf, 0, dtype) # type: ignore[arg-type] + csevar = self.cse.generate(self.loads, line, dtype=dtype) + csevar.update_on_args("load", (self, name, index), {}) + assert isinstance(csevar, CppCSEVariable) + csevar.is_vec = True + return csevar + else: + new_index = self.transform_indexing(index) + return super().load(name, new_index) + + def store(self, name, index, value, mode=None): + assert "buf" in name + assert isinstance(value, CppCSEVariable), value + if not value.is_vec: + # this happens when we store a scalar into a vectorized buffer like "fill" + value = self.broadcast(value) + + var = self.args.output(name) + + inner = self.inner_itervar() + index = self.rename_indexing(index) + if self.need_vec_transpose(index): + tile_var = self.gen_transposed_tile_load_store( + name, var, index, is_store=True, store_mode=mode + ) + # vector store inside the kernel inner loop + storebuf = f"{tile_var} + {cexpr_index(inner * self.num_elems)}" + if self.tail_size or V.graph.get_dtype(name) in DTYPE_LOWP_FP + [ + torch.uint8, + torch.int8, + torch.float8_e4m3fn, + torch.float8_e5m2, + ]: + line = f"{value}.store({storebuf}, {cexpr_index(self.num_elems)});" + else: + line = f"{value}.store({storebuf});" + self.stores.writeline(DeferredLine(name, line)) + else: + new_index = self.transform_indexing(index) + super().store(name, new_index, value, mode) + + def codegen_inner_loops(self, code): + inner = self.inner_itervar() + if self.inner_is_tiling_idx: + code.writeline( + f"for (long {inner} = 0; {inner} < {cexpr_index(self.outer_num_elems)}; {inner}++)" + ) + else: + code.writeline( + f"for (long {inner} = 0; {inner} < {cexpr_index(self.inner_num_elems)}; {inner}++)" + ) + + def set_ranges(self, group, reduction_group): + vars = super().set_ranges(group, reduction_group) + # do vertical reduction as the tail loop + self.outer_idx, self.tiling_idx = ( + self.tiling_indices + if self.tiling_indices[1] < self.reduction_depth + else reversed(self.tiling_indices) + ) + if self.tiling_idx == self.tiling_indices[0]: + self.tail_size = self.outer_tail_size + self.num_elems = self.outer_num_elems + self.inner_is_tiling_idx = False + else: + self.tail_size = self.inner_tail_size + self.num_elems = self.inner_num_elems + self.inner_is_tiling_idx = True + return vars + + def transform_indexing(self, index: sympy.Expr) -> sympy.Expr: + return self.scale_index_with_offset( + index, + itervar_idx=self.outer_idx, + offset=self.inner_itervar(), + ) + + +def get_loop_body_lowp_fp(_body: LoopBody) -> tuple[torch.dtype | None, bool]: + """ + Returns the low precision data type (torch.float16/torch.bfloat16) contained in the nodes + and if all the nodes can codegen with this data type without converting to float. + Otherwise returns None and True. + """ + sub_blocks = [_body.root_block] + list(_body.subblocks.values()) + + _lowp_fp_type: torch.dtype | None = None + _use_fp32 = False + for sub_block in sub_blocks: + for _node in sub_block.graph.nodes: + if _node.op == "placeholder" or _node.target in ( + "get_index", + "index_expr", + ): + continue + + # Fast path if all operations can support bf16/fp16 without converting to fp32 + if _node.target not in [ + "load", + "store", + "abs", + "neg", + "output", + ]: + _use_fp32 = True + + if hasattr(_node, "meta") and _node.meta: + assert OptimizationContext.key in _node.meta + opt_ctx: OptimizationContext = _node.meta[OptimizationContext.key] + if not opt_ctx.dtype or opt_ctx.dtype not in DTYPE_LOWP_FP: + _use_fp32 = True + elif _lowp_fp_type is not None: + if _lowp_fp_type != opt_ctx.dtype: + warnings.warn("bf16 and fp16 are mixed in the scheduler node.") + else: + _lowp_fp_type = opt_ctx.dtype + else: + _use_fp32 = True + + return _lowp_fp_type, _use_fp32 + + +class TilingSelect: + """ + Implement the heuristic to select the tiling factors and tiling indices. + In the future, we can implement advanced heuristic in a subclass. + """ + + def select_tiling( + self, + fn_list, + var_sizes_list, + ) -> tuple[list[int], list[int]]: + # TODO(jgong5): support alternative tiling factors and data types + loop_bodies = _get_loop_body(fn_list) + all_dtypes = _get_dtype_from_loopbodies(loop_bodies) + assert all_dtypes + if any(dtype not in VECTORIZABLE_DTYPES for dtype in all_dtypes): + return [], [] + dtype = torch.float + _lowp_fp_dtype = get_loop_body_lowp_fp(loop_bodies[0])[0] + if _lowp_fp_dtype and all( + (get_loop_body_lowp_fp(loop_body)[0] == _lowp_fp_dtype) + for loop_body in loop_bodies[1:] + ): + dtype = _lowp_fp_dtype + + tiling_factor = cpu_vec_isa.pick_vec_isa().nelements(dtype=dtype) + tiling_indices = self._select_tiling_indices( + fn_list, var_sizes_list, tiling_factor + ) + + if tiling_indices: + group, reduction_group = max( + var_sizes_list, key=lambda sizes: len(sizes[1]) + ) + call_ranges = tuple(group) + tuple(reduction_group) + + if config.cpp.enable_tiling_heuristics: + + def _try_get_stride( + index, + itervars, + tiling_factor, + tiling_indices, + ): + itervar = itervars[tiling_indices[0]] + stride = stride_at_vec_range(index, itervar, tiling_factor) + return stride if stride.is_number else None + + def _update_negative_op_count( + node_name, non_contig_indexing_op_counter + ): + if node_name not in non_contig_indexing_op_counter: + non_contig_indexing_op_counter[node_name] = 1 + else: + non_contig_indexing_op_counter[node_name] += 1 + + def _is_valid_indices( + itervars, + tiling_indices, + ): + return ( + len(tiling_indices) == 1 + and len(itervars) > 0 + and ( + tiling_indices[0] + if tiling_indices[0] >= 0 + else tiling_indices[0] + len(itervars) + ) + < len(itervars) + ) + + itervars = [ + sympy_index_symbol_with_prefix(SymT.XBLOCK, n) + for n in range(len(call_ranges)) + ] + reduction_depth = len(group) + vars, reduction_vars = ( + itervars[:reduction_depth], + itervars[reduction_depth:], + ) + op_counter: dict[str, int] = {} + # ops may cause overhead with vectorization, like non-contiguous + # index_expr, load, store + non_contig_indexing_op_counter: dict[str, int] = {} + for _body in loop_bodies: + sub_blocks = [_body.root_block] + list(_body.subblocks.values()) + for sub_block in sub_blocks: + for _node in sub_block.graph.nodes: + if _node.target in ["index_expr", "load", "store"]: + # get the index and replace prefix from z to x + arg_idx = 1 if _node.target == "index_expr" else 2 + index = sub_block.body.indexing_from_args( + (vars, reduction_vars) + )[_node.args[arg_idx].args[0]] + if _is_valid_indices(itervars, tiling_indices): + stride = _try_get_stride( + index, itervars, tiling_factor, tiling_indices + ) + if ( + stride is None + if _node.target == "index_expr" + else stride not in [0, 1] + ): + _update_negative_op_count( + _node.target, non_contig_indexing_op_counter + ) + if isinstance(_node.target, str) and not ( + _node.target.startswith("masked_subblock") + or _node.target + in ["ops", "output", "constant", "get_index"] + ): + if _node.target not in op_counter: + op_counter[_node.target] = 1 + else: + op_counter[_node.target] += 1 + + op_num = sum(op_counter.values()) + non_contig_indexing_op_num = sum( + non_contig_indexing_op_counter.values() + ) + ratio_threshold = 0.12 + quantity_threshold = 35 + if non_contig_indexing_op_num >= quantity_threshold or ( + op_num > 0 + and non_contig_indexing_op_num / op_num >= ratio_threshold + ): + # Too many non-contiguous load/store/index_expr which hurts the + # vectorization performance. Disable vectorization when exceeding + # the thresholds. + return [], [] + + if ( + not reduction_group + and group + and len(tiling_indices) == 1 + and not has_free_symbols( + [ + group[tiling_indices[0]], + ] + ) + and group[tiling_indices[0]] < tiling_factor / 4 + and op_num < 10 + ): + # We found that when the number of elements in the inner loop range is + # relatively small(< tiling_factor / 4) and the number of operations is + # not large(< 10), vectorization is not efficient. + # And found that `#pragma GCC ivdep` has better performance than + # `#pragma omp simd simdlen(8)` for these cases. + return [], [] + + if dtype in DTYPE_LOWP_FP: + # For lower precision data type, if the call_range is not long enough, + # use tiling_factor // 2 for better performance + factor_lowp = cpu_vec_isa.pick_vec_isa().nelements(dtype=dtype) + for tiling_indice in tiling_indices: + if tiling_indice < 0: + tiling_indice = tiling_indice + len(call_ranges) + if tiling_indice < 0 or tiling_indice >= len(call_ranges): + continue + if has_free_symbols(call_ranges): + call_range = V.graph.sizevars.optimization_hint( + call_ranges[tiling_indice], fallback=0 + ) + if call_range < factor_lowp: + V.graph.sizevars.check_lt(call_range, factor_lowp) # type: ignore[arg-type] + tiling_factor = factor_lowp // 2 + break + elif call_ranges[tiling_indice] < factor_lowp: + tiling_factor = factor_lowp // 2 + break + + if len(tiling_indices) == 1: + return [tiling_factor], tiling_indices + if len(tiling_indices) == 2: + return [tiling_factor, tiling_factor], tiling_indices + return [], [] + + def _select_tiling_indices( + self, + fn_list, + var_sizes_list, + tiling_factor, + ): + all_index = [] + for fn, var_sizes in zip(fn_list, var_sizes_list): + rw = dependencies.extract_read_writes(fn, *var_sizes) + all_index += [dep.index for dep in itertools.chain(rw.reads, rw.writes)] + contig_vars = OrderedSet[int]() + contig_vars_list = [] + non_contig_stride_const = OrderedSet[int]() + non_contig_stride_other = OrderedSet[int]() + for index in all_index: + for var in index.free_symbols: + if not re.search(r"^d\d+$", var.name): + continue + stride = stride_at_vec_range(index, var, tiling_factor) + if stride == 0: + continue + elif stride == 1: + contig_vars.add(int(var.name[1:])) + contig_vars_list.append(int(var.name[1:])) + elif all(symbol_is_type(s, SymT.SIZE) for s in stride.free_symbols): + non_contig_stride_const.add(int(var.name[1:])) + else: + non_contig_stride_other.add(int(var.name[1:])) + contig_only = contig_vars - non_contig_stride_const - non_contig_stride_other + group, reduction_group = max(var_sizes_list, key=lambda sizes: len(sizes[1])) + num_itervars = len(group) + len(reduction_group) + if len(contig_vars) == 0: + # no contiguous vars + return [num_itervars - 1] + if contig_only: + return sorted(contig_only)[-1:] + contig_and_const_stride = ( + contig_vars & non_contig_stride_const + ) - non_contig_stride_other + contig_vars_sorted = sorted(contig_vars) + if ( + len(contig_vars_sorted) == 2 + and contig_vars_sorted[-1] in contig_and_const_stride + and contig_vars_sorted[-1] == num_itervars - 1 + ): + return contig_vars_sorted + return sorted(contig_vars_sorted, key=contig_vars_list.count)[-1:] + + +class CppKernelProxy(CppKernel): + # Subclass CppKernel, CppVecKernel, etc., to customize code generation. + # Override CppOverrides or CppVecOverrides to emit custom ops. + # Earlier, this meant copying codegen_functions() to use your subclasses. + # Now, use kernel_cls and vec_kernel_cls class attributes instead. + # This lets CppKernelProxy subclasses inject custom behavior cleanly. + # No need to duplicate codegen_functions() just to swap kernel classes. + kernel_cls: type[CppKernel] = CppKernel + vec_kernel_cls: type[CppVecKernel] = CppVecKernel + tile2d_kernel_cls: type[CppTile2DKernel] = CppTile2DKernel + + def __init__(self, kernel_group): + super().__init__(kernel_group.args, kernel_group.ws.num_threads) + self.kernel_group = kernel_group + self.loop_nest = None + self.call_ranges = None + self.picked_vec_isa: cpu_vec_isa.VecISA = cpu_vec_isa.pick_vec_isa() + self.kernels: list[CppKernel] = [] + + def data_type_propagation(self, nodes): + for _node in nodes: + assert isinstance(_node, SchedulerNode) + DataTypePropagation.propagate_scheduler_node(_node) + + # Check if all the nodes of a given fx graph can support BF16/FP16 + def is_lowp_fp_scheduler(self, scheduler_node: SchedulerNode): + if not isinstance(scheduler_node._body, LoopBody): + return True + # Propagate the dtype to check if all the fx node is bf16/fp16 + DataTypePropagation.propagate_scheduler_node(scheduler_node) + return ( + get_loop_body_lowp_fp(scheduler_node._body)[0] is not None + and not get_loop_body_lowp_fp(scheduler_node._body)[1] + ) + + def legalize_lowp_fp_dtype_loopbody(self, loop_body: LoopBody): + def add_to_dtype(sub_graph: torch.fx.Graph): + def get_input_dtype(node: torch.fx.Node) -> torch.dtype | None: + """Get input dtype for nodes that may consumes lowp fp dt""" + if node.target == "store": + return V.graph.get_dtype(node.args[1]) # type: ignore[arg-type] + elif node.target == "to_dtype_bitcast": + return node.args[-1] # type: ignore[return-value] + elif node.target == "to_dtype": + if len(node.args) > 3: + return node.args[3] # type: ignore[return-value] + else: + return node.kwargs.get("src_dtype", None) # type: ignore[return-value] + else: + return None + + def get_output_dtype(node: torch.fx.Node) -> torch.dtype | None: + """Get output dtype for nodes that may produce lowp fp dt""" + if node.target == "load": + assert len(node.args) == 3 + return V.graph.get_dtype(node.args[1]) # type: ignore[arg-type] + elif node.target in ["to_dtype", "constant", "index_expr"]: + return node.args[-1] # type: ignore[return-value] + elif node.target == "to_dtype_bitcast": + return node.args[2] # type: ignore[return-value] + else: + return None + + def is_lowp_fp_source(node: torch.fx.Node, dt: torch.dtype): + """Check if the given node produces output with expected low precision floating point data type.""" + assert dt in DTYPE_LOWP_FP + return get_output_dtype(node) == dt + + def is_lowp_fp_sink(node: torch.fx.Node, dt: torch.dtype): + """Check if the given node accept input with expected low precision floating point data type.""" + assert dt in DTYPE_LOWP_FP + if input_dtype := get_input_dtype(node): + return input_dtype == dt + elif node.target == "to_dtype": + # The `src_dtype` of a `to_dtype` node might miss, in which case the node accept any input dtype. + return True + else: + return False + + def is_lowp_fp_source_no_promote(node: torch.fx.Node, dt: torch.dtype): + """Check if the node is a lowp fp sources which are all directly fed to ops that accepts lowp fp input + thus no need to promote to float + """ + return is_lowp_fp_source(node, dt) and all( + is_lowp_fp_sink(user, dt) for user in node.users + ) + + sub_graph_nodes = list(sub_graph.nodes) + to_lowp_fp_legalized_nodes = [] + for _node in sub_graph_nodes: + if ( + _node.target in ["load", "index_expr"] + and (dt := get_output_dtype(_node)) in DTYPE_LOWP_FP + ): + # No need to promote to float if all users are ops that accepts lowp fp input + # pyrefly: ignore [bad-argument-type] + if all(is_lowp_fp_sink(user, dt) for user in _node.users): + continue + ops = _node.args[0] + with sub_graph.inserting_after(_node): + to_type_node = sub_graph.call_method( + "to_dtype", args=(ops, _node, torch.float) + ) + _node.replace_all_uses_with( + to_type_node, lambda n: n is not to_type_node + ) + # pyrefly: ignore [bad-assignment] + metrics.cpp_to_dtype_count += 1 + elif ( + _node.target == "store" + and (dt := get_input_dtype(_node)) in DTYPE_LOWP_FP + ): + ops, name, _, value_var, _ = _node.args + # pyrefly: ignore [bad-argument-type] + if is_lowp_fp_source_no_promote(value_var, dt): + continue + dtype = V.graph.get_dtype(name) + with sub_graph.inserting_before(_node): + to_type_node = sub_graph.call_method( + "to_dtype", args=(ops, value_var, dtype) + ) + _node.replace_input_with(value_var, to_type_node) + # pyrefly: ignore [bad-assignment] + metrics.cpp_to_dtype_count += 1 + elif _node.target == "reduction": + ( + ops, + dtype, + src_dtype, + reduction_type, + value, + ) = _node.args + if src_dtype in DTYPE_LOWP_FP: + # Since we always convert the load/store value to float if the tensor is bfloat16/float16. + # Therefore, the reduction should never work with bfloat16/float16 value. Hence, we update + # the bfloat16/float16 reduction by + # 1) updating the src_dtype to float + # and 2) updating the dtype to float if it is bfloat16/float16. + assert dtype in [ + torch.float, + torch.bfloat16, + torch.float16, + torch.int64, + ] + _node.args = ( + ops, + torch.float if dtype in DTYPE_LOWP_FP else dtype, + torch.float, + reduction_type, + value, + ) + elif _node.target == "constant" and _node.args[-1] in DTYPE_LOWP_FP: + # No need to promote to float if all users are ops that accepts lowp fp input + (ops, value, dt) = _node.args + if all(is_lowp_fp_sink(user, dt) for user in _node.users): # type: ignore[arg-type] + continue + _node.args = (ops, value, torch.float) + elif _node.target == "to_dtype" and _node.args[-1] in DTYPE_LOWP_FP: + # No need to promote to float if all users are ops that accepts lowp fp input + (ops, x, dt) = _node.args + if all(is_lowp_fp_sink(user, dt) for user in _node.users): # type: ignore[arg-type] + continue + # The legalization always loads the BF16/FP16 tensor as FP32 for computation + # and converts back to BF16/FP16 after the computation. + # Hence, there should be no computation w/ BF16/FP16. + # Therefore, we update the to_dtype by replacing the bf16/fp16 dtype with fp32. + # Save the legalized to_dtype node for the elimination(eliminate_to_dtype step): + # 1) Eliminate the redundant to_dtype node if we have a pattern as follows: + # graph(): + # %lowp_fp_legalized = call_method[target=to_dtype](args = (%ops, %input, torch.float)) + # %to_dtype2 = call_method[target=to_dtype](args = (%ops, %lowp_fp_legalized, torch.bfloat16/float16)) + # Regarding the first to_dtype, it is redundant because + # the second to_type also converts to the torch.bfloat16/torch.float16. + # Hence, we remove the first to_type. + to_lowp_fp_legalized_nodes.append(_node) + _node.args = (ops, x, torch.float) + elif _node.target == "to_dtype_bitcast": + (ops, value_var, dtype, src_dtype) = _node.args + + # to_dtype_bitcast act as a lowp fp sink: + # c10::bit_cast requires the source and target have the same bitwidth. Because the input tensor's + # dtype could be promoted, e.g. from float16 to float, we have to cast the tensor to its original + # source dtype before invoking bit_cast. + if src_dtype in DTYPE_LOWP_FP: + # No need to promote to float if it is a user of a lowp fp sources + # which are all directly fed to ops that accepts lowp fp input + if not is_lowp_fp_source_no_promote(value_var, src_dtype): + with sub_graph.inserting_before(_node): + to_type_node = sub_graph.call_method( + "to_dtype", args=(ops, value_var, src_dtype) + ) + _node.replace_input_with(value_var, to_type_node) + # pyrefly: ignore [bad-assignment] + metrics.cpp_to_dtype_count += 1 + + # to_dtype_bitcast act as a lowp fp source: + # We also need to convert the bit-casted tensor back to float to make sure we keep using higher + # precision values for the rest of the computation. + if dtype in DTYPE_LOWP_FP: + # No need to promote to float if all users are ops that accepts lowp fp input + if not ( + all(is_lowp_fp_sink(user, dtype) for user in _node.users) + ): + ops = _node.args[0] + with sub_graph.inserting_after(_node): + to_type_node = sub_graph.call_method( + "to_dtype", args=(ops, _node, torch.float) + ) + _node.replace_all_uses_with( + to_type_node, lambda n: n is not to_type_node + ) + # pyrefly: ignore [bad-assignment] + metrics.cpp_to_dtype_count += 1 + + def eliminate_to_dtype(sub_graph: torch.fx.Graph): + def _eliminate_duplicate_to_node(sub_graph: torch.fx.Graph): + # Eliminate the redundant to_dtype node. Let's consider a pattern as follows: + # graph(): + # %to_dtype1 = call_method[target=to_dtype](args = (%ops, %input, torch.float), kwargs = {}) + # %to_dtype2 = call_method[target=to_dtype](args = (%ops, %to_dtype1, torch.float), kwargs = {}) + # Regarding the first to_dtype, it is redundant because the second to_type also converts to the + # torch.float. Hence, we remove the first to_type + def _used_by_to(to_node: torch.fx.Node): + return all(usr.target == "to_dtype" for usr in to_node.users) + + all_to_nodes = [ + node for node in sub_graph.nodes if node.target == "to_dtype" + ] + all_to_nodes_and_users = [ + {node: node.users} for node in all_to_nodes if _used_by_to(node) + ] + for node_users in all_to_nodes_and_users: + for node, users in node_users.items(): + if node in sub_graph.nodes and ( + all(usr.args[-1] == node.args[-1] for usr in users) + or ( + node in to_lowp_fp_legalized_nodes + and all( + usr.args[-1] in DTYPE_LOWP_FP for usr in users + ) + ) + ): + val_node = node.all_input_nodes[-1] + node.replace_all_uses_with(val_node) + sub_graph.erase_node(node) + + # For debug mode, the graph of LoopBody will attach a new GraphModule as + # owning_module for debugging while the release mode will not. The lint will + # check whether the graph has owning_module to decide if it needs to check + # call_module. LoopBody might contain get_index as a module call. But it + # is just a function. Hence, it cannot pass the lint check for debug mode. + # We bypass the check if the owning_module is None. Eventually, we should call + # get_index via call_function but not call_module. + if sub_graph.owning_module is None: + sub_graph.lint() + + _eliminate_duplicate_to_node(sub_graph) + + eliminate_to_dtype(sub_graph) + + sub_blocks = [loop_body.root_block] + list(loop_body.subblocks.values()) + for sub_block in sub_blocks: + add_to_dtype(sub_block.graph) + + def legalize_lowp_fp_dtype(self, nodes): + if all( + isinstance(_node, SchedulerNode) and self.is_lowp_fp_scheduler(_node) + for _node in nodes + ): + # Mark the load node to load bf16/fp16 + for _node in nodes: + sub_blocks = [_node._body.root_block] + list( + _node._body.subblocks.values() + ) + for sub_block in sub_blocks: + for fx_node in sub_block.graph.nodes: + if fx_node.target in ["load", "store"]: + assert fx_node.meta + assert OptimizationContext.key in fx_node.meta + opt_ctx: OptimizationContext = fx_node.meta[ + OptimizationContext.key + ] + assert opt_ctx.dtype in DTYPE_LOWP_FP + + # Bypass the legalization as the kernel can run with bf16/fp16 directly + return + + for _node in nodes: + assert isinstance(_node, SchedulerNode) + assert isinstance(_node._body, LoopBody) + body: LoopBody = _node._body + if not body.is_memory_copy(): + self.legalize_lowp_fp_dtype_loopbody(body) + + def codegen_functions(self, fn_list, var_sizes_list): + assert len(fn_list) == len(var_sizes_list) + kernel_group = self.kernel_group + group, reduction_group = max(var_sizes_list, key=lambda sizes: len(sizes[1])) + + self.set_ranges(group, reduction_group) + + def codegen_kernel(cls, *args): + with kernel_group.new_kernel(cls, *args) as kernel: + # Ugly hack to maintain the metrics kernel count since + # we only count in CppKernelProxy, not those contained in it + # pyrefly: ignore [bad-assignment] + metrics.generated_kernel_count -= 1 + + run(kernel) + return kernel + + def run(kernel): + vars, reduction_vars = kernel.set_ranges(group, reduction_group) + in_suffix = False + for fn, var_sizes in zip(fn_list, var_sizes_list): + if var_sizes in [ + (group, reduction_group), + (tuple(itertools.chain(group, reduction_group)), ()), + ]: + assert not in_suffix + fn(vars, reduction_vars) + else: + in_suffix = True + assert var_sizes == ( + group, + (), + ), f"unexpected group: {var_sizes} != {group}, {reduction_group}" + # we can fuse in some extra pointwise into the suffix + with kernel.write_to_suffix(): + fn(vars, ()) + + scalar_kernel = codegen_kernel(self.kernel_cls) + V.graph.removed_buffers |= scalar_kernel.removed_buffers + V.graph.inplaced_to_remove |= scalar_kernel.inplaced_to_remove + self.loop_nest = LoopNest.build(scalar_kernel) + + if not self.picked_vec_isa or not self.itervars: + self.kernels = [scalar_kernel] + self.aggregate_reduction_buffers(False, None) + self.loop_nest.set_kernel(self) + return + + # Kernels share the same global contexts like V.graph.wrapper_code, V.kernel.args. + # But the generated scalar kernel has updated these global contexts. Hence, the other kernels + # should not do this again to avoid context conflict. By now, we only control the + # config.inplace_buffers. In the future, we could maintain more contexts. + with torch._inductor.config.patch(inplace_buffers=False): + tiling_select = TilingSelect() + tiling_factors, tiling_indices = tiling_select.select_tiling( + fn_list, var_sizes_list + ) + assert len(tiling_factors) == len(tiling_indices) + _inner_loop_reduction_outer_not = False + _outer_loop = None + if tiling_indices: + inner_loop_reduction = False + outer_loop_level = tiling_indices[0] + inner_loop_level = outer_loop_level + 1 + if len(self.loop_nest.loops) > inner_loop_level: + inner_loop_reduction = self.loop_nest.loops[ + inner_loop_level + ].is_reduction + outer_loop_reduction = self.loop_nest.loops[ + outer_loop_level + ].is_reduction + _inner_loop_reduction_outer_not = ( + inner_loop_reduction and not outer_loop_reduction + ) + + if len(tiling_indices) == 1: + # pyrefly: ignore [bad-assignment] + metrics.generated_cpp_vec_kernel_count += 1 + loop = self.loop_nest.tile(tiling_indices[0], factor=tiling_factors[0]) + vec_kernel = codegen_kernel( + self.vec_kernel_cls, tiling_factors[0], tiling_indices[0] + ) + tail_size = loop.size - loop.tiled_size + vec_kernel.active_ranges = {loop.var: (0, loop.tiled_size)} + if config.cpp.enable_loop_tail_vec: + tail_kernel = codegen_kernel( + self.vec_kernel_cls, + tiling_factors[0], + tiling_indices[0], + tail_size, + ) + else: + tail_kernel = scalar_kernel + scalar_kernel.inner_itervars = [loop.var] + tail_kernel.active_ranges = {loop.var: (loop.tiled_size, loop.size)} + self.kernels = [vec_kernel, tail_kernel] + _outer_loop = loop + elif len(tiling_indices) == 2: + assert ( + tiling_indices[1] == len(self.itervars) - 1 + and tiling_factors[0] == tiling_factors[1] + ) + + # pyrefly: ignore [bad-assignment] + metrics.generated_cpp_vec_kernel_count += 2 + outer_loop = self.loop_nest.tile( + tiling_indices[0], factor=tiling_factors[0] + ) + outer_ranges = { + "main": (0, outer_loop.tiled_size), + "tail": (outer_loop.tiled_size, outer_loop.size), + } + outer_tail_size = outer_loop.size - outer_loop.tiled_size + inner_loop = self.loop_nest.tile( + tiling_indices[1], factor=tiling_factors[0] + ) + inner_ranges = { + "main": (0, inner_loop.tiled_size), + "tail": (inner_loop.tiled_size, inner_loop.size), + } + inner_tail_size = inner_loop.size - inner_loop.tiled_size + tile2d_kernel = codegen_kernel( + self.tile2d_kernel_cls, + tiling_factors[0], + tiling_indices, + ) + tile2d_kernel.active_ranges = { + outer_loop.var: outer_ranges["main"], + inner_loop.var: inner_ranges["main"], + } + tail_kernel = [] + if config.cpp.enable_loop_tail_vec: + for outer_r, inner_r in ( + ("main", "tail"), + ("tail", "main"), + ("tail", "tail"), + ): + _inner_tail_size = ( + inner_tail_size if inner_r == "tail" else None + ) + _outer_tail_size = ( + outer_tail_size if outer_r == "tail" else None + ) + kernel = codegen_kernel( + self.tile2d_kernel_cls, + tiling_factors[0], + tiling_indices, + _inner_tail_size, + _outer_tail_size, + ) + kernel.active_ranges = { + outer_loop.var: outer_ranges[outer_r], + inner_loop.var: inner_ranges[inner_r], + } + tail_kernel.append(kernel) + else: + vec_kernel = codegen_kernel( + self.vec_kernel_cls, tiling_factors[0], tiling_indices[0] + ) + vec_kernel.active_ranges = { + outer_loop.var: outer_ranges["main"], + inner_loop.var: inner_ranges["tail"], + } + vec_kernel.inner_itervars = [inner_loop.var] + tail_kernel.append(vec_kernel) + scalar_kernel.active_ranges = { + outer_loop.var: outer_ranges["tail"], + inner_loop.var: (0, inner_loop.size), + } + scalar_kernel.inner_itervars = [inner_loop.var, outer_loop.var] + tail_kernel.append(scalar_kernel) + self.kernels = [tile2d_kernel] + tail_kernel + _outer_loop = outer_loop + else: + self.kernels = [scalar_kernel] + self.aggregate_reduction_buffers( + _inner_loop_reduction_outer_not, _outer_loop + ) + self.loop_nest.set_kernel(self) + + def codegen_loop_bodies(self, loop_bodies, var_sizes_list): + for body in loop_bodies: + self.legalize_lowp_fp_dtype_loopbody(body) + DataTypePropagation.propagate_loopbody(body) + self.codegen_functions(loop_bodies, var_sizes_list) + + def codegen_nodes(self, nodes: list[SchedulerNode]): + # Legalize BF16 node by adding to_dtype explicitly + self.legalize_lowp_fp_dtype(nodes) + self.data_type_propagation(nodes) + assert len(nodes) >= 1 + + def fn(node, *index_vars): + node.decide_inplace_update() + node.mark_run() + if isinstance(V.kernel, NullKernelHandler): + return node._body(*index_vars) + else: + return node.codegen(index_vars) + + fn_list = [functools.partial(fn, node) for node in nodes] + + if ( + isinstance(V.local_buffer_context, LocalBufferContext) + and V.local_buffer_context.local_buffers + ): + + def wrap_fn(fn): + wrapped_fn = V.local_buffer_context.localize_function( + fn, + ) + wrapped_fn.original_fn = fn + return wrapped_fn + + fn_list = [wrap_fn(fn) for fn in fn_list] + + var_sizes_list = [node.group[1] for node in nodes] + self.codegen_functions(fn_list, var_sizes_list) + + def codegen_loops(self, code, worksharing): + self.codegen_loops_impl(self.loop_nest, code, worksharing) + + def update_stores_with_parallel_reduction(self): + for kernel in self.kernels: + kernel.update_stores_with_parallel_reduction() + + def gen_body(self, code: BracesBuffer | None = None): + assert code is not None + if_prefix = "C10_LIKELY" + for kernel in self.kernels: + with contextlib.ExitStack() as stack: + if kernel.codegen_conditions(code, if_prefix): + if_prefix = "C10_UNLIKELY" + stack.enter_context(code.indent()) + code.splice(kernel.gen_body()) + + def aggregate_reduction_buffers( + self, inner_loop_reduction_outer_not: bool, outer_loop: Optional["LoopLevel"] + ): + """ + CppKernel/CppVecKernel/CppTile2dKernel have reduction buffers themselves. + Here, we decide how to aggregate them together and place new reduction buffers + under CppKernelProxy. + """ + + def aggregate_reduction_prefix_suffix(outer_loop: "LoopLevel"): + assert len(self.kernels) >= 2 + main_loop_kernel = self.kernels[0] + tail_loop_kernel = self.kernels[-1] + assert isinstance(main_loop_kernel, self.vec_kernel_cls) + + # Prefix + if type(tail_loop_kernel) is self.kernel_cls: + # if tail loop kernel is a scalar kernel, we need to extend tmp_acc -> tmp_acc_arr[] to + # hold the temporary inner loop acc result for outer tail loop + tail_loop_kernel.finalize_reduction_prefix( + main_loop_kernel.tiling_factor + ) + main_loop_kernel.finalize_reduction_prefix() + self.reduction_prefix.splice( + tail_loop_kernel.reduction_prefix + + main_loop_kernel.reduction_prefix + ) + else: + main_loop_kernel.finalize_reduction_prefix() + self.reduction_prefix.splice(main_loop_kernel.reduction_prefix) + + # Suffix + suffix_buf = BracesBuffer() + with contextlib.ExitStack() as stack: + if main_loop_kernel.codegen_conditions( + suffix_buf, "C10_LIKELY", outer_loop.var + ): + stack.enter_context(suffix_buf.indent()) + suffix_buf.splice(main_loop_kernel.reduction_suffix) + with contextlib.ExitStack() as stack: + if tail_loop_kernel.codegen_conditions( + suffix_buf, "C10_UNLIKELY", outer_loop.var + ): + stack.enter_context(suffix_buf.indent()) + if type(tail_loop_kernel) is self.kernel_cls: + reduction_vars = tail_loop_kernel.reduction_var_names + for name in reduction_vars: + new_name = f"{name}_arr[{outer_loop.var}_tail - {cexpr_index(outer_loop.tiled_size)}]" + replace_acc_name(tail_loop_kernel.stores, name, new_name) + replace_acc_name( + tail_loop_kernel.reduction_suffix, name, new_name + ) + # If tail loop kernel is a scalar kernel, use direct sum instead of cascade_sum_combine + # as the reduction vars are extended: tmp_acc -> tmp_acc_arr[]. + replace_cascade_sum_with_add(tail_loop_kernel.stores) + suffix_buf.splice( + move_code_under_inner_loop( + tail_loop_kernel.reduction_suffix, + outer_loop.var, + f"{outer_loop.var}_tail", + outer_loop.tiled_size, + outer_loop.size, + ) + ) + else: + suffix_buf.splice(tail_loop_kernel.reduction_suffix) + self.reduction_suffix = suffix_buf + + main_kernel = self.kernels[0] + if inner_loop_reduction_outer_not: + assert outer_loop + aggregate_reduction_prefix_suffix(outer_loop) + else: + main_kernel.finalize_reduction_prefix() + self.reduction_prefix.splice(main_kernel.reduction_prefix) + self.reduction_suffix.splice(main_kernel.reduction_suffix) + self.parallel_reduction_prefix.splice(main_kernel.parallel_reduction_prefix) + self.parallel_reduction_suffix.splice(main_kernel.parallel_reduction_suffix) + self.local_reduction_init.splice(main_kernel.local_reduction_init) + self.local_reduction_stores.splice(main_kernel.local_reduction_stores) + self.non_parallel_reduction_prefix.splice( + main_kernel.non_parallel_reduction_prefix + ) + self.non_parallel_reduction_suffix.splice( + main_kernel.non_parallel_reduction_suffix + ) + + +class OuterLoopFusedKernel(CppKernel): + def __init__(self, kernel_group): + super().__init__(kernel_group.args, kernel_group.ws.num_threads) + self.inner: list[LoopNest] = [] + + def decide_parallel_depth(self, max_parallel_depth, threads): + kernels_parallel_depth = [] + nested_kernels: list[CppKernel] = [ + loop_nest.get_kernel() for loop_nest in self.inner + ] + # TODO(leslie-fang-intel): only enable parallel within all outer loop levels. + for kernel in nested_kernels: + # For any ScalarKernel, VecKernel, or Tile2DKernel, + # they should all have the same call_ranges + call_ranges = kernel.call_ranges + assert call_ranges is not None + kernels_parallel_depth.append( + kernel.decide_parallel_depth( + ParallelDepth( + parallel_depth=( + len(call_ranges) - max_parallel_depth.start_depth + ), + start_depth=max_parallel_depth.start_depth, + ), + threads, + ).parallel_depth + ) + return ParallelDepth( + parallel_depth=min( + max_parallel_depth.parallel_depth, max(kernels_parallel_depth) + ), + start_depth=max_parallel_depth.start_depth, + ) + + +class ReasonFusedNodes(Enum): + SAME_VARS_REDUCE = "same_vars_reduce" + COMPATIBLE_REDUCTION = "compatible_reduction" + COMPATIBLE_RANGES_NO_REDUCTION = "compatible_ranges_no_reduction" + + +class CppScheduling(BaseScheduling): + # Subclass CppKernelProxy to customize codegen without copying codegen_node(). + # Use kernel_proxy_cls to inject custom proxies in CppScheduling subclasses. + # Avoid duplicating codegen_node() just to swap in a custom kernel proxy class. + kernel_proxy_cls: type[CppKernelProxy] = CppKernelProxy + # ctypes limits the number of args to 1024, refer to: + # https://github.com/python/cpython/commit/a285af7e626d1b81cf09f8b2bf7656f100bc1237 + # We set a conservative threshold here. + MAX_FUSED_KERNEL_ARGS_NUM = 500 + backend_features = OrderedSet( + [ + BackendFeature.INPLACE_BUFFERS, + BackendFeature.REDUCE_TO_SINGLE_ELEMENT, + ] + ) + + @classmethod + def get_backend_features(cls, device: torch.device) -> OrderedSet[BackendFeature]: + return cls.backend_features + + def __init__(self, scheduler): + super().__init__(scheduler) + if scheduler: + self.reset_kernel_group() + self._ready_to_flush = False + + def _set_flush_status(self, status: bool): + self._ready_to_flush = status + + def group_fn(self, sizes): + return tuple(tuple(map(V.graph.sizevars.simplify, s)) for s in sizes) + + def reset_kernel_group(self): + self.kernel_group = KernelGroup() + + def fuse(self, node1, node2): + if node1.is_foreach() or node2.is_foreach(): + return ForeachKernelSchedulerNode.fuse(node1, node2) + elif node1.is_template(): + assert not node2.is_template() + return FusedSchedulerNode.fuse(node1, node2) + else: + if ( + self._why_fuse_nodes(node1, node2) + == ReasonFusedNodes.COMPATIBLE_RANGES_NO_REDUCTION + ): + assert isinstance(node1, (SchedulerNode, FusedSchedulerNode)) + assert isinstance(node2, (SchedulerNode, FusedSchedulerNode)) + + _, (vars1, reduce1) = node1.group + _, (vars2, reduce2) = node2.group + assert reduce1 == () and reduce2 == (), (reduce1, reduce2) + + def get_indexing_ranges_exprs(node): + if isinstance(node, FusedSchedulerNode): + assert len(node.snodes) > 0, node.snodes + var_ranges = None + indexing_exprs = OrderedSet[Any]() + for snode in node.snodes: + v, exprs = get_indexing_ranges_exprs(snode) + if var_ranges is None: + var_ranges = v + assert var_ranges == v, (var_ranges, v, node.snodes) + indexing_exprs.update(exprs) + return var_ranges, list(indexing_exprs) + else: + assert isinstance(node, SchedulerNode) + comp_buffer = node.node + assert isinstance(comp_buffer, ir.ComputedBuffer) + _, body, _ = comp_buffer.get_default_sizes_body() + return body.var_ranges, list(body.indexing_exprs.values()) + + node_to_recomp = node1 if len(vars1) < len(vars2) else node2 + assert isinstance(node_to_recomp, SchedulerNode) + + ref_node = node2 if len(vars1) < len(vars2) else node1 + + ref_indexing_constraints = get_indexing_ranges_exprs(ref_node) + + node_to_recomp.recompute_size_and_body( + extra_indexing_constraints=ref_indexing_constraints + ) + + _, (vars1, _) = node1.group + _, (vars2, _) = node2.group + + if vars1 == vars2: + return FusedSchedulerNode.fuse(node1, node2) + + # recompute ref_node if its ranges are also changed + node_to_recomp_indexing_constraints = get_indexing_ranges_exprs( + node_to_recomp + ) + if isinstance(ref_node, SchedulerNode): + ref_node.recompute_size_and_body( + extra_indexing_constraints=node_to_recomp_indexing_constraints + ) + else: + assert isinstance(ref_node, FusedSchedulerNode) + for snode in ref_node.snodes: + assert isinstance(snode, SchedulerNode) + snode.recompute_size_and_body( + extra_indexing_constraints=node_to_recomp_indexing_constraints + ) + ref_node = FusedSchedulerNode(ref_node.scheduler, ref_node.snodes) + + _, (vars1, _) = node1.group + _, (vars2, _) = node2.group + assert vars1 == vars2, (vars1, vars2) + return FusedSchedulerNode.fuse(node1, node2) + elif self.can_fuse_vertical_outer_loop(node1, node2): + return OuterLoopFusedSchedulerNode.fuse( + node1, node2, self._get_outer_loop_fusion_depth(node1, node2) + ) + else: + return FusedSchedulerNode.fuse(node1, node2) + + def _why_fuse_nodes(self, node1, node2) -> ReasonFusedNodes | None: + _, (vars1, reduce1) = node1.group + _, (vars2, reduce2) = node2.group + + if vars1 == vars2 and reduce1 == reduce2: + return ReasonFusedNodes.SAME_VARS_REDUCE + if reduce1 == () and vars1 == vars2 + reduce2: + return ReasonFusedNodes.COMPATIBLE_REDUCTION + if self._can_fuse_nodes_with_compatible_ranges(node1, node2): + return ReasonFusedNodes.COMPATIBLE_RANGES_NO_REDUCTION + # TODO(jansel): allow fusion pointwise (vars1, ()) suffix? + return None + + def _can_fuse_nodes_with_compatible_ranges(self, node1, node2): + # Here we try to fuse SchedulerNode/FusedSchedulerNode with compatible ranges + # e.g. (s0, s1, s2) and (s0 * s1 * s2) + _, (vars1, reduce1) = node1.group + _, (vars2, reduce2) = node2.group + + c1 = reduce1 == () and reduce2 == () + c2 = math.prod(vars1) == math.prod(vars2) + c3 = len(vars1) == 1 or len(vars2) == 1 + if not (c1 and c2 and c3): + return False + + node_to_recomp = node1 if len(vars1) < len(vars2) else node2 + ref_node = node2 if len(vars1) < len(vars2) else node1 + + # We can not recompute sizes and body for nodes other than SchedulerNode + # TODO: we can extend fusion support with compatible ranges for FusedSchedulerNode + if isinstance(node_to_recomp, FusedSchedulerNode): + return False + + # It may happen that node1 and node2 compatible number of elements + # but different original ranges, for example: + # {d0: s0, d1: s1, d2: s2} vs {d0: s0*s1*s2} + # See https://github.com/pytorch/pytorch/pull/120077/files#r1500427848 for more details + # TODO: we can fix if it allows us to CSE at least one of the variables + + assert isinstance(node_to_recomp, SchedulerNode) + if isinstance(node_to_recomp.node, ir.TemplateBuffer): + return False + assert isinstance(node_to_recomp.node, ir.ComputedBuffer) + # node.data.get_size() is a cheaper version of node.get_read_writes().var_ranges + # but without variable name + ranges2 = node_to_recomp.node.data.get_size() + ranges1 = None + if isinstance(ref_node, FusedSchedulerNode): + ranges_set = OrderedSet[tuple[Any, ...]]() + for snode in ref_node.snodes: + if isinstance(snode.node, ir.TemplateBuffer): + break + assert isinstance(snode.node, ir.ComputedBuffer) + ranges_set.add(tuple(snode.node.data.get_size())) + + if len(ranges_set) != 1: + return False + + ranges1 = list(next(iter(ranges_set))) + else: + assert isinstance(ref_node, SchedulerNode) + assert isinstance(ref_node.node, ir.ComputedBuffer) + ranges1 = ref_node.node.data.get_size() # type: ignore[assignment] + + if ranges1 != ranges2: + return False + + return True + + def _can_fuse_horizontal_impl(self, node1, node2): + assert isinstance( + node1, (FusedSchedulerNode, SchedulerNode, ExternKernelSchedulerNode) + ) + assert isinstance(node2, (FusedSchedulerNode, SchedulerNode)) + if any( + isinstance(node, (OuterLoopFusedSchedulerNode, ExternKernelSchedulerNode)) + for node in (node1, node2) + ): + return False + return self._why_fuse_nodes(node1, node2) is not None + + def can_fuse_horizontal(self, node1, node2): + if node1.is_template() or node2.is_template(): + return False + if ( + len(node1.get_nodes()) + len(node2.get_nodes()) + > config.cpp.max_horizontal_fusion_size + ): + return False + + return self._can_fuse_horizontal_impl(node1, node2) + + def can_fuse_multi_outputs_template( + self, node1: BaseSchedulerNode, node2: BaseSchedulerNode + ) -> bool: + if template_buf := node1.get_template_node(): + return ( + isinstance(template_buf.layout, ir.MultiOutputLayout) + and isinstance(node2.node, ir.MultiOutput) + and len(node2.node.inputs) == 1 + and node2.node.inputs[0].get_name() == template_buf.name # type: ignore[union-attr] + ) + return False + + def _get_outer_loop_fusion_depth(self, node1, node2): + DISABLE_OUTER_LOOP_FUSION = 0 + if not all( + type(node) + in (OuterLoopFusedSchedulerNode, FusedSchedulerNode, SchedulerNode) + for node in (node1, node2) + ): + return DISABLE_OUTER_LOOP_FUSION + + _node1 = ( + node1.get_outer_nodes()[-1] + if isinstance(node1, OuterLoopFusedSchedulerNode) + else node1 + ) + assert isinstance(_node1, (FusedSchedulerNode, SchedulerNode)) + _node2 = ( + node2.get_outer_nodes()[0] + if isinstance(node2, OuterLoopFusedSchedulerNode) + else node2 + ) + assert isinstance(_node2, (FusedSchedulerNode, SchedulerNode)) + + _, (vars1, reduce1) = _node1.group + _, (vars2, reduce2) = _node2.group + if vars1 == () and vars2 == () and reduce1 != () and reduce2 != (): + # Reduction only + return DISABLE_OUTER_LOOP_FUSION + if all(type(node) is OuterLoopFusedSchedulerNode for node in (node1, node2)): + return ( + node1.outer_loop_fusion_depth + if node1.outer_loop_fusion_depth == node2.outer_loop_fusion_depth + else DISABLE_OUTER_LOOP_FUSION + ) + outer_loop_fusion_depth = min(len(vars1), len(vars2)) + if ( + outer_loop_fusion_depth >= 1 + and vars1[:outer_loop_fusion_depth] == vars2[:outer_loop_fusion_depth] + ): + if any( + type(node) is OuterLoopFusedSchedulerNode for node in (node1, node2) + ): + _compare_node = ( + node1 if type(node1) is OuterLoopFusedSchedulerNode else node2 + ) + if _compare_node.outer_loop_fusion_depth == outer_loop_fusion_depth: + # Same outer loop fusion depth as prev nodes in OuterLoopFusedSchedulerNode + return outer_loop_fusion_depth + else: + return DISABLE_OUTER_LOOP_FUSION + else: + # First 2 nodes to generate OuterLoopFusedSchedulerNode + return outer_loop_fusion_depth + return DISABLE_OUTER_LOOP_FUSION + + def can_fuse_vertical_outer_loop(self, node1, node2): + return ( + not node1.is_template() + and not node2.is_template() + and node1.get_operation_names() & node2.ancestors + and not ( + self._can_fuse_horizontal_impl(node1, node2) + and not node1.is_reduction() + ) + and self._get_outer_loop_fusion_depth(node1, node2) >= 1 + ) + + def get_fusion_pair_priority(self, node1, node2): + if self.can_fuse_vertical_outer_loop(node1, node2): + # Outer loop fusion with lower priority + return 1 + else: + return 0 + + def can_fuse_vertical(self, node1, node2): + if node2.is_template(): + # TODO(jgong5): support pre-op fusion with template + return False + if node1.is_template(): + template_fusion_supported, _ = template_fusion_with_epilogues_supported( + node1, [node2] + ) + return not node2.is_reduction() and template_fusion_supported + return ( + self._can_fuse_horizontal_impl(node1, node2) and not node1.is_reduction() + ) or self.can_fuse_vertical_outer_loop(node1, node2) + + def try_loop_split(self, nodes: list[SchedulerNode]): + """ + Apply loop split optimization. + When one of the indexing_exprs contains a division, we eliminate the division by splitting the loop + to avoid non-contiguous loads, subject to the following conditions: + 1. No reduction and no mudular index for all nodes. + 2. The indexing_exprs of all nodes contain only one (or more, but all the same) division, + where the divisor is an integer and not too small (the divisor > 8), the dividend is + one of the iter_vars, and this var, i.e. the dimension that needs to be split, is + contiguous in all other indexing_exprs. + + For example, if the node's var_ranges: {z0: 2, z1: 9216, z2: 960} and indexing_exprs: + {'index0': 8847360*z0 + 960*z1 + z2, 'index1': 32*z0 + (z2//30), 'index2': z2}, + we will split z2 -> 30*z2 + z3, then the node's var_ranges will be changed to + {z0: 2, z1: 9216, z2: 32, z3: 30} and indexing_exprs will be changed to + {'index0': 8847360*z0 + 960*z1 + 30*z2 + z3, 'index1': 32*z0 + z2, 'index2': 30*z2 + z3}. + """ + + # No reduction and no mudular + if any( + len(node.group[1][1]) != 0 + or any( + expr.has(ModularIndexing) for expr in node._body.indexing_exprs.values() + ) + for node in nodes + ): + return nodes + + split_var = None + split_number = None + num_div = 0 + div_expr_ = None + match_div = False + matched_node = None + matched_index_size = None + + # Collect node info for later compatibility check + node_bodies: list[tuple[Any, Any]] = [] + + for node in nodes: + assert isinstance(node.node, ir.ComputedBuffer) + sizes_body = node.node.get_default_sizes_body() + node_bodies.append((node, sizes_body)) + (index_size, _), original_body, _ = sizes_body + for name, expr in original_body.indexing_exprs.items(): + if not isinstance(expr, sympy.Expr): + continue + for div_expr in expr.find(FloorDiv): + if ( + any(div_expr.has(var) for var in original_body.iter_vars) + and div_expr != div_expr_ + ): + div_expr_ = div_expr + num_div += 1 + if num_div > 1: + return nodes + if ( + isinstance(div_expr.args[1], sympy.core.numbers.Integer) + and div_expr.args[0] in original_body.iter_vars + and name is not None + and all( + stride_at_vec_range(expr_, div_expr.args[0]) in (0, 1) + for name_, expr_ in original_body.indexing_exprs.items() + if name_ != name + ) + and div_expr.args[1] > 8 + ): + split_var = div_expr.args[0] + split_number = div_expr.args[1] + match_div = True + matched_node = node + matched_index_size = index_size + + # Only one node contains a division, and the split dimension is contiguous in all other indexing_exprs. + if not match_div: + return nodes + + # Check if all nodes have split_var in their iter_vars and have compatible sizes + # (same number of index dimensions). If not, bail out to avoid incompatible + # var_ranges after loop split which would cause assertion failures in + # simplify_and_reorder or codegen_functions. + assert matched_index_size is not None + matched_num_dims = len(matched_index_size) + + for node, ((index_size, _), original_body, _) in node_bodies: + if split_var not in original_body.iter_vars: + return nodes + if len(index_size) != matched_num_dims: + return nodes + + extra_indexing_constraints = None + + def loop_split(sizes, body, vars): + index_size, reduce_size = sizes + index_vars, reduce_vars = vars + split_idx = index_vars.index(split_var) + new_index_size = index_size.copy() + new_index_size[split_idx] = index_size[split_idx] // split_number + new_index_size.insert(split_idx + 1, split_number) + (new_index_vars, _), var_ranges = dependencies.index_vars_no_squeeze( + new_index_size, reduce_size, prefix="y" + ) + iter_vars = new_index_vars.copy() + divisor_var = iter_vars.pop(split_idx + 1) + iter_vars[split_idx] = split_number * iter_vars[split_idx] + divisor_var + body = ir.LoopBody( + body, [iter_vars, reduce_vars], var_ranges, new_index_vars, reduce_vars + ) + nonlocal extra_indexing_constraints + if not extra_indexing_constraints: + extra_indexing_constraints = ( + body.var_ranges, + list(body.indexing_exprs.values()), + ) + return ( + (new_index_size, reduce_size), + body, + (new_index_vars, reduce_vars), + ) + + # Here decide the final loop order + for node in nodes: + if node == matched_node: + node.recompute_size_and_body(recompute_sizes_body_func=loop_split) + for node in nodes: + if node != matched_node: + node.recompute_size_and_body( + extra_indexing_constraints=extra_indexing_constraints, + recompute_sizes_body_func=loop_split, + ) + + return nodes + + def codegen_outer_loop_node( + self, + node: OuterLoopFusedSchedulerNode, + ): + """ + Generate the code for the outer loop fused scheduler node. + 1. Codegen with fused outer loop: depends on the analysis of + the outer loop fused scheduler node, with or without the local buffer. + 2. If failed, fallback to standard codegen. + """ + kernel_group = self.kernel_group + generated_cpp_vec_kernel_count = metrics.generated_cpp_vec_kernel_count + cpp_kernel_proxy_list: list[self.kernel_proxy_cls] = [] # type: ignore[name-defined] + nodes_list: list[list[SchedulerNode]] = [] + assert isinstance(node, OuterLoopFusedSchedulerNode) + + def try_outer_loop_fusion_with_local_buf(node: OuterLoopFusedSchedulerNode): + """ + Codegen code with fused outer loop and local Buffer. + """ + assert isinstance(node, OuterLoopFusedSchedulerNode) + cpp_kernel_proxy_list.clear() + nodes_list.clear() + + def get_call_ranges(node: BaseSchedulerNode): + assert isinstance(node, (SchedulerNode, FusedSchedulerNode)) + nodes: list[SchedulerNode] = node.get_nodes() # type: ignore[assignment] + _, (group, reduction_group) = max( + nodes, key=lambda x: int(x.is_reduction()) + ).group + call_ranges = tuple(group) + tuple(reduction_group) + return call_ranges + + local_buffers: list[ir.Buffer] = [] + # Map local buffer name to a list of global buffers + local_to_global_buffers: dict[str, list[ir.Buffer]] = {} + if all( + len(get_call_ranges(_node)) == node.outer_loop_fusion_depth + 1 + for _node in node.get_outer_nodes() + ): + # Ref to the typical case of local buffer in + # https://github.com/pytorch/pytorch/blob/1115a25c36340554442f28f9570abd42f0aface2/aten/src/ATen/native/cpu/SoftMaxKernel.cpp#L159 # noqa: B950 + # where the buffer is with size of last dim and contiguous. + # Only support this typical case at first. + visited_scheduler_nodes: OrderedSet[str] = OrderedSet() + for scheduler_node in node.get_nodes(): + # all users inside same OuterLoopFusedSchedulerNode + assert isinstance(scheduler_node, SchedulerNode) + visited_scheduler_nodes.add(scheduler_node.get_name()) + if ( + scheduler_node.is_reduction() + or len(scheduler_node.get_outputs()) != 1 + ): + continue + + scheduler_buffer = scheduler_node.get_outputs()[0] + if all( + user.node in node.get_nodes() for user in scheduler_buffer.users + ): + global_buffer = scheduler_buffer.node + assert isinstance(global_buffer, ir.ComputedBuffer) + global_buffer_layout = global_buffer.get_layout() + size_offset = node.outer_loop_fusion_depth - len( + get_call_ranges(scheduler_node) + ) + + def is_all_write_read_contiguous(): + contiguous_index_expr = 0 + stride = 1 + for var, range in reversed( + # pyrefly: ignore [missing-attribute] + scheduler_node._body.var_ranges.items() + ): + contiguous_index_expr += stride * var + stride *= range + # pyrefly: ignore [missing-attribute] + write_index_expr = scheduler_node._body.get_write_expr( + scheduler_buffer.get_name() + ) + + def is_contiguous_index(x): + return x == contiguous_index_expr + + return is_contiguous_index(write_index_expr) and all( + isinstance(user.node, SchedulerNode) + and is_contiguous_index( + user.node._body.get_read_expr( + scheduler_buffer.get_name() + ), + ) + for user in scheduler_buffer.users + ) + + if not ( + global_buffer_layout.is_contiguous() + and is_all_write_read_contiguous() + ): + continue + # Local Buffer is a view of global buffer + local_buffer_stride: list[int] = [] + stride = global_buffer_layout.stride[-1] + local_buffer_size = get_call_ranges(scheduler_node)[ + size_offset: + ] + for sz in reversed(local_buffer_size): + local_buffer_stride.insert(0, stride) + stride *= sz + local_buffer_layout = ir.FixedLayout( + global_buffer_layout.device, + global_buffer_layout.dtype, + local_buffer_size, + local_buffer_stride, + ) + + def try_share_local_buffer(local_buffer_layout, local_buffers): + for local_buf in local_buffers: + if local_buffer_layout == local_buf.layout and all( + all( + user.node.get_name() in visited_scheduler_nodes + for user in V.graph.scheduler.name_to_buf[ + global_buffer.name + ].users + ) + for global_buffer in local_to_global_buffers[ + local_buf.name + ] + if global_buffer.name is not None + ): + return local_buf + return None + + local_buf_prefix = "local_buffer_data" + # Share existing local buffer + local_buffer_used = try_share_local_buffer( + local_buffer_layout, local_buffers + ) + if not local_buffer_used: + # Create new local buffer + local_buffer_used = ir.Buffer( + name=f"{local_buf_prefix}_{len(local_buffers)}", + layout=local_buffer_layout, + ) + local_buffers.append(local_buffer_used) + local_to_global_buffers[local_buffer_used.name] = [] # type: ignore[index] + + local_to_global_buffers[local_buffer_used.name].append( + global_buffer, + ) + + with LocalBufferContext(kernel_group.args) as scope: + if len(local_buffers) > 0: + for local_buffer in local_buffers: + assert local_buffer.name is not None + scope.add_local_buffer( + local_buffer, local_to_global_buffers[local_buffer.name] + ) + for _node in node.get_outer_nodes(): + assert isinstance(_node, (FusedSchedulerNode, SchedulerNode)) + cpp_kernel_proxy = self.kernel_proxy_cls(kernel_group) + cpp_kernel_proxy.codegen_nodes(_node.get_nodes()) # type: ignore[arg-type] + cpp_kernel_proxy_list.append(cpp_kernel_proxy) + nodes_list.append(_node.get_nodes()) # type: ignore[arg-type] + + if not node.check_outer_fusion_loop_level_attr( + cpp_kernel_proxy_list, node.outer_loop_fusion_depth + ): + for removed_buffer in scope.removed_buffers: + # Restore the removed buffers by this context before + # fallback to codegen without using Local Buffer + V.graph.removed_buffers.remove(removed_buffer) + return False + metrics.cpp_outer_loop_fused_inner_counts.append( + metrics.CppOuterLoopFusedCount( + len(cpp_kernel_proxy_list), + local_buffer_number=len(scope.local_buffers), + ) + ) + outer_fusion_cpp_kernel_proxy = node.merge_outer_fusion_kernels( + cpp_kernel_proxy_list, + ) + kernel_group.finalize_kernel( + outer_fusion_cpp_kernel_proxy, + [*itertools.chain.from_iterable(nodes_list)], + ) + + return True + + if not try_outer_loop_fusion_with_local_buf(node): + # Reset generated_cpp_vec_kernel_count to codegen again + metrics.generated_cpp_vec_kernel_count = generated_cpp_vec_kernel_count + cpp_kernel_proxy_list.clear() + nodes_list.clear() + # Similar as comment in + # https://github.com/pytorch/pytorch/blob/469383755fe416eb1c41fa724762ad3eaecdff07/torch/_inductor/codegen/cpp.py#L3269-L3272 + # Kernels share the same global contexts like V.graph.wrapper_code, V.kernel.args. + with torch._inductor.config.patch(inplace_buffers=False): + for _node in node.get_outer_nodes(): + assert isinstance(_node, (FusedSchedulerNode, SchedulerNode)) + _nodes: list[SchedulerNode] = _node.get_nodes() # type: ignore[assignment] + cpp_kernel_proxy = self.kernel_proxy_cls(kernel_group) + cpp_kernel_proxy.codegen_nodes(_nodes) + kernel_group.finalize_kernel(cpp_kernel_proxy, _nodes) + + def codegen_node( + self, + node: OuterLoopFusedSchedulerNode | FusedSchedulerNode | SchedulerNode, + ): + """ + Turn an set of pre-fused nodes into a C++ kernel. + """ + kernel_group = self.kernel_group + + if isinstance(node, OuterLoopFusedSchedulerNode): + self.codegen_outer_loop_node(node) + else: + nodes: list[SchedulerNode] = node.get_nodes() # type: ignore[assignment] + nodes = self.try_loop_split(nodes) + cpp_kernel_proxy = self.kernel_proxy_cls(kernel_group) + cpp_kernel_proxy.codegen_nodes(nodes) + kernel_group.finalize_kernel(cpp_kernel_proxy, nodes) + + args_num = self._get_scheduled_num_args() + if args_num > CppScheduling.MAX_FUSED_KERNEL_ARGS_NUM: + self._set_flush_status(True) + + def is_cpp_template(self, node: BaseSchedulerNode) -> bool: + return isinstance(node, SchedulerNode) and isinstance( + node.node, ir.CppTemplateBuffer + ) + + def codegen_template( + self, + template_node: BaseSchedulerNode, + epilogue_nodes: Sequence[BaseSchedulerNode], + prologue_nodes: Sequence[BaseSchedulerNode], + ): + """ + Codegen a CPP template, possibly with fused epilogues + """ + assert not prologue_nodes + + # remove MultiOutput from epilogue_nodes + epilogue_nodes = [ + epilogue_node + for epilogue_node in epilogue_nodes + if isinstance(epilogue_node, (SchedulerNode, FusedSchedulerNode)) + ] + # The counter cpp_templated_kernel_counter is used for verifying if a + # a templated kernel was successfully compiled in a UT + counters["inductor"]["cpp_templated_kernel_counter"] += 1 + counters["inductor"]["cpp_epilogue_fusion_counter"] += len(epilogue_nodes) + assert self.is_cpp_template(template_node), ( + "Template node passed to CppScheduler.codegen_template must be a SchedulerNode that wraps a CppTemplateBuffer" + ) + template_node = cast(SchedulerNode, template_node) + _, (_, rnumel) = template_node.group + assert rnumel == () + ctb: ir.CppTemplateBuffer = cast(ir.CppTemplateBuffer, template_node.node) + epilogue_ir_nodes: list[ir.Operation | None] = [n.node for n in epilogue_nodes] + assert all(isinstance(n, ir.ComputedBuffer) for n in epilogue_ir_nodes), ( + "Epilogue nodes must all be instances of ir.ComputedBuffer" + ) + + def template_buffer_has_other_users( + template_buffer, outputs_by_name, epilogue_nodes + ): + if not epilogue_nodes: + return False + + assert template_buffer.get_name() in outputs_by_name + users = outputs_by_name[template_buffer.get_name()].users + return not all( + isinstance(user.node, BaseSchedulerNode) + and user.node.node in epilogue_nodes + for user in users + ) + + flag_template_buffer_has_other_users = template_buffer_has_other_users( + ctb, template_node.outputs_by_name, epilogue_ir_nodes + ) + kernel, render = ctb.make_kernel_render( # type: ignore[misc] + ctb, + flag_template_buffer_has_other_users=flag_template_buffer_has_other_users, + epilogue_nodes=epilogue_ir_nodes, + ) + with kernel: + if not is_multi_outputs_template(template_node.node): + template_node.mark_run() # type: ignore[attr-defined] + for node in epilogue_nodes: + node.mark_run() # type: ignore[attr-defined] + src_code = render() + + with V.set_kernel_handler(kernel): + node_schedule = [template_node, *epilogue_nodes] + kernel_name = self.define_kernel(src_code, node_schedule, kernel.args) + + if is_multi_outputs_template(template_node.node): + # For multi outputs template, allocate buffers for each output after the epilogue + # codegen to which determines if the buffer has been removed. + assert len(template_node.outputs) == 1, ( + "Multi outputs template should be with 1 output template buffer of MultiOutputLayout" + ) + for user in template_node.outputs[0].users: + assert isinstance(user.node, ExternKernelSchedulerNode), ( + "Multi outputs template should be with ExternKernelSchedulerNode" + ) + assert isinstance(user.node.node, ir.MultiOutput), ( + "Multi outputs template has multi users with MultiOutput" + ) + user.node.mark_run() + + self.codegen_comment(node_schedule, kernel_name) + kernel.call_kernel(kernel_name, ctb) + V.graph.removed_buffers |= kernel.removed_buffers + self.free_buffers_in_scheduler() + + def _get_scheduled_num_args(self): + return self.kernel_group.get_num_args() + + def ready_to_flush(self): + return self._ready_to_flush + + def codegen_sync(self): + pass + + def define_kernel(self, src_code, nodes, kernel_args=None): + wrapper = V.graph.wrapper_code + if src_code in wrapper.src_to_kernel: + kernel_name = wrapper.src_to_kernel[src_code] + else: + fused_name = ( + get_fused_kernel_name(nodes, config.cpp.descriptive_names) + if config.cpp.descriptive_names + else "" + ) + kernel_name = "_".join(["cpp", fused_name, wrapper.next_kernel_suffix()]) + wrapper.src_to_kernel[src_code] = kernel_name + kernel_decl_name = kernel_name if V.graph.cpp_wrapper else "kernel" + src_code = src_code.replace(str(Placeholder.KERNEL_NAME), kernel_decl_name) + src_code = src_code.replace(str(Placeholder.DESCRIPTIVE_NAME), kernel_name) + # TODO(voz): Ostensibly, we should not need this. But there are cases where C++ codegen does + # not use BracesBuffer, so we have no good indicator of a C++ buffer atm. + src_code = src_code.replace("#pragma CMT", "//") + + # Get the lines in the source code representing the function definition, + # excluding the first line including cpp_prefix.h. + first_char = src_code.rfind('extern "C"') + last_char = src_code.find(")", first_char) + if _IS_WINDOWS: + # get_export_declaration introduced one more ')' in Windows + last_char = src_code.find(")", last_char + 1) + kernel_definition = f"{src_code[first_char : last_char + 1]};\n" + + compile_wrapper = IndentedBuffer() + args = self.kernel_group.args if kernel_args is None else kernel_args + _, _, arg_types = args.cpp_argdefs() + if not V.graph.cpp_wrapper: + compile_wrapper.writeline( + f"async_compile.cpp_pybinding({arg_types!r}, r'''" + ) + compile_wrapper.splice(src_code, strip=True) + if not V.graph.cpp_wrapper: + compile_wrapper.writeline("''')") + wrapper.define_kernel( + kernel_name, + compile_wrapper.getvalue(), + gpu=False, + cpp_definition=kernel_definition, + ) + return kernel_name + + def flush(self): + src_code = self.kernel_group.codegen_group() + if src_code: + kernel_name = self.define_kernel( + src_code, self.kernel_group.scheduled_nodes + ) + self.codegen_comment(self.kernel_group.scheduled_nodes, kernel_name) + if config.cpp.enable_kernel_profile: + V.graph.wrapper_code.write_kernel_context_guard_begin() + V.graph.wrapper_code.write_kernel_context_guard( + kernel_name, + self.kernel_group.scheduled_nodes, # type: ignore[arg-type] + ) + self.kernel_group.call_kernel(V.graph.wrapper_code, kernel_name) + if config.cpp.enable_kernel_profile: + V.graph.wrapper_code.write_kernel_context_guard_end() + + self.reset_kernel_group() + self._set_flush_status(False) + + def codegen_comment(self, node_schedule, kernel_name=None): + # below add provenance tracing info for cpu CppKernel types + wrapper = V.graph.wrapper_code + debug_handle = set_kernel_post_grad_provenance_tracing( + node_schedule, # type: ignore[arg-type] + # pyrefly: ignore [bad-argument-type] + kernel_name, + ) + wrapper.write_provenance_debug_handle(kernel_name, debug_handle) + + +class KernelGroup: + def __init__(self): + super().__init__() + self.args = KernelArgs() + self.loops_code = BracesBuffer() + self.ws = WorkSharing(self.loops_code) + self.stack = contextlib.ExitStack() + self.stack.enter_context(self.ws) + self.scheduled_nodes = [] + + def new_kernel(self, cls, *args): + return cls(self.args, parallel_num_threads(), *args) + + def finalize_kernel(self, new_kernel, nodes): + self.scheduled_nodes += nodes + code = self.loops_code + ws = self.ws + new_kernel.codegen_loops(code, ws) + + def get_num_args(self): + arg_defs, _call_args, _arg_types = self.args.cpp_argdefs() + args_num = len(arg_defs) + return args_num + + def codegen_group(self, name=None) -> str: + self.stack.close() + if not self.scheduled_nodes: + return "" + code = BracesBuffer() + # 1. Include header files + # TODO: support kernel profile on other platforms + enable_kernel_profile = config.cpp.enable_kernel_profile and sys.platform in [ + "linux", + "win32", + ] + if enable_kernel_profile: + code.writelines(["#include "]) + code.writeline("#include ") + + # 2. Function definition + kernel_decl_name = str(Placeholder.KERNEL_NAME) if name is None else name + kernel_name = str(Placeholder.DESCRIPTIVE_NAME) if name is None else name + arg_defs, _, _ = self.args.cpp_argdefs() + arg_defs = ",\n".ljust(25).join(arg_defs) + func_export_decl = get_export_declaration() + inline_attr = ( + "C10_ALWAYS_INLINE_ATTRIBUTE" if config.cpp.force_inline_kernel else "" + ) + code.writeline( + f'extern "C" {func_export_decl} void {inline_attr} {kernel_decl_name}({arg_defs})' + ) + + # 3. Function body + with code.indent(): + if enable_kernel_profile: + graph_id = V.graph.graph_id + prefix = "graph_" + str(graph_id) + "_" if graph_id is not None else "" + code.writelines( + [ + ( + "torch::aot_inductor::RAIIAtenRecordFunctionHandle " + f'record_{prefix + kernel_name}_("{prefix + kernel_name}", nullptr);' + ) + ] + ) + for old, new in self.args.aliases(): + code.writeline(f"auto {old} = {new};") + code.splice(self.loops_code) + return code.getvalue() + + def call_kernel(self, wrapper, kernel_name): + _, call_args, arg_types = self.args.cpp_argdefs() + wrapper.generate_kernel_call( + kernel_name, + call_args, + triton=False, + arg_types=arg_types, + ) + + +class WorkSharing: + def __init__(self, code): + self.code = code + self.in_parallel = False + self.num_threads = None + self.stack = contextlib.ExitStack() + + def parallel(self, threads): + if self.in_parallel and threads != self.num_threads: + # wrong number of threads + self.close() + if not self.in_parallel: + self.num_threads = threads + self.in_parallel = True + # Decide whether to use dynamic threading + use_dynamic = False + if config.cpp.threads >= 1: + # User explicitly set config.cpp.threads (hardcode it) + use_dynamic = False + elif threads == os.cpu_count(): + # Thread count matches system CPU count (most likely default, use dynamic) + use_dynamic = True + else: + # Thread count differs from system (user probably set it so hardcode) + use_dynamic = False + + if use_dynamic or config.cpp.dynamic_threads: + self.code.writeline("#pragma omp parallel") + else: + self.code.writeline(f"#pragma omp parallel num_threads({threads})") + self.stack.enter_context(self.code.indent()) + self.code.writeline( + "int tid = omp_get_thread_num();", + ) + + def single(self): + if self.in_parallel: + self.code.writeline("#pragma omp single") + return self.in_parallel + + def close(self): + self.stack.close() + self.in_parallel = False + + def __enter__(self): + self.stack.__enter__() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.stack.__exit__(exc_type, exc_val, exc_tb) + + +@dataclasses.dataclass +class LoopLevel: + var: sympy.Expr | None = None + size: sympy.Expr | None = None + offset: sympy.Expr = sympy.S.Zero + # Note [tiled_size] + # We may do loop-tiling at this loop level. + # When var is in [offset, tiled_size), we will perform the vectorization kernel. + # When var is in [tiled_size, size), we will perform the scalar or masked vectorization kernel. + # for (var = offset; var < size; var += steps) { + # if (var >= offset && var < tiled_size) vec_loop_body(); + # if (var >= tiled_size && var < size) scalar_or_maskvec_loop_body(); + # } + tiled_size: sympy.Expr = sympy.S.Zero + steps: sympy.Expr = sympy.S.One + parallel: int = 0 + simd_omp: bool = False + simd_vec: bool = False + collapsed: bool = False + is_reduction: bool = False + + def __post_init__(self): + # Regarding the C++/OpenMP backend, `cpu_vec_isa.pick_vec_isa()` to check + # vectorization ISA is a time-consuming and one-shot operation. It leads + # to taking a longer time to import `codegen.cpp` package because the + # `LoopLevel` of the package is decorated by `@dataclasses.dataclass` while + # the decorator will invoke `cpu_vec_isa.pick_vec_isa()` to initialize the + # `simd_nelements` of the `LoopLevel`. It might introduce additional compilation + # overhead to the Triton backend. Therefore, we moved the `simd_nelements` to + # `__post_init__` + picked_vec_isa: cpu_vec_isa.VecISA = cpu_vec_isa.pick_vec_isa() + self.simd_nelements: int = picked_vec_isa.nelements() if picked_vec_isa else 0 + + def tile(self, factor): + sympy_factor = sympy.Integer(factor) + loop = LoopLevel(self.var, self.size) + loop.steps = sympy_factor + loop.simd_vec = True + loop.tiled_size = FloorDiv(loop.size, sympy_factor) * sympy_factor + loop.parallel = self.parallel + loop.collapsed = False + loop.is_reduction = self.is_reduction + return loop + + def lines(self): + offset_expr = cexpr_index(self.offset) + size_expr = cexpr_index(self.size) + if config.cpp.no_redundant_loops and offset_expr == size_expr: + return None + simd = ( + f"simd simdlen({self.simd_nelements}) " + if self.simd_omp and self.simd_nelements > 1 + else "" + ) + if self.parallel: + # TODO(jansel): look into chunk size and other schedules + line1 = "#pragma omp for" + if self.parallel > 1: + line1 += f" collapse({self.parallel})" + if self.simd_omp: + line1 = line1.replace(" for ", f" for {simd}") + elif self.simd_vec: + line1 = "" + elif self.simd_omp: + line1 = f"#pragma omp {simd}" + elif not self.is_reduction and cpp_builder.is_gcc(): + line1 = "#pragma GCC ivdep" + else: + line1 = "" + offset_str = f"{INDEX_TYPE} {self.var}={offset_expr}" + size_str = f"{self.var}<{size_expr}" + if self.steps.is_number: + steps_str = f"{self.var}+={cexpr_index(self.steps)}" + else: + # If the step size is 0, change it to 1 because a step size of 0 + # will cause floating point exception (core dump) during parallelization. + steps_str = ( + f"{self.var}+=({cexpr_index(self.steps)} == 0 ? " + f"1 : {cexpr_index(self.steps)})" + ) + line2 = f"for({offset_str}; {size_str}; {steps_str})" + if self.collapsed or not line1: + return [line2] + return [line1, line2] + + +@dataclasses.dataclass +class LoopNest: + """ + A loop-nest-like structure. It is built with the `build` method + as a loop nest and then will perform loop-tiling at some depth. + + A typical case is for vectorization, where we typically do loop-tiling + at the innermost loop level. A more complicated case is when we do + 2D tiling at both the innermost and outer levels. + """ + + loops: list[LoopLevel] | None = None + kernel: CppKernel | None = None + + @staticmethod + def build(kernel: CppKernel): + """Build a LoopNest with the given `kernel` as the leaf""" + itervars = kernel.itervars + ranges = kernel.ranges + reduction_depth = kernel.reduction_depth + assert reduction_depth is not None + + loops: list[LoopLevel] | None = None + for loop_idx, (var, size) in enumerate(zip(itervars, ranges)): + loop = LoopLevel(var, size) + if not loops: + loops = [loop] + else: + loops.append(loop) + if loop_idx >= reduction_depth: + loop.is_reduction = kernel.is_reduction + + loop_nest = LoopNest(loops) + return loop_nest + + def __bool__(self): + return bool(self.loops) + + @cache_on_self + def max_parallel_depth(self): + """ + Maximal allowed depth for parallelism: All reduction or non-reduction levels. + When the range of the first inner loop beyond the maximum parallel depth is much + larger than the range of all outer loops within the maximum parallel depth, + change the starting depth of parallelism to the first inner loop and recalculate + the maximum parallel depth. + """ + if self.loops is None: + return ParallelDepth(parallel_depth=0, start_depth=0) + + start_depth = 0 + max_depth = 0 + is_reduction = self.loops[0].is_reduction + num_steps = sympy.Integer(1) + for loop in self.loops: + if loop.is_reduction != is_reduction: + break + num_steps = num_steps * FloorDiv(loop.size, loop.steps) + max_depth += 1 + + def get_simd_vec_depth(loops): + # Return the first loop level which is simd_vec + for i, loop in enumerate(loops): + if loop.simd_vec: + return i + return None + + simd_vec_depth = get_simd_vec_depth(self.loops) + + def has_scalar_kernel(loop_nest: LoopNest): + assert isinstance(loop_nest.kernel, CppKernelProxy) + return any( + not isinstance(kernel, CppVecKernel) + for kernel in loop_nest.kernel.kernels + ) + + # When the number of steps of the first inner loop is much larger than the number of steps of + # all outer loops, change `start_depth` to the first inner loop and recalculate `max_depth`. + if ( + max_depth < len(self.loops) + and isinstance(num_steps, sympy.Integer) + and isinstance(self.loops[max_depth].size, sympy.Integer) + and num_steps * 300 + < FloorDiv(self.loops[max_depth].size, self.loops[max_depth].steps) + and not ( + # Disable parallel reduction under the vec loop + simd_vec_depth is not None + and max_depth > simd_vec_depth + and self.loops[max_depth].is_reduction + and has_scalar_kernel(self) + ) + ): + start_depth = max_depth + max_depth = 0 + is_reduction = self.loops[start_depth].is_reduction + for i in range(start_depth, len(self.loops)): + if self.loops[i].is_reduction != is_reduction: + break + max_depth += 1 + return ParallelDepth(parallel_depth=max_depth, start_depth=start_depth) + + def mark_parallel(self, par_depth): + assert par_depth.parallel_depth <= self.max_parallel_depth().parallel_depth, ( + "Parallel depth cannot exceed the maximal allowed parallel depth" + ) + assert self.loops is not None + assert len(self.loops) >= par_depth.parallel_depth + loop = self.loops[par_depth.start_depth] + loop.parallel = par_depth.parallel_depth + if loop.is_reduction: + # pyrefly: ignore [bad-assignment] + metrics.parallel_reduction_count += 1 + for i in range(par_depth.start_depth + 1, par_depth.parallel_depth): + self.loops[i].collapsed = True + + def tile(self, depth, factor): + """ + Do loop-tiling at the `depth` level with `factor`. + for (x0 = 0; x0 < x0_end; x0++) + -> + for (x0 = 0; x0 < x0_end; x0 += factor) + See details in Note [tiled_size]. + """ + assert self.loops + self.loops[depth] = self.loops[depth].tile(factor) + return self.loops[depth] + + def get_kernel(self) -> CppKernel: + assert self.kernel + return self.kernel + + def set_kernel(self, kernel): + self.kernel = kernel + + def from_loop_level(self, level: int): + assert self.loops + assert len(self.loops) >= level + loops = None if level == len(self.loops) else self.loops[level:] + return LoopNest(loops, self.kernel) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_bmm_template.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_bmm_template.py new file mode 100644 index 0000000000000000000000000000000000000000..e516a3ac311469a955e83c7789b4c66e39a9abda --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_bmm_template.py @@ -0,0 +1,268 @@ +# mypy: allow-untyped-defs +import contextlib +import itertools +from collections.abc import Callable +from typing import Any +from unittest.mock import patch + +import sympy + +from .. import ir +from ..select_algorithm import PartialRender +from ..virtualized import V +from .common import ArgName +from .cpp_gemm_template import CppGemmTemplate, GEMM_TEMPLATE +from .cpp_micro_gemm import LayoutType +from .cpp_template_kernel import CppTemplateKernel +from .cpp_utils import DTYPE_TO_CPP, GemmBlocking + + +# We pass all sizevars present in BY to the GEMM templates so variables are not renamed in the BMM definition +GEMM_SINGLE_THREAD_MM_STUB = r""" +{{kernel.def_kernel( + inputs={"X": X, "W": W}, + outputs={"Y": Y_2d}, + aliases=aliases, + function_name=kernel_name+"_single_thread_mm", + extra_sizevars=BY_sizevars + [b_index], + placeholder="")}}""" + +GEMM_THREADED_MM_STUB = r""" +{{kernel.def_kernel( + inputs={"X": X, "W": W}, + outputs={"Y": Y_2d}, + aliases=aliases, + function_name=kernel_name+"_threaded_mm", + extra_sizevars=BY_sizevars + [b_index], + placeholder="")}}""" + +BMM_TEMPLATE = r""" +{{ template.codegen_microkernel_def() }} +{{ template.codegen_single_thread_gemm() }} +{{ template.codegen_multi_thread_gemm() }} + +extern "C" +{{kernel.def_kernel(inputs={"X": BX, "W": BW}, outputs={"Y": BY}, aliases=aliases)}} +{ + const int64_t B = {{kernel.size(BY_2d, 0)}}; + {%- if num_threads > 1 %} + constexpr int64_t num_threads = {{num_threads}}; + int64_t B_single_thread_block = (B / num_threads) * num_threads; + + {%- set use_dynamic_threads = ((config.cpp.threads < 1) and (num_threads == cpu_count)) or config.cpp.dynamic_threads %} + {%- if use_dynamic_threads %} + #pragma omp parallel for + {%- else %} + #pragma omp parallel for num_threads({{num_threads}}) + {%- endif %} + {%- else %} + int64_t B_single_thread_block = B; + {%- endif %} + for (int64_t b_start = 0; b_start < B_single_thread_block; ++b_start) { + {{template.get_gemm_function_call( + kernel, + kernel_name+"_single_thread_mm", + "", + b_index="b_start", + )}} + } + for (int64_t b_start = B_single_thread_block; b_start < B; ++b_start) { + {{template.get_gemm_function_call( + kernel, + kernel_name+"_threaded_mm", + "", + b_index="b_start", + )}} + } +} +""" + + +class CppBmmTemplate(CppGemmTemplate): + def __init__( + self, + input_nodes, + layout: ir.Layout, + num_threads: int, + register_blocking: GemmBlocking, + beta=1, + alpha=1, + has_bias=False, + epilogue_creator: Callable[[ir.Buffer], ir.Pointwise] | None = None, + should_block_weights: bool = False, + name="bmm", + ): + """ + In order to simplify the implementation and increase code reuse, the BMM template implements + two versions of the GEMM kernel: a single-threaded version and a multi-threaded version. + GEMM kernels are called in a loop over the batch dimension, with single-threaded GEMM calls + for all but the last (B % num_threads), which are handled by the multi-threaded GEMM kernel. + + We use an extra sizevar `b_index` to index the batch dimension, which we pass into the GEMM + template as a sympy.Symbol. This allows us to slice the 3D batch tensors in the GEMM template + without any changes to the GEMM template itself. + """ + super().__init__( + input_nodes, + layout, + num_threads, + register_blocking, + beta=beta, + alpha=alpha, + has_bias=has_bias, + epilogue_creator=epilogue_creator, + should_block_weights=should_block_weights, + name=name, + ) + self.b_index = sympy.Symbol("s_b_index", integer=True, nonnegative=True) + + @staticmethod + def get_padded_size(n, block_n, k, should_block_weight): + if should_block_weight: + # Tensor is constant or not contiguous, so we will pad and block + new_size, padded_n = CppGemmTemplate.get_padded_size( + n, block_n, k, should_block_weight + ) + # Add the new batch dimension + new_size.insert(0, -1) + return new_size, padded_n + else: + new_size = [-1, k, n] + return new_size, n + + @staticmethod + def check_if_block_weight(W, micro_gemm): + assert isinstance(W, ir.IRNode) + _, n = W.get_size()[-2:] + result = ( + not W.get_layout().is_contiguous() + or W.get_name() in V.graph.constants + or ( + n % micro_gemm.register_blocking.block_n != 0 + and micro_gemm.get_b_layout != LayoutType.NORMAL + ) + ) + return result + + def get_gemm_function_call( + self, + kernel: CppTemplateKernel, + function_name: str, + placeholder: str, + b_index: str, + ) -> str: + """ + Similar to 'def_kernel' in cpp_template_kernel, but instead of generating a function definition, + generate a function call for the GEMM kernel. + Args: + placeholder: The string to replace the function call with + b_index: The index for slicing the 3D batch tensors + """ + + def hook(): + arg_defs, call_args, _, _ = kernel.args.python_argdefs() + for i, buf in enumerate(call_args): + if buf == self.b_index: + arg_defs[i] = ArgName(b_index) + call = f"{function_name}({', '.join(x.full_name() for x in arg_defs)});" + return call + + assert placeholder not in kernel.render_hooks + kernel.render_hooks[placeholder] = hook + return placeholder + + def get_default_reindexers(self, epilogue_nodes): + def reindexer(args): + # if epilogue nodes exist, they have 3D ranges but args are 2D, so add 0 index + return [self.b_index] + args + + return [reindexer] * len(epilogue_nodes) + + def get_options( + self, + kernel: CppTemplateKernel, + template_buffer_node: ir.CppTemplateBuffer | None = None, + flag_template_buffer_has_other_users: bool | None = None, + epilogue_nodes: list[ir.IRNode] | None = None, + **kwargs, + ) -> dict[str, Any]: + options = super().get_options( + kernel=kernel, + template_buffer_node=template_buffer_node, + flag_template_buffer_has_other_users=flag_template_buffer_has_other_users, + epilogue_nodes=epilogue_nodes, + **kwargs, + ) + + BX, BW, BY = options["X"], options["W"], options["Y"] + options["BX"], options["BW"], options["BY"] = BX, BW, BY + options["BY_2d"] = options["Y_2d"] + for kword in ["X", "W", "GemmOut", "Y_2d"]: + options[kword] = kernel.select(options[kword], 0, self.b_index) + for kword in ["X", "W", "Y_2d"]: + options[kword + "_dtype"] = DTYPE_TO_CPP[options[kword].dtype] + options["b_index"] = self.b_index + options["BY_sizevars"] = [ + s + for sym in itertools.chain(BY.get_size(), BY.get_stride()) + if isinstance(sym, sympy.Expr) + for s in sym.free_symbols + ] + options["kernel_name"] = kernel.kernel_name + + return options + + def render( # type: ignore[override, return] + self, + kernel: CppTemplateKernel, + template_buffer_node: ir.CppTemplateBuffer | None = None, + flag_template_buffer_has_other_users: bool | None = None, + epilogue_nodes: list[ir.IRNode] | None = None, + **kwargs, + ) -> str: + options = self.get_options( + kernel=kernel, + template_buffer_node=template_buffer_node, + flag_template_buffer_has_other_users=flag_template_buffer_has_other_users, + epilogue_nodes=epilogue_nodes, + **kwargs, + ) + self.render_options = options + + with contextlib.ExitStack() as stack: + for buf in options["fake_buffers"]: + stack.enter_context( + patch.object(V.graph, "get_dtype", self._fake_get_dtype(buf)) + ) + result = self._template_from_string(BMM_TEMPLATE).render(**options) + + # Finalize the function definitions for the gemm routines + sub_mm_hooks = { + name: hook + for name, hook in kernel.render_hooks.items() + if "FOR_BMM" in name + } + result = PartialRender(result, sub_mm_hooks).finalize_all() + for name in sub_mm_hooks: + del kernel.render_hooks[name] + del kernel.args.sizevars[options["b_index"]] + return result + + def codegen_single_thread_gemm(self): + stub = self._template_from_string(GEMM_SINGLE_THREAD_MM_STUB).render( + self.render_options + ) + return stub + self._template_from_string(GEMM_TEMPLATE).render( + {**self.render_options, "num_threads": 1} + ) + + def codegen_multi_thread_gemm(self): + stub = self._template_from_string(GEMM_THREADED_MM_STUB).render( + self.render_options + ) + return stub + self._template_from_string(GEMM_TEMPLATE).render( + self.render_options + ) + + def codegen_gemm_stub_def(self): + return "" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_flex_attention_template.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_flex_attention_template.py new file mode 100644 index 0000000000000000000000000000000000000000..30104954bb1881e99978e1f0f199b8c75dd5cdef --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_flex_attention_template.py @@ -0,0 +1,1089 @@ +# mypy: allow-untyped-defs +import contextlib +import logging +import re +from unittest.mock import patch + +import sympy + +import torch +import torch.utils + +from ...utils._ordered_set import OrderedSet +from .. import ir +from ..ir import TensorBox +from ..select_algorithm import DataProcessorTemplateWrapper +from ..utils import parallel_num_threads +from ..virtualized import V +from .cpp_template import CppTemplate +from .cpp_utils import GemmBlocking + + +log = logging.getLogger(__name__) + +# TODO: reuse cpp codegen to generate below pointwise/reduction kernels +SOFTMAX_FUSIONS = r""" +// 1) out = exp(a - val) +// 2) val = sum(out) +template +inline void {{kernel_name}}_exp_reduce_sum_fusion_kernel( + T1* a, + const int& size, + T2* out, + T1& val) { + auto vec_size = at::vec::Vectorized::size(); + auto vec_max = at::vec::Vectorized(val); + T1 tmp_sum = 0; + auto vec_tmp_sum = at::vec::Vectorized(tmp_sum); + for (long i = 0; i < vec_size * (size / vec_size); i += vec_size) { + auto tmp0 = at::vec::Vectorized::loadu(a + i); + auto tmp1 = tmp0 - vec_max; + auto tmp2 = tmp1.exp_u20(); + vec_tmp_sum += tmp2; + at::native::_store(out + i, tmp2); + } + tmp_sum = at::vec::vec_reduce_all( + [](at::vec::Vectorized& x, at::vec::Vectorized& y) { + return x + y; + }, + vec_tmp_sum); + for (long i = vec_size * (size / vec_size); i < size; i++) { + auto tmp0 = a[i]; + auto tmp1 = tmp0 - val; + auto tmp2 = exp(tmp1); + tmp_sum += tmp2; + out[i] = tmp2; + } + val = tmp_sum; +} + +// 1) out = a * scale +// 2) max = max(out) +template +inline void {{kernel_name}}_mul_reduce_max_fusion_kernel( + const scalar_t* a, + const scalar_t& scale, + const int& size, + scalar_t* out, + scalar_t& max) { + auto vec_size = at::vec::Vectorized::size(); + auto vec_scale = at::vec::Vectorized(scale); + scalar_t tmp_max = -std::numeric_limits::infinity(); + auto vec_tmp_max = at::vec::Vectorized(tmp_max); + for (long i = 0; i < vec_size * (size / vec_size); i += vec_size) { + auto tmp0 = at::vec::Vectorized::loadu(a + i); + auto tmp1 = tmp0 * vec_scale; + vec_tmp_max = at::vec::maximum(vec_tmp_max, tmp1); + at::native::_store(out + i, tmp1); + } + for (long i = vec_size * (size / vec_size); i < size; i++) { + auto tmp0 = a[i]; + auto tmp1 = tmp0 * scale; + tmp_max = std::max(tmp_max, tmp1); + out[i] = tmp1; + } + max = std::max( + tmp_max, + at::vec::vec_reduce_all( + [](at::vec::Vectorized& x, at::vec::Vectorized& y) { + return at::vec::maximum(x, y); + }, + vec_tmp_max)); +} + +template +static inline scalar_t* {{kernel_name}}_conditional_data_ptr(scalar_t* ptr, scalar_t* ptr2) { + TORCH_CHECK(ptr2 == nullptr); + return ptr; +} + +template , int> = 0> +static inline scalar_t* {{kernel_name}}_conditional_data_ptr(float* ptr, scalar_t* ptr2) { + return ptr2; +} + +template +inline void {{kernel_name}}_fill_stub(scalar_t* data, scalar_t val, int64_t size) { + using Vec = at::vec::Vectorized; + Vec data_vec = Vec(val); + int64_t d = 0; + for (; d < size - (size % Vec::size()); d += Vec::size()) { + data_vec.store(data + d); + } + #if !defined(_MSC_VER) && !defined(COMPILING_FOR_MIN_SIZE) + # pragma unroll + #endif + for (; d < size; d++) { + data[d] = val; + } +} + +// out = a * scale +template +inline void {{kernel_name}}_mul_scale_kernel( + scalar_t* a, + scalar_t scale, + int64_t size) { + auto vec_size = at::vec::Vectorized::size(); + auto vec_scale = at::vec::Vectorized(scale); + for (int64_t i = 0; i < vec_size * (size / vec_size); i += vec_size) { + auto tmp0 = at::vec::Vectorized::loadu(a + i); + auto tmp1 = tmp0 * vec_scale; + at::native::_store(a + i, tmp1); + } + for (int64_t i = vec_size * (size / vec_size); i < size; i++) { + auto tmp0 = a[i]; + auto tmp1 = tmp0 * scale; + a[i] = tmp1; + } +} + +""" + +BRGEMM_PACK_FUNCTIONS = r""" +template +inline void {{kernel_name}}_copy_value_with_pad( + const scalar_t* value_ptr, + scalar_t* dst_ptr, + int64_t rows, + int64_t cols, + int64_t prows, + int64_t pcols, + int64_t ldi) { + auto vec_size = at::vec::Vectorized::size(); + int64_t i = 0; + for (; i < rows; i++) { + int64_t j = 0; + for (; j < cols - (cols % vec_size); j += vec_size) { + auto vec_v = + at::vec::Vectorized::loadu(value_ptr + i * ldi + j); + vec_v.store(dst_ptr + i * pcols + j); + } + + if (j < cols) { + auto vec_v = at::vec::Vectorized::loadu( + value_ptr + i * ldi + j, cols - j); + vec_v.store(dst_ptr + i * pcols + j, cols - j); + } + + // col padding + auto psize = pcols - cols; + if (psize > 0) { + auto zero_vec = at::vec::Vectorized(0); + int64_t pj = 0; + for (; pj < psize - (psize % vec_size); pj += vec_size) { + zero_vec.store(dst_ptr + i * pcols + cols + pj); + } + if (pj < psize) { + zero_vec.store(dst_ptr + i * pcols + cols + pj, psize - pj); + } + } + } + // row padding + for (; i < prows; i++) { + auto zero_vec = at::vec::Vectorized(0); + int64_t j = 0; + for (; j < pcols - (pcols % vec_size); j += vec_size) { + zero_vec.store(dst_ptr + i * pcols + j); + } + if (j < pcols) { + zero_vec.store(dst_ptr + i * pcols + j, pcols - j); + } + + } +} +""" + +MICRO_GEMM_TEMPLATE = r""" +GEMM_DEFINE +""" + +ALLOCATE_BUFFER = r""" + int64_t {{buffer_name}}_dtype_itemsize = c10::is_reduced_floating_point_v<{{buffer_dtype}}> ? 2 : 4; + auto& {{buffer_name}}_allocator = *at::getCPUAllocator(); + auto {{buffer_name}}_work_data = {{buffer_name}}_allocator.allocate({{buffer_size}}*{{buffer_name}}_dtype_itemsize); + void* {{buffer_name}}_data_ptr = {{buffer_name}}_work_data.get(); + {{buffer_dtype}}* {{buffer_name}} = ({{buffer_dtype}}*){{buffer_name}}_data_ptr; +""" + +FLEX_ATTENTION_TEMPLATE = r""" +{{template.header().getvalue()}} +#include +#include +#include +{{template.codegen_micro_gemm(kernel.kernel_name)}} +{{template.codegen_softmax_fusion(kernel.kernel_name)}} +{{template.codegen_brgemm_pack_function(kernel.kernel_name)}} +{%- set kernel_args = {"query": query, "key": key, "value": value, + "kv_num_blocks": kv_num_blocks, "kv_indices": kv_indices, + "full_kv_num_blocks": full_kv_num_blocks, "full_kv_indices": full_kv_indices } %} +{%- set kernel_args = template.update_kernel_args(kernel_args) %} + +extern "C" +{{kernel.def_kernel(inputs=kernel_args, outputs={"output": output}, extra_sizevars=template.extra_sizevars)}} +{ + {{ kernel.maybe_codegen_profile() }} + int64_t qBlockSize = {{qBlockSize}}; + int64_t kvBlockSize = {{kvBlockSize}}; + int64_t num_thread = {{num_thread}}; + + // dtypes of kernel and internal buffers + using scalar_t = {{kernel.dtype(query)}}; + constexpr bool is_reduced_type = c10::is_reduced_floating_point_v; + using accum_t = at::opmath_type<{{kernel.dtype(query)}}>; + using Vec = at::vec::Vectorized; + accum_t scaling_factor = {{scale}}; + int64_t batchSize = {{kernel.size(query, 0)}}; + int64_t qSize = {{kernel.size(query, 1)}}; + int64_t num_head = {{kernel.size(query, 2)}}; + int64_t headSize = {{kernel.size(query, 3)}}; + int64_t batchSize_k = {{kernel.size(key, 0)}}; + int64_t num_head_k = {{kernel.size(key, 2)}}; + int64_t headSize_v = {{kernel.size(value, 3)}}; + bool is_broadcast_bs_kv = batchSize != batchSize_k; + bool is_broadcast_head_kv = num_head != num_head_k; + int64_t gqa_shards = num_head / num_head_k; + int64_t bs_shards = batchSize / batchSize_k; + + int64_t batchSize_kvi = {{kernel.size(kv_indices, 0)}}; + int64_t num_head_kvi = {{kernel.size(kv_indices, 1)}}; + int64_t block_num_kvi = {{kernel.size(kv_indices, 3)}}; + bool is_broadcast_bs_kvi = batchSize != batchSize_kvi; + bool is_broadcast_head_kvi = num_head != num_head_kvi; + int64_t gqa_shards_kvi = num_head / num_head_kvi; + int64_t bs_shards_kvi = batchSize / batchSize_kvi; + + int64_t kviStrideB = {{kernel.stride(kv_indices, 0)}}; + int64_t kviStrideH = {{kernel.stride(kv_indices, 1)}}; + int64_t kviStrideQ = {{kernel.stride(kv_indices, 2)}}; + + int64_t num_kviStrideB = {{kernel.stride(kv_num_blocks, 0)}}; + int64_t num_kviStrideH = {{kernel.stride(kv_num_blocks, 1)}}; + +{%- if has_full_kv_block %} + int64_t full_kviStrideB = {{kernel.stride(full_kv_indices, 0)}}; + int64_t full_kviStrideH = {{kernel.stride(full_kv_indices, 1)}}; + int64_t full_kviStrideQ = {{kernel.stride(full_kv_indices, 2)}}; + + int64_t full_num_kviStrideB = {{kernel.stride(full_kv_num_blocks, 0)}}; + int64_t full_num_kviStrideH = {{kernel.stride(full_kv_num_blocks, 1)}}; + auto full_kv_indices_data = full_kv_indices; + auto full_kv_num_blocks_data = full_kv_num_blocks; +{%- endif %} + + auto kv_num_blocks_data = kv_num_blocks; + auto kv_indices_data = kv_indices; + + // Strides + int64_t qStrideB = {{kernel.stride(query, 0)}}; + int64_t qStrideM = {{kernel.stride(query, 1)}}; + int64_t qStrideH = {{kernel.stride(query, 2)}}; + int64_t kStrideB = {{kernel.stride(key, 0)}}; + int64_t kStrideN = {{kernel.stride(key, 1)}}; + int64_t kStrideH = {{kernel.stride(key, 2)}}; + int64_t vStrideB = {{kernel.stride(value, 0)}}; + int64_t vStrideN = {{kernel.stride(value, 1)}}; + int64_t vStrideH = {{kernel.stride(value, 2)}}; + int64_t oStrideB = {{kernel.stride(output, 0)}}; + int64_t oStrideM = {{kernel.stride(output, 2)}}; + int64_t oStrideH = {{kernel.stride(output, 1)}}; + + int64_t kvSize = {{kernel.size(key, 1)}}; + + int64_t qSplitSize = qBlockSize; + int64_t kvSplitSize = kvBlockSize; + + + qSplitSize = qSplitSize > qSize ? qSize : qSplitSize; + kvSplitSize = kvSplitSize > kvSize ? kvSize : kvSplitSize; + int64_t qSlice = (qSize + qSplitSize - 1) / qSplitSize; + int64_t kvSlice = (kvSize + kvSplitSize - 1) / kvSplitSize; + int64_t kvTail = (kvSize - 1) % kvSplitSize + 1; + + bool need_pack = false; + // Whether pack is needed for BFloat16/Half + if (is_reduced_type) { + // check platform ability + need_pack = std::is_same_v ? at::native::cpublas::could_pack(at::kBFloat16) + : at::native::cpublas::could_pack(at::kHalf); + } + if (need_pack) { + // When the number of gemm is greater than the number of pack, + // the pack overhead can be overlapped. + int64_t thresh_size = 64; + need_pack = kvSize >= thresh_size && qSize >= thresh_size; + if (need_pack) { + double pack_size = batchSize * num_head * kvSize * headSize; + double qs_per_thread = (batchSize * num_head * qSlice + num_thread - 1) / num_thread; + double gemm_size_per_thread = qs_per_thread * qSplitSize * kvSize * headSize; + need_pack = gemm_size_per_thread / pack_size >= 4; + } + } + // Pad is needed for packing when K is not even + bool headSize_even = headSize % 2 == 0; + int64_t eheadSize = need_pack && !headSize_even ? headSize + 1: headSize; + int64_t ekvSplitSize = need_pack && (kvSplitSize % 2 != 0) ? kvSplitSize + 1 : kvSplitSize; + int64_t ekvTail = need_pack && (kvTail % 2 != 0) ? kvTail + 1 : kvTail; + int64_t kv_padding_size = (kvSize - 1) / kvSplitSize * ekvSplitSize + ekvTail; + + // Allocate per thread temp buf (accumulate type) + int64_t _size_per_thread = + /* qk */ qSplitSize * kvSplitSize + + /* qk_max */ qSplitSize + + /* qk_sum */ qSplitSize + + /* dst */ qSplitSize * headSize_v; + + // Inputs/outputs buffers + const scalar_t* q_data = query; + const scalar_t* k_data = key; + const scalar_t* v_data = value; + scalar_t* out_data = output; + + // Buffers to store accum results, padding query and transpose/packing key/value + {{template.codegen_allocate_buffer("buf_data", "accum_t", "num_thread*_size_per_thread")}} + {{template.codegen_allocate_buffer("buf_reduced_data", "scalar_t", "num_thread*qSplitSize*ekvSplitSize")}} + {{template.codegen_allocate_buffer("key_reorder_ptr", "scalar_t", "batchSize_k*num_head_k*eheadSize*kvSize")}} + {{template.codegen_allocate_buffer("value_reorder_ptr", "scalar_t", "batchSize_k*num_head_k*kv_padding_size*headSize_v")}} + {{template.codegen_allocate_buffer("transpose_buffer_ptr", "scalar_t", "num_thread*kvSplitSize*headSize")}} + {{template.codegen_allocate_buffer("query_padding_ptr", "scalar_t", "num_thread*qSplitSize*eheadSize")}} + if (need_pack) { + // Pack K, V + at::parallel_for(0, batchSize_k * num_head_k * kvSlice, 1, [&](int64_t begin, int64_t end) { + int ompIdx = at::get_thread_num(); + int64_t i = 0, j = 0, l = 0, n = 0; + scalar_t* transpose_ptr = need_pack? transpose_buffer_ptr + ompIdx * kvSplitSize * headSize : nullptr; + at::native::data_index_init(begin, i, batchSize_k, j, num_head_k, l, kvSlice); + for ([[maybe_unused]] auto z : c10::irange(begin, end)) { + n = l * kvSplitSize; + int64_t cur_kvSplitSize = std::min(kvSplitSize, kvSize - n); + auto k_addr = + k_data + i * kStrideB + j * kStrideH + n * kStrideN; + auto v_addr = + v_data + i * vStrideB + j * vStrideH + n * vStrideN; + // transpose [cur_kvSplitSize, headSize] -> [headSize, cur_kvSplitSize] + at::native::utils::transpose( + cur_kvSplitSize, + headSize, + /* src_ptr */ + reinterpret_cast(k_addr), + /* ld_src */ kStrideN, + /* dst */ reinterpret_cast(transpose_ptr), + /* ld_dst */ cur_kvSplitSize); + + // Pack [headSize, cur_kvSplitSize] + at::vec::pack_vnni2( + /* src */ reinterpret_cast(transpose_ptr), + /* dst */ reinterpret_cast(key_reorder_ptr + i * num_head_k * eheadSize * kvSize + + j * eheadSize * kvSize + n * eheadSize), + /* ld_src */ cur_kvSplitSize, + /* K */ headSize, + /* N */ cur_kvSplitSize); + + // Pack [cur_kvSplitSize, headSize_v] + at::vec::pack_vnni2( + /* src */ reinterpret_cast(v_addr), + /* dst */ reinterpret_cast(value_reorder_ptr + + i * num_head_k * kv_padding_size * headSize_v + + j * kv_padding_size * headSize_v + n * headSize_v), + /* ld_src */ vStrideN, + /* K */ cur_kvSplitSize, + /* N */ headSize_v); + // Move to the next query + at::native::data_index_step(i, batchSize_k, j, num_head_k, l, kvSlice); + } + }); + } + // Attention loop below + at::parallel_for(0, batchSize * num_head * qSlice, 1, [&](int64_t begin, int64_t end) { + int64_t i = 0, j = 0, k = 0; + at::native::data_index_init(begin, i, batchSize, j, num_head, k, qSlice); + int ompIdx = at::get_thread_num(); + accum_t* buf_ptr = buf_data + ompIdx * _size_per_thread; + accum_t* qk_data = buf_ptr; + accum_t* qk_max_data = qk_data + qSplitSize * kvSplitSize; + accum_t* qk_sum_data = qk_max_data + qSplitSize; + accum_t* dst_data = qk_sum_data + qSplitSize; + scalar_t *qk_reduced_data = + is_reduced_type + ? buf_reduced_data + ompIdx * qSplitSize * ekvSplitSize + : nullptr; + scalar_t* query_t_padding_ptr = (!headSize_even && need_pack) + ? query_padding_ptr + ompIdx * qSplitSize * eheadSize + : nullptr; + + for ([[maybe_unused]] auto z : c10::irange(begin, end)) { + auto i_kvi = is_broadcast_bs_kvi ? i/bs_shards_kvi : i; + auto j_kvi = is_broadcast_head_kvi ? j/gqa_shards_kvi : j; + auto kv_logical_num_data = kv_num_blocks_data + i_kvi * num_kviStrideB + + j_kvi * num_kviStrideH + k; + int kv_indice_num = *kv_logical_num_data; + std::vector kv_indice_list(kv_indice_num); + for(int kv_i = 0; kv_i < kv_indice_num; kv_i++){ + auto kv_logical_data = kv_indices_data + i_kvi * kviStrideB + + j_kvi * kviStrideH + k*kviStrideQ + kv_i; + kv_indice_list[kv_i] = *kv_logical_data; + } + bool is_skip_kv = kv_indice_num > 0 ? false : true; +{%- if has_full_kv_block %} + auto full_kv_logical_num_data = full_kv_num_blocks_data + i_kvi * num_kviStrideB + + j_kvi * num_kviStrideH + k; + int full_kv_indice_num = *full_kv_logical_num_data; + std::vector full_kv_indice_list(full_kv_indice_num); + for(int kv_i = 0; kv_i < full_kv_indice_num; kv_i++){ + auto full_kv_logical_data = full_kv_indices_data + i_kvi * full_kviStrideB + + j_kvi * full_kviStrideH + k*full_kviStrideQ + kv_i; + full_kv_indice_list[kv_i] = *full_kv_logical_data; + } + is_skip_kv = kv_indice_num + full_kv_indice_num > 0 ? false : true; +{%- endif %} + int64_t m = k * qSplitSize; + int64_t cur_qSplitSize = std::min(qSplitSize, qSize - m); + if (!is_skip_kv){ + // Initialize max and sum + {{kernel.kernel_name}}_fill_stub(qk_max_data, + -std::numeric_limits::infinity(), cur_qSplitSize); + {{kernel.kernel_name}}_fill_stub(qk_sum_data, + static_cast(0), cur_qSplitSize); + + if (!headSize_even && need_pack) { + // Pad query if headSize is not even + {{kernel.kernel_name}}_copy_value_with_pad( + q_data + i * qStrideB + j * qStrideH + m * qStrideM, + query_t_padding_ptr, + cur_qSplitSize, + headSize, + cur_qSplitSize, + eheadSize, + qStrideM + ); + } + } + +{%- if has_full_kv_block %} + for (int64_t n_idx = 0; n_idx < kv_indice_num + full_kv_indice_num ; n_idx += 1) { + auto n = n_idx < kv_indice_num ? kv_indice_list[n_idx]*kvSplitSize : full_kv_indice_list[n_idx - kv_indice_num]*kvSplitSize; +{%- else %} + for (int64_t n_idx = 0; n_idx < kv_indice_num ; n_idx += 1) { + auto n = kv_indice_list[n_idx]*kvSplitSize; +{%- endif %} + + auto cur_n = n/kvSplitSize; + int64_t cur_kvSplitSize = std::min(kvSplitSize, kvSize - n); + int64_t cur_ekvSplitSize = (need_pack && cur_kvSplitSize % 2 != 0) ? cur_kvSplitSize + 1 : cur_kvSplitSize; + + // Calculate scale * q @ k.T + auto i_kv = is_broadcast_bs_kv ? i/bs_shards : i; + auto j_kv = is_broadcast_head_kv ? j/gqa_shards : j; + + if (!need_pack) { + auto k_addr = + k_data + i_kv * kStrideB + j_kv * kStrideH + n * kStrideN; + + {{kernel.kernel_name}}_kernel_micro_gemm_transpose_b(false)>( + q_data + i * qStrideB + j * qStrideH + + m * qStrideM, + k_addr, + qk_data, + cur_qSplitSize, + cur_kvSplitSize, + headSize, + qStrideM, + kStrideN, + cur_kvSplitSize); + + } else { + at::native::cpublas::brgemm( + cur_qSplitSize, + cur_kvSplitSize, + eheadSize, + headSize_even ? qStrideM : eheadSize, + cur_kvSplitSize, + cur_kvSplitSize, + false, + !headSize_even + ? query_t_padding_ptr + : q_data + i * qStrideB + j * qStrideH + m * qStrideM, + key_reorder_ptr + i_kv * num_head_k * eheadSize * kvSize + + j_kv * eheadSize * kvSize + n * eheadSize, + qk_data, + need_pack); + } + + {{kernel.kernel_name}}_mul_scale_kernel(qk_data, scaling_factor, cur_qSplitSize*cur_kvSplitSize); + +{%- if score_mod and mask_mod %} + // TODO: reduce the number of calls of q_idx and kv_idx initialization + std::vector q_idx(cur_qSplitSize); + for (int64_t i = 0; i < cur_qSplitSize; ++i) { + q_idx[i] = m + i; + } + + std::vector kv_idx(cur_kvSplitSize); + for (int64_t i = 0; i < cur_kvSplitSize; ++i) { + kv_idx[i] = n + i; + } + + std::vector b_idx = {i}; + std::vector h_idx = {j}; + + accum_t* in_ptr0 = qk_data; + + auto in_ptr1 = b_idx.data(); + auto in_ptr2 = h_idx.data(); + auto in_ptr3 = q_idx.data(); + auto in_ptr4 = kv_idx.data(); + + // apply score mod function + { + {{ template.generate_other_buffer("score_others", 0, "len_score_other", kernel.args) }} + accum_t* out_ptr{{score_buf_idx}} = in_ptr0; + {{ template.modification(score_mod, score_buf_name, score_buf_idx)|indent(12, false) }} + } + + if ((std::find(kv_indice_list.begin(), kv_indice_list.end(), cur_n) != kv_indice_list.end()) ){ + // Apply block mask, fill unused with -inf + { + {{ template.generate_other_buffer("mask_others", -1, "len_mask_other", kernel.args) }} + accum_t* out_ptr{{mask_buf_idx}} = in_ptr0; + {{ template.modification(mask_mod, mask_buf_name, mask_buf_idx)|indent(12, false) }} + } + } + +{%- endif %} + // Update coefficients with Softmax + accum_t tmp_max = 0, tmp_sum = 0, exp_tmp = 0; + for (int64_t row = 0; row < cur_qSplitSize; ++row) { + // apply scaling factor and max per row in fusion + {{kernel.kernel_name}}_mul_reduce_max_fusion_kernel( + qk_data + row * cur_kvSplitSize, + static_cast(1), + cur_kvSplitSize, + qk_data + row * cur_kvSplitSize, + tmp_max); + tmp_max = qk_max_data[row] > tmp_max ? qk_max_data[row] : tmp_max; + if (tmp_max == -std::numeric_limits::infinity()) { + // to avoid `nan = exp2f(-inf - (-inf))` + {{kernel.kernel_name}}_fill_stub( + {{kernel.kernel_name}}_conditional_data_ptr(qk_data, qk_reduced_data) + row * cur_ekvSplitSize, + static_cast(0), cur_kvSplitSize); + } else { + tmp_sum = tmp_max; + // qk <- exp(qk - max) and sum per row + {{kernel.kernel_name}}_exp_reduce_sum_fusion_kernel( + qk_data + row * cur_kvSplitSize, cur_kvSplitSize, + {{kernel.kernel_name}}_conditional_data_ptr(qk_data, qk_reduced_data) + row * cur_ekvSplitSize, + tmp_sum); + // exp_tmp <- exp(max[row] - max) + exp_tmp = std::exp(qk_max_data[row] - tmp_max); + // sum[row] <- sum + exp_tmp * sum[row] + qk_sum_data[row] = tmp_sum + exp_tmp * qk_sum_data[row]; + // max[row] <- max + qk_max_data[row] = tmp_max; + // dst <- dst * exp_tmp + if (n_idx > 0) { + at::vec::map( + [exp_tmp](Vec x) { return x * Vec(exp_tmp); }, + dst_data + row * headSize_v, + dst_data + row * headSize_v, + headSize_v); + } + } + if (need_pack && cur_kvSplitSize % 2 != 0) { + // Pad: [qSplitSize, cur_kvSplitSize] -> [qSplitSize, cur_kvSplitSize + 1] + *(qk_reduced_data + row * (1 + cur_kvSplitSize) + cur_kvSplitSize) = scalar_t(0); + } + } + // Calculate Softmax(q @ k.T) @ v + if (!need_pack) { + auto v_addr = + v_data + i_kv * vStrideB + j_kv * vStrideH + n * vStrideN; + // Fallback Half brgemm is slower than micro gemm + if (!std::is_same_v) { + at::native::cpublas::brgemm( + cur_qSplitSize, + headSize_v, + cur_ekvSplitSize, + cur_ekvSplitSize, + vStrideN, + headSize_v, + n_idx > 0, + {{kernel.kernel_name}}_conditional_data_ptr(qk_data, qk_reduced_data), + v_addr, + dst_data, + need_pack); + } else { + if (n_idx > 0) { + {{kernel.kernel_name}}_kernel_micro_gemm(true)>( + {{kernel.kernel_name}}_conditional_data_ptr(qk_data, qk_reduced_data), + v_addr, + dst_data, + cur_qSplitSize, + headSize_v, + cur_ekvSplitSize, + cur_ekvSplitSize, + vStrideN, + headSize_v); + } else { + {{kernel.kernel_name}}_kernel_micro_gemm(false)>( + {{kernel.kernel_name}}_conditional_data_ptr(qk_data, qk_reduced_data), + v_addr, + dst_data, + cur_qSplitSize, + headSize_v, + cur_ekvSplitSize, + cur_ekvSplitSize, + vStrideN, + headSize_v); + } + } + } else { + int64_t psize = n / kvSplitSize * ekvSplitSize; + at::native::cpublas::brgemm( + cur_qSplitSize, + headSize_v, + cur_ekvSplitSize, + cur_ekvSplitSize, + headSize_v, + headSize_v, + n_idx > 0, + qk_reduced_data, + value_reorder_ptr + + i_kv * num_head_k * kv_padding_size * headSize_v + + j_kv * kv_padding_size * headSize_v + psize * headSize_v, + dst_data, + need_pack); + } + } + + // dst <- dst / sum[row] + // reorder MHA output with strides + for (int64_t row = 0; row < cur_qSplitSize; ++row) { + // Row sums for full masked out rows are 0, we set them to 1 + // in order to avoid NaNs in the output and instead set fully + // masked out rows to 0 + qk_max_data[row] = qk_max_data[row] == -std::numeric_limits::infinity() ? 0 : qk_max_data[row]; + qk_sum_data[row] = qk_sum_data[row] == 0 ? 1 : qk_sum_data[row]; + accum_t sum_reciprocal = 1 / qk_sum_data[row]; + at::vec::map( + [sum_reciprocal, is_skip_kv](Vec x) { return is_skip_kv ? Vec(0.0) : x * Vec(sum_reciprocal); }, + out_data + i * oStrideB + j * oStrideH + m * oStrideM + row * oStrideM, + dst_data + row * headSize_v, + headSize_v); + } + + // Move to the next query + at::native::data_index_step(i, batchSize, j, num_head, k, qSlice); + } + + at::native::cpublas::brgemm_release(need_pack); + + }); +} +""" + + +class CppFlexAttentionTemplate(CppTemplate): + def __init__( + self, + input_nodes, + layout: ir.Layout, + scale, + score_mod, + mask_mod, + kv_block_size, + q_block_size, + has_other_buffer, + no_full_kv_block, + fake_buffers, + len_score_other, + len_mask_other, + kernel_input_name_to_buffer, + block_vars, + ) -> None: + assert layout.dtype in [torch.float, torch.bfloat16, torch.float16] + super().__init__("flex_attention", input_nodes, layout, parallel_num_threads()) + self.scale = scale + self.score_mod = score_mod + self.mask_mod = mask_mod + self.score_buf_name = ( + V.graph.register_buffer(self.score_mod) if self.score_mod else None + ) + self.mask_buf_name = ( + V.graph.register_buffer(self.mask_mod) if self.mask_mod else None + ) + + def get_idx(buf_name): + match = re.search(r"\d+", buf_name) + assert match, f"incorrect score buf name: {buf_name}" + return match.group() + + self.score_buf_idx = ( + get_idx(self.score_buf_name) if self.score_buf_name else None + ) + self.mask_buf_idx = get_idx(self.mask_buf_name) if self.mask_buf_name else None + self.kv_block_size = kv_block_size + self.q_block_size = q_block_size + self.has_other_buffer = has_other_buffer + self.no_full_kv_block = no_full_kv_block + self.other_buffer_input_offset = 2 + if self.no_full_kv_block: + self.other_buffer_input_offset = 0 + self.fake_buffers = fake_buffers + self.len_score_other = len_score_other + self.len_mask_other = len_mask_other + self.kernel_input_name_to_buffer = kernel_input_name_to_buffer + self.block_vars = block_vars + self.extra_sizevars = list( + OrderedSet( + val + for val in self.kernel_input_name_to_buffer.values() + if isinstance(val, sympy.Symbol) + ) + ) + self.other_buf_start_idx = 5 + self.score_mod_other_buffers = ( + self.input_nodes[ + self.other_buf_start_idx + + self.other_buffer_input_offset : self.other_buf_start_idx + + self.other_buffer_input_offset + + self.len_score_other + ] + if self.has_other_buffer + else None + ) + self.mask_mod_other_buffers = ( + self.input_nodes[ + self.other_buf_start_idx + + self.other_buffer_input_offset + + self.len_score_other : + ] + if self.has_other_buffer + else None + ) + self.other_ptr_data = {} # type: ignore[var-annotated] + + def update_kernel_args(self, kernel_args): + kernel_args.update( + { + key: value + for key, value in self.kernel_input_name_to_buffer.items() + if not isinstance(value, sympy.Symbol) + } + ) + return kernel_args + + def generate_other_buffer(self, buf_list, start_offset, len_attr, kernel_args): + kernel_input_name_to_buffer_name = { + key: value if isinstance(value, sympy.Symbol) else value.get_name() + for key, value in self.kernel_input_name_to_buffer.items() + } + + def get_arg(name): + return kernel_input_name_to_buffer_name.get(name) + + def get_arg_name(name): + if isinstance(get_arg(name), sympy.Symbol): + return kernel_args.sizevars.get(get_arg(name)) + return kernel_args.input_buffers.get(get_arg(name)) + + if not self.has_other_buffer: + return "" + + if start_offset == -1: + start_offset = self.len_score_other + + length = getattr(self, len_attr) + for i in range(length): + pointer = f"in_ptr{self.other_buf_start_idx + start_offset + i}" + buffer_key = f"{buf_list}_{i}" + if pointer not in self.other_ptr_data: + self.other_ptr_data[pointer] = ( + get_arg_name(buffer_key), + get_arg(buffer_key), + ) + + return "\n".join( + f"auto {ptr} = {name};" for ptr, (name, _) in self.other_ptr_data.items() + ) + + def modification(self, subgraph_buffer, output_name, output_idx): + assert isinstance(subgraph_buffer, ir.ComputedBuffer) + subgraph_buffer_data = subgraph_buffer.data + from ..loop_body import LoopBody + from ..utils import sympy_index_symbol_with_prefix, SymT + from ..virtualized import V + from .cpp import CppKernelProxy, KernelGroup, ParallelDepth + + kernel_group = KernelGroup() + kernel_input_args = { + "score": "in_ptr0", + "b": "in_ptr1", + "h": "in_ptr2", + "q_idx": "in_ptr3", + "kv_idx": "in_ptr4", + } + if self.has_other_buffer: + kernel_input_args.update( + {arg: ptr for ptr, (_, arg) in self.other_ptr_data.items()} + ) + + kernel_output_args = {output_name: f"out_ptr{output_idx}"} + + args = kernel_group.args + for name, inp in kernel_input_args.items(): + args.input_buffers[name] = inp + + for name, inp in kernel_output_args.items(): + args.output_buffers[name] = inp + + for name in self.extra_sizevars: + args.sizevars[name] = f"k{name}" + + kernel_group.args = args + + cpp_kernel_proxy = CppKernelProxy(kernel_group) + bodies = [] + var_sizes_list = [] + var_sizes = tuple(subgraph_buffer.get_size()) + var_ranges = { + sympy_index_symbol_with_prefix(SymT.INDEX, i): sz + for i, sz in enumerate(var_sizes) + } + + dst_layout = subgraph_buffer.get_layout() + output_index = dst_layout.make_indexer()([*var_ranges.keys()]) + + def fn(*args): + V.ops.store( + output_name, + output_index, + subgraph_buffer_data.make_loader()(args).value, + ) + + body = LoopBody( + fn, + (list(var_ranges.keys())), + var_ranges, + list(var_ranges.keys()), + tuple(), + ) + + from ..loop_body import MemoryUsageType + + assert all( + mem.buffer_name in kernel_group.args.input_buffers + for mem in body.memory_usage[MemoryUsageType.LOAD] + ), ( + "All the buffers in the score and mask subgraph should be in kernel_group.args.input_buffers" + ) + + bodies.append(body) + var_sizes_list.append((var_sizes, ())) + + cpp_kernel_proxy.codegen_loop_bodies(bodies, var_sizes_list) + + def max_parallel_depth(): + return ParallelDepth(parallel_depth=0, start_depth=0) + + # This loop is not parallelized since it is not the outermost loop. + with patch.object( + cpp_kernel_proxy.loop_nest, "max_parallel_depth", max_parallel_depth + ): + kernel_group.finalize_kernel(cpp_kernel_proxy, []) + output_code = kernel_group.loops_code.getvalue() + + var_q_symbol, var_kv_symbol = self.block_vars + # See [Note] Handle the case where the split sizes are not statically known. + # We don't know the value of qBlockSize and rkvBlockSize during compilation time + # thus we've represented them by symbols. + # We change the symbol strings back to "cur_qSplitSize" and "cur_kvSplitSize" + # in the generated code thus they'll be filled with the real value during runtime. + if var_q_symbol in kernel_group.args.sizevars: + output_code = output_code.replace( + kernel_group.args.sizevars[var_q_symbol], "cur_qSplitSize" + ) + if var_kv_symbol in kernel_group.args.sizevars: + output_code = output_code.replace( + kernel_group.args.sizevars[var_kv_symbol], "cur_kvSplitSize" + ) + + return output_code + + @staticmethod + def add_choices( + choices, + input_nodes, + layout, + scale, + score_mod, + mask_mod, + kv_block_size, + q_block_size, + has_other_buffer, + no_full_kv_block, + fake_buffers, + len_score_other, + len_mask_other, + kernel_input_name_to_buffer, + block_vars, + ): + def preprocessor(input_nodes, layout): + return input_nodes, layout + + def postprocessor(output): + return output + + template = DataProcessorTemplateWrapper( + CppFlexAttentionTemplate, + preprocessor, + postprocessor, + input_nodes=input_nodes, + layout=layout, + scale=scale, + score_mod=score_mod, + mask_mod=mask_mod, + kv_block_size=kv_block_size, + q_block_size=q_block_size, + has_other_buffer=has_other_buffer, + no_full_kv_block=no_full_kv_block, + fake_buffers=fake_buffers, + len_score_other=len_score_other, + len_mask_other=len_mask_other, + kernel_input_name_to_buffer=kernel_input_name_to_buffer, + block_vars=block_vars, + ) + template.maybe_append_choice(choices) + return template + + def apply_score_mod(self, score, b, h, q_idx, kv_idx): + return self.score_mod.graph_module(score, b, h, q_idx, kv_idx).item() + + def render( # type: ignore[override,return] + self, + kernel, + template_buffer_node: ir.CppTemplateBuffer | None = None, + epilogue_nodes: list[ir.IRNode] | None = None, + **kwargs, + ) -> str: + if epilogue_nodes is not None and epilogue_nodes != []: + raise NotImplementedError( + "Unsupported for `epilogue_nodes` in CppFlexAttentionTemplate." + ) + # Query (Batch x Num_heads x Q_seq_len x Dim_per_head) + # -> (Batch x Q_seq_len x Num_heads x Dim_per_head) + # Key (Batch x Num_heads x KV_seq_len x Dim_per_head) + # -> (Batch x KV_seq_len x Num_heads x Dim_per_head) + # Value (Batch x Num_heads x KV_seq_len x Dim_per_head) + # -> (Batch x KV_seq_len x Num_heads x Dim_per_head) + + query = kernel.permute(self.input_nodes[0], [0, 2, 1, 3]) + key = kernel.permute(self.input_nodes[1], [0, 2, 1, 3]) + value = kernel.permute(self.input_nodes[2], [0, 2, 1, 3]) + self.accumulate_dtype = torch.float + self.input_dtype = query.layout.dtype + + num_threads = parallel_num_threads() + assert isinstance(self.output_node, ir.IRNode) + buf_out = TensorBox.create(self.output_node) + if template_buffer_node is not None: + buf_out = template_buffer_node + options = dict( + query=query, + key=key, + value=value, + kv_num_blocks=self.input_nodes[3], + kv_indices=self.input_nodes[4], + full_kv_num_blocks=( + self.input_nodes[5] if not self.no_full_kv_block else None + ), + full_kv_indices=self.input_nodes[6] if not self.no_full_kv_block else None, + score_mod_other_buffers=self.score_mod_other_buffers, + mask_mod_other_buffers=self.mask_mod_other_buffers, + scale=self.scale, + has_full_kv_block=not self.no_full_kv_block, + accumulate_dtype=self.accumulate_dtype, + query_dtype=self.input_dtype, + kvBlockSize=self.kv_block_size, + qBlockSize=self.q_block_size, + template=self, + output=buf_out, + kernel=kernel, + num_thread=num_threads, + score_mod=self.score_mod, + mask_mod=self.mask_mod, + score_buf_name=self.score_buf_name, + mask_buf_name=self.mask_buf_name, + score_buf_idx=self.score_buf_idx, + mask_buf_idx=self.mask_buf_idx, + ) + with contextlib.ExitStack() as stack: + for buf in self.fake_buffers: + stack.enter_context( + patch.object(V.graph, "get_dtype", self._fake_get_dtype(buf)) + ) + return self._template_from_string(FLEX_ATTENTION_TEMPLATE).render(**options) + + def codegen_softmax_fusion(self, kernel_name: str): + # TODO: use inductor IR to rewrite those fusions + return self._template_from_string(SOFTMAX_FUSIONS).render( + dict(kernel_name=kernel_name) + ) + + def codegen_brgemm_pack_function(self, kernel_name: str): + # TODO: make them general for common bmm templates + return self._template_from_string(BRGEMM_PACK_FUNCTIONS).render( + dict(kernel_name=kernel_name) + ) + + def codegen_allocate_buffer(self, buffer_name: str, buffer_dtype, buffer_size): + return self._template_from_string(ALLOCATE_BUFFER).render( + dict( + buffer_name=buffer_name, + buffer_dtype=buffer_dtype, + buffer_size=buffer_size, + ) + ) + + def micro_gemm_define(self, kernel_name: str): + from torch._inductor.codegen.cpp_gemm_template import ( + CppTemplateKernel, + parallel_num_threads, + ) + from torch._inductor.codegen.cpp_micro_gemm import CppMicroGemmFP32Vec + from torch._inductor.virtualized import V + + micro_gemm_trans = CppMicroGemmFP32Vec( + kernel_name + "_kernel_micro_gemm_transpose_b", + self.input_dtype, + self.input_dtype, + self.accumulate_dtype, + self.accumulate_dtype, + GemmBlocking(1, 16, 1), + 1, + True, + True, + ) + + micro_gemm = CppMicroGemmFP32Vec( + kernel_name + "_kernel_micro_gemm", + self.input_dtype, + self.input_dtype, + self.accumulate_dtype, + self.accumulate_dtype, + GemmBlocking(1, 16, 1), + 1, + True, + False, + ) + + with V.set_graph_handler(V.graph): + kernel = CppTemplateKernel("cpp_micro_gemm", parallel_num_threads()) + code_trans = micro_gemm_trans.codegen_define(kernel) + code = micro_gemm.codegen_define(kernel) + return code + code_trans + + def codegen_micro_gemm(self, kernel_name: str): + micro_gemm = self.micro_gemm_define(kernel_name) + GEMM_SOURCE_CODE = MICRO_GEMM_TEMPLATE.replace("GEMM_DEFINE", micro_gemm) + return self._template_from_string(GEMM_SOURCE_CODE).render() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_gemm_template.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_gemm_template.py new file mode 100644 index 0000000000000000000000000000000000000000..911d566a1f9876da1481520d6e3c028ef97a00d8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_gemm_template.py @@ -0,0 +1,1837 @@ +# mypy: allow-untyped-defs +import contextlib +import logging +import math +import os +from collections.abc import Callable +from functools import lru_cache +from typing import Any, cast, TypeVar +from unittest.mock import patch + +import torch +import torch.utils +from torch.utils._ordered_set import OrderedSet + +from ..._dynamo.utils import counters +from .. import config, ir, lowering as L +from ..kernel.mm_common import mm_args +from ..select_algorithm import DataProcessorTemplateWrapper +from ..utils import ( + has_free_symbols, + is_same_mkldnn_tensor, + is_same_tensor, + parallel_num_threads, +) +from ..virtualized import ops, V +from .cpp import get_export_declaration +from .cpp_micro_gemm import ( + CppMicroBrgemm, + CppMicroGemm, + CppMicroGemmAMX, + CppMicroGemmFP32Vec, + create_micro_gemm, + is_int8_woq_gemm_small_m_dim_corner_case, + LayoutType, +) +from .cpp_template import CppTemplate +from .cpp_template_kernel import CppTemplateKernel +from .cpp_utils import ( + create_epilogue_with_attr, + DTYPE_TO_CPP, + GemmBlocking, + get_gemm_template_output_and_compute_dtype, +) + + +log = logging.getLogger(__name__) + +GEMM_TEMPLATE_INIT_BLOCKING_BASIC_BLOCK = r""" + constexpr int64_t num_threads = {{num_threads}}; + constexpr int64_t N = {{N}}; + constexpr int64_t K = {{K}}; + constexpr int64_t Mr = {{micro_gemm.register_blocking.block_m}}; + constexpr int64_t Nr = {{micro_gemm.register_blocking.block_n}}; + constexpr int64_t Kr = {{micro_gemm.register_blocking.block_k}}; + constexpr int64_t Nr_blocks = (N + Nr - 1) / Nr; + constexpr int64_t Kr_blocks = (K + Kr - 1) / Kr; +{%- if is_dynamic_M %} + const int64_t M = {{kernel.size(GemmOut, 0)}}; + const int64_t Mr_blocks = (M + Mr - 1) / Mr; +{%- else %} + constexpr int64_t M = {{kernel.size(GemmOut, 0)}}; + constexpr int64_t Mr_blocks = (M + Mr - 1) / Mr; +{%- endif %} +""" + +GEMM_TEMPLATE_INIT_BLOCKING_EXTENDED = r""" +{%- if is_dynamic_M %} + {%- if num_threads > 1 %} + int64_t Mt_blocks, Nt_blocks, Kt_blocks; + mm_get_thread_blocking(num_threads, {{config.cpp.gemm_max_k_slices}}, M, N, K, Mr, Nr, Kr, Mt_blocks, Nt_blocks, Kt_blocks); + {%- else %} + const auto Mt_blocks = Mr_blocks; + const auto Nt_blocks = Nr_blocks; + const auto Kt_blocks = Kr_blocks; + {%- endif %} + int64_t Mc_blocks, Nc_blocks, Kc_blocks; + uint32_t L1_cache_size = {{L1_cache_size}}; + uint32_t L2_cache_size = {{L2_cache_size}}; + mm_get_cache_blocking<{{kernel.dtype(X)}}, {{kernel.dtype(W)}}>( + num_threads, + M, + N, + K, + Mr, + Nr, + Kr, + Mt_blocks, + Nt_blocks, + Kt_blocks, + Mc_blocks, + Nc_blocks, + Kc_blocks, + L1_cache_size, + L2_cache_size + ); + const int64_t num_Mc_blocks = (Mr_blocks + Mc_blocks - 1) / Mc_blocks; + const int64_t num_Nc_blocks = (Nr_blocks + Nc_blocks - 1) / Nc_blocks; + const int64_t num_Mt_blocks = (Mr_blocks + Mt_blocks - 1) / Mt_blocks; + const int64_t num_Nt_blocks = (Nr_blocks + Nt_blocks - 1) / Nt_blocks; + const int64_t num_Kt_blocks = (Kr_blocks + Kt_blocks - 1) / Kt_blocks; +{%- else %} + constexpr int64_t Mt_blocks = {{template.thread_blocking(num_threads).block_m}}; + constexpr int64_t Nt_blocks = {{template.thread_blocking(num_threads).block_n}}; + constexpr int64_t Kt_blocks = {{template.thread_blocking(num_threads).block_k}}; + constexpr int64_t Mc_blocks = {{template.cache_blocking(num_threads).block_m}}; + constexpr int64_t Nc_blocks = {{template.cache_blocking(num_threads).block_n}}; + constexpr int64_t Kc_blocks = {{template.cache_blocking(num_threads).block_k}}; + constexpr int64_t num_Mc_blocks = (Mr_blocks + Mc_blocks - 1) / Mc_blocks; + constexpr int64_t num_Nc_blocks = (Nr_blocks + Nc_blocks - 1) / Nc_blocks; + constexpr int64_t num_Mt_blocks = (Mr_blocks + Mt_blocks - 1) / Mt_blocks; + constexpr int64_t num_Nt_blocks = (Nr_blocks + Nt_blocks - 1) / Nt_blocks; + constexpr int64_t num_Kt_blocks = (Kr_blocks + Kt_blocks - 1) / Kt_blocks; +{%- endif %} +{%- if is_woq_int4 %} + int64_t group_size = *q_group_size; +{%- endif %} + + // make sure all partitions are assigned + {{kernel.assert_function}}( + Mt_blocks * Nt_blocks * Kt_blocks * {{num_threads}} >= Mr_blocks * Nr_blocks * Kr_blocks, + "Not all partitions are assigned." + ); +""" + +GEMM_TEMPLATE_MULTI_THREADS_PARAMS = r""" +const int tid = omp_get_thread_num(); +const int64_t k_group_id = tid / num_Kt_blocks; +const int64_t k_slice_id = tid % num_Kt_blocks; +const int64_t n_group_id = k_group_id / num_Nt_blocks; +const int64_t n_slice_id = k_group_id % num_Nt_blocks; +const int64_t k_block_start = k_slice_id * Kt_blocks; +const int64_t k_block_end = std::min(k_block_start + Kt_blocks, Kr_blocks); +const int64_t n_block_start = n_slice_id * Nt_blocks; +const int64_t n_block_end = std::min(n_block_start + Nt_blocks, Nr_blocks); +const int64_t m_block_start = std::min(n_group_id * Mt_blocks, Mr_blocks); +const int64_t m_block_end = std::min(m_block_start + Mt_blocks, Mr_blocks); +const int64_t num_Mc_blocks_per_thread = (m_block_end - m_block_start + Mc_blocks - 1) / Mc_blocks; +""" + +GEMM_TEMPLATE_SINGLE_THREAD_PARAMS = r""" +constexpr int tid = 0; +constexpr int64_t k_group_id = 0; +constexpr int64_t k_slice_id = 0; +constexpr int64_t n_group_id = 0; +constexpr int64_t n_slice_id = 0; +constexpr int64_t m_block_start = 0; +constexpr int64_t n_block_start = 0; +constexpr int64_t n_block_end = Nr_blocks; +constexpr int64_t k_block_start = 0; +constexpr int64_t k_block_end = Kr_blocks; +{%- if is_dynamic_M %} +const int64_t num_Mc_blocks_per_thread = num_Mc_blocks; +const int64_t m_block_end = Mr_blocks; +{%- else %} +constexpr int64_t num_Mc_blocks_per_thread = num_Mc_blocks; +constexpr int64_t m_block_end = Mr_blocks; +{%- endif %} +""" + +GEMM_TEMPLATE_M_LOOP_PARAMS = r""" +const int64_t my_mc_block_id = (mc_block_id + n_slice_id) % num_Mc_blocks_per_thread; +const int64_t mc = m_block_start + my_mc_block_id * Mc_blocks; +const int64_t m_start = mc * Mr; +const int64_t m_end = std::min(std::min(mc + Mc_blocks, m_block_end) * Mr, M); +const int64_t m_size = m_end - m_start; +""" + +GEMM_TEMPLATE_N_LOOP_PARAMS = r""" +const int64_t n_start = nc * Nr; +const int64_t n_end = std::min(std::min(nc + Nc_blocks, n_block_end) * Nr, N); +const int64_t n_size = n_end - n_start; +// NB: assume we pad N, nc_block_end won't exceed padded N here. +const int64_t nc_block_end = std::min(nc + Nc_blocks, n_block_end); +""" + +GEMM_TEMPLATE_MICROKERNEL_DEF = r""" +{{template.header().getvalue()}} + +{{micro_gemm.codegen_define(kernel)}} +""" + +GEMM_TEMPLATE_STUB_DEF = r""" +{%- if x_scale is not none %} + {%- set kernel_args = {"X": X, "W": W, "inp": inp, "x_scale": x_scale, "x_zp": x_zp, "w_scale": w_scale, "w_zp": w_zp,} %} +{%- elif is_woq_int4 %} + {%- set kernel_args = {"X": X, "W": W, "q_group_size": q_group_size, "qscale_and_zeros": qscale_and_zeros} %} +{%- else %} + {%- set kernel_args = {"X": X, "W": W, "inp": inp} %} +{%- endif %} + +extern "C" {{export_declaration}} +{{kernel.def_kernel(inputs=kernel_args, outputs={"Y": Y}, aliases=aliases)}} +""" + +GEMM_TEMPLATE = r""" +{{ template.codegen_gemm_stub_def() }} +{ + {{ kernel.maybe_codegen_profile() }} + {{ template.codegen_blocks( + num_threads, N, K, micro_gemm, is_dynamic_M, kernel, GemmOut, config, L1_cache_size, L2_cache_size, X, W + ) }} + +{%- if maybe_k_slicing %} + std::unique_ptr[]> local_buf_ptrs; + if (num_Kt_blocks > 1) { + local_buf_ptrs.reset(new std::unique_ptr<{{DTYPE_TO_CPP[acc_buf_dtype]}}[]>[num_Mc_blocks * num_Nc_blocks * num_Kt_blocks]); + } +{%- endif %} + +{%- if num_threads > 1 %} + {%- set use_dynamic_threads = ((config.cpp.threads < 1) and (num_threads == cpu_count)) or config.cpp.dynamic_threads %} + {%- if use_dynamic_threads %} + #pragma omp parallel + {%- else %} + #pragma omp parallel num_threads({{num_threads}}) + {%- endif %} + { + {{ template.codegen_multi_threads_params()|indent(8, false) }} +{%- else %} + { + {{ template.codegen_single_thread_params(is_dynamic_M)|indent(8, false) }} +{%- endif %} + {{ micro_gemm.codegen_init(kernel) }} +{%- if use_local_acc %} + {%- set acc_buf_name = "local_acc_buf" %} + {{ kernel.define_buffer(acc_buf_name, ["Mc_blocks*Mr", "Nc_blocks*Nr"], acc_buf_dtype) }} +{%- endif %} + for (int64_t mc_block_id = 0; mc_block_id < num_Mc_blocks_per_thread; mc_block_id++) { + {{ template.codegen_m_loop_params()|indent(12, false) }} + for (int64_t nc = n_block_start; nc < n_block_end; nc += Nc_blocks) { + {{ template.codegen_n_loop_params()|indent(16, false) }} +{%- if use_local_acc %} + {%- set acc = kernel.local_buffers[acc_buf_name] %} + {{ kernel.reinit_buffer_if_null(acc_buf_name) }} +{%- else %} + {%- set acc = kernel.slice_nd(GemmOut, [("m_start", "m_end"), ("n_start", "n_end")]) %} +{%- endif %} + for (int64_t kc = k_block_start; kc < k_block_end; kc += Kc_blocks) { + int64_t k_start = kc * Kr; + int64_t k_end = std::min(std::min(kc + Kc_blocks, k_block_end) * Kr, K); +{%- set tile_X = kernel.slice_nd(X, [("m_start", "m_end"), ("k_start", "k_end")]) %} + for (int64_t nci = nc; nci < nc_block_end; nci++) { +{%- set acc_slice = kernel.slice_nd(acc, [("0", "m_end - m_start"), ("(nci - nc)*Nr", "(nci - nc + 1)*Nr")]) %} +{%- if template.should_block_weights and not is_woq_int4 %} +{%- set tile_W_3d = kernel.slice_nd(W, [("nci", "nci + 1"), ("k_start", "k_end"), ()]) %} +{%- set tile_W = kernel.view(tile_W_3d, ["k_end - k_start", micro_gemm.register_blocking.block_n]) %} +{%- else %} + {%- if is_woq_int4 %} + {%- set tile_W = kernel.slice_nd(W, [("nci * Nr", "(nci + 1) * Nr"), ("k_start * Nr / 2", "k_end * Nr / 2")]) %} + {%- set tile_qparam = kernel.slice_nd( + qscale_and_zeros, [("k_start // group_size", "k_end // group_size"), ("nci * Nr", "(nci + 1) * Nr"), ()]) %} + {%- else %} + {%- set tile_W = kernel.slice_nd(W, [("k_start", "k_end"), ("n_start", "n_start + n_size")]) %} + {%- set tile_qparam = None %} + {%- endif %} +{%- endif %} + if (kc == k_block_start) { + {{ micro_gemm.codegen_call(kernel, + tile_X, + tile_W, + acc_slice, + accum=False, + qscale_and_zeros=tile_qparam)|indent(28, false) + }} + } else { + {{ micro_gemm.codegen_call(kernel, + tile_X, + tile_W, + acc_slice, + accum=True, + qscale_and_zeros=tile_qparam)|indent(28, false) + }} + } + } + } +{%- if maybe_k_slicing %} + if (num_Kt_blocks > 1) { + const int64_t mxn_cache_block_id = (mc / Mc_blocks) * num_Nc_blocks + nc; + local_buf_ptrs[mxn_cache_block_id * num_Kt_blocks + k_slice_id].reset( + {{ kernel.release_buffer(acc_buf_name) }}); + } else +{%- endif %} + { +{%- set tile_Y = kernel.slice_nd(Y_2d, [("m_start", "m_end"), ("n_start", "n_end")]) %} +{%- set tile_acc = kernel.slice_nd(acc, [("0", "m_end - m_start"), ("0", "n_end - n_start")]) %} + {{ kernel.store_output( + tile_Y, tile_acc, GemmOut, epilogue_nodes, offsets=("m_start", "n_start"), reindexers=reindexers + )|indent(20, false) + }} + } + } + } +{%- if maybe_k_slicing %} + if (num_Kt_blocks > 1) { + #pragma omp barrier + for (int64_t mc = m_block_start; mc < m_block_end; mc += Mc_blocks) { + // We slice M-dim and each thread in the k-slicing group works on a slice + const int64_t m_start_unsliced = mc * Mr; + const int64_t m_end_unsliced = std::min(std::min(mc + Mc_blocks, m_block_end) * Mr, M); + const int64_t m_size_unsliced = m_end_unsliced - m_start_unsliced; + const int64_t m_slice_size = (m_size_unsliced + num_Kt_blocks - 1) / num_Kt_blocks; + const int64_t m_start = std::min(m_start_unsliced + m_slice_size * k_slice_id, m_end_unsliced); + const int64_t m_end = std::min(m_start_unsliced + m_slice_size * (k_slice_id + 1), m_end_unsliced); + const int64_t m_size = m_end - m_start; + const int64_t m_offset = m_start - m_start_unsliced; + for (int64_t nc = n_block_start; nc < n_block_end; nc += Nc_blocks) { + const int64_t n_start = nc * Nr; + const int64_t n_end = std::min(std::min(nc + Nc_blocks, n_block_end) * Nr, N); + const int64_t n_size = n_end - n_start; + const int64_t mxn_cache_block_id = (mc / Mc_blocks) * num_Nc_blocks + nc; + auto {{acc_buf_name}} = local_buf_ptrs[mxn_cache_block_id * num_Kt_blocks].get(); + for (int64_t other_slice = 1; other_slice < num_Kt_blocks; other_slice++) { + auto other_acc = local_buf_ptrs[mxn_cache_block_id * num_Kt_blocks + other_slice].get(); + for (int64_t m = m_offset; m < m_offset + m_size; m++) { + #pragma omp simd + for (int64_t n = 0; n < n_size; n++) { + {{acc_buf_name}}[m*Nr + n] += other_acc[m*Nr + n]; + } + } + } + {%- set tile_acc_m_slice = kernel.slice_nd(tile_acc, [("m_offset", "m_offset + m_end - m_start"), ()]) %} + {{ kernel.store_output( + tile_Y, tile_acc_m_slice, GemmOut, epilogue_nodes, offsets=("m_start", "n_start"), reindexers=reindexers + )|indent(20, false) + }} + } + } + } +{%- endif %} + {{ micro_gemm.codegen_finalize(kernel) }} + } +} +""" + +SMALL_M_GEMM_TEMPLATE = r""" +{{ template.codegen_gemm_stub_def() }} +{ + {{ kernel.maybe_codegen_profile() }} + {{ template.codegen_blocks( + num_threads, N, K, micro_gemm, is_dynamic_M, kernel, GemmOut, config, L1_cache_size, L2_cache_size, X, W + ) }} + # pragma omp parallel + { + #pragma omp for nowait + for (int64_t nr_block_id = 0; nr_block_id < Nr_blocks; nr_block_id++) { + // Handle one output M * Nr block in each thread + int64_t n_start = nr_block_id * Nr; + int64_t n_end = (nr_block_id + 1) * Nr; +{%- if use_local_acc %} + {%- set acc_buf_name = "local_acc_buf" %} + {{ kernel.define_stack_allocated_buffer(acc_buf_name, ["M", "Nr"], acc_buf_dtype) }} + {%- set acc = kernel.local_buffers[acc_buf_name] %} +{%- else %} + {%- set acc = kernel.slice_nd(GemmOut, [(0, "M"), ("n_start", "n_end")]) %} +{%- endif %} + for (int64_t kr_block_id = 0; kr_block_id < Kr_blocks; kr_block_id++) { + // this loop is not parallelized + int64_t k_start = kr_block_id * Kr; + int64_t k_end = std::min((kr_block_id + 1) * Kr, K); +{%- set tile_X = kernel.slice_nd(X, [(0, "M"), ("k_start", "k_end")]) %} +{%- set tile_W_3d = kernel.slice_nd(W, [("nr_block_id", "nr_block_id + 1"), ("k_start", "k_end"), ()]) %} +{%- set tile_W = kernel.view(tile_W_3d, ["k_end - k_start", micro_gemm.register_blocking.block_n]) %} + if C10_UNLIKELY(kr_block_id == 0) { + {{ micro_gemm.codegen_call(kernel, tile_X, tile_W, acc, accum=False, prefetch=True)|indent(20, false) }} + } else if C10_UNLIKELY(k_end == K) { + {{ micro_gemm.codegen_call(kernel, tile_X, tile_W, acc, accum=True, prefetch=False)|indent(20, false) }} + } else { + {{ micro_gemm.codegen_call(kernel, tile_X, tile_W, acc, accum=True, prefetch=True)|indent(20, false) }} + } + } +{%- set tile_Y = kernel.slice_nd(Y_2d, [("0", "M"), ("n_start", "n_end")]) %} +{%- set tile_acc = kernel.slice_nd(acc, [("0", "M"), ("0", "n_end - n_start")]) %} + {{ kernel.store_output( + tile_Y, tile_acc, GemmOut, epilogue_nodes, offsets=("0", "n_start"), reindexers=reindexers + )|indent(20, false) }} + } + } +} +""" + + +def _is_int8_gemm(inputs): + return ( + isinstance(inputs[0], ir.IRNode) + and inputs[0].get_dtype() in [torch.uint8, torch.int8] + ) or ( + isinstance(inputs[0], torch.Tensor) + and inputs[0].dtype in [torch.uint8, torch.int8] + ) + + +def get_padded_n(n, block_n): + return (n + block_n - 1) // block_n * block_n + + +_T = TypeVar("_T", ir.IRNode, torch.Tensor) + + +def transpose_w(W: _T, trans_w: bool) -> _T: + """ + Transpose W based on the trans_w flag. + """ + if isinstance(W, ir.IRNode): + if trans_w: + if not isinstance(W, ir.TensorBox): + # pyrefly: ignore [bad-assignment] + W = ir.TensorBox(W) + W = L.permute(W, [1, 0]) + else: + if trans_w: + assert isinstance(W, torch.Tensor) + # pyrefly: ignore [bad-assignment] + W = W.transpose(0, 1) + # pyrefly: ignore [bad-return] + return W + + +def expand_bias(B: _T | None, X: _T) -> _T | None: + """ + Expand Bias to the same size of X. + """ + if B is not None: + if isinstance(B, ir.IRNode): + if not isinstance(B, ir.TensorBox): + # pyrefly: ignore [bad-assignment] + B = ir.TensorBox(B) + assert hasattr(X, "get_size") + # pyrefly: ignore [missing-attribute] + B = L.expand(B, (X.get_size()[0], B.get_size()[-1])) + else: + assert isinstance(B, torch.Tensor) + assert isinstance(X, torch.Tensor) + # pyrefly: ignore [bad-assignment] + B = B.expand(X.shape[0], B.shape[-1]) + return B + + +def prune_tensors(input_nodes: list[ir.IRNode], new_input_nodes: list[ir.IRNode]): + """ + Prune unused tensors from `V.graph` since the GEMM Template use new packed weight. + """ + + def share_storage(base_tensor: torch.Tensor, comp_tensor: torch.Tensor): + return base_tensor.is_mkldnn == comp_tensor.is_mkldnn and ( + is_same_tensor(base_tensor, comp_tensor) + or is_same_mkldnn_tensor(base_tensor, comp_tensor) + ) + + def get_candidates(input_nodes, new_input_nodes): + # Only Constant Buffer like weight and bias might be changed in GEMM Template. + # The Inductor IR Node may changed, but still share the storage. For example: + # bias in bfloat16 case which only do the expand + return [ + node + for node in input_nodes + if ( + node not in new_input_nodes + and isinstance(node, (ir.TensorBox, ir.StorageBox)) + and node.get_name() in V.graph.constants + and not any( + ( + isinstance(new_node, (ir.TensorBox, ir.StorageBox)) + and new_node.get_name() in V.graph.constants + and share_storage( + V.graph.constants[node.get_name()], + V.graph.constants[new_node.get_name()], + ) + ) + for new_node in new_input_nodes + ) + ) + ] + + for candidate_node in get_candidates(input_nodes, new_input_nodes): + # By using the new packed weight for the GEMM template, we can prune the + # old weight if it has no other users. This saves memory but makes the FX graph + # non-retraceable. To support retracing, we can add a repack node to the + # FX graph. For example: + # mkldnn._linear_pointwise <- repack_linear_wgt <- packed_wgt_for_template + candidate_tensor_users = 0 + candidate_tensor = V.graph.constants[candidate_node.get_name()] + for node in reversed(V.graph.graph.nodes): + # Case may happen when the candidate tensor is used by more than 1 get_attr node + # https://github.com/pytorch/pytorch/issues/134998 + if node.op == "get_attr" and hasattr( + V.graph.module, node.target + ): # candidate tensor might already be deleted + comp_tensor = getattr(V.graph.module, node.target) + if isinstance(comp_tensor, torch.Tensor) and share_storage( + candidate_tensor, comp_tensor + ): + candidate_tensor_users += 1 + + for node in reversed(V.graph.graph.nodes): + # The get_attr node has only 1 user fx node + # The candidate tensor has been used by only 1 get_attr node + if ( + node.op == "get_attr" + and node.target == candidate_node.get_name() + and len(node.users) == 1 + and candidate_tensor_users == 1 + ): + del V.graph.constants[node.target] + delattr(V.graph.module, node.target) + delattr(V.graph.graph.owning_module, node.target) + counters["inductor"]["select_algorithm_weight_prune"] += 1 + + +def gen_2d_view_of_epilogue_buf( + Y: ir.Buffer, + template_buffer: ir.Buffer, + epilogue_nodes: list[ir.IRNode], + reindexers: list[Callable[[list[Any]], list[Any]] | None], + default_reindexers: list[Callable[[list[Any]], list[Any]] | None], +) -> tuple[ + ir.Buffer | ir.ReinterpretView, + list[Callable[[list[Any]], list[Any]] | None], +]: + """ + The dimension and the indexing could be different between the GEMM output, i.e. `template_buffer`, which is + 2D with MxN) and the output from the template after epilogues, i.e. `Y`. In the GEMM template code, + we are not aware of the dimension and the indexing of the epilogues and always work on 2D tiles according to + the indexing of the GEMM output. + In this function, we return a 2D buffer (`Y_2d`) according to GEMM output (reinterpreted from `Y` if needed) and + build a reindexer that converts the indexing of `Y` into `Y_2d`. + """ + Y_2d: ir.Buffer | ir.ReinterpretView = Y + if ( + Y.get_size() == template_buffer.get_size() + and Y.get_stride() == template_buffer.get_stride() + ): + reindexers.extend(default_reindexers) + Y_2d = Y + else: + + def get_reindexer(epilogue_node, default_reindexer=None): + # From template_buffer to epilogue_node_ordered (ordered by stride decreasingly, in dense format), for example: + # template_buffer: + # size (324, 512), stride (512, 1) + # epilogue_node_ordered (ordered by stride decreasingly, in dense format): + # size (1, 18, 18, 512), stride (165888, 9216, 512, 1) + stride_order = list( + ir.get_stride_order( + V.graph.sizevars.guarding_hints_or_throw(epilogue_node.get_stride()) + ) + ) + fill_order = ir.stride_order2fill_order(stride_order) + reversed_fill_order = list(reversed(fill_order)) + size_with_stride_ordered_decreasingly = [ + epilogue_node.get_size()[i] for i in reversed_fill_order + ] + reshape_reindex = ir.View.dynamic_reshape_indexer( + size_with_stride_ordered_decreasingly, + template_buffer.get_size(), + ) + if default_reindexer: + reshape_reindex = ir.fuse_reindexing(reshape_reindex, default_reindexer) + + # From epilogue_node_ordered (ordered by stride decreasingly, in dense format) to epilogue_node, for example: + # epilogue_node_ordered (ordered by stride decreasingly, in dense format): + # size (1, 18, 18, 512), stride (165888, 9216, 512, 1) + # epilogue_node: + # size (1, 18, 18, 512), stride (165888, 1, 9216, 512) + from_stride_ordered_decreasingly_to_epilogue_node_order = [ + (len(stride_order) - 1) - stride_order[i] + for i in range(len(stride_order)) + ] + stride_reindex = ir.same_reorder( + from_stride_ordered_decreasingly_to_epilogue_node_order + ) + + reindexer = ir.fuse_reindexing(stride_reindex, reshape_reindex) # type: ignore[var-annotated] + return reindexer + + if default_reindexers is None: + default_reindexers = [None] * len(epilogue_nodes) + new_reindexers = [ + get_reindexer(epilogue_node, default_reindexer) + for epilogue_node, default_reindexer in zip( + epilogue_nodes, default_reindexers + ) + ] + reindexers.extend(new_reindexers) + if isinstance(Y, ir.BaseView): + storage = ir.StorageBox(Y.unwrap_view()) + else: + assert isinstance(Y, ir.Buffer) + storage = ir.StorageBox(Y) + Y_2d = ir.ReinterpretView(data=storage, layout=template_buffer.get_layout()) + return Y_2d, reindexers + + +class CppGemmTemplate(CppTemplate): + """ + GEMM Template for Inductor CPP Backend. + """ + + def __init__( + self, + input_nodes, + layout: ir.Layout, + num_threads: int, + register_blocking: GemmBlocking, + beta=1, + alpha=1, + has_bias=False, + epilogue_creator: Callable[[ir.Buffer], ir.Pointwise] | None = None, + should_block_weights: bool = True, + name="packed_gemm", + ) -> None: + assert layout.dtype in [ + torch.float, + torch.bfloat16, + torch.half, + torch.uint8, + torch.int8, + ] + super().__init__( + name, + input_nodes, + layout, + num_threads, + epilogue_creator=epilogue_creator, + ) + self.beta = beta + self.alpha = alpha + self.has_bias = has_bias + self.register_blocking = register_blocking + m, n = layout.size[-2:] + k = input_nodes[0].get_size()[-1] + self.m, self.n, self.k = m, n, k + self.padded_n = get_padded_n(n, self.register_blocking.block_n) + self.is_dynamic_M = has_free_symbols((m,)) + self.should_block_weights = should_block_weights + self.thread_blocking = self.make_thread_blocking_cache() + self.cache_blocking = self.make_cache_blocking_cache() + + def make_thread_blocking_cache(self): + cache = lru_cache()(self._thread_blocking) + + def thread_blocking(num_threads: int) -> GemmBlocking: + return cache(num_threads) + + return thread_blocking + + def _thread_blocking(self, num_threads: int) -> GemmBlocking: + """ + NOTE [Thread blocking in Cpp GEMM] + We use simple heuristics to decide the thread blocking: + 1. Make sure all threads are occupied as much as possible. + 2. For (m, n) blocks, favor more square-sized thread blocks for better data reuse. + 3. If (m, n) blocks cannot occupy all the threads, we consider k-slicing. + TODO(jgong5): allow tuning various blocking options + """ + + def get_factors(number): + factors = [] + for i in range(int(number**0.5), 0, -1): + if number % i == 0: + factors.append(number // i) + factors.append(i) + return factors + + def get_blocking(m_factor, n_factor, k_factor, m_blocks, n_blocks, k_blocks): + thread_block_k = math.ceil(k_blocks / k_factor) + thread_block_n = math.ceil(n_blocks / n_factor) + thread_block_m = math.ceil(m_blocks / m_factor) + return GemmBlocking(thread_block_m, thread_block_n, thread_block_k) + + assert not self.is_dynamic_M, ( + "Unable to determine thread blocking for dynamic M." + ) + register_blocking = self.register_blocking + m_blocks = math.ceil(self.m / register_blocking.block_m) + n_blocks = math.ceil(self.n / register_blocking.block_n) + k_blocks = math.ceil(self.k / register_blocking.block_k) + factors = get_factors(num_threads) + assert len(factors) > 0 + + if config.cpp.gemm_thread_factors is not None: + factors = [int(i) for i in config.cpp.gemm_thread_factors.split(",")] + assert len(factors) == 3 + assert math.prod(factors) == self.num_threads + return get_blocking( + factors[0], factors[1], factors[2], m_blocks, n_blocks, k_blocks + ) + + # we favor square-sized thread blocks for good data reuse + def get_better_blocking(blocking, best_blocking): + if best_blocking is None: + best_blocking = blocking + else: + block_m_size = blocking.block_m * register_blocking.block_m + block_n_size = blocking.block_n * register_blocking.block_n + best_block_m_size = best_blocking.block_m * register_blocking.block_m + best_block_n_size = best_blocking.block_n * register_blocking.block_n + if blocking.block_k > best_blocking.block_k: + best_blocking = blocking + elif ( + blocking.block_k == best_blocking.block_k + and block_m_size + block_n_size + < best_block_m_size + best_block_n_size + ): + best_blocking = blocking + return best_blocking + + best_blocking = None + # check if we can have a thread-blocking to occupy all threads without k-slicing + for n_factor in factors: + m_factor = num_threads // n_factor + if n_blocks >= n_factor and m_blocks >= m_factor: + blocking = get_blocking( + m_factor, n_factor, 1, m_blocks, n_blocks, k_blocks + ) + best_blocking = get_better_blocking(blocking, best_blocking) + + if best_blocking is None: + for k_factor in factors: + if k_blocks >= k_factor and ( + config.cpp.gemm_max_k_slices == 0 + or k_factor <= config.cpp.gemm_max_k_slices + ): + n_factors = get_factors(num_threads // k_factor) + for n_factor in n_factors: + m_factor = (num_threads // k_factor) // n_factor + if n_blocks >= n_factor and m_blocks >= m_factor: + blocking = get_blocking( + m_factor, + n_factor, + k_factor, + m_blocks, + n_blocks, + k_blocks, + ) + best_blocking = get_better_blocking(blocking, best_blocking) + + if best_blocking is None: + for n_factor in factors: + m_factor = num_threads // n_factor + if n_blocks >= n_factor or m_blocks >= m_factor: + blocking = get_blocking( + m_factor, n_factor, 1, m_blocks, n_blocks, k_blocks + ) + best_blocking = get_better_blocking(blocking, best_blocking) + + assert best_blocking is not None + return best_blocking + + def make_cache_blocking_cache(self): + cache = lru_cache()(self._cache_blocking) + + def cache_blocking(num_threads: int) -> GemmBlocking: + return cache(num_threads) + + return cache_blocking + + def _cache_blocking(self, num_threads: int) -> GemmBlocking: + def get_cache_blocking(register_blocking, thread_blocking): + Mr = register_blocking.block_m + Nr = register_blocking.block_n + Kr = register_blocking.block_k + + Mt_blocks = thread_blocking.block_m + Nt_blocks = thread_blocking.block_n + Kt_blocks = thread_blocking.block_k + + if config.cpp.gemm_cache_blocking is not None: + blockings = [int(i) for i in config.cpp.gemm_cache_blocking.split(",")] + assert len(blockings) == 3 + Mc_blocks, Nc_blocks, Kc_blocks = blockings + return ( + min(Mc_blocks, Mt_blocks), + min(Nc_blocks, Nt_blocks), + min(Kc_blocks, Kt_blocks), + ) + + # The ratios below are empirically determined to decide + # the effective sizes of L1 and L2. + # TODO: tune the factor here + L1_limit_factor = 0.8 + L2_limit_factor = 0.5 + + L1_cache_size = torch.cpu.get_capabilities().get( + "l1d_cache_size", 0 + ) # per core cache size in Bytes + assert L1_cache_size > 0, ( + f"Expect L1_cache_size > 0 but got {L1_cache_size}" + ) + L1 = L1_cache_size * L1_limit_factor + + L2_cache_size = torch.cpu.get_capabilities().get( + "l2_cache_size", 0 + ) # per core cache size in Bytes + assert L2_cache_size > 0, ( + f"Expect L2_cache_size > 0 but got {L2_cache_size}" + ) + L2 = L2_cache_size * L2_limit_factor + + def get_num_byte(dtype): + return torch.tensor([], dtype=dtype).element_size() + + dtype_A = self.input_nodes[0].get_dtype() + dtype_B = self.input_nodes[1].get_dtype() + num_byte_A = get_num_byte(dtype_A) + num_byte_B = get_num_byte(dtype_B) + if dtype_A is torch.bfloat16 and dtype_B is torch.int8 and Kr != 1: + # We will cache dequantized weights (BF16) in L1D for AMX micro-kernel. + # In this case, the choice of the micro-kernel being used can't be decoupled from + # the cache blocking. + # TODO: Decouple the choice of micro-kernel from cache blocking + num_byte_B *= num_byte_A + + # NOTE [CPP GEMM Cache Blocking Algorithm] + # Our overall strategy is to + # 1) Make cache blocks of B L1-reside and reused by multiple rows of A, i.e. Mc. + # Here, B is Kc x Nr where Nr is a single register block. We use L1 size to + # decide Kc. We want to make Mc large enough to better reuse B. + # 2) Make cache blocks of A L2-reside, which would limit Mc. We want to reuse A + # along N, where we have two sub-strategies (see notes below) to decide Mc and Nc. + + # Step 1: Decide Kc assuming B block is L1-reside. + size_cache_B = Kr * Kt_blocks * Nr * num_byte_B + + Kc_blocks = Kt_blocks + if size_cache_B > L1: + Kc_blocks = math.floor(L1 / (Kr * Nr * num_byte_B)) + + if ( + config.cpp.use_small_dequant_buffer + and dtype_A is torch.bfloat16 + and Mt_blocks == 1 + ): + if dtype_B is torch.uint8: + # A16W4 + # Make a small dequant_B buffer for woq int4 [q_group_size, Nr] + # Since when Mt_blocks == 1, L1-reside B block can't be reused by A. + if Kc_blocks * Kr >= self.q_group_size(): + Kc_blocks = self.q_group_size() // Kr + + elif dtype_B is torch.int8: + # A16W8 + # Make A, B, C buffer in L1 + A_buf_size_div_K = self.m * num_byte_A + B_buf_size_div_K = Nr * num_byte_B + # assume acc in float32/int32 and Mc_blocks = Nc_blocks = 1 + C_buf_size = Mr * Nr * 4 + K_block_size = (L1 - C_buf_size) // ( + A_buf_size_div_K + B_buf_size_div_K + ) + if Kc_blocks * Kr >= K_block_size: + Kc_blocks = (K_block_size + Kr - 1) // Kr + + # Step 2: Decide Mc assuming A block is L2-reside. + min_Mc_ratio = 2 # TODO(jgong5): something to tune? + min_Mc_blocks = math.ceil(min_Mc_ratio * Mr / Nr) + assert min_Mc_blocks >= 1 + Kt_bytes = Kt_blocks * Kr * num_byte_A + if min_Mc_blocks * Mr * Kt_bytes < L2: + # Strategy 1: A (Mc x Kt) resides in L2 and reused by all Nt + # when Nc_blocks is kept 1. Mc should be large enough (>= min_Mc_blocks) + # to reuse B (Kc x Nr) in L1. This makes C (Mc x Nr) small enough to reside + # in L1. + Mc_blocks = min(Mt_blocks, math.floor(L2 / (Mr * Kt_bytes))) + Nc_blocks = 1 + else: + # Strategy 2: Kt is too large to hold A (Mc x Kt) in L2, we reuse + # A (Mc x Kc) in L2 by B (Kc x Nc). C (Mc x Nc) resides in L2. + Mc_blocks = Mt_blocks + Nc_blocks = min(math.ceil(Mc_blocks * Mr / Nr), Nt_blocks) + Nc_bytes = Nc_blocks * Nr * 4 # assume C or acc is float32/int32 + Kc_bytes = Kc_blocks * Kr * num_byte_A + if Mc_blocks * Mr * (Kc_bytes + Nc_bytes) > L2: + # The following is the solution for 4*Mc*Nc + Mc*Kc_bytes = L2, + # assuming Mc == Nc for good data reuse. + M_max = (math.sqrt(Kc_bytes * Kc_bytes + 16 * L2) - Kc_bytes) / 8 + if M_max < Mc_blocks * Mr: + Mc_blocks = math.floor(M_max / Mr) + Nc_blocks = min(math.ceil(Mc_blocks * Mr / Nr), Nt_blocks) + + return Mc_blocks, Nc_blocks, Kc_blocks + + assert not self.is_dynamic_M, ( + "Unable to determine cache blocking for dynamic M." + ) + register_blocking = self.register_blocking + thread_blocking = self.thread_blocking(num_threads) + + return GemmBlocking(*get_cache_blocking(register_blocking, thread_blocking)) + + def log_blockings(self): + log.debug(f"Register blocking: {self.register_blocking}") # noqa: G004 + if self.is_dynamic_M: + # thread and cache blockings are determined at runtime for dynamic shapes + return + log.debug( + f"Cache blocking: {self.cache_blocking(self.num_threads)}" # noqa: G004 + ) + thread_blocking = self.thread_blocking(self.num_threads) + log.debug(f"Thread blocking: {thread_blocking}") # noqa: G004 + + def get_occupancy(): + m_blocks = math.ceil(self.m / self.register_blocking.block_m) + n_blocks = math.ceil(self.n / self.register_blocking.block_n) + k_blocks = math.ceil(self.k / self.register_blocking.block_k) + m = math.ceil(m_blocks / thread_blocking.block_m) + n = math.ceil(n_blocks / thread_blocking.block_n) + k = math.ceil(k_blocks / thread_blocking.block_k) + return (m, n, k) + + log.debug( + f"Number of threads: {self.num_threads}, occupancy: {get_occupancy()}" # noqa: G004 + ) + + def maybe_k_slicing(self): + if self.num_threads == 1: + return False + if self.is_dynamic_M: + # TODO(jgong5): perhaps use size hint to decide? + return True + register_blocking = self.register_blocking + k_blocks = math.ceil(self.k / register_blocking.block_k) + thread_blocking = self.thread_blocking(self.num_threads) + return k_blocks > thread_blocking.block_k + + @classmethod + def add_choices( + cls, + choices, + layout, + input_nodes, + beta=1, + alpha=1, + has_bias=False, + trans_w=False, + input_indices=None, + epilogue_creator: Callable[[ir.Buffer], ir.Pointwise] | None = None, + act_mapping: dict[int, ir.IRNode] | None = None, + ): + """ + Add choices for the GEMM template. + """ + # Fast path to save the epilogue calculation when x_scale/x_zp/w_scale are constant + use_int8_fast_compensation_path = _is_int8_gemm(input_nodes) and all( + ( + isinstance(input_nodes[idx], ir.TensorBox) + and isinstance(input_nodes[idx].data.data, ir.ConstantBuffer) + ) + for idx in [1, 2, 4] + ) + + if input_indices is None: + input_indices = list(range(len(input_nodes))) + + def reorder_and_filter(inputs, layout_or_out): + if has_bias: + assert len(input_indices) >= 3 + # Assume the input order is [inp, x, w] and we reorder it to [x, w, inp] + inp_idx = input_indices[0] + x_idx = input_indices[1] + w_idx = input_indices[2] + return [ + inputs[x_idx], + inputs[w_idx], + inputs[inp_idx], + *[inputs[idx] for idx in input_indices[3:]], + ], layout_or_out + elif len(inputs) >= len(input_indices): + assert len(input_indices) >= 2 + return [inputs[idx] for idx in input_indices], layout_or_out + else: + # For when input is used for x and w, i.e. X@X.T or similar + # Assumes the first input is the only input + assert len(inputs) == 1 + return [inputs[0]] * len(input_indices), layout_or_out + + new_inputs, new_layout = reorder_and_filter(input_nodes, layout) + is_mkldnn_wgt = ( + new_inputs[1].get_name() in V.graph.constants + and V.graph.constants[new_inputs[1].get_name()].is_mkldnn + ) + if is_mkldnn_wgt: + # It shouldn't happen as viewing an mkldnn tensor, we can extend the + # implementation if it does. + assert not isinstance(new_inputs[1], ir.BaseView) + # Note that the layout of MKLDNN Tensor is with the wrong stride + view_size = new_inputs[1].layout.size + view_stride = new_inputs[1].layout.stride + view_offset = new_inputs[1].layout.offset + + def maybe_to_dense(inputs, layout_or_out): + new_inputs = list(inputs) + if isinstance(inputs[1], torch.Tensor): + W = inputs[1] + new_inputs[1] = W.to_dense() if W.is_mkldnn else W + return new_inputs, layout_or_out + + def normalize_shapes(inputs, layout_or_out): + new_inputs = list(inputs) + if not is_mkldnn_wgt and isinstance(new_inputs[1], torch.Tensor): + if has_free_symbols(view_size): + # If batch size B is dynamic, we need to set the batch size and possibly stride + assert not has_free_symbols(view_size[1:]) + view_size[:] = V.graph.sizevars.guarding_hints_or_throw(view_size) + view_stride[:] = V.graph.sizevars.guarding_hints_or_throw( + view_stride + ) + # With the assumptation that W is the storage of unwrap view + # thus view it back here + new_inputs[1] = new_inputs[1].as_strided( + view_size, view_stride, view_offset + ) + + if not trans_w: + return new_inputs, layout_or_out + X = new_inputs[0] + W = new_inputs[1] + B = new_inputs[2] if has_bias else None + W = transpose_w(W, trans_w) + B = expand_bias(B, X) # type:ignore[arg-type] + new_inputs[1] = W + if B is not None: + new_inputs[2] = B + return new_inputs, layout_or_out + + # TODO(jgong5): decide proper number of threads per problem size + num_threads = parallel_num_threads() + new_inputs, _ = normalize_shapes(*maybe_to_dense(new_inputs, new_layout)) + m, n, k, *_ = mm_args( + new_inputs[0], + new_inputs[1], + mat2_transposed=cls.is_woq_int4(), + use_4x2_dim=cls.is_woq_int4(), + ) + output_dtype, compute_dtype = get_gemm_template_output_and_compute_dtype( + new_inputs[0].get_dtype() + ) + micro_gemm = create_micro_gemm( + "micro_gemm", + m, + n, + k, + input_dtype=new_inputs[0].get_dtype(), + input2_dtype=new_inputs[1].get_dtype(), + output_dtype=output_dtype, + compute_dtype=compute_dtype, + alpha=alpha, + num_threads=num_threads, + use_ref=not cls.is_woq_int4(), + q_group_size=cls.q_group_size(), + ) + assert micro_gemm is not None + pre_block_weights = cls.check_if_block_weight(new_inputs[1], micro_gemm) + micro_gemm.use_local_vnni_blocking(not pre_block_weights) + only_one_input = ( + input_nodes[0] == input_nodes[1] if len(input_nodes) > 1 else False + ) and not pre_block_weights # If weights are blocked, use the second input + + def preprocessor(inputs, layout): + new_inputs, new_layout = normalize_shapes( + *maybe_to_dense(*reorder_and_filter(inputs, layout)) + ) + if only_one_input and isinstance(new_inputs[0], torch.Tensor): + return new_inputs[1:], new_layout + return cls.prep_weight( + new_inputs, + new_layout, + # pyrefly: ignore [bad-argument-type] + micro_gemm, + pre_block_weights, + use_int8_fast_compensation_path, + ) + + def postprocessor(output): + if isinstance(output, ir.TensorBox): + # prepack the weight as input to the template buffer + template_buffer = ir.InputsKernel.unwrap_storage_for_input(output) + assert isinstance(template_buffer, ir.CppTemplateBuffer) + new_input_nodes, _ = reorder_and_filter(input_nodes, layout) + + W_node = new_input_nodes[1] + if W_node.get_name() not in V.graph.constants: + return output + W = V.graph.constants[W_node.get_name()] + new_input_nodes[1] = W + new_input_nodes, new_layout = normalize_shapes( + *maybe_to_dense(new_input_nodes, layout) + ) + new_input_nodes, _ = cls.prep_weight( + new_input_nodes, + new_layout, + # pyrefly: ignore [bad-argument-type] + micro_gemm, + pre_block_weights, + use_int8_fast_compensation_path, + skip_int8_compensation=True, + ) + W_packed = new_input_nodes[1] + W_packed_constant = V.graph.add_tensor_constant(W_packed) + new_input_nodes[1] = W_packed_constant + + # Prune unused tensors + prune_tensors(input_nodes, new_input_nodes) + + template_buffer.inputs[1] = ir.InputsKernel.unwrap_storage_for_input( + W_packed_constant + ) + return output + + template = DataProcessorTemplateWrapper( + cls, + preprocessor, + postprocessor, + input_nodes=input_nodes, + layout=layout, + num_threads=num_threads, + register_blocking=micro_gemm.register_blocking, + beta=beta, + alpha=alpha, + has_bias=has_bias, + epilogue_creator=epilogue_creator, + should_block_weights=pre_block_weights, + name=micro_gemm.__class__.__name__, + ) + template.maybe_append_choice(choices) + return template + + @staticmethod + def get_padded_size(n, block_n, k, should_block_weight): + padded_n = get_padded_n(n, block_n) + # We assume that all GEMM weight tensors should be blocked and padded + new_size = [padded_n // block_n, k, block_n] + return new_size, padded_n + + @staticmethod + def _maybe_remove_storage_offset(node: ir.IRNode): + if node.get_layout().offset == 0: + return node + # node may be contiguous but still have a non-zero storage offset. + # GEMM_TEMPLATE emits code like: + # W.data_ptr[node.offset + ...] + # but runtime W.data_ptr (after normalize_shapes()) already includes this offset. + # To avoid double-offsetting, we remove the offset in the node also in the generated code. + # W.data_ptr[...] + return ir.ExternKernel.copy_input(node) + + @classmethod + def prep_weight( + cls, + inputs, + layout: ir.Layout, + micro_gemm: CppMicroGemm, + should_block_weight: bool, + use_int8_fast_compensation_path: bool = False, + skip_int8_compensation: bool = False, + ): + """ + NOTE Weight prep consists of 2 separate steps: + 1. Blocking the weight tensor into a 3D shape: [n//block_n, k, block_n] + This is always done if the weight tensor is constant, i.e. for all GEMM and some BMM. + For BMM, we also block non-contiguous weight tensors, since they would be reshaped anyway. + This assumes that blocked, contiguous weights will be more efficient for the GEMM kernel, + and is worth the overhead of reshape and blocking. + + This blocking includes additional padding, when n is not a multiple of block_n. + This padding allows a more efficient microkernel implementation. For BMM, this is only done + if reshape would happen anyway, i.e. if the weight tensor is constant, is not contiguous, + or is using AMX VNNI layout. + 2. Packing the weight tensor into a VNNI-friendly shape. For constant input, + this is done at the same time as the weight blocking. + + At compile time, the constant weight tensors are blocked and packed. For non-constant tensors (e.g. BMM) + which will be blocked (non-contiguous or VNNI-layout tensors), the weight tensor is blocked and packed at runtime. + + CppBmmTemplate overrides the methods get_padded_size, and block_weight in order to accommodate + an additional dimension for the batch size and to determine if the weight tensor should be blocked. + """ + W = inputs[1] + new_inputs = list(inputs) + if cls.is_woq_int4(): + assert ( + len(W.get_size()) == 2 + if isinstance(W, ir.IRNode) + else len(W.shape) == 2 + ) + n, k = W.get_size() if isinstance(W, ir.IRNode) else W.shape + else: + k, n = W.get_size()[-2:] if isinstance(W, ir.IRNode) else W.shape[-2:] + _, block_n, _ = micro_gemm.register_blocking + new_size, padded_n = cls.get_padded_size(n, block_n, k, should_block_weight) + padding = padded_n - n + + if should_block_weight and not cls.is_woq_int4(): + blocked_w = cls.block_weight(W, new_size, padding) + new_inputs[1] = cls.pack_vnni_weight(blocked_w, micro_gemm, new_size) + elif should_block_weight: + assert cls.is_woq_int4() + new_inputs[1] = cls.block_weight(W, new_size, padding) + elif isinstance(W, ir.IRNode): + # Require W layout to be fixed & contiguous, happens inplace. + ir.ExternKernel.require_contiguous(W) + new_inputs[1] = cls._maybe_remove_storage_offset(W) + + if not skip_int8_compensation and _is_int8_gemm(new_inputs): + BCompensate = None + x_w_scale = None + + def _get_compensation_node(W, use_int8_fast_compensation_path): + BCompensate = V.graph.add_tensor_constant( + V.graph.constants[W.get_name() + "_BMatrixCompens"], + W.get_name() + "_BMatrixCompens", + ) + x_w_scale = None + if use_int8_fast_compensation_path: + x_w_scale = V.graph.add_tensor_constant( + V.graph.constants[W.get_name() + "_x_w_compens"], + W.get_name() + "_x_w_compens", + ) + return BCompensate, x_w_scale + + if use_int8_fast_compensation_path: + # new_inputs has been reordered: [x, w, optional[bias], x_scale, x_zp, w_scale, w_zp] + x_scale = new_inputs[-4] + x_zp = new_inputs[-3] + w_scale = new_inputs[-2] + if isinstance(W, ir.IRNode): + BCompensate, x_w_scale = _get_compensation_node( + W, use_int8_fast_compensation_path + ) + else: + # Use the original W, not the blocked_w in new_inputs[1] to calculate BCompensate + BCompensate = torch.sum(W.to_dense().to(torch.float), dim=0) # type: ignore[assignment] + assert all( + isinstance(item, torch.Tensor) + for item in (x_scale, x_zp, w_scale) + ) + BCompensate = BCompensate * x_scale * w_scale * x_zp + x_w_scale = x_scale * w_scale + new_inputs.append(BCompensate) + new_inputs.append(x_w_scale) + else: + if isinstance(W, ir.IRNode): + BCompensate, _ = _get_compensation_node( + W, use_int8_fast_compensation_path + ) + else: + # Use the original W, not the blocked_w in new_inputs[1] to calculate BCompensate + BCompensate = torch.sum(W.to_dense().to(torch.float), dim=0) # type: ignore[assignment] + new_inputs.append(BCompensate) + return new_inputs, layout + + @staticmethod + def check_if_block_weight(W, micro_gemm): + return True + + @classmethod + def block_weight(cls, W, new_size, padding): + # These are separated into two methods to allow subclasses to override them separately + if isinstance(W, ir.IRNode): + if W.get_name() in V.graph.constants: + # Create a new buffer, representing the constant blocked tensor + blocked_w = ir.Buffer( + name=W.get_name(), # Borrow the registered buffer name + layout=ir.FixedLayout( + W.get_device_or_error(), + W.get_dtype(), + new_size, + ir.FlexibleLayout.contiguous_strides(new_size), + 0, + ), + ) + else: + if not isinstance(W, ir.TensorBox): + W = ir.TensorBox(W) + permute_dims = list(range(len(new_size))) + permute_dims[-2], permute_dims[-3] = permute_dims[-3], permute_dims[-2] + permute_size = list(new_size) + permute_size[-2], permute_size[-3] = permute_size[-3], permute_size[-2] + blocked_w = L.constant_pad_nd(W, (0, padding)) + blocked_w = L.permute( + L.view(blocked_w, permute_size), # type: ignore[arg-type] + permute_dims, + ) + else: + assert isinstance(W, torch.Tensor) + # Pad the weight tensor and reshape it into a 3D blocked shape + blocked_size = list(new_size) + blocked_size[-2], blocked_size[-3] = blocked_size[-3], blocked_size[-2] + blocked_w = ( + torch.nn.functional.pad(W, (0, padding)) # type: ignore[assignment] + .reshape(*blocked_size) + .transpose(-3, -2) + .contiguous() + ) + return blocked_w + + @classmethod + def pack_vnni_weight(cls, W, micro_gemm, new_size): + # WOQ INT4 weights are reordered in microkernel so do not pack them here + should_pack = ( + micro_gemm.get_b_layout() != LayoutType.NORMAL + and not micro_gemm.is_woq_int4() + ) + + # These are separated into two methods to allow subclasses to override them separately + if isinstance(W, ir.IRNode): + if isinstance(W, ir.Buffer) and W.get_name() in V.graph.constants: + return W + k = new_size[-2] + if not isinstance(W, ir.TensorBox): + W = ir.TensorBox(W) + if should_pack: + permute_dims = list(range(len(new_size) + 1)) + permute_dims[-1], permute_dims[-2] = permute_dims[-2], permute_dims[-1] + vnni_size = 4 if micro_gemm.get_b_layout() == LayoutType.VNNI4 else 2 + vnni_view_size = list(new_size) + vnni_view_size[-2] = k // vnni_size + vnni_view_size.insert(-1, vnni_size) + W = L.view( + L.permute(L.view(W, vnni_view_size), permute_dims), + new_size, + ) + W = ir.ExternKernel.realize_input(W) + W = ir.ExternKernel.require_contiguous(W) + return W + else: + k = new_size[-2] + # Apply VNNI packing to the weight tensor + if should_pack: + # TODO: Move VNNI weight packing for non-constant tensors into the template, + # to improve cache locality and avoid full-tensor copy. + layout_str = ( + "VNNI4" + if micro_gemm.get_b_layout() == LayoutType.VNNI4 + else "VNNI2" + ) + assert micro_gemm.get_b_layout() in [ + LayoutType.VNNI2, + LayoutType.VNNI4, + ], f"We only support {layout_str} for now" + vnni_size = 4 if micro_gemm.get_b_layout() == LayoutType.VNNI4 else 2 + assert k % vnni_size == 0, ( + f"k should be divisible by vnni_size for {layout_str} layout" + ) + vnni_view_size = list(new_size) + vnni_view_size[-2] = k // vnni_size + vnni_view_size.insert(-1, vnni_size) + W = W.view(vnni_view_size).transpose(-1, -2).contiguous().view(new_size) + # normalize stride to be "contiguous_strides" per size + # this avoids the problems in L.view during template codegen + new_stride = [1] + for sz in reversed(W.shape[1:]): + new_stride.insert(0, new_stride[0] * sz) + W = W.as_strided(W.shape, new_stride) + return W + + def get_default_reindexers(self, epilogue_nodes): + return [None] * len(epilogue_nodes) + + def get_options( + self, + kernel: CppTemplateKernel, + template_buffer_node: ir.CppTemplateBuffer | None = None, + flag_template_buffer_has_other_users: bool | None = None, + epilogue_nodes: list[ir.IRNode] | None = None, + ) -> dict[str, Any]: + assert len(self.input_nodes) >= 2 + + int8_gemm = self.input_nodes[0].get_dtype() in [torch.uint8, torch.int8] + x_scale = None + x_zp = None + w_scale = None + w_zp = None + inp = None + q_group_size_node = None + qscale_and_zeros = None + if int8_gemm: + X, W = self.input_nodes[0], self.input_nodes[1] + bias_idx = 2 if self.has_bias else 1 + inp = self.input_nodes[bias_idx] if self.has_bias else None + x_scale = self.input_nodes[bias_idx + 1] + x_zp = self.input_nodes[bias_idx + 2] + w_scale = self.input_nodes[bias_idx + 3] + w_zp = self.input_nodes[bias_idx + 4] + Y = self.output_node + elif self.is_woq_int4(): + X, W = self.input_nodes[0], self.input_nodes[1] + Y = self.output_node + q_group_size_node = self.input_nodes[2] + qscale_and_zeros = self.input_nodes[3] + else: + X, W = self.input_nodes[0], self.input_nodes[1] + Y = self.output_node + inp = self.input_nodes[2] if self.has_bias else None + + template_buffer_has_other_users = None + + if template_buffer_node is not None: + # Use the updated prepacked weight buffer + W = template_buffer_node.inputs[1] + Y = template_buffer_node + + assert flag_template_buffer_has_other_users is not None + template_buffer_has_other_users = flag_template_buffer_has_other_users + + template_buffer = Y + gemm_output_buffer = template_buffer + + epilogues: list[ir.IRNode] = [] + reindexers: list[Callable[[list[Any]], list[Any]] | None] = [] + epilogue_creators: list[Callable[[ir.Buffer], ir.Pointwise]] = [] + fake_buffers: list[ir.Buffer] = [] + Y_aliases: OrderedSet[str] = OrderedSet() + + use_local_acc = ( + self.layout.dtype != torch.float + or template_buffer_has_other_users + or int8_gemm + or self.padded_n != self.n + or self.maybe_k_slicing() + or (epilogue_nodes and epilogue_nodes[-1].get_dtype() != self.layout.dtype) + ) + + # TODO(jgong5): for int8 gemm, bias-add is handled outside of gemm template, + # but we'd better move it here to align with fp. + if inp is not None and self.beta != 0 and not int8_gemm: + # add an epilogue for bias add + def _bias_add_epilogue(buf): + return create_epilogue_with_attr( + buf, "bias_add", other=inp, beta=self.beta, dtype=self.layout.dtype + ) + + epilogue_creators.append(_bias_add_epilogue) + + if self.epilogue_creator is not None: + epilogue_creators.append(self.epilogue_creator) + + # When the GEMM output buffer is localized but it has users other than the epilogue nodes, + # we need to copy the value in the GEMM output local buffer to a global buffer. + def need_copy_from_local_to_global_buffer_epilogue( + use_local_acc, template_buffer_has_other_users, epilogue_creators + ): + # The GEMM output buffer is a global buffer, thus copy is not needed. + if not use_local_acc: + return False + + # The possible value of template_buffer_has_other_users is (None, False, True) + # It is None when generating the gemm template during autotune and it will have value during scheduler codegen. + # extra copy_from_local_to_global_buffer_epilogue is not needed in either of the below two cases: + # 1. template_buffer_has_other_users is None (i.e. when doing the codegen during autotune) + # 2. template_buffer_has_other_users is False, which means it's safe to keep the value in the + # GEMM output buffer in local buffer only (no users outside of the epilogues will use its value). + if not template_buffer_has_other_users: + return False + + # When bias is not None or self.epilogue_creator is not None, + # there will be epilogue_creators after the GEMM. + # The GEMM output buffer is localized while + # the output buffer of the epilogue_creators is a global buffer. + if epilogue_creators: + return False + + return True + + if need_copy_from_local_to_global_buffer_epilogue( + use_local_acc, template_buffer_has_other_users, epilogue_creators + ): + + def copy_from_local_to_global_buffer_epilogue(input_buffer: ir.Buffer): + dtype = self.layout.dtype + input_loader = input_buffer.make_loader() + + def copy_inner(index): + input = input_loader(index) + result = ops.to_dtype(input, dtype) + return result + + return ir.Pointwise( + device=input_buffer.get_device_or_error(), + dtype=self.layout.dtype, + inner_fn=copy_inner, + ranges=input_buffer.get_size(), + ) + + epilogue_creators.append(copy_from_local_to_global_buffer_epilogue) + + # NOTE [How CPP GEMM template epilogues are organized] + # gemm_output_buffer + # --> zero or more in-template epilogues (created by `epilogue_creators`) --> + # template_buffer + # --> zero or more out-of-template epilogues (`epilogue_nodes`) --> + # Y + if epilogue_creators: + assert isinstance(template_buffer, ir.IRNode) + gemm_output_name = f"{template_buffer.get_name()}_GemmOut" + gemm_output_buffer = ir.Buffer( + name=gemm_output_name, + # pyrefly: ignore [missing-attribute] + layout=template_buffer.layout, + ) + current_input_buffer = gemm_output_buffer + for i, creator in enumerate(epilogue_creators): + if i == len(epilogue_creators) - 1: + buffer_name = template_buffer.get_name() + else: + buffer_name = f"{gemm_output_name}_epilogue_{i}" + epilogues.append( + ir.ComputedBuffer( + name=buffer_name, + # pyrefly: ignore [missing-attribute] + layout=template_buffer.layout, + data=creator(current_input_buffer), + ) + ) + fake_buffers.append(current_input_buffer) + Y_aliases.add(current_input_buffer.get_name()) + reindexers.append(None) + if i < len(epilogue_creators) - 1: + current_input_buffer = ir.Buffer( + name=buffer_name, + # pyrefly: ignore [missing-attribute] + layout=template_buffer.layout, + ) + + assert isinstance(Y, (ir.Buffer, ir.ReinterpretView)) + Y_2d: ir.Buffer | ir.ReinterpretView = Y + + if epilogue_nodes: + if not template_buffer_has_other_users: + assert isinstance(template_buffer, ir.IRNode) + Y_aliases.add(template_buffer.get_name()) + epilogues.extend(epilogue_nodes) + assert Y.get_numel() == epilogues[-1].get_numel() + Y = cast(ir.Buffer, epilogues[-1]) + assert isinstance(template_buffer, ir.Buffer) + Y_2d, reindexers = gen_2d_view_of_epilogue_buf( + Y, + template_buffer, + epilogue_nodes, + reindexers, + default_reindexers=self.get_default_reindexers(epilogue_nodes), + ) + + output_dtype, compute_dtype = get_gemm_template_output_and_compute_dtype( + X.get_dtype() + ) + micro_gemm = create_micro_gemm( + f"{kernel.kernel_name}_micro_gemm", + self.m, + self.n, + self.k, + input_dtype=X.get_dtype(), + input2_dtype=W.get_dtype(), + output_dtype=output_dtype, + compute_dtype=compute_dtype, + alpha=self.alpha, + num_threads=self.num_threads, + use_ref=not self.is_woq_int4(), + q_group_size=self.q_group_size(), + ) + assert micro_gemm is not None + micro_gemm.use_local_vnni_blocking(not self.should_block_weights) + assert self.register_blocking == micro_gemm.register_blocking + self.log_blockings() + if isinstance(micro_gemm, CppMicroGemmAMX): + counters["inductor"]["cpp_micro_gemm_amx_counter"] += 1 + if isinstance(micro_gemm, CppMicroBrgemm): + counters["inductor"]["cpp_micro_brgemm_counter"] += 1 + + L1_cache_size = torch.cpu.get_capabilities().get( + "l1d_cache_size", 0 + ) # per core cache size in Bytes + assert L1_cache_size > 0, f"Expect L1_cache_size > 0 but got {L1_cache_size}" + + L2_cache_size = torch.cpu.get_capabilities().get( + "l2_cache_size", 0 + ) # per core cache size in Bytes + assert L2_cache_size > 0, f"Expect L2_cache_size > 0 but got {L2_cache_size}" + + options = dict( + X=X, + W=W, + inp=inp, + Y=Y, + N=self.n, + K=self.k, + PADDED_N=self.padded_n, + GemmOut=gemm_output_buffer, + aliases={alias: Y.get_name() for alias in Y_aliases}, + beta=self.beta, + alpha=self.alpha, + num_threads=self.num_threads, + micro_gemm=micro_gemm, + is_dynamic_M=self.is_dynamic_M, + template=self, + kernel=kernel, + export_declaration=get_export_declaration(), + epilogue_nodes=epilogues, + reindexers=reindexers, + Y_2d=Y_2d, + use_local_acc=use_local_acc, + maybe_k_slicing=self.maybe_k_slicing(), + x_scale=x_scale, + x_zp=x_zp, + w_scale=w_scale, + w_zp=w_zp, + acc_buf_dtype=torch.int32 if int8_gemm else torch.float, + DTYPE_TO_CPP=DTYPE_TO_CPP, + L1_cache_size=L1_cache_size, + L2_cache_size=L2_cache_size, + config=config, + fake_buffers=fake_buffers, + is_woq_int4=self.is_woq_int4(), + q_group_size=q_group_size_node, + qscale_and_zeros=qscale_and_zeros, + cpu_count=os.cpu_count(), + ) + return options + + def is_int8_woq_gemm_small_m_dim( + self, + X: ir.ReinterpretView, + W: ir.ReinterpretView, + N, + K, + micro_gemm, + ): + """Use SMALL_M_GEMM_TEMPLATE""" + return ( + isinstance(micro_gemm, CppMicroGemmFP32Vec) + and is_int8_woq_gemm_small_m_dim_corner_case( + micro_gemm, X.get_size()[0], N, K + ) + and X.get_dtype() is torch.bfloat16 + and W.get_dtype() is torch.int8 + ) + + def render( # type: ignore[override, return] + self, + kernel: CppTemplateKernel, + template_buffer_node: ir.CppTemplateBuffer | None = None, + flag_template_buffer_has_other_users: bool | None = None, + epilogue_nodes: list[ir.IRNode] | None = None, + **kwargs, + ) -> str: + options = self.get_options( + kernel=kernel, + template_buffer_node=template_buffer_node, + flag_template_buffer_has_other_users=flag_template_buffer_has_other_users, + epilogue_nodes=epilogue_nodes, + ) + self.render_options = options + + with contextlib.ExitStack() as stack: + for buf in options["fake_buffers"]: + stack.enter_context( + patch.object(V.graph, "get_dtype", self._fake_get_dtype(buf)) + ) + if not options["is_dynamic_M"] and self.is_int8_woq_gemm_small_m_dim( + options["X"], + options["W"], + options["N"], + options["K"], + options["micro_gemm"], + ): + template_str = SMALL_M_GEMM_TEMPLATE + else: + template_str = GEMM_TEMPLATE + return self._template_from_string(template_str).render(**options) + + def codegen_blocks( + self, + num_threads, + N, + K, + micro_gemm, + is_dynamic_M, + kernel, + GemmOut, + config, + L1_cache_size, + L2_cache_size, + X, + W, + ): + options = dict( + num_threads=num_threads, + N=N, + K=K, + micro_gemm=micro_gemm, + is_dynamic_M=is_dynamic_M, + kernel=kernel, + GemmOut=GemmOut, + config=config, + L1_cache_size=L1_cache_size, + L2_cache_size=L2_cache_size, + template=self, + X=X, + W=W, + is_woq_int4=self.is_woq_int4(), + ) + template_str = GEMM_TEMPLATE_INIT_BLOCKING_BASIC_BLOCK + if not ( + not is_dynamic_M + and self.is_int8_woq_gemm_small_m_dim(X, W, N, K, micro_gemm) + ): + template_str += GEMM_TEMPLATE_INIT_BLOCKING_EXTENDED + return self._template_from_string(template_str).render(options) + + def codegen_microkernel_def(self): + return self._template_from_string(GEMM_TEMPLATE_MICROKERNEL_DEF).render( + self.render_options + ) + + def codegen_gemm_stub_def(self): + microkernel = self.codegen_microkernel_def() + return microkernel + self._template_from_string(GEMM_TEMPLATE_STUB_DEF).render( + self.render_options + ) + + def codegen_multi_threads_params(self): + return self._template_from_string(GEMM_TEMPLATE_MULTI_THREADS_PARAMS).render() + + def codegen_single_thread_params(self, is_dynamic_M): + options = dict( + is_dynamic_M=is_dynamic_M, + ) + return self._template_from_string(GEMM_TEMPLATE_SINGLE_THREAD_PARAMS).render( + options + ) + + def codegen_m_loop_params(self): + return self._template_from_string(GEMM_TEMPLATE_M_LOOP_PARAMS).render() + + def codegen_n_loop_params(self): + return self._template_from_string(GEMM_TEMPLATE_N_LOOP_PARAMS).render() + + @classmethod + def is_woq_int4(cls): + return False + + @classmethod + def q_group_size(cls): + return None + + +class CppWoqInt4GemmTemplateMeta(type): + def __getitem__(cls, q_group_size): + class CppWoqInt4GemmTemplateInstance(CppGemmTemplate): + def __init__( + self, + *args, + **kwargs, + ) -> None: + super().__init__( + *args, + **kwargs, + ) + + @classmethod + def is_woq_int4(cls): + return True + + @classmethod + def q_group_size(cls): + return q_group_size + + @staticmethod + def check_if_block_weight(W, micro_gemm): + # For WOQ INT4, weight is already packed + # However, for AMX microkernel, we want to change the blocking of weight + from .cpp_micro_gemm import CppMicroGemmWoQInt4Amx + + return isinstance(micro_gemm, CppMicroGemmWoQInt4Amx) + + @classmethod + def block_weight(cls, W, new_size, padding): + # This method is called only if AMX microkernels are used. + # In this case, we unpack and repack weight so that block_n=32 + # the format of packed weight is described here: + # https://github.com/pytorch/pytorch/blob/32eee8ed225d9f10fbbcb38c24b8b44c24c0c97c/aten/src/ATen/native/cpu/int4mm_kernel.cpp#L583 + if isinstance(W, ir.IRNode): + # in this case, we do nothing + ir.ExternKernel.require_contiguous(W) + blocked_w = W + else: + # in this case, we unpack and repack weight + assert isinstance(W, torch.Tensor) + assert W.dim() == 2 + N = W.size(0) + K = W.size(-1) * 2 + G = cls.q_group_size() + # x and qscales_and_zeros are in bfloat16 instead of float to use the optimized kernel + # so that the unpacking process is faster + x = torch.eye(K).bfloat16() + # Here we use scale=1 and qzero=8 because we want to unpack weight + # without dequantizing it. The qzero here is 8 instead of 0 because + # int4 values are converted to [-7, 8] in the _weight_int4pack_mm_for_cpu kernel: + # https://github.com/pytorch/pytorch/blob/32eee8ed225d9f10fbbcb38c24b8b44c24c0c97c/aten/src/ATen/native/cpu/int4mm_kernel.cpp#L95 + qscales_and_zeros = ( + torch.tensor([1.0, 8.0]) + .bfloat16() + .expand(K // G, N, 2) + .contiguous() + ) + # shape: [K, N] + unpacked_w = torch.ops.aten._weight_int4pack_mm_for_cpu( + x, + W, + G, + qscales_and_zeros, + ).to(torch.uint8) + block_n = 32 + # shape: [N // block_n, K, block_n] + w_blocked = ( + unpacked_w.view(K, N // block_n, block_n) + .permute(1, 0, 2) + .contiguous() + ) + # pack 2 int4 -> 1 int8 + # block_n: [a0, a1, ..., a15, b0, b1, ..., b15] + # -> [(a0 & 0xf) | (b0 << 4), (a1 & 0xf) | (b1 << 4), ...] + # shape: [N // block_n, K, 2, block_n // 2] + w_blocked = w_blocked.view(N // block_n, K, 2, block_n // 2) + # shape: [N // block_n, K, block_n // 2] + w_blocked_packed = (w_blocked[:, :, 0, :] & 0xF) | ( + w_blocked[:, :, 1, :] << 4 + ) + # shape: [N, K // 2] + blocked_w = w_blocked_packed.view(N, K // 2) + + return blocked_w + + return CppWoqInt4GemmTemplateInstance + + +class CppWoqInt4GemmTemplate(metaclass=CppWoqInt4GemmTemplateMeta): + pass diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_grouped_gemm_template.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_grouped_gemm_template.py new file mode 100644 index 0000000000000000000000000000000000000000..d5fca03d489296c9b0299a7b14d9d808fc0f77e6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_grouped_gemm_template.py @@ -0,0 +1,521 @@ +import contextlib +import logging +import os +from collections.abc import Callable +from typing import Any, cast, TypeVar +from unittest.mock import patch + +import torch +import torch.utils +from torch.utils._ordered_set import OrderedSet + +from ..._dynamo.utils import counters +from .. import config, ir +from ..kernel.mm_common import mm_args +from ..select_algorithm import ChoiceCaller, DataProcessorTemplateWrapper +from ..utils import parallel_num_threads +from ..virtualized import V +from .cpp import get_export_declaration +from .cpp_gemm_template import ( + CppGemmTemplate, + expand_bias, + gen_2d_view_of_epilogue_buf, + prune_tensors, + transpose_w, +) +from .cpp_micro_gemm import CppMicroGemmAMX, create_micro_gemm +from .cpp_template_kernel import CppTemplateKernel +from .cpp_utils import ( + create_epilogue_with_attr, + DTYPE_TO_CPP, + GemmBlocking, + get_gemm_template_output_and_compute_dtype, +) + + +log = logging.getLogger(__name__) + +GEMM_TEMPLATE = r""" +{{template.header().getvalue()}} +{{micro_gemm.codegen_define(kernel)}} + +extern "C" {{export_declaration}} +{{kernel.def_kernel(inputs=kernel_args, outputs=Y_list, aliases=aliases)}} +{ + {{kernel.maybe_codegen_profile()}} + {{ template.codegen_blocks( + num_threads, N, K, micro_gemm, is_dynamic_M, kernel, GemmOuts[0], config, L1_cache_size, L2_cache_size, X_list[0], W_list[0] + ) }} +{%- if num_threads > 1 %} + {%- set use_dynamic_threads = ((config.cpp.threads < 1) and (num_threads == cpu_count)) or config.cpp.dynamic_threads %} + {%- if use_dynamic_threads %} + #pragma omp parallel + {%- else %} + #pragma omp parallel num_threads({{num_threads}}) + {%- endif %} + { + {{ template.codegen_multi_threads_params()|indent(8, false) }} +{%- else %} + { + {{ template.codegen_single_thread_params(is_dynamic_M)|indent(8, false) }} +{%- endif %} + {{ micro_gemm.codegen_init(kernel) }} +{%- set acc_buf_name_list=[] %} +{%- set acc_buf_name_prefix = "local_acc_buf_" %} +{%- for gemm_idx in range(0, gemm_grouped_num, 1) %} + {%- set acc_buf_name = acc_buf_name_prefix + gemm_idx|string %} + {{ kernel.define_buffer(acc_buf_name, ["Mc_blocks*Mr", "Nc_blocks*Nr"], acc_buf_dtype) }} + {%- set acc_buf_name_list=acc_buf_name_list.append(acc_buf_name) %} +{%- endfor %} + for (int64_t mc_block_id = 0; mc_block_id < num_Mc_blocks_per_thread; mc_block_id++) { + {{ template.codegen_m_loop_params()|indent(12, false) }} + for (int64_t nc = n_block_start; nc < n_block_end; nc += Nc_blocks) { + {{ template.codegen_n_loop_params()|indent(16, false) }} +{%- set acc_list=[] %} +{%- for gemm_idx in range(0, gemm_grouped_num, 1) %} + {%- set acc_list = acc_list.append( kernel.local_buffers[acc_buf_name_list[gemm_idx]] ) %} + {{ kernel.reinit_buffer_if_null(acc_buf_name_list[gemm_idx]) }} +{%- endfor %} + for (int64_t kc = k_block_start; kc < k_block_end; kc += Kc_blocks) { + int64_t k_start = kc * Kr; + int64_t k_end = std::min(std::min(kc + Kc_blocks, k_block_end) * Kr, K); +{%- set tile_X_list=[] %} +{%- for gemm_idx in range(0, gemm_grouped_num, 1) %} + {%- set tile_X_list = tile_X_list.append( kernel.slice_nd(X_list[gemm_idx], [("m_start", "m_end"), ("k_start", "k_end")]) ) %} +{%- endfor %} + for (int64_t nci = nc; nci < nc_block_end; nci++) { +{%- set tile_W_3d_list=[] %} +{%- set tile_W_list=[] %} +{%- set acc_slice_list=[] %} +{%- for gemm_idx in range(0, gemm_grouped_num, 1) %} + {%- set acc_slice_list = acc_slice_list.append( + kernel.slice_nd(acc_list[gemm_idx], [("0", "m_end - m_start"), ("(nci - nc)*Nr", "(nci - nc + 1)*Nr")]) + ) %} + {%- set tile_W_3d_list = tile_W_3d_list.append( + kernel.slice_nd(W_list[gemm_idx], [("nci", "nci + 1"), ("k_start", "k_end"), ()]) + ) %} +{%- endfor %} +{%- for gemm_idx in range(0, gemm_grouped_num, 1) %} + {%- set tile_W_list = tile_W_list.append( + kernel.view(tile_W_3d_list[gemm_idx], ["k_end - k_start", micro_gemm.register_blocking.block_n]) + ) %} +{%- endfor %} + if (kc == k_block_start) { + {%- for gemm_idx in range(0, gemm_grouped_num, 1) %} + {{ micro_gemm.codegen_call( + kernel, tile_X_list[gemm_idx], tile_W_list[gemm_idx], acc_slice_list[gemm_idx], accum=False + )|indent(28, false) }} + {%- endfor %} + } else { + {%- for gemm_idx in range(0, gemm_grouped_num, 1) %} + {{ micro_gemm.codegen_call( + kernel, tile_X_list[gemm_idx], tile_W_list[gemm_idx], acc_slice_list[gemm_idx], accum=True + )|indent(28, false) }} + {%- endfor %} + } + } + } + { +{%- set tile_acc_list = [] %} +{%- set tile_Y_list = [] %} +{%- for gemm_idx in range(0, gemm_grouped_num, 1) %} + {%- set tile_acc_list = tile_acc_list.append( + kernel.slice_nd(acc_list[gemm_idx], [("0", "m_end - m_start"), ("0", "n_end - n_start")]) + ) %} + {%- set tile_Y_list = tile_Y_list.append( + kernel.slice_nd(Y_2d_list[gemm_idx], [("m_start", "m_end"), ("n_start", "n_end")]) + ) %} +{%- endfor %} + {{ kernel.store_outputs( + tile_Y_list, + tile_acc_list, + GemmOuts, + epilogue_nodes, + offsets=("m_start", "n_start"), + reindexers=reindexers, + multi_output_buffers=multi_output_buffers + )|indent(20, false) + }} + } + } + } + {{ micro_gemm.codegen_finalize(kernel) }} + } +} +""" + + +def get_deduplicated_act(act_mapping: dict[int, ir.IRNode]) -> list[ir.IRNode]: + act_deduplicated = [] + act_deduplicated_name: OrderedSet[str] = OrderedSet() + for act_idx in range(len(act_mapping.values())): + act = act_mapping[act_idx] + if act.get_name() not in act_deduplicated_name: + act_deduplicated.append(act) + act_deduplicated_name.add(act.get_name()) + return act_deduplicated + + +class CppGroupedGemmTemplate(CppGemmTemplate): + def __init__( + self, + input_nodes: list[ir.IRNode], + layout: ir.Layout, + num_threads: int, + register_blocking: GemmBlocking, + beta: int = 1, + alpha: int = 1, + has_bias: bool = False, + epilogue_creator: Callable[[ir.Buffer], ir.Pointwise] | None = None, + act_mapping: dict[int, ir.IRNode] | None = None, + gemm_grouped_num: int = 1, + ) -> None: + """ + Template for Group of GEMMs: + * Each GEMM has the same dimensions (m, n, k) and the same leading dimensions (lda, ldb, ldc) + for their A, B, and C matrices. + * Each GEMM has distinct or shared activations, has distinct weight, has unique bias or no bias, has distinct epilogues. + * In the current implementation, the outputs of all GEMMs are accumulated using pointwise epilogues. + This behavior can be extended in the future if needed. + """ + super().__init__( + input_nodes, + layout, + num_threads, + register_blocking, + beta, + alpha, + has_bias, + epilogue_creator, + ) + self.act_mapping = act_mapping + self.gemm_grouped_num = gemm_grouped_num + # pyrefly: ignore [bad-override] + self.output_node: list[ir.Buffer] = [ + ir.Buffer(name="buf_out" + str(idx), layout=layout) + for idx in range(gemm_grouped_num) + ] + + @classmethod + # pyrefly: ignore [bad-override] + def add_choices( + cls, + choices: list[ChoiceCaller], + layout: ir.Layout, + input_nodes: list[ir.IRNode], + beta: int = 1, + alpha: int = 1, + has_bias: tuple[bool, ...] = (False, False), + trans_w: bool = False, + input_indices: list[int] | None = None, + epilogue_creator: Callable[[ir.Buffer], ir.Pointwise] | None = None, + act_mapping: dict[int, ir.IRNode] | None = None, # gemm idx to its act buf + ) -> DataProcessorTemplateWrapper: + # Input nodes order: x, optional[x1], ... w0, w1, ... optional[b0], optional[b1], ... + gemm_grouped_num = len(has_bias) + assert act_mapping + act_deduplicated = get_deduplicated_act(act_mapping) + wgt_start_idx = len(act_deduplicated) + bias_start_idx = wgt_start_idx + gemm_grouped_num + input_indices = list(range(len(input_nodes))) + + _T = TypeVar("_T", ir.IRNode, torch.Tensor) + _U = TypeVar("_U", ir.Layout, torch.Tensor) + + def reorder_and_filter( + inputs: list[_T], + layout_or_out: _U, + ) -> tuple[list[_T], _U]: + assert input_indices is not None, "input_indices must be set" + return [inputs[idx] for idx in input_indices], layout_or_out + + new_inputs, new_layout = reorder_and_filter(input_nodes, layout) + + def maybe_to_dense( + inputs: list[_T], + layout_or_out: _U, + ) -> tuple[list[_T], _U]: + new_inputs = list(inputs) + for idx in range(wgt_start_idx, wgt_start_idx + gemm_grouped_num): + if isinstance(inputs[idx], torch.Tensor): + W = inputs[idx] + assert isinstance(W, torch.Tensor), "W must be a torch.Tensor" + # pyrefly: ignore [unsupported-operation] + new_inputs[idx] = W.to_dense() if W.is_mkldnn else W + return new_inputs, layout_or_out + + def normalize_shapes( + inputs: list[_T], + layout_or_out: _U, + ) -> tuple[list[_T], _U]: + new_inputs: list[_T] = list(inputs) + if not trans_w: + return new_inputs, layout_or_out + X = new_inputs[0] + for wgt_idx in range(wgt_start_idx, wgt_start_idx + gemm_grouped_num): + new_input = new_inputs[wgt_idx] + new_inputs[wgt_idx] = transpose_w(new_input, trans_w) + for bias_idx in range(bias_start_idx, len(new_inputs)): + # pyrefly: ignore [bad-argument-type] + new_bias = expand_bias(new_inputs[bias_idx], X) + assert new_bias is not None + # pyrefly: ignore [unsupported-operation] + new_inputs[bias_idx] = new_bias + return new_inputs, layout_or_out + + num_threads = parallel_num_threads() + new_inputs, _ = normalize_shapes(*maybe_to_dense(new_inputs, new_layout)) + m, n, k, *_ = mm_args(new_inputs[0], new_inputs[wgt_start_idx]) + output_dtype, compute_dtype = get_gemm_template_output_and_compute_dtype( + new_inputs[0].get_dtype() + ) + micro_gemm = create_micro_gemm( + "micro_gemm", + m, + n, + k, + input_dtype=new_inputs[0].get_dtype(), + input2_dtype=new_inputs[wgt_start_idx].get_dtype(), + output_dtype=output_dtype, + compute_dtype=compute_dtype, + alpha=alpha, + num_threads=num_threads, + ) + assert micro_gemm is not None + _, block_n, _ = micro_gemm.register_blocking + new_size, padded_n = cls.get_padded_size( + n, block_n, k, should_block_weight=True + ) + padding = padded_n - n + + def pack_weight( + inputs: list[_T], + layout_or_out: _U, + ) -> tuple[list[_T], _U]: + new_W_list = [] + new_inputs = list(inputs) + W_list = new_inputs[wgt_start_idx : wgt_start_idx + gemm_grouped_num] + for W in W_list: + blocked_w = cls.block_weight(W, new_size, padding) + new_W_list.append(cls.pack_vnni_weight(blocked_w, micro_gemm, new_size)) + new_inputs[wgt_start_idx : wgt_start_idx + gemm_grouped_num] = new_W_list + return new_inputs, layout_or_out + + def preprocessor( + inputs: list[_T], + layout: _U, + ) -> tuple[list[_T], _U]: + return pack_weight( + *normalize_shapes(*maybe_to_dense(*reorder_and_filter(inputs, layout))) + ) + + def postprocessor(output: _T) -> _T: + if isinstance(output, ir.TensorBox): + template_buffer = ir.InputsKernel.unwrap_storage_for_input(output) + assert isinstance(template_buffer, ir.CppTemplateBuffer) + new_input_nodes, _ = reorder_and_filter(input_nodes, layout) + W_nodes = new_input_nodes[ + wgt_start_idx : wgt_start_idx + gemm_grouped_num + ] + W_tensor = [] + for W_node in W_nodes: + assert W_node.get_name() in V.graph.constants + # pyrefly: ignore [bad-argument-type] + W_tensor.append(V.graph.constants[W_node.get_name()]) + # pyrefly: ignore [unsupported-operation] + new_input_nodes[wgt_start_idx : wgt_start_idx + gemm_grouped_num] = ( + W_tensor # type: ignore[assignment] + ) + new_input_nodes, _ = pack_weight( + *normalize_shapes(*maybe_to_dense(new_input_nodes, layout)) + ) + # Prune unused tensors + prune_tensors(input_nodes, new_input_nodes) + for idx in range(wgt_start_idx, wgt_start_idx + gemm_grouped_num): + W_packed = new_input_nodes[idx] + assert isinstance(W_packed, torch.Tensor) + W_packed_constant = V.graph.add_tensor_constant(W_packed) + template_buffer.inputs[idx] = ( + ir.InputsKernel.unwrap_storage_for_input(W_packed_constant) + ) + + return output + + template = DataProcessorTemplateWrapper( + CppGroupedGemmTemplate, + preprocessor, + postprocessor, + input_nodes=input_nodes, + layout=layout, + num_threads=num_threads, + register_blocking=micro_gemm.register_blocking, + beta=beta, + alpha=alpha, + has_bias=has_bias, + epilogue_creator=epilogue_creator, + act_mapping=act_mapping, + gemm_grouped_num=gemm_grouped_num, + ) + template.maybe_append_choice(choices) + return template + + def render( # type: ignore[override,return,no-untyped-def] + self, + kernel: CppTemplateKernel, + template_buffer_node: ir.CppTemplateBuffer | None = None, + flag_template_buffer_has_other_users: bool | None = None, + epilogue_nodes: list[ir.IRNode] | None = None, + **kwargs, + ) -> str: + assert self.act_mapping + act_deduplicated = get_deduplicated_act(self.act_mapping) + wgt_start_idx = len(act_deduplicated) + bias_start_idx = wgt_start_idx + self.gemm_grouped_num + X_list = list(self.act_mapping.values()) + W_list = self.input_nodes[wgt_start_idx : wgt_start_idx + self.gemm_grouped_num] + inp_list = [] + cur_idx = bias_start_idx + for inp_idx in range(self.gemm_grouped_num): + inp = None + # pyrefly: ignore [bad-index, index-error] + if self.has_bias[inp_idx]: + inp = self.input_nodes[cur_idx] + cur_idx += 1 + inp_list.append(inp) + + Y_list = self.output_node + multi_output_buffers = None + if template_buffer_node is not None: + W_list = template_buffer_node.inputs[ + wgt_start_idx : wgt_start_idx + self.gemm_grouped_num + ] + assert isinstance(template_buffer_node.outputs, list) + Y_list = template_buffer_node.outputs + counters["inductor"]["cpp_grouped_gemm_template"] += 1 + multi_output_buffers = template_buffer_node.outputs + + template_buffer = Y_list[0] + fake_buffers: list[ir.Buffer] = [] + Y_2d_list = Y_list + output_dtype, compute_dtype = get_gemm_template_output_and_compute_dtype( + X_list[0].get_dtype() + ) + micro_gemm = create_micro_gemm( + f"{kernel.kernel_name}_micro_gemm", + self.m, + self.n, + self.k, + input_dtype=X_list[0].get_dtype(), + input2_dtype=W_list[0].get_dtype(), + output_dtype=output_dtype, + compute_dtype=compute_dtype, + alpha=self.alpha, + num_threads=self.num_threads, + ) + assert micro_gemm is not None + assert self.register_blocking == micro_gemm.register_blocking + self.log_blockings() + if isinstance(micro_gemm, CppMicroGemmAMX): + counters["inductor"]["cpp_micro_gemm_amx_counter"] += 1 + + L1_cache_size = torch.cpu.get_capabilities().get( + "l1d_cache_size", 0 + ) # per core cache size in Bytes + assert L1_cache_size > 0, f"Expect L1_cache_size > 0 but got {L1_cache_size}" + + L2_cache_size = torch.cpu.get_capabilities().get( + "l2_cache_size", 0 + ) # per core cache size in Bytes + assert L2_cache_size > 0, f"Expect L2_cache_size > 0 but got {L2_cache_size}" + + epilogues: list[ir.IRNode] = [] + reindexers: list[Callable[[list[Any]], list[Any]] | None] = [] + gemm_output_buffers: list[ir.Buffer] = [] + for out_buf_idx in range(self.gemm_grouped_num): + gemm_output_name = f"{template_buffer.get_name()}_GemmOut" + str( + out_buf_idx + ) + gemm_output_buffers.append( + ir.Buffer(name=gemm_output_name, layout=template_buffer.layout) + ) + + assert not self.epilogue_creator, ( + "epilogue_creator is not supported yet in Grouped GEMM Template" + ) + + kernel_args: dict[str, ir.IRNode | None] = {} + for x_idx in range(wgt_start_idx): + kernel_args["X" + str(x_idx)] = act_deduplicated[x_idx] + for w_idx in range(self.gemm_grouped_num): + kernel_args["W" + str(w_idx)] = W_list[w_idx] + for inp_idx in range(self.gemm_grouped_num): + kernel_args["inp" + str(inp_idx)] = inp_list[inp_idx] + + def _bias_add_epilogue(buf: ir.IRNode, inp: ir.IRNode) -> ir.Pointwise: + return create_epilogue_with_attr( + buf, "bias_add", other=inp, beta=self.beta, dtype=self.layout.dtype + ) + + for gemm_idx, inp in enumerate(inp_list): + if inp: + buffer_name = Y_list[gemm_idx].get_name() + epilogues.append( + ir.ComputedBuffer( + name=buffer_name, + layout=template_buffer.layout, + data=_bias_add_epilogue(gemm_output_buffers[gemm_idx], inp), + ) + ) + reindexers.append(None) + + if epilogue_nodes: + epilogues.extend(epilogue_nodes) + for epilogue_node in epilogue_nodes: + Y = cast(ir.Buffer, epilogue_node) + _, reindexers = gen_2d_view_of_epilogue_buf( + Y, + template_buffer, + [ + epilogue_node, + ], + reindexers, + default_reindexers=[ + None, + ], + ) + + options = dict( + N=self.n, + K=self.k, + PADDED_N=self.padded_n, + aliases={}, + beta=self.beta, + alpha=self.alpha, + num_threads=self.num_threads, + micro_gemm=micro_gemm, + is_dynamic_M=self.is_dynamic_M, + template=self, + kernel=kernel, + export_declaration=get_export_declaration(), + acc_buf_dtype=torch.float, + DTYPE_TO_CPP=DTYPE_TO_CPP, + L1_cache_size=L1_cache_size, + L2_cache_size=L2_cache_size, + config=config, + epilogue_nodes=epilogues, + GemmOuts=gemm_output_buffers, + reindexers=reindexers, + kernel_args=kernel_args, + X_list=X_list, + W_list=W_list, + gemm_grouped_num=self.gemm_grouped_num, + Y_list={"Y" + str(idx): Y for idx, Y in enumerate(Y_list)}, + Y_2d_list=Y_2d_list, + multi_output_buffers=multi_output_buffers, + cpu_count=os.cpu_count(), + ) + with contextlib.ExitStack() as stack: + stack.enter_context( + patch.object(V.graph, "get_dtype", self._fake_get_dtype(fake_buffers)) + ) + return self._template_from_string(GEMM_TEMPLATE).render(**options) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_micro_gemm.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_micro_gemm.py new file mode 100644 index 0000000000000000000000000000000000000000..a6ba0d93f22f61311bff0b15615bd00d63e84272 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_micro_gemm.py @@ -0,0 +1,2231 @@ +# mypy: allow-untyped-defs +import dataclasses +import operator +import sys +from collections.abc import Callable +from enum import Enum + +import torch + +from .. import cpp_builder, ir +from ..cpu_vec_isa import ( + pick_vec_isa, + VecAMX, + VecAVX2, + VecAVX512, + VecAVX512VNNI, + VecISA, + VecNEON, + VecSVE256, +) +from ..utils import IndentedBuffer, parallel_num_threads +from ..virtualized import V +from .common import KernelTemplate +from .cpp_template_kernel import CppTemplateKernel +from .cpp_utils import DTYPE_TO_CPP, GemmBlocking, value_to_cpp + + +class LayoutType(Enum): + NORMAL = 0 + VNNI2 = 1 + VNNI4 = 2 + + +_IS_WINDOWS = sys.platform == "win32" + + +def get_restrict_keyword() -> str: + if _IS_WINDOWS: + # https://learn.microsoft.com/en-us/cpp/cpp/extension-restrict?view=msvc-170 + return "__restrict" + else: + return "__restrict__" + + +class CppMicroGemm: + """ + A class that codegens a kernel that computes small-sized matrix multiplication. + + A micro GEMM kernel is responsible for register blocking, instruction selection, + and other CPU architecture-specific optimizations. + + The subclasses need to override `codegen_define` to define the kernel function + that is called by the code generated by `codegen_call`. + """ + + # TODO(jgong5): support constant shapes and lds as template args. + DECLARE_KERNEL = r""" +template +inline void {{kernel_name}}( +{%- if kernel_extra_args_declare %} + {{kernel_extra_args_declare}} +{%- endif %} + const {{input_t}}* {{restrict_keyword}} A, + const {{input2_t}}* {{restrict_keyword}} B, + {{output_t}}* {{restrict_keyword}} C, + int64_t M, + int64_t N, + int64_t K, + int64_t lda, + int64_t ldb, + int64_t ldc +) +""" + + def __init__( + self, + name, + input_dtype, + input2_dtype, + output_dtype, + compute_dtype, + register_blocking, + alpha=1, + ) -> None: + self.name = name + self.input_dtype = input_dtype + assert input2_dtype is not None + self.input2_dtype = input2_dtype + self.output_dtype = output_dtype + self.compute_dtype = compute_dtype + self.register_blocking = register_blocking + self.alpha = alpha + self.pack_vnni_B_locally = False + + def get_common_options(self): + if self.input_dtype in [torch.uint8, torch.int8]: + assert self.compute_dtype == torch.int32 + assert self.output_dtype == torch.int32 + assert self.input2_dtype == torch.int8 + return { + "torch": torch, + "kernel_name": self.name, + "input_dtype": self.input_dtype, + "input2_dtype": self.input2_dtype, + "output_dtype": self.output_dtype, + "compute_dtype": self.compute_dtype, + "input_t": DTYPE_TO_CPP[self.input_dtype], + "input2_t": DTYPE_TO_CPP[self.input2_dtype], + "output_t": DTYPE_TO_CPP[self.output_dtype], + "compute_t": DTYPE_TO_CPP[self.compute_dtype], + "alpha": self.alpha, + "kernel_extra_args_declare": self.get_kernel_extra_args_declare(), + "int8_gemm": self.input_dtype in [torch.uint8, torch.int8], + "vnni_size": 4 if self.input_dtype in [torch.uint8, torch.int8] else 2, + "restrict_keyword": get_restrict_keyword(), + "pack_vnni_B_locally": self.pack_vnni_B_locally, + "template": self, + "is_woq_int4": self.is_woq_int4(), + } + + def get_kernel_declaration(self): + options = self.get_common_options() + return KernelTemplate._template_from_string(self.DECLARE_KERNEL).render(options) + + def get_kernel_extra_args_declare(self) -> str: + return "" + + def get_kernel_extra_args(self, **kwargs) -> list[str]: + return [] + + def codegen_define(self, kernel: CppTemplateKernel) -> str: + raise NotImplementedError + + def codegen_call( + self, + kernel: CppTemplateKernel, + A: ir.Buffer, + B: ir.Buffer, + C: ir.Buffer, + accum: bool, + prefetch: bool = False, + **kwargs_for_extra_args, + ) -> str: + """ + Generate the code for calling the templated kernel that computes + `C += alpha * A @ B` if `accum` is True, or `C = alpha * A @ B` otherwise. + """ + A_ptr = f"&({kernel.index(A, [0, 0])})" + B_ptr = f"&({kernel.index(B, [0, 0])})" + C_ptr = f"&({kernel.index(C, [0, 0])})" + M = kernel.size(C, 0) + N = kernel.size(C, 1) + K = kernel.size(A, 1) + lda = kernel.stride(A, 0) + ldb = kernel.stride(B, 0) + ldc = kernel.stride(C, 0) + res = IndentedBuffer() + res.writeline( + f"{self.name}<{value_to_cpp(accum, 'bool')}, {value_to_cpp(prefetch, 'bool')}>(" + ) + with res.indent(): + kwargs_for_extra_args.update({"kernel": kernel}) + extra_args = self.get_kernel_extra_args(**kwargs_for_extra_args) + for arg in extra_args: + res.writeline(arg) + res.writeline(f"{A_ptr},") + res.writeline(f"{B_ptr},") + res.writeline(f"{C_ptr},") + res.writeline(f"{M},") + res.writeline(f"{N},") + res.writeline(f"{K},") + res.writeline(f"{lda},") + res.writeline(f"{ldb},") + res.writeline(f"{ldc}") + res.writeline(");") + return res.getvalue() + + def use_local_vnni_blocking(self, should_block_weight: bool): + self.pack_vnni_B_locally = should_block_weight + + def codegen_init( + self, + kernel: CppTemplateKernel, + ) -> str: + return "" + + def codegen_finalize( + self, + kernel: CppTemplateKernel, + ) -> str: + return "" + + def get_b_layout(self) -> LayoutType: + return LayoutType.NORMAL + + ALLOCATE_WEIGHT_BUFFER = r""" + {%- if is_msvc_compiler %} + // MSVC doesn't support stack-allocated dynamic-sized arrays, so using heap memory here. + auto heap_deq_b_buf_ptr = std::make_unique<{{buffer_dtype}}[]>({{buffer_size}}); + {{buffer_dtype}}* {{buffer_name}} = heap_deq_b_buf_ptr.get(); + {%- else %} + // It's safe to use a stack-allocated array since the blocking strategy would + // require us to allocate an array that's smaller than the size of L1D cache, + // and the default per thread max stack size on Linux is quite higher, + // so we need not worry about stack overflow. + alignas(4096) {{buffer_dtype}} {{buffer_name}}[{{buffer_size}}]; + {%- endif %} +""" + + def codegen_allocate_weight_buffer( + self, buffer_name: str, buffer_dtype: str, *size_args + ) -> str: + buffer_size = " * ".join(map(str, size_args)) + return KernelTemplate._template_from_string(self.ALLOCATE_WEIGHT_BUFFER).render( + { + "buffer_name": buffer_name, + "buffer_dtype": buffer_dtype, + "buffer_size": buffer_size, + "is_msvc_compiler": cpp_builder.is_msvc_cl(), + } + ) + + def is_woq_int4(self): + return False + + +@dataclasses.dataclass +class CppMicroGemmConfig: + input_dtype: torch.dtype + input2_dtype: torch.dtype + output_dtype: torch.dtype + compute_dtype: torch.dtype + vec_isa_cls: type[VecISA] + register_blocking: GemmBlocking + extra_check: Callable[..., bool] | None = None + + +micro_gemm_configs: dict[type[CppMicroGemm], list[CppMicroGemmConfig]] = {} + + +def register_micro_gemm(*configs): + def inner(cls): + assert cls not in micro_gemm_configs, ( + f"Duplicate micro_gemm registration for {cls}" + ) + assert len(configs) > 0, f"No micro_gemm configs provided for {cls}" + micro_gemm_configs[cls] = list(configs) + return cls + + return inner + + +def generate_gemm_config( + vec_isa_cls, + register_blockings, + input_dtype=torch.float, + input2_dtype=None, + output_dtype=None, + compute_dtype=None, + extra_check=None, +): + if output_dtype is None: + output_dtype = input_dtype + if compute_dtype is None: + compute_dtype = output_dtype + if input2_dtype is None: + input2_dtype = input_dtype + return [ + CppMicroGemmConfig( + input_dtype, + input2_dtype, + output_dtype, + compute_dtype, + vec_isa_cls, + GemmBlocking(*blocking), + extra_check, + ) + for blocking in register_blockings + ] + + +class CppMicroGemmRef(CppMicroGemm): + """ + A reference implementation of the CppMicroGemm class with naive C++ code. + It is used for correctness debugging. + """ + + TEMPLATE_ENTRY = r""" +{{declare_kernel}} { + for (int64_t m = 0; m < M; ++m) { + for (int64_t n = 0; n < N; ++n) { + {{compute_t}} result = accum ? C[m * ldc + n] : 0; + for (int64_t k = 0; k < K; ++k) { + result += ({{compute_t}})A[m * lda + k] * ({{compute_t}})B[k * ldb + n] * {{alpha}}; + } + C[m * ldc + n] = result; + } + } +} +""" + + def __init__( + self, name, input_dtype, input2_dtype, output_dtype, compute_dtype, alpha + ) -> None: + super().__init__( + name, + input_dtype, + input2_dtype, + output_dtype, + compute_dtype, + GemmBlocking(1, 1, 1), + alpha, + ) + + def codegen_define(self, kernel: CppTemplateKernel) -> str: + options = { + "declare_kernel": self.get_kernel_declaration(), + **self.get_common_options(), + } + return KernelTemplate._template_from_string(self.TEMPLATE_ENTRY).render(options) + + +def is_int8_woq_gemm_small_m_dim_corner_case(config, m, n, k): + return ( + k % config.register_blocking.block_k == 0 + and n % config.register_blocking.block_n == 0 + and m < 16 + ) + + +# extra check for small M dimension for int8 WoQ case +def check_int8_woq_small_m_dim(config, m, n, k, alpha, num_threads, **kwargs): + return is_int8_woq_gemm_small_m_dim_corner_case(config, m, n, k) and not kwargs.get( + "dynamic_M", False + ) + + +# For int8 WoQ GEMM with small M, we use different blockings that shouldn't be used otherwise +def do_not_use_with_small_m_for_int8_woq(config, m, n, k, alpha, num_threads, **kwargs): + return not check_int8_woq_small_m_dim(config, m, n, k, alpha, num_threads, **kwargs) + + +@register_micro_gemm( + *generate_gemm_config( + VecAVX512, + [(8, 48, 1), (8, 32, 1), (16, 16, 1)], + input_dtype=torch.float, + ), + *generate_gemm_config( + VecAVX512, + [(8, 48, 1), (8, 32, 1), (16, 16, 1)], + input_dtype=torch.bfloat16, + output_dtype=torch.float, + ), + *generate_gemm_config( + VecAVX512, + [(8, 48, 1), (8, 32, 1), (16, 16, 1)], + input_dtype=torch.half, + output_dtype=torch.float, + ), + *generate_gemm_config( + VecAVX512, + [(8, 48, 1), (8, 32, 1), (16, 16, 1)], + input_dtype=torch.bfloat16, + input2_dtype=torch.int8, + output_dtype=torch.float, + compute_dtype=torch.float, + extra_check=do_not_use_with_small_m_for_int8_woq, + ), + *generate_gemm_config( + VecAVX512, + [ + (4, 32, 64), + (8, 32, 64), + ], + input_dtype=torch.bfloat16, + input2_dtype=torch.int8, + output_dtype=torch.float, + compute_dtype=torch.float, + extra_check=check_int8_woq_small_m_dim, + ), + *generate_gemm_config( + VecAVX2, + [(4, 24, 1), (4, 16, 1), (8, 8, 1)], + input_dtype=torch.float, + ), + *generate_gemm_config( + VecAVX2, + [(4, 24, 1), (4, 16, 1), (8, 8, 1)], + input_dtype=torch.bfloat16, + output_dtype=torch.float, + ), + *generate_gemm_config( + VecAVX2, + [(4, 24, 1), (4, 16, 1), (8, 8, 1)], + input_dtype=torch.half, + output_dtype=torch.float, + ), + *generate_gemm_config( + VecAVX2, + [(4, 24, 1), (4, 16, 1), (8, 8, 1)], + input_dtype=torch.bfloat16, + input2_dtype=torch.int8, + output_dtype=torch.float, + compute_dtype=torch.float, + extra_check=do_not_use_with_small_m_for_int8_woq, + ), + *generate_gemm_config( + VecAVX2, + [ + (2, 16, 64), + (4, 16, 64), + ], + input_dtype=torch.bfloat16, + input2_dtype=torch.int8, + output_dtype=torch.float, + compute_dtype=torch.float, + extra_check=check_int8_woq_small_m_dim, + ), + *generate_gemm_config( + VecNEON, + [(4, 24, 1), (4, 16, 1), (8, 8, 1)], + input_dtype=torch.float, + input2_dtype=torch.float, + output_dtype=torch.float, + compute_dtype=torch.float, + ), + *generate_gemm_config( + VecSVE256, + [(4, 24, 1), (4, 16, 1), (8, 8, 1)], + input_dtype=torch.float, + input2_dtype=torch.float, + output_dtype=torch.float, + compute_dtype=torch.float, + ), +) +class CppMicroGemmFP32Vec(CppMicroGemm): + """ + This class generates the code for micro gemm using fp32 vec instructions for compute. + It supports input types of torch.float, torch.bfloat16, and torch.half with fp32 output. + The output of the microkernel is in FP32, but it would be converted to BF16/FP16 in the template, + if the desired output is BF16/FP16. + """ + + TEMPLATE_ENTRY = r""" +{{declare_kernel}} { + using Vectorized = at::vec::Vectorized<{{compute_t}}>; + constexpr auto VLEN = Vectorized::size(); + {{kernel.assert_function}}({{block_n}} % VLEN == 0, "block_n dimension must be multiple of Vector size"); + {{kernel.assert_function}}(K % {{block_k}} == 0, "K dimension must be multiple of {{block_k}}"); + // TODO(jgong5): loop unroll for M and N + for (int64_t m = 0; m < M; m += {{block_m}}) { + int64_t block_m = std::min(M - m, {{block_m}}); + for (int64_t n = 0; n < N; n += {{block_n}}) { + int64_t block_n = std::min(N - n, {{block_n}}); + if (block_m == {{block_m}} && block_n == {{block_n}}) { +{%- if not trans_b %} + {{kernel_name}}_kernel<{{block_m}}, {{block_n}}, accum, prefetch>( +{%- else %} + {{kernel_name}}_transpose_b_kernel<{{block_m}}, {{block_n}}, accum, prefetch>( +{%- endif %} + A + m * lda, +{%- if not trans_b %} + B + n, +{%- else %} + B + n * ldb, +{%- endif %} + C + m * ldc + n, + K, + lda, + ldb, + ldc + ); +{%- if tail_n %} + } else if (block_n == {{block_n}}){ +{%- else %} + } else { +{%- endif %} + switch (block_m) { +{%- for b in range(block_m - 1, 0, -1) %} + case {{b}}: + {%- if not trans_b %} + {{kernel_name}}_kernel<{{b}}, {{block_n}}, accum, prefetch>( + {%- else %} + {{kernel_name}}_transpose_b_kernel<{{b}}, {{block_n}}, accum, prefetch>( + {%- endif %} + A + m * lda, + {%- if not trans_b %} + B + n, + {%- else %} + B + n * ldb, + {%- endif %} + C + m * ldc + n, + K, + lda, + ldb, + ldc + ); + break; +{%- endfor %} + default: + {{kernel.assert_function}}(false, "Unsupported block_m: {{block_m}}"); + } + +{%- if tail_n %} + } else { + switch (block_m) { + {%- for b in range(block_m, 0, -1) %} + case {{b}}: + {%- if not trans_b %} + {{kernel_name}}_ntail_kernel<{{b}}, {{block_n}}, accum, prefetch>( + {%- else %} + {{kernel_name}}_ntail_transpose_b_kernel<{{b}}, {{block_n}}, accum, prefetch>( + {%- endif %} + A + m * lda, + {%- if not trans_b %} + B + n, + {%- else %} + B + n * ldb, + {%- endif %} + C + m * ldc + n, + block_n, + K, + lda, + ldb, + ldc + ); + break; + {%- endfor %} + default: + {{kernel.assert_function}}(false, "Unsupported block_m: {{block_m}}"); + } + } +{%- else %} + } +{%- endif %} + } + } +} +""" + + TEMPLATE_KERNEL = r""" + +template +{%- if not trans_b %} + {%- if tail_n %} +inline void {{kernel_name}}_ntail_kernel( + {%- else %} +inline void {{kernel_name}}_kernel( + {%- endif %} +{%- else %} + {%- if tail_n %} +inline void {{kernel_name}}_ntail_transpose_b_kernel( + {%- else %} +inline void {{kernel_name}}_transpose_b_kernel( + {%- endif %} +{%- endif %} + const {{input_t}}* {{restrict_keyword}} A, + const {{input2_t}}* {{restrict_keyword}} B, + {{output_t}}* {{restrict_keyword}} C, +{%- if tail_n %} + int64_t N, +{%- endif %} + int64_t K, + int64_t lda, + int64_t ldb, + int64_t ldc +) { + using Vectorized = at::vec::Vectorized<{{compute_t}}>; +{%- if input2_dtype in [torch.bfloat16, torch.float16] %} + using VectorizedIn = at::vec::Vectorized<{{input_t}}>; +{%- endif %} + +{%- if not trans_b %} + constexpr auto VLEN = Vectorized::size(); + constexpr auto ROWS = BLOCK_M; + constexpr auto COLS = BLOCK_N / VLEN; + + Vectorized va; + at::vec::VectorizedN<{{compute_t}}, COLS> vb; + at::vec::VectorizedN<{{compute_t}}, ROWS*COLS> vc; + + {%- if tail_n %} + int64_t rCOLS = (N + VLEN - 1) / VLEN; + int ntail = N % VLEN; + {%- endif %} + auto loadc = [&](auto i) { + if constexpr (accum) { + constexpr int row = i / COLS; + constexpr int col = i % COLS; + {%- if tail_n %} + int load_size = (col == rCOLS - 1 && ntail != 0) ? ntail : VLEN; + if (col < rCOLS) { + vc[i] = Vectorized::loadu(C + row * ldc + col * VLEN, load_size); + } + {%- else %} + vc[i] = Vectorized::loadu(C + row * ldc + col * VLEN); + {%- endif %} + } else { + vc[i] = Vectorized(0.0f); + } + }; + c10::ForcedUnroll{}(loadc); + + auto compute = [&, COLS](auto i, int k) { + constexpr int row = i / COLS; + constexpr int col = i % COLS; + {%- if tail_n %} + int load_size = (col == rCOLS - 1 && ntail != 0) ? ntail : VLEN; + {%- endif %} + if constexpr (col == 0) { + {%- if alpha != 1 %} + va = Vectorized(static_cast<{{compute_t}}>(A[row * lda + k]) * {{alpha}}); + {%- else %} + va = Vectorized(static_cast<{{compute_t}}>(A[row * lda + k])); + {%- endif %} + } + + if constexpr (row == 0) { + {%- if tail_n %} + if (col < rCOLS) { + {%- if input2_dtype in [torch.bfloat16, torch.float16] %} + auto b = VectorizedIn::loadu(B + k * ldb + col * VLEN, load_size); + vb[col] = at::vec::convert<{{compute_t}}>(b); + {%- elif input2_dtype == torch.int8 %} + // Convert VLEN int8 elements to int32, and then fp32 + auto b32 = at::vec::convert_to_int32(B + k * ldb + col * VLEN, load_size); + vb[col] = at::vec::convert(b32); + {%- else %} + vb[col] = Vectorized::loadu(B + k * ldb + col * VLEN, load_size); + {%- endif %} + } else { + vb[col] = Vectorized(0.0f); + } + + {%- else %} + + {%- if input2_dtype in [torch.bfloat16, torch.float16] %} + auto b = VectorizedIn::loadu(B + k * ldb + col * VLEN, VLEN); + vb[col] = at::vec::convert<{{compute_t}}>(b); + {%- elif input2_dtype == torch.int8 %} + // Convert VLEN int8 elements to int32, and then fp32 + auto b32 = at::vec::convert_to_int32(B + k * ldb + col * VLEN); + if constexpr (prefetch) { + _mm_prefetch(B + (k + {{block_k}}) * ldb + col * VLEN, _MM_HINT_T0); + } + vb[col] = at::vec::convert(b32); + {%- else %} + vb[col] = Vectorized::loadu(B + k * ldb + col * VLEN); + {%- endif %} + {%- endif %} + + } + + constexpr int idx = row * COLS + col; + {%- if tail_n %} + if (col < rCOLS) { + vc[idx] = at::vec::fmadd(va, vb[col], vc[idx]); + } + {%- else %} + vc[idx] = at::vec::fmadd(va, vb[col], vc[idx]); + {%- endif %} + }; + + for (int k = 0; k < K; ++k) { + c10::ForcedUnroll{}(compute, k); + } + + // store to C + auto storec = [&](auto i) { + constexpr int row = i / COLS; + constexpr int col = i % COLS; + {%- if tail_n %} + int store_size = (col == rCOLS - 1 && ntail != 0) ? ntail : VLEN; + if (col < rCOLS) { + vc[i].store(C + row * ldc + col * VLEN, store_size); + } + {%- else %} + vc[i].store(C + row * ldc + col * VLEN); + {%- endif %} + }; + c10::ForcedUnroll{}(storec); + +{%- else %} + // Use 2 implementations for the transposed B: + // First implementation: + // Transpose first and then perform outer product calculation in sub-blocks, + // which introduces an additional transpose overhead of [K, N] compared to the non-transpose version. + // Second implementation: + // Directly perform inner product calculation in sub-blocks, + // which introduces an additional vector reduction of [M, N] compared to the non-transpose version. + // Therefore, when M * N / (K * N) is large, the first implementation has better performance. + {%- if tail_n %} + if (K % Vectorized::size() == 0 && N % Vectorized::size() == 0 && 24 * BLOCK_M > K) { + {%- else %} + if (K % Vectorized::size() == 0 && 24 * BLOCK_M > K) { + {%- endif %} + // First implementation: + constexpr auto VLEN = Vectorized::size(); + constexpr auto ROWS = BLOCK_M; + constexpr auto COLS = BLOCK_N / VLEN; + int _K = K / VLEN; + Vectorized va; + at::vec::VectorizedN<{{compute_t}}, VLEN> vb; + at::vec::VectorizedN<{{compute_t}}, ROWS*COLS> vc; + auto loadc = [&](auto i) { + if constexpr (accum) { + constexpr int row = i / COLS; + constexpr int col = i % COLS; + vc[i] = Vectorized::loadu(C + row * ldc + col * VLEN); + } else { + vc[i] = Vectorized(0.0f); + } + }; + c10::ForcedUnroll{}(loadc); + auto unroll_loadB = [&](auto i, const {{input2_t}}* {{restrict_keyword}} src_ptr) { + {%- if input2_dtype in [torch.bfloat16, torch.float16] %} + auto b = VectorizedIn::loadu(src_ptr + i * ldb, VLEN); + vb[i] = at::vec::convert<{{compute_t}}>(b); + {%- elif input2_dtype == torch.int8 %} + auto b32 = at::vec::convert_to_int32(src_ptr + i * ldb, VLEN); + vb[i] = at::vec::convert(b32); + {%- else %} + vb[i] = Vectorized::loadu(src_ptr + i * ldb, VLEN); + {%- endif %} + }; + auto compute_trans = [&, COLS](auto i, int k) { + constexpr int row = i % ROWS; + constexpr int col = i / ROWS; + constexpr int e_col = col * VLEN; + int idk = k * VLEN; + if constexpr (row == 0) { + c10::ForcedUnroll{}(unroll_loadB, B + e_col * ldb + idk); + at::vec::transpose_block(vb); + } + constexpr int idx = row * COLS + col; + {{kernel.unroll_pragma(16)}} + for (int j = 0; j < VLEN; j++) { + {%- if alpha != 1 %} + va = Vectorized(static_cast<{{compute_t}}>(A[row * lda + idk + j]) * {{alpha}}); + {%- else %} + va = Vectorized(static_cast<{{compute_t}}>(A[row * lda + idk + j])); + {%- endif %} + vc[idx] = at::vec::fmadd(va, vb[j], vc[idx]); + } + }; + for (int k = 0; k < _K; ++k) { + c10::ForcedUnroll{}(compute_trans, k); + } + // store to C + auto storec = [&](auto i) { + constexpr int row = i / COLS; + constexpr int col = i % COLS; + vc[i].store(C + row * ldc + col * VLEN); + }; + c10::ForcedUnroll{}(storec); + } else { + // Second implementation + {%- if input2_dtype in [torch.bfloat16, torch.float16] %} + constexpr auto VLEN = VectorizedIn::size(); + {%- else %} + constexpr auto VLEN = Vectorized::size(); + {%- endif %} + int _K = (K + VLEN - 1) / VLEN; + // sub-block size of BLOCK_N and BLOCK_M + constexpr int sM = {{sub_block_m}}; + constexpr int sN = {{sub_block_n}}; + {%- if tail_n %} + int bN = (N + sN - 1) / sN; + {%- else %} + constexpr int bN = (BLOCK_N + sN - 1) / sN; + {%- endif %} + constexpr int bM = (BLOCK_M + sM - 1) / sM; + + {%- if input2_dtype in [torch.bfloat16, torch.float16] %} + at::vec::VectorizedN<{{compute_t}}, 2> va; + at::vec::VectorizedN<{{compute_t}}, 2 * sN> vb; + {%- else %} + at::vec::Vectorized<{{compute_t}}> va; + at::vec::VectorizedN<{{compute_t}}, sN> vb; + {%- endif %} + at::vec::VectorizedN<{{compute_t}}, sN * sM> vmid; + + {%- if tail_n %} + int ntail = N % sN; + {%- else %} + constexpr int ntail = BLOCK_N % sN; + {%- endif %} + constexpr int mtail = BLOCK_M % sM; + int ktail = K % VLEN; + + auto compute_trans = [&](int m, int n, int k) { + {%- if tail_n %} + int e_n = (n == bN - 1 && ntail != 0) ? (N - n * sN) : sN; + {%- else %} + int e_n = (n == bN - 1 && ntail != 0) ? (BLOCK_N - n * sN) : sN; + {%- endif %} + int e_m = (m == bM - 1 && mtail != 0) ? (BLOCK_M - m * sM) : sM; + int e_k = (k == _K - 1 && ktail != 0) ? (K - k * VLEN) : VLEN; + {{kernel.unroll_pragma(sub_block_n)}} + for (int i = 0; i < e_n; i++) { + {%- if input2_dtype in [torch.bfloat16, torch.float16] %} + auto b = VectorizedIn::loadu(B + (sN * n + i) * ldb + k * VLEN, e_k); + std::tie(vb[2 * i], vb[2 * i + 1]) = at::vec::convert_to_float<{{input_t}}>(b); + {%- elif input2_dtype == torch.int8 %} + auto b32 = at::vec::convert_to_int32(B + (sN * n + i) * ldb + k * VLEN, e_k); + vb[i] = at::vec::convert(b32); + {%- else %} + vb[i] = Vectorized::loadu(B + (sN * n + i) * ldb + k * VLEN, e_k); + {%- endif %} + } + + {{kernel.unroll_pragma(sub_block_m)}} + for (int s = 0; s < e_m; s++) { + {%- if input2_dtype in [torch.bfloat16, torch.float16] %} + auto a = VectorizedIn::loadu(A + (sM * m + s) * lda + k * VLEN, e_k); + std::tie(va[0], va[1]) = at::vec::convert_to_float<{{input_t}}>(a); + {%- elif input2_dtype == torch.int8 %} + auto a32 = at::vec::convert_to_int32(A + (sM * m + s) * lda + k * VLEN, e_k); + va = at::vec::convert(a32); + {%- else %} + va = Vectorized::loadu(A + (sM * m + s) * lda + k * VLEN, e_k); + {%- endif %} + + {%- if alpha != 1 %} + va = va * Vectorized({{alpha}}); + {%- endif %} + if (k == 0) { + {{kernel.unroll_pragma(sub_block_n)}} + for (int i = 0; i < e_n; i++) { + {%- if input2_dtype in [torch.bfloat16, torch.float16] %} + vmid[sN * s + i] = at::vec::fmadd(va[0], vb[2 * i], Vectorized(0.0f)); + vmid[sN * s + i] = at::vec::fmadd(va[1], vb[2 * i + 1], vmid[sN * s + i]); + {%- else %} + vmid[sN * s + i] = at::vec::fmadd(va, vb[i], Vectorized(0.0f)); + {%- endif %} + } + } else { + {{kernel.unroll_pragma(sub_block_n)}} + for (int i = 0; i < e_n; i++) { + {%- if input2_dtype in [torch.bfloat16, torch.float16] %} + vmid[sN * s + i] = at::vec::fmadd(va[0], vb[2 * i], vmid[sN * s + i]); + vmid[sN * s + i] = at::vec::fmadd(va[1], vb[2 * i + 1], vmid[sN * s + i]); + {%- else %} + vmid[sN * s + i] = at::vec::fmadd(va, vb[i], vmid[sN * s + i]); + {%- endif %} + } + } + } + + // store to C + if (k == _K - 1) { + {{kernel.unroll_pragma(sub_block_m)}} + for (int s = 0; s < e_m; s++) { + {{kernel.unroll_pragma(sub_block_n)}} + for (int i = 0; i < e_n; i++) { + auto v = at::vec::vec_reduce_all([](Vectorized& x, Vectorized& y) { return x + y; }, vmid[sN * s + i]); + if constexpr (accum) { + auto c = *(C + (sM * m + s) * ldc + sN * n + i); + *(C + (sM * m + s) * ldc + sN * n + i) = c + v; + } else { + *(C + (sM * m + s) * ldc + sN * n + i) = v; + } + } + } + } + }; + + for (int n = 0; n < bN; ++n) { + for (int m = 0; m < bM; ++m) { + for (int k = 0; k < _K; ++k) { + compute_trans(m, n, k); + } + } + } + } +{%- endif %} +} +""" + + # set trans_b to generate gemm that supports transposed B matrix + # set tail_n to support the tail of N + # TODO add trans_b support for other micro gemms + # and move setting of trans_b to the init of CppMicroGemm + def __init__( + self, + name, + input_dtype, + input2_dtype, + output_dtype, + compute_dtype, + register_blocking, + alpha=1, + tail_n=False, + trans_b=False, + ) -> None: + super().__init__( + name, + input_dtype, + input2_dtype, + output_dtype, + compute_dtype, + register_blocking, + alpha, + ) + self.tail_n = tail_n + # trans_b is only supported on platforms that + # support avx512 or avx2 since transpose_block is + # only implemented on these platforms + if trans_b: + vec_isa = pick_vec_isa() + assert issubclass(vec_isa.__class__, VecAVX512) or issubclass( + vec_isa.__class__, VecAVX2 + ) + self.trans_b = trans_b + + def codegen_define(self, kernel: CppTemplateKernel) -> str: + options = { + "declare_kernel": self.get_kernel_declaration(), + "kernel": kernel, + "block_m": self.register_blocking.block_m, + "block_n": self.register_blocking.block_n, + "block_k": self.register_blocking.block_k, + "trans_b": False, + "tail_n": False, + "restrict_keyword": get_restrict_keyword(), + **self.get_common_options(), + } + if self.trans_b: + # TODO supports tuning of sub_block_m/sub_block_n + # to get better performance for specific shapes + sub_block_m = min(1, self.register_blocking.block_m) + sub_block_n = min(4, self.register_blocking.block_n) + # update options to generate kernel with trans_b and sub-block size + options.update( + { + "trans_b": self.trans_b, + "sub_block_m": sub_block_m, + "sub_block_n": sub_block_n, + } + ) + result = KernelTemplate._template_from_string(self.TEMPLATE_KERNEL).render( + options + ) + # update options to generate the kernel for the tail of N + if self.tail_n: + options.update( + { + "tail_n": self.tail_n, + } + ) + result += KernelTemplate._template_from_string(self.TEMPLATE_KERNEL).render( + options + ) + result += KernelTemplate._template_from_string(self.TEMPLATE_ENTRY).render( + options + ) + return result + + +def check_vnni_extra(config, m, n, k, alpha, num_threads, **kwargs): + assert config.input_dtype == torch.uint8 and config.input2_dtype == torch.int8 + vnni_size = 4 + return k % vnni_size == 0 + + +@register_micro_gemm( + *generate_gemm_config( + VecAVX512VNNI, + # (block_m, block_n, block_k) + [(6, 64, 4)], + input_dtype=torch.uint8, + input2_dtype=torch.int8, + output_dtype=torch.int32, + compute_dtype=torch.int32, + extra_check=check_vnni_extra, + ), +) +class CppMicroGemmAVX512VNNI(CppMicroGemm): + """ + This class generates the code for micro gemm using AVX512 VNNI instructions for compute. + It supports u8s8s32 GEMM only. + AVX512_VNNI ISA has been available since the 3rd gen of Intel Xeon. + """ + + TEMPLATE_ENTRY = r""" +{{declare_kernel}} { + {{kernel.assert_function}}(N % {{block_n}} == 0, "N dimension must be multiple of {{block_n}}"); + {{kernel.assert_function}}(K % {{vnni_size}} == 0, "K dimension must be multiple of {{vnni_size}}"); + constexpr int64_t M_BLOCK = {{block_m}}; + const int64_t M_TAIL = M % M_BLOCK; + const int64_t M_MAIN = M - M_TAIL; + for (int64_t m = 0; m < M_MAIN; m += M_BLOCK) { + for (int64_t n = 0; n < N; n += {{block_n}}) { + {{kernel_name}}_kernel( + A + m * lda, + B + n, + C + m * ldc + n, + K, + lda, + ldb, + ldc + ); + } + } + if (M_TAIL > 0) { + switch (M_TAIL) { +{%- for m_tail in range(block_m - 1, 0, -1) %} + case ({{m_tail}}): + for (int64_t n = 0; n < N; n += {{block_n}}) { + {{kernel_name}}_kernel<{{m_tail}}, {{block_n}}, accum>( + A + M_MAIN * lda, + B + n, + C + M_MAIN * ldc + n, + K, + lda, + ldb, + ldc + ); + } + break; +{%- endfor %} + default: + {{kernel.assert_function}}(false, "Unsupported M_TAIL: {}", M_TAIL); + } // switch M_TAIL + } // if M_TAIL +} +""" + + TEMPLATE_KERNEL = r""" +template +inline void {{kernel_name}}_kernel( + const {{input_t}}* {{restrict_keyword}} A, + const {{input2_t}}* {{restrict_keyword}} B, + {{output_t}}* {{restrict_keyword}} C, + int64_t K, + int64_t lda, + int64_t ldb, + int64_t ldc +) { + constexpr const int COLS = N / {{vec_len}}; + __m512i va; + __m512i vb[COLS]; + __m512i vc[M * COLS]; + + c10::ForcedUnroll{}([&](auto i) { vc[i] = _mm512_setzero_epi32(); }); + + auto compute = [&](auto i, int k) { + constexpr const int row = i / COLS; + constexpr const int col = i % COLS; + + if constexpr (col == 0) { + va = _mm512_set1_epi32(*(int32_t*)(A + row * lda + k)); + } + + if constexpr (row == 0) { + // B block in VNNI layout: [K / {{vnni_size}}, N, {{vnni_size}}] + int64_t offset = k * ldb + col * {{vec_len}} * {{vnni_size}}; + vb[col] = _mm512_loadu_si512((__m512i const*)(B + offset)); + } + vc[i] = _mm512_dpbusd_epi32(vc[i], va, vb[col]); + }; + + // Accumulate along k + constexpr const int k_unroll = 2; + int k = 0; + int k_limit = K / {{vnni_size}} / k_unroll; + for (; k < k_limit; k++) { + c10::ForcedUnroll{}( + [&](auto i) { + c10::ForcedUnroll{}(compute, {{vnni_size}} * (k * k_unroll + i)); + } + ); + } + k *= {{vnni_size}} * k_unroll; + for (; k < K; k += {{vnni_size}}) { + c10::ForcedUnroll{}(compute, k); + } + + // Store to C + auto store_c = [&](auto i) { + constexpr const int row = i / COLS; + constexpr const int col = i % COLS; + if constexpr (accum) { + __m512i vc_old = _mm512_loadu_si512((__m512i const*)(C + row * ldc + col * {{vec_len}})); + vc[i] = _mm512_add_epi32(vc[i], vc_old); + } + _mm512_storeu_si512((__m512i*)(C + row * ldc + col * {{vec_len}}), vc[i]); + }; + c10::ForcedUnroll{}(store_c); +} +""" + + def __init__( + self, + name, + input_dtype, + input2_dtype, + output_dtype, + compute_dtype, + register_blocking, + alpha=1, + ) -> None: + super().__init__( + name, + input_dtype, + input2_dtype, + output_dtype, + compute_dtype, + register_blocking, + alpha, + ) + assert input_dtype == torch.uint8 and input2_dtype == torch.int8, ( + f"Only u8s8s32 GEMM is supported by AVX512VNNI microkernel, got A:{input_dtype}, B:{input2_dtype}, C:{output_dtype}." + ) + + def codegen_define(self, kernel: CppTemplateKernel) -> str: + options = { + "declare_kernel": self.get_kernel_declaration(), + "kernel": kernel, + "block_m": self.register_blocking.block_m, + "block_n": self.register_blocking.block_n, + "block_k": self.register_blocking.block_k, + "restrict_keyword": get_restrict_keyword(), + "vec_len": 16, # = 512 / 32 for C + **self.get_common_options(), + } + return KernelTemplate._template_from_string(self.TEMPLATE_KERNEL).render( + options + ) + KernelTemplate._template_from_string(self.TEMPLATE_ENTRY).render(options) + + def get_b_layout(self): + return LayoutType.VNNI4 + + +# extra check for CppMicroGemmAMX +def check_amx_extra(config, m, n, k, alpha, num_threads, **kwargs): + vnni_size = 4 if config.input_dtype in [torch.uint8, torch.int8] else 2 + return k % vnni_size == 0 and alpha == 1 + + +def check_int8_bf16_amx_extra(config, m, n, k, alpha, num_threads, **kwargs): + # We need avx512_bf16 to dequant int8 to bf16 + vec_isa = kwargs.get("vec_isa") + assert vec_isa is not None + return vec_isa.is_avx512_bf16_supported() and check_amx_extra( + config, m, n, k, alpha, num_threads, **kwargs + ) + + +# amx_fp16 need to be checked separately since it is not always supported when amx is supported +def check_amx_fp16_extra(config, m, n, k, alpha, num_threads, **kwargs): + assert config.input_dtype == torch.float16 and config.output_dtype == torch.float + vec_isa = kwargs.get("vec_isa") + assert vec_isa is not None + vnni_size = 2 + return vec_isa.is_amx_fp16_supported() and k % vnni_size == 0 and alpha == 1 + + +@register_micro_gemm( + *generate_gemm_config( + VecAMX, + [(32, 32, 64), (48, 16, 64)], + input_dtype=torch.int8, + input2_dtype=torch.int8, + output_dtype=torch.int32, + compute_dtype=torch.int32, + extra_check=check_amx_extra, + ), + *generate_gemm_config( + VecAMX, + [(32, 32, 32), (48, 16, 32)], + input_dtype=torch.bfloat16, + input2_dtype=torch.int8, + output_dtype=torch.float, + compute_dtype=torch.float, + extra_check=check_int8_bf16_amx_extra, + ), + *generate_gemm_config( + VecAMX, + [(32, 16, 32), (32, 32, 32), (48, 16, 32), (16, 48, 32)], + input_dtype=torch.bfloat16, + output_dtype=torch.float, + extra_check=check_amx_extra, + ), + *generate_gemm_config( + VecAMX, + [(32, 32, 32), (48, 16, 32), (16, 48, 32)], + input_dtype=torch.float16, + output_dtype=torch.float, + extra_check=check_amx_fp16_extra, + ), + *generate_gemm_config( + VecAMX, + [(32, 32, 64), (48, 16, 64)], + input_dtype=torch.uint8, + input2_dtype=torch.int8, + output_dtype=torch.int32, + compute_dtype=torch.int32, + extra_check=check_amx_extra, + ), +) +class CppMicroGemmAMX(CppMicroGemm): + """ + This class generates the code for micro gemm using Advanced Matrix extension (AMX) + instructions available in 4th generation Intel Xeon for compute. + It supports input types of torch.bfloat16 with fp32 output. + """ + + TEMPLATE_ENTRY = r""" +{{declare_kernel}} { + {{kernel.assert_function}}(N % {{block_n}} == 0, "N dimension must be multiple of {{block_n}}"); + {{kernel.assert_function}}(K % 2 == 0, "K dimension must be multiple of 2"); +{%- if pack_vnni_B_locally %} + {{template.codegen_allocate_weight_buffer("packed_B_buf", input2_t, "K", block_n)}} +{%- endif %} +{%- if use_cached_dequantized_B %} + // Create a stack-allocated buffer for tiles of B. + // Except maybe for the tail-case, an AMX tile of B has 16x32 BF16 elements. + // we cache K * {{block_n}} elements of dequantized B + {{template.codegen_allocate_weight_buffer("dequantized_B_buf", input_t, "K", block_n)}} + const auto buf_size = K * {{block_n}}; + auto load_dequantized_B = [&](int base_idx) { + // Load a tile of B & cache it in L1D. + {{input2_t}}* base_addr = const_cast<{{input2_t}}*>(B) + base_idx; + for (int idx_dq = 0, idx_q = 0; idx_dq < buf_size; idx_q += ldb, idx_dq += {{block_n}}) { + {%- for vec_idx in range(0, block_n, 32) %} + _mm_prefetch(base_addr + idx_q + 64 * ldb, _MM_HINT_T0); + {%- if (block_n - vec_idx) >= 32 %} + // 1) Load 32 x int8 + __m256i v8 = _mm256_loadu_si256((const __m256i*)(base_addr + idx_q + {{vec_idx}})); + // 2) Extract two halves + __m128i v8_lo = _mm256_extracti128_si256(v8, 0); + __m128i v8_hi = _mm256_extracti128_si256(v8, 1); + // 3) Widen each half to i32 + __m512i v32_lo = _mm512_cvtepi8_epi32(v8_lo); + __m512i v32_hi = _mm512_cvtepi8_epi32(v8_hi); + // 4) Convert to f32 + __m512 f_lo = _mm512_cvtepi32_ps(v32_lo); + __m512 f_hi = _mm512_cvtepi32_ps(v32_hi); + // 5) f32 -> bf16 (round-to-nearest-even) and pack 32 lanes to 512b + // Packs the second operand (f_lo) into the lower 16 bf16 lanes and the first (f_hi) into the upper 16. + __m512i bf = (__m512i)_mm512_cvtne2ps_pbh(f_hi, f_lo); + // 6) Store 32 x bf16 (512 bits) + _mm512_storeu_si512((__m512i*)(dequantized_B_buf + idx_dq + {{vec_idx}}), bf); + {%- elif (block_n - vec_idx) >= 16 %} + // 1) Load 16 x int8 (128 bits) + __m128i v8 = _mm_loadu_si128((const __m128i*)(base_addr + idx_q + {{vec_idx}})); + // 2) Widen: 16 x i8 -> 16 x i32 + __m512i v32 = _mm512_cvtepi8_epi32(v8); + // 3) Convert to f32 + __m512 f32 = _mm512_cvtepi32_ps(v32); + // 4) Convert f32 -> bf16 (round-to-nearest-even) + __m256i bf16 = (__m256i)_mm512_cvtneps_pbh(f32); + // 5) Store 16 x bf16 (256 bits) + _mm256_storeu_si256((__m256i*)(dequantized_B_buf + idx_dq + {{vec_idx}}), bf16); + {%- else %} + auto b_int8_tail = at::vec::Vectorized::loadu( + base_addr + idx_q + {{block_n - (block_n % 32)}}, + static_cast({{block_n % 32}}) + ); + auto b_bf16_tail = at::vec::convert<{{input_t}}>(b_int8_tail); + b_bf16_tail.store( + dequantized_B_buf + idx_dq + {{block_n - (block_n % 32)}}, + static_cast({{block_n % 32}}) + ); + {%- endif %} + {%- endfor %} + } + }; +{%- endif %} +// The ldb would not be block_n if N != block_n +{%- if use_cached_dequantized_B or pack_vnni_B_locally %} + const int64_t updated_ldb = {{block_n}}; +{%- else %} + const int64_t updated_ldb = ldb; +{%- endif %} + // TODO(jgong5): loop unroll for M and N + for (int64_t n = 0; n < N; n += {{block_n}}) { +{%- if pack_vnni_B_locally %} + // Pack non-constant weights into VNNI interleaved format in packed_B_buf + at::vec::pack_vnni2(B + n, packed_B_buf, ldb, K, {{block_n}}); +{%- elif use_cached_dequantized_B %} + // Dequantize K * block_n int8 B elements into BF16 + load_dequantized_B(n); +{%- endif %} + for (int64_t m = 0; m < M; m += {{block_m}}) { + int64_t block_m = std::min(M - m, {{block_m}}); + int64_t m_tail = m; +{%- for num_rows in range(block_m, 0, -16) %} + {%- if num_rows != block_m %} + else + {%- endif %} + if (block_m >= {{num_rows}}) { + {{kernel_name}}_amx_kernel_{{num_rows}}_{{num_columns}}( + amx_state, + A + m * lda, +{%- if use_cached_dequantized_B %} + dequantized_B_buf, +{%- elif pack_vnni_B_locally %} + packed_B_buf, +{%- else %} + B + n, +{%- endif %} + C + m * ldc + n, + K, + lda, + updated_ldb, + ldc, + 16 + ); + block_m -= {{num_rows}}; + m_tail += {{num_rows}}; + } +{%- endfor %} + if (block_m > 0) { + {{kernel_name}}_amx_kernel_16_{{num_columns}}( + amx_state, + A + m_tail * lda, +{%- if use_cached_dequantized_B %} + dequantized_B_buf, +{%- elif pack_vnni_B_locally %} + packed_B_buf, +{%- else %} + B + n, +{%- endif %} + C + m_tail * ldc + n, + K, + lda, + updated_ldb, + ldc, + block_m + ); + } + } + } +} +""" + + TEMPLATE_KERNEL = r""" + +template +inline void {{kernel_name}}_amx_kernel_{{num_rows}}_{{num_columns}}( + AMXState& amx_state, + const {{input_t}}* {{restrict_keyword}} A, +{%- if use_cached_dequantized_B %} + const {{input_t}}* {{restrict_keyword}} B, +{%- else %} + const {{input2_t}}* {{restrict_keyword}} B, +{%- endif %} + {{output_t}}* {{restrict_keyword}} C, + int64_t K, + int64_t lda, + int64_t ldb, + int64_t ldc, + uint8_t tilecfg_rows +) { + // TODO(jgong5): add prefetch hint for A, B, C + auto loadconfig = [](const amx_tilecfg& cfg) { + _tile_loadconfig(&cfg); + }; + const auto last_k_offset = K / {{block_k}} * {{block_k}}; + const auto tail_k_size = K - last_k_offset; + if C10_LIKELY (last_k_offset > 0) { + amx_state.configure(tilecfg_rows, 64, {{num_rows}} / 16, {{num_columns}}, loadconfig); + } else { + amx_state.configure(tilecfg_rows, tail_k_size * sizeof({{input_t}}), {{num_rows}} / 16, {{num_columns}}, loadconfig); + } + auto load_c = [&]() { +{%- for tile_row in range(num_rows // 16) %} + {%- for tile_col in range(num_columns) %} + {%- set tile_idx = tile_row * num_columns + tile_col %} + _tile_loadd({{tile_idx}}, C + {{tile_row * 16}} * ldc + {{tile_col * 16}}, ldc * sizeof({{output_t}})); + {%- endfor %} +{%- endfor %} + }; + auto zero_c = [&]() { +{%- for tile_row in range(num_rows // 16) %} + {%- for tile_col in range(num_columns) %} + {%- set tile_idx = tile_row * num_columns + tile_col %} + _tile_zero({{tile_idx}}); + {%- endfor %} +{%- endfor %} + }; + + if constexpr (accum) { + load_c(); + } else { + zero_c(); + } + + auto compute = [&](int k) { +{%- set tile_offset_a = num_rows // 16 * num_columns %} +{%- set tile_offset_b = tile_offset_a + num_rows // 16 %} +{%- for tile_row in range(num_rows // 16) %} + {%- for tile_col in range(num_columns) %} + {%- set tile_idx_a = tile_offset_a + tile_row %} + {%- set tile_idx_b = tile_offset_b + tile_col %} + {%- set tile_idx_c = tile_row * num_columns + tile_col %} + {%- if tile_col == 0 %} + _tile_stream_loadd({{tile_idx_a}}, A + {{tile_row * 16}} * lda + k, lda * sizeof({{input_t}})); + {%- endif %} + {%- if tile_row == 0 %} + _tile_loadd({{tile_idx_b}}, B + k * ldb + {{tile_col * 16 * vnni_size}}, ldb * {{vnni_size}} * sizeof({{input_t}})); + {%- endif %} + {%- if int8_gemm %} + {%- if input_dtype == torch.int8 %} + _tile_dpbssd({{tile_idx_c}}, {{tile_idx_a}}, {{tile_idx_b}}); + {%- else %} + _tile_dpbusd({{tile_idx_c}}, {{tile_idx_a}}, {{tile_idx_b}}); + {%- endif %} + {%- else %} + {%- if input_dtype == torch.float16 %} + _tile_dpfp16ps({{tile_idx_c}}, {{tile_idx_a}}, {{tile_idx_b}}); + {%- else %} + _tile_dpbf16ps({{tile_idx_c}}, {{tile_idx_a}}, {{tile_idx_b}}); + {%- endif %} + {%- endif %} + {%- endfor %} +{%- endfor %} + }; + + {{kernel.unroll_pragma(4)}} + for (int k = 0; k < last_k_offset; k += {{block_k}}) { + compute(k); + } + + auto store_c = [&]() { + // store to C +{%- for tile_row in range(num_rows // 16) %} + {%- for tile_col in range(num_columns) %} + {%- set tile_idx = tile_row * num_columns + tile_col %} + _tile_stored({{tile_idx}}, C + {{tile_row * 16}} * ldc + {{tile_col * 16}}, ldc * sizeof({{output_t}})); + {%- endfor %} +{%- endfor %} + }; + + // TODO(jgong5): move tail k computation to separate loopnest to save tile configuration overhead + if C10_UNLIKELY (tail_k_size > 0) { + if C10_LIKELY (last_k_offset > 0) { + store_c(); + amx_state.configure(tilecfg_rows, tail_k_size * sizeof({{input_t}}), {{num_rows}} / 16, {{num_columns}}, loadconfig); + load_c(); + } + compute(last_k_offset); + } + + store_c(); +} +""" + + def codegen_define(self, kernel: CppTemplateKernel) -> str: + block_m, block_n, block_k = self.register_blocking + assert block_m % 16 == 0, "Only support block_m % 16 == 0 for AMX" + assert block_n % 16 == 0, "Only support block_n % 16 == 0 for AMX" + if self.input_dtype in [torch.uint8, torch.int8]: + assert block_k == 64, "Only support block_k = 64 for AMX INT8" + else: + assert block_k == 32, "Only support block_k = 32 for AMX Bfloat16/Float16" + num_columns = block_n // 16 + options = { + "declare_kernel": self.get_kernel_declaration(), + "use_cached_dequantized_B": ( + self.input_dtype == torch.bfloat16 + and self.input2_dtype in [torch.int8, torch.uint8] + ), + "kernel": kernel, + "block_m": block_m, + "block_n": block_n, + "block_k": block_k, + "num_columns": num_columns, + "restrict_keyword": get_restrict_keyword(), + **self.get_common_options(), + } + result = "" + for num_rows in range(block_m, 0, -16): + amx_kernel_options = {**options, "num_rows": num_rows} + result += KernelTemplate._template_from_string(self.TEMPLATE_KERNEL).render( + amx_kernel_options + ) + result += KernelTemplate._template_from_string(self.TEMPLATE_ENTRY).render( + options + ) + return result + + def codegen_init( + self, + kernel: CppTemplateKernel, + ) -> str: + return "AMXState amx_state;" + + def codegen_finalize( + self, + kernel: CppTemplateKernel, + ) -> str: + return "amx_state.release([]() { _tile_release(); });" + + def get_kernel_extra_args_declare(self) -> str: + return "AMXState& amx_state," + + def get_kernel_extra_args(self, **kwargs) -> list[str]: + return ["amx_state,"] + + def get_b_layout(self): + if self.input_dtype in [torch.uint8, torch.int8]: + return LayoutType.VNNI4 + else: + return LayoutType.VNNI2 + + +# extra check for CppMicroBrgemm +def check_brgemm_extra(config, m, n, k, alpha, num_threads, **kwargs): + assert config.input_dtype == torch.half and config.output_dtype == torch.float + vnni_size = 2 + # use brgemm for Half when amx_fp16 is supported + return torch.cpu._is_amx_fp16_supported() and k % vnni_size == 0 and alpha == 1 + + +@register_micro_gemm( + *generate_gemm_config( + VecAMX, + [(32, 32, 32), (48, 16, 32), (16, 48, 32)], + input_dtype=torch.half, + output_dtype=torch.float, + extra_check=check_brgemm_extra, + ), +) +class CppMicroBrgemm(CppMicroGemm): + """ + This class generates the code for micro gemm using oneDNN brgemm. + It supports input types of torch.half. + """ + + TEMPLATE_ENTRY = r""" +#include +{{declare_kernel}} { +{%- if pack_vnni_B_locally %} + {{template.codegen_allocate_weight_buffer("packed_B_buf", input2_t, "K * N")}} + at::vec::pack_vnni2(B, packed_B_buf, ldb, K, N); +{%- endif %} + at::native::cpublas::brgemm( + M, N, K, + {%- if pack_vnni_B_locally %} + lda, N, ldc, + {%- else %} + lda, ldb, ldc, + {%- endif %} + accum, + A, + {%- if pack_vnni_B_locally %} + packed_B_buf, + {%- else %} + B, + {%- endif %} + C); +} +""" + + def codegen_define(self, kernel: CppTemplateKernel) -> str: + options = { + "declare_kernel": self.get_kernel_declaration(), + "kernel": kernel, + "block_m": self.register_blocking.block_m, + "block_n": self.register_blocking.block_n, + "block_k": self.register_blocking.block_k, + "restrict_keyword": get_restrict_keyword(), + **self.get_common_options(), + } + result = "" + result += KernelTemplate._template_from_string(self.TEMPLATE_ENTRY).render( + options + ) + return result + + def codegen_finalize( + self, + kernel: CppTemplateKernel, + ) -> str: + return "at::native::cpublas::brgemm_release();" + + def get_b_layout(self): + assert self.input_dtype == torch.half and torch.cpu._is_amx_fp16_supported() + return LayoutType.VNNI2 + + +def check_woq_int4_extra(config, m, n, k, alpha, num_threads, **kwargs): + if alpha != 1: + return False + q_group_size = kwargs.get("q_group_size") + assert q_group_size is not None + if ( + q_group_size not in [32, 64, 128] + or k % q_group_size != 0 + or config.register_blocking.block_k > q_group_size + ): + return False + return k % config.register_blocking.block_k == 0 and n % 64 == 0 + + +@register_micro_gemm( + # TODO: support float/half input + *generate_gemm_config( + VecAVX512, + [(4, 64, 32), (4, 64, 64), (4, 64, 128)], + input_dtype=torch.bfloat16, + input2_dtype=torch.uint8, + output_dtype=torch.float, + compute_dtype=torch.float, + extra_check=check_woq_int4_extra, + ), +) +class CppMicroGemmWoQInt4Avx512(CppMicroGemmFP32Vec): + """ + This class generates the code for WoQ int4 micro gemm using AVX512 intrinsics. + It is based on the corresponding ATen kernel. + Shape of packed weight = [N // 64, K, 32], viewed as [N, K // 2] + Shape of packed ScalesAndZeros = [K // group_size, N, 2] + """ + + TEMPLATE_ENTRY = r""" +{{declare_kernel}} { + {{kernel.assert_function}}(N % {{block_n}} == 0, "N dimension must be multiple of {{block_n}}"); + {{kernel.assert_function}}(K % {{block_k}} == 0, "K dimension must be multiple of {{block_k}}"); + auto group_size = q_group_size; + for (int64_t m = 0; m < M; m += {{block_m}}) { + int64_t block_m = std::min(M - m, {{block_m}}); + for (int64_t n = 0; n < N; n += {{block_n}}) { + if (block_m == {{block_m}}) { + {{kernel_name}}_kernel<{{block_m}}, {{block_n}}, accum>( + A + m * lda, + reinterpret_cast(B) + n * ldb, + C + m * ldc + n, + K, + lda, + /* ldb */ {{block_n}} / 2, + ldc, + group_size, + ScaleAndZeros + n * 2, + lds, + k_start + ); + } else { + switch (block_m) { + {%- for b in range(block_m - 1, 0, -1) %} + case {{b}}: + {{kernel_name}}_kernel<{{b}}, {{block_n}}, accum>( + A + m * lda, + reinterpret_cast(B) + n * ldb, + C + m * ldc + n, + K, + lda, + /* ldb */ {{block_n}} / 2, + ldc, + group_size, + ScaleAndZeros + n * 2, + lds, + k_start + ); + break; + {%- endfor %} + default: + {{kernel.assert_function}}(false, "Unsupported block_m: ", block_m); + } + } + } + } +} +""" + + TEMPLATE_KERNEL = r""" +inline bool {{kernel_name}}_is_block_start(int index, int k_start, int group_size) { + return (k_start + index) % group_size == 0; +} + +inline __m128i {{kernel_name}}_convert_int4_to_int8(const uint8_t* data) { + __m128i tmp = _mm_loadu_si64((const __m128i*)data); + __m128i bytes = _mm_cvtepu8_epi16(tmp); + const __m128i lowMask = _mm_set1_epi8(0xF); + __m128i high = _mm_andnot_si128(lowMask, bytes); + __m128i low = _mm_and_si128(lowMask, bytes); + high = _mm_slli_epi16(high, 4); + bytes = _mm_or_si128(low, high); + return bytes; +} + +template +inline void {{kernel_name}}_kernel( + const {{input_t}}* {{restrict_keyword}} A, + const uint8_t* {{restrict_keyword}} B, + {{output_t}}* {{restrict_keyword}} C, + int64_t K, + int64_t lda, + int64_t ldb, + int64_t ldc, + int64_t q_group_size, + const at::BFloat16* {{restrict_keyword}} ScaleAndZeros, + int64_t lds, // leading dimension of ScaleAndZeros + int64_t k_start) { + constexpr int BLOCK_K = {{block_k}}; + constexpr int ROWS = BLOCK_M; + constexpr int COLS = BLOCK_N / 16; + + const int PREFETCH_SIZE_K = 16 * 4; + const int PREFETCH_SIZE_KB = (PREFETCH_SIZE_K + BLOCK_K - 1) / BLOCK_K; + + // number of blocks on K + const int KB = K / BLOCK_K; + + __m512 va; + __m512 vb[COLS]; + __m512 vc[ROWS * COLS]; + __m512 scale[COLS]; + __m512 zero[COLS]; + + // Lookup table to de-quantize int4 values to bf16. + // Values are dequantized as truly int4 [-8, 7] range; + // + // dequant = (bf16(int4_value) * bf16_scale) + bf16_zero + // + static const __m512 lut = _mm512_set_ps( + 7.0f, 6.0f, 5.0f, 4.0f, + 3.0f, 2.0f, 1.0f, 0.0f, + -1.0f, -2.0f, -3.0f, -4.0f, + -5.0f, -6.0f, -7.0f, -8.0f); + + // index for transpose + static const __m512i idx1 = _mm512_set_epi32( + 30, 28, 26, 24, 22, 20, 18, 16, + 14, 12, 10, 8, 6, 4, 2, 0); + static const __m512i idx2 = _mm512_set_epi32( + 31, 29, 27, 25, 23, 21, 19, 17, + 15, 13, 11, 9, 7, 5, 3, 1); + + // load scale and zero point + auto load_scale_and_zeros = [&](int i, int _kb) { + // load 2x bfloat16 vector + __m512i t = _mm512_loadu_si512((__m512i*)(ScaleAndZeros + _kb * lds + 32 * i)); + _mm_prefetch(ScaleAndZeros + (_kb + PREFETCH_SIZE_KB) * lds + 32 * i, _MM_HINT_T0); + + // convert to 2x f32 vector + __m512 a, b; + at::vec::cvtbf16_fp32(t, a, b); + + // transpose scale_and_zero from {16, 2} to {2, 16} + // inputs: + // a: {s0, z0, s1, z1, ..., s7, z7} + // b: {s8, z8, s9, z9, ..., s15, z15} + // output: + // scale: {s0, s1, s2, ..., s15} + // zero: {z0, z1, z2, ..., z15} + scale[i] = _mm512_mask_permutex2var_ps(a, 0xffff, idx1, b); + zero[i] = _mm512_mask_permutex2var_ps(a, 0xffff, idx2, b); + }; + + auto loadc = [&](auto i) { + if constexpr (accum) { + constexpr int row = i / COLS; + constexpr int col = i % COLS; + vc[i] = _mm512_loadu_ps(C + row * ldc + col * 16); + } else { + vc[i] = _mm512_setzero_ps(); + } + }; + c10::ForcedUnroll{}(loadc); + + auto compute = [&, COLS](auto i, int k) { + constexpr int row = i / COLS; + constexpr int col = i % COLS; + + if constexpr (col == 0) { + float aa = static_cast(A[row * lda + k]); + _mm_prefetch(A + row * lda + k + PREFETCH_SIZE_K, _MM_HINT_T0); + va = _mm512_set1_ps(aa); + } + + if constexpr (row == 0) { + if constexpr (COLS == 4) { + // when BLOCK_N = 64, handle each row at a time + // to reduce de-quantize overhead. + if constexpr (col == 0) { + __m256i b4 = _mm256_loadu_si256((__m256i*)(B + k * ldb)); + _mm_prefetch(B + (k + PREFETCH_SIZE_K) * ldb, _MM_HINT_T0); + + __m512i b32 = _mm512_cvtepu8_epi32(_mm256_castsi256_si128(b4)); + vb[0] = _mm512_permutexvar_ps(b32, lut); + vb[0] = _mm512_fmadd_ps(vb[0], scale[0], zero[0]); + vb[2] = _mm512_permutexvar_ps(_mm512_srli_epi32(b32, 4), lut); + vb[2] = _mm512_fmadd_ps(vb[2], scale[2], zero[2]); + + b32 = _mm512_cvtepu8_epi32(_mm256_extracti128_si256(b4, 1)); + vb[1] = _mm512_permutexvar_ps(b32, lut); + vb[1] = _mm512_fmadd_ps(vb[1], scale[1], zero[1]); + vb[3] = _mm512_permutexvar_ps(_mm512_srli_epi32(b32, 4), lut); + vb[3] = _mm512_fmadd_ps(vb[3], scale[3], zero[3]); + } + } else { + __m128i b8 = {{kernel_name}}_convert_int4_to_int8(B + k * ldb + col * 8); + __m512i b32 = _mm512_cvtepu8_epi32(b8); + vb[col] = _mm512_permutexvar_ps(b32, lut); + vb[col] = _mm512_fmadd_ps(vb[col], scale[col], zero[col]); + } + } + + constexpr int idx = row * COLS + col; + vc[idx] = _mm512_fmadd_ps(va, vb[col], vc[idx]); + }; + + for (int k = 0, kb = 0; k < K; ++k) { + if ({{kernel_name}}_is_block_start(k, k_start, q_group_size)) { + c10::ForcedUnroll{}(load_scale_and_zeros, kb++); + } + c10::ForcedUnroll{}(compute, k); + } + + //store to C + auto storec = [&, COLS](auto i) { + constexpr int row = i / COLS; + constexpr int col = i % COLS; + _mm512_storeu_ps(C + row * ldc + col * 16, vc[i]); + }; + c10::ForcedUnroll{}(storec); +} +""" + + def get_kernel_extra_args_declare(self) -> str: + return ( + "const int64_t q_group_size,\n" + " const at::BFloat16* __restrict__ ScaleAndZeros,\n" + " const int64_t lds,\n" + " int64_t k_start," + ) + + def get_kernel_extra_args(self, **kwargs) -> list[str]: + assert "kernel" in kwargs + assert "qscale_and_zeros" in kwargs + kernel = kwargs["kernel"] + qscale_and_zeros = kwargs["qscale_and_zeros"] + return [ + "group_size,", + f"&({kernel.index(qscale_and_zeros, [0, 0, 0])}),", + "N * 2,", # lds + "k_start,", + ] + + def is_woq_int4(self): + return True + + +@register_micro_gemm( + *generate_gemm_config( + VecAMX, + [ # (block_m, block_n, block_k) + (16, 32, 32), + (32, 32, 32), + ], + input_dtype=torch.bfloat16, + input2_dtype=torch.uint8, + output_dtype=torch.float, + compute_dtype=torch.float, + extra_check=check_amx_extra, + ), +) +class CppMicroGemmWoQInt4Amx(CppMicroGemmAMX): + """ + This class generates the code for WoQ int4 micro gemm using AMX intrinsics, + which are available on 4th and newer generations of Intel Xeon. + Shape of packed weight = [N // 32, K, 16], viewed as [N, K // 2] + Shape of packed ScalesAndZeros = [K // group_size, N, 2] + Reuse TEMPLATE_KERNEL of CppMicroGemmAMX. + """ + + TEMPLATE_ENTRY = r""" +inline bool {{kernel_name}}_is_block_start(int index, int k_start, int group_size) { + // check if (k_start + index) % group_size == 0, assuming group_size = 32/64/128 + return ((k_start + index) & (group_size - 1)) == 0; +} + +{{declare_kernel}} { + {{kernel.assert_function}}(N % {{block_n}} == 0, "N dimension must be multiple of {{block_n}}"); + {{kernel.assert_function}}(K % 2 == 0, "K dimension must be multiple of 2"); + {{kernel.assert_function}}({{block_n}} == 32, "block_n must be 32 for WOQ int4"); + + // Create a stack-allocated buffer for tiles of B. + // Except maybe for the tail-case, an AMX tile of B has 16x32 BF16 elements. + // we cache K * {{block_n}} elements of dequantized B + {{template.codegen_allocate_weight_buffer("dequantized_B_buf", input_t, "K", block_n)}} + + constexpr int BLOCK_K = {{block_k}}; + constexpr int64_t BLOCK_N = {{block_n}}; + constexpr int COLS = BLOCK_N / 16; + const int PREFETCH_SIZE_K = 16 * 4; + const int PREFETCH_SIZE_KB = (PREFETCH_SIZE_K + BLOCK_K - 1) / BLOCK_K; + const int KB = K / BLOCK_K; + + __m512i b32[COLS * 2]; + __m512 vb[COLS * 2]; + __m512 scale[COLS]; + __m512 zero[COLS]; + + // Lookup table to de-quantize int4 values to bf16. + // Values are dequantized as truly int4 [-8, 7] range; + // + // dequant = (bf16(int4_value) * bf16_scale) + bf16_zero + // + static const __m512 lut = _mm512_set_ps( + 7.0f, 6.0f, 5.0f, 4.0f, + 3.0f, 2.0f, 1.0f, 0.0f, + -1.0f, -2.0f, -3.0f, -4.0f, + -5.0f, -6.0f, -7.0f, -8.0f); + + // index for transpose + static const __m512i idx1 = _mm512_set_epi32( + 30, 28, 26, 24, 22, 20, 18, 16, + 14, 12, 10, 8, 6, 4, 2, 0); + static const __m512i idx2 = _mm512_set_epi32( + 31, 29, 27, 25, 23, 21, 19, 17, + 15, 13, 11, 9, 7, 5, 3, 1); + + // Indices for VNNI layout conversion + __m512i idx_low = _mm512_set_epi32( + 0x17, + 0x07, + 0x16, + 0x06, + 0x15, + 0x05, + 0x14, + 0x04, + 0x13, + 0x03, + 0x12, + 0x02, + 0x11, + 0x01, + 0x10, + 0x00); + __m512i idx_high = _mm512_set_epi32( + 0x1f, + 0x0f, + 0x1e, + 0x0e, + 0x1d, + 0x0d, + 0x1c, + 0x0c, + 0x1b, + 0x0b, + 0x1a, + 0x0a, + 0x19, + 0x09, + 0x18, + 0x08); + + // load scale and zero point + auto load_scale_and_zeros = [&](int i, int _kb) { + // load 2x bfloat16 vector + __m512i t = _mm512_loadu_si512((__m512i*)(ScaleAndZeros + _kb * lds + 32 * i)); + _mm_prefetch(ScaleAndZeros + (_kb + PREFETCH_SIZE_KB) * lds + 32 * i, _MM_HINT_T0); + + // convert to 2x f32 vector + __m512 a, b; + at::vec::cvtbf16_fp32(t, a, b); + + // transpose scale_and_zero from {16, 2} to {2, 16} + // inputs: + // a: {s0, z0, s1, z1, ..., s7, z7} + // b: {s8, z8, s9, z9, ..., s15, z15} + // output: + // scale: {s0, s1, s2, ..., s15} + // zero: {z0, z1, z2, ..., z15} + scale[i] = _mm512_mask_permutex2var_ps(a, 0xffff, idx1, b); + zero[i] = _mm512_mask_permutex2var_ps(a, 0xffff, idx2, b); + }; + + // Dequantize a B block of 2 * block_n into bf16 + // So, it handles k and k+1 at the same time + auto dequantize_B = [&](int n) { + constexpr int64_t ldb_int4 = BLOCK_N / 2; // 16 + for (int k = 0, kb = 0; k < K; k += 2) { + // Since block_k must be 32 for AMX microkernels, k_start may not be + // a multiple of q_group_size. In that case, we need to load scales + // and zero points immediately when k == 0 here + if ({{kernel_name}}_is_block_start(k, k_start, q_group_size) || k == 0) { + c10::ForcedUnroll{}(load_scale_and_zeros, kb++); + } + + _mm_prefetch(B + (k + PREFETCH_SIZE_K) * ldb_int4, _MM_HINT_T0); + + // load 256 bits = 64 elements in int4 + __m128i b4 = _mm_loadu_si128((__m128i*)(B + n / 2 * K + k * ldb_int4)); + b32[0] = _mm512_cvtepu8_epi32(b4); + b32[1] = _mm512_srli_epi32(b32[0], 4); + vb[0] = _mm512_permutexvar_ps(b32[0] , lut); + vb[0] = _mm512_fmadd_ps(vb[0], scale[0], zero[0]); + vb[1] = _mm512_permutexvar_ps(b32[1], lut); + vb[1] = _mm512_fmadd_ps(vb[1], scale[1], zero[1]); + + __m128i b4_2 = _mm_loadu_si128((__m128i*)(B + n / 2 * K + (k + 1) * ldb_int4)); + b32[0 + COLS] = _mm512_cvtepu8_epi32(b4_2); + b32[1 + COLS] = _mm512_srli_epi32(b32[0 + COLS], 4); + vb[0 + COLS] = _mm512_permutexvar_ps(b32[0 + COLS] , lut); + vb[0 + COLS] = _mm512_fmadd_ps(vb[0 + COLS], scale[0], zero[0]); + vb[1 + COLS] = _mm512_permutexvar_ps(b32[1 + COLS], lut); + vb[1 + COLS] = _mm512_fmadd_ps(vb[1 + COLS], scale[1], zero[1]); + + for (int i = 0; i < COLS; i++) { + // convert to VNNI + auto low = _mm512_permutex2var_ps(vb[i], idx_low, vb[i + COLS]); + auto high = _mm512_permutex2var_ps(vb[i], idx_high, vb[i + COLS]); + // convert lower 16 float32 values to bfloat16 + auto v0_bf16 = reinterpret_cast<__m256i>(_mm512_cvtneps_pbh(low)); + // convert higher 16 float32 values to bfloat16 + auto v1_bf16 = reinterpret_cast<__m256i>(_mm512_cvtneps_pbh(high)); + // combine the lower 16 and higher 16 bfloat16 values + auto v = _mm512_castsi256_si512(v0_bf16); + v = _mm512_inserti64x4(v, v1_bf16, 1); + // store the VNNI format bfloat16 values + {{input_t}}* addr = dequantized_B_buf + k * 32 + (i % 2) * 32; + _mm512_storeu_si512(addr, v); + } + } + }; + + for (int64_t n = 0; n < N; n += {{block_n}}) { + // Dequantize K * block_n int8 B elements into BF16 + dequantize_B(n); + for (int64_t m = 0; m < M; m += {{block_m}}) { + int64_t block_m = std::min(M - m, {{block_m}}); + int64_t m_tail = m; + {%- for num_rows in range(block_m, 0, -16) %} + {%- if num_rows != block_m %} + else + {%- endif %} + if (block_m >= {{num_rows}}) { + {{kernel_name}}_amx_kernel_{{num_rows}}_{{num_columns}}( + amx_state, + A + m * lda, + dequantized_B_buf + n * K, + C + m * ldc + n, + K, + lda, + {{block_n}}, + ldc, + 16 + ); + block_m -= {{num_rows}}; + m_tail += {{num_rows}}; + } + {%- endfor %} + if (block_m > 0) { + {{kernel_name}}_amx_kernel_16_{{num_columns}}( + amx_state, + A + m_tail * lda, + dequantized_B_buf + n * K, + C + m_tail * ldc + n, + K, + lda, + {{block_n}}, + ldc, + block_m + ); + } + } // for m + } // for n +} +""" + + def get_kernel_extra_args_declare(self) -> str: + return ( + "AMXState& amx_state,\n" + " const int64_t q_group_size,\n" + " const c10::BFloat16* __restrict__ ScaleAndZeros,\n" + " const int64_t lds,\n" + " int64_t k_start," + ) + + def get_kernel_extra_args(self, **kwargs) -> list[str]: + assert "kernel" in kwargs + assert "qscale_and_zeros" in kwargs + kernel = kwargs["kernel"] + qscale_and_zeros = kwargs["qscale_and_zeros"] + return [ + "amx_state,", + "group_size,", + f"&({kernel.index(qscale_and_zeros, [0, 0, 0])}),", + "N * 2,", # lds + "k_start,", + ] + + def is_woq_int4(self): + return True + + +def create_micro_gemm( + name, + m, + n, + k, + input_dtype, + input2_dtype, + output_dtype=None, + compute_dtype=None, + alpha=1, + num_threads=-1, + use_ref=True, + q_group_size=None, +) -> CppMicroGemm | None: + """ + Based on the provided info, try to find the config of the micro-kernel that would + deliver the best performance in terms of lower latency for this case. + """ + + def create_from_config(cls, config: CppMicroGemmConfig): + return cls( + name, + config.input_dtype, + config.input2_dtype, + config.output_dtype, + config.compute_dtype, + config.register_blocking, + alpha, + ) + + def skip_amx_kernel_for_woq(dynamic_M): + # For WoQ GEMM, AMX micro-kernel may not perform well if m is small. + # Exception: for dynamic shapes, we consider using the AMX micro-kernel. + if ( + dynamic_M + or input_dtype != torch.bfloat16 + or input2_dtype not in [torch.int8, torch.uint8] + ): + return False + m_threshold = 5 + return m < m_threshold + + assert isinstance(n, int) or n.is_number, n + assert isinstance(k, int) or k.is_number, k + from ..utils import has_free_symbols + + dynamic_M = has_free_symbols((m,)) + m = V.graph.sizevars.optimization_hint(m, fallback=1) + assert isinstance(m, int) or m.is_number, m + if output_dtype is None: + output_dtype = input_dtype + if compute_dtype is None: + compute_dtype = output_dtype + if num_threads < 0: + num_threads = parallel_num_threads() + vec_isa = pick_vec_isa() + matched_configs = [] + for cls, configs in micro_gemm_configs.items(): + for config in configs: + if not issubclass(vec_isa.__class__, config.vec_isa_cls): + continue + if ( + config.input_dtype == input_dtype + and config.compute_dtype == compute_dtype + and config.input2_dtype == input2_dtype + and config.output_dtype == output_dtype + # The output_dtype here is the output dtype of the micro-kernel. + # In some cases, the actual output dtype of the op for which the micro-kernel + # is being created would be same as that of the activation, but the micro-kernels + # compute output in Float/int32, which is converted in the GEMM template. This is + # subject to change in the future. + ): + if config.extra_check is not None and not config.extra_check( + config, + m, + n, + k, + alpha, + num_threads, + dynamic_M=dynamic_M, + q_group_size=q_group_size, + vec_isa=vec_isa, + ): + continue + block_m, block_n, block_k = config.register_blocking + if config.vec_isa_cls == VecAMX and skip_amx_kernel_for_woq(dynamic_M): + continue + # Criteria on the ranking of configurations + # 1. ISA: AMX > VNNI > VEC + # 2. Dividable by block sizes (block_m, block_n, block_k) + # 3. Number of mxn blocks is large enough to occupy all the threads + # 4. Register blocks are larger + isa_score = 0 + if config.vec_isa_cls == VecAMX: + isa_score += 2 + elif config.vec_isa_cls == VecAVX512VNNI: + isa_score += 1 + dividable_score = 0 + if m % block_m == 0: + dividable_score += 1 + if n % block_n == 0: + dividable_score += 1 + if k % block_k == 0: + dividable_score += 1 + occupancy_score = 0 + n_blocks = (n + block_n - 1) // block_n + total_mxn_blocks = n_blocks * ((m + block_m - 1) // block_m) + if n_blocks >= num_threads: + occupancy_score += 1 + if total_mxn_blocks >= num_threads: + occupancy_score += 1 + register_bytes = ( + block_m * block_n * config.compute_dtype.itemsize + + (block_m * block_k + block_k * block_n) + * config.input_dtype.itemsize + ) + size_score = register_bytes + # if number of mxn blocks can not occupy all the threads, + # we favor smaller register blocks. + if occupancy_score == 0: + size_score = 0 - register_bytes + matched_configs.append( + ( + (isa_score, dividable_score, occupancy_score, size_score), + cls, + config, + ) + ) + if len(matched_configs) == 0: + if use_ref: + return CppMicroGemmRef( + name, input_dtype, input2_dtype, output_dtype, compute_dtype, alpha + ) + else: + return None + # TODO(jgong5): allow autotuning on choices of configs + return create_from_config(*max(matched_configs, key=operator.itemgetter(0))[1:]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_template.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_template.py new file mode 100644 index 0000000000000000000000000000000000000000..5b71d24a81572cb5a44b84b97e39b139d2e05af2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_template.py @@ -0,0 +1,139 @@ +# mypy: allow-untyped-defs +import ctypes +import functools +import itertools +import logging +import sys +from collections.abc import Callable, Iterable +from unittest.mock import patch + +import sympy + +from .. import config, ir +from ..autotune_process import CppBenchmarkRequest, TensorMeta +from ..utils import IndentedBuffer, Placeholder, unique +from ..virtualized import V +from .common import KernelTemplate +from .cpp_template_kernel import CppTemplateCaller, CppTemplateKernel + + +log = logging.getLogger(__name__) + + +class CppTemplate(KernelTemplate): + index_counter = itertools.count() + + def __init__( + self, + name: str, + input_nodes, + layout: ir.Layout, + num_threads: int, + epilogue_creator: Callable[[ir.Buffer], ir.Pointwise] | None = None, + ) -> None: + super().__init__(name) + self.input_nodes = input_nodes + self.index = next(self.index_counter) + self.output_node: ir.Buffer | list[ir.Buffer] = ir.Buffer( + name=f"buf_out{self.index}", layout=layout + ) + self.layout = layout + self.num_threads = num_threads + self.epilogue_creator = epilogue_creator + + def generate(self, **kwargs): + kernel_name = f"cpp_{self.name}" + with ( + patch.object(V.graph, "get_dtype", self._fake_get_dtype(self.output_node)), + patch.object(ir.FlexibleLayout, "allow_indexing", True), + V.graph.set_current_device(self.layout.device), + CppTemplateKernel( + kernel_name=kernel_name, num_threads=self.num_threads + ) as kernel, + ): + code = kernel.render(self, **kwargs) + _, call_args, _, _ = kernel.args.python_argdefs() + log.debug("Generated Code:\n%s", code) + log.debug( + "Args: cpp_argdefs: %s, python_argdefs: %s", + kernel.args.cpp_argdefs(), + kernel.args.python_argdefs(), + ) + + expected_args = list( + unique(input_node.get_name() for input_node in self.input_nodes) + ) + if isinstance(self.output_node, Iterable): + expected_args.extend([node.get_name() for node in self.output_node]) + else: + expected_args.extend([self.output_node.get_name()]) + assert list(call_args)[: len(expected_args)] == expected_args, ( + call_args, + expected_args, + ) + # extra_args are only used for benchmarking, not compiled kernel correctness + extra_args = V.graph.sizevars.optimization_hints( + map(sympy.expand, call_args[len(expected_args) :]) + ) + # Cast the size hint from int to ctypes.c_ulonglong explicitly + # since in cpp kernel, we bind it to C long + extra_args = tuple(ctypes.c_ulonglong(x) for x in extra_args) + + kernel_hash_name = f"cpp_{self.name}_{self.index}" + + # Create the BenchmarkRequest for CPP + bmreq = CppBenchmarkRequest( + kernel_name=kernel_name, + input_tensor_meta=TensorMeta.from_irnodes(self.input_nodes), + output_tensor_meta=TensorMeta.from_irnodes(self.output_node), + extra_args=extra_args, + source_code=code, + ) + + def make_kernel_render( + template_node: ir.CppTemplateBuffer, + flag_template_buffer_has_other_users: bool, + epilogue_nodes: list[ir.IRNode] | None = None, + ): + kernel = CppTemplateKernel( + kernel_name=str(Placeholder.KERNEL_NAME), num_threads=self.num_threads + ) + render = functools.partial( + kernel.render, + self, + template_buffer_node=template_node, + flag_template_buffer_has_other_users=flag_template_buffer_has_other_users, + epilogue_nodes=epilogue_nodes, + **kwargs, + ) + return kernel, render + + return CppTemplateCaller( + kernel_hash_name, + self.name, + self.input_nodes, + # pyrefly: ignore [bad-index, index-error] + self.output_node[0].get_layout() + if isinstance(self.output_node, Iterable) + else self.output_node.get_layout(), + make_kernel_render, + bmreq, + self, + ) + + def header(self) -> IndentedBuffer: + res = IndentedBuffer() + res.writeline("#include ") + # TODO: add c10::ForcedUnroll test to test_aoti_abi_check + res.splice("""#include """) + res.splice("""#include """) + enable_kernel_profile = config.cpp.enable_kernel_profile and sys.platform in [ + "linux", + "win32", + ] + if enable_kernel_profile: + res.writelines(["#include "]) + return res + + def render(self, **kwargs) -> str: + raise NotImplementedError diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_template_kernel.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_template_kernel.py new file mode 100644 index 0000000000000000000000000000000000000000..836d50b5984a66a8d59af548fd2cfa6019ab7a72 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_template_kernel.py @@ -0,0 +1,621 @@ +# mypy: allow-untyped-defs +import itertools +from collections.abc import Callable, Iterable +from typing import Any +from unittest.mock import patch + +import sympy +from sympy.parsing.sympy_parser import parse_expr + +import torch +from torch._inductor.utils import do_bench_using_profiling +from torch.utils._ordered_set import OrderedSet +from torch.utils._sympy.symbol import SymT + +from .. import config, cpp_builder, ir, lowering as L +from ..autotune_process import CppBenchmarkRequest +from ..loop_body import LoopBody +from ..select_algorithm import PartialRender +from ..utils import sympy_index_symbol, sympy_index_symbol_with_prefix +from ..virtualized import V +from .common import REMOVED +from .cpp import CppKernel, CppKernelProxy, KernelGroup, ParallelDepth +from .cpp_utils import cexpr_index, DTYPE_TO_CPP, LocalBufferContext + + +def parse_expr_with_index_symbols(expr): + if isinstance(expr, sympy.Expr): + return expr + elif isinstance(expr, (list, tuple)): + return [parse_expr_with_index_symbols(e) for e in expr] + else: + expr = parse_expr(str(expr)) + int_symbols = {sym: sympy_index_symbol(sym.name) for sym in expr.free_symbols} + return expr.subs(int_symbols) + + +def wrap_with_tensorbox(node) -> ir.TensorBox: + return ( + ir.TensorBox.create(node) if isinstance(node, ir.Buffer) else ir.TensorBox(node) + ) + + +class CppTemplateKernel(CppKernel): + def __init__(self, kernel_name, num_threads): + super().__init__(None, num_threads) + self.kernel_name = kernel_name + self.render_hooks = {} + self.local_buffers = {} + + def render(self, template, **kwargs): + return PartialRender( + template.render(kernel=self, **kwargs), self.render_hooks + ).finalize_all() + + def def_kernel( + self, + inputs: dict[str, ir.Buffer], + outputs: dict[str, ir.Buffer], + aliases: dict[str, str] | None = None, + function_name: str = "", + extra_sizevars: list[sympy.Expr] | None = None, + placeholder: str = "", + ) -> str: + if len(function_name) == 0: + function_name = str(self.kernel_name) + for name, inp in inputs.items(): + if inp is not None: + self.args.input_buffers[inp.get_name()] = name + for name, out in outputs.items(): + self.args.output_buffers[out.get_name()] = name + if aliases is not None: + for alias, orig in aliases.items(): + if orig in self.args.input_buffers: + self.args.input_buffers[alias] = self.args.input_buffers[orig] + if orig in self.args.output_buffers: + self.args.output_buffers[alias] = self.args.output_buffers[orig] + + unique_sizevars = OrderedSet( + s + for input in inputs.values() + if input is not None + for sym in itertools.chain(input.get_size(), input.get_stride()) + if isinstance(sym, sympy.Expr) + for s in sym.free_symbols + ) + unique_sizevars.update( + s + for sym in extra_sizevars or [] + if isinstance(sym, sympy.Expr) + for s in sym.free_symbols + ) + unique_sizevars.update( + s + for output in outputs.values() + for sym in itertools.chain(output.get_size(), output.get_stride()) + if isinstance(sym, sympy.Expr) + for s in sym.free_symbols + ) + sizevars = sorted(unique_sizevars, key=str) + for sizevar in sizevars: + self.args.sizevars[sizevar] = f"k{sizevar}" + + def hook(): + # remove all aliases before generate function definition + if aliases is not None: + for alias in aliases: + if alias in self.args.input_buffers: + raise AssertionError( + f"input_buffers cannot be removed: {alias}" + ) + if alias in self.args.output_buffers: + self.args.output_buffers[alias] = REMOVED + cpp_argdefs, _, _ = self.args.cpp_argdefs() + return f"void {function_name}({', '.join(cpp_argdefs)})" + + assert placeholder not in self.render_hooks + self.render_hooks[placeholder] = hook + return placeholder + + def call_kernel(self, name: str, node: ir.CppTemplateBuffer): + wrapper = V.graph.wrapper_code + _, call_args, arg_types = self.args.cpp_argdefs() + wrapper.generate_kernel_call(name, call_args, triton=False, arg_types=arg_types) + + def dtype(self, node: ir.Buffer) -> str: + return DTYPE_TO_CPP[node.get_dtype()] + + def acc_dtype(self, node: ir.Buffer) -> str: + if node.get_dtype() in [torch.float32, torch.bfloat16, torch.half]: + return "float" + else: + raise NotImplementedError(f"Unsupported dtype: {node.get_dtype()}") + + def size(self, node: ir.Buffer, dim: int) -> str: + return cexpr_index(self.rename_indexing(node.get_size()[dim])) + + def stride(self, node: ir.Buffer, dim: int) -> str: + return cexpr_index(self.rename_indexing(node.get_stride()[dim])) + + def index(self, node: ir.Buffer, indices: list[Any]) -> str: + indexer = node.get_layout().as_fixed().make_indexer() + index = indexer(parse_expr_with_index_symbols(indices)) + index = self.rename_indexing(index) + outer_name = node.get_name() + inner_name = ( + outer_name + if outer_name in self.local_buffers + else self.args.input(node.get_name()) + ) + return f"{inner_name}[{cexpr_index(index)}]" + + def slice_nd(self, node, ranges: list[tuple[Any, Any]]) -> ir.ReinterpretView: + """ + Slice the given node with a list of ranges (start and end) corresponding to its dims. + The dim is not sliced if the corresponding range is empty. + """ + assert len(ranges) == len(node.get_size()), f"{ranges=}, {node=}" + sliced = wrap_with_tensorbox(node) + for dim, _range in enumerate(ranges): + if len(_range) == 0: + continue + assert len(_range) == 2 + start, end = parse_expr_with_index_symbols(_range) + sliced = L.slice_(sliced, dim, start, end, clamp=False) + assert isinstance(sliced, ir.TensorBox) + assert isinstance(sliced.data, ir.ReinterpretView), sliced.data + return sliced.data + + def select(self, node, dim: int, idx: int) -> ir.ReinterpretView: + # We avoid using L.select here because we need clamp=False so the dim after slicing + # is 1 instead of a sympy expression of symbol - dim_size. + node = wrap_with_tensorbox(node) + idx = ir.View.handle_negative_index(idx, node.get_size()[dim]) + sliced = L.squeeze(L.slice_(node, dim, idx, idx + 1, clamp=False), dim) + assert isinstance(sliced.data, ir.ReinterpretView), sliced.data + return sliced.data + + def view(self, node, sizes: list[Any]) -> ir.IRNode: + node = wrap_with_tensorbox(node) + sizes = parse_expr_with_index_symbols(sizes) + return L.view(node, sizes).data # type: ignore[arg-type] + + def permute(self, node, dims): + node = wrap_with_tensorbox(node) + permuted = L.permute(node, dims).data + assert isinstance(permuted, ir.ReinterpretView) + return permuted + + def maybe_codegen_profile(self) -> str: + if config.cpp.enable_kernel_profile: + graph_id = V.graph.graph_id + prefix = "graph_" + str(graph_id) + "_" if graph_id is not None else "" + handle_str = ( + "torch::aot_inductor::RAIIAtenRecordFunctionHandle " + f'record_{prefix}{self.kernel_name}_("{prefix}{self.kernel_name}", nullptr);' + ) + return handle_str + else: + return "" + + def unroll_pragma(self, unroll): + if cpp_builder.is_gcc(): + return f"#pragma GCC unroll {unroll}" + else: + return f"#pragma unroll {unroll}" + + def define_buffer(self, name, sizes: list[Any], dtype=torch.float) -> str: + """Define kernel local buffer""" + sizes = parse_expr_with_index_symbols(sizes) + buf = ir.Buffer( + name=name, layout=ir.FixedLayout(torch.device("cpu"), dtype, sizes) + ) + self.local_buffers[name] = buf + ctype = f"{DTYPE_TO_CPP[dtype]}" + numel = f"{cexpr_index(buf.get_numel())}" + return f"auto _{name} = std::make_unique<{ctype}[]>({numel}); auto {name} = _{name}.get();" + + def define_stack_allocated_buffer( + self, name, sizes: list[Any], dtype=torch.float + ) -> str: + """Define stack-allocated buffer""" + sizes = parse_expr_with_index_symbols(sizes) + buf = ir.Buffer( + name=name, layout=ir.FixedLayout(torch.device("cpu"), dtype, sizes) + ) + self.local_buffers[name] = buf + ctype = f"{DTYPE_TO_CPP[dtype]}" + numel = f"{cexpr_index(buf.get_numel())}" + return f"alignas(64) {ctype} _{name}[{numel}]; {ctype}* {name} = _{name};" + + def reinit_buffer_if_null(self, name): + """Reinit the previously defined local buffer if it is null""" + assert name in self.local_buffers + buf = self.local_buffers[name] + ctype = f"{DTYPE_TO_CPP[buf.layout.dtype]}" + numel = f"{cexpr_index(buf.get_numel())}" + return f"if (_{name} == nullptr) {{ _{name} = std::make_unique<{ctype}[]>({numel}); {name} = _{name}.get(); }}" + + def release_buffer(self, name): + """Codegen the code to release the ownership of a local buffer to others""" + assert name in self.local_buffers + return f"_{name}.release()" + + def store_pointwise_nodes( + self, + dst: ir.Buffer, + nodes: list[ir.IRNode], + offsets: list[sympy.Expr] | None = None, + reindexers: list[Callable[[list[Any]], list[Any]] | None] | None = None, + ) -> str: + var_sizes = (tuple(dst.get_size()), ()) + var_ranges = { + sympy_index_symbol_with_prefix(SymT.INDEX, i): sz + for i, sz in enumerate(var_sizes[0]) + } + if not offsets: + offsets = [sympy.S.Zero] * len(var_sizes[0]) + if not reindexers: + reindexers = [None] * len(nodes) + assert len(offsets) == len(var_sizes[0]) + output_index = dst.get_layout().make_indexer()([*var_ranges.keys()]) + kernel_group = KernelGroup() + kernel_group.args = self.args + cpp_kernel_proxy = CppKernelProxy(kernel_group) + bodies = [] + var_sizes_list = [] + for i, node in enumerate(nodes): + output_name = node.get_name() if i < len(nodes) - 1 else dst.get_name() + node = node.data if isinstance(node, ir.ComputedBuffer) else node + assert isinstance(node, ir.Pointwise), node + + def fn(*args): + assert len(args) == 2 + assert len(args[0]) == len(var_sizes[0]) + assert len(args[1]) == 0 + new_args = [arg + offset for arg, offset in zip(args[0], offsets)] # type: ignore[arg-type] + if reindexers[i] is not None: + new_args = reindexers[i](new_args) # type: ignore[misc] + V.ops.store( + output_name, + output_index, + node.make_loader()(new_args).value, + ) + + body = LoopBody( + fn, + (list(var_ranges.keys()), ()), + var_ranges, + list(var_ranges.keys()), + tuple(), + ) + bodies.append(body) + var_sizes_list.append(var_sizes) + + cpp_kernel_proxy.codegen_loop_bodies(bodies, var_sizes_list) + + def max_parallel_depth(): + return ParallelDepth(parallel_depth=0, start_depth=0) + + # This loop is not parallelized since it is not the outermost loop. + with patch.object( + cpp_kernel_proxy.loop_nest, "max_parallel_depth", max_parallel_depth + ): + kernel_group.finalize_kernel(cpp_kernel_proxy, []) + return kernel_group.loops_code.getvalue() + + def store_grouped_gemm_pointwise_nodes( + self, + dst: tuple[ir.Buffer], + nodes: list[ir.IRNode], + offsets: list[sympy.Expr], + reindexers: list[Callable[[list[Any]], list[Any]] | None], + output_names: list[str], + ) -> str: + ref_dst = dst[0] + var_sizes = (tuple(ref_dst.get_size()), ()) + var_ranges = { + sympy_index_symbol_with_prefix(SymT.INDEX, i): sz + for i, sz in enumerate(var_sizes[0]) + } + assert offsets, "offsets should be set outside" + assert all(len(offset) == len(var_sizes[0]) for offset in offsets) + output_index = ref_dst.get_layout().make_indexer()([*var_ranges.keys()]) + kernel_group = KernelGroup() + kernel_group.args = self.args + cpp_kernel_proxy = CppKernelProxy(kernel_group) + bodies = [] + var_sizes_list = [] + for i, node in enumerate(nodes): + output_name = output_names[i] + node = node.data if isinstance(node, ir.ComputedBuffer) else node + assert isinstance(node, ir.Pointwise), node + + def fn(*args): + assert len(args) == 2 + assert len(args[0]) == len(var_sizes[0]) + assert len(args[1]) == 0 + new_args = [arg + offset for arg, offset in zip(args[0], offsets[i])] # type: ignore[arg-type] + if reindexers[i] is not None: + new_args = reindexers[i](new_args) # type: ignore[misc] + V.ops.store( + output_name, + output_index, + node.make_loader()(new_args).value, + ) + + body = LoopBody( + fn, + (list(var_ranges.keys()), ()), + var_ranges, + list(var_ranges.keys()), + tuple(), + ) + bodies.append(body) + var_sizes_list.append(var_sizes) + + cpp_kernel_proxy.codegen_loop_bodies(bodies, var_sizes_list) + + def max_parallel_depth(): + return ParallelDepth(parallel_depth=0, start_depth=0) + + # This loop is not parallelized since it is not the outermost loop. + with patch.object( + cpp_kernel_proxy.loop_nest, "max_parallel_depth", max_parallel_depth + ): + kernel_group.finalize_kernel(cpp_kernel_proxy, []) + return kernel_group.loops_code.getvalue() + + def store_output( + self, + dst: ir.Buffer, + src: ir.Buffer, + orig_src: ir.Buffer | None = None, + epilogue_nodes: list[ir.IRNode] | None = None, + offsets: list[Any] | None = None, + reindexers: list[Callable[[list[Any]], list[Any]] | None] | None = None, + ): + """ + Store the `src` buffer to the `dst` buffer. The size of `src` and `dst` should match. + If `epilogue_nodes` is provided, the `src` buffer is firstly computed with the epilogues + before stored to `dst`. The `epilogues_nodes` are all pointwise. + + Notes: + 1. `src` and `dst` buffer could be the same buffer in which case we are doing in-place compute + and stores. In case `epilogue_nodes` are not provided, we do nothing. + 2. The `epilogue_nodes`, if exist, have computations on `src` before storing to `dst` but since + they come form the original Inductor IR, they might need to be adjusted before working with + `src` and `dst` as outlined below: + a) `src` or `dst` buffer could be a sub-slice of the ranges the `epilogue_nodes`work on. + In this case, the `offsets` could be provided to adjust the indices passed to + `epilogue_nodes` during codegen and the data ranges are also configured according to + the sizes of `src` and `dst`. + b) `dst` might be indexed in a different way as the `epilogue_nodes`, hence a `reindexer` is + needed on the indices to `epilogue_nodes` to match the indexing of `dst`. + c) If `src` is local, we need to add a local buffer for it and localize the `orig_src` buffer + in `epilogue_nodes` with `src`. + """ + assert isinstance(dst, (ir.Buffer, ir.ReinterpretView)) + assert dst.get_size() == src.get_size(), f"{dst=}, {src=}" + if offsets: + offsets = parse_expr_with_index_symbols(offsets) + if epilogue_nodes: + with LocalBufferContext(self.args) as scope: + assert orig_src is not None + if orig_src.get_name() != src.get_name(): + scope.add_local_buffer( + src, + [ + orig_src, + ], + ) + epilogue_nodes = scope.localize_nodes(epilogue_nodes) + return self.store_pointwise_nodes( + dst, + epilogue_nodes, # type: ignore[arg-type] + offsets, + reindexers, + ) + else: + if dst.get_name() != src.get_name(): + # src is local + copy = L.copy(dst, src).data.data + with LocalBufferContext(self.args) as scope: + scope.add_local_buffer(src) + + return self.store_pointwise_nodes(dst, [copy]) + else: + assert dst.layout == src.layout, f"{dst=}, {src=}" + return "" + + def store_outputs( + self, + dst: tuple[ir.Buffer], + src: tuple[ir.IRNode], + orig_src: tuple[ir.IRNode] | None = None, + epilogue_nodes: list[ir.IRNode] | None = None, + offsets: list[Any] | None = None, + reindexers: list[Callable[[list[Any]], list[Any]] | None] | None = None, + multi_output_buffers: tuple[ir.MultiOutput, ...] | None = None, + ): + assert isinstance(dst, Iterable) + assert all(_dst.get_size() == _src.get_size() for _src, _dst in zip(src, dst)) + if offsets: + offsets = parse_expr_with_index_symbols(offsets) + gemm_num = len(src) + final_offsets = [] + output_names = [] + if epilogue_nodes: + if not reindexers: + reindexers = [None] * len(epilogue_nodes) + with LocalBufferContext(self.args) as scope: + assert orig_src is not None + localize_epilogue_nodes = [] + all_read_names = [] + for epilogue in epilogue_nodes: + all_read_names.extend(list(epilogue.get_read_names())) + localize_epilogue_nodes.extend(scope.localize_nodes(epilogue_nodes)) + final_offsets.extend([offsets] * len(localize_epilogue_nodes)) + output_names.extend( + [node.get_name() for node in localize_epilogue_nodes] + ) + for gemm_idx in range(gemm_num): + if orig_src[gemm_idx].get_name() != src[gemm_idx].get_name(): + if orig_src[gemm_idx].get_name() in all_read_names or ( + multi_output_buffers + and multi_output_buffers[gemm_idx].get_name() + in all_read_names + ): + # If any of the Epilogue nodes use this GEMM output, let's localize the GEMM output + global_buffers = [orig_src[gemm_idx]] + if ( + multi_output_buffers + and multi_output_buffers[gemm_idx].get_name() + in all_read_names + and orig_src[gemm_idx].get_name() not in all_read_names + ): + # Epilogue might directly read the MultiOutput, Locallize MultiOutput to the local Buffer + # if this MultiOutput has not been stored by in-template epilogue + # otherwise, use the cse store cache if it will be stored before used + global_buffers.append(multi_output_buffers[gemm_idx]) + scope.add_local_buffer( + src[gemm_idx], + global_buffers, + ) + else: + scope.add_local_buffer(src[gemm_idx]) + localize_epilogue_nodes.extend( + [L.copy(dst[gemm_idx], src[gemm_idx]).data.data] + ) + reindexers.append(None) + output_names.append(dst[gemm_idx].get_name()) + final_offsets.append( + [sympy.S.Zero] * len(dst[gemm_idx].get_size()) + ) + res = self.store_grouped_gemm_pointwise_nodes( + dst, + localize_epilogue_nodes, + final_offsets, + reindexers, + output_names=output_names, + ) + for gemm_idx in range(gemm_num): + if ( + multi_output_buffers + and multi_output_buffers[gemm_idx].get_name() in all_read_names + ): + # If the MultiOutput is used in the Epilogue, let's remove it from args + multi_output_name = multi_output_buffers[gemm_idx].get_name() + if ( + multi_output_name in self.args.output_buffers + and self.args.output_buffers[multi_output_name] + is not REMOVED + ): + self.remove_buffer(multi_output_name) + return res + else: + if dst[0].get_name() != src[0].get_name(): + copy_list = [] + with LocalBufferContext(self.args) as scope: + for _src, _dst in zip(src, dst): + copy_list.extend([L.copy(_dst, _src).data.data]) + scope.add_local_buffer(_src) + output_names.append(_dst.get_name()) + final_offsets.append([sympy.S.Zero] * len(_dst.get_size())) + reindexers = [None] * len(copy_list) + return self.store_grouped_gemm_pointwise_nodes( + dst, + nodes=copy_list, + offsets=final_offsets, + reindexers=reindexers, + output_names=output_names, + ) + else: + assert all( + _src.get_name() == _dst.get_name() for _src, _dst in zip(src, dst) + ) + assert all( + _src.get_layout() == _dst.get_layout() + for _src, _dst in zip(src, dst) + ) + return "" + + def check_bounds(self, expr, size, lower, upper): + # CppTemplateKernel does not need codegen related operations + return + + +class CppTemplateCaller(ir.ChoiceCaller): + """ + CppTemplateCaller + + This class represents a caller for CPP template kernels. It is a subclass of ir.ChoiceCaller. + Attributes: + name (str): The name of the caller. + category (str): The category of the caller. + bmreq (CppBenchmarkRequest): The benchmark request for the caller. + template_buffer (ir.CppTemplateBuffer): The template buffer for the caller. + """ + + def __init__( + self, + name: str, + category: str, + input_nodes: list[ir.Buffer], + layout: ir.Layout, + make_kernel_render: Callable[ + [ + ir.CppTemplateBuffer, + bool, + list[ir.IRNode] | None, + ], + str, + ], + bmreq: CppBenchmarkRequest, + template: "CppTemplate", # type: ignore[name-defined] # noqa: F821 + info_kwargs: dict[str, ir.PrimitiveInfoType | list[ir.PrimitiveInfoType]] + | None = None, + ): + super().__init__(name, input_nodes, layout, description="") + self.category = category + self.make_kernel_render = make_kernel_render + self.bmreq = bmreq + self.template = template + self.info_kwargs = info_kwargs + + def precompile(self) -> None: + assert self.bmreq is not None + self.bmreq.precompile() + + def benchmark(self, *args, out) -> float: + assert self.bmreq is not None + if config.profile_bandwidth_with_do_bench_using_profiling: + algo = self.bmreq.make_run_fn(*args, out=out) + return do_bench_using_profiling(algo) + return self.bmreq.benchmark(*args, out=out) + + def hash_key(self) -> str: + return "-".join( + [ + self.category, + self.bmreq.hash_key, + ] + ) + + def info_dict( + self, + ) -> dict[str, ir.PrimitiveInfoType | list[ir.PrimitiveInfoType]]: + return {"backend": "CPP", "op_type": "unknown"} + + def output_node(self) -> ir.TensorBox: + buffer = ir.CppTemplateBuffer( + layout=self.layout, + inputs=self.input_nodes, + make_kernel_render=self.make_kernel_render, + template=self.template, + choice=self, + ) + # Pass KTC annotation to the buffer for encoding + if "ktc" in self.annotations: + buffer.annotations["ktc"] = self.annotations["ktc"] + return ir.TensorBox.create(buffer) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..e5299897ead7eed465f46a7dcbb13570ac78fe3b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_utils.py @@ -0,0 +1,790 @@ +# mypy: allow-untyped-defs +import contextlib +import dataclasses +import functools +import math +import sys +from collections import namedtuple +from collections.abc import Callable, Sequence +from typing import Any +from unittest.mock import patch + +import sympy + +import torch +from torch._prims_common import is_integer_dtype +from torch.utils._ordered_set import OrderedSet +from torch.utils._sympy.printers import CppPrinter as _CppPrinter +from torch.utils._sympy.symbol import symbol_is_type, SymT +from torch.utils._sympy.value_ranges import ValueRanges + +from .. import ir +from ..dependencies import Dep +from ..loop_body import LoopBody +from ..scheduler import BaseSchedulerNode, SchedulerBuffer +from ..shape_propagation import BlockShapeType +from ..utils import IndentedBuffer, sympy_index_symbol_with_prefix, sympy_subs +from ..virtualized import ops, OpsValue, V +from .common import CSEVariable, Kernel, KernelArgs, OptimizationContext + + +DTYPE_TO_CPP = { + torch.float32: "float", + torch.float64: "double", + torch.float16: "at::Half", + torch.int64: "int64_t", + torch.int32: "int32_t", + torch.int16: "int16_t", + torch.int8: "int8_t", + torch.uint64: "uint64_t", + torch.uint32: "uint32_t", + torch.uint16: "uint16_t", + torch.uint8: "uint8_t", + torch.bool: "bool", + torch.bfloat16: "at::BFloat16", + torch.complex32: "at::complex", + torch.complex64: "at::complex", + torch.complex128: "at::complex", + torch.float8_e4m3fn: "at::Float8_e4m3fn", + torch.float8_e5m2: "at::Float8_e5m2", + torch.float8_e4m3fnuz: "at::Float8_e4m3fnuz", + torch.float8_e5m2fnuz: "at::Float8_e5m2fnuz", +} + +DTYPE_TO_ATEN = { + torch.float32: "at::kFloat", + torch.float64: "at::kDouble", + torch.float16: "at::kHalf", + torch.int64: "at::kLong", + torch.int32: "at::kInt", + torch.int16: "at::kShort", + torch.int8: "at::kChar", + torch.uint64: "at::kUInt64", + torch.uint32: "at::kUInt32", + torch.uint16: "at::kUInt16", + torch.uint8: "at::kByte", + torch.uint32: "at::kUInt32", + torch.uint64: "at::kUInt64", + torch.bool: "at::kBool", + torch.bfloat16: "at::kBFloat16", + torch.complex32: "at::kComplexHalf", + torch.complex64: "at::kComplexFloat", + torch.complex128: "at::kComplexDouble", + torch.float8_e4m3fn: "at::kFloat8_e4m3fn", + torch.float8_e5m2: "at::kFloat8_e5m2", + torch.float8_e4m3fnuz: "at::kFloat8_e4m3fnuz", + torch.float8_e5m2fnuz: "at::kFloat8_e5m2fnuz", +} + +DEVICE_TO_ATEN = { + "meta": "at::kMeta", + "cpu": "at::kCPU", + "cuda": "at::kCUDA", + "xpu": "at::kXPU", + "mps": "at::kMPS", +} + +LAYOUT_TO_ATEN = { + torch.strided: "at::kStrided", + torch._mkldnn: "at::kMkldnn", # type: ignore[attr-defined] +} + +# matches c10/core/DeviceType.h +DEVICE_TO_INT = {"cpu": 0, "cuda": 1} + +_IS_WINDOWS = sys.platform == "win32" + +INDEX_TYPE = "int64_t" + +GemmBlocking = namedtuple("GemmBlocking", ["block_m", "block_n", "block_k"]) + + +def get_promote_dtype(args): + return ( + # pyrefly: ignore [no-matching-overload] + functools.reduce( + torch.promote_types, # type: ignore[arg-type] + # pyrefly: ignore [bad-argument-type] + [n.dtype for n in args if isinstance(n, CppCSEVariable)], + ) + if all(n.dtype is not None for n in args if isinstance(n, CppCSEVariable)) + else None # not enough info to calculate the promote dtype + ) + + +def promote_args(new_args): + def promote_arg(arg, promote_type): + if ( + isinstance(arg, CppCSEVariable) + and arg.dtype + and promote_type + and arg.dtype != promote_type + ): + arg = ops.to_dtype(arg, promote_type) + arg = arg.value if isinstance(arg, OpsValue) else arg + arg.dtype = promote_type + return arg + + promote_type = get_promote_dtype(new_args) + promote_fn = functools.partial( + promote_arg, + promote_type=promote_type, + ) + if ( + all( + new_arg.dtype is not None + for new_arg in new_args + if isinstance(new_arg, CppCSEVariable) + ) + and promote_type + ): + new_args = list(map(promote_fn, new_args)) + return new_args + + +class CppCSEVariable(CSEVariable): + def __init__( + self, + name, + bounds: ValueRanges[Any], + dtype: torch.dtype | None = None, + shape: BlockShapeType = None, + ) -> None: + super().__init__(name, bounds, dtype, shape=shape) + self.is_vec = False + self.dependent_itervars = OrderedSet[sympy.Symbol]() + + def __repr__(self) -> str: + return ( + f"CppCSEVariable(name: {self.name}, bounds: {self.bounds}, is_vec: {self.is_vec}, dtype: {self.dtype}, " + f"dependent_itervars: {self.dependent_itervars})" + ) + + def update_on_args(self, name, args, kwargs): + if name == "load": + # args[2] is index + self._set_dependent_itervars(args[2]) + else: + # propagate relevant itervars and is_vec from args + self.dependent_itervars.update( + *[ + arg.dependent_itervars + for arg in args + if isinstance(arg, CppCSEVariable) + ] + ) + if name == "index_expr": + self._set_dependent_itervars(args[0]) + if any(arg.is_vec for arg in args if isinstance(arg, CppCSEVariable)): + self.is_vec = True + + def _set_dependent_itervars(self, index: sympy.Expr): + """ + Set the relevant itervars for this variable based on the `index` expression. + This includes the itervars directly used in the `index` as well as relevant itervars + of other cse variables used in the `index`. + """ + for s in index.free_symbols: + if s in V.kernel.itervars: + self.dependent_itervars.add(s) # type: ignore[arg-type] + elif s.name in V.kernel.cse.varname_map: # type: ignore[attr-defined] + self.dependent_itervars.update( + V.kernel.cse.varname_map[s.name].dependent_itervars # type: ignore[attr-defined] + ) + + def depends_on(self, itervar: sympy.Symbol): + return itervar in self.dependent_itervars + + +class CppPrinter(_CppPrinter): + def doprint(self, expr, *, simplify: bool = True, p=True): + # TODO: why are people passing strings to the printer here :think: + if simplify and isinstance(expr, sympy.Expr) and hasattr(V.graph, "sizevars"): + expr = V.graph.sizevars.simplify(expr) + return super().doprint(expr) + + def parenthesize(self, item: sympy.Expr, level: int, strict: bool = False) -> str: + if isinstance(item, sympy.Mod): + # use parenthesis to enforce precedence. + # in sympy 1.13.3, -2*Mod(x,y) becomes -2*x%y, which is wrong. + # pyrefly: ignore [missing-attribute] + return f"({self._print(item)})" + else: + return super().parenthesize(item, level, strict) + + +# A function to print, useful for printing sympy symbols. +cexpr = CppPrinter().doprint + + +def cexpr_index(index): + return f"static_cast<{INDEX_TYPE}>({cexpr(index)})" + + +def value_to_cpp(value, cpp_type): + if value == float("-inf"): + return f"-std::numeric_limits<{cpp_type}>::infinity()" + elif value == float("inf"): + return f"std::numeric_limits<{cpp_type}>::infinity()" + elif isinstance(value, bool): + return f"static_cast<{cpp_type}>({str(value).lower()})" + elif math.isnan(value): + return f"std::numeric_limits<{cpp_type}>::quiet_NaN()" + else: + return f"static_cast<{cpp_type}>({repr(value)})" + + +def rewrite_index_for_function( + localize_buffer_handler: "LocalizeBufferHandler", + index: sympy.Expr, + global_buf_name: str, +): + # Local buffer at the inner dimensions + snode = V.graph.scheduler.name_to_buf[global_buf_name].defining_op + assert snode is not None + local_buf = localize_buffer_handler.global_to_local[global_buf_name] + scheduler_nodes = snode.get_nodes() + _, (group, reduction_group) = max( + scheduler_nodes, key=lambda x: int(x.is_reduction()) + ).group + call_ranges = tuple(group) + tuple(reduction_group) + indices_to_keep = [ + f"x{len(call_ranges) - (idx + 1)}" + for idx in range(len(local_buf.get_layout().size)) + ] + sorted_symbols = sorted(index.free_symbols, key=lambda s: s.name) # type: ignore[attr-defined] + replacements = {} + for x in sorted_symbols: + if x.name.startswith("x") and x.name not in indices_to_keep: # type: ignore[attr-defined] + # Only keep index used by local buffer + replacements[x] = sympy.core.numbers.Zero() + index = sympy_subs(index, replacements) # type: ignore[arg-type] + return index + + +def rewrite_index_for_nodes( + localize_buffer_handler: "LocalizeBufferHandler", + index: sympy.Expr, + global_buf_name: str, +): + used_vars = OrderedSet( + s for s in index.free_symbols if symbol_is_type(s, SymT.INDEX) + ) + index_vars = [] + local_buf = localize_buffer_handler.global_to_local[global_buf_name] + for i in range(len(local_buf.get_size())): + var = sympy_index_symbol_with_prefix(SymT.INDEX, i) + index_vars.append(var if var in used_vars else 0) + index = local_buf.get_layout().make_indexer()(index_vars) + return index + + +class LocalizeBufferHandler(V.WrapperHandler): # type: ignore[name-defined] + def __init__( + self, + inner, + global_to_local: dict[str, ir.Buffer], + rewrite_index: Callable[["LocalizeBufferHandler", sympy.Expr, str], sympy.Expr], + ) -> None: + super().__init__(inner) + self.global_to_local = global_to_local + self.rewrite_index = rewrite_index + + def localize(self, name: str, index: sympy.Expr): + if self.global_to_local and name in self.global_to_local: + assert self.rewrite_index is not None + index = self.rewrite_index(self, index, name) + name = self.global_to_local[name].get_name() + return name, index + + def load(self, name: str, index: sympy.Expr): + return self._inner.load(*self.localize(name, index)) + + def store(self, name, index, value, mode=None): + local_buffer_name, local_buffer_index = self.localize(name, index) + res = self._inner.store(local_buffer_name, local_buffer_index, value, mode) + if ( + self.global_to_local + and name in self.global_to_local + and isinstance(V.kernel, Kernel) + ): + # Remove name of local buffer from Kernel.store_buffer_names + # local_buffer_name is added to Kernel.store_buffer_names in Kernel.CSEProxy.store. + V.kernel.store_buffer_names.discard(local_buffer_name) + return res + + def store_reduction(self, name, index, value): + # pyrefly: ignore [bad-argument-count] + return self._inner.store_reduction(*self.localize(name, index), value) + + +class LocalBufferContext: + """ + This class creates a context that helps to generate code involving Inductor IR with + function local buffers. These buffers are constructed during the codegen process and + are used to store intermediate results such as local accumulators. We do not want to + add them to `V.graph` since they are not global and we do not want to add them as + function arguments either. So we patch the codegen processes under this scope to support + these buffers without exposure to the outside world. + """ + + def __init__(self, kernel_args: KernelArgs) -> None: + self.kernel_args = kernel_args + self.exit_stack = contextlib.ExitStack() + # map local buffer name to local buffer + self.local_buffers: dict[str, ir.Buffer] = {} + # map global buffer name to global buffer + self.global_buffers: dict[str, ir.Buffer] = {} + # map global buffer name to local buffer + self.global_to_local: dict[str, ir.Buffer] = {} + # record the global buffers that are removed by this LocalBufferContext + self.removed_buffers: OrderedSet[str] = OrderedSet() + + def __enter__(self): + self.exit_stack.__enter__() + original_get_dtype = V.graph.get_dtype + + def get_dtype(name): + if name in self.local_buffers: + return self.local_buffers[name].get_dtype() + return original_get_dtype(name) + + self.exit_stack.enter_context(patch.object(V.graph, "get_dtype", get_dtype)) + + original_input = self.kernel_args.input + + def input(name): + if name in self.local_buffers: + return name + return original_input(name) + + self.exit_stack.enter_context(patch.object(self.kernel_args, "input", input)) + + original_output = self.kernel_args.output + + def output(name): + if name in self.local_buffers: + return name + return original_output(name) + + self.exit_stack.enter_context(patch.object(self.kernel_args, "output", output)) + + # Set current LocalBufferContext into V + self.exit_stack.enter_context(V.set_local_buffer_context(self)) + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.local_buffers.clear() + self.exit_stack.__exit__(exc_type, exc_val, exc_tb) + + def add_local_buffer( + self, local_buffer: ir.Buffer, global_buffers: list[ir.Buffer] | None = None + ): + assert local_buffer.get_name() not in self.local_buffers + self.local_buffers[local_buffer.get_name()] = local_buffer + if global_buffers: + for global_buffer in global_buffers: + global_buffer_name = global_buffer.get_name() + assert ( + global_buffer_name not in self.global_buffers + and global_buffer_name not in self.global_to_local + ) + self.global_buffers[global_buffer_name] = global_buffer + self.global_to_local[global_buffer_name] = local_buffer + if global_buffer_name not in V.graph.removed_buffers: + # Record the global buffers that are removed by this LocalBufferContext + # since which may need to restore. Refer to issue: + # https://github.com/pytorch/pytorch/issues/144186 + self.removed_buffers.add(global_buffer_name) + V.graph.removed_buffers.add(global_buffer_name) + + def localize_function( + self, + fn: Callable[..., Any], + rewrite_index: Callable[ + ["LocalizeBufferHandler", sympy.Expr, str], sympy.Expr + ] = rewrite_index_for_function, + ): + def inner(*args, **kwargs): + with V.set_ops_handler( + LocalizeBufferHandler( + V.get_ops_handler(), + global_to_local=self.global_to_local, + rewrite_index=rewrite_index, + ) + ): + return fn(*args, **kwargs) + + return inner + + def localize_nodes( + self, + nodes: list[ir.IRNode], + rewrite_index: Callable[ + ["LocalizeBufferHandler", sympy.Expr, str], sympy.Expr + ] = rewrite_index_for_nodes, + ) -> list[ir.IRNode]: + """ + Given `local_buf` and `global_buf` registered in current `LocalBufferContext` + though the method of `add_local_buffer`, localizes the `global_buf` to `local_buf` + for the given `nodes` and returns a new list of IR nodes that work on `local_buf` + instead of `global_buf`, i.e., all the loads and stores are redirected to + `local_buf`. This helps the fused loops to work on smaller-sized local buffers + for better data locality. + + The data access of `local_buf` is assumed to be contiguous with the + same order as the `global_buf`. + """ + assert len(nodes) > 0 + + def wrap_inner_fn_for_node(node: ir.IRNode): + loops = node.data if isinstance(node, ir.ComputedBuffer) else node + assert isinstance(loops, ir.Loops) + new_inner_fn = self.localize_function( + loops.inner_fn, + rewrite_index, + ) + + new_loops = dataclasses.replace(loops, inner_fn=new_inner_fn) + if isinstance(node, ir.ComputedBuffer): + new_node = ir.ComputedBuffer( + name=node.get_name(), layout=node.get_layout(), data=new_loops + ) + else: + new_node = new_loops # type: ignore[assignment] + + return new_node + + return [wrap_inner_fn_for_node(node) for node in nodes] + + +def unify_mask_base_type( + buffer: IndentedBuffer, + vars: tuple[CSEVariable, ...], + dtype=torch.float, +): + """ + Given list of cse variables, + Cast each to new mask base dtype and return casted cse variable. + """ + new_vars = ( + V.kernel.cse.generate( + buffer, + f"{V.kernel._get_mask_cast(var, dtype)}", + ) + for var in vars + ) + return new_vars + + +def may_unify_binary_op_mask_type(a, b): + """ + Given two cse variables, when dtype is bool, unify them to the same mask dtype and return casted cse variable. + """ + if a.dtype == torch.bool: + assert b.dtype == torch.bool + mask_dtype = torch.int32 + return unify_mask_base_type(V.kernel.compute, (a, b), mask_dtype) + return a, b + + +def codegen_rand(offset, code, rand_function, dst_dtype=torch.float32): + assert is_integer_dtype(offset.dtype) + code.writeline("[&]()") + with code.indent(): + code.writeline( + f"{DTYPE_TO_CPP[offset.dtype]} offset[{V.kernel.tiling_factor}];" + ) + code.writeline(f"{DTYPE_TO_CPP[dst_dtype]} result[{V.kernel.tiling_factor}];") + code.writeline(f"{offset}.store(offset);") + code.writeline( + f"for( {DTYPE_TO_CPP[offset.dtype]} offset_idx = 0; offset_idx < {V.kernel.tiling_factor}; offset_idx++ )" + ) + with code.indent(): + code.writeline(rand_function) + num_vectors = V.kernel._get_num_vectors(dtype=dst_dtype) + if num_vectors == 1: + code.writeline( + f"return at::vec::Vectorized<{DTYPE_TO_CPP[dst_dtype]}>::loadu(result);" + ) + else: + code.writeline( + f"return at::vec::VectorizedN<{DTYPE_TO_CPP[dst_dtype]}, {num_vectors}>::loadu(result);" + ) + code.writeline("()") + return code + + +def get_gemm_template_output_and_compute_dtype(input_dtype): + if input_dtype in [torch.uint8, torch.int8]: + return (torch.int32, torch.int32) + else: + return (torch.float32, torch.float32) + + +def create_epilogue_with_attr(input_buffer, attr, **kwargs): + input_loader = input_buffer.make_loader() + dtype = input_buffer.get_dtype() + if attr == "relu": + + def inner_fn(index): + input = input_loader(index) + zero = ops.constant(0, dtype) + return ops.maximum(input, zero) + + elif attr == "gelu": + assert "algorithm" in kwargs + if kwargs["algorithm"] == "none": + + def inner_fn(index): + input = input_loader(index) + if dtype != torch.float: + input = ops.to_dtype(input, torch.float) + half = ops.constant(0.5, torch.float) + one = ops.constant(1.0, torch.float) + const = ops.constant(0.7071067811865476, torch.float) + result = input * half * (ops.erf(input * const) + one) + if dtype != torch.float: + result = ops.to_dtype(result, dtype) + return result + + else: + assert kwargs["algorithm"] == "tanh" + + def inner_fn(index): + input = input_loader(index) + if dtype != torch.float: + input = ops.to_dtype(input, torch.float) + half = ops.constant(0.5, torch.float) + one = ops.constant(1.0, torch.float) + const1 = ops.constant(0.7978845608028654, torch.float) + const2 = ops.constant(0.044715, torch.float) + result = ( + half + * input + * ( + one + + ops.tanh(const1 * (input + const2 * input * input * input)) + ) + ) + if dtype != torch.float: + result = ops.to_dtype(result, dtype) + return result + + elif attr == "swish": + + def inner_fn(index): + input = input_loader(index) + result = input * ops.sigmoid(input) + return result + + elif attr == "sigmoid": + + def inner_fn(index): + return ops.sigmoid(input_loader(index)) + + elif attr == "tanh": + + def inner_fn(index): + return ops.tanh(input_loader(index)) + + elif attr == "hardswish" or attr == "hardsigmoid": + + def hardsigmoid_float(input): + zero = ops.constant(0, torch.float) + six = ops.constant(6, torch.float) + three = ops.constant(3, torch.float) + one_over_six = ops.constant(0.16666666666666666, torch.float) + max = ops.maximum(input + three, zero) + min = ops.minimum(max, six) + return min * one_over_six + + def inner_fn(index): + input = input_loader(index) + if dtype != torch.float: + input = ops.to_dtype(input, torch.float) + result = hardsigmoid_float(input) + if attr == "hardswish": + result = input * result + if dtype != torch.float: + result = ops.to_dtype(result, dtype) + return result + + elif attr == "leaky_relu": + assert "scalars" in kwargs + assert len(kwargs["scalars"]) == 1 + negative_slope = kwargs["scalars"][0] + + def inner_fn(index): + input = input_loader(index) + if dtype != torch.float: + input = ops.to_dtype(input, torch.float) + zero = ops.constant(0, torch.float) + result = ops.where( + input > zero, input, input * ops.constant(negative_slope, torch.float) + ) + if dtype != torch.float: + result = ops.to_dtype(result, dtype) + return result + + elif attr == "hardtanh": + assert "scalars" in kwargs + assert len(kwargs["scalars"]) == 2 + min_value = kwargs["scalars"][0] + max_value = kwargs["scalars"][1] + + def inner_fn(index): + input = input_loader(index) + if dtype != torch.float: + input = ops.to_dtype(input, torch.float) + result = ops.minimum( + ops.maximum(input, ops.constant(min_value, torch.float)), + ops.constant(max_value, torch.float), + ) + if dtype != torch.float: + result = ops.to_dtype(result, dtype) + return result + + elif attr in ["add", "sub", "mul"]: + assert "other" in kwargs + other = kwargs["other"] + num_input_dims = len(input_buffer.get_size()) + num_other_dims = len(other.get_size()) + dims_diff = num_input_dims - num_other_dims + other_loader = other.make_loader() + + def inner_fn(index): + op = getattr(ops, attr) + if dims_diff != 0: + return op(input_loader(index), other_loader(index[dims_diff:])) + else: + return op(input_loader(index), other_loader(index)) + + elif attr == "bias_add": + assert "other" in kwargs + assert "beta" in kwargs + assert "dtype" in kwargs + beta = kwargs["beta"] + other = kwargs["other"] + dtype = kwargs["dtype"] + bias_loader = other.make_loader() + + def inner_fn(index): + bias = bias_loader(index) + input = input_loader(index) + if beta != 1: + result = ops.constant(beta, torch.float) * bias + input + else: + result = bias + input + return result + + else: + raise ValueError(f"Unsupported epilogue attribute: {attr}") + return ir.Pointwise( + device=input_buffer.get_device(), + dtype=dtype, + inner_fn=inner_fn, + ranges=input_buffer.get_size(), + ) + + +def _get_loop_body(fn_list): + if all(isinstance(fn, LoopBody) for fn in fn_list): + loop_bodies = fn_list + else: + if hasattr(fn_list[0], "original_fn"): + # For the case of local buffer, we wrap the fn with localize_function + assert all(hasattr(fn, "original_fn") for fn in fn_list) + assert all( + isinstance(fn.original_fn.args[0]._body, LoopBody) for fn in fn_list + ) + loop_bodies = [fn.original_fn.args[0]._body for fn in fn_list] + else: + assert all(isinstance(fn, functools.partial) for fn in fn_list) + assert all(isinstance(fn.args[0]._body, LoopBody) for fn in fn_list) + loop_bodies = [fn.args[0]._body for fn in fn_list] + assert loop_bodies is not None + return loop_bodies + + +def _get_dtype_from_loopbodies(loop_bodies): + dtypes = OrderedSet[torch.dtype]() + for loop_body in loop_bodies: + graphs = [loop_body.root_block.graph] + [ + body.graph for body in list(loop_body.subblocks.values()) + ] + for graph in graphs: + for node in graph.nodes: + if node.op != "call_method": + continue + dtypes.add(node.meta[OptimizationContext.key].dtype) + return dtypes + + +def template_fusion_with_epilogues_supported( + template: BaseSchedulerNode, epilogues: list[BaseSchedulerNode] +) -> tuple[bool, bool]: + def _get_indexes_of_template_buf_read( + epilogue_node: ir.Operation, template_buf_names: list[str] + ) -> list[sympy.Expr]: + return [ + read.index + for read in epilogue_node.get_reads() + if read.name in template_buf_names + ] + + def _check_supported_and_same_indexes( + index_of_template_buf_read: Sequence[sympy.Expr], + epilogue_writes: OrderedSet[Dep], + ) -> tuple[bool, bool]: + num_indexes = len(OrderedSet(index_of_template_buf_read)) + + if num_indexes > 1: + same_index = False + supported = False # Different read indexes not supported + elif num_indexes == 0: + same_index = True + supported = True # No reads, automatically supported + elif num_indexes == 1: + iotbr = index_of_template_buf_read[0] + same_index = all(write.index == iotbr for write in epilogue_writes) + # TODO: Add support of fusion when the read of template buffer and the write of epilogue output + # in the epilogue node don't have the same index and change supported to True + supported = same_index + else: + raise AssertionError("Should not reach here") + + return supported, same_index + + def _template_fusion_supported( + template_outputs: Sequence[SchedulerBuffer], epilogue_nodes: list[ir.Operation] + ) -> tuple[bool, bool]: + template_buf_names = [x.get_name() for x in template_outputs] + indexes_of_template_buf_reads = [ + _get_indexes_of_template_buf_read(epilogue_node, template_buf_names) + for epilogue_node in epilogue_nodes + ] + epilogue_nodes_writes = [ + epilogue_node.get_read_writes().writes for epilogue_node in epilogue_nodes + ] + + results = [ + _check_supported_and_same_indexes(reads, writes) + for reads, writes in zip( + indexes_of_template_buf_reads, epilogue_nodes_writes + ) + ] + supported, same_indexes = zip(*results) + return all(supported), all(same_indexes) + + assert template.is_template() + template_outputs = template.get_outputs() + + epilogue_nodes = [ + n.node + for epilogue in epilogues + for n in epilogue.get_nodes() + if n.node is not None + ] + return _template_fusion_supported(template_outputs, epilogue_nodes) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_wrapper_cpu.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_wrapper_cpu.py new file mode 100644 index 0000000000000000000000000000000000000000..dc3e3c94d5809db39f3865449a7f73dceafa76f4 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_wrapper_cpu.py @@ -0,0 +1,3100 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import ctypes +import functools +import math +import os +import sys +import textwrap +from itertools import chain, count +from typing import Any, Protocol, TYPE_CHECKING + +import sympy + +import torch +import torch._higher_order_ops.torchbind +import torch._inductor.async_compile # noqa: F401 required to warm up AsyncCompile pools +import torch._ops +from torch._inductor.runtime.runtime_utils import dynamo_timed +from torch.fx.experimental.symbolic_shapes import ConvertIntKey, DivideByKey, SymTypes +from torch.utils._ordered_set import OrderedSet +from torch.utils._sympy.functions import CleanDiv, FloorDiv, Mod, ModularIndexing +from torch.utils._sympy.symbol import symbol_is_type, SymT + +from .. import config, cpp_builder, ir +from ..ir import ExternKernel +from ..utils import _align, DeferredLineBase, LineContext, normalize_name +from ..virtualized import V +from .aoti_hipify_utils import maybe_hipify_code_wrapper +from .common import get_device_op_overrides, IndentedBuffer, Kernel +from .cpp_utils import cexpr, DEVICE_TO_ATEN, DEVICE_TO_INT, DTYPE_TO_ATEN, DTYPE_TO_CPP +from .wrapper import ( + codegen_reinterpret_view_helper, + EnterSubgraphLine, + ExitSubgraphLine, + PythonWrapperCodegen, + SymbolicCallArg, +) + + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + + from ..graph import GraphLowering + + # At most, the list nesting can go one layer deep. + _OUTPUT_ARGS_TYPE = list[str | None | list[str | None]] + + from ..scheduler import BaseSchedulerNode + + +class HasWriteLine(Protocol): + def writeline(self, line: LineContext | DeferredLineBase | str) -> None: ... + + +class CppWrapperCpu(PythonWrapperCodegen): + """ + Generates cpp wrapper for running on CPU and calls cpp kernels + """ + + def __init__(self): + if not hasattr(self, "device"): + self.device = "cpu" + # must be initialized prior to calling super().__init__() + self.included_devices: OrderedSet[str] = OrderedSet() + self.model_class_name_suffix = ( + "" + if config.aot_inductor.dynamic_linkage + else config.aot_inductor.model_name_for_generated_files + ) + self.aoti_model_class_name = f"AOTInductorModel{self.model_class_name_suffix}" + + super().__init__() + + self.declare = "auto " + self.declare_maybe_reference = "decltype(auto) " + self.ending = ";" + self.comment = "//" + self.none_str = "nullptr" + self.supports_intermediate_hooks = False + self.kernel_callsite_id = count() + self.int_array_id = count() # for int array local variable declarations + self.declared_int_array_vars: OrderedSet[str] = OrderedSet() + self.tmp_tensor_id = count() # for tmp tensor local variable declarations + self.arg_var_id = count() + self.used_cached_devices: OrderedSet[str] = OrderedSet() + self.used_cached_dtypes: OrderedSet[str] = OrderedSet() + self.used_cached_layouts: OrderedSet[str] = OrderedSet() + self.used_cached_memory_formats: OrderedSet[str] = OrderedSet() + self.used_cond_predicate: OrderedSet[str] = OrderedSet() + self.cached_output_id = count() + self.scalar_to_tensor_id = count() + self.custom_op_wrapper_loaded = False + # For GEMM kernels that must be initialized and are resolved at linking. + self.initialized_kernels: dict[str, Kernel] = {} + self.device_codegen = get_device_op_overrides(self.device) + # only need to include each header once + self.include_extra_header = functools.lru_cache(None)( # type: ignore[method-assign] + self._include_extra_header + ) + self.codegen_int_array_var_cache = {} + + @staticmethod + def create( + is_subgraph: bool, + subgraph_name: str | None, + parent_wrapper: PythonWrapperCodegen | None, + partition_signatures: ir.GraphPartitionSignature | None = None, + ): + # TODO - support subgraph codegen by lifting functions. Check the + # comment at CppWrapperCpu `codegen_subgraph` function. + return CppWrapperCpu() + + @staticmethod + def _generate_temporary_array_pointer( + c_type: str, elements: Sequence[str], *, force_mutable: bool = False + ) -> str: + """Get a pointer to an array that only exists for the duration of the C++ + statement it's used in.""" + # If the c_type is already a pointer, return a mutable pointer to the array. + # Otherwise, return a const pointer. In the C-shim API, pointer types are only + # const-qualified with respect to the underlying value, not any nested pointers. + # e.g. const double** is possible, but not const double* const*. This means + # that an array containing pointers must _already_ be properly const-qualified + # by the c_type, and not add additional const-ness. + # MSVC does not support implicitly converting a const iterator to a const pointer. + ptr_call = ( + "data()" + if force_mutable or c_type.endswith("*") or cpp_builder.is_msvc_cl() + else "cbegin()" + ) + return ( + f"std::array<{c_type}, {len(elements)}>{{{', '.join(elements)}}}.{ptr_call}" + ) + + def _generate_kernel_call_helper( + self, + kernel_name: str, + call_args, + *, + device=None, + triton=True, + arg_types=None, + raw_keys=None, + raw_args=None, + triton_meta=None, + inductor_meta=None, + graph_name="", + original_fxnode_name=None, + current_stream_idx=None, + ): + """ + Generates kernel call code. + + triton: Defines whether the GPU backend uses Triton for codegen. + Otherwise it uses the CUDA language for codegen. + Only valid when cuda == True. + """ + assert arg_types is not None and len(call_args) == len(arg_types), ( + "Mismatch call_args and arg_types in generate_kernel_call:\n" + f"call_args: {call_args}\n" + f"arg_types: {arg_types}" + ) + new_args = [] + for idx, arg in enumerate(call_args): + if isinstance(arg_types[idx], str) and "*" in arg_types[idx]: + new_args.append(f"({arg_types[idx]})({arg}.data_ptr())") + else: + # arg is a scalar - ensure it's a string for C++ codegen + # With Triton support, arg might be a SymPy expression or other type + new_args.append(str(arg) if not isinstance(arg, str) else arg) + # debug printer related logic for cpp kernel type. + debug_printer_manager = V.graph.wrapper_code.debug_printer + debug_printer_manager.set_printer_args( + call_args, + kernel_name, + None, + None, + "cpp", + ) + with debug_printer_manager: + self.writeline(self.wrap_kernel_call(kernel_name, new_args)) + + def write_constant(self, name, hashed): + # include a hash so our code cache gives different constants different files + self.header.writeline(f"// {name} {hashed}") + + @staticmethod + def get_device_include_path(device: str) -> str: + if V.graph.aot_mode: + return f"#include " + return f"#include " + + def add_device_include(self, device: str) -> None: + if device in self.included_devices: + return + + self.included_devices.add(device) + + # Add the default header for this device, plus any C-shim extensions that are + # present. + self.header.splice(self.get_device_include_path(device)) + extend_aoti_c_shim_include = ( + f"torch/csrc/inductor/aoti_torch/generated/extend/c_shim_{self.device}.h" + ) + extend_aoti_c_shim_path = os.path.join( + os.path.dirname(torch.__file__), + "include", + extend_aoti_c_shim_include, + ) + if os.path.exists(extend_aoti_c_shim_path): + self.header.splice(f"#include <{extend_aoti_c_shim_include}>") + + def write_header(self): + if V.graph.is_const_graph: + # We do not write header for constant graph, it will be written by main module. + return + + if not V.graph.aot_mode: + self.header.splice( + ''' + import torch + from torch._inductor.codecache import CppWrapperCodeCache + + cpp_wrapper_src = ( + r""" + ''' + ) + + for device in V.graph.device_types: + if device != "meta": + self.add_device_include(device) + + if V.graph.aot_mode: + if config.aot_inductor.dynamic_linkage: + with open( + os.path.join( + os.path.dirname(__file__), "aoti_runtime", "interface.cpp" + ) + ) as f: + self.header.splice(f.read()) + else: + # we produce a separate model header for each model in static linkage + self.header.splice(f"""#include \"{self.model_class_name_suffix}.h\"""") + self.header.splice("\n") + + if config.cpp.enable_kernel_profile: + self.header.splice( + "#include " + ) + self.header.splice( + """ + namespace torch::aot_inductor { + thread_local KernelContext* tls_kernel_context = nullptr; + } + """ + ) + + def _include_extra_header(self, header: str): + # This is needed for cpp to python dtype conversion + self.header.splice(f"#include <{header}>") + + def mark_output_type(self): + # mark output type to unwrap tensor back to python scalar + from ..ir import ShapeAsConstantBuffer + + output_is_tensor = {} + for idx, x in enumerate(V.graph.graph_outputs): + if isinstance(x, ShapeAsConstantBuffer): + output_is_tensor[idx] = False + else: + output_is_tensor[idx] = True + + self.output_is_tensor = output_is_tensor + + def write_prefix(self): + if V.graph.is_const_graph: + # We do not write prefix for constant graph, it will be written by main module. + return + if config.aot_inductor.custom_ops_to_c_shims: + # custom_ops_to_c_shims contains declaration of custom ops with C shim. + # TODO: this could be auto-generated from a passed-in custom op schema + custom_c_shims = list( + chain(*config.aot_inductor.custom_ops_to_c_shims.values()) + ) + declarations = "\n".join( + [f"extern {textwrap.dedent(shim)};" for shim in custom_c_shims] + ) + self.prefix.splice( + f""" + extern "C" {{ + {declarations} + }} + """ + ) + if V.graph.aot_mode: + self.prefix.writeline("namespace torch::aot_inductor {") + + def write_input_output_info( + self, + info_kind: str, + idx: int, + name: str, + ): + self.prefix.writeline(f"""{info_kind}[{idx}].name = "{name}";""") + + def codegen_input_symbol_assignment( + self, + name: str, + value: ir.TensorBox, + bound_vars: OrderedSet[sympy.Symbol], + ): + code = self.prefix + + @functools.cache + def sizeof(name): + self.codegen_input_size_var_decl(code, name) + return f"{name}_size" + + @functools.cache + def strideof(name): + self.codegen_input_stride_var_decl(code, name) + return f"{name}_stride" + + def codegen_symbol( + sym_or_exp: sympy.Symbol | sympy.Expr, + base_name: str, + name_fn: Callable[[str], str], + dim: int, + ): + if isinstance(sym_or_exp, sympy.Symbol): + if sym_or_exp in bound_vars: + return + code.writeline(f"int64_t {sym_or_exp} = {name_fn(base_name)}[{dim}];") + bound_vars.add(sym_or_exp) + elif isinstance(sym_or_exp, sympy.Expr): + undefined_symbols = [ + sym for sym in sym_or_exp.free_symbols if sym not in bound_vars + ] + if len(undefined_symbols) != 1: + # Skip if expression contains no symbols or if multiple + # symbols exists since we assume each base symbol is defined + # by other codegen_symbol calls. + return + + from torch.utils._sympy.solve import try_solve + + free_symbol = undefined_symbols.pop() + base_name = name_fn(base_name) + # Use a size symbol to solve the free symbol + size_symbol = sympy.Symbol(f"{base_name}_{dim}", integer=True) + code.writeline(f"int64_t {size_symbol} = {base_name}[{dim}];") + solution = try_solve(sympy.Eq(sym_or_exp, size_symbol), free_symbol) + if solution is not None: + code.writeline(f"int64_t {free_symbol} = {cexpr(solution[1])};") + bound_vars.add(free_symbol) + else: + raise AssertionError( + str(sympy.Eq(sym_or_exp, size_symbol)) + " is not solvable" + ) + + if isinstance(value, sympy.Expr): + if not isinstance(value, sympy.Symbol) or value in bound_vars: + return + if value.is_integer: + decl = "int64_t" + elif value.is_float: + decl = "double" + else: + raise AssertionError("Unexpected symbol type") + code.writeline(f"{decl} {value} = {name};") + bound_vars.add(value) + elif isinstance(value, ir.TensorBox): + for dim, size in enumerate(value.get_size()): + codegen_symbol(size, name, sizeof, dim) + for dim, stride in enumerate(value.get_stride()): + codegen_symbol(stride, name, strideof, dim) + elif isinstance(value, ir.TorchBindObject): + # torchbind objects are loaded in proxy executor + pass + else: + raise AssertionError(f"Unknown value type: {type(value)}") + + def generate_input_output_runtime_checks(self): + """ + In debug_compile mode, we generate checks to ensure the dtype/shape/stride/device of each + real input/output tensor match ones provided at compile time via sample + input/output. + """ + + def gen_check(handle_kind, idx, name, tensor): + # Wrap AtenTensorHandle with ConstantHandle for cleaner utility function access + self.prefix.writeline( + f"ConstantHandle {name} = ConstantHandle({handle_kind}[{idx}]);" + ) + self.codegen_tensor_dtype_var_decl(self.prefix, name) + expected_dtype_name = DTYPE_TO_ATEN[tensor.dtype] + dtype_str = str(tensor.dtype).split(".")[-1] + self.prefix.splice( + f""" + int32_t {name}_expected_dtype = aoti_torch_dtype_{dtype_str}(); + if ({name}_expected_dtype != {name}_dtype) {{ + std::stringstream ss; + ss << "{handle_kind}[{idx}]: unmatched dtype, " + << "expected: " << {name}_expected_dtype << "({expected_dtype_name}), " + << "but got: " << {name}_dtype << "\\n"; + throw std::runtime_error(ss.str()); + }} + """ + ) + self.codegen_input_size_var_decl(self.prefix, name) + for dim_idx, d in enumerate(tensor.get_size()): + if isinstance(d, (int, sympy.Integer)): + self.prefix.splice( + f""" + if ({d} != {name}_size[{dim_idx}]) {{ + std::stringstream ss; + ss << "{handle_kind}[{idx}]: unmatched dim value at {dim_idx}, " + << "expected: {d}, " << "but got: " << {name}_size[{dim_idx}] + << "\\n"; + throw std::runtime_error(ss.str()); + }} + """ + ) + else: + from torch.utils._sympy.value_ranges import bound_sympy + + sym_range = bound_sympy(d, V.graph.sizevars.shape_env.var_to_range) + if config.aot_inductor.check_lowerbound and not math.isinf( + sym_range.lower + ): + self.prefix.splice( + f""" + if ({name}_size[{dim_idx}] < {sym_range.lower}) {{ + std::stringstream ss; + ss << "{handle_kind}[{idx}]: dim value is too small at {dim_idx}, " + << "expected it to be >= {sym_range.lower}, " << "but got: " + << {name}_size[{dim_idx}] << "\\n"; + throw std::runtime_error(ss.str()); + }} + """ + ) + if not math.isinf(sym_range.upper): + # Limit upper bound to max C long long value (2^63 - 1) + max_long_long = ctypes.c_longlong(2**63 - 1).value + upper_bound = min(sym_range.upper, max_long_long) + self.prefix.splice( + f""" + if ({name}_size[{dim_idx}] > {upper_bound}) {{ + std::stringstream ss; + ss << "{handle_kind}[{idx}]: dim value is too large at {dim_idx}, " + << "expected to be <= {upper_bound}, " << "but got: " + << {name}_size[{dim_idx}] << "\\n"; + throw std::runtime_error(ss.str()); + }} + """ + ) + + self.codegen_input_stride_var_decl(self.prefix, name) + for stride_idx, s in enumerate(tensor.get_stride()): + if not isinstance(s, (int, sympy.Integer)): + continue + self.prefix.splice( + f""" + if ({s} != {name}_stride[{stride_idx}]) {{ + std::stringstream ss; + ss << "{handle_kind}[{idx}]: unmatched stride value at {stride_idx}, " + << "expected: {s}, " << "but got: " << {name}_stride[{stride_idx}] + << "\\n"; + throw std::runtime_error(ss.str()); + }} + """ + ) + + # check input device type + if isinstance(tensor, ir.TensorBox): + tensor_device = tensor.get_device() + if tensor_device is not None: + expected_device_type = DEVICE_TO_INT.get(tensor_device.type) + if expected_device_type is not None: + self.codegen_input_device_type_var_decl(self.prefix, name) + device_type_str = str(tensor_device.type) + self.prefix.splice( + f""" + int32_t {name}_expected_device_type = {expected_device_type}; + if ({name}_expected_device_type != {name}_device_type) {{ + std::stringstream ss; + ss << "{handle_kind}[{idx}]: unmatched device type, " + << "expected: " << {name}_expected_device_type << "{expected_device_type}({device_type_str}), " + << "but got: " << {name}_device_type << "\\n"; + throw std::runtime_error(ss.str()); + }} + """ + ) + + # Create a separate function for each input check to avoid "too big to optimize" error + for idx, (name, tensor) in enumerate(V.graph.graph_inputs.items()): + self.prefix.splice( + f""" + AOTI_NOINLINE static void check_input_{idx}( + AtenTensorHandle* input_handles + ) {{ + """ + ) + with self.prefix.indent(): + gen_check("input_handles", idx, name, tensor) + self.prefix.writeline("}") + + # force noinline to avoid any potential compilation slowdown due to aggressive + # inline done by the host compiler + self.prefix.splice( + """ + static bool _check_aoti_runtime_check_inputs_env() { + const static char* env_var_value = getenv("AOTI_RUNTIME_CHECK_INPUTS"); + const static bool result = env_var_value != nullptr && env_var_value[0] != '0'; + return result; + } + + AOTI_NOINLINE static void __check_inputs_outputs( + AtenTensorHandle* input_handles, + AtenTensorHandle* output_handles) { + if (!_check_aoti_runtime_check_inputs_env()){ + return; + } + """ + ) + with self.prefix.indent(): + for idx in range(len(V.graph.graph_inputs)): + self.prefix.writeline(f"check_input_{idx}(input_handles);") + self.prefix.writeline("}") + + def write_wrapper_decl(self): + inputs_len = len(V.graph.graph_inputs.keys()) + if V.graph.aot_mode: + self.codegen_additional_funcs() + + if V.graph.const_module: + self.header.splice(V.graph.const_module.wrapper_code.header) + + assert V.graph.const_wrapper_code is not None + self.prefix.splice(V.graph.const_wrapper_code) + + assert V.graph.const_kernel_code is not None + self.kernel_declarations.splice(V.graph.const_kernel_code) + + if V.graph.is_const_graph: + self.prefix.splice( + f""" + void {self.aoti_model_class_name}::_const_run_impl( + std::vector& output_handles, + DeviceStreamType stream, + AOTIProxyExecutorHandle proxy_executor + ) {{ + """ + ) + else: + if not config.aot_inductor.use_runtime_constant_folding: + # If we do not split the constant graph, we'll just create + # an empty implementation when wrapping the main module. + self.prefix.splice( + f""" + void {self.aoti_model_class_name}::_const_run_impl( + std::vector& output_handles, + DeviceStreamType stream, + AOTIProxyExecutorHandle proxy_executor + ) {{}} + + """ + ) + + run_impl_proto = f""" + void {self.aoti_model_class_name}::run_impl( + AtenTensorHandle* + input_handles, // array of input AtenTensorHandle; handles + // are stolen; the array itself is borrowed + AtenTensorHandle* + output_handles, // array for writing output AtenTensorHandle; handles + // will be stolen by the caller; the array itself is + // borrowed + DeviceStreamType stream, + AOTIProxyExecutorHandle proxy_executor + ) {{ + __check_inputs_outputs(input_handles, output_handles); + """ + + self.generate_input_output_runtime_checks() + self.prefix.splice(run_impl_proto) + else: + # cpp entry function for JIT with cpp wrapper + self.prefix.splice( + """ + void inductor_entry_impl( + AtenTensorHandle* + input_handles, // array of input AtenTensorHandle; handles + // are stolen; the array itself is borrowed + AtenTensorHandle* + output_handles // array for writing output AtenTensorHandle; handles + // will be stolen by the caller; the array itself is + // borrowed) + ) { + """ + ) + with self.prefix.indent(): + # assign inputs and outputs in both cases so the later codegen can be simplified + if not V.graph.is_const_graph: + if V.graph.aot_mode: + num_args = len(V.graph.graph_inputs) + else: + # Weights are promoted in the JIT mode + num_args = len(V.graph.graph_inputs) + len(V.graph.constants) + # release GIL to support multiple instances inference (in different threads of the same process) + self.prefix.splice("py::gil_scoped_release_simple release;") + + self.prefix.splice( + f""" + auto inputs = steal_from_raw_handles_to_raii_handles(input_handles, {num_args}); + """ + ) + + if inputs_len != 0: + for idx, input_key in enumerate(V.graph.graph_inputs.keys()): + # unwrap input tensor back to scalar + if isinstance(V.graph.graph_inputs[input_key], sympy.Expr): + from ..graph import may_get_constant_buffer_dtype + + dtype = may_get_constant_buffer_dtype( + V.graph.graph_inputs[input_key] # type: ignore[arg-type] + ) + assert dtype is not None, ( + "Fails to get the dtype of the sympy.Expr" + ) + self.codegen_tensor_item( + dtype, f"inputs[{idx}]", input_key, self.prefix + ) + else: + self.prefix.writeline( + f"auto {input_key} = std::move(inputs[{idx}]);" + ) + # debug printing for all input args to AOTI model + debug_printer_manager = V.graph.wrapper_code.debug_printer + debug_printer_manager.codegen_model_inputs_value_print( + input_args_to_print=[ + input_key + for input_key in V.graph.graph_inputs + if input_key.startswith("arg") + ] + ) + + assert all( + isinstance(v, torch.Tensor) for v in list(V.graph.constants.values()) + ), "Expect all constants to be Tensor" + for idx, constants_key in enumerate(V.graph.constants.keys()): + if V.graph.aot_mode: + # Weights are stored in constants_ and owned by ConstantHandle there. + # Don't call std::move here because it will cause constants_ to lose the ownership. + self.prefix.writeline( + f"""[[maybe_unused]] auto& {constants_key} = constants_->at({idx});""" + ) + else: + # Append constants as inputs to the graph + constants_idx = inputs_len + idx + self.prefix.writeline( + f"[[maybe_unused]] auto {constants_key} = std::move(inputs[{constants_idx}]);" + ) + + self.codegen_inputs() + + if V.graph.aot_mode: + if not V.graph.is_const_graph: + self.prefix.writeline("inputs.clear();") + self.prefix.writeline( + "[[maybe_unused]] auto& kernels = static_cast(*this->kernels_.get());" + ) + + def codegen_tensor_dtype_var_decl(self, code: IndentedBuffer, name): + code.writeline(f"int32_t {name}_dtype;") + code.writeline( + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_get_dtype({name}, &{name}_dtype));" + ) + + def codegen_input_size_var_decl(self, code: IndentedBuffer, name): + code.writeline(f"auto {name}_size = {name}.sizes();") + + def codegen_input_stride_var_decl(self, code: IndentedBuffer, name): + code.writeline(f"auto {name}_stride = {name}.strides();") + + def codegen_input_device_type_var_decl(self, code: IndentedBuffer, name): + code.writeline(f"int32_t {name}_device_type;") + code.writeline( + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_get_device_type({name}, &{name}_device_type));" + ) + + def codegen_additional_funcs(self): + pass + + def codegen_model_kernels(self): + self.prefix.writeline("namespace {") + + # Tell compiler we need to link with the non-mangled symbols + for kernel in self.initialized_kernels.values(): + assert hasattr(kernel, "get_signature"), ( + f"{kernel} must have get_signature implemented" + ) + signature = kernel.get_signature() + self.prefix.writeline(f'extern "C" {signature};') + + self.prefix.writeline( + "class AOTInductorModelKernels : public AOTInductorModelKernelsBase {" + ) + self.prefix.writeline(" public:") + declare_kernel = OrderedSet(self.src_to_kernel.values()) - OrderedSet( + self.initialized_kernels.keys() + ) + declare_kernel.update( + entry[0] for entry in self.user_defined_kernel_cache.values() + ) + if V.graph.const_module: + declare_kernel.update( + V.graph.const_module.wrapper_code.src_to_kernel.values() + ) + for kernel in sorted(declare_kernel): + self.prefix.writeline( + maybe_hipify_code_wrapper( + f" {self.device_codegen.cpp_kernel_type()} {kernel}{{nullptr}};" + ) + ) + for name, kernel in self.initialized_kernels.items(): + assert hasattr(kernel, "get_signature"), ( + f"{kernel} must have get_signature implemented" + ) + kernel_ptr = f"(*{name})" + signature = kernel.get_signature().replace(name, kernel_ptr) + self.prefix.writeline(f" {signature} = torch::aot_inductor::{name};") + self.prefix.writeline("};") + self.prefix.writeline("} // namespace\n\n") + + if config.aot_inductor.embed_kernel_binary: + self.prefix.writeline('extern "C" {') + for name in sorted(declare_kernel): + self.prefix.writeline( + f" extern const unsigned char __{name}_start[];" + ) + if torch.xpu.is_available(): + self.prefix.writeline( + f" extern const unsigned char __{name}_end[];" + ) + self.prefix.writeline("}") + + # MSVC string was longer than the limit of 16380 single-byte characters. + # https://learn.microsoft.com/en-us/cpp/error-messages/compiler-errors-1/compiler-error-c2026 + MSVC_C2026_MAX_STRING_LENGTH = 16000 + + def codegen_write_arg_with_large_length_string( + self, + arg_name: str, + arg_str_val: str, + max_truncate_length: int = MSVC_C2026_MAX_STRING_LENGTH, + ): + def truncate_string(s: str, length: int) -> list[str]: + return [s[i : i + length] for i in range(0, len(s), length)] + + if len(arg_str_val) > max_truncate_length: + truncated_strs = truncate_string(arg_str_val, max_truncate_length) + self.prefix.writeline(f"{arg_name} =") + for truncate_str in truncated_strs: + self.prefix.writeline(f'R"({truncate_str})"') + self.prefix.writeline(";") + else: + self.prefix.writeline(f'{arg_name} = R"({arg_str_val})";') + + def codegen_model_constructor(self): + """ + // Generated code example + AOTInductorModel::AOTInductorModel() + : AOTInductorModelBase(4, 1) { + inputs_info_[0].name = "input0"; + inputs_info_[0].dtype = "torch.float16"; + ... + constants_info_[0].name = "L__self___weight"; + constants_info_[0].dtype = at::kFloat; + constants_info_[0].offset = 0; + constants_info_[0].data_size = 8192; + constants_info_[0].shape = {64, 32}; + constants_info_[0].stride = {32, 1}; + ... + outputs_info_[0].name = "output0"; + outputs_info_[0].dtype = "torch.float16"; + } + """ + + num_inputs = len(V.graph.graph_inputs) + num_outputs = len(V.graph.graph_outputs) + num_constants = len(V.graph.constants) + include_weights = ( + "true" + if config.aot_inductor.package_constants_in_so + and config.aot_inductor.package_constants_on_disk_format != "binary_blob" + else "false" + ) + self.prefix.splice( + f""" + {self.aoti_model_class_name}::{self.aoti_model_class_name}(std::shared_ptr constants_map, + std::shared_ptr> constants_array, + const std::string& device_str, + std::optional cubin_dir) + : AOTInductorModelBase({num_inputs}, + {num_outputs}, + {num_constants}, + device_str, + std::move(cubin_dir), + {include_weights}) {{ + """ + ) + + with self.prefix.indent(): + for idx, (name, inp) in enumerate(V.graph.graph_inputs.items()): + assert not isinstance(inp, sympy.Expr), ( + f"input {name=} cannot be symbolic" + ) + self.write_input_output_info("inputs_info_", idx, name) + + all_cuda = all( + V.graph.get_original_value_of_constant(name).is_cuda + for name in V.graph.constants + if name not in V.graph.folded_constants + ) + for idx, name in enumerate(V.graph.constants.keys()): + tensor = V.graph.get_original_value_of_constant(name) + assert isinstance(tensor, torch.Tensor) + self.prefix.writeline(f"""constants_info_[{idx}].name = "{name}";""") + self.prefix.writeline( + f"constants_info_[{idx}].dtype = {self.codegen_dtype(tensor.dtype)};" + ) + # Mixed-device constants are only supported when the secondary device is CPU + if tensor.device.type != self.device and tensor.device.type != "cpu": + raise AssertionError( + f"Mixed-device constants are only supported when the secondary " + f"device is CPU. Model device is '{self.device}', but constant " + f"'{name}' is on device '{tensor.device}'." + ) + # device_index is not needed because it can be set at runtime + device_type, _ = self.codegen_device(tensor.device).split(", ") + self.prefix.writeline( + f"constants_info_[{idx}].device_type = {device_type};" + ) + self.prefix.writeline( + f"constants_info_[{idx}].offset = {tensor.storage_offset()};" + ) + + # If constants to serialize contain cpu tensors, we always align data_size it to 64. + # When loading the constants, the valid data will depends on the size + # not the data_size so there won't be correctness issue. + data_size = ( + torch.ops.mkldnn._nbytes(tensor) + if tensor.is_mkldnn + else tensor.untyped_storage().nbytes() + ) + self.prefix.writeline( + f"constants_info_[{idx}].data_size = {data_size if all_cuda else _align(data_size)};" + ) + + from_folded = "true" if name in V.graph.folded_constants else "false" + self.prefix.writeline( + f"constants_info_[{idx}].from_folded = {from_folded};" + ) + + if name in V.graph.folded_constants: + constant_type_str = "FoldedConstant" + elif name.startswith("_tensor_constant"): + constant_type_str = "TensorConstant" + elif any( + name == normalize_name(parameter_name) + for parameter_name in V.graph.named_parameters + ): + constant_type_str = "Parameter" + elif any( + name == normalize_name(buffer_name) + for buffer_name in V.graph.named_buffers + ): + constant_type_str = "Buffer" + else: + constant_type_str = "Unknown" + self.prefix.writeline( + f"constants_info_[{idx}].type = static_cast(torch::aot_inductor::ConstantType::{constant_type_str});" + ) + + size_str = ", ".join([str(s) for s in tensor.size()]) + self.prefix.writeline(f"constants_info_[{idx}].shape = {{{size_str}}};") + + stride_str = ", ".join([str(s) for s in tensor.stride()]) + self.prefix.writeline( + f"constants_info_[{idx}].stride = {{{stride_str}}};" + ) + self.prefix.writeline( + f"constants_info_[{idx}].layout = static_cast({self.codegen_layout(tensor.layout)});" + ) + + if tensor.is_mkldnn: + opaque_metadata_tensor = torch.ops.mkldnn._get_mkldnn_serialized_md( + tensor + ) + assert opaque_metadata_tensor.dim() == 1, ( + "Expect opaque_metadata_tensor to be 1-D" + ) + + opaque_metadata_list = opaque_metadata_tensor.tolist() + opaque_metadata_str = self.codegen_shape_tuple(opaque_metadata_list) + self.prefix.writeline( + f"constants_info_[{idx}].opaque_metadata = {opaque_metadata_str};" + ) + if name in V.graph.dynamo_flat_name_to_original_fqn: + original_fqn = V.graph.dynamo_flat_name_to_original_fqn.get( + name, name + ) + elif name in V.graph.allocated_constant_name: + original_fqn = V.graph.allocated_constant_name[name] + else: + raise AssertionError("original_fqn must be set for constant") + self.prefix.writeline( + f"""constants_info_[{idx}].original_fqn = "{original_fqn}";""" + ) + self.prefix.writeline("update_constants_map(std::move(constants_map));") + self.prefix.writeline("update_constants_array(std::move(constants_array));") + + def escape_string(x): + return ( + x.replace("\\", "\\\\") + .replace('"', '\\"') + .replace("\n", "\\n") + .replace("\t", "\\t") + ) + + # Origin code: self.prefix.writeline(f'in_spec_ = R"({config.aot_inductor.serialized_in_spec})";') + # Fix msvc C2026 error via codegen_write_arg_with_large_length_string + self.codegen_write_arg_with_large_length_string( + arg_name="in_spec_", arg_str_val=config.aot_inductor.serialized_in_spec + ) + # Origin code: self.prefix.writeline(f'out_spec_ = R"({config.aot_inductor.serialized_out_spec})";') + # Fix msvc C2026 error via codegen_write_arg_with_large_length_string + self.codegen_write_arg_with_large_length_string( + arg_name="out_spec_", + arg_str_val=config.aot_inductor.serialized_out_spec, + ) + + for idx, output in enumerate(V.graph.graph_outputs): + assert not isinstance(output, sympy.Expr), ( + f"output {name=} cannot be symbolic" + ) + name = f"output{idx}" + self.write_input_output_info("outputs_info_", idx, name) + + self.prefix.writeline( + "this->kernels_ = std::make_unique();" + ) + + self.prefix.writeline("}") + + def codegen_const_run_driver(self): + """ + // Generated code example + std::unordered_map AOTInductorModel::const_run_impl( + DeviceStreamType stream, + AOTIProxyExecutorHandle proxy_executor, + bool initialization + ) { + std::unordered_map folded_constants_map; + std::vector output_handles; + // build up output_handles over here. + _const_run_impl(output_handles, stream, proxy_executor); + // build up folded_constants_map + return folded_constants_map; + } + """ + + self.prefix.splice( + f""" + std::unordered_map {self.aoti_model_class_name}::const_run_impl( + DeviceStreamType stream, + AOTIProxyExecutorHandle proxy_executor, + bool initialization + ) {{ + """ + ) + if not config.aot_inductor.use_runtime_constant_folding: + self.prefix.splice( + """ + if (!initialization) { + std::cerr << "[WARNING] Calling constant_folding in model, but compiled with config: " + << "aot_inductor.use_runtime_constant_folding=False\\n"; + } + return {}; + } + """ + ) + return + + with self.prefix.indent(): + # This is a mapping to the index of constant folding graph's output + const_index_mapping: list[tuple[int, str] | None] = [None] * len( + V.graph.const_output_index + ) + for idx, (name, _) in enumerate(V.graph.constants.items()): + if name in V.graph.const_output_index: + const_index_mapping[V.graph.const_output_index[name]] = (idx, name) # type: ignore[call-overload] + assert None not in const_index_mapping, ( + "Not all constant gets mapped for constant folding graph." + ) + + self.prefix.writeline( + f""" + std::unordered_map folded_constants_map; + folded_constants_map.reserve({len(const_index_mapping)}); + std::vector output_handles({len(const_index_mapping)}); + """ + ) + + self.prefix.splice( + """ + // The below assignment of output_handles to constants is not used directly. + // It's only used to memo the correspondence of handle and constants. + """ + ) + + for output_idx, (const_idx, _) in enumerate(const_index_mapping): # type: ignore[misc] + self.prefix.writeline( + f"output_handles[{output_idx}] = constants_->at({const_idx});" + ) + + self.prefix.writeline( + "_const_run_impl(output_handles, stream, proxy_executor);" + ) + + for output_idx, (_, const_name) in enumerate(const_index_mapping): # type: ignore[misc] + self.prefix.writeline( + f'folded_constants_map["{const_name}"] = output_handles[{output_idx}];' + ) + self.prefix.writeline("return folded_constants_map;") + + self.prefix.writeline("}") + + def generate(self, is_inference): + with dynamo_timed("CppWrapperCpu.generate", log_pt2_compile_event=True): + self.write_wrapper_decl() + return super().generate(is_inference) + + def finalize_prefix(self): + prior = self.prefix + self.prefix = aot_mode_decls = IndentedBuffer() + if V.graph.aot_mode and not V.graph.is_const_graph: + aot_mode_decls.writeline("namespace torch::aot_inductor {") + self.codegen_model_kernels() + self.codegen_model_constructor() + self.codegen_const_run_driver() + aot_mode_decls.writeline("} // namespace torch::aot_inductor") + aot_mode_decls.writeline("using namespace torch::aot_inductor;") + + self.prefix = cache_decls = IndentedBuffer() + for dtype in self.used_cached_dtypes: + cache_decls.writeline(f"CACHE_TORCH_DTYPE({dtype});") + for device in self.used_cached_devices: + cache_decls.writeline(f"CACHE_TORCH_DEVICE({device});") + for layout in self.used_cached_layouts: + cache_decls.writeline(f"CACHE_TORCH_LAYOUT({layout});") + for memory_format in self.used_cached_memory_formats: + cache_decls.writeline(f"CACHE_TORCH_MEMORY_FORMAT({memory_format});") + + self.prefix.splice(aot_mode_decls) + self.prefix.splice(prior) + + def _define_kernel_helper( + self, + kernel_name: str, + kernel_body: str, + metadata: str | None = None, + gpu: bool = False, + cpp_definition: str | None = None, + ): + if cpp_definition is not None: + self.header.splice(cpp_definition) + self.kernel_declarations.splice(f"\n{kernel_body}\n") + else: + self.header.splice(f"\n{kernel_body}\n") + + def codegen_scalar_to_tensor(self, output: str): + name = f"scalar_to_tensor_{next(self.scalar_to_tensor_id)}" + self.wrapper_call.writeline( + f"RAIIAtenTensorHandle {name} = scalar_to_tensor_handle({output});" + ) + return name + + def codegen_tensor_item( + self, dtype: torch.dtype, tensor: str, scalar: str, indented_buffer=None + ): + dtype_str = str(dtype).split(".")[-1] + writer = indented_buffer or self + + if dtype == torch.float16 or dtype == torch.bfloat16: + scalar_tmp = f"{scalar}_tmp" + writer.writeline(f"{DTYPE_TO_CPP[dtype]} {scalar_tmp};") + writer.writeline( + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_item_{dtype_str}({tensor}, &{scalar_tmp}));" + ) + writer.writeline(f"float {scalar} = float({scalar_tmp});") + else: + writer.writeline(f"{DTYPE_TO_CPP[dtype]} {scalar};") + writer.writeline( + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_item_{dtype_str}({tensor}, &{scalar}));" + ) + + def generate_return(self, output_refs: list[str]): + cst_names = V.graph.constants.keys() + output2idx: dict[str, int] = {} + + # If any output ref represents an rvalue tensor, materialize it to an lvalue + # RAIIAtenTensorHandle first. This prevents situations where the code for the + # rvalue tensor references tensor handles whose contents are modified below. + output_refs = [ + self.create_tmp_raii_handle_var_if_needed(o, self.wrapper_call) + for o in output_refs + ] + + for idx, output in enumerate(output_refs): + if output == "nullptr": + continue + + is_constant_buffer = output in cst_names + output_buffer = V.graph.graph_outputs[idx] + if isinstance(output_buffer, ir.BaseView): + output_storage = output_buffer.unwrap_view() + assert isinstance(output_storage, (ir.BaseView, ir.MutableBox)) + if isinstance(output_storage.data, ir.ConstantBuffer): + is_constant_buffer = True + + if isinstance(output_buffer, ir.ShapeAsConstantBuffer): + # Need to wrap scalar into tensor as the main function returns a vector of tensors + output_tensor = self.codegen_scalar_to_tensor(output) + self.wrapper_call.writeline( + f"output_handles[{idx}] = {output_tensor}.release();" + ) + continue + + if is_constant_buffer: + # See NOTE(return_constant) above. + self.wrapper_call.writeline( + f"aoti_torch_clone({output}, &output_handles[{idx}]);" + ) + else: + if output in output2idx: + src_idx = output2idx[output] + self.wrapper_call.writeline( + f"output_handles[{idx}] = output_handles[{src_idx}];" + ) + else: + self.wrapper_call.writeline( + f"output_handles[{idx}] = {output}.release();" + ) + + if output not in output2idx: + output2idx[output] = idx + + def generate_before_suffix(self, result): + if not V.graph.is_const_graph: + if V.graph.aot_mode: + result.writeline(f"}} // {self.aoti_model_class_name}::run_impl") + else: + result.writeline("} // inductor_entry_impl") + + def generate_end(self, result): + """Generates the end of the code block, and any code needed to call it.""" + if V.graph.aot_mode: + if V.graph.is_const_graph: + result.writeline(f"}} // {self.aoti_model_class_name}::_const_run_impl") + else: + result.writeline("} // namespace torch::aot_inductor\n\n\n") + return + + if config.cpp_wrapper_build_separate: + # Close the wrapper code block, then write any kernel definitions. + result.splice('"""\n)') + if self.kernel_declarations: + result.splice('\nkernel_src = (\nr"""') + result.splice(self.kernel_declarations.getvalue()) + result.splice('"""\n)') + else: + result.splice( + """ + kernel_src = '' + """ + ) + else: + # Merge main code and kernel code + result.splice(self.kernel_declarations.getvalue()) + self.kernel_declarations.clear() + # Close the wrapper code block + result.splice('"""\n)') + + kernel_code = "kernel_src" if config.cpp_wrapper_build_separate else "None" + # Cpp entry function for JIT with cpp wrapper + result.splice( + f""" + inductor_entry = CppWrapperCodeCache.load_pybinding( + argtypes=["std::vector"], + main_code=cpp_wrapper_src, + device_type="{self.device}", + num_outputs={len(V.graph.graph_outputs)}, + kernel_code={kernel_code}, + ) + """ + ) + + wrapper_body = "input_tensors = [arg if isinstance(arg, torch.Tensor) else torch.tensor(arg, device='cpu') for arg in args]" + if V.graph.constants: + # Append constants to the input args for cpp wrapper. + # Python wrapper directly gets the value inside the wrapper call + # as a global variable passed when calling exec(code, mod.__dict__, mod.__dict__). + # For cpp wrapper, we need to pass this python value to the inductor_entry_impl function explicitly. + assert all( + isinstance(v, torch.Tensor) for v in list(V.graph.constants.values()) + ), "Expect all constants to be Tensor" + constants_str = f"[{', '.join(V.graph.constants.keys())}]" + wrapper_body += f""" + constants_tensor = {constants_str} + input_tensors.extend(constants_tensor) + """ + # Convert vector of at::Tensor to vector of AtenTensorHandle. + # If we pass at::Tensor, the compilation will be too slow. + wrapper_body += """ + input_handles = torch._C._aoti.unsafe_alloc_void_ptrs_from_tensors(input_tensors) + """ + # Release the inputs for memory reuse. + wrapper_body += """ + args.clear() + del input_tensors + """ + + # unwrap output tensor back to python scalar + if all(x for x in self.output_is_tensor.values()): + # If no ShapeAsConstantBuffer in the output, directly return the output as tensors + outputs_str = "output_tensors" + else: + outputs = [ + ( + f"output_tensors[{i}]" + if self.output_is_tensor[i] + else f"output_tensors[{i}].item()" + ) + for i in range(len(V.graph.graph_outputs)) + ] + outputs_str = f"[{', '.join(outputs)}]" + wrapper_body += f""" + output_handles = f(input_handles) + output_tensors = torch._C._aoti.alloc_tensors_by_stealing_from_void_ptrs(output_handles) + return {outputs_str} + """ + + # Wrap the func to support setting result._boxed_call = True + result.splice( + f""" + def _wrap_func(f): + def g(args): + {wrapper_body} + return g + + call = _wrap_func(inductor_entry) + """ + ) + + @staticmethod + def get_c_shim_func_name(kernel: str, device: str) -> str: + if kernel.startswith("aoti_torch_"): + return kernel + + assert "::" in kernel, "Cpp kernel name: " + kernel + " does not contain '::'" + kernel_tokens = kernel.split("::") + kernel_suffix = kernel_tokens[-1] + if kernel_suffix == "call": + kernel_suffix = kernel_tokens[-2] + + shim_fn = f"aoti_torch_{device}_{kernel_suffix}" + return shim_fn + + def generate_c_shim_extern_kernel_call( + self, + kernel: str, + args: list[str], + device: str, + *, + debug_args: list[str] | None = None, + stack_traces: OrderedSet[str] | None = None, + ) -> None: + """debug_args kwarg allows CppWrapperCpuArrayRef to pass in wrapped arguments in + place of args while preserving debug printer output.""" + # We can do this unconditionally, since we cache this call. + self.add_device_include(device) + + debug_printer_manager = V.graph.wrapper_code.debug_printer + debug_printer_manager.set_printer_args( + debug_args if debug_args is not None else args, kernel, None, None, "extern" + ) + enable_kernel_profile = config.cpp.enable_kernel_profile and sys.platform in [ + "linux", + "win32", + ] + with debug_printer_manager: + shim_fn = self.get_c_shim_func_name(kernel, device) + shim_fn_codes = [ + f"AOTI_TORCH_ERROR_CODE_CHECK({shim_fn}({', '.join(args)}));" + ] + if enable_kernel_profile: + stack_trace_str = 'R"(' + if stack_traces: + for stack_trace in stack_traces: + for line in stack_trace.split("\n"): + stack_trace_str += f"\n{line}" + stack_trace_str += "\n" + stack_trace_str += ')"' + + shim_fn_codes = [ + "{", + f"""KernelContextGuard _ctx("{shim_fn}", {stack_trace_str});""", + f"""RAIIAtenRecordFunctionHandle record_{shim_fn}_("{shim_fn}", nullptr);""", + shim_fn_codes[0], + "}", + ] + self.writelines(shim_fn_codes) + + def generate_c_shim_extern_kernel_alloc( + self, extern_kernel: ir.ExternKernelAlloc, args: list[str] + ) -> None: + # registered output buffer name + name = extern_kernel.name + output_handle_name = f"{name}_handle" + is_inplace = ( + isinstance(extern_kernel.op_overload, torch._ops.OpOverload) + and torch.Tag.inplace_view in extern_kernel.op_overload.tags + ) + + if not is_inplace: + self.writeline(f"AtenTensorHandle {output_handle_name};") + args = [*args, f"&{output_handle_name}"] + + device = d.type if (d := extern_kernel.get_device()) else self.device + + self.generate_c_shim_extern_kernel_call( + extern_kernel.get_kernel_name(), args, device + ) + + if extern_kernel.python_kernel_name in ( + "torch.ops._c10d_functional.all_reduce_.default", + "torch.ops._c10d_functional.wait_tensor.default", + ): + # all_reduce_ is an inplace op and its returned tensor is not used anywhere. + # wait_tensor returns its input without any modification and the returned tensor is not used anywhere. + # In both cases, we can immediately delete the returned AtenTensorHandle to reduce its lifetime. + self.writeline( + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_delete_tensor_object({output_handle_name}));" + ) + elif not is_inplace: + self.writeline(f"RAIIAtenTensorHandle {name}({output_handle_name});") + + def _generate_extern_kernel_alloc_helper(self, extern_kernel, args): + if getattr(extern_kernel, "outputs", None): + # ir.ExternKernelAlloc may have outputs if it returns a tuple + self.generate_c_shim_fallback_kernel(extern_kernel, args) + else: + self.generate_c_shim_extern_kernel_alloc(extern_kernel, args) + + def generate_c_shim_fallback_kernel( + self, fallback_kernel: ir.FallbackKernel, args: list[str] + ) -> None: + output_args = [] + output_raii_handles = [] + output_name_base = fallback_kernel.get_name() + for idx, output in enumerate(fallback_kernel.outputs): + if isinstance(output, ir.MultiOutput): + # TODO: handle integer output (e.g., as in attention) + name = f"{output.get_name()}" + output_handle_name = f"{name}_handle" + if output.indices: + assert output.indices[0][1] == idx, ( + f"expected {output.indices[0][1]=} == {idx=} for {output_name_base=}" + ) + self.writeline(f"AtenTensorHandle {output_handle_name};") + output_args.append(f"&{output_handle_name}") + output_raii_handles.append( + f"RAIIAtenTensorHandle {name}({output_handle_name});" + ) + elif isinstance(output, int): + output_name = f"{output_name_base}_{idx}" + self.writeline(f"int64_t {output_name} = {output};") + output_args.append(f"&{output_name}") + elif isinstance(output, sympy.Expr): + output_name = f"{output_name_base}_{idx}" + self.writeline(f"auto {output_name} = {cexpr(output)};") + output_args.append(f"&{output_name}") + elif output is None: + output_args.append("nullptr") + else: + raise NotImplementedError(f"unsupported type of {output=}") + args = args + output_args + device = d.type if (d := fallback_kernel.get_device()) else self.device + + self.generate_c_shim_extern_kernel_call( + fallback_kernel.cpp_kernel_name, # type: ignore[arg-type] + args, + device, + ) + for raii_handle in output_raii_handles: + self.writeline(raii_handle) + + def _generate_extern_kernel_out_helper( + self, + kernel: str, + out: str, + out_view: str | None, + args: list[str], + device: str, + stack_traces: OrderedSet[str] | None = None, + ) -> None: + if out_view: + out_name = f"{out}_as_strided" + self.writeline(f"auto {out_name} = {out_view};") + args.insert(0, out_name) + else: + args.insert(0, out) + + self.generate_c_shim_extern_kernel_call( + kernel, args, device, stack_traces=stack_traces + ) + + def _get_scatter_reduce_enum(self, reduce): + # Follow aten/src/ATen/native/ReductionType.h:get_operator_enum + get_operator_enum = {"add": "sum", "multiply": "prod"} + if reduce in get_operator_enum: + reduce = get_operator_enum[reduce] + + return reduce + + def _generate_scatter_fallback( + self, + output, + inputs, + cpp_kernel_name, + python_kernel_name, + src_is_tensor, + reduce, + kwargs, + device, + ): + reduce = self._get_scatter_reduce_enum(reduce) + + # call the ABI shim function instead of the ATen one + self.add_device_include(device) + cpp_kernel_name = self.get_c_shim_func_name(cpp_kernel_name, device) + # TODO: consider remove "_out" and add missing inplace variants to fallback_ops.py + cpp_kernel_name = cpp_kernel_name.replace("__", "_") + "_out" + inputs_wrapped = [str(x) for x in inputs] + line = f"{cpp_kernel_name}({output}, {','.join(inputs_wrapped)}" + + if python_kernel_name.startswith("aten.scatter_reduce"): + line += f", {','.join(kwargs)}" + else: + if src_is_tensor: + if reduce: + line += f", {V.graph.wrapper_code.val_to_arg_str(reduce)}" + else: + assert reduce is None, ( + "Expect reduce to be None for aten.scatter_ with scalar src" + ) + line += ");" + self.writeline(line) + + def _generate_index_put_fallback(self, kernel, x, indices, values, accumulate): + # TODO: update aoti_torch_index_put_out in ir.py to use autogen out version + # See the comment in codegen_reinterpret_view about why having something like + # RAIIAtenTensorHandle(tmp_tensor_handle_2) in a tmp array can cause the corresponding + # tensor prematurely deallocated, thus the temporary array trick here. + indices_str = self._generate_temporary_array_pointer( + "AtenTensorHandle", indices + ) + args = [ + x, + indices_str, + str(len(indices)), + values, + accumulate, + ] + args.insert(0, x) # set x as the output tensor, this fallback mutates x. + self.writeline(self.wrap_kernel_call(kernel, args)) + + def add_benchmark_harness(self, output): + if V.graph.aot_mode: + return + super().add_benchmark_harness(output) + + def _extract_divisors_from_expr( + self, expr: sympy.Expr + ) -> list[tuple[sympy.Expr, str]]: + """ + Walk the sympy expression and extract all divisors/modulos from + FloorDiv, CleanDiv, Mod, and ModularIndexing nodes. + + Returns a list of (divisor_expr, operation_name) tuples for divisors + that are not constant positive integers (which cannot be zero). + """ + divisors: list[tuple[sympy.Expr, str]] = [] + seen: OrderedSet[str] = OrderedSet() + + def is_constant_nonzero(e: sympy.Expr) -> bool: + """Check if expression is a constant that cannot be zero.""" + if isinstance(e, sympy.Integer): + return int(e) != 0 + if isinstance(e, sympy.Number): + return float(e) != 0 + return False + + def maybe_add_divisor(divisor: sympy.Expr, op_name: str) -> None: + """Add divisor to list if it could potentially be zero.""" + if not is_constant_nonzero(divisor): + key = str(divisor) + if key not in seen: + seen.add(key) + divisors.append((divisor, op_name)) + + def walk(e: sympy.Expr) -> None: + if isinstance(e, (FloorDiv, CleanDiv)): + _, div = e.args + maybe_add_divisor(div, "floor division") + elif isinstance(e, Mod): + _, mod = e.args + maybe_add_divisor(mod, "modulo") + elif isinstance(e, ModularIndexing): + _, div, mod = e.args + maybe_add_divisor(div, "modular indexing division") + maybe_add_divisor(mod, "modular indexing modulo") + + # Recurse into arguments + if hasattr(e, "args"): + for arg in e.args: + if isinstance(arg, sympy.Expr): + walk(arg) + + walk(expr) + return divisors + + def codegen_cpp_sizevar(self, x: sympy.Expr, *, simplify: bool = True) -> str: + maybe_simplified_x = V.graph.sizevars.simplify(x) if simplify else x + # In AOT mode, emit runtime checks for potential division/modulo by zero + # to prevent SIGFPE crashes when symbolic tensor shapes can be 0 + if V.graph.aot_mode: + divisors = self._extract_divisors_from_expr(maybe_simplified_x) + for divisor, op_name in divisors: + divisor_str = cexpr(divisor) + self.writeline( + f'AOTI_TORCH_CHECK({divisor_str} != 0, "Integer {op_name} by zero");' + ) + return cexpr(maybe_simplified_x) + + def codegen_sizevar(self, x: sympy.Expr) -> str: + return self.codegen_cpp_sizevar(x) + + def codegen_tuple_access(self, basename: str, name: str, index: str) -> str: + # in the abi_compatible mode, outputs are returned via arguments + return name + + def codegen_shape_tuple(self, shape: Sequence[sympy.Expr]) -> str: + parts = [*map(self.codegen_sizevar, shape)] + if len(parts) == 0: + return "{}" + if len(parts) == 1: + return f"{{{parts[0]}, }}" + return f"{{{', '.join(parts)}}}" + + def ensure_size_computed(self, sym: sympy.Symbol): + if isinstance(sym, sympy.Symbol) and symbol_is_type(sym, SymT.PRECOMPUTED_SIZE): + if sym in self.computed_sizes: + return + self.computed_sizes.add(sym) + expr = V.graph.sizevars.inv_precomputed_replacements[sym] + self.writeline(f"int64_t {sym} = {cexpr(expr)};") + + def _generate_symbolic_call_arg_helper( + self, arg: SymbolicCallArg, graph: GraphLowering + ) -> None: + enable_kernel_profile = config.cpp.enable_kernel_profile and sys.platform in [ + "linux", + "win32", + ] + if enable_kernel_profile or (arg.inner, graph) not in self.kernel_numel_expr: + # When enable_kernel_profile is on, each kernel call is wrapped in + # its own {} scope block, so we must redeclare the variable each + # time since prior declarations are no longer visible. + self.kernel_numel_expr.add((arg.inner, graph)) + self.writeline(f"int64_t {arg.inner} = {cexpr(arg.inner_expr)};") + else: + self.writeline(f"{arg.inner} = {cexpr(arg.inner_expr)};") + + def _codegen_dynamic_scalar(self, node): + (data,) = (t.codegen_reference() for t in node.inputs) + self.codegen_tensor_item(node.inputs[0].get_dtype(), data, f"{node.sym}_raw") + + if len(node.keypath) == 0: + self.writeline(f"auto {node.sym} = {node.sym}_raw;") + elif len(node.keypath) == 1 and isinstance(node.keypath[0], ConvertIntKey): + self.writeline(f"int64_t {node.sym} = {node.sym}_raw ? 1 : 0;") + elif len(node.keypath) == 1 and isinstance(node.keypath[0], DivideByKey): + # TODO: assert divisibility here + self.writeline( + f"int64_t {node.sym} = {node.sym}_raw / {node.keypath[0].divisor};" + ) + else: + raise AssertionError(f"unrecognized keypath {node.keypath}") + + # record in unbacked_symbol_decls so we won't generate a declaration of the symbol again + self.unbacked_symbol_decls.add(str(node.sym)) + + def codegen_dynamic_select_index(self, node, clamp): + index_cpp_str = self.val_to_arg_str_for_prim_type(node.index, int) + size_cpp_str = self.val_to_arg_str_for_prim_type(node.size, int) + + # codegen index + sym = node.unbacked_offset_symbol + index_str = ( + f"{index_cpp_str} < 0 ? {index_cpp_str} + " + f"{self.val_to_arg_str_for_prim_type(node.size, int)}: {index_cpp_str}" + ) + self.writeline(f"auto {sym}_index = {index_str};") + index_str_clamped = ( + f"{sym}_index < 0 ? 0 : ({sym}_index > {size_cpp_str} ? {size_cpp_str} : {sym}_index)" + if clamp + else f"{sym}_index" + ) + self.writeline(f"auto {sym}_index_clamped = {index_str_clamped};") + self.writeline( + f"auto {sym} = {self.val_to_arg_str_for_prim_type(node.base_offset, int)} + " + f"{self.val_to_arg_str_for_prim_type(node.base_dim_stride, int)} * {sym}_index_clamped;" + ) + # record in unbacked_symbol_decls so we won't generate a declaration of the symbol again + self.unbacked_symbol_decls.add(str(sym)) + + def codegen_dynamic_slice_size(self, node): + start_cpp_str = self.val_to_arg_str_for_prim_type(node.start, int) + end_cpp_str = self.val_to_arg_str_for_prim_type(node.end, int) + size_cpp_str = self.val_to_arg_str_for_prim_type(node.size, int) + step_cpp_str = self.val_to_arg_str_for_prim_type(node.step, int) + sym = node.unbacked_size_symbol + + def codegen_clamp(index_str, start=True): + suf = "st" if start else "en" + index_ = f"{sym}_{suf}_index" + self.writeline( + f"int64_t {index_} = {index_str} < 0 ? {index_str} + {size_cpp_str} : {index_str};" + ) + self.writeline( + f"int64_t {sym}_{suf}_cl = {index_} < 0 ? 0 : ({index_} > {size_cpp_str} ? {size_cpp_str} : {index_});" + ) + + codegen_clamp(start_cpp_str, start=True) + codegen_clamp(end_cpp_str, start=False) + if node.step == 1: + step_str = f"{sym}_en_cl - {sym}_st_cl" + else: + step_str = ( + f"({sym}_en_cl - {sym}_st_cl + {step_cpp_str} - 1) / {step_cpp_str}" + ) + self.writeline(f"int64_t {sym}_with_step = {step_str};") + self.writeline(f"int64_t {sym} = {sym}_with_step < 0 ? 0 : {sym}_with_step;") + self.unbacked_symbol_decls.add(str(sym)) + + def make_buffer_free(self, buffer): + return ( + "" + if isinstance(buffer.get_output_spec(), ir.MultiOutputLayout) + or isinstance(buffer, ir.TMADescriptor) + else f"{buffer.get_name()}.reset();" + ) + + def make_free_by_names(self, names_to_del: list[str]): + return " ".join(f"{name}.reset();" for name in names_to_del) + + def codegen_exact_buffer_reuse(self, old_name: str, new_name: str, del_line: str): + return f"auto {new_name} = std::move({old_name}); // reuse" + + def generate_profiler_mark_wrapper_call(self, stack): + self.wrapper_call.writeline( + 'RAIIAtenRecordFunctionHandle record_inductor_wrapper_call_("inductor_wrapper_call", nullptr);' + ) + + def generate_start_graph(self): + pass + + def generate_end_graph(self): + pass + + def generate_inf_and_nan_checker(self, nodes): + for buf in nodes.get_names(): + # TODO: Add buf name directly into check_inf_and_nan. + self.writeline( + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_check_inf_and_nan({buf}));" + ) + + def codegen_device(self, device): + assert device.type in DEVICE_TO_ATEN, ( + device.type + " not found in DEVICE_TO_ATEN" + ) + device_str = DEVICE_TO_ATEN[device.type][5:].lower() # remove "at::k" + self.used_cached_devices.add(device_str) + return f"cached_torch_device_type_{device_str}, {device.index if device.index else 0}" + + def codegen_dtype(self, dtype): + dtype_str = str(dtype).split(".")[-1] + self.used_cached_dtypes.add(dtype_str) + return f"cached_torch_dtype_{dtype_str}" + + def codegen_layout(self, layout): + layout_str = str(layout).split(".")[-1] + self.used_cached_layouts.add(layout_str) + return f"cached_torch_layout_{layout_str}" + + def codegen_memory_format(self, memory_format): + memory_format_str = str(memory_format).split(".")[-1] + self.used_cached_memory_formats.add(memory_format_str) + return f"cached_torch_memory_format_{memory_format_str}" + + def codegen_int_array_var( + self, + int_array: str, + writeline: Callable[..., None], + known_statically=False, + graph=None, # for per-graph caching + ) -> str: + # Use id(graph) for caching to avoid circular references + cache_key = ( + int_array, + id(writeline), + known_statically, + id(graph) if graph else None, + ) + if cache_key not in self.codegen_int_array_var_cache: + self.codegen_int_array_var_cache[cache_key] = ( + self._codegen_int_array_var_impl(int_array, writeline, known_statically) + ) + + return self.codegen_int_array_var_cache[cache_key] + + def _codegen_int_array_var_impl( + self, + int_array: str, + writeline: Callable[..., None], + known_statically: bool, + ) -> str: + # Used for size/stride declaration + # + # Because the memory planning is done in two passes (see the implementation + # of self.generate), the writeline behavior is different in the two passes. + # As a result, the emitted int array declarations may appear in a later + # position of the generated code, so the second pass codegen should not + # reuse int array declarations generated in the first pass. + # This is why writeline needs to explicitly passed in as a parameter. + var = f"int_array_{next(self.int_array_id)}" + ctype = "int64_t" + if int_array == "{}": + # An array of unknown bound cannot be initialized with {}. + if known_statically: + if config.cpp.use_constexpr_for_int_array: + writeline(f"static constexpr {ctype} *{var}=nullptr;") + else: + writeline(f"static const {ctype} *{var}=nullptr;") + else: + writeline(f"const {ctype} *{var}=nullptr;") + else: + if var not in self.declared_int_array_vars: + self.declared_int_array_vars.add(var) + if known_statically: + if config.cpp.use_constexpr_for_int_array: + writeline(f"static constexpr {ctype} {var}[] = {int_array};") + else: + writeline(f"static const {ctype} {var}[] = {int_array};") + else: + writeline(f"const {ctype} {var}[] = {int_array};") + return var + + def make_buffer_allocation(self, buffer): + return self.make_allocation( + buffer.get_name(), + buffer.get_device(), + buffer.get_dtype(), + buffer.get_size(), + buffer.get_stride(), + V.graph.get_allocation_size(buffer), + buffer.get_is_pinned(), + ) + + def make_allocation( + self, name, device, dtype, shape, stride, allocation_shape=None, is_pinned=False + ): + if allocation_shape is None: + allocation_shape = shape + + orig_stride = stride + device_str = self.codegen_device(device) + dtype_code = self.codegen_dtype(dtype) + size = self.codegen_shape_tuple(shape) + allocation_size = self.codegen_shape_tuple(allocation_shape) + stride = self.codegen_shape_tuple(orig_stride) + + size_array_var = self.codegen_int_array_var( + size, + self.wrapper_call.writeline, + known_statically=self.is_statically_known_list_of_ints(shape), + graph=self.get_codegened_graph(), + ) + + if allocation_size != size: + allocation_size_array_var = self.codegen_int_array_var( + allocation_size, + self.wrapper_call.writeline, + known_statically=self.is_statically_known_list_of_ints( + allocation_shape + ), + graph=self.get_codegened_graph(), + ) + else: + allocation_size_array_var = size_array_var + + stride_array_var = self.codegen_int_array_var( + stride, + self.wrapper_call.writeline, + known_statically=self.is_statically_known_list_of_ints(orig_stride), + graph=self.get_codegened_graph(), + ) + device_type, device_id = device_str.split(",") + device_idx = "this->device_idx_" if V.graph.aot_mode else device_id + + handle_name = f"{name}_handle" + args = [ + str(len(shape)), + allocation_size_array_var, + stride_array_var, + dtype_code, + device_type, + device_idx, + f"&{handle_name}", + ] + + self.wrapper_call.writeline(f"AtenTensorHandle {handle_name};") + pinned_str = "_pinned" if is_pinned else "" + self.wrapper_call.writeline( + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_empty_strided{pinned_str}({', '.join(args)}));" + ) + + if allocation_size != size: + old_handle_name, handle_name = handle_name, f"{name}_handle_restrided" + self.wrapper_call.writeline(f"AtenTensorHandle {handle_name};") + args = [ + old_handle_name, + size_array_var, + stride_array_var, + f"&{handle_name}", + ] + self.wrapper_call.writeline( + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_as_strided({', '.join(args)}));" + ) + self.wrapper_call.writeline( + f"wrap_with_raii_handle_if_needed({old_handle_name});" + ) + + return f"RAIIAtenTensorHandle {name}({handle_name});" + + def codegen_alloc_from_pool( + self, name, offset, dtype, shape, stride + ) -> tuple[str, list[str]]: + size = self.codegen_shape_tuple(shape) + stride = self.codegen_shape_tuple(stride) + tmp_name = f"tmp_tensor_handle_{next(self.tmp_tensor_id)}" + args = [ + name, + cexpr(offset), # bytes not numel + self.codegen_dtype(dtype), + str(len(shape)), + self.codegen_int_array_var( + size, self.wrapper_call.writeline, graph=self.get_codegened_graph() + ), + self.codegen_int_array_var( + stride, self.wrapper_call.writeline, graph=self.get_codegened_graph() + ), + f"&{tmp_name}", + ] + # We return the lines instead of writing here because writing here is bug prune. + # If you write aoti_torch__alloc_from_pool lines, you must write the RAIIAtenTensorHandle + # as well, otherwise you get memory leaks + allocations_to_write = [ + f"AtenTensorHandle {tmp_name};", + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch__alloc_from_pool({', '.join(args)}));", + ] + return f"RAIIAtenTensorHandle({tmp_name})", allocations_to_write + + def codegen_reinterpret_view( + self, + data, + size, + stride, + offset, + writeline: Callable[..., None], + dtype=None, + ) -> str: + """Returns a newly-created, temporary RAII tensor handle containing the + reinterpreted tensor data. Callers of this function are responsible for saving + the handle if persistent access is needed.""" + + d_size, d_stride, d_offset, d_dtype, collapsible = ( + codegen_reinterpret_view_helper(data) + ) + + dim = str(len(size)) + original_offset = offset + offset = self.codegen_sizevar(offset) + call_strs = [] + final_tensor_str = None + + def create_reinterpret_call() -> str: + args = [ + f"{data.get_name()}", + dim, + self.codegen_int_array_var( + self.codegen_shape_tuple(size), + writeline, + known_statically=self.is_statically_known_list_of_ints(size), + graph=self.get_codegened_graph(), + ), + self.codegen_int_array_var( + self.codegen_shape_tuple(stride), + writeline, + known_statically=self.is_statically_known_list_of_ints(stride), + graph=self.get_codegened_graph(), + ), + offset, + ] + return f"wrap_with_raii_handle_if_needed(reinterpret_tensor_wrapper({', '.join(args)}))" + + def create_dtypeview_call(reinterpret_call: str) -> tuple[str, list[str]]: + tmp_AtenTensorHandle = f"tmp_{data.get_name()}_{next(self.tmp_tensor_id)}" + tmp_call_strs = [f"AtenTensorHandle {tmp_AtenTensorHandle};"] + device_name = data.layout.device.type + dtypeview_function = f"aoti_torch_{device_name}_view_dtype" + tmp_call_strs.append( + f"AOTI_TORCH_ERROR_CODE_CHECK({dtypeview_function}" + f"({reinterpret_call}, {self.codegen_dtype(dtype)}, &{tmp_AtenTensorHandle}));" + ) + return f"RAIIAtenTensorHandle({tmp_AtenTensorHandle})", tmp_call_strs + + def create_new_tensor_handle() -> tuple[str, list[str]]: + tmp_AtenTensorHandle = f"tmp_{data.get_name()}_{next(self.tmp_tensor_id)}" + tmp_call_strs = [ + f"AtenTensorHandle {tmp_AtenTensorHandle};", + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_new_tensor_handle({data.get_name()}, &{tmp_AtenTensorHandle}));", + ] + return f"RAIIAtenTensorHandle({tmp_AtenTensorHandle})", tmp_call_strs + + collapsed = collapsible and original_offset == d_offset + if collapsed: + same_layout = size == d_size and stride == d_stride + base_dtype = d_dtype + else: + same_layout = ( + size == data.layout.size + and stride == data.layout.stride + and original_offset == data.layout.offset + ) + base_dtype = data.dtype + + if same_layout: + # pure dtypeview + if dtype is not None and dtype != base_dtype: + final_tensor_str, tmp_call_strs = create_dtypeview_call(data.get_name()) + else: + final_tensor_str, tmp_call_strs = create_new_tensor_handle() + call_strs.extend(tmp_call_strs) + else: + # firstly create reinterpretview + final_tensor_str = create_reinterpret_call() + if dtype is not None and dtype != base_dtype: + # wrap it with dtypeview + final_tensor_str, tmp_call_strs = create_dtypeview_call( + final_tensor_str + ) + call_strs.extend(tmp_call_strs) + + for line in call_strs: + writeline(line) + + # NB, the return handle here represents a temporary tensor, which will be automatically + # released. + # Here's a sample usage in the cpp wrapper code: + # ``` + # aoti_torch_addmm_out( + # buf1, + # arg1_1, + # RAIIAtenTensorHandle(tmp_tensor_handle_0), + # buf0, + # 1L, + # 1L)); + # ``` + # RAIIAtenTensorHandle(tmp_tensor_handle_0) will be released after the call to addmm_out. + # This could be problematic when it's used in a different pattern, for example: + # ```` + # AtenTensorHandle tensor_args[] = {RAIIAtenTensorHandle(tmp_tensor_handle_2), buf5, buf6}; + # aoti_torch_proxy_executor_call_function(..., tensor_args); + # ```` + # RAIIAtenTensorHandle(tmp_tensor_handle_2) will be invalid when it's used in the latter + # kernel call. + # + # This is solved by updating the proxy_executor invocation to + # ``` + # aoti_torch_proxy_executor_call_function(..., + # std::array{ + # RAIIAtenTensorHandle(tmp_tensor_handle_2), buf5, buf6 + # }.cbegin() + # ); + # ``` + return final_tensor_str + + def codegen_device_copy(self, src, dst, non_blocking: bool | str): + """This function is overridden by cpp_wrapper_cpu_array_ref, so we don't need to + handle cases where dst is not an AtenTensorHandle.""" + self.writeline( + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_copy_({dst}, {src}, {non_blocking}));" + ) + + def codegen_multi_output(self, node: ir.MultiOutput): + # in the abi_compatible mode, outputs are retrieved by passing + # output pointers, so we skip its codegen here. + pass + + def codegen_subgraph_prefix(self, subgraph, outer_inputs, outer_outputs): + assert len(subgraph.graph.graph_inputs) == len(outer_inputs) + + for (inner_input, inner_input_val), outer_input in zip( + subgraph.graph.graph_inputs.items(), outer_inputs + ): + if not isinstance(inner_input_val, ir.TensorBox): + continue + + # in ABI-compatible mode, we copy the underlying at::Tensor of the conditional + # input (outer_input) into another at::Tensor to be used as a subgraph input + # (inner_input) in the nested scope. we can't std::move here, as the codegened + # outer input may be an expression / rvalue (e.g., reinterpret_view(x)), so we + # can't necessarily std::move it back to the origin (x). + self.writeline(f"AtenTensorHandle {inner_input}_handle;") + self.writeline( + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_assign_tensors_out({outer_input}, &{inner_input}_handle));" + ) + self.writeline(f"RAIIAtenTensorHandle {inner_input}({inner_input}_handle);") + + def codegen_subgraph_suffix(self, subgraph, outer_inputs, outer_outputs): + for inner_output, outer_output in zip( + subgraph.graph.graph_outputs, outer_outputs + ): + src = inner_output.codegen_reference() + if not isinstance(inner_output, ir.ShapeAsConstantBuffer): + # in ABI-compatible mode, we need to std::move subgraph output (inner_output) + # to the conditional output (outer_output), as RAIIAtenTensorHandle's copy + # constructor is deleted. + src = f"std::move({src})" + # in case the outer_output carried a value + # before (e.g., in the while_loop codegen) + self.writeline(f"{outer_output}.reset();") + self.writeline(f"{outer_output} = {src};") + + def codegen_invoke_subgraph(self, invoke_subgraph): + raise NotImplementedError( + "codegen invoke_subgraph is not implemented for cpp wrapper" + ) + + def codegen_conditional(self, conditional): + outer_inputs = [f"{buf.codegen_reference()}" for buf in conditional.operands] + outer_outputs = [] + for out in conditional.outputs: + # in ABI-compatible mode, ir.MultiOutput is not codegened, + # hence pre-declare output variables directly and separately + self.writeline(f"RAIIAtenTensorHandle {out.get_name()};") + outer_outputs.append(out.get_name()) + + if not isinstance(conditional.predicate, ir.ShapeAsConstantBuffer): + # in ABI-compatible mode, we need to use the ABI shim function + # to extract a C++ bool from the underlying scalar bool Tensor + predicate = f"{conditional.predicate.get_name()}_scalar" + if predicate not in self.used_cond_predicate: + self.codegen_tensor_item( + torch.bool, + conditional.predicate.codegen_reference(), + predicate, + ) + self.used_cond_predicate.add(predicate) + else: + # the predicate is not a Tensor: SymBool or Python bool + predicate = conditional.predicate.codegen_reference() + + self.writeline(f"if ({predicate}) {{") + self.writeline(EnterSubgraphLine(self, conditional.true_subgraph.graph)) + self.codegen_subgraph(conditional.true_subgraph, outer_inputs, outer_outputs) + self.writeline(ExitSubgraphLine(self)) + self.writeline("} else {") + self.writeline(EnterSubgraphLine(self, conditional.false_subgraph.graph)) + self.codegen_subgraph(conditional.false_subgraph, outer_inputs, outer_outputs) + self.writeline(ExitSubgraphLine(self)) + self.writeline("}") + + def codegen_subgraph(self, subgraph, outer_inputs, outer_outputs): + # TODO (desertfire) - This function is the old way of supporting + # subgraph codegen by inlining subgraphs in the output code. For python + # wrapper, we have moved to lifting subgraphs as functions, supported by + # PythonWrapperCode `codegen_subgraph` function. We should perhaps + # support lifting of subgraphs as functions for cpp wrapper as well. + try: + self.push_codegened_graph(subgraph.graph) + self.writeline(f"// subgraph: {subgraph.name}") + self.codegen_subgraph_prefix(subgraph, outer_inputs, outer_outputs) + parent_graph = V.graph + with V.set_graph_handler(subgraph.graph): + subgraph.graph.codegen_subgraph( + parent_graph=parent_graph, + ) + self.codegen_subgraph_suffix(subgraph, outer_inputs, outer_outputs) + finally: + self.pop_codegened_graph() + + def codegen_while_loop(self, while_loop, stack_output=False): + if stack_output: + raise NotImplementedError("NYI cpp wrapper for while_loop_stack_output") + is_bool_pred = isinstance( + while_loop.cond_subgraph.graph.graph_outputs[0], ir.ShapeAsConstantBuffer + ) + name = while_loop.get_name() + outer_carried_inputs = [ + buf.codegen_reference() for buf in while_loop.carried_inputs + ] + outer_additional_inputs = [ + buf.codegen_reference() for buf in while_loop.additional_inputs + ] + cond_result_name = f"{name}_cond_result" + if is_bool_pred: + self.writeline(f"bool {cond_result_name};") + else: + self.writeline(f"RAIIAtenTensorHandle {cond_result_name};") + + cond_outer_inputs = [] + for inp, out in zip(outer_carried_inputs, while_loop.outputs): + # in ABI-compatible mode, the carried inputs are codegened + # as buffers outside the while loop and set to the initial + # values. at the end of each while_loop iteration, they + # will be assigned the carried values. + out_name = out.get_name() + self.writeline(f"AtenTensorHandle {out_name}_handle;") + self.writeline( + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_assign_tensors_out({inp}, &{out_name}_handle));" + ) + self.writeline(f"RAIIAtenTensorHandle {out_name}({out_name}_handle);") + cond_outer_inputs.append(out_name) + + # additional inputs will be assigned within the while_loop + # iteration directly from the corresponding outer graph buffers + cond_outer_inputs.extend(outer_additional_inputs) + + cond_outer_outputs = [cond_result_name] + body_outer_inputs = list(cond_outer_inputs) + body_outer_outputs = body_outer_inputs[: len(outer_carried_inputs)] + + self.writeline("while (1) {") + self.writeline(EnterSubgraphLine(self, while_loop.cond_subgraph.graph)) + self.codegen_subgraph( + while_loop.cond_subgraph, cond_outer_inputs, cond_outer_outputs + ) + + if is_bool_pred: + cond_result = f"{cond_result_name}" + else: + cond_result = f"{cond_result_name}_scalar" + self.codegen_tensor_item(torch.bool, cond_result_name, cond_result) + self.writeline(f"if (!{cond_result}) break;") + + self.writeline(ExitSubgraphLine(self)) + self.writeline(EnterSubgraphLine(self, while_loop.body_subgraph.graph)) + self.codegen_subgraph( + while_loop.body_subgraph, body_outer_inputs, body_outer_outputs + ) + self.writeline(ExitSubgraphLine(self)) + self.writeline("}") + + def generate_extern_kernel_args_decl_if_needed( + self, + op_overload: torch._ops.OpOverload | torch._ops.HigherOrderOperator, + raw_args: Sequence[Any], + output_args: _OUTPUT_ARGS_TYPE, + raw_outputs: Sequence[ir.Buffer], + ): + """ + Generates declarations for external kernel arguments if needed, based on the provided + operator and its arguments. It processes both input and output arguments, categorizing + them into tensor and integer arguments for further code generation. + """ + schema = None + if isinstance(op_overload, torch._higher_order_ops.torchbind.CallTorchBind): + obj = raw_args[0] + method = raw_args[1] + schema = op_overload.schema(obj, method) + else: + assert isinstance(op_overload, torch._ops.OpOverload), type(op_overload) + schema = op_overload._schema + assert schema is not None + arg_types = [x.real_type for x in schema.arguments] + return_types = [x.type for x in schema.returns] + + new_tensor_args = [] + new_int_args = [] + + def fill_args(arg, arg_type): + static_arg_types = ( + torch.FloatType, + torch.BoolType, + torch.StringType, + torch.Type, + torch.DeviceObjType, + ) + inductor_tensor_buffers = ( + ir.Buffer, + ir.ReinterpretView, + ) + + if isinstance(arg_type, torch.TensorType): + assert isinstance(arg, inductor_tensor_buffers), f"got {type(arg)}" + new_tensor_args.append(f"{arg.codegen_reference()}") + elif isinstance(arg_type, torch.IntType): + # int + new_int_args.append(str(arg)) + elif isinstance(arg_type, torch.SymIntType): + # SymInt + expr = arg.node.expr if isinstance(arg, torch.SymInt) else arg + new_int_args.append(cexpr(expr)) + elif isinstance(arg_type, torch.NumberType): + # Scalar of type int + assert isinstance(arg, (int, float, bool)) + # Only treat int Scalar as dynamic + if isinstance(arg, int): + new_int_args.append(str(arg)) + elif isinstance(arg, ir.TorchBindObject): + # torchbind objects are loaded in proxy executor + pass + elif isinstance(arg_type, torch.ListType): + assert isinstance(arg, (list, tuple)) + + # List[Tensor] + if isinstance(arg_type.getElementType(), torch.TensorType): + new_tensor_args.extend([f"{a.codegen_reference()}" for a in arg]) + # List[Optional[Tensor]] + elif isinstance( + arg_type.getElementType(), torch.OptionalType + ) and isinstance( + arg_type.getElementType().getElementType(), torch.TensorType + ): + new_tensor_args.extend( + [f"{a.codegen_reference()}" for a in arg if a is not None] + ) + # List[int] + elif isinstance(arg_type.getElementType(), torch.IntType): + new_int_args.extend([str(a) for a in arg]) + # List[SymInt] + elif isinstance(arg_type.getElementType(), torch.SymIntType): + expressions = [ + a.node.expr if isinstance(a, torch.SymInt) else a for a in arg + ] + new_int_args.extend([cexpr(expr) for expr in expressions]) + # List[Scalar] + elif isinstance(arg_type.getElementType(), torch.NumberType): + # Only treat int Scalar as dynamic + is_int_type = [isinstance(a, int) for a in arg] + if any(is_int_type): + assert all(is_int_type), ( + "AOTInductor only supports int scalars of the same type" + ) + new_int_args.extend([str(a) for a in arg]) + else: + assert isinstance( + arg_type.getElementType(), + static_arg_types, # type: ignore[arg-type] + ), ( + f"Fall through arguments must be one of static_arg_types, got {type(arg_type)}" + ) + else: + assert isinstance( + arg_type, + static_arg_types, # type: ignore[arg-type] + ), ( + f"Fall through arguments must be one of static_arg_types, got {type(arg_type)}" + ) + + for arg, arg_type in zip(raw_args, arg_types): + if arg is not None: + if isinstance(arg_type, torch.OptionalType): + fill_args(arg, arg_type.getElementType()) + else: + fill_args(arg, arg_type) + + def fill_output_arg( + arg: str, return_type: torch.JitType, is_mutated_output: bool + ) -> None: + if isinstance(return_type, torch.TensorType): + if not is_mutated_output: + self.writeline(f"AtenTensorHandle {arg}_handle; // output buffer") + self.writeline( + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_new_uninitialized_tensor(&{arg}_handle));" + ) + self.writeline(f"RAIIAtenTensorHandle {arg}({arg}_handle);") + new_tensor_args.append(f"{arg}") + elif isinstance(return_type, torch.SymIntType): + raise NotImplementedError("NYI support for return type: SymInt") + elif isinstance(return_type, torch.ListType) and isinstance( + return_type.getElementType(), torch.SymIntType + ): + raise NotImplementedError("NYI support for return type: List[SymInt]") + else: + raise AssertionError(f"Unsupported return type found: {return_type}") + + # TODO: Only support None and tensor(s) returns for now, SymInt is not implemented yet + for return_type in return_types: + if isinstance( + return_type, (torch.TensorType, torch.NoneType, torch.IntType) + ): + pass + elif isinstance(return_type, torch.OptionalType): + assert isinstance(return_type.getElementType(), torch.TensorType) + elif isinstance(return_type, torch.ListType): + assert isinstance(return_type.getElementType(), torch.TensorType) + else: + raise NotImplementedError( + f"return type {return_type} is not yet supported." + ) + + for output_arg, raw_output_arg in zip(output_args, raw_outputs): # type: ignore[arg-type] + # None output is supported, but Optional return types are not yet supported + if output_arg is None: + continue + elif isinstance(raw_output_arg, int): + new_int_args.append(str(raw_output_arg)) + elif isinstance(output_arg, list): + for out in output_arg: + assert out is not None, out + fill_output_arg( + out, + torch.TensorType.get(), + isinstance(raw_output_arg, ir.MutationOutput), + ) + else: + fill_output_arg( + output_arg, + torch.TensorType.get(), + isinstance(raw_output_arg, ir.MutationOutput), + ) + + return new_tensor_args, new_int_args + + @staticmethod + def _compatible_with_stableivalue(op: torch._ops.OpOverload) -> bool: + """Returns true if op_overload._schema only utilizes types supported by the AOT + C-shim *internal* function to_ivalue. to_ivalue is an implementation detail, so + these types are not guaranteed to be supported long-term. When generating code + for cpp_wrapper mode, we don't have to be forward-compatible, so changing this + function's implementation in future is fine.""" + supported_types = ( + torch.BoolType, + torch.DeviceObjType, + torch.FloatType, + # ScalarTypeType, LayoutType, and MemoryFormatType are seen as IntType + # when queried via torch.JitType.type. + torch.IntType, + torch.TensorType, + ) + + def type_supported(t: torch.JitType) -> bool: + if isinstance(t, torch.OptionalType): + return type_supported(t.getElementType()) + return isinstance(t, supported_types) + + return all( + type_supported(a.type) + for a in chain(op._schema.arguments, op._schema.returns) + ) + + def generate_fallback_kernel_with_runtime_lookup( + self, + buf_name: str, + python_kernel_name: str, + get_args: Callable[[], Sequence[str]], + op_overload: torch._ops.OpOverload | torch._ops.HigherOrderOperator, + raw_args: Sequence[Any], + outputs: Sequence[ir.Buffer], + ) -> None: + """Generate a call to a kernel not contained in the C-shim. This results in + different code paths for AOT Inductor vs cpp_wrapper Inductor mode.""" + + def extract_output_name( + out: ir.Buffer | Sequence[ir.Buffer] | None, + ) -> str | None | _OUTPUT_ARGS_TYPE: + if out is None: + return None + if isinstance(out, (ir.MultiOutput, ir._CollectiveKernel)): + return out.get_name() + if isinstance(out, ir.MutationOutput): + mutated_buf_names = out.get_mutation_names() + assert ( + isinstance(mutated_buf_names, list) and len(mutated_buf_names) == 1 + ), "Expect only one mutated buffer in MutationOutput" + return mutated_buf_names[0] + if isinstance(out, (list, tuple)): + return [extract_output_name(o) for o in out] # type: ignore[misc] + if isinstance(out, int): + return str(out) + raise AssertionError(f"Unexpected output: {type(out)}") + + if isinstance(op_overload, torch._ops.HigherOrderOperator): + assert isinstance( + op_overload, torch._higher_order_ops.torchbind.CallTorchBind + ), type(op_overload) + assert len(raw_args) > 1 + obj = raw_args[0] + method = raw_args[1] + return_schema = op_overload.schema(obj, method).returns + else: + return_schema = op_overload._schema.returns + + # output_args has the same pytree structure as outputs + if not return_schema: + # kernel does not return a value + output_args: _OUTPUT_ARGS_TYPE = [] + elif isinstance(output_name := extract_output_name(outputs), str): + output_args = [output_name] + else: + # If the schema indicates a return value, we should have a non-None value by + # this point. + assert isinstance(output_name, list), type(output_name) + output_args = output_name + + # In AOT mode, we use a ProxyExecutor to run fallback kernels. + if V.graph.aot_mode: + self.generate_fallback_kernel_with_runtime_lookup_aot( + op_overload, + raw_args, + output_args, + outputs, + ) + return + + assert isinstance(op_overload, torch._ops.OpOverload), type(op_overload) + for output in output_args: + assert output is None or isinstance(output, str), ( + "fallback kernels with runtime lookup currently only support tensor " + "returns, not more complicated types (such as list-of-list-of-tensor)" + ) + + # In non-AOT mode, we use aoti_torch_call_dispatcher if all the inputs and + # outputs of the op can be represented with StableIValue. This avoids the + # overhead of calling back into Python, and covers most remaining fallback ops. + if self._compatible_with_stableivalue(op_overload): + self.generate_fallback_kernel_with_runtime_lookup_nopython( + get_args, + op_overload, + output_args, # type: ignore[arg-type] + outputs, + ) + return + + # Otherwise, we call back into Python, which has some extra runtime overhead, + # but handles situations like list[Tensor] (currently unrepresentable via + # StableIValue). + self.generate_fallback_kernel_with_runtime_lookup_python( + buf_name, + python_kernel_name, + op_overload, + raw_args, + output_args, # type: ignore[arg-type] + outputs, + ) + + def generate_scoped_gil_acquire(self, declarations_before_scope, lines_in_scope): + scoped_lines = IndentedBuffer() + for declaration in declarations_before_scope: + scoped_lines.writeline(declaration) + + scoped_lines.writeline("{") + with scoped_lines.indent(): + scoped_lines.writeline("py::gil_scoped_acquire_simple acquire;") + scoped_lines.writelines(lines_in_scope.split("\n")) + scoped_lines.writelines("}") + return scoped_lines._lines + + def load_custom_op_wrapper(self): + # TODO: need to support control flow + if self.custom_op_wrapper_loaded: + return + + lines = """ +RAIIPyObject codecache_module(PyImport_ImportModule("torch._inductor.codecache")); +if (!codecache_module) { + throw std::runtime_error("Failed to load torch._inductor.codecache"); +} +custom_op_wrapper = PyObject_GetAttrString(codecache_module, "custom_op_wrapper"); +if (!custom_op_wrapper) { + throw std::runtime_error("Failed to load torch._inductor.codecache.custom_op_wrapper"); +}""" + + declarations_before_scope = ["RAIIPyObject custom_op_wrapper;"] + scope_gil_acquire = self.generate_scoped_gil_acquire( + declarations_before_scope, lines + ) + self.writelines(scope_gil_acquire) + + self.custom_op_wrapper_loaded = True + + def generate_float_value(self, val): + assert isinstance(val, float) + if val == float("inf"): + return "std::numeric_limits::infinity()" + elif val == float("-inf"): + return "-std::numeric_limits::infinity()" + elif math.isnan(val): + return "std::numeric_limits::quiet_NaN()" + else: + return f"{val}" + + def generate_py_arg(self, py_args_var, idx, raw_arg, arg_type): + """Generate C++ code that converts a single operator argument into a Python + object and inserts it into a PyTuple at the given index.""" + + def generate_py_arg_inner(lines, raw_arg, arg_type): + def handle_scalar(scalar): + if isinstance(scalar, int): + return f"PyLong_FromLongLong({scalar})" + if isinstance(scalar, float): + return f"PyFloat_FromDouble({self.generate_float_value(scalar)})" + if isinstance(scalar, bool): + return f"PyBool_FromLong({1 if scalar else 0})" + if isinstance(scalar, complex): + real = self.generate_float_value(scalar.real) + imag = self.generate_float_value(scalar.imag) + return f"PyComplex_FromDoubles({real}, {imag})" + if isinstance(scalar, SymTypes): + scalar_var = cexpr(scalar.node.expr) + if isinstance(scalar, torch.SymBool): + return f"PyBool_FromLong({scalar_var})" + if isinstance(scalar, torch.SymFloat): + return f"PyFloat_FromDouble({scalar_var})" + return f"PyLong_FromLongLong({scalar_var})" + raise NotImplementedError( + f"scalar {scalar}, {type(scalar)} cannot be handled by handle_scalar" + ) + + if raw_arg is None: + # Py_None is a singleton, so we have to explicitly incref it here + lines.append("Py_INCREF(Py_None);\n") + return "Py_None" + elif isinstance(arg_type, torch.TensorType): + # In some cases, scalar arguments may be passed in place of tensors. + if not hasattr(raw_arg, "codegen_reference"): + return handle_scalar(raw_arg) + + # Store AtenTensorHandle as void*. All Python args are constructed in a + # nested scope, so this handle will self-destruct after the function + # call. + base_handle = self.create_tmp_raii_handle_var_if_needed( + raw_arg.codegen_reference(), lines + ) + return f"PyCapsule_New(reinterpret_cast({base_handle}.get()), NULL, NULL)" + elif isinstance(arg_type, torch.OptionalType): + return generate_py_arg_inner(lines, raw_arg, arg_type.getElementType()) + elif isinstance(arg_type, torch.IntType): + # int + return f"PyLong_FromLongLong({raw_arg})" + elif isinstance(arg_type, torch.SymIntType): + # SymInt + expr = ( + raw_arg.node.expr if isinstance(raw_arg, torch.SymInt) else raw_arg + ) + return f"PyLong_FromLongLong({cexpr(expr)})" + elif isinstance(arg_type, torch.FloatType): + return f"PyFloat_FromDouble({self.generate_float_value(raw_arg)})" + elif isinstance(arg_type, torch.BoolType): + return f"PyBool_FromLong({1 if raw_arg else 0})" + elif isinstance(arg_type, torch.StringType): + return f'PyUnicode_FromString("{raw_arg}")' + elif isinstance(arg_type, torch.NumberType): + # Union[bool, int, float, complex] + # torch/_prims_common/__init__.py + return handle_scalar(raw_arg) + elif isinstance(raw_arg, torch.device): + device_str, device_index = self.codegen_device(raw_arg).split(", ") + return f"THPDevice_New(c10::Device(static_cast({device_str}), {device_index}))" + elif isinstance(raw_arg, torch.dtype): + return f"Py_NewRef(torch::getTHPDtype(static_cast({self.codegen_dtype(raw_arg)})))" + elif isinstance(raw_arg, torch.layout): + return f"Py_NewRef(torch::getTHPLayout(static_cast({self.codegen_layout(raw_arg)})))" + elif isinstance(raw_arg, torch.memory_format): + return ( + "Py_NewRef(torch::utils::getTHPMemoryFormat(static_cast(" + f"{self.codegen_memory_format(raw_arg)})))" + ) + else: + raise NotImplementedError( + f"arg type {arg_type} is not yet supported by custom_op_wrapper" + ) + + def handle_sequence_arg(raw_arg_, arg_type_, lines_): + assert isinstance(raw_arg_, (list, tuple)), str(raw_arg) + " is not a list" + lines_.append( + f"PyObject* {py_args_var}_{idx} = PyList_New({len(raw_arg)});\n" + ) + for i, elem in enumerate(raw_arg_): + lines_.append( + f"PyList_SetItem({py_args_var}_{idx}, {i}, {generate_py_arg_inner(lines, elem, arg_type_.getElementType())});\n" + ) + lines_.append( + f"PyTuple_SetItem({py_args_var}, {idx}, {py_args_var}_{idx});\n" + ) + + lines = [] + if isinstance(arg_type, torch.ListType): + handle_sequence_arg(raw_arg, arg_type, lines) + elif isinstance(arg_type, torch.OptionalType) and isinstance( + arg_type.getElementType(), torch.ListType + ): + if raw_arg is None: + lines.append( + f"PyTuple_SetItem({py_args_var}, {idx}, {generate_py_arg_inner(lines, raw_arg, arg_type)});\n" + ) + else: + handle_sequence_arg(raw_arg, arg_type.getElementType(), lines) + else: + lines.append( + f"PyTuple_SetItem({py_args_var}, {idx}, {generate_py_arg_inner(lines, raw_arg, arg_type)});\n" + ) + return "".join(lines) + + def generate_fallback_kernel_with_runtime_lookup_nopython( + self, + get_args: Callable[[], Sequence[str]], + op_overload: torch._ops.OpOverload, + output_args: Sequence[str | None], + raw_outputs: Sequence[ir.Buffer], + ) -> None: + """Generate fallback kernel calls with runtime (non-AOT) dispatch. This can + only be called in cpp_wrapper mode, and assumes that the input is a non-None + OpOverload. + + In the future, we may switch over to directly calling c10::Dispatcher if we need + to support more datatypes.""" + if raw_outputs: + declarations_before_scope = [ + f"RAIIAtenTensorHandle {output_arg};" + for output_arg, raw_output_arg in zip(output_args, raw_outputs) # type: ignore[arg-type] + if output_arg is not None + and not isinstance(raw_output_arg, ir.MutationOutput) + ] + else: + declarations_before_scope = [ + f"RAIIAtenTensorHandle {output_arg};" + for output_arg in output_args # type: ignore[arg-type] + if output_arg is not None + ] + + dispatch_lines = IndentedBuffer() + dispatch_lines.writelines(declarations_before_scope) + dispatch_lines.writeline("{") + + with dispatch_lines.indent(): + tmp_var_number = count() + + def parse_arg(arg_type: torch.JitType, codegen_arg: str) -> str: + # Strip off any temporary references; we're in an indented context, so + # any saved-off variables will be auto-destroyed. + new_codegen_arg = codegen_arg.removeprefix("&temporary_reference(") + if new_codegen_arg != codegen_arg: + # If we removed temporary_reference, there's a good chance the + # variable ends with get() (which would retrieve an ATenTensorHandle + # from a temporary RAII handle). Strip that off too, since we're + # going to save this in a temporary RAII handle. + if codegen_arg.endswith(".get())"): + codegen_arg = new_codegen_arg.removesuffix(".get())") + else: + codegen_arg = new_codegen_arg.removesuffix(")") + + if isinstance(arg_type, torch.OptionalType): + # If we have a pointer to a variable, strip it off and let + # from handle any internal pointers. + codegen_arg = codegen_arg.removeprefix("&") + + if codegen_arg == "nullptr": + return "torch::stable::detail::from(std::nullopt)" + + var_name = f"tmp_var_{next(tmp_var_number)}" + dispatch_lines.writeline( + f"std::optional {var_name}{{{parse_arg(arg_type.getElementType(), codegen_arg)}}};" + ) + return f"torch::stable::detail::from({var_name})" + + raii_var = self.create_tmp_raii_handle_var_if_needed( + codegen_arg, dispatch_lines + ) + temp_handle = raii_var != codegen_arg + + if isinstance(arg_type, torch.TensorType): + if not temp_handle: + # If the RAII tensor being referenced _isn't_ a temporary, + # scoped to this fallback call, then create a new handle + # referencing it which from can steal. + var_name = f"tmp_var_{next(tmp_var_number)}" + dispatch_lines.writeline(f"AtenTensorHandle {var_name};") + dispatch_lines.writeline( + f"aoti_torch_new_tensor_handle({raii_var}, &{var_name});" + ) + return f"torch::stable::detail::from({var_name})" + # If the RAII tensor _is_ a temporary scoped to this fallback call, + # simply release and steal the handle. + return f"torch::stable::detail::from({raii_var}.release())" + return f"torch::stable::detail::from({codegen_arg})" + + codegen_args = get_args() + ivalue_args = ( + parse_arg(a.type, c) + for a, c in zip(op_overload._schema.arguments, codegen_args) + ) + array_len = max(len(codegen_args), len(output_args)) + dispatch_lines.writeline( + f"std::array dispatch_vars{{{', '.join(ivalue_args)}}};" + ) + dispatch_lines.writeline("AOTI_TORCH_ERROR_CODE_CHECK(") + with dispatch_lines.indent(): + dispatch_lines.writeline( + f'aoti_torch_call_dispatcher("{op_overload._schema.name}", "{op_overload._schema.overload_name}", dispatch_vars.data())' # noqa: B950 + ) + dispatch_lines.writeline(");") + + # assign result(s), ignoring None + for idx, output_arg in enumerate(output_args): + if output_arg is None: + continue + dispatch_lines.writeline( + f"{output_arg} = torch::stable::detail::to(dispatch_vars[{idx}]);" + ) + + dispatch_lines.writeline("}") + self.writelines(dispatch_lines.getvalue().splitlines()) + + def generate_fallback_kernel_with_runtime_lookup_python( + self, + buf_name: str, + python_kernel_name: str, + op_overload: torch._ops.OpOverload, + raw_args: Sequence[Any], + output_args: Sequence[str | None], + raw_outputs: Sequence[ir.Buffer], + ) -> None: + """Generate fallback kernel calls with runtime (non-AOT) dispatch. This can + only be called in cpp_wrapper mode, and assumes that the input is a non-None + OpOverload. + + This function calls into Python to dispatch, which allows it to handle datatypes + that cannot be contained in StableIValue, at the cost of some performance.""" + self.load_custom_op_wrapper() + + num_args = len(raw_args) + py_args_var = f"py_args_{next(self.arg_var_id)}" + # First arg is always the python op name + lines = textwrap.dedent( + f""" + RAIIPyObject {py_args_var}(PyTuple_New({num_args + 1})); + if (!{py_args_var}) {{ + throw std::runtime_error("PyTuple_New {py_args_var} failed"); + }} + PyTuple_SetItem({py_args_var}, 0, PyUnicode_FromString("{python_kernel_name}")); + """ + ) + + for idx, (raw_arg, schema_arg) in enumerate( + zip(raw_args, op_overload._schema.arguments) + ): + lines += self.generate_py_arg( + py_args_var, idx + 1, raw_arg, schema_arg.real_type + ) + + lines += textwrap.dedent( + f""" + // Call the custom op in Python + RAIIPyObject py_{buf_name}(PyObject_CallObject(custom_op_wrapper, {py_args_var})); + if (!py_{buf_name}) {{ + if (PyErr_Occurred()) {{ + return; + }} + throw std::runtime_error("PyObject_CallObject {python_kernel_name} failed"); + }} + """ + ) + + if len(output_args) == 1 and (output := output_args[0]) is not None: + # result is a single tensor + lines += f"{output} = reinterpret_cast(PyCapsule_GetPointer(py_{buf_name}.get(), NULL));\n" + else: + # result is a tuple of tensors + for idx, output_arg in enumerate(output_args): + if output_arg is None: + continue + lines += f"{output_arg} = reinterpret_cast(PyCapsule_GetPointer(PyList_GET_ITEM(py_{buf_name}.get(), {idx}), NULL));\n" # noqa: B950 + + if raw_outputs: + declarations_before_scope = [ + f"RAIIAtenTensorHandle {output_arg};" + for output_arg, raw_output_arg in zip(output_args, raw_outputs) # type: ignore[arg-type] + if output_arg is not None + and not isinstance(raw_output_arg, ir.MutationOutput) + ] + else: + declarations_before_scope = [ + f"RAIIAtenTensorHandle {output_arg};" + for output_arg in output_args # type: ignore[arg-type] + if output_arg is not None + ] + scope_gil_acquire = self.generate_scoped_gil_acquire( + declarations_before_scope, lines + ) + self.writelines(scope_gil_acquire) + + def generate_fallback_kernel_with_runtime_lookup_aot( + self, + op_overload: torch._ops.OpOverload | torch._ops.HigherOrderOperator, + raw_args: Sequence[Any], + output_args: _OUTPUT_ARGS_TYPE, + raw_outputs: Sequence[ir.Buffer], + ) -> None: + ( + tensor_call_args, + int_call_args, + ) = self.generate_extern_kernel_args_decl_if_needed( + op_overload, + raw_args, + output_args, + raw_outputs, + ) + # force both temporary arrays to generate mutable data pointers, since the proxy + # executor signature requires that datatype + int_call_str = self._generate_temporary_array_pointer( + "int64_t", int_call_args, force_mutable=True + ) + tensor_call_str = self._generate_temporary_array_pointer( + "AtenTensorHandle", tensor_call_args, force_mutable=True + ) + + extern_kernel_node_index = len(V.extern_kernel_nodes) - 1 + self.writeline( + f"aoti_torch_proxy_executor_call_function(proxy_executor, " + f"{extern_kernel_node_index}, " + f"{len(int_call_args)}, " + f"{int_call_str}, " + f"{len(tensor_call_args)}, " + f"{tensor_call_str});" + ) + + def generate_reset_kernel_saved_flags(self): + pass + + def generate_save_uncompiled_kernels(self): + pass + + def c_type_for_prim_type(self, val, type_) -> str: + if isinstance(type_, torch.OptionalType): + return f"{self.c_type_for_prim_type(val, type_.getElementType())}*" + elif isinstance(type_, torch.TensorType): + return "AtenTensorHandle" + elif isinstance(type_, (torch.IntType, torch.SymIntType)): + return "int64_t" + elif isinstance( + type_, (torch.BoolType, torch.SymBoolType, torch.EnumType) + ) or repr(type_) in ("Layout", "MemoryFormat", "ScalarType"): + return "int32_t" + elif isinstance(type_, torch.FloatType): + return "double" + elif isinstance(type_, torch.NumberType): + if isinstance(val, bool): + return "int32_t" + elif isinstance(val, (int, float)): + return "double" + elif val is None: + # This could happen when val is an optional value + return "double" + else: + raise AssertionError( + f"Unexpected type in c_type_for_prim_type: {type_=}" + ) + elif isinstance(type_, torch.StringType): + return "const char*" + else: + raise AssertionError(f"Unexpected type in c_type_for_prim_type: {type_=}") + + def val_to_arg_str_for_prim_type(self, val, type_) -> str: + # TODO: not using type_ as the first step of refactoring. Will update this later. + if isinstance(val, bool): + return "1" if val else "0" + elif isinstance(val, int): + # uint64_t is long on Linux, but long long on MacOS and Windows + return f"{val}LL" if sys.platform in ["darwin", "win32"] else f"{val}L" + elif isinstance(val, complex): + return f"c10::complex{{ {self.generate_float_value(val.real)}, {self.generate_float_value(val.imag)} }}" + elif isinstance(val, str): + return f'"{val}"' + elif isinstance( + val, (ir.Buffer, ir.ReinterpretView, ir.StorageBox, ir.TensorBox) + ): + return val.codegen_reference() + elif isinstance(val, torch.device): + return self.codegen_device(val) + elif isinstance(val, torch.dtype): + return self.codegen_dtype(val) + elif isinstance(val, torch.layout): + return self.codegen_layout(val) + elif isinstance(val, torch.memory_format): + return self.codegen_memory_format(val) + elif isinstance(val, float): + return self.generate_float_value(val) + elif isinstance(val, (list, tuple)): + # FIXME: This happens because type_ is not always properly set to torch.ListType + return f"{{{', '.join(self.val_to_arg_str(x, None) for x in val)}}}" + elif isinstance(val, SymTypes): + return cexpr(val.node.expr) + elif isinstance(val, sympy.Expr): + return cexpr(val) + else: + return repr(val) + + def val_to_arg_str(self, val, type_=None) -> str: + if val is None: + # None needs special care. It either represent nullopt or an empty tensor + if type_ is None or isinstance(type_, torch.OptionalType): + if type_ is not None and isinstance( + type_.getElementType(), + ( + torch.DeviceObjType, + torch.ListType, + torch.TupleType, + ), + ): + return "nullptr, 0" + return "nullptr" + + if isinstance(type_, torch.TensorType): + # create an empty tensor, the equivalent of at::Tensor() + var_name = f"var_{next(self.arg_var_id)}" + self.writeline(f"AtenTensorHandle {var_name}_handle;") + self.writeline( + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_new_uninitialized_tensor(&{var_name}_handle));" + ) + self.writeline(f"RAIIAtenTensorHandle {var_name}({var_name}_handle);") + return var_name + + raise AssertionError("Can not map None to a known data type") + + if isinstance(type_, torch.OptionalType): + element_type = type_.getElementType() + arg_str = self.val_to_arg_str(val, element_type) + # Handle optional iterables as a special case. Utilize the + # temporary_reference function to avoid saving them off and increasing + # memory usage. + if isinstance(element_type, (torch.ListType, torch.TupleType)): + main_value, aux = arg_str.rsplit(", ", maxsplit=1) + return f"&temporary_reference({main_value}), {aux}" + + # Handle optional tensors as a special case, as above. + if isinstance(element_type, torch.TensorType): + base_handle = self.val_to_arg_str(val, element_type) + return f"&temporary_reference({base_handle}.get())" + + var_name = f"var_{next(self.arg_var_id)}" + if isinstance(element_type, torch.DeviceObjType): + main_value, aux = arg_str.rsplit(", ", maxsplit=1) + self.writeline(f"auto {var_name} = {main_value};") + return f"&{var_name}, {aux}" + + self.writeline( + f"{self.c_type_for_prim_type(val, element_type)} {var_name} = {arg_str};" + ) + return f"&{var_name}" + + if isinstance(type_, (torch.ListType, torch.TupleType)): + assert isinstance(val, (list, tuple)), ( + f"{val} does not match with arg type {type_}" + ) + element_type = type_.getElementType() + + if len(val) == 0: + # Zero-size array is not supported in the C or C++ standard, so return a + # nullptr. + return "nullptr, 0" + + result = [self.val_to_arg_str(x, element_type) for x in val] + if isinstance(element_type, torch.TensorType): + result = [f"{t}.get()" for t in result] + + c_type = self.c_type_for_prim_type(val[0], element_type) + # see the comment in self._generate_temporary_array_pointer for an + # explanation of why this c_type gets modified + if isinstance(element_type, torch.OptionalType) and not c_type.startswith( + "const" + ): + c_type = f"const {c_type}" + + # need to pass the array length, because we can't use the std::array member + # function + return ( + f"{self._generate_temporary_array_pointer(c_type, result)}, {len(val)}" + ) + + val_is_scalar = isinstance(val, (bool, complex, float, int, *SymTypes)) + if isinstance(type_, torch.TensorType) and val_is_scalar: + val_str = self.val_to_arg_str_for_prim_type(val, None) + return self.codegen_scalar_to_tensor(val_str) + + return self.val_to_arg_str_for_prim_type(val, type_) + + def create_tmp_raii_handle_var_if_needed( + self, handle: str, writer: HasWriteLine | list[str] | None = None + ) -> str: + """If the input handle is an rvalue RAII tensor, creates an lvalue variable for + it in writer. Returns a variable name that can be used to access handle.""" + if not handle.startswith( + ( + "borrow_arrayref_tensor_as_tensor(", + "copy_arrayref_tensor_to_tensor(", + "wrap_with_raii_handle_if_needed(", + "RAIIAtenTensorHandle(", + ) + ): + return handle + + tmp_var_name = f"var_{next(self.arg_var_id)}" + call_str = f"auto {tmp_var_name} = {handle};" + + writer = writer if writer is not None else self + if isinstance(writer, list): + writer.append(call_str) + else: + writer.writeline(call_str) + + return tmp_var_name + + def write_kernel_context_guard_begin( + self, + ): + # Beginning of a kernel context guarded block. + # The block looks like this: + # { + # KernelContextGuard _ctx("{kernel_name}", {stack_trace_str}); + # ... operations... + # } + self.writeline("{") + + def write_kernel_context_guard_end( + self, + ): + # End of a kernel context guarded block. + self.writeline("}") + + def write_kernel_context_guard( + self, + kernel_name: str, + node_schedule: Sequence[BaseSchedulerNode] | ExternKernel, + ): + def aggregate_stack_traces( + node_schedule: Sequence[BaseSchedulerNode] | ExternKernel, + ) -> OrderedSet[str]: + if isinstance(node_schedule, list): + return functools.reduce( + lambda a, b: a | b, + [ + # pyrefly: ignore [missing-attribute] + node.node.get_stack_traces() + for node in node_schedule + if hasattr(node, "node") and node.node + ], + OrderedSet(), + ) + elif isinstance(node_schedule, ExternKernel): + return node_schedule.get_stack_traces() + else: + return OrderedSet() + + stack_trace_str = 'R"(' + stack_traces = aggregate_stack_traces(node_schedule) + + for stack_trace in stack_traces: + for line in stack_trace.split("\n"): + stack_trace_str += f"\n{line}" + stack_trace_str += "\n" + stack_trace_str += ')"' + self.writeline(f'KernelContextGuard _ctx("{kernel_name}", {stack_trace_str});') diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_wrapper_cpu_array_ref.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_wrapper_cpu_array_ref.py new file mode 100644 index 0000000000000000000000000000000000000000..86827fb701290b37706fd524ff733a01a8e918a4 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_wrapper_cpu_array_ref.py @@ -0,0 +1,899 @@ +# mypy: allow-untyped-defs +from collections.abc import Callable, Sequence +from typing import Any + +import sympy + +import torch +import torch._inductor.async_compile # noqa: F401 required to warm up AsyncCompile pools +import torch._ops + +from .. import config, ir +from ..utils import sympy_product +from ..virtualized import V +from .cpp_utils import DTYPE_TO_CPP +from .cpp_wrapper_cpu import CppWrapperCpu +from .wrapper import ( + BufferLike, + EnterSubgraphLine, + ExitSubgraphLine, + MemoryPlanningLine, + MemoryPlanningState, + PythonWrapperCodegen, +) + + +BufferName = str + +# Default thread stack sizes vary by platform: +# - Linux: 8 MB +# - macOS: 512 KB +# - Windows: 1 MB +# Just pick something comfortably smaller than the smallest for now. +MAX_STACK_ALLOCATION_SIZE = 1024 * 100 + + +class CppWrapperCpuArrayRef(CppWrapperCpu): + """ + Generates cpp wrapper for running on CPU and calls cpp kernels + + This class is forked from CppWrapperCpu, with a difference that tensors may be + represented as ArrayRef, see torch/csrc/inductor/aoti_runtime/arrayref_tensor.h + """ + + def __init__(self): + super().__init__() + assert self.device == "cpu", "ArrayRefTensor only supported on CPU!" + self.allow_stack_allocation = config.aot_inductor.allow_stack_allocation + self.stack_allocated_buffers: dict[BufferName, BufferLike] = {} + + @staticmethod + def create( + is_subgraph: bool, + subgraph_name: str | None, + parent_wrapper: PythonWrapperCodegen | None, + partition_signatures: ir.GraphPartitionSignature | None = None, + ): + # TODO - support subgraph codegen by lifting functions. Check the + # comment at CppWrapperCpu `codegen_subgraph` function. + return CppWrapperCpuArrayRef() + + @staticmethod + def get_input_cpp_type(input): + assert config.aot_inductor.use_minimal_arrayref_interface + + if isinstance(input, sympy.Expr): + from ..graph import may_get_constant_buffer_dtype + + dtype = may_get_constant_buffer_dtype(input) + assert dtype is not None, f"Failed to get the dtype of sympy.Expr: {input}" + return DTYPE_TO_CPP[dtype] + return f"ArrayRefTensor<{DTYPE_TO_CPP[input.get_dtype()]}>" + + @staticmethod + def get_device_include_path(device: str) -> str: + assert device == "cpu", "ArrayRef only supported on CPU!" + if V.graph.aot_mode: + return "#include " + return "#include " + + def codegen_input_numel_asserts(self): + for name, buf in V.graph.graph_inputs.items(): + if isinstance(buf, sympy.Expr): + continue + + # comparing strides for 0 size tensor is tricky. Ignore them for now. + if sympy_product(buf.get_size()) == 0: + continue + numel = buf.get_numel() + self.prefix.writeline(f"assert_numel({name}, {numel});") + + def generate_extern_kernel_alloc(self, *args, **kwargs): + # Disable stack allocation for extern kernels. + self.allow_stack_allocation = False + super().generate_extern_kernel_alloc(*args, **kwargs) + + def generate_extern_kernel_out(self, *args, **kwargs): + # Disable stack allocation for extern kernels. + self.allow_stack_allocation = False + super().generate_extern_kernel_out(*args, **kwargs) + + def generate_fallback_kernel(self, node: ir.FallbackKernel) -> None: + # Disable stack allocation for extern kernels. + self.allow_stack_allocation = False + super().generate_fallback_kernel(node) + + def _generate_kernel_call_helper( + self, + kernel_name: str, + call_args, + *, + device=None, + triton=True, + arg_types=None, + raw_keys=None, + raw_args=None, + triton_meta=None, + inductor_meta=None, + graph_name="", + original_fxnode_name=None, + current_stream_idx=None, + ): + """ + Generates kernel call code. + + triton: Defines whether the GPU backend uses Triton for codegen. + Otherwise it uses the CUDA language for codegen. + Only valid when cuda == True. + """ + assert not triton, ( + "CppWrapperCpuArrayRef.generate_kernel_call does not support GPU" + ) + assert arg_types is not None and len(call_args) == len(arg_types), ( + "Mismatch call_args and arg_types in generate_kernel_call" + ) + new_args = [] + for idx, arg in enumerate(call_args): + if "*" in arg_types[idx]: + var_name = f"var_{next(self.arg_var_id)}" + self.writeline(f"auto* {var_name} = get_data_ptr_wrapper({arg});") + new_args.append(f"({arg_types[idx]})({var_name})") + else: + # arg is a scalar + new_args.append(arg) + # debug printer related logic for cpp kernel type. + debug_printer_manager = V.graph.wrapper_code.debug_printer + debug_printer_manager.set_printer_args( + call_args, + kernel_name, + None, + None, + "cpp", + ) + with debug_printer_manager: + self.writeline(self.wrap_kernel_call(kernel_name, new_args)) + + def write_wrapper_decl(self): + inputs_len = len(V.graph.graph_inputs.keys()) + if V.graph.aot_mode: + if ( + config.aot_inductor.use_minimal_arrayref_interface + and not V.graph.is_const_graph + ): + input_cpp_types = ", ".join( + f"{CppWrapperCpuArrayRef.get_input_cpp_type(x)}" + for x in V.graph.graph_inputs.values() + ) + output_arrayref_types = ", ".join( + f"ArrayRefTensor<{DTYPE_TO_CPP[x.get_dtype()]}>" + for x in V.graph.graph_outputs + ) + + self.prefix.splice( + f""" + using AOTInductorModelInputs = std::tuple<{input_cpp_types}>; + using AOTInductorModelOutputs = std::tuple<{output_arrayref_types}>; + """ + ) + + if V.graph.const_module: + self.header.splice(V.graph.const_module.wrapper_code.header) + + assert V.graph.const_wrapper_code is not None + self.prefix.splice(V.graph.const_wrapper_code) + + assert V.graph.const_kernel_code is not None + self.kernel_declarations.splice(V.graph.const_kernel_code) + + if V.graph.is_const_graph: + self.prefix.splice( + """ + void AOTInductorModel::_const_run_impl( + std::vector& output_handles, + DeviceStreamType stream, + AOTIProxyExecutorHandle proxy_executor + ) { + """ + ) + else: + if not config.aot_inductor.use_runtime_constant_folding: + # If we do not split the constant graph, we'll just create + # an empty implementation when wrapping the main module. + self.prefix.splice( + """ + void AOTInductorModel::_const_run_impl( + std::vector& output_handles, + DeviceStreamType stream, + AOTIProxyExecutorHandle proxy_executor + ) {} + + """ + ) + + run_impl_proto = """ + void AOTInductorModel::run_impl( + AtenTensorHandle* + input_handles, // array of input AtenTensorHandle; handles + // are stolen; the array itself is borrowed + AtenTensorHandle* + output_handles, // array for writing output AtenTensorHandle; handles + // will be stolen by the caller; the array itself is + // borrowed + DeviceStreamType stream, + AOTIProxyExecutorHandle proxy_executor + ) { + """ + + self.generate_input_output_runtime_checks() + run_impl_proto += """ + __check_inputs_outputs(input_handles, output_handles); + """ + + if config.aot_inductor.use_minimal_arrayref_interface: + self.prefix.splice( + """ + template <> + AOTInductorModelOutputs AOTInductorModel::run_impl_minimal_arrayref_interface< + AOTInductorModelInputs, AOTInductorModelOutputs>( + const AOTInductorModelInputs& inputs, + DeviceStreamType stream, + AOTIProxyExecutorHandle proxy_executor + ) { + """ + ) + self.suffix.splice(run_impl_proto) + self.suffix.splice( + """ + AOTInductorModelInputs inputs; + convert_handles_to_inputs(input_handles, inputs); + auto outputs = run_impl_minimal_arrayref_interface( + inputs, stream, proxy_executor); + // NOTE: outputs is full of ArrayRef to thread_local storage. If in the future we need this + // interface to perform well for a DSO using the minimal arrayref interface, all we need + // to do is provide ThreadLocalCachedTensor for each one! + convert_outputs_to_handles(outputs, output_handles); + } + """ + ) + + self.suffix.splice( + """ + extern "C" AOTIRuntimeError AOTInductorModelRunMinimalArrayrefInterface( + AOTInductorModelHandle model_handle, + const AOTInductorModelInputs& inputs, + AOTInductorModelOutputs& outputs) { + auto model = reinterpret_cast(model_handle); + CONVERT_EXCEPTION_TO_ERROR_CODE({ + outputs = model->run_impl_minimal_arrayref_interface( + inputs, + (torch::aot_inductor::DeviceStreamType)nullptr, + nullptr); + }) + } + """ + ) + else: + self.prefix.splice(run_impl_proto) + else: + # cpp entry function for JIT with cpp wrapper + self.prefix.splice( + """ + void inductor_entry_impl( + AtenTensorHandle* + input_handles, // array of input AtenTensorHandle; handles + // are stolen; the array itself is borrowed + AtenTensorHandle* + output_handles // array for writing output AtenTensorHandle; handles + // will be stolen by the caller; the array itself is + // borrowed) + ) { + """ + ) + with self.prefix.indent(): + # assign inputs and outputs in both cases so the later codegen can be simplified + if not config.aot_inductor.use_minimal_arrayref_interface: + if not V.graph.is_const_graph: + if V.graph.aot_mode: + num_args = len(V.graph.graph_inputs) + else: + # Weights are promoted in the JIT mode + num_args = len(V.graph.graph_inputs) + len(V.graph.constants) + # release GIL to support multiple instances inference (in different threads of the same process) + self.prefix.splice("py::gil_scoped_release_simple release;") + + self.prefix.splice( + f""" + auto inputs = steal_from_raw_handles_to_raii_handles(input_handles, {num_args}); + """ + ) + + if inputs_len != 0: + for idx, input_key in enumerate(V.graph.graph_inputs.keys()): + if config.aot_inductor.use_minimal_arrayref_interface: + self.prefix.writeline( + f"auto {input_key} = std::get<{idx}>(inputs);" + ) + continue + # unwrap input tensor back to scalar + if isinstance(V.graph.graph_inputs[input_key], sympy.Expr): + from ..graph import may_get_constant_buffer_dtype + + dtype = may_get_constant_buffer_dtype( + V.graph.graph_inputs[input_key] # type: ignore[arg-type] + ) + assert dtype is not None, ( + "Fails to get the dtype of the sympy.Expr" + ) + self.codegen_tensor_item( + dtype, f"inputs[{idx}]", input_key, self.prefix + ) + else: + self.prefix.writeline( + f"auto {input_key} = std::move(inputs[{idx}]);" + ) + + assert all( + isinstance(v, torch.Tensor) for v in list(V.graph.constants.values()) + ), "Expect all constants to be Tensor" + for idx, constants_key in enumerate(V.graph.constants.keys()): + if V.graph.aot_mode: + # Weights are stored in constants_ and owned by RAIIAtenTensorHandle there. + # Don't call std::move here because it will cause constants_ to lose the ownership. + self.prefix.writeline( + f"""auto {constants_key} = constants_->at({idx});""" + ) + else: + # Append constants as inputs to the graph + constants_idx = inputs_len + idx + self.prefix.writeline( + f"auto {constants_key} = std::move(inputs[{constants_idx}]);" + ) + + self.codegen_inputs() + + if V.graph.aot_mode: + if not V.graph.is_const_graph: + if config.aot_inductor.use_minimal_arrayref_interface: + # TODO: input shape checking for regular tensor interface as well? + self.codegen_input_numel_asserts() + else: + self.prefix.writeline("inputs.clear();") + self.prefix.writeline( + "[[maybe_unused]] auto& kernels = static_cast(*this->kernels_.get());" + ) + + def generate_return(self, output_refs: list[str]): + cst_names = V.graph.constants.keys() + arr_iface = ( + not V.graph.is_const_graph + and config.aot_inductor.use_minimal_arrayref_interface + ) # For brevity. + + def use_thread_local_cached_output_tensor(idx, output): + cached_output_name = f"cached_output_{next(self.cached_output_id)}" + cache_type = "Array" if arr_iface else "Tensor" + self.wrapper_call.writeline( + f"thread_local ThreadLocalCachedOutput{cache_type}> " + f"{cached_output_name}({output});" + ) + if arr_iface: + self.wrapper_call.writeline( + f"{cached_output_name}.copy_data_from({output});" + ) + output_entry = f"std::get<{idx}>(output_arrayref_tensors)" + element_type = f"std::decay_t" + self.wrapper_call.writeline( + f"{output_entry} = {cached_output_name}.arrayref_tensor<{element_type}>();" + ) + else: + self.wrapper_call.writeline( + f"{cached_output_name}.copy_data_from({output});" + ) + self.wrapper_call.writeline( + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_new_uninitialized_tensor(&output_handles[{idx}]));" + ) + self.wrapper_call.writeline( + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_assign_tensors({cached_output_name}.tensor(), " + f"output_handles[{idx}]));" + ) + + if arr_iface: + self.wrapper_call.writeline( + "AOTInductorModelOutputs output_arrayref_tensors;" + ) + + output2idx: dict[str, int] = {} + for idx, output in enumerate(output_refs): + if output == "nullptr": + continue + + is_constant_buffer = output in cst_names + output_buffer = V.graph.graph_outputs[idx] + if isinstance(output_buffer, ir.BaseView): + output_storage = output_buffer.unwrap_view() + assert isinstance(output_storage, (ir.BaseView, ir.MutableBox)) + if isinstance(output_storage.data, ir.ConstantBuffer): + is_constant_buffer = True + + if isinstance(output_buffer, ir.ShapeAsConstantBuffer): + # Need to wrap scalar into tensor as the main function returns a vector of tensors + output_tensor = self.codegen_scalar_to_tensor(output) + self.wrapper_call.writeline( + f"output_handles[{idx}] = {output_tensor}.release();" + ) + continue + + output_is_tensor_handle_expr = ( + f"std::is_same_v," + "RAIIAtenTensorHandle> || " + f"std::is_same_v," + "AtenTensorHandle> || " + f"std::is_same_v," + "ConstantHandle>" + ) + self.wrapper_call.writeline( + f"if constexpr ({output_is_tensor_handle_expr}) {{" + ) + with self.wrapper_call.indent(): + if arr_iface: + cached_output_name = f"cached_output_{next(self.cached_output_id)}" + self.wrapper_call.writeline( + f"thread_local RAIIAtenTensorHandle {cached_output_name};" + ) + if is_constant_buffer: + # NOTE(return_constant): In some rare cases where we return + # a constant, we have to return a copy of this constant, + # because (1) constants are not owned by the Model instance + # (2) constants remain the same cross inference runs, + # assuming they are not updated at runtime Basically, we + # cannot release or transfer the ownership of any original + # constant to the user. + self.wrapper_call.writeline( + f"AtenTensorHandle {cached_output_name}_tmp;" + ) + self.wrapper_call.writeline( + f"aoti_torch_clone({output}, &{cached_output_name}_tmp);" + ) + self.wrapper_call.writeline( + f"{cached_output_name} = {cached_output_name}_tmp;" + ) + else: + self.wrapper_call.writeline( + f"{cached_output_name} = {output}.release();" + ) + self.wrapper_call.writeline( + f"convert_handle_to_arrayref_tensor({cached_output_name}, " + f"std::get<{idx}>(output_arrayref_tensors));" + ) + else: + if is_constant_buffer: + # See NOTE(return_constant) above. + self.wrapper_call.writeline( + f"aoti_torch_clone({output}, &output_handles[{idx}]);" + ) + else: + if output in output2idx: + src_idx = output2idx[output] + self.wrapper_call.writeline( + f"output_handles[{idx}] = output_handles[{src_idx}];" + ) + else: + self.wrapper_call.writeline( + f"output_handles[{idx}] = {output}.release();" + ) + self.wrapper_call.writeline("} else {") + with self.wrapper_call.indent(): + use_thread_local_cached_output_tensor(idx, output) + self.wrapper_call.writeline("}") + + if output not in output2idx: + output2idx[output] = idx + if arr_iface: + self.wrapper_call.writeline("return output_arrayref_tensors;") + + def memory_plan(self): + from .memory_planning import MemoryPlanner + + self.lines = MemoryPlanner(self).plan(self.lines) + # TODO: integrate memory planning & stack allocation? + self.allow_stack_allocation = False + + def memory_plan_reuse(self): + out_names = V.graph.get_output_names() + + while ( + self.lines + and isinstance(self.lines[-1], MemoryPlanningLine) + # TODO: this seems legit, NullLine has no node + and self.lines[-1].node.name not in out_names # type: ignore[attr-defined] + ): + # these lines will be pointless + self.lines.pop() + + # codegen allocations in two passes + planning_states = [MemoryPlanningState()] + past_planning_states = [] + for i in range(len(self.lines)): + line = self.lines[i] + if isinstance(line, MemoryPlanningLine): + self.lines[i] = line.plan(planning_states[-1]) + elif isinstance(line, EnterSubgraphLine): + planning_states.append(MemoryPlanningState()) + elif isinstance(line, ExitSubgraphLine): + past_planning_states.append(planning_states.pop()) + past_planning_states.append(planning_states.pop()) + assert len(planning_states) == 0 + + # conservatively use the sum of all allocated buffer sizes + # in potentially nested scopes as the total allocated size + total_allocated_buffer_size = sum( + s.total_allocated_buffer_size for s in past_planning_states + ) + + self.allow_stack_allocation = ( + self.allow_stack_allocation is not False + and config.aot_inductor.allow_stack_allocation + and total_allocated_buffer_size <= MAX_STACK_ALLOCATION_SIZE + ) + + def can_stack_allocate_buffer(self, buffer): + return ( + self.allow_stack_allocation + and buffer.get_device().type == "cpu" + and self.can_prove_buffer_has_static_shape(buffer) + and ir.is_contiguous_strides_for_shape( + buffer.get_stride(), buffer.get_size() + ) + ) + + def make_buffer_free(self, buffer): + return ( + "" + if isinstance(buffer.get_output_spec(), ir.MultiOutputLayout) + or (V.graph.aot_mode and buffer.get_name() in self.stack_allocated_buffers) + or ( + config.aot_inductor.use_minimal_arrayref_interface + and V.graph.aot_mode + and buffer.get_name() in V.graph.graph_inputs + ) + else f"{buffer.get_name()}.reset();" + ) + + def make_buffer_allocation(self, buffer): + return self.make_allocation( + buffer.get_name(), + buffer.get_device(), + buffer.get_dtype(), + buffer.get_size(), + buffer.get_stride(), + buffer if self.can_stack_allocate_buffer(buffer) else None, + buffer.get_is_pinned(), + ) + + def make_allocation( + self, + name, + device, + dtype, + shape, + stride, + buffer_if_can_stack_allocate=None, + is_pinned=False, + ): + orig_stride = stride + device_str = self.codegen_device(device) + dtype_code = self.codegen_dtype(dtype) + size = self.codegen_shape_tuple(shape) + stride = self.codegen_shape_tuple(orig_stride) + size_array_var = self.codegen_int_array_var( + size, + self.wrapper_call.writeline, + known_statically=self.is_statically_known_list_of_ints(shape), + graph=self.get_codegened_graph(), + ) + stride_array_var = self.codegen_int_array_var( + stride, + self.wrapper_call.writeline, + known_statically=self.is_statically_known_list_of_ints(orig_stride), + graph=self.get_codegened_graph(), + ) + device_type, device_id = device_str.split(",") + device_idx = "this->device_idx_" if V.graph.aot_mode else device_id + if buffer_if_can_stack_allocate is not None: + self.stack_allocated_buffers[name] = buffer_if_can_stack_allocate + cpp_type = DTYPE_TO_CPP[dtype] + numel = buffer_if_can_stack_allocate.get_numel() + # Note: we don't zero storage because empty_strided doesn't zero either. + self.wrapper_call.writeline(f"{cpp_type} {name}_storage[{numel}];") + args = [ + f"{name}_storage", + size_array_var, + stride_array_var, + device_type, + device_idx, + ] + return f"ArrayRefTensor<{cpp_type}> {name}({', '.join(args)});" + + args = [ + str(len(shape)), + size_array_var, + stride_array_var, + dtype_code, + device_type, + device_idx, + f"&{name}_handle", + ] + + self.wrapper_call.writeline(f"AtenTensorHandle {name}_handle;") + pinned_str = "_pinned" if is_pinned else "" + self.wrapper_call.writeline( + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_empty_strided{pinned_str}({', '.join(args)}));" + ) + + return f"RAIIAtenTensorHandle {name}({name}_handle);" + + def make_buffer_reuse(self, old: BufferLike, new: BufferLike, delete_old: bool): + assert old.get_dtype() == new.get_dtype() + old_name = old.get_name() + new_name = new.get_name() + del_line = ";" + if old_name not in V.graph.get_output_names() and delete_old: + del_line = f"; {self.make_buffer_free(old)}" + + if old.get_size() == new.get_size() and old.get_stride() == new.get_stride(): + if old_name in self.stack_allocated_buffers: + self.stack_allocated_buffers[new_name] = new + return self.codegen_exact_buffer_reuse(old_name, new_name, del_line) + + reinterpret_view = self.codegen_reinterpret_view( + old, new.get_size(), new.get_stride(), 0, self.wrapper_call.writeline + ) + if reinterpret_view in self.stack_allocated_buffers: + self.stack_allocated_buffers[new_name] = new + # The only way to get into this case is via an exact buffer reuse, since all + # other options result in a new tensor handle. + return self.codegen_exact_buffer_reuse(old_name, new_name, del_line) + return f"{self.declare}{new_name} = {reinterpret_view}{del_line} // reuse" + + def _assert_safe_to_use_borrow_arrayref_tensor_as_tensor(self): + # Borrowing arguments to shim functions is only safe because we know + # that the arguments can't be stack-allocated. Otherwise, to be sure + # we can't return a dangling pointer, we need to either 1) be + # certain that the shim function cannot return an alias of a + # borrowed argument, or 2) be certain that the returned Tensor from + # the shim function cannot escape. + assert self.is_safe_to_use_borrow_arrayref_tensor_as_tensor(), ( + "borrowing arguments to shim functions is unsafe with " + "stack allocation on! (see comment above this assertion)" + ) + + def is_safe_to_use_borrow_arrayref_tensor_as_tensor(self): + return not self.allow_stack_allocation and not self.stack_allocated_buffers + + def generate_c_shim_extern_kernel_call( + self, kernel: str, args: list[str], device: str, **_ + ) -> None: + # In the abi_compatible mode, we call fallback aten ops through a C shim layer + # Setting self.allow_stack_allocation to False because the exchange between + # ArrayRefTensor and at::Tensor is still fragile. + self.allow_stack_allocation = False + + wrapped_args = [] + for arg in args: + # We only really *need* borrow_arrayref_tensor_as_tensor for + # ArrayRefTensors. The code flowing into here uses `0` for nullptr, which + # borrow_arrayref_tensor_as_tensor would blindly coerce to int, so just + # avoid wrapping integers. Name matching is to find tensor is hacky, but + # fixing all the ArrayRefTensor issues is not a priority for now. + if isinstance(arg, str) and arg.startswith( + ("buf", "arg", "wrap_with_raii_handle_if_needed") + ): + self._assert_safe_to_use_borrow_arrayref_tensor_as_tensor() + arg = f"borrow_arrayref_tensor_as_tensor({arg})" + wrapped_args.append(arg) + + super().generate_c_shim_extern_kernel_call( + kernel, wrapped_args, device, debug_args=args + ) + + def generate_scatter_fallback(self, node: ir.ScatterFallback): + # No stack allocation when there is a fallback op + self.allow_stack_allocation = False + super().generate_scatter_fallback(node) + + def _generate_scatter_fallback( + self, + output, + inputs, + cpp_kernel_name, + python_kernel_name, + src_is_tensor, + reduce, + kwargs, + device, + ): + reduce = self._get_scatter_reduce_enum(reduce) + + # call the ABI shim function instead of the ATen one + self.add_device_include(device) + cpp_kernel_name = self.get_c_shim_func_name(cpp_kernel_name, device) + + # TODO: consider remove "_out" and add missing inplace variants to fallback_ops.py + cpp_kernel_name = cpp_kernel_name.replace("__", "_") + "_out" + self._assert_safe_to_use_borrow_arrayref_tensor_as_tensor() + inputs_wrapped = [ + (f"borrow_arrayref_tensor_as_tensor({x})" if isinstance(x, str) else str(x)) + for x in inputs + ] + line = f"{cpp_kernel_name}(borrow_arrayref_tensor_as_tensor({output}), {','.join(inputs_wrapped)}" + + if python_kernel_name.startswith("aten.scatter_reduce"): + line += f", {','.join(kwargs)}" + else: + if src_is_tensor: + if reduce: + line += f", {V.graph.wrapper_code.val_to_arg_str(reduce)}" + else: + assert reduce is None, ( + "Expect reduce to be None for aten.scatter_ with scalar src" + ) + line += ");" + self.writeline(line) + + def generate_index_put_fallback(self, node: ir.IndexPutFallback) -> None: + # No stack allocation when there is a fallback op + self.allow_stack_allocation = False + super().generate_index_put_fallback(node) + + def _generate_index_put_fallback(self, kernel, x, indices, values, accumulate): + self._assert_safe_to_use_borrow_arrayref_tensor_as_tensor() + # TODO: update aoti_torch_index_put_out in ir.py to use autogen out version + # See the comment in codegen_reinterpret_view about why having something like + # RAIIAtenTensorHandle(tmp_tensor_handle_2) in a tmp array can cause the corresponding + # tensor prematurely deallocated, thus the temporary array trick here. + indices_str = self._generate_temporary_array_pointer( + "AtenTensorHandle", + [f"borrow_arrayref_tensor_as_tensor({i})" for i in indices], + ) + args = [ + f"borrow_arrayref_tensor_as_tensor({x})", + indices_str, + str(len(indices)), + f"borrow_arrayref_tensor_as_tensor({values})", + accumulate, + ] + args.insert( + 0, f"borrow_arrayref_tensor_as_tensor({x})" + ) # set x as the output tensor, this fallback mutates x. + self.writeline(self.wrap_kernel_call(kernel, args)) + + def generate_fallback_kernel_with_runtime_lookup( + self, + buf_name: str, + python_kernel_name: str, + get_args: Callable[[], Sequence[str]], + op_overload: torch._ops.OpOverload | torch._ops.HigherOrderOperator, + raw_args: Sequence[Any], + outputs: Sequence[ir.Buffer], + ) -> None: + # No stack allocation when there is a fallback op + self.allow_stack_allocation = False + super().generate_fallback_kernel_with_runtime_lookup( + buf_name, python_kernel_name, get_args, op_overload, raw_args, outputs + ) + + def codegen_device_copy(self, src, dst, non_blocking: bool | str): + # aoti_torch_tensor_copy_ takes AtenTensorHandle as input, + # while stack-allocation results in ArrayRefTensor + # so disable stack allocation here + self.allow_stack_allocation = False + self.writeline( + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_copy_(expensive_copy_to_tensor_if_needed({dst}), {src}, {non_blocking}));" + ) + + def codegen_reinterpret_view( + self, + data, + size, + stride, + offset, + writeline: Callable[..., None], + dtype=None, + ) -> str: + """Returns a newly-created, temporary RAII tensor handle containing the + reinterpreted tensor data. Callers of this function are responsible for saving + the handle if persistent access is needed.""" + dim = str(len(size)) + + def create_reinterpret_call() -> str: + args = [ + f"{data.get_name()}", + dim, + self.codegen_int_array_var( + self.codegen_shape_tuple(size), + writeline, + known_statically=self.is_statically_known_list_of_ints(size), + graph=self.get_codegened_graph(), + ), + self.codegen_int_array_var( + self.codegen_shape_tuple(stride), + writeline, + known_statically=self.is_statically_known_list_of_ints(stride), + graph=self.get_codegened_graph(), + ), + offset, + ] + return f"wrap_with_raii_handle_if_needed(reinterpret_tensor_wrapper({', '.join(args)}))" + + def create_new_tensor_handle() -> tuple[str, list[str]]: + # Calling reset() on ArrayRefTensor does nothing, since the array is + # const-allocated on the stack. Thus, it's safe to return a reference to + # the original array. + if (name := data.get_name()) in self.stack_allocated_buffers: + return name, [] + + tmp_AtenTensorHandle = f"tmp_{name}_{next(self.tmp_tensor_id)}" + tmp_call_strs = [ + f"AtenTensorHandle {tmp_AtenTensorHandle};", + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_new_tensor_handle({data.get_name()}, &{tmp_AtenTensorHandle}));", + ] + return f"RAIIAtenTensorHandle({tmp_AtenTensorHandle})", tmp_call_strs + + if ( + size == data.layout.size + and stride == data.layout.stride + and offset == data.layout.offset + and (dtype is None or dtype == data.dtype) + ): + final_tensor_str, call_strs = create_new_tensor_handle() + for line in call_strs: + writeline(line) + return final_tensor_str + + return super().codegen_reinterpret_view( + data, size, stride, offset, writeline, dtype + ) + + def val_to_arg_str(self, val, type_=None) -> str: + if ( + val is not None + and isinstance(type_, torch.OptionalType) + and isinstance(type_.getElementType(), torch.TensorType) + ): + # Handle optional tensors as a special case, as in the parent class. + base_handle = self.val_to_arg_str(val, torch.TensorType) + if config.aot_inductor.use_minimal_arrayref_interface: + if self.is_safe_to_use_borrow_arrayref_tensor_as_tensor(): + base_handle = f"borrow_arrayref_tensor_as_tensor({base_handle})" + else: + base_handle = f"copy_arrayref_tensor_to_tensor({base_handle})" + return f"&temporary_reference({base_handle}.get())" + + return super().val_to_arg_str(val, type_) + + def codegen_tensor_item( + self, dtype: torch.dtype, tensor: str, scalar: str, indented_buffer=None + ): + dtype_str = str(dtype).split(".")[-1] + writer = indented_buffer or self + + if dtype == torch.float16 or dtype == torch.bfloat16: + scalar_tmp = f"{scalar}_tmp" + writer.writeline(f"{DTYPE_TO_CPP[dtype]} {scalar_tmp};") + + # We know that item_ doesn't alias the input, so borrowing should be safe. + tensor = f"borrow_arrayref_tensor_as_tensor({tensor})" + + writer.writeline( + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_item_{dtype_str}({tensor}, &{scalar_tmp}));" + ) + writer.writeline(f"float {scalar} = float({scalar_tmp});") + else: + writer.writeline(f"{DTYPE_TO_CPP[dtype]} {scalar};") + + # We know that item_ doesn't alias the input, so borrowing should be safe. + tensor = f"borrow_arrayref_tensor_as_tensor({tensor})" + + writer.writeline( + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_item_{dtype_str}({tensor}, &{scalar}));" + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_wrapper_gpu.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_wrapper_gpu.py new file mode 100644 index 0000000000000000000000000000000000000000..94a6d2ca2d4c907d48ec32a459e794bf9f85f773 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_wrapper_gpu.py @@ -0,0 +1,1326 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import dataclasses +import re +import sys +from itertools import count, zip_longest +from typing import Any +from typing_extensions import Self + +import sympy + +import torch +from torch import dtype as torch_dtype +from torch._inductor.codecache import get_cpp_wrapper_cubin_path_name +from torch._inductor.runtime.runtime_utils import dynamo_timed +from torch.utils._ordered_set import OrderedSet + +from .. import config +from ..codecache import CudaKernelParamCache +from ..ir import ( + GraphPartitionSignature, + TensorBox, + TMADescriptorExperimental, + TMADescriptorStable, +) +from ..utils import ( + cache_on_self, + get_gpu_type, + GPU_ALIGN_BYTES, + IndentedBuffer, + XPU_KERNEL_FORMAT, +) +from ..virtualized import V +from .aoti_hipify_utils import maybe_hipify_code_wrapper +from .common import get_device_op_overrides, TritonScratchWorkspace +from .cpp_utils import cexpr +from .cpp_wrapper_cpu import CppWrapperCpu +from .multi_kernel import MultiKernelCall +from .triton_utils import should_unwrap_unspec_arg +from .wrapper import PythonWrapperCodegen, SymbolicCallArg + + +_cpp_string_literal_escapes = { + "\\": "\\\\", + '"': '\\"', + "\n": "\\n", + "\t": "\\t", + "\r": "\\r", +} +_cpp_string_literal_pattern = re.compile(r'["\\\n\t\r]') + + +def cpp_string_literal(s: str) -> str: + escaped = _cpp_string_literal_pattern.sub( + lambda match: _cpp_string_literal_escapes[match.group(0)], s + ) + return f'"{escaped}"' + + +TRITON_SIGNATURE_TO_CPP = { + "i32": "int32_t", + "i64": "int64_t", + "fp32": "float", + "fp64": "double", +} + + +def signature_is_tma_desc(sig: str | None) -> bool: + """Check if a Triton signature represents a TMA descriptor.""" + if not sig: + return False + if sig == "nvTmaDesc": + return True + if sig.startswith("tensordesc<"): + return True + return False + + +def _unpack_tma_descriptor_args(var_name: str, sig_type: str) -> list[str]: + """Unpack a StableTMADescriptor into kernel launch args. + + Given a variable name holding a StableTMADescriptor and its tensordesc<...> + signature, returns the list of pointer args: &var.m, &var.block_shape[i]..., + &var.strides[i]... + """ + match = re.match(r"tensordesc<[^[]*\[([^\]]*)\]", sig_type) + assert match is not None, f"Cannot parse tensordesc signature: {sig_type}" + ndim = match.group(1).count(",") + 1 + result = [f"&{var_name}.m"] + for i in range(ndim): + result.append(f"&{var_name}.block_shape[{i}]") + for i in range(ndim): + result.append(f"&{var_name}.strides[{i}]") + return result + + +@dataclasses.dataclass +class DeferredTritonCallWrapper: + """ + When using cpp wrapper, GPU kernel load and launch needs to wait for Triton kernels + to be tuned and stored as cubin files, so use a deferred generating the final wrapper around + the triton kernel until right before the prefix is written. + """ + + wrapper_name: str + kernel_name: str + kernel_name_to_body: dict[str, str] + arg_types: list[Any] + triton_meta: dict[str, Any] | None = None + inductor_meta: dict[str, Any] | None = None + tma_tensor_args: dict[str, str] = dataclasses.field(default_factory=dict) + + @cache_on_self + def _get_tma_args(self) -> dict[str, str]: + """Get mapping of TMA descriptor arg names to their signature types.""" + triton_meta = self.triton_meta or {} + signature = triton_meta.get("signature", {}) + for name, sig_type in signature.items(): + if sig_type == "nvTmaDesc": + raise RuntimeError( + f"nvTmaDesc (experimental TMA API) is not supported in lazy compile " + f"for arg '{name}'. Use the stable tensordesc API instead." + ) + return { + name: sig_type + for name, sig_type in signature.items() + if isinstance(sig_type, str) and sig_type.startswith("tensordesc<") + } + + def _get_cpp_param_type( + self, name: str, arg_type: Any, signature: dict[str, str] | None = None + ) -> str: + """Get the C++ parameter declaration for a given arg type.""" + if isinstance(arg_type, (torch_dtype, UnwrapUnspecArg)): + # TMA descriptors need non-const references since their fields + # are passed as void* pointers to kernel launch args + if signature and signature_is_tma_desc(signature.get(name)): + return f"{name}_type_& {name}" + return f"const {name}_type_& {name}" + elif issubclass(arg_type, (SymbolicCallArg, sympy.Expr, int)): + return f"int64_t {name}" + elif arg_type is float: + return f"float {name}" + elif arg_type is bool: + return f"bool {name}" + else: + raise ValueError(f"Unexpected arg type {arg_type}") + + def _write_wrapper_signature( + self, + prefix: IndentedBuffer, + wrapper: CppWrapperGpu, + arg_names: list[str], + arg_types: list[Any] | None = None, + signature: dict[str, str] | None = None, + ) -> None: + """Write the wrapper function signature including template and parameters.""" + if arg_types is None: + arg_types = self.arg_types + + # Generate template types for tensor arguments + template_types = [ + f"typename {name}_type_" + for name, arg_type in zip(arg_names, arg_types) + if isinstance(arg_type, (torch_dtype, UnwrapUnspecArg)) + ] + if V.graph.aot_mode: + template_types.append("typename kernels_type_") + + if template_types: + prefix.writeline(f"template <{', '.join(template_types)}>") + + # Build parameter list + param_lines = [ + self._get_cpp_param_type(name, arg_type, signature) + for name, arg_type in zip(arg_names, arg_types) + ] + param_lines.append("int32_t device_idx_") + param_lines.append( + maybe_hipify_code_wrapper( + f"{wrapper.device_codegen.cpp_stream_type()} stream_" + ) + ) + if V.graph.aot_mode: + param_lines.append("kernels_type_& kernels_") + param_lines.append( + "const std::optional& cubin_dir_ = std::nullopt" + ) + + # Write function signature + prefix.writeline(f"static __attribute__((noinline)) void {self.wrapper_name}(") + with prefix.indent(): + for i, param in enumerate(param_lines): + comma = "," if i < len(param_lines) - 1 else "" + prefix.writeline(f"{param}{comma}") + prefix.writeline("){") + + def generate(self, wrapper: CppWrapperGpu): + """ + Generate the GPU kernel definition, as well as load and launch code. + """ + prefix = wrapper.prefix + if self.kernel_name.startswith("multi_kernel_"): + # MultiKernel will select one kernel after running the autotune block + self.kernel_name = MultiKernelCall.lookup_choice(self.kernel_name) + + # Defer compilation to runtime if autotune_at_compile_time is False (JIT only) + if not V.graph.aot_mode and config.triton.autotune_at_compile_time is False: + return self.generate_lazy(wrapper) + + params = CudaKernelParamCache.get(self.kernel_name) + assert params, f"CudaKernelParamCache not populated for {self.kernel_name}" + def_args = params["def_args"] + arg_types = self.arg_types + inductor_meta = params["inductor_meta"] + + if "extra_launcher_args" in inductor_meta and len(def_args) > len(arg_types): + # extra_launcher_args should already be in def_args + assert len(def_args) == len(arg_types) - len( + inductor_meta["extra_launcher_args"] + ) + arg_types = arg_types + [SymbolicCallArg] * len( + inductor_meta["extra_launcher_args"] + ) + + if not V.graph.aot_mode: + prefix.writeline( + maybe_hipify_code_wrapper( + f"static {wrapper.device_codegen.cpp_kernel_type()} {self.kernel_name} = nullptr;" + ) + ) + kernel_var_name = self.kernel_name + else: + kernel_var_name = f"kernels_.{self.kernel_name}" + + # Write wrapper function signature + self._write_wrapper_signature(prefix, wrapper, def_args, arg_types) + + with prefix.indent(): + if V.graph.aot_mode: + # Emit the original Triton kernel for debugging purposes + prefix.writeline("/*") + prefix.splice(self.kernel_name_to_body[self.kernel_name]) + prefix.writeline("*/") + self.generate_grid(prefix, inductor_meta, params) + self.generate_load_kernel(prefix, kernel_var_name, params) + self.generate_launch_kernel(prefix, wrapper, kernel_var_name, params) + prefix.writeline("}") + + if not config.aot_inductor.embed_kernel_binary: + # Ensure the cubin file is included in the package + V.graph.wrapper_code.additional_files.append( + params[get_cpp_wrapper_cubin_path_name()] + ) + + def _resolve_lazy_arg_names(self) -> tuple[list[str], list[str]]: + """Compute wrapper and kernel arg names from triton_meta signature. + + Returns (wrapper_arg_names, kernel_arg_names) where: + - wrapper_arg_names: params accepted by the C++ wrapper function + - kernel_arg_names: params passed to the GPU kernel launch (non-constexpr only) + """ + assert self.triton_meta is not None, ( + f"triton_meta is required for lazy compile of {self.kernel_name}" + ) + signature = self.triton_meta.get("signature", {}) + inductor_meta = self.inductor_meta or {} + extra_launcher_args_count = len(inductor_meta.get("extra_launcher_args", [])) + tma_tensor_args = self.tma_tensor_args + num_tma_tensor_args = len(tma_tensor_args) + + internal_config_suffixes = ("BLOCK", "RSPLIT", "RSPLIT_SIZE") + # Declared constexpr params (tl.constexpr in kernel signature) are excluded + # from arg_types for user-defined kernels, while value-based constexpr params + # (e.g. numel=1, arg=None) are still in arg_types. + declared_constexpr_names = OrderedSet( + inductor_meta.get("declared_constexpr_names", []) + ) + wrapper_arg_names = [] + kernel_arg_names = [] + for name, sig_type in signature.items(): + if name.endswith(internal_config_suffixes): + continue + if sig_type != "constexpr": + kernel_arg_names.append(name) + if name not in declared_constexpr_names: + wrapper_arg_names.append(name) + + num_wrapper_args = ( + len(self.arg_types) - extra_launcher_args_count - num_tma_tensor_args + ) + if num_wrapper_args != len(wrapper_arg_names): + raise AssertionError( + f"Mismatch between ({num_wrapper_args}) arg_types and " + f"{len(wrapper_arg_names)} wrapper_arg_names for {self.kernel_name}." + ) + + # Append grid args: passed to wrapper. Kernel args will handle grids separately. + for i in range(extra_launcher_args_count): + wrapper_arg_names.append(f"_grid_{i}") + + # Add TMA tensor args after grid args + if tma_tensor_args: + sig_tma_keys = list(self._get_tma_args().keys()) + assert list(tma_tensor_args.keys()) == sig_tma_keys, ( + f"TMA tensor args order mismatch for {self.kernel_name}: " + f"{list(tma_tensor_args.keys())} vs signature order {sig_tma_keys}" + ) + for desc_name in tma_tensor_args: + wrapper_arg_names.append(f"_tma_tensor_{desc_name}") + + return wrapper_arg_names, kernel_arg_names + + def _generate_lazy_grid(self, prefix: IndentedBuffer) -> None: + """Generate grid computation code for lazy-compiled kernels.""" + kernel_name = self.kernel_name + grid_type = self.inductor_meta.get("grid_type") if self.inductor_meta else None + + # For PrecomputedGrid, generate switch statement on config_index + if grid_type == "PrecomputedGrid": + assert self.inductor_meta is not None + precomputed_grids = self.inductor_meta.get("precomputed_grids", []) + extra_launcher_args = self.inductor_meta.get("extra_launcher_args", []) + + switch_cases = [] + for idx, entry in enumerate(precomputed_grids): + cpp_grids = list(entry.get("cpp", ["1L", "1L", "1L"])) + # Replace internal arg names with C++ parameter names + # e.g., _launcher_s0 -> _grid_0 + for i, arg_name in enumerate(extra_launcher_args): + cpp_grids = [g.replace(arg_name, f"_grid_{i}") for g in cpp_grids] + g0 = cpp_grids[0] + g1 = cpp_grids[1] if len(cpp_grids) > 1 else "1" + g2 = cpp_grids[2] if len(cpp_grids) > 2 else "1" + switch_cases.append( + f"case {idx}: grid_0 = {g0}; grid_1 = {g1}; grid_2 = {g2}; break;" + ) + switch_cases.append("default: grid_0 = 1; grid_1 = 1; grid_2 = 1; break;") + switch_body = "\n ".join(switch_cases) + + prefix.splice( + f"""\ + uint32_t grid_0, grid_1, grid_2; + switch ({kernel_name}_result.config_index) {{ + {switch_body} + }} + if (grid_0 == 0) return; + """ + ) + else: + from ..runtime.triton_heuristics import GridExpr + + grid = GridExpr.from_meta_lazy(self.inductor_meta, kernel_name) + for line in grid.prefix: + prefix.writeline(line) + + prefix.splice( + f"""\ + uint32_t grid_0 = {grid.x_grid}; + uint32_t grid_1 = {grid.y_grid}; + uint32_t grid_2 = {grid.z_grid}; + if (grid_0 == 0) return; + """ + ) + + def _generate_lazy_tma_args( + self, + prefix: IndentedBuffer, + call_args_str: str, + kernel_arg_names: list[str], + tma_arg_names: OrderedSet[str], + signature: dict[str, str], + ) -> str: + """Unpack TMA descriptor args into kernel launch args.""" + for arg_name in kernel_arg_names: + if arg_name in tma_arg_names: + tma_parts = _unpack_tma_descriptor_args(arg_name, signature[arg_name]) + tma_str = ", ".join(tma_parts) + call_args_str = ( + f"{call_args_str}, {tma_str}" if call_args_str else tma_str + ) + return call_args_str + + def _generate_lazy_scratch( + self, + prefix: IndentedBuffer, + wrapper: CppWrapperGpu, + call_args_str: str, + ) -> str: + """Generate scratch space allocations with runtime-known sizes.""" + kernel_name = self.kernel_name + dtype_str = wrapper.codegen_dtype(torch.uint8) + device_type, _ = wrapper.codegen_device(torch.device(get_gpu_type())).split( + ", " + ) + device_ptr_type = wrapper.device_codegen.cpp_device_ptr() + for scratch_name in ("global_scratch", "profile_scratch"): + size_expr = f"{kernel_name}_result.{scratch_name}" + var = f"{scratch_name}_ptr" + prefix.splice( + maybe_hipify_code_wrapper( + f"""\ + {device_ptr_type} {var} = 0; + RAIIAtenTensorHandle {var}_tensor; + if ({size_expr} > 0) {{ + int64_t {var}_size[] = {{{size_expr}}}; + int64_t {var}_stride[] = {{1}}; + AtenTensorHandle {var}_handle; + AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_empty_strided( + 1, {var}_size, {var}_stride, {dtype_str}, + {device_type}, device_idx_, &{var}_handle)); + {var}_tensor = RAIIAtenTensorHandle({var}_handle); + {var} = reinterpret_cast<{device_ptr_type}>({var}_tensor.data_ptr()); + }} + """ + ) + ) + call_args_str += f", &{var}" + return call_args_str + + def _generate_lazy_launch( + self, + prefix: IndentedBuffer, + wrapper: CppWrapperGpu, + wrapper_arg_names: list[str], + kernel_arg_names: list[str], + ) -> None: + """Generate kernel launch code for lazy-compiled kernels.""" + kernel_name = self.kernel_name + signature = (self.triton_meta or {}).get("signature", {}) + tma_tensor_args = self.tma_tensor_args + num_tma_tensor_args = len(tma_tensor_args) + + # wrapper_arg_names may include grid and TMA tensor args at the end; + # only the leading portion maps 1:1 to kernel signature params. + num_signature_args = len(wrapper_arg_names) - num_tma_tensor_args + inductor_meta = self.inductor_meta or {} + num_signature_args -= len(inductor_meta.get("extra_launcher_args", [])) + + arg_type_lookup = dict( + zip(wrapper_arg_names, self.arg_types[:num_signature_args]) + ) + + # Identify TMA args — they are already passed as StableTMADescriptor params, + # so we just unpack them directly (no need to reconstruct from tensors). + tma_arg_names = OrderedSet(self._get_tma_args().keys()) + + # Non-TMA args go through generate_args_decl + non_tma_arg_names = [n for n in kernel_arg_names if n not in tma_arg_names] + non_tma_arg_types = [ + arg_type_lookup[n] for n in non_tma_arg_names if n in arg_type_lookup + ] + non_tma_arg_sigs = [signature.get(n) for n in non_tma_arg_names] + + call_args_str = wrapper.generate_args_decl( + prefix, + non_tma_arg_names, + non_tma_arg_types, + non_tma_arg_sigs, + ) + + call_args_str = self._generate_lazy_tma_args( + prefix, call_args_str, kernel_arg_names, tma_arg_names, signature + ) + call_args_str = self._generate_lazy_scratch(prefix, wrapper, call_args_str) + + launch_args = ( + f"{kernel_name}, grid_0, grid_1, grid_2," + f" {kernel_name}_result.num_warps," + f" {kernel_name}_result.shared_mem," + f" kernel_args_, stream_" + ) + + prefix.splice( + f"""\ + void* kernel_args_[] = {{{call_args_str}}}; + launchKernel({launch_args}); + """ + ) + + def generate_lazy(self, wrapper: CppWrapperGpu): + """ + Generate C++ code that embeds Triton source and compiles it at runtime. + """ + prefix = wrapper.prefix + kernel_name = self.kernel_name + # Track kernel names for parallel initialization + wrapper._lazy_kernel_names.append(kernel_name) + + # Include TMA helpers if any args use TMA descriptors + tma_signature_types = self._get_tma_args() + if tma_signature_types: + wrapper.write_tma_descriptor_helpers_once() + + kernel_var_decl = maybe_hipify_code_wrapper( + f"static {wrapper.device_codegen.cpp_kernel_type()} {kernel_name} = nullptr;" + ) + prefix.writeline(kernel_var_decl) + # Use delimited raw string to handle )" in kernel source + kernel_source_str = self.kernel_name_to_body.get(kernel_name, "") + kernel_body = f'R"TRITON(\n{kernel_source_str}\n)TRITON"' + prefix.writeline(f"static const char* {kernel_name}_source = {kernel_body};") + prefix.writeline(f"static LazyKernelCompileResult {kernel_name}_result;") + + wrapper_arg_names, kernel_arg_names = self._resolve_lazy_arg_names() + signature = (self.triton_meta or {}).get("signature", {}) + self._write_wrapper_signature( + prefix, wrapper, wrapper_arg_names, self.arg_types, signature + ) + + # Build autotune args - for TMA, pass tensors instead of descriptors. + # Only iterate over signature params and grid args, not the trailing + # TMA tensor params (those are only in the C++ wrapper signature). + tma_tensor_args = self.tma_tensor_args + num_autotune_args = len(wrapper_arg_names) - len(tma_tensor_args) + autotune_arg_list = [] + # Track which args need scalar extraction for the autotune call. + # UnwrapUnspecArg args are 0-dim tensors in C++ that Triton expects + # as Python scalars; we use codegen_tensor_item to extract them. + scalar_extractions: list[tuple[str, str, torch_dtype]] = [] + for idx, name in enumerate(wrapper_arg_names[:num_autotune_args]): + if name in tma_signature_types: + autotune_arg_list.append(f"_tma_tensor_{name}") + elif isinstance(self.arg_types[idx], UnwrapUnspecArg): + scalar_var = f"_autotune_scalar_{name}" + scalar_extractions.append((name, scalar_var, self.arg_types[idx].dtype)) + autotune_arg_list.append(scalar_var) + else: + autotune_arg_list.append(name) + autotune_args = ", ".join(autotune_arg_list) + # Lazy compile with autotuning on first invocation + with prefix.indent(): + prefix.writeline(f"if ({kernel_name} == nullptr) {{") + with prefix.indent(): + for tensor_name, scalar_var, dtype in scalar_extractions: + wrapper.codegen_tensor_item( + dtype, tensor_name, scalar_var, indented_buffer=prefix + ) + prefix.splice( + f"""\ + {kernel_name}_result = runTritonKernelWithAutotune( + _module_pending_kernels, "{kernel_name}", stream_, {autotune_args}); + + {kernel_name} = loadKernel( + {kernel_name}_result.cubin_path, + {kernel_name}_result.mangled_name, + {kernel_name}_result.shared_mem); + + // First invocation already ran the kernel, so return early + return; + """ + ) + prefix.writeline("}") + + self._generate_lazy_grid(prefix) + self._generate_lazy_launch( + prefix, + wrapper, + wrapper_arg_names, + kernel_arg_names, + ) + prefix.writeline("}") + + def generate_grid( + self, + prefix: IndentedBuffer, + inductor_meta: dict[str, Any], + params: dict[str, Any], + ): + from ..runtime.triton_heuristics import GridExpr + + grid = GridExpr.from_meta(inductor_meta, params["config"], mode="cpp") + for line in grid.prefix: + prefix.writeline(line) + prefix.splice( + f"""\ + uint32_t grid_0 = {grid.x_grid}; + uint32_t grid_1 = {grid.y_grid}; + uint32_t grid_2 = {grid.z_grid}; + """ + ) + prefix.writeline("if (grid_0 == 0 || grid_1 == 0 || grid_2 == 0) return;") + + def generate_load_kernel(self, prefix, kernel_var_name, params): + prefix.writeline(f"if ({kernel_var_name} == nullptr) {{") + with prefix.indent(): + embed_kernel_args = [f"__{params['inductor_meta']['kernel_name']}_start"] + if torch.xpu.is_available(): + # XPU needs the end address of the kernel to calculate the size of the kernel binary. + embed_kernel_args.append( + f"__{params['inductor_meta']['kernel_name']}_end" + ) + + if V.graph.aot_mode and config.aot_inductor.embed_kernel_binary: + load_kernel_args = [ + *embed_kernel_args, + cpp_string_literal(params["mangled_name"]), + str(params["shared_mem"]), + ] + if torch.xpu.is_available(): + is_spv = "true" if XPU_KERNEL_FORMAT == "spv" else "false" + if config.aot_inductor.emit_multi_arch_kernel: + is_spv = "true" + load_kernel_args.append(is_spv) + else: + load_kernel_args = [ + cpp_string_literal(params[get_cpp_wrapper_cubin_path_name()]), + cpp_string_literal(params["mangled_name"]), + str(params["shared_mem"]), + "cubin_dir_", + ] + + prefix.writeline( + f"{kernel_var_name} = loadKernel({', '.join(load_kernel_args)}); " + ) + prefix.writeline("}") + + def generate_launch_kernel(self, prefix, wrapper, kernel_var_name, params): + """ + Generate the GPU kernel launching code. + This is where all the call args being sorted out and generated. + If enable_kernel_profile is enabled, all args related information would be packed in this function. + """ + triton_meta = params["triton_meta"] + assert len(self.arg_types) == len(params["def_args"]), ( + self.arg_types, + params["def_args"], + ) + arg_type_lookup = dict(zip(params["def_args"], self.arg_types)) + # difference between Python and C++ wrapper: C++ wrapper strips out equal_to_1 constants + call_args = [ + name for name in params["call_args"] if name not in triton_meta["constants"] + ] + arg_types = [arg_type_lookup[name] for name in call_args] + arg_signatures = [triton_meta["signature"][name] for name in call_args] + num_ctas = params.get("config", {}).get("num_ctas", 1) + scratch_spaces = { + name: params[name] * num_ctas + for name in ["global_scratch", "profile_scratch"] + if params.get(name, None) is not None + } + call_args_str = wrapper.generate_args_decl( + prefix, + call_args, + arg_types, + arg_signatures, + scratch_spaces=scratch_spaces, + ) + prefix.writeline(f"void* kernel_args_[] = {{{call_args_str}}};") + launch_kernel_args = [ + kernel_var_name, + "grid_0", + "grid_1", + "grid_2", + str(params["num_warps"]), + str(params["shared_mem"]), + "kernel_args_", + "stream_", + ] + + enable_kernel_profile = config.cpp.enable_kernel_profile and sys.platform in [ + "linux", + "win32", + ] + if enable_kernel_profile: + normalized_kernel_name = re.sub(r"[^a-zA-Z0-9_]", "_", f"{kernel_var_name}") + prefix.writeline("{") + with prefix.indent(): + prefix.writelines( + [ + f"std::unordered_map kwargs_{normalized_kernel_name};", + "", + ] + ) + # Add launch args info + record_launch_kernel_args = [ + ("grid_0", "grid_0"), + ("grid_1", "grid_1"), + ("grid_2", "grid_2"), + ("num_warps", str(params["num_warps"])), + ("shared_mem", str(params["shared_mem"])), + ] + for k, v in record_launch_kernel_args: + arg_name = f"{normalized_kernel_name}_{k}" + prefix.writelines( + [ + f"// Create c10::IValue for {k}", + f"C10IValueHandle tmp_{arg_name};", + f"aoti_torch_int64_to_ivalue({v}, &tmp_{arg_name});", + f"RAIIC10IValueHandle RAII_{arg_name}(tmp_{arg_name});", + f'kwargs_{normalized_kernel_name}.emplace("{k}", RAII_{arg_name});', + ] + ) + + # Add input info (This copies the logic from args_decl) + curr_arg_id = -1 + total_args = [] + ordered_argsname = [] + + def write_dummy_scalar_ivalue(arg_name): + # We only care about the shape, therefore we create a dummy scalar here. + prefix.writelines( + [ + f"// Create c10::IValue for arg_{curr_arg_id}", + f"C10IValueHandle tmp_{arg_name};", + f"aoti_torch_int64_to_ivalue(0, &tmp_{arg_name});", + f"RAIIC10IValueHandle RAII_{arg_name}(tmp_{arg_name});", + ] + ) + # pyrefly: ignore [bad-argument-type] + total_args.append(f"tmp_{arg_name}") + + def process_args_for_input_shape(arg, arg_type, arg_signature=None): + nonlocal curr_arg_id + curr_arg_id += 1 + arg_name = f"{normalized_kernel_name}_arg_{curr_arg_id}" + # ignore tma descriptors, as host-side TMA descriptors need + # to be passed to the compiled Triton kernel by value + if isinstance( + arg_type, UnwrapUnspecArg + ) and not signature_is_tma_desc(arg_signature): + write_dummy_scalar_ivalue(arg_name) + elif isinstance( + arg_type, torch_dtype + ) and not signature_is_tma_desc(arg_signature): + # This is an at::Tensor. + prefix.writelines( + [ + f"// Create c10::IValue for arg_{curr_arg_id}", + f"C10IValueHandle tmp_{arg_name};", + f"aoti_torch_tensor_to_ivalue({arg}, &tmp_{arg_name});", + f"RAIIC10IValueHandle RAII_{arg_name}(tmp_{arg_name});", + ] + ) + # pyrefly: ignore [bad-argument-type] + total_args.append(f"tmp_{arg_name}") + elif ( + isinstance(arg_type, type(SymbolicCallArg)) + and arg_signature is not None + and arg_signature in TRITON_SIGNATURE_TO_CPP + ) or arg_type in (sympy.Integer, int, sympy.Float, float): + write_dummy_scalar_ivalue(arg_name) + elif arg_signature and arg_signature.startswith("tensordesc<"): + # Skip tma related args + pass + else: + write_dummy_scalar_ivalue(arg_name) + + # Add input name and shape information + for arg, arg_type, arg_signature in zip_longest( + call_args, arg_types, arg_signatures + ): + # pyrefly: ignore [bad-argument-type] + ordered_argsname.append(f'"{arg}"') + process_args_for_input_shape(arg, arg_type, arg_signature) + + # Add input name into kwargs + name_var = f"{normalized_kernel_name}_input_names" + prefix.writelines( + [ + "// Create c10::IValue for input names", + f"C10IValueHandle tmp_{name_var};", + f"std::vector {name_var}({{{', '.join(ordered_argsname)}}});", + f"aoti_torch_strlist_to_ivalue({name_var}.data(), {len(ordered_argsname)}, &tmp_{name_var});", + f"RAIIC10IValueHandle RAII_{name_var}(tmp_{name_var});", + f'kwargs_{normalized_kernel_name}.emplace("Input Args", RAII_{name_var});', + ] + ) + + inputs_info_ = f"{normalized_kernel_name}_inputs_info_" + # We pass in the non-RAII handles, since C10 doesn't automatically free them. + # The RAII will make sure they get freed when they are out of scope. + tmp_args = ",".join(total_args) + prefix.writelines( + [ + "// Aggregate all c10::IValue for inputs", + f"std::vector {inputs_info_}({{{tmp_args}}});", + ] + ) + + # Start recording Function + prefix.writelines( + [ + "", + ( + "torch::aot_inductor::RAIIAtenRecordFunctionHandle " + f"record_{normalized_kernel_name}_" + f'("{kernel_var_name}", ' + f"reinterpret_cast(&kwargs_{normalized_kernel_name}), " + f"{inputs_info_});" + ), + "", + f"launchKernel({', '.join(launch_kernel_args)});", + ] + ) + prefix.writeline("}") + else: + prefix.writeline(f"launchKernel({', '.join(launch_kernel_args)});") + + +class CppWrapperGpu(CppWrapperCpu): + """ + Generates cpp wrapper for running on GPU and calls CUDA kernels + """ + + def __init__(self) -> None: + self.device = get_gpu_type() + self.device_codegen = get_device_op_overrides(self.device) + super().__init__() + self.grid_id = count() + self._kernel_name_to_body: dict[str, str] = {} + self._triton_call_wrappers: dict[str, DeferredTritonCallWrapper] = {} + self.autotune_input_prefix = "_REAL_AUTOTUNE_INPUT" + self._lazy_kernel_names: list[str] = [] + + @staticmethod + def create( + is_subgraph: bool, + subgraph_name: str | None, + parent_wrapper: PythonWrapperCodegen | None, + partition_signatures: GraphPartitionSignature | None = None, + ): + # TODO - support subgraph codegen by lifting functions. Check the + # comment at CppWrapperCpu `codegen_subgraph` function. + return CppWrapperGpu() + + def write_header(self): + if V.graph.is_const_graph: + # We do not write header for constant graph, it will be written by main module. + return + + super().write_header() + self.header.splice( + maybe_hipify_code_wrapper(self.device_codegen.kernel_driver()) + ) + + @cache_on_self + def write_tma_descriptor_helpers_once(self): + self.header.splice(self.device_codegen.tma_descriptor_helpers()) + + def write_get_raw_stream(self, device_idx: int, graph_name: str) -> str: + name = f"stream{device_idx}" + self.writeline( + maybe_hipify_code_wrapper( + f"{self.device_codegen.cpp_stream_type()} {name};" + ) + ) + self.writeline( + f"AOTI_TORCH_ERROR_CODE_CHECK({self.device_codegen.aoti_get_stream()}({device_idx}, (void**)&{name}));" + ) + return name + + def get_autotuning_input_name(self, idx): + return f"{self.autotune_input_prefix}_{idx}" + + def codegen_inputs(self): + # See Note: [Input Alignment handling in Inductor] + # + # JIT Inductor does not guard on input alignment. It relies on copy_misaligned_inputs to + # copy misaligned inputs to aligned buffers. For AOTInductor, we need to do the same in cpp. + + if config.is_fbcode(): + # TODO: This is added because FC. Remove this once the newly added shim symbols, + # e.g. aoti_torch_clone_preserve_strides, have landed + return super().codegen_inputs() + + if V.graph.aot_mode and V.graph.inputs_to_check: + for idx in V.graph.inputs_to_check: + input_name = V.graph.graph_input_names[idx] + assert input_name in V.graph.graph_inputs, ( + f"{input_name} not found in graph inputs" + ) + value = V.graph.graph_inputs[input_name] + assert isinstance(value, TensorBox), ( + f"{input_name} is expected to be tensor but found as {type(value)}" + ) + warn_msg = ( + f"Input {idx} was compiled as {GPU_ALIGN_BYTES}-bytes aligned, " + "but it is not aligned at run time. Copying to an aligned tensor " + "to guarantee correctness, but expect a performance hit." + ) + self.prefix.splice( + f""" + if ((reinterpret_cast({input_name}.data_ptr()) & ({GPU_ALIGN_BYTES} -1)) != 0) {{ + AOTI_TORCH_WARN("{warn_msg}"); + AtenTensorHandle {input_name}_aligned; + aoti_torch_clone_preserve_strides({input_name}, &{input_name}_aligned); + {input_name} = std::move(RAIIAtenTensorHandle({input_name}_aligned)); + }} + """ + ) + + super().codegen_inputs() + + def _define_kernel_helper( + self, + kernel_name: str, + kernel_body: str, + metadata: str | None = None, + gpu: bool = True, + cpp_definition: str | None = None, + ): + if gpu: + self._kernel_name_to_body[kernel_name] = kernel_body + if config.triton.autotune_at_compile_time: + # Call PythonWrapperCodegen to create the autotune code block + PythonWrapperCodegen._define_kernel_helper( + self, kernel_name, kernel_body, metadata, gpu, cpp_definition + ) + else: + return CppWrapperCpu._define_kernel_helper( + self, kernel_name, kernel_body, metadata, gpu, cpp_definition + ) + + def generate(self, is_inference): + with dynamo_timed("CppWrapperGpu.generate", log_pt2_compile_event=True): + return super().generate(is_inference) + + def finalize_prefix(self): + """Define the triton kernels now that autotuning is finished""" + old_prefix = self.prefix # new content should go at start of prefix + + # Generating triton kernel callers can modify the prefix (cached dtypes), + # so do this before running finalize_prefix(), but put the generated code + # after the finalize_prefix() code. + self.prefix = IndentedBuffer() + for kernel in self._triton_call_wrappers.values(): + self.prefix.writeline("\n") + kernel.generate(self) + + # Generate parallel kernel compilation initialization function + if self._lazy_kernel_names: + start_compile_calls = "\n ".join( + f'startKernelCompile(_module_pending_kernels, "{name}", {name}_source);' + for name in self._lazy_kernel_names + ) + self.prefix.splice( + f"""\ +// Start parallel compilation of all Triton kernels +static inline void start_all_triton_kernel_compiles() {{ + loadLazyCompileFuncs(); + _module_pending_kernels = PyDict_New(); + AOTI_TORCH_CHECK(_module_pending_kernels, "Failed to create pending kernels dict"); + {start_compile_calls} +}} + +// Static initializer to start kernel compilation on module load +static struct TritonKernelCompileInit {{ + TritonKernelCompileInit() {{ + start_all_triton_kernel_compiles(); + }} +}} __triton_kernel_compile_init; +""" + ) + + triton_prefix = self.prefix + + self.prefix = IndentedBuffer() + super().finalize_prefix() + + self.prefix.splice(triton_prefix) + + self.prefix.writeline("\n") + self.prefix.splice(old_prefix) + + def generate_tma_descriptor(self, desc): + self.write_tma_descriptor_helpers_once() + + if isinstance(desc, TMADescriptorExperimental): + self._generate_experimental_tma_descriptor(desc) + else: + assert isinstance(desc, TMADescriptorStable) + self._generate_stable_tma_descriptor(desc) + + def _generate_experimental_tma_descriptor(self, desc): + # generate data pointer for the source tensor + source = self.generate_args_decl( + code=self, + call_args=[self.val_to_arg_str(desc.tensor)], + arg_types=[desc.tensor.get_dtype()], + arg_signatures=[None], + # these args are passed to initNDTMADescriptor, which is NOT a triton kernel + is_triton_kernel=False, + ) + + desc_name = desc.name + self.writeline(f"alignas(64) CUtensorMap {desc_name};") + + # `source` is in the form of `&var_x`, where `var_x` is the data pointer + # (CUdeviceptr); we dereference `source` and cast to `void*` to pass to + # the data pointer of the source tensor to the helper function + # `init{1,2}DTMADescriptor` + ptr = f"reinterpret_cast(*({source}))" + dims = ", ".join(self.val_to_arg_str(dim) for dim in desc.dims) + block_dims = ", ".join(self.val_to_arg_str(dim) for dim in desc.block_dims) + element_size = self.val_to_arg_str(desc.element_size) + fn = f"init{desc.rank}DTMADescriptor" + args = f"&{desc_name}, {ptr}, {dims}, {block_dims}, {element_size}" + self.writeline(f"{fn}({args});") + + def _generate_stable_tma_descriptor(self, desc): + source = self.generate_args_decl( + code=self, + call_args=[self.val_to_arg_str(desc.tensor)], + arg_types=[desc.tensor.get_dtype()], + arg_signatures=[None], + # these args are passed to initNDTMADescriptor, which is NOT a triton kernel + is_triton_kernel=False, + ) + + desc_name = desc.name + # Pack the relevant information into a StableTMADescriptor struct. + # See [Note: AOTI TMA Stable handling] for more details. + self.writeline(f"alignas(64) StableTMADescriptor {desc_name};") + + def fill_array(name, values): + for i, val in enumerate(values): + self.writeline(f"{name}[{i}] = {val};") + + ptr = f"reinterpret_cast(*({source}))" + rank = len(desc.tensor.get_size()) + + fill_array(f"{desc_name}.block_shape", desc.block_shape) + fill_array(f"{desc_name}.global_shape", desc.tensor.get_size()) + fill_array(f"{desc_name}.strides", desc.tensor.get_stride()) + + element_size = self.val_to_arg_str(desc.tensor.get_dtype().itemsize) + fn = "initTMADescriptor" + args = ", ".join( + str(x) + for x in [ + f"&{desc_name}.m", + ptr, + element_size, + rank, + f"{desc_name}.block_shape", + f"{desc_name}.global_shape", + f"{desc_name}.strides", + ] + ) + self.writeline(f"{fn}({args});") + + def generate_args_decl( + self, + code: IndentedBuffer | Self, + call_args, + arg_types, + arg_signatures, + is_triton_kernel=True, + scratch_spaces: dict[str, int] | None = None, + ): + """ + Generates any declarations of args to pass into a kernel call, and then returns the arg names. + + In more detail: + * declarations: e.g. this function has a side effect of generating lines like `auto var_0 = ...;` + * returns: a string with the list of args, e.g. "var_0, var_1" + + call_args: list of call arguments + arg_types: list of argument types + arg_signatures: list with signatures of all the args + is_triton_kernel: whether these are passed into a triton kernel or not. In particular, + calls to triton kernels will have an additional global scratch space + arg injected at the front of the arg list. + """ + new_args: list[str] = [] + + def process_tma_stable_arg(arg, arg_type, arg_signature, var_name): + # [Note: AOTI TMA Stable handling] + # For most args, a single arg passed to the python triton interface + # maps to a single arg in the cubin interface. However, for host-side + # TMA descriptors, a single python arg turns into 1 + 2 * N args in the + # cubin interface (where N is the rank). + # + # To do this: at TMA codegen time (for aoti), we generate a struct + # (StableTMADescriptor) containing the necessary information; and then + # when we call the function (i.e. here), we unpack the struct members. + code.writeline(f"auto {var_name} = {cexpr(arg)};") + return _unpack_tma_descriptor_args(var_name, arg_signature) + + def process_args(arg, arg_type, arg_signature=None): + var_name = f"var_{next(self.arg_var_id)}" + # ignore tma descriptors, as host-side TMA descriptors need + # to be passed to the compiled Triton kernel by value + if isinstance(arg_type, UnwrapUnspecArg) and not signature_is_tma_desc( + arg_signature + ): + self.codegen_tensor_item( + arg_type.dtype, + arg, + var_name, + indented_buffer=code, + ) + new_args.append(f"&{var_name}") + elif isinstance(arg_type, torch_dtype) and not signature_is_tma_desc( + arg_signature + ): + device_ptr_type = self.device_codegen.cpp_device_ptr() + code.writeline( + maybe_hipify_code_wrapper( + f"{device_ptr_type} {var_name} = reinterpret_cast<{device_ptr_type}>({arg}.data_ptr());" + ) + ) + new_args.append(f"&{var_name}") + # For symbolic call arguments, examine the arg signatures from triton meta + # to explicitly cast to the right type + # Reason: `auto` can infer unexpected type against kernel input signature. + elif ( + isinstance(arg_type, type(SymbolicCallArg)) + and arg_signature is not None + and arg_signature in TRITON_SIGNATURE_TO_CPP + ): + code.writeline( + f"{TRITON_SIGNATURE_TO_CPP[arg_signature]} {var_name} = {cexpr(arg)};" + ) + new_args.append(f"&{var_name}") + elif arg_type in (sympy.Integer, int): + code.writeline(f"int {var_name} = {cexpr(arg)};") + new_args.append(f"&{var_name}") + elif arg_type in (sympy.Float, float): + # Use signature type if available, otherwise default to float + cpp_type = TRITON_SIGNATURE_TO_CPP.get( # pyrefly: ignore[no-matching-overload] + arg_signature, "float" + ) + code.writeline(f"{cpp_type} {var_name} = {cexpr(arg)};") + new_args.append(f"&{var_name}") + elif arg_signature and arg_signature.startswith("tensordesc<"): + new_args.extend( + process_tma_stable_arg(arg, arg_type, arg_signature, var_name) + ) + else: + code.writeline(f"auto {var_name} = {cexpr(arg)};") + new_args.append(f"&{var_name}") + + for arg, arg_type, arg_signature in zip_longest( + call_args, arg_types, arg_signatures + ): + process_args(arg, arg_type, arg_signature) + + for scratch_name, workspace_size in (scratch_spaces or {}).items(): + if ( + is_triton_kernel + and ( + scratch := self.device_codegen.cpp_scratch( + next(self.arg_var_id), + workspace=TritonScratchWorkspace( + size=workspace_size, + generate_dtype_str=( + lambda: self.codegen_dtype(torch.uint8) + ), + ), + prefix=scratch_name, + ) + ) + is not None + ): + scratch_def, scratch_var = scratch + code.writelines([maybe_hipify_code_wrapper(x) for x in scratch_def]) + new_args.append(f"&{scratch_var}") + + return ", ".join(new_args) + + def _generate_kernel_call_helper( + self, + kernel_name: str, + call_args, + *, + device=None, + triton=True, + arg_types=None, + raw_keys=None, + raw_args=None, + triton_meta=None, + inductor_meta=None, + graph_name="", + original_fxnode_name=None, + current_stream_idx=None, + ): + """ + Override the default value of argument 'gpu' to True here. + generate_kernel_call can still be called with gpu=False because of + a mix of cpu kernels and gpu kernels. + """ + device = device or V.graph.get_current_device_or_throw() + if device.type == "cpu": + # Even in CppWrapperGpu, we may see cpp kernels + return CppWrapperCpu._generate_kernel_call_helper( + self, + kernel_name, + call_args, + device=device, + triton=triton, + arg_types=arg_types, + raw_keys=raw_keys, + raw_args=raw_args, + triton_meta=triton_meta, + inductor_meta=inductor_meta, + ) + + if ( + triton + and config.triton.autotune_at_compile_time + and kernel_name not in self.kernel_autotune_names + ): + # Call PythonWrapperCodegen to create the autotune code block + PythonWrapperCodegen._generate_kernel_call_helper( + self, + kernel_name, + call_args, + device=device, + triton=triton, + arg_types=arg_types, + raw_keys=raw_keys, + raw_args=raw_args, + triton_meta=triton_meta, + inductor_meta=inductor_meta, + original_fxnode_name=original_fxnode_name, + ) + + stream = ( + "stream" + if V.graph.aot_mode + else self.write_get_raw_stream(device.index, graph_name) + ) + + if triton: + call_args, arg_types = self.prepare_triton_wrapper_args( + call_args, + # pyrefly: ignore [bad-argument-type] + arg_types, + ) + + # For lazy compile mode with TMA, extract underlying tensor names + tma_tensor_args: dict[str, str] = {} + is_lazy_compile = ( + not V.graph.aot_mode and config.triton.autotune_at_compile_time is False + ) + if is_lazy_compile and raw_args and triton_meta: + signature = triton_meta.get("signature", {}) + raw_keys_list = raw_keys or [] + for key, raw_arg in zip(raw_keys_list, raw_args): + sig_type = signature.get(key, "") + if isinstance(sig_type, str) and signature_is_tma_desc(sig_type): + if isinstance(raw_arg, TMADescriptorStable): + # Get the underlying tensor name + tensor_name = raw_arg.get_tensor().get_name() + tma_tensor_args[key] = tensor_name + else: + raise AssertionError("Unsupported TMA descriptor type") + + wrapper_name = f"call_{kernel_name}" + if wrapper_name not in self._triton_call_wrappers: + self._triton_call_wrappers[wrapper_name] = DeferredTritonCallWrapper( + wrapper_name, + kernel_name, + self._kernel_name_to_body, + arg_types, + triton_meta=triton_meta, + inductor_meta=inductor_meta, + tma_tensor_args=tma_tensor_args, + ) + + # For TMA in lazy compile mode, add tensor args to the call + if is_lazy_compile and tma_tensor_args: + for tensor_name in tma_tensor_args.values(): + call_args.append(tensor_name) + arg_types.append( + torch.float32 + ) # dtype doesn't matter, just need tensor type + + device_idx = "this->device_idx_" if V.graph.aot_mode else str(device.index) + call_args.append(device_idx) + call_args.append(stream) + if V.graph.aot_mode: + call_args.append("kernels") + call_args.append("this->cubin_dir_") + debug_printer_manager = V.graph.wrapper_code.debug_printer + debug_printer_manager.set_printer_args( + call_args[: len(arg_types)], kernel_name, arg_types, None + ) + with debug_printer_manager: + self.writeline(f"{wrapper_name}({', '.join(call_args)});") + else: + casted = [] + # pyrefly: ignore [bad-argument-type, no-matching-overload] + for arg_type, arg in zip(arg_types, call_args): + new_arg = arg + if arg_type.endswith("*") and arg != "nullptr": + new_arg = f"{arg}.data_ptr()" + # pyrefly: ignore [bad-argument-type] + casted.append(f"({arg_type}){cexpr(new_arg)}") + call_args_str = ", ".join(casted) + self.writeline(f"kernels.{kernel_name}({call_args_str}, {stream});") + + @staticmethod + def prepare_triton_wrapper_args( + call_args: list[Any], arg_types: list[Any] + ) -> tuple[list[Any], list[Any]]: + assert len(call_args) == len(arg_types), (call_args, arg_types) + new_args = [] + new_args_types = [] + for arg, arg_type in zip(call_args, arg_types): + if isinstance(arg, str): + if isinstance(arg_type, torch_dtype) and should_unwrap_unspec_arg(arg): + # dynamo wraps unspec variable as 0d CPU tensor, need convert to scalar + arg_type = UnwrapUnspecArg(dtype=arg_type) + new_args.append(arg) + elif isinstance(arg, bool): + new_args.append(str(arg).lower()) + elif isinstance(arg, (int, float, SymbolicCallArg)): + new_args.append(str(arg)) + else: + new_args.append(cexpr(V.graph.sizevars.simplify(arg))) + new_args_types.append(arg_type) + return new_args, new_args_types + + def make_zero_buffer(self, name): + return f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_zero_({name}.get()));" + + +@dataclasses.dataclass +class UnwrapUnspecArg: + """Marker that we need to call .item() on the tensor""" + + dtype: torch_dtype diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_wrapper_mps.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_wrapper_mps.py new file mode 100644 index 0000000000000000000000000000000000000000..86b6cf92870cf3eea98c921ce8d2ccdaf309ef89 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpp_wrapper_mps.py @@ -0,0 +1,304 @@ +from typing import Any + +import sympy + +import torch +from torch.utils._ordered_set import OrderedSet + +from ..ir import GraphPartitionSignature +from ..virtualized import V +from .cpp_wrapper_cpu import CppWrapperCpu +from .cpp_wrapper_gpu import CppWrapperGpu +from .wrapper import KernelCallLine, PythonWrapperCodegen + + +class CppWrapperMps(CppWrapperGpu): + """ + Generates cpp wrapper for running on MPS and calls metal kernels + """ + + def __init__(self) -> None: + super().__init__() + self._used_kernel_names: OrderedSet[str] = OrderedSet() + self._lambda_counter: int = 0 + + @staticmethod + def create( + is_subgraph: bool, + subgraph_name: str | None, + parent_wrapper: PythonWrapperCodegen | None, + partition_signatures: GraphPartitionSignature | None = None, + ) -> "CppWrapperMps": + return CppWrapperMps() + + def _generate_kernel_call_helper( + self, + kernel_name: str, + call_args: list[str], + *, + device: torch.device | None = None, + triton: bool = True, + arg_types: tuple[Any, ...] | None = None, + raw_keys: tuple[Any, ...] | None = None, + raw_args: tuple[Any, ...] | None = None, + triton_meta: dict[str, Any] | None = None, + inductor_meta: dict[str, Any] | None = None, + graph_name: str = "", + original_fxnode_name: str | None = None, + current_stream_idx: int | None = None, + ) -> None: + """ + Generates MPS kernel call code. It should look something like: + ``` + auto mps_lib_0_lambda = [&](AOTIMetalKernelFunctionHandle handle) { + aoti_torch_mps_start_encoding(handle); + aoti_torch_mps_set_arg_tensor(handle, 0, buf0); + aoti_torch_mps_set_arg_tensor(handle, 1, arg0_1); + aoti_torch_mps_set_arg_tensor(handle, 2, arg1_1); + aoti_torch_mps_dispatch_single(handle, static_cast(10LL)); + }; + + std::function mps_lib_0_func_wrapper = mps_lib_0_lambda; + aoti_torch_mps_run_command_block(get_mps_lib_0_handle(), aoti_torch_mps_shared_callback, &mps_lib_0_func_wrapper); + ``` + """ + device = device or V.graph.get_current_device_or_throw() + if device.type == "cpu": + # Even in CppWrapperGpu, we may see cpp kernels + return CppWrapperCpu._generate_kernel_call_helper( + self, + kernel_name, + call_args, + device=device, + triton=triton, + arg_types=arg_types, + raw_keys=raw_keys, + raw_args=raw_args, + triton_meta=triton_meta, + inductor_meta=inductor_meta, + ) + + assert device.type == "mps" + + assert arg_types is not None + + new_args = [] + for idx, (arg, arg_type) in enumerate(zip(call_args[:-2], arg_types[:-2])): + if isinstance(arg_type, torch.dtype): + new_args.append(f"aoti_torch_mps_set_arg_tensor(handle, {idx}, {arg});") + elif arg_type in (int, sympy.core.symbol.Symbol): + new_args.append(f"aoti_torch_mps_set_arg_int(handle, {idx}, {arg});") + else: + raise NotImplementedError( + f"Unsupported arg type {arg_type} for arg {arg} for kernel {kernel_name}" + ) + + threads, group_size = call_args[-2], call_args[-1] + if threads is None: + raise NotImplementedError("No threads or group_size provided") + + # Check if threads is a single value or an array-like structure + threads_str = str(threads) + is_single_value = ( + threads_str.startswith("{") + and threads_str.endswith("}") + and threads_str.count(",") == 0 + ) or not threads_str.startswith(("{", "[")) + + if is_single_value: + # Extract single value from braces if present + if threads_str.startswith("{") and threads_str.endswith("}"): + single_value = threads_str[1:-1].strip() # Remove braces + else: + single_value = threads_str + + if group_size is None: + new_args.append( + f"aoti_torch_mps_dispatch_single(handle, {single_value});" + ) + else: + # Extract group size value if it's also in braces + group_size_str = str(group_size) + if group_size_str.startswith("{") and group_size_str.endswith("}"): + group_size_value = group_size_str[1:-1].strip() + else: + group_size_value = group_size_str + new_args.append( + f"aoti_torch_mps_dispatch_single_with_group_size(handle, {single_value}, {group_size_value});" + ) + else: + # Handle array case - need to convert initializer list to array + # Use kernel name to make variable names unique + threads_var = f"{kernel_name}_threads_array" + group_size_var = f"{kernel_name}_group_size_array" + + # Extract array size from the initializer list string + def get_array_size(array_str: str) -> int: + # Remove braces and whitespace + content = array_str.strip() + if content.startswith("{") and content.endswith("}"): + content = content[1:-1].strip() + + if not content: # Empty array + return 0 + + # Count elements by counting commas, accounting for nested structures + depth = 0 + comma_count = 0 + for char in content: + if char in "({[<": + depth += 1 + elif char in ")}]>": + depth -= 1 + elif char == "," and depth == 0: + comma_count += 1 + + return comma_count + 1 # Number of elements = commas + 1 + + threads_size = get_array_size(threads_str) + + if group_size is None: + new_args.append("{") + new_args.append(f" uint64_t {threads_var}[] = {threads};") + new_args.append( + f" aoti_torch_mps_dispatch_array(handle, {threads_var}, {threads_size});" + ) + new_args.append("}") + else: + group_size_str = str(group_size) + group_size_size = get_array_size(group_size_str) + new_args.append("{") + new_args.append(f" uint64_t {threads_var}[] = {threads};") + new_args.append(f" uint64_t {group_size_var}[] = {group_size};") + dispatch_args = f"handle, {threads_var}, {threads_size}, {group_size_var}, {group_size_size}" + new_args.append( + f" aoti_torch_mps_dispatch_array_with_group_size({dispatch_args});" + ) + new_args.append("}") + + # debug printer related logic for cpp kernel type. + debug_printer_manager = V.graph.wrapper_code.debug_printer + debug_printer_manager.set_printer_args( + call_args[:-2], + kernel_name, + None, + None, + "cpp", + ) + with debug_printer_manager: + self.write_mps_kernel_call(kernel_name, new_args) + + def write_mps_kernel_call(self, name: str, call_args: list[str]) -> None: + # Generate unique variable names to avoid duplicate declarations + # when the same MPS lib is used multiple times + unique_suffix = self._lambda_counter + self._lambda_counter += 1 + + lambda_name = f"{name}_lambda_{unique_suffix}" + wrapper_name = f"{name}_func_wrapper_{unique_suffix}" + + # Generate the function call code (in current location) + # Create lambda that captures by reference and pass its pointer through void* + self.writeline( + f"auto {lambda_name} = [&](AOTIMetalKernelFunctionHandle handle) {{" + ) + self.writeline(" aoti_torch_mps_start_encoding(handle);") + + # Output call args directly since we're capturing by reference + for call_arg in call_args: + self.writeline(f" {call_arg}") + self.writeline("};") + self.writeline("") + + # Pass lambda pointer through void* + self.writeline( + f"std::function {wrapper_name} = {lambda_name};" + ) + self.writeline( + f"aoti_torch_mps_run_command_block(get_{name}_handle(), aoti_torch_mps_shared_callback, &{wrapper_name});" + ) + + @staticmethod + def get_device_include_path(device: str) -> str: + assert V.graph.aot_mode + return ( + "#include \n" + "#include " + ) + + def codegen_additional_funcs(self) -> None: + """ + Generate thread-safe lazy singleton pattern for MPS shader libraries with RAII cleanup. + + The generated code will look like: + ``` + AOTIMetalKernelFunctionHandle get_mps_lib_0_handle() { + static auto kernel_handle = []() { + AOTIMetalShaderLibraryHandle lib_handle = nullptr; + AOTIMetalKernelFunctionHandle kern_handle = nullptr; + + aoti_torch_mps_create_shader_library(mps_lib_0_source, &lib_handle); + aoti_torch_mps_get_kernel_function(lib_handle, "generated_kernel", &kern_handle); + + // RAII wrapper with custom deleter + auto lib_deleter = [](AOTIMetalShaderLibraryHandle h) { + if (h) aoti_torch_mps_delete_shader_library(h); + }; + + using LibDeleter = decltype(lib_deleter); + using LibPtr = std::unique_ptr; + + // Return pair of kernel handle and library smart pointer for cleanup + return std::make_pair(kern_handle, LibPtr(lib_handle, lib_deleter)); + }(); + return kernel_handle.first; + } + ``` + """ + + # Add shimified handles and functions + shader_libraries: OrderedSet[str] = OrderedSet() + for line in self.lines: + if not isinstance(line, KernelCallLine): + continue + if line.device.type != "mps": + continue + + # Extract library name from kernel name (e.g., "mps_lib_0" from kernel calls) + if line.kernel_name not in self._used_kernel_names: + self._used_kernel_names.add(line.kernel_name) + shader_libraries.add(line.kernel_name) + + # NOTE: For shimified version, we expect the shader source constant to be generated + # by the existing MPS shader generation process, but instead of instantiating the + # DynamicMetalShaderLibrary directly, we'll use our shim functions. + # The existing codegen should produce something like: + # const char* mps_lib_0_source = R"MTL(...shader_source...)MTL"; + # instead of: + # at::native::mps::DynamicMetalShaderLibrary mps_lib_0(R"MTL(...shader_source...)MTL"); + + # Generate thread-safe lazy singleton with RAII for each library + for lib_name in shader_libraries: + self.prefix.splice(f""" +AOTIMetalKernelFunctionHandle get_{lib_name}_handle() {{ + static auto kernel_handle = []() {{ + AOTIMetalShaderLibraryHandle lib_handle = nullptr; + AOTIMetalKernelFunctionHandle kern_handle = nullptr; + + aoti_torch_mps_create_shader_library({lib_name}_source, &lib_handle); + aoti_torch_mps_get_kernel_function(lib_handle, "generated_kernel", &kern_handle); + + // RAII wrapper with custom deleter + auto lib_deleter = [](AOTIMetalShaderLibraryHandle h) {{ + if (h) aoti_torch_mps_delete_shader_library(h); + }}; + + using LibDeleter = decltype(lib_deleter); + using LibPtr = std::unique_ptr; + + // Return pair of kernel handle and library smart pointer for cleanup + return std::make_pair(kern_handle, LibPtr(lib_handle, lib_deleter)); + }}(); + return kernel_handle.first; +}} +""") diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpu_device_op_overrides.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpu_device_op_overrides.py new file mode 100644 index 0000000000000000000000000000000000000000..ccada837abbd4dbdaf16984c0a44ff7f90cedc04 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cpu_device_op_overrides.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from textwrap import dedent + +from .common import DeviceOpOverrides, register_device_op_overrides + + +class CpuDeviceOpOverrides(DeviceOpOverrides): + def import_get_raw_stream_as(self, name: str) -> str: + return dedent( + """ + def get_raw_stream(_): + return 0 + """ + ) + + def cpp_kernel_type(self) -> str: + return "void*" + + def set_device(self, device_idx: int) -> str: + return "pass" + + def synchronize(self) -> str: + return "pass" + + def device_guard(self, device_idx: int) -> str: + return "pass" + + +register_device_op_overrides("cpu", CpuDeviceOpOverrides()) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cuda/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cuda/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cuda/compile_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cuda/compile_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..cee25029c86f3c2db7f6e2a7d9eaf8de7a2956e2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cuda/compile_utils.py @@ -0,0 +1,271 @@ +# mypy: allow-untyped-defs +import logging +import os +import shutil +from pathlib import Path + +import torch +from torch._inductor import config +from torch._inductor.codegen.cuda import cuda_env +from torch._inductor.cpp_builder import _set_gpu_runtime_env, _transform_cuda_paths +from torch._inductor.utils import is_linux + + +if config.is_fbcode(): + from triton.fb.build import build_paths + + +log = logging.getLogger(__name__) +autotuning_log = torch._logging.getArtifactLogger(__name__, "autotuning") + + +def use_re_build() -> bool: + """ + Use for CUTLASS compilation only right now. + """ + if config.is_fbcode() and not cuda_env.nvcc_exist(_cuda_compiler()): + from triton.fb.re_build_helper import should_build_locally + + return not should_build_locally() + return False + + +def _cutlass_path() -> str: + if config.is_fbcode(): + from libfb.py import parutil + + return parutil.get_dir_path("cutlass-4-headers") + else: + return config.cutlass.cutlass_dir + + +def _cutlass_paths() -> list[str]: + return [ + "include", + "tools/library/include", + "tools/library/src", + "tools/util/include", + ] + + +def _clone_cutlass_paths(build_root: str) -> list[str]: + paths = _cutlass_paths() + cutlass_root = _cutlass_path() + for path in _cutlass_paths(): + old_path = os.path.join(cutlass_root, path) + new_path = os.path.join(build_root, path) + shutil.copytree(old_path, new_path, dirs_exist_ok=True) + return paths + + +def _cutlass_include_paths() -> list[str]: + cutlass_path = _cutlass_path() + return [ + # Use realpath to get canonical absolute paths, in order not to mess up cache keys + os.path.realpath(os.path.join(cutlass_path, path)) + for path in _cutlass_paths() + ] + + +def _cuda_compiler() -> str | None: + if cuda_env.nvcc_exist(config.cuda.cuda_cxx): + return config.cuda.cuda_cxx + if config.is_fbcode(): + return os.path.join(build_paths.sdk_home, "bin", "nvcc") + if cuda_env.nvcc_exist(os.getenv("CUDACXX")): + return os.getenv("CUDACXX", "") + if cuda_env.nvcc_exist(os.getenv("CUDA_HOME")): + return os.path.realpath(os.path.join(os.getenv("CUDA_HOME", ""), "bin/nvcc")) + return "nvcc" + + +def _cuda_lib_options() -> list[str]: + """ + Util function for CUTLASS backend to find the correct CUDA libraries. + """ + _set_gpu_runtime_env() # cpp_extension consults the env + from torch.utils import cpp_extension + + lpaths = cpp_extension.library_paths(device_type="cuda") + if use_re_build(): + lpaths += [ + build_paths.sdk_lib, + os.path.join(build_paths.sdk_lib, "stubs"), + ] + extra_ldflags: list[str] = [] + if is_linux(): + _transform_cuda_paths(lpaths) + for path in lpaths: + if "torch/lib" in path: + # don't want to depend on pytorch + continue + extra_ldflags.append(f"-L{path}") + # -rpath ensures the DLL can find its dependencies when loaded, even + # if the library path is non-standard. + # But do not add the stubs folder to rpath as the driver is expected to be found at runtime + if os.path.basename(path) != "stubs": + extra_ldflags.extend(["-Xlinker", f"-rpath={path}"]) + extra_ldflags.append("-lcuda") + extra_ldflags.append("-lcudart") + else: + raise NotImplementedError( + "Unsupported env, failed to find cuda libs! Currently only Linux is supported." + ) + return extra_ldflags + + +def _nvcc_host_compiler_options() -> list[str]: + return [ + "-fPIC", + "-fno-strict-aliasing", + "-fvisibility=hidden", + "-Wconversion", + ] + + +def _nvcc_arch_as_compile_option() -> str: + arch = cuda_env.get_cuda_arch() + if arch == "90": + # Required by cutlass compilation. + return "90a" + if arch == "100": + return "100a" + if arch == "101": + return "101a" + if arch == "103": + return "103a" + if arch == "110": + return "110a" + if arch == "120": + return "120a" + if arch == "121": + return "121a" + return arch + + +def _nvcc_compiler_options() -> list[str]: + arch = _nvcc_arch_as_compile_option() + code = [f"sm_{arch}", f"compute_{arch}"] + if config.cuda.enable_cuda_lto: + code += [f"lto_{arch}"] + options = [ + "-t=0", + "-DCUTLASS_ENABLE_TENSOR_CORE_MMA=1", + "-DCUTLASS_ENABLE_SM90_EXTENDED_MMA_SHAPES=1", + "-DCUTE_SM90_EXTENDED_MMA_SHAPES_ENABLED", + "-w", + f"-gencode=arch=compute_{arch},code=[{','.join(code)}]", + config.cutlass.compile_opt_level, + "-std=c++20", + "--expt-relaxed-constexpr", + "-DNDEBUG", + ] + if config.is_fbcode(): + options.extend(["-ccbin", os.path.dirname(build_paths.gcc)]) + if config.cutlass.enable_debug_info: + options.extend(["-lineinfo", "-g", "-DCUTLASS_DEBUG_TRACE_LEVEL=1"]) + if config.cuda.enable_ptxas_info: + options.extend( + [ + "--keep", # Keep the intermediate files for debugging (including ptx, sass, cubin etc.) + "--ptxas-options=--warn-on-local-memory-usage", # warn us if local memory is used in CUDA Kernels + "--ptxas-options=--warn-on-spills", # warn us if register spilling happens in CUDA Kernels + "--resource-usage", # Report on CUDA resource usage (shared mem, registers etc.) + "--source-in-ptx", + ] + ) # Annotate the ptx file with source information + if config.cutlass.use_fast_math: + options.extend( + [ + "--use_fast_math", + "-DCUTLASS_USE_TANH_FOR_SIGMOID=1", + ] + ) + return options + + +def cuda_compile_command( + src_files: list[str], + dst_file: str, + dst_file_ext: str, + extra_args: list[str] | None = None, +) -> str: + if extra_args is None: + extra_args = [] + if use_re_build(): + build_path = os.path.dirname(dst_file) + include_paths = _clone_cutlass_paths(build_path) + src_files = [os.path.basename(src_file) for src_file in src_files] + dst_file = os.path.basename(dst_file) + else: + include_paths = _cutlass_include_paths() + cuda_lib_options = _cuda_lib_options() + nvcc_host_compiler_options = _nvcc_host_compiler_options() + nvcc_compiler_options = _nvcc_compiler_options() + options = ( + nvcc_compiler_options + + extra_args + + [ + f"-Xcompiler {opt}" if "=" in opt else f"-Xcompiler={opt}" + for opt in nvcc_host_compiler_options + ] + + ["-I" + path for path in include_paths] + + cuda_lib_options + ) + src_file = " ".join(src_files) + res = "" + if dst_file_ext == "o": + res = f"{_cuda_compiler()} {' '.join(options)} -c -o {dst_file} {src_file}" + elif dst_file_ext == "so": + options.append("-shared") + res = f"{_cuda_compiler()} {' '.join(options)} -o {dst_file} {src_file}" + elif dst_file_ext == "exe": + res = f"{_cuda_compiler()} {' '.join(options)} -o {dst_file} {src_file}" + else: + raise NotImplementedError(f"Unsupported output file suffix {dst_file_ext}!") + if log.isEnabledFor(logging.DEBUG): + log.debug("CUDA command: %s", res) + else: + autotuning_log.debug("CUDA command: %s", res) + return res + + +class CUDACompileSourceCapturingContext: + # Helper class for Benchmarking and Testing CUTLASS Kernels in isolation. + # Can be used to capture the sourcecode passed to CUDACodeCache.compile + + def __init__(self): + self.sources = [] + self._compile_patch = None + + def __enter__(self, *args, **kwargs): + import unittest.mock as mock + + import torch._inductor.codecache + + _compile_method_orig = torch._inductor.codecache.CUDACodeCache.compile + + def my_compile(source_code, dst_file_ext, extra_args: list[str] | None = None): + self.sources.append(source_code) + return _compile_method_orig(source_code, dst_file_ext) + + # pyrefly: ignore [bad-assignment] + self._compile_patch = mock.patch( + "torch._inductor.codecache.CUDACodeCache.compile", my_compile + ) + self._compile_patch.__enter__(*args, **kwargs) # type: ignore[union-attr] + return self + + def __exit__(self, *args, **kwargs): + self._compile_patch.__exit__(*args, **kwargs) # type: ignore[union-attr] + + +def cuda_standalone_runner_compile_command(srcpath: Path, exepath: Path): + # returns command string to compile a (captured) CUDA GEMM Kernel source to a standalone executable that's ready to run + # Passes the correct preprocessor define to nvcc to ensure the standalone runner is enabled. + + extra_args = ["-DGENERATE_STANDALONE_RUNNER=1", "-DCUTLASS_DEBUG_TRACE_LEVEL=1"] + compile_command = cuda_compile_command( + [str(srcpath)], str(exepath), "exe", extra_args=extra_args + ) + return compile_command diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cuda/cuda_env.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cuda/cuda_env.py new file mode 100644 index 0000000000000000000000000000000000000000..e12bd94d2a677f1e4c03fba78e7fa3b288d046b7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cuda/cuda_env.py @@ -0,0 +1,54 @@ +import functools +import logging +import shutil + +import torch +from torch._inductor.utils import clear_on_fresh_cache + +from ... import config + + +log = logging.getLogger(__name__) + + +@clear_on_fresh_cache +@functools.lru_cache(1) +def get_cuda_arch() -> str | None: + try: + cuda_arch = config.cuda.arch + if cuda_arch is None: + # Get Compute Capability of the first Visible device + major, minor = torch.cuda.get_device_capability(0) + return str(major * 10 + minor) + return str(cuda_arch) + except Exception: + log.exception("Error getting cuda arch") + return None + + +@clear_on_fresh_cache +@functools.lru_cache(1) +def is_datacenter_blackwell_arch() -> bool: + arch = get_cuda_arch() + if arch is None: + return False + arch_number = int(arch) + return arch_number >= 100 and arch_number < 110 + + +@clear_on_fresh_cache +@functools.lru_cache(1) +def get_cuda_version() -> str | None: + try: + cuda_version = config.cuda.version + if cuda_version is None: + cuda_version = torch.version.cuda + return cuda_version + except Exception: + log.exception("Error getting cuda version") + return None + + +@functools.cache +def nvcc_exist(nvcc_path: str | None = "nvcc") -> bool: + return nvcc_path is not None and shutil.which(nvcc_path) is not None diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cuda/device_op_overrides.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cuda/device_op_overrides.py new file mode 100644 index 0000000000000000000000000000000000000000..8d0f210fd6c25847e6b384ae9c443e4eb8afecf6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cuda/device_op_overrides.py @@ -0,0 +1,365 @@ +from __future__ import annotations + +import torch + +from ..common import ( + DeviceOpOverrides, + register_device_op_overrides, + TritonScratchWorkspace, +) + + +class CUDADeviceOpOverrides(DeviceOpOverrides): + """ + CUDA-specific codegen functions, see DeviceOpOverrides for details + """ + + def import_get_raw_stream_as(self, name: str) -> str: + return f"from torch._C import _cuda_getCurrentRawStream as {name}" + + def set_device(self, device_idx: int) -> str: + return f"torch.cuda.set_device({device_idx})" + + def synchronize(self) -> str: + return "torch.cuda.synchronize()" + + def device_guard(self, device_idx: int) -> str: + return f"torch.cuda._DeviceGuard({device_idx})" + + def cpp_device_guard(self) -> str: + return "at::cuda::CUDAGuard" + + def cpp_aoti_device_guard(self) -> str: + return "AOTICudaGuard" + + def cpp_stream_guard(self) -> str: + return "at::cuda::CUDAStreamGuard" + + def cpp_aoti_stream_guard(self) -> str: + return "AOTICudaStreamGuard" + + def cpp_getStreamFromExternal(self) -> str: + return "at::cuda::getStreamFromExternal" + + def kernel_header(self) -> str: + source_codes = """ + #include + #include + #include + """ + return source_codes + + def kernel_driver(self) -> str: + source_codes = """ + #define CUDA_DRIVER_CHECK(EXPR) \\ + do { \\ + CUresult code = EXPR; \\ + const char *msg; \\ + CUresult code_get_error = cuGetErrorString(code, &msg); \\ + if (code_get_error != CUDA_SUCCESS) { \\ + throw std::runtime_error( \\ + std::string("CUDA driver error: ") + \\ + std::string("invalid error code!")); \\ + } \\ + if (code != CUDA_SUCCESS) { \\ + throw std::runtime_error( \\ + std::string("CUDA driver error: ") + \\ + std::string(msg)); \\ + } \\ + } while (0); + + static inline CUfunction loadKernel( + std::string filePath, + const std::string &funcName, + uint32_t sharedMemBytes, + const std::optional &cubinDir = std::nullopt) { + if (cubinDir) { + std::filesystem::path p1{*cubinDir}; + std::filesystem::path p2{filePath}; + filePath = (p1 / p2.filename()).string(); + } + + CUmodule mod; + CUfunction func; + CUDA_DRIVER_CHECK(cuModuleLoad(&mod, filePath.c_str())); + CUDA_DRIVER_CHECK(cuModuleGetFunction(&func, mod, funcName.c_str())); + if (sharedMemBytes > 0) { + CUDA_DRIVER_CHECK(cuFuncSetAttribute( + func, + CU_FUNC_ATTRIBUTE_MAX_DYNAMIC_SHARED_SIZE_BYTES, + sharedMemBytes + )) + } + return func; + } + + static inline CUfunction loadKernel(const void* start, const std::string &funcName, uint32_t sharedMemBytes) { + CUmodule mod; + CUfunction func; + CUDA_DRIVER_CHECK(cuModuleLoadData(&mod, start)); + CUDA_DRIVER_CHECK(cuModuleGetFunction(&func, mod, funcName.c_str())); + if (sharedMemBytes > 0) { + CUDA_DRIVER_CHECK(cuFuncSetAttribute( + func, + CU_FUNC_ATTRIBUTE_MAX_DYNAMIC_SHARED_SIZE_BYTES, + sharedMemBytes + )) + } + return func; + } + + static inline void launchKernel( + CUfunction func, + uint32_t gridX, + uint32_t gridY, + uint32_t gridZ, + uint32_t numWarps, + uint32_t sharedMemBytes, + void* args[], + cudaStream_t stream) { + CUDA_DRIVER_CHECK(cuLaunchKernel( + func, gridX, gridY, gridZ, 32*numWarps, 1, 1, sharedMemBytes, stream, args, nullptr + )); + } + """ + if torch.version.hip is not None: + # Adjusting the warp size to GPU supported wavefront size on AMD GPU + prop = torch.cuda.get_device_properties(torch.cuda.current_device()) + source_codes = source_codes.replace( + "32*numWarps", str(prop.warp_size) + "*numWarps" + ) + return source_codes + + def tma_descriptor_helpers(self) -> str: + """ + CUDA helper functions for initializing TMA Descriptors on host side + """ + if torch.version.hip is not None: + raise RuntimeError("Host-side TMA descriptors not supported on HIP.") + + # helper functions for initializing 1D and 2D TMA descriptors in C++. borrowed from the Triton code here: + # Old APIs (fill(1|2)DTMADescriptor): + # https://github.com/triton-lang/triton/blob/6af4f88591c85de079d8a36a4d7dba67918e2b39/third_party/nvidia/backend/driver.c#L283 + # New APIs (fillTMADescriptor): + # https://github.com/triton-lang/triton/blob/main/third_party/nvidia/backend/driver.c#L283 + return """ + #if !defined(USE_ROCM) && defined(CUDA_VERSION) && CUDA_VERSION >= 12000 + [[maybe_unused]] static void init1DTMADescriptor( + CUtensorMap* m, + void* globalAddress, + uint64_t dim, + uint32_t blockDim, + uint32_t elementSize) { + uint64_t dims[1] = {dim}; + uint64_t globalStrides[1] = {dim * elementSize}; + uint32_t tensorDims[1] = {blockDim}; + uint32_t elementStrides[1] = {1}; + + CUtensorMapDataType type; + switch (elementSize) { + case 1: + type = CU_TENSOR_MAP_DATA_TYPE_UINT8; + break; + case 2: + type = CU_TENSOR_MAP_DATA_TYPE_UINT16; + break; + case 4: + type = CU_TENSOR_MAP_DATA_TYPE_UINT32; + break; + default: + throw std::runtime_error("elementSize must be 1, 2, or 4"); + } + + if (elementSize * blockDim < 32) { + throw std::runtime_error("block size too small"); + } + + int rank = 1; + + CUDA_DRIVER_CHECK(cuTensorMapEncodeTiled( + m, type, rank, globalAddress, dims, + globalStrides, tensorDims, elementStrides, CU_TENSOR_MAP_INTERLEAVE_NONE, + CU_TENSOR_MAP_SWIZZLE_NONE, CU_TENSOR_MAP_L2_PROMOTION_NONE, + CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE)); + } + + [[maybe_unused]] static void init2DTMADescriptor( + CUtensorMap* m, + void* globalAddress, + uint64_t dim1, + uint64_t dim0, + uint32_t blockDim1, + uint32_t blockDim0, + uint32_t elementSize) { + uint64_t dims[2] = {dim0, dim1}; + uint32_t tensorDims[2] = {blockDim0, blockDim1}; + uint64_t globalStrides[2] = {dims[0] * elementSize, + dims[0] * dims[1] * elementSize}; + uint32_t elementStrides[2] = {1, 1}; + + CUtensorMapDataType type; + switch (elementSize) { + case 1: + type = CU_TENSOR_MAP_DATA_TYPE_UINT8; + break; + case 2: + type = CU_TENSOR_MAP_DATA_TYPE_UINT16; + break; + case 4: + type = CU_TENSOR_MAP_DATA_TYPE_UINT32; + break; + default: + throw std::runtime_error("elementSize must be 1, 2, or 4"); + } + + int rank = 2; + + CUtensorMapSwizzle swizzle = CU_TENSOR_MAP_SWIZZLE_128B; + uint32_t contigDimSizeInByte = elementSize * tensorDims[0]; + if (contigDimSizeInByte >= 128) { + swizzle = CU_TENSOR_MAP_SWIZZLE_128B; + } else if (contigDimSizeInByte >= 64) { + swizzle = CU_TENSOR_MAP_SWIZZLE_64B; + } else if (contigDimSizeInByte >= 32) { + swizzle = CU_TENSOR_MAP_SWIZZLE_32B; + } else { + throw std::runtime_error("block size too small"); + } + + if (contigDimSizeInByte > 128) { + tensorDims[0] = 128 / elementSize; + } + + CUDA_DRIVER_CHECK(cuTensorMapEncodeTiled( + m, type, rank, globalAddress, dims, + globalStrides, tensorDims, elementStrides, CU_TENSOR_MAP_INTERLEAVE_NONE, + swizzle, CU_TENSOR_MAP_L2_PROMOTION_L2_128B, + CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE)); + } + + [[maybe_unused]] static void initTMADescriptor( + CUtensorMap* m, + void* globalAddress, + int elemSize, + int rank, + uint32_t* blockSize, + uint64_t* shape, + uint64_t* stride + ) { + uint32_t elementStrides[5] = {1, 1, 1, 1, 1}; + uint32_t blockSizeInt[5]; + uint64_t shapeInt[5]; + uint64_t stridesLL[5]; + + // Reorder blockSize (reverse the order) + for (int i = 0; i < rank; ++i) { + blockSizeInt[rank - i - 1] = blockSize[i]; + } + + // Reorder shape (reverse the order) + for (int i = 0; i < rank; ++i) { + shapeInt[rank - i - 1] = shape[i]; + } + + // Reorder and calculate strides + for (int i = 0; i + 1 < rank; ++i) { + stridesLL[rank - i - 2] = elemSize * stride[i]; + } + stridesLL[rank - 1] = + shapeInt[rank - 1] * (rank == 1 ? elemSize : stridesLL[rank - 2]); + + CUtensorMapDataType type; + // In Triton this is computed ahead of time; but for simplicity + // in the PyTorch version we copied this code from the old + // TMA API handling (i.e. init2DTMADescriptor) + switch (elemSize) { + case 1: + type = CU_TENSOR_MAP_DATA_TYPE_UINT8; + break; + case 2: + type = CU_TENSOR_MAP_DATA_TYPE_UINT16; + break; + case 4: + type = CU_TENSOR_MAP_DATA_TYPE_UINT32; + break; + default: + throw std::runtime_error("elemSize must be 1, 2, or 4"); + } + + // Calculate the size of the most contiguous dimension in bytes + CUtensorMapSwizzle swizzle = CU_TENSOR_MAP_SWIZZLE_128B; + uint32_t contigDimSizeInByte = elemSize * blockSizeInt[0]; + if (rank == 1) { + // rank 1 should not be swizzled + swizzle = CU_TENSOR_MAP_SWIZZLE_NONE; + } else if (contigDimSizeInByte >= 128) { + swizzle = CU_TENSOR_MAP_SWIZZLE_128B; + } else if (contigDimSizeInByte >= 64) { + swizzle = CU_TENSOR_MAP_SWIZZLE_64B; + } else if (contigDimSizeInByte >= 32) { + swizzle = CU_TENSOR_MAP_SWIZZLE_32B; + } else { + throw std::runtime_error("block size too small"); + } + + CUDA_DRIVER_CHECK(cuTensorMapEncodeTiled( + m, type, rank, globalAddress, + shapeInt, stridesLL, blockSizeInt, elementStrides, + CU_TENSOR_MAP_INTERLEAVE_NONE, (CUtensorMapSwizzle)swizzle, + CU_TENSOR_MAP_L2_PROMOTION_L2_128B, CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE)); + } + + struct StableTMADescriptor { + CUtensorMap m; + uint32_t block_shape[5]; + uint64_t global_shape[5]; + uint64_t strides[5]; + }; + #endif + """ + + def cpp_stream_type(self) -> str: + return "cudaStream_t" + + def aoti_get_stream(self) -> str: + return "aoti_torch_get_current_cuda_stream" + + def cpp_kernel_type(self) -> str: + return "CUfunction" + + def cpp_device_ptr(self) -> str: + return "CUdeviceptr" + + def cpp_scratch( + self, idx: int, workspace: TritonScratchWorkspace, prefix: str | None = None + ) -> tuple[list[str], str] | None: + prefix = f"{prefix}_" if prefix else "" + var_name = f"{prefix}scratch_{idx}" + if workspace.size > 0: + size_expr = ( + f"static_cast({workspace.size}) * grid_0 * grid_1 * grid_2" + ) + size_array = f"int64_t {var_name}_size[] = {{{size_expr}}};" + stride_array = f"int64_t {var_name}_stride[] = {{1}};" + device_type = "cached_torch_device_type_cuda" + device_idx = "device_idx_" + + return ( + [ + f"{size_array}", + f"{stride_array}", + f"AtenTensorHandle {var_name}_handle;", + ( + f"AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_empty_strided(1, {var_name}_size, {var_name}_stride, " + f"{workspace.generate_dtype_str()}, {device_type}, {device_idx}, &{var_name}_handle));" + ), + f"RAIIAtenTensorHandle {var_name}_tensor({var_name}_handle);", + f"CUdeviceptr {var_name} = reinterpret_cast({var_name}_tensor.data_ptr());", + ], + var_name, + ) + else: + return [f"CUdeviceptr {var_name} = 0;"], var_name + + +register_device_op_overrides("cuda", CUDADeviceOpOverrides()) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cuda_combined_scheduling.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cuda_combined_scheduling.py new file mode 100644 index 0000000000000000000000000000000000000000..b69225b78a2c3d48be09271a4bd8548892eade18 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cuda_combined_scheduling.py @@ -0,0 +1,186 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +from typing import Any, TYPE_CHECKING + +from ..scheduler import ( + BaseSchedulerNode, + BaseScheduling, + FusedSchedulerNode, + Scheduler, + SchedulerNode, +) +from .cutedsl.cutedsl_scheduling import CuteDSLScheduling +from .cutlass.scheduling import CUTLASSScheduling +from .nv_universal_gemm.nv_universal_gemm_scheduling import NVUniversalGemmScheduling +from .rocm.rocm_cpp_scheduling import ROCmCPPScheduling +from .triton import TritonScheduling + + +if TYPE_CHECKING: + from collections.abc import Sequence + from typing import TypeAlias + + from sympy import Expr + + import torch + from torch.utils._ordered_set import OrderedSet + + from .common import BackendFeature + + _IntLike: TypeAlias = int | Expr + + +class CUDACombinedScheduling(BaseScheduling): + """ + Scheduler for CUDA Kernels, which delegates calls as appropriate + to the CUDA-C++ and Triton Schedulers, which both work for CUDA devices + and use a unified-wrapper for codegen. + + If Scheduling code needs to be specialized for the case of mixed Triton / CUDA C++ code, + this would also be the place to do it. + """ + + def __init__(self, scheduler: Scheduler | None) -> None: + super().__init__(scheduler) + self._triton_scheduling = TritonScheduling(scheduler) + self._cutlass_scheduling = CUTLASSScheduling(scheduler) + self._rocm_cpp_scheduling = ROCmCPPScheduling(scheduler) + self._cutedsl_scheduling = CuteDSLScheduling(scheduler) + self._nv_universal_gemm_scheduling = NVUniversalGemmScheduling(scheduler) + + def get_backend_features(self, device: torch.device) -> OrderedSet[BackendFeature]: + return self._triton_scheduling.get_backend_features(device) + + def choose_node_backend(self, node: BaseSchedulerNode) -> BaseScheduling: + if self._cutlass_scheduling.is_cutlass_template(node): + return self._cutlass_scheduling + if self._rocm_cpp_scheduling.is_rocm_cpp_template(node): + return self._rocm_cpp_scheduling + if self._cutedsl_scheduling.is_cutedsl_template(node): + return self._cutedsl_scheduling + if self._nv_universal_gemm_scheduling.is_nv_universal_gemm_template(node): + return self._nv_universal_gemm_scheduling + return self._triton_scheduling + + def can_fuse_vertical( + self, node1: BaseSchedulerNode, node2: BaseSchedulerNode + ) -> bool: + if self._cutlass_scheduling.can_fuse_vertical(node1, node2): + return True + elif self._cutlass_scheduling.is_cutlass_template( + node1 + ) or self._cutlass_scheduling.is_cutlass_template(node2): + return False + # CuteDSL doesn't support vertical fusion currently + elif self._cutedsl_scheduling.is_cutedsl_template( + node1 + ) or self._cutedsl_scheduling.is_cutedsl_template(node2): + return False + # NVIDIA Universal GEMM doesn't support vertical fusion currently + elif self._nv_universal_gemm_scheduling.is_nv_universal_gemm_template( + node1 + ) or self._nv_universal_gemm_scheduling.is_nv_universal_gemm_template(node2): + return False + return self._triton_scheduling.can_fuse_vertical(node1, node2) + + def can_fuse_horizontal( + self, node1: BaseSchedulerNode, node2: BaseSchedulerNode + ) -> bool: + for node in (node1, node2): + if self._cutlass_scheduling.is_cutlass_template(node): + return self._cutlass_scheduling.can_fuse_horizontal( + node1, node2 + ) # always False at the moment + if self._cutedsl_scheduling.is_cutedsl_template(node): + return self._cutedsl_scheduling.can_fuse_horizontal( + node1, node2 + ) # always False at the moment + if self._nv_universal_gemm_scheduling.is_nv_universal_gemm_template(node): + return self._nv_universal_gemm_scheduling.can_fuse_horizontal( + node1, node2 + ) # always False at the moment + return self._triton_scheduling.can_fuse_horizontal(node1, node2) + + def group_fn( + self, sizes: Sequence[Sequence[_IntLike]] + ) -> tuple[tuple[_IntLike, ...], ...]: + return self._triton_scheduling.group_fn(sizes) + + def codegen_template( + self, + template_node: BaseSchedulerNode, + epilogue_nodes: Sequence[BaseSchedulerNode], + prologue_nodes: Sequence[BaseSchedulerNode], + ) -> str | None: + if self._cutlass_scheduling.is_cutlass_template(template_node): + assert not prologue_nodes + return self._cutlass_scheduling.codegen_template( + template_node, epilogue_nodes, prologue_nodes + ) + elif self._rocm_cpp_scheduling.is_rocm_cpp_template(template_node): + assert not epilogue_nodes + assert not prologue_nodes + return self._rocm_cpp_scheduling.codegen_template( + template_node, epilogue_nodes, prologue_nodes + ) + elif self._cutedsl_scheduling.is_cutedsl_template(template_node): + # TODO remove this when we add epilogue support + assert not epilogue_nodes + assert not prologue_nodes + return self._cutedsl_scheduling.codegen_template( + template_node, epilogue_nodes, prologue_nodes + ) + elif self._nv_universal_gemm_scheduling.is_nv_universal_gemm_template( + template_node + ): + # NVIDIA Universal GEMM doesn't support epilogue/prologue fusion yet + assert not epilogue_nodes + assert not prologue_nodes + return self._nv_universal_gemm_scheduling.codegen_template( + template_node, epilogue_nodes, prologue_nodes + ) + else: + return self._triton_scheduling.codegen_template( + template_node, epilogue_nodes, prologue_nodes + ) + + def codegen_mix_order_reduction(self, node): + return self._triton_scheduling.codegen_mix_order_reduction(node) + + def codegen_node(self, node: FusedSchedulerNode | SchedulerNode) -> None: + return self._triton_scheduling.codegen_node(node) + + def codegen_sync(self) -> None: + return self._triton_scheduling.codegen_sync() + + def flush(self) -> None: + return self._triton_scheduling.flush() + + def codegen_combo_kernel(self, *args: Any, **kwargs: Any) -> None: + return self._triton_scheduling.codegen_combo_kernel(*args, **kwargs) + + def benchmark_fused_nodes( + self, nodes: Sequence[BaseSchedulerNode] + ) -> tuple[float, str]: + return self._triton_scheduling.benchmark_fused_nodes(nodes) + + def benchmark_codegened_module(self, module): + return self._triton_scheduling.benchmark_codegened_module(module) + + def generate_kernel_code_from_nodes( + self, + nodes: Sequence[Any], + benchmark_kernel: bool = False, + hint_override: int | None = None, + ) -> str: + return self._triton_scheduling.generate_kernel_code_from_nodes( + nodes, benchmark_kernel, hint_override=hint_override + ) + + def benchmark_combo_kernel( + self, node_list: Sequence[BaseSchedulerNode], node_benchmark_results + ) -> tuple[float, float, list[str | None]]: + return self._triton_scheduling.benchmark_combo_kernel( + node_list, node_benchmark_results + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/custom_extern_kernel_codegen.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/custom_extern_kernel_codegen.py new file mode 100644 index 0000000000000000000000000000000000000000..fd2df36d5aa3a6c7ad8f581250fe70ca43b1513e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/custom_extern_kernel_codegen.py @@ -0,0 +1,101 @@ +""" +Custom extern kernel codegen registry. + +This module provides a registry that maps operators to their custom codegen +implementations. This allows us to keep all custom implementations in one place +and easily extend support for both Python and C++ wrappers. + +To add a new custom implementation: +1. Create a codegen function with signature: + def my_codegen(node: ir.FallbackKernel, writeline: Callable[[str], None]) -> None: +2. Register it in CUSTOM_EXTERN_KERNEL_CODEGEN with the operator name as key + +Example: + CUSTOM_EXTERN_KERNEL_CODEGEN = { + "torch.ops.higher_order.print": CustomCodegen( + python=generate_print_python, + cpp=generate_print_cpp, # Optional + ), + } + +Usage: + codegen = CUSTOM_EXTERN_KERNEL_CODEGEN[op] + codegen.python(node, writeline) # Direct attribute access +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, TYPE_CHECKING + + +if TYPE_CHECKING: + from collections.abc import Callable + + from .. import ir + + # Type alias for codegen function signature (only used for type checking) + CodegenFunc = Callable[[ir.FallbackKernel, Callable[[str], None]], None] + + +@dataclass +class CustomCodegen: + """ + Container for custom codegen implementations. + + Attributes: + python: Codegen function for Python wrapper (optional) + cpp: Codegen function for C++ wrapper (optional) + """ + + python: Any = None + cpp: Any = None + + +def generate_print_python( + node: ir.FallbackKernel, + writeline: Callable[[str], None], +) -> None: + """ + Generate a builtin print call for the print HOP fallback (Python wrapper). + + This function generates Python code that calls the builtin print function + with format string interpolation. + + Args: + node: The FallbackKernel IR node representing the print HOP call. + writeline: A function that writes a line of code to the output buffer. + + Example generated code: + print('x = {}, y = {}'.format(buf0, buf1)) + print('x = {x}, y = {y}'.format(x=buf0, y=buf1)) + print('x = {}, y = {y}'.format(buf0, y=buf1)) + """ + codegen_args: list[str] = node.codegen_args() + codegen_kwargs: list[str] = node.codegen_kwargs() + + # First arg is the format string + if not codegen_args: + raise ValueError( + "generate_print_python requires a format string as the first positional argument" + ) + format_str: str = codegen_args[0] + + # Remaining args are positional arguments for .format() + positional_args = codegen_args[1:] + + args_str = ", ".join(positional_args + codegen_kwargs) + writeline( + f"print({format_str}.format({args_str}))" + if args_str + else f"print({format_str})" + ) + + +# Registry mapping operator names to their custom codegen implementations +# Usage: CUSTOM_EXTERN_KERNEL_CODEGEN[op_name].python(node, writeline) +CUSTOM_EXTERN_KERNEL_CODEGEN: dict[str, CustomCodegen] = { + "torch.ops.higher_order.print": CustomCodegen( + python=generate_print_python, + ), +} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutedsl/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutedsl/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f12fa963fd60c00deb9f36f9515e3e794c9529ef --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutedsl/__init__.py @@ -0,0 +1,8 @@ +# mypy: allow-untyped-defs +from .cutedsl_template import CuteDSLTemplate, CuteDSLTemplateCaller + + +__all__ = [ + "CuteDSLTemplate", + "CuteDSLTemplateCaller", +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutedsl/_cutedsl_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutedsl/_cutedsl_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..7b6898127786be07b346c235d33e17cfa48dd166 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutedsl/_cutedsl_utils.py @@ -0,0 +1,29 @@ +# mypy: disable-error-code=import-not-found +# pyrefly: ignore [import-error, missing-import] +import cutlass.cute as cute + + +@cute.jit # type: ignore[misc] +def ssa_to_indexable(ssa_value: cute.TensorSSA, dtype: str) -> cute.Numeric: + """ + Convert SSA form to indexable non-SSA form. + + Workaround for lack of gather support: SSA values cannot be used directly + as indices in tensor loads. This converts SSA → fragment → scalar for indexing. + """ + frag = cute.make_rmem_tensor(1, dtype) + frag.store(ssa_value) + return frag[0] + + +@cute.jit # type: ignore[misc] +def result_to_ssa(value: cute.Numeric, dtype: str) -> cute.TensorSSA: + """ + Convert non-SSA result back to SSA form. + + After performing operations with non-SSA values (like indexed loads), + convert the result back to SSA form for further computation. + """ + frag = cute.make_rmem_tensor(1, dtype) + frag[0] = value + return frag.load() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutedsl/cutedsl_kernel.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutedsl/cutedsl_kernel.py new file mode 100644 index 0000000000000000000000000000000000000000..175860efbaff0b569c9b59ec01168f4e02253268 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutedsl/cutedsl_kernel.py @@ -0,0 +1,613 @@ +# mypy: allow-untyped-defs +import contextlib +import dataclasses +import logging +import textwrap +from collections.abc import Callable +from typing import Any + +import sympy + +import torch +from torch._inductor import config +from torch._inductor.codegen.common import ( + CSE, + CSEVariable, + IndentedBuffer, + Kernel, + ValueRanges, +) +from torch._inductor.ir import ( + BaseView, + Buffer, + ComputedBuffer, + ExternKernel, + InputBuffer, + MutableBox, + ReinterpretView, +) +from torch._inductor.ops_handler import StoreMode +from torch._inductor.utils import OrderedSet +from torch._inductor.virtualized import V + +from ...utils import sympy_index_symbol +from .cutedsl_op_overrides import CuteDSLOpOverrides + + +# TODO setting the 'main' kernel w/ this suffix. We have 3 should probably just auto generate this +MAIN_SUFFIX = "main" + + +log = logging.getLogger(__name__) +kernel_code_log = torch._logging.getArtifactLogger(__name__, "kernel_code") + + +class CuteDSLKernelWrapper: + """Wrapper to provide .run() interface for CuteDSL kernels""" + + def __init__(self, kernel_fn: Callable[..., Any], kernel_path: str | None = None): + self.kernel_fn = kernel_fn + self.kernel_path = kernel_path + kernel_code_log.info("CuteDSL kernel path: %s", kernel_path) + + def run(self, *args, stream=None, **kwargs): + """ + Execute the CuteDSL kernel. + + Args: + *args: Arguments to pass to the kernel function + stream: CUDA stream to pass to the kernel function + **kwargs: Additional keyword arguments for the kernel + + Returns: + Result of the kernel execution + """ + return self.kernel_fn(*args, stream=stream, **kwargs) + + +@dataclasses.dataclass +class CuteDSLSubgraphInfo: + """Minimal subgraph info for CuteDSL kernels.""" + + body: IndentedBuffer + template_mask: str | None = None + template_out: str | None = None + cse: CSE[Any] | None = None + + def __post_init__(self): + self.only_copy_if_non_none_fields = ("cse",) + + def to_dict(self): + return { + field.name: getattr(self, field.name) for field in dataclasses.fields(self) + } + + +class CuteDSLTemplateKernel(Kernel): + """ + Template kernel implementation for CuteDSL (CUTLASS Python DSL). + Handles code generation and argument management for CuteDSL CUDA kernels. + Provides CuteDSL-specific functionality for tensor conversion and kernel configuration. + """ + + def __init__( + self, + kernel_name: str, + input_nodes: list[Buffer], + output_node: Buffer, + subgraphs: list[Buffer] | None = None, + ) -> None: + # Call parent Kernel constructor + super().__init__() + self.kernel_name = kernel_name + self.input_nodes = input_nodes + self.output_node = output_node + self.subgraphs = subgraphs + self.subgraph_bodies: dict[str, CuteDSLSubgraphInfo] = {} + + # Template attributes + self.body: IndentedBuffer = IndentedBuffer() + self.template_mask: str | None = None + self.template_out: str | None = None + self.template_indices: list[Any] | None = None + self.render_hooks: dict[str, Any] = {} + + # TODO Additional attributes needed by template system + self.prologue_fused_inputs: OrderedSet[str] = OrderedSet() + self.prologue_fused_inputs_preserve_zero: OrderedSet[str] = OrderedSet() + self.named_input_nodes: dict[str, Buffer] = {} + + # Create named input nodes mapping + for i, input_node in enumerate(input_nodes): + node_name = getattr(input_node, "name", f"input_{i}") + self.named_input_nodes[node_name] = input_node + + self.cse = CSE(name_prefix="tmp") + + # Track all tensor buffers added during modification processing + self.collected_tensor_buffers: list[str] = [] + + def kexpr(self, expr: sympy.Expr) -> str: + """Convert sympy expression to CuteDSL string representation.""" + return str(expr) + + def gen_imports(self) -> str: + """Generate common imports for CuteDSL templates.""" + imports = IndentedBuffer() + imports.splice( + """ + import torch + import cutlass + import cutlass.cute as cute + from cutlass.cute.runtime import from_dlpack + import cuda.bindings.driver as cuda + from cutlass._mlir.dialects import math as mlir_math + import operator + from torch._inductor.codegen.cutedsl._cutedsl_utils import ssa_to_indexable, result_to_ssa + """ + ) + return imports.getvalue() + + def gen_defines(self, **kwargs) -> str: + """Generate CuteDSL parameter definitions from kwargs, similar to Triton's gen_defines.""" + params = IndentedBuffer() + for name, val in kwargs.items(): + params.writeline(f"{name}: cutlass.Constexpr = {val}") + return params.getvalue() + + def render(self, template, **kwargs): + from torch._inductor.select_algorithm import PartialRender + + """Render the kernel using the template, returning PartialRender object with hooks.""" + # Available {{}} hooks for jinja rendering + template_env = { + "def_kernel": self.def_kernel, + "gen_defines": lambda: self.gen_defines(**kwargs), + "get_output": self.get_output, + "get_tensor_buffers": self.get_tensor_buffers, + "unpack_buffers": self.unpack_buffers, + "modification": self.modification, + "set_cute_hash": self.set_cute_hash, + } + + # Render the template with the environment and provided kwargs + rendered_code = template.render( + kernel_name=self.kernel_name, + input_nodes=self.input_nodes, + output_node=self.output_node, + **template_env, + **kwargs, + ) + + # Always prepend the common imports + imports = self.gen_imports() + full_code = imports + rendered_code + + return PartialRender(full_code, self.render_hooks) + + @contextlib.contextmanager + def set_subgraph_body(self, body_name: str): + """Set the active subgraph body for template processing.""" + assert all( + hasattr(self, field.name) + for field in dataclasses.fields(CuteDSLSubgraphInfo) + ) + old_state = { + key.name: getattr(self, key.name) + for key in dataclasses.fields(CuteDSLSubgraphInfo) + } + + if body_name not in self.subgraph_bodies: + self.subgraph_bodies[body_name] = CuteDSLSubgraphInfo( + body=IndentedBuffer(), + template_mask=None, + template_out=None, + cse=None, + ) + + subgraph = self.subgraph_bodies[body_name] + for key, value in subgraph.to_dict().items(): + if value is None and key in getattr( + subgraph, "only_copy_if_non_none_fields", () + ): + continue + setattr(self, key, value) + + try: + yield + finally: + # Save current state back to subgraph + self.subgraph_bodies[body_name] = CuteDSLSubgraphInfo( + **{ + key.name: getattr(self, key.name) + for key in dataclasses.fields(CuteDSLSubgraphInfo) + } + ) + # Restore old state + for key, value in old_state.items(): + setattr(self, key, value) + + @contextlib.contextmanager + def create_subgraph_body(self, body_name: str, *, clear_cse: bool = False): + """Create a new subgraph body for template processing.""" + assert body_name not in self.subgraph_bodies, ( + f"Subgraph body '{body_name}' already exists" + ) + new_cse = self.cse.clone() if clear_cse else None + self.subgraph_bodies[body_name] = CuteDSLSubgraphInfo( + body=IndentedBuffer(), + template_mask=None, + template_out=None, + cse=new_cse, + ) + with self.set_subgraph_body(body_name): + yield + + def _get_reinterpret_view(self, node) -> ReinterpretView | None: + """Extract or convert to ReinterpretView from a node, handling all views.""" + while isinstance(node, MutableBox): + node = node.data + if isinstance(node, BaseView): + return ExternKernel.convert_to_reinterpret_view(node) + return None + + def def_kernel(self, *argnames): + """Define kernel function signature for CuteDSL templates. + + When inputs are ReinterpretViews of the same underlying buffer (e.g., Q/K/V + from fused QKV projection), we generate separate arguments for each input + even though they share the same underlying buffer. + """ + renames = IndentedBuffer(initial_indent=1) + + # Track template input args - each input gets its own arg even if buffers are shared + self._template_input_args: list[tuple[str, Buffer]] = [] + self._seen_input_args: OrderedSet[str] = OrderedSet() + + for i, input_node in enumerate(self.input_nodes): + buf_name = input_node.get_name() + # Register with args system (may deduplicate, but we track separately) + self.args.input(buf_name) + + if i < len(argnames): + template_name = argnames[i] + arg_name = f"arg_{template_name}" + self.args.input_buffers[buf_name] = arg_name + renames.writeline(f"{template_name} = {arg_name}") + self._template_input_args.append((arg_name, input_node)) + self._seen_input_args.add(arg_name) + + if self.output_node: + self.args.output(self.output_node.get_name()) + + def hook(): + # Generate signature with template input args plus additional args (output, sizevars) + code = IndentedBuffer() + code.writeline(f"# Kernel function signature: {self.kernel_name}") + + # Start with template input args + params = [arg_name for arg_name, _ in self._template_input_args] + + # Get additional args from python_argdefs (output, sizevars, etc.) + arg_defs, _, _, _ = self.args.python_argdefs() + for arg_def in arg_defs: + if arg_def.full_name() not in self._seen_input_args: + params.append(arg_def.full_name()) + + params.append("stream") + code.writeline( + f"def {self.kernel_name}_{MAIN_SUFFIX}({', '.join(params)}):" + ) + with code.indent(): + code.splice(renames.getvalue()) + return code.getvalue() + + assert "" not in self.render_hooks + # Placeholder-based rendering: hook will be called when template encounters "" + self.render_hooks[""] = hook + return "" + + def get_output(self): + """Get the actual argument name for the output buffer.""" + assert self.output_node, "Output node must exist to get output buffer name" + buf_name = self.output_node.get_name() + output = self.args.output_buffers.get(buf_name, None) + if output is None: + raise ValueError(f"Output buffer '{buf_name}' not found in args") + return output + + def set_cute_hash(self, func_name: str, suffix: str = ""): + """Generate code to set __cute_hash__ on a codegen function. + + This allows hash_callable in flash_attn to skip expensive runtime hashing + for Inductor-generated functions. The hash is based on the kernel name + which already contains a unique hash suffix. + """ + hash_value = f"{self.kernel_name}_{suffix}" if suffix else self.kernel_name + return f'{func_name}.__cute_hash__ = "{hash_value}"' + + def get_tensor_buffers(self): + """Get list of tensor buffer names that were collected during modifications.""" + return self.collected_tensor_buffers + + def unpack_buffers(self, buffer_list_name: str, *, indent_width: int = 4): + """Generate buffer unpacking code via render hook.""" + + def hook(): + tensor_buffers = self.get_tensor_buffers() + if not tensor_buffers: + return "" + + # Generate unpacking assignments: in_ptr4 = buffers[0], etc. + unpacking_lines = [] + for i, buffer_name in enumerate(tensor_buffers): + # pyrefly: ignore [bad-argument-type] + unpacking_lines.append(f"{buffer_name} = {buffer_list_name}[{i}]") + + indent = " " * indent_width + return "\n" + indent + ("\n" + indent).join(unpacking_lines) + + # Register the hook and return placeholder + placeholder = "" + # TODO: I think double invoking is fine for this specific hook + # assert placeholder not in self.render_hooks + self.render_hooks[placeholder] = hook + return placeholder + + def call_kernel(self, name: str, node=None): + """Call the kernel function. Simplified version of TritonTemplateKernel.call_kernel. + + For inputs that are ReinterpretViews (e.g., Q/K/V slices from fused QKV), + we generate reinterpret_tensor() calls to properly handle the views. + """ + wrapper = V.graph.wrapper_code + + # Build call args matching the signature generated in `def_kernel` + call_args = [] + arg_types = [] + + for _, input_node in self._template_input_args: + reinterpret_view = self._get_reinterpret_view(input_node) + if reinterpret_view is not None: + call_args.append(reinterpret_view.codegen_reference()) + else: + call_args.append(input_node.get_name()) + arg_types.append(V.graph.get_dtype(input_node.get_name())) + + # Add additional args from python_argdefs (output, sizevars, ..) + orig_arg_defs, orig_call_args, _, orig_arg_types = self.args.python_argdefs() + for arg_def, call_arg, arg_type in zip( + orig_arg_defs, orig_call_args, orig_arg_types + ): + # dedupe + if arg_def.full_name() not in self._seen_input_args: + call_args.append(call_arg) + arg_types.append(arg_type) + + # TODO this karg really should not be called `triton` + wrapper.generate_kernel_call(name, call_args, triton=True, arg_types=arg_types) + + def _get_subgraph(self, subgraph_number: int): + """Get subgraph by number for modification processing.""" + assert isinstance(subgraph_number, int) + assert isinstance(self.subgraphs, list) + assert subgraph_number < len(self.subgraphs), ( + f"Invalid subgraph number provided to create_modification, {subgraph_number} must be < {len(self.subgraphs)}" + ) + assert self.body.getvalue() == "", ( + "Body should be clear before adding a modification" + ) + return self.subgraphs[subgraph_number] + + def modification( + self, + subgraph_number: int, + output_name: str | None, + mask: str | None = None, + **fixed_inputs, + ) -> str: + """Generate CuteDSL code for a subgraph modification.""" + # Find unique name to avoid collisions between multiple modifications of same subgraph + num = 0 + while f"mod_{subgraph_number}_{num}" in self.subgraph_bodies: + num += 1 + + with self.create_subgraph_body(f"mod_{subgraph_number}_{num}", clear_cse=True): + subgraph = self._get_subgraph(subgraph_number) + modification_handler = ModificationWrapperCuteDSL( + self, subgraph_number, fixed_inputs, mask + ) + with V.set_kernel_handler(self), V.set_ops_handler(modification_handler): + assert isinstance(subgraph, (ComputedBuffer, list)), ( + f"Expected ComputedBuffer or List[ComputedBuffer], got {type(subgraph)}" + ) + + if isinstance(subgraph, list): + raise NotImplementedError( + "Scatter graphs are not supported for CuteDSL" + ) + + if isinstance(subgraph.data, InputBuffer): + # grad_score_mod can be InputBuffers + out = subgraph.data.make_loader()(()) + else: + # Inline a pointwise lowering into the template + out = subgraph.data.inner_fn(()) + + if output_name is not None: + assert out is not None, ( + f"Expected computation result for named output {output_name}" + ) + self.body.writeline(f"{output_name} = {out.value}") + else: + # Side-effect only: no output assignment (currently only for scatter operations) + raise NotImplementedError( + "Side-effect only modifications not yet supported for CuteDSL" + ) + + # Add Buffers that were added during modification + self.collected_tensor_buffers.extend(modification_handler.tensor_buffers) + + return self.body.getvalue() + + +class ModificationWrapperCuteDSL(V.WrapperHandler): # type: ignore[name-defined] + """ + Wrapper handler that enables CuteDSL code generation during subgraph modifications. + + This class sits between the PyTorch IR and CuteDSL code generation, providing: + 1. Operation substitution: converts PyTorch ops to CuteDSL equivalents via CuteDSLOpOverrides + 2. Placeholder handling: resolves fixed_inputs during template processing + 3. Limited operation support: currently restricted to pointwise operations + + """ + + def __init__( + self, + kernel, + subgraph_number: int, + fixed_inputs: dict[str, Any], + mask: str | None, + ): + cutedsl_ops = CuteDSLOpOverrides() + super().__init__(cutedsl_ops) + self.name = f"CuteDSLPlaceholderSubstitution_{subgraph_number}" + self.kernel = kernel + self.fixed_inputs = fixed_inputs + self.mask = mask + # Track tensor buffers that get added during modification processing + self.tensor_buffers: list[str] = [] + + def _get_input_dtype(self, name: str) -> torch.dtype: + """Get the dtype for an input from the kernel's named_input_nodes.""" + if name in self.kernel.named_input_nodes: + return self.kernel.named_input_nodes[name].dtype + # TODO: Fallback for common dimension names - should be replaced with proper dtype tracking + return torch.float32 if name not in ("b", "h", "m", "n") else torch.int32 + + def load(self, name: str, index: sympy.Expr): + """Handle loading from tensor or fixed(template args) input for CuteDSL.""" + from torch._inductor.kernel.flex.flex_flash_attention import HierarchicalIndex + + if name not in self.fixed_inputs: + var = self._add_kernel_input(name) + buffer = V.graph.get_buffer(name) + var_dtype = buffer.dtype + + cute_dtype = CuteDSLOpOverrides.TORCH_TO_CUTE_DTYPE.get( + var_dtype, "cutlass.Float32" + ) + idx_vars = [ + self._emit_scalar_fragment( + self.kernel.kexpr(self.kernel.rename_indexing(dim_index)), + "cutlass.Int32", + torch.int32, + ) + for dim_index in ( + index.args if isinstance(index, HierarchicalIndex) else (index,) + ) + ] + + val_frag = self.kernel.cse.newvar(dtype=var_dtype) + self.kernel.body.writeline( + f"{val_frag} = cute.make_rmem_tensor(1, {cute_dtype})" + ) + self.kernel.body.writeline( + f"{val_frag}[0] = ({var}[{', '.join(idx_vars)}])" + ) + + final_expr = f"{val_frag}.load()" + + if ( + var_dtype in (torch.float16, torch.bfloat16) + and config.triton.codegen_upcast_to_fp32 + ): + final_expr = f"({final_expr}).to(cutlass.Float32)" + var_dtype = torch.float32 + + out = self.kernel.cse.generate( + self.kernel.body, + final_expr, + dtype=var_dtype, + bounds=ValueRanges.unknown(), + ) + return out + + value = self.fixed_inputs[name] + dtype = self._get_input_dtype(name) + + return self.kernel.cse.generate( + self.kernel.body, value, bounds=ValueRanges.unknown(), dtype=dtype + ) + + def _emit_scalar_fragment( + self, expr_str: str, cute_dtype: str, torch_dtype: torch.dtype + ) -> str: + """ + Convert expression to indexable scalar for tensor loads. + + Workaround for lack of gather support: SSA values cannot be used directly + as indices in tensor loads. This generates code to convert SSA → indexable + scalar. Compile-time integer constants are already indexable and are + returned directly without the SSA round-trip. + """ + # Constant integer expressions (e.g. sympy-folded offsets like "0") + # are already valid indices — skip the ssa_to_indexable round-trip + # which only accepts TensorSSA, not bare Python ints. + if expr_str.lstrip("-").isdigit(): + return expr_str + + result = self.kernel.cse.newvar(dtype=torch_dtype) + self.kernel.body.writeline( + f"{result} = ssa_to_indexable({expr_str}, {cute_dtype})" + ) + return str(result) + + def indirect_indexing(self, index_var: str, size, check, wrap_neg=True): + """Convert index variable to symbolic form.""" + return sympy_index_symbol(str(index_var)) + + # pyrefly: ignore [bad-override] + def store( + self, name: str, index: sympy.Expr, value: CSEVariable, mode: StoreMode = None + ) -> str: + raise NotImplementedError( + "Store operations not supported - CuteDSL limited to read-only operations" + ) + + def _add_kernel_input(self, name: str): + """Add name as input to kernel and return input ref.""" + # Get the remapped name that will be used in the kernel + remapped_name = self.kernel.args.input(name) + # Track the remapped name for later collection + if remapped_name not in self.tensor_buffers: + self.tensor_buffers.append(remapped_name) + return remapped_name + + def _process_indexing(self, index): + """Process and rename indexing, adding symbols as kernel inputs.""" + renamed = self.kernel.rename_indexing(index) + return self.kernel.kexpr(renamed) + + def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: + try: + return getattr(self._inner, name)(*args, **kwargs) + except NotImplementedError as e: + bar = "=" * 80 + msg = textwrap.dedent(f""" + {bar} + UNSUPPORTED CUTEDSL OPERATION: '{name}' + {bar} + This operation is not yet implemented in Inductor. + + Please open an issue at: https://github.com/pytorch/pytorch/issues + with the following information: + + Operation: {name} + Args: {args!r} + Kwargs: {kwargs!r} + + Title your issue: [CuteDSL] Missing operation: {name} + {bar} + """).strip() + raise NotImplementedError(msg) from e diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutedsl/cutedsl_op_overrides.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutedsl/cutedsl_op_overrides.py new file mode 100644 index 0000000000000000000000000000000000000000..ebfb2e0c085070c6c4734e8b13063ac9994c888f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutedsl/cutedsl_op_overrides.py @@ -0,0 +1,528 @@ +# mypy: allow-untyped-defs +""" +CuteDSL-specific operation overrides for pointwise operations. + +This module provides CuteDSL implementations of common operations used in +template kernels, particularly for flex attention modifications. +""" + +import math + +import sympy + +import torch +from torch._inductor.codegen.common import CSEVariable, OpOverrides +from torch._inductor.virtualized import OpsValue, V +from torch.utils._sympy.value_ranges import ValueRanges + + +CuteDSLArg = CSEVariable | str | bool | float | int + + +def upcast_compute_type(dtype: torch.dtype) -> torch.dtype: + """Maybe upcast [b]float16 to float32""" + if dtype in (torch.float16, torch.bfloat16): + return torch.float32 + return dtype + + +class CuteDSLOpOverrides(OpOverrides): + """ + CuteDSL-specific operation overrides that generate code using CuteDSL syntax. + + CuteDSL TensorSSA objects have built-in operator overloads (__add__, __mul__, etc.) + and math functions (cute.math.exp, cute.math.sqrt, etc.) + """ + + TORCH_TO_CUTE_DTYPE = { + torch.float16: "cutlass.Float16", + torch.bfloat16: "cutlass.BFloat16", + torch.float32: "cutlass.Float32", + torch.float64: "cutlass.Float64", + torch.int8: "cutlass.Int8", + torch.int16: "cutlass.Int16", + torch.int32: "cutlass.Int32", + torch.int64: "cutlass.Int64", + torch.uint8: "cutlass.Uint8", + torch.bool: "cutlass.Boolean", + torch.float8_e4m3fn: "cutlass.Float8E4M3FN", + torch.float8_e5m2: "cutlass.Float8E5M2", + } + + # Math constants + LOG2_E = 1.4426950408889634 # 1/ln(2) for converting natural exp to base-2 exp + + @staticmethod + def _get_cse_var(arg: CuteDSLArg) -> CSEVariable | None: + """Extract CSEVariable from arg if it's a tensor (either direct or wrapped in OpsValue).""" + if isinstance(arg, CSEVariable): + return arg + if isinstance(arg, OpsValue) and isinstance(arg.value, CSEVariable): + return arg.value + return None + + @staticmethod + def _as_expr(arg: CuteDSLArg) -> str: + cse_var = CuteDSLOpOverrides._get_cse_var(arg) + if cse_var is not None: + return str(cse_var) + return str(arg) + + @staticmethod + def _node_tensor_flags() -> tuple[bool, bool] | None: + node = V.current_node + if not isinstance(node, torch.fx.Node) or len(node.args) < 2: + return None + + def _is_tensor(raw_arg: object) -> bool: + if hasattr(raw_arg, "meta"): + val = raw_arg.meta.get("val") + return isinstance(val, torch.Tensor) and val.ndim > 0 + return False + + return _is_tensor(node.args[0]), _is_tensor(node.args[1]) + + @staticmethod + def _ensure_tensor_ssa( + arg: CuteDSLArg, template_tensor: CuteDSLArg, *, is_tensor: bool + ) -> str: + """ + Convert scalar arguments to TensorSSA using cute.full_like if needed. + + Args: + arg: The argument to check (CSEVariable for tensors, str for scalars, or OpsValue wrapper) + template_tensor: A tensor argument to use as template for full_like + + Returns: + String representation suitable for CuteDSL operations + """ + arg_expr = CuteDSLOpOverrides._as_expr(arg) + if is_tensor: + return arg_expr + template_expr = CuteDSLOpOverrides._as_expr(template_tensor) + return f"cute.full_like({template_expr}, {arg_expr})" + + @staticmethod + def _extract_dtype_and_bounds( + *args: CuteDSLArg, + ) -> tuple[torch.dtype | None, ValueRanges[sympy.Expr]]: + """Extract dtype and bounds from CSEVariable arguments (including OpsValue wrappers).""" + for arg in args: + cse_var = CuteDSLOpOverrides._get_cse_var(arg) + if cse_var is not None: + return cse_var.dtype, cse_var.bounds + return None, ValueRanges.unknown() + + @staticmethod + def _apply_binary_op(a: CuteDSLArg, b: CuteDSLArg, op_format: str) -> CuteDSLArg: + """ + Apply a binary operation with automatic scalar-to-tensor conversion. + + CuteDSL requires both operands to be TensorSSA objects for tensor operations. + This helper automatically converts scalar arguments to TensorSSA using + cute.full_like when at least one argument is a tensor (CSEVariable or OpsValue). + + Args: + a: First operand (CSEVariable for tensors, str for scalars, or OpsValue wrapper) + b: Second operand (CSEVariable for tensors, str for scalars, or OpsValue wrapper) + op_format: Format string with {a} and {b} placeholders for the operation + + Returns: + CSEVariable if at least one operand is a tensor, otherwise string + """ + a_cse = CuteDSLOpOverrides._get_cse_var(a) + b_cse = CuteDSLOpOverrides._get_cse_var(b) + + node_flags = CuteDSLOpOverrides._node_tensor_flags() + if node_flags is not None: + a_is_tensor, b_is_tensor = node_flags + else: + a_is_tensor = a_cse is not None + b_is_tensor = b_cse is not None + + tensor_arg = a if a_is_tensor else (b if b_is_tensor else None) + if tensor_arg is None: + tensor_arg = a_cse or b_cse + + if tensor_arg is not None: + if a_cse is None and b_cse is None: + return op_format.format( + a=CuteDSLOpOverrides._as_expr(a), + b=CuteDSLOpOverrides._as_expr(b), + ) + + a_ssa = CuteDSLOpOverrides._ensure_tensor_ssa( + a, tensor_arg, is_tensor=a_is_tensor + ) + b_ssa = CuteDSLOpOverrides._ensure_tensor_ssa( + b, tensor_arg, is_tensor=b_is_tensor + ) + result_expr = op_format.format(a=a_ssa, b=b_ssa) + + dtype, bounds = CuteDSLOpOverrides._extract_dtype_and_bounds(a, b) + expected = CuteDSLOpOverrides._expected_tensor_val() + if dtype is None: + dtype = expected.dtype if expected is not None else torch.int32 + if a_cse is not None: + shape = a_cse.shape + elif b_cse is not None: + shape = b_cse.shape + else: + shape = tuple(expected.size()) if expected is not None else None + + # Create and return CSEVariable using CSE generation for caching + return V.kernel.cse.generate( + V.kernel.body, result_expr, bounds=bounds, dtype=dtype, shape=shape + ) + + return op_format.format(a=a, b=b) + + @staticmethod + def _expected_tensor_val() -> torch.Tensor | None: + """Return the fake-tensor value from the current FX node's metadata, if any.""" + node = V.current_node + if not isinstance(node, torch.fx.Node): + return None + val = node.meta.get("val") + return val if isinstance(val, torch.Tensor) else None + + @staticmethod + def _cast_expr(expr: str, dtype: torch.dtype) -> str: + cute_type = CuteDSLOpOverrides.TORCH_TO_CUTE_DTYPE.get(dtype) + if cute_type is None: + return expr + return f"{cute_type}({expr})" + + @staticmethod + def _apply_unary_op(x: CuteDSLArg, op_format: str) -> CuteDSLArg: + """ + Apply a unary operation, returning CSEVariable if input is a tensor. + + Args: + x: Input operand (CSEVariable for tensors, str for scalars, or OpsValue wrapper) + op_format: Format string with {x} placeholder for the operation + + Returns: + CSEVariable if input is a tensor, otherwise string + """ + cse_var = CuteDSLOpOverrides._get_cse_var(x) + if cse_var is not None: + result_expr = op_format.format(x=str(cse_var)) + return V.kernel.cse.generate( + V.kernel.body, result_expr, bounds=cse_var.bounds, dtype=cse_var.dtype + ) + + return op_format.format(x=x) + + @staticmethod + def constant(value: bool | float | int, dtype: torch.dtype) -> str: + """Generate CuteDSL constant representation.""" + if value == float("-inf"): + return "float('-inf')" + elif value == float("inf"): + return "float('inf')" + elif math.isnan(value): + return "float('nan')" + return repr(value) + + @staticmethod + def add(a: CuteDSLArg, b: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._apply_binary_op(a, b, "({a} + {b})") + + @staticmethod + def mul(a: CuteDSLArg, b: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._apply_binary_op(a, b, "({a} * {b})") + + @staticmethod + def sub(a: CuteDSLArg, b: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._apply_binary_op(a, b, "({a} - {b})") + + @staticmethod + def truediv(a: CuteDSLArg, b: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._apply_binary_op(a, b, "({a} / {b})") + + @staticmethod + def floordiv(a: CuteDSLArg, b: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._apply_binary_op(a, b, "({a} // {b})") + + @staticmethod + def mod(a: CuteDSLArg, b: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._apply_binary_op(a, b, "({a} % {b})") + + @staticmethod + def remainder(a, b): + return CuteDSLOpOverrides._apply_binary_op(a, b, "({a} % {b})") + + @staticmethod + # pyrefly: ignore [bad-override] + def exp(x: CuteDSLArg) -> CuteDSLArg: + """Exponential using CuteDSL cute.math.exp2 with log2(e) scaling.""" + if CuteDSLOpOverrides._get_cse_var(x) is None: + x = CuteDSLOpOverrides._cast_expr(str(x), torch.float32) + return CuteDSLOpOverrides._apply_unary_op( + x, f"cute.math.exp2({{x}} * {CuteDSLOpOverrides.LOG2_E})" + ) + + @staticmethod + # pyrefly: ignore [bad-override] + def sqrt(x: CuteDSLArg) -> CuteDSLArg: + """Square root using CuteDSL cute.math.sqrt function.""" + return CuteDSLOpOverrides._apply_unary_op(x, "cute.math.sqrt({x})") + + @staticmethod + # pyrefly: ignore [bad-override] + def log(x: CuteDSLArg) -> CuteDSLArg: + """Natural logarithm using CuteDSL cute.math.log function.""" + return CuteDSLOpOverrides._apply_unary_op(x, "cute.math.log({x})") + + @staticmethod + # pyrefly: ignore [bad-override] + def cos(x: CuteDSLArg) -> CuteDSLArg: + """Cosine using CuteDSL cute.math.cos function.""" + return CuteDSLOpOverrides._apply_unary_op(x, "cute.math.cos({x})") + + @staticmethod + # pyrefly: ignore [bad-override] + def sin(x: CuteDSLArg) -> CuteDSLArg: + """Sine using CuteDSL cute.math.sin function.""" + return CuteDSLOpOverrides._apply_unary_op(x, "cute.math.sin({x})") + + @staticmethod + # pyrefly: ignore [bad-override] + def erf(x: CuteDSLArg) -> CuteDSLArg: + """Error function using CuteDSL cute.math.erf function.""" + return CuteDSLOpOverrides._apply_unary_op(x, "cute.math.erf({x})") + + @staticmethod + # pyrefly: ignore [bad-override] + def sigmoid(x: CuteDSLArg) -> CuteDSLArg: + """Sigmoid with fp32 compute and cast-back to expected output dtype.""" + x_cse = CuteDSLOpOverrides._get_cse_var(x) + if x_cse is not None: + x_fp32: CuteDSLArg = CuteDSLOpOverrides.to_dtype( + x_cse, + torch.float32, + use_compute_types=False, + ) + else: + x_fp32 = CuteDSLOpOverrides._cast_expr(str(x), torch.float32) + + result = CuteDSLOpOverrides._apply_unary_op( + x_fp32, + f"(1.0 / (1.0 + cute.math.exp2(-{{x}} * {CuteDSLOpOverrides.LOG2_E})))", + ) + + expected = CuteDSLOpOverrides._expected_tensor_val() + expected_dtype = expected.dtype if expected is not None else None + if expected_dtype is not None and expected_dtype != torch.float32: + result_cse = CuteDSLOpOverrides._get_cse_var(result) + if result_cse is not None: + return CuteDSLOpOverrides.to_dtype( + result_cse, + expected_dtype, + use_compute_types=False, + ) + return CuteDSLOpOverrides._cast_expr(str(result), expected_dtype) + return result + + @staticmethod + def _minmax(a: CuteDSLArg, b: CuteDSLArg, *, op: str) -> CuteDSLArg: + tensor_arg = CuteDSLOpOverrides._get_cse_var( + a + ) or CuteDSLOpOverrides._get_cse_var(b) + if tensor_arg is not None: + return CuteDSLOpOverrides._apply_binary_op( + a, b, f"cute.where(({{a}}) {op} ({{b}}), {{a}}, {{b}})" + ) + + lhs = str(a) + rhs = str(b) + expected = CuteDSLOpOverrides._expected_tensor_val() + if expected is not None: + lhs = CuteDSLOpOverrides._cast_expr(lhs, expected.dtype) + rhs = CuteDSLOpOverrides._cast_expr(rhs, expected.dtype) + return f"({lhs} if {lhs} {op} {rhs} else {rhs})" + + @staticmethod + # pyrefly: ignore [bad-override] + def maximum(a: CuteDSLArg, b: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._minmax(a, b, op=">") + + @staticmethod + # pyrefly: ignore [bad-override] + def minimum(a: CuteDSLArg, b: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._minmax(a, b, op="<") + + @staticmethod + # pyrefly: ignore [bad-override] + def where( + condition: CuteDSLArg, + a: CuteDSLArg, + b: CuteDSLArg, + ) -> CuteDSLArg: + """Conditional selection - handles CSEVariable, OpsValue, and string inputs.""" + a_cse = CuteDSLOpOverrides._get_cse_var(a) + b_cse = CuteDSLOpOverrides._get_cse_var(b) + cond_cse = CuteDSLOpOverrides._get_cse_var(condition) + tensor_arg = a_cse or b_cse or cond_cse + + if tensor_arg is not None: + a_ssa = CuteDSLOpOverrides._ensure_tensor_ssa( + a, tensor_arg, is_tensor=a_cse is not None + ) + b_ssa = CuteDSLOpOverrides._ensure_tensor_ssa( + b, tensor_arg, is_tensor=b_cse is not None + ) + cond_ssa = CuteDSLOpOverrides._ensure_tensor_ssa( + condition, tensor_arg, is_tensor=cond_cse is not None + ) + result_expr = f"cute.where({cond_ssa}, {a_ssa}, {b_ssa})" + + dtype, bounds = CuteDSLOpOverrides._extract_dtype_and_bounds( + a, b, condition + ) + + return V.kernel.cse.generate( + V.kernel.body, result_expr, bounds=bounds, dtype=dtype + ) + + return f"cute.where({condition}, {a}, {b})" + + @staticmethod + def pow(a: CuteDSLArg, b: CuteDSLArg): + return CuteDSLOpOverrides._apply_binary_op(a, b, "({a} ** {b})") + + @staticmethod + # pyrefly: ignore [bad-override] + def abs(x: CuteDSLArg) -> CuteDSLArg: + """Absolute value using CuteDSL cute.math.abs function.""" + if isinstance(x, CSEVariable): + x_dtype = x.dtype + elif isinstance(x, OpsValue) and isinstance(x.value, CSEVariable): + x_dtype = x.value.dtype + else: + x_dtype = torch.float32 + + abs_op = ( + "mlir_math.absf" + if x_dtype in (torch.float16, torch.bfloat16, torch.float32) + else "mlir_math.absi" + ) + return CuteDSLOpOverrides._apply_unary_op( + x, + f"cute.TensorSSA({abs_op}({{x}}), {{x}}.shape, {{x}}.dtype)", + ) + + @staticmethod + def neg(x: CuteDSLArg) -> CuteDSLArg: + """Negation for both TensorSSA and scalar-like expressions.""" + # TensorSSA path: avoid relying on __neg__ directly due upstream issue. + if CuteDSLOpOverrides._get_cse_var(x) is not None: + return CuteDSLOpOverrides._apply_unary_op( + x, "cute.TensorSSA(-{x}, {x}.shape, {x}.dtype)" + ) + # Scalar path: shape/dtype attributes are unavailable. + return CuteDSLOpOverrides._apply_unary_op(x, "(-{x})") + + @staticmethod + def to_dtype( + x: CuteDSLArg, dtype: torch.dtype, src_dtype=None, use_compute_types=True + ) -> CuteDSLArg: + """Type conversion using CuteDSL TensorSSA.to(Type[Numeric]). + + Maps torch dtypes to cutlass.cute.typing numeric types and emits + `{x}.to(cute.typing.)`. + + Raises NotImplementedError for unsigned integer and unsupported dtypes. + """ + if use_compute_types: + dtype = upcast_compute_type(dtype) + + cute_type = CuteDSLOpOverrides.TORCH_TO_CUTE_DTYPE.get(dtype) + if cute_type is None: + raise NotImplementedError( + f"CuteDSL dtype cast not implemented for torch dtype: {dtype}" + ) + + if isinstance(x, CSEVariable): + result_expr = f"{str(x)}.to({cute_type})" + return V.kernel.cse.generate( + V.kernel.body, result_expr, bounds=x.bounds, dtype=dtype + ) + + return f"{x}.to({cute_type})" + + @staticmethod + def tanh(x0: CuteDSLArg) -> CuteDSLArg: + """Hyperbolic tangent using CuteDSL cute.math.tanh function.""" + return CuteDSLOpOverrides._apply_unary_op(x0, "cute.math.tanh({x})") + + # Logical operations + @staticmethod + def logical_and(x0: CuteDSLArg, x1: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._apply_binary_op(x0, x1, "({a} & {b})") + + @staticmethod + def logical_or(x0: CuteDSLArg, x1: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._apply_binary_op(x0, x1, "({a} | {b})") + + # Bitwise operations (override parent class to properly CSE) + @staticmethod + # pyrefly: ignore [bad-override] + def bitwise_and(x: CuteDSLArg, y: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._apply_binary_op(x, y, "({a} & {b})") + + @staticmethod + # pyrefly: ignore [bad-override] + def bitwise_or(x: CuteDSLArg, y: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._apply_binary_op(x, y, "({a} | {b})") + + @staticmethod + # pyrefly: ignore [bad-override] + def bitwise_xor(x: CuteDSLArg, y: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._apply_binary_op(x, y, "({a} ^ {b})") + + @staticmethod + # pyrefly: ignore [bad-override] + def bitwise_not(x: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._apply_unary_op(x, "(~{x})") + + @staticmethod + # pyrefly: ignore [bad-override] + def bitwise_left_shift(x: CuteDSLArg, y: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._apply_binary_op(x, y, "({a} << {b})") + + @staticmethod + # pyrefly: ignore [bad-override] + def bitwise_right_shift(x: CuteDSLArg, y: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._apply_binary_op(x, y, "({a} >> {b})") + + @staticmethod + def logical_not(a): + """Logical NOT.""" + return CuteDSLOpOverrides._apply_unary_op(a, "({x} == 0)") + + # Comparison operations + @staticmethod + def eq(a: CuteDSLArg, b: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._apply_binary_op(a, b, "operator.eq({a}, {b})") + + @staticmethod + def ne(a: CuteDSLArg, b: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._apply_binary_op(a, b, "operator.ne({a}, {b})") + + @staticmethod + def lt(a: CuteDSLArg, b: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._apply_binary_op(a, b, "operator.lt({a}, {b})") + + @staticmethod + def le(a: CuteDSLArg, b: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._apply_binary_op(a, b, "operator.le({a}, {b})") + + @staticmethod + def gt(a: CuteDSLArg, b: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._apply_binary_op(a, b, "operator.gt({a}, {b})") + + @staticmethod + def ge(a: CuteDSLArg, b: CuteDSLArg) -> CuteDSLArg: + return CuteDSLOpOverrides._apply_binary_op(a, b, "operator.ge({a}, {b})") diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutedsl/cutedsl_scheduling.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutedsl/cutedsl_scheduling.py new file mode 100644 index 0000000000000000000000000000000000000000..4fc1089a4082acc02f4b039f2fda9c0a726648d1 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutedsl/cutedsl_scheduling.py @@ -0,0 +1,141 @@ +# mypy: allow-untyped-defs +import hashlib +import logging +from collections.abc import Sequence +from typing import cast + +from torch._inductor.utils import Placeholder +from torch.utils._ordered_set import OrderedSet + +from ... import config +from ...codecache import code_hash, get_path +from ...ir import CuteDSLTemplateBuffer +from ...scheduler import ( + BaseSchedulerNode, + BaseScheduling, + FusedSchedulerNode, + SchedulerNode, +) +from ...select_algorithm import PartialRender +from ...utils import get_fused_kernel_name, get_kernel_metadata +from ...virtualized import V +from ..common import BackendFeature, IndentedBuffer + + +log = logging.getLogger(__name__) + + +class CuteDSLScheduling(BaseScheduling): + """ + Scheduling implementation for CuteDSL (CUTLASS Python DSL) kernels. + This class is intended to be used in combination with other schedulers, + and delegated to by CUDACombinedScheduling. + """ + + @classmethod + def get_backend_features(cls, device) -> OrderedSet[BackendFeature]: + return OrderedSet() + + @staticmethod + def is_cutedsl_template(node: BaseSchedulerNode) -> bool: + """Check if a node is a CuteDSL template.""" + return isinstance(node, SchedulerNode) and isinstance( + node.node, CuteDSLTemplateBuffer + ) + + def is_cutedsl_fused_template(self, node: BaseSchedulerNode) -> bool: + """Check if a node is a fused CuteDSL template.""" + return isinstance(node, FusedSchedulerNode) and self.is_cutedsl_template(node) + + def can_fuse_vertical( + self, node1: BaseSchedulerNode, node2: BaseSchedulerNode + ) -> bool: + """ + TODO CuteDSL doesn't support vertical fusion yet. + This could be extended in the future for epilogue fusion. + """ + return False + + def define_kernel(self, src_code_str: str, node_schedule) -> str: + """Produce the kernel string + Args: + src_code_str: The finalized kernel code string + node_schedule: List of nodes in the schedule + + Note: + This is a little weird since async_compile.cutedsl() has to write the string to + a file in order to cute compile it. Feels bad to have two... + """ + wrapper = V.graph.wrapper_code + + # Use the string as the key for caching + if src_code_str in wrapper.src_to_kernel: + kernel_name = wrapper.src_to_kernel[src_code_str] + else: + fused_name = ( + get_fused_kernel_name(node_schedule, config.triton.descriptive_names) + if config.triton.descriptive_names + else "" + ) + + kernel_hash = hashlib.sha256(src_code_str.encode("utf-8")).hexdigest()[:8] + if fused_name == "fused": + kernel_name = f"cutedsl_{kernel_hash}" + else: + kernel_name = f"cutedsl_{fused_name}_{kernel_hash}" + wrapper.src_to_kernel[src_code_str] = kernel_name + src_code_str = src_code_str.replace( + str(Placeholder.KERNEL_NAME), kernel_name + ) + + _, _, kernel_path = get_path(code_hash(src_code_str), "py") + + compile_wrapper = IndentedBuffer() + compile_wrapper.writeline(f"async_compile.cutedsl({kernel_name!r}, r'''") + compile_wrapper.splice(src_code_str, strip=True) + compile_wrapper.writeline("''')") + + metadata_comment = f"# kernel path: {kernel_path}" + origins, detailed_origins = get_kernel_metadata(node_schedule, wrapper) + metadata_comment += "\n" + origins + "\n" + detailed_origins + wrapper.define_kernel( + kernel_name, compile_wrapper.getvalue(), metadata_comment + ) + return kernel_name + + def codegen_template( + self, + template_node: BaseSchedulerNode, + epilogue_nodes: Sequence[BaseSchedulerNode], + prologue_nodes: Sequence[BaseSchedulerNode], + ): + """ + Codegen a CuteDSL template. Currently doesn't support fusion. + """ + assert self.is_cutedsl_template(template_node), ( + "Template node passed to CuteDSLScheduling.codegen_template must be a " + "SchedulerNode that wraps a CuteDSLTemplateBuffer" + ) + # TODO remove when supported + assert not epilogue_nodes, "CuteDSL doesn't support epilogue fusion yet" + assert not prologue_nodes, "CuteDSL doesn't support prologue fusion yet" + + template_node = cast(SchedulerNode, template_node) + ctb: CuteDSLTemplateBuffer = cast(CuteDSLTemplateBuffer, template_node.node) + + kernel, render = ctb.make_kernel_render(ctb) # type: ignore[misc] + template_node.mark_run() + src_code = render() + # Finalize PartialRender if needed + if isinstance(src_code, PartialRender): + src_code_str = src_code.finalize_all() + else: + src_code_str = src_code + + with V.set_kernel_handler(kernel): + node_schedule = [template_node] + kernel_name = self.define_kernel(src_code_str, node_schedule) + self.codegen_comment(node_schedule, kernel_name) + kernel.call_kernel(kernel_name, ctb) + V.graph.removed_buffers |= kernel.removed_buffers + self.free_buffers_in_scheduler() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutedsl/cutedsl_template.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutedsl/cutedsl_template.py new file mode 100644 index 0000000000000000000000000000000000000000..17f681a302730d770c32b4f9045155867eebf7ee --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutedsl/cutedsl_template.py @@ -0,0 +1,212 @@ +# mypy: allow-untyped-defs +import functools +import itertools +from collections.abc import Iterable +from typing import Any +from unittest.mock import patch + +from torch._inductor.utils import Placeholder +from torch._inductor.virtualized import V +from torch._logging import getArtifactLogger + +from ...autotune_process import CuteDSLBenchmarkRequest, TensorMeta +from ...ir import Buffer, ChoiceCaller, CuteDSLTemplateBuffer, IRNode, Layout, TensorBox +from ..common import KernelTemplate +from .cutedsl_kernel import CuteDSLTemplateKernel + + +log = getArtifactLogger(__name__, "output_code") + + +class CuteDSLTemplate(KernelTemplate): + """Template for generating CuteDSL (CUTLASS Python DSL) kernels.""" + + kernel_type: type[Any] = CuteDSLTemplateKernel + index_counter = itertools.count() + all_templates: dict[str, "CuteDSLTemplate"] = {} + + def __init__( + self, + name: str, + source: str, + subgraph_fn: Any | None = None, + mask_fn: Any | None = None, + ) -> None: + super().__init__(name) + self.source = source + self.subgraph_fn = subgraph_fn + self.mask_fn = mask_fn + self.template = CuteDSLTemplate._template_from_string(source) + assert name not in self.all_templates, f"duplicate template name, {name}" + CuteDSLTemplate.all_templates[name] = self + + @staticmethod + @functools.lru_cache(None) + # pyrefly: ignore [bad-override] + def _template_from_string(source: str) -> Any: + return KernelTemplate._template_from_string(source) + + def maybe_append_choice( + self, choices: list[Any], **kwargs: Any + ) -> NotImplementedError | None: + """ + Maybe generates a new ChoiceCaller and appends it into existing choices. + Returns None if success, otherwise returns the error. + """ + try: + choices.append(self.generate(**kwargs)) + return None + except NotImplementedError as e: + log.debug("CuteDSL template choice generation failed: %s", e) # noqa: G200 + return e + except Exception as e: + log.debug("CuteDSL template choice generation error: %s", e) # noqa: G200 + return NotImplementedError(f"CuteDSL template failed: {e}") + + def generate(self, **kwargs: Any) -> ChoiceCaller: + """Generate the CuteDSL kernel caller.""" + input_nodes = kwargs.pop("input_nodes") + layout = kwargs.pop("layout") + mutated_inputs = kwargs.pop("mutated_inputs", None) + subgraphs = kwargs.pop("subgraphs", None) + template_kwargs = dict(kwargs) + + kernel_name = f"cutedsl_{self.name}_{next(self.index_counter)}" + + if self.template is None: + raise RuntimeError("Template compilation failed (Jinja2 required)") + + self.output_node: Buffer = Buffer(name="buf_out", layout=layout) + # Patch V.graph.get_dtype to handle the fake buf_out buffer + with patch.object( + V.graph, "get_dtype", KernelTemplate._fake_get_dtype(self.output_node) + ): + kernel = self.kernel_type( + kernel_name=kernel_name, + input_nodes=input_nodes, + output_node=self.output_node, + subgraphs=subgraphs, + ) + code = kernel.render(self.template, **kwargs) + + log.debug("Generated CuteDSL Code:\n%s", code) + + bmreq = CuteDSLBenchmarkRequest( + kernel_name=kernel_name, + input_tensor_meta=TensorMeta.from_irnodes(input_nodes), + output_tensor_meta=TensorMeta.from_irnodes(self.output_node), + extra_args=tuple(), + source_code=code, + ) + + def make_kernel_render(out_node, hint_override: int | None = None): + """ + Factory function that creates a kernel renderer for the final output. + + This closure captures the current template and parameters, but allows + the output node to be specified later. This is used during the final + kernel selection phase when the actual output buffer is available. + """ + render_kernel = self.kernel_type( + kernel_name=str(Placeholder.KERNEL_NAME), + input_nodes=input_nodes, + output_node=out_node, + subgraphs=subgraphs, + ) + + def render(): + return render_kernel.render(self.template, **kwargs) + + return render_kernel, render + + return CuteDSLTemplateCaller( + name=kernel_name, + input_nodes=input_nodes, + layout=layout, + make_kernel_render=make_kernel_render, + bmreq=bmreq, + template=self, + mutated_inputs=mutated_inputs, + template_kwargs=template_kwargs, + ) + + +class CuteDSLTemplateCaller(ChoiceCaller): + """Caller for CuteDSL templates that integrates with the autotuning system.""" + + def __init__( + self, + name: str, + input_nodes: list[Buffer], + layout: Layout, + make_kernel_render: Any, + bmreq: CuteDSLBenchmarkRequest, + template: "CuteDSLTemplate", + mutated_inputs: Iterable[IRNode] | None = None, + template_kwargs: dict[str, Any] | None = None, + ): + description = self._build_description(name, template_kwargs) + super().__init__( + name=name, + input_nodes=input_nodes, + layout=layout, + description=description, + ) + self.make_kernel_render = make_kernel_render + self.bmreq = bmreq + self.template = template + self.mutated_inputs = mutated_inputs + + def _build_description( + self, name: str, template_kwargs: dict[str, Any] | None + ) -> str: + if not template_kwargs: + return f"CuteDSL template {name}" + kwargs_desc = ", ".join(f"{k}={v}" for k, v in template_kwargs.items()) + return f"CuteDSL template {name} ({kwargs_desc})" + + def __str__(self) -> str: + return f"CuteDSLTemplateCaller({self.name})" + + def benchmark(self, *args, out) -> float: + """Benchmark the kernel execution.""" + return self.bmreq.benchmark(*args, out=out) + + def output_node(self) -> TensorBox: + """Create the output node for this template choice.""" + buffer = CuteDSLTemplateBuffer( + layout=self.layout, + inputs=self.input_nodes, + make_kernel_render=self.make_kernel_render, + template=self.template, + mutated_inputs=self.mutated_inputs, + ) + # Pass KTC annotation to the buffer for encoding + if "ktc" in self.annotations: + buffer.annotations["ktc"] = self.annotations["ktc"] + return TensorBox.create(buffer) + + def call_name(self) -> str: + """Return the kernel call name.""" + return self.name + + def to_callable(self) -> Any: + """Return callable that can execute this kernel.""" + return self.make_kernel_render + + def hash_key(self) -> str: + """Return unique hash key for this choice.""" + return "-".join( + [ + self.name.rsplit("_", 1)[0], + self.bmreq.module_cache_key, + ] + ) + + def info_dict(self) -> dict[str, Any]: + """Return information about this kernel.""" + return { + "name": self.name, + "backend": "CuteDSL", + "template": self.template.name, + } diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/cache.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/cache.py new file mode 100644 index 0000000000000000000000000000000000000000..b990f052e1d16a1b52ed2200333d40bbe3bafdb9 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/cache.py @@ -0,0 +1,122 @@ +# mypy: allow-untyped-defs +import functools +import hashlib +import inspect +import json +import logging +import os +import time +from typing import Any + +import torch._inductor.config as config +from torch._inductor.codecache import cutlass_key +from torch._inductor.codegen.cutlass import serialization, utils +from torch._inductor.codegen.cutlass.serialization import ( + get_cutlass_operation_serializer, +) +from torch._inductor.runtime.cache_dir_utils import cache_dir +from torch._inductor.utils import clear_on_fresh_cache + + +log = logging.getLogger(__name__) + + +CONFIG_PREFIX: str = "configs" + + +def get_config_request_key( + arch: str, + toolkit_version: str, + instantiation_level: str, +) -> str: + """ + Return a key for the full ops, based on cutlass key, arch, toolkit version, instantiation level, and serialization.py file hash. + """ + + # Get hash of serialization.py and cutlass_utils.py files using their module file paths + def get_file_hash(file_module): + file_path = inspect.getfile(file_module) + with open(file_path, "rb") as f: + return hashlib.sha256(f.read()).hexdigest() + + serialization_hash = get_file_hash(serialization) + cutlass_utils_hash = get_file_hash(utils) + + hash_target = "-".join( + [ + cutlass_key().hex(), + arch, + toolkit_version, + instantiation_level, + serialization_hash, + cutlass_utils_hash, + ] + ) + return hashlib.sha256(hash_target.encode("utf-8")).hexdigest()[0:8] + + +def _generate_config_filename(request_key: str) -> str: + """ + Generate a filename for the full ops. + """ + return f"{CONFIG_PREFIX}_{request_key}.json" + + +@clear_on_fresh_cache +@functools.cache +def maybe_fetch_ops(device_type: str) -> list[Any] | None: + """ + Fetch ops from databases. + """ + if config.force_disable_caches: + return None + + # setup + arch: str = utils.cutlass_arch(device_type) + version: str = utils.toolkit_version(device_type) + if device_type == "cuda": + # get_cuda_version might return "12.4.0" or "12.4" + # but we want to use "12.4" + version = ".".join(version.split(".")[:2]) + instantiation_level: str = config.cutlass.cutlass_instantiation_level + + # filename and filepath + request_key: str = get_config_request_key(arch, version, instantiation_level) + filename: str = _generate_config_filename(request_key) + filepath: str = os.path.join(cache_dir(), filename) + + # try fetch + serialized_ops: list[str] | None = None + start_time = time.time() + if os.path.isfile(filepath): + # locally + try: + with open(filepath) as f: + serialized_ops = json.load(f) + + assert isinstance(serialized_ops, list), ( + f"Expected serialized ops is a list, got {type(serialized_ops)}" + ) + except Exception: + log.warning( + "Failed to load CUTLASS config %s from local cache", + filename, + exc_info=True, + ) + serialized_ops = None + elif config.is_fbcode(): + from torch._inductor.fb.cutlass_remote_cache import ( + maybe_fetch_cutlass_configs_from_remote, + ) + + # from remote + serialized_ops = maybe_fetch_cutlass_configs_from_remote(filepath) + + if serialized_ops is None: + return None + + # deserialize + serializer = get_cutlass_operation_serializer() + full_ops = [serializer.deserialize(x) for x in serialized_ops] # type: ignore[union-attr] + log.info("Loaded ops from %s cache in %.3fs", filename, time.time() - start_time) + return full_ops diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/gemm_template.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/gemm_template.py new file mode 100644 index 0000000000000000000000000000000000000000..365ce8449320ac2a787d0ff8f690e2ada5518590 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/gemm_template.py @@ -0,0 +1,2040 @@ +# mypy: allow-untyped-defs +import copy +import enum +import functools +import logging +import re +import time +from abc import ABC, abstractmethod +from typing import Any + +import torch +import torch.utils._pytree as pytree +from torch._inductor.autotune_process import TensorMeta +from torch._inductor.codegen.cutlass.cache import maybe_fetch_ops +from torch._inductor.codegen.wrapper import PythonWrapperCodegen +from torch._inductor.runtime.runtime_utils import dynamo_timed +from torch._inductor.scheduler import BaseSchedulerNode +from torch._inductor.select_algorithm import create_inputs_key +from torch._inductor.utils import clear_on_fresh_cache + +from ... import ir +from ...config import cutlass as inductor_cutlass_config +from ...ir import ( + Buffer, + ChoiceCaller, + CUTLASSTemplateBuffer, + FixedLayout, + IRNode, + Layout, + ReinterpretView, +) +from ...utils import is_dynamic, Placeholder +from ...virtualized import V +from ..common import IndentedBuffer +from ..cuda import cuda_env +from . import utils as cutlass_utils +from .kernel import CUTLASSTemplateKernel +from .python_evt import CutlassEVTCodegen, scaled_mm_evt +from .template import CUTLASSTemplate +from .utils import ( + ACCUMULATOR_DTYPES, + dtype_match, + torch_dtype_to_cutlass_type, + XW_DTYPES, +) + + +GemmOperation = Any +EVTArgRenames = Any + +log = logging.getLogger(__name__) + +# Jinja template for GEMM Kernel, used by the CUTLASSGemm3xTemplate class below. +GEMM_TEMPLATE_CUTLASS_3X = r""" +{{template.header().getvalue()}} +{{template.globals().getvalue()}} +{{epilogue_visitor_tree}} +{{instance_definition}} +// When workspace_size is not a nullptr, populates requested workspace_size and returns. +// Otherwise, computes the Gemm kernel using the given workspace ptr. +extern "C" { +PT_EXPORT {{kernel_call_signature}} { + try { + using ElementComputeEpilogue = {{instance_type}}::ElementAccumulator; + using coord_t = cutlass::gemm::GemmCoord::Index; + static cutlass::KernelHardwareInfo hw_info; + if (hw_info.sm_count == 0) { + hw_info.sm_count = cutlass::KernelHardwareInfo::query_device_multiprocessor_count(0); + CUTLASS_TRACE_HOST("Query result for SM count per device: " << hw_info.sm_count); + } + {{dynamic_cluster}} + {{instance_type}}::Arguments arguments; + {{template.render_gemm_arguments(argument_template, epilogue_template, should_swap_xw, + X, W, Bias, Y, alpha, beta, kernel, epilogue_args)}} + {{instance_type}} gemm_op; + if (workspace_size) { + *workspace_size = gemm_op.get_workspace_size(arguments); + return 0; + } + // check for null pointers after workspace size, since querying workspace size doesn't require valid data pointers +#ifndef CUTLASS_BACKEND_DISABLE_CHECKS + { + auto status = gemm_op.can_implement(arguments); + CUTLASS_CHECK(status); + } +#endif +#ifdef CUTLASS_DEBUG_TRACE_LEVEL +#if CUTLASS_DEBUG_TRACE_LEVEL == 1 + { + // Print the maximum number of active blocks per SM for the kernel if CUTLASS_DEBUG_TRACE_LEVEL == 1 + // we don't need a print statement, it's happening inside the function. + gemm_op.maximum_active_blocks(); + } +#endif +#endif + { + auto status = gemm_op.initialize(arguments, workspace, stream); + CUTLASS_CHECK(status); + } + { + auto status = gemm_op(stream); + CUTLASS_CHECK(status); + } + } + catch (std::exception& e) { + std::cerr << "Runtime error: " << e.what() << std::endl; + return -1; + } + catch (...) { + return -1; + } + return 0; +} +} + +// configuration name: {{op_conf_name}} +""" + +# Jinja template for Cutlass 3.x GEMM Kernel arguments, used by the CUTLASSGemmTemplate class below. +GEMM_ARGS_CUTLASS_3X = r""" + // Initialize GemmUniversal3xInstance arguments. + arguments = { + {{template.gemm_mode()}}, // GemmUniversalMode mode + { + static_cast({{M}}), + static_cast({{N}}), + static_cast(K), + static_cast(B) + }, // ProblemShape problem_shape + { + {{template.cutlass_type_cast(X, kernel.ptr(X))}}, // ElementA const* ptr_A + { + {{template.cute_int(kernel.stride(X, -2), "stride_x0")}}, + {{template.cute_int(kernel.stride(X, -1), "stride_x1")}}, + {{template.cute_int(kernel.batch_stride(X), "batch_stride_x")}} + }, // StrideA dA + {{template.cutlass_type_cast(W, kernel.ptr(W))}}, // ElementB const* ptr_B + { + {{template.cute_int(kernel.stride(W, -1), "stride_w1")}}, + {{template.cute_int(kernel.stride(W, -2), "stride_w0")}}, + {{template.cute_int(kernel.batch_stride(W), "batch_stride_w")}} + }, // StrideB dB + }, // MainloopArguments mainloop + {{epilogue_arguments}}, + hw_info + }; + arguments.scheduler.max_swizzle_size = swizzle; +""" + +# Jinja template for Cutlass 3.x GEMM Kernel arguments if epilogue fusion is applied, +# used by the CUTLASSGemmTemplate class below. +GEMM_ARGS_CUTLASS_3X_EPILOGUE = r""" + // see https://tinyurl.com/4rk89z48 + { + {{epilogue_args}}, // thread, typename FusionCallbacks::Arguments ( EVT ) or ThreadEpilogueOp::Params (non-EVT ) + {{template.cutlass_type_cast(Bias, kernel.ptr(Bias))}}, // ElementC const* ptr_C + { + {{template.cute_int(kernel.stride(Bias, -2, 1), "stride_bias0")}}, + {{template.cute_int(kernel.stride(Bias, -1, 1), "stride_bias1")}}, + {{template.cute_int(kernel.batch_stride(Bias), "batch_stride_bias")}} + }, // StrideC dC + {{template.cutlass_type_cast(Y, kernel.ptr(Y))}}, // ElementD const* ptr_D + { + {{template.cute_int(kernel.stride(Y, -2), "stride_y0")}}, + {{template.cute_int(kernel.stride(Y, -1), "stride_y1")}}, + {{template.cute_int(kernel.batch_stride(Y), "batch_stride_y")}} + }, // StrideD dD + }, // EpilogueArguments epilogue +""" + +# Jinja template for GEMM Kernel, used by the CUTLASS2xGemmTemplate class below. +GEMM_TEMPLATE_CUTLASS_2X = r""" +{{template.header().getvalue()}} +{{template.globals().getvalue()}} +{{instance_definition}} +// When workspace_size is not a nullptr, populates requested workspace_size and returns. +// Otherwise, computes the Gemm kernel using the given workspace ptr. +extern "C" { +PT_EXPORT {{kernel_call_signature}} { + try { + int B = {{kernel.size(Y, 0, -3, default_value=1)}}; + using ElementComputeEpilogue = {{instance_type}}::ElementAccumulator; + using coord_t = cutlass::gemm::GemmCoord::Index; + static cutlass::KernelHardwareInfo hw_info; + if (hw_info.sm_count == 0) { + hw_info.sm_count = cutlass::KernelHardwareInfo::query_device_multiprocessor_count(0); + CUTLASS_TRACE_HOST("Query result for SM count per device: " << hw_info.sm_count); + } + {{instance_type}}::Arguments arguments; + {{template.render_gemm_arguments(instance_type, argument_template, epilogue_template, should_swap_xw, + X, W, Bias, Meta, Y, alpha, beta, kernel, epilogue_args)}} + {{instance_type}} gemm_op; + if (workspace_size) { + *workspace_size = gemm_op.get_workspace_size(arguments); + return 0; + } + + // check for null pointers after workspace size, since querying workspace size doesn't require valid data pointers +#ifndef CUTLASS_BACKEND_DISABLE_CHECKS + { + auto status = gemm_op.can_implement(arguments); + CUTLASS_CHECK(status); + } +#endif +#ifdef CUTLASS_DEBUG_TRACE_LEVEL +#if CUTLASS_DEBUG_TRACE_LEVEL == 1 + { + // Print the maximum number of active blocks per SM for the kernel if CUTLASS_DEBUG_TRACE_LEVEL == 1 + // we don't need a print statement, it's happening inside the function. + gemm_op.maximum_active_blocks(); + } +#endif +#endif + + { + auto status = gemm_op.initialize(arguments, workspace, stream); + CUTLASS_CHECK(status); + } + { + auto status = gemm_op(stream); + CUTLASS_CHECK(status); + } + } + catch (std::exception& e) { + std::cerr << "Runtime error: " << e.what() << std::endl; + return -1; + } + catch (...) { + return -1; + } + return 0; +} +} +""" + +# Jinja template for Cutlass 2.x GEMM Kernel arguments, used by the CUTLASS2xGemmTemplate class below. +GEMM_ARGS_CUTLASS_2X = r""" + int64_t batch_stride_x = {{kernel.stride(X, -3)}}; + int64_t row_stride_x = {{kernel.row_or_column_stride(X)}}; + int64_t batch_stride_w = {{kernel.stride(W, -3)}}; + int64_t row_stride_w = {{kernel.row_or_column_stride(W)}}; + int64_t batch_stride_bias = {{kernel.stride(Bias, -3)}}; + int64_t row_stride_bias = {{kernel.row_or_column_stride(Bias)}}; + int64_t batch_stride_y = {{kernel.stride(Y, -3)}}; + int64_t row_stride_y = {{kernel.row_or_column_stride(Y)}}; + // Initialize GemmUniversalInstance arguments. + arguments = { + {{template.gemm_mode()}}, // GemmUniversalMode mode + { + static_cast(M), + static_cast(N), + static_cast(K) + }, // GemmCoord problem_size + {{split_k if split_k > 1 else 'B'}}, // int batch_count + {ElementComputeEpilogue({{alpha}}), ElementComputeEpilogue({{beta}})}, // typename EpilogueOutputOp::Params epilogue + {{template.cutlass_type_cast(X, kernel.ptr(X))}}, // void const * ptr_A + {{template.cutlass_type_cast(W, kernel.ptr(W))}}, // void const * ptr_B + {{template.cutlass_type_cast(Bias, kernel.ptr(Bias))}}, // void const * ptr_C + {{template.cutlass_type_cast(Y, kernel.ptr(Y))}}, // void * ptr_D + batch_stride_x, // int64_t batch_stride_A + batch_stride_w, // int64_t batch_stride_B + batch_stride_bias, // int64_t batch_stride_C + batch_stride_y, // int64_t batch_stride_D + row_stride_x, // typename LayoutA::Stride::LongIndex lda + row_stride_w, // typename LayoutB::Stride::LongIndex ldb + row_stride_bias, // typename LayoutC::Stride::LongIndex ldc + row_stride_y, // typename LayoutC::Stride::LongIndex ldd + }; +""" + +GEMM_ARGS_SPARSE_CUTLASS_2X = r""" + using TensorRefA = cutlass::TensorRef<{{instance_type}}::ElementA, + {{instance_type}}::LayoutA>; + using TensorRefB = cutlass::TensorRef<{{instance_type}}::ElementB, + {{instance_type}}::LayoutB>; + using TensorRefC = cutlass::TensorRef<{{instance_type}}::ElementC, + {{instance_type}}::LayoutC>; + using TensorRefE = cutlass::TensorRef<{{instance_type}}::ElementE, + {{instance_type}}::LayoutE>; + // Note that "X" and "W" names may be misleading here. Namely, for + // sparse GEMM, the first argument is always sparse, while typically + // weight matrix, implied by name "W" will be sparse in + // applications. Thus, just remember that here: "X" refers to first + // argument, that is sparse, and "W" to second, that is dense. + TensorRefA X_ref({{template.cutlass_type_cast(X, kernel.ptr(X))}}, {{kernel.row_or_column_stride(X)}}); + TensorRefB W_ref({{template.cutlass_type_cast(W, kernel.ptr(W))}}, {{kernel.row_or_column_stride(W)}}); + TensorRefC Y_ref({{template.cutlass_type_cast(Y, kernel.ptr(Y))}}, {{kernel.row_or_column_stride(Y)}}); + TensorRefE Meta_ref({{template.cutlass_sparse_meta_type_cast(Meta, kernel.ptr(Meta))}}, + TensorRefE::Layout::packed({ {{kernel.size(Meta, 0)}}, {{kernel.size(Meta, 1)}} })); + // Initialize GemmSparse arguments. + arguments = { + { + static_cast(M), + static_cast(N), + static_cast(2 * K), + }, // GemmCoord problem_size + X_ref, // TensorRef ref_A + W_ref, // TensorRef ref_B + Y_ref, // TensorRef ref_C + Y_ref, // TensorRef ref_D + Meta_ref, // TensorRef ref_E + {ElementComputeEpilogue({{alpha}}), ElementComputeEpilogue({{beta}})}, // typename EpilogueOutputOp::Params epilogue, + }; +""" + +# Additional includes which are necessary if the standalone test / debug runner is generated as well +GEMM_STANDALONE_RUNNER_ADDITIONAL_INCLUDES = r""" +#ifdef GENERATE_STANDALONE_RUNNER +#include "cutlass/util/distribution.h" +#include "cutlass/util/host_tensor.h" +#include "cutlass/util/packed_stride.hpp" +#include "cutlass/util/tensor_view_io.h" +#include "cutlass/util/reference/device/gemm_complex.h" +#include "cutlass/util/reference/device/tensor_compare.h" +#include "cutlass/util/reference/device/tensor_fill.h" +#include +#endif +""" + +# Jinja template for the standalone runner that may be generated as part of the code. +GEMM_STANDALONE_RUNNER_TEMPLATE = r""" +#ifdef GENERATE_STANDALONE_RUNNER +/// Helper to initialize a block of device data +template +bool initialize_block( + cutlass::DeviceAllocation& block, + uint64_t seed, float max=1.0, float min=-1.0) { + if (block.size()<=0) return false; + Element scope_max(static_cast(max)), scope_min(static_cast(min)); + cutlass::reference::device::BlockFillRandomUniform( + (Element*)block.get(), block.size(), seed, scope_max, scope_min); + + return true; +} + +{% if Meta is defined and Meta is not none %} +template +bool initialize_block_meta( + cutlass::DeviceAllocation& block, + uint64_t seed) { + if (block.size()<=0) return false; + cutlass::reference::device::BlockFillRandomSparseMeta( + (Element*)block.get(), block.size(), seed, {{instance_type}}::kMetaSizeInBits); + return true; +} +{% endif %} + +extern "C" int run_standalone(uint64_t seed, int repetitions) { + std::cout << "Starting GEMM Standalone test run with seed " << seed << std::endl; + size_t workspace_size = 0; + size_t* workspace_size_ptr = &workspace_size; + + int M = {{kernel.get_layout_args()[0]}}; + int N = {{kernel.get_layout_args()[1]}}; + int K = {{kernel.get_layout_args()[2]}}; + int B = {{kernel.get_layout_args()[3]}}; + int lda = {{kernel.get_layout_args()[4]}}; + int ldb = {{kernel.get_layout_args()[5]}}; + int ldc = {{kernel.get_layout_args()[6]}}; + int ldd = {{kernel.get_layout_args()[7]}}; + uint8_t swizzle = {{kernel.runtime_arg_values[0]}}; + + using ElementA = {{kernel.cutlass_dtype(X)}}; + using ElementB = {{kernel.cutlass_dtype(W)}}; + using ElementC = {{kernel.cutlass_dtype(Bias, default_dtype='uint8_t')}}; // may not be void + using ElementD = {{kernel.cutlass_dtype(Y)}}; + {% if Meta is defined and Meta is not none %} + using ElementE = {{kernel.cutlass_dtype(Meta)}}; + {% endif %} + + cutlass::DeviceAllocation X_data({{kernel.max_valid_index(X)+1}}); + initialize_block(X_data, seed++); + cutlass::DeviceAllocation W_data({{kernel.max_valid_index(W)+1}}); + initialize_block(W_data, seed++); + cutlass::DeviceAllocation Bias_data({{kernel.max_valid_index(Bias)+1}}); + initialize_block(Bias_data, seed++); + cutlass::DeviceAllocation Y_data({{kernel.max_valid_index(Y)+1}}); + {% if Meta is defined and Meta is not none %} + cutlass::DeviceAllocation Meta_data({{kernel.max_valid_index(Meta)+1}}); + initialize_block_meta(Meta_data, seed++); + {% endif %} + + cutlass::DeviceAllocation workspace_data; + // Call once with workspace_size_ptr set to get workspace size + + std::cout << "Calling once to get workspace size" << std::endl; + {{test_call_statement}}; + // Allocate workspace if necessary + if (workspace_size > 0) { + workspace_data.reset(workspace_size); + std::cout << "Allocated workspace size of " << workspace_size << " bytes" << std::endl; + } + std::cout << "Calling Kernel as {{test_call_statement}};" << std::endl; + workspace_size_ptr = nullptr; + for (int i=0; i None: + """ + Args: + input_nodes (List[Buffer]): List of input nodes of the GEMM kernel. + layout (Layout): Layout type of the resulting output node. + alpha (float): The scaling factor for the product of the inputs in the GEMM operation. + beta (float): The scaling factor applied to the output matrix. + input_reorder (Optional[List[int]]): Specifies the reordering of the input nodes. If not provided, + no reordering is performed. Defaults to None. + """ + super().__init__( + str(Placeholder.KERNEL_NAME), input_nodes, layout, input_reorder + ) + self.alpha = alpha + self.beta = beta + self.use_fast_accum = use_fast_accum + assert 2 <= len(input_nodes) <= 5 + assert self._are_inputs_layout_compatible( + [node.get_layout() for node in input_nodes] + ) + + self.cache_key: str = create_inputs_key(self.input_nodes) + + @staticmethod + @abstractmethod + def add_cutlass_gemm_choices( + choices: list[ChoiceCaller], + layout: ir.Layout, + input_nodes: list[Buffer], + alpha: float | int = 1, + beta: float | int = 0, + input_reorder: list[int] | None = None, + use_fast_accum: bool | None = None, + **extra_kwargs, + ) -> None: + raise NotImplementedError + + @staticmethod + @abstractmethod + def _get_supported_ops() -> "list[cutlass_library.gemm_operation.GemmOperation]": # type: ignore[name-defined] # noqa: F821 + raise NotImplementedError + + @staticmethod + @abstractmethod + def _has_tma_epilogue(self) -> bool: + raise NotImplementedError + + @abstractmethod + def _get_template(self) -> str: + raise NotImplementedError + + @abstractmethod + def _get_template_args( + self, + op: "cutlass_library.gemm_op.GemmOperation", # type: ignore[name-defined] # noqa: F821 + ) -> tuple[str, str | None]: + raise NotImplementedError + + @abstractmethod + def _are_inputs_layout_compatible(self, layouts: list[Layout]) -> bool: + raise NotImplementedError + + @abstractmethod + def _shape_match( + self, + op: "cutlass_library.gemm_op.GemmOperation", # type: ignore[name-defined] # noqa: F821 + ) -> bool: + raise NotImplementedError + + @abstractmethod + def _alignment_match( + self, + op: "cutlass_library.gemm_op.GemmOperation", # type: ignore[name-defined] # noqa: F821 + ) -> bool: + raise NotImplementedError + + @abstractmethod + def _set_bias_layout_and_alignment( + self, + op: "cutlass_library.gemm_op.GemmOperation", # type: ignore[name-defined] # noqa: F821 + ) -> bool: + raise NotImplementedError + + @abstractmethod + def _define_gemm_instance( + self, + op: GemmOperation, + evt_name: str | None = None, + ) -> tuple[str, str]: + raise NotImplementedError + + @abstractmethod + def _get_extra_inputs_and_names( + self, + op: "cutlass_gemm_op.GemmOperation" = None, # type: ignore[name-defined] # noqa: F821 + ) -> tuple[Buffer | None, list[Buffer | None], list[str]]: + raise NotImplementedError + + @abstractmethod + def _update_arg_names_for_test_call_statement( + self, + arg_names: list[str], + input_nodes: list[Buffer], + ) -> list[str]: + raise NotImplementedError + + def _add_cutlass_gemm_choices( + self, + choices: list[ChoiceCaller], + layout: ir.Layout, + input_nodes: list[Buffer], + alpha: float | int = 1, + beta: float | int = 0, + input_reorder: list[int] | None = None, + **extra_kwargs, + ) -> None: + """ + Adds Cutlass GEMM configurations choices to the auto-tuning list. + + This function mutates the passed list of choices by appending the choices for Cutlass GEMM configs to it. + + Args: + choices (list): The list to which choices are appended. + layout (ir.Layout): The layout configuration. + input_nodes (list): The list of input nodes. + alpha (float,int): Scaling factor, defaults to 1. + beta (float,int): Offset, defaults to 0. + input_reorder (list, optional): Order of the inputs, defaults to None. + **extra_kwargs: Additional keyword arguments. + + """ + + ops = self.gen_ops() + + # pre-computation + layout_repr: str = str(layout) + input_tensor_meta: TensorMeta | list[TensorMeta] = TensorMeta.from_irnodes( + self.input_nodes + ) + # When input_reorder is set (e.g. [2, 0, 1] for addmm), the kernel + # function signature is reordered (e.g. from [X, W, Bias] to + # [Bias, X, W]). input_tensor_meta must follow the same order + # because subprocess benchmarking creates tensors from this metadata + # and passes them positionally to the compiled kernel. Without this + # reorder the kernel receives mismatched pointers/strides, causing + # out-of-bounds GPU memory access for large shapes. + if self.input_reorder is not None and isinstance(input_tensor_meta, list): + input_tensor_meta = [input_tensor_meta[idx] for idx in self.input_reorder] + output_tensor_meta: TensorMeta | list[TensorMeta] = TensorMeta.from_irnodes( + self.output_node + ) + + with dynamo_timed("CUTLASSGemmTemplate.maybe_append_choice"): + for name, op in ops: + for ( + swizzle + ) in inductor_cutlass_config.cutlass_max_profiling_swizzle_options: + description = f"{name} swizzle={swizzle}" + self.maybe_append_choice( + choices, + op=op, + name=name, + description=description, + input_key=self.cache_key, + layout_repr=layout_repr, + input_tensor_meta=input_tensor_meta, + output_tensor_meta=output_tensor_meta, + swizzle=swizzle, + ) + + if len(ops) == 0: + log.info( + "No suitable Cutlass GEMM configs found, fallbacks used " + "( len(ops)=%d, output_layout=%s, input_layouts=%s, input_strides=%s )", + len(ops), + layout, + [node.get_layout() for node in input_nodes], + [node.get_stride() for node in input_nodes], + ) + log.debug( + "Added %d Cutlass gemm configs.", + len(ops), + ) + + def header(self) -> IndentedBuffer: + """ + Returns a buffer containing CUDA C++ code for the header section of the CUTLASS GEMM template. + This section primarily includes the necessary header files. + + Returns: + IndentedBuffer: An instance of IndentedBuffer that contains the generated CUDA C++ header code. + """ + res = super().header() + res.splice( + """ + #include "cutlass/gemm/gemm.h" + #include "cutlass/gemm/device/gemm_universal.h" + #include "cutlass/gemm/device/gemm_universal_adapter.h" + #include "cutlass/gemm/kernel/gemm_universal.hpp" + #include "cutlass/gemm/collective/collective_builder.hpp" + #include "cutlass/epilogue/collective/collective_builder.hpp" + #include "cutlass/epilogue/collective/default_epilogue.hpp" + #include "cutlass/epilogue/thread/linear_combination.h" + #include "cutlass/epilogue/thread/activation.h" + #include "cutlass/gemm/dispatch_policy.hpp" + #include "cutlass/gemm/kernel/tile_scheduler.hpp" + #include "cutlass/tensor_ref.h" + #include "cutlass/util/distribution.h" + #include "cutlass/util/packed_stride.hpp" + #include "cutlass/util/tensor_view_io.h" + """ + ) + if self.device_type != "xpu": + # XPU SYCL-TLA does not support sparse gemm yet + res.splice( + """ + #include "cutlass/gemm/device/gemm_sparse.h" + """ + ) + if inductor_cutlass_config.generate_test_runner and not is_dynamic( + *self.input_nodes, self.output_node + ): + res.splice(GEMM_STANDALONE_RUNNER_ADDITIONAL_INCLUDES) + return res + + @staticmethod + def cutlass_layout(torch_layout: ir.Layout) -> "cutlass_lib.LayoutType | None": # type: ignore[name-defined] # noqa: F821 + """ + Converts an ir.Layout instance into the corresponding cutlass_library.LayoutType enum value + (RowMajor, ColumnMajor, or None if no matching value is found ). + + Args: + torch_layout (ir.Layout): The layout that needs to be looked up. + + Returns: + cutlass_lib.LayoutType: The converted layout corresponding to the `torch_layout` or None if no matching + value is found. + """ + assert cutlass_utils.try_import_cutlass() + import cutlass_library.library as cutlass_lib + + if V.graph.sizevars.statically_known_equals(torch_layout.stride[-1], 1): + return cutlass_lib.LayoutType.RowMajor + elif V.graph.sizevars.statically_known_equals(torch_layout.stride[-2], 1): + return cutlass_lib.LayoutType.ColumnMajor + else: + return None + + @staticmethod + def flip_cutlass_layout( + cutlass_layout: "cutlass_lib.LayoutType", # type: ignore[name-defined] # noqa: F821 + ) -> "cutlass_lib.LayoutType": # type: ignore[name-defined] # noqa: F821 + """Helper method: Flips a given cutlass layout (cutlass_lib.LayoutType) from RowMajor + to ColumnMajor or vice versa""" + assert cutlass_utils.try_import_cutlass() + import cutlass_library.library as cutlass_lib + + if cutlass_layout == cutlass_lib.LayoutType.RowMajor: + return cutlass_lib.LayoutType.ColumnMajor + else: + return cutlass_lib.LayoutType.RowMajor + + @staticmethod + @functools.lru_cache(32) + def layout_match( + torch_layout: ir.Layout, + cutlass_layout: "cutlass_lib.LayoutType", # type: ignore[name-defined] # noqa: F821 + ) -> bool: + """Helper Method: Determines whether a given torch layout matches a given Cutlass layout""" + return CUTLASSGemmTemplate.cutlass_layout(torch_layout) == cutlass_layout + + @staticmethod + def set_layout(tensor_desc: "TensorDescription", torch_layout: ir.Layout) -> None: # type: ignore[name-defined] # noqa: F821 + """ + Helper method: Sets the layout of a given tensor description to match the given torch layout + """ + if CUTLASSGemmTemplate.layout_match(torch_layout, tensor_desc.layout): + return + tensor_desc.layout = CUTLASSGemmTemplate.cutlass_layout(torch_layout) + + @staticmethod + def set_alignment(torch_layout, op_element) -> bool: + """ + Helper method to update the alignment of a given CUTLASS GEMM op operand's element. + + This method modifies the alignment of the given Cutlass GEMM op operand's element to match the + layout of the corresponding ir.Buffer node. + + Args: + torch_layout: The layout of the corresponding ir.Buffer node. + op_element: The Cutlass GEMM op operand's element whose alignment is to be updated. + + Returns: + bool: True if the alignment was successfully updated, False otherwise. + """ + alignment = cutlass_utils.get_max_alignment(torch_layout) + if torch.cuda.is_available(): + cuda_arch = cuda_env.get_cuda_arch() + cuda_arch = cutlass_utils._normalize_cuda_arch(cuda_arch) + if cuda_arch and int(cuda_arch) >= 90 and alignment < op_element.alignment: + return False + + op_element.alignment = alignment + return True + + @staticmethod + def should_swap_XW( + bias: IRNode, + ) -> bool: + """ + Helper method to determine whether we should do an explicit transpose by switching the order of the + matmul operands. This might be necessary when we can't otherwise arrive at the right memory + layout for the given Bias operand. + + Note: This method is a workaround for CUDA Errors that seemingly non-deterministically + occurred in practice in some CUTLASS GEMM Kernels with Linear epilogues that have a bias term. + it might make sense to check on newer Cutlass releases whether it makes sense to keep + returning True in certain cases or whether it becomes unnecessary. + """ + # If bias is row major, swap all M and N dimensions + if ( + bias is not None + and len(bias.get_stride()) >= 2 + and bias.get_stride()[-1] in (0, 1) + ): + log.debug("GEMM Layout swapped X and W -> explicit transpose") + return True + return False + + @staticmethod + def swap_XW( + op: "cutlass_library.gemm_op.GemmOperation", # type: ignore[name-defined] # noqa: F821 + ) -> "cutlass_library.gemm_op.GemmOperation": # type: ignore[name-defined] # noqa: F821 + """ + Swap operands X and W (aka operans A and B) of the GEMM operation. This + requires transposing the operands, which is done by swapping the strides. + Note that we don't change the apparent external layout, just the operand layout. + this is intentional. + """ + new_op = copy.deepcopy(op) + new_op.A.layout = CUTLASSGemmTemplate.flip_cutlass_layout(new_op.A.layout) + new_op.B.layout = CUTLASSGemmTemplate.flip_cutlass_layout(new_op.B.layout) + new_op.A, new_op.B = new_op.B, new_op.A + new_op.C.layout = CUTLASSGemmTemplate.flip_cutlass_layout(new_op.C.layout) + new_op.D.layout = CUTLASSGemmTemplate.flip_cutlass_layout(new_op.D.layout) + return new_op + + def fix_op_layout( + self, + op: "cutlass_library.gemm_op.GemmOperation", # type: ignore[name-defined] # noqa: F821 + X: Buffer, + W: Buffer, + Bias: Buffer | None, + Y: Buffer | ReinterpretView, + ) -> "cutlass_library.gemm_op.GemmOperation": # type: ignore[name-defined] # noqa: F821 + # This is a workaround to deal with cases where the input layouts have changed + # between autotuning and rendering. This happens if the inputs layout + # are FlexibleLayout instances. In this case, we need to update the + # op's input layouts. It is a hack, because now the op + # we benchmarked is not the same as the op we render, + # but there is no simple way to fix this in the autotuner, since that would + # potentially disable other optimizations. + a_layout = X.get_layout() + b_layout = W.get_layout() + c_layout = Bias.get_layout() if Bias is not None else None + + d_layout = copy.deepcopy(Y.get_layout()) + match_list = [ + CUTLASSGemmTemplate.layout_match(buf.get_layout(), op_layout) + for buf, op_layout in zip( + (X, W, Bias, Y), + (op.A.layout, op.B.layout, op.C.layout, op.D.layout), + ) + if buf is not None + ] + all_match = all(match_list) + if all_match: + return op + log.warning( + f"Cutlass GEMM Layout change: Input and/or output layouts have changed between autotuning/retuning and call to render on {self}. Applying workaround. This can lead to suboptimal performance. Match List: {match_list}" # noqa: G004, B950 + ) + new_op = copy.deepcopy(op) + + if a_layout is not None: + new_op.A.layout = CUTLASSGemmTemplate.cutlass_layout(a_layout) + if b_layout is not None: + new_op.B.layout = CUTLASSGemmTemplate.cutlass_layout(b_layout) + if c_layout is not None: + new_op.C.layout = CUTLASSGemmTemplate.cutlass_layout(c_layout) + new_op.C.element = cutlass_utils.torch_dtype_to_cutlass_type(c_layout.dtype) + if d_layout is not None: + new_op.D.layout = CUTLASSGemmTemplate.cutlass_layout(d_layout) + return new_op + + def _dtype_match( + self, + op: "cutlass_library.gemm_op.GemmOperation", # type: ignore[name-defined] # noqa: F821 + ) -> bool: + """ + Checking dtypes of A, B, acc, D here. + + Empirically speaking, CUTLASS2x ops have same dtype for C and D. + """ + X = self.input_nodes[0] + W = self.input_nodes[1] + + accumulator_torch_dtype = cutlass_utils.get_accumulator_dtype( + [X.get_dtype(), W.get_dtype()], + ) + if not ( + cutlass_utils.dtype_match(X.get_dtype(), op.A.element) + and cutlass_utils.dtype_match(W.get_dtype(), op.B.element) + and cutlass_utils.dtype_match( + self.output_node.get_layout().dtype, op.D.element + ) + and cutlass_utils.dtype_match( + accumulator_torch_dtype, op.accumulator_type() + ) + ): + return False + + return True + + @classmethod + def global_filter_ops( + cls, + ops: list["cutlass_library.gemm_op.GemmOperation"], # type: ignore[name-defined] # noqa: F821 + ) -> list["cutlass_library.gemm_op.GemmOperation"]: # type: ignore[name-defined] # noqa: F821 + """ + Filter ops without using information about the torch op, input nodes and output node. + """ + assert cutlass_utils.try_import_cutlass() + import cutlass_library.library as cutlass_lib # type: ignore[import] + + # Skip simt kernels + ops = [ + op + for op in ops + if op.tile_description.math_instruction.opcode_class + != cutlass_lib.OpcodeClass.Simt + ] + + # only keep the set of row x column ops + # for other layout, we modify in place in filter_op, after deepcopy + ops = [ + op + for op in ops + if op.A.layout.name == "RowMajor" and op.B.layout.name == "ColumnMajor" + ] + + # filter by supported accumulator types + ops = [ + op + for op in ops + if any( + dtype_match(torch_dtype, op.accumulator_type()) + for torch_dtype in ACCUMULATOR_DTYPES + ) + ] + + # check if dtypes of A and B are supported + ops = [ + op + for op in ops + if any(dtype_match(torch_dtype, op.A.element) for torch_dtype in XW_DTYPES) + and any(dtype_match(torch_dtype, op.B.element) for torch_dtype in XW_DTYPES) + ] + + return ops + + def filter_op( + self, + op: "cutlass_library.gemm_op.GemmOperation", # type: ignore[name-defined] # noqa: F821 + ) -> "cutlass_library.gemm_op.GemmOperation": # type: ignore[name-defined] # noqa: F821 + """ + Helper method: + + Determines whether a given Cutlass GEMM op definition is suitable for the current + input / output of the operation that this template is supposed to implement. + + Takes memory layout, dtype and support for EVT operations into account, + and filters potentially problematic ops. + + Returns None if the op is not suitable, otherwise returns the op to be used, which might + have been mutated. + """ + + if op.gemm_kind not in self._get_supported_ops(): + return None + + X = self.input_nodes[0] + W = self.input_nodes[1] + + # Filter ops according to the shape match. + if not self._shape_match(op): + return None + + # Filter ops by dtypes. + if not self._dtype_match(op): + return None + + # Filter ops by alignment. + if not self._alignment_match(op): + log.debug( + "Skipping due to alignment mismatch. op: %s", op.configuration_name() + ) + return None + + # only use stream k for static shape + if op.tile_scheduler.name == "StreamK": + static_shape = PythonWrapperCodegen.statically_known_list_of_ints_or_none( + tuple(X.get_size()) + tuple(W.get_size()) + ) + if not static_shape: + return None + + # Update op. + op = copy.deepcopy(op) + + # set layouts for X and W + self.set_layout(op.A, X.get_layout()) + self.set_layout(op.B, W.get_layout()) + + # Set output layout. + op.D.layout = CUTLASSGemmTemplate.cutlass_layout(self.output_node.get_layout()) + + # Filter ops by alignments and set alignments. + status = ( + self.set_alignment(X.get_layout(), op.A) + and self.set_alignment(W.get_layout(), op.B) + and self.set_alignment(self.output_node.get_layout(), op.D) + ) + if not status: + log.debug( + "Skipping due to alignment setting failure. op: %s", + op.configuration_name(), + ) + return None + + if inductor_cutlass_config.cutlass_tma_only and not self._has_tma_epilogue(op): + return None + + # Set epilogue. + # TODO: update epilogue functor according to epilogues. + op.element_epilogue = op.accumulator_type() + + if self.use_fast_accum is not None: + is_op_fast_accum = "fastaccum" in op.configuration_name() + if self.use_fast_accum ^ is_op_fast_accum: + return None + + # Set bias layout and alignment. + status = self._set_bias_layout_and_alignment(op) + if not status: + log.debug( + "Skipping due to bias layout and alignment setting failure. op: %s", + op.configuration_name(), + ) + return None + + # Apply regex filters at the end when configuration name doesn't change anymore + if inductor_cutlass_config.cutlass_op_allowlist_regex: + if not re.search( + inductor_cutlass_config.cutlass_op_allowlist_regex, + op.configuration_name(), + ): + return None + if inductor_cutlass_config.cutlass_op_denylist_regex is not None: + if re.search( + inductor_cutlass_config.cutlass_op_denylist_regex, + op.configuration_name(), + ): + return None + + # `_procedural_name` is decorated with @functools.cached_property in cutlass, and its value is + # cached based on the key `self`. After we modify some attributes of + # `self` (e.g., layout or alignment), the `self` itself doesn’t change, so the + # cached value remains stale. We therefore need to clear the cached value so that + # `_procedural_name` can be recomputed with the updated attributes. + del op._procedural_name + return op + + def gen_ops(self) -> "list[tuple[str, cutlass_gemm_op.GemmOperation]]": # type: ignore[name-defined] # noqa: F821 + """ + Creates a list of Cutlass GemmOperation instances that match the operation this template is designed to represent. + The matching is carried out with respect to the input and output specifications of the operation. + + No function arguments. + + Returns: + List[tuple[str, cutlass_gemm_op.GemmOperation]]: A list of (cutlass_name, GemmOperation) + tuples that are compatible with the operation requirements of this template. + """ + assert cutlass_utils.try_import_cutlass() + import cutlass_library.gemm_operation as cutlass_gemm_op + + if self.cache_key in self.filtered_ops_cache: + log.debug("Using cached ops for %s", self.cache_key) + return self.filtered_ops_cache[self.cache_key] + + with dynamo_timed("CUTLASSGemmTemplate.maybe_fetch_ops"): + maybe_ops = maybe_fetch_ops(self.device_type) + if maybe_ops is None: + log.debug("Cannot fetch ops from cache, generating ops from scratch") + full_ops = cutlass_utils.gen_ops(self.device_type) + ops = pytree.tree_flatten(full_ops)[0] + else: + log.debug("Using cached ops from cache") + ops = maybe_ops + + ops = self.global_filter_ops(ops) + + res: dict[str, cutlass_gemm_op.GemmOperation] = {} + start_time = time.time() + for op in ops: + # if changed, need to also change CUTLASS_OPERATION_KIND + assert isinstance(op, cutlass_gemm_op.GemmOperation) + filter_res = self.filter_op(op) + if ( + filter_res is not None + and res.get(filter_res.configuration_name()) is None + ): + res[filter_res.configuration_name()] = filter_res + log.info( + "Got cutlass configs: total number of ops: %d. Filtering took %.2f seconds", + len(res), + time.time() - start_time, + ) + sorted_res = sorted(res.items()) + ret_res = sorted_res[: inductor_cutlass_config.cutlass_max_profiling_configs] + if len(self.filtered_ops_cache) < 50: + self.filtered_ops_cache[self.cache_key] = ret_res + else: + log.debug("Not caching ops since filtered_ops_cache has reached size 50.") + return ret_res + + def gemm_mode(self) -> str: + """ + Returns a Cutlass GEMM mode string for the current operation, dependent on whether this op implements + a batched GEMM or a simple GEMM without batch dimension. + + Returns: + str: A string indicating the Cutlass GEMM mode. If the output node has more than two dimensions, + "cutlass::gemm::GemmUniversalMode::kBatched" is returned, otherwise + "cutlass::gemm::GemmUniversalMode::kGemm" is returned. + """ + sizes = self.output_node.get_size() + if len(sizes) > 2: + return "cutlass::gemm::GemmUniversalMode::kBatched" + else: + return "cutlass::gemm::GemmUniversalMode::kGemm" + + def _dynamic_cluster_block(self, op: "cutlass_gemm_op.GemmOperation") -> str: # type: ignore[name-defined] # noqa: F821 + """ + Temporary workaround for CUTLASS GEMMs that encode cluster shape as runtime values. + + For dynamic-cluster kernels, CUTLASS expects `KernelHardwareInfo.cluster_shape` and + `KernelHardwareInfo.cluster_shape_fallback` to be populated with valid runtime values. + Today we provide a single global preferred/fallback pair (configurable via env), + which is sufficient for correctness but is not performance-optimal. + + Note: This is intentionally minimal because this code path is transitional. Cluster-shape + selection should ultimately be handled in the CuTe/DSL implementation rather than + investing heavily in the legacy CUTLASS template pipeline. + """ + shape = getattr(getattr(op, "tile_description", None), "cluster_shape", None) + if not shape or len(shape) < 2 or (shape[0] > 0 and shape[1] > 0): + return "" + + preferred = inductor_cutlass_config.cutlass_dynamic_cluster_shape + fallback = inductor_cutlass_config.cutlass_dynamic_cluster_fallback + + cluster_k = shape[2] if len(shape) > 2 and shape[2] > 0 else preferred[2] + preferred = (preferred[0], preferred[1], cluster_k) + fallback_k = fallback[2] if len(fallback) > 2 and fallback[2] > 0 else cluster_k + fallback = (fallback[0], fallback[1], fallback_k) + + return ( + f" hw_info.cluster_shape = {{{preferred[0]}, {preferred[1]}, {preferred[2]}}};\n" + f" hw_info.cluster_shape_fallback = {{{fallback[0]}, {fallback[1]}, {fallback[2]}}};" + ) + + def render( # type: ignore[override] + self, + kernel: CUTLASSTemplateKernel, + op: "cutlass_gemm_op.GemmOperation" = None, # type: ignore[name-defined] # noqa: F821 + template_buffer_node: CUTLASSTemplateBuffer | None = None, + epilogue_nodes: list[BaseSchedulerNode] | None = None, + **kwargs, + ) -> str: + """ + The primary entry point for the code rendering process used in this template. + Renders the Cutlass based CUDA/XPU C++ code for the GEMM Kernel that this template is designed to implement, + including potentially fused epilogues. + + Args: + kernel (CUTLASSTemplateKernel): The kernel to be rendered. + op (cutlass_gemm_op.GemmOperation, optional): A GEMM operation that is required to be compatible with the + input and output definitions as well as a possible epilogue. Defaults to None. + **kwargs: Additional keyword arguments. Currently unused. + + Returns: + str: Cutlass based CUDA/XPU C++ code fragment as a string, to be used by the current + CUTLASSTemplateKernel or autotuning code. + + Note: + All inputs and their corresponding buffer addresses and names take precedence over previously + passed inputs to the template at construction time. However, they should be layout compatible. + """ + assert cutlass_utils.try_import_cutlass() + import cutlass_library.gemm_operation as cutlass_gemm_op + import cutlass_library.library as cutlass_lib + + assert isinstance(op, cutlass_gemm_op.GemmOperation), ( + "op argument is required and has to be an instance of GemmOperation" + ) + + if epilogue_nodes and not self._has_tma_epilogue(op): + raise NotImplementedError( + "Non-TMA epilogue visitor tree is not supported in Cutlass." + ) + + assert len(self.input_nodes) >= 2 and self.output_node is not None + X, W = self.input_nodes[0], self.input_nodes[1] + for input_node in self.input_nodes: + if not isinstance(X.layout, FixedLayout): + input_node.freeze_layout() + + Y = self.output_node + if template_buffer_node is not None: + Y = template_buffer_node + + Bias, extra_inputs, extra_names = self._get_extra_inputs_and_names(op) + + # Define Kernel call signature + # Important: This step also populates Kernel name to node mapping data structures, + # which are required further below ( for example by the template renderer ) + inputs = [X, W, Bias, *extra_inputs] + names = ["X", "W", "Bias", *extra_names] + ["Y"] + names_str = ",".join(names) + if self.input_reorder is not None: + input_reorder = self.input_reorder + else: + input_reorder = None + + # The layouts might have changed between autotuning and this call if they were FlexibleLayout + # we need to adapt, which might lead to suboptimal performance. + op = self.fix_op_layout(op, X, W, Bias, Y) + + # to make op mutable without affecting others + op = copy.deepcopy(op) + is_scaled_mm = len(self.input_nodes) in (4, 5) + if Bias is not None and not is_scaled_mm: + assert Bias.get_dtype() == X.get_dtype() + # This might have been set to void during filtering, when the assumption was still that there's no C + # operand + op.C.element = op.A.element + + assert op.C.element == op.D.element, ( + f"Expect C and D to have the same dtype, found {op.C.element} and {op.D.element}" + ) + + argument_template, epilogue_template = self._get_template_args(op) + should_swap_xw: bool = False + if Bias is not None and self._has_tma_epilogue(op): + if ( + op.epilogue_schedule + != cutlass_lib.EpilogueScheduleType.EpilogueTransposed + and self.should_swap_XW(Bias) + ): + # TMA epilogue requires bias vector in column major to get best perf. + op = self.swap_XW(op) + should_swap_xw = True + + name_to_buffer = {node.get_name(): node for node in self.input_nodes} + # handle the fake output buffer during lowering + name_to_buffer[Y.get_name()] = Y # type: ignore[assignment] + + if epilogue_nodes or is_scaled_mm: + if epilogue_nodes: + ( + input_names, + output_names, + var_name_to_buffer_name, + evt_py_code, + ) = CutlassEVTCodegen.ir_to_evt_python_code( + Y.get_name(), epilogue_nodes, V.kernel.removed_buffers + ) + + # TODO: mlazos remove this by returning buffer metadata from + # ir_to_evt_python code + for name, buf in ( + V.graph.name_to_buffer | V.graph.graph_inputs + ).items(): + if name not in name_to_buffer: + name_to_buffer[name] = buf # type: ignore[assignment] + + D_output_name = var_name_to_buffer_name["D"] + D_output_buffer = name_to_buffer[D_output_name] + Y = D_output_buffer # type: ignore[assignment] + # Interestingly, I don't think the rest of the layout matters here since we + # use the properties of the Y buffer to fill in D's properties in the epilogue + # args. This is needed though because it defines types expected in the epilogue args. + op.D.element = cutlass_utils.torch_dtype_to_cutlass_type( + D_output_buffer.get_dtype() + ) + + assert output_names, "There should be at least one write" + + epilogue_inputs = [name_to_buffer[name] for name in input_names] + outputs = [name_to_buffer[name] for name in output_names] + else: # Scaled MM, we read the two scale matrices (and optional bias) and write a single output + bias = None if len(self.input_nodes) < 5 else self.input_nodes[4] + bias_name = bias.get_name() if bias else None + + ( + evt_read_names, + var_name_to_buffer_name, + evt_py_code, + ) = scaled_mm_evt( + self.input_nodes[2].get_name(), # scale_A + self.input_nodes[3].get_name(), # scale_B + bias_name, + Y.get_name(), + ) + + input_names = list(evt_read_names) + output_names = [] # We only need Y + epilogue_inputs = [self.input_nodes[2], self.input_nodes[3]] + if bias: + epilogue_inputs.append(bias) + outputs = [] + + acc_dtype = cutlass_utils.get_accumulator_dtype( + [X.get_dtype(), W.get_dtype()] + ) + assert acc_dtype, "Could not determine accumulator dtype" + + evt_name, evt_args, evt_code, evt_arg_renames = self._render_evt( + op, + evt_py_code, + var_name_to_buffer_name, + name_to_buffer, + Y.get_dtype(), + acc_dtype, + ) + + inputs = [ + X, + W, + Bias, + *epilogue_inputs, # type: ignore[list-item] + Y, + *extra_inputs, + ] + input_names = [evt_arg_renames.get(name) for name in input_names] + output_names = [evt_arg_renames.get(name) for name in output_names] + + names_str = ",".join( + ["X", "W", "Bias", *input_names, "Y", *output_names, *extra_names] + ) + else: + evt_name = None + outputs = [Y] + evt_args = f"{{ElementComputeEpilogue({self.alpha}), ElementComputeEpilogue({self.beta})}}" + evt_code = "" + + kernel_call_signature = kernel.def_kernel( + inputs=inputs, # type: ignore[arg-type] + outputs=outputs, # type: ignore[arg-type] + names_str=names_str, + input_reorder=input_reorder, + ) + + test_call_statement = self.test_call_statement(kernel, inputs, names_str) + + instance_definition, instance_type = self._define_gemm_instance(op, evt_name) + dynamic_cluster = self._dynamic_cluster_block(op) + + options = { + "alpha": self.alpha, + "beta": self.beta, + "X": X, + "W": W, + "Y": Y, + "kernel_call_signature": kernel_call_signature, + "Bias": Bias, + "epilogue_template": epilogue_template, + "argument_template": argument_template, + "should_swap_xw": should_swap_xw, + "template": self, + "kernel": kernel, + "instance_definition": instance_definition, + "instance_type": instance_type, + "input_reorder": self.input_reorder, + "epilogue_args": evt_args, + "test_call_statement": test_call_statement, + "op_conf_name": op.configuration_name(), + "epilogue_visitor_tree": evt_code, + "dynamic_cluster": dynamic_cluster, + } + options.update(dict(zip(extra_names, extra_inputs))) + res = self._template_from_string(self._get_template()).render(**options) + if inductor_cutlass_config.generate_test_runner and not is_dynamic( + X, W, Y, Bias + ): + test_runner_code = self._template_from_string( + GEMM_STANDALONE_RUNNER_TEMPLATE + ).render(**options) + res += "\n\n" + test_runner_code + + # splice to remove trailing spaces in each line + buf = IndentedBuffer() + buf.splice(res) + return buf.getvalue() + + def test_call_statement( + self, + kernel, + input_nodes, + names_str: str = "", + ) -> str: + """ + Helper method to render the Cutlass CUDA/XPU C++ code required for calling the GEMM operation in the standalone + test runner that might also be generated along with the rest of the code, if the corresponding config is + enabled. + + Returns a C++ statement that calls the GEMM operation with the correct arguments. + """ + _, __, arg_types = kernel.args.cpp_argdefs(cutlass_utils.DTYPE_TO_CUTLASS_TYPE) + arg_names = [name.strip() for name in names_str.strip().split(",")] + arg_names = self._update_arg_names_for_test_call_statement( + arg_names, input_nodes + ) + arguments = [ + f"(({arg_type}){arg_name}_data.get())" + for arg_type, arg_name in zip(arg_types, arg_names) + ] + return f"{kernel.kernel_name}({', '.join(arguments)}, M, N, K, B, lda, ldb, ldc, ldd, 0, 0, 0, swizzle, workspace_size_ptr, (uint8_t*)workspace_data.get(), 0);" # noqa: B950 + + def _render_evt( + self, + op: GemmOperation, + evt_py_code: str, + buffer_renames: dict[str, str], + name_to_buffer: dict[str, Buffer], + output_dtype: torch.dtype, + accumulator_dtype: torch.dtype, + ) -> tuple[str, str, str, EVTArgRenames]: # type: ignore[name-defined] # noqa: F821 + raise NotImplementedError("_render_evt in CUTLASSGemmTemplate not implemented") + + +class CUTLASS3xGemmTemplate(CUTLASSGemmTemplate): + """ + CUTLASS 3x GEMM Template, which is used to generate CUTLASS GEMM kernels + including those which allow flexible fusions with epilogues. + """ + + @staticmethod + def add_cutlass_gemm_choices( + choices: list[ChoiceCaller], + layout: ir.Layout, + input_nodes: list[Buffer], + alpha: float | int = 1, + beta: float | int = 0, + input_reorder: list[int] | None = None, + use_fast_accum: bool | None = None, + **extra_kwargs, + ) -> None: + template = CUTLASS3xGemmTemplate( + input_nodes, + layout, + alpha, + beta, + input_reorder, + use_fast_accum, + ) + template._add_cutlass_gemm_choices( + choices, layout, input_nodes, alpha, beta, input_reorder, **extra_kwargs + ) + + @staticmethod + @functools.lru_cache(1) + def _get_supported_ops() -> "list[cutlass_library.gemm_operation.GemmOperation]": # type: ignore[name-defined] # noqa: F821 + import cutlass_library.library as cutlass_lib + + return [cutlass_lib.GemmKind.Universal3x] + + def _get_template(self) -> str: + return GEMM_TEMPLATE_CUTLASS_3X + + def _get_template_args( + self, + op: "cutlass_library.gemm_op.GemmOperation", # type: ignore[name-defined] # noqa: F821 + ) -> tuple[str, str | None]: + return (GEMM_ARGS_CUTLASS_3X, GEMM_ARGS_CUTLASS_3X_EPILOGUE) + + @staticmethod + def _has_tma_epilogue( # noqa: F821 # type: ignore[arg-type,name-defined] + op: "cutlass_library.gemm_op.GemmOperation", # type: ignore[name-defined,arg-type] # noqa: F821 + ) -> bool: # type: ignore[name-defined] + """Helper method: Determine whether a given Cutlass GEMM op has a TMA Epilogue""" + assert cutlass_utils.try_import_cutlass() + import cutlass_library.library as cutlass_lib + + result = False + if op.gemm_kind == cutlass_lib.GemmKind.Universal3x: + epilogue_schedule_str = str(op.epilogue_schedule).split(".")[-1] + result = epilogue_schedule_str.lower().startswith("tma") + return result + + @staticmethod + def supports_epilogue_fusion(op: GemmOperation) -> bool: + return CUTLASS3xGemmTemplate._has_tma_epilogue(op) + + def _are_inputs_layout_compatible(self, layouts: list[Layout]) -> bool: + """ + Evaluates whether input layouts are compatible for General Matrix Multiply (GEMM). + + This function checks compatibility of A, B, and possibly C operand layouts for + a General Matrix Multiply (GEMM) operation, expressed as 'alpha * matmul(A, B) + beta * C'. + It verifies requirements such as matching data types, minimum rank, and suitability + for broadcasting, as defined by PyTorch operations like `torch.matmul`, `torch.aten.mm`, + `addmm`, `bmm`, `baddbmm`, etc. + + Args: + layouts (List[Layout]): List containing 2 or 3 Layout objects representing + the input matrices A, B, and possibly C. + + Returns: + bool: True if layouts are GEMM compatible, otherwise False. + """ + assert 2 <= len(layouts) <= 5 + # Check if A and B are compatible + A_layout, B_layout = layouts[:2] + if len(A_layout.size) < 1: + return False + if len(B_layout.size) < 1: + return False + A_size = list(V.graph.sizevars.guarding_hints_or_throw(A_layout.size)) + B_size = list(V.graph.sizevars.guarding_hints_or_throw(B_layout.size)) + if len(A_size) < 2: + A_size.insert(0, 1) + if len(B_size) < 2: + A_size.insert(1, 1) + # Are batch dims broadcastable? + while len(A_size) < len(B_size): + A_size.insert(0, 1) + while len(B_size) < len(A_size): + B_size.insert(0, 1) + K = max(A_size[-1], B_size[-2]) + M = A_size[-2] + N = B_size[-1] + if K != A_size[-1] and A_size[-1] != 1: + return False + if K != B_size[-2] and B_size[-1] != 1: + return False + # check batch dim broadcastable + for i in range(len(A_size) - 2): + if A_size[i] != B_size[i] and A_size[i] != 1 and B_size[i] != 1: + return False + if len(layouts) == 3: + C_layout = layouts[2] + C_size = list(V.graph.sizevars.guarding_hints_or_throw(C_layout.size)) + while len(C_size) < len(A_size): + C_size.insert(0, 1) + # check batch dims + for i in range(len(A_size) - 2): + bd = max(A_size[i], B_size[i]) + if bd != C_size[i] and C_size[i] != 1: + return False + if len(C_size) > len(A_size): + # This may happen if the last elements of C are contiguous and + # their multiplied size equals the last dim size of B + if M != C_size[len(A_size) - 2] and C_size[len(A_size) - 2] != 1: + return False + remaining_size = 1 + for i in range(len(A_size) - 1, len(C_size)): + remaining_size *= C_size[i] + if N != remaining_size and remaining_size != 1: + return False + return True + assert len(C_size) == len(A_size) + if M != C_size[-2] and C_size[-2] != 1: + return False + if N != C_size[-1] and C_size[-1] != 1: + return False + return True + + def _render_evt( + self, + op: GemmOperation, + evt_py_code: str, + var_name_to_buffer_name: dict[str, str], + name_to_buffer: dict[str, Buffer], + output_dtype: torch.dtype, + accumulator_dtype: torch.dtype, + ) -> tuple[str, str, str, EVTArgRenames]: + from .lib_extensions.evt_extensions import create_example_tensors, trace + + acc_dtype = torch_dtype_to_cutlass_type(accumulator_dtype) + output_dtype = torch_dtype_to_cutlass_type(output_dtype) + + # TODO: size_hint_fn is passed to both create_example_tensors (just for + # tracing examples) and trace -> _render_argument_type -> _get_arg_from_node + # where stride values are baked as C++ literals into the generated CUTLASS + # argument struct. For the latter, accessing hint is wrong: even for + # backed symbols we access the hint without installing a guard, so the + # generated strides could be incorrect for different dynamic shape inputs. + # guarding_hint_or_throw would be wrong here — it does not install + # guards either. This should use guard_int to properly guard on the values. + examples = create_example_tensors( + var_name_to_buffer_name, + name_to_buffer, # type: ignore[arg-type] + V.graph.sizevars.optimization_hint, + ) + evt_name, evt_args, evt_code, arg_renames = trace( + evt_py_code, + examples, + acc_dtype, + output_dtype, + op.tile_description, # type: ignore[attr-defined] + op.epilogue_schedule, # type: ignore[attr-defined] + {k: name_to_buffer[v] for k, v in var_name_to_buffer_name.items()}, # type: ignore[arg-type,misc] + V.graph.sizevars.guarding_hint_or_throw, + kernel_schedule=op.kernel_schedule, + ) + + return ( + evt_name, + evt_args, + evt_code, + arg_renames, + ) + + def _shape_match( + self, + op: "cutlass_library.gemm_op.GemmOperation", # type: ignore[name-defined] # noqa: F821 + ) -> bool: + return True + + def _alignment_match( + self, + op: "cutlass_library.gemm_op.GemmOperation", # type: ignore[name-defined] # noqa: F821 + ) -> bool: + return True + + def _set_bias_layout_and_alignment( + self, + op: "cutlass_library.gemm_op.GemmOperation", # type: ignore[name-defined] # noqa: F821 + ) -> bool: + import cutlass_library.library as cutlass_lib + + has_bias = len(self.input_nodes) == 3 and self.input_nodes[2] is not None + if has_bias: + Bias = self.input_nodes[2] + # bias dtype + op.C.element = cutlass_utils.torch_dtype_to_cutlass_type( + Bias.get_layout().dtype + ) + + # Bias layout + bias_layout = CUTLASSGemmTemplate.cutlass_layout(Bias.get_layout()) + op.C.layout = bias_layout + + # Bias alignment + status = self.set_alignment(Bias.get_layout(), op.C) + if not status: + return False + else: + op.C.element = cutlass_lib.DataType.void + return True + + def _define_gemm_instance( + self, + op: GemmOperation, + evt_name: str | None = None, + ) -> tuple[str, str]: + """Defines and renders the Cutlass / CUDA/XPU C++ code for a given GEMM operation instance. + + This function uses the Cutlass library to generate key parts of the codegen process. General Matrix Multiply + forms a core part of a number of scientific applications, so this efficient and adaptable implementation is + crucial. + + Args: + op (cutlass_library.gemm_op.GemmOperation): This is the core GEMM operation that we are defining and rendering. + + Returns: + tuple[str, str]: A tuple where the first part is a string that constitutes the defined GEMM operation in C++ + code (render) and the second part is the string that specifies the operation type. + """ + assert cutlass_utils.try_import_cutlass() + import cutlass_library.library as cutlass_lib + + from .lib_extensions import gemm_operation_extensions as gemm_extensions + + emitter = gemm_extensions.EmitGemmUniversal3xInstanceWithEVT( + evt_name=evt_name, device_type=self.device_type + ) # type: ignore[call-arg] + + if not hasattr(op, "epilogue_functor") or not isinstance( + op.epilogue_functor, enum.Enum + ): + op = copy.deepcopy(op) + op.epilogue_functor = cutlass_lib.EpilogueFunctor.LinearCombination + + op_def = emitter.emit(op) + pattern = re.compile(r"\s*struct\s(.*?)\s:") + decl = [line for line in op_def.split("\n") if "struct " in line][-1] + + match = pattern.match(decl) + if match is None: + raise RuntimeError("Invalid Gemm config: \n" + op_def) + op_type = match.groups()[0] + if op.gemm_kind == cutlass_lib.GemmKind.Universal3x: + op_def += f"\n using {op_type}_device_type = cutlass::gemm::device::GemmUniversalAdapter<{op_type}>;\n" + op_type = f"{op_type}_device_type" + + return op_def, op_type + + def _get_extra_inputs_and_names( + self, + op: "cutlass_gemm_op.GemmOperation" = None, # type: ignore[name-defined] # noqa: F821 + ) -> tuple[Buffer | None, list[Buffer | None], list[str]]: + Bias = self.input_nodes[2] if len(self.input_nodes) == 3 else None + inputs: list[Buffer | None] = [] + names: list[str] = [] + return (Bias, inputs, names) + + def _update_arg_names_for_test_call_statement( + self, + arg_names: list[str], + input_nodes: list[Buffer], + ) -> list[str]: + if input_nodes[2] is None: + del arg_names[2] + else: + # Reorder them as Bias, A, B + if self.input_reorder is not None: + arg_names[0 : len(self.input_reorder)] = [ + arg_names[i] for i in self.input_reorder + ] + return arg_names + + def render_gemm_arguments( + self, + argument_template: str, + epilogue_template: str, + should_swap_xw: bool, + X: IRNode, + W: IRNode, + Bias: IRNode, + Y: IRNode, + alpha: float, + beta: float, + kernel: CUTLASSTemplateKernel, + epilogue_args, + ) -> str: + """ + Render the Cutlass CUDA/XPU C++ code required for passing arguments to the GEMM operation. + + Args: + argument_template (str): Template for the GEMM operation arguments. + epilogue_template (str): Template for the epilogue arguments. + should_swap_xw (bool): Determines whether X, W operands should be swapped. If True, applies an explicit + transpose operation to X and W. + X (IRNode): The X input tensor. + W (IRNode): The W input tensor. + Bias (IRNode): The bias tensor. + Y (IRNode): The output tensor. + alpha (float): Scaling factor for the product of the inputs. + beta (float): Scaling factor for the output tensor. + kernel (CUTLASSTemplateKernel): CUDA/XPU Template kernel for the operation. + epilogue_args (any): Additional arguments for the epilogue state. + + Returns: + str: A block of CUDA/XPU C++ code as a string, ready to be used as arguments for the GEMM operation. + + Note: If `should_swap_xw` is True, a transpose operation will be applied to the X, W, Bias, and Y + tensors. This operation also implies the M and N dimensions of Bias and GEMM output to be swapped + before the function call. + """ + options = { + "alpha": alpha, + "beta": beta, + "X": X, + "W": W, + "Y": Y, + "Bias": Bias, + "template": self, + "kernel": kernel, + "M": "M", + "N": "N", + "epilogue_args": epilogue_args, + } + assert epilogue_template is not None + + if should_swap_xw: + # Swap + def clone_with_transposed_stride(node: IRNode) -> IRNode: + old_layout = node.get_layout() + new_stride = list(old_layout.stride) # type: ignore[union-attr] + new_stride[-2], new_stride[-1] = new_stride[-1], new_stride[-2] + assert old_layout.device is not None + new_layout = FixedLayout( + old_layout.device, + old_layout.dtype, + list(old_layout.size), # type: ignore[union-attr] + new_stride, + old_layout.offset, # type: ignore[union-attr] + ) + return Buffer(name=node.get_name(), layout=new_layout) + + new_X = clone_with_transposed_stride(X) + new_W = clone_with_transposed_stride(W) + new_Bias = clone_with_transposed_stride(Bias) + new_Y = clone_with_transposed_stride(Y) + options["X"], options["W"], options["Bias"], options["Y"] = ( + new_W, + new_X, + new_Bias, + new_Y, + ) + options["M"], options["N"] = "N", "M" + + epilogue_arguments = self._template_from_string(epilogue_template).render( + **options + ) + arguments = self._template_from_string(argument_template).render( + epilogue_arguments=epilogue_arguments, **options + ) + + return arguments + + +class CUTLASS2xGemmTemplate(CUTLASSGemmTemplate): + """CUTLASS 2x GEMM Template, which is used to generate CUTLASS 2x GEMM kernels""" + + def __init__( + self, + input_nodes: list[Buffer], + layout: Layout, + alpha: float, + beta: float, + input_reorder: list[int] | None = None, + ): + super().__init__(input_nodes, layout, alpha, beta, input_reorder) + + @staticmethod + def add_cutlass_gemm_choices( + choices: list[ChoiceCaller], + layout: ir.Layout, + input_nodes: list[Buffer], + alpha: float | int = 1, + beta: float | int = 0, + input_reorder: list[int] | None = None, + use_fast_accum: bool | None = False, + **extra_kwargs, + ) -> None: + template = CUTLASS2xGemmTemplate( + input_nodes, layout, alpha, beta, input_reorder + ) + template._add_cutlass_gemm_choices( + choices, layout, input_nodes, alpha, beta, input_reorder, **extra_kwargs + ) + + @staticmethod + def _get_supported_ops() -> "list[cutlass_library.gemm_operation.GemmOperation]": # type: ignore[name-defined] # noqa: F821 + import cutlass_library.library as cutlass_lib + + return [cutlass_lib.GemmKind.Universal, cutlass_lib.GemmKind.Sparse] + + @staticmethod + def _has_tma_epilogue(self) -> bool: + return False + + def _get_template(self) -> str: + return GEMM_TEMPLATE_CUTLASS_2X + + def _get_template_args( + self, + op: "cutlass_library.gemm_op.GemmOperation", # type: ignore[name-defined] # noqa: F821 + ) -> tuple[str, str | None]: + import cutlass_library.library as cutlass_lib + + if op.gemm_kind == cutlass_lib.GemmKind.Sparse: + return (GEMM_ARGS_SPARSE_CUTLASS_2X, None) + + return (GEMM_ARGS_CUTLASS_2X, None) + + def _are_inputs_layout_compatible(self, layouts: list[Layout]) -> bool: + """ + Evaluates whether input layouts are compatible for set of operations supported by this class. + + Args: + layouts (List[Layout]): List containing Layout objects representing + the input matrices. + + Returns: + bool: True if layouts are GEMM compatible, otherwise False. + """ + assert len(layouts) == 2 or len(layouts) == 3 + # Check if A and B are compatible + A_layout, B_layout = layouts[:2] + if len(A_layout.size) != 2: + return False + if len(B_layout.size) != 2: + return False + A_size = [int(i) for i in A_layout.size] + B_size = [int(i) for i in B_layout.size] + K = max(A_size[1], B_size[0]) + return (K == A_size[1] or K == 2 * A_size[1]) and K == B_size[0] + + def _shape_match( + self, + op: "cutlass_library.gemm_op.GemmOperation", # type: ignore[name-defined] # noqa: F821 + ) -> bool: + import cutlass_library.library as cutlass_lib + + X, W = self.input_nodes[0], self.input_nodes[1] + + if op.gemm_kind == cutlass_lib.GemmKind.Sparse: + return X.get_size()[1] * 2 == W.get_size()[0] + + return X.get_size()[1] == W.get_size()[0] + + def _alignment_match( + self, + op: "cutlass_library.gemm_op.GemmOperation", # type: ignore[name-defined] # noqa: F821 + ) -> bool: + import cutlass_library.library as cutlass_lib + + if op.gemm_kind != cutlass_lib.GemmKind.Sparse: + return True + + # SparseGemm in CUTLASS has specific alignment check that for + # small k could make some of the choices throw kMisalignedOperand + # CUTLASS error when run, see: + # https://github.com/NVIDIA/cutlass/blob/e01b9b5029b7caca5a43c29f7d2714d7cf1dcae8/include/cutlass/gemm/kernel/sparse_gemm.h#L198-L200 # noqa: B950 + # So, let's skip these choices if that would be the case. + X = self.input_nodes[0] + return (X.get_size()[1] * 2) % op.tile_description.tile_shape[2] == 0 + + def _set_bias_layout_and_alignment( + self, + op: "cutlass_library.gemm_op.GemmOperation", # type: ignore[name-defined] # noqa: F821 + ) -> bool: + import cutlass_library.library as cutlass_lib + + if op.gemm_kind == cutlass_lib.GemmKind.Sparse: + op.C.layout = op.D.layout + return True + + if len(self.input_nodes) >= 3 and self.input_nodes[2] is not None: + Bias = self.input_nodes[2] + bias_layout = CUTLASSGemmTemplate.cutlass_layout(Bias.get_layout()) + if bias_layout != op.D.layout: + # For cutlass2, bias and output layout must match + return False + if not self.set_alignment(Bias.get_layout(), op.C): + return False + else: + op.C.layout = op.D.layout + return True + + def _define_gemm_instance( + self, + op: GemmOperation, + evt_name: str | None = None, + ) -> tuple[str, str]: + """Defines and renders the Cutlass / CUDA C++ code for a given GEMM operation instance. + + This function uses the Cutlass library to generate key parts of the codegen process. General Matrix Multiply + forms a core part of a number of scientific applications, so this efficient and adaptable implementation is + crucial. + + Args: + op (cutlass_library.gemm_op.GemmOperation): This is the core GEMM operation that we are defining and rendering. + + Returns: + tuple[str, str]: A tuple where the first part is a string that constitutes the defined GEMM operation in C++ + code (render) and the second part is the string that specifies the operation type. + """ + assert cutlass_utils.try_import_cutlass() + import cutlass_library.gemm_operation as cutlass_gemm_op + import cutlass_library.library as cutlass_lib + + if op.gemm_kind == cutlass_lib.GemmKind.Sparse: + emitter = cutlass_gemm_op.EmitSparseGemmInstance() + else: + emitter = cutlass_gemm_op.EmitGemmInstance() + op_def = emitter.emit(op) + op_def = op_def.replace( + "cutlass::gemm::device::Gemm", "cutlass::gemm::device::GemmUniversal" + ) + if op.gemm_kind != cutlass_lib.GemmKind.Sparse: + op_def = op_def.replace("false,", "") + pattern = re.compile(r"\s*using\s(.*?)\s=") + decl = op_def.split("\n")[2] + + match = pattern.match(decl) + if match is None: + raise RuntimeError("Invalid Gemm config: \n" + op_def) + op_type = match.groups()[0] + return op_def, op_type + + def _get_extra_inputs_and_names( + self, + op: "cutlass_gemm_op.GemmOperation" = None, # type: ignore[name-defined] # noqa: F821 + ) -> tuple[Buffer | None, list[Buffer | None], list[str]]: + import cutlass_library.library as cutlass_lib + + if op.gemm_kind == cutlass_lib.GemmKind.Sparse: + Bias = None + Meta = self.input_nodes[2] + else: + Bias = None if len(self.input_nodes) == 2 else self.input_nodes[2] + Meta = None + inputs = [Meta] + names = ["Meta"] + return (Bias, inputs, names) + + def _update_arg_names_for_test_call_statement( + self, + arg_names: list[str], + input_nodes: list[Buffer], + ) -> list[str]: + if input_nodes[3] is None: + del arg_names[3] + if input_nodes[2] is None: + del arg_names[2] + return arg_names + + def render_gemm_arguments( + self, + instance_type: str, + argument_template: str, + epilogue_template: str, + should_swap_xw: bool, + X: IRNode, + W: IRNode, + Bias: IRNode, + Meta: IRNode, + Y: IRNode, + alpha: float, + beta: float, + kernel: CUTLASSTemplateKernel, + epilogue_args, + ) -> str: + """ + Render the Cutlass CUDA C++ code required for passing arguments to the GEMM operation. + + Args: + instance_type (str): GEMM instance type. + argument_template (str): Template for the GEMM operation arguments. + epilogue_template (str): Template for the epilogue arguments. + should_swap_xw (bool): Determines whether X, W operands should be swapped. If True, applies an explicit + transpose operation to X and W. + X (IRNode): The X input tensor. + W (IRNode): The W input tensor. + Bias (IRNode): The bias tensor. + Meta (IRNode): The meta tensor. + Y (IRNode): The output tensor. + alpha (float): Scaling factor for the product of the inputs. + beta (float): Scaling factor for the output tensor. + kernel (CUTLASSTemplateKernel): CUDA Template kernel for the operation. + epilogue_args (any): Additional arguments for the epilogue state. + + Returns: + str: A block of CUDA C++ code as a string, ready to be used as arguments for the GEMM operation. + + Note: If `should_swap_xw` is True, a transpose operation will be applied to the X, W, Bias, and Y + tensors. This operation also implies the M and N dimensions of Bias and GEMM output to be swapped + before the function call. + """ + options = { + "instance_type": instance_type, + "alpha": alpha, + "beta": beta, + "X": X, + "W": W, + "Y": Y, + "Bias": Bias, + "Meta": Meta, + "template": self, + "kernel": kernel, + "M": "M", + "N": "N", + "epilogue_args": epilogue_args, + } + + if epilogue_template is None: + arguments = self._template_from_string(argument_template).render( + split_k=1, **options + ) + return arguments + + epilogue_arguments = self._template_from_string(epilogue_template).render( + **options + ) + arguments = self._template_from_string(argument_template).render( + epilogue_arguments=epilogue_arguments, **options + ) + + return arguments diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/kernel.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/kernel.py new file mode 100644 index 0000000000000000000000000000000000000000..5498fc71290833bb5306f06785773b03ff919b17 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/kernel.py @@ -0,0 +1,687 @@ +# mypy: allow-untyped-defs +import functools +import itertools +import logging +from collections import defaultdict +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any, Literal, TYPE_CHECKING + +from sympy import Expr, symbols + +import torch._inductor.config as config +from torch import dtype as torch_dtype +from torch._inductor.codegen.common import get_device_op_overrides +from torch._inductor.codegen.cpp_wrapper_cpu import CppWrapperCpu +from torch._inductor.scheduler import BaseSchedulerNode +from torch._inductor.utils import do_bench_using_profiling, OrderedSet, Placeholder +from torch.utils._sympy.value_ranges import ValueRanges + +from .utils import DTYPE_TO_CUTLASS_TYPE + + +if TYPE_CHECKING: + from .template import ArgInfo + +from ...autotune_process import CUTLASSBenchmarkRequest +from ...ir import ( + Buffer, + ChoiceCaller, + CUTLASSTemplateBuffer, + IRNode, + Layout, + PrimitiveInfoType, + TensorBox, +) +from ...utils import sympy_product +from ...virtualized import V +from ..common import ( + CSEVariable, + IndentedBuffer, + Kernel, + OpOverrides, + WorkspaceArg, + WorkspaceZeroMode, +) +from ..cpp_utils import CppPrinter, DTYPE_TO_CPP + + +if TYPE_CHECKING: + from torch._inductor.codegen.cutlass.template import CUTLASSTemplate + +log = logging.getLogger(__name__) + +cexpr = CppPrinter().doprint + + +def _normalize_idx(index: int, total_length: int) -> int: + return index if index >= 0 else index + total_length + + +ValidLayoutSymbols = Literal["M", "N", "K", "B", "lda", "ldb", "ldc", "ldd"] +ValidLayoutAttrs = Literal["size", "stride"] + + +@dataclass(frozen=True) +class LayoutArg: + node: IRNode + symbol: ValidLayoutSymbols + attr: ValidLayoutAttrs + dim: int + + def matches(self, node, attr, dim) -> bool: + return self.node == node and self.attr == attr and self.dim == dim + + +class CUTLASSKernel(Kernel): + """ + Baseclass for Cutlass based Kernels + """ + + overrides = OpOverrides # type: ignore[assignment] + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.layout_args: dict[str, list[LayoutArg]] = defaultdict(list) + self.size_args: list[Expr | int] = [] + # Mapping from arg name to IRNode. + self.named_nodes: dict[str, IRNode] = {} + + def find_symbol(self, node: IRNode, attr: ValidLayoutAttrs, dim: int) -> str | None: + arg = self.find_layout_arg(node, attr, dim) + return arg.symbol if arg else None + + def find_layout_arg( + self, node: IRNode, attr: ValidLayoutAttrs, dim: int + ) -> LayoutArg | None: + matches = [ + arg + for arg in itertools.chain.from_iterable(self.layout_args.values()) + if arg.matches(node, attr, dim) + ] + if len(matches) >= 1: + # Verify all matches have the same node, attribute, and dimension + # And if they come from the same node, whichever symbol we use is fine. + # if in runtime the logic changes, this would trigger guard + first_match = matches[0] + if not all( + match.node == first_match.node + and match.attr == first_match.attr + and match.dim == first_match.dim + for match in matches + ): + raise AssertionError("All matching layout args should be identical") + return first_match + return None + + def add_layout_arg( + self, symbol: ValidLayoutSymbols, node: IRNode, attr: ValidLayoutAttrs, dim: int + ): + arg = LayoutArg(node, symbol, attr, dim) + self.layout_args[symbol].append(arg) + + def init_layout_args(self) -> None: + X = self.named_nodes["X"] + W = self.named_nodes["W"] + Y = self.named_nodes["Y"] + Bias = self.named_nodes.get("Bias", None) + x_mdim = _normalize_idx(-2, len(X.get_size())) + x_kdim = _normalize_idx(-1, len(X.get_size())) + w_kdim = _normalize_idx(-2, len(W.get_size())) + w_ndim = _normalize_idx(-1, len(W.get_size())) + y_mdim = _normalize_idx(-2, len(Y.get_size())) + y_ndim = _normalize_idx(-1, len(Y.get_size())) + self.add_layout_arg("M", X, "size", x_mdim) + self.add_layout_arg("K", X, "size", x_kdim) + self.add_layout_arg("K", W, "size", w_kdim) + self.add_layout_arg("N", W, "size", w_ndim) + self.add_layout_arg("M", Y, "size", y_mdim) + self.add_layout_arg("N", Y, "size", y_ndim) + if len(X.get_size()) > 2: + self.add_layout_arg("B", X, "size", 0) + + lda_dim = self.find_ld_idx(X) + ldb_dim = self.find_ld_idx(W) + ldc_dim = self.find_ld_idx(Bias) if Bias else None + ldd_dim = self.find_ld_idx(Y) + self.add_layout_arg("lda", X, "stride", lda_dim) + self.add_layout_arg("ldb", W, "stride", ldb_dim) + if Bias is not None and ldc_dim is not None: + self.add_layout_arg("ldc", Bias, "stride", ldc_dim) + self.add_layout_arg("ldd", Y, "stride", ldd_dim) + + def get_layout_args(self) -> tuple[Expr | int, ...]: + X = self.named_nodes["X"] + W = self.named_nodes["W"] + Y = self.named_nodes["Y"] + Bias = self.named_nodes.get("Bias", None) + mdim = _normalize_idx(-2, len(X.get_size())) + ndim = _normalize_idx(-1, len(W.get_size())) + kdim = _normalize_idx(-1, len(X.get_size())) + + def get_ld(node) -> Expr | int: + dim = self.find_ld_idx(node) + return node.get_stride()[dim] + + M = X.get_size()[mdim] + N = W.get_size()[ndim] + K = X.get_size()[kdim] + B = X.get_size()[0] if len(X.get_size()) > 2 else 1 + LDA = get_ld(X) + LDB = get_ld(W) + LDC = get_ld(Bias) if Bias else 0 + LDD = get_ld(Y) + return (M, N, K, B, LDA, LDB, LDC, LDD) + + def get_dynamic_shape_args(self) -> list[Expr | int]: + return [*self.get_layout_args(), *self.size_args] + + def get_offset_args(self) -> list[Expr]: + return [node.get_layout().offset for node in self.named_nodes.values()] + + @staticmethod + def find_ld_idx(node: IRNode) -> int: + strides = node.get_stride() + # Handle 1D tensor case + if V.graph.sizevars.statically_known_equals(strides[-1], 1): + return _normalize_idx(-2, len(strides)) + + assert V.graph.sizevars.statically_known_equals(strides[-2], 1), strides[-2] + return _normalize_idx(-1, len(strides)) + + +class CUTLASSTemplateKernel(CUTLASSKernel): + """ + Template kernels defined by Cutlass in C++. + """ + + def __init__( + self, + kernel_name: str, + runtime_arg_info: list["ArgInfo"], + runtime_arg_values: list[Any], + device_type: str = "cuda", # type: ignore[assignment] + ) -> None: + """ + Initializes a new instance of the CUTLASSTemplateKernel class. + + Args: + kernel_name (str): The name of the kernel. + """ + super().__init__() + self.kernel_name = kernel_name + self.runtime_arg_info = runtime_arg_info + self.runtime_arg_values = runtime_arg_values + self.device_type = device_type + self.device_codegen = get_device_op_overrides(self.device_type) + self._EXTRA_CPP_ARGS = f"size_t* workspace_size, uint8_t* workspace, {self.device_codegen.cpp_stream_type()} stream" + + def check_not_null(self, node: IRNode) -> str: + """ + Generates code to check that a node is not null. + """ + if node is None: + return "" + + size_str = self.size(node, 0, -1) + name_str = self.arg_name(node) + if name_str is None: + return "" + + res = IndentedBuffer(initial_indent=2) + res.tabwidth = 1 + res.splice( + f""" + {{ + if (!{name_str}) {{ + int64_t {name_str}_size = {size_str}; + if ({name_str}_size > 0) {{ + throw std::runtime_error("input {name_str} is null but size is not 0!"); + }} + }} + }} + """ + ) + return res.getvalue() + + def get_signature(self) -> str: + return self.signature + + def def_kernel( + self, + inputs: list[IRNode], + outputs: list[IRNode], + names_str: str = "", + input_reorder: list[int] | None = None, + ) -> str: + """ + Hook called from template code to generate function definition and + needed args. + + Args: + inputs: List of input IRNodes + outputs: List of output IRNodes + names_str: Comma separated list of input + output argument names. + input_reorder: The actual order of input nodes. + e.g. The template might have input argument defined as [X, W, Bias], + and the actual input passed into this template could be [Bias, X, W]. + In this case, the `input_reorder` would be [2, 0, 1]. + additional_size_args: Additional size arguments for epilogue inputs + """ + # NB: name order matters here, it's used to match up offsets + names = [x.strip() for x in names_str.strip().split(",")] + if len(inputs) + len(outputs) != len(names): + raise RuntimeError( + f"{len(inputs) + len(outputs)=} != {len(names)=}, {inputs=}, {outputs=}, {names=}" + ) + + if input_reorder is not None: + assert len(inputs) == len(input_reorder) + else: + input_reorder = list(range(len(inputs))) + + for idx in input_reorder: + name = names[idx] + node = inputs[idx] + if node is not None: + self.named_nodes[name] = node + self.args.input_buffers[node.get_name()] = name + + free_symbols: OrderedSet[Expr] = OrderedSet() + for name, node in zip(names[len(inputs) : len(inputs) + len(outputs)], outputs): + if node is not None: + # NB: named nodes must be populated in the order of names + self.named_nodes[name] = node + self.args.output_buffers[node.get_name()] = name + + if name not in ( + "X", + "W", + "Bias", + "Y", + ): # we handle these symbolic shapes explicitly + for expr in itertools.chain(node.get_size(), node.get_stride()): + if isinstance(expr, Expr): + for s in expr.free_symbols: + free_symbols.add(s) # type: ignore[arg-type] + + arg_defs, *_ = self.args.cpp_argdefs(DTYPE_TO_CUTLASS_TYPE) + + self.init_layout_args() + size_vars = ["M", "N", "K", "B", "lda", "ldb", "ldc", "ldd"] + size_vars.extend(str(s) for s in free_symbols) + self.size_args.extend(free_symbols) + size_args = [f"const int {s}" for s in size_vars] + offset_args = [f"const int {name}_offset" for name in self.named_nodes] + runtime_arg_decls = ",".join( + [f"{arg.ty} {arg.name}" for arg in self.runtime_arg_info] + ) + if runtime_arg_decls: + runtime_arg_decls += ", " + + signature = ( + f"int {self.kernel_name}({', '.join(arg_defs + size_args + offset_args)},\ + {runtime_arg_decls}{self._EXTRA_CPP_ARGS})" + ) + self.signature = signature + return signature + + def call_kernel( + self, + name: str, + node: "CUTLASSTemplateBuffer", # type: ignore[name-defined] + ) -> None: + """ + Generates code to call the kernel through V.graph.wrapper_code. + used from within torch._inductor.wrapper.PythonWrapperCodegen + + name: Name of kernel function. + node: The CUTLASSTemplateBuffer node which contains information about the kernel, it's fused epilogue nodes + as well as all required inputs and outputs. + """ + wrapper = V.graph.wrapper_code + + arg_types: list[Any] + if V.graph.cpp_wrapper: + # Make sure we initialize these kernels since they're exported as + # C-style symbol names. + assert isinstance(wrapper, CppWrapperCpu) + wrapper.initialized_kernels[name] = self + # We always originally initialize name with "KERNEL_NAME". So, we + # we replace with the real kernel name passed as an arg to this function. + self.signature = self.signature.replace(str(Placeholder.KERNEL_NAME), name) + _, call_args, arg_types = self.args.cpp_argdefs(DTYPE_TO_CUTLASS_TYPE) + else: + _, call_args, _, arg_types = self.args.python_argdefs() + + dynamic_shape_args = self.get_dynamic_shape_args() + offset_args = self.get_offset_args() + call_args.extend(dynamic_shape_args) # type: ignore[arg-type] + call_args.extend(offset_args) # type: ignore[arg-type] + for arg in self.runtime_arg_values: + call_args.append(str(arg)) + arg_types.extend("const int" for _ in dynamic_shape_args) + arg_types.extend("const int" for _ in offset_args) + for arg in self.runtime_arg_info: + arg_types.append(arg.ty) + # dynamo wraps unspec variable as 0d CPU tensor, need convert to scalar + for i in range(len(call_args)): + if V.graph.is_unspec_arg(call_args[i]): + call_args[i] = call_args[i] + ".item()" + elif isinstance(arg_types[i], torch_dtype): + call_args[i] = ( + call_args[i] + if V.graph.cpp_wrapper + else f"c_void_p({call_args[i]}.data_ptr())" + ) + + # workspace_size ptr is NULL to mark this call is not intended for retrieving workspace_size. + # workspace_size should have already been retrieved prior to this call. + # workspace_size is here. + call_args.append("nullptr" if V.graph.cpp_wrapper else "None") + if V.graph.cpp_wrapper: + arg_types.append("size_t*") + + if node.get_workspace_size() > 0: + ws = WorkspaceArg( + count=node.get_workspace_size(), + device=V.graph.get_current_device_or_throw(), + zero_mode=WorkspaceZeroMode.UNINITIALIZED, + outer_name=WorkspaceArg.unique_name(), + ) + wrapper.generate_workspace_allocation(ws) + workspace = str(ws.outer_name) + call_args.append( + workspace + if V.graph.cpp_wrapper + else f"c_void_p({workspace}.data_ptr())" + ) + else: + ws = None + call_args.append("nullptr" if V.graph.cpp_wrapper else "None") + if V.graph.cpp_wrapper: + arg_types.append("uint8_t*") + + wrapper.generate_kernel_call( + name, + call_args, + triton=False, + arg_types=arg_types, + ) + if ws: + wrapper.generate_workspace_deallocation(ws) + + def dtype(self, node: IRNode) -> str | None: + """ + Generates code which represents dtype of a given node. + """ + + if node is None: + return "void" + return DTYPE_TO_CPP.get(node.get_layout().dtype) + + def cutlass_dtype(self, node: IRNode, default_dtype="void") -> str | None: + # Helper method, called into from CUTLASSGemmTemplate + if node is None: + return default_dtype + from torch._inductor.codegen.cutlass.template import CUTLASSTemplate + + return CUTLASSTemplate._DTYPE_TO_CUTLASS[node.get_layout().dtype] + + def max_valid_index(self, node: IRNode, default=-1): + # Helper method, called into from CUTLASSGemmTemplate + if node is None: + return default + max_valid_offset = 0 + for i in range(len(node.get_size())): + max_valid_offset += (node.get_size()[i] - 1) * node.get_stride()[i] + return max_valid_offset + + def ptr(self, node: IRNode) -> str: + """ + Generates code which represents pointer of a given node. + """ + + if node is None: + return "nullptr" + arg_name = self.arg_name(node) + if arg_name is None: + return "nullptr" + return f"{arg_name} + {arg_name}_offset" + + def size( + self, + node: IRNode, + start_index: int, + end_index: int | None = None, + default_value: int = 0, + ) -> str: + """ + Hook called from template code to get the size of an arg. + Generates code which represents size of a given node in [start_index, end_index). + If node is None, returns default_value. + + TODO: Will add needed args to pass it in if it is dynamic. + """ + + if node is None: + return str(default_value) + + start_index = _normalize_idx(start_index, len(node.get_size())) + if end_index is None: + end_index = start_index + end_index = _normalize_idx(end_index, len(node.get_size())) + sizes = [ + self.find_symbol(node, "size", dim=i) or node.get_size()[i] + for i in range(start_index, end_index + 1) + ] + if len(sizes) == 0: + return str(default_value) + + sizes = [symbols(v) if isinstance(v, str) else v for v in sizes] + val = sympy_product(sizes) + return val + + def stride(self, node: IRNode, index: int, default_value: int = 0) -> str: + """ + Hook called from template code to get the stride of an arg. + Generates code which represents stride of a given node at index. + If node is None, returns default_value. + + TODO: Will add needed args to pass it in if it is dynamic. + """ + + if node is None: + return str(default_value) + + index = _normalize_idx(index, len(node.get_size())) + if index < 0: + return str(default_value) + + stride = node.get_stride()[index] + if V.graph.sizevars.statically_known_leq(stride, 1): + return str(stride) + return self.find_symbol(node, "stride", dim=index) or str(stride) + + def batch_stride(self, node: IRNode, default_value: int = 0) -> str: + """ + Hook called from template code to get the batch stride of an arg. + Returns 0 if batch dim is not present. + + This method assumes that batch stride is the largest stride. + """ + + if node is None: + return str(default_value) + + if len(node.get_size()) < 3: + return str(default_value) + + batch_stride = node.get_stride()[0] + if V.graph.sizevars.statically_known_leq(batch_stride, 1): + return str(batch_stride) + + return "{}*{}".format( + self.find_symbol(node, "size", dim=1) or node.get_size()[1], + self.find_symbol(node, "size", dim=2) or node.get_size()[2], + ) + + def row_or_column_stride(self, node: IRNode, default_value: int = 0) -> str: + """ + Hook called from template code to get the row or column stride of an arg. + This is required by some CUTLASS 2.X APIs. + If the node is in row_major, it returns stride[-2]. + If the node is in column_major, it returns stride[-1]. + + TODO: Will add needed args to pass it in if it is dynamic. + """ + + if node is None or len(node.get_stride()) < 2: + return str(default_value) + + stride0 = node.get_stride()[-1] + stride1 = node.get_stride()[-2] + if stride0 == 1: + return cexpr(self.rename_indexing(stride1)) + elif stride1 == 1: + return cexpr(self.rename_indexing(stride0)) + else: + raise RuntimeError( + f"At least 1 stride should be 1. Strides: {node.get_stride()=}" + ) + + def load(self, name: str, index: Expr, mode: Any = None) -> CSEVariable: + """ + Mock load function for memory planning to optimize allocations properly. + """ + return self.create_cse_var(name, bounds=ValueRanges.unknown()) + + def store(self, name: str, index: Expr, value: Any, mode: Any = None) -> None: + """ + Mock store function for memory planning to optimize allocations properly. + """ + self.store_buffer_names.add(name) + + +class CUTLASSTemplateCaller(ChoiceCaller): + """ + CUTLASSTemplateCaller + + This class represents a caller for CUTLASS template kernels. It is a subclass of ChoiceCaller. + Attributes: + name (str): The name of the caller. + category (str): The category of the caller. + bmreq (CUTLASSBenchmarkRequest): The benchmark request for the caller. + template_buffer (CUTLASSTemplateBuffer): The template buffer for the caller. + """ + + def __init__( + self, + name: str, + category: str, + input_nodes: list[Buffer], + layout: Layout, + make_kernel_render: Callable[ + [CUTLASSTemplateBuffer, list[BaseSchedulerNode] | None], + tuple[CUTLASSTemplateKernel, functools.partial[str]], + ], + bmreq: CUTLASSBenchmarkRequest, + supports_epilogue_fusion: bool, + template: "CUTLASSTemplate", # type: ignore[name-defined] + info_kwargs: dict[str, PrimitiveInfoType | list[PrimitiveInfoType]] | None, # type: ignore[type-arg] + description: str, + ) -> None: + super().__init__(name, input_nodes, layout, description) + self.category = category + self.make_kernel_render = make_kernel_render + self.bmreq = bmreq + self.supports_epilogue_fusion = supports_epilogue_fusion + self.template = template + self.info_kwargs = info_kwargs + + def precompile(self) -> None: + assert self.bmreq is not None + self.bmreq.precompile() + + def benchmark(self, *args, out) -> float: + assert self.bmreq is not None + if config.profile_bandwidth_with_do_bench_using_profiling: + algo = self.bmreq.make_run_fn(*args, out=out) + return do_bench_using_profiling(algo) + return self.bmreq.benchmark(*args, out=out) + + def __str__(self) -> str: + return f"CUTLASSTemplateCaller(source_file={self.bmreq.source_file})" + + def call_name(self) -> str: + return f"cutlass_template_kernels.{self.name}" + + def kernel_hash_key(self) -> str: + """ + Return kernel hash key that does not depend on swizzle. + """ + return "-".join( + [ + self.category, + self.bmreq.hash_key, + ] + ) + + def hash_key(self) -> str: + """ + Return kernel hash key that does not depend on swizzle. + """ + swizzle_str: str = ( + str(self.info_kwargs.get("swizzle")) + if isinstance(self.info_kwargs, dict) + else "None" + ) + return "-".join( + [ + self.category, + self.bmreq.hash_key, + swizzle_str, + ] + ) + + def info_dict(self) -> dict[str, PrimitiveInfoType | list[PrimitiveInfoType]]: + """ + Information returned here is logged to the autotune log file when that is enabled. + + In general, we should avoid calling this function as it is expensive to compute, + and can add up very fast. + """ + if self.info_kwargs is not None and "op" in self.info_kwargs: + op: Any = self.info_kwargs["op"] + return { + "backend": "CUDA", + "op_type": type(op).__name__, + "op_conf_name": str(op.configuration_name()), + "op_arch": str(op.arch), + "tile_shape": str(op.tile_description.tile_shape), + "epilogue_schedule": str(op.epilogue_schedule), + "kernel_schedule": str(op.kernel_schedule), + "element_accumulator": str(op.accumulator_type()), + "op_name": str(op.procedural_name()), + "instruction_shape": str( + op.tile_description.math_instruction.instruction_shape + ), + "swizzle": str(self.info_kwargs["swizzle"]), + } + else: + return {"backend": "CUDA", "op_type": "unknown"} + + def output_node(self) -> TensorBox: + self.bmreq.update_workspace_size() + buffer = CUTLASSTemplateBuffer( + layout=self.layout, + inputs=self.input_nodes, + make_kernel_render=self.make_kernel_render, + workspace_size=self.bmreq.workspace_size, + supports_epilogue_fusion=self.supports_epilogue_fusion, + template=self.template, + ) + # Pass KTC annotation to the buffer for encoding + if "ktc" in self.annotations: + buffer.annotations["ktc"] = self.annotations["ktc"] + return TensorBox.create(buffer) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/cutlass_mock_imports/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/cutlass_mock_imports/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/cutlass_mock_imports/cuda/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/cutlass_mock_imports/cuda/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e12a86af8ab0ab8d7d7b2d8bf37ec6dec861e0ff --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/cutlass_mock_imports/cuda/__init__.py @@ -0,0 +1,6 @@ +import torch + + +__version__ = torch.version.cuda + +from .cuda import * # noqa: F403 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/cutlass_mock_imports/cuda/cuda.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/cutlass_mock_imports/cuda/cuda.py new file mode 100644 index 0000000000000000000000000000000000000000..ad41f04fc897e33f4530eb42c76a104def58f413 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/cutlass_mock_imports/cuda/cuda.py @@ -0,0 +1,24 @@ +# mypy: disable-error-code="no-untyped-def" +# flake8: noqa +import torch + + +class CUdeviceptr: + pass + + +class CUstream: + def __init__(self, v): + pass + + +class CUresult: + CUDA_SUCCESS = True + + +class nvrtc: + pass + + +def cuDeviceGetCount(): + return (CUresult.CUDA_SUCCESS, torch.cuda.device_count()) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/cutlass_mock_imports/cuda/cudart.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/cutlass_mock_imports/cuda/cudart.py new file mode 100644 index 0000000000000000000000000000000000000000..ca2ee5f1f6163d7b20336d6102ce5d8f97880c87 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/cutlass_mock_imports/cuda/cudart.py @@ -0,0 +1,17 @@ +# mypy: disable-error-code="no-untyped-def" +import torch.cuda + + +class cudaError_t: + cudaSuccess = True + + +def cudaFree(n): + return (cudaError_t.cudaSuccess,) + + +def cudaGetDeviceProperties(d): + class DummyError: + value = False + + return (DummyError(), torch.cuda.get_device_properties(d)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/cutlass_mock_imports/pydot/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/cutlass_mock_imports/pydot/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8aefb6171b682f062cfe57a1876f51b280f120cc --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/cutlass_mock_imports/pydot/__init__.py @@ -0,0 +1,2 @@ +# mypy: disable-error-code="var-annotated" +Dot = None diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/cutlass_mock_imports/scipy/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/cutlass_mock_imports/scipy/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f0378d35a9c442559373f035e45de19b2be927cd --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/cutlass_mock_imports/scipy/__init__.py @@ -0,0 +1,3 @@ +# typing: ignore +# flake8: noqa +from .special import * diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/cutlass_mock_imports/scipy/special.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/cutlass_mock_imports/scipy/special.py new file mode 100644 index 0000000000000000000000000000000000000000..79af3029aa0b18d0ad55633f8cca8af8b76b520b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/cutlass_mock_imports/scipy/special.py @@ -0,0 +1,2 @@ +# mypy: disable-error-code="var-annotated" +erf = None diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/evt_extensions.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/evt_extensions.py new file mode 100644 index 0000000000000000000000000000000000000000..5357dc0bd98a52c4a3e399df0c1856b217166073 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/evt_extensions.py @@ -0,0 +1,289 @@ +from collections.abc import Callable +from typing import Any + +from sympy import Expr + +from torch._inductor.ir import ( + ComputedBuffer, + InputBuffer, + is_contiguous_strides_for_shape, +) +from torch.utils._ordered_set import OrderedSet + +from ..utils import torch_dtype_to_cutlass_type, try_import_cutlass + + +EpilogueFunctor = Any # EpilogueFunctor local class defined in _trace +Buffer = ComputedBuffer | InputBuffer +CutlassTupleType = Any # cutlass.backend.c_types.tuple_factory_..TupleType +CutlassVisitorType = Any # cutlass.backend.c_types.visitor_factory..VisitorType +CutlassArgType = ( + Any # Can be a CutlassTupleType, CutlassVisitorType, EmptyByte, or ctype.c_void_p +) + + +if try_import_cutlass(): + import ast + import ctypes + import textwrap + + from cutlass_cppgen.backend.c_types import ( # type: ignore[import-not-found] + EmptyByte, + ) + from cutlass_cppgen.backend.epilogue import ( # type: ignore[import-not-found] + dtype2ctype, + ) + from cutlass_cppgen.backend.evt import ( # type: ignore[import-not-found] + EpilogueFunctorVisitor, + ) + from cutlass_cppgen.backend.evt.backend.emitter_base import ( # type: ignore[import-not-found] + FusionCallbacks, + ) + from cutlass_cppgen.backend.evt.backend.sm100_emitter import ( # type: ignore[import-not-found] + Sm100CollectiveEpilogue, + ) + from cutlass_cppgen.backend.evt.backend.sm90_emitter import ( # type: ignore[import-not-found] + CollectiveEpilogue, + ) + from cutlass_cppgen.backend.evt.frontend import ( # type: ignore[import-not-found] + PythonASTFrontend, + ) + from cutlass_cppgen.backend.evt.ir.tensor import ( # type: ignore[import-not-found] + Tensor as CutlassTensor, + ) + from cutlass_library import ( + DataType, + EpilogueScheduleType, + LayoutType, + TileDescription, + ) + + from torch._inductor.codegen.cuda import cuda_env + from torch._inductor.utils import IndentedBuffer + + _CUTLASS_C_DTYPES = OrderedSet(dtype2ctype.values()) # type: ignore[var-annotated] + + class EVTArgRenames: + """Handles mapping buffer names to variable names in the cpp kernel signature and body""" + + def __init__(self) -> None: + self.buf_renames: dict[str, str] = {} + + def new_name(self, name: str) -> str: + if name in self.buf_renames: + return self.buf_renames[name] + else: + new_name = f"ptr_{len(self.buf_renames)}" + self.buf_renames[name] = new_name + return new_name + + def get(self, name: str) -> str: + return self.buf_renames.get(name, name) + + def create_example_tensors( + var_name_to_buffer_name: dict[str, str], + name_to_buffer: dict[str, Buffer], + size_hint_fn: Callable[[Expr | int], int], + ) -> dict[str, CutlassTensor]: + def cutlass_tensor_from_buffer( + buffer: Buffer, + ) -> CutlassTensor: + shape = buffer.get_layout().size + stride = buffer.get_layout().stride + shape = tuple(size_hint_fn(x) for x in shape) + stride = tuple(size_hint_fn(x) for x in stride) + + is_row_major = is_contiguous_strides_for_shape(stride, shape) + is_column_major = is_contiguous_strides_for_shape(stride[::-1], shape[::-1]) + + if not is_row_major and not is_column_major: + raise RuntimeError( + f"Cannot create example tensor for {buffer.get_name()} with \ +non-contiguous layout, received stride: {stride} and shape: {shape}" + ) + + return CutlassTensor( + shape=shape, + layout_tag=( + LayoutType.RowMajor if is_row_major else LayoutType.ColumnMajor + ), + element=torch_dtype_to_cutlass_type(buffer.get_layout().dtype), + ) + + return { + key: cutlass_tensor_from_buffer(name_to_buffer[name]) + for key, name in var_name_to_buffer_name.items() + } + + def trace( + fn_src: str, + example_tensors: dict[str, CutlassTensor], + accum_type: DataType, + output_type: DataType, + tile_description: TileDescription, + epilogue_schedule: EpilogueScheduleType, + name_to_buffer: dict[str, Buffer], + size_hint_fn: Callable[[Expr | int], int], + kernel_schedule: Any | None = None, + **kwargs: dict[str, Any], + ) -> tuple[str, str, str, EVTArgRenames]: + cuda_arch = int(cuda_env.get_cuda_arch()) # type: ignore[arg-type] + assert cuda_arch >= 90, "Only SM90+ is supported for EVT" + epilogue_functor = _trace(fn_src, example_tensors, cuda_arch, **kwargs) + visitor = EpilogueFunctorVisitor(cuda_arch, epilogue_functor) + fusion_callbacks = FusionCallbacks(visitor.graph, cuda_arch, emit_CD=False) + if cuda_arch < 100: + collective_epilogue = CollectiveEpilogue( + tile_description, + epilogue_schedule, + accum_type, + output_type, + fusion_callbacks, + ) + else: + collective_epilogue = Sm100CollectiveEpilogue( + tile_description=tile_description, + kernel_schedule=kernel_schedule, + epilogue_schedule=epilogue_schedule, + element_accumulator=accum_type, + element_d=output_type, + fusion_callbacks=fusion_callbacks, + ) + evt_name, evt_code = collective_epilogue.emit() + evt_args, arg_renames = _render_argument_type( + epilogue_functor, name_to_buffer, size_hint_fn + ) + return evt_name, evt_args, evt_code, arg_renames + + # Based off of + # https://github.com/NVIDIA/cutlass/blob/df18f5e4f5de76bed8be1de8e4c245f2f5ec3020/python/cutlass/epilogue/epilogue.py#L117 + # This is modified to enable directly passing the source code of the epilogue vs getting it from a bona-fide python function + # The reason for this is that inspect.getsource does not work with functions defined at runtime via exec/eval + def _trace( + fn_src: str, + example_tensors: dict[str, CutlassTensor], + cc: int, + **kwargs: Any, + ) -> EpilogueFunctor: + class EpilogueFunctor(PythonASTFrontend): + def __init__(self, cc: int, **kwargs: Any): + self.source = textwrap.dedent(fn_src) + super().__init__(cc, **kwargs) + + def parse( + self, + example_inputs: dict[str, CutlassTensor], + ) -> None: + self.example_inputs = example_inputs + self.ast = ast.parse(self.source) + # pyrefly: ignore [missing-attribute] + self.visit(self.ast) + + cc = int(cuda_env.get_cuda_arch()) + epilogue_functor = EpilogueFunctor(cc=cc, **kwargs) + epilogue_functor.trace(example_tensors) + return epilogue_functor + + def _render_argument_type( + epilogue_functor: EpilogueFunctor, + name_to_buffer: dict[str, Buffer], + size_hint_fn: Callable[[Expr | int], int], + ) -> tuple[str, EVTArgRenames]: + epilogue_thread_type = epilogue_functor.epilogue_thread_type + arg_renames = EVTArgRenames() + + # Fragile, but this is the only way to guarantee t is expected type because t is a local class + def is_nested_visitor_type(t: type) -> bool: + return ( + ".".join([t.__module__, t.__qualname__]) + == "cutlass_cppgen.backend.c_types.visitor_factory..VisitorType" + ) + + buffer = IndentedBuffer() + with buffer.set_tabwidth(2): + + def render_argument_type(name: str, t: CutlassArgType) -> None: + if issubclass(t, ctypes.c_byte): + buffer.writeline(f"{{}}, /* {name} */") + else: + fields = [ + ( + fname, + _get_arg_from_node( + ty, name_to_buffer[name], size_hint_fn, arg_renames + ), + ) + for fname, ty in t._fields_ + ] + field_strs = [ + f"/* {fname} */ {str(field)}" for fname, field in fields + ] + buffer.writeline(f"{{{', '.join(field_strs)}}}, /* {name} */") + + def render_thread_type(name: str, t: CutlassArgType) -> None: + if is_nested_visitor_type(t): + buffer.writeline(f"{{ /* {name} */") + with buffer.indent(): + for name, inner_t in t._fields_: + render_thread_type(name, inner_t) + buffer.writeline("},") + else: + render_argument_type(name, t) + + # unroll the recursion once to address special case formatting + # namely, no ending comma and no indentation for the outermost thread type + buffer.writeline("{ /* thread */") + with buffer.indent(3): + if is_nested_visitor_type(epilogue_thread_type): + with buffer.indent(): + for name, inner_t in epilogue_thread_type._fields_: + render_thread_type(name, inner_t) + else: + render_argument_type("thread", epilogue_thread_type) + buffer.writeline("}") + + return buffer.getvalue(), arg_renames + + def _get_arg_from_node( + arg_ty: type, + node: Buffer, + size_hint_fn: Callable[[Expr | int], int], + arg_renames: EVTArgRenames, + ) -> str: + from ..template import CUTLASSTemplate + + # Today, arguments are either a pointer to the + # node's memory, a stride tuple, the datatype + # Once again, need to check for local class type for stride tuple + if ( + str(arg_ty) + == ".TupleType'>" + ): + DEFAULT_STRIDE_LEN = 3 + assert len(node.get_layout().stride) <= DEFAULT_STRIDE_LEN + stride = [size_hint_fn(x) for x in node.get_layout().stride] + for _ in range(DEFAULT_STRIDE_LEN - len(stride)): + stride.append(0) + + def render_stride(x: int) -> str: + # Handle EBO for 0 and 1 + if x == 0: + return "_0{}" + elif x == 1: + return "_1{}" + else: + return str(x) + + return f"{{{', '.join([render_stride(x) for x in stride])}}}" + + elif issubclass(arg_ty, ctypes.c_void_p): + name = arg_renames.new_name(node.get_name()) + return f"({CUTLASSTemplate._DTYPE_TO_CUTLASS[node.get_layout().dtype]}*) ({name} + {name}_offset)" + elif ( + arg_ty in _CUTLASS_C_DTYPES + ): # Assumption: this is the element dtype, this holds for all cutlass ir nodes currently + return f"{CUTLASSTemplate._DTYPE_TO_CUTLASS[node.get_layout().dtype]}(0)" + elif issubclass(arg_ty, EmptyByte): + return "{}" + + raise NotImplementedError(f"Unsupported arg type: {arg_ty}") diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/gemm_operation_extensions.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/gemm_operation_extensions.py new file mode 100644 index 0000000000000000000000000000000000000000..f0f229136b2953e7fb13faf820615b30a539faa1 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/lib_extensions/gemm_operation_extensions.py @@ -0,0 +1,418 @@ +# mypy: ignore-errors +from ..utils import try_import_cutlass + + +# copied / modified from original at +# https://github.com/NVIDIA/cutlass/blob/8783c41851cd3582490e04e69e0cd756a8c1db7f/tools/library/scripts/gemm_operation.py#L658 + +if try_import_cutlass(): + import enum + + from cutlass_library.gemm_operation import * # noqa: F401, F403 + from cutlass_library.library import * # noqa: F401, F403 + + _LOGGER = logging.getLogger(__name__) + + class EmitGemmUniversal3xInstanceWithEVT: + """Responsible for emitting a CUTLASS 3.x template definition""" + + def __init__(self, operation_suffix="", evt_name=None, device_type="cuda"): + self.operation_suffix = operation_suffix + self.includes = [ + "cutlass/cutlass.h", + "cutlass/gemm/gemm.h", + "cutlass/numeric_types.h", + "cutlass/gemm/kernel/gemm_universal.hpp", + "cutlass/gemm/collective/collective_builder.hpp", + "cutlass/epilogue/collective/collective_builder.hpp", + ] + self.builtin_epilogue_functor_template = """${epilogue_functor}< + ${element_d}, + ${element_epilogue}, + ${element_c}, + ${element_epilogue} + >""" + self.evt_name = evt_name + self.device_type = device_type + self.gemm_template = """ +using ${operation_name}_epilogue = +typename cutlass::epilogue::collective::CollectiveBuilder< + ${arch}, ${opcode_class_epi}, + cute::Shape, + cute::Shape<${cluster_shape_m}, ${cluster_shape_n}, ${cluster_shape_k}>, + ${epi_tile_mn}, + ${element_accumulator}, ${element_epilogue}, + ${element_c}, ${layout_c}, ${align_c}, + ${element_d}, ${layout_d}, ${align_d}, + ${epilogue_schedule}, + ${epilogue_functor} +>::CollectiveOp; + +${mixed_dtype_prepare_code} + +using ${operation_name}_mainloop = +typename cutlass::gemm::collective::CollectiveBuilder< + ${arch}, ${opcode_class_main}, + ${element_a}, ${layout_a}, ${align_a}, + ${element_b}, ${layout_b}, ${align_b}, + ${element_accumulator}, + cute::Shape, + cute::Shape<${cluster_shape_m}, ${cluster_shape_n}, ${cluster_shape_k}>, + ${stages}, + ${kernel_schedule} +>::CollectiveOp; + +// Gemm operator ${operation_name} +using ${operation_name}_base = cutlass::gemm::kernel::GemmUniversal< + ${problem_shape}, + ${operation_name}_mainloop, + ${operation_name}_epilogue, + ${tile_scheduler}>; + +// Define named type +struct ${operation_name} : +public ${operation_name}_base { }; + + """ + + # + def instance_template(self): + return """ +${compile_guard_start} +{ + using GemmKernel = cutlass::gemm::device::GemmUniversalAdapter<${operation_name}>; + manifest.append( + new ${gemm_kind}("${operation_name}")); +} +${compile_guard_end} + """ + + def emit_block_scale_epilogue_functor(self, operation): + block_scaled_template = """ + ${epilogue_functor}< + ${epi_vs}, + ${element_d}, + ${element_accumulator}, + ${element_sfd}, + ${layout_sfd}, + ${element_c}, + ${element_scalar} + > + """ + block_scaled_values = { + "epi_vs": str(operation.ScaleFactorVectorSize), + "element_d": str(DataTypeTag[operation.D.element]), + "element_sfd": str(DataTypeTag[operation.ScaleFactorD.element]), + "layout_sfd": LayoutTag[operation.ScaleFactorD.layout], + "epilogue_functor": EpilogueFunctor3xTag[ + EpilogueFunctor3x.LinearCombinationBlockScaleFactor + ], + "element_accumulator": str(DataTypeTag[operation.accumulator_type()]), + "element_scalar": str(DataTypeTag[operation.accumulator_type()]), + "element_c": str(DataTypeTag[operation.C.element]), + } + return SubstituteTemplate(block_scaled_template, block_scaled_values) + + @staticmethod + def pointerize_if_grouped(operation, layout): + return layout if not is_grouped(operation.gemm_kind) else layout + "* " + + @staticmethod + def problem_shape(operation): + gemm_shape_type = "cute::Shape" + grouped_gemm_shape_type = "cute::Shape" + grouped_gemm_shape_type = ( + "cutlass::gemm::GroupProblemShape<" + grouped_gemm_shape_type + ">" + ) + + return ( + gemm_shape_type + if not is_grouped(operation.gemm_kind) + else grouped_gemm_shape_type + ) + + def emit(self, operation): + """Given a gem operation, emits a template definition of the operation""" + + opcode_class_main = operation.tile_description.math_instruction.opcode_class + opcode_class_epi = opcode_class_main + + tile_shape = operation.tile_description.tile_shape + instruction_shape = ( + operation.tile_description.math_instruction.instruction_shape + ) + cluster_m = operation.tile_description.cluster_shape[0] + cluster_n = operation.tile_description.cluster_shape[1] + + tile_shape_m, tile_shape_n, tile_shape_k = tile_shape + + # account for static/dynamic cluster shapes + cta_m = tile_shape[0] // cluster_m if cluster_m > 0 else tile_shape[0] + cta_n = tile_shape[1] // cluster_n if cluster_n > 0 else tile_shape[1] + + # Shape passed to epilogue builder + is_sm100_kernel = operation.arch == 100 + if is_sm100_kernel: + cta_m_per_mma_instruction = ( + 2 if "2sm" in operation.procedural_name() else 1 + ) + if cluster_m <= 0: + cta_m = cta_m // cta_m_per_mma_instruction + + if opcode_class_main in [ + OpcodeClass.TensorOp, + OpcodeClass.BlockScaledTensorOp, + ]: + tile_shape_m = instruction_shape[0] + tile_shape_n = instruction_shape[1] + + # stage count set to zero indicates builder automatic stage selection + if operation.tile_description.stages > 0: + stage_count_string = f"cutlass::gemm::collective::StageCount<\ +{str(operation.tile_description.stages)}>" + else: + stage_count_string = ( + f"cutlass::gemm::collective::StageCountAutoCarveout(\ +sizeof(typename {str(operation.procedural_name())}_epilogue::SharedStorage))>" + ) + if self.device_type == "xpu": + stage_count_string = "cutlass::gemm::collective::StageCountAuto" + + epi_tile_mn = "cutlass::epilogue::collective::EpilogueTileAuto" + + ( + instance_layout_A, + instance_layout_B, + instance_layout_C, + instance_layout_D, + ) = ( + operation.A.layout, + operation.B.layout, + operation.C.layout, + operation.D.layout, + ) + + # 3.0 profiler integration only supports trivial epilogues for now + epilogue_vector_length = 1 + + # Support built-in epilogue functors or user-defined functions + if isinstance(operation.epilogue_functor, enum.Enum): + values = { + "element_epilogue": str(DataTypeTag[operation.element_epilogue]), + "epilogue_functor": EpilogueFunctor3xTag[ + operation.epilogue_functor + ], + } + epilogue_functor = SubstituteTemplate( + self.builtin_epilogue_functor_template, values + ) + + if ( + is_block_scaled(operation.gemm_kind) + and operation.ScaleFactorD.element != DataType.void + ): + epilogue_functor = self.emit_block_scale_epilogue_functor(operation) + else: + epilogue_functor = self.epilogue_functor.emit_declaration() + + if ( + is_block_scaled(operation.gemm_kind) + and operation.ScaleFactorD.element != DataType.void + ): + epilogue_functor = self.emit_block_scale_epilogue_functor(operation) + + # + # Cutlass3x complex kernels' ElementA(B) is a tuple in collective mainloop builder, + # e.g. cute::tuple, Transform : cute::identity / cute::conjugate. + element_a = ( + DataTypeTag[operation.A.element] + if not operation.is_complex() + else f"cute::tuple<{str(DataTypeTag[operation.A.element])},\ +{str(ComplexTransformTag3x[operation.A.complex_transform])}>" + ) + element_b = ( + DataTypeTag[operation.B.element] + if not operation.is_complex() + else f"cute::tuple<{str(DataTypeTag[operation.B.element])},\ +{str(ComplexTransformTag3x[operation.B.complex_transform])}>" + ) + epilogue_schedule_type = EpilogueScheduleTag[operation.epilogue_schedule] + + if opcode_class_main == OpcodeClass.BlockScaledTensorOp: + is_no_smem_epilogue = operation.epilogue_schedule in [ + EpilogueScheduleType.NoSmemWarpSpecialized1Sm, + EpilogueScheduleType.NoSmemWarpSpecialized2Sm, + ] + grouped = is_grouped(operation.gemm_kind) + if cta_n == 256 and operation.kernel_schedule == to_grouped_schedule( + KernelScheduleType.Nvf4TmaWarpSpecialized1SmSm100, grouped + ): + epi_tile_mn = "cute::Shape" + if not is_no_smem_epilogue: + epilogue_schedule_type = EpilogueScheduleTag[ + to_grouped_schedule( + EpilogueScheduleType.TmaWarpSpecialized1Sm, grouped + ) + ] + if cta_n == 256 and operation.kernel_schedule == to_grouped_schedule( + KernelScheduleType.Nvf4TmaWarpSpecialized2SmSm100, grouped + ): + epi_tile_mn = "cute::Shape" + if not is_no_smem_epilogue: + epilogue_schedule_type = EpilogueScheduleTag[ + to_grouped_schedule( + EpilogueScheduleType.TmaWarpSpecialized2Sm, grouped + ) + ] + element_a = f"cute::tuple<{str(element_a)},{str(DataTypeTag[operation.ScaleFactorA])}>" + element_b = f"cute::tuple<{str(element_b)},{str(DataTypeTag[operation.ScaleFactorB])}>" + + operation_name_str = operation.procedural_name() + layout_a_str = LayoutTag[instance_layout_A] + layout_b_str = LayoutTag[instance_layout_B] + mixed_dtype_prepare_code = "" + if operation.mixed_input_mode is not None: + A_dtype = operation.A.element + B_dtype = operation.B.element + A_dtype_bits = DataTypeSize[A_dtype] + B_dtype_bits = DataTypeSize[B_dtype] + is_A_dtype_narrow = A_dtype_bits < B_dtype_bits + if is_A_dtype_narrow: + narrow_dtype, wide_dtype = (A_dtype, B_dtype) + narrow_dtype_bits, wide_dtype_bits = (A_dtype_bits, B_dtype_bits) + else: + narrow_dtype, wide_dtype = (B_dtype, A_dtype) + narrow_dtype_bits, wide_dtype_bits = (B_dtype_bits, A_dtype_bits) + + narrow_tag = DataTypeTag[narrow_dtype] + wide_tag = DataTypeTag[wide_dtype] + scale_tag = DataTypeTag[wide_dtype] + zero_tag = DataTypeTag[wide_dtype] + + do_shuffle = False + value_shuffle_str = "" + if narrow_dtype_bits == 4 and wide_dtype_bits == 16: + value_shuffle_str = "cute::Layout, \ +cute::Stride>" + do_shuffle = True + if narrow_dtype_bits == 8 and wide_dtype_bits == 16: + value_shuffle_str = "cute::Layout, \ +cute::Stride>" + do_shuffle = True + do_shuffle = operation.mixed_input_shuffle and do_shuffle + + if do_shuffle: + if is_A_dtype_narrow: + stride_narrow_str = ( + f"cutlass::detail::TagToStrideA_t<{layout_a_str}>" + ) + layout_a_str = f"{operation_name_str}_LayoutNarrowReordered" + else: + stride_narrow_str = ( + f"cutlass::detail::TagToStrideB_t<{layout_b_str}>" + ) + layout_b_str = f"{operation_name_str}_LayoutNarrowReordered" + # The {operation_name_str}_ prefixs in mixed_dtype_prepare_code and + # layout_{a, b}_str are to prevent errors in Windows platform unity build + mixed_dtype_prepare_code = f""" + using {operation_name_str}_StrideNarrow = {stride_narrow_str}; + using {operation_name_str}_ValueShuffle = {value_shuffle_str}; + static constexpr int {operation_name_str}_NumShuffleAtoms = 1; + using {operation_name_str}_MmaAtomShape = \ +cute::Layout>>; + using {operation_name_str}_LayoutAtomQuant = \ +decltype(cutlass::compute_memory_reordering_atom<{wide_tag}, {operation_name_str}_MmaAtomShape, \ +{operation_name_str}_ValueShuffle>()); + using {operation_name_str}_LayoutNarrowReordered = \ +decltype(cute::tile_to_shape({operation_name_str}_LayoutAtomQuant{{}}, \ +cute::Layout, {operation_name_str}_StrideNarrow>{{}})); + """ + + mixed_input_modes_to_element = { + MixedInputMode.ConvertOnly: narrow_tag, + MixedInputMode.ScaleOnly: f"cute::tuple<{narrow_tag}, {scale_tag}>", + MixedInputMode.ScaleWithZeroPoint: f"cute::tuple<{narrow_tag}, {scale_tag}, {zero_tag}>", + } + narrow_element = mixed_input_modes_to_element.get( + operation.mixed_input_mode, narrow_tag + ) + + if narrow_dtype == DataType.s4 and ( + wide_dtype == DataType.e4m3 or wide_dtype == DataType.e5m2 + ): + narrow_element = ( + f"cute::tuple<{narrow_tag}, cutlass::Array<{scale_tag}, 8>>" + ) + + if is_A_dtype_narrow: + element_a = narrow_element + else: + element_b = narrow_element + + if self.evt_name: + epilogue_functor = self.evt_name + + if self.device_type == "xpu": + arch = f"cutlass::arch::Xe{operation.arch}" + else: + arch = f"cutlass::arch::Sm{operation.arch}" + + values = { + "operation_name": operation_name_str, + "operation_suffix": self.operation_suffix, + "problem_shape": self.problem_shape(operation), + "element_a": element_a, + "layout_a": self.pointerize_if_grouped(operation, layout_a_str), + "element_b": element_b, + "layout_b": self.pointerize_if_grouped(operation, layout_b_str), + "element_c": DataTypeTag[operation.C.element], + "layout_c": self.pointerize_if_grouped( + operation, LayoutTag[instance_layout_C] + ), + "element_d": DataTypeTag[operation.D.element], + "layout_d": self.pointerize_if_grouped( + operation, LayoutTag[instance_layout_D] + ), + "element_accumulator": DataTypeTag[operation.accumulator_type()], + "opcode_class_main": OpcodeClassTag[opcode_class_main], + "opcode_class_epi": OpcodeClassTag[opcode_class_epi], + "arch": arch, + "tile_shape_m": str(tile_shape_m), + "tile_shape_n": str(tile_shape_n), + "tile_shape_k": str(tile_shape_k), + "cluster_shape_m": "cute::_" + + str(operation.tile_description.cluster_shape[0]) + if operation.tile_description.cluster_shape[0] > 0 + else "int", + "cluster_shape_n": "cute::_" + + str(operation.tile_description.cluster_shape[1]) + if operation.tile_description.cluster_shape[1] > 0 + else "int", + "cluster_shape_k": "cute::_" + + str(operation.tile_description.cluster_shape[2]) + if operation.tile_description.cluster_shape[2] > 0 + else "int", + "instruction_shape_m": str(instruction_shape[0]), + "instruction_shape_n": str(instruction_shape[1]), + "instruction_shape_k": str(instruction_shape[2]), + "kernel_schedule": str(KernelScheduleTag[operation.kernel_schedule]), + "epilogue_schedule": str(epilogue_schedule_type), + "epi_tile_mn": epi_tile_mn, + "epilogue_functor": epilogue_functor, + "stages": stage_count_string, + "align_a": str(operation.A.alignment), + "align_b": str(operation.B.alignment), + "align_c": str(operation.C.alignment), + "align_d": str(operation.D.alignment), + "transform_a": ComplexTransformTag[operation.A.complex_transform], + "transform_b": ComplexTransformTag[operation.B.complex_transform], + "math_operation": MathOperationTag[ + operation.tile_description.math_instruction.math_operation + ], + "epilogue_vector_length": str(epilogue_vector_length), + "element_epilogue": str(DataTypeTag[operation.element_epilogue]), + "tile_scheduler": str(TileSchedulerTag[operation.tile_scheduler]), + "mixed_dtype_prepare_code": mixed_dtype_prepare_code, + } + + return SubstituteTemplate(self.gemm_template, values) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/python_evt.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/python_evt.py new file mode 100644 index 0000000000000000000000000000000000000000..26e4a9b9cc062f18337da522dd948b5049a756ad --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/python_evt.py @@ -0,0 +1,327 @@ +import itertools +from collections.abc import Generator, Iterable, Iterator, Sequence +from contextlib import contextmanager +from os import linesep +from typing import Any + +import sympy + +import torch +import torch._inductor.virtualized as virtualized +from torch._inductor.ir import ComputedBuffer, Pointwise +from torch._inductor.ops_handler import DefaultHandler, WrapperHandler +from torch._inductor.scheduler import BaseSchedulerNode +from torch._inductor.utils import DelayReplaceLine, IndentedBuffer, OrderedSet +from torch._inductor.virtualized import OpsValue + +from ...virtualized import V + + +_ACCUMULATOR_ARG_NAME = "accum" + + +def scaled_mm_evt( + scale_A_name: str, scale_B_name: str, bias_name: str | None, output_name: str +) -> tuple[list[str], dict[str, Any], str]: + evt_read_names = [scale_A_name, scale_B_name] + var_name_to_buffer_name = {n: n for n in [scale_A_name, scale_B_name]} + var_name_to_buffer_name["D"] = output_name + var_name_to_buffer_name[_ACCUMULATOR_ARG_NAME] = output_name + expr = f"accum * {scale_A_name} * {scale_B_name}{linesep}" + if bias_name: + expr = f"({expr}) + {bias_name}" + evt_read_names.append(bias_name) + var_name_to_buffer_name[bias_name] = bias_name + + evt_py_code = f"def fn(accum, {','.join(evt_read_names)}):{linesep}\ + D = {expr}{linesep}\ + return D{linesep}" + + return evt_read_names, var_name_to_buffer_name, evt_py_code + + +class CutlassEVTOpsMixIn: + @staticmethod + def _infix_bin_op(op: str, a: str, b: str) -> str: + return f"{a} {op} {b}" + + @staticmethod + def _prefix_bin_op(op: str, a: str, b: str) -> str: + return f"{op}({a}, {b})" + + @staticmethod + def _prefix_un_op(op: str, a: str) -> str: + return f"{op}({a})" + + @staticmethod + def to_dtype( + x: str, + dtype: Any, + src_dtype: torch.dtype | None = None, + use_compute_types: bool = False, + ) -> str: + return x + + @staticmethod + def constant(value: Any, dtype: Any) -> str: + raise NotImplementedError + + @staticmethod + def mul(x0: str, x1: str) -> str: + return CutlassEVTOpsMixIn._infix_bin_op("*", x0, x1) + + @staticmethod + def truediv(x0: str, x1: str) -> str: + return CutlassEVTOpsMixIn._infix_bin_op("/", x0, x1) + + @staticmethod + def ge(x0: str, x1: str) -> str: + raise NotImplementedError + + @staticmethod + def add(x0: str, x1: str) -> str: + return CutlassEVTOpsMixIn._infix_bin_op("+", x0, x1) + + @staticmethod + def relu(x0: str) -> str: + return CutlassEVTOpsMixIn._prefix_un_op("relu", x0) + + @staticmethod + def sigmoid(x0: str) -> str: + return CutlassEVTOpsMixIn._prefix_un_op("sigmoid", x0) + + @staticmethod + def sub(x0: str, x1: str) -> str: + return CutlassEVTOpsMixIn._infix_bin_op("-", x0, x1) + + @staticmethod + def tanh(x0: str) -> str: + return CutlassEVTOpsMixIn._prefix_un_op("tanh", x0) + + @staticmethod + def exp(x0: str) -> str: + return CutlassEVTOpsMixIn._prefix_un_op("exp", x0) + + +class MockCutlassHandler(CutlassEVTOpsMixIn, WrapperHandler): + """Passthrough handler for cutlass ops, used for running epilogue nodes for memory planning""" + + +class _AssignmentFormatter(DefaultHandler): + def __init__(self, parent_handler: "CutlassEVTCodegen"): + self.parent_handler = parent_handler + + def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: + # Handle op dispatch here + if hasattr(self.parent_handler, name): + fn = getattr(self.parent_handler, name) + line = fn(*args, **kwargs) + if name in ("load", "store"): + return OpsValue(line) + else: + var = self.parent_handler._tmp_var() + line = DelayReplaceLine( + var, + lambda: "D" + if var == self.parent_handler.last_stored_var_name + else var, + f"{var} = {line}", + ) + self.parent_handler.body.writeline(line) + return OpsValue(var) + else: + raise NotImplementedError(name) + + +class CutlassEVTCodegen(CutlassEVTOpsMixIn): + """ + Notes: + * Used by CUTLASSGemmTemplate. + * This class should not be instantiated by users, it is intended to be used + by calling CutlassEVTCodegen.ir_to_evt_python_code(...) + which instantiates this class as an ops handler for virtualized.V.ops.[op-name] + * Extend this with more _op_ nodes to add support for new pointwise operations. + """ + + def __init__(self, accumulator_node_name: str, removed_buffers: OrderedSet[str]): + """ + + Initializes a CutlassEVTEpilogueArgumentFormatter object. Do not instantiate directly. + Use the CutlassEVTCodegen.ir_to_evt_python_code static method. + + Args: + accumulator_node_name: The name of the accumulator node which should contain + the Matmul result before fusion according to the IR graph. + epilogue_nodes: The list of scheduler nodes to be fused into the epilogue + """ + self.accumulator_node_name: str = accumulator_node_name # + self.body: IndentedBuffer = IndentedBuffer(1) # The body buffer for codegen + self.var_counter: Iterator[int] = itertools.count() + self.store_name_to_value: dict[str, OpsValue] = ( + dict() + ) # Aliases for subexpression functors + self.reads: OrderedSet[str] = OrderedSet([]) + # Used for creating example tensors + self.var_name_to_buffer_name: dict[str, str] = { + _ACCUMULATOR_ARG_NAME: accumulator_node_name + } + self.removed_buffers: OrderedSet[str] = removed_buffers + self.cur_node: ComputedBuffer | None = None + self.name_to_buffer = V.graph.name_to_buffer | V.graph.graph_inputs + for name in V.graph.constants: + # pyrefly: ignore [unsupported-operation] + self.name_to_buffer[name] = V.graph.add_tensor_constant( + V.graph.constants[name], name + ) + self.is_D_assigned = False + self.D_var_name = None + + if accumulator_node_name not in removed_buffers: + # cannot return accumulator directly, so alias it + var = self._tmp_var() + self.body.writeline(f"{var} = {_ACCUMULATOR_ARG_NAME}") + self.store(accumulator_node_name, value=OpsValue(var)) + + @staticmethod + def ir_to_evt_python_code( + cutlass_template_node_name: str, + epilogue_nodes: list[BaseSchedulerNode], + removed_buffers: OrderedSet[str], + ) -> tuple[list[str], list[str], dict[str, Any], str]: + codegen = CutlassEVTCodegen(cutlass_template_node_name, removed_buffers) + handler = _AssignmentFormatter(codegen) + + with virtualized.V.set_ops_handler(handler): + for s_node in epilogue_nodes: + node = s_node.node + assert isinstance(node, ComputedBuffer) + with codegen.set_cur_node(node): + index_vars = CutlassEVTCodegen.get_index_vars(node) + node.get_store_function()(index_vars) + + codegen.finalize() + + return ( + codegen.get_reads(), + codegen.get_writes(), + codegen.get_renames(), + codegen.get_value(), + ) + + def get_value(self) -> str: + return linesep.join( + [ + self._render_input_signature(), + self.body.getvalue(), + self._render_return_statement(), + ] + ) + + def finalize(self) -> None: + # Rename the last store to D + # no other code references this store + # to workaround https://github.com/NVIDIA/cutlass/issues/2288 + # Note: the delayed line will automatically rewrite the last assignment to + # be to D + buffer_name = self.var_name_to_buffer_name[self.last_stored_var_name] + self.var_name_to_buffer_name.pop(self.last_stored_var_name) + self.var_name_to_buffer_name["D"] = buffer_name + self.store_name_to_value[buffer_name] = OpsValue("D") + + @contextmanager + def set_cur_node(self, node: ComputedBuffer) -> Generator[None, Any, Any]: + prev_node = self.cur_node + try: + self.cur_node = node + yield + finally: + self.cur_node = prev_node + + def get_renames(self) -> dict[str, str]: + return dict(self.var_name_to_buffer_name) + + def get_reads(self) -> list[str]: + return list(self.reads.difference(self.store_name_to_value.keys())) + + def get_writes(self) -> list[str]: + return list(self.store_name_to_value.keys()) + + def load(self, name: str, index: Any) -> str: + self._check_indexing(name, index) + if name in self.store_name_to_value: + return self.store_name_to_value[name].value + elif name == self.accumulator_node_name: + return _ACCUMULATOR_ARG_NAME + else: + self.reads.add(name) + self.var_name_to_buffer_name[name] = name + return name + + def store( + self, name: Any, index: Any = None, value: Any = None, mode: Any = None + ) -> None: + if name not in self.removed_buffers: + if index: + self._check_indexing(name, index) + assert value.value != _ACCUMULATOR_ARG_NAME, ( + "Cannot store accumulator arg name" + ) + self.var_name_to_buffer_name[value.value] = name + self.store_name_to_value[name] = value + self.last_stored_var_name = value.value + return None + + def _get_cur_node(self) -> ComputedBuffer: + assert self.cur_node + return self.cur_node + + @staticmethod + def get_index_vars(node: ComputedBuffer) -> Sequence[sympy.Expr]: + data = node.data + # TODO mlazos: relax this, cutlass supports reductions and other ops + assert isinstance(data, Pointwise) + return data._index(data.ranges) + + def _get_current_index_vars(self) -> Sequence[sympy.Expr]: + return self.get_index_vars(self._get_cur_node()) + + def _check_indexing(self, name: str, index: sympy.Expr) -> None: + # We only support indexing that matches the layout today because + # CUTLASS doesn't support arbitrary indexing + buffer_name = ( + self.accumulator_node_name if name == _ACCUMULATOR_ARG_NAME else name + ) + buffer = self.name_to_buffer[buffer_name] + index_strides = V.graph.sizevars.stride_vars( + index, self._get_current_index_vars() + ) + stride = buffer.get_layout().stride + if not self._stride_compatible(stride, index_strides): + raise NotImplementedError( + f"Unsupported indexing for {name} with index {index}, index strides {index_strides}, and layout stride {stride}" + ) + + def _stride_compatible( + self, left: Iterable[sympy.Expr], right: Iterable[sympy.Expr] + ) -> bool: + return all( + sympy.Eq(l, r) or sympy.Eq(l, 0) or sympy.Eq(r, 0) + for l, r in (zip(left, right)) + ) + + def _render_input_signature(self) -> str: + arguments = ", ".join( + [_ACCUMULATOR_ARG_NAME] + + [name for name in self.reads if name != self.accumulator_node_name] + ) + return f"def fn({arguments}):" + + def _render_return_statement(self) -> str: + return_vars = OrderedSet( + op_v.value for op_v in self.store_name_to_value.values() + ) + assert "D" in return_vars + return f"return {', '.join(return_vars)}" + + def _tmp_var(self) -> str: + return f"tmp_{next(self.var_counter)}" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/scheduling.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/scheduling.py new file mode 100644 index 0000000000000000000000000000000000000000..933a6ed33593bb92a3baaf3c1caabdff080881c6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/scheduling.py @@ -0,0 +1,295 @@ +# mypy: allow-untyped-defs +import hashlib +import logging +from collections.abc import Sequence +from typing import cast, TypeGuard + +from torch._inductor.codegen.cutlass.python_evt import ( + CutlassEVTCodegen, + MockCutlassHandler, +) +from torch._inductor.utils import Placeholder +from torch.utils._ordered_set import OrderedSet + +from ...._dynamo.utils import counters +from ... import config +from ...codecache import code_hash, get_path +from ...ir import Buffer, ComputedBuffer, CUTLASSTemplateBuffer, Pointwise +from ...scheduler import ( + BaseSchedulerNode, + BaseScheduling, + FusedSchedulerNode, + SchedulerNode, + WhyNoFuse, +) +from ...utils import get_fused_kernel_name, get_kernel_metadata, sympy_product +from ...virtualized import V +from ..common import BackendFeature, IndentedBuffer + + +log = logging.getLogger(__name__) + + +class WhyNoFuseNames(WhyNoFuse): + def __init__(self, name1: str, name2: str) -> None: + self.name1 = name1 + self.name2 = name2 + + +class CUTLASSScheduling(BaseScheduling): + """ + Partial Scheduling implementation for cutlass C++ Kernels. + This class is intended to be used in combination with TritonScheduling, + and delegated to by CUDACombinedScheduling/XPUCombinedScheduling. + + It handles fusion decisions and cutlass C++ specific template code generation. + """ + + @classmethod + def get_backend_features(cls, device) -> OrderedSet[BackendFeature]: + return OrderedSet() + + def group_fn(self, sizes): + return tuple(V.graph.sizevars.simplify(sympy_product(s)) for s in sizes) + + @staticmethod + def is_cutlass_template(node: BaseSchedulerNode) -> TypeGuard[SchedulerNode]: + return isinstance(node, SchedulerNode) and isinstance( + node.node, CUTLASSTemplateBuffer + ) + + def is_cutlass_fused_template(self, node: BaseSchedulerNode) -> bool: + return isinstance(node, FusedSchedulerNode) and self.is_cutlass_template(node) + + def can_fuse_vertical( + self, node1: BaseSchedulerNode, node2: BaseSchedulerNode + ) -> bool: + if self.is_cutlass_template(node1) and isinstance(node2, BaseSchedulerNode): + assert node1.node, "node1.node should not be None" + return self._can_fuse_epilogue_impl( + cast(CUTLASSTemplateBuffer, node1.node), + [], + node2, # type: ignore[arg-type] + ) + elif self.is_cutlass_fused_template(node1) and isinstance( + node2, BaseSchedulerNode + ): + assert node1.node, "node1.node should not be None" + assert node2.node, "node2.node should not be None" + fnode1 = cast(FusedSchedulerNode, node1) + return self._can_fuse_epilogue_impl( + fnode1.get_template_node(), # type: ignore[arg-type] + self._unwrap_epilogue_nodes(fnode1), + node2, # type: ignore[arg-type] + ) + + return False + + def define_kernel(self, src_code: str, node_schedule) -> str: + wrapper = V.graph.wrapper_code + if src_code in wrapper.src_to_kernel: + kernel_name = wrapper.src_to_kernel[src_code] + else: + fused_name = ( + get_fused_kernel_name(node_schedule, config.triton.descriptive_names) + if config.triton.descriptive_names + else "" + ) + + # use the original src_code as the key + kernel_hash = hashlib.sha256(src_code.encode("utf-8")).hexdigest()[:8] + if fused_name == "fused": + # no EVT kernel, use the original kernel name + kernel_name = f"cutlass_{kernel_hash}" + else: + kernel_name = f"cutlass_{fused_name}_{kernel_hash}" + wrapper.src_to_kernel[src_code] = kernel_name + src_code = src_code.replace(str(Placeholder.KERNEL_NAME), kernel_name) + + _, _, kernel_path = get_path(code_hash(src_code), "py") + + compile_wrapper = IndentedBuffer() + compile_wrapper.writeline(f"async_compile.{V.graph.device_type}(r'''") + compile_wrapper.splice(src_code, strip=True) + compile_wrapper.writeline( + f"''', 'so', aot_compile={str(V.graph.aot_mode)})" + ) + + metadata_comment = f"# kernel path: {kernel_path}" + origins, detailed_origins = get_kernel_metadata(node_schedule, wrapper) + metadata_comment += "\n" + origins + "\n" + detailed_origins + wrapper.define_kernel( + kernel_name, compile_wrapper.getvalue(), metadata_comment + ) + return kernel_name + + def codegen_template( + self, + template_node: BaseSchedulerNode, + epilogue_nodes: Sequence[BaseSchedulerNode], + prologue_nodes: Sequence[BaseSchedulerNode], + ): + """ + Codegen a cutlass template, possibly with fused epilogues + """ + counters["inductor"]["cutlass_epilogue_fusion_counter"] += len(epilogue_nodes) + assert self.is_cutlass_template(template_node), ( + "Template node passed to CUTLASSScheduling.codegen_template must be a SchedulerNode that wraps a CUTLASSTemplateBuffer" + ) + _, (_numel, rnumel) = template_node.group + assert rnumel == 1 + ctb: CUTLASSTemplateBuffer = cast(CUTLASSTemplateBuffer, template_node.node) + epilogue_ir_nodes: list[Buffer] = [n.node for n in epilogue_nodes] # type: ignore[misc] + assert all(isinstance(n, ComputedBuffer) for n in epilogue_ir_nodes), ( + "Epilogue nodes must all be instances of ir.ComputedBuffer" + ) + kernel, render = ctb.make_kernel_render( # type: ignore[misc] + ctb, epilogue_nodes=epilogue_nodes + ) + with kernel: + for node in [template_node, *epilogue_nodes]: + node.mark_run() + + # typically there is a codegen pass which runs after mark_run + # for this kernel we've already generated the C++ code, but we still + # need to let the kernel know about loads/stores that occur in the fused + # kernel for memory planning to properly optimize allocations + ctb.emulate_store_fn() + for node in epilogue_ir_nodes: + with V.set_ops_handler(MockCutlassHandler(V.get_ops_handler())): + assert isinstance( + node, ComputedBuffer + ) # Not sure why we need to do this again + node.get_store_function()(CutlassEVTCodegen.get_index_vars(node)) + + with V.set_kernel_handler(kernel): + src_code = render() + node_schedule = [template_node, *epilogue_nodes] + kernel_name = self.define_kernel(src_code, node_schedule) + + # debug printing values of intermediate tensors + _, call_args, arg_signatures, _ = kernel.args.python_argdefs() + debug_printer_manager = V.graph.wrapper_code.debug_printer + debug_printer_manager.set_printer_args( + call_args, kernel_name, arg_signatures, kernel + ) + with debug_printer_manager: + self.codegen_comment(node_schedule, kernel_name) + kernel.call_kernel(kernel_name, ctb) + + V.graph.removed_buffers |= kernel.removed_buffers + self.free_buffers_in_scheduler() + + @staticmethod + def _unwrap_epilogue_nodes( + fused_node: FusedSchedulerNode, + ) -> list[BaseSchedulerNode]: + nodes = fused_node.get_nodes() + template_node = fused_node.get_template_node() + assert all(n.node is not None for n in nodes), ( + "All epilogue nodes should have an IRNode" + ) + # pyrefly: ignore [redundant-cast] + return cast( + list[BaseSchedulerNode], [n for n in nodes if n.node is not template_node] + ) + + def _can_fuse_epilogue_impl( + self, + cutlass_template_buffer: CUTLASSTemplateBuffer, + existing_epilogue_nodes: list[BaseSchedulerNode], + node_to_fuse: BaseSchedulerNode, + ) -> bool: + """ + Check if the given node can be fused with the epilogue. At the moment, Kernels + support fusion with Pointwise operations, wrapped in (named) ComputedBuffer nodes. + + Args: + cutlass_template_buffer : A CUTLASSTemplateBuffer object representing the CUTLASS template and it's result buffer + existing_epilogue_nodes : List[SchedulerNode]: The list of already fused epilogue nodes. + node_to_fuse: The SchedulerNode node to be checked if it can be fused with the epilogue. + Returns: + - bool: True if the given node can be fused with the epilogue, False otherwise. + + """ + why = WhyNoFuseNames( + cutlass_template_buffer.get_name(), node_to_fuse.get_name() + ) + + scheduler_nodes_to_fuse = node_to_fuse.get_nodes() + + assert isinstance(cutlass_template_buffer, CUTLASSTemplateBuffer) + + # Checks on constituent nodes + for s_node in scheduler_nodes_to_fuse: + node = s_node.node + + if not isinstance(node, ComputedBuffer): + why(f"{node} is not a ComputedBuffer") + return False + elif not isinstance(node.data, Pointwise): + why(f"{node} is not a Pointwise op") + return False + elif not node.get_computed_buffer_name(): # type: ignore[attr-defined] + why(f"{node} does not have a computed buffer name") + return False + + name = node.get_computed_buffer_name() # type: ignore[attr-defined] + # dtype can differ, and strides can differ as long as they are broadcastable + if node.get_size() != cutlass_template_buffer.get_size(): + why( + f"{name}'s size: {node.get_size()} differs from {cutlass_template_buffer.get_name()}'s \ +size: {cutlass_template_buffer.get_size()}" + ) + return False + + assert len( + existing_epilogue_nodes + ) or cutlass_template_buffer.get_name() in OrderedSet( + [rd.name for rd in node_to_fuse.read_writes.reads] + ), "First epilogue node must read from cutlass template buffer" + + if node_to_fuse.has_aliasing_or_mutation(): + why(f"{node_to_fuse.get_name()} has aliasing or mutation") + return False + elif node_to_fuse.is_reduction(): + why( + f"{node_to_fuse.get_name()} is a reduction which is not yet supported by EVT" + ) + return False + elif ( + not config.cutlass.cutlass_epilogue_fusion_enabled + or not config.epilogue_fusion + ): + why("cutlass epilogue fusion is not enabled") + return False + elif not cutlass_template_buffer.supports_epilogue_fusion: + why("epilogue fusion is only supported for TMA-enabled gemm ops") + return False + + try: + from torch._inductor.codegen.cutlass.python_evt import CutlassEVTCodegen + + CutlassEVTCodegen.ir_to_evt_python_code( + cutlass_template_buffer.get_name(), + existing_epilogue_nodes + list(node_to_fuse.get_nodes()), + OrderedSet(), + ) + + except NotImplementedError as e: + not_implemented_op = str(e) + if not_implemented_op.startswith("_op_"): + not_implemented_op = not_implemented_op[4:] + why( + f"Cannot fuse epilogue node {node_to_fuse} into {cutlass_template_buffer.name}, \ +likely due to unsupported operation: {not_implemented_op}" # noqa: G004, B950 + ) + return False + else: # Likely due to unsupported dtype. + why( + f"Cannot fuse epilogue node {node_to_fuse} into {cutlass_template_buffer.name}. \ +Reason: {not_implemented_op}" # noqa: G004, B950 + ) + return False + + return True diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/serialization.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/serialization.py new file mode 100644 index 0000000000000000000000000000000000000000..c1649b1b2fffa74aa78f6d0f516d4c91dadf9309 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/serialization.py @@ -0,0 +1,507 @@ +# mypy: allow-untyped-defs +import functools +import json +from enum import Enum +from typing import Any, Optional + +from torch._inductor.codegen.cutlass.utils import try_import_cutlass + + +class CUTLASSOperationSerializer: + """Serializes and deserializes CUTLASS GEMM operations to/from JSON. + + Handles GemmOperation objects and their nested components (TileDescription, TensorDescription). + """ + + # not used, but keeping in case we want to generalize the serializer + _SUPPORTED_CLASSES: list[str] = [ + "GemmOperation", + "GemmKind", + "TileDescription", + "TensorDescription", + "DataType", + "EpilogueFunctor", + "EpilogueFunctor3x", + "SwizzlingFunctor", + "KernelScheduleType", + "EpilogueScheduleType", + "TileSchedulerType", + ] + + @classmethod + def serialize(cls, operation: "GemmOperation") -> str: # type: ignore[name-defined] # noqa: F821 + """Serialize a GEMM operation to JSON string. + + Args: + operation: GemmOperation object + + Returns: + str: JSON string representation of the operation + """ + assert operation.__class__.__qualname__ == "GemmOperation", ( + "Only GemmOperation objects are supported via the main API" + ) + return json.dumps(cls._gemm_operation_to_json(operation)) + + @classmethod + def deserialize(cls, json_str: str) -> "GemmOperation": # type: ignore[name-defined] # noqa: F821 + """Deserialize JSON string to a GEMM operation. + + Args: + json_str: JSON string of a GEMM operation + + Returns: + GemmOperation: Reconstructed operation + """ + json_dict = json.loads(json_str) + return cls._json_to_gemm_operation(json_dict) + + @classmethod + def _gemm_operation_to_json(cls, operation: "GemmOperation") -> dict[str, Any]: # type: ignore[name-defined] # noqa: F821 + """Convert GemmOperation to JSON-serializable dict. + + Args: + operation: GemmOperation object + + Returns: + dict: Dictionary representation + """ + from cutlass_library.library import TensorDescription + + # Create the main dictionary with required and optional parameters + result = { + # Required parameters + "gemm_kind": cls._enum_to_json(operation.gemm_kind), + "arch": operation.arch, + "tile_description": cls._tile_description_to_json( + operation.tile_description + ), + "A": cls._tensor_description_to_json(operation.A), + "B": cls._tensor_description_to_json(operation.B), + "C": cls._tensor_description_to_json(operation.C), + "element_epilogue": cls._enum_to_json(operation.element_epilogue), + # Optional parameters + "epilogue_functor": cls._enum_to_json(operation.epilogue_functor), + "swizzling_functor": cls._enum_to_json(operation.swizzling_functor), + "D": cls._tensor_description_to_json(operation.D) if operation.D else None, + "kernel_schedule": cls._enum_to_json(operation.kernel_schedule), + "epilogue_schedule": cls._enum_to_json(operation.epilogue_schedule), + "tile_scheduler": cls._enum_to_json(operation.tile_scheduler), + } + + # Process optional attributes + optional_attrs = [ + "mixed_input_mode", + "mixed_input_shuffle", + "ScaleFactorA", + "ScaleFactorB", + "ScaleFactorD", + "ScaleFactorMVecSize", + "ScaleFactorNVecSize", + "ScaleFactorKVecSize", + "ScaleFactorVectorSize", + "is_3x", + ] + + for attr in optional_attrs: + if not hasattr(operation, attr): + continue + + value = getattr(operation, attr) + + if isinstance(value, TensorDescription): + result[attr] = cls._tensor_description_to_json(value) + elif isinstance(value, Enum): + result[attr] = cls._enum_to_json(value) + else: + result[attr] = value + + return result + + @classmethod + def _json_to_gemm_operation(cls, json_dict: dict[str, Any]) -> "GemmOperation": # type: ignore[name-defined] # noqa: F821 + """Convert JSON dict to GemmOperation object. + + Args: + json_dict: Dictionary representation + + Returns: + GemmOperation: Reconstructed object + """ + from cutlass_library import DataType + from cutlass_library.gemm_operation import GemmKind, GemmOperation + from cutlass_library.library import ( + EpilogueFunctor, + EpilogueFunctor3x, + EpilogueScheduleType, + KernelScheduleType, + MixedInputMode, + SwizzlingFunctor, + TileSchedulerType, + ) + + # Extract constructor parameters from the JSON dictionary + gemm_kind = cls._json_to_enum(json_dict["gemm_kind"], GemmKind) + arch = json_dict["arch"] + tile_description = cls._json_to_tile_description(json_dict["tile_description"]) + A = cls._json_to_tensor_description(json_dict.get("A"), "A") + B = cls._json_to_tensor_description(json_dict.get("B"), "B") + C = cls._json_to_tensor_description(json_dict.get("C"), "C") + element_epilogue = cls._json_to_enum(json_dict["element_epilogue"], DataType) + + # Get optional parameters with defaults + epilogue_functor = cls._json_to_enum( + json_dict.get("epilogue_functor"), + EpilogueFunctor3x if json_dict.get("is_3x") else EpilogueFunctor, + ) + swizzling_functor = cls._json_to_enum( + json_dict.get("swizzling_functor"), SwizzlingFunctor + ) + D = cls._json_to_tensor_description(json_dict.get("D"), "D") + kernel_schedule = cls._json_to_enum( + json_dict.get("kernel_schedule"), KernelScheduleType + ) + epilogue_schedule = cls._json_to_enum( + json_dict.get("epilogue_schedule"), EpilogueScheduleType + ) + tile_scheduler = cls._json_to_enum( + json_dict.get("tile_scheduler"), TileSchedulerType + ) + + mixed_input_mode = cls._json_to_enum( + json_dict.get("mixed_input_mode"), MixedInputMode + ) + mixed_input_shuffle = json_dict.get("mixed_input_shuffle", False) + + # Scale factors + ScaleFactorA = cls._json_to_enum(json_dict.get("ScaleFactorA"), DataType) + ScaleFactorB = cls._json_to_enum(json_dict.get("ScaleFactorB"), DataType) + + ScaleFactorD = None + if "ScaleFactorD" in json_dict and "ScaleFactorVectorSize" in json_dict: + ScaleFactorD = { + "tensor": cls._json_to_tensor_description( + json_dict.get("ScaleFactorD"), "ScaleFactorD" + ), + "vector_size": json_dict.get("ScaleFactorVectorSize"), + } + + ScaleFactorMVecSize = json_dict.get("ScaleFactorMVecSize") + ScaleFactorNVecSize = json_dict.get("ScaleFactorNVecSize") + ScaleFactorKVecSize = json_dict.get("ScaleFactorKVecSize") + + # Create the GemmOperation with the extracted parameters + operation = GemmOperation( + gemm_kind=gemm_kind, + arch=arch, + tile_description=tile_description, + A=A, + B=B, + C=C, + element_epilogue=element_epilogue, + epilogue_functor=epilogue_functor, + swizzling_functor=swizzling_functor, + D=D, + kernel_schedule=kernel_schedule, + epilogue_schedule=epilogue_schedule, + tile_scheduler=tile_scheduler, + mixed_input_mode=mixed_input_mode, + mixed_input_shuffle=mixed_input_shuffle, + ScaleFactorA=ScaleFactorA, + ScaleFactorB=ScaleFactorB, + ScaleFactorD=ScaleFactorD, + ScaleFactorMVecSize=ScaleFactorMVecSize, + ScaleFactorNVecSize=ScaleFactorNVecSize, + ScaleFactorKVecSize=ScaleFactorKVecSize, + ) + + return operation + + @classmethod + @functools.lru_cache(None) + def _tile_description_to_json(cls, tile_desc: "TileDescription") -> str: # type: ignore[name-defined] # noqa: F821 + """ + Convert TileDescription to JSON string. + + Args: + tile_desc: TileDescription object + + Returns: + str: JSON string representation + """ + + # Create the main dictionary with field names matching TileDescription constructor parameters + result = { + "threadblock_shape": tile_desc.threadblock_shape, + "stages": tile_desc.stages, + "warp_count": tile_desc.warp_count, + "math_instruction": cls._math_instruction_to_json( + tile_desc.math_instruction + ), + "min_compute": tile_desc.minimum_compute_capability, # Store as min_compute for constructor + "max_compute": tile_desc.maximum_compute_capability, # Store as max_compute for constructor + "cluster_shape": tile_desc.cluster_shape, + "explicit_vector_sizes": tile_desc.explicit_vector_sizes, + } + + # Add tile_shape if it exists and differs from threadblock_shape + if ( + hasattr(tile_desc, "tile_shape") + and tile_desc.tile_shape != tile_desc.threadblock_shape + ): + result["tile_shape"] = tile_desc.tile_shape + + return json.dumps(result) + + @classmethod + @functools.lru_cache(None) + def _json_to_tile_description( + cls, json_dict: str | None + ) -> Optional["TileDescription"]: # type: ignore[name-defined] # noqa: F821 + """ + Convert JSON dict to TileDescription object. + + Args: + json_dict: Dictionary representation + + Returns: + TileDescription: Reconstructed object + """ + if json_dict is None: + return None + + tile_dict = json.loads(json_dict) + + from cutlass_library.library import TileDescription + + math_instruction = cls._json_to_math_instruction(tile_dict["math_instruction"]) + + # Get compute capability values, checking both naming conventions + min_compute = tile_dict.get( + "min_compute", tile_dict.get("minimum_compute_capability") + ) + max_compute = tile_dict.get( + "max_compute", tile_dict.get("maximum_compute_capability") + ) + + # Get cluster shape with default value + cluster_shape = tile_dict.get("cluster_shape", [1, 1, 1]) + + # Create the TileDescription object + tile_desc = TileDescription( + threadblock_shape=tile_dict["threadblock_shape"], + stages=tile_dict["stages"], + warp_count=tile_dict["warp_count"], + math_instruction=math_instruction, + min_compute=min_compute, + max_compute=max_compute, + cluster_shape=cluster_shape, + explicit_vector_sizes=tile_dict.get("explicit_vector_sizes"), + ) + + # Set tile_shape if it exists and differs from threadblock_shape + if ( + "tile_shape" in tile_dict + and tile_dict["tile_shape"] != tile_dict["threadblock_shape"] + ): + tile_desc.tile_shape = tile_dict["tile_shape"] + + return tile_desc + + @classmethod + @functools.lru_cache(None) + def _math_instruction_to_json( + cls, + math_instruction: Optional["MathInstruction"], # type: ignore[name-defined] # noqa: F821 + ) -> str | None: + """Convert MathInstruction to JSON string. + + Args: + math_instruction: MathInstruction object + + Returns: + Optional[str]: JSON string representation or None + """ + if math_instruction is None: + return None + + result = { + "instruction_shape": math_instruction.instruction_shape, + "element_a": cls._enum_to_json(math_instruction.element_a), + "element_b": cls._enum_to_json(math_instruction.element_b), + "element_accumulator": cls._enum_to_json( + math_instruction.element_accumulator + ), + "opcode_class": cls._enum_to_json(math_instruction.opcode_class), + "math_operation": cls._enum_to_json(math_instruction.math_operation), + "element_scale_factor": cls._enum_to_json( + math_instruction.element_scale_factor + ), + } + + return json.dumps(result) + + @classmethod + @functools.lru_cache(None) + def _json_to_math_instruction( + cls, json_dict: str | None + ) -> Optional["MathInstruction"]: # type: ignore[name-defined] # noqa: F821 + """Convert JSON string to MathInstruction object. + + Args: + json_dict: JSON string representation + + Returns: + Optional[MathInstruction]: Reconstructed object or None + """ + if json_dict is None: + return None + + from cutlass_library import DataType + from cutlass_library.library import MathInstruction, MathOperation, OpcodeClass + + mi_dict = json.loads(json_dict) + + # Convert string enum names back to enum values + element_a = cls._json_to_enum(mi_dict["element_a"], DataType) + element_b = cls._json_to_enum(mi_dict["element_b"], DataType) + element_acc = cls._json_to_enum(mi_dict["element_accumulator"], DataType) + + # Get the opcode_class enum + opcode_class = cls._json_to_enum(mi_dict["opcode_class"], OpcodeClass) + + # Get the math_operation enum + math_op = cls._json_to_enum(mi_dict["math_operation"], MathOperation) + + # Create the MathInstruction object + math_instruction_obj = MathInstruction( + instruction_shape=mi_dict["instruction_shape"], + element_a=element_a, + element_b=element_b, + element_accumulator=element_acc, + opcode_class=opcode_class, + math_operation=math_op, + ) + + # Add element_scale_factor if it exists + if ( + "element_scale_factor" in mi_dict + and mi_dict["element_scale_factor"] is not None + ): + math_instruction_obj.element_scale_factor = cls._json_to_enum( + mi_dict["element_scale_factor"], DataType + ) + + return math_instruction_obj + + @classmethod + @functools.lru_cache(None) + def _tensor_description_to_json( + cls, + tensor_desc: Optional["TensorDescription"], # type: ignore[name-defined] # noqa: F821 + ) -> str | None: + """Convert TensorDescription to JSON string. + + Args: + tensor_desc: TensorDescription object + + Returns: + Optional[str]: JSON string representation or None + """ + if tensor_desc is None: + return None + + result = { + "element": cls._enum_to_json(tensor_desc.element), + "layout": cls._enum_to_json(tensor_desc.layout), + "alignment": tensor_desc.alignment, + "complex_transform": cls._enum_to_json(tensor_desc.complex_transform), + } + + return json.dumps(result) + + @classmethod + @functools.lru_cache(None) + def _json_to_tensor_description( + cls, + json_dict: str | None, + tensor_name: str | None = None, + ) -> Optional["TensorDescription"]: # type: ignore[name-defined] # noqa: F821 + """Convert JSON string to TensorDescription object. + + Args: + json_dict: JSON string representation + tensor_name: Name of the tensor to avoid cache in the same op + + Returns: + Optional[TensorDescription]: Reconstructed object or None + """ + if json_dict is None: + return None + + tensor_dict = json.loads(json_dict) + + from cutlass_library import DataType + from cutlass_library.library import ( + ComplexTransform, + LayoutType, + TensorDescription, + ) + + element = cls._json_to_enum(tensor_dict["element"], DataType) + layout = cls._json_to_enum(tensor_dict["layout"], LayoutType) + alignment = tensor_dict["alignment"] + complex_transform = cls._json_to_enum( + tensor_dict["complex_transform"], ComplexTransform + ) + + return TensorDescription(element, layout, alignment, complex_transform) + + @classmethod + @functools.lru_cache(None) + def _enum_to_json(cls, enum_value: Enum | None) -> str | None: + """Convert enum value to JSON string. + + Args: + enum_value: Enum value + + Returns: + Optional[str]: JSON string representation or None + """ + if enum_value is None: + return None + + result = { + "type": enum_value.__class__.__name__, + "name": enum_value.name, + } + + return json.dumps(result) + + @classmethod + @functools.lru_cache(None) + def _json_to_enum(cls, json_dict: str | None, enum_class: Any) -> Enum | None: + """Convert JSON string to enum value. + + Format: {name: "EnumName", value: 1} + + Args: + json_dict: JSON string representation + enum_class: Target enum class + + Returns: + Optional[Enum]: Reconstructed enum value or None + """ + if json_dict is None: + return None + + enum_dict = json.loads(json_dict) + + return enum_class[enum_dict["name"]] + + +@functools.lru_cache(1) +def get_cutlass_operation_serializer() -> CUTLASSOperationSerializer | None: + if not try_import_cutlass(): + return None + return CUTLASSOperationSerializer() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/template.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/template.py new file mode 100644 index 0000000000000000000000000000000000000000..189338cb8f2d7cc37bd48ba0b222c81f4574aeea --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/template.py @@ -0,0 +1,383 @@ +# mypy: allow-untyped-defs +import functools +import hashlib +import itertools +from dataclasses import dataclass +from typing import Any, TYPE_CHECKING +from unittest.mock import patch + +import sympy + +import torch +from torch._inductor import config +from torch._inductor.utils import clear_on_fresh_cache, Placeholder +from torch._logging import getArtifactLogger + +from ...autotune_process import CUTLASSBenchmarkRequest, TensorMeta +from ...ir import Buffer, CUTLASSTemplateBuffer, IRNode, Layout +from ...utils import IndentedBuffer, unique +from ...virtualized import V +from ..common import KernelTemplate +from .kernel import CUTLASSTemplateCaller, CUTLASSTemplateKernel +from .utils import DTYPE_TO_CUTLASS_TYPE + + +if TYPE_CHECKING: + from ...scheduler import BaseSchedulerNode # noqa: TC004 +else: + BaseSchedulerNode = Any + +GemmOperation = Any + +autotuning_log = getArtifactLogger(__name__, "autotuning") + + +@dataclass(frozen=True) +class ArgInfo: + name: str + ty: str + + +@clear_on_fresh_cache +class CUTLASSTemplate(KernelTemplate): + """ + CUTLASSTemplate is a class that provides a template for generating CUTLASS Templates. Used as a baseclass for the + CUTLASSGemmTemplate, providing functionality that might also be relevant for non-GEMM CUTLASS Kernels. + """ + + index_counter = itertools.count() + # dict of cache key to (code, size_args) + code_cache: dict[str, tuple[str, tuple[int, ...], tuple[int, ...]]] = {} + cache_clear = staticmethod(code_cache.clear) + + def __init__( + self, + name: str, + input_nodes: list[Buffer], + layout: Layout, + input_reorder: list[int] | None = None, + ) -> None: + """ + Baseclass for CUTLASS C++ Templates, derived from KernelTemplate. + Not to be instantiated directly. + + Args: + name (str): The name of the CUTLASSTemplate object. + input_nodes (List[IRNode]): A list of input IRNodes. + layout (Layout): The layout of the output buffer / tensor. + input_reorder (Optional[List[int]]): An optional list that specifies + the order of the input nodes. + """ + super().__init__(name) + self.input_nodes = input_nodes + self.output_node: Buffer = Buffer(name="buf_out", layout=layout) + self.input_reorder = input_reorder + self.layout = layout + self.device_type = layout.device.type + + @classmethod + @functools.lru_cache(None) + # pyrefly: ignore [bad-override] + def _template_from_string(cls, source: str) -> Any: + return KernelTemplate._template_from_string(source) + + @staticmethod + def supports_epilogue_fusion(op: GemmOperation) -> bool: + return False + + def make_key(self, name: str, input_key: str, layout_repr: str) -> str: + """ + Make a key for the code cache. The idea of the method is to cache + everything that matters but doesn't include runtime param values, i.e., + self.get_runtime_arg_values(). + + Args: + kwargs: Additional keyword arguments. Including op (GemmOperation). + """ + return hashlib.sha256( + str( + ( + input_key, + self.input_reorder, + # output layout, same as self.output_node.get_layout() + layout_repr, + self.get_runtime_arg_info(), + name, + ) + ).encode("utf-8") + ).hexdigest() + + def generate_code_and_args( + self, name: str, input_key: str, layout_repr: str, **kwargs + ) -> tuple[str, tuple[int, ...]]: + """ + Generate code and args with caching. We cache the code even if runtime + args are different. + """ + key: str | None = None + if config.cutlass.enable_caching_codegen: + key = self.make_key(name=name, input_key=input_key, layout_repr=layout_repr) + + if key is not None and key in self.code_cache: + code, size_args, offset_args = self.code_cache[key] + extra_args = tuple( + list(size_args) + + list(offset_args) + + list(self.get_runtime_arg_values(**kwargs)) + ) + return code, extra_args + + kernel_name = str(Placeholder.KERNEL_NAME) + kernel = CUTLASSTemplateKernel( + kernel_name=kernel_name, + runtime_arg_info=self.get_runtime_arg_info(), + runtime_arg_values=self.get_runtime_arg_values(**kwargs), + device_type=self.device_type, + ) + with patch.object(V.graph, "get_dtype", self._fake_get_dtype(self.output_node)): + code = self.render(kernel=kernel, **kwargs) + _, call_args, _, _ = kernel.args.python_argdefs() + autotuning_log.debug("Generated Code:\n%s", code) + autotuning_log.debug( + "Args: cpp_argdefs: %s, python_argdefs: %s", + kernel.args.cpp_argdefs(DTYPE_TO_CUTLASS_TYPE), + kernel.args.python_argdefs(), + ) + + input_reorder = ( + self.input_reorder + if self.input_reorder is not None + else list(range(len(self.input_nodes))) + ) + expected_args = list( + unique(self.input_nodes[idx].get_name() for idx in input_reorder) + ) + expected_args.extend([self.output_node.get_name()]) + assert list(call_args)[: len(expected_args)] == expected_args, ( + call_args, + expected_args, + ) + # Resolve symbolic sizes to concrete ints for benchmarking only. + V.graph.sizevars.optimization_hints( + map(sympy.expand, call_args[len(expected_args) :]) + ) + size_args = V.graph.sizevars.optimization_hints(kernel.get_dynamic_shape_args()) + offset_args = V.graph.sizevars.optimization_hints(kernel.get_offset_args()) + + if key is not None: + self.code_cache[key] = code, size_args, offset_args + + # extra args has runtime params, which shouldn't be cached + extra_args = tuple( + list(size_args) + list(offset_args) + self.get_runtime_arg_values(**kwargs) + ) + + return code, extra_args + + def generate( # type: ignore[override] + self, + name: str, + description: str, + input_key: str, + layout_repr: str, + input_tensor_meta: TensorMeta | list[TensorMeta], + output_tensor_meta: TensorMeta | list[TensorMeta], + **kwargs, + ) -> CUTLASSTemplateCaller: + """ + Generates the CUDA template caller object for the given GEMM template and operation. + This CUTLASSTemplateCaller may be used to call and benchmark the generated CUDA kernel + in a standalone manner to enable Autotuning. + + Args: + description: op name followed by swizzle. + kwargs: Additional keyword arguments. + + Returns: + A CUTLASSTemplateCaller object representing the generated CUDA template caller. + """ + code, extra_args = self.generate_code_and_args( + name=name, + input_key=input_key, + layout_repr=layout_repr, + **kwargs, + ) + + # not caching since kernel name is needed below + kernel_hash = hashlib.sha256(code.encode("utf-8")).hexdigest()[:8] + kernel_name = f"cutlass_{kernel_hash}" + code = code.replace(self.name, kernel_name) + + # create the BenchmarkRequest + bmreq = CUTLASSBenchmarkRequest( + kernel_name=kernel_name, + input_tensor_meta=input_tensor_meta, + output_tensor_meta=output_tensor_meta, + extra_args=extra_args, + source_code=code, + device_type=self.device_type, + ) + + # kwargs has "op" argument in case of CUTLASSGemmTemplate + op = kwargs["op"] + if not op: + supports_epilogue_fusion = False + else: + # epilogue fusion is only supported for TMA kernels + supports_epilogue_fusion = self.supports_epilogue_fusion(op) + + def make_kernel_render( + template_node: CUTLASSTemplateBuffer, + epilogue_nodes: list[BaseSchedulerNode] | None = None, + ) -> tuple[CUTLASSTemplateKernel, functools.partial[str]]: + assert supports_epilogue_fusion or not epilogue_nodes, ( + "epilogue fusion is not supported for this kernel" + ) + kernel = CUTLASSTemplateKernel( + kernel_name=str(Placeholder.KERNEL_NAME), + runtime_arg_info=self.get_runtime_arg_info(), + runtime_arg_values=self.get_runtime_arg_values(**kwargs), + device_type=self.device_type, + ) + render = functools.partial( + self.render, + kernel=kernel, + template_buffer_node=template_node, + epilogue_nodes=epilogue_nodes, + **kwargs, # includes "op" argument in case of CUTLASSGemmTemplate + ) + return kernel, render + + return CUTLASSTemplateCaller( + kernel_name, + "cutlass_gemm", + self.input_nodes, + self.output_node.get_layout(), + make_kernel_render, + bmreq, + supports_epilogue_fusion, + self, + kwargs, + description, + ) + + def header(self) -> IndentedBuffer: + res = IndentedBuffer() + res.splice( + """ + #include + #include + #include + #include + #include + """ + ) + res.splice( + """ + #include "cute/tensor.hpp" + #include "cutlass/cutlass.h" + #include "cutlass/numeric_types.h" + #include "cutlass/tensor_ref.h" + #include "cutlass/util/host_tensor.h" + #include "cutlass/util/reference/host/tensor_fill.h" + #include "cutlass/util/reference/device/tensor_fill.h" + #include "cutlass/util/device_memory.h" + """ + ) + return res + + def globals(self) -> IndentedBuffer: + res = IndentedBuffer() + res.splice( + """ + // We compile all models with -fvisibility=hidden. Any symbols that need to be + // exposed in the final shared library must be declared with PT_EXPORT to make + // them visible. + #ifdef __GNUC__ // Applies to any compiler with GNU extensions (clang and g++) + #define PT_EXPORT __attribute__((__visibility__("default"))) + #else + #ifdef _WIN32 + #define PT_EXPORT __declspec(dllexport) + #else + #define PT_EXPORT + #endif + #endif + """ + ) + res.splice( + """ + using namespace cute; + #define CUTLASS_CHECK(status) \\ + { \\ + cutlass::Status error = status; \\ + if (error != cutlass::Status::kSuccess) { \\ + auto msg = std::string("[") + __FILE__ + "] Got cutlass error: " + \\ + cutlassGetStatusString(error) + " at: " + std::to_string(__LINE__); \\ + throw std::runtime_error(msg); \\ + } \\ + } + + // Used as pass-through functor in EVT just for type casting / rounding + template + struct identity_op { + CUTLASS_HOST_DEVICE + T operator()(T val) const { return val; } + }; + + """ + ) + return res + + def cute_int(self, int_str: str, var_name: str) -> str: + res = "" + if int_str in ("1", "1L"): + res = "cute::Int<1>{}" + else: + res = int_str + + return f"{res} /* {var_name} */" + + _DTYPE_TO_CUTLASS = { + torch.float32: "float", + torch.float64: "double", + torch.float16: "cutlass::half_t", + torch.int32: "int32_t", + torch.int16: "int16_t", + torch.int8: "int8_t", + torch.uint8: "uint8_t", + torch.bool: "bool", + torch.bfloat16: "cutlass::bfloat16_t", + torch.float8_e4m3fn: "cutlass::float_e4m3_t", + torch.float8_e5m2: "cutlass::float_e5m2_t", + } + + _DTYPE_TO_CUTLASS_SPARSE_META = { + torch.int32: "uint32_t", + torch.int16: "uint16_t", + } + + def cutlass_type_cast(self, node: IRNode, ptr: str) -> str: + if node is None: + return ptr + else: + return f"({self._DTYPE_TO_CUTLASS.get(node.get_dtype())}*)({ptr})" + + def cutlass_sparse_meta_type_cast(self, node: IRNode, ptr: str) -> str: + if node is None: + return ptr + else: + return ( + f"({self._DTYPE_TO_CUTLASS_SPARSE_META.get(node.get_dtype())}*)({ptr})" + ) + + def render(self, **kwargs) -> str: + raise NotImplementedError + + def get_runtime_arg_info(self) -> list[ArgInfo]: + return [ArgInfo("swizzle", "const uint8_t")] + + def get_runtime_arg_values(self, **kwargs) -> list[Any]: + """ + Helper method to retrieve runtime args from generate kwargs + """ + return [kwargs[arg.name] for arg in self.get_runtime_arg_info()] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..5fac9f392abb9a44795a2fc16b109a0761e4553c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/cutlass/utils.py @@ -0,0 +1,536 @@ +# mypy: allow-untyped-defs +import atexit +import functools +import logging +import os +import shutil +import sys +import time +from dataclasses import dataclass +from typing import Any +from typing_extensions import TypeIs + +import sympy + +import torch +from torch._inductor.runtime.runtime_utils import dynamo_timed +from torch._inductor.utils import clear_on_fresh_cache +from torch.utils._ordered_set import OrderedSet + +from ... import config +from ...ir import Layout +from ...runtime.runtime_utils import cache_dir +from ...virtualized import V +from ..cuda.cuda_env import get_cuda_arch, get_cuda_version +from ..xpu.xpu_env import get_xpu_arch, get_xpu_version + + +log = logging.getLogger(__name__) + +CUTLASS_OPERATION_KIND: str = "gemm" +ACCUMULATOR_DTYPES: OrderedSet[torch.dtype] = OrderedSet([torch.float, torch.int32]) +XW_DTYPES: OrderedSet[torch.dtype] = OrderedSet( + [torch.half, torch.bfloat16, torch.float8_e4m3fn, torch.int8, torch.float8_e5m2] +) + + +@atexit.register +def move_cutlass_compiled_cache() -> None: + """Move CUTLASS compiled cache file to the cache directory if it exists.""" + if try_import_cutlass.cache_info().currsize == 0: + return + + try: + import cutlass_cppgen # type: ignore[import-not-found] + except ImportError: + return + + # Check if the CACHE_FILE attribute exists in cutlass_cppgen and if the file exists + if not hasattr(cutlass_cppgen, "CACHE_FILE") or not os.path.exists( + cutlass_cppgen.CACHE_FILE + ): + return + + try: + filename = os.path.basename(cutlass_cppgen.CACHE_FILE) + shutil.move(cutlass_cppgen.CACHE_FILE, os.path.join(cache_dir(), filename)) + log.debug("Moved CUTLASS compiled cache file to %s", cache_dir()) + except OSError: + log.warning("Failed to move CUTLASS compiled cache file", exc_info=True) + + +def _rename_cutlass_import(content: str, cutlass_modules: list[str]) -> str: + for cutlass_module in cutlass_modules: + content = content.replace( + f"from {cutlass_module} import ", + f"from cutlass_library.{cutlass_module} import ", + ) + return content + + +@functools.cache +def try_import_cutlass() -> bool: + """ + We want to support three ways of passing in CUTLASS: + 1. fbcode, handled by the internal build system. + 2. User specifies cutlass_dir. The default is ../third_party/cutlass/, + which is the directory when developers build from source. + """ + if config.is_fbcode(): + try: + import cutlass_cppgen # type: ignore[import-not-found] # noqa: F401 + import cutlass_library # type: ignore[import-not-found] + except ImportError as e: + log.warning( # noqa: G200 + "Failed to import CUTLASS packages in fbcode: %s, ignoring the CUTLASS backend.", + e, + ) + return False + + return True + + # Copy CUTLASS python scripts to a temp dir and add the temp dir to Python search path. + # This is a temporary hack to avoid CUTLASS module naming conflicts. + # TODO(ipiszy): remove this hack when CUTLASS solves Python scripts packaging structure issues. + + # TODO(mlazos): epilogue visitor tree currently lives in python/cutlass, + # but will be moved to python/cutlass_library in the future (later 2025) + def path_join(path0, path1): + return os.path.abspath(os.path.join(path0, path1)) + + # contains both cutlass and cutlass_library + # we need cutlass for eVT + cutlass_dir = ( + config.xpu.cutlass_dir + if torch.xpu._is_compiled() + else config.cutlass.cutlass_dir + ) + cutlass_python_path = path_join(cutlass_dir, "python") + torch_root = os.path.abspath(os.path.dirname(torch.__file__)) + mock_src_path = os.path.join( + torch_root, + "_inductor", + "codegen", + "cutlass", + "lib_extensions", + "cutlass_mock_imports", + ) + + cutlass_library_src_path = path_join(cutlass_python_path, "cutlass_library") + cutlass_cppgen_src_path = path_join(cutlass_python_path, "cutlass_cppgen") + pycute_src_path = path_join(cutlass_python_path, "pycute") + + tmp_cutlass_full_path = os.path.abspath(os.path.join(cache_dir(), "torch_cutlass")) + + dst_link_library = path_join(tmp_cutlass_full_path, "cutlass_library") + dst_link_cutlass_cppgen = path_join(tmp_cutlass_full_path, "cutlass_cppgen") + dst_link_pycute = path_join(tmp_cutlass_full_path, "pycute") + + # mock modules to import cutlass + mock_modules = ["cuda", "scipy", "pydot"] + + if os.path.isdir(cutlass_python_path): + if tmp_cutlass_full_path not in sys.path: + + def link_and_append(dst_link, src_path, parent_dir): + if os.path.lexists(dst_link): + assert os.path.islink(dst_link), ( + f"{dst_link} is not a symlink. Try to remove {dst_link} manually and try again." + ) + assert os.path.realpath(os.readlink(dst_link)) == os.path.realpath( + src_path, + ), f"Symlink at {dst_link} does not point to {src_path}" + else: + os.makedirs(parent_dir, exist_ok=True) + os.symlink(src_path, dst_link) + + if parent_dir not in sys.path: + sys.path.append(parent_dir) + + link_and_append( + dst_link_library, cutlass_library_src_path, tmp_cutlass_full_path + ) + link_and_append( + dst_link_cutlass_cppgen, cutlass_cppgen_src_path, tmp_cutlass_full_path + ) + link_and_append(dst_link_pycute, pycute_src_path, tmp_cutlass_full_path) + + for module in mock_modules: + link_and_append( + path_join(tmp_cutlass_full_path, module), # dst_link + path_join(mock_src_path, module), # src_path + tmp_cutlass_full_path, # parent + ) + + try: + import cutlass_cppgen # type: ignore[import-not-found] # noqa: F401, F811 + import cutlass_library.generator # noqa: F401 + import cutlass_library.library # noqa: F401 + import cutlass_library.manifest # noqa: F401 + import pycute # type: ignore[import-not-found] # noqa: F401 + + return True + except ImportError as e: + log.debug( # noqa: G200 + "Failed to import CUTLASS packages: %s, ignoring the CUTLASS backend.", + e, + ) + else: + log.debug( + "Failed to import CUTLASS packages: CUTLASS repo does not exist: %s", + cutlass_python_path, + ) + return False + + +def _normalize_xpu_arch(arch: str) -> str: + if arch.startswith("Xe"): + return arch[2:] + if 12 <= int(arch) and int(arch) <= 50: + return arch + else: + raise NotImplementedError(f"Unsupported xpu arch: {arch}") + + +def _normalize_cuda_arch(arch: str) -> str: + arch_num = arch + if isinstance(arch, str): + digits = "".join(ch for ch in arch if ch.isdigit()) + if not digits: + raise ValueError(f"Unrecognized cuda arch: {arch}") + arch_num = int(digits) + else: + arch_num = int(arch) + + if arch_num > 103: + log.warning("Detected CUDA architecture > 103: %s. Please file an issue.", arch) + return str(arch_num) + if arch_num >= 103: + return "103" + if arch_num >= 100: + return "100" + if arch_num >= 90: + return "90" + if arch_num >= 80: + return "80" + if arch_num >= 75: + return "75" + if arch_num >= 70: + return "70" + raise NotImplementedError(f"Unsupported cuda arch: {arch}") + + +@functools.lru_cache(8) +def cutlass_arch(device_type: str) -> str: + if device_type == "xpu": + arch = get_xpu_arch() + return _normalize_xpu_arch(arch) + else: + arch = get_cuda_arch() + return _normalize_cuda_arch(arch) + + +@functools.lru_cache(1) +def toolkit_version(device_type: str) -> str: + if device_type == "xpu": + return get_xpu_version() + else: + return get_cuda_version() + + +@dataclass +class CUTLASSArgs: + """ + CUTLASS args used to initialize a CUTLASS Manifest. + """ + + architectures: str | None = None + toolkit_version: str | None = None + instantiation_level: str | None = None + operations: str | None = None + + build_dir = "" + curr_build_dir = "" + generator_target = "" + kernels = "all" + ignore_kernels = "" + exclude_kernels = "" + # TODO: these three look dead? + kernel_filter_file: None = None + selected_kernel_list: None = None + interface_dir: None = None + filter_by_cc = True + disable_full_archs_compilation = False + device_type: str = "cuda" + + def __post_init__(self): + if self.architectures is None or self.toolkit_version is None: + raise RuntimeError( + f"{self.architectures=} or {self.toolkit_version=} is None!" + ) + + +@clear_on_fresh_cache +@functools.cache +def _gen_ops_cached(arch: str, version: str, device_type: str) -> dict[Any, Any]: + # Note: Cache needs to be specific for cuda architecture and version + + # Import cutlass python scripts. + assert try_import_cutlass() + import cutlass_library.generator as cutlass_generator + import cutlass_library.manifest as cutlass_manifest + + if arch is None or version is None: + log.error( + "Cannot detect cuda arch %s or version %s. " + "Will discard all cutlass ops. " + "Please consider setting _inductor.cuda.arch and _inductor.cuda.version configs.", + arch, + version, + ) + return {} + + gen_arch = ( + "100" if arch == "103" else arch + ) # CUTLASS SM103 generator only covers NVFB4; fallback to SM100 set + instantiation_level: str = config.cutlass.cutlass_instantiation_level + args = CUTLASSArgs( + architectures=gen_arch, + toolkit_version=version, + instantiation_level=instantiation_level, + operations=CUTLASS_OPERATION_KIND, + device_type=device_type, + ) + manifest = cutlass_manifest.Manifest(args) + + start_time = time.time() + if device_type == "xpu": + if hasattr(cutlass_generator, "GenerateIntelXe"): + cutlass_generator.GenerateIntelXe( + manifest, args.toolkit_version, arch=int(arch) + ) + else: + raise NotImplementedError( + "Arch " + arch + " is not supported by current cutlass lib." + ) + + elif arch == "100": + if hasattr(cutlass_generator, "GenerateSM100"): + cutlass_generator.GenerateSM100(manifest, args.toolkit_version) + cutlass_generator.GenerateSM90(manifest, args.toolkit_version) + else: + try: + func = getattr(cutlass_generator, "GenerateSM" + gen_arch) + func(manifest, args.toolkit_version) + except AttributeError as e: + raise NotImplementedError( + "Arch " + gen_arch + " is not supported by current cutlass lib." + ) from e + + log.info( + "CUTLASS library generated a dict of %d operation kinds in %.2f seconds", + len(manifest.operations), + time.time() - start_time, + ) + return manifest.operations + + +def gen_ops(device_type: str) -> dict[Any, Any]: + """ + Generates all supported CUTLASS operations. + """ + with dynamo_timed("cutlass_utils.gen_ops"): + arch = cutlass_arch(device_type) + version = toolkit_version(device_type) + return _gen_ops_cached(arch, version, device_type) + + +from ..cpp_utils import DTYPE_TO_CPP + + +if torch.xpu._is_compiled(): + DTYPE_TO_CUTLASS_TYPE = { + **DTYPE_TO_CPP, + torch.float16: "uint16_t", + torch.bfloat16: "uint16_t", + torch.float8_e4m3fn: "uint8_t", + torch.float8_e5m2: "uint8_t", + } +else: + DTYPE_TO_CUTLASS_TYPE = { + **DTYPE_TO_CPP, + torch.float16: "__half", + torch.bfloat16: "__nv_bfloat16", + torch.float8_e4m3fn: "__nv_fp8_e4m3", + torch.float8_e5m2: "__nv_fp8_e5m2", + } + + +@functools.lru_cache(32) +def torch_dtype_to_cutlass_type( + torch_dtype: torch.dtype, +) -> "cutlass_library.library.DataType": # type: ignore[name-defined] # noqa: F821 + # Import cutlass python scripts. + assert try_import_cutlass() + import cutlass_library # type: ignore[import] + + if torch_dtype == torch.float: + return cutlass_library.library.DataType.f32 + elif torch_dtype == torch.half: + return cutlass_library.library.DataType.f16 + elif torch_dtype == torch.bfloat16: + return cutlass_library.library.DataType.bf16 + elif torch_dtype == torch.float8_e4m3fn: + return cutlass_library.library.DataType.e4m3 + elif torch_dtype == torch.float8_e5m2: + return cutlass_library.library.DataType.e5m2 + else: + raise NotImplementedError(f"Unsupported data type: {torch_dtype=}") + + +@functools.lru_cache(32) +def dtype_match( + torch_dtype: torch.dtype | None, + cutlass_dtype: "cutlass_library.library.DataType", # type: ignore[name-defined] # noqa: F821 +) -> bool: + # Import cutlass python scripts. + assert try_import_cutlass() + import cutlass_library + + if torch_dtype == torch.float: + return ( + cutlass_dtype == cutlass_library.library.DataType.f32 + or cutlass_dtype == cutlass_library.library.DataType.tf32 + ) + elif torch_dtype == torch.half: + return cutlass_dtype == cutlass_library.library.DataType.f16 + elif torch_dtype == torch.bfloat16: + return cutlass_dtype == cutlass_library.library.DataType.bf16 + elif torch_dtype == torch.int8: + return cutlass_dtype == cutlass_library.library.DataType.s8 + elif torch_dtype == torch.uint8: + return cutlass_dtype == cutlass_library.library.DataType.u8 + elif torch_dtype == torch.int32: + return cutlass_dtype == cutlass_library.library.DataType.s32 + elif torch_dtype == torch.float8_e4m3fn: + return cutlass_dtype == cutlass_library.library.DataType.e4m3 + elif torch_dtype == torch.float8_e5m2: + return cutlass_dtype == cutlass_library.library.DataType.e5m2 + else: + return False + + +def get_accumulator_dtype( + input_torch_dtypes: list[torch.dtype], +) -> torch.dtype | None: + """ + Given a pair of input torch dtypes, returns the inferred accumulator torch dtype. + """ + + assert OrderedSet(input_torch_dtypes) <= XW_DTYPES, ( + f"{input_torch_dtypes=} is not supported" + ) + + if len(input_torch_dtypes) != 2: + return None + + if OrderedSet(input_torch_dtypes) == OrderedSet( + [torch.float8_e5m2, torch.float8_e4m3fn] + ): + return torch.float + + torch_dtype = None + if input_torch_dtypes[0] == input_torch_dtypes[1]: + torch_dtype = input_torch_dtypes[0] + else: + size0 = torch.tensor([], dtype=input_torch_dtypes[0]).element_size() + size1 = torch.tensor([], dtype=input_torch_dtypes[1]).element_size() + if size0 > size1: + dtype0, dtype1 = input_torch_dtypes + else: + dtype1, dtype0 = input_torch_dtypes + if dtype0 in [torch.half, torch.bfloat16] and dtype1 in [ + torch.int8, + torch.uint8, + ]: + torch_dtype = dtype0 + + if torch_dtype in ( + torch.float16, + torch.bfloat16, + torch.float, + torch.float8_e4m3fn, + torch.float8_e5m2, + ): + accumulator_dtype = torch.float + elif torch_dtype == torch.int8: + accumulator_dtype = torch.int32 + else: + raise NotImplementedError(f"Unsupported data types: {input_torch_dtypes=}") + + assert accumulator_dtype in ACCUMULATOR_DTYPES, ( + f"{accumulator_dtype=} is not supported" + ) + return accumulator_dtype + + +@functools.lru_cache(32) +def get_alignments(torch_dtype: torch.dtype) -> list[int]: + """ + Returns all possible valid CUTLASS alignments in terms of the number of elements for a given dtype. + CUTLASS gemm / conv SM80 APIs support 16 bytes max alignment, and 2 bytes min alignment. + """ + + if torch_dtype in (torch.half, torch.bfloat16): + return [8, 4, 2, 1] + elif torch_dtype == torch.float: + return [4, 2, 1] + elif torch_dtype in ( + torch.uint8, + torch.int8, + torch.float8_e4m3fn, + torch.float8_e5m2, + ): + return [16, 8, 4, 2] + elif torch_dtype == torch.int32: + return [4, 2, 1] + else: + raise NotImplementedError(f"unsupported {torch_dtype=} for alignments") + + +def get_max_alignment(inductor_layout: Layout) -> int: + """ + Returns the max alignment (in terms of number of elements) for a given Inductor Layout. + """ + + dtype = inductor_layout.dtype + size = inductor_layout.size + offset = inductor_layout.offset + + def is_static_int(number: object) -> TypeIs[int | sympy.Integer]: + return isinstance(number, (int | sympy.Integer)) + + def a_factor_of(x, alignment): + if is_static_int(x) and is_static_int(alignment): + return x % alignment == 0 + rem = sympy.Mod(x, alignment) + return V.graph.sizevars.evaluate_expr(sympy.Eq(rem, 0)) + + try: + contiguous_dim = inductor_layout.stride.index(1) + except ValueError: + # No dim with stride 1 found, return 1 + return 1 + alignments = get_alignments(dtype) + for alignment in alignments: + if not a_factor_of(size[contiguous_dim], alignment) or not a_factor_of( + offset, alignment + ): + continue + if all( + (dim == contiguous_dim) + or a_factor_of(inductor_layout.stride[dim], alignment) + for dim in range(len(size)) + ): + return alignment + return 1 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/debug_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/debug_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..24f2c88add6b86cf1de2d93c939e6cfc255a2cd3 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/debug_utils.py @@ -0,0 +1,290 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import functools +import logging +import os +from enum import Enum +from typing import TYPE_CHECKING + +import torch +from torch import dtype as torch_dtype + +from .. import config +from ..virtualized import V +from .multi_kernel import MultiKernel + + +if TYPE_CHECKING: + from collections.abc import Callable + + +log = logging.getLogger(__name__) + + +def _print_debugging_tensor_value_info(msg, arg): + # helper for printing debugging stats for intermediate tensor values + # at jit inductor level codegen + max_numel_to_print = 64 + print(msg) + if not isinstance(arg, torch.Tensor): + print("Value: ", arg) + return + numel = arg.float().numel() + # print the debug printing stats + if numel <= max_numel_to_print: + print(arg) + print("Number of elements: ", numel) + print("Size: ", arg.float().size()) + print("Dtype: ", arg.float().mean().item()) + print("Mean: ", arg.float().mean().item()) + print("Min: ", arg.float().min().item()) + print("Max: ", arg.float().max().item()) + print("Std: ", arg.float().std().item()) + + +# AOTI debug printing related configs +class IntermediateValueDebuggingLevel(Enum): + # OFF: No intermediate tensor value debug info will be printed or saved. + OFF = "0" + # LEVEL 1: Save all intermediate tensor values to individual `.pt` files. No debug printing will be displayed. + SAVE_ONLY = "1" + # LEVEL 2: Print all intermediate tensor values by default to the console. No debug saving will be performed. + PRINT_ONLY = "2" + # LEVEL 3: Print all kernel names to the console only. No debug saving/printing for input tensor value info will be performed. + # This mode can be helpful in cases when you just want to pinpointing what kernel is running into a CUDA IMA issue, etc. + PRINT_KERNEL_NAMES_ONLY = "3" + + +class DebugPrinterManager: + def __init__( + self, + debug_printer_level, + use_array_ref: bool, + writeline: Callable[..., None] | None = None, + args_to_print_or_save: list[str] | None = None, + kernel_name: str = "", + kernel=None, + arg_signatures: list[type] | None = None, + kernel_type=None, + ): + self.debug_printer_level = IntermediateValueDebuggingLevel(debug_printer_level) + self.use_array_ref = use_array_ref + if args_to_print_or_save is None: + args_to_print_or_save = [] + self.args_to_print_or_save = args_to_print_or_save + self.kernel_name = kernel_name + self.arg_signatures: list[type] | None = None + self.kernel = kernel + self.filtered_kernel_names_to_print = self._get_debug_filtered_kernel_names() + self.kernel_type = None + + def __enter__(self): + self._perform_debug_print_or_save_helper( + self.args_to_print_or_save, + self.kernel_name, + before_launch=True, + arg_signatures=self.arg_signatures, + ) + + def __exit__(self, args_to_print_or_save, kernel_name, arg_signatures): + self._perform_debug_print_or_save_helper( + args_to_print_or_save, + kernel_name, + before_launch=False, + arg_signatures=arg_signatures, + ) + + def _perform_debug_print_or_save_helper( + self, + args_to_print_or_save, + kernel_name, + before_launch, + arg_signatures: list[type] | None = None, + ): + if self.debug_printer_level == IntermediateValueDebuggingLevel.OFF: + return + if self.debug_printer_level == IntermediateValueDebuggingLevel.SAVE_ONLY: + # by default save all the tensor values before launch + self.codegen_intermediate_tensor_value_save( + self.args_to_print_or_save, + self.kernel_name, + before_launch, + arg_signatures=self.arg_signatures, + ) + if self.debug_printer_level == IntermediateValueDebuggingLevel.PRINT_ONLY: + # by default print all the tensor values before launch + self.codegen_intermediate_tensor_value_print( + self.args_to_print_or_save, + self.kernel_name, + before_launch, + arg_signatures=self.arg_signatures, + ) + if ( + self.debug_printer_level + == IntermediateValueDebuggingLevel.PRINT_KERNEL_NAMES_ONLY + ): + # Print all kernel names to the console only + self.codegen_intermediate_tensor_value_print( + [], + self.kernel_name, + before_launch, + ) + + @functools.lru_cache # noqa: B019 + def _get_debug_filtered_kernel_names(self) -> list[str]: + if config.aot_inductor.filtered_kernel_names is None: + return [] + return [ + x.strip() + for x in config.aot_inductor.filtered_kernel_names.lower().split(",") + ] + + def set_printer_args( + self, + args_to_print_or_save: list[str], + kernel_name: str, + arg_signatures: list[type] | None, + kernel, + kernel_type=None, + ): + # Note: MultiKernel debug printing is not supported for now + if isinstance(kernel, MultiKernel): + log.info( + "MultiKernel type is not supported in AOTI debug printer tool yet." + ) + self.debug_printer_level = IntermediateValueDebuggingLevel.OFF + + self.kernel_type = kernel_type + # Note: if the kernel type is an extern kernel (or cpp kernel), we do a special handling to + # get the list of args_to_print_or_save + # TODO: Find a more reliable way to detect kernel args types to print for extern kernel calls + if kernel_type == "extern": + args_to_print_or_save_extern = [ + arg + for arg in args_to_print_or_save + if isinstance(arg, str) and arg.startswith(("buf", "arg")) + ] + self.args_to_print_or_save = args_to_print_or_save_extern + elif kernel_type == "cpp": + self.args_to_print_or_save = [ + ( + f"copy_arrayref_tensor_to_tensor({arg})" + if self.use_array_ref + else arg + ) + for arg in args_to_print_or_save + if isinstance(arg, str) and arg.startswith(("buf", "arg")) + ] + else: + self.args_to_print_or_save = args_to_print_or_save + self.kernel_name = kernel_name + self.arg_signatures = arg_signatures + self.kernel = kernel + + def codegen_model_inputs_value_print(self, input_args_to_print: list[str]) -> None: + if self.debug_printer_level != IntermediateValueDebuggingLevel.PRINT_ONLY: + return + for arg in input_args_to_print: + if V.graph.cpp_wrapper: + V.graph.wrapper_code.prefix.writeline( + f'aoti_torch_print_tensor_handle({arg}, "aoti_model_inputs - {arg}");' + ) + + def codegen_intermediate_tensor_value_save( + self, + args_to_save, + kernel_name, + before_launch=True, + arg_signatures: list[type] | None = None, + ) -> None: + for i, arg in enumerate(args_to_save): + if arg_signatures is not None and not isinstance( + arg_signatures[i], torch_dtype + ): + # infer from the arg data type (has torch.dtype) to see if it is a tensor type + continue + launch_prefix = "before_launch" if before_launch else "after_launch" + if V.graph.cpp_wrapper: + V.graph.wrapper_code.writeline( + f'aoti_torch_save_tensor_handle({arg}, "{arg}", "{launch_prefix}", "{kernel_name}");' + ) + else: + cwd = os.getcwd() + saved_dir = cwd + "/tmp/jit_inductor/" + if not os.path.exists(saved_dir): + log.info( + "Creating directory to save inductor intermediate tensor values." + ) + os.makedirs(saved_dir) + # Save the model to the directory + saved_path = saved_dir + f"{launch_prefix}_{kernel_name}_{arg}.pt" + log.info( + "Saved intermediate tensor %s for %s to %s", + arg, + kernel_name, + saved_path, + ) + line = f"torch.save({arg}, '{saved_path}')" + V.graph.wrapper_code.writeline(line) + + def codegen_intermediate_tensor_value_print( + self, + args_to_print, + kernel_name, + before_launch=True, + arg_signatures: list[type] | None = None, + ) -> None: + launch_prefix = "before_launch" if before_launch else "after_launch" + + # if the debug printing level is PRINT_KERNEL_NAMES_ONLY + # we only print the kernel name to the console + if ( + self.debug_printer_level + == IntermediateValueDebuggingLevel.PRINT_KERNEL_NAMES_ONLY + ): + if V.graph.cpp_wrapper: + V.graph.wrapper_code.writeline( + f'printf("[ {launch_prefix}: {kernel_name} ]\\n");' + ) + return + + if self.debug_printer_level != IntermediateValueDebuggingLevel.PRINT_ONLY: + return + for i, arg in enumerate(args_to_print): + # when debug printing is enabled i.e. IntermediateValueDebuggingLevel.PRINT_ONLY, + # check if filtered kernel name list is provided + if ( + len(self.filtered_kernel_names_to_print) > 0 + and kernel_name.lower() not in self.filtered_kernel_names_to_print + ): + continue + if V.graph.cpp_wrapper: + if arg_signatures is not None and isinstance( + arg_signatures[i], torch_dtype + ): + # infer from the arg data type (has torch.dtype) to see if it is a tensor type + V.graph.wrapper_code.writeline( + f'aoti_torch_print_tensor_handle({arg}, "{launch_prefix} - {kernel_name} - {arg}");' + ) + elif arg_signatures is not None and isinstance( + arg_signatures[i], + ( + type(torch._inductor.codegen.wrapper.SymbolicCallArg), + type(int), + type(float), + type(bool), + ), + ): + V.graph.wrapper_code.writeline( + f'printf("[ {launch_prefix} - {kernel_name} - {arg}: %ld ]", {arg}); printf("\\\\n");' + ) + else: + if arg_signatures is None and self.kernel_type in ("cpp", "extern"): + V.graph.wrapper_code.writeline( + f'aoti_torch_print_tensor_handle({arg}, "{launch_prefix} - {kernel_name} - {arg}");' + ) + else: + V.graph.wrapper_code.writeline( + f'_print_debugging_tensor_value_info("inductor: {launch_prefix} - {kernel_name} - {arg}", {arg})' + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/halide.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/halide.py new file mode 100644 index 0000000000000000000000000000000000000000..f782dea55fd4e6a4efd091836bc179b921c503d2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/halide.py @@ -0,0 +1,1808 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import dataclasses +import functools +import itertools +import logging +import re +import sys +from collections import defaultdict +from typing import Any, cast, TYPE_CHECKING + +import sympy + +import torch +import torch._logging + +from ..._prims_common import is_integer_dtype +from ...utils._ordered_set import OrderedSet +from ...utils._sympy.functions import FloorDiv, ModularIndexing +from ...utils._sympy.symbol import symbol_is_type, SymT +from ...utils._sympy.value_ranges import ValueRanges +from .. import config, ir +from ..codecache import HalideCodeCache +from ..ir import get_reduction_combine_fn +from ..metrics import is_metric_table_enabled, log_kernel_metadata +from ..ops_handler import AddParenHandler +from ..runtime.hints import HalideInputSpec, HalideMeta +from ..utils import ( + get_bounds_index_expr, + get_kernel_metadata, + parallel_num_threads, + sympy_index_symbol, + sympy_subs, +) +from ..virtualized import _ops as ops, V +from .common import ( + BackendFeature, + CSEVariable, + DeferredLine, + IndentedBuffer, + KernelArgType, + OpOverrides, + PythonPrinter, + SizeArg, + TensorArg, +) +from .cpp import DTYPE_TO_CPP +from .cpp_utils import cexpr +from .simd import constant_repr, SIMDKernel, SIMDScheduling + + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + + from ..ops_handler import ReductionType, StoreMode + from ..shape_propagation import BlockShapeType + +log = logging.getLogger(__name__) + + +def halide_constant(val): + if isinstance(val, int) and not (-2147483648 <= val <= 2147483647): + info = torch.iinfo(torch.int64) + if val == info.min: + return "hl.Int(64).min()" + if val == info.max: + return "hl.Int(64).max()" + return f"hl.i64({val!r})" + if isinstance(val, float): + return f"hl.f64({constant_repr(val)})" + return repr(val) + + +class Unsupported(RuntimeError): + def __init__(self, thing) -> None: + super().__init__(f"halide backend does not support: {thing}") + + +class HalidePrinter(PythonPrinter): # noqa: docstring_linter + @staticmethod + def cast_index(expr): + return f"hl.cast({V.kernel.index_dtype}, {expr})" + + @staticmethod + def cast_float(expr): + return f"hl.cast(hl.Float(32), {expr})" + + def _print_Float(self, expr): + return f"hl.f32({expr})" + + def _print_ToFloat(self, expr): + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"hl.f32({self._print(expr.args[0])})" + + def _print_floor(self, expr): + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return self.cast_index(f"hl.floor({self._print(expr.args[0])})") + + _print_FloorToInt = _print_floor + + def _print_Trunc(self, expr): + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return self.cast_index(f"hl.trunc({self._print(expr.args[0])})") + + _print_TruncToInt = _print_Trunc + + def _print_ceiling(self, expr): + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return self.cast_index(f"hl.ceil({self._print(expr.args[0])})") + + def _helper_sqrt(self, expr): + # pyrefly: ignore [missing-attribute] + return f"hl.sqrt({self.cast_float(self._print(expr))})" + + def _print_Where(self, expr): + c = self.doprint(expr.args[0]) + p = self.doprint(expr.args[1]) + q = self.doprint(expr.args[2]) + return f"hl.select({c}, {p}, {q})" + + def _print_Min(self, expr): + if len(expr.args) == 1: + # pyrefly: ignore [missing-attribute] + return self._print(expr.args[0]) + + mid = len(expr.args) // 2 + # pyrefly: ignore [missing-attribute] + a = self._print(sympy.Min(*expr.args[:mid])) + # pyrefly: ignore [missing-attribute] + b = self._print(sympy.Min(*expr.args[mid:])) + return f"hl.min({a}, {b})" + + def _print_Max(self, expr): + if len(expr.args) == 1: + # pyrefly: ignore [missing-attribute] + return self._print(expr.args[0]) + + mid = len(expr.args) // 2 + # pyrefly: ignore [missing-attribute] + a = self._print(sympy.Max(*expr.args[:mid])) + # pyrefly: ignore [missing-attribute] + b = self._print(sympy.Max(*expr.args[mid:])) + + return f"hl.max({a}, {b})" + + def _print_Abs(self, expr): + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return self.cast_index(f"hl.abs({self._print(expr.args[0])})") + + def _print_OpaqueUnaryFn_cos(self, expr): + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"hl.cos({self._print(expr.args[0])})" + + def _print_OpaqueUnaryFn_cosh(self, expr): + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"hl.cosh({self._print(expr.args[0])})" + + def _print_OpaqueUnaryFn_acos(self, expr): + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"hl.acos({self._print(expr.args[0])})" + + def _print_OpaqueUnaryFn_sin(self, expr): + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"hl.sin({self._print(expr.args[0])})" + + def _print_OpaqueUnaryFn_sinh(self, expr): + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"hl.sinh({self._print(expr.args[0])})" + + def _print_OpaqueUnaryFn_asin(self, expr): + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"hl.asin({self._print(expr.args[0])})" + + def _print_OpaqueUnaryFn_tan(self, expr): + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"hl.tan({self._print(expr.args[0])})" + + def _print_OpaqueUnaryFn_tanh(self, expr): + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"hl.tanh({self._print(expr.args[0])})" + + def _print_OpaqueUnaryFn_atan(self, expr): + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"hl.atan({self._print(expr.args[0])})" + + def _print_OpaqueUnaryFn_log2(self, expr): + raise NotImplementedError("log2") + + def _print_FloorDiv(self, expr): + if expr.is_integer: + return super()._print_FloorDiv(expr) + + x, div = expr.args + x = self.cast_float(self.doprint(x)) + div = self.cast_float(self.doprint(div)) + return self.cast_index(f"hl.floor({x} / {div})") + + def _print_Round(self, expr): + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return self.cast_index(f"hl.round({self._print(expr.args[0])})") + + _print_RoundToInt = _print_Round + + def _print_IntTrueDiv(self, expr): + a, b = expr.args + # force a cast to float + return f"({a}) / ({b}+hl.f32(0))" + + def _print_RoundDecimal(self, expr): + val, n = expr.args + # pyrefly: ignore [missing-attribute] + val = self._print(val) + n = int(n) + return f"hl.f32({10.0 ** (-n)!r})*hl.round(({val})*hl.f32({10.0**n!r}))" + + +texpr = HalidePrinter().doprint +pexpr = PythonPrinter().doprint + + +_halide_type = { + torch.bool: "hl.Bool()", + torch.bfloat16: "hl.BFloat(16)", + torch.float16: "hl.Float(16)", + torch.float32: "hl.Float(32)", + torch.float64: "hl.Float(64)", + torch.int8: "hl.Int(8)", + torch.int16: "hl.Int(16)", + torch.int32: "hl.Int(32)", + torch.int64: "hl.Int(64)", + torch.uint8: "hl.UInt(8)", + torch.uint16: "hl.UInt(16)", + torch.uint32: "hl.UInt(32)", + torch.uint64: "hl.UInt(64)", +} + + +def halide_type(dtype): + return _halide_type[dtype] + + +def halide_acc_type(dtype): + if is_integer_dtype(dtype) and dtype.is_signed and dtype != torch.int64: + dtype = torch.int32 + if dtype in (torch.float16, torch.bfloat16): + dtype = torch.float32 + return halide_type(dtype) + + +class HalideOverrides(OpOverrides): + @staticmethod + def to_dtype( + x, + dtype: torch.dtype, + src_dtype: torch.dtype | None = None, + use_compute_types=True, + ): + if dtype == torch.bool: + return f"({x} != 0)" + return f"hl.cast({halide_type(dtype)}, {x})" + + @staticmethod + def to_dtype_bitcast(x, dtype: torch.dtype, src_dtype: torch.dtype): + if src_dtype in (torch.float16, torch.bfloat16): + x = f"hl.cast({halide_type(src_dtype)}, {x})" # body compute is upcast to fp32 + line = f"hl.reinterpret({halide_type(dtype)}, {x})" + if dtype in (torch.float16, torch.bfloat16): + line = f"hl.cast(hl.Float(32), {line})" + return line + + @classmethod + def constant(cls, value, dtype): + return cls.to_dtype(halide_constant(value), dtype) + + @staticmethod + # pyrefly: ignore [bad-override] + def abs(x): + return f"hl.abs({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def exp(x): + if not hasattr(x, "name"): + return f"hl.exp({x})" + return f"hl.fast_exp(hl.cast(hl.Float(32), {x})) if {x.name}.type().bits() <= 32 else hl.exp({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def sqrt(x): + return f"hl.sqrt({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def minimum(a, b): + # return f"hl.min({a}, {b})" <== handles nan wrong + if not hasattr(a, "name"): + return f"hl.min({a}, {b})" + b = f"hl.cast({a.name}.type(), {b})" + return f"hl.select(({a}<{b})|hl.is_nan({a}), {a}, {b}) if {a.name}.type().is_float() else hl.min({a}, {b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def maximum(a, b): + # return f"hl.max({a}, {b})" <== handles nan wrong + if not hasattr(a, "name"): + return f"hl.max({a}, {b})" + b = f"hl.cast({a.name}.type(), {b})" + return f"hl.select(({a}>{b})|hl.is_nan({a}), {a}, {b}) if {a.name}.type().is_float() else hl.max({a}, {b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def where(a, b, c): + if hasattr(b, "name"): + c = f"hl.cast({b.name}.type(), {c})" + return f"hl.select({a}, {b}, {c})" + + @staticmethod + # pyrefly: ignore [bad-override] + def cos(x): + return f"hl.cos({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def sin(x): + return f"hl.sin({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def lgamma(x): + raise Unsupported("lgamma") + + @staticmethod + # pyrefly: ignore [bad-override] + def erf(x): + return f"hl.erf({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def cosh(x): + return f"hl.cosh({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def sinh(x): + return f"hl.sinh({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def acos(x): + return f"hl.acos({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def acosh(x): + return f"hl.acosh({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def asin(x): + return f"hl.asin({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def asinh(x): + return f"hl.asinh({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def atan2(x, y): + return f"hl.atan2({x}, {y})" + + @staticmethod + # pyrefly: ignore [bad-override] + def atan(x): + return f"hl.atan({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def atanh(x): + return f"hl.atanh({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def copysign(x, y): + raise Unsupported("copysign") + + @staticmethod + # pyrefly: ignore [bad-override] + def erfinv(x): + raise Unsupported("erfinv") + + @staticmethod + # pyrefly: ignore [bad-override] + def hypot(x, y): + return f"hl.hypot({x}, {y})" + + @staticmethod + # pyrefly: ignore [bad-override] + def nextafter(x, y): + raise Unsupported("nextafter") + + @staticmethod + # pyrefly: ignore [bad-override] + def logical_and(a, b): + return f"{a} & {b}" + + @staticmethod + def logical_not(a): + return f"{a} == 0" + + @staticmethod + # pyrefly: ignore [bad-override] + def logical_or(a, b): + return f"{a} | {b}" + + @staticmethod + # pyrefly: ignore [bad-override] + def logical_xor(a, b): + return f"({a} ^ {b})" + + @staticmethod + def bitwise_and(a, b): + return f"{a} & {b}" + + @staticmethod + def bitwise_not(a): + return f"~{a}" + + @staticmethod + def bitwise_or(a, b): + return f"{a} | {b}" + + @staticmethod + def bitwise_xor(a, b): + return f"{a} ^ {b}" + + @staticmethod + def bitwise_left_shift(a, b): + return f"{a} << {b}" + + @staticmethod + def bitwise_right_shift(a, b): + return f"{a} >> {b}" + + @staticmethod + def rand(seed, offset): + return f"halide_helpers.rand({seed}, {offset})" + + @staticmethod + def randn(seed, offset): + return f"halide_helpers.randn({seed}, {offset})" + + @staticmethod + def rand_eager(seed, base_offset, threads_per_round, tid, vec): + return f"halide_helpers.rand_eager_kernel({seed}, {base_offset}, {threads_per_round}, {tid}, {vec})" + + @staticmethod + def randint64(seed, offset, low, high): + return f"halide_helpers.randint64({seed}, {offset}, {low}, {high})" + + @staticmethod + def load_seed(name, offset): + return f"{ops.load(name, 0)} + {V.kernel.args.seed_offset('load_seed_offset', offset)}" + + @staticmethod + # pyrefly: ignore [bad-override] + def rsqrt(x): + # return f"hl.fast_inverse_sqrt({x})" <== accuracy issues + return f"1./hl.sqrt({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def tan(x): + return f"hl.tan({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def tanh(x): + return f"hl.tanh({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def signbit(x): + return f"(hl.reinterpret(hl.UInt(32), hl.cast(hl.Float(32), {x})) >> 31) != 0" + + @staticmethod + # pyrefly: ignore [bad-override] + def fmod(a, b): + # TODO(jansel): find a better way to do this, builtin % has wrong sign + return f"{a} - hl.trunc({a}/{b})*{b}" + + @staticmethod + def pow(a, b): + return f"hl.pow({a}, {b})" # hl.fast_pow fails accuracy + + @staticmethod + # pyrefly: ignore [bad-override] + def ldexp(x, n): + raise Unsupported("ldexp") + + @staticmethod + # pyrefly: ignore [bad-override] + def log(x): + return f"hl.log({x})" # hl.fast_log fails accuracy + + @staticmethod + def log2(x): + raise NotImplementedError("log2") + + @staticmethod + # pyrefly: ignore [bad-override] + def isinf(x): + # workaround https://github.com/halide/Halide/issues/8309 + return f"hl.is_inf(hl.cast(hl.Float(32), {x}))" + + @staticmethod + # pyrefly: ignore [bad-override] + def isnan(x): + # workaround https://github.com/halide/Halide/issues/8309 + return f"hl.is_nan(hl.cast(hl.Float(32), {x}))" + + @staticmethod + # pyrefly: ignore [bad-override] + def round(x): + return f"hl.round({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def floor(x): + return f"hl.floor({x})" + + @staticmethod + def int_truediv(a, b): + return f"({a}) / ({b} + hl.f32(0))" + + @staticmethod + def floordiv(a, b): + # TODO(jansel): find a better ways to do this, the select-based trick from triton.py didn't work + return ( + f"hl.floor(hl.cast(hl.Float(max(32, {a.name}.type().bits())), {a}) / {b})" + ) + + @classmethod + def sign(cls, x): + left = ops.to_dtype(ops.lt("0", x), torch.int8) + right = ops.to_dtype(ops.lt(x, "0"), torch.int8) + sub = ops.sub(left, right) + return f"hl.cast({x.name}.type(), {sub})" + + @staticmethod + # pyrefly: ignore [bad-override] + def trunc(x): + return f"hl.trunc({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def truncdiv(a, b): + # this causes crashes with floating point exception, see test_div_zero_dim_cpu + # return f"hl.div_round_to_zero({a}, {b})" + return ( + f"hl.trunc(hl.cast(hl.Float(max(32, {a.name}.type().bits())), {a}) / {b})" + ) + + @staticmethod + # pyrefly: ignore [bad-override] + def ceil(x): + return f"hl.ceil({x})" + + @staticmethod + def relu(x): + return f"hl.max({x}, 0)" + + @classmethod + def index_expr(cls, expr, dtype): + index = V.kernel.prepare_indexing(expr) + var = V.kernel.genfunc( + V.kernel.index_to_str(index), + V.kernel.used_dims_from_index(index), + bounds=get_bounds_index_expr(expr), + ) + if dtype not in (torch.int32, torch.int64): + return ops.to_dtype(var, dtype) + return var + + @classmethod + def indirect_indexing(cls, index_var, size, check=True, wrap_neg=True): + # TODO(jansel): Halide only supports 32-bit indexing, we should error on overflow + index_var = ops.to_dtype(index_var, torch.int32) + index_var = ops.halide_clamp(index_var, size, check) + index_var.indirect_indexing_size = size + return sympy_index_symbol(str(index_var)) + + @classmethod + def halide_clamp(cls, value, size, check): + end = V.kernel.kexpr(V.kernel.rename_indexing(size) - 1) + if not isinstance(size, (int, sympy.Integer)): + end = f"hl.cast({value.name}.type(), {end})" + # Skip unsafe_promise_clamped to workaround: https://github.com/halide/Halide/issues/8261#issuecomment-2148835692 + # return f"hl.unsafe_promise_clamped({value}, 0, {end})" + return f"hl.clamp({value}, 0, {end})" + + @staticmethod + def masked(mask, body, other): + with V.kernel.mask_loads(mask, other) as new_mask: + result = body() + + if result.bounds.is_bool: + other = bool(other) + + # Take dtype from result to prevent accidental promotion + other = V.kernel.genfunc( + f"hl.cast({result.name}.type(), {halide_constant(other)})", + [], + bounds=ValueRanges.wrap(other), + shape=result.shape, + ) + # TODO(jansel): look into removing the where in the same places triton does + return ops.where(new_mask, result, other) + + @staticmethod + # pyrefly: ignore [bad-override] + def frexp(x): + raise NotImplementedError("frexp") + + @staticmethod + def device_assert_async(cond, msg): + raise NotImplementedError("device_assert_async") + + @staticmethod + # pyrefly: ignore [bad-override] + def partial_accumulate( + name: str, + reduction_type: str, + value: CSEVariable, + extra_meta: dict[str, Any], + ) -> None: + raise NotImplementedError + + +HalideOverrides._initialize_pointwise_overrides("halide") + + +class HalideCSEVariable(CSEVariable): + undefined_re = re.compile(r"\b(tmp\d+)\[\?\]") + + def __init__( + self, + name, + bounds: ValueRanges[Any], + dtype: torch.dtype | None = None, + shape: BlockShapeType = None, + ) -> None: + super().__init__(name, bounds, dtype, shape=shape) + self.used_dims: list[sympy.Symbol] | None = None + + def update_on_args(self, name, args, kwargs): + used = OrderedSet(self.used_dims or ()) + for arg in itertools.chain(args, kwargs.values()): + if isinstance(arg, HalideCSEVariable): + assert arg.used_dims is not None, (name, arg, args) + used.update(arg.used_dims) + self.used_dims = V.kernel.sort_used_dims(used) + + def index_str(self, dims): + if len(dims) == 0: + return f"{self.name}[()]" + # Reversed since Halide is column major + return f"{self.name}[{', '.join(map(str, dims))}]" + + def __str__(self) -> str: + if self.used_dims is None: + # This will get recomputed and replaced in codegen_kernel() + return f"{self.name}[?]" + return self.index_str(self.used_dims) + + def subs_str(self, replacements): + assert self.used_dims is not None and all( + isinstance(x, sympy.Expr) for x in self.used_dims + ) + return self.index_str([replacements.get(n, n) for n in self.used_dims]) + + +@dataclasses.dataclass +class DimensionInfo: + expr: sympy.Expr | None + size: sympy.Expr + stride: sympy.Expr + + def __init__(self, expr, size, stride) -> None: + super().__init__() + if V.graph.sizevars.statically_known_lt(stride, 0): + stride = -stride + expr = -expr + self.expr = expr + self.size = size + self.stride = stride + + def index_str(self, replacements=None, zero_vars=False): + assert self.expr is not None + expr = self.expr + if zero_vars and expr == 0: + return "hl.Var()" + if replacements: + replacements = {**replacements} + # pyrefly: ignore [missing-attribute] + for sym in expr.free_symbols: + if symbol_is_type(sym, SymT.TMP): + assert isinstance(sym, sympy.Symbol) + var = V.kernel.lookup_cse_var(sym.name) + assert isinstance(var, HalideCSEVariable) + replacements[sym] = sympy_index_symbol(var.subs_str(replacements)) + expr = sympy_subs(expr, replacements) + return V.kernel.index_to_str(expr) + + +def eq(left, right): + return V.graph.sizevars.guard_or_false(sympy.Eq(left, right)) + + +def lt(left, right): + """Compare sizes: only use on inputs known to be >= 0.""" + if V.graph.sizevars.guard_or_false(sympy.Lt(left, right)): + return True + + # GCD fallback: if gcd(left, right) == left then left divides right, + # so left <= right. Combined with left != right this gives left < right. + # + # TODO: This is NOT always sound for unbacked symints. + # e.g. lt(u0, 10*u0): gcd=u0, gcd==left, u0 != 10*u0 → returns True, + # but if u0=0 then 0 < 0 is False. The >= 0 checks mitigate the + # negative case but the zero case remains. + # TODO shall we add a runtime assertion at least. + gcd = sympy.gcd(left, right) + if gcd == left: + return left != right + + return False + + +class HalideKernel(SIMDKernel): + overrides = HalideOverrides # type: ignore[assignment] + kexpr: Callable[[sympy.Expr], str] = texpr + + def __init__( + self, + tiling: dict[str, sympy.Expr], + **kwargs, + ) -> None: + super().__init__(tiling, **kwargs) + # For halide, we just write directly to the body + self.compute = self.body + self.loads = self.body + self.stores = self.body + self.indexing_code_dom = IndentedBuffer() + self.needs_dom_indexing = self.inside_reduction + self.has_reduction = self.inside_reduction + self.buffer_dimensions: dict[str, list[DimensionInfo]] = {} + self.buffer_offsets: dict[str, sympy.Expr] = {} + # {h0: size1, h1: size2, ...} + self.halide_vars: dict[sympy.Symbol, sympy.Expr] = {} + # {x0: h0, x1: h1+10*h2, ...} + self.index_replacements: dict[sympy.Expr, sympy.Expr] = {} + # {h1: hr1, ...} + self.reduction_renames: dict[sympy.Symbol, sympy.Symbol] = {} + # {"i": {h0: hi0}, "o": ...} + self.dom_renames: dict[str, dict[sympy.Symbol, sympy.Symbol]] = {} + # {"in_ptr0": ["in_ptr0_view0"], ...} + self.buffer_aliases: dict[str, list[str]] = defaultdict(list) + self.has_indirect_indexing = False + + def dtype_to_str(self, dtype: torch.dtype) -> str: + return halide_type(dtype) + + # pyrefly: ignore [bad-override] + def create_cse_var(self, name, bounds=None, dtype=None, shape=None): + self.body.writeline(f"{name} = hl.Func({name!r})") + # pyrefly: ignore [bad-argument-type] + return HalideCSEVariable(name, bounds, dtype, shape) + + def finalize_indexing(self, indices: Sequence[sympy.Expr]): + """ + Hook called right before codegen with every index that will be + used in the fused kernel. + + This populates self.halide_vars/index_replacements/reduction_renames which is an alternate indexing + scheme that avoids using divide and modulus. Instead of xindex/yindex/rindex + we base indexing on a larger number of vars whose product combines to those. + + This function populates self.halide_vars, self.index_replacements, and self.reduction_renames + """ + assert not ( + self.index_replacements or self.halide_vars or self.reduction_renames + ) + size_hint = functools.partial( + V.graph.sizevars.optimization_hint, fallback=sys.maxsize + ) + # pyrefly: ignore [bad-assignment] + indices = dict.fromkeys(map(super().prepare_indexing, indices)) + all_used_symbols = OrderedSet[Any]() + sym_to_node = { + n.symbol(): n + for n in itertools.chain.from_iterable( + [tree.nodes.values() for tree in self.range_trees] + ) + } + + def simplify(expr): + return sympy.simplify( + V.graph.sizevars.remove_precomputed_replacements(expr) + ) + + def visit_modular_indexing(base, divisor, modulus): + if base in sym_to_node: + node = sym_to_node[base] + all_used_symbols.add( + node.root.lookup( + node.divisor * divisor, + V.graph.sizevars.evaluate_min( + modulus, + FloorDiv(node.length, divisor), + ), + ).symbol() + ) + + def visit_floor_div(base, divisor): + if base in sym_to_node: + node = sym_to_node[base] + all_used_symbols.add( + node.root.lookup( + node.divisor * divisor, + FloorDiv(node.length, divisor), + ).symbol() + ) + + # first figure out all_used_symbols to do dead symbol elimination + for index in indices: + if index.has(ModularIndexing): + index.replace( + ModularIndexing( + sympy.Wild("base"), + sympy.Wild("divisor"), + sympy.Wild("modulus"), + ), + visit_modular_indexing, + ) + if index.has(FloorDiv): + index.replace( + FloorDiv( + sympy.Wild("base"), + sympy.Wild("divisor"), + ), + visit_floor_div, + ) + all_used_symbols.update(super().prepare_indexing(index).free_symbols) + + self.has_indirect_indexing = any( + symbol_is_type(sym, SymT.INDIRECT) for sym in all_used_symbols + ) + + had_fallback = False + for tree in reversed(self.range_trees): + nodes = [n for n in tree.nodes.values() if n.symbol() in all_used_symbols] + nodes.sort(key=lambda n: size_hint(n.divisor)) + if not nodes: + nodes.append(tree.lookup(1, tree.numel)) + handled_count = 0 + divisor = sympy.S.One + added_sym_size = [] + # decide on a minimal set of symbols and put them in self.halide_vars + while handled_count < len(nodes) and not eq(tree.numel, divisor): + sizes_to_add = [ + simplify(n.length) for n in nodes if eq(n.divisor, divisor) + ] + handled_count += len(sizes_to_add) + assert sizes_to_add, nodes + end = divisor * functools.reduce( + lambda a, b: V.graph.sizevars.evaluate_max(a, b), + sizes_to_add, + ) + sizes_to_add.extend( + [ + simplify(n.divisor / divisor) + for n in nodes + if lt(divisor, n.divisor) and lt(n.divisor, end) + ] + ) + # pyrefly: ignore [bad-assignment] + while sizes_to_add: + next_size = functools.reduce(sympy.gcd, sizes_to_add) + if eq(next_size, 1): + # sizes share no common factors, e.g [2, 21, 42, 441, 889056] + # TODO(jansel): we should just prevent fusion in cases that hit this + next_size = simplify(tree.numel / divisor) + assert not eq(next_size, 1) + sizes_to_add = [] + handled_count = len(nodes) + had_fallback = True + sym = sympy_index_symbol(f"h{len(self.halide_vars)}") + # pyrefly: ignore [missing-argument] + if tree.is_reduction: + self.reduction_renames[sym] = sympy_index_symbol( + f"hr{len(self.halide_vars)}" + ) + self.halide_vars[sym] = next_size + added_sym_size.append((sym, next_size)) + divisor *= next_size + new_sizes = [n.length for n in nodes if eq(n.divisor, divisor)] + handled_count += len(new_sizes) + prior_len = len(sizes_to_add) + sizes_to_add = [ + sympy.simplify(s / next_size) + for s in sizes_to_add + if not eq(s, next_size) + ] + assert len(sizes_to_add) < prior_len or prior_len == 0 + sizes_to_add.extend(new_sizes) + + # create a mapping to the new set of symbols in self.index_replacements + for node in nodes: + try: + idx = 0 + divisor = 1 + while not eq(node.divisor, divisor): + sym, size = added_sym_size[idx] + idx += 1 + divisor *= size + length = 1 + expr = sympy.S.Zero + while not eq(node.length, length): + sym, size = added_sym_size[idx] + idx += 1 + expr += length * sym + length *= size + self.index_replacements[node.symbol()] = expr + except IndexError: + assert had_fallback + full_index = sympy.S.Zero + stride = sympy.S.One + for sym, size in added_sym_size: + full_index += stride * sym + stride *= size + self.index_replacements[node.symbol()] = ( + V.graph.sizevars.simplify_with_ranges( + ModularIndexing(full_index, node.divisor, node.length), + self.halide_vars, # type: ignore[arg-type] + ) + ) + + # codegen the variable definitions + for sym in self.halide_vars: + self.indexing_code.writeline(f"{sym} = hl.Var({sym.name!r})") + if self.reduction_renames: + self.codegen_rdom( + "rdom", + {rv: self.halide_vars[v] for v, rv in self.reduction_renames.items()}, + ) + + def setup_dom_indexing(self): + """RDom based indexing uses explicit iteration ranges for Func updates""" + prefix = "i" if self.inside_reduction else "o" + if prefix in self.dom_renames: + return self.dom_renames[prefix] + + renames = {} + for var in self.halide_vars: + if not self.inside_reduction and var in self.reduction_renames: + continue + m = re.match(r"^h(\d+)$", var.name) + assert m + renames[var] = sympy_index_symbol(f"h{prefix}{m.group(1)}") + + self.codegen_rdom( + f"{prefix}dom", {rv: self.halide_vars[v] for v, rv in renames.items()} + ) + + self.dom_renames[prefix] = renames + return renames + + def codegen_rdom(self, name, vars): + rsizes = [ + f"hl.Range(0, {self.kexpr(self.rename_indexing(size))})" + for size in vars.values() + ] + self.indexing_code.writeline(f"{name} = hl.RDom([{', '.join(rsizes)}])") + for i, rsym in enumerate(vars.keys()): + self.indexing_code.writeline(f"{rsym} = {name}[{i}]") + + def prepare_indexing( + self, + index: sympy.Expr, + ): + index = super().prepare_indexing(index) + index = sympy_subs(index, self.index_replacements) + return V.graph.sizevars.simplify_with_ranges(index, self.halide_vars) # type: ignore[arg-type] + + def sym_size(self, sym): + """The size of an index symbol""" + if symbol_is_type(sym, SymT.TMP): + return self.lookup_cse_var(sym.name).indirect_indexing_size + return self.halide_vars[sym] + + def indexing_to_dimensions(self, var: str, index: sympy.Expr, is_store: bool): + """Convert address-based indexing into dimensions using self.halide_vars""" + symbols = [] + for sym in sorted(index.free_symbols, key=lambda x: x.name): # type: ignore[attr-defined] + if symbol_is_type(sym, (SymT.HALIDE, SymT.TMP)): + symbols.append(sym) + else: + assert symbol_is_type( + sym, + ( + SymT.UNBACKED_INT, + SymT.SIZE, + SymT.PRECOMPUTED_SIZE, + ), + ), sym + + # group the expression by variables used + offset = sympy.S.Zero + split_expr = dict.fromkeys(symbols, sympy.S.Zero) + split_failed: list[tuple[list[sympy.Symbol], sympy.Expr]] = [] + index = sympy.expand(self.rename_indexing(index)) + for part in index.args if isinstance(index, sympy.Add) else [index]: + part_vars = [v for v in part.free_symbols if v in split_expr] + if len(part_vars) == 0: + offset += part + elif len(part_vars) == 1: + split_expr[part_vars[0]] += part + else: + new_split_failed = [] + for i in range(len(split_failed)): + assert split_failed[i] is not None + other_vars, other_part = split_failed[i] + if OrderedSet(other_vars) & OrderedSet(part_vars): + part_vars.extend([v for v in other_vars if v not in part_vars]) + part += other_part + else: + new_split_failed.append((other_vars, other_part)) + split_failed = [*new_split_failed, (part_vars, part)] + + def expr_to_dimension(expr, syms): + expr = sympy.factor(expr) + if len(syms) == 1: + stride_wild = sympy.Wild("wild", exclude=symbols) + m = expr.match(stride_wild * syms[0]) + if m: + return DimensionInfo( + syms[0], self.sym_size(syms[0]), m[stride_wild] + ) + assert not is_store, expr + length = sympy.simplify( + sympy_subs(expr, {sym: self.sym_size(sym) - 1 for sym in syms}) + 1 + ) + stride = sympy.S.One + if isinstance(expr, sympy.Mul): + for term in expr.args: + if isinstance(term, sympy.Integer): + stride *= term + expr = sympy.simplify(expr / term) + length = sympy.simplify(sympy.ceiling(length / term)) + return DimensionInfo(expr, length, stride) + + # try to turn each group into a strided access + dims = [] + for syms, expr in split_failed: + for v in syms: + expr += split_expr.pop(v) + dims.append(expr_to_dimension(expr, syms)) + for sym, expr in split_expr.items(): + dims.append(expr_to_dimension(expr, [sym])) + dims.sort( + key=lambda d: V.graph.sizevars.optimization_hint( + d.stride, fallback=sys.maxsize + ) + ) # type: ignore[arg-type] + + if not dims: # scalar load/store + if self.has_indirect_indexing: + # workaround https://github.com/halide/Halide/issues/8338 + dims.append(DimensionInfo(sympy.S.Zero, 1, 1)) + elif not V.graph.sizevars.statically_known_equals(dims[0].stride, 1): + # Halide assumes dimension 0 is stride == 1, so add a dummy dimension + dims.insert( + 0, DimensionInfo(sympy.S.Zero, 1 if is_store else dims[0].stride, 1) + ) + + if dims and not is_store: + if var in self.buffer_offsets and V.graph.sizevars.statically_known_geq( + offset, self.buffer_offsets[var] + ): + # reuse the existing offset to avoid needing an input alias + self.apply_offset_to_dimension(dims, offset - self.buffer_offsets[var]) + offset = self.buffer_offsets[var] + elif V.graph.sizevars.statically_known_gt( + offset, 0 + ): # TODO(jansel): negative offsets + # roll the offset into the dimensions for cleaner indexing + self.apply_offset_to_dimension(dims, offset) + offset = 0 + + orig_var = var + for i in itertools.count(): + if self.install_dims(var, dims, offset, is_store): + return var, dims + assert not is_store + var = f"{orig_var}_view{i}" + if var not in self.buffer_aliases[orig_var]: + self.buffer_aliases[orig_var].append(var) + + def install_dims(self, var, dims, offset, is_store): + """Try to set self.buffer_dimensions[var], return True on success""" + if var not in self.buffer_dimensions: + self.buffer_dimensions[var] = dims + self.buffer_offsets[var] = offset + return True + if self.buffer_offsets[var] != offset or len( + self.buffer_dimensions[var] + ) != len(dims): + return False + if is_store: + return self.buffer_dimensions[var] == dims + for old, new in zip(self.buffer_dimensions[var], dims): + if old.stride != new.stride: + return False + if old.size != new.size or old.expr != new.expr: + old.size = V.graph.sizevars.evaluate_max(old.size, new.size) + old.expr = None + return True + + def apply_offset_to_dimension(self, dims, offset): + if offset == 0: + return + for i in reversed(range(len(dims))): + if dims[i].stride == 1 or V.graph.sizevars.statically_known_geq( + offset, dims[i].stride + ): + part = FloorDiv(offset, dims[i].stride) + offset -= part * dims[i].stride + dims[i].expr += part + assert offset == 0 + + def used_dims_from_index(self, index: sympy.Expr): + """Detect which range trees are used to populate HalideCSEVariable.used_dims""" + used_dims = OrderedSet[sympy.Symbol]() + for sym in index.free_symbols: + assert isinstance(sym, sympy.Symbol) + if symbol_is_type(sym, SymT.TMP): + # indirect indexing + cse_var = self.lookup_cse_var(sym.name) + assert ( + isinstance(cse_var, HalideCSEVariable) + and cse_var.used_dims is not None + ) + used_dims.update(cse_var.used_dims) + elif symbol_is_type(sym, SymT.HALIDE): + used_dims.add(sym) + elif symbol_is_type( + sym, (SymT.UNBACKED_INT, SymT.SIZE, SymT.PRECOMPUTED_SIZE, SymT.INDEX) + ): + pass + else: + raise NotImplementedError(f"unhandled symbol {sym}") + return self.sort_used_dims(used_dims) + + def sort_used_dims(self, used_dims): + assert all(isinstance(x, sympy.Expr) for x in used_dims) + ordered = [ + sym + for sym in itertools.chain( + self.halide_vars, self.reduction_renames.values() + ) + if sym in used_dims + ] + assert len(ordered) == len(used_dims) + return ordered + + def make_index_str(self, dims, replacements=None, zero_vars=False): + index_str = ", ".join(d.index_str(replacements, zero_vars) for d in dims) + if len(dims) == 0: + index_str = "()" + elif len(dims) == 1: + # workaround for https://github.com/halide/Halide/issues/8299 + index_str = f"{index_str}," + return index_str + + def load(self, name: str, index: sympy.Expr): + """Codegen a load from an InputBuffer""" + var = self.args.input(name) + index = self.prepare_indexing(index) + var, dims = self.indexing_to_dimensions(var, index, False) + line = f"{var}[{self.make_index_str(dims)}]" + dtype = V.graph.get_dtype(name) + if dtype in (torch.float16, torch.bfloat16): + dtype = torch.float32 + line = f"hl.cast(hl.Float(32), {line})" + + if self._load_mask: + assert ( + isinstance(self._load_mask, HalideCSEVariable) + and self._load_mask.used_dims is not None + ) + used_dims = OrderedSet( + (*self.used_dims_from_index(index), *self._load_mask.used_dims) + ) + result = self.newfunc(self.sort_used_dims(used_dims)) + if result.used_dims: + self.body.writeline(f"{result.name}_mask = hl.RDom([hl.Range(0, 1)])") + self.body.writeline(f"{result.name}_mask.where({self._load_mask})") + other = self.kexpr(self._load_other or 0) # type: ignore[arg-type] + self.body.writeline( + f"{result} = hl.cast({halide_type(dtype)}, {other})" + ) + self.body.writeline( + f"{result} = {line} + hl.cast({halide_type(dtype)}, {result.name}_mask)" + ) + else: + # scalar case + self.body.writeline( + f"{result} = hl.select({self._load_mask}, {line}, hl.cast({halide_type(dtype)}, 0))" + ) + return result + else: + return self.genfunc(line, self.used_dims_from_index(index)) + + def lookup_cse_var(self, name: str): + return self.cse.varname_map[re.sub(r"\[.*", "", name)] + + def store( + self, name: str, index: sympy.Expr, value: CSEVariable, mode: StoreMode = None + ) -> None: + """Codegen a store to an OutputBuffer""" + assert isinstance(value, HalideCSEVariable) + var = self.args.output(name) + index = self.prepare_indexing(index) + var, dims = self.indexing_to_dimensions(var, index, True) + if self.is_indirect_indexing(index) or mode is not None: + replacements = self.setup_dom_indexing() + index_str = self.make_index_str(dims, replacements) + value_str = value.subs_str(replacements) + undef_dims = (", ".join(["hl.Var()"] * len(dims))) or "()" + self.body.writeline( + DeferredLine(name, f"{var}[{undef_dims}] = hl.undef({var}.type())") + ) + else: + index_str = self.make_index_str(dims, zero_vars=True) + value_str = str(value) + + dtype = V.graph.get_dtype(name) + if mode is None: + line = f"{var}[{index_str}] = hl.cast({halide_type(dtype)}, {value_str})" + elif mode == "atomic_add": + line = f"{var}[{index_str}] += hl.cast({halide_type(dtype)}, {value_str})" + else: + raise NotImplementedError(f"store mode={mode}") + self.body.writeline(DeferredLine(name, line)) + + def reduction( + self, + dtype: torch.dtype, + src_dtype: torch.dtype, + reduction_type: ReductionType, + value: CSEVariable | tuple[CSEVariable, ...], + ) -> CSEVariable | tuple[CSEVariable, ...]: + """Codegen a reduction operation""" + assert self.inside_reduction + assert not self._load_mask + cache_key = (src_dtype, reduction_type, value) + if cache_key in self.cse.reduction_cache: + return self.cse.reduction_cache[cache_key] + + if isinstance(value, tuple): + assert reduction_type == "welford_combine" + self.cse.reduction_cache[cache_key] = result_tuple = ( + self.welford_combine_impl(*value) + ) + return result_tuple + + assert isinstance(value, HalideCSEVariable) and value.used_dims is not None + reduction_vars = OrderedSet(self.reduction_renames) + result_var = self.newfunc( + [v for v in value.used_dims if v not in reduction_vars], + ) + if reduction_vars - OrderedSet(value.used_dims): + value = self.genfunc( + f"{value}", + self.sort_used_dims(OrderedSet((*value.used_dims, *reduction_vars))), + shape=value.shape, + ) + value_str = value.subs_str(self.reduction_renames) + default = ir.Reduction.default_accumulator(reduction_type, src_dtype) + acc_type = halide_acc_type(dtype) + + if reduction_type in ("argmax", "argmin"): + index = f"{result_var.name}_{reduction_type}" + self.body.writeline(f"{index} = hl.{reduction_type}(rdom, {value_str})") + # turn the N-D argmax index into a 1-D one + parts = [] + stride = 1 + for i, sym in enumerate(self.reduction_renames): + # pyrefly: ignore [bad-argument-type] + parts.append(f"{index}[{i}]") + if stride != 1: + # pyrefly: ignore [unsupported-operation] + parts[-1] += f"*{stride}" + stride *= self.halide_vars[sym] + self.body.writeline(f"{result_var} = {' + '.join(parts)}") + elif reduction_type == "welford_reduce": + # TODO(jansel): implement welford_reduce without fallback + result_var = self.welford_reduce_fallback(dtype, value) + else: + combine_fn = get_reduction_combine_fn(reduction_type, acc_type) + with V.set_ops_handler(AddParenHandler(HalideOverrides())): + combine_str = combine_fn(result_var, value_str) # type: ignore[arg-type] + default_str = f"hl.cast({acc_type}, {halide_constant(default)})" + self.body.writeline(f"{result_var} = {default_str}") + self.body.writeline(f"{result_var} = {combine_str}") + + self.cse.reduction_cache[cache_key] = result_var + return result_var + + def welford_combine_impl(self, mean, m2, weight): + assert isinstance(mean, HalideCSEVariable) and mean.used_dims is not None + assert isinstance(m2, HalideCSEVariable) and m2.used_dims is not None + assert isinstance(weight, HalideCSEVariable) and weight.used_dims is not None + used_dims = OrderedSet( + (*mean.used_dims, *m2.used_dims, *weight.used_dims) or self.halide_vars + ) + used_dims -= OrderedSet(self.reduction_renames) + result_var = self.newfunc(self.sort_used_dims(used_dims)) + default = [f"hl.cast({x.name}.type(), 0)" for x in (mean, m2, weight)] + pfx = result_var.name + self.body.writeline(f"{result_var} = hl.Tuple([{', '.join(default)}])") + self.body.writeline(f"{pfx}_mean_1 = {result_var}[0]") + self.body.writeline(f"{pfx}_m2_1 = {result_var}[1]") + self.body.writeline(f"{pfx}_weight_1 = {result_var}[2]") + self.body.writeline(f"{pfx}_mean_2 = {mean.subs_str(self.reduction_renames)}") + self.body.writeline(f"{pfx}_m2_2 = {m2.subs_str(self.reduction_renames)}") + self.body.writeline( + f"{pfx}_weight_2 = {weight.subs_str(self.reduction_renames)}" + ) + self.body.writeline(f"{pfx}_delta = {pfx}_mean_2 - {pfx}_mean_1") + self.body.writeline(f"{pfx}_new_weight = {pfx}_weight_1 + {pfx}_weight_2") + self.body.writeline( + f"{pfx}_w2_over_w = hl.select({pfx}_new_weight == 0.0, 0.0, {pfx}_weight_2 / {pfx}_new_weight)" + ) + update = [ + f"{pfx}_mean_1 + {pfx}_delta * {pfx}_w2_over_w", + f"{pfx}_m2_1 + {pfx}_m2_2 + {pfx}_delta * {pfx}_delta * {pfx}_weight_1 * {pfx}_w2_over_w", + f"{pfx}_new_weight", + ] + self.body.writeline(f"{result_var} = hl.Tuple([{', '.join(update)}])") + + unpacked = [] + for i in range(3): + unpacked.append(self.newfunc(result_var.used_dims)) + self.body.writeline(f"{unpacked[-1]} = {result_var}[{i}]") + return tuple(unpacked) + + def scan( + self, + dtypes: tuple[torch.dtype, ...], + combine_fn: Callable[ + [tuple[CSEVariable, ...], tuple[CSEVariable, ...]], tuple[CSEVariable, ...] + ], + values_orig: tuple[CSEVariable, ...], + ) -> tuple[CSEVariable, ...]: + assert self.inside_reduction + assert len(dtypes) == len(values_orig) + values: list[HalideCSEVariable] = [] + all_used_dims = OrderedSet[sympy.Symbol]() + + for value in values_orig: + assert isinstance(value, HalideCSEVariable) and value.used_dims is not None + if OrderedSet(value.used_dims) & OrderedSet(self.reduction_renames): + values.append(value) + else: + values.append( + self.genfunc( + f"{value}", + [*value.used_dims, [*self.reduction_renames][:1]], + shape=value.shape, + ) + ) + all_used_dims.update(value.used_dims) + result_var = self.newfunc(self.sort_used_dims(all_used_dims)) + assert result_var.used_dims and OrderedSet(result_var.used_dims) & OrderedSet( + self.reduction_renames + ) + initial = [ + f"hl.cast({halide_acc_type(dtype)}, {value})" + for dtype, value in zip(dtypes, values) + ] + + length = self.kexpr(self.rename_indexing(self.range_trees[-1].numel)) + scan_dom = f"{result_var.name}_rdom" + scan = f"{scan_dom}.x" + self.body.writeline(f"{scan_dom} = hl.RDom([hl.Range(1, {length})])") + + assert len(self.reduction_renames) == 1, ( + "multi-dimensional scan not implemented" + ) + (scan_var,) = [*self.reduction_renames] # type: ignore[misc] + scan_renames_cur = {scan_var: sympy_index_symbol(scan)} + scan_renames_pri = {scan_var: sympy_index_symbol(scan) - 1} + + if len(values) == 1: + + def maybe_tuple(x): + return x[0] + + read_left = [result_var.subs_str(scan_renames_pri)] + read_right = [result_var.subs_str(scan_renames_cur)] + else: + + def maybe_tuple(x): + return f"hl.Tuple([{', '.join(x)}])" + + read_left = [ + result_var.subs_str(scan_renames_pri) + f"[{i}]" + for i in range(len(values)) + ] + read_right = [ + result_var.subs_str(scan_renames_cur) + f"[{i}]" + for i in range(len(values)) + ] + + self.body.writeline(f"{result_var} = {maybe_tuple(initial)}") + + # Disable CSE for update fn + with V.set_ops_handler(AddParenHandler(HalideOverrides())): + combine_str = combine_fn(read_left, read_right) # type: ignore[arg-type] + self.body.writeline( + f"{result_var.subs_str(scan_renames_cur)} = {maybe_tuple(combine_str)}" + ) + + if len(values) == 1: + return (result_var,) + + unpack_vars = [self.newfunc(self.sort_used_dims(all_used_dims)) for _ in values] + for i, v in enumerate(unpack_vars): + self.body.writeline(f"{v} = {result_var}[{i}]") + return tuple(unpack_vars) + + def genfunc( + self, + line, + used_dims, + *, + bounds=ValueRanges.unknown(), + shape: BlockShapeType = None, + ) -> HalideCSEVariable: + var = self.cse.generate(self.body, line, bounds=bounds, shape=shape) + assert isinstance(var, HalideCSEVariable) + var.used_dims = used_dims + return var + + def newfunc(self, used_dims, *, shape: BlockShapeType = None) -> HalideCSEVariable: + var = self.cse.newvar(shape=shape) + assert isinstance(var, HalideCSEVariable) + var.used_dims = used_dims + return var + + def halide_buffer_numel(self, name: str): + """ + We map all tensors to 1D buffers in Halide since Halide has trouble representing some strides that PyTorch + supports. If there are gaps in the underlying layout the numel we pass to Halide includes the gaps while + PyTorch's numel excludes them. + """ + return V.graph.get_buffer(name).get_layout().storage_size() + + def halide_argdefs(self): + """ + Halide requires scalar inputs before outputs, so need to reorder args. + """ + + def arg_order(arg_tuple): + _call_str, arg = arg_tuple + if isinstance(arg, SizeArg): + return 1 # this would normally be at the end, move it to middle + elif "out_ptr" in arg.name: + return 2 + else: + assert "in_ptr" in arg.name + return 0 + + result: list[tuple[str | None, KernelArgType]] = [] + _, a, b, _ = self.args.python_argdefs() + for call_str, arg in sorted(zip(a, b), key=arg_order): + result.append((call_str, arg)) + if isinstance(arg, TensorArg): + assert arg.offset == 0 and arg.alias_of is None + result.extend( + ( + None, + TensorArg( + alias, + arg.buffer, + arg.dtype, + arg.offset, + alias_of=arg.name, + ), + ) + for alias in self.buffer_aliases.get(arg.name, ()) + ) + return result + + def halide_kernel_meta(self) -> HalideMeta: + """Compute metadata required by codecache.py""" + argtypes = [] + for _, arg in self.halide_argdefs(): + if isinstance(arg, SizeArg): + shape = None + stride = None + offset = None + dtype = "long" + else: + shape = [ + cexpr(self.rename_indexing(x.size)) + for x in self.buffer_dimensions[arg.name] + ] + stride = [ + cexpr(self.rename_indexing(x.stride)) + for x in self.buffer_dimensions[arg.name] + ] + assert len(shape) == len(stride) + offset = cexpr(self.buffer_offsets[arg.name]) + dtype = f"{DTYPE_TO_CPP[arg.dtype]}*" + argtypes.append( + HalideInputSpec( + dtype, + arg.name, + shape=shape, + stride=stride, + offset=offset, + alias_of=arg.alias_of, + ) + ) + + current_device = V.graph.get_current_device_or_throw() + if current_device.type == "cpu": + target = [config.halide.cpu_target] + scheduler = config.halide.scheduler_cpu + scheduler_flags = { + "parallelism": parallel_num_threads(), + } + cuda_device = None + else: + assert current_device.type == "cuda", "only cpu/cuda supported" + assert current_device.index <= 0, "only default device supported" + target = [config.halide.gpu_target] + scheduler = config.halide.scheduler_cuda + capability = torch.cuda.get_device_properties(current_device) + if "cuda_capability" not in target[0]: + for major, minor in [(8, 6), (8, 0), (7, 5), (7, 0), (6, 1)]: + if capability.major >= major and capability.minor >= minor: + target.append(f"cuda_capability_{major}{minor}") + break + target.append("user_context") + scheduler_flags = { + "parallelism": capability.multi_processor_count, + # TODO(jansel): explore other flags, see: + # grep parser.parse ~/Halide/src/autoschedulers/anderson2021/AutoSchedule.cpp + } + cuda_device = max(0, current_device.index) + + # strict_float is requires for correctness + target.append("strict_float") + + # without this we will initialize cuda once per kernel and hit errors + target.append("no_runtime") + + if not config.halide.asserts: + target.append("no_asserts") + + if config.halide.debug: + target.append("debug") + + if "64" in self.index_dtype: + # TODO(jansel): it is unclear if this does anything, since input sizes are still int32 + target.append("large_buffers") + + return HalideMeta( + argtypes, + target="-".join(target), + scheduler=scheduler, + scheduler_flags=scheduler_flags, # type: ignore[arg-type] + cuda_device=cuda_device, + ) + + def codegen_kernel(self, name=None): + """Called at the end to generate a final kernel string""" + if self.args.inplace_buffers: + raise Unsupported("inplace_buffers") + meta = self.halide_kernel_meta() # ensure needed args are added early + code = IndentedBuffer() + code.splice( + """ + import halide as hl + from torch._inductor.runtime import halide_helpers + from math import inf, nan + + @hl.generator(name="kernel") + class Kernel: + """, + strip=True, + ) + code.do_indent() + for _, arg in self.halide_argdefs(): + if isinstance(arg, SizeArg): + code.writeline(f"{arg.name} = hl.InputScalar({self.index_dtype})") + else: + assert arg.buffer, arg + argcls = "hl.OutputBuffer" if "out" in arg.name else "hl.InputBuffer" + argtype = halide_type(arg.dtype) + ndim = len(self.buffer_dimensions[arg.name]) + code.writeline(f"{arg.name} = {argcls}({argtype}, {ndim})") + code.splice( + """ + def generate(g): + """ + ) + code.do_indent() + for _, arg in self.halide_argdefs(): + code.writeline(f"{arg.name} = g.{arg.name}") + for old, new in self.args.aliases(): + code.writeline(f"{old} = {new}") + code.splice(self.indexing_code) + + def update_index(m): + var = cast(HalideCSEVariable, self.cse.varname_map[m.group(1)]) + assert var.used_dims is not None, var + return str(var) + + for line in self.body._lines: + if isinstance(line, str): + # fill in missing indices + line = HalideCSEVariable.undefined_re.sub(update_index, line) + code.writeline(line) + code.writeline("") + code.writeline("assert g.using_autoscheduler()") + + for _, arg in self.halide_argdefs(): + # fallback=1 below because halide requires buffers to be at least as large as the estimates + # This causes crashes if our estimate is greater than the vector length + # https://github.com/halide/Halide/issues/3103 + if isinstance(arg, SizeArg): + hint = V.graph.sizevars.optimization_hint(arg.expr, fallback=1) + code.writeline(f"{arg.name}.set_estimate({hint})") + else: + dims = self.buffer_dimensions[arg.name] + range_hints = [] + for i, dim in enumerate(dims): + hint = self._autoscheduler_workarounds( + V.graph.sizevars.optimization_hint(dim.size, fallback=1), dims + ) + # pyrefly: ignore [bad-argument-type] + range_hints.append(f"hl.Range(0, {hint})") + if "out" not in arg.name: + code.writeline(f"{arg.name}.dim({i}).set_min(0)") + try: + code.writeline( + f"{arg.name}.dim({i}).set_stride({int(dim.stride)})" + ) + except TypeError: + pass # not integer + try: + code.writeline( + f"{arg.name}.dim({i}).set_extent({int(dim.size)})" + ) + except TypeError: + pass # not integer + code.writeline(f"{arg.name}.set_estimates([{', '.join(range_hints)}])") + + code.do_unindent(2) + code.splice( + """ + if __name__ == "__main__": + hl.main() + """.rstrip(), + ) + if meta.scheduler: + code.splice( + f""" + else: + hl.load_plugin({HalideCodeCache.find_libautoschedule(meta.scheduler)!r}) + target = hl.Target({meta.target!r}) + autoscheduler = hl.AutoschedulerParams({meta.scheduler!r}, {meta.scheduler_flags!r}) + with hl.GeneratorContext(target, autoscheduler): + gen = Kernel() + pipeline = gen._build_pipeline() + # gen.compile_to_callable() does not run the autoscheduler + pipeline.apply_autoscheduler(target, autoscheduler) + kernel = pipeline.compile_to_callable([ + gen._get_input_parameter(a.name)._to_argument() + for a in gen._get_arginfos() + if a.dir == hl.ArgInfoDirection.Input + ], target) + """, + strip=True, + ) + else: + code.splice( + f""" + else: + with hl.GeneratorContext(hl.Target({meta.target!r})): + kernel = Kernel().compile_to_callable() + """, + strip=True, + ) + return code.getvalue() + + @staticmethod + def _autoscheduler_workarounds(n, dims): + if ( + len(dims) == 1 + and config.halide.scheduler_cuda == "Anderson2021" + and V.graph.get_current_device_or_throw().type == "cuda" + ): + # workaround https://github.com/halide/Halide/issues/8246 + n = max(2, n) + return n + + def call_kernel(self, name: str, node=None, deallocate_ws: bool = True): + """Codegen a call to this kernel""" + wrapper = V.graph.wrapper_code + call_args = [f"{n}" for n, arg in self.halide_argdefs() if arg.alias_of is None] + current_device = V.graph.get_current_device_or_throw() + if current_device.type == "cuda": + stream_name = wrapper.write_get_raw_stream( + current_device.index, V.graph.name + ) + call_args.append(stream_name) + wrapper.generate_kernel_call( + name, + call_args, + device=current_device, + triton=False, + ) + + def generate_assert(self, check): + return False # TODO(jansel): support asserts + + def check_bounds( + self, expr: sympy.Expr, size: sympy.Expr, lower: bool, upper: bool + ): + pass # TODO(jansel): support asserts + + +class HalideScheduling(SIMDScheduling): + kernel_type = HalideKernel # type: ignore[arg-type,assignment] + + @classmethod + def get_backend_features(cls, device: torch.device) -> OrderedSet[BackendFeature]: + result = OrderedSet( + [ + BackendFeature.TUPLE_REDUCTION, + BackendFeature.PREFER_STORE_LOOP_ORDER, + BackendFeature.REDUCE_TO_SINGLE_ELEMENT, + ] + ) + if config.halide.scan_kernels: + result.add(BackendFeature.SCAN) + return result + + def define_kernel(self, src_code, node_schedule, kernel): + """Codegen kernel definition to go in output wrapper code""" + wrapper = V.graph.wrapper_code + if src_code in wrapper.src_to_kernel: + kernel_name = wrapper.src_to_kernel[src_code] + else: + kernel_name = f"halide_kernel_{wrapper.next_kernel_suffix()}" + wrapper.src_to_kernel[src_code] = kernel_name + wrapper.add_import_once( + "from torch._inductor.runtime.hints import HalideMeta, HalideInputSpec" + ) + + compile_wrapper = IndentedBuffer() + compile_wrapper.writeline( + f"async_compile.halide({kernel.halide_kernel_meta()!r}, '''" + ) + compile_wrapper.splice(src_code, strip=True) + compile_wrapper.writeline("''')") + + origins, detailed_origins = get_kernel_metadata(node_schedule, wrapper) + metadata_comment = f"{origins}\n{detailed_origins}" + wrapper.define_kernel( + kernel_name, compile_wrapper.getvalue(), metadata_comment + ) + if is_metric_table_enabled("kernel_metadata"): + log_kernel_metadata(kernel_name, "", src_code) + + return kernel_name diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/memory_planning.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/memory_planning.py new file mode 100644 index 0000000000000000000000000000000000000000..4f2f362b5b2b5bc6e4bc1ff607fbe9b5207aadfb --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/memory_planning.py @@ -0,0 +1,816 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import collections +import dataclasses +import itertools +import pprint +from typing import Any, Protocol, TYPE_CHECKING + +import sympy + +import torch +from torch.fx.experimental.symbolic_shapes import free_unbacked_symbols +from torch.utils._ordered_set import OrderedSet + +from .. import config +from ..utils import _align, align, cache_on_self, CachedMethod, IndentedBuffer +from ..virtualized import V +from .wrapper import ( + AllocateLine, + BufferLike, + FreeIfNotReusedLine, + MemoryPlanningLine, + NullLine, + ReuseLine, +) + + +if TYPE_CHECKING: + from collections.abc import Iterable + + +@dataclasses.dataclass +class LiveRange: + """ + A range where a given tensor is live. Begin and end are both counters + representing points in the program of grouped memory operations. + Begin is inclusive, end is exclusive. + + Invariant: begin <= end + """ + + begin: float # int | +/-inf + end: float # int | +/-inf + + def contains(self, other: LiveRange): + """Is other entirely within self""" + return self.begin <= other.begin and other.end <= self.end + + def join(self, other: LiveRange): + """Combine two ranges using a union operation""" + return LiveRange(min(self.begin, other.begin), max(self.end, other.end)) + + def __len__(self): + return self.end - self.begin + + +class LiveRanges: + """ + A collection of LiveRange regions, allowing for non-contiguous + live regions. + + Invariant: LiveRanges.ranges is in sorted order and non-overlapping + """ + + def __init__(self, ranges: Iterable[LiveRange]): + ranges = [*sorted(ranges, key=lambda x: x.begin)] + self.ranges = ranges[:1] + for r in ranges[1:]: + assert self.ranges[-1].begin <= r.begin + if self.ranges[-1].end >= r.begin: + self.ranges[-1] = LiveRange.join(self.ranges[-1], r) + else: + self.ranges.append(r) + + def overlaps(self, other: LiveRanges): + """Check if any pair of ranges in self and other overlap""" + left = collections.deque(self.ranges) + right = collections.deque(other.ranges) + while left and right: + if left[0].begin > right[0].begin: + left, right = right, left + assert left[0].begin <= right[0].begin + if left[0].end > right[0].begin: + return True + left.popleft() + return False + + @property + def begin(self): + return self.ranges[0].begin + + @property + def end(self): + return self.ranges[-1].end + + def __repr__(self): + return f"{self.__class__.__name__}([{', '.join(map(repr, self.ranges))}])" + + +class AllocationTreeNode: + """ + Abstract base class for nodes in allocation pool. + """ + + def allocate(self, block: Allocation, is_last: bool) -> bool: + """ + Try to assign block to a memory location in this bool. Return True if + an assignment was made. + """ + return False + + def get_live_ranges(self) -> LiveRanges: + """Aggregate LiveRanges for all objects below this in tree""" + raise NotImplementedError + + def get_size_hint(self) -> int: + """Number of bytes used for example inputs""" + raise NotImplementedError + + def get_symbolic_size(self) -> sympy.Expr: + """Number of bytes needed at runtime""" + raise NotImplementedError + + def finalize(self, pool, offset) -> AllocationTreeNode: + """Called after all allocations have been made""" + return self + + def is_empty(self): + return False + + +@dataclasses.dataclass +class Allocation(AllocationTreeNode): + """ + Represents memory allocated to a given node in the allocation pool. + """ + + node: BufferLike + live_range: LiveRange + size_hint: int + symbolic_size: sympy.Expr + allocated: bool = False + pool: AllocationPool | None = None + offset: sympy.Expr | None = None + earliest_available: float | None = None + + def __post_init__(self) -> None: + has_unbacked_sym = False + for s in self.node.get_layout().size: + if free_unbacked_symbols(s): + has_unbacked_sym = True + break + + if has_unbacked_sym: + self.earliest_available = self.get_live_ranges().begin + + @property + def device(self): + return self.node.get_device() + + def get_live_ranges(self): + return LiveRanges([self.live_range]) + + def get_size_hint(self): + return self.size_hint + + def get_symbolic_size(self): + return self.symbolic_size + + def mark_allocated(self): + assert not self.allocated + self.allocated = True + + def finalize(self, pool, offset): + assert self.pool is None and self.offset is None + self.pool = pool + self.offset = offset + return self + + def codegen_alloc_from_pool(self, wrapper): + assert self.pool + node = self.node + shape = tuple(node.get_size()) + stride = tuple(node.get_stride()) + return wrapper.codegen_alloc_from_pool( + self.pool.name, self.offset, node.get_dtype(), shape, stride + ) + + def __repr__(self): + return ( + f"{self.__class__.__name__}(" + f"node={self.node.get_name()}, " + f"live_range={self.live_range}, " + f"size_hint={self.size_hint}, " + f"symbolic_size={self.symbolic_size}, " + f"pool={self.pool.name if self.pool else None}, " + f"offset={self.offset})" + ) + + def get_earliest_available(self): + return self.earliest_available + + +@dataclasses.dataclass +class Empty(AllocationTreeNode): + """ + Placeholder to represent empty space in the allocation pool. + Only exists to get the size_hint correct in parent nodes. + """ + + size_hint: int + + def get_live_ranges(self): + return LiveRanges([]) + + def get_size_hint(self): + return self.size_hint + + def get_symbolic_size(self): + return 0 + + def is_empty(self): + return True + + +class MemorySplitProtocol(Protocol): + get_live_ranges: CachedMethod[[], LiveRanges] + get_size_hint: CachedMethod[[], int] + get_symbolic_size: CachedMethod[[], sympy.Expr] + + def _allocate(self, block: Allocation, is_last: bool) -> bool: ... + + +class ClearCacheOnAllocateMixin(MemorySplitProtocol): + """ + Helper to assist in caching get_live_ranges, get_size_hint, and + get_symbolic_size. + """ + + def allocate(self, block: Allocation, is_last: bool): + is_allocated = self._allocate(block, is_last) + if is_allocated: + self.clear_cache() + return is_allocated + + def clear_cache(self): + self.get_live_ranges.clear_cache(self) + self.get_size_hint.clear_cache(self) + self.get_symbolic_size.clear_cache(self) + + +@dataclasses.dataclass +class TemporalSplit(ClearCacheOnAllocateMixin, AllocationTreeNode): + """ + Contains a list of allocations not overlapping in LiveRanges. + + Invariant: no pair (a,b) in self.allocations will have: + a.get_live_ranges().overlaps(b.get_live_ranges()) + """ + + allocations: list[AllocationTreeNode] + + def _allocate(self, block: Allocation, is_last: bool): + slot_size = self.get_size_hint() + block_size = block.get_size_hint() + if not is_last and block_size > slot_size: + return False # doesn't fit + + block_live = block.get_live_ranges() + overlapping = [ + s for s in self.allocations if s.get_live_ranges().overlaps(block_live) + ] + if len(overlapping) > 1: + # TODO(jansel): we could try harder here by merging overlapping in space + return False + elif len(overlapping) == 1: + return overlapping[0].allocate(block, is_last) + else: + block.mark_allocated() + + if len(self.allocations) == 1 and isinstance(self.allocations[-1], Empty): + self.allocations.pop() + + if slot_size == block_size: + # perfect fit + self.allocations.append(block) + elif slot_size > block_size: + self.allocations.append( + SpatialSplit.create(block, slot_size - block_size) + ) + else: # grow this allocation + assert is_last + self.allocations = [ + *( + SpatialSplit.create(a, block_size - slot_size) + for a in self.allocations + ), + block, + ] + return True + + @cache_on_self + def get_live_ranges(self) -> LiveRanges: + return LiveRanges( + itertools.chain.from_iterable( + x.get_live_ranges().ranges for x in self.allocations + ) + ) + + @cache_on_self + def get_size_hint(self) -> int: + if not self.allocations: + return 0 + return max(x.get_size_hint() for x in self.allocations) + + @cache_on_self + def get_symbolic_size(self) -> sympy.Expr: + if not self.allocations: + return 0 # type: ignore[return-value] + return sympy.Max(*[x.get_symbolic_size() for x in self.allocations]) + + def is_empty(self): + return len(self.allocations) == 1 and self.allocations[0].is_empty() + + def finalize(self, pool, offset): + self.allocations = [block.finalize(pool, offset) for block in self.allocations] + self.clear_cache() + if len(self.allocations) == 1: + return self.allocations[0] + return self + + +@dataclasses.dataclass +class SpatialSplit(ClearCacheOnAllocateMixin, AllocationTreeNode): + """ + Contains two allocations, left and right, that do not overlap in space. + Right will be allocated immediately after left in memory. + """ + + left: TemporalSplit + right: TemporalSplit + + @staticmethod + def create(left, extra_space): + assert isinstance(left, AllocationTreeNode) + assert isinstance(extra_space, int) and extra_space >= 1 + return SpatialSplit(TemporalSplit([left]), TemporalSplit([Empty(extra_space)])) + + def _allocate(self, block: Allocation, is_last: bool): + return self.left.allocate(block, False) or self.right.allocate(block, is_last) + + @cache_on_self + def get_live_ranges(self): + return LiveRanges( + itertools.chain( + self.left.get_live_ranges().ranges, self.right.get_live_ranges().ranges + ) + ) + + @cache_on_self + def get_size_hint(self) -> int: + return _align(self.left.get_size_hint()) + self.right.get_size_hint() + + @cache_on_self + def get_symbolic_size(self) -> sympy.Expr: + return align(self.left.get_symbolic_size()) + self.right.get_symbolic_size() + + def finalize(self, pool, offset): + self.left = self.left.finalize(pool, offset) + self.right = self.right.finalize( + pool, offset + align(self.left.get_symbolic_size()) + ) + self.clear_cache() + if self.right.is_empty(): + return self.left + return self + + +@dataclasses.dataclass +class AllocationPool: + """ + Represents a pool of allocations that will be generated by a single + call to torch.empty. + """ + + device: torch.device + root: TemporalSplit + can_expand: bool = True + restrict_live_range: LiveRange | None = None + name: str | None = None + names_to_del: list[str] = dataclasses.field(default_factory=list) + creation_cache: dict[str, str] = dataclasses.field(default_factory=dict) + + def __post_init__(self) -> None: + for block in self.root.allocations: + if isinstance(block, Allocation): + self.update_restrict_live_range(block) + + def allocate(self, block: Allocation, is_last: bool): + if ( + self.restrict_live_range is not None + and not self.restrict_live_range.contains(block.live_range) + ): + return False + + block_earliest_available = block.get_earliest_available() + pool_begin = self.root.get_live_ranges().begin + if block_earliest_available and block_earliest_available > pool_begin: + return False + + is_last = self.can_expand and is_last + if self.root.allocate(block, is_last): + self.update_restrict_live_range(block) + return True + + if is_last: + return self.allocate_at_end(block) + + return False + + def update_restrict_live_range(self, block: Allocation): + if block_earliest_available := block.get_earliest_available(): + if self.restrict_live_range is None: + self.restrict_live_range = LiveRange( + block_earliest_available, float("inf") + ) + else: + self.restrict_live_range = LiveRange( + min(self.restrict_live_range.begin, block_earliest_available), + self.restrict_live_range.end, + ) + + def allocate_at_end(self, block): + block.mark_allocated() + self.root = TemporalSplit([SpatialSplit(self.root, TemporalSplit([block]))]) + self.update_restrict_live_range(block) + return True + + def finalize(self, name): + assert not self.name + self.name = name + self.names_to_del.append(name) + self.root.finalize(self, 0) + + def codegen_create(self, wrapper, code: IndentedBuffer): + assert self.name + nbytes = self.root.get_symbolic_size() + for block in self.root.allocations: + if isinstance(block, Allocation) and nbytes == block.get_symbolic_size(): + node = block.node + code.writeline( + wrapper.make_allocation( + self.name, + device=self.device, + dtype=node.get_dtype(), + shape=tuple(node.get_size()), + stride=tuple(node.get_stride()), + ) + ) + return + else: + code.writeline( + wrapper.make_allocation( + self.name, + device=self.device, + dtype=torch.uint8, + shape=(nbytes,), + stride=(1,), + ) + ) + + def codegen_destroy(self, wrapper, code: IndentedBuffer): + code.writeline(wrapper.make_free_by_names(self.names_to_del)) + + def __eq__(self, other): + return self is other + + def __hash__(self): + return id(self) + + +@dataclasses.dataclass +class AllocationPools: + """ + Collection of many AllocationPool objects grouped by device. + """ + + device_to_pools: dict[torch.device, list[AllocationPool]] = dataclasses.field( + default_factory=dict + ) + + def get_pools(self, block): + if block.device not in self.device_to_pools: + self.device_to_pools[block.device] = [] + return self.device_to_pools[block.device] + + def allocate(self, block: Allocation): + pools = self.get_pools(block) + + for pool in pools: + if pool.allocate(block, is_last=pool is pools[-1]): + return + + # everything is full, make a new pool + pools.append( + AllocationPool( + block.device, + TemporalSplit([block]), + can_expand=config.memory_pool != "none", + ) + ) + block.mark_allocated() + + def allocate_output(self, block: Allocation): + """Outputs get different pools so memory gets freed properly""" + pools = self.get_pools(block) + if pools and config.memory_pool in ("outputs", "combined"): + pools[-1].allocate_at_end(block) + else: + # create a new pool + block.mark_allocated() + pools.append( + AllocationPool( + block.device, + TemporalSplit([block]), + can_expand=config.memory_pool == "combined", + ) + ) + + def finalize(self): + """Called at the end of allocation process""" + for i, pool in enumerate( + itertools.chain.from_iterable(self.device_to_pools.values()) + ): + pool.finalize(f"pool{i}") + + def pprint(self): + for pool in itertools.chain.from_iterable(self.device_to_pools.values()): + print() + print(pool.name) + print(pool.root.get_live_ranges()) + pprint.pprint(pool.root) + + +class BufferGroup: + """ + Due to inplace reuse an allocated buffer can have many names. + This tracks these collections of buffers sharing underlying memory. + """ + + def __init__(self, node: BufferLike): + self.node = node + self.names = [node.get_name()] + self.is_output = False + self.allocation: Allocation | None = None + self.live_range = LiveRange(float("inf"), -float("inf")) + + def update_usage(self, timestep: int): + """Expand self.live_range to include timestep""" + self.live_range = LiveRange( + min(timestep, self.live_range.begin), + max(timestep, self.live_range.end), + ) + + def sym_nbytes(self): + return self.node.get_layout().storage_size() * self.node.get_dtype().itemsize + + def make_allocation(self): + assert not self.allocation, "multiple allocations" + assert isinstance(self.live_range.begin, int), "live ranges not computed" + nbytes = self.sym_nbytes() + # For now, fallback value will be used if we encounter an unbacked SymInt. The longer-term plan is to have + # size_hint() use better heuristics for unbackeds, at which point the fallback value will be ignored. + size_hint = V.graph.sizevars.optimization_hint(nbytes, fallback=64) + self.allocation = Allocation( + self.node, + self.live_range, + size_hint=size_hint, + symbolic_size=nbytes, + ) + + def __repr__(self): + return ( + f"{self.__class__.__name__}({self.names!r}, is_output={self.is_output}, " + f"live_range={self.live_range}" + ) + + +@dataclasses.dataclass +class PoolMemoryPlanningLine(MemoryPlanningLine): + """Abstract base class for {Alloc,Dealloc}FromPoolLine""" + + group: BufferGroup + timestep: int | None = None + + @property + def node(self): + return self.group.node + + +@dataclasses.dataclass +class AllocFromPoolLine(PoolMemoryPlanningLine): + """Similar to AllocationLine, but takes memory from a pool""" + + is_first_pool_usage: bool = False + + def codegen(self, code: IndentedBuffer): + allocation = self.group.allocation + assert allocation and allocation.pool + pool = allocation.pool + name = self.node.get_name() + + if self.is_first_pool_usage: + pool.codegen_create(self.wrapper, code) + + pool.names_to_del.extend(self.group.names) + alloc_from_pool, allocation_lines_to_write = allocation.codegen_alloc_from_pool( + self.wrapper + ) + code.writelines(allocation_lines_to_write) + if alloc_from_pool in pool.creation_cache: + code.writeline( + self.wrapper.make_tensor_alias( + name, pool.creation_cache[alloc_from_pool], "alloc" + ) + ) + else: + pool.creation_cache[alloc_from_pool] = name + code.writeline( + f"{self.wrapper.declare}{name} = {alloc_from_pool}{self.wrapper.ending}" + ) + + +@dataclasses.dataclass +class DeallocFromPoolLine(PoolMemoryPlanningLine): + """Similar to FreeIfNotReusedLine, but takes memory from a pool""" + + is_last_pool_usage: bool = False + + def codegen(self, code: IndentedBuffer): + if self.is_last_pool_usage: + assert self.group.allocation and self.group.allocation.pool + self.group.allocation.pool.codegen_destroy(self.wrapper, code) + + +@dataclasses.dataclass +class MemoryPlanner: + """ + Coordination object to run memory planning passes during wrapper + codegen. + """ + + wrapper: Any + pools: AllocationPools = dataclasses.field(default_factory=AllocationPools) + buffer_groups: list[BufferGroup] | None = None + + def plan(self, lines: list[Any]) -> list[Any]: + """Call all the memory planning passes in sequence""" + lines = [*lines] + self.drop_removed_buffers(lines) + self.convert_to_pool_lines(lines) + self.compute_live_ranges(lines) + self.allocate_groups() + self.mark_first_last_usage(lines) + return lines + + def drop_removed_buffers(self, lines): + """ + Replace any memory planning lines in V.graph.removed_buffers with NullLine + """ + # drop any removed buffers + for i, line in enumerate(lines): + if isinstance(line, (AllocateLine, FreeIfNotReusedLine, ReuseLine)): + if line.node.get_name() in V.graph.removed_buffers: + lines[i] = NullLine(self.wrapper) + + def compute_buffer_groups(self, lines): + """ + Populates self.buffer_groups with BufferGroup objects that join + allocations with common storage (due to inplace reuse) into a + single object. + """ + name_to_group = {} + for line in lines: + if isinstance(line, AllocateLine): + name = line.node.get_name() + assert name not in name_to_group + name_to_group[name] = BufferGroup(line.node) + elif isinstance(line, ReuseLine): + old_name = line.node.get_name() + new_name = line.reused_as.get_name() + assert new_name not in name_to_group + # TODO(jansel): we should support reusing buffers created via ExternKernelAlloc + if old_name in name_to_group: + name_to_group[old_name].names.append(new_name) + name_to_group[new_name] = name_to_group[old_name] + + outputs = OrderedSet(V.graph.get_output_names()) + unique_groups = [*{id(g): g for g in name_to_group.values()}.values()] + for group in unique_groups: + group.is_output = any(x in outputs for x in group.names) + + assert self.buffer_groups is None + self.buffer_groups = unique_groups + return name_to_group + + def convert_to_pool_lines(self, lines): + """ + Convert AllocateLine/FreeIfNotReusedLine/ReuseLine into their + pool-based counterparts. + """ + name_to_group = self.compute_buffer_groups(lines) + for i, line in enumerate(lines): + if isinstance(line, AllocateLine): + if line.node.get_name() in name_to_group: + lines[i] = AllocFromPoolLine( + self.wrapper, name_to_group[line.node.get_name()] + ) + elif isinstance(line, FreeIfNotReusedLine): + assert not line.is_reused + if line.node.get_name() in name_to_group: + lines[i] = DeallocFromPoolLine( + self.wrapper, name_to_group[line.node.get_name()] + ) + elif isinstance(line, ReuseLine): + if line.node.get_name() in name_to_group: + line.delete_old = False + + def compute_live_ranges(self, lines): + """Populate every BufferGroup.live_ranges field based on first/last usage""" + timestep = 0 + worklist = collections.deque(lines) + while worklist: + if isinstance(worklist[0], MemoryPlanningLine): + timestep += 1 + while worklist and isinstance(worklist[0], MemoryPlanningLine): + line = worklist.popleft() + if isinstance(line, PoolMemoryPlanningLine): + line.group.update_usage(timestep) + line.timestep = timestep + else: + worklist.popleft() + + timestep += 1 + assert self.buffer_groups is not None + for group in self.buffer_groups: + if group.is_output: + group.update_usage(timestep) + + def allocate_groups(self): + """ + Assign every allocation to a specific location in a specific AllocationPool. + """ + assert config.memory_pool in ("none", "intermediates", "outputs", "combined") + assert self.buffer_groups is not None + + for group in self.buffer_groups: + group.make_allocation() + + outputs: list[Allocation] = [] + intermediates: list[Allocation] = [] + for group in self.buffer_groups: + assert group.allocation + if group.is_output and config.memory_pool != "combined": + outputs.append(group.allocation) + else: + intermediates.append(group.allocation) + + for block in sorted( + outputs, + key=lambda x: ( + x.size_hint, + -len(x.live_range), + ), + ): + self.pools.allocate_output(block) + + for block in sorted( + intermediates, + key=lambda x: ( + -x.size_hint, + -len(x.live_range), + ), + ): + self.pools.allocate(block) + + self.pools.finalize() + + def mark_first_last_usage(self, lines): + """ + Populate the AllocFromPoolLine.is_first_pool_usage and + DeallocFromPoolLine.is_last_pool_usage fields so that pools + are created/destroyed. + """ + seen = OrderedSet[AllocationPool]() + for line in lines: + if isinstance(line, AllocFromPoolLine): + assert line.group.allocation + pool = line.group.allocation.pool + assert pool is not None + if pool not in seen: + line.is_first_pool_usage = True + seen.add(pool) + + seen = OrderedSet[AllocationPool]() + for line in reversed(lines): + if isinstance(line, DeallocFromPoolLine): + assert line.group.allocation + pool = line.group.allocation.pool + assert pool is not None + if pool not in seen: + line.is_last_pool_usage = ( + pool.root.get_live_ranges().end <= line.timestep + ) + seen.add(pool) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/mps.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/mps.py new file mode 100644 index 0000000000000000000000000000000000000000..a6661976387b49b6292771ad190bbf30ff1bc556 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/mps.py @@ -0,0 +1,1235 @@ +# This is not a feature-complete compiler backend +# Just an early prototype that shows that one can compile elementwise ops into a Metal shader +from __future__ import annotations + +import functools +import itertools +import logging +import math +from pathlib import Path +from typing import Any, TYPE_CHECKING + +import sympy +from sympy.printing.precedence import PRECEDENCE + +import torch +from torch.utils._cpp_embed_headers import _embed_headers +from torch.utils._ordered_set import OrderedSet +from torch.utils._sympy.printers import CppPrinter, ExprPrinter as ExprPrinter_ +from torch.utils._sympy.value_ranges import ValueRanges + +from ..utils import ceildiv, get_bounds_index_expr, get_kernel_metadata +from ..virtualized import ops, OpsWrapper, V +from .common import ( + CSEVariable, + DeferredLine, + DTYPE_TO_COMPUTATION_DTYPE, + IndentedBuffer, + OpOverrides, + PythonPrinter, +) +from .simd import IterationRangesEntry, SIMDKernel, SIMDScheduling + + +if TYPE_CHECKING: + from ..ops_handler import ReductionType, StoreMode + from ..scheduler import Scheduler, SchedulerNode + from .common import OpVarT + +log = logging.getLogger(__name__) + +DTYPE_TO_METAL = { + torch.bool: "bool", + torch.int8: "char", + torch.int16: "short", + torch.int32: "int", + torch.int64: "long", + torch.uint8: "uchar", + torch.float: "float", + torch.half: "half", + torch.bfloat16: "bfloat", +} + + +def value_to_metal(val: float | int | bool | str | CSEVariable) -> str: + if isinstance(val, float): + if val == torch.inf: + return "HUGE_VALF" + elif val == -torch.inf: + return "-HUGE_VALF" + elif val != val: # Only float that not equal to self is nan + return "NAN" + return str(val) + elif isinstance(val, bool): + return "true" if val else "false" + return str(val) + + +class MetalExprPrinter(ExprPrinter_): + """Converts sympy expression to Metal code snippet""" + + def _print_FloorDiv(self, expr: sympy.Expr) -> str: + x, div = expr.args + x = self.doprint(x) + div = self.doprint(div) + if expr.is_integer: + return f"c10::metal::floor_divide({x}, {div})" + return f"metal::floor({x}) / ({div})" + + def _print_ModularIndexing(self, expr: sympy.Expr) -> str: + x, div, mod = expr.args + # Workaround for Metal compiler bug with fused (x / A) % B, see PR 175481 + use_safe_mod = div == 65536 and (mod & (mod - 1)) != 0 + + x = self.doprint(x) + if div != 1: + div = self.doprint(div) + if expr.is_integer: + x = f"({x}) / ({div})" + else: + x = f"metal::floor({x}) / ({div})" + mod = self.doprint(mod) + if use_safe_mod: + return f"c10::metal::safe_mod({x}, {mod})" + return f"({x}) % ({mod})" + + def _print_Min(self, expr: sympy.Expr) -> str: + if len(expr.args) != 2: + raise RuntimeError("metal::min only supported for 2 args") + # pyrefly: ignore [missing-attribute] + a, b = map(self._print, expr.args) + typecast_a = f"static_cast({a})" + typecast_b = f"static_cast({b})" + return f"metal::min({typecast_a}, {typecast_b})" + + def _print_Max(self, expr: sympy.Expr) -> str: + if len(expr.args) != 2: + raise RuntimeError("metal::max only supported for 2 args") + # pyrefly: ignore [missing-attribute] + a, b = map(self._print, expr.args) + typecast_a = f"static_cast({a})" + typecast_b = f"static_cast({b})" + return f"metal::max({typecast_a}, {typecast_b})" + + def _print_Abs(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"metal::abs({self._print(expr.args[0])})" + + def _print_RoundToInt(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"static_cast(metal::rint({self._print(expr.args[0])}))" + + def _print_RoundDecimal(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 2 + number, ndigits = expr.args + if number.is_integer: + # ndigits < 0 should have been filtered by the sympy function + assert ndigits < 0 + raise ValueError( + f"For integer inputs, only non-negative ndigits are currently supported, but got {ndigits}." + ) + number_str = self.parenthesize(number, PRECEDENCE["Mul"]) + return f"static_cast(metal::rint(1e{ndigits} * {number_str}) * 1e{-ndigits})" + + def _print_IntTrueDiv(self, expr: sympy.Expr) -> str: + lhs, rhs = expr.args + # TODO: This is only accurate up to 2**23 + # pyrefly: ignore [missing-attribute] + return f"static_cast({self._print(lhs)}) / static_cast({self._print(rhs)})" + + def _print_PowByNatural(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 2 + x, y = map(self.doprint, expr.args) + return f"metal::precise::pow(static_cast({x}), static_cast({y}))" + + def _print_ToFloat(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + x = self.doprint(expr.args[0]) + return f"static_cast({x})" + + def _print_Float(self, expr: sympy.Expr) -> str: + if expr.is_integer: + # sympy considers 0.0 to be integer, but Metal doesn't. + # this workaround prints the float as an integer + # xref: https://github.com/sympy/sympy/issues/26620 + return str(int(expr)) + else: + return str(expr) + + def _print_FloorToInt(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + x = self.doprint(expr.args[0]) + return f"static_cast(metal::floor(static_cast({x})))" + + _print_floor = _print_FloorToInt + + def _print_TruncToInt(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + x = self.doprint(expr.args[0]) + return f"static_cast(metal::trunc({x}))" + + def _print_OpaqueUnaryFn_log2(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + x = self.doprint(expr.args[0]) + return f"metal::precise::log2({x})" + + def _print_Where(self, expr: sympy.Expr) -> str: + c, p, q = ( + self.parenthesize(arg, PRECEDENCE["Atom"] - 0.5) for arg in expr.args + ) + return f"{c} ? {p} : {q}" + + +class MetalOverrides(OpOverrides): + """Implements Metal-specific overrides for ops. Base class emits Python-friendly overrides.""" + + @staticmethod + def to_dtype( + x: CSEVariable, + dtype: torch.dtype, + src_dtype: torch.dtype | None = None, + use_compute_types: bool = True, + ) -> str: + if dtype == torch.double: + log.warning( + "float64 cast requested, probably from tensorify_python_scalars" + ) + return f"static_cast({x})" + return f"static_cast<{DTYPE_TO_METAL[dtype]}>({x})" + + @staticmethod + def to_dtype_bitcast( + x: CSEVariable, dtype: torch.dtype, src_dtype: torch.dtype + ) -> str: + return f"as_type<{DTYPE_TO_METAL[dtype]}>(static_cast<{DTYPE_TO_METAL[src_dtype]}>({x}))" + + @staticmethod + def constant(val: bool | float | int, dtype: torch.dtype) -> str: + return value_to_metal(val) + + @staticmethod + def index_expr(expr: sympy.Expr, dtype: torch.dtype) -> str: + idx_str = V.kernel.index_to_str(V.kernel.prepare_indexing(expr)) + var = V.kernel.cse.generate( + V.kernel.compute, idx_str, bounds=get_bounds_index_expr(expr) + ) + return ops.to_dtype(var, dtype) + + @staticmethod + def masked(mask: CSEVariable, body: sympy.Expr, other: CSEVariable) -> str: + # TODO: Type annotation for other is wrong, it's often float or int + # TODO: Should it be converted to lambda on MacOS-15+? + + other_str = value_to_metal(other) + scoped_body = IndentedBuffer() + with V.kernel.swap_buffers(scoped_body), scoped_body.indent(): + # Reset the scoped variable counter so that each invocation of the same body + # generates identical variable names. Without this reset, repeated calls to + # body() would keep incrementing the counter, resulting in different cache key. + V.kernel.cse.iter_buffer_ids = itertools.count() + # Append "_scoped" to the current prefix so each nesting level gets unique vars + V.kernel.cse.name_prefix += "_scoped" + rc = body() + + # Compute cache key manually as variable name is needed to actually generate the code + cache_key = f"{mask}:{scoped_body.getvalue()}:{other_str}" + var = V.kernel.cse.try_get(cache_key) + if not var: + var = V.kernel.cse.newvar(dtype=rc.dtype) + V.kernel.cse.put(cache_key, var) + V.kernel.compute.writelines( + [f"{DTYPE_TO_METAL[rc.dtype]} {var};", f"if ({mask}) {{"] + ) + with V.kernel.compute.indent(): + V.kernel.compute.splice(scoped_body) + V.kernel.compute.writeline( + f"{var} = static_cast({rc});" + ) + V.kernel.compute.writeline( + f"}} else {var} = static_cast({other_str});" + ) + return var + + @staticmethod + # pyrefly: ignore [bad-override] + def where(a: OpVarT, b: OpVarT, c: OpVarT) -> str: + return f"{a} ? {b} : static_cast({value_to_metal(c)})" + + @staticmethod + def remainder(a: OpVarT, b: OpVarT) -> str: + return f"c10::metal::remainder({a}, {b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def maximum(a: CSEVariable, b: CSEVariable) -> str: + typecast_a = f"static_cast({a})" + typecast_b = f"static_cast({b})" + return f"c10::metal::max({typecast_a}, {typecast_b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def minimum(a: CSEVariable, b: CSEVariable) -> str: + typecast_a = f"static_cast({a})" + typecast_b = f"static_cast({b})" + return f"c10::metal::min({typecast_a}, {typecast_b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def logical_or(a: CSEVariable, b: CSEVariable) -> str: + return f"{a} || {b}" + + @staticmethod + # pyrefly: ignore [bad-override] + def logical_and(a: CSEVariable, b: CSEVariable) -> str: + return f"{a} && {b}" + + @staticmethod + # pyrefly: ignore [bad-override] + def isnan(x: CSEVariable) -> str: + return f"metal::isnan({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def isinf(x: CSEVariable) -> str: + return f"metal::isinf({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def log(x: CSEVariable) -> str: + return f"metal::precise::log({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def exp(x: CSEVariable) -> str: + return f"metal::precise::exp({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def abs(x: CSEVariable) -> str: + return f"metal::abs({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def signbit(x: CSEVariable) -> str: + return f"metal::signbit({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def sin(x: CSEVariable) -> str: + return f"metal::precise::sin({x})" + + @staticmethod + def sinc(x: CSEVariable) -> str: + return f"c10::metal::sinc({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def cos(x: CSEVariable) -> str: + return f"metal::precise::cos({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def tan(x: CSEVariable) -> str: + return f"metal::precise::tan({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def asin(x: CSEVariable) -> str: + return f"metal::precise::asin({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def acos(x: CSEVariable) -> str: + return f"metal::precise::acos({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def atan(x: CSEVariable) -> str: + return f"metal::precise::atan({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def atan2(x: CSEVariable, y: CSEVariable) -> str: + return f"::metal::precise::atan2({x}, {y})" + + @staticmethod + # pyrefly: ignore [bad-override] + def sqrt(x: CSEVariable) -> str: + return f"metal::precise::sqrt({x})" + + @staticmethod + def neg(x: CSEVariable) -> str: + # TODO: Does it rely on undefined behavior? + # If so, add special logic for unsigned types + return f"static_cast(-{x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def rsqrt(x: CSEVariable) -> str: + return f"metal::precise::rsqrt({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def tanh(x: CSEVariable) -> str: + return f"metal::precise::tanh({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def atanh(x: CSEVariable) -> str: + return f"metal::precise::atanh({x})" + + @staticmethod + def floordiv(a: CSEVariable, b: CSEVariable) -> str: + # a and b must be of integer type + return f"c10::metal::floor_divide({a}, {b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def floor(x: CSEVariable) -> str: + return f"metal::floor({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def sign(x: CSEVariable) -> str: + return f"metal::sign({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def fmod(a: CSEVariable, b: CSEVariable) -> str: + typecast_a = f"static_cast({a})" + typecast_b = f"static_cast({b})" + return f"metal::fmod({typecast_a}, {typecast_b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def trunc(x: CSEVariable) -> str: + return f"metal::trunc({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def truncdiv(a: CSEVariable, b: CSEVariable) -> str: + quot = f"{a} / {b}" + if (a.dtype is not None and a.dtype.is_floating_point) or ( + b.dtype is not None and b.dtype.is_floating_point + ): + return f"metal::trunc({quot})" + return quot + + @staticmethod + # pyrefly: ignore [bad-override] + def ceil(x: CSEVariable) -> str: + return f"metal::ceil({x})" + + @staticmethod + def rand(seed: CSEVariable, offset: CSEVariable) -> str: + V.kernel.headers.add("random") + return f"c10::metal::rand({seed}, {offset})" + + @staticmethod + def randn(seed: CSEVariable, offset: CSEVariable) -> str: + V.kernel.headers.add("random") + return f"c10::metal::randn({seed}, {offset})" + + @staticmethod + def randint64( + seed: CSEVariable, offset: CSEVariable, low: CSEVariable, high: CSEVariable + ) -> str: + V.kernel.headers.add("random") + return f"c10::metal::randint64({seed}, {offset}, {low}, {high})" + + @staticmethod + # pyrefly: ignore [bad-override] + def round(x: CSEVariable) -> str: + return f"metal::rint({x})" + + @staticmethod + def pow(a: CSEVariable, b: CSEVariable) -> str: + cast_a = f"static_cast({a})" + cast_b = f"static_cast({b})" + return f"metal::pow({cast_a}, {cast_b})" + + def _special_unary(self, a: CSEVariable, name: str) -> str: + V.kernel.headers.add("special_math") + return f"c10::metal::{name}({a})" + + def _special_binary(self, a: CSEVariable, b: CSEVariable, name: str) -> str: + V.kernel.headers.add("special_math") + return f"c10::metal::{name}({a}, {b})" + + @classmethod + def _initialize_special_ops(cls) -> None: + # Unary special ops + for name in [ + "erf", + "erfinv", + "i0", + "i0e", + "i1", + "i1e", + "digamma", + "spherical_bessel_j0", + ]: + setattr(cls, name, functools.partialmethod(cls._special_unary, name=name)) + + cls.lgamma = functools.partialmethod(cls._special_unary, name="log_gamma") # type: ignore[assignment] + + # Unary special ops with forward in method name + for name in [ + "bessel_j0", + "bessel_j1", + "bessel_y0", + "bessel_y1", + "modified_bessel_i0", + "modified_bessel_i1", + "modified_bessel_k0", + "modified_bessel_k1", + "scaled_modified_bessel_k0", + "scaled_modified_bessel_k1", + ]: + setattr( + cls, + name, + functools.partialmethod(cls._special_unary, name=name + "_forward"), + ) + + # Binary special ops + for name in [ + "polygamma", + "igamma", + "igammac", + "zeta", + ]: + setattr(cls, name, functools.partialmethod(cls._special_binary, name=name)) + + # Binary special ops with forward in method name + for name in [ + "chebyshev_polynomial_t", + "chebyshev_polynomial_u", + "chebyshev_polynomial_v", + "chebyshev_polynomial_w", + "hermite_polynomial_h", + "hermite_polynomial_he", + "shifted_chebyshev_polynomial_t", + "shifted_chebyshev_polynomial_u", + "shifted_chebyshev_polynomial_v", + "shifted_chebyshev_polynomial_w", + ]: + setattr( + cls, + name, + functools.partialmethod(cls._special_binary, name=name + "_forward"), + ) + + +MetalOverrides._initialize_pointwise_overrides("mps") +MetalOverrides._initialize_special_ops() + + +class MetalKernel(SIMDKernel): + """Implement Metal codegen based on the SIMDKernel abstraction""" + + overrides = MetalOverrides # type: ignore[assignment] + suffix = ";" + newvar_prefix = "auto " + max_threadgroup_size = 1024 + simd_group_size = 32 + pexpr = PythonPrinter().doprint + cexpr = CppPrinter().doprint + sexpr = MetalExprPrinter().doprint + kexpr = sexpr + headers: OrderedSet[str] = OrderedSet(["utils"]) + multistage_reduction_entry: list[IterationRangesEntry] = [] + + def __init__( + self, + tiling: dict[str, sympy.Expr], + **kwargs: Any, + ) -> None: + super().__init__(tiling, **kwargs) + self.acc_var_ids = itertools.count() + + def dtype_to_str(self, dtype: torch.dtype) -> str: + return DTYPE_TO_METAL[dtype] + + def load(self, name: str, index: sympy.Expr) -> CSEVariable: + """Codegen a load from an InputBuffer""" + var = self.args.input(name) + index = self.prepare_indexing(index) + dtype = V.graph.get_dtype(name) + line = f"{var}[{self.index_to_str(index)}]" + if dtype in [torch.float16, torch.bfloat16]: + # TODO(NS): Figure out the right balance between optype casts + # op_math_t for half-precision floats should be float32 + # Otherwise it can lead to a correctness issues with eager + line = f"static_cast({line})" + dtype = torch.float32 + return self.cse.generate(self.loads, line, dtype=dtype) + + def store( + self, name: str, index: sympy.Expr, value: CSEVariable, mode: StoreMode = None + ) -> None: + var = self.args.output(name) + index = self.prepare_indexing(index) + dtype_str = self.dtype_to_str(V.graph.get_dtype(name)) + cast_val = f"static_cast<{dtype_str}>({value})" + if mode is None: + line = f"{var}[{self.index_to_str(index)}] = {cast_val};" + elif mode == "atomic_add": + self.headers.add("atomic") + atomic_type = f"c10::metal::AtomicType<{dtype_str}>" + cast_var = f"reinterpret_cast({var})" + line = f"{atomic_type}::atomic_add({cast_var}, {self.index_to_str(index)}, {cast_val});" + else: + raise RuntimeError(f"Unimplemented store mode {mode}") + if self.inside_reduction: + self.compute.writeline(DeferredLine(name, line)) + else: + self.stores.writeline(DeferredLine(name, line)) + + def store_reduction(self, name: str, index: sympy.Expr, value: CSEVariable) -> None: + var = self.args.output(name) + index = self.prepare_indexing(index) + dtype_str = self.dtype_to_str(V.graph.get_dtype(name)) + + reduction_dim = next(t for t in self.range_trees if t.is_reduction) + # Only one thread in the reduction group needs to store the results + line = f"{var}[{self.index_to_str(index)}] = static_cast<{dtype_str}>({value});" + line = f"if ({reduction_dim.name} == 0) {line}" + self.stores.writeline(DeferredLine(name, line)) + + def _new_idxvar( + self, + dtype: str | torch.dtype, + elem_count: int | None = None, + default_value: Any | None = None, + is_threadgroup: bool = True, + bounds: ValueRanges[Any] = ValueRanges.unknown(), + ) -> CSEVariable: + if isinstance(dtype, torch.dtype): + dtype = self.dtype_to_str(dtype) + var_name = f"tmp_acc_{next(self.acc_var_ids)}" + var = V.kernel.create_cse_var(var_name, bounds, dtype) + var_def = "threadgroup " if is_threadgroup else "" + var_def += f"{dtype} {var_name}" + if elem_count: + var_def += f"[{self.sexpr(elem_count)}]" + if default_value is not None: + assert not is_threadgroup, "Thread group var can not have default value" + var_def += f" = {default_value}" + self.indexing_code.writeline(var_def + self.suffix) + return var + + def reduction( + self, + dtype: torch.dtype, + src_dtype: torch.dtype, + reduction_type: ReductionType, + value: CSEVariable | tuple[CSEVariable, ...], + ) -> CSEVariable | tuple[CSEVariable, ...]: + "Caching wrapper around _reduction_nocache" + cache_key = (src_dtype, reduction_type, value) + # Return cached reduction + if cache_key in self.cse.reduction_cache: + return self.cse.reduction_cache[cache_key] + result = self._reduction_nocache(dtype, src_dtype, reduction_type, value) + self.cse.reduction_cache[cache_key] = result # type: ignore[assignment] + return result + + def _reduction_nocache( + self, + dtype: torch.dtype, + src_dtype: torch.dtype, + reduction_type: ReductionType, + value: CSEVariable | tuple[CSEVariable, ...], + ) -> CSEVariable | tuple[CSEVariable, ...]: + """Codegen a reduction operation. + Only sum and prod operations are somewhat reasonable optimized""" + assert self.inside_reduction + assert not self._load_mask + + def _unwrap_helper(res3: CSEVariable) -> tuple[CSEVariable, ...]: + # Uwraps vec3 dtype into individual components + return OpsWrapper._unwrap( + [CSEVariable(f"{res3}.{t}", res3.bounds, res3.dtype) for t in "xyz"] + ) + + # Establish reduction buffer size and index expression + reduction_idx = "" + acc_buf_size = 1 + for rd in self.range_trees: + if not rd.is_reduction: + continue + if reduction_idx: + reduction_idx += " + " + reduction_idx += f"{rd.name} * {acc_buf_size}" + + if isinstance(rd.numel, sympy.Integer): + acc_buf_size *= rd.numel + else: + acc_buf_size *= sympy.Symbol( + f"{rd.prefix}numel", integer=True, positive=True + ) + + acc_buf_size = sympy.Min(acc_buf_size, self.max_threadgroup_size) + acc_buf_size_str = self.sexpr(acc_buf_size) + shmem_buf_size = ( + ceildiv(acc_buf_size, self.simd_group_size) + if isinstance(acc_buf_size, sympy.Integer) + else self.simd_group_size + ) + + if reduction_type == "any": + acc = self._new_idxvar(dtype) + self.indexing_code.writeline(f"{acc} = false;") + self.indexing_code.writeline( + "threadgroup_barrier(metal::mem_flags::mem_threadgroup);" + ) + self.compute.splice( + f""" + if ({value}) {{ + {acc} = true; + }} + """ + ) + self.stores.writeline( + "threadgroup_barrier(metal::mem_flags::mem_threadgroup);" + ) + return acc + + self.headers.add("reduction_utils") + + if reduction_type in ["prod", "sum"]: + acc_dtype = DTYPE_TO_COMPUTATION_DTYPE[src_dtype] + acc_buf = self._new_idxvar(acc_dtype, shmem_buf_size) + if not self.multistage_reduction_entry: + val = value + else: + default_val, reduction_op = ( + (0, "+") if reduction_type == "sum" else (1, "*") + ) + val = self._new_idxvar( + acc_dtype, default_value=default_val, is_threadgroup=False + ) + self.compute.splice(f"{val} {reduction_op}= {value};") + + return self.cse.generate( + self.stores, + f"c10::metal::threadgroup_{reduction_type}({acc_buf}, {val}, {reduction_idx}, {acc_buf_size_str})", + dtype=DTYPE_TO_COMPUTATION_DTYPE[dtype], + ) + if reduction_type in ["max", "min"]: + acc_buf = self._new_idxvar(src_dtype, shmem_buf_size) + src_metal_type = DTYPE_TO_METAL[src_dtype] + cast_value = f"static_cast<{src_metal_type}>({value})" + if not self.multistage_reduction_entry: + val = cast_value # type: ignore[assignment] + else: + lim_fn = "lowest" if reduction_type.endswith("max") else "max" + limit_val = f"::metal::numeric_limits<{src_metal_type}>::{lim_fn}()" + val = self._new_idxvar( + src_dtype, default_value=limit_val, is_threadgroup=False + ) + self.compute.splice( + f"{val} = ::c10::metal::{reduction_type}({val}, {cast_value});" + ) + return self.cse.generate( + self.stores, + f"c10::metal::threadgroup_{reduction_type}({acc_buf}, {val}, {reduction_idx}, {acc_buf_size_str})", + dtype=DTYPE_TO_COMPUTATION_DTYPE[dtype], + ) + if reduction_type in ["argmin", "argmax"]: + data_acc_buf = self._new_idxvar(src_dtype, shmem_buf_size) + idx_acc_buf = self._new_idxvar(dtype, shmem_buf_size) + src_metal_type = DTYPE_TO_METAL[src_dtype] + cast_value = f"static_cast<{src_metal_type}>({value})" + if not self.multistage_reduction_entry: + val = cast_value # type: ignore[assignment] + idx_val = f"static_cast<{DTYPE_TO_METAL[dtype]}>({reduction_idx})" + else: + lim_fn = "lowest" if reduction_type.endswith("max") else "max" + limit_val = f"::metal::numeric_limits<{src_metal_type}>::{lim_fn}()" + val = self._new_idxvar( + src_dtype, default_value=limit_val, is_threadgroup=False + ) + idx_val = self._new_idxvar(dtype, default_value=0, is_threadgroup=False) # type: ignore[assignment] + idx_var = next( + t for t in self.range_tree_nodes.values() if t.is_reduction + ) + cmp_op = ">" if reduction_type == "argmax" else "<" + nan_suffix = ( + f" || ::metal::isnan({value}) " + if src_dtype.is_floating_point + else "" + ) + self.compute.splice(f""" + if ({value} {cmp_op} {val}{nan_suffix}) {{ + {val} = {value}; + {idx_val} = {idx_var.name}; + }} + """) + return self.cse.generate( + self.stores, + f"c10::metal::threadgroup_{reduction_type}({data_acc_buf}, {idx_acc_buf}, " + f"{val}, {idx_val}, {reduction_idx}, {acc_buf_size_str})", + dtype=dtype, + ) + if reduction_type == "welford_reduce": + if not self.multistage_reduction_entry: + acc_buf = self._new_idxvar(src_dtype, acc_buf_size) + self.compute.splice(f"{acc_buf}[{reduction_idx}] = {value};") + wf_res = self.cse.generate( + self.compute, + f"c10::metal::threadgroup_{reduction_type}({acc_buf}, {acc_buf_size_str})", + dtype=torch.float32, + ) + return _unwrap_helper(wf_res) + acc_buf = self._new_idxvar("float3", acc_buf_size) + acc_thread_var = f"{acc_buf}[{reduction_idx}]" + self.indexing_code.splice(f"{acc_thread_var} = 0.0;") + self.compute.writeline( + f"{acc_thread_var} = ::c10::metal::welford_combine({acc_thread_var}, float3({value}, 0.0, 1.0));" + ) + wf_res = self.cse.generate( + self.stores, + f"c10::metal::threadgroup_welford_combine({acc_buf}, {acc_buf_size})", + dtype=torch.float32, + ) + return _unwrap_helper(wf_res) + if reduction_type == "welford_combine": + assert isinstance(value, tuple), "Input to welford combine must be tuple" + acc_buf = self._new_idxvar("float3", acc_buf_size) + acc_thread_var = f"{acc_buf}[{reduction_idx}]" + inp_value = f"float3({value[0]}, {value[1]}, {value[2]})" + self.indexing_code.splice(f"{acc_thread_var} = 0.0;") + if self.multistage_reduction_entry: + self.indexing_code.splice(f"{acc_thread_var} = 0.0;") + self.compute.writeline( + f"{acc_thread_var} = ::c10::metal::welford_combine({acc_thread_var}, {inp_value});" + ) + else: + self.compute.writeline(f"{acc_thread_var} = {inp_value};") + wf_res = self.cse.generate( + self.stores if self.multistage_reduction_entry else self.compute, + f"c10::metal::threadgroup_{reduction_type}({acc_buf}, {acc_buf_size_str})", + dtype=torch.float32, + ) + return _unwrap_helper(wf_res) + raise NotImplementedError(reduction_type) + + def codegen_iteration_ranges_entry(self, entry: IterationRangesEntry) -> None: + index_expr = self.rename_indexing(entry.expr) + index_str = self.sexpr(index_expr) # type: ignore[misc] + + if not entry.is_reduction or ( + isinstance(entry.root.numel, sympy.Integer) + and entry.root.numel <= self.max_threadgroup_size + ): + self.indexing_code.writeline( + f"{self.index_dtype} {entry.name} = {index_str};" + ) + return + + acc_size = ( + entry.root.numel + if isinstance(entry.root.numel, sympy.Integer) + else sympy.Symbol(f"{entry.root.prefix}numel", integer=True, positive=True) + ) + + # Check if we've already generated a loop for this reduction root + root_already_processed = any( + e.root is entry.root for e in self.multistage_reduction_entry + ) + + linear_idx_name = f"{entry.root.prefix}_linear_idx" + + if not root_already_processed: + self.multistage_reduction_entry.append(entry) + # When reducing the tensor whose size exceeds max threadgroup size + # loop over extra indices per reduction thread and perform part of the operation + # using values in the shared memory + + # Use floats so that it doesn't do integer division + loop_size = (acc_size + float(self.max_threadgroup_size - 1)) // float( + self.max_threadgroup_size + ) + loop_size_str = self.sexpr(loop_size) + + root_name = entry.root.name + + self.body.writeline( + f"for(auto {entry.root.prefix}_cnt = 0; {entry.root.prefix}_cnt < {loop_size_str}; ++{entry.root.prefix}_cnt) {{" + ) + with self.body.indent(): + if isinstance(acc_size, sympy.Symbol): + self.body.writeline( + f"{self.index_dtype} {linear_idx_name} = " + f"{self.max_threadgroup_size} * {entry.root.prefix}_cnt + {root_name};" + ) + else: + self.body.writeline( + f"{self.index_dtype} {linear_idx_name} = {loop_size_str} * {root_name} + {entry.root.prefix}_cnt;" + ) + + # Check that reduction is performed only within tensor boundary + if ( + isinstance(acc_size, sympy.Symbol) + or loop_size * self.max_threadgroup_size != acc_size + ): + self.body.writeline(f"if ({linear_idx_name} >= {acc_size}) break;") + + # Compute entry value from linear index by substituting root name + sub_index_str = index_str.replace(entry.root.name, linear_idx_name) + self.body.writeline( + f"{self.index_dtype} {entry.name} = {sub_index_str};" + ) + else: + # root is already processed so just need to compute this entry's value inside the existing loop + with self.body.indent(): + sub_index_str = index_str.replace(entry.root.name, linear_idx_name) + self.body.writeline( + f"{self.index_dtype} {entry.name} = {sub_index_str};" + ) + + def codegen_body(self) -> None: + """ + Concat output code from index_code, loads, compute, stores, + suffix into self.body. + + For pointwise kernels, this is called just once at the end. + + For reduction kernels, this generates a loop over the reduction + axis. + """ + if self.multistage_reduction_entry: + with self.body.indent(): + self.body.splice(self.loads) + self.body.splice(self.compute) + self.body.writeline("}" * len(self.multistage_reduction_entry)) + # Invalidate variables instantiated inside loop + # But results of reduction alive. Reduction cache values can be + # either CSEVariable or tuple of CSEVariables, in which case all + # variables in the tuple must be preserved + self.cse.invalidate( + OrderedSet( + v + for item in self.cse.reduction_cache.values() + for v in (item if isinstance(item, tuple) else (item,)) + ) + ) + # And loop codegen + while self.multistage_reduction_entry: + self.multistage_reduction_entry.pop().cache_clear() + else: + self.body.splice(self.loads) + self.body.splice(self.compute) + self.body.splice(self.stores) + self.loads.clear() + self.compute.clear() + self.stores.clear() + + def codegen_kernel(self, name: str = "generated_kernel") -> str: + """Called at the end to generate a final kernel string""" + self.codegen_body() + code = IndentedBuffer() + fn_name = name + + if V.graph.cpp_wrapper: + code.writeline('(R"MTL(') + else: + code.writeline("compile_mps_shader('''") + + idx_vars = self.active_range_trees() + with code.indent(): + if not V.graph.cpp_wrapper: + for header in self.headers: + code.writeline(f"#include ") + else: + headers = [ + f"#include " for header in self.headers + ] + header_contents = _embed_headers( + headers, + [Path(__file__).parent.parent.parent / "include"], + OrderedSet(), # type: ignore[arg-type] + ) + code.writeline(header_contents) + + if self.inside_reduction: + total_reduction_size = math.prod( + t.numel for t in self.range_trees if t.is_reduction + ) + # If using dynamic shapes, set the threadgroup size to be the + # max possible size + threadgroup_size = ( + min(total_reduction_size, self.max_threadgroup_size) + if isinstance(total_reduction_size, sympy.Integer) + else self.max_threadgroup_size + ) + code.writeline( + f"[[max_total_threads_per_threadgroup({threadgroup_size})]]" + ) + code.writeline(f"kernel void {fn_name}(") + with code.indent(): + for outer, inner in self.args.output_buffers.items(): + if outer in self.removed_buffers: + continue + dtype_str = self.dtype_to_str(V.graph.get_dtype(outer)) + code.writeline(f"device {dtype_str}* {inner},") + for outer, inner in self.args.input_buffers.items(): + dtype = V.graph.get_dtype(outer) + # MPS does not support float64, but scalar inputs are fine + if dtype == torch.float64: + outer_buf = V.graph.try_get_buffer(outer) + if outer_buf is None or outer_buf.get_size() != []: + raise RuntimeError("float64 is not supported by MPS") + dtype_str = "float" + else: + dtype_str = self.dtype_to_str(dtype) + code.writeline(f"constant {dtype_str}* {inner},") + for inner in self.args.sizevars.values(): + code.writeline(f"constant long& {inner},") + + # Write dynamic values as inputs + for idx_var in idx_vars: + if isinstance(idx_var.numel, sympy.Integer): + pass + else: + code.writeline(f"constant long& {idx_var.prefix}numel,") + + # Add error buffer parameter if error header is used + if "error" in self.headers: + code.writeline("device c10::metal::ErrorMessages* error_buf,") + + assert len(idx_vars) < 4, "Up to 3 index variables are supported" + thread_pos_dtype = ( + f"uint{len(idx_vars)}" if len(idx_vars) > 1 else "uint" + ) + thread_pos_var_name = ( + idx_vars[0].name if len(idx_vars) == 1 else "thread_pos" + ) + thread_pos_suffix = "," if self.inside_reduction else "" + code.writeline( + f"{thread_pos_dtype} {thread_pos_var_name} [[thread_position_in_grid]]{thread_pos_suffix}" + ) + if self.inside_reduction: + code.writeline( + f"{thread_pos_dtype} group_pos [[thread_position_in_threadgroup]]" + ) + code.writeline(") {") + with code.indent(): + if len(idx_vars) > 1: + for idx, var in enumerate(idx_vars): + code.writeline( + f"auto {var.name} = thread_pos.{chr(120 + idx)};" + ) + code.splice(self.indexing_code) + code.splice(self.body) + code.writeline("}") + + if V.graph.cpp_wrapper: + code.writeline(')MTL");') + else: + code.writeline("''')") + + return code.getvalue() + + def call_kernel( + self, name: str, node: Any = None, deallocate_ws: bool = True + ) -> None: + """ + Codegens a call to this kernel + """ + wrapper = V.graph.wrapper_code + # Make sure sizevars has been computed + for v in self.args.sizevars: + wrapper.ensure_size_computed(v) + + _, call_args, _, arg_types = self.args.python_argdefs() + arg_name_to_type = { + str(call_arg): arg_type for call_arg, arg_type in zip(call_args, arg_types) + } + + args = [*self.args.output_buffers.keys(), *self.args.input_buffers.keys()] + args = [arg for arg in args if arg not in self.removed_buffers] + args += [str(v) for v in self.args.sizevars] + arg_types = [arg_name_to_type[arg] for arg in args] + + # Add any dynamic ints as inputs + for tree in self.range_trees: + if isinstance(tree.numel, (sympy.Integer, int)): + # Don't need to pass in integers as inputs + continue + elif isinstance(tree.numel, sympy.Symbol): + expr = tree.numel + else: + expr = V.graph.wrapper_code.generate_numel_expr(name, tree).inner + + if not tree.is_reduction or self.inside_reduction: + args.append(str(expr)) + arg_types.append(int) + + expr_printer = self.cexpr if V.graph.cpp_wrapper else self.pexpr + + def format_threads(threads: list[str], kwarg: str) -> str: + if V.graph.cpp_wrapper: + threads = [f"static_cast({t})" for t in threads] + return f"{{{', '.join(threads)}}}" + else: + return f"{kwarg}=[{', '.join(threads)}]" + + # For reduction kernels, limit the maximum size over reduction dimensions to + # a maximum threadgroup size + if len(self.active_range_trees()) > 0: + threads = [ + expr_printer( + sympy.Min(v.numel, self.max_threadgroup_size) # type: ignore[misc] + if v.is_reduction + else v.numel + ) + for v in self.active_range_trees() + ] + + args.append(format_threads(threads, "threads")) + arg_types.append(list) + else: + if V.graph.cpp_wrapper: + raise RuntimeError("We should always have threads?") + + if self.inside_reduction: + threads = [ + expr_printer(sympy.Min(v.numel, self.max_threadgroup_size)) # type: ignore[misc] + if v.is_reduction + else "1" + for v in self.active_range_trees() + ] + args.append(format_threads(threads, "group_size")) + arg_types.append(list) + else: + if V.graph.cpp_wrapper: + # Add a None so that we always have a group_size in the + # arguments. We won't use it if the value is None. + args += [None] # type: ignore[list-item] + arg_types.append(None) + + # Add error buffer index if error reporting is used + # TODO(malfet) Figure out how to do it for aoti + if "error" in self.headers and not V.graph.cpp_wrapper: + args.append( + f"error_buf_idx={len([arg for arg in args if arg is not None and '=' not in arg])}" + ) + + wrapper.generate_kernel_call( + name, + args, + device=torch.device("mps"), + triton=False, + arg_types=arg_types, + ) + + def device_assert_async(self, cond: CSEVariable, msg: str) -> None: + if V.graph.cpp_wrapper: + self.cse.generate(self.compute, f"if (!{cond}) return", assignment=False) + else: + self.headers.add("error") + self.compute.writelines( + [ + f"if (!{cond}) {{", + f" TORCH_REPORT_ERROR(error_buf, {repr(msg)});", + " return;", + "}", + ] + ) + + def check_bounds( + self, expr: sympy.Expr, size: sympy.Expr, lower: bool, upper: bool + ) -> None: + if not (lower or upper): + return + + expr_str = self.index_to_str(expr) + size_str = self.index_to_str(size) + + # Generate bounds checking with error reporting + # TODO(malfet): Is upper bound inclusive or exclusive? + if lower and upper: + # Check both lower and upper bounds + condition = f"({expr_str} < 0 || {expr_str} >= {size_str})" + elif lower: + condition = f"{expr_str} < 0" + else: + condition = f"{expr_str} >= {size_str}" + + # Generate error reporting code + if V.graph.cpp_wrapper: + self.cse.generate( + self.compute, f"if ({condition}) return", assignment=False + ) + else: + # Add error header for error reporting + self.headers.add("error") + self.compute.writelines( + [ + f"if ({condition}) {{", + f' TORCH_REPORT_ERROR(error_buf, "Index ", {expr_str}, " out of range [0, ", {size_str}, ")");', + " return;", + "}", + ] + ) + + +class MetalScheduling(SIMDScheduling): + kernel_type = MetalKernel # type: ignore[assignment] + _kernel_fn_counter: int = 0 + + def __init__(self, scheduler: Scheduler | None) -> None: + super().__init__(scheduler) + + def define_kernel( + self, src_code: str, node_schedule: list[SchedulerNode], kernel: MetalKernel + ) -> str: + wrapper = V.graph.wrapper_code + if src_code in wrapper.src_to_kernel: + return wrapper.src_to_kernel[src_code] + + if V.graph.cpp_wrapper: + # C++ path: one library per kernel (each has a single "generated_kernel" function) + mps_lib_name = f"mps_lib_{wrapper.next_kernel_suffix()}" + kernel_name = mps_lib_name + wrapper.src_to_kernel[src_code] = kernel_name + src_code = f"const char* {mps_lib_name}_source = " + src_code + origins, detailed_origins = get_kernel_metadata(node_schedule, wrapper) + wrapper.define_kernel( + mps_lib_name, src_code, f"{origins}\n{detailed_origins}", gpu=False + ) + return kernel_name + + # Python path: register kernel with async_compile; wait() will compile all + # accumulated Metal kernels into a single library and replace each placeholder. + fn_name = f"generated_kernel_{self._kernel_fn_counter}" + self._kernel_fn_counter += 1 + wrapper.src_to_kernel[src_code] = fn_name + + # Extract Metal source from compile_mps_shader('''...''') call + metal_src_start = "compile_mps_shader('''" + start = src_code.index(metal_src_start) + len(metal_src_start) + end = src_code.rindex("''')") + metal_src = src_code[start:end] + + # Strip #include lines and rename the kernel function + body_lines = [] + for line in metal_src.split("\n"): + if line.strip().startswith("#include"): + continue + body_lines.append( + line.replace("kernel void generated_kernel(", f"kernel void {fn_name}(") + ) + + headers_repr = repr(sorted(kernel.headers)) + wrapper.header.writeline(f"{fn_name} = async_compile.metal({fn_name!r}, '''") + for line in body_lines: + wrapper.header.writeline(line) + wrapper.header.writeline(f"''', {headers_repr})") + + return fn_name diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/mps_device_op_overrides.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/mps_device_op_overrides.py new file mode 100644 index 0000000000000000000000000000000000000000..8b4ddb163ef4f9957e1a64a4ab25ec865e8206b5 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/mps_device_op_overrides.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from .common import DeviceOpOverrides, register_device_op_overrides + + +class MPSDeviceOpOverrides(DeviceOpOverrides): + def device_guard(self, device_idx: int) -> str: + assert device_idx == 0 + return "torch._ops.contextlib.nullcontext()" + + def set_device(self, device_idx: int) -> str: + assert device_idx == 0 + return "pass # MPS set device" + + def kernel_driver(self) -> str: + return """ + #include + """ + + def cpp_kernel_type(self) -> str: + return "MTLFunction_t" + + +register_device_op_overrides("mps", MPSDeviceOpOverrides()) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/mtia/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/mtia/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/mtia/device_op_overrides.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/mtia/device_op_overrides.py new file mode 100644 index 0000000000000000000000000000000000000000..135bee2b8fe9226d5b69077201c0b08bfc8460a4 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/mtia/device_op_overrides.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from ..common import DeviceOpOverrides, register_device_op_overrides + + +class MTIADeviceOpOverrides(DeviceOpOverrides): + def import_get_raw_stream_as(self, name: str) -> str: + return f"from torch._C import _mtia_getCurrentRawStream as {name}" + + def set_device(self, device_idx: int) -> str: + return f"torch.mtia.set_device({device_idx})" + + def synchronize(self) -> str: + return "torch.mtia.synchronize()" + + def device_guard(self, device_idx: int) -> str: + return f"torch.mtia.device({device_idx})" + + +register_device_op_overrides("mtia", MTIADeviceOpOverrides()) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/multi_kernel.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/multi_kernel.py new file mode 100644 index 0000000000000000000000000000000000000000..f4ba4ad542d5557e4fcc74c33b1d3929d6f48897 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/multi_kernel.py @@ -0,0 +1,610 @@ +# mypy: allow-untyped-defs +import functools +import logging +import math +import os +import pathlib +from typing import Any + +from torch._inductor.ir import MultiTemplateBuffer +from torch._inductor.metrics import get_metric_table, is_metric_table_enabled +from torch.utils._ordered_set import OrderedSet + +from .. import config +from ..codecache import code_hash, CodeCacheFuture, get_path, write_atomic +from ..runtime.benchmarking import benchmarker +from ..utils import cache_on_self, IndentedBuffer +from ..virtualized import V +from .common import TensorArg, WorkspaceArg + + +log = logging.getLogger(__name__) + + +class MultiKernelState: + """ + Maintain state of multi-kernel compilation so we don't define duplicated + multi-kernel for the same set of sub-kernels. + + V.graph.wrapper_code has a reference to MultiKernelState instance. + """ + + def __init__(self): + self.subkernel_to_kernel_name = {} + self.kernel_defs = IndentedBuffer() + + def define_kernel( + self, + kernels: list[Any], + kernel_shape_keys: list[None | tuple[tuple[int, ...], ...]] | None = None, + ) -> str: + """ + Previously we name the multi kernel as "multi_kernel_{kernel_names[0]}". + This has some minor issue. + + E.g. for persistent reduction https://gist.github.com/shunting314/39e7c00ff8bb2055942ed5a3255d61ca , + there are 2 flavors of non-persistent reduction: + https://gist.github.com/shunting314/056d43d35907e87efb883970b35c17d4 + and + https://gist.github.com/shunting314/02ee753b65c513c54e695626afe682bd + + The only different is cache eviction policy. + + We should name the multi-kernel differently in these 2 cases. + + kernels: + A list of kernels + kernel_shape_keys: + Specified for size-hint multi-kernels. + Each list element is a shape key, corresponding to the concrete input & output size hints each kernel was tuned for. + """ + # Prevent circular import + from ..select_algorithm import TritonTemplateKernel + + kernel_names = tuple(k.kernel_name for k in kernels) + if kernel_names in self.subkernel_to_kernel_name: + return self.subkernel_to_kernel_name[kernel_names] + + # name the multi kernel based on the first kernel + multi_kernel_name = f"multi_kernel_{len(self.subkernel_to_kernel_name)}" + self.subkernel_to_kernel_name[kernel_names] = multi_kernel_name + + if V.graph.cpp_wrapper and not config.triton.autotune_at_compile_time: + # we should not generate any python code for multi-kernel during + # the second pass of cpp-wrapper. + return multi_kernel_name + + arg_index: dict[int, list[slice]] = {} + _, call_args, _, arg_types = kernels[0].args.python_argdefs() + if isinstance(kernels[0], TritonTemplateKernel) and isinstance( + kernels[0].output_node, MultiTemplateBuffer + ): + for i, kernel in enumerate(kernels): + additional_call_args, _ = kernel.additional_call_args_and_types() + if i not in arg_index: + arg_index[i] = [] + arg_index[i].append(slice(0, len(call_args))) + arg_index[i].append( + slice( + len(call_args) + i * len(additional_call_args), + len(call_args) + (i + 1) * len(additional_call_args), + ) + ) + else: + kernels[0].add_numel_to_call_args(multi_kernel_name, call_args, arg_types) + for i in range(len(kernels)): + arg_index[i] = [slice(0, len(call_args))] + + keyed_by_sizes = kernel_shape_keys is not None + buf = self.kernel_defs + buf.writeline("") + buf.writeline("arg_index = {") + for key, slice_list in arg_index.items(): + slice_reprs = ", ".join(repr(s) for s in slice_list) + buf.writeline(f" {key}: [{slice_reprs}],") + buf.writeline("}") + + if not keyed_by_sizes: # no size hint keys, just call with list of kernels + buf.writeline( + f"{multi_kernel_name} = async_compile.multi_kernel({multi_kernel_name!r}, [" + ) + with buf.indent(): + for name in kernel_names: + buf.writeline(f"{name},") + buf.writeline("], arg_index=arg_index)") + else: # call with dict[size hint key, kernel] + assert isinstance(kernels[0], TritonTemplateKernel) + assert isinstance(kernel_shape_keys, list) + assert len(kernels) == len(kernel_shape_keys) + buf.writeline( + f"{multi_kernel_name} = async_compile.size_hint_multi_kernel({multi_kernel_name!r}, {{" + ) + with buf.indent(): + for shape_key, name in zip(kernel_shape_keys, kernel_names): + buf.writeline(f"{shape_key}: {name},") + buf.writeline("}, arg_index=arg_index)") + + if config.triton.autotune_at_compile_time: + V.graph.wrapper_code.src_to_kernel["\n".join(kernel_names)] = ( + multi_kernel_name + ) + + return multi_kernel_name + + +class MultiKernel: + """ + This class maintains the compile time state for multi kernels. + + Assume we do codegen for a MultiKernel encapsulating kernel1 and kernel2. + The generated definition for the multi-kernel will looks like: + ``` + multi_kernel_kernel1 = MultiKernelCall( + [kernel1, kernel2], multi_kernel_definition_code + ) + ``` + + Here is a concrete example: https://gist.github.com/shunting314/d9f3fb6bc6cee3dbae005825ca196d39 + """ + + def __init__(self, kernels): + assert len(kernels) >= 2 + + self.kernels = kernels + self.kernel_name = V.graph.wrapper_code.multi_kernel_state.define_kernel( + kernels + ) + + # need this since some code in inductor check if the kernel object has an args + # attribute to decide if it's a non-null kernel. + self.args = object() + + @staticmethod + def _merge_workspace_args(left: list[WorkspaceArg], right: list[WorkspaceArg]): + if left == right: + return left + result = {x.inner_name: x for x in left} + for arg in right: + if arg.inner_name in result: + result[arg.inner_name] = WorkspaceArg.maximum( + result[arg.inner_name], arg + ) + else: + result[arg.inner_name] = arg + return [*result.values()] + + @staticmethod + def merge_workspaces_inplace(kernels): + if len(kernels) < 2: + return + # All kernels must share the same workspace + workspace_args = functools.reduce( + MultiKernel._merge_workspace_args, + [kernel.args.workspace_args for kernel in kernels], + ) + for kernel in kernels: + kernel.args.workspace_args = workspace_args + return workspace_args + + def call_kernel(self, kernel_name): + """ + Collect the union of arguments from all subkernels as the arguments + for the multi-kernel. + """ + # Prevent circular import + from ..select_algorithm import TritonTemplateKernel + + assert kernel_name == self.kernel_name + V.graph.wrapper_code.write_triton_header_once() + _, call_args, _, arg_types = self.kernels[0].args.python_argdefs() + for kernel in self.kernels[1:]: + _, other_call_args, _, other_arg_types = kernel.args.python_argdefs() + assert call_args == other_call_args, (call_args, other_call_args) + assert arg_types == other_arg_types + + if V.graph.cpp_wrapper and not config.triton.autotune_at_compile_time: + # for the second pass of cpp-wrapper codegen, we should call + # the fast kernel directly + kernel_name = MultiKernelCall.lookup_choice(self.kernel_name) + + if isinstance(self.kernels[0], TritonTemplateKernel) and isinstance( + self.kernels[0].output_node, MultiTemplateBuffer + ): + # For matmuls the grid arguments are passed in as additional arguments + # to the kernel run method. These grids change based on the various + # parameters of the matmul. So we need to pass each kernel's grid into + # the multi call kernel. + multi_call_args = call_args + multi_call_arg_types = arg_types + for kernel in self.kernels: + additional_call_args, additional_arg_types = ( + kernel.additional_call_args_and_types() + ) + multi_call_args.extend(list(additional_call_args)) + multi_call_arg_types.extend(list(additional_arg_types)) + else: + # numels for all subkernels should be the same. Use kernels[0] here + self.kernels[0].add_numel_to_call_args(kernel_name, call_args, arg_types) + multi_call_args = call_args + multi_call_arg_types = arg_types + + for ws in self.kernels[0].args.workspace_args: + V.graph.wrapper_code.generate_workspace_allocation(ws) + + if V.graph.cpp_wrapper: + # We have already selected the best kernel at compile time + # so we only have one set of call args. NB: this currently + # doesn't work with MultiTemplateBuffer kernels. @bobrenjc93 + # will add it in a subsequent PR. + V.graph.wrapper_code.generate_kernel_call( + kernel_name, call_args, arg_types=arg_types + ) + else: + V.graph.wrapper_code.generate_kernel_call( + kernel_name, multi_call_args, arg_types=multi_call_arg_types + ) + + for ws in reversed(self.kernels[0].args.workspace_args): + V.graph.wrapper_code.generate_workspace_deallocation(ws) + + def codegen_nan_check(self): + wrapper = V.graph.wrapper_code + seen: OrderedSet[str] = OrderedSet() + for k in self.kernels: + _, call_args, precompile_args, _ = k.args.python_argdefs() + for arg, precompile_arg in zip(call_args, precompile_args): + if arg in seen: + continue + seen.add(arg) + if isinstance(precompile_arg, TensorArg): + line = f"assert not {arg}.isnan().any().item()" + wrapper.writeline(line) + line = f"assert not {arg}.isinf().any().item()" + wrapper.writeline(line) + + @property + def removed_buffers(self): + return OrderedSet.intersection(*[k.removed_buffers for k in self.kernels]) + + @property + def inplaced_to_remove(self): + return OrderedSet.intersection(*[k.inplaced_to_remove for k in self.kernels]) + + @property + @cache_on_self + def inplace_update_buffers(self): + """ + Make sure all kernels have the same inplace update mappings. + """ + for k in self.kernels[1:]: + assert k.inplace_update_buffers == self.kernels[0].inplace_update_buffers + return self.kernels[0].inplace_update_buffers + + def warn_mix_layout(self, kernel_name: str): + pass + + +class MultiKernelCall: + """ + This class is called at run time to actually run the kernel + """ + + def __init__(self, multi_kernel_name, kernels, arg_index): + assert len(kernels) >= 1 + self._kernels = kernels + self.multi_kernel_name = multi_kernel_name + + self.disable_cache = os.environ.get( + "TORCHINDUCTOR_DISABLE_MULTI_KERNEL_CACHE" + ) == "1" or is_metric_table_enabled("persistent_red_perf") + + self.picked_kernel = None + self.arg_index = arg_index + if config.triton.multi_kernel > 1: + # manually force a subkernel to ease perf testing + picked_by_config = config.triton.multi_kernel - 2 + assert picked_by_config < len(self._kernels) + # pyrefly: ignore [bad-assignment] + self.picked_kernel = picked_by_config + elif not self.disable_cache: + self.load_cache() + + self._recorded = False + + def cache_file_path(self): + key = code_hash( + ",".join( + [ + f"{k.fn.cache_key}{k.size_hints!r}{k.triton_meta!r}" + for k in self.kernels + ] + ) + ) + _, _, path = get_path(key, "picked_kernel") + return pathlib.Path(path) + + def load_cache(self): + assert self.picked_kernel is None + path = self.cache_file_path() + if path.exists(): + with path.open() as fd: + # pyrefly: ignore [bad-assignment] + self.picked_kernel = int(fd.read()) + # pyrefly: ignore [unsupported-operation] + assert self.picked_kernel >= 0 and self.picked_kernel < len( + self._kernels + ) + log.debug( + "Load picked kernel %d from cache file %s", self.picked_kernel, path + ) + + def store_cache(self): + assert self.picked_kernel is not None + path = self.cache_file_path() + path.parent.mkdir(parents=True, exist_ok=True) + + write_atomic(path, str(self.picked_kernel)) + log.debug("Store picked kernel %d to cache file %s", self.picked_kernel, path) + + @property + def kernels(self): + """ + Read results from future. + + This should be called after parallel compilation is done. + In case you call this before compilation is done, + it may slow down the parallel compilation. + """ + for i, kernel in enumerate(self._kernels): + if isinstance(kernel, CodeCacheFuture): + self._kernels[i] = kernel.result() + + return self._kernels + + def benchmark_sub_kernels(self, *args, **kwargs): + """ + Benchmark all the sub kernels and return the execution time + (in milliseconds) for each of time. + + Unit test may mock this method to force a specific kernel to + be picked. + """ + + def wrap_fn(kernel, index): + def inner(): + filtered_args = self._get_filtered_args(args, index) + args_clone, kwargs_clone = kernel.clone_args(*filtered_args, **kwargs) + return kernel.run(*args_clone, **kwargs_clone) + + return inner + + return [ + benchmarker.benchmark( + wrap_fn(kernel, index), + # Currently the kernel type must be a CachingAutotuner + device=kernel.device_props.type, + rep=40, + ) + for index, kernel in enumerate(self.kernels) + ] + + def _get_filtered_args(self, args, index): + """ + We pass in all arguments to all kernels into the MultiKernelCall + so when invoking a particular kernel we need to filter to only the + arguments for that specific kernel. + """ + + # This is sometimes invoked at runtime where V.graph is + # a NullHandler + if hasattr(V.graph, "cpp_wrapper") and V.graph.cpp_wrapper: + # for cpp-wrapper, we should not filter the args since + # we already have chosen a single kernel and arg set. + return args + return [item for s in self.arg_index[index] for item in args[s]] + + # record_choice and lookup_choice are helper functions for cpp-wrapper + # codegen. The first pass use record_choice to keep the choice and + # the second pass do lookup by calling lookup_choice. + # + # An alternative that reused the multi-kernel cache does not work well + # since during codegen of the second pass, it's very hard to know the + # path for the cache file. Also reading the cache file need do some IO + # which can be slower. + @staticmethod + def record_choice(multi_kernel_name: str, picked_kernel_name: str): + """ + Record the multi-kernel choice for cpp-wrapper after autotuning + + We should do nothing if this function is not called during codegen. + """ + from torch._inductor.graph import GraphLowering + + if not isinstance(V.graph, GraphLowering): + return + + if not V.graph.record_multi_kernel_choice: + return + + V.graph.multi_kernel_to_choice[multi_kernel_name] = picked_kernel_name + + @staticmethod + def lookup_choice(multi_kernel_name: str) -> str: + # this should always been done during cpp-wrapper codegen + assert ( + V.graph.record_multi_kernel_choice + and multi_kernel_name in V.graph.multi_kernel_to_choice + ) + # there should be no miss + return V.graph.multi_kernel_to_choice[multi_kernel_name] + + def run(self, *args, **kwargs): + if self.picked_kernel is None: + timings = self.benchmark_sub_kernels(*args, **kwargs) + self.picked_kernel = timings.index(min(timings)) + k0 = self.kernels[0] + log.debug( + "pick %dth sub-kernel in %s. Size hints %s. Reduction hint %s. Timings %s", + self.picked_kernel, + [k.inductor_meta.get("kernel_name") for k in self.kernels], + k0.size_hints, + k0.inductor_meta.get("reduction_hint"), + timings, + ) + get_metric_table("persistent_red_perf").add_row( + functools.partial(self._metrics_table_row, timings) + ) + + if not self.disable_cache: + self.store_cache() + + if not self._recorded: + self._recorded = True + picked_kernel_name = self.kernels[self.picked_kernel].inductor_meta.get( + "kernel_name" + ) + assert picked_kernel_name is not None + self.record_choice(self.multi_kernel_name, picked_kernel_name) + + run = self.kernels[self.picked_kernel].run # type: ignore[method-assign] + filtered_args = self._get_filtered_args(args, self.picked_kernel) + run(*filtered_args, **kwargs) + + def _metrics_table_row(self, timings): + def get_kernel_path(k): + return k.fn.fn.__code__.co_filename + + k0 = self.kernels[0] + row = { + "size_hints": k0.size_hints, + "reduction_hint": k0.inductor_meta.get("reduction_hint"), + } + max_kernels = 4 + assert len(timings) <= max_kernels + for i in range(max_kernels): + if i < len(self.kernels): + row[f"kernel{i}_path"] = get_kernel_path(self.kernels[i]) + row[f"kernel{i}_latency"] = timings[i] + else: + row[f"kernel{i}_path"] = "" + row[f"kernel{i}_latency"] = "" + return row + + +class SizeHintMultiKernel(MultiKernel): + """ + Version of multi-kernel that generates kernels based on specified size hints. + Currently only performs 1-d search over hints; doesn't perform combinatorial n-d search + if n > 1 dynamic dimensions are specified. + + e.g. matmul([s0, s1], [s1, s2]) with size-hints [64, 256] only generates 2 kernels, + based on tuning shapes ([64, 64], [64, 64]) and ([256, 256], [256, 256]) + """ + + def __init__(self, kernels): + assert isinstance(kernels, dict) and len(kernels) >= 1 + + self.kernels, self.kernel_shape_keys = [], [] + for shape_key, kernel in kernels.items(): + self.kernels.append(kernel) + self.kernel_shape_keys.append(shape_key) + self.kernel_name = V.graph.wrapper_code.multi_kernel_state.define_kernel( + self.kernels, self.kernel_shape_keys + ) + + # need this since some code in inductor check if the kernel object has an args + # attribute to decide if it's a non-null kernel. + self.args = object() + + +class SizeHintMultiKernelCall(MultiKernelCall): + """ + Runtime class for size-hint multi-kernels. + Instead of having a plain list of kernels to benchmark over, keys them by input & output shapes, + and optionally perform shape-based selection. The pre-generated kernel is chosen based on the shape keys, + with the heuristic being log2 l1 distance between the pre-generated / runtime input & output shapes. + """ + + def __init__(self, multi_kernel_name, kernels, arg_index): + super().__init__(multi_kernel_name, list(kernels.values()), arg_index) + self._kernel_hints = list(kernels.keys()) + + # Caches results for unique shapes. + self._shape_cache = {} + + def _get_shape_cache_key(self, *args, **kwargs): + """ + Generate a cache key based on tensor shapes for shape-specialized dispatch. + """ + shapes = [] + for arg in args: + if hasattr(arg, "shape"): + shapes.append(tuple(arg.shape)) + return tuple(shapes) + + def _get_cached_shape_choice(self, cache_key): + """ + Get cached kernel choice for a specific shape. + """ + return self._shape_cache.get(cache_key) + + def _cache_shape_choice(self, cache_key, kernel_idx): + """ + Cache kernel choice for a specific shape. + """ + self._shape_cache[cache_key] = kernel_idx + + def _dist_heuristic(self, k1, k2): + """ + log2 L1 distance heuristic for kernel selection. + """ + + def dist(x, y): + lx = math.log2(x) if x > 0 else -1 + ly = math.log2(y) if y > 0 else -1 + return abs(lx - ly) + + out = 0 + for s1, s2 in zip(k1, k2): + out += sum(dist(x, y) for x, y in zip(s1, s2)) + return out + + def run(self, *args, **kwargs): + cache_key = self._get_shape_cache_key(*args, **kwargs) + cached_choice = self._get_cached_shape_choice(cache_key) + if cached_choice is not None: + self.picked_kernel = cached_choice + log.debug( + "using cached shape-specialized choice %dth sub-kernel in %s. Cache key: %s", + self.picked_kernel, + [k.inductor_meta.get("kernel_name") for k in self.kernels], + cache_key, + ) + else: + self._select_kernel_by_shape(*args, **kwargs) + + if not self._recorded: + self._recorded = True + picked_kernel_name = self.kernels[self.picked_kernel].inductor_meta.get( + "kernel_name" + ) + assert picked_kernel_name is not None + self.record_choice(self.multi_kernel_name, picked_kernel_name) + + run = self.kernels[self.picked_kernel].run # type: ignore[method-assign] + filtered_args = self._get_filtered_args(args, self.picked_kernel) + run(*filtered_args, **kwargs) + + def _select_kernel_by_shape(self, *args, **kwargs): + """ + Benchmark kernels for a particular shape and return the + best kernel for this shape. + """ + shape_key = self._get_shape_cache_key(*args, **kwargs) + dists = [ + self._dist_heuristic(shape_key, key) if key is not None else 2**62 + for key in self._kernel_hints + ] + # pyrefly: ignore [bad-assignment] + self.picked_kernel = dists.index(min(dists)) + self._cache_shape_choice(shape_key, self.picked_kernel) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/nv_universal_gemm/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/nv_universal_gemm/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..51bb97c9b2bb11db570dd652b1a1d7d57589c8b6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/nv_universal_gemm/__init__.py @@ -0,0 +1,17 @@ +# mypy: allow-untyped-defs +from .nv_universal_gemm import ( + add_nv_universal_gemm_choices, + add_nv_universal_grouped_gemm_choices, + add_nv_universal_scaled_gemm_choices, + GemmVariant, + NVUniversalGemmCaller, +) + + +__all__ = [ + "GemmVariant", + "NVUniversalGemmCaller", + "add_nv_universal_gemm_choices", + "add_nv_universal_grouped_gemm_choices", + "add_nv_universal_scaled_gemm_choices", +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/nv_universal_gemm/kernel_cache.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/nv_universal_gemm/kernel_cache.py new file mode 100644 index 0000000000000000000000000000000000000000..f0a8cf924b6bd0bdd23ae3a2513e5aaef3e5b623 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/nv_universal_gemm/kernel_cache.py @@ -0,0 +1,96 @@ +# mypy: allow-untyped-defs +""" +Global kernel cache for NVIDIA Universal GEMM. + +This module provides a lazy-initialized cache for cutlass_api kernels, +avoiding expensive manifest scans on every kernel lookup. + +The first call to get_kernel_by_name() loads all kernels from cutlass_api +(~10 seconds) and builds a name->kernel dict. Subsequent calls use the +dict for O(1) lookup (~0.1 μs). +""" + +import logging +from collections.abc import Callable +from typing import Any + + +log = logging.getLogger(__name__) + +# Global cache: kernel_name -> kernel object +_kernel_by_name_cache: dict[str, Any] | None = None + + +def _build_kernel_cache() -> dict[str, Any]: + """Build the kernel name -> kernel object cache.""" + import cutlass_api + + log.debug("Building NVGEMM kernel cache (this may take a few seconds)...") + + try: + from torch._inductor.kernel.vendored_templates.cutedsl import ( # noqa: F401 + wrappers, + ) + except ImportError: + log.debug("Vendored kernel wrappers not available") + + all_kernels = cutlass_api.get_kernels() + cache = {k.metadata.kernel_name: k for k in all_kernels} + log.debug("NVGEMM kernel cache built: %d kernels", len(cache)) + return cache + + +def get_compatible_kernels( + args: Any, + cc: int, + metadata_filter: Callable[[Any], bool] | None = None, +) -> list[Any]: + """Get kernels compatible with the given arguments from the cache.""" + global _kernel_by_name_cache + + if _kernel_by_name_cache is None: + _kernel_by_name_cache = _build_kernel_cache() + + compatible = [] + for kernel in _kernel_by_name_cache.values(): + if kernel.metadata.min_cc > cc: + continue + + if metadata_filter is not None and not metadata_filter(kernel.metadata): + continue + + status = kernel.supports(args) + if status.error is not None: + continue + compatible.append(kernel) + + log.debug( + "Found %d compatible kernels from cache of %d total", + len(compatible), + len(_kernel_by_name_cache), + ) + return compatible + + +def get_kernel_by_name(kernel_name: str) -> Any: + """Get a cutlass_api kernel by name using the global cache.""" + global _kernel_by_name_cache + + if _kernel_by_name_cache is None: + _kernel_by_name_cache = _build_kernel_cache() + + return _kernel_by_name_cache.get(kernel_name) + + +def ensure_cache_initialized() -> None: + """Ensure the kernel cache is initialized.""" + global _kernel_by_name_cache + + if _kernel_by_name_cache is None: + _kernel_by_name_cache = _build_kernel_cache() + + +def clear_cache() -> None: + """Clear the kernel cache.""" + global _kernel_by_name_cache + _kernel_by_name_cache = None diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/nv_universal_gemm/nv_universal_gemm.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/nv_universal_gemm/nv_universal_gemm.py new file mode 100644 index 0000000000000000000000000000000000000000..bf934fab6bc95baff9f57e4ea24a8d01c581db12 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/nv_universal_gemm/nv_universal_gemm.py @@ -0,0 +1,555 @@ +# mypy: allow-untyped-defs +""" +NVIDIA Universal GEMM (NVGEMM) backend for PyTorch Inductor. + +This module provides integration with the cutlass_api library to enable +high-performance GEMM kernels for NVIDIA GPUs. +""" + +import itertools +from enum import auto, Enum +from typing import Any + +import torch +from torch._inductor import config +from torch._inductor.autotune_process import ( + BenchmarkRequest, + GPUDeviceBenchmarkMixin, + TensorMeta, +) +from torch._inductor.codegen.cuda.cuda_env import get_cuda_arch +from torch._inductor.codegen.nv_universal_gemm.nv_universal_gemm_utils import ( + to_cutlass_scale_mode, +) +from torch._inductor.ir import Buffer, ChoiceCaller, Layout, TensorBox +from torch._inductor.kernel_inputs import MMKernelInputs +from torch._inductor.template_heuristics.nv_universal_gemm import get_nvgemm_heuristics +from torch._inductor.utils import ensure_nv_universal_gemm_available +from torch._logging import getArtifactLogger + + +log = getArtifactLogger(__name__, "output_code") + + +class GemmVariant(Enum): + """ + Enum for different GEMM operation types supported by NVIDIA Universal GEMM. + """ + + GEMM = auto() + + GROUPED_GEMM = auto() + + SCALED_GEMM = auto() + + @property + def op_name(self) -> str: + """Return the operation name for logging and naming.""" + if self == GemmVariant.GROUPED_GEMM: + return "nv_universal_grouped_gemm" + if self == GemmVariant.SCALED_GEMM: + return "nv_universal_scaled_gemm" + return "nv_universal_gemm" + + @property + def arguments_class_name(self) -> str: + """Return the cutlass_api arguments class name.""" + if self == GemmVariant.GROUPED_GEMM: + return "GroupedGemmArguments" + return "GemmArguments" + + +class NVUniversalGemmBenchmarkRequest(GPUDeviceBenchmarkMixin, BenchmarkRequest): + """Benchmark request for NVIDIA Universal GEMM kernels.""" + + def __init__( + self, + kernel_name: str, + input_tensor_meta: TensorMeta | list[TensorMeta], + output_tensor_meta: TensorMeta | list[TensorMeta], + kernel, # cutlass_api.Kernel object + accumulator_type: torch.dtype, + variant: GemmVariant, + workspace_size: int = 0, + scale_type_a: Any | None = None, + scale_type_b: Any | None = None, + swizzle_type_a: Any | None = None, + swizzle_type_b: Any | None = None, + ) -> None: + super().__init__(kernel_name, input_tensor_meta, output_tensor_meta, ()) + self.kernel = kernel + self.accumulator_type = accumulator_type + self._compiled_artifact = None + self._workspace: torch.Tensor | None = None + self.workspace_size = workspace_size + self.variant = variant + self.scale_type_a = scale_type_a + self.scale_type_b = scale_type_b + self.swizzle_type_a = swizzle_type_a + self.swizzle_type_b = swizzle_type_b + + def benchmark( + self, + *input_tensors: torch.Tensor, + out: torch.Tensor | None = None, + ) -> float: + """Benchmark the NVIDIA Universal GEMM kernel. + + Override the base class to always create tensors from input_tensor_meta. + This is necessary because input_nodes may be ReinterpretViews that share + the same underlying buffer name. The autotuning framework deduplicates + inputs by name (in AlgorithmSelectorCache.get_inputs()), resulting in + fewer tensors than expected. By always creating from input_tensor_meta, + we ensure each input gets its own tensor with the correct size/stride/offset + from the view's layout. + """ + # Always create tensors from input_tensor_meta, ignoring passed-in tensors + input_tensors = tuple(x.to_tensor() for x in self.input_tensor_meta) + if out is None: + out = self.output_tensor_meta.to_tensor() + + fn = self.make_run_fn(*input_tensors, out=out) + return self.do_bench(fn, *input_tensors, out=out) + + def make_run_fn(self, *input_tensors: torch.Tensor, out: torch.Tensor): + """Create a function to run the NVIDIA Universal GEMM kernel.""" + import cutlass_api + + args = self._create_gemm_arguments(cutlass_api, input_tensors, out) + + if self._compiled_artifact is None: + self._compiled_artifact = self.kernel.compile(args) + artifact = self._compiled_artifact + kernel = self.kernel + + # Allocate workspace if needed + if self.workspace_size > 0: + self._workspace = torch.empty( + self.workspace_size, device=out.device, dtype=torch.int8 + ) + else: + self._workspace = None + + workspace = self._workspace + + def run_kernel(): + stream = torch.cuda.current_stream() + kernel.run( + args, + artifact, + stream=stream, + workspace=workspace, + assume_supported_args=True, + ) + + return run_kernel + + def _create_gemm_arguments(self, cutlass_api, input_tensors, out): + """Create the appropriate GemmArguments based on variant.""" + if self.variant == GemmVariant.GROUPED_GEMM: + a, b, offsets = input_tensors + return cutlass_api.arguments.GroupedGemmArguments( + a, + b, + out, + accumulator_type=self.accumulator_type, + offsets=offsets, + ) + elif self.variant == GemmVariant.SCALED_GEMM: + from cutlass_api.arguments import ScaledTensor + + scale_mode_a, swizzle_mode_a = to_cutlass_scale_mode( + self.scale_type_a, self.swizzle_type_a + ) + scale_mode_b, swizzle_mode_b = to_cutlass_scale_mode( + self.scale_type_b, self.swizzle_type_b + ) + + a, b, scale_a, scale_b = input_tensors + scaled_a = ScaledTensor(a, scale_a, scale_mode_a, swizzle_mode_a) + scaled_b = ScaledTensor(b, scale_b, scale_mode_b, swizzle_mode_b) + return cutlass_api.arguments.GemmArguments( + scaled_a, + scaled_b, + out, + accumulator_type=self.accumulator_type, + ) + else: + a, b = input_tensors + return cutlass_api.arguments.GemmArguments( + a, + b, + out, + accumulator_type=self.accumulator_type, + ) + + def cleanup_run_fn(self) -> None: + self._workspace = None + + +class NVUniversalGemmCaller(ChoiceCaller): + """ + ChoiceCaller for NVIDIA Universal GEMM kernels. + + Wraps a cutlass_api kernel and integrates with Inductor's autotuning. + """ + + index_counter = itertools.count() + + def __init__( + self, + name: str, + input_nodes: list[Buffer], + layout: Layout, + kernel, # cutlass_api.Kernel object + accumulator_type: torch.dtype, + variant: GemmVariant, + workspace_size: int = 0, + scale_type_a: Any | None = None, + scale_type_b: Any | None = None, + swizzle_type_a: Any | None = None, + swizzle_type_b: Any | None = None, + ) -> None: + super().__init__( + name=name, + input_nodes=input_nodes, + layout=layout, + description=f"{variant.op_name} {kernel.metadata.kernel_name}", + ) + self.kernel = kernel + self.accumulator_type = accumulator_type + self.workspace_size = workspace_size + self.variant = variant + self.scale_type_a = scale_type_a + self.scale_type_b = scale_type_b + self.swizzle_type_a = swizzle_type_a + self.swizzle_type_b = swizzle_type_b + + output_buffer = Buffer(name=f"{variant.op_name}_out", layout=layout) + + self.bmreq = NVUniversalGemmBenchmarkRequest( + kernel_name=name, + input_tensor_meta=TensorMeta.from_irnodes(input_nodes), + output_tensor_meta=TensorMeta.from_irnodes(output_buffer), + kernel=kernel, + accumulator_type=accumulator_type, + workspace_size=workspace_size, + variant=variant, + scale_type_a=scale_type_a, + scale_type_b=scale_type_b, + swizzle_type_a=swizzle_type_a, + swizzle_type_b=swizzle_type_b, + ) + + def __str__(self) -> str: + return f"NVUniversalGemmCaller({self.kernel.metadata.kernel_name})" + + def benchmark(self, *args, out) -> float: + return self.bmreq.benchmark(*args, out=out) + + def output_node(self) -> TensorBox: + from torch._inductor.ir import NVUniversalGemmBuffer + + buffer = NVUniversalGemmBuffer( + layout=self.layout, + inputs=self.input_nodes, + kernel=self.kernel, + accumulator_type=self.accumulator_type, + workspace_size=self.workspace_size, + variant=self.variant, + scale_type_a=self.scale_type_a, + scale_type_b=self.scale_type_b, + swizzle_type_a=self.swizzle_type_a, + swizzle_type_b=self.swizzle_type_b, + ) + # Pass KTC annotation to the buffer for encoding + if "ktc" in self.annotations: + buffer.annotations["ktc"] = self.annotations["ktc"] + return TensorBox.create(buffer) + + def call_name(self) -> str: + return self.name + + def to_callable(self): + return self.bmreq.make_run_fn + + def hash_key(self) -> str: + return f"{self.variant.op_name}_{self.kernel.metadata.kernel_name}" + + def info_dict(self) -> dict[str, Any]: + return { + "name": self.name, + "backend": self.variant.op_name, + "kernel_name": self.kernel.metadata.kernel_name, + } + + +def _create_dummy_tensor_from_layout(layout: Layout) -> torch.Tensor | None: + """ + Create a FakeTensor from a Layout for kernel filtering. + + Uses Layout.get_example() which creates FakeTensors within V.fake_mode, + avoiding real CUDA memory allocation. cutlass_api only needs shape/stride/dtype + metadata for its supports() checks. + """ + try: + return layout.get_example() + except Exception: + return None + + +def _exclude_efc_kernels(metadata) -> bool: + """ + Filter out EFC kernels. + + EFC kernels support custom epilogue operations but have additional overhead. + Since NVGEMM doesn't support epilogue fusion yet (see nv_universal_gemm_scheduling.py), + we use non-EFC kernels which are equivalent for identity epilogue (out = acc). + TODO(nikhilap): Remove this filter once NVGEMM supports epilogue fusion. + """ + return "EFC" not in metadata.kernel_class.__name__ + + +def _add_nv_gemm_choices_impl( + choices: list[ChoiceCaller], + layout: Layout, + input_nodes: list[Buffer], + variant: GemmVariant, + accumulator_type: torch.dtype, + mm_inputs: MMKernelInputs | None = None, + scale_type_a: Any | None = None, + scale_type_b: Any | None = None, + swizzle_type_a: Any | None = None, + swizzle_type_b: Any | None = None, +) -> None: + """ + Unified implementation for adding NVIDIA Universal GEMM choices. + + Args: + choices: List to append ChoiceCaller objects to + layout: Output layout + input_nodes: Input tensor nodes + variant: The GEMM variant (determines behavior) + accumulator_type: Accumulator dtype + mm_inputs: Optional MMKernelInputs for heuristics + scale_type_a: ScalingType for A (required for SCALED_GEMM) + scale_type_b: ScalingType for B (required for SCALED_GEMM) + swizzle_type_a: SwizzleType for A (required for SCALED_GEMM) + swizzle_type_b: SwizzleType for B (required for SCALED_GEMM) + """ + import cutlass_api + + from torch._inductor.codegen.nv_universal_gemm.kernel_cache import ( + get_compatible_kernels, + ) + + # Create dummy tensors for cutlass_api's supports() checks + dummy_tensors = [ + _create_dummy_tensor_from_layout(node.get_layout()) for node in input_nodes + ] + out_tensor = _create_dummy_tensor_from_layout(layout) + + if any(t is None for t in dummy_tensors) or out_tensor is None: + log.debug("Failed to create dummy tensors for %s", variant.op_name) + return + + if variant == GemmVariant.GROUPED_GEMM: + a_tensor, b_tensor, offs_tensor = dummy_tensors + assert b_tensor is not None + args = cutlass_api.arguments.GroupedGemmArguments( + a_tensor, + b_tensor, + out_tensor, + accumulator_type=accumulator_type, + offsets=offs_tensor, + ) + elif variant == GemmVariant.SCALED_GEMM: + from cutlass_api.arguments import ScaledTensor + + scale_mode_a, swizzle_mode_a = to_cutlass_scale_mode( + scale_type_a, swizzle_type_a + ) + scale_mode_b, swizzle_mode_b = to_cutlass_scale_mode( + scale_type_b, swizzle_type_b + ) + if scale_mode_a is None or scale_mode_b is None: + return + + a_tensor, b_tensor, scale_a_tensor, scale_b_tensor = dummy_tensors + scaled_a = ScaledTensor( + a_tensor, + scale_a_tensor, + scale_mode_a, + swizzle_mode_a, + ) + scaled_b = ScaledTensor( + b_tensor, + scale_b_tensor, + scale_mode_b, + swizzle_mode_b, + ) + args = cutlass_api.arguments.GemmArguments( + scaled_a, + scaled_b, + out_tensor, + accumulator_type=accumulator_type, + ) + else: + a_tensor, b_tensor = dummy_tensors + args = cutlass_api.arguments.GemmArguments( + a_tensor, + b_tensor, + out_tensor, + accumulator_type=accumulator_type, + ) + + cc = get_cuda_arch() + if cc is None: + log.debug("Failed to get CUDA arch") + return + cc_int = int(cc) + + kernels = get_compatible_kernels(args, cc_int, metadata_filter=_exclude_efc_kernels) + if not kernels: + log.debug("No compatible %s kernels found", variant.op_name) + return + + max_configs = config.nvgemm_max_profiling_configs or len(kernels) + if variant in (GemmVariant.GEMM, GemmVariant.SCALED_GEMM) and mm_inputs is not None: + heuristics = get_nvgemm_heuristics() + kernels = heuristics.filter_kernels( + kernels, mm_inputs, max_configs, accumulator_type + ) + else: + # TODO(nikhilap): Enable heuristics for grouped GEMM + # when nvMatmulHeuristics adds support + kernels = kernels[:max_configs] + + # Add callers for each kernel + num_added = 0 + for kernel in kernels: + name = f"{variant.op_name}_{next(NVUniversalGemmCaller.index_counter)}" + workspace_size = kernel.get_workspace_size(args) + try: + caller = NVUniversalGemmCaller( + name=name, + input_nodes=input_nodes, + layout=layout, + kernel=kernel, + accumulator_type=accumulator_type, + workspace_size=workspace_size, + variant=variant, + scale_type_a=scale_type_a, + scale_type_b=scale_type_b, + swizzle_type_a=swizzle_type_a, + swizzle_type_b=swizzle_type_b, + ) + choices.append(caller) + num_added += 1 + except Exception: + log.debug("Failed to create %s choice", variant.op_name, exc_info=True) + + log.debug("Added %d %s choices", num_added, variant.op_name) + + +def add_nv_universal_gemm_choices( + choices: list[ChoiceCaller], + layout: Layout, + inputs: MMKernelInputs, + accumulator_type: torch.dtype | None = None, +) -> None: + """ + Add NVIDIA Universal GEMM kernels to the autotune choices. + + Thin wrapper around _add_nv_gemm_choices_impl for regular GEMM. + """ + if not ensure_nv_universal_gemm_available(): + log.debug("cutlass_api not available, skipping NVIDIA Universal GEMM choices") + return + + _add_nv_gemm_choices_impl( + choices=choices, + layout=layout, + input_nodes=inputs.nodes(), + variant=GemmVariant.GEMM, + accumulator_type=accumulator_type or torch.float32, + mm_inputs=inputs, + ) + + +def add_nv_universal_grouped_gemm_choices( + choices: list[ChoiceCaller], + layout: Layout, + input_nodes: list[Buffer], + accumulator_type: torch.dtype | None = None, +) -> None: + """ + Add NVIDIA Universal Grouped GEMM kernels to the autotune choices. + + Thin wrapper around _add_nv_gemm_choices_impl for grouped GEMM. + + For grouped GEMM (contiguous offset variant): + - A is (TotalM, K) with problems stacked along M + - B is (G, K, N) where B[i] is the weight for problem i + - offsets is (G,) marking where each problem ends in A + - Output is (TotalM, N) + """ + if not ensure_nv_universal_gemm_available(): + log.debug( + "cutlass_api not available, skipping NVIDIA Universal Grouped GEMM choices" + ) + return + + _add_nv_gemm_choices_impl( + choices=choices, + layout=layout, + input_nodes=input_nodes, + variant=GemmVariant.GROUPED_GEMM, + accumulator_type=accumulator_type or torch.float32, + ) + + +def add_nv_universal_scaled_gemm_choices( + choices: list[ChoiceCaller], + layout: Layout, + input_nodes: list[Buffer], + accumulator_type: torch.dtype | None = None, + kernel_inputs: MMKernelInputs | None = None, +) -> None: + """ + Add NVIDIA Universal Scaled GEMM (FP8) kernels to the autotune choices. + + The scaling type is inferred from the input shapes/dtypes. + If the scaling mode is unsupported by NVGEMM, this function returns without + adding any choices. + """ + if not ensure_nv_universal_gemm_available(): + return + + from torch._inductor.utils import infer_scale_swizzle_ir + + if len(input_nodes) < 4: + return + + mat_a, mat_b, scale_a, scale_b = input_nodes[:4] + + scale_type_a, swizzle_type_a = infer_scale_swizzle_ir(mat_a, scale_a) + scale_type_b, swizzle_type_b = infer_scale_swizzle_ir( + mat_b, scale_b, transpose=True + ) + + if scale_type_a is None or scale_type_b is None: + return + + _add_nv_gemm_choices_impl( + choices=choices, + layout=layout, + input_nodes=input_nodes, + variant=GemmVariant.SCALED_GEMM, + accumulator_type=accumulator_type or torch.float32, + mm_inputs=kernel_inputs, + scale_type_a=scale_type_a, + scale_type_b=scale_type_b, + swizzle_type_a=swizzle_type_a, + swizzle_type_b=swizzle_type_b, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/nv_universal_gemm/nv_universal_gemm_kernel.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/nv_universal_gemm/nv_universal_gemm_kernel.py new file mode 100644 index 0000000000000000000000000000000000000000..aae76dfd3246cb5dd2bfaee75791d2f1d2d08cd8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/nv_universal_gemm/nv_universal_gemm_kernel.py @@ -0,0 +1,293 @@ +# mypy: allow-untyped-defs +""" +NVIDIA Universal GEMM kernel code generation. + +This module generates Python code that calls cutlass_api to execute GEMM operations. +""" + +from __future__ import annotations + +import logging +from typing import Any, TYPE_CHECKING + +from torch._inductor.codegen.common import ( + IndentedBuffer, + Kernel, + WorkspaceArg, + WorkspaceZeroMode, +) +from torch._inductor.codegen.cutedsl.cutedsl_op_overrides import CuteDSLOpOverrides +from torch._inductor.codegen.nv_universal_gemm.nv_universal_gemm_utils import ( + to_cutlass_scale_mode, +) +from torch._inductor.ir import ( + BaseView, + Buffer, + ExternKernel, + MutableBox, + ReinterpretView, +) +from torch._inductor.virtualized import V +from torch.utils._ordered_set import OrderedSet + + +if TYPE_CHECKING: + from torch._inductor.codegen.nv_universal_gemm.nv_universal_gemm import GemmVariant + + +log = logging.getLogger(__name__) + + +class NVUniversalGemmKernelWrapper: + """Wrapper to provide .run() interface for NVIDIA Universal GEMM kernels.""" + + def __init__(self, kernel_fn, kernel_path: str | None = None): + self.kernel_fn = kernel_fn + self.kernel_path = kernel_path + + def run(self, *args, stream=None, **kwargs): + """Execute the NVIDIA Universal GEMM kernel.""" + return self.kernel_fn(*args, stream=stream, **kwargs) + + +class NVUniversalGemmKernel(Kernel): + """ + Kernel implementation for NVIDIA Universal GEMM. + + Generates Python code that calls cutlass_api to execute GEMM operations. + Unlike CuteDSL which uses Jinja templates, this generates simpler direct + Python code. + """ + + def __init__( + self, + kernel_name: str, + input_nodes: list[Buffer], + output_node: Buffer, + kernel_metadata: dict[str, Any], + accumulator_type: Any, + variant: GemmVariant, + workspace_size: int = 0, + scale_type_a: Any | None = None, + scale_type_b: Any | None = None, + swizzle_type_a: Any | None = None, + swizzle_type_b: Any | None = None, + ) -> None: + super().__init__() + self.kernel_name = kernel_name + self.input_nodes = input_nodes + self.output_node = output_node + self.kernel_metadata = kernel_metadata + self.accumulator_type = accumulator_type + self.workspace_size = workspace_size + self.variant = variant + self.scale_type_a = scale_type_a + self.scale_type_b = scale_type_b + self.swizzle_type_a = swizzle_type_a + self.swizzle_type_b = swizzle_type_b + + self._template_input_args: list[tuple[str, Buffer]] = [] + self._seen_input_args: OrderedSet[str] = OrderedSet() + + for i, input_node in enumerate(input_nodes): + param_name = f"in_ptr{i}" + self._template_input_args.append((param_name, input_node)) + self._seen_input_args.add(param_name) + + def render(self) -> str: + """ + Render the NVIDIA Universal GEMM kernel code as a Python source string. + + Generates Python code that: + 1. Looks up the cutlass_api kernel by name from the manifest (cached in + _nv_universal_gemm_kernel_cache to avoid repeated manifest searches) + 2. Creates GemmArguments with the input/output tensors and accumulator type + 3. Compiles the kernel for the specific tensor shapes/dtypes (cached in + _nv_universal_gemm_artifact_cache keyed by (shape, dtype) tuple) + 4. Runs the kernel with the compiled artifact and CUDA stream + + The caching strategy ensures: + - Kernel lookup happens once per unique kernel name + - Compilation happens once per unique (shape, dtype) combination + - Runtime execution is just the kernel.run() call with cached artifact + + Returns: + Python source code string to be written to a .py file and loaded + via async_compile.nv_universal_gemm() + """ + from torch._inductor.codegen.nv_universal_gemm.nv_universal_gemm import ( + GemmVariant, + ) + + kernel_name_str = self.kernel_metadata["kernel_name"] + is_grouped = self.variant == GemmVariant.GROUPED_GEMM + is_scaled = self.variant == GemmVariant.SCALED_GEMM + + acc_dtype_str = CuteDSLOpOverrides.TORCH_TO_CUTE_DTYPE.get( + self.accumulator_type, "cutlass.Float32" + ) + + input_params = [f"in_ptr{i}" for i, _ in enumerate(self.input_nodes)] + input_params.append("out_ptr0") + if self.workspace_size > 0: + input_params.append("workspace") + input_params.append("stream=None") + params_str = ", ".join(input_params) + + workspace_arg = "workspace" if self.workspace_size > 0 else "None" + + var_prefix = self.variant.op_name.upper() + cache_var = f"_{var_prefix}_compiled_cache" + kernel_name_var = f"_{var_prefix}_KERNEL_NAME" + + extra_imports = "" + if is_scaled: + extra_imports = """from cutlass_api.arguments import ScaledTensor + from cutlass_api.library import ScaleMode, ScaleSwizzleMode""" + + # Variant-specific code generation: + # - cache_key_code: expression for cache key + # - create_args_code: code to create Arguments object + if is_grouped: + cache_key_code = "(in_ptr0.shape, in_ptr0.dtype, in_ptr1.shape, in_ptr1.dtype, in_ptr2.shape)" + create_args_code = f"""args = cutlass_api.arguments.GroupedGemmArguments( + in_ptr0, + in_ptr1, + out_ptr0, + accumulator_type={acc_dtype_str}, + offsets=in_ptr2, + )""" + elif is_scaled: + scale_mode_a, swizzle_mode_a = to_cutlass_scale_mode( + self.scale_type_a, self.swizzle_type_a + ) + scale_mode_b, swizzle_mode_b = to_cutlass_scale_mode( + self.scale_type_b, self.swizzle_type_b + ) + scale_mode_a_str = scale_mode_a.name if scale_mode_a else "" + scale_mode_b_str = scale_mode_b.name if scale_mode_b else "" + swizzle_mode_a_str = swizzle_mode_a.name if swizzle_mode_a else "" + swizzle_mode_b_str = swizzle_mode_b.name if swizzle_mode_b else "" + cache_key_code = "(in_ptr0.shape, in_ptr0.dtype, in_ptr1.shape, in_ptr1.dtype, in_ptr2.shape, in_ptr3.shape)" + create_args_code = f"""scaled_a = ScaledTensor( + in_ptr0, in_ptr2, ScaleMode.{scale_mode_a_str}, ScaleSwizzleMode.{swizzle_mode_a_str} + ) + scaled_b = ScaledTensor( + in_ptr1, in_ptr3, ScaleMode.{scale_mode_b_str}, ScaleSwizzleMode.{swizzle_mode_b_str} + ) + args = cutlass_api.arguments.GemmArguments( + scaled_a, + scaled_b, + out_ptr0, + accumulator_type={acc_dtype_str}, + )""" + else: + cache_key_code = ( + "(in_ptr0.shape, in_ptr0.dtype, in_ptr1.shape, in_ptr1.dtype)" + ) + create_args_code = f"""args = cutlass_api.arguments.GemmArguments( + in_ptr0, + in_ptr1, + out_ptr0, + accumulator_type={acc_dtype_str}, + )""" + + code = IndentedBuffer() + code.splice( + f""" + import cutlass + import cutlass_api + from torch._inductor.codegen.nv_universal_gemm.kernel_cache import get_kernel_by_name + {extra_imports} + + {kernel_name_var} = "{kernel_name_str}" + # Maps (shape, dtype, shape, dtype, ...) -> compiled kernel artifact + {cache_var} = {{}} + + def {self.kernel_name}_main({params_str}): + global {cache_var} + + kernel = get_kernel_by_name({kernel_name_var}) + if kernel is None: + raise RuntimeError(f"Could not find kernel: {{{kernel_name_var}}}") + + {create_args_code} + + cache_key = {cache_key_code} + artifact = {cache_var}.get(cache_key) + if artifact is None: + artifact = kernel.compile(args) + {cache_var}[cache_key] = artifact + + kernel.run(args, artifact, stream=stream, workspace={workspace_arg}, assume_supported_args=True) + """ + ) + + return code.getvalue() + + def _get_reinterpret_view(self, node) -> ReinterpretView | None: + """Extract or convert to ReinterpretView from a node, handling all views.""" + while isinstance(node, MutableBox): + node = node.data + if isinstance(node, BaseView): + return ExternKernel.convert_to_reinterpret_view(node) + return None + + def call_kernel(self, name: str, node=None): + """ + Generate the kernel call in the wrapper code. + + Similar to CuteDSLTemplateKernel.call_kernel but simplified for NVIDIA Universal GEMM. + """ + wrapper = V.graph.wrapper_code + + call_args: list[str] = [] + arg_types: list[Any] = [] + raw_args: list[Buffer | ReinterpretView | None] = [] + + for _, input_node in self._template_input_args: + reinterpret_view = self._get_reinterpret_view(input_node) + if reinterpret_view is not None: + call_args.append(reinterpret_view.codegen_reference()) + # Pass the ReinterpretView as raw_arg so autotune_at_compile_time + # can use it to generate example tensors + raw_args.append(reinterpret_view) + else: + call_args.append(input_node.get_name()) + raw_args.append(input_node) + arg_types.append(V.graph.get_dtype(input_node.get_name())) + + output_name = self.output_node.get_name() + call_args.append(output_name) + arg_types.append(V.graph.get_dtype(output_name)) + raw_args.append(None) # Output buffer is findable by name + + # Allocate workspace if needed + ws: WorkspaceArg | None = None + if self.workspace_size > 0: + ws = WorkspaceArg( + count=self.workspace_size, + device=V.graph.get_current_device_or_throw(), + zero_mode=WorkspaceZeroMode.UNINITIALIZED, + outer_name=WorkspaceArg.unique_name(), + ) + wrapper.generate_workspace_allocation(ws) + call_args.append(ws.outer_name) + arg_types.append(ws.dtype) + raw_args.append(None) + + # Generate the kernel call using triton=True for Python-based kernels + # Pass raw_keys as None list to match raw_args length + # TODO(nikhilap) We don't use autotune_args like the Triton path + wrapper.generate_kernel_call( + name, + call_args, + triton=True, + arg_types=arg_types, + raw_args=raw_args, + raw_keys=[None] * len(raw_args), + ) + + # Deallocate workspace after kernel call + if ws is not None: + wrapper.generate_workspace_deallocation(ws) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/nv_universal_gemm/nv_universal_gemm_scheduling.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/nv_universal_gemm/nv_universal_gemm_scheduling.py new file mode 100644 index 0000000000000000000000000000000000000000..e2e48a39bad9ec50bdae6ddc9e2c9a0bf48b37b0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/nv_universal_gemm/nv_universal_gemm_scheduling.py @@ -0,0 +1,153 @@ +# mypy: allow-untyped-defs +""" +NVIDIA Universal GEMM scheduling for PyTorch Inductor. +""" + +import hashlib +import logging +from collections.abc import Sequence +from typing import cast + +from torch._inductor.utils import ( + get_fused_kernel_name, + get_kernel_metadata, + Placeholder, +) +from torch.utils._ordered_set import OrderedSet + +from ... import config +from ...codecache import code_hash, get_path +from ...ir import NVUniversalGemmBuffer +from ...scheduler import ( + BaseSchedulerNode, + BaseScheduling, + FusedSchedulerNode, + SchedulerNode, +) +from ...virtualized import V +from ..common import BackendFeature, IndentedBuffer + + +log = logging.getLogger(__name__) + +MAIN_SUFFIX = "main" + + +class NVUniversalGemmScheduling(BaseScheduling): + """ + Scheduling implementation for NVIDIA Universal GEMM kernels. + + This class is intended to be used in combination with other schedulers, + and delegated to by CUDACombinedScheduling. + """ + + @classmethod + def get_backend_features(cls, device) -> OrderedSet[BackendFeature]: + return OrderedSet() + + @staticmethod + def is_nv_universal_gemm_template(node: BaseSchedulerNode) -> bool: + """Check if a node is a NVIDIA Universal GEMM template.""" + return isinstance(node, SchedulerNode) and isinstance( + node.node, NVUniversalGemmBuffer + ) + + def is_nv_universal_gemm_fused_template(self, node: BaseSchedulerNode) -> bool: + """Check if a node is a fused NVIDIA Universal GEMM template.""" + return isinstance( + node, FusedSchedulerNode + ) and self.is_nv_universal_gemm_template(node) + + def can_fuse_vertical( + self, node1: BaseSchedulerNode, node2: BaseSchedulerNode + ) -> bool: + # NVIDIA Universal GEMM templates don't support vertical fusion yet + return False + + def can_fuse_horizontal( + self, node1: BaseSchedulerNode, node2: BaseSchedulerNode + ) -> bool: + # NVIDIA Universal GEMM templates don't support horizontal fusion yet + return False + + def define_kernel(self, src_code: str, node_schedule) -> str: + """ + Define a NVIDIA Universal GEMM kernel by writing source code and generating wrapper. + + Based on CuteDSLScheduling.define_kernel. + """ + wrapper = V.graph.wrapper_code + + # Use the string as the key for caching + if src_code in wrapper.src_to_kernel: + return wrapper.src_to_kernel[src_code] + + fused_name = ( + get_fused_kernel_name(node_schedule, config.triton.descriptive_names) + if config.triton.descriptive_names + else "" + ) + + kernel_hash = hashlib.sha256(src_code.encode("utf-8")).hexdigest()[:8] + if fused_name == "fused": + kernel_name = f"nv_universal_gemm_{kernel_hash}" + else: + kernel_name = f"nv_universal_gemm_{fused_name}_{kernel_hash}" + + wrapper.src_to_kernel[src_code] = kernel_name + + src_code = src_code.replace(str(Placeholder.KERNEL_NAME), kernel_name) + + _, _, kernel_path = get_path(code_hash(src_code), "py") + + compile_wrapper = IndentedBuffer() + compile_wrapper.writeline( + f"async_compile.nv_universal_gemm({kernel_name!r}, r'''" + ) + compile_wrapper.splice(src_code, strip=True) + compile_wrapper.writeline("''')") + + metadata_comment = f"# kernel path: {kernel_path}" + origins, detailed_origins = get_kernel_metadata(node_schedule, wrapper) + metadata_comment += "\n" + origins + "\n" + detailed_origins + wrapper.define_kernel(kernel_name, compile_wrapper.getvalue(), metadata_comment) + + return kernel_name + + def codegen_template( + self, + template_node: BaseSchedulerNode, + epilogue_nodes: Sequence[BaseSchedulerNode], + prologue_nodes: Sequence[BaseSchedulerNode], + ): + """ + Codegen a NVIDIA Universal GEMM template. Currently doesn't support fusion. + """ + assert self.is_nv_universal_gemm_template(template_node), ( + "Template node passed to NVUniversalGemmScheduling.codegen_template must be a " + "SchedulerNode that wraps a NVUniversalGemmBuffer" + ) + # TODO: add support for fusion when needed + assert not epilogue_nodes, ( + "NVIDIA Universal GEMM doesn't support epilogue fusion yet" + ) + assert not prologue_nodes, ( + "NVIDIA Universal GEMM doesn't support prologue fusion yet" + ) + + template_node = cast(SchedulerNode, template_node) + ctb: NVUniversalGemmBuffer = cast(NVUniversalGemmBuffer, template_node.node) + + assert ctb.make_kernel_render is not None + kernel, render = ctb.make_kernel_render(ctb) + template_node.mark_run() + src_code = render() + + with V.set_kernel_handler(kernel): + node_schedule = [template_node] + kernel_name = self.define_kernel(src_code, node_schedule) + + self.codegen_comment(node_schedule, kernel_name) + kernel.call_kernel(kernel_name, ctb) + V.graph.removed_buffers |= kernel.removed_buffers + self.free_buffers_in_scheduler() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/nv_universal_gemm/nv_universal_gemm_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/nv_universal_gemm/nv_universal_gemm_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..077e03dfb3a7ce8c594b2181b6c3c47140cc26fe --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/nv_universal_gemm/nv_universal_gemm_utils.py @@ -0,0 +1,42 @@ +# mypy: allow-untyped-defs +""" +Utility functions for NVIDIA Universal GEMM. +""" + +from typing import Any + +from torch.nn.functional import ScalingType, SwizzleType + + +def to_cutlass_scale_mode( + scale_type: Any, swizzle_type: Any +) -> tuple[Any | None, Any | None]: + """ + Map PyTorch ScalingType/SwizzleType to cutlass_api ScaleMode/ScaleSwizzleMode. + + Args: + scale_type: ScalingType from torch.nn.functional + swizzle_type: SwizzleType from torch.nn.functional + + Returns: + Tuple of (ScaleMode, ScaleSwizzleMode) from cutlass_api.library, + or (None, None) if the types are not supported. + + The returned enum objects can be used directly with cutlass_api, or their + .name attribute can be used for codegen (e.g., scale_mode.name -> "Blockwise1x32"). + + NOTE: + Currently on Blackwell (SM100), NVGEMM only supports MXFP8 scaling modes. + Update this mapping when additional scaling modes are added. + """ + from cutlass_api.library import ScaleMode, ScaleSwizzleMode + + scale_mode_map = { + ScalingType.BlockWise1x32: ScaleMode.Blockwise1x32, + ScalingType.BlockWise1x16: ScaleMode.Blockwise1x16, + } + swizzle_mode_map = { + SwizzleType.SWIZZLE_32_4_4: ScaleSwizzleMode.Swizzle32x4x4, + SwizzleType.NO_SWIZZLE: ScaleSwizzleMode.SwizzleNone, + } + return scale_mode_map.get(scale_type), swizzle_mode_map.get(swizzle_type) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/pallas.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/pallas.py new file mode 100644 index 0000000000000000000000000000000000000000..2bbf781e334d07e6620ecb3f1558e4ee8a77924e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/pallas.py @@ -0,0 +1,4938 @@ +from __future__ import annotations + +import dataclasses +import hashlib +import math +import re +import typing_extensions +from typing import Any, cast, TYPE_CHECKING + +import sympy # noqa: TC002 + +import torch # noqa: TC001 +from torch.utils._ordered_set import OrderedSet +from torch.utils._sympy.functions import ModularIndexing + +from .. import config +from ..runtime.runtime_utils import torch_dtype_to_jax +from ..utils import get_fused_kernel_name, get_kernel_metadata +from ..virtualized import V +from .block_analysis import BlockPatternMatcher +from .common import ( + BackendFeature, + CSEVariable, + IndentedBuffer, + OpOverrides, + PythonPrinter, +) +from .simd import IterationRangesEntry, SIMDKernel, SIMDScheduling + + +class PallasPrinter(PythonPrinter): + """ + Custom sympy printer for Pallas that handles JAX-specific constructs. + """ + + def _print_Where(self, expr: sympy.Expr) -> str: + """Convert sympy Where to jnp.where.""" + c = self.doprint(expr.args[0]) + p = self.doprint(expr.args[1]) + q = self.doprint(expr.args[2]) + return f"jnp.where({c}, {p}, {q})" + + def _print_Min(self, expr: sympy.Expr) -> str: + """Convert sympy Min to jnp.minimum for JAX compatibility.""" + args = [self.doprint(arg) for arg in expr.args] + result = args[0] + for arg in args[1:]: + result = f"jnp.minimum({result}, {arg})" + return result + + def _print_Max(self, expr: sympy.Expr) -> str: + """Convert sympy Max to jnp.maximum for JAX compatibility.""" + args = [self.doprint(arg) for arg in expr.args] + result = args[0] + for arg in args[1:]: + result = f"jnp.maximum({result}, {arg})" + return result + + +# Use Pallas-specific printer for expression generation +pallas_pexpr = PallasPrinter().doprint + + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + + from ..ir import IRNode + from ..ops_handler import ReductionType + from ..scheduler import BaseSchedulerNode + + +# Main function suffix used in generated Pallas code +MAIN_SUFFIX = "main" + +# Logger for Pallas kernel code +kernel_code_log = torch._logging.getArtifactLogger(__name__, "kernel_code") + + +class PallasKernelWrapper: + """Wrapper to provide .run() interface for Pallas kernels""" + + def __init__(self, kernel_fn: Callable[..., Any], kernel_path: str | None = None): + self.kernel_fn = kernel_fn + self.kernel_path = kernel_path + kernel_code_log.info("Pallas kernel path: %s", kernel_path) + + def run(self, *args, stream=None, **kwargs): + """ + Execute the Pallas kernel. + + Args: + *args: Arguments to pass to the kernel function + stream: CUDA stream to pass to the kernel function + **kwargs: Additional keyword arguments for the kernel + + Returns: + Result of the kernel execution + """ + return self.kernel_fn(*args, stream=stream, **kwargs) + + +class Unsupported(RuntimeError): + """Exception raised when an operation is not supported by the Pallas backend.""" + + +class PallasKernelOverrides(OpOverrides): + """ + Map element-wise ops to JAX/Pallas operations. + + For now, we use the default Python operators which are compatible + with JAX numpy broadcasting semantics. + """ + + @staticmethod + # pyrefly: ignore [bad-override] + def sin(x: str) -> str: + return f"jnp.sin({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def cos(x: str) -> str: + return f"jnp.cos({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def tan(x: str) -> str: + return f"jnp.tan({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def sinh(x: str) -> str: + return f"jnp.sinh({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def cosh(x: str) -> str: + return f"jnp.cosh({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def tanh(x: str) -> str: + return f"jnp.tanh({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def asin(x: str) -> str: + return f"jnp.arcsin({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def acos(x: str) -> str: + return f"jnp.arccos({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def atan(x: str) -> str: + return f"jnp.arctan({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def exp(x: str) -> str: + return f"jnp.exp({x})" + + @staticmethod + def exp2(x: str) -> str: + return f"jnp.exp2({x})" + + @staticmethod + def expm1(x: str) -> str: + return f"jnp.expm1({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def log(x: str) -> str: + return f"jnp.log({x})" + + @staticmethod + def log10(x: str) -> str: + return f"jnp.log10({x})" + + @staticmethod + def log2(x: str) -> str: + return f"jnp.log2({x})" + + @staticmethod + def log1p(x: str) -> str: + return f"jnp.log1p({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def sqrt(x: str) -> str: + return f"jnp.sqrt({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def rsqrt(x: str) -> str: + return f"jax.lax.rsqrt({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def abs(x: str) -> str: + return f"jnp.abs({x})" + + @staticmethod + def neg(x: str) -> str: + return f"(-{x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def floor(x: str) -> str: + return f"jnp.floor({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def ceil(x: str) -> str: + return f"jnp.ceil({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def trunc(x: str) -> str: + return f"jnp.trunc({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def round(x: str) -> str: + return f"jnp.round({x})" + + @staticmethod + def sigmoid(x: str) -> str: + return f"jax.nn.sigmoid({x})" + + @staticmethod + def relu(x: str) -> str: + return f"jnp.maximum({x}, 0)" + + @staticmethod + def pow(a: str, b: str) -> str: + return f"jnp.power({a}, {b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def maximum(a: str, b: str) -> str: + return f"jnp.maximum({a}, {b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def minimum(a: str, b: str) -> str: + return f"jnp.minimum({a}, {b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def where(cond: str, a: str, b: str) -> str: + return f"jnp.where({cond}, {a}, {b})" + + @staticmethod + def masked(mask: str, body: Callable[[], str], other: float) -> str: + """ + Computes body, but only uses the result where mask is true. + Where mask is false, uses the 'other' value instead. + """ + result = body() + # Format the 'other' value properly for JAX + if isinstance(other, float): + if math.isnan(other): + other_str = "jnp.nan" + elif math.isinf(other): + other_str = "jnp.inf" if other > 0 else "-jnp.inf" + else: + other_str = repr(other) + else: + other_str = repr(other) + # Use jnp.where to select between result and other based on mask + return f"jnp.where({mask}, {result}, {other_str})" + + @staticmethod + def to_dtype( + x: str, + dtype: torch.dtype, + src_dtype: torch.dtype | None = None, + use_compute_types: bool = True, + ) -> str: + # TPU doesn't support 64-bit types + if dtype == torch.int64 and V.graph.get_current_device_or_throw().type == "tpu": + dtype = torch.int32 + jax_dtype = torch_dtype_to_jax(dtype) + # Wrap in jnp.asarray to handle scalars from integer indexing + return f"jnp.asarray({x}).astype({jax_dtype})" + + @staticmethod + def to_dtype_bitcast(x: str, dtype: torch.dtype, src_dtype: torch.dtype) -> str: + """Bitcast a value from one dtype to another with the same size.""" + jax_dtype = torch_dtype_to_jax(dtype) + jax_src_dtype = torch_dtype_to_jax(src_dtype) + # First ensure the value is the correct source dtype, then bitcast + return f"jax.lax.bitcast_convert_type(jnp.asarray({x}).astype({jax_src_dtype}), {jax_dtype})" + + @staticmethod + def index_expr(expr: sympy.Expr, dtype: torch.dtype) -> str: + """Convert a sympy expression to a JAX array indexing expression.""" + from ..utils import get_bounds_index_expr + + # Track which iteration variables are used + V.kernel.used_iter_vars.update(V.kernel._get_used_iter_vars(expr)) + + # Prepare and rename indexing to register size symbols as kernel args + prepared = V.kernel.prepare_indexing(expr) + renamed = V.kernel.rename_indexing(prepared) + idx_str = V.kernel.kexpr(renamed) + var = V.kernel.cse.generate( + V.kernel.compute, idx_str, bounds=get_bounds_index_expr(expr) + ) + return PallasKernelOverrides.to_dtype(var, dtype) + + @staticmethod + def constant(val, dtype: torch.dtype) -> str: + """Convert a constant value to JAX representation.""" + jax_dtype = torch_dtype_to_jax(dtype) + if dtype == torch.bool: + return "True" if val else "False" + # Handle special float values + if isinstance(val, float): + if math.isnan(val): + return "jnp.nan" + if math.isinf(val): + return "jnp.inf" if val > 0 else "-jnp.inf" + return f"jnp.array({val}, dtype={jax_dtype})" + + @staticmethod + def real(x: str) -> str: + return f"jnp.real({x})" + + @staticmethod + def imag(x: str) -> str: + return f"jnp.imag({x})" + + @staticmethod + def conj(x: str) -> str: + return f"jnp.conj({x})" + + @staticmethod + def angle(x: str) -> str: + return f"jnp.angle({x})" + + @staticmethod + def view_as_real(x: str) -> str: + """View complex tensor as real tensor with extra dimension.""" + return f"jnp.stack([jnp.real({x}), jnp.imag({x})], axis=-1)" + + @staticmethod + def view_as_complex(x: str) -> str: + """View real tensor as complex tensor.""" + return f"({x}[..., 0] + 1j * {x}[..., 1])" + + # Comparison operations + @staticmethod + def eq(a: str, b: str) -> str: + return f"({a} == {b})" + + @staticmethod + def ne(a: str, b: str) -> str: + return f"({a} != {b})" + + @staticmethod + def lt(a: str, b: str) -> str: + return f"({a} < {b})" + + @staticmethod + def le(a: str, b: str) -> str: + return f"({a} <= {b})" + + @staticmethod + def gt(a: str, b: str) -> str: + return f"({a} > {b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def isnan(x: str) -> str: + return f"jnp.isnan({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def isinf(x: str) -> str: + return f"jnp.isinf({x})" + + @staticmethod + def isfinite(x: str) -> str: + return f"jnp.isfinite({x})" + + @staticmethod + def ge(a: str, b: str) -> str: + return f"({a} >= {b})" + + # Logical operations + @staticmethod + # pyrefly: ignore [bad-override] + def logical_and(a: str, b: str) -> str: + return f"jnp.logical_and({a}, {b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def logical_or(a: str, b: str) -> str: + return f"jnp.logical_or({a}, {b})" + + @staticmethod + def logical_not(x: str) -> str: + return f"jnp.logical_not({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def logical_xor(a: str, b: str) -> str: + return f"jnp.logical_xor({a}, {b})" + + # Math operations + @staticmethod + # pyrefly: ignore [bad-override] + def atan2(a: str, b: str) -> str: + return f"jnp.arctan2({a}, {b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def hypot(a: str, b: str) -> str: + return f"jnp.hypot({a}, {b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def fmod(a: str, b: str) -> str: + return f"jnp.fmod({a}, {b})" + + @staticmethod + def remainder(a: str, b: str) -> str: + return f"jnp.remainder({a}, {b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def truncdiv(a: str, b: str) -> str: + # Truncated division (rounds toward zero) + # For integers: sign(a)*sign(b) * (abs(a) // abs(b)) + return f"(jnp.sign({a}) * jnp.sign({b}) * (jnp.abs({a}) // jnp.abs({b}))).astype({a}.dtype)" + + @staticmethod + def floordiv(a: str, b: str) -> str: + return f"({a} // {b})" + + @staticmethod + def clamp(x: str, min_val: str, max_val: str) -> str: + return f"jnp.clip({x}, {min_val}, {max_val})" + + clip = clamp + + # Sign operations + @staticmethod + # pyrefly: ignore [bad-override] + def sign(x: str) -> str: + # PyTorch returns 0 for NaN, JAX returns NaN + return f"jnp.where(jnp.isnan({x}), 0.0, jnp.sign({x}))" + + @staticmethod + # pyrefly: ignore [bad-override] + def signbit(x: str) -> str: + return f"jnp.signbit({x})" + + # Special math functions + @staticmethod + # pyrefly: ignore [bad-override] + def erf(x: str) -> str: + return f"jax.scipy.special.erf({x})" + + @staticmethod + def erfc(x: str) -> str: + return f"jax.scipy.special.erfc({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def erfinv(x: str) -> str: + return f"jax.scipy.special.erfinv({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def lgamma(x: str) -> str: + return f"jax.scipy.special.gammaln({x})" + + @staticmethod + def digamma(x: str) -> str: + return f"jax.scipy.special.digamma({x})" + + @staticmethod + def bessel_j0(x: str) -> str: + # bessel_jn requires float64 and has numerical issues at x=0 (returns NaN) + # bessel_jn(x, v=n) returns array of shape (n+1, ...) with J_0 to J_n + # Handle by: convert to float64, compute, handle x=0, convert back + # J0(0) = 1.0 + return ( + f"jnp.where({x}.astype(jnp.float64) == 0.0, 1.0, " + f"jax.scipy.special.bessel_jn({x}.astype(jnp.float64), v=0)[0])" + f".astype({x}.dtype)" + ) + + @staticmethod + def bessel_j1(x: str) -> str: + # bessel_jn requires float64 and has numerical issues at x=0 (returns NaN) + # bessel_jn(x, v=n) returns array of shape (n+1, ...) with J_0 to J_n + # Handle by: convert to float64, compute, handle x=0, convert back + # J1(0) = 0.0 + return ( + f"jnp.where({x}.astype(jnp.float64) == 0.0, 0.0, " + f"jax.scipy.special.bessel_jn({x}.astype(jnp.float64), v=1)[1])" + f".astype({x}.dtype)" + ) + + @staticmethod + def modified_bessel_i0(x: str) -> str: + # Modified Bessel function of the first kind I_0(x) + # I_0(x) = bessel_i0e(x) * exp(|x|) where bessel_i0e is the scaled version + return f"jax.lax.bessel_i0e({x}) * jnp.exp(jnp.abs({x}))" + + @staticmethod + def modified_bessel_i1(x: str) -> str: + # Modified Bessel function of the first kind I_1(x) + # I_1(x) = bessel_i1e(x) * exp(|x|) where bessel_i1e is the scaled version + return f"jax.lax.bessel_i1e({x}) * jnp.exp(jnp.abs({x}))" + + @staticmethod + def spherical_bessel_j0(x: str) -> str: + # Spherical Bessel function of the first kind j_0(x) = sin(x) / x + # Handle x=0: j_0(0) = 1 + return f"jnp.where({x} == 0.0, 1.0, jnp.sin({x}) / {x})" + + i0 = modified_bessel_i0 + + @staticmethod + def i0e(x: str) -> str: + # Exponentially scaled modified Bessel function I_0 + return f"jax.lax.bessel_i0e({x})" + + i1 = modified_bessel_i1 + + @staticmethod + def i1e(x: str) -> str: + # Exponentially scaled modified Bessel function I_1 + return f"jax.lax.bessel_i1e({x})" + + @staticmethod + def gammainc(x: str, y: str) -> str: + # Regularized lower incomplete gamma function P(a, x) + # Note: PyTorch uses gammainc(input, other) where input is a (shape param) + return f"jax.scipy.special.gammainc({x}, {y})" + + @staticmethod + def gammaincc(x: str, y: str) -> str: + # Regularized upper incomplete gamma function Q(a, x) + return f"jax.scipy.special.gammaincc({x}, {y})" + + igamma = gammainc + + igammac = gammaincc + + @staticmethod + def polygamma(x: str, y: str) -> str: + # Polygamma function psi^(n)(x), x is order n, y is the value + # Note: JAX uses polygamma(n, x) where n is integer order + return f"jax.scipy.special.polygamma({x}.astype(jnp.int32), {y})" + + @staticmethod + def ndtri(x: str) -> str: + # Inverse of the standard normal CDF + return f"jax.scipy.special.ndtri({x})" + + @staticmethod + def zeta(x: str, y: str) -> str: + # Hurwitz zeta function zeta(x, q) = sum_{k=0}^inf 1/(k+q)^x + return f"jax.scipy.special.zeta({x}, {y})" + + @staticmethod + def xlogy(x: str, y: str) -> str: + # x * log(y), with proper handling of x=0 + return f"jax.scipy.special.xlogy({x}, {y})" + + @staticmethod + def xlog1py(x: str, y: str) -> str: + # x * log1p(y), with proper handling of x=0 + return f"jax.scipy.special.xlog1py({x}, {y})" + + @staticmethod + # pyrefly: ignore [bad-override] + def chebyshev_polynomial_t(x: str, n: str) -> str: + # Chebyshev polynomial of the first kind T_n(x) + # For |x| <= 1: T_n(x) = cos(n * arccos(x)) + # For x > 1: T_n(x) = cosh(n * arccosh(x)) + # For x < -1: T_n(x) = (-1)^n * cosh(n * arccosh(-x)) + return ( + f"jnp.where(jnp.abs({x}) <= 1, " + f"jnp.cos({n} * jnp.arccos(jnp.clip({x}, -1, 1))), " + f"jnp.where({x} > 1, " + f"jnp.cosh({n} * jnp.arccosh(jnp.maximum({x}, 1.0))), " + f"((-1.0) ** {n}) * jnp.cosh({n} * jnp.arccosh(jnp.maximum(-{x}, 1.0)))))" + ) + + @staticmethod + # pyrefly: ignore [bad-override] + def chebyshev_polynomial_u(x: str, n: str) -> str: + # Chebyshev polynomial of the second kind U_n(x) + # For |x| < 1: U_n(x) = sin((n+1) * arccos(x)) / sqrt(1 - x^2) + # For x = 1: U_n(1) = n+1 + # For x = -1: U_n(-1) = (-1)^n * (n+1) + # For x > 1: U_n(x) = sinh((n+1) * arccosh(x)) / sqrt(x^2 - 1) + # For x < -1: U_n(x) = (-1)^n * U_n(-x) (symmetry) + return ( + f"jnp.where(jnp.abs({x}) < 1, " + f"jnp.sin(({n} + 1) * jnp.arccos(jnp.clip({x}, -1, 1))) / " + f"jnp.sqrt(jnp.maximum(1 - {x}**2, 1e-10)), " + f"jnp.where({x} >= 1, " + f"jnp.where({x} == 1, {n} + 1.0, " + f"jnp.sinh(({n} + 1) * jnp.arccosh(jnp.maximum({x}, 1.0))) / " + f"jnp.sqrt(jnp.maximum({x}**2 - 1, 1e-10))), " + f"jnp.where({x} == -1, ((-1.0) ** {n}) * ({n} + 1.0), " + f"((-1.0) ** {n}) * jnp.sinh(({n} + 1) * jnp.arccosh(jnp.maximum(-{x}, 1.0))) / " + f"jnp.sqrt(jnp.maximum({x}**2 - 1, 1e-10)))))" + ) + + @staticmethod + # pyrefly: ignore [bad-override] + def chebyshev_polynomial_v(x: str, n: str) -> str: + # Chebyshev polynomial of the third kind V_n(x) + # V_n(x) = (T_n(x) - T_{n+1}(x)) / (1 - x) for x != 1 + # V_n(1) = 1, recurrence: V_0 = 1, V_1 = 2x - 1, V_n = 2x*V_{n-1} - V_{n-2} + # Explicit: V_0 = 1, V_1 = 2x-1, V_2 = 4x^2-2x-1, V_3 = 8x^3-4x^2-4x+1 + return ( + f"jnp.where({n} == 0, jnp.ones_like({x}), " + f"jnp.where({n} == 1, 2*{x} - 1, " + f"jnp.where({n} == 2, 4*{x}**2 - 2*{x} - 1, " + f"jnp.where({n} == 3, 8*{x}**3 - 4*{x}**2 - 4*{x} + 1, " + f"jnp.where({n} == 4, 16*{x}**4 - 8*{x}**3 - 12*{x}**2 + 4*{x} + 1, " + f"jnp.where({n} == 5, 32*{x}**5 - 16*{x}**4 - 32*{x}**3 + 12*{x}**2 + 6*{x} - 1, " + f"jnp.zeros_like({x})))))))" + ) + + @staticmethod + # pyrefly: ignore [bad-override] + def chebyshev_polynomial_w(x: str, n: str) -> str: + # Chebyshev polynomial of the fourth kind W_n(x) + # W_n(x) = (T_n(x) + T_{n+1}(x)) / (1 + x) for x != -1 + # W_n(-1) = (-1)^n, recurrence: W_0 = 1, W_1 = 2x + 1, W_n = 2x*W_{n-1} - W_{n-2} + # Explicit: W_0 = 1, W_1 = 2x+1, W_2 = 4x^2+2x-1, W_3 = 8x^3+4x^2-4x-1 + return ( + f"jnp.where({n} == 0, jnp.ones_like({x}), " + f"jnp.where({n} == 1, 2*{x} + 1, " + f"jnp.where({n} == 2, 4*{x}**2 + 2*{x} - 1, " + f"jnp.where({n} == 3, 8*{x}**3 + 4*{x}**2 - 4*{x} - 1, " + f"jnp.where({n} == 4, 16*{x}**4 + 8*{x}**3 - 12*{x}**2 - 4*{x} + 1, " + f"jnp.where({n} == 5, 32*{x}**5 + 16*{x}**4 - 32*{x}**3 - 12*{x}**2 + 6*{x} + 1, " + f"jnp.zeros_like({x})))))))" + ) + + @staticmethod + # pyrefly: ignore [bad-override] + def shifted_chebyshev_polynomial_t(x: str, n: str) -> str: + return PallasKernelOverrides.chebyshev_polynomial_t(f"(2 * {x} - 1)", n) + + @staticmethod + # pyrefly: ignore [bad-override] + def shifted_chebyshev_polynomial_u(x: str, n: str) -> str: + return PallasKernelOverrides.chebyshev_polynomial_u(f"(2 * {x} - 1)", n) + + @staticmethod + # pyrefly: ignore [bad-override] + def shifted_chebyshev_polynomial_v(x: str, n: str) -> str: + return PallasKernelOverrides.chebyshev_polynomial_v(f"(2 * {x} - 1)", n) + + @staticmethod + # pyrefly: ignore [bad-override] + def shifted_chebyshev_polynomial_w(x: str, n: str) -> str: + return PallasKernelOverrides.chebyshev_polynomial_w(f"(2 * {x} - 1)", n) + + @staticmethod + # pyrefly: ignore [bad-override] + def hermite_polynomial_h(x: str, n: str) -> str: + # Physicist's Hermite polynomial H_n(x) + # H_n(x) = 2^n * x^n - n*(n-1)/2 * 2^(n-2) * x^(n-2) + ... + # Use explicit formula: H_n(x) = n! * sum_{m=0}^{n//2} (-1)^m / (m! * (n-2m)!) * (2x)^(n-2m) + # For simplicity, use the relation: H_n(x) = 2^(n/2) * He_n(x * sqrt(2)) where He is probabilist's + # Actually simpler: use recurrence or closed form + # H_0 = 1, H_1 = 2x, H_2 = 4x^2 - 2, H_3 = 8x^3 - 12x + return ( + f"jnp.where({n} == 0, jnp.ones_like({x}), " + f"jnp.where({n} == 1, 2 * {x}, " + f"jnp.where({n} == 2, 4 * {x}**2 - 2, " + f"jnp.where({n} == 3, 8 * {x}**3 - 12 * {x}, " + f"jnp.where({n} == 4, 16 * {x}**4 - 48 * {x}**2 + 12, " + f"jnp.where({n} == 5, 32 * {x}**5 - 160 * {x}**3 + 120 * {x}, " + f"jnp.zeros_like({x})))))))" # Fallback for higher n + ) + + @staticmethod + # pyrefly: ignore [bad-override] + def hermite_polynomial_he(x: str, n: str) -> str: + # Probabilist's Hermite polynomial He_n(x) + # He_0 = 1, He_1 = x, He_2 = x^2 - 1, He_3 = x^3 - 3x + return ( + f"jnp.where({n} == 0, jnp.ones_like({x}), " + f"jnp.where({n} == 1, {x}, " + f"jnp.where({n} == 2, {x}**2 - 1, " + f"jnp.where({n} == 3, {x}**3 - 3 * {x}, " + f"jnp.where({n} == 4, {x}**4 - 6 * {x}**2 + 3, " + f"jnp.where({n} == 5, {x}**5 - 10 * {x}**3 + 15 * {x}, " + f"jnp.zeros_like({x})))))))" # Fallback for higher n + ) + + @staticmethod + # pyrefly: ignore [bad-override] + def laguerre_polynomial_l(x: str, n: str) -> str: + # Laguerre polynomial L_n(x) + # L_0 = 1, L_1 = 1 - x, L_2 = (x^2 - 4x + 2)/2, L_3 = (-x^3 + 9x^2 - 18x + 6)/6 + return ( + f"jnp.where({n} == 0, jnp.ones_like({x}), " + f"jnp.where({n} == 1, 1 - {x}, " + f"jnp.where({n} == 2, ({x}**2 - 4*{x} + 2) / 2, " + f"jnp.where({n} == 3, (-{x}**3 + 9*{x}**2 - 18*{x} + 6) / 6, " + f"jnp.where({n} == 4, ({x}**4 - 16*{x}**3 + 72*{x}**2 - 96*{x} + 24) / 24, " + f"jnp.where({n} == 5, (-{x}**5 + 25*{x}**4 - 200*{x}**3 + 600*{x}**2 - 600*{x} + 120) / 120, " + f"jnp.zeros_like({x})))))))" # Fallback for higher n + ) + + @staticmethod + # pyrefly: ignore [bad-override] + def legendre_polynomial_p(x: str, n: str) -> str: + # Legendre polynomial P_n(x) + # P_0 = 1, P_1 = x, P_2 = (3x^2 - 1)/2, P_3 = (5x^3 - 3x)/2 + return ( + f"jnp.where({n} == 0, jnp.ones_like({x}), " + f"jnp.where({n} == 1, {x}, " + f"jnp.where({n} == 2, (3 * {x}**2 - 1) / 2, " + f"jnp.where({n} == 3, (5 * {x}**3 - 3 * {x}) / 2, " + f"jnp.where({n} == 4, (35 * {x}**4 - 30 * {x}**2 + 3) / 8, " + f"jnp.where({n} == 5, (63 * {x}**5 - 70 * {x}**3 + 15 * {x}) / 8, " + f"jnp.zeros_like({x})))))))" # Fallback for higher n + ) + + # Reciprocal and square + @staticmethod + def reciprocal(x: str) -> str: + return f"jnp.reciprocal({x})" + + @staticmethod + def square(x: str) -> str: + return f"jnp.square({x})" + + # Additional operations + @staticmethod + def fma(a: str, b: str, c: str) -> str: + """Fused multiply-add: a * b + c + + JAX doesn't have jnp.fma, so we use the unfused version. + The compiler may still fuse this on supported hardware. + """ + return f"(({a}) * ({b}) + ({c}))" + + @staticmethod + # pyrefly: ignore [bad-override] + def copysign(a: str, b: str) -> str: + return f"jnp.copysign({a}, {b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def nextafter(a: str, b: str) -> str: + return f"jnp.nextafter({a}, {b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def ldexp(a: str, b: str) -> str: + return f"jnp.ldexp({a}, {b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def frexp(x: str) -> str: + return f"jnp.frexp({x})" + + @staticmethod + def modf(x: str) -> str: + return f"jnp.modf({x})" + + # Bitwise operations + @staticmethod + def bitwise_and(a: str, b: str) -> str: + return f"jnp.bitwise_and({a}, {b})" + + @staticmethod + def bitwise_or(a: str, b: str) -> str: + return f"jnp.bitwise_or({a}, {b})" + + @staticmethod + def bitwise_xor(a: str, b: str) -> str: + return f"jnp.bitwise_xor({a}, {b})" + + @staticmethod + def bitwise_not(x: str) -> str: + return f"jnp.bitwise_not({x})" + + @staticmethod + def left_shift(a: str, b: str) -> str: + return f"jnp.left_shift({a}, {b})" + + @staticmethod + def right_shift(a: str, b: str) -> str: + return f"jnp.right_shift({a}, {b})" + + # Random number generation operations + @staticmethod + def load_seed(name: str, offset: str) -> str: + """Load the random seed value from a buffer.""" + # Load the seed from the buffer and add offset for uniqueness + seed_offset = V.kernel.args.seed_offset("load_seed_offset", offset) + return f"({V.kernel.args.input(name)}[0] + {seed_offset})" + + @staticmethod + def rand(seed: str, offset: str) -> str: + """Generate uniform random numbers in [0, 1). + + Uses JAX's threefry2x32 PRNG directly for vectorized random generation. + The seed provides the base key, offset provides per-element uniqueness. + """ + # For vectorized random, we use jax.random.uniform with shape from offset + # Create a base key from seed, then use fold_in with vmap for per-element keys + # Use float32 dtype to match PyTorch's default + return ( + f"jax.vmap(lambda o: jax.random.uniform(" + f"jax.random.fold_in(jax.random.PRNGKey(jnp.uint32({seed})), jnp.uint32(o)), (), dtype=jnp.float32))" + f"(jnp.asarray({offset}).flatten()).reshape(jnp.asarray({offset}).shape)" + ) + + @staticmethod + def randn(seed: str, offset: str) -> str: + """Generate standard normal random numbers. + + Uses JAX's threefry2x32 PRNG directly for vectorized random generation. + The seed provides the base key, offset provides per-element uniqueness. + """ + # For vectorized random, use vmap to fold in each offset value + # Use float32 dtype to match PyTorch's default + return ( + f"jax.vmap(lambda o: jax.random.normal(" + f"jax.random.fold_in(jax.random.PRNGKey(jnp.uint32({seed})), jnp.uint32(o)), (), dtype=jnp.float32))" + f"(jnp.asarray({offset}).flatten()).reshape(jnp.asarray({offset}).shape)" + ) + + @staticmethod + def randint64(seed: str, offset: str, low: str, high: str) -> str: + """Generate random int64 values in [low, high).""" + # For vectorized random, use vmap to fold in each offset value + return ( + f"jax.vmap(lambda o: jax.random.randint(" + f"jax.random.fold_in(jax.random.PRNGKey(jnp.uint32({seed})), jnp.uint32(o)), (), {low}, {high}, dtype=jnp.int64))" + f"(jnp.asarray({offset}).flatten()).reshape(jnp.asarray({offset}).shape)" + ) + + +@dataclasses.dataclass +class _IndirectAccessInfo: + """Describes a detected indirect (data-dependent) buffer access.""" + + table_param: str + table_buf_name: str + table_shape: tuple + indirect_dim: int + indirect_var: str + indices_param: str + + +@dataclasses.dataclass +class _BufferIndexing: + """Encapsulates index string and flattening requirements for buffer access.""" + + index_str: str + needs_flatten: bool + + +@dataclasses.dataclass +class _BroadcastedIterVar: + """Encapsulates information needed to codegen a broadcasted iteration var""" + + # index of this var in `self.range_tree_nodes.items()`` + idx: int + var_sym: sympy.Symbol + entry: IterationRangesEntry + length_val: int | None + + +@dataclasses.dataclass +class _CodegenContext: + """Bundles local state shared across codegen_kernel helper methods.""" + + code: IndentedBuffer + kernel_name: str + is_tpu: bool + interpret_is_cpu: bool + interpret_literal: str + kernel_params: list[str] + pure_out_params: list[str] + output_params: list[str] + size_var_params: list[str] + output_buffer_lookup: dict[str, str] + aliasable_flags: dict[str, bool] + alias_params: list[str] + pointer_tail: list[str] + kernel_input_params: list[str] + full_kernel_params: list[str] + non_alias_out_set: OrderedSet[str] + copy_output_indices: list[int] + alias_pairs: list[tuple[int, int]] + + +class PallasKernel(SIMDKernel): + """Pallas kernel codegen for TPU and GPU (Mosaic backend). + + Generates Python code that defines a Pallas kernel and a host entrypoint, + compiled and loaded via async_compile.pallas. + """ + + overrides = PallasKernelOverrides # type: ignore[assignment] + kexpr: Callable[[sympy.Expr], str] = pallas_pexpr # Use Pallas expression printer + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Determine device type once at initialization + device = V.graph.get_current_device_or_throw() + self.is_gpu = device.type == "cuda" + self.is_tpu = device.type == "tpu" + # Use TMA (Tensor Memory Accelerator) for GPU to handle non-aligned tensor sizes + # TMA automatically masks OOB accesses, eliminating the need for explicit + # padding to multiples of 128. Uses lax.fori_loop with direct TMA primitives. + self.use_emit_pipeline = self.is_gpu # Enable TMA approach for GPU + # Track which output param each store uses: list of (out_ptr_name, store_line) + self.store_with_output: list[tuple[str, str]] = [] + # Track load index expressions for reduction axis detection + self.load_index_exprs: dict[str, sympy.Expr] = {} + # Track outputs that need to be readable (for scatter operations) + self.outputs_need_read: OrderedSet[str] = OrderedSet() + # Map input buffer names to their detected permutation tuples. + self.permuted_input_buffers: dict[str, tuple[int, ...]] = {} + self.collapsed_reshape_inputs: dict[str, tuple[int, ...]] = {} + self.collapsed_output_shape: tuple[int, ...] | None = None + self._cpu_max_grid_product: int | None = None + # Precompute output buffer names from scheduler nodes so that the + # load path can check output shapes before stores are processed. + self._output_buffer_names: list[str] = [] + for snode in self.features.scheduler_nodes(): + for dep in snode.read_writes.writes: + self._output_buffer_names.append(dep.name) + # Track which iteration variables are actually used in the kernel + self.used_iter_vars: OrderedSet[sympy.Symbol] = OrderedSet() + # Iteration vars that have been emitted in tile-relative form + # (safe for tiling even when they appear in the compute body) + self.tile_relative_iter_vars: OrderedSet[sympy.Symbol] = OrderedSet() + # Track if any load/store uses flatten-based indexing (buf[...].flatten()[idx]) + self.has_flatten_indexing = False + # Strided input buffers: map graph buffer name -> per-dim + # (stride, offset, skip) triples. Used to reshape inputs outside + # the kernel and generate static indexing inside + # (e.g. in_ref[:, :, offset] instead of in_ref[...].flatten()[idx]). + self.strided_input_buffers: dict[str, list[tuple[int, int, int]]] = {} + # Buffers that already use flatten+gather indexing; strided + # decomposition must not reshape these (it would break flat offsets). + self.flatten_indexed_buffers: OrderedSet[str] = OrderedSet() + # Indirect (data-dependent) access info for scalar prefetch + self.indirect_access: _IndirectAccessInfo | None = None + self._cse_to_param: dict[str, str] = {} + self._param_to_graph_name: dict[str, str] = {} + self.is_tpu = device.type == "tpu" + + def check_bounds( + self, expr: sympy.Expr, size: sympy.Expr, lower: bool, upper: bool + ) -> None: + """Check array bounds for indirect indexing.""" + # For now, skip explicit bounds checking as JAX/Pallas handles this internally + # TODO: Implement explicit bounds checking with assertions if needed + + def _get_index_str(self, index: sympy.Expr) -> str: + """ + Convert an index expression to a string suitable for Pallas indexing. + + Pallas operates on full arrays, so we need to convert index expressions + to JAX array slicing. For example: + - x0 -> "..." (contiguous access, full array) + - 2*x0 -> "::2" (strided access with stride 2) + - 2*x0 + 1 -> "1::2" (strided access with offset 1, stride 2) + + Args: + index: The indexing expression to convert + + Returns: + The indexing string to use in generated code + """ + # Prepare and simplify the index + prepared_index = self.prepare_indexing(index) + + # Note: Block variable detection (im2col patterns) is handled in load()/store() + # where we have access to buffer dimensions. We check the buffer size + # against iteration variables there to detect gather patterns. + + # For simple single-symbol access (contiguous case), we can use [...] + # which is more efficient as it operates on the entire array at once + if isinstance(prepared_index, sympy.Symbol): + return "..." + elif prepared_index.is_Integer: + # Scalar index + return str(prepared_index) + else: + # Complex expression (strided/scatter access) + # Try to extract stride and offset for common patterns + return self._convert_to_jax_slice(prepared_index) + + def _convert_to_jax_slice(self, index: sympy.Expr) -> str: + """ + Convert a sympy index expression to JAX slice notation. + + Handles common patterns like: + - stride*var -> ::stride + - stride*var + offset -> offset::stride + + For more complex patterns, falls back to explicit indexing. + Uses BlockPatternMatcher for robust pattern matching. + """ + # Get the iteration variables for this kernel + if not self.range_trees: + return "..." + + # Rename symbolic sizes to kernel parameter names upfront + index = self.rename_indexing(index) + + # Check for ModularIndexing - this is NOT contiguous access + # ModularIndexing is used for roll/wrap-around operations + if index.has(ModularIndexing): + # Track which iteration variables are used before returning + self.used_iter_vars.update(self._get_used_iter_vars(index)) + # Generate actual index expression - iteration variables are already + # defined as jnp.arange arrays, so we just convert to JAX code + return self.kexpr(index) + + # Simplify the index + index = V.graph.sizevars.simplify(index) + # Find which iteration variable(s) are used + used_vars = self._get_used_iter_vars(index) + + # Track which iteration variables are used + self.used_iter_vars.update(used_vars) + + if len(used_vars) == 0: + # No iteration variables, this is a constant index + return str(index) + elif len(used_vars) == 1: + # Single iteration variable - try to extract stride and offset using BlockPatternMatcher + var = next(iter(used_vars)) + + # Get the subexpression involving this variable + var_expr = BlockPatternMatcher.get_subexpr_involving_symbol(index, var) + + # Try to match affine pattern: stride * var + stride = BlockPatternMatcher.match_affine_block_expr(var_expr, var) + + if stride is not None: + offset = index - var_expr + offset = V.graph.sizevars.simplify(offset) + + if stride < 0: + return self.kexpr(index) + + if offset == 0: + return "..." + + # Non-zero offset: check if we can use slice notation + if stride != 1: + return self.kexpr(index) + + try: + offset_val = int(offset) + if offset_val < 0: + return self.kexpr(index) + except (TypeError, ValueError): + return self.kexpr(index) + + return f"{self.kexpr(offset)}::1" + else: + # Couldn't match affine pattern, fall back to original logic + offset = index - var_expr + offset = V.graph.sizevars.simplify(offset) + if offset == 0 and var_expr == var: + # Just the variable itself, unit stride + return "..." + elif len(used_vars) > 1: + # Multi-dimensional indexing + # For contiguous multi-dim access, all terms should have unit stride + all_unit_stride = True + for var in used_vars: + var_expr = BlockPatternMatcher.get_subexpr_involving_symbol(index, var) + stride = BlockPatternMatcher.match_affine_block_expr(var_expr, var) + if stride != 1: + all_unit_stride = False + break + if all_unit_stride: + # Contiguous multi-dimensional access + return "..." + else: + # Strided multi-dimensional access + # For most cases, inputs are made contiguous before passing to JAX, + # so strided tensors become contiguous and we can use [...] + # The buffer size check in load() handles im2col-like patterns + return "..." + + # For complex cases, use [...] since inputs are made contiguous + return "..." + + def _generate_strided_index(self, index: sympy.Expr) -> str: + """ + Generate JAX code to compute an index array for strided/complex indexing patterns. + + For expressions like `2 * x3 + 32 * x2 + 256 * x1 + 1024 * x0`, we generate + code that computes the flattened index array using broadcasting. + + The iteration variables (x0, x1, x2, x3) are already defined as jnp.arange arrays + in the kernel. We just need to convert the sympy expression to JAX code. + """ + free_symbols = index.free_symbols + iter_vars = self._get_iter_vars() + + # Check that all free symbols are iteration variables (no indirect vars) + used_vars = free_symbols & iter_vars + if used_vars != free_symbols: + raise Unsupported( + f"Pallas backend does not yet support mixed index pattern: {index}" + ) + + # Track which iteration variables are used + self.used_iter_vars.update(used_vars) + + # Convert sympy expression to Python/JAX code string + # The iteration variables are already defined as jnp.arange arrays + index_str = self.kexpr(index) + + # Mark this as requiring flatten access + return index_str + + def _get_iter_vars(self) -> OrderedSet: + """Get the set of iteration variable symbols.""" + return OrderedSet(self.range_tree_nodes.keys()) + + def _get_used_iter_vars(self, index: sympy.Expr) -> OrderedSet: + """Get iteration variables used in an index expression.""" + return index.free_symbols & self._get_iter_vars() + + def _has_iteration_vars(self, index: sympy.Expr) -> bool: + """Check if index expression contains iteration variables.""" + return bool(self._get_used_iter_vars(index)) + + def _get_indirect_vars(self, index: sympy.Expr) -> list[sympy.Symbol]: + """Get list of indirect variable symbols (tmp*) in an index expression.""" + return [s for s in index.free_symbols if str(s).startswith("tmp")] + + def _has_indirect_vars(self, index: sympy.Expr) -> bool: + """Check if index expression contains indirect variables.""" + return len(self._get_indirect_vars(index)) > 0 + + def _decompose_strided_access( + self, index: sympy.Expr, name: str + ) -> list[tuple[int, int, int]] | None: + """Decompose a flat index into per-dimension (stride, offset, skip) triples. + + Given flat index like ``64*x0 + 2*x1 + 5`` and buffer shape ``(32, 64)`` + with C-contiguous strides ``[64, 1]``: + - x0 coefficient 64 / buffer_stride[0]=64 -> dim 0: stride=1 + - x1 coefficient 2 / buffer_stride[1]=1 -> dim 1: stride=2 + - constant 5: dim 0 gets 5//64=0, dim 1 gets 5//1=5 + - dim 1 offset 5 with stride 2: skip=5//2=2, offset=5%2=1 + + Returns per-dim ``[(stride, offset, skip), ...]`` where: + - stride: access stride on this dim (1 = contiguous) + - offset: static index into the stride dim (0 <= offset < stride) + - skip: number of stride-blocks to skip at the start of this dim + Returns None if decomposition fails. + """ + if self._has_indirect_vars(index) or index.has(ModularIndexing): + return None + + # Don't reshape a buffer that already has flatten+gather loads; + # the reshape would change the flat layout and break those loads. + if name in self.flatten_indexed_buffers: + return None + + info = self._get_buffer_info(name) + if info is None: + return None + _, buf_size, _, _, _ = info + + buf_shape_or_none = [self._safe_int(s) for s in buf_size] + if any(s is None or s <= 0 for s in buf_shape_or_none): + return None + buf_shape: list[int] = cast(list[int], buf_shape_or_none) + ndim = len(buf_shape) + if ndim == 0: + return None + + c_strides = self._c_contiguous_strides(buf_shape) + + # Extract per-variable coefficients + used_vars = self._get_used_iter_vars(index) + if not used_vars: + return None + + # [stride, raw_offset] per dim — raw_offset may be >= stride + result: list[list[int]] = [[1, 0] for _ in range(ndim)] + # Track which variable maps to which dimension + var_to_dim: dict[sympy.Symbol, int] = {} + + remaining = V.graph.sizevars.simplify(index) + for var in used_vars: + var_expr = BlockPatternMatcher.get_subexpr_involving_symbol(remaining, var) + coeff = BlockPatternMatcher.match_affine_block_expr(var_expr, var) + if coeff is None: + return None + coeff_int = self._safe_int(coeff) + if coeff_int is None or coeff_int <= 0: + return None + + # Find which buffer dim this variable maps to + dim = None + for d in range(ndim): + if c_strides[d] == 0: + continue + if coeff_int % c_strides[d] == 0: + per_dim_stride = coeff_int // c_strides[d] + if per_dim_stride >= 1: + dim = d + break + if dim is None: + return None + + per_dim_stride = coeff_int // c_strides[dim] + if per_dim_stride < 1 or buf_shape[dim] % per_dim_stride != 0: + return None + + result[dim][0] = per_dim_stride + var_to_dim[var] = dim + remaining = V.graph.sizevars.simplify(remaining - var_expr) + + # Remaining is the constant offset; distribute across dims using + # divmod with C-contiguous strides (largest stride first). + offset_val = self._safe_int(remaining) + if offset_val is None: + return None + if offset_val < 0: + return None + for d in range(ndim): + if c_strides[d] > 0: + result[d][1] = offset_val // c_strides[d] + offset_val = offset_val % c_strides[d] + if offset_val != 0: + return None + + # Only return if there's at least one dim with stride > 1. + # Contiguous accesses with just an offset (stride=1 everywhere) + # are handled by the normal tiling path. + if all(s == 1 for s, _ in result): + return None + + # Decompose each dim's raw offset into (skip, offset) where + # offset < stride: raw = skip * stride + offset. + # Then validate the output numel matches. + decomposed: list[tuple[int, int, int]] = [] + output_numel_expected = 1 + for d in range(ndim): + stride, raw_offset = result[d] + offset = raw_offset % stride + skip = raw_offset // stride + n_blocks = buf_shape[d] // stride + if skip >= n_blocks: + return None + output_numel_expected *= n_blocks - skip + decomposed.append((stride, offset, skip)) + + output_numel, _ = self._compute_output_numel_from_index(index) + if output_numel != output_numel_expected: + return None + + # Verify each variable's range matches its assigned dimension's + # effective size. When the kernel collapses multiple buffer dims + # into one iteration variable (e.g. batch*channels), the variable + # range won't match any single buffer dimension and we must bail + # out to avoid shape mismatches in the generated code. + for var, dim in var_to_dim.items(): + if var not in self.range_tree_nodes: + return None + var_range = self._safe_int(self.range_tree_nodes[var].length) + if var_range is None: + return None + stride_d, _offset_d, skip_d = decomposed[dim] + effective_size = buf_shape[dim] // stride_d - skip_d + if var_range != effective_size: + return None + + return decomposed + + @staticmethod + def _strided_load_expr(buf: str, decomp: list[tuple[int, int, int]]) -> str: + """Build ``buf[:, :, offset]`` for strided dims, ``:`` for others.""" + parts: list[str] = [] + for stride, offset, _skip in decomp: + if stride == 1: + parts.append(":") + else: + parts.append(":") # the halved dim + parts.append(str(offset)) # static index into stride dim + return f"{buf}[{', '.join(parts)}]" + + def _codegen_strided_reshapes( + self, code: IndentedBuffer, params: list[str] + ) -> None: + """Emit reshape + optional slice for strided input parameters. + + For each strided param, reshapes ``(M, N)`` to ``(M, N/stride, stride)`` + and, when ``skip > 0``, slices off leading blocks so the remaining + elements align with the output. + """ + for param in params: + buf_name = self._param_to_buf_name(param) + if buf_name is None or buf_name not in self.strided_input_buffers: + continue + strides = self.strided_input_buffers[buf_name] + info = self._get_buffer_info(buf_name) + if info is None: + continue + _, buf_size, _, _, _ = info + new_shape_parts: list[str] = [] + for d, (stride, _offset, _skip) in enumerate(strides): + dim = self._safe_int(buf_size[d]) + if dim is None: + break + if stride > 1: + new_shape_parts.append(str(dim // stride)) + new_shape_parts.append(str(stride)) + else: + new_shape_parts.append(str(dim)) + else: + code.writeline( + f"{param} = {param}.reshape({', '.join(new_shape_parts)})" + ) + if any(skip > 0 for _, _, skip in strides): + slice_parts: list[str] = [] + for stride, _offset, skip in strides: + if stride == 1: + slice_parts.append(":") + else: + slice_parts.append(f"{skip}:" if skip > 0 else ":") + slice_parts.append(":") + code.writeline(f"{param} = {param}[{', '.join(slice_parts)}]") + + @staticmethod + def _get_actual_out_strides(out_buf, n: int) -> list[int] | None: + """Extract actual output buffer strides from its layout.""" + layout = getattr(out_buf, "get_layout", lambda: None)() + if layout is None: + return None + stride_raw = getattr(layout, "stride", None) + if stride_raw is None or len(stride_raw) != n: + return None + strides: list[int] = [] + for s in stride_raw: + v = int(s) if isinstance(s, (int, sympy.Integer)) else None + if v is None: + return None + strides.append(v) + return strides + + def _compute_store_coeffs(self, ordered: list) -> dict | None: + """Compute store-side linearization coefficients from range tree nesting. + + The tree structure encodes the output iteration order: later + trees (prefix ``x``) are innermost, earlier trees (``y``, ``z``) + are outer. Within a tree, dict order goes inner-to-outer. + The innermost variable gets coefficient 1; each successive + variable (moving outward) multiplies by the previous range. + + Returns ``{sympy.Symbol: int}`` mapping each RT var to its store + coefficient, or ``None`` on failure. + """ + prefix_groups: dict[str, list] = {} + prefix_order: list[str] = [] + for v in ordered: + node = self.range_tree_nodes[v] + p = node.prefix + if p not in prefix_groups: + prefix_groups[p] = [] + prefix_order.append(p) + prefix_groups[p].append(v) + inner_to_outer: list = [] + for p in reversed(prefix_order): + inner_to_outer.extend(prefix_groups[p]) + coeffs: dict = {} + coeff = 1 + for v in inner_to_outer: + sz = self._safe_int(self.range_tree_nodes[v].length) + if sz is None: + return None + coeffs[v] = coeff + coeff *= sz + return coeffs + + def _get_full_load_permutation( + self, name: str, index: sympy.Expr + ) -> tuple[int, ...] | None: + """Return permutation for a full-array load, or None. + + Computes the permutation by mapping each range-tree variable to + both an output dimension (via store coefficients + actual output + strides) and an input dimension (via load coefficients + input + C-contiguous strides). The permutation is then: + + perm[out_dim] = in_dim for each RT variable + + Using actual output strides (not C-contiguous) is critical: the + scheduler may choose a non-standard output layout (e.g. column- + major) to optimise for transposed inputs. + + When all dimensions collapse to a single flat RT variable (e.g. + (2,2,2,2,2) with all dims size 2), infers the permutation + directly from output strides vs input C-contiguous strides. + """ + info = self._get_buffer_info(name) + if not info: + return None + _, buf_size, _, _, is_contiguous = info + in_shape_raw = [self._safe_int(s) for s in buf_size] + if len(in_shape_raw) < 2 or None in in_shape_raw: + return None + in_shape: list[int] = cast(list[int], in_shape_raw) + if not is_contiguous: + return None # .contiguous() at JAX boundary handles this + + # Extract index coefficients for each non-reduction RT variable. + iter_used = self._get_used_iter_vars(index) + ordered = [ + s + for s, e in self.range_tree_nodes.items() + if s in iter_used and not e.is_reduction + ] + if len(ordered) != len(in_shape): + # All dims may have collapsed to a single flat RT variable + # (e.g. (2,2,2,2,2) → single x0 of length 32). In this + # case, infer the permutation directly from output strides + # vs input C-contiguous strides. + n = len(in_shape) + if len(ordered) == 1 and self._safe_int( + self.range_tree_nodes[ordered[0]].length + ) == math.prod(in_shape): + in_strides = self._c_contiguous_strides(in_shape) + for out_name in self._output_buffer_names: + out_buf = V.graph.get_buffer(out_name) + if out_buf is None: + continue + out_shape = [self._safe_int(s) for s in out_buf.get_size()] + if any(s is None for s in out_shape) or len(out_shape) != n: + continue + actual = self._get_actual_out_strides(out_buf, n) + if actual is None: + break + # Map each output dim to the input dim with the + # same stride. + perm = self._map_coeffs_to_dims(actual, in_strides) + if perm is None: + break + if list(perm) == list(range(n)): + return None + return tuple(perm) + return None + coeffs_raw = [ + self._get_index_coefficient(V.graph.sizevars.simplify(index), v) + for v in ordered + ] + if not all(isinstance(c, int) and c > 0 for c in coeffs_raw): + return None + coeffs: list[int] = cast(list[int], coeffs_raw) + + n = len(ordered) + in_strides = self._c_contiguous_strides(in_shape) + store_coeffs = self._compute_store_coeffs(ordered) + + # --- Primary path: dimension-mapping with actual output strides --- + if store_coeffs is not None: + for out_name in self._output_buffer_names: + out_buf = V.graph.get_buffer(out_name) + if out_buf is None: + continue + out_shape = [self._safe_int(s) for s in out_buf.get_size()] + if any(s is None for s in out_shape) or len(out_shape) != n: + continue + + actual = self._get_actual_out_strides(out_buf, n) + if actual is not None: + rt_to_out = self._map_coeffs_to_dims( + [store_coeffs[v] for v in ordered], actual + ) + rt_to_in = self._map_coeffs_to_dims(list(coeffs), in_strides) + if rt_to_out is not None and rt_to_in is not None: + perm = [0] * n + for k in range(n): + perm[rt_to_out[k]] = rt_to_in[k] + if list(perm) == list(range(n)): + return None + return tuple(perm) + break + + return None + + def _get_collapsed_load_permutation( + self, name: str, index: sympy.Expr + ) -> tuple[tuple[int, ...], tuple[int, ...]] | None: + """Handle permutation when range tree has collapsed dimensions. + + When simplify_and_reorder merges contiguous dims, the range tree + has fewer variables than the buffer's rank. This method detects + the permutation in the collapsed space and returns + (collapsed_input_shape, perm) so the caller can generate: + jnp.permute_dims(load.reshape(collapsed_shape), perm) + + Uses index coefficients on both sides: load-index coefficients + map vars to collapsed input dims, and store-side coefficients + (derived from the range tree nesting) map vars to collapsed + output dims. Both sets of strides are always unique, so + matching is unambiguous even with duplicate group sizes. + """ + info = self._get_buffer_info(name) + if not info: + return None + _, buf_size, _, _, is_contiguous = info + in_shape_raw = [self._safe_int(s) for s in buf_size] + if len(in_shape_raw) < 2 or None in in_shape_raw: + return None + in_shape: list[int] = cast(list[int], in_shape_raw) + if not is_contiguous: + return None + + iter_used = self._get_used_iter_vars(index) + ordered = [ + s + for s, e in self.range_tree_nodes.items() + if s in iter_used and not e.is_reduction + ] + n = len(ordered) + if n < 2 or n >= len(in_shape): + return None + ranges_raw = [self._safe_int(self.range_tree_nodes[v].length) for v in ordered] + if None in ranges_raw: + return None + ranges: list[int] = cast(list[int], ranges_raw) + if math.prod(ranges) != math.prod(in_shape): + return None + + # Group consecutive input dims (right-to-left) to match ranges + in_groups = self._group_dims_to_ranges(in_shape, ranges) + if in_groups is None: + return None + + # Compute collapsed input strides (row-major) and use load-index + # coefficients to map each range tree var to a collapsed input dim. + # Strides are always unique, so this is unambiguous even when + # group sizes are duplicated. + collapsed_in_strides = [0] * n + stride = 1 + for i in range(n - 1, -1, -1): + collapsed_in_strides[i] = stride + stride *= in_groups[i] + + simplified = V.graph.sizevars.simplify(index) + in_coeffs_raw = [self._get_index_coefficient(simplified, v) for v in ordered] + if not all(isinstance(c, int) and c > 0 for c in in_coeffs_raw): + return None + in_coeffs: list[int] = cast(list[int], in_coeffs_raw) + + in_stride_to_dim = {s: i for i, s in enumerate(collapsed_in_strides)} + var_to_in_dim = [] + for coeff in in_coeffs: + dim = in_stride_to_dim.get(coeff) + if dim is None: + return None + var_to_in_dim.append(dim) + + store_coeffs = self._compute_store_coeffs(ordered) + if store_coeffs is None: + return None + + # Find the output-side mapping using store coefficients. + for out_name in self._output_buffer_names: + out_buf = V.graph.get_buffer(out_name) + if out_buf is None: + continue + out_shape_raw = [self._safe_int(s) for s in out_buf.get_size()] + if any(s is None for s in out_shape_raw) or len(out_shape_raw) < 2: + continue + out_shape: list[int] = cast(list[int], out_shape_raw) + if math.prod(out_shape) != math.prod(in_shape): + continue + out_groups = self._group_dims_to_ranges(out_shape, list(in_groups)) + if out_groups is None: + continue + + # Compute collapsed output strides and match store coefficients. + collapsed_out_strides = [0] * n + stride = 1 + for i in range(n - 1, -1, -1): + collapsed_out_strides[i] = stride + stride *= out_groups[i] + + out_stride_to_dim = {s: j for j, s in enumerate(collapsed_out_strides)} + var_to_out_dim = [] + for v in ordered: + j = out_stride_to_dim.get(store_coeffs[v]) + if j is None: + return None + var_to_out_dim.append(j) + + # Build perm: perm[out_dim] = in_dim + perm = [0] * n + for k in range(n): + perm[var_to_out_dim[k]] = var_to_in_dim[k] + if perm == list(range(n)): + return None + return (tuple(in_groups), tuple(perm)) + return None + + @staticmethod + def _group_dims_to_ranges(dims: list[int], ranges: list[int]) -> list[int] | None: + """Group consecutive dims (right-to-left) to match range values. + + Returns collapsed shape (left-to-right) or None if no valid grouping. + """ + available = list(ranges) + groups: list[int] = [] + product = 1 + for i in range(len(dims) - 1, -1, -1): + product *= dims[i] + try: + idx = available.index(product) + except ValueError: + continue + groups.append(product) + available.pop(idx) + product = 1 + if product != 1 or available: + return None + groups.reverse() + return groups + + def _get_index_expr(self, index: sympy.Expr) -> _BufferIndexing: + """Get the index expression string and whether it needs flattening.""" + has_indirect = self._has_indirect_vars(index) + has_iter_vars = self._has_iteration_vars(index) + + if has_indirect and has_iter_vars: + return _BufferIndexing( + index_str=self._handle_mixed_indexing(index), needs_flatten=True + ) + elif has_indirect: + return _BufferIndexing(index_str=self.kexpr(index), needs_flatten=False) + else: + index_str = self._get_index_str(index) + # Check if index contains ModularIndexing - this requires flattened access + # ModularIndexing is used for roll/wrap-around operations + needs_flatten = index.has(ModularIndexing) and index_str != "..." + # If index_str is an actual expression (not "..." or a slice pattern), + # we need flattened access because it uses block variables + if not needs_flatten and index_str != "...": + # Check if it's a simple slice pattern (::N or M::N) + if not ("::" in index_str or index_str.lstrip("-").isdigit()): + needs_flatten = True + return _BufferIndexing(index_str=index_str, needs_flatten=needs_flatten) + + @staticmethod + def _safe_int(val: Any) -> int | None: + """Convert value to int, returning None on failure.""" + try: + return int(val) + except (TypeError, ValueError): + return None + + @staticmethod + def _c_contiguous_strides(shape: list[int]) -> list[int]: + """Return C-contiguous strides for the given shape.""" + n = len(shape) + strides = [1] * n + for i in range(n - 2, -1, -1): + strides[i] = strides[i + 1] * shape[i + 1] + return strides + + @staticmethod + def _map_coeffs_to_dims(coeffs: list[int], strides: list[int]) -> list[int] | None: + """Map coefficient values to dimension indices via stride matching. + + Returns a list where entry k is the dimension whose stride equals + coeffs[k], or None if the mapping is ambiguous or incomplete. + """ + stride_to_dim: dict[int, int] = {} + for d, s in enumerate(strides): + if s in stride_to_dim: + return None # duplicate strides + stride_to_dim[s] = d + mapping: list[int] = [] + for c in coeffs: + d = stride_to_dim.get(c) + if d is None: + return None + mapping.append(d) + if len(OrderedSet(mapping)) != len(coeffs): + return None + return mapping + + def _zero_dim_output_flags(self, ctx: _CodegenContext) -> tuple[bool, bool]: + """Return whether an output has a zero or unknown dimension.""" + has_unknown_dim = False + for buf_name in ctx.output_buffer_lookup.values(): + buf = V.graph.try_get_buffer(buf_name) + if buf is None: + has_unknown_dim = True + continue + for dim in buf.get_size(): + dim_int = self._safe_int(dim) + if dim_int == 0: + return True, has_unknown_dim + if dim_int is None: + has_unknown_dim = True + return False, has_unknown_dim + + def _get_reduction_axes(self) -> tuple[int, ...]: + """Determine which axes of the loaded array are reduction axes. + + Finds the innermost reduction stride from the load index + expression, then walks outward through the buffer's dims + using stride ratios until the accumulated product reaches + red_numel. Falls back to stride-direction analysis for + gather/flatten loads. + """ + if not self.load_index_exprs: + return (-1,) + + r_vars = [v for v, e in self.range_tree_nodes.items() if e.is_reduction] + pw_vars = [v for v, e in self.range_tree_nodes.items() if not e.is_reduction] + if not r_vars or not pw_vars: + return (-1,) + + red_numel = self._compute_reduction_numel() + if not red_numel or red_numel <= 1: + return (-1,) + + for buf_name, load_index in self.load_index_exprs.items(): + info = self._get_buffer_info(buf_name) + if info is None: + continue + _, buf_size, _, actual_strides, _ = info + nd = len(buf_size) + if nd < 2: + continue + strides_or_none = [self._safe_int(s) for s in actual_strides] + if any(s is None for s in strides_or_none): + continue + strides: list[int] = cast(list[int], strides_or_none) + + # Get reduction stride coefficients by zeroing pw_vars. + r_only = load_index + for pv in pw_vars: + r_only = r_only.subs(pv, 0) + r_coeffs: OrderedSet[int] = OrderedSet() + for term in sympy.Add.make_args(r_only): + if term.is_number: + continue + coeff, _ = term.as_coeff_Mul() + c = self._safe_int(coeff) + if c is not None and c > 0: + r_coeffs.add(c) + if not r_coeffs: + continue + + # Match all coefficients against buffer strides + matched = sorted( + (i for i in range(nd) if strides[i] in r_coeffs), + ) + if not matched: + continue + + # Multiple r_vars each map to a distinct dim — return directly. + # Single r_var with multiple coefficients (transposed access) + # → skip to fallback. + if len(r_coeffs) > 1: + if len(r_coeffs) == len(matched) and len(r_vars) > 1: + return tuple(i - nd for i in matched) + continue + + # Single coefficient: walk outward from the matched dim + # using span to find flattened contiguous dims. + r_stride = next(iter(r_coeffs)) + span = (red_numel - 1) * r_stride + is_contiguous = all(strides[i] > strides[i + 1] for i in range(nd - 1)) + if is_contiguous: + # Walk by dim index (strides are in descending order) + inner = matched[-1] + start = inner + while start > 0 and span > strides[start - 1]: + start -= 1 + axes = list(range(start, inner + 1)) + else: + # Non-contiguous layout: collect dims whose strides + # fall within the r_var's traversal range + axes = sorted( + i + for i in range(nd) + if r_stride <= strides[i] and strides[i] < span + r_stride + ) + if not axes: + axes = list(matched) + return tuple(i - nd for i in axes) + + # Fallback: stride-direction for gather/flatten loads + load_index = next(iter(self.load_index_exprs.values())) + r_coeff = load_index.coeff(r_vars[0]) + r_stride = self._safe_int(r_coeff) if r_coeff != 0 else 1 + if r_stride is None: + r_stride = 1 + pw_coeff = load_index.coeff(pw_vars[0]) + pw_stride = self._safe_int(pw_coeff) if pw_coeff != 0 else 1 + if pw_stride is None: + pw_stride = 1 + if r_stride > pw_stride: + return (0,) + return (-1,) + + def _compute_prefix_numel(self, prefixes: OrderedSet) -> int | None: + """Compute total numel for given prefixes (e.g., pointwise prefixes).""" + result = 1 + for p in prefixes: + if p in self.numels: + numel = self._safe_int(self.numels[p]) + if numel is None: + return None + result *= numel + return result + + def _compute_reduction_numel(self) -> int | None: + """Compute total reduction numel.""" + result = 1 + for tree in self.range_trees: + if tree.is_reduction: + numel = self._safe_int(tree.numel) + if numel is None: + return None + result *= numel + return result + + def _can_use_tma_approach(self) -> bool: + """ + Check if TMA (Tensor Memory Accelerator) approach can be used. + TMA works for simple element-wise ops but not for: + - Reductions (need different accumulation patterns) + TODO: TMA supports float64 for loading but not for reductions + - Broadcasting (inputs have different shapes or output differs) + - Non-contiguous tensors (strided, transposed) + """ + # TMA flattens to 1D tiles, incompatible with permutation detection + # which emits jnp.permute_dims expecting N-D input. + if self.permuted_input_buffers: + return False + + # Check for reductions + reduction_numel = self._compute_reduction_numel() + if reduction_numel is not None and reduction_numel > 1: + return False + + # Check all input buffers for contiguity, dtype, and shape consistency + input_shapes: list[tuple] = [] + for name in self.args.input_buffers: + info = self._get_buffer_info(name) + if info is None: + return False + buf_obj, buf_size, buf_numel, actual_strides, is_contiguous = info + if not is_contiguous: + return False + + # Check for unsupported dtypes + # TODO: TMA supports float64 for loading but current JAX Mosaic GPU + # implementation doesn't support it yet. Re-enable when JAX adds support. + buf_dtype = getattr(buf_obj, "get_dtype", lambda: None)() + if buf_dtype is not None: + import torch + + if buf_dtype == torch.float64: + return False + + # Collect shape as tuple for comparison + shape_tuple = tuple(self._safe_int(s) for s in buf_size) + if None in shape_tuple: + return False # Dynamic shapes not supported + input_shapes.append(shape_tuple) + + # Check if all input shapes are identical (no broadcasting) + if input_shapes and len(OrderedSet(input_shapes)) > 1: + return False + + # Check that output numel matches input numel (no broadcasting expansion) + if input_shapes: + input_numel = 1 + for s in input_shapes[0]: + input_numel *= s + + # Compute output numel from pointwise range trees (non-reduction) + output_numel = 1 + for tree in self.range_trees: + if not tree.is_reduction: + numel = self._safe_int(tree.numel) + if numel is None: + return False # Dynamic shapes not supported + output_numel *= numel + + if output_numel != input_numel: + return False + + return True + + def _get_buffer_info(self, name: str) -> tuple[Any, Any, Any, list, bool] | None: + """Get buffer metadata (buf_obj, buf_size, buf_numel, actual_strides, is_contiguous). + + Returns None if the buffer doesn't exist. + """ + buf_obj = V.graph.get_buffer(name) + if buf_obj is None: + return None + buf_size = buf_obj.get_size() + buf_numel = 1 + for s in buf_size: + sval = self._safe_int(s) + buf_numel *= sval if sval is not None else s + + # Get buffer strides and check contiguity + actual_strides: list = [] + is_contiguous = True + + layout = getattr(buf_obj, "get_layout", lambda: None)() + buf_stride = getattr(layout, "stride", None) if layout else None + + if buf_stride is not None: + for i in range(len(buf_size)): + actual_stride = self._safe_int(buf_stride[i]) + actual_strides.append(actual_stride) + + # Check contiguity + if len(buf_size) == 1: + if actual_strides[0] is not None and actual_strides[0] != 1: + is_contiguous = False + elif len(buf_size) > 1: + expected_stride = 1 + for i in range(len(buf_size) - 1, -1, -1): + actual_stride = actual_strides[i] + if actual_stride is None or actual_stride != expected_stride: + is_contiguous = False + dim_size = self._safe_int(buf_size[i]) + if dim_size is not None: + expected_stride *= dim_size + + return buf_obj, buf_size, buf_numel, actual_strides, is_contiguous + + def _compute_output_numel_from_index( + self, index: sympy.Expr + ) -> tuple[int, OrderedSet]: + """Compute expected output numel and used vars from iteration variables in index.""" + used_vars = self._get_used_iter_vars(index) + + used_range_lengths = [] + for var in used_vars: + if var in self.range_tree_nodes: + entry = self.range_tree_nodes[var] + length_val = self._safe_int(entry.length) + if length_val is not None: + used_range_lengths.append(length_val) + + output_numel = 1 + for l in used_range_lengths: + output_numel *= l + + return output_numel, used_vars + + def _get_index_coefficients( + self, index: sympy.Expr, used_vars: OrderedSet + ) -> OrderedSet: + """ + Extract coefficients of iteration variables from index expression. + """ + coefficients: OrderedSet = OrderedSet() + for var in used_vars: + var_expr = BlockPatternMatcher.get_subexpr_involving_symbol(index, var) + stride = BlockPatternMatcher.match_affine_block_expr(var_expr, var) + if stride is None: + stride = 1 # Variable without explicit coefficient has stride 1 + coef = self._safe_int(stride) + coefficients.add(coef if coef is not None else stride) + return coefficients + + def _check_gather_pattern( + self, + buf_size: list, + actual_strides: list, + is_contiguous: bool, + coefficients: OrderedSet, + ) -> bool: + """ + Check if access pattern requires gather (non-standard striding). + """ + expected_strides = [1] # 1D buffers have stride 1 + + if len(buf_size) > 1: + expected_stride = 1 + expected_strides = [] + for i in range(len(buf_size) - 1, -1, -1): + expected_strides.insert(0, expected_stride) + dim_size = self._safe_int(buf_size[i]) + if dim_size is not None: + expected_stride *= dim_size + + if is_contiguous: + # Buffer is contiguous - check if access coefficients match expected strides + expected_stride_set = OrderedSet(expected_strides) + for coef in coefficients: + if coef not in expected_stride_set: + return True + else: + # Buffer is NOT contiguous (strided input) + # Check if coefficients match actual buffer strides + actual_stride_set = OrderedSet(s for s in actual_strides if s is not None) + for coef in coefficients: + if coef not in actual_stride_set: + return True + + return False + + def _needs_strided_indexing( + self, + name: str, + index: sympy.Expr, + indexing: _BufferIndexing, + ) -> _BufferIndexing: + """Check if buffer access needs strided indexing due to size mismatch or gather patterns.""" + # Only applies when full array access is indicated + if indexing.index_str != "..." or indexing.needs_flatten: + return indexing + + info = self._get_buffer_info(name) + if info is None: + return indexing + + buf_obj, buf_size, buf_numel, actual_strides, is_contiguous = info + output_numel, used_vars = self._compute_output_numel_from_index(index) + all_iter_vars = self._get_iter_vars() + coefficients = self._get_index_coefficients(index, used_vars) + + # Check for gather pattern + has_non_unit_strides = self._check_gather_pattern( + buf_size, actual_strides, is_contiguous, coefficients + ) + + # Check for im2col-like pattern (more iter vars used than buffer dims) + buf_effective_dims = sum(1 for s in buf_size if self._safe_int(s) != 1) + not_all_vars_used = ( + len(used_vars) < len(all_iter_vars) + and len(used_vars) > 0 + and buf_effective_dims > 1 + and len(used_vars) > len(buf_size) + ) + + # Check various conditions for skipping strided indexing + is_tpu = V.graph.get_current_device_or_throw().type == "tpu" + is_known_non_contiguous = not is_contiguous and all( + s is not None for s in actual_strides + ) + has_symbolic_coef = any(not isinstance(c, int | float) for c in coefficients) + skip_for_non_contiguous = ( + is_known_non_contiguous and not is_tpu and buf_numel == output_numel + ) + + # Determine if strided indexing is needed + if ( + output_numel > 0 + and (buf_numel != output_numel or not_all_vars_used or has_non_unit_strides) + and len(used_vars) > 0 + and not skip_for_non_contiguous + and not has_symbolic_coef + ): + return _BufferIndexing( + index_str=self._generate_strided_index(index), needs_flatten=True + ) + + return indexing + + def _adjust_index_for_buffer_shape( + self, + name: str, + index: sympy.Expr, + indexing: _BufferIndexing, + ) -> _BufferIndexing: + """ + Adjust index expression based on buffer shape (0-dim scalar, multi-dim, etc.). + """ + if indexing.needs_flatten or indexing.index_str == "...": + return indexing + + buf_obj = V.graph.get_buffer(name) + if buf_obj is None: + return indexing + + buf_size = buf_obj.get_size() + + # 0-dimensional (scalar) buffer - use [...] to access it + if len(buf_size) == 0: + return _BufferIndexing( + index_str="...", needs_flatten=indexing.needs_flatten + ) + + # Multi-dimensional buffer with constant/scalar index + if len(buf_size) > 1: + has_iter_vars = self._has_iteration_vars(index) + if not has_iter_vars: + return _BufferIndexing( + index_str=indexing.index_str, needs_flatten=True + ) # Use flattened access + elif "::" in indexing.index_str: + # Strided slice patterns need flattened indexing for multi-dim + return _BufferIndexing( + index_str=self._generate_strided_index(index), needs_flatten=True + ) + + # GPU doesn't support gather from slice patterns on 1D buffers + if self.is_gpu and "::" in indexing.index_str: + return _BufferIndexing( + index_str=self._generate_strided_index(index), needs_flatten=True + ) + + return indexing + + def _try_multidim_slice( + self, + name: str, + index: sympy.Expr, + indexing: _BufferIndexing, + ) -> _BufferIndexing: + """ + Try to emit multi-dim slice notation instead of flatten + gather. + + For a buffer with shape (d0, ..., dk) and index `stride * var + offset`, + emit `buf[:, ..., :, offset::stride]` when stride divides dk. + """ + if not indexing.needs_flatten: + return indexing + + buf_obj = V.graph.get_buffer(name) + if buf_obj is None: + return indexing + + buf_size = buf_obj.get_size() + ndim = len(buf_size) + if ndim < 2: + return indexing + + # Need a single iteration variable with an affine index + used_vars = self._get_used_iter_vars(index) + if len(used_vars) != 1: + return indexing + + var = next(iter(used_vars)) + var_expr = BlockPatternMatcher.get_subexpr_involving_symbol(index, var) + stride = self._safe_int( + BlockPatternMatcher.match_affine_block_expr(var_expr, var) + ) + if stride is None or stride <= 1: + return indexing + + offset = V.graph.sizevars.simplify(index - var_expr) + try: + offset_val = int(offset) + except (TypeError, ValueError): + return indexing + + if offset_val < 0 or offset_val >= stride: + return indexing + + last_dim = self._safe_int(buf_size[-1]) + if last_dim is None or last_dim % stride != 0: + return indexing + + # Verify the iteration variable covers all buffer elements at the + # given stride: var_length * stride == buf_numel. This ensures + # the flattened stride-access 0, stride, 2*stride, ... maps exactly + # to buf[:, ..., :, offset::stride]. + entry = self.range_tree_nodes.get(var) + if entry is None: + return indexing + var_length = self._safe_int(entry.length) + buf_numel = 1 + for s in buf_size: + d = self._safe_int(s) + if d is None: + return indexing + buf_numel *= d + if var_length is None or var_length * stride != buf_numel: + return indexing + + prefix = ":, " * (ndim - 1) + if offset_val == 0: + slice_str = f"{prefix}::{stride}" + else: + slice_str = f"{prefix}{offset_val}::{stride}" + return _BufferIndexing(index_str=slice_str, needs_flatten=False) + + @staticmethod + def _gather_permute_expr(load_expr: str, perm: tuple[int, ...]) -> str: + """Generate gather-based permutation instead of jnp.permute_dims. + + Avoids a Mosaic compiler bug where jnp.permute_dims produces + corrupted output tensors on TPU for 3D+ arrays. Uses + pallas_permute which flattens to 1D and does a 1D gather. + """ + return f"pallas_permute({load_expr}, {perm})" + + def _trace_to_load_source(self, var_name: str) -> str | None: + """Trace a tmp variable back to its source buffer's kernel param. + + Follows CSE assignments backward through bounds-checking (where/clamp) + until it finds a variable that was directly loaded from a buffer. + """ + if var_name in self._cse_to_param: + return self._cse_to_param[var_name] + for line in self.compute._lines: + line_str = str(line).lstrip() + if not line_str.startswith(f"{var_name} = "): + continue + for ref in re.findall(r"\btmp\d+\b", line_str.split(" = ", 1)[1]): + result = self._trace_to_load_source(ref) + if result is not None: + return result + return None + + def _detect_indirect_access( + self, buf: str, name: str, index: sympy.Expr + ) -> _IndirectAccessInfo | None: + """Detect a load with data-dependent indexing suitable for scalar prefetch. + + Matches exactly one indirect variable whose coefficient corresponds to + a C-contiguous stride dimension. Rejects 1-to-1 gather patterns where + the indices buffer covers the full iteration space. + """ + buf_info = self._get_buffer_info(name) + if buf_info is None: + return None + _, buf_size, _, _, _ = buf_info + buf_size_raw = [self._safe_int(s) for s in buf_size] + if len(buf_size_raw) < 2 or any(s is None for s in buf_size_raw): + return None + buf_size_ints: list[int] = cast(list[int], buf_size_raw) + + indirect_vars = self._get_indirect_vars(index) + if len(indirect_vars) != 1: + return None + indirect_var = indirect_vars[0] + + coeff = self._get_index_coefficient(index, indirect_var) + if coeff == 0 or not isinstance(coeff, int): + return None + + # Use existing stride mapping to find which dimension is indirected + strides = self._c_contiguous_strides(buf_size_ints) + mapping = self._map_coeffs_to_dims([coeff], strides) + if mapping is None: + return None + indirect_dim = mapping[0] + + ndim = len(buf_size_ints) + if indirect_dim >= max(1, ndim - 2): + return None + + indirect_var_name = str(indirect_var) + indices_param = self._trace_to_load_source(indirect_var_name) + if indices_param is None: + return None + + # Reject gather patterns: only 1-D static index tensors supported + indices_graph_name = self._param_to_graph_name.get(indices_param) + if indices_graph_name is not None: + indices_info = self._get_buffer_info(indices_graph_name) + if indices_info is not None: + _, indices_size, _, _, _ = indices_info + if len(indices_size) != 1: + return None + if self._safe_int(indices_size[0]) is None: + return None + indices_numel = math.prod( + v for s in indices_size if (v := self._safe_int(s)) is not None + ) + iter_product = math.prod( + length + for var in self._get_used_iter_vars(index) + if var in self.range_tree_nodes + if (length := self._safe_int(self.range_tree_nodes[var].length)) + is not None + ) + if indices_numel >= iter_product: + return None + + return _IndirectAccessInfo( + table_param=buf, + table_buf_name=name, + table_shape=tuple(buf_size_ints), + indirect_dim=indirect_dim, + indirect_var=indirect_var_name, + indices_param=indices_param, + ) + + def _eliminate_dead_indirect_code(self) -> None: + """Remove dead compute lines after scalar prefetch replaces indirect load. + + When the table load is simplified to buf[0] (scalar prefetch handles + indexing), the indices load and all derived bounds-checking code become + dead. This performs backward liveness analysis from the store variables + to identify and remove dead lines. + """ + # Collect variables used by stores (live roots) + live_vars: OrderedSet[str] = OrderedSet() + for _, store_line in self.store_with_output: + for m in re.finditer(r"\btmp\d+\b", store_line): + live_vars.add(m.group()) + + # Parse assignments from compute lines + assignments: list[tuple[str | None, str, Any]] = [] + for line in self.compute._lines: + line_str = str(line).lstrip() + m = re.match(r"^(tmp\d+)\s*=\s*(.*)", line_str, re.DOTALL) + if m: + assignments.append((m.group(1), m.group(2), line)) + else: + assignments.append((None, line_str, line)) + + # Propagate liveness backward + changed = True + while changed: + changed = False + for var_name, rhs, _ in reversed(assignments): + if var_name and var_name in live_vars: + for m in re.finditer(r"\btmp\d+\b", rhs): + if m.group() not in live_vars: + live_vars.add(m.group()) + changed = True + + # Keep only live assignments (and non-assignment lines) + self.compute._lines = [ + line + for var_name, _, line in assignments + if var_name is None or var_name in live_vars + ] + + def _build_load_expr( + self, + buf: str, + name: str, + index: sympy.Expr, + indexing: _BufferIndexing, + ) -> str: + """ + Build the load expression based on indexing mode. + """ + if indexing.needs_flatten: + # Detect indirect (data-dependent) access for scalar prefetch + indirect = self._detect_indirect_access(buf, name, index) + if indirect is not None: + if self.indirect_access is not None: + # Fused nodes may re-visit the same indirect load (e.g. + # a reduction + pointwise over the same embedding). + # Allow that, but reject truly different indirect accesses. + assert indirect == self.indirect_access, ( + "only one indirect access per kernel supported" + ) + self.indirect_access = indirect + return f"{buf}[0]" + + self.has_flatten_indexing = True + self.flatten_indexed_buffers.add(name) + # Flatten then index for non-contiguous access (gather operation) + has_minmax = index.has(sympy.Min) or index.has(sympy.Max) + idx_dtype = "jnp.int32" if self.is_tpu else "jnp.int64" + idx = ( + f"({indexing.index_str}).astype({idx_dtype})" + if has_minmax + else indexing.index_str + ) + return f"{buf}[...].flatten()[{idx}]" + else: + # Direct indexing for contiguous access + load_expr = f"{buf}[{indexing.index_str}]" + + if indexing.index_str == "..." and not self.is_gpu: + perm = self._get_full_load_permutation(name, index) + if perm is not None: + load_expr = self._gather_permute_expr(load_expr, perm) + self.permuted_input_buffers[name] = perm + else: + collapsed = self._get_collapsed_load_permutation(name, index) + if collapsed is not None: + collapsed_shape, cperm = collapsed + load_expr = f"jnp.permute_dims({load_expr}, {cperm})" + # Don't store cperm in permuted_input_buffers as it's for the reshaped tensor + # not the original shape, which causes issues later when used for tiling + self.collapsed_reshape_inputs[name] = collapsed_shape + self.collapsed_output_shape = tuple( + collapsed_shape[p] for p in cperm + ) + + return load_expr + + def _maybe_squeeze_intermediate_buffer(self, name: str, load_expr: str) -> str: + """ + Squeeze (N,1) intermediate buffers when kernel has 1D graph inputs. + + This avoids wrong broadcasting: (N,) op (N,1) -> (N,N) instead of (N,) + """ + if not name.startswith("buf"): + return load_expr + + # Check if any input buffer is a 1D graph input + has_1d_input = any( + not buf_name.startswith("buf") + and (buf_obj := V.graph.get_buffer(buf_name)) is not None + and len(buf_obj.get_size()) == 1 + for buf_name in self.args.input_buffers + ) + + if has_1d_input: + buf_obj = V.graph.get_buffer(name) + if buf_obj is not None: + buf_size = buf_obj.get_size() + if len(buf_size) == 2 and buf_size[-1] == 1: + return f"jnp.squeeze({load_expr}, axis=-1)" + + return load_expr + + def _maybe_broadcast_1d_buffer( + self, name: str, index: sympy.Expr, load_expr: str + ) -> str: + """Reshape 1D buffers for higher-dim broadcasting in reduction kernels. + + When a 1D buffer (e.g. a reduction result from a prior kernel, or a + batch-norm parameter) is loaded into a kernel with 2+ iteration dims, + JAX right-aligns it for broadcasting: (N,) becomes (1, N). This is + wrong when the buffer corresponds to a non-trailing axis; we reshape + to (N, 1, ...) so broadcasting matches the correct axis. + """ + buf_obj = V.graph.get_buffer(name) + if buf_obj is None or len(buf_obj.get_size()) != 1: + return load_expr + + # Only graph inputs, not intermediate buffers — intermediates are + # already shaped by the IR and their dim order may not match the + # reference buffer used below for axis inference. + if name.startswith("buf"): + return load_expr + + buf_length = self._safe_int(buf_obj.get_size()[0]) + if buf_length is None: + return load_expr + + dtype = V.graph.get_dtype(name) + if dtype is not None and not dtype.is_floating_point: + return load_expr + + # Find a higher-dimensional reference buffer + ref_buf_size = None + for buf_name in self.args.input_buffers: + other_buf = V.graph.get_buffer(buf_name) + if other_buf is not None and len(other_buf.get_size()) > 1: + ref_buf_size = [self._safe_int(s) for s in other_buf.get_size()] + if all(s is not None for s in ref_buf_size): + break + ref_buf_size = None + if ref_buf_size is None or len(ref_buf_size) <= 1: + return load_expr + + # Must use exactly one iteration variable + used_vars = self._get_used_iter_vars(index) + if len(used_vars) != 1: + return load_expr + used_var = next(iter(used_vars)) + if used_var not in self.range_tree_nodes: + return load_expr + + # Verify buffer length matches variable length + entry = self.range_tree_nodes[used_var] + if self._safe_int(entry.length) != buf_length: + return load_expr + + # Buffer length must uniquely match one non-reduction iteration variable. + # If multiple pointwise vars share the same length (e.g. 2D pointwise + # kernel with both dims equal), the axis is ambiguous and we bail out. + matching_vars = [ + v + for v, e in self.range_tree_nodes.items() + if self._safe_int(e.length) == buf_length and not e.is_reduction + ] + if len(matching_vars) != 1: + return load_expr + + # Determine axis position from the iteration variable's position + # in the range tree (pointwise vars first, then reduction vars). + axis_pos = None + matching_dims = [i for i, s in enumerate(ref_buf_size) if s == buf_length] + if len(matching_dims) == 1: + axis_pos = matching_dims[0] + else: + # Ambiguous by size (e.g. square tensor with reduction). + # Use the variable's position in the range tree. + pw_idx = 0 + for sym, e in self.range_tree_nodes.items(): + if sym == used_var: + axis_pos = pw_idx + break + if not e.is_reduction: + pw_idx += 1 + + if axis_pos is None: + return load_expr + if axis_pos == len(ref_buf_size) - 1: + return load_expr # Last dim uses default broadcasting + + reshape_dims = [1] * len(ref_buf_size) + reshape_dims[axis_pos] = -1 + return f"{load_expr}.reshape({', '.join(map(str, reshape_dims))})" + + def _check_im2col_pattern( + self, index: sympy.Expr, indexing: _BufferIndexing + ) -> _BufferIndexing: + """ + Check for im2col-like patterns where store uses block variables but load doesn't. + + For cat/expand patterns, both load and store prepared indices share block vars. + For im2col patterns, store compresses to block vars but load doesn't. + """ + if indexing.index_str != "..." or indexing.needs_flatten: + return indexing + + prepared_index = self.prepare_indexing(index) + iter_vars = self._get_iter_vars() + store_orig_vars = self._get_used_iter_vars(index) + store_prep_vars = ( + prepared_index.free_symbols + if hasattr(prepared_index, "free_symbols") + else OrderedSet() + ) & iter_vars + new_vars = store_prep_vars - store_orig_vars + + # Only trigger if store introduces new block vars + if not new_vars or len(store_orig_vars) <= 1: + return indexing + + # Check if loads are compatible with broadcast or cat pattern + has_im2col_pattern = False + for buf_name, load_index in self.load_index_exprs.items(): + load_orig_vars = self._get_used_iter_vars(load_index) + if not load_orig_vars: + continue + + # Load has iteration variables + if load_orig_vars != store_orig_vars: + continue + + # Same vars - check if load gets compressed too + prep_load = self.prepare_indexing(load_index) + load_prep_vars = ( + prep_load.free_symbols + if hasattr(prep_load, "free_symbols") + else OrderedSet() + ) & iter_vars + + # If store compresses but load doesn't, check for strided input vs im2col + if load_orig_vars != load_prep_vars or store_prep_vars == store_orig_vars: + continue + + # Check if load coefficients match buffer strides + if not self._check_load_is_strided_input( + buf_name, load_index, load_orig_vars + ): + has_im2col_pattern = True + break + + if has_im2col_pattern: + return _BufferIndexing( + index_str=self._generate_strided_index(prepared_index), + needs_flatten=True, + ) + + return indexing + + def _check_load_is_strided_input( + self, buf_name: str, load_index: sympy.Expr, load_orig_vars: OrderedSet + ) -> bool: + """ + Check if load coefficients match buffer strides (strided input vs im2col). + """ + buf = V.graph.get_buffer(buf_name) + if buf is None: + return False + + layout = getattr(buf, "get_layout", lambda: None)() + if layout is None: + return False + + buf_strides = getattr(layout, "stride", None) + if buf_strides is None: + return False + + buf_sizes = buf.get_size() + + # Get load coefficients + load_coeffs = [] + for var in load_orig_vars: + var_expr = BlockPatternMatcher.get_subexpr_involving_symbol(load_index, var) + coef = BlockPatternMatcher.match_affine_block_expr(var_expr, var) + if coef is not None: + int_coef = self._safe_int(coef) + load_coeffs.append(int_coef if int_coef is not None else coef) + + # Check if coefficients match buffer strides + # Only include strides for non-trivial dimensions (size > 1) + buf_stride_set = OrderedSet() + for i, s in enumerate(buf_strides): + dim_size = self._safe_int(buf_sizes[i]) + if dim_size is None or dim_size > 1: + int_s = self._safe_int(s) + buf_stride_set.add(int_s if int_s is not None else s) + + return OrderedSet(load_coeffs) == buf_stride_set + + def _check_store_needs_transpose(self, name: str) -> bool: + """ + Check if output needs transpose for column-major storage. + + Transpose on store is needed when: + - Output has column-major stride (s0 < s1) + - But input(s) have row-major stride + - And we haven't already transposed on load + """ + if self.permuted_input_buffers: + return False + + info = self._get_buffer_info(name) + if info is None: + return False + + _, buf_size, _, actual_strides, _ = info + if len(actual_strides) != 2 or len(buf_size) != 2: + return False + + size0 = self._safe_int(buf_size[0]) + size1 = self._safe_int(buf_size[1]) + s0 = actual_strides[0] + s1 = actual_strides[1] + + # Check if output is column-major with valid dimensions + if not ( + s0 is not None + and s1 is not None + and s0 < s1 + and size0 is not None + and size1 is not None + and size0 > 1 + and size1 > 1 + ): + return False + + # Check if any input is column-major (if so, no transpose needed) + for inp_name in self.args.input_buffers: + inp_info = self._get_buffer_info(inp_name) + if inp_info is None: + continue + _, _, _, inp_strides, _ = inp_info + if len(inp_strides) != 2: + continue + inp_s0 = inp_strides[0] + inp_s1 = inp_strides[1] + if inp_s0 is not None and inp_s1 is not None and inp_s0 < inp_s1: + return False # Input is also column-major + + return True + + def _build_full_array_store_expr( + self, out: str, value: CSEVariable, needs_transpose: bool + ) -> list[str]: + """ + Build store expression for full array assignment. + + Handles scalar broadcast, shape matching, and optional transpose. + Returns a list of lines to emit (variable assignment + store). + """ + lines = [f"_val = jnp.asarray({value})"] + if needs_transpose: + lines.append( + f"{out}[...] = " + f"jnp.full({out}.shape, _val) if _val.ndim == 0 " + f"else jnp.transpose(_val)" + ) + else: + lines.append( + f"{out}[...] = " + f"jnp.full({out}.shape, _val) if _val.ndim == 0 " + f"else (_val.reshape({out}.shape) if _val.size == {out}.size " + f"else jnp.broadcast_to(_val, {out}.shape))" + ) + return lines + + def _build_store_expr( + self, + out: str, + name: str, + index: sympy.Expr, + value: CSEVariable, + indexing: _BufferIndexing, + mode: Any = None, + ) -> list[str]: + """ + Build the store expression based on indexing mode. + mode can be None (set) or "atomic_add" (accumulate). + Returns a list of lines to emit. + """ + if indexing.index_str == "...": + # Full array store with shape matching + needs_transpose = self._check_store_needs_transpose(name) + return self._build_full_array_store_expr(out, value, needs_transpose) + + if indexing.needs_flatten: + self.has_flatten_indexing = True + # Block variable indexing (e.g., im2col) - use flattened scatter + scatter_op = "add" if mode == "atomic_add" else "set" + return [ + f"{out}[...] = {out}[...].flatten().at[({indexing.index_str}).flatten()].{scatter_op}(" + f"jnp.asarray({value}).flatten()).reshape({out}.shape)" + ] + + # Direct indexed assignment + has_indirect = self._has_indirect_vars(index) + buf = V.graph.get_buffer(name) + + if buf is not None: + buf_size = buf.get_size() + if len(buf_size) > 1 and not self._has_iteration_vars(index): + # Multi-dim output with constant index - use [...] for full assignment + return self._build_full_array_store_expr(out, value, False) + + if has_indirect: + # Indirect indexed store (scatter): use .add() for atomic_add, .set() otherwise + scatter_op = "add" if mode == "atomic_add" else "set" + lines = [f"_val = jnp.asarray({value})"] + value_expr = f"(jnp.full({indexing.index_str}.shape, _val) if _val.ndim == 0 else {value})" + if mode == "atomic_add": + # For atomic_add, mark output as needing to be readable (for aliasing) + self.outputs_need_read.add(out) + alias_param = f"{out}_alias" + lines.append( + f"{out}[...] = {alias_param}[...].flatten().at[({indexing.index_str}).flatten()].{scatter_op}(" + f"{value_expr}.flatten()).reshape({out}.shape)" + ) + else: + lines.append(f"{out}[{indexing.index_str}] = {value_expr}") + return lines + + return [f"{out}[{indexing.index_str}] = {value}"] + + def _build_scatter_store_expr( + self, + out: str, + value: CSEVariable, + scatter_info: dict[str, Any], + name: str, + mode: Any, + ) -> str: + """Build store expression for scatter operations (indirect indexing).""" + is_point_scatter = scatter_info.get("is_point_scatter", False) + + # Mark this output parameter as needing to be readable (for aliasing) + self.outputs_need_read.add(out) + alias_param = f"{out}_alias" + + # Use .add() for atomic_add mode, .set() otherwise + scatter_op = "add" if mode == "atomic_add" else "set" + + if is_point_scatter: + # Single-element scatter + indirect_var = scatter_info["indirect_var"] + indirect_dim = scatter_info["indirect_dim"] + output_shape = scatter_info["output_shape"] + + # Build index tuple with 0s for other dimensions + index_parts = [] + for dim in range(len(output_shape)): + if dim == indirect_dim: + index_parts.append(indirect_var) + else: + index_parts.append("0") + + index_tuple = ", ".join(index_parts) + return f"{out}[...] = {alias_param}[...].at[{index_tuple}].{scatter_op}({value})" + + # Scatter with iteration variables + indirect_var = scatter_info["indirect_var"] + dims_before = scatter_info["dims_before"] + dims_after = scatter_info["dims_after"] + + # Determine if element-wise or slice-based scatter + buf = V.graph.get_buffer(name) + output_ndim = len(buf.get_size()) if buf is not None else 0 + + num_iter_vars_in_store = len(dims_before) + len(dims_after) + total_kernel_iter_vars = len(self.range_tree_nodes) + remaining_dims = output_ndim - 1 # dims other than indirect + + is_element_wise = ( + num_iter_vars_in_store == remaining_dims + and num_iter_vars_in_store == total_kernel_iter_vars + ) + + if is_element_wise: + # Element-wise scatter: use iteration variable names + index_parts = [var_name for var_name, size in dims_before] + + # Reshape indirect var for broadcasting if needed + n_leading = len(dims_before) + n_trailing = len(dims_after) + if n_leading > 0 and n_trailing > 0: + leading_ones = "None, " * n_leading + trailing_nones = ", None" * n_trailing + indirect_reshaped = f"{indirect_var}[{leading_ones}...{trailing_nones}]" + else: + indirect_reshaped = indirect_var + index_parts.append(indirect_reshaped) + + index_parts.extend(var_name for var_name, size in dims_after) + else: + # Slice-based scatter: use : for iteration dimensions + index_parts = [":" for _ in dims_before] + index_parts.append(indirect_var) + index_parts.extend(":" for _ in dims_after) + + index_tuple = ", ".join(index_parts) + return ( + f"{out}[...] = {alias_param}[...].at[{index_tuple}].{scatter_op}({value})" + ) + + @typing_extensions.override + def load(self, name: str, index: sympy.Expr) -> CSEVariable: + buf = self.args.input(name) + dtype = V.graph.get_dtype(name) + + # Track the load index expression for argmax/argmin axis detection + self.load_index_exprs[name] = index + + # Get base index expression + indexing = self._get_index_expr(index) + + # Check for buffer size mismatch requiring strided indexing + indexing = self._needs_strided_indexing(name, index, indexing) + + # Try strided decomposition before multidim slice or flatten. + # This generates reshape + static indexing which works on both + # CPU and TPU (unlike slice notation which fails on Mosaic). + decomp = self._decompose_strided_access(index, name) + if decomp is not None: + self.strided_input_buffers[name] = decomp + load_expr = self._strided_load_expr(buf, decomp) + else: + # Adjust index for buffer shape (scalar, multi-dim, etc.) + indexing = self._adjust_index_for_buffer_shape(name, index, indexing) + + # Try to emit multi-dim slice instead of flatten + gather + indexing = self._try_multidim_slice(name, index, indexing) + + # Build the load expression + load_expr = self._build_load_expr(buf, name, index, indexing) + + # Handle intermediate buffer squeezing for correct broadcasting + if not indexing.needs_flatten and indexing.index_str == "...": + load_expr = self._maybe_squeeze_intermediate_buffer(name, load_expr) + # Handle 1D buffer broadcasting for higher-dimensional kernels + load_expr = self._maybe_broadcast_1d_buffer(name, index, load_expr) + + cse_var = self.cse.generate( + self.compute, + load_expr, + dtype=dtype, + ) + # Track CSE var -> param -> graph name for indirect access detection + buf_param = self.args.input(name) + self._cse_to_param[str(cse_var)] = buf_param + self._param_to_graph_name[buf_param] = name + return cse_var + + def _handle_mixed_indexing(self, index: sympy.Expr) -> str: + """ + Handle indexing with both indirect variables and iteration variables. + + For example, x[indices, :] generates index = i0 + stride * tmp0 + where tmp0 is loaded from indices and i0 is the iteration variable. + + We need to convert this to JAX advanced indexing with proper broadcasting. + When there are multiple iteration variables, they need different shapes + to form an outer product (grid) rather than broadcasting together. + + Special case: For gather operations where a single iteration variable + and single indirect variable have the same extent, they should be + element-wise aligned, not broadcast into an outer product. + + PyTorch advanced indexing semantics: When multiple indirect indices have + the same shape, they are paired element-wise (not outer product), and + the combined result dimension appears at the FRONT of the output. + """ + used_iter_vars_set = self._get_used_iter_vars(index) + + # Track which iteration variables are used + self.used_iter_vars.update(used_iter_vars_set) + + if len(used_iter_vars_set) == 0: + return self.kexpr(index) + + # Sort iteration variables by their coefficient (stride) in the index expression. + # Variables with larger strides correspond to earlier output dimensions. + # Use inf default so symbolic coefficients sort as outermost dimensions. + def _coeff(var): + return self._get_index_coefficient(index, var, default=float("inf")) + + used_iter_vars = sorted(used_iter_vars_set, key=_coeff, reverse=True) + iter_coeffs = [_coeff(var) for var in used_iter_vars] + + # Rename symbolic sizes to kernel parameter names + index_str = self.kexpr(self.rename_indexing(index)) + indirect_var_syms = self._get_indirect_vars(index) + indirect_vars = [str(sym) for sym in indirect_var_syms] + + # Get coefficients for indirect vars to determine output ordering + indirect_coeffs = {str(s): _coeff(s) for s in indirect_var_syms} + + # Special case: reduction var + single indirect var = element-wise gather + # Reduction vars (r prefix) iterate over the reduction dimension, and when paired + # with an indirect var, both are aligned to that dimension (element-wise). + # Pointwise vars form output dimensions and need the complex reshape code. + if len(used_iter_vars) == 1 and len(indirect_vars) == 1: + var = used_iter_vars[0] + var_name = str(var) + is_reduction_var = ( + var in self.range_tree_nodes and self.range_tree_nodes[var].is_reduction + ) + + if is_reduction_var: + # Reduction var: simple element-wise gather + if var in self.range_tree_nodes: + range_entry = self.range_tree_nodes[var] + range_size = range_entry.length + # Rename to use kernel parameter names for symbolic sizes + renamed_size = self.rename_indexing(range_size) + arange_expr = f"jnp.arange({self.kexpr(renamed_size)})" + index_str = index_str.replace(var_name, arange_expr) + return index_str + # For pointwise vars, fall through to the complex reshape code + + # Check if multiple indirect vars should be paired element-wise. + # In PyTorch, when multiple advanced indices have the same shape, they pair up. + # The paired dimension goes to the FRONT of the output. + # However, if indirect vars have different shapes (e.g., (1,4) and (4,1)), + # they form an outer product instead. + # We detect element-wise pairing when: + # 1. Multiple indirect vars exist + # 2. There's exactly ONE unused iteration variable (for the shared paired dim) + # For outer product, there are MULTIPLE unused iter vars (one per indirect dim) + paired_indirect = False + if len(indirect_vars) > 1: + # Count unused iteration variables (defined but not in index expression) + unused_iter_vars = self._get_iter_vars() - used_iter_vars_set + # Element-wise pairing: one unused iter var for the shared paired dimension + # Outer product: multiple unused iter vars (one for each indirect var dimension) + paired_indirect = len(unused_iter_vars) == 1 + + if paired_indirect: + # Multiple indirect vars with element-wise pairing + # Output order: (paired_indirect_dim, iter_var_dims...) + # All indirect vars get the same shape: (N, 1, 1, ...) for first dim + # Iter vars come after: second dim onwards + + # Count total output dims: 1 (paired) + len(iter_vars) for non-newaxis + # But some iter vars may be for newaxis dimensions (size 1) + n_output_dims = 1 + len(used_iter_vars) + + # Reshape indirect vars to occupy the first dimension + for indirect_var in indirect_vars: + trailing_ones = ", 1" * len(used_iter_vars) + reshape_expr = f"{indirect_var}.reshape(-1{trailing_ones})" + index_str = index_str.replace(indirect_var, reshape_expr) + + # Reshape iteration variables to occupy subsequent dimensions + # Sort by coefficient (descending) to determine order + for i, var in enumerate(used_iter_vars): + var_name = str(var) + if var in self.range_tree_nodes: + range_entry = self.range_tree_nodes[var] + range_size = range_entry.length + # Rename to use kernel parameter names for symbolic sizes + renamed_size = self.rename_indexing(range_size) + + # Shape: (1, ..., N, ..., 1) where N is at position i+1 + # Position 0 is for paired indirect vars + shape_parts = ["1"] * n_output_dims + shape_parts[i + 1] = self.kexpr(renamed_size) + shape_str = ", ".join(shape_parts) + arange_expr = ( + f"jnp.arange({self.kexpr(renamed_size)}).reshape({shape_str})" + ) + + index_str = index_str.replace(var_name, arange_expr) + + return index_str + + # Single indirect var case (or no indirect vars handled above) + # Build a sorted list of all components by coefficient (descending) + # Each component is (coeff, type, var) where type is 'iter' or 'indirect' + all_components = [] + for var in used_iter_vars: + all_components.append((_coeff(var), "iter", var)) + for sym in indirect_var_syms: + all_components.append((_coeff(sym), "indirect", sym)) + all_components.sort(key=lambda x: x[0], reverse=True) + + # Calculate trailing dims needed for each component + # Each component needs trailing dims for all subsequent iter vars + # plus trailing dims for all dimensions of subsequent indirect vars + # For simplicity, assume each indirect var contributes some dimensions + # that will be handled by the reshape at store time + + # For iter vars, we need to count how many dimensions come after in the output + for i, var in enumerate(used_iter_vars): + var_name = str(var) + if var in self.range_tree_nodes: + range_entry = self.range_tree_nodes[var] + range_size = range_entry.length + # Rename to use kernel parameter names for symbolic sizes + renamed_size = self.rename_indexing(range_size) + var_coeff = _coeff(var) + + arange_expr = f"jnp.arange({self.kexpr(renamed_size)})" + + # Count trailing dims needed: + # - One for each subsequent iter var (with smaller coeff) + # - One for each dimension of indirect vars with smaller coeff + # For indirect vars, assume each contributes 2 dims (common case) + # The actual reshape at store time will fix any shape mismatches + n_trailing_iter = sum(1 for c in iter_coeffs if c < var_coeff) + n_trailing_indirect = sum( + 2 for c in indirect_coeffs.values() if c < var_coeff + ) + n_trailing = n_trailing_iter + n_trailing_indirect + + if n_trailing > 0: + trailing_dims = ", None" * n_trailing + arange_expr = f"{arange_expr}[:{trailing_dims}]" + + index_str = index_str.replace(var_name, arange_expr) + + # Reshape indirect variables for proper broadcasting. + for indirect_var in indirect_vars: + indirect_coeff = indirect_coeffs[indirect_var] + + # Count dims needed before and after this indirect var + n_leading = sum(1 for c in iter_coeffs if c > indirect_coeff) + n_trailing = sum(1 for c in iter_coeffs if c < indirect_coeff) + + # Build the indexing expression with leading Nones, ellipsis, trailing Nones + if n_leading > 0 and n_trailing > 0: + leading_nones = "None, " * n_leading + trailing_nones = ", None" * n_trailing + reshape_expr = f"{indirect_var}[{leading_nones}...{trailing_nones}]" + elif n_leading > 0: + leading_nones = "None, " * n_leading + reshape_expr = f"{indirect_var}[{leading_nones}...]" + elif n_trailing > 0: + trailing_nones = ", None" * n_trailing + reshape_expr = f"{indirect_var}[...{trailing_nones}]" + else: + reshape_expr = indirect_var + + index_str = index_str.replace(indirect_var, reshape_expr) + + return index_str + + @typing_extensions.override + def store( + self, name: str, index: sympy.Expr, value: CSEVariable, mode: Any = None + ) -> None: + # mode can be None (set), "atomic_add" (accumulate), etc. + if mode is not None and mode != "atomic_add": + raise Unsupported(f"pallas store mode '{mode}' not supported") + out = self.args.output(name) + self.store_buffer_names.add(name) + + # Check if this is a scalar output (reduction to scalar) + buf = V.graph.get_buffer(name) + is_scalar = buf is not None and len(buf.get_size()) == 0 + + if is_scalar: + store_lines = [ + f"_val = jnp.asarray({value})", + f"{out}[...] = jnp.full({out}.shape, _val) if _val.ndim == 0 else _val.reshape({out}.shape)", + ] + else: + # When collapsed_output_shape is set, the load-side permutation + # already produces data in the correct layout for the collapsed + # output. Force a full-array store ("...") so the scatter index + # (which was computed for the original output layout) does not + # rearrange the permuted data. + if self.collapsed_output_shape is not None: + store_lines = self._build_full_array_store_expr(out, value, False) + else: + # Check for scatter pattern (indirect indexing for stores) + scatter_info = self._detect_scatter_pattern(index, name) + + if scatter_info is not None: + # Track iteration variables used in scatter index + self.used_iter_vars.update(self._get_used_iter_vars(index)) + store_lines = [ + self._build_scatter_store_expr( + out, value, scatter_info, name, mode + ) + ] + else: + # Get base index expression + indexing = self._get_index_expr(index) + + # Check for im2col-like patterns + indexing = self._check_im2col_pattern(index, indexing) + + # Build the store expression + store_lines = self._build_store_expr( + out, name, index, value, indexing, mode + ) + + for line in store_lines: + self.stores.writeline(line) + # Track which output param this store uses for filtering in codegen_kernel + self.store_with_output.append((out, line)) + + @staticmethod + def _get_index_coefficient( + index: sympy.Expr, var: sympy.Symbol, default: int | float = 0 + ) -> int | float: + """Get integer coefficient of a variable in an index expression.""" + coeff = index.coeff(var) + if coeff == 0: + coeff = sympy.diff(index, var) + try: + return int(coeff) + except (TypeError, ValueError): + return default + + def _detect_scatter_pattern( + self, index: sympy.Expr, output_name: str = "" + ) -> dict[str, Any] | None: + """Detect scatter operation pattern. Returns scatter info dict or None.""" + indirect_syms = self._get_indirect_vars(index) + if len(indirect_syms) != 1: + return None + + indirect_sym = indirect_syms[0] + indirect_var = str(indirect_sym) + indirect_coeff: int = int(self._get_index_coefficient(index, indirect_sym)) + if indirect_coeff == 0: + return None + + # Point scatter: no iteration variables, just indirect indexing + if not self._has_iteration_vars(index): + return self._detect_point_scatter(output_name, indirect_var, indirect_coeff) + + # Regular scatter: has both indirect and iteration variables + return self._detect_iter_scatter(index, indirect_var, indirect_coeff) + + def _detect_point_scatter( + self, output_name: str, indirect_var: str, indirect_coeff: int + ) -> dict[str, Any] | None: + """Detect single-element scatter pattern.""" + if not output_name: + return None + try: + buf = V.graph.get_buffer(output_name) + output_shape = [int(s) for s in buf.get_size()] + except Exception: + return None + + if len(output_shape) < 2: + return None + + # Find which dimension indirect var indexes based on coefficient + cumulative = 1 + indirect_dim = len(output_shape) - 1 + for dim in range(len(output_shape) - 1, -1, -1): + if indirect_coeff == cumulative: + indirect_dim = dim + break + cumulative *= output_shape[dim] + + return { + "indirect_var": indirect_var, + "indirect_dim": indirect_dim, + "dims_before": [], + "dims_after": [], + "is_point_scatter": True, + "output_shape": output_shape, + } + + def _detect_iter_scatter( + self, index: sympy.Expr, indirect_var: str, indirect_coeff: int + ) -> dict[str, Any] | None: + """Detect scatter pattern with iteration variables.""" + used_iter_vars = self._get_used_iter_vars(index) + + # Collect (var_name, coefficient, length) for each variable + all_vars: list[tuple[str, int, int]] = [] + for var in used_iter_vars: + coeff = int(self._get_index_coefficient(index, var)) + if coeff > 0 and var in self.range_tree_nodes: + length = self._safe_int(self.range_tree_nodes[var].length) + if length is None: + return None + all_vars.append((str(var), coeff, length)) + + all_vars.append((indirect_var, indirect_coeff, -1)) + all_vars.sort(key=lambda x: x[1], reverse=True) + + # Find indirect variable position + indirect_pos = next( + (i for i, (name, _, _) in enumerate(all_vars) if name == indirect_var), + None, + ) + if indirect_pos is None: + return None + + # Verify coefficients form valid stride pattern + expected = 1 + for _, coeff, length in reversed(all_vars[indirect_pos + 1 :]): + if coeff != expected: + return None + expected *= length + if indirect_coeff != expected: + return None + + return { + "indirect_var": indirect_var, + "indirect_dim": indirect_pos, + "dims_before": [(n, l) for n, _, l in all_vars[:indirect_pos]], + "dims_after": [(n, l) for n, _, l in all_vars[indirect_pos + 1 :]], + "is_point_scatter": False, + "output_shape": None, + } + + def reduction( + self, + dtype: torch.dtype, + src_dtype: torch.dtype, + reduction_type: ReductionType, + value: CSEVariable | tuple[CSEVariable, ...], + ) -> CSEVariable | tuple[CSEVariable, ...]: # type: ignore[override] + """ + Generate code for reduction operations in JAX/Pallas. + + Reductions in Pallas work by: + 1. Loading the input data into the kernel + 2. Applying JAX reduction operations (jnp.sum, jnp.max, etc.) + 3. Storing the reduced result + + The reduction happens over the loaded block of data. + """ + assert self.inside_reduction + + # Handle welford_reduce using the fallback (computes via sum reductions) + if reduction_type == "welford_reduce": + return self.welford_reduce_fallback(dtype, value) + + if isinstance(value, tuple): + raise Unsupported( + "Tuple reductions (e.g., welford_combine) not supported in Pallas backend" + ) + + # Check if this reduction is already cached. + cache_key = (src_dtype, reduction_type, value) + if cache_key in self.cse.reduction_cache: + return self.cse.reduction_cache[cache_key] + + # Map reduction types to JAX functions + reduction_ops = { + "sum": "jnp.sum", + "prod": "jnp.prod", # CPU only - not supported in Pallas GPU (Mosaic) backend + "max": "jnp.max", + "min": "jnp.min", + "any": "jnp.any", + "argmax": "jnp.argmax", + "argmin": "jnp.argmin", + } + + # Determine if this is a partial reduction (has pointwise dimensions) + # or a full reduction to scalar + pointwise_prefixes = OrderedSet(["x", "y", "z"]) + has_pointwise = any(p in self.numels for p in pointwise_prefixes) + pointwise_numel: int | None = self._compute_prefix_numel(pointwise_prefixes) + reduction_numel: int | None = self._compute_reduction_numel() + n_reduction_dims = sum( + 1 for var, entry in self.range_tree_nodes.items() if entry.is_reduction + ) + + is_partial_reduction = ( + has_pointwise + and pointwise_numel is not None + and pointwise_numel > 1 + and reduction_numel + and n_reduction_dims > 0 + ) + is_symbolic_partial = ( + has_pointwise and n_reduction_dims > 0 and pointwise_numel is None + ) + + if reduction_type == "xor_sum": + if is_partial_reduction: + axes = self._get_reduction_axes() + axis_expr = axes[0] if len(axes) == 1 else axes + reduction_expr = f"jnp.bitwise_xor.reduce({value}, axis={axis_expr})" + else: + reduction_expr = f"jnp.bitwise_xor.reduce({value})" + elif reduction_type in ("argmax", "argmin"): + reduction_op = reduction_ops[reduction_type] + if is_partial_reduction: + # argmax/argmin only accept a single axis + axes = self._get_reduction_axes() + reduction_expr = f"{reduction_op}({value}, axis={axes[-1]})" + else: + reduction_expr = f"{reduction_op}({value})" + elif reduction_type in reduction_ops: + reduction_op = reduction_ops[reduction_type] + if is_partial_reduction: + axes = self._get_reduction_axes() + axis_expr = axes[0] if len(axes) == 1 else axes + reduction_expr = ( + f"{reduction_op}({value}, axis={axis_expr}, keepdims=True)" + ) + elif is_symbolic_partial: + # With symbolic shapes, strided loads produce a degenerate + # batch dim at axis=0 that just needs squeezing. + reduction_expr = f"{reduction_op}({value}, axis=0)" + else: + reduction_expr = f"{reduction_op}({value})" + else: + raise Unsupported( + f"Reduction type '{reduction_type}' not yet supported in Pallas backend. " + f"Supported types: {list(reduction_ops.keys())}, xor_sum" + ) + + # Generate CSE variable for the reduction result + result = self.cse.generate( + self.compute, + reduction_expr, + dtype=dtype, + ) + + # Cache the result + self.cse.reduction_cache[cache_key] = result + return result + + @staticmethod + def _buffer_is_contiguous(buffer_name: str) -> bool: + buf = V.graph.get_buffer(buffer_name) + layout = buf.get_layout() + return layout.is_contiguous() + + def _can_tile_cpu_tpu(self) -> bool: + """Check if this kernel can use tiling on CPU/TPU. + + Tiling is compatible with reductions, transpositions, and multi-range-tree + kernels as long as no flatten-based indexing is used (buf[...].flatten()[idx]). + Flatten indexing requires global flat indices which don't decompose into + per-tile local indices. + + Reject: + - GPU (has its own TMA / padding path) + - Flatten-based indexing + - Scatter outputs (indirect indexing complicates tile boundaries) + """ + if self.is_gpu: + return False + if self.has_flatten_indexing: + return False + if self.outputs_need_read: + return False + + # If iteration variables appear in the compute body (not just in + # load/store index resolution that collapses to [...]), tiling is + # unsafe because the arange-based vars have full-tensor shapes. + # Exception: vars emitted in tile-relative form are safe. + if self.used_iter_vars: + compute_text = "\n".join(str(line) for line in self.compute._lines) + for var_sym in self.used_iter_vars: + if var_sym in self.tile_relative_iter_vars: + continue + if str(var_sym) in compute_text: + return False + + # Determine the reference output shape (highest-ndim output). + out_bufs = list(self.args.output_buffers.keys()) + + # Only check the current kernel's actual output buffers for transpose, + # not _has_column_major_output() which scans all graph buffers and can + # be triggered by unrelated intermediates (e.g., (N,1) reductions with + # degenerate column-major strides). + has_col_major_out = False + for buf_name in out_bufs: + info = self._get_buffer_info(buf_name) + if info is None: + continue + _, buf_size, _, actual_strides, _ = info + if len(actual_strides) >= 2 and len(buf_size) >= 2: + s0 = actual_strides[0] + s1 = actual_strides[1] + d0 = self._safe_int(buf_size[0]) + d1 = self._safe_int(buf_size[1]) + if ( + s0 is not None + and s1 is not None + and s0 < s1 + and d0 is not None + and d1 is not None + and d0 > 1 + and d1 > 1 + ): + has_col_major_out = True + break + self.tile_has_transpose = bool(self.permuted_input_buffers) or has_col_major_out + + # Count trailing reduction dimensions in the output shape that must + # not be tiled (the kernel body needs the full reduction range). + # Only count when the kernel actually performs reduction (numel > 1). + reduction_numel = self._compute_reduction_numel() + has_reduction = reduction_numel is not None and reduction_numel > 1 + self.tile_skip_last_n = ( + sum(1 for tree in self.range_trees if tree.is_reduction) + if has_reduction + else 0 + ) + + ref_shape: list[int] = [] + for buf_name in out_bufs: + info = self._get_buffer_info(buf_name) + if info is None: + return False + _, buf_size, _, _, _ = info + int_size = [self._safe_int(s) for s in buf_size] + if any(s is None for s in int_size): + return False + if len(int_size) > len(ref_shape): + ref_shape = int_size # type: ignore[assignment] + + if not ref_shape: + return False + + # For collapsed permutation kernels, override ref_shape with the + # collapsed output shape so all compatibility checks operate in + # collapsed-shape space. + if self.collapsed_output_shape is not None: + ref_shape = list(self.collapsed_output_shape) + + ref_nd = len(ref_shape) + + all_bufs = list(self.args.input_buffers) + out_bufs + has_tileable = False + for buf_name in all_bufs: + info = self._get_buffer_info(buf_name) + if info is None: + return False + _, buf_size, _, _, _ = info + if len(buf_size) == 0: + continue # scalar + + # Use collapsed shapes when available so dimension checks + # operate in the same space as the kernel. + if buf_name in self.collapsed_reshape_inputs: + int_size = list(self.collapsed_reshape_inputs[buf_name]) + elif self.collapsed_output_shape is not None and buf_name in out_bufs: + int_size = list(self.collapsed_output_shape) + else: + int_size = [self._safe_int(s) for s in buf_size] + if any(s is None for s in int_size): + return False + buf_nd = len(int_size) + + if buf_nd == ref_nd: + # Same ndim: check dimensions match or are broadcast (1). + # Allow strided buffers (dims may differ after reshape). + is_strided = buf_name in self.strided_input_buffers + mismatch = False + for i in range(ref_nd): + if ( + int_size[i] == ref_shape[i] + or int_size[i] == 1 + or ref_shape[i] == 1 + or is_strided + ): + continue + mismatch = True + break + + if mismatch and buf_name in self.permuted_input_buffers: + perm = self.permuted_input_buffers[buf_name] + if not ( + len(perm) == ref_nd + and all( + int_size[perm[i]] == ref_shape[i] + or int_size[perm[i]] == 1 + or ref_shape[i] == 1 + for i in range(ref_nd) + ) + ): + return False + elif mismatch: + return False + + # At least one buffer with a tileable dim + if is_strided or any( + int_size[i] == ref_shape[i] and ref_shape[i] > 1 + for i in range(ref_nd) + ): + has_tileable = True + + elif buf_nd > ref_nd: + # Reduction input with extra dims. Find an alignment offset k + # such that buf_shape[k+i] == ref_shape[i] for all i (skipping + # broadcast dims where ref_shape[i] == 1). + found = False + for k in range(buf_nd - ref_nd + 1): + ok = True + for i in range(ref_nd): + if ref_shape[i] == 1: + continue + if int_size[k + i] != ref_shape[i]: + ok = False + break + if ok: + found = True + break + if not found: + return False + has_tileable = True + + else: + # Fewer dims: verify numpy-style broadcastability + for a, b in zip(reversed(int_size), reversed(ref_shape)): + if a != b and a != 1 and b != 1: + return False + + if not has_tileable: + return False + + # On CPU (interpret mode) each tile iteration has significant + # Python/JAX overhead, so cap the grid size. Store the cap + # so _codegen_tiled_specs can pass it to pallas_compute_tiling, + # which will scale up tiles to stay within the limit. + is_tpu = V.graph.get_current_device_or_throw().type == "tpu" + if not is_tpu: + self._cpu_max_grid_product = 64 + else: + self._cpu_max_grid_product = None + + return True + + def codegen_kernel(self, name: str | None = None) -> str: # type: ignore[override] + """ + Generate the complete Pallas kernel code as a Python string. + + This includes: + - Import statements for JAX/Pallas + - The kernel function that operates on refs + - The main wrapper function that handles PyTorch<->JAX conversions via DLPack + + Args: + name: Optional kernel name (will use placeholder if not provided) + + Returns: + str: Complete Python source code for the Pallas kernel + """ + code = IndentedBuffer() + + # Define the Pallas kernel: accepts refs, uses broadcasted expressions + arg_defs, call_args, _, _ = self.args.python_argdefs() + kernel_params = [a.name for a in arg_defs] + pure_out_params = [p for p in kernel_params if p.startswith("out_ptr")] + output_params = [ + p for p in kernel_params if p.startswith(("out_ptr", "in_out_ptr")) + ] + # Identify size variable parameters (scalars like load_seed_offset) + size_var_names = OrderedSet(self.args.sizevars.values()) + size_var_params = [p for p in kernel_params if p in size_var_names] + if not output_params: + raise RuntimeError("Pallas backend requires at least one output buffer") + + output_buffer_lookup = { + inner: outer + for outer, inner in self.args.output_buffers.items() + if isinstance(inner, str) + } + + kernel_name = name or "" + interpret_is_cpu = V.graph.get_current_device_or_throw().type == "cpu" + interpret_literal = "True" if interpret_is_cpu else "False" + + aliasable_flags: dict[str, bool] = {} + for param in pure_out_params: + aliasable_flags[param] = True + alias_params = [ + f"{param}_alias" for param in pure_out_params if aliasable_flags[param] + ] + pointer_tail = [ + p for p in kernel_params if p.startswith(("in_out_ptr", "in_ptr")) + ] + kernel_input_params = alias_params + pointer_tail + full_kernel_params = alias_params + kernel_params + non_alias_out_set = OrderedSet( + [name for name, flag in aliasable_flags.items() if not flag] + ) + # On CPU (interpret=True), pallas_call returns new arrays so we must + # copy back every output. On TPU, call_custom_kernel with + # input_output_aliases handles donation (zero-copy), so no copy is + # needed. On CUDA, aliased outputs are mutated in-place by the + # donated-buffer mechanism so only non-aliased outputs need a copy. + if interpret_is_cpu: + copy_output_indices = list(range(len(output_params))) + elif self.is_tpu: + copy_output_indices = [] + else: + copy_output_indices = [ + idx + for idx, name in enumerate(output_params) + if name in non_alias_out_set + ] + + ctx = _CodegenContext( + code=code, + kernel_name=kernel_name, + is_tpu=self.is_tpu, + interpret_is_cpu=interpret_is_cpu, + interpret_literal=interpret_literal, + kernel_params=kernel_params, + pure_out_params=pure_out_params, + output_params=output_params, + size_var_params=size_var_params, + output_buffer_lookup=output_buffer_lookup, + aliasable_flags=aliasable_flags, + alias_params=alias_params, + pointer_tail=pointer_tail, + kernel_input_params=kernel_input_params, + full_kernel_params=full_kernel_params, + non_alias_out_set=non_alias_out_set, + copy_output_indices=copy_output_indices, + alias_pairs=[], + ) + self.aliasable_out_ptrs = aliasable_flags + + self._codegen_imports(ctx) + + kernel_body = IndentedBuffer() + with kernel_body.indent(): + self._codegen_iteration_vars(kernel_body, ctx) + + for line in self.compute._lines: + kernel_body.writeline(str(line)) + + # Recompute kernel parameters after kernel body generation. + # Size variables may have been registered during kernel body generation + # (e.g., via rename_indexing for symbolic sizes), so we need to re-fetch + # the arg defs to capture all parameters including newly-registered size vars. + arg_defs, call_args, _, _ = self.args.python_argdefs() + kernel_params = [a.name for a in arg_defs] + size_var_names = OrderedSet(self.args.sizevars.values()) + ctx.size_var_params = [p for p in kernel_params if p in size_var_names] + ctx.pointer_tail = [ + p for p in kernel_params if p.startswith(("in_out_ptr", "in_ptr")) + ] + ctx.kernel_input_params = alias_params + ctx.pointer_tail + ctx.full_kernel_params = alias_params + kernel_params + + # Decide whether to use tiling for CPU/TPU after kernel body is fully + # generated (used_iter_vars is populated during load/store codegen). + self.tile_cpu_tpu = self._can_tile_cpu_tpu() + + extra_kernel_params = "" + if self.tile_relative_iter_vars: + extra_kernel_params = ", _pallas_tile=None, _pallas_ax2g=None" + + ctx.alias_pairs = self._compute_alias_pairs(ctx, aliasable_flags) + + use_scalar_prefetch = bool(self.indirect_access) + + if use_scalar_prefetch: + self._eliminate_dead_indirect_code() + kernel_body_sp = IndentedBuffer() + with kernel_body_sp.indent(): + for line in self.compute._lines: + kernel_body_sp.writeline(str(line)) + self._codegen_scalar_prefetch_wrapper( + ctx, + kernel_name, + kernel_body_sp, + ) + return code.getvalue() + + # Emit the kernel function with the correct signature + kernel_signature = f"def {kernel_name}_kernel({', '.join(ctx.full_kernel_params)}{extra_kernel_params}):" + code.writeline(kernel_signature) + + with code.indent(): + self._emit_kernel_body(code, kernel_body, ctx) + + code.writeline("") + jit_wrapper_name = f"{kernel_name}_jit_wrapper" + donate_indices = [] + base_offset = 2 + len(ctx.size_var_params) + for idx, name in enumerate(ctx.kernel_input_params): + if (name in alias_params) or name.startswith("in_out_ptr"): + donate_indices.append(idx + base_offset) + if donate_indices: + donate_literal = "(" + ", ".join(str(x) for x in donate_indices) + ",)" + else: + donate_literal = "()" + static_argnums = list(range(2 + len(ctx.size_var_params))) + static_argnums_literal = "(" + ", ".join(str(x) for x in static_argnums) + ",)" + code.writeline( + "@functools.partial(" + f"jax.jit, static_argnums={static_argnums_literal}, donate_argnums=" + f"{donate_literal})" + ) + wrapper_params = ( + ["out_shapes", "out_dtypes"] + ctx.size_var_params + ctx.kernel_input_params + ) + code.writeline(f"def {jit_wrapper_name}({', '.join(wrapper_params)}):") + + alias_map_literal = ", ".join(f"{i}: {o}" for (i, o) in ctx.alias_pairs) + + has_zero_dim, has_unknown_dim = self._zero_dim_output_flags(ctx) + + zero_dim_return = ( + "results = tuple(jnp.empty(s, dtype=dt) " + "for s, dt in zip(out_shapes, out_dtypes))", + "return results if len(results) > 1 else results[0]", + ) + + with code.indent(): + if has_zero_dim: + code.writelines(zero_dim_return) + else: + if has_unknown_dim: + code.writeline("if any(0 in shape for shape in out_shapes):") + with code.indent(): + code.writelines(zero_dim_return) + # Pallas requires >= 1-d tensors; promote 0-d to (1,) + code.writeline( + "_pallas_out_shapes = tuple(" + "s if len(s) > 0 else (1,) for s in out_shapes)" + ) + if self.collapsed_output_shape is not None: + code.writeline( + f"_pallas_out_shapes = ({self.collapsed_output_shape},)" + ) + # Reshape aliased inputs to match promoted output shapes + for input_idx, out_idx in ctx.alias_pairs: + param = ctx.kernel_input_params[input_idx] + code.writeline( + f"{param} = {param}.reshape(_pallas_out_shapes[{out_idx}])" + ) + code.writeline("out_shapes_pallas = tuple(") + code.writeline(" jax.ShapeDtypeStruct(shape, dtype)") + code.writeline( + " for shape, dtype in zip(_pallas_out_shapes, out_dtypes)" + ) + code.writeline(")") + if self.tile_cpu_tpu: + self._codegen_tiled_specs(ctx) + else: + self._codegen_strided_reshapes(code, ctx.kernel_input_params) + for param in ctx.kernel_input_params: + buf_name = self._param_to_buf_name(param) + cshape = ( + self.collapsed_reshape_inputs.get(buf_name) + if buf_name + else None + ) + if cshape is not None: + code.writeline(f"{param} = {param}.reshape({cshape})") + + code.writeline("out_specs_pallas = tuple(") + code.writeline(" pallas_make_block_spec_non_tiled(shape)") + code.writeline( + " for shape, dtype in zip(_pallas_out_shapes, out_dtypes)" + ) + code.writeline(")") + code.writeline("in_specs_pallas = tuple(") + code.writeline(" pallas_make_block_spec_non_tiled(i.shape)") + code.writeline( + " for i in [" + ", ".join(ctx.kernel_input_params) + "]" + ) + code.writeline(")") + + if self.tile_relative_iter_vars: + if self.tile_cpu_tpu: + code.writeline("_pallas_tile = _tile") + code.writeline("_pallas_ax2g = _ax2g") + else: + code.writeline("_pallas_tile = _pallas_out_shapes[0]") + code.writeline("_pallas_ax2g = {}") + + # Wrap kernel with functools.partial to pass scalar arguments (size variables) + partial_args = [] + for sv_param in ctx.size_var_params: + partial_args.append(f"{sv_param}={sv_param}") + + if self.tile_relative_iter_vars: + partial_args.append("_pallas_tile=_pallas_tile") + partial_args.append("_pallas_ax2g=_pallas_ax2g") + + if partial_args: + kernel_arg = f"functools.partial({kernel_name}_kernel, {', '.join(partial_args)})," + else: + kernel_arg = f"{kernel_name}_kernel," + + use_tma = ( + self.is_gpu + and self.use_emit_pipeline + and self._can_use_tma_approach() + ) + if use_tma: + self._codegen_jit_wrapper_tma(ctx, kernel_arg) + elif self.is_gpu: + self._codegen_jit_wrapper_legacy_gpu(ctx, kernel_arg) + else: + self._codegen_jit_wrapper_cpu_tpu( + ctx, kernel_arg, ctx.alias_pairs, alias_map_literal + ) + + self._codegen_main_entry(ctx, jit_wrapper_name) + return code.getvalue() + + @staticmethod + def _compute_alias_pairs( + ctx: _CodegenContext, aliasable_flags: dict[str, bool] + ) -> list[tuple[int, int]]: + alias_pairs: list[tuple[int, int]] = [] + for out_idx, name in enumerate(ctx.output_params): + if name.startswith("out_ptr"): + if aliasable_flags.get(name, False): + alias_name = f"{name}_alias" + input_idx = ctx.kernel_input_params.index(alias_name) + alias_pairs.append((input_idx, out_idx)) + else: + input_idx = ctx.kernel_input_params.index(name) + alias_pairs.append((input_idx, out_idx)) + return alias_pairs + + def _emit_kernel_body( + self, + code: IndentedBuffer, + kernel_body: IndentedBuffer, + ctx: _CodegenContext, + ) -> None: + """Emit the kernel body lines and store operations into code.""" + for line in kernel_body._lines: + if isinstance(line, str): + code.writeline(line.lstrip()) + else: + code._lines.append(line) + for out_ptr, store_line in self.store_with_output: + if out_ptr in ctx.full_kernel_params: + code.writeline(store_line) + + def _codegen_scalar_prefetch_wrapper( + self, + ctx: _CodegenContext, + kernel_name: str, + kernel_body: IndentedBuffer, + ) -> None: + """Emit kernel, JIT wrapper, and main entry for scalar prefetch.""" + assert self.indirect_access is not None + indirect = self.indirect_access + code = ctx.code + + alias_set = OrderedSet(ctx.alias_params) + other_input_params = [ + p + for p in ctx.kernel_input_params + if p != indirect.indices_param + and p != indirect.table_param + and p not in alias_set + ] + + # Emit kernel function with params reordered for PrefetchScalarGridSpec: + # [scalar_prefetch] + [in_specs refs] + [out_specs refs] + prefetch_kernel_params = ( + [indirect.indices_param] + + [indirect.table_param] + + other_input_params + + list(ctx.alias_params) + + ctx.output_params + ) + code.writeline( + f"def {kernel_name}_kernel({', '.join(prefetch_kernel_params)}):" + ) + with code.indent(): + self._emit_kernel_body(code, kernel_body, ctx) + + # Emit JIT wrapper + code.writeline("") + jit_wrapper_name = f"{kernel_name}_jit_wrapper" + wrapper_params = ( + ["out_shapes", "out_dtypes"] + ctx.size_var_params + ctx.kernel_input_params + ) + static_argnums = list(range(2 + len(ctx.size_var_params))) + static_argnums_literal = "(" + ", ".join(str(x) for x in static_argnums) + ",)" + code.writeline( + f"@functools.partial(jax.jit, static_argnums={static_argnums_literal})" + ) + code.writeline(f"def {jit_wrapper_name}({', '.join(wrapper_params)}):") + + with code.indent(): + table = indirect.table_param + indices = indirect.indices_param + + ind_dim = indirect.indirect_dim + ndim = len(indirect.table_shape) + code.writeline("_D = 1") + for i in range(ndim): + if i != ind_dim: + code.writeline(f"_D = _D * {table}.shape[{i}]") + code.writeline(f"_seq = {indices}.shape[0]") + + if ind_dim == 0: + code.writeline(f"_table_3d = {table}.reshape({table}.shape[0], 1, _D)") + else: + perm = (ind_dim, *[d for d in range(ndim) if d != ind_dim]) + code.writeline( + f"_table_3d = {table}.transpose{perm}.reshape(" + f"{table}.shape[{ind_dim}], 1, _D)" + ) + + # Reshape other (non-table, non-indices) inputs to 3D to match the + # table's (seq, 1, D) layout. Currently handles: + # - 2D with leading dim == seq: row-aligned, reshape to (seq, 1, D) + # - 1D: broadcast scalar/vector, reshape to (1, 1, numel) + # - else: flatten to (1, 1, -1) — assumes broadcastable with + # (seq, 1, D). This may not work correctly for 3D+ inputs. + pallas_call_other_args = [] + for p in other_input_params: + p3d = f"_{p}_3d" + code.writeline(f"if {p}.ndim == 2 and {p}.shape[0] == _seq:") + code.writeline(f" {p3d} = {p}.reshape(_seq, 1, _D)") + code.writeline(f"elif {p}.ndim == 1:") + code.writeline(f" {p3d} = {p}.reshape(1, 1, {p}.shape[0])") + code.writeline("else:") + code.writeline(f" {p3d} = {p}.reshape(1, 1, -1)") + pallas_call_other_args.append(p3d) + + pallas_call_alias_args = [] + for p in ctx.alias_params: + p3d = f"_{p}_3d" + code.writeline(f"{p3d} = {p}.reshape(_seq, 1, _D)") + pallas_call_alias_args.append(p3d) + + partial_args = [f"{sv}={sv}" for sv in ctx.size_var_params] + if partial_args: + kernel_ref = ( + f"functools.partial({kernel_name}_kernel," + f" {', '.join(partial_args)})" + ) + else: + kernel_ref = f"{kernel_name}_kernel" + + # Reusable row-tiled BlockSpec (all i32 index_map for Mosaic compat) + code.writeline( + "_ROW_SPEC = pl.BlockSpec((1, 1, _D)," + " lambda i, _: (i, jnp.int32(0), jnp.int32(0)))" + ) + + num_non_alias_in_specs = 1 + len(pallas_call_other_args) + code.writeline("_in_specs = [") + with code.indent(): + code.writeline( + "pl.BlockSpec((1, 1, _D)," + " lambda gi, idx: (idx[gi], jnp.int32(0), jnp.int32(0)))," + ) + for p3d in pallas_call_other_args: + code.writeline( + f"_ROW_SPEC" + f" if {p3d}.shape[0] == _seq else" + f" pl.BlockSpec({p3d}.shape," + f" lambda i, _: (jnp.int32(0), jnp.int32(0), jnp.int32(0)))," + ) + for _ in ctx.alias_params: + code.writeline("_ROW_SPEC,") + code.writeline("]") + + num_outputs = len(ctx.output_params) + code.writeline( + "_out_specs = [" + ", ".join(["_ROW_SPEC"] * num_outputs) + "]" + ) + + # input_output_aliases: pallas_call arg index -> output index + # (offset by 1 for scalar prefetch arg) + alias_map_parts = [] + for out_idx, _ in enumerate(ctx.alias_params): + arg_idx = 1 + num_non_alias_in_specs + out_idx + alias_map_parts.append(f"{arg_idx}: {out_idx}") + alias_map_literal = ", ".join(alias_map_parts) + + out_shape_parts = [ + f"jax.ShapeDtypeStruct((_seq, 1, _D), out_dtypes[{i}])" + for i in range(num_outputs) + ] + out_shape_expr = "[" + ", ".join(out_shape_parts) + "]" + + code.writeline("_result = pl.pallas_call(") + with code.indent(): + code.writeline(f"{kernel_ref},") + code.writeline(f"out_shape={out_shape_expr},") + code.writeline("grid_spec=pltpu.PrefetchScalarGridSpec(") + with code.indent(): + code.writeline("num_scalar_prefetch=1,") + code.writeline("grid=(_seq,),") + code.writeline("in_specs=_in_specs,") + code.writeline("out_specs=_out_specs,") + code.writeline("),") + if alias_map_parts: + code.writeline(f"input_output_aliases={{ {alias_map_literal} }},") + if not self.is_tpu: + code.writeline(f"interpret={ctx.interpret_literal},") + + all_pallas_args = ( + [indices] + + ["_table_3d"] + + pallas_call_other_args + + pallas_call_alias_args + ) + code.writeline(f")({', '.join(all_pallas_args)})") + + code.writeline( + "return tuple(r.reshape(s) for r, s in zip(_result, out_shapes))" + ) + + self._codegen_main_entry(ctx, jit_wrapper_name) + + def _codegen_imports(self, ctx: _CodegenContext) -> None: + imports = """ +import functools +import math +import torch +import jax +import jax.numpy as jnp +from jax.experimental import pallas as pl +from torch.utils._ordered_set import OrderedSet +from torch._inductor.runtime.runtime_utils import ( + pallas_compute_tiling, pallas_make_block_spec, pallas_permute, + pallas_gpu_align_output_specs, pallas_gpu_pad_inputs, + pallas_gpu_unpad_results, + pallas_ensure_nonzero_rank, + pallas_make_block_spec_non_tiled, + torch_dtype_to_jax_runtime, +) +""" + if ctx.is_tpu: + imports += "\nimport jax.export" + imports += "\nfrom jax.experimental.pallas import tpu as pltpu" + imports += "\nfrom torch_tpu._internal.pallas import tpu_torch_pallas" + elif not ctx.interpret_is_cpu: + imports += "\nfrom jax.experimental.pallas import mosaic_gpu as plgpu" + if self.indirect_access and not ctx.is_tpu: + imports += ( + "\nimport os as _os; _os.environ.setdefault('JAX_PLATFORMS', 'cpu')" + ) + imports += "\nfrom jax.experimental.pallas import tpu as pltpu" + ctx.code.splice(imports, strip=True) + + def _get_iter_var_axis(self, var_sym: sympy.Symbol) -> int | None: + """Map an iteration variable to its output tensor axis index. + + Non-reduction variables map to axes 0, 1, 2, ... in order. + Reduction variables map to axes after all pointwise axes. + Returns None if the mapping cannot be determined. + """ + pw_idx = 0 + r_idx = 0 + n_pw = sum(1 for _, e in self.range_tree_nodes.items() if not e.is_reduction) + for sym, entry in self.range_tree_nodes.items(): + if sym == var_sym: + return pw_idx if not entry.is_reduction else n_pw + r_idx + if entry.is_reduction: + r_idx += 1 + else: + pw_idx += 1 + return None + + def _get_reshape_target_shape_and_numel( + self, + ) -> tuple[tuple[int, ...] | None, int | None]: + # Find reshape target: N-D shape whose numel matches an iteration + # var. Try output first (repeat/upsample), then inputs (reductions). + iter_lengths = OrderedSet( + [ + int(e.length) + for e in self.range_tree_nodes.values() + if isinstance(e.length, (int, sympy.Integer)) + ] + ) + + def _get_nd_shape_if_matches(buf_name): + buf = V.graph.try_get_buffer(buf_name) + if buf is None or len(buf.get_size()) <= 1: + return None, None + shape = tuple( + int(s) if isinstance(s, (int, sympy.Integer)) else s + for s in buf.get_size() + ) + numel = math.prod(shape) + return (shape, numel) if numel in iter_lengths else (None, None) + + candidate_buf_names = self._output_buffer_names.copy() + candidate_buf_names.extend(self.args.input_buffers) + + reshape_target_shape, reshape_target_numel = None, None + for buf_name in candidate_buf_names: + result = _get_nd_shape_if_matches(buf_name) + if result[0]: + reshape_target_shape, reshape_target_numel = result + break + + return reshape_target_shape, reshape_target_numel + + def _make_broadcasted_iteration_var_expr( + self, broadcast_vars: list[_BroadcastedIterVar], broadcast_idx: int + ) -> str: + bv = broadcast_vars[broadcast_idx] + length = bv.entry.length + renamed_length = self.rename_indexing(length) + length_str = self.kexpr(renamed_length) + + num_broadcast_dims = len(broadcast_vars) + axis_idx = self._broadcast_axis_idx( + broadcast_vars, broadcast_idx, num_broadcast_dims + ) + shape_parts = ["1"] * num_broadcast_dims + shape_parts[axis_idx] = length_str + shape_str = ", ".join(shape_parts) + arange = f"jnp.arange({length_str})" + reshaped = f"{arange}.reshape({shape_str})" + return reshaped + + def _codegen_iteration_vars( + self, kernel_body: IndentedBuffer, ctx: _CodegenContext + ) -> None: + # Generate iteration variables as jnp.arange arrays + # Skip on GPU - jnp.arange is not supported by Pallas Mosaic backend + if not (self.range_tree_nodes and not self.is_gpu and self.used_iter_vars): + return + + kernel_body.writeline("# Define iteration variables as JAX arrays") + + reshape_target_shape, reshape_target_numel = ( + self._get_reshape_target_shape_and_numel() + ) + + var_items = list(self.range_tree_nodes.items()) + + broadcast_vars = [] + total_var_idx = None + for idx, (var_sym, entry) in enumerate(var_items): + length_val = self._safe_int(entry.length) + if length_val is not None and length_val == reshape_target_numel: + total_var_idx = idx + else: + broadcast_vars.append( + _BroadcastedIterVar(idx, var_sym, entry, length_val) + ) + + num_broadcast_dims = len(broadcast_vars) + + for idx, (var_sym, entry) in enumerate(var_items): + if var_sym not in self.used_iter_vars: + continue + var_name = str(var_sym) + length = entry.length + renamed_length = self.rename_indexing(length) + length_str = self.kexpr(renamed_length) + length_val = self._safe_int(length) + + if length_val is None: + if ( + reshape_target_shape + and num_broadcast_dims > 1 + and idx != total_var_idx + ): + broadcast_idx = next( + (i for i, v in enumerate(broadcast_vars) if v.idx == idx), + None, + ) + if broadcast_idx is not None: + expr = self._make_broadcasted_iteration_var_expr( + broadcast_vars, broadcast_idx + ) + kernel_body.writeline(f"{var_name} = {expr}") + continue + kernel_body.writeline(f"{var_name} = jnp.arange({length_str})") + continue + + if ( + reshape_target_shape + and len(reshape_target_shape) > 1 + and length_val == reshape_target_numel + ): + shape_str = ", ".join(str(s) for s in reshape_target_shape) + arange = f"jnp.arange({length_str})" + kernel_body.writeline(f"{var_name} = {arange}.reshape({shape_str})") + elif num_broadcast_dims > 1 and idx != total_var_idx: + broadcast_idx = next( + i for i, v in enumerate(broadcast_vars) if v.idx == idx + ) + expr = self._make_broadcasted_iteration_var_expr( + broadcast_vars, broadcast_idx + ) + kernel_body.writeline(f"{var_name} = {expr}") + else: + # Simple 1D arange — emit tile-relative form so tiling is safe. + # When grid=(1,), _pallas_tile[ax] == full length and + # pl.program_id(0) == 0, so this degenerates to jnp.arange(N). + # Only do this when the var actually appears in compute body + # (otherwise tiling is not blocked and the full arange is fine). + # Skip for scatter/index kernels where the iter var is used + # as a global index, not a data value. + compute_text = "\n".join(str(line) for line in self.compute._lines) + var_in_compute = var_name in compute_text + can_tile_relative = ( + var_in_compute + and not self.is_gpu + and not self.outputs_need_read + and not self.has_flatten_indexing + and not entry.is_reduction + ) + axis_idx = ( + self._get_iter_var_axis(var_sym) if can_tile_relative else None + ) + if axis_idx is not None: + kernel_body.writeline( + f"{var_name} = jnp.arange(_pallas_tile[{axis_idx}])" + f" + pl.program_id(_pallas_ax2g.get({axis_idx}, 0))" + f" * _pallas_tile[{axis_idx}]" + ) + self.tile_relative_iter_vars.add(var_sym) + else: + kernel_body.writeline(f"{var_name} = jnp.arange({length_str})") + + @staticmethod + def _broadcast_axis_idx( + broadcast_vars: list[_BroadcastedIterVar], + broadcast_idx: int, + num_broadcast_dims: int, + ) -> int: + # Axis placement depends on var types (reduction r* vs x*): + # - Mixed: pointwise first, reduction last for output reshape + # - Same-type: reverse order, first var innermost + has_reduction_vars = any( + str(bv.var_sym).startswith("r") for bv in broadcast_vars + ) + has_pointwise_vars = any( + not str(bv.var_sym).startswith("r") for bv in broadcast_vars + ) + is_mixed = has_reduction_vars and has_pointwise_vars + if is_mixed: + return broadcast_idx + return num_broadcast_dims - 1 - broadcast_idx + + def _codegen_jit_wrapper_tma(self, ctx: _CodegenContext, kernel_arg: str) -> None: + code = ctx.code + kernel_input_params = ctx.kernel_input_params + output_params = ctx.output_params + + # TMA automatically handles out-of-bounds accesses + code.writeline("# Use lax.fori_loop with TMA for automatic OOB masking") + code.writeline("from jax import lax") + code.writeline("_tile_size = 128 # Warpgroup size") + code.writeline("_orig_out_shapes = out_shapes") + + code.writeline("_max_numel = 0") + for param in kernel_input_params: + code.writeline(f"_max_numel = max(_max_numel, {param}.size)") + code.writeline("for shape in out_shapes:") + code.writeline(" _max_numel = max(_max_numel, math.prod(shape))") + + code.writeline("_num_tiles = (_max_numel + _tile_size - 1) // _tile_size") + + gmem_input_params = [f"{p}_gmem" for p in kernel_input_params] + gmem_output_params = [f"{p}_gmem" for p in output_params] + smem_input_params = [f"{p}_smem" for p in kernel_input_params] + smem_output_params = [f"{p}_smem" for p in output_params] + + code.writeline("") + code.writeline("# Wrapper kernel using lax.fori_loop with direct TMA") + + wrapper_kernel_params = gmem_input_params + gmem_output_params + all_smem_params = smem_input_params + smem_output_params + barrier_params = [f"_barrier_{i}" for i in range(len(kernel_input_params))] + scratch_params = ", ".join(all_smem_params + barrier_params) + + code.writeline( + f"def _tma_kernel({', '.join(wrapper_kernel_params)}, *, {scratch_params}):" + ) + with code.indent(): + code.writeline("") + code.writeline("def _tile_body(_tile_idx, _):") + with code.indent(): + code.writeline("_tile_start = _tile_idx * _tile_size") + code.writeline("") + + code.writeline("# TMA load inputs from GMEM to SMEM (OOB auto-masked)") + for i, (gmem_in, smem_in) in enumerate( + zip(gmem_input_params, smem_input_params) + ): + code.writeline( + f"plgpu.copy_gmem_to_smem({gmem_in}.at[pl.ds(_tile_start, _tile_size)], {smem_in}, _barrier_{i})" + ) + + code.writeline("") + code.writeline("# Wait for TMA loads to complete") + for i, _ in enumerate(gmem_input_params): + code.writeline(f"plgpu.barrier_wait(_barrier_{i})") + + code.writeline("") + code.writeline("# Compute on SMEM tiles") + kernel_call_args = smem_input_params + smem_output_params + kernel_fn = kernel_arg.rstrip(",").strip() + code.writeline(f"{kernel_fn}({', '.join(kernel_call_args)})") + + code.writeline("") + code.writeline( + "# TMA store outputs from SMEM to GMEM (OOB auto-masked)" + ) + code.writeline("plgpu.commit_smem()") + for gmem_out, smem_out in zip(gmem_output_params, smem_output_params): + code.writeline( + f"plgpu.copy_smem_to_gmem({smem_out}, {gmem_out}.at[pl.ds(_tile_start, _tile_size)])" + ) + code.writeline("plgpu.wait_smem_to_gmem(0)") + code.writeline("") + code.writeline("return None") + + code.writeline("") + code.writeline("# Iterate over all tiles") + code.writeline("lax.fori_loop(0, _num_tiles, _tile_body, None)") + + # Build scratch_shapes dict + code.writeline("") + code.writeline( + "# Build SMEM scratch shapes for inputs, outputs, and TMA barriers" + ) + code.writeline("_scratch_shapes = {}") + for i, smem_param in enumerate(smem_input_params): + orig_param = kernel_input_params[i] + code.writeline( + f"_scratch_shapes['{smem_param}'] = plgpu.SMEM((_tile_size,), {orig_param}.dtype)" + ) + for i, smem_param in enumerate(smem_output_params): + code.writeline( + f"_scratch_shapes['{smem_param}'] = plgpu.SMEM((_tile_size,), out_dtypes[{i}])" + ) + for barrier_param in barrier_params: + code.writeline( + f"_scratch_shapes['{barrier_param}'] = plgpu.Barrier(num_arrivals=1)" + ) + + code.writeline("") + code.writeline("# Create flattened output specs aligned to tile size") + code.writeline( + "_flat_out_specs, _ = pallas_gpu_align_output_specs(out_shapes, out_dtypes, _tile_size)" + ) + + code.writeline("") + code.writeline("# Call plgpu.kernel with TMA kernel") + code.writeline("_result = plgpu.kernel(") + with code.indent(): + code.writeline("_tma_kernel,") + code.writeline("out_shape=_flat_out_specs,") + code.writeline("scratch_shapes=_scratch_shapes,") + code.writeline(")(") + for param in kernel_input_params: + code.writeline(f" {param}.flatten(),") + code.writeline(")") + + code.writeline("") + code.writeline("# Reshape results to original shapes") + code.writeline("return pallas_gpu_unpad_results(_result, _orig_out_shapes)") + + def _codegen_jit_wrapper_legacy_gpu( + self, ctx: _CodegenContext, kernel_arg: str + ) -> None: + code = ctx.code + kernel_input_params = ctx.kernel_input_params + input_list = f"[{', '.join(kernel_input_params)}]" + + # Legacy GPU path with explicit padding (use_emit_pipeline=False) + # Mosaic GPU requires tensor sizes to be multiples of 128. + # Only apply padding when all tensors have the same size (no broadcasting). + code.writeline("# Check if all tensors have same size (no broadcasting)") + code.writeline("_all_sizes = []") + for param in kernel_input_params: + code.writeline(f"_all_sizes.append({param}.size)") + code.writeline("for shape in out_shapes:") + code.writeline(" _all_sizes.append(math.prod(shape))") + code.writeline("_unique_sizes = OrderedSet(_all_sizes)") + code.writeline( + "_can_pad = len(_unique_sizes) == 1 and all(s > 1 for s in _unique_sizes)" + ) + + code.writeline("") + code.writeline("if _can_pad:") + code.writeline(" # All tensors same size - safe to flatten and pad") + code.writeline(f" _padded_inputs = pallas_gpu_pad_inputs({input_list})") + code.writeline( + " _aligned_out_specs, _is_scalar = pallas_gpu_align_output_specs(out_shapes, out_dtypes)" + ) + code.writeline(" _result = plgpu.kernel(") + code.writeline(" " + kernel_arg) + code.writeline(" out_shape=_aligned_out_specs,") + code.writeline(" )(*_padded_inputs)") + code.writeline( + " return pallas_gpu_unpad_results(_result, out_shapes, _is_scalar)" + ) + + code.writeline("else:") + code.writeline( + " # Different sizes - check if it's a reduction (scalar output)" + ) + code.writeline(" _out_numel = math.prod(out_shapes[0])") + code.writeline(" ") + code.writeline(" if _out_numel <= 1:") + code.writeline( + " # Scalar output (reduction) - pad inputs but keep scalar output" + ) + code.writeline(f" _padded_inputs = pallas_gpu_pad_inputs({input_list})") + code.writeline(" _aligned_out_specs = tuple(") + code.writeline(" jax.ShapeDtypeStruct(shape, dtype)") + code.writeline(" for shape, dtype in zip(out_shapes, out_dtypes)") + code.writeline(" )") + code.writeline(" _result = plgpu.kernel(") + code.writeline(" " + kernel_arg) + code.writeline(" out_shape=_aligned_out_specs,") + code.writeline(" )(*_padded_inputs)") + code.writeline(" return _result") + code.writeline(" else:") + code.writeline( + " # Non-scalar output with broadcasting - broadcast inputs to output shape" + ) + code.writeline(" _target_shape = out_shapes[0]") + code.writeline(" _broadcasted = [") + code.writeline( + f" jnp.broadcast_to(_inp, _target_shape) for _inp in {input_list}" + ) + code.writeline(" ]") + code.writeline(" _padded_inputs = pallas_gpu_pad_inputs(_broadcasted)") + code.writeline( + " _aligned_out_specs, _is_scalar = pallas_gpu_align_output_specs(out_shapes, out_dtypes)" + ) + code.writeline(" _result = plgpu.kernel(") + code.writeline(" " + kernel_arg) + code.writeline(" out_shape=_aligned_out_specs,") + code.writeline(" )(*_padded_inputs)") + code.writeline( + " return pallas_gpu_unpad_results(_result, out_shapes, _is_scalar)" + ) + + def _param_to_buf_name(self, param: str) -> str | None: + """Map a kernel parameter name back to its graph buffer name.""" + for graph_name, inner_name in self.args.input_buffers.items(): + if inner_name == param: + return graph_name + return None + + def _codegen_tiled_specs(self, ctx: _CodegenContext) -> None: + """Generate tiled BlockSpec and grid variables for CPU/TPU. + + Tiles the last 1–2 dimensions of each tensor, respecting TPU + alignment constraints (last dim multiple of 128, second-to-last + multiple of 8). Lower-ndim inputs are right-aligned with the + reference output shape per numpy broadcast rules. + """ + code = ctx.code + skip_n = self.tile_skip_last_n + has_transpose = "True" if self.tile_has_transpose else "False" + is_tpu_literal = "True" if ctx.is_tpu else "False" + + # Collect per-input permutations for tiling alignment. + all_perms: list[tuple[int, ...] | None] = [] + for p in ctx.kernel_input_params: + buf_name = self._param_to_buf_name(p) + all_perms.append( + self.permuted_input_buffers.get(buf_name) if buf_name else None + ) + + mgp = self._cpu_max_grid_product + mgp_arg = f", max_grid_product={mgp}" if mgp else "" + perms_arg = ( + f", permutations={repr(all_perms)}" + if any(p is not None for p in all_perms) + else "" + ) + code.writeline( + f"_tile, _grid, _ax2g = pallas_compute_tiling(" + f"_pallas_out_shapes[0], " + f"transpose={has_transpose}, " + f"skip_last_n={skip_n}, exact_only=len(_pallas_out_shapes[0]) < 2, " + f"is_tpu={is_tpu_literal}" + f"{perms_arg}{mgp_arg})" + ) + code.writeline("_ng = len(_grid)") + code.writeline("_ref = _pallas_out_shapes[0]") + + code.writeline("out_specs_pallas = tuple(") + code.writeline( + " pallas_make_block_spec(s, _ref, _tile, _ax2g, _ng, is_output=True)" + ) + code.writeline(" for s in _pallas_out_shapes") + code.writeline(")") + + self._codegen_strided_reshapes(code, ctx.kernel_input_params) + + # Reshape collapsed inputs before building specs. + for param in ctx.kernel_input_params: + buf_name = self._param_to_buf_name(param) + cshape = self.collapsed_reshape_inputs.get(buf_name) if buf_name else None + if cshape is not None: + code.writeline(f"{param} = {param}.reshape({cshape})") + + # Build input BlockSpecs (with per-input permutation when needed). + input_list = ", ".join(ctx.kernel_input_params) + if any(p is not None for p in all_perms): + perm_list = ", ".join(repr(p) for p in all_perms) + code.writeline(f"_perm_flags = [{perm_list}]") + code.writeline("in_specs_pallas = tuple(") + code.writeline( + f" pallas_make_block_spec(i.shape, _ref, _tile, _ax2g, _ng, permutation=p)" + f" for i, p in zip([{input_list}], _perm_flags)" + ) + code.writeline(")") + else: + code.writeline("in_specs_pallas = tuple(") + code.writeline( + f" pallas_make_block_spec(i.shape, _ref, _tile, _ax2g, _ng) for i in [{input_list}]" + ) + code.writeline(")") + + def _codegen_jit_wrapper_cpu_tpu( + self, + ctx: _CodegenContext, + kernel_arg: str, + alias_pairs: list[tuple[int, int]], + alias_map_literal: str, + ) -> None: + code = ctx.code + grid_expr = "_grid" if self.tile_cpu_tpu else "(1,)" + code.writeline("_result = pl.pallas_call(") + code.writeline(" " + kernel_arg) + code.writeline(" out_shape=out_shapes_pallas,") + code.writeline(" out_specs=out_specs_pallas,") + code.writeline(" in_specs=in_specs_pallas,") + code.writeline(f" interpret={ctx.interpret_literal},") + code.writeline(f" grid={grid_expr},") + code.writeline( + f" input_output_aliases={{ {alias_map_literal} }}," + if alias_pairs + else " input_output_aliases={}," + ) + code.writeline(")(") + if ctx.kernel_input_params: + kernel_input_params_nonzero_rank = [ + f"pallas_ensure_nonzero_rank({p})" for p in ctx.kernel_input_params + ] + code.writeline(f" {', '.join(kernel_input_params_nonzero_rank)},") + code.writeline(")") + # Reshape results back to original shapes (restores 0-d from promoted (1,)) + code.writeline("if isinstance(_result, tuple):") + code.writeline( + " _result = tuple(r.reshape(s) for r, s in zip(_result, out_shapes))" + ) + code.writeline("else:") + code.writeline(" _result = _result.reshape(out_shapes[0])") + code.writeline("return _result") + + def _codegen_main_entry(self, ctx: _CodegenContext, jit_wrapper_name: str) -> None: + if ctx.is_tpu: + self._codegen_main_entry_tpu(ctx, jit_wrapper_name) + else: + self._codegen_main_entry_default(ctx, jit_wrapper_name) + + def _codegen_main_entry_tpu( + self, ctx: _CodegenContext, jit_wrapper_name: str + ) -> None: + code = ctx.code + code.writeline("") + main_name = f"{ctx.kernel_name}_main" + kernel_name_str = ctx.kernel_name + code.writeline( + f"def {main_name}({', '.join(ctx.full_kernel_params)}, stream=None):" + ) + with code.indent(): + # `jax_enable_x64` is per-process. The CPU path sets it to True, + # so running both CPU and TPU tests in one process can cause + # x64-related TPU crashes if we do not explicitly set it to + # False here. + code.writeline("jax.config.update('jax_enable_x64', False)") + code.writeline("jax.clear_caches()") + + # Convert int64 inputs to int32 (TPU doesn't support int64) + all_input_params = list(ctx.alias_params) + list(ctx.pointer_tail) + for param_name in all_input_params: + code.writeline( + f"{param_name} = {param_name}.to(torch.int32) " + f"if {param_name}.dtype == torch.int64 else {param_name}" + ) + + # Build JAX placeholders for all inputs + code.writeline("# Build JAX placeholders for export tracing") + all_jax_input_names = [] + for alias_name in ctx.alias_params: + code.writeline( + f"{alias_name}_placeholder = jax.ShapeDtypeStruct(" + f"{alias_name}.shape, torch_dtype_to_jax_runtime({alias_name}.dtype))" + ) + all_jax_input_names.append(f"{alias_name}_placeholder") + for ptr in ctx.pointer_tail: + code.writeline( + f"{ptr}_placeholder = jax.ShapeDtypeStruct(" + f"{ptr}.shape, torch_dtype_to_jax_runtime({ptr}.dtype))" + ) + all_jax_input_names.append(f"{ptr}_placeholder") + + # Prepare output metadata + code.writeline( + "out_shapes = (" + + ", ".join([f"tuple({name}.shape)" for name in ctx.output_params]) + + ",)" + ) + dtype_exprs: list[str] = [] + for name in ctx.output_params: + buf_name = ctx.output_buffer_lookup.get(name) + if buf_name is not None: + dtype = V.graph.get_dtype(buf_name) + if dtype is not None: + dtype_exprs.append(torch_dtype_to_jax(dtype)) + continue + dtype_exprs.append(f"torch_dtype_to_jax_runtime({name}.dtype)") + code.writeline("out_dtypes = (" + ", ".join(dtype_exprs) + ",)") + + # Export the jit_wrapper + wrapper_placeholder_args = ["out_shapes", "out_dtypes"] + wrapper_placeholder_args.extend(ctx.size_var_params) + wrapper_placeholder_args.extend(all_jax_input_names) + code.writeline( + f"exported = jax.export.export(" + f"{jit_wrapper_name}, platforms=['tpu'])" + f"({', '.join(wrapper_placeholder_args)})" + ) + + # Register and call via tpu_torch_pallas + # Include all output and input shapes in the key to avoid stale + # cache hits when the same kernel name is compiled with different + # input/output ranks (e.g. broadcasting vs non-broadcasting calls). + shape_key_parts = [] + for p in ctx.output_params: + shape_key_parts.append(f"'_'.join(str(s) for s in {p}.shape)") + output_key_expr = ( + " + 'x' + ".join(shape_key_parts) if shape_key_parts else "''" + ) + input_key_parts = [] + for p in ctx.kernel_input_params: + input_key_parts.append(f"'_'.join(str(s) for s in {p}.shape)") + input_key_expr = ( + " + 'x' + ".join(input_key_parts) if input_key_parts else "''" + ) + code.writeline( + f"kernel_key = '{kernel_name_str}_out_' + " + f"{output_key_expr}" + f" + '_in_' + {input_key_expr}" + ) + + code.writeline( + f"if not tpu_torch_pallas.lookup_custom_kernel('{kernel_name_str}', kernel_key):" + ) + with code.indent(): + code.writeline("try:") + with code.indent(): + code.writeline( + f"tpu_torch_pallas.register_custom_kernel(" + f"'{kernel_name_str}', kernel_key, " + f"serialized_mlir_module=exported.mlir_module_serialized)" + ) + code.writeline("except TypeError:") + with code.indent(): + code.writeline( + f"tpu_torch_pallas.register_custom_kernel(" + f"'{kernel_name_str}', kernel_key, " + f"exported.mlir_module_serialized)" + ) + + # Build input tensor list (all non-size-var inputs) + input_tensor_names = list(ctx.alias_params) + list(ctx.pointer_tail) + code.writeline(f"input_tensors = [{', '.join(input_tensor_names)}]") + + # Build output shapes list + code.writeline("output_shape_tensors = [") + with code.indent(): + for name in ctx.output_params: + buf_name = ctx.output_buffer_lookup.get(name) + if buf_name is not None: + dtype = V.graph.get_dtype(buf_name) + if dtype is not None: + code.writeline( + f"torch.empty({name}.shape, dtype={dtype!r}, device='tpu')," + ) + continue + code.writeline( + f"torch.empty({name}.shape, dtype={name}.dtype, device='tpu')," + ) + code.writeline("]") + + # Build input_output_aliases for zero-copy donation + if ctx.alias_pairs: + alias_map_str = ", ".join(f"{i}: {o}" for (i, o) in ctx.alias_pairs) + code.writeline(f"_input_output_aliases = {{ {alias_map_str} }}") + else: + code.writeline("_input_output_aliases = {}") + + code.writeline("try:") + with code.indent(): + code.writeline( + f"tpu_torch_pallas.call_custom_kernel(" + f"'{kernel_name_str}', kernel_key, " + f"inputs=input_tensors, " + f"output_shapes=output_shape_tensors, " + f"input_output_aliases=_input_output_aliases)" + ) + code.writeline("except TypeError:") + with code.indent(): + code.writeline( + f"tpu_torch_pallas.call_custom_kernel(" + f"input_tensors, output_shape_tensors, " + f"'{kernel_name_str}', kernel_key, " + f"_input_output_aliases)" + ) + + def _codegen_main_entry_default( + self, ctx: _CodegenContext, jit_wrapper_name: str + ) -> None: + code = ctx.code + code.writeline("") + main_name = f"{ctx.kernel_name}_main" + code.writeline( + f"def {main_name}({', '.join(ctx.full_kernel_params)}, stream=None):" + ) + with code.indent(): + code.writeline("jax.config.update('jax_enable_x64', True)") + if ctx.interpret_is_cpu: + code.writeline( + "jax.config.update('jax_default_device', jax.devices('cpu')[0])" + ) + code.writeline("jax.clear_caches()") + if ctx.alias_params: + code.writeline("# Convert Torch -> JAX for donated outputs") + for alias_name in ctx.alias_params: + # On CPU/TPU, alias outputs may be non-contiguous (e.g. + # torch.cat slices) and JAX's from_dlpack rejects + # non-trivially strided tensors. Making them contiguous + # is safe because CPU/TPU already copies all results back + # via copy_output_indices. On CUDA, the donated-buffer + # mechanism requires the original buffer for in-place + # mutation, so we cannot make a contiguous copy. + self._emit_torch_to_jax( + code, + alias_name, + ctx.is_tpu, + contiguous=ctx.interpret_is_cpu, + ) + code.writeline("# Convert Torch -> JAX for in-place tensors") + for ptr in ctx.pointer_tail: + if ptr.startswith("in_out_ptr"): + self._emit_torch_to_jax(code, ptr, ctx.is_tpu, contiguous=False) + code.writeline("# Convert Torch -> JAX for inputs") + for ptr in ctx.pointer_tail: + if ptr.startswith("in_ptr"): + self._emit_torch_to_jax(code, ptr, ctx.is_tpu, contiguous=True) + + code.writeline("# Prepare output metadata from PyTorch tensor") + code.writeline( + "out_shapes = (" + + ", ".join([f"tuple({name}.shape)" for name in ctx.output_params]) + + ",)" + ) + dtype_exprs: list[str] = [] + for name in ctx.output_params: + buf_name = ctx.output_buffer_lookup.get(name) + if buf_name is not None: + dtype = V.graph.get_dtype(buf_name) + if dtype is not None: + dtype_exprs.append(torch_dtype_to_jax(dtype)) + continue + dtype_exprs.append(f"torch_dtype_to_jax_runtime({name}.dtype)") + code.writeline("out_dtypes = (" + ", ".join(dtype_exprs) + ",)") + arg_name_map: dict[str, str] = {} + for alias_name in ctx.alias_params: + arg_name_map[alias_name] = f"{alias_name}_jax" + for ptr in ctx.pointer_tail: + arg_name_map[ptr] = f"{ptr}_jax" + + wrapper_call_args = ["out_shapes", "out_dtypes"] + wrapper_call_args.extend(ctx.size_var_params) + wrapper_call_args.extend( + arg_name_map[name] for name in ctx.kernel_input_params + ) + code.writeline(f"res = {jit_wrapper_name}({', '.join(wrapper_call_args)})") + code.writeline("jax.block_until_ready(res)") + if ctx.copy_output_indices: + code.writeline( + "result_values = res if isinstance(res, tuple) else (res,)" + ) + for idx in ctx.copy_output_indices: + out_name = ctx.output_params[idx] + code.writeline( + f"{out_name}.copy_(torch.from_dlpack(result_values[{idx}]))" + ) + + @staticmethod + def _emit_torch_to_jax( + code: IndentedBuffer, var_name: str, is_tpu: bool, *, contiguous: bool + ) -> None: + suffix = ".detach().contiguous()" if contiguous else ".detach()" + code.writeline(f"{var_name}_jax = jax.dlpack.from_dlpack({var_name}{suffix})") + + def call_kernel(self, name: str, node: IRNode | None = None) -> None: # type: ignore[override] + """Generate the Python code that calls this Pallas kernel.""" + wrapper = V.graph.wrapper_code + arg_defs, call_args, _, _ = self.args.python_argdefs() + kernel_param_names = [a.name for a in arg_defs] + pure_out_params = [p for p in kernel_param_names if p.startswith("out_ptr")] + call_arg_strs = list(map(str, call_args)) + aliasable = getattr(self, "aliasable_out_ptrs", {}) + alias_call_args = [ + call_arg_strs[kernel_param_names.index(p)] + for p in pure_out_params + if aliasable.get(p, False) + ] + + # Generate kernel call: kernel_name.run(arg1, arg2, ...) + # Note: async_compile.pallas loads {name}_main function and wraps it in PallasKernelWrapper + # which exposes a run() method + kernel_call = f"{name}.run({', '.join(alias_call_args + call_arg_strs)})" + wrapper.writeline(kernel_call) + + +class PallasScheduling(SIMDScheduling): + kernel_type = PallasKernel # type: ignore[assignment] + + @classmethod + def get_backend_features(cls, device: torch.device) -> OrderedSet[BackendFeature]: + # Pallas/JAX can handle reductions to single elements efficiently + # without requiring split reductions + return OrderedSet([BackendFeature.REDUCE_TO_SINGLE_ELEMENT]) + + def can_fuse(self, node1, node2): # type: ignore[override] + if not super().can_fuse(node1, node2): + return False + # Pallas partial reductions use keepdims, so fusing two reductions + # that read the same buffer with different index patterns produces + # intermediates with incompatible shapes (e.g. (1,8) + (8,1) = (8,8) + # instead of (8,)). Prevent this by rejecting fusion when the read + # indices differ. + if node1.is_reduction() and node2.is_reduction(): + from torch._inductor.dependencies import MemoryDep + + reads1 = {} + for dep in node1.read_writes.reads: + if isinstance(dep, MemoryDep): + reads1[dep.name] = dep.index + for dep in node2.read_writes.reads: + if isinstance(dep, MemoryDep) and dep.name in reads1: + if reads1[dep.name] != dep.index: + return False + return True + + can_fuse_vertical = can_fuse # type: ignore[assignment] + can_fuse_horizontal = can_fuse # type: ignore[assignment] + + def define_kernel( + self, + src_code: str, + node_schedule: Sequence[BaseSchedulerNode], + kernel: PallasKernel, + ) -> str: # type: ignore[override] + wrapper = V.graph.wrapper_code + if src_code in wrapper.src_to_kernel: + return wrapper.src_to_kernel[src_code] + + fused_name = ( + get_fused_kernel_name(node_schedule, config.triton.descriptive_names) + if config.triton.descriptive_names + else "" + ) + kernel_hash = hashlib.sha256(src_code.encode("utf-8")).hexdigest()[:8] + if fused_name == "fused": + kernel_name = f"pallas_{kernel_hash}" + else: + kernel_name = f"pallas_{fused_name}_{kernel_hash}" + wrapper.src_to_kernel[src_code] = kernel_name + + # Replace placeholder if any + src_code = src_code.replace("", kernel_name) + + compile_wrapper = IndentedBuffer() + compile_wrapper.writeline(f"async_compile.pallas({kernel_name!r}, r'''") + compile_wrapper.splice(src_code, strip=True) + compile_wrapper.writeline("''')") + + origins, detailed_origins = get_kernel_metadata(node_schedule, wrapper) + metadata_comment = f"{origins}\n{detailed_origins}" + wrapper.define_kernel(kernel_name, compile_wrapper.getvalue(), metadata_comment) + + return kernel_name diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/python_wrapper_mtia.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/python_wrapper_mtia.py new file mode 100644 index 0000000000000000000000000000000000000000..4a0309f2088ae1785a548db4f77103320d11a5b0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/python_wrapper_mtia.py @@ -0,0 +1,33 @@ +from typing_extensions import override + +from torch._inductor import ir + +from .wrapper import PythonWrapperCodegen + + +class PythonWrapperMtia(PythonWrapperCodegen): + """ + A thin wrapper of PythonWrapperCodegen with MTIA specific logic + """ + + @override + def write_header(self) -> None: + super().write_header() + + # MITA specific imports + self.imports.splice("import mtia.host_runtime.torch_mtia.dynamic_library") + + @override + @staticmethod + def create( + is_subgraph: bool, + subgraph_name: str | None, + parent_wrapper: PythonWrapperCodegen | None, + partition_signatures: ir.GraphPartitionSignature | None = None, + ) -> PythonWrapperCodegen: + if is_subgraph: + # Delegate to the parent class to handle the case of subgraph + return PythonWrapperCodegen.create( + is_subgraph, subgraph_name, parent_wrapper, partition_signatures + ) + return PythonWrapperMtia() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/ck_conv_template.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/ck_conv_template.py new file mode 100644 index 0000000000000000000000000000000000000000..277b6ed3749486074a583d61f6f2909886eb60c9 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/ck_conv_template.py @@ -0,0 +1,627 @@ +# mypy: allow-untyped-defs +import copy +import logging +import random +from typing import Any +from typing_extensions import override + +from torch._inductor.virtualized import V + +from .rocm_template import ArgInfo + + +try: + import ck4inductor # type: ignore[import] +except ImportError: + ck4inductor = None + +if ck4inductor is not None: + from ck4inductor.grouped_conv_fwd.gen_instances import ( # type: ignore[import] + gen_conv_ops_library, + ) + from ck4inductor.grouped_conv_fwd.op import ( # type: ignore[import] # noqa: TCH002 + CKGroupedConvFwdOp, + ) +else: + + def gen_conv_ops_library(): + return [] + + +from torch._inductor import config +from torch._inductor.codegen.rocm.ck_template import CKTemplate +from torch._inductor.codegen.rocm.rocm_kernel import ROCmTemplateKernel +from torch._inductor.utils import IndentedBuffer + + +log = logging.getLogger(__name__) + + +def torch_layout_to_ck_layouts(torch_layout): + # logically, torch tensors are always NCHW, + # and channels-last memory layout is visible in the strides + if V.graph.sizevars.statically_known_equals(torch_layout.stride[-1], 1): + # when input or output is NCHW + # NB: torch.conv2d result is always NCHW + return ["NGCHW", "GKCYX", "NGKHW"] + elif V.graph.sizevars.statically_known_equals(torch_layout.stride[-3], 1): + # when input or output or weight is channels-last + return ["NHWGC", "GKYXC", "NHWGK"] + else: + return None + + +def torch_layout_to_ck_input_layout(torch_layout): + if V.graph.sizevars.statically_known_equals(torch_layout.stride[-1], 1): + return "NGCHW" + elif V.graph.sizevars.statically_known_equals(torch_layout.stride[-3], 1): + return "NHWGC" + else: + return None + + +def torch_layout_to_ck_weight_layout(torch_layout): + if V.graph.sizevars.statically_known_equals(torch_layout.stride[-1], 1): + return "GKCYX" + elif V.graph.sizevars.statically_known_equals(torch_layout.stride[-3], 1): + return "GKYXC" + else: + return None + + +def torch_layout_to_ck_output_layout(torch_layout): + if V.graph.sizevars.statically_known_equals(torch_layout.stride[-1], 1): + return "NGKHW" + elif V.graph.sizevars.statically_known_equals(torch_layout.stride[-3], 1): + return "NHWGK" + else: + return None + + +class CKGroupedConvFwdTemplate(CKTemplate): + conv_template = r""" + {{headers}} + {{globals}} + {{instance_definition}} + extern "C" { + PT_EXPORT {{kernel_definition}} { + auto conv = {{instance_type}} {}; + auto invoker = conv.MakeInvoker(); + + using ck::index_t; + + constexpr index_t NumDTensor = {{n_d_tensors}}; + constexpr index_t NDimSpatial = {{n_dim_spatial}}; + const std::vector FilterSize = { FilterSize_0, FilterSize_1 }; + const std::vector InputSize = { InputSize_0, InputSize_1 }; + const std::vector ConvolutionStrides = { ConvolutionStrides_0, ConvolutionStrides_1 }; + const std::vector Dilations = { Dilations_0, Dilations_1 }; + const std::vector LeftPads = { LeftPads_0, LeftPads_1 }; + const std::vector RightPads = { RightPads_0, RightPads_1 }; + + + auto conv_param = ck::utils::conv::ConvParam { + NDimSpatial, + GroupCount, + NBatch, + NOutChannels, + NInChannels, + FilterSize, + InputSize, + ConvolutionStrides, + Dilations, + LeftPads, + RightPads, + }; + + using InLayout = ck::tensor_layout::convolution::{{input_layout}}; + using WeiLayout = ck::tensor_layout::convolution::{{weight_layout}}; + using OutLayout = ck::tensor_layout::convolution::{{output_layout}}; + + const auto in_g_n_c_wis_desc = + ck::utils::conv::make_input_host_tensor_descriptor_g_n_c_wis_packed(conv_param); + const auto wei_g_k_c_xs_desc = + ck::utils::conv::make_weight_host_tensor_descriptor_g_k_c_xs_packed(conv_param); + const auto out_g_n_k_wos_desc = + ck::utils::conv::make_output_host_tensor_descriptor_g_n_k_wos_packed(conv_param); + + const void* p_a = input; + const void* p_b = weight; + const std::array p_ds; + void* p_e = output; + std::array a_g_n_c_wis_lengths; + std::array a_g_n_c_wis_strides; + std::array b_g_k_c_xs_lengths; + std::array b_g_k_c_xs_strides; + std::array, NumDTensor> ds_g_n_k_wos_lengths; + std::array, NumDTensor> ds_g_n_k_wos_strides; + std::array e_g_n_k_wos_lengths; + std::array e_g_n_k_wos_strides; + std::array conv_filter_strides; + std::array conv_filter_dilations; + std::array input_left_pads; + std::array input_right_pads; + const auto a_element_op = PassThrough {}; + const auto b_element_op = PassThrough {}; + const auto cde_element_op = PassThrough {}; + + auto copy = [](auto& x, auto& y) { ck::ranges::copy(x, y.begin()); }; + + copy(in_g_n_c_wis_desc.GetLengths(), a_g_n_c_wis_lengths); + copy(in_g_n_c_wis_desc.GetStrides(), a_g_n_c_wis_strides); + copy(wei_g_k_c_xs_desc.GetLengths(), b_g_k_c_xs_lengths); + copy(wei_g_k_c_xs_desc.GetStrides(), b_g_k_c_xs_strides); + copy(out_g_n_k_wos_desc.GetLengths(), e_g_n_k_wos_lengths); + copy(out_g_n_k_wos_desc.GetStrides(), e_g_n_k_wos_strides); + copy(conv_param.conv_filter_strides_, conv_filter_strides); + copy(conv_param.conv_filter_dilations_, conv_filter_dilations); + copy(conv_param.input_left_pads_, input_left_pads); + copy(conv_param.input_right_pads_, input_right_pads); + + auto argument = conv.MakeArgument( + p_a, + p_b, + p_ds, + p_e, + a_g_n_c_wis_lengths, + a_g_n_c_wis_strides, + b_g_k_c_xs_lengths, + b_g_k_c_xs_strides, + ds_g_n_k_wos_lengths, + ds_g_n_k_wos_strides, + e_g_n_k_wos_lengths, + e_g_n_k_wos_strides, + conv_filter_strides, + conv_filter_dilations, + input_left_pads, + input_right_pads, + a_element_op, + b_element_op, + cde_element_op + ); + if (!conv.IsSupportedArgument(argument)) { + // we do our best to statically avoid this case in `filter_op` + std::cerr << "invalid argument for conv instance " << conv.GetTypeString() << std::endl; + argument.Print(); + return -23; + } + if (workspace_size) { + *workspace_size = conv.GetWorkSpaceSize(&argument); + return 0; + } + + if (p_a == nullptr) { + std::cerr << "p_a is nullptr" << std::endl; + return -1; + } + if (p_b == nullptr) { + std::cerr << "p_b is nullptr" << std::endl; + return -1; + } + if (p_e == nullptr) { + std::cerr << "p_e is nullptr" << std::endl; + return -1; + } + + // when debugging, do time kernel to serialize launches + auto stream_config = StreamConfig{stream, /* time kernel */ false, /* log level */ 0}; + + if (workspace != nullptr) { + conv.SetWorkSpacePointer(&argument, workspace, stream_config); + } + + // run the kernel + float elapsed_time = invoker.Run(argument, stream_config); + return 0; + } // kernel definition + } // extern C + + #ifdef GENERATE_CK_STANDALONE_RUNNER + int main(int argc, char** argv) { + (void) argc; + (void) argv; + return 0; + } + #endif // GENERATE_CK_STANDALONE_RUNNER +""" + + def globals(self) -> IndentedBuffer: + res = super().globals() + res.splice( + """ + // CK conv globals + + using NWC = ck::tensor_layout::convolution::NWC; + using NHWC = ck::tensor_layout::convolution::NHWC; + using NDHWC = ck::tensor_layout::convolution::NDHWC; + + using KXC = ck::tensor_layout::convolution::KXC; + using KYXC = ck::tensor_layout::convolution::KYXC; + using KZYXC = ck::tensor_layout::convolution::KZYXC; + + using NWK = ck::tensor_layout::convolution::NWK; + using NHWK = ck::tensor_layout::convolution::NHWK; + using NDHWK = ck::tensor_layout::convolution::NDHWK; + + using GNWC = ck::tensor_layout::convolution::GNWC; + using GNHWC = ck::tensor_layout::convolution::GNHWC; + using GNDHWC = ck::tensor_layout::convolution::GNDHWC; + + using GKXC = ck::tensor_layout::convolution::GKXC; + using GKYXC = ck::tensor_layout::convolution::GKYXC; + using GKZYXC = ck::tensor_layout::convolution::GKZYXC; + + using GKCX = ck::tensor_layout::convolution::GKCX; + using GKCYX = ck::tensor_layout::convolution::GKCYX; + using GKCZYX = ck::tensor_layout::convolution::GKCZYX; + + using GNWK = ck::tensor_layout::convolution::GNWK; + using GNHWK = ck::tensor_layout::convolution::GNHWK; + using GNDHWK = ck::tensor_layout::convolution::GNDHWK; + + using NGKW = ck::tensor_layout::convolution::NGKW; + using NGKHW = ck::tensor_layout::convolution::NGKHW; + using NGKDHW = ck::tensor_layout::convolution::NGKDHW; + + using NWGC = ck::tensor_layout::convolution::NWGC; + using NHWGC = ck::tensor_layout::convolution::NHWGC; + using NDHWGC = ck::tensor_layout::convolution::NDHWGC; + + using KXGC = ck::tensor_layout::convolution::KXGC; + using KYXGC = ck::tensor_layout::convolution::KYXGC; + using KZYXGC = ck::tensor_layout::convolution::KZYXGC; + + using NWGK = ck::tensor_layout::convolution::NWGK; + using NHWGK = ck::tensor_layout::convolution::NHWGK; + using NDHWGK = ck::tensor_layout::convolution::NDHWGK; + + using NGCW = ck::tensor_layout::convolution::NGCW; + using NGCHW = ck::tensor_layout::convolution::NGCHW; + using NGCDHW = ck::tensor_layout::convolution::NGCDHW; + + using G_K = ck::tensor_layout::convolution::G_K; + + using BlockGemmPipelineScheduler = ck::BlockGemmPipelineScheduler; + using GemmSpecialization = ck::tensor_operation::device::GemmSpecialization; + using BlockGemmPipelineVersion = ck::BlockGemmPipelineVersion; + + using ConvolutionForwardSpecialization = ck::tensor_operation::device::ConvolutionForwardSpecialization; + + using OutElementOp = PassThrough; + + namespace ck { + namespace utils { + namespace conv { + + ConvParam::ConvParam(ck::index_t n_dim, + ck::index_t group_count, + ck::index_t n_batch, + ck::index_t n_out_channels, + ck::index_t n_in_channels, + const std::vector& filters_len, + const std::vector& input_len, + const std::vector& strides, + const std::vector& dilations, + const std::vector& left_pads, + const std::vector& right_pads) + : num_dim_spatial_(static_cast(n_dim)), + G_(static_cast(group_count)), + N_(static_cast(n_batch)), + K_(static_cast(n_out_channels)), + C_(static_cast(n_in_channels)), + filter_spatial_lengths_(num_dim_spatial_), + input_spatial_lengths_(num_dim_spatial_), + output_spatial_lengths_(num_dim_spatial_), + conv_filter_strides_(num_dim_spatial_), + conv_filter_dilations_(num_dim_spatial_), + input_left_pads_(num_dim_spatial_), + input_right_pads_(num_dim_spatial_) + { + if(static_cast(filter_spatial_lengths_.size()) != num_dim_spatial_ || + static_cast(input_spatial_lengths_.size()) != num_dim_spatial_ || + static_cast(conv_filter_strides_.size()) != num_dim_spatial_ || + static_cast(conv_filter_dilations_.size()) != num_dim_spatial_ || + static_cast(input_left_pads_.size()) != num_dim_spatial_ || + static_cast(input_right_pads_.size()) != num_dim_spatial_) + { + throw( + std::runtime_error("ConvParam::ConvParam: " + "parameter size is different from number of declared dimensions!")); + } + + for(ck::index_t i = 0; i < num_dim_spatial_; ++i) + { + filter_spatial_lengths_[i] = static_cast(filters_len[i]); + input_spatial_lengths_[i] = static_cast(input_len[i]); + conv_filter_strides_[i] = static_cast(strides[i]); + conv_filter_dilations_[i] = static_cast(dilations[i]); + input_left_pads_[i] = static_cast(left_pads[i]); + input_right_pads_[i] = static_cast(right_pads[i]); + + // XEff = (X - 1) * conv_dilation_w + 1; + // Wo = (Wi + in_left_pad_w + in_right_pad_w - XEff) / conv_stride_w + 1; + const ck::long_index_t x_eff = + (filter_spatial_lengths_[i] - 1) * conv_filter_dilations_[i] + 1; + + output_spatial_lengths_[i] = + (input_spatial_lengths_[i] + input_left_pads_[i] + input_right_pads_[i] - x_eff) / + conv_filter_strides_[i] + + 1; + } + } + + } // namespace conv + } // namespace utils + } // namespace ck + + const std::vector& HostTensorDescriptor::GetLengths() const { return mLens; } + const std::vector& HostTensorDescriptor::GetStrides() const { return mStrides; } + std::size_t HostTensorDescriptor::GetNumOfDimension() const { return mLens.size(); } + void HostTensorDescriptor::CalculateStrides() { + mStrides.clear(); + mStrides.resize(mLens.size(), 0); + if(mStrides.empty()) + return; + + mStrides.back() = 1; + std::partial_sum( + mLens.rbegin(), mLens.rend() - 1, mStrides.rbegin() + 1, std::multiplies()); + } + """ + ) + return res + + def header(self) -> IndentedBuffer: + res = super().header() + res.splice( + """ + // CK conv headers + + #include "ck/tensor_operation/gpu/device/impl/device_grouped_conv_fwd_multiple_abd_xdl_cshuffle_v3.hpp" + #include "ck/tensor_operation/gpu/device/convolution_forward_specialization.hpp" + #include "ck/tensor_operation/gpu/device/gemm_specialization.hpp" + + #include "ck/library/utility/convolution_parameter.hpp" + #include "ck/library/utility/convolution_host_tensor_descriptor_helper.hpp" + """ + ) + return res + + @staticmethod + def add_ck_conv_choices( + choices, + layout, + input_nodes, + *, + stride, + padding, + dilation, + groups, + n_spatial_dimensions, + ): + template = CKGroupedConvFwdTemplate( + input_nodes, + layout, + stride=stride, + padding=padding, + dilation=dilation, + groups=groups, + n_spatial_dimensions=n_spatial_dimensions, + ) + ops = template.gen_ops() + for op in ops: + template.maybe_append_choice( + choices, + op=op, + ) + + def __init__( + self, + input_nodes, + layout, + *, + stride, + padding, + dilation, + groups, + n_spatial_dimensions, + ): + super().__init__( + "ck_conv_template", + input_nodes, + layout, + ) + self.stride = stride + self.padding = padding + self.dilation = dilation + self.groups = groups + self.n_spatial_dimensions = n_spatial_dimensions + + def filter_op(self, op: "CKGroupedConvFwdOp"): # type: ignore[name-defined] + metas = [ + T.get_layout() + for T in [*self.input_nodes, self.output_node] + if T is not None + ] + X_meta = metas[0] + W_meta = metas[1] + Y_meta = metas[-1] + # disable the instance if dtypes don't match + if op.a_element_dtype != self._TORCH_DTYPE_TO_CK[X_meta.dtype]: + return None + if op.b_element_dtype != self._TORCH_DTYPE_TO_CK[W_meta.dtype]: + return None + if op.e_element_dtype != self._TORCH_DTYPE_TO_CK[Y_meta.dtype]: + return None + # disable the instance if layouts don't match + if op.a_layout != torch_layout_to_ck_input_layout(X_meta): + return None + if op.b_layout != torch_layout_to_ck_weight_layout(W_meta): + return None + if op.e_layout != torch_layout_to_ck_output_layout(Y_meta): + return None + # disable the instance if number of spatial dimensions doesn't match + if op.n_dim_spatial != self.n_spatial_dimensions: + return None + # disable 1x1 and odd-channels conv specializations for now + if "Default" not in op.conv_forward_specialization: + return None + return op + + def gen_ops(self): + unfiltered_instances = gen_conv_ops_library() + + filtered_instances = list( + filter(lambda op: self.filter_op(op), unfiltered_instances) + ) + # NB: when using a fixed list order, most likely we will pick the subset of instances + # which are very similar to each other. Randomizing the choice seems to solve this. + random.seed(-11) + chosen_instances = ( + random.sample( + filtered_instances, + min(len(filtered_instances), config.rocm.ck_max_profiling_configs), + ) + if config.rocm.ck_max_profiling_configs + else filtered_instances + ) + log.debug( + "generated %d ck instances after filter: %s", + len(chosen_instances), + chosen_instances, + ) + return chosen_instances + + def emit_ck_instance(self, op: "CKGroupedConvFwdOp") -> tuple[str, str]: # type: ignore[name-defined] + # The Jinja template for generating a C++ type alias *definition* for a Universal GEMM instance + template_definition = r""" + // Gemm operator {{operation_name}} + using Operation_{{operation_name}} = + ck::tensor_operation::device::DeviceGroupedConvFwdMultipleABD_Xdl_CShuffle_V3< + {{template_params}}>; + +""" + # The Jinja template for generating a C++ type alias *usage* for a Universal GEMM instance + template_type = r""" + Operation_{{operation_name}} +""" + template_params = [] + for field_name, field_value in op.dict_items(): + if isinstance(field_value, tuple): + tuple_elements = ", ".join(map(str, iter(field_value))) + if "ds" in field_name: # element type and layout for bias + arg = f"/* {field_name} */ Tuple<{tuple_elements}>" + else: # tile shape + arg = f"/* {field_name} */ S<{tuple_elements}>" + # pyrefly: ignore [bad-argument-type] + template_params.append(arg) + else: + if field_value is not None: + # pyrefly: ignore [bad-argument-type] + template_params.append(f"/* {field_name} */ {field_value}") + return self._template_from_string(template_definition).render( + operation_name=op.name(), + template_params=(",\n" + 12 * " ").join(template_params), + ), self._template_from_string(template_type).render(operation_name=op.name()) + + def render( # type: ignore[override] + self, + kernel: ROCmTemplateKernel, + op: "CKGroupedConvFwdOp", # type: ignore[name-defined] + **kwargs, + ) -> str: + template_buffer_node = kwargs.get("template_buffer_node") + if template_buffer_node is not None: + self.output_node = template_buffer_node + X, W = self.input_nodes[0], self.input_nodes[1] + Y = self.output_node + Bias = self.input_nodes[2] if 3 == len(self.input_nodes) else None + + op = copy.deepcopy(op) + + instance_definition, instance_type = self.emit_ck_instance(op) + + size_arg_strs = [ + "GroupCount", + "NBatch", + "NOutChannels", + "NInChannels", + "FilterSize_0", + "FilterSize_1", + "InputSize_0", + "InputSize_1", + "ConvolutionStrides_0", + "ConvolutionStrides_1", + "Dilations_0", + "Dilations_1", + "LeftPads_0", + "LeftPads_1", + "RightPads_0", + "RightPads_1", + ] + + return self._template_from_string(self.conv_template).render( + headers=self.header().getvalue(), + globals=self.globals().getvalue(), + instance_definition=instance_definition, + instance_type=instance_type, + kernel_definition=kernel.def_kernel( + inputs=[X, W, Bias] if Bias is not None else [X, W], + outputs=[Y], + names_str="input, weight, bias, output" + if Bias is not None + else "input, weight, output", + size_args=[f"int32_t {arg}" for arg in size_arg_strs], + ), + n_d_tensors=1 if Bias is not None else 0, + n_dim_spatial=self.n_spatial_dimensions, + input_layout=op.a_layout, + weight_layout=op.b_layout, + output_layout=op.e_layout, + ) + + def size_args(self): + x, w = self.input_nodes[0], self.input_nodes[1] + y = self.output_node + + group_count = self.groups + n_batch = x.shape[0] # type: ignore[index] + n_out_channels = y.shape[1] # type: ignore[index] + n_in_channels = x.shape[1] # type: ignore[index] + + filter_size_0, filter_size_1 = w.shape[2:4] # type: ignore[index] + input_size_0, input_size_1 = x.shape[2:4] # type: ignore[index] + convolution_strides_0, convolution_strides_1 = self.stride + dilations_0, dilations_1 = self.dilation + left_pads_0, left_pads_1 = self.padding + right_pads_0, right_pads_1 = self.padding + + return ( + group_count, + n_batch, + n_out_channels, + n_in_channels, + filter_size_0, + filter_size_1, + input_size_0, + input_size_1, + convolution_strides_0, + convolution_strides_1, + dilations_0, + dilations_1, + left_pads_0, + left_pads_1, + right_pads_0, + right_pads_1, + ) + + @override + def get_runtime_arg_info(self) -> list[ArgInfo]: + return [] + + @override + def get_runtime_arg_values(self, **kwargs: Any) -> list[Any]: + """ + Helper method to retrieve runtime args from generate kwargs + """ + return [] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/ck_template.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/ck_template.py new file mode 100644 index 0000000000000000000000000000000000000000..b1eaf5c228eed80b5b9e40e3bbbd4e2de07b7c45 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/ck_template.py @@ -0,0 +1,110 @@ +from typing import Any +from typing_extensions import override + +import torch +from torch._inductor.codegen.rocm.rocm_template import ROCmTemplate +from torch._inductor.ir import IRNode +from torch._inductor.utils import IndentedBuffer + +from .rocm_template import ArgInfo + + +class CKTemplate(ROCmTemplate): + """ + Base class for generating CK templates, has common, i.e. non-gemm-specific, code generation logic + """ + + _TORCH_DTYPE_TO_CK = { + torch.float32: "F32", + torch.float64: "F64", + torch.float16: "F16", + torch.bfloat16: "BF16", + torch.int32: "I32", + torch.int8: "I8", + torch.float8_e4m3fnuz: "F8", # gfx94 + torch.float8_e4m3fn: "F8", # gfx95 + torch.float8_e5m2fnuz: "BF8", # gfx94 + torch.float8_e5m2: "BF8", # gfx95 + } + + def header(self) -> IndentedBuffer: + res = super().header() + res.splice( + """ + // CK headers + + #ifdef DEBUG_LOG + #define DEBUG_LOG_TMP DEBUG_LOG + #undef DEBUG_LOG + #else + #define DEBUG_LOG_TMP 0 + #endif + #include "ck/ck.hpp" + #undef DEBUG_LOG + #define DEBUG_LOG DEBUG_LOG_TMP + + #include "ck/utility/data_type.hpp" + #include "ck/library/utility/check_err.hpp" + #include "ck/library/utility/device_memory.hpp" + #include "ck/library/utility/fill.hpp" + #include "ck/library/utility/host_tensor.hpp" + #include "ck/library/utility/host_tensor_generator.hpp" + #include "ck/library/utility/literals.hpp" + """ + ) + return res + + def globals(self) -> IndentedBuffer: + res = super().globals() + res.splice( + """ + // CK globals + + template + using S = ck::Sequence; + + template + using Tuple = ck::Tuple; + + using PassThrough = ck::tensor_operation::element_wise::PassThrough; + using Bilinear = ck::tensor_operation::element_wise::Bilinear; + using Scale = ck::tensor_operation::element_wise::Scale; + using ScaleAdd = ck::tensor_operation::element_wise::ScaleAdd; + using MultiplyMultiply = ck::tensor_operation::element_wise::MultiplyMultiply; + + // see "composable_kernel/include/ck/utility/data_type.hpp" + using F8 = ck::f8_t; + using BF8 = ck::bf8_t; + using F16 = ck::half_t; + using F32 = float; + // using F64 = double; + using BF16 = ck::bhalf_t; + // using I32 = int32_t; + // using I8 = int8_t; + // using I4 = ck::int4_t; + + #if DEBUG_LOG + static constexpr auto kDEBUG_LOG = 1; + #else + static constexpr auto kDEBUG_LOG = 0; + #endif + """ + ) + return res + + def torch_type_to_ck(self, node: IRNode, ptr: str) -> str: + if node is None: + return ptr + else: + return f"({self._TORCH_DTYPE_TO_CK.get(node.get_dtype())}*)({ptr})" + + @override + def get_runtime_arg_info(self) -> list[ArgInfo]: + return [ArgInfo("kBatch", "int32_t")] + + @override + def get_runtime_arg_values(self, **kwargs: Any) -> list[Any]: + """ + Helper method to retrieve runtime args from generate kwargs + """ + return [kwargs[arg.name] for arg in self.get_runtime_arg_info()] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/ck_tile_template.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/ck_tile_template.py new file mode 100644 index 0000000000000000000000000000000000000000..70d31d635cc36dca295b1d82066376a1185c4da9 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/ck_tile_template.py @@ -0,0 +1,58 @@ +import torch +from torch._inductor.codegen.rocm.rocm_template import ROCmTemplate +from torch._inductor.ir import IRNode +from torch._inductor.utils import IndentedBuffer + + +class CKTileTemplate(ROCmTemplate): + """ + Base class for generating CK templates, has common, i.e. non-gemm-specific, code generation logic + """ + + _TORCH_DTYPE_TO_CK = { + torch.float32: "F32", + torch.float64: "F64", + torch.float16: "F16", + torch.bfloat16: "BF16", + torch.int32: "I32", + torch.int8: "I8", + torch.float8_e4m3fnuz: "F8", # gfx94 + torch.float8_e4m3fn: "F8", # gfx95 + torch.float8_e5m2fnuz: "BF8", # gfx94 + torch.float8_e5m2: "BF8", # gfx95 + } + + ck_dtype_to_size = { + "FP16": 2, + "BF16": 2, + } + + def header(self) -> IndentedBuffer: + res = super().header() + res.splice( + """ + // CK headers + #include "ck_tile/core.hpp" + + """ + ) + return res + + def globals(self) -> IndentedBuffer: + res = super().globals() + res.splice( + """ + using F8 = ck_tile::fp8_t; + using BF8 = ck_tile::bf8_t; + using F16 = ck_tile::half_t; + using F32 = float; + using BF16 = ck_tile::bfloat16_t; + """ + ) + return res + + def torch_type_to_ck(self, node: IRNode, ptr: str) -> str: + if node is None: + return ptr + else: + return f"({self._TORCH_DTYPE_TO_CK.get(node.get_dtype())}*)({ptr})" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/ck_tile_universal_gemm_template.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/ck_tile_universal_gemm_template.py new file mode 100644 index 0000000000000000000000000000000000000000..94a79297ef5e47e16f98a8968c815262a9d24d75 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/ck_tile_universal_gemm_template.py @@ -0,0 +1,979 @@ +# mypy: allow-untyped-defs, disable-error-code="attr-defined, valid-type" +import functools +import logging +import random +from dataclasses import asdict, dataclass +from typing import Any + +import torch +from torch._inductor import config +from torch._inductor.codegen.rocm.ck_tile_template import CKTileTemplate +from torch._inductor.codegen.rocm.rocm_kernel import ROCmTemplateKernel +from torch._inductor.codegen.rocm.rocm_template import ArgInfo +from torch._inductor.ir import Buffer, Layout +from torch.utils._ordered_set import OrderedSet + +from ...utils import IndentedBuffer + + +log = logging.getLogger(__name__) + + +def is_static_int(number): + import sympy + + return isinstance(number, (int, sympy.Integer)) + + +def torch_layout_to_ck_layout(torch_layout): + if torch_layout.stride[-1] == 1: + return "Row" + elif torch_layout.stride[-2] == 1: + return "Col" + else: + return None + + +@dataclass +class CKTileGemmOperation: + layout_a: str + layout_b: str + layout_c: str + + datatype_a: str + datatype_b: str + datatype_c: str + + tile_m: int + tile_n: int + tile_k: int + + warp_m: int + warp_n: int + warp_k: int + + warp_tile_m: int + warp_tile_n: int + warp_tile_k: int + + m_is_padded: str + n_is_padded: str + k_is_padded: str + + pipeline: str + scheduler: str + epilogue: str + + def layout_repr(self): + return f"{self.layout_a[0]}{self.layout_b[0]}{self.layout_c[0]}" + + def dtype_repr(self): + return f"{self.datatype_a}{self.datatype_b}{self.datatype_c}" + + def tile_sizes(self): + return "_".join( + [ + f"{self.tile_m}{self.tile_n}{self.tile_k}", + f"{self.warp_m}{self.warp_n}{self.warp_k}", + f"{self.warp_tile_m}{self.warp_tile_n}{self.warp_tile_k}", + ] + ) + + def name(self): + return "ck_tile_gemm_universal_" + "_".join( + [ + f"{self.layout_repr()}", + f"{self.dtype_repr()}", + f"{self.tile_sizes()}", + f"{self.pipeline}", + f"{self.scheduler}", + f"{self.epilogue}", + ] + ) + + def dict_items(self): + return asdict(self).items() + + +@functools.cache +def ops(): + """ + Generate the supported instance dataclasses + """ + import itertools + + compute_v3_instances = [ + CKTileGemmOperation( + layout_a=layout_a, + layout_b=layout_b, + layout_c=layout_c, + datatype_a=datatype_a, + datatype_b=datatype_b, + datatype_c=datatype_c, + tile_m=tile_m, + tile_n=tile_n, + tile_k=tile_k, + warp_m=warp_m, + warp_n=warp_n, + warp_k=warp_k, + warp_tile_m=warp_tile_m, + warp_tile_n=warp_tile_n, + warp_tile_k=warp_tile_k, + m_is_padded=m_is_padded, + n_is_padded=n_is_padded, + k_is_padded=k_is_padded, + pipeline="CompV3", + scheduler="Intrawave", + epilogue=epilogue, + ) + for (layout_a, layout_b, layout_c) in [ + ("Row", "Row", "Row"), + ("Row", "Col", "Row"), + ] + for (datatype_a, datatype_b, datatype_c) in [("FP16",) * 3, ("BF16",) * 3] + for (tile_m, tile_n, tile_k) in [(256, 256, 32), (256, 256, 64)] + for (warp_m, warp_n, warp_k) in [(2, 2, 1)] + for (warp_tile_m, warp_tile_n, warp_tile_k) in [(32, 32, 16)] + for m_is_padded in ["true", "false"] + for n_is_padded in ["true", "false"] + for k_is_padded in ["true", "false"] + for epilogue in ["Default", "CShuffle"] + ] + + compute_v4_instances = [ + CKTileGemmOperation( + layout_a=layout_a, + layout_b=layout_b, + layout_c=layout_c, + datatype_a=datatype_a, + datatype_b=datatype_b, + datatype_c=datatype_c, + tile_m=tile_m, + tile_n=tile_n, + tile_k=tile_k, + warp_m=warp_m, + warp_n=warp_n, + warp_k=warp_k, + warp_tile_m=warp_tile_m, + warp_tile_n=warp_tile_n, + warp_tile_k=warp_tile_k, + m_is_padded=m_is_padded, + n_is_padded=n_is_padded, + k_is_padded=k_is_padded, + pipeline="CompV4", + scheduler="Intrawave", + epilogue=epilogue, + ) + for (layout_a, layout_b, layout_c) in [ + ("Row", "Row", "Row"), + ("Row", "Col", "Row"), + ] + for (datatype_a, datatype_b, datatype_c) in [("FP16",) * 3, ("BF16",) * 3] + for (tile_m, tile_n, tile_k) in [ + (256, 256, 32) + ] # half the tile size since it has double buffering + for (warp_m, warp_n, warp_k) in [(2, 2, 1)] + for (warp_tile_m, warp_tile_n, warp_tile_k) in [(32, 32, 16)] + for m_is_padded in ["true", "false"] + for n_is_padded in ["true", "false"] + for k_is_padded in ["true", "false"] + for epilogue in ["Default", "CShuffle"] + ] + + mem_instances = [ + CKTileGemmOperation( + layout_a=layout_a, + layout_b=layout_b, + layout_c=layout_c, + datatype_a=datatype_a, + datatype_b=datatype_b, + datatype_c=datatype_c, + tile_m=tile_m, + tile_n=tile_n, + tile_k=tile_k, + warp_m=warp_m, + warp_n=warp_n, + warp_k=warp_k, + warp_tile_m=warp_tile_m, + warp_tile_n=warp_tile_n, + warp_tile_k=warp_tile_k, + m_is_padded=m_is_padded, + n_is_padded=n_is_padded, + k_is_padded=k_is_padded, + pipeline="Mem", + scheduler=scheduler, + epilogue=epilogue, + ) + for (layout_a, layout_b, layout_c) in [ + ("Row", "Row", "Row"), + ("Row", "Col", "Row"), + ] + for (datatype_a, datatype_b, datatype_c) in [("FP16",) * 3, ("BF16",) * 3] + for (tile_m, tile_n, tile_k) in [(256, 256, 32), (256, 256, 64)] + for (warp_m, warp_n, warp_k) in [(2, 2, 1)] + for (warp_tile_m, warp_tile_n, warp_tile_k) in [(32, 32, 16)] + for m_is_padded in ["true", "false"] + for n_is_padded in ["true", "false"] + for k_is_padded in ["true", "false"] + for scheduler in ["Intrawave", "Interwave"] + for epilogue in ["Default", "CShuffle"] + ] + + return list( + itertools.chain(compute_v3_instances, compute_v4_instances, mem_instances) + ) + + +class CKTileGemmTemplate(CKTileTemplate): + """ + This class is used for rendering CK-Tile Universal GEMM kernels + """ + + gemm_template = r"""{{version_comment}} + {{headers}} + {{globals}} + {{instance_definition}} + extern "C" { + PT_EXPORT {{kernel_definition}} { + + using {{instance_namespace}}::BaseGemmPipeline; + using {{instance_namespace}}::TilePartitioner; + + constexpr auto TileK = {{instance_namespace}}::TileK; + constexpr auto kPrefetchStages = BaseGemmPipeline::PrefetchStages; + + const auto BiasTerms = std::array (); + const auto BiasStrides = std::array (); + + auto kargs = ck_tile::UniversalGemmKernelArgs<> { + {X}, + {W}, + BiasTerms, + Y, + M, + N, + K, + {LDA}, + {LDB}, + BiasStrides, + LDC, + kBatch + }; + + if (workspace_size) { + *workspace_size = 0; + return 0; + } + + // run the kernel + const auto dispatch = [&](const auto has_hot_loop_, const auto tail_number_) constexpr { + using Kernel = {{instance_namespace}}::Kernel; + + if (!Kernel::IsSupportedArgument(kargs)) { + // we do our best to statically avoid this case in `filter_op` + throw std::runtime_error("invalid argument"); + } + auto stream_config = ck_tile::stream_config{stream}; + auto grid_size = Kernel::GridSize(M, N, kBatch); + constexpr auto block_size = Kernel::BlockSize(); + constexpr auto lds_bytes = 0; + constexpr auto kBlockPerCU = 1; + auto gemm = ck_tile::make_kernel(Kernel{}, grid_size, block_size, lds_bytes, kargs); + float elapsed_time = ck_tile::launch_kernel(stream_config, gemm); + }; + + const ck_tile::index_t k_grain = kBatch * TileK; + const ck_tile::index_t K_split = (K + k_grain - 1) / k_grain * TileK; + const ck_tile::index_t num_loop = TilePartitioner::GetLoopNum(K_split); + const bool has_hot_loop = BaseGemmPipeline::BlockHasHotloop(num_loop); + const ck_tile::TailNumber tail_num = BaseGemmPipeline::GetBlockLoopTailNum(num_loop); + + {{rendered_dispatch}} + + return 0; + } // kernel definition + } // extern C + """ + + def __init__( + self, + input_nodes: list[Buffer], + layout: Layout, + ) -> None: + super().__init__( + "ck_tile_gemm_template", + input_nodes=input_nodes, + layout=layout, + ) + + def header(self) -> IndentedBuffer: + res = super().header() + res.splice( + """ + // CK GEMM header(s) + + #include "ck_tile/ops/gemm.hpp" + #include "ck_tile/ops/epilogue.hpp" + """ + ) + return res + + def globals(self) -> IndentedBuffer: + res = super().globals() + res.splice( + """ + // CK GEMM globals + + using Row = ck_tile::tensor_layout::gemm::RowMajor; + using Col = ck_tile::tensor_layout::gemm::ColumnMajor; + + template + void dispatch_memory_pipeline_hot_loop(const ck_tile::TailNumber tail_num, Dispatcher dispatch) + { + if(tail_num == ck_tile::TailNumber::One) + { + dispatch(ck_tile::bool_constant{}, + ck_tile::integral_constant{}); + } + else if(tail_num == ck_tile::TailNumber::Full) + { + dispatch(ck_tile::bool_constant{}, + ck_tile::integral_constant{}); + } + + if constexpr(PrefetchStages > 2) + { + if(tail_num == ck_tile::TailNumber::Two) + { + dispatch(ck_tile::bool_constant{}, + ck_tile::integral_constant{}); + } + } + if constexpr(PrefetchStages > 3) + { + if(tail_num == ck_tile::TailNumber::Three) + { + dispatch(ck_tile::bool_constant{}, + ck_tile::integral_constant{}); + } + } + if constexpr(PrefetchStages > 4) + { + if(tail_num == ck_tile::TailNumber::Four) + { + dispatch(ck_tile::bool_constant{}, + ck_tile::integral_constant{}); + } + } + if constexpr(PrefetchStages > 5) + { + if(tail_num == ck_tile::TailNumber::Five) + { + dispatch(ck_tile::bool_constant{}, + ck_tile::integral_constant{}); + } + } + if constexpr(PrefetchStages > 6) + { + if(tail_num == ck_tile::TailNumber::Six) + { + dispatch(ck_tile::bool_constant{}, + ck_tile::integral_constant{}); + } + } + if constexpr(PrefetchStages > 7) + { + if(tail_num == ck_tile::TailNumber::Seven) + { + dispatch(ck_tile::bool_constant{}, + ck_tile::integral_constant{}); + } + } + } + """ + ) + return res + + def check_dtypes(self, op: "CKTileGemmOperation"): + X_dtype, W_dtype, out_dtype = [ + T.get_layout().dtype for T in [*self.input_nodes, self.output_node] + ] + if op.datatype_a != self._TORCH_DTYPE_TO_CK[X_dtype]: + return False + if op.datatype_b != self._TORCH_DTYPE_TO_CK[W_dtype]: + return False + if op.datatype_c != self._TORCH_DTYPE_TO_CK[out_dtype]: + return False + return True + + def check_layouts(self, op: "CKTileGemmOperation"): + X_layout, W_layout, out_layout = [ + torch_layout_to_ck_layout(T.get_layout()) + for T in [*self.input_nodes, self.output_node] + ] + if op.layout_a != X_layout: + return False + if op.layout_b != W_layout: + return False + if op.layout_c != out_layout: + return False + return True + + def get_gemm_problem_size(self): + X_size, W_size = [T.get_layout().size for T in [*self.input_nodes]] + + M, K = X_size + _, N = W_size + + return M, N, K + + def check_block_tiles(self, op: "CKTileGemmOperation"): + """ + The contiguous dimension of a tensor must be divisible by the block tile size + This helper function enforces it for the inputs and the output. + """ + M, N, K = self.get_gemm_problem_size() + + def check(dim_size, tile_size, is_padded): + if ( + is_static_int(dim_size) + and dim_size % tile_size != 0 + and is_padded == "false" + ): + return False + return True + + if op.layout_a == "Row": + # handle in kBatch check + return True + elif op.layout_a == "Col": + if not check(M, op.tile_m, op.m_is_padded): + return False + else: + raise AssertionError(f"Invalid layout {op.layout_a=}") + + if op.layout_b == "Row": + if not check(N, op.tile_n, op.n_is_padded): + return False + elif op.layout_b == "Col": + # handle in kBatch check + return True + else: + raise AssertionError(f"Invalid {op.layout_b=}") + + if op.layout_c == "Row": + if not check(N, op.tile_n, op.n_is_padded): + return False + elif op.layout_c == "Col": + if not check(M, op.tile_m, op.m_is_padded): + return False + else: + raise AssertionError(f"Invalid layout {op.layout_c=}") + + return True + + def check_alignments(self, op: "CKTileGemmOperation"): + """ + The contiguous dimension of a tensor must be divisible by the vector load size. + """ + M, N, K = self.get_gemm_problem_size() + + def max_alignment(contiguous_elements_per_tile, elements_per_thread, ck_dtype): + for vector_load_bytes in (16, 8, 4, 2, 1): + alignment = vector_load_bytes // self.ck_dtype_to_size[ck_dtype] + if ( + alignment > 0 + and contiguous_elements_per_tile % alignment == 0 + and elements_per_thread % alignment == 0 + ): + return alignment + + threads_per_block = ( + op.warp_m * op.warp_n * op.warp_k * self.gfx9_threads_per_warp + ) + a_elements_per_thread = op.tile_m * op.tile_k / threads_per_block + b_elements_per_thread = op.tile_n * op.tile_k / threads_per_block + + if op.layout_a == "Row": + # K is contiguous tensor dimension + a_max_vector_size = max_alignment( + op.tile_k, a_elements_per_thread, op.datatype_a + ) + if is_static_int(K) and K % a_max_vector_size != 0: + return False + elif op.layout_a == "Col": + # M is contiguous tensor dimension + a_max_vector_size = max_alignment( + op.tile_m, a_elements_per_thread, op.datatype_a + ) + if is_static_int(M) and M % a_max_vector_size != 0: + return False + else: + raise AssertionError(f"Invalid layout {op.layout_a=}") + + if op.layout_b == "Row": + # N is contiguous tensor dimension + b_max_vector_size = max_alignment( + op.tile_n, b_elements_per_thread, op.datatype_b + ) + if is_static_int(N) and N % b_max_vector_size != 0: + return False + elif op.layout_b == "Col": + # K is contiguous tensor dimension + b_max_vector_size = max_alignment( + op.tile_k, b_elements_per_thread, op.datatype_b + ) + if is_static_int(K) and K % b_max_vector_size != 0: + return False + else: + raise AssertionError(f"Invalid layout {op.layout_b=}") + + # the `default` epilogue writes C to memory by 1 tensor element + # (divisibility check not necessary) + # the `cshuffle` epilogue writes C to memory by 16 bytes + # (so the contiguous C dimension size must be divisible by the number of tensor elements in 16 bytes) + if op.epilogue == "CShuffle": + if ( + op.layout_c == "Row" + and is_static_int(N) + and N % (16 / self.ck_dtype_to_size[op.datatype_c]) != 0 + ): + return False + + return True + + def check_warp_tiles(self, op: "CKTileGemmOperation"): + if op.tile_m % (op.warp_m * op.warp_tile_m) != 0: + return False + if op.tile_n % (op.warp_n * op.warp_tile_n) != 0: + return False + if op.tile_k % (op.warp_k * op.warp_tile_k) != 0: + return False + return True + + def check_block_tile_size(self, op: "CKTileGemmOperation"): + # assuming LDS size is 64KB + if op.pipeline == "CompV4": + max_block_tile_size = 2**15 + else: + max_block_tile_size = 2**16 + + block_tile_size = ( + self.ck_dtype_to_size[op.datatype_a] * op.tile_m * op.tile_k + + self.ck_dtype_to_size[op.datatype_b] * op.tile_n * op.tile_k + ) + if block_tile_size > max_block_tile_size: + return False + return True + + def filter_op(self, op: "CKTileGemmOperation"): + """ + Determines whether a given op definition is suitable for the current + input / output of the operation that this template implements. + + Filter is based on inputs' dtype, layout and statically inferred size. + + Returns None if the op is not suitable, otherwise returns the op to be used. + """ + if not self.check_dtypes(op): + return None + if not self.check_layouts(op): + return None + if not self.check_block_tiles(op): + return None + if not self.check_alignments(op): + return None + + return op + + def emit_ck_instance(self, op: "CKTileGemmOperation"): + """ + This method is used to generate code which defines the type alias for the generated kernel class + """ + template_definition = r""" + // Gemm operator {{operation_name}} + + namespace {{operation_name}} { + // block tile + constexpr int32_t TileM = {{tile_m}}; + constexpr int32_t TileN = {{tile_n}}; + constexpr int32_t TileK = {{tile_k}}; + // warps per block + constexpr int32_t WarpM = {{warp_m}}; + constexpr int32_t WarpN = {{warp_n}}; + constexpr int32_t WarpK = {{warp_k}}; + // xdl tile + constexpr int32_t WarpTileM = {{warp_tile_m}}; + constexpr int32_t WarpTileN = {{warp_tile_n}}; + constexpr int32_t WarpTileK = {{warp_tile_k}}; + + constexpr bool kPadM = {{m_is_padded}}; + constexpr bool kPadN = {{n_is_padded}}; + constexpr bool kPadK = {{k_is_padded}}; + + using ALayout = {{layout_a}}; + using BLayout = {{layout_b}}; + using CLayout = {{layout_c}}; + + using ADataType = {{datatype_a}}; + using BDataType = {{datatype_b}}; + using CDataType = {{datatype_c}}; + using AccDataType = F32; + + constexpr bool permuteA = false; + constexpr bool permuteB = false; + constexpr bool DoubleSmemBuffer = {{has_double_smem_buffer}}; + constexpr bool TransposeC = false; + + constexpr int kBlockPerCu = 1; + constexpr ck_tile::index_t TilePartitionerGroupNum = 8; + constexpr ck_tile::index_t TilePartitionerM01 = 4; + + using GemmShape = + ck_tile::TileGemmShape, + ck_tile::sequence, + ck_tile::sequence, + permuteA, + permuteB>; + + using TilePartitioner = + ck_tile::GemmSpatiallyLocalTilePartitioner; + + using Traits = + ck_tile::TileGemmTraits; + + using GemmUniversalTraits = + ck_tile::TileGemmUniversalTraits; + + using GemmPipelineProblem = + ck_tile::GemmPipelineProblem; + + {{rendered_scheduler}} + + template + using UniversalGemmProblem = + ck_tile::UniversalGemmPipelineProblem; + + {{rendered_pipeline}} + + {{rendered_epilogue}} + + template + using Kernel = ck_tile::GemmKernel, GemmEpilogue>; + } + +""" + + def render_epilogue(epilogue_type): + if epilogue_type == "Default": + return r""" + using EpilogueProblem = ck_tile::DefaultGemm2DEpilogueProblem; + using GemmEpilogue = ck_tile::DefaultGemm2DEpilogue; + """ + elif epilogue_type == "CShuffle": + return r""" + constexpr auto kMemoryOperation = ck_tile::memory_operation_enum::set; + using DsDataType = ck_tile::tuple<>; // no bias terms for vanilla GEMM + using DsLayout = ck_tile::tuple<>; + constexpr auto ELayout = CLayout; + using CDEElementWise = ck_tile::element_wise::PassThrough; // no-op + using EpilogueProblem = ck_tile::CShuffleEpilogueProblem; + + using GemmEpilogue = ck_tile::CShuffleEpilogue; + """ + else: + raise AssertionError("Epilogue must be set") + + def render_pipeline(pipeline_type): + return rf""" + using BaseGemmPipeline = ck_tile::BaseGemmPipelineAgBgCr{pipeline_type}; + + template + using GemmPipeline = ck_tile::GemmPipelineAgBgCr{pipeline_type}>; + """ + + def render_scheduler(scheduler_type): + return rf""" + constexpr auto scheduler = ck_tile::GemmPipelineScheduler::{scheduler_type}; + """ + + rendered_definition = self._template_from_string(template_definition).render( + operation_name=op.name(), + **asdict(op), + rendered_scheduler=render_scheduler(op.scheduler), + rendered_pipeline=render_pipeline(op.pipeline), + rendered_epilogue=render_epilogue(op.epilogue), + has_double_smem_buffer=("true" if op.pipeline == "CompV4" else "false"), + ) + return rendered_definition + + def render( # type: ignore[override] + self, kernel: ROCmTemplateKernel, op: "CKTileGemmOperation", **kwargs + ) -> str: + """ + The primary entry point for the code rendering process used in this template. + """ + epilogue_nodes = kwargs.get("epilogue_nodes") + assert epilogue_nodes is None or 0 == len(epilogue_nodes) + template_buffer_node = kwargs.get("template_buffer_node") + if template_buffer_node is not None: + self.output_node = template_buffer_node + assert 2 == len(self.input_nodes) + X, W = self.input_nodes + Y = self.output_node + + instance_definition = self.emit_ck_instance(op) + + version_comment = rf"""/** +* Generated code for CK inductor backend +* See {type(self).__module__}.{type(self).__qualname__} +* +* Template instance {op} +* +* {torch.__version__=} +* torch.version.git_version={getattr(torch.version, "git_version", "None")} +*/ +""" + + def render_dispatch(pipeline_type, op_name): + switch_tailnum_template = r""" + switch (tail_num) { + {% for tail_num in valid_tailnums %} + case ck_tile::TailNumber::{{tail_num}}: + dispatch({{has_hot_loop}}, + ck_tile::integral_constant{}); + break; + {% endfor %} + default: + std::ostringstream err; + err << "Unsupported dispatch: " + << "Pipeline: " << "{{pipeline}}" + << "Prefetch stages: " << kPrefetchStages + << "Tail num: " << tail_num; + throw std::runtime_error(err.str()); + } // switch tail_num + """ + dispatch_template = r""" + if (has_hot_loop) { + {{rendered_with_hot_loop}} + } + else { // has_hot_loop == false + {{rendered_without_hot_loop}} + } // if has_hot_loop + """ + if pipeline_type == "CompV3": + return self._template_from_string(dispatch_template).render( + rendered_with_hot_loop=self._template_from_string( + switch_tailnum_template + ).render( + has_hot_loop="ck_tile::integral_constant{}", + valid_tailnums=("Full", "Odd", "Even"), + pipeline=pipeline_type, + ), + rendered_without_hot_loop=self._template_from_string( + switch_tailnum_template + ).render( + has_hot_loop="ck_tile::integral_constant{}", + valid_tailnums=("Full", "Odd", "Even"), + pipeline=pipeline_type, + ), + ) + elif pipeline_type == "Mem": + return self._template_from_string(dispatch_template).render( + rendered_with_hot_loop="dispatch_memory_pipeline_hot_loop(tail_num, dispatch);", + rendered_without_hot_loop=self._template_from_string( + switch_tailnum_template + ).render( + has_hot_loop="ck_tile::integral_constant{}", + valid_tailnums=("Full", "Odd", "Even"), + pipeline=pipeline_type, + ), + ) + elif pipeline_type == "CompV4": + return self._template_from_string(dispatch_template).render( + rendered_with_hot_loop=self._template_from_string( + switch_tailnum_template + ).render( + has_hot_loop="ck_tile::integral_constant{}", + valid_tailnums=("Two", "Three"), + pipeline=pipeline_type, + ), + rendered_without_hot_loop=self._template_from_string( + switch_tailnum_template + ).render( + has_hot_loop="ck_tile::integral_constant{}", + valid_tailnums=("Full", "Odd", "Even"), + pipeline=pipeline_type, + ), + ) + else: + raise AssertionError(f"Pipeline {pipeline_type} is not supported") + + return self._template_from_string(self.gemm_template).render( + headers=self.header().getvalue(), + globals=self.globals().getvalue(), + instance_definition=instance_definition, + kernel_definition=kernel.def_kernel( + inputs=[X, W], # type: ignore[list-item] + outputs=[Y], + names_str="X, W, Y", + size_args=[ + f"int32_t {arg}" for arg in ["M", "N", "K", "LDA", "LDB", "LDC"] + ], + ), + instance_namespace=op.name(), + version_comment=version_comment, + rendered_dispatch=render_dispatch(op.pipeline, op.name()), + ) + + def gen_ops(self): + """ + Creates a list of `CKTileGemmOperation` instances that match the GEMM operation this template represents. + The instances are guaranteed to have the correct layout, dtype and dimension padding for the GEMM input arguments. + + An instance may invalidate the GEMM configuration at runtime. + Such instances will be assigned +inf runtime by the autotune process. + """ + instances = ops() + if not instances: + raise AssertionError( + "No Composable Kernel Universal GEMM instances found. " + "Please check if the library is installed." + ) + filtered_instances = list(filter(self.filter_op, instances)) + # NB: when using a fixed list order, most likely we will pick the subset of instances + # which are very similar to each other. Randomizing the choice seems to solve this. + random.seed(-11) + chosen_instances = ( + random.sample( + filtered_instances, + min(len(filtered_instances), config.rocm.ck_tile_max_profiling_configs), + ) + if config.rocm.ck_tile_max_profiling_configs + else filtered_instances + ) + log.debug( + "generated %d ck instances after sample: %s", + len(chosen_instances), + chosen_instances, + ) + return chosen_instances + + @staticmethod + def add_choices( + choices, + layout, + input_nodes, + ): + """ + Add Composable Kernel Universal GEMM instance choices to the auto-tuning list. + """ + template = CKTileGemmTemplate( + input_nodes, + layout, + ) + ops = template.gen_ops() + for op in ops: + for k_batch in template.k_batch_choices(op): + template.maybe_append_choice( + choices, + op=op, + kBatch=k_batch, + ) + + def k_batch_choices(self, op: "CKTileGemmOperation") -> tuple[int, ...]: + """ + Returns a list of k_batch choices for the template. + """ + default_choices = (1, 2, 4, 8, 16, 32) + + def check(dim_size, tile_size, is_padded): + if ( + is_static_int(dim_size) + and dim_size % tile_size != 0 + and is_padded == "false" + ): + return False + return True + + _, _, K, _, _, _ = self.size_args() + if op.layout_a == "Row" or op.layout_b == "Col": + choices = tuple( + filter( + lambda k_batch: check(K, op.tile_k * k_batch, op.k_is_padded), + default_choices, + ) + ) + else: + choices = default_choices + + if op.epilogue == "Default": + choices = (1,) + + return choices + + def size_args(self): + """ + Sizes and strides to be used for the kernel call + """ + X = self.input_nodes[0] + W = self.input_nodes[1] + Y = self.output_node + + M = X.get_size()[0] + K = X.get_size()[1] + N = W.get_size()[1] + LDA = X.get_stride()[0 if X.get_stride()[1] == 1 else 1] + LDB = W.get_stride()[0 if W.get_stride()[1] == 1 else 1] + LDC = Y.get_stride()[0 if Y.get_stride()[1] == 1 else 1] + + return M, N, K, LDA, LDB, LDC + + def get_runtime_arg_info(self) -> list[ArgInfo]: + return [ArgInfo("kBatch", "int32_t")] + + def get_runtime_arg_values(self, **kwargs: Any) -> list[Any]: + # maybe_append_choice kwarg for k_batch must match the name of the argument + arg_names = OrderedSet([arg.name for arg in self.get_runtime_arg_info()]) + if not arg_names.issubset(kwargs): + raise ValueError( + "Missing runtime arguments: " + ", ".join(arg_names - kwargs.keys()) + ) + return [kwargs[k] for k in arg_names] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/ck_universal_gemm_template.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/ck_universal_gemm_template.py new file mode 100644 index 0000000000000000000000000000000000000000..57e5053acd6071dc655e5072fbb949711d6c7923 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/ck_universal_gemm_template.py @@ -0,0 +1,1018 @@ +# mypy: allow-untyped-defs, disable-error-code="attr-defined, valid-type" +import copy +import logging +import math +import random +from collections import namedtuple + +import sympy + +import torch +from torch._inductor import config +from torch._inductor.codegen.cpp_utils import DTYPE_TO_CPP +from torch._inductor.codegen.rocm.ck_template import CKTemplate +from torch._inductor.codegen.rocm.compile_command import rocm_compile_command +from torch._inductor.codegen.rocm.rocm_kernel import ROCmTemplateKernel +from torch._inductor.ir import Buffer, Layout +from torch._inductor.runtime.runtime_utils import next_power_of_2 + +from ...utils import IndentedBuffer, is_dynamic, try_import_ck_lib + + +_, gen_ops_library, gen_ops_preselected, CKGemmOperation = try_import_ck_lib() + + +log = logging.getLogger(__name__) + +# lightweight collection of information about a single op +InductorROCmOp = namedtuple("InductorROCmOp", ["op", "kBatch"]) + +padding_lookup = { + "M": { + "GemmSpecialization::MPadding": True, + "GemmSpecialization::MNPadding": True, + "GemmSpecialization::MKPadding": True, + "GemmSpecialization::MNKPadding": True, + }, + "N": { + "GemmSpecialization::NPadding": True, + "GemmSpecialization::MNPadding": True, + "GemmSpecialization::NKPadding": True, + "GemmSpecialization::MNKPadding": True, + }, + "K": { + "GemmSpecialization::KPadding": True, + "GemmSpecialization::MKPadding": True, + "GemmSpecialization::NKPadding": True, + "GemmSpecialization::MNKPadding": True, + }, +} + + +def is_static_int(number): + return isinstance(number, (int, sympy.Integer)) + + +def torch_layout_to_ck_layout(torch_layout): + if torch_layout.stride[-1] == 1: + return "Row" + elif torch_layout.stride[-2] == 1: + return "Col" + else: + return None + + +class CKGemmTemplate(CKTemplate): + # the JINJA template for rendering CK Universal GEMMs + gemm_template = r"""{{version_comment}} + {{headers}} + {{globals}} + {{instance_definition}} + extern "C" { + PT_EXPORT {{kernel_definition}} { + auto gemm = {{instance_type}} {}; + auto invoker = gemm.MakeInvoker(); + {% if is_batched %} + auto argument = gemm.MakeArgument( + reinterpret_cast(X), + reinterpret_cast(W), + std::array{ {{ds_names}} }, + reinterpret_cast<{{c_element_dtype}}*>(Y), + M, + N, + K, + B, + LDA, + LDB, + std::array{ {{ds_strides}} }, + LDC, + M * K, // batch_stride_A + N * K, // batch_stride_B + std::array{ {{ds_batch_strides}} }, + M * N, // batch_stride_C + {{a_elementwise_op}}, + {{b_elementwise_op}}, + {{epilogue}} // c_elementwise_op + ); + {% else %} + auto argument = gemm.MakeArgument( + reinterpret_cast(X), + reinterpret_cast(W), + std::array{ {{ds_names}} }, + reinterpret_cast<{{c_element_dtype}}*>(Y), + M, + N, + K, + LDA, + LDB, + std::array{ {{ds_strides}} }, + LDC, + kBatch, // kBatch + {{a_elementwise_op}}, + {{b_elementwise_op}}, + {{epilogue}} // c_elementwise_op + ); + {% endif %} + if (!gemm.IsSupportedArgument(argument)) { + // we do our best to statically avoid this case in `filter_op` + std::cerr << "invalid argument for gemm instance " << gemm.GetTypeString() << std::endl; + argument.Print(); + return -23; + } + if (workspace_size) { + *workspace_size = gemm.GetWorkSpaceSize(&argument); + return 0; + } + // run the kernel + #ifdef GENERATE_CK_STANDALONE_RUNNER + const auto stream_config = StreamConfig{ + stream, + /* time kernel */ 1, + /* log level */ 1, + /* n_cold_iter */ 100, + /* n_hot_iter */ 100, + /* flush_l2_cache */ 1, + /* rotate_count */ 5}; + #else + const auto stream_config = StreamConfig{stream, /* time kernel */ false, /* log level */ 0}; + #endif + + const float elapsed_time = invoker.Run(argument, stream_config); + + #ifdef GENERATE_CK_STANDALONE_RUNNER + std::cout << "elapsed time: " << elapsed_time << " ms" << std::endl; + #else + (void)elapsed_time; + #endif + return 0; + } // kernel definition + } // extern C + """ + + standalone_runner_template = r""" + #ifdef GENERATE_CK_STANDALONE_RUNNER + // standalone runner for the generated CK GEMM kernel + + {{inline_utils}} + + extern "C" { + int run_main(int argc, char** argv) { + {% if is_batched %} + const int32_t B = {{B}}; + {% endif %} + const int32_t M = {{M}}; + const int32_t N = {{N}}; + const int32_t K = {{K}}; + const int32_t LDA = {{LDA}}; + const int32_t LDB = {{LDB}}; + const int32_t LDC = {{LDC}}; + const int32_t LDD = {{LDD}}; + const int32_t kBatch = {{kBatch}}; + + using AElementType = {{a_ck_dtype}}; + using BElementType = {{b_ck_dtype}}; + using CElementType = {{c_ck_dtype}}; + {% if has_bias %} + using BiasElementType = {{bias_ck_dtype}}; + {% endif %} + {% if has_scale %} + using ScaleAElementType = {{scale_a_ck_dtype}}; + using ScaleBElementType = {{scale_b_ck_dtype}}; + {% endif %} + + using AArgType = {{a_torch_dtype}}; + using BArgType = {{b_torch_dtype}}; + using CArgType = {{c_torch_dtype}}; + {% if has_bias %} + using BiasArgType = {{bias_torch_dtype}}; + {% endif %} + {% if has_scale %} + using ScaleAArgType = {{scale_a_torch_dtype}}; + using ScaleBArgType = {{scale_b_torch_dtype}}; + {% endif %} + + using ALayout = {{a_layout}}; + using BLayout = {{b_layout}}; + using CLayout = {{c_layout}}; + {% if has_bias %} + using BiasLayout = {{bias_layout}}; + {% endif %} + + {% if is_batched %} + using strides_t = std::array; + auto get_strides = [](int32_t batch_stride, int32_t leading_dimension, auto layout) constexpr -> strides_t { + if constexpr (std::is_same_v) { + return {batch_stride, leading_dimension, 1}; + } + return {batch_stride, 1, leading_dimension}; + }; + auto a_size = strides_t{B, M, K}; + auto a_stride = get_strides(M * K, LDA, ALayout{}); + auto b_size = strides_t{B, N, K}; + auto b_stride = get_strides(N * K, LDB, BLayout{}); + auto c_size = strides_t{B, M, N}; + auto c_stride = get_strides(M * N, LDC, CLayout{}); + {% else %} + using strides_t = std::array; + auto get_strides = [](int32_t leading_dimension, auto layout) constexpr -> strides_t { + if constexpr (std::is_same_v) { + return {leading_dimension, 1}; + } + return {1, leading_dimension}; + }; + auto a_size = strides_t{M, K}; + auto a_stride = get_strides(LDA, ALayout{}); + auto b_size = strides_t{N, K}; + auto b_stride = get_strides(LDB, BLayout{}); + auto c_size = strides_t{M, N}; + auto c_stride = get_strides(LDC, CLayout{}); + {% endif %} + + Tensor a_m_k ( HostTensorDescriptor ( a_size, a_stride ) ); + Tensor b_k_n ( HostTensorDescriptor ( b_size, b_stride ) ); + {% if has_bias %} + Tensor d_m_n ( HostTensorDescriptor ( c_size, get_strides(LDD, BiasLayout{}) ) ); + {% endif %} + {% if has_scale %} + // NB: these are hardcoded + Tensor s_a_m_n ( HostTensorDescriptor ( strides_t{M, N}, get_strides(0, Row{}) )); + Tensor s_b_m_n ( HostTensorDescriptor ( strides_t{M, N}, get_strides(0, Col{}) )); + {% endif %} + + Tensor c_m_n_host ( HostTensorDescriptor ( c_size, c_stride ) ); + Tensor c_m_n_device ( HostTensorDescriptor ( c_size, c_stride ) ); + + a_m_k.GenerateTensorValue(GeneratorTensor_2()); + b_k_n.GenerateTensorValue(GeneratorTensor_2()); + {% if has_bias %} + d_m_n.GenerateTensorValue(GeneratorTensor_2()); + {% endif %} + {% if has_scale %} + s_a_m_n.GenerateTensorValue(GeneratorTensor_2()); + s_b_m_n.GenerateTensorValue(GeneratorTensor_2()); + {% endif %} + DeviceMem a_m_k_device_buf(sizeof(AElementType) * a_m_k.mDesc.GetElementSpaceSize()); + DeviceMem b_k_n_device_buf(sizeof(BElementType) * b_k_n.mDesc.GetElementSpaceSize()); + {% if has_bias %} + DeviceMem d_m_n_device_buf(sizeof(BiasElementType) * d_m_n.mDesc.GetElementSpaceSize()); + {% endif %} + {% if has_scale %} + DeviceMem s_a_m_n_device_buf(sizeof(ScaleAElementType) * s_a_m_n.mDesc.GetElementSpaceSize()); + DeviceMem s_b_m_n_device_buf(sizeof(ScaleBElementType) * s_b_m_n.mDesc.GetElementSpaceSize()); + {% endif %} + DeviceMem c_m_n_device_buf(sizeof(CElementType) * c_m_n_device.mDesc.GetElementSpaceSize()); + + a_m_k_device_buf.ToDevice(a_m_k.mData.data()); + b_k_n_device_buf.ToDevice(b_k_n.mData.data()); + {% if has_bias %} + d_m_n_device_buf.ToDevice(d_m_n.mData.data()); + {% endif %} + {% if has_scale %} + s_a_m_n_device_buf.ToDevice(s_a_m_n.mData.data()); + s_b_m_n_device_buf.ToDevice(s_b_m_n.mData.data()); + {% endif %} + + {{kernel_name}}( + static_cast(a_m_k_device_buf.GetDeviceBuffer()), + static_cast(b_k_n_device_buf.GetDeviceBuffer()), + {% if has_scale %} + static_cast(s_a_m_n_device_buf.GetDeviceBuffer()), + static_cast(s_b_m_n_device_buf.GetDeviceBuffer()), + {% endif %} + {% if has_bias %} + static_cast(d_m_n_device_buf.GetDeviceBuffer()), + {% endif %} + static_cast(c_m_n_device_buf.GetDeviceBuffer()), + {% if is_batched %} + B, + {% endif %} + M, + N, + K, + LDA, + LDB, + LDC, + LDD, + nullptr, // workspace_size + nullptr, // workspace + nullptr); // stream + + hip_check_error(hipDeviceSynchronize()); + + return 0; + } // run_main + } // extern C + + int main(int argc, char** argv) { + return run_main(argc, argv); + } + // compile with: {{compile_cmd}} + #endif // GENERATE_CK_STANDALONE_RUNNER + """ + + def __init__( + self, + input_nodes: list[Buffer], + layout: Layout, + alpha: float, + beta: float, + input_reorder: list[int] | None = None, + ) -> None: + is_batched = len(layout.size) == 3 + name = "ck_batched_gemm_template" if is_batched else "ck_gemm_template" + super().__init__( + name=name, + input_nodes=input_nodes, + layout=layout, + input_reorder=input_reorder, + ) + self.alpha = alpha + self.beta = beta + self.is_batched = is_batched + + def header(self) -> IndentedBuffer: + res = super().header() + if self.is_batched: + res.splice( + """ + // CK GEMM header(s) + + #include "ck/tensor_operation/gpu/device/impl/device_batched_gemm_multiple_d_xdl_cshuffle_v3.hpp" + """ + ) + else: + res.splice( + """ + // CK GEMM header(s) + + #include "ck/tensor_operation/gpu/device/impl/device_gemm_multiple_d_xdl_cshuffle_v3.hpp" + """ + ) + return res + + def globals(self) -> IndentedBuffer: + res = super().globals() + res.splice( + """ + // CK GEMM globals + + using Row = ck::tensor_layout::gemm::RowMajor; + using Col = ck::tensor_layout::gemm::ColumnMajor; + + using BlockGemmPipelineScheduler = ck::BlockGemmPipelineScheduler; + using GemmSpecialization = ck::tensor_operation::device::GemmSpecialization; + using BlockGemmPipelineVersion = ck::BlockGemmPipelineVersion; + + struct MultiplyMultiplyAdd { + template + __host__ __device__ constexpr void + operator()(E& e, const C& c, const D0& d0, const D1& d1, const D2& d2) const { + e = ck::type_convert( + ck::type_convert(c) + * ck::type_convert(d0) + * ck::type_convert(d1) + + ck::type_convert(d2) + ); + } + }; + """ + ) + return res + + def inline_utils(self): + res = IndentedBuffer() + res.splice( + """ + #include "host_tensor.cpp" + #include "device_memory.cpp" + """ + ) + return res + + def _has_padding(self, dimension, gemm_specialization): + # Get the relevant padding map for the given dimension + dimension_padding = padding_lookup.get(dimension, {}) + + # Check if the specialization is in the dimension's padding map + return dimension_padding.get(gemm_specialization, False) + + def filter_op(self, op_info: InductorROCmOp): + """ + Determines whether a given op definition is suitable for the current + input / output of the operation that this template implements. + + Filter is based on inputs' dtype, layout and statically inferred size. + + Returns None if the op is not suitable, otherwise returns the op to be used. + """ + op, kBatch = op_info.op, op_info.kBatch + metas = [T.get_layout() for T in [*self.input_nodes, self.output_node]] + X_meta = metas[0] + W_meta = metas[1] + Y_meta = metas[-1] + # disable the instance if dtypes don't match + if op.a_element_dtype != self._TORCH_DTYPE_TO_CK[X_meta.dtype]: + return None + if op.b_element_dtype != self._TORCH_DTYPE_TO_CK[W_meta.dtype]: + return None + if op.c_element_dtype != self._TORCH_DTYPE_TO_CK[Y_meta.dtype]: + return None + # disable the instance if layouts don't match + if op.a_layout != torch_layout_to_ck_layout(X_meta): + return None + if op.b_layout != torch_layout_to_ck_layout(W_meta): + return None + if op.c_layout != torch_layout_to_ck_layout(Y_meta): + return None + # try to avoid launching the instance with invalid problem size + # see GridwiseGemm_xdl_cshuffle_v3::CheckValidity + + M = X_meta.size[-2] + K = X_meta.size[-1] + N = W_meta.size[-1] + + if is_static_int(M): + if not self._has_padding("M", op.gemm_specialization): + if M % op.m_per_block != 0: + return None + if is_static_int(N): + if not self._has_padding("N", op.gemm_specialization): + if N % op.n_per_block != 0: + return None + if is_static_int(K): + if not self._has_padding("K", op.gemm_specialization): + if K % op.k_per_block != 0: + return None + K_t = kBatch * op.k_per_block + if K % K_t != 0: + return None + else: + # need another kBatch check here + lcm = abs(op.a_k1 * op.b_k1) // math.gcd(op.a_k1, op.b_k1) + K_t = kBatch * lcm + k_read_pad_splited = math.ceil(K / K_t) * lcm + if (k_read_pad_splited * (kBatch - 1)) >= K: + return None + + a_contig_size = ( + K if op.a_layout == "Row" else M if op.a_layout == "Col" else None + ) + if ( + is_static_int(a_contig_size) + and a_contig_size % op.a_block_transfer_src_scalar_per_vector != 0 + ): + return None + b_contig_size = ( + N if op.b_layout == "Row" else K if op.b_layout == "Col" else None + ) + if ( + is_static_int(b_contig_size) + and b_contig_size % op.b_block_transfer_src_scalar_per_vector != 0 + ): + return None + c_contig_size = ( + N if op.c_layout == "Row" else M if op.c_layout == "Col" else None + ) + c_shuffle_block_transfer_scalar_per_vector_n_per_block = ( + op.c_shuffle_block_transfer_scalar_per_vector_n_per_block[0] + if isinstance( + op.c_shuffle_block_transfer_scalar_per_vector_n_per_block, tuple + ) + else op.c_shuffle_block_transfer_scalar_per_vector_n_per_block + ) + if ( + is_static_int(c_contig_size) + and c_contig_size % c_shuffle_block_transfer_scalar_per_vector_n_per_block + != 0 + ): + return None + if not self._check_num_k_loops(op, kBatch): + return None + # TBD disable instances with invalid number of pipeline prefetch stages + # It will avoid compiling a small percentage of unrunnable instances which fail the gemm argument check + + return op + + def _check_num_k_loops(self, op, kBatch): + # Additional splitK scenario check + metas = [T.get_layout() for T in [*self.input_nodes]] + X_meta = metas[0] + W_meta = metas[1] + K = X_meta.size[-1] + if kBatch > 1: + if op.block_gemm_pipeline_version != "BlockGemmPipelineVersion::v1": + try: + prefetch_stages = self._prefetch_stages( + op, + torch.empty((), dtype=X_meta.dtype).element_size(), + torch.empty((), dtype=W_meta.dtype).element_size(), + torch.cuda.get_device_properties(X_meta.device).warp_size, + ) + except Exception as e: + log.debug( # noqa: G200 + "Failed to prefetch_stages for %s with exception %s", op.name, e + ) + # be conservative here and disable the op + return False + + K_t = op.k_per_block * kBatch + ak0 = (K + K_t - 1) // K_t * (op.k_per_block // op.a_k1) + num_k_loop = ak0 // (op.k_per_block // op.a_k1) + if num_k_loop <= prefetch_stages: + log.debug( + "Op %s is not compatible due to invalid number of pipeline prefetch stages. " + "Parameters: kBatch=%s, block_gemm_pipeline_version=%s, prefetch_stages=%s, num_k_loop=%s", + op.name(), + kBatch, + op.block_gemm_pipeline_version, + prefetch_stages, + num_k_loop, + ) + return False + + return True + + # small helper to figure out the prefetch stages on AMD + def _prefetch_stages(self, op, a_dtype_size, b_dtype_size, warp_size: int = 64): + version_str = op.block_gemm_pipeline_version.split("::")[-1] + try: + version = int(version_str[1:]) # Assuming the format is always 'vX' + except ValueError as e: + raise ValueError(f"Invalid version string: {version_str}") from e + if version not in [1, 2, 3, 4, 5]: + raise ValueError( + f"unknown prefetch stages for {op.block_gemm_pipeline_version}" + ) + # Define the mapping of versions to stages + version_to_stages = {1: 1, 3: 2, 4: 4, 5: 3} + # Get the stages for the given version + stages = version_to_stages.get(version) + if stages is None: + # This means we're at stage 2, and this requires computation + # See github.com/ROCm/composable_kernel/blob/d6a4605/include/ck/tensor_operation/gpu/block/blockwise_gemm_pipeline_xdlops_v2.hpp#L143 # noqa: B950 + wgp_per_cu = max(4 * warp_size // op.block_size, 1) + full_mem_band_prefetch_stages = math.ceil( + 32768 + / wgp_per_cu + / ( + (op.m_per_block * a_dtype_size + op.n_per_block * b_dtype_size) + * op.k_per_block + ) + ) + stages = min(max(full_mem_band_prefetch_stages, 2), 8) + + return stages + + def emit_ck_instance(self, op: "CKGemmOperation"): + # The Jinja template for generating a C++ type alias *definition* for a Universal GEMM instance + struct_name = ( + "DeviceBatchedGemmMultiD_Xdl_CShuffle_V3" + if self.is_batched + else "DeviceGemmMultiD_Xdl_CShuffle_V3" + ) + template_definition = r""" + // Gemm operator {{operation_name}} + using Operation_{{operation_name}} = + ck::tensor_operation::device::{{struct_name}}< + {{template_params}}>; + +""" + # The Jinja template for generating a C++ type alias *usage* for a Universal GEMM instance + template_type = r""" + Operation_{{operation_name}} +""" + template_params = [] + for field_name, field_value in op.dict_items(): + if isinstance(field_value, tuple): + tuple_elements = ", ".join(map(str, iter(field_value))) + if "ds" in field_name: # element type and layout for bias + arg = f"/* {field_name} */ Tuple<{tuple_elements}>" + else: # tile shape + arg = f"/* {field_name} */ S<{tuple_elements}>" + # pyrefly: ignore [bad-argument-type] + template_params.append(arg) + else: + if field_value is not None: + # pyrefly: ignore [bad-argument-type] + template_params.append(f"/* {field_name} */ {field_value}") + operation_name = op.name().replace("(", "").replace(",", "").replace(")", "") + return self._template_from_string(template_definition).render( + operation_name=operation_name, + template_params=(",\n" + 12 * " ").join(template_params), + struct_name=struct_name, + ), self._template_from_string(template_type).render( + operation_name=operation_name + ) + + def render( # type: ignore[override] + self, + kernel: ROCmTemplateKernel, + op: "CKGemmOperation", + **kwargs, + ) -> str: + """ + The primary entry point for the code rendering process used in this template. + """ + epilogue_nodes = kwargs.get("epilogue_nodes") + assert epilogue_nodes is None or 0 == len(epilogue_nodes) + template_buffer_node = kwargs.get("template_buffer_node") + if template_buffer_node is not None: + self.output_node = template_buffer_node + # input nodes: + # * X, W for matmul + # * X, W, Bias for addmm + # * X, W, inv_scale_x, inv_scale_w for scaled_mm + # * X, W, inv_scale_x, inv_scale_w, Bias for scaled_mm with bias + X, W = self.input_nodes[0], self.input_nodes[1] + Y = self.output_node + Bias = ( + self.input_nodes[2] + if 3 == len(self.input_nodes) + else self.input_nodes[4] + if 5 == len(self.input_nodes) + else None + ) + has_bias = Bias is not None + has_scale = len(self.input_nodes) in (4, 5) + op = copy.deepcopy(op) + + # This parameter is converted into tuple because of change + # from DeviceGemm_Xdl_CShuffleV3 to DeviceGemmMultiD_Xdl_CShuffle_V3. + # The first tuple element corresponds to matmul result... + if not isinstance( + op.c_shuffle_block_transfer_scalar_per_vector_n_per_block, tuple + ): + op.c_shuffle_block_transfer_scalar_per_vector_n_per_block = ( + op.c_shuffle_block_transfer_scalar_per_vector_n_per_block, + ) + + if has_scale: + scale_x = self.input_nodes[2] + scale_w = self.input_nodes[3] + if 1 == scale_x.get_numel() and 1 == scale_w.get_numel(): + # tensorwise scale for both X, W + if has_bias: + op.c_elementwise_op = "ScaleAdd" + else: + op.c_elementwise_op = "Scale" + else: + # rowwise scale for both X, W + if has_bias: + op.c_elementwise_op = "MultiplyMultiplyAdd" + else: + op.c_elementwise_op = "MultiplyMultiply" + op.c_shuffle_dtype = "F32" + op.ds_layouts = ( + torch_layout_to_ck_layout(scale_x.get_layout()), + torch_layout_to_ck_layout(scale_w.get_layout()), + ) + op.ds_element_dtypes = ( + self._TORCH_DTYPE_TO_CK[scale_x.get_layout().dtype], + self._TORCH_DTYPE_TO_CK[scale_w.get_layout().dtype], + ) + op.c_shuffle_block_transfer_scalar_per_vector_n_per_block += (1, 1) + else: + scale_x = None + scale_w = None + + bias_dtype = "" + if Bias is not None: + bias_layout = torch_layout_to_ck_layout(Bias.get_layout()) + bias_dtype = self._TORCH_DTYPE_TO_CK[Bias.get_layout().dtype] + op.ds_layouts += (bias_layout,) + op.ds_element_dtypes += (bias_dtype,) + if not has_scale: + op.c_elementwise_op = "Bilinear" + # c_shuffle_dtype is also used for adding bias to matmul result + # before converting down to the result dtype + op.c_shuffle_dtype = op.acc_dtype + # this parameter needs to be set accordingly to bias stride for correct accumulation + if bias_layout == "Row": + # bias has (N, ) shape + bias_shuffle_block_transfer_scalar_per_vector_n_per_block = ( + op.c_shuffle_block_transfer_scalar_per_vector_n_per_block + ) + elif bias_layout == "Col": + # bias has (M, 1) shape + bias_shuffle_block_transfer_scalar_per_vector_n_per_block = (1,) + else: + raise AssertionError( + "Bias layout is neither row-major nor column-major" + ) + # ...and the second tuple element corresponds to the bias + op.c_shuffle_block_transfer_scalar_per_vector_n_per_block += ( + bias_shuffle_block_transfer_scalar_per_vector_n_per_block + ) + + instance_definition, instance_type = self.emit_ck_instance(op) + + version_comment = rf"""/** +* Generated code for CK inductor backend +* See {type(self).__module__}.{type(self).__qualname__} +* +* Template instance {op} +* +* {torch.__version__=} +* torch.version.git_version={getattr(torch.version, "git_version", "None")} +*/ +""" + epilogue = None + + if op.c_elementwise_op == "Bilinear" and scale_w is None: + epilogue = f"Bilinear {{ {self.alpha}, {self.beta} }}" + + elif op.c_elementwise_op == "Scale": + epilogue = "Scale { (inv_scale_w && inv_scale_x) ? (*inv_scale_w * *inv_scale_x) : 1.0f }" + + elif op.c_elementwise_op == "ScaleAdd": + epilogue = "ScaleAdd { (inv_scale_w && inv_scale_x) ? (*inv_scale_w * *inv_scale_x) : 1.0f }" + + elif op.c_elementwise_op == "MultiplyMultiply": + epilogue = "MultiplyMultiply {}" + + elif op.c_elementwise_op == "MultiplyMultiplyAdd": + epilogue = "MultiplyMultiplyAdd {}" + + elif op.c_elementwise_op == "PassThrough": + epilogue = "PassThrough {}" + + assert epilogue is not None, "CK GEMM epilogue is not set" + + size_arg_strs = ["M", "N", "K", "LDA", "LDB", "LDC", "LDD"] + if self.is_batched: + size_arg_strs.insert(0, "B") + + res = self._template_from_string(self.gemm_template).render( + inline_utils=self.inline_utils(), + headers=self.header().getvalue(), + globals=self.globals().getvalue(), + instance_definition=instance_definition, + kernel_definition=kernel.def_kernel( + inputs=[X, W, scale_x, scale_w, Bias], # type: ignore[list-item] + outputs=[Y], + names_str="X, W, inv_scale_x, inv_scale_w, Bias, Y", + input_reorder=self.input_reorder, + size_args=[f"int32_t {arg}" for arg in size_arg_strs], + ), + instance_type=instance_type, + a_element_dtype=op.a_element_dtype, + b_element_dtype=op.b_element_dtype, + c_element_dtype=op.c_element_dtype, + bias_element_dtype=bias_dtype, + alpha=self.alpha, + beta=self.beta, + a_elementwise_op="PassThrough {}", + b_elementwise_op="PassThrough {}", + epilogue=epilogue, + has_bias=has_bias, + ds_size=1 + if op.c_elementwise_op in ("Bilinear", "ScaleAdd") + else 2 + if op.c_elementwise_op == "MultiplyMultiply" + else 3 + if op.c_elementwise_op == "MultiplyMultiplyAdd" + else 0, + ds_names=", ".join( + ["Bias"] + if op.c_elementwise_op in ("Bilinear", "ScaleAdd") + else ["inv_scale_x", "inv_scale_w"] + if op.c_elementwise_op == "MultiplyMultiply" + else ["inv_scale_x", "inv_scale_w", "Bias"] + if op.c_elementwise_op == "MultiplyMultiplyAdd" + else [] + ), + ds_strides=", ".join( + ["LDD"] + if op.c_elementwise_op in ("Bilinear", "ScaleAdd") + else ["0", "0"] + if op.c_elementwise_op == "MultiplyMultiply" + else ["0", "0", "LDD"] + if op.c_elementwise_op == "MultiplyMultiplyAdd" + else [] + ), + version_comment=version_comment, + is_batched=self.is_batched, + ds_batch_strides=", ".join([]), # FIXME when supporting baddbmm + ) + + if config.rocm.generate_test_runner: + is_static_problem = all(is_static_int(arg) for arg in self.size_args()) + # NOTE: size_arg_strs is defined above + size_arg_vals = ( + self.size_args() + if is_static_problem + else ( + f"std::stoi(argv[{k}])" for k, _ in enumerate(self.size_args(), 1) + ) + ) + size_args = dict(zip(size_arg_strs, size_arg_vals, strict=True)) + runtime_args = dict( + zip( + [a.name for a in self.get_runtime_arg_info()], + self.get_runtime_arg_values(), + ) + ) + runner_code = self._template_from_string( + self.standalone_runner_template + ).render( + inline_utils=self.inline_utils().getvalue(), + kernel_name=kernel.kernel_name, + has_bias=has_bias, + has_scale=has_scale, + is_batched=self.is_batched, + a_ck_dtype=op.a_element_dtype, + b_ck_dtype=op.b_element_dtype, + c_ck_dtype=op.c_element_dtype, + bias_ck_dtype=op.ds_element_dtypes[0] if has_bias else "", + scale_a_ck_dtype=op.ds_element_dtypes[0] + if has_scale and 2 == len(op.ds_element_dtypes) + else "BF16", + scale_b_ck_dtype=op.ds_element_dtypes[1] + if has_scale and 2 == len(op.ds_element_dtypes) + else "BF16", + a_torch_dtype=DTYPE_TO_CPP[X.get_layout().dtype], + b_torch_dtype=DTYPE_TO_CPP[W.get_layout().dtype], + c_torch_dtype=DTYPE_TO_CPP[Y.get_layout().dtype], + bias_torch_dtype=DTYPE_TO_CPP[Bias.get_layout().dtype] + if Bias is not None + else "", + scale_a_torch_dtype=DTYPE_TO_CPP[scale_x.get_layout().dtype] + if scale_x is not None + else "", + scale_b_torch_dtype=DTYPE_TO_CPP[scale_w.get_layout().dtype] + if scale_w is not None + else "", + a_layout=torch_layout_to_ck_layout(X.get_layout()), + b_layout=torch_layout_to_ck_layout(W.get_layout()), + c_layout=torch_layout_to_ck_layout(Y.get_layout()), + bias_layout=torch_layout_to_ck_layout(Bias.get_layout()) + if Bias is not None + else "", + compile_cmd=rocm_compile_command( + [""], "", "exe" + ), + **size_args, + **runtime_args, + ) + res += runner_code + + return res + + def _is_rcr_f16(self): + X_meta, W_meta, Y_meta = ( + T.get_layout() for T in [*self.input_nodes, self.output_node] + ) + X_dtype, W_dtype, Y_dtype = ( + self._TORCH_DTYPE_TO_CK[m.dtype] for m in (X_meta, W_meta, Y_meta) + ) + X_layout, W_layout, Y_layout = ( + torch_layout_to_ck_layout(m) for m in (X_meta, W_meta, Y_meta) + ) + + return ( + X_dtype == "F16" + and W_dtype == "F16" + and Y_dtype == "F16" + and X_layout == "Row" + and W_layout == "Col" + and Y_layout == "Row" + ) + + # helper to calculate a potentially optimal kBatch(es) for a problem + def _get_kBatch(self, op): + # we only set a higher kBatch if K > 16 * the larger of M and N + # this is a hand-tuned heuristic to start + metas = [T.get_layout() for T in [*self.input_nodes]] + X_meta = metas[0] + W_meta = metas[1] + M = X_meta.size[-2] + K = X_meta.size[-1] + N = W_meta.size[-1] + if is_dynamic(*self.input_nodes): + return [1] + if K // max(M, N) < config.rocm.split_k_threshold: + return [1] + # if the user is telling us which kBatches to sweep, just use those + if config.rocm.kBatch_sweep is not None: + return config.rocm.kBatch_sweep + # Calculate the number of blocks needed for each dimension + total_k_blocks = math.ceil(K / op.k_per_block) + # we want to calculate how many blocks we need to fit per CU + cus = torch.cuda.get_device_properties(X_meta.device).multi_processor_count + # again, manual heuristics as much larger kBatch are significantly worse in + # initial testing + kBatch = min(max(next_power_of_2(total_k_blocks // cus), 1), 128) + return [kBatch] + + def gen_ops(self) -> list[InductorROCmOp]: + """ + Creates a list of `CKGemmOperation` instances that match the GEMM operation this template represents. + The instances are guaranteed to have the correct layout, dtype and dimension padding for the GEMM input arguments. + + An instance may invalidate the GEMM configuration at runtime. + Such instances will be assigned +inf runtime by the autotune process. + """ + try: + from ck4inductor.batched_universal_gemm.gen_instances import ( # type: ignore[import] + gen_ops_library as gen_batched_gemm_ops_library, + ) + from ck4inductor.universal_gemm.gen_instances import ( # type: ignore[import] + gen_ops_library as gen_gemm_ops_library, + gen_ops_preselected as gen_gemm_ops_preselected, + ) + except ImportError: + return [] + + generator = None + if self.is_batched: + generator = gen_batched_gemm_ops_library + else: + generator = gen_gemm_ops_library + if config.rocm.use_preselected_instances and self._is_rcr_f16(): + generator = gen_gemm_ops_preselected + + assert generator is not None + + rops = generator() + ops = [] + for o in rops: + kBatches = self._get_kBatch(o) + for kBatch in kBatches: + # pyrefly: ignore [bad-argument-type] + ops.append(InductorROCmOp(op=o, kBatch=kBatch)) + + filtered_instances = list(filter(lambda op: self.filter_op(op), ops)) + + # NB: when using a fixed list order, most likely we will pick the subset of instances + # which are very similar to each other. Randomizing the choice seems to solve this. + random.seed(-11) + chosen_instances = ( + random.sample( + filtered_instances, + min(len(filtered_instances), config.rocm.ck_max_profiling_configs), + ) + if config.rocm.ck_max_profiling_configs + else filtered_instances + ) + log.debug( + "generated %d ck instances after filter: %s", + len(chosen_instances), + chosen_instances, + ) + return chosen_instances + + @staticmethod + def add_ck_gemm_choices( + choices, + layout, + input_nodes, + alpha=1, + beta=0, + input_reorder=None, + ): + """ + Add Composable Kernel Universal GEMM instance choices to the auto-tuning list. + """ + template = CKGemmTemplate( + input_nodes, + layout, + alpha=alpha, + beta=beta, + input_reorder=input_reorder, + ) + ops = template.gen_ops() + for op in ops: + template.maybe_append_choice( + choices, + op=op.op, + kBatch=op.kBatch, + ) + + def size_args(self): + X = self.input_nodes[0] + W = self.input_nodes[1] + Bias = ( + self.input_nodes[2] + if len(self.input_nodes) == 3 + else self.input_nodes[4] + if len(self.input_nodes) == 5 + else None + ) + Y = self.output_node + + M = X.get_size()[-2] + K = X.get_size()[-1] + N = W.get_size()[-1] + LDA = X.get_stride()[-2 if X.get_stride()[-1] == 1 else -1] + LDB = W.get_stride()[-2 if W.get_stride()[-1] == 1 else -1] + LDC = Y.get_stride()[-2 if Y.get_stride()[-1] == 1 else -1] + LDD = ( + 0 + if (Bias is None or len(Bias.get_size()) == 1) + else Bias.get_stride()[-2 if Bias.get_stride()[-1] == 1 else -1] + ) + if self.is_batched: + B = X.get_size()[0] + return B, M, N, K, LDA, LDB, LDC, LDD + else: + return M, N, K, LDA, LDB, LDC, LDD diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/compile_command.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/compile_command.py new file mode 100644 index 0000000000000000000000000000000000000000..2aa4494e0aa32a6cfbb75d4a418a2f22b076fa62 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/compile_command.py @@ -0,0 +1,152 @@ +# mypy: allow-untyped-defs +import logging +import os + +from torch._inductor import config +from torch._inductor.utils import is_linux, try_import_ck_lib + + +log = logging.getLogger(__name__) + + +def _rocm_include_paths(dst_file_ext: str) -> list[str]: + from torch.utils import cpp_extension + + rocm_include = ( + os.path.join(config.rocm.rocm_home, "include") + if config.rocm.rocm_home + else cpp_extension._join_rocm_home("include") + ) + + if config.is_fbcode(): + from libfb.py import parutil + + ck_path = parutil.get_dir_path("composable-kernel-headers") + else: + if not config.rocm.ck_dir: + ck_dir, _, _, _ = try_import_ck_lib() + if not ck_dir: + log.warning("Unspecified Composable Kernel directory") + config.rocm.ck_dir = ck_dir + ck_path = config.rocm.ck_dir or cpp_extension._join_rocm_home( + "composable_kernel" + ) + + log.debug("Using ck path %s", ck_path) + + ck_include = os.path.join(ck_path, "include") + ck_library_include = os.path.join(ck_path, "library", "include") + + # CK has to take priority over ROCm include paths + # Since CK is potentially more up-to-date + paths = [ + os.path.realpath(p) for p in (ck_include, ck_library_include, rocm_include) + ] + if dst_file_ext == "exe": + ck_utility_include = os.path.join(ck_path, "library", "src", "utility") + paths.append(os.path.realpath(ck_utility_include)) + return paths + + +def _rocm_lib_options(dst_file_ext: str) -> list[str]: + from torch.utils import cpp_extension + + rocm_lib_dir = ( + os.path.join(config.rocm.rocm_home, "lib") + if config.rocm.rocm_home + else cpp_extension._join_rocm_home("lib") + ) + hip_lib_dir = ( + os.path.join(config.rocm.rocm_home, "hip", "lib") + if config.rocm.rocm_home + else cpp_extension._join_rocm_home("hip", "lib") + ) + + opts = [ + "-include __clang_hip_runtime_wrapper.h", + f"-L{os.path.realpath(rocm_lib_dir)}", + f"-L{os.path.realpath(hip_lib_dir)}", + "-lamdhip64", + ] + if dst_file_ext == "exe": + opts += ["-lpthread", "-lstdc++"] + return opts + + +def _rocm_compiler_options() -> list[str]: + arch_list = config.rocm.arch or ["native"] + gpu_arch_flags = [f"--offload-arch={arch}" for arch in arch_list] + opts = [ + config.rocm.compile_opt_level, + "-x", + "hip", + "-std=c++20", + *gpu_arch_flags, + "-fno-gpu-rdc", + "-fPIC", + "-fvisibility=hidden", + "-mllvm", + "-amdgpu-early-inline-all=true", + "-mllvm", + "-amdgpu-function-calls=false", + "-mllvm", + "-enable-post-misched=0", + ] + if config.rocm.is_debug: + opts += ["-DDEBUG_LOG=1", "-g"] + if config.rocm.save_temps: + opts += ["--save-temps=obj"] + if config.rocm.print_kernel_resource_usage: + opts += ["-Rpass-analysis=kernel-resource-usage"] + if config.rocm.flush_denormals: + opts += ["-fgpu-flush-denormals-to-zero"] + if config.rocm.use_fast_math: + opts += ["-ffast-math"] + return opts + + +def rocm_compiler() -> str | None: + if is_linux(): + if config.rocm.rocm_home: + return os.path.realpath( + os.path.join(config.rocm.rocm_home, "llvm", "bin", "clang") + ) + try: + from torch.utils import cpp_extension + + return os.path.realpath( + cpp_extension._join_rocm_home("llvm", "bin", "clang") + ) + except OSError: + # neither config.rocm.rocm_home nor env variable ROCM_HOME are set + return "clang" + return None + + +def rocm_compile_command( + src_files: list[str], + dst_file: str, + dst_file_ext: str, + extra_args: list[str] | None = None, +) -> str: + include_paths = _rocm_include_paths(dst_file_ext) + lib_options = _rocm_lib_options(dst_file_ext) + compiler_options = _rocm_compiler_options() + compiler = rocm_compiler() + options = ( + compiler_options + + (extra_args or []) + + [f"-I{path}" for path in include_paths] + + lib_options + ) + src_file = " ".join(src_files) + # supported extensions: .o, .so, .exe + if dst_file_ext == "o": + options.append("-c") + elif dst_file_ext == "so": + options.append("-shared") + elif dst_file_ext == "exe": + options.append("-DGENERATE_CK_STANDALONE_RUNNER") + else: + raise NotImplementedError(f"Unsupported output file suffix {dst_file_ext}!") + return f"{compiler} {' '.join(options)} -o {dst_file} {src_file}" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/rocm_benchmark_request.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/rocm_benchmark_request.py new file mode 100644 index 0000000000000000000000000000000000000000..b07f33654ca2743d89858a645e2659526b835bae --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/rocm_benchmark_request.py @@ -0,0 +1,143 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import functools +import logging +from ctypes import byref, c_int, c_size_t, c_void_p +from typing import Any, TYPE_CHECKING + +import torch +from torch._inductor import config +from torch._inductor.autotune_process import ( + BenchmarkRequest, + GPUDeviceBenchmarkMixin, + TensorMeta, +) +from torch._inductor.codecache import DLLWrapper, ROCmCodeCache + + +if TYPE_CHECKING: + from collections.abc import Callable, Iterable + + +log = logging.getLogger(__name__) + + +class ROCmBenchmarkRequest(GPUDeviceBenchmarkMixin, BenchmarkRequest): + # Important: Instances of this class have to be serializable + # across process boundaries. Do not put CUDA Tensors in here! + + def __init__( + self, + kernel_name: str, + input_tensor_meta: TensorMeta | list[TensorMeta], + output_tensor_meta: TensorMeta | list[TensorMeta], + extra_args: Iterable[Any], + source_code: str, + ) -> None: + super().__init__(kernel_name, input_tensor_meta, output_tensor_meta, extra_args) + self.source_code = source_code + self.workspace_size: int = 0 + self.workspace: torch.Tensor | None = None + self.DLL: DLLWrapper | None = None + self._workspace_size_updated = False + self.hash_key: str = "" + self.source_file: str = "" + self.hash_key, self.source_file = ROCmCodeCache.write(self.source_code, "so") + + def precompile(self): + # Prepopulate code cache + # may happen in separate Threadpool + log.debug("Precompiling %s", self) + ROCmCodeCache.compile(self.source_code, "so") + if config.rocm.generate_test_runner: + ROCmCodeCache.compile(self.source_code, "exe") + log.debug("Done precompiling %s", self) + + def make_run_fn( + self, *input_tensors: torch.Tensor, out: torch.Tensor + ) -> Callable[[], None]: + self.ensure_dll_loaded() + self.update_workspace_size() + args = [c_void_p(tensor.data_ptr()) for tensor in list(input_tensors) + [out]] + size_args = [c_int(arg) for arg in self.extra_args] + log.debug( + "make_run_fn: self.kernel_name=%s, self.source_file=%s, self.hash_key=%s, self.DLL=%s, args=%s, self.extra_args=%s", + self.kernel_name, + self.source_file, + self.hash_key, + self.DLL, + args, + self.extra_args, + ) + stream_ptr = c_void_p(torch.cuda.current_stream().cuda_stream) + run_method = getattr(self.DLL, self.kernel_name) + workspace_ptr = c_void_p(0) + if self.workspace_size > 0: + self.workspace = torch.zeros( + (self.workspace_size + 7) // 8, + dtype=torch.float64, + device=out.device, + ) + workspace_ptr = c_void_p(self.workspace.data_ptr()) + + # Generate partial function. + return functools.partial( + run_method, + *args, + *size_args, + None, # null workspace size ptr + workspace_ptr, # set workspace ptr, + stream_ptr, + ) + + def update_workspace_size(self) -> None: + if self._workspace_size_updated: + return + self.ensure_dll_loaded() + unique_input_count = len( + dict.fromkeys(meta.name for meta in self.input_tensor_meta) + ) + args = [c_void_p(None) for _ in range(unique_input_count + 1)] + stream_ptr = c_void_p(torch.cuda.current_stream().cuda_stream) + + run_method = getattr(self.DLL, self.kernel_name) + # Retrieve workspace_size and initialize workspace. + c_workspace_size = c_size_t() + size_args = [c_int(arg) for arg in self.extra_args] + run_method( + *args, # input ptrs and output ptrs + *size_args, + byref( + c_workspace_size + ), # set workspace size ptr to retrieve workspace size + None, # null workspace ptr + stream_ptr, + ) + torch.cuda.synchronize() # shake out any CUDA errors + self.workspace_size = c_workspace_size.value + log.debug( + "update_workspace_size called: new workspace size=%d, self.kernel_name=%s, self.source_file=%s, self.hash_key=%s, self.DLL=%s, args=%s, self.extra_args=%s", # noqa: B950 + self.workspace_size, + self.kernel_name, + self.source_file, + self.hash_key, + self.DLL, + args, + self.extra_args, + ) + self._workspace_size_updated = True + + def ensure_dll_loaded(self): + if self.DLL is None: + self.DLL, self.hash_key, self.source_file = ROCmCodeCache.load( + self.source_code, "so" + ) + + def cleanup_run_fn(self) -> None: + if self.DLL is not None: + self.DLL.close() + self.workspace = None + + def __str__(self) -> str: + return f"{self.kernel_name=}, {self.source_file=}, {self.hash_key=}" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/rocm_cpp_scheduling.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/rocm_cpp_scheduling.py new file mode 100644 index 0000000000000000000000000000000000000000..eb8769de8db7920a11aa35518406d2227487bcce --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/rocm_cpp_scheduling.py @@ -0,0 +1,99 @@ +# mypy: allow-untyped-defs +import logging +from collections.abc import Sequence +from typing import cast, TypeGuard + +from ... import config +from ...codecache import code_hash, get_path +from ...scheduler import BaseSchedulerNode, BaseScheduling, SchedulerNode +from ...utils import get_fused_kernel_name, get_kernel_metadata, sympy_product +from ...virtualized import V +from ..common import IndentedBuffer +from .rocm_template_buffer import ROCmTemplateBuffer + + +log = logging.getLogger(__name__) + + +class ROCmCPPScheduling(BaseScheduling): + """ + Partial Scheduling implementation for ROCm C++ Kernels. + This class is intended to be used in combination with TritonScheduling, + and delegated to by CUDACombinedScheduling. + + It handles fusion decisions and ROCm C++ specific template code generation. + """ + + def group_fn(self, sizes): + return tuple(V.graph.sizevars.simplify(sympy_product(s)) for s in sizes) + + @staticmethod + def is_rocm_cpp_template(node: BaseSchedulerNode) -> TypeGuard[SchedulerNode]: + return isinstance(node, SchedulerNode) and isinstance( + node.node, ROCmTemplateBuffer + ) + + def can_fuse_vertical( + self, node1: BaseSchedulerNode, node2: BaseSchedulerNode + ) -> bool: + return False + + def define_kernel(self, src_code: str, node_schedule) -> str: + wrapper = V.graph.wrapper_code + if src_code in wrapper.src_to_kernel: + kernel_name = wrapper.src_to_kernel[src_code] + else: + fused_name = ( + get_fused_kernel_name(node_schedule, config.triton.descriptive_names) + if config.triton.descriptive_names + else "" + ) + kernel_name = "_".join(["rocm", fused_name, wrapper.next_kernel_suffix()]) + # use the original src_code as the key + wrapper.src_to_kernel[src_code] = kernel_name + src_code = src_code.replace("KERNEL_NAME", kernel_name) + + _, _, kernel_path = get_path(code_hash(src_code), "py") + + compile_wrapper = IndentedBuffer() + compile_wrapper.writeline("async_compile.rocm(r'''") + compile_wrapper.splice(src_code, strip=True) + compile_wrapper.writeline( + f"''', 'so', aot_compile={str(V.graph.aot_mode)})" + ) + + metadata_comment = f"# kernel path: {kernel_path}" + origins, detailed_origins = get_kernel_metadata(node_schedule, wrapper) + metadata_comment += "\n" + origins + "\n" + detailed_origins + wrapper.define_kernel( + kernel_name, compile_wrapper.getvalue(), metadata_comment + ) + return kernel_name + + def codegen_template( + self, + template_node: BaseSchedulerNode, + epilogue_nodes: Sequence[BaseSchedulerNode], + prologue_nodes: Sequence[BaseSchedulerNode], + ): + """ + Codegen a ROCm template, possibly with fused epilogues + """ + assert self.is_rocm_cpp_template(template_node), ( + "Template node passed to ROCmScheduler.codegen_template must be a SchedulerNode that wraps a ROCmTemplateBuffer" + ) + _, (_numel, rnumel) = template_node.group + assert rnumel == 1 + ctb: ROCmTemplateBuffer = cast(ROCmTemplateBuffer, template_node.node) + kernel, render = ctb.make_kernel_render(ctb) # type: ignore[misc] + with kernel: + template_node.mark_run() + src_code = render() + + with V.set_kernel_handler(kernel): + node_schedule = [template_node] + kernel_name = self.define_kernel(src_code, node_schedule) + self.codegen_comment(node_schedule, kernel_name) + kernel.call_kernel(kernel_name, ctb) + V.graph.removed_buffers |= kernel.removed_buffers + self.free_buffers_in_scheduler() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/rocm_kernel.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/rocm_kernel.py new file mode 100644 index 0000000000000000000000000000000000000000..99373589c227a7869f516db0d0db49e551a7da03 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/rocm_kernel.py @@ -0,0 +1,289 @@ +# mypy: allow-untyped-defs +import logging +from collections.abc import Callable, Sequence +from typing import Any, TYPE_CHECKING + +import torch._inductor.config as config +from torch._inductor.codegen.cpp_wrapper_cpu import CppWrapperCpu +from torch._inductor.utils import do_bench_using_profiling + +from ...ir import Buffer, ChoiceCaller, IRNode, Layout, PrimitiveInfoType, TensorBox +from ...virtualized import V +from ..common import Kernel, OpOverrides, WorkspaceArg, WorkspaceZeroMode +from ..cpp_utils import CppPrinter +from .rocm_benchmark_request import ROCmBenchmarkRequest +from .rocm_template_buffer import ROCmTemplateBuffer +from .rocm_utils import DTYPE_TO_ROCM_TYPE + + +if TYPE_CHECKING: + from torch._inductor.codegen.rocm.rocm_template import ArgInfo, ROCmTemplate + +log = logging.getLogger(__name__) + +cexpr = CppPrinter().doprint + + +def _normalize_idx(index: int, total_length: int) -> int: + return index if index >= 0 else index + total_length + + +class ROCmKernel(Kernel): + """ + Baseclass for ROCm based Kernels + """ + + overrides = OpOverrides # type: ignore[assignment] + + +class ROCmTemplateKernel(ROCmKernel): + """ + Template kernels defined by ROCm in C++. + """ + + _EXTRA_CPP_ARGS = "size_t* workspace_size, uint8_t* workspace, hipStream_t stream" + + def __init__( + self, + kernel_name: str, + runtime_arg_info: list["ArgInfo"], + runtime_arg_values: list[Any], + ) -> None: + """ + Initializes a new instance of the ROCmTemplateKernel class. + + Args: + kernel_name (str): The name of the kernel. + """ + super().__init__() + self.kernel_name = kernel_name + # Mapping from arg name to IRNode. + self.named_nodes: dict[str, IRNode] = {} + self.runtime_arg_info = runtime_arg_info + self.runtime_arg_values = runtime_arg_values + + def get_signature(self): + return self.signature + + def def_kernel( + self, + inputs: list[IRNode], + outputs: list[IRNode], + size_args: list[str], + names_str: str = "", + input_reorder: list[int] | None = None, + ) -> str: + """ + Hook called from template code to generate function definition and + needed args. + + Args: + inputs: List of input IRNodes + outputs: List of output IRNodes + names_str: Comma separated list of input + output argument names. + input_reorder: The actual order of input nodes. + e.g. The template might have input argument defined as [X, W, Bias], + and the actual input passed into this template could be [Bias, X, W]. + In this case, the `input_reorder` would be [2, 0, 1]. + """ + names = [x.strip() for x in names_str.strip().split(",")] + if len(inputs) + len(outputs) != len(names): + raise RuntimeError( + f"{len(inputs) + len(outputs)=} != {len(names)=}, {inputs=}, {outputs=}, {names=}" + ) + + if input_reorder == [2, 0, 1]: + input_reorder = [4, 0, 1, 2, 3] + + if input_reorder is not None: + assert len(inputs) == len(input_reorder) + else: + input_reorder = list(range(len(inputs))) + + for idx in input_reorder: + name = names[idx] + node = inputs[idx] + if node is not None: + self.named_nodes[name] = node + self.args.input_buffers[node.get_name()] = name + + for name, node in zip(names[len(inputs) : len(inputs) + len(outputs)], outputs): + if node is not None: + self.named_nodes[name] = node + self.args.output_buffers[node.get_name()] = name + + arg_defs, *_ = self.args.cpp_argdefs(DTYPE_TO_ROCM_TYPE) + + runtime_arg_defs = [f"{arg.ty} {arg.name}" for arg in self.runtime_arg_info] + + signature = f"int {self.kernel_name}({', '.join(arg_defs + size_args + runtime_arg_defs)},{self._EXTRA_CPP_ARGS})" + self.signature = signature + return signature + + def call_kernel( + self, + name: str, + node: "ROCmTemplateBuffer", # type: ignore[name-defined] + ) -> None: + """ + Generates code to call the kernel through V.graph.wrapper_code. + used from within torch._inductor.wrapper.PythonWrapperCodegen + + name: Name of kernel function. + node: The ROCmTemplateBuffer node which contains information about the kernel, it's fused epilogue nodes + as well as all required inputs and outputs. + """ + wrapper = V.graph.wrapper_code + + arg_types: list[Any] + if V.graph.cpp_wrapper: + # Make sure we initialize these kernels since they're exported as + # C-style symbol names. + assert isinstance(wrapper, CppWrapperCpu) + wrapper.initialized_kernels[name] = self + # Kinda hacky because we always originally initialize name with "KERNEL_NAME" + # So, we replace with the real kernel name passed as an arg to this function. + self.signature = self.signature.replace("KERNEL_NAME", name) + _, call_args, arg_types = self.args.cpp_argdefs(DTYPE_TO_ROCM_TYPE) + else: + _, call_args, _, arg_types = self.args.python_argdefs() + + kernel_args = [] + for arg in call_args: + # dynamo wraps unspec variable as 0d CPU tensor, need convert to scalar + if V.graph.is_unspec_arg(arg): + arg = arg + ".item()" + else: + if not V.graph.cpp_wrapper: + arg = f"c_void_p({arg}.data_ptr())" + kernel_args.append(arg) + + # add size args + size_args = [ + f"{V.graph.sizevars.simplify(sarg)}" for sarg in node.template.size_args() + ] + + if V.graph.cpp_wrapper: + kernel_args.extend(size_args) + else: + kernel_args.extend(f"c_int({sarg})" for sarg in size_args) + + if V.graph.cpp_wrapper: + arg_types.extend(["int"] * len(node.template.size_args())) + + # the runtime args come right after the size args + kernel_args.extend(self.runtime_arg_values) + for arg in self.runtime_arg_info: + arg_types.append(arg.ty) + + # workspace_size ptr is NULL to mark this call is not intended for retrieving workspace_size. + # workspace_size should have already been retrieved prior to this call. + kernel_args.append("nullptr" if V.graph.cpp_wrapper else "None") + if V.graph.cpp_wrapper: + arg_types.append("size_t*") + + if node.get_workspace_size() > 0: + ws = WorkspaceArg( + count=node.get_workspace_size(), + device=V.graph.get_current_device_or_throw(), + zero_mode=WorkspaceZeroMode.UNINITIALIZED, + outer_name=WorkspaceArg.unique_name(), + ) + wrapper.generate_workspace_allocation(ws) + data_ptr = f"{ws.outer_name}.data_ptr()" + kernel_args.append( + data_ptr if V.graph.cpp_wrapper else f"c_void_p({data_ptr})" + ) + else: + ws = None + kernel_args.append("nullptr" if V.graph.cpp_wrapper else "None") + if V.graph.cpp_wrapper: + arg_types.append("uint8_t*") + wrapper.generate_kernel_call( + name, + kernel_args, + triton=False, + arg_types=arg_types, + ) + if ws: + wrapper.generate_workspace_deallocation(ws) + + +class ROCmTemplateCaller(ChoiceCaller): + """ + ROCmTemplateCaller + + This class represents a caller for ROCm template kernels. It is a subclass of ChoiceCaller. + Attributes: + name (str): The name of the caller. + category (str): The category of the caller. + bmreq (ROCmBenchmarkRequest): The benchmark request for the caller. + template_buffer (ROCmTemplateBuffer): The template buffer for the caller. + """ + + def __init__( + self, + name: str, + category: str, + input_nodes: list[Buffer], + layout: Layout, + make_kernel_render: Callable[ + [ROCmTemplateBuffer, Sequence[IRNode] | None], str + ], + bmreq: ROCmBenchmarkRequest, + template: "ROCmTemplate", # type: ignore[name-defined] + info_kwargs: dict[str, PrimitiveInfoType | list[PrimitiveInfoType]] | None, # type: ignore[type-arg] + ) -> None: + super().__init__(name, input_nodes, layout, description="") + self.category = category + self.make_kernel_render = make_kernel_render + self.bmreq = bmreq + self.template = template + self.info_kwargs = info_kwargs + + def precompile(self) -> None: + assert self.bmreq is not None + self.bmreq.precompile() + + def benchmark(self, *args, out) -> float: + assert self.bmreq is not None + if config.profile_bandwidth_with_do_bench_using_profiling: + algo = self.bmreq.make_run_fn(*args, out=out) + return do_bench_using_profiling(algo) + return self.bmreq.benchmark(*args, out=out) + + def __str__(self) -> str: + return f"ROCmTemplateCaller(source_file={self.bmreq.source_file}, {self.info_dict()})" + + def call_name(self) -> str: + return f"rocm_template_kernels.{self.name}" + + def hash_key(self) -> str: + return "-".join( + [ + self.category, + self.bmreq.hash_key, + ] + ) + + def info_dict(self) -> dict[str, PrimitiveInfoType | list[PrimitiveInfoType]]: + """Information returned here is logged to the autotune log file when that is enabled.""" + return { + "backend": "ROCm", + "name": self.name, + **dict(self.info_kwargs["op"].dict_items()), # type: ignore[union-attr, index] + } + + def output_node(self) -> TensorBox: + self.bmreq.update_workspace_size() + buffer = ROCmTemplateBuffer( + layout=self.layout, + inputs=self.input_nodes, + make_kernel_render=self.make_kernel_render, + workspace_size=self.bmreq.workspace_size, + template=self.template, + ) + # Pass KTC annotation to the buffer for encoding + if "ktc" in self.annotations: + buffer.annotations["ktc"] = self.annotations["ktc"] + return TensorBox.create(buffer) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/rocm_template.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/rocm_template.py new file mode 100644 index 0000000000000000000000000000000000000000..ac5f514fa6460669197419837e2280ef3b7ef6a5 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/rocm_template.py @@ -0,0 +1,191 @@ +# mypy: allow-untyped-defs +import functools +import itertools +import logging +from collections.abc import Sequence +from dataclasses import dataclass +from typing import Any +from unittest.mock import patch + +from ...autotune_process import TensorMeta +from ...ir import Buffer, IRNode, Layout +from ...utils import IndentedBuffer, unique +from ...virtualized import V +from ..common import KernelTemplate +from .rocm_benchmark_request import ROCmBenchmarkRequest +from .rocm_kernel import ROCmTemplateCaller, ROCmTemplateKernel +from .rocm_template_buffer import ROCmTemplateBuffer +from .rocm_utils import DTYPE_TO_ROCM_TYPE + + +log = logging.getLogger(__name__) + + +# FIXME: unify with the CUDA version +@dataclass(frozen=True) +class ArgInfo: + name: str + ty: str + + +class ROCmTemplate(KernelTemplate): + index_counter = itertools.count() + gfx9_threads_per_warp = 64 + + def __init__( + self, + name: str, + input_nodes: list[Buffer], + layout: Layout, + input_reorder: list[int] | None = None, + ) -> None: + """ + + Baseclass for ROCm C++ Templates, derived from KernelTemplate. Not to be instantiated directly. + + Args: + name (str): The name of the ROCmTemplate object. + input_nodes (List[IRNode]): A list of input IRNodes. + layout (Layout): The layout of the output buffer / tensor. + input_reorder (Optional[List[int]]): An optional list that specifies the order of the input nodes. + + """ + super().__init__(name) + self.input_nodes = input_nodes + self.output_node: Buffer = Buffer(name="buf_out", layout=layout) + self.input_reorder = input_reorder + self.layout = layout + + def generate( # type: ignore[override] + self, + **kwargs, + ) -> ROCmTemplateCaller: + """ + Generates the ROCm template caller object for the given GEMM template and operation. This ROCmTemplateCaller + may be used to call and benchmark the generated ROCm kernel in a standalone manner to enable Autotuning. + + Args: + kwargs: Additional keyword arguments. + + Returns: + A ROCmTemplateCaller object representing the generated ROCm template caller. + """ + kernel_name = f"rocm_{self.name}" + kernel_hash_name = f"rocm_{self.name}_{next(self.index_counter)}" + with ( + patch.object(V.graph, "get_dtype", self._fake_get_dtype(self.output_node)), + ROCmTemplateKernel( + kernel_name=kernel_name, + runtime_arg_info=self.get_runtime_arg_info(), + runtime_arg_values=self.get_runtime_arg_values(**kwargs), + ) as kernel, + ): + code = self.render(kernel=kernel, **kwargs) + _, call_args, _, _ = kernel.args.python_argdefs() + log.debug("Autotune key: %s, Generated Code:\n%s", kernel_hash_name, code) + log.debug( + "Args: cpp_argdefs: %s, python_argdefs: %s", + kernel.args.cpp_argdefs(DTYPE_TO_ROCM_TYPE), + kernel.args.python_argdefs(), + ) + + input_reorder = ( + self.input_reorder + if self.input_reorder is not None + else list(range(len(self.input_nodes))) + ) + expected_args = list( + unique(self.input_nodes[idx].get_name() for idx in input_reorder) + ) + expected_args.extend([self.output_node.get_name()]) + assert list(call_args)[: len(expected_args)] == expected_args, ( + call_args, + expected_args, + ) + + size_args = ( + self.size_args() if hasattr(self, "size_args") else () + ) # subclass should define def size_args() + # Resolve symbolic sizes to concrete ints for benchmarking only. + size_args_ints = list(V.graph.sizevars.optimization_hints(size_args)) + # The runtime args come right after the size args + runtime_args = self.get_runtime_arg_values(**kwargs) + extra_args = size_args_ints + runtime_args + bmreq = ROCmBenchmarkRequest( + kernel_name=kernel_name, + input_tensor_meta=TensorMeta.from_irnodes(self.input_nodes), + output_tensor_meta=TensorMeta.from_irnodes(self.output_node), + extra_args=extra_args, + source_code=code, + ) + + def make_kernel_render( + template_node: ROCmTemplateBuffer, + epilogue_nodes: Sequence[IRNode] | None = None, + ): + kernel = ROCmTemplateKernel( + kernel_name="KERNEL_NAME", + runtime_arg_info=self.get_runtime_arg_info(), + runtime_arg_values=self.get_runtime_arg_values(**kwargs), + ) + render = functools.partial( + self.render, + kernel=kernel, + template_buffer_node=template_node, + epilogue_nodes=epilogue_nodes, + **kwargs, # includes "op" argument in case of CUTLASSGemmTemplate + ) + return kernel, render + + return ROCmTemplateCaller( + kernel_hash_name, + self.name, + self.input_nodes, + self.output_node.get_layout(), + make_kernel_render, + bmreq, + self, + kwargs, + ) + + def header(self) -> IndentedBuffer: + res = IndentedBuffer() + res.splice( + """ + #include + #include + #include + #include + #include + """ + ) + return res + + def globals(self) -> IndentedBuffer: + res = IndentedBuffer() + res.splice( + """ + // We compile all models with -fvisibility=hidden. Any symbols that need to be + // exposed in the final shared library must be declared with PT_EXPORT to make + // them visible. + #ifdef __GNUC__ // Applies to any compiler with GNU extensions (clang and g++) + #define PT_EXPORT __attribute__((__visibility__("default"))) + #else + #ifdef _WIN32 + #define PT_EXPORT __declspec(dllexport) + #else + #define PT_EXPORT + #endif + #endif + """ + ) + return res + + def render(self, **kwargs) -> str: + raise NotImplementedError + + def get_runtime_arg_info(self) -> list[ArgInfo]: + return [] + + def get_runtime_arg_values(self, **kwargs) -> list[Any]: + return [] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/rocm_template_buffer.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/rocm_template_buffer.py new file mode 100644 index 0000000000000000000000000000000000000000..9c90d71c19980279f13cad86d7385825ba7212c9 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/rocm_template_buffer.py @@ -0,0 +1,27 @@ +from collections.abc import Callable, Sequence +from typing import TypeVar +from typing_extensions import ParamSpec + +from ...ir import Buffer, Layout, TemplateBuffer + + +_P = ParamSpec("_P") +_T = TypeVar("_T") + + +class ROCmTemplateBuffer(TemplateBuffer): + def __init__( + self, + layout: Layout, + inputs: Sequence[Buffer], + make_kernel_render: Callable[_P, _T], + workspace_size: int, + template: "ROCmTemplate", # type: ignore[name-defined] # noqa: F821 + ) -> None: + super().__init__(layout, inputs, make_kernel_render) + # Global memory (in bytes) needed for this template. + self.workspace_size = workspace_size + self.template = template + + def get_workspace_size(self) -> int: + return self.workspace_size if self.workspace_size is not None else 0 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/rocm_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/rocm_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..36871ac5c7f8fcf0a8b91a143168ab1b90530b0b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/rocm/rocm_utils.py @@ -0,0 +1,17 @@ +# mypy: allow-untyped-defs + + +import torch + +from ..cpp_utils import DTYPE_TO_CPP + + +DTYPE_TO_ROCM_TYPE = { + **DTYPE_TO_CPP, + torch.float16: "uint16_t", + torch.float8_e4m3fnuz: "uint8_t", + torch.float8_e5m2fnuz: "uint8_t", + torch.float8_e4m3fn: "uint8_t", + torch.float8_e5m2: "uint8_t", + torch.bfloat16: "uint16_t", +} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/segmented_tree.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/segmented_tree.py new file mode 100644 index 0000000000000000000000000000000000000000..40d132d66ea236526c8ba89c9a263e6786ae6b78 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/segmented_tree.py @@ -0,0 +1,242 @@ +from collections.abc import Callable +from typing import Generic, TypeVar + + +T = TypeVar("T") + + +def _value_or(opt: T | None, default: T) -> T: + return opt if opt is not None else default + + +class SegmentedTree(Generic[T]): + def __init__( + self, + values: list[T], + update_op: Callable[[T, T], T], + summary_op: Callable[[T, T], T], + identity_element: T, + ): + """ + Initialize a segment tree with the given values and operations. + + Args: + values: list of initial values + update_op: Function to apply when updating a value (e.g., addition) + summary_op: Function to summarize two values (e.g., min, max, sum) + identity_element: Identity element for the summary_op (e.g., 0 for sum, float('inf') for min) + + Raises: + ValueError: If the input values list is empty + """ + if not values: + raise ValueError("Cannot create a segment tree with empty values list") + + self.n = len(values) + self.update_op = update_op + self.summary_op = summary_op + self.identity = identity_element + + # Size of segment tree array (next power of 2 * 2) + # The tree follows a standard heap layout where + # node `n`'s children are at `2*n` and `2*n+1`. + # Index 0 is unused. + self.size = 1 + while self.size < self.n: + self.size *= 2 + self.size *= 2 + + # Initialize tree and lazy arrays + self.tree = [identity_element] * self.size + # The lazy array contains updates to the given node + # Upon update, we only push updates to the top-most + # nodes that fully receive the update. We then + # propagate the update down as required (i.e., when + # we receive an interval query that neither fully + # contains the node nor fully doesn't contain the + # node + self.lazy: list[T | None] = [None] * self.size + + # Build the tree + self._build(values, 1, 0, self.n - 1) + + def _build(self, values: list[T], node: int, start: int, end: int) -> None: + """ + Build the segment tree recursively. + + Args: + values: Original array of values + node: Current node index in the segment tree + start: Start index of the segment + end: End index of the segment + """ + if start == end: + # Leaf node + if start < len(values): + self.tree[node] = values[start] + return + + mid = (start + end) // 2 + left_child = 2 * node + right_child = 2 * node + 1 + + # Recursively build left and right subtrees + self._build(values, left_child, start, mid) + self._build(values, right_child, mid + 1, end) + + # Update current node with summary of children + self.tree[node] = self.summary_op(self.tree[left_child], self.tree[right_child]) + + def _children(self, node: int) -> list[int]: + return [2 * node, 2 * node + 1] + + def _push_lazy(self, node: int, start: int, end: int) -> None: + """ + Push lazy updates down to children. + + Args: + node: Current node index + start: Start index of the segment + end: End index of the segment + """ + lazy_node = self.lazy[node] + if lazy_node is None: + return + + # Apply lazy update to current node + self.tree[node] = self.update_op(self.tree[node], lazy_node) + + if start != end: # Not a leaf node + # Propagate to children + for child in self._children(node): + self.lazy[child] = self.update_op( + _value_or(self.lazy[child], self.identity), lazy_node + ) + + # Clear the lazy value + self.lazy[node] = None + + def _update_range_helper( + self, node: int, start: int, end: int, left: int, right: int, value: T + ) -> None: + """ + Helper method to update a range of values in the segment tree. + + Args: + node: Current node index + start: Start index of the current segment + end: End index of the current segment + left: Start index of the range to update + right: End index of the range to update + value: Value to apply to the range + """ + # Push lazy updates before processing this node + self._push_lazy(node, start, end) + + # No overlap + if start > right or end < left: + return + + # Complete overlap + if start >= left and end <= right: + # Apply update to current node + self.lazy[node] = value + self._push_lazy(node, start, end) + return + + # Partial overlap, recurse to children + mid = (start + end) // 2 + left_child = 2 * node + right_child = 2 * node + 1 + + self._update_range_helper(left_child, start, mid, left, right, value) + self._update_range_helper(right_child, mid + 1, end, left, right, value) + + # Update current node based on children + self.tree[node] = self.summary_op(self.tree[left_child], self.tree[right_child]) + + def _query_range_helper( + self, node: int, start: int, end: int, left: int, right: int + ) -> T: + """ + Helper method to query a range of values in the segment tree. + + Args: + node: Current node index + start: Start index of the current segment + end: End index of the current segment + left: Start index of the range to query + right: End index of the range to query + + Returns: + Summary value for the range + """ + # No overlap + if start > right or end < left: + return self.identity + + # Push lazy updates before processing this node + self._push_lazy(node, start, end) + + # Complete overlap + if start >= left and end <= right: + return self.tree[node] + + # Partial overlap, recurse to children + mid = (start + end) // 2 + left_child = 2 * node + right_child = 2 * node + 1 + + left_result = self._query_range_helper(left_child, start, mid, left, right) + right_result = self._query_range_helper(right_child, mid + 1, end, left, right) + + # Combine results from children + return self.summary_op(left_result, right_result) + + def update_range(self, start: int, end: int, value: T) -> None: + """ + Update a range of values in the segment tree. + + Args: + start: Start index of the range to update (inclusive) + end: End index of the range to update (inclusive) + value: Value to apply to the range + + Raises: + ValueError: If start > end or indices are out of bounds + """ + if start > end: + raise ValueError("Start index must be less than or equal to end index") + + if start < 0 or start >= self.n: + raise ValueError(f"Start index {start} out of bounds [0, {self.n - 1}]") + + if end < 0 or end >= self.n: + raise ValueError(f"End index {end} out of bounds [0, {self.n - 1}]") + + self._update_range_helper(1, 0, self.n - 1, start, end, value) + + def summarize_range(self, start: int, end: int) -> T: + """ + Query a range of values in the segment tree. + + Args: + start: Start index of the range to query (inclusive) + end: End index of the range to query (inclusive) + + Returns: + Summary value for the range according to the summary operation + + Raises: + ValueError: If start > end or indices are out of bounds + """ + if start > end: + raise ValueError("Start index must be less than or equal to end index") + + if start < 0 or start >= self.n: + raise ValueError(f"Start index {start} out of bounds [0, {self.n - 1}]") + + if end < 0 or end >= self.n: + raise ValueError(f"End index {end} out of bounds [0, {self.n - 1}]") + + return self._query_range_helper(1, 0, self.n - 1, start, end) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/simd.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/simd.py new file mode 100644 index 0000000000000000000000000000000000000000..406d59ae8ec2ca8975e81abb10702344fc42b5f0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/simd.py @@ -0,0 +1,3242 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import collections +import contextlib +import dataclasses +import functools +import itertools +import logging +import math +import operator +import textwrap +from collections import Counter +from typing import Any, Generic, NamedTuple, TYPE_CHECKING +from typing_extensions import TypeVar + +import sympy + +import torch +import torch._logging +from torch._inductor import metrics +from torch._inductor.ir import MultiTemplateBuffer +from torch._inductor.tiling_utils import analyze_memory_coalescing +from torch.fx.experimental.symbolic_shapes import free_unbacked_symbols +from torch.fx.immutable_collections import immutable_dict +from torch.utils._ordered_set import OrderedSet +from torch.utils._sympy.functions import FloorDiv, Identity, ModularIndexing +from torch.utils._sympy.symbol import ( + free_symbol_is_type, + prefix_str, + symbol_is_type, + SymT, +) + +from ..._dynamo.utils import counters +from .. import config, ir, scheduler +from ..analyze_preserves_zero_mask import prologue_preserves_zero_mask +from ..codecache import code_hash, PyCodeCache +from ..dependencies import MemoryDep, StarDep, WeakDep + + +if TYPE_CHECKING: + from collections.abc import Callable + + from ..ir import IRNode + +from ..optimize_indexing import indexing_dtype_strength_reduction +from ..runtime.coordinate_descent_tuner import CoordescTuner +from ..runtime.hints import DeviceProperties +from ..runtime.runtime_utils import green_text, last_power_of_2, yellow_text +from ..scheduler import BaseSchedulerNode, BaseScheduling, WhyNoFuse +from ..utils import ( + cache_property_on_self, + expr_fits_within_32bit, + get_dtype_size, + IndentedBuffer, + Placeholder, + prefix_is_reduction, + sympy_index_symbol, + sympy_product, + sympy_subs, + unique, +) +from ..virtualized import ops, OpsWrapper, V +from .block_analysis import BlockPatternMatcher +from .common import CSEVariable, index_prevent_reordering, Kernel, PythonPrinter +from .multi_kernel import MultiKernel, SizeHintMultiKernel +from .simd_kernel_features import ( + DisableReduction, + EnableReduction, + NodeScheduleEntry, + NodeScheduleMarker, + SIMDKernelFeatures, +) + + +if TYPE_CHECKING: + from collections.abc import Iterable, Iterator, Sequence + + from torch._inductor.tiling_utils import CoalesceVarAnalysis + + +log = logging.getLogger(__name__) +perf_hint_log = torch._logging.getArtifactLogger(__name__, "perf_hints") +schedule_log = torch._logging.getArtifactLogger(__name__, "schedule") +fusion_log = torch._logging.getArtifactLogger(__name__, "fusion") + + +pexpr = PythonPrinter().doprint + +all_prefixes = OrderedSet(["z", "y", "x", "r0_", "r1_"]) + + +def get_max_tiles(default: int = 2) -> int: + max_tiles = torch._inductor.config.triton.max_tiles + return max_tiles if max_tiles is not None else default + + +@dataclasses.dataclass +class IterationRanges: + """ + Each range tree represents multiple sets of iteration indexing + in a single tiled dimension in the output kernel. + + If you have two loops ranges one (4, 3, 2) and another (4, 6), + then the range tree will be: + 4 (i0) + 3 (i1) 6 (i3) + 2 (i2) + Where i0 is shared between both loops, but then the split into + different indexing vars. All loop ranges must iterate over + the same number of elements. + """ + + def __init__( + self, + name: str, + var_list: list[sympy.Symbol], + var_ranges: dict[sympy.Symbol, sympy.Expr], + numel: sympy.Expr, + prefix: str, + *, + kernel: SIMDKernel, + divisor=sympy.S.One, + length=sympy.S.One, + root: IterationRangesRoot, + ) -> None: + super().__init__() + self.name = name + self.var_list = var_list + self.var_ranges = var_ranges + self.numel = numel + self.prefix = prefix + self.divisor = divisor + self.length = length + self.kernel = kernel + self.root = root + + @property + @cache_property_on_self + def is_reduction(self) -> bool: + return prefix_is_reduction(self.prefix) + + def symbol(self) -> sympy.Symbol: + return sympy_index_symbol(self.name) + + @property + @cache_property_on_self + def symt(self) -> SymT: + prefix_to_symt = {prefix: symt for symt, prefix in prefix_str.items()} + return prefix_to_symt[self.prefix] + + +class IterationRangesRoot(IterationRanges): + """ + Root of a iteration range tree that represents a single + tiled dimension in the output kernel. It contains multiple + sets of iteration represented with IterationRangesEntry. + """ + + def __init__( + self, + name: str, + numel: sympy.Expr, + prefix: str, + index: int, + kernel: SIMDKernel, + pid_cache: dict[str, str] | None = None, + *, + is_loop: bool, + tensor_dim: int | None, + grid_dim: int | None, + has_zdim: bool, + ) -> None: + if pid_cache is None: + pid_cache = {} + super().__init__( + name=name, + var_list=[], + var_ranges={}, + numel=numel, + prefix=prefix, + kernel=kernel, + root=self, + ) + self.index = index + # Store all the nodes in one flat list + self.nodes: dict[sympy.Expr, IterationRangesEntry] = {} + # This is for re-ordering program ID in triton mm template + # pid_cache["tl.program_id(0)"] = pid_m + self.pid_cache: dict[str, str] = pid_cache + + # True if the dimension is implemented as a single program looping over + # the full dimension (currently only used for non-persistent reduction) + + assert not is_loop or (self.is_reduction and grid_dim is None) + self.is_loop = is_loop + # Index of corresponding dimension on triton tensors + self.tensor_dim = tensor_dim + # Index of corresponding dimension in the triton grid + self.grid_dim = grid_dim + self.has_zdim = has_zdim + + def __repr__(self) -> str: + return f"IterationRangesRoot({self.name!r}, {self.numel}, ...)" + + def cache_clear(self) -> None: + for node in self.nodes.values(): + node.cache_clear() + + def index_sym(self) -> sympy.Symbol: + return sympy_index_symbol(f"{self.prefix}index") + + def lookup(self, divisor: sympy.Expr, length: sympy.Expr) -> IterationRangesEntry: + """ + Lookup a given RangeTreeEntry, creating it if needed + """ + if V.graph.sizevars.statically_known_equals(divisor * length, self.numel): + expr = FloorDiv(self.index_sym(), divisor) + else: + expr = ModularIndexing(self.index_sym(), divisor, length) + + if expr not in self.nodes: + node = IterationRangesEntry( + f"{self.prefix}{next(V.kernel.iter_vars_count)}", + divisor, + length, + expr, + self, + ) + V.kernel.range_tree_nodes[node.symbol()] = node + self.var_list.append(node.symbol()) + self.var_ranges[node.symbol()] = length + self.nodes[expr] = node + return self.nodes[expr] + + def construct_entries( + self, lengths: list[sympy.Expr] + ) -> list[IterationRangesEntry]: + divisor = sympy.S.One + itervars = [] + for length in reversed(lengths): + itervars.append(self.lookup(divisor, length)) + divisor = divisor * length + return [*reversed(itervars)] + + def construct(self, lengths: list[sympy.Expr]) -> list[sympy.Symbol]: + return [e.symbol() for e in self.construct_entries(lengths)] + + def vars_and_sizes( + self, index: sympy.Expr + ) -> tuple[list[sympy.Symbol], list[sympy.Expr]]: + """Figure out vars from this tree used in index""" + + def get_sort_key(x: IterationRangesEntry) -> tuple[int, bool]: + """ + Gets the key for sorting nodes. When two nodes have the + same divisor, the node with length as 1 should be handled + first so the current divisor is not changed after multiplied + node.length. Returns `not length_is_one_hint` for ascending + sort. + """ + divisor_hint = V.graph.sizevars.optimization_hint(x.divisor) + length_is_one_hint = V.graph.sizevars.optimization_hint(x.length) == 1 + return (divisor_hint, not length_is_one_hint) + + nodes = [V.kernel.range_tree_nodes.get(s) for s in index.free_symbols] + nodes = [n for n in nodes if n and n.prefix == self.prefix] + nodes.sort(key=lambda x: get_sort_key(x)) + divisor = sympy.S.One + index_vars = [] + sizes = [] + + def add(node): + nonlocal divisor + index_vars.append(node.symbol()) + sizes.append(node.length) + divisor = divisor * node.length + + for node in nodes: + if not V.graph.sizevars.statically_known_equals(node.divisor, divisor): + # fill in unused index var + add(self.lookup(divisor, FloorDiv(node.divisor, divisor))) + divisor = node.divisor + add(node) + if not V.graph.sizevars.statically_known_equals(self.numel, divisor): + # fill in unused index var + add(self.lookup(divisor, FloorDiv(self.numel, divisor))) + + return [*reversed(index_vars)], [*reversed(sizes)] + + +class IterationRangesEntry(IterationRanges): + def __init__( + self, + name: str, + divisor: sympy.Expr, + length: sympy.Expr, + expr: sympy.Expr, + parent: IterationRanges, + ) -> None: + super().__init__( + name=name, + numel=parent.numel / length, + var_list=parent.var_list, + var_ranges=parent.var_ranges, + prefix=parent.prefix, + divisor=divisor, + length=length, + kernel=parent.kernel, + root=parent.root, + ) + self.parent = parent + self.codegen = functools.lru_cache(None)(self._codegen) + self.expr = expr + + def __repr__(self) -> str: + return f"IterationRangesEntry({self.name}, {self.divisor}, {self.length}, {self.expr}, {self.var_ranges})" + + def set_name(self, name: str) -> None: + self.codegen = lambda: name # type: ignore[assignment] + self.codegen.cache_clear = lambda: None # type: ignore[method-assign] + self.name = name + + def cache_clear(self) -> None: + self.codegen.cache_clear() + + def _codegen(self) -> str: + V.kernel.codegen_iteration_ranges_entry(self) + return self.name + + def precomputed_args(self) -> list[sympy.Expr]: + # for dynamic shapes, find parts of indexing expressions that have to be precomputed + precomputed_args: list[sympy.Expr] = [] + if isinstance(self.expr, sympy.Symbol): + return precomputed_args + assert isinstance(self.expr, (FloorDiv, ModularIndexing)), type(self.expr) + for arg in self.expr.args[1:]: + if not isinstance(arg, (sympy.Integer, sympy.Symbol)): + symbols = arg.free_symbols + if len(symbols) > 0 and all( + symbol_is_type(s, SymT.SIZE) for s in symbols + ): + precomputed_args.append(arg) + return precomputed_args + + def __hash__(self) -> int: + return hash(self.name) + + def __eq__(self, other: object) -> bool: + assert isinstance(other, IterationRangesEntry) + return self.name == other.name + + +def constant_repr(value: int | float) -> str: + if value == float("inf"): + return 'float("inf")' + elif value == float("-inf"): + return 'float("-inf")' + elif math.isnan(value): + return 'float("nan")' + return repr(value) + + +CSEVariableType = TypeVar("CSEVariableType", bound=CSEVariable, default=CSEVariable) + + +@dataclasses.dataclass +class PartialAccumulate: + buffer_name: str + reduction_type: str + value: Any + + +class NodeInfo(NamedTuple): + """ + Pre-computed node information for combo kernel partitioning. + """ + + node_schedule: list + tiling: dict + numel: Any + rnumel: Any + features: SIMDKernelFeatures + is_persistent_reduction: bool + + +class SIMDKernel(Kernel[CSEVariableType], Generic[CSEVariableType]): + """ + Common base class for Triton/Halide codegen which both use flattened indexing rather than loop nests. + """ + + sexpr: Callable[[sympy.Expr], str] = pexpr + kexpr: Callable[[sympy.Expr], str] + allow_block_ptr: bool = False + # pyrefly: ignore [bad-override] + kernel_name: str + + def __init__( + self, + tiling: dict[str, sympy.Expr], + features: SIMDKernelFeatures, + pid_cache: dict[str, str] | None = None, + override_persistent_reduction: bool | None = None, + override_cooperative_reduction: bool | None = None, + tiling_scores: dict[str, sympy.Expr] | None = None, + mix_order_reduction: bool = False, + ) -> None: + if pid_cache is None: + pid_cache = {} + super().__init__() + self.features = features + self.mutations = features.get_mutations() + self.body = IndentedBuffer() + self.indexing_code = IndentedBuffer() + self.numels = { + prefix: V.graph.sizevars.simplify(val) for prefix, val in tiling.items() + } + self.range_trees: list[IterationRangesRoot] = [] + self.range_tree_nodes: dict[sympy.Symbol, IterationRangesEntry] = {} + self.iter_vars_count = itertools.count() + self.inside_reduction = features.is_reduction() + self.cooperative_reduction: bool = ( + override_cooperative_reduction + if override_cooperative_reduction is not None + else self.should_use_cooperative_reduction() + ) + self.tiling_scores: dict[str, sympy.Expr] | None = tiling_scores + self.tiling: dict[str, sympy.Expr] = tiling + self.persistent_reduction: bool = ( + override_persistent_reduction + if override_persistent_reduction is not None + else self.should_use_persistent_reduction() + ) + self.mix_order_reduction: bool = mix_order_reduction + self.no_x_dim = self.want_no_x_dim() + self.code_hash: str | None = None + # Info to enable multiple store_output calls for epilogue subtiling + self.store_output_ctr = itertools.count() + self.is_native_matmul = False + if config.triton.native_matmul: + for node in self.features.node_schedule: + if ( + isinstance(node, scheduler.SchedulerNode) + and isinstance(node.node, ir.ComputedBuffer) + and node.node.get_reduction_type() == "dot" + ): + self.is_native_matmul = True + break + + # define this in a closure to make cache local to object + @functools.cache + def simplify_indexing(index: sympy.Expr): + index = V.graph.sizevars.simplify_with_ranges(index, self.var_ranges()) + for tree in self.range_trees: + index = self.combine_contiguous_dims(index, tree) + + return self.combine_modular_indexing_pairs(index) + + self.simplify_indexing = simplify_indexing + self.initialize_range_tree(pid_cache) + + self.rsplit_size = 0 + self.saved_partial_accumulate: list[PartialAccumulate] = [] + + def codegen_template_body( + self, + scheduling, + template_node, + epilogue_nodes, + prologue_nodes, + buf_name_to_prologue_group, + prologue_preserves_zero_mask_fn, + render, + ) -> str: + """Generate template source code with fused prologues and epilogues. + + Subclasses override this to implement custom code generation. + The default implementation raises NotImplementedError — the actual + standard path lives in ``TritonTemplateKernel.codegen_template_body``. + """ + raise NotImplementedError + + def get_unfused_epilogues(self) -> list[Any]: + """Return epilogue nodes that were not fused into the kernel. + + These nodes need separate codegen (via ``call_kernel``) and must + be excluded from ``mark_run`` in ``_codegen_single_template``. + + The standard path fuses all epilogues, so this returns ``[]``. + ``ExternalTritonTemplateKernel`` overrides this for epilogues that + don't read exactly one template output and cannot be fused. + """ + return [] + + def _get_store_output_subgraph_name(self, i: int) -> str: + return f"" + + def get_store_output_count(self): + total = next(self.store_output_ctr) + self.store_output_ctr = itertools.count(start=total - 1, step=1) + return total + + @property + @cache_property_on_self + def num_reduction_dims(self) -> int: + return sum(prefix_is_reduction(prefix) for prefix in self.numels) + + def dtype_to_str(self, dtype: torch.dtype) -> str: + raise NotImplementedError + + def get_index_dtype_as_torch_dtype(self) -> torch.dtype: + return self.features.select_index_dtype() + + @property + def index_dtype(self) -> str: + return self.dtype_to_str(self.get_index_dtype_as_torch_dtype()) + + def want_no_x_dim(self) -> bool: + return False + + def construct_range_trees( + self, + pid_cache: dict[str, str] | None, + inside_reduction: bool, + is_reduction: bool, + numels: dict[str, sympy.Expr], + no_x_dim: bool, + ) -> list[IterationRangesRoot]: + active_prefixes = OrderedSet( + prefix for prefix in all_prefixes if prefix in numels + ) + no_r_dim = not inside_reduction or not is_reduction + + def filtered_index_map(seq, mask) -> dict[Any, int]: + return { + val: idx for idx, val in enumerate(val for val in seq if val in mask) + } + + grid_dims = ["x", "y", "z"] + pointwise_tensor_dims = list(reversed(grid_dims)) + reduction_dims = ["r0_", "r1_"] + if no_x_dim: + tensor_dims = reduction_dims + elif no_r_dim: + tensor_dims = pointwise_tensor_dims + else: + tensor_dims = pointwise_tensor_dims + reduction_dims + + # Filter out unused tensor dims. + # Convert to dicts for O(1) index lookup. + tensor_dim_map = filtered_index_map(tensor_dims, active_prefixes) + grid_dim_map = filtered_index_map(grid_dims, all_prefixes) + + range_trees = [] + for i, prefix in enumerate(active_prefixes): + is_reduction = prefix_is_reduction(prefix) + tensor_dim = tensor_dim_map.get(prefix) + grid_dim = grid_dim_map.get(prefix) + index = i if grid_dim is None else grid_dim + range_trees.append( + IterationRangesRoot( + f"{prefix}index", + numels[prefix], + prefix, + index, + self, # type: ignore[arg-type] + pid_cache=pid_cache, + is_loop=is_reduction and not self.persistent_reduction, + tensor_dim=tensor_dim, + grid_dim=grid_dim, + has_zdim="z" in numels, + ) + ) + return range_trees + + def initialize_range_tree(self, pid_cache: dict[str, str]) -> None: + range_trees = self.construct_range_trees( + pid_cache, + self.inside_reduction, + self.features.is_reduction(), + self.numels, + self.no_x_dim, + ) + self.range_trees.extend(range_trees) + + def finalize_indexing(self, indices: Sequence[sympy.Expr]) -> None: + """ + Hook called right before codegen with every index that will be + used in the fused kernel. + """ + + def store_reduction(self, name: str, index: sympy.Expr, value: CSEVariable) -> None: + prior = self.inside_reduction + self.inside_reduction = False + try: + return self.store(name, index, value) + finally: + self.inside_reduction = prior + + def should_use_cooperative_reduction(self) -> bool: + return False # defined in subclass + + def should_use_persistent_reduction(self) -> bool: + return False # defined in subclass + + def var_ranges(self) -> dict[sympy.Symbol, sympy.Expr]: + return dict( + itertools.chain.from_iterable( + tree.var_ranges.items() for tree in self.range_trees + ) + ) + + def triton_tensor_ndim(self) -> int: + return sum(int(tree.tensor_dim is not None) for tree in self.range_trees) + + def indexing_size_str(self, i: int) -> str: + sizes = ["None"] * self.triton_tensor_ndim() + sizes[i] = ":" + return f"[{', '.join(sizes)}]" + + def dense_size_list(self) -> list[str]: + sizes = ["1"] * self.triton_tensor_ndim() + for tree in self.range_trees: + if tree.tensor_dim is None: + continue + + if not tree.is_reduction or self.inside_reduction: + sizes[tree.tensor_dim] = f"{tree.prefix.upper()}BLOCK" + return sizes + + def create_constant_mask(self, entry) -> str: + x = entry.prefix + if entry.tensor_dim is None: + sizestr = self.dense_size_str() + return f"{x}mask = tl.full({sizestr}, True, tl.int1)" + sizes = ["None"] * self.triton_tensor_ndim() + sizes[entry.tensor_dim] = ":" + suffix = ", ".join(sizes) + out = f"{x}mask = tl.full([{x.upper()}BLOCK], True, tl.int1)[{suffix}]" + return out + + def dense_size_str(self) -> str: + sizes = self.dense_size_list() + return f"[{', '.join(sizes)}]" + + def combine_modular_indexing_pairs(self, index: sympy.Expr) -> sympy.Expr: + if not isinstance(index, ModularIndexing): + return index + x = index.args[0] + if (tree_node := self.range_tree_nodes.get(x)) is None: + return index + new_index = sympy_subs(index, {x: tree_node.expr}) + new_index = V.graph.sizevars.combine_modular_indexing_pairs(new_index) + # the index now contains xindex/etc, which is nonstandard, fix it up + return sympy_subs( + new_index, + { + tree_node.root.index_sym(): tree_node.root.lookup( + sympy.S.One, tree_node.root.numel + ).symbol() + }, + ) + + def combine_contiguous_dims( + self, index: sympy.Expr, tree: IterationRangesRoot + ) -> sympy.Expr: + if expand_res := V.graph.sizevars.expand_floor_div(index): + new_index, denominator = expand_res # type: ignore[misc] + return FloorDiv(self._combine_contiguous_dims(new_index, tree), denominator) + else: + return self._combine_contiguous_dims(index, tree) + + def _combine_contiguous_dims( + self, index: sympy.Expr, tree: IterationRangesRoot + ) -> sympy.Expr: + """ + More aggressive simplification to merge contiguous dims + """ + if isinstance(index, (sympy.Integer, sympy.Symbol)): + return index + index_vars, sizes = tree.vars_and_sizes(index) + if len(sizes) <= 1: + return index + new_sizes, reindex, _prune = V.graph.sizevars._simplify_loops( + index_vars, sizes, index_prevent_reordering([index], index_vars, sizes) + ) + if new_sizes == sizes: + return index + new_index_vars = tree.construct(new_sizes) + new_index = sympy_subs(index, dict(zip(index_vars, reindex(new_index_vars)))) + return new_index + + def disable_reduction(self) -> contextlib.AbstractContextManager[None]: + should_flush = self.range_trees[-1].is_loop or self.cooperative_reduction + + @contextlib.contextmanager + def ctx(): + if not self.features.is_reduction(): + assert not self.inside_reduction + yield + return + if should_flush: + # calling codegen_body() will flush all the pending buffers + # and write out a reduction loop + self.codegen_body() + self.inside_reduction = False + try: + yield + if should_flush: + # flush out any code before opening the next loop + self.codegen_body() + finally: + self.inside_reduction = True + + return ctx() + + def set_ranges(self, *lengths: sympy.Expr) -> list[sympy.Symbol]: + assert len(lengths) == len(self.range_trees) + return [ + ranges.construct(length) + for length, ranges in zip(lengths, self.range_trees) + ] + + @staticmethod + def _split_iteration_ranges( + groups: Iterable[sympy.Expr], lengths: Sequence[Sequence[sympy.Expr]] + ) -> tuple[ + list[list[sympy.Expr]], list[list[Callable[[list[sympy.Expr]], sympy.Expr]]] + ]: + # Special case: if a node's sizes are ([], []), there's nothing to split. + if all(len(length) == 0 for length in lengths): + return [[] for group in groups], [] + + sv = V.graph.sizevars + new_ranges: list[list[sympy.Expr]] = [[] for _ in groups] + remaining = [sv.simplify(g) for g in groups] + var_count = itertools.count() + + def add_range(i: int, expr: sympy.Expr) -> int: + expr = sv.simplify(expr) + if not sv.statically_known_multiple_of(remaining[i], expr): + raise CantSplit(remaining[i], expr) + # guard on the last item out + remaining[i] = FloorDiv(remaining[i], expr) + new_ranges[i].append(expr) + return next(var_count) + + def make_combined( + sizes: list[sympy.Expr], idxs: list[int] + ) -> Callable[[list[sympy.Expr]], sympy.Expr]: + """ + Builds the nested expression: + ((...((s1*v[i1] + v[i2]) * s2 + v[i3]) ... ) * sk + v[i(k+1)]) + """ + assert len(idxs) == len(sizes) + 1 + + def getter(flat_vars: list[sympy.Expr]) -> sympy.Expr: + expr = flat_vars[idxs[0]] + for s, idx in zip(sizes, idxs[1:]): + expr = s * expr + flat_vars[idx] + return expr + + return getter + + return_getters_groups = [] + current_group = 0 + for length_group in lengths: + return_getters = [] + for size in length_group: + if sv.statically_known_equals(size, 1): # type: ignore[arg-type] + return_getters.append(lambda _: sympy.S.Zero) + continue + + while current_group < len(remaining) and sv.statically_known_equals( + remaining[current_group], + 1, # type: ignore[arg-type] + ): + # scroll to next group with remaining elements + current_group += 1 + + # During native matmul on bmm, we enforce tiling order (z, y, x, r). + # When fusing a bmm node with loop (z, y, x, r) with a pw node + # of shape (z*y*x, 1), we need to split the pw iteration range + # into three dimensions. + # The group becomes [z, y, x, 1], with lengths ([z*y*x], []). + # In this case, we decompose the combined size z*y*x into three + # consecutive groups. Previously, _split_iteration_ranges supported + # splitting into at most two dimensions, but we now extend it to do + # three splits when the total size is divisible by all three. + + # is group having (z,y,x,r=1) form? + is_bmm_then_pw = len(remaining) == 4 and remaining[-1] == 1 + if ( + current_group + 2 < len(remaining) + and sv.statically_known_gt( + size, remaining[current_group] * remaining[current_group + 1] + ) + and is_bmm_then_pw + ): + # need to break size in three + if not sv.statically_known_multiple_of( + size, remaining[current_group] * remaining[current_group + 1] + ): + raise CantSplit( + size, + remaining[current_group] * remaining[current_group + 1], + ) + + size1 = remaining[current_group] + size2 = remaining[current_group + 1] + size3 = FloorDiv(size, size1 * size2) + return_getters.append( + # pyrefly: ignore [bad-argument-type] + make_combined( + [size2, size3], + [ + add_range(current_group, size1), + add_range(current_group + 1, size2), + add_range(current_group + 2, size3), + ], + ) + ) + + # Two-dimensional tiling: split size across current_group and next group. + elif current_group + 1 < len(remaining) and ( + sv.statically_known_gt(size, remaining[current_group]) + or + # statically_known_gt(size, remaining) may return False for symbolic + # expressions like 64*u0 vs u0, because both could be 0. Similarly for + # backed expressions like s25*(((s70 - 5)//4)) - s25 and + # (s25*(((s70 - 5)//4)) - s25)*64. + # We want to assume tensor sizes are not 0 and pass the gt + # using the following logic. + # + # if A//B = C and C >= 1 + # then A = B * C + R + # and assuming A!=0 + # A must be > B . + # + sv.statically_known_gt(FloorDiv(size, remaining[current_group]), 1) + ): + # need to break size in two + if not sv.statically_known_multiple_of( + size, remaining[current_group] + ): + raise CantSplit(size, remaining[current_group]) + + size1 = remaining[current_group] + size2 = FloorDiv(size, remaining[current_group]) + return_getters.append( + # pyrefly: ignore [bad-argument-type] + make_combined( + [size2], + [ + add_range(current_group, size1), + add_range(current_group + 1, size2), + ], + ) + ) + else: + if current_group >= len(remaining): + raise CantSplit(size, 0) + return_getters.append( + # pyrefly: ignore [bad-argument-type] + operator.itemgetter(add_range(current_group, size)) + ) + return_getters_groups.append(return_getters) + + assert all( + V.graph.sizevars.guarding_hint_or_throw(s) == 1 for s in remaining + ), f"failed to set ranges {remaining} {lengths}" + # pyrefly: ignore [bad-return] + return new_ranges, return_getters_groups + + @classmethod + def prepare_split_iteration_lengths( + cls, + groups: Iterable[sympy.Expr], + lengths: Sequence[Sequence[sympy.Expr]], + reduction_numel: sympy.Expr = sympy.S.One, + ) -> Sequence[Sequence[sympy.Expr]]: + "Fill in the reduction numel of lengths if missing" + sizevars = V.graph.sizevars + if len(lengths[1]) == 0 and ( + not sizevars.statically_known_equals(reduction_numel, sympy.S.One) + and sizevars.statically_known_equals( + sympy_product(groups), + sympy_product(lengths[0]) * reduction_numel, + ) + ): + return (lengths[0], [reduction_numel]) + + return lengths + + @classmethod + def is_compatible( + cls, + groups: Iterable[sympy.Expr], + lengths: Sequence[Sequence[sympy.Expr]], + reduction_numel: sympy.Expr = sympy.S.One, + ) -> bool: + lengths = cls.prepare_split_iteration_lengths(groups, lengths, reduction_numel) + + try: + cls._split_iteration_ranges(groups, lengths) + return True + except CantSplit: + return False + + def split_and_set_ranges( + self, lengths: Sequence[Sequence[sympy.Expr]] + ) -> list[list[sympy.Expr]]: + """ + Split and set iteration ranges for the kernel based on the provided lengths. + + This method maps the kernel's tiling structure to the node's iteration space, + handling both pointwise and reduction dimensions appropriately. + + Args: + lengths: A sequence of sequences of symbolic expressions representing + the sizes of different dimensions for each node. + + Returns: + A list of lists of symbolic expressions representing the mapped + iteration variables for each dimension. + """ + # Create a dictionary mapping each range tree prefix to its total number of elements + tiling = {rt.prefix: rt.numel for rt in self.range_trees} + + # If we're not inside a reduction loop, set all reduction dimensions to 1 + # This effectively disables reduction dimensions when not needed + if not self.inside_reduction: + for prefix in tiling: + if prefix_is_reduction(prefix): + tiling[prefix] = sympy.S.One + + # Extract the values from the tiling dictionary to create groups + groups = [*tiling.values()] + + # Map the kernel's group structure to the node's sizes and set the ranges + # using the set_ranges method, returning the resulting iteration variables + return self.map_kernel_groups_to_node_sizes(groups, lengths, self.set_ranges) + + @classmethod + def map_kernel_groups_to_node_sizes( + cls, + groups: Sequence[sympy.Expr], + lengths: Sequence[Sequence[sympy.Expr]], + set_ranges, + ) -> list[list[sympy.Expr]]: + """ + We may want to fuse `for i0 in s0*s1` into a tiled kernel with groups (s0, s1). + + To do this we need to split up the iteration space of i0 into something like: + for i1 in s0: + for i2 in s1: + i0 = i1*s1 + i2 + .... + + This function matches and resplits lengths to the groups of + this kernel to enable tiled + non-tiled fusions. + """ + if len(lengths) == len(groups) and all( + V.graph.sizevars.simplify(sympy_product(x) - g) == 0 + for x, g in zip(lengths, groups) + ): + return set_ranges(*lengths) + + new_ranges, return_getters_groups = cls._split_iteration_ranges(groups, lengths) + itervars = [*itertools.chain.from_iterable(set_ranges(*new_ranges))] + return [[fn(itervars) for fn in fns] for fns in return_getters_groups] + + def is_indirect_indexing(self, index: sympy.Expr) -> bool: + # tmpX means indirect indexing + return free_symbol_is_type(index, SymT.TMP) + + def is_broadcasted(self, index: sympy.Expr) -> bool: + # Note. This may not be correct when there is indirect indexing + if self.is_indirect_indexing(index): + return False + + index_numels = [1] * len(self.numels) + for symbol in index.free_symbols: + if symbol not in self.range_tree_nodes: + # Non-iterated variables, e.g. strides + continue + entry = self.range_tree_nodes[symbol] # type: ignore[index] + assert isinstance(entry.parent, IterationRangesRoot) + index_numels[entry.parent.index] *= entry.length + + # If the index variables only iterate over a subset of the kernel + # numels, then it must be broadcasted. + simplify = V.graph.sizevars.simplify + return any( + simplify(idx_range) != simplify(iter_range) # type: ignore[arg-type] + for idx_range, iter_range in zip(index_numels, self.numels.values()) + ) + + def index_to_str(self, index: sympy.Expr) -> str: + """ + Convert an index expr to a string that can be used in output code. + e.g. a sympy expression "s2" may actually appear as "ks1" in the generated kernel. + + Index expressions often need to be passed in as arguments to the triton kernel. + Rename_indexing and codegen_indexing keep track of the needed indices and add + new parameters to the function signature. + """ + if isinstance(index, list): + return f"[{', '.join(map(self.index_to_str, index))}]" + return self.kexpr(self.rename_indexing(index)) # type: ignore[call-arg] + + def prepare_indexing( + self, + index: sympy.Expr, + ) -> sympy.Expr: + index = self.simplify_indexing(index) + index = sympy_subs(index, V.graph.sizevars.precomputed_replacements) + # if simple replacements didn't get rid of floor/ceil, try full subs + if len(index.atoms(sympy.floor)) or len(index.atoms(sympy.ceiling)): + index = index.subs(V.graph.sizevars.precomputed_replacements) + # last resort, if no range vars are in the expr, hoist it + # TODO instead of trying to blindly find complicated exprs, we should hoist the + # inputs/outputs sizes and strides, but at the time indexing is generated + # kernel inputs and outputs are not set yet, we'd need a deeper refactor + # to do it this way + + if len(index.atoms(sympy.ceiling)): + for a in index.atoms(sympy.ceiling): + # for nested exprs, atoms yields top level first (?) + # so if everything goes fine, lower level replacements will come up empty + symbols = a.free_symbols + if len(symbols) > 0 and all( + symbol_is_type(s, (SymT.SIZE, SymT.PRECOMPUTED_SIZE)) + for s in symbols + ): + replacements = {a: V.graph.sizevars.lookup_precomputed_size(a)} + index = sympy_subs(index, replacements) + + simp_index = self.simplify_indexing(index) + + # Now that we are done simplifying we can unwrap Identity so that downstream handling + # for its contained expression will work. previously, tl.full wrapping of sympy.Integer + # would not occur + simp_index = ( + simp_index if not isinstance(simp_index, Identity) else simp_index.args[0] + ) + + return self.codegen_indexing(simp_index) + + def active_range_trees(self) -> list[IterationRangesRoot]: + return [ + t for t in self.range_trees if not t.is_reduction or self.inside_reduction + ] + + def codegen_indexing(self, expr: sympy.Expr) -> sympy.Expr: + expr = V.graph.sizevars.simplify_with_ranges(expr, self.var_ranges()) + for sym in sorted(expr.free_symbols, key=str): + if sym in self.range_tree_nodes: + # if indexing expression is complicated, we precompute it on the host side + # and send the result as a kernel argument + replacements = {} + for ps in self.range_tree_nodes[sym].precomputed_args(): # type: ignore[index] + replacements[ps] = V.graph.sizevars.lookup_precomputed_size(ps) + if len(replacements) > 0: + self.range_tree_nodes[sym].expr = sympy_subs( # type: ignore[index] + self.range_tree_nodes[sym].expr, + replacements, # type: ignore[index] + ) + self.range_tree_nodes[sym].codegen() # type: ignore[index] + return expr + + def codegen_nan_check(self) -> None: + raise NotImplementedError("NYI: codegen_nan_check") + + def deallocate_workspaces(self): + wrapper = V.graph.wrapper_code + for ws in reversed(self.args.workspace_args): + wrapper.generate_workspace_deallocation(ws) + + def call_kernel( + self, name: str, node: IRNode | None = None, deallocate_ws: bool = True + ) -> None: + raise NotImplementedError("NYI: call_kernel") + + @contextlib.contextmanager + def mask_loads(self, mask: str | OpsWrapper, value: int | float) -> Iterator[str]: + """Context manager to add an additional mask to tl.load/store""" + prior = self._load_mask + prior_val = self._load_other + if prior: + mask = ops.logical_and(mask, prior) + + mask = OpsWrapper._unwrap(mask) + self._load_mask = mask + self._load_other = value + try: + # TODO(jansel): do we need a reshape here? + yield mask + finally: + self._load_mask = prior + self._load_other = prior_val + + def get_strides_of_load(self, index: sympy.Expr) -> dict[sympy.Symbol, sympy.Expr]: + """ + This gets the stride of the index for each of the tiling variables + (technically, it does it at index 0) + + For example, if + xindex = x0 + 512*x1 + 1024*r0 + x0 = (xindex//512) + x1 = (xindex % 512) + r0 = rindex // 1024 + + this function would return + {xindex: 512, rindex: 1024} + """ + index_to_tile_indexes = {k: v.expr for k, v in self.range_tree_nodes.items()} + index_in_tile_vars = sympy_subs(index, index_to_tile_indexes) # type: ignore[arg-type] + strides = {} + for range_tree in self.range_trees: + s = sympy_index_symbol(range_tree.name) + strides[s] = sympy_subs(index_in_tile_vars, {s: 1}) - sympy_subs( + index_in_tile_vars, {s: 0} + ) + return strides + + @staticmethod + def _map_tuple_or_scalar(fn, value): + if isinstance(value, tuple): + return tuple(map(fn, value)) + return fn(value) + + def estimate_flops(self) -> int | None: + flops = [ + node.estimate_flops() + for node in NodeScheduleMarker.only_nodes(self.features.node_schedule) + ] + return sum(filter(None, flops)) + + def estimate_kernel_num_bytes(self): + """ + Try the best to estimate the total size (in bytes) of the + kernel's inputs and outputs, which is used for estimating the memory + throughput of this kernel. This information is used for checking how + far we are from the peak memory bandwidth. It's important that + we want to avoid overestimating the sizes of the inputs and outputs, + because it can wrongfully give us a very large memory traffic value, + which may be even larger than the theoretical bandwidth and thus + become very misleading. This is particularly problematic for cases + where we slice some inputs. In those cases, we should only count + the size of the "slices" instead of the original inputs, because + only the slices contribute to the real memory traffic. + """ + nbytes = [] + ninplace_args = len(unique(self.args.inplace_buffers.values())) + _, call_args, _, _ = self.args.python_argdefs() + buf_accesses = self.features.buf_accesses() + + # For pointwise and reduction kernels, this is the upper-bound numels + # for the output buffer. + # FIXME: This is not exactly right for cases like below: + # def foo(tensor0, tensor1): + # x0 = narrow(tensor0) + # return cat(x0, tensor1) + # For this example, we will end up overestimate the size for the + # slice s0. Potentially, we could have precise inputs information + # if we maintained the original inputs of the Pointwise kernel created + # for the "cat". However, I think it might be a bit overwhelming that + # we add such complexity only for handling some particular cases for + # benchmarking. + out_numel = V.graph.sizevars.optimization_hint( + sympy_product(self.numels.values()) + ) + for i, arg in enumerate(call_args): + # "buf" may be narrowed. In this case, the number of memory accesses + # should be estimated based on the reinterpreted layout. + # On the other hand, buf may be broadcasted. In this case, + # counting the size of the underline storage would give us + # a better estimation in terms of memory accesses. + if arg not in buf_accesses: + nbytes.append(0) + continue + arg_numel = V.graph.get_numel(arg) + buf_size = V.graph.sizevars.optimization_hint(arg_numel) + if buf_size > out_numel: + # This arg points to a buf that has been sliced. + # We need to count each individual slice to have + # a better estimation. + indices = OrderedSet[Any]() + no_index_dep_count = 0 + for dep in buf_accesses[arg]: + if isinstance(dep, (StarDep, WeakDep)): + indices.add(f"no_index_dep_{no_index_dep_count}") + no_index_dep_count += 1 + else: + indices.add(dep.index) + numel = len(indices) * out_numel + else: + numel = buf_size + dtype = V.graph.get_dtype(arg) + dtype_size = get_dtype_size(dtype) + # pyrefly: ignore [bad-argument-type] + nbytes.append(numel * dtype_size * (1 + int(i < ninplace_args))) + return sum(nbytes) + + def warn_mix_layout(self, kernel_name): + """ + Print message if the kernel have mixed layout inputs. + Only care about 4D tensor for now. + """ + if ( + len(self.args.input_buffers) == 1 + and len(self.args.output_buffers) == 1 + and len(self.args.inplace_buffers) == 0 + ): + # even if input buffer and output buffer have different layout, + # this can be a layout conversion kernel. No need to warn for + # the mix layouts. + return + + argdefs, call_args, _signature, _ = self.args.python_argdefs() + uniform_stride_order = None + # pyrefly: ignore [bad-assignment] + for arg_name in call_args: + buf = V.graph.try_get_buffer(arg_name) + if not buf: + continue + layout = buf.get_layout() + if len(layout.size) == 4: + # ignore the tensor if only 1 dimension is non-zero + if len([x for x in layout.size if x == 1]) == 3: + continue + stride_order = ir.get_stride_order(layout.stride) + if uniform_stride_order is None: + uniform_stride_order = stride_order + elif uniform_stride_order != stride_order: + msg = yellow_text( + f"Expected stride order {uniform_stride_order}, but found stride order" + + f" {stride_order} for kernel {kernel_name}" + ) + log.warning(msg) + + stride_order_list = [ + ir.get_stride_order( + V.graph.get_buffer(name).get_layout().stride + ) + if V.graph.try_get_buffer(name) + else None + for name in call_args + ] + size_list = [ + V.graph.get_buffer(name).get_layout().size + if V.graph.try_get_buffer(name) + else None + for name in call_args + ] + source_list = [ + "GraphInput" + if name in V.graph.graph_inputs + else "IntermediateBuffer" + if name in V.graph.name_to_buffer + else None + for name in call_args + ] + + argdef_names = [x.name for x in argdefs] + msg = yellow_text( + f" param names {argdef_names}\n buf names {call_args}\n strides {stride_order_list}" + + f"\n sizes {size_list}\n sources {source_list}\n" + ) + log.warning(msg) + return + msg = green_text( + f"All the inputs for the triton kernel {kernel_name} have uniform layout" + ) + log.warning(msg) + + def welford_reduce_fallback(self, dtype, value): + sum_ = ops.reduction(dtype, dtype, "sum", value) + self.inside_reduction = False + rnumel = ops.index_expr(self.features.reduction_numel, dtype) + mean = ops.truediv(sum_, rnumel) + + self.inside_reduction = True + dx = ops.sub(value, mean) + dx2 = ops.mul(dx, dx) + m2 = ops.reduction(dtype, dtype, "sum", dx2) + return OpsWrapper._unwrap((mean, m2, rnumel)) + + def prepare_softmax_twopass_fallback(self, dtype, value): + vmax = ops.reduction(dtype, dtype, "max", value) + sub = ops.sub(value, vmax) + exp = ops.exp(sub) + vsum = ops.reduction(dtype, dtype, "sum", exp) + return OpsWrapper._unwrap((vmax, vsum)) + + def codegen_kernel(self): + raise NotImplementedError + + def codegen_body(self): + pass + + def codegen_iteration_ranges_entry(self, entry: IterationRangesEntry): + pass + + +class SIMDScheduling(BaseScheduling): + """ + Single Instruction Multiple Data parent class used for fusion across + multiple different backends. + """ + + kernel_type: type[Any] = SIMDKernel # override in subclass + + def group_fn(self, sizes): + return tuple(V.graph.sizevars.simplify(sympy_product(s)) for s in sizes) + + def can_fuse(self, node1, node2): + """ + Hook called by Scheduler to determine if the Triton backend + can fuse node1 and node2. These nodes might already be + FusedSchedulerNodes. + """ + if isinstance(node1, scheduler.ForeachKernelSchedulerNode) or isinstance( + node2, scheduler.ForeachKernelSchedulerNode + ): + return scheduler.ForeachKernelSchedulerNode.can_fuse(node1, node2) + + _, (numel1, rnumel1) = node1.group + _, (numel2, rnumel2) = node2.group + why = WhyNoFuse(node1, node2) + + if node1.is_split_scan() and not node2.is_split_scan(): + if node2.is_reduction(): + why("Split scan cannot fuse with reductions") + elif node2.is_split_scan() and not node1.is_split_scan(): + if node1.is_reduction(): + why("Split scan cannot fuse with reductions") + + if node1.is_reduction() and node2.is_reduction(): + reduction_can_fuse = numel1 == numel2 and rnumel1 == rnumel2 + if not reduction_can_fuse: + from torch._inductor.scheduler import MixOrderReduction + + reduction_can_fuse = MixOrderReduction.can_fuse(node1, node2) + + if not reduction_can_fuse: + why( + "numel/rnumel mismatch (reduce) (%s, %s), (%s, %s)", + numel1, + numel2, + rnumel1, + rnumel2, + ) + + if reduction_can_fuse and ( + node1.is_native_matmul() or node2.is_native_matmul() + ): + # Ensure node1 is always the native matmul side + if not node1.is_native_matmul(): + node1, node2 = node2, node1 + + # 1. A native matmul node keeps its original loop order. + # For example: C[z,y,x] = torch.bmm(A[z,y,r], B[z,r,x]) keeps (z,y,x) order. + # (see simplify_and_reorder in ir.py) + # + # 2. Triton kernels with native matmul always tile loops as (z,y,x) + # (see get_tiling_and_scores in this file) + # + # 3. If a candidate node (node2) uses a different loop order (e.g., (z,x,y,r)), + # its tiling is incompatible with native matmul tiling (z,y,x,r). + # This means _split_iteration_ranges will fail, so these nodes should not be fused. + tiling = self.select_tiling(node1.get_nodes(), numel1, rnumel1) + if not all( + SIMDKernel.is_compatible( + tiling.values(), n2.get_ranges(), reduction_numel=rnumel1 + ) + for n2 in node2.get_nodes() + ): + why("invalid loop order and tiling for native matmul") + return False + + return reduction_can_fuse + + if not node1.is_reduction() and not node2.is_reduction(): + if not (numel1 == numel2 and rnumel1 == rnumel2): + if not node2.is_template(): + why( + "numel/rnumel mismatch (non-reduce) (%s, %s), (%s, %s)", + numel1, + numel2, + rnumel1, + rnumel2, + ) + return False + else: + # prologue fusion input sizes differ from output group + # fuse so long as this node matches the group of existing prologue nodes + for node in node2.get_nodes(): + # dont need to check epilogue nodes for prologue fusion, break after template + if node.is_template(): + break + # we would have already restricted prologue from fusing if it had multiple + # uses, so it must be fusing into this node + if not node.used_buffer_names() & node1.get_buffer_names(): + continue + _, (pro_numel, pro_rnumel) = node.group + if not (numel1 == pro_numel and rnumel1 == pro_rnumel): + why( + "numel/rnumel mismatch prologue mismatch (%s, %s), (%s, %s)", + numel1, + pro_numel, + rnumel1, + pro_rnumel, + ) + return False + + for n in (node1, node2): + if n.is_template(): + return True + + # check for a bad combined tiling + tiling1 = self.select_tiling(node1.get_nodes(), numel1, rnumel1) + tiling2 = self.select_tiling(node2.get_nodes(), numel1, rnumel1) + tiling3 = self.select_tiling( + node1.get_nodes() + node2.get_nodes(), numel1, rnumel1 + ) + if config.triton.tiling_prevents_pointwise_fusion: + cond = True + if len(tiling1) > 2: + if len(tiling2) > 2: + cond = tiling1 == tiling2 == tiling3 + else: + cond = tiling1 == tiling3 + elif len(tiling2) > 2: + cond = tiling2 == tiling3 + if not cond: + why( + "tiling mismatch (%s, %s, %s)", + tiling1, + tiling2, + tiling3, + ) + return False + + return True + + if not node1.is_reduction() and node2.is_reduction(): + assert rnumel1 == 1 and rnumel2 != 1 + if numel1 == numel2 * rnumel2: + if not all( + SIMDKernel.is_compatible((numel2, rnumel2), n.get_ranges()) + for n in node1.get_nodes() + ): + why("nodes numel/rnumel incompatibility") + return False + if ( + config.triton.tiling_prevents_reduction_fusion + and not node1.is_template() + ): + is_reduction_tiling_valid = tuple( + self.select_tiling(node1.get_nodes(), numel1).values() + ) in ( + (numel1, 1), + (numel2, rnumel2, 1), + ) + if not is_reduction_tiling_valid: + why("invalid tiling for reduction") + return is_reduction_tiling_valid + return True + + if numel1 != numel2: + why("nodes numel incompatibility") + return numel1 == numel2 + + assert node1.is_reduction() and not node2.is_reduction() + # swap args to hit the case above + return self.can_fuse_horizontal(node2, node1) + + can_fuse_vertical = can_fuse + can_fuse_horizontal = can_fuse + + def generate_node_schedule(self, nodes, numel, rnumel): + node_schedule: list[Any] = [] + done = OrderedSet[scheduler.BaseSchedulerNode]() + # Writes with a reduced shape, meaning they are only present once the + # reduction loop has ended + not_ready_yet_nodes: OrderedSet[str] = OrderedSet() + current_loop_buffer_usage: OrderedSet[str] = OrderedSet() + maybe_split_index: int | None = None + + def fits_in_main_body(n): + _, (node_numel, node_rnumel) = n.group + return (node_numel == numel and node_rnumel == rnumel) or ( + node_numel == numel * rnumel and node_rnumel == 1 + ) + + def fits_outside_reduction(n): + _, (node_numel, node_rnumel) = n.group + return node_numel == numel and node_rnumel == 1 and rnumel != 1 + + def expect_improved_memory_usage(n): + for read in n.read_writes.reads: + if read.name in current_loop_buffer_usage: + return True + return False + + def schedule_node_in_loop(n): + done.add(n) + node_schedule.append(n) + current_loop_buffer_usage.update([x.name for x in n.read_writes.reads]) + + # A scan is modelled as a reduction in the scheduler but has a + # full sized output that can be used inside the loop body + if ( + n.is_reduction() + and isinstance(n, scheduler.SchedulerNode) + and isinstance(n.node, ir.ComputedBuffer) + and not isinstance(n.node.data, ir.Scan) + ): + not_ready_yet_nodes.add(n.get_name()) + else: # this node is available within the loop + current_loop_buffer_usage.update([x.name for x in n.read_writes.writes]) + + @contextlib.contextmanager + def end_current_reduction_loop(): + nonlocal maybe_split_index + if node_schedule and node_schedule[-1] is EnableReduction: + node_schedule.pop() + else: + node_schedule.append(DisableReduction) + if maybe_split_index: + node_schedule.insert(maybe_split_index, DisableReduction) + node_schedule.insert(maybe_split_index + 1, EnableReduction) + maybe_split_index = None + yield + node_schedule.append(EnableReduction) + not_ready_yet_nodes.clear() + current_loop_buffer_usage.clear() + + def requires_closing_previous_reduction(node, node_schedule): + if rnumel == 1: + return False + if not not_ready_yet_nodes & node.ancestors: + return False + assert node_schedule and not isinstance( + node_schedule[-1], (EnableReduction, DisableReduction) + ) + return bool(not_ready_yet_nodes) + + for node in nodes: + if node in done: + continue + done.add(node) + + if fits_in_main_body(node): + if requires_closing_previous_reduction(node, node_schedule): + with end_current_reduction_loop(): + pass # need to start a new reduction loop + + if current_loop_buffer_usage and not expect_improved_memory_usage(node): + # If we don't improve memory usage, then it is better to split into two loops + maybe_split_index = maybe_split_index or len(node_schedule) + else: + # Memory usage got improved, cancel the loop split + maybe_split_index = None + + schedule_node_in_loop(node) + elif fits_outside_reduction(node): + with end_current_reduction_loop(): + node_schedule.append(node) + else: + raise NotImplementedError( + f"unexpected group: ({numel}, {rnumel}) != {node.group[1]}" + ) + + return node_schedule + + def codegen_mix_order_reduction(self, node): + node1, node2 = node.node1, node.node2 + + # Make sure there are no producer/consumer relationship + assert not (node1.ancestors & node2.get_operation_names()) and not ( + node2.ancestors & node1.get_operation_names() + ) + + self._codegen_mix_order_reduction(node1, node2) + + def _split_mix_order_reduction_epilogue(self, node): + # TODO: do more validation here + nodes = node.get_nodes() + reductions = [] + epilogues = [] + for node in nodes: + if node.is_reduction(): + reductions.append(node) + else: + epilogues.append(node) + return reductions, epilogues + + def _generate_kernel_code_for_mix_order_reduction( + self, kernel_features, split_size, for_benchmark + ): + """ + for_benchmark: + True if the generated code is for benchmarking. We need make + sure benchmark harness code is generated. + """ + numel, rnumel = kernel_features.numel, kernel_features.reduction_numel + node_schedule = kernel_features.node_schedule + + kernel = self.create_kernel_choices( + kernel_features, + [{"x": numel, "r0_": rnumel}], + { + "features": kernel_features, + "tiling_scores": None, + "mix_order_reduction": True, + "override_persistent_reduction": True, + }, + )[0] + assert kernel.persistent_reduction + assert kernel.mix_order_reduction + kernel.rsplit_size = split_size + self.codegen_node_schedule_with_kernel(node_schedule, kernel) + + # allocate workspace for this kernel + _, ws_name, ws_off = kernel.args.workspace( + len(kernel.saved_partial_accumulate) + * kernel.numels["r0_"] + * ((kernel.numels["x"] + kernel.rsplit_size - 1) // kernel.rsplit_size), + False, + dtype=torch.float, + ) + assert ws_off == 0, f"{ws_off=}" + with kernel: + kernel.codegen_body() + + stack = contextlib.ExitStack() + with V.set_kernel_handler(kernel), stack: + if for_benchmark: + stack.enter_context(config.patch(benchmark_kernel=True)) + src_code = kernel.codegen_kernel() + + if for_benchmark: + # only do this if we are doing benchmarking. + # When we are generating final code, the kernel name + # should be decided differently with node type, fx node name + # etc. + src_code = src_code.replace(str(Placeholder.KERNEL_NAME), "triton_") + return kernel, ws_name, src_code + + # pyrefly: ignore [bad-override] + def benchmark_codegened_module( + self, mod, n_spills_threshold=8, node_names: OrderedSet[str] | None = None + ) -> tuple[float, str]: + raise NotImplementedError + + def _codegen_mix_order_reduction(self, node1, node2): + numel, rnumel = scheduler.MixOrderReduction.get_numel_rnumel(node1) + + def _pick_split_size(): + # the overridden has highest priority + if config.triton.mix_order_reduction_split_size is not None: + return config.triton.mix_order_reduction_split_size + + # heuristics based on number of SMs + device_prop = DeviceProperties.create(node1.get_device()) + num_sm = device_prop.multi_processor_count + estimated_num_splits = num_sm * 8 + + # split_size is decided based on hint. + # optimization_hint is fine here: the result is clamped to [16, 128], + # so any fallback value still produces a valid split size. + numel_hint = V.graph.sizevars.optimization_hint(numel) + split_size = max(last_power_of_2(numel_hint // estimated_num_splits), 16) + split_size = min(split_size, 128) + return split_size + + split_size = _pick_split_size() + + # pyrefly: ignore [bad-assignment] + metrics.codegen_mix_order_reduction += 1 + + # split epilogue out of node2 + node2_reductions, node2_epilogue = self._split_mix_order_reduction_epilogue( + node2 + ) + + converted_nodes = [] + for subnode in node2_reductions: + subnode.cancel_reduction_split() + converted = subnode.extract_pw_from_reduction() + converted.swap_pw_red_dimension() + converted_nodes.append(converted) + node_schedule = self.generate_node_schedule( + node1.get_nodes() + converted_nodes, numel, rnumel + ) + kernel_features = SIMDKernelFeatures(node_schedule, numel, rnumel) + + # The autotuning is skipped in deterministic mode + if ( + not torch._inductor.config.deterministic + and config.triton.mix_order_reduction_split_size is None + and ( + config.triton.mix_order_reduction_autotune_split_size + or config.max_autotune + or config.coordinate_descent_tuning + ) + ): + + def _bench(candidate_split_size): + _, _, src_code = self._generate_kernel_code_for_mix_order_reduction( + kernel_features, + split_size=candidate_split_size, + for_benchmark=True, + ) + mod = PyCodeCache.load(src_code) + ms, _ = self.benchmark_codegened_module(mod) + return ms + + split_size = CoordescTuner.autotune_single_field( + _bench, + split_size, + 8, + ) + + kernel, ws_name, src_code = self._generate_kernel_code_for_mix_order_reduction( + kernel_features, + split_size=split_size, + for_benchmark=False, + ) + + # rename intermediate reduction output to final reduction + # output + is_split_reduction = bool(node2_reductions[0].node._split_size) + rename = {} + if is_split_reduction: + for subnode in node2_reductions: + bufname = subnode.get_outputs()[0].node.get_name() + username = ( + subnode.get_outputs()[0] + .users[0] + .node.get_outputs()[0] + .node.get_name() + ) + rename[bufname] = username + assert self.scheduler + self.scheduler.removed_ops.add( + subnode.get_outputs()[0].users[0].node.get_name() + ) + V.graph.removed_buffers.add(bufname) + + for partial_accum in kernel.saved_partial_accumulate: + partial_accum.buffer_name = rename.get( + partial_accum.buffer_name, partial_accum.buffer_name + ) + + kernel_name = self.define_kernel(src_code, node_schedule, kernel) + kernel.kernel_name = kernel_name + kernel.code_hash = code_hash(src_code) + + with V.set_kernel_handler(kernel): + for node in kernel_features.scheduler_nodes(): + # No need to allocate buffer for split reduction + # since we are gonna to allocate workspace to store the + # intermediate reduction reduction + if node.get_outputs()[0].node.get_name() not in rename: + node.mark_run() + + V.graph.wrapper_code.make_comment("# Call mix order reduction kernel") + self.codegen_comment(node_schedule, None) + # workspace args is still needed after the call + kernel.call_kernel(kernel.kernel_name, deallocate_ws=False) + V.graph.removed_buffers |= kernel.removed_buffers + V.graph.inplaced_to_remove |= kernel.inplaced_to_remove + + # a extra round of reduction + assert len(converted_nodes) == len(kernel.saved_partial_accumulate) + nsplit = V.graph.wrapper_code.codegen_python_sizevar( + (numel + split_size - 1) // split_size + ) + for idx, partial_accum in enumerate(kernel.saved_partial_accumulate): + buffer_name = partial_accum.buffer_name + + stride_str = f"({nsplit}) * ({rnumel})" + start = f"{idx} * {stride_str}" + end = f"({idx} + 1) * {stride_str}" + reduction_type2op = { + "min": "amin", + "max": "amax", + } + opname = reduction_type2op.get( + partial_accum.reduction_type, partial_accum.reduction_type + ) + + # Check if the original reduction used keepdim=True by comparing dimensions. + # Without keepdim, reduction produces [rnumel]; with keepdim, [1, rnumel]. + buffer = V.graph.get_buffer(buffer_name) + keepdim = buffer is not None and len(buffer.get_layout().size) > 1 + + final_reduce = f"{buffer_name} = {ws_name}[{start} : {end}].view({nsplit}, {rnumel}).{opname}(dim=0, keepdim={keepdim})" + + # The workspace tensor is in torch.float, need a cast if the buffer is + # not. + if (buffer_dtype := V.graph.get_dtype(buffer_name)) != torch.float: + final_reduce += f".to({buffer_dtype})" + V.graph.wrapper_code.writeline(final_reduce) + # mark the buffer as allocated, so we don't try to allocate + # it again when it's later used + V.graph.wrapper_code.allocated.add(buffer_name) + + kernel.deallocate_workspaces() + + if node2_epilogue: + self._codegen_nodes(node2_epilogue) + + self.free_buffers_in_scheduler() + + def _codegen_nodes( + self, + nodes: Sequence[scheduler.SchedulerNode], + coalesce_analysis: CoalesceVarAnalysis | None = None, + ): + assert self.scheduler + nodes = [ + node for node in nodes if node.get_name() not in self.scheduler.removed_ops + ] + if not nodes: + return + _, (numel, rnumel) = max(nodes, key=lambda x: int(x.is_reduction())).group + + node_schedule = self.generate_node_schedule(nodes, numel, rnumel) + schedule_log.debug("Schedule:\n %s", node_schedule) + + return self.codegen_node_schedule( + SIMDKernelFeatures(node_schedule, numel, rnumel, coalesce_analysis) + ) + + def codegen_node( + self, node: scheduler.FusedSchedulerNode | scheduler.SchedulerNode + ): + """ + Given a set of pre-fused nodes, generate a Triton kernel. + """ + assert self.scheduler + nodes = [ + node + for node in node.get_nodes() + if node.get_name() not in self.scheduler.removed_ops + ] + if len(nodes) == 0: + return + + if torch._inductor.config.triton.coalesce_tiling_analysis: + if len(nodes) != len(node.get_nodes()): + assert self.scheduler + node = scheduler.FusedSchedulerNode(self.scheduler, nodes) + coalesce_analysis = analyze_memory_coalescing(node) + else: + coalesce_analysis = None + + return self._codegen_nodes(nodes, coalesce_analysis) # type: ignore[arg-type] + + @staticmethod + def can_use_32bit_indexing( + numel: sympy.Expr, + buffers: Iterable[ir.Buffer | ir.TensorBox | ir.TorchBindObject | ir.IRNode], + ) -> bool: + int_max = torch.iinfo(torch.int32).max + + if not expr_fits_within_32bit(numel): + return False + + # Any use of a MultiOutputLayout will create a buffer with a + # Layout whose sizes are accounted for + buf_sizes = [ + buf.get_layout().storage_size() + for buf in buffers + if buf.has_tensor_output() + ] + + for buf in buffers: + if not buf.has_tensor_output() and isinstance(buf, ir.MutationOutput): + mutated_bufs = buf.get_mutation_buffers() + buf_sizes += [ + buf.get_layout().storage_size() + for buf in mutated_bufs + if buf.has_tensor_output() + ] + + if not all(expr_fits_within_32bit(size) for size in buf_sizes): + return False + + # Only install guards for 32-bit indexing as there is no correctness + # issue with using 64-bit for everything + V.graph.sizevars.check_leq(numel, int_max) # type: ignore[arg-type] + for size in buf_sizes: + V.graph.sizevars.check_leq(size, int_max) # type: ignore[arg-type] + return True + + def process_kernel( + self, + kernel: Any, + node_schedule: list[NodeScheduleEntry], + only_gen_src_code: bool = False, + ) -> None: + """ + Process a kernel by generating code for its node schedule and updating graph state. + """ + self.codegen_node_schedule_with_kernel(node_schedule, kernel) + if not only_gen_src_code: + with V.set_kernel_handler(kernel): + for node in NodeScheduleMarker.only_nodes(node_schedule): + node.mark_run() + V.graph.removed_buffers |= kernel.removed_buffers + V.graph.inplaced_to_remove |= kernel.inplaced_to_remove + + def _collect_config_patches(self, node_schedule: list[Any]) -> dict[str, Any]: + """Collect and merge config_patches from all operations in the node schedule. + + This enables scoped config (e.g., coordinate_descent_tuning) for kernels + that contain decomposition operations. + """ + merged_patches: dict[str, Any] = {} + for node in node_schedule: + if isinstance(node, BaseSchedulerNode) and node.node is not None: + patches = node.node.get_config_patches() + if patches: + merged_patches.update(patches) + return merged_patches + + def codegen_node_schedule(self, kernel_features: SIMDKernelFeatures): + """ + Generate code for nodes in kernel_features + """ + node_schedule = kernel_features.node_schedule + + tiling, tiling_score = self.get_tiling_and_scores( + node_schedule, + kernel_features.numel, + kernel_features.reduction_numel, + kernel_features.coalesce_analysis, + ) + kernels = self.create_kernel_choices( + kernel_features, + [tiling], + {"features": kernel_features, "tiling_scores": tiling_score}, + ) + for kernel in kernels: + self.codegen_node_schedule_with_kernel(node_schedule, kernel) + MultiKernel.merge_workspaces_inplace(kernels) + + # Collect config_patches from operations (e.g., decomposition ops with + # coordinate_descent_tuning) and apply during kernel codegen + config_patches = self._collect_config_patches(node_schedule) + + for kernel in kernels: + with V.set_kernel_handler(kernel), config.patch(**config_patches): + src_code = kernel.codegen_kernel() + kernel_name = self.define_kernel(src_code, node_schedule, kernel) + log.debug("Generating kernel code with kernel_name: %s", kernel_name) + kernel.kernel_name = kernel_name + kernel.code_hash = code_hash(src_code) + del kernel + + final_kernel: SIMDKernel | MultiKernel + if len(kernels) > 1: + final_kernel = MultiKernel(kernels) + else: + (final_kernel,) = kernels + + with V.set_kernel_handler(final_kernel): + for node in kernel_features.scheduler_nodes(): + node.mark_run() + + # filter out NodeScheduleMarker + base_scheduler_nodes = [ + node for node in node_schedule if isinstance(node, BaseSchedulerNode) + ] + self.codegen_comment(base_scheduler_nodes, final_kernel.kernel_name) + if config.cpp.enable_kernel_profile: + V.graph.wrapper_code.write_kernel_context_guard_begin() + V.graph.wrapper_code.write_kernel_context_guard( + final_kernel.kernel_name, + base_scheduler_nodes, # type: ignore[arg-type] + ) + final_kernel.call_kernel(final_kernel.kernel_name) + if config.cpp.enable_kernel_profile: + V.graph.wrapper_code.write_kernel_context_guard_end() + + if config.nan_asserts: + final_kernel.codegen_nan_check() + if config.warn_mix_layout: + final_kernel.warn_mix_layout(kernels[0].kernel_name) + + V.graph.removed_buffers |= final_kernel.removed_buffers + V.graph.inplaced_to_remove |= final_kernel.inplaced_to_remove + + if ( + V.graph.wrapper_code.supports_intermediate_hooks # type: ignore[has-type] + and config.generate_intermediate_hooks + ): + # Not every node in the schedule will actually be live on output; + # we can't check dead buffers. + live_outs = kernels[0].args.live_output_buffers() + for node in kernel_features.scheduler_nodes(): + name = node.get_name() + if name not in live_outs: + continue + assert node.node is not None + origin_node = node.node.get_origin_node() + if origin_node is not None: + counters["inductor"]["intermediate_hooks"] += 1 + V.graph.wrapper_code.writeline( + f"run_intermediate_hooks({origin_node.name!r}, {name})" + ) + + self.free_buffers_in_scheduler() + + def create_kernel_choices( + self, kernel_features: SIMDKernelFeatures, kernel_args, kernel_kwargs + ) -> list[SIMDKernel]: + return [ + self.kernel_type( + *kernel_args, + **kernel_kwargs, + ) + ] + + def codegen_node_schedule_with_kernel(self, node_schedule, kernel): + with kernel: + stack = contextlib.ExitStack() + all_indexing = {} + + # First pass to collect indexing and decide inplace updates + for node in node_schedule: + if node is DisableReduction: + stack.enter_context(kernel.disable_reduction()) + elif node is EnableReduction: + stack.close() + else: + node.decide_inplace_update() + index_vars = kernel.split_and_set_ranges(node.get_ranges()) + all_indexing.update( + dict.fromkeys( + node._body.indexing_from_args(index_vars).values() + ) + ) + + kernel.finalize_indexing(all_indexing.keys()) + + # Second pass to do codegen + for node in node_schedule: + if node is DisableReduction: + stack.enter_context(kernel.disable_reduction()) + elif node is EnableReduction: + stack.close() + else: + # TODO - use split ranges ? + indexing_dtype_strength_reduction(node._body) + index_vars = kernel.split_and_set_ranges(node.get_ranges()) + node.codegen(index_vars) + + def _codegen_single_template( + self, + kernel, + render, + template_node, + epilogue_nodes, + prologue_nodes, + *, + only_gen_src_code=False, + ): + """ + Helper method to codegen a single template kernel variant + """ + buf_name_to_prologue_group = {} + template_reads = template_node.used_buffer_names() + prologue_group = [] + for prologue in prologue_nodes: + names = prologue.get_buffer_names() + prologue_group.append(prologue) + # this must be the end of a prologue group + if names & template_reads: + assert len(names) == 1 + buf_name_to_prologue_group[next(iter(names))] = prologue_group + kernel.prologue_fused_inputs.add(next(iter(names))) + prologue_group = [] + + # all prologue groups should have finalized with use in template + assert len(prologue_group) == 0 + + # Remove prologue-fused inputs from input_buffers so that + # remove_kernel_local_buffers can remove them. + for buf_name in kernel.prologue_fused_inputs: + kernel.args.input_buffers.pop(buf_name, None) + + # Dispatch to the kernel for source generation. TritonTemplateKernel + # handles the standard Triton path; ExternalTritonTemplateKernel + # handles external backends (e.g. Helion). + src_code = kernel.codegen_template_body( + self, + template_node, + epilogue_nodes, + prologue_nodes, + buf_name_to_prologue_group, + prologue_preserves_zero_mask, + render, + ) + + if config.benchmark_kernel: + num_gb = kernel.estimate_kernel_num_bytes() / 1e9 + src_code = ( + f"{kernel.imports_for_benchmark_kernel()}\n" + f"{src_code}\n" + f"{kernel.codegen_kernel_benchmark(num_gb).getvalue()}" + ) + + node_schedule = [*prologue_nodes, template_node, *epilogue_nodes] + + if only_gen_src_code: + return src_code + + # Unfused epilogues are codegen'd separately in call_kernel; + # exclude them from mark_run. + unfused_set = OrderedSet([id(n) for n in kernel.get_unfused_epilogues()]) + with V.set_kernel_handler(kernel): + template_node.mark_run() + for node in epilogue_nodes: + if id(node) not in unfused_set: + node.mark_run() + for node in prologue_nodes: + node.mark_run() + + kernel.kernel_name = self.define_kernel(src_code, node_schedule, kernel) + + return kernel + + def _get_multikernel_shapes( + self, node: MultiTemplateBuffer + ) -> tuple[tuple[int, ...], ...]: + from ..ir import IRNode + + def get_size(arg): + if not isinstance(arg, IRNode): + return None + if isinstance(arg, ir.BaseView): # triton templates want the base tensor. + arg = arg.unwrap_view() + if (size := arg.maybe_get_size()) is None: + return None + return tuple(s for s in size) + + out = [] + for arg in list(node.inputs) + [node]: + if isinstance(arg, (list, tuple)): + out.append(tuple(get_size(_arg) for _arg in arg)) + else: + out.append(get_size(arg)) + return tuple(out) + + def _kernel_has_dynamic_shapes(self, node: MultiTemplateBuffer) -> bool: + shapes = self._get_multikernel_shapes(node) + return any( + any( + isinstance(s, sympy.Expr) and not isinstance(s, sympy.Integer) + for s in shape + ) + for shape in shapes + ) + + def _make_shape_cache_key( + self, node: MultiTemplateBuffer, hint: int + ) -> tuple[tuple[int, ...], ...]: + """ + Returns cache key for hint-based multi-graph; key is tuple of shapes with hint filled in. + """ + shapes = self._get_multikernel_shapes(node) + return tuple( + tuple( + hint + if isinstance(s, sympy.Expr) and not isinstance(s, sympy.Integer) + else s + for s in shape + ) + for shape in shapes + ) + + def codegen_template( + self, + template_node, + epilogue_nodes, + prologue_nodes, + *, + only_gen_src_code=False, + hint_override: int | None = None, + ) -> str | None: + """ + Codegen a triton template with multi-kernel dispatch support + + If `only_gen_src_code=True` the src code will be returned instead of being + codegenned into the wrapper + """ + + _, (_numel, rnumel) = template_node.group + assert rnumel == 1 + + if ( + isinstance(template_node.node, MultiTemplateBuffer) + and template_node.node._make_kernel_renders + and len(template_node.node._make_kernel_renders) > 1 + and self._kernel_has_dynamic_shapes(template_node.node) + ): + kernels = {} + src_codes = [] + + for ( + size_hint, + make_kernel_render, + ) in template_node.node._make_kernel_renders.items(): + kernel, render = make_kernel_render( + template_node.node, hint_override=hint_override + ) + + if only_gen_src_code: + src_code = self._codegen_single_template( + kernel, + render, + template_node, + epilogue_nodes, + prologue_nodes, + only_gen_src_code=True, + ) + assert isinstance(src_code, str) + # pyrefly: ignore [bad-argument-type] + src_codes.append(src_code) + else: + if size_hint is None: + continue # skip kernel generation based on real runtime value; only use hints + kernel = self._codegen_single_template( + kernel, + render, + template_node, + epilogue_nodes, + prologue_nodes, + only_gen_src_code=False, + ) + shape_cache_key = ( + None + if size_hint is None + else self._make_shape_cache_key(template_node.node, size_hint) + ) + # pyrefly: ignore [unsupported-operation] + kernels[shape_cache_key] = kernel + + if only_gen_src_code: + return "\n\n".join(src_codes) + + MultiKernel.merge_workspaces_inplace(list(kernels.values())) + multi_kernel = SizeHintMultiKernel(kernels) + node_schedule = [*prologue_nodes, template_node, *epilogue_nodes] + self.codegen_comment(node_schedule, multi_kernel.kernel_name) + multi_kernel.call_kernel(multi_kernel.kernel_name) + V.graph.removed_buffers |= multi_kernel.removed_buffers + V.graph.inplaced_to_remove |= multi_kernel.inplaced_to_remove + self.free_buffers_in_scheduler() + return None + else: + kernel, render = template_node.node.make_kernel_render( + template_node.node, hint_override=hint_override + ) + + if only_gen_src_code: + return self._codegen_single_template( + kernel, + render, + template_node, + epilogue_nodes, + prologue_nodes, + only_gen_src_code=True, + ) + else: + kernel = self._codegen_single_template( + kernel, + render, + template_node, + epilogue_nodes, + prologue_nodes, + only_gen_src_code=False, + ) + + node_schedule = [*prologue_nodes, template_node, *epilogue_nodes] + self.codegen_comment(node_schedule, kernel.kernel_name) + kernel.call_kernel(kernel.kernel_name, template_node.node) + + V.graph.removed_buffers |= kernel.removed_buffers + V.graph.inplaced_to_remove |= kernel.inplaced_to_remove + self.free_buffers_in_scheduler() + return None + + def codegen_sync(self): + V.graph.wrapper_code.writeline(V.graph.device_ops.synchronize()) + + def generate_combo_kernel_code( + self, + subkernel_nodes: list[BaseSchedulerNode], + custom_part_algorithm: bool, + enable_autotune: bool, + mixed_sizes: bool, + only_gen_src_code: bool = False, + ) -> list[tuple[str | None, Any, Any]]: + """ + Generate kernel code for combo kernel partitions. + + Partitions subkernel_nodes using horizontal_partition(), then generates + kernel code for each partition. Single-node partitions are generated as + regular kernels, while multi-node partitions use ComboKernel. + + Returns a list of (src_code, kernel, node_group) tuples. + """ + from .triton import TritonKernel + from .triton_combo_kernel import ComboKernel + + # This is currently the only type supported by this method + assert issubclass(self.kernel_type, TritonKernel) + + fused_node_lists = [node.get_nodes() for node in subkernel_nodes] + node_schedule_map: dict[Any, NodeInfo] = {} + for pn, nodes in zip(subkernel_nodes, fused_node_lists): + _, (numel, rnumel) = max(nodes, key=lambda x: int(x.is_reduction())).group + node_schedule = self.generate_node_schedule(nodes, numel, rnumel) + tiling = self.select_tiling(node_schedule, numel, rnumel) + features = SIMDKernelFeatures(node_schedule, numel, rnumel) + is_persistent_reduction = ( + features.is_reduction() + and V.choices.should_use_persistent_reduction( + features, cooperative_reduction=False + ) + ) + node_schedule_map[pn] = NodeInfo( + node_schedule=node_schedule, + tiling=tiling, + numel=numel, + rnumel=rnumel, + features=features, + is_persistent_reduction=is_persistent_reduction, + ) + + partitions = ComboKernel.horizontal_partition( + nodes=subkernel_nodes, + triton_scheduling=self, + custom_algorithm=custom_part_algorithm, + node_info_map=node_schedule_map, + ) + log.debug( + "ComboKernels: %d nodes partitioned into %s groups", + len(subkernel_nodes), + [len(p) for p in partitions], + ) + kernel_code_list = [] + for node_group in partitions: + if len(node_group) == 0: + continue + + if len(node_group) == 1: + # Single-node partition + node_info = node_schedule_map[node_group[0]] + if only_gen_src_code: + # Skip code generation - caller has cached benchmark results + kernel_code_list.append((None, None, node_group)) + else: + # Generate regular kernel + kernel = self.kernel_type( + node_info.tiling, + features=node_info.features, + ) + self.process_kernel( + kernel, node_info.node_schedule, only_gen_src_code + ) + with V.set_kernel_handler(kernel): + src_code = kernel.codegen_kernel() + # pyrefly: ignore [bad-argument-type] + kernel_code_list.append((src_code, kernel, node_group)) + else: + # Multi-node: create ComboKernel with combo subkernels + kernel = ComboKernel( + triton_kernel_cls=self.kernel_type, + enable_autotune=enable_autotune, + mixed_sizes=mixed_sizes, + ) + for pn in node_group: + node_info = node_schedule_map[pn] + subkernel = ComboKernel.create_triton_kernel( + node_info.tiling, + features=node_info.features, + optimize_mask=not mixed_sizes, + triton_kernel_cls=self.kernel_type, + ) + self.process_kernel( + kernel.create_sub_kernel(subkernel), + node_info.node_schedule, + only_gen_src_code, + ) + + src_code = kernel.codegen_kernel() + # pyrefly: ignore [bad-argument-type] + kernel_code_list.append((src_code, kernel, node_group)) + # pyrefly: ignore [bad-return] + return kernel_code_list + + def codegen_combo_kernel(self, combo_kernel_node): + subkernel_nodes = combo_kernel_node.get_subkernel_nodes() + custom_part_algorithm = combo_kernel_node.use_custom_partition_algo + enable_autotune = combo_kernel_node.enable_autotune + mixed_sizes = config.combo_kernel_allow_mixed_sizes > 1 or ( + config.combo_kernel_allow_mixed_sizes == 1 and custom_part_algorithm + ) + + kernel_code_list = self.generate_combo_kernel_code( + subkernel_nodes, custom_part_algorithm, enable_autotune, mixed_sizes + ) + + for src_code, kernel, _ in kernel_code_list: + kernel_name = self.define_kernel(src_code, [combo_kernel_node], kernel) + self.codegen_comment(combo_kernel_node.snodes, kernel_name) + log.debug("ComboKernels: generated kernel %s.", kernel_name) + kernel.call_kernel(kernel_name) + + self.free_buffers_in_scheduler() + + @classmethod + @functools.lru_cache(32) + def candidate_tilings(cls, node, numel, reduction_numel) -> list[CandidateTiling]: + is_pointwise = reduction_numel == 1 + + def tile_ranges(is_pointwise: bool, ranges, rw) -> list[CandidateTiling]: + """ + Compute tiling candidates by dividing up the iteration ranges. + """ + assert len(rw.range_vars) == len(ranges), f"{rw.range_vars=} {ranges=}" + + # isinstance(dep, MemoryDep): this filters out StarDeps. StarDeps refer to reads + # that need to access the entire tensor; they don't contribute read indexing + # information (and practically, they don't have dep.index so they can't be used + # for stride_hints below + dep_sources = [rw.reads, rw.writes] + assert all( + isinstance(dep, (MemoryDep, StarDep)) + for dep in itertools.chain.from_iterable(dep_sources) + ) + deps = [ + dep + for dep in itertools.chain.from_iterable(dep_sources) + if dep.name not in V.graph.removed_buffers + and isinstance(dep, MemoryDep) + ] + write_names = OrderedSet([dep.name for dep in rw.writes]) + + def collapse_ranges(ranges: Sequence[sympy.Expr]) -> sympy.Expr: + return V.graph.sizevars.simplify(sympy_product(ranges)) + + # Default to no tiling. + tilings = [ + CandidateTiling( + tiling=cls.create_partial_tiling( + [collapse_ranges(ranges)], is_pointwise + ), + name="none", + score=0, + ) + ] + + # Find non-trivial tiling candidates. + for dep in deps: + strides = V.graph.sizevars.stride_hints(dep.index, rw.range_vars) + assert len(strides) == len(ranges) + try: + split = strides.index(1) + 1 + if split == len(ranges): + continue + if all(s == 0 for s in strides[split:]): + # if this is a broadcasted tensor and all dimensions after split are broadcast, + # this is not a real split + continue + + except ValueError: + continue + + tiled_groups = ( + collapse_ranges(ranges[:split]), + collapse_ranges(ranges[split:]), + ) + + # score by number of elements + score = V.graph.sizevars.optimization_hint( + sympy_product( + size for size, stride in zip(ranges, strides) if stride != 0 + ) + ) + if dep.name in write_names: + # ngimel said contiguous writes is more important than reads + score *= 2 + if CandidateTiling.is_good_size(tiled_groups[0]): + score *= 2 + if CandidateTiling.is_good_size(tiled_groups[1]): + score *= 2 + + if ( + V.graph.sizevars.optimization_hint( + score - sympy_product(itertools.chain(ranges, reduction_ranges)) + ) + >= 0 + ): + tilings.append( + CandidateTiling( + tiling=cls.create_partial_tiling( + [ + collapse_ranges(ranges[:split]), + collapse_ranges(ranges[split:]), + ], + reduction_numel, + ), + score=score, + name=dep.name, + ) + ) + + return tilings + + pointwise_ranges, reduction_ranges = node.get_ranges() + if ( + len(pointwise_ranges) <= 1 + and len(reduction_ranges) <= 1 + or free_unbacked_symbols(pointwise_ranges + reduction_ranges) + ): + return [] + + # Tile either pointwise or reduction dims. + pointwise_ranges, reduction_ranges = node.get_ranges() + partial_tilings = tile_ranges( + is_pointwise, + pointwise_ranges if is_pointwise else reduction_ranges, + node.pointwise_or_reduction_read_writes(is_pointwise), + ) + + # Fill in the missing ranges. + full_tilings = [ + CandidateTiling( + tiling=cls.complete_partial_tiling( + tiling.tiling, numel, reduction_numel + ), + score=tiling.score, + name=tiling.name, + ) + for tiling in partial_tilings + ] + + return full_tilings + + @classmethod + def create_tiling( + cls, pw_tiling: Sequence[sympy.Expr], reduction_tiling: Sequence[sympy.Expr] + ) -> immutable_dict[str, sympy.Expr]: + """ + Create a tiling dict from pointwise and reduction splits. + """ + pw_prefixes = ("z", "y", "x") + reduction_prefixes = ("r0_", "r1_") + assert len(pw_tiling) <= len(pw_prefixes) + assert len(reduction_tiling) <= len(reduction_prefixes) + + return immutable_dict( + [ + *zip(pw_prefixes[-len(pw_tiling) :], pw_tiling, strict=False), + *zip(reduction_prefixes, reduction_tiling, strict=False), + ] + ) + + @classmethod + def create_partial_tiling( + cls, + tiling: Sequence[sympy.Expr], + is_pointwise: bool, + ) -> immutable_dict[str, sympy.Expr]: + return cls.create_tiling( + tiling if is_pointwise else [], + tiling if not is_pointwise else [], + ) + + @classmethod + def complete_partial_tiling( + cls, + tiling: dict[str, sympy.Expr], + numel: sympy.Expr, + reduction_numel: sympy.Expr, + ) -> immutable_dict[str, sympy.Expr]: + """ + Given a tiling for only pointwise or reduction dimensions, adds the missing one. + """ + splits = list(tiling.values()) + is_pointwise = "x" in tiling + + total_numel = numel * reduction_numel + missing_tiling = [total_numel / sympy_product(splits)] + + tiling_args = ( + (splits, missing_tiling) if is_pointwise else (missing_tiling, splits) + ) + return cls.create_tiling(*tiling_args) + + @classmethod + def get_nd_tilings( + cls, + node_schedule, + pointwise_numel, + reduction_numel, + ) -> list[immutable_dict[str, sympy.Expr]]: + """ + Creates N-dimensional tiling candidates, attempting to simplify loads/stores + by tiling the kernel into higher dimensions. + + Returns a list of tilings ranked by dimensionality. + """ + + def collapse_dims( + dims: Sequence[sympy.Expr], fallback_numel: sympy.Expr + ) -> tuple[sympy.Expr, ...]: + """ + Collapse dimensions to the maximum allowed number of tiles. + """ + if not dims: + return (fallback_numel,) + max_tiles = get_max_tiles(2) + if V.graph.sizevars.statically_known_equals( + pointwise_numel, 1 + ) and V.graph.sizevars.statically_known_gt(reduction_numel, 1): + # We only have at most two dimensions to tile over when emitting a + # reduction-only kernel. + max_tiles = min(max_tiles, 2) + num_leading_dims = max(0, len(dims) - max_tiles) + first_trailing_dim = num_leading_dims + 1 + collapsed_leading_dim = sympy_product(dims[:first_trailing_dim]) + return (collapsed_leading_dim,) + tuple(dims[first_trailing_dim:]) + + def tile_var_ranges( + var_ranges, ranges_to_tile, total_numel + ) -> tuple[sympy.Expr, ...]: + # Pattern match the subexpression pertaining to each index variable. + tiling = [] + for var, numel in var_ranges: + index = BlockPatternMatcher.get_subexpr_involving_symbol(dep.index, var) + + # Heuristic to bound the maximum dimensionality of the block. + num_dims = max( + 2, + index.count(FloorDiv) + index.count(ModularIndexing), + len(ranges_to_tile), + ) + + # Attempt to pattern match the index expr. + # Failed matches default to the full range. + match_result = BlockPatternMatcher.match_mod_div_block_expr( + index, var, numel, num_dims + ) + dims = match_result[0] if match_result is not None else [numel] + tiling.extend(dims) + + # Prune dimensions of size 1. + tiling = [ + dim + for dim in tiling + if not V.graph.sizevars.statically_known_equals(dim, sympy.S.One) + ] + + return collapse_dims(tiling, total_numel) + + is_pointwise = reduction_numel == 1 + tilings = OrderedSet[immutable_dict[str, sympy.Expr]]() + for node in EnableReduction.filter(node_schedule): + if not isinstance(node, scheduler.SchedulerNode): + continue + + # If this is a reduction schedule, skip nodes which are missing their + # reduction ranges. + node_ranges = node.get_ranges() + if not is_pointwise and len(node_ranges[1]) == 0: + continue + + # Use the node ranges as the default tiling candidate. + default_pointwise_tiling = collapse_dims(node_ranges[0], pointwise_numel) + default_reduction_tiling = collapse_dims(node_ranges[1], reduction_numel) + node_tilings = [(default_pointwise_tiling, default_reduction_tiling)] + + # Search the indexing expressions for more candidates. + # If we see modular indexing, try to subdivide ranges into their implied + # block shape. + memory_deps = [ + dep + for dep in node.read_writes.reads_and_writes() + if isinstance(dep, MemoryDep) and len(dep.ranges) > 0 + ] + for dep in memory_deps: + # Attempt to partition variable ranges into pointwise and reduction groups. + # To achieve this, merge the leading ranges until we reach the pointwise numel. + all_var_ranges = [*dep.ranges.items()] + pointwise_vars_numel = sympy.S.One + sizevars = V.graph.sizevars + pointwise_end_idx = 0 + for idx, (_var, numel) in enumerate(all_var_ranges): + pointwise_vars_numel *= numel + pointwise_end_idx = idx + if sizevars.statically_known_geq( + pointwise_vars_numel, pointwise_numel + ): + break + + # Reject the split if it does not match the total pointwise numel. + if not sizevars.statically_known_equals( + pointwise_vars_numel, pointwise_numel + ): + continue + + # Partition var ranges into pointwise and reduction splits, tiling them + # separately. + reduction_start_idx = pointwise_end_idx + 1 + pointwise_var_ranges = all_var_ranges[:reduction_start_idx] + reduction_var_ranges = ( + None if is_pointwise else all_var_ranges[reduction_start_idx:] + ) + pointwise_tiling = tile_var_ranges( + pointwise_var_ranges, node_ranges[0], pointwise_numel + ) + reduction_tiling = ( + (sympy.S.One,) + if is_pointwise + else tile_var_ranges( + reduction_var_ranges, node_ranges[1], reduction_numel + ) + ) + + if len(pointwise_tiling) and len(reduction_tiling) > 0: + node_tilings.append((pointwise_tiling, reduction_tiling)) + + # Each memory dependency contributes one pointwise and reduction tiling. We + # take the Cartesian product of these, yielding all possible joint tilings. + for pointwise_tiling, reduction_tiling in itertools.product( + *zip(*node_tilings) + ): + tilings.add(cls.create_tiling(pointwise_tiling, reduction_tiling)) + + # Rank tilings by the number of dimensions. E.g., prefer 2D to 1D. + # Since this is a stable sort, ties are broken by schedule order. + ranked_tilings = sorted( + tilings, + key=len, + reverse=True, + ) + + return ranked_tilings + + @classmethod + def compute_tiling_strategy( + cls, + node_schedule: list[NodeScheduleEntry], + pointwise_numel: sympy.Expr, + reduction_numel: sympy.Expr, + coalesce_analysis: CoalesceVarAnalysis, + ) -> tuple[dict[str, sympy.Expr], dict[str, sympy.Expr] | None]: + """ + Generates a tiling, and a score of each tile according to each tile's coalesced memory accesses. + """ + tiling_var: sympy.Expr | None = ( + None + if not coalesce_analysis.suggested_split + else coalesce_analysis.suggested_split.var + ) + + all_iter_vars = coalesce_analysis.norm_read_writes.index_vars + all_red_vars = coalesce_analysis.norm_read_writes.reduce_vars + ranges = coalesce_analysis.norm_read_writes.var_ranges + + pw_ranges = [ranges[v] for v in all_iter_vars] + red_ranges = [ranges[v] for v in all_red_vars] + + # Sometimes dynamic shapes is unable to prove equality without hint + get_hint = V.graph.sizevars.optimization_hint + torch._check( + get_hint(sympy_product(pw_ranges)) == get_hint(pointwise_numel), + lambda: f"{pw_ranges}, {pointwise_numel}, {node_schedule}", + ) + + torch._check( + get_hint(sympy_product(red_ranges)) == get_hint(reduction_numel), + lambda: f"{red_ranges}, {reduction_numel}, {node_schedule}", + ) + + # score of a pointwise or reduction split + scored_sub_split: dict[Any, tuple[list[int], list[int]]] = {} + + score_split: list[ + tuple[tuple[list[int], list[int]], tuple[list[int], list[int]]] + ] = [] + + def process_node_vars( + vars_to_use: tuple[sympy.Expr, ...] = (), + use_split_var: bool = False, + is_pointwise: bool = False, + ) -> tuple[list[int], list[int]]: + """ + Generate a tiling, and a tiling score, given vars to use as splits. + """ + + ranges = pw_ranges if is_pointwise else red_ranges + target_numel = pointwise_numel if is_pointwise else reduction_numel + # Some kernels have no reduction ranges, and a reduction numel of 1 + if not ranges: + if target_numel: + return ([target_numel], []) + else: + return ([], []) + + key = (repr(vars_to_use), use_split_var, is_pointwise) + if out := scored_sub_split.get(key): + return out + + splitting_vars = all_iter_vars if is_pointwise else all_red_vars + + splits = [] + split_scores = [] + prod = 1 + prev_var_coalesced_score = 0 + + # iterate from non-dense to dense + for v, v_range in zip(splitting_vars, ranges): + if v not in vars_to_use: + prod *= v_range + prev_var_coalesced_score = coalesce_analysis.coalesced_by_var.get( + v, 0 + ) + continue + + if use_split_var and v == tiling_var: + var_tiling = coalesce_analysis.suggested_split + assert var_tiling is not None + + tile = var_tiling.tiling_factor + remainder = FloorDiv(v_range, var_tiling.tiling_factor) + + splits.append(prod * remainder) + split_scores.append(var_tiling.score) + + splits.append(tile) + split_scores.append(coalesce_analysis.coalesced_by_var.get(v, 0)) + + prod = 1 + prev_var_coalesced_score = 0 + + continue + + prod *= v_range + splits.append(prod) + split_scores.append(coalesce_analysis.coalesced_by_var.get(v, 0)) + prod = 1 + + if prod != 1 or (is_pointwise and len(splits) == 0): + splits.append(prod) + split_scores.append(prev_var_coalesced_score) + + # penalize splits that leave small blocks + # where we can't fully utilize full memory transaction + # TODO: incorporate exact bitwidth, and read/write + # coalesced write is 2x more important + for i in range(len(splits)): + s = V.graph.sizevars.optimization_hint(splits[i], fallback=32) + s = min(s, 8) + split_scores[i] = int(split_scores[i] * s / 8) + + scored_sub_split[key] = (splits, split_scores) + return (splits, split_scores) + + # add the default tiling + score_split.append( + ( + process_node_vars(is_pointwise=True), + process_node_vars(is_pointwise=False), + ) + ) + + if tiling_var: + score_split.append( + ( + process_node_vars( + (tiling_var,), use_split_var=True, is_pointwise=True + ), + process_node_vars(is_pointwise=False), + ) + ) + + # TODO, add tests, reduction splits if config.triton.tile_reductions + # TODO: we should ignore tiny increases in score for extra splits + overlapping_iter_vars = ( + all_iter_vars & coalesce_analysis.coalesced_by_var.keys() + ) + for v in overlapping_iter_vars: + score_split.append( + ( + process_node_vars((v,), is_pointwise=True), + process_node_vars(is_pointwise=False), + ) + ) + + if get_max_tiles(default=3) == 3 and reduction_numel == 1: + for vars_to_use in itertools.combinations(overlapping_iter_vars, 2): + score_split.append( + ( + process_node_vars(vars_to_use, is_pointwise=True), + process_node_vars(is_pointwise=False), + ) + ) + + tilings: list[tuple[CandidateTiling, immutable_dict[str, sympy.Expr]]] = [] + for (pw_split, pw_score), (red_split, red_score) in score_split: + candidate = CandidateTiling( + cls.create_tiling(pw_split, red_split), + score=sum(pw_score) + sum(red_score), + ) + tiling_score = cls.create_tiling(pw_score, red_score) + tilings.append((candidate, tiling_score)) + + default_tiling = cls.create_tiling([pointwise_numel], [reduction_numel]) + + # add a slight penalty for longer tilings that dont increase score much, + # and are poor sizes + bad_size_additional_tiling_penalty = 1.025 + good_size_tiling_penalty = 1.005 + + total_uncoalesced = sum(coalesce_analysis.uncoalesced_addrs.values()) + + def score_mod(t): + score_factor = 1.0 + for tile_size in t[0].tiling.values(): + if not CandidateTiling.is_good_size(tile_size): + score_factor = score_factor / bad_size_additional_tiling_penalty + else: + score_factor = score_factor / good_size_tiling_penalty + + # Add uncoalesced memory score to prevent small coalesced benefits + # from dominating large amounts of uncoalesced memory + uncoalesced_penalty = total_uncoalesced * 0.05 + + return -(t[0].score + uncoalesced_penalty) * score_factor + + # apply penalty for longer tilings that dont increase score much + for cand, tiling_score in sorted(tilings, key=score_mod): + if ( + cls.tiling_is_compatible( + node_schedule, pointwise_numel, reduction_numel, cand.tiling + ) + or cand.tiling == default_tiling + ): + # we always include default reduction numel == 1, dont include + tiling_len = len(cand.tiling) - (1 if reduction_numel == 1 else 0) + if tiling_len > get_max_tiles(default=3): + perf_hint_log.info( + "Found optimal tiling with %s tiles but torch._inductor.config.triton.max_tiles " + "set to %s. Consider increasing", + tiling_len, + torch._inductor.config.triton.max_tiles, + ) + continue + + return cand.tiling, tiling_score + + # surprisingly, the default tiling is not always read as compatible by `tiling_is_compatible` + # TODO - look into, occurs with dynamic shapes often + if cand.tiling == default_tiling: + return cand.tiling, tiling_score + + return default_tiling, None + + @classmethod + def tiling_is_compatible( + cls, + node_schedule: list[NodeScheduleEntry], + numel: sympy.Expr, + reduction_numel: sympy.Expr, + tiling: dict[str, sympy.Expr], + ): + assert isinstance(tiling, dict) + return all( + SIMDKernel.is_compatible( + tiling.values(), node.get_ranges(), reduction_numel=reduction_numel + ) + for node in node_schedule + if isinstance(node, scheduler.SchedulerNode) + ) + + @classmethod + def get_first_compatible_tiling( + cls, + node_schedule: list[NodeScheduleEntry], + numel: sympy.Expr, + reduction_numel: sympy.Expr, + ranked_tilings: list[dict[str, sympy.Expr]], + ): + for tiling in ranked_tilings: + if cls.tiling_is_compatible(node_schedule, numel, reduction_numel, tiling): + return tiling + + return None + + @classmethod + def select_tiling( + cls, + node_schedule, + numel, + reduction_numel=sympy.S.One, + coalesce_analysis: CoalesceVarAnalysis | None = None, + ) -> dict[str, sympy.Expr]: + return cls.get_tiling_and_scores( + node_schedule, numel, reduction_numel, coalesce_analysis + )[0] + + @classmethod + def get_tiling_and_scores( + cls, + node_schedule, + numel, + reduction_numel=sympy.S.One, + coalesce_analysis: CoalesceVarAnalysis | None = None, + ) -> tuple[dict[str, sympy.Expr], dict[str, sympy.Expr] | None]: + """ + Heuristics to decide how to tile kernels. + Currently, we tile based on stride-1 dimensions. + + Returns: + `(tile1, tile2, reduction_numel)` s.t. `tile1 * tile2 == numel` + + """ + # If this is a reduction, only tile reduction dims. + is_pointwise = reduction_numel == 1 + + # Tiled reductions are gated by a config flag. + default_tiling = cls.create_tiling([numel], [reduction_numel]) + + # Force tiling compatible with matmul dimensions + # when natively generating matmul without template calls. + for node in EnableReduction.filter(node_schedule): + if isinstance(node.node, ir.ComputedBuffer): + if ( + node.node.get_reduction_type() == "dot" + and config.triton.native_matmul + ): + # A[M,K] @ B[K,N] + # force tiling to be {'y':M, 'x':N, 'r0_':K} + node_ranges = node.get_ranges() + range_y_x = node_ranges[0] # (M,N) + range_r = node_ranges[1] # (K) + tiling = cls.create_tiling(range_y_x, range_r) + return tiling, None + + # # TODO: enable by default + if ( + torch._inductor.config.triton.coalesce_tiling_analysis + and coalesce_analysis + and not config.triton.prefer_nd_tiling + ): + return cls.compute_tiling_strategy( + node_schedule, numel, reduction_numel, coalesce_analysis + ) + + if (not is_pointwise and not config.triton.tile_reductions) or get_max_tiles( + default=2 + ) <= 1: + # Emit a perf hint in case we miss an opportunity to tile a reduction. + if perf_hint_log.level <= logging.WARNING: + for node in EnableReduction.filter(node_schedule): + if ( + not config.triton.tile_reductions + and len(cls.candidate_tilings(node, numel, reduction_numel)) > 0 + ): + perf_hint_log.info( + textwrap.dedent( + """ + Reduction over non-contiguous dims. + Consider setting config.triton.tile_reductions to True. + """ + ) + ) + break + + return default_tiling, None + + seen_names: OrderedSet[str] = OrderedSet() + candidate_tiles: Counter[CandidateTiling] = collections.Counter() + for node in EnableReduction.filter(node_schedule): + for candidate_tiling in cls.candidate_tilings(node, numel, reduction_numel): + if candidate_tiling.name in seen_names: + continue + elif candidate_tiling.name is not None: + seen_names.add(candidate_tiling.name) + candidate_tiles[candidate_tiling] += candidate_tiling.score + + ranked_tilings: list[dict[str, sympy.Expr]] = [ + candidate_tiling.tiling + for candidate_tiling, score in candidate_tiles.most_common() + ] + + if get_max_tiles(default=2) >= 3 and is_pointwise: + # Consider adding a third dimension of tiling, but only + # when a1 is a multiple of b1; otherwise, you have a lot + # of stragglers which is annoying to generate code for. + # + # NB: More than three max tiles is not enabled by default. + + def convert_tiling_to_3d( + tiling0: dict[str, sympy.Expr], tiling1: dict[str, sympy.Expr] + ) -> dict[str, sympy.Expr] | None: + a0, a1 = tiling0["x"], tiling0.get("y", 1) + b0, b1 = tiling1["x"], tiling1.get("y", 1) + + # TODO: These tiling decisions (equality, ordering, divisibility) + # are structural — they determine the kernel's iteration space + # decomposition — but are NOT guarded here. If the relationship + # between a1 and b1 changes across dynamic shape inputs, the + # compiled kernel could be wrong. Seems scary, unless there is a reason + # this is safe in that case probably we need better comment here ! + hint = V.graph.sizevars.guarding_hint_or_throw + if hint(a1 - b1) == 0: + return None + if hint(a1 - b1) < 0: + # swap so a0 is bigger + (a0, a1), (b0, b1) = (b0, b1), (a0, a1) + + assert hint(a1 - b1) > 0 + if not V.graph.sizevars.statically_known_multiple_of(a1, b1): + return None + + new_tiling = { + "z": a0, + "y": FloorDiv(a1, b1), + "x": b1, + "r0_": tiling0["r0_"], + } + + return new_tiling + + for i in range(1, len(ranked_tilings)): + new_3d_tiling = convert_tiling_to_3d( + ranked_tilings[0], ranked_tilings[i] + ) + if new_3d_tiling is not None: + ranked_tilings = [new_3d_tiling] + ranked_tilings + break # only 1 choice for now + + if len(ranked_tilings) > 1: + perf_hint_log.info("possibly bad tiling: %s", ranked_tilings) + + # Optionally, prefer tiling into as many dimensions as possible. + # pyrefly: ignore [unbound-name] + if config.triton.prefer_nd_tiling: + ranked_tilings = ( + cls.get_nd_tilings(node_schedule, numel, reduction_numel) + + ranked_tilings + ) + + if tiling := cls.get_first_compatible_tiling( + node_schedule, numel, reduction_numel, ranked_tilings + ): + return tiling, None + + return default_tiling, None + + def flush(self): + pass + + def ready_to_flush(self) -> bool: + return False + + def generate_kernel_code_from_nodes( + self, nodes, benchmark_kernel=False, hint_override: int | None = None + ): + if not any(n.is_template() for n in nodes): + _, (numel, rnumel) = max(nodes, key=lambda x: int(x.is_reduction())).group + node_schedule = self.generate_node_schedule(nodes, numel, rnumel) + tiling = self.select_tiling(node_schedule, numel, rnumel) + kernel = self.kernel_type( + tiling, + features=SIMDKernelFeatures(node_schedule, numel, rnumel), + ) + self.codegen_node_schedule_with_kernel(node_schedule, kernel) + # Collect config_patches from operations + config_patches = self._collect_config_patches(node_schedule) + config_patches["benchmark_kernel"] = benchmark_kernel + with ( + config.patch(**config_patches), + V.set_kernel_handler(kernel), + ): + src_code = kernel.codegen_kernel() + else: + prologue, template, epilogue = nodes[0].get_prologue_template_epilogue( + nodes + ) + with config.patch("benchmark_kernel", benchmark_kernel): + src_code = self.codegen_template( + template, + epilogue, + prologue, + only_gen_src_code=True, + hint_override=hint_override, + ) + + # pyrefly: ignore [missing-attribute] + src_code = src_code.replace(str(Placeholder.KERNEL_NAME), "triton_") + return src_code + + def define_kernel(self, src_code, node_schedule, kernel): + raise NotImplementedError + + +@dataclasses.dataclass(frozen=True) +class CandidateTiling: + tiling: dict[str, sympy.Expr] + score: int # higher is better + name: str | None = None + + @staticmethod + def is_good_size(s): + """Somewhat arbitrary heuristic used to boost scores for some sizes""" + s = V.graph.sizevars.optimization_hint(s, fallback=8192) + return s >= 32 and (s % 32 == 0) + + +class CantSplit(Exception): + def __init__(self, expr, remaining): + super().__init__() + self.expr = expr + self.remaining = remaining + + def __str__(self): + return f"{self.expr} not divisible by {self.remaining}" diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/simd_kernel_features.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/simd_kernel_features.py new file mode 100644 index 0000000000000000000000000000000000000000..5b89828ef199e197881e0c843117cbcbd5bd8180 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/simd_kernel_features.py @@ -0,0 +1,637 @@ +from __future__ import annotations + +import collections +import dataclasses +import functools +import itertools +import typing +from typing import Any + +import sympy + +import torch + +from ...utils._ordered_set import OrderedSet +from ...utils._sympy.functions import FloorDiv, ModularIndexing +from ...utils._sympy.symbol import make_symbol, SymT +from ..dependencies import Dep, extract_loop_body_with_args, MemoryDep +from ..runtime.hints import ReductionHint +from ..scheduler import SchedulerNode +from ..utils import cache_on_self +from ..virtualized import V + + +if typing.TYPE_CHECKING: + from collections.abc import Iterable, Sequence + + from torch._inductor.tiling_utils import CoalesceVarAnalysis + + +class NodeScheduleMarker: + @staticmethod + def only_nodes(it: Iterable[NodeScheduleEntry]) -> Iterable[SchedulerNode]: + for item in it: + if not (item is DisableReduction or item is EnableReduction): + yield item # type: ignore[misc] + + @staticmethod + def is_reduction() -> bool: + return False + + +NodeScheduleEntry = SchedulerNode | type[NodeScheduleMarker] + + +class DisableReduction(NodeScheduleMarker): + """ + Marker to invoke `kernel.disable_reduction()`. This closes a + reduction loop and allows for pointwise ops to occur on the output + of a reduction. + """ + + +class EnableReduction(NodeScheduleMarker): + """ + Marker to end a DisableReduction block. + """ + + @staticmethod + def filter(node_schedule: list[NodeScheduleEntry]) -> Iterable[SchedulerNode]: + """ + Get the nodes from node_schedule skipping those in a + DisableReduction block. + """ + disabled = False + for node in node_schedule: + if node in (EnableReduction, DisableReduction): + # Don't tile stuff outside the main reduction loop + disabled = node is DisableReduction + elif disabled: + pass + else: + yield node # type: ignore[misc] + + +class SIMDKernelFeatures: + """ + An ordered schedule of nodes that will become a single kernel. + """ + + def __init__( + self, + node_schedule: list[NodeScheduleEntry], + numel: sympy.Expr, + reduction_numel: sympy.Expr = sympy.S.One, + coalesce_analysis: CoalesceVarAnalysis | None = None, + ): + self.node_schedule = node_schedule + # numel excludes reduction_numel + self.numel: sympy.Expr = V.graph.sizevars.simplify(numel) + self.reduction_numel: sympy.Expr = V.graph.sizevars.simplify(reduction_numel) + self._stats_cache: dict[tuple[sympy.Expr, ...], MemoryStats] = {} + self.coalesce_analysis = coalesce_analysis + + @cache_on_self + def is_reduction(self) -> bool: + return self.reduction_numel != 1 + + @cache_on_self + def scheduler_nodes(self) -> Iterable[SchedulerNode]: + return tuple(NodeScheduleMarker.only_nodes(self.node_schedule)) + + def reduction_nodes(self) -> list[SchedulerNode]: + return [n for n in self.scheduler_nodes() if n.is_reduction()] + + @cache_on_self + def buf_accesses(self) -> dict[str, list[Dep]]: + """only needed for config.benchmark_kernel""" + buf_accesses = collections.defaultdict(list) + for node in self.scheduler_nodes(): + for access in node.read_writes.reads | node.read_writes.writes: + buf_accesses[access.name].append(access) + return buf_accesses + + @cache_on_self + def op_counts(self) -> collections.Counter[str]: + counts: collections.Counter[str] = collections.Counter() + for node in self.scheduler_nodes(): + counts.update(node._body.op_counts) + return counts + + def contains_op(self, op_name: str) -> bool: + """True if V.ops.{op_name} is used in node_schedule""" + return bool(self.op_counts().get(op_name)) + + def get_mutations(self) -> OrderedSet[str]: + mutations: OrderedSet[str] = OrderedSet() + for node in self.scheduler_nodes(): + for buf in node.get_outputs(): + mutations.update(buf.get_mutations()) + return mutations + + @cache_on_self + def select_index_dtype(self) -> torch.dtype: + # Gather all used buffer names + buffer_names: OrderedSet[str] = OrderedSet() + for node in self.scheduler_nodes(): + buffer_names.update(node.get_buffer_names()) + buffer_names.update(node.used_buffer_names()) + buffers = [V.graph.get_buffer(name) for name in buffer_names] + + # In theory we can separately check xnumel and rnumel are <= int_max + # but some indexers do use the full linear index so we need to be + # conservative here. + total_numel = self.numel * self.reduction_numel + + from .simd import SIMDScheduling + + if SIMDScheduling.can_use_32bit_indexing(total_numel, buffers): + return torch.int32 + return torch.int64 + + def get_reduction_hint( + self, tiling_scores: dict[str, int] | None = None + ) -> ReductionHint: + reductions = self.reduction_nodes() + if len(reductions) > 0: + hints = [self.reduction_hint(n) for n in reductions] + if hints.count(hints[0]) == len(hints): + reduction_hint_val = hints[0] + else: + reduction_hint_val = ReductionHint.DEFAULT + + if ( + reduction_hint_val == ReductionHint.INNER + and self.has_non_contiguous_pw_in_reduction_kernel() + ): + reduction_hint_val = ReductionHint.DEFAULT + + # Upgrade DEFAULT to INNER for inner reductions based on tiling scores + if ( + reduction_hint_val == ReductionHint.DEFAULT + and tiling_scores is not None + and "x" in tiling_scores + and "r0_" in tiling_scores + ): + # If reduction dimension has much better coalescing than non-reduction dimensions, + # this is an inner reduction + from ..codegen.triton import INNER_REDUCTION_RATIO_THRESHOLD + + r_coalesce_ratio = tiling_scores["r0_"] / max(tiling_scores["x"], 1) + contiguous_red = r_coalesce_ratio >= INNER_REDUCTION_RATIO_THRESHOLD + if contiguous_red: + reduction_hint_val = ReductionHint.INNER + else: + reduction_hint_val = ReductionHint.DEFAULT + return reduction_hint_val + + @cache_on_self + def buffer_read_counts(self) -> dict[str, int]: + """Counts how many times each buffer is read within the kernel""" + read_counts: dict[str, int] = collections.defaultdict(int) + + for node in self.scheduler_nodes(): + # node.read_writes.reads contains MemoryDep objects for each read + for read_dep in node.read_writes.reads: + read_counts[read_dep.name] += 1 + + return dict(read_counts) # Convert defaultdict to regular dict + + def has_non_contiguous_pw_in_reduction_kernel(self) -> bool: + pointwise_nodes = [ + n + for n in self.scheduler_nodes() + if not n.is_reduction() + and n.group[1][0] == self.numel * self.reduction_numel + ] + for node in pointwise_nodes: + # An index can be an integer when loading a random seed. + if not all( + not isinstance(dep, MemoryDep) + or dep.is_contiguous() + or isinstance(dep.index, (sympy.Integer, int)) + or dep.stride1_for_last_dim() + for dep in itertools.chain( + node.read_writes.reads, node.read_writes.writes + ) + ): + return True + return False + + @staticmethod + def reduction_hint(node: Any) -> ReductionHint: + assert node.is_reduction() + if node.node.data.reduction_hint != ReductionHint.INNER and all( + dep.is_contiguous() + for dep in itertools.chain(node.read_writes.reads, node.read_writes.writes) + ): + return ReductionHint.INNER + else: + return node.node.data.reduction_hint + + def memory_stats( + self, groups_dict: dict[str, sympy.Expr] | None = None + ) -> MemoryStats: + """Analysis to generate features that can be used in heuristics""" + if groups_dict is None: + groups = (self.numel, self.reduction_numel) + elif groups_dict.keys() == OrderedSet(["x", "r0_"]): + groups = (groups_dict["x"], groups_dict["r0_"]) + else: + raise NotImplementedError(f"groups_dict={groups_dict!r}") + result = self._stats_cache.get(groups) + if result is None: + self._stats_cache[groups] = result = MemoryStats.compute( + MemoryEstimator(self, groups) + ) + return result + + +class MemoryEstimator: + """ + Estimate various properties of the kernel for use in heuristics. + We simulate the memory effects of CSE/buffer elimination in codegen. + """ + + kernel_sizes: tuple[sympy.Expr, ...] + outside_loop: MemoryEstimate + loops: list[MemoryEstimate] + persistent: MemoryEstimate + symbols: list[sympy.Symbol] + + def __init__(self, features: SIMDKernelFeatures, groups: Sequence[sympy.Expr]): + self.features = features + self.inside_reduction = features.is_reduction() + self.store_buffer_names: OrderedSet[str] = OrderedSet() + self.must_keep_buffers: OrderedSet[str] = OrderedSet() + self.num_reductions_dims = 1 + self.groups = groups + self.symbols = [make_symbol(SymT.INDEX, i) for i in range(len(groups))] + # We are doing two estimates simultaneously: + # 1) the first is a for a non-persistent (aka looped) reduction, using self.outside_loop/self.loops + # we add an item to loops each corresponding to each reduction loop in the kernel + # outside_loop is only used for broadcasting or point-wise ops that don't use the reduction dimension + # 2) the second is for a persistent kernel, using self.persistent + # persistent kernels don't have loops, so we only have one MemoryEstimate() + # for point-wise ops the two estimates will be the same, they matter for reductions only + self.outside_loop = MemoryEstimate() + self.loops = [MemoryEstimate()] + self.persistent = MemoryEstimate() + self.simulate_codegen() + self.remove_kernel_local() + + def simulate_codegen(self) -> None: + from .simd import SIMDKernel + + kernel_size_outside_loop = (*self.groups[:-1], sympy.S.One) + kernel_size_inside_loop = tuple(self.groups) + self.kernel_sizes = kernel_size_inside_loop + + for node in self.features.node_schedule: + if node is DisableReduction: + self.inside_reduction = False + self.kernel_sizes = kernel_size_outside_loop + continue + elif node is EnableReduction: + self.inside_reduction = True + self.kernel_sizes = kernel_size_inside_loop + self.loops.append(MemoryEstimate()) + continue + assert isinstance(node, SchedulerNode) + rw = extract_loop_body_with_args( + node._body, + SIMDKernel.map_kernel_groups_to_node_sizes( + self.kernel_sizes, node.get_ranges(), self.set_ranges + ), + dict(zip(self.symbols, self.kernel_sizes)), + ) + + for dep in rw._reads: + if not isinstance(dep, MemoryDep): + continue + dep = dep.simplify_with_ranges() + if not self.persistent.writes.get(dep.name): # cache miss? + self.persistent.reads[dep.name].add(dep) + # the cache behavior of looped kernels is more complex than the persistent case above + # some operations are lifted outside the loop (if they don't use the reduction dimension) + # other operations are inside the loop, and can only be reused within the same loop + if not ( + self.outside_loop.writes.get(dep.name) + or self.loops[-1].writes.get(dep.name) + ): + self.scope(dep).reads[dep.name].add(dep) + if dep.name in self.store_buffer_names and self.loops[-1].reads.get( + dep.name + ): + self.must_keep_buffers.add(dep.name) + + for dep in rw._writes: + if not isinstance(dep, MemoryDep): + continue + dep = dep.simplify_with_ranges() + self.store_buffer_names.add(dep.name) + self.persistent.writes[dep.name].add(dep) + self.scope(dep).writes[dep.name].add(dep) + + def remove_kernel_local(self) -> None: + # Remove any kernel-local buffers + fused_node_names = OrderedSet( + [n.get_name() for n in self.features.scheduler_nodes()] + ) + for name in self.store_buffer_names: + if not self.persistent.reads.get( + name + ) and V.graph.scheduler.can_buffer_be_removed_through_fusion( + name, fused_node_names + ): + self.persistent.remove(name) + if name not in self.must_keep_buffers: + # we can also remove this from the looped kernel + self.outside_loop.remove(name) + for loop in self.loops: + loop.remove(name) + + if not self.loops[-1]: + self.loops.pop() # for pointwise ops + + def scope(self, dep: MemoryDep) -> MemoryEstimate: + """Determine how a read/write should be categorized""" + if self.inside_reduction and ( + self.has_reduction_var(dep.index) or dep.is_indirect() + ): + return self.loops[-1] + return self.outside_loop + + def has_reduction_var(self, index: sympy.Expr) -> bool: + for sym in self.symbols[-self.num_reductions_dims :]: + if isinstance(sym, sympy.Symbol) and sym in index.free_symbols: + return True + return False + + def set_ranges(self, *lengths: list[list[sympy.Expr]]) -> list[list[sympy.Expr]]: + assert len(self.kernel_sizes) == len(lengths) + return [ + self.make_flat_range(sym, numel, length) + for sym, numel, length in zip(self.symbols, self.kernel_sizes, lengths) + ] + + @staticmethod + def make_flat_range( + sym: sympy.Symbol, numel: sympy.Expr, lengths: list[sympy.Expr] + ) -> list[sympy.Expr]: + if len(lengths) == 1 and numel == lengths[0]: + return [sym] + divisor = sympy.S.One + itervars = [] + for length in reversed(lengths): + if V.graph.sizevars.statically_known_equals(divisor * length, numel): + expr = FloorDiv(sym, divisor) + else: + expr = ModularIndexing(sym, divisor, length) + itervars.append(expr) + divisor = divisor * length + return [*reversed(itervars)] + + +@dataclasses.dataclass +class MemoryEstimate: + """Tracks the memory usage of a single loop in the generated kernel""" + + reads: dict[str, OrderedSet[MemoryDep]] = dataclasses.field( + default_factory=functools.partial(collections.defaultdict, OrderedSet) + ) + writes: dict[str, OrderedSet[MemoryDep]] = dataclasses.field( + default_factory=functools.partial(collections.defaultdict, OrderedSet) + ) + + def remove(self, name: str) -> None: + self.reads.pop(name, None) + self.writes.pop(name, None) + + def __bool__(self) -> bool: + return bool(self.reads or self.writes) + + def __repr__(self) -> str: + return f"""MemoryEstimate( + reads={[*itertools.chain.from_iterable(self.reads.values())]!r}, + writes={[*itertools.chain.from_iterable(self.writes.values())]!r} + )""" + + +@dataclasses.dataclass +class StatsForDim: + """Memory usage stats for a block dimension in the generated kernel (different from user dimensions)""" + + # the number of load/store ops + count_per_thread_contiguous: int = 0 + count_per_thread_broadcast: int = 0 + count_per_thread_non_contiguous: int = 0 # excludes broadcast + + # total bytes in each load/store op for a single element + bytes_per_thread_contiguous: int = 0 + bytes_per_thread_broadcast: int = 0 + bytes_per_thread_non_contiguous: int = 0 # excludes broadcast + + # total bytes read by entire kernel + bytes_contiguous_or_broadcast: sympy.Expr = sympy.S.Zero + bytes_non_contiguous: sympy.Expr = sympy.S.Zero + + def __add__(self, other: typing.Self) -> StatsForDim: + return StatsForDim( + count_per_thread_contiguous=self.count_per_thread_contiguous + + other.count_per_thread_contiguous, + count_per_thread_broadcast=self.count_per_thread_broadcast + + other.count_per_thread_broadcast, + count_per_thread_non_contiguous=self.count_per_thread_non_contiguous + + other.count_per_thread_non_contiguous, + bytes_per_thread_contiguous=self.bytes_per_thread_contiguous + + other.bytes_per_thread_contiguous, + bytes_per_thread_broadcast=self.bytes_per_thread_broadcast + + other.bytes_per_thread_broadcast, + bytes_per_thread_non_contiguous=self.bytes_per_thread_non_contiguous + + other.bytes_per_thread_non_contiguous, + bytes_contiguous_or_broadcast=self.bytes_contiguous_or_broadcast + + other.bytes_contiguous_or_broadcast, + bytes_non_contiguous=self.bytes_non_contiguous + other.bytes_non_contiguous, + ) + + @property + def count_per_thread(self) -> int: + return ( + self.count_per_thread_contiguous + + self.count_per_thread_broadcast + + self.count_per_thread_non_contiguous + ) + + @property + def bytes_per_thread(self) -> int: + return ( + self.bytes_per_thread_contiguous + + self.bytes_per_thread_broadcast + + self.bytes_per_thread_non_contiguous + ) + + @property + def bytes(self) -> sympy.Expr: + return self.bytes_contiguous_or_broadcast + self.bytes_non_contiguous + + @property + def contiguous_score(self) -> float: + return 1.0 - self.count_per_thread_non_contiguous / max( + self.count_per_thread, 1 + ) + + +@dataclasses.dataclass +class StatsForLoop: + """Memory usage stats for single loop in the generated kernel""" + + # load/store ops + count_per_thread: int = 0 + bytes_per_thread: int = 0 + + def __add__(self, other: typing.Self) -> StatsForLoop: + return StatsForLoop( + count_per_thread=self.count_per_thread + other.count_per_thread, + bytes_per_thread=self.bytes_per_thread + other.bytes_per_thread, + ) + + +@dataclasses.dataclass +class StatsForReadsOrWrites: + """Memory usage stats that are collected for reads/writes/both""" + + dim: list[StatsForDim] + loop: list[StatsForLoop] + # total bytes contiguous in any dimension + bytes_contiguous_or_broadcast: sympy.Expr = sympy.S.Zero + bytes_non_contiguous: sympy.Expr = sympy.S.Zero + + def __add__(self, other: typing.Self) -> StatsForReadsOrWrites: + assert len(self.dim) == len(other.dim) + assert len(self.loop) == len(other.loop) + return StatsForReadsOrWrites( + dim=[a + b for a, b in zip(self.dim, other.dim)], + loop=[a + b for a, b in zip(self.loop, other.loop)], + bytes_contiguous_or_broadcast=self.bytes_contiguous_or_broadcast + + self.bytes_contiguous_or_broadcast, + bytes_non_contiguous=self.bytes_non_contiguous + other.bytes_non_contiguous, + ) + + @property + def count_per_thread(self) -> int: + return self.dim[0].count_per_thread + + @property + def bytes_per_thread(self) -> int: + return self.dim[0].bytes_per_thread + + @property + def bytes(self) -> sympy.Expr: + return self.bytes_contiguous_or_broadcast + self.bytes_non_contiguous + + @classmethod + def compute( + cls, + loop_deps: list[dict[str, OrderedSet[MemoryDep]]], + index_symbols: list[sympy.Symbol], + ) -> typing.Self: + ndim = len(index_symbols) + result = cls(dim := [StatsForDim() for _ in range(ndim)], []) + for dep_group in loop_deps: + result.loop.append(loop_stats := StatsForLoop()) + for name, deps in dep_group.items(): + assert deps + contiguous_or_broadcast = [True] * ndim + numel = sympy.S.Zero + itemsize = V.graph.get_dtype(name).itemsize + loop_stats.count_per_thread += len(deps) + loop_stats.bytes_per_thread += itemsize * len(deps) + for dep in deps: + strides: list[sympy.Expr] = V.graph.sizevars.stride_vars( + dep.index, index_symbols + ) + for i in range(ndim): + if V.graph.sizevars.statically_known_equals(strides[i], 1): + dim[i].count_per_thread_contiguous += 1 + dim[i].bytes_per_thread_contiguous += itemsize + elif ( + V.graph.sizevars.statically_known_equals(strides[i], 0) + and not dep.is_indirect() + ): + dim[i].count_per_thread_broadcast += 1 + dim[i].bytes_per_thread_broadcast += itemsize + else: + dim[i].count_per_thread_non_contiguous += 1 + dim[i].bytes_per_thread_non_contiguous += itemsize + contiguous_or_broadcast[i] = False + numel += dep.get_numel() + if len(deps) > 1: + # can't read more elements than exist in the buffer + numel = sympy.Min(numel, V.graph.get_numel(name)) + nbytes = numel * itemsize + for i in range(ndim): + if contiguous_or_broadcast[i]: + dim[i].bytes_contiguous_or_broadcast += nbytes + else: + dim[i].bytes_non_contiguous += nbytes + if any(contiguous_or_broadcast): + result.bytes_contiguous_or_broadcast += nbytes + else: + result.bytes_non_contiguous += nbytes + if len(result.loop) > 1: + # the first loop represent the "outside of the loop" compute which could be long lived + result.loop = [result.loop[0] + x for x in result.loop[1:]] + return result + + +@dataclasses.dataclass +class StatsForKernelType: + """Memory usage stats that are collected for both persistent and looped kernels""" + + reads: StatsForReadsOrWrites + writes: StatsForReadsOrWrites + memory: StatsForReadsOrWrites + + @classmethod + def compute( + cls, loops: list[MemoryEstimate], estimator: MemoryEstimator + ) -> typing.Self: + reads = StatsForReadsOrWrites.compute( + [loop.reads for loop in loops], estimator.symbols + ) + writes = StatsForReadsOrWrites.compute( + [loop.writes for loop in loops], estimator.symbols + ) + return cls( + reads=reads, + writes=writes, + memory=reads + writes, + ) + + +@dataclasses.dataclass +class MemoryStats: + """Memory usage stats collected for each generated kernel""" + + persistent: StatsForKernelType + looped: StatsForKernelType + + def get(self, persistent: bool) -> StatsForKernelType: + return self.persistent if persistent else self.looped + + @classmethod + def compute(cls, estimator: MemoryEstimator) -> typing.Self: + persistent = StatsForKernelType.compute([estimator.persistent], estimator) + if len(estimator.loops) == 1 and not ( + estimator.outside_loop and estimator.loops[0] + ): + looped = persistent # loops/persistent is the same in this common case + else: + looped = StatsForKernelType.compute( + [estimator.outside_loop, *estimator.loops], estimator + ) + return cls( + persistent=persistent, + looped=looped, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/subgraph.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/subgraph.py new file mode 100644 index 0000000000000000000000000000000000000000..167891ba04dede3d4eb2e26438df63d7c9fc6218 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/subgraph.py @@ -0,0 +1,605 @@ +import contextlib +import functools +import itertools +import logging +from collections.abc import Callable +from typing import Any + +import sympy + +import torch +import torch._inductor.config as config +from torch._dynamo.utils import counters +from torch._inductor import ir +from torch._inductor.autotune_process import ( + SubgraphCPUBenchmarkRequest, + SubgraphGPUBenchmarkRequest, + TensorMeta, +) +from torch._inductor.codegen.common import KernelTemplate +from torch._inductor.ir import ( + Buffer, + FixedLayout, + get_free_symbols, + get_symbolic_inputs, + gm_original_output_strides, + ir_node_to_tensor, + Layout, +) +from torch._inductor.runtime.benchmarking import benchmarker +from torch._inductor.utils import do_bench_using_profiling +from torch._inductor.virtualized import V +from torch.utils._ordered_set import OrderedSet + + +log = logging.getLogger(__name__) + + +def inline_subgraph_to_ir_nodes( + gm: torch.fx.GraphModule, inputs: list[Any], name: str +) -> Any: + """Inline a subgraph by converting its FX operations to individual IR nodes. + + This converts a subgraph to multiple ComputedBuffer nodes (fusable), + enabling epilogue fusion with subsequent operations. + + Returns: + TensorBox containing the final operation result as individual IR nodes + """ + from torch._inductor.lowering import process_subgraph_nodes + + # Temporarily switch V.graph.module to subgraph during processing; restore to prevent IR nodes added to wrong graph + original_module = V.graph.module + try: + V.graph.module = gm + return process_subgraph_nodes(gm, inputs) + finally: + V.graph.module = original_module + + +class SubgraphChoiceCaller(ir.ChoiceCaller): + """ + Represents a Subgraph Autotuning choice, and the subgraph can be any arbitrary + GraphModule. Compiles the Subgraph down to a module for benchmarking. + """ + + def __init__( + self, + name: str, + input_nodes: list[Buffer], + layout: Layout, + description: str, + make_fx_graph: Callable[..., Any], + input_gen_fns: dict[int, Callable[[Any], torch.Tensor]] | None = None, + ) -> None: + super().__init__(name, input_nodes, layout, description) + + # Create inputs for tracing and benchmarking: + # - trace_inputs: Always use symbolic inputs for tracing so the graph works + # for any size at runtime. This correctly captures shape-dependent operations + # like x * x.shape[0]. + # - benchmark_inputs: Use input_gen_fns if provided (for range-specific benchmarks) + # or concrete inputs from ir_node_to_tensor with guard_shape=False. + trace_inputs = [] + self.benchmark_inputs = [] + with V.fake_mode: + for i, inp in enumerate(self.input_nodes): + # Here there will be no unbacked symbols, as SubgraphBuffer does not support them + assert len(get_free_symbols(inp.get_size(), unbacked_only=True)) == 0 + assert len(get_free_symbols(inp.get_stride(), unbacked_only=True)) == 0 + + inp.data.freeze_layout() # type: ignore[attr-defined] + + # Always use symbolic inputs for tracing + trace_inputs.append(ir_node_to_tensor(inp)) + + # Use input_gen_fn for benchmarking if provided, otherwise concrete sizes + if input_gen_fns is not None and i in input_gen_fns: + self.benchmark_inputs.append(input_gen_fns[i](inp)) + else: + self.benchmark_inputs.append( + ir_node_to_tensor(inp, replace_symbols_with_hints=True) + ) + + # Trace with symbolic inputs for guard detection + self.gm = make_fx_graph(*trace_inputs) + gm_original_output_strides(self.gm) + # Store symbolic inputs for sym_input computation + self.example_inputs = trace_inputs + + self.sym_inputs = get_symbolic_inputs(self.input_nodes) + self.sym_input_values = self._compute_sym_input_values() + + # Cached decomposition info for range-based dispatch (set via cache_decomposition) + self.decomposition: Callable[..., Any] | None = None + self.decomposition_kwargs: dict[str, Any] = {} + # Config patches to apply during kernel codegen (e.g., coordinate_descent_tuning) + self.config_patches: dict[str, Any] = {} + # Cache compiled module to avoid recompiling on every benchmark call + self._compiled_module: Any = None + # Cache benchmark request for async autotuning + self._bmreq: ( + SubgraphGPUBenchmarkRequest | SubgraphCPUBenchmarkRequest | None + ) = None + + # Pre-compile only if using async pipelined autotuning + # Must happen in __init__ because compilation requires virtualized context (V.graph, V.debug) + if config.pipeline_max_autotune_gemm: + with V.fake_mode: + self._compiled_module = self._compile_for_benchmarking() + self._bmreq = self._create_benchmark_request() + + def _compute_sym_input_values(self) -> list[int]: + """Extract concrete dimension values for sym_inputs from benchmark_inputs. + + The compiled module expects symbolic dimension values as runtime arguments. + This maps each symbolic variable to its concrete value from the benchmark tensors. + Used for range based autotuning. + """ + sym_input_names = OrderedSet( + [s.name for s in self.sym_inputs if hasattr(s, "name")] + ) + + # Build mapping: symbolic dimension name -> concrete value + sym_name_to_value: dict[str, int] = {} + for inp_node, benchmark_inp in zip(self.input_nodes, self.benchmark_inputs): + if isinstance(benchmark_inp, torch.Tensor): + for sym_dim, actual_dim in zip( + inp_node.get_size(), benchmark_inp.shape + ): + if isinstance(sym_dim, sympy.Symbol): + sym_name_to_value[sym_dim.name] = int(actual_dim) + elif str(sym_dim) in sym_input_names: + sym_name_to_value[str(sym_dim)] = int(actual_dim) + + result = [] + for sym_var in self.sym_inputs: + if isinstance(sym_var, sympy.Symbol) and sym_var.name in sym_name_to_value: + result.append(sym_name_to_value[sym_var.name]) + else: + hint = V.graph.sizevars.shape_env.optimization_hint(sym_var, fallback=1) + result.append(int(hint)) + return result + + def cache_decomposition( + self, decomposition: Callable[..., Any], kwargs: dict[str, Any] + ) -> None: + """Cache decomposition function and kwargs for range-based dispatch lookup.""" + self.decomposition = decomposition + self.decomposition_kwargs = kwargs + + def __str__(self) -> str: + return f"SubgraphCaller({self.name})" + + def _compile_for_benchmarking(self) -> Any: + """Compile the subgraph for benchmarking, returns the compiled module.""" + from torch._inductor.graph import GraphLowering + + safe_name = self.name.replace("::", "_").replace(".", "_") + + # Symbolic inputs produce code with symbolic sizes resolved at runtime + # via sym_input_values. Without symbols (static shapes), compile with + # benchmark_inputs whose sizes match the actual benchmark tensors. + compile_inputs = ( + self.example_inputs if self.sym_inputs else self.benchmark_inputs + ) + log.debug("Benchmark compile %s: sym_inputs=%s", self.name, self.sym_inputs) + + assert self.gm is not None + bm_graph_lowering = GraphLowering( + gm=self.gm, + example_inputs=compile_inputs, + shape_env=V.graph._shape_env, + cpp_wrapper=V.graph.cpp_wrapper, + aot_mode=V.graph.aot_mode, + extern_node_serializer=V.graph.extern_node_serializer, + is_inference=V.graph.is_inference, + is_backward=V.graph.is_backward, + name=f"benchmark_{safe_name}", + ) + + for sym_inp in self.sym_inputs: + bm_graph_lowering.graph_inputs[sym_inp.name] = sym_inp + bm_graph_lowering.graph_input_names.append(sym_inp.name) + + with V.set_graph_handler(bm_graph_lowering): + # Apply config_patches during benchmarking (e.g., coordinate_descent_tuning) + # Also disable max_autotune to avoid nested autotuning + benchmark_config: dict[str, Any] = { + "max_autotune": False, + "max_autotune_gemm": False, + "max_autotune_gemm_backends": "ATEN", + "benchmark_fusion": False, + "pipeline_max_autotune_gemm": False, + **self.config_patches, + } + with config.patch(benchmark_config): + bm_graph_lowering.run(*compile_inputs) + return bm_graph_lowering.compile_to_module() + + def _create_benchmark_request( + self, + ) -> SubgraphGPUBenchmarkRequest | SubgraphCPUBenchmarkRequest: + """Create a benchmark request for async autotuning.""" + assert self._compiled_module is not None, ( + "Module must be compiled before creating benchmark request" + ) + input_tensor_meta = TensorMeta.from_irnodes(self.input_nodes) + output_tensor_meta = TensorMeta.from_irnodes(self.layout) + + if self.layout.device.type == "cpu": + bmreq_cls = SubgraphCPUBenchmarkRequest + else: + bmreq_cls = SubgraphGPUBenchmarkRequest + + return bmreq_cls( + kernel_name=self.name, + input_tensor_meta=input_tensor_meta, + output_tensor_meta=output_tensor_meta, + extra_args=tuple(), + module_path=self._compiled_module.__file__, + module_cache_key=self._compiled_module.key, + sym_input_values=self.sym_input_values, + ) + + @property + def bmreq( + self, + ) -> SubgraphGPUBenchmarkRequest | SubgraphCPUBenchmarkRequest: + """Benchmark request for async autotuning. Pre-compiled when pipeline_max_autotune_gemm is enabled.""" + assert self._bmreq is not None, ( + "bmreq accessed but pipeline_max_autotune_gemm was not enabled during __init__" + ) + return self._bmreq + + def _ensure_compiled(self) -> None: + """Ensure the module is compiled. Used for lazy compilation in non-async path.""" + if self._compiled_module is None: + self._compiled_module = self._compile_for_benchmarking() + + def benchmark(self, *args: list[Any], out: torch.Tensor) -> float: + """Regular benchmarking: compile if needed, then use benchmarker.""" + self._ensure_compiled() + bm_func = self._compiled_module.call + sym_inputs = self.sym_input_values + + def fn() -> Any: + return bm_func([*sym_inputs, *args]) + + if self._benchmark_with_cudagraphs: + return benchmarker.benchmark_gpu_with_cuda_graph(fn) + + if config.profile_bandwidth_with_do_bench_using_profiling: + return do_bench_using_profiling(fn) + return benchmarker.benchmark( + fn, + device=benchmarker.infer_device(*sym_inputs, *args), + ) + + def benchmark_collective(self, *args: list[Any], out: torch.Tensor) -> None: + """Run once for collective benchmarking (barrier sync handled by caller).""" + self._ensure_compiled() + self._compiled_module.call([*self.sym_input_values, *args]) + + def hash_key(self) -> str: + assert self.gm is not None + return "-".join( + [ + self.name.rsplit("_", 1)[0], + *[str(inp.get_size()) for inp in self.input_nodes], + *[str(inp.get_stride()) for inp in self.input_nodes], + str(self.gm.graph), + ] + ) + + def output_node(self) -> ir.TensorBox: + assert self.gm is not None + return ir.TensorBox.create( + ir.SubgraphBuffer( + layout=self.layout, + input_nodes=self.input_nodes, + gm=self.gm, + example_inputs=self.example_inputs, + subgraph_name=self.name, + config_patches=self.config_patches if self.config_patches else None, + ) + ) + + def info_dict(self) -> dict[str, Any]: + """Information returned here is logged to the autotune log file when that is enabled.""" + return { + "backend": "subgraph", + "kernel_name": self.name, + } + + def autoheuristic_id(self) -> str: + return f"subgraph_{self.name}" + + +class SubgraphTemplate(KernelTemplate): + """ + A template for subgraph evaluation to be used in autotuning. + + This class allows creating customized subgraphs that can be appended + as choices during the autotuning process, enabling the selection of + optimal implementations for complex operations. + """ + + index_counter = itertools.count() + + def __init__( + self, + name: str, + ): + """ + Initialize a subgraph template. + + Args: + name: The name of this template + graph: The FX graph + """ + super().__init__(name=name) + + def generate( # type: ignore[override] + self, + name: str, + input_nodes: list[Buffer], + layout: Layout, + make_fx_graph: Callable[..., Any], + description: str = "", + input_gen_fns: dict[int, Callable[[Any], torch.Tensor]] | None = None, + **kwargs: Any, + ) -> SubgraphChoiceCaller: + """ + Generate a SubgraphChoiceCaller instance for autotuning. + + Args: + name: The name for this subgraph choice + input_nodes: List of input nodes to the subgraph + layout: Memory layout information for the output + make_fx_graph: Callable that creates the FX graph for this subgraph + description: Optional description of this choice + input_gen_fns: Optional dict mapping input indices to tensor generators + **kwargs: Additional keyword arguments + + Returns: + SubgraphChoiceCaller: A callable object that can be used for autotuning + """ + + return SubgraphChoiceCaller( + name=f"{name}_{next(SubgraphTemplate.index_counter)}", + input_nodes=input_nodes, + layout=layout, + description=description, + make_fx_graph=make_fx_graph, + input_gen_fns=input_gen_fns, + ) + + def generate_custom_op_choices( + self, + name: str, + decompositions: list[Callable[..., Any]], + input_nodes: list[Buffer], + non_tensor_args: list[dict[str, Any]], + default_impl: Callable[..., Any] | None = None, + input_gen_fns: dict[int, Callable[[Any], torch.Tensor]] | None = None, + config_patches_list: list[dict[str, Any]] | None = None, + ) -> list[SubgraphChoiceCaller]: + """ + Generate multiple SubgraphChoiceCaller instances for custom op autotuning. + + This method extends SubgraphTemplate to support custom op decompositions, + allowing multiple implementations to compete in autotuning. + + Args: + name: Base name for the choices + decompositions: List of decomposition functions to compete in autotuning + input_nodes: List of tensor inputs. All tensor arguments must be passed here. + non_tensor_args: List of non-tensor kwargs only, one dict per corresponding decomposition. + default_impl: Default implementation for layout inference + input_gen_fns: Optional dict mapping input indices to tensor generators + config_patches_list: Optional list of config patches per decomposition + + Returns: + List of SubgraphChoiceCaller instances for autotuning + """ + if not decompositions: + return [] + + assert len(decompositions) == len(non_tensor_args), ( + f"decompositions and non_tensor_args must have same length, " + f"got {len(decompositions)} decompositions and {len(non_tensor_args)} kwargs" + ) + + # Default to empty config_patches if not provided + if config_patches_list is None: + config_patches_list = [{} for _ in decompositions] + + # Infer layouts and ensure layout consistency for fair autotuning comparison + layouts = [ + self._infer_custom_op_layout( + input_nodes, decomp, kwargs, default_impl, input_gen_fns + ) + for decomp, kwargs in zip(decompositions, non_tensor_args) + ] + + # Validate all decompositions produce equivalent layouts for fair comparison + self._validate_layout_equivalence(name, decompositions, layouts) + layout = layouts[0] # All layouts are now validated to be equivalent + + choices: list[SubgraphChoiceCaller] = [] + for decomp, decomp_kwargs, config_patches in zip( + decompositions, non_tensor_args, config_patches_list + ): + # Create make_fx_graph function for this decomposition + # Uses error_on_new_guards to detect impls that add guards + from torch.fx.experimental.symbolic_shapes import _ShapeEnvGuardError + + def make_fx_graph( + *args: Any, + decomp: Callable[..., Any] = decomp, + decomp_kwargs: dict[str, Any] = decomp_kwargs, + ) -> Any: + # decomp_kwargs contains all merged parameters: CustomOpConfig params + runtime kwargs + + from torch.fx.experimental.proxy_tensor import make_fx + + from ..decomposition import select_decomp_table + + decomposition_table = select_decomp_table() + shape_env = V.fake_mode.shape_env + + # Use error_on_new_guards to detect impls that add guards during tracing + guard_ctx = ( + shape_env.error_on_new_guards() + if shape_env is not None + else contextlib.nullcontext() + ) + with guard_ctx: + return make_fx( + functools.partial(decomp, **decomp_kwargs), + decomposition_table=decomposition_table, + tracing_mode="symbolic", + )(*args) + + # Generate descriptive name for this variant + variant_name = self._generate_variant_name(decomp, decomp_kwargs) + + # Try to create choice; skip if it adds guards + try: + choice = self.generate( + name=f"{name}_{variant_name}", + input_nodes=input_nodes, + layout=layout, + make_fx_graph=make_fx_graph, + description=f"CustomOp {decomp.__name__}", + input_gen_fns=input_gen_fns, + ) + except _ShapeEnvGuardError: + log.info( + "Skipping decomposition %s: adds guards during tracing", + decomp.__name__, + ) + counters["inductor"]["custom_op_decomp_guard_skips"] += 1 + continue + + # Cache decomposition info for range-based dispatch + choice.cache_decomposition(decomp, decomp_kwargs) + # Store config_patches for this choice + choice.config_patches = config_patches + choices.append(choice) + + return choices + + def _generate_variant_name( + self, decomp: Callable[..., Any], kwargs: dict[str, Any] + ) -> str: + """Generate a descriptive name for a decomposition variant with its parameters.""" + import re + + base_name = decomp.__name__ + if not kwargs: + return base_name + + def sanitize_value(v: Any) -> str: + """Convert a value to a valid Python identifier component.""" + s = str(v) + # Replace invalid characters with underscores + s = re.sub(r"[^a-zA-Z0-9_]", "_", s) + # Ensure it doesn't start with a digit + if s and s[0].isdigit(): + s = "_" + s + return s + + param_suffix = "_".join( + f"{k}_{sanitize_value(v)}" for k, v in sorted(kwargs.items()) + ) + return f"{base_name}_{param_suffix}" + + def _validate_non_tensor_kwargs(self, kwargs: dict[str, Any]) -> None: + """Validate that kwargs contains only non-tensor arguments.""" + for key, value in kwargs.items(): + assert not isinstance(value, (torch.Tensor, Buffer)), ( + f"kwargs['{key}'] contains tensor {type(value)}. " + f"Tensor arguments should be in input_nodes, not kwargs. " + f"Only scalar/non-tensor parameters should be in kwargs." + ) + + def _validate_layout_equivalence( + self, + op_name: str, + decompositions: list[Callable[..., Any]], + layouts: list[Layout], + ) -> None: + """Ensure all layouts have consistent stride, device, dtype, and sizes for fair autotuning.""" + if not layouts: + return + + reference = layouts[0] + for i, layout in enumerate(layouts[1:], start=1): + if (layout.device, layout.dtype, layout.size, layout.stride) != ( + reference.device, + reference.dtype, + reference.size, + reference.stride, + ): + raise AssertionError( + f"Layout mismatch in custom op '{op_name}': " + f"decomposition '{decompositions[i].__name__}' produces " + f"({layout.device}, {layout.dtype}, {layout.size}, {layout.stride}) " + f"but '{decompositions[0].__name__}' produces " + f"({reference.device}, {reference.dtype}, {reference.size}, {reference.stride})" + ) + + def _infer_custom_op_layout( + self, + input_nodes: list[Buffer], + function_decomposition: Callable[..., Any], + kwargs: dict[str, Any], + default_impl: Callable[..., Any] | None = None, + input_gen_fns: dict[int, Callable[[Any], torch.Tensor]] | None = None, + ) -> Layout: + """Infer output layout for custom ops using the default implementation when available. + Note that the Subgraph assumes custom ops return exactly one tensor output. + TODO: Add support for multiple output custom ops. + """ + import functools + + from torch._inductor.virtualized import V + + # Assert kwargs contain only non-tensor arguments + self._validate_non_tensor_kwargs(kwargs) + + with V.fake_mode: + example_inputs = [] + for i, inp in enumerate(input_nodes): + if input_gen_fns and i in input_gen_fns: + fake_tensor = input_gen_fns[i](inp) + else: + raw_shape = inp.get_size() + concrete_shape = V.graph.sizevars.optimization_hints(raw_shape) + raw_stride = inp.get_stride() + concrete_stride = V.graph.sizevars.optimization_hints(raw_stride) + fake_tensor = torch.empty_strided( + concrete_shape, + concrete_stride, + dtype=inp.get_dtype(), + device=inp.get_device(), + ) + example_inputs.append(fake_tensor) + + fn = functools.partial(function_decomposition, **kwargs) + output = fn(*example_inputs) + + # Assert single output + assert isinstance(output, torch.Tensor), ( + f"Expected single tensor output, got {type(output)}. " + f"Multi-output custom ops not yet supported in autotuning." + ) + + return FixedLayout( + device=output.device, + dtype=output.dtype, + size=output.shape, + stride=output.stride(), + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/triton.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/triton.py new file mode 100644 index 0000000000000000000000000000000000000000..58a71b789f030671141d17c8407ce5ba02e2512d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/triton.py @@ -0,0 +1,6960 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import ast +import collections +import contextlib +import copy +import dataclasses +import functools +import itertools +import logging +import math +import operator +import os +import textwrap +from abc import abstractmethod +from collections.abc import Callable, Iterable, Sequence +from functools import lru_cache +from typing import Any, cast, TYPE_CHECKING, TypeVar + +import sympy +from sympy.printing.precedence import PRECEDENCE + +import torch +import torch._logging +import torch.utils._pytree as pytree +from torch._dynamo.device_interface import get_interface_for_device +from torch._dynamo.utils import identity, preserve_rng_state +from torch._prims_common import is_integer_dtype, type_to_dtype +from torch.utils._ordered_set import OrderedSet +from torch.utils._sympy.functions import ( + CeilDiv, + FloorDiv, + ModularIndexing, + TruncToFloat, + TruncToInt, +) +from torch.utils._triton import ( + get_triton_version, + has_triton_package, + has_triton_stable_tma_api, +) + +from ...utils._sympy.symbol import free_symbol_is_type, prefix_str, symbol_is_type, SymT +from ...utils._sympy.value_ranges import ValueRanges +from .. import config, ir, metrics, utils +from ..async_compile import AsyncCompile +from ..codecache import code_hash, get_path, PyCodeCache, write_atomic +from ..debug import set_kernel_post_grad_provenance_tracing +from ..ops_handler import DefaultHandler +from ..runtime import triton_heuristics +from ..runtime.benchmarking import benchmarker +from ..runtime.hints import ( + AutotuneHint, + DeviceProperties, + ReductionHint, + TRITON_MAX_BLOCK, + TRITON_MAX_RSPLIT, +) +from ..runtime.runtime_utils import get_max_y_grid, next_power_of_2 +from ..scheduler import ( + BaseSchedulerNode, + FusedExternTritonKernelSchedulerNode, + FusedSchedulerNode, + Scheduler, + SchedulerNode, +) +from ..shape_propagation import get_broadcasted_shape +from ..stream_utils import get_raw_stream_name +from ..utils import ( + cache_on_self, + DelayReplaceLine, + get_bounds_index_expr, + get_fused_kernel_name, + get_kernel_metadata, + is_welford_reduction, + Placeholder, + prefix_is_reduction, + sympy_dot, + sympy_product, + sympy_subs, + triton_type, + triton_version_uses_attrs_dict, + upcast_compute_type, +) +from ..virtualized import _ops as ops, ReductionType, StoreMode, V +from ..wrapper_benchmark import get_kernel_category_by_source_code +from .block_analysis import BlockPatternMatcher +from .common import ( + ArgName, + BackendFeature, + ConstexprArg, + CSE, + CSEVariable, + DeferredLine, + IndentedBuffer, + InplacedBuffer, + is_buffer_removed, + OpOverrides, + PythonPrinter, + RemovedArg, + SizeArg, + TensorArg, + WorkspaceArg, + WorkspaceZeroMode, +) +from .simd import ( + constant_repr, + IterationRanges, + IterationRangesEntry, + IterationRangesRoot, + PartialAccumulate, + SIMDKernel, + SIMDScheduling, +) +from .triton_utils import ( + config_of, + equal_1_arg_indices, + non_constexpr_signature, + should_unwrap_unspec_arg, + signature_to_meta, +) +from .wrapper import SymbolicCallArg + + +if TYPE_CHECKING: + from types import ModuleType + + from torch._inductor.dtype_propagation import DtypePropagationOpsHandler + from torch.fx.experimental.symbolic_shapes import ShapeEnv + + from ..ir import IRNode + from .common import BlockShapeType + from .simd_kernel_features import SIMDKernelFeatures + + _T = TypeVar("_T") + +log = logging.getLogger(__name__) +perf_hint_log = torch._logging.getArtifactLogger(__name__, "perf_hints") +schedule_log = torch._logging.getArtifactLogger(__name__, "schedule") +fusion_log = torch._logging.getArtifactLogger(__name__, "fusion") +async_compile = AsyncCompile() + +# Threshold for detecting inner reductions based on tiling score ratio. +# If r0_tiling_score / x_tiling_score >= this value, upgrade DEFAULT hint to INNER. +INNER_REDUCTION_RATIO_THRESHOLD = 8 + + +def get_triton_reduction_function(reduction_type): + use_helper = reduction_type in ("any", "max", "min", "prod") + module = "triton_helpers" if use_helper else "tl" + if reduction_type in ("max", "min"): + return f"{module}.{reduction_type}2" + else: + return f"{module}.{reduction_type}" + + +def is_sympy_integer_like(expr: object): + """ " + Is this expression a Sympy Integer or is it an integer sympy Expr + containing no free symbols. The latter case can happen with Identity expr. + """ + if not isinstance(expr, sympy.Expr): + return False + return isinstance(expr, sympy.Integer) or ( + expr.is_integer and len(expr.free_symbols) == 0 + ) + + +def _materialize_trunc_to_float_expr( + expr: sympy.Expr, dtype: torch.dtype +) -> sympy.Expr: + if not dtype.is_floating_point or not expr.has(TruncToInt): + return expr + + # Preserve float truncation semantics when materializing symbolic scalars + # into floating tensors. Casting to the kernel index dtype first can + # overflow before the requested floating-point conversion happens. Only + # rewrite truncations that are already participating in floating-point + # computation; integer subexpressions and predicates must keep exact + # integer semantics until the final materialization cast. + if expr.func is TruncToInt: + return TruncToFloat(*expr.args) + + def is_predicate_expr(node: sympy.Basic) -> bool: + return bool( + getattr(node, "is_Boolean", False) or getattr(node, "is_Relational", False) + ) + + def rewrite_float_subexpr(node: sympy.Expr) -> sympy.Expr: + if not node.has(TruncToInt): + return node + if node.func is TruncToInt: + return TruncToFloat(*node.args) + if is_predicate_expr(node) or node.is_integer: + return node + + new_args = tuple( + rewrite_float_subexpr(arg) + if isinstance(arg, sympy.Expr) and not is_predicate_expr(arg) + else arg + for arg in node.args + ) + if new_args == node.args: + return node + return node.func(*new_args) + + return rewrite_float_subexpr(expr) + + +class OpDtypeSupport: + """ + Some Triton ops such as libdevice and tl.math only support float32 and float64. + This class records which dtypes are supported by specific IR ops. + """ + + supported_dtypes: dict[str, OrderedSet[torch.dtype]] = {} + convert_outputs: dict[str, bool] = {} + + @classmethod + def register_upcast(cls, func: Callable[..., str], convert_output: bool) -> None: + op_name = func.__name__ + cls.supported_dtypes[op_name] = OrderedSet([torch.float32, torch.float64]) + cls.convert_outputs[op_name] = convert_output + + +@lru_cache(None) +def gen_attr_descriptor_import() -> str: + """ + import AttrsDescriptor if the triton version is new enough to have this + class defined. + """ + if not has_triton_package(): + return "" + + import triton.compiler.compiler + + # Note: this works because triton.compiler.compiler imports AttrsDescriptor from triton.backends.compiler + # When support for the legacy AttrsDescriptor is removed then this import path should be changed. + if hasattr(triton.compiler.compiler, "AttrsDescriptor"): + return "from triton.compiler.compiler import AttrsDescriptor" + else: + return "" + + +class TritonSymbols: + """ + Stores sympy.Symbol instances and constants associated with triton codegen. + """ + + reduction_types = OrderedSet([SymT.R0_INDEX, SymT.R1_INDEX]) + block_types = OrderedSet([SymT.XBLOCK, SymT.YBLOCK, SymT.ZBLOCK, *reduction_types]) + + block_offsets = { + symt: sympy.Symbol(f"{prefix_str[symt]}offset", integer=True, nonnegative=True) + for symt in block_types + } + + block_sizes = { + symt: sympy.Symbol( + f"{prefix_str[symt].upper()}BLOCK", integer=True, positive=True + ) + for symt in block_types + } + + @classmethod + def get_block_shape(cls, expr: sympy.Expr) -> BlockShapeType: + # return block shape of sympy Expression + # e.g., + # tmp13 = y1 + # tmp14 = x0 - tmp13 + # + # get_block_shape(y1) = (YBLOCK,1,1) + # get_block_shape(x0-tmp13) = (YBLOCK,XBLOCK,1) + + expr_shape: BlockShapeType = () + expr_vars = expr.free_symbols + for var in expr_vars: + if symbol_is_type(var, SymT.TMP): + cse_var = V.kernel.cse.varname_map[var.name] + var_shape = cse_var.shape + elif symbol_is_type( + var, + ( + SymT.UNBACKED_INT, + SymT.SIZE, + SymT.PRECOMPUTED_SIZE, + SymT.INDEX, + SymT.FLOAT, + SymT.UNBACKED_FLOAT, + ), + ): + var_shape = () + else: + symbol_matches = [ + symt for symt in cls.block_types if symbol_is_type(var, symt) + ] + assert len(symbol_matches) == 1, f"Ambiguous type: {var.name}" + + sym = symbol_matches[0] + ndim = V.kernel.triton_tensor_ndim() + shape = ["1"] * ndim + + tree_match = [ + tree + for tree in V.kernel.active_range_trees() + if prefix_str[sym] == tree.prefix + ] + assert len(tree_match) == 1, "# of Match expected to 1" + + shape[tree_match[0].tensor_dim] = str(cls.get_block_size(tree_match[0])) + var_shape = tuple(shape) + + # Union current variable shape + expr_shape = get_broadcasted_shape(expr_shape, var_shape) + + assert expr_shape is not None + + return expr_shape + + @classmethod + def get_block_size(cls, tree: IterationRanges) -> sympy.Symbol: + return cls.block_sizes[tree.symt] + + @classmethod + def get_block_offset(cls, tree: IterationRanges) -> sympy.Symbol: + return cls.block_offsets[tree.symt] + + +@dataclasses.dataclass +class IndexingOptions: + index_str: str + mask_vars: OrderedSet[str] + expand_str: str | None + _has_rindex: bool + index: sympy.Expr + expand_shape: Sequence[int | str] | None + + def has_mask(self) -> bool: + return bool(self.mask_vars) + + def has_indirect(self) -> bool: + return free_symbol_is_type(self.index, SymT.TMP) + + def has_rindex(self) -> bool: + return self._has_rindex + + def has_tmpmask(self) -> bool: + return any(str(mask).startswith("tmp") for mask in self.mask_vars) + + def has_rmask(self) -> bool: + return any(str(mask).startswith("r") for mask in self.mask_vars) + + @property + def mask_str(self) -> str: + # The sorted call is added to make sure the order is still + # deterministic if self.mask_vars contains mix of string + # and TritonCSEVariable + return ( + " & ".join(sorted(map(str, self.mask_vars))) if self.mask_vars else "None" + ) + + +@dataclasses.dataclass +class BlockDescriptorOptions: + """ + This is a base class that describes a block descriptor used in Triton kernels. + It can be used to create either a tensor descriptor (with TensorDescriptorOptions) + or a block pointer (with BlockPtrOptions). + """ + + params: BlockParameters + constant_offset: sympy.Expr + order: list[int] + mask_vars: OrderedSet[str] + broadcast_shape: Sequence[sympy.Expr] + broadcasting_dims: list[bool] + final_shape: Sequence[sympy.Expr] + # If the BlockParameters have been sorted using a particular stride order + # transpose load / store blocks at runtime using the information in + # stride_sorter. + stride_sorter: BlockParameters.StrideSorter + _boundary_check: list[int] | None = None + # Can we safely lift the constructor + # to the top of the kernel? + can_lift: bool = False + + @property + def shape(self) -> list[sympy.Expr]: + return self.params.shape + + @property + def block_shape(self) -> list[sympy.Expr]: + return self.params.block_shape + + @property + def strides(self) -> list[sympy.Expr]: + return self.params.strides + + @property + def offsets(self) -> list[sympy.Expr]: + return self.params.offsets + + @classmethod + def create( + cls, + *, + params: BlockParameters, + constant_offset: sympy.Expr, + range_trees: list[IterationRangesRoot], + mask_vars: OrderedSet[str], + get_max_block: Callable[[str], int], + stride_sorter_cls: type[BlockParameters.StrideSorter], + can_lift: bool = False, + ) -> BlockDescriptorOptions: + """Helper to create a BlockDescriptorOptions instance""" + + sizevars = V.graph.sizevars + + def lookup_size(exprs: Iterable[sympy.Expr]) -> list[sympy.Expr]: + return [sizevars.lookup_precomputed_size(expr) for expr in exprs] + + # Look up precomputed sizes + params.shape = lookup_size(params.shape) + params.strides = lookup_size(params.strides) + + # Strip out dimensions of size 1. + # Size 1 dimensions are redundant since the triton kernel shape + # will be e.g. [YBLOCK, XBLOCK], so tl.reshape would just remove these + # dimensions anyway + singleton_dims = [ + sizevars.statically_known_equals(dim, 1) for dim in params.block_shape + ] + if all(singleton_dims): + # Handle a pure singletons, e.g. [1, 1] + singleton_dims[-1] = False + + # Drop singleton dimensions from the block descriptor. + params = params.remove_dims(singleton_dims) + + # Maybe reorder dimensions based on strides + # with tl.trans applied at load / store time + params, stride_sorter = params.maybe_sort_with_stride_order( + stride_sorter_cls=stride_sorter_cls, shape_env=V.graph._shape_env + ) + + # Strip out dimensions of stride 0. + # These will be restored with tl.broadcast_to. + broadcasting_dims = [ + sizevars.statically_known_equals(stride, 0) for stride in params.strides + ] + + # Record the post-broadcast shape before broadcasting dims are removed. + # The pre-broadcast shape is identical to this, except broadcasting dims are + # replaced with 1. + broadcast_shape = params.block_shape + + # Drop broadcasting dims from the block descriptor. + params = params.remove_dims(broadcasting_dims) + + # Compute the final shape, adjusting for special kernel types. + final_shape = [TritonSymbols.get_block_size(tree) for tree in range_trees] + if V.kernel.no_x_dim: + assert range_trees[0].prefix == "x" + final_shape.pop(0) + + reduction_ndim = V.kernel.num_reduction_dims + if ( + not V.kernel.inside_reduction + and len(params.strides) == len(V.kernel.numels) - reduction_ndim + and V.kernel.features.is_reduction() + ): + # Need to expand rank to match the rank used inside the reduction loop + final_shape += [sympy.S.One] * reduction_ndim + + try: + # Get permutation to sort strides in ascending order. + # This is used as the order argument in tl.make_block_ptr + order = utils.argsort_sym(V.graph._shape_env, params.strides) + except AssertionError: + # Symbolic shapes, failed to evaluate comparison expression + order = list(reversed(range(len(params.strides)))) + + result = cls( + params=params, + constant_offset=V.graph.sizevars.lookup_precomputed_size(constant_offset), + order=order, + mask_vars=mask_vars, + final_shape=final_shape, + broadcast_shape=broadcast_shape, + broadcasting_dims=broadcasting_dims, + stride_sorter=stride_sorter, + can_lift=can_lift, + ) + result.compute_boundary_check(get_max_block, range_trees) + return result + + def replace_offset( + self, expr: sympy.Expr, replacement: sympy.Expr, symt: SymT + ) -> sympy.Expr: + """ + Replaces instances of {symt}_offset with the new expression. + """ + roffset = TritonSymbols.block_offsets[symt] + return sympy_subs(expr, {roffset: replacement}) + + def remove_roffsets(self, expr: sympy.Expr) -> sympy.Expr: + for symt in TritonSymbols.reduction_types: + expr = self.replace_offset(expr, sympy.Integer(0), symt) + return expr + + def compute_boundary_check( + self, + get_max_block: Callable[[str], int], + range_trees: list[IterationRangesRoot], + ) -> None: + """List of indices to pass to tl.load(boundary_check=...)""" + sizevars = V.graph.sizevars + + # Substitute maximum block sizes in shape expressions. + # This works in multiple_of checks because block sizes are powers of 2. + block_to_max: dict[sympy.Expr, Any] = { + TritonSymbols.block_sizes[t.symt]: get_max_block(prefix_str[t.symt]) + for t in range_trees + } + + # Also see Note: Constant mask optimisation + # if ynumel / YBLOCK > max_ygrid, then the z dimension is used to handle + # the remaining programs that cannot fit into the y dimension. This means + # it's possible that more than the required number of programs are launched, + # possibly leading to out-of-bounds accesses. So even if ynumel divides YBLOCK, + # boundary checking is required in the dimensions that are based on YBLOCK + # e.g. for [YBLOCK // 16, YBLOCK, XBLOCK] dimensions 0 and 1 need boundary + # checks when max_ygrid is exceeded. + needs_overflow_grid = any(map(V.kernel.needs_yz_grid_overflow, range_trees)) + self._boundary_check = [ + idx + for idx in range(len(self.shape)) + if ( + not sizevars.statically_known_equals(self.strides[idx], sympy.S.Zero) + and ( + ( + needs_overflow_grid + and TritonSymbols.block_sizes[SymT.YBLOCK] + in self.block_shape[idx].free_symbols + ) + or ( + not sizevars.statically_known_multiple_of( + self.shape[idx], self.block_shape[idx] + ) + and not sizevars.statically_known_multiple_of( + self.shape[idx], + sympy_subs(self.block_shape[idx], block_to_max), + ) + ) + ) + and not ( + V.kernel.no_x_dim + and self.block_shape[idx] == TritonSymbols.block_sizes[SymT.XBLOCK] + ) + ) + ] + + def boundary_check(self) -> list[int]: + assert self._boundary_check is not None + return self._boundary_check + + def has_indirect(self) -> bool: + return False # block_ptr can't do indirect indexing + + def has_rindex(self) -> bool: + return any( + free_symbol_is_type(expr, TritonSymbols.reduction_types) + for expr in self.block_shape + ) + + def has_rmask(self) -> bool: + return self.has_rindex() + + def has_tmpmask(self) -> bool: + return False # block_ptr can't do indirect indexing + + def has_mask(self) -> bool: + return bool(self.boundary_check()) + + def codegen_broadcast_and_reshape( + self, + value: str, + initial_shape: Sequence[sympy.Expr], + final_shape: Sequence[sympy.Expr], + allow_implicit: bool, + for_store: bool, + ) -> str: + """ + Generate a broadcast and a reshape for the block descriptor. + This restores stride-0 dimensions which were removed from the block descriptor. + + Transposes are also applied to the input using self.stride_sorter: + if for_store is True: + - First Broadcast the value. Since self.broadcast_shape is stored in + descending stride order, it must be reverted to the original order + since the input value does not have dims with descending strides + - After, transpose the broadcasted value so that dimensions are in + descending stride order + - Finally reshape to the block shape + else (for load): + - First broadcast the value to self.broadcast_shape (strides are descending) + - Then transpose the value so that dimensions no longer have descending strides + - Finally reshape the block to the final kernel tile shape + """ + broadcast_shape = self.broadcast_shape + broadcasting_dims = self.broadcasting_dims + + # If the block parameters have been sorted by descending strides, + # permute the broadcasting parameters so that they are compatible + # with the value being stored. This is because the dimensions + # of the value being stored are not sorted in descending stride order, + # but the broadcasting parameters are based on the dims in sorted order + if for_store: + broadcast_shape = self.stride_sorter.revert(self.broadcast_shape) + broadcasting_dims = self.stride_sorter.revert(self.broadcasting_dims) + + # Reshape to add singletons. + pre_broadcast_shape = [ + sympy.S.One if is_broadcasting else dim + for dim, is_broadcasting in zip(broadcast_shape, broadcasting_dims) + ] + value = triton_reshape(value, initial_shape, pre_broadcast_shape) + + if ( + not self.stride_sorter.is_identity + and not for_store + and len(pre_broadcast_shape) == len(final_shape) + ): + # If all we need to do is transpose to match the final shape + # with implicit broadcasting then we don't need an explicit broadcast + # unless the caller requests it. So just test implicit broadcast support + # with the transposed pre broadcast shape + pre_broadcast_shape = self.stride_sorter.revert(pre_broadcast_shape) + + # Broadcast singletons. + # For loads, we can often implicitly broadcast singleton dimensions. + # We need an explicit broadcast for stores, or if the final reshape does more + # than add singletons. + sizevars = V.graph.sizevars + supports_implicit_broadcast = allow_implicit and ( + len(pre_broadcast_shape) == len(final_shape) + and all( + sizevars.statically_known_equals(pre_dim, 1) + or sizevars.statically_known_equals(pre_dim, post_dim) + for pre_dim, post_dim in zip(pre_broadcast_shape, final_shape) + ) + ) + + if any(self.broadcasting_dims) and not supports_implicit_broadcast: + value = ( + f"tl.broadcast_to({value}, {V.kernel.index_to_str(broadcast_shape)})" + ) + + old_shape = self.broadcast_shape + if not self.stride_sorter.is_identity: + # if for_store the transform is + # (non-descending strides) broadcasted kernel tile shape + # -> (descending strides) block descriptor shape + # o/w if loading the transform is + # (descending strides) ((maybe implicitly) broadcasted block shape + # -> (non-descending) (maybe implicitly) broadcasted kernel tile shape + permute_dims = ( + self.stride_sorter.sort_idx + if for_store + else self.stride_sorter.revert_sort_idx + ) + value = f"tl.trans({value}, {permute_dims})" + old_shape = ( + self.broadcast_shape + if for_store + else self.stride_sorter.revert(self.broadcast_shape) + ) + + # Reshape to the final shape. + value = triton_reshape(value, old_shape, final_shape) + + return value + + +@dataclasses.dataclass +class TensorDescriptorOptions(BlockDescriptorOptions): + def format(self, name: str, roffset=True) -> str: + """ + Codegen a call to tl.make_tensor_descriptor() + + Args: + name: variable name for pointer + roffset: unused, but kept for compatibility with BlockPtrOptions.format() + + Returns: + "tl.make_tensor_descriptor(...)" + """ + + f = V.kernel.index_to_str + args = [ + ( + f"{name} + ({f(self.constant_offset)})" + if self.constant_offset != 0 + else name + ), + f"shape={f(self.shape)}", + f"strides={f(self.strides)}", + f"block_shape={f(self.block_shape)}", + ] + + return f"tl.make_tensor_descriptor({', '.join(args)})" + + +@dataclasses.dataclass +class BlockPtrOptions(BlockDescriptorOptions): + def replace_offset( + self, expr: sympy.Expr, replacement: sympy.Expr, symt: SymT + ) -> sympy.Expr: + """ + Replaces instances of {symt}_offset with the new expression. + """ + roffset = TritonSymbols.block_offsets[symt] + return sympy_subs(expr, {roffset: replacement}) + + def remove_roffsets(self, expr: sympy.Expr) -> sympy.Expr: + for symt in TritonSymbols.reduction_types: + expr = self.replace_offset(expr, sympy.Integer(0), symt) + return expr + + def format(self, name: str, roffset=True) -> str: + """ + Codegen a call to tl.make_block_ptr() + + Args: + name: variable name for pointer + roffset: should rn_offset be included in offsets=..., for use with tl.advance() + + Returns: + "tl.make_block_ptr(...)" + """ + f = V.kernel.index_to_str + offsets = [*self.offsets] + if not roffset: + offsets = [self.remove_roffsets(offset) for offset in offsets] + args = [ + ( + f"{name} + ({f(self.constant_offset)})" + if self.constant_offset != 0 + else name + ), + f"shape={f(self.shape)}", + f"strides={f(self.strides)}", + f"block_shape={f(self.block_shape)}", + f"order={f(self.order)}", + f"offsets={f(offsets)}", + ] + return f"tl.make_block_ptr({', '.join(args)})" + + def advance_roffset(self, symt: SymT) -> sympy.Expr: + """ + Codegen string to pass to tl.advance(name, ...). + + Advance is the difference between offsets in each loop iteration. + To compute it, we replace rN_offset with multiples of RN_BLOCK. + Since we expect rN_offset to vary in range(0, rN_numel, RN_BLOCK), the first + iteration has rN_offset=0, while the second has rN_offset=RN_BLOCK. + """ + rblock = TritonSymbols.block_sizes[symt] + advance = [ + ( + self.replace_offset(offset, rblock, symt) + - self.replace_offset(offset, sympy.S.Zero, symt) + ) + for offset in self.offsets + ] + return advance + + +def triton_reshape( + value: str, old_shape: Sequence[sympy.Expr], new_shape: Sequence[sympy.Expr] +) -> str: + """Workaround https://github.com/triton-lang/triton/issues/2836""" + assert isinstance(old_shape, list) and isinstance(new_shape, list) + + old_shape_str = [V.kernel.index_to_str(shape) for shape in old_shape] + new_shape_str = [V.kernel.index_to_str(shape) for shape in new_shape] + + if old_shape_str == new_shape_str: + return value + if [s for s in new_shape_str if s != "1"] != old_shape_str: + return f"tl.reshape({value}, [{', '.join(new_shape_str)}])" + # rewrite to [:, None] syntax, which is less buggy + idx = 0 + expand = [] + for size in new_shape_str: + if idx < len(old_shape_str) and size == old_shape_str[idx]: + expand.append(":") + idx += 1 + else: + assert size == "1" + expand.append("None") + assert idx == len(old_shape_str) + return f"{value}[{', '.join(expand)}]" + + +# NB: Inheriting from PythonPrinter is somewhat dangerous, because there are a +# number of operators which Triton "implements", but in a way that is +# inconsistent with Python semantics (and consistent with C semantics). We +# must override all of these, or it is potential silent correctness problem +class TritonPrinter(PythonPrinter): # noqa: docstring_linter + def _print_TruncToInt(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + return ( + # pyrefly: ignore [missing-attribute] + f"libdevice.trunc({self._print(expr.args[0])}).to({V.kernel.index_dtype})" + ) + + def _print_TruncToFloat(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + value = self._print(expr.args[0]) + # Adding +0.0 preserves large floating results while canonicalizing + # libdevice.trunc(-0.0) back to Python's +0.0 materialization behavior. + # pyrefly: ignore [missing-attribute] + return f"(libdevice.trunc({value}) + tl.zeros_like({value}))" + + def _print_Float(self, expr: sympy.Expr) -> str: + if expr.is_integer: + # sympy considers 0.0 to be integer, but triton doesn't. + # this workaround prints the float as an integer + # xref: https://github.com/sympy/sympy/issues/26620 + ret = str(int(expr)) + elif config.is_fbcode() and torch.version.hip: + ret = f"{expr}" + else: + ret = f"tl.full([], {expr}, tl.float64)" + return ret + + def _print_ToFloat(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + # pyrefly: ignore [bad-argument-type] + s = self.parenthesize(expr.args[0], PRECEDENCE["Atom"] - 0.5) + return f"{s}.to(tl.float64)" + + def _print_PythonMod(self, expr: sympy.Expr) -> str: + quot, div = expr.args + if quot.is_nonnegative and div.is_nonnegative: + return self.stringify(expr.args, " % ", PRECEDENCE["Atom"] - 0.5) + # pyrefly: ignore [missing-attribute] + quot_s = self._print(quot) + # pyrefly: ignore [missing-attribute] + div_s = self._print(div) + return f"triton_helpers.remainder_integer({quot_s}, {div_s})" + + def _print_FloorDiv(self, expr: sympy.Expr) -> str: + assert expr.is_integer + quot, div = expr.args + if quot.is_nonnegative and div.is_nonnegative: + return self.stringify(expr.args, " // ", PRECEDENCE["Atom"] - 0.5) + # pyrefly: ignore [missing-attribute] + quot_s = self._print(quot) + # pyrefly: ignore [missing-attribute] + div_s = self._print(div) + return f"triton_helpers.div_floor_integer({quot_s}, {div_s})" + + # TODO: This is wrong, when lhs, rhs > 2**53, Python does a higher + # precision algorithm, which we would need to replicate here + def _print_IntTrueDiv(self, expr: sympy.Expr) -> str: + return self.stringify(expr.args, " / ", PRECEDENCE["Atom"] - 0.5) + + # NB: sympy.floor/ceiling produce integers, so we have to do the + # conversion to index dtype + def _print_floor(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + return ( + # pyrefly: ignore [missing-attribute] + f"libdevice.floor({self._print(expr.args[0])}).to({V.kernel.index_dtype})" + ) + + def _print_FloorToInt(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + return ( + # pyrefly: ignore [missing-attribute] + f"libdevice.floor({self._print(expr.args[0])}).to({V.kernel.index_dtype})" + ) + + def _print_ceiling(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"libdevice.ceil({self._print(expr.args[0])}).to({V.kernel.index_dtype})" + + def _print_CeilToInt(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"libdevice.ceil({self._print(expr.args[0])}).to({V.kernel.index_dtype})" + + def _helper_sqrt(self, expr: sympy.Expr) -> str: + # pyrefly: ignore [missing-attribute] + return f"tl.sqrt_rn(({self._print(expr)}).to(tl.float32))" + + def _print_FloatPow(self, expr: sympy.Expr) -> str: + # pyrefly: ignore [missing-attribute] + base = self._print(expr.args[0]) + # pyrefly: ignore [missing-attribute] + exp = self._print(expr.args[1]) + # libdevice.pow requires both arguments to have the same type. + # Always cast to float64 for consistency. This is scalar shape math, + # not tensor ops, so the performance impact is negligible. + return f"libdevice.pow(({base}).to(tl.float64), ({exp}).to(tl.float64))" + + def _print_PowByNatural(self, expr: sympy.Expr) -> str: + if expr.args[0].is_Integer: + base = f"tl.full([], {float(expr.args[0])}, tl.float64)" + else: + # pyrefly: ignore [missing-attribute] + base = f"({self._print(expr.args[0])}).to(tl.float64)" + exp_val = expr.args[1] + if exp_val.is_Integer: + exp = f"tl.full([], {float(exp_val)}, tl.float64)" + else: + # pyrefly: ignore [missing-attribute] + exp = f"({self._print(exp_val)}).to(tl.float64)" + # libdevice.pow requires both arguments to have the same type. + # Always cast to float64 for consistency. This is scalar shape math, + # not tensor ops, so the performance impact is negligible. + return f"libdevice.pow({base}, {exp})" + + def _print_Where(self, expr: sympy.Expr) -> str: + c = self.doprint(expr.args[0]) + p = self.doprint(expr.args[1]) + q = self.doprint(expr.args[2]) + return f"tl.where({c}, {p}, {q})" + + def _print_min_max_helper(self, expr: sympy.Expr, cmp: str) -> str: + """ + Helper for max/min code generation. + cmp: > or < + """ + if len(expr.args) == 1: + # pyrefly: ignore [missing-attribute] + return self._print(expr.args[0]) + + mid = len(expr.args) // 2 + cls = type(expr) + # pyrefly: ignore [missing-attribute] + a = self._print(cls(*expr.args[:mid])) + # pyrefly: ignore [missing-attribute] + b = self._print(cls(*expr.args[mid:])) + + # Use a macro so we can propagate constexprs. + # https://github.com/triton-lang/triton/issues/3815 + a, b = tuple(f"({x})" for x in (a, b)) + assert cmp in (">", "<"), f"Unexpected comparator: '{cmp}'" + return f"({a} * ({a} {cmp}= {b}) + {b} * ({b} {cmp} {a}))" + + def _print_Min(self, expr: sympy.Expr) -> str: + return self._print_min_max_helper(expr, "<") + + def _print_Max(self, expr: sympy.Expr) -> str: + return self._print_min_max_helper(expr, ">") + + def _print_Abs(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"tl_math.abs({self._print(expr.args[0])})" + + def _print_OpaqueUnaryFn_cos(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"libdevice.cos(({self._print(expr.args[0])}).to(tl.float32))" + + def _print_OpaqueUnaryFn_cosh(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"libdevice.cosh(({self._print(expr.args[0])}).to(tl.float32))" + + def _print_OpaqueUnaryFn_acos(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"libdevice.acos(({self._print(expr.args[0])}).to(tl.float32))" + + def _print_OpaqueUnaryFn_sin(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"libdevice.sin(({self._print(expr.args[0])}).to(tl.float32))" + + def _print_OpaqueUnaryFn_sinh(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"libdevice.sinh(({self._print(expr.args[0])}).to(tl.float32))" + + def _print_OpaqueUnaryFn_asin(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"libdevice.asin(({self._print(expr.args[0])}).to(tl.float32))" + + def _print_OpaqueUnaryFn_tan(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"libdevice.tan(({self._print(expr.args[0])}).to(tl.float32))" + + def _print_OpaqueUnaryFn_tanh(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"libdevice.tanh(({self._print(expr.args[0])}).to(tl.float32))" + + def _print_OpaqueUnaryFn_atan(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"libdevice.atan(({self._print(expr.args[0])}).to(tl.float32))" + + def _print_OpaqueUnaryFn_log2(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + # pyrefly: ignore [missing-attribute] + return f"libdevice.log2(({self._print(expr.args[0])}).to(tl.float32))" + + def _print_RoundToInt(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 1 + return ( + # pyrefly: ignore [missing-attribute] + f"libdevice.llrint({self._print(expr.args[0])}).to({V.kernel.index_dtype})" + ) + + def _print_RoundDecimal(self, expr: sympy.Expr) -> str: + assert len(expr.args) == 2 + number, ndigits = expr.args + if number.is_integer: + # ndigits < 0 should have been filtered by the sympy function + assert ndigits < 0 + raise ValueError( + f"For integer inputs, only non-negative ndigits are currently supported, but got {ndigits}." + ) + + number_str = self.parenthesize(number, PRECEDENCE["Mul"]) + return f"libdevice.nearbyint(1e{ndigits} * {number_str}) * 1e{-ndigits}" + + +texpr = TritonPrinter().doprint + + +def triton_compute_type(dtype: torch.dtype) -> str: + """Convert torch.dtype to triton type and upcast [b]float16 to float32""" + return triton_type(upcast_compute_type(dtype)) + + +def triton_store_type(dtype: torch.dtype) -> str: + """Convert torch.dtype to triton type, with fix for storing tl.bool""" + if dtype == torch.bool: + dtype = torch.int8 + return triton_type(dtype) + + +def upcast_acc_dtype(dtype: torch.dtype) -> torch.dtype: + """Implicit upcasts used for Triton reduction types""" + if is_integer_dtype(dtype) and dtype.is_signed and dtype.itemsize <= 4: + return torch.int32 + return upcast_compute_type(dtype) + + +def triton_acc_type(dtype: torch.dtype) -> str: + """Convert torch.dtype to triton type, with reduction upcasts""" + return triton_compute_type(upcast_acc_dtype(dtype)) + + +def low_precision_fp(dtype: torch.dtype) -> bool: + return dtype.itemsize <= 2 and dtype.is_floating_point + + +def low_precision_fp_var(var: CSEVariable | Any) -> bool: + if not isinstance(var, CSEVariable): + return False + + dtype = var.dtype + return low_precision_fp(dtype) if isinstance(dtype, torch.dtype) else False + + +def triton_arg_dtype(arg: Any) -> torch.dtype | None: + if isinstance(arg, CSEVariable): + return arg.dtype + if isinstance(arg, torch._prims_common.Number): + return type_to_dtype(type(arg)) + return None + + +def needs_upcast_to_float32(arg: Any) -> bool: + return ( + not config.triton.codegen_upcast_to_fp32 + and isinstance(arg, CSEVariable) + and arg.dtype in (torch.float16, torch.bfloat16) + ) + + +class TritonCSEVariable(CSEVariable): + def __init__( + self, + name: str, + bounds: ValueRanges[Any], + dtype: torch.dtype, + shape: BlockShapeType = None, + ) -> None: + super().__init__(name, bounds, dtype, shape=shape) + # We'll use this to track which masks the variable needs when used for indirect indexing + self.mask_vars: OrderedSet[str] = OrderedSet() + assert dtype is not None, "TritonCSEVariable must have dtype" + assert shape is not None, "TritonCSEVariable must have shape" + + def update_on_args(self, name, args, kwargs): + for arg in args: + if isinstance(arg, TritonCSEVariable): + self.mask_vars.update(arg.mask_vars) + elif isinstance(arg, sympy.Symbol): + # most of the time index vars don't need masks associated with them + # however, when index vars are used to compute indices for indirect reads + # those reads should subsequently be masked, + for symt in TritonSymbols.block_types: + if symbol_is_type(arg, symt): + self.mask_vars.update([f"{prefix_str[symt]}mask"]) + break + + +def get_dtype_handler() -> DtypePropagationOpsHandler: + from torch._inductor.dtype_propagation import DtypePropagationOpsHandler + + return DtypePropagationOpsHandler() + + +def maybe_upcast_float32(convert_output: bool = True) -> Callable[[_T], _T]: + """ + Codegen helper to upcast arguments to float32, depending on the config and dtype. + This decorates tl.math/libdevice codegen functions. + """ + + def maybe_upcast_arg(var) -> str: + upcast_string = ".to(tl.float32)" if needs_upcast_to_float32(var) else "" + return f"{var}{upcast_string}" + + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: + # Record that this function only supports float32 and float64. + OpDtypeSupport.register_upcast(func, convert_output) + + def wrapped(*args, **kwargs) -> str: + # Optionally upcast args to float32. + upcast_args = [maybe_upcast_arg(arg) for arg in args] + upcast_kwargs = {key: maybe_upcast_arg(val) for key, val in kwargs.items()} + + # Call the decorated function, optionally downcasting the result. + result = func(*upcast_args, **upcast_kwargs) + any_needs_upcast = convert_output and any( + needs_upcast_to_float32(var) + for var in itertools.chain(args, kwargs.values()) + ) + result_dtype = ( + None + if not any_needs_upcast + else getattr(get_dtype_handler(), func.__name__)(*args, **kwargs) + ) + needs_downcast = result_dtype not in (torch.float32, None) + downcast_string = ( + f".to({triton_type(result_dtype)})" + if needs_downcast and result_dtype is not None + else "" + ) + return f"{result}{downcast_string}" + + return wrapped + + return decorator # type: ignore[return-value] + + +class TritonOverrides(OpOverrides): + """Map element-wise ops to Triton e.g., ops.to_dtype(x,...) -> x.to(...)""" + + _LOG_2_E = math.log2(math.e) + + @staticmethod + def to_dtype( + x, + dtype: torch.dtype, + src_dtype: torch.dtype | None = None, + use_compute_types=True, + ): + def _get_min_elements_per_thread( + src_dtype: torch.dtype, dst_dtype: torch.dtype + ) -> int: + if src_dtype == dst_dtype: + # No data type conversion is needed. No requirements on min_elem_per_thread. + return 0 + + # fp8 data type conversions has min_elem_per_thread requirements. + # Refer to Triton implementations here: + # https://github.com/triton-lang/triton/blob/10f59d8ce04052521c1bc0cb3a3f8b98918fc7e3/lib/Conversion/TritonGPUToLLVM/ElementwiseOpToLLVM.cpp#L10. + fp8_dtypes = ( + torch.float8_e4m3fn, + torch.float8_e5m2, + ) + # Triton doesn't support type conversions between fp8_e4m3 and fp8_e5m2. + assert not ( + src_dtype in fp8_dtypes + and dst_dtype in fp8_dtypes + and src_dtype != dst_dtype + ), "Conversions between float8_e5m2 and float8_e4m3fn is not supported!" + if src_dtype == torch.float8_e5m2 or dst_dtype == torch.float8_e5m2: + return 4 + if src_dtype == torch.float8_e4m3fn or dst_dtype == torch.float8_e4m3fn: + return 2 + # No requirements on min_elem_per_thread. + return 0 + + if src_dtype is not None: + # Both dtype and src_dtype are set. This is used by torch to(dtype=dtype). + # It takes the maximum min_elem_per_thread if there are multiple fp8 conversions + # in the same kernel. + V.kernel.min_elem_per_thread = max( + _get_min_elements_per_thread(src_dtype, dtype), + V.kernel.min_elem_per_thread, + ) + + if dtype == torch.bool: + return f"({x} != 0)" + elif dtype == torch.uint8 and ( + src_dtype is not None and src_dtype.is_floating_point or src_dtype is None + ): + # to work around llvm uint conversion semantics that produces 0's for negative + # values when converting from floating types. + # optimization - if source type is known and it's not a floating type, then + # do not apply conversion to the intermediate type. + return f"{x}.to(tl.int16).to(tl.uint8)" + + if use_compute_types: + out_dtype = triton_compute_type(dtype) + else: + out_dtype = triton_store_type(dtype) + + return f"{x}.to({out_dtype})" + + @staticmethod + def to_dtype_bitcast(x, dtype: torch.dtype, src_dtype: torch.dtype): + assert src_dtype.itemsize == dtype.itemsize + # We may promote float16 or bfloat16 to float32 and cause the + # bitwidth of dtype to be different from the input tensor (i.e. float32). + # In such as case, we will have to convert the input tensor to + # its src_type, perform bitcast, and then convert the bit-casted + # tensor back to float to ensure we use values with the right precision. + if x.dtype != src_dtype: + x = f"{x}.to({triton_type(src_dtype)})" + + out = f"{x}.to({triton_type(dtype)}, bitcast=True)" + if upcast_compute_type(dtype) != dtype: + out = f"{out}.to({triton_type(upcast_compute_type(dtype))})" + + return out + + @staticmethod + def _shaped_constant(value, dtype, shape): + type_ = torch._prims_common.dtype_to_type(dtype) + triton_val = constant_repr(type_(value)) + triton_type = triton_compute_type(dtype) + + # Triton's scalar_constant() treats -0.0 as 0 (since -0.0 == 0 in Python), + # Work around by encoding -0.0 as its IEEE 754 hex in uint and bitcasting. + if value == 0 and math.copysign(1.0, value) < 0: + if triton_type == "tl.float32": + return f"tl.full({shape}, 0x80000000, tl.uint32).to({triton_type}, bitcast=True)" + elif triton_type == "tl.float64": + return f"tl.full({shape}, 0x8000000000000000, tl.uint64).to({triton_type}, bitcast=True)" + + # NOTE: We use tl.full here to get the expected type. + # Otherwise, subnormal float32 values are treated as fp64 + # causing fp32 * fp64 promotion and different numerical results. + if value < 0 and not dtype.is_signed: + triton_signed_type = f"tl.{triton_type[4:]}" + return f"tl.full({shape}, {triton_val}, {triton_signed_type}).to({triton_type})" + else: + return f"tl.full({shape}, {triton_val}, {triton_type})" + + @classmethod + def constant(cls, value, dtype): + return cls._shaped_constant(value, dtype, shape=[]) + + @classmethod + def _cast_libdevice_arg(cls, arg, dtype: torch.dtype) -> str: + if isinstance(arg, torch._prims_common.Number): + return cls.constant(arg, dtype) + if triton_arg_dtype(arg) == dtype: + return f"{arg}" + return f"({arg}).to({triton_type(dtype)})" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def abs(x): + return f"tl_math.abs({x})" + + # TODO - register these ops as having divergent dtype + # output if doing graph pass to remove consecutive casts + + @staticmethod + def truediv(x, y): + x_dtype = getattr(x, "dtype", None) + y_dtype = getattr(y, "dtype", None) + + if ( + x_dtype == torch.float32 + and y_dtype == torch.float32 + and config.eager_numerics.division_rounding + ): + # x / y in Triton is lowered to div.full which is approx + # we want div_rn to adhere with eager + out = f"triton.language.div_rn({x}, {y})" + else: + out = f"({x} / {y})" + + if low_precision_fp_var(x) or low_precision_fp_var(y): + out_dtype = get_dtype_handler().truediv(x, y) + if out_dtype in (torch.float16, torch.float32): + out = f"{out}.to({triton_type(out_dtype)})" + + return out + + @staticmethod + def div_rn(x, y): + """ + Division with round-to-nearest rounding mode. + Always uses triton.language.div_rn for float32 inputs to match eager CUDA behavior. + """ + x_dtype = getattr(x, "dtype", None) + y_dtype = getattr(y, "dtype", None) + + if x_dtype == torch.float32 and y_dtype == torch.float32: + out = f"triton.language.div_rn({x}, {y})" + else: + # Fall back to regular division for non-float32 types + out = f"({x} / {y})" + + if low_precision_fp_var(x) or low_precision_fp_var(y): + out_dtype = get_dtype_handler().truediv(x, y) + if out_dtype in (torch.float16, torch.float32): + out = f"{out}.to({triton_type(out_dtype)})" + + return out + + @staticmethod + def mod(x, y): + out = f"({x} % {y})" + if low_precision_fp_var(x) or low_precision_fp_var(y): + out_dtype = get_dtype_handler().mod(x, y) + if out_dtype in (torch.float16, torch.float32): + out = f"{out}.to({triton_type(out_dtype)})" + return out + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def exp(x): + """ + When use_fast_math, use the ftz (flushing to zero) variant + of exponent computation. + + Check https://github.com/triton-lang/triton/issues/5735 for + more details. + """ + if config.use_fast_math: + return f"tl_math.exp({x})" + else: + return f"libdevice.exp({x})" + + @staticmethod + @maybe_upcast_float32() + def exp2(x): + return f"libdevice.exp2({x})" + + @staticmethod + @maybe_upcast_float32() + def expm1(x): + return f"libdevice.expm1({x})" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def sqrt(x): + return f"tl.sqrt_rn({x})" + + @staticmethod + def relu(x): + bug = config.triton.inject_relu_bug_TESTING_ONLY + if bug == "compile_error": + return "compile error!" + elif bug == "runtime_error": + # NB: this only triggers runtime error as long as input + # is not all zero + return f'triton_helpers.device_assert_then({x} == 0, "injected assert fail", {x})' + elif bug == "accuracy": + return f"{x} + 1" + elif bug is None: + return ops.maximum(ops.constant(0, torch.int32), x) + else: + raise AssertionError( + f"unrecognized config triton.inject_relu_bug_TESTING_ONLY = {bug!r}" + ) + + @staticmethod + # pyrefly: ignore [bad-override] + def minimum(a, b): + if torch.version.hip: + return f"tl.minimum({a}, {b}, tl.PropagateNan.ALL)" + else: + return f"triton_helpers.minimum({a}, {b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def maximum(a, b): + if torch.version.hip: + return f"tl.maximum({a}, {b}, tl.PropagateNan.ALL)" + else: + return f"triton_helpers.maximum({a}, {b})" + + @staticmethod + # pyrefly: ignore [bad-override] + def where(a, b, c): + return f"tl.where({a}, {b}, {c})" + + @staticmethod + # pyrefly: ignore [bad-override] + def dot(a, b): + """ + Triton code generation for lowering ops.dot to tl.dot. + + The logic is as follows: + + 1. Downcasting for performance + If the data was previously upcasted to fp32, we downcast back to the + original dtype (e.g., fp16 or bf16) for better performance. While + surrounding operations may run in fp32, matmul itself is executed at the + original precision to optimize throughput. + + 2. Handling non-constant reduction masks + If the reduction mask is not constant and there was any operation between + tl.load and tl.dot, we zero out regions outside the mask using + tl.where(r0_mask, val, 0). + This ensures that values outside the mask do not contribute to the dot + product, preventing incorrect results. + + 3. Shape alignment for tl.dot + We massage shapes to match the tl.dot requirement of (Y, R) x (R, X). + Current codegen eagerly broadcasts tl.arange to create unique axes. We + reshape, transpose, or broadcast to align with the (Y, R) x (R, X) shape. + We avoid using 3D dot ((Z, Y, R) x (Z, R, X)) because 3D tl.dot has + poor performance. During batched matmul (bmm), we keep ZBLOCK=1 and call + the 2D dot kernel instead. + """ + assert V.kernel.is_native_matmul + orig_a, orig_b = a, b + + def is_where_needed(var): + # Skip if the variable doesn't have a reduction mask + if not any(map(prefix_is_reduction, var.mask_vars)): + return False + + reduction_range = V.kernel.range_trees[-1] + assert reduction_range.is_reduction + + # Skip if reduction mask was already constant + if V.kernel._has_constant_mask(reduction_range): + return False + + # Skip if the variable is already zeroed outside the mask + # (e.g., from tl.load(..., other=0.0)) + # TODO : track the value of outside of mask region with cse + for k, v in V.kernel.cse._cache.items(): + if v == var and "tl.load" in k and "other=0.0" in k: + return False + + return True + + def where_cond(var): + default = ir.Reduction.default_value("dot", var.dtype) + reduction_mask = [ + f"{tree.prefix}mask" + for tree in V.kernel.range_trees + if tree.is_reduction + ] + + assert len(reduction_mask) == 1, "don't tile reduction when native matmul" + + where_var = TritonKernelOverrides.where(reduction_mask[0], var, default) + return V.kernel.cse.generate( + V.kernel.compute, where_var, dtype=var.dtype, shape=var.shape + ) + + # When computing expressions like ((A+1) @ (B+2)), + # native codegen will do + # + # a = tl.load(..., r0_mask, other=0.0) + # b = tl.load(..., r0_mask, other=0.0) + # tmp0 = a+1 + # tmp1 = b+2 + # tmp2 = tl.dot(tmp0, tmp1) + # + # This produces incorrect results because outside of r0_mask is not zero. + # So before calling tl.dot, apply tl.where to zero out values properly. + # TODO: Optimize - We don't need both operands to be zeroed except NaN * 0 + if is_where_needed(orig_a): + a = where_cond(a) + if is_where_needed(orig_b): + b = where_cond(b) + + def reshape_transpose_broadcast_for_dot( + value, + initial_shape: Sequence[sympy.Expr], + final_shape: Sequence[sympy.Expr], + ) -> str: + """ + Generate a reshape, transpose, and broadcast for the tl.dot. + tl.dot requires specific shape requirement : (Y,R) x (R,X) + but the current triton codegen eagerly broadcast the tl.arange so + it needs to be reshaped to meet the requirement. + + This is done by three steps. + 1. remove the empty dimension (dim with size 1) and make it 2d with tl.reshape + 2. permute the dimension if needed (e.g., (X,R) -> (R,X)) with tl.trans + 3. broadcast if needed with broadcast_to. + - This shows up when matmul operand is broadcasted with torch.expand/repeat. + - e.g., torch.rand((16,)).expand(16,16) @ B + + e.g., (Y,1,R), (Y,R) -> tl.reshape(var, (Y,R)) + e.g., (1,X,R), (R,X) -> tl.trans(tl.reshape(var, (X,R))) + e.g., (1,X,1), (R,X) -> tl.broadcast_to(tl.trans(tl.reshape(var, (X,1))), (R,X)) + + TODO : eventually we want to remove this function when lazy broadcasting arrives + """ + + # Triton 3d dot is slower than 2d dot, so we want to keep block shape in 2d + # by fixing ZBLOCK=1 in the autotune config + if ZBLOCK in initial_shape: + initial_shape = ["1" if dim == ZBLOCK else dim for dim in initial_shape] + + if final_shape == [YBLOCK, RBLOCK]: + assert XBLOCK not in initial_shape, ( + "left tl.dot operand cannot depend on x" + ) + + shape_2d = ["1", "1"] + if YBLOCK in initial_shape: + shape_2d[0] = YBLOCK + if RBLOCK in initial_shape: + shape_2d[1] = RBLOCK + + # reshape it into 2d + value = triton_reshape(value, initial_shape, shape_2d) + + # broadcast if needed + broadcast_needed = shape_2d != [YBLOCK, RBLOCK] + if broadcast_needed: + value = f"tl.broadcast_to({value}, ({YBLOCK}, {RBLOCK}))" + + elif final_shape == [RBLOCK, XBLOCK]: + assert YBLOCK not in initial_shape, ( + "right tl.dot operand cannot depend on y" + ) + + shape_2d = ["1", "1"] + if XBLOCK in initial_shape: + shape_2d[0] = XBLOCK + if RBLOCK in initial_shape: + shape_2d[1] = RBLOCK + + # reshape it into 2d (X,R) + value = triton_reshape(value, initial_shape, shape_2d) + + # transpose to (R,X) + value = f"tl.trans({value})" + + # broadcast if needed + broadcast_needed = shape_2d != [XBLOCK, RBLOCK] + if broadcast_needed: + value = f"tl.broadcast_to({value}, ({RBLOCK}, {XBLOCK}))" + else: + raise NotImplementedError + + return value + + assert len(V.kernel.dense_size_list()) >= 3, "tl.dot can only do mm and bmm" + + XBLOCK = str(TritonSymbols.block_sizes[SymT.XBLOCK]) + YBLOCK = str(TritonSymbols.block_sizes[SymT.YBLOCK]) + ZBLOCK = str(TritonSymbols.block_sizes[SymT.ZBLOCK]) + RBLOCK = str(TritonSymbols.block_sizes[SymT.R0_INDEX]) + + a = V.kernel.cse.generate( + V.kernel.compute, + reshape_transpose_broadcast_for_dot(a, list(a.shape), [YBLOCK, RBLOCK]), + dtype=a.dtype, + shape=(YBLOCK, RBLOCK), + ) + + b = V.kernel.cse.generate( + V.kernel.compute, + reshape_transpose_broadcast_for_dot(b, list(b.shape), [RBLOCK, XBLOCK]), + dtype=b.dtype, + shape=(RBLOCK, XBLOCK), + ) + + if torch.backends.cuda.matmul.fp32_precision == "tf32": + input_precision = "tf32" + else: + input_precision = "ieee" + + return f'tl.dot({a}, {b}, input_precision="{input_precision}")' + + @staticmethod + def inline_asm_elementwise( + *inputs, + asm, + constraints=None, + dtype=torch.float32, + is_pure=True, + pack=1, + input_dtypes=None, + ): + # Use the actual dtype, not the compute type — the asm operates on + # specific register types and Triton needs to know the real output type. + asm_triton_type = triton_type(dtype) + if constraints is None: + constraints = ", ".join(["=r"] + ["r" for _ in inputs]) + + # Inductor computes bf16/fp16 in fp32. For "h" (16-bit register) + # constraints, cast back to the original dtype so the asm sees the + # right register type. + constraint_parts = [p.strip() for p in constraints.split(",")] + input_constraints = [p for p in constraint_parts if not p.startswith("=")] + cast_inputs = [] + for i, (inp, c) in enumerate(zip(inputs, input_constraints[: len(inputs)])): + if ( + c == "h" + and input_dtypes is not None + and isinstance(inp, CSEVariable) + and inp.dtype != input_dtypes[i] + ): + cast_inputs.append(f"{inp}.to({triton_type(input_dtypes[i])})") + else: + cast_inputs.append(str(inp)) + + if torch.version.hip: + # AMDGCN asm strings may contain real newlines (instructions are + # newline-separated, unlike PTX which uses semicolons). The + # generated code is nested inside two Python string layers: + # Layer 1 : the cached wrapper .py file + # Layer 2 : the Triton kernel source (a triple-quoted string + # inside that wrapper, exec'd / JIT-compiled) + # repr() escapes \n -> \\n, then we double the backslashes so + # they survive both layers: \\\\n -> (L1 parse) \\n -> (L2 parse) \n. + asm_literal = repr(asm).replace("\\", "\\\\") + constraints_literal = repr(constraints).replace("\\", "\\\\") + else: + asm_literal = f"'{asm}'" + constraints_literal = f"'{constraints}'" + + def asm_call(args): + return ( + f"tl.inline_asm_elementwise({asm_literal}, {constraints_literal}, " + f"[{args}], dtype={asm_triton_type}, is_pure={is_pure}, pack={pack})" + ) + + if pack <= 1: + return asm_call(", ".join(cast_inputs)) + + first_input = inputs[0] + compute = V.kernel.compute + cse = V.kernel.cse + result = cse.newvar(dtype=dtype, shape=first_input.shape) + packed_args = ", ".join( + f"triton_helpers.inline_asm_pack({inp}, {pack})" for inp in cast_inputs + ) + compute.writeline(f"{result} = {asm_call(packed_args)}") + compute.writeline( + f"{result} = triton_helpers.inline_asm_unpack({result}, {first_input}, {pack})" + ) + return result + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def cos(x): + return f"tl_math.cos({x})" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def sin(x): + return f"tl_math.sin({x})" + + @classmethod + def index_expr(cls, expr, dtype): + raise NotImplementedError("ops.index_expr not implemented outside a kernel") + + @staticmethod + def masked(mask, body, other): + raise NotImplementedError("ops.masked not implemented outside a kernel") + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def lgamma(x): + return f"libdevice.lgamma({x})" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def erf(x): + return f"libdevice.erf({x})" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def cosh(x): + return f"libdevice.cosh({x})" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def sinh(x): + return f"libdevice.sinh({x})" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def acos(x): + return f"libdevice.acos({x})" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def acosh(x): + return f"libdevice.acosh({x})" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def asin(x): + return f"libdevice.asin({x})" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def asinh(x): + return f"libdevice.asinh({x})" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def atan2(x, y): + return f"libdevice.atan2({x}, {y})" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def atan(x): + return f"libdevice.atan({x})" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def atanh(x): + return f"libdevice.atanh({x})" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def copysign(x, y): + return f"libdevice.copysign({x}, {y})" + + @staticmethod + @maybe_upcast_float32() + def erfc(x): + return f"libdevice.erfc({x})" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def erfinv(x): + return f"libdevice.erfinv({x})" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def hypot(x, y): + return f"libdevice.hypot({x}, {y})" + + @staticmethod + @maybe_upcast_float32() + def log10(x): + return f"libdevice.log10({x})" + + @staticmethod + @maybe_upcast_float32() + def log2(x): + return f"libdevice.log2({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def ldexp(x, n): + return f"libdevice.ldexp({x}, {n}.to(tl.int32))" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def nextafter(x, y): + return f"libdevice.nextafter({x}, {y})" + + @staticmethod + # pyrefly: ignore [bad-override] + def logical_and(a, b): + return f"{a} & {b}" + + @staticmethod + def logical_not(a): + return f"{a} == 0" + + @staticmethod + # pyrefly: ignore [bad-override] + def logical_or(a, b): + return f"{a} | {b}" + + @staticmethod + # pyrefly: ignore [bad-override] + def logical_xor(a, b): + return f"({a} ^ {b})" + + @staticmethod + def bitwise_and(a, b): + return f"{a} & {b}" + + @staticmethod + def bitwise_not(a): + return f"~{a}" + + @staticmethod + def bitwise_or(a, b): + return f"{a} | {b}" + + @staticmethod + def bitwise_xor(a, b): + return f"{a} ^ {b}" + + @staticmethod + def bitwise_left_shift(a, b): + return f"{a} << {b}" + + @staticmethod + def bitwise_right_shift(a, b): + return f"{a} >> {b}" + + @staticmethod + def rand(seed, offset): + offset = f"({offset}).to(tl.uint32)" + return f"tl.rand({seed}, {offset})" + + @staticmethod + def rand_eager(seed, base_offset, threads_per_round, tid, vec): + # vec: 4 for fp32, 8 for fp16/bf16 + tid_u32 = f"({tid}).to(tl.uint32)" + denom = f"(({vec})*({threads_per_round}))" + r = f"(({tid_u32})//({denom})*({vec}//4))" + tid_trunc = f"(({tid_u32})%({denom}))" + + return f"triton_helpers.rand_eager_kernel({seed}, {base_offset}+{r}, {tid_trunc}, VEC={vec})" + + @staticmethod + def randn(seed, offset): + offset = f"({offset}).to(tl.uint32)" + return f"tl.randn({seed}, {offset})" + + @staticmethod + def randint64(seed, offset, low, high): + offset = f"({offset}).to(tl.uint32)" + return f"triton_helpers.randint64({seed}, {offset}, {low}, {high})" + + @staticmethod + def load_seed(name, offset): + raise NotImplementedError("ops.load_seed not implemented outside a kernel") + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def rsqrt(x): + if torch.version.hip: + return f"tl.rsqrt({x})" + else: + return f"libdevice.rsqrt({x})" + + @staticmethod + @maybe_upcast_float32() + def log1p(x): + return f"libdevice.log1p({x})" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def tan(x): + return f"libdevice.tan({x})" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def tanh(x): + cse_var = V.kernel.cse.varname_map.get(x) + if cse_var and hasattr(cse_var, "dtype"): + dtype = cse_var.dtype + else: + dtype = None + if ( + config.use_fast_math + and torch.version.hip + and get_triton_version() > (3, 5) + and dtype != torch.float64 + and dtype is not None + ): + # Requires upstream Triton 3.6+ for latest fast_tanhf support + # https://github.com/triton-lang/triton/pull/8551 + return f"libdevice.fast_tanhf({x})" + else: + return f"libdevice.tanh({x})" + + @staticmethod + @maybe_upcast_float32() + def sigmoid(x): + return f"tl.sigmoid({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def signbit(x): + # XX: This is wrong for the value -0.0 in floating point + return ( + f"(libdevice.signbit({x}) != 0) if ({x}).dtype is tl.float32 else {x} < 0" + ) + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def fmod(a, b): + return f"libdevice.fmod({a}, {b})" + + @classmethod + def pow(cls, a, b): + result_dtype = get_dtype_handler().pow(a, b) + if result_dtype is not None and is_integer_dtype(result_dtype): + base = cls._cast_libdevice_arg(a, result_dtype) + exponent = ( + cls.constant(b, torch.int64) + if isinstance(b, torch._prims_common.Number) + else f"{b}" + ) + return f"triton_helpers.pow_integer({base}, {exponent})" + + any_needs_upcast = needs_upcast_to_float32(a) or needs_upcast_to_float32(b) + pow_dtype = result_dtype + if pow_dtype not in (torch.float32, torch.float64): + # libdevice.pow only accepts fp32/fp64. Keep low-precision floating + # cases on the existing fp32 path, and otherwise fall back to fp64 + # for symbolic integer scalar pow expressions like 2 ** ks0. + pow_dtype = ( + torch.float32 + if low_precision_fp(result_dtype) or any_needs_upcast + else torch.float64 + ) + + cast_a = cls._cast_libdevice_arg(a, pow_dtype) + cast_b = cls._cast_libdevice_arg(b, pow_dtype) + result = f"libdevice.pow({cast_a}, {cast_b})" + if result_dtype is not None and result_dtype != pow_dtype: + if low_precision_fp(result_dtype): + if any_needs_upcast: + result = f"{result}.to({triton_type(result_dtype)})" + else: + result = f"{result}.to({triton_type(result_dtype)})" + return result + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def log(x): + return f"tl_math.log({x})" + + @staticmethod + @maybe_upcast_float32(convert_output=False) + # pyrefly: ignore [bad-override] + def isinf(x): + return f"libdevice.isinf({x}).to(tl.int1)" + + @staticmethod + @maybe_upcast_float32(convert_output=False) + # pyrefly: ignore [bad-override] + def isnan(x): + return f"libdevice.isnan({x}).to(tl.int1)" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def round(x): + return f"libdevice.nearbyint({x})" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def floor(x): + return f"libdevice.floor({x})" + + @staticmethod + def floordiv(a, b): + # See the comment in lowering.div_mode. a and b are integer type. + # Notice that // in triton behaves as truncdiv instead of floordiv. + # + # We avoid feeding negative values into Triton's // operator because + # Triton's AxisInfo analysis incorrectly deduplicates signed division + # results for contiguous inputs (triton-lang/triton#XXXX). Instead we + # use bitwise complement (~) to make the dividend non-negative: + # floor_div(a, b) = ~(~a // b) when a < 0, a // b when a >= 0 + # For negative b we negate both operands first. + zero = ops.constant(0, torch.int32) + one = ops.constant(1, torch.int32) + # Guard against integer division by zero before the division to + # avoid undefined behavior (LLVM may optimize away a post-division + # check assuming UB doesn't happen). Replace b with 1 when b is 0 + # so the division is safe, then select 0 as the final result. + b_zero = ops.eq(b, zero) + b = ops.where(b_zero, one, b) + b_neg = ops.lt(b, zero) + a = ops.where(b_neg, ops.sub(zero, a), a) + b = ops.where(b_neg, ops.sub(zero, b), b) + a_neg = ops.lt(a, zero) + a = ops.where(a_neg, ops.bitwise_not(a), a) + quot = ops.truncdiv(a, b) + quot = ops.where(a_neg, ops.bitwise_not(quot), quot) + return ops.where(b_zero, zero, quot) + + @staticmethod + # pyrefly: ignore [bad-override] + def sign(x): + z = ops.constant(0, torch.int32) + left = ops.to_dtype((ops.lt(z, x)), torch.int8) + right = ops.to_dtype((ops.lt(x, z)), torch.int8) + sub = ops.sub(left, right) + return f"{sub}.to({x}.dtype)" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def trunc(x): + return f"libdevice.trunc({x})" + + @staticmethod + # pyrefly: ignore [bad-override] + def truncdiv(a, b): + # See the comment in lowering.div_mode. a and b are integer type. + # Notice that // in triton behaves as truncdiv instead of floordiv + return f"{a} // {b}" + + @staticmethod + @maybe_upcast_float32() + # pyrefly: ignore [bad-override] + def ceil(x): + return f"libdevice.ceil({x})" + + +# Register the custom pow override after class creation so type checkers see +# a plain callable instead of the class-body staticmethod descriptor. +OpDtypeSupport.register_upcast(TritonOverrides.pow, True) +TritonOverrides._initialize_pointwise_overrides("triton") + + +class TritonKernelOverrides(TritonOverrides): + """Map element-wise ops to Triton within a TritonKernel + + Unlike TritonOverrides, these assume the code is going to be inserted into + the body of the main triton kernel and so it may use indexing and mask + variables which are assumed to already be defined in the current scope. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # happens in __init__ unlike _initialize_pointwise_overrides + # because the libdevice registrations are populated during lowerings + self._setup_libdevice_routing() + + @classmethod + @functools.cache + def _setup_libdevice_routing(cls): + """Set up routing to libdevice implementations for fp64 inputs.""" + + from torch._inductor.codegen.common import OpDecompositions + + for fn_name in torch._inductor.utils.op_requires_libdevice_fp64: + assert hasattr(cls, fn_name) + original_impl = getattr(cls, fn_name) + + def decomposition_router(x, _original_impl, _fn_name): + if x.dtype != torch.float64: + return _original_impl(x) + else: + return getattr(OpDecompositions, _fn_name)(x).value + + if fn_name == "sigmoid": + assert hasattr(OpDecompositions, "sigmoid") + fn = functools.partial( + decomposition_router, _original_impl=original_impl, _fn_name=fn_name + ) + fn.__name__ = fn_name # type: ignore[attr-defined] + setattr(cls, fn_name, staticmethod(fn)) + continue + + def dtype_router(x, _original_impl, _fn_name): + if x.dtype == torch.float64: + return f"libdevice.{_fn_name}({x})" + else: + return _original_impl(x) + + fn = functools.partial( + dtype_router, _original_impl=original_impl, _fn_name=fn_name + ) + fn.__name__ = fn_name # type: ignore[attr-defined] + setattr(cls, fn_name, staticmethod(fn)) + + @classmethod + def constant(cls, value, dtype): + # NOTE: Cannot use shape=[] as it's not supported by triton-rocm + # We could use shape=[1] instead but starting with the correct + # ndim avoids extra `tt.expand_dim` ops appearing in the triton IR. + ndim = V.kernel.triton_tensor_ndim() + shape = [1] * ndim + return cls._shaped_constant(value, dtype, shape=shape) + + @classmethod + def index_expr(cls, expr, dtype): + expr = _materialize_trunc_to_float_expr(expr, dtype) + indexing = V.kernel.indexing( + expr, block_ptr=False, tma_compatibility_checker=None + ) + assert isinstance(indexing, IndexingOptions) + + shape: BlockShapeType + if indexing.expand_shape: + shape = indexing.expand_shape + else: + shape = TritonSymbols.get_block_shape(indexing.index) + + # Our sympy expr printing casts to the current kernel index dtype. + # we only respect non int32-int64 dtypes and otherwise use current kernel indexing dtype + index_dtype = V.kernel.get_index_dtype_as_torch_dtype() + dtype = dtype if dtype not in (torch.int32, torch.int64) else index_dtype + + # after we emit this var we cast it to the correct dtype + orig = config.test_configs.runtime_triton_dtype_assert + try: + config.test_configs.runtime_triton_dtype_assert = False + var = V.kernel.cse.generate( + V.kernel.compute, + indexing.index_str, + bounds=get_bounds_index_expr(expr), + dtype=dtype, + shape=shape, + ) + finally: + config.test_configs.runtime_triton_dtype_assert = orig + + if dtype not in (torch.int32, torch.int64): + var = V.kernel.cse.generate( + V.kernel.compute, + cls.to_dtype(var, dtype), + dtype=upcast_compute_type(dtype), + shape=var.shape, + ) + else: + # TODO: we are not always consistent in enforcing that the output of the index expr printing + # results in the indexing dtype. So if we detect that we have an input which might type promote + # to a dtype other than indexing dtype, add a cast. + # Trying to avoid + dtype = index_dtype + for index_var in expr.free_symbols: + if symbol_is_type(index_var, SymT.TMP): + dtype = torch.promote_types( + dtype, V.kernel.cse.varname_map[index_var.name].dtype + ) + + if dtype != index_dtype: + var = V.kernel.cse.generate( + V.kernel.compute, + cls.to_dtype(var, index_dtype), + dtype=index_dtype, + shape=var.shape, + ) + + var.mask_vars = indexing.mask_vars + return var + + @staticmethod + def masked(mask, body, other): + if mask is not None and torch.version.hip is not None: + mask = V.kernel.cse.generate( + V.kernel.compute, + f"{mask}.to(tl.int1)", + dtype=torch.bool, + shape=mask.shape, + ) + + nodes = body.graph.find_nodes(op="output") + assert nodes, "graph for body does not contain an output" + + need_where = False + # If we have a tl.load with a masking operator and no other value + # we can add the mask here and the other value to the tl.load + # operator to save the branching cost. + for node in nodes: + for arg in node.args: + if arg.target != "load" or should_unwrap_unspec_arg(arg.args[1]): + need_where = True + break + + value = None if need_where else other + + with V.kernel.mask_loads(mask, value=value) as new_mask: + result = body() + + if need_where: + # Remove once CSEVariables track the dtype + if result.bounds.is_bool: + other = bool(other) + # Take dtype from result to prevent accidental promotion + other = V.kernel.cse.generate( + V.kernel.compute, + f"tl.full({result}.shape, {constant_repr(other)}, {result}.dtype)", + bounds=ValueRanges.wrap(other), + dtype=result.dtype, + shape=result.shape, + ) + ret = ops.where(new_mask, result, other) + else: + ret = result + + ret.mask_vars.discard(new_mask) + return ret + + @staticmethod + def load_seed(name, offset): + var = V.kernel.args.input(name) + return ( + f"tl.load({var} + {V.kernel.args.seed_offset('load_seed_offset', offset)})" + ) + + @staticmethod + # pyrefly: ignore [bad-override] + def frexp(x): + cache_key = f"frexp({x})" + if cse_val := V.kernel.cse.try_get(cache_key): + return cse_val + + mantissa = V.kernel.cse.newvar(dtype=x.dtype, shape=x.shape) + exponent = V.kernel.cse.newvar(dtype=torch.int32, shape=x.shape) + V.kernel.compute.writeline( + f"{mantissa}, {exponent} = triton_helpers.frexp({x})" + ) + V.kernel.cse.put(cache_key, (mantissa, exponent)) + return (mantissa, exponent) + + @staticmethod + # pyrefly: ignore [bad-override] + def partial_accumulate( + name: str, + reduction_type: str, + value: CSEVariable, + extra_meta: dict[str, Any], + ) -> None: + raise NotImplementedError + + +class HelperFunctions: + """An ordered set of helper functions.""" + + _templates_seen: dict[str, str] # Template code to function name + finalized_helpers: list[str] + + def __init__(self) -> None: + self._templates_seen = {} + self.finalized_helpers = [] + + def add(self, template_code: str, *, base_name="_triton_helper_fn") -> str: + """This accepts a function definition with the function name + left as a format specifier e.g. + + @triton.jit + def {name}(arg0, arg1): + return arg0 + arg1 + + We add the templated code to the function set and return the name + assigned to that function. + + """ + existing_name = self._templates_seen.get(template_code) + if existing_name is not None: + # Don't duplicate existing helpers + return existing_name + + name = f"{base_name}{len(self.finalized_helpers)}" + self._templates_seen[template_code] = name + self.finalized_helpers.append(template_code.format(name=name)) + return name + + def __iter__(self): + return iter(self.finalized_helpers) + + def __getitem__(self, idx): + return self.finalized_helpers[idx] + + +@dataclasses.dataclass +class BlockParameters: + """ + Class representing ND block dimensions, for block pointer analysis. + """ + + shape: list[sympy.Expr] = dataclasses.field(default_factory=list) + block_shape: list[sympy.Expr] = dataclasses.field(default_factory=list) + strides: list[sympy.Expr] = dataclasses.field(default_factory=list) + offsets: list[sympy.Expr] = dataclasses.field(default_factory=list) + + @dataclasses.dataclass + class StrideSorter: + original_strides: list[int] + sort_idx: list[int] + revert_sort_idx: list[int] = dataclasses.field(init=False) + + def __post_init__(self): + assert len(self.original_strides) > 0 + assert len(self.sort_idx) == len(self.original_strides) + + identity_sort_idx = list(range(len(self.original_strides))) + self._is_identity = self.sort_idx == identity_sort_idx + + # Set revert_sort_idx + sorted_dims_by_strides_map = {k: i for i, k in enumerate(self.sort_idx)} + self.revert_sort_idx = [ + sorted_dims_by_strides_map[i] + for i in range(len(sorted_dims_by_strides_map)) + ] + + @property + def is_identity(self): + return self._is_identity + + @classmethod + @abstractmethod + def create( + cls, original_strides: list[int | sympy.Expr], shape_env: ShapeEnv + ) -> BlockParameters.StrideSorter: + """Create a `StrideSorter` that can be used to sort block parameters.""" + + def sort(self, attr): + if not self.is_identity: + return [attr[i] for i in self.sort_idx] + return attr + + def revert(self, attr): + if not self.is_identity: + return [attr[i] for i in self.revert_sort_idx] + return attr + + @dataclasses.dataclass + class IdentityStrideSorter(StrideSorter): + def __post_init__(self): + super().__post_init__() + + @classmethod + def create( + cls, original_strides: list[int | sympy.Expr], shape_env: ShapeEnv + ) -> BlockParameters.StrideSorter: + return cls( + original_strides=original_strides, + sort_idx=list(range(len(original_strides))), + ) + + @dataclasses.dataclass + class TensorDecriptorStrideSorter(StrideSorter): + """ + Sorts BlockParameters dimensions with strides in descending order. + """ + + def __post_init__(self): + super().__post_init__() + + @classmethod + def create( + cls, original_strides: list[int | sympy.Expr], shape_env: ShapeEnv + ) -> BlockParameters.StrideSorter: + """ + If the strides are not all known constants or if the strides are already + sorted in descending order, return identity sort. + + For example if block_shape @ strides is [ZBLOCK, XBLOCK, YBLOCK] @ [8, 1, 16] + The indices to sort the strides in descending order will be [2, 0, 1]. + The indices to revert back to the original order will be [1, 2, 0]. + """ + identity_sort = list(range(len(original_strides))) + try: + # TODO: even if the strides are not in descending order the strides + # may be tensor descriptor compliant + # i.e. innermost stride == 1 and outer strides 16 byte aligned + # We should benchmark the effect of applying a transpose to these + # cases vs leaving them unsorted. + sort_idx = utils.argsort_sym(shape_env, original_strides, reverse=True) + except AssertionError: + # Symbolic shapes, failed to evaluate comparison expression + sort_idx = identity_sort + + return cls( + original_strides=original_strides, + sort_idx=sort_idx, + ) + + def __add__(self, other: BlockParameters) -> BlockParameters: + """ + Concatenates block parameters. + """ + cls = type(self) + a, b = tuple(dataclasses.asdict(x) for x in (self, other)) + return cls(**{key: a[key] + b[key] for key in a}) + + def maybe_sort_with_stride_order( + self, stride_sorter_cls: type[StrideSorter], shape_env: ShapeEnv + ) -> tuple[BlockParameters, BlockParameters.StrideSorter]: + """ + Sort `BlockParameter` with stride_sorter_cls. Returns block parameters + as well as a `StrideSorter` which contains information on how the sort + can be reverted. + """ + stride_sorter = stride_sorter_cls.create(self.strides, shape_env=shape_env) + params = BlockParameters( + **{ + key: stride_sorter.sort(val) + for key, val in dataclasses.asdict(self).items() + } + ) + return params, stride_sorter + + def remove_dims(self, removable_dims: list[bool]) -> BlockParameters: + """ + Remove dimensions where removable_dims is True. + """ + + def filter_dims(it): + return [ + item + for item, is_removable in zip(it, removable_dims) + if not is_removable + ] + + return BlockParameters( + **{key: filter_dims(val) for key, val in dataclasses.asdict(self).items()}, + ) + + +class CooperativeReductionWorkspaceCache: + """ + The scratch space used for cooperative reductions can be reused + after two reduction loops. This keeps track of what can be reused. + """ + + def __init__(self, args): + self.args = args + self.current_loop = [] + self.prior_loop = [] + self.ready_for_reuse = collections.defaultdict(collections.deque) + self.loop_count = 0 + self.store_count = 0 + + def allocate(self, nbytes: sympy.Expr): + cached = self.ready_for_reuse.get(nbytes) + if cached: + return cached.popleft() + ws_name, _, ws_offset = self.args.workspace(nbytes, False) + self.current_loop.append((nbytes, ws_name, ws_offset)) + return (ws_name, ws_offset) + + def on_loop_end(self): + # Buffers can be reused after 2 loop ends + for nbytes, ws_name, ws_offset in self.prior_loop: + self.ready_for_reuse[nbytes].append((ws_name, ws_offset)) + self.prior_loop = self.current_loop + self.current_loop = [] + self.loop_count += 1 + + def increment_store_count(self): + prior = self.store_count + self.store_count += 1 + return prior + + +@dataclasses.dataclass +class FixedTritonConfig: + config: dict[str, int] + + def __getitem__(self, item): + return self.config[item] + + def __contains__(self, item): + return item in self.config + + +class TritonCSE(CSE[TritonCSEVariable, str | tuple[str, str]]): + """ + Subclasses CSE to apply the current load mask to the cache key to avoid CSEing + variables across separate masked blocks. + """ + + def augment_key(self, cache_key: str) -> str | tuple[str, str]: + if mask := V.kernel._load_mask: + return (cache_key, mask.name) + else: + return cache_key + + +@dataclasses.dataclass +class TMACompatibilityChecker: + """ + Checks if the TMA API can be used for load / store triton operations. + """ + + kernel: TritonKernel + dtype: torch.dtype + for_store: bool + force: bool + + def __post_init__(self): + self.failed_debug_prefix = "Cannot use TMA descriptor for load / store since: " + + # Also see Note: TMA API Restrictions for the below + def can_use_tma( + self, + ) -> bool: + if self.force: + return True + if not ( + ( + ( + V.graph.get_current_device_or_throw().type == "cuda" + and torch.cuda.get_device_capability()[0] >= 9 + and config.assume_aligned_inputs + ) + or V.graph.get_current_device_or_throw().type == "xpu" + ) + and config.triton.use_tensor_descriptor + and has_triton_stable_tma_api() + # For CUDA The base ptr needs to be aligned + ): + log.debug( + ( + "%s Requires triton>=3.4.0, a CUDA device with cc>=9.0 and" + " `use_tensor_descriptor` and `assume_aligned_inputs` options enabled" + ), + self.failed_debug_prefix, + ) + return False + + # `no_x_dim` => XBLOCK=1, and for reductions this means only one element + # is to be stored . However the TMA API requires that + # the store will be 16 byte aligned, which is not attainable with a single + # element + if self.for_store and self.kernel.no_x_dim: + log.debug( + "%s stores with `no_x_dim` cannot load 16 bytes.", + self.failed_debug_prefix, + ) + return False + + return True + + def are_block_parameters_compatible( + self, + block_params: BlockParameters, + ) -> bool: + """ + Check if the block parameters are valid for TMA. + If force, we allow relying on symbolic hints equivalent + to what we check for Triton templates. + """ + if self.force: + strides = [ + V.graph.sizevars.replace_backed_symbols_with_hints(st) + for st in block_params.strides + ] + else: + strides = block_params.strides + + # The TMA API requires that the innermost stride is 1 + # and that the outer strides are 16 byte aligned + if not V.graph.sizevars.statically_known_equals(strides[-1], sympy.Integer(1)): + log.debug( + "%s TMA API requires innermost stride to be 1. Strides are: %s", + self.failed_debug_prefix, + strides, + ) + return False + + element_size = self.dtype.itemsize + for stride in strides[:-1]: + if not V.graph.sizevars.statically_known_equals( + ModularIndexing(stride * element_size, 1, sympy.Integer(16)), + sympy.Integer(0), + ): + log.debug( + "%s TMA API requires outer strides to be 16 byte aligned. Dtype bytes: %d, strides: %s", + self.failed_debug_prefix, + element_size, + strides, + ) + return False + + # Now compute the minimum value of the block type that is used + # in the innermost block size that can guarantee that 16 bytes of data + # can be loaded / stored. + # Start with finding the innermost block type + innermost_block_shape = block_params.block_shape[-1] + + # Pure singleton case + if V.graph.sizevars.statically_known_equals( + innermost_block_shape, sympy.Integer(1) + ): + log.debug( + "%s innermost block shape cannot load 16 bytes. Block shape: %s", + self.failed_debug_prefix, + block_params.block_shape, + ) + return False + + innermost_block_type = None + innermost_block_symt = None + for block_type_str in innermost_block_shape.free_symbols: + for block_symt in TritonSymbols.block_types: + if symbol_is_type(block_type_str, block_symt): + innermost_block_type = block_type_str + innermost_block_symt = block_symt + break + + assert innermost_block_type and innermost_block_symt, ( + f"{innermost_block_shape} expr must contain a single block type from {TritonSymbols.block_types}" + ) + + # For persistent reductions, the reduction block sizes are fixed at compile time. + # Only apply this logic when the innermost block is a reduction block; + # persistent reductions can still have pointwise-style loads where the innermost block is X/Y/Z, + # and in that case we should fall back to the generic analysis below. + if ( + self.kernel.persistent_reduction + and not self.for_store + and innermost_block_symt in TritonSymbols.reduction_types + ): + # For a discontiguous tensor, a 1D block will be split across several + # dimensions, e.g. R0_BLOCK: + # block_shape=[XBLOCK, ((R0_BLOCK + 31)//32), Min(1, ((R0_BLOCK + 31)//32)), Min(32, R0_BLOCK)] + # The persistent R0_BLOCK will be a power of 2 that is at least r0_numel So it + # should be guaranteed that Min(32, R0_BLOCK) * element_size >= 16 + innermost_tree_prefix = prefix_str[innermost_block_symt] + tree_numel = None + for t in self.kernel.range_trees: + if t.is_reduction and t.prefix == innermost_tree_prefix: + tree_numel = t.numel + break + if tree_numel is None: + # If we can't map the innermost reduction block type to a reduction range tree, + # we cannot determine the persistent RBLOCK value, + # so we cannot validate the 16-byte innermost-dimension requirement for TMA. + # Treat this as incompatible rather than asserting during compilation, fallback to non-TMA loads. + log.debug( + "%s could not find reduction range tree for innermost prefix %s Block shape: %s", + self.failed_debug_prefix, + innermost_tree_prefix, + block_params.block_shape, + ) + return False + persistent_rblock = self.kernel._get_persistent_RBLOCK(tree_numel) + innermost_block_bytes = ( + innermost_block_shape.subs({innermost_block_type: persistent_rblock}) + * element_size + ) + if not V.graph.sizevars.statically_known_geq( + innermost_block_bytes, sympy.Integer(16) + ): + log.debug( + "%s persistent reduction innermost block shape cannot load 16 bytes. Block shape: %s, persistent RBLOCK: %d", + self.failed_debug_prefix, + block_params.block_shape, + persistent_rblock, + ) + return False + + else: + # E.g. if the innermost block shape is Min(2, XBLOCK) + # then the TMA API can only be used if the dtype has an 8 byte element + # size so that 16 bytes of data can be loaded in the innermost dimension + try: + + def indexing_div_rep( + x: sympy.Expr, + y: sympy.Expr, + z: sympy.Expr | None = None, + ) -> sympy.Expr: + div = x / y + if z: + div = div % z + return div + + solve_expr = innermost_block_shape * element_size - 16 + # Sympy cannot handle FloorDiv and ModularIndexing well, so simplify + solve_expr_simplified = solve_expr.replace( + FloorDiv, indexing_div_rep + ).replace(ModularIndexing, indexing_div_rep) + min_block_size = next_power_of_2( + int( + sympy.nsolve( + solve_expr_simplified, + innermost_block_type, + 1, + ) + ) + ) + + # TODO: min block size may be too large / introduce redundancy + if min_block_size > self.kernel.max_block( + prefix_str[innermost_block_symt] + ): + log.debug( + "%s the minimum block size to satisfy expression %s is too large: %d", + self.failed_debug_prefix, + solve_expr_simplified, + min_block_size, + ) + return False + + block_type_str = self.kernel.index_to_str(innermost_block_type) + # Check block sizes if the user has provided a fixed triton config + if self.kernel.fixed_config: + if min_block_size > self.kernel.fixed_config[block_type_str]: + log.debug( + "%s For block %s, fixed config block size %d is smaller " + "than the minimum required: %d", + self.failed_debug_prefix, + block_type_str, + self.kernel.fixed_config[block_type_str], + min_block_size, + ) + return False + else: + # Update the minimum block sizes that are passed to triton + # heuristics + self.kernel.tma_min_block_sizes[block_type_str] = max( + min_block_size, + self.kernel.tma_min_block_sizes.get(block_type_str, 1), + ) + + except ValueError: + log.debug( + "%s innermost block shape cannot load 16 bytes. Block params: %s", + self.failed_debug_prefix, + block_params.block_shape, + ) + return False + + return True + + def can_lift(self) -> bool: + """ + Can you lift the make_tensor_descriptor + call to the top of the kernel? This requires + being certain that all of the shape, stride, + and block_shape information is handled in arguments + or top level definitions. + + Right now we assume this is always possible if you force TMA. + """ + return self.force + + +class TritonKernel(SIMDKernel[TritonCSEVariable]): + """A class to represent a triton kernel and helpers to generate + triton kernel programmatically + """ + + overrides = TritonKernelOverrides # type: ignore[assignment] + helper_functions: HelperFunctions + kexpr: Callable[[sympy.Expr], str] = texpr + allow_block_ptr = True + tma_compatibility_checker_cls = TMACompatibilityChecker + block_ptr_options_cls: type[BlockPtrOptions] = BlockPtrOptions + tensor_descriptor_options_cls: type[TensorDescriptorOptions] = ( + TensorDescriptorOptions + ) + transpose_discontiguous_tensor_descriptors_override: bool | None = None + + def __init__( + self, + tiling: dict[str, sympy.Expr], + min_elem_per_thread=0, + optimize_mask=True, + fixed_config: FixedTritonConfig | None = None, + hint_override: int | None = None, + is_combo_kernel: bool = False, + **kwargs, + ) -> None: + self.optimize_mask: bool = optimize_mask + self.fixed_config = fixed_config + self.is_combo_kernel: bool = is_combo_kernel + super().__init__(tiling, **kwargs) + self.cse = TritonCSE(self.newvar_prefix, self.suffix) + # Cache of values that can be reused for the prologue. + self.prologue_cache: dict[str, str] = {} + self.prologue: IndentedBuffer = IndentedBuffer() + self.post_loop_combine: IndentedBuffer = IndentedBuffer() + self.post_loop_store: IndentedBuffer = IndentedBuffer() + self.outside_loop_vars = OrderedSet[Any]() + self.min_elem_per_thread = min_elem_per_thread + self.block_ptr_id = itertools.count() + self.block_ptr_to_buffer = dict[str, str]() + self.helper_functions = HelperFunctions() + self.pointer_advancements: dict[SymT, dict[str, list[sympy.Expr]]] = ( + collections.defaultdict(dict) + ) + self.tma_min_block_sizes = dict[str, int]() + self.hint_override = hint_override + self._load_counts: collections.Counter[str] = collections.Counter() + self._pdl_load_index = 0 + self._pdl_has_wait = False + + # A set of autotuning hints to pass as part of triton_meta + self.autotune_hints = OrderedSet[AutotuneHint]() + self.triton_meta: dict[str, Any] | None = None + + if self.inside_reduction: + self.codegen_reduction_numels(self.body) + + if self.cooperative_reduction: + self.init_cooperative_reduction() + + self.codegen_range_tree() + + if self.cooperative_reduction: + self.init_cooperative_reduction_mask() + + self.has_load_with_contiguous_rdim = False + # We track the store name since a store can be canceled later + self.stores_with_contiguous_rdim: list[str] = [] + + @staticmethod + def _has_stride1_on_rdim(index) -> bool: + # These analysis is only needed in deterministic mode so far + # to filter triton configs. Return false immediately to avoid + # increasing compilation time when the mode is off. + if not ( + config.deterministic or config.test_configs.force_filter_reduction_configs + ): + return False + support_vars = index.free_symbols + reduce_vars = [ + var + for var in support_vars + if symbol_is_type(var, TritonSymbols.reduction_types) + ] + + if len(reduce_vars) == 0: + return False + + # for expression "x0 + 150528*((x1//(s27*s38))) + 3*(ModularIndexing(x1, 1, s38)) + 672*(ModularIndexing(x1, s38, s27))" + # stride_vars will results in DivisionByZero error + try: + stride_vars = V.graph.sizevars.stride_vars(index, reduce_vars, support_vars) + except ZeroDivisionError: + return False + + return any(stride == 1 for stride in stride_vars) + + @property + def has_store_with_contiguous_rdim(self) -> bool: + return not all( + is_buffer_removed(name) for name in self.stores_with_contiguous_rdim + ) + + def dtype_to_str(self, dtype: torch.dtype) -> str: + return triton_type(dtype) + + def should_use_cooperative_reduction(self) -> bool: + return self.inside_reduction and V.choices.should_use_cooperative_reduction( + self.features + ) + + def init_cooperative_reduction(self): + """One time setup code for cooperative reductions.""" + assert self.cooperative_reduction + + # shift all the grids over since tl.program_id(0) is for rsplit + for tree in self.range_trees: + if tree.grid_dim is not None: + tree.grid_dim += 1 + + sem_count = self.numels["x"] + if self.fixed_config: + sem_count = CeilDiv(sem_count, self.fixed_config["XBLOCK"]) + self.semaphores_name = self.args.semaphores(sem_count) + self.cooperative_reduction_workspace_cache = CooperativeReductionWorkspaceCache( + self.args + ) + self.body.splice( + """\ + RSPLIT_NEXT_POWER_OF_2: tl.constexpr = triton_helpers.constexpr_next_power_of_2(RSPLIT) + RSPLIT_IS_POWER_OF_2: tl.constexpr = RSPLIT == RSPLIT_NEXT_POWER_OF_2 + HAS_RSPLIT: tl.constexpr = RSPLIT > 1 + rsplit_id = tl.program_id(0) + num_rblocks = (rnumel + RBLOCK - 1) // RBLOCK + rsplit_chunk = (num_rblocks + RSPLIT - 1) // RSPLIT * RBLOCK + rsplit_start = rsplit_chunk * rsplit_id + rsplit_end = rsplit_chunk * (rsplit_id + 1) + """, + ) + if any( + not self._has_constant_mask(tree) + for tree in self.range_trees + if tree.is_reduction + ): + self.body.writeline( + "rsplit_end = tl.where(rsplit_end < rnumel, rsplit_end, rnumel)" + ) + + def init_cooperative_reduction_mask(self): + rsplit_arange = "tl.arange(0, RSPLIT_NEXT_POWER_OF_2)" + if not self.no_x_dim: + rsplit_arange = f"{rsplit_arange}[None, :]" + self.body.writeline(f"rsplit_arange = {rsplit_arange}") + + if self._has_constant_xmask(): + self.body.splice( + """\ + if RSPLIT_IS_POWER_OF_2: + rsplit_mask: tl.constexpr = None + else: + rsplit_mask = rsplit_arange < RSPLIT + """ + ) + else: + assert not self.no_x_dim + self.body.writeline( + "rsplit_mask = xmask if RSPLIT_IS_POWER_OF_2 else ((rsplit_arange < RSPLIT) & xmask)" + ) + + def codegen_range_tree(self): + for tree in self.range_trees: + # reduction indexing goes inside a loop + if not tree.is_loop: + self.iteration_ranges_codegen_header(tree, self.body) + elif self.inside_reduction: + # workaround for this issue: + # https://gist.github.com/jansel/6527126f781559095c5531f98a4235a7 + self.body.writeline( + f"{tree.prefix}base = {self.iteration_ranges_ranges_code(tree)}" + ) + + if self.inside_reduction: + if any(tree.is_loop for tree in self.range_trees): + # If the kernel contains loops, compute rbase. + rn_bases = self._get_reduction_symbols( + "base", integer=True, nonnegative=True + ) + rbase = self._flatten_reduction_indices(rn_bases) + self.body.splice(f"rbase = {self.index_to_str(rbase)}") + else: + # For looped reductions, indexing is deferred to the innermost loop. + self.codegen_reduction_indices(self.body) + + def need_numel_args(self): + """ + Indicate whether we need provide numel as arguments for the generated + kernel calls in the benchmark. + + Should be true for pointwise/reduction kernels but false for triton + matmul kernels. + """ + return True + + def should_use_persistent_reduction(self) -> bool: + return self.inside_reduction and V.choices.should_use_persistent_reduction( + self.features, self.cooperative_reduction + ) + + def want_no_x_dim(self): + return ( + self.persistent_reduction + and len(self.numels) == self.num_reduction_dims + 1 + and self.fixed_config + and self.fixed_config["XBLOCK"] == 1 + ) + + @property + def assert_function(self) -> str: + return "tl.device_assert" + + def indexing( + self, + index: sympy.Expr, + *, + copy_shape: str | tuple[str] | None = None, + dense_indexing=False, + override_mask=None, + block_ptr=False, + tma_compatibility_checker: TMACompatibilityChecker | None = None, + ): + """ + Compute the index and mask to pass to tl.load() or tl.store() + """ + index = self.prepare_indexing(index) + index_vars = index.free_symbols + has_rindex = False + + mask_vars: OrderedSet[str] = OrderedSet() + for var in sorted(index_vars, key=operator.attrgetter("name")): + assert isinstance(var, sympy.Symbol) + has_rindex = has_rindex or symbol_is_type( + var, TritonSymbols.reduction_types + ) + if override_mask: + pass + elif symbol_is_type(var, SymT.TMP): + # indirect indexing + cse_var = self.cse.varname_map[var.name] + mask_vars.update(cse_var.mask_vars) + elif symbol_is_type( + var, + ( + SymT.UNBACKED_INT, + SymT.SIZE, + SymT.PRECOMPUTED_SIZE, + SymT.INDEX, + SymT.FLOAT, + SymT.UNBACKED_FLOAT, + ), + ): + pass + else: + # var is one of xN, yN, r0_N or r1_N + prefix_matches = [ + prefix_str[symt] + for symt in TritonSymbols.block_types + if symbol_is_type(var, symt) + ] + if len(prefix_matches) == 0: + pass + assert len(prefix_matches) == 1, f"Ambiguous type: {var.name}" + mask_vars.add(f"{prefix_matches[0]}mask") + + need_dense = ( + config.triton.dense_indexing + or dense_indexing + or self._load_mask is not None + ) and index != 0 + + have_dense = True + have_loop_vars = False + dense_mask_vars: OrderedSet[str] = OrderedSet() + + for tree in self.active_range_trees(): + if index_vars.intersection(tree.var_list): + have_loop_vars = True + else: + have_dense = False + dense_mask_vars.add(f"{tree.prefix}mask") + + if ( + ( + (block_ptr and self.allow_block_ptr and config.triton.use_block_ptr) + or ( + tma_compatibility_checker + and tma_compatibility_checker.can_use_tma() + ) + ) + and not override_mask + and not self._load_mask + and len(mask_vars - dense_mask_vars) == 0 + and not self.is_indirect_indexing(index) + and have_loop_vars + # workaround https://github.com/triton-lang/triton/issues/2821 + and self.index_dtype == "tl.int32" + ): + + def match_affine_block( + index: sympy.Expr, range_tree: IterationRangesRoot + ) -> BlockParameters | None: + """ + Matches expressions of the form: + idx = s * xindex + + This implies stride (s,), and shape (XBLOCK,). + """ + stride = BlockPatternMatcher.match_affine_block_expr( + index, range_tree.symbol() + ) + if stride is None: + return None + + return BlockParameters( + shape=[range_tree.numel], + block_shape=[TritonSymbols.get_block_size(range_tree)], + strides=[stride], + offsets=[TritonSymbols.get_block_offset(range_tree)], + ) + + def match_mod_div_block( + index: sympy.Expr, range_tree: IterationRangesRoot + ) -> BlockParameters | None: + """ + Matches higher-dimensional blocks coming from FloorDiv and ModularIndexing. + + Example expression to match: + sN * ((rindex//(d1 * ... * d(N-1)))) + + s1 * ModularIndexing(rindex, 1, d1) + + ... + + s(N-1) * ModularIndexing(rindex, d1 * ... * d(N-2), d(N-1)) + + This iterates over a block of shape (dN, ..., d1) and stride + (sN, ..., s1). (d1,...,d(N-1)) and (s1,...,sN) are + wildcards that we match. + + Note that dN does not appear in the expression, but we solve for it + using range tree numels and the other dims. + """ + index_var = range_tree.symbol() + + # Bound the possible number of dims. We use the following heuristics: + # - At least one dim for each range tree node. + # - At least one dim for every FloorDiv or ModularIndexing op. + # - At least 2 dims to pattern match. + denom, modulo = sympy.symbols( + "denom modulo", + cls=functools.partial(sympy.Wild, exclude=[index_var]), + ) + + num_dims = max( + 2, + # range_tree.nodes only includes the entries for the range tree + # len(range_tree.nodes) <= self.range_tree_nodes + len(range_tree.nodes), + ( + index.count(FloorDiv(index_var, denom)) + + index.count(ModularIndexing(index_var, denom, modulo)) + ), + ) + + # [Note: Precomputed replacements with BlockPatternMatch] + # If there are precomputed replacements in an expression e.g. + # ModularIndexing(d0 * d1, d0, d1), replaced with + # ModularIndexing(p0, d0, d1), it is not possible to match p0 + # with d0 * d1 since sympy is unaware of this fact. Precomputed + # replacements are therefore removed prior to matching a + # BlockPattern, and are reintroduced after any analysis that + # works best on an expression with precomputed replacements removed + sizevars = V.graph.sizevars + index = sizevars.remove_precomputed_replacements(index) + numel = sizevars.remove_precomputed_replacements(range_tree.numel) + + match_result = BlockPatternMatcher.match_mod_div_block_expr( + index, index_var, numel, num_dims + ) + if match_result is None: + return None + + ( + dims, + strides, + block_index_exprs, + ) = match_result + slice_numels = BlockPatternMatcher.get_slice_numels(dims) + + # Check for applicable iteration range sizes. + # When mapping a 1D block into an ND one, we need to know that + # the number of elements is not changed. This means the slice numels of + # the ND iteration range must evenly divide the length of the 1D block. + # There are two cases where we can guarantee this: + # 1. Numels are powers of 2. If numel == 2 ** n, and we know XBLOCK == 2 ** m, + # with n and m integers, then either numel is a multiple of XBLOCK, or numel + # is less than XBLOCK. (If numel is less than XBLOCK, we round up to 1 below.) + # 2. Numels are multiples of the maximum possible block size. + sizevars = V.graph.sizevars + max_block = self.max_block(range_tree.prefix) + if any( + not sizevars.statically_known_multiple_of(numel, max_block) + and not sizevars.statically_known_power_of_2(numel) + for numel in slice_numels + ): + return None + + # Compute the ND block shape from the linear block size. + # Use CielDiv to round leading dimensions up to 1. + # Non-leading dimensions are clamped to the size of the iteration range, + # while the leading dimension can exceed this to accommodate a larger + # block size. + # See [Note: Precomputed replacements with BlockPatternMatch] for + # the call to lookup_precomputed_size + linear_block_size = TritonSymbols.get_block_size(range_tree) + block_shape: list[sympy.Expr] = [ + CeilDiv( + linear_block_size, + sizevars.lookup_precomputed_size(slice_numels[0]), + ) + ] + [ + sympy.Min( + CeilDiv( + linear_block_size, sizevars.lookup_precomputed_size(numel) + ), + sizevars.lookup_precomputed_size(dim), + ) + for numel, dim in zip(slice_numels[1:], dims[1:]) + ] + + # Compute block offsets from {xyzr}offset and the matched expressions. + block_offsets: list[sympy.Expr] = [ + sympy_subs( + expr, {index_var: TritonSymbols.get_block_offset(range_tree)} + ) + for expr in block_index_exprs + ] + + return BlockParameters( + shape=[sizevars.lookup_precomputed_size(d) for d in dims], + block_shape=block_shape, + strides=strides, + offsets=block_offsets, + ) + + def match_block_subexpr( + expr: sympy.Expr, + range_tree: IterationRangesRoot, + ) -> BlockParameters | None: + """ + Match a block indexing subexpression involving a single range tree. + """ + for match_func in ( + match_affine_block, + match_mod_div_block, + ): + factored_index_expr = BlockPatternMatcher.factor_index_expr( + expr, range_tree.symbol() + ) + match = match_func(factored_index_expr, range_tree) + if match is not None: + return match + + return None + + def match_block_expr() -> BlockDescriptorOptions | None: + index_relative_to_xyr_index = sympy_subs( + index, {v: t.expr for v, t in self.range_tree_nodes.items()} + ) + range_trees = self.active_range_trees() + + # Partition the index into subexpressions pertaining to each range tree. + # For example xindex * 5 + r0_index * 3 is partitioned to + # (xindex * 5, r0_index * 3). + index_subexprs = [ + BlockPatternMatcher.get_subexpr_involving_symbol( + index_relative_to_xyr_index, tree.symbol() + ) + for tree in range_trees + ] + + # Match each range tree's subexpression separately. + range_symbols = OrderedSet(tree.symbol() for tree in range_trees) + block_params = BlockParameters() + for tree, subexpr in zip(range_trees, index_subexprs): + # Reject mixed terms, e.g. xindex * r0_index. + # NB: the zero expression is allowed, for broadcasting. + if len(range_symbols.intersection(subexpr.free_symbols)) > 1: + return None + + # Match the subexpression for this range tree. + params = match_block_subexpr(subexpr, tree) + if params is None: + return None + block_params += params + + # Collect leftover terms as a constant offset. + offset = index_relative_to_xyr_index - sum(index_subexprs) + + # Form the block pointer or TMA descriptor. + self.filter_masks(mask_vars) + + options_class = ( + self.block_ptr_options_cls + if config.triton.use_block_ptr + else self.tensor_descriptor_options_cls + ) + nonlocal tma_compatibility_checker + stride_sorter_cls: type[BlockParameters.StrideSorter] + if config.triton.use_block_ptr: + can_lift = False + stride_sorter_cls = BlockParameters.IdentityStrideSorter + else: + tma_compatibility_checker = cast( + TMACompatibilityChecker, tma_compatibility_checker + ) + can_lift = tma_compatibility_checker.can_lift() + + if ( + self.transpose_discontiguous_tensor_descriptors_override + is not None + ): + transpose_contiguous = ( + self.transpose_discontiguous_tensor_descriptors_override + ) + else: + transpose_contiguous = ( + config.triton.transpose_discontiguous_tensor_descriptor + ) + + # For templates: + # Only try transpose if we know the output shape + # in case we need to transpose the data. + if hasattr(self, "template_out_shape"): + transpose_contiguous &= copy_shape is not None + + stride_sorter_cls = ( + BlockParameters.TensorDecriptorStrideSorter + if transpose_contiguous + else BlockParameters.IdentityStrideSorter + ) + + options = options_class.create( + params=block_params, + constant_offset=offset, + range_trees=range_trees, + mask_vars=mask_vars, + get_max_block=self.max_block, + can_lift=can_lift, + stride_sorter_cls=stride_sorter_cls, + ) + if options_class == TensorDescriptorOptions: + tma_compatibility_checker = cast( + TMACompatibilityChecker, tma_compatibility_checker + ) + if not tma_compatibility_checker.are_block_parameters_compatible( + options.params + ): + return None + + return options + + # Return a block pointer, if indexing matches the pattern. + options = match_block_expr() + if options is not None: + return options + expand_str = None + expand_shape: BlockShapeType = None + index_str = self.index_to_str(index) + + def _get_expand_str(): + if copy_shape: + if isinstance(copy_shape, str): + return f"{copy_shape}.shape", None + else: + return "[" + ", ".join(str(c) for c in copy_shape) + "]", copy_shape + else: + return self.dense_size_str(), tuple(self.dense_size_list()) + + if is_sympy_integer_like(index): + # Integer indexing produces a size-1 scalar tensor with the same shape + # as the dense dimension. E.g, if dense_size = [YBLOCK, XBLOCK, R0_BLOCK], + # then we create tl.full([1, 1, 1], int). + # + # Exceptions: + # 1. If copy_shape is explicitly provided, use copy_shape expansion instead. + # 2. If the dense tensor has only one dimension (e.g., [XBLOCK]), + # broadcasting does not apply. For example: + # tl.arange(0, XBLOCK) + tl.full([1], int) # -> broadcasting error + # In this case, we fall back to dense indexing: + # tl.full([XBLOCK], int) + if copy_shape or len(self.dense_size_list()) == 1: + expand_str, expand_shape = _get_expand_str() + else: + expand_str = str([1] * len(self.dense_size_list())) + expand_shape = tuple([1] * len(self.dense_size_list())) + + index_str = f"tl.full({expand_str}, {index_str}, tl.int32)" + if self.fixed_config or self.is_combo_kernel: + mask_vars = OrderedSet( + f"{tree.prefix}mask" + for tree in self.range_trees + if not tree.is_reduction and not self._has_constant_mask(tree) + ) + else: + mask_vars = OrderedSet() + if self._load_mask: + mask_vars.add(self._load_mask) + return IndexingOptions( + index_str, + mask_vars, + expand_str, + has_rindex, + index, + expand_shape=expand_shape, + ) + + if need_dense and not have_dense: + if self.inside_reduction and self.is_native_matmul: + # This avoids full broadcasting (need_dense) when performing native matmul. + # For example, self._load_mask previously required tl.broadcast_to() in index_str. + # Due to the restrictions of tl.dot semantics, we only want to expand the block + # shape for the necessary axes. + # + # Previously: + # tmp1 = tl.load(ptr + tl.broadcast_to(r0, [YBLOCK, XBLOCK, R0_BLOCK]), + # r0_mask & tmp0 & xmask) + # + # Now: + # tmp1 = tl.load(ptr + tl.broadcast_to(r0, [1, 1, R0_BLOCK]), + # r0_mask & tmp0 & xmask) + # + # We achieve this by determining the required block shape through mask inspection. + # When a temporary variable appears in the mask (e.g., self._load_mask), we retrieve + # its true shape by inspecting tmp.mask_vars tracked by TritonCSEVariable. + # + # Caution: it may miss the correct block shape if the specific mask was constant + # and thus not tracked in TritonCSEVariable.mask_vars. + # + # TODO: Once the shape propagation PR lands, reimplement this logic: + # https://github.com/pytorch/pytorch/pull/152198 + mask_shape = mask_vars.copy() + if self._load_mask: + mask_shape.add(self._load_mask) + + xyzr = OrderedSet(["xmask", "ymask", "zmask", "r0_mask"]) + while not mask_shape.issubset(xyzr): + tmp_masks = mask_shape.difference(xyzr) + tmp = tmp_masks.pop() + assert isinstance(tmp, TritonCSEVariable) + mask_shape.discard(tmp) + mask_shape.update(tmp.mask_vars) + + # e.g., expand_list becomes ['ZBLOCK', 1, 1, 'R0_BLOCK'] + expand_list = ["1"] * len(self.dense_size_list()) + for mask in mask_shape: + assert isinstance(mask, str) + for tree in self.active_range_trees(): + if mask.startswith(tree.prefix): + dim = tree.tensor_dim + assert isinstance(dim, int) + expand_list[dim] = self.dense_size_list()[dim] + + expand_str = "[" + ",".join(map(str, expand_list)) + "]" + expand_shape = tuple(expand_list) + index_str = f"tl.broadcast_to({index_str}, {expand_str})" + else: + expand_str, expand_shape = _get_expand_str() + index_str = f"tl.broadcast_to({index_str}, {expand_str})" + mask_vars = dense_mask_vars + elif not have_loop_vars and copy_shape: + expand_shape_str, expand_shape = _get_expand_str() + index_str = f"tl.broadcast_to({index_str}, {expand_shape_str})" + mask_vars = dense_mask_vars + + if expand_shape is None: + if need_dense or have_dense: + _, expand_shape = _get_expand_str() + else: + expand_shape = () + + if override_mask: + mask_vars = OrderedSet([override_mask]) + + if self._load_mask: + mask_vars.add(self._load_mask) + + self.filter_masks(mask_vars) + + return IndexingOptions( + index_str, + mask_vars, + expand_str, + has_rindex, + index, + expand_shape=expand_shape, + ) + + def codegen_block_ptr( + self, + name: str, + var: str, + indexing: BlockPtrOptions | TensorDescriptorOptions, + other="", + ) -> tuple[str, str]: + """Generate a block pointer or tensor descriptor for Triton kernel operations. + + This method creates either a block pointer (for regular Triton operations) or + a tensor descriptor (for TMA operations) based on the indexing type. It handles + caching and reuse of descriptors for performance optimization. + + Args: + name: The name of the buffer/tensor being accessed + var: The variable name for the pointer + indexing: Block pointer options or tensor descriptor options containing + indexing information and boundary check settings + other: Additional parameters string (e.g., padding options) + + Returns: + A tuple containing: + - block_descriptor: The generated block pointer or tensor descriptor variable name + - other: Modified additional parameters string with boundary check options + """ + check = indexing.boundary_check() + if isinstance(indexing, TensorDescriptorOptions): + if check and other: + # The TMA API currently does not support padding values + # but the default is zero + assert other == ", other=0.0" + other = "" + else: + if not check: + # workaround https://github.com/triton-lang/triton/issues/2813 + other = "" + elif other: + assert other == ", other=0.0" + other = f", boundary_check={check!r}, padding_option='zero'" + else: + other = f", boundary_check={check!r}" + + if ( + self.inside_reduction + and self.range_trees[-1].is_loop + and indexing.has_rindex() + ) or indexing.can_lift: + if indexing.can_lift and var in self.prologue_cache: + # Check for epilogue subtiling to reuse the same + # tensor descriptor. + block_descriptor = self.prologue_cache[var] + else: + block_ptr_line = indexing.format(var, roffset=False) + block_var = self.cse.try_get(block_ptr_line) + + # Early return if block descriptor already exists + if block_var: + return str(block_var), other + + block_descriptor_id = next(self.block_ptr_id) + if isinstance(indexing, BlockPtrOptions): + block_descriptor = f"block_ptr{block_descriptor_id}" + else: + block_descriptor = f"tma_descriptor{block_descriptor_id}" + named_var = self.cse.namedvar( + block_descriptor, dtype=torch.uint64, shape=[] + ) + self.cse.put(block_ptr_line, named_var) + + line_body = DeferredLine(name, f"{block_descriptor} = {block_ptr_line}") + if indexing.can_lift: + self.prologue.writeline(line_body) + # Cache the descriptor for epilogue subtiling + self.prologue_cache[var] = block_descriptor + else: + self.body.writeline(line_body) + + if isinstance(indexing, BlockPtrOptions): + # Store for later use. If the buffer is removed the below advancements + # are no longer necessary + self.block_ptr_to_buffer[block_descriptor] = name + + # Generate block pointer advancements, for later use. + for symt in TritonSymbols.reduction_types: + advance_offsets = indexing.advance_roffset(symt) + + # Ignore identity advancements. + if all( + V.graph.sizevars.statically_known_equals( + offset, sympy.Integer(0) + ) + for offset in advance_offsets + ): + continue + + advancements = self.pointer_advancements[symt] + assert block_descriptor not in advancements, ( + f"duplicate advancement for pointer '{block_descriptor}' at type '{symt}'" + ) + advancements[block_descriptor] = advance_offsets + else: + block_descriptor = indexing.format(var) + return block_descriptor, other + + def codegen_block_ptr_store_line(self, name, indexing, block_ptr, value, other=""): + # Stores require an explicit broadcast. We do this in two phases: + # 1. Broadcast the operand to the final shape of the range trees, e.g. [ZBLOCK, + # YBLOCK, XBLOCK]. This protects against implicit broadcasting from loads. + # 2. In case the block pointer / tma descriptor has different dimensionality, broadcast/reshape the + # result to the shape of the pointer. + value = f"tl.broadcast_to({value}, {indexing.final_shape})" + + # These dims no longer need broadcasting. + for idx, (dim, broadcast_dim) in enumerate( + zip(indexing.final_shape, indexing.broadcast_shape) + ): + if V.graph.sizevars.statically_known_equals(dim, broadcast_dim): + indexing.broadcasting_dims[idx] = False + + value = indexing.codegen_broadcast_and_reshape( + value, + indexing.final_shape, + indexing.block_shape, + allow_implicit=False, + for_store=True, + ) + + # workaround https://github.com/triton-lang/triton/issues/2814 + # For inplace-mutated buffers, the block pointer element type comes from + # the actual tensor (input buffer), which may differ from the graph dtype + # (e.g., _to_copy produces fp32 values stored into a bf16 gradient buffer). + store_dtype = V.graph.get_dtype(name) + if name in self.args.inplace_buffers: + buf = self.args.inplace_buffers[name] + if not isinstance(buf, RemovedArg): + store_dtype = V.graph.get_dtype(buf.other_names[0]) + value = f"{value}.to({triton_store_type(store_dtype)})" + if isinstance(indexing, BlockPtrOptions): + return f"tl.store({block_ptr}, {value}{other})" + return f"{block_ptr}.store({V.kernel.index_to_str(indexing.offsets)}, {value})" + + def check_bounds( + self, + expr: sympy.Expr, + size: sympy.Expr, + lower: bool, + upper: bool, + ): + if not (lower or upper): + return + + assert isinstance(expr, sympy.Expr) + indexing = self.indexing(expr, block_ptr=False, tma_compatibility_checker=None) + assert isinstance(indexing, IndexingOptions) + + index_str = indexing.index_str + mask_str = indexing.mask_str if indexing.has_mask() else None + size_str = texpr(self.rename_indexing(size)) if upper else None + + # expr is already wrapped + line = self.indirect_assert( + index_str, "0" if lower else None, size_str, mask_str + ) + + buffer = self.get_load_buffer(indexing) + self.cse.generate(buffer, line, assignment=False, dtype=torch.int32) + + def get_load_buffer(self, indexing): + if indexing.has_indirect() or indexing.has_tmpmask(): + # Masked loads must come after the mask is computed + return self.compute + elif ( + self.inside_reduction + and self.range_trees[-1].is_loop + and not indexing.has_rindex() + ): + # can lift a common load outside of reduction loop + # One exception is when this is an indirect_load. + return self.body + else: + return self.loads + + GDC_WAIT = "tl.extra.cuda.gdc_wait()" + GDC_LAUNCH = "tl.extra.cuda.gdc_launch_dependents()" + + @staticmethod + def _enable_pdl_codegen(): + if not torch._inductor.config.triton.enable_pdl: + return False + if isinstance(V.kernel, torch._inductor.select_algorithm.TritonTemplateKernel): + return False + # PDL uses CUDA-specific intrinsics (gdc_wait/gdc_launch), not available on ROCm + if torch.version.hip: + return False + return ( + V.graph.get_current_device_or_throw().type == "cuda" + and torch.cuda.get_device_capability()[0] >= 9 + ) + + def _handle_pdl_before_access( + self, wait_buffer, *dependencies, consider_reads=False + ): + if not self._enable_pdl_codegen(): + return + current_node = V.kernel.current_node + prev_node = ( + V.graph.scheduler.previous_node if V.graph.scheduler is not None else None + ) + + def matching_dep(dep): + assert prev_node is not None + prev_deps = prev_node.read_writes.writes + if consider_reads: + prev_deps = itertools.chain(prev_deps, prev_node.read_writes.reads) + return any( + dep == current_node.mutation_renames.get(w.name, w.name) + for w in prev_deps + ) + + assert dependencies + need_wait = prev_node is None or any(matching_dep(d) for d in dependencies) + if not need_wait: + return + # hoist before the loop + if self.inside_reduction and self.range_trees[-1].is_loop: + wait_buffer = self.body + + wait_buffer.writeline(self.GDC_WAIT) + + def _handle_pdl_after_load(self, launch_buffer, result_var): + if not self._enable_pdl_codegen(): + return + if result_var.use_count > 1: # we already went through this + return + # hoist after the loop + if self.inside_reduction and self.range_trees[-1].is_loop: + launch_buffer = self.post_loop_combine + + # the issue is that we need to (a) make sure this happens + # but (b) do not know if this is last yet + # so we need to remember this (has_wait), which tells use + # whether we would have needed it, and check if we are last + launch_buffer.writeline(self.GDC_WAIT) + launch_buffer.writeline(self.GDC_LAUNCH) + + def _filter_pdl(self, code: IndentedBuffer): + new_lines = [] + has_wait = False + previous_launch = None + for l in code._lines: + if type(l) is str and self.GDC_WAIT in l: + if has_wait: + continue + else: + has_wait = True + if type(l) is str and self.GDC_LAUNCH in l: + if previous_launch is not None: + new_lines.pop(previous_launch) + previous_launch = len(new_lines) + new_lines.append(l) + code._lines = new_lines + + def partial_accumulate( + self, name: str, reduction_type, val, extra_meta: dict[str, Any] + ): + self.saved_partial_accumulate.append( + PartialAccumulate(name, reduction_type, val) + ) + + def load(self, name: str, index: sympy.Expr): + """ + Load from the memory location 'name', offset by some indexing expression 'index'. + """ + var = self.args.input(name) + load_counts = self._load_counts + load_counts[name] += 1 + make_line: Callable[[str], str | DelayReplaceLine] = identity + indirect_indexing = self.is_indirect_indexing(index) + original_index = index + dtype = V.graph.get_dtype(name) + indexing = self.indexing( + index, + block_ptr=True, + tma_compatibility_checker=self.tma_compatibility_checker_cls( + self, + dtype, + for_store=False, + force=False, + ), + ) + + if isinstance(indexing, IndexingOptions) and self._has_stride1_on_rdim( + indexing.index + ): + self.has_load_with_contiguous_rdim = True + + has_rindex = indexing.has_rindex() + has_tmpmask = indexing.has_tmpmask() + + # Keep the variable in cache if were going to reuse it. Equiv., if any of the following hold + # 1) We are doing broadcasting + # 2) It is a non-coalesced load. The intuition is that if it's + # non-coalesced, we will likely load each element multiple times in + # practice. + # 3) It will be used later and it won't be CSE'd. Equiv., if all the following hold + # 3.1) We are in a reduction loop + # 3.2) Its not its last use + # 3.3) This load will not be lifted to the body + # + is_coalesced = any( + i == 1 for i in self.get_strides_of_load(original_index).values() + ) + if self.is_broadcasted(original_index): + ep = ", eviction_policy='evict_last'" + elif not is_coalesced: + ep = ", eviction_policy='evict_last'" + elif self.inside_reduction and self.range_trees[-1].is_loop: + + def decide_later(): + if load_counts[name] > expected_count and ( + has_rindex or indirect_indexing + ): + return "evict_last" + return "evict_first" + + expected_count = load_counts[name] + ep = ", eviction_policy=''" + make_line = functools.partial(DelayReplaceLine, "", decide_later) + else: + ep = "" + + if (has_tmpmask or has_rindex) and indexing.has_mask(): + if self._load_other: + other = f", other={constant_repr(self._load_other)}" + else: + other = ", other=0.0" + else: + other = "" + + """Check if the buffer we're about to load, has + more than one read dependency + NOTE: enabled with env variable TORCHINDUCTOR_SKIP_L1 + """ + has_read_deps = True + if config.triton.skip_l1_cache: + buffer_read_counts = self.features.buffer_read_counts() + # Graph inputs, primals_*, arg*_* would not be tracked by `buffer_read_counts` + # and it'd be fair to expect them to be reused. + if name in buffer_read_counts: + has_read_deps = buffer_read_counts[name] > 1 + """Skip L1 cache if we're (pretty?) sure the data is used only once + """ + skip_l1_cache = ( + not self.is_broadcasted(original_index) + and not self.inside_reduction + and not has_read_deps + and is_coalesced # for indirect loads is_coalesced is False? + ) + cachemod = "" + if skip_l1_cache: + cachemod = ", cache_modifier='.cg'" + + append_broadcast = None + shape: BlockShapeType = None + + if should_unwrap_unspec_arg(name): + line = var + # unwrapped bf16/fp16 0d tensors are passed in as float32 scalars + # see triton_utils.py:signature_of + if dtype in (torch.float16, torch.bfloat16): + if config.triton.codegen_upcast_to_fp32: + dtype = torch.float32 + else: + line += f".to({triton_type(dtype)})" + shape = () + + else: + if isinstance(indexing, (BlockPtrOptions, TensorDescriptorOptions)): + block_descriptor, other = self.codegen_block_ptr( + name, var, indexing, other + ) + if isinstance(indexing, BlockPtrOptions): + line = f"tl.load({block_descriptor}{other}{ep}{cachemod})" + else: + line = f"{block_descriptor}.load({V.kernel.index_to_str(indexing.offsets)})" + line = indexing.codegen_broadcast_and_reshape( + line, + indexing.block_shape, + indexing.final_shape, + allow_implicit=True, + for_store=False, + ) + shape = indexing.final_shape + elif is_sympy_integer_like(original_index): + line = f"tl.load({var} + ({original_index}))" + append_broadcast = indexing.expand_str + shape = () + else: + line = f"tl.load({var} + ({indexing.index_str}), {indexing.mask_str}{ep}{other}{cachemod})" + + # The block shape of tl.load depends on the indexing expression. + # Inferring shape solely from the mask may miss cases where the mask is constant. + # Inferring from indexing.expand_shape alone may also fail when dense indexing is absent. + # so, iterate over variables in the indexexpr to accurately infer the block shape. + if indexing.expand_shape: + shape = indexing.expand_shape + else: + shape = TritonSymbols.get_block_shape(indexing.index) + + if ( + dtype in (torch.float16, torch.bfloat16) + and config.triton.codegen_upcast_to_fp32 + ): + line += ".to(tl.float32)" + dtype = torch.float32 + if dtype == torch.bool and torch.version.hip is None: + # Workaround for https://github.com/triton-lang/triton/issues/2151 + # tl.load returns int8 when loading from pointer to int1 + # NOTE: Currently causes hangs on bool UTs for ROCm + line += ".to(tl.int1)" + dtype = torch.bool + + load_buffer = self.get_load_buffer(indexing) + self._handle_pdl_before_access(load_buffer, name) + result_var = self.cse.generate( + load_buffer, make_line(line), dtype=dtype, shape=shape + ) + self._handle_pdl_after_load(load_buffer, result_var) + if result_var.use_count > 1: + load_counts[name] -= 1 # don't double count cache hit + assert isinstance(result_var, TritonCSEVariable) + result_var.mask_vars = indexing.mask_vars # type: ignore[assignment] + + if append_broadcast: + line = f"tl.broadcast_to({result_var}, {append_broadcast})" + result_var = self.cse.generate( + load_buffer, line, dtype=dtype, shape=indexing.expand_shape + ) + if indexing.mask_vars: + if dtype.is_floating_point: + zero = "0.0" + elif dtype == torch.bool: + zero = "True" + else: + zero = "0" + other_val = ( + constant_repr(self._load_other) if self._load_other else zero + ) + line = f"tl.where({indexing.mask_str}, {result_var}, {other_val})" + result_var = self.cse.generate( + load_buffer, line, dtype=dtype, shape=result_var.shape + ) + + if not self.inside_reduction or (not indexing.has_rmask() and not has_rindex): + self.outside_loop_vars.add(result_var) + + return result_var + + def store( + self, name: str, index: sympy.Expr, value: CSEVariable, mode: StoreMode = None + ) -> None: + """ + store the 'value' to the memory location 'name', offset by some indexing expression 'index'. + """ + + var = self.args.output(name) + original_index = index + dtype = V.graph.get_dtype(name) + + tma_compatibility_checker = None + if mode is None or mode == "tma": + force = mode == "tma" + tma_compatibility_checker = self.tma_compatibility_checker_cls( + self, + dtype, + for_store=True, + force=force, + ) + indexing = self.indexing( + index, + dense_indexing=True, + block_ptr=mode is None, + tma_compatibility_checker=tma_compatibility_checker, + ) + + if isinstance(indexing, IndexingOptions) and self._has_stride1_on_rdim( + indexing.index + ): + self.stores_with_contiguous_rdim.append(name) + + # Guard against write-after-read corruption in triton. + # See # https://github.com/triton-lang/triton/issues/1615 + # This triton bug means that a load which is broadcasted over multiple + # warps may see the result of a store that happens later in the triton + # program. The workaround is to add a barrier before storing, which + # enforces that all warps have already read the data. + is_inplace = name in self.args.inplace_buffers + is_broadcasted = self.is_broadcasted(original_index) + if is_inplace and is_broadcasted: + self.stores.writeline(DeferredLine(name, "tl.debug_barrier()")) + + if isinstance(indexing, (BlockPtrOptions, TensorDescriptorOptions)): + block_descriptor, other = self.codegen_block_ptr(name, var, indexing) + # block_ptr / tma descriptor stores don't do implicit casting + line = self.codegen_block_ptr_store_line( + name, indexing, block_descriptor, value, other + ) + elif mode is None: + # If indexing is an integer and value has block shape larger than one, + # broadcasting fails. So, we manually broadcast indexing to the value shape. + # Without broadcast : + # tl.store(out_ptr0 + (tl.full([1, 1], 0, tl.int32)), tmp4, xmask) # Fail + # + # With broadcast: + # tl.store(out_ptr0 + (tl.full([1, 1], 0, tl.int32).broadcast_to((XBLOCK,1)), tmp4, xmask) + indexing_str = indexing.index_str + if is_sympy_integer_like(index): + if self.is_combo_kernel: + # In combo kernels, broadcast pointer to match mask shape + indexing_str += f".broadcast_to({self.dense_size_str()})" + elif value.shape is not None and not all( + str(x) == "1" for x in value.shape + ): + value_shape = ", ".join(map(str, value.shape)) + indexing_str += f".broadcast_to({value_shape})" + line = f"tl.store({var} + ({indexing_str}), {value}, {indexing.mask_str})" + elif mode == "atomic_add": + self.atomic_add_found = True + indexing_str = indexing.index_str + if ( + is_sympy_integer_like(index) + and value.shape is not None + and not all(str(x) == "1" for x in value.shape) + ): + value_shape = ", ".join(map(str, value.shape)) + indexing_str += f".broadcast_to({value_shape})" + line = f"tl.atomic_add({var} + ({indexing_str}), {value}, {indexing.mask_str}, sem='relaxed')" + else: + raise NotImplementedError(f"store mode={mode}") + + exit_stack = contextlib.ExitStack() + if not self.inside_reduction and self.cooperative_reduction: + exit_stack.enter_context(self.guard_cooperative_store(name, self.stores)) + + self._handle_pdl_before_access(self.stores, name, consider_reads=True) + self.stores.writeline(DeferredLine(name, line)) + + if not self.inside_reduction: + self.outside_loop_vars.add(value) + + exit_stack.close() + + def device_assert_async(self, cond, msg) -> None: + self.compute.writeline(f"tl.device_assert({cond}, {repr(msg)})") + + def guard_cooperative_store(self, name, buffer): + """ + For cooperative reductions only one thread block should write out the result. + We rotate which thread block does each write for better parallelism + """ + idx = self.cooperative_reduction_workspace_cache.increment_store_count() + buffer.writeline(DeferredLine(name, f"if rsplit_id == ({idx} % RSPLIT):")) + return buffer.indent() + + def _combine_masks(self, *variables: CSEVariable | None): + masks = None + for elem in variables: + if elem is None: + continue + if hasattr(elem, "mask_vars"): + if masks is None: + masks = elem.mask_vars + else: + masks = masks | elem.mask_vars + return masks + + def bucketize( + self, + values: CSEVariable, + boundaries: tuple[str, sympy.Expr, sympy.Expr, sympy.Expr], + boundary_indices: CSEVariable, + indexing_dtype: torch.dtype, + right: bool, + sorter: tuple[str, sympy.Expr] | None = None, + sorter_indices: CSEVariable | None = None, + ) -> CSEVariable: + """ + See [Note: Inductor bucketize op] + """ + + # Triton performance for bucketize_binary_search is much better when the number + # of threads equals the number of elements. + # If we're trying to use a bucketize kernel, we should make sure that an + # autotuning config with num_elements_per_warp=(warp_size) exists. + self.autotune_hints.add(AutotuneHint.ONE_ELEMENT_PER_THREAD) + + boundaries_ptr = self.args.input(boundaries[0]) + boundary_size = self.index_to_str(boundaries[1]) + boundaries_underlying_numel = self.index_to_str(boundaries[2]) + boundary_stride = self.index_to_str(boundaries[3]) + sorter_ptr = self.args.input(sorter[0]) if sorter else "None" + sorter_stride = self.index_to_str(sorter[1]) if sorter else "None" + + if indexing_dtype == torch.int32: + triton_dtype = "tl.int32" + elif indexing_dtype == torch.int64: + triton_dtype = "tl.int64" + else: + raise NotImplementedError( + "Bucketize only supports indexing with int32 and int64" + ) + + self._handle_pdl_before_access( + self.compute, boundaries[0], *([sorter[0]] if sorter else []) + ) + result = self.cse.generate( + self.compute, + f"triton_helpers.bucketize_binary_search({values}, " + f"{boundaries_ptr}, {boundary_size}, {boundaries_underlying_numel}, {boundary_stride}, " + f"{boundary_indices}, " + f"{triton_dtype}, " + f"{right}, " + f"{sorter_ptr}, {sorter_stride}, " + f"{sorter_indices}, " + ")", + dtype=indexing_dtype, # type: ignore[attr-defined] + shape=values.shape, + ) + self._handle_pdl_after_load(self.compute, result) + + masks = self._combine_masks(values, boundary_indices, sorter_indices) + result.mask_vars = masks # type: ignore[attr-defined] + + return result + + def reduction_resize(self, value) -> str: + ndims = self.triton_tensor_ndim() + if ndims == 1: + return f"triton_helpers.promote_to_tensor({value})" + + nreduce = self.num_reduction_dims + sizes = [":"] * (ndims - nreduce) + ["None"] * nreduce + return f"{value}[{', '.join(sizes)}]" + + def reduction_resize_and_shape(self, value, shape) -> tuple[str, BlockShapeType]: + ndims = self.triton_tensor_ndim() + if ndims == 1: + return f"triton_helpers.promote_to_tensor({value})", shape + + nreduce = self.num_reduction_dims + sizes = [":"] * (ndims - nreduce) + ["None"] * nreduce + new_shape = ( + (*shape[: (ndims - nreduce)], *[1] * nreduce) if shape is not None else None + ) + return f"{value}[{', '.join(sizes)}]", new_shape + + def reduction_collapse_dims( + self, buffer, value: CSEVariable, dtype: torch.dtype + ) -> CSEVariable: + """ + Reshape to RBLOCK, collapsing all reduction dims. + """ + # This is not needed for 1D reductions. + if self.num_reduction_dims == 1: + return value + + target_ndim = self.triton_tensor_ndim() - self.num_reduction_dims + initial_shape = self.dense_size_list() + target_shape = initial_shape[:target_ndim] + ["RBLOCK"] + return self.cse.generate( + buffer, + triton_reshape(str(value), initial_shape, target_shape), + dtype=dtype, + shape=tuple(target_shape), + ) + + def reduction( + self, + dtype: torch.dtype, + src_dtype: torch.dtype, + reduction_type: ReductionType, + value: CSEVariable | tuple[CSEVariable, ...], + ) -> CSEVariable | tuple[CSEVariable, ...]: + """ + codegen reduction of value to Triton according the reduction_type + """ + + def should_upcast(d: torch.dtype | None) -> bool: + return d is not None and d.is_floating_point and d.itemsize < 4 + + def maybe_upcast(value: CSEVariable) -> CSEVariable: + # Math reductions in small floats are less accurate because the Triton + # compiler does not automatically promote to FP32 for accumulation. + # Additionally, max/min reductions do not support FP16/BF16. Manually + # promote to FP32 here. + return ( + ops.to_dtype(value, torch.float32) + if should_upcast(value.dtype) + else value + ) + + do_upcast = pytree.tree_any(lambda v: should_upcast(v.dtype), value) + original_dtype = dtype + if do_upcast: + # Only promote FB16/BF16; do not promote other integer/boolean dtypes + value = pytree.tree_map(maybe_upcast, value) + src_dtype = torch.float32 if should_upcast(src_dtype) else src_dtype + dtype = torch.float32 if should_upcast(dtype) else dtype + + assert self.inside_reduction + masks = OrderedSet(f"{tree.prefix}mask" for tree in self.range_trees) + self.filter_masks(masks) + masks = sorted(masks) + if self._load_mask: + masks.append(self._load_mask) + reduction_range_prefix = self.range_trees[-1].prefix[0] + + # When we do native matmtul codegen, + # we don't want to keep the R0_BLOCK/R1_BLOCK in the accumulator. + # so instead of naively calling dense_size_str(), we filter out + # reduction block from accumulator and only keep (Y,X). + # In bmm (Z,Y,R)x(Z,R,X) case, we also remove z dimension from accumulator + # because 3d (Z,Y,X) tl.dot is somehow slower than 2d tl.dot. + # Instead, we force ZBLOCK to be always 1 during autotune. + dense_size_str: str + if self.is_native_matmul: + dense_sizes = self.dense_size_list() + assert len(dense_sizes) >= 3 + xy_sizes_only = [size for size in dense_sizes if "X" in size or "Y" in size] + dense_size_str = f"[{', '.join(xy_sizes_only)}]" + value_shape = tuple(xy_sizes_only) + else: + dense_size_str = self.dense_size_str() + value_shape = tuple(self.dense_size_list()) + + # Say we have + # tmp0 = ops.constant(1, torch.int64) + # tmp1 = ops.reduction(torch.int64, torch.int64, "sum", tmp0) + # tmp0 in the triton code is either a scalar, or single-element tensor + # so if we emit tl.sum directly, it will only give 1 instead of RBLOCK * 1 + # To avoid this, we broadcast to the expected shape first. + value = self._map_tuple_or_scalar( + lambda v: self.cse.generate( + self.compute, + f"tl.broadcast_to({v}, {dense_size_str})", + dtype=v.dtype, + shape=value_shape, + ), + value, + ) + + logical_index = None + if reduction_type in ("argmin", "argmax"): + if isinstance(value, tuple): + value, logical_index = value + + dim = self.triton_tensor_ndim() - self.num_reduction_dims + root_op: str + + def final_reduction( + buffer, + value: CSEVariable, + result_type: torch.dtype | None, + ) -> tuple[str, torch.dtype | None, BlockShapeType]: + """ + Helper to generate a reduction call, e.g. tl.sum. + """ + triton_reduction_fn = get_triton_reduction_function(reduction_type) + + value = self.reduction_collapse_dims(buffer, value, dtype) + if reduction_type == "dot": + # Native matmul is a special case because accumulator shape is fixed to (Y,X) + is_bmm = len(self.dense_size_list()) == 4 + assert value.shape is not None + if is_bmm: + result = f"{value}[None,:,:,None]" # (Y,X) to (Z=1,Y,X,R=1) + shape = [1, *value.shape, 1] + else: + result = f"{value}[:,:,None]" # (Y,X) to (Y,X,R=1) + shape = [*value.shape, 1] + else: + result, shape = self.reduction_resize_and_shape( # type: ignore[assignment] + f"{triton_reduction_fn}({value}, {dim})", value.shape + ) + + if result_type is not None: + result = f"{result}.to({self.dtype_to_str(result_type)})" + else: + result_type = value.dtype + + return result, result_type, shape + + def final_reduction_define( + buffer, + result_var: CSEVariable, + value: CSEVariable, + result_type: torch.dtype | None, + ) -> None: + """ + Generate a reduction and assign it to an existing variable. + """ + # pyrefly: ignore [bad-assignment] + value, _, _ = final_reduction(buffer, value, result_type) + buffer.splice(f"{result_var} = {value}") + + def final_argreduce(buffer, result_var, value, index): + value = self.reduction_collapse_dims(buffer, value, dtype) + index = self.reduction_collapse_dims(buffer, index, dtype) + buffer.splice( + f"""\ + {result_var}_val, {result_var}_idx = triton_helpers.{root_op}_with_index({value}, {index}, {dim}) + {result_var} = {self.reduction_resize(f"{result_var}_idx")} + """ + ) + + cache_key = (src_dtype, reduction_type, value) + if cache_key in self.cse.reduction_cache: + return self.cse.reduction_cache[cache_key] + + acc_type = triton_acc_type(src_dtype) + torch_acc_type = upcast_acc_dtype(src_dtype) + result_shape = list(self.dense_size_list()) + result_shape[dim] = "1" + result_var: Any = self.cse.newvar( + dtype=torch_acc_type, shape=tuple(result_shape) + ) + result_var.mask_vars = OrderedSet( + var for var in masks if not prefix_is_reduction(var[0]) + ) + cond = " & ".join(masks) + + def where_cond(tval, fval): + if not cond: + return tval + return TritonKernelOverrides.where(cond, tval, fval) + + if self.persistent_reduction: + default = ir.Reduction.default_value(reduction_type, src_dtype) + + def update_constant_dtype(constant, src_dtype, dst_dtype): + "update reduction constant mask value to match dst_dtype" + + # int is the only mask which may not fit within lower bitwidth, + # because float uses inf/-inf + if src_dtype.is_floating_point or src_dtype == torch.bool: + return constant + + if src_dtype == dst_dtype or constant == 0: + return constant + + if constant == torch.iinfo(src_dtype).max: + return torch.iinfo(dst_dtype).max + elif constant == torch.iinfo(src_dtype).min: + return torch.iinfo(dst_dtype).min + else: + return constant + + def _mask_value(value, default) -> CSEVariable: + default = update_constant_dtype(default, src_dtype, value.dtype) + default_str = self._map_tuple_or_scalar(constant_repr, default) + + return self.cse.generate( + self.compute, + where_cond(value, default_str), + dtype=value.dtype, + shape=value.shape, + ) + + masked_value: CSEVariable | Sequence[CSEVariable] | None + if reduction_type == "online_softmax_reduce": + # Don't generate mask value for online_softmax since we + # will fallback below + masked_value = None + elif isinstance(value, tuple): + masked_value = [_mask_value(v, d) for v, d in zip(value, default)] # type: ignore[arg-type] + elif reduction_type == "dot": + # Here, we don't perform the masking. + # Masking w/ where condition in native matmul is handled in ops.dot codegen. + # Since tl.dot performs reduction within the triton block, + # masking should happen before the tl.dot is called. + masked_value = self.cse.generate(self.compute, value, dtype=value.dtype) + else: + masked_value = _mask_value(value, default) + + if reduction_type in ("argmax", "argmin"): + assert isinstance(masked_value, CSEVariable) + accumulator_dtype = V.kernel.get_index_dtype_as_torch_dtype() + if logical_index: + accumulator_index = f"({str(logical_index)}).to({self.dtype_to_str(accumulator_dtype)})" + else: + accumulator_index = str( + self.cse.generate( + self.compute, + f"tl.broadcast_to({reduction_range_prefix}index, {masked_value}.shape)", + dtype=accumulator_dtype, + shape=masked_value.shape, + ) + ) + root_op = {"argmax": "max", "argmin": "min"}[reduction_type] + final_argreduce( + self.compute, result_var, masked_value, accumulator_index + ) + result_var.dtype = accumulator_dtype + elif reduction_type == "welford_reduce": + if self.cooperative_reduction: + # cooperative reductions require full welford for correctness + result_var = self.welford_reduce( + result_var, reduction_type, value, where_cond, acc_type, dtype + ) + else: + # For persistent reductions, don't bother with + # welford's algorithm since it uses more registers, and + # taking two reductions doesn't increase memory usage. + result_var = self.welford_reduce_fallback(dtype, value) + elif reduction_type == "welford_combine": + assert isinstance(masked_value, Sequence) + (mean, m2, weight) = masked_value + result_var = tuple( + self.cse.generate(self.compute, value, dtype=dtype, shape=shape) + for value, shape in self._welford( + self.compute, mean, m2, weight, dim, dtype + ) + ) + elif reduction_type == "online_softmax_reduce": + # All data is loaded to register anyway, no need to do + # online softmax + result_var = self.prepare_softmax_twopass_fallback(dtype, value) + else: + assert isinstance(masked_value, CSEVariable) + _result, _dtype, _shape = final_reduction( + self.compute, masked_value, masked_value.dtype + ) + result_var = self.cse.generate( + self.compute, _result, dtype=_dtype, shape=_shape + ) + else: + accumulator = self.cse.namedvar( + f"_{result_var}", + dtype=torch_acc_type, + shape=tuple(self.dense_size_list()), + ) + default = ir.Reduction.default_accumulator(reduction_type, src_dtype) + default = self._map_tuple_or_scalar(constant_repr, default) + if not isinstance(default, tuple): + if reduction_type == "dot": + dense_sizes = self.dense_size_list() + assert len(dense_sizes) >= 3 + xy_sizes_only = [ + size for size in dense_sizes if "X" in size or "Y" in size + ] + accumulator.shape = tuple(xy_sizes_only) + dense_size_str = f"[{', '.join(xy_sizes_only)}]" + self.body.writeline( + f"{accumulator} = tl.full({dense_size_str}, {default}, {acc_type})" + ) + else: + self.body.writeline( + f"{accumulator} = tl.full({self.dense_size_str()}, {default}, {acc_type})" + ) + + if reduction_type in ("argmax", "argmin"): + accumulator_index = f"_{result_var}_index" + index_dtype = self.features.select_index_dtype() + self.body.writeline( + f"{accumulator_index} = tl.full({self.dense_size_str()}, " + f"{torch.iinfo(index_dtype).max}, {self.dtype_to_str(index_dtype)})" + ) + root_op = {"argmax": "max", "argmin": "min"}[reduction_type] + # Use logical_index if it was unpacked, otherwise fall back to physical index + index_var = ( + f"({str(logical_index)}).to({self.dtype_to_str(index_dtype)})" + if logical_index is not None + else f"{reduction_range_prefix}index" + ) + self.compute.splice( + f"""\ + {accumulator}_next, {accumulator_index}_next = triton_helpers.{root_op}imum_with_index( + {accumulator}, {accumulator_index}, {value}, {index_var} + ) + {accumulator} = {where_cond(f"{accumulator}_next", accumulator)} + {accumulator_index} = {where_cond(f"{accumulator_index}_next", accumulator_index)} + """ + ) + final_argreduce( + self.post_loop_combine, result_var, accumulator, accumulator_index + ) + elif is_welford_reduction(reduction_type): + result_var = self.welford_reduce( + result_var, reduction_type, value, where_cond, acc_type, dtype + ) + elif reduction_type == "online_softmax_reduce": + accumulator_max = f"_{result_var}_max" + accumulator_sum = f"_{result_var}_sum" + + # setup accumulator + self.body.writeline( + f"{accumulator_max} = tl.full({self.dense_size_str()}, float('-inf'), {acc_type})" + ) + self.body.writeline( + f"{accumulator_sum} = tl.zeros({self.dense_size_str()}, {acc_type})" + ) + + # combine + # Note, we pass config.use_fast_math to the JITFunction + # since a triton kernel can not access a config. + self.compute.splice( + f""" + {accumulator_max}_next, {accumulator_sum}_next = triton_helpers.online_softmax_combine( + {accumulator_max}, {accumulator_sum}, {value}, {config.use_fast_math} + ) + """ + ) + + # mask + self.compute.splice( + f""" + {accumulator_max} = {where_cond(f"{accumulator_max}_next", accumulator_max)} + {accumulator_sum} = {where_cond(f"{accumulator_sum}_next", accumulator_sum)} + """ + ) + + # reduce. Similar to the final reduction for coopereative + # reduction + result_max = result_var + result_sum = self.cse.newvar(dtype=dtype, shape=result_max.shape) + + result_var = self.online_softmax_reduce_final_reduction( + self.post_loop_combine, + result_max, + result_sum, + accumulator_max, + accumulator_sum, + dim, + dtype, + ) + else: + combine_fn = ir.get_reduction_combine_fn(reduction_type, src_dtype) + updated = combine_fn(accumulator, value) + if reduction_type == "dot": + self.compute.writeline(f"{accumulator} = {updated}") + else: + self.compute.writeline( + f"{accumulator} = {where_cond(updated, accumulator)}" + ) + + if src_dtype == torch.bool: + # This is only really used for aten.any. It changes the + # final reduction of a non-persistent reduction from + # tmp5 = triton_helpers.max(_tmp5, 1)[:, None] + # to + # tmp5 = triton_helpers.max(_tmp5.to(tl.int8), 1)[:, None].to(tl.int1) + # which is needed because tl.reduce doesn't support tl.int1 + accumulator = self.cse.generate( + self.post_loop_combine, + f"{accumulator}.to(tl.int8)", + dtype=torch.int8, + shape=accumulator.shape, + ) + + final_reduction_define( + self.post_loop_combine, result_var, accumulator, None + ) + + if self.cooperative_reduction: + default = ir.Reduction.default_accumulator(reduction_type, src_dtype) + exit_stack = contextlib.ExitStack() + for buf in (self.post_loop_combine, self.post_loop_store): + # only do cooperative reduction combines if we have more than one thread block + buf.writeline("if HAS_RSPLIT:") + exit_stack.enter_context(buf.indent()) + + if reduction_type in ("argmax", "argmin"): + self.post_loop_combine.writeline( + f"{result_var}_bval = {self.reduction_resize(f'{result_var}_val')}" + ) + peer_val = self.codegen_cooperative_reduction_peer_combine( + f"{result_var}_bval", src_dtype, default + ) + index_dtype = self.features.select_index_dtype() + peer_idx = self.codegen_cooperative_reduction_peer_combine( + result_var, index_dtype, torch.iinfo(index_dtype).max + ) + final_argreduce(self.post_loop_store, result_var, peer_val, peer_idx) + elif is_welford_reduction(reduction_type): + assert reduction_type == "welford_reduce" + result_mean, result_m2, result_weight = result_var + peer_mean = self.codegen_cooperative_reduction_peer_combine( + result_mean, + upcast_acc_dtype(src_dtype), + default[0], # type: ignore[index] + ) + peer_m2 = self.codegen_cooperative_reduction_peer_combine( + result_m2, + upcast_acc_dtype(src_dtype), + default[1], # type: ignore[index] + ) + peer_weight = self.codegen_cooperative_reduction_peer_combine( + result_weight, + upcast_acc_dtype(src_dtype), + default[2], # type: ignore[index] + ) + self.welford_reduce_final_reduction( + self.post_loop_store, + result_mean, + result_m2, + result_weight, + peer_mean, + peer_m2, + peer_weight, + dim, + dtype, + ) + elif reduction_type == "online_softmax_reduce": + result_max, result_sum = result_var + assert isinstance(default, Sequence) + peer_max = self.codegen_cooperative_reduction_peer_combine( + result_max, upcast_acc_dtype(src_dtype), default[0] + ) + peer_sum = self.codegen_cooperative_reduction_peer_combine( + result_sum, upcast_acc_dtype(src_dtype), default[1] + ) + self.online_softmax_reduce_final_reduction( + self.post_loop_store, + result_max, + result_sum, + peer_max, + peer_sum, + dim, + dtype, + ) + else: + peers = self.codegen_cooperative_reduction_peer_combine( + result_var, upcast_acc_dtype(src_dtype), default + ) + final_reduction_define(self.post_loop_store, result_var, peers, None) + exit_stack.close() + + self.cse.reduction_cache[cache_key] = result_var + + result_tuple = result_var if isinstance(result_var, tuple) else (result_var,) + self.outside_loop_vars.update(result_tuple) + assert all(isinstance(x, TritonCSEVariable) for x in result_tuple) + + # If BF16/F16 upcasting was done, ensure the output is downcast to the + # expected dtype. + if do_upcast: + for result in result_tuple: + if result.dtype != original_dtype: + self.post_loop_combine.writeline( + f"{result} = {result}.to({triton_compute_type(original_dtype)})" + ) + + return result_var + + def _online_softmax_reduce( + self, buffer, accumulator_max, accumulator_sum, dim, dtype: torch.dtype + ): + accumulator_max = self.reduction_collapse_dims(buffer, accumulator_max, dtype) + accumulator_sum = self.reduction_collapse_dims(buffer, accumulator_sum, dtype) + result_max, result_sum = [str(self.cse.newvar(dtype=dtype)) for _ in range(2)] + buffer.splice( + f""" + {result_max}, {result_sum} = triton_helpers.online_softmax_reduce( + {accumulator_max}, {accumulator_sum}, {dim}, {config.use_fast_math}) + {result_max} = {self.reduction_resize(f"{result_max}")} + {result_sum} = {self.reduction_resize(f"{result_sum}")} + """ + ) + + return result_max, result_sum + + def _welford(self, buffer, mean, m2, weight, dim, dtype: torch.dtype): + """ + Helper to codegen triton_helpers.welford. + """ + mean, m2, weight = ( + self.reduction_collapse_dims(buffer, value, dtype) + for value in (mean, m2, weight) + ) + welford = f"triton_helpers.welford({mean}, {m2}, {weight}, {dim})" + + def reduced_shape(shape): + return tuple(shape[0:dim] + shape[dim + 1 :]) + + welford_results = [ + self.cse.newvar(dtype=dtype, shape=reduced_shape(value.shape)) + for value in (mean, m2, weight) + ] + buffer.writeline(f"{', '.join([str(r) for r in welford_results])} = {welford}") + + return tuple( + self.reduction_resize_and_shape(value, value.shape) + for value in welford_results + ) + + def welford_reduce( + self, result_var, reduction_type, value, where_cond, acc_type, dtype + ): + """Helper to codegen a welford reduction""" + dim = self.triton_tensor_ndim() - self.num_reduction_dims + + accumulator = TritonCSEVariable( + f"{result_var}_mean", + shape=tuple(self.dense_size_list()), + dtype=acc_type, + bounds=ValueRanges.unknown(), + ) + accumulator_m2 = TritonCSEVariable( + f"{result_var}_m2", + shape=tuple(self.dense_size_list()), + dtype=acc_type, + bounds=ValueRanges.unknown(), + ) + accumulator_weight = TritonCSEVariable( + f"{result_var}_weight", + shape=tuple(self.dense_size_list()), + dtype=acc_type, + bounds=ValueRanges.unknown(), + ) + self.body.writeline( + f"{accumulator} = tl.zeros({self.dense_size_str()}, {acc_type})" + ) + self.body.writeline( + f"{accumulator_m2} = tl.zeros({self.dense_size_str()}, {acc_type})" + ) + self.body.writeline( + f"{accumulator_weight} = tl.zeros({self.dense_size_str()}, {acc_type})" + ) + if reduction_type == "welford_combine": + mean, m2, weight = value + self.compute.splice( + f"""\ + {accumulator}_next, {accumulator_m2}_next, {accumulator_weight}_next = triton_helpers.welford_combine( + {accumulator}, {accumulator_m2}, {accumulator_weight}, + {mean}, {m2}, {weight} + ) + """ + ) + else: + assert reduction_type == "welford_reduce" + self.compute.splice( + f"""\ + {accumulator}_next, {accumulator_m2}_next, {accumulator_weight}_next = triton_helpers.welford_reduce( + {value}, {accumulator}, {accumulator_m2}, {accumulator_weight}, roffset == 0 + ) + """ + ) + self.compute.splice( + f"""\ + {accumulator} = {where_cond(f"{accumulator}_next", accumulator)} + {accumulator_m2} = {where_cond(f"{accumulator_m2}_next", accumulator_m2)} + {accumulator_weight} = {where_cond(f"{accumulator_weight}_next", accumulator_weight)} + """ + ) + result_mean = result_var + return self.welford_reduce_final_reduction( + self.post_loop_combine, + result_mean, + None, + None, + accumulator, + accumulator_m2, + accumulator_weight, + dim, + dtype, + ) + + def welford_reduce_final_reduction( + self, + buffer, + result_mean, + result_m2, + result_weight, + mean, + m2, + weight, + dim, + dtype, + ): + """Helper to codegen call to triton_helpers.welford""" + values = list(self._welford(buffer, mean, m2, weight, dim, dtype)) + + result_exprs = [result_mean, result_m2, result_weight] + for i, (result_expr, (value, shape)) in enumerate(zip(result_exprs, values)): + if result_expr is None: + result_expr = self.cse.newvar(dtype=dtype, shape=shape) + result_exprs[i] = result_expr + buffer.splice(f"{result_expr} = {value}") + + return tuple(result_exprs) + + def online_softmax_reduce_final_reduction( + self, buffer, result_max, result_sum, peer_max, peer_sum, dim, dtype + ): + accumulator_max = self.reduction_collapse_dims(buffer, peer_max, dtype) + accumulator_sum = self.reduction_collapse_dims(buffer, peer_sum, dtype) + buffer.splice( + f""" + {result_max}, {result_sum} = triton_helpers.online_softmax_reduce( + {accumulator_max}, {accumulator_sum}, {dim}, {config.use_fast_math}) + {result_max} = {self.reduction_resize(f"{result_max}")} + {result_sum} = {self.reduction_resize(f"{result_sum}")} + """ + ) + return result_max, result_sum + + def max_rsplit(self): + if self.fixed_config: + return self.fixed_config["RSPLIT"] + return TRITON_MAX_RSPLIT + + def codegen_cooperative_reduction_peer_combine( + self, result_var, dtype, default_val + ) -> CSEVariable: + """ + Generate code to save a [XBLOCK, RSPLIT] temporary workspace, where each thread block writes a different + column. After the barrier, every thread block loads the completed value so that it can compute the final + value independently. + """ + xnumel = self.numels["x"] + mask = "xindex < xnumel" if not self._has_constant_xmask() else None + + nbytes = xnumel * dtype.itemsize * self.max_rsplit() + ws_name, ws_offset = self.cooperative_reduction_workspace_cache.allocate(nbytes) + + self.post_loop_combine.splice( + f""" + {result_var}_ws = ({ws_name} + {self.index_to_str(ws_offset)}).to(tl.pointer_type({triton_type(dtype)})) + tl.store({result_var}_ws + (xindex * RSPLIT + rsplit_id), {result_var}, {mask}) + """, + strip=True, + ) + peers = self.create_cse_var( + f"{result_var}_peers", + shape=["XBLOCK", "RSPLIT"], + dtype=dtype, + bounds=ValueRanges.unknown(), + ) + self.post_loop_store.writeline( + f"{peers} = tl.load({result_var}_ws + (xindex * RSPLIT + rsplit_arange), " + f"rsplit_mask, eviction_policy='evict_first', other=triton_helpers.if_mask(rsplit_mask, {constant_repr(default_val)}))" + ) + return peers + + def store_reduction( + self, + name: str, + index: sympy.Expr, + value: CSEVariable, + ): + assert self.inside_reduction + self.inside_reduction = False + dtype = V.graph.get_dtype(name) + indexing = self.indexing( + index, + block_ptr=True, + tma_compatibility_checker=self.tma_compatibility_checker_cls( + kernel=self, + dtype=dtype, + for_store=True, + force=False, + ), + ) + self.inside_reduction = True + var = self.args.output(name) + + exit_stack = contextlib.ExitStack() + if self.cooperative_reduction: + exit_stack.enter_context( + self.guard_cooperative_store(name, self.post_loop_store) + ) + + self._handle_pdl_before_access(self.post_loop_store, var) + + if isinstance(indexing, (BlockPtrOptions, TensorDescriptorOptions)): + self.post_loop_store.writeline( + DeferredLine( + name, + self.codegen_block_ptr_store_line( + name, + indexing, + indexing.format(var), + value, + f", boundary_check={indexing.boundary_check()!r}", + ), + ) + ) + else: + assert isinstance(indexing, IndexingOptions) + + indexing_str = indexing.index_str + if ( + is_sympy_integer_like(index) + and value.shape is not None + and not all(str(x) == "1" for x in value.shape) + ): + value_shape = ", ".join(map(str, value.shape)) + indexing_str += f".broadcast_to({value_shape})" + + self.post_loop_store.writeline( + DeferredLine( + name, + f"tl.store({var} + ({indexing_str}), {value}, {indexing.mask_str})", + ) + ) + + exit_stack.close() + + def _lift_helper( + self, fn, values: tuple[CSEVariable, ...], dtypes: tuple[torch.dtype, ...] + ) -> str: + # Lift IR function for scan operations into a triton function + # in the global namespace + helper = IndentedBuffer() + helper.writeline("@triton.jit") + cse = CSE() + + args = [ + tuple( + cse.namedvar(f"arg{i}_{n}", dtype=dtype, shape=value.shape) + for n, (value, dtype) in enumerate(zip(values, dtypes)) + ) + for i in range(2) + ] + signature = ", ".join(str(x) for x in itertools.chain.from_iterable(args)) + helper.writeline(f"def {{name}}({signature}):") + + overrides = TritonOverrides() + + # Build a name that changes depending on fn to workaround a triton bug + # where the combine_fn to reduce and scan is not hashed, and so different + # scan ops may collide in the triton cache. + # This is fixed with the latest triton pin, but not the triton-rocm pin. + helper_name = "_triton_helper_fn" + + from torch._inductor.dtype_propagation import DtypePropagationOpsHandler + from torch._inductor.shape_propagation import ShapePropagationOpsHandler + + shape_handler = ShapePropagationOpsHandler() + dtype_handler = DtypePropagationOpsHandler() + + class CSEProxy(DefaultHandler): + def _default( + self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any] + ) -> Any: + nonlocal helper_name + helper_name += f"_{name}" + + output_dtype = getattr( + dtype_handler, + name, + )(*args, **kwargs) + + output_shape = getattr( + shape_handler, + name, + )(*args, **kwargs) + + return cse.generate( + helper, + getattr(overrides, name)(*args, **kwargs), + dtype=output_dtype, + shape=output_shape, + ) + + with helper.indent(), V.set_ops_handler(CSEProxy()): + outputs = fn(*args) + outputs = ", ".join(str(output) for output in outputs) + helper.writeline(f"return {outputs}") + + return self.helper_functions.add(helper.getvalue(), base_name=helper_name) + + def scan( + self, + dtypes: tuple[torch.dtype, ...], + combine_fn: Callable[ + [tuple[CSEVariable, ...], tuple[CSEVariable, ...]], tuple[CSEVariable, ...] + ], + values: tuple[CSEVariable, ...], + ) -> tuple[CSEVariable, ...]: + """ + Perform an associative scan on 'values'. + """ + assert self.inside_reduction + assert not self.cooperative_reduction, "TODO" + masks = OrderedSet(f"{tree.prefix}mask" for tree in self.range_trees) + self.filter_masks(masks) + masks = sorted(masks) + assert not self._load_mask, "ops.scan not supported inside ops.masked" + + broadcasted_values = [] + accumulators = [] + + dtypes = tuple(upcast_compute_type(dtype) for dtype in dtypes) + cse_compute = functools.partial(self.cse.generate, self.compute) + combine_helper_fn = self._lift_helper(combine_fn, values, dtypes) + dim = self.triton_tensor_ndim() - self.num_reduction_dims + + for value, dtype in zip(values, dtypes): + value_dtype = self.cse.generate( + self.compute, + f"{value}.to({triton_compute_type(dtype)})", + dtype=dtype, + shape=value.shape, + ) + value = self.cse.generate( + self.compute, + f"tl.broadcast_to({value_dtype}, {self.dense_size_str()})", + dtype=dtype, + shape=tuple(self.dense_size_list()), + ) + broadcasted_values.append(value) + + acc_type = triton_acc_type(dtype) + + if not self.persistent_reduction: + reduced_size = self.dense_size_list() + reduced_size[-1] = "1" + accumulator = self.cse.newvar(dtype=dtype, shape=reduced_size) + reduced_size_str = f"[{', '.join(reduced_size)}]" + + default = "float('nan')" if dtype.is_floating_point else "-1" + self.body.writeline( + f"{accumulator} = tl.full({reduced_size_str}, {default}, {acc_type})" + ) + + accumulators.append(accumulator) + + def csv(values): + return " ".join(f"{value}," for value in values) + + def cse_multiple(line, values, masks, dtypes): + n = len(values) + cache_keys = [f"{line}, {i}, {masks}" for i in range(n)] + if all(self.cse.contains(cache_key) for cache_key in cache_keys): + return [self.cse.get(cache_key) for cache_key in cache_keys] + result_vars = [ + self.cse.newvar(dtype=dtype, shape=value.shape) + for (dtype, value) in zip(dtypes, values) + ] + self.compute.writeline( + f"{csv(result_vars)} = {line}", + ) + for result_var, cache_key in zip(result_vars, cache_keys): + if masks: + result_var.mask_vars = masks # type: ignore[attr-defined] + self.cse.put(cache_key, result_var) + return tuple(result_vars) + + partial_scan_vars = cse_multiple( + f"tl.associative_scan(({csv(broadcasted_values)}), {dim}, {combine_helper_fn})", + broadcasted_values, + masks, + dtypes, + ) + + if not self.persistent_reduction: + # tl.reduce doesn't work for non-commutative operators, so instead + # of repeating the scan op as a reduction, we use sum to select the + # last scan value + def _partial_scan_shape(var): + if var.shape is None: + return None + else: + shape = list(var.shape) + shape[-1] = "1" + return shape + + partial_reduce_vars = [ + cse_compute( + f"triton_helpers.select_one(({partial_scan_var}), rbase == (RBLOCK - 1), dim=-1, keep_dims=True)", + dtype=upcast_compute_type(partial_scan_var.dtype), + shape=_partial_scan_shape(partial_scan_var), + ) + for partial_scan_var in partial_scan_vars + ] + accs_next = combine_fn(tuple(accumulators), tuple(partial_reduce_vars)) + full_scan_vars = combine_fn(tuple(accumulators), partial_scan_vars) + result_vars = [ + cse_compute( + f"tl.where(roffset > 0, {full_scan}, {partial_scan})", + dtype=partial_scan.dtype, + shape=partial_scan.shape, + ) + for full_scan, partial_scan in zip(full_scan_vars, partial_scan_vars) + ] + for acc_next, accumulator, partial_reduce in zip( + accs_next, accumulators, partial_reduce_vars + ): + self.compute.writeline( + f"{accumulator} = tl.where(roffset > 0, {acc_next}, {partial_reduce})" + ) + else: + result_vars = partial_scan_vars + + for result_var in result_vars: + assert isinstance(result_var, TritonCSEVariable) + result_var.mask_vars = OrderedSet(masks) + + return tuple(result_vars) + + def sort( + self, + dtypes: tuple[torch.dtype, ...], + values: tuple[CSEVariable, ...], + stable: bool, + descending: bool, + ) -> tuple[CSEVariable, ...]: + assert self.inside_reduction + assert not self.cooperative_reduction, "TODO" + masks = OrderedSet(f"{tree.prefix}mask" for tree in self.range_trees) + self.filter_masks(masks) + masks = sorted(masks) + assert not self._load_mask, "ops.sort not supported inside ops.masked" + assert self.persistent_reduction, ( + "ops.sort is only supported in persistent reductions" + ) + + cse_compute = functools.partial(self.cse.generate, self.compute) + dim = self.triton_tensor_ndim() - self.num_reduction_dims + + dtypes = tuple(upcast_compute_type(dtype) for dtype in dtypes) + assert len(dtypes) == len(values) + broadcasted_values = [ + cse_compute( + f"tl.broadcast_to({value}, {self.dense_size_str()})", + dtype=dtypes[i], + shape=tuple(self.dense_size_list()), + ) + for i, value in enumerate(values) + ] + + def csv(values): + return " ".join(f"{value}," for value in values) + + def cse_multiple(line, broadcasted_values, masks, dtypes): + n = len(broadcasted_values) + cache_keys = [f"{line}, {i}, {masks}" for i in range(n)] + if all(self.cse.contains(cache_key) for cache_key in cache_keys): + return [self.cse.get(cache_key) for cache_key in cache_keys] + result_vars = [ + self.cse.newvar(dtype=dtype, shape=value.shape) + for dtype, value in zip(dtypes, broadcasted_values) + ] # type: ignore[attr-defined] + self.compute.writeline( + f"{csv(result_vars)} = {line}", + ) + for result_var, cache_key in zip(result_vars, cache_keys): + if masks: + result_var.mask_vars = masks # type: ignore[attr-defined] + self.cse.put(cache_key, result_var) + return tuple(result_vars) + + assert self.range_trees[-1].is_reduction + rnumel = "None" if self._has_constant_mask(self.range_trees[-1]) else "rnumel" + + if len(values) == 2: + line = ( + f"triton_helpers.sort_with_index({broadcasted_values[0]}, {broadcasted_values[1]}," + f" {rnumel}, {dim}, stable={stable}, descending={descending})" + ) + result_vars = cse_multiple(line, broadcasted_values, masks, dtypes) + else: + raise AssertionError("Unhandled sort") + + for result_var, input_var in zip(result_vars, values): + result_var.mask_vars = masks # type: ignore[attr-defined] + result_var.bounds = input_var.bounds + + return tuple(result_vars) + + def codegen_prologue(self, code: IndentedBuffer): + """ + Generate the output from prologue. This should be + extracted from the subgraph, which is why this is + partitioned from codegen_body. + """ + if not self.prologue: + return + + code.splice(self.prologue) + self.prologue.clear() + self.prologue_cache.clear() + + def codegen_body(self): + """ + Concat output code from index_code, loads, compute, stores, + suffix into self.body. + + For pointwise kernels, this is called just once at the end. + + For reduction kernels, this generates a loop over the reduction + axis. + """ + if not ( + self.indexing_code + or self.loads + or self.stores + or self.compute + or self.post_loop_combine + or self.post_loop_store + ): + return + + loop_trees = [tree for tree in self.range_trees if tree.is_loop] + if self.mix_order_reduction: + assert self.persistent_reduction, ( + "Mix order reduction requires persistent reduction" + ) + accumname2var = {} + for idx, partial_accum in enumerate(self.saved_partial_accumulate): + reduction_type = partial_accum.reduction_type + default = ir.Reduction.default_accumulator(reduction_type, torch.float) + default = self._map_tuple_or_scalar(constant_repr, default) + name = f"accum{idx}" + self.body.writeline( + f"{name} = tl.full([R0_BLOCK], {default}, tl.float32)[None, :]" + ) + accumname2var[name] = self.cse.namedvar( + name, dtype=torch.float, shape=("1", "R0_BLOCK") + ) + self.body.writeline("split_size = min(RSPLIT_SIZE, xnumel - xoffset)") + self.body.writeline( + "for _ in tl.range(0, split_size, XBLOCK, num_stages=NUM_STAGES):" + ) + with self.body.indent(offset=1): + # generate xmask if it's not constant + if not self._has_constant_xmask(): + entry = self.range_trees[0] + assert entry.prefix == "x" + x = entry.prefix + self.body.writeline(f"{x}mask = {entry.name} < {x}numel") + self.body.splice(self.indexing_code) + self.body.writelines( + [ + "xindex += XBLOCK", + ] + ) + self.body.splice(self.loads) + self.body.splice(self.compute) + self.body.splice(self.stores) + self.body.splice(self.post_loop_store) + + # no need to sum if XBLOCK == 1, or does that matter? + for idx, partial_accum in enumerate(self.saved_partial_accumulate): + var = partial_accum.value + name = f"accum{idx}" + combine_fn = ir.get_reduction_combine_fn( + partial_accum.reduction_type, torch.float + ) + triton_reduction_function = get_triton_reduction_function( + partial_accum.reduction_type, + ) + newval = self.cse.generate( + self.body, + f"{triton_reduction_function}({var}, 0)", + dtype=var.dtype, + shape=("R0_BLOCK",), + ) + import unittest + + with unittest.mock.patch.object(self, "compute", self.body): + updated = combine_fn( + accumname2var[name], + newval, + ) + self.body.writeline(f"{name} = {updated}") + + for idx in range(len(self.saved_partial_accumulate)): + self.body.writeline( + f"tl.store(ws_ptr + (tl.program_id(0) + {idx} * tl.num_programs(0)) * r0_numel + r0_index, accum{idx}, r0_mask)" + ) + + elif self.inside_reduction and len(loop_trees) > 0: + # Write the loop headers. + for level, tree in enumerate(loop_trees): + with self.body.indent(offset=level): + prefix = tree.prefix + loop_start = "rsplit_start" if self.cooperative_reduction else "0" + loop_end = ( + "rsplit_end" if self.cooperative_reduction else f"{prefix}numel" + ) + # Conditionalize pipelining on HIP for Triton due to + # reports of numerical inaccuracies on older Triton + if torch.version.hip and get_triton_version() > (3, 2): + num_stages = ", num_stages = 2" + else: + num_stages = "" + self.body.writeline( + f"for {prefix}offset in tl.range({loop_start}, {loop_end}, {prefix.upper()}BLOCK{num_stages}):" + ) + with self.body.indent(offset=level + 1): + self.iteration_ranges_codegen_header(tree, self.body) + + # The innermost loop performs the reduction. + with self.body.indent(offset=len(loop_trees)): + self.codegen_reduction_indices(self.body) + self.body.splice(self.indexing_code) + self.body.splice(self.loads) + self.body.splice(self.compute) + self.body.splice(self.stores) + + # Write loop suffixes. + for level, tree in reversed([*enumerate(loop_trees)]): + with self.body.indent(offset=level + 1): + # Advance pointers at the end of each loop. + for block_ptr, advancement in self.pointer_advancements[ + tree.symt + ].items(): + # Subtract any advancements made in the previous loop level. + if level < len(loop_trees) - 1: + prev_tree = loop_trees[level + 1] + prev_advancements = self.pointer_advancements[ + prev_tree.symt + ] + # block_ptr may not exist in the inner loop's advancements + # if its advancement was identity (zero) and was skipped + if block_ptr in prev_advancements: + prev_advancement = prev_advancements[block_ptr] + prev_block = TritonSymbols.get_block_size(prev_tree) + prev_num_iter = CeilDiv(prev_tree.numel, prev_block) + advancement = [ + cur - prev * prev_num_iter + for cur, prev in zip(advancement, prev_advancement) + ] + + self.body.writeline( + DeferredLine( + self.block_ptr_to_buffer[block_ptr], + f"{block_ptr} = tl.advance({block_ptr}, {V.kernel.index_to_str(advancement)})", + ) + ) + + # Invalidate any cache entries that came from inside the loop. + self.cse.invalidate(self.outside_loop_vars) + tree.cache_clear() + else: + self.body.splice(self.indexing_code) + self.body.splice(self.loads) + self.body.splice(self.compute) + self.body.splice(self.stores) + self.body.splice(self.post_loop_combine) + if self.cooperative_reduction and ( + self.post_loop_combine or self.post_loop_store + ): + sem_ptr = f"{self.semaphores_name} + tl.program_id(1)" + self.body.splice( + f""" + if HAS_RSPLIT: + triton_helpers.x_grid_barrier({sem_ptr}) + """, + strip=True, + ) + self.cooperative_reduction_workspace_cache.on_loop_end() + if not self.mix_order_reduction: + self.body.splice(self.post_loop_store) + self.indexing_code.clear() + self.loads.clear() + self.compute.clear() + self.stores.clear() + self.post_loop_combine.clear() + self.post_loop_store.clear() + + def kernel_benchmark_extra_args(self) -> list[str]: + args = [] + if self.need_numel_args(): + numel_args: list[sympy.Expr] = [] + self.add_numel_to_call_args("", numel_args, []) + for arg in numel_args: + if isinstance(arg, int): + args.append(str(arg)) + elif isinstance(arg, SymbolicCallArg): + hint = V.graph.sizevars.optimization_hint_with_override( + arg.inner_expr, + hint_override=self.hint_override, + ) + args.append(str(hint)) + elif isinstance(arg, sympy.Expr): + hint = V.graph.sizevars.optimization_hint_with_override( + arg, + hint_override=self.hint_override, + ) + args.append(str(hint)) + else: + raise ValueError(f"Unsupported numel argument type: {type(arg)}") + return args + + def codegen_kernel_benchmark(self, num_gb: float | None) -> IndentedBuffer: + """ + Generates Python code for benchmarking this Triton kernel. + - Creates example inputs (random tensors, constants, sizes). + - Runs the kernel on the current GPU/stream. + - Prints runtime (ms) and throughput (GB/s) using `num_gb`. + Args: + num_gb (float): The number of gigabytes to use for throughput calculation. + Returns: + IndentedBuffer: A buffer containing the generated Python benchmark code. + """ + result = IndentedBuffer() + _argdefs, call_args, signature, _ = self.args.python_argdefs() + + result.writelines(["", "", "def get_args():"]) + with result.indent(): + name_cnt = itertools.count() + var_names = [] + for arg_name, arg_sig in zip(call_args, signature): + var_name = f"arg_{next(name_cnt)}" + buf = V.graph.try_get_buffer(arg_name) + if buf: + size = V.graph.sizevars.optimization_hints_with_override( + buf.get_size(), + hint_override=self.hint_override, + ) + stride = V.graph.sizevars.optimization_hints_with_override( + buf.get_stride(), + hint_override=self.hint_override, + ) + result.writeline( + f"{var_name} = rand_strided({size}, {stride}, device='{buf.get_device()}', dtype={buf.get_dtype()})" # noqa: B950 line too long + ) + elif arg_name in V.graph.constants: + # note that random seed is put in V.graph.constants + const_tensor = V.graph.constants[arg_name] + size = V.graph.sizevars.optimization_hints_with_override( + const_tensor.size(), + hint_override=self.hint_override, + ) + stride = V.graph.sizevars.optimization_hints_with_override( + const_tensor.stride(), + hint_override=self.hint_override, + ) + result.writeline( + f"{var_name} = rand_strided({size}, {stride}, device='{const_tensor.device}', dtype={const_tensor.dtype})" # type: ignore[arg-type] # noqa: B950 line too long + ) + elif isinstance(arg_sig, SizeArg): + symval_hint = V.graph.sizevars.optimization_hint_with_override( + arg_sig.expr, + hint_override=self.hint_override, + ) + + # Force the seed_offset to be 0 so calls to the same kernel + # using different seed offset will have the same benchmark harness. + # We can dedup kernel definitions in this case. + if "seed_offset" in arg_sig.name: + symval_hint = 0 + result.writeline(f"{var_name} = {symval_hint}") + elif isinstance(arg_sig, WorkspaceArg): + device = V.graph.get_current_device_or_throw() + count = V.graph.sizevars.optimization_hint_with_override( + arg_sig.count, hint_override=self.hint_override + ) + result.writeline( + f"{var_name} = torch.zeros({count}, device='{device}', dtype={arg_sig.dtype})" + ) + else: + raise KeyError( + f"Don't find the buffer or const tensor for {arg_name}" + ) + var_names.append(var_name) + var_names.extend(self.kernel_benchmark_extra_args()) + result.writeline(f"return {', '.join(var_names)},") + + result.writelines(["\n", "\n", "def call(args):"]) + current_device = V.graph.get_current_device_or_throw() + index = current_device.index + with result.indent(): + result.writeline(f"with {V.graph.device_ops.device_guard(index)}:") + with result.indent(): + result.writeline( + V.graph.device_ops.set_device(index) + ) # no-op to ensure context + stream_name = get_raw_stream_name(index) + result.writeline(f"{stream_name} = get_raw_stream({index})") + result.writeline( + f"{str(Placeholder.KERNEL_NAME)}.run(*args, stream={stream_name})" + ) + + # benchmark all configs + result.writelines(["\n", "\n", "def benchmark_all_configs(args):"]) + with result.indent(): + result.writeline(f"with {V.graph.device_ops.device_guard(index)}:") + with result.indent(): + result.writeline( + V.graph.device_ops.set_device(index) + ) # no-op to ensure context + result.writeline( + f"return {str(Placeholder.KERNEL_NAME)}.benchmark_all_configs(*args)" + ) + + result.writelines(["\n", "\n", "if __name__ == '__main__':"]) + with result.indent(): + result.writeline( + "from torch._inductor.runtime.benchmarking import benchmarker" + ) + result.writeline("") + + result.writeline("args = get_args()") + result.writeline( + f"ms = benchmarker.benchmark(lambda: call(args), device='{V.graph.get_current_device_or_throw().type}', rep=40)" # noqa: B950 line too long + ) + result.writeline(f"num_gb = {num_gb}") + result.writeline("gb_per_s = num_gb / (ms / 1e3)") + result.writeline( + 'print(f"{ms:.3f}ms {num_gb:.3f}GB {gb_per_s:.2f}GB/s")' + ) + + return result + + def imports_for_benchmark_kernel(self): + return textwrap.dedent( + """ + from torch._dynamo.testing import rand_strided + {} + import torch + """.format(V.graph.device_ops.import_get_raw_stream_as("get_raw_stream")) + ) + + def _get_heuristic(self): + if self.fixed_config: + return "fixed_config" + elif self.cooperative_reduction: + return "cooperative_reduction" + elif self.persistent_reduction: + assert self.inside_reduction + return "persistent_reduction" + elif self.inside_reduction: + return "reduction" + return "pointwise" + + @classmethod + @lru_cache(None) + def gen_common_triton_imports(cls) -> str: + imports = IndentedBuffer() + imports.splice( + """ + import triton + import triton.language as tl + """ + ) + try: + import triton.language.extra.tlx # noqa: F401 + + imports.splice( + """ + import triton.language.extra.tlx as tlx # noqa: F401 + """ + ) + except ImportError: + pass + if attr_desc := gen_attr_descriptor_import(): + imports.writeline(attr_desc) + + imports.splice( + """ + from torch._inductor.runtime import triton_helpers, triton_heuristics + from torch._inductor.runtime.triton_helpers import libdevice, math as tl_math + from torch._inductor.runtime.hints import AutotuneHint, ReductionHint, TileHint, DeviceProperties + """ + ) + if config.triton.proton_profiling: + imports.splice( + """ + import triton.profiler as proton + import triton.profiler.language as pl + pl.enable_semantic('triton') + """ + ) + + return imports.getvalue() + + @classmethod + def triton_meta_common(cls): + return { + "enable_fp_fusion": not config.emulate_precision_casts, + "launch_pdl": cls._enable_pdl_codegen(), + "disable_ftz": config.eager_numerics.disable_ftz, + } + + @classmethod + def inductor_meta_common(cls): + inductor_meta = { + "backend_hash": torch.utils._triton.triton_hash_with_backend(), + "assert_indirect_indexing": config.assert_indirect_indexing, + "autotune_local_cache": config.autotune_local_cache, + "autotune_pointwise": config.triton.autotune_pointwise, + "autotune_remote_cache": config.autotune_remote_cache, + "force_disable_caches": config.force_disable_caches, + "dynamic_scale_rblock": config.dynamic_scale_rblock, + "max_autotune": config.max_autotune, + "max_autotune_pointwise": config.max_autotune_pointwise, + "min_split_scan_rblock": config.triton.min_split_scan_rblock, + "spill_threshold": config.triton.spill_threshold, + "store_cubin": config.triton.store_cubin, + "deterministic": config.deterministic, + "force_filter_reduction_configs": config.test_configs.force_filter_reduction_configs, + "mix_order_reduction_allow_multi_stages": config.triton.mix_order_reduction_allow_multi_stages, + } + + if config.write_are_deterministic_algorithms_enabled: + inductor_meta["are_deterministic_algorithms_enabled"] = ( + torch.are_deterministic_algorithms_enabled() + ) + + if torch.version.hip is not None: + inductor_meta["is_hip"] = True + if config.is_fbcode(): + inductor_meta["is_fbcode"] = True + if config.profile_bandwidth: + inductor_meta["profile_bandwidth"] = config.profile_bandwidth + inductor_meta["profile_bandwidth_regex"] = config.profile_bandwidth_regex + inductor_meta["profile_bandwidth_output"] = config.profile_bandwidth_output + inductor_meta["profile_bandwidth_with_do_bench_using_profiling"] = ( + config.profile_bandwidth_with_do_bench_using_profiling + ) + if config.coordinate_descent_tuning: + inductor_meta["coordinate_descent_tuning"] = ( + config.coordinate_descent_tuning + ) + inductor_meta["coordinate_descent_search_radius"] = ( + config.coordinate_descent_search_radius + ) + inductor_meta["coordinate_descent_check_all_directions"] = ( + config.coordinate_descent_check_all_directions + ) + return inductor_meta + + def codegen_kernel(self, name=None) -> str: + """ + Convert the TritonKernel from Inductor SIMD IR to triton code, including inductor triton heuristics, imports, + metadata, and benchmarking infra. + """ + + code = IndentedBuffer() + + size_hints = {} + for prefix, numel in self.numels.items(): + if prefix_is_reduction(prefix) and not self.inside_reduction: + continue + + numel_hint = V.graph.sizevars.optimization_hint(numel) + size_hint = next_power_of_2(int(numel_hint)) + size_hints[prefix] = size_hint + + if name is None: + code.splice(self.gen_common_triton_imports()) + device_type = V.graph.get_current_device_or_throw().type + if device_type == "cpu": + code.splice("triton_helpers.set_driver_to_cpu()") + else: + code.splice("triton_helpers.set_driver_to_gpu()") + + if config.benchmark_kernel: + code.splice(self.imports_for_benchmark_kernel()) + + argdefs, _, signature, _ = self.args.python_argdefs() + # maps actual expression to SizeArg if it is in sizevars replacements + for i, arg in enumerate(signature): + if isinstance(arg, SizeArg): + # mypy is unhappy about the sympy.Expr + # type for the key of the dict below + symbol = cast(sympy.Symbol, arg.expr) + if symbol in V.graph.sizevars.inv_precomputed_replacements: + signature[i] = SizeArg( + arg.name, V.graph.sizevars.inv_precomputed_replacements[symbol] + ) + + mutated_args: OrderedSet[str] = OrderedSet() + for mutation in self.mutations: + if mutation in self.args.input_buffers: + mutated_args.add(self.args.input_buffers[mutation]) + if ( + mutation in self.args.inplace_buffers + and mutation not in V.graph.removed_buffers + and mutation not in self.removed_buffers + ): + mutated_args.add( + cast(InplacedBuffer, self.args.inplace_buffers[mutation]).inner_name + ) + if mutation in self.args.output_buffers: + mutation_arg = self.args.output_buffers[mutation] + assert not isinstance(mutation_arg, RemovedArg) + mutated_args.add(mutation_arg) + + # Note: [Workspace Mutation] + # workspace arguments are mutated, but are not marked as mutations in self.mutations + # because their buffers are added during codegen, and aren't tracked during + # lowering/scheduling. So we add them as mutated_args explicitly below. + # + # In the logic below, we only mark the workspaces a mutated if they are marked with + # zero_fill: that's because, if we don't expect the buffer to be pre-filled with + # zeros, then, although we still mutate the data, we don't care about those + # mutations because we don't make any assumptions about the contents of the + # workspace buffer. Similarly, ZERO_PER_GRAPH requires the kernel to return + # the buffer back to its original state. + for argname, arg in zip(argdefs, signature): + if ( + isinstance(arg, WorkspaceArg) + and arg.zero_mode == WorkspaceZeroMode.ZERO_ON_CALL + ): + mutated_args.add(argname.name) + + # pyrefly: ignore [bad-assignment] + mutated_args = sorted(mutated_args) + + for tree in self.active_range_trees(): + sizearg = SizeArg(f"{tree.prefix}numel", tree.numel) + signature.append(sizearg) + argdefs.append(ArgName(sizearg.name)) + # constexpr version causes issues, see + # https://github.com/pytorch/torchdynamo/pull/1362 + # triton_meta["constants"][len(argdefs)] = V.graph.sizevars.size_hint( + # tree.numel + # ) + # argdefs.append(f"{tree.prefix}numel: tl.constexpr") + + def add_constexpr_arg(arg_name): + # new versions (but not old versions) of Triton need constexprs included in the signature + if triton_version_uses_attrs_dict(): + signature.append(ConstexprArg(arg_name)) + argdefs.append(ArgName(arg_name, is_constexpr=True)) + + for tree in self.range_trees: + if tree.is_reduction and self.persistent_reduction: + # Rn_BLOCK for persistent_reduction is defined in codegen_static_numels + continue + if tree.tensor_dim is None: + continue + + add_constexpr_arg(f"{tree.prefix.upper()}BLOCK") + + if self.cooperative_reduction: + add_constexpr_arg("RSPLIT") + + if self.mix_order_reduction: + add_constexpr_arg("RSPLIT_SIZE") + add_constexpr_arg("NUM_STAGES") + + triton_meta_signature = signature_to_meta( + signature, size_dtype=self.index_dtype, argdefs=argdefs + ) + triton_meta: dict[str, Any] = { + "signature": triton_meta_signature, + "device": DeviceProperties.create(V.graph.get_current_device_or_throw()), + "constants": {}, + "native_matmul": ( + torch._inductor.config.triton.native_matmul + and ("tl.dot" in str(self.body) or "tl.dot" in str(self.compute)) + ), + **self.triton_meta_common(), + } + + if self.cooperative_reduction: + # Cooperative reductions rely on multi-block synchronization that + # requires cooperative-grid launches to avoid hanging. + triton_meta["launch_cooperative_grid"] = True + + # Skip memory optimization for forward of the training loop where we expect + # every new node will increase the peak memory and our greedy approach would + # introduce a lot of unnecessary cpu copies. + optimize_mem = V.graph.is_inference or V.graph.is_backward + + inductor_meta = { + "grid_type": self._get_grid_type().__name__, + # Triton will not accept an OrderedSet for autotune_hints + "autotune_hints": set(self.autotune_hints), # noqa: set_linter + "kernel_name": str(Placeholder.DESCRIPTIVE_NAME), + "mutated_arg_names": mutated_args, + "optimize_mem": optimize_mem, + "no_x_dim": self.no_x_dim, + "atomic_add_found": self.atomic_add_found, + "num_load": self.num_load, + "num_store": self.num_store, + "num_reduction": self.num_reduction, + **self.inductor_meta_common(), + } + + if self.mix_order_reduction: + inductor_meta["RSPLIT_SIZE"] = self.rsplit_size + + if config.deterministic or config.test_configs.force_filter_reduction_configs: + inductor_meta["has_loadstore_with_contiguous_rdim"] = ( + self.has_load_with_contiguous_rdim + or self.has_store_with_contiguous_rdim + ) + + # Bail on 3d tiling, which has more complicated coalesce patterns + looped_red = V.kernel.features.is_reduction() and not self.persistent_reduction + tiling_scores = self.tiling_scores + two_d_red = len(self.tiling) == 2 + if looped_red and two_d_red: + memory_stats = self.features.memory_stats(self.tiling) + dim_stats = memory_stats.persistent.memory.dim[0] + mem_ops_per_thread = dim_stats.count_per_thread + + if ( + tiling_scores is not None + and "x" in tiling_scores + and "r0_" in tiling_scores + ): + # large rblock inhibits xblock size, dont attempt if there is a decent amount of + # reads coalesced by xblock + r_coalesce_ratio = tiling_scores["r0_"] / max(tiling_scores["x"], 1) + contiguous_red = r_coalesce_ratio >= INNER_REDUCTION_RATIO_THRESHOLD + else: + contiguous_red = ( + self.features.get_reduction_hint(tiling_scores) + == ReductionHint.INNER + ) + + looped_mem = memory_stats.looped.memory.bytes + persistent_mem = memory_stats.persistent.memory.bytes + # check that we save significant memory by doing persistent + saved_bytes_ratio = V.graph.sizevars.optimization_hint(looped_mem) / max( + V.graph.sizevars.optimization_hint(persistent_mem), + 1, + ) + + # TODO - rnumel should be reasonably close to power of 2 + if ( + # significant memory bandwidth savings + saved_bytes_ratio >= 1.3 + and contiguous_red + # TODO - need more detailed register analysis + and V.graph.sizevars.statically_known_leq( + self.features.reduction_numel, 32768 + ) + # We will already generate a persistent config in this case + and V.graph.sizevars.statically_known_gt( + self.features.reduction_numel, 2048 + ) + and mem_ops_per_thread <= 10 + ): + inductor_meta["add_persistent_rblock"] = True + + if self.tiling_scores: + inductor_meta["tiling_scores"] = self.tiling_scores + + if self.tma_min_block_sizes: + inductor_meta["tma_min_block_sizes"] = self.tma_min_block_sizes + + if self.cooperative_reduction: + inductor_meta["persistent_reduction"] = self.persistent_reduction + + num_gb = None + if config.benchmark_kernel or config.profile_bandwidth: + num_gb = self.estimate_kernel_num_bytes() / 1e9 + if num_gb is not None: + inductor_meta["kernel_num_gb"] = num_gb + if config.benchmark_kernel: + flops = self.estimate_flops() + if flops is not None: + inductor_meta["kernel_flop"] = flops + + # Triton compiler includes equal_to_1 args into constants even + # when they are not constexpr. otherwise there may be a segfault + # during launching the Inductor-compiled Triton kernel. + # https://github.com/pytorch/pytorch/issues/120478#issuecomment-1962822307 + # https://github.com/triton-lang/triton/blob/231efe9ed2d200be0f69a07c298e4342b08efe3d/python/triton/runtime/jit.py#L384 + for arg_num in equal_1_arg_indices(signature): # type: ignore[index] + triton_meta["constants"][signature[arg_num].name] = 1 # type: ignore[index,union-attr] + + self.triton_meta = triton_meta + self.inductor_meta = inductor_meta + + self.codegen_prologue(self.body) + self.codegen_body() + self._filter_pdl(self.body) + + # Compute configs after codegen_body() so we know if the kernel + # uses atomic ops. On HIP, buffer ops don't support atomics, so + # we must not tag any args with pointer_range_32 in that case. + if torch.version.hip is not None and self.atomic_add_found: + triton_meta["configs"] = [config_of(signature, pointer_range_override=())] + else: + triton_meta["configs"] = [config_of(signature)] + + for helper in self.helper_functions: + code.writeline("") + code.splice(helper) + + if self.fixed_config: + heuristics_line = f""" + @triton_heuristics.{self._get_heuristic()}( + config={self.fixed_config.config!r}, + filename=__file__, + triton_meta={triton_meta!r}, + inductor_meta={inductor_meta!r} + ) + @triton.jit + """ + elif self.inside_reduction: + reduction_hint = self.features.get_reduction_hint(self.tiling_scores) + heuristics_line = f""" + @triton_heuristics.{self._get_heuristic()}( + size_hints={size_hints!r}, + reduction_hint={reduction_hint}, + filename=__file__, + triton_meta={triton_meta!r}, + inductor_meta={inductor_meta!r} + ) + @triton.jit + """ + else: + tile_hint = "" + if len(size_hints) == 2: + if ( + len(non_constexpr_signature(signature)) == 4 + ): # input, output and 2 args + tile_hint = "tile_hint=TileHint.SQUARE," + else: + tile_hint = "tile_hint=TileHint.DEFAULT," + heuristics_line = f""" + @triton_heuristics.{self._get_heuristic()}( + size_hints={size_hints!r}, {tile_hint} + filename=__file__, + triton_meta={triton_meta!r}, + inductor_meta={inductor_meta!r}, + min_elem_per_thread={self.min_elem_per_thread} + ) + @triton.jit + """ + code.splice(heuristics_line) + kernel_name = name or str(Placeholder.KERNEL_NAME) + code.writeline( + f"def {kernel_name}({', '.join(x.full_name() for x in argdefs)}):" + ) + with code.indent(): + if config.triton.proton_profiling: + code.writeline(f'pl.enter_scope("{kernel_name}")') + self.codegen_static_numels(code) + for old, new in self.args.aliases(): + code.writeline(f"{old} = {new}") + code.splice(self.body) + if config.triton.proton_profiling: + code.writeline(f'pl.exit_scope("{kernel_name}")') + + if config.benchmark_kernel: + code.splice(self.codegen_kernel_benchmark(num_gb)) + + return code.getvalue() + + @staticmethod + def _get_persistent_RBLOCK(rnumel): + rnumel = V.graph.sizevars.simplify(rnumel) + if isinstance(rnumel, (sympy.Integer, int)): + val = int(rnumel) + val = next_power_of_2(val) + else: + val = 1 + while not V.graph.sizevars.statically_known_leq(rnumel, val): + if val > 16 * 1024: + raise ValueError(f"Failed to find static RBLOCK for {rnumel}") + val *= 2 + + return val + + return val + + @staticmethod + def has_persistent_RBLOCK(rnumel): + try: + TritonKernel._get_persistent_RBLOCK(rnumel) + return True + except ValueError: + return False + + def codegen_static_numels(self, code): + """ + We get a small speedup from hard coding numels if they are static. + + This code stomps on the passed-in values by writing an constant to the top of the kernel. + + In a kernel like: + def KERNEL_NAME(in_ptr0, in_ptr1, out_ptr2, xnumel, r0_numel, XBLOCK : tl.constexpr, R0_BLOCK : tl.constexpr): + + We would add + xnumel = 4096 + r0_numel = 768 + + After the signature, before the kernel code, if we decided to make these static. As its hardcoded, it becomes + a better signal to triton on how to unroll and do some static indexing. So, it's not so much that downstream + knows that its a static numel, as that you just plop a constant into the kernel. + """ + + def is_static_integer(expr: sympy.Expr) -> bool: + return isinstance(expr, (sympy.Integer, int)) + + for tree in self.range_trees: + if not tree.is_reduction or self.inside_reduction: + simplified_tree_numel = V.graph.sizevars.simplify(tree.numel) + if is_static_integer(simplified_tree_numel): + code.writeline(f"{tree.prefix}numel = {int(simplified_tree_numel)}") + + if tree.is_reduction and self.persistent_reduction: + if self.cooperative_reduction: + numel = self.kexpr(self.rename_indexing(tree.numel)) + val = f"triton_helpers.constexpr_next_power_of_2(({numel} + RSPLIT - 1) // RSPLIT)" + else: + val = self._get_persistent_RBLOCK(tree.numel) + if self.is_native_matmul: + # tl.dot only supports shapes >= 16 + val = max(val, 16) + + code.writeline(f"{tree.prefix.upper()}BLOCK: tl.constexpr = {val}") + + if tree.prefix == "x" and self.no_x_dim: + code.writeline("XBLOCK: tl.constexpr = 1") + + def _get_grid_type(self) -> type[triton_heuristics.GridExpr]: + n = sum([int(not tree.is_reduction) for tree in self.range_trees]) + if self.mix_order_reduction: + assert n == 1 + return triton_heuristics.MixOrderReductionGrid + elif self.cooperative_reduction: + assert n == 1 + return triton_heuristics.CooperativeReductionGrid + elif n == 1: + return triton_heuristics.Grid1D + elif n == 2: + if any(map(self.needs_yz_grid_overflow, self.range_trees)): + return triton_heuristics.Grid2DWithYZOverflow + return triton_heuristics.Grid2D + elif n == 3: + if self.is_native_matmul: + return triton_heuristics.BatchMatmulGrid3D + return triton_heuristics.Grid3D + raise ValueError(f"Unsupported number of dimensions: {n}") + + def add_numel_to_call_args(self, name, call_args, arg_types): + # TODO(jansel): if there are constants, we shouldn't bother passing them as args + for tree in self.range_trees: + if isinstance(tree.numel, (sympy.Integer, sympy.Symbol)): + expr = tree.numel + else: + expr = V.graph.wrapper_code.generate_numel_expr(name, tree) + + if not tree.is_reduction or self.inside_reduction: + call_args.append(expr) + arg_types.append(type(expr)) + + def call_kernel( + self, name: str, node: IRNode | None = None, deallocate_ws: bool = True + ): + wrapper = V.graph.wrapper_code + wrapper.write_triton_header_once() + _, call_args, _, arg_types = self.args.python_argdefs() + self.add_numel_to_call_args(name, call_args, arg_types) + + for ws in self.args.workspace_args: + wrapper.generate_workspace_allocation(ws) + + wrapper.generate_kernel_call( + name, + call_args, + triton=True, + arg_types=arg_types, + triton_meta=self.triton_meta, + inductor_meta=self.inductor_meta, + ) + + if deallocate_ws: + self.deallocate_workspaces() + + def codegen_nan_check(self) -> None: + wrapper = V.graph.wrapper_code + _, call_args, arg_signatures, _ = self.args.python_argdefs() + for arg, arg_signature in zip(call_args, arg_signatures): + if isinstance(arg_signature, TensorArg): + if V.graph.cpp_wrapper: + wrapper.writeline( + f'AOTI_TORCH_ERROR_CODE_CHECK(aoti_torch_check_inf_and_nan("{arg}", {arg}));' + ) + else: + line = f"assert not {arg}.isnan().any().item()" + wrapper.writeline(line) + line = f"assert not {arg}.isinf().any().item()" + wrapper.writeline(line) + + def create_cse_var(self, *args, **kwargs) -> TritonCSEVariable: + return TritonCSEVariable(*args, **kwargs) + + def codegen_iteration_ranges_entry(self, entry: IterationRangesEntry): + line = f"{entry.name} = {self.kexpr(self.rename_indexing(entry.expr))}" + + # mix order reduction introduces an extra loop across the x + # dimension + if entry.root.is_loop or (self.mix_order_reduction and entry.prefix == "x"): + self.indexing_code.writeline(line) + else: + # lift non-reduction stores outside loop + self.body.writeline(line) + + def iteration_ranges_ranges_code(self, entry: IterationRangesRoot) -> str: + assert entry.tensor_dim is not None + size = self.indexing_size_str(entry.tensor_dim) + # For batch matmul, we always set the ZBLOCK=1. + # In this case, we found not broadcasting tl.arange(0, ZBLOCK) is faster. + if ( + self.is_native_matmul + and entry.tensor_dim == 0 + and self.triton_tensor_ndim() == 4 + ): + size = "" + index_dtype = self.index_dtype + suffix = f".to({index_dtype})" if index_dtype != "tl.int32" else "" + if ( + self.cooperative_reduction + and self.persistent_reduction + and entry.is_reduction + ): + suffix = f"{suffix} + rsplit_start" + return f"tl.arange(0, {entry.prefix.upper()}BLOCK){size}{suffix}" + + def iteration_ranges_scalar_code( + self, entry: IterationRangesRoot, value: Any + ) -> str: + index_dtype = self.index_dtype + ndim = self.triton_tensor_ndim() + size = [1] * ndim + return f"tl.full({size}, {value}, {index_dtype})" + + def iteration_ranges_get_pid(self, entry: IterationRangesRoot) -> str: + assert entry.grid_dim is not None + key = f"tl.program_id({entry.grid_dim})" + # y_grid has a limit, so express it in terms of y and z in case of overflow. + # z grid is only exercised when max_tiles == 3 (off by default). + if self.needs_yz_grid_overflow(entry): + # For ynumel larger than max_ygrid, we need to use zdim. + # For each z dimension, there are tl.num_programs(1) yblocks which is passed by grad(x,y,z). + # So, we need to add tl.program_id(z) * tl.num_programs(y) *YBLOCK to get the correct yoffset. + key = f"({key} + tl.program_id({entry.grid_dim + 1}) * tl.num_programs({entry.grid_dim}))" + + # For batched matmul, we intentionally remap program_id axes so that the + # batch dimension is placed on CUDA gridDim.x (the fastest-varying launch axis). + # - gridDim.x scales to much larger sizes than gridDim.z (limited to 65536 in CUDA) + if self.is_native_matmul and self.triton_tensor_ndim() == 4: + reversed_pid_map = {0: 2, 1: 1, 2: 0} + key = f"tl.program_id({reversed_pid_map[entry.grid_dim]})" + + pid = entry.pid_cache.get(key, key) + if self.index_dtype != "tl.int32": + return f"{pid}.to({self.index_dtype})" + return pid + + def needs_yz_grid_overflow(self, entry: IterationRangesRoot) -> bool: + # Combo kernels use flattened dispatch where y_pid_offset is computed + # from the flattened pid, so YZ overflow is not needed + if self.is_combo_kernel and config.combo_kernel_per_subkernel_blocks: + return False + return ( + entry.grid_dim == 1 + and not entry.has_zdim + and not self.cooperative_reduction + and not V.graph.sizevars.statically_known_leq(entry.numel, get_max_y_grid()) + ) + + def max_block(self, prefix: str) -> int: + if self.fixed_config: + return self.fixed_config[f"{prefix.upper()}BLOCK"] + return TRITON_MAX_BLOCK[prefix.upper()] + + def _has_constant_mask(self, tree: IterationRangesRoot) -> bool: + if self.is_native_matmul: + # tl.dot requires the shape to be >= 16, + # so when matmul shape is smaller than 16, we always keep the mask. + if V.graph.sizevars.statically_known_lt(tree.numel, 16): + return False + + if not self.optimize_mask: + return False + + if self.fixed_config and f"{tree.prefix.upper()}BLOCK" in self.fixed_config: + if self.fixed_config[f"{tree.prefix.upper()}BLOCK"] == 1: + return True + elif not self.is_combo_kernel: + if V.graph.sizevars.statically_known_equals(tree.numel, 1): + if not (tree.is_reduction and self.persistent_reduction): + return True + + # Masks are superfluous if numel is a multiple of BLOCK + # (We use the fact that BLOCK is required by triton to be a power of 2) + if tree.is_reduction and self.persistent_reduction: + max_block = self._get_persistent_RBLOCK(tree.numel) + # Triton's auto-tuner can map a full hardware warp along the + # reduction axis. When RBLOCK < warp_size the excess lanes + # would execute out-of-bounds global loads. This results in + # faults on AMD hardware. Keep the dynamic mask so that all + # hardware stays correct. + device = V.graph.get_current_device_or_throw() + warp_size = DeviceProperties.create(device).warp_size or 32 + if isinstance(max_block, int) and max_block < warp_size: + return False + elif tree.prefix == "x" and self.no_x_dim: + max_block = 1 + else: + max_block = self.max_block(tree.prefix) + + if tree.is_reduction and self.cooperative_reduction: + max_block = max_block * self.max_rsplit() + + # [Note: Constant mask optimisation] + # Optional optimization: if block divides numel exactly, we will + # never need to do a masked load to handle stragglers at the end. + # If this tree is for the y dimension, we should only use a constant + # mask if it can be guaranteed that: + # 1. (ynumel / YBLOCK) < max_ygrid or + # 2. (ynumel / YBLOCK) % max_ygrid == 0 + # Because YBLOCK is not constant, use a conservative heuristic: + # only use a constant mask if ynumel < max_ygrid. + # It's faster to avoid masking at all. But it is sound to always + # mask. + if V.graph.sizevars.statically_known_multiple_of(tree.numel, max_block): + return ( + tree.grid_dim != 1 + or tree.has_zdim + or V.graph.sizevars.statically_known_leq(tree.numel, get_max_y_grid()) + ) + + return False + + def _has_constant_xmask(self) -> bool: + xtree = self.range_trees[0] + assert xtree.prefix == "x" + return self._has_constant_mask(xtree) + + def filter_masks(self, mask_vars: OrderedSet[str]) -> None: + for tree in self.range_trees: + if self._has_constant_mask(tree): + mask_vars.discard(f"{tree.prefix}mask") + + # can be added as an override_mask + mask_vars.discard("None") + + @cache_on_self + def get_reduction_prefixes(self) -> list[str]: + return [ + prefix_str[symt] + for symt in list(TritonSymbols.reduction_types)[: self.num_reduction_dims] + ] + + def codegen_reduction_numels(self, buffer: IndentedBuffer) -> None: + """ + Generates code that flattens ND reduction numels, block sizes, etc. into 1D. + """ + # rnumel = r0_numel * ... * r(n-1)_numel + reduction_trees = [tree for tree in self.range_trees if tree.is_reduction] + rnumel = " * ".join(sorted(f"{tree.prefix}numel" for tree in reduction_trees)) + buffer.splice(f"rnumel = {self.kexpr(rnumel)}") + + # RBLOCK = R0_BLOCK * ... * R(N-1)_BLOCK + rn_blocks = [ + TritonSymbols.block_sizes[tree.symt] + for tree in self.range_trees + if tree.is_reduction + ] + rblock = sympy_product(rn_blocks) + buffer.splice(f"RBLOCK: tl.constexpr = {self.kexpr(rblock)}") + + def _get_reduction_symbols(self, suffix: str, **kwargs) -> list[sympy.Symbol]: + """ + Helper to initialize symbols like rn_numel, rn_base, etc. + """ + rn_prefixes = self.get_reduction_prefixes() + return [sympy.Symbol(f"{prefix}{suffix}", **kwargs) for prefix in rn_prefixes] + + @cache_on_self + def _get_reduction_index_coeffs(self) -> list[sympy.Expr]: + """ + Compute coefficients to convert ND reduction indices to linear indices. + For example: + rindex = r0_index * r1_numel * ... * rn_numel + ... + rn_index. + """ + rn_prefixes = self.get_reduction_prefixes() + rn_numels = self._get_reduction_symbols("numel", integer=True, positive=True) + return [ + sympy_product(rn_numels[idx + 1 :]) for idx in range(len(rn_prefixes) - 1) + ] + [sympy.Integer(1)] + + def _flatten_reduction_indices(self, multi_inds: list[sympy.Expr]) -> sympy.Expr: + """ + Compute linear reduction indices from N dimensional ones. + """ + coeffs = self._get_reduction_index_coeffs() + return sympy_dot(coeffs, multi_inds) + + def codegen_reduction_indices(self, buffer: IndentedBuffer) -> None: + """ + Generates code that converts ND reduction indices into linear indices. + """ + # Gather relevant numels, indices, etc. + rn_offsets = self._get_reduction_symbols( + "offset", integer=True, nonnegative=True + ) + rn_inds = self._get_reduction_symbols("index", integer=True, nonnegative=True) + + # Compute roffset and rindex. + roffset = self._flatten_reduction_indices(rn_offsets) + buffer.splice(f"roffset = {self.index_to_str(roffset)}") + rindex = self._flatten_reduction_indices(rn_inds) + buffer.splice(f"rindex = {self.index_to_str(rindex)}") + + def iteration_ranges_codegen_header( + self, entry: IterationRangesRoot, code: IndentedBuffer + ) -> None: + x = entry.prefix + if entry.is_loop: + code.writeline(f"{entry.name} = {x}offset + {x}base") + elif entry.grid_dim is None: + # no need to "{x}offset = " + code.writeline(f"{entry.name} = {self.iteration_ranges_ranges_code(entry)}") + code.writeline(f"{x}offset = 0") + else: + if entry.tensor_dim is not None: + line = f"{x}offset + {self.iteration_ranges_ranges_code(entry)}" + else: + line = self.iteration_ranges_scalar_code(entry, f"{x}offset") + + block_size = ( + f"{x.upper()}BLOCK" if not self.mix_order_reduction else "RSPLIT_SIZE" + ) + code.writelines( + [ + f"{x}offset = {self.iteration_ranges_get_pid(entry)} * {block_size}", + f"{entry.name} = {line}", + ] + ) + if self._has_constant_mask(entry): + code.writeline(self.create_constant_mask(entry)) + elif not (x == "x" and self.mix_order_reduction): + # mix order reduction should generate xmask inside the loop + code.writeline(f"{x}mask = {entry.name} < {x}numel") + + +class FusedUserDefinedTritonKernel(TritonKernel): + """ + When fusing a user-defined triton kernel with epilogues, we use this class to generate the modified triton kernel source + """ + + def __init__( + self, + tiling: dict[str, sympy.Expr], + features: SIMDKernelFeatures, + scheduler_node: FusedExternTritonKernelSchedulerNode, + ) -> None: + super().__init__( + tiling, + features=features, + min_elem_per_thread=0, + optimize_mask=True, + fixed_config=None, + hint_override=None, + is_combo_kernel=False, + ) + self.scheduler_node = scheduler_node + assert isinstance( + self.scheduler_node.kernel_node.node, ir.UserDefinedTritonKernel + ) + self.ir_node: ir.UserDefinedTritonKernel = self.scheduler_node.kernel_node.node + assert self.ir_node.can_fuse_epilogue() + + # must be true because `self.ir_node.can_fuse_epilogue()` + assert len(self.ir_node.kernel_stores.stores) == 1 + self.original_stored_expr = ast.unparse( + self.ir_node.kernel_stores.stores[0].store_value_node + ) + self.original_stored_expr = self.original_stored_expr.replace("\n", "") + + def load(self, name: str, index: sympy.Expr) -> TritonCSEVariable: + # must be true because `self.ir_node.can_fuse_epilogue()` + assert len(self.ir_node.mutable_args) == 1 + if name == self.ir_node.mutable_args[0].get_name(): + # when the fused epilogue nodes tries to load the mutated buffer + # we replace the load with the expression stored by the user kernel + loaded_expr = self.original_stored_expr + indexing = self.indexing(index) + dtype = V.graph.get_dtype(name) + if indexing.expand_shape: + shape = indexing.expand_shape + else: + shape = TritonSymbols.get_block_shape(indexing.index) + result_var = self.cse.generate( + self.loads, loaded_expr, dtype=dtype, shape=shape + ) + return result_var + else: + # The scheduler should prevent this. + raise AssertionError( + f"Epilogue attempted to load from '{name}'. " + "Inductor indexing variables are not defined in user kernel scope. " + ) + + def store( + self, name: str, index: sympy.Expr, value: CSEVariable, mode: StoreMode = None + ) -> None: + assert isinstance(self.scheduler_node.fused_epilogue.node, ir.ComputedBuffer) + if name == self.scheduler_node.fused_epilogue.node.get_name(): + # when the fused epilogue nodes tries to store to its destination buffer + # we remember this expr and then later replace it into the `tl.store` call of the original kernel + self.new_store_cse_var = value + else: + super().store(name, index, value, mode) + + # returns a str which is the src code of a modified version of the user kernel that includes the epilogues + def codegen(self) -> str: + with self: + index_vars = self.split_and_set_ranges( + self.scheduler_node.fused_epilogue.get_ranges() + ) + self.scheduler_node.fused_epilogue.codegen(index_vars) + + # Generate a new AST where the store value expr is replaced with the new value + new_ast = copy.deepcopy(self.ir_node.kernel_ast) + from torch._higher_order_ops.triton_kernel_wrap import ( + identify_triton_stores, + identify_triton_stores_from_ast, + ) + + # avoid redundant cache entry of new_ast + kernel_stores = identify_triton_stores_from_ast(new_ast) + assert len(kernel_stores.stores) == 1 + + new_store_value_node = ast.Name(self.new_store_cse_var.name) + + def _replace_arg( + call_node: ast.Call, + arg_name: str, + positional_index: int, + new_arg, + ): + if len(call_node.args) > positional_index: + call_node.args[positional_index] = new_arg + + # Check keyword args + for keyword in call_node.keywords: + if keyword.arg == arg_name: + keyword.value = new_arg + + _replace_arg( + kernel_stores.stores[0].store_node, "value", 1, new_store_value_node + ) + + src_with_store_replaced = ast.unparse(new_ast) + + # then, we need to inject the additional `load` and `compute` lines generated when we `codegen` the epilogue nodes + + src_lines = src_with_store_replaced.splitlines() + + # identify the store again, because the previous parse-modify-unparse could've change its location + kernel_stores = identify_triton_stores(src_with_store_replaced) + # python ast lineno is 1-indexed + store_line_index = kernel_stores.stores[0].store_node.lineno - 1 + + indentations = " " * kernel_stores.stores[0].store_node.col_offset + + load_lines = [indentations + l for l in self.loads.get_lines_ref()] + compute_lines = [indentations + l for l in self.compute.get_lines_ref()] + + new_src_lines = ( + src_lines[:store_line_index] + + load_lines + + compute_lines + + src_lines[store_line_index:] + ) + return "\n".join(new_src_lines) + + +class TritonScheduling(SIMDScheduling): + """Scheduling backend for Triton kernel code generation.""" + + kernel_type: type[Any] = TritonKernel + backend_features = OrderedSet( + [ + BackendFeature.FOREACH, + BackendFeature.BUCKETIZE, + BackendFeature.INPLACE_BUFFERS, + BackendFeature.MASKED_SCATTER_WITH_INDEX, + BackendFeature.SCAN, + BackendFeature.SORT, + BackendFeature.TRITON_TEMPLATES, + BackendFeature.TUPLE_REDUCTION, + ] + ) + + def __init__(self, scheduler: Scheduler | None) -> None: + super().__init__(scheduler) + if scheduler is None or not hasattr(scheduler, "nodes"): + return + for node in scheduler.nodes: + if isinstance(node, (SchedulerNode, FusedSchedulerNode)): + node.debug_device_str = debug_triton_code + + @classmethod + def get_backend_features(cls, device: torch.device): + if ( + config.triton.cooperative_reductions + or config.triton.force_cooperative_reductions + ): + return OrderedSet( + [*cls.backend_features, BackendFeature.REDUCE_TO_SINGLE_ELEMENT] + ) + return cls.backend_features + + def codegen_comment(self, node_schedule, kernel_name=None): + wrapper = V.graph.wrapper_code + origins, _detailed_origins = get_kernel_metadata(node_schedule, wrapper) + if origins: + wrapper.make_comment(origins) + + if config.debug_fusion: + from torch._inductor.scheduler import ( + BaseSchedulerNode, + ForeachKernelSchedulerNode, + ) + + if not any( + isinstance(n, ForeachKernelSchedulerNode) for n in node_schedule + ): + # We probably should look what are the nodes inside a foreach + # schedule node + node_names = [ + n.get_name() + for n in node_schedule + if isinstance(n, BaseSchedulerNode) + ] + wrapper.make_comment( + f"{wrapper.comment} Fused node name list: {', '.join(node_names)}" + ) + + if kernel_name: + debug_handle = set_kernel_post_grad_provenance_tracing( + node_schedule, # type: ignore[arg-type] + kernel_name, + ) + wrapper.write_provenance_debug_handle(kernel_name, debug_handle) + + def _emit_kernel_to_wrapper( + self, + wrapper, + kernel, + src_code, + kernel_name, + subs_name, + node_schedule, + kernel_path, + get_kernel_metadata, + ): + """Emit kernel to wrapper, with support for external template handlers.""" + # External template handlers (e.g. Helion) can override kernel emission + if kernel.emit_kernel_override( + wrapper, + src_code, + kernel_name, + node_schedule, + kernel_path, + get_kernel_metadata, + ): + return # External handler handled it + + compile_wrapper = IndentedBuffer() + + if async_compile.use_process_pool(): + # The process pool is warm, we can shell out to workers right away. This + # allows us to save the result in async_compile.CompiledTritonKernels, + # so that the second time we call async_compile.triton, we do no work. + async_compile.triton(subs_name, src_code) + + compile_wrapper.writeline(f"async_compile.triton({subs_name!r}, '''") + + compile_wrapper.splice(src_code, strip=True) + current_device = V.graph.get_current_device_or_throw() + compile_wrapper.writeline(f"''', device_str='{current_device.type}')") + + metadata_comment = f"# kernel path: {kernel_path}" + origins, detailed_origins = get_kernel_metadata(node_schedule, wrapper) + metadata_comment += "\n" + origins + "\n" + detailed_origins + wrapper.define_kernel(kernel_name, compile_wrapper.getvalue(), metadata_comment) + + def define_kernel(self, src_code, node_schedule, kernel): + wrapper = V.graph.wrapper_code + if src_code in wrapper.src_to_kernel: + kernel_name = wrapper.src_to_kernel[src_code] + else: + fused_name = ( + get_fused_kernel_name(node_schedule, config.triton.descriptive_names) + if config.triton.descriptive_names + else "" + ) + if fused_name: + fused_name = V.choices.customize_fused_kernel_name(fused_name, src_code) + kernel_category = get_kernel_category_by_source_code(src_code)[:3] + kernel_name = "_".join( + ["triton", kernel_category, fused_name, wrapper.next_kernel_suffix()] + ) + if config.aot_inductor.model_name_for_generated_files: + # When AOTI compiles multiple submodules, we need to use the model name to + # distinguish kernel related symbols. + kernel_name = f"{config.aot_inductor.model_name_for_generated_files}_{kernel_name}" + + # use the original src_code as the key + wrapper.src_to_kernel[src_code] = kernel_name + subs_name = kernel_name if config.triton.unique_kernel_names else "triton_" + + # DESCRIPTIVE_NAME is used for profiling purposes; it shows the full kernel name + # even when unique_kernel_names is turned off. Meanwhile, KERNEL_NAME is sometimes set + # to "triton_" to maximize caching opportunities (when unique_kernel_names = False). + src_code = src_code.replace(str(Placeholder.DESCRIPTIVE_NAME), kernel_name) + src_code = src_code.replace(str(Placeholder.KERNEL_NAME), subs_name) + + # TODO(voz): Ostensibly, we should not need this. But there are cases where C++ codegen does + # not use BracesBuffer, so we have no good indicator of a C++ buffer atm. + src_code = src_code.replace("#pragma CMT", "#") + + _basename, _, kernel_path = get_path(code_hash(src_code.strip()), "py") + + self._emit_kernel_to_wrapper( + wrapper, + kernel, + src_code, + kernel_name, + subs_name, + node_schedule, + kernel_path, + get_kernel_metadata, + ) + + # log kernel metadata for offline analysis. + # E.g. one can find all unaligned inner reduction and check if + # padding helps with the perf kernel by kernel. + if metrics.is_metric_table_enabled("kernel_metadata"): + metrics.log_kernel_metadata(kernel_name, kernel_path, src_code) + + return kernel_name + + def benchmark_fused_nodes(self, nodes, n_spills_threshold=8) -> tuple[float, str]: + """ + Benchmark fused list of nodes and return the execution time + in milliseconds on randomly generated inputs. + """ + src_code = self.generate_kernel_code_from_nodes(nodes, benchmark_kernel=True) + mod = PyCodeCache.load(src_code) + return self.benchmark_codegened_module( + mod, n_spills_threshold, node_names=OrderedSet(n.get_name() for n in nodes) + ) + + def benchmark_codegened_module( + self, mod, n_spills_threshold=8, node_names: OrderedSet[str] | None = None + ) -> tuple[float, str]: + """Benchmark an already compiled module""" + device_interface = get_interface_for_device(V.graph.device_type) + with ( + preserve_rng_state(), + device_interface.device(V.graph.get_current_device_or_throw()), # type: ignore[attr-defined] + ): + ms = None + + def cache_file_path(): + assert mod.__file__ is not None + return os.path.splitext(mod.__file__)[0] + ".kernel_perf" + + def store_cache(): + path = cache_file_path() + write_atomic(path, str(ms)) + + def load_cache(): + path = cache_file_path() + if os.path.exists(path): + with open(path) as fd: + return float(fd.read()) + return None + + node_names = ( + node_names if node_names is not None else OrderedSet(["unknown"]) + ) + log.debug( + "kernel src code for %s written to: %s", + node_names, + mod.__file__, + ) + ms = load_cache() + if ms is not None: + return ms, mod.__file__ + + args = mod.get_args() + call = mod.call + wrapped_jit_function = mod.triton_ + # call once to trigger the compilation + try: + call(wrapped_jit_function.clone_args(*args)[0]) + except Exception as e: + if config.triton.disallow_failing_autotune_kernels_TESTING_ONLY: + raise + log.debug( # noqa: G200 + "Exception (%s) in compiling fused nodes %s", + e, + node_names, + ) + ms = float("inf") + store_cache() + return ms, mod.__file__ + + launchers = wrapped_jit_function.launchers + assert len(launchers) == 1 + # n_spills does not necessarily mean it's not profitable to fuse, + # and sometimes it can be inaccurate + if launchers[0].n_spills > n_spills_threshold: + # skip benchmarking the kernel if there are register spills + ms = float("inf") + else: + device = V.graph.get_current_device_or_throw() + # We have to clone the inplace updated arguments to avoid earlier calls + # generating out of range indices for later calls. + ms = benchmarker.benchmark( + lambda: call(wrapped_jit_function.clone_args(*args)[0]), + device=device, + ) + # overhead of cloning args gives bias for fusing the kernel + # in the case of mutating/in-placeable second fusion + # TODO - would be better as a hook in triton do_bench that reset + # the input values between benchmarking + if len(wrapped_jit_function.mutated_arg_names) > 0: + ms = ms - benchmarker.benchmark( + lambda: wrapped_jit_function.clone_args(*args), + device=str(device), + ) + + log.debug( + "The fused kernel for %s took %.3f ms to run", + node_names, + ms, + ) + store_cache() + return ms, mod.__file__ + + def create_kernel_choices( # type: ignore[override] + self, + kernel_features: SIMDKernelFeatures, + kernel_args: list[Any], + kernel_kwargs: dict[str, Any], + ) -> list[TritonKernel]: + is_scan = kernel_features.contains_op("scan") + is_split_scan = is_scan and any( + node.is_split_scan() for node in kernel_features.scheduler_nodes() + ) + kernel_type: type[TritonKernel] = self.kernel_type + if is_split_scan: + from .triton_split_scan import TritonSplitScanKernel + + kernel_type = TritonSplitScanKernel + + if is_scan: + # TODO(jansel): scan does not yet work with cooperative reductions + kernel_kwargs["override_cooperative_reduction"] = False + + # ops.sort only works with persistent reduction, and is not bandwidth bound anyway + # so taking the hit of non-coalesced loads is okay + if kernel_features.contains_op("sort"): + kernel_kwargs["override_persistent_reduction"] = True + kernel_kwargs["override_cooperative_reduction"] = False + + if not TritonKernel.has_persistent_RBLOCK(kernel_features.reduction_numel): + # Cannot use persistent reduction with unknown dynamic rnumel + assert not kernel_kwargs.get("override_persistent_reduction") + kernel_kwargs["override_persistent_reduction"] = False + + kernel_kwargs = V.choices.triton_kernel_kwargs( + kernel_type, kernel_features, kernel_args, kernel_kwargs + ) + kernel = kernel_type(*kernel_args, **kernel_kwargs) + return self.add_multi_kernel_choices(kernel, kernel_args, kernel_kwargs) + + def add_multi_kernel_choices( + self, + kernel: TritonKernel, + kernel_args: list[Any], + kernel_kwargs: dict[str, Any], + ) -> list[TritonKernel]: + kernels: list[TritonKernel] = [kernel] + if not config.triton.multi_kernel: + return kernels + + optional_persistent = kernel.persistent_reduction and not kernel_kwargs.get( + "override_persistent_reduction" + ) + optional_cooperative = kernel.cooperative_reduction and not kernel_kwargs.get( + "override_cooperative_reduction" + ) + if optional_persistent: + kernels.append( + self.kernel_type( + *kernel_args, + **kernel_kwargs, + override_persistent_reduction=False, + ) + ) + if optional_cooperative: + rnumel = kernel.features.reduction_numel + # for larger sizes non-cooperative gets very slow + if V.graph.sizevars.statically_known_leq(rnumel, 65536): + kernels.append( + other := self.kernel_type( + *kernel_args, + **kernel_kwargs, + override_cooperative_reduction=False, + ) + ) + if optional_persistent and other.persistent_reduction: + kernels.append( + self.kernel_type( + *kernel_args, + **kernel_kwargs, + override_cooperative_reduction=False, + override_persistent_reduction=False, + ) + ) + + if len(kernels) > 1: + for kernel2 in kernels[1:]: + # Keep buffers needed by the non-persistent reduction so both kernels have the same arguments + kernel2.must_keep_buffers = kernel.must_keep_buffers + # persistent kernels must be generated last so must_keep_buffers works right + kernels.sort(key=lambda k: k.persistent_reduction) + return kernels + + def benchmark_combo_kernel(self, node_list, node_benchmark_results): + """ + Benchmark combo kernel partitions and return total execution time. + + Generates kernel code for each partition and benchmarks them. + Single-node partitions use benchmark_fused_nodes(), while multi-node + partitions use the combo kernel benchmarking path. + + Returns (total_ms, total_clone_ms, file_list). + """ + mod: ModuleType + ms: float + ms_clone: float + + def cache_file_path(): + assert mod.__file__ is not None + return os.path.splitext(mod.__file__)[0] + ".kernel_perf" + + def load_cache(): + path = cache_file_path() + if os.path.exists(path): + with open(path) as fd: + return tuple(float(e) for e in fd.read().split()) + return (None, None) + + def store_cache(): + path = cache_file_path() + write_atomic(path, str(ms) + " " + str(ms_clone)) + + total_ms, file_list = 0, [] + total_clone_ms: float = 0.0 + removed_buffers_orig = V.graph.removed_buffers + V.graph.removed_buffers = OrderedSet(removed_buffers_orig) + inplaced_to_remove_orig = V.graph.inplaced_to_remove + V.graph.inplaced_to_remove = OrderedSet(inplaced_to_remove_orig) + enable_autotune = config.combo_kernels_autotune > 0 + mixed_sizes = config.combo_kernel_allow_mixed_sizes > 0 + kernel_code_list = self.generate_combo_kernel_code( + subkernel_nodes=node_list, + custom_part_algorithm=True, + enable_autotune=enable_autotune, + mixed_sizes=mixed_sizes, + only_gen_src_code=True, + ) + + # pyrefly: ignore [bad-assignment] + for src_code, kernel, node_group in kernel_code_list: + fused_node_lists = [node.get_nodes() for node in node_group] + names = [n.get_name() for nodes in fused_node_lists for n in nodes] + + if len(node_group) == 1: + # Single-node partition: use cached benchmark results from speedup_by_combo_kernel + node_ms, path = node_benchmark_results[node_group[0]] + # Regular kernels have negligible clone overhead + total_ms += node_ms + total_clone_ms += 0 + file_list.append(path) + continue + + assert src_code is not None + src_code = src_code.replace(str(Placeholder.KERNEL_NAME), "triton_") + mod = PyCodeCache.load(src_code) + + log.debug( + "kernel src code for %s written to: %s", + names, + mod.__file__, + ) + ms, ms_clone = load_cache() + if ms is not None: + total_ms += ms # type: ignore[assignment] + total_clone_ms += ms_clone + file_list.append(mod.__file__) + continue + + args = mod.get_args() + call = mod.call + wrapped_jit_function = mod.triton_ + + # call once to trigger the compilation + call(wrapped_jit_function.clone_args(*args)[0]) + + launchers = wrapped_jit_function.launchers + assert len(launchers) == 1 + if launchers[0].n_spills > 0: + # skip benchmarking the kernel if there are register spills + ms = ms_clone = float("inf") + else: + device = V.graph.get_current_device_or_throw() + # We have to clone the inplace updated arguments to avoid earlier calls + # generating out of range indices for later calls. + ms = benchmarker.benchmark( + lambda: call(wrapped_jit_function.clone_args(*args)[0]), + device=device, + ) + ms_clone = benchmarker.benchmark( + lambda: wrapped_jit_function.clone_args(*args)[0], + device=device, + ) + + log.debug( + "The fused kernel for %s took %.3f ms to run, %.3f ms to clone inputs", + OrderedSet(n.get_name() for n in node_group), + ms, + ms_clone, + ) + store_cache() + total_ms += ms + total_clone_ms += ms_clone + file_list.append(mod.__file__) + V.graph.removed_buffers = removed_buffers_orig + V.graph.inplaced_to_remove = inplaced_to_remove_orig + return total_ms, total_clone_ms, file_list + + +def debug_triton_code(node: BaseSchedulerNode) -> list[str]: + lines = [] + multi_template = node.get_template_node() + assert multi_template is None or isinstance(multi_template, ir.MultiTemplateBuffer) + if multi_template and multi_template.make_kernel_render is None: + lines.append(f"{node.get_name()} Unfinalized multi template buffer") + else: + from torch._inductor.codegen.cuda_combined_scheduling import ( + CUDACombinedScheduling, + ) + from torch._inductor.codegen.xpu.xpu_combined_scheduling import ( + XPUCombinedScheduling, + ) + + device = node.get_device() + assert device is not None + backend = node.scheduler.get_backend(device) + assert isinstance( + backend, (SIMDScheduling, CUDACombinedScheduling, XPUCombinedScheduling) + ), ( + f"Scheduling backend should be SIMD or CUDACombined when generating debug Triton strings, got: {type(backend)}" + ) + + with V.graph.set_current_device(device): + # Don't increment kernel count when generating debug string. + # This will confuse some unit tests that check the number of + # generated kernels. + old_generated_kernel_count = metrics.generated_kernel_count + triton_code = backend.generate_kernel_code_from_nodes( + node.get_nodes() + ).strip() + metrics.generated_kernel_count = old_generated_kernel_count + + lines.append(f"{node.get_name()} Triton code:") + lines.append(textwrap.indent(triton_code, " ")) + return lines diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/triton_combo_kernel.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/triton_combo_kernel.py new file mode 100644 index 0000000000000000000000000000000000000000..1c21a1594b01785c6cb5f312ca86a2d9f59826ce --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/triton_combo_kernel.py @@ -0,0 +1,1223 @@ +import itertools +import logging +import textwrap +from collections import defaultdict +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any, cast + +import sympy +from sympy import Integer, Symbol + +from torch.utils._ordered_set import OrderedSet + +from .. import config, metrics +from ..runtime.hints import DeviceProperties +from ..runtime.runtime_utils import next_power_of_2 +from ..runtime.triton_heuristics import ( + RoundRobinComboKernelGrid, + SequentialComboKernelGrid, + SequentialFlattenComboKernelGrid, +) +from ..scheduler import BaseSchedulerNode +from ..stream_utils import get_raw_stream_name +from ..utils import Placeholder, triton_version_uses_attrs_dict +from ..virtualized import V +from .common import ( + ArgName, + ConstexprArg, + DeferredLine, + IndentedBuffer, + InplacedBuffer, + Kernel, + PythonPrinter, + RemovedArg, + SizeArg, + WorkspaceArg, +) +from .simd import NodeInfo, prefix_is_reduction, SIMDScheduling +from .simd_kernel_features import SIMDKernelFeatures +from .triton import TritonKernel +from .triton_utils import config_of, equal_1_arg_indices, signature_to_meta + + +# Default block sizes used when combo kernel autotuning is disabled. +DEFAULT_COMBO_BLOCK_SIZE_1D = 1024 +DEFAULT_COMBO_BLOCK_SIZE_2D = 32 + + +log = logging.getLogger(__name__) +pexpr = PythonPrinter().doprint +LARGE_NUMELS = 512e5 +BLOCK_UTILIZATION = 0.8 + + +def _default_custom_combo_kernel_horizontal_partition( + nodes: list[BaseSchedulerNode], + triton_scheduling: SIMDScheduling, + node_info_map: dict[BaseSchedulerNode, NodeInfo], +) -> list[list[BaseSchedulerNode]]: + """Horizontally partition the given list of nodes into a list of list of nodes where each sublist + represents a partition. Nodes in different partitions are implemented in different combo kernels. + Nodes in the same partition are likely to be implemented + in the same combo kernel, but subject to subsequent restrictions like CUDA limits for number of args. + + Input arguments: + nodes: a list of fused scheduler nodes to partition. + triton_scheduling: TritonScheduling instance. + node_info_map: a map from node to NodeInfo NamedTuple + Output: + a list of list of nodes with each sublist representing a partition. + + The default algorithm is to partition nodes based on the following rules: + 1) nodes with the same number of block dimensions are grouped together. + 2) large pointwise nodes (numels greater than LARGE_NUMELS) are separated from other nodes. + 3) large reduce nodes are separated from other nodes. + """ + + assert len(nodes) >= 1 + + # first partition nodes based on number of block dimensions + tilings = [node_info_map[n].tiling for n in nodes] + + max_dims = max(len(t) for t in tilings) + nodes_per_ndim: list[list[BaseSchedulerNode]] = [] + for i in range(2, max_dims + 1): + group_per_dim = [n for n, t in zip(nodes, tilings) if len(t) == i] + reduction = [ + n for n in group_per_dim if node_info_map[n].features.is_reduction() + ] + not_reduction = [n for n in group_per_dim if n not in reduction] + # rnumel > 2048 usually has long execution time + # BaseSchedulerNode.group[-1][-1] is rnumel for reduction nodes + # Scheduling heuristic: separate long reductions (rnumel > 2048). + # Uses optimization_hint with fallback=1 so unbacked defaults to short reduction. + long_reduction = [ + n + for n in reduction + if V.graph.sizevars.optimization_hint(n.group[-1][-1], fallback=1) > 2048 # type: ignore[arg-type] + ] + short_reduction = [n for n in reduction if n not in long_reduction] + if long_reduction: + log.debug( + "ComboKernels: %d long reduction nodes are separated", + len(long_reduction), + ) + large_pointwise = [ + n + for n in not_reduction + if not node_info_map[n].features.is_reduction() + and len(node_info_map[n].tiling) == 2 + and V.graph.sizevars.optimization_hint( + node_info_map[n].tiling["x"], fallback=1 + ) + > LARGE_NUMELS # type: ignore[arg-type] + ] + if large_pointwise: + # TODO benchmark the performance when large pointwise nodes combining with others + log.debug( + "ComboKernels: %d large pointwise nodes are separated", + len(large_pointwise), + ) + not_reduction = [n for n in not_reduction if n not in large_pointwise] + nodes_per_ndim.extend([node] for node in large_pointwise) + + nodes_per_ndim.extend( + g for g in (not_reduction, short_reduction, long_reduction) if g + ) + + assert sum(len(p) for p in nodes_per_ndim) == len(nodes) + return nodes_per_ndim + + +_custom_combo_kernel_horizontal_partition_algorithm: Callable[ + [ + list[BaseSchedulerNode], + SIMDScheduling, + dict[BaseSchedulerNode, NodeInfo], + ], + list[list[BaseSchedulerNode]], +] = _default_custom_combo_kernel_horizontal_partition + + +def set_custom_combo_kernel_horizontal_partition( + algorithm: Callable[ + [ + list[BaseSchedulerNode], + SIMDScheduling, + dict[BaseSchedulerNode, NodeInfo], + ], + list[list[BaseSchedulerNode]], + ], +) -> None: + """Sets the algorithm used to partition nodes into horizontal partitions. Nodes in different partitions + are implemented in different combo kernels. Nodes in the same partition are likely to be implemented + in the same combo kernel, but subject to subsequent restricts like CUDA limits for number of args. + + The algorithm should take a list of nodes and return a list of list of nodes. + + The default algorithm is to partition nodes based on number of block dimensions. + """ + global _custom_combo_kernel_horizontal_partition_algorithm + _custom_combo_kernel_horizontal_partition_algorithm = algorithm + + +@dataclass +class PartitionState: + partitions: list[list[BaseSchedulerNode]] + cur_partition: list[BaseSchedulerNode] + cur_count: int + + def finalize(self) -> None: + if self.cur_partition: + self.partitions.append(self.cur_partition) + + +class ComboKernel(Kernel): + """ + A kernel that combines multiple sub-kernels into a single fused kernel. + """ + + @staticmethod + def _update_partition( + partition_state: PartitionState, + node_rw_count: int, + node_info: BaseSchedulerNode, + ) -> None: + if partition_state.cur_count + node_rw_count > config.combo_kernel_max_num_args: + partition_state.partitions.append(partition_state.cur_partition) + partition_state.cur_partition = [node_info] + partition_state.cur_count = node_rw_count + else: + partition_state.cur_count += node_rw_count + partition_state.cur_partition.append(node_info) + + @staticmethod + def _base_horizontal_partition( + subkernel_nodes: list[BaseSchedulerNode], + triton_scheduling: SIMDScheduling, + node_info_map: dict[BaseSchedulerNode, NodeInfo], + custom_algorithm: bool, + ) -> list[list[BaseSchedulerNode]]: + """Generates a list of lists of node info tuples which consist of (fused_nodes, tiling, numel, rnumel) + for each subkernel node where each sublist is guaranteed to not exceed CUDA limits for number of args + (read/writes) and to have the same 2D or 1D blocking strategy.""" + # TODO support combination of kernels with different block dimensions + assert len(subkernel_nodes) >= 1 + mixed_sizes = config.combo_kernel_allow_mixed_sizes > 1 or ( + config.combo_kernel_allow_mixed_sizes == 1 and custom_algorithm + ) + + ndim_to_partition_state: dict[int, PartitionState] = defaultdict( + lambda: PartitionState([], [], 0) + ) + yelem_to_partition_state: dict[int, PartitionState] = defaultdict( + lambda: PartitionState([], [], 0) + ) + all_partitions = [] + + for node in subkernel_nodes: + tiled_groups = node_info_map[node].tiling + node_info = node + + read_writes = node.read_writes + read_write_count = len(read_writes.reads) + len(read_writes.writes) + + ndim = len(tiled_groups) + assert ndim >= 2, f"Combokernel not support tile {tiled_groups}" + + # Skip 2d reductions (r0_,r1_) and 3D pointwise (x,y,z) from combo + keys = tiled_groups.keys() + if ("r0_" in keys and "r1_" in keys) or "z" in keys: + all_partitions.append([node_info]) + continue + + if not mixed_sizes and ndim == 3: + y_elem = tiled_groups["y"] + partition_state = yelem_to_partition_state[y_elem] + ComboKernel._update_partition( + partition_state, read_write_count, node_info + ) + else: + assert mixed_sizes or ndim <= 3, f"No mixed sizes: tile {tiled_groups}" + partition_state = ndim_to_partition_state[ndim] + ComboKernel._update_partition( + partition_state, read_write_count, node_info + ) + + for partition_state in ndim_to_partition_state.values(): + partition_state.finalize() + all_partitions.extend(partition_state.partitions) + for partition_state in yelem_to_partition_state.values(): + partition_state.finalize() + all_partitions.extend(partition_state.partitions) + return all_partitions + + @staticmethod + def horizontal_partition( + nodes: list[BaseSchedulerNode], + triton_scheduling: SIMDScheduling, + node_info_map: dict[BaseSchedulerNode, NodeInfo], + custom_algorithm: bool = False, + ) -> list[list[BaseSchedulerNode]]: + """Generates a list of lists of node info tuples which consist of (fused_nodes, tiling, numel, rnum) + for each subkernel node where each sublist forms a ComboKernel. It horizontally partitions nodes into + sublists in the following way: + 1) call _custom_combo_kernel_horizontal_partition_algorithm() if custom_algorithm is True + 2) then, call _base_horizontal_partition() to partition nodes into sublists, each sublist is + guaranteed to not exceed CUDA limits for number of args (read/writes) and to have the same + 2D or 1D blocking strategy. + """ + if custom_algorithm: + raw_partitions = _custom_combo_kernel_horizontal_partition_algorithm( + nodes, triton_scheduling, node_info_map + ) + else: + raw_partitions = [nodes] + + """Generates a list of lists of node info tuples which consist of (fused_nodes, tiling, numel, rnumel) + for each subkernel node where each sublist is guaranteed to not exceed CUDA limits for number of args + (read/writes) and to have the same 2D or 1D blocking strategy.""" + all_partitions = [] + for raw_partition in raw_partitions: + all_partitions.extend( + ComboKernel._base_horizontal_partition( + raw_partition, triton_scheduling, node_info_map, custom_algorithm + ) + ) + return all_partitions + + class SequentialDispatch: + """ + The dispatcher which dispatches the subkernels in a sequential manner: + the blocks are first dispatched to the 1st subkernel (until it is filled), + then to the 2nd subkernel, and so on. + The class defines the methods specific to the dispatch algorithm. + Methods: + codegen_pid_range(...): codegen the pid range for each subkernel. + grid(...): codegen the grid size for launching the combo kernel. + """ + + grid_expr = SequentialComboKernelGrid + + @classmethod + def codegen_pid_range( + cls, kernel: "ComboKernel", num: int, code: IndentedBuffer + ) -> None: + if num == 0: + cls._calculate_xblocks(kernel, code) + code.splice(f"if pid < num_xblocks_{num}:") + with code.indent(): + code.splice("pid_offset = pid") + else: + code.splice(f"elif pid < num_xblocks_{num}:") + with code.indent(): + code.splice(f"pid_offset = pid - num_xblocks_{num - 1}") + + @classmethod + def _calculate_xblocks( + cls, kernel: "ComboKernel", code: IndentedBuffer + ) -> None: + x_numels_list = kernel.x_numels_list + for i in range(len(x_numels_list)): + xnumels, no_x_dim = ( + (x_numels_list[i], False) + if isinstance(x_numels_list[i], str) + and cast(str, x_numels_list[i])[0] != "-" + or ( + isinstance(x_numels_list[i], int) + and cast(int, x_numels_list[i]) > 0 + ) + else (kernel.min_x_blocks_list[i], True) + ) + xblock_str = ( + f"tl.cdiv({xnumels}, XBLOCK)" if not no_x_dim else f"{xnumels}" + ) + if i == 0: + code.splice(f"num_xblocks_{i} = {xblock_str}") + else: + code.splice(f"num_xblocks_{i} = num_xblocks_{i - 1} + {xblock_str}") + + class SequentialFlattenGridDispatch: + """ + Flattened grid dispatch for per-subkernel blocks. + Uses flattened grid (sum of x*y blocks, 1, 1) and computes + x_pid_offset, y_pid_offset from the flattened pid. + """ + + grid_expr = SequentialFlattenComboKernelGrid + + @classmethod + def codegen_pid_range( + cls, kernel: "ComboKernel", num: int, code: IndentedBuffer + ) -> None: + if num == 0: + cls._calculate_total_blocks(kernel, code) + code.splice(f"if pid < num_blocks_{num}:") + else: + code.splice(f"elif pid < num_blocks_{num}:") + + with code.indent(): + # Compute local pid within this subkernel's block range + if num == 0: + code.splice("local_pid = pid") + else: + code.splice(f"local_pid = pid - num_blocks_{num - 1}") + + # Compute x/y indices from flattened local_pid + if kernel.y_tree_list[num]: + code.splice(f"x_pid_offset = local_pid % x_blocks_{num}") + code.splice(f"y_pid_offset = local_pid // x_blocks_{num}") + else: + code.splice("x_pid_offset = local_pid") + + @classmethod + def _calculate_total_blocks( + cls, kernel: "ComboKernel", code: IndentedBuffer + ) -> None: + """ + Calculate total blocks for each subkernel (x_blocks * y_blocks) + and cumulative block counts for dispatch boundaries. + """ + for i, sub_kernel in enumerate(kernel.sub_kernels): + no_x_dim = sub_kernel.no_x_dim + xnumel = ( + kernel.min_x_blocks_list[i] if no_x_dim else kernel.x_numels_list[i] + ) + x_blocks_str = ( + f"tl.cdiv({xnumel}, XBLOCK_{i})" if not no_x_dim else f"{xnumel}" + ) + code.splice(f"x_blocks_{i} = {x_blocks_str}") + + if kernel.y_tree_list[i]: + numel = V.graph.sizevars.simplify(kernel.y_tree_list[i].numel) + ynumel = ( + int(numel) + if isinstance(numel, (Integer, int)) + else f"ynumel_{i}" + ) + code.splice(f"y_blocks_{i} = tl.cdiv({ynumel}, YBLOCK_{i})") + + blocks_expr = ( + f"x_blocks_{i} * y_blocks_{i}" + if kernel.y_tree_list[i] + else f"x_blocks_{i}" + ) + code.splice( + f"num_blocks_{i} = {blocks_expr}" + if i == 0 + else f"num_blocks_{i} = num_blocks_{i - 1} + {blocks_expr}" + ) + + class RoundRobinDispatch: + """ + The dispatcher which dispatches the subkernels in a round robin manner: + the blocks are interleavedly dispatched to each subkernel to execute them + in parallel. + The class defines the methods specific to the dispatch algorithm. + Methods: + codegen_pid_range(...): codegen the pid range for each subkernel. + grid(...): codegen the grid size for launching the combo kernel. + """ + + grid_expr = RoundRobinComboKernelGrid + + @classmethod + def codegen_pid_range( + cls, kernel: "ComboKernel", num: int, code: IndentedBuffer + ) -> None: + num_kernels = len(kernel.sub_kernels) + if num == 0: + cond = "if" + else: + cond = "elif" + code.splice(f"{cond} pid % {num_kernels} == {num}:") + with code.indent(): + code.splice(f"pid_offset = pid // {num_kernels}") + + def __init__( + self, + triton_kernel_cls: type[TritonKernel], + enable_autotune: bool = False, + mixed_sizes: bool = False, + ) -> None: + super().__init__() + self.triton_kernel_cls = triton_kernel_cls + self.sub_kernels: list[TritonKernel] = [] + self.iter_vars_count = itertools.count() + self.grids: list[list[int]] = [] + self.min_x_blocks_list: list[int | str] = [] + self.x_numels_list: list[int | str] = [] + self.y_tree_list: list = [] + self.enable_autotune = enable_autotune + self.mixed_sizes = mixed_sizes + self.dispatch_class: ( + type[ + ComboKernel.SequentialDispatch + | ComboKernel.SequentialFlattenGridDispatch + | ComboKernel.RoundRobinDispatch + ] + | None + ) = None + self.block_args: list[str] = [] + # the following are used when autotuning is disabled + self.block_size_1d = DEFAULT_COMBO_BLOCK_SIZE_1D + self.block_size_2d = DEFAULT_COMBO_BLOCK_SIZE_2D + self.num_warps = 8 + self.block_size_reduce = 256 + self.dynamic_shape_args: list[str] = [] + + def create_sub_kernel(self, triton_kernel: TritonKernel) -> TritonKernel: + sub_kernel = triton_kernel + # pyrefly: ignore [bad-assignment] + metrics.generated_kernel_count -= 1 + sub_kernel.args = self.args + sub_kernel.iter_vars_count = self.iter_vars_count + sub_kernel.cse.iter_buffer_ids = self.cse.iter_buffer_ids + self.sub_kernels.append(sub_kernel) + return sub_kernel + + @staticmethod + def create_triton_kernel( + tiling: dict[str, sympy.Expr], + features: SIMDKernelFeatures, + optimize_mask: bool, + triton_kernel_cls: type[TritonKernel], + ) -> TritonKernel: + """ + Only allow optimize_mask=True when 1) sequential dispatch is used, + 2) numels except x dimension are the same for each sub kernel. + """ + # Flattened dispatch: all dimensions derived from single pid + if config.combo_kernel_per_subkernel_blocks: + pid_cache = { + "tl.program_id(0)": "x_pid_offset", + "tl.program_id(1)": "y_pid_offset", + } + else: + pid_cache = {"tl.program_id(0)": "pid_offset"} + + return triton_kernel_cls( + tiling, + features=features, + pid_cache=pid_cache, + optimize_mask=optimize_mask, + is_combo_kernel=True, + # foreach kernels don't work with cooperative reductions + override_cooperative_reduction=False, + ) + + def codegen_static_numels_sub_kernel( + self, code: IndentedBuffer, sub_kernel: TritonKernel, num: int + ) -> list[str]: + """ + We get a small speedup from hard coding numels if they are static. + + This code stomps on the passed-in values by writing an constant to the top of the kernel. + + In a kernel like: + def KERNEL_NAME(in_ptr0, in_ptr1, out_ptr2, xnumel, rnumel, XBLOCK : tl.constexpr, R0_BLOCK : tl.constexpr): + + We would add + xnumel = 4096 + rnumel = 768 + + After the signature, before the kernel code, if we decided to make these static. As its hardcoded, it becomes + a better signal to triton on how to unroll and do some static indexing. So, it's not so much that downstream + knows that its a static numel, as that you just plop a constant into the kernel. + """ + grid = [] + uniquify_block_sizes = [] + for tree in sub_kernel.range_trees: + simplified_tree_numel = V.graph.sizevars.simplify(tree.numel) + if isinstance(simplified_tree_numel, (Integer, int)): + code.writeline(f"{tree.prefix}numel = {int(simplified_tree_numel)}") + else: + assert f"{tree.prefix}numel_{num}" in self.dynamic_shape_args + uniquify_block_sizes.append(f"{tree.prefix}numel") + + if not tree.is_reduction: + if isinstance(simplified_tree_numel, (Integer, int)): + grid.append(int(simplified_tree_numel)) + else: + # pyrefly: ignore [bad-argument-type] + grid.append(f"{tree.prefix}numel_{num}") + + if tree.is_reduction and sub_kernel.persistent_reduction: + if isinstance(simplified_tree_numel, (Integer, int)): + val = int(simplified_tree_numel) + else: + raise RuntimeError( + "Dynamic shape on reduction dimension is not supported" + ) + val = next_power_of_2(val) + code.writeline( + f"{tree.prefix.upper()}BLOCK_{num}: tl.constexpr = {val}" + ) + + if tree.prefix == "x" and sub_kernel.no_x_dim: + code.writeline(f"XBLOCK_{num}: tl.constexpr = 1") + uniquify_block_sizes.append("XBLOCK") + elif tree.prefix in ("x", "y") and config.combo_kernel_per_subkernel_blocks: + uniquify_block_sizes.append(f"{tree.prefix.upper()}BLOCK") + elif tree.is_reduction: + if ( + config.combo_kernel_per_subkernel_blocks + or sub_kernel.persistent_reduction + ): + uniquify_block_sizes.append(f"{tree.prefix.upper()}BLOCK") + self.grids.append(grid) + return uniquify_block_sizes + + def min_x_blocks_sub_kernel(self, sub_kernel: TritonKernel, num: int) -> None: + """ + Kernels with no_x_dim being true has no tunable XBLOCK. They have a fixed number of X blocks. + Grid calculation needs to make sure that they are assigned with enough number of blocks. + """ + min_x_blocks: int | str = 0 + x_numels: int | str = 0 + for tree in sub_kernel.range_trees: + simplified_tree_numel = V.graph.sizevars.simplify(tree.numel) + if tree.prefix == "x": + if isinstance(simplified_tree_numel, (Integer, int)): + x_numels = int(simplified_tree_numel) + else: + x_numels = f"{tree.prefix}numel_{num}" + if sub_kernel.no_x_dim: + min_x_blocks = x_numels + x_numels = ( + # pyrefly: ignore [unsupported-operation] + -min_x_blocks + if isinstance(x_numels, int) + # pyrefly: ignore [redundant-cast] + else "-" + cast(str, x_numels) + ) + else: + if isinstance(simplified_tree_numel, (Integer, int)): + x_numels = int(simplified_tree_numel) + else: + x_numels = f"{tree.prefix}numel_{num}" + self.min_x_blocks_list.append(min_x_blocks) + self.x_numels_list.append(x_numels) + + def select_heuristics(self, sub_kernel: TritonKernel) -> tuple[str, dict[str, int]]: + size_hints = { + prefix: next_power_of_2(V.graph.sizevars.optimization_hint(numel)) + for prefix, numel in sub_kernel.numels.items() + if not prefix_is_reduction(prefix) or sub_kernel.inside_reduction + } + if sub_kernel.persistent_reduction: + assert sub_kernel.inside_reduction + heuristics = "persistent_reduction" + elif sub_kernel.inside_reduction: + heuristics = "reduction" + else: + heuristics = "pointwise" + return heuristics, size_hints + + def select_combo_heuristics( + self, heuristics_list: list[str], size_hints_list: list[dict[str, int]] + ) -> tuple[str, dict[str, int], TritonKernel]: + if not self.enable_autotune: + return "foreach", size_hints_list[0], self.sub_kernels[0] + if "reduction" in heuristics_list: + i, _ = max( + enumerate(size_hints_list), + key=lambda x: x[1]["x"] if heuristics_list[x[0]] == "reduction" else 0, + ) + return heuristics_list[i], size_hints_list[i], self.sub_kernels[i] + elif "pointwise" in heuristics_list: + i, _ = max( + enumerate(size_hints_list), + key=lambda x: x[1]["x"] if heuristics_list[x[0]] == "pointwise" else 0, + ) + # modify size_hint to avoid oom check fail (may be a false alarm) + num_pointwise = len([e for e in heuristics_list if e == "pointwise"]) + num_reduction = len([e for e in heuristics_list if e == "reduction"]) + num_persistent_reduction = len( + [e for e in heuristics_list if e == "persistent_reduction"] + ) + assert num_reduction == 0, ( + "combining pointwise and reduction are not supported yet." + ) + heuristics = ( + "pointwise_with_reduction" + if num_persistent_reduction > 0 + else "pointwise" + ) + if len(heuristics_list) - num_pointwise >= 4: + size_hints = size_hints_list[i] + size_hints["x"] = min(128, size_hints["x"]) + return heuristics, size_hints_list[i], self.sub_kernels[i] + else: + # find persistent_reduction with maximum rnumel + i, _ = max( + enumerate(size_hints_list), + key=lambda x: max( + (v for k, v in x[1].items() if prefix_is_reduction(k)) + ), + ) + return heuristics_list[i], size_hints_list[i], self.sub_kernels[i] + + def get_mutated_args_sub_kernels(self) -> list[str]: + mutated_args: OrderedSet[str] = OrderedSet() + for sub_kernel in self.sub_kernels: + for mutation in sub_kernel.mutations: + if mutation in sub_kernel.args.input_buffers: + mutated_args.add(sub_kernel.args.input_buffers[mutation]) + if ( + mutation in sub_kernel.args.inplace_buffers + and mutation not in V.graph.removed_buffers + and mutation not in sub_kernel.removed_buffers + ): + mutated_args.add( + cast( + InplacedBuffer, sub_kernel.args.inplace_buffers[mutation] + ).inner_name + ) + if mutation in sub_kernel.args.output_buffers: + arg = sub_kernel.args.output_buffers[mutation] + assert not isinstance(arg, RemovedArg) + mutated_args.add(arg) + return sorted(mutated_args) + + def select_dispatch_strategy(self) -> None: + if self.dispatch_class is not None: + return + if config.combo_kernel_per_subkernel_blocks: + self.dispatch_class = ComboKernel.SequentialFlattenGridDispatch + return + # mixed_sizes is used for optimize_mask, so it only allows sequential dispatch + # Not mixed sizes on y dim technically is ok to use round robin as wells. + if not self.mixed_sizes or any(isinstance(e, str) for e in self.x_numels_list): + # str in x_numels_list means a dynamic shape + self.dispatch_class = ComboKernel.SequentialDispatch + return + # A negative x_blocks_list element means the kernel is not tunable, + # i.e., no_x_dim = True + x_numels_list = [abs(cast(int, e)) for e in self.x_numels_list] + total = max(x_numels_list) * len(x_numels_list) + needed = sum(x_numels_list) + if needed / total > BLOCK_UTILIZATION: + # Introduced overhead (masked blocks) is less than 20% + self.dispatch_class = ComboKernel.RoundRobinDispatch + else: + self.dispatch_class = ComboKernel.SequentialDispatch + + def jit_line( + self, + heuristics: str, + size_hints: dict[str, int], + selected_kernel: TritonKernel, + signature: list[Any], + argdefs: list[ArgName], + size_hints_list: list[dict[str, int]], + pointwise_with_reduce: bool = False, + ) -> str: + """Write the @triton_heuristics. decorator line for the combo kernel.""" + + can_use_32bit = all(k.index_dtype == "tl.int32" for k in self.sub_kernels) + size_dtype = "tl.int32" if can_use_32bit else "tl.int64" + for i, sub in enumerate(self.sub_kernels): + self.min_x_blocks_sub_kernel(sub, i) + self.select_dispatch_strategy() + triton_meta = { + "signature": signature_to_meta( + signature, size_dtype=size_dtype, argdefs=argdefs + ), + "device": DeviceProperties.create(V.graph.get_current_device_or_throw()), + "constants": {}, + } + triton_meta["enable_fp_fusion"] = ( + # pyrefly: ignore [bad-typed-dict-key, unsupported-operation] + not config.emulate_precision_casts + ) + + for arg_num in equal_1_arg_indices(signature): + triton_meta["constants"][signature[arg_num].name] = 1 # type: ignore[index,union-attr] + + # pyrefly: ignore [bad-typed-dict-key, unsupported-operation] + triton_meta["configs"] = [config_of(signature)] + + if TritonKernel._enable_pdl_codegen(): + # pyrefly: ignore [bad-typed-dict-key, unsupported-operation] + triton_meta["launch_pdl"] = True + + mutated_args = self.get_mutated_args_sub_kernels() + dispatch = self.dispatch_class + assert dispatch is not None + + # Compute the max persistent R0_BLOCK across sub-kernels. + # This is used by _reduction_configs() to avoid generating configs + # where XBLOCK * max_persistent_rblock creates pathologically large + # tiles that cause extreme ROCm compilation times. + # The max_persistent_rblock mirrors how R0_BLOCK is computed in + # codegen_static_numels_sub_kernel() for persistent reductions. + max_persistent_rblock = 0 + for sub in self.sub_kernels: + if sub.persistent_reduction: + for tree in sub.range_trees: + if tree.is_reduction: + simplified_numel = V.graph.sizevars.simplify(tree.numel) + if isinstance(simplified_numel, (Integer, int)): + val = next_power_of_2(int(simplified_numel)) + max_persistent_rblock = max(max_persistent_rblock, val) + + inductor_meta = { + "grid_type": dispatch.grid_expr.__name__, + "combo_grid_meta": self.combo_grid_meta(size_hints_list), + "kernel_name": str(Placeholder.DESCRIPTIVE_NAME), + "mutated_arg_names": mutated_args, + **self.triton_kernel_cls.inductor_meta_common(), + } + if max_persistent_rblock > 0: + inductor_meta["max_persistent_rblock"] = max_persistent_rblock + + sub_kernel = selected_kernel + if heuristics == "foreach": + heuristics_line = f""" + @triton_heuristics.foreach( + filename=__file__, + triton_meta={triton_meta!r}, + inductor_meta={inductor_meta!r}, + ) + @triton.jit + """ + elif sub_kernel.inside_reduction: + reduction_hint = sub_kernel.features.get_reduction_hint() + heuristics_line = f""" + @triton_heuristics.{heuristics}( + size_hints={size_hints!r}, + reduction_hint={reduction_hint}, + filename=__file__, + triton_meta={triton_meta!r}, + inductor_meta={inductor_meta!r} + ) + @triton.jit + """ + else: + tile_hint = "" + if len(size_hints) == 2: + tile_hint = "tile_hint=TileHint.SQUARE," + else: + tile_hint = "tile_hint=TileHint.DEFAULT," + heuristics_line = f""" + @triton_heuristics.{heuristics}( + size_hints={size_hints!r}, {tile_hint} + filename=__file__, + triton_meta={triton_meta!r}, + inductor_meta={inductor_meta!r} + ) + @triton.jit + """ + + self.triton_meta = triton_meta + self.inductor_meta = inductor_meta + + return heuristics_line + + def codegen_blocks(self, code: IndentedBuffer) -> None: + has_yblock = any(self.y_tree_list) + + for block in self.block_args: + if "YBLOCK" in block: + size = self.block_size_2d + elif "XBLOCK" in block: + size = self.block_size_2d if has_yblock else self.block_size_1d + elif "R0_BLOCK" in block: + size = self.block_size_reduce + else: + raise AssertionError(f"{block} is not supported without autotuning") + code.splice(f"{block}: tl.constexpr = {size}") + + def get_block_args(self) -> list[ConstexprArg]: + """ + Calculate blocks from sub_kernels and range_trees. + Update self.block_args, self.y_tree_list + Return the block args + """ + block_names = {} + for i, sub_kernel in enumerate(self.sub_kernels): + y_tree = None + for tree in sub_kernel.range_trees: + if tree.is_reduction and ( + not sub_kernel.inside_reduction or sub_kernel.persistent_reduction + ): + continue + if tree.prefix == "x" and sub_kernel.no_x_dim: + continue + if tree.prefix == "y": + y_tree = tree + if config.combo_kernel_per_subkernel_blocks: + block_names[f"{tree.prefix.upper()}BLOCK_{i}"] = tree.prefix + else: + block_names[f"{tree.prefix.upper()}BLOCK"] = tree.prefix + self.y_tree_list.append(y_tree) + self.block_args = list(block_names.keys()) + + return [ConstexprArg(x) for x in block_names] + + def add_numel_to_args( + self, argdefs: list[ArgName], signature: list[Any] + ) -> list[ArgName]: + for num, sub_kernel in enumerate(self.sub_kernels): + for tree in sub_kernel.active_range_trees(): + if not isinstance(tree.numel, (Integer, int)): + # only if it is a dynamic shape + sizearg = SizeArg(f"{tree.prefix}numel_{num}", tree.numel) + signature.append(sizearg) + argdefs.append(ArgName(f"{tree.prefix}numel_{num}")) + self.dynamic_shape_args.append(f"{tree.prefix}numel_{num}") + return argdefs + + def add_numel_to_call_args( + self, name: str, call_args: list[Any], arg_types: list[Any] + ) -> None: + for num, sub_kernel in enumerate(self.sub_kernels): + for tree in sub_kernel.range_trees: + numel_name = f"{tree.prefix}numel_{num}" + if numel_name not in self.dynamic_shape_args: + continue + if isinstance(tree.numel, (Integer, Symbol)): + expr = tree.numel + else: + expr = V.graph.wrapper_code.generate_numel_expr( + name, tree, suffix=str(num) + ) + + if not tree.is_reduction or sub_kernel.inside_reduction: + call_args.append(expr) + arg_types.append(type(expr)) + + def kernel_benchmark_extra_args(self) -> list[str]: + extra_args = [] + for num, sub_kernel in enumerate(self.sub_kernels): + for tree in sub_kernel.range_trees: + numel_name = f"{tree.prefix}numel_{num}" + if numel_name not in self.dynamic_shape_args: + continue + + if not tree.is_reduction or sub_kernel.inside_reduction: + extra_args.append( + str(V.graph.sizevars.optimization_hint(tree.numel)) + ) + return extra_args + + def codegen_kernel(self, name: str | None = None) -> str: + """Generate the triton code for a combo kernel that fuses multiple sub-kernels.""" + # TODO: is it correct to use the first sub kernel's heuristics? + heuristics_list, size_hints_list = [], [] + for subkernel in self.sub_kernels: + h, s = self.select_heuristics(subkernel) + heuristics_list.append(h) + size_hints_list.append(s) + heuristics, size_hints, selected_kernel = self.select_combo_heuristics( + heuristics_list, size_hints_list + ) + pointwise_with_reduction, heuristics = ( + (True, "pointwise") + if heuristics == "pointwise_with_reduction" + else (False, heuristics) + ) + code = IndentedBuffer() + + code.splice(self.triton_kernel_cls.gen_common_triton_imports()) + if config.benchmark_combo_kernel: + code.splice(self.imports_for_benchmark_kernel()) + + seen_helpers: OrderedSet[str] = OrderedSet() + for sub_kernel in self.sub_kernels: + for helper in sub_kernel.helper_functions: + if helper not in seen_helpers: + code.writeline("") + code.splice(helper) + seen_helpers.add(helper) + + argdefs, _, signature, _ = self.args.python_argdefs() + argdefs = self.add_numel_to_args(argdefs, signature) + block_args = self.get_block_args() + if self.enable_autotune: + argdefs.extend([ArgName(x.name, is_constexpr=True) for x in block_args]) + if triton_version_uses_attrs_dict(): + signature.extend(block_args) + + code.splice( + self.jit_line( + heuristics, + size_hints, + selected_kernel, + pointwise_with_reduce=pointwise_with_reduction, + signature=signature, + argdefs=argdefs, + size_hints_list=size_hints_list, + ) + ) + kernel_name = name or str(Placeholder.KERNEL_NAME) + code.writeline( + f"def {kernel_name}({', '.join(x.full_name() for x in argdefs)}):" + ) + + with code.indent(): + if config.triton.proton_profiling: + code.writeline(f'pl.enter_scope("{kernel_name}")') + code.splice("pid = tl.program_id(0)") + if not self.enable_autotune: + self.codegen_blocks(code) + + for num, sub_kernel in enumerate(self.sub_kernels): + assert self.dispatch_class is not None + self.dispatch_class.codegen_pid_range(self, num, code) + with code.indent(): + uniquify = self.codegen_static_numels_sub_kernel( + code, sub_kernel, num + ) + sub_kernel.codegen_body() + sub_kernel._filter_pdl(sub_kernel.body) + uniquified_body = self.uniquify_block_sizes( + sub_kernel.body, num, uniquify + ) + code.splice(uniquified_body) + + code.splice("else:") + with code.indent(): + code.splice("pass") + if config.triton.proton_profiling: + code.writeline(f'pl.exit_scope("{kernel_name}")') + + if config.benchmark_combo_kernel: + code.splice(self.codegen_kernel_benchmark(num_gb=0)) + + return code.getvalue() + + def codegen_kernel_benchmark(self, num_gb: float) -> IndentedBuffer: + """ + Generates Python code for benchmarking this combo kernel. + - Creates example inputs (random tensors, constants, sizes). + - Runs the kernel on the current GPU/stream. + - Prints runtime (ms) and throughput (GB/s) using `num_gb`. + Args: + num_gb (float): The number of gigabytes to use for throughput calculation. + Returns: + IndentedBuffer: A buffer containing the generated Python benchmark code. + """ + result = IndentedBuffer() + _argdefs, call_args, signature, _ = self.args.python_argdefs() + result.writelines(["", "", "def get_args():"]) + with result.indent(): + name_cnt = itertools.count() + var_names = [] + for arg_name, arg_sig in zip(call_args, signature): + var_name = f"arg_{next(name_cnt)}" + buf = V.graph.try_get_buffer(arg_name) + if buf: + size = V.graph.sizevars.optimization_hints(buf.get_size()) + stride = V.graph.sizevars.optimization_hints(buf.get_stride()) + result.writeline( + f"{var_name} = rand_strided({size}, {stride}, device='{buf.get_device()}', dtype={buf.get_dtype()})" # noqa: B950 line too long + ) + elif arg_name in V.graph.constants: + # note that random seed is put in V.graph.constants + const_tensor = V.graph.constants[arg_name] + size = V.graph.sizevars.optimization_hints(const_tensor.size()) + stride = V.graph.sizevars.optimization_hints(const_tensor.stride()) + result.writeline( + f"{var_name} = rand_strided({size}, {stride}, device='{const_tensor.device}', dtype={const_tensor.dtype})" # type: ignore[arg-type] # noqa: B950 line too long + ) + elif isinstance(arg_sig, SizeArg): + symval_hint = V.graph.sizevars.optimization_hint(arg_sig.expr) + + # Force the seed_offset to be 0 so calls to the same kernel + # using different seed offset will have the same benchmark harness. + # We can dedup kernel definitions in this case. + if "seed_offset" in arg_sig.name: + symval_hint = 0 + result.writeline(f"{var_name} = {symval_hint}") + elif isinstance(arg_sig, WorkspaceArg): + device = V.graph.get_current_device_or_throw() + count = V.graph.sizevars.optimization_hint(arg_sig.count) + # for benchmark harness, we ignore arg_sig.zero_mode and always zero it + result.writeline( + f"{var_name} = torch.zeros({count}, device='{device}', dtype={arg_sig.dtype})" + ) + else: + raise KeyError( + f"Don't find the buffer or const tensor for {arg_name}" + ) + var_names.append(var_name) + if self.dynamic_shape_args: + var_names.extend(self.kernel_benchmark_extra_args()) + result.writeline(f"return {', '.join(var_names)},") + + result.writelines(["\n", "\n", "def call(args):"]) + device = V.graph.get_current_device_or_throw() + index = V.graph.get_current_device_or_throw().index + with result.indent(): + result.writeline(f"with {V.graph.device_ops.device_guard(index)}:") + with result.indent(): + result.writeline( + V.graph.device_ops.set_device(index) + ) # no-op to ensure context + stream_name = get_raw_stream_name(index) + result.writeline(f"{stream_name} = get_raw_stream({index})") + result.writeline( + f"{str(Placeholder.KERNEL_NAME)}.run(*args, stream={stream_name})" + ) + + # benchmark all configs + result.writelines(["\n", "\n", "def benchmark_all_configs(args):"]) + with result.indent(): + result.writeline(f"with {V.graph.device_ops.device_guard(index)}:") + with result.indent(): + result.writeline( + V.graph.device_ops.set_device(index) + ) # no-op to ensure context + result.writeline( + f"return {str(Placeholder.KERNEL_NAME)}.benchmark_all_configs(*args)" + ) + + result.writelines(["\n", "\n", "if __name__ == '__main__':"]) + with result.indent(): + result.writeline( + "from torch._inductor.runtime.benchmarking import benchmarker" + ) + result.writeline("") + + result.writeline("args = get_args()") + result.writeline( + f"ms = benchmarker.benchmark(call, fn_args=(args,), device={device.type},rep=40)" + ) + result.writeline(f"num_gb = {num_gb}") + result.writeline("gb_per_s = num_gb / (ms / 1e3)") + result.writeline( + 'print(f"{ms:.3f}ms {num_gb:.3f}GB {gb_per_s:.2f}GB/s")' + ) + + return result + + def imports_for_benchmark_kernel(self) -> str: + return textwrap.dedent( + """ + from torch._dynamo.testing import rand_strided + {} + import torch + """.format(V.graph.device_ops.import_get_raw_stream_as("get_raw_stream")) + ) + + def uniquify_block_sizes( + self, code: IndentedBuffer, num_kernel: int, uniquify: list[str] + ) -> IndentedBuffer: + if not uniquify: + return code + modified = IndentedBuffer(initial_indent=code._indent) + for line in code._lines: + if isinstance(line, str) and (blocks := [e for e in uniquify if e in line]): + modified_line = line + for block in blocks: + modified_line = modified_line.replace( + block, f"{block}_{num_kernel}" + ) + modified.writeline(modified_line) + elif isinstance(line, DeferredLine) and ( + blocks := [e for e in uniquify if e in line.line] + ): + modified_line = line.line + for block in blocks: + modified_line = modified_line.replace( + block, f"{block}_{num_kernel}" + ) + new_line = DeferredLine(line.name, modified_line) + modified.writeline(new_line) + else: + modified.writeline(line) + return modified + + def call_kernel(self, name: str) -> None: + _, call_args, _, arg_types = self.args.python_argdefs() + + wrapper = V.graph.wrapper_code + assert self.dispatch_class is not None + if self.dynamic_shape_args: + self.add_numel_to_call_args(name, call_args, arg_types) + + wrapper.generate_kernel_call( + name, + call_args, + triton=True, + arg_types=arg_types, + triton_meta=self.triton_meta, + inductor_meta=self.inductor_meta, + ) + + def combo_grid_meta(self, size_hints_list: list[dict[str, int]]) -> dict[str, Any]: + dynamic_shape = bool(self.dynamic_shape_args) + num_kernels = len(self.sub_kernels) + min_blocks = ( + max(self.min_x_blocks_list) * num_kernels if not dynamic_shape else None + ) + + meta: dict[str, Any] = { + "num_kernels": num_kernels, + "min_blocks": min_blocks, + } + + if not self.enable_autotune: + default_config: dict[str, int] = {} + if config.combo_kernel_per_subkernel_blocks: + # Per-subkernel block sizes: XBLOCK_0, XBLOCK_1, etc. + for num, sub_kernel in enumerate(self.sub_kernels): + if sub_kernel.no_x_dim: + default_config[f"XBLOCK_{num}"] = 1 + else: + block_size = ( + self.block_size_2d + if any(self.y_tree_list) + else self.block_size_1d + ) + default_config[f"XBLOCK_{num}"] = block_size + + if self.y_tree_list[num]: + default_config[f"YBLOCK_{num}"] = self.block_size_2d + else: + if "YBLOCK" in self.block_args: + default_config = { + "XBLOCK": self.block_size_2d, + "YBLOCK": self.block_size_2d, + } + else: + default_config = {"XBLOCK": self.block_size_1d} + meta["default_config"] = default_config + else: + meta["default_config"] = None + + for num, sub_kernel in enumerate(self.sub_kernels): + meta[f"no_x_dim_{num}"] = sub_kernel.no_x_dim + + if config.combo_kernel_per_subkernel_blocks: + meta[f"heuristic_{num}"] = ( + "persistent_reduction" + if sub_kernel.persistent_reduction + else "reduction" + if sub_kernel.inside_reduction + else "pointwise" + ) + + meta[f"size_hints_{num}"] = size_hints_list[num] + if meta[f"heuristic_{num}"] == "pointwise": + if len(size_hints_list[num]) == 2: + meta[f"tile_hint_{num}"] = "TileHint.SQUARE" + else: + meta[f"tile_hint_{num}"] = "TileHint.DEFAULT" + else: + meta[f"reduction_hint_{num}"] = ( + sub_kernel.features.get_reduction_hint().name + ) + + for tree in sub_kernel.range_trees: + if not tree.is_reduction: + numel_name = f"{tree.prefix}numel_{num}" + if numel_name in self.dynamic_shape_args: + meta[numel_name] = None + else: + meta[numel_name] = int(V.graph.sizevars.simplify(tree.numel)) + + return meta diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/triton_split_scan.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/triton_split_scan.py new file mode 100644 index 0000000000000000000000000000000000000000..e109eab9963243a99bd2449d84ad05401aa23623 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/triton_split_scan.py @@ -0,0 +1,223 @@ +# mypy: allow-untyped-defs +import functools + +import sympy + +from torch._inductor import config +from torch._inductor.codegen.simd import IterationRangesRoot, prefix_is_reduction +from torch._inductor.codegen.triton import ( + triton_compute_type, + TritonCSEVariable, + TritonKernel, +) +from torch._inductor.runtime.triton_heuristics import SplitScanGrid +from torch.utils._ordered_set import OrderedSet +from torch.utils._sympy.functions import CeilDiv + +from ..utils import sympy_product + + +class TritonSplitScanKernel(TritonKernel): + """Generates a triton kernel that supports ops.scan calls while also splitting + the reduction dimension over multiple triton programs. + + For this kernel, loop numels will always take the form ``(xdim, rdim)`` + and the grid has the shape ``(CeilDiv(rdim, RBLOCK), xdim)``. Communication + between blocks occurs within a global memory workspace buffer, which + must be zero-filled before launching the kernel. + + Note that generation for ``ops.reduction`` is not supported. + + For details of the communication strategy, see + https://research.nvidia.com/publication/2016-03_single-pass-parallel-prefix-scan-decoupled-look-back + + """ + + def __init__( + self, + tiling: dict[str, sympy.Expr], + pid_cache=None, + fixed_config=None, + **kwargs, + ) -> None: + assert pid_cache is None, "not supported" + assert fixed_config is None, "not supported" + super().__init__( + tiling, + **kwargs, + ) + self.no_x_dim = True + + def should_use_persistent_reduction(self) -> bool: + return False + + def should_use_cooperative_reduction(self) -> bool: + return False + + def initialize_range_tree(self, pid_cache): + prefixes = ["y", "x", "r0_"] + assert len(self.numels) <= len(prefixes), ( + "z dimension not supported for split scan" + ) + active_prefixes = prefixes[len(prefixes) - len(self.numels) :] + + grid_dims = {"r0_": 0, "x": 1, "y": 2} + for prefix in active_prefixes: + numel = self.numels[prefix] + tensor_dim = 0 if prefix_is_reduction(prefix) else None + grid_dim = grid_dims[prefix] + self.range_trees.append( + IterationRangesRoot( + f"{prefix}index", + numel, + prefix, + grid_dim, + self, # type: ignore[arg-type] + pid_cache=pid_cache, + is_loop=False, + tensor_dim=tensor_dim, + grid_dim=grid_dim, + has_zdim=False, + ) + ) + + def reduction(self, dtype, src_dtype, reduction_type, value): + raise NotImplementedError("NYI TritonSplitDimKernel reductions") + + def scan(self, dtypes, combine_fn, values): + """ + Perform an associative scan on 'values'. + """ + import triton.language as tl + + (dtype,) = dtypes + (value,) = values + + compute_type = triton_compute_type(dtype) + compute_type_triton = getattr(tl, compute_type[3:]) + + element_nbits = compute_type_triton.primitive_bitwidth + + scratch_type = "tl.uint32" if element_nbits <= 16 else "tl.uint64" + scratch_type_triton = getattr(tl, scratch_type[3:]) + scratch_elems_per_block = 3 if element_nbits == 64 else 1 + scratch_nbytes_per_block = scratch_elems_per_block * ( + scratch_type_triton.primitive_bitwidth // 8 + ) + + cse_load = functools.partial(self.cse.generate, self.loads, dtype=dtype) + cse_compute = functools.partial(self.cse.generate, self.compute) + + assert len(self.numels) == 2, "Unexpected tiling" + min_rblock = config.triton.min_split_scan_rblock + reduction_numel = sympy_product( + numel + for prefix, numel in self.numels.items() + if prefix_is_reduction(prefix) + ) + pointwise_numel = sympy_product( + numel + for prefix, numel in self.numels.items() + if not prefix_is_reduction(prefix) + ) + max_blocks = pointwise_numel * CeilDiv(reduction_numel, min_rblock) + nbytes = scratch_nbytes_per_block * max_blocks + scratch_base: str | TritonCSEVariable + scratch_base, _, offset = self.args.workspace(nelem=nbytes, zero_fill=True) + if offset != 0: + scratch_base = cse_load( + f"{scratch_base} + {self.index_to_str(offset)}", shape=() + ) + runtime_rblocks = cse_load( + f"tl.num_programs({self.range_trees[-1].index})", shape=() + ) + scratch_base = cse_load( + f"{scratch_base}.to(tl.pointer_type({scratch_type})) + xoffset * " + f"{scratch_elems_per_block} * {runtime_rblocks}", + shape=(), + ) + + masks = OrderedSet(f"{tree.prefix}mask" for tree in self.range_trees) + self.filter_masks(masks) + assert not self._load_mask, "ops.scan not supported inside ops.masked" + + value = cse_compute( + f"{value}.to({compute_type})", + dtype=dtype, + shape=value.shape, + ) + value = cse_compute( + f"tl.broadcast_to({value}, {self.dense_size_str()})", + dtype=dtype, + shape=self.dense_size_list(), + ) + + combine_helper_fn = self._lift_helper(combine_fn, (value,), (dtype,)) + dim = self.triton_tensor_ndim() - 1 + assert dim == 0, "" + shape = list(self.dense_size_list()) + del shape[dim] + + block_sum = cse_compute( + f"tl.reduce({value}, {dim}, {combine_helper_fn})", + dtype=dtype, + shape=shape, + ) + exclusive_prefix = self.cse.newvar( + dtype=dtype, + shape=shape, + ) + if element_nbits == 64: + self.compute.splice( + f""" + {exclusive_prefix} = triton_helpers.exclusive_scan_decoupled_lookback_64( + {scratch_base}, + {block_sum}, + {self.iteration_ranges_get_pid(self.range_trees[-1])}, + {combine_helper_fn}, + ) + """, + strip=True, + ) + + else: + assert element_nbits <= 32 + value_as_uint_dtype = f"tl.uint{element_nbits}" + + self.compute.splice( + f""" + {exclusive_prefix} = triton_helpers.exclusive_scan_decoupled_lookback( + {scratch_base}, + {block_sum}, + {self.iteration_ranges_get_pid(self.range_trees[-1])}, + {combine_helper_fn}, + DTYPE_VALUE_AS_UINT={value_as_uint_dtype}, + DTYPE_PACK={scratch_type}, + ) + """, + strip=True, + ) + # Compute final cumsum + block_scan = cse_compute( + f"tl.associative_scan({value}, {dim}, {combine_helper_fn})", + dtype=dtype, + shape=shape, + ) + combined_result = cse_compute( + f"{combine_helper_fn}({exclusive_prefix}, {block_scan})", + dtype=dtype, + shape=shape, + ) + return ( + cse_compute( + f"tl.where(roffset == 0, {block_scan}, {combined_result})", + dtype=dtype, + shape=block_scan.shape, + ), + ) + + def _get_heuristic(self): + return "split_scan" + + def _get_grid_type(self) -> type[SplitScanGrid]: + return SplitScanGrid diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/triton_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/triton_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f89c72ebfb1c49e45dbae10ea5c85ff45ba4fc75 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/triton_utils.py @@ -0,0 +1,300 @@ +# mypy: allow-untyped-defs +from typing import Any + +import sympy + +import torch +from torch.utils._sympy.symbol import symbol_is_type, SymT + +from .. import config +from ..runtime.hints import AttrsDescriptorWrapper +from ..utils import _type_of, expr_fits_within_32bit, triton_version_uses_attrs_dict +from ..virtualized import V +from .common import ( + ArgName, + ConstexprArg, + KernelArgType, + SizeArg, + TensorArg, + TMADescriptorArg, + WorkspaceArg, +) + + +def should_unwrap_unspec_arg(name: str): + if V.graph.is_unspec_arg(name): + # Unwrap on all devices except CPU + if V.graph.get_current_device_or_throw().type != "cpu": + return True + # Only unwrap on CPU if the input is not used as an output + if name not in V.graph.mutated_buffers: + return True + return False + + +def signature_of(arg: KernelArgType, *, size_dtype: str | None) -> str: + if isinstance(arg, TensorArg): + typ = _type_of(arg.dtype) + if should_unwrap_unspec_arg(arg.buffer): + # had unwrapped 0d tensor as scalar + new_typ = typ.lstrip("*") + if new_typ in ["fp16", "bf16"]: + return "fp32" + else: + return new_typ + else: + return typ + if isinstance(arg, SizeArg): + if arg.expr is None: + if triton_version_uses_attrs_dict(): + # In newer versions of Triton, the signature includes "None" args + # and their type is marked as "constexpr" + return "constexpr" + else: + # In older versions of Triton... + # From triton/runtime/jit.py + # `None` is nullptr. Implicitly convert to *i8. + return "*i8" + elif _arg_equals_1(arg) and triton_version_uses_attrs_dict(): + # In new versions of Triton, if we have an equal-to-1 arg that's marked as a constant, + # it should be marked as "constexpr" in the signature. + return "constexpr" + elif isinstance(arg.expr, (float, sympy.Float)): + # Python floats are natively fp64, so use fp64 to preserve precision + return "fp64" if config._use_fp64_for_unbacked_floats else "fp32" + elif isinstance(arg.expr, sympy.Symbol) and symbol_is_type( + arg.expr, (SymT.UNBACKED_FLOAT) + ): + # Unbacked floats from .item() should preserve fp64 precision + return "fp64" if config._use_fp64_for_unbacked_floats else "fp32" + elif isinstance(arg.expr, bool): + return "i1" + + # if this is a integer + if size_dtype == "tl.int32": + return "i32" + elif size_dtype == "tl.int64": + return "i64" + elif size_dtype is None: + # no hint: we'll see if we know that this is a 32-bit int, and guard if possible. + int_max = torch.iinfo(torch.int32).max + if expr_fits_within_32bit(arg.expr): + V.graph.sizevars.check_leq(arg.expr, int_max) + return "i32" + else: + return "i64" + else: + raise NotImplementedError(f"unhandled size_dtype {size_dtype}") + if isinstance(arg, WorkspaceArg): + return _type_of(arg.dtype) + if isinstance(arg, TMADescriptorArg): + if arg.api_type == "experimental": + return "nvTmaDesc" + else: + # https://github.com/triton-lang/triton/blob/9695baed9b46cf957e08b157bb4133f4a4b331c5/python/triton/runtime/jit.py#L360-L363 + assert arg.api_type == "stable" + assert arg.block_shape is not None + assert arg.dtype is not None + inner = _type_of(arg.dtype)[1:] # strip the `*`: *fp32 -> fp32 + return f"tensordesc<{inner}{list(arg.block_shape)}>" + if isinstance(arg, ConstexprArg): + return "constexpr" + raise NotImplementedError(f"unhandled {type(arg)}: {arg}") + + +def non_constexpr_signature(signature): + new_signature = [] + for arg in signature: + if not isinstance(arg, ConstexprArg): + new_signature.append(arg) + + return new_signature + + +def signature_to_meta( + signature: list[KernelArgType], + *, + size_dtype: str | None, + argdefs: list[ArgName], + indices: list[int] | None = None, + is_template: bool = False, +) -> dict[str, str]: + if indices is None: + indices = list(range(len(signature))) + + def _decide_tl_dtype(arg): + # Even if the ks0 symbol itself is within tl.int32 range, it's + # risky to use tl.int32 dtype since we may have ks0*ks1 later + # for kernels like torch.mean when dynamic shape is enabled. + # + # Check config.triton.use_block_ptr, since Triton block pointer + # does not support 64bit indexing: + # https://gist.github.com/shunting314/6a41c776171720ce4561f202dcde0ad6 + # + # If the triton metadata is for a template, don't use tl.int64 index. + # Templates like flex attention/decoding uses block pointers which + # does not support 64 bit indexing. + if ( + not config.triton.use_block_ptr + and not is_template + and isinstance(arg, SizeArg) + and arg.name.startswith("ks") + ): + return "tl.int64" + return size_dtype + + return { + argdefs[i].name: signature_of(arg, size_dtype=_decide_tl_dtype(arg)) + for i, arg in zip(indices, signature) + } + + +def _get_buffer_layout(buf_name: str) -> "torch._inductor.ir.Layout": + """Get the layout for a buffer, handling both scheduler buffers and graph inputs.""" + if V.graph.scheduler: + layout = V.graph.scheduler.get_buffer_layout(buf_name) + else: + buffer = V.graph.try_get_buffer(buf_name) + # output arg + if not buffer: + assert buf_name == V.kernel.output_node.name + layout = V.kernel.output_node.layout + else: + layout = buffer.get_layout() + return layout + + +def is_unaligned_buffer(arg: TensorArg): + buf_name = arg.buffer + if buf_name in V.graph.unaligned_buffers: + return True + + if buf_name in V.graph.graph_inputs: + # See Note: [Input Alignment handling in Inductor] + # For graph inputs that is not recorded in V.graph.unaligned_buffers, + # we know for sure the tensor is aligned. + return False + + if buf_name in V.graph.constants: + # all constants are assumed to be aligned + return False + + layout = _get_buffer_layout(buf_name) + if isinstance(layout, torch._inductor.ir.NonOwningLayout): + return not layout.maybe_guard_aligned() + else: + return False + + +def _arg_equals_1(arg: KernelArgType) -> bool: + return ( + isinstance(arg, SizeArg) + and isinstance(arg.expr, (int, sympy.Integer)) + and V.graph.sizevars.statically_known_equals(arg.expr, 1) # type: ignore[arg-type] + ) + + +def equal_1_arg_indices( + args: list[KernelArgType], + *, + indices: list[int] | None = None, +) -> tuple[int, ...]: + if indices is None: + indices = list(range(len(args))) + + equal_to_1 = tuple(i for i, arg in zip(indices, args) if _arg_equals_1(arg)) + + return equal_to_1 + + +def _is_tensor_within_2gb(arg: TensorArg) -> bool: + """Check if a tensor argument's storage is provably within 2GB. + + Mirrors HIPBackend.is_within_2gb() but uses compile-time symbolic analysis + instead of runtime tensor inspection. This enables canonicalize_pointers to + decompose pointer arithmetic into (splat(base), offset) form for buffer ops. + """ + MAX_BYTES = 2**31 - 1 + try: + # Graph inputs aren't tracked by the scheduler; get their layout + # from the graph_inputs dict to avoid KeyError in get_buffer_layout. + if arg.buffer in V.graph.graph_inputs: + inp = V.graph.graph_inputs[arg.buffer] + if hasattr(inp, "get_layout"): + layout = inp.get_layout() + else: + return False + else: + layout = _get_buffer_layout(arg.buffer) + storage_bytes = layout.storage_size() * arg.dtype.itemsize + return V.graph.sizevars.statically_known_true(storage_bytes <= MAX_BYTES) + except Exception: + return False + + +def config_of( + args: list[KernelArgType], + *, + indices: list[int] | None = None, + pointer_range_override: tuple[int, ...] | None = None, +) -> Any: + if indices is None: + indices = list(range(len(args))) + + def is_aligned(x: KernelArgType, alignment: int, include_tensor: bool) -> bool: + """ + Roughly follow triton code here: + https://github.com/triton-lang/triton/blob/5282ed890d453e10b9ee30076ef89115dd197761/python/triton/runtime/jit.py#L208-L222 + """ + if isinstance(x, TensorArg): + if include_tensor: + offset_aligned = V.graph.sizevars.statically_known_multiple_of( + x.offset * x.dtype.itemsize, + alignment, # type: ignore[arg-type] + ) + return offset_aligned and not is_unaligned_buffer(x) + else: + return False + if isinstance(x, SizeArg): + # TODO(voz): These are kinda redundant, if we can solve out statically_known_multiple_of with + # _maybe_evaluate_static... + if x.name.startswith("load_seed_offset"): + return False + if x.expr is None: + return False + if isinstance(x.expr, (float, bool)): + return False + return V.graph.sizevars.statically_known_multiple_of(x.expr, alignment) # type: ignore[arg-type] + if isinstance(x, WorkspaceArg): + # We allocate the workspace ourselves, so it is always aligned + return True + if isinstance(x, (TMADescriptorArg, ConstexprArg)): + return False + raise NotImplementedError(f"unhandled {type(x)}: {x}") + + if config.triton.divisible_by_16: + divisible_by_16 = tuple( + i + for i, arg in zip(indices, args) + if is_aligned(arg, alignment=16, include_tensor=True) + ) + else: + divisible_by_16 = () + + equal_to_1 = equal_1_arg_indices(args, indices=indices) + + # On AMD/HIP, tag tensor args whose storage fits in 2GB so Triton + # can use 32-bit pointer offsets and emit buffer load/store ops. + if pointer_range_override is not None: + pointer_range_32 = pointer_range_override + elif torch.version.hip is not None: + pointer_range_32 = tuple( + i + for i, arg in zip(indices, args) + if isinstance(arg, TensorArg) and _is_tensor_within_2gb(arg) + ) + else: + pointer_range_32 = () + + # pyrefly: ignore [bad-argument-count, bad-argument-type] + return AttrsDescriptorWrapper(divisible_by_16, equal_to_1, pointer_range_32) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/wrapper.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/wrapper.py new file mode 100644 index 0000000000000000000000000000000000000000..5ec3c9589d7a0dd29e202ac2c8fdec55aafdb344 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/wrapper.py @@ -0,0 +1,4353 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import collections +import contextlib +import dataclasses +import dis +import functools +import inspect +import logging +import operator +import os +import random +import re +import tempfile +from collections.abc import Callable +from itertools import chain, count +from typing import Any, TYPE_CHECKING + +import sympy +from sympy import Expr + +import torch +import torch._ops +import torch.utils._pytree as pytree +from torch import dtype as torch_dtype +from torch._dynamo.utils import counters, dynamo_timed, get_debug_dir +from torch._inductor.codegen.debug_utils import DebugPrinterManager +from torch._inductor.codegen.multi_kernel import MultiKernelState +from torch._inductor.runtime.runtime_utils import cache_dir +from torch._library.opaque_object import get_opaque_obj_repr, is_opaque_value_type +from torch._logging import trace_structured +from torch.fx.experimental.symbolic_shapes import ( + CallMethodKey, + ConvertIntKey, + DivideByKey, + resolve_unbacked_bindings, + SymTypes, +) +from torch.fx.node import _get_qualified_name +from torch.utils._ordered_set import OrderedSet +from torch.utils._sympy.singleton_int import SingletonInt +from torch.utils._sympy.symbol import symbol_is_type, SymT + +from .. import async_compile, config, ir +from ..codecache import output_code_log +from ..ir import IRNode, ReinterpretView +from ..runtime import triton_heuristics +from ..runtime.hints import DeviceProperties +from ..stream_constants import DEFAULT_STREAM, DEFAULT_STREAM_IDX, STREAM_NAME_TEMPLATE +from ..stream_utils import get_raw_stream_name, get_stream_name +from ..utils import ( + cache_on_self, + DelayReplaceLine, + get_benchmark_name, + get_dtype_size, + IndentedBuffer, + is_codegen_graph_partition_subgraph, + is_using_cudagraph_partition, + LineContext, + sympy_product, + sympy_str, + sympy_subs, + triton_version_uses_attrs_dict, +) +from ..virtualized import V +from .common import ( + ArgName, + CodeGen, + DeferredLine, + PythonPrinter, + WorkspaceArg, + WorkspaceZeroMode, +) +from .cpp_utils import cexpr +from .custom_extern_kernel_codegen import CUSTOM_EXTERN_KERNEL_CODEGEN +from .triton_utils import config_of, should_unwrap_unspec_arg, signature_to_meta + + +if TYPE_CHECKING: + from collections.abc import Iterable, Iterator, Sequence + + import triton + + from ..graph import GraphLowering + from ..ir import ExternKernel + from ..scheduler import BaseSchedulerNode + from .wrapper_fxir import FxConverter + + +log = logging.getLogger(__name__) + +pexpr = PythonPrinter().doprint + + +ReuseKey = tuple[torch.device, torch.dtype, str, bool, int] +CommBufferReuseKey = tuple[torch.device, torch.dtype, str, "ir.CommBufferType", str] +BufferLike = ir.Buffer | WorkspaceArg +FxConversionFunc = Callable[["WrapperLine"], None] + + +def buffer_reuse_key(node: BufferLike) -> ReuseKey: + storage_size = V.graph.get_allocation_storage_size(node) + alignment = node.get_name() not in V.graph.unaligned_buffers + stream = V.graph.scheduler.get_buf_stream(node.get_name()) + return ( + node.get_device_or_error(), + node.get_dtype(), + # NB: this is symbolic so that we don't try to reuse a buffer + # for s0 for s1, just because they happen to share the same + # size hint + sympy_str(V.graph.sizevars.simplify(storage_size)), + alignment, + stream, + ) + + +def comm_buffer_reuse_key(node: BufferLike) -> CommBufferReuseKey: + # Comm buffers can only be reused by other comm buffers with the same (device, dtype, size, comm_buffer_type, group_name). + storage_size = V.graph.get_allocation_storage_size(node) + layout = node.get_output_spec() + assert isinstance(layout, ir.CommBufferLayout) + return ( + node.get_device_or_error(), + node.get_dtype(), + sympy_str(V.graph.sizevars.simplify(storage_size)), + layout.comm_buffer_type, + layout.group_name, + ) + + +def can_match_buffer_size(input_buf: BufferLike, output_buf: BufferLike): + # Return True if input_buf can be re-inplaced for output_buf. + # This differs from `buffer_reuse_key` for general buffer reuse. + if input_buf.get_device_or_error() != output_buf.get_device_or_error(): + return False + + if input_buf.get_dtype() != output_buf.get_dtype(): + return False + + input_size = V.graph.sizevars.simplify( + V.graph.get_allocation_storage_size(input_buf) + ) + output_size = V.graph.sizevars.simplify( + V.graph.get_allocation_storage_size(output_buf) + ) + + if ( + # NB: this is symbolic so that we don't try to reuse a buffer + # for s0 for s1, just because they happen to share the same + # size hint + sympy_str(input_size) == sympy_str(output_size) + ) or ( + # statically known that 0.95 * input_size <= output_size <= input_size + V.graph.sizevars.statically_known_geq(output_size, 0.95 * input_size) + and V.graph.sizevars.statically_known_leq(output_size, input_size) + ): + return True + + return False + + +def codegen_reinterpret_view_helper(data): + """ + Collapse a chain of ReinterpretView <- StorageBox + <- ReinterpretView <- StorageBox.... <- buffer wrappers if every layer + has the same offset as the innermost (base) buffer. + + Returns: + (size, stride, offset, dtype, collapsible: bool) + """ + if isinstance(data, ir.Buffer): + lay = data.get_layout() + return lay.size, lay.stride, lay.offset, lay.dtype, True + + layouts: list[Any] = [] + cur = data + while isinstance(cur, (ir.TensorBox, ir.StorageBox, ir.ReinterpretView)): + lay = cur.get_layout() + if lay is None: + return None, None, None, None, False + layouts.append(lay) + cur = cur.data # unwrap + + if not isinstance(cur, ir.Buffer): + return None, None, None, None, False + + # All wrapper offsets must match base offset to be collapsible + for lay in layouts: + if lay.offset != cur.get_layout().offset: + return None, None, None, None, False + + base_lay = cur.get_layout() + return base_lay.size, base_lay.stride, base_lay.offset, base_lay.dtype, True + + +# TODO: Move to a well known place +TritonMetaParams = dict[str, int] +TritonGrid = ( + tuple[int | sympy.Expr, ...] | Callable[[TritonMetaParams], tuple[int, ...]] +) + + +def user_defined_kernel_grid_fn_code( + name: str, + configs: list[triton.Config], # type: ignore[name-defined] + grids: list[TritonGrid], + wrapper: PythonWrapperCodegen | None = None, + original_fxnode_name: str | None = None, +) -> tuple[str, str]: + output = IndentedBuffer() + + def _convert_to_sympy_expr(item: int | sympy.Expr) -> sympy.Expr: + return item if isinstance(item, sympy.Expr) else sympy.Integer(item) + + def determine_grid( + grid: TritonGrid, + example_grid: TritonGrid | None = None, + ): + """ + This function return a tuple of two values: the first one is for the real grid + which is used in the generated code; the second one is an example grid with + concreate values which is used in the autotune block to run the generated + kernels at compile time. + """ + if wrapper is None or callable(grid): + # return as-is when used in eager mode or when grid is callable + return grid, grid + # Grid contains ints/Expr, so utilize wrapper's expr printer for codegen + sympy_grid = tuple(_convert_to_sympy_expr(g) for g in grid) + if not example_grid: + example_grid = sympy_grid + return ( + wrapper.codegen_python_shape_tuple(sympy_grid), + ( + wrapper.codegen_python_shape_tuple( + tuple( + wrapper.generate_example_arg_value(g, type(g)) + for g in example_grid # type: ignore[union-attr] + ) + ) + if config.triton.autotune_at_compile_time + else None + ), + ) + + def writeline(line: str, example_grid: str | None = None): + output.writeline(line) + if ( + wrapper + and config.triton.autotune_at_compile_time + and name not in wrapper.kernel_autotune_names + ): + wrapper.kernel_autotune_calls.writeline(example_grid or line) + + fn_name = f"grid_wrapper_for_{name}" + writeline(f"def {fn_name}(meta):") + kernel_autotune_calls_indent = ( + wrapper.kernel_autotune_calls.indent() + if wrapper and config.triton.autotune_at_compile_time + else contextlib.nullcontext() + ) + with output.indent(), kernel_autotune_calls_indent: + if ( + config.triton.autotune_at_compile_time + and original_fxnode_name + and V.graph.autotuning_grids + and original_fxnode_name in V.graph.autotuning_grids + ): + example_grids = V.graph.autotuning_grids[original_fxnode_name] + else: + example_grids = [None] * len(grids) + if len(grids) == 1: + grid, example_grid = determine_grid(grids[0], example_grids[0]) + writeline(f"return {grid}", f"return {example_grid}") + else: + assert len(grids) > 1 + assert len(grids) == len(configs) + seen: OrderedSet[str] = OrderedSet() + # sort the configs from the largest # of kwargs to the smallest to + # emit the grids in the order of (approximately) decreasing specificity + # TODO(aakhundov): the sorting below is generally not sufficient, so + # maybe we'll need to restrict the supported cases to identical kwarg + # names in all autotuning configs. + for grid, c, example_grid in sorted( + zip(grids, configs, example_grids), + key=lambda x: len(x[1].kwargs), + reverse=True, + ): + guardslist = [] + if c.kwargs: + # Remove AMD specific kwargs. + for kwarg in c.kwargs: + if kwarg not in [ + "matrix_instr_nonkdim", + "waves_per_eu", + "kpack", + ]: + guardslist.append(f"meta['{kwarg}'] == {c.kwargs[kwarg]}") + if guardslist: + guards = " and ".join(guardslist) + else: + guards = "True" # for configs with empty kwargs + grid, example_grid = determine_grid(grid, example_grid) + statement = f"if {guards}: return {grid}" + if statement in seen: + continue + seen.add(statement) + writeline(statement, f"if {guards}: return {example_grid}") + + return fn_name, output.getvalue() + + +def user_defined_triton_kernel_transitive_closure_source_code( + kernel, epilogue_fusion: tuple[ir.ComputedBuffer, str] | None = None +) -> str: + """ + Given a triton kernel function pointer collect the transitive closure of + its dependencies + + epilogue_fusion: Optional[(fused epilogue node, modified kerel src code)] + """ + compile_wrapper = IndentedBuffer() + kernel_src = kernel.src + if epilogue_fusion: + kernel_src = epilogue_fusion[1] + compile_wrapper.splice(kernel_src, strip=True) + + # Also include any possible kernel being called indirectly + import triton + from triton import JITFunction # type: ignore[name-defined, attr-defined] + from triton.language import constexpr # type: ignore[name-defined] + from triton.language.core import dtype as triton_dtype + + # global constexpr vars handled above + symbols_included = OrderedSet([kernel.__name__]) + + def traverse(cur_kernel): + # here we extract the unqualified names (i.e., not attributes and + # without prepended module name) loaded in the kernel code, which + # are matched with the co_names and __globals__ below to codegen + # the respective imports necessary for the kernel compilation + unqualified_loads = OrderedSet( + inst.argval + for inst in dis.Bytecode(cur_kernel.fn) + if inst.opname == "LOAD_GLOBAL" + ) + global_annotations = cur_kernel.fn.__globals__.get("__annotations__", {}) + for symbol_name in cur_kernel.fn.__code__.co_names: + if symbol_name in symbols_included: + continue + if symbol_name in cur_kernel.fn.__globals__: + symbol = cur_kernel.fn.__globals__[symbol_name] + if isinstance(symbol, JITFunction): + compile_wrapper.newline() + compile_wrapper.writeline("@triton.jit") + compile_wrapper.splice(symbol.src, strip=True) + symbols_included.add(symbol_name) + traverse(symbol) + elif hasattr(triton, "constexpr_function") and isinstance( + symbol, + triton.runtime.jit.ConstexprFunction, + ): + # Import dtype class if used in type annotations + if "dtype" in symbol.src and "dtype" not in symbols_included: + dtype_symbol = symbol.fn.__globals__.get("dtype") + if ( + dtype_symbol + and hasattr(dtype_symbol, "__module__") + and dtype_symbol.__module__.startswith("triton") + ): + compile_wrapper.writeline( + f"from {dtype_symbol.__module__} import dtype as dtype" + ) + symbols_included.add("dtype") + compile_wrapper.newline() + compile_wrapper.writeline("@triton.constexpr_function") + compile_wrapper.splice(symbol.src, strip=True) + if symbol_name != symbol.fn.__name__: + compile_wrapper.writeline( + f"{symbol_name} = {symbol.fn.__name__}" + ) + symbols_included.add(symbol_name) + traverse(symbol) + elif isinstance(symbol, (int, str, bool, constexpr)): + compile_wrapper.newline() + if isinstance(symbol, constexpr): + symbol_str = f"tl.constexpr({symbol.value!r})" + else: + symbol_str = f"{symbol!r}" + if annotation := global_annotations.get(symbol_name): + if isinstance(annotation, type): + annotation_code = ( + f": {annotation.__module__}.{annotation.__name__}" + ) + else: + annotation_code = f": {annotation!r}" + compile_wrapper.writeline( + f"{symbol_name}{annotation_code} = {symbol_str}" + ) + else: + compile_wrapper.writeline(f"{symbol_name} = {symbol_str}") + symbols_included.add(symbol_name) + elif ( + symbol_name in unqualified_loads + and symbol_name != "tl" # already imported + and hasattr(symbol, "__module__") + # only codegen imports from triton; JITFunctions + # imported from other modules will be codegened + # in the separate branch above + and symbol.__module__.startswith("triton") + ): + # a global symbol imported from triton is referenced + # without module qualification (i.e., `store` instead + # of `tl.store`): need to codegen an import + + # Triton dtype instances have .name instead of .__name__ + if isinstance(symbol, triton_dtype): + compile_wrapper.writeline(f"{symbol_name} = tl.{symbol.name}") + elif hasattr(symbol, "__name__"): + compile_wrapper.writeline( + f"from {symbol.__module__} import {symbol.__name__} as {symbol_name}" + ) + symbols_included.add(symbol_name) + + traverse(kernel) + return compile_wrapper.getvalue() + + +@dataclasses.dataclass +class SymbolicCallArg: + inner: sympy.Symbol + # the original symbolic expression represented by inner + inner_expr: sympy.Expr + + def __str__(self): + return str(self.inner) + + +class MemoryPlanningState: + def __init__(self): + super().__init__() + # Regular buffer reuse pool + self.reuse_pool: dict[ReuseKey, list[FreeIfNotReusedLine]] = ( + collections.defaultdict(list) + ) + # Separate pool for comm buffers (comm-comm reuse only) + self.comm_buffer_reuse_pool: dict[ + CommBufferReuseKey, list[FreeIfNotReusedLine] + ] = collections.defaultdict(list) + self.total_allocated_buffer_size: int = 0 + + def __contains__(self, key: ReuseKey) -> bool: + return bool(self.reuse_pool.get(key, None)) + + def pop(self, key: ReuseKey) -> FreeIfNotReusedLine: + item = self.reuse_pool[key].pop() + assert not item.is_reused + return item + + def push(self, key: ReuseKey, item: FreeIfNotReusedLine) -> None: + assert not item.is_reused + self.reuse_pool[key].append(item) + + def comm_buffer_contains(self, key: CommBufferReuseKey) -> bool: + return bool(self.comm_buffer_reuse_pool.get(key, None)) + + def comm_buffer_pop(self, key: CommBufferReuseKey) -> FreeIfNotReusedLine: + item = self.comm_buffer_reuse_pool[key].pop() + assert not item.is_reused + return item + + def comm_buffer_push( + self, key: CommBufferReuseKey, item: FreeIfNotReusedLine + ) -> None: + assert not item.is_reused + self.comm_buffer_reuse_pool[key].append(item) + + +class WrapperLine: + def codegen_fx(self, converter: FxConverter) -> FxConversionFunc: + raise NotImplementedError(f"FX codegen not yet supported for type {type(self)}") + + +@dataclasses.dataclass +class EnterSubgraphLine(WrapperLine): + wrapper: PythonWrapperCodegen + graph: GraphLowering + + def __post_init__(self) -> None: + self.wrapper.push_computed_sizes(self.wrapper.computed_sizes) + + def codegen(self, code: IndentedBuffer) -> None: + self.wrapper.push_codegened_graph(self.graph) + code.do_indent() + + def codegen_fx(self, converter: FxConverter) -> FxConversionFunc: + return converter._generate_enter_subgraph + + +@dataclasses.dataclass +class ConditionalLine(WrapperLine): + wrapper: PythonWrapperCodegen + node: ir.Conditional + + def codegen(self, code: IndentedBuffer) -> None: + raise NotImplementedError("Only supports FX codegen") + + @staticmethod + def codegen_fx(converter: FxConverter) -> FxConversionFunc: + return converter._generate_conditional + + +@dataclasses.dataclass +class CommentLine(WrapperLine): + line: LineContext + + def codegen(self, code: IndentedBuffer) -> None: + code.writeline(self.line) + + @staticmethod + def codegen_fx(converter: FxConverter) -> FxConversionFunc: + return converter._generate_comment + + +@dataclasses.dataclass +class DynamicScalarLine(WrapperLine): + wrapper: PythonWrapperCodegen + node: ir.DynamicScalar + + def codegen(self, code: IndentedBuffer) -> None: + self.wrapper._codegen_dynamic_scalar(self.node) + + @staticmethod + def codegen_fx(converter: FxConverter) -> FxConversionFunc: + return converter._generate_dynamic_scalar + + +@dataclasses.dataclass +class ExitSubgraphLine(WrapperLine): + wrapper: PythonWrapperCodegen + + def __post_init__(self) -> None: + self.wrapper.computed_sizes = self.wrapper.pop_computed_sizes() + + def codegen(self, code: IndentedBuffer) -> None: + self.wrapper.pop_codegened_graph() + code.do_unindent() + + def codegen_fx(self, converter: FxConverter) -> FxConversionFunc: + return converter._generate_exit_subgraph + + +@dataclasses.dataclass +class EnterDeviceContextManagerLine(WrapperLine): + device_idx: int + last_seen_device_guard_index: int | None + + def codegen(self, code: IndentedBuffer) -> None: + if V.graph.cpp_wrapper: + code.writeline("\n") + if V.graph.aot_mode: + # In AOT mode, we have a stream provided as a param. A stream is + # associated with a device, so we never expect the device to change. + # CUDAStreamGuard sets the stream and the device. + if self.last_seen_device_guard_index is None: + code.writeline( + f"{V.graph.device_ops.cpp_aoti_stream_guard()} stream_guard(stream, this->device_idx_);" + ) + else: + assert self.last_seen_device_guard_index == self.device_idx, ( + "AOTInductor only supports running on one CUDA device" + ) + else: + if self.last_seen_device_guard_index is None: + code.writeline( + f"{V.graph.device_ops.cpp_aoti_device_guard()} device_guard({self.device_idx});" + ) + else: + code.writeline(f"device_guard.set_index({self.device_idx});") + else: + # Note _DeviceGuard has less overhead than device, but only accepts + # integers + code.writeline(f"with {V.graph.device_ops.device_guard(self.device_idx)}:") + code.do_indent() + code.writeline(V.graph.device_ops.set_device(self.device_idx)) + + def codegen_fx(self, converter: FxConverter) -> FxConversionFunc: + return converter._generate_enter_device_context_manager + + +class ExitDeviceContextManagerLine(WrapperLine): + def codegen(self, code: IndentedBuffer) -> None: + if not V.graph.cpp_wrapper: + code.do_unindent() + + def codegen_fx(self, converter: FxConverter) -> FxConversionFunc: + return converter._generate_exit_device_context_manager + + +@dataclasses.dataclass +class ExternKernelAllocLine(WrapperLine): + wrapper: PythonWrapperCodegen + node: ir.ExternKernelAlloc + + def codegen(self, code: IndentedBuffer) -> None: + node = self.node + args = [*node.codegen_args(), *node.codegen_kwargs()] + self.wrapper._generate_extern_kernel_alloc_helper(self.node, args) + + def codegen_fx(self, converter: FxConverter) -> FxConversionFunc: + return converter._generate_extern_kernel_alloc + + +@dataclasses.dataclass +class ExternKernelOutLine(WrapperLine): + wrapper: PythonWrapperCodegen + node: ir.ExternKernelOut + + def codegen(self, code: IndentedBuffer) -> None: + node = self.node + args = [*node.codegen_args(), *node.codegen_kwargs(skip_out=True)] + kernel_name = node.get_kernel_name() + if ( + V.graph.cpp_wrapper + and node.cpp_kernel_name == "torch::inductor::_mm_plus_mm" + ): + # For https://github.com/pytorch/pytorch/issues/128474 + kernel_name = "aoti_torch__mm_plus_mm_out" + else: + kernel_name = node.get_kernel_name() + device = d.type if (d := node.get_device()) else V.graph.device_type + self.wrapper._generate_extern_kernel_out_helper( + kernel_name, + node.codegen_reference(), + node.output_view.codegen_reference() if node.output_view else None, + args, + device, + self.node.get_stack_traces(), + ) + + def codegen_fx(self, converter: FxConverter) -> FxConversionFunc: + return converter._generate_extern_kernel_out + + +@dataclasses.dataclass +class ExternKernelMultiOutLine(WrapperLine): + """Codegen line for multi-output .out() variant calls. + + Generates a kernel call with pre-allocated output buffers passed as + keyword arguments. E.g. kernel(x, out0=buf0, out1=buf1). + """ + + wrapper: PythonWrapperCodegen + node: ir.ExternKernelMultiOut + + def codegen(self, code: IndentedBuffer) -> None: + node = self.node + kernel_name = node.get_kernel_name() + + args = [*node.codegen_args(), *node.codegen_kwargs()] + for out_name, out_node in zip( + node.out_arg_names, node.out_variant_output_nodes + ): + args.append(f"{out_name}={out_node.get_name()}") + + code.writeline(f"{node.get_name()} = {kernel_name}({', '.join(args)})") + + for out_node in node.out_variant_output_nodes: + if isinstance(out_node.layout, ir.Layout): + out_node.codegen_size_asserts(self.wrapper) + + +@dataclasses.dataclass +class FreeLine(WrapperLine): + wrapper: PythonWrapperCodegen + node: BufferLike | ir.TorchBindObject + + def codegen(self, code: IndentedBuffer) -> None: + assert self.node.get_name() not in V.graph.removed_buffers + code.writeline(self.wrapper.make_buffer_free(self.node)) + + def codegen_fx(self, converter: FxConverter) -> FxConversionFunc: + return converter._generate_free + + +@dataclasses.dataclass +class KernelCallLine(WrapperLine): + wrapper: PythonWrapperCodegen + kernel_name: str + call_args: tuple[Any, ...] + raw_keys: tuple[Any, ...] + raw_args: tuple[Any, ...] + arg_types: list[str] + triton: bool + triton_meta: dict[str, Any] + inductor_meta: dict[str, Any] | None + device: torch.device + graph_name: str + original_fxnode_name: str + current_stream_idx: int | None = None + + def codegen(self, code: IndentedBuffer) -> None: + self.wrapper._generate_kernel_call_helper( + self.kernel_name, + self.call_args, + triton=self.triton, + arg_types=self.arg_types, + raw_keys=self.raw_keys, + raw_args=self.raw_args, + triton_meta=self.triton_meta, + inductor_meta=self.inductor_meta, + device=self.device, + graph_name=self.graph_name, + original_fxnode_name=self.original_fxnode_name, + current_stream_idx=self.current_stream_idx, + ) + + def codegen_fx(self, converter: FxConverter) -> FxConversionFunc: + return converter._generate_kernel_call + + +@dataclasses.dataclass +class KernelDefinitionLine(WrapperLine): + wrapper: PythonWrapperCodegen + kernel_name: str + kernel_body: str + metadata: str | None = None + gpu: bool = True + cpp_definition: str | None = None + + def codegen(self, code: IndentedBuffer) -> None: + self.wrapper._define_kernel_helper( + self.kernel_name, + self.kernel_body, + metadata=self.metadata, + gpu=self.gpu, + cpp_definition=self.cpp_definition, + ) + + def codegen_fx(self, converter: FxConverter) -> FxConversionFunc: + return converter._generate_kernel_definition + + +@dataclasses.dataclass +class MemoryPlanningLine(WrapperLine): + wrapper: PythonWrapperCodegen + + def plan(self, state: MemoryPlanningState) -> MemoryPlanningLine: + """First pass to find reuse""" + return self + + def codegen(self, code: IndentedBuffer) -> None: + """Second pass to output code""" + + def __str__(self) -> str: + """ + Emits a string representation that fits on one line. + """ + args: list[str] = [] + for field in dataclasses.fields(self): + if field.name == "wrapper": + continue + val = getattr(self, field.name) + args.append( + f"{field.name}={val.get_name() if field.type is ir.Buffer else val}" + ) + return f"{type(self).__name__}({', '.join(args)})" + + +@dataclasses.dataclass +class EnterDeviceContextManagerWithStreamInfoLine(EnterDeviceContextManagerLine): + """Enter a CUDA device context and retrieve user stream objects. + + Attributes: + num_streams: Number of streams (determined by user annotations on nodes). + stream_idx_to_user_obj_idx: Maps stream_idx → user_object_index for + retrieving user stream objects via get_external_object_by_index. + """ + + num_streams: int = 1 + stream_idx_to_user_obj_idx: dict[int, int] = dataclasses.field(default_factory=dict) + + def codegen(self, code: IndentedBuffer) -> None: + """Generate context switching and stream retrieval code.""" + if V.graph.cpp_wrapper: + super().codegen(code) + else: + super().codegen(code) + code.writeline(f"{DEFAULT_STREAM} = torch.cuda.current_stream()") + + if self.num_streams > 1: + for i in range(1, self.num_streams): + user_obj_idx = self.stream_idx_to_user_obj_idx[i] + code.writeline( + f"{STREAM_NAME_TEMPLATE.format(stream_idx=i)} " + f"= get_external_object_by_index({user_obj_idx})", + ) + + +@dataclasses.dataclass +class ExitDeviceContextManagerWithStreamInfoLine(ExitDeviceContextManagerLine): + """Exit a CUDA device context. + + Attributes: + num_streams: Number of streams that were allocated (must match Enter). + """ + + num_streams: int = 1 + + def codegen(self, code: IndentedBuffer) -> None: + """Generate context exit code.""" + if not V.graph.cpp_wrapper: + code.do_unindent() + + +@dataclasses.dataclass +class EnterCudaStreamContextLine(WrapperLine): + """Enter a context executed by respective CUDA Stream. + + Attributes: + stream_idx: The index number corresponds to the entering CUDA Stream context. + """ + + stream_idx: int + + def codegen(self, code: IndentedBuffer) -> None: + code.writeline(f"with torch.cuda.stream({get_stream_name(self.stream_idx)}):") + code.do_indent() + + +@dataclasses.dataclass +class ExitCudaStreamContextLine(WrapperLine): + """Generate code to exit the current stream context.""" + + def codegen(self, code: IndentedBuffer) -> None: + code.do_unindent() + + +class EfficientPeakEstimate: + def __init__(self): + from ..memory import estimate_peak_memory, get_freeable_input_buf + + scheduler_nodes = V.graph.scheduler.nodes + graph_inputs = OrderedSet(V.graph.graph_inputs.keys()) + graph_outputs = OrderedSet(V.graph.get_output_names()) + names_to_freeable_bufs = get_freeable_input_buf(scheduler_nodes, graph_inputs) + self.overall_peak_memory, peak_by_scheduler_node = estimate_peak_memory( + scheduler_nodes, + names_to_freeable_bufs, + graph_outputs, + ) + + from .segmented_tree import SegmentedTree + + self.segmented_tree = SegmentedTree( + peak_by_scheduler_node, operator.add, max, 0 + ) + + def _get_size(self, node: BufferLike) -> int: + return V.graph.sizevars.optimization_hint( + V.graph.get_allocation_storage_size(node), fallback=0 + ) * get_dtype_size(node.get_dtype()) + + def peak_between(self, line_a: FreeIfNotReusedLine, line_b: AllocateLine): + return self.segmented_tree.summarize_range( + line_a.scheduler_node_index + 1, line_b.scheduler_node_index - 1 + ) + + def update_peak_between(self, line_a: FreeIfNotReusedLine, line_b: AllocateLine): + if line_a.scheduler_node_index + 1 == line_b.scheduler_node_index: + return + self.segmented_tree.update_range( + line_a.scheduler_node_index + 1, + line_b.scheduler_node_index - 1, + self._get_size(line_b.node), + ) + + +@dataclasses.dataclass +class AllocateLine(MemoryPlanningLine): + """Represents a buffer allocation during memory planning.""" + + node: BufferLike + comm_buffer: bool = False + + def __post_init__(self): + assert V.graph.scheduler.current_node is not None + self.scheduler_node_index = V.graph.scheduler.nodes.index( + V.graph.scheduler.current_node + ) + + def should_reuse_buffer(self, free_line: FreeIfNotReusedLine, size: int) -> bool: + if self.comm_buffer: + return True + if free_line.scheduler_node_index + 1 == self.scheduler_node_index: + return True + overall_peak_memory = self.wrapper.estimate_peak.overall_peak_memory + peak_memory_in_range = self.wrapper.estimate_peak.peak_between(free_line, self) + new_peak_memory = size + peak_memory_in_range + return new_peak_memory <= overall_peak_memory + + def plan(self, state: MemoryPlanningState) -> MemoryPlanningLine: + if self.node.get_name() in V.graph.removed_buffers: + return NullLine(self.wrapper) + + if self.comm_buffer: + # Comm buffers use separate pool (comm-comm reuse only) + key = comm_buffer_reuse_key(self.node) + if config.allow_buffer_reuse and state.comm_buffer_contains(key): + free_line = state.comm_buffer_pop(key) + free_line.is_reused = True + return ReuseLine( + self.wrapper, free_line.node, self.node, comm_buffer=True + ) + return self + + # Regular buffer reuse + # Stream is part of the key, so cross-stream reuse is naturally prevented. + key = buffer_reuse_key(self.node) + if config.allow_buffer_reuse and key in state: + free_line = state.pop(key) + size = V.graph.sizevars.optimization_hint( + V.graph.get_allocation_storage_size(self.node), fallback=0 + ) * get_dtype_size(self.node.get_dtype()) + if self.should_reuse_buffer(free_line, size): + free_line.is_reused = True + self.wrapper.estimate_peak.update_peak_between(free_line, self) + return ReuseLine(self.wrapper, free_line.node, self.node) + else: + state.push(key, free_line) + return self + + if self.node.get_device_or_error().type == "cpu": + static_shape = self.wrapper.static_shape_for_buffer_or_none(self.node) + if static_shape is not None: + state.total_allocated_buffer_size += int( + functools.reduce(operator.mul, static_shape, 1) + ) + + return self + + def codegen(self, code: IndentedBuffer) -> None: + assert self.node.get_name() not in V.graph.removed_buffers + if self.comm_buffer: + self._codegen_comm_buffer(code) + else: + line = self.wrapper.make_buffer_allocation(self.node) + code.writeline(line) + + def _codegen_comm_buffer(self, code: IndentedBuffer) -> None: + """Generate allocation code for comm buffers.""" + name = self.node.get_name() + device = self.node.get_device() + assert device is not None and device.index is not None, ( + f"Comm buffer requires a valid CUDA device with index, got {device}" + ) + dtype = self.node.get_dtype() + shape = tuple(self.node.get_size()) + stride = tuple(self.node.get_stride()) + layout = self.node.get_output_spec() + assert isinstance(layout, ir.CommBufferLayout) + comm_buffer_type = layout.comm_buffer_type + group_name = layout.group_name + + if comm_buffer_type == ir.CommBufferType.SYMM_MEM: + line = ( + f"{name} = empty_strided_p2p(" + f"{self.wrapper.codegen_shape_tuple(shape)}, " + f"{self.wrapper.codegen_shape_tuple(stride)}, " + f"{dtype}, " + f'torch.device("cuda:{device.index}"), ' + f'group_name="{group_name}", ' + f"alloc_id={random.randint(0, 2**64 - 1)})" + ) + else: + raise NotImplementedError( + f"Unsupported comm buffer type: {comm_buffer_type}" + ) + code.writeline(line) + + def codegen_fx(self, converter: FxConverter) -> FxConversionFunc: + if self.comm_buffer: + return converter._generate_comm_buffer_allocate + return converter._generate_allocate + + +@dataclasses.dataclass +class FreeIfNotReusedLine(MemoryPlanningLine): + node: BufferLike + is_reused: bool = False + comm_buffer: bool = False + + def __post_init__(self): + assert V.graph.scheduler.current_node is not None + self.scheduler_node_index = V.graph.scheduler.nodes.index( + V.graph.scheduler.current_node + ) + + def plan(self, state: MemoryPlanningState) -> MemoryPlanningLine: + if len(self.node.get_inputs_that_alias_output()) > 0: + return self + if isinstance(self.node.layout, ir.MultiOutputLayout): + return self + assert not self.is_reused + if self.node.get_name() in V.graph.removed_buffers: + return NullLine(self.wrapper) + if config.allow_buffer_reuse: + if self.comm_buffer: + # Comm buffers use separate pool (comm-comm reuse only) + key = comm_buffer_reuse_key(self.node) + state.comm_buffer_push(key, self) + else: + key = buffer_reuse_key(self.node) + state.push(key, self) + return self + + def codegen(self, code: IndentedBuffer) -> None: + assert self.node.get_name() not in V.graph.removed_buffers + if not self.is_reused: + line = self.wrapper.make_buffer_free(self.node) + if self.comm_buffer: + layout = self.node.get_output_spec() + assert isinstance(layout, ir.CommBufferLayout) + code.writeline(f"{line} # {layout.comm_buffer_type.value} buffer free") + else: + code.writeline(line) + + def codegen_fx(self, converter: FxConverter) -> FxConversionFunc: + if self.comm_buffer: + return converter._generate_comm_buffer_free + return converter._generate_free_if_not_reused + + +@dataclasses.dataclass +class ReinterpretLine(MemoryPlanningLine): + node: BufferLike + reused_as: BufferLike + layout: ir.Layout + + def plan(self, state: MemoryPlanningState) -> MemoryPlanningLine: + return self + + def codegen(self, code: IndentedBuffer) -> None: + assert isinstance(self.layout, ir.NonOwningLayout) + assert isinstance(self.layout.view, ir.ReinterpretView) + self.wrapper.codegen_deferred_allocation( + self.reused_as.get_name(), self.layout.view + ) + + def codegen_fx(self, converter: FxConverter) -> FxConversionFunc: + return converter._generate_reinterpret + + +@dataclasses.dataclass +class ReuseLine(MemoryPlanningLine): + node: BufferLike + reused_as: BufferLike + delete_old: bool = True + comm_buffer: bool = False + + def plan(self, state: MemoryPlanningState) -> MemoryPlanningLine: + if self.node.get_name() in V.graph.removed_buffers: + assert self.reused_as.get_name() in V.graph.removed_buffers + return NullLine(self.wrapper) + assert self.reused_as.get_name() not in V.graph.removed_buffers + return self + + def codegen(self, code: IndentedBuffer) -> None: + assert self.node.get_name() not in V.graph.removed_buffers + assert self.reused_as.get_name() not in V.graph.removed_buffers + code.writeline( + self.wrapper.make_buffer_reuse(self.node, self.reused_as, self.delete_old) + ) + + def codegen_fx(self, converter: FxConverter) -> FxConversionFunc: + return converter._generate_reuse + + +class NullLine(MemoryPlanningLine): + def codegen_fx(self, converter: FxConverter) -> FxConversionFunc: + return converter._generate_null + + +@dataclasses.dataclass +class MultiOutputLine(WrapperLine): + """ + Given a MultiOutputLayout buffer, indexes actual buffer(s) from the result. + """ + + wrapper: PythonWrapperCodegen + result_name: str + arg_name: str + indices: Sequence[Any] + + def codegen(self, code: IndentedBuffer) -> None: + def codegen_list_tuple_access(basename, indices): # type: ignore[no-untyped-def] + if len(indices) > 0: + itype, i = indices[0] + if issubclass(itype, list): + return codegen_list_tuple_access(f"{basename}[{i}]", indices[1:]) + elif issubclass(itype, tuple): + # cpp wrapper code needs to use std::get<> to access a tuple + tuple_access = self.wrapper.codegen_tuple_access( + basename, self.result_name, str(i) + ) + return codegen_list_tuple_access(tuple_access, indices[1:]) + elif issubclass(itype, dict): + return codegen_list_tuple_access(f"{basename}['{i}']", indices[1:]) + else: + raise AssertionError("non supported index type: ", itype) + else: + return basename + + value = codegen_list_tuple_access(self.arg_name, self.indices) + code.writeline( + f"{self.wrapper.declare}{self.result_name} = {value}{self.wrapper.ending}" + ) + + def codegen_fx(self, converter: FxConverter) -> FxConversionFunc: + return converter._generate_multi_output + + +@dataclasses.dataclass +class IndexPutFallbackLine(WrapperLine): + wrapper: PythonWrapperCodegen + node: ir.IndexPutFallback + indices: list[ir.IRNode | None] + + def codegen(self, code: IndentedBuffer) -> None: + node = self.node + assert ir.is_node_sequence(node.inputs) + (x, values) = (t.codegen_reference() for t in node.inputs[:2]) + indices = [ + idx.codegen_reference() if idx else self.wrapper.none_str + for idx in self.indices + ] + + self.wrapper._generate_index_put_fallback( + node.get_kernel_name(), x, indices, values, *node.codegen_const_args() + ) + + def codegen_fx(self, converter: FxConverter) -> FxConversionFunc: + return converter._generate_index_put_fallback + + +@dataclasses.dataclass +class ScatterFallbackLine(WrapperLine): + wrapper: PythonWrapperCodegen + node: ir.ScatterFallback + + def codegen(self, code: IndentedBuffer) -> None: + node = self.node + assert ir.is_node_sequence(node.inputs) + if node.src_is_tensor: + (x, index, src) = (t.codegen_reference() for t in node.inputs) + else: + (x, index) = (t.codegen_reference() for t in node.inputs) + src = node.constant_args[1] + device = d.type if (d := node.get_device()) else V.graph.device_type + self.wrapper._generate_scatter_fallback( + x, + [x, node.constant_args[0], index, src], + node.cpp_kernel_name, + node.python_kernel_name, + node.src_is_tensor, + node.kwargs["reduce"], + node.codegen_kwargs(), + device, + ) + + def codegen_fx(self, converter: FxConverter) -> FxConversionFunc: + return converter._generate_scatter_fallback + + +@dataclasses.dataclass +class SymbolicCallArgLine(WrapperLine): + wrapper: PythonWrapperCodegen + arg: SymbolicCallArg + graph: GraphLowering + + def codegen(self, code: IndentedBuffer) -> None: + self.wrapper._generate_symbolic_call_arg_helper(self.arg, self.graph) + + def codegen_fx(self, converter: FxConverter) -> FxConversionFunc: + return converter._generate_symbolic_call_arg + + +@dataclasses.dataclass +class UnbackedSymbolDefsLine(WrapperLine): + wrapper: PythonWrapperCodegen + output_name: str + outputs: Any + unbacked_bindings: dict[sympy.Symbol, pytree.KeyPath] | None + + def codegen(self, code: IndentedBuffer) -> None: + self.wrapper._codegen_unbacked_symbol_defs_for_outputs( + self.output_name, self.outputs, self.unbacked_bindings + ) + + def codegen_fx(self, converter: FxConverter) -> FxConversionFunc: + return converter._generate_unbacked_symbol_defs + + +@dataclasses.dataclass +class AssertSizeStrideLine(WrapperLine): + name: str + size: str + stride: str + + def codegen(self, code: IndentedBuffer) -> None: + code.writeline(f"assert_size_stride({self.name}, {self.size}, {self.stride})") + + @staticmethod + def codegen_fx(converter: FxConverter) -> FxConversionFunc: + return converter._generate_assert_size_stride + + +BufferName = str +Line = MemoryPlanningLine | LineContext + + +class PythonWrapperCodegen(CodeGen): + """ + Generate outer wrapper in Python that calls the kernels. + """ + + supports_caching: bool = True # Whether the output code is cacheable. + + def __init__(self): + super().__init__() + self._pending_input_asserts: dict[str, tuple[str, str]] = {} + self._pending_alignment_copies: OrderedSet[str] = OrderedSet() + self._names_iter: Iterator[int] = count() + self.args_to_buffers: dict[ + str, None | ir.TensorBox | ir.Buffer | ir.TorchBindObject + ] = {} + self.imports = IndentedBuffer() + self.header = IndentedBuffer() + self.prefix = IndentedBuffer() + self.suffix = IndentedBuffer() + self.kernel_declarations = IndentedBuffer() + self.wrapper_call = IndentedBuffer() + self.kernel_autotune_defs = IndentedBuffer() + self.kernel_autotune_calls = IndentedBuffer() + self.subgraph_definitions = IndentedBuffer() + self.kernel_autotune_names: OrderedSet[str] = OrderedSet() + # Map key is the kernel argument name; value is a tuple of the resulting example + # tensor name with the kernel where that tensor was most recently used. + self.kernel_autotune_example_args: dict[str, tuple[str, str]] = {} + self.kernel_autotune_tmp_arg_idx: int = 0 + # If the generated source code is exactly the same, reuse the + # pre-existing kernel for it + self.src_to_kernel: dict[str, str] = {} + self.kernel_numel_expr: OrderedSet[tuple[str, GraphLowering]] = OrderedSet() + self.lines: list[Line] = [] + self.declare = "" + self.declare_maybe_reference = "" + self.ending = "" + self.comment = "#" + self.none_str = "None" + self.move_begin = "std::move(" if V.graph.cpp_wrapper else "" + self.move_end = ")" if V.graph.cpp_wrapper else "" + self.last_seen_device_guard_index: int | None = None + self.supports_intermediate_hooks = True + self.user_defined_kernel_cache: dict[ + tuple[Any, ...], tuple[str, Any, dict[str, Any]] + ] = {} + self.unbacked_symbol_decls: OrderedSet[str] = ( + OrderedSet() + ) # str of sympy.Symbol + self.computed_sizes: OrderedSet[sympy.Symbol] = OrderedSet() + self.launcher_fn_name = None + # This function can be overridden to change the launcher name + self.set_launcher_fn_name() + + # this is used for tracking which GraphLowering instance---parent graph + # or (nested) subgraph---is currently codegened; the primary use case is + # including the graph instance into a cache key to avoid cross-graph + # caching during lowering of nested subgraphs + self.codegened_graph_stack = [] + self.computed_sizes_stack = [] + + self.write_header() + + if not is_codegen_graph_partition_subgraph(self): + # See [Note: Removed Graph Partition Arguments] + self.write_prefix() + + self.write_kernel_autotune_defs_header() + + if not V.graph.aot_mode: + for name, hashed in V.graph.constant_reprs.items(): + # include a hash so our code cache puts different constants into different files + self.write_constant(name, hashed) + + self.allocated = OrderedSet[BufferName]() + self.freed = OrderedSet[BufferName]() + + # maps from reusing buffer to reused buffer + self.reuses: dict[BufferName, BufferName] = {} + + self.write_get_raw_stream = functools.lru_cache(None)( # type: ignore[assignment] + self.write_get_raw_stream + ) + + @functools.cache + def add_import_once(line: str) -> None: + self.imports.writeline(line) + if config.triton.autotune_at_compile_time: + self.kernel_autotune_calls.writeline(line) + + self.add_import_once = add_import_once + self._metas: dict[str, str] = {} + self._meta_vars: OrderedSet[str] = OrderedSet() + self.multi_kernel_state = MultiKernelState() + self.already_codegened_subgraphs: OrderedSet[str] = OrderedSet() + self.allocated_workspaces: dict[str, Any] = {} + + # intermediate tensor value printing utility + self.debug_printer = DebugPrinterManager( + debug_printer_level=config.aot_inductor.debug_intermediate_value_printer, + use_array_ref=config.aot_inductor.allow_stack_allocation, + ) + + # Additional files that are dependent to the wrapper (ex. cubin files) + self.additional_files = [] + + @staticmethod + def create( + is_subgraph: bool, + subgraph_name: str | None, + parent_wrapper: PythonWrapperCodegen | None, + partition_signatures: ir.GraphPartitionSignature | None = None, + ): + if is_subgraph: + assert subgraph_name is not None + assert parent_wrapper is not None + return SubgraphPythonWrapperCodegen( + subgraph_name, parent_wrapper, partition_signatures + ) + return PythonWrapperCodegen() + + def set_launcher_fn_name(self) -> None: + # pyrefly: ignore [bad-assignment] + self.launcher_fn_name = "call" + + def write_constant(self, name: str, hashed: str) -> None: + self.header.writeline(f"{name} = None # {hashed}") + + def write_header(self) -> None: + """Write the header section of the generated Python wrapper code.""" + context = torch._guards.TracingContext.try_get() + aot_config_comment = "" + if context is not None and context.aot_graph_name is not None: + aot_config_comment = f"# AOT ID: {context.aot_graph_name}" + inductor_debug_utils = "" + if int(config.aot_inductor.debug_intermediate_value_printer) > 0: + inductor_debug_utils = "from torch._inductor.codegen.debug_utils import _print_debugging_tensor_value_info" + elif torch._inductor.config.test_configs.track_memory_lifecycle: + inductor_debug_utils = "from torch._inductor.runtime.debug_utils import tracked_empty_strided\n" + + self.imports.splice( + f""" + {aot_config_comment} + from ctypes import c_void_p, c_long, c_int + import torch + import math + import random + import os + import tempfile + from math import inf, nan + from cmath import nanj + from torch._inductor.hooks import run_intermediate_hooks + from torch._inductor.utils import maybe_profile + from torch._inductor.codegen.memory_planning import _align as align + from torch import device, empty_strided + from {async_compile.__name__} import AsyncCompile + from torch._inductor.select_algorithm import extern_kernels + {inductor_debug_utils} + """, + strip=True, + ) + self.header.splice( + """ + aten = torch.ops.aten + inductor_ops = torch.ops.inductor + _quantized = torch.ops._quantized + assert_size_stride = torch._C._dynamo.guards.assert_size_stride + assert_alignment = torch._C._dynamo.guards.assert_alignment + empty_strided_cpu = torch._C._dynamo.guards._empty_strided_cpu + empty_strided_cpu_pinned = torch._C._dynamo.guards._empty_strided_cpu_pinned + empty_strided_cuda = torch._C._dynamo.guards._empty_strided_cuda + empty_strided_xpu = torch._C._dynamo.guards._empty_strided_xpu + empty_strided_mtia = torch._C._dynamo.guards._empty_strided_mtia + reinterpret_tensor = torch._C._dynamo.guards._reinterpret_tensor + alloc_from_pool = torch.ops.inductor._alloc_from_pool + async_compile = AsyncCompile() + """, + strip=True, + ) + try: + # Only add empty_strided_p2p() if distributed and SymmetricMemory + # is available + from torch._C._distributed_c10d import _SymmetricMemory # noqa: F401 + + self.header.splice( + """ + empty_strided_p2p = torch._C._distributed_c10d._SymmetricMemory.empty_strided_p2p + """, + strip=True, + ) + except (AttributeError, ImportError): + pass + if config.annotate_training: + self.header.writeline("from torch.cuda import nvtx") + if config.triton.proton_profiling: + self.header.writeline("import triton.profiler as proton") + self.header.writeline("import triton.profiler.language as pl") + self.header.writeline( + "from triton.profiler.hooks import HookManager as _ProtonHookManager" + ) + self.header.writeline("import triton") + self.header.writeline("import atexit") + self.header.writeline("import os") + self.header.writeline( + "triton.set_allocator(lambda size, align, stream: " + "torch.empty(size, dtype=torch.uint8, device='cuda'))" + ) + output_dir = config.triton.proton_output_dir or os.path.join( + get_debug_dir(), "proton" + ) + self.header.writeline(f'os.makedirs("{output_dir}", exist_ok=True)') + proton_name = f'os.path.join("{output_dir}", "inductor")' + trace_path = f'os.path.join("{output_dir}", "inductor.chrome_trace")' + group_by_sm = config.triton.proton_group_by_sm + split_invocations = config.triton.proton_split_invocations + per_cta_occupancy = config.triton.proton_per_cta_occupancy + self.header.writeline( + "from torch._inductor.runtime.proton_utils import process_proton_trace as _proton_process_trace" + ) + self.header.splice( + f""" + def _proton_finalize_and_postprocess(): + proton.finalize() + _trace_path = {trace_path} + if os.path.exists(_trace_path): + _proton_process_trace( + _trace_path, + group_by_sm={group_by_sm}, + split_invocations={split_invocations}, + per_cta_occupancy={per_cta_occupancy}, + ) + """ + ) + # Start proton before kernel compilation (instrumentation backend needs to hook JIT) + self.header.writeline( + "if not _ProtonHookManager.active_hooks: " + f'proton.start({proton_name}, backend="instrumentation", data="trace"); ' + "atexit.register(_proton_finalize_and_postprocess)" + ) + self.header.writeline('pl.enable_semantic("triton")') + + def include_extra_header(self, header: str): + pass + + def write_kernel_autotune_defs_header(self) -> None: + self.kernel_autotune_defs.splice( + f""" + import torch + from torch._dynamo.testing import rand_strided + from torch._dynamo.utils import preserve_rng_state + from torch._inductor.select_algorithm import AlgorithmSelectorCache + from {async_compile.__name__} import AsyncCompile + + async_compile = AsyncCompile() + generate_example_value = AlgorithmSelectorCache.generate_example_value + empty_strided_cuda = torch._C._dynamo.guards._empty_strided_cuda + empty_strided_xpu = torch._C._dynamo.guards._empty_strided_xpu + """ + ) + + try: + from torch._C import _cuda_getCurrentRawStream # noqa: F401 + + self.kernel_autotune_defs.splice( + """ + get_raw_stream = torch._C._cuda_getCurrentRawStream + """, + strip=True, + ) + except (ImportError, AttributeError): + pass + + @cache_on_self + def write_triton_header_once(self) -> None: + import_str = f""" + import triton + import triton.language as tl + from {triton_heuristics.__name__} import start_graph, end_graph + """ + if config.triton.autotune_at_compile_time: + self.kernel_autotune_calls.splice(import_str) + self.kernel_autotune_calls.writeline( + V.graph.device_ops.import_get_raw_stream_as("get_raw_stream") + ) + if not V.graph.cpp_wrapper: + self.imports.splice(import_str, strip=True) + self.imports.writeline( + V.graph.device_ops.import_get_raw_stream_as("get_raw_stream") + ) + + def write_get_raw_stream_header(self) -> None: + import_get_raw_stream_str = V.graph.device_ops.import_get_raw_stream_as( + "get_raw_stream" + ) + if config.triton.autotune_at_compile_time: + if not self.kernel_autotune_calls.contains(import_get_raw_stream_str): + self.kernel_autotune_calls.writeline(import_get_raw_stream_str) + if not V.graph.cpp_wrapper: + if not self.imports.contains(import_get_raw_stream_str): + self.imports.writeline(import_get_raw_stream_str) + + @cache_on_self + def write_get_raw_stream_header_once(self) -> None: + self.write_get_raw_stream_header() + + def add_meta_once(self, meta: TritonMetaParams) -> str: + # pyrefly: ignore [bad-assignment] + meta = repr(meta) + if meta not in self._metas: + var = f"meta{len(self._metas)}" + # pyrefly: ignore [unsupported-operation] + self._metas[meta] = var + self.header.writeline(f"{var} = {meta}") + if config.triton.autotune_at_compile_time: + self.kernel_autotune_calls.writeline(f"{var} = {meta}") + self._meta_vars.add(var) + # pyrefly: ignore [bad-index, index-error] + return self._metas[meta] + + @cache_on_self + def get_output_refs(self) -> list[str]: + return [ + x.codegen_reference(self.wrapper_call) for x in self.get_graph_outputs() + ] + + def mark_output_type(self) -> None: + return + + def get_graph_inputs( + self, + ) -> dict[str, ir.TensorBox | ir.TorchBindObject | sympy.Expr]: + return V.graph.graph_inputs + + def get_graph_outputs(self) -> list[IRNode]: + return V.graph.graph_outputs + + def codegen_input_size_asserts(self) -> None: + for name, buf in self.get_graph_inputs().items(): + if isinstance( + buf, + ( + sympy.Basic, + ir.TorchBindObject, + ir.GeneratorState, + ir.OpaqueObjectState, + ), + ): + continue + + # a graph partition may take an IRNode output from a previous partition + if name not in V.graph.graph_input_names: + continue + + # comparing strides for 0 size tensor is tricky. Ignore them for now. + if sympy_product(buf.get_size()) == 0: + continue + size = self.codegen_python_shape_tuple(buf.get_size()) + stride = self.codegen_python_shape_tuple(buf.get_stride()) + self._pending_input_asserts[name] = (size, stride) + + def codegen_input_nan_asserts(self) -> None: + self.prefix.writeline("# make sure graph inputs are not nan/inf") + for name, buf in self.get_graph_inputs().items(): + if isinstance(buf, (sympy.Basic, ir.TorchBindObject)): + continue + line = f"assert not {name}.isnan().any().item()" + self.prefix.writeline(line) + line = f"assert not {name}.isinf().any().item()" + self.prefix.writeline(line) + + def write_async_compile_wait(self) -> None: + self.prefix.splice( + """ + + async_compile.wait(globals()) + del async_compile + """ + ) + + def write_args(self, input_names: list[str]): + lhs = ", ".join(input_names) + if len(input_names) == 1: + lhs += "," + self.prefix.writeline(f"{lhs} = args") + self.prefix.writeline("args.clear()") + + def write_launcher_fn_call_get_indent(self) -> int: + if config.graph_partition: + self.prefix.splice( + """ + class Runner: + def __init__(self, partitions): + self.partitions = partitions + + def recursively_apply_fns(self, fns): + new_callables = [] + for fn, c in zip(fns, self.partitions): + new_callables.append(fn(c)) + self.partitions = new_callables + + def call(self, args): + """ + ) + prefix_indent = 2 + else: + self.prefix.splice( + f""" + def {self.launcher_fn_name}(args): + """ + ) + prefix_indent = 1 + + return prefix_indent + + def get_graph_input_names(self) -> list[str]: + return V.graph.graph_input_names + + def write_prefix(self) -> None: + assert self.launcher_fn_name is not None + self.write_async_compile_wait() + prefix_indent = self.write_launcher_fn_call_get_indent() + + with self.prefix.indent(prefix_indent): + if config.triton.debug_sync_graph: + self.prefix.writeline(V.graph.device_ops.synchronize()) + phase = V.graph.get_training_phase() + if config.annotate_training: + self.prefix.writeline( + f"training_annotation = nvtx._device_range_start('{phase}')" + ) + + if graph_input_names := self.get_graph_input_names(): + self.write_args(graph_input_names) + + self.codegen_inputs() + + # avoid duplicating asserts for both partition functions and + # the call function when using cudagraph partition + if not ( + is_using_cudagraph_partition() + and (not is_codegen_graph_partition_subgraph(self)) + ): + self.codegen_input_size_and_nan_asserts() + + def codegen_input_size_and_nan_asserts(self) -> None: + if config.size_asserts: + self.codegen_input_size_asserts() + if config.nan_asserts: + self.codegen_input_nan_asserts() + + # Input size/stride assertions are deferred from the top of call() to just + # before the first kernel that uses each input. This avoids a block of N + # sequential assert calls (~1 us each) on the critical path before the first + # GPU kernel launch. Called from the scheduler codegen loop. + def codegen_deferred_input_asserts(self, input_names: Iterable[str]) -> None: + for name in input_names: + if name in self._pending_input_asserts: + size, stride = self._pending_input_asserts.pop(name) + self.writeline(AssertSizeStrideLine(name, size, stride)) + + def register_alignment_check_inputs(self) -> None: + """Populate pending alignment copies for non-mutated inputs. + Called from the scheduler after mutated_input_idxs is computed.""" + if V.graph.cpp_wrapper: + return + inputs_to_check = V.graph.inputs_to_check + if not inputs_to_check: + return + # Mutated inputs are handled separately by the runtime wrapper, + # which needs to copy back the mutation after the call. + mutated_idxs = OrderedSet(V.graph.mutated_input_idxs) + for idx in inputs_to_check: + if idx not in mutated_idxs: + name = V.graph.graph_input_names[idx] + self._pending_alignment_copies.add(name) + if self._pending_alignment_copies: + V.graph._defers_input_alignment = True + self.imports.writeline( + "from torch._C._dynamo.guards import copy_misaligned" + ) + + def codegen_deferred_alignment_copies(self, input_names: Iterable[str]) -> None: + """Emit alignment check + clone just before the first kernel + that reads each input, hiding the cost behind GPU execution.""" + if V.graph.cpp_wrapper: + return + for name in input_names: + if name in self._pending_alignment_copies: + self._pending_alignment_copies.discard(name) + self.writeline(f"{name} = copy_misaligned({name})") + + # this function (and below) takes the graph name as input so + # that stream caching happens per graph instance. this + # is important for nested subgraph codegening. + def write_get_raw_stream(self, device_idx: int, graph_name: str) -> str: + self.write_get_raw_stream_header() + name = get_raw_stream_name(device_idx) + if config.triton.autotune_at_compile_time: + self.kernel_autotune_calls.writeline( + f"{name} = get_raw_stream({device_idx})" + ) + if V.graph.cpp_wrapper: + # For cpp wrapper, no need to continue codegen for the main body + return name + self.writeline(f"{name} = get_raw_stream({device_idx})") + return name + + def get_codegened_graph(self): + return self.codegened_graph_stack[-1] + + def push_codegened_graph(self, graph): + self.codegened_graph_stack.append(graph) + + def pop_codegened_graph(self): + return self.codegened_graph_stack.pop() + + def push_computed_sizes(self, computed_sizes): + from copy import deepcopy + + return self.computed_sizes_stack.append(deepcopy(computed_sizes)) + + def pop_computed_sizes(self): + return self.computed_sizes_stack.pop() + + def next_kernel_suffix(self) -> str: + return f"{next(self._names_iter)}" + + def codegen_device_guard_enter( + self, + device_idx: int, + num_streams: int = 1, + stream_idx_to_user_obj_idx: dict[int, int] | None = None, + ) -> None: + if num_streams > 1: + assert stream_idx_to_user_obj_idx is not None + import_line = ( + "from torch._dynamo.graph_bytecode_inputs import " + "get_external_object_by_index" + ) + if not self.imports.contains(import_line): + self.imports.writeline(import_line) + self.writeline( + EnterDeviceContextManagerWithStreamInfoLine( + device_idx, + self.last_seen_device_guard_index, + num_streams, + stream_idx_to_user_obj_idx, + ), + ) + else: + self.writeline( + EnterDeviceContextManagerLine( + device_idx, self.last_seen_device_guard_index + ) + ) + if config.triton.autotune_at_compile_time: + # mimic logic of EnterDeviceContextManagerLine.codegen for the autotune code block + self.write_triton_header_once() + self.kernel_autotune_calls.writeline( + f"with {V.graph.device_ops.device_guard(device_idx)}:" + ) + self.kernel_autotune_calls.do_indent() + if is_codegen_graph_partition_subgraph(self): + # Need get_raw_stream for subgraph + self.write_get_raw_stream_header() + self.kernel_autotune_calls.writeline( + f"{get_raw_stream_name(device_idx)} = get_raw_stream({device_idx})" + ) + self.last_seen_device_guard_index = device_idx + self._num_streams: int = num_streams + + def codegen_device_guard_exit(self) -> None: + if hasattr(self, "_num_streams") and self._num_streams > 1: + self.writeline( + ExitDeviceContextManagerWithStreamInfoLine(self._num_streams) + ) + else: + self.writeline(ExitDeviceContextManagerLine()) + if config.triton.autotune_at_compile_time: + self.kernel_autotune_calls.do_unindent() + + def codegen_cuda_stream_enter( + self, + stream_idx: int, + ) -> EnterCudaStreamContextLine: + """Generate data structure for entering a CUDA Stream context. + + Args: + stream_idx: The index number of the entering CUDA Stream context. + + Raises: + ValueError: If this function is called while the previous stream context isn't exited. + """ + # pyre-fixme[16]: scheduler.current_stream_name added in scheduler commit + if (current_stream_name := V.graph.scheduler.current_stream_name) is not None: + raise ValueError( + f"Nested stream context switching: {current_stream_name} -> " + f"{get_stream_name(stream_idx)}", + ) + ctx_entrance = EnterCudaStreamContextLine(stream_idx=stream_idx) + self.writeline(ctx_entrance) + return ctx_entrance + + def codegen_cuda_stream_exit(self) -> None: + """Generate data structure for exiting a CUDA Stream context.""" + self.writeline(ExitCudaStreamContextLine()) + + def generate_return(self, output_refs: list[str]) -> None: + if output_refs: + if config.nan_asserts: + self.wrapper_call.writeline( + "return_vars = (" + ", ".join(output_refs) + ", )" + ) + self.wrapper_call.writeline("for var in return_vars:") + self.wrapper_call.do_indent() + self.wrapper_call.writeline("if isinstance(var, torch.Tensor):") + self.wrapper_call.do_indent() + self.wrapper_call.writeline("assert not var.isnan().any().item()") + self.wrapper_call.writeline("assert not var.isinf().any().item()") + self.wrapper_call.do_unindent(2) + + self.wrapper_call.writeline("return (" + ", ".join(output_refs) + ", )") + else: + self.wrapper_call.writeline("return ()") + + def generate_before_suffix(self, result: IndentedBuffer) -> None: + return + + def generate_after_suffix(self, result: IndentedBuffer) -> None: + if config.graph_partition: + all_partition_name_list = ", ".join(self.all_partition_names) + ( + "," if len(self.all_partition_names) == 1 else "" + ) + + result.splice( + f""" + runner = Runner(partitions=[{all_partition_name_list}]) + call = runner.call + recursively_apply_fns = runner.recursively_apply_fns + """ + ) + + def generate_end(self, result: IndentedBuffer) -> None: + return + + def generate_fallback_kernel(self, node: ir.FallbackKernel) -> None: + # Check if this op has a custom codegen implementation + op_name = node.python_kernel_name + if op_name is not None and op_name in CUSTOM_EXTERN_KERNEL_CODEGEN: + custom_codegen = CUSTOM_EXTERN_KERNEL_CODEGEN[op_name].python + if custom_codegen is not None: + custom_codegen(node, self.writeline) + return + self.writeline(ExternKernelAllocLine(self, node)) + + def generate_extern_kernel_multi_out(self, node: ir.ExternKernelMultiOut) -> None: + """Generate .out() call with pre-allocated output buffers.""" + for out_node in node.out_variant_output_nodes: + self.codegen_allocation(out_node) + self.writeline(ExternKernelMultiOutLine(self, node)) + + def generate_extern_kernel_alloc(self, node: ir.ExternKernelAlloc): + node.codegen_comment(self) + self.writeline(ExternKernelAllocLine(self, node)) + if isinstance(node.layout, ir.Layout): + node.codegen_size_asserts(self) + + def _generate_extern_kernel_alloc_helper(self, extern_kernel, args): + # If it's a NoneLayout then the extern_kernel should essentially be + # treated as if it doesn't return anything + no_return = isinstance(extern_kernel.layout, ir.NoneLayout) + output_name = extern_kernel.get_name() + origin_node = extern_kernel.get_origin_node() + kernel_name = extern_kernel.get_kernel_name() + ending = self.ending + if config.memory_planning and "view_as_complex" in kernel_name: + # view operation fallbacks cause issues since inductor + # doesn't know the memory is still needed and might reuse it. + ending = f".clone(){ending}" + + if no_return: + self.writeline(f"{self.declare}{kernel_name}({', '.join(args)}){ending}") + else: + self.writeline( + f"{self.declare}{output_name} = {kernel_name}({', '.join(args)}){ending}" + ) + if ( + self.supports_intermediate_hooks + and config.generate_intermediate_hooks + and origin_node is not None + ): + counters["inductor"]["intermediate_hooks"] += 1 + self.writeline( + f"run_intermediate_hooks({origin_node.name!r}, {output_name})" + ) + + def generate_extern_kernel_out( + self, + node: ir.ExternKernelOut, + ) -> None: + node.codegen_comment(self) + self.writeline(ExternKernelOutLine(self, node)) + + def _generate_extern_kernel_out_helper( + self, + kernel: str, + out: str, + out_view: str | None, + args: list[str], + device: str, + stack_traces: OrderedSet[str] | None = None, + ) -> None: + # add debug printer code for triton kernel calls at (jit) inductor level + debug_printer_manager = V.graph.wrapper_code.debug_printer + debug_printer_manager.set_printer_args(args, kernel, None, None, "extern") + args.append(f"out={out_view if out_view else out}") + with debug_printer_manager: + self.writeline(f"{kernel}({', '.join(args)})") + + def _generate_tma_descriptor_call_experimental(self, desc, apply_size_hints=False): + dims = desc.dims + block_dims = desc.block_dims + if apply_size_hints: + dims = V.graph.sizevars.optimization_hint(dims) + block_dims = V.graph.sizevars.optimization_hints(block_dims) + + ptr = f"{desc.tensor.codegen_reference()}.data_ptr()" + # Explicitly call the Python version of val_to_arg_str + dims = ", ".join(PythonWrapperCodegen.val_to_arg_str(self, dim) for dim in dims) + block_dims = ", ".join( + PythonWrapperCodegen.val_to_arg_str(self, dim) for dim in block_dims + ) + element_size = PythonWrapperCodegen.val_to_arg_str(self, desc.element_size) + prefix = "triton.tools.experimental_descriptor" + fn = f"{prefix}.create_{desc.rank}d_tma_descriptor" + args = f"{ptr}, {dims}, {block_dims}, {element_size}" + call = f"{fn}({args})" + return call + + def _generate_tma_descriptor_call_stable(self, desc, apply_size_hints=False): + block_shape = desc.block_shape + if apply_size_hints: + block_shape = V.graph.sizevars.optimization_hints(block_shape) + + prefix = "triton.tools.tensor_descriptor.TensorDescriptor" + fn = f"{prefix}.from_tensor" + args = f"{desc.tensor.codegen_reference()}, {block_shape}" + call = f"{fn}({args})" + return call + + def _generate_tma_descriptor_call(self, desc, apply_size_hints=False): + if isinstance(desc, ir.TMADescriptorExperimental): + return self._generate_tma_descriptor_call_experimental( + desc, apply_size_hints + ) + else: + assert isinstance(desc, ir.TMADescriptorStable) + return self._generate_tma_descriptor_call_stable(desc, apply_size_hints) + + def generate_tma_descriptor(self, desc): + call = self._generate_tma_descriptor_call(desc) + line = f"{desc.name} = {call}{self.ending}" + self.writeline(line) + + def generate_scatter_fallback(self, node: ir.ScatterFallback): + self.writeline(ScatterFallbackLine(self, node)) + + def _generate_scatter_fallback( + self, + output, + inputs, + cpp_kernel_name, + python_kernel_name, + src_is_tensor, + reduce, + kwargs, + device, + ): + line = f"{python_kernel_name}({','.join(map(str, inputs))}" + if python_kernel_name.startswith("aten.scatter_reduce"): + line += ", ".join([""] + kwargs) + else: + if reduce: + line += f", reduce={repr(reduce)}" + line += ")" + self.writeline(line) + + def generate_index_put_fallback(self, node: ir.IndexPutFallback) -> None: + # Collect index tensors into a list. + indices: list[ir.IRNode | None] = [] + valid_indices = node.inputs[2:] + iter_valid_indices = iter(valid_indices) + for i, _ in enumerate(node.indices): + if node.indices[i] is not None: + index = next(iter_valid_indices) + assert isinstance(index, ir.IRNode) + indices.append(index) + else: + indices.append(None) + + self.writeline(IndexPutFallbackLine(self, node, indices)) + + def _generate_index_put_fallback(self, kernel, x, indices, values, accumulate): + indices_str = f"[{', '.join(indices)}]" + args = [x, indices_str, values, accumulate] + self.writeline(self.wrap_kernel_call(kernel, args)) + + def generate_fallback_kernel_with_runtime_lookup( + self, + buf_name: str, + python_kernel_name: str, + get_args: Callable[[], Sequence[str]], + op_overload: torch._ops.OpOverload | torch._ops.HigherOrderOperator, + raw_args: Sequence[Any], + outputs: Sequence[ir.Buffer], + ) -> None: + self.writeline(f"{buf_name} = {python_kernel_name}({', '.join(get_args())})") + + def generate(self, is_inference): + with dynamo_timed("PythonWrapperCodegen.generate"): + return self._generate(is_inference) + + def get_wrapper_call_indent(self) -> int: + if config.graph_partition: + return 2 + else: + return 1 + + @contextlib.contextmanager + def set_writeline(self, new: Callable[..., None]) -> Iterator[Callable[..., None]]: + old = self.writeline + try: + self.writeline = new # type: ignore[method-assign] + yield new + finally: + self.writeline = old # type: ignore[method-assign] + + def _write_multi_kernel_defs(self) -> None: + kernel_defs = self.multi_kernel_state.kernel_defs + if config.triton.autotune_at_compile_time: + self.kernel_autotune_defs.splice(kernel_defs) + else: + self.header.splice(kernel_defs) + + def _generate(self, is_inference): + if config.profile_bandwidth: + self.write_triton_header_once() + + with contextlib.ExitStack() as stack: + stack.enter_context(self.wrapper_call.indent()) + if config.profiler_mark_wrapper_call: + self.generate_profiler_mark_wrapper_call(stack) + if config.profile_bandwidth: + self.generate_start_graph() + + self.run_wrapper_ir_passes(is_inference) + + if config.triton.store_cubin and not config.triton.autotune_at_compile_time: + self.generate_reset_kernel_saved_flags() + + # At this point, we shouldn't generate any new memory planning lines. + # Override writeline to point at the wrapper call, in case it gets called. + with self.set_writeline(self.wrapper_call.writeline): + for line in self.lines: + if isinstance(line, WrapperLine): + # pyrefly: ignore [missing-attribute] + line.codegen(self.wrapper_call) + else: + self.wrapper_call.writeline(line) + + self._write_multi_kernel_defs() + + output_refs = self.get_output_refs() + self.mark_output_type() + if config.triton.debug_sync_graph: + self.wrapper_call.writeline(V.graph.device_ops.synchronize()) + + if config.profile_bandwidth: + self.generate_end_graph() + + if config.triton.proton_profiling: + self.generate_proton_finalize() + + if config.triton.store_cubin and not config.triton.autotune_at_compile_time: + self.generate_save_uncompiled_kernels() + + if config.triton.autotune_at_compile_time: + self.generate_and_run_autotune_block() + + # cpp_wrapper currently doesn't support nvtx + if config.annotate_training and not config.cpp_wrapper: + self.wrapper_call.writeline( + "nvtx._device_range_end(training_annotation)" + ) + self.generate_return(output_refs) + + # Assemble the final code from sections. + result = IndentedBuffer() + result.splice(self.imports) + result.writeline("") + result.splice(self.header) + # We do not want the cpp header for intermediate const graph. Headers would be + # rendered by the main module instead. + if V.graph.aot_mode and V.graph.cpp_wrapper and V.graph.is_const_graph: + result = IndentedBuffer() + + # Add subgraph definitions to the result + result.splice(self.subgraph_definitions) + self.finalize_prefix() + result.splice(self.prefix) + + wrapper_call_indent = self.get_wrapper_call_indent() + + with result.indent(wrapper_call_indent): + result.splice(self.wrapper_call) + + self.generate_before_suffix(result) + result.splice(self.suffix) + self.generate_after_suffix(result) + + self.generate_end(result) + + self.add_benchmark_harness(result) + + return ( + result.getvaluewithlinemap(), + self.kernel_declarations.getvaluewithlinemap(), + ) + + def generate_and_run_autotune_block(self): + """ + Compose self.kernel_autotune_defs and self.kernel_autotune_calls into a single block of + code and execute it to trigger Triton kernel compilation and auto-tuning + """ + self.kernel_autotune_defs.splice( + """ + async_compile.wait(globals()) + del async_compile + """ + ) + scope = {} # type: ignore[var-annotated] + if config.triton.autotune_at_compile_time and V.graph.autotuning_inputs: + scope = { + self.get_autotuning_input_name(idx): v # type: ignore[attr-defined] + for idx, v in enumerate(V.graph.autotuning_inputs) + } + tuning_code = ( + self.kernel_autotune_defs.getvalue() + + "\n" + + self.kernel_autotune_calls.getvalue() + ) + if output_code_log.level == logging.DEBUG: + # Save the autotuning code block into a file + # Create a temporary file + with tempfile.NamedTemporaryFile( + dir=cache_dir(), suffix=".py", delete=False + ) as f: + f.write(tuning_code.encode("utf-8")) + file_path = f.name + output_code_log.debug( + "Auto-tuning code written to %s", + file_path, + ) + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "inductor_autotune_at_compile_time_code", + "encoding": "string", + }, + payload_fn=lambda: tuning_code, + ) + # Execute the code to autotune kernels + try: + exec(tuning_code, scope) + except Exception as e: + raise RuntimeError(f"Failed to run autotuning code block: {e}") from e + + def memory_plan(self): + from .memory_planning import MemoryPlanner + + self.lines = MemoryPlanner(self).plan(self.lines) + + def memory_plan_reuse(self): + outputs = self.get_graph_outputs() + out_names = V.graph._get_output_names(outputs) + + while ( + self.lines + and isinstance(self.lines[-1], MemoryPlanningLine) + # TODO: this seems legit, NullLine has no node + and self.lines[-1].node.name not in out_names # type: ignore[attr-defined] + ): + # these lines will be pointless + self.lines.pop() + + # codegen allocations in two passes + planning_states = [MemoryPlanningState()] + past_planning_states = [] + for i in range(len(self.lines)): + line = self.lines[i] + if isinstance(line, MemoryPlanningLine): + self.lines[i] = line.plan(planning_states[-1]) + elif isinstance(line, EnterSubgraphLine): + planning_states.append(MemoryPlanningState()) + elif isinstance(line, ExitSubgraphLine): + past_planning_states.append(planning_states.pop()) + past_planning_states.append(planning_states.pop()) + assert len(planning_states) == 0 + + # conservatively use the sum of all allocated buffer sizes + # in potentially nested scopes as the total allocated size + # FIXME(rec): not used + _total_allocated_buffer_size = sum( + s.total_allocated_buffer_size for s in past_planning_states + ) + + def run_wrapper_ir_passes(self, is_inference: bool): + # We disable planning during training because it presently increases peak memory consumption. + if is_inference and config.memory_planning: + self.memory_plan() + else: + if config.allow_buffer_reuse: + self.estimate_peak = EfficientPeakEstimate() + self.memory_plan_reuse() + + def codegen_input_symbol_assignment( + self, + name: str, + value: ir.TensorBox, + bound_vars: OrderedSet[sympy.Symbol], + ): + code = self.prefix + + @functools.cache + def sizeof(name): + code.writeline(f"{name}_size = {name}.size()") + return f"{name}_size" + + @functools.cache + def strideof(name): + code.writeline(f"{name}_stride = {name}.stride()") + return f"{name}_stride" + + if isinstance(value, sympy.Expr): + if not isinstance(value, sympy.Symbol) or value in bound_vars: + return + code.writeline(f"{value} = {name}") + bound_vars.add(value) + elif isinstance(value, ir.TensorBox): + for dim, size in enumerate(value.get_size()): + if isinstance(size, sympy.Symbol) and size not in bound_vars: + code.writeline(f"{size} = {sizeof(name)}[{dim}]") + bound_vars.add(size) + for dim, stride in enumerate(value.get_stride()): + if isinstance(stride, sympy.Symbol) and stride not in bound_vars: + code.writeline(f"{stride} = {strideof(name)}[{dim}]") + bound_vars.add(stride) + elif isinstance( + value, (ir.TorchBindObject, ir.GeneratorState, ir.OpaqueObjectState) + ): + return + else: + if torch._inductor.config.graph_partition: + pass + else: + raise AssertionError(f"Unknown value type: {type(value)}") + + def codegen_inputs(self): + """Assign all symbolic shapes to locals""" + bound_vars = OrderedSet[sympy.Symbol]() + # There is a subtle case in the cpp wrapper codegen which requires generating + # symbol inputs first followed by non-symbol ones. + # + # When a dynamic size constraint specified at the Export time is an expression, + # we need to solve that expression to proper define a symbol in cpp. Thus we + # are enforcing this iterating order here to make sure all plain size symbols + # are defined first. + graph_inputs = self.get_graph_inputs() + inputs = [ + (k, v) for k, v in graph_inputs.items() if isinstance(v, sympy.Symbol) + ] + [(k, v) for k, v in graph_inputs.items() if not isinstance(v, sympy.Symbol)] + for name, value in inputs: + self.codegen_input_symbol_assignment(name, value, bound_vars) + + def _verify_input_symbol_assignment( + value: ir.TensorBox, + bound_vars: OrderedSet[sympy.Symbol], + ): + for expr in chain.from_iterable([value.get_size(), value.get_stride()]): + if not isinstance(expr, Expr) or isinstance(expr, sympy.Symbol): + continue + + undefined_symbols = [ + sym for sym in expr.free_symbols if sym not in bound_vars + ] + if len(undefined_symbols) > 0: + raise AssertionError( + f"For {expr}, expected {undefined_symbols} to have been codegen-ed." + ) + + # For inputs with size/strides which contain sympy expressions, we can + # encounter symbols that weren't defined yet. Now, let's check each + # symbol is defined. + for _, value in inputs: + if not isinstance(value, ir.TensorBox): + continue + _verify_input_symbol_assignment(value, bound_vars) + + def ensure_size_computed(self, sym: sympy.Symbol): + if isinstance(sym, sympy.Symbol) and symbol_is_type(sym, SymT.PRECOMPUTED_SIZE): + if sym in self.computed_sizes: + return + self.computed_sizes.add(sym) + expr = V.graph.sizevars.inv_precomputed_replacements[sym] + arg = SymbolicCallArg(sym, expr) + self.writeline(SymbolicCallArgLine(self, arg, V.graph)) + + def finalize_prefix(self): + pass + + def codegen_cpp_sizevar(self, x: Expr, *, simplify: bool = True) -> str: + raise RuntimeError("codegen_cpp_sizevar is only implemented for cpp_wrapper!") + + def codegen_python_sizevar(self, x: Expr, *, simplify: bool = True) -> str: + return pexpr(x, simplify=simplify) + + def codegen_sizevar(self, x: Expr) -> str: + return self.codegen_python_sizevar(x) + + def codegen_tuple_access(self, basename: str, name: str, index: str) -> str: + return f"{basename}[{index}]" + + def codegen_python_shape_tuple(self, shape: Sequence[Expr]) -> str: + parts = [*map(self.codegen_python_sizevar, shape)] + if len(parts) == 0: + return "()" + if len(parts) == 1: + return f"({parts[0]}, )" + return f"({', '.join(parts)})" + + def codegen_shape_tuple(self, shape: Sequence[Expr]) -> str: + return self.codegen_python_shape_tuple(shape) + + def codegen_alloc_from_pool( + self, name, offset, dtype, shape, stride + ) -> tuple[str, list[str]]: + return "alloc_from_pool({})".format( + ", ".join( + [ + name, + pexpr(offset), # bytes not numel + str(dtype), + self.codegen_python_shape_tuple(shape), + self.codegen_python_shape_tuple(stride), + ] + ) + ), [] + + def codegen_reinterpret_view( + self, + data, + size, + stride, + offset, + writeline: Callable[..., None], + dtype=None, + ) -> str: + # Get the innermost buffer's layout info to help reinterpret view. + # Consider a chain of (ReinterpretView <- TensorBox| StorageBox)... <- buffer + # If we only use x.data to determine the reinterpret, we may get wrong layout. + # For example: + # x = ReinterpretView( + # Storage( + # ReinterpretView( + # storage( + # Buffer(name='buf0', layout=(size=(2, 5, 10), ...) + # ), + # layout=(10, 10), + # ), + # ), + # layout=(10, 10), + # ) + # In this case, x.data.layout == x.layout is (10, 10), the reinterpret view will return buf0, + # but buf0 need to be viewed from (2, 5, 10) to (10, 10). + # So we need to dig into the chain to find the innermost buffer's layout. + d_size, d_stride, d_offset, d_dtype, collapsible = ( + codegen_reinterpret_view_helper(data) + ) + + def apply_reinterpret( + name, tgt_size, tgt_stride, tgt_offset, cast_dtype, base_dtype + ): + s = self.codegen_python_shape_tuple(tgt_size) + st = self.codegen_python_shape_tuple(tgt_stride) + off = self.codegen_sizevar(tgt_offset) + expr = f"reinterpret_tensor({name}, {s}, {st}, {off})" + if cast_dtype is not None and cast_dtype != base_dtype: + return f"aten.view.dtype({expr}, {cast_dtype})" + return expr + + name = data.get_name() + collapsed = collapsible and offset == d_offset + if collapsed: + same_layout = size == d_size and stride == d_stride + base_dtype = d_dtype + else: + same_layout = ( + size == data.layout.size + and stride == data.layout.stride + and offset == data.layout.offset + ) + base_dtype = data.dtype + + if same_layout: + if dtype is not None and dtype != base_dtype: + return f"aten.view.dtype({name}, {dtype})" + return f"{name}" + + return apply_reinterpret(name, size, stride, offset, dtype, base_dtype) + + def codegen_device_copy(self, src, dst, non_blocking: bool | str): + self.writeline(f"{dst}.copy_({src}, {non_blocking})") + + def codegen_multi_output(self, node: ir.MultiOutput): + result_name = node.get_name() + arg_name = node.input_name(0) + self.writeline(MultiOutputLine(self, result_name, arg_name, node.indices)) + + def codegen_dynamic_select_index(self, node, clamp): + index_str = f"{node.index} + {node.size} if {node.index} < 0 else {node.index}" + if clamp: + index_str = f"max(0, min({node.size}, {index_str}))" + self.writeline( + f"{node.unbacked_offset_symbol} = {node.base_offset} + {node.base_dim_stride} * ({index_str})" + ) + # record in unbacked_symbol_decls so we won't generate a declaration of the symbol again + self.unbacked_symbol_decls.add(str(node.unbacked_offset_symbol)) + + def codegen_dynamic_slice_size(self, node): + def clamp_index(x): + pos = self.codegen_sizevar(sympy.Max(0, sympy.Min(x, node.size))) + neg = self.codegen_sizevar( + sympy.Max(0, sympy.Min(x + node.size, node.size)) + ) + x_cond = self.codegen_sizevar(x) + return f"{pos} if {x_cond} >= 0 else {neg}" + + def codegen_with_step(start_var, end_var, step): + if step == 1: + return f"{end_var} - {start_var}" + step_ = self.codegen_sizevar(step) + return f"({end_var} - {start_var} + {step_} - 1) // {step_}" + + # codegen start, end + sym = node.unbacked_size_symbol + start = clamp_index(node.start) + end = clamp_index(node.end) + self.writeline(f"{sym}_start = {start}") + self.writeline(f"{sym}_end = {end}") + with_step = codegen_with_step(f"{sym}_start", f"{sym}_end", node.step) + self.writeline(f"{sym} = max(0, {with_step})") + self.unbacked_symbol_decls.add(str(node.unbacked_size_symbol)) + + def codegen_dynamic_scalar(self, node): + self.writeline(DynamicScalarLine(self, node)) + + def _codegen_dynamic_scalar(self, node): + (data,) = (t.codegen_reference() for t in node.inputs) + if len(node.keypath) == 0: + self.writeline(f"{node.sym} = {data}.item()") + elif len(node.keypath) == 1 and isinstance(node.keypath[0], ConvertIntKey): + self.writeline(f"{node.sym} = 1 if {data}.item() else 0") + elif len(node.keypath) == 1 and isinstance(node.keypath[0], DivideByKey): + self.writeline(f"{node.sym}_undivided = {data}.item()") + self.writeline( + f"assert {node.sym}_undivided % {node.keypath[0].divisor} == 0, " + f"f'{{{node.sym}_undivided}} not divisible by {node.keypath[0].divisor}'" + ) + self.writeline( + f"{node.sym} = {node.sym}_undivided // {node.keypath[0].divisor}" + ) + else: + raise AssertionError(f"unrecognized keypath {node.keypath}") + # No one should ever use this buffer, but for uniformity + # define the variable and assign it None + self.writeline(f"{node.get_name()} = None") + + def benchmark_compiled_module(self, output): + """Write out codegen for benchmarking the output code""" + + def add_fake_input(name, shape, stride, device, dtype): + output.writeline( + f"{name} = rand_strided(" + f"{self.codegen_python_shape_tuple(shape)}, " + f"{self.codegen_python_shape_tuple(stride)}, " + f"device='{device}', dtype={dtype})" + ) + + def add_expr_input(name, val): + output.writeline(f"{name} = {val}") + + def add_torchbind_input(name, value): + if value is None: + output.writeline(f"{name} = None") + return + + import pickle + + try: + output.writeline(f"{name} = pickle.loads({pickle.dumps(value)!r})") + except (TypeError, AttributeError, pickle.PicklingError) as e: + output.writeline( + f'raise TypeError("Failed to pickle opaque type {type(value)} for variable {name}: {str(e)}")' + ) + + # Generate get_args() to create input tensors separately from benchmarking + output.writelines(["", "", "def get_args():"]) + with output.indent(): + output.splice( + """ + from torch._dynamo.testing import rand_strided + """, + strip=True, + ) + + for name, value in V.graph.constants.items(): + # all the constants are global variables, that's why we need + # these 'global var_name' lines + output.writeline(f"global {name}") + add_fake_input( + name, value.size(), value.stride(), value.device, value.dtype + ) + + if len(V.graph.torchbind_constants) > 0: + output.writeline("import pickle") + for name, torchbind_obj in V.graph.torchbind_constants.items(): + # all the constants are global variables, that's why we need + # these 'global var_name' lines + output.writeline(f"global {name}") + add_torchbind_input(name, torchbind_obj) + + for name, value in V.graph.graph_inputs.items(): + if isinstance(value, sympy.Symbol) and isinstance( + V.graph.sizevars.backed_var_to_val.get(value, None), SingletonInt + ): + # Inductor should only work with dense -> dense graph, and + # SingletonInts belong to metadata that should only live on + # the subclass. + continue + if isinstance(value, ir.TorchBindObject): + output.writeline(f"{name} = None") + elif isinstance(value, sympy.Expr): # Don't need to add symbolic + # TODO: this fallback and those below actually will generate possibly + # invalid benchmark code, because it's not guaranteed 42 + # is actually a valid value for the kernel in question. + # See https://github.com/pytorch/pytorch/issues/124686 + add_expr_input( + name, V.graph.sizevars.optimization_hint(value, fallback=42) + ) + elif isinstance(value, sympy.Basic): + # sympy.Boolean (e.g. StrictLessThan from torch.cond predicates) + # is not a sympy.Expr so optimization_hint cannot handle it. + # Use False as a fallback for benchmark harness purposes. + add_expr_input(name, False) + elif isinstance(value, ir.GeneratorState): + add_expr_input( + name, + f"torch.cuda.default_generators[{value.device.index}].graphsafe_get_state()", + ) + elif isinstance(value, ir.OpaqueObjectState): + output.writeline(f"{name} = None") + else: + shape = V.graph.sizevars.optimization_hints( + value.get_size(), fallback=42 + ) + stride = V.graph.sizevars.optimization_hints( + value.get_stride(), fallback=42 + ) + + add_fake_input( + name, + shape, + stride, + value.get_device(), + value.get_dtype(), + ) + + output.writeline(f"return [{', '.join(V.graph.graph_inputs.keys())}]") + + # Generate benchmark_compiled_module() that takes args as parameter + output.writelines( + ["", "", "def benchmark_compiled_module(args, times=10, repeat=10):"] + ) + with output.indent(): + output.splice( + """ + from torch._inductor.utils import print_performance + fn = lambda: call(list(args)) + return print_performance(fn, times=times, repeat=repeat) + """, + strip=True, + ) + + def add_benchmark_harness(self, output): + """ + Append a benchmark harness to generated code for debugging + """ + if not config.benchmark_harness: + return + + self.benchmark_compiled_module(output) + + output.writelines(["", "", 'if __name__ == "__main__":']) + with output.indent(): + output.writelines( + [ + "from torch._inductor.wrapper_benchmark import compiled_module_main", + "args = get_args()", + ( + f"compiled_module_main('{get_benchmark_name()}', " + "lambda times, repeat: benchmark_compiled_module(args, times=times, repeat=repeat))" + ), + ] + ) + + def define_kernel( + self, + kernel_name: str, + kernel_body: str, + metadata: str | None = None, + gpu: bool = True, + cpp_definition: str | None = None, + ): + self.writeline( + KernelDefinitionLine( + self, + kernel_name, + kernel_body, + metadata=metadata, + gpu=gpu, + cpp_definition=cpp_definition, + ) + ) + + @staticmethod + def _format_kernel_definition( + kernel_name: str, kernel_body: str, metadata: str | None = None + ): + if config.triton.autotune_at_compile_time and metadata: + # Generating autotune block + # Need to replace C++ comment starter with Python comment starter + metadata = re.sub(r"^// ", "# ", metadata, flags=re.MULTILINE) + metadata_comment = f"{metadata}\n" if metadata else "" + body = f"\n\n{metadata_comment}{kernel_name} = {kernel_body}" + return body + + def _define_kernel_helper( + self, + kernel_name: str, + kernel_body: str, + metadata: str | None = None, + gpu: bool = True, + cpp_definition: str | None = None, + ): + if config.triton.autotune_at_compile_time and gpu: + body = self._format_kernel_definition( + kernel_name, kernel_body, metadata=metadata + ) + self.kernel_autotune_defs.splice(body) + if V.graph.cpp_wrapper: + # For cpp wrapper, no need to continue codegen for the main body + return + + body = self._format_kernel_definition( + kernel_name, kernel_body, metadata=metadata + ) + self.header.splice(body) + + def define_subgraph_launcher_fn(self, name: str, subgraph_code): + self.subgraph_definitions.splice(subgraph_code.value) + + @classmethod + def _get_triton_info_kernel_cls(cls): + # Other inductor triton backends may subclass from + # the `TritonKernel` class. An override of this method + # allows them to set which subclass to use to get information + # such as common triton imports or inductor metadata + from .triton import TritonKernel + + return TritonKernel + + def define_user_defined_triton_kernel( + self, + kernel, + configs, + kwargs, + restore_value_args, + reset_to_zero_args, + grids: list[list[int | sympy.Expr]], + epilogue_fusion: tuple[ir.ComputedBuffer, str] | None, + ): + from ..runtime.triton_heuristics import ( + config_to_dict, + FixedGrid, + PrecomputedGrid, + ) + from .common import ( + ConstexprArg, + KernelArgType, + SizeArg, + TensorArg, + TMADescriptorArg, + ) + + original_name = kernel.__name__ + signature: list[KernelArgType] = [] + constants: dict[str, Any] = {} + arg_indices: list[int] = [] + equal_to_1_args: list[str] = [] + + def add_to_signature(idx, arg): + signature.append(arg) + arg_indices.append(idx) + + def add_arg(idx, arg, is_constexpr=False, equals_1=False, equals_none=False): + if is_constexpr: + if triton_version_uses_attrs_dict(): + # tl.constexpr args appear in the signature in new versions of triton, + # but not in old versions of triton. + add_to_signature(idx, arg) + + if arg.name in kwargs: + # the arg may not appear in kwargs if it is an autotuned arg. + # in this case, it will be added in triton_heuristics after autotuning. + constants[arg.name] = kwargs[arg.name] + + else: + # the only case where arg name isn't in kwargs, should be + # when the arg is a constexpr. + assert arg.name in kwargs + + if equals_1: + if triton_version_uses_attrs_dict(): + # new versions of triton: add the equal-to-1 arg in the signature (labeled as "constexpr"), + # and add the arg as a constant. + # new versions of triton: add the equal-to-1 arg in the signature (labeled as, e.g., "i32"), + # and add the arg as a constant. + add_to_signature(idx, ConstexprArg(name=arg.name)) + else: + add_to_signature(idx, arg) + constants[arg.name] = 1 + elif equals_none: + if triton_version_uses_attrs_dict(): + # new versions of triton: add the none arg in the signature (as a constexpr arg) and as a constant + # old versions of triton: include the none arg as a constant (but not in the signature) + add_to_signature(idx, ConstexprArg(name=arg.name)) + constants[arg.name] = None + else: + add_to_signature(idx, arg) + + arg_names = [p.name for p in kernel.params] + constexprs = [p.num for p in kernel.params if p.is_constexpr] + for idx, key in enumerate(arg_names): + if idx in constexprs: + add_arg(idx, ConstexprArg(name=key), is_constexpr=True) + continue + + if key not in kwargs: + continue + + arg = kwargs[key] + + if kwargs[key] is None: + add_arg(idx, ConstexprArg(name=key), equals_none=True) + else: + if isinstance(arg, ir.TMADescriptor): + api_type, block_shape, dtype = ( + ("stable", arg.block_shape, arg.tensor.get_dtype()) + if isinstance(arg, ir.TMADescriptorStable) + else ("experimental", None, None) + ) + add_arg( + idx, + TMADescriptorArg( + name=key, + api_type=api_type, + block_shape=block_shape, + dtype=dtype, + ), + ) + elif isinstance(arg, ir.Buffer): + add_arg( + idx, + TensorArg( + name=key, + buffer=arg.get_name(), + dtype=arg.get_dtype(), + ), + ) + elif isinstance(arg, ir.ReinterpretView): + # for ReinterpretView we use the underlying + # buffer name and note the (possibly non-zero) + # offset relative to the underlying buffer + add_arg( + idx, + TensorArg( + name=key, + buffer=arg.data.get_name(), + dtype=arg.get_dtype(), + offset=arg.layout.offset, + ), + ) + else: + equals_1 = isinstance( + arg, (int, sympy.Integer) + ) and V.graph.sizevars.statically_known_equals( + arg, + 1, # type: ignore[arg-type] + ) + add_arg(idx, SizeArg(key, arg), equals_1=equals_1) + + triton_signature = signature_to_meta( + signature, + size_dtype=None, # try to infer based on symints + indices=arg_indices, + argdefs=[ArgName(x) for x in kernel.arg_names], + ) + triton_meta: dict[str, Any] = { + "signature": triton_signature, + "device": DeviceProperties.create(V.graph.get_current_device_or_throw()), + # Triton compiler includes equal_to_1 args into constants even + # when they are not constexpr. otherwise there may be a segfault + # during launching the Inductor-compiled Triton kernel. + # TODO(aakhundov): add None args to constants, too. currently, this + # causes CUDA errors in test_aot_inductor.test_triton_kernel_with_none_input. + # https://github.com/pytorch/pytorch/issues/120478#issuecomment-1962822307 + # https://github.com/triton-lang/triton/blob/231efe9ed2d200be0f69a07c298e4342b08efe3d/python/triton/runtime/jit.py#L384 + "constants": { + **constants, + **dict.fromkeys(equal_to_1_args, 1), + }, + "configs": [ + config_of( + signature, + indices=arg_indices, + ) + ], + } + + if restore_value_args: + triton_meta["restore_value"] = tuple(restore_value_args) + + if reset_to_zero_args: + triton_meta["reset_to_zero"] = tuple(reset_to_zero_args) + + if len(grids) == 1: + # compute the grid in the wrapper and pass it in as an arg + inductor_meta: dict[str, Any] = FixedGrid.setup_grid_as_args() + extra_launcher_call_args = [*map(sympy.sympify, grids[0])] + else: + + def rename_sizes_for_launcher(expr: int | sympy.Expr) -> sympy.Expr: + if isinstance(expr, sympy.Expr): + symbols = [*expr.free_symbols] + if not symbols: + return expr + symbols.sort(key=str) + for sym in symbols: + if sym in extra_launcher_args: + continue + extra_launcher_args[sym] = sympy.Symbol( + f"_launcher_s{len(extra_launcher_args)}" + ) + return sympy_subs(expr, extra_launcher_args) + assert isinstance(expr, int) + return sympy.Integer(expr) + + extra_launcher_args: dict[sympy.Symbol, sympy.Symbol] = {} + grids = [[*map(rename_sizes_for_launcher, grid)] for grid in grids] + + assert grids and len(grids) == len(configs) + precomputed_grids = [] + for grid, cfg in sorted( + zip(grids, configs), key=lambda x: len(x[1].kwargs), reverse=True + ): + precomputed_grids.append( + { + "config": config_to_dict(cfg), + "python": [*map(pexpr, grid)], + "cpp": [*map(cexpr, grid)], + "python_slow": [*map(pexpr, grid)], + } + ) + inductor_meta = { + "grid_type": PrecomputedGrid.__name__, + "precomputed_grids": precomputed_grids, + "extra_launcher_args": [*map(str, extra_launcher_args.values())], + } + extra_launcher_call_args = [*extra_launcher_args.keys()] + + if constexprs: + inductor_meta["declared_constexpr_names"] = [ + arg_names[i] for i in constexprs + ] + + # Distinguish between different functions using function id + cache_key: Any = [id(kernel.fn)] + if len(configs) > 0: + for arg in kwargs.values(): + # We need to key on non tensor arg only in autotune mode + if not isinstance(arg, (ir.Buffer, ir.ReinterpretView)): + cache_key.append(arg) + cache_key.append(str(triton_meta)) + cache_key.extend(str(inductor_meta)) + cache_key = tuple(cache_key) + if cache_key in self.user_defined_kernel_cache: + name, triton_meta, cached_inductor_meta = self.user_defined_kernel_cache[ + cache_key + ] + return ( + name, + triton_meta, + cached_inductor_meta, + extra_launcher_call_args, + ) + + name = f"{original_name}_{len(self.user_defined_kernel_cache)}" + + compile_wrapper = IndentedBuffer() + if config.triton.unique_user_kernel_names: + compile_wrapper.writeline(f"async_compile.triton({name!r}, '''") + else: + compile_wrapper.writeline(f"async_compile.triton({original_name!r}, '''") + + inductor_meta["kernel_name"] = name + triton_info_kernel_cls = self._get_triton_info_kernel_cls() + inductor_meta.update(triton_info_kernel_cls.inductor_meta_common()) + + compile_wrapper.splice(triton_info_kernel_cls.gen_common_triton_imports()) + if config.triton.proton_profiling: + compile_wrapper.writeline('pl.enable_semantic("triton")') + + compile_wrapper.splice( + f""" + @triton_heuristics.user_autotune( + configs={[*map(config_to_dict, configs)]!r}, + inductor_meta={inductor_meta!r}, + triton_meta={triton_meta!r}, + filename=__file__, + custom_kernel=True, + ) + @triton.jit + """ + ) + kernel_src = user_defined_triton_kernel_transitive_closure_source_code( + kernel, epilogue_fusion + ) + if config.triton.unique_user_kernel_names: + # We replace the original_name with the unique name. + kernel_src = kernel_src.replace(f"def {original_name}(", f"def {name}(") + if config.cpp_wrapper: + # With cpp_wrapper + autotune_at_compile_time=False, the source is + # further embedded in a C++ raw string inside a Python r"""...""" wrapper. + # So we need to add backslash here. + kernel_src = kernel_src.replace('"""', '\\"\\"\\"') + kernel_src = kernel_src.replace("'''", "\\'\\'\\'") + compile_wrapper.splice(kernel_src) + + current_device = V.graph.get_current_device_or_throw() + compile_wrapper.writeline(f"''', device_str='{current_device.type}')") + _, lineno = inspect.getsourcelines(kernel.fn) + srcfile = inspect.getsourcefile(kernel.fn) + metadata = f"# Original path: {srcfile}:{lineno}" + self.define_kernel( + name, + compile_wrapper.getvalue(), + metadata, + ) + # Add to the cache for the next use + self.user_defined_kernel_cache[cache_key] = (name, triton_meta, inductor_meta) + return name, triton_meta, inductor_meta, extra_launcher_call_args + + def generate_numel_expr(self, kernel_name: str, tree, suffix: str | None = None): + sym_name = f"{kernel_name}_{tree.prefix}numel" + if suffix is not None: + sym_name += f"_{suffix}" + sym = sympy.Symbol(sym_name, is_integer=True, is_positive=True) + + # We can get symbolic expressions here, like s0*64 + # It is fine to have them here, but we need to handle them correctly as their own type + # This is tricky to do, so we wrap in a custom type, distinct from scalars, but also from sympy* + # scalars as well. + # This is handled in `generate_args_decl` which has a correct comment of: TODO: only works for + # constant now, need type info. I agree, this needs type info, and while this is not true type info + # it suffices as a type hint for the purposes of producing the correct code for this type. + arg = SymbolicCallArg(sym, tree.numel) + + is_benchmark_kernel = kernel_name == "" + if not is_benchmark_kernel: + self.writeline(SymbolicCallArgLine(self, arg, V.graph)) + + return arg + + def _generate_symbolic_call_arg_helper( + self, arg: SymbolicCallArg, graph: GraphLowering + ) -> None: + self.writeline(f"{arg.inner} = {pexpr(arg.inner_expr)}") + + def generate_workspace_allocation(self, ws: WorkspaceArg): + name = ws.get_name() + line = AllocateLine(self, ws) + if ws.zero_mode == WorkspaceZeroMode.UNINITIALIZED: + self.writeline(line) + elif ws.zero_mode == WorkspaceZeroMode.ZERO_ON_CALL: + self.writeline(line) + self.writeline(self.make_zero_buffer(name)) + elif ws.zero_mode == WorkspaceZeroMode.ZERO_PER_GRAPH: + prior = self.allocated_workspaces.get(name) + if prior: + assert isinstance(prior, AllocateLine) and isinstance( + prior.node, WorkspaceArg + ) + # expand existing allocation + prior.node = WorkspaceArg.maximum(prior.node, ws) + else: + self.writeline(line) + self.writeline(self.make_zero_buffer(name)) + self.allocated_workspaces[name] = line + else: + raise AssertionError(ws.zero_mode) + + if config.triton.autotune_at_compile_time: + self.kernel_autotune_calls.writeline( + PythonWrapperCodegen.make_allocation( + self, + name, + ws.device, + ws.dtype, + shape=(V.graph.sizevars.optimization_hint(ws.count),), + stride=(1,), + ) + ) + if ws.zero_mode != WorkspaceZeroMode.UNINITIALIZED: + self.kernel_autotune_calls.writeline( + PythonWrapperCodegen.make_zero_buffer(self, name) + ) + + def generate_workspace_deallocation(self, ws: WorkspaceArg): + if ws.zero_mode != WorkspaceZeroMode.ZERO_PER_GRAPH: + self.writeline(FreeIfNotReusedLine(self, ws)) + + def make_zero_buffer(self, name): + return f"{name}.zero_(){self.ending}" + + def wrap_kernel_call(self, name, call_args): + return f"{name}({', '.join(call_args)}){self.ending}" + + def generate_profiler_mark_wrapper_call(self, stack): + self.wrapper_call.writeline("from torch.profiler import record_function") + self.wrapper_call.writeline( + f"with record_function('graph_{V.graph.graph_id}_inductor_wrapper_call'):" + ) + stack.enter_context(self.wrapper_call.indent()) + + def generate_start_graph(self): + self.wrapper_call.writeline("start_graph()") + + def generate_end_graph(self): + self.wrapper_call.writeline(f"end_graph({config.profile_bandwidth_output!r})") + + def generate_proton_finalize(self): + """Synchronize GPU to ensure proton captures all kernel events.""" + self.wrapper_call.writeline(V.graph.device_ops.synchronize()) + + def generate_reset_kernel_saved_flags(self): + self.wrapper_call.splice( + f""" + for kernel in globals().values(): + if isinstance(kernel, {triton_heuristics.__name__}.CachingAutotuner): + kernel.cuda_kernel_saved = False + """ + ) + + def generate_save_uncompiled_kernels(self): + """ + Precompile and save the CUBINs of the Triton kernels that haven't + been precompiled and saved as a side effect of running the generated + JIT model (Python wrapper). This can happen when the model contains + control flow: only one pass through the control flow operators covers + the kernels that are saved, the remaining kernels are not launched, + hence not saved. The main purpose of this codegen is to compile and + save the Triton kernels outside the active control flow path for + subsequent AOTInductor code generation and compilation. + """ + self.wrapper_call.splice( + f""" + for kernel in globals().values(): + if isinstance(kernel, {triton_heuristics.__name__}.CachingAutotuner): + if not kernel.cuda_kernel_saved: + if len(kernel.launchers) == 0: + kernel.precompile() + kernel.save_gpu_kernel( + stream="stream", # use dummy stream + launcher=kernel.launchers[0], + ) + """ + ) + + def prepare_triton_kernel_call(self, call_args): + def wrap_arg(arg): + if isinstance(arg, str): + # dynamo wraps unspec variable as 0d CPU tensor, need convert to scalar + return arg + ".item()" if should_unwrap_unspec_arg(arg) else arg + elif isinstance(arg, (int, float, bool, SymbolicCallArg)): + return str(arg) + else: + return pexpr(V.graph.sizevars.simplify(arg)) + + return [wrap_arg(arg) for arg in call_args] + + def generate_example_arg_value(self, arg, arg_type, raw_arg=None): + if isinstance(arg_type, torch_dtype): + if isinstance(raw_arg, ir.TMADescriptor): + # first we generate the underlying buffer + buf_name = raw_arg.get_tensor().get_name() + buf = self.args_to_buffers[arg] + elif self.args_to_buffers.get(arg): + buf_name = arg + buf = self.args_to_buffers[arg] + else: + assert raw_arg is not None, ( + "V.graph.get_buffer(arg) and raw_arg can't be None at the same time" + ) + buf_name = f"tmp_arg_{self.kernel_autotune_tmp_arg_idx}" + buf = raw_arg + self.kernel_autotune_tmp_arg_idx += 1 + + assert buf is not None, f"Failed to find a buffer for arg {arg}" + size = V.graph.sizevars.optimization_hints(buf.get_size()) + allocation_size = V.graph.sizevars.optimization_hints( + V.graph.get_allocation_size(buf) + ) + stride = V.graph.sizevars.optimization_hints(buf.get_stride()) + + device = buf.get_device() + dtype = buf.get_dtype() + offset = V.graph.sizevars.optimization_hint(buf.get_layout().offset) + value = f"generate_example_value({size}, {stride}, '{device}', {dtype}, {offset}, {allocation_size})" + self.kernel_autotune_calls.writeline(f"{buf_name} = {value}") + + if isinstance(raw_arg, ir.TMADescriptor): + # generate another line initializing a host-side TMA + # descriptor from the underlying buffer created above + value = self._generate_tma_descriptor_call( + desc=raw_arg, + apply_size_hints=True, + ) + buf_name = arg + self.kernel_autotune_calls.writeline(f"{buf_name} = {value}") + + return buf_name + elif issubclass(arg_type, sympy.Basic) or isinstance(arg, SymbolicCallArg): + # arg is a symbol or symbolic expression + if isinstance(arg, str): + if arg in self._meta_vars: + return arg + if raw_arg is None: + return "None" + arg = raw_arg + if isinstance(arg, SymbolicCallArg): + arg = arg.inner_expr + if arg in V.graph.sizevars.inv_precomputed_replacements: + arg = V.graph.sizevars.inv_precomputed_replacements[arg] + + return str(V.graph.sizevars.optimization_hint(arg)) + + elif isinstance(arg, (str, int, float, bool)): + return str(arg) + elif isinstance(arg, list): + return f"[{', '.join(self.generate_example_arg_value(a, type(a)) for a in arg)}]" + else: + raise NotImplementedError(f"Unsupported type {type(arg)}") + + def _grid_dim_str(self, grid_per_dim): + if isinstance(grid_per_dim, list): + return ( + "[" + ", ".join(self._grid_dim_str(item) for item in grid_per_dim) + "]" + ) + else: + return pexpr(grid_per_dim) + + def generate_kernel_call( + self, + kernel_name: str, + call_args, + *, + device=None, + triton=True, + arg_types=None, + raw_keys=None, + raw_args=None, + triton_meta=None, + inductor_meta=None, + original_fxnode_name=None, + ): + """ + Generates kernel call code. + + triton: Defines whether the backend uses Triton for codegen. Otherwise it uses the CUDA language when gpu=True, + and C++ when gpu=False. + """ + + # Store buffers corresponding to each call arg. + # This is used to generate example args for autotuning later on. + self.args_to_buffers.update( + { + arg: V.graph.try_get_buffer(arg) + for arg in call_args + if isinstance(arg, str) + } + ) + + device = device or V.graph.get_current_device_or_throw() + current_stream_idx = V.graph.scheduler.current_stream_idx + self.writeline( + KernelCallLine( + self, + kernel_name=kernel_name, + call_args=call_args, + # pyrefly: ignore [bad-argument-type] + raw_keys=raw_keys, + # pyrefly: ignore [bad-argument-type] + raw_args=raw_args, + # pyrefly: ignore [bad-argument-type] + arg_types=arg_types, + triton=triton, + # pyrefly: ignore [bad-argument-type] + triton_meta=triton_meta, + inductor_meta=inductor_meta, + device=device, + graph_name=V.graph.name, + # pyrefly: ignore [bad-argument-type] + original_fxnode_name=original_fxnode_name, + current_stream_idx=current_stream_idx, + ) + ) + + def _generate_kernel_call_helper( + self, + kernel_name: str, + call_args, + *, + device=None, + triton=True, + arg_types=None, + raw_keys=None, + raw_args=None, + triton_meta=None, + inductor_meta=None, + graph_name="", + original_fxnode_name=None, + current_stream_idx=None, + ): + device = device or V.graph.get_current_device_or_throw() + if not triton and device.type not in ("cuda", "xpu"): + if device.type == "cpu": + self.writeline(self.wrap_kernel_call(kernel_name, call_args)) + elif device.type == "mps": + # TODO: Fix me, MPS does not expose streams now + self.writeline(self.wrap_kernel_call(kernel_name, call_args)) + else: + raise RuntimeError(f"device {device.type} nyi") + return + + call_args_str = self.prepare_triton_kernel_call(call_args) + call_args_str = ", ".join(call_args_str) + if current_stream_idx is not None and current_stream_idx != DEFAULT_STREAM_IDX: + # Inside a user stream context: emit a fresh get_raw_stream call so + # it picks up the active stream at runtime, rather than reusing the + # LRU-cached stream0 variable which captured the default stream. + self.write_get_raw_stream_header() + stream_name = "raw_stream" + self.writeline(f"{stream_name} = get_raw_stream({device.index})") + else: + stream_name = PythonWrapperCodegen.write_get_raw_stream( + self, device.index, graph_name + ) + if not triton: + stream_ptr = f"c_void_p({stream_name})" + self.writeline( + f"{kernel_name}.{kernel_name}({call_args_str}, {stream_ptr})" + ) + return + + self.write_triton_header_once() + + if ( + config.triton.autotune_at_compile_time + and kernel_name not in self.kernel_autotune_names + ): + # Create example args for autotune in a separate epilogue + assert arg_types is not None and len(call_args) == len(arg_types), ( + "call_args and arg_types do not match" + ) + + autotune_args = None + if original_fxnode_name and V.graph.autotuning_mapping: + autotune_args = V.graph.autotuning_mapping.get( + original_fxnode_name, None + ) + + def get_autotune_deletion_call() -> str: + """After all the autotune kernel calls have been written (i.e. + self.kernel_autotune_example_args is complete), returns a deletion call + for all autotune example tensors that are unnecessary after kernel_name + is called.""" + tensors_to_delete = [ + tensor + for tensor, kn in self.kernel_autotune_example_args.values() + if kn == kernel_name + ] + if tensors_to_delete: + return f"del {', '.join(tensors_to_delete)}\n" + return "" + + def infer_arg_by_inputs(raw_keys, raw_args, idx, reused_args): + """We try to infer raw_arg (i.e. raw_args[idx]) from remaining raw_args. + This is particularly useful for jagged cases, where the dimension is often + being passed in as an input.""" + + target_arg = raw_args[idx] + if target_arg in reused_args: + return True + + for i, (raw_key, raw_arg) in enumerate(zip(raw_keys, raw_args)): + if i == idx or not isinstance(raw_arg, IRNode): + continue + + triton_input = "" + if autotune_args and raw_key in autotune_args: + triton_input = self.get_autotuning_input_name( # type: ignore[attr-defined] + autotune_args[raw_key] + ) + if triton_input == "": + continue + + try: + layout = raw_arg.get_layout() + for dim, s in enumerate(layout.size): + if s == target_arg: + reused_args[target_arg] = f"{triton_input}.shape[{dim}]" + return True + except NotImplementedError: + # If layout for this IRNode is not implemented, we could just skip. + # Only raise for other Error cases. + continue + return False + + all_args = [] + if raw_args is None: + # create a dummy raw_args for uniform behavior in the following loop + assert raw_keys is None, "keys are not None but args are" + raw_keys = [None] * len(call_args) + raw_args = [None] * len(call_args) + else: + assert len(raw_args) == len(call_args), ( + "call_args and raw_args do not match" + ) + + reused_args = {} + for i, (arg, arg_type, raw_key, raw_arg) in enumerate( + # pyrefly: ignore [bad-argument-type, no-matching-overload] + zip(call_args, arg_types, raw_keys, raw_args) + ): + key = None + if isinstance(arg, str) and "=" in str(arg): + # arg may be passed in a kwarg style, and then we need to extract its value + key, arg = arg.split("=") + + triton_input: str | None = None + if autotune_args and raw_key in autotune_args: + triton_input = self.get_autotuning_input_name( # type: ignore[attr-defined] + autotune_args[raw_key] + ) + + if triton_input: + arg_str = triton_input + if not isinstance(arg_type, torch_dtype) and ( + issubclass(arg_type, sympy.Basic) + or isinstance(arg, SymbolicCallArg) + ): + reused_args[raw_arg] = arg_str + elif raw_key == "" and infer_arg_by_inputs( + raw_keys, raw_args, i, reused_args + ): + # Empty raw_key means this is a arg that's not native to the triton kernel, + # and is being added by inductor. + arg_str = reused_args[raw_arg] + elif isinstance(arg_type, torch_dtype): + # workspace allocation is already generated by `generate_workspace_allocation()` + # in `TritonKernel.call_kernel()`. + if re.match(r"^(workspace|semaphore)", arg): + arg_str = arg + elif arg not in self.kernel_autotune_example_args: + arg_str = self.generate_example_arg_value( + arg, arg_type, raw_arg + ) + else: + arg_str = self.kernel_autotune_example_args[arg][0] + self.kernel_autotune_example_args[arg] = (arg_str, kernel_name) + else: + arg_str = self.generate_example_arg_value(arg, arg_type, raw_arg) + + if isinstance(arg, str) and should_unwrap_unspec_arg(arg): + arg_str += ".item()" + all_args.append(arg_str if key is None else f"{key}={arg_str}") + + # Make sure kernel launch under a device guard because models don't always run on device 0 + self.kernel_autotune_calls.writeline( + f"with {V.graph.device_ops.device_guard(device.index)}:" + ) + self.kernel_autotune_calls.do_indent() + self.kernel_autotune_calls.writeline( + f"{kernel_name}.run({', '.join(all_args)}, stream={stream_name})" + ) + self.kernel_autotune_calls.do_unindent() + + self.kernel_autotune_calls.writeline( + DelayReplaceLine("", get_autotune_deletion_call, "") + ) + self.kernel_autotune_names.add(kernel_name) + if V.graph.cpp_wrapper: + # For cpp wrapper, no need to continue codegen for the main body + return + + # add debug printer code for triton kernel calls at (jit) inductor level + debug_printer_manager = V.graph.wrapper_code.debug_printer + debug_printer_manager.set_printer_args(call_args, kernel_name, arg_types, None) + with debug_printer_manager: + self.writeline(f"{kernel_name}.run({call_args_str}, stream={stream_name})") + self.write_triton_header_once() + + def writeline(self, line): + self.lines.append(line) + + def writelines(self, lines): + for line in lines: + self.writeline(line) + + def enter_context(self, ctx): + self.lines.append(LineContext(ctx)) + + def val_to_arg_str(self, s, type_=None): + from torch.utils._triton import has_triton_package + + if has_triton_package(): + import triton + + if isinstance(s, SymTypes): + return pexpr(s.node.expr) + elif isinstance(s, sympy.Expr): + return pexpr(s) + elif isinstance(s, (tuple, list)): + + @dataclasses.dataclass + class Shim: + ref: Any + + def __repr__(self): + return self.ref + + # Explicitly call the Python version of val_to_arg_str + return repr( + type(s)(Shim(PythonWrapperCodegen.val_to_arg_str(self, a)) for a in s) + ) + elif isinstance(s, torch._ops.OpOverload): + return _get_qualified_name(s) + elif isinstance(s, (ir.Buffer, ir.MutableBox, ReinterpretView)): + return s.codegen_reference() + elif has_triton_package() and isinstance(s, triton.language.dtype): # type: ignore[possibly-undefined] + return repr(s) + elif isinstance(s, (ir.GeneratorState, ir.OpaqueObjectState)): + return s.codegen_reference() + elif is_opaque_value_type(type(s)): + obj_repr, opaque_types = get_opaque_obj_repr(s) + for n, t in opaque_types.items(): + V.graph.opaque_value_type_classes[n] = t + return obj_repr + else: + return repr(s) + + # The following methods are for memory management + def make_buffer_allocation(self, buffer: BufferLike): + device = buffer.get_device() + dtype = buffer.get_dtype() + shape = tuple(buffer.get_size()) + allocation_shape = tuple(V.graph.get_allocation_size(buffer)) + stride = tuple(buffer.get_stride()) + is_pinned = buffer.get_is_pinned() + return self.make_allocation( + buffer.get_name(), device, dtype, shape, stride, allocation_shape, is_pinned + ) + + @cache_on_self + def write_memory_track_allocation_once(self): + import_str = """ + from torch._inductor.runtime.debug_utils import check_memory_step, track_tensor + """ + if not V.graph.cpp_wrapper: + self.imports.splice(import_str, strip=True) + + def make_allocation( + self, name, device, dtype, shape, stride, allocation_shape=None, is_pinned=False + ): + if allocation_shape is None: + allocation_shape = shape + + codegen_shape_tuple = self.codegen_python_shape_tuple(shape) + codegen_allocation_shape_tuple = self.codegen_python_shape_tuple( + allocation_shape + ) + codegen_stride_tuple = self.codegen_python_shape_tuple(stride) + if torch._inductor.config.test_configs.track_memory_lifecycle: + out = ( + f"{name} = tracked_empty_strided(" + f"{codegen_allocation_shape_tuple}, " + f"{codegen_stride_tuple}, " + f"dtype={dtype}, " + f"device='{device.type}', " + f"name='{name}')" + ) + elif device.type == "cpu" and is_pinned: + out = ( + f"{name} = empty_strided_cpu_pinned(" + f"{codegen_allocation_shape_tuple}, " + f"{codegen_stride_tuple}, " + f"{dtype})" + ) + elif device.type in ("cpu", "cuda", "xpu", "mtia"): + # optimized path for faster allocations, saving ~2us versus the stuff below + out = ( + f"{name} = empty_strided_{device.type}(" + f"{codegen_allocation_shape_tuple}, " + f"{codegen_stride_tuple}, " + f"{dtype})" + ) + # all other devices: + else: + out = ( + f"{name} = empty_strided(" + f"{codegen_allocation_shape_tuple}, " + f"{codegen_stride_tuple}, " + f"device='{device.type}', dtype={dtype})" + ) + if codegen_shape_tuple != codegen_allocation_shape_tuple: + # need an extra as_strided call + out = out + f".as_strided({codegen_shape_tuple}, {codegen_stride_tuple})" + return out + + def make_comment(self, line): + self.writeline(CommentLine(line)) + + def make_tensor_alias(self, new_name, old_name, comment=""): + return f"{self.declare}{new_name} = {old_name}{self.ending} {self.comment} {comment}" + + def make_buffer_free(self, buffer: BufferLike | ir.TorchBindObject): + return f"del {buffer.get_name()}" + + def make_free_by_names(self, names_to_del: list[str]): + return f"del {', '.join(name for name in names_to_del)}" + + def codegen_exact_buffer_reuse(self, old_name: str, new_name: str, del_line: str): + return f"{self.declare_maybe_reference}{new_name} = {old_name}{del_line}{self.ending} {self.comment} reuse" + + def write_provenance_debug_handle( + self, + kernel_name, + debug_handle: int | None = None, + ): + if debug_handle is not None: + self.writeline( + f"{self.comment} [Provenance debug handles] {kernel_name}:{debug_handle}" + ) + + def make_buffer_reuse(self, old: BufferLike, new: BufferLike, delete_old: bool): + assert old.get_dtype() == new.get_dtype() + old_name = old.get_name() + new_name = new.get_name() + del_line = ";" + if old_name not in V.graph.get_output_names() and delete_old: + del_line = f"; {self.make_buffer_free(old)}" + + if old.get_size() == new.get_size() and old.get_stride() == new.get_stride(): + return self.codegen_exact_buffer_reuse(old_name, new_name, del_line) + + reinterpret_view = self.codegen_reinterpret_view( + old, new.get_size(), new.get_stride(), 0, self.wrapper_call.writeline + ) + return f"{self.declare}{new_name} = {reinterpret_view}{del_line} {self.comment} reuse" + + def codegen_deferred_allocation(self, name: str, view: ir.ReinterpretView) -> None: + self.writeline( + DeferredLine( + name, + f"{self.declare}{name} = {view.codegen_reference()}{self.ending} {self.comment} alias", + ) + ) + + def codegen_allocation(self, buffer: ir.Buffer): + name = buffer.get_name() + + if ( + name in V.graph.removed_buffers + or name in self.allocated + or isinstance(buffer, (ir.DonatedBuffer, ir.SubgraphBuffer, ir.InputBuffer)) + ): + return + self.allocated.add(name) + if ( + isinstance( + buffer.get_defining_op(), + (ir.ExternKernelAlloc, ir.MultiOutput), + ) + and not buffer.should_allocate() + ): + return + + layout = buffer.get_output_spec() + if isinstance(layout, ir.MutationLayoutSHOULDREMOVE): + return + if isinstance(layout, ir.NoneLayout): + return + if isinstance(layout, ir.NonOwningLayout): + assert isinstance(layout.view, ir.ReinterpretView), ( + f"unexpected {type(layout.view)}: {layout.view}" + ) + box = layout.view.data + assert isinstance(box, ir.StorageBox), type(box) + input_buffer = box.data + assert isinstance(input_buffer, (ir.Buffer, ir.ReinterpretView)), type( + input_buffer + ) + if isinstance(input_buffer, ir.ReinterpretView): + + def unwrap_views(target) -> ir.Buffer: + if isinstance(target, ir.BaseView): + return unwrap_views(target.unwrap_view()) + if isinstance(target, ir.MutableBox): + return unwrap_views(target.data) + assert isinstance(target, ir.Buffer), type(target) + return target + + input_buffer = unwrap_views(input_buffer) + self.codegen_allocation(input_buffer) + self.writeline(ReinterpretLine(self, input_buffer, buffer, layout)) + return + + if isinstance(layout, ir.CommBufferLayout): + self.writeline(AllocateLine(self, buffer, comm_buffer=True)) + return + + self.writeline(AllocateLine(self, buffer)) + + def codegen_free(self, buffer): + name = buffer.get_name() + + # can be freed but not reused + if isinstance(buffer, (ir.InputBuffer, ir.TorchBindObject)): + self.writeline(FreeLine(self, buffer)) + return + + if isinstance(buffer.get_output_spec(), ir.CommBufferLayout): + # Comm buffers are not eligible for in-place reuse. Their reuse is + # achieved exclusively via buffer planning. + self.writeline(FreeIfNotReusedLine(self, buffer, comm_buffer=True)) + return + + if not self.can_reuse(buffer): + return + self.freed.add(name) + + self.writeline(FreeIfNotReusedLine(self, buffer)) + + def can_reuse(self, input_buffer, output_buffer=None): + name = input_buffer.get_name() + return not ( + name in V.graph.removed_buffers + or ( + name in V.graph.graph_inputs + and not isinstance( + V.graph.graph_inputs_original[name], ir.DonatedBuffer + ) + ) + or name in V.graph.constants + or name in V.graph.torchbind_constants + or name in V.graph.never_reuse_buffers + or name in self.freed + ) + + def did_reuse(self, buffer, reused_buffer): + # Check whether a given buffer was reused by a possible reuser in the wrapper codegen + # Can be consulted from inside ir codegen, e.g. to determine whether a copy is needed + return ( + buffer.get_name() in self.reuses + and self.reuses[buffer.get_name()] == reused_buffer.get_name() + ) + + def codegen_inplace_reuse(self, input_buffer: ir.Buffer, output_buffer: ir.Buffer): + assert can_match_buffer_size(input_buffer, output_buffer) + self.codegen_allocation(input_buffer) + self.freed.add(input_buffer.get_name()) + self.allocated.add(output_buffer.get_name()) + self.reuses[output_buffer.get_name()] = input_buffer.get_name() + self.writeline(ReuseLine(self, input_buffer, output_buffer)) + + def codegen_unbacked_symbol_decl(self, symbol): + name = str(symbol) + if name in self.unbacked_symbol_decls: + return name + else: + # When in CppWrapperCpu, we should only generate the declaration once + self.unbacked_symbol_decls.add(name) + return self.declare + name + + def codegen_unbacked_symbol_defs_for_outputs( + self, + output_name: str, + outputs: Any, + unbacked_bindings: dict[sympy.Symbol, pytree.KeyPath] | None, + ) -> None: + unbacked_bindings = resolve_unbacked_bindings( + V.graph.sizevars.shape_env, unbacked_bindings + ) + self.writeline( + UnbackedSymbolDefsLine(self, output_name, outputs, unbacked_bindings) + ) + + def _codegen_unbacked_symbol_defs_for_outputs( + self, + output_name: str, + outputs: Any, + unbacked_bindings: dict[sympy.Symbol, pytree.KeyPath] | None, + ) -> None: + if not unbacked_bindings: + return + + # This code is designed to generate code expressions from symbolic paths (keypaths) + # associated with certain symbols (unbacked bindings). These keypaths describe how + # to access the unbacked symbol in a structured way. + # For example, we might want to generate "u0 = outs[0].stride(1)"", where s = u0, and the keypath + # describes the structure of "outs[0].stride(1)", like [SequenceKey(0), CallMethodKey("stride"), SequenceKey[1]]. + for s, keypath in unbacked_bindings.items(): + # `go` recursively constructs a code expression by processing each element of + # the keypath and construct the expression incrementally. + # For example, given output name outs and keypath [SequenceKey(0), CallMethodKey("stride", 1)], + # it generates "outs[0]" based on SequenceKey(0), then recursively go("outs[0]", [CallMethodKey("stride"), ...]) + def go(expr: str, keypath: pytree.KeyPath): + if keypath == (): + return expr + + if ( + len(keypath) >= 2 + and isinstance(keypath[0], CallMethodKey) + and isinstance(keypath[1], pytree.SequenceKey) + ): + return go( + f"{expr}.{keypath[0].name}({keypath[1].idx})", keypath[2:] + ) + elif isinstance(keypath[0], CallMethodKey): + return go(f"{expr}.{keypath[0].name}()", keypath[1:]) + elif isinstance(keypath[0], pytree.SequenceKey): + return ( + go(f"std::get<{keypath[0].idx}>({expr})", keypath[1:]) + if V.graph.cpp_wrapper + else go(f"{expr}[{keypath[0].idx}]", keypath[1:]) + ) + elif isinstance(keypath[0], DivideByKey): + # TODO: need to assert divisibility + # TODO: this is invalid C++ codegen + return go(f"{expr}.__floordiv__({keypath[0].divisor})", keypath[1:]) + else: + raise AssertionError(f"unrecognized keypath {keypath}") + + # `go_outer` manages the top-level logic for generating the final expression. + # It handles special cases for C++ code generation and adjusts + # the keypath based on the context (e.g., single vs. multiple outputs). + def go_outer(): # type: ignore[no-untyped-def] + if V.graph.cpp_wrapper: + # Special handling for the top level buffer access, + # because self.get_name() is actually never bound; the + # individual output arguments are bound by + # generate_c_shim_fallback_kernel + if len(outputs) == 1: + out = outputs[0] + # When fallback kernel returns a list consisting of a single tensor, + # the output is represented as a MultiOutput with non empty indices. + # In this case, we strip the first key path away. + return go( + outputs[0].get_name(), + keypath[1:] + if isinstance(out, ir.MultiOutput) and len(out.indices) != 0 + else keypath, + ) + else: + assert isinstance(keypath[0], pytree.SequenceKey) + return go(outputs[keypath[0].idx].get_name(), keypath[1:]) + else: + return go(output_name, keypath) + + self.writeline( + f"{self.codegen_unbacked_symbol_decl(s)} = {go_outer()}{self.ending}" + ) + + def codegen_subgraph_by_inlining(self, subgraph, outer_inputs, outer_outputs): + # TODO (desertfire) - This function is the old way of supporting + # subgraph codegen by inlining subgraphs in the output code. For python + # wrapper, we have moved to lifting subgraphs as functions, supported by + # `codegen_subgraph` function. + # + # However this does not work with cpp wrapper. With cpp wrapper, we make + # two passes and the kernels are shared from the first pass to the next. + # Therefore, both the Python and CppWrapper need to share the some + # codegen infra. For now, CppWrapperCpu has not been updated to lift the + # subgraph as functions. Therefore for cpp_wrapper first pass with + # PythonWrapper, we still fallback to the old way of inlining subgraphs + # in the output code. Once we update CppWrapperCpu, we can remove this + # function. + def _codegen_subgraph_prefix(): + assert len(subgraph.graph.graph_inputs) == len(outer_inputs) + for inner_input, outer_input in zip( + subgraph.graph.graph_inputs, outer_inputs + ): + self.writeline( + f"{self.declare}{inner_input} = {outer_input}{self.ending}" + ) + + def _codegen_subgraph_suffix(): + assert len(subgraph.graph.graph_outputs) == len(outer_outputs) + for inner_output, outer_output in zip( + subgraph.graph.graph_outputs, outer_outputs + ): + self.writeline( + f"{outer_output} = {inner_output.codegen_reference()}{self.ending}" + ) + + try: + self.push_codegened_graph(subgraph.graph) + self.writeline(f"{self.comment} subgraph: {subgraph.name}") + _codegen_subgraph_prefix() + parent_graph = V.graph + with V.set_graph_handler(subgraph.graph): + subgraph.graph.codegen_subgraph( + parent_graph=parent_graph, + ) + _codegen_subgraph_suffix() + finally: + self.pop_codegened_graph() + + def codegen_partition_call( + self, + partition_id: int, + partition_signatures: ir.GraphPartitionSignature, + ): + """Generate code to call a graph partition""" + input_deallocation = partition_signatures.input_deallocation + output_nodes = partition_signatures.output_nodes + + input_names = list(input_deallocation.keys()) + [ + symbol_input.name for symbol_input in partition_signatures.symbol_inputs + ] + + inputs = ", ".join(input_names) + ("," if len(input_names) == 1 else "") + + output_names = [node.get_name() for node in output_nodes] + outputs = ", ".join(output_names) + ("," if len(output_nodes) == 1 else "") + + # Create a list of inputs for the subgraph call + self.writeline(f"partition{partition_id}_args = [{inputs}]") + + names_to_del = [ + name for name, deallocate in input_deallocation.items() if deallocate + ] + if names_to_del: + self.writeline(f"del {', '.join(names_to_del)}") + + # Call the subgraph launcher function + self.writeline( + f"({outputs}) = self.partitions[{partition_id}](partition{partition_id}_args)" + ) + self.writeline(f"del partition{partition_id}_args") + + def set_all_partition_names(self, num_partitions: int): + self.all_partition_names = [f"partition_{idx}" for idx in range(num_partitions)] + + def codegen_subgraph_call_with_flattened_outputs( + self, subgraph, outer_inputs, outer_flattened_outputs + ): + # Get the input and output names of the subgraph + outer_output_names = ", ".join(outer_flattened_outputs) + ( + "," if len(outer_flattened_outputs) == 1 else "" + ) + outer_input_names = ", ".join(outer_inputs) + ( + "," if len(outer_inputs) == 1 else "" + ) + + self.writeline(f"{subgraph.graph.name}_args = [{outer_input_names}]") + + # Call the subgraph launcher function + self.writeline( + f"({outer_output_names}) = {subgraph.graph.name}({subgraph.graph.name}_args)" + ) + + def codegen_subgraph_call(self, subgraph, outer_inputs, outer_buffer_name): + # Get the input and output names of the subgraph + outer_input_names = ", ".join(outer_inputs) + ( + "," if len(outer_inputs) == 1 else "" + ) + + self.writeline(f"{subgraph.graph.name}_args = [{outer_input_names}]") + + # Since the buffers are already put into the args list, we can free the + # buffers here. + V.graph.scheduler.free_buffers() + + # Call the subgraph launcher function + self.writeline( + f"{outer_buffer_name} = {subgraph.graph.name}({subgraph.graph.name}_args)" + ) + + def codegen_subgraph_common(self, subgraph): + self.push_codegened_graph(subgraph.graph) + self.make_comment("") + self.make_comment(f"{self.comment} subgraph: {subgraph.name}") + + parent_graph = V.graph + subgraph.graph.cpp_wrapper = parent_graph.cpp_wrapper + subgraph.graph.fx_wrapper = parent_graph.fx_wrapper + + if subgraph.graph.name not in self.already_codegened_subgraphs: + # If it is already codegened, the parent wrapper already has + # subgraph fn by name subgraph.graph.name + with V.set_graph_handler(subgraph.graph): + # do not graph partition for subgraph + with config.patch("graph_partition", False): + # Call the codegen of subgraph recursively + subgraph_code, _ = subgraph.graph.codegen() + subgraph_name = subgraph.graph.name + self.already_codegened_subgraphs.add(subgraph_name) + self.define_subgraph_launcher_fn(subgraph_name, subgraph_code) + + def codegen_subgraph_with_flattened_outputs( + self, subgraph, outer_inputs, outer_flattened_outputs + ): + self.codegen_subgraph_common(subgraph) + self.codegen_subgraph_call_with_flattened_outputs( + subgraph, outer_inputs, outer_flattened_outputs + ) + + def codegen_subgraph(self, subgraph, outer_inputs, outer_buffer_name): + # Codegen subgraph by recursively calling the codegen for the subgraph. + # This lifts the subgraph as a function in the output code. + self.codegen_subgraph_common(subgraph) + self.codegen_subgraph_call(subgraph, outer_inputs, outer_buffer_name) + + def codegen_invoke_subgraph(self, invoke_subgraph): + name = invoke_subgraph.get_name() + + self.writeline(f"{name} = [None] * {len(invoke_subgraph.outputs)}") + outer_inputs = [buf.codegen_reference() for buf in invoke_subgraph.inputs] + + if V.graph.aot_mode: + outer_outputs = [ + f"{name}[{i}]" for i in range(len(invoke_subgraph.outputs)) + ] + self.codegen_subgraph_by_inlining( + invoke_subgraph.subgraph, outer_inputs, outer_outputs + ) + else: + self.codegen_subgraph(invoke_subgraph.subgraph, outer_inputs, name) + + def codegen_conditional(self, conditional) -> None: + name = conditional.get_name() + + outer_inputs = [buf.codegen_reference() for buf in conditional.operands] + + predicate = conditional.predicate.codegen_reference() + if not isinstance(conditional.predicate, ir.ShapeAsConstantBuffer): + # move the Tensor predicate to host + predicate = f"{predicate}.item()" + + self.writeline(f"{name} = [None] * {len(conditional.outputs)}") + self.writeline(f"if {predicate}:") + self.writeline(EnterSubgraphLine(self, conditional.true_subgraph.graph)) + if V.graph.aot_mode: + outer_outputs = [f"{name}[{i}]" for i in range(len(conditional.outputs))] + self.codegen_subgraph_by_inlining( + conditional.true_subgraph, outer_inputs, outer_outputs + ) + else: + self.codegen_subgraph(conditional.true_subgraph, outer_inputs, name) + + self.writeline(ExitSubgraphLine(self)) + self.writeline("else:") + self.writeline(EnterSubgraphLine(self, conditional.false_subgraph.graph)) + if V.graph.aot_mode: + outer_outputs = [f"{name}[{i}]" for i in range(len(conditional.outputs))] + self.codegen_subgraph_by_inlining( + conditional.false_subgraph, outer_inputs, outer_outputs + ) + else: + self.codegen_subgraph(conditional.false_subgraph, outer_inputs, name) + self.writeline(ExitSubgraphLine(self)) + + def codegen_while_loop(self, while_loop, stack_output): + """while_loop is codegened as a host side while_loop""" + + def codegen_subgraph(subgraph, outer_inputs, outer_outputs): + """Helper method to deduplicate subgraph codegen logic""" + if V.graph.aot_mode: + self.codegen_subgraph_by_inlining(subgraph, outer_inputs, outer_outputs) + else: + self.codegen_subgraph_with_flattened_outputs( + subgraph, outer_inputs, outer_outputs + ) + + name = while_loop.get_name() + outer_carried_inputs = [ + buf.codegen_reference() for buf in while_loop.carried_inputs + ] + outer_additional_inputs = [ + buf.codegen_reference() for buf in while_loop.additional_inputs + ] + + ckp_offset = len(outer_carried_inputs) + self.writeline(f"{name} = [None] * {len(outer_carried_inputs)}") + if stack_output: + self.writeline( + f"{name}.extend([[] for _ in range({len(outer_carried_inputs)})])" + ) + + for i, inp in enumerate(outer_carried_inputs): + # set the initial state before the loop + self.writeline(f"{name}[{i}] = {inp}") + + cond_outer_inputs = [ + *[f"{name}[{i}]" for i in range(len(outer_carried_inputs))], + *outer_additional_inputs, + ] + cond_outer_outputs = [f"{name}_cond_result"] + body_outer_inputs = list( + cond_outer_inputs + ) # same inputs for cond_fn and body_fn + # Carry over the state from body_fn. Note: We only carry over + # the carried_inputs part of the inputs, the additional ones + # are passed in as they're before. + body_outer_outputs = body_outer_inputs[: len(outer_carried_inputs)] + # Check condition at the beginning and set up flag + codegen_subgraph( + while_loop.cond_subgraph, cond_outer_inputs, cond_outer_outputs + ) + self.writeline(f"should_loop = {cond_outer_outputs[0]}") + self.writeline("if not should_loop:") + if stack_output: + # Handle the case when loop never executes + for i, carried_input in enumerate(outer_carried_inputs): + self.writeline(EnterSubgraphLine(self, while_loop.body_subgraph.graph)) + self.writeline(f"{name}[{i}] = {carried_input}.unsqueeze(0).clone()") + self.writeline(ExitSubgraphLine(self)) + else: + for i, carried_input in enumerate(outer_carried_inputs): + self.writeline(EnterSubgraphLine(self, while_loop.body_subgraph.graph)) + self.writeline(f"{name}[{i}] = {carried_input}.clone()") + self.writeline(ExitSubgraphLine(self)) + + self.writeline("while should_loop:") + # Body execution + self.writeline(EnterSubgraphLine(self, while_loop.body_subgraph.graph)) + codegen_subgraph( + while_loop.body_subgraph, body_outer_inputs, body_outer_outputs + ) + self.writeline(ExitSubgraphLine(self)) + + # Collect outputs if enabled + if stack_output: + self.writeline(EnterSubgraphLine(self, while_loop.body_subgraph.graph)) + for i in range(len(outer_carried_inputs)): + self.writeline(f"{name}[{i + ckp_offset}].append({name}[{i}])") + self.writeline(ExitSubgraphLine(self)) + + # Condition check at end of loop + self.writeline(EnterSubgraphLine(self, while_loop.cond_subgraph.graph)) + codegen_subgraph( + while_loop.cond_subgraph, cond_outer_inputs, cond_outer_outputs + ) + self.writeline(ExitSubgraphLine(self)) + self.writeline(f" should_loop = {cond_outer_outputs[0]}") + + # Stack outputs after loop completion + if stack_output: + self.writeline("# Stack outputs after loop completion") + for i in range(len(outer_carried_inputs)): + self.writeline(f"if len({name}[{i + ckp_offset}]) > 0:") + self.writeline(EnterSubgraphLine(self, while_loop.body_subgraph.graph)) + self.writeline( + f"{name}[{i}] = torch.stack({name}[{i + ckp_offset}], dim=0)" + ) + self.writeline(ExitSubgraphLine(self)) + + @staticmethod + def statically_known_int_or_none(x): + try: + if getattr(x, "free_symbols", None): + # _maybe_evaluate_static will return (s0 // (2 // s0)) as 2, but + # the actual codegen will still generate the full expression here. + return None + if isinstance(x, int): + return x + val = V.graph._shape_env._maybe_evaluate_static(x) + if val is None: + return val + return int(val) # type: ignore[call-overload] + except Exception: + return None + + @staticmethod + def statically_known_list_of_ints_or_none(lst): + result = [] + for x in lst: + num = PythonWrapperCodegen.statically_known_int_or_none(x) + if num is None: + return None + result.append(num) + return result + + @staticmethod + def is_statically_known_list_of_ints(lst): + return ( + PythonWrapperCodegen.statically_known_list_of_ints_or_none(lst) is not None + ) + + @staticmethod + def static_shape_for_buffer_or_none(buffer): + return PythonWrapperCodegen.statically_known_list_of_ints_or_none( + buffer.get_size() + ) + + @staticmethod + def can_prove_buffer_has_static_shape(buffer): + return PythonWrapperCodegen.static_shape_for_buffer_or_none(buffer) is not None + + def write_kernel_context_guard( + self, + kernel_name: str, + node_schedule: Sequence[BaseSchedulerNode] | ExternKernel, + ): + return + + def write_kernel_context_guard_begin( + self, + ): + """ + Mark the beginning of kernel context guard + """ + return + + def write_kernel_context_guard_end( + self, + ): + """ + Mark the end of kernel context guard + """ + return + + +class SubgraphPythonWrapperCodegen(PythonWrapperCodegen): + """ + A wrapper codegen that generates code for a subgraph. For most of the + methods, we rely on the implementation in the PythonWrapperCodegen. But we + override a few functions to produce cleaner code (like avoiding writing + imports twice in the output code) + """ + + def __init__( + self, + subgraph_name: str, + parent_wrapper: PythonWrapperCodegen, + partition_signatures: ir.GraphPartitionSignature | None = None, + ): + # It is necessary to set the subgraph_name before calling super __init__ + # because __init__ calls set_launcher_fn_name + self.subgraph_name = subgraph_name + self.parent_wrapper = parent_wrapper + self.partition_signatures = partition_signatures + + super().__init__() + + root = self.get_root_graph() + # Only generate auto-tuning block in the main graph + self.kernel_autotune_defs = root.kernel_autotune_defs + self.kernel_autotune_calls = root.kernel_autotune_calls + # Only store kernel src to name mapping in the main graph + self.src_to_kernel = root.src_to_kernel + # Same here, only define user-defined Triton kernels in the main graph + self.user_defined_kernel_cache = root.user_defined_kernel_cache + + def set_launcher_fn_name(self) -> None: + # This sets up the name of the function containing the launcher code of + # the subgraph. + # pyrefly: ignore [bad-assignment] + self.launcher_fn_name = self.subgraph_name + + def write_header(self) -> None: + pass + + def add_benchmark_harness(self, output): + pass + + def benchmark_compiled_module(self, output): + pass + + def write_async_compile_wait(self): + pass + + def next_kernel_suffix(self) -> str: + # Ensures that subgraphs kernels do not clash with each other + return self.parent_wrapper.next_kernel_suffix() + + def generate_after_suffix(self, result: IndentedBuffer) -> None: + return + + def write_launcher_fn_call_get_indent(self) -> int: + self.prefix.splice( + f""" + def {self.launcher_fn_name}(args): + """ + ) + prefix_indent = 1 + return prefix_indent + + def get_wrapper_call_indent(self) -> int: + return 1 + + def get_graph_inputs( + self, + ) -> dict[str, ir.TensorBox | ir.TorchBindObject | sympy.Expr | None]: + if signature := self.partition_signatures: + inputs = signature.input_nodes | { + str(s): s for s in signature.symbol_inputs + } + else: + inputs = V.graph.graph_inputs + return inputs + + def get_graph_input_names(self) -> list[str]: + if signature := self.partition_signatures: + names = list(signature.input_nodes.keys()) + [ + symbol_input.name for symbol_input in signature.symbol_inputs + ] + else: + names = V.graph.graph_input_names + return names + + def get_graph_outputs(self) -> list[IRNode]: + if signature := self.partition_signatures: + outputs = signature.output_nodes + else: + outputs = V.graph.graph_outputs + return outputs + + def codegen_allocation(self, buffer: ir.Buffer): + name = buffer.get_name() + if (signature := self.partition_signatures) and name in signature.input_nodes: + # skip allocation if buffer is a subgraph input. + # This allows reusing an input buffer in graph partition, + # although this is not allowed in general. + return + + super().codegen_allocation(buffer) + + def _write_get_raw_stream( + self, device_idx: int, graph: GraphLowering | None = None + ) -> str: + self.write_triton_header_once() + # pyre-fixme[16]: scheduler.current_stream_name added in scheduler commit + if (current_stream_name := V.graph.scheduler.current_stream_name) is not None: + name = f"{current_stream_name}_raw" + self.writeline(f"{name} = {current_stream_name}.cuda_stream") + else: + name = get_raw_stream_name(device_idx) + self.writeline(f"{name} = get_raw_stream({device_idx})") + return name + + def codegen_graph_nvtx_range_push(self, post_grad_graph_id: int) -> None: + """Generate NVTX range push for graph.""" + self.writeline(f"torch.cuda.nvtx.range_push('graph {post_grad_graph_id}')") + + def codegen_graph_nvtx_range_pop(self) -> None: + """Generate NVTX range pop for graph.""" + self.writeline("torch.cuda.nvtx.range_pop()") + + @cache_on_self + def write_triton_header_once(self) -> None: + # TODO: Uncomment in future. This will be needed to support subgraph + # codegen for cpp wrapper. + # if config.triton.autotune_at_compile_time: + # import_str = self.triton_header_str() + # self.kernel_autotune_calls.splice(import_str) + self.parent_wrapper.write_triton_header_once() + + @cache_on_self + def write_get_raw_stream_header_once(self) -> None: + # TODO: Uncomment in future. This will be needed to support subgraph + # codegen for cpp wrapper. + # if config.triton.autotune_at_compile_time: + # self.kernel_autotune_calls.writeline( + # V.graph.device_ops.import_get_raw_stream_as("get_raw_stream") + # ) + self.parent_wrapper.write_get_raw_stream_header_once() + + @cache_on_self + def get_root_graph(self) -> PythonWrapperCodegen: + root: PythonWrapperCodegen | SubgraphPythonWrapperCodegen = self + while isinstance(root, SubgraphPythonWrapperCodegen): + root = root.parent_wrapper + + assert isinstance(root, PythonWrapperCodegen) + return root + + def generate_and_run_autotune_block(self): + # Only execute auto-tuning block in the main graph + pass diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/wrapper_fxir.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/wrapper_fxir.py new file mode 100644 index 0000000000000000000000000000000000000000..4a1cce8bdb964c4feabb62375714676701db67a5 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/wrapper_fxir.py @@ -0,0 +1,1251 @@ +import dataclasses +import functools +import logging +import operator +import textwrap +from collections import Counter +from collections.abc import Callable, Iterable, Sequence +from typing import Any + +import sympy + +import torch +from torch._export.passes._node_metadata_hook import ( + _node_metadata_hook, + _set_node_metadata_hook, +) +from torch._higher_order_ops.triton_kernel_wrap import ( + TraceableTritonKernelWrapper, + tracing_triton_hopifier_singleton, + triton_kernel_wrapper_mutation, +) +from torch._inductor.codecache import LambdaFuture, PyCodeCache +from torch._inductor.runtime.triton_heuristics import CachingAutotuner +from torch._inductor.select_algorithm import extern_kernels # noqa: F401 +from torch._inductor.utils import convert_to_symint +from torch._inductor.virtualized import V +from torch._library.triton import wrap_triton +from torch.fx import GraphModule +from torch.fx.experimental.symbolic_shapes import ( + CallMethodKey, + ConvertIntKey, + DivideByKey, +) +from torch.utils import _pytree as pytree +from torch.utils._ordered_set import OrderedSet +from torch.utils._sympy.functions import FloorDiv +from torch.utils._sympy.interp import _run_sympy_handler, sympy_interp +from torch.utils._sympy.reference import OptimizedPythonReferenceAnalysis +from torch.utils._sympy.solve import try_solve + +from .. import config, ir +from ..runtime.triton_compat import Config +from ..utils import cache_property_on_self, LineContext, ValueWithLineMap +from .common import ( + CodegenSymbol, + FileBackedGraphModule, + WorkspaceArg, + WorkspaceZeroMode, +) +from .wrapper import ( + AllocateLine, + BufferLike, + CommentLine, + ConditionalLine, + DynamicScalarLine, + EnterDeviceContextManagerLine, + EnterSubgraphLine, + ExitDeviceContextManagerLine, + ExitSubgraphLine, + ExternKernelAllocLine, + ExternKernelOutLine, + FreeIfNotReusedLine, + FreeLine, + IndexPutFallbackLine, + KernelCallLine, + KernelDefinitionLine, + Line, + MultiOutputLine, + NullLine, + PythonWrapperCodegen, + ReinterpretLine, + ReuseLine, + ScatterFallbackLine, + SubgraphPythonWrapperCodegen, + SymbolicCallArg, + SymbolicCallArgLine, + UnbackedSymbolDefsLine, + WrapperLine, +) + + +aten = torch.ops.aten +log = logging.getLogger(__name__) + + +@dataclasses.dataclass +class SymbolBuffer(CodegenSymbol): + """ + Represents a sympy.Symbol graph input. + """ + + symbol: sympy.Symbol + + def get_name(self) -> str: + return str(self.symbol) + + def get_example(self) -> torch.Tensor | torch.SymInt: + sym_int = convert_to_symint(self.symbol) + assert isinstance(sym_int, torch.SymInt) + return sym_int + + +CodegenBuffer = BufferLike | SymbolBuffer + + +@dataclasses.dataclass +class TritonKernel: + """ + Stores metadata about Triton kernels for use in FX. + """ + + tuner: CachingAutotuner + wrapped: TraceableTritonKernelWrapper + + +def replace_floor_div(expr: sympy.Expr) -> sympy.Expr: + """ + Replace sympy.floor with FloorDiv. + """ + + def replace(expr: sympy.Expr) -> sympy.Expr: + expr = sympy.together(expr) + + # Division is represented as a Mul with a Rational factor or a Pow with negative + # exponent. We convert floor(Mul(...)) to FloorDiv(numerator, denominator) by + # partitioning factors into the numerator and denominator. + (numerator, denominator) = (sympy.S.One,) * 2 + for arg in sympy.Mul.make_args(expr): + if isinstance(arg, sympy.Rational): + numerator *= arg.numerator + denominator *= arg.denominator + elif isinstance(arg, sympy.Pow) and arg.exp.is_negative: + denominator *= arg.base**-arg.exp + else: + numerator *= arg + + return FloorDiv(numerator, denominator) + + return expr.replace(sympy.floor, replace) + + +class WrapperFxCodegen(PythonWrapperCodegen): + """ + Backend to generate wrapper code as an FX IR graph. + """ + + supports_caching = False + + def __init__(self, *args: Any, **kwargs: Any): + super().__init__(*args, **kwargs) + self.subgms: dict[str, torch.fx.GraphModule] = {} + + def codegen_inputs(self) -> None: + """ + This would generate code for symbolic input shapes, strides, etc. + Since the FX converter handles this, do nothing here. + """ + + def codegen_conditional(self, conditional: ir.Conditional) -> None: + """ + Conditional codegen normally emits a number of different wrapper lines. + Instead, FX conversion uses a dedicated line for the whole conditional. + """ + self.writeline(ConditionalLine(self, conditional)) + for subgraph in (conditional.true_subgraph, conditional.false_subgraph): + self.codegen_subgraph_common(subgraph) + + def define_subgraph_launcher_fn( + self, name: str, subgraph_code: ValueWithLineMap | FileBackedGraphModule + ) -> None: + """ + Record subgms as they're generated. + """ + assert isinstance(subgraph_code, FileBackedGraphModule) + self.subgms[name] = subgraph_code.gm + + @property + @cache_property_on_self + def is_subgraph(self) -> bool: + return isinstance(self, SubgraphPythonWrapperCodegen) + + def get_fx_graph_inputs( + self, + ) -> dict[str, ir.TensorBox | ir.TorchBindObject | sympy.Expr | None]: + """ + Get the input nodes corresponding to FX graph placeholders. + """ + + if V.aot_compilation and not self.is_subgraph: + # AOT graphs must match the signature of the input module. + return { + node.name: V.graph.graph_inputs.get(node.name) + for node in V.graph.module.graph.find_nodes(op="placeholder") # type: ignore[operator, union-attr] + } + + return self.get_graph_inputs() + + def _generate(self, is_inference: bool) -> tuple[FileBackedGraphModule, None]: + self.run_wrapper_ir_passes(is_inference) + + prologue = "\n".join( + [ + self.imports.getvalue(), + self.header.getvalue(), + ] + ) + gm = FxConverter( + lines=self.lines, + prologue=prologue, + graph_inputs=self.get_fx_graph_inputs(), + graph_outputs=self.get_graph_outputs(), + subgms=self.subgms, + is_subgraph=self.is_subgraph, + ).generate() + + compiled_fn = self.compile_graph(gm) + + return FileBackedGraphModule(gm, compiled_fn), None + + def compile_graph(self, gm: GraphModule) -> Callable[..., Any]: + """ + Converts the graph module into a runnable function. The default implementation + is simply an interpreter calling kernels in eager mode. Derived backends can + override this to do further compilation. + """ + return gm.forward + + def write_header(self) -> None: + """ + Python subgraphs normally lack headers. + Override this behavior to generate prologues for FX subgraphs. + """ + PythonWrapperCodegen.write_header(self) + + def register_alignment_check_inputs(self) -> None: + """FXIR does not emit deferred alignment copies. + Alignment is handled by the runtime wrapper.""" + + def codegen_deferred_alignment_copies(self, input_names: Iterable[str]) -> None: + """FXIR does not emit deferred alignment copies.""" + + @classmethod + def create( + cls: type["WrapperFxCodegen"], + is_subgraph: bool, + subgraph_name: str | None, + parent_wrapper: PythonWrapperCodegen | None, + partition_signatures: ir.GraphPartitionSignature | None = None, + ) -> "WrapperFxCodegen": + if is_subgraph: + assert subgraph_name is not None + assert parent_wrapper is not None + + # Subgraphs override some methods of PythonWrapperCodegen. + # Apply these overrides to the user-provided class, with priority given to + # user-provided methods. + class SubgraphFxWrapperCodegen(cls, SubgraphPythonWrapperCodegen): # type: ignore[misc,valid-type] + def compile_graph(self, gm: GraphModule) -> Callable[..., Any]: + """ + Skip graph compilation for subgraphs. + """ + + def crash_if_run(*args: Any) -> None: + raise NotImplementedError("Cannot run a subgraph in isolation!") + + return crash_if_run + + return SubgraphFxWrapperCodegen( + subgraph_name, parent_wrapper, partition_signatures + ) + + return cls() + + +@dataclasses.dataclass +class FxConverter: + """ + Generates FX IR from Wrapper IR. As each instance is only meant to be used once, the + input and output code are stored as attributes. + """ + + lines: list[Line] + prologue: str + graph_inputs: dict[str, ir.TensorBox | ir.TorchBindObject | sympy.Expr | None] + graph_outputs: list[ir.IRNode] + subgms: dict[str, torch.fx.GraphModule] + is_subgraph: bool + + def __post_init__(self) -> None: + graph = torch.fx.Graph() + self.gm = GraphModule({}, graph) # Wrapper FX IR. + self.buffer_to_node: dict[ + str | None, torch.fx.Node + ] = {} # Symbol table for codegen. + self.kernels: dict[str, TritonKernel] = {} # Table to store Triton kernels. + self._unique_symbol_ids: Counter[str] = Counter() + self.tracer = torch.fx.proxy.GraphAppendingTracer(graph) + self.expr_to_proxy: dict[sympy.Expr, torch.fx.Proxy] = {} + + def _import_kernel(self, code: str, kernel_name: str) -> CachingAutotuner: + """ + Imports a kernel from source, possibly autotuning block parameters. + """ + module_code = "\n".join([self.prologue, code]) + mod = PyCodeCache.load(module_code) + kernel = getattr(mod, kernel_name) + + if isinstance(kernel, LambdaFuture): + kernel = kernel.result() + + if not isinstance(kernel, CachingAutotuner): + raise NotImplementedError( + textwrap.dedent(f""" + Unsupported type for kernel {kernel_name}: {type(kernel)}. + FX conversion only supports Triton kernels. + """) + ) + + return kernel + + def _create_as_strided( + self, + input_node: torch.fx.Node, + size: tuple[Any, ...], + stride: tuple[Any, ...], + offset: int | sympy.Expr, + ) -> torch.fx.Node: + return self.gm.graph.call_function( + torch.as_strided, + args=( + input_node, + self._generate_sym_nodes(size), + self._generate_sym_nodes(stride), + self._generate_sym_node(offset), + ), + ) + + def _record_allocation(self, buffer: CodegenBuffer, node: torch.fx.Node) -> None: + """ + Updates the symbol table to record that an Inductor buffer maps to the result of + an FX node. + """ + assert node not in self.buffer_to_node + self.buffer_to_node[buffer.get_name()] = node + + def _free(self, buffer: CodegenBuffer | ir.TorchBindObject) -> None: + """ + Removes the buffer from the symbol table. + """ + name = buffer.get_name() + del self.buffer_to_node[name] + + def _lookup_args(self, args: tuple[Any, ...]) -> tuple[Any, ...]: + """ + Maps call args back to FX nodes. + """ + return tuple( + self.buffer_to_node[arg] + if isinstance(arg, str) + else arg.inner_expr + if isinstance(arg, SymbolicCallArg) + else arg + for arg in args + ) + + def _get_buffer(self, node: ir.IRNode) -> CodegenBuffer: + """ + Extract buffer data from an IR node. + """ + if isinstance(node, (ir.Buffer, WorkspaceArg)): + return node + elif isinstance(node, (ir.BaseView, ir.MutableBox)): + return self._get_buffer(node.data) + elif isinstance(node, sympy.Symbol): + return SymbolBuffer(node) + else: + raise NotImplementedError(f"Unable to extract buffer from node: {node}") + + def _generate_size_proxy( + self, node: torch.fx.Node, expr: sympy.Expr + ) -> torch.fx.Proxy: + proxy = torch.fx.Proxy(node, tracer=self.tracer) + self.expr_to_proxy[expr] = proxy + return proxy + + def _generate_graph_inputs(self) -> None: + """ + Converts graph inputs to FX placeholders. + """ + + for name, ir_node in self.graph_inputs.items(): + if ir_node is None: + # Create dummy input nodes to match the input signature + self.gm.graph.placeholder(name) + continue + + # Introduce a new symbol for constant inputs. + is_constant = isinstance(ir_node, (int, float, sympy.Integer, sympy.Float)) + buffer = ( + SymbolBuffer(sympy.Symbol(name, is_integer=True)) + if is_constant + else self._get_buffer(ir_node) + ) + placeholder_node = self.gm.graph.placeholder(buffer.get_name()) + placeholder_node.meta["val"] = ( + ir_node if is_constant else buffer.get_example() + ) + self._record_allocation(buffer, placeholder_node) + + # Record symbol definitions for dynamic shapes. + if isinstance(ir_node, sympy.Symbol): + self._generate_size_proxy(placeholder_node, ir_node) + + def _generate_graph_input_shapes(self) -> None: + """ + Generate nodes creating symints that are part of graph input + shape/strides. + """ + + def _codegen_symbol( + sym_or_exp: sympy.Symbol | sympy.Expr, + base_node: torch.fx.Node, + target: torch._ops.OpOverload, + dim: int, + ) -> None: + def codegen_proxy() -> torch.fx.Proxy: + size_node = self.gm.graph.call_function(target, (base_node, dim)) + size_proxy = self._generate_size_proxy(size_node, sym_or_exp) + return size_proxy + + if isinstance(sym_or_exp, sympy.Symbol): + if sym_or_exp in self.expr_to_proxy: + return + codegen_proxy() + + elif isinstance(sym_or_exp, sympy.Integer): + return + + elif isinstance(sym_or_exp, sympy.Expr): + # Check if we need to solve for an undefined symbol. + undefined_symbols = [ + sym + for sym in sym_or_exp.free_symbols + if sym not in self.expr_to_proxy + ] + if len(undefined_symbols) == 0: + self._sympy_interp(sym_or_exp) + return + elif len(undefined_symbols) > 1: + raise ValueError(f"Underdetermined input expression: {sym_or_exp}") + + # Define a new symbol for the input size. + size_proxy = codegen_proxy() + size_symbol = sympy.Symbol( + size_proxy.node.name, integer=True, nonnegative=True + ) + self.expr_to_proxy[size_symbol] = size_proxy + + # Solve for the undefined symbol. + undefined_symbol = undefined_symbols[0] + solution = try_solve( + sympy.Eq(sym_or_exp, size_symbol), undefined_symbol + ) + if solution is None: + raise ValueError(f"Cannot solve input expression: {sym_or_exp}") + + # Since the symbol is a size, it must be an integer. + # Therefore, we can convert division to FloorDiv. + undefined_symbol_expr = solution[1] + if undefined_symbol.is_integer: + undefined_symbol_expr = replace_floor_div( + sympy.floor(undefined_symbol_expr) + ) + + # Generate FX for the symbol. + self._sympy_interp(undefined_symbol_expr) + self.expr_to_proxy[undefined_symbol] = self.expr_to_proxy[ + undefined_symbol_expr + ] + + for ir_node in self.graph_inputs.values(): + if isinstance(ir_node, ir.TensorBox): + buffer = self._get_buffer(ir_node) + placeholder_node = self.buffer_to_node[buffer.get_name()] + + for dim, size in enumerate(ir_node.get_size()): + _codegen_symbol( + size, placeholder_node, torch.ops.aten.sym_size.int, dim + ) + for dim, stride in enumerate(ir_node.get_stride()): + _codegen_symbol( + stride, placeholder_node, torch.ops.aten.sym_stride.int, dim + ) + + def _generate_graph_constants(self) -> None: + for name, value in V.graph.constants.items(): + node = self.gm.graph.get_attr(name) + node.meta["val"] = value + setattr(self.gm, name, value) + self.buffer_to_node[name] = node + + def _generate_buffer(self, node: ir.IRNode) -> torch.fx.Node | None: + """ + Generates FX IR for transformations on a buffer, such as ReinterpretView. + Does nothing if no such transformations are present. + """ + + if isinstance(node, ir.ShapeAsConstantBuffer): + # Generate FX nodes to compute the shape expression. + return self._sympy_interp(node.expr).node + + def generate_to_buffer(node: ir.IRNode) -> BufferLike | None: + if isinstance(node, (ir.Buffer, WorkspaceArg)): + return node + elif isinstance(node, ir.NoneAsConstantBuffer): + return None + elif isinstance(node, ir.MutableBox): + return generate_to_buffer(node.data) + elif isinstance(node, ir.ReinterpretView): + # We need to introduce a new symbol if the output is a ReinterpretView. + # Use a WorkspaceArg for this. + buffer = self._get_buffer(node.data) + assert isinstance(buffer, (ir.Buffer, WorkspaceArg)) + unique_name = self.gm.graph._graph_namespace.create_name( + f"{buffer.get_name()}_view", None + ) + device = buffer.get_device() + assert device + reused_as = WorkspaceArg( + count=buffer.get_size(), + zero_mode=WorkspaceZeroMode.UNINITIALIZED, + device=device, + outer_name=unique_name, + dtype=buffer.get_dtype(), + ) + + # Generate FX IR for the view. + self._generate_reinterpret_helper(buffer, reused_as, node.layout) + + return reused_as + else: + raise NotImplementedError(f"Unrecognized buffer/view node: {node}") + + buffer = generate_to_buffer(node) + return self.buffer_to_node[buffer.get_name()] if buffer is not None else None + + def _generate_outputs( + self, + ) -> torch.fx.Node | None | list[torch.fx.Node | None]: + """ + Generate FX IR for graph outputs. + """ + output_nodes = [ + self._generate_buffer(node) for idx, node in enumerate(self.graph_outputs) + ] + + # Parent graphs with single return elements don't use a tuple. + output_value = ( + output_nodes[0] + if len(output_nodes) == 1 and not self.is_subgraph + else output_nodes + ) + + return output_value + + def _generate_subgm_getattrs(self) -> None: + """ + Generate getattr nodes for subgms. + """ + + def generate_getattr(name: str, subgm: torch.fx.GraphModule) -> torch.fx.Node: + self.gm.add_submodule(name, subgm) + node = self.gm.graph.get_attr(name) + node.meta["val"] = subgm + return node + + self.subgm_getattrs = { + name: generate_getattr(name, subgm) for name, subgm in self.subgms.items() + } + + def _get_subgm_attr(self, subgraph: ir.Subgraph) -> torch.fx.Node: + """ + Look up the getattr node for a subgraph. + """ + graph = subgraph.graph + assert graph is not None + return self.subgm_getattrs[graph.name] + + def generate(self) -> torch.fx.GraphModule: + """ + Main entrypoint for FX codegen. + """ + self._generate_graph_inputs() + self._generate_graph_constants() + self._generate_subgm_getattrs() + + with _set_node_metadata_hook( + self.gm, + functools.partial(_node_metadata_hook, fake_mode=V.fake_mode), + ): + self._generate_graph_input_shapes() + + # Generate FX IR from Wrapper IR lines. + for line in self.lines: + if isinstance(line, WrapperLine): + line.codegen_fx(self)(line) + elif isinstance(line, LineContext): + # Ignore line context in FX IR. + pass + else: + raise NotImplementedError( + textwrap.dedent( + f""" + Found line of unrecognized type '{type(line)}': + '{line}' + + FX conversion only supports Wrapper IR lines. + """ + ) + ) + + output = self._generate_outputs() + + self.gm.graph.output(output) + self.gm.recompile() + return self.gm + + def _sympy_interp(self, expr: sympy.Expr) -> torch.fx.Proxy: + # hash cons + if expr in self.expr_to_proxy: + return self.expr_to_proxy[expr] + # base cases, don't cache + if isinstance( + expr, + ( + sympy.Integer, + sympy.Number, + sympy.Symbol, + sympy.logic.boolalg.BooleanAtom, + ), + ): + return sympy_interp( + OptimizedPythonReferenceAnalysis, self.expr_to_proxy, expr + ) + + # hash cons on arguments, run expr handler + self.expr_to_proxy[expr] = _run_sympy_handler( + OptimizedPythonReferenceAnalysis, + [self._sympy_interp(arg) for arg in expr.args], + expr, + ) + return self.expr_to_proxy[expr] + + def _generate_sym_node(self, s: int | sympy.Expr) -> int | torch.fx.Node: + if isinstance(s, (int, sympy.Integer)): + return int(s) + elif isinstance(s, sympy.Symbol): + assert s in self.expr_to_proxy, ( + f"Could not find a node corresponding to the symbol {s}" + ) + return self.expr_to_proxy[s].node + elif isinstance(s, sympy.Expr): + return self._sympy_interp(s).node + + elif isinstance(s, torch.fx.Node): + return s + + else: + raise ValueError(f"{s} of type {type(s)} is not a valid input") + + def _generate_sym_nodes( + self, shape: Sequence[sympy.Expr] + ) -> list[int | torch.fx.Node]: + return [self._generate_sym_node(s) for s in shape] + + def _generate_allocate(self, line: WrapperLine) -> None: + assert isinstance(line, AllocateLine) + buffer = line.node + name = buffer.get_name() + assert name not in V.graph.removed_buffers + + device = buffer.get_device() + assert device + dtype = buffer.get_dtype() + shape = self._generate_sym_nodes(buffer.get_size()) + stride = self._generate_sym_nodes(buffer.get_stride()) + + node = self.gm.graph.call_function( + torch.empty_strided, + args=(shape, stride), + kwargs={"dtype": dtype, "device": device.type}, + ) + assert name + node.name = name + self._record_allocation(buffer, node) + + def _generate_conditional(self, line: WrapperLine) -> None: + assert isinstance(line, ConditionalLine) + + def get_subgm_attr(subgraph: ir.Subgraph | None) -> torch.fx.Node: + assert subgraph is not None + return self._get_subgm_attr(subgraph) + + # Access the subgraphs as getattrs. + ir_node = line.node + (true_subgm, false_subgm) = [ + get_subgm_attr(subgraph) + for subgraph in (ir_node.true_subgraph, ir_node.false_subgraph) + ] + + def generate_buffer(node: ir.IRNode | None) -> torch.fx.Node | None: + assert node is not None + return self._generate_buffer(node) + + predicate = generate_buffer(ir_node.predicate) + assert ir_node.operands is not None + operands = tuple(generate_buffer(arg) for arg in ir_node.operands) + fx_node = self.gm.graph.call_function( + torch.ops.higher_order.cond, + args=(predicate, true_subgm, false_subgm, operands), + ) + self._record_allocation(ir_node, fx_node) + + def _generate_assert_size_stride(self, line: WrapperLine) -> None: + pass + + def _generate_comment(self, line: WrapperLine) -> None: + assert isinstance(line, CommentLine) + # We ignore comments in FX IR. + + def _generate_dynamic_scalar(self, line: WrapperLine) -> None: + assert isinstance(line, DynamicScalarLine) + + ir_node = line.node + (input_ir_node,) = ir_node.inputs + assert isinstance(input_ir_node, ir.IRNode) + input_fx_node = self._generate_buffer(input_ir_node) + keypath = ir_node.keypath + graph = self.gm.graph + + def generate_item(x: torch.fx.Node | None) -> torch.fx.Node: + assert x is not None + return graph.call_function( + aten.item.default, + args=(x,), + ) + + if len(keypath) == 0: + result_fx_node = generate_item(input_fx_node) + elif len(keypath) == 1 and isinstance(keypath[0], ConvertIntKey): + where_fx_node = graph.call_function( + aten.where.Scalar, + args=(input_fx_node, 1, 0), + ) + result_fx_node = generate_item(where_fx_node) + else: + raise NotImplementedError(f"Unsupported keypath: {keypath}") + + result_symbol = ir_node.sym + result_buffer = SymbolBuffer(result_symbol) + self._record_allocation(result_buffer, result_fx_node) + self._generate_size_proxy(result_fx_node, result_symbol) + + def _generate_enter_device_context_manager(self, line: WrapperLine) -> None: + assert isinstance(line, EnterDeviceContextManagerLine) + # We ignore the device context in FX IR. + + def _generate_exit_device_context_manager(self, line: WrapperLine) -> None: + assert isinstance(line, ExitDeviceContextManagerLine) + # We ignore the device context in FX IR. + + def _generate_enter_subgraph(self, line: WrapperLine) -> None: + assert isinstance(line, EnterSubgraphLine) + # We ignore memory planning lines in FX IR. + + def _generate_exit_subgraph(self, line: WrapperLine) -> None: + assert isinstance(line, ExitSubgraphLine) + # We ignore memory planning lines in FX IR. + + def _generate_free(self, line: WrapperLine) -> None: + assert isinstance(line, FreeLine) + + buf = line.node + + # No need to free placeholders. + if self.buffer_to_node[buf.get_name()].op == "placeholder": + return + + self._free(buf) + + def _generate_free_if_not_reused(self, line: WrapperLine) -> None: + assert isinstance(line, FreeIfNotReusedLine) + buf = line.node + assert buf.get_name() not in V.graph.removed_buffers + if not line.is_reused: + self._free(buf) + + def _generate_line_context(self, line: WrapperLine) -> None: + assert isinstance(line, LineContext) + # We ignore line context in FX IR. + + def _generate_reinterpret(self, line: WrapperLine) -> None: + assert isinstance(line, ReinterpretLine) + self._generate_reinterpret_helper(line.node, line.reused_as, line.layout) + + def _generate_reinterpret_helper( + self, input_buffer: BufferLike, result_buffer: BufferLike, layout: ir.Layout + ) -> None: + input_node = self.buffer_to_node[input_buffer.get_name()] + + # Look up output metadata. + name = result_buffer.get_name() + assert name + size = tuple(layout.size) + stride = tuple(layout.stride) + if isinstance(layout, ir.NonOwningLayout): + # Look up the view's layout. + view = layout.view + assert isinstance(view, ir.ReinterpretView), ( + f"unexpected type: {type(view)}" + ) + layout = view.layout + offset = input_buffer.get_offset() + layout.offset + + # Map ReinterpretView to as_strided. + result_node = self._create_as_strided(input_node, size, stride, offset) + result_node.name = name + self._record_allocation(result_buffer, result_node) + + def _generate_reuse(self, line: WrapperLine) -> None: + assert isinstance(line, ReuseLine) + old = line.node + new = line.reused_as + assert not any(buf.get_name() in V.graph.removed_buffers for buf in (old, new)) + assert old.get_dtype() == new.get_dtype() + + old_node = self.buffer_to_node[old.get_name()] + result_node = old_node + + # Change shape and stride. + size = tuple(new.get_size()) + stride = tuple(new.get_stride()) + offset = new.get_offset() + if ( + tuple(old.get_size()) != size + or tuple(old.get_stride()) != stride + or old.get_offset() != offset + ): + result_node = self._create_as_strided(old_node, size, stride, offset) + + self._record_allocation(new, result_node) + + # Free the old buffer, if we allocated a new tensor. + if ( + old.get_name() not in V.graph.get_output_names() + and line.delete_old + and result_node is not old_node + ): + self._free(old) + + def _generate_multi_output(self, line: WrapperLine) -> None: + assert isinstance(line, MultiOutputLine) + + arg_node = self.buffer_to_node[line.arg_name] + + # For non-tuple / non-list outputs, map the + # output to the same node as the input. + if len(line.indices) == 0: + self.buffer_to_node[line.result_name] = arg_node + return + + # Extract the index for tuple access. + inds = line.indices[0][1:] + assert len(inds) == 1, f"Cannot convert {inds} to an index." + idx = inds[0] + + node = self.gm.graph.call_function(operator.getitem, args=(arg_node, idx)) + node.name = line.result_name + self.buffer_to_node[line.result_name] = node + + def _generate_fallback_call( + self, + ir_node: ir.ExternKernel, + args: tuple[Any, ...] | None = None, + kwargs: dict[str, Any] | None = None, + ) -> None: + fx_node = self.gm.graph.call_function( + ir_node.op_overload, # type: ignore[arg-type] + args=args, + kwargs=kwargs, + ) + result_buffer = ir_node.codegen_reference() + self.buffer_to_node[result_buffer] = fx_node + # For in-place mutation ops (e.g., scatter_reduce_, index_put_), + # update the buffer mapping for mutated inputs so downstream + # references to the mutated buffer see the post-mutation node. + for mutated_name in ir_node.get_mutation_names(): + self.buffer_to_node[mutated_name] = fx_node + + def _generate_index_put_fallback(self, line: WrapperLine) -> None: + assert isinstance(line, IndexPutFallbackLine) + ir_node = line.node + + def generate_buffer_or_none( + x: ir.IRNode | Sequence[ir.IRNode] | None, + ) -> torch.fx.Node | None: + """ + Handles None before calling _generate_buffer. + """ + if x is None: + return None + + assert isinstance(x, ir.IRNode) + return self._generate_buffer(x) + + (x, values) = [generate_buffer_or_none(t) for t in ir_node.inputs[:2]] + indices = tuple(generate_buffer_or_none(t) for t in line.indices) + accumulate = ir_node.constant_args[0] + args = (x, indices, values, accumulate) + self._generate_fallback_call(ir_node, args) + + def _generate_scatter_fallback(self, line: WrapperLine) -> None: + assert isinstance(line, ScatterFallbackLine) + ir_node = line.node + assert ir.is_node_sequence(ir_node.inputs) + (x, index, src) = [self._generate_buffer(t) for t in ir_node.inputs] + ( + [] if ir_node.src_is_tensor else [ir_node.constant_args[1]] + ) + args = (x, ir_node.constant_args[0], index, src) + kwargs = {} + if reduce := ir_node.kwargs.get("reduce"): + kwargs["reduce"] = reduce + # Only pass kwargs that the op's schema actually accepts, since + # ScatterFallback stores both reduce and include_self for all + # scatter variants, but not all ops support them (e.g., + # scatter_.value has no kwargs, scatter_reduce_.two has both). + assert isinstance(ir_node.op_overload, torch._ops.OpOverload) + schema_arg_names = OrderedSet( + [a.name for a in ir_node.op_overload._schema.arguments] + ) + kwargs = {k: v for k, v in ir_node.kwargs.items() if k in schema_arg_names} + + self._generate_fallback_call(ir_node, args, kwargs) + + def _generate_null(self, line: WrapperLine) -> None: + assert isinstance(line, NullLine) + # Does nothing. + + def _generate_comm_buffer_allocate(self, line: WrapperLine) -> None: + assert isinstance(line, AllocateLine) and line.comm_buffer + raise NotImplementedError("Comm buffer allocation is not yet supported") + + def _generate_comm_buffer_free(self, line: WrapperLine) -> None: + assert isinstance(line, FreeIfNotReusedLine) and line.comm_buffer + self._free(line.node) + + def _generate_triton_call(self, line: WrapperLine) -> None: + assert isinstance(line, KernelCallLine) + + # Collect all kwargs, including autotuned block sizes. + call_args = self._lookup_args(line.call_args) + kernel = self.kernels[line.kernel_name] + tuner = kernel.tuner + + def tune_kernel(tuner: CachingAutotuner, call_args: Sequence[Any]) -> None: + from triton.runtime import driver + + log.info("Autotuning Triton kernel %s at compile time.", kernel_name) + + device = driver.active.get_current_device() + + stream = driver.active.get_current_stream(device) + + def node_to_tuning_arg(arg: Any) -> Any: + """ + Create real tensors for autotuning arguments, substituting size hints + for dynamic shapes. + """ + + def to_size_hint_sympy_int(arg: sympy.Expr | int) -> int: + return V.graph.sizevars.optimization_hint(arg) + + def to_size_hint_list(arg: list[torch.SymInt | int]) -> list[int]: + args_sympy = [ + x.node.expr if isinstance(x, torch.SymInt) else x for x in arg + ] + return pytree.tree_map(to_size_hint_sympy_int, args_sympy) + + if not isinstance(arg, torch.fx.Node): + return to_size_hint_sympy_int(arg) + + fake = arg.meta["val"] + return torch.empty_strided( + to_size_hint_list(fake.shape), + to_size_hint_list(fake.stride()), + dtype=fake.dtype, + device=device, + ).zero_() + + # call args can be fx nodes or sympy expressions or integers! + arg_values = [node_to_tuning_arg(arg) for arg in call_args] + tuner.run(*arg_values, stream=stream) + + # Optionally autotune the kernels. + # The FX backend currently only supports compile-time tuning. + kernel_name = tuner.fn.__name__ + if config.triton.autotune_at_compile_time: + # Skip compile-time autotuning if any unbacked symbol lacks a user-provided + # optimization hint — autotuning with the generic fallback would + # produce meaningless results. + hinted = V.graph.sizevars.all_unbacked_explicitly_hinted + can_tune = True + for arg in call_args: + if isinstance(arg, torch.fx.Node): + fake = arg.meta["val"] + if not hinted(list(fake.shape) + list(fake.stride())): + can_tune = False + break + elif not hinted(arg): + can_tune = False + break + if can_tune: + tune_kernel(tuner, call_args) + else: + log.info( + "Detected unhinted unbacked symints. Skipping compile-time autotuning for kernel %s.", + kernel_name, + ) + else: + log.info( + "Skipping autotuning for kernel %s. Set config.triton.autotune_at_compile_time = True to enable.", + kernel_name, + ) + + triton_meta = tuner.triton_meta + signature = triton_meta["signature"] + + def add_constants_to_call_args( + call_args: Sequence[Any], cfg: Config + ) -> tuple[Any, ...]: + """ + Add constant kwargs to the arg list. + """ + # Add args from the proper Triton signature. + # Exclude constants and config kwargs, as those are tracked separately. + new_call_args = [] + constants = triton_meta["constants"] + call_kwargs = { + key: val + for key, val in zip(signature, call_args) + # pyrefly: ignore [missing-attribute] + if key not in constants and key not in cfg.kwargs + } + + # Add constants stored as Triton metadata, in signature order. + call_kwargs |= constants + new_call_args = [ + call_kwargs[key] + for key in signature + # pyrefly: ignore [missing-attribute] + if key not in cfg.kwargs + ] + + # Add Inductor's extra launcher args to the end. + if extra_launcher_args := tuner.inductor_meta.get("extra_launcher_args"): + new_call_args.extend( + call_args[len(call_args) - len(extra_launcher_args) :] + ) + + return tuple(new_call_args) + + kernel_config = tuner.compile_results[0].config + extra_options = getattr(kernel_config, "extra_options", None) + call_args = add_constants_to_call_args(call_args, kernel_config) + call_args, grid = tuner._interpret_args_grid(call_args, kernel_config) + call_kwargs = dict(zip(signature, call_args)) + # pyrefly: ignore [missing-attribute] + assert not any(kwarg in kernel_config.kwargs for kwarg in call_kwargs), ( + f"kwargs overlap config: {call_kwargs}" + ) + # pyrefly: ignore [missing-attribute] + call_kwargs.update(kernel_config.kwargs) + + # Replace sympy.floor with FloorDiv, to make the expression traceable. + grid = [replace_floor_div(x) if isinstance(x, sympy.Expr) else x for x in grid] + wrapper_grid = [tuple(self._generate_sym_nodes(grid))] + call_kwargs = { + name: self._generate_sym_node(val) for name, val in call_kwargs.items() + } + + # Store non-graphable kwargs in the side table. + ( + call_kwargs, + constant_args_idx, + ) = tracing_triton_hopifier_singleton.store_non_graphable_args(call_kwargs) + + triton_node = self.gm.graph.call_function( + triton_kernel_wrapper_mutation, + kwargs={ + "kernel_idx": kernel.wrapped.kernel_idx, + "constant_args_idx": constant_args_idx, + "grid": wrapper_grid, + "tma_descriptor_metadata": {}, + "kwargs": call_kwargs, + }, + ) + if extra_options: + triton_node.meta["extra_options"] = extra_options + + def _generate_extern_kernel_alloc(self, line: WrapperLine) -> None: + assert isinstance(line, ExternKernelAllocLine) + node = line.node + self._generate_extern_kernel_common(node, node) + + def _generate_extern_kernel_out( + self, + line: WrapperLine, + ) -> None: + assert isinstance(line, ExternKernelOutLine) + node = line.node + out_node = node.output_view if node.output_view else node + self._generate_extern_kernel_common(node, out_node) + + def _generate_extern_kernel_common( + self, kernel: ir.ExternKernel, out_ir_node: ir.IRNode + ) -> None: + """ + Generates FX IR from either ExternKernelAlloc or ExternKernelOut. + """ + + # Get FX nodes corresponding to the call args. + assert ir.is_node_sequence(kernel.inputs) + tensor_nodes = tuple(self._generate_buffer(arg) for arg in kernel.inputs) + if hasattr(kernel, "unflatten_args"): + args, _ = kernel.unflatten_args(tensor_nodes, kernel.constant_args) + else: + args = tensor_nodes + tuple(kernel.constant_args) + + # Get the result buffer. + # Some kernels write to a pre-existing output tensor via the "out" kwarg. + # Materialize any IR nodes in kwargs to FX nodes (e.g., TensorBox -> Tensor). + kwargs = { + k: self._generate_buffer(v) if isinstance(v, ir.IRNode) else v + for k, v in kernel.kwargs.items() + } + + result_buffer: str | None = None + if isinstance(kernel, ir.ExternKernelOut): + kwargs["out"] = self.buffer_to_node[out_ir_node.codegen_reference()] + elif isinstance(kernel.layout, (ir.Layout, ir.MultiOutputLayout)): + result_buffer = kernel.get_name() + elif isinstance(kernel.layout, ir.NoneLayout): + pass + else: + raise NotImplementedError(f"Unrecognized output layout: {kernel.layout}") + + fx_node = self.gm.graph.call_function( + kernel.op_overload, # type: ignore[arg-type] + args=args, + kwargs=kwargs, + ) + + # Assign the result to the given name. + if result_buffer: + assert "out" not in kwargs, ( + f"Extern kernel '{kernel}' has both result and out kwarg. Expected only one." + ) + fx_node.name = result_buffer + self.buffer_to_node[result_buffer] = fx_node + + def _generate_kernel_call(self, line: WrapperLine) -> None: + assert isinstance(line, KernelCallLine) + if not line.triton: + raise NotImplementedError("FX conversion only supports Triton kernels.") + + self._generate_triton_call(line) + + def _generate_kernel_definition(self, line: WrapperLine) -> None: + assert isinstance(line, KernelDefinitionLine) + + # Generate code for the kernel. + kernel_code = PythonWrapperCodegen._format_kernel_definition( + line.kernel_name, line.kernel_body, metadata=line.metadata + ) + + # Import the module and store the JIT kernel. + tuner = self._import_kernel(kernel_code, line.kernel_name) + wrapped = wrap_triton(tuner.fn) + self.kernels[line.kernel_name] = TritonKernel(tuner, wrapped) + + def _generate_symbolic_call_arg(self, line: WrapperLine) -> None: + assert isinstance(line, SymbolicCallArgLine) + # Store the arg: expr mapping for later use. + arg = line.arg + + inner_expr_proxy = self._sympy_interp(arg.inner_expr) + self.expr_to_proxy[arg.inner] = inner_expr_proxy + + def _generate_unbacked_symbol_defs(self, line: WrapperLine) -> None: + assert isinstance(line, UnbackedSymbolDefsLine) + graph = self.gm.graph + + def convert_key(node: torch.fx.Node, path: pytree.KeyPath) -> torch.fx.Node: + """ + Generate FX IR for each key entry. + """ + # Base case. + if len(path) == 0: + return node + + # Process the first entry and recurse. + entry = path[0] + if isinstance(entry, CallMethodKey): + target = { + "size": aten.sym_size.int, + "stride": aten.sym_stride.int, + "storage_offset": aten.sym_storage_offset, + }[entry.name] + assert callable(target) + node = graph.call_function( + target, + args=( + (node, path[1].idx) + if len(path) > 1 and isinstance(path[1], pytree.SequenceKey) + else (node,) + ), + ) + return convert_key(node, path[1 + len(node.args) :]) + elif isinstance(entry, pytree.SequenceKey): + node = graph.call_function(operator.getitem, args=(node, entry.idx)) + return convert_key(node, path[1:]) + elif isinstance(entry, DivideByKey): + node = graph.call_function( + operator.floordiv, args=(node, entry.divisor) + ) + return convert_key(node, path[1:]) + else: + raise NotImplementedError(f"Unrecognized entry type: {type(entry)}") + + root_node = self.buffer_to_node[line.output_name] + unbacked_bindings = line.unbacked_bindings + assert unbacked_bindings is not None + for s, keypath in unbacked_bindings.items(): + # Check if we already generated this symbol. + if s.name in self.buffer_to_node: + continue + + node = convert_key(root_node, keypath) + out_buffer = SymbolBuffer(s) + self._record_allocation(out_buffer, node) + self._generate_size_proxy(node, s) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/xpu/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/xpu/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/xpu/compile_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/xpu/compile_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..e979ad570c909253409d7794e465a0f54120ed77 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/xpu/compile_utils.py @@ -0,0 +1,130 @@ +# mypy: allow-untyped-defs +import logging +import os +import shutil + +from torch._inductor import config +from torch._inductor.codegen.xpu.xpu_env import get_xpu_arch +from torch._inductor.utils import is_linux + +from ..cuda.compile_utils import _cutlass_include_paths + + +log = logging.getLogger(__name__) + + +def _sycl_compiler() -> str: + # Search order: + # 0) which icpx + # 1) config.xpu.oneapi_root + # 2) ONEAPI_ROOT environment variable + # 3) default system search PATH. + if shutil.which("icpx"): + return "icpx" + + if os.path.exists(config.xpu.oneapi_root or ""): + oneapi_root = config.xpu.oneapi_root + elif os.path.exists(os.getenv("ONEAPI_ROOT") or ""): + oneapi_root = os.getenv("ONEAPI_ROOT") + else: + oneapi_root = None + + if oneapi_root: + oneapi_inclue = os.path.join(oneapi_root, "include") + if "CPLUS_INCLUDE_PATH" in os.environ: + os.environ["CPLUS_INCLUDE_PATH"] += ":" + oneapi_inclue + else: + os.environ["CPLUS_INCLUDE_PATH"] = oneapi_inclue + return os.path.realpath(os.path.join(oneapi_root, "bin/icpx")) + else: + raise RuntimeError("Can not find Intel compiler.") + + +def _sycl_lib_options() -> list[str]: + """ + Util function for CUTLASS backend to find the correct XPU libraries. + """ + # _set_gpu_runtime_env() # cpp_extension consults the env + from torch.utils import cpp_extension + + lpaths = cpp_extension.library_paths(device_type="xpu") + extra_ldflags: list[str] = [] + if is_linux(): + for path in lpaths: + if "torch/lib" in path: + # don't want to depend on pytorch + continue + # -rpath ensures the DLL can find its dependencies when loaded, even + # if the library path is non-standard. + extra_ldflags.extend([f"-L{path}", "-Xlinker", f"-rpath={path}"]) + + extra_ldflags.append("-lsycl") + else: + raise NotImplementedError( + "Unsupported env, failed to find xpu libs! Currently only Linux is supported." + ) + return extra_ldflags + + +def _sycl_arch_as_compile_option() -> str: + arc_option_map = {"Xe12": "intel_gpu_pvc", "Xe20": "intel_gpu_bmg_g21"} + arch = get_xpu_arch() + return arc_option_map.get(arch, "intel_gpu_pvc") + + +def _sycl_compiler_options() -> list[str]: + options = [ + "-DCUTLASS_ENABLE_SYCL", + "-DSYCL_INTEL_TARGET", + "-DCUTLASS_VERSIONS_GENERATED", + "-O3", + "-DNDEBUG", + "-std=c++20", + "-fPIC", + "-fsycl", + f"-fsycl-targets={_sycl_arch_as_compile_option()}", + "-Xspirv-translator", + "-spirv-ext=+SPV_INTEL_split_barrier,+SPV_INTEL_2d_block_io,+SPV_INTEL_subgroup_matrix_multiply_accumulate", + "-fno-sycl-instrument-device-code", + "-DMKL_ILP64", + "-MD", + "-MT", + ] + if config.cutlass.enable_debug_info: + options.extend(["-lineinfo", "-g", "-DCUTLASS_DEBUG_TRACE_LEVEL=1"]) + return options + + +def xpu_compile_command( + src_files: list[str], + dst_file: str, + dst_file_ext: str, + extra_args: list[str] | None = None, +) -> str: + if extra_args is None: + extra_args = [] + include_paths = _cutlass_include_paths() + sycl_lib_options = _sycl_lib_options() + sycl_compiler_options = _sycl_compiler_options() + + options = ( + extra_args + + ["-I" + path for path in include_paths] + + ["-isystem", "/include"] + + sycl_compiler_options + + sycl_lib_options + ) + src_file = " ".join(src_files) + res = "" + if dst_file_ext == "o": + res = f"{_sycl_compiler()} {' '.join(options)} -c -o {dst_file} {src_file}" + elif dst_file_ext == "so": + options.append("-shared") + res = f"{_sycl_compiler()} {' '.join(options)} -o {dst_file} {src_file}" + elif dst_file_ext == "exe": + res = f"{_sycl_compiler()} {' '.join(options)} -o {dst_file} {src_file}" + else: + raise NotImplementedError(f"Unsupported output file suffix {dst_file_ext}!") + + log.debug("XPU command: %s", res) + return res diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/xpu/device_op_overrides.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/xpu/device_op_overrides.py new file mode 100644 index 0000000000000000000000000000000000000000..f6f2ff3eb6e5cfab9b78a903999b7b076d5bdec1 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/xpu/device_op_overrides.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from ..common import ( + DeviceOpOverrides, + register_device_op_overrides, + TritonScratchWorkspace, +) + + +class XPUDeviceOpOverrides(DeviceOpOverrides): + def import_get_raw_stream_as(self, name: str) -> str: + return f"from torch._C import _xpu_getCurrentRawStream as {name}" + + def set_device(self, device_idx: int) -> str: + return f"torch.xpu.set_device({device_idx})" + + def synchronize(self) -> str: + return "torch.xpu.synchronize()" + + def device_guard(self, device_idx: int) -> str: + return f"torch.xpu._DeviceGuard({device_idx})" + + def cpp_device_guard(self) -> str: + return "at::DeviceGuard" + + def cpp_aoti_device_guard(self) -> str: + return "AOTIXpuGuard" + + def cpp_stream_guard(self) -> str: + return "at::xpu::XPUStreamGuard" + + def cpp_aoti_stream_guard(self) -> str: + return "AOTIXpuStreamGuard" + + def cpp_getStreamFromExternal(self) -> str: + return "at::xpu::getStreamFromExternal" + + def kernel_header(self) -> str: + source_codes = """ + #include + """ + return source_codes + + def kernel_driver(self) -> str: + return "" + + def cpp_stream_type(self) -> str: + return "sycl::queue*" + + def aoti_get_stream(self) -> str: + return "aoti_torch_get_current_xpu_stream" + + def cpp_kernel_type(self) -> str: + return "std::unique_ptr" + + def cpp_device_ptr(self) -> str: + return "void *" + + def cpp_scratch( + self, idx: int, workspace: TritonScratchWorkspace, prefix: str | None = None + ) -> tuple[list[str], str] | None: + return [f"void *global_scratch_{idx} = 0;"], f"global_scratch_{idx}" + + +register_device_op_overrides("xpu", XPUDeviceOpOverrides()) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/xpu/xpu_combined_scheduling.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/xpu/xpu_combined_scheduling.py new file mode 100644 index 0000000000000000000000000000000000000000..b7c1b5eb3933ef3a776d847376aec9e94c517909 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/xpu/xpu_combined_scheduling.py @@ -0,0 +1,135 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +from typing import Any, TYPE_CHECKING + +from torch._inductor.scheduler import ( + BaseSchedulerNode, + BaseScheduling, + FusedSchedulerNode, + Scheduler, + SchedulerNode, +) + +from ..cutlass.scheduling import CUTLASSScheduling +from ..triton import TritonScheduling + + +if TYPE_CHECKING: + from collections.abc import Sequence + from typing import TypeAlias + + from sympy import Expr + + import torch + from torch.utils._ordered_set import OrderedSet + + from ..common import BackendFeature + + _IntLike: TypeAlias = int | Expr + + +class XPUCombinedScheduling(BaseScheduling): + """ + Scheduler for XPU Kernels, which delegates calls as appropriate + to the SYCL-C++ and Triton Schedulers, which both work for XPU devices + and use a unified-wrapper for codegen. + + If Scheduling code needs to be specialized for the case of mixed Triton / SYCL C++ code, + this would also be the place to do it. + """ + + def __init__(self, scheduler: Scheduler | None) -> None: + super().__init__(scheduler) + self._triton_scheduling = TritonScheduling(scheduler) + self._cutlass_scheduling = CUTLASSScheduling(scheduler) + + def get_backend_features(self, device: torch.device) -> OrderedSet[BackendFeature]: + return self._triton_scheduling.get_backend_features(device) + + def choose_node_backend(self, node: BaseSchedulerNode) -> BaseScheduling: + if self._cutlass_scheduling.is_cutlass_template(node): + return self._cutlass_scheduling + return self._triton_scheduling + + def can_fuse_vertical( + self, node1: BaseSchedulerNode, node2: BaseSchedulerNode + ) -> bool: + if self._cutlass_scheduling.can_fuse_vertical(node1, node2): + return True + elif self._cutlass_scheduling.is_cutlass_template( + node1 + ) or self._cutlass_scheduling.is_cutlass_template(node2): + return False + return self._triton_scheduling.can_fuse_vertical(node1, node2) + + def can_fuse_horizontal( + self, node1: BaseSchedulerNode, node2: BaseSchedulerNode + ) -> bool: + for node in (node1, node2): + if self._cutlass_scheduling.is_cutlass_template(node): + return self._cutlass_scheduling.can_fuse_horizontal( + node1, node2 + ) # always False at the moment + return self._triton_scheduling.can_fuse_horizontal(node1, node2) + + def group_fn( + self, sizes: Sequence[Sequence[_IntLike]] + ) -> tuple[tuple[_IntLike, ...], ...]: + return self._triton_scheduling.group_fn(sizes) + + def codegen_template( + self, + template_node: BaseSchedulerNode, + epilogue_nodes: Sequence[BaseSchedulerNode], + prologue_nodes: Sequence[BaseSchedulerNode], + ) -> str | None: + if self._cutlass_scheduling.is_cutlass_template(template_node): + assert not prologue_nodes + return self._cutlass_scheduling.codegen_template( + template_node, epilogue_nodes, prologue_nodes + ) + else: + return self._triton_scheduling.codegen_template( + template_node, epilogue_nodes, prologue_nodes + ) + + def codegen_mix_order_reduction(self, node): + return self._triton_scheduling.codegen_mix_order_reduction(node) + + def codegen_node(self, node: FusedSchedulerNode | SchedulerNode) -> None: + return self._triton_scheduling.codegen_node(node) + + def codegen_sync(self) -> None: + return self._triton_scheduling.codegen_sync() + + def flush(self) -> None: + return self._triton_scheduling.flush() + + def codegen_combo_kernel(self, *args: Any, **kwargs: Any) -> None: + return self._triton_scheduling.codegen_combo_kernel(*args, **kwargs) + + def benchmark_fused_nodes( + self, nodes: Sequence[BaseSchedulerNode] + ) -> tuple[float, str]: + return self._triton_scheduling.benchmark_fused_nodes(nodes) + + def benchmark_codegened_module(self, module): + return self._triton_scheduling.benchmark_codegened_module(module) + + def generate_kernel_code_from_nodes( + self, + nodes: Sequence[Any], + benchmark_kernel: bool = False, + hint_override: int | None = None, + ) -> str: + return self._triton_scheduling.generate_kernel_code_from_nodes( + nodes, benchmark_kernel, hint_override=hint_override + ) + + def benchmark_combo_kernel( + self, node_list: Sequence[BaseSchedulerNode], node_benchmark_results + ) -> tuple[float, float, list[str | None]]: + return self._triton_scheduling.benchmark_combo_kernel( + node_list, node_benchmark_results + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/xpu/xpu_env.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/xpu/xpu_env.py new file mode 100644 index 0000000000000000000000000000000000000000..a74b4cf2a24c7d079b68d46216c24cb2e484717c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/codegen/xpu/xpu_env.py @@ -0,0 +1,38 @@ +import functools +import logging + +import torch +from torch._inductor.utils import clear_on_fresh_cache + + +log = logging.getLogger(__name__) + + +@clear_on_fresh_cache +@functools.lru_cache(1) +def get_xpu_arch() -> str | None: + from torch.testing._internal.common_xpu import get_xpu_codename, XPUCodename + + name2arch = { + XPUCodename.PVC: "Xe12", + XPUCodename.BMG: "Xe20", + } + + codename = get_xpu_codename() + if not codename or codename not in name2arch: + log.warning("Unknown XPU codename, cannot determine architecture") + return None + + return name2arch[codename] + + +@clear_on_fresh_cache +@functools.lru_cache(1) +def get_xpu_version() -> str | None: + # string of version, like 20250101 + try: + xpu_version = torch.version.xpu or "" + return xpu_version + except Exception: + log.exception("Error getting xpu version") + return None diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/comm_analysis.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/comm_analysis.py new file mode 100644 index 0000000000000000000000000000000000000000..6f277daf47d55232f6902cc8589c05aea977fb3a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/comm_analysis.py @@ -0,0 +1,539 @@ +import functools +import logging +import math +import operator +from enum import IntEnum +from typing import Any + +import sympy + +import torch +import torch.utils._pytree as pytree +from torch.fx.experimental.symbolic_shapes import optimization_hint +from torch.fx.operator_schemas import normalize_function + +from . import ir +from .utils import get_dtype_size, snode_args_kwargs, sympy_product +from .virtualized import V + + +log = logging.getLogger(__name__) + + +class NCCL_COLL(IntEnum): + ALL_REDUCE = 0 + ALL_GATHER = 1 + REDUCE_SCATTER = 2 + ALL_TO_ALL = 3 + UNSUPPORTED = 4 + P2P = 5 + + +class NVIDIA_GPU_TYPE(IntEnum): + VOLTA = 0 + AMPERE = 1 + HOPPER = 2 + BLACKWELL = 3 + + +@functools.lru_cache +def get_gpu_type() -> NVIDIA_GPU_TYPE: + gpu_info = torch.utils.collect_env.get_gpu_info(torch.utils.collect_env.run) or "" + if "V100" in gpu_info: + return NVIDIA_GPU_TYPE.VOLTA + elif "A100" in gpu_info: + return NVIDIA_GPU_TYPE.AMPERE + elif "H100" in gpu_info: + return NVIDIA_GPU_TYPE.HOPPER + elif any(gpu in gpu_info for gpu in ("B100", "B200", "B300")): + return NVIDIA_GPU_TYPE.BLACKWELL + else: + # for other gpu types, assume Ampere + return NVIDIA_GPU_TYPE.AMPERE + + +def get_collective_type_from_kernel_name(kernel_name: str) -> NCCL_COLL: + assert kernel_name is not None + if "all_reduce" in kernel_name: + return NCCL_COLL.ALL_REDUCE + elif "all_gather" in kernel_name: + return NCCL_COLL.ALL_GATHER + elif "reduce_scatter" in kernel_name: + return NCCL_COLL.REDUCE_SCATTER + elif any(comm in kernel_name for comm in ("all_to_all", "alltoall")): + return NCCL_COLL.ALL_TO_ALL + elif any(comm in kernel_name for comm in ("isend", "irecv", "batch_p2p")): + return NCCL_COLL.P2P + else: + return NCCL_COLL.UNSUPPORTED + + +def get_collective_type(node: ir.IRNode) -> NCCL_COLL: + if not isinstance(node, ir._CollectiveKernel): + raise ValueError(f"node is not a collective kernel: {node}") + + name = node.python_kernel_name + assert name is not None + return get_collective_type_from_kernel_name(name) + + +def get_ir_node_size_numel(size: torch.Size, fallback: int = 4096 * 4096) -> int: + numel = sympy_product(size) + if isinstance(numel, sympy.Integer): + return int(numel) + return V.graph.sizevars.optimization_hint(numel, fallback=fallback) + + +def get_fx_node_size_numel(size: torch.Size, fallback: int = 4096 * 4096) -> int: + numel = functools.reduce(operator.mul, size, 1) + result = optimization_hint(numel, fallback=fallback) + return result + + +def get_collective_input_size_bytes(node: ir.IRNode) -> int: + sz_bytes = 0 + for inp in node.inputs: # type: ignore[attr-defined] + numel = get_ir_node_size_numel(inp.layout.size) + sz_bytes += numel * get_dtype_size(inp.layout.dtype) + return sz_bytes + + +def get_collective_group_size(node: ir.IRNode) -> int: + if isinstance(node, ir._CollectiveKernel) and not isinstance(node, ir._WaitKernel): + from torch.distributed.distributed_c10d import _get_group_size_by_name + + return _get_group_size_by_name(node.constant_args[-1]) + else: + raise TypeError(f"Unsupported collective type: {node}") + + +#################################################################################################################### +# The following code and constants are adapted from https://github.com/NVIDIA/nccl/blob/master/src/graph/tuning.cc # +#################################################################################################################### + + +class NCCL_HW(IntEnum): + NVLINK = 0 + PCI = 1 + NET = 2 + + +class NCCL_ALGO(IntEnum): + TREE = 0 + RING = 1 + + +class NCCL_PROTO(IntEnum): + # The ordering and enum values here matches original in + # https://github.com/NVIDIA/nccl/blob/0b083e52096c387bad7a5c5c65b26a9dca54de8c/src/include/devcomm.h#L28 + # For difference between these protocols, see https://github.com/NVIDIA/nccl/issues/281#issuecomment-571816990 + LL = 0 # Low-latency + # LL128 = 1 # Low-latency 128-byte + # SIMPLE = 2 + + +# Latencies in us +# len(NCCL_ALGO) x len(NCCL_PROTO) +# NOTE: use array instead of tensor to prevent incompatibility with fake mode +baseLat = [ + # Tree + [ + 6.8, # LL + ], + # Ring + [ + 6.6, # LL + ], +] + +# Latencies in us +# len(NCCL_HW) x len(NCCL_ALGO) x len(NCCL_PROTO) +hwLat = [ + # NVLINK + [ + [0.6], # Tree (LL) + [0.6], # Ring (LL) + ], + # PCI + [ + [1.0], # Tree (LL) + [1.0], # Ring (LL) + ], + # NET + [ + [5.0], # Tree (LL) + [2.7], # Ring (LL) + ], +] + + +# LL128 max BW per channel +llMaxBws = [ + # Volta-N1/Intel-N2/Intel-N4 + [ + 39.0, + 39.0, + 20.4, + ], + # Ampere-N1/AMD-N2/AMD-N4 + [ + 87.7, + 22.5, # avg of ring & tree + 19.0, + ], + # Hopper-N1/AMD-N2/AMD-N4 + [ + 141.0, + 45.0, # avg of ring & tree + 35.0, + ], + # Blackwell-N1/AMD-N2/AMD-N4 + [ + 282.0, + 90.0, # avg of ring & tree + 70.0, + ], +] + + +def estimate_nccl_collective_runtime_nccl_estimator(snode) -> float | None: # type: ignore[no-untyped-def] + kernel = snode.node + assert kernel is not None + py_kernel_name = getattr(kernel, "python_kernel_name", "") + pg_name = kernel.constant_args[-1] # type: ignore[attr-defined] + from torch.distributed.distributed_c10d import _resolve_process_group + + pg = _resolve_process_group(pg_name) + rank: int = torch.distributed.get_rank(pg) + # TODO(ivankobzarev): Figure out how we can use time estimations, + # without cuda allocations. + device = torch.device(f"cuda:{rank}") + + fn = eval(py_kernel_name) + args, kwargs = snode_args_kwargs(snode) + + # TODO(ivankobzarev): fix out variants snode_args_kwargs + if "all_gather_into_tensor_out" in py_kernel_name: + args = args[1:] + args[0] + + with torch.distributed._time_estimator(group=pg, device=device) as time_estimator: + w = fn(*args, **kwargs) + torch.ops._c10d_functional.wait_tensor.default(w) + + est_time_us = time_estimator.estimated_time + # -1000 constant is NCCL return in case of error during estimations. + # Observed it for all_to_all estimations. + if est_time_us < 0: + return None + est_time_ms = est_time_us / 1e3 + return est_time_ms + + +def estimate_nccl_collective_runtime_impl( + tensor_storage_size_bytes: int, group_size: int, coll: NCCL_COLL +) -> float: + """ + Returns estimated NCCL collective runtime in milliseconds (ms). + + The following heuristics are copied from https://github.com/NVIDIA/nccl/blob/master/src/graph/tuning.cc. + We aim to estimate the runtime as accurately as possible. + + Assumptions: + - only ring algorithm (NCCL_ALGO_RING) is used + - only Low-Latency protocol (NCCL_PROTO_LL) is used, i.e. Simple or LL128 is not used + - 8 gpus per node # TODO: Need to find a way to get accurate "gpus per node" and "# nodes" info. + - collective is one of: allreduce, reducescatter, allgather + """ + # Convert bytes to GB + tensor_storage_size_GB = tensor_storage_size_bytes / 1024 / 1024 / 1024 + + # Currently assumes each node has 8 gpus. And when >1 node is used, assumes each node uses all 8 gpus. + # TODO: Need to find a way to get accurate "gpus per node" and "# nodes" info. + num_gpus_per_node = 8 + nNodes = math.ceil(group_size / num_gpus_per_node) + nRanks = group_size # this is total # of gpus globally that participate in this collective op + + if nRanks <= 1: + return 0 + + if coll == NCCL_COLL.UNSUPPORTED: + return 0 + + # Assumes ring algorithm + nccl_algo = NCCL_ALGO.RING + nccl_proto = NCCL_PROTO.LL + + # =============== bandwidth computation =============== + # First compute bandwidth in GB/s; then at the end, convert it to GB/ns + + bwIntra = torch._inductor.config.intra_node_bw + bwInter = torch._inductor.config.inter_node_bw + + compCapIndex = get_gpu_type() + index2 = nNodes - 1 if nNodes <= 2 else 2 + # LL: for single node, we look at GPU type; for multi-node, we look at CPU type + index1 = compCapIndex if nNodes == 1 else 0 + llMaxBw = llMaxBws[index1][index2] + + # NOTE: each step of ring algorithm is synchronized, + # and is bottlenecked by the slowest link which is the inter-node interconnect. + # hence when nNodes >= 2, bw is inter-node bandwidth. + # NOTE: the original code in https://github.com/NVIDIA/nccl/blob/master/src/graph/tuning.cc + # have this as `if nNodes <= 2` which seems wrong. Corrected it here. + bw = bwIntra if nNodes == 1 else bwInter + nChannels = 2 # Assume # channels is 2 + busBw = nChannels * bw + + # Various model refinements + busBw = min( + llMaxBw, + busBw + * (1.0 / 4.0 if (nNodes > 1 or coll == NCCL_COLL.ALL_REDUCE) else 1.0 / 3.0), + ) + + if coll == NCCL_COLL.ALL_REDUCE: + nsteps = 2 * (nRanks - 1) + elif coll == NCCL_COLL.ALL_TO_ALL: + nsteps = 2 * (nRanks - 1) + elif coll in (NCCL_COLL.REDUCE_SCATTER, NCCL_COLL.ALL_GATHER): + nsteps = nRanks - 1 + elif coll == NCCL_COLL.P2P: + # assume 1 hop per pair + nsteps = 1 + + # Convert bus BW to algorithm BW (tensor bytes / algoBW = actual execution time) + ratio = (1.0 * nRanks) / nsteps # type: ignore[possibly-undefined] + bandwidth = busBw * ratio + # Convert GB/s to GB/ns + bandwidth_GB_per_ns = bandwidth / 1e9 + + # =============== latency computation =============== + intraHw = NCCL_HW.NVLINK + + if coll == NCCL_COLL.ALL_REDUCE: + if nNodes > 1: + nInterSteps = 2 * nNodes + else: + nInterSteps = 0 + elif coll in (NCCL_COLL.REDUCE_SCATTER, NCCL_COLL.ALL_GATHER, NCCL_COLL.ALL_TO_ALL): + nInterSteps = nNodes - 1 + elif coll == NCCL_COLL.P2P: + # p2p may cross node bdry + nInterSteps = 1 if nNodes > 1 else 0 + + # First compute latency in us; then at the end, convert it to ns + latency = baseLat[nccl_algo][nccl_proto] + intraLat = hwLat[intraHw][nccl_algo][nccl_proto] + interLat = hwLat[NCCL_HW.NET][nccl_algo][nccl_proto] + + # Inter-node rings still have to launch nsteps * net overhead. + netOverhead = 0.0 + if nNodes > 1: + netOverhead = 1.0 # getNetOverhead(comm); + intraLat = max(intraLat, netOverhead) + latency += (nsteps - nInterSteps) * intraLat + nInterSteps * interLat # type: ignore[possibly-undefined] + # Convert us to ns + latency_ns = latency * 1e3 + + # =============== final result =============== + transport_ns = tensor_storage_size_GB / bandwidth_GB_per_ns + ns = transport_ns + latency_ns + ms = ns / 1e6 + return ms + + +################################################################################################################ +# The above code and constants are adapted from https://github.com/NVIDIA/nccl/blob/master/src/graph/tuning.cc # +################################################################################################################ + + +def estimate_nccl_collective_runtime(node: ir.IRNode) -> float: + """ + Returns estimated NCCL collective runtime in nanoseconds (ms). + + The following heuristics are copied from https://github.com/NVIDIA/nccl/blob/master/src/graph/tuning.cc. + We aim to estimate the runtime as accurately as possible. + + Assumptions: + - only ring algorithm (NCCL_ALGO_RING) is used + - only Low-Latency protocol (NCCL_PROTO_LL) is used, i.e. Simple or LL128 is not used + - 8 gpus per node # TODO: Need to find a way to get accurate "gpus per node" and "# nodes" info. + - collective is one of: allreduce, reducescatter, allgather + """ + tensor_storage_size_bytes = get_collective_input_size_bytes(node) + group_size = get_collective_group_size(node) + coll = get_collective_type(node) + return estimate_nccl_collective_runtime_impl( + tensor_storage_size_bytes, group_size, coll + ) + + +def estimate_fx_collective_size(fx_node: torch.fx.Node) -> int: + """Estimate the size of a collective operation in bytes, including inputs and outputs.""" + input_bytes = None + + args, kwargs = fx_node.args, fx_node.kwargs + kwargs = dict(kwargs) + + # dont double count pre-allocated buffer passed in + kwargs.pop("out", None) + + def tensor_bytes(t: torch.Tensor) -> int: + return get_fx_node_size_numel(t.size()) * get_dtype_size(t.dtype) + + def add_inp_bytes(inp: torch.fx.Node): + inp_val = inp.meta.get("val", None) + if not isinstance(inp_val, torch.Tensor): + return + + nonlocal input_bytes + if input_bytes is None: + input_bytes = 0 + input_bytes += tensor_bytes(inp_val) + + pytree.tree_map_only( + torch.fx.Node, + add_inp_bytes, + (args, kwargs), + ) + + output_val = fx_node.meta.get("val", None) + + if input_bytes is None or output_val is None: + return 0 + + # Coalesced collectives return a list of tensors + if isinstance(output_val, (list, tuple)): + output_bytes = sum( + tensor_bytes(t) for t in output_val if isinstance(t, torch.Tensor) + ) + elif isinstance(output_val, torch.Tensor): + output_bytes = tensor_bytes(output_val) + else: + return 0 + + return input_bytes + output_bytes + + +def estimate_fx_collective_memory_footprint(fx_node: torch.fx.Node) -> int: + """Estimate the memory footprint of a collective operation in bytes. + + This returns the total bytes that need to be live concurrently in memory. + For all_reduce, we divide by 2 since it can be done in-place. + """ + from torch._inductor.fx_passes.bucketing import ( + is_all_reduce_tensor as is_all_reduce, + ) + + size = estimate_fx_collective_size(fx_node) + return size if not is_all_reduce(fx_node) else size // 2 + + +def estimate_nccl_collective_runtime_from_fx_node( + fx_node: torch.fx.Node, + override_size: int | None = None, + use_nccl_estimator: bool = True, +) -> float: + """ + Returns estimated NCCL collective runtime in nanoseconds (ms). + + The following heuristics are copied from https://github.com/NVIDIA/nccl/blob/master/src/graph/tuning.cc. + We aim to estimate the runtime as accurately as possible. + + Assumptions: + - only ring algorithm (NCCL_ALGO_RING) is used + - only Low-Latency protocol (NCCL_PROTO_LL) is used, i.e. Simple or LL128 is not used + - 8 gpus per node # TODO: Need to find a way to get accurate "gpus per node" and "# nodes" info. + - collective is one of: allreduce, reducescatter, allgather + """ + from torch.distributed.distributed_c10d import _get_group_size_by_name + + if fx_node.target is torch.ops._c10d_functional.all_to_all_single.default: + # TODO(ivankobzarev): Temporarily disabled - NCCL estimator returns internal error. + # for all_to_all during inductor compilation. Falls back to heuristic estimation. + use_nccl_estimator = False + + if override_size is None: + tensor_storage_size_bytes = estimate_fx_collective_size(fx_node) + else: + tensor_storage_size_bytes = override_size + + assert not isinstance(fx_node.target, str) + opt_args_kwargs = normalize_function( + fx_node.target, + args=fx_node.args, + kwargs=fx_node.kwargs, + normalize_to_only_use_kwargs=True, + ) + assert opt_args_kwargs is not None + args, kwargs = opt_args_kwargs + + group_name = kwargs["group_name"] + group_size = _get_group_size_by_name(group_name) + assert isinstance(fx_node.target, torch._ops.OpOverload) + coll = get_collective_type_from_kernel_name(fx_node.target.name()) + + def _nccl_estimate() -> float | None: + # TODO: Refactor with estimate_nccl_collective_runtime_nccl_estimator + from torch.distributed.distributed_c10d import _resolve_process_group, Backend + + pg = _resolve_process_group(group_name) + if torch.distributed.distributed_c10d.get_backend(pg) == Backend.FAKE: + # nccl estimator requires real process group + return None + + device = torch.device("cuda") + try: + backend = pg._get_backend(device) + except RuntimeError: + return None + if not backend.supports_time_estimate: + return None + + flat_args, flat_args_pytree_spec = pytree.tree_flatten((args, kwargs)) + + def _tensor(size, dtype, device) -> torch.Tensor: # type: ignore[no-untyped-def] + return torch.empty( + size if override_size is None else [override_size], + dtype=dtype, + device=device, + ) + + def to_real_tensor(e: Any) -> Any: + if isinstance(e, torch.fx.Node): + return to_real_tensor(e.meta["val"]) + if isinstance(e, torch.Tensor): + return _tensor([get_fx_node_size_numel(e.size())], e.dtype, e.device) + return e + + flat_args = [to_real_tensor(a) for a in flat_args] + real_args, real_kwargs = pytree.tree_unflatten(flat_args, flat_args_pytree_spec) + + fn = fx_node.target + assert isinstance(fn, torch._ops.OpOverload) + with torch.distributed._time_estimator( + group=pg, device=device + ) as time_estimator: + w = fn(*real_args, **real_kwargs) + # Coalesced collectives return a list of tensors + if isinstance(w, (list, tuple)): + for t in w: + torch.ops._c10d_functional.wait_tensor.default(t) + else: + torch.ops._c10d_functional.wait_tensor.default(w) + est_time_us = time_estimator.estimated_time + # -1000 constant is NCCL return in case of error during estimations. + # Observed it for all_to_all estimations. + if est_time_us < 0: + return None + est_time_ms = est_time_us / 1e3 + return est_time_ms + + if use_nccl_estimator: + est_time_ms = _nccl_estimate() + if est_time_ms is not None: + return est_time_ms + + return estimate_nccl_collective_runtime_impl( + tensor_storage_size_bytes, group_size, coll + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/comm_lowering.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/comm_lowering.py new file mode 100644 index 0000000000000000000000000000000000000000..e5dfd1fd18127324ff03ab1cbf31385cb90f2c01 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/comm_lowering.py @@ -0,0 +1,866 @@ +# mypy: allow-untyped-defs +import logging + +import torch +import torch.utils._pytree as pytree +from torch._inductor.utils import is_symbolic +from torch.utils._ordered_set import OrderedSet + +from . import config, ir +from .virtualized import V + + +log = logging.getLogger(__name__) + + +# NOTE [lowering-time collective optimization] +# +# In collective communication libraries such as NCCL, every rank maintains +# communication buffers that are remotely accessible by some peers. Depending +# on the underlying transport, remote accessibility may be established via +# mechanisms such as ib_reg_mr, CUDA P2P, or CUDA multicast. Typically, these +# buffers are private to the communication library by default, and +# communication ops copy user data in and out of these buffers. +# +# To prevent these copies, an optimization commonly known as "user buffer +# registration" can be employed. This allows direct establishment of remote +# accessibility on user buffers, eliminating the need for copying. However, +# this optimization introduces stringent usage requirements, which are +# typically hard to satisfy without being intrusive to the user code: +# +# - Establishing remote accessibility is expensive and often done ahead of +# time. In such implementations, all ranks must agree on the set of allocations +# used for every collective op. Failing to meet this requirement can +# lead to runtime errors or even silent correctness issues. +# - Even if the collective communication library supports gracefully falling +# back to "unregistered" implementations, the fallback mechanism would nullify +# the optimization. +# - Some communication mechanisms impose stricter requirements than others. For +# example, CUDA's multicast + multi-mem instructions require all ranks to agree +# not only on the allocations used for every collective but also on the offsets +# within these allocations. +# +# To support all different mechanisms with optimal results, we aim to satisfy +# the strictest requirement for this family of optimizations - we ensures that +# every collective op invocation is guaranteed to operate on the same +# allocation, at the same offset, in every iteration. +# +# For eligible collective ops, we identify communication buffers at lowering +# time and optionally choose to lower the op to a different kernel +# (communication libraries like NCCL handle both registered and non-registered +# buffers transparently within the same op, though some may require different +# ops for different cases). Later, the codegen will perform "persistent +# allocation" to satisfy the aforementioned constraints, and optionally, +# perform buffer planning to optimize overall memory usage. +def can_realize_as_comm_buffer( + x: ir.TensorBox, comm_buffer_type: ir.CommBufferType +) -> bool: + """ + Check if an input can be realized as a comm buffer of the specified + `comm_buffer_type`. + """ + data = _get_data(x) + + if isinstance(data, ir.Loops): + return True + + # We cannot realize buffers as comm buffers if we don't control their + # allocation. + if isinstance(data, ir.Buffer) and not data.should_allocate(): + return False + + layout = data.get_output_spec() + if isinstance(layout, ir.CommBufferLayout): + return True + + if isinstance(layout, ir.FixedLayout): + return True + + if isinstance(layout, ir.FlexibleLayout) and not is_symbolic(data.get_numel()): + return True + + return False + + +def realize_as_comm_buffer( + x: ir.TensorBox, + comm_buffer_type: ir.CommBufferType, + group_name: "torch.distributed.distributed_c10d.GroupName", +) -> None: + """ + Realize an input as a comm buffer of the specified `comm_buffer_type`. + + Specifically, this realizes the underlying buffer if it's still unrealized + and changes the layout of the buffer to `ir.CommBufferLayout`. + """ + x.realize() + buffer = _get_data(x) + assert isinstance(buffer, ir.Buffer) + + layout = buffer.get_output_spec() + if isinstance(layout, ir.CommBufferLayout): + return + + # The buffer may have already been frozen to FixedLayout if it was used + # by another operation before the comm operation. + if not isinstance(layout, (ir.FlexibleLayout, ir.FixedLayout)): + raise AssertionError( + "A buffer can only be realized as a comm buffer if it " + f"has `FlexibleLayout` or `FixedLayout` (got {layout})." + ) + + if is_symbolic(buffer.get_numel()): + raise AssertionError( + "A buffer with symbolic shape cannot be converted to " + f"a comm buffer (got {layout})." + ) + + buffer.layout = ir.CommBufferLayout( + layout=layout, + comm_buffer_type=comm_buffer_type, + group_name=group_name, + ) + + +def _get_data(x: ir.TensorBox) -> ir.IRNode: + if isinstance(x.data, ir.BaseView): + # TensorBox -> *View -> StorageBox -> IRNode + node = x.data.unwrap_view() + assert isinstance(node, (ir.BaseView, ir.MutableBox)) + return node.data + elif isinstance(x.data, ir.StorageBox): + # TensorBox -> StorageBox -> IRNode + return x.data.data + else: + raise AssertionError( + "Expect the data attr of a `TensorBox` to be either " + f"an `ir.BaseView` or `ir.StorageBox` (got {x.data})." + ) + + +_bufs_to_skip_wait = OrderedSet[tuple[int, str]]() + + +def mark_as_skip_wait(x: ir.IRNode) -> None: + """ + If a non-blocking collective is lowered as a blocking collective, the wait + node in the original graph becomes useless and we can skip the lowering it. + """ + _bufs_to_skip_wait.add((id(V.graph), x.get_name())) + + +def should_skip_wait(x: ir.IRNode) -> bool: + return (id(V.graph), x.get_name()) in _bufs_to_skip_wait + + +def _should_lower_as_one_shot_all_reduce( + inp: ir.TensorBox, + reduce_op: str, + group_name: "torch.distributed.distributed_c10d.GroupName", +): + from torch.distributed._symmetric_memory import is_symm_mem_enabled_for_group + + inp_size = inp.get_numel() * inp.get_dtype().itemsize + return ( + config._collective.auto_select + and is_symm_mem_enabled_for_group(group_name) + and can_realize_as_comm_buffer(inp, ir.CommBufferType.SYMM_MEM) + and reduce_op == "sum" + and inp_size <= config._collective.one_shot_all_reduce_threshold_bytes + ) + + +def _one_shot_all_reduce(inp: ir.TensorBox, reduce_op, group_name): + realize_as_comm_buffer(inp, ir.CommBufferType.SYMM_MEM, group_name) + return pytree.tree_map( + ir.TensorBox.create, + ir.FallbackKernel.create( + torch.ops.symm_mem.one_shot_all_reduce.default, + inp, + reduce_op, + group_name, + ), + ) + + +def register_comm_lowerings(): + """ + Register lowerings for the comm subsystem. + """ + try: + torch.ops._c10d_functional.all_reduce + except AttributeError: + log.info( + "Inductor support for distributed collectives depends on building " + "torch.distributed" + ) + return + + from .lowering import ( + add_layout_constraint, + clone, + constrain_to_fx_strides, + copy_, + register_lowering, + ) + + def register_comm_lowering(fn): + add_layout_constraint(fn, constrain_to_fx_strides) + return register_lowering(fn) + + c10d = torch.ops._c10d_functional + + @register_comm_lowering(c10d.all_reduce) # type: ignore[misc] + def _all_reduce( + inp: ir.TensorBox, + reduce_op: str, + group_name: "torch.distributed.distributed_c10d.GroupName", + ) -> ir.TensorBox: + if _should_lower_as_one_shot_all_reduce(inp, reduce_op, group_name): + return _one_shot_all_reduce(inp, reduce_op, group_name) + + # Lower as c10d.all_reduce_ + inp = clone(inp) + if config.reorder_for_compute_comm_overlap: + # The horizontal fusion of this clone often severely delays the + # scheduling of the all_reduce_ node. Horizontally fusing this + # clone can almost never out-perform scheduling the all_reduce_ + # earlier. Also in most cases, this clone is eliminated via + # in-place reuse. Therefore, we tell the scheduler to not fuse it. + inp.realize() + V.graph.no_fuse_buffer_names.add(inp.get_name()) + # pyrefly: ignore [bad-assignment] + inp = ir.ExternKernel.require_contiguous(inp) + # Because we are lowering as inplace c10d.all_reduce_, we should generate + # _AllReduce_Kernel instead of _AllReduceKernel. + ir._AllReduce_Kernel.create_inplace( + c10d.all_reduce_.default, + inp, # type: ignore[arg-type] + reduce_op, + group_name, # type: ignore[arg-type] + ) + return inp # type: ignore[return-value] + + @register_comm_lowering(c10d.all_reduce_) # type: ignore[misc] + def _all_reduce_( + inp: ir.TensorBox, + reduce_op: str, + group_name: "torch.distributed.distributed_c10d.GroupName", + ) -> ir.TensorBox: + if _should_lower_as_one_shot_all_reduce(inp, reduce_op, group_name): + ret = copy_( + inp, + _one_shot_all_reduce(inp, reduce_op, group_name), + ) + mark_as_skip_wait(ret) + return inp + + # Lower as c10d.all_reduce_ + # pyrefly: ignore [bad-assignment] + inp = ir.ExternKernel.require_contiguous(inp) + ir._AllReduce_Kernel.create_inplace( + c10d.all_reduce_.default, + inp, # type: ignore[arg-type] + reduce_op, + group_name, # type: ignore[arg-type] + ) + return inp # type: ignore[return-value] + + @register_comm_lowering(c10d.all_reduce_coalesced) + def _all_reduce_coalesced(inputs, reduce_op, group_name): + inputs = [clone(inp) for inp in inputs] + ir._CollectiveKernel.create_inplace( + c10d.all_reduce_coalesced_.default, + inputs, + reduce_op, + group_name, + ) + return inputs + + @register_comm_lowering(c10d.all_reduce_coalesced_) + def _all_reduce_coalesced_(inputs, reduce_op, group_name): + ir._CollectiveKernel.create_inplace( + c10d.all_reduce_coalesced_.default, + inputs, + reduce_op, + group_name, + ) + return inputs + + def _create_out_of_place(kernel, inputs, *args) -> ir.IRNode: + node = ir._CollectiveKernel.create_out_of_place(kernel, inputs, *args) + assert isinstance(node, ir.IRNode) + return ir.TensorBox.create(node) + + @register_comm_lowering(c10d.all_gather_into_tensor) + def _all_gather_into_tensor(inp, group_size, group_name): + return _create_out_of_place( + c10d.all_gather_into_tensor.default, + inp, + group_size, + group_name, + ) + + @register_comm_lowering(c10d.all_gather_into_tensor_coalesced) + def _all_gather_into_tensor_coalesced(inputs, group_size, group_name): + return pytree.tree_map( + ir.TensorBox.create, + ir._CollectiveKernel.create_out_of_place( + c10d.all_gather_into_tensor_coalesced.default, + inputs, + group_size, + group_name, + ), + ) + + @register_comm_lowering(c10d.all_gather_into_tensor_out) + def _all_gather_into_tensor_out(inp, group_size, group_name, *, out): + ir._CollectiveKernel.create_inplace( + c10d.all_gather_into_tensor_out.default, + inp, + group_size, + group_name, + out=out, + ) + return out + + @register_comm_lowering(c10d.reduce_scatter_tensor) + def _reduce_scatter_tensor(inp, reduce_op, group_size, group_name): + return _create_out_of_place( + c10d.reduce_scatter_tensor.default, + inp, + reduce_op, + group_size, + group_name, + ) + + @register_comm_lowering(c10d.reduce_scatter_tensor_out) + def _reduce_scatter_tensor_out(inp, reduce_op, group_size, group_name, *, out): + ir._CollectiveKernel.create_inplace( + c10d.reduce_scatter_tensor_out.default, + inp, + reduce_op, + group_size, + group_name, + out=out, + ) + return out + + @register_comm_lowering(c10d.reduce_scatter_tensor_coalesced) + def _reduce_scatter_tensor_coalesced(inputs, reduce_op, group_size, group_name): + return pytree.tree_map( + ir.TensorBox.create, + ir._CollectiveKernel.create_out_of_place( + c10d.reduce_scatter_tensor_coalesced.default, + inputs, + reduce_op, + group_size, + group_name, + ), + ) + + @register_comm_lowering(c10d.all_to_all_single) + def _all_to_all_single(inp, output_split_sizes, input_split_sizes, group_name): + return _create_out_of_place( + c10d.all_to_all_single.default, + inp, + output_split_sizes, + input_split_sizes, + group_name, + ) + + @register_comm_lowering(c10d.broadcast) + def _broadcast(inp, src, group_name): + inp = clone(inp) + ir._CollectiveKernel.create_inplace( + c10d.broadcast_.default, inp, src, group_name + ) + return inp + + @register_comm_lowering(c10d.broadcast_) + def _broadcast_(inp, src, group_name): + ir._CollectiveKernel.create_inplace( + c10d.broadcast_.default, inp, src, group_name + ) + return inp + + @register_comm_lowering(torch.ops._dtensor.shard_dim_alltoall) + def _shard_dim_alltoall(inp, gather_dim, shard_dim, group_name): + return _create_out_of_place( + torch.ops._dtensor.shard_dim_alltoall.default, + inp, + gather_dim, + shard_dim, + group_name, + ) + + @register_comm_lowering(c10d.wait_tensor) + def _wait_tensor(inp): + if should_skip_wait(inp): + return inp + + ir._WaitKernel.create_wait(c10d.wait_tensor.default, inp) + return inp + + @register_comm_lowering(c10d.isend) # type: ignore[misc] + def _isend(inp, dst, tag, group_name): + inp = ir.ExternKernel.require_contiguous(inp) + return _create_out_of_place(c10d.isend.default, inp, dst, tag, group_name) + + @register_comm_lowering(c10d.irecv) # type: ignore[misc] + def _irecv(inp, src, tag, group_name): + inp = ir.ExternKernel.require_contiguous(inp) + ir._CollectiveKernel.create_inplace( + c10d.irecv.default, inp, src, tag, group_name + ) + return inp + + @register_comm_lowering(c10d.batch_p2p_ops) # type: ignore[misc] + def _batch_p2p_ops(op_list, peer_list, tag_list, tensors, group_name): + tensors = [ir.ExternKernel.require_contiguous(t) for t in tensors] + kernel = c10d.batch_p2p_ops.default + with V.graph.fake_mode: + ( + example_output, + tensor_args, + non_tensor_args, + unflatten_args, + unbacked_bindings, + ) = ir._CollectiveKernel.process_kernel( + kernel, + op_list, + peer_list, + tag_list, + tensors, + group_name, + ) + assert not unbacked_bindings, f"{kernel} {unbacked_bindings}" + for op, tensor_arg in zip(op_list, tensor_args): + tensor_arg.realize() + if op == "irecv": + V.graph.mark_buffer_mutated(tensor_arg.get_name()) + + device = tensor_args[0].get_device() + packed = ir._CollectiveKernel( + ir.MultiOutputLayout(device=device), + kernel, + tensor_args, + non_tensor_args, + unflatten_args, + ) + + results = [] + for i, (op, t, ex_out) in enumerate(zip(op_list, tensors, example_output)): + if op == "irecv": + packed.mutation_outputs.append( + ir.MutationOutput(ir.NoneLayout(device=device), t, packed) + ) + packed.alias_names.append(t.get_name()) + results.append(t) + else: + # isend: 0-element placeholder output connected to the collective + placeholder = ir.MultiOutput( + ir._CollectiveKernel.tensor_to_layout(ex_out), + packed, + [(list, i)], + ) + results.append(ir.TensorBox.create(placeholder)) + return results + + +def register_symm_mem_lowerings(): + """ + Register lowerings for symmetric memory (symm_mem) operations. + """ + try: + symm_mem = torch.ops.symm_mem + # Check for an actual operation, not just the namespace. + # torch.ops.symm_mem is a lazy namespace that always exists, + # but the operations may not exist on non-CUDA platforms or + # when USE_DISTRIBUTED is disabled. + symm_mem.one_shot_all_reduce + except AttributeError: + log.info("symm_mem ops not available, skipping symm_mem lowerings") + return + + from torch._library._out_variant import register_out_variant + + # Register manual out variant mappings for symm_mem ops. + register_out_variant( + symm_mem.one_shot_all_reduce.default, + symm_mem.one_shot_all_reduce_out.default, + ) + register_out_variant( + symm_mem.one_shot_all_reduce_copy.default, + symm_mem.one_shot_all_reduce_copy_out.default, + ) + + from .lowering import register_lowering + + def _copy_input_to_comm_buffer( + inp: ir.TensorBox, + comm_buffer_type: ir.CommBufferType, + group_name: "torch.distributed.distributed_c10d.GroupName", + ) -> ir.TensorBox: + """ + Fallback: insert a Pointwise identity copy allocated in P2P via + CommBufferLayout. Used when we don't control the input's allocation. + """ + inp.realize() + copy = ir.Pointwise.create( + device=inp.get_device(), + dtype=inp.get_dtype(), + inner_fn=inp.make_loader(), + ranges=inp.get_size(), + ) + realize_as_comm_buffer(copy, comm_buffer_type, group_name) + return copy + + def _maybe_realize_symm_mem( + inp: ir.TensorBox, + group_name: str, # type: ignore[arg-type] + ) -> ir.TensorBox: + """ + Ensure inp is in P2P memory for a symm_mem collective. + + If inductor controls the buffer's allocation (ComputedBuffer, + or any buffer with FlexibleLayout/FixedLayout), switch its + layout to CommBufferLayout in-place, zero-copy. + + If inductor does not control allocation (e.g. InputBuffer), + insert a Pointwise identity copy into a new CommBufferLayout buffer. + This adds an extra Triton kernel. Returns the possibly new TensorBox. + + TODO(tianrengao): eliminate the extra kernel for static-shape + InputBuffers by pre-allocating P2P memory in the wrapper and DMA .copy_() + """ + if can_realize_as_comm_buffer(inp, ir.CommBufferType.SYMM_MEM): + realize_as_comm_buffer(inp, ir.CommBufferType.SYMM_MEM, group_name) # type: ignore[arg-type] + return inp + else: + return _copy_input_to_comm_buffer( + inp, + ir.CommBufferType.SYMM_MEM, + group_name, # type: ignore[arg-type] + ) + + @register_lowering(symm_mem.one_shot_all_reduce) + def _symm_mem_one_shot_all_reduce( + inp: ir.TensorBox, + reduce_op: str, + group_name: str, + ): + inp = _maybe_realize_symm_mem(inp, group_name) + return pytree.tree_map( + ir.TensorBox.create, + ir.FallbackKernel.create( + symm_mem.one_shot_all_reduce.default, + inp, + reduce_op, + group_name, + ), + ) + + @register_lowering(symm_mem.one_shot_all_reduce_out) + def _symm_mem_one_shot_all_reduce_out( + inp: ir.TensorBox, + reduce_op: str, + group_name: str, + out: ir.TensorBox, + ): + inp = _maybe_realize_symm_mem(inp, group_name) + return pytree.tree_map( + ir.TensorBox.create, + ir.FallbackKernel.create( + symm_mem.one_shot_all_reduce_out.default, + inp, + reduce_op, + group_name, + out, + ), + ) + + @register_lowering(symm_mem.one_shot_all_reduce_copy) + def _symm_mem_one_shot_all_reduce_copy( + symm_buffer: ir.TensorBox, + local_input: ir.TensorBox, + reduce_op: str, + group_name: str, + ): + symm_buffer = _maybe_realize_symm_mem(symm_buffer, group_name) + return pytree.tree_map( + ir.TensorBox.create, + ir.FallbackKernel.create( + symm_mem.one_shot_all_reduce_copy.default, + symm_buffer, + local_input, + reduce_op, + group_name, + ), + ) + + @register_lowering(symm_mem.one_shot_all_reduce_copy_out) + def _symm_mem_one_shot_all_reduce_copy_out( + symm_buffer: ir.TensorBox, + local_input: ir.TensorBox, + reduce_op: str, + group_name: str, + out: ir.TensorBox, + ): + symm_buffer = _maybe_realize_symm_mem(symm_buffer, group_name) + return pytree.tree_map( + ir.TensorBox.create, + ir.FallbackKernel.create( + symm_mem.one_shot_all_reduce_copy_out.default, + symm_buffer, + local_input, + reduce_op, + group_name, + out, + ), + ) + + @register_lowering(symm_mem.two_shot_all_reduce_) + def _symm_mem_two_shot_all_reduce_( + inp: ir.TensorBox, + reduce_op: str, + group_name: str, + ): + inp = _maybe_realize_symm_mem(inp, group_name) + ir.FallbackKernel.create( + symm_mem.two_shot_all_reduce_.default, + inp, + reduce_op, + group_name, + ) + return inp + + @register_lowering(symm_mem.two_shot_all_reduce_out) + def _symm_mem_two_shot_all_reduce_out( + inp: ir.TensorBox, + reduce_op: str, + group_name: str, + output: ir.TensorBox, + ): + inp = _maybe_realize_symm_mem(inp, group_name) + return pytree.tree_map( + ir.TensorBox.create, + ir.FallbackKernel.create( + symm_mem.two_shot_all_reduce_out.default, + inp, + reduce_op, + group_name, + output, + ), + ) + + @register_lowering(symm_mem.multimem_all_reduce_) + def _symm_mem_multimem_all_reduce_( + inp: ir.TensorBox, + reduce_op: str, + group_name: str, + ): + inp = _maybe_realize_symm_mem(inp, group_name) + ir.FallbackKernel.create( + symm_mem.multimem_all_reduce_.default, + inp, + reduce_op, + group_name, + ) + return inp + + @register_lowering(symm_mem.multimem_one_shot_all_reduce) + def _symm_mem_multimem_one_shot_all_reduce( + inp: ir.TensorBox, + reduce_op: str, + group_name: str, + ): + inp = _maybe_realize_symm_mem(inp, group_name) + return pytree.tree_map( + ir.TensorBox.create, + ir.FallbackKernel.create( + symm_mem.multimem_one_shot_all_reduce.default, + inp, + reduce_op, + group_name, + ), + ) + + @register_lowering(symm_mem.multimem_one_shot_all_reduce_out) + def _symm_mem_multimem_one_shot_all_reduce_out( + inp: ir.TensorBox, + reduce_op: str, + group_name: str, + out: ir.TensorBox, + ): + inp = _maybe_realize_symm_mem(inp, group_name) + return pytree.tree_map( + ir.TensorBox.create, + ir.FallbackKernel.create( + symm_mem.multimem_one_shot_all_reduce_out.default, + inp, + reduce_op, + group_name, + out, + ), + ) + + @register_lowering(symm_mem.multimem_one_shot_reduce_out) + def _symm_mem_multimem_one_shot_reduce_out( + inp: ir.TensorBox, + reduce_op: str, + root: int, + group_name: str, + out: ir.TensorBox, + ): + inp = _maybe_realize_symm_mem(inp, group_name) + return pytree.tree_map( + ir.TensorBox.create, + ir.FallbackKernel.create( + symm_mem.multimem_one_shot_reduce_out.default, + inp, + reduce_op, + root, + group_name, + out, + ), + ) + + @register_lowering(symm_mem.multimem_all_gather_out) + def _symm_mem_multimem_all_gather_out( + inp: ir.TensorBox, + group_name: str, + out: ir.TensorBox, + ): + inp = _maybe_realize_symm_mem(inp, group_name) + return pytree.tree_map( + ir.TensorBox.create, + ir.FallbackKernel.create( + symm_mem.multimem_all_gather_out.default, + inp, + group_name, + out, + ), + ) + + @register_lowering(symm_mem.reduce_scatter_out) + def _symm_mem_reduce_scatter_out( + inp: ir.TensorBox, + group_name: str, + split_last_dim: bool, + output: ir.TensorBox, + ): + inp = _maybe_realize_symm_mem(inp, group_name) + return pytree.tree_map( + ir.TensorBox.create, + ir.FallbackKernel.create( + symm_mem.reduce_scatter_out.default, + inp, + group_name, + split_last_dim, + output, + ), + ) + + @register_lowering(symm_mem.all_to_all_vdev) + def _symm_mem_all_to_all_vdev( + inp: ir.TensorBox, + out: ir.TensorBox, + in_splits: ir.TensorBox, + out_splits_offsets: ir.TensorBox, + group_name: str, + ): + inp = _maybe_realize_symm_mem(inp, group_name) + out = _maybe_realize_symm_mem(out, group_name) + ir.FallbackKernel.create( + symm_mem.all_to_all_vdev.default, + inp, + out, + in_splits, + out_splits_offsets, + group_name, + ) + return None + + @register_lowering(symm_mem.all_to_all_vdev_2d) + def _symm_mem_all_to_all_vdev_2d( + inp: ir.TensorBox, + out: ir.TensorBox, + in_splits: ir.TensorBox, + out_splits_offsets: ir.TensorBox, + group_name: str, + major_align=None, + ): + inp = _maybe_realize_symm_mem(inp, group_name) + out = _maybe_realize_symm_mem(out, group_name) + ir.FallbackKernel.create( + symm_mem.all_to_all_vdev_2d.default, + inp, + out, + in_splits, + out_splits_offsets, + group_name, + major_align, + ) + return None + + @register_lowering(symm_mem.all_to_all_vdev_2d_offset) + def _symm_mem_all_to_all_vdev_2d_offset( + inp: ir.TensorBox, + out: ir.TensorBox, + in_splits_offsets: ir.TensorBox, + out_splits_offsets: ir.TensorBox, + group_name: str, + ): + inp = _maybe_realize_symm_mem(inp, group_name) + out = _maybe_realize_symm_mem(out, group_name) + ir.FallbackKernel.create( + symm_mem.all_to_all_vdev_2d_offset.default, + inp, + out, + in_splits_offsets, + out_splits_offsets, + group_name, + ) + return None + + @register_lowering(symm_mem.tile_reduce) + def _symm_mem_tile_reduce( + in_tile: ir.TensorBox, + out_tile: ir.TensorBox, + root: int, + group_name: str, + reduce_op: str = "sum", + ): + in_tile = _maybe_realize_symm_mem(in_tile, group_name) + out_tile = _maybe_realize_symm_mem(out_tile, group_name) + ir.FallbackKernel.create( + symm_mem.tile_reduce.default, + in_tile, + out_tile, + root, + group_name, + reduce_op, + ) + return None + + @register_lowering(symm_mem.multi_root_tile_reduce) + def _symm_mem_multi_root_tile_reduce( + in_tiles, # list of TensorBox + out_tile: ir.TensorBox, + roots, # list of int + group_name: str, + reduce_op: str = "sum", + ): + for i, in_tile in enumerate(in_tiles): + in_tiles[i] = _maybe_realize_symm_mem(in_tile, group_name) + out_tile = _maybe_realize_symm_mem(out_tile, group_name) + ir.FallbackKernel.create( + symm_mem.multi_root_tile_reduce.default, + in_tiles, + out_tile, + roots, + group_name, + reduce_op, + ) + return None diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/comms.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/comms.py new file mode 100644 index 0000000000000000000000000000000000000000..34a1c2a693d8a6605810c50fb230644df6e47b48 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/comms.py @@ -0,0 +1,2722 @@ +# mypy: allow-untyped-defs +# pyre-strict +from __future__ import annotations + +import heapq +import importlib +import itertools +import logging +import operator +import sys +from collections import defaultdict +from dataclasses import dataclass +from typing import Any, TYPE_CHECKING + +import torch +from torch._logging import trace_structured +from torch.multiprocessing.reductions import StorageWeakRef +from torch.utils._ordered_set import OrderedSet + +from . import config, config_comms, ir +from .dependencies import WeakDep + + +if TYPE_CHECKING: + from .ir import IRNode, Operation + +from .memory import ( + estimate_peak_memory_allocfree, + FreeableInputBuffer, + get_freeable_input_buf, + SNodeMemory, +) +from .utils import ( + contains_collective, + contains_wait, + find_recursive_deps_of_node, + find_recursive_users_of_node, + is_collective, + is_fallback_op, + is_wait, +) +from .virtualized import V + + +log = logging.getLogger(__name__) +overlap_log = torch._logging.getArtifactLogger(__name__, "overlap") + +if TYPE_CHECKING: + from torch._inductor.scheduler import BaseSchedulerNode + + +def align_runtime_estimations_across_all_distributed_ranks( + snodes: list[BaseSchedulerNode], +): + from torch._inductor.scheduler import _get_mm_like_fn + + runtime_estimations = {} + runtime_estimations_for_mms = {} + + for snode in snodes: + runtime_estimations[snode] = snode.get_estimated_runtime() + if _get_mm_like_fn(snode) is not None: + runtime_estimations_for_mms[snode] = runtime_estimations[snode] + + import torch.distributed as dist + from torch.distributed.distributed_c10d import _get_default_group + + world_size = dist.get_world_size() + pg = _get_default_group() + gathered_runtime_estimations_for_mms: list[list[float]] = [ + [] for _ in range(world_size) + ] + dist.all_gather_object( + gathered_runtime_estimations_for_mms, + list(runtime_estimations_for_mms.values()), + pg, + ) + median_runtime_estimations_for_mms = torch.median( + torch.tensor(gathered_runtime_estimations_for_mms), dim=0 + ).values.tolist() + for idx, snode in enumerate(runtime_estimations_for_mms.keys()): + runtime_estimations_for_mms[snode] = median_runtime_estimations_for_mms[idx] + + for snode in snodes: + if snode in runtime_estimations_for_mms: + runtime_estimations[snode] = runtime_estimations_for_mms[snode] + snode.override_estimated_runtime = runtime_estimations[snode] + + +def sink_waits(snodes: list[BaseSchedulerNode]) -> list[BaseSchedulerNode]: + """ + Greedily schedules waits as late as possible. + """ + return _schedule_for_comm( + snodes, raise_comms=False, sink_waits=True, reorder_for_overlap=False + ) + + +def raise_comms(snodes: list[BaseSchedulerNode]) -> list[BaseSchedulerNode]: + """ + Greedily schedules comms as early as possible. + """ + return _schedule_for_comm( + snodes, raise_comms=True, sink_waits=False, reorder_for_overlap=False + ) + + +def reorder_compute_for_overlap( + snodes: list[BaseSchedulerNode], +) -> list[BaseSchedulerNode]: + """ + This achieves the following overall scheduling procedure: + Step 1: Given that we've currently scheduled comm N, we now schedule all compute nodes + that are required for comm N + 1 but do not depend on comm N, to run at the same time with comm N. + Step 2: If all those compute nodes are sufficient to overlap comm N, we're done. + Otherwise, we now need to look elsewhere to find compute that overlaps with comm N. + We prioritize compute nodes that are needed sooner. + Step 3: We schedule the compute nodes dependent on comm N and required for comm N + 1. + Step 4: We schedule comm N + 1. + Repeat this for subsequent comm nodes. + """ + return _schedule_for_comm( + snodes, raise_comms=True, sink_waits=True, reorder_for_overlap=True + ) + + +def reorder_communication_preserving_peak_memory( + snodes: list[BaseSchedulerNode], +) -> list[BaseSchedulerNode]: + """ + Reorders communication ops relative to computation ops to improve communication-compute overlapping and hide comm + latency. Stops moving a particular op if it reaches a point that would have increased the peak memory footprint. + + Currently, follows these heuristics (subject to change or tune): + - never reorders collectives relative to one another, for SPMD safety + - has an option for per-collective prefetch limit, but does not enable it by default + - limits the total number of reorder steps to some factor of the graph size to prevent worst-case quadratic + performance + + Prerequisite: sink_comms_and_waits - ensure comm and wait nodes are scheduled as late as possible, respecting data + dependencies. That allows reorder_communication_preserving_peak_memory to take a best case peak-memory snapshot, + and then monotonically improve latency by moving collectives backward in time. + + Peak memory impact is computed in an iterative fashion. First, memory use at each timestep is computed, and global + peak memory is computed as a max over timesteps. Then, when swapping any two adjacent nodes, only the curr-memory + for the earlier of the nodes after the swap is affected. This enables checking step by step whether a swap is + peak-memory-safe, and bailing out if not. Example: + + 0 n0 C0 + 1 n1 C0 + Allocs(n1) - Frees(n1) + 2 n2 C0 + Allocs(n1) - Frees(n1) + Allocs(n2) - Frees(n2) + + 0 n0 C0 + 1 n2 C0 + Allocs(n2) - Frees(n2) <-- After moving n2 to Time 1, only time1 memory changes + 2 n1 C0 + Allocs(n2) - Frees(n2) + Allocs(n1) - Frees(n1) + + """ + reordered_snodes, node_stats = ( + _reorder_communication_preserving_peak_memory_internal(snodes) + ) + + return reordered_snodes + + +@dataclass +class ReorderInfo: + """ + Debug info describing how an individual snode was reordered + """ + + limiting_factor: str = "None" + moves: int = 0 + grouped: int = 0 + grouped_info: str = "" + comm_time: float = -1.0 + comp_time: float = -1.0 + initial_exposed: float = -1.0 + final_exposed: float = -1.0 + overlap_info: str = "None" + + @property + def improvement(self): + return self.initial_exposed - self.final_exposed + + +def is_gemm_like(node: IRNode | Operation | None) -> bool: + if node is None: + return False + + if is_fallback_op( + node, # type: ignore[arg-type] + torch.ops.aten._scaled_dot_product_flash_attention.default, + ): + return True + + if ( + python_kernel_name := getattr(node, "python_kernel_name", None) + ) and "extern_kernels" in python_kernel_name: + return True + return False + + +def contains_gemm_like(snode: BaseSchedulerNode) -> bool: + from torch._inductor.scheduler import GroupedSchedulerNode + + if isinstance(snode, GroupedSchedulerNode): + return any(contains_gemm_like(x) for x in snode.snodes) + else: + return is_gemm_like(snode.node) + + +def _temp_group_visit_leaves(snode: BaseSchedulerNode, fn): + from torch._inductor.scheduler import GroupedSchedulerNode + + if isinstance(snode, GroupedSchedulerNode) and snode.temp_grouping: + for _snode in snode.snodes: + fn(_snode) + else: + fn(snode) + + +def _group_name(snode, with_bufs=False) -> str: + ret = "" + for n in snode.snodes: + if ret: + ret += "_" + ret += n.get_name() + if with_bufs: + ret += f"{list(snode.get_buffer_names())}" + return ret + + +def _is_fake_dep(d): + return isinstance(d, WeakDep) and d.is_fake + + +def _group_names(gns: list[BaseSchedulerNode]) -> str: + return "~".join([gn.get_name() for gn in gns]) + + +def _initialize_memory_tracking(snodes, graph_inputs, graph_outputs): + """Initialize memory tracking data structures""" + name_to_freeable_input_buf = get_freeable_input_buf(snodes, graph_inputs) + peak_memory, snodes_curr_memory, snodes_allocfree, buf_to_snode_last_use = ( + estimate_peak_memory_allocfree( + snodes, name_to_freeable_input_buf, graph_outputs + ) + ) + _curr_memory = dict(zip(snodes, snodes_curr_memory)) + # pyrefly: ignore [unsupported-operation] + _curr_memory[None] = (0, 0) + + # Build candidate buffer map for optimization + candidate_buffer_map = _build_candidate_buffer_map(buf_to_snode_last_use) + + return ( + peak_memory, + _curr_memory, + snodes_allocfree, + buf_to_snode_last_use, + name_to_freeable_input_buf, + candidate_buffer_map, + ) + + +def _initialize_double_linked_list( + snodes: list[BaseSchedulerNode], +) -> tuple[ + dict[BaseSchedulerNode, BaseSchedulerNode | None], + dict[BaseSchedulerNode, BaseSchedulerNode | None], + BaseSchedulerNode, +]: + """Create double-linked list structure from snodes""" + _prev = {} + _next = {} + for i, snode in enumerate(snodes): + _prev[snode] = snodes[i - 1] if i > 0 else None + _next[snode] = snodes[i + 1] if i < len(snodes) - 1 else None + _head = snodes[0] + return _prev, _next, _head + + +def _build_candidate_buffer_map( + buf_to_snode_last_use: dict, +) -> dict[BaseSchedulerNode, OrderedSet]: + """ + Build inverted index: node -> set of buffers where node appears in successors. + + This optimization reduces buffer iteration from O(total_buffers) to O(buffers_per_node). + Since buffer successors are immutable during reordering, this map doesn't need updates. + + Returns: + dict mapping each node to the set of buffers that have this node in their successors + """ + node_to_candidate_bufs: dict[BaseSchedulerNode, OrderedSet] = defaultdict( + OrderedSet + ) + + for buf in buf_to_snode_last_use: + # Add to every successor node's buffer set + for succ_node in buf.mpi_buffer.succ_nodes: + node_to_candidate_bufs[succ_node].add(buf) + + return dict(node_to_candidate_bufs) + + +def _precompute_node_output_sets( + snodes: list[BaseSchedulerNode], +) -> dict[BaseSchedulerNode, OrderedSet[str]]: + """ + Pre-compute output name sets for all nodes. + + This optimization avoids creating OrderedSet objects repeatedly during + exposed time calculations. + + Returns: + dict mapping each node to a set of its output names + """ + return { + snode: OrderedSet(o.get_name() for o in snode.get_outputs()) for snode in snodes + } + + +def _op_runtime_estimate_mult(snode): + # Apply multipliers for faster experimentation. + # TODO(ivankobzarev): Remove after confirmation that runtime estimations are correct. + if contains_collective(snode): + return config_comms.reorder_sink_runtime_estimations_comm_mult + + return config_comms.reorder_sink_runtime_estimations_non_comm_mult + + +def is_async_collective(snode): + """ + Filtering out ops that contain Collective and Wait inside and considered as Collectives. + See contains_collective function. + If the op contains Wait inside - consider as Synchronous compute. + """ + if python_kernel_name := getattr(snode.node, "python_kernel_name", None): + if "torch.ops._dtensor.shard_dim_alltoall.default" in python_kernel_name: + return False + + return True + + +def contains_async_collective(snode): + return contains_collective(snode, is_async_collective) + + +def _group_nodes_from_linked_list( + head: BaseSchedulerNode | None, + tail: BaseSchedulerNode | None, + next_dict: dict[BaseSchedulerNode, BaseSchedulerNode | None], +) -> list[BaseSchedulerNode]: + """ + Traverse doubly-linked list from head to tail and return nodes as a list. + + Args: + head: Starting node of the segment + tail: Ending node of the segment (inclusive) + next_dict: Dictionary mapping each node to its next node + + Returns: + List of nodes from head to tail (inclusive) + """ + ret = [] + n = head + while True: + if n is not None: + ret.append(n) + if n == tail: + break + n = next_dict[n] # type: ignore[index] + return ret + + +def _is_corresponding_collective_wait( + collective_snode: BaseSchedulerNode, + wait_snode: BaseSchedulerNode, + node_output_sets: dict[BaseSchedulerNode, frozenset[str]], + node_dep_sets: dict[BaseSchedulerNode, frozenset[str]], +) -> bool: + """ + Check if a wait node corresponds to a given collective node. + Uses pre-computed sets for O(1) lookup. + """ + collective_outs = node_output_sets[collective_snode] + unmet_deps = node_dep_sets[wait_snode] + return bool(unmet_deps & collective_outs) + + +def _coll_exposed_communication_time( + collective_snode: BaseSchedulerNode, + next_dict: dict[BaseSchedulerNode, BaseSchedulerNode | None], + runtimes: dict[BaseSchedulerNode, float], + node_output_sets: dict[BaseSchedulerNode, frozenset[str]], + node_dep_sets: dict[BaseSchedulerNode, frozenset[str]], +) -> tuple[float, float, str]: + """ + Calculate exposed communication time by iterating directly over linked list. + Avoids O(N) list construction for each call. + + The collective_snode is the starting point, iteration continues via next_dict. + """ + comm_time = runtimes[collective_snode] + comp_time = 0.0 + collective_outs = node_output_sets[collective_snode] + overlap_info = "" + collectives_found: list[BaseSchedulerNode] = [] + + snode = next_dict[collective_snode] + while snode is not None: + unmet_deps = node_dep_sets[snode] + + if unmet_deps & collective_outs: + overlap_info += f"->W[{snode.get_name()}]" + break + + if contains_collective(snode): + if not contains_async_collective(snode): + break + else: + collectives_found.append(snode) + snode = next_dict[snode] + continue + if contains_wait(snode): + has_wait_for_collectives_found = False + for _coll in collectives_found: + if _is_corresponding_collective_wait( + collective_snode, snode, node_output_sets, node_dep_sets + ): + has_wait_for_collectives_found = True + break + if has_wait_for_collectives_found: + break + + comp_time_before = comp_time + + def accumulate_time(_snode: BaseSchedulerNode) -> None: + nonlocal comp_time + comp_time += runtimes[_snode] + + _temp_group_visit_leaves(snode, accumulate_time) + comp_time_after = comp_time + overlap_info += f"+{snode.get_name()}[{comp_time_after - comp_time_before}]" + + snode = next_dict[snode] + + return comm_time, comp_time, overlap_info + + +def _wait_exposed_communication_time( + wait_snode: BaseSchedulerNode, + head: BaseSchedulerNode, + prev_dict: dict[BaseSchedulerNode, BaseSchedulerNode | None], + runtimes: dict[BaseSchedulerNode, float], + node_output_sets: dict[BaseSchedulerNode, frozenset[str]], + node_dep_sets: dict[BaseSchedulerNode, frozenset[str]], +) -> tuple[float, float, str]: + """ + Calculate exposed communication time for a wait operation by iterating + directly over linked list backwards. Avoids O(N) list construction. + + Iterates from wait_snode backwards using prev_dict to find corresponding collective. + """ + comm_time = 0.0 + comp_time = 0.0 + overlap_info = "" + waits_found: list[BaseSchedulerNode] = [] + + snode = prev_dict[wait_snode] + while snode is not None: + if contains_wait(snode): + waits_found.append(snode) + if contains_collective(snode): + if _is_corresponding_collective_wait( + snode, wait_snode, node_output_sets, node_dep_sets + ): + comm_time = runtimes[snode] + overlap_info += f"->C[{snode.get_name()}]" + break + + if not contains_async_collective(snode): + comp_time = 0.0 + snode = prev_dict[snode] + continue + else: + for w in waits_found: + if _is_corresponding_collective_wait( + snode, w, node_output_sets, node_dep_sets + ): + comp_time = 0.0 + break # inner loop break + snode = prev_dict[snode] + continue + + comp_time_before = comp_time + + def accumulate_time(_snode: BaseSchedulerNode) -> None: + nonlocal comp_time + comp_time += runtimes[_snode] + + _temp_group_visit_leaves(snode, accumulate_time) + comp_time_after = comp_time + overlap_info += f"+{snode.get_name()}[{comp_time_after - comp_time_before}]" + + snode = prev_dict[snode] + + return comm_time, comp_time, overlap_info + + +def _perform_double_linked_list_swap( + candidate: BaseSchedulerNode, + group_head: BaseSchedulerNode, + group_tail: BaseSchedulerNode, + prev_dict: dict[BaseSchedulerNode, BaseSchedulerNode | None], + next_dict: dict[BaseSchedulerNode, BaseSchedulerNode | None], + head: BaseSchedulerNode, +) -> BaseSchedulerNode: + """ + Swap positions of candidate and group in doubly-linked list. + + Transforms: + candidate_prev -> candidate -> group_head...group_tail -> group_tail_next + Into: + candidate_prev -> group_head...group_tail -> candidate -> group_tail_next + + Args: + candidate: Node to swap with group + group_head: First node of group + group_tail: Last node of group + prev_dict: Dictionary mapping nodes to their previous nodes + next_dict: Dictionary mapping nodes to their next nodes + head: Current head of the linked list + + Returns: + New head of the linked list (may change if candidate was the head) + """ + # 0: Update candidate's previous node + candidate_prev = prev_dict[candidate] + if candidate_prev: + next_dict[candidate_prev] = group_head + prev_dict[group_head] = candidate_prev + + # 2: Update group_tail's next node + group_tail_next = next_dict[group_tail] + if group_tail_next: + prev_dict[group_tail_next] = candidate + next_dict[candidate] = group_tail_next + + # 1: Link group_tail to candidate + prev_dict[candidate] = group_tail + next_dict[group_tail] = candidate + + # Update head if candidate was the head + if head == candidate: + return group_head + return head + + +def _calculate_potential_peak_memory_reorder( + candidate: BaseSchedulerNode, + gns: list[BaseSchedulerNode], + group_tail: BaseSchedulerNode, + group_peak_memory: int, + candidate_delta_mem: int, + candidate_allocfree: SNodeMemory, + group_n_to_bufs_after_swap_dealloc_by_candidate: dict, + curr_memory: dict, +) -> tuple[int, dict[BaseSchedulerNode, int]]: + """ + Calculate potential peak memory after swapping candidate with group (reorder version). + + Computes new memory levels for all affected nodes and returns the potential + peak memory along with cached post-allocation memory values for each node. + + Args: + candidate: Node being moved + gns: Group nodes + group_tail: Last node of group + group_peak_memory: Current peak memory within the group + candidate_delta_mem: Net memory change from candidate (alloc - free) + candidate_allocfree: Candidate's allocation/free info + group_n_to_bufs_after_swap_dealloc_by_candidate: Buffers whose deallocation moves to candidate + curr_memory: Current memory state dict + + Returns: + Tuple of (potential_peak_memory, post_alloc_update_dict) + """ + # Caching calculations of memory for group nodes and candidate, + # to apply without recalculation after swap. + _post_alloc_update: dict[BaseSchedulerNode, int] = {} + potential_peak: int = 0 + if not group_n_to_bufs_after_swap_dealloc_by_candidate: + # Not accounting for buffers last use change + potential_peak = max( + group_peak_memory - candidate_delta_mem, + curr_memory[group_tail][1] + - candidate_delta_mem + + candidate_allocfree.size_alloc, + ) + return potential_peak, _post_alloc_update + + # If candidate will be after group, the starting memory level of group nodes + # changes to the -(candidate.size_alloc - candidate.size_free) + mem_after_reorder_delta: int = -candidate_delta_mem + for gn in gns: + gn_post_alloc_mem = curr_memory[gn][0] + mem_after_reorder_delta + _post_alloc_update[gn] = gn_post_alloc_mem + potential_peak = max(potential_peak, gn_post_alloc_mem) + + bufs = group_n_to_bufs_after_swap_dealloc_by_candidate.get(gn) + if bufs is not None: + for buf in bufs: + # Candidate will deallocate those buffers + mem_after_reorder_delta += buf.mpi_buffer.size_free + + candidate_mem_post_alloc = ( + curr_memory[group_tail][1] + + mem_after_reorder_delta + + candidate_allocfree.size_alloc + ) + _post_alloc_update[candidate] = candidate_mem_post_alloc + potential_peak = max(potential_peak, candidate_mem_post_alloc) + return potential_peak, _post_alloc_update + + +def _update_memory_tracking_after_swap_reorder( + candidate: BaseSchedulerNode, + gns: list[BaseSchedulerNode], + group_tail: BaseSchedulerNode, + candidate_delta_mem: int, + candidate_allocfree: SNodeMemory, + group_n_to_bufs_after_swap_dealloc_by_candidate: dict, + post_alloc_update: dict[BaseSchedulerNode, int], + curr_memory: dict, + buf_to_snode_last_use: dict, + snodes_allocfree: dict, +) -> None: + """ + Update memory tracking structures after swap (reorder version). + + Updates curr_memory, buf_to_snode_last_use, and snodes_allocfree dictionaries + to reflect the new memory state after swapping candidate with group. + + Args: + candidate: Node that was moved + gns: Group nodes + group_tail: Last node of group + candidate_delta_mem: Net memory change from candidate (alloc - free) + candidate_allocfree: Candidate's allocation/free info + group_n_to_bufs_after_swap_dealloc_by_candidate: Buffers whose deallocation moves to candidate + post_alloc_update: Cached post-allocation memory values + curr_memory: Current memory state dict (mutated) + buf_to_snode_last_use: Buffer to last-use node mapping (mutated) + snodes_allocfree: Node allocation/free info dict (mutated) + """ + if not group_n_to_bufs_after_swap_dealloc_by_candidate: + for gn in gns: + cm = curr_memory[gn] + curr_memory[gn] = ( + cm[0] - candidate_delta_mem, + cm[1] - candidate_delta_mem, + ) + _candidate_post_alloc_mem = ( + curr_memory[group_tail][1] + candidate_allocfree.size_alloc + ) + _candidate_post_free_mem = ( + _candidate_post_alloc_mem - candidate_allocfree.size_free + ) + curr_memory[candidate] = ( + _candidate_post_alloc_mem, + _candidate_post_free_mem, + ) + return + + # Candidate becomes last use of some bufs + for bufs in group_n_to_bufs_after_swap_dealloc_by_candidate.values(): + for buf in bufs: + buf_to_snode_last_use[buf] = candidate + + size_free_to_move_to_candidate_sum: int = 0 + for n in gns: + _gn_post_alloc_mem: int = post_alloc_update[n] + size_free_to_move_to_candidate: int = sum( + buf.mpi_buffer.size_free + for buf in group_n_to_bufs_after_swap_dealloc_by_candidate[n] + ) + size_free_to_move_to_candidate_sum += size_free_to_move_to_candidate + # group node does not deallocate this after swap + snodes_allocfree[n].size_free -= size_free_to_move_to_candidate + gn_post_free_mem: int = _gn_post_alloc_mem - snodes_allocfree[n].size_free + curr_memory[n] = (_gn_post_alloc_mem, gn_post_free_mem) + _candidate_post_alloc_mem = post_alloc_update[candidate] + snodes_allocfree[candidate].size_free += size_free_to_move_to_candidate_sum + candidate_post_free_mem = ( + _candidate_post_alloc_mem - snodes_allocfree[candidate].size_free + ) + curr_memory[candidate] = ( + _candidate_post_alloc_mem, + candidate_post_free_mem, + ) + + +def _find_buffers_with_changed_last_use( + candidate: BaseSchedulerNode, + gns: list[BaseSchedulerNode], + buf_to_snode_last_use: dict, + candidate_buffer_map: dict[BaseSchedulerNode, OrderedSet], +) -> dict[BaseSchedulerNode, list[FreeableInputBuffer | Any]]: + """ + Find buffers whose last use will change after swapping candidate with group. + + When we swap [candidate [group]] to [[group] candidate], some buffers that + were last used by a group node will now be last used by candidate instead. + This affects memory deallocation timing. + + Args: + candidate: The node being moved + gns: Group nodes being swapped with candidate + buf_to_snode_last_use: Mapping of buffers to their current last-use nodes + candidate_buffer_map: Pre-computed map of node -> buffers using that node + + Returns: + Dict mapping group nodes to buffers that will change their last-use node + """ + group_n_to_bufs_after_swap_dealloc_by_candidate: dict[ + BaseSchedulerNode, list[FreeableInputBuffer | Any] + ] = defaultdict(list) + + # Optimization: only check buffers where candidate is a successor + # Reduces from O(all_buffers) to O(buffers_per_candidate) + candidate_bufs = candidate_buffer_map.get(candidate, OrderedSet()) + gns_set = OrderedSet(gns) # O(1) membership testing + + for buf in candidate_bufs: + snode_last_use = buf_to_snode_last_use[buf] + if snode_last_use in gns_set: + group_n_to_bufs_after_swap_dealloc_by_candidate[snode_last_use].append(buf) + + return group_n_to_bufs_after_swap_dealloc_by_candidate + + +def _is_node_groupable_for_reorder( + candidate: BaseSchedulerNode, +) -> tuple[bool, str | None]: + """ + Check if a candidate node can be grouped with collective during reordering. + + This pass processes collectives left to right, so we avoid grouping with + already-processed collectives based on configuration. + + Args: + candidate: Node to check for groupability + + Returns: + Tuple of (is_groupable, reason_if_not_groupable) + """ + # This pass processes collectives left to right, + # Do not group with processed collectives. + # Leaving config for experimentation in 2D + if not config_comms.reorder_iterative_group_with_collectives: + if contains_async_collective(candidate): + return ( + False, + f"candidate contains_collective {candidate.get_name()}", + ) + if not config_comms.reorder_iterative_use_runtime_estimations: + if contains_gemm_like(candidate): + return False, "contains_gemm_like" + return True, None + + +def _format_and_log_reordering_stats( + stats: dict[BaseSchedulerNode, ReorderInfo], + head: BaseSchedulerNode, + next_dict: dict[BaseSchedulerNode, BaseSchedulerNode | None], + original_snodes_num: int, + peak_memory: int, + name_to_freeable_input_buf: dict, + graph_outputs: OrderedSet[str], +) -> list[BaseSchedulerNode]: + """ + Format reordering statistics, log them, and return final node list. + + Computes improvement metrics, creates a formatted table (using tabulate if + available), validates the reordered node count, recalculates peak memory, + and logs all information. + + Args: + stats: Per-node reordering statistics + head: Head of the reordered linked list + next_dict: Linked list next pointers + original_snodes_num: Original number of nodes (for validation) + peak_memory: Initial peak memory before reordering + name_to_freeable_input_buf: Buffer memory tracking info + graph_outputs: Graph output names + + Returns: + Final reordered list of scheduler nodes + """ + node_stats = stats + improvement = {snode: node_stats[snode].improvement for snode in node_stats} + total_improvement = sum([improvement[snode] for snode in improvement]) + total_moves = sum([node_stats[snode].moves for snode in node_stats]) + + reorder_log_str = ( + f"reorder_communication_preserving_peak_memory improved overlap by {total_improvement} ns" + f" after {total_moves} reorders.\n" + ) + headers = [ + "Collective node", + "comm_time(us)", + "comp_time(us)", + "initial exposed(us)", + "final exposed(us)", + "improvement(us)", + "limiting factor", + "moves", + "grouped", + "grouped_info", + "overlap_info", + ] + rows = [ + [ + node_summary(snode), + node_info.comm_time / 1e3, + node_info.comp_time / 1e3, + node_info.initial_exposed / 1e3, + node_info.final_exposed / 1e3, + node_info.improvement / 1e3, + node_info.limiting_factor, + node_info.moves, + node_info.grouped, + node_info.grouped_info, + node_info.overlap_info, + ] + for snode, node_info in node_stats.items() + ] + if importlib.util.find_spec("tabulate"): + from tabulate import tabulate + + reorder_log_str += tabulate( + rows, + headers=headers, + ) + else: + reorder_log_str += ( + "Please `pip install tabulate` to nicely render overlap stats.\n" + ) + reorder_log_str += str(headers) + "\n" + reorder_log_str += "\n".join(map(str, rows)) + + new_snodes = _group_nodes_from_linked_list(head, None, next_dict) + assert len(new_snodes) == original_snodes_num + new_peak_memory, _, _, _ = estimate_peak_memory_allocfree( + new_snodes, name_to_freeable_input_buf, graph_outputs + ) + reorder_log_str += f"\n peak_memory_before:{peak_memory}" + reorder_log_str += f"\n peak_memory_after:{new_peak_memory}" + + overlap_log.info(reorder_log_str) + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "reorder_communication_preserving_peak_memory", + "encoding": "string", + }, + payload_fn=lambda: reorder_log_str, + ) + + return new_snodes + + +def _reorder_communication_preserving_peak_memory_internal( + snodes: list[BaseSchedulerNode], +) -> tuple[list[BaseSchedulerNode], dict[BaseSchedulerNode, ReorderInfo]]: + """ + Internal testing helper that also returns debug info. + Returns: + - reordered snodes list + - dict {snode: ReorderInfo} + """ + has_collectives = False + for snode in snodes: + if contains_collective(snode): + has_collectives = True + break + if not has_collectives: + return snodes, {} + + original_snodes_num = len(snodes) + # heuristic to avoid degenerating to quadratic time + graph_inputs: OrderedSet[str] = OrderedSet(V.graph.graph_inputs.keys()) + graph_outputs: OrderedSet[str] = OrderedSet(V.graph.get_output_names()) + ( + peak_memory, + _curr_memory, + snodes_allocfree, + buf_to_snode_last_use, + name_to_freeable_input_buf, + candidate_buffer_map, + ) = _initialize_memory_tracking(snodes, graph_inputs, graph_outputs) + + runtimes: dict[BaseSchedulerNode, float] = { + snode: estimate_op_runtime(snode) * _op_runtime_estimate_mult(snode) + for snode in snodes + } + + # Pre-compute output and dependency sets for O(1) lookup instead of O(N) creation per iteration + node_output_sets: dict[BaseSchedulerNode, frozenset[str]] = { + snode: frozenset(o.get_name() for o in snode.get_outputs()) for snode in snodes + } + node_dep_sets: dict[BaseSchedulerNode, frozenset[str]] = { + snode: frozenset( + d.name for d in snode.unmet_dependencies if not _is_fake_dep(d) + ) + for snode in snodes + } + + # debug stats + stats: dict[BaseSchedulerNode, ReorderInfo] = {} + + total_moves = 0 + + _prev, _next, _head = _initialize_double_linked_list(snodes) + + debug_num_collectives_to_reorder: int | None = ( + config_comms.reorder_iterative_debug_limit_to_reorder + ) + + num_processed_collectives: int = 0 + curr: BaseSchedulerNode | None = _head + debug_iterative_memory_recompute = ( + config_comms.reorder_iterative_debug_memory_recompute + ) + iterative_recompute_error = False + + while curr is not None and _next[curr] is not None: + _next_curr = _next[curr] + if iterative_recompute_error: + break + + if not contains_async_collective(curr): + curr = _next_curr + continue + + if debug_num_collectives_to_reorder is not None and ( + num_processed_collectives >= debug_num_collectives_to_reorder + ): + break + num_processed_collectives += 1 + + info = stats[curr] = ReorderInfo() + comm_time, comp_time, overlap_info = _coll_exposed_communication_time( + curr, _next, runtimes, node_output_sets, node_dep_sets + ) + info.comm_time = comm_time + info.comp_time = comp_time + info.initial_exposed = info.final_exposed = comm_time - comp_time + info.overlap_info = overlap_info + + candidate = _prev[curr] + group_head = curr + group_tail = curr + group_waits = {} + group_runtime = 0.0 + group_peak_memory = _curr_memory[curr][0] # post_alloc memory + + # Track group dependencies incrementally - initialize from pre-computed sets + group_unmet_deps_names = OrderedSet(node_dep_sets[curr]) + group_output_names = OrderedSet(node_output_sets[curr]) + + while candidate is not None: + if config_comms.reorder_iterative_use_runtime_estimations and ( + info.final_exposed + < -config_comms.reorder_iterative_extra_comm_comp_overlap + * info.comm_time + ): + info.limiting_factor = "unexposed by runtime estimations" + break + + if ( + not config_comms.reorder_iterative_unsafe_collectives_reorder + and contains_collective(candidate) + ): + info.limiting_factor = "collective ordering" + break + + # Early exit: if group has no unmet dependencies, candidate can't have data dependency + data_deps_names = group_unmet_deps_names - group_output_names + if not data_deps_names: + data_dep = False + else: + # Calculate effective dependencies (not satisfied within group) + # Use pre-computed set for O(1) lookup + candidate_out_names = node_output_sets[candidate] + data_dep = bool(candidate_out_names & data_deps_names) + + if data_dep: + is_groupable_result, grouping_reason = _is_node_groupable_for_reorder( + candidate + ) + if is_groupable_result: + group_head = candidate + + # Update incremental dependency tracking using pre-computed sets + group_unmet_deps_names.update(node_dep_sets[candidate]) + group_output_names.update(node_output_sets[candidate]) + + if config_comms.reorder_iterative_use_runtime_estimations: + if contains_wait(candidate): + comm_time, comp_time, _ = _wait_exposed_communication_time( + candidate, + _head, + _prev, + runtimes, + node_output_sets, + node_dep_sets, + ) + group_waits[candidate] = comm_time, comp_time + if not contains_async_collective(candidate): + group_runtime += runtimes[candidate] + + group_peak_memory = max( + group_peak_memory, _curr_memory[candidate][0] + ) + info.grouped += 1 + candidate = _prev[candidate] + continue + else: + msg = ( + f"data dependency detected" + f"\n candidate:{candidate.get_name()}(outs:{[o.get_name() for o in candidate.get_outputs()]})" + f"\n non_group_reason:{grouping_reason}" + ) + info.limiting_factor = msg + break + + if config_comms.reorder_iterative_use_runtime_estimations: + # Check if candidate has sync runtime + if not contains_async_collective(candidate): + c_runtime = runtimes[candidate] + + if c_runtime > 0 and len(group_waits) > 0: + # pyrefly: ignore[no-matching-overload] + exposed_before = max(0, info.comm_time - info.comp_time) + # pyrefly: ignore[no-matching-overload] + exposed_after = max( + 0, info.comm_time - info.comp_time - c_runtime + ) + exposed_delta = exposed_after - exposed_before + for gw_comm_time, gw_comp_time in group_waits.values(): + # pyrefly: ignore [no-matching-overload] + gw_exposed_before = max(0, gw_comm_time - gw_comp_time) + # pyrefly: ignore [no-matching-overload] + gw_exposed_after = max( + 0, gw_comm_time - gw_comp_time + c_runtime + ) + + exposed_delta += gw_exposed_after - gw_exposed_before + + if exposed_delta > 0: + info.limiting_factor = ( + f"candidate has compute {c_runtime}," + f" group contains waits, total_exposed_delta {exposed_delta}" + ) + break + else: + # Update all group_colls comm_time, comp_time + for gw, ( + gw_comm_time, + gw_comp_time, + ) in group_waits.items(): + group_waits[gw] = ( + gw_comm_time, + gw_comp_time - c_runtime, + ) + else: + # Candidate is async_collective + + # Unsafe collectives reordering + # Cj -> [...group_runtime..., Ci] -> Wj + # Checking that we are not increasing exposed time of Cj + if group_runtime > 0: + comm_time, comp_time, _ = _coll_exposed_communication_time( + candidate, _next, runtimes, node_output_sets, node_dep_sets + ) + # pyrefly: ignore[no-matching-overload] + exposed_before = max(0, comm_time - comp_time) + # pyrefly: ignore[no-matching-overload] + exposed_after = max(0, comm_time - comp_time + group_runtime) + exposed_delta = exposed_after - exposed_before + if exposed_delta > 0: + info.limiting_factor = ( + f"candidate {candidate.get_name()} is collective," + f" group_runtime:{group_runtime}," + f" exposed_delta:{exposed_delta} c_comm_time:{comm_time} c_comp_time:{comp_time}" + ) + break + + # Create group nodes list once for swap operations + gns: list[BaseSchedulerNode] = _group_nodes_from_linked_list( + group_head, group_tail, _next + ) + + candidate_allocfree: SNodeMemory = snodes_allocfree[candidate] + candidate_delta_mem: int = ( + candidate_allocfree.size_alloc - candidate_allocfree.size_free + ) + # candidate and one of group nodes are successors of the same buffer + # and last use of the buffer happen in group nodes. + # This last use deallocates it. + # If we swap [candidate [group]] to [[group] candidate], + # candidate becomes the last use + # and deallocated this buffer instead of group node. + # we need to update size_free accordingly to group_node and candidate, + # and recalculate post_alloc, post_free for them. + # + # Buf that changes its last use snode, + # after swap will be deallocated only by candidate, + # while before it was deallocated by group node. + group_n_to_bufs_after_swap_dealloc_by_candidate = ( + _find_buffers_with_changed_last_use( + candidate, gns, buf_to_snode_last_use, candidate_buffer_map + ) + ) + + potential_peak, _post_alloc_update = ( + _calculate_potential_peak_memory_reorder( + candidate, + gns, + group_tail, + group_peak_memory, + candidate_delta_mem, + candidate_allocfree, + group_n_to_bufs_after_swap_dealloc_by_candidate, + _curr_memory, + ) + ) + + if ( + potential_peak - peak_memory + > peak_memory * config_comms.reorder_iterative_peak_memory_budget + ): + info.limiting_factor = ( + f"peak memory new:{potential_peak} vs base:{peak_memory}" + ) + break + info.moves += 1 + total_moves += 1 + + _head = _perform_double_linked_list_swap( + candidate, group_head, group_tail, _prev, _next, _head + ) + + comm_time, comp_time, overlap_info = _coll_exposed_communication_time( + curr, _next, runtimes, node_output_sets, node_dep_sets + ) + info.comm_time = comm_time + info.comp_time = comp_time + info.overlap_info = overlap_info + info.final_exposed = comm_time - comp_time + + _update_memory_tracking_after_swap_reorder( + candidate, + gns, + group_tail, + candidate_delta_mem, + candidate_allocfree, + group_n_to_bufs_after_swap_dealloc_by_candidate, + _post_alloc_update, + _curr_memory, + buf_to_snode_last_use, + snodes_allocfree, + ) + + if debug_iterative_memory_recompute: + # Compare iteratively recomputed memory data + # with full run of estimate_peak_memory + + from .comms_debug import _debug_iterative_memory_recompute + + iterative_recompute_error = _debug_iterative_memory_recompute( + candidate, + gns, + _group_names(gns), + _group_nodes_from_linked_list(_head, None, _next), + name_to_freeable_input_buf, + graph_outputs, + peak_memory, + _curr_memory, + snodes_allocfree, + "reorder_communication_preserving_peak_memory", + group_n_to_bufs_after_swap_dealloc_by_candidate, + ) + if iterative_recompute_error: + break + candidate = _prev[group_head] + curr = _next_curr + + if not config_comms.reorder_sink_verbose_logging: + new_snodes = _group_nodes_from_linked_list(_head, None, _next) + return new_snodes, stats + + new_snodes = _format_and_log_reordering_stats( + stats, + _head, + _next, + original_snodes_num, + peak_memory, + name_to_freeable_input_buf, + graph_outputs, + ) + + return new_snodes, stats + + +def _schedule_for_comm( + snodes: list[BaseSchedulerNode], + raise_comms: bool, + sink_waits: bool, + reorder_for_overlap: bool, +) -> list[BaseSchedulerNode]: + """ + Schedule `snodes` for various comm optimization objectives. + + Args: + snodes: the nodes to be scheduled. + raise_comms: whether to greedily schedule collectives as early as possible + sink_wait: whether to greedily schedule waits as late as possible + reorder_compute_for_overlap: whether to reorder compute nodes to + optimize for compute/communication overlapping. + + Returns: + The new schedule order. + + Some notes on the synergy between different options: + - `raise_comms` provides more overlapping oppurtunies for `reorder_compute_for_overlap`. + - When both `raise_comms` and `sink_waits` is `True`, `raise_comms` is prioritized. + """ + # We assign each node a tuple of scores (score_0, score_1, score_2), + # decreasing in importance, with a lower value indicating a higher ranking: + # + # - score_0: the lowest comm_idx among the comm nodes that the node blocks. + # If a node doesn't block any comm nodes, its score_0 is set to + # sys.maxsize. This score ensures that comm nodes get scheduled as early as + # possible. + # - score_1: 1 if the node is a wait node, 0 otherwise. This score ensures + # that wait nodes are deferred as late as possible. + # - score_2: the index of the node in the original topological order. This + # score provides stability in case of ties. + # + # When only raise_comms is True, only score_0 and score_2 are considered. + # When only sink_waits is True, only score_1 and score_2 are considered. + # When neither is True, the original order is yielded. + buf_name_to_snode = {} + name_to_fused_node = {} + scores_0, scores_1, scores_2 = {}, {}, {} + for idx, snode in enumerate(snodes): + for buf_name in snode.get_buffer_names(): + buf_name_to_snode[buf_name] = snode + + for op_name in snode.get_operation_names(): + name_to_fused_node[op_name] = snode + name_to_fused_node[snode.get_name()] = snode + + node_name = snode.get_name() + scores_0[node_name] = sys.maxsize + scores_1[node_name] = 0 + scores_2[node_name] = idx + + comm_idx = 0 + for snode in snodes: + if raise_comms and contains_collective(snode): + scores_0[snode.get_name()] = comm_idx + for ancestor in snode.ancestors: + anc_fused_name = name_to_fused_node[ancestor].get_name() + scores_0[anc_fused_name] = min(scores_0[anc_fused_name], comm_idx) + comm_idx += 1 + elif sink_waits and contains_wait(snode): + scores_1[snode.get_name()] = 1 + + class Runnable: + def __init__(self, snode) -> None: + self.snode = snode + name = next(iter(snode.get_operation_names())) + fused_name = name_to_fused_node[name].get_name() + self.score = ( + scores_0[fused_name], + scores_1[fused_name], + scores_2[fused_name], + ) + + def __lt__(self, other): + return self.score < other.score + + unmet_deps: dict[BaseSchedulerNode, OrderedSet[str]] = { + snode: OrderedSet(dep.name for dep in snode.unmet_dependencies) + for snode in snodes + } + + ready: list[Runnable] = [] + buffer_users: dict[str, OrderedSet[BaseSchedulerNode]] = defaultdict(OrderedSet) + snode_to_cost = {snode: estimate_op_runtime(snode) for snode in snodes} + + for snode, deps in unmet_deps.items(): + if len(deps) == 0: + heapq.heappush(ready, Runnable(snode)) + for dep in deps: + buffer_users[dep].add(snode) + + scheduled = [] + + def schedule(snode): + """ + Schedules `snode` and put all unblocked nodes onto the ready queue. + """ + scheduled.append(snode) + for buf_name in snode.get_buffer_names(): + for snode in buffer_users[buf_name]: + unmet_deps[snode].remove(buf_name) + if len(unmet_deps[snode]) == 0: + heapq.heappush(ready, Runnable(snode)) + + def get_overlapping_candidate(): + """ + Return the next node in the ready queue that's neither a collective or + a wait. + """ + candidates = [ + x + for x in ready + if not contains_collective(x.snode) and not contains_wait(x.snode) + ] + if len(candidates) == 0: + return None + return min(candidates, key=lambda x: x.score) + + def schedule_collective_for_overlap(snode): + """ + Schedules collective node `snode`, along with one or more compute nodes + to overlap with it. The strategy is described in the comment of + `reorder_compute_for_overlap`. + """ + assert contains_collective(snode) + schedule(snode) + + collective_cost = snode_to_cost[snode] + while ( + collective_cost > 0 + and (candidate := get_overlapping_candidate()) is not None + ): + ready.remove(candidate) + + schedule(candidate.snode) + + collective_cost -= snode_to_cost[candidate.snode] + heapq.heapify(ready) + + while ready: + snode = heapq.heappop(ready).snode + if reorder_for_overlap and contains_collective(snode): + schedule_collective_for_overlap(snode) + else: + schedule(snode) + + for deps in unmet_deps.values(): + assert len(deps) == 0, ( + f"Detected unscheduled nodes. Nodes with unmet dependencies: {unmet_deps}" + ) + return scheduled + + +def decide_global_ordering_of_comms( + nodes: list[BaseSchedulerNode], name_to_buf, name_to_fused_node +) -> list[BaseSchedulerNode]: + """ + Decide global ordering of comms, by just enforcing the ordering that's in the input graph + (might not be the same ordering as the eager mode program). + TODO: Come up with a better approach + """ + if not torch.distributed.is_available(): + return nodes + + comm_nodes = [n for n in nodes if contains_collective(n)] + + for i in range(1, len(comm_nodes)): + # Enforce ordering by making previous comm a `WeakDep` dependency of the next comm + mutating_buf = next(iter(comm_nodes[i].get_buffer_names())) + for buf in comm_nodes[i - 1].get_buffer_names(): + comm_nodes[i].add_fake_dep( + WeakDep(buf, mutating_buf=mutating_buf, is_fake=True) + ) + + return nodes + + +@dataclass +class SinkWaitInfo: + grouped: int = 0 + grouped_info: str = "" + moves: int = 0 + moves_info: str = "" + limiting_factor: str = "None" + comm_time: float = -1.0 + comp_time: float = -1.0 + initial_exposed: float = -1.0 + final_exposed: float = -1.0 + overlap_info: str = "None" + + @property + def improvement(self): + return self.initial_exposed - self.final_exposed + + +def _is_node_groupable_for_sink_waits( + candidate: BaseSchedulerNode, +) -> tuple[bool, str | None]: + """ + Check if a candidate node can be grouped during sink_waits pass. + + Sink Waits traverses waits right to left, so we don't group with + processed waits on the right or with async collectives. + + Args: + candidate: Node to check for groupability + + Returns: + Tuple of (is_groupable, reason_if_not_groupable) + """ + # Sink Waits traverse Waits right to left, + # => we do not group with processed Waits on the right. + if contains_wait(candidate): + return False, f"candidate contains wait {candidate.get_name()}" + if contains_async_collective(candidate): + return ( + False, + f"candidate contains_async_collective {candidate.get_name()}", + ) + + if not config_comms.sink_iterative_use_runtime_estimations: + # Heuristics pre-use_runtime_estimations: + # TODO(ivankobzarev): Remove them after confirming, + # that using runtime estimations always give better results. + # We do not want to group with collectives to not reorder them forward. + if contains_collective(candidate): + return ( + False, + f"candidate contains collective {candidate.get_name()}", + ) + if contains_gemm_like(candidate): + return ( + False, + f"candidate contains gemm_like {candidate.get_name()}", + ) + return True, None + + +def _update_memory_tracking_after_swap_sink_waits( + candidate: BaseSchedulerNode, + gns: list[BaseSchedulerNode], + candidate_delta_mem: int, + candidate_allocfree: SNodeMemory, + group_n_to_bufs_after_swap_dealloc_instead_of_candidate: dict, + post_alloc_update: dict[BaseSchedulerNode, int], + size_free_delta_update: dict[BaseSchedulerNode, int], + curr_memory: dict, + snodes_allocfree: dict, +) -> None: + """ + Update memory tracking structures after swap (sink_waits version). + + Updates curr_memory and snodes_allocfree dictionaries to reflect the new + memory state after swapping candidate with group. + + Args: + candidate: Node that was moved + gns: Group nodes + candidate_delta_mem: Net memory change from candidate (alloc - free) + candidate_allocfree: Candidate's allocation/free info + group_n_to_bufs_after_swap_dealloc_instead_of_candidate: Buffers whose deallocation moves from candidate to group + post_alloc_update: Cached post-allocation memory values + size_free_delta_update: Cached size-free delta values + curr_memory: Current memory state dict (mutated) + snodes_allocfree: Node allocation/free info dict (mutated) + """ + group_head = gns[0] + pre_group_mem = curr_memory[group_head][0] - snodes_allocfree[group_head].size_alloc + if not group_n_to_bufs_after_swap_dealloc_instead_of_candidate: + candidate_post_alloc = pre_group_mem + candidate_allocfree.size_alloc + curr_memory[candidate] = ( + candidate_post_alloc, + candidate_post_alloc - candidate_allocfree.size_free, + ) + for gn in gns: + cm = curr_memory[gn] + curr_memory[gn] = ( + cm[0] + candidate_delta_mem, + cm[1] + candidate_delta_mem, + ) + return + + for n in [candidate, *gns]: + post_alloc = post_alloc_update[n] + snodes_allocfree[n].size_free += size_free_delta_update.get(n, 0) + curr_memory[n] = ( + post_alloc, + post_alloc - snodes_allocfree[n].size_free, + ) + + +def _calculate_potential_peak_memory_sink_waits( + candidate: BaseSchedulerNode, + gns: list[BaseSchedulerNode], + group_head: BaseSchedulerNode, + group_peak_memory: int, + candidate_delta_mem: int, + candidate_allocfree: SNodeMemory, + group_n_to_bufs_after_swap_dealloc_instead_of_candidate: dict, + curr_memory: dict, + snodes_allocfree: dict, +) -> tuple[int, dict[BaseSchedulerNode, int], dict[BaseSchedulerNode, int]]: + """ + Calculate potential peak memory after swapping candidate with group (sink_waits version). + + Computes new memory levels for all affected nodes and returns the potential + peak memory along with cached post-allocation and size-free delta values. + + Args: + candidate: Node being moved + gns: Group nodes + group_head: First node of group + group_peak_memory: Current peak memory within the group + candidate_delta_mem: Net memory change from candidate (alloc - free) + candidate_allocfree: Candidate's allocation/free info + group_n_to_bufs_after_swap_dealloc_instead_of_candidate: Buffers whose deallocation moves from candidate to group + curr_memory: Current memory state dict + snodes_allocfree: Allocation/free info for all nodes + + Returns: + Tuple of (potential_peak_memory, post_alloc_update_dict, size_free_delta_update_dict) + """ + pre_group_mem = curr_memory[group_head][0] - snodes_allocfree[group_head].size_alloc + # Stash memory tracing updates to not recompute them after swap + _post_alloc_update: dict[BaseSchedulerNode, int] = {} + _size_free_delta_update: dict[BaseSchedulerNode, int] = {} + + potential_peak = 0 + if not group_n_to_bufs_after_swap_dealloc_instead_of_candidate: + # Not accounting for buffers liveliness change + potential_peak = max( + group_peak_memory + candidate_delta_mem, + pre_group_mem + candidate_allocfree.size_alloc, + ) + return potential_peak, _post_alloc_update, _size_free_delta_update + + candidate_post_alloc = pre_group_mem + candidate_allocfree.size_alloc + _post_alloc_update[candidate] = candidate_post_alloc + potential_peak = candidate_post_alloc + candidate_size_free_to_move = sum( + buf.mpi_buffer.size_free # type: ignore[attr-defined] + for buf in itertools.chain.from_iterable( + group_n_to_bufs_after_swap_dealloc_instead_of_candidate.values() + ) + ) + _size_free_delta_update[candidate] = -candidate_size_free_to_move + delta_mem = candidate_delta_mem + candidate_size_free_to_move + for gn in gns: + gn_post_alloc = curr_memory[gn][0] + delta_mem + _post_alloc_update[gn] = gn_post_alloc + potential_peak = max(potential_peak, gn_post_alloc) + gn_size_free_to_add = 0 + if gn in group_n_to_bufs_after_swap_dealloc_instead_of_candidate: + bufs = group_n_to_bufs_after_swap_dealloc_instead_of_candidate[gn] + for buf in bufs: + gn_size_free_to_add += buf.mpi_buffer.size_free + _size_free_delta_update[gn] = gn_size_free_to_add + delta_mem -= gn_size_free_to_add + return potential_peak, _post_alloc_update, _size_free_delta_update + + +def _perform_double_linked_list_swap_sink_waits( + candidate: BaseSchedulerNode, + group_head: BaseSchedulerNode, + group_tail: BaseSchedulerNode, + prev_dict: dict[BaseSchedulerNode, BaseSchedulerNode | None], + next_dict: dict[BaseSchedulerNode, BaseSchedulerNode | None], + head: BaseSchedulerNode, +) -> BaseSchedulerNode: + """ + Swap positions of candidate and group in doubly-linked list (sink_waits version). + + Transforms (moves candidate to the left): + group_head_prev -> group_head...group_tail -> candidate -> candidate_next + Into: + group_head_prev -> candidate -> group_head...group_tail -> candidate_next + + Args: + candidate: Node to swap with group + group_head: First node of group + group_tail: Last node of group + prev_dict: Dictionary mapping nodes to their previous nodes + next_dict: Dictionary mapping nodes to their next nodes + head: Current head of the linked list + + Returns: + New head of the linked list (may change if group_head was the head) + """ + # 0: Update group_head's previous node + group_head_prev = prev_dict[group_head] + if group_head_prev: + next_dict[group_head_prev] = candidate + prev_dict[candidate] = group_head_prev + + # 2: Update candidate's next node + candidate_next = next_dict[candidate] + if candidate_next: + prev_dict[candidate_next] = group_tail + next_dict[group_tail] = candidate_next + + # 1: Link candidate to group_head + prev_dict[group_head] = candidate + next_dict[candidate] = group_head + + # Update head if group_head was the head + if group_head == head: + return candidate + return head + + +def _format_and_log_sink_waits_stats( + stats: dict[BaseSchedulerNode, SinkWaitInfo], + head: BaseSchedulerNode, + next_dict: dict[BaseSchedulerNode, BaseSchedulerNode | None], + original_snodes_num: int, + peak_memory: int, + name_to_freeable_input_buf: dict, + graph_outputs: OrderedSet[str], +) -> list[BaseSchedulerNode]: + """ + Format sink_waits statistics, log them, and return final node list. + + Computes improvement metrics, creates a formatted table (using tabulate if + available), validates the reordered node count, recalculates peak memory, + and logs all information. + + Args: + stats: Per-node sink_waits statistics + head: Head of the reordered linked list + next_dict: Linked list next pointers + original_snodes_num: Original number of nodes (for validation) + peak_memory: Initial peak memory before reordering + name_to_freeable_input_buf: Buffer memory tracking info + graph_outputs: Graph output names + + Returns: + Final reordered list of scheduler nodes + """ + headers = [ + "Wait node", + "comm_time(us)", + "comp_time(us)", + "initial exposed(us)", + "final exposed(us)", + "improvement(us)", + "limiting factor", + "grouped", + "grouped_info", + "moves", + "moves_info", + "overlap_info", + ] + rows = [ + [ + node_summary(snode), + info.comm_time / 1e3, + info.comp_time / 1e3, + info.initial_exposed / 1e3, + info.final_exposed / 1e3, + info.improvement / 1e3, + info.limiting_factor, + info.grouped, + info.grouped_info, + info.moves, + info.moves_info, + info.overlap_info, + ] + for snode, info in stats.items() + ] + log_str = "" + if importlib.util.find_spec("tabulate"): + from tabulate import tabulate + + log_str += tabulate( + rows, + headers=headers, + ) + else: + log_str += "Please `pip install tabulate` to nicely render overlap stats.\n" + log_str += str(headers) + "\n" + log_str += "\n".join(map(str, rows)) + overlap_log.info(log_str) + new_snodes = _group_nodes_from_linked_list(head, None, next_dict) + assert len(new_snodes) == original_snodes_num + new_peak_memory, _, _, _ = estimate_peak_memory_allocfree( + new_snodes, name_to_freeable_input_buf, graph_outputs + ) + log_str += f"\n sink_waits_iterative peak_memory_before:{peak_memory}" + log_str += f"\n sink_waits_iterative peak_memory_after:{new_peak_memory}" + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "sink_waits_iterative_info", + "encoding": "string", + }, + payload_fn=lambda: log_str, + ) + return new_snodes + + +def _find_buffers_with_changed_last_use_sink_waits( + candidate: BaseSchedulerNode, + gns: list[BaseSchedulerNode], + buf_to_snode_last_use: dict, + candidate_buffer_map: dict[BaseSchedulerNode, OrderedSet], +) -> dict[BaseSchedulerNode, list[FreeableInputBuffer | Any]]: + """ + Find buffers whose last use will change after swapping in sink_waits pass. + + When we swap [group] candidate to candidate [group], some buffers that + were last used by candidate will now be last used by a group node instead. + This is the opposite direction from the reorder version. + + Args: + candidate: The node being moved (currently last use) + gns: Group nodes being swapped with candidate + buf_to_snode_last_use: Mapping of buffers to their current last-use nodes + candidate_buffer_map: Pre-computed map of node -> buffers using that node + + Returns: + Dict mapping group nodes to buffers that will change their last-use node + """ + group_n_to_bufs_after_swap_dealloc_instead_of_candidate: dict[ + BaseSchedulerNode, list[FreeableInputBuffer | Any] + ] = defaultdict(list) + + # Optimization: only check buffers where candidate is a successor + # Reduces from O(all_buffers) to O(buffers_per_candidate) + candidate_bufs = candidate_buffer_map.get(candidate, OrderedSet()) + + for buf in candidate_bufs: + snode_last_use = buf_to_snode_last_use[buf] + if snode_last_use != candidate: # noqa: E711 + continue + + # candidate is last use of buf + # Find last group node in successors (maintains order) + succ_nodes = buf.mpi_buffer.succ_nodes + last_succ_gn = None + for gn in gns: + if gn in succ_nodes: + last_succ_gn = gn + + if last_succ_gn is None: + continue + + # gn has successors of buf that after potential swap will become + # last use of buf and start deallocating buf instead of candidate + group_n_to_bufs_after_swap_dealloc_instead_of_candidate[last_succ_gn].append( + buf + ) + + return group_n_to_bufs_after_swap_dealloc_instead_of_candidate + + +def _sink_waits_iterative_internal( + snodes: list[BaseSchedulerNode], +) -> tuple[list[BaseSchedulerNode], dict[BaseSchedulerNode, SinkWaitInfo]]: + original_snodes_num = len(snodes) + if original_snodes_num == 0: + return snodes, {} + graph_inputs: OrderedSet[str] = OrderedSet(V.graph.graph_inputs.keys()) + graph_outputs: OrderedSet[str] = OrderedSet(V.graph.get_output_names()) + ( + peak_memory, + _curr_memory, + snodes_allocfree, + buf_to_snode_last_use, + name_to_freeable_input_buf, + candidate_buffer_map, + ) = _initialize_memory_tracking(snodes, graph_inputs, graph_outputs) + + _prev, _next, _head = _initialize_double_linked_list(snodes) + + stats: dict[BaseSchedulerNode, SinkWaitInfo] = {} + + runtimes: dict[BaseSchedulerNode, float] = { + snode: estimate_op_runtime(snode) * _op_runtime_estimate_mult(snode) + for snode in snodes + } + + # Pre-compute output and dependency sets for O(1) lookup instead of O(N) creation per iteration + node_output_sets: dict[BaseSchedulerNode, frozenset[str]] = { + snode: frozenset(o.get_name() for o in snode.get_outputs()) for snode in snodes + } + node_dep_sets: dict[BaseSchedulerNode, frozenset[str]] = { + snode: frozenset( + d.name for d in snode.unmet_dependencies if not _is_fake_dep(d) + ) + for snode in snodes + } + + curr: BaseSchedulerNode | None = snodes[-1] + + processed_waits = OrderedSet() # type: ignore[var-annotated] + debug_iterative_memory_recompute = ( + config_comms.reorder_iterative_debug_memory_recompute + ) + debug_num_sink_waits_to_reorder: int | None = ( + config_comms.sink_waits_iterative_debug_limit_to_sink + ) + + iterative_recompute_error = False + while curr is not None and _prev[curr] is not None: + _prev_curr = _prev[curr] + if iterative_recompute_error: + break + if ( + debug_num_sink_waits_to_reorder is not None + and len(processed_waits) >= debug_num_sink_waits_to_reorder + ): + break + + if not (contains_wait(curr) and curr not in processed_waits): + curr = _prev_curr + continue + + processed_waits.add(curr) + info = stats[curr] = SinkWaitInfo() + comm_time, comp_time, overlap_info = _wait_exposed_communication_time( + curr, _head, _prev, runtimes, node_output_sets, node_dep_sets + ) + info.initial_exposed = info.final_exposed = comm_time - comp_time + info.comm_time = comm_time + info.comp_time = comp_time + info.overlap_info = overlap_info + + candidate = _next[curr] + group_head = curr + group_tail = curr + group_colls = {} + group_runtime = 0.0 + group_peak_memory = _curr_memory[curr][0] + + # Track group outputs and check collective status incrementally - initialize from pre-computed set + group_output_names = OrderedSet(node_output_sets[curr]) + group_contains_collective = contains_collective(curr) + + while candidate is not None: + if config_comms.sink_iterative_use_runtime_estimations and ( + info.final_exposed + < -config_comms.sink_iterative_extra_comm_comp_overlap * info.comm_time + ): + info.limiting_factor = "unexposed by runtime estimations" + break + + # Early exit: if group has no outputs, candidate can't depend on it + if not group_output_names: + data_dep = False + else: + # Calculate candidate dependencies using pre-computed set + candidate_dep_names = node_dep_sets[candidate] + data_dep = bool(candidate_dep_names & group_output_names) + + # Conservative sink wait, limiting by space before next collective. + # The global strategy is that bucketing should create space. + # For 2D we can experiment with allowing to sink Wait beyond non current group collective. + + if not config_comms.sink_waits_iterative_swap_with_collectives: + if contains_async_collective(candidate): + info.limiting_factor = ( + f"candidate contains_async_collective {candidate.get_name()}" + ) + break + + # 1. If we have data_dep - we can not swap => trying to group + # 2. If swap candidate and current node both contain collectives => trying to group + both_contain_comms = group_contains_collective and contains_collective( + candidate + ) + if data_dep or both_contain_comms: + _is_groupable, groupable_reason = _is_node_groupable_for_sink_waits( + candidate + ) + if _is_groupable: + group_tail = candidate + + # Update incremental tracking using pre-computed set + group_output_names.update(node_output_sets[candidate]) + group_contains_collective = ( + group_contains_collective or contains_collective(candidate) + ) + + if ( + config_comms.sink_iterative_use_runtime_estimations + and contains_collective(candidate) + ): + comm_time, comp_time, _ = _coll_exposed_communication_time( + candidate, _next, runtimes, node_output_sets, node_dep_sets + ) + group_colls[candidate] = (comm_time, comp_time) + if not contains_async_collective(candidate): + group_runtime += runtimes[candidate] + + group_peak_memory = max( + group_peak_memory, _curr_memory[candidate][0] + ) + info.grouped += 1 + candidate = _next[candidate] + continue + elif not data_dep: + if ( + not config_comms.sink_waits_iterative_unsafe_collectives_reorder + and both_contain_comms + ): + info.limiting_factor = ( + f"collective ordering" + f"\n with candidate:{candidate.get_name()}" + ) + break + else: + info.limiting_factor = ( + f"data dependency detected" + f"\n candidate:{candidate.get_name()}" + f"\n non_group_reason:{groupable_reason}" + ) + break + + if config_comms.sink_iterative_use_runtime_estimations: + if is_wait(candidate.node): + # Corresponding collective is before the group, + # Swap can increase exposed time of corresponding collective + comm_time, comp_time, _ = _wait_exposed_communication_time( + candidate, + _head, + _prev, + runtimes, + node_output_sets, + node_dep_sets, + ) + # pyrefly: ignore[no-matching-overload] + exposed_before = max(0, comm_time - comp_time) + # pyrefly: ignore[no-matching-overload] + exposed_after = max(0, comm_time - comp_time + group_runtime) + # We do not know how much we can sink more after this swap, + # Just comparing advantage at the moment for now. + if exposed_after > exposed_before: + info.limiting_factor = ( + "candidate is wait," + f" exposed_before:{exposed_before} vs exposed_after:{exposed_after}" + ) + break + + # Check if candidate has sync runtime + if not contains_async_collective(candidate): + # If candidate has sync runtime, + # Waits of gorup_colls are on the right from group. + # Swap can increase their exposed time. + c_runtime = runtimes[candidate] + + if c_runtime > 0 and len(group_colls) > 0: + # Advantage for current Wait to do the Swap + # pyrefly: ignore[no-matching-overload] + exposed_delta = max( + 0, + info.comm_time - info.comp_time, + ) + # pyrefly: ignore[no-matching-overload] + -max(0, info.comm_time - info.comp_time - c_runtime) + for gc_comm_time, gc_comp_time in group_colls.values(): + # pyrefly: ignore [no-matching-overload] + exposed_delta += max(0, gc_comm_time - gc_comp_time) - max( + 0, gc_comm_time - gc_comp_time + c_runtime + ) + if exposed_delta > 0: + info.limiting_factor = ( + f"candidate has compute {c_runtime}, group contains collectives," + f" total_exposed_delta {exposed_delta}" + ) + break + else: + # Update all group_colls comm_time, comp_time + for gc, ( + gc_comm_time, + gc_comp_time, + ) in group_colls.items(): + group_colls[gc] = ( + gc_comm_time, + gc_comp_time - c_runtime, + ) + + # Create group nodes list once for swap operations + gns: list[BaseSchedulerNode] = _group_nodes_from_linked_list( + group_head, group_tail, _next + ) + + candidate_allocfree: SNodeMemory = snodes_allocfree[candidate] + candidate_delta_mem = ( + candidate_allocfree.size_alloc - candidate_allocfree.size_free + ) + # [group] candidate -> candidate [group] + # Check for buffers with successors in group and candidate last successor + # + # Buf that changes its last use snode, + # It was deallocated by candidate, + # but after swap it will be deallocated by group node. + group_n_to_bufs_after_swap_dealloc_instead_of_candidate = ( + _find_buffers_with_changed_last_use_sink_waits( + candidate, gns, buf_to_snode_last_use, candidate_buffer_map + ) + ) + + potential_peak, _post_alloc_update, _size_free_delta_update = ( + _calculate_potential_peak_memory_sink_waits( + candidate, + gns, + group_head, + group_peak_memory, + candidate_delta_mem, + candidate_allocfree, + group_n_to_bufs_after_swap_dealloc_instead_of_candidate, + _curr_memory, + snodes_allocfree, + ) + ) + if ( + potential_peak - peak_memory + > peak_memory * config_comms.sink_iterative_peak_memory_budget + ): + info.limiting_factor = ( + f"peak memory new:{potential_peak} vs base:{peak_memory}" + ) + break + + info.moves += 1 + info.moves_info += f"+{candidate.get_name()}" + + _head = _perform_double_linked_list_swap_sink_waits( + candidate, group_head, group_tail, _prev, _next, _head + ) + + comm_time, comp_time, overlap_info = _wait_exposed_communication_time( + curr, _head, _prev, runtimes, node_output_sets, node_dep_sets + ) + info.comm_time = comm_time + info.comp_time = comp_time + info.final_exposed = comm_time - comp_time + info.overlap_info = overlap_info + + _update_memory_tracking_after_swap_sink_waits( + candidate, + gns, + candidate_delta_mem, + candidate_allocfree, + group_n_to_bufs_after_swap_dealloc_instead_of_candidate, + _post_alloc_update, + _size_free_delta_update, + _curr_memory, + snodes_allocfree, + ) + + if debug_iterative_memory_recompute: + from .comms_debug import _debug_iterative_memory_recompute + + iterative_recompute_error = _debug_iterative_memory_recompute( + candidate, + gns, + _group_names(gns), + _group_nodes_from_linked_list(_head, None, _next), + name_to_freeable_input_buf, + graph_outputs, + peak_memory, + _curr_memory, + snodes_allocfree, + "sink_waits_iterative", + group_n_to_bufs_after_swap_dealloc_instead_of_candidate, + ) + if iterative_recompute_error: + break + + candidate = _next[group_tail] + curr = _prev_curr + + if not config_comms.reorder_sink_verbose_logging: + new_snodes = _group_nodes_from_linked_list(_head, None, _next) + return new_snodes, stats + + new_snodes = _format_and_log_sink_waits_stats( + stats, + _head, + _next, + original_snodes_num, + peak_memory, + name_to_freeable_input_buf, + graph_outputs, + ) + + return new_snodes, stats + + +def sink_waits_iterative(snodes: list[BaseSchedulerNode]) -> list[BaseSchedulerNode]: + """ + Similarly to reorder_communication_preserving_peak_memory this pass will try to iteratively + push Wait nodes later, recomputing estimated peak memory before each swap, + and preventing peak memory regressions. + + Pass will be applied to every Wait node. If there are immediate dependencies with next node, + pass will try to group them together and on the next step to swap the group with next candidate. + + If _inductor.config_comms.sink_iterative_use_runtime_estimations is set True, + pass will stop reordering of Wait once corresponding Collective is unexposed, + based on runtime estimations. + + inductor.config_comms.sink_iterative_peak_memory_budget allows to tune how much pass + can regress initial peak memory. + E.g.: + sink_iterative_peak_memory_budget == 0.0 - No regression of initial peak memory is allowed + sink_iterative_peak_memory_budget == 0.2 - Pass can improve comm-compute overlap, sacrificing + 20% of initial peak memory value. + + inductor.config_comms.sink_iterative_extra_comm_comp_overlap config allows to more aggressively + sink waits, stopping only when overlap_compute >= (1 + extra_comm_comp_overlap) * comm_time + """ + return _sink_waits_iterative_internal(snodes)[0] + + +def estimate_op_runtime(snode: BaseSchedulerNode) -> float: + """ + Returns estimated op runtime in milliseconds (ms) + """ + if config.estimate_op_runtime == "default": + runtime = snode.get_estimated_runtime() + else: + assert callable(config.estimate_op_runtime) + runtime = config.estimate_op_runtime(snode) + return runtime + + +def node_summary(snode): + snodes = snode.get_nodes() + if len(snodes) == 1: + detail = "" + if isinstance(snode.node, (ir.ExternKernelOut, ir._CollectiveKernel)): + outs_str = f"outs:{[o.get_name() for o in snode.get_outputs()]}" + ins_str = f"ins:{[d.name for d in snode.unmet_dependencies]}" + detail = f" {snode.get_name()} ({snode.node.python_kernel_name})\n {outs_str}({ins_str})" + layouts = [child.node.get_output_spec() for child in snode.get_nodes()] + out_tensor_info = ",".join( + [ + f" (size={layout.size}, stride={layout.stride})" + if isinstance(layout, ir.Layout) + else "" + for layout in layouts + ] + ) + try: + node_name = snode.node.maybe_get_name() + except AttributeError: + # TODO: node_summary was written without FusedSchedulerNode in mind, generally needs to be hardened + node_name = "" + return f"{snode.node.__class__.__name__}{detail}{out_tensor_info} ({node_name} ({snode.get_estimated_runtime():.0f} ns)" + + # Flatten the summaries for Fused/Foreach/Grouped nodes + summaries = [] + for child_snode in snodes: + summaries.append(node_summary(child_snode)) + return f"{snode.__class__.__name__}: {', '.join(summaries)}" + + +def visualize_overlap(order): + # TODO - this function probably doesn't do a very good job estimating the runtime because it doesn't carefully model + # streams and overlap. For now its mostly useful as a debug visualization. + + total_est_runtime: float = 0.0 + cur_comm_node = None + + def step_log(step, msg): + overlap_log.debug(f"{step:>6}: {msg}") # noqa: G004 + + for step, snode in enumerate(order): + if cur_comm_node is None: + if contains_collective(snode): + total_est_runtime += estimate_op_runtime(snode) + cur_comm_node = snode.node + elif is_wait(snode.node): + # raise AssertionError( + # "Wait is not expected when there is no collective running" + # ) + pass + else: # exposed compute op + total_est_runtime += estimate_op_runtime(snode) + step_log(step, f"{node_summary(snode)}") + else: # cur_comm_node is not None + if contains_collective(snode): + total_est_runtime += estimate_op_runtime(snode) + cur_comm_node = snode.node + step_log(step, f"{node_summary(snode)}") # noqa: G004 + elif is_wait(snode.node): # end of this comm op + step_log(step, f"{node_summary(snode)}") + cur_comm_node = None + else: # overlapped compute op + step_log(step, f"| {node_summary(snode)}") + overlap_log.debug( + f"Est. runtime (ms): {total_est_runtime / 1000 / 1000}" # noqa: G004 + ) + + +def reorder_compute_and_comm_for_overlap( + snodes: list[BaseSchedulerNode], +) -> list[BaseSchedulerNode]: + order = snodes + # pyrefly: ignore [bad-assignment] + for p in config.reorder_for_compute_comm_overlap_passes: + if isinstance(p, str) and p in globals(): + p = globals()[p] # it is a builtin pass + assert callable(p), ( + f"Invalid reorder_compute_and_comm_for_overlap pass: {p} is not callable" + ) + order = p(order) # type: ignore[operator] + # pyrefly: ignore [bad-return] + return order + + +def remove_fsdp2_unsharded_param_graph_input_usage(graph: torch.fx.Graph): + """ + This FX graph pass replaces uses of FSDP2 unsharded params with their corresponding + graph intermediates that were fsdp.copy_ into the unsharded params in the original graph. + + NOTE: Can only apply this pass to any of the FSDP2 unsharded params that have this pattern + (or repetition of): `resize_(full) -> copy_ -> resize_(0)`. Because of this, for partial-graph case + where `resize_(full) -> copy_` is in one graph and `resize_(0)` is in another graph, we can't + remove these resize and copy ops and thus we will have worse performance there. + + In other words, "do we try to remove all the resize_(full) -> copy_ -> resize_(0) nodes for this unsharded param" + is actually a per-unsharded-param decision, since for each unsharded param, we look at its resize sequence pattern + (in `check_resize_pattern()`) to determine if its set of resize and copy nodes can be removed. + """ + node_list = list(graph.nodes) + + # Find all graph inputs and their resize counts + graph_input_to_resized_to_full_node_idxes = defaultdict(list) + graph_input_to_resized_to_0_node_idxes = defaultdict(list) + for idx, node in enumerate(node_list): + if ( + node.op == "call_function" + and node.target is torch.ops.inductor.resize_storage_bytes_.default + ): + assert node.args[0].op == "placeholder", f"""\ +Resize can only operate on graph inputs, but got {node} which is resizing non-graph-input {node.args[0]} +""" + graph_input = node.args[0] + new_size = node.args[1] + if new_size > 0: + graph_input_to_resized_to_full_node_idxes[graph_input].append(idx) + else: + graph_input_to_resized_to_0_node_idxes[graph_input].append(idx) + + def check_resize_pattern(graph_input): + # Check the number of resize-to-full and resize-to-0 nodes are equal, + # and that for each (resize-to-full, resize-to-0) pair, the resize-to-full node + # always happens before the resize-to-0 node. + # This is the precondition for being able to remove all the resize and copy nodes + # for this specific unsharded param. + resized_to_full_idxes = graph_input_to_resized_to_full_node_idxes.get( + graph_input, [] + ) + resized_to_0_idxes = graph_input_to_resized_to_0_node_idxes.get(graph_input, []) + + if len(resized_to_full_idxes) != len(resized_to_0_idxes): + log.warning( + f""" +Unequal number of resize-to-full and resize-to-0 nodes for graph input {graph_input}: +{len(resized_to_full_idxes)} vs. {len(resized_to_0_idxes)}. +Skipping `remove_fsdp2_unsharded_param_graph_input_usage` FX graph pass. +""" # noqa: G004 + ) + return False + + # Check the sequence: (resize_to_full -> resize_to_0)+ + for resize_to_full_idx, resize_to_0_idx in zip( + resized_to_full_idxes, resized_to_0_idxes + ): + if resize_to_full_idx >= resize_to_0_idx: + log.warning( + f""" +For graph input {graph_input}: resize-to-full node {node_list[resize_to_full_idx]} at index {resize_to_full_idx} +happens after resize-to-0 node {node_list[resize_to_0_idx]} at index {resize_to_0_idx}. +Skipping `remove_fsdp2_unsharded_param_graph_input_usage` FX graph pass for that unsharded param. +""" # noqa: G004 + ) + return False + return True + + # Find all eligible unsharded params and their corresponding graph intermediates. + unsharded_param_to_fsdp_copy_node_idxes = defaultdict(list) + for idx, node in enumerate(node_list): + if node.op == "call_function" and node.target is torch.ops.fsdp.copy_.default: + fsdp_copy_node = node + unsharded_param = node.args[0] + assert unsharded_param.op == "placeholder", f""" +Assumed all FSDP2 `unsharded_param`s to be graph input, but it's not true! +Offending node: {unsharded_param}. Graph: {graph} +""" + if check_resize_pattern(unsharded_param): + unsharded_param_to_fsdp_copy_node_idxes[unsharded_param].append(idx) + + def is_allowed_mutation(node): + return ( + node.target is torch.ops.fsdp.copy_.default + or node.target is torch.ops.inductor.resize_storage_bytes_.default + ) + + def is_node_mutating_unsharded_param_or_its_alias(node, unsharded_params): + # Check whether the node is mutating any of the unsharded params or their aliases. + mutated_arg_idxes = ( + [ + i + for i, x in enumerate(node.target._schema.arguments) + if x.alias_info is not None and x.alias_info.is_write + ] + if isinstance(node.target, torch._ops.OpOverload) + else [] + ) + mutated_node_arg_storages = OrderedSet( + [ + StorageWeakRef(node.args[i].meta["val"].untyped_storage()) + for i in mutated_arg_idxes + ] + ) + storages_of_unsharded_params = OrderedSet( + [ + StorageWeakRef(unsharded_param.meta["val"].untyped_storage()) + for unsharded_param in unsharded_params + ] + ) + return len(mutated_node_arg_storages & storages_of_unsharded_params) > 0 + + # Check no user mutation on any unsharded_param + for node in node_list: + if ( + node.op == "call_function" + and isinstance(node.target, torch._ops.OpOverload) + and node.target._schema.is_mutable + and not is_allowed_mutation(node) + ): + assert not is_node_mutating_unsharded_param_or_its_alias( + node, unsharded_param_to_fsdp_copy_node_idxes.keys() + ), f"""\ +User mutation on FSDP2 unsharded param is not allowed when Traceable FSDP2 is used. Violating node: {node} +""" + + # For each `fsdp.copy_(unsharded_param, Y)`, replace downstream usage of `unsharded_param` with `Y`. + # + # NOTE: Because of "layer reuse" use case, there could be multiple `fsdp.copy_` to the same `unsharded_param` graph input. + # e.g. + # ``` + # fsdp_copy_1 = fsdp.copy_(unsharded_param_1, Y1) + # ... (use of unsharded_param_1) -> Subgraph 1 + # fsdp_copy_2 = fsdp.copy_(unsharded_param_1, Y2) + # ... (use of unsharded_param_1) -> Subgraph 2 + # fsdp_copy_3 = fsdp.copy_(unsharded_param_1, Y3) + # ... (use of unsharded_param_1) -> Subgraph 3 + # ``` + # We must do the replacement only within each subgraph. + for ( + unsharded_param, + fsdp_copy_node_idxes, + ) in unsharded_param_to_fsdp_copy_node_idxes.items(): + for i, fsdp_copy_node_idx in enumerate(fsdp_copy_node_idxes): + fsdp_copy_node = node_list[fsdp_copy_node_idx] + assert fsdp_copy_node.args[0] is unsharded_param + _, replacement = fsdp_copy_node.args + # subgraph_start_idx is exclusive + subgraph_start_idx = fsdp_copy_node_idx + 1 + # subgraph_end_idx is exclusive (also intentionally don't replace args in return op) + subgraph_end_idx = ( + fsdp_copy_node_idxes[i + 1] + if i < len(fsdp_copy_node_idxes) - 1 + else len(node_list) - 1 + ) + subgraph_nodes = node_list[subgraph_start_idx:subgraph_end_idx] + assert not any( + is_node_mutating_unsharded_param_or_its_alias(node, [unsharded_param]) + for node in subgraph_nodes + ), f"""\ +Assumed no ops mutating unsharded param {unsharded_param} in subgraph {subgraph_nodes}, but it's not true! +Graph: {graph} +""" + for node in subgraph_nodes: + if ( + node.op == "call_function" + and unsharded_param in node.args + and node.target != torch.ops.inductor.resize_storage_bytes_.default + ): # TODO(yf225): implement replacement in kwargs + new_args = tuple( + replacement if arg is unsharded_param else arg + for arg in node.args + ) + node.args = new_args + + # Delete `fsdp.copy_(unsharded_param, Y)` nodes + for fsdp_copy_node_idxes in unsharded_param_to_fsdp_copy_node_idxes.values(): + for fsdp_copy_node_idx in fsdp_copy_node_idxes: + fsdp_copy_node = node_list[fsdp_copy_node_idx] + graph.erase_node(fsdp_copy_node) + + # Delete `resize_(unsharded_param, ...)` nodes + for node in node_list: + if ( + node.op == "call_function" + and node.target is torch.ops.inductor.resize_storage_bytes_.default + and node.args[0] in unsharded_param_to_fsdp_copy_node_idxes + ): + graph.erase_node(node) + + +def reinplace_fsdp_all_gather(graph: torch.fx.Graph) -> None: + try: + import torch.distributed.fsdp._fully_shard._fsdp_collectives + + assert torch.distributed.is_available() + # Assert existence of these ops + assert ( + torch.ops._c10d_functional.all_gather_into_tensor + and torch.ops._c10d_functional.all_gather_into_tensor_out + ) + except (ImportError, AttributeError, AssertionError): + return + + from .pattern_matcher import ( + CallFunction, + KeywordArg, + Match, + PatternMatcherPass, + register_graph_pattern, + ) + + """ + all_gather_copy_in = torch.ops.fsdp.all_gather_copy_in.default(...); + getitem = all_gather_copy_in[0]; + (getitem_1 = all_gather_copy_in[1];) # optional + + all_gather_into_tensor = torch.ops._c10d_functional.all_gather_into_tensor.default(getitem, ...); + + -> + + all_gather_copy_in = torch.ops.fsdp.all_gather_copy_in.default(...); + getitem = all_gather_copy_in[0]; + getitem_1 = all_gather_copy_in[1]; + + all_gather_into_tensor = torch.ops._c10d_functional.all_gather_into_tensor_out.default(getitem, ..., out=getitem_1); + """ + + def remove_unused_getitem(g): + # Remove `getitem_X = all_gather_copy_in[1]` which is never used. + node_list = list(g.nodes) + for n in node_list: + if ( + n.target is operator.getitem + and n.args[0].target is torch.ops.fsdp.all_gather_copy_in.default + and n.args[1] == 1 + ): + g.erase_node(n) + + graph_pass = PatternMatcherPass() + + @register_graph_pattern( + CallFunction( + torch.ops._c10d_functional.all_gather_into_tensor.default, + CallFunction( + operator.getitem, + CallFunction( + torch.ops.fsdp.all_gather_copy_in.default, + KeywordArg("all_gather_inputs"), + KeywordArg("all_gather_output"), + KeywordArg("inp_split_sizes"), + KeywordArg("all_gather_input_numel"), + KeywordArg("rank"), + ), + KeywordArg("item_idx"), + ), + KeywordArg("group_size"), + KeywordArg("group_name"), + ), + # pyrefly: ignore [bad-argument-type] + pass_dict=graph_pass, + extra_check=lambda match: match.kwargs["item_idx"] == 0, + ) + def reinplace_all_gather(match: Match, *args, **kwargs): + def repl( + *args, + ): + copy_in_args = args[:-2] + group_size = args[-2] + group_name = args[-1] + all_gather_copy_in = torch.ops.fsdp.all_gather_copy_in.default( + *copy_in_args + ) + getitem = all_gather_copy_in[0] + getitem_1 = all_gather_copy_in[1] + all_gather_into_tensor = ( + torch.ops._c10d_functional.all_gather_into_tensor_out.default( + getitem, group_size, group_name, out=getitem_1 + ) + ) + return all_gather_into_tensor + + match.replace_by_example( + # pyrefly: ignore [bad-argument-type] + repl, + [ + kwargs["all_gather_inputs"], + kwargs["all_gather_output"], + kwargs["inp_split_sizes"], + kwargs["all_gather_input_numel"], + kwargs["rank"], + kwargs["group_size"], + kwargs["group_name"], + ], + ) + + remove_unused_getitem(graph) + graph_pass.apply(graph) # type: ignore[arg-type] + + +def get_op_idx(snode): + assert not isinstance( + snode, + ( + torch._inductor.scheduler.FusedSchedulerNode, + torch._inductor.scheduler.GroupedSchedulerNode, + ), + ) + return int(snode.get_name()[2:]) + + +def enforce_comm_ordering_for_fsdp( + snodes: list[torch._inductor.scheduler.BaseSchedulerNode], + name_to_buf: dict[str, torch._inductor.scheduler.SchedulerBuffer], + name_to_fused_node: dict[str, BaseSchedulerNode], +) -> list[torch._inductor.scheduler.BaseSchedulerNode]: + from . import scheduler + + new_order: list[BaseSchedulerNode] = [] + scheduled = OrderedSet[Any]() + ag_exists = False + rs_exists = False + ag_grouped_node_to_wait_grouped_node = {} + rs_grouped_node_to_wait_grouped_node = {} + snode_name_to_final_snode = {} + + def _create_group_node(snodes_to_group): + group_node = scheduler.GroupedSchedulerNode.create(snodes_to_group) + for snode in snodes_to_group: + snode_name_to_final_snode[snode.get_name()] = group_node + snode_name_to_final_snode[group_node.get_name()] = group_node + return group_node + + # Create grouped nodes for specific sets of ops + for snode in snodes: + # Case 1: Handle AllGather + if is_collective( + snode.node, op=torch.ops._c10d_functional.all_gather_into_tensor_out.default + ) and any( + is_fallback_op( + name_to_fused_node[x].node, torch.ops.fsdp.all_gather_copy_in.default + ) + for x in snode.ancestors + ): + ag_exists = True + ag_snode = snode + ag_related_snode_set: OrderedSet[scheduler.BaseSchedulerNode] = OrderedSet() + + # Find the "cast + copy_in + getitem + all_gather" code block + find_recursive_deps_of_node( + ag_snode, + ag_related_snode_set, + name_to_buf, + name_to_fused_node, + ) + + # Find the "all_gather + all_gather_wait_tensor + copy_out" code block + allowed_ops = OrderedSet( + [ + torch.ops._c10d_functional.all_gather_into_tensor_out.default, + torch.ops._c10d_functional.wait_tensor.default, + torch.ops.fsdp.split_with_sizes_copy.default, + ] + ) + find_recursive_users_of_node( + ag_snode, + ag_related_snode_set, + name_to_buf, + name_to_fused_node, + criteria_cb=lambda x: not ( + isinstance(x, scheduler.NopKernelSchedulerNode) + or ( + isinstance(x, scheduler.ExternKernelSchedulerNode) + and x.node.op_overload in allowed_ops # type: ignore[union-attr] + ) + ), + ) + + # sort nodes by original operation order + ag_related_snodes = sorted( + ag_related_snode_set, key=lambda x: get_op_idx(x) + ) + + # In the "reuse layer" case, some ops in the 2nd all-gather code block could also + # depend on ops in the 1st all-gather code block, and we don't want to group them together. + end_idx_of_current_ag_block = len(ag_related_snodes) + copy_out_count = 0 + for i in range(len(ag_related_snodes)): + cur_snode = ag_related_snodes[i] + if is_fallback_op( + cur_snode.node, torch.ops.fsdp.split_with_sizes_copy.default + ): + copy_out_count += 1 + if copy_out_count > 1: + end_idx_of_current_ag_block = i + break + + ag_related_snodes = ag_related_snodes[:end_idx_of_current_ag_block] + + # Group "cast + copy_in + getitem + all_gather" into one GroupedSchedulerNode + wait_node_idx = None + for i in range(len(ag_related_snodes) - 1): + if isinstance(ag_related_snodes[i + 1].node, ir._WaitKernel): + wait_node_idx = i + 1 + break + assert wait_node_idx is not None + ag_group_node = _create_group_node(ag_related_snodes[:wait_node_idx]) + + # Group "all_gather_wait_tensor + copy_out" into one GroupedSchedulerNode + ag_wait_group_node = _create_group_node(ag_related_snodes[wait_node_idx:]) + + ag_grouped_node_to_wait_grouped_node[ag_group_node] = ag_wait_group_node + + # Case 2: Handle ReduceScatter + elif is_fallback_op(snode.node, torch.ops.fsdp.chunk_cat.default): + rs_exists = True + rs_snode = snode + + # Find the "reduce_scatter copy-in + reduce_scatter comm + reduce_scatter wait" code block + rs_related_snode_set: OrderedSet[scheduler.BaseSchedulerNode] = OrderedSet() + find_recursive_users_of_node( + rs_snode, + rs_related_snode_set, + name_to_buf, + name_to_fused_node, + ) + + # sort nodes by original operation order + rs_related_snodes = sorted( + rs_related_snode_set, key=lambda x: get_op_idx(x) + ) + + # Group "reduce_scatter copy-in + reduce_scatter comm" into one GroupedSchedulerNode + wait_node_idx = None + for i in range(len(rs_related_snodes) - 1): + if isinstance(rs_related_snodes[i + 1].node, ir._WaitKernel): + wait_node_idx = i + 1 + break + assert wait_node_idx is not None + rs_group_node = _create_group_node(rs_related_snodes[:wait_node_idx]) + + # Group "reduce_scatter wait + related output nodes" into one GroupedSchedulerNode + rs_wait_group_node = _create_group_node(rs_related_snodes[wait_node_idx:]) + + rs_grouped_node_to_wait_grouped_node[rs_group_node] = rs_wait_group_node + + assert len(snode_name_to_final_snode) > 0 + if ag_exists: + assert len(ag_grouped_node_to_wait_grouped_node) > 0 + if rs_exists: + assert len(rs_grouped_node_to_wait_grouped_node) > 0 + + # Build the new node schedule, taking GroupedSchedulerNode into account + for snode in snodes: + if snode.get_name() in snode_name_to_final_snode: + snode = snode_name_to_final_snode[snode.get_name()] + if snode in scheduled: + continue + new_order.append(snode) + scheduled.add(snode) + + # Enforce AllGather ordering: previous AllGather's "wait then copy_out" group node must run + # before next AllGather's "copy_in then AG" group node + prev_ag_wait = None + for ag_group_node, wait_group_node in ag_grouped_node_to_wait_grouped_node.items(): + if prev_ag_wait is not None: + mutating_buf = next(iter(ag_group_node.get_buffer_names())) + for o in prev_ag_wait.get_outputs(): + ag_group_node.add_fake_dep( + WeakDep(o.get_name(), mutating_buf=mutating_buf, is_fake=True) + ) + prev_ag_wait = wait_group_node + + # Enforce ReduceScatter ordering: previous ReduceScatter's "wait" group node must run + # before next ReduceScatter's "copy_in then RS" group node + prev_rs_wait = None + for rs_group_node, wait_group_node in rs_grouped_node_to_wait_grouped_node.items(): + if prev_rs_wait is not None: + mutating_buf = next(iter(rs_group_node.get_buffer_names())) + for o in prev_rs_wait.get_outputs(): + rs_group_node.add_fake_dep( + WeakDep(o.get_name(), mutating_buf=mutating_buf, is_fake=True) + ) + prev_rs_wait = wait_group_node + + return new_order # type: ignore[return-value] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/comms_debug.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/comms_debug.py new file mode 100644 index 0000000000000000000000000000000000000000..f40df1a2436fd68d70727180c00526f0a26c42b9 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/comms_debug.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from torch._logging import trace_structured + +from .memory import estimate_peak_memory_allocfree + + +if TYPE_CHECKING: + from torch.utils._ordered_set import OrderedSet + + from .memory import FreeableInputBuffer, SNodeMemory + from .scheduler import BaseSchedulerNode, SchedulerBuffer + + +def _debug_iterative_memory_recompute( + candidate: BaseSchedulerNode, + gns: list[BaseSchedulerNode], + group_names: str, + snodes: list[BaseSchedulerNode], + name_to_freeable_input_buf: dict[str, FreeableInputBuffer], + graph_outputs: OrderedSet[str], + peak_memory: int, + iter_curr_memory: dict[BaseSchedulerNode, tuple[int, int]], + snodes_allocfree: dict[BaseSchedulerNode, SNodeMemory], + tlparse_name: str, + gn_to_bufs_last_use: dict[ + BaseSchedulerNode, list[FreeableInputBuffer | SchedulerBuffer] + ], +) -> bool: + iterative_recompute_error = False + candidate_allocfree = snodes_allocfree[candidate] + est_peak_memory, snodes_curr_memory, snodes_allocfree, _ = ( + estimate_peak_memory_allocfree( + snodes, name_to_freeable_input_buf, graph_outputs + ) + ) + est_curr_memory = dict(zip(snodes, snodes_curr_memory)) + iter_cm = iter_curr_memory[candidate] + new_cm = est_curr_memory[candidate] + log = "" + if est_peak_memory > peak_memory: + log = "ITERATIVE PEAK DOES NOT MATCH" + iterative_recompute_error = True + if iter_cm != new_cm: + log = "ITERATIVE CURR MEMORY CANDIDATE DOES NOT MATCH" + iterative_recompute_error = True + for gn in gns: + iter_gnm = iter_curr_memory[gn] + new_gnm = est_curr_memory[gn] + if iter_gnm != new_gnm: + log = f"ITERATIVE GN CURR MEMORY DOES NOT MATCH:{gn.get_name()}" + iterative_recompute_error = True + if iterative_recompute_error: + log += ( + f"\nCANDIDATE:{candidate.get_name()}" + f"\nGROUP:{group_names}" + f"\nPEAK_MEMORY_BEFORE:{peak_memory}" + f"\nPEAK_MEMORY_AFTER_SWAP:{est_peak_memory}" + f"\nCANDIDATE:{candidate.debug_str()}" + f"\nCANDIDATE_ITER_CURR_MEMORY:{iter_cm}" + f"\nCANDIDATE_NEW__CURR_MEMORY:{new_cm}" + f"\nCANDIDATE_ITER_ALLOCFREE:{candidate_allocfree}" + f"\nCANDIDATE_NEW_ALLOCFREE:{snodes_allocfree[candidate]}" + ) + peak_log = "" + for i, (pre, _post) in enumerate(snodes_curr_memory): + if est_peak_memory == pre: + n = snodes[i] + peak_log = ( + f"\nNEW_PEAK:{est_peak_memory}(BASE:{peak_memory})" + f" @ SNODE[{i}/{len(snodes)}]:{n.get_name()} {n.debug_str()}" + ) + break + group_log = "" + for i, gn in enumerate(gns): + iter_gnm = iter_curr_memory[gn] + new_gnm = est_curr_memory[gn] + group_log += ( + f"\nGROUP_NODE[{i}]:{gn.debug_str()}" + f"\nGROUP_NODE[{i}] ITER_GNM[{gn.get_name()}]:{iter_gnm}" + f"\nGROUP_NODE[{i}] ESTM_GNM[{gn.get_name()}]:{new_gnm}" + f"\nGROUP_NODE[{i}] ITER_allocfree:{snodes_allocfree[gn]}" + f"\nGROUP_NODE[{i}] ESTM_allocfree:{snodes_allocfree[gn]}" + ) + log += peak_log + log += group_log + log += f"\nGN_TO_BUFS_LAST_USE:{gn_to_bufs_last_use}" + log += "\n\n".join( + [ + ( + f"\nSNODE[{i}]\n{n.debug_str()}" + f"\nITER_cur_mem:{iter_curr_memory[n]}" + f"\nESTM_cur_mem:{est_curr_memory[n]}" + f"\nITER_allocfree:{snodes_allocfree[n]}" + f"\nESTM_allocfree:{snodes_allocfree[n]}" + ) + for i, n in enumerate(snodes) + ] + ) + tname = f"{tlparse_name}_ITERATIVE_RECOMPUTE_ERROR" + print(f"{tname}:\n{log}") + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": tname, + "encoding": "string", + }, + payload_fn=lambda: log, + ) + return iterative_recompute_error diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_fx.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_fx.py new file mode 100644 index 0000000000000000000000000000000000000000..bd7e96e0f727ae3f2d4b3b9b52fc1d985a0990d1 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_fx.py @@ -0,0 +1,3281 @@ +from __future__ import annotations + +import contextlib +import copy +import enum +import functools +import io +import itertools +import json +import logging +import os +import sys +import time +import warnings +from abc import ABC, abstractmethod +from collections import defaultdict +from contextlib import AbstractContextManager +from dataclasses import dataclass +from inspect import currentframe +from itertools import count +from operator import attrgetter +from typing import Any, TYPE_CHECKING, TypeVar +from typing_extensions import Never, override, ParamSpec, Protocol, TypedDict, Unpack +from unittest import mock + +import torch._inductor.async_compile +import torch.fx +import torch.utils._pytree as pytree +from functorch.compile import min_cut_rematerialization_partition +from torch import fx +from torch._dispatch.python import enable_python_dispatcher +from torch._dynamo import ( + compiled_autograd, + config as dynamo_config, + logging as dynamo_logging, + utils as dynamo_utils, +) +from torch._dynamo.backends import common as dynamo_common +from torch._dynamo.device_interface import get_interface_for_device +from torch._dynamo.repro.after_aot import wrap_compiler_debug +from torch._dynamo.utils import ( + chromium_event_timed, + CompileEventLogger, + counters, + detect_fake_mode, + dynamo_timed, + flatten_graph_inputs, + get_inputs_devices, + get_metrics_context, + GmWrapper, + lazy_format_graph_code, + set_feature_use, +) +from torch._functorch import aot_autograd, config as functorch_config +from torch._functorch._aot_autograd.subclass_parametrization import ( + unwrap_tensor_subclass_parameters, +) +from torch._functorch.aot_autograd import ( + aot_export_module, + GraphOutputName, + make_boxed_func, + SerializableAOTDispatchCompiler, +) +from torch._inductor.codecache import code_hash, FxGraphCache, output_code_log +from torch._inductor.cudagraph_utils import ( + BoxedDeviceIndex, + cudagraphs_log, + format_default_skip_message, + log_cudagraph_skip_and_bump_counter, + PlaceholderInfo, +) +from torch._inductor.custom_graph_pass import CustomPartitionerFn +from torch._inductor.debug import ( + create_mapping_pre_post_grad_nodes, + save_args_for_compile_fx_inner, +) +from torch._inductor.output_code import ( + CompiledAOTI, + CompiledFxGraph, + CompiledFxGraphConstantsWithGm, + get_expanded_dims, + index_expanded_dims, + OutputCode, +) +from torch._inductor.runtime.cache_dir_utils import cache_dir +from torch._inductor.utils import ( + BoxedBool, + count_tangents, + fresh_cache, + get_all_devices, + get_static_bw_input_idxs, + InputType, + is_gpu, + should_assume_input_aligned, + should_use_remote_fx_graph_cache, + tensor_is_aligned, +) +from torch._library.fake_class_registry import FakeScriptObject +from torch._library.opaque_object import is_opaque_type +from torch._logging import trace_structured +from torch._utils_internal import compile_time_strobelight_meta +from torch.fx import GraphModule +from torch.fx.experimental.symbolic_shapes import free_unbacked_symbols, SymExprPrinter +from torch.fx.passes.fake_tensor_prop import FakeTensorProp +from torch.monitor import _WaitCounter +from torch.utils._ordered_set import OrderedSet + +from .._dynamo.exc import ShortenTraceback, SkipFrame +from ..fx._lazy_graph_module import _use_lazy_graph_module +from ..fx.graph import _PyTreeCodeGen +from ..utils._triton import has_triton +from . import config, distributed_autotune, metrics +from .codegen.common import get_wrapper_codegen_for_device, init_backend_registration +from .debug import DebugContext +from .decomposition import select_decomp_table +from .exc import InductorError +from .fx_passes.joint_graph import joint_graph_passes +from .fx_passes.post_grad import post_grad_passes, view_to_reshape +from .fx_passes.pre_grad import pre_grad_passes +from .graph import GraphLowering +from .ir import get_device_type, IRNode +from .output_code import complex_memory_overlap # noqa: F401 +from .triton_bundler import TritonBundler +from .utils import ( + align_inputs_from_check_idxs, + clone_preserve_strides, + copy_misaligned_inputs, + get_cloned_parameter_buffer_name, + get_first_incompatible_cudagraph_node, + maybe_get_suppress_shape_guards_ctx, + output_node, + remove_unaligned_input_idxs, + shape_env_from_inputs, +) +from .virtualized import V + + +if TYPE_CHECKING: + from collections.abc import Callable, Generator, Sequence + + from torch._inductor.output_code import _StrideExprStr + from torch._ops import OpOverload + from torch.export.pt2_archive._package_weights import Weights + + from .ir import ExternKernelNode + + +_P = ParamSpec("_P") +_T = TypeVar("_T") + +if TYPE_CHECKING or not config.is_fbcode(): + # no-op decorator + def time_and_log(attr: str) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]: + return dynamo_utils.identity + + def log_optimus_to_scuba(*args: object, **kwargs: object) -> None: + pass + +else: + from torch._inductor.fb.utils import log_optimus_to_scuba, time_and_log + +if TYPE_CHECKING: + from torch._functorch._aot_autograd.schemas import ( + FQN, + GraphInputName, + GraphSignature, + ) + + CompileFxOutput = ( + Callable[[list[object]], Sequence[torch.Tensor]] | str | list[str] | Weights + ) + + +class FxCompileMode(enum.Enum): + NORMAL = 0 + # For testing - use the serde FxCompile scheme to debug serialization and + # deserialization of GraphMoule and CompiledFxGraph. + SERIALIZE = 1 + # Compile using a subprocess instead of in-process. + SUBPROCESS = 2 + + +@dataclass +class FxCompileConfig: + mode: FxCompileMode + use_async: bool + use_progressive: bool + + +def _fx_compile_mode_default() -> FxCompileConfig: + name = "TORCHINDUCTOR_FX_COMPILE_MODE" + value = os.environ.get(name) + if value is None: + return FxCompileConfig(FxCompileMode.NORMAL, False, False) + + use_async = False + use_progressive = False + + if value.lower().startswith("progressive+"): + use_progressive = True + value = value[12:] + if value.lower().startswith("async+"): + use_async = True + value = value[6:] + + try: + value = value.upper() + return FxCompileConfig(FxCompileMode[value], use_async, use_progressive) + except KeyError: + import logging + + log = logging.getLogger(__name__) + log.error( + "Invalid value of %s for %s. Expected one of %s. Using default.", + value, + name, + ", ".join(sorted(repr(x) for x in FxCompileMode.__members__)), + ) + # Remove from the environment so subprocesses don't ALSO complain. + os.environ.pop(name) + return FxCompileConfig(FxCompileMode.NORMAL, False, False) + + +def _get_progression_configs() -> list[dict[str, Any]]: + # TODO make this configurable + return [ + {"max_autotune": True}, + ] + + +_fx_compile_config = _fx_compile_mode_default() +fx_compile_mode = _fx_compile_config.mode +fx_compile_async = _fx_compile_config.use_async +fx_compile_progressive = _fx_compile_config.use_progressive + +log = logging.getLogger(__name__) +perf_hint_log = torch._logging.getArtifactLogger(__name__, "perf_hints") +pre_grad_graphs_log = torch._logging.getArtifactLogger(__name__, "pre_grad_graphs") +post_grad_graphs_log = torch._logging.getArtifactLogger(__name__, "post_grad_graphs") +static_inputs_log = torch._logging.getArtifactLogger( + __name__, "cudagraph_static_inputs" +) +inductor_metrics_log = torch._logging.getArtifactLogger(__name__, "inductor_metrics") + + +def get_static_input_idxs(num_fixed: int) -> list[int]: + # If we are inlining NNModules, we treat all torch.nn.Parameters as static for the purposes + # of cudagraphs. Rather than copying these into cudagraph-owned memory + # like we do for normal inputs on each run, we will re-record a cudagraph if these + # parameter locations change. + context = torch._guards.TracingContext.try_get() + fixed = list(range(num_fixed)) + if not context or not context.fw_metadata: + return fixed + + return context.fw_metadata.static_input_indices + + +def record_original_output_strides(gm: GraphModule) -> None: + output_node = gm.graph.find_nodes(op="output")[0] + output_strides = [] + + if not isinstance(output_node.args[0], torch.fx.Node): + output_node_args = output_node.args[0] + else: + output_node_args = output_node.args + + for output in output_node_args: + if ( + isinstance(output, torch.fx.Node) + and (val := output.meta.get("val")) is not None + and isinstance(val, torch.Tensor) + ): + output_strides.append(val.stride()) + else: + # pyrefly: ignore [bad-argument-type] + output_strides.append(None) + output_node.meta["original_output_strides"] = output_strides + + +def _recursive_record_original_output_strides(gm: GraphModule) -> None: + # invoke_subgraph HOP requires output strides to be respected + for node in gm.graph.find_nodes( + op="call_function", target=torch.ops.higher_order.invoke_subgraph + ): + subgraph = getattr(gm, node.args[0].target) + _recursive_record_original_output_strides(subgraph) + + record_original_output_strides(gm) + + +def _recursive_record_user_visible_output_idxs(gm: GraphModule) -> None: + # invoke_subgraph HOP requires output strides to be respected + for node in gm.graph.find_nodes( + op="call_function", target=torch.ops.higher_order.invoke_subgraph + ): + subgraph = getattr(gm, node.args[0].target) + + for node in subgraph.graph.find_nodes(op="output"): + node.meta["user_visible_output_idxs"] = [ + idx + for idx in range(len(node.args[0])) + if isinstance(node.args[0][idx], torch.fx.Node) + ] + _recursive_record_user_visible_output_idxs(subgraph) + + +@functools.lru_cache(None) +def _step_logger() -> Callable[..., None]: + return dynamo_logging.get_step_logger(log) + + +@functools.cache +def _warn_tf32_disabled() -> None: + if ( + torch.cuda.is_available() + and torch.backends.cuda.matmul.fp32_precision != "tf32" + and torch.cuda.get_device_capability() >= (8, 0) + ): + warnings.warn( + "TensorFloat32 tensor cores for float32 matrix multiplication available but not enabled. " + "Consider setting `torch.set_float32_matmul_precision('high')` for better performance." + ) + + +def _resolve_name_collision(mod: GraphModule, gm: GraphModule) -> None: + """ + In aot_export_module (make_fx), we create get_attr nodes with name prefix + "_tensor_constant" and "_torchbind_obj". See Tracer.create_arg() in + torch/fx/_symbolic_trace.py + + However, this might result in name collision if the original mod already + has a different buffer with the same name. + + We resolve this potential name collision here by changing the target name + with a new number post fix. + """ + + existing_keys = OrderedSet( + [name for name, val in mod.named_parameters(remove_duplicate=False)] + ) + existing_keys.update( + OrderedSet([name for name, val in mod.named_buffers(remove_duplicate=False)]) + ) + + def find_smallest_i(graph: fx.Graph, prefix: str) -> int: + i = 0 + for node in graph.nodes: + if node.op == "get_attr" and node.target.startswith(prefix): + if len(node.target) > len(prefix): + post_fix = node.target.split(prefix)[-1] + if post_fix.isdigit(): + i = max(i, int(post_fix)) + for key in existing_keys: + if key.startswith(prefix): + if len(key) > len(prefix): + post_fix = key.split(prefix)[-1] + if post_fix.isdigit(): + i = max(i, int(post_fix)) + return i + 1 + + for node in gm.graph.nodes: + if node.op == "get_attr": + target_name = node.target + if not target_name.startswith( + "_tensor_constant" + ) and not target_name.startswith("_torchbind_obj"): + continue + + if not hasattr(mod, target_name): + continue + gm_target = attrgetter(target_name)(gm) + model_target = attrgetter(target_name)(mod) + if isinstance(gm_target, FakeScriptObject): + if ( + isinstance(model_target, FakeScriptObject) + and gm_target.real_obj is model_target.real_obj + ): + continue + elif ( + gm_target.device == model_target.device + and gm_target.dtype == model_target.dtype + and torch.equal(gm_target, model_target) + ): + # If tensors with same name from gm and model are indeed the same, we don't need to rename + # Check device first, to avoid torch.equal(wrapper_CUDA__equal) raise when different device + continue + + prefix = ( + "_tensor_constant" + if target_name.startswith("_tensor_constant") + else "_torchbind_obj" + ) + new_id = find_smallest_i(gm.graph, prefix) + new_target_name = f"{prefix}{new_id}" + node.target = new_target_name + setattr(gm, new_target_name, gm_target) + existing_keys.add(new_target_name) + + +def _unlift_graph( + mod: GraphModule, gm: GraphModule, graph_signature: GraphSignature +) -> GraphModule: + from torch.export.unflatten import _assign_attr, _AttrKind + + _resolve_name_collision(mod, gm) + + state_dict: dict[str, torch.nn.parameter.Parameter | torch.Tensor] = {} + for name, param in mod.named_parameters(remove_duplicate=False): + state_dict[name] = param + _assign_attr( + param, + gm, + name, + attr_kind=_AttrKind.PARAMETER, + ) + for name, buffer in mod.named_buffers(remove_duplicate=False): + state_dict[name] = buffer + _assign_attr( + buffer, + gm, + name, + attr_kind=_AttrKind.BUFFER, + ) + + placeholder_nodes = gm.graph.find_nodes(op="placeholder") + lifted_inputs: list[FQN | None] = [] + + # In AOTI, module parameters and buffers are not lifted as graph inputs. + # As a result, mutation to buffers has side effect which makes their initial + # values different from Eager. So we clone them here as a copy. + # We are not cloning for parameters, although it will be needed if we want to + # support training. + for node in placeholder_nodes: + node_name = node.name + if node_name in graph_signature.inputs_to_parameters: + parameter_name = graph_signature.inputs_to_parameters[node_name] + lifted_inputs.append(parameter_name) + elif node_name in graph_signature.inputs_to_buffers: + buffer_name = graph_signature.inputs_to_buffers[node_name] + lifted_inputs.append(buffer_name) + gm.meta[get_cloned_parameter_buffer_name(buffer_name)] = ( + clone_preserve_strides(state_dict[buffer_name]) + ) + else: + assert node_name in graph_signature.user_inputs + lifted_inputs.append(None) + + from torch.export._unlift import _unlift + + outputs: tuple[torch.fx.Node, ...] = tuple(gm.graph.output_node().args[0]) # type: ignore[arg-type] + mutated_outputs = [] + buffer_mutations = graph_signature.buffers_to_mutate + user_input_mutations = graph_signature.user_inputs_to_mutate + output_tokens = graph_signature.output_tokens + for idx, out in enumerate(outputs): + value: FQN | GraphInputName | None = None + + if idx < len(buffer_mutations) + len(user_input_mutations) + len(output_tokens): + name = GraphOutputName(out.name) + if name in buffer_mutations: + value = buffer_mutations[name] + elif name in user_input_mutations: + value = user_input_mutations[name] + + mutated_outputs.append(value) + + unlifted_gm = _unlift( + gm, + lifted_inputs, + mutated_outputs, + pytree.treespec_leaf(), + None, + ) + # After unlifting, the buffer mutation information is lost. Pass the information + # so that Inductor can do optimizations correctly. + unlifted_gm.meta["mutated_named_buffers"] = OrderedSet(buffer_mutations.values()) + return unlifted_gm + + +def _get_subgraph_names( + gm: GraphModule, skip_invoke_subgraph: bool = False +) -> Generator[str, None, None]: + all_subgraph_names: OrderedSet[str] = OrderedSet( + x.target for x in gm.graph.find_nodes(op="get_attr") + ) + fx_subgraph_names: OrderedSet[str] = OrderedSet() + for child_name, child_module in gm.named_children(): + # Sometimes an owning_module can have unused children. Skip them + # by checking them from get_attr node targets. + if child_name in all_subgraph_names and isinstance( + child_module, torch.fx.GraphModule + ): + fx_subgraph_names.add(child_name) + + if skip_invoke_subgraph: + for node in gm.graph.find_nodes( + op="call_function", target=torch.ops.higher_order.invoke_subgraph + ): + fx_subgraph_names.discard(node.args[0].target) + + yield from fx_subgraph_names + + +def _recursive_pre_grad_passes( + gm: GraphModule, + example_inputs: Sequence[InputType], +) -> GraphModule: + with dynamo_timed( + "_recursive_pre_grad_passes", + log_pt2_compile_event=True, + dynamo_compile_column_us="pre_grad_pass_time_us", + ): + if not config.use_pre_grad_passes: + return gm + + add_passes = config.add_pre_grad_passes + remove_passes = config.remove_pre_grad_passes + for subgraph_name in _get_subgraph_names(gm): + subgraph = getattr(gm, subgraph_name) + # as we don't have recursive example inputs, passing empty set here + new_subgraph = _recursive_pre_grad_passes(subgraph, ()) + setattr(gm, subgraph_name, new_subgraph) + return pre_grad_passes(gm, example_inputs, add_passes, remove_passes) + + +def _recursive_joint_graph_passes( + gm: GraphModule, + skip_invoke_subgraph: bool = False, + input_device: torch.device | None = None, +) -> GraphModule: + def _run_on_sub_graph_module(subgraph_name: str) -> None: + subgraph = getattr(gm, subgraph_name) + new_subgraph = _recursive_joint_graph_passes( + subgraph, skip_invoke_subgraph, input_device + ) + setattr(gm, subgraph_name, new_subgraph) + + with dynamo_timed( + "_recursive_joint_graph_passes", + log_pt2_compile_event=True, + dynamo_compile_column_us="joint_graph_pass_time_us", + ): + if not config.use_joint_graph_passes: + return gm + + # invoke_subgraph already runs the _recursive_joint_graph_passes. In + # AOTAutograd, `run_joint_graph_passes_on_hops` partitions the + # invoke_subgraph HOP before calling the partitioner on the outer graph. + # AOTAutograd has access to partition_fn, which internally calls the + # `_recursive_joint_graph_passes` for the subgraph. So, skip recursing + # skip_invoke_subgraph. + old_subgraph_names = OrderedSet(_get_subgraph_names(gm, skip_invoke_subgraph)) + for subgraph_name in old_subgraph_names: + _run_on_sub_graph_module(subgraph_name) + + out_gm = joint_graph_passes(gm, input_device) + + # Some joint graph passes may create new sub graph module. Run one round + # for the newly created graph modules. + # We should not skip graphs for invoke_subgraph HOPs for newly + # generated subgraphs. + for subgraph_name in _get_subgraph_names(out_gm, skip_invoke_subgraph=False): + if subgraph_name not in old_subgraph_names: + _run_on_sub_graph_module(subgraph_name) + return out_gm + + +def _recursive_post_grad_passes(gm: GraphModule, is_inference: bool = False) -> None: + with dynamo_timed( + "_recursive_post_grad_passes", + log_pt2_compile_event=True, + dynamo_compile_column_us="post_grad_pass_time_us", + ): + if not config.use_post_grad_passes: + return + + for subgraph_name in _get_subgraph_names(gm): + subgraph = getattr(gm, subgraph_name) + _recursive_post_grad_passes(subgraph, is_inference) + post_grad_passes(gm, is_inference) + + +def split_const_gm( + gm: GraphModule, + skip_constructor: bool = True, + lifted_constant_names: list[str] | None = None, + skip_folding_node_fn: Callable[[torch.fx.Node], bool] | None = None, +) -> tuple[GraphModule, dict[str, int]]: + """ + This function takes an GraphModule input "gm". + The gm will be split into 2 components, + 1) const_gm, which consists the subgraph of gm that can be constant folded. + 2) gm (being inplace modified,) which returns the graph after constant folding. + + If an additional "lifted_constants" argument is passed in, we will assume the gm has + been lifted and run the transformation accordingly. + + When a "skip_folding_node_fn" callback is passed, we will skip constant folding on + the nodes for which the callback returns True. + + const_output_index is a mapping of corresponding node name from gm to the + output index of const_gm. + Returns (const_gm, const_output_index) + """ + from torch._inductor.constant_folding import ( + CONST_MODULE_TAG, + META_TAG, + MODULE_TAG, + replace_node_with_constant, + run_and_get_constant_graph, + ) + + const_gm = run_and_get_constant_graph( + gm, skip_constructor, lifted_constant_names, skip_folding_node_fn + ) + const_result = const_gm() if lifted_constant_names is None else None + + const_outputs = { + x.name: idx for idx, x in enumerate(tuple(const_gm.graph.nodes)[-1].args[0]) + } + + to_erase_node = [] + to_replace_node = [] + const_output_index = {} + for node in gm.graph.nodes: + if node.name in const_outputs: + to_replace_node.append(node) + elif node.meta[META_TAG] == CONST_MODULE_TAG and node.op != "placeholder": + to_erase_node.append(node) + + for node in to_replace_node: + new_const_name = "_FOLDED_CONST_" + node.name + replace_node_with_constant( + gm, + node, + ( + const_result[const_outputs[node.name]] # type:ignore[index] + if lifted_constant_names is None + else None + ), + new_const_name, + ) + const_output_index[new_const_name] = const_outputs[node.name] + for node in to_erase_node[::-1]: + if node.users: + for n in node.users: + assert n.meta[META_TAG] == MODULE_TAG, f"node: {node} user not empty." + else: + gm.graph.erase_node(node) + gm.recompile() + + return const_gm, const_output_index + + +def is_tf32_warning_applicable(gm: GraphModule) -> bool: + aten = torch.ops.aten + tf32_ops = OrderedSet( + [ + aten.mm.default, + aten.addmm.default, + aten.bmm.default, + aten.baddbmm.default, + ] + ) + for target in tf32_ops: + for node in gm.graph.find_nodes(op="call_function", target=target): + if ( + isinstance(node.meta.get("val", None), torch.Tensor) + and node.meta["val"].dtype == torch.float32 + and node.meta["val"].device.type == "cuda" + ): + return True + return False + + +def maybe_disable_comprehensive_padding( + example_inputs: Sequence[InputType], +) -> AbstractContextManager[None, None]: + """ + For CPU backend, enable comprehensive padding causes some unit tests + fail due to changing number of generated kernels. Skip for now. + """ + has_gpu = any( + is_gpu(t.device.type) for t in example_inputs if isinstance(t, torch.Tensor) + ) + + if config.disable_padding_cpu and config.comprehensive_padding and not has_gpu: + perf_hint_log.info("Skip comprehensive padding on CPU") + return config.patch(comprehensive_padding=False) + elif config.aot_inductor.use_runtime_constant_folding: + perf_hint_log.info( + "Skip comprehensive padding for use_runtime_constant_folding" + ) + return config.patch(comprehensive_padding=False) + else: + return contextlib.nullcontext() + + +def maybe_disable_graph_partition( + cpp_wrapper: bool, aot_mode: bool +) -> AbstractContextManager[None, None]: + """ + graph partition does not support cpp_wrapper and aot_mode yet. + """ + if cpp_wrapper or aot_mode: + return config.patch(graph_partition=False) + else: + return contextlib.nullcontext() + + +def fake_tensor_prop( + gm: GraphModule, + example_inputs: Sequence[InputType], + force_allow_non_fake_inputs: bool = False, +) -> torch._subclasses.FakeTensorMode: + """ + If we can not detect fake mode from the context of inputs, create one. + + The created fake mode will be returned. + """ + # Ensure that decomps that support symbolic shapes are used + with enable_python_dispatcher(): + fake_mode = detect_fake_mode(example_inputs) + if not fake_mode: + fake_mode = torch._subclasses.FakeTensorMode(allow_non_fake_inputs=True) + FakeTensorProp(gm, mode=fake_mode).propagate(*example_inputs) + else: + ctx = ( + contextlib.nullcontext() + if not force_allow_non_fake_inputs + else mock.patch.object(fake_mode, "allow_non_fake_inputs", True) + ) + with ctx: # type: ignore[attr-defined] + FakeTensorProp(gm, mode=fake_mode).propagate_dont_convert_inputs( + *example_inputs + ) + + return fake_mode + + +# pass config dict back to user +def get_patched_config_dict( + config_patches: str | dict[str, Any] | None = None, +) -> dict[str, Any]: + with config.patch(config_patches): + return config.get_config_copy() + + +@contextlib.contextmanager +def with_fresh_cache_if_config() -> Generator[None, None, None]: + if config.force_disable_caches: + # Don't delete the cache dir because it has to survive beyond the + # compile_fx call. Let's put the temp dirs under the default cache + # dir so they're easier to locate. + with fresh_cache(dir=cache_dir(), delete=False): + yield + else: + yield + + +class _CompileFxKwargs(TypedDict, total=False): + cudagraphs: BoxedBool | None + static_input_idxs: Sequence[int] + is_backward: bool + graph_id: int | None + cpp_wrapper: bool + aot_mode: bool + is_inference: bool + layout_opt: bool | None + extern_node_serializer: Callable[[list[ExternKernelNode]], Any] | None + boxed_forward_device_index: BoxedDeviceIndex | None + fx_wrapper: bool + get_decomp_fn: Callable[..., dict[Any, Callable[..., Any]]] + + +class _CompileFxCallable(Protocol): + def __call__( + self, + gm: GraphModule, + example_inputs: Sequence[InputType], + compile_region_name: str | None = None, + **kwargs: Unpack[_CompileFxKwargs], + ) -> OutputCode: ... + + +def compile_fx_inner( + gm: GraphModule, + example_inputs: Sequence[InputType], + compile_region_name: str | None = None, + **kwargs: Unpack[_CompileFxKwargs], +) -> OutputCode: + kwargs.setdefault("cudagraphs", None) + kwargs.setdefault("static_input_idxs", ()) + kwargs.setdefault("is_backward", False) + kwargs.setdefault("graph_id", None) + kwargs.setdefault("cpp_wrapper", False) + kwargs.setdefault("fx_wrapper", False) + kwargs.setdefault("is_inference", False) + kwargs.setdefault("boxed_forward_device_index", None) + kwargs.setdefault("layout_opt", None) + kwargs.setdefault("extern_node_serializer", None) + + # Need with_fresh_cache_if_config for compile_fx_inner even if we already have one for + # compile_fx. The reason is the compilation for backward graph may happen after + # compile_fx return and we may want to use the _LazyGraphModule for compiling + # the backward graph as well. + with contextlib.ExitStack() as stack: + stack.enter_context(torch.utils._python_dispatch._disable_current_modes()) + stack.enter_context(_use_lazy_graph_module(dynamo_config.use_lazy_graph_module)) + stack.enter_context( + dynamo_utils.dynamo_timed( + "compile_fx_inner", + phase_name="inductor_compile", + log_pt2_compile_event=True, + log_waitcounter=True, + waitcounter_name_override="compile_inductor", + dynamo_compile_column_us="inductor_cumulative_compile_time_us", + ) + ) + stack.enter_context(with_fresh_cache_if_config()) + stack.enter_context(DebugContext()) + CompileEventLogger.pt2_compile( + "inductor_compile", + is_backward=kwargs["is_backward"], + ) + return wrap_compiler_debug(_compile_fx_inner, compiler_name="inductor")( + gm, + example_inputs, + compile_region_name=compile_region_name, + **kwargs, + ) + + +@time_and_log(attr="compilation time (in seconds)") +def _compile_fx_inner( + gm: GraphModule, + example_inputs: Sequence[InputType], + compile_region_name: str | None = None, + **graph_kwargs: Unpack[_CompileFxKwargs], +) -> OutputCode: + """ + Inductor API that compiles a single graph. + + If you change the argument list for this function, make sure you + also update the call to save_args_for_compile_fx_inner below accordingly. + """ + aot_mode: bool = V.aot_compilation + + from torch._inductor.autotune_process import use_pipelined_autotuning + + if use_pipelined_autotuning(): + # Warm up max-autotune process pool asap + from torch._inductor.autotune_process import AutotuneProcessPool + + pool_instance = AutotuneProcessPool.get_instance() + pool_instance.warm_up() + + # Clean up Compiled Triton Kernels per inductor compile, as the future objects + # may not be valid for use after they are run/autotuned + torch._inductor.async_compile.CompiledTritonKernels.cache_clear() + + if ( + dynamo_utils.count_calls(gm.graph) == 0 + and not aot_mode + and not torch._functorch.config.bundled_autograd_cache + ): + # trigger the real recompilation for _LazyGraphModule before returning + # the forward method. + from torch._dynamo.utils import CompileEventLogLevel + from torch.fx._lazy_graph_module import _LazyGraphModule + + _LazyGraphModule.force_recompile(gm) + compile_id = torch._guards.CompileContext.current_compile_id() + CompileEventLogger.log_instant_event( + "backward no-op", + metadata={"compile_id": compile_id}, + log_level=CompileEventLogLevel.PT2_COMPILE, + ) + + # pyrefly: ignore[bad-return] + return make_boxed_func(gm.forward) + + static_input_idxs: Sequence[int] = graph_kwargs.setdefault("static_input_idxs", ()) + static_inputs_log.debug("static input idxs compile_fx_inner: %s", static_input_idxs) + inputs_to_check = get_input_idxs_to_check(example_inputs, static_input_idxs) + + assert isinstance(next(iter(reversed(gm.graph.nodes))).args[0], (tuple, list)), ( + f"inductor can only compile FX graphs which return a tuple/list, but got {gm.graph}" + ) + + if graph_kwargs.get("cudagraphs") is None: + graph_kwargs["cudagraphs"] = BoxedBool(config.triton.cudagraphs) + if config.save_args: + save_args_for_compile_fx_inner( + gm, + example_inputs, + compile_region_name=compile_region_name, + **graph_kwargs, + ) + + start = time.time() + + fx_graph_remote_cache = should_use_remote_fx_graph_cache() + + # Check if the registered backend(s) support caching. + init_backend_registration() + backends_support_caching = all( + backend.supports_caching + for backend in ( + get_wrapper_codegen_for_device( + device.type, config.cpp_wrapper, config.fx_wrapper + ) + for device in get_all_devices(gm) + ) + if backend is not None + ) + + with dynamo_timed( + "fx_codegen_and_compile", log_pt2_compile_event=True, log_waitcounter=True + ): + use_cache = ( + not config.force_disable_caches + and (config.fx_graph_cache or fx_graph_remote_cache) + and not aot_mode + and backends_support_caching + and not torch._functorch.config.bundled_autograd_cache + ) + local = config.fx_graph_cache + remote = fx_graph_remote_cache + set_feature_use("fx_cache", use_cache) + + log.debug( + "FX cache status: use_cache=%s, local=%s, remote=%s, aot_mode=%s, force_disable_caches=%s", + use_cache, + local, + remote, + aot_mode, + config.force_disable_caches, + ) + + # TODO: This is a hack purely to get some info to extract_tensor_metadata_for_cache_key, + # figure out how to not have to modify example inputs + for i, input in enumerate(example_inputs): + if ( + isinstance(input, torch.Tensor) + and is_gpu(input.device.type) + and i in static_input_idxs + ): + input._is_inductor_static = True # type: ignore[attr-defined] + + mb_compiled_graph: OutputCode | None = None + key_info = None + cache_info = None + remote_cache = None + constants = CompiledFxGraphConstantsWithGm(gm) + # TODO: this time will be slightly inconsistent with the one computed + # in prepare_key/load_with_key, dump those settings of "cache_event_time" + start_time = time.time_ns() + + if use_cache: + (key_info, cache_info) = FxGraphCache.prepare_key( + gm, example_inputs, graph_kwargs, inputs_to_check, remote + ) + + # Attempt a cache lookup + if key_info is not None: + key, debug_lines = key_info + log.debug("FX cache key generated: %s", key) + if remote: + remote_cache = FxGraphCache.get_remote_cache() + log.debug("Using remote FX cache") + mb_compiled_graph, cache_info = FxGraphCache.load_with_key( + key, + debug_lines, + example_inputs, + local, + remote_cache, + is_backward=graph_kwargs.get("is_backward", False), + constants=constants, + ) + else: + log.debug("Failed to generate FX cache key") + + if torch._functorch.config.bundled_autograd_cache: + assert mb_compiled_graph is None + assert cache_info is None + # When using bundled autograd cache, we still want + # to use the TritonBundler, but we don't want to save + # the results here. The results will get saved directly + # to AOTAutogradCache. + TritonBundler.begin_compile() + try: + mb_compiled_graph = fx_codegen_and_compile( + gm, + example_inputs, + inputs_to_check, + compile_region_name=compile_region_name, + **graph_kwargs, + ) + assert mb_compiled_graph is not None + ( + triton_bundle, + triton_bundler_meta, + ) = TritonBundler.collect() + mb_compiled_graph.set_triton_bundle(triton_bundle) + except (ShortenTraceback, SkipFrame): + raise + except Exception as e: + raise InductorError(e, currentframe()).with_traceback( + e.__traceback__ + ) from None + finally: + TritonBundler.end_compile() + + # CACHE BYPASS: Compile the graph, don't save it to the cache + # (this can happen either because cache was disabled, or we + # determined the input is uncacheable) + elif cache_info is None or cache_info["cache_state"] == "bypass": + assert mb_compiled_graph is None + log.debug( + "FX cache bypass reason: %s", + ( + cache_info.get("cache_bypass_reason", "unknown") + if cache_info is not None + else "FX cache disabled or key generation failed" + ), + ) + try: + mb_compiled_graph = fx_codegen_and_compile( + gm, + example_inputs, + inputs_to_check, + compile_region_name=compile_region_name, + **graph_kwargs, + ) + except Exception as e: + raise InductorError(e, currentframe()).with_traceback( + e.__traceback__ + ) from None + + # CACHE MISS: Compile the graph and save to cache + elif cache_info["cache_state"] == "miss": + assert mb_compiled_graph is None + assert key_info is not None + log.debug("FX cache miss, compiling and saving to cache") + TritonBundler.begin_compile() + try: + mb_compiled_graph = fx_codegen_and_compile( + gm, + example_inputs, + inputs_to_check, + compile_region_name=compile_region_name, + **graph_kwargs, + ) + assert mb_compiled_graph is not None + mb_compiled_graph._time_taken_ns = time.time_ns() - start_time + cache_key, debug_lines = key_info + mb_compiled_graph._fx_graph_cache_key = cache_key + mb_compiled_graph._fx_graph_cache_debug_lines = debug_lines + ( + triton_bundle, + triton_bundler_meta, + ) = TritonBundler.collect() + mb_compiled_graph.set_triton_bundle(triton_bundle) + except (ShortenTraceback, SkipFrame): + raise + except Exception as e: + raise InductorError(e, currentframe()).with_traceback( + e.__traceback__ + ) from None + finally: + TritonBundler.end_compile() + if triton_bundler_meta is not None: + cache_info["triton_bundler_meta"] = str(triton_bundler_meta) + cache_info["time_taken_ns"] = mb_compiled_graph._time_taken_ns + log.debug("Saving compiled graph to FX cache with key: %s", cache_key) + FxGraphCache._save_graph( + cache_key, + mb_compiled_graph, + example_inputs, + local, + remote_cache, + ) + + # CACHE HIT: not much to really do, just make sure the cache key + # is recorded on the graph + else: + assert cache_info["cache_state"] == "hit" + assert mb_compiled_graph is not None + assert key_info is not None + (cache_key, debug_lines) = key_info + log.debug("FX cache hit with key: %s", cache_key) + mb_compiled_graph._fx_graph_cache_key = cache_key + mb_compiled_graph._fx_graph_cache_debug_lines = debug_lines + + assert mb_compiled_graph is not None + compiled_graph = mb_compiled_graph + if isinstance(compiled_graph, CompiledFxGraph): + compiled_graph.compile_region_name = compile_region_name + + # Logging and observability: we log a single chromium event + # and a tlparse log for every cache action. + # In the event of a bypass, we also logged to the remote table earlier + # with log_cache_bypass. + cache_state = ( + cache_info["cache_state"] if cache_info is not None else "disabled" + ) + # Here for grepping: + # fx_graph_cache_hit + # fx_graph_cache_miss + # fx_graph_cache_bypass + # fx_graph_cache_disabled + CompileEventLogger.instant( + f"fx_graph_cache_{cache_state}", + metadata=cache_info or {}, + time_ns=start_time, + ) + # Add event data about cache hits/miss + # TODO: add remote cache get/put timings here too + CompileEventLogger.try_add_pt2_compile( + "inductor_compile", + cache_state=cache_state, + cache_event_time=start_time, + key=cache_info.get("key") if cache_info else None, + components=cache_info.get("components") if cache_info else None, + cache_bypass_reason=( + cache_info.get("cache_bypass_reason") + if cache_info + else "cache not enabled" + ), + remote_cache_enabled=remote, + local_cache_enabled=local, + ) + + # Don't clog up the main tlparse output with disabled cache + if cache_info is not None: + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": f"fx_graph_cache_{cache_state}", + "encoding": "json", + }, + payload_fn=lambda: json.dumps(cache_info), + ) + compiled_graph.post_compile(example_inputs, constants, graph_kwargs) + + policy = config.cudagraph_policy + if policy is not None: + compiled_graph = policy.wrap_output(compiled_graph) + + log.debug("FX codegen and compilation took %.3fs", time.time() - start) + + # This message is for printing overview information of inductor mm counts, shapes,etc after lowering + if log.isEnabledFor(logging.INFO): + mm_table_data = [] + for key, value in counters["aten_mm_info"].items(): + parts = key.split("_") + if len(parts) < 3: + # Unexpected format, show as-is + mm_table_data.append([key, "-", "?", "?", "?", value]) + continue + + # Determine if this is a batched operation by checking the operation name + name = "_".join(parts[:-4]) if len(parts) >= 4 else "_".join(parts[:-3]) + is_batched = name.endswith(("bmm", "baddbmm")) + + if is_batched and len(parts) >= 4: + # Batched operation: last 4 parts are batch, m, n, k + batch, m, n, k = parts[-4:] + name = "_".join(parts[:-4]) + mm_table_data.append([name, batch, m, n, k, value]) + else: + # Non-batched operation: last 3 parts are m, n, k + m, n, k = parts[-3:] + name = "_".join(parts[:-3]) + mm_table_data.append([name, "-", m, n, k, value]) + + log.info("Overview info of inductor aten mms: ") + log.info( + "{:<30} | {:<20} | {:<20} | {:<20} | {:<20} | {:<20}".format( # noqa: G001 + "Name", "B", "M", "N", "K", "Count" + ) + ) + log.info("-" * 130) + for row in mm_table_data: + log.info("{:<30} | {:<20} | {:<20} | {:<20} | {:<20} | {:<20}".format(*row)) # noqa: G001 + log.info("-" * 130) + + # Not strictly necessary, but good to clean up straggling futures + # that are unused to reclaim memory. + torch._inductor.async_compile.CompiledTritonKernels.cache_clear() + + _step_logger()( + logging.INFO, + "torchinductor done compiling " + f"{'BACKWARDS' if graph_kwargs['is_backward'] else 'FORWARDS'} " + f"graph {graph_kwargs['graph_id']}", + ) + return compiled_graph + + +class _FxCompileStat: + # Count of successful compiles of this type + codegen_and_compile: int = 0 + + def __repr__(self) -> str: + return f"codegen_and_compile: {self.codegen_and_compile}" + + +class FxCompile(ABC): + """ + An FxCompile represents a mechanism that can turn a GraphModule into an + OutputCode. + """ + + # Some stats for logging/debugging + _compile_stats: dict[type[FxCompile], _FxCompileStat] = defaultdict(_FxCompileStat) + compile_region_name: str | None = None + + # TODO: We should probably eventually add some kind of async version of this + # so we can kick off a compile and then go do other things - but we'll need + # to know what kind of API we want for that first. + @abstractmethod + def codegen_and_compile( + self, + gm: GraphModule, + example_inputs: Sequence[InputType], + inputs_to_check: Sequence[int], + graph_kwargs: _CompileFxKwargs, + ) -> OutputCode: ... + + @classmethod + def _reset_stats(cls) -> None: + cls._compile_stats.clear() + + +class _InProcessFxCompile(FxCompile): + @override + def codegen_and_compile( + self, + gm: GraphModule, + example_inputs: Sequence[InputType], + inputs_to_check: Sequence[int], + graph_kwargs: _CompileFxKwargs, + ) -> OutputCode: + """ + Generates the OutputCode from the GraphModule and example_inputs. + """ + # Sorry about the mess, we need graph_kwargs to continue to be able + # to propagate it further on + # TODO: _CompileFxKwargs actually has stronger types than in the + # signature, need to tighten it up + + assert "cudagraphs" in graph_kwargs and graph_kwargs["cudagraphs"] is not None + cudagraphs: BoxedBool = graph_kwargs["cudagraphs"] + static_input_idxs: Sequence[int] = graph_kwargs.get("static_input_idxs", ()) + is_backward: bool = graph_kwargs.get("is_backward", False) + graph_id: int | None = graph_kwargs.get("graph_id", None) + cpp_wrapper: bool = graph_kwargs.get("cpp_wrapper", False) + fx_wrapper: bool = graph_kwargs.get("fx_wrapper", False) + aot_mode: bool = V.aot_compilation + is_inference: bool = graph_kwargs.get("is_inference", False) + extern_node_serializer: Callable[[list[ExternKernelNode]], Any] | None = ( + graph_kwargs.get("extern_node_serializer", None) + ) + get_decomp_fn: Callable[..., dict[Any, Callable[..., Any]]] = graph_kwargs.get( + "get_decomp_fn", select_decomp_table + ) + with ( + _WaitCounter("pytorch.wait_counter.actual_codegen_and_compile").guard(), + dynamo_utils.preserve_rng_state(), + ): + if (sleep_sec := config.sleep_sec_TESTING_ONLY) is not None: + import time + + log.warning( + "Sleeping for %s since sleep_sec_TESTING_ONLY is set", sleep_sec + ) + time.sleep(sleep_sec) + + if is_tf32_warning_applicable(gm): + _warn_tf32_disabled() + + inductor_counters = counters["inductor"].copy() + + # lift the maximum depth of the Python interpreter stack + # to adapt large/deep models + sys.setrecursionlimit(max(sys.getrecursionlimit(), 2000)) + + _step_logger()( + logging.INFO, + "torchinductor compiling " + f"{'BACKWARDS' if is_backward else 'FORWARDS'} " + f"graph {graph_id}", + ) + + fd = io.StringIO() + torch._dynamo.repro.after_aot.save_graph_repro( + fd, gm, example_inputs, "inductor", save_dir=None + ) + runnable_graph_str = fd.getvalue() + + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "fx_graph_runnable", + "encoding": "string", + }, + payload_fn=lambda: runnable_graph_str, + ) + + V.debug.fx_graph(gm, example_inputs) + # TODO: Should we actually dump this? It should be redundant with the aot + # structured logs... + # trace_structured("inductor_input_graph", payload_fn=lambda: gm.print_readable(print_output=False)) + + shape_env = gm.shape_env + if shape_env is None: + shape_env = shape_env_from_inputs(example_inputs) + + # Convert view to reshape in the graph. This is necessary primarily for + # layout optimization. Do it unconditionally for uniformity. + # + # It's needed because when we do layout optimization, an contiguous tensor + # in eager mode may becomes a channels last tensor. A view op previously + # can be applied to the contiguous tensor may not be able to be applied + # on the channels tensor any more. An error like + # RuntimeError: view size is not compatible with input tensor's size and stride + # (at least one dimension spans across two contiguous subspaces). Use .reshape(...) instead. + # will be printed. + # + # Replace view op to reshape op in this case. + # As an example, timm_resnest/botnet26t_256/convnext_base etc. will fail if we don't do this. + # + # Also this has to be done before FakeTensorProp below to avoid the failed + # .view() call. + view_to_reshape(gm) + + with dynamo_timed( + "additional_fake_tensor_prop", log_pt2_compile_event=True + ): + # It is safe to run FakeTensorProp under no_grad because by the time + # we're in inductor, we assume that AOTAutograd has already "taken care" + # of autograd, so there should be no more autograd-related API's in the + # graph. + with torch.no_grad(): + fake_mode = fake_tensor_prop(gm, example_inputs) + + _recursive_record_original_output_strides(gm) + + # pattern matcher passes might not preserve striding information + # on node.meta["val"]. if in the future we rely on these being + # correct we will need to fix. + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "before_post_grad_graph", + "encoding": "string", + }, + payload_fn=lambda: gm.print_readable( + print_output=False, include_stride=True, include_device=True + ), + ) + with V.set_fake_mode(fake_mode): + # has some issues with memory in training + cuda_context = get_cuda_device_context(gm) + with cuda_context: + _recursive_post_grad_passes(gm, is_inference=is_inference) + V.debug.fx_graph_transformed(gm, example_inputs) + post_grad_graphs_log.debug( + "%s", + lazy_format_graph_code( + "AFTER POST GRAD", + gm, + include_stride=True, + include_device=True, + colored=True, + ), + ) + + # We're printing the graph to be used as a cache key - so a + # printer which is a little less readable but faster is + # appropriate. + inductor_post_grad_graph_str = gm.print_readable( + print_output=False, + include_stride=True, + include_device=True, + fast_sympy_print=True, + ) + # "inductor_post_grad_graph" is used in inductor provenance + # tracking highlighter front-end. + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "inductor_post_grad_graph", + "encoding": "string", + }, + payload_fn=lambda: inductor_post_grad_graph_str, + ) + if config.trace.provenance_tracking_level != 0: + provenance_tracking_json = ( + torch.fx.traceback.get_graph_provenance_json(gm.graph) + ) + torch._inductor.debug._inductor_post_to_pre_grad_nodes = ( + create_mapping_pre_post_grad_nodes( + torch._inductor.debug._pre_grad_graph_id, + provenance_tracking_json, + ) + ) + + metrics_context = get_metrics_context() + if metrics_context.in_progress(): + num_graph_breaks = counters["graph_break"].total() + CompileEventLogger.compilation_metric( + overwrite=True, num_graph_breaks=num_graph_breaks + ) + if config.is_fbcode(): + try: + log_optimus_to_scuba( + extra_logging={ + "pt2_configs": str(get_patched_config_dict()) + } + ) + except Exception: + # TODO(T216453900): need to work around for now to support vllm + # See details in vllm/compilation/pass_manager.py. + log.warning("failed to log pt2_configs") + + with ( + V.set_fake_mode(fake_mode), + maybe_disable_comprehensive_padding(example_inputs), + maybe_disable_graph_partition(cpp_wrapper, aot_mode), + ): + const_output_index = None + const_graph = None + const_wrapper_code = None + const_kernel_code = None + + if aot_mode and config.aot_inductor.use_runtime_constant_folding: + # torchbind objects have name that starts with _torchbind_obj + # See caffe2/torch/fx/_symbolic_trace.py?lines=406 + const_gm, const_output_index = split_const_gm( + gm, + skip_folding_node_fn=lambda node: node.op == "get_attr" + and isinstance(node.target, str) + and ( + node.target.startswith("_torchbind_obj") + or isinstance(node.meta.get("val", None), FakeScriptObject) + ), + ) + + const_graph = GraphLowering( + const_gm, + example_inputs=[], + shape_env=shape_env, + graph_id=graph_id, + cpp_wrapper=cpp_wrapper, + aot_mode=aot_mode, + extern_node_serializer=extern_node_serializer, + is_inference=is_inference, + is_backward=is_backward, + is_const_graph=True, + fx_wrapper=fx_wrapper, + get_decomp_fn=get_decomp_fn, + ) + with ( + V.set_graph_handler(const_graph), + V.set_extern_kernel_nodes([]), + ): + assert cpp_wrapper, "AOT mode only supports C++ wrapper" + const_graph.run() + const_wrapper_code, const_kernel_code = ( + const_graph.codegen_with_cpp_wrapper() + ) + + graph = GraphLowering( + gm, + # example_inputs will be used by AOTInductor to dry-run the generated code for Triton kernel tuning. + # For the forward pass, we have the real inputs to be used as example_inputs. For the backward pass, + # we currently use fake tensors and defake them later. + example_inputs=example_inputs, + shape_env=shape_env, + graph_id=graph_id, + cpp_wrapper=cpp_wrapper, + aot_mode=aot_mode, + extern_node_serializer=extern_node_serializer, + is_inference=is_inference, + is_backward=is_backward, + const_output_index=const_output_index, + const_wrapper_code=( + const_wrapper_code.value if const_wrapper_code else None + ), + const_kernel_code=( + const_kernel_code.value if const_kernel_code else None + ), + const_module=const_graph, + inputs_to_check=inputs_to_check, + fx_wrapper=fx_wrapper, + get_decomp_fn=get_decomp_fn, + ) + metrics_helper = metrics.CachedMetricsHelper() + + # We are going to start code generating runtime asserts, so make sure + # you don't start adding new ones in the lowering process + graph.freeze_runtime_asserts() + with ( + V.set_graph_handler(graph), + V.set_extern_kernel_nodes([]), + distributed_autotune.graph_context(), + ): + graph.run(*example_inputs) + output_strides: list[tuple[_StrideExprStr, ...] | None] = [] + if graph.graph_outputs is not None: + # We'll put the output strides in the compiled graph so we + # can later return them to the caller via TracingContext + p = SymExprPrinter() + for out in graph.graph_outputs: + if ( + isinstance(out, IRNode) + and out.has_tensor_output() + and len(free_unbacked_symbols(out.get_stride())) == 0 + ): + # Convert to string for eval on the load path + output_strides.append( + tuple(p.doprint(s) for s in out.get_layout().stride) + ) + else: + output_strides.append(None) + + _check_triton_bf16_support(graph) + + # TODO: The switching between AOT mode and not here is a bit + # messy, but it's localized to the block of code below so I'm + # not going to touch it for now + + compiled_fn: Any + compiled_fn_runner = None + with dynamo_timed( + "GraphLowering.compile_to_fn", log_pt2_compile_event=True + ): + if graph.aot_mode and graph.fx_wrapper: + assert not graph.cpp_wrapper + compiled_fn = graph.codegen()[0].gm # type: ignore[attr-defined] + output_code_log.debug( + "Output graph module: \n%s", + compiled_fn.print_readable(print_output=False), + ) + + elif graph.aot_mode: + from .codecache import AotCodeCompiler + + assert graph.cpp_wrapper, ( + "AOT mode only supports C++ wrapper" + ) + wrapper_code, kernel_code = graph.codegen_with_cpp_wrapper() + output_code_log.debug( + "Output wrapper code: \n%s", wrapper_code.value + ) + if kernel_code.value: + output_code_log.debug( + "Output kernel code:\n%s", kernel_code.value + ) + + serialized_extern_kernel_nodes = None + if V.extern_kernel_nodes: + serialized_extern_kernel_nodes = ( + graph.extern_node_serializer(V.extern_kernel_nodes) + ) + output_code_log.debug( + "Serialized Extern Kernel Nodes: \n%s", + serialized_extern_kernel_nodes, + ) + + with dynamo_timed( + "AotCodeCompiler.compile", log_pt2_compile_event=True + ): + # Directly return the file path with the compiled code + compiled_fn = AotCodeCompiler.compile( + graph, + wrapper_code.value, + kernel_code.value, + serialized_extern_kernel_nodes, + device_type=graph.device_type, + additional_files=[ + *dict.fromkeys( + graph.wrapper_code.additional_files + + ( + const_graph.wrapper_code.additional_files + if const_graph + else [] + ) + ) + ], + ) + else: + compiled_module = graph.compile_to_module() + compiled_fn = compiled_module.call + compiled_fn_runner = getattr( + compiled_module, "runner", None + ) + + # Dump provenance artifacts for debugging trace + inductor_provenance_tracking_node_mappings = None + inductor_kernel_stack_trace_str = None + if config.trace.provenance_tracking_level != 0: + inductor_provenance_tracking_node_mappings = json.dumps( + torch._inductor.debug.dump_inductor_provenance_info() + ) + inductor_kernel_stack_trace_str = json.dumps( + torch._inductor.debug._inductor_kernel_stack_trace + ) + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "inductor_provenance_tracking_node_mappings", + "encoding": "json", + }, + payload_fn=lambda: inductor_provenance_tracking_node_mappings, + ) + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "inductor_provenance_tracking_kernel_stack_traces", + "encoding": "json", + }, + payload_fn=lambda: inductor_kernel_stack_trace_str, + ) + if inductor_kernel_stack_trace_str: + metrics_context = get_metrics_context() + if metrics_context.in_progress(): + metrics_context.add_to_set( + "inductor_provenance", + inductor_kernel_stack_trace_str, + ) + + node_runtimes = None + if inductor_metrics_log.isEnabledFor(logging.INFO): + num_bytes, nodes_num_elem, node_runtimes = graph.count_bytes() + # pyrefly: ignore [bad-assignment] + metrics.num_bytes_accessed += num_bytes + metrics.node_runtimes += node_runtimes + metrics.nodes_num_elem += nodes_num_elem + inductor_metrics_log.info( + "Graph Metrics:\n%s", + { + "num_bytes_accessed": num_bytes, + "nodes_num_elem": nodes_num_elem, + "node_runtimes": node_runtimes, + }, + ) + + # Collect and dump op runtimes and tensor metadata for TLParse + if config.log_tlparse: + _, _, node_runtimes = graph.count_bytes() + torch._inductor.debug.log_runtime_and_tensor_meta(node_runtimes) + + # Collect and dump collective-op schedule for external diagnostics + torch._inductor.debug.log_collective_schedule(graph.scheduler.nodes) + + # When graph_partition is enabled, skip this check - partitioning handles dynamic shapes + if ( + cudagraphs + and config.triton.cudagraph_skip_dynamic_graphs + and not config.graph_partition + and not V.graph.disable_cudagraphs_reason + and torch._inductor.utils.any_is_symbolic(*example_inputs) + ): + stack_trace = None + for node in gm.graph.nodes: + meta_val = node.meta.get("val", None) + if ( + node.op == "placeholder" + or not isinstance(meta_val, torch.Tensor) + or not torch._inductor.utils.any_is_symbolic(meta_val) + ): + continue + + if stack_trace := node.meta.get("stack_trace", None): + break + disable = "graph with symbolic shapes inputs and config.triton.cudagraph_skip_dynamic_graphs=True." + if stack_trace: + disable = f"{disable} Found from {stack_trace}\n" + else: + disable = f"{disable}\n" + # pyrefly: ignore [unbound-name] + V.graph.disable_cudagraphs_reason = disable + + # pyrefly: ignore [unbound-name] + # When graph_partition is enabled, skip this check - partitioning handles incompatible ops + if ( + cudagraphs + # pyrefly: ignore [unbound-name] + and not config.graph_partition + # pyrefly: ignore [unbound-name] + and not V.graph.disable_cudagraphs_reason + ): + maybe_incompat_node = get_first_incompatible_cudagraph_node(gm) + if maybe_incompat_node: + disable = f"disabling cudagraphs due to incompatible op {maybe_incompat_node.target}" + if stack_trace := maybe_incompat_node.meta.get( + "stack_trace", None + ): + disable = f"{disable} Found from {stack_trace}\n" + # pyrefly: ignore [unbound-name] + V.graph.disable_cudagraphs_reason = disable + + # pyrefly: ignore [unbound-name] + if V.aot_compilation: + assert isinstance( + compiled_fn, + # pyrefly: ignore [unbound-name] + (str, list, torch.fx.GraphModule), + ), type(compiled_fn) + return CompiledAOTI( + filename=compiled_fn, device_type=graph.device_type + ) + + # TODO: Hoist this above V.aot_compilation + # pyrefly: ignore [unbound-name] + if cudagraphs and not V.graph.disable_cudagraphs_reason: + from torch._inductor.cudagraph_utils import ( + check_lowering_disable_cudagraph, + ) + + # pyrefly: ignore [unbound-name] + V.graph.disable_cudagraphs_reason = ( + check_lowering_disable_cudagraph( + # pyrefly: ignore [unbound-name] + V.graph.device_node_mapping + ) + ) + + self._compile_stats[type(self)].codegen_and_compile += 1 + + if ( + # pyrefly: ignore [unbound-name] + torch._inductor.debug.RECORD_GRAPH_EXECUTION + # pyrefly: ignore [unbound-name] + and torch._inductor.debug.GRAPH_COMPILE_IDS is not None + ): + compile_id = str( + # pyrefly: ignore [unbound-name] + torch._guards.CompileContext.current_compile_id() + ) + graph_id = graph_kwargs.get("graph_id") + if graph_id is not None: + # pyrefly: ignore [unbound-name] + torch._inductor.debug.GRAPH_COMPILE_IDS[graph_id] = ( + compile_id + ) + + return CompiledFxGraph( + compiled_fn, + graph, + gm, + output_strides, + # pyrefly: ignore [unbound-name] + V.graph.disable_cudagraphs_reason, + metrics_helper.get_deltas(), + counters["inductor"] - inductor_counters, + cudagraphs, + example_inputs, + static_input_idxs, + self.compile_region_name, + graph_kwargs, + inputs_to_check, + runnable_graph_str, + inductor_post_grad_graph_str, + compiled_fn_runner, + inductor_provenance_tracking_node_mappings, + inductor_kernel_stack_trace_str, + ) + + +def fx_codegen_and_compile( + gm: GraphModule, + example_inputs: Sequence[InputType], + # This is derivable from the other inputs to this function, but we pass it + # in explicitly because it's nontrivial to compute + inputs_to_check: Sequence[int], + compile_region_name: str | None = None, + **graph_kwargs: Unpack[_CompileFxKwargs], +) -> OutputCode: + scheme: FxCompile + + if fx_compile_mode == FxCompileMode.NORMAL: + scheme = _InProcessFxCompile() + elif fx_compile_mode == FxCompileMode.SERIALIZE: + from .compile_fx_ext import _DebugSerdeFxCompile + + scheme = _DebugSerdeFxCompile() + elif fx_compile_mode == FxCompileMode.SUBPROCESS: + from .compile_fx_subproc import _SubprocessFxCompile + + scheme = _SubprocessFxCompile() + + if fx_compile_async: + from .compile_fx_async import _AsyncFxCompile + from .compile_fx_ext import _OutOfProcessFxCompile + + # pyrefly: ignore [unbound-name] + assert isinstance(scheme, _OutOfProcessFxCompile), ( + "async is only valid with an out-of-process compile mode" + ) + # pyrefly: ignore [unbound-name] + scheme = _AsyncFxCompile(scheme) + scheme._compile.compile_region_name = ( + compile_region_name # pyrefly: ignore[attr-defined] + ) + + if fx_compile_progressive: + from .compile_fx_async import _ProgressiveFxCompile + from .compile_fx_ext import _OutOfProcessFxCompile + + # pyrefly: ignore [unbound-name] + assert isinstance(scheme, _OutOfProcessFxCompile), ( + "progressive is only valid with an out-of-process compile mode" + ) + + progression_configs = _get_progression_configs() + + # Use in-process compile for the fast version + fast_scheme = _InProcessFxCompile() + fast_scheme.compile_region_name = compile_region_name + + # pyrefly: ignore [unbound-name] + scheme = _ProgressiveFxCompile(fast_scheme, scheme, progression_configs) + scheme._optimized_compile.compile_region_name = ( + compile_region_name # pyrefly: ignore[attr-defined] + ) + + scheme.compile_region_name = compile_region_name # pyrefly: ignore[unbound-name] + + # pyrefly: ignore [unbound-name] + return scheme.codegen_and_compile(gm, example_inputs, inputs_to_check, graph_kwargs) + + +def get_input_idxs_to_check( + inputs: Sequence[InputType], + static_input_idxs: Sequence[int], +) -> Sequence[int]: + """ + This function runs at compile time, and generates a list of indices for which we + might need to do a copy to preserve alignment requirements. + """ + ids_to_check = [] + + for i, input in enumerate(inputs): + if not isinstance(input, torch.Tensor): + # non-tensors don't need alignment + continue + if not is_gpu(input.device.type): + # right now we only care for gpu tensors + continue + with maybe_get_suppress_shape_guards_ctx(): + # suppress guards so that tensor_is_aligned and should_assume_input_aligned + # do not add guards on input's storage offset + if i in static_input_idxs and tensor_is_aligned(input): + continue + if not should_assume_input_aligned(input): + continue + + # if we get here, then + # (a) our triton code assumes that the input is aligned + # (b) we can't be sure ahead of time that the input will actually be aligned. + # therefore, at runtime, we'll need to check that the input is aligned + # (and if not, clone it to make it aligned.) + ids_to_check.append(i) + + return ids_to_check + + +def cudagraphify( + model: Callable[..., Any], + static_input_idxs: Sequence[int] = (), + *, + device_index: int, + stack_traces: list[str | None], + is_backward: bool, + is_inference: bool, + constants: tuple[torch.Tensor, ...] = (), + placeholders: Sequence[PlaceholderInfo] = (), + mutated_input_idxs: tuple[int, ...] = (), +) -> Callable[..., Any]: + from torch._inductor.cudagraph_trees import ( + cudagraphify_impl as new_cudagraphify_impl, + ) + + cudagraphify_fn: Callable[..., Any] + if config.triton.cudagraph_trees: + cudagraphify_fn = functools.partial( + new_cudagraphify_impl, + device_index=device_index, + stack_traces=stack_traces, + is_backward=is_backward, + is_inference=is_inference, + constants=constants, + placeholders=placeholders, + mutated_input_idxs=mutated_input_idxs, + compile_id=torch._guards.CompileContext.current_compile_id(), + ) + else: + cudagraphify_fn = cudagraphify_impl + + compiled_fn = None + + def run(new_inputs: Sequence[InputType]) -> Any: + nonlocal compiled_fn + if compiled_fn is None: + with dynamo_utils.preserve_rng_state(): + compiled_fn = cudagraphify_fn(model, new_inputs, static_input_idxs) # type: ignore[arg-type] + return compiled_fn(new_inputs) # type: ignore[arg-type] + + return run + + +def static_input(x: torch.Tensor) -> torch.Tensor: + """ + Copy and input while preserving strides + """ + return torch.empty_strided(x.size(), x.stride(), dtype=x.dtype, device=x.device) + + +def index_expanded_dims_and_copy_( + dst: torch.Tensor, + src: torch.Tensor, + expanded_dims: list[int], +) -> None: + "Index into expanded dimensions of both dst and src then copy_" + dst = index_expanded_dims(dst, expanded_dims) + src = index_expanded_dims(src, expanded_dims) + dst.copy_(src) + + +def cudagraphify_impl( + model: Callable[..., Any], + inputs: list[torch.Tensor], + static_input_idxs: Sequence[int] = (), +) -> Callable[[list[InputType]], Any]: + """ + Assumes inputs[static_input_idxs[i]] are always the same memory address + """ + check_input_idxs = get_input_idxs_to_check(inputs, static_input_idxs) # type: ignore[arg-type] + # pyrefly: ignore [annotation-mismatch, redefinition] + static_input_idxs: OrderedSet[int] = OrderedSet( + remove_unaligned_input_idxs(inputs, static_input_idxs) # type: ignore[arg-type] + ) + copy_misaligned_inputs(inputs, check_input_idxs) # type: ignore[arg-type] + + assert isinstance(inputs, list) + + inps_expanded_dims = [ + get_expanded_dims(x) if idx not in static_input_idxs else [] + for idx, x in enumerate(inputs) + ] + + # allocate static tensor inputs + static_inputs = [ + ( + x + if not isinstance(x, torch.Tensor) + else static_input(x) + if idx not in static_input_idxs + else x.detach() + ) + for idx, x in enumerate(inputs) + ] + + # copy over input values for fresh allocations + for idx, (x, expanded_dims) in enumerate(zip(inputs, inps_expanded_dims)): + if isinstance(x, torch.Tensor) and idx not in static_input_idxs: + index_expanded_dims_and_copy_(static_inputs[idx], x, expanded_dims) + + # warmup + torch.cuda.synchronize() + stream = torch.cuda.Stream() + stream.wait_stream(torch.cuda.current_stream()) + # copy static_inputs because it will be cleared in model + with torch.cuda.stream(stream): + model(list(static_inputs)) + stream.synchronize() + torch.cuda.current_stream().wait_stream(stream) + torch.cuda.synchronize() + + # record + graph = torch.cuda.CUDAGraph() + with torch.cuda.graph(graph, stream=stream, capture_error_mode="thread_local"): + static_outputs = model(list(static_inputs)) + if not isinstance(static_outputs, (list, tuple)): + static_outputs = (static_outputs,) + + if config.size_asserts: + + def run(new_inputs: list[InputType]) -> Callable[[list[InputType]], Any]: + assert len(static_inputs) == len(new_inputs) + for idx, (dst, src, expanded_dims) in enumerate( + zip(static_inputs, new_inputs, inps_expanded_dims) + ): + if not isinstance(dst, torch.Tensor): + continue + assert isinstance(src, torch.Tensor) + if idx in static_input_idxs: + assert dst.data_ptr() == src.data_ptr() + else: + # TODO - could make one single op of multiple slices + # and avoid dispatch. + # Could also pre-index the `dst` tensors + index_expanded_dims_and_copy_(dst, src, expanded_dims) + new_inputs.clear() + graph.replay() + # pyrefly: ignore [bad-return] + return static_outputs + + else: + copy_indices = [ + idx for idx in range(len(static_inputs)) if idx not in static_input_idxs + ] + + def run(new_inputs: list[InputType]) -> Callable[[list[InputType]], Any]: + for idx in copy_indices: + expanded_dims = inps_expanded_dims[idx] + src = new_inputs[idx] + assert isinstance(src, torch.Tensor) + index_expanded_dims_and_copy_(static_inputs[idx], src, expanded_dims) + new_inputs.clear() + graph.replay() + # pyrefly: ignore [bad-return] + return static_outputs + + return align_inputs_from_check_idxs(run, check_input_idxs, OrderedSet()) + + +def compile_fx_aot( + model_: GraphModule, + example_inputs_: list[InputType], + inner_compile: _CompileFxCallable = compile_fx_inner, + config_patches: dict[str, Any] | None = None, +) -> list[str | Weights] | str | GraphModule: + assert isinstance(model_, GraphModule), model_ + + # [See NOTE] Unwrapping subclasses AOT + unwrap_tensor_subclass_parameters(model_) + + # pyrefly: ignore [annotation-mismatch, redefinition] + config_patches: dict[str, Any] = copy.deepcopy(config_patches or {}) + + if not (config_patches.get("fx_wrapper", False) or config.fx_wrapper): + # If fx_wrapper is not set, then set cpp_wrapper + config_patches["cpp_wrapper"] = True + + output_path = config_patches.get( + "aot_inductor.output_path", config.aot_inductor.output_path + ) + + if output_path: + assert not output_path.endswith(".pt2"), ( + "The output path for aot_compile should not have an extension with .pt2 " + "this is for specifying the output path for the .so in AOTInductor. " + "If you would like to package the AOTInductor generated files " + "into a pt2, please call `torch._inductor.aoti_compile_and_package`." + ) + else: + config_patches = { + **config_patches, + "aot_inductor.output_path": code_hash(model_.code), + } + + from .utils import maybe_aoti_standalone_config + + config_patches = maybe_aoti_standalone_config(config_patches) + + extern_node_serializer = config_patches.pop("extern_node_serializer", None) + saved_compile_id = model_.meta.get("dynamo_compile_id", None) + saved_compile_context = torch._guards.CompileContext(saved_compile_id) + with ( + V.set_aot_compilation(True), + torch._guards.compile_context(saved_compile_context), + chromium_event_timed( + "compile_fx_aot", + log_pt2_compile_event=True, + reset_event_log_on_exit=True, + ), + get_metrics_context(), + ): + compiled_artifacts = compile_fx( + model_, + example_inputs_, + inner_compile=functools.partial( + inner_compile, + extern_node_serializer=extern_node_serializer, + ), + config_patches=config_patches, + ) + + assert isinstance(compiled_artifacts, CompiledAOTI) + + return compiled_artifacts.filename + + +_graph_counter = count(0) + + +def fw_compiler_freezing( + aot_autograd_model: GraphModule, + aot_example_inputs: Sequence[InputType], + dynamo_model: GraphModule, + num_example_inputs: int, + inner_compile: Callable[..., Any], + # TODO: Take compiler_config_extra instead + cudagraphs: BoxedBool, + graph_id: int, + forward_device: BoxedDeviceIndex, +) -> Callable[[list[object]], Sequence[torch.Tensor]]: + from torch._inductor.freezing import convert_conv_weights_to_channels_last, freeze + + # partition_fn won't be called + inputs_devices = get_inputs_devices(aot_example_inputs, aot_autograd_model) + aot_autograd_model = _recursive_joint_graph_passes( + aot_autograd_model, + input_device=next(iter(inputs_devices)), + ) + + layout_opt = GraphLowering.decide_layout_opt(aot_autograd_model, is_inference=True) + if layout_opt: + # make sure meta['val'] is properly setup + fake_tensor_prop(aot_autograd_model, aot_example_inputs, True) + convert_conv_weights_to_channels_last(aot_autograd_model) + + opt_model, preserved_arg_indices = freeze( + dynamo_model, + aot_autograd_model, + aot_example_inputs, # type: ignore[arg-type] + ) + + aot_example_inputs = [aot_example_inputs[ind] for ind in preserved_arg_indices] + + fake_mode = detect_fake_mode(aot_example_inputs) + + # for freezing, all graph outputs should be user visible + *_, model_outputs_node = opt_model.graph.nodes + model_outputs = model_outputs_node.args[0] + model_outputs_node.meta["user_visible_output_idxs"] = [ + idx for idx, n in enumerate(model_outputs) if isinstance(n, torch.fx.Node) + ] + + static_input_idxs: list[Any] = [] + # constant params will be real tensors, not fake + tracing_context = torch._guards.TracingContext.try_get() + unwrapped_args_offsets = [0] + max_offset_idx = 0 + if tracing_context is not None: + assert tracing_context.params_flat_unwrap_subclasses is not None + params_flat_unwrap = tracing_context.params_flat_unwrap_subclasses + max_offset_idx = max(0, len(params_flat_unwrap) - 1) + preserved_indices_params_flat = OrderedSet[int]() + unwrapped_idxs = tracing_context.params_unwrapped_to_flat_index + assert unwrapped_idxs is not None + current_offset = 0 + if len(params_flat_unwrap) > 0: + unwrapped_args_offsets = [] + + for i in range(len(params_flat_unwrap)): + if i not in preserved_arg_indices: + params_flat_unwrap[i] = None + if i > 0 and unwrapped_idxs[i] == unwrapped_idxs[i - 1]: + current_offset += 1 + else: + preserved_indices_params_flat.add(unwrapped_idxs[i]) + unwrapped_args_offsets.append(current_offset) + + # Deallocate wrapped params, if all subelements were deallocated + assert tracing_context.params_flat is not None + for i in range(len(tracing_context.params_flat)): + if i not in preserved_indices_params_flat: + tracing_context.params_flat[i] = None + + if tracing_context.fw_metadata: + static_input_idxs = tracing_context.fw_metadata.static_input_indices + + with mock.patch.object(fake_mode, "allow_non_fake_inputs", True): + optimized_function = inner_compile( + opt_model, + aot_example_inputs, + static_input_idxs=static_input_idxs, + cudagraphs=cudagraphs, + graph_id=graph_id, + is_inference=True, + boxed_forward_device_index=forward_device, + layout_opt=layout_opt, + ) + + # aot_inductor codegens a call that takes in just the inputs, so we don't return a wrapper + # that drops constant-ified params + if V.aot_compilation: + return optimized_function + + def wrapper(args: list[object]) -> Sequence[torch.Tensor]: + args_new = [ + args[i - unwrapped_args_offsets[min(i, max_offset_idx)]] + for i in preserved_arg_indices + ] + args.clear() + return optimized_function(args_new) + + wrapper._boxed_call = True # type: ignore[attr-defined] + + return wrapper + + +def get_cpp_wrapper_config() -> dict[str, object]: + if config.triton.cudagraphs and config.graph_partition: + log_cudagraph_skip_and_bump_counter( + format_default_skip_message( + "cpp-wrapper does not support graph partition yet" + ) + ) + + autotune_at_compile_time = ( + config.triton.autotune_at_compile_time + if config.triton.autotune_at_compile_time is not None + # Default to True for AOTI. Subject to change in future. + else has_triton() and V.aot_compilation + ) + return { + "triton.autotune_at_compile_time": autotune_at_compile_time, + "triton.autotune_cublasLt": not autotune_at_compile_time, + "triton.cudagraphs": ( + config.triton.cudagraphs + and not V.aot_compilation + and not config.graph_partition + ), + "triton.store_cubin": True, + } + + +def get_cuda_device_context(gm: torch.fx.GraphModule) -> AbstractContextManager[None]: + """ + Returns a cuda device context manager if there is a single device in the graph + """ + if not torch.cuda.is_available(): + return contextlib.nullcontext() + + cuda_devices: OrderedSet[torch.device] = OrderedSet( + device for device in get_all_devices(gm) if device.type == "cuda" + ) + + return ( + torch.cuda.device(next(iter(cuda_devices))) # type: ignore[return-value] + if len(cuda_devices) == 1 + else contextlib.nullcontext() + ) + + +def partition_fn( + gm: GraphModule, + joint_inputs: Sequence[object], + **kwargs: object, +) -> tuple[GraphModule, GraphModule]: + cuda_context = get_cuda_device_context(gm) + with cuda_context: + # We can skip the invoke_subgraph because the + # entire_partition_fn is called recursively for invoke_subgraph + # in partitioning. + inputs_devices = get_inputs_devices(joint_inputs, gm) + gm = _recursive_joint_graph_passes( + gm, + skip_invoke_subgraph=True, + input_device=next(iter(inputs_devices)), + ) + + static_lifetime_input_indices: list[int] | None = kwargs.pop( # type: ignore[assignment] + "static_lifetime_input_indices", None + ) + + if config.custom_partitioner_fn is None: + with dynamo_utils.dynamo_timed( + "min_cut_rematerialization_partition", log_pt2_compile_event=True + ): + return min_cut_rematerialization_partition( + gm, + joint_inputs, + compiler="inductor", + static_lifetime_input_indices=static_lifetime_input_indices, + # pyrefly: ignore[bad-argument-type] + **kwargs, + ) + else: + assert isinstance(config.custom_partitioner_fn, CustomPartitionerFn) + with dynamo_utils.dynamo_timed( + config.custom_partitioner_fn.__class__.__name__, + log_pt2_compile_event=True, + ): + return config.custom_partitioner_fn( + gm, + joint_inputs, + compiler="inductor", + static_lifetime_input_indices=static_lifetime_input_indices, + **kwargs, + ) + + +def get_num_model_outputs(model: GraphModule) -> int: + model_outputs_node = output_node(model) + model_outputs = pytree.arg_tree_leaves(*model_outputs_node.args) + return len(model_outputs) + + +def cudagraph_annotation_context( + cudagraphs: BoxedBool, +) -> contextlib.AbstractContextManager[None]: + # When an annotation force-enables cudagraphs but the global config has them + # off, patch config.triton.cudagraphs for the duration of compilation, + # so existing codepaths that access config.triton.cudagraphs work + if cudagraphs.value and not config.triton.cudagraphs: + return config.patch({"triton.cudagraphs": True}) + return contextlib.nullcontext() + + +@dataclass(frozen=True) +class CompilerConfigExtra: + cudagraphs: BoxedBool + graph_id: int + forward_device: BoxedDeviceIndex + forward_is_partitioned: BoxedBool + cudagraphs_bwd_override: bool | None = None + + +def create_compiler_config_extra( + gm: GraphModule | GmWrapper, +) -> CompilerConfigExtra: + gm_meta = gm.meta if isinstance(gm, GraphModule) else None + + # Although cudagraphs may have been enabled via config, various + # conditions (which are tested within the bowels of Inductor) may + # force cudagraphs to be disabled. This mutable box lets us retrieve + # the final determination if cudagraphs actually can be used or not. + cudagraphs = BoxedBool(config.triton.cudagraphs) + + cudagraphs_bwd_override: bool | None = None + + # Override cudagraphs BoxedBool based on override_cudagraphs annotation. + # Disabling fwd disables bwd (copying activations isn't profitable), + # so cudagraphs_bwd_override is only needed for fwd=True / bwd=False. + if ( + gm_meta is not None + and (annotation := gm_meta.get("cudagraph_annotation")) is not None + ): + if annotation.fwd is not None and annotation.fwd != config.triton.cudagraphs: + cudagraphs = BoxedBool(annotation.fwd) + if annotation.fwd: + cudagraphs_log.info( + "enabling cudagraphs due to override_cudagraphs annotation" + ) + else: + log_cudagraph_skip_and_bump_counter( + "disabling cudagraphs due to override_cudagraphs annotation" + ) + + # bwd override only matters when fwd enables cudagraphs but bwd + # explicitly disables them. + if cudagraphs.value and annotation.bwd is not None and not annotation.bwd: + cudagraphs_bwd_override = annotation.bwd + log_cudagraph_skip_and_bump_counter( + "disabling cudagraphs for backward due to override_cudagraphs annotation" + ) + + # TODO: The modern style is to use CompileId from TracingContext to + # identify Inductor compilation. However, this CompileId cannot + # uniquely identify multiple Inductor compilations that arise from + # DDPOptimizer + graph_id = next(_graph_counter) + + # See [Backward Generation Handling] + forward_device = BoxedDeviceIndex(None) + + # Set by the forward compilation when it is partitioned for CUDA graphs. + # The backward reads this to decide whether saved tensors can be assumed + # to have fixed addresses. + forward_is_partitioned = BoxedBool(False) + + return CompilerConfigExtra( + cudagraphs=cudagraphs, + graph_id=graph_id, + forward_device=forward_device, + cudagraphs_bwd_override=cudagraphs_bwd_override, + forward_is_partitioned=forward_is_partitioned, + ) + + +def compile_fx_forward( + gm: GraphModule, + example_inputs: Sequence[InputType], + num_orig_model_outputs: int, + num_example_inputs: int, + compiler_config_extra: CompilerConfigExtra, + inner_compile: Callable[..., OutputCode] = compile_fx_inner, + is_inference: bool = False, +) -> OutputCode: + """ + Compile the forward graph of the given graph module. + + Args: + gm: The graph module to compile. + example_inputs: The example inputs to use for compilation. + num_orig_model_outputs: The number of model outputs from the original dynamo graph. + num_example_inputs: The number of example inputs from the original dynamo graph. + compiler_config_extra: Extra configuration for the compiler. + inner_compile: The inner compile function to use. + is_inference: Whether this is an inference graph. + """ + + if is_inference: + # partition_fn won't be called + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "before_joint_graph", + "encoding": "string", + }, + payload_fn=lambda: gm.print_readable( + print_output=False, include_stride=True, include_device=True + ), + ) + + # Snapshot stack traces on the output node before passes run, + # as later passes may strip stack_trace from individual nodes. + output = output_node(gm) + output.meta["output_stack_traces"] = [ + ( + arg.meta.get("stack_trace") + if isinstance(arg, torch.fx.node.Node) + else None + ) + for arg in output.args[0] # type: ignore[union-attr] + ] + + inputs_devices = get_inputs_devices(example_inputs, gm) + gm = _recursive_joint_graph_passes(gm, input_device=next(iter(inputs_devices))) + + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "after_joint_graph", + "encoding": "string", + }, + payload_fn=lambda: gm.print_readable( + print_output=False, include_stride=True, include_device=True + ), + ) + + fixed = torch._inductor.utils.num_fw_fixed_arguments( + num_example_inputs, len(example_inputs) + ) + + model_outputs_node = output_node(gm) + if config.keep_output_stride: + model_outputs = pytree.arg_tree_leaves(*model_outputs_node.args) + num_model_outputs = len(model_outputs) + + context = torch._guards.TracingContext.try_get() + # See Note [User Outputs in the inductor graph] + if context is not None and context.fw_metadata and not is_inference: + original_output_start_index = ( + context.fw_metadata.num_mutated_inp_runtime_indices + ) + else: + original_output_start_index = 0 + + assert num_orig_model_outputs <= num_model_outputs + + # Note [User Outputs in the inductor graph] + # We makes the following assumption + # For inference + # len(orig_model_outputs) == len(model_outputs) + # For training + # len(orig_model_outputs) <= len(model_outputs) + # During training, most of the time the model_outputs starts with + # original module's outputs followed by saved activations. + # But this can be not true if the model have inplace updated tensors. + # AOTAutograd will make those tensors being returned before the original + # module's output. + # To make things safe, we'll use original_output_start_index field + # set by AOTAutograd to decide where the original module outputs start. + orig_output_end_idx = original_output_start_index + num_orig_model_outputs + # Sanity check: we are about to splice out the "user" outputs from the full set + # of "graph" outputs. Make sure we're within bounds. + assert orig_output_end_idx <= num_model_outputs + + model_outputs_node.meta["user_visible_output_idxs"] = [ + idx + for idx in range(original_output_start_index, orig_output_end_idx) + if isinstance(model_outputs[idx], torch.fx.Node) + ] + else: + model_outputs_node.meta["user_visible_output_idxs"] = [] + + # We also mark the invoke_subgraph outputs as user_visible to + # force the outputs of invoke_subgraph subgraph to follow the + # original strides + _recursive_record_user_visible_output_idxs(gm) + + with cudagraph_annotation_context(compiler_config_extra.cudagraphs): + result = inner_compile( + gm, + example_inputs, + static_input_idxs=get_static_input_idxs(fixed), + cudagraphs=compiler_config_extra.cudagraphs, + graph_id=compiler_config_extra.graph_id, + is_inference=is_inference, + boxed_forward_device_index=compiler_config_extra.forward_device, + ) + + if ( + not is_inference + and isinstance(result, CompiledFxGraph) + and result.partition_maps + and len(result.partition_maps) > 1 + ): + compiler_config_extra.forward_is_partitioned.value = True + + return result + + +def compile_fx_backward( + gm: GraphModule, + example_inputs: Sequence[InputType], + compiler_config_extra: CompilerConfigExtra, + inner_compile: Callable[..., OutputCode] = compile_fx_inner, +) -> OutputCode: + """ + Compile the backward graph of the given graph module. + + Args: + gm: The graph module to compile. + example_inputs: The example inputs to use for compilation. + compiler_config_extra: Extra configuration for the compiler. + inner_compile: The inner compile function to use. + """ + from torch._dynamo.convert_frame import compile_lock + + with compile_lock: + model_outputs_node = output_node(gm) + if config.bw_outputs_user_visible: + model_outputs = pytree.arg_tree_leaves(*model_outputs_node.args) + model_outputs_node.meta["user_visible_output_idxs"] = [ + idx + for idx, n in enumerate(model_outputs) + if isinstance(n, torch.fx.Node) + ] + else: + model_outputs_node.meta["user_visible_output_idxs"] = [] + + fixed = count_tangents(gm) + + # Check if cudagraphs should be overridden for backward via annotation + cudagraphs = compiler_config_extra.cudagraphs + if compiler_config_extra.cudagraphs_bwd_override is not None: + cudagraphs = BoxedBool(compiler_config_extra.cudagraphs_bwd_override) + + # When the forward was partitioned, saved activations from inline + # code between partitions are NOT at fixed addresses. Only mark + # primals (params/buffers) as static. + if compiler_config_extra.forward_is_partitioned.value: + static_input_idxs: Sequence[int] = get_static_bw_input_idxs(gm) + else: + static_input_idxs = list(range(fixed)) + with ( + ( + config.patch(get_cpp_wrapper_config()) + if config.cpp_wrapper + else contextlib.nullcontext() + ), + cudagraph_annotation_context(cudagraphs), + ): + return inner_compile( + gm, + example_inputs, + static_input_idxs=static_input_idxs, + cudagraphs=cudagraphs, + is_backward=True, + graph_id=compiler_config_extra.graph_id, + boxed_forward_device_index=compiler_config_extra.forward_device, + ) + + +def run_pre_grad_passes( + model_: GraphModule, example_inputs_: Sequence[InputType] +) -> GraphModule: + # "before_pre_grad_graph" is used in inductor provenance + # tracking highlighter front-end. + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "before_pre_grad_graph", + "encoding": "string", + }, + payload_fn=lambda: model_.print_readable( + print_output=False, include_stride=True, include_device=True + ) + + f"\n\n # graph id: {id(model_.graph)}", + ) + pre_grad_graphs_log.debug( + "%s", + lazy_format_graph_code( + "BEFORE PRE GRAD", + model_, + include_stride=True, + include_device=True, + colored=True, + ), + ) + torch._inductor.debug._pre_grad_graph_id = id(model_.graph) + + if config.trace.provenance_tracking_level == 1: + for node in model_.graph.nodes: + if node.stack_trace: + torch._inductor.debug._inductor_pre_grad_node_stack_trace[node.name] = ( + node.stack_trace + ) + + model_ = _recursive_pre_grad_passes(model_, example_inputs_) + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "after_pre_grad_graph", + "encoding": "string", + }, + payload_fn=lambda: model_.print_readable( + print_output=False, include_stride=True, include_device=True + ) + + f"\n\n # graph id: {id(model_.graph)}", + ) + return model_ + + +def compile_fx( + model_: GraphModule, + example_inputs_: Sequence[InputType], + inner_compile: Callable[..., OutputCode] = compile_fx_inner, + config_patches: dict[str, Any] | None = None, + decompositions: dict[OpOverload, Callable[..., Any]] | None = None, + ignore_shape_env: bool = False, + compile_region_name: str | None = None, +) -> CompileFxOutput: + """ + Main entry point for compiling given FX graph. Despite the fact that this + lives in :mod:`torch._inductor`, this function is responsible for calling + into AOT Autograd (and we will eventually get a callback to + ``inner_compile`` to perform actual compilation. In other words, this + function orchestrates end-to-end compilation for the inductor backend when + you use :func:`torch.compile`. + + NB: This function TAKES OWNERSHIP of the input ``model_`` and can potentially + mutate it! Make a copy if you need to preserve the original GraphModule. + """ + if decompositions is not None: + + def get_decomp_fn() -> dict[Any, Callable[..., Any]]: + return decompositions # pyrefly: ignore[bad-return] + else: + get_decomp_fn = select_decomp_table + + # Some arguments trigger a recursive call to compile_fx. Handle these + # short circuits first, before anything else + + from torch._inductor.compiler_bisector import CompilerBisector + + if CompilerBisector.disable_subsystem("inductor", "pre_grad_graph"): + # pyrefly: ignore [bad-return] + return model_ + + if config_patches: + with config.patch(config_patches): + return compile_fx( + model_, + example_inputs_, + # need extra layer of patching as backwards is compiled out of scope + inner_compile=config.patch(config_patches)(inner_compile), + decompositions=decompositions, + ignore_shape_env=ignore_shape_env, + compile_region_name=compile_region_name, + ) + + # Keep region names out of graph_kwargs so they don't perturb FX cache keys. + inner_compile = functools.partial( + inner_compile, + compile_region_name=compile_region_name, + ) + + # Wake up the AsyncCompile subproc pool as early as possible (if there's cuda). + if any( + isinstance(e, torch.Tensor) and e.device.type in ("cuda", "xpu") + for e in example_inputs_ + ): + torch._inductor.async_compile.AsyncCompile.wakeup() + + if config.cpp_wrapper or config.fx_wrapper: + from torch._export.non_strict_utils import _fakify_script_objects + + cpp_wrapper_config = config.cpp_wrapper + fx_wrapper_config = config.fx_wrapper + + with ( + config.patch(get_cpp_wrapper_config()), + V.set_real_inputs(example_inputs_), + ): + inputs_: Sequence[InputType] = ( + _extract_inputs_from_exported_gm(model_, example_inputs_) + if isinstance(model_, GraphModule) + else example_inputs_ + ) + fake_mode = detect_fake_mode(inputs_) + with _fakify_script_objects(model_, inputs_, {}, fake_mode) as ( + patched_mod, + fake_args, + _, + _, + _, + ): + return _maybe_wrap_and_compile_fx_main( + patched_mod, + fake_args, + inner_compile=functools.partial( + inner_compile, + cpp_wrapper=cpp_wrapper_config, + fx_wrapper=fx_wrapper_config, + ), + ignore_shape_env=ignore_shape_env, + get_decomp_fn=get_decomp_fn, + compile_region_name=compile_region_name, + ) + + return _maybe_wrap_and_compile_fx_main( + model_, + example_inputs_, + inner_compile, + ignore_shape_env, + get_decomp_fn=get_decomp_fn, + compile_region_name=compile_region_name, + ) + + +def _extract_inputs_from_exported_gm( + gm: GraphModule, example_inputs_: Sequence[InputType] +) -> Sequence[InputType]: + fake_inputs = [ + node.meta.get("val") for node in gm.graph.nodes if node.op == "placeholder" + ] + + if not config.fx_wrapper: + # Replace non-tensor inputs with Nones + # constant scalars embedded in the graph + # symbolic scalars (symint) are not supported in non-fx_wrapper mode + fake_inputs = [ + inp if isinstance(inp, torch.Tensor) else None for inp in fake_inputs + ] + + if any(v is not None for v in fake_inputs): + # Validate devices before switching to fake tensors. + for idx, fi, i in zip(count(), fake_inputs, example_inputs_): + if fi is not None and isinstance(fi, torch.Tensor): + assert isinstance(i, torch.Tensor) + if fi.device != i.device: + raise ValueError( + f"Device mismatch between fake input and example input at position #{idx}: " + f"{fi.device} vs {i.device}. If the model was exported via torch.export(), " + "make sure torch.export() and torch.aot_compile() run on the same device." + ) + return fake_inputs + + return example_inputs_ + + +def _maybe_wrap_and_compile_fx_main( + model_: GraphModule, + example_inputs_: Sequence[InputType], + inner_compile: Callable[..., OutputCode], + ignore_shape_env: bool, + *, + get_decomp_fn: Callable[..., dict[Any, Callable[..., Any]]] = select_decomp_table, + compile_region_name: str | None = None, +) -> CompileFxOutput: + """ + Part of compile_fx, called after patching configs. + + Ultimately we want to call _compile_fx_main, where the actual work happens. + But under various conditions, various forms of wrapping might be needed + around _compile_fx_main. + """ + # Each wrapper below takes a self-contained compile_gm function which is + # called inside the wrapper. This just recursively calls this function. + compile_gm = functools.partial( + _maybe_wrap_and_compile_fx_main, + inner_compile=inner_compile, + ignore_shape_env=ignore_shape_env, + get_decomp_fn=get_decomp_fn, + compile_region_name=compile_region_name, + ) + if not graph_returns_tuple(model_): + return make_graph_return_tuple(model_, example_inputs_, compile_gm) + + if isinstance(model_, GraphModule) and isinstance( + model_.graph._codegen, _PyTreeCodeGen + ): + # this graph is the result of dynamo.export() + return handle_dynamo_export_graph(model_, example_inputs_, compile_gm) + + if any(isinstance(x, (list, tuple, dict)) for x in example_inputs_): + # NB: this short circuit never occurs for Dynamo produced graphs + # (which are pre-flattened) + return flatten_graph_inputs(model_, example_inputs_, compile_gm) + + # Finally do the actual work! + return _compile_fx_main( + model_, + example_inputs_, + inner_compile, + ignore_shape_env, + get_decomp_fn=get_decomp_fn, + compile_region_name=compile_region_name, + ) + + +def _compile_fx_main( + model_: GraphModule, + example_inputs_: Sequence[InputType], + inner_compile: Callable[..., OutputCode], + ignore_shape_env: bool, + *, + get_decomp_fn: Callable[..., dict[Any, Callable[..., Any]]] = select_decomp_table, + compile_region_name: str | None = None, +) -> CompileFxOutput: + """ + Main part of compile_fx, called after wrapping is done. + + Roughly speaking, here the steps will be: + (1) apply pre-grad passes + (2) create `fw_compiler` and `bw_compiler` functions out of `inner_compile` + (3) call aot_autograd, which: + - (3a) creates a joint graph with `decompositions`, + - (3b) partitions it with `partition_fn` into fw and bw graphs (applying joint-graph passes), + - (3c) calls `fw_compiler` and `bw_compiler` on those graphs (applying post-grad passes) + - (3d) finally, assembles the fw and bw compiled functions back together and returns. + """ + with ( + _use_lazy_graph_module(dynamo_config.use_lazy_graph_module), + enable_python_dispatcher(), + torch.fx.traceback.preserve_node_meta( + config.trace.provenance_tracking_level == 1 + ), + torch._inductor.debug.reset_provenance_globals(), + ): + # Note: Pre-grad passes are now run inside aot_module_simplified (via the + # pre_grad_passes callback) after the cache lookup. + + assert not config._raise_error_for_testing + + num_example_inputs = len(example_inputs_) + + compiler_config_extra = create_compiler_config_extra(model_) + + decompositions = get_decomp_fn() + inner_compile = functools.partial(inner_compile, get_decomp_fn=get_decomp_fn) + + def fw_compiler_base( + gm: GraphModule, + example_inputs: Sequence[InputType], + is_inference: bool, + ) -> OutputCode: + with dynamo_utils.dynamo_timed("compile_fx..fw_compiler_base"): + if isinstance(model_, GraphModule): + num_orig_model_outputs = get_num_model_outputs(model_) + else: + num_orig_model_outputs = get_num_model_outputs(gm) + return compile_fx_forward( + gm, + example_inputs, + num_orig_model_outputs=num_orig_model_outputs, + num_example_inputs=num_example_inputs, + compiler_config_extra=compiler_config_extra, + inner_compile=inner_compile, + is_inference=is_inference, + ) + + fw_compiler: Callable[[GraphModule, Sequence[InputType]], OutputCode] = ( + functools.partial(fw_compiler_base, is_inference=False) + ) + fw_compiler = SerializableAOTDispatchCompiler(OutputCode, fw_compiler) + + if config.freezing and not torch.is_grad_enabled(): + inference_compiler: Callable[..., Any] = functools.partial( + fw_compiler_freezing, + dynamo_model=model_, + num_example_inputs=num_example_inputs, + inner_compile=inner_compile, + cudagraphs=compiler_config_extra.cudagraphs, + graph_id=compiler_config_extra.graph_id, + forward_device=compiler_config_extra.forward_device, + ) + else: + inference_compiler = functools.partial(fw_compiler_base, is_inference=True) + inference_compiler = SerializableAOTDispatchCompiler( + OutputCode, inference_compiler + ) + + @compile_time_strobelight_meta(phase_name="backward") + def bw_compiler( + gm: GraphModule, example_inputs: Sequence[InputType] + ) -> OutputCode: + with ( + dynamo_utils.dynamo_timed("compile_fx..bw_compiler"), + ): + return compile_fx_backward( + gm, + example_inputs, + compiler_config_extra=compiler_config_extra, + inner_compile=inner_compile, + ) + + bw_compiler = SerializableAOTDispatchCompiler(OutputCode, bw_compiler) + + fake_mode = detect_fake_mode( + example_inputs_ + ) or torch._subclasses.FakeTensorMode(allow_non_fake_inputs=True) + tracing_context = ( + torch._guards.TracingContext.try_get() + or torch._guards.TracingContext(fake_mode) + ) + + if V.aot_compilation and not config.enable_autograd_for_aot: + from .utils import is_valid_aoti_model_name + + is_valid_aoti_model_name() + + # Pre-grad passes for the aot_autograd path are run inside + # aot_module_simplified after the cache lookup. The + # aot_export_module path doesn't use that cache so run them here. + if isinstance(model_, GraphModule): + model_ = run_pre_grad_passes(model_, example_inputs_) + + with functorch_config.patch( + unlift_effect_tokens=True, + selective_decompose=config.selective_decompose, + ): + gm, graph_signature = aot_export_module( + model_, + example_inputs_, + trace_joint=False, + decompositions=decompositions, + ) + assert isinstance(gm, GraphModule) + from torch._export.utils import _detect_fake_mode_from_gm + + fake_mode = _detect_fake_mode_from_gm(gm) # type: ignore[assignment] + # aot_export_module doesn't account for constant tensor attributes + # so we end up having tensors that don't have fake vals attached. + # This can happen when upstream export is non-strict where we + # preserve the original module params/buffers. Once AOTI switches + # to ep.run_decompositions() flow to lower to post-autograd opset + # this will go away. + for node in gm.graph.nodes: + if node.op == "get_attr" and "val" not in node.meta: + target = attrgetter(node.target)(gm) + if isinstance(target, torch.Tensor): + assert fake_mode is not None + node.meta["val"] = fake_mode.from_tensor( + target, static_shapes=True + ) + elif isinstance(target, torch.ScriptObject) or is_opaque_type( + type(target) + ): + node.meta["val"] = ( + torch._library.fake_class_registry.maybe_to_fake_obj( + fake_mode, target + ) + ) + elif isinstance(target, FakeScriptObject): + node.meta["val"] = target + + unlifted_gm = _unlift_graph(model_, gm, graph_signature) + if "dynamo_flat_name_to_original_fqn" in model_.meta: + unlifted_gm.meta["dynamo_flat_name_to_original_fqn"] = model_.meta[ + "dynamo_flat_name_to_original_fqn" + ] + + if "dynamo_compile_id" in model_.meta: + unlifted_gm.meta["dynamo_compile_id"] = model_.meta["dynamo_compile_id"] + + # Disable amp as in aot_dispatch_autograd (https://github.com/pytorch/pytorch/pull/86515) + # In inference_compiler (fw_compiler_base), _recursive_joint_graph_passes will call into + # _sfdp_init() to register patterns. + # When fallback_random is set to True, the sdpa patterns will be traced during runtime. + # If amp is turned on, the traced FP32 patterns will have prims.convert_element_type which + # will be the same as the generated FP16 patterns. + disable_amp = torch._C._is_any_autocast_enabled() + context = ( + torch._C._DisableAutocast if disable_amp else contextlib.nullcontext + ) + with V.set_fake_mode(fake_mode), compiled_autograd._disable(), context(): + return inference_compiler(unlifted_gm, example_inputs_) + + with ( + V.set_fake_mode(fake_mode), + torch._guards.tracing(tracing_context), + compiled_autograd._disable(), + functorch_config.patch( + unlift_effect_tokens=True, + selective_decompose=config.selective_decompose, + ), + ): + try: + return dynamo_common.aot_autograd( + fw_compiler=fw_compiler, + bw_compiler=bw_compiler, + inference_compiler=inference_compiler, + decompositions=decompositions, + partition_fn=partition_fn, + keep_inference_input_mutations=True, + compiler_config_extra=compiler_config_extra, + ignore_shape_env=ignore_shape_env, + pre_grad_passes=run_pre_grad_passes, + compile_region_name=compile_region_name, + )(model_, example_inputs_) + except ShortenTraceback as e: + # We will also shorten the traceback inside dynamo. + # This is only useful if inductor is called directly with an FX graph. + raise e.remove_dynamo_frames() from None # see TORCHDYNAMO_VERBOSE=1 + + +def graph_returns_tuple(gm: GraphModule) -> bool: + """True if a FX graph returns a tuple""" + if not isinstance(gm, GraphModule): + return True # can't check this, assume true + (rv,) = output_node(gm).args + if isinstance(rv, (list, tuple)): + return True + if ( + isinstance(rv, torch.fx.node.Node) + and hasattr(rv.target, "_schema") + and len(rv.target._schema.returns) > 1 + and all(str(ret.type) == "Tensor" for ret in rv.target._schema.returns) + ): + # for graphs whose result is one node with multiple outputs + return True + return False + + +def make_graph_return_tuple( + gm: GraphModule, + inputs: Sequence[InputType], + compile_gm: Callable[..., Any], +) -> Callable[..., Any]: + """ + Mutate gm so it returns a tuple. This is only needed for graphs + not created by torchdynamo that return non-tuples. + """ + node = output_node(gm) + (rv,) = node.args + rv, spec = pytree.tree_flatten(rv) + with gm.graph.inserting_before(node): + gm.graph.output(rv) + gm.graph.erase_node(node) + assert graph_returns_tuple(gm) + + compiled_fn = compile_gm(gm, inputs) + + @functools.wraps(compiled_fn) + def wrapper(*args: Any, **kwargs: Any) -> Any: + return pytree.tree_unflatten(compiled_fn(*args, **kwargs), spec) + + return wrapper + + +def handle_dynamo_export_graph( + gm: GraphModule, + inputs: Sequence[InputType], + compile_gm: Callable[..., Any], +) -> Callable[..., Any]: + """ + `torch._dynamo.export` embeds pytrees in the FX graph codegen object, + convert that to a normal FX graph so inductor can compile it. + """ + codegen = gm.graph._codegen + gm.graph._codegen = torch.fx.graph.CodeGen() + gm.recompile() + + compiled_fn = compile_gm(gm, codegen.process_inputs(*inputs)) + + @functools.wraps(compiled_fn) # type: ignore[misc] + def wrapper(*args: Any) -> Any: + return codegen.process_outputs(compiled_fn(*codegen.process_inputs(*args))) + + return wrapper + + +def _check_triton_bf16_support(graph: GraphLowering) -> None: + def warn_and_skip(device: torch.device | None) -> Never: + from torch._dynamo.exc import SkipFrame + + assert device is not None + + device_interface = get_interface_for_device(device.type) + device_props = device_interface.get_device_properties(device) + warnings.warn( + f"{device_props.name} does not support bfloat16 compilation natively, skipping" + ) + raise SkipFrame("BF16 is not supported") + + for node in itertools.chain(graph.graph_inputs.values(), graph.graph_outputs): + if not isinstance(node, IRNode): + continue + device_type = get_device_type(node) + if ( + not device_type + or not is_gpu(device_type) + or node.get_dtype() != torch.bfloat16 + ): + continue + # Print warning and skip frame if attempting to compile for bfloat16 + # on device without hardware support for dtype + device_interface = get_interface_for_device(device_type) + if device_interface.is_bf16_supported(including_emulation=False): + return + warn_and_skip(node.get_device()) + + +def _aoti_flatten_inputs( + gm: torch.fx.GraphModule, + args: list[Any] | tuple[Any, ...], + kwargs: dict[str, Any] | None = None, + *, + options: dict[str, Any] | None = None, +) -> tuple[list[Any], dict[str, Any]]: + """ + Flatten the inputs to the graph module and return the flat inputs and options. + Add "aot_inductor.serialized_in_spec" and "aot_inductor.serialized_out_spec" to the options. + """ + # pyrefly: ignore [missing-module-attribute] + from .compile_fx import graph_returns_tuple + + assert graph_returns_tuple(gm), ( + "Graph output must be a tuple(). This is so that we can avoid " + "pytree processing of the outputs. Please change the module to " + "have tuple outputs." + ) + + # We will serialize the pytree info into the .so as constant strings + in_spec = None + out_spec = None + if isinstance(gm.graph._codegen, torch.fx.graph._PyTreeCodeGen): + codegen = gm.graph._codegen + gm.graph._codegen = torch.fx.graph.CodeGen() + gm.recompile() + + if codegen.pytree_info.in_spec is not None: + in_spec = codegen.pytree_info.in_spec + if codegen.pytree_info.out_spec is not None: + out_spec = codegen.pytree_info.out_spec + + else: + if hasattr(gm, "_in_spec"): + in_spec = gm._in_spec + if hasattr(gm, "_out_spec"): + out_spec = gm._out_spec + + serialized_in_spec = pytree.treespec_dumps(in_spec) if in_spec is not None else "" + serialized_out_spec = ( + pytree.treespec_dumps(out_spec) if out_spec is not None else "" + ) + + flat_args_with_path, received_spec = pytree.tree_flatten_with_path( + (args, kwargs or {}) + ) + + if any(isinstance(x[1], torch.ScriptObject) for x in flat_args_with_path): + from torch._dynamo.exc import UserError, UserErrorType + + raise UserError( + UserErrorType.INVALID_INPUT, + "TorchBind objects found in inputs. TorchBind object inputs are not supported in AOTInductor. " + "TorchBind objects can only be attributes.", + ) + + # Replace non-tensor (constant) inputs with Nones, since these are not being + # used anyways by the graph + flat_example_inputs = [ + x[1] if isinstance(x[1], torch.Tensor) else None for x in flat_args_with_path + ] + + if in_spec is not None and received_spec != in_spec: + raise ValueError( # noqa: B904 + "Trying to flatten user inputs with exported input tree spec: \n" + f"{in_spec}\n" + "but actually got inputs with tree spec of: \n" + f"{received_spec}" + ) + + options = ( + { + "aot_inductor.serialized_in_spec": serialized_in_spec, + "aot_inductor.serialized_out_spec": serialized_out_spec, + } + if options is None + else { + **options, + "aot_inductor.serialized_in_spec": serialized_in_spec, + "aot_inductor.serialized_out_spec": serialized_out_spec, + } + ) + return flat_example_inputs, options + + +def autograd_cache_key( + graph, + example_inputs, + ignore_shape_env: bool, + decompositions=None, +): + if config.cpp_wrapper or config.fx_wrapper: + raise RuntimeError( + "autograd_cache_key is not supported with cpp_wrapper or fx_wrapper" + ) + + decompositions = ( + decompositions if decompositions is not None else select_decomp_table() + ) + # compile_fx applies these graph transforms before reaching _compile_fx_main. + # Neither occurs on the torch.compile/Dynamo path (which always produces + # tuple-returning, pre-flattened graphs). Not supported by this API. + if isinstance(graph, GraphModule) and not graph_returns_tuple(graph): + raise NotImplementedError( + "autograd_cache_key does not support graphs that don't return a tuple" + ) + if any(isinstance(x, (list, tuple, dict)) for x in example_inputs): + raise NotImplementedError( + "autograd_cache_key does not support nested container inputs" + ) + + compiler_config_extra = create_compiler_config_extra(graph) + + # These context managers replicate the ones that _compile_fx_main sets up + # before calling aot_autograd, so that the config snapshot captured by + # autograd_cache_key is identical to a real compile_fx run: + # _compile_fx_main outer with-block: _use_lazy_graph_module, + # enable_python_dispatcher, preserve_node_meta, + # reset_provenance_globals + # _compile_fx_main aot_autograd with-block: V.set_fake_mode, + # torch._guards.tracing, compiled_autograd._disable, + # functorch_config.patch + + fake_mode = detect_fake_mode(example_inputs) or torch._subclasses.FakeTensorMode( + allow_non_fake_inputs=True + ) + tracing_context = ( + torch._guards.TracingContext.try_get() + or torch._guards.TracingContext(fake_mode) + ) + + with ( + functorch_config.patch( + unlift_effect_tokens=True, selective_decompose=config.selective_decompose + ), + _use_lazy_graph_module(dynamo_config.use_lazy_graph_module), + enable_python_dispatcher(), + torch.fx.traceback.preserve_node_meta( + config.trace.provenance_tracking_level == 1 + ), + torch._inductor.debug.reset_provenance_globals(), + V.set_fake_mode(fake_mode), + torch._guards.tracing(tracing_context), + compiled_autograd._disable(), + ): + return aot_autograd.autograd_cache_key( + graph, + example_inputs, + ignore_shape_env=ignore_shape_env, + decompositions=decompositions, + compiler_config_extra=compiler_config_extra, + keep_inference_input_mutations=True, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_fx_async.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_fx_async.py new file mode 100644 index 0000000000000000000000000000000000000000..732a2c0a526599a8ca50773ea498add78cb5cfef --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_fx_async.py @@ -0,0 +1,408 @@ +from __future__ import annotations + +from collections import deque +from dataclasses import dataclass +from typing import Any, TYPE_CHECKING +from typing_extensions import final, override + +import torch._inductor.async_compile # noqa: F401 required to warm up AsyncCompile pools +from torch._inductor.output_code import ( + CompiledFxGraph, + CompiledFxGraphConstants, + OutputCode, +) + +from .compile_fx import _CompileFxKwargs, _InProcessFxCompile, FxCompile +from .output_code import complex_memory_overlap # noqa: F401 + + +# When async compile works with cache, remove the disabling below +BUG_CACHES_DONT_WORK_WITH_ASYNC = True + + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + from concurrent.futures import Future + + from torch._inductor.utils import InputType + from torch.fx import GraphModule + + from .compile_fx_ext import _OutOfProcessFxCompile, _WireProtocolPickledOutput + + +@dataclass +class _PostCompileData: + example_inputs: Sequence[InputType] + constants: CompiledFxGraphConstants + graph_kwargs: _CompileFxKwargs + + +@dataclass +class ProgressiveCompilationState: + progression_futures: deque[Future[_WireProtocolPickledOutput]] + callback: Callable[[_WireProtocolPickledOutput], OutputCode] + post_compile_data: _PostCompileData | None + + def check_and_get_ready_stage(self) -> int: + """Check if any progression stage is ready and return its index, or -1 if none are ready.""" + if not self.progression_futures: + return -1 + + stage_index = -1 + if self.post_compile_data: + for i, future in enumerate(self.progression_futures): + if future.done(): + stage_index = i + + return stage_index + + def switch_to_progression_stage(self, stage_index: int) -> tuple[OutputCode, bool]: + """ + Switch to the specified progression stage and return the optimized output code. + Returns a tuple of (optimized_output_code, should_clear_compilation_state). + """ + future = self.progression_futures[stage_index] + assert future is not None + optimized_output_code = self.callback(future.result()) + + if pcd := self.post_compile_data: + optimized_output_code.post_compile( + pcd.example_inputs, pcd.constants, pcd.graph_kwargs + ) + + # Clear earlier progression futures to free memory + for _ in range(stage_index + 1): + self.progression_futures.popleft() + + # Return whether all compilation state should be cleared + should_clear_state = not self.progression_futures + return optimized_output_code, should_clear_state + + +# _AsyncOutputCode handles the actual management of waiting for an +# out-of-process compile to finish and then switching over to it. +@final +class _AsyncOutputCode(OutputCode): + _eager_fn: Callable[..., Any] | None + _output_code: OutputCode | None + _future: Future[_WireProtocolPickledOutput] | None + _callback: Callable[[_WireProtocolPickledOutput], OutputCode] + _post_compile_data: _PostCompileData | None = None + _boxed_call: bool # Copied from the forward/output_code + + def __init__( + self, + # eager_fn is run until the future is finished. + eager_fn: Callable[..., Any], + # this responds with the result of the out-of-process compile when it's + # ready. + future: Future[_WireProtocolPickledOutput], + # this callback gets called to turn the _WireProtocolPickledOutput into an OutputCode + callback: Callable[[_WireProtocolPickledOutput], OutputCode], + ) -> None: + self._eager_fn = eager_fn + self._boxed_call = getattr(eager_fn, "_boxed_call", False) + self._output_code = None + + self._future = future + self._callback = callback + + @override + def __call__(self, *args: Any) -> Any: + if self._future is not None and self._future.done(): + args = self._switch_to_compiled_fn(args) + + if eager_fn := self._eager_fn: + _AsyncFxCompile._stat_eager_runs += 1 + return eager_fn(*args) + + else: + _AsyncFxCompile._stat_compiled_runs += 1 + assert self._output_code is not None + return self._output_code.__call__(*args) + + # Takes and returns the args (converted to the "right" boxed mode) + def _switch_to_compiled_fn(self, args: tuple[Any, ...]) -> tuple[Any, ...]: + assert self._future is not None + + # TODO: If the future ended in an exception do we want to continue + # running eager or hit the exception now? + f, self._future = self._future, None + output_code = self._callback(f.result()) + + if pcd := self._post_compile_data: + self._post_compile_data = None + + output_code.post_compile( + pcd.example_inputs, pcd.constants, pcd.graph_kwargs + ) + + self._output_code = output_code + self._eager_fn = None + boxed_call = getattr(output_code, "_boxed_call", False) + + if self._boxed_call != boxed_call: + if self._boxed_call: + # Was boxed, now unboxed + args = args[0] if len(args) > 0 else () + else: + # Was unboxed, now boxed + args = (args,) + + self._boxed_call = boxed_call + return args + + @override + def post_compile( + self, + example_inputs: Sequence[InputType], + constants: CompiledFxGraphConstants, + graph_kwargs: _CompileFxKwargs, + ) -> None: + if self._eager_fn is not None: + self._post_compile_data = _PostCompileData( + example_inputs, constants, graph_kwargs + ) + else: + assert self._output_code is not None + self._output_code.post_compile(example_inputs, constants, graph_kwargs) + + +# Given an FxCompile for an out-of-process compile _AsyncFxCompile will run +# eager until the compiled artifact is ready then it will automatically switch +# over to using the compiled version. +@final +class _AsyncFxCompile(FxCompile): + _compile: _OutOfProcessFxCompile + + # Some debugging stats: + # Number of times we started a background compile. + _stat_bg_started: int = 0 + # Number of times we finished a background compile. + _stat_bg_finished: int = 0 + # Number of times we ran "eager" + _stat_eager_runs: int = 0 + # Number of times we ran our compiled (out-of-process) artifact + _stat_compiled_runs: int = 0 + + def __init__(self, compile: _OutOfProcessFxCompile) -> None: + self._compile = compile + + @classmethod + def _reset_stats(cls) -> None: + cls._stat_bg_started = 0 + cls._stat_bg_finished = 0 + cls._stat_eager_runs = 0 + cls._stat_compiled_runs = 0 + + @override + def codegen_and_compile( + self, + gm: GraphModule, + example_inputs: Sequence[InputType], + inputs_to_check: Sequence[int], + graph_kwargs: _CompileFxKwargs, + ) -> OutputCode: + eager_compile = _InProcessFxCompile() + eager_compile.compile_region_name = self.compile_region_name + eager_output_code = eager_compile.codegen_and_compile( + gm, example_inputs, inputs_to_check, graph_kwargs + ) + + # This is similar to _SerializedFxCompile.codegen_and_compile() but + # handles the async routing. + + serialized = self._compile.serialize_compile( + gm, example_inputs, inputs_to_check, graph_kwargs + ) + if not serialized: + # We can't serialize - just return the eager OutputCode + return eager_output_code + + inputs, constants = serialized + + _AsyncFxCompile._stat_bg_started += 1 + f = self._compile._send_to_child_async(inputs) + + # This is called by _switch_to_compiled_fn() when f has a result... + def callback(pickled_output: _WireProtocolPickledOutput) -> OutputCode: + _AsyncFxCompile._stat_bg_finished += 1 + output = pickled_output.deserialize(constants) + if isinstance(output.graph, CompiledFxGraph): + output.graph.compile_region_name = self.compile_region_name + self._compile._postprocess(output) + return output.graph + + return _AsyncOutputCode(eager_output_code, f, callback) + + +# _ProgressiveOutputCode handles running a fast compile first, then hot-swapping +# to a more optimized version when the expensive compile finishes. +@final +class _ProgressiveOutputCode(OutputCode): + _fast_output_code: OutputCode | None + _optimized_output_code: OutputCode | None + _compilation_state: ProgressiveCompilationState | None + # _boxed_call state is effectively cached (we sometimes wrap unboxed w/ + # lambdas to box them) so we can't change it mid-way. Since _boxed_call=True + # is more common let's default to that and we'll convert if necessary. + _boxed_call: bool = True + + def __init__( + self, + # Fast compile that runs faster than the progressive compiles + fast_output_code: OutputCode, + # Futures for the progressive optimized compiles + progression_futures: Sequence[Future[_WireProtocolPickledOutput]], + # Callback to convert the optimized result to OutputCode + callback: Callable[[_WireProtocolPickledOutput], OutputCode], + ) -> None: + self._fast_output_code = fast_output_code + self._optimized_output_code = None + self._compilation_state = ProgressiveCompilationState( + progression_futures=deque(progression_futures), + callback=callback, + post_compile_data=None, + ) + + @override + def __call__(self, args: Sequence[Any]) -> Any: + # Check if any newer progression stage is ready and switch to it + self._check_and_switch_progression() + + if self._optimized_output_code is not None: + _ProgressiveFxCompile._stat_optimized_runs += 1 + output_code = self._optimized_output_code + else: + _ProgressiveFxCompile._stat_fast_runs += 1 + assert self._fast_output_code is not None + output_code = self._fast_output_code + + boxed_call = getattr(output_code, "_boxed_call", False) + if boxed_call: + res = output_code.__call__(args) + else: + res = output_code.__call__(*args) + return res + + def _check_and_switch_progression(self) -> None: + if not self._compilation_state: + return + + stage_index = self._compilation_state.check_and_get_ready_stage() + if stage_index == -1: + # no futures are ready + return + + self._switch_to_progression_stage(stage_index) + + def _switch_to_progression_stage(self, stage_index: int) -> None: + assert self._compilation_state is not None + optimized_output_code, should_clear_state = ( + self._compilation_state.switch_to_progression_stage(stage_index) + ) + + self._optimized_output_code = optimized_output_code + self._fast_output_code = None + + # Clear all compilation state if no more progression futures are left + if should_clear_state: + self._compilation_state = None + + @override + def post_compile( + self, + example_inputs: Sequence[InputType], + constants: CompiledFxGraphConstants, + graph_kwargs: _CompileFxKwargs, + ) -> None: + assert self._fast_output_code is not None + self._fast_output_code.post_compile(example_inputs, constants, graph_kwargs) + + assert self._compilation_state is not None + # Store for later when optimized version is ready + self._compilation_state.post_compile_data = _PostCompileData( + example_inputs, constants, graph_kwargs + ) + + +# _ProgressiveFxCompile runs a fast compile immediately, then kicks off +# progressive compiles in the background and hot-swaps when they're ready. +@final +class _ProgressiveFxCompile(FxCompile): + _fast_compile: FxCompile + _optimized_compile: _OutOfProcessFxCompile + _progression_configs: list[dict[str, Any]] + + # Debugging stats + _stat_bg_started: int = 0 + _stat_bg_finished: int = 0 + _stat_fast_runs: int = 0 + _stat_optimized_runs: int = 0 + + def __init__( + self, + fast_compile: FxCompile, + optimized_compile: _OutOfProcessFxCompile, + progression_configs: list[dict[str, Any]], + ) -> None: + self._fast_compile = fast_compile + self._optimized_compile = optimized_compile + self._progression_configs = progression_configs + + @classmethod + def _reset_stats(cls) -> None: + cls._stat_bg_started = 0 + cls._stat_bg_finished = 0 + cls._stat_fast_runs = 0 + cls._stat_optimized_runs = 0 + + @override + def codegen_and_compile( + self, + gm: GraphModule, + example_inputs: Sequence[InputType], + inputs_to_check: Sequence[int], + graph_kwargs: _CompileFxKwargs, + ) -> OutputCode: + import torch._inductor.config as inductor_config + + progression_futures: list[Future[_WireProtocolPickledOutput]] = [] + + for config in self._progression_configs: + with inductor_config.patch(config): + _ProgressiveFxCompile._stat_bg_started += 1 + + # Start the progressive compiles in the background + serialized = self._optimized_compile.serialize_compile( + gm, example_inputs, inputs_to_check, graph_kwargs + ) + + if not serialized: + continue + + inputs, constants = serialized + future = self._optimized_compile._send_to_child_async(inputs) + progression_futures.append(future) + + fast_output_code = self._fast_compile.codegen_and_compile( + gm, example_inputs, inputs_to_check, graph_kwargs + ) + + if not progression_futures: + # All async compile attempts failed - just return the fast version + return fast_output_code + + # Callback to handle the optimized result. + # This callback may be called multiple times, once for each progressive level completed, + # but may be skipped if a level either never completes or if a more optimal level + # completes before a less optimal one is switched to. + def callback(pickled_output: _WireProtocolPickledOutput) -> OutputCode: + _ProgressiveFxCompile._stat_bg_finished += 1 + output = pickled_output.deserialize(constants) + if isinstance(output.graph, CompiledFxGraph): + output.graph.compile_region_name = self.compile_region_name + self._optimized_compile._postprocess(output) + return output.graph + + return _ProgressiveOutputCode(fast_output_code, progression_futures, callback) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_fx_ext.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_fx_ext.py new file mode 100644 index 0000000000000000000000000000000000000000..1ed07fdc0672b5a0329c5e7b902b3ce2327b95dd --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_fx_ext.py @@ -0,0 +1,725 @@ +from __future__ import annotations + +import contextlib +import dataclasses +import functools +import logging +import os +import queue +import sys +import tempfile +import warnings +from abc import abstractmethod +from dataclasses import dataclass +from typing import Any, TYPE_CHECKING, TypeGuard +from typing_extensions import final, override, Self + +import torch._inductor.async_compile # noqa: F401 required to warm up AsyncCompile pools +import torch.fx +from torch._inductor.codecache import BypassFxGraphCache, FxGraphCache +from torch._inductor.metrics import CachedMetricsDeltas, CachedMetricsHelper +from torch._inductor.output_code import ( + CompiledFxGraph, + CompiledFxGraphConstants, + CompiledFxGraphConstantsWithGm, + OutputCode, +) +from torch._subclasses import FakeTensorMode +from torch.utils._ordered_set import OrderedSet + +from . import config +from .compile_fx import _CompileFxKwargs, _InProcessFxCompile, FxCompile, log +from .debug import DebugContext +from .graph import GraphLowering +from .output_code import complex_memory_overlap # noqa: F401 +from .virtualized import V + + +if TYPE_CHECKING: + import types + from collections.abc import Generator, Mapping, Sequence + from concurrent.futures import Future + + from torch._inductor.utils import InputType + from torch.fx import GraphModule + + +def _graph_contains_triton_kernel_wrappers(gm: GraphModule) -> bool: + """ + Check if the graph contains triton kernel wrapper nodes. These nodes contain + references to the kernel_side_table which is process-local and can't be + serialized across processes. + """ + from torch._higher_order_ops.triton_kernel_wrap import ( + triton_kernel_wrapper_functional, + triton_kernel_wrapper_mutation, + ) + + for module in gm.modules(): + if not isinstance(module, torch.fx.GraphModule): + continue + for node in module.graph.nodes: + if node.target in ( + triton_kernel_wrapper_functional, + triton_kernel_wrapper_mutation, + ): + return True + return False + + +@dataclass +class _VirtualizedSerializer: + """ + This handles the data for serializing Virtualized. + """ + + # The values here get serialized. We don't grab everything because some of + # the fields can't be serialized. + aot_compilation: Any = None + choices: Any = None + local_buffer_context: Any = None + ops: Any = None + kernel: Any = None + current_node: Any = None + + @classmethod + def serialize(cls) -> _VirtualizedSerializer: + """ + Turn the current state of torch._inductor.virtualized.V into a + serializable structure. + """ + kwargs = {} + for f in dataclasses.fields(cls): + kwargs[f.name] = getattr(V, f.name) + return _VirtualizedSerializer(**kwargs) + + def patch(self) -> _VirtualizedSerializerContextManager: + """ + Returns a context manager which patches the saved values into the + current environment. While patched, any value not listed above will be + poisoned so that reads will raise an error. + """ + return _VirtualizedSerializerContextManager(self) + + +class _VirtualizedSerializerContextManager(contextlib.ExitStack): + """ + Helper for _VirtualizedSerializer.patch() + """ + + def __init__(self, virtualized: _VirtualizedSerializer) -> None: + super().__init__() + self.virtualized = virtualized + + @override + def __enter__(self) -> Self: + super().__enter__() + + for set_name in dir(V): + if not set_name.startswith("set_"): + continue + name = set_name[4:] + name = name.removesuffix("_handler") + set_handler = getattr(V, set_name) + if hasattr(self.virtualized, name): + value = getattr(self.virtualized, name) + else: + # poison any values that we don't serialize so that any + # unset accesses are caught. + value = torch._inductor.virtualized._PoisonedVirtual + self.enter_context(set_handler(value)) + + return self + + +def _is_fallback_handler(op: object) -> bool: + try: + return op._is_fallback_handler # type: ignore[attr-defined] + except AttributeError: + return False + + +class _LoweringSerializer: + """ + This handles the data for serializing lowering.lowering + """ + + # A full implementation would make sure that all lowerings are copied over + # (or at least detected and raise a bypass when a non-standard lowering is + # used). For now we just handle tests by looking for lowerings that were + # overridden with a forced fallback. + fallbacks: OrderedSet[str] + + def __init__(self) -> None: + from . import lowering + + self.fallbacks = OrderedSet( + str(k) for k, v in lowering.lowerings.items() if _is_fallback_handler(v) + ) + + def patch(self) -> _LoweringSerializerContextManager: + return _LoweringSerializerContextManager(self) + + +class _LoweringSerializerContextManager(contextlib.ExitStack): + """ + Helper for _LoweringSerializer.patch() + """ + + def __init__(self, lowering: _LoweringSerializer) -> None: + super().__init__() + self.lowering = lowering + + @override + def __enter__(self) -> Self: + super().__enter__() + + from . import lowering + + for k, v in lowering.lowerings.items(): + name = str(k) + if name in self.lowering.fallbacks: + if not _is_fallback_handler(v): + self.enter_context(lowering.force_fallback(k)) # type: ignore[arg-type] + + return self + + +@dataclass +class _FakeTensorModeSerializer: + allow_non_fake_inputs: bool + + def __init__(self, fake_mode: FakeTensorMode) -> None: + self.allow_non_fake_inputs = fake_mode.allow_non_fake_inputs + self.shape_env = fake_mode.shape_env + + @contextlib.contextmanager + def patch(self, fake_mode: FakeTensorMode) -> Generator[None, None, None]: + saved_allow_non_fake_inputs = fake_mode.allow_non_fake_inputs + fake_mode.allow_non_fake_inputs = self.allow_non_fake_inputs + + yield + + fake_mode.allow_non_fake_inputs = saved_allow_non_fake_inputs + + +@dataclass +class _WireProtocolInput: + """ + For _SerializedFxCompile - encapsulates all the data being transferred + (sent) from the parent to the child. + """ + + gm: torch.fx.GraphModule + example_inputs: Sequence[InputType] + inputs_to_check: Sequence[int] + graph_kwargs: _CompileFxKwargs + tracing_context: torch._guards.TracingContext | None + config: dict[str, object] + virtualized: _VirtualizedSerializer + deterministic_guard_for_testing: ( # type: ignore[name-defined] # mypy bug + torch.testing._internal.common_utils.DeterministicGuard | None + ) + logger_state: _LoggerState + lowering: _LoweringSerializer + fake_tensor_mode: _FakeTensorModeSerializer + + def serialize(self) -> _WireProtocolPickledInput: + """ + Turns this object into a _WireProtocolPickledInput which can be + directly transferred across a stream. + """ + from torch.fx._graph_pickler import GraphPickler + + return _WireProtocolPickledInput(GraphPickler.dumps(self)) + + +def _current_fake_mode() -> FakeTensorMode: + fake_mode = None + if context := torch._guards.TracingContext.try_get(): + fake_mode = context.fake_mode + if fake_mode is not None: + return fake_mode + + shape_env = torch.fx.experimental.symbolic_shapes.ShapeEnv() + return FakeTensorMode(shape_env=shape_env) + + +@dataclass +class _WireProtocolPickledInput: + value: bytes + + def deserialize(self) -> _WireProtocolInput: + """ + Turn this streamable object back into a _WireProtocolInput. + """ + from torch.fx._graph_pickler import GraphPickler + + fake_mode = _current_fake_mode() + result = GraphPickler.loads(self.value, fake_mode) + assert isinstance(result, _WireProtocolInput) + return result + + +@dataclass +class _WireProtocolOutput: + """ + For _SerializedFxCompile - encapsulates all the data being transferred + (returned) back from the child to the parent. + """ + + graph: OutputCode + metrics: CachedMetricsDeltas + logs: list[logging.LogRecord] + warning_replay: list[warnings.WarningMessage] | None + shape_env: torch.fx.experimental.symbolic_shapes.ShapeEnv | None + + def serialize(self) -> _WireProtocolPickledOutput: + """ + Turns this object into a _WireProtocolPickledOutput which can be + directly transferred across a stream. + """ + from torch.fx._graph_pickler import GraphPickler + + if isinstance(self.graph, CompiledFxGraph): + self.graph.prepare_for_serialization() + return _WireProtocolPickledOutput(GraphPickler.dumps(self)) + + +@dataclass +class _WireProtocolPickledOutput: + value: bytes + + def deserialize(self, constants: CompiledFxGraphConstants) -> _WireProtocolOutput: + """ + Turn this streamable object back into a _WireProtocolOutput. + """ + from torch.fx._graph_pickler import GraphPickler + + fake_mode = _current_fake_mode() + result = GraphPickler.loads(self.value, fake_mode) + assert isinstance(result, _WireProtocolOutput) + if isinstance(result.graph, CompiledFxGraph): + result.graph.after_deserialization(constants) + return result + + +class _LoggerState: + """ + This class is for tracking logging that happens during an out-of-process + compile so we can "replay" those messages when the compile is done. Used as + a context manager which returns the captured logs (object). + """ + + loggers: dict[str, int] + # The actual log capturing mechanism - this should be None when we're not + # actively capturing logs. + captured_logs: _CapturedLogs | None = None + + def __init__(self) -> None: + # Mapping from logger name to level. + self.loggers = {} + + def filter( + logger: logging.Logger | logging.PlaceHolder, + ) -> TypeGuard[logging.Logger]: + if not isinstance(logger, logging.Logger): + # Assume that Placeholders propagate + return False + # We only want to track torch._inductor logging + if not logger.name.startswith("torch._inductor"): + return False + # If this logger propagates then assume we'll track its parent + if logger.propagate: + return False + return True + + root = logging.getLogger("torch._inductor") + if sys.version_info < (3, 12): + # logging.getChildren() doesn't exist until 3.12 + logging._acquireLock() # type: ignore[attr-defined] + try: + for logger in root.manager.loggerDict.values(): + if filter(logger): + self.loggers[logger.name] = logger.level + finally: + logging._releaseLock() # type: ignore[attr-defined] + else: + q = [root] + while q: + logger = q.pop() + if filter(logger): + self.loggers[logger.name] = logger.level + q.extend(logger.getChildren()) + + def __enter__(self) -> _CapturedLogs: + assert self.captured_logs is None + self.captured_logs = _CapturedLogs(self) + self.captured_logs.apply() + return self.captured_logs + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: types.TracebackType | None, + ) -> None: + assert self.captured_logs is not None + self.captured_logs.remove() + + +class _CapturedLogs: + """ + Helper for _LoggerState - this class actually attaches to the logger in + the child process and grabs the log messages themselves. + """ + + state: _LoggerState + queue: queue.Queue[logging.LogRecord] + handlers: dict[str, logging.Handler] | None + + def __init__(self, state: _LoggerState) -> None: + self.state = state + # A queue of the log entries + # TODO: For memory purposes should we log to a file and then respond with that? + self.queue = queue.Queue(-1) + # Mapping from name to handler (only valid when applied) + self.handlers = None + + def finish(self) -> list[logging.LogRecord]: + assert self.handlers is None + logs = [] + try: + while True: + logs.append(self.queue.get_nowait()) + except queue.Empty: + pass + return logs + + def remove(self) -> None: + assert self.handlers is not None + handlers, self.handlers = self.handlers, None + for name, handler in handlers.items(): + logger = logging.getLogger(name) + logger.removeHandler(handler) + + def apply(self) -> None: + from logging.handlers import QueueHandler + + assert self.handlers is None + self.handlers = {} + for name, level in self.state.loggers.items(): + logger = logging.getLogger(name) + handler = QueueHandler(self.queue) + self.handlers[name] = handler + logger.addHandler(handler) + if level != logging.NOTSET: + logger.setLevel(level) + + +class _SerializedFxCompile(FxCompile): + """ + This is used to represent an FxCompile which occurs across a serialized + boundary. + """ + + @override + def codegen_and_compile( + self, + gm: GraphModule, + example_inputs: Sequence[InputType], + inputs_to_check: Sequence[int], + graph_kwargs: _CompileFxKwargs, + ) -> OutputCode: + # If this code changes it's likely _AsyncFxCompile.codegen_and_compile() + # will also need to match. + + serialized = self.serialize_compile( + gm, example_inputs, inputs_to_check, graph_kwargs + ) + if not serialized: + eager_compile = _InProcessFxCompile() + eager_compile.compile_region_name = self.compile_region_name + return eager_compile.codegen_and_compile( + gm, example_inputs, inputs_to_check, graph_kwargs + ) + + inputs, constants = serialized + output = self._send_to_child(inputs).deserialize(constants) + if isinstance(output.graph, CompiledFxGraph): + output.graph.compile_region_name = self.compile_region_name + + self._postprocess(output) + self._compile_stats[type(self)].codegen_and_compile += 1 + + # TODO: Do we need to figure out what changed in TracingContext in the + # child and plumb that back up to the parent? + + return output.graph + + def serialize_compile( + self, + gm: GraphModule, + example_inputs: Sequence[InputType], + inputs_to_check: Sequence[int], + graph_kwargs: _CompileFxKwargs, + ) -> tuple[_WireProtocolPickledInput, CompiledFxGraphConstantsWithGm] | None: + """ + Prepare a _WireProtocolInput to compile. If None is returned then it + wasn't possible to serialize and we should fallback to in-process. + """ + try: + # _check_for_hop raises BypassFxGraphCache when it detects something + # we can't cache (or serialize) + FxGraphCache._check_for_hop(gm) + except BypassFxGraphCache as e: + log.debug("Skipping %s compile: %s", type(self), e) # noqa: G200 + return None + + # Triton kernel wrapper nodes contain references to the kernel_side_table + # which is process-local and can't be serialized across processes. + if _graph_contains_triton_kernel_wrappers(gm): + log.debug( + "Skipping %s compile: graph contains triton kernel wrappers", type(self) + ) + return None + + context = torch._guards.TracingContext.try_get() + constants = CompiledFxGraphConstantsWithGm(gm) + logger_state = _LoggerState() + lowering = _LoweringSerializer() + + # If we're running tests then grab the DeterministicGuard (don't want to + # import this if it isn't already imported because it has side-effects) + deterministic_guard_for_testing: ( # type: ignore[name-defined] # mypy bug + torch.testing._internal.common_utils.DeterministicGuard | None + ) = None + try: + deterministic_guard_for_testing = ( + torch.testing._internal.common_utils.DeterministicGuard._current_state() # type: ignore[attr-defined] # mypy bug + ) + except AttributeError: + pass + + fake_mode = _current_fake_mode() + fake_tensor_mode = _FakeTensorModeSerializer(fake_mode) + + from pickle import PicklingError + + try: + input = _WireProtocolInput( + gm, + example_inputs, + inputs_to_check, + graph_kwargs, + context, + config.save_config_portable(), + _VirtualizedSerializer.serialize(), + deterministic_guard_for_testing, + logger_state, + lowering, + fake_tensor_mode, + ).serialize() + return (input, constants) + except (AttributeError, BypassFxGraphCache, PicklingError): + # For example: AttributeError: Can't pickle local object + # 'make_opaque_unary_fn..OpaqueUnaryFn' + + # TODO: scuba record about not being able to do this? + log.warning("Unable to pickle input graph or example inputs", exc_info=True) + + return None + + @abstractmethod + def _send_to_child( + self, pickled_input: _WireProtocolPickledInput + ) -> _WireProtocolPickledOutput: + # The implementation of this should transfer `input` to the child, call + # `_run_in_child(input)` and transfer the result back. + ... + + def _postprocess(self, output: _WireProtocolOutput) -> None: + pass + + @classmethod + def _run_in_child( + cls, + pickled_input: _WireProtocolPickledInput, + extra_env: Mapping[str, str] | None = None, + ) -> _WireProtocolPickledOutput: + metrics = CachedMetricsHelper() + + with contextlib.ExitStack() as stack: + if extra_env is not None: + import unittest + + stack.enter_context(unittest.mock.patch.dict("os.environ", extra_env)) + + # Save warnings to "replay" in the parent + warning_replay = stack.enter_context(warnings.catch_warnings(record=True)) + + # TODO: Should we split the input into multiple sections where each + # section sets up state for the previous section? (i.e. a Config section + # which we decode and apply, followed by a FakeTensorMode section which + # we decode and apply, etc) + input = pickled_input.deserialize() + + stack.enter_context(input.virtualized.patch()) + stack.enter_context(input.lowering.patch()) + stack.enter_context(config.patch(input.config)) + captured_logs = stack.enter_context(input.logger_state) + if input.deterministic_guard_for_testing: + stack.enter_context(input.deterministic_guard_for_testing) + stack.enter_context(torch._guards.tracing(input.tracing_context)) + stack.enter_context(DebugContext()) + + fake_mode = _current_fake_mode() + stack.enter_context(input.fake_tensor_mode.patch(fake_mode)) + + output_graph = _InProcessFxCompile().codegen_and_compile( + input.gm, + input.example_inputs, + input.inputs_to_check, + input.graph_kwargs, + ) + + logs = captured_logs.finish() + + return _WireProtocolOutput( + output_graph, + metrics.get_deltas(), + logs, + warning_replay, + fake_mode.shape_env, + ).serialize() + + +# This is a debugging/testing implementation of FxCompile which serializes the +# input and output but still runs the FxCompile in-process. +@final +class _DebugSerdeFxCompile(_SerializedFxCompile): + @override + def _send_to_child( + self, pickled_input: _WireProtocolPickledInput + ) -> _WireProtocolPickledOutput: + # For debugging just serde the input and output but don't run in a + # subprocess. + return self._run_in_child(pickled_input) + + +class _OutOfProcessFxCompile(_SerializedFxCompile): + """ + Represents an FxCompile which is run outside the current process (in + either a subprocess or possibly even a separate machine). + """ + + @override + @final + def _send_to_child( + self, pickled_input: _WireProtocolPickledInput + ) -> _WireProtocolPickledOutput: + f = self._send_to_child_async(pickled_input) + + # For debugging: If we want to print status updates... + # last = time.time() + # while not f.done(): + # print("tick...") + # time.sleep(0.125) + # now = time.time() + # if now - last > 1: + # last = now + + return f.result() + + @abstractmethod + def _send_to_child_async( + self, pickled_input: _WireProtocolPickledInput + ) -> Future[_WireProtocolPickledOutput]: ... + + def _postprocess(self, output: _WireProtocolOutput) -> None: + # Since our metrics were gathered in a subprocess make sure to add them + # here. + CachedMetricsHelper.apply_deltas(output.metrics) + + # This is used by tests to check the output for specific details. For + # remote things (subproc and RE) we need to do the `save_output_code` + # here since it didn't happen earlier in-process. In the future if this + # doesn't have "source_code" (it's a CompiledAOTI, for example) and we + # need it we'll have to grab it and serialize it separately from the + # child. + if GraphLowering.save_output_code is not None: + GraphLowering.save_output_code(output.graph.source_code) # type: ignore[attr-defined] + + # And forward our collected logs. The cache is cleared when the outer + # function exits. + @functools.cache + def getLogger(name: str) -> logging.Logger: + return logging.getLogger(name) + + if output.warning_replay: + for w in output.warning_replay: + warnings.warn_explicit( + message=w.message, + category=w.category, + filename=w.filename, + lineno=w.lineno, + source=w.source, + ) + + for record in output.logs: + logger = getLogger(record.name) + logger.handle(record) + + +# For debugging - create a _FxCompile which writes the serialized data to a file +# and then exits. +# +# TODO: make this a FxCompileMode value? +# +# The "child runner" should look something like this: +# +# import torch +# from torch._inductor import compile_fx +# idx = 0 +# with open(f"/tmp/pytorch_compile_fx_tmp_input_{idx}.bin", "rb") as f: +# input = compile_fx._WireProtocolPickledInput(f.read()) +# result = compile_fx._SubprocessFxCompile._run_in_child(input) +# with open(f"/tmp/pytorch_compile_fx_tmp_output_{idx}.bin", "wb") as f: +# f.write(result.value) +# +@final +class _DebugFileFxCompile(_SerializedFxCompile): + file_index = 0 + + @override + def _send_to_child( + self, pickled_input: _WireProtocolPickledInput + ) -> _WireProtocolPickledOutput: + idx = _DebugFileFxCompile.file_index + _DebugFileFxCompile.file_index += 1 + + name = os.path.join( + tempfile.gettempdir(), f"pytorch_compile_fx_tmp_input_{idx}.bin" + ) + with open(name, "wb") as f: + f.write(pickled_input.value) + print(f"Wrote to {name}") + + if False: + name = os.path.join( + tempfile.gettempdir(), f"pytorch_compile_fx_tmp_actual_{idx}.bin" + ) + actual = self._run_in_child(pickled_input) + with open(name, "wb") as f: + f.write(actual.value) + return actual + elif False: + name = os.path.join( + tempfile.gettempdir(), f"pytorch_compile_fx_tmp_output_{idx}.bin" + ) + with open(name, "rb") as f: + result = _WireProtocolPickledOutput(f.read()) + print(f"Read from {name}") + return result + else: + os._exit(-1) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_fx_subproc.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_fx_subproc.py new file mode 100644 index 0000000000000000000000000000000000000000..0dbf71cbb175c5d949d1e63a432568b72fc05441 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_fx_subproc.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import atexit +import functools +import os +from typing import TYPE_CHECKING +from typing_extensions import final, override + +import torch._inductor.async_compile # noqa: F401 required to warm up AsyncCompile pools +import torch.fx +from torch._inductor.compile_worker.subproc_pool import ( + AnyPool, + SubprocKind, + SubprocPool, +) +from torch._inductor.utils import clear_caches + +from .compile_fx_ext import ( + _OutOfProcessFxCompile, + _WireProtocolPickledInput, + _WireProtocolPickledOutput, +) +from .output_code import complex_memory_overlap # noqa: F401 + + +if TYPE_CHECKING: + from collections.abc import Mapping + from concurrent.futures import Future + + +@final +class _SubprocessFxCompile(_OutOfProcessFxCompile): + @override + def _send_to_child_async( + self, input: _WireProtocolPickledInput + ) -> Future[_WireProtocolPickledOutput]: + # TODO: Do we need to copy across some kind of logging IDs? (ChromiumEventLogger) + + pool = self.process_pool() + + # TODO: This is probably the wrong thing to do long-term - but for now + # let's share the cache so we can identify tests broken by this later. + env_vars = ["TORCHINDUCTOR_CACHE_DIR", "TRITON_CACHE_DIR"] + extra_env = {v: os.environ[v] for v in env_vars if v in os.environ} + + return pool.submit( + _SubprocessFxCompile._run_in_child_subprocess, input, extra_env + ) + + @staticmethod + @functools.cache + def process_pool() -> AnyPool: + pool = SubprocPool( + # TODO: Consider raising this limit if we start using async w/ + # subprocess and want to compile multiple graphs in parallel. + 1, + kind=SubprocKind.SPAWN, + ) + + atexit.register(pool.shutdown) + + return pool + + @classmethod + def _run_in_child_subprocess( + cls, + pickled_input: _WireProtocolPickledInput, + extra_env: Mapping[str, str] | None, + ) -> _WireProtocolPickledOutput: + # TODO: In subprocess mode we need to clear the inductor caches. + # The problem: + # 1. We compile in worker A which fills stuff in tmpdir + # 2. parent clears inductor caches which deletes tmpdirs and tells + # cpp_prefix_path() to clear its LRU cache + # 3. We compile a second time in subproc A - but since we never told + # cpp_prefix_path() in worker A to clear its LRU it thinks the + # tmpdir still exists and fails to compile. + # + # TODO: We probably should be using a separate tmpdir in the worker + # anyway... but we should probably still respect clear_caches() + # in the parent... maybe? + # + # TODO: We could be less aggressive by keeping a clock which gets + # incremented when we clear the cache, send the clock to the worker and + # only clear caches if the clock changed since last time. + # + clear_caches() + torch._inductor.metrics.reset() + + # TODO: turn off config.fx_graph_async_compile + + result = cls._run_in_child(pickled_input, extra_env) + return result diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_worker/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_worker/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_worker/__main__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_worker/__main__.py new file mode 100644 index 0000000000000000000000000000000000000000..6ca0f1e5a4fb2a6aeb1224285d76e78a05a0f499 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_worker/__main__.py @@ -0,0 +1,80 @@ +# mypy: allow-untyped-defs +import argparse +import base64 +import functools +import importlib +import logging +import os +import sys +from typing import TypeVar + +from torch._inductor.async_compile import pre_fork_setup +from torch._inductor.codecache import torch_key +from torch._inductor.compile_worker.subproc_pool import ( + SubprocKind, + SubprocMain, + SubprocPickler, +) +from torch._inductor.compile_worker.utils import _async_compile_initializer +from torch._inductor.runtime.compile_tasks import _set_triton_ptxas_path + + +_T = TypeVar("_T") + + +log = logging.getLogger(__name__) + +_set_triton_ptxas_path() + +try: + import triton + + assert triton is not None # preload in parent +except ImportError: + pass + + +def _lookup_and_create_type(base: type[_T], qname: str) -> _T: + """ + Given a base type and qualified name: import & lookup that name, check + that it's of the given type and then instantiate it. + """ + pkg, name = qname.rsplit(".", 1) + mod = importlib.import_module(pkg) + ty = getattr(mod, name) + if not issubclass(ty, base): + raise TypeError(f"Type {ty} is not a subtype of {base}") + return ty() + + +def main(): + try: + parser = argparse.ArgumentParser() + parser.add_argument( + "--pickler", type=functools.partial(_lookup_and_create_type, SubprocPickler) + ) + parser.add_argument("--kind", type=SubprocKind) + parser.add_argument("--workers", type=int) + parser.add_argument("--parent", type=int) + parser.add_argument("--read-fd", type=int) + parser.add_argument("--write-fd", type=int) + parser.add_argument("--torch-key", type=str) + args = parser.parse_args() + if os.getppid() != args.parent: + sys.exit(0) + read_fd = os.fdopen(args.read_fd, "rb") + write_fd = os.fdopen(args.write_fd, "wb") + + pre_fork_setup() + + torch_key.set(base64.b64decode(args.torch_key.encode("utf-8"))) # type: ignore[attr-defined] + + _async_compile_initializer(args.parent) + + SubprocMain(args.pickler, args.kind, args.workers, read_fd, write_fd).main() + except Exception: + log.exception("Uncaught exception in compile_worker subprocess") + + +if __name__ == "__main__": + main() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_worker/subproc_pool.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_worker/subproc_pool.py new file mode 100644 index 0000000000000000000000000000000000000000..319c2d6918a9b2c2ef69fad434ed2207834ecb6a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_worker/subproc_pool.py @@ -0,0 +1,496 @@ +import base64 +import functools +import itertools +import logging +import multiprocessing +import os +import pickle +import struct +import subprocess +import sys +import threading +import traceback +import typing +from collections.abc import Callable +from concurrent.futures import Future, ProcessPoolExecutor +from concurrent.futures.process import BrokenProcessPool +from enum import Enum, IntEnum +from typing import Any, IO, TypeVar +from typing_extensions import Never, ParamSpec + +# _thread_safe_fork is needed because the subprocesses in the pool can read +# justknobs, e.g., in the Triton compiler. For internal, the import installs +# functionality to destroy singletons before forking and re-enable them after. +import torch._thread_safe_fork # noqa: F401 +from torch._inductor import config +from torch._inductor.codecache import torch_key +from torch._inductor.compile_worker.timer import Timer +from torch._inductor.compile_worker.tracked_process_pool import ( + TrackedProcessPoolExecutor, +) +from torch._inductor.compile_worker.utils import _async_compile_initializer +from torch._inductor.utils import get_ld_library_path, python_subprocess_env +from torch._utils_internal import find_compile_subproc_binary +from torch.monitor import _WaitCounter, _WaitCounterTracker + + +log = logging.getLogger(__name__) + +_P = ParamSpec("_P") +_T = TypeVar("_T") + + +class MsgHeader(IntEnum): + ERROR = 0 + SHUTDOWN = 1 + QUIESCE = 2 + WAKEUP = 3 + JOB = 4 + + +def _pack_msg(msg_header: MsgHeader, job_id: int, length: int) -> bytes: + return struct.pack("nnn", int(msg_header), job_id, length) + + +def _unpack_msg(data: bytes) -> tuple[MsgHeader, int, int]: + if not data: + return MsgHeader.ERROR, -1, -1 + msg_header, job_id, length = struct.unpack("nnn", data) + return MsgHeader(msg_header), job_id, length + + +msg_bytes = len(_pack_msg(MsgHeader.JOB, 0, 0)) + + +def _send_msg( + write_pipe: IO[bytes], msg_header: MsgHeader, job_id: int = -1, data: bytes = b"" +) -> None: + length = len(data) + write_pipe.write(_pack_msg(msg_header, job_id, length)) + if length > 0: + write_pipe.write(data) + write_pipe.flush() + + +def _recv_msg(read_pipe: IO[bytes]) -> tuple[MsgHeader, int, bytes]: + msg_header, job_id, length = _unpack_msg(read_pipe.read(msg_bytes)) + data = read_pipe.read(length) if length > 0 else b"" + return msg_header, job_id, data + + +class _SubprocExceptionInfo: + """ + Carries exception info from subprocesses across the wire. traceback + objects are not pickleable, so we store the trace as a string and + use it for the message in the exception thrown in the main process. + """ + + def __init__(self, details: str) -> None: + self.details = details + + +class SubprocException(Exception): + """ + Thrown when a job in a subprocess raises an Exception. + """ + + def __init__(self, details: str, name: str = "") -> None: + self.details = details + super().__init__( + f"An exception occurred in a subprocess:\n\nName={name}\n{details}" + ) + + def with_name(self, name: str) -> "SubprocException": + return SubprocException(self.details, name) + + +class SubprocPickler: + """ + Allows a caller to provide a custom pickler for passing data with the + subprocess. + """ + + def dumps(self, obj: object) -> bytes: + return pickle.dumps(obj, pickle.HIGHEST_PROTOCOL) + + def loads(self, data: bytes) -> object: + return pickle.loads(data) + + +class SubprocKind(Enum): + FORK = "fork" + SPAWN = "spawn" + + +class SubprocPool: + """ + Mimic a concurrent.futures.ProcessPoolExecutor, but wrap it in + a subprocess.Popen() to try to avoid issues with forking/spawning + """ + + def __init__( + self, + nprocs: int, + pickler: SubprocPickler | None = None, + kind: SubprocKind = SubprocKind.FORK, + quiesce: bool = False, + ) -> None: + entry = os.path.join(os.path.dirname(__file__), "__main__.py") + self.pickler = pickler or SubprocPickler() + self.kind = kind + + subproc_read_fd, write_fd = os.pipe() + read_fd, subproc_write_fd = os.pipe() + self.write_pipe = os.fdopen(write_fd, "wb") + self.read_pipe = os.fdopen(read_fd, "rb") + torch_key_str = base64.b64encode(torch_key()).decode("utf-8") + + cmd = [ + sys.executable, + entry, + ] + if (binary := find_compile_subproc_binary()) is not None: + cmd = [binary] + + args = [ + f"--pickler={self.pickler.__class__.__module__}.{self.pickler.__class__.__name__}", + f"--kind={self.kind.value}", + f"--workers={nprocs}", + f"--parent={os.getpid()}", + f"--read-fd={str(subproc_read_fd)}", + f"--write-fd={str(subproc_write_fd)}", + f"--torch-key={torch_key_str}", + ] + cmd.extend(args) + log_path = None + self.log_file = None + + if config.worker_suppress_logging: + log_path = os.devnull + log.info("Suppressing compile worker output due to config") + else: + log_path = config.torchinductor_worker_logpath + if not log_path: + log_path = config.get_worker_log_path() + + if log_path: + # pyrefly: ignore [bad-assignment] + self.log_file = open(log_path, "w") # noqa:SIM115 + + self.process = subprocess.Popen( + cmd, + env={ + **python_subprocess_env(), + # Safeguard against creating a SubprocPool in the subprocess. + "TORCH_WARM_POOL": "0", + # Some internal usages need a modified LD_LIBRARY_PATH. + "LD_LIBRARY_PATH": get_ld_library_path(), + }, + pass_fds=(subproc_read_fd, subproc_write_fd), + stdout=self.log_file, + stderr=self.log_file, + ) + self.write_lock = threading.Lock() + self.read_thread = threading.Thread( + target=self._read_thread, name="InductorSubproc", daemon=True + ) + + self.futures_lock = threading.Lock() + self.pending_futures: dict[int, Future[Any]] = {} + # The pending waitcounter, is used to indicate the time when we have any specific job running. + self.pending_waitcounters: dict[int, Any] = {} + self.job_id_count = itertools.count() + + # The running waitcounter indicates the time when the SubProcPool object exists. + self.running = True + self.running_waitcounter = _WaitCounter( + "pytorch.wait_counter.subproc_pool.running" + ).guard() + self.running_waitcounter.__enter__() + + # The quiesce waitcounter indicates when the job is in a quiesced state. + self.quiesce_waitcounter: _WaitCounterTracker | None = None + + # Firstjob is used to capture the time from when the firstjob is queued, to when the first job is done. + self.firstjob = True + self.firstjob_id: int | None = None + self.firstjob_waitcounter = _WaitCounter( + "pytorch.wait_counter.subproc_pool.first_job" + ).guard() + + if quiesce: + self.timer: Timer | None = Timer( + config.quiesce_async_compile_time, self.quiesce + ) + else: + self.timer = None + + # Start thread last to ensure all member variables are initialized + # before any access. + self.read_thread.start() + + def submit( + self, job_fn: Callable[_P, _T], *args: _P.args, **kwargs: _P.kwargs + ) -> Future[_T]: + if args or kwargs: + # pyrefly: ignore [bad-assignment] + job_fn = functools.partial(job_fn, *args, **kwargs) + job_data = self.pickler.dumps(job_fn) + future: Future[_T] + with self.futures_lock: + job_id = next(self.job_id_count) + self.pending_futures[job_id] = future = Future() + self.pending_waitcounters[job_id] = _WaitCounter( + "pytorch.wait_counter.subproc_pool.job" + ).guard() + self.pending_waitcounters[job_id].__enter__() + if self.quiesce_waitcounter: + self.firstjob = True + self.quiesce_waitcounter.__exit__() + self.quiesce_waitcounter = None + # This can be entered from either quiesce wakeup, or from startup. + if self.firstjob: + self.firstjob_id = job_id + self.firstjob_waitcounter.__enter__() + self.firstjob = False + future.set_running_or_notify_cancel() + self._send(MsgHeader.JOB, job_id, job_data) + return future + + def _send(self, msg_header: MsgHeader, job_id: int = -1, data: bytes = b"") -> None: + with self.write_lock: + if not self.running: + raise RuntimeError("Attempting to use a closed pool") + _send_msg(self.write_pipe, msg_header, job_id, data) + + def _read_thread(self) -> None: + while True: + data = b"" + job_id = -1 + try: + msg_header, job_id, data = _recv_msg(self.read_pipe) + except Exception: + # Something went wrong during the read. There's no way we have a + # valid msg. + log.exception("failure in subproc_pool._recv_msg") + msg_header = MsgHeader.ERROR + + if msg_header != MsgHeader.JOB: + # read_pipe returned None or got exception + if self.running: + log.warning("SubprocPool unclean exit") + self.running = False + self.running_waitcounter.__exit__() + self.read_pipe.close() + # Cancel all the pending futures. + self.shutdown() + return + + try: + result = self.pickler.loads(data) + except Exception as e: + # Something went wrong unpickling. We have a job_id so just + # notify that particular future and continue on. + log.exception("unpickle failure in SubprocPool._read_thread") + result = e + + with self.futures_lock: + if not self.running: + return + if self.timer: + self.timer.record_call() + if isinstance(result, _SubprocExceptionInfo): + # An exception occurred in the submitted job + self.pending_futures[job_id].set_exception( + SubprocException(result.details) + ) + elif isinstance(result, Exception): + # An exception occurred in some of our subprocess machinery. + self.pending_futures[job_id].set_exception(result) + else: + self.pending_futures[job_id].set_result(result) + + self.pending_waitcounters[job_id].__exit__() + del self.pending_waitcounters[job_id] + if self.firstjob_id == job_id: + self.firstjob_waitcounter.__exit__() + + del self.pending_futures[job_id] + + def quiesce(self) -> None: + self._send(MsgHeader.QUIESCE) + if self.quiesce_waitcounter is None: + self.quiesce_waitcounter = _WaitCounter( + "pytorch.wait_counter.subproc_pool.quiesced" + ).guard() + self.quiesce_waitcounter.__enter__() + + def wakeup(self) -> None: + self._send(MsgHeader.WAKEUP) + + def shutdown(self) -> None: + try: + with self.write_lock: + if not self.running: + return + if self.timer: + self.timer.quit() + self.running = False + self.running_waitcounter.__exit__() + _send_msg(self.write_pipe, MsgHeader.SHUTDOWN) + self.write_pipe.close() + self.process.wait(300) + if self.log_file: + self.log_file.close() + except OSError: + log.warning("Ignored OSError in pool shutdown", exc_info=True) + finally: + with self.futures_lock: + for future in self.pending_futures.values(): + if not future.cancel(): + future.set_exception(RuntimeError("SubprocPool closed")) + self.pending_futures.clear() + + +class SubprocMain: + """Communicates with a SubprocPool in the parent process, called by __main__.py""" + + def __init__( + self, + pickler: SubprocPickler, + kind: SubprocKind, + nprocs: int, + read_pipe: IO[bytes], + write_pipe: IO[bytes], + ) -> None: + self.pickler = pickler + self.kind = kind + self.read_pipe = read_pipe + self.write_pipe = write_pipe + self.write_lock = threading.Lock() + self.nprocs = nprocs + self.pool: ProcessPoolExecutor | None = None + self.running = True + + def main(self) -> None: + while True: + msg_header, job_id, data = _recv_msg(self.read_pipe) + if msg_header == MsgHeader.JOB: + self.submit(job_id, data) + elif msg_header == MsgHeader.WAKEUP: + self._start_pool() + elif msg_header == MsgHeader.QUIESCE: + self._quiesce() + else: + return self._shutdown() + + def _quiesce(self) -> None: + if self.pool is not None: + self.pool.shutdown(wait=False) + self.pool = None + + def _shutdown(self) -> None: + with self.write_lock: + self.running = False + try: + _send_msg(self.write_pipe, MsgHeader.SHUTDOWN) + self.write_pipe.close() + except BrokenPipeError: + pass # parent process already shutdown + self.read_pipe.close() + self._quiesce() + + def submit(self, job_id: int, data: bytes) -> None: + while self.running: + try: + self._submit_inner(job_id, data) + return + except BrokenProcessPool: + # If any subprocess in the pool crashes, we get a BrokenProcessPool + # exception and the whole pool becomes unusable. Handle crashes by + # recreating the pool and resubmitting. + self.pool = None + + def _submit_inner(self, job_id: int, data: bytes) -> None: + def callback(fut: Future[Any]) -> None: + if not self.running: + return + try: + result = fut.result() + except Exception as e: + log.exception("Error in subprocess") + result = self.pickler.dumps(e) + assert isinstance(result, bytes) + with self.write_lock: + if self.running: + _send_msg(self.write_pipe, MsgHeader.JOB, job_id, result) + return + + self._start_pool() + assert self.pool is not None + + future = self.pool.submit( + functools.partial(SubprocMain.do_job, self.pickler, data) + ) + future.add_done_callback(callback) + + def _start_pool(self) -> None: + if self.pool is not None: + return + + self.pool = TrackedProcessPoolExecutor( + self.nprocs, + mp_context=multiprocessing.get_context(self.kind.value), + initializer=functools.partial(_async_compile_initializer, os.getpid()), + ) + multiprocessing.util.Finalize( + None, self.pool.shutdown, exitpriority=sys.maxsize + ) + _warm_process_pool(self.pool, self.nprocs) + + @staticmethod + def do_job(pickler: SubprocPickler, data: bytes) -> bytes: + # do the pickle/unpickle in the sub-subproc + job = typing.cast(Callable[[], object], pickler.loads(data)) + + try: + result = job() + except Exception: + result = _SubprocExceptionInfo(traceback.format_exc()) + return pickler.dumps(result) + + +AnyPool = ProcessPoolExecutor | SubprocPool + + +def _warm_process_pool(pool: ProcessPoolExecutor, n: int) -> None: + # We have to fork processes for compiler workers, but the more memory and other resources that are loaded, the + # slower the os.fork time is, quite drastically. It also holds the GIL so we can't put it on another thread. + + # Examples: + # A simple x + x + x script: 10ms seconds in the middle of the program, 2ms at startup + # tf_efficientnet_b0 benchmark: 50ms! in the middle of the program , 3ms at startup + + # So we want to start the workers early when it is still cheap, and also to allow the workers to get + # ready before we have work for them. + + # ProcessPoolExecutor also does not launch the workers until it finds a point when all the workers are idle. + # But if we waited until then fork time will be long and we will be waiting for the processes to initialize. + + # We force them to start here with some YOLOing of the internal methods. + + if hasattr(pool, "_start_queue_management_thread"): + pool._start_queue_management_thread() + else: + for _ in range(n): + pool._adjust_process_count() + if hasattr(pool, "_start_executor_manager_thread"): + pool._start_executor_manager_thread() + + +class TestException(RuntimeError): + pass + + +def raise_testexc() -> Never: + raise TestException diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_worker/timer.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_worker/timer.py new file mode 100644 index 0000000000000000000000000000000000000000..35ed0b810439c195b22910a12f541a56224d7381 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_worker/timer.py @@ -0,0 +1,54 @@ +from collections.abc import Callable +from threading import Lock, Thread +from time import monotonic, sleep + + +class Timer: + """ + This measures how long we have gone since last receiving an event and if it is greater than a set interval, calls a function. + """ + + def __init__( + self, + duration: int | float, # Duration in seconds + call: Callable[[], None], # Function to call when we expire + ) -> None: + # We don't start the background thread until we actually get an event. + self.background_thread: Thread | None = None + self.last_called: float | None = None + self.duration = duration + self.sleep_time = duration / 2 + self.call = call + self.exit = False + + self.lock = Lock() + + def record_call(self) -> None: + with self.lock: + if self.background_thread is None: + self.background_thread = Thread( + target=self.check, daemon=True, name="subproc_worker_timer" + ) + self.background_thread.start() + self.last_called = monotonic() + + def quit(self) -> None: + with self.lock: + self.exit = True + + def check(self) -> None: + while True: + # We have to be sensitive on checking here, to avoid too much impact on cpu + sleep(self.sleep_time) + with self.lock: + if self.exit: + return + assert self.last_called is not None + if self.last_called + self.duration >= monotonic(): + continue + self.last_called = None + self.background_thread = None + + # Releasing lock in case self.call() takes a very long time or is reentrant + self.call() + return diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_worker/tracked_process_pool.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_worker/tracked_process_pool.py new file mode 100644 index 0000000000000000000000000000000000000000..44f2457cd5d82a9005b17bf17188e42aa4fd0e08 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_worker/tracked_process_pool.py @@ -0,0 +1,113 @@ +import atexit +import concurrent +import dataclasses +import logging +import threading +from collections.abc import Callable +from concurrent.futures import Future, ProcessPoolExecutor +from dataclasses import dataclass +from multiprocessing.context import BaseContext +from time import time +from typing import Any, TypeVar +from typing_extensions import ParamSpec + +# _thread_safe_fork is needed because the subprocesses in the pool can read +# justknobs, e.g., in the Triton compiler. For internal, the import installs +# functionality to destroy singletons before forking and re-enable them after. +import torch._thread_safe_fork # noqa: F401 + + +_P = ParamSpec("_P") +_R = TypeVar("_R") + + +log = logging.getLogger(__name__) + + +@dataclass +class _QueueStats: + # Mapping from id(future) -> start time + pending: dict[int, float] = dataclasses.field(default_factory=dict) + timing: list[float] = dataclasses.field(default_factory=list) + enqueue_count: int = 0 + dequeue_count: int = 0 + max_queue_depth: int = 0 + pool_count: int = 0 + + +# The queue statistics tracked by TrackedProcessPoolExecutor. Always grab +# _queue_stats_lock before touching. +_queue_stats = _QueueStats() +_queue_stats_lock = threading.Lock() + + +class TrackedProcessPoolExecutor(ProcessPoolExecutor): + def __init__( + self, + max_workers: int | None = None, + mp_context: BaseContext | None = None, + initializer: Callable[[], object] | None = None, + ) -> None: + with _queue_stats_lock: + _queue_stats.pool_count += 1 + super().__init__(max_workers, mp_context, initializer) + + def _record_dequeue(self, f: Future[Any]) -> None: + now = time() + with _queue_stats_lock: + stats = _queue_stats + if (start_time := stats.pending.pop(id(f), None)) is None: + return + stats.dequeue_count += 1 + duration = now - start_time + stats.timing.append(duration) + + def _record_enqueue(self, f: Future[Any]) -> None: + # Monkeypatch the set_running_or_notify_cancel so we can track when the Future moves out of PENDING. + saved_running_or_notify_cancel = f.set_running_or_notify_cancel + + def set_running_or_notify_cancel() -> Any: + self._record_dequeue(f) + return saved_running_or_notify_cancel() + + now = time() + with _queue_stats_lock: + stats = _queue_stats + stats.pending[id(f)] = now + stats.enqueue_count += 1 + stats.max_queue_depth = max(stats.max_queue_depth, len(stats.pending)) + f.set_running_or_notify_cancel = set_running_or_notify_cancel # type: ignore[method-assign] + + if f._state != concurrent.futures._base.PENDING: + self._record_dequeue(f) + + def submit( + self, fn: Callable[_P, _R], /, *args: _P.args, **kwargs: _P.kwargs + ) -> Future[_R]: + # pyrefly: ignore [bad-argument-type] + f = super().submit(fn, *args, **kwargs) + self._record_enqueue(f) + return f + + +@atexit.register +def _queue_stats_report() -> None: + stats = _queue_stats + if stats.pool_count == 0: + return + + timing = stats.timing + timing.sort() + + log.info("AsyncCompile Metrics:") + log.info(" Pools %s", stats.pool_count) + log.info( + " Items %d enqueued / %d dequeued", stats.enqueue_count, stats.dequeue_count + ) + log.info(" Max Queue Depth: %d", stats.max_queue_depth) + n = len(timing) + if n > 0: + log.info(" Longest queue time: %0.2fs", timing[-1]) + log.info(" P50: %0.2fs", timing[n // 2]) + if n >= 20: + log.info(" P95: %0.2fs", timing[n * 95 // 100]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_worker/utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_worker/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..dc6c3aff0176f2b51459e72532accae39a52df5e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compile_worker/utils.py @@ -0,0 +1,53 @@ +import os +import signal +from threading import Thread +from time import sleep + + +_IN_TOPLEVEL_PROCESS = True + + +def in_toplevel_process() -> bool: + global _IN_TOPLEVEL_PROCESS + return _IN_TOPLEVEL_PROCESS + + +# If this process dies abnormally (e.g. segfault) +# it will not shut down the workers. Instead, +# the workers will have their parent reassigned to the +# init process. This launches a separate thread to +# watch for the worker getting reassigned, +# and cleans it up in this case. +# +# This function cannot be an inner function since otherwise mp_context="spawn" would +# not work for ProcessPoolExecutor since inner functions cannot be pickled. +def _async_compile_initializer(orig_ppid: int) -> None: + import torch._C + + def run() -> None: + while True: + sleep(60) + if orig_ppid != os.getppid(): + os.kill(os.getpid(), signal.SIGKILL) + + global _watchdog_thread, _original_parent + _original_parent = orig_ppid + _watchdog_thread = Thread(target=run, daemon=True) + _watchdog_thread.start() + # Ignore Ctrl-C (i.e. SIGINT) sent to pool workers to avoid meaningless log spam. + signal.signal(signal.SIGINT, signal.SIG_IGN) + + # Install a crash handler to print out the stacktrace for SEGV + torch._C._initCrashHandler() + + # Set a bit to distinguish async_compile subprocesses from the toplevel process. + global _IN_TOPLEVEL_PROCESS + _IN_TOPLEVEL_PROCESS = False + + +_watchdog_thread: Thread | None = None +_original_parent: int | None = None + + +def has_parent_changed() -> bool: + return _original_parent != os.getppid() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compiler_bisector.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compiler_bisector.py new file mode 100644 index 0000000000000000000000000000000000000000..f3977179583ebe3c330eecbd9140da5c91cff6d2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/compiler_bisector.py @@ -0,0 +1,765 @@ +import atexit +import collections +import dataclasses +import functools +import os +import shutil +import sys +import tempfile +from collections.abc import Callable +from dataclasses import dataclass, field + +from torch._inductor.runtime.cache_dir_utils import cache_dir + + +# Set the subdirectory name +SUBDIR_NAME = "bisect" + + +@dataclass +class Subsystem: + name: str + + +@dataclass +class BisectSubsystem(Subsystem): + pass + + +@dataclass +class BinarySubsystem(Subsystem): + pass + + +@dataclass +class ConfigChange(BinarySubsystem): + name: str = field(init=False) + config_name: str + config_field: str + config_value: object + + def __post_init__(self) -> None: + self.name = f"{self.config_name}_{self.config_field}" + + +# Dictionary of backend -> subsystems +BACKENDS: dict[str, list[Subsystem]] = { + # run dynamo without aot_autograd + "eager": [], + # run dynamo with aot_autograd, but no partitioner or decomps + "aot_eager": [], + # run dynamo with aot autograd, decompositions and partitioner + "aot_eager_decomp_partition": [ + ConfigChange("aot_eager_decomp_partition", "cse", False), + BisectSubsystem( + "decomposition" + ), # number of decompositions we apply in tracing + ], # TODO - add cse ? + # applies CrossRefFakeMode on invocation + "aot_eager_decomp_partition_crossref": [], + "inductor": [ + BisectSubsystem("pre_grad_passes"), # passes applied on pre-grad IR + BisectSubsystem("joint_graph_passes"), # passes applied on joint graph + BisectSubsystem( + "post_grad_passes" + ), # passes applied individually on forward, and backward in inductor + ConfigChange("inductor", "fallback_random", True), + ConfigChange("inductor", "emulate_precision_casts", True), + ConfigChange("inductor", "layout_optimization", False), + ConfigChange("inductor", "comprehensive_padding", False), + BisectSubsystem("cudagraphs"), # cudagraph wrapping of compiled graphs + BisectSubsystem("lowerings"), # lowering aten operators to inductor + ], # TODO - add more - fusions ? +} + +subsystem_call_counter: dict[str, int] = collections.Counter() +call_counter_debug_info: dict[int, str] = {} + + +def reset_counters() -> None: + subsystem_call_counter.clear() + call_counter_debug_info.clear() + + +@functools.cache +def get_env_val(env_str: str) -> str | None: + return os.environ.get(env_str, None) + + +@dataclasses.dataclass +class BisectionResult: + """ + backend: torch.compile backend responsible for failure + subsystem: optional, registered component identified for failure + bisect_number: optional, number of times the subsystem needed to be applied to trigger failure + debug_info: associated info of the triggering bisect application of subsystem + """ + + backend: str + subsystem: str | None = None + bisect_number: int | None = None + debug_info: str | None = None + + +class CompilerBisector: + """ + This class iteratively runs torch.compile backends (eager, aot_eager, inductor) to find the + first backend that can repro an issue. + + Once it discovers the offending backend it will iteratively disable subsystems within the backend. + For subsystems which are applied repeatedly, such as the number of post grad passes or number + of lowering of nodes to inductor ir, it will bisect to find the offending application. + + The idiomatic way to run it is with `do_bisect`. You can also use it by setting the env flags + `TORCH_BISECT_BACKEND`, `TORCH_BISECT_SUBSYSTEM` and `TORCH_BISECT_MAX`. + + It also supports a CLI interface: + + python -m torch._inductor.compiler_bisector start + python -m torch._inductor.compiler_bisector good + python -m torch._inductor.compiler_bisector bad + python -m torch._inductor.compiler_bisector end + + Or use `run` to automatically bisect by running a command repeatedly: + + python -m torch._inductor.compiler_bisector run + + The command's exit code determines the result: 0 = good, non-zero = bad. + The TORCH_COMPILE_BACKEND env var is set to the backend being tested. + """ + + bisection_enabled: bool = False + + in_process_cache: str | None = None + + @classmethod + def get_dir(cls) -> str: + return f"{cache_dir() if not cls.in_process_cache else cls.in_process_cache}/{SUBDIR_NAME}" + + @classmethod + def write_lines_to_file(cls, file_path: str, lines: list[str]) -> None: + os.makedirs(os.path.dirname(file_path), exist_ok=True) + with open(file_path, "w") as file: + file.writelines(lines) + + @classmethod + def read_lines_from_file(cls, file_path: str) -> list[str]: + if os.path.exists(file_path): + with open(file_path) as file: + return file.readlines() + return [] + + @classmethod + def update_run_state( + cls, backend_name: str, subsystem: Subsystem, run_state: str + ) -> None: + file_path = os.path.join( + cls.get_dir(), backend_name, f"{subsystem.name}_run_state.txt" + ) + if isinstance(subsystem, ConfigChange): + assert run_state == "test_disable" + cls.set_config_values( + backend_name, + subsystem.name, + {subsystem.config_field: subsystem.config_value}, + ) + + cls.write_lines_to_file(file_path, [run_state]) + + @classmethod + def set_config_values( + cls, backend: str, subsystem: str, config_data: dict[str, object] + ) -> None: + file_path = os.path.join(cls.get_dir(), backend, f"{subsystem}_config.txt") + lines = [f"{k}={v}\n" for k, v in config_data.items()] + cls.write_lines_to_file(file_path, lines) + + @classmethod + def update_bisect_status(cls, backend_name: str, subsystem_name: str) -> None: + assert isinstance(subsystem_name, str) + file_path = os.path.join(cls.get_dir(), "bisect_status.txt") + lines = [f"backend={backend_name}\n", f"subsystem={subsystem_name}\n"] + cls.write_lines_to_file(file_path, lines) + + @classmethod + def update_bisect_range( + cls, backend_name: str, subsystem_name: str, low: int, high: int + ) -> None: + assert isinstance(subsystem_name, str) + file_path = os.path.join( + cls.get_dir(), backend_name, f"{subsystem_name}_bisect_range.txt" + ) + lines = [f"low={low}\n", f"high={high}\n"] + cls.write_lines_to_file(file_path, lines) + + @classmethod + def get_backend(cls) -> str | None: + """ + Returns the active backend, if any + """ + if val := get_env_val("TORCH_BISECT_BACKEND"): + return val + + file_path = os.path.join(cls.get_dir(), "bisect_status.txt") + lines = cls.read_lines_from_file(file_path) + for line in lines: + if line.startswith("backend="): + return line.strip().split("=")[1] + return None + + @classmethod + def get_subsystem(cls) -> str | None: + """ + Returns the active subsystem, if any + """ + + if val := get_env_val("TORCH_BISECT_SUBSYSTEM"): + return val + + file_path = os.path.join(cls.get_dir(), "bisect_status.txt") + lines = cls.read_lines_from_file(file_path) + for line in lines: + if line.startswith("subsystem="): + out = line.strip().split("=")[1] + return out if out else None + return None + + @classmethod + def get_subsystem_object(cls, backend_name: str, subsystem_name: str) -> Subsystem: + return next(obj for obj in BACKENDS[backend_name] if obj.name == subsystem_name) + + @classmethod + def get_run_state(cls, backend_name: str, subsystem_name: str) -> str | None: + """ + Returns the current stage of bisecting, if Any + """ + + file_path = os.path.join( + cls.get_dir(), backend_name, f"{subsystem_name}_run_state.txt" + ) + lines = cls.read_lines_from_file(file_path) + if lines: + out = lines[0].strip() + assert out in ("test_disable", "find_max_bounds", "bisect") + return out + return None + + @classmethod + def get_bisect_range( + cls, backend_name: str, subsystem_name: str + ) -> tuple[int, int]: + file_path = os.path.join( + cls.get_dir(), backend_name, f"{subsystem_name}_bisect_range.txt" + ) + lines = cls.read_lines_from_file(file_path) + low = None + high = None + # pyrefly: ignore [bad-assignment] + for line in reversed(lines): + if line.startswith("low="): + low = int(line.strip().split("=")[1]) + elif line.startswith("high="): + high = int(line.strip().split("=")[1]) + + if low is not None and high is not None: + break + + if low is None or high is None: + raise RuntimeError( + f"Trying to get bisect range when it is not set: subsystem {subsystem_name}" + ) + + return low, high + + @classmethod + def update_config_change(cls, backend: str, subsystem: ConfigChange) -> None: + file_path = os.path.join(cls.get_dir(), backend, f"{subsystem.name}_config.txt") + lines = [ + f"config_name={subsystem.config_name}\n", + f"config_field={subsystem.config_field}\n", + f"config_value={subsystem.config_value}\n", + ] + cls.write_lines_to_file(file_path, lines) + + @classmethod + def get_config_change(cls, config_name: str) -> dict[str, object] | None: + backend = cls.get_backend() + subsystem = cls.get_subsystem() + + if not backend or not subsystem: + return None + + file_path = os.path.join(cls.get_dir(), backend, f"{subsystem}_config.txt") + + if not os.path.exists(file_path): + return None + + lines = cls.read_lines_from_file(file_path) + config_data = {} + for line in lines: + key, value = line.strip().split("=", 1) + config_data[key] = eval(value) + + return config_data + + @classmethod + def delete_bisect_status(cls) -> None: + # in process_cache we have created if it exists, just the subdirectory of non created dir + dir_name = cls.in_process_cache if cls.in_process_cache else cls.get_dir() + if os.path.exists(dir_name): + shutil.rmtree(dir_name) + print("Bisection status deleted.") + else: + print("No bisection status found.") + + @classmethod + def get_system_counter(cls, name: str, increment: bool = True) -> int: + global subsystem_call_counter + curr = subsystem_call_counter[name] + if increment: + subsystem_call_counter[name] += 1 + return curr + + @classmethod + def disable_subsystem( + cls, + backend: str, + subsystem: str, + debug_info: Callable[[], str] | None = None, + ) -> bool: + if not cls.bisection_enabled: + return False + + if cls.get_backend() != backend: + return False + + if cls.get_subsystem() != subsystem: + return False + + if val := get_env_val("TORCH_BISECT_MAX"): + counter = cls.get_system_counter(subsystem, increment=True) + return counter > int(val) + + run_state = cls.get_run_state(backend, subsystem) + if run_state == "test_disable": + # First run, disable completely + return True + elif run_state == "find_max_bounds": + # Second run, update bisection range and return True to enable the subsystem + cls.update_bisect_range( + backend, + subsystem, + 0, + cls.get_system_counter(subsystem, increment=True), + ) + return False + else: + assert run_state == "bisect" + # If the environment variable is not set, use the bisection range midpoint + low, high = cls.get_bisect_range(backend, subsystem) + # if high - low <= 2: + midpoint = (low + high) // 2 + call_counter = cls.get_system_counter(subsystem) + + if ( + call_counter >= low + and call_counter <= high + and (low - high) <= 2 + and debug_info is not None + ): + call_counter_debug_info[call_counter] = debug_info() + + return call_counter > midpoint + + @classmethod + def advance_subsystem( + cls, curr_backend: str, curr_subsystem: Subsystem + ) -> Subsystem | None: + """ + Tries to move to the next subsystem within the current system. + """ + print(f"Disabling {curr_subsystem.name} did not fix the issue.") + + current_subsystems = BACKENDS[curr_backend] + current_subsystem_index = next( + i + for i, subsystem in enumerate(current_subsystems) + if subsystem.name == curr_subsystem.name + ) + + if current_subsystem_index < len(current_subsystems) - 1: + next_subsystem = current_subsystems[current_subsystem_index + 1] + cls.update_bisect_status(curr_backend, next_subsystem.name) + cls.update_run_state(curr_backend, next_subsystem, "test_disable") + print( + f"Moving to the next subsystem: {curr_backend} - {next_subsystem.name}" + ) + return next_subsystem + else: + print( + f"All subsystems in {curr_backend} have been checked. The issue is not in this system." + ) + return None + + @classmethod + def advance_backend(cls, curr_backend: str) -> str | None: + """ + Tries Move to the next backend. + """ + current_system_index = list(BACKENDS.keys()).index(curr_backend) + + if current_system_index < len(BACKENDS) - 1: + curr_backend = list(BACKENDS.keys())[current_system_index + 1] + cls.update_bisect_status(curr_backend, "") + print(f"Moving to the next system: {curr_backend}") + return curr_backend + else: + return None + + @classmethod + def process_subsystem( + cls, + curr_backend: str, + curr_subsystem: Subsystem, + fn: Callable[[], bool], + cli_interface: bool = True, + ) -> bool: + """ + Process the current subsystem. Returns True if the issue is found, False otherwise. + """ + assert isinstance(curr_subsystem, Subsystem) + while True: + run_state = cls.get_run_state(curr_backend, curr_subsystem.name) + reset_counters() + if run_state == "test_disable": + if not fn(): + next_subsystem = cls.advance_subsystem(curr_backend, curr_subsystem) + if not next_subsystem: + return False + curr_subsystem = next_subsystem + else: + if isinstance(curr_subsystem, ConfigChange): + print( + f"Setting config {curr_subsystem.config_name} field {curr_subsystem.config_field} " + f"to {curr_subsystem.config_value} fixed the issue" + ) + else: + print(f"Disabling {curr_subsystem.name} fixed the issue.") + if isinstance(curr_subsystem, BinarySubsystem): + return True + print("Starting bisect by getting upper bound.") + cls.update_run_state( + curr_backend, curr_subsystem, "find_max_bounds" + ) + elif run_state == "find_max_bounds": + if fn(): + raise RuntimeError( + f"Function succeeded with 'find_max_bounds' status for {curr_backend} - {curr_subsystem.name}." + ) + else: + _, high = cls.get_bisect_range(curr_backend, curr_subsystem.name) + print(f"Upper bound of {high} found for {curr_backend}.") + cls.update_run_state(curr_backend, curr_subsystem, "bisect") + elif run_state == "bisect": + low, high = cls.get_bisect_range(curr_backend, curr_subsystem.name) + midpoint = (low + high) // 2 + print( + f"Bisecting {curr_backend} - {curr_subsystem.name} (Range: [{low}, {high}], Midpoint: {midpoint})" + ) + if fn(): + cls.update_bisect_range( + curr_backend, curr_subsystem.name, midpoint + 1, high + ) + else: + cls.update_bisect_range( + curr_backend, curr_subsystem.name, low, midpoint + ) + low, high = cls.get_bisect_range(curr_backend, curr_subsystem.name) + if low == high: + print( + f"Binary search completed for {curr_backend} - {curr_subsystem.name}. The bisect number is {low}. " + f"Debug info: {call_counter_debug_info.get(low, 'not found')}" + ) + return True + else: + raise RuntimeError(f"Unexpected run_state {run_state}") + + if cli_interface: + sys.exit(0) + + @classmethod + def initialize_system(cls) -> None: + curr_backend = next(iter(BACKENDS.keys())) + curr_subsystem = "" + cls.update_bisect_status(curr_backend, curr_subsystem) + print(f"Starting bisection process with system: {curr_backend}") + + @classmethod + def do_bisect( + cls, fn: Callable[[], bool], cli_interface: bool = False + ) -> BisectionResult | None: + """ + Run fn repeatedly attempting to bisect torch.compile. fn should return True on success and False on failure. + """ + + # TODO graph bisecting is not well composed with lowering + # bisector so far. Use a config to opt-in + import torch._inductor.config as inductor_config + + if inductor_config.test_configs.bisect_pre_grad_graph: + BACKENDS["inductor"].insert(0, BisectSubsystem("pre_grad_graph")) + + if not cli_interface: + bisection_enabled_orig = cls.bisection_enabled + cls.delete_bisect_status() + cls.bisection_enabled = True + # Only create temp dir if not already set (CLI run mode pre-sets it) + if not cls.in_process_cache: + cls.in_process_cache = tempfile.mkdtemp() + + def cleanup() -> None: + cls.bisection_enabled = bisection_enabled_orig + cls.delete_bisect_status() + cls.in_process_cache = None + + if BACKENDS["inductor"][0].name == "pre_grad_graph": + del BACKENDS["inductor"][0] + + cleanup_handler = atexit.register(cleanup) + + class DisableBisect: + def __del__(self) -> None: + cleanup() + atexit.unregister(cleanup_handler) + + _cleanup = DisableBisect() + + curr_backend = cls.get_backend() + curr_subsystem_name = cls.get_subsystem() + + if not curr_backend: + cls.initialize_system() + curr_backend = cls.get_backend() + assert curr_backend is not None + curr_subsystem_name = cls.get_subsystem() + + curr_subsystem = ( + cls.get_subsystem_object(curr_backend, curr_subsystem_name) + if curr_subsystem_name is not None + else None + ) + while True: + assert curr_backend is not None + reset_counters() + if curr_subsystem: + result = cls.process_subsystem( + curr_backend, curr_subsystem, fn, cli_interface=cli_interface + ) + if result: + curr_subsystem = cls.get_subsystem_object( + curr_backend, + cls.get_subsystem(), # type: ignore[arg-type] + ) + + if isinstance(curr_subsystem, BinarySubsystem): + return BisectionResult( + curr_backend, + curr_subsystem.name, + 0, + curr_subsystem.name, + ) + + low, _ = cls.get_bisect_range(curr_backend, curr_subsystem.name) + return BisectionResult( + curr_backend, + curr_subsystem.name, + low, + call_counter_debug_info.get(low), + ) + + next_subsystem = cls.advance_subsystem(curr_backend, curr_subsystem) + if not next_subsystem: + print( + f"The issue is in the {curr_backend} system, but could not identify subsystem." + ) + assert curr_backend is not None + return BisectionResult(curr_backend) + + curr_subsystem = next_subsystem + else: + if fn(): + next_backend = cls.advance_backend(curr_backend) + if not next_backend: + print("All systems have been checked.") + return None + + curr_backend = next_backend + else: + current_subsystems = BACKENDS[curr_backend] + if current_subsystems: + curr_subsystem = current_subsystems[0] + cls.update_bisect_status(curr_backend, curr_subsystem.name) + cls.update_run_state( + curr_backend, curr_subsystem, "test_disable" + ) + print( + f"The issue is in the {curr_backend} system. Moving to the first subsystem: {curr_subsystem}" + ) + else: + print(f"The issue is in the {curr_backend} system.") + return BisectionResult(curr_backend) + + if cli_interface: + sys.exit(0) + + +HELP_TEXT = """\ +Usage: python -m torch._inductor.compiler_bisector + +Commands: + start Start a new bisection session (manual mode) + good Mark the current state as good (no issue) + bad Mark the current state as bad (issue present) + end End the bisection session and clean up + run Automatically bisect by running a command repeatedly + Exit code 0 = good, non-zero = bad + +Example - using 'run' for automatic bisection: + + 1. Create a test script (e.g., test_bug.py): + + import os, sys, torch + + def main(): + torch._dynamo.reset() + # Use the backend the bisector is testing + backend = os.environ.get("TORCH_COMPILE_BACKEND", "inductor") + + @torch.compile(backend=backend) + def fn(x): + return x.sin() # or your failing operation + + x = torch.randn(10) + compiled = fn(x) + expected = x.sin() # eager reference + + if torch.allclose(compiled, expected, rtol=1e-4, atol=1e-4): + return 0 # PASS + else: + print(f"FAIL: got {compiled}, expected {expected}") + return 1 # FAIL + + if __name__ == "__main__": + sys.exit(main()) + + 2. Run the bisector: + + python -m torch._inductor.compiler_bisector run python test_bug.py + + 3. The bisector will: + - Test backends (eager, aot_eager, inductor) to find which fails + - Test subsystems within that backend (passes, lowerings, etc.) + - Binary search to find the exact operation causing the issue + + Output: + Disabling lowerings fixed the issue. + Binary search completed for inductor - lowerings. The bisect number is 42. + Bisection complete: BisectionResult(backend='inductor', subsystem='lowerings', ...) + +Environment variables set for your test script: + TORCH_COMPILE_BACKEND - The backend being tested (use this in torch.compile) +""" + + +def command_line_usage() -> None: + """Entry point for the compiler bisector command-line interface.""" + if len(sys.argv) < 2: + print(HELP_TEXT) + sys.exit(1) + + bisection_manager = CompilerBisector() + command = sys.argv[1] + + if command == "end": + bisection_manager.delete_bisect_status() + sys.exit(0) + + if command == "start": + bisection_manager.delete_bisect_status() + bisection_manager.initialize_system() + sys.exit(0) + + if command == "run": + if len(sys.argv) < 3: + print( + "Usage: python -m torch._inductor.compiler_bisector run [args...]" + ) + sys.exit(1) + + import subprocess + + run_cmd = sys.argv[2:] + + def test_function() -> bool: + # Pass bisection state to subprocess via environment variables + env = os.environ.copy() + backend = bisection_manager.get_backend() + subsystem = bisection_manager.get_subsystem() + + if backend: + # For test script to select the right backend + env["TORCH_COMPILE_BACKEND"] = backend + # For bisector in subprocess to know which backend we're testing + env["TORCH_BISECT_BACKEND"] = backend + + if subsystem: + assert backend is not None # subsystem requires a backend + env["TORCH_BISECT_SUBSYSTEM"] = subsystem + # Get run_state to determine TORCH_BISECT_MAX + run_state = bisection_manager.get_run_state(backend, subsystem) + if run_state == "test_disable": + # -1 means always disable (counter > -1 is always True) + env["TORCH_BISECT_MAX"] = "-1" + # For find_max_bounds and bisect, let the subprocess use file-based + # mechanisms. The subprocess reads run_state and bisect_range from + # files, and writes the actual count during find_max_bounds. + + result = subprocess.run(run_cmd, env=env) + return result.returncode == 0 + + bisection_manager.delete_bisect_status() + bisection_manager.bisection_enabled = True + # Use shared cache_dir instead of temp dir so subprocesses can access files + CompilerBisector.in_process_cache = cache_dir() + result = bisection_manager.do_bisect(test_function, cli_interface=False) + if result: + print(f"\nBisection complete: {result}") + else: + print("\nBisection complete: no issue found") + sys.exit(0) + + if command not in ["good", "bad"]: + print(f"Invalid command: {command}") + print("Must be 'good', 'bad', 'start', 'end', or 'run'.") + sys.exit(1) + + def test_function() -> bool: + return command == "good" + + if not bisection_manager.get_backend(): + raise ValueError("Must call start prior to good or bad") + + bisection_manager.do_bisect(test_function, cli_interface=True) + + +def get_is_bisection_enabled() -> bool: + return ( + CompilerBisector.get_subsystem() is not None + or CompilerBisector.get_backend() is not None + ) + + +CompilerBisector.bisection_enabled = get_is_bisection_enabled() + +if __name__ == "__main__": + command_line_usage() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/config.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/config.py new file mode 100644 index 0000000000000000000000000000000000000000..f68171baef7c0ec63462bd1b85891a319b32a93b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/config.py @@ -0,0 +1,2735 @@ +import os +import sys +from collections.abc import Callable +from typing import Any, cast, Literal, TYPE_CHECKING + +import torch +import torch._inductor.custom_graph_pass +from torch._environment import is_fbcode +from torch.utils._config_module import ( + Config, + get_tristate_env, + inherit_fields_from, + install_config_module, +) + + +if TYPE_CHECKING: + from torch._inductor.choices import InductorChoices + from torch._inductor.cudagraph_utils import CUDAGraphPolicy + +inplace_padding = os.environ.get("TORCHINDUCTOR_INPLACE_PADDING", "1") == "1" +can_inplace_pad_graph_input = False # ease testing + + +def fx_graph_remote_cache_default() -> bool | None: + return get_tristate_env("TORCHINDUCTOR_FX_GRAPH_REMOTE_CACHE") + + +def vec_isa_ok_default() -> bool | None: + if os.environ.get("TORCHINDUCTOR_VEC_ISA_OK") == "1": + return True + if os.environ.get("TORCHINDUCTOR_VEC_ISA_OK") == "0": + return False + return None + + +def autotune_remote_cache_default() -> bool | None: + return get_tristate_env("TORCHINDUCTOR_AUTOTUNE_REMOTE_CACHE") + + +def bundled_autotune_remote_cache_default() -> bool | None: + return get_tristate_env("TORCHINDUCTOR_BUNDLED_AUTOTUNE_REMOTE_CACHE") + + +def bundle_triton_into_fx_graph_cache_default() -> bool | None: + return get_tristate_env( + "TORCHINDUCTOR_BUNDLE_TRITON_INTO_FX_GRAPH_CACHE", + True if not is_fbcode() else None, + ) + + +def autotune_at_compile_time_default() -> bool | None: + return get_tristate_env("TORCHINDUCTOR_AUTOTUNE_AT_COMPILE_TIME") + + +def static_cuda_launcher_default() -> bool: + STATIC_CUDA_LAUNCHER_VERSION = 2 + + if "TORCHINDUCTOR_USE_STATIC_CUDA_LAUNCHER" in os.environ: + return os.environ.get("TORCHINDUCTOR_USE_STATIC_CUDA_LAUNCHER") == "1" + elif is_fbcode(): + version = torch._utils_internal.justknobs_getval_int( + "pytorch/inductor:static_cuda_launcher_version" + ) + return version <= STATIC_CUDA_LAUNCHER_VERSION + else: + # Default true in OSS + return True + + +def prologue_fusion_enabled() -> bool: + ENABLE_PROLOGUE_FUSION_VERSION = 0 + + if "TORCHINDUCTOR_PROLOGUE_FUSION" in os.environ: + return os.environ.get("TORCHINDUCTOR_PROLOGUE_FUSION") == "1" + elif is_fbcode(): + jk_name = "pytorch/inductor:prologue_fusion_version" + version = torch._utils_internal.justknobs_getval_int(jk_name) + return version <= ENABLE_PROLOGUE_FUSION_VERSION + else: + return True + + +# Enable auto_functionalized_v2 (enabled by default) +enable_auto_functionalized_v2 = ( + os.environ.get("TORCHDYNAMO_AUTO_FUNCTIONALIZED_V2", "1") == "1" +) + +# add some debug printouts +debug = False + +# Whether to disable a progress bar for autotuning +disable_progress = True + +# Whether to enable printing the source code for each future +verbose_progress = False + +# Configurable compile worker logging path for subproc_pool +worker_log_path = ( + "/logs/dedicated_log_torch_compile_worker_rank" if is_fbcode() else None +) + +# precompilation timeout +precompilation_timeout_seconds: int = int( + os.environ.get("TORCHINDUCTOR_PRECOMPILATION_TIMEOUT_SECONDS", 60 * 5) +) + +# use fx aot graph codegen cache +fx_graph_cache: bool = Config( + justknob="pytorch/remote_cache:enable_local_fx_graph_cache", + env_name_default="TORCHINDUCTOR_FX_GRAPH_CACHE_DEFAULT", + env_name_force="TORCHINDUCTOR_FX_GRAPH_CACHE", + default=True, +) + +remote_gemm_autotune_cache: bool = False + +# use remote fx aot graph codegen cache +# False: Disables the cache +# True: Enables the cache +# None: Not set -- Off for OSS, JustKnobs based for internal +fx_graph_remote_cache: bool | None = fx_graph_remote_cache_default() + +# should we bundle triton caching into fx graph cache +bundle_triton_into_fx_graph_cache: bool | None = ( + bundle_triton_into_fx_graph_cache_default() +) + +non_blocking_remote_cache_write: bool = Config( + justknob="pytorch/remote_cache:enable_non_blocking_remote_cache_write_v2", + env_name_force="TORCHINDUCTOR_NON_BLOCKING_REMOTE_CACHE_WRITE", + default=True, +) + +# Enable autotune local cache. +# +# See bundled_autotune_remote_cache for the effect this flag has on the bundled +# remote cache. +autotune_local_cache: bool = True + +# Enable autotune remote cache. +# +# Enables/disables the autotune remote cache regardless of the state of +# autotune_local_cache. If both local and remote are enabled then on write both +# are written and on read local is checked first and only on a cache miss is +# remote read. +# +# False: Disables the cache +# True: Enables the cache +# None: Not set -- Off for OSS, JustKnobs based for internal +autotune_remote_cache: bool | None = autotune_remote_cache_default() + +# Enable bundled autotune cache. +# +# Enables/disables the bundled autotune cache regardless of the state of +# autotune_remote_cache. However it does depend on the local cache for local +# state management - as a result if the local cache is disabled this will also +# disable the bundled autotune cache. +# +# False: Disables the cache +# True: Enables the cache (requires autotune_local_cache) +# None: Not set -- Off for OSS, JustKnobs based for internal +bundled_autotune_remote_cache: bool | None = bundled_autotune_remote_cache_default() + +# See torch.compiler.config.force_disable_caches +force_disable_caches: bool = Config(alias="torch.compiler.config.force_disable_caches") + +# Unsafe way to skip dynamic shape guards to get faster cache load +unsafe_skip_cache_dynamic_shape_guards: bool = False + +# Unsafe way to mark non torch functions as safe to cache +# dictionary is from function name -> cache key +# Any function name in the dictionary will be allowed to be cacheable +# by AOTAutogradCache and FxGraphCache. +# changing the cache key value will change the resulting +# FXGraphCache key. +# Example usage: +# torch._inductor.config.unsafe_marked_cacheable_functions = { +# 'torch.ops.my_function' : torch.__version__ +# } +# The above example causes the custom op torch.ops.my_function to be cacheable, +# and for cache keys to be keyed by the current torch version +unsafe_marked_cacheable_functions: dict[str, str] = {} + +# sleep in inductor for testing +sleep_sec_TESTING_ONLY: int | None = None + +# The default layout constraint for user-defined triton kernels. +# See "The default layout constraint for custom operators" for options. +triton_kernel_default_layout_constraint: Literal[ + "needs_fixed_stride_order", "flexible_layout" +] = "needs_fixed_stride_order" + +# use cpp wrapper instead of python wrapper +# incompatible with disable_cpp_codegen +cpp_wrapper: bool = os.environ.get("TORCHINDUCTOR_CPP_WRAPPER", "0") == "1" + +# controls whether to compile entry and kernel separately for cpp_wrapper mode. +# turn on this option to compile entry and kernel separately and minimize compile time of the entry part. +# see https://github.com/pytorch/pytorch/pull/148773 +# Note: compiling entry and kernel separately may have a non-negligible impact on the performance. +# see https://github.com/pytorch/pytorch/issues/156037 +cpp_wrapper_build_separate: bool = ( + os.environ.get("TORCHINDUCTOR_CPP_WRAPPER_BUILD_SEPARATE", "0") == "1" +) + +fx_wrapper: bool = os.environ.get("TORCHINDUCTOR_FX_WRAPPER", "0") == "1" + +# Controls automatic precompiling of common include files for codecache.CppCodeCache +# (i.e. for cpp_wrapper mode and for cpp kernels on CPU). AOTI header precompiling is +# controlled by a separate flag. +cpp_cache_precompile_headers: bool = ( + os.environ.get( + "TORCHINDUCTOR_CPP_CACHE_PRECOMPILE_HEADERS", "0" if is_fbcode() else "1" + ) + == "1" +) + +online_softmax = os.environ.get("TORCHINDUCTOR_ONLINE_SOFTMAX", "1") == "1" + +apply_gumbel_max_trick = ( + os.environ.get("TORCHINDUCTOR_APPLY_GUMBEL_MAX_TRICK", "1") == "1" +) + +# dead code elimination +dce = False + +# assume weight tensors are fixed size +static_weight_shapes = True + +# put correctness assertions in generated code +size_asserts = os.environ.get("TORCHINDUCTOR_SIZE_ASSERTS", "1") == "1" +nan_asserts = os.environ.get("TORCHINDUCTOR_NAN_ASSERTS") == "1" +runtime_triton_nan_asserts = ( + os.environ.get("TORCHINDUCTOR_RUNTIME_TRITON_NAN_ASSERTS") == "1" +) +scalar_asserts = os.environ.get("TORCHINDUCTOR_SCALAR_ASSERTS", "1") == "1" + +# Disable by default in fbcode +alignment_asserts = ( + os.environ.get("TORCHINDUCTOR_ALIGNMENT_ASSERTS", "0" if is_fbcode() else "1") + == "1" +) + +# enable loop reordering based on input orders +pick_loop_orders = True + +# reuse a kernel input as the output +inplace_buffers = True + +# reuse a buffer for an unrelated purpose +allow_buffer_reuse = True + +# Enable pooled allocations for non-output tensors +memory_planning = os.environ.get("TORCHINDUCTOR_MEMORY_PLANNING", "0") == "1" + +# Enable to allow using ftz variant of exponenet instruction in triton codegen. +use_fast_math = os.environ.get("TORCHINDUCTOR_USE_FAST_MATH") == "1" + +# How to organize memory under memory_planning=True: +# - "none": do not try to pool storage, just reuse +# - "intermediates": all non-outputs share storage, outputs each get unique storage +# - "outputs": two pools, one for intermediates (freed on return) and one for outputs +# - "combined": a single pool for both intermediates and outputs +# pyrefly: ignore [bad-assignment] +memory_pool: Literal["none", "intermediates", "outputs", "combined"] = os.environ.get( + "TORCHINDUCTOR_MEMORY_POOL", "intermediates" +) # type: ignore[assignment] + +# codegen benchmark harness +benchmark_harness = True + +# fuse pointwise into templates epilogues +epilogue_fusion = True + +# fuse pointwise into template prologues +prologue_fusion = prologue_fusion_enabled() + +# do epilogue fusions before other fusions +epilogue_fusion_first = False + +# do epilogue fusions for user defined triton kernels +epilogue_fusion_user_defined_triton_kernel = False + +# enable pattern match+replace optimizations +pattern_matcher = True + +# set to True to enable the back-to-back GEMM pass +b2b_gemm_pass = False + +# register custom graph optimization pass hook. so far, pre/post passes are +# only applied before/after pattern_matcher in post_grad_passes. +# +# Implement CustomGraphPass to allow Inductor to graph compiled artifacts +# to which your custom passes have been applied: +post_grad_custom_pre_pass: torch._inductor.custom_graph_pass.CustomGraphPassType = None +post_grad_custom_post_pass: torch._inductor.custom_graph_pass.CustomGraphPassType = None + +# Allow users to pass in custom partition function +custom_partitioner_fn: torch._inductor.custom_graph_pass.CustomPartitionerFnType = None + +# Registers a custom joint graph pass. +joint_custom_pre_pass: torch._inductor.custom_graph_pass.CustomGraphPassType = None +joint_custom_post_pass: torch._inductor.custom_graph_pass.CustomGraphPassType = None + +# Registers a custom pregrad pass. Note that the pre-grad IR is 1. +# non-functional, 2. non-normalized, and 3. prone to change. Ideally we should +# use post-grad passes. +pre_grad_custom_pass: torch._inductor.custom_graph_pass.CustomGraphPassType = None + +# Registers a custom pass to be run right before fusion in Inductor scheduler. +# WARNING: Inductor scheduler IR is at prototype stage and subject to change, +# hence custom IR passes built on top of it might break in the future. +_pre_fusion_custom_pass: ( + Callable[ + [list["torch._inductor.scheduler.BaseSchedulerNode"]], + list["torch._inductor.scheduler.BaseSchedulerNode"], + ] + | None +) = None + +# Registers a custom pass to be run right after fusion in Inductor scheduler. +# WARNING: Inductor scheduler IR is at prototype stage and subject to change, +# hence custom IR passes built on top of it might break in the future. +_post_fusion_custom_pass: ( + Callable[ + [list["torch._inductor.scheduler.BaseSchedulerNode"]], + list["torch._inductor.scheduler.BaseSchedulerNode"], + ] + | None +) = None + +# Deprecated +split_cat_fx_passes = True + +# Optimize conv-batchnorm if batchnorm is in eval mode. Slightly reduces numerical stability. +efficient_conv_bn_eval_fx_passes = False + +# Enable predispatch aten IR for export +is_predispatch = False + +# Deprecated +group_fusion = False + +# Deprecated +batch_fusion = True + +# Pre grad fusion and options in order, set to empty dict to disable fusion. +# Call `torch._inductor.fx_passes.group_batch_fusion.list_group_batch_fusions()` to see available fusions. +# batch fusion options: +# batch_linear +# batch_linear_lhs +# batch_layernorm +# batch_tanh +# batch_relu +# batch_sigmoid + +# split cat fusion options: +# normalization_pass +# remove_split_with_size_one_pass +# merge_getitem_cat_pass +# merge_stack_tahn_unbind +# merge_splits_pass +# mutate_cat_pass +# split_cat_pass +pre_grad_fusion_options: dict[str, dict[str, Any]] = {} + +# Post grad fusion and options, set to empty dict to disable fusion. +# Call `torch._inductor.fx_passes.group_batch_fusion.list_group_batch_fusions(False)` to see available fusions. +post_grad_fusion_options: dict[str, dict[str, Any]] = {} + +# enable reordering pass for improving memory locality +reorder_for_locality = True + +# Scale down Rn_BLOCK for better occupancy +dynamic_scale_rblock = os.environ.get("TORCHINDUCTOR_DYNAMIC_SCALE_RBLOCK", "1") == "1" + +# this forces fusion for int_mm with mul. Needed when you want to avoid realizing the int32 +# but the mul gets fused with other pointwise ops instead. +force_fuse_int_mm_with_mul = False + +# Prevent unfusing addmm into mm+add for bf16/fp16 to avoid precision loss +# from extra truncation at the mm output. Set to False to allow unfusing +# (may improve perf at the cost of accuracy for some models). +keep_addmm_fused_for_half_dtypes = True + +# DEPRECATED. This setting is ignored. +use_mixed_mm = True + +# enable runtime numeric check for pre/post grad fx passes +# floating point provides limited accuracy (about 7 decimal digits for single precision +# floating point numbers,about 16 decimal digits for double precision floating point numbers) +# according to PyTorch documentation. +# https://pytorch.org/docs/stable/notes/numerical_accuracy.html#batched-computations-or-slice-computations +fx_passes_numeric_check: dict[str, Any] = { + "pre_grad": False, + "precision": 1e-4, + "num_iterations": 1, + "requires_optimizer": True, +} + +# DEPRECATED. This setting is ignored. +mixed_mm_choice: Literal["default", "triton", "aten", "heuristic"] = "heuristic" + +# enable reordering pass for increasing overlap between compute and communication +reorder_for_compute_comm_overlap = False + +# passes (in execution order) for increasing overlap between compute and communication +# for built-in passes, use string name; for user-defined passes, pass in the function handle +# WARNING: Inductor scheduler IR is at prototype stage and subject to change, +# hence custom IR passes built on top of it might break in the future. +# +# See aten_distributed_optimizations, it is recommended way for distributed optimizations. +# +# Recommended configuration for reorder_for_compute_comm_overlap_passes: +# [ +# "reorder_communication_preserving_peak_memory", +# "sink_waits_iterative", +# "reorder_communication_preserving_peak_memory", +# ] +reorder_for_compute_comm_overlap_passes: list[ + str + | Callable[ + [list["torch._inductor.scheduler.BaseSchedulerNode"]], + list["torch._inductor.scheduler.BaseSchedulerNode"], + ] +] = [] + +# Maximum number of positions to advance a given collective, unlimited by default +reorder_prefetch_limit: int | None = None + +# enable operator reordering for peak memory optimization +reorder_for_peak_memory = True +reorder_for_peak_memory_debug = False + +# In some cases, when all the nodes that can be scheduled are quite large, +# it is beneficial to switch the scheduling strategy. So instead of using +# size as the criterion, we choose a node that can unlock more nodes to +# become schedulable by analyzing their successor nodes. The default value +# is zero, which turns off this optimization. +size_threshold_for_succ_based_strategy: int = 0 + + +bucket_all_gathers_fx: Literal["none", "all", "only_fsdp"] = "none" +# By default torch._inductor.fx_passes.bucketing.bucket_size_determinator is used +bucket_all_gathers_fx_bucket_size_determinator: Callable[[int], int] | None = None + +bucket_all_gathers_bucket_mode: Literal[ + "default", "custom_ops", "custom_ops_multidtype" +] = "default" + +bucket_reduce_scatters_fx: Literal["none", "all"] = "none" +# By default torch._inductor.fx_passes.bucketing.bucket_size_determinator is used +bucket_reduce_scatters_fx_bucket_size_determinator: Callable[[int], int] | None = None + +bucket_reduce_scatters_bucket_mode: Literal[ + "default", "custom_ops", "custom_ops_multidtype" +] = "default" + + +bucket_all_reduces_fx: Literal["none", "all"] = "none" +# By default torch._inductor.fx_passes.bucketing.bucket_size_determinator is used +bucket_all_reduces_fx_bucket_size_determinator: Callable[[int], int] | None = None + +# runtime estimation function for ops +# for built-in estimation function, pass in "default"; for user-defined estimation function, pass in the function handle +estimate_op_runtime = "default" + +runtime_estimations_mms_benchmark: bool = False + +# unit: GB/s, uni-directional P2P bandwidth per card +# default value is NVLink +intra_node_bw = 300 + +# unit: GB/s, uni-directional P2P bandwidth per node +# default value is InfiniBand +inter_node_bw = 25 + +# unit: GB/s, uni-directional CPU<>GPU bandwidth +# default value is PCIe; modify for your hardware or measured bandwidth +cpu_gpu_bw = 50.0 + +# use Inductor's experimental benchmarker (runtime/benchmarking.py) +# to benchmark kernels during autotuning, otherwise fall back to +# Triton's `do_bench`. the experimental benchmarker may produce +# results that are not consistent with `do_bench`'s results +use_experimental_benchmarker: bool = Config( + default=True, + env_name_force="TORCHINDUCTOR_USE_EXPERIMENTAL_BENCHMARKER", + justknob="pytorch/inductor:use_experimental_benchmarker", +) + +# Enable distributed autotuning. When this is enabled we will distribute the +# autotuning across distributed ranks in the same program group - so instead of +# each rank autotuning every kernel they only autotune 1/world size kernels and +# then share the results. +distributed_max_autotune_gemm = ( + os.environ.get("TORCHINDUCTOR_DISTRIBUTED_MAX_AUTOTUNE_GEMM") == "1" +) + +# Pipeline autotuning for max-autotune-gemm. Overlap lowering and benchmarking on GPU +pipeline_max_autotune_gemm = ( + os.environ.get("TORCHINDUCTOR_PIPELINE_GEMM_AUTOTUNING") == "1" +) + +# enable slow autotuning passes to select algorithms +max_autotune = os.environ.get("TORCHINDUCTOR_MAX_AUTOTUNE") == "1" + +# enable slow autotuning passes to select pointwise/reductions algorithms +max_autotune_pointwise = os.environ.get("TORCHINDUCTOR_MAX_AUTOTUNE_POINTWISE") == "1" + +# enable slow autotuning passes to select gemm algorithms +max_autotune_gemm = os.environ.get("TORCHINDUCTOR_MAX_AUTOTUNE_GEMM") == "1" + +inductor_default_autotune_warmup = int( + os.getenv("TORCHINDUCTOR_DEFAULT_AUTOTUNE_WARMUP", 25) +) +inductor_default_autotune_rep = int( + os.getenv("TORCHINDUCTOR_DEFAULT_AUTOTUNE_REP", 100) +) + + +# Modifies the number of autotuning choices displayed, set to None for all +def _autotune_num_choices_displayed_default() -> int | None: + env_val = os.environ.get("TORCHINDUCTOR_AUTOTUNE_NUM_CHOICES_DISPLAYED") + if env_val is None: + return 10 + if env_val.lower() in ("none", "all"): + return None + return int(env_val) + + +autotune_num_choices_displayed: int | None = _autotune_num_choices_displayed_default() + +# Report the autotune choices and their benchmark results. Default is True. +max_autotune_report_choices_stats = ( + os.environ.get("TORCHINDUCTOR_MAX_AUTOTUNE_REPORT_CHOICES_STATS", "1") == "1" +) + +# Prune configs that have a theoretical maximum shared memory usage than the hardware limit +# Will over-prune - pruning some valid configs with theoretical shared memory usage higher +# than real shared memory usage, ensuring that invalid configs are not possibly autotuned +max_autotune_prune_choices_based_on_shared_mem = ( + os.environ.get("TORCHINDUCTOR_MAX_AUTOTUNE_PRUNE_CHOICES_BASED_ON_SHARED_MEM", "0") + == "1" +) + +# Disable triton from trying to initialize and detect devices on the host +triton_disable_device_detection = ( + os.environ.get("TORCHINDUCTOR_TRITON_DISABLE_DEVICE_DETECTION", "0") == "1" +) + +# enable inductor graph partition to allow multiple inductor graphs for the same dynamo graph +graph_partition: bool = ( + os.environ.get("TORCHINDUCTOR_GRAPH_PARTITION", "1" if not is_fbcode() else "0") + == "1" +) + +# Pluggable CUDAGraph wrapping policy. When set to a ``CUDAGraphPolicy`` +# instance, ``post_compile`` delegates cudagraph wrapping to the policy +# instead of the built-in ``cudagraphify`` pipeline. This allows custom +# cudagraph implementations, selective inner-vs-outer wrapping for +# regional compilation, and shared memory pool management. +# +# See ``torch._inductor.cudagraph_utils.CUDAGraphPolicy`` for the base class. +cudagraph_policy: "CUDAGraphPolicy | None" = None + +# register ops upon which inductor should partition the graph. name format should be +# "namespace::kernel_name" (e.g., aten::mm) for op overload packet, or +# "namespace::kernel_name.overload" (e.g., aten::mm.default). +custom_should_partition_ops: list[str] = [] + +# register ops whose OUTPUT unbacked symints should cause partition. Any tensors or ops +# that use these output unbacked symints (e.g. in their shapes, strides, or offsets) +# will be excluded from cudagraph partitions. This is useful for operators that produce +# data-dependent unbacked symints (e.g., from a custom op that returns a SymInt). +# Note: Input symints to these ops remain cudagraph-safe; only the output symints are +# marked as cudagraph-unsafe. Name format should be "namespace::kernel_name" +# (e.g., mylib::get_split_point) for op overload packet, or +# "namespace::kernel_name.overload" for specific overloads. +cudagraph_unsafe_unbacked_ops: list[str] = [] + +# whether template autotuning should allow flexible layouts if possible (e.g. only extern choices) +max_autotune_allow_flexible_layouts: bool = False + +# force cublas and triton to use the same precision; cublas supports TF32 for matmul operations +# when m, n, k are multiples of 16, 16, 8, whereas triton supports TF32 for matmul operations +# for any combinations of m, n, k, regardless of their alignment. setting this flag will ensure +# that triton does not use TF32 wherever cublas would not use TF32 +# DEPRECATED. cuBLAS no longer has the above alignment requirements. will remove in the future. +force_same_precision: bool = Config( + justknob="pytorch/compiler:force_same_precision", + env_name_force="TORCHINDUCTOR_FORCE_SAME_PRECISION", + default=False, +) + +# Size hints for multi-kernel dispatch. +# A reasonable default value of this config would be [64, 256, 4096] +# TODO: @bobrenjc93 to roll this out to a few internal models to ensure this works +# as expected before turning it on for everyone. +multi_kernel_hints: list[int] = [] + + +# Specify candidate backends for gemm autotune. +# Possible choices are combinations of: ATen, Triton, CUTLASS, CUTEDSL, NVGEMM, CK, CKTILE, CPP. +# ATen: default Pytorch ATen kernels. +# Triton: Triton templates defined in torch inductor (AMD and NVidia GPUs). +# CUTLASS: Cutlass templates and kernels (NVidia GPUs only). +# CUTEDSL: CuteDSL templates for Blackwell GPUs (NVidia SM100-SM109 only). +# NVGEMM: NVIDIA Universal GEMM via cutlass_api (NVidia GPUs only). +# CK: Composable Kernel templates and kernels (AMD Instinct GPUs only). +# CKTILE: Composable Kernel templates and kernels, new API (AMD Instinct GPUs only). +# CPP: CPP templates and kernels for CPU. +max_autotune_gemm_backends = os.environ.get( + "TORCHINDUCTOR_MAX_AUTOTUNE_GEMM_BACKENDS", "ATEN,TRITON,CPP" +).upper() + + +# Configures the maximum number of NVIDIA Universal GEMM (NVGEMM) configs to profile +# in max_autotune. By default it's 5, to keep compile time reasonable. +# Set to None (or env var "none"/"all") to tune all configs. +def _nvgemm_max_profiling_configs_default() -> int | None: + env_val = os.environ.get("TORCHINDUCTOR_NVGEMM_MAX_PROFILING_CONFIGS", "5") + if env_val.lower() in ("none", "all"): + return None + return int(env_val) + + +nvgemm_max_profiling_configs: int | None = _nvgemm_max_profiling_configs_default() + + +# As above, specify candidate backends for conv autotune. +# NB: in some cases for 1x1 convs we emit as matmul, +# which will use the backends of `max_autotune_gemm_backends` +max_autotune_conv_backends = os.environ.get( + "TORCHINDUCTOR_MAX_AUTOTUNE_CONV_BACKENDS", "ATEN,TRITON" +).upper() + + +# Specify the size of the search space for GEMM autotuning. +# DEFAULT - balance between compile time overhead and performance +# EXHAUSTIVE - maximize performance +# pyrefly: ignore [bad-assignment] +max_autotune_gemm_search_space: Literal["DEFAULT", "EXHAUSTIVE"] = os.environ.get( + "TORCHINDUCTOR_MAX_AUTOTUNE_GEMM_SEARCH_SPACE", "DEFAULT" +).upper() # type: ignore[assignment] + +# Specify the size of the search space for flex attention autotuning. +# DEFAULT - balance between compile time overhead and performance +# EXHAUSTIVE - maximize performance +# pyrefly: ignore [bad-assignment] +max_autotune_flex_search_space: Literal["DEFAULT", "EXHAUSTIVE"] = os.environ.get( + "TORCHINDUCTOR_MAX_AUTOTUNE_FLEX_SEARCH_SPACE", "DEFAULT" +).upper() # type: ignore[assignment] + + +# Fall back to ATen for all ops by default, except those nodes that users explicitly +# annotated with regional inductor compile. Please read torch.fx.passes.regional_inductor +# on to explicitly annotate. This is currently only used by inductor lite mode. +# Different from default inductor mode that fuses all nodes, this config enables an +# opt-in mode that only fuse for user-specified nodes. The motivation is to provide +# guaranteed numeric correctness and give full control to users. +fallback_by_default: bool = False + + +# This config allows selective decomposition of certain operators in the graph. +# Currently the only use case is to patch the same-name config in functorch, for +# inductor lite mode. See more details in [Note: Selective Decomposition] +selective_decompose: bool = False + + +# Use dead code elimination +use_dce: bool = True + + +# Use fx graph passes +use_pre_grad_passes: bool = True + +# "early": pre-grad passes run before cache lookup (every compile). +# "late": pre-grad passes run after cache lookup (only on cache miss); +# requires custom passes to implement uuid() for the cache key. +# "default": resolves to "late" when possible (no custom pass, or custom pass +# with uuid), falls back to "early" otherwise. +pre_grad_pass_timing: Literal["early", "late", "default"] = "default" + + +use_joint_graph_passes: bool = True +use_post_grad_passes: bool = True + + +cutedsl_enable_autotuning: bool = ( + os.environ.get("CUTEDSL_ENABLE_AUTOTUNING", "0") == "1" +) + +# DEPRECATED. This setting is ignored. +autotune_fallback_to_aten = False + +# the value used as a fallback for the unbacked SymInts +# that can appear in the input shapes (e.g., in autotuning) +unbacked_symint_fallback = 8192 + +# DEPRECATED. This setting is ignored. +search_autotune_cache = False + +save_args = os.environ.get("TORCHINDUCTOR_SAVE_ARGS") == "1" + +# We will disable creating subprocess for autotuning if this is False +autotune_in_subproc = os.environ.get("TORCHINDUCTOR_AUTOTUNE_IN_SUBPROC") == "1" + +# The following three timeouts are applicable if autotune_in_subproc is True: + +# Max time that a valid benchmark result may take during autotuning +max_autotune_subproc_result_timeout_seconds = 60.0 +# DEPRECATED. This setting is ignored. +max_autotune_subproc_graceful_timeout_seconds = 0.0 +# DEPRECATED. This setting is ignored. +max_autotune_subproc_terminate_timeout_seconds = 0.0 + +# If autotuning in subprocess, whether to use multiple devices +autotune_multi_device = os.environ.get("TORCHINDUCTOR_AUTOTUNE_MULTI_DEVICE") == "1" + +# Number of benchmark runs for collective operations +collective_benchmark_nruns = int( + os.environ.get("TORCHINDUCTOR_COLLECTIVE_BENCHMARK_NRUNS", "50") +) + +# Timeout in seconds for collective benchmarking +collective_benchmark_timeout = float( + os.environ.get("TORCHINDUCTOR_COLLECTIVE_BENCHMARK_TIMEOUT", "30") +) + +coordinate_descent_tuning = ( + os.environ.get("TORCHINDUCTOR_COORDINATE_DESCENT_TUNING") == "1" +) +coordinate_descent_check_all_directions = ( + os.environ.get("TORCHINDUCTOR_COORDINATE_DESCENT_CHECK_ALL_DIRECTIONS") == "1" +) +coordinate_descent_search_radius = int( + os.environ.get("TORCHINDUCTOR_COORDINATE_DESCENT_RADIUS", "1") +) + +# AutoHeuristic is a framework that allows one to collect data from autotuning, use the data to learn a heuristic, and +# generate the learned heuristic to code which is shipped with the compiler + + +def _parse_autoheuristic_collect_env(): + collect_env = os.environ.get("TORCHINDUCTOR_AUTOHEURISTIC_COLLECT", "").split(",") + return collect_env + + +def _parse_autoheuristic_use_env(): + use_env = os.environ.get("TORCHINDUCTOR_AUTOHEURISTIC_USE", "mixed_mm").split(",") + return use_env + + +class autoheuristic_collect: + """ + Config for which autoheuristic optimizations should collect training data. + """ + + pad_mm = "pad_mm" in _parse_autoheuristic_collect_env() + mixed_mm = "mixed_mm" in _parse_autoheuristic_collect_env() + + +class autoheuristic_use: + """ + Config for which autoheuristic optimizations should use learned heuristics. + """ + + pad_mm = "pad_mm" in _parse_autoheuristic_use_env() + mixed_mm = "mixed_mm" in _parse_autoheuristic_collect_env() + + +# If set to 1, will run a JIT post compile hook if one is set. +run_jit_post_compile_hook = ( + os.environ.get("TORCHINDUCTOR_RUN_JIT_POST_COMPILE_HOOK", "0") == "1" +) + + +def run_autoheuristic(name: str) -> bool: + return collect_autoheuristic(name) or use_autoheuristic(name) + + +def collect_autoheuristic(name: str) -> bool: + if name == "pad_mm": + return autoheuristic_collect.pad_mm + elif name == "mixed_mm": + return autoheuristic_collect.mixed_mm + else: + # For test compatibility with non-standard ops (e.g. "test", "foo" used in tests) + return name in _parse_autoheuristic_collect_env() + + +def use_autoheuristic(name: str) -> bool: + if name == "pad_mm": + return autoheuristic_use.pad_mm + elif name == "mixed_mm": + return autoheuristic_use.mixed_mm + else: + # For test compatibility with non-standard ops (e.g. "test", "foo" used in tests) + return name in _parse_autoheuristic_use_env() + + +# If set to "DEFAULT", this will use the default log path specified in autoheuristic.py. +# If set to another path, autoheuristic will instead log results to the given path. +autoheuristic_log_path = os.environ.get( + "TORCHINDUCTOR_AUTOHEURISTIC_LOG_PATH", "DEFAULT" +) + +# Disabled by default on ROCm, opt-in if model utilises NHWC convolutions +layout_opt_default = "1" if not torch.version.hip else "0" +layout_optimization = ( + os.environ.get("TORCHINDUCTOR_LAYOUT_OPTIMIZATION", layout_opt_default) == "1" +) + +force_layout_optimization = os.environ.get("TORCHINDUCTOR_FORCE_LAYOUT_OPT", "0") == "1" + +# Cache SDPA constraint results keyed by (tensor identity, stride_order) to avoid +# creating duplicate buffers when the same tensor feeds multiple SDPA positions +# (e.g., key=value in simplified PMA attention). +cache_sdpa_constraint = ( + os.environ.get("TORCHINDUCTOR_CACHE_SDPA_CONSTRAINT", "0") == "1" +) + + +# Whether to keep the output strides the same as eager after layout optimization. +keep_output_stride = os.environ.get("TORCHINDUCTOR_KEEP_OUTPUT_STRIDE", "1") == "1" + +# Enabling this will let compiler print warning messages if a generated triton +# kernel has inputs with mixed layouts. This is helpful for perf debugging +# since kernel with mixed layout inputs may run much slower then one whose inputs +# have uniform layouts. +warn_mix_layout = os.environ.get("TORCHINDUCTOR_WARN_MIX_LAYOUT") == "1" + +# control store vs recompute heuristic +# For fanouts, rematerialization can lead to exponential blowup. So, have +# smaller threshold +realize_reads_threshold = 4 +realize_opcount_threshold = 30 + +# Threshold to prevent excessive accumulation of ops in one buffer during lowering +realize_acc_reads_threshold = 8 +realize_acc_reads_size_threshold: int | None = ( + None # TODO(xuanzh): harden this to make it non optional +) + +# Defer early realization of cheap output nodes (0 buffer reads, small opcount) +# to prevent cascade materialization in fullgraph compilation. +# Shared constants/indices saved for backward get eagerly materialized because +# they are graph outputs with multiple users, which inflates downstream read +# counts and can trigger suboptimal Triton block size heuristics. +delay_realize_cheap_outputs: bool = Config( + env_name_force="TORCHINDUCTOR_DELAY_REALIZE_CHEAP_OUTPUTS", + default=True, +) + +# fallback to eager for random/dropout, this is slow but useful for debugging +fallback_random = False + +# align random/dropout as eager mode(aten) behavior, maintaining fused possibility and faster gpu kernel +align_random_eager = False + +# fallback embedding_bag_byte_unpack to eager +fallback_embedding_bag_byte_unpack = False + +# automatically create fallbacks when encountering an unhandled op +implicit_fallbacks = True +assume_unaligned_fallback_output = ( + os.environ.get("TORCHINDUCTOR_ASSUME_UNALIGNED_FALLBACK_OUTPUT") == "1" +) + +# Factory callable that returns a custom InductorChoices instance. +# A callable (rather than a class) is used to defer imports and avoid circular +# dependencies between config and the choices module. Example: +# +# def _custom_choices_factory(): +# from my_package.choices import MyInductorChoices +# return MyInductorChoices() +# +# config.inductor_choices_class = _custom_choices_factory +# +# The returned instance must implement uuid() for cache key serialization. +inductor_choices_class: Callable[[], "InductorChoices"] | None = None + +# fuse even in cases without common reads +aggressive_fusion = False + +# For each fused kernel in the wrapper, comment with the nodes that get fused. +# Useful for debugging fusion. +debug_fusion: bool = os.environ.get("TORCHINDUCTOR_DEBUG_FUSION") == "1" +benchmark_fusion: bool = os.environ.get("TORCHINDUCTOR_BENCHMARK_FUSION") == "1" +enabled_metric_tables = os.environ.get("TORCHINDUCTOR_ENABLED_METRIC_TABLES", "") +loop_ordering_after_fusion: bool = ( + os.environ.get( + "TORCHINDUCTOR_LOOP_ORDERING_AFTER_FUSION", "0" if is_fbcode() else "1" + ) + == "1" +) + + +# When trying to fuse two nodes, one with: +# a[contiguous_writes] = fn(...) +# and another node: +# b[contiguous_writes] = a[discontiguous_reads] +# If b is unary, and we can figure out an inverse formula for +# discontiguous writes, invert b as : +# b[inverse(discontiguous_writes)] = a[contiguous_reads] +# so that the nodes can fuse. for more details: https://gist.github.com/eellison/6f9f4a7ec10a860150b15b719f9285a9 +loop_index_inversion_in_fusion: bool = True + +# If fusing two nodes only save less then score_fusion_memory_threshold memory, +# we should not bother fusing the nodes. +# +# This is especially helpful to resolve https://github.com/pytorch/pytorch/issues/133242 +# Previously we fuse two nodes because of common read of a scalar tensor. +# If we skip it, the loop ordering after fusion mechanism kicks in and can +# brings more savings. +# +# For the cases loop ordering after fusion does not help, we don't lose much. +score_fusion_memory_threshold = 10 + +# For Triton Templates, select fastest of best template + epilogue vs best template + separate epilogue kernel +benchmark_epilogue_fusion = ( + os.environ.get("TORCHINDUCTOR_BENCHMARK_EPILOGUE_FUSION", "1") == "1" +) + +# Take how many of the top triton kernels to benchmark epilogue +max_epilogue_benchmarked_choices = 1 + +# how many nodes to allow into a single fusion +max_fusion_size = 64 + +# Minimum overlap ratio to consider fusion beneficial when inputs are shared by no indices overlapped. +# Valid range: [0, 1]. Default to not fusion. +min_overlap_ratio = 1.1 + +# how many nodes to attempt pairwise fusion with in a buffer group +max_fusion_buffer_group_pairwise_attempts = 64 + +# maximum number of unique input/output buffers allowed in fused kernels. +# The check is disabled if set to None. +max_fusion_unique_io_buffers: int | None = None + +# max number of inputs to generate cat as a pointwise op with masked loads +max_pointwise_cat_inputs = 8 + +# force concat to be generated as a pointwise op with masked loads +force_pointwise_cat = False + +# replace small reductions with pointwise, disable with `= 1` +unroll_reductions_threshold = 8 + +# Add extra comments to output code (causes compile cache misses) +comment_origin = False + +# Convert 1x1 convs into matmuls +conv_1x1_as_mm = False + +# For reductions with a small output size (usually 1, e.g. x.sum()) there is not enough +# parallelism to saturate the GPU. We have two ways of handling this, either `split_reductions` +# or `triton.cooperative_reductions` which are mutually exclusive. +# split_reductions: uses multiple kernels to gain more parallelism +# triton.cooperative_reductions: uses cross thread-block synchronization to gain more parallelism +# enabling both of these will implicitly disable split_reductions +split_reductions = os.getenv("TORCHINDUCTOR_SPLIT_REDUCTIONS", "1") == "1" + +# A deterministic mode that skips any on device benchmarking in Inductor +# if we know they affect numerics. WARNING: Expect perf hit in this mode. +deterministic = os.getenv("TORCHINDUCTOR_DETERMINISTIC") == "1" + +# When we do split reduction, this number control the minimum value for +# num_split. Too small num_split make the split reduction less efficient. +# It's a much bigger problem when we compile a dynamic shape kernel with +# non-representative inputs. +min_num_split = int(os.environ.get("TORCHINDUCTOR_MIN_NUM_SPLIT", 0)) + +benchmark_kernel = os.environ.get("TORCHINDUCTOR_BENCHMARK_KERNEL", "0") == "1" + +# Enable constant and index_expr folding +constant_and_index_propagation = True + +# we always add constants into graph.constants without +# performing any constant-inlining optimization +always_keep_tensor_constants = False + +# assert that indirect indexing does not read / write out of bounds +assert_indirect_indexing = True + +# skip emitting runtime assertions for unbacked symbols in generated code +do_not_emit_runtime_assertions = False + +# compute CSE bounds on variables that do not appear in the FX graph +compute_all_bounds = False + +# enable the combo kernel that combines data-independent kernels (additional +# to foreach kernels) into a single one (Experimental) +combo_kernels = False +# benchmark combo kernels and only allow ones with perf gains +benchmark_combo_kernel = False +# combo_kernel autotuning options: 0 - disable, 1 - enable except for foreach, +# 2 - enable for all +combo_kernels_autotune = 1 +# Enable masking for combining kernels of mixed sizes: 0 - disable, 1 - enable +# for all except for foreach, 2 - enable for all +combo_kernel_allow_mixed_sizes = 1 +# Enable dynamic shapes for foreach kernels +combo_kernel_foreach_dynamic_shapes = True +# Maximum number of arguments (read/write buffers) allowed in a combo kernel +combo_kernel_max_num_args = 250 +# When True, each combo sub-kernel gets its own block sizes (XBLOCK_0, YBLOCK_0, etc.) +# allowing different sub-kernels to use different tile sizes based on their heuristics. +# When False, all sub-kernels share block sizes (XBLOCK, YBLOCK, etc.) +combo_kernel_per_subkernel_blocks = False +# When True, only pointwise kernels are eligible for combo kernel fusion. +combo_kernels_pointwise_only = False + +# constant folding on the joint graph +joint_graph_constant_folding = True + +# Enable indirect_indexing asserts for decompositions and lowerings +debug_index_asserts = False + +# warnings intended for PyTorch developers, disable for point releases +is_nightly_or_source = "dev" in torch.__version__ or "git" in torch.__version__ +developer_warnings = is_fbcode() or is_nightly_or_source + +# This pattern matches a special usage of scatter +# 1. It's applied to a constant tensor +# 2. The index tensor has size 1 in the scatter dimension +# Such pattern generates a sparse matrix when the const tensor is all-zero. +# We can lower this pattern to a pointwise kernel for more fusion opportunities +# and saving memory footprint. +optimize_scatter_upon_const_tensor = ( + os.environ.get("TORCHINDUCTOR_OPTIMIZE_SCATTER_UPON_CONST_TENSOR", "1") == "1" +) + +# options in caffe2/torch/_inductor/fx_passes/pre_grad.py +add_pre_grad_passes: str | None = None +remove_pre_grad_passes: str | None = None + +# Comma-separated list of pass names to disable. Passes disabled via this config +# will be skipped when they go through GraphTransformObserver. +# Can be set via TORCHINDUCTOR_DISABLED_PASSES env var. +# Use uppercase pass names (e.g., "PASS1,PASS2"). +disabled_passes: str = Config( + env_name_force="TORCHINDUCTOR_DISABLED_PASSES", + default="", +) + + +# The multiprocessing start method to use for inductor workers in the codecache. +def decide_worker_start_method() -> str: + if "TORCHINDUCTOR_WORKER_START" in os.environ: + start_method = os.environ["TORCHINDUCTOR_WORKER_START"] + else: + start_method = "subprocess" + assert start_method in ( + "subprocess", + "fork", + "spawn", + ), f"Invalid start method: {start_method}" + return start_method + + +worker_start_method: str = decide_worker_start_method() + +# Threshold to decide if a kernel has small memory access in bytes +# Default value is 16 MB which is arbitrarily selected. +small_memory_access_threshold: int = 16777216 + +# Whether to log from subprocess workers that are launched. +worker_suppress_logging: bool = Config( + justknob="pytorch/compiler:worker_suppress_logging", + env_name_force="TORCHINDUCTOR_WORKER_SUPPRESS_LOGGING", + default=True, +) + +# Log per-operation runtime estimates for TLParse analysis. +log_tlparse: bool = Config( + env_name_force="LOG_TLPARSE", + default=False, +) + +# Flags to turn on all_reduce fusion. These 2 flags should be automatically turned +# on by DDP and should not be set by the users. +_fuse_ddp_communication = False +_fuse_ddp_bucket_size = 25 + +# Flag to control which fusion passes to apply. Functions in the list will +# be applied in order. There are two different different fusion passes +# --"fuse_ddp_with_concat_op" and "fuse_ddp_with_coalesced_op". The default +# one is "fuse_ddp_with_concat_op". Users can also change this to a customized +# fusion function. +# +# The fusion currently does not support multiple DDP with different PG or +# data type. This feature will be added in the future PRs. +# +# "schedule_comm_wait" is used to delay the wait ops to maximize comm/comp +# overlapping. At this moment, this pass performs better than +# reorder_for_compute_comm_overlap_passes but we will add the logic of +# "schedule_comm_wait" in the future and remove the one here. +_fuse_ddp_communication_passes: list[Callable[..., None] | str] = [ + "fuse_ddp_with_concat_op", + "schedule_comm_wait", +] + +_micro_pipeline_tp: bool = False + + +# Enable/disable partitioned scatter optimization for atomic add kernels +# this will improve kernel performance at cost of memory usage. +partitioned_scatter_enabled = ( + os.environ.get("TORCHINDUCTOR_PARTITIONED_SCATTER_ENABLED", "0") == "1" +) + +# Min partitions for scatter optimization +partitioned_scatter_min_partitions: int = 2 + +# Max partitions for scatter optimization +partitioned_scatter_max_partitions: int = 128 + +# Memory budget fraction for scatter buffers +partitioned_scatter_memory_budget: float = 0.10 + + +class _collective: + auto_select: bool = False + one_shot_all_reduce_threshold_bytes: int = 128 * 1024 + + +class aten_distributed_optimizations: + """Configuration for distributed optimization passes on ATen FX graphs.""" + + # Enable overlap scheduling pass + enable_overlap_scheduling: bool = False + + # Enable overlap-preserving collective bucketing + collective_bucketing: bool | None = None + + # Insert ordering dependencies to preserve overlap relationships. This should only be used if + # compiling with inductor, or for subsequent passes before removing the ops prior to execution + insert_overlap_deps: bool | None = None + + # Maximum compute node prefetch distance for overlap scheduling + max_compute_pre_fetch: int | None = None + + compute_overlap_multipler: float | None = None + + # Custom runtime estimation function for ops + # For user-defined estimation function, pass in the function handle + # None means use default estimations + # TODO - need estimated and profile based version + custom_runtime_estimation: Callable[[torch.fx.Node], float | None] | None = None + + # Method for estimating collective runtime + # "analytical": Use bandwidth formulas (default) + # "benchmark": Use CUDA events with power-of-2 rounding and interpolation + # In deterministic mode, this setting is ignored and "analytical" is used. + collective_estimator: Literal["analytical", "benchmark"] = "analytical" + + # Method for estimating compute (ATen op) runtime + # "analytical": Use roofline model estimates (deterministic, no GPU sync) + # "benchmark": Use GPU benchmarking (more accurate, requires GPU sync) + # In deterministic mode, this setting is ignored and "analytical" is used. + compute_estimator: Literal["analytical", "benchmark"] = "benchmark" + + # Maximum memory increase above baseline for prefetch operations + # Uses minimum of absolute cap and ratio of baseline + max_memory_increase_gb: float | None = None # Absolute cap in GB + max_memory_increase_ratio: float | None = None # Ratio of baseline peak memory + + # Maximum GB of concurrent collective data in flight. Too much in flight memory + # can cause memory fragmentation within the CUDA Caching Allocator. + max_in_flight_gb: float | None = None + + # Maximum prefetch or bucketing candidates. Mainly intended for compile time. + max_coll_distance: int | None = None + log_final_collectives_estimations: bool = False + + # Bucket exposed collectives first (None means auto) + bucket_exposed_first: bool | None = None + + # Experimental setting to bucket only internode communications + bucket_only_internode_comms: bool = False + + # Enable fusion region detection for overlap scheduling cost estimation. + # When enabled, groups of fusible ops (pointwise, reduction, etc.) are treated + # as atomic units with memory-bound runtime estimates. + enable_fusion_regions: bool | None = None + + # Default bucketing mode for auto and manual overlap scheduling + # "default": traced bucketing, fully lowered by inductor during compilation + # "custom_ops": temporary bucketing using custom ops to hide parts from inductor + # "custom_ops_multidtype": same as custom_ops but buckets multiple dtypes + # (e.g. bf16 and fp32) into one bucket + # "coalesced": zero-copy batching via reduce_scatter_tensor_coalesced + # (reduce_scatter only; all_gather falls back to default) + # None means "auto" — the compiler picks the best mode + bucket_mode: ( + Literal["default", "custom_ops", "custom_ops_multidtype", "coalesced"] | None + ) = None + + # Prioritize bucketing during overlap scheduling by grouping candidates by bucket key + prioritize_bucketing_during_scheduling: bool = True + + # Verify FX graphs are identical across ranks before overlap scheduling. + # Detects non-SPMD graphs that would cause NCCL collective ordering + # mismatches and hangs. + spmd_check: bool = True + + # Action on SPMD graph mismatch: "warn" logs a warning, "error" raises + # RuntimeError. "error" fails fast instead of risking silent NCCL hang. + # TODO(ivankobzarev): change default to "error" after real-world testing. + spmd_mismatch: Literal["warn", "error"] = "warn" + + # When True, automatically remove extra deps that create cycles instead of + # raising an error. Set this to True as a workaround if overlap scheduling + # fails with a cycle error, and file a bug so the root cause can be fixed. + overlap_scheduling_autofix_cycles: bool = False + + +def parallel_compile_enabled_internally() -> bool: + """ + TODO: Remove when parallel compiled is fully enabled internally. For rollout, use a + knob to enable / disable. The justknob should not be performed at import, however. + So for fbcode, we assign compile_threads to 'None' below and initialize lazily in + async_compile.py. + """ + ENABLE_PARALLEL_COMPILE_VERSION = 1 + + jk_name = "pytorch/inductor:enable_parallel_compile_version" + version = torch._utils_internal.justknobs_getval_int(jk_name) + return ENABLE_PARALLEL_COMPILE_VERSION >= version + + +def decide_compile_threads() -> int: + """ + Here are the precedence to decide compile_threads + 1. User can override it by TORCHINDUCTOR_COMPILE_THREADS. One may want to disable async compiling by + setting this to 1 to make pdb happy. + 2. Set to 1 if it's win32 platform + 3. decide by the number of CPU cores + """ + import logging + + # Defined locally so install_config_module doesn't try to parse + # as a config option. + log = logging.getLogger(__name__) + + if "TORCHINDUCTOR_COMPILE_THREADS" in os.environ: + compile_threads = int(os.environ["TORCHINDUCTOR_COMPILE_THREADS"]) + log.info("compile_threads set to %d via env", compile_threads) + elif sys.platform == "win32": + compile_threads = 1 + log.info("compile_threads set to 1 for win32") + elif is_fbcode() and not parallel_compile_enabled_internally(): + compile_threads = 1 + log.info("compile_threads set to 1 in fbcode") + else: + cpu_count = torch._utils.cpu_count() + assert cpu_count + compile_threads = min(32, cpu_count) + log.info("compile_threads set to %d", compile_threads) + + return compile_threads + + +# TODO: Set directly after internal rollout. +compile_threads: int | None = None if is_fbcode() else decide_compile_threads() + +# Whether to quiesce the Triton-compile subprocess pool at the end of each compilation. +quiesce_async_compile_pool: bool = Config( + justknob="pytorch/inductor:quiesce_async_compile_pool", + env_name_force="TORCHINDUCTOR_QUIESCE_ASYNC_COMPILE_POOL", + default=True, +) + +# Time in seconds to wait before quiescing +quiesce_async_compile_time: int = Config( + default=60, +) + +# Whether or not to enable statically launching CUDA kernels +# compiled by triton (instead of using triton's own launcher) +use_static_cuda_launcher: bool = static_cuda_launcher_default() + +# Alias of use_static_cuda_launcher, used by both CUDA/XPU. +use_static_triton_launcher: bool = Config( + alias="torch._inductor.config.use_static_cuda_launcher" +) + +# Attempt to statically launch user defined triton kernels +# Requires use_static_cuda_launcher +static_launch_user_defined_triton_kernels: bool = Config( + justknob="pytorch/inductor:static_launch_user_defined_triton_kernels", + env_name_force="TORCHINDUCTOR_STATIC_LAUNCH_USER_DEFINED_TRITON_KERNELS", + default=False, +) + +# Raise error if we bypass the launcher +strict_static_cuda_launcher: bool = ( + os.environ.get("TORCHINDUCTOR_STRICT_STATIC_CUDA_LAUNCHER", "0") == "1" +) + +# Alias of strict_static_cuda_launcher, used by both CUDA/XPU. +strict_static_triton_launcher: bool = Config( + alias="torch._inductor.config.strict_static_cuda_launcher" +) + +# gemm autotuning global cache dir +global_cache_dir: str | None +if is_fbcode(): + try: + from libfb.py import parutil + + if __package__: + global_cache_dir = parutil.get_dir_path( + os.path.join(__package__.replace(".", os.sep), "fb/cache") + ) + else: + global_cache_dir = parutil.get_dir_path("fb/cache") + except (ValueError, ImportError): + global_cache_dir = None + +else: + global_cache_dir = None + +# If kernel is fused, the name is generated from the origin node op names +# for larger kernels limit this +kernel_name_max_ops = 10 + +# Pad input tensors of matmul/bmm/addmm to leverage Tensor Cores in NVIDIA GPUs +shape_padding = os.environ.get("TORCHINDUCTOR_SHAPE_PADDING", "1") == "1" + +# Control if we will do padding for pointwise/reductions +comprehensive_padding = ( + os.environ.get("TORCHINDUCTOR_COMPREHENSIVE_PADDING", "1") == "1" +) +pad_channels_last = False + +# Control if we will do padding on dynamic shapes +pad_dynamic_shapes = False + +# Disable comprehensive padding on the CPU +disable_padding_cpu = True + +# Control if we will expand the dimension of pointwise nodes to fuse +expand_dimension_for_pointwise_nodes = False + +# The width of comprehensive padding, in bytes. +# CUDA max memory transaction size is 128 bytes for a warp. +padding_alignment_bytes = 128 + +# Threshold on the minimum stride that will be padded. +# +# Don't align a too small stride since that causes too much memory increase. +# Pad too small stride may also cause perf loss. We may result in many tiny data blocks +# with gaps in between. That causes less coalesced GPU memory access! +# +# Initially we pick 320 as the threshold since for alignment=16, +# that results in at most 5% memory cost. +# +# But later on we raise the threshold to 1024 to avoid interfere with persistent reduction. +# Let's say an inner reduction has a row size 513. Inductor will generate +# persistent reduction code. +# If we do padding, the strides are not contiguous any more. Inductor +# uses a much smaller threshold for persistent reduction in this case and +# generates potentially worse non-persistent reduction code. +# +# This change turns HF AllenaiLongformerBase amp training from a loss of 1.09x to a win of 1.05x. +# (baseline: 71.09ms, padding w/o this change: 77.38ms, padding with this change: 67.77ms) +padding_stride_threshold = 1024 + +# Enable padding outputs, even if they would not be padded in eager mode. +# By default, we use the same strides as eager mode. +pad_outputs = False + +# Whether to treat output of the backward graph as user visible. +# For user visible outputs, inductor will make sure the stride matches with eager. +bw_outputs_user_visible = True + +# Whether to always use shape padding if it is enabled and possible +force_shape_pad: bool = False + +# Fx-based linear/matmul/bmm + permute/transpose vertical fusion +permute_fusion = os.environ.get("TORCHINDUCTOR_PERMUTE_FUSION", "0") == "1" + +# Mark the wrapper call in PyTorch profiler +profiler_mark_wrapper_call = False + +# Generate hook calls to torch._inductor.hooks.run_intermediate_hooks for +# every intermediate for which we can correlate it with an intermediate +# from the original FX graph +generate_intermediate_hooks = False + +# Populate traceback field on IRNode; good for debugging why origin_node is +# not populated, or finding out where an IRNode was constructed +debug_ir_traceback = False + +# used for debugging to make sure config is properly set +_raise_error_for_testing = False + +# Use fp64 for unbacked float scalars (from .item()) in Triton kernel signatures +# to preserve precision. When False, uses fp32 (legacy behavior with precision loss). +_use_fp64_for_unbacked_floats: bool = not is_fbcode() + +_profile_var = os.environ.get("TORCHINDUCTOR_PROFILE", "") +profile_bandwidth = _profile_var != "" +profile_bandwidth_regex = "" if _profile_var == "1" else _profile_var +# Specify a file where we print out the profiling results. +# None means we do not dump results to a file. +profile_bandwidth_output: str | None = os.environ.get( + "TORCHINDUCTOR_PROFILE_OUTPUT", None +) +# Switch to do_bench_using_profiling to exclude the CPU overheads +profile_bandwidth_with_do_bench_using_profiling = ( + os.environ.get("TORCHINDUCTOR_PROFILE_WITH_DO_BENCH_USING_PROFILING") == "1" +) + + +# TODO: remove later +# incompatible with cpp_wrapper +disable_cpp_codegen = False + + +# Freezing will attempt to inline weights as constants in optimization +# and run constant folding and other optimizations on them. After freezing, weights +# can no longer be updated. +freezing: bool = os.environ.get("TORCHINDUCTOR_FREEZING", "0") == "1" + +# Make freezing invalidate the eager Parameters of nn modules, to avoid memory overhead +# of potentially keeping multiple copies of weights. +freezing_discard_parameters: bool = False + +# decompose some memory bound matmul/bmm to mul +decompose_mem_bound_mm: bool = False + +# Wrap compiled regions in inductor_compiled_code HOP to make them visible to +# TorchDispatchModes like DebugMode and Selective Activation Checkpointing. +wrap_inductor_compiled_regions: bool = False + +# assume_aligned_inputs means that we assume that inputs will be aligned; we generate +# code using this assumption, and clone tensors before use if they aren't aligned. +# In the common case, most inputs will be aligned. +assume_aligned_inputs: bool = False + +# assume_32bit_indexing means that we assume 32-bit indexing is always safe; we always +# use 32-bit indices regardless of tensor sizes. If assume_32bit_indexing contradicts +# with example inputs we throw. This is useful when all dynamic shapes are unbacked and +# you know you only operate with 32-bit sizes. +assume_32bit_indexing: bool = False + +# For the user-written Triton kernels compiled with the model, ignore the unsupported +# arguments passed to the @triton.autotune in the user's code; this is unsafe, as +# ignoring the unsupported args may lead to unexpected autotuning behavior: don't +# set unless you know what you're doing. +unsafe_ignore_unsupported_triton_autotune_args: bool = False + +# When True, we will check in scheduler.py _codegen that there are no "loops" +# in the call stack; that is to say, the same frame multiple times. This +# ensures that a cProfile trace to this frame will be a straight line without +# any cycles. Incompatible with cpp_wrapper. +check_stack_no_cycles_TESTING_ONLY: bool = False + +# When True, complex_memory_overlap always reports True +always_complex_memory_overlap_TESTING_ONLY: bool = False + +# enable linear binary folding +enable_linear_binary_folding = ( + os.environ.get("TORCHINDUCTOR_ENABLE_LINEAR_BINARY_FOLDING", "0") == "1" +) + + +# Adds NVTX annotations around training phases +annotate_training: bool = os.environ.get("TORCHINDUCTOR_ANNOTATE_TRAINING", "0") == "1" + +# Enable caching codegen of triton templates. +enable_caching_generated_triton_templates: bool = True + +# Lookup table for overriding autotune configs based on hash of Triton source code +autotune_lookup_table: dict[str, dict[str, Any]] = {} + +file_lock_timeout: int = int(os.environ.get("TORCHINDUCTOR_FILE_LOCK_TIMEOUT", "600")) + +enable_autograd_for_aot: bool = False + + +def get_worker_log_path() -> str | None: + log_loc = None + if is_fbcode(): + mast_job_name = os.environ.get("MAST_HPC_JOB_NAME", None) + global_rank = os.environ.get("ROLE_RANK", "0") + + if mast_job_name is not None: + log_loc = f"/logs/dedicated_log_torch_compile_worker_rank{global_rank}" + + return log_loc + + +torchinductor_worker_logpath: str = Config( + env_name_force="TORCHINDUCTOR_WORKER_LOGPATH", + default="", +) + + +class auto_chunker: + enable: bool = os.environ.get("TORCHINDUCTOR_AUTO_CHUNKER") == "1" + + # Don't chunk from a node if the output size is not large enough + output_size_threshold: int = 1024 * 1024 + + # Don't chunk from a node if it does not 'amplify' the inputs a lot + amplify_ratio_threshold: int = 8 + + num_chunk: int | None = ( + int(os.environ.get("TORCHINDUCTOR_CHUNKER_NUM_CHUNKS")) # type: ignore[arg-type] + if os.environ.get("TORCHINDUCTOR_CHUNKER_NUM_CHUNKS") is not None + else None + ) # If not None, use this to force number of chunks + + +# config specific to codegen/cpp.py +class cpp: + """ + Settings for cpp backend. + This class provides a centralized location for managing cpp backend settings. + """ + + # set to torch.get_num_threads() + threads = -1 + + # Do not generate loops when the condition doesn't hold, like: + # for(long i0=4096; i0<4096; i0+=1) + no_redundant_loops = ( + os.environ.get("TORCHINDUCTOR_CPP_NO_REDUNDANT_LOOPS", "1") == "1" + ) + + # Assume number of threads is dynamic, don't specialize thread number. + # Kernels don't recompile on thread number changes with this flag on. + # For single-threaded workload, turning it on would incur a slight + # performance degradation. + dynamic_threads = os.environ.get("TORCHINDUCTOR_CPP_DYNAMIC_THREADS", "0") == "1" + + simdlen: int | None = None + min_chunk_size = int(os.environ.get("TORCHINDUCTOR_CPP_MIN_CHUNK_SIZE", "512")) + + cxx: tuple[None, str] = ( + None, # download gcc12 from conda-forge if conda is installed + os.environ.get("CXX", "clang++" if sys.platform == "darwin" else "g++"), + ) # type: ignore[assignment] + + # Allow kernel performance profiling via PyTorch profiler + enable_kernel_profile = ( + os.environ.get("TORCHINDUCTOR_CPP_ENABLE_KERNEL_PROFILE", "0") == "1" + ) + + # enable weight prepacking to get a better performance; may lead to large memory footprint + weight_prepack = os.environ.get("TORCHINDUCTOR_CPP_WEIGHT_PREPACK", "1") == "1" + + # Inject a bug into our relu implementation; useful for testing our repro + # extraction and minification functionality. + # Valid values: "compile_error", "runtime_error", "accuracy" + inject_relu_bug_TESTING_ONLY: str | None = None + inject_log1p_bug_TESTING_ONLY: str | None = None + + # If None, autodetect whether or not AVX512/AVX2 can be used. Otherwise, + # force usage as specified, without testing. Default None. + vec_isa_ok: bool | None = get_tristate_env("TORCHINDUCTOR_VEC_ISA_OK") + + # similar to config.triton.descriptive_names + descriptive_names: Literal["torch", "original_aten", "inductor_node"] = ( + "original_aten" + ) + + # how many nodes to allow into a single horizontal fusion + max_horizontal_fusion_size = int( + os.environ.get("TORCHINDUCTOR_CPP_MAX_HORIZONTAL_FUSION_SIZE", "16") + ) + + # Make scatter_reduce fallback when reduce is sum to avoid performance regression + # using atomic_add. + fallback_scatter_reduce_sum = ( + os.environ.get("TORCHINDUCTOR_CPP_FALLBACK_SCATTER_REDUCE_SUM", "1") == "1" + ) + + # Use funsafe-math-optimizations when compiling + enable_unsafe_math_opt_flag = ( + os.environ.get("TORCHINDUCTOR_CPP_ENABLE_UNSAFE_MATH_OPT_FLAG", "0") == "1" + ) + + # Use ffp-contract when compiling + # Options: "off" (default), "on", "fast" + # Per https://godbolt.org/z/bf4bvfc9r , clang/gcc has different behavior for "fast" + enable_floating_point_contract_flag = os.environ.get( + "TORCHINDUCTOR_CPP_ENABLE_FLOATING_POINT_CONTRACT_FLAG", "off" + ) + + # Disable the tiling select heuristic + enable_tiling_heuristics = ( + os.environ.get("TORCHINDUCTOR_CPP_ENABLE_TILING_HEURISTIC", "1") == "1" + ) + + # Enable the Grouped GEMM Fusion + enable_grouped_gemm_template = False + + # Maximal allowed number of slices on K-dim for a GEMM kernel. This controls + # the maximal parallelism of K-slicing. Since K-slicing requires extra thread + # synchronization and buffers, the maximal number of slices is limited to + # mitigate the sync overhead and memory usage. + # When set to 0, the number of slices is unlimited. + gemm_max_k_slices = int(os.environ.get("TORCHINDUCTOR_CPP_GEMM_MAX_K_SLICES", "1")) + + # For perf tuning and debugging purpose, configure the pre-defined cache blocking for + # MxNxK dims respectively. The blockings are separated by comma and the unit is + # the number of register blocks. + # For example, "4,1,10" means 4 register blocks on M, 1 on N and 10 on K respectively. + gemm_cache_blocking = os.environ.get("TORCHINDUCTOR_CPP_GEMM_CACHE_BLOCKING", None) + + # For perf tuning and debugging purpose, configure the pre-defined thread blocking factors for + # MxNxK dims respectively. The factors are separated by comma and their product + # should be the same as the total number of threads. + # For example, if the total number of threads is 56, "7,4,2" means the work is + # decomposed into 7x4x2 thread blocks along MxNxK of a GEMM. + gemm_thread_factors = os.environ.get("TORCHINDUCTOR_CPP_GEMM_THREAD_FACTORS", None) + + # Whether to enable masked vectorization for the tail_loop. + enable_loop_tail_vec = True + + # Whether to enable concat linear for cpu device + # Currently concat linear on CPU not always have benefit, depends on linear'shape or + # computing resource. We set this default to False to avoid regressions. User and + # enable this feature by their need. + enable_concat_linear = False + + # Whether to use decomposed tanh for cpu device + # Disable by default due to https://github.com/pytorch/pytorch/issues/148241 + use_decompose_tanh = ( + os.environ.get("TORCHINDUCTOR_CPP_USE_DECOMPOSE_TANH", "0") == "1" + ) + + # Use a small dequant buffer for wgt of woq int4 size as: [q_group_size, Nr] + use_small_dequant_buffer = False + + force_inline_kernel = ( + os.environ.get("TORCHINDUCTOR_CPP_FORCE_INLINE_KERNEL", "0") == "1" + ) + + # Use static constexpr or static const for int array + use_constexpr_for_int_array = ( + os.environ.get("TORCHINDUCTOR_CPP_USE_CONSTEXPR_FOR_INT_ARRAY", "1") == "1" + ) + + # threshold between two step reduction algorithm and welford reduction algorithm + use_two_step_variance_threshold = 1024 + + +class triton: + """ + Config specific to codegen/triton.py + """ + + # Use cudagraphs on output code + cudagraphs = os.environ.get("TORCHINDUCTOR_CUDAGRAPHS") == "1" + + # Use cudagraph trees for memory pooling if `cudagraphs` is True + cudagraph_trees = True + + # Should we skip cudagraphing graphs with dynamic shape inputs + # If False, we will re-record a graph for each unique set of shape inputs + cudagraph_skip_dynamic_graphs = False + + # Specify dynamic shapes to capture cudagraphs and skip cudagraph for other shapes. + # Default to None, which means we capture cudagraphs for all shapes. + cudagraph_capture_sizes: tuple[int | tuple[int, ...]] | None = None + + # Minimum number of nodes (kernels) required for a cudagraph partition. + # If a partition has fewer nodes than this threshold, it won't be cudagraphed. + # This helps avoid overhead for very small partitions where cudagraph + # recording/replay cost outweighs the benefits. + # Set to 0 to disable this check. + cudagraph_min_partition_size = 0 + + # assertions not on the fast path, steady state + slow_path_cudagraph_asserts = True + + # TODO - need to debug why this prevents cleanup + cudagraph_trees_history_recording = False + + # Emit objgraph backref dumps for leaked cudagraph pool tensors + cudagraph_trees_objgraph = False + + # Enable cudagraph support for mutated inputs from prior cudagraph pool + cudagraph_support_input_mutation = not is_fbcode() + + # Maximal number of allowed cudagraph re-record for a function and + # a cudagraph node due to static input tensor address changes or + # cudagraph managed tensor data pointer changed. + # i.e., allow num_recording <= cudagraph_unexpected_rerecord_limit + # note: we are conservative here and choose a large limit. + cudagraph_unexpected_rerecord_limit = 128 + + # Warn loudly when the number of cudagraphs due to dynamic shape + # exceeds this limit + cudagraph_dynamic_shape_warn_limit: int | None = 8 + + # synchronize after cudagraph invocation + force_cudagraph_sync = False + + # always run cudagraphs in the eager warmup stage + # instead of recording and executing cudagraphs + force_cudagraphs_warmup = False + + # If False (default), torch.compile skips cudagraph for a graph if it + # contains cudagraph-unsafe ops. If True, we require that all cuda ops + # be captured into cudagraph. If this is not possible, this will raise + # an error. + cudagraph_or_error: bool = Config( + env_name_force="TORCHINDUCTOR_CUDAGRAPH_OR_ERROR", + default=False, + ) + + # reorder nodes to minimize the number of graph partitions while + # not incurring large memory overhead + reorder_for_reducing_graph_partitions: bool = True + + # assertions on the fast path + fast_path_cudagraph_asserts = False + + # skip warmup for cudagraph trees + skip_cudagraph_warmup = False + + # Synchronize before and after every compiled graph. + debug_sync_graph = False + + # Synchronize after every kernel launch, to help pinpoint bugs + debug_sync_kernel = False + + # Always load full blocks (rather than broadcasting inside the block) + dense_indexing = False + + # TODO - enable by default + coalesce_tiling_analysis: bool = ( + os.environ.get( + "TORCHINDUCTOR_COALESCE_TILING_ANALYSIS", "1" if not is_fbcode() else "0" + ) + == "1" + ) + + # limit tiling dimensions + # - max_tiles=1 disables tiling + # - max_tiles=2 + # - max_tiles=3 is experimental and may have bugs + # higher values are unsupported + + # We use a max of 3 if coalesce_tiling_analysis is True, and 2 otherwise. + # Note - coalesce_tiling_analysis does not yet apply to dynamic shapes. + max_tiles: int | None = None + + # Prefer higher dimensional tilings. This simplifies indexing expressions, making + # it easier to identify block pointers. + prefer_nd_tiling: bool = False + + # use triton.autotune for pointwise ops with complex layouts + # this should only be disabled for debugging/testing + autotune_pointwise = True + + # max autotune gemm with cublasLt + autotune_cublasLt = True + + # Tune the generated Triton kernels at compile time instead of first time they run + # Setting to None means uninitialized + autotune_at_compile_time: bool | None = autotune_at_compile_time_default() + + # We use random tensors for autotune by default. Setting this as true will let us + # use inputs from sample inputs to autotune user defined triton kernels. + # Side effect for this option is increased memory footprint during first pass compilation. + autotune_with_sample_inputs: bool = False + + # Allows tiling reductions into multiple dimensions. + # For best results, this should be used with prefer_nd_tiling. + tile_reductions: bool = False + + # Codegen matmul natively with tl.dot without using a template. + # This option makes Inductor generate matrix multiplication from scratch, + # instead of calling predefined Triton templates (mm, bmm, mm_plus_mm). + # Compile time may be longer because native matmul benchmarks more Triton configs + # than regular pointwise or reduction kernels. + # Native matmul often aggressively fuses operations around the matrix multiply, + # which can make it faster or slower depending on your program. + # + # This option takes priority over other GEMM implementations. If Inductor determines + # that a matmul can be generated, it will always generate it with native_matmul. + # That means optimized kernels such as decompose_k or persistent_tma_matmul will + # not be called when this option is enabled. + # + # Note: Native matmul does not currently support block pointers or TMA matmul. + # If both native_matmul and (use_block_ptr or enable_persistent_tma_matmul) are enabled, + # an error will be thrown. + native_matmul: bool = os.getenv("TORCHINDUCTOR_NATIVE_MATMUL", "0") == "1" + + # should we stop a fusion to allow better tiling? + tiling_prevents_pointwise_fusion = True + tiling_prevents_reduction_fusion = True + + # should we give different names to kernels + # Note: This is orthogonal to descriptive_names - this is deciding whether + # our triton kernel names should all be `triton_` (to maximize caching) or + # whether they should be unique. + unique_kernel_names = ( + os.environ.get("TORCHINDUCTOR_UNIQUE_KERNEL_NAMES", "1") == "1" + ) + + # similar to the option above, but this is specific to user defined kernels, + # while unique_kernel_name is for kernels generated by inductor. + # We have this option because sometimes we reuse user's kernel code with different + # configs which would result in the same name. + # Note: This MODIFIES the user's kernel function name within inductor phase. + unique_user_kernel_names = ( + os.environ.get("TORCHINDUCTOR_UNIQUE_USER_KERNEL_NAMES", "0") == "1" + ) + + # should we put op names in kernel names + # "torch": Maps to the fx op in the Dynamo graph (module name, method name, etc.) + # "original_aten": Maps to the highest-level aten op (i.e. pre-decompositions) + # "inductor_node": Maps to the node name in the FX graph passed to Inductor + descriptive_names: Literal["torch", "original_aten", "inductor_node"] = ( + "original_aten" + ) + + # use alternate codegen for smaller reductions + persistent_reductions = ( + os.environ.get("TORCHINDUCTOR_PERSISTENT_REDUCTIONS", "1") == "1" + ) + + # Decompose sort-based ops (sort, mode, median) to generate Triton + # kernels instead of falling back to ATen eager. When enabled, sort + # removes the default 512-element dimension limit and uses int32 + # indices (up to 2^31-1 elements), and mode/median decompose into + # sort + reduction / pointwise ops that Inductor can lower to Triton. + decompose_sort_ops: bool = ( + os.environ.get("TORCHINDUCTOR_DECOMPOSE_SORT_OPS", "0") == "1" + ) + + # For small output size reductions uses cross thread-block synchronization to gain more parallelism + cooperative_reductions = ( + os.environ.get("TORCHINDUCTOR_COOPERATIVE_REDUCTIONS", "0") == "1" + ) + + # used for debugging cooperative reduction codegen, always generate cooperative_reductions + force_cooperative_reductions = False + + # 0: disable + # 1/True: enable, use tuning to pick between different subkernels + # 2: enable, force using persistent reduction (for debugging) + # 3: enable, force using non-persistent reduction (for debugging) + # pyrefly: ignore [bad-assignment] + multi_kernel: Literal[0, 1, 2, 3] = int( + os.environ.get("TORCHINDUCTOR_MULTI_KERNEL", "0") + ) # type: ignore[assignment] + + # hint to Triton when arguments are divisible by 16 + divisible_by_16 = os.environ.get("TORCHINDUCTOR_DIVISIBLE_BY_16", "1") == "1" + + # Minimum R0_BLOCK to be used for a TritonSplitScanKernel + # NOTE: This also indirectly controls the size of workspace buffer required + min_split_scan_rblock = 256 + + # Store the generated cubin files for cpp wrapper code to load + store_cubin = False + + # the max number of spills we allow for the configs we benchmark. + # Setting this to 0 means we skip a config if it spills even a single + # register. + # Setting it to a larger value allows a config spilling a small amount + # of registers being benchmarked. + # + # NOTE: triton will always report >0 register spills for kernels using sin/cos. + # (check this issue https://github.com/triton-lang/triton/issues/1756 ) + # So far we see a fixed 8 spilled registers for kernels using sin/cos. + # Raise the threshold to 16 to be safe. + # We should revisit this once we understand more of the source of register spills. + spill_threshold: int = 32 if torch.version.hip else 16 + + # Generate code containing the newer tl.make_block_ptr() API for loads/store + use_block_ptr = False + + # (Experimental) + # Generate code using the tl.make_tensor_descriptor() API for loads/store + # [Note: TMA API Restrictions] Currently the TMA API requires the following: + # - For Nvidia GPUs, the compute capability should be >= 9.0 + # - The innermost stride of a descriptor should be 1 + # - The size of the block shape in the innermost dimension should load / store + # at least 16 bytes. + # - Tensors are 16 byte aligned. Enabling this option therefore requires + # assume_aligned_inputs to also be enabled + # TMA descriptors are only going to be generated if the above conditions + # can be satisfied, along with any existing requirements for index expressions + use_tensor_descriptor = False + + # (Experimental) + # Whether to allow reordering tensor descriptor matches with descending + # strides, at the expense of transposing values after load / before store. + transpose_discontiguous_tensor_descriptor = True + + # Inject a bug into our relu implementation; useful for testing our repro + # extraction and minification functionality. + # Valid values: "compile_error", "runtime_error", "accuracy" + inject_relu_bug_TESTING_ONLY: str | None = None + + # Whether to upcast float16 / bfloat16 to float32 in triton codegen (Experimental) + codegen_upcast_to_fp32 = True + + # Whether persistent matmul kernels should be enabled. On NVIDIA H100+ with TMA support, + # this enables TMA persistent kernels. On AMD GPUs without TMA, this enables + # non-TMA persistent kernels as a fallback. + enable_persistent_tma_matmul = ( + os.environ.get("ENABLE_PERSISTENT_TMA_MATMUL", "0") == "1" + ) + # Should TMA store be enable from templates. TODO: Remove once we + # can autotune over the result. + enable_template_tma_store = os.environ.get("ENABLE_TEMPLATE_TMA_STORE", "0") == "1" + # Skip L1 cache for buffers that are used only once. Disabled by default + skip_l1_cache = os.environ.get("TORCHINDUCTOR_SKIP_L1", "0") == "1" + + # During autotuning, if one of the kernels/configs fails for some reason, + # Inductor will usually skip it (and assign its latency to inf). + # For testing it's helpful to be able to assert that none of the configs fail. + # Note: it may also need to be used with config.compile_threads = 1 + disallow_failing_autotune_kernels_TESTING_ONLY = False + + # specify number of splits to autotune on for decompose_k. 0 disables decompose_k + # Disabled on ROCm by default pending performance validation. + num_decompose_k_splits = int( + os.environ.get( + "TORCHINDUCTOR_NUM_DECOMPOSE_K_SPLITS", "0" if torch.version.hip else "10" + ) + ) + + # specify minimum ratio of K to M AND N in order to autotune on decompose_k. 0 enables + # it as an autotuning choice for all matmuls + decompose_k_threshold = int( + os.environ.get("TORCHINDUCTOR_DECOMPOSE_K_THRESHOLD", "32") + ) + + # Programmatic Dependent Launch improves launch latency on Nvidia Hopper+ devices + # If set to true, will generate PDL code on devices that support it. + # If set to false, will never generate PDL code. + enable_pdl = os.environ.get("TORCHINDUCTOR_ENABLE_PDL", "0") == "1" + + mix_order_reduction = ( + os.environ.get("TORCHINDUCTOR_MIX_ORDER_REDUCTION", "0" if is_fbcode() else "1") + == "1" + ) + mix_order_reduction_initial_xblock = 1 + + mix_order_reduction_split_size: int | None = None + mix_order_reduction_autotune_split_size = ( + os.environ.get("TORCHINDUCTOR_MIX_ORDER_REDUCTION_AUTOTUNE_SPLIT_SIZE", "0") + == "1" + ) + # If set to true, will skip some non-critical checks in the mix order reduction + # this could be helpful to avoid recompilations in some cases + mix_order_reduction_non_strict_mode = False + + # Don't allow multi-stages by default to avoid out of shared memory + mix_order_reduction_allow_multi_stages = ( + os.environ.get("TORCHINDUCTOR_MIX_ORDER_REDUCTION_ALLOW_MULTI_STAGES") == "1" + ) + + # Map for storing the amount of kernel runs with dumped input tensors + # Based on hash of Triton source code to avoid bloating the folder + debug_dump_kernel_inputs: dict[str, int] = {} + + # Value for the maximum amount of runs with dumped kernel input tensors + # When the maximum is reached the first values get overwritten + # This ensures the last N runs are saved, where N is this value + max_kernel_dump_occurrences = 3 + + proton_profiling: bool = ( + os.environ.get("TORCHINDUCTOR_TRITON_PROTON_PROFILING", "0") == "1" + ) + # If not specified, proton traces will be saved to the debug directory + proton_output_dir: str | None = os.environ.get( + "TORCHINDUCTOR_TRITON_PROTON_OUTPUT_DIR" + ) + # Group CTAs by SM in proton trace files. + proton_group_by_sm: bool = ( + os.environ.get("TORCHINDUCTOR_TRITON_PROTON_GROUP_BY_SM", "1") == "1" + ) + # Split proton trace files by kernel invocation. + proton_split_invocations: bool = ( + os.environ.get("TORCHINDUCTOR_TRITON_PROTON_SPLIT_INVOCATIONS", "1") == "1" + ) + # Process warp tracks into CTA tracks (min warp start, max warp end) and + # assign CTAs to slots per SM such that CTAs do not overlap. + proton_per_cta_occupancy: bool = ( + os.environ.get("TORCHINDUCTOR_TRITON_PROTON_PER_CTA_OCCUPANCY", "1") == "1" + ) + + +class aot_inductor: + """ + Settings for Ahead-Of-Time Inductor Compilation + """ + + # AOTInductor output path + # If an absolute path is specified, the generated lib files will be stored under the directory; + # If a relative path is specified, it will be used as a subdirectory under the default caching path; + # If not specified, a temp directory will be created under the default caching path. + # If the specified path contains something like "model.so", the sub-string will be used + # to name the generated library. + output_path = "" + + debug_compile = os.environ.get("AOT_INDUCTOR_DEBUG_COMPILE", "0") == "1" + debug_symbols = os.environ.get("AOT_INDUCTOR_DEBUG_SYMBOLS", "0") == "1" + + # Enable frame pointers for profiling tools (e.g. strobelight) + enable_frame_pointer = ( + os.environ.get("AOT_INDUCTOR_ENABLE_FRAME_POINTER", "0") == "1" + ) + + # Annotate generated main wrapper function, i.e. AOTInductorModel::run_impl, + # to use which cpp compiler optimization level, default to O1 + compile_wrapper_opt_level = os.environ.get( + "AOT_INDUCTOR_COMPILE_WRAPPER_OPT_LEVEL", "O1" + ) + + # option for debug printing/saving for intermediate tensor values for aot inductor + # 0: disable debug dumping + # 1: enable saving intermediate tensor values + # 2: enable printing intermediate tensor values + # 3: enable printing kernel names only (useful for pinpointing troublesome kernels) + # pyrefly: ignore [bad-assignment] + debug_intermediate_value_printer: Literal["0", "1", "2", "3"] = os.environ.get( + "AOT_INDUCTOR_DEBUG_INTERMEDIATE_VALUE_PRINTER", "0" + ) # type: ignore[assignment] + + # filtered nodes to be printed for debug values. Specify this option when debug_intermediate_value_printer is set to 2 + filtered_kernel_names = os.environ.get( + "AOT_INDUCTOR_FILTERED_KERNELS_TO_PRINT", None + ) + + # Serialized tree spec for flattening inputs + # TODO: Move this into metadata + serialized_in_spec = "" + + # Serialized tree spec for flattening outputs + # TODO: Move this into metadata + serialized_out_spec = "" + + # flag to decide whether to create a submodule for constant graph. + use_runtime_constant_folding: bool = False + + # flag to force weight to be appended to the shared library and mapped by the runtime + # rather than embedded into the data section. Needed to support 1B+ parameter models + force_mmap_weights: bool = False + + # Default value of use_consts_asm_build is True, it will build by assembly language. + # When the value is False, it will build by c++ language. + use_consts_asm_build = True + + package: bool = False + package_cpp_only: bool | None = None + + # If package_cpp_only is True, whether cpp files will be compiled to a + # dynamically linked library or static linked library + dynamic_linkage: bool = True + + # Dictionary of metadata users might want to save to pass to the runtime. + # TODO: Move this somewhere else, since it's no longer really a config + metadata: dict[str, str] = {} + + # fbcode only. Whether to raise error if C++ codegen is too big to optimize + raise_error_on_ignored_optimization: bool = ( + os.environ.get("AOTINDUCTOR_RAISE_ERROR_ON_IGNORED_OPTIMIZATION", "1") == "1" + ) + + # Whether to check lowerbound constraints on dynamic shapes during runtime. + # When disabled, allows models with dynamic sizes of 0 or 1 to work with + # AOTI_RUNTIME_CHECK_INPUTS=1, avoiding errors from the [2+, ...] lowerbound + # restriction when backed_size_oblivious is off. + check_lowerbound: bool = True + + # dump an aoti minifier if program errors + dump_aoti_minifier: bool = os.environ.get("DUMP_AOTI_MINIFIER", "0") == "1" + + # Compiler compilation debug info + # 1: Dumps the original graph out to repro.py if compilation fails + # 2: Dumps a minifier_launcher.py if aoti fails. + # 3: Always dumps a minifier_launcher.py. Good for segfaults. + # 4: Dumps a minifier_launcher.py if the accuracy fails. + repro_level: int = int(os.environ.get("AOTINDUCTOR_REPRO_LEVEL", 2)) + + # Dictionary of presets that can be passed in + presets: dict[str, Any] = {} + + # Kill switch for allowing temporary tensors to be allocated as stack arrays. Tests + # should be run with this flag both on and off to make sure we have coverage. + allow_stack_allocation: bool = False + + # Enables an alternate DSO interface (the "minimal ArrayRef interface") intended + # to maximize performance for use cases that it can accommodate at the expense of + # generality. In brief: + # - inputs and outputs are ArrayRefTensor (note that strides are required, but the + # tensor must be contiguous) + # - constant handling is unchanged because it is not a per-inference-iteration bottleneck + # + # When the DSO is generated in this mode, the usual interface will also be supported, + # but performance for that interface may be degraded. + use_minimal_arrayref_interface: bool = False + + # Set to True if we want to use Pytorch's CUDACachingAllocator for weight management + weight_use_caching_allocator: bool = ( + os.environ.get("AOT_INDUCTOR_WEIGHT_USE_CACHING_ALLOCATOR", "0") == "1" + ) + + # Experimental. Flag to control whether to include weight in .so + # Not supported for cross_target_platform="windows". + package_constants_in_so: bool = True + + # Experimental. Flag to control whether to package weight separately on disk and which + # format to package it in. + # Options: + # None: + # Do not package weight separately on disk. + # "pickle_weights": + # Each weight is pickled and stored separately in data/weights. We also store the + # FQN names of each weight in a weights_config.json in each model's data/aot_inductor/model folder. + # Can only be load back from python using torch._inductor.aoti_load_package API now. + # "binary_blob": + # Stores all weights in a single binary blob in data/aot_inductor/model folder for each model. + # This option and config.aot_inductor.force_mmap_weights cannot both be True + package_constants_on_disk_format: str | None = None + + # Experimental. Controls automatic precompiling of common AOTI include files. + precompile_headers: bool = not is_fbcode() + + # Embed generated kernel binary files into model.so + embed_kernel_binary: bool | None = None + + # Generate kernel files that support multiple archs + # For CUDA, this means generating fatbin files for kernels, and the fatbin files + # contains PTX and SASS for the current architecture. + # For XPU, this means generating SPIR-V files for kernels, and the SPIR-V files + # will be compiled to target different XPU architectures at runtime. + emit_multi_arch_kernel: bool | None = None + + # If not None, the generated files with use this name in file stem. + # If None, we will use a hash to name files. + # + # If package_cpp_only, this name is also used for the target name in CMakelists.txt + # The default target name is "aoti_model" + # + # If compile_standalone, the aoti model class name is f"AOTInductorModel{name}" + # + # This name can only contain letters, numbers, and underscores. + model_name_for_generated_files: str | None = None + + # Custom ops that have implemented C shim wrappers, defined as an op to C shim declaration dict + custom_ops_to_c_shims: dict[torch._ops.OpOverload, list[str]] = {} + # custom op libs that have implemented C shim wrappers + custom_op_libs: list[str] | None = None + + # Whether to enable link-time-optimization + enable_lto = os.environ.get("AOT_INDUCTOR_ENABLE_LTO", "0") == "1" + + # Whether the compiled .so should link to libtorch + link_libtorch: bool = True + + # Currently the only valid option is "windows". + # We'll use x86_64-w64-mingw32-gcc to cross-compile a .dll file + # If using cuda, you also need to set WINDOWS_CUDA_HOME env var + # to point to windows CUDA toolkit. + # Example: WINDOWS_CUDA_HOME=cuda-windows-base/cuda_cudart/cudart/ + # The path should contain lib cuda and lib cudart + cross_target_platform: str | None = None + + # If link_libtorch is False and cross_target_platform is windows, + # a library needs to be provided to provide the shim implementations. + aoti_shim_library: str | list[str] | None = None + aoti_shim_library_path: str | None = None + + +# a convenient class that automatically sets a group of the configs in aot_inductor +# it should only control the flags in aot_inductor. +# it should not do anything else. +class aot_inductor_mode: + # dynamic_linkage=False + # link_libtorch=False + # package_cpp_only=True + # embed_kernel_binary=True + # emit_multi_arch_kernel=True + compile_standalone: bool = False + + +class cutlass: + """ + Config specific to cutlass backend. + """ + + compile_opt_level: Literal["-O0", "-O1", "-O2", "-O3", "-OS"] = "-O1" + + # Whether to enable debug info, e.g. line number, cutlass debug info. + enable_debug_info = False + + # Whether to use fast math. + use_fast_math = False + + # Path to the CUTLASS repo root directory. + # The default path only works under PyTorch local development environment. + cutlass_dir = os.path.realpath( + os.environ.get( + "TORCHINDUCTOR_CUTLASS_DIR", + os.path.join( + os.path.dirname(torch.__file__), + "../third_party/cutlass/", + ), + ) + ) + + # Configures the maximum number of CUTLASS configs to profile in max_autotune. + # By default it's None, so that all CUTLASS configs are tuned. + # This is mainly used to reduce test time in CI. + cutlass_max_profiling_configs: int | None = None + + # The L2 swizzle values to consider when profiling CUTLASS configs in max_autotune. + cutlass_max_profiling_swizzle_options: list[int] = [1, 2, 4, 8] + + cutlass_dynamic_cluster_shape: tuple[int, int, int] = cast( + tuple[int, int, int], + tuple( + int(x) + for x in os.environ.get( + "TORCHINDUCTOR_CUTLASS_DYNAMIC_CLUSTER_SHAPE", "2,1,1" + ).split(",") + ), + ) + cutlass_dynamic_cluster_fallback: tuple[int, int, int] = cast( + tuple[int, int, int], + tuple( + int(x) + for x in os.environ.get( + "TORCHINDUCTOR_CUTLASS_DYNAMIC_CLUSTER_FALLBACK", + ",".join(str(v) for v in cutlass_dynamic_cluster_shape), + ).split(",") + ), + ) + + # Whether to use CUTLASS EVT for epilogue fusion + cutlass_epilogue_fusion_enabled = ( + os.environ.get("CUTLASS_EPILOGUE_FUSION", "0") == "1" + ) + + # Whether to only use TMA-compatible kernels in CUTLASS + cutlass_tma_only = False + + # Minimum value of M*N*K to consider the CUTLASS backend for GEMM ops. + cutlass_backend_min_gemm_size: int = 1 + + # enable generation of inline standalone runner in CUDA CPP generated code + # which allows to compile the generated code into a standalone executable. + generate_test_runner: bool = ( + os.environ.get("INDUCTOR_CUDA_BACKEND_GENERATE_TEST_RUNNER_CODE", "0") == "1" + ) + + # Keep only Cutlass op configs which contain this regular expression pattern + # Set this to "warpspecialized_cooperative_epi_tma" to enable only SM90 TMA Cutlass Kernels for large GEMMs + cutlass_op_allowlist_regex: str | None = os.environ.get( + "TORCHINDUCTOR_CUTLASS_ALLOWLIST" + ) + + # Note: Names of Cutlass ops names can be obtained by calling + # op.configuration_name() on a Cutlass op instance, for example those + # returned from cutlass_utils.gen_ops() or the op argument passed to + # CUTLASSGemmTemplate.render(...) + + # Filter Cutlass configs which contain this regular expression pattern + # Set this to "pingpong" to avoid numerical issues + # caused by the op ordering of the "pingpong" memory access + # pattern used by some Cutlass Kernels. + cutlass_op_denylist_regex: str | None = os.environ.get( + "TORCHINDUCTOR_CUTLASS_DENYLIST" + ) + + # Non-negative integer which determines how many kernels are instantiated. + # 0 = 0000 generates the fewest kernels, 9999 generates all possible combinations. + # increasing first digit reduces schedule / mixed type pruning, + # increasing second digit generates more cluster sizes, + # increasing third digit generates more MMA multipliers, + # increasing fourth digit generates more instruction shapes. + cutlass_instantiation_level: str = os.environ.get( + "TORCHINDUCTOR_CUTLASS_INSTANTIATION_LEVEL", "0" + ) + + # use compile command to create kernel .cu and .so name + cutlass_hash_with_compile_cmd: bool = ( + os.environ.get("TORCHINDUCTOR_CUTLASS_HASH_WITH_COMPILE_CMD", "0") == "1" + ) + + # Experimental. Prescreen top x configs before tuning on swizzle. + cutlass_prescreening: bool = ( + os.environ.get("TORCHINDUCTOR_CUTLASS_PRESCREENING", "1") == "1" + ) + + # Specify which operations should use CUTLASS backend + # Comma-separated list like "mm,addmm,bmm", "all" for all operations, and "" for none. + # Acceptable operations: mm, int_mm, addmm, sparse_semi_structured_mm, bmm, scaled_mm + cutlass_enabled_ops: str = os.environ.get( + "TORCHINDUCTOR_CUTLASS_ENABLED_OPS", "all" + ) + + # Whether to consult the binary remote cache + use_binary_remote_cache: bool = True + + # Whether to upload compiled kernels to remote cache + upload_to_binary_remote_cache: bool = False + + # Whether to force upload if the key already exists + # Use this to overwrite and handle cache pollution + binary_remote_cache_force_write: bool = False + + # Enable caching codegen of cuda templates. + enable_caching_codegen: bool = True + + +@inherit_fields_from(cutlass) +class cuda(cutlass): + # CUDA arch to use for CUDA template kernel compilation. + # e.g. "70", "75", "80", "90", etc. + # When arch is None, Inductor uses torch.cuda.get_device_capability(0). + arch: str | None = None + + # CUDA version to use for CUDA template kernel compilation. + # e.g. "11.4", "12.1", etc. + # When version is None, Inductor uses torch.version.cuda. + version: str | None = None + + # Path to CUDA NVCC. + # NVCC search order: + # 1) cuda_cxx set in this config + # 2) CUDACXX environment variable + # 3) CUDA_HOME environment variable + # 4) default system search PATH. + cuda_cxx: str | None = None + + # Whether to enable device LTO (link-time-optimization). + enable_cuda_lto = False + + # Whether to keep intermediate files dring compilation. + enable_ptxas_info = False + + # Configures the maximum number of NVIDIA Universal GEMM (NVGEMM) configs to profile in max_autotune. + # By default it's 5, to keep compile time to a reasonable level. + nvgemm_max_profiling_configs: int | None = 5 + + +@inherit_fields_from(cutlass) +class xpu(cutlass): + # Xe arch to use for SYCL kernel compilation. + # eg. 12, 20, which corresponding to Xe12(PVC) and Xe20 (BMG) + arch: str | None = None + # oneAPI version to use for SYCL kernel compilation. + # e.g. "20250201". + version: str | None = None + + # Path to Intel OneAPI. + oneapi_root: str | None = None + + cutlass_dir = os.path.realpath(os.environ.get("TORCHINDUCTOR_CUTLASS_DIR", "")) + + +class rocm: + # Offload arch list for device code compilation, e.g. ["gfx90a", "gfx942"]. + # If empty, the `native` arch is used + arch: list[str] = [] + + # Enable the CK backend for CDNA2 and CDNA3 only (for now) + # Processor name reference: https://llvm.org/docs/AMDGPUUsage.html#processors + ck_supported_arch: list[Literal["gfx90a", "gfx942", "gfx950"]] = [ + "gfx90a", + "gfx942", + "gfx950", + ] + + # Optimization level, use to balance compilation speed and runtime performance. + # The type will not necessarily be comprehensive and won't be enforced at runtime. + compile_opt_level: Literal[ + "-O0", "-O1", "-O2", "-O3", "-Os", "-Oz", "-Omin", "-Ofast", "-Omax" + ] = "-O2" + + # Flag to keep debug information in compiled objects + is_debug = False + + # Flag to keep intermediate files (assembly listings, preprocessed sources, etc.) + save_temps = False + + # Flag to add `-ffast-math`` to compile flags + use_fast_math = True + + # Flag to add `-fgpu-flush-denormals-to-zero` to compile flags + flush_denormals = True + + # Flag to print register and LDS usage during compilation + print_kernel_resource_usage = False + + # Path to ROCm installation, if None, use env variable ROCM_HOME. + # In fbcode see triton/fb/TARGETS for how ROCM_HOME gets set. + rocm_home: str | None = None + + # Path to Composable Kernel library. + # Install with `pip install git+https://github.com/rocm/composable_kernel@develop`. + ck_dir = os.environ.get("TORCHINDUCTOR_CK_DIR") + + # generate standalone executables for instances generated with the CK backend + generate_test_runner: bool = ( + os.environ.get("INDUCTOR_CK_BACKEND_GENERATE_TEST_RUNNER_CODE", "0") == "1" + ) + + # Deprecated, use CK and/or CK-tile specific settings + n_max_profiling_configs: int | None = None + + # Number of op instance choices to trade off between runtime perf and compilation time + # For CK Kernels + ck_max_profiling_configs: int | None = None + + # Number of op instance choices to trade off between runtime perf and compilation time + # For CK-Tile Kernels + ck_tile_max_profiling_configs: int | None = None + + # Flag to use a short list of CK instances which perform well across a variety of shapes. + # Currently RCR and F16 only + use_preselected_instances: bool = False + + # List to determine kBatch parameters to sweep over. By default, we calculate one in splitK + # scenarios, and run on kBatch=1 in non-splitK scenarios + kBatch_sweep: list[int] | None = None + + # The threshold at which we trigger a splitK config - K // max(M,N) has to be greater than this + split_k_threshold: int = 16 + + # The threshold at which we trigger a contiguous subgraph transformation + contiguous_threshold: int = 16 + + +# Backend to use for CPU codegen either "cpp" or "triton" (experimental) or "halide" (experimental) or "pallas" (experimental) +cpu_backend: Literal["cpp", "triton", "halide", "pallas"] = "cpp" + +# Backend to use for CUDA codegen either +# "triton", "halide" (experimental) or "pallas" (experimental) +cuda_backend: Literal["triton", "halide", "pallas"] = "triton" + +# Backend to use for TPU codegen +tpu_backend: Literal["pallas"] = "pallas" + +# Backend to use for XPU codegen either "triton" +xpu_backend: Literal["triton"] = "triton" + + +class mtia: + # Configuration to force inductor to never use welford reductions for MTIA backend + disable_welford_reduction = False + + +class halide: + # Base halide target to use for CPU devices + cpu_target = "host" + + # Base halide target to use for CUDA devices + gpu_target = "host-cuda" + + # Halide autoscheduler to use, choices are: + # "Anderson2021" (gpu-only), "Li2018", "Adams2019" (cpu-only), or "Mullapudi2016" (cpu-only) + scheduler_cuda: Literal["Anderson2021", "Li2018", "Adams2019", "Mullapudi2016"] = ( + "Anderson2021" + ) + scheduler_cpu: Literal["Anderson2021", "Li2018", "Adams2019", "Mullapudi2016"] = ( + "Adams2019" + ) + + # Controls `no_asserts` flag passed to Halide target (warning: can false positive) + asserts = False + + # Controls `debug` flag passed to Halide target + debug = False + + # Enable (or fallback on) scan kernels such as cumsum + # Halide autoschedulers struggle with these kernels + scan_kernels = False + + +# create a directory containing lots of debug information +class trace: + # master switch for all debugging flags below + enabled = os.environ.get("TORCH_COMPILE_DEBUG", "0") == "1" + + # save real tensors + save_real_tensors = os.environ.get("TORCH_COMPILE_DEBUG_SAVE_REAL", "0") == "1" + + # Save debug information to a temporary directory + # If not specified, a temp directory will be created by system + debug_dir: str | None = None + + # Save python logger call >=logging.DEBUG + debug_log = False + + # Save python logger call >=logging.INFO + info_log = False + + # Save input FX graph (post decomps, pre optimization) + fx_graph = True + + # Save FX graph after transformations + fx_graph_transformed = True + + # Save TorchInductor IR before fusion pass + ir_pre_fusion = True + + # Save TorchInductor IR after fusion pass + ir_post_fusion = True + + # Copy generated code to trace dir + output_code = True + + # SVG figure showing post-fusion graph + graph_diagram = os.environ.get("INDUCTOR_POST_FUSION_SVG", "0") == "1" + + # SVG figure showing fx with fusion + draw_orig_fx_graph = os.environ.get("INDUCTOR_ORIG_FX_SVG", "0") == "1" + + # We draw our fx graphs with the "record" shape attribute by default. + # Sometimes, when the graph is very complex, we may hit dot errors like below: + # "flat edge between adjacent nodes one of which has a record shape - + # replace records with HTML-like labels" + # and thus fail to generate a graph. So, let's give the user an option + # to specify the shape attribute for the dot graph. For example, passing + # INDUCTOR_DOT_GRAPH_SHAPE_SVG = "none" would let us generate HTML-like labels + # to workaround the above failure. + dot_graph_shape = os.environ.get("INDUCTOR_DOT_GRAPH_SHAPE_SVG", None) + + # If not None, this is the URL that saves the SVG files of the input/output + # graph of each pass that changed the graph + # The nodes that are being transformed in each pass will be colored in yellow + # URL only supports local directory for now + log_url_for_graph_xform = os.environ.get("INDUCTOR_LOG_URL_FOR_GRAPH_XFORM", None) + + # Store cProfile (see snakeviz to view) + compile_profile = False + + # Upload the .tar.gz file + # Needs to be overridden based on specific environment needs + upload_tar: Callable[[str], None] | None = None + + log_autotuning_results = os.environ.get("LOG_AUTOTUNE_RESULTS", "0") == "1" + + # Save mapping info from inductor generated kernel to post_grad/pre_grad fx nodes + # Levels: + # 0 - disabled (default) + # 1 - normal + # 2 - basic + # Backward compatibility: + # If TORCH_COMPILE_DEBUG=1, level is set to at least 1. + # If INDUCTOR_PROVENANCE is set, use its integer value. + provenance_tracking_level: int = int( + os.environ.get( + "INDUCTOR_PROVENANCE", os.environ.get("TORCH_COMPILE_DEBUG", "0") + ) + ) + + +_save_config_ignore: list[str] = [ + # workaround: "Can't pickle " + "trace.upload_tar", + "joint_custom_pre_pass", + "joint_custom_post_pass", + "pre_grad_custom_pass", + "aot_inductor.repro_level", + "aot_inductor.dump_aoti_minifier", + "post_grad_custom_pre_pass", + "post_grad_custom_post_pass", + "_fuse_ddp_communication_passes", + "_pre_fusion_custom_pass", + # CUDAGraphPolicy objects are not picklable and only affect + # post_compile wrapping, not compiled code itself. + "cudagraph_policy", +] + +_cache_config_ignore_prefix: list[str] = [ + # trace functions are not relevant to config caching + "trace", + # uses absolute path + "cuda.cutlass_dir", + "cutlass.cutlass_dir", + "xpu.cutlass_dir", + # not relevant + "worker_start_method", + "compile_threads", + # see CustomGraphPass; these are handled specially + "post_grad_custom_post_pass", + "post_grad_custom_pre_pass", + "joint_custom_pre_pass", + "joint_custom_post_pass", + "pre_grad_custom_pass", + "_fuse_ddp_communication_passes", + "_pre_fusion_custom_pass", + # CUDAGraphPolicy only affects post_compile, not compiled output + "cudagraph_policy", + # tests assume that changes here don't invalidate cache + "always_complex_memory_overlap_TESTING_ONLY", + # timing affects cache structure, not cache content + "pre_grad_pass_timing", + # cache related options are not relevant to cache results + "fx_graph_cache", + "fx_graph_remote_cache", + "autotune_local_cache", + "autotune_remote_cache", +] + +# Config keys whose values are callable factories. save_config_portable will +# instantiate the factory and use .uuid() for serialization. +_cache_config_factory_keys: list[str] = [ + "inductor_choices_class", +] + +# External callable for matmul tuning candidates +external_matmul: list[Callable[[torch.Tensor, torch.Tensor, torch.Tensor], None]] = [] + +write_are_deterministic_algorithms_enabled = ( + os.getenv("TORCHINDUCTOR_WRITE_ARE_DETERMINISTIC_ALGORITHMS_ENABLED", "1") == "1" +) + + +class lookup_table: + # Lookup table for template config overrides + table: dict[str, list[dict[str, Any]]] | None = None + + # Enable template src_hash checking in lookup table to prevent using stale configs. + # If True, configs with 'template_hash' field will be compared against the template's + # src_hash at runtime and filtered out if they don't match. If False, no + # hash checking is performed. + check_src_hash: bool = True + + +class test_configs: + force_extern_kernel_in_multi_template: bool = False + + # Force custom op autotuning choice selection: + # - None: normal autotuning (default) + # - True: force decomposition to win + # - False: force fallback to win + force_custom_op_decomposition: bool | None = None + + # Force custom op autotuning to prevent grouping ranges by implementation. + # When True, each range is treated as a separate impl group, forcing torch.cond dispatch. + force_no_impl_grouping: bool = False + + max_mm_configs: int | None = None + + runtime_triton_dtype_assert = False + runtime_triton_shape_assert = False + static_cpp_dtype_assert = False + + # regex to control the set of considered autotuning + # choices (aka configs) by name and / or description + # Can be set via TORCHINDUCTOR_AUTOTUNE_CHOICE_NAME_REGEX and + # TORCHINDUCTOR_AUTOTUNE_CHOICE_DESC_REGEX environment variables + + autotune_choice_name_regex: str | None = os.environ.get( + "TORCHINDUCTOR_AUTOTUNE_CHOICE_NAME_REGEX" + ) + autotune_choice_desc_regex: str | None = os.environ.get( + "TORCHINDUCTOR_AUTOTUNE_CHOICE_DESC_REGEX" + ) + + graphsafe_rng_func_ignores_fallback_random = False + + track_memory_lifecycle: Literal["assert", "log"] | None = None + + # If set to True, AOTI-generated CMakelists.txt will still use libtorch + # for unit testing + use_libtorch = False + + # Assume bucketing reduces latency (mostly for testing) + assume_bucketing_reduces_latency: bool = True + + # A test config to ease the test for perf of reduction config filtering + force_filter_reduction_configs = ( + os.getenv("TORCHINDUCTOR_FORCE_FILTER_REDUCTION_CONFIGS") == "1" + ) + + # a testing config to distort benchmarking result + # - empty string to disable + # - "inverse" to inverse the numbers + # - "random" return a random value + distort_benchmarking_result = os.getenv( + "TORCHINDUCTOR_DISTORT_BENCHMARKING_RESULT", "" + ) + + bisect_pre_grad_graph = False + bisect_keep_custom_backend_for_inductor = False + + +if TYPE_CHECKING: + from torch.utils._config_typing import * # noqa: F401, F403 + + +class eager_numerics: + # x / y in Triton is lowered to div.full which is approx + # PyTorch eager uses the equivalent of Triton's div_rn, which can + # come at a performance penalty + division_rounding: bool = ( + os.environ.get("TORCHINDUCTOR_EMULATE_DIVISION_ROUNDING", "0") == "1" + ) + + disable_ftz: bool = False + + # Use the CUDA toolkit's libdevice instead of Triton's bundled version. + # Triton bundles its own libdevice.10.bc which may use different polynomial + # coefficients than CUDA's version, causing ~1 ULP differences in pow. + use_pytorch_libdevice: bool = False + + +# Mode to emulate PyTorch eager numerics when doing lower precision compute +# (fp16, bf16). PyTorch eager computes bf16/fp16 by upcasting inputs to fp32 +# and downcasting after. When two low precision operators are fused together, +# Inductor will elide the downcast-upcast pairs (effectively a precision +# truncation) that would occur between these two operators. Typically, +# Inductor's behavior should be closer to fp64 ref numerics. However, with +# this knob you can ensure the downcast-upcast are preserved so that you can +# emulate the eager numerics. +emulate_precision_casts: bool = ( + os.environ.get("TORCHINDUCTOR_EMULATE_PRECISION_CASTS", "0") == "1" +) + +# adds patch, save_config, etc +install_config_module(sys.modules[__name__]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/config_comms.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/config_comms.py new file mode 100644 index 0000000000000000000000000000000000000000..deed06c54990c751c79ff2a003c15729e76a89e0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/config_comms.py @@ -0,0 +1,71 @@ +import os +import sys + +from torch.utils._config_module import install_config_module + + +# Whether to use c10d._time_estimator for collectives runtime estimations. +runtime_estimations_use_nccl_lib_estimations: bool = False + +# Config to enable sync of runtime estimations across distributed ranks, +# To prevent passes using this runtime estimations to make different +# decisions on different distributed ranks. +runtime_estimations_align_across_all_distributed_ranks: bool = False + +reorder_iterative_debug_memory_recompute: bool = False +reorder_iterative_debug_limit_to_reorder: int | None = ( + None + if (env_str := os.getenv("PYTORCH_REORDER_COLLECTIVES_LIMIT")) is None + else int(env_str) +) +sink_waits_iterative_debug_limit_to_sink: int | None = ( + # pyrefly: ignore[unbound-name] + None if (env_str := os.getenv("PYTORCH_SINK_WAITS_LIMIT")) is None else int(env_str) +) + + +# Should be used with config.runtime_estimations_mms_benchmark = True +reorder_iterative_use_runtime_estimations: bool = False +sink_iterative_use_runtime_estimations: bool = False + +# Broadcast runtime estimations doing real Collective operation between all ranks. +# If non-deterministic runtime estimations are used this must be used to make +# all ranks to do identical decisions and prevent global Collectives reordering, +# (that will result un NCCL hangs) +reorder_for_compute_comm_overlap_broadcast_runtime_estimations: bool = False + +# Block of Ratios to workaround imperfection of current runtime estimations +# for collectives and compute for different scenarios. +# Multiplier of collectives estimated durations +reorder_sink_runtime_estimations_comm_mult: float = 2.0 +# Multiplier of compute estimated durations +reorder_sink_runtime_estimations_non_comm_mult: float = 1.0 +# The reordering will stop to reorder +# when overlap_comp >= (1 + extra_overlap_ratio) * comm_time +# Allows to configure more aggressive overlap +reorder_iterative_extra_comm_comp_overlap: float = 0.5 +# The sink waits reordering will stop to reorder +# when overlap_comp >= (1 + extra_overlap_ratio) * comm_time +# Allows to configure more aggressive sink waits +sink_iterative_extra_comm_comp_overlap: float = 0.5 + +# Allow reorder iterative pass to increase peak memory +# up to peak_memory_before_pass * (1 + budget) +reorder_iterative_peak_memory_budget: float = 0.2 +# Allow sink waits iterative pass to increase peak memory +# up to peak_memory_before_pass * (1 + budget) +sink_iterative_peak_memory_budget: float = 0.2 + +# Experimental unsafe configuration that allows changing relative collectives order. +# Must be used with runtime_estimations_align_across_all_distributed_ranks = True +reorder_iterative_unsafe_collectives_reorder: bool = True +sink_waits_iterative_unsafe_collectives_reorder: bool = True + +# Allow group and move other collectives during reordering +reorder_iterative_group_with_collectives: bool = False +sink_waits_iterative_swap_with_collectives: bool = False + +reorder_sink_verbose_logging: bool = False + +# adds patch, save_config, etc +install_config_module(sys.modules[__name__]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/constant_folding.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/constant_folding.py new file mode 100644 index 0000000000000000000000000000000000000000..6e0ea4ebf46defedaaa5325585526d1cdd85d770 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/constant_folding.py @@ -0,0 +1,429 @@ +import collections +import typing_extensions +from collections.abc import Callable +from typing import Any + +import torch +import torch.utils._pytree as pytree +from torch._inductor.freezing_utils import maybe_set_is_frozen_param +from torch.utils._ordered_set import OrderedSet + + +aten = torch.ops.aten + +# We would like to split modules into two subgraphs for runtime weight updates to work correctly. +# The use case and more information could be found at: +# https://docs.google.com/document/d/1inZC-8KarJ6gKB7G9egmYLx1V_dKX_apxon0w4zPC0Q/edit?usp=sharing +META_TAG = "MODULE_TYPE" +MODULE_TAG = "_MAIN_MODULE" +CONST_MODULE_TAG = "_CONST_MODULE" + +_dont_constant_fold: list[torch.fx.node.Target] = [] + + +def add_dont_constant_fold(op: torch.fx.node.Target) -> None: + global _dont_constant_fold + _dont_constant_fold.append(op) + + +def clear_dont_constant_fold() -> None: + global _dont_constant_fold + _dont_constant_fold.clear() + + +def replace_node_with_constant( + gm: torch.fx.GraphModule, + node: torch.fx.Node, + constant: torch.Tensor | None = None, + name: str | None = None, +) -> None: + g = gm.graph + + if name: + qualname = name + else: + if not hasattr(gm, "_frozen_param_count"): + gm._frozen_param_count = 0 # type: ignore[assignment] + i = gm._frozen_param_count + # pyrefly: ignore [bad-assignment] + while True: + qualname = f"_frozen_param{i}" + if not hasattr(gm, qualname): + break + i += 1 # type: ignore[assignment, operator] + + gm._frozen_param_count = i + 1 # type: ignore[assignment, operator] + + with g.inserting_before(node): + if constant is not None: + new_input_node = g.create_node("get_attr", qualname, (), {}) + else: + # this is the case for lifted constants + new_input_node = g.create_node("placeholder", qualname, (), {}) + node.replace_all_uses_with(new_input_node) + new_input_node.meta.update(node.meta) + g.erase_node(node) + new_input_node.name = node.name + + if constant is not None: + # needed to suppress `does not reference an nn.Module, nn.Parameter, or buffer` warning + gm.register_buffer(qualname, constant) + setattr(gm, qualname, constant) + # mark any constants created during freezing + maybe_set_is_frozen_param(constant) + + +def is_const_source( + node: torch.fx.Node, lifted_constant_names: list[str] | None +) -> bool: + return node.op == "get_attr" or node.name in (lifted_constant_names or ()) + + +class ConstantFolder(torch.fx.Interpreter): + def __init__( + self, + gm: torch.fx.GraphModule, + skip_constructors: bool = False, + lifted_constant_names: list[str] | None = None, + skip_folding_node_fn: Callable[[torch.fx.Node], bool] | None = None, + ) -> None: + super().__init__(gm) + self.node_replacements: dict[torch.fx.Node, Any] = {} + self.replaced_uses: dict[torch.fx.Node, int] = collections.Counter() + self.unknown_value = object() + self.skip_constructors: bool = skip_constructors + + # overwrite this to deallocate env values if their only remaining use + # is the output + self.user_to_last_uses = self.node_to_last_non_output_use() + self.lifted_constant_names = lifted_constant_names + self.deferred_value = object() + self.skip_folding_node_fn = skip_folding_node_fn + + def _support_dynamic_shape(self) -> bool: + # ConstantFolder not support dynamic shape now + return False + + def _deduce_value(self, node: torch.fx.Node) -> Any: + if self.lifted_constant_names is None: + return super().run_node(node) + # if lifted_constant_names is passed in, no concrete value is available + # so we just check if all inputs have values + if self.skip_folding_node_fn is not None and self.skip_folding_node_fn(node): + return self.unknown_value + flattened_node_inps = pytree.arg_tree_leaves(*node.args, **node.kwargs) + for inp in flattened_node_inps: + if ( + isinstance(inp, torch.fx.Node) + and inp.name not in (self.lifted_constant_names or ()) + and self.env[inp] is not self.deferred_value + ): + return self.unknown_value + return self.deferred_value + + def is_impure(self, node: torch.fx.node.Node) -> bool: + def is_woq_int8_pattern(node: torch.fx.node.Node) -> bool: + return ( + node.target is torch.ops.prims.convert_element_type.default # type: ignore[return-value] + and isinstance(node.args[0], torch.fx.Node) + and "val" in node.args[0].meta + and node.args[0].meta["val"].dtype == torch.int8 # type: ignore[union-attr] + and node.args[1] == torch.bfloat16 + ) + + if ( + is_woq_int8_pattern(node) + or ( + node.target is torch.ops.aten.permute.default + and len(node.users) == 1 + and is_woq_int8_pattern(next(iter(node.users))) + ) + ) and is_const_source( + node.args[0], # type: ignore[arg-type] + self.lifted_constant_names, + ): + # Case 1: int8_weight -> dq -> bf16_weight + # Case 2: int8_weight -> permute -> dq -> bf16_weight + return True + + quant_registered = ( + getattr(torch.ops.quantized_decomposed, "dequantize_per_channel", None) + is not None + ) + if quant_registered and node.target in [ + torch.ops.quantized_decomposed.dequantize_per_channel.default, + torch.ops.quantized_decomposed.dequantize_per_tensor.default, + torch.ops.quantized_decomposed.dequantize_per_tensor.tensor, + torch.ops.quantized_decomposed.convert_element_type.no_fuse, + ]: + # For the pattern fp32_weight -> q -> dq + # We only folding fp32_weight -> q + # int8_weight and leave dq in graph to be fused + return True + + if node.target in _dont_constant_fold: + return True + return False + + def node_to_last_non_output_use(self) -> dict[torch.fx.Node, list[torch.fx.Node]]: + last_non_output_use = collections.defaultdict(list) + seen_uses = OrderedSet[torch.fx.Node]() + output_node = next(iter(reversed(self.module.graph.nodes))) # type: ignore[arg-type, union-attr] + + for node in reversed(self.module.graph.nodes): # type: ignore[arg-type, union-attr] + if node.target == "output": + continue + + def add_use(inp: torch.fx.Node) -> None: + if inp in seen_uses: + return + + seen_uses.add(inp) + last_non_output_use[node].append(inp) + + # In-place is fine since we don't mutate + pytree.tree_map_only_(torch.fx.Node, add_use, (node.args, node.kwargs)) + + # if this node is only used in output, we want to gc it right away + if len(node.users) == 1 and output_node in node.users: + last_non_output_use[node].append(node) + + return last_non_output_use + + @typing_extensions.override + def run_node(self, node: torch.fx.Node) -> Any: + if node.target == "output": + # because we remove nodes from env on last non output use, + # re-define them now or we'll get error in interpreter + def set_env(arg: torch.fx.Node) -> None: + self.env[arg] = self.unknown_value + + # In-place is fine since we don't mutate + pytree.tree_map_only_(torch.fx.Node, set_env, node.args) + return super().run_node(node) + + args, kwargs = self.fetch_args_kwargs_from_env(node) + flattened_inputs = pytree.arg_tree_leaves(*args, **kwargs) + + # We need to do this weird thing because in cases where flattened_inputs + # contains a ScriptObject, equality checking results in a type error if + # the types are different. + if any( + type(self.unknown_value) is type(input_) and self.unknown_value == input_ + for input_ in flattened_inputs + ): + return self.unknown_value + + # TODO - fix errors with this + if ( + node.op == "call_function" + and node.target is aten._efficientzerotensor.default + ): + return self.unknown_value + + # TODO - constant folding triton kernel returns the inputs -- fix this + if ( + node.op == "call_function" + and node.name == "triton_kernel_wrapper_functional_proxy" + ): + return self.unknown_value + + # skip constructors, since inductor generates optimal code for them already + # and turning into tensor would result in an additional global memory read + # TODO - more complicated strategy + if ( + self.skip_constructors + and not is_const_source(node, self.lifted_constant_names) + and not any(isinstance(e, torch.Tensor) for e in flattened_inputs) + ): + return self.unknown_value + + # All mutations should either be removed or on inputs which we did not make constant + if ( + isinstance(node.target, torch._ops.OpOverload) + and torch.Tag.nondeterministic_seeded in node.target.tags + ): + return self.unknown_value + + if node.op == "call_function" and isinstance( + node.target, torch._ops.HigherOrderOperator + ): + return self.unknown_value + + out = self._deduce_value(node) + + if isinstance( + out, + ( + torch._C.ScriptObject, + torch._library.fake_class_registry.FakeScriptObject, + ), + ): + return out + + if out is self.unknown_value: + return self.unknown_value + + if not is_const_source(node, self.lifted_constant_names) and ( + isinstance(out, torch.Tensor) or out is self.deferred_value + ): + if ( + out is not self.deferred_value + and out.device.type == "meta" # pyrefly: ignore[missing-attribute] + ): + return out + + if not self.insertable_tensor_check( + out # pyrefly: ignore[bad-argument-type] + ): + return out + + if self.is_impure(node): + return self.unknown_value + + self.add_node_replacement(node, out) # pyrefly: ignore[bad-argument-type] + + flattened_node_inps = pytree.arg_tree_leaves(*node.args, **node.kwargs) + + for n in flattened_node_inps: + if not isinstance(n, torch.fx.Node): + continue + + self.replaced_uses[n] += 1 + + for to_delete in self.user_to_last_uses.get(node, []): + if self.replaced_uses[to_delete] == len(to_delete.users): + self.node_replacements.pop(to_delete, None) + + return out + + def insertable_tensor_check(self, tensor: torch.Tensor) -> bool: + return True + + def add_node_replacement(self, node: torch.fx.Node, tensor: torch.Tensor) -> None: + self.node_replacements[node] = tensor + + def run(self) -> Any: # type: ignore[override] + env: dict[torch.fx.Node, Any] = {} + self.insert_placerholder_values(env) + return super().run(initial_env=env) + + def insert_placerholder_values(self, env: dict[torch.fx.Node, Any]) -> None: + for n in self.module.graph.find_nodes(op="placeholder"): # type: ignore[operator, union-attr] + env[n] = self.unknown_value # type: ignore[assignment] + if self.lifted_constant_names is None: + return + for n in self.module.graph.nodes: # type: ignore[union-attr] + if n.name in (self.lifted_constant_names or ()): + env[n] = self.deferred_value + + +def constant_fold( + gm: torch.fx.GraphModule, + constraint_fn: Callable[[torch.fx.Node], bool] | None = None, +) -> None: + with torch.utils._python_dispatch._disable_current_modes(): + cf = ConstantFolder(gm, skip_constructors=True) + cf.run() + + for node, constant in cf.node_replacements.items(): + if constraint_fn is not None and not constraint_fn(node): + continue + replace_node_with_constant(gm, node, constant) + + erased_params = [] + for node in gm.graph.find_nodes(op="get_attr"): + if len(node.users) == 0: + if hasattr(gm, node.target): + delattr(gm, node.target) + erased_params.append(node) + + for node in erased_params: + gm.graph.erase_node(node) + + gm.graph.eliminate_dead_code() + gm.graph.lint() + gm.recompile() + + +def constant_graph_tag( + gm: torch.fx.GraphModule, + skip_constructors: bool = True, + lifted_constant_names: list[str] | None = None, + skip_folding_node_fn: Callable[[torch.fx.Node], bool] | None = None, +) -> None: + with torch.utils._python_dispatch._disable_current_modes(): + cf = ConstantFolder( + gm, + skip_constructors=skip_constructors, + lifted_constant_names=lifted_constant_names, + skip_folding_node_fn=skip_folding_node_fn, + ) + cf.run() + + for node in gm.graph.nodes: + if skip_folding_node_fn is not None and skip_folding_node_fn(node): + node.meta[META_TAG] = MODULE_TAG + continue + if ( + is_const_source(node, lifted_constant_names) + or node in cf.node_replacements + or node in cf.replaced_uses + ): + node.meta[META_TAG] = CONST_MODULE_TAG + else: + node.meta[META_TAG] = MODULE_TAG + + +def run_and_get_constant_graph( + gm: torch.fx.GraphModule, + skip_constructors: bool = True, + lifted_constant_names: list[str] | None = None, + skip_folding_node_fn: Callable[[torch.fx.Node], bool] | None = None, +) -> torch.fx.GraphModule: + """ + Construct a GraphModule which corresponds to the part which could be + constant folded in provided gm. + """ + + constant_graph_tag( + gm, skip_constructors, lifted_constant_names, skip_folding_node_fn + ) + + def untag(node: torch.fx.Node) -> bool: + used_to_fold = False + for u in node.users: + if u.meta[META_TAG] == CONST_MODULE_TAG: + used_to_fold = True + break + if not used_to_fold: + node.meta[META_TAG] = MODULE_TAG + return used_to_fold + + # We rewrite the tags, if it's a constant being directly consumed, without + # any folding opportunity, we keep it in main gm. + for node in gm.graph.nodes: + if node.op == "get_attr" or (node.name in (lifted_constant_names or ())): + untag(node) + + new_graph = torch.fx.Graph() + + node_remapping: dict[torch.fx.Node, torch.fx.Node] = {} + output_nodes = [] + for node in gm.graph.nodes: + if node.meta[META_TAG] == MODULE_TAG: + continue + + new_node = new_graph.node_copy(node, lambda x: node_remapping[x]) + node_remapping[node] = new_node + + for user in node.users: + if user.meta[META_TAG] == MODULE_TAG: + output_nodes.append(new_node) + break + + new_graph.output(tuple(output_nodes)) + new_graph.lint() + new_gm = torch.fx.GraphModule(gm, new_graph) + + return new_gm diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/cpp_builder.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/cpp_builder.py new file mode 100644 index 0000000000000000000000000000000000000000..c4a801d9ba674bd1a267dcbd3068659eb1f47cde --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/cpp_builder.py @@ -0,0 +1,2698 @@ +# This CPP builder is designed to support both Windows and Linux OS. +# The design document please check this RFC: https://github.com/pytorch/pytorch/issues/124245 + +import copy +import ctypes +import errno +import functools +import json +import locale +import logging +import os +import platform +import re +import shlex +import shutil +import subprocess +import sys +import sysconfig +import tempfile +import textwrap +import warnings +from collections.abc import Sequence +from ctypes import cdll, wintypes +from ctypes.util import find_library +from pathlib import Path +from typing import Any + +import torch +from torch._dynamo.utils import dynamo_timed +from torch._inductor import config, exc +from torch._inductor.cpu_vec_isa import invalid_vec_isa, VecISA +from torch._inductor.runtime.runtime_utils import cache_dir +from torch.torch_version import TorchVersion + + +if config.is_fbcode(): + from triton.fb.build import _run_build_command, build_paths + + from torch._inductor.fb.utils import ( + log_global_cache_errors, + log_global_cache_stats, + log_global_cache_vals, + use_global_cache, + ) +else: + + def log_global_cache_errors(*args: Any, **kwargs: Any) -> None: # type: ignore[misc] + pass + + def log_global_cache_stats(*args: Any, **kwargs: Any) -> None: # type: ignore[misc] + pass + + def log_global_cache_vals(*args: Any, **kwargs: Any) -> None: # type: ignore[misc] + pass + + def use_global_cache() -> bool: # type: ignore[misc] + return False + + +# Windows need setup a temp dir to store .obj files. +_BUILD_TEMP_DIR = "CxxBuild" +_HERE = os.path.abspath(__file__) +_TORCH_PATH = os.path.dirname(os.path.dirname(_HERE)) +_LINKER_SCRIPT = os.path.join(_TORCH_PATH, "_inductor/script.ld") + +# initialize variables for compilation +_IS_LINUX = sys.platform.startswith("linux") +_IS_MACOS = sys.platform.startswith("darwin") +_IS_WINDOWS = sys.platform == "win32" + +MINGW_GXX = "x86_64-w64-mingw32-g++" + +SUBPROCESS_DECODE_ARGS = (locale.getpreferredencoding(),) if _IS_WINDOWS else () + +log = logging.getLogger(__name__) + + +# =============================== toolchain =============================== +@functools.lru_cache(1) +def cpp_compiler_search(search: str) -> str: + from torch._inductor.codecache import get_lock_dir, LOCK_TIMEOUT + + for cxx in search: + try: + if cxx is None: + # gxx package is only available for Linux + # according to https://anaconda.org/conda-forge/gxx/ + if sys.platform != "linux": + continue + # Do not install GXX by default + if not os.getenv("TORCH_INDUCTOR_INSTALL_GXX"): + continue + from torch.utils._filelock import FileLock + + lock_dir = get_lock_dir() + lock = FileLock( + os.path.join(lock_dir, "g++.lock"), timeout=LOCK_TIMEOUT + ) + with lock: + cxx = install_gcc_via_conda() + subprocess.check_output([cxx, "--version"]) + return cxx + except (subprocess.SubprocessError, FileNotFoundError, ImportError): + continue + raise exc.InvalidCxxCompiler + + +def install_gcc_via_conda() -> str: + """On older systems, this is a quick way to get a modern compiler""" + prefix = os.path.join(cache_dir(), "gcc") + cxx_path = os.path.join(prefix, "bin", "g++") + if not os.path.exists(cxx_path): + log.info("Downloading GCC via conda") + conda = os.environ.get("CONDA_EXE", "conda") + if conda is None: + conda = shutil.which("conda") + if conda is not None: + subprocess.check_call( + [ + conda, + "create", + f"--prefix={prefix}", + "--channel=conda-forge", + "--quiet", + "-y", + "python=3.8", + "gxx", + ], + stdout=subprocess.PIPE, + ) + return cxx_path + + +@functools.cache +def check_compiler_exist_windows(compiler: str) -> None: + """ + Check if compiler is ready, in case end user not activate MSVC environment. + """ + try: + subprocess.check_output([compiler, "/help"], stderr=subprocess.STDOUT) + except FileNotFoundError as exc: + raise RuntimeError(f"Compiler: {compiler} is not found.") from exc + except subprocess.SubprocessError: + # Expected that some compiler(clang, clang++) is exist, but they not support `/help` args. + pass + + +class WinPeFileVersionInfo: + def __init__(self, file_path: str) -> None: + self.file_path = file_path + self.version_dll = ctypes.WinDLL("version.dll") # type: ignore[attr-defined] + self._setup_functions() + self._get_version_info() + + def _setup_functions(self) -> None: + self.version_dll.GetFileVersionInfoSizeW.argtypes = [ + wintypes.LPCWSTR, + wintypes.LPDWORD, + ] + self.version_dll.GetFileVersionInfoSizeW.restype = wintypes.DWORD + + self.version_dll.GetFileVersionInfoW.argtypes = [ + wintypes.LPCWSTR, + wintypes.DWORD, + wintypes.DWORD, + wintypes.LPVOID, + ] + self.version_dll.GetFileVersionInfoW.restype = wintypes.BOOL + + self.version_dll.VerQueryValueW.argtypes = [ + wintypes.LPCVOID, + wintypes.LPCWSTR, + ctypes.POINTER(ctypes.c_void_p), + ctypes.POINTER(wintypes.UINT), + ] + self.version_dll.VerQueryValueW.restype = wintypes.BOOL + + def _get_version_info(self) -> None: + dummy = wintypes.DWORD() + size = self.version_dll.GetFileVersionInfoSizeW( + self.file_path, ctypes.byref(dummy) + ) + + if size == 0: + raise RuntimeError(f"Can't get version info size of {self.file_path}.") + + self.version_info = ctypes.create_string_buffer(size) + success = self.version_dll.GetFileVersionInfoW( + self.file_path, 0, size, self.version_info + ) + + if not success: + raise RuntimeError(f"Can't get version info of {self.file_path}.") + + def get_language_id(self) -> int: + lp_buffer = ctypes.c_void_p() + u_len = wintypes.UINT() + + success = self.version_dll.VerQueryValueW( + self.version_info, + r"\VarFileInfo\Translation", + ctypes.byref(lp_buffer), + ctypes.byref(u_len), + ) + + if not success or u_len.value == 0: + return 0 + + translations = [] + lang_id: int = 0 + if lp_buffer.value is not None: + for i in range(u_len.value // 4): + offset = i * 4 + data = ctypes.string_at(lp_buffer.value + offset, 4) + lang_id = int.from_bytes(data[:2], "little") + code_page = int.from_bytes(data[2:4], "little") + translations.append((lang_id, code_page)) + else: + # Handle the case where lp_buffer.value is None + print("Buffer is None") + + return lang_id + + +@functools.cache +def check_msvc_cl_language_id(compiler: str) -> None: + """ + Torch.compile() is only work on MSVC with English language pack well. + Check MSVC's language pack: https://github.com/pytorch/pytorch/issues/157673#issuecomment-3051682766 + """ + + def get_msvc_cl_path() -> tuple[bool, str]: + """ + Finds the path to cl.exe using vswhere.exe. + """ + vswhere_path = os.path.join( + os.environ.get("ProgramFiles(x86)", "C:\\Program Files (x86)"), + "Microsoft Visual Studio", + "Installer", + "vswhere.exe", + ) + if not os.path.exists(vswhere_path): + vswhere_path = os.path.join( + os.environ.get("ProgramFiles", "C:\\Program Files"), + "Microsoft Visual Studio", + "Installer", + "vswhere.exe", + ) + if not os.path.exists(vswhere_path): + return False, "" # vswhere.exe not found + + try: + # Get the Visual Studio installation path + cmd = [ + vswhere_path, + "-latest", + "-prerelease", + "-products", + "*", + "-requires", + "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + "-property", + "installationPath", + ] + vs_install_path = subprocess.check_output( + cmd, text=True, encoding="utf-8" + ).strip() + + if not vs_install_path: + return False, "" + + # Find the latest MSVC toolset version within the installation + msvc_tools_path = os.path.join(vs_install_path, "VC", "Tools", "MSVC") + if not os.path.exists(msvc_tools_path): + return False, "" + + # Get the latest toolset version directory + toolset_versions = [ + d + for d in os.listdir(msvc_tools_path) + if os.path.isdir(os.path.join(msvc_tools_path, d)) + ] + if not toolset_versions: + return False, "" + latest_toolset_version = sorted(toolset_versions, reverse=True)[0] + + # Construct the full cl.exe path + cl_path = os.path.join( + msvc_tools_path, + latest_toolset_version, + "bin", + "HostX64", + "x64", + "cl.exe", + ) + if os.path.exists(cl_path): + return True, cl_path + else: + # Fallback for older versions or different architectures if needed + cl_path = os.path.join( + msvc_tools_path, + latest_toolset_version, + "bin", + "HostX86", + "x86", + "cl.exe", + ) + if os.path.exists(cl_path): + return True, cl_path + + except (subprocess.CalledProcessError, FileNotFoundError): + return False, "" + + return False, "" + + if not _is_msvc_cl(compiler): + return + + if os.path.exists(compiler): + # Passed compiler with path. + cl_exe_path = compiler + else: + b_ret, cl_exe_path = get_msvc_cl_path() + if b_ret is False: + return + + version_info = WinPeFileVersionInfo(cl_exe_path) + lang_id = version_info.get_language_id() + if lang_id != 1033: + # MSVC English language id is 0x0409, and the DEC value is 1033. + raise RuntimeError( + "Torch.compile() is only support MSVC with English language pack," + "Please reinstall its language pack to English." + ) + + +@functools.cache +def check_mingw_win32_flavor(compiler: str) -> str: + """ + Check if MinGW `compiler` exists and return it's flavor (win32 or posix). + """ + try: + out = subprocess.check_output( + [compiler, "-v"], stderr=subprocess.STDOUT, text=True + ) + except FileNotFoundError as e: + raise RuntimeError(f"Compiler: {compiler} is not found.") from e + except Exception as e: + raise RuntimeError(f"Failed to run {compiler} -v") from e + + flavor: str | None = None + for line in out.splitlines(): + if "Thread model" in line: + flavor = line.split(":", 1)[-1].strip().lower() + + if flavor is None: + raise RuntimeError( + f"Cannot determine the flavor of {compiler} (win32 or posix). No Thread model found in {compiler} -v" + ) + + if flavor not in ("win32", "posix"): + raise RuntimeError( + f"Only win32 and pofix flavor of {compiler} is supported. The flavor is {flavor}" + ) + + return flavor + + +def get_cpp_compiler() -> str: + if ( + config.aot_inductor.cross_target_platform == "windows" + and sys.platform != "win32" + ): + # we're doing cross-compilation + compiler = MINGW_GXX + if not config.aot_inductor.package_cpp_only: + check_mingw_win32_flavor(compiler) + return compiler + + if _IS_WINDOWS: + compiler = os.environ.get("CXX", "cl") + compiler = normalize_path_separator(compiler) + check_compiler_exist_windows(compiler) + check_msvc_cl_language_id(compiler) + else: + if config.is_fbcode(): + return build_paths.cc + if isinstance(config.cpp.cxx, (list, tuple)): + search = tuple(config.cpp.cxx) + else: + search = (config.cpp.cxx,) + compiler = cpp_compiler_search(search) + return compiler + + +def get_ld_and_objcopy(use_relative_path: bool) -> tuple[str, str]: + if _IS_WINDOWS: + raise RuntimeError("Windows is not supported yet.") + else: + if config.is_fbcode(): + ld = build_paths.ld + objcopy = ( + build_paths.objcopy_fallback + if use_relative_path + else build_paths.objcopy + ) + else: + ld = "ld" + objcopy = "objcopy" + return ld, objcopy + + +def convert_cubin_to_obj( + cubin_file: str, + kernel_name: str, + ld: str, + objcopy: str, +) -> str: + obj_file = cubin_file + ".o" + # Convert .cubin to .o + cmd = f"{ld} -r -b binary -z noexecstack -o {obj_file} {cubin_file}" + subprocess.run(cmd.split(), capture_output=True, text=True, check=True) + # Rename .data to .rodata + cmd = f"{objcopy} --rename-section .data=.rodata,alloc,load,readonly,data,contents {obj_file}" + subprocess.run(cmd.split(), capture_output=True, text=True, check=True) + # By default objcopy will create *_start, *_size, *_end symbols using the full path + # Rename to use the unique kernel name + file_name = re.sub(r"[\W]", "_", cubin_file) + cmd = ( + objcopy + + f" --redefine-sym _binary_{file_name}_start=__{kernel_name}_start " + + f"--redefine-sym _binary_{file_name}_size=__{kernel_name}_size " + + f"--redefine-sym _binary_{file_name}_end=__{kernel_name}_end " + + obj_file + ) + subprocess.run(cmd.split(), capture_output=True, text=True, check=True) + return obj_file + + +def batch_convert_cubins_to_obj( + cubins: list[tuple[str, str]], + output_dir: str, + cpp_compiler: str = "gcc", +) -> str: + """Convert multiple cubin files to a single .o using batched .incbin assembly. + + Instead of spawning 3 subprocesses per cubin (ld + 2x objcopy), generates + a single .S file with .incbin directives for all cubins and compiles it + with one compiler invocation. Produces bit-identical rodata and symbols + as the per-cubin convert_cubin_to_obj approach. + + Args: + cubins: list of (cubin_file_path, kernel_name) tuples. + output_dir: directory for the generated .S and .o files. + cpp_compiler: C compiler to use for assembling (default: gcc). + + Returns: + Path to the combined .o file. + """ + asm_path = os.path.join(output_dir, "cubins_combined.S") + obj_path = os.path.join(output_dir, "cubins_combined.o") + + with open(asm_path, "w") as f: + f.write(".section .rodata\n") + for cubin_file, kernel_name in cubins: + # Use absolute path to avoid issues with working directory + abs_cubin = os.path.abspath(cubin_file) + escaped_path = abs_cubin.replace("\\", "\\\\").replace('"', '\\"') + f.write( + f".balign 16\n" + f".global __{kernel_name}_start\n" + f".global __{kernel_name}_end\n" + f"__{kernel_name}_start:\n" + f'.incbin "{escaped_path}"\n' + f"__{kernel_name}_end:\n" + f".global __{kernel_name}_size\n" + f".set __{kernel_name}_size, " + f"__{kernel_name}_end - __{kernel_name}_start\n" + ) + + subprocess.run( + [cpp_compiler, "-c", asm_path, "-o", obj_path], + capture_output=True, + text=True, + check=True, + ) + return obj_path + + +@functools.cache +def _is_apple_clang(cpp_compiler: str) -> bool: + version_string = subprocess.check_output([cpp_compiler, "--version"]).decode("utf8") + return "Apple" in version_string.splitlines()[0] + + +@functools.cache +def _is_clang(cpp_compiler: str) -> bool: + # Mac OS apple clang maybe named as gcc, need check compiler info. + if sys.platform == "darwin": + return _is_apple_clang(cpp_compiler) + elif _IS_WINDOWS: + # clang suite have many compilers, and only clang-cl is supported. + if re.search(r"((clang$)|(clang\+\+$))", cpp_compiler): + raise RuntimeError( + "Please use clang-cl, due to torch.compile only support MSVC-like CLI (compiler flags syntax)." + ) + return bool(re.search(r"(clang-cl)", cpp_compiler)) + return bool(re.search(r"(clang|clang\+\+)", cpp_compiler)) + + +@functools.cache +def _is_gcc(cpp_compiler: str) -> bool: + # Since "clang++" ends with "g++", the regex match below would validate on it. + if _is_clang(cpp_compiler): + return False + return bool(re.search(r"(gcc|g\+\+|gnu-c\+\+)", cpp_compiler)) + + +@functools.cache +def _is_msvc_cl(cpp_compiler: str) -> bool: + if not _IS_WINDOWS: + return False + + try: + output_msg = ( + subprocess.check_output([cpp_compiler, "/help"], stderr=subprocess.STDOUT) + .strip() + .decode(*SUBPROCESS_DECODE_ARGS) + ) + return "Microsoft" in output_msg.splitlines()[0] + except FileNotFoundError: + return False + + # pyrefly: ignore [unreachable] + return False + + +@functools.cache +def _is_intel_compiler(cpp_compiler: str) -> bool: + def _check_minimal_version(compiler_version: TorchVersion) -> None: + """ + On Windows: early version icx has `-print-file-name` issue, and can't preload correctly for inductor. + """ + min_version = "2024.2.1" if _IS_WINDOWS else "0.0.0" + if compiler_version < TorchVersion(min_version): + raise RuntimeError( + f"Intel Compiler error: less than minimal version {min_version}." + ) + + try: + output_msg = ( + subprocess.check_output( + [cpp_compiler, "--version"], stderr=subprocess.DEVNULL + ) + .strip() + .decode(*SUBPROCESS_DECODE_ARGS) + ) + is_intel_compiler = "Intel" in output_msg.splitlines()[0] + if is_intel_compiler: + if _IS_WINDOWS: + if re.search(r"((icx$)|(icx-cc$))", cpp_compiler): + raise RuntimeError( + "Please use icx-cl, due to torch.compile only support MSVC-like CLI (compiler flags syntax)." + ) + + # Version check + icx_ver_search = re.search(r"(\d+[.]\d+[.]\d+[.]\d+)", output_msg) + if icx_ver_search is not None: + icx_ver = icx_ver_search.group(1) + _check_minimal_version(TorchVersion(icx_ver)) + + return is_intel_compiler + except FileNotFoundError: + return False + except subprocess.SubprocessError: + # --version args not support. + return False + + # pyrefly: ignore [unreachable] + return False + + +@functools.cache +def is_gcc() -> bool: + return _is_gcc(get_cpp_compiler()) + + +@functools.cache +def is_clang() -> bool: + return _is_clang(get_cpp_compiler()) + + +@functools.cache +def is_intel_compiler() -> bool: + return _is_intel_compiler(get_cpp_compiler()) + + +@functools.cache +def is_apple_clang() -> bool: + return _is_apple_clang(get_cpp_compiler()) + + +@functools.cache +def is_msvc_cl() -> bool: + return _is_msvc_cl(get_cpp_compiler()) + + +@functools.cache +def get_compiler_version_info(compiler: str) -> str: + env = os.environ.copy() + env["LC_ALL"] = "C" # Don't localize output + try: + version_string = subprocess.check_output( + [compiler, "-v"], stderr=subprocess.STDOUT, env=env + ).decode(*SUBPROCESS_DECODE_ARGS) + except Exception: + try: + version_string = subprocess.check_output( + [compiler, "--version"], stderr=subprocess.STDOUT, env=env + ).decode(*SUBPROCESS_DECODE_ARGS) + except Exception: + return "" + # Multiple lines to one line string. + version_string = version_string.replace("\r", "_") + version_string = version_string.replace("\n", "_") + return version_string + + +# =============================== cpp builder =============================== +def _append_list(dest_list: list[str], src_list: list[str]) -> None: + dest_list.extend(copy.deepcopy(item) for item in src_list) + + +def _remove_duplication_in_list(orig_list: list[str]) -> list[str]: + new_list: list[str] = [] + for item in orig_list: + if item not in new_list: + new_list.append(item) + return new_list + + +def _create_if_dir_not_exist(path_dir: str) -> None: + if not os.path.exists(path_dir): + try: + Path(path_dir).mkdir(parents=True, exist_ok=True) + except OSError as exc: # Guard against race condition + if exc.errno != errno.EEXIST: + raise RuntimeError(f"Fail to create path {path_dir}") from exc + + +def _remove_dir(path_dir: str) -> None: + if os.path.exists(path_dir): + for root, dirs, files in os.walk(path_dir, topdown=False): + for name in files: + file_path = os.path.join(root, name) + os.remove(file_path) + for name in dirs: + dir_path = os.path.join(root, name) + os.rmdir(dir_path) + os.rmdir(path_dir) + + +def _run_compile_cmd(cmd_line: str, cwd: str) -> None: + cmd = shlex.split(cmd_line) + try: + subprocess.run( + cmd, cwd=cwd, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) + except subprocess.CalledProcessError as e: + output = e.stdout.decode(*SUBPROCESS_DECODE_ARGS) + openmp_problem = "'omp.h' file not found" in output or "libomp" in output + if openmp_problem and sys.platform == "darwin": + instruction = ( + "\n\nOpenMP support not found. Please try one of the following solutions:\n" + "(1) Set the `CXX` environment variable to a compiler other than Apple clang++/g++ " + "that has builtin OpenMP support;\n" + "(2) install OpenMP via conda: `conda install llvm-openmp`;\n" + "(3) install libomp via brew: `brew install libomp`;\n" + "(4) manually setup OpenMP and set the `OMP_PREFIX` environment variable to point to a path" + " with `include/omp.h` under it." + ) + output += instruction + raise exc.CppCompileError(cmd, output) from e + + +def run_compile_cmd(cmd_line: str, cwd: str) -> None: + with dynamo_timed("compile_file"): + _run_compile_cmd(cmd_line, cwd) + + +def normalize_path_separator(orig_path: str) -> str: + if _IS_WINDOWS: + return orig_path.replace(os.sep, "/") + return orig_path + + +class BuildOptionsBase: + """ + This is the Base class for store cxx build options, as a template. + Actually, to build a cxx shared library. We just need to select a compiler + and maintains the suitable args. + """ + + def __init__( + self, + compiler: str = "", + definitions: list[str] | None = None, + include_dirs: list[str] | None = None, + cflags: list[str] | None = None, + ldflags: list[str] | None = None, + libraries_dirs: list[str] | None = None, + libraries: list[str] | None = None, + passthrough_args: list[str] | None = None, + aot_mode: bool = False, + use_relative_path: bool = False, + compile_only: bool = False, + precompiling: bool = False, + preprocessing: bool = False, + ) -> None: + self._compiler = compiler + self._definitions: list[str] = definitions or [] + self._include_dirs: list[str] = include_dirs or [] + self._cflags: list[str] = cflags or [] + self._ldflags: list[str] = ldflags or [] + self._libraries_dirs: list[str] = libraries_dirs or [] + self._libraries: list[str] = libraries or [] + # Some args are hard to abstract to OS compatible, passthrough directly. + self._passthrough_args: list[str] = passthrough_args or [] + + # Optionally, the path to a precompiled header which should be included on the + # build command line. + self.precompiled_header: str | None = None + + self._aot_mode: bool = aot_mode + self._use_relative_path: bool = use_relative_path + self._compile_only: bool = compile_only + self._precompiling: bool = precompiling + self._preprocessing: bool = preprocessing + + def _process_compile_only_options(self) -> None: + if self._compile_only or self._precompiling or self._preprocessing: + self._libraries_dirs = [] + self._libraries = [] + self._ldflags = [] + + def _remove_duplicate_options(self) -> None: + self._definitions = _remove_duplication_in_list(self._definitions) + self._include_dirs = _remove_duplication_in_list(self._include_dirs) + self._cflags = _remove_duplication_in_list(self._cflags) + self._ldflags = _remove_duplication_in_list(self._ldflags) + self._libraries_dirs = _remove_duplication_in_list(self._libraries_dirs) + self._libraries = _remove_duplication_in_list(self._libraries) + self._passthrough_args = _remove_duplication_in_list(self._passthrough_args) + + def _finalize_options(self) -> None: + self._process_compile_only_options() + self._remove_duplicate_options() + + def get_compiler(self) -> str: + return self._compiler + + def get_definitions(self) -> list[str]: + return self._definitions + + def get_include_dirs(self) -> list[str]: + return self._include_dirs + + def get_cflags(self) -> list[str]: + return self._cflags + + def get_ldflags(self) -> list[str]: + return self._ldflags + + def get_libraries_dirs(self) -> list[str]: + return self._libraries_dirs + + def get_libraries(self) -> list[str]: + return self._libraries + + def get_passthrough_args(self) -> list[str]: + return self._passthrough_args + + def get_aot_mode(self) -> bool: + return self._aot_mode + + def get_use_relative_path(self) -> bool: + return self._use_relative_path + + def get_compile_only(self) -> bool: + return self._compile_only + + def get_precompiling(self) -> bool: + return self._precompiling + + def get_preprocessing(self) -> bool: + return self._preprocessing + + def save_flags_to_json(self, file: str) -> None: + attrs = { + "compiler": self.get_compiler(), + "definitions": self.get_definitions(), + "include_dirs": self.get_include_dirs(), + "cflags": self.get_cflags(), + "ldflags": self.get_ldflags(), + "libraries_dirs": self.get_libraries_dirs(), + "libraries": self.get_libraries(), + "passthrough_args": self.get_passthrough_args(), + "aot_mode": self.get_aot_mode(), + "use_relative_path": self.get_use_relative_path(), + "compile_only": self.get_compile_only(), + } + + with open(file, "w") as f: + json.dump(attrs, f) + + +def _get_warning_all_cflag(warning_all: bool = True) -> list[str]: + if not _IS_WINDOWS: + return ["Wall"] if warning_all else [] + else: + return [] + + +def _get_cpp_std_cflag(std_num: str = "c++20") -> list[str]: + if _IS_WINDOWS: + """ + On Windows, only c++20 can support `std::enable_if_t`. + Ref: https://learn.microsoft.com/en-us/cpp/overview/cpp-conformance-improvements-2019?view=msvc-170#checking-for-abstract-class-types # noqa: B950 + Note: + Only setup c++20 for Windows inductor. I tried to upgrade all project to c++20, but it is failed: + https://github.com/pytorch/pytorch/pull/131504 + """ + std_num = "c++20" + return [f"std:{std_num}"] + else: + return [f"std={std_num}"] + + +def _get_os_related_cpp_cflags(cpp_compiler: str) -> list[str]: + if _IS_WINDOWS: + cflags = [ + "wd4819", + "wd4251", + "wd4244", + "wd4267", + "wd4275", + "wd4018", + "wd4190", + "wd4624", + "wd4067", + "wd4068", + "EHsc", + # For Intel oneAPI, ref: https://learn.microsoft.com/en-us/cpp/build/reference/zc-cplusplus?view=msvc-170 + "Zc:__cplusplus", + # Enable max compatible to msvc for oneAPI headers. + # ref: https://github.com/pytorch/pytorch/blob/db38c44ad639e7ada3e9df2ba026a2cb5e40feb0/cmake/public/utils.cmake#L352-L358 # noqa: B950 + "permissive-", + ] + else: + cflags = ["Wno-unused-variable", "Wno-unknown-pragmas"] + if _is_clang(cpp_compiler): + ignored_optimization_argument = ( + "Werror=ignored-optimization-argument" + if config.aot_inductor.raise_error_on_ignored_optimization + else "Wno-ignored-optimization-argument" + ) + cflags.append(ignored_optimization_argument) + if _is_gcc(cpp_compiler): + # Issue all the warnings demanded by strict ISO C and ISO C++. + # Ref: https://github.com/pytorch/pytorch/issues/153180#issuecomment-2986676878 + cflags.append("pedantic") + return cflags + + +def _get_os_related_cpp_definitions(cpp_compiler: str) -> list[str]: + os_definitions: list[str] = [] + if _IS_WINDOWS: + # On Windows, we need disable min/max macro to avoid C2589 error, as PyTorch CMake: + # https://github.com/pytorch/pytorch/blob/9a41570199155eee92ebd28452a556075e34e1b4/CMakeLists.txt#L1118-L1119 + os_definitions.append("NOMINMAX") + return os_definitions + + +def _get_ffast_math_flags() -> list[str]: + if _IS_WINDOWS: + flags = [] + else: + # ffast-math is equivalent to these flags as in + # https://github.com/gcc-mirror/gcc/blob/4700ad1c78ccd7767f846802fca148b2ea9a1852/gcc/opts.cc#L3458-L3468 + # however gcc<13 sets the FTZ/DAZ flags for runtime on x86 even if we have + # -ffast-math -fno-unsafe-math-optimizations because the flags for runtime + # are added by linking in crtfastmath.o. This is done by the spec file which + # only does globbing for -ffast-math. + flags = [ + "fno-trapping-math", + "funsafe-math-optimizations", + "ffinite-math-only", + "fno-signed-zeros", + "fno-math-errno", + ] + + flags.append("fno-finite-math-only") + if not config.cpp.enable_unsafe_math_opt_flag: + flags.append("fno-unsafe-math-optimizations") + flags.append(f"ffp-contract={config.cpp.enable_floating_point_contract_flag}") + + if is_gcc(): + flags.append("fexcess-precision=fast") + + return flags + + +def _get_inductor_debug_symbol_cflags() -> tuple[list[str], list[str]]: + """ + When we turn on generate debug symbol. + On Windows, it should create a [module_name].pdb file. It helps debug by WinDBG. + On Linux, it should create some debug sections in binary file. + """ + cflags: list[str] = [] + ldflags: list[str] = [] + + if _IS_WINDOWS: + cflags = ["ZI", "_DEBUG"] + ldflags = ["DEBUG", "ASSEMBLYDEBUG ", "OPT:REF", "OPT:ICF"] + else: + cflags.append("g") + + return cflags, ldflags + + +def _get_optimization_cflags( + cpp_compiler: str, min_optimize: bool = False +) -> tuple[list[str], list[str]]: + cflags: list[str] = [] + ldflags: list[str] = [] + + should_use_optimized_flags = not ( + config.aot_inductor.debug_compile + or os.environ.get("TORCHINDUCTOR_DEBUG_COMPILE", "0") == "1" + ) + should_add_debug_symbol_flags = ( + config.aot_inductor.debug_compile + or config.aot_inductor.debug_symbols + or os.environ.get("TORCHINDUCTOR_DEBUG_COMPILE", "0") == "1" + or os.environ.get("TORCHINDUCTOR_DEBUG_SYMBOL", "0") == "1" + ) + if should_use_optimized_flags: + if _IS_WINDOWS: + cflags += ["O1" if min_optimize else "O2"] + else: + cflags += [ + config.aot_inductor.compile_wrapper_opt_level if min_optimize else "O3", + "DNDEBUG", + ] + else: + if _IS_WINDOWS: + cflags += ["Od", "Ob0", "Oy-"] + else: + cflags += ["O0"] + + if should_add_debug_symbol_flags: + debug_cflags, debug_ldflags = _get_inductor_debug_symbol_cflags() + cflags += debug_cflags + ldflags += debug_ldflags + + if config.aot_inductor.enable_frame_pointer: + if _IS_WINDOWS: + cflags.append("Oy-") + else: + cflags.append("fno-omit-frame-pointer") + + cflags += _get_ffast_math_flags() + + if _IS_WINDOWS: + pass + else: + if sys.platform != "darwin": + # on macos, unknown argument: '-fno-tree-loop-vectorize' + if _is_gcc(cpp_compiler): + cflags.append("fno-tree-loop-vectorize") + # https://stackoverflow.com/questions/65966969/why-does-march-native-not-work-on-apple-m1 + # `-march=native` is unrecognized option on M1 + if not config.is_fbcode(): + if platform.machine() == "ppc64le": + cflags.append("mcpu=native") + elif platform.machine() == "riscv64": + cflags.append("march=rv64gc") + elif platform.machine() == "riscv32": + cflags.append("march=rv32gc") + else: + cflags.append("march=native") + + if config.aot_inductor.enable_lto and _is_clang(cpp_compiler): + cflags.append("flto=thin") + + return cflags, ldflags + + +def _get_shared_cflags(do_link: bool) -> list[str]: + if _IS_WINDOWS: + """ + MSVC `/MD` using python `ucrtbase.dll` lib as runtime. + https://learn.microsoft.com/en-us/cpp/c-runtime-library/crt-library-features?view=msvc-170 + """ + return ["DLL", "MD"] + if platform.system() == "Darwin" and "clang" in get_cpp_compiler(): + # This causes undefined symbols to behave the same as linux + return ["shared", "fPIC", "undefined dynamic_lookup"] + flags = [] + if do_link: + flags.append("shared") + + flags.append("fPIC") + return flags + + +def get_cpp_options( + cpp_compiler: str, + do_link: bool, + warning_all: bool = True, + extra_flags: Sequence[str] = (), + min_optimize: bool = False, +) -> tuple[list[str], list[str], list[str], list[str], list[str], list[str], list[str]]: + definitions: list[str] = [] + include_dirs: list[str] = [] + cflags: list[str] = [] + ldflags: list[str] = [] + libraries_dirs: list[str] = [] + libraries: list[str] = [] + passthrough_args: list[str] = [] + + opt_cflags, opt_ldflags = _get_optimization_cflags(cpp_compiler, min_optimize) + + cflags = ( + opt_cflags + + _get_shared_cflags(do_link) + + _get_warning_all_cflag(warning_all) + + _get_cpp_std_cflag() + + _get_os_related_cpp_cflags(cpp_compiler) + ) + + definitions += _get_os_related_cpp_definitions(cpp_compiler) + + if not _IS_WINDOWS and config.aot_inductor.enable_lto and _is_clang(cpp_compiler): + ldflags.append("fuse-ld=lld") + ldflags.append("flto=thin") + + passthrough_args.append(" ".join(extra_flags)) + + if config.aot_inductor.cross_target_platform == "windows": + passthrough_args.extend(["-static-libstdc++", "-static-libgcc"]) + if check_mingw_win32_flavor(MINGW_GXX) == "posix": + passthrough_args.append("-Wl,-Bstatic -lwinpthread -Wl,-Bdynamic") + + return ( + definitions, + include_dirs, + cflags, + ldflags + opt_ldflags, + libraries_dirs, + libraries, + passthrough_args, + ) + + +class CppOptions(BuildOptionsBase): + """ + This class is inherited from BuildOptionsBase, and as cxx build options. + This option need contains basic cxx build option, which contains: + 1. OS related args. + 2. Toolchains related args. + 3. Cxx standard related args. + Note: + 1. This Options is good for assist modules build, such as x86_isa_help. + """ + + def __init__( + self, + compile_only: bool = False, + warning_all: bool = True, + extra_flags: Sequence[str] = (), + use_relative_path: bool = False, + compiler: str = "", + min_optimize: bool = False, + precompiling: bool = False, + preprocessing: bool = False, + ) -> None: + super().__init__( + compile_only=compile_only, + use_relative_path=use_relative_path, + precompiling=precompiling, + preprocessing=preprocessing, + ) + self._compiler = compiler if compiler else get_cpp_compiler() + + ( + definitions, + include_dirs, + cflags, + ldflags, + libraries_dirs, + libraries, + passthrough_args, + ) = get_cpp_options( + cpp_compiler=self._compiler, + do_link=not (compile_only or precompiling or preprocessing), + extra_flags=extra_flags, + warning_all=warning_all, + min_optimize=min_optimize, + ) + + _append_list(self._definitions, definitions) + _append_list(self._include_dirs, include_dirs) + _append_list(self._cflags, cflags) + _append_list(self._ldflags, ldflags) + _append_list(self._libraries_dirs, libraries_dirs) + _append_list(self._libraries, libraries) + _append_list(self._passthrough_args, passthrough_args) + self._finalize_options() + + +def _get_torch_cpp_wrapper_definition() -> list[str]: + defs = ["TORCH_INDUCTOR_CPP_WRAPPER", "STANDALONE_TORCH_HEADER"] + if config.cpp_cache_precompile_headers: + defs.append("TORCH_INDUCTOR_PRECOMPILE_HEADERS") + return defs + + +def _use_custom_generated_macros() -> list[str]: + return [" C10_USING_CUSTOM_GENERATED_MACROS"] + + +def _use_fb_internal_macros() -> list[str]: + if not _IS_WINDOWS: + if config.is_fbcode(): + fb_internal_macros = [ + "C10_USE_GLOG", + "C10_USE_MINIMAL_GLOG", + "C10_DISABLE_TENSORIMPL_EXTENSIBILITY", + ] + if platform.machine() == "x86_64": + fb_internal_macros += [ + "ATEN_MKL_ENABLED_FBCODE=1", + "ATEN_MKLDNN_ENABLED_FBCODE=1", + ] + return fb_internal_macros + else: + return [] + else: + return [] + + +def _setup_standard_sys_libs( + cpp_compiler: str, + aot_mode: bool, + use_relative_path: bool, +) -> tuple[list[str], list[str], list[str], list[str]]: + cflags: list[str] = [] + include_dirs: list[str] = [] + passthrough_args: list[str] = [] + ldflags: list[str] = [] + if _IS_WINDOWS: + return cflags, include_dirs, passthrough_args, ldflags + + if config.is_fbcode(): + # TODO(T203137008) Can we unify these flags with triton_cc_command? + cflags.append("nostdinc") + # Note that the order of include paths do matter, as a result + # we need to have several branches interleaved here + include_dirs.append(build_paths.sleef_include) + include_dirs.append(build_paths.openmp_include) + include_dirs.append(build_paths.python_include) + include_dirs.append(build_paths.cc_include) + include_dirs.append(build_paths.libgcc_include) + include_dirs.append(build_paths.libgcc_arch_include) + include_dirs.append(build_paths.libgcc_backward_include) + include_dirs.append(build_paths.glibc_include) + include_dirs.append(build_paths.linux_kernel_include) + include_dirs.append("include") + + if aot_mode and not use_relative_path: + linker_script = _LINKER_SCRIPT + else: + linker_script = os.path.basename(_LINKER_SCRIPT) + + if _is_clang(cpp_compiler): + passthrough_args.append(" --rtlib=compiler-rt") + passthrough_args.append(" -B" + build_paths.glibc_lib) + ldflags.append("fuse-ld=lld") + ldflags.append(f"Wl,--script={linker_script}") + ldflags.append("L" + build_paths.glibc_lib) + + return cflags, include_dirs, passthrough_args, ldflags + + +def _get_build_args_of_chosen_isa(vec_isa: VecISA) -> tuple[list[str], list[str]]: + macros: list[str] = [] + build_flags: list[str] = [] + if vec_isa != invalid_vec_isa: + # Add Windows support later. + macros.extend(copy.deepcopy(x) for x in vec_isa.build_macro()) + + build_flags = [vec_isa.build_arch_flags()] + + if config.is_fbcode(): + cap = str(vec_isa).upper() + macros = [ + f"CPU_CAPABILITY={cap}", + f"CPU_CAPABILITY_{cap}", + f"HAVE_{cap}_CPU_DEFINITION", + ] + + return macros, build_flags + + +def _get_torch_related_args( + include_pytorch: bool, aot_mode: bool +) -> tuple[list[str], list[str], list[str]]: + from torch.utils.cpp_extension import include_paths, TORCH_LIB_PATH + + libraries = [] + include_dirs = include_paths() + + if config.aot_inductor.link_libtorch: + libraries_dirs = [TORCH_LIB_PATH] + if sys.platform != "darwin" and not config.is_fbcode(): + libraries.extend(["torch", "torch_cpu"]) + if _IS_WINDOWS: + libraries.append("c10") + if not aot_mode: + libraries.append("torch_python") + else: + libraries_dirs = [] + if config.aot_inductor.cross_target_platform == "windows": + aoti_shim_library = config.aot_inductor.aoti_shim_library + + assert aoti_shim_library, ( + "'config.aot_inductor.aoti_shim_library' must be set when 'cross_target_platform' is 'windows'." + ) + if isinstance(aoti_shim_library, str): + libraries.append(aoti_shim_library) + else: + assert isinstance(aoti_shim_library, list) + libraries.extend(aoti_shim_library) + + if config.aot_inductor.cross_target_platform == "windows": + assert config.aot_inductor.aoti_shim_library_path, ( + "'config.aot_inductor.aoti_shim_library_path' must be set to the path of the AOTI shim library", + " when 'cross_target_platform' is 'windows'.", + ) + libraries_dirs.append(config.aot_inductor.aoti_shim_library_path) + + if _IS_WINDOWS: + libraries.append("sleef") + + return include_dirs, libraries_dirs, libraries + + +def _get_python_include_dirs() -> list[str]: + include_dir = Path(sysconfig.get_path("include")) + # On Darwin Python executable from a framework can return + # non-existing /Library/Python/... include path, in which case + # one should use Headers folder from the framework + if not include_dir.exists() and platform.system() == "Darwin": + std_lib = Path(sysconfig.get_path("stdlib")) + include_dir = (std_lib.parent.parent / "Headers").absolute() + if not (include_dir / "Python.h").exists(): + warnings.warn(f"Can't find Python.h in {str(include_dir)}") + return [str(include_dir)] + + +def _get_python_related_args() -> tuple[list[str], list[str]]: + python_include_dirs = _get_python_include_dirs() + python_include_path = sysconfig.get_path( + "include", scheme="nt" if _IS_WINDOWS else "posix_prefix" + ) + if python_include_path is not None: + python_include_dirs.append(python_include_path) + + if _IS_WINDOWS: + python_lib_path = [ + str( + ( + Path(sysconfig.get_path("include", scheme="nt")).parent / "libs" + ).absolute() + ) + ] + else: + python_lib_path = [sysconfig.get_config_var("LIBDIR")] + + if config.is_fbcode(): + python_include_dirs.append(build_paths.python_include) + + return python_include_dirs, python_lib_path + + +@functools.cache +def is_conda_llvm_openmp_installed() -> bool: + try: + command = "conda list llvm-openmp --json" + output = subprocess.check_output(command.split()).decode("utf8") + return len(json.loads(output)) > 0 + except (subprocess.SubprocessError, FileNotFoundError): + return False + + +@functools.cache +def homebrew_libomp() -> tuple[bool, str]: + try: + # check if `brew` is installed + if shutil.which("brew") is None: + return False, "" + # get the location of `libomp` if it is installed + # this is the location that `libomp` **would** be installed + # see https://github.com/Homebrew/brew/issues/10261#issuecomment-756563567 for details + libomp_path = ( + subprocess.check_output(["brew", "--prefix", "libomp"]) + .decode("utf8") + .strip() + ) + # check if `libomp` is installed + omp_available = os.path.exists(libomp_path) + return omp_available, libomp_path + except subprocess.SubprocessError: + return False, "" + + +@functools.cache +def perload_clang_libomp_win(cpp_compiler: str, omp_name: str) -> None: + try: + output = subprocess.check_output([cpp_compiler, "-print-file-name=bin"]).decode( + "utf8" + ) + omp_path = os.path.join(output.rstrip(), omp_name) + if os.path.isfile(omp_path): + os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" + cdll.LoadLibrary(omp_path) + except subprocess.SubprocessError: + pass + + +@functools.cache +def perload_icx_libomp_win(cpp_compiler: str) -> None: + def _load_icx_built_in_lib_by_name(cpp_compiler: str, lib_name: str) -> bool: + try: + output = subprocess.check_output( + [cpp_compiler, f"-print-file-name={lib_name}"], + stderr=subprocess.DEVNULL, + ).decode(*SUBPROCESS_DECODE_ARGS) + omp_path = output.rstrip() + if os.path.isfile(omp_path): + os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" + cdll.LoadLibrary(omp_path) + return True + except subprocess.SubprocessError: + pass + return False + + """ + Intel Compiler implemented more math libraries than clang, for performance proposal. + We need preload them like openmp library. + """ + preload_list = [ + "libiomp5md.dll", # openmp + "svml_dispmd.dll", # svml library + "libmmd.dll", # libm + ] + + for lib_name in preload_list: + _load_icx_built_in_lib_by_name(cpp_compiler, lib_name) + + +def _get_openmp_args( + cpp_compiler: str, +) -> tuple[list[str], list[str], list[str], list[str], list[str], list[str]]: + cflags: list[str] = [] + ldflags: list[str] = [] + include_dir_paths: list[str] = [] + lib_dir_paths: list[str] = [] + libs: list[str] = [] + passthrough_args: list[str] = [] + + if config.aot_inductor.cross_target_platform == "windows": + return cflags, ldflags, include_dir_paths, lib_dir_paths, libs, passthrough_args + if _IS_MACOS: + # Per https://mac.r-project.org/openmp/ right way to pass `openmp` flags to MacOS is via `-Xclang` + cflags.append("Xclang") + cflags.append("fopenmp") + + # only Apple builtin compilers (Apple Clang++) require openmp + omp_available = not _is_apple_clang(cpp_compiler) + + # check the `OMP_PREFIX` environment first + omp_prefix = os.getenv("OMP_PREFIX") + if omp_prefix is not None: + header_path = os.path.join(omp_prefix, "include", "omp.h") + valid_env = os.path.exists(header_path) + if valid_env: + include_dir_paths.append(os.path.join(omp_prefix, "include")) + lib_dir_paths.append(os.path.join(omp_prefix, "lib")) + else: + warnings.warn("environment variable `OMP_PREFIX` is invalid.") + omp_available = omp_available or valid_env + + if not omp_available: + libs.append("omp") + + # prefer to use openmp from `conda install llvm-openmp` + conda_prefix = os.getenv("CONDA_PREFIX") + if not omp_available and conda_prefix is not None: + omp_available = is_conda_llvm_openmp_installed() + if omp_available: + conda_lib_path = os.path.join(conda_prefix, "lib") + include_dir_paths.append(os.path.join(conda_prefix, "include")) + lib_dir_paths.append(conda_lib_path) + # Prefer Intel OpenMP on x86 machine + if os.uname().machine == "x86_64" and os.path.exists( + os.path.join(conda_lib_path, "libiomp5.dylib") + ): + libs.append("iomp5") + + # next, try to use openmp from `brew install libomp` + if not omp_available: + omp_available, libomp_path = homebrew_libomp() + if omp_available: + include_dir_paths.append(os.path.join(libomp_path, "include")) + lib_dir_paths.append(os.path.join(libomp_path, "lib")) + + # if openmp is still not available, we let the compiler to have a try, + # and raise error together with instructions at compilation error later + elif _IS_WINDOWS: + """ + On Windows, `clang` and `icx` have their specific openmp implenmention. + And the openmp lib is in compiler's some sub-directory. + For dynamic library(DLL) load, the Windows native APIs are `LoadLibraryA` and `LoadLibraryExA`, and their search + dependencies have some rules: + https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexa#searching-for-dlls-and-dependencies + In some case, the rules may not include compiler's sub-directories. + So, it can't search and load compiler's openmp library correctly. + And then, the whole application would be broken. + + To avoid the openmp load failed, we can automatic locate the openmp binary and preload it. + 1. For clang, the function is `perload_clang_libomp_win`. + 2. For icx, the function is `perload_icx_libomp_win`. + """ + if _is_clang(cpp_compiler): + cflags.append("openmp") + libs.append("libomp") + perload_clang_libomp_win(cpp_compiler, "libomp.dll") + elif _is_intel_compiler(cpp_compiler): + cflags.append("Qiopenmp") + libs.append("libiomp5md") + perload_icx_libomp_win(cpp_compiler) + else: + # /openmp, /openmp:llvm + # llvm on Windows, new openmp: https://devblogs.microsoft.com/cppblog/msvc-openmp-update/ + # msvc openmp: https://learn.microsoft.com/zh-cn/cpp/build/reference/openmp-enable-openmp-2-0-support?view=msvc-170 + cflags.append("openmp") + cflags.append("openmp:experimental") # MSVC CL + else: + if config.is_fbcode(): + include_dir_paths.append(build_paths.openmp_include) + + passthrough_args.append("-Wp,-fopenmp") + lib_dir_paths.append(os.path.dirname(build_paths.openmp_lib_so)) + + libs.append("omp") + else: + if _is_clang(cpp_compiler): + # TODO: fix issue, can't find omp.h + cflags.append("fopenmp") + libs.append("gomp") + elif _is_intel_compiler(cpp_compiler): + cflags.append("fiopenmp") + else: + cflags.append("fopenmp") + libs.append("gomp") + + return cflags, ldflags, include_dir_paths, lib_dir_paths, libs, passthrough_args + + +def _get_libstdcxx_args() -> tuple[list[str], list[str]]: + """ + For fbcode cpu case, we should link stdc++ instead assuming the binary where dlopen is executed is built with dynamic stdc++. + """ + lib_dir_paths: list[str] = [] + libs: list[str] = [] + if config.is_fbcode(): + lib_dir_paths = [sysconfig.get_config_var("LIBDIR")] + libs.append("stdc++") + + return lib_dir_paths, libs + + +def get_mmap_self_macro( + use_mmap_weights: bool, use_mmap_weights_external: bool +) -> list[str]: + macros = [] + + if use_mmap_weights and use_mmap_weights_external: + raise RuntimeError( + "Only one of use_mmap_weights and use_mmap_weights_external should be true" + ) + if use_mmap_weights: + macros.append(" USE_MMAP_SELF") + elif use_mmap_weights_external: + macros.append(" USE_MMAP_EXTERNAL") + return macros + + +def get_caching_allocator_macro() -> list[str]: + from torch._inductor import config + + macros = [] + if config.aot_inductor.weight_use_caching_allocator: + macros.append(" AOT_INDUCTOR_USE_CACHING_ALLOCATOR") + return macros + + +def get_cpp_torch_options( + cpp_compiler: str, + vec_isa: VecISA, + include_pytorch: bool, + aot_mode: bool, + use_relative_path: bool, + use_mmap_weights: bool, + use_mmap_weights_external: bool, +) -> tuple[list[str], list[str], list[str], list[str], list[str], list[str], list[str]]: + """ + This function is used to get the build args of torch related build options. + 1. Torch include_directories, libraries, libraries_directories. + 2. Python include_directories, libraries, libraries_directories. + 3. OpenMP related. + 4. Torch MACROs. + 5. MISC + 6. Return the build args + """ + definitions: list[str] = [] + include_dirs: list[str] = [] + cflags: list[str] = [] + ldflags: list[str] = [] + libraries_dirs: list[str] = [] + libraries: list[str] = [] + passthrough_args: list[str] = [] + + torch_cpp_wrapper_definitions = _get_torch_cpp_wrapper_definition() + use_custom_generated_macros_definitions = _use_custom_generated_macros() + + ( + sys_libs_cflags, + sys_libs_include_dirs, + sys_libs_passthrough_args, + sys_libs_ldflags, + ) = _setup_standard_sys_libs(cpp_compiler, aot_mode, use_relative_path) + + isa_macros, isa_ps_args_build_flags = _get_build_args_of_chosen_isa(vec_isa) + + ( + torch_include_dirs, + torch_libraries_dirs, + torch_libraries, + ) = _get_torch_related_args(include_pytorch=include_pytorch, aot_mode=aot_mode) + + python_include_dirs, python_libraries_dirs = _get_python_related_args() + + ( + omp_cflags, + omp_ldflags, + omp_include_dir_paths, + omp_lib_dir_paths, + omp_lib, + omp_passthrough_args, + ) = _get_openmp_args(cpp_compiler) + + fb_macro_passthrough_args = _use_fb_internal_macros() + + mmap_self_macros = get_mmap_self_macro(use_mmap_weights, use_mmap_weights_external) + caching_allocator_macros = get_caching_allocator_macro() + + definitions = ( + torch_cpp_wrapper_definitions + + use_custom_generated_macros_definitions + + isa_macros + + fb_macro_passthrough_args + + mmap_self_macros + + caching_allocator_macros + ) + include_dirs = ( + sys_libs_include_dirs + + python_include_dirs + + torch_include_dirs + + omp_include_dir_paths + ) + cflags = sys_libs_cflags + omp_cflags + ldflags = sys_libs_ldflags + omp_ldflags + libraries_dirs = python_libraries_dirs + torch_libraries_dirs + omp_lib_dir_paths + libraries = torch_libraries + omp_lib + passthrough_args = ( + sys_libs_passthrough_args + isa_ps_args_build_flags + omp_passthrough_args + ) + + return ( + definitions, + include_dirs, + cflags, + ldflags, + libraries_dirs, + libraries, + passthrough_args, + ) + + +class CppTorchOptions(CppOptions): + """ + This class is inherited from CppTorchOptions, which automatic contains + base cxx build options. And then it will maintains torch related build + args. + 1. Torch include_directories, libraries, libraries_directories. + 2. Python include_directories, libraries, libraries_directories. + 3. OpenMP related. + 4. Torch MACROs. + 5. MISC + """ + + def __init__( + self, + vec_isa: VecISA = invalid_vec_isa, + include_pytorch: bool = False, + warning_all: bool = True, + aot_mode: bool = False, + compile_only: bool = False, + use_relative_path: bool = False, + use_mmap_weights: bool = False, + use_mmap_weights_external: bool = False, + shared: bool = True, + extra_flags: Sequence[str] = (), + compiler: str = "", + min_optimize: bool = False, + precompiling: bool = False, + preprocessing: bool = False, + ) -> None: + super().__init__( + compile_only=compile_only, + warning_all=warning_all, + extra_flags=extra_flags, + use_relative_path=use_relative_path, + compiler=compiler, + min_optimize=min_optimize, + precompiling=precompiling, + preprocessing=preprocessing, + ) + + self._aot_mode = aot_mode + + ( + torch_definitions, + torch_include_dirs, + torch_cflags, + torch_ldflags, + torch_libraries_dirs, + torch_libraries, + torch_passthrough_args, + ) = get_cpp_torch_options( + cpp_compiler=self._compiler, + vec_isa=vec_isa, + include_pytorch=include_pytorch, + aot_mode=aot_mode, + use_relative_path=use_relative_path, + use_mmap_weights=use_mmap_weights, + use_mmap_weights_external=use_mmap_weights_external, + ) + + _append_list(self._definitions, torch_definitions) + _append_list(self._include_dirs, torch_include_dirs) + _append_list(self._cflags, torch_cflags) + _append_list(self._ldflags, torch_ldflags) + _append_list(self._libraries_dirs, torch_libraries_dirs) + _append_list(self._libraries, torch_libraries) + _append_list(self._passthrough_args, torch_passthrough_args) + self._finalize_options() + + +def _set_gpu_runtime_env() -> None: + if ( + config.is_fbcode() + and torch.version.hip is None + and "CUDA_HOME" not in os.environ + and "CUDA_PATH" not in os.environ + ): + os.environ["CUDA_HOME"] = build_paths.sdk_home + + +@functools.lru_cache(8) +def _find_libcudart_static(path: str) -> Path | None: + lib_dirs = list(Path(path).rglob("libcudart_static.a")) + if lib_dirs: + return lib_dirs[0].resolve().parent + log_msg = f'"libcudart_static.a" not found under {path}' + log.info(log_msg) + return None + + +def _gen_mingw_import_lib(dll_path: str, def_path: str, import_lib_path: str) -> None: + """Generate a MinGW import library (.a) from a DLL using gendef and dlltool.""" + dll_name = os.path.basename(dll_path) + with open(def_path, "w") as def_file: + subprocess.run( + ["gendef", "-", dll_path], + stdout=def_file, + stderr=subprocess.PIPE, + check=True, + ) + + subprocess.run( + [ + "x86_64-w64-mingw32-dlltool", + "-d", + def_path, + "-l", + import_lib_path, + "-D", + dll_name, + ], + stderr=subprocess.PIPE, + check=True, + ) + log.info("Generated MinGW import library %s from %s", import_lib_path, dll_name) + + +# MSVC /GS buffer security check stubs for MinGW cross-compilation. +# CUDA 13.0+ cudart.lib contains MSVC-compiled static objects that reference +# these symbols (__security_cookie, __security_check_cookie, __GSHandlerCheck). +# When cross-compiling with MinGW, we provide no-op stubs so the linker can +# resolve them. At runtime on Windows, the CUDA runtime DLL handles its own +# security checks internally; the static loader code that references these +# symbols is a thin shim whose /GS instrumentation is safe to stub out. +_MSVC_GS_STUBS_SOURCE = """\ +#include +uint64_t __security_cookie = 0x00002B992DDFA232ULL; +void __security_check_cookie(uint64_t cookie) { (void)cookie; } +void __GSHandlerCheck(void) {} +""" + + +def _create_msvc_gs_stubs_lib(output_dir: str) -> str | None: + """ + Create a static library with MSVC GS security symbol stubs for MinGW. + + Returns the library name (without lib prefix / .a suffix) if successful, + or None on failure. + """ + stubs_lib = os.path.join(output_dir, "libmsvc_gs_stubs.a") + if os.path.exists(stubs_lib): + return "msvc_gs_stubs" + + src_path = "" + obj_path = "" + try: + src_path = os.path.join(output_dir, "_msvc_gs_stubs.c") + obj_path = os.path.join(output_dir, "_msvc_gs_stubs.o") + with open(src_path, "w") as f: + f.write(_MSVC_GS_STUBS_SOURCE) + + mingw_gcc = MINGW_GXX.replace("g++", "gcc") + subprocess.run( + [mingw_gcc, "-c", src_path, "-o", obj_path], + stderr=subprocess.PIPE, + check=True, + ) + mingw_ar = MINGW_GXX.replace("g++", "ar") + subprocess.run( + [mingw_ar, "rcs", stubs_lib, obj_path], + stderr=subprocess.PIPE, + check=True, + ) + log.info("Created MSVC GS stubs library: %s", stubs_lib) + return "msvc_gs_stubs" + except (FileNotFoundError, subprocess.CalledProcessError): + log.warning( + "Failed to create MSVC GS stubs library.", + exc_info=True, + ) + if os.path.exists(stubs_lib): + os.remove(stubs_lib) + return None + finally: + for f in [src_path, obj_path]: + if f and os.path.exists(f): + os.remove(f) + + +def _ensure_mingw_cudart_import_lib(libraries_dirs: list[str]) -> list[str]: + """ + Auto-generate a MinGW-compatible import library (libcudart.a) + from the CUDA runtime DLL. This avoids linking against the hybrid cudart.lib + which contains MSVC-compiled static objects with /GS security symbols that + MinGW cannot resolve. + + Falls back to creating MSVC GS security stubs if the DLL is unavailable, + and falls back gracefully to the original cudart.lib if that also fails. + + Returns a list of extra library names to link (e.g. ["msvc_gs_stubs"]). + """ + import glob + + windows_cuda_home = os.environ.get("WINDOWS_CUDA_HOME") + if not windows_cuda_home: + log.debug( + "WINDOWS_CUDA_HOME not set, skipping MinGW cudart import lib generation" + ) + return [] + + for lib_dir in libraries_dirs: + if os.path.exists(os.path.join(lib_dir, "libcudart.a")): + log.debug("libcudart.a already exists in %s, skipping generation", lib_dir) + return [] + + # Find the CUDA runtime DLL for import lib generation + bin_dir = os.path.join(windows_cuda_home, "bin", "x64") + if not os.path.isdir(bin_dir): + bin_dir = os.path.join(windows_cuda_home, "bin") + dll_candidates = glob.glob(os.path.join(bin_dir, "cudart64_*.dll")) + + # Find a writable directory containing cudart.lib for output + output_dir = None + for lib_dir in libraries_dirs: + if os.path.isdir(lib_dir) and os.access(lib_dir, os.W_OK): + if os.path.exists(os.path.join(lib_dir, "cudart.lib")): + output_dir = lib_dir + break + + if not dll_candidates: + log.warning( + "No cudart64_*.dll found in %s. Cannot generate MinGW import library. " + "Will create MSVC GS security stubs as fallback.", + bin_dir, + ) + # Fallback: create GS stubs so the hybrid cudart.lib can link + if output_dir is not None: + stub_lib = _create_msvc_gs_stubs_lib(output_dir) + if stub_lib: + return [stub_lib] + return [] + + if output_dir is None: + log.warning( + "No writable directory containing cudart.lib found. " + "Cannot generate MinGW import library. " + "If linking fails with undefined references to __security_cookie, " + "ensure cudart.lib is present in one of: %s", + libraries_dirs, + ) + return [] + + dll_path = dll_candidates[0] + dll_name = os.path.basename(dll_path) + + def_path = os.path.join(output_dir, dll_name.replace(".dll", ".def")) + import_lib_path = os.path.join(output_dir, "libcudart.a") + + try: + _gen_mingw_import_lib(dll_path, def_path, import_lib_path) + return [] + except (FileNotFoundError, subprocess.CalledProcessError): + log.warning( + "Failed to generate MinGW cudart import library. " + "Falling back to MSVC GS stubs.", + exc_info=True, + ) + for f in [def_path, import_lib_path]: + if os.path.exists(f): + os.remove(f) + # Fallback: create GS stubs + stub_lib = _create_msvc_gs_stubs_lib(output_dir) + if stub_lib: + return [stub_lib] + return [] + + +def _transform_cuda_paths(lpaths: list[str]) -> None: + # This handles two cases: + # 1. Cases where libs are in (e.g.) lib/cuda-12 and lib/cuda-12/stubs + # 2. Linux machines may have CUDA installed under either lib64/ or lib/ + for i, path in enumerate(lpaths): + if "CUDA_HOME" in os.environ and path.startswith(os.environ["CUDA_HOME"]): + lib_dir: Path | None = _find_libcudart_static(path) + if lib_dir is None: + continue + lpaths[i] = str(lib_dir) + stub_dir = lib_dir / "stubs" + if stub_dir.exists(): + lpaths.append(str(stub_dir)) + + +def get_cpp_torch_device_options( + device_type: str, + aot_mode: bool = False, + compile_only: bool = False, +) -> tuple[list[str], list[str], list[str], list[str], list[str], list[str], list[str]]: + """ + This function is used to get the build args of device related build options. + 1. Device include_directories, libraries, libraries_directories. + 2. Device MACROs. + 3. MISC + 4. Return the build args + """ + definitions: list[str] = [] + include_dirs: list[str] = [] + cflags: list[str] = [] + ldflags: list[str] = [] + libraries_dirs: list[str] = [] + libraries: list[str] = [] + passthrough_args: list[str] = [] + if ( + config.is_fbcode() + and "CUDA_HOME" not in os.environ + and "CUDA_PATH" not in os.environ + ): + os.environ["CUDA_HOME"] = build_paths.sdk_home + + _set_gpu_runtime_env() + from torch.utils import cpp_extension + + include_dirs = cpp_extension.include_paths( + device_type, config.aot_inductor.link_libtorch is None + ) + link_libtorch = config.aot_inductor.link_libtorch + libraries_dirs = cpp_extension.library_paths( + device_type, + torch_include_dirs=link_libtorch, + cross_target_platform=config.aot_inductor.cross_target_platform, + ) + if device_type == "cuda": + definitions.append(" USE_ROCM" if torch.version.hip else " USE_CUDA") + + if torch.version.hip is not None: + if config.is_fbcode() or not link_libtorch: + libraries += ["amdhip64"] + else: + libraries += ["torch_hip"] + definitions.append(" __HIP_PLATFORM_AMD__") + else: + if config.is_fbcode() or not link_libtorch: + libraries += ["cuda"] + else: + libraries += ["cuda", "torch_cuda"] + if config.aot_inductor.cross_target_platform == "windows": + extra_libs = _ensure_mingw_cudart_import_lib(libraries_dirs) + libraries += ["cudart"] + extra_libs + _transform_cuda_paths(libraries_dirs) + + if device_type == "xpu": + definitions.append(" USE_XPU") + xpu_error_string = ( + "Intel GPU driver is not properly installed, please follow the instruction " + "in https://github.com/pytorch/pytorch?tab=readme-ov-file#intel-gpu-support." + ) + if _IS_WINDOWS: + ze_root = os.getenv("LEVEL_ZERO_V1_SDK_PATH") + if ze_root is None: + raise OSError(xpu_error_string) + include_dirs += [os.path.join(ze_root, "include")] + libraries_dirs += [os.path.join(ze_root, "lib")] + else: + # Suppress multi-line comment warnings in sycl headers + cflags += ["Wno-comment"] + if not find_library("ze_loader"): + raise OSError(xpu_error_string) + + libraries += ["ze_loader", "sycl"] + if link_libtorch: + libraries += ["torch_xpu"] + + if device_type == "mps": + definitions.append(" USE_MPS") + + if config.is_fbcode(): + include_dirs.append(build_paths.sdk_include) + + if aot_mode and device_type == "cuda": + if torch.version.hip is None: + if not compile_only: + # Only add link args, when compile_only is false. + passthrough_args = ["-Wl,-Bstatic -lcudart_static -Wl,-Bdynamic"] + + if device_type == "cpu": + ( + stdcxx_lib_dir_paths, + stdcxx_libs, + ) = _get_libstdcxx_args() + libraries_dirs += stdcxx_lib_dir_paths + libraries += stdcxx_libs + + if config.aot_inductor.custom_op_libs: + libraries += config.aot_inductor.custom_op_libs + + return ( + definitions, + include_dirs, + cflags, + ldflags, + libraries_dirs, + libraries, + passthrough_args, + ) + + +class CppTorchDeviceOptions(CppTorchOptions): + """ + This class is inherited from CppTorchOptions, which automatic contains + base cxx build options and torch common build options. And then it will + maintains cuda/xpu device related build args. + """ + + def __init__( + self, + vec_isa: VecISA = invalid_vec_isa, + include_pytorch: bool = False, + device_type: str = "cuda", + aot_mode: bool = False, + compile_only: bool = False, + use_relative_path: bool = False, + use_mmap_weights: bool = False, + use_mmap_weights_external: bool = False, + shared: bool = True, + extra_flags: Sequence[str] = (), + min_optimize: bool = False, + precompiling: bool = False, + preprocessing: bool = False, + compiler: str = "", + ) -> None: + super().__init__( + vec_isa=vec_isa, + include_pytorch=include_pytorch, + aot_mode=aot_mode, + compile_only=compile_only, + use_relative_path=use_relative_path, + use_mmap_weights=use_mmap_weights, + use_mmap_weights_external=use_mmap_weights_external, + extra_flags=extra_flags, + min_optimize=min_optimize, + precompiling=precompiling, + preprocessing=preprocessing, + compiler=compiler, + ) + + device_definitions: list[str] = [] + device_include_dirs: list[str] = [] + device_cflags: list[str] = [] + device_ldflags: list[str] = [] + device_libraries_dirs: list[str] = [] + device_libraries: list[str] = [] + device_passthrough_args: list[str] = [] + + ( + device_definitions, + device_include_dirs, + device_cflags, + device_ldflags, + device_libraries_dirs, + device_libraries, + device_passthrough_args, + ) = get_cpp_torch_device_options( + device_type=device_type, + aot_mode=aot_mode, + compile_only=compile_only, + ) + _append_list(self._definitions, device_definitions) + _append_list(self._include_dirs, device_include_dirs) + _append_list(self._cflags, device_cflags) + _append_list(self._ldflags, device_ldflags) + _append_list(self._libraries_dirs, device_libraries_dirs) + _append_list(self._libraries, device_libraries) + _append_list(self._passthrough_args, device_passthrough_args) + self._finalize_options() + + def _finalize_options(self) -> None: + super()._finalize_options() + if config.is_fbcode(): + # Re-order library search paths in case there are lib conflicts + # that also live in the FBCode python lib dir. + _, python_lib_dirs = _get_python_related_args() + assert len(python_lib_dirs) == 1, f"Python lib dirs: {python_lib_dirs}" + if python_lib_dirs[0] in self._libraries_dirs: + self._libraries_dirs.remove(python_lib_dirs[0]) + self._libraries_dirs.append(python_lib_dirs[0]) + + +def get_name_and_dir_from_output_file_path( + file_path: str, +) -> tuple[str, str]: + """ + This function help prepare parameters to new cpp_builder. + Example: + input_code: /tmp/tmpof1n5g7t/5c/c5crkkcdvhdxpktrmjxbqkqyq5hmxpqsfza4pxcf3mwk42lphygc.cpp + name, dir = get_name_and_dir_from_output_file_path(input_code) + Run result: + name = c5crkkcdvhdxpktrmjxbqkqyq5hmxpqsfza4pxcf3mwk42lphygc + dir = /tmp/tmpof1n5g7t/5c/ + + put 'name' and 'dir' to CppBuilder's 'name' and 'output_dir'. + CppBuilder --> get_target_file_path will format output path according OS: + Linux: /tmp/tmppu87g3mm/zh/czhwiz4z7ca7ep3qkxenxerfjxy42kehw6h5cjk6ven4qu4hql4i.so + Windows: [Windows temp path]/tmppu87g3mm/zh/czhwiz4z7ca7ep3qkxenxerfjxy42kehw6h5cjk6ven4qu4hql4i.dll + """ + name_and_ext = os.path.basename(file_path) + name, _ext = os.path.splitext(name_and_ext) + dir = os.path.dirname(file_path) + + return name, dir + + +class CppBuilder: + """ + CppBuilder is a cpp jit builder, and it supports both Windows, Linux and MacOS. + Args: + name: + 1. Build target name, the final target file will append extension type automatically. + 2. Due to the CppBuilder is supports multiple OS, it will maintains ext for OS difference. + sources: + Source code file list to be built. + BuildOption: + Build options to the builder. + output_dir: + 1. The output_dir the target file will output to. + 2. The default value is empty string, and then the use current dir as output dir. + 3. Final target file: output_dir/name.ext + """ + + @staticmethod + def __get_python_module_flags() -> tuple[str, str]: + extension = ".pyd" if _IS_WINDOWS else ".so" + output_flags = "/Fe" if _IS_WINDOWS else "-o" + return extension, output_flags + + @staticmethod + def __get_object_flags() -> tuple[str, str]: + extension = ".obj" if _IS_WINDOWS else ".o" + output_flags = "/c /Fo" if _IS_WINDOWS else "-c -o" # codespell:ignore + return extension, output_flags + + @staticmethod + def __get_precompiled_header_flags() -> tuple[str, str]: + extension = ".pch" if _IS_WINDOWS or not is_gcc() else ".gch" + output_flags = "/Fp" if _IS_WINDOWS else "-o" + return extension, output_flags + + @staticmethod + def __get_preprocessor_output_flags() -> tuple[str, str]: + extension = ".i" + output_flags = "/EP /P" if _IS_WINDOWS else "-E -P -o" + return extension, output_flags + + def __init__( + self, + name: str, + sources: str | list[str], + BuildOption: BuildOptionsBase, + output_dir: str = "", + ) -> None: + self._compiler = "" + self._cflags_args = "" + self._definitions_args = "" + self._include_dirs_args = "" + self._ldflags_args = "" + self._libraries_dirs_args = "" + self._libraries_args = "" + self._passthrough_parameters_args = "" + + # When relative path is used, we need to maintain the source dir list. + self._orig_source_paths = [] + self._output_dir = "" + self._target_file = "" + + self._use_relative_path: bool = False + self._aot_mode: bool = False + + self._name = name + self._target_name = ( + config.aot_inductor.model_name_for_generated_files or "aoti_model" + ) + + # Code start here, initial self internal variables firstly. + self._build_option = BuildOption + self._compiler = BuildOption.get_compiler() + self._use_relative_path = BuildOption.get_use_relative_path() + self._aot_mode = BuildOption.get_aot_mode() + + self._output_dir = output_dir + + self._compile_only = BuildOption.get_compile_only() + self._precompiling = BuildOption.get_precompiling() + self._preprocessing = BuildOption.get_preprocessing() + # Only one of these options (if any) should be true at any given time. + assert sum((self._compile_only, self._precompiling, self._preprocessing)) <= 1 + self._do_link = not ( + self._compile_only or self._precompiling or self._preprocessing + ) + + # MSVC produces two files when precompiling: the actual .pch file, as well as an + # object file which must be linked into the final library. This class assumes + # only one output file of note, so for now we'll error out here. + assert not _IS_WINDOWS or not self._precompiling, ( + "Cannot currently precompile headers on Windows!" + ) + + if self._compile_only: + file_ext, output_flags = self.__get_object_flags() + elif self._precompiling: + file_ext, output_flags = self.__get_precompiled_header_flags() + elif self._preprocessing: + file_ext, output_flags = self.__get_preprocessor_output_flags() + else: + file_ext, output_flags = self.__get_python_module_flags() + self._target_file = os.path.join(self._output_dir, f"{self._name}{file_ext}") + + relative_target_file = ( + os.path.basename(self._target_file) + if self._use_relative_path + else self._target_file + ) + if _IS_WINDOWS: + if self._preprocessing: + # The target file name is automatically determined by MSVC. + self._output = output_flags + else: + self._output = f"{output_flags}{relative_target_file}" + else: + self._output = f"{output_flags} {relative_target_file}" + + if isinstance(sources, str): + sources = [sources] + + # Use relative paths only when requested (typically for remote builds) + if config.is_fbcode() and self._use_relative_path: + # Will create another temp directory for building, so do NOT use the + # absolute path. + self._orig_source_paths = list(sources) + sources = [os.path.basename(i) for i in sources] + + if self._precompiling: + assert len(sources) == 1 + # See above; we can currently assume this is not on MSVC. + self._sources_args = f"-x c++-header {sources[0]}" + if self._use_relative_path and _is_clang(BuildOption.get_compiler()): + # Store PCH paths relative to -isysroot so the .pch can + # be used from a different build directory. The matching + # -isysroot is injected by build_fbcode_re(). + self._cflags_args += " -relocatable-pch -Xclang -fno-pch-timestamp " + else: + self._sources_args = " ".join(sources) + + for cflag in BuildOption.get_cflags(): + if _IS_WINDOWS: + self._cflags_args += f"/{cflag} " + else: + self._cflags_args += f"-{cflag} " + + for definition in BuildOption.get_definitions(): + if _IS_WINDOWS: + self._definitions_args += f"/D {definition} " + else: + self._definitions_args += f"-D {definition} " + + if precompiled_header := BuildOption.precompiled_header: + if _IS_WINDOWS: + log.warning( + "Precompiled header support for MSVC is currently unavailable; ignoring %s", + precompiled_header, + ) + else: + self._include_dirs_args = f"-include {precompiled_header} " + if self._use_relative_path and _is_clang(BuildOption.get_compiler()): + # Skip clang's own PCH validation during consumption. + # _precompile_header() already handles cache invalidation + # via content hashing, and -fno-validate-pch allows the + # PCH to be used even when the original source file is at + # a different path (e.g. across Remote Execution workers). + self._cflags_args += " -Xclang -fno-validate-pch " + + for inc_dir in BuildOption.get_include_dirs(): + if _IS_WINDOWS: + self._include_dirs_args += f'/I "{inc_dir}" ' + else: + self._include_dirs_args += f"-I{shlex.quote(inc_dir)} " + + for ldflag in BuildOption.get_ldflags(): + if _IS_WINDOWS: + self._ldflags_args += f"/{ldflag} " + else: + self._ldflags_args += f"-{ldflag} " + + for lib_dir in BuildOption.get_libraries_dirs(): + if _IS_WINDOWS: + self._libraries_dirs_args += f'/LIBPATH:"{lib_dir}" ' + else: + self._libraries_dirs_args += f"-L{lib_dir} " + + for lib in BuildOption.get_libraries(): + if _IS_WINDOWS: + self._libraries_args += f'"{lib}.lib" ' + else: + self._libraries_args += f"-l{lib} " + + for passthrough_arg in BuildOption.get_passthrough_args(): + self._passthrough_parameters_args += f"{passthrough_arg} " + + def get_command_line(self) -> str: + def format_build_command( + compiler: str, + sources: str, + include_dirs_args: str, + definitions_args: str, + cflags_args: str, + ldflags_args: str, + libraries_args: str, + libraries_dirs_args: str, + passthrough_args: str, + output: str, + ) -> str: + if _IS_WINDOWS: + # https://learn.microsoft.com/en-us/cpp/build/walkthrough-compile-a-c-program-on-the-command-line?view=msvc-1704 + # https://stackoverflow.com/a/31566153 + cmd = ( + f"{compiler} {include_dirs_args} {definitions_args} {cflags_args} " + f"{sources} {passthrough_args} {output}" + ) + if self._do_link: + cmd += f" /LD /link {libraries_dirs_args} {libraries_args} {ldflags_args}" + cmd = normalize_path_separator(cmd) + else: + cmd = ( + f"{compiler} {sources} {definitions_args} {cflags_args} " + f"{include_dirs_args} {passthrough_args} {output}" + ) + if self._do_link: + cmd += f" {ldflags_args} {libraries_args} {libraries_dirs_args}" + return cmd + + command_line = format_build_command( + compiler=self._compiler, + sources=self._sources_args, + include_dirs_args=self._include_dirs_args, + definitions_args=self._definitions_args, + cflags_args=self._cflags_args, + ldflags_args=self._ldflags_args, + libraries_args=self._libraries_args, + libraries_dirs_args=self._libraries_dirs_args, + passthrough_args=self._passthrough_parameters_args, + output=self._output, + ) + return command_line + + def get_target_file_path(self) -> str: + return normalize_path_separator(self._target_file) + + def build_fbcode_re( + self, + ) -> None: + with dynamo_timed("compile_file"): + command = shlex.split(self.get_command_line()) + try: + output_path = self._target_file + # When we build remotely, we need to make sure to carefully copy any files + # that are required during the compilation process into our build directly. + # This is where all of the ATen/c10/Torch includes come from. + torch_includes_path = os.path.join(_TORCH_PATH, "include") + with tempfile.TemporaryDirectory() as tmp_dir: + # Copy everything to tmp compilation folder + shutil.copy(_LINKER_SCRIPT, os.path.join(tmp_dir, "script.ld")) + for src in self._orig_source_paths: + shutil.copy(src, os.path.join(tmp_dir, os.path.basename(src))) + dest_include_path = os.path.join(tmp_dir, "include") + shutil.copytree(torch_includes_path, dest_include_path) + + # Copy precompiled header (.h and .gch/.pch) into the + # build directory and rewrite the -include flag so the + # compiler can find it. + pch_header = self._build_option.precompiled_header + if pch_header and os.path.isfile(pch_header): + pch_ext = ".pch" if _IS_WINDOWS or not is_gcc() else ".gch" + pch_compiled = pch_header + pch_ext + pch_basename = os.path.basename(pch_header) + shutil.copy(pch_header, os.path.join(tmp_dir, pch_basename)) + if os.path.isfile(pch_compiled): + shutil.copy( + pch_compiled, + os.path.join(tmp_dir, pch_basename + pch_ext), + ) + command = [ + pch_basename if arg == pch_header else arg + for arg in command + ] + + # Relocatable PCH stores include paths relative to + # -isysroot. Set sysroot to the tmp build dir so + # paths resolve correctly in both precompilation and + # later kernel compilations that consume the PCH. + if self._precompiling or ( + pch_header and os.path.isfile(pch_header) + ): + command[1:1] = ["-isysroot", "."] + + # Run the build, raising RuntimeError on failure instead of + # SkipFrame so compilation errors propagate rather than + # silently falling back to eager execution. + tmp_output_path = _run_build_command( + command, + tmp_dir, + os.path.basename(output_path), + exception_class=RuntimeError, + ) + # Copy output from the build + if os.path.exists(output_path): + os.remove(output_path) + shutil.copy(tmp_output_path, output_path) + if output_path.endswith(".o"): + os.chmod(output_path, 0o644) + elif output_path.endswith(".so"): + os.chmod(output_path, 0o755) + except subprocess.CalledProcessError as e: + raise exc.CppCompileError(command, e.output.decode("utf-8")) from e + + def build(self) -> None: + """ + It is must need a temporary directory to store object files in Windows. + After build completed, delete the temporary directory to save disk space. + """ + if self._use_relative_path: + # remote build uses relative path + return self.build_fbcode_re() + _create_if_dir_not_exist(self._output_dir) + _build_tmp_dir = os.path.join( + self._output_dir, f"{self._name}_{_BUILD_TEMP_DIR}" + ) + _create_if_dir_not_exist(_build_tmp_dir) + + build_cmd = self.get_command_line() + run_compile_cmd(build_cmd, cwd=_build_tmp_dir) + _remove_dir(_build_tmp_dir) + + def save_compile_cmd_to_cmake( + self, + cmake_path: str, + device_type: str, + ) -> None: + """ + Save global cmake settings here, e.g. compiler options. + If targeting CUDA, also emit a custom function to embed CUDA kernels. + """ + + definitions = " ".join(self._build_option.get_definitions()) + target_library_type = ( + "STATIC" if not config.aot_inductor.dynamic_linkage else "SHARED" + ) + + contents = textwrap.dedent( + f""" + cmake_minimum_required(VERSION 3.27 FATAL_ERROR) + project({self._target_name} LANGUAGES CXX) + set(CMAKE_CXX_STANDARD 20) + + # Set a library target + add_library({self._target_name} {target_library_type}) + + """ + ) + + if config.aot_inductor.link_libtorch or config.test_configs.use_libtorch: + # When compile_standalone is True, the generated cpp project should + # not use Torch. But for unit testing purpose, we need to use Torch here. + contents += textwrap.dedent( + """ + # May need to point CMAKE_PREFIX_PATH to the right torch location + find_package(Torch REQUIRED) + + """ + ) + # flags and macros here are mostly CPU specific. Not emitting them for GPU models + # will make the generated CMake file more portable and won't really hurt performance. + # NOTE: standalone focuses on GPU now. For CPU, some of the flags and macros may + # be still needed. + contents += textwrap.dedent( + f""" + # Add macro definitions + target_compile_definitions({self._target_name} PRIVATE {definitions}) + + # Add compile flags + target_compile_options({self._target_name} PRIVATE {self._cflags_args}) + + # Backend-specific flags + target_compile_options({self._target_name} PRIVATE {self._passthrough_parameters_args} -c) + + """ + ) + else: + # When compile_standalone is True, use TorchStandalone instead of Torch + contents += textwrap.dedent( + f""" + find_package(TorchStandalone REQUIRED) + # Set up include directories to find headers at the correct paths + target_include_directories({self._target_name} PRIVATE ${{TorchStandalone_INCLUDE_DIRS}}) + target_include_directories({self._target_name} PRIVATE ${{TorchStandalone_INCLUDE_DIRS}}/standalone) + + """ + ) + + if device_type == "cuda" and torch.version.hip is None: + from torch._inductor.codegen.cuda.compile_utils import ( + _nvcc_arch_as_compile_option, + ) + + current_arch = _nvcc_arch_as_compile_option() + contents += textwrap.dedent( + f""" + enable_language(CUDA) + set(CMAKE_CUDA_STANDARD 17) + find_package(CUDAToolkit REQUIRED) + target_include_directories({self._target_name} PRIVATE ${{CUDAToolkit_INCLUDE_DIRS}}) + target_compile_definitions({self._target_name} PRIVATE USE_CUDA) + target_link_libraries({self._target_name} PRIVATE cuda CUDA::cudart_static) + + find_program(OBJCOPY_EXECUTABLE objcopy) + if(NOT OBJCOPY_EXECUTABLE) + message(FATAL_ERROR "objcopy not found. Cannot embed fatbin as object file") + endif() + + set(KERNEL_TARGETS "") + set(KERNEL_OBJECT_FILES "") + # Function to embed a single kernel + function(embed_gpu_kernel KERNEL_NAME PTX_FILE) + set(FATBIN_BASENAME ${{KERNEL_NAME}}.fatbin) + set(FATBIN_FILE ${{CMAKE_CURRENT_BINARY_DIR}}/${{FATBIN_BASENAME}}) + set(OBJECT_BASENAME ${{KERNEL_NAME}}.fatbin.o) + set(OBJECT_FILE ${{CMAKE_CURRENT_BINARY_DIR}}/${{OBJECT_BASENAME}}) + + # --- Define UNIQUE C symbol names --- + set(SYMBOL_START __${{KERNEL_NAME}}_start) + set(SYMBOL_END __${{KERNEL_NAME}}_end) + set(SYMBOL_SIZE __${{KERNEL_NAME}}_size) + string(REGEX REPLACE "[^a-zA-Z0-9]" "_" MANGLED_BASENAME ${{FATBIN_FILE}}) + set(OBJCOPY_START_SYM _binary_${{MANGLED_BASENAME}}_start) + set(OBJCOPY_END_SYM _binary_${{MANGLED_BASENAME}}_end) + set(OBJCOPY_SIZE_SYM _binary_${{MANGLED_BASENAME}}_size) + + # --- PTX to FATBIN Command & Target --- + add_custom_command( + OUTPUT ${{FATBIN_FILE}} + COMMAND ${{CUDAToolkit_NVCC_EXECUTABLE}} --fatbin ${{PTX_FILE}} -o ${{FATBIN_FILE}} ${{NVCC_GENCODE_FLAGS}} + -gencode arch=compute_{current_arch},code=compute_{current_arch} + -gencode arch=compute_{current_arch},code=sm_{current_arch} + DEPENDS ${{PTX_FILE}} + ) + + # --- FATBIN to Object File (.o) Command --- + add_custom_command( + OUTPUT ${{OBJECT_FILE}} + COMMAND ${{CMAKE_LINKER}} -r -b binary -z noexecstack -o ${{OBJECT_FILE}} ${{FATBIN_FILE}} + COMMAND ${{OBJCOPY_EXECUTABLE}} --rename-section .data=.rodata,alloc,load,readonly,data,contents + ${{OBJECT_FILE}} + COMMAND ${{OBJCOPY_EXECUTABLE}} + --redefine-sym ${{OBJCOPY_START_SYM}}=${{SYMBOL_START}} + --redefine-sym ${{OBJCOPY_END_SYM}}=${{SYMBOL_END}} + --redefine-sym ${{OBJCOPY_SIZE_SYM}}=${{SYMBOL_SIZE}} + ${{OBJECT_FILE}} + DEPENDS ${{FATBIN_FILE}} + ) + add_custom_target(build_kernel_object_${{KERNEL_NAME}} DEPENDS ${{OBJECT_FILE}}) + + # --- Add to a list for linking later --- + set(KERNEL_TARGETS ${{KERNEL_TARGETS}} build_kernel_object_${{KERNEL_NAME}} PARENT_SCOPE) + set(KERNEL_OBJECT_FILES ${{KERNEL_OBJECT_FILES}} ${{OBJECT_FILE}} PARENT_SCOPE) + endfunction() + + """ + ) + elif device_type == "xpu": + contents += textwrap.dedent( + """ + find_program(OBJCOPY_EXECUTABLE objcopy) + if(NOT OBJCOPY_EXECUTABLE) + message(FATAL_ERROR "objcopy not found. Cannot embed spv as object file") + endif() + + set(KERNEL_TARGETS "") + set(KERNEL_OBJECT_FILES "") + # Function to embed a single kernel + function(embed_gpu_kernel KERNEL_NAME SPV_FILE) + set(OBJECT_BASENAME ${KERNEL_NAME}.spv.o) + set(OBJECT_FILE ${CMAKE_CURRENT_BINARY_DIR}/${OBJECT_BASENAME}) + + # --- Define UNIQUE C symbol names --- + set(SYMBOL_START __${KERNEL_NAME}_start) + set(SYMBOL_END __${KERNEL_NAME}_end) + set(SYMBOL_SIZE __${KERNEL_NAME}_size) + string(REGEX REPLACE "[^a-zA-Z0-9]" "_" MANGLED_BASENAME ${SPV_FILE}) + set(OBJCOPY_START_SYM _binary_${MANGLED_BASENAME}_start) + set(OBJCOPY_END_SYM _binary_${MANGLED_BASENAME}_end) + set(OBJCOPY_SIZE_SYM _binary_${MANGLED_BASENAME}_size) + + # --- SPV_FILE to Object File (.o) Command --- + add_custom_command( + OUTPUT ${OBJECT_FILE} + COMMAND ${CMAKE_LINKER} -r -b binary -z noexecstack -o ${OBJECT_FILE} ${SPV_FILE} + COMMAND ${OBJCOPY_EXECUTABLE} --rename-section .data=.rodata,alloc,load,readonly,data,contents + ${OBJECT_FILE} + COMMAND ${OBJCOPY_EXECUTABLE} + --redefine-sym ${OBJCOPY_START_SYM}=${SYMBOL_START} + --redefine-sym ${OBJCOPY_END_SYM}=${SYMBOL_END} + --redefine-sym ${OBJCOPY_SIZE_SYM}=${SYMBOL_SIZE} + ${OBJECT_FILE} + DEPENDS ${SPV_FILE} + ) + add_custom_target(build_kernel_object_${KERNEL_NAME} DEPENDS ${OBJECT_FILE}) + + # --- Add to a list for linking later --- + set(KERNEL_TARGETS ${KERNEL_TARGETS} build_kernel_object_${KERNEL_NAME} PARENT_SCOPE) + set(KERNEL_OBJECT_FILES ${KERNEL_OBJECT_FILES} ${OBJECT_FILE} PARENT_SCOPE) + endfunction() + + """ + ) + + with open(cmake_path, "w") as f: + f.write(contents) + + def save_src_to_cmake(self, cmake_path: str, src_path: str) -> None: + # Remove the directory part of file_path + src_path = "${CMAKE_CURRENT_SOURCE_DIR}/" + Path(src_path).name + with open(cmake_path, "a") as f: + f.write(f"target_sources({self._target_name} PRIVATE {src_path})\n") + + def save_kernel_asm_to_cmake(self, cmake_path: str, asm_files: list[str]) -> None: + # TODO: make this work beyond CUDA + with open(cmake_path, "a") as f: + for asm_file in asm_files: + kernel_name = Path(asm_file).name.split(".")[0] + asm_file = f"${{CMAKE_CURRENT_SOURCE_DIR}}/{Path(asm_file).name}" + contents = textwrap.dedent( + f""" + embed_gpu_kernel({kernel_name} {asm_file}) + """ + ) + f.write(contents) + if asm_files: + f.write(f"add_dependencies({self._target_name} ${{KERNEL_TARGETS}})\n") + f.write( + f"target_link_libraries({self._target_name} PRIVATE ${{KERNEL_OBJECT_FILES}})\n" + ) + + def save_link_cmd_to_cmake(self, cmake_path: str) -> None: + lflags = " ".join(self._build_option.get_ldflags()) + libs = " ".join(self._build_option.get_libraries()) + contents = textwrap.dedent( + f""" + # Add linker flags + target_link_options({self._target_name} PRIVATE {lflags}) + + # Add libraries + target_link_libraries({self._target_name} PRIVATE {libs}) + """ + ) + + assert os.path.exists(cmake_path), ( + f"save_link_cmd_to_cmakefile expects {cmake_path} to already exist" + ) + with open(cmake_path, "a") as f: + f.write(contents) + + +def run_asm_build_object(src: str, target: str, cwd: str) -> None: + def get_asm_compiler() -> str: + if _IS_WINDOWS: + ASM_CC = "ml64" + else: + ASM_CC = get_cpp_compiler() + # Intel compiler is not support to compile asm, switch to gcc. + if _is_intel_compiler(ASM_CC): + ASM_CC = "gcc" + return ASM_CC + + def get_command_line(asm_cc: str, src: str, target: str) -> str: + if _IS_WINDOWS: + # Format reference: + # https://learn.microsoft.com/en-us/cpp/assembler/masm/ml-and-ml64-command-line-reference?view=msvc-170 + cmd = f"{asm_cc} {src} /c /Fo {target}" # codespell:ignore /Fo + else: + cmd = f"{asm_cc} -c {src} -o {target}" + + return cmd + + asm_cc = get_asm_compiler() + cmd = get_command_line( + asm_cc=asm_cc, + src=normalize_path_separator(src), + target=normalize_path_separator(target), + ) + run_compile_cmd(cmd, cwd=normalize_path_separator(cwd)) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/cpu_vec_isa.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/cpu_vec_isa.py new file mode 100644 index 0000000000000000000000000000000000000000..e4aee2d6c0258e69927c2fb6a23c4018e63e4926 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/cpu_vec_isa.py @@ -0,0 +1,565 @@ +# mypy: allow-untyped-defs +import dataclasses +import functools +import os +import platform +import re +import subprocess +import sys +import warnings +from collections.abc import Callable +from typing import Any + +import torch +from torch._inductor import config +from torch._inductor.utils import python_subprocess_env + + +_IS_WINDOWS = sys.platform == "win32" + + +def _get_isa_dry_compile_fingerprint(isa_flags: str) -> str: + # ISA dry compile will cost about 1 sec time each startup time. + # Please check the issue: https://github.com/pytorch/pytorch/issues/100378 + # Actually, dry compile is checking compile capability for ISA. + # We just record the compiler version, isa options and pytorch version info, + # and generated them to output binary hash path. + # It would optimize and skip compile existing binary. + from torch._inductor.cpp_builder import get_compiler_version_info, get_cpp_compiler + + compiler_info = get_compiler_version_info(get_cpp_compiler()) + torch_version = torch.__version__ + fingerprint = f"{compiler_info}={isa_flags}={torch_version}" + return fingerprint + + +class VecISA: + _bit_width: int + _macro: list[str] + _arch_flags: str + _dtype_nelements: dict[torch.dtype, int] + + # Note [Checking for Vectorized Support in Inductor] + # TorchInductor CPU vectorization reuses PyTorch vectorization utility functions + # Hence, TorchInductor would depend on Sleef* to accelerate mathematical functions + # like exp, pow, sin, cos and etc. + # But PyTorch and TorchInductor might use different compilers to build code. If + # PyTorch uses gcc-7/g++-7 to build the release package, the libtorch_cpu.so + # will not expose the Sleef* AVX512 symbols since gcc-7/g++-7 cannot pass + # avx512 check in CMake - FindAVX.cmake. But TorchInductor install the latest + # gcc/g++ compiler by default while it could support the AVX512 compilation. + # Therefore, there would be a conflict sleef version between PyTorch and + # TorchInductor. Hence, we dry-compile the following code to check whether current + # HW platform and PyTorch both could support AVX512 or AVX2. And suppose ARM + # also needs the logic + # In fbcode however, we are using the same compiler for pytorch and for inductor codegen, + # making the runtime check unnecessary. + _avx_code = """ +#if defined(CPU_CAPABILITY_AVX512) || defined(CPU_CAPABILITY_AVX2) || defined(CPU_CAPABILITY_ZVECTOR) || defined(CPU_CAPABILITY_NEON) || defined(CPU_CAPABILITY_VSX) || defined(CPU_CAPABILITY_SVE) +#include +#include +#endif + +alignas(64) float in_out_ptr0[16] = {0.0}; + +extern "C" void __avx_chk_kernel() { + auto tmp0 = at::vec::Vectorized(1); + auto tmp1 = tmp0.exp(); + tmp1.store(in_out_ptr0); +} +""" # noqa: B950 + + _avx_py_load = """ +import torch +from ctypes import cdll +cdll.LoadLibrary("__lib_path__") +""" + + def bit_width(self) -> int: + return self._bit_width + + def nelements(self, dtype: torch.dtype = torch.float) -> int: + return self._dtype_nelements[dtype] + + def build_macro(self) -> list[str]: + return self._macro + + def build_arch_flags(self) -> str: + return self._arch_flags + + def __hash__(self) -> int: + return hash(str(self)) + + def check_build(self, code: str) -> bool: + from torch._inductor.codecache import get_lock_dir, LOCK_TIMEOUT, write + from torch._inductor.cpp_builder import ( + CppBuilder, + CppTorchOptions, + normalize_path_separator, + ) + + key, input_path = write( + code, + "cpp", + extra=_get_isa_dry_compile_fingerprint(self._arch_flags), + ) + from torch.utils._filelock import FileLock + + lock_dir = get_lock_dir() + lock = FileLock(os.path.join(lock_dir, key + ".lock"), timeout=LOCK_TIMEOUT) + with lock: + output_dir = os.path.dirname(input_path) + buid_options = CppTorchOptions(vec_isa=self, warning_all=False) + x86_isa_help_builder = CppBuilder( + key, + [input_path], + buid_options, + output_dir, + ) + try: + # Check if the output file exist, and compile when not. + output_path = normalize_path_separator( + x86_isa_help_builder.get_target_file_path() + ) + if not os.path.isfile(output_path): + x86_isa_help_builder.build() + + # Check build result + subprocess.check_call( + [ + sys.executable, + "-c", + VecISA._avx_py_load.replace("__lib_path__", output_path), + ], + cwd=output_dir, + stderr=subprocess.DEVNULL, + env=python_subprocess_env(), + ) + except Exception: + return False + + return True + + def __bool__(self) -> bool: + return self.__bool__impl(config.cpp.vec_isa_ok) + + @functools.cache # noqa: B019 + def __bool__impl(self, vec_isa_ok) -> bool: + if vec_isa_ok is not None: + return vec_isa_ok + + if config.is_fbcode(): + return True + + return self.check_build(VecISA._avx_code) + + +@dataclasses.dataclass +class VecNEON(VecISA): + _bit_width = 128 # This is required to leverage the compute implemented in aten/src/ATen/cpu/vec/vec128/vec128_float_neon.h + _macro = ["CPU_CAPABILITY_NEON", "AT_BUILD_ARM_VEC256_WITH_SLEEF"] + _arch_flags = "" # Unused + _dtype_nelements = {torch.float: 4, torch.bfloat16: 8, torch.float16: 8} + + def __str__(self) -> str: + if config.is_fbcode(): + return "neon" + return "asimd" # detects the presence of advanced SIMD on armv8-a kernels + + __hash__: Callable[[VecISA], Any] = VecISA.__hash__ # type: ignore[assignment] + + +@dataclasses.dataclass +class VecSVE256(VecISA): + # this function can be repurposed for SVE with variable vec length + _bit_width = 256 + _macro = [ + "CPU_CAPABILITY_SVE", + "CPU_CAPABILITY_SVE256", + "AT_BUILD_ARM_VEC256_WITH_SLEEF", + "__ARM_FEATURE_BF16", + ] + _arch_flags = "-march=armv8-a+sve+bf16 -msve-vector-bits=256" + + _dtype_nelements = {torch.float: 8, torch.bfloat16: 16, torch.float16: 16} + + def __str__(self) -> str: + if config.is_fbcode(): + return "neon" + return "asimd" + + __hash__: Callable[[VecISA], Any] = VecISA.__hash__ # type: ignore[assignment] + + +@dataclasses.dataclass +class VecAVX512(VecISA): + _bit_width = 512 + _macro = ["CPU_CAPABILITY_AVX512"] + _arch_flags = ( + "-mavx512f -mavx512dq -mavx512vl -mavx512bw -mfma" + if not _IS_WINDOWS + else "/arch:AVX512" + ) # TODO: use cflags + _dtype_nelements = {torch.float: 16, torch.bfloat16: 32, torch.float16: 32} + _is_avx512_bf16_supported = False + + def __str__(self) -> str: + return "avx512" + + __hash__: Callable[[VecISA], Any] = VecISA.__hash__ # type: ignore[assignment] + + _avx512_bf16_code = """ +#include +#include + +extern "C" __m512bh __avx512_bf16_chk_kernel(__m512 a, __m512 b) { + return _mm512_cvtne2ps_pbh(a, b); +} +""" + + @functools.cache # noqa: B019 + # pyrefly: ignore [bad-override] + def __bool__(self) -> bool: + if super().__bool__(): + if config.is_fbcode(): + return False + # check avx512_bf16 + if torch.cpu._is_avx512_bf16_supported() and not _IS_WINDOWS: + # save _arch_flags + base_flags = self._arch_flags + # temporarily change _arch_flags for avx512_bf16 check_build + self._arch_flags += " -mavx512bf16" + if self.check_build(self._avx512_bf16_code): + self._is_avx512_bf16_supported = True + # restore _arch_flags + self._arch_flags = base_flags + + return True + return False + + @functools.lru_cache(None) # noqa: B019 + def is_avx512_bf16_supported(self) -> bool: + return self._is_avx512_bf16_supported + + def build_arch_flags(self) -> str: + if self._is_avx512_bf16_supported: + return self._arch_flags + " -mavx512bf16" + else: + return self._arch_flags + + +@dataclasses.dataclass +class VecAVX512VNNI(VecAVX512): + _bit_width = 512 + _arch_flags = VecAVX512._arch_flags + " -mavx512vnni -mavx512vl" + _dtype_nelements = { + torch.float: 16, + torch.bfloat16: 32, + torch.float16: 32, + torch.int8: 64, + torch.uint8: 64, + } + + def __str__(self) -> str: + return super().__str__() + " avx512_vnni" + + __hash__: Callable[[VecISA], Any] = VecISA.__hash__ # type: ignore[assignment] + + _avx512_vnni_code = """ +#include +#include + +extern "C" __m256i __avx512_vnni_chk_kernel_1(__m256i src, __m256i a, __m256i b) { + return _mm256_dpbusd_epi32(src, a, b); +} + +extern "C" __m512i __avx512_vnni_chk_kernel_2(__m512i src, __m512i a, __m512i b) { + return _mm512_dpbusd_epi32(src, a, b); +} +""" + + @functools.cache # noqa: B019 + def __bool__(self) -> bool: + if super().__bool__(): + if config.is_fbcode(): + return False + if ( + torch.cpu._is_vnni_supported() + and not _IS_WINDOWS + and self.check_build(self._avx512_vnni_code) + ): + return True + return False + + def build_arch_flags(self) -> str: + return self._arch_flags + + +@dataclasses.dataclass +class VecAMX(VecAVX512VNNI): + _arch_flags = VecAVX512VNNI._arch_flags + " -mamx-tile -mamx-bf16 -mamx-int8" + # check amx_fp16 separately since it is not always supported when amx is supported + # amx_fp16 intrinsic compilation need gcc >=13 on platforms which support amx_fp16 + _is_amx_fp16_supported = False + + def __str__(self) -> str: + return super().__str__() + " amx_tile" + + __hash__: Callable[[VecISA], Any] = VecISA.__hash__ + + _amx_code = """ +#include +#include + +struct amx_tilecfg { + uint8_t palette_id; + uint8_t start_row; + uint8_t reserved_0[14]; + uint16_t colsb[16]; + uint8_t rows[16]; +}; + +extern "C" void __amx_chk_kernel() { + amx_tilecfg cfg = {0}; + _tile_loadconfig(&cfg); + _tile_zero(0); + _tile_dpbf16ps(0, 1, 2); + _tile_dpbusd(0, 1, 2); +} +""" + + _amx_fp16_code = _amx_code.replace("_tile_dpbf16ps", "_tile_dpfp16ps") + + @functools.cache # noqa: B019 + def __bool__(self) -> bool: + if super().__bool__(): + if config.is_fbcode(): + return False + if self.check_build(VecAMX._amx_code) and torch.cpu._init_amx(): + # check amx-fp16 as well when check amx + if torch.cpu._is_amx_fp16_supported(): + # save _arch_flags + base_flags = self._arch_flags + # temporarily change _arch_flags for amx-fp16 check_build + self._arch_flags += " -mamx-fp16" + if self.check_build(VecAMX._amx_fp16_code): + self._is_amx_fp16_supported = True + # restore _arch_flags + self._arch_flags = base_flags + + return True + return False + + @functools.lru_cache(None) # noqa: B019 + def is_amx_fp16_supported(self) -> bool: + return self._is_amx_fp16_supported + + def build_arch_flags(self) -> str: + extra_flags = "" + if self._is_avx512_bf16_supported: + # avx512_bf16 is not among the base flags, so we need to check and add it here + # And we need this flag in the WOQ case for dequantization + extra_flags += " -mavx512bf16" + if self._is_amx_fp16_supported: + extra_flags += " -mamx-fp16" + return self._arch_flags + extra_flags + + +@dataclasses.dataclass +class VecAVX2(VecISA): + _bit_width = 256 + _macro = ["CPU_CAPABILITY_AVX2"] + _arch_flags = ( + "-mavx2 -mfma -mf16c" if not _IS_WINDOWS else "/arch:AVX2" + ) # TODO: use cflags + _dtype_nelements = {torch.float: 8, torch.bfloat16: 16, torch.float16: 16} + + def __str__(self) -> str: + return "avx2" + + __hash__: Callable[[VecISA], Any] = VecISA.__hash__ # type: ignore[assignment] + + +@dataclasses.dataclass +class VecZVECTOR(VecISA): + _bit_width = 256 + _macro = [ + "CPU_CAPABILITY_ZVECTOR", + "CPU_CAPABILITY=ZVECTOR", + "HAVE_ZVECTOR_CPU_DEFINITION", + ] + _arch_flags = "-mvx -mzvector" + _dtype_nelements = {torch.float: 8, torch.bfloat16: 16, torch.float16: 16} + + def __str__(self) -> str: + return "zvector" + + __hash__: Callable[[VecISA], Any] = VecISA.__hash__ # type: ignore[assignment] + + +@dataclasses.dataclass +class VecVSX(VecISA): + _bit_width = 256 # VSX simd supports 128 bit_width, but aten is emulating it as 256 + _macro = ["CPU_CAPABILITY_VSX"] + _arch_flags = "-mvsx" + _dtype_nelements = {torch.float: 8, torch.bfloat16: 16, torch.float16: 16} + + def __str__(self) -> str: + return "vsx" + + __hash__: Callable[[VecISA], Any] = VecISA.__hash__ # type: ignore[assignment] + + +class InvalidVecISA(VecISA): + _bit_width = 0 + _macro = [""] + _arch_flags = "" + _dtype_nelements = {} + + def __str__(self) -> str: + return "INVALID_VEC_ISA" + + def __bool__(self) -> bool: # type: ignore[override] + return False + + __hash__: Callable[[VecISA], Any] = VecISA.__hash__ # type: ignore[assignment] + + +def x86_isa_checker() -> list[str]: + supported_isa: list[str] = [] + + def _check_and_append_supported_isa( + dest: list[str], isa_supported: bool, isa_name: str + ) -> None: + if isa_supported: + dest.append(isa_name) + + Arch = platform.machine() + """ + Arch value is x86_64 on Linux, and the value is AMD64 on Windows. + """ + if Arch != "x86_64" and Arch != "AMD64": + return supported_isa + + avx2 = torch.cpu._is_avx2_supported() + avx512 = torch.cpu._is_avx512_supported() + avx512_vnni = avx512 and torch.cpu._is_vnni_supported() + amx_tile = torch.cpu._is_amx_tile_supported() + + _check_and_append_supported_isa(supported_isa, avx2, "avx2") + _check_and_append_supported_isa(supported_isa, avx512, "avx512") + _check_and_append_supported_isa(supported_isa, avx512_vnni, "avx512_vnni") + _check_and_append_supported_isa(supported_isa, amx_tile, "amx_tile") + + return supported_isa + + +invalid_vec_isa = InvalidVecISA() +supported_vec_isa_list = [ + VecAMX(), + VecAVX512VNNI(), + VecAVX512(), + VecAVX2(), + VecNEON(), + VecSVE256(), +] + + +def get_isa_from_cpu_capability( + capability: str | None, + vec_isa_list: list[VecISA], + invalid_vec_isa: InvalidVecISA, +): + # AMX setting is not supported in eager + # VecAMX will be prioritized for selection when setting ATEN_CPU_CAPABILITY to avx512 + # TODO add sve256 support + capability_to_isa_str = { + "default": "INVALID_VEC_ISA", + "zvector": "zvector", + "vsx": "vsx", + "avx2": "avx2", + "avx512": "avx512", + } + if capability in capability_to_isa_str: + # pyrefly: ignore [bad-index, index-error] + isa_str = capability_to_isa_str[capability] + if isa_str == "INVALID_VEC_ISA": + return invalid_vec_isa + for vec_isa in vec_isa_list: + if isa_str in str(vec_isa): + return vec_isa + + if capability: + warnings.warn(f"ignoring invalid value for ATEN_CPU_CAPABILITY {capability}") + + return vec_isa_list[0] + + +# Cache the cpuinfo to avoid I/O overhead. Meanwhile, the cpuinfo content +# might have too much redundant content that is useless for ISA check. Hence, +# we only cache some key isa information. +@functools.cache +def valid_vec_isa_list() -> list[VecISA]: + isa_list: list[VecISA] = [] + if sys.platform == "darwin" and platform.processor() == "arm": + isa_list.append(VecNEON()) + + if sys.platform not in ["linux", "win32"]: + return isa_list + + arch = platform.machine() + if arch == "s390x": + with open("/proc/cpuinfo") as _cpu_info: + while True: + line = _cpu_info.readline() + if not line: + break + # process line + featuresmatch = re.match(r"^features\s*:\s*(.*)$", line) + if featuresmatch: + for group in featuresmatch.groups(): + if re.search(r"[\^ ]+vxe[\$ ]+", group): + isa_list.append(VecZVECTOR()) + break + elif arch == "ppc64le": + isa_list.append(VecVSX()) + elif arch == "aarch64": + if torch.backends.cpu.get_cpu_capability() == "SVE256": + isa_list.append(VecSVE256()) + else: + isa_list.append(VecNEON()) + + elif arch in ["x86_64", "AMD64"]: + """ + arch value is x86_64 on Linux, and the value is AMD64 on Windows. + """ + _cpu_supported_x86_isa = x86_isa_checker() + isa_list.extend( + isa + for isa in supported_vec_isa_list + if all(flag in _cpu_supported_x86_isa for flag in str(isa).split()) and isa + ) + + return isa_list + + +def pick_vec_isa() -> VecISA: + if config.is_fbcode() and (platform.machine() in ["x86_64", "AMD64"]): + return VecAVX2() + + _valid_vec_isa_list: list[VecISA] = valid_vec_isa_list() + if not _valid_vec_isa_list: + return invalid_vec_isa + + # If the simdlen is None, set simdlen based on the environment ATEN_CPU_CAPABILITY + # to control CPU vec ISA + if config.cpp.simdlen is None: + return get_isa_from_cpu_capability( + os.getenv("ATEN_CPU_CAPABILITY"), _valid_vec_isa_list, invalid_vec_isa + ) + + for isa in _valid_vec_isa_list: + if config.cpp.simdlen == isa.bit_width(): + return isa + + return invalid_vec_isa diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/cudagraph_trees.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/cudagraph_trees.py new file mode 100644 index 0000000000000000000000000000000000000000..d962820cdc9e2058acb4be0e995348c184093df0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/cudagraph_trees.py @@ -0,0 +1,2751 @@ +""" +CUDA graph trees are a safety abstraction over CUDAGraphs, similar to make_graph_callables, +which share the same memory pool. Sharing a memory pool is an extremely +important optimization when chaining multiple CUDA graphs together, as it +prevents you from needing to copy intermediate tensors from one graph to the +next, and reduces overall memory usage by allowing dead memory from the first +pool to be reused in the second. + +The standard graph/make_graph_callables support sharing memory pool, but +with a lot of caveats. CUDA graph trees remove these restrictions: + +* Previously, if you recorded graphs A, B, you had to replay A, B in that + order. With CUDA graph trees, after replaying A, you can change your + mind and record/replay a different graph B'; we will support efficient + execution of both A, B and A, B', using only max(mem(A, B), mem(A, B')). In + other words: we support arbitrary trees of CUDA graph operations, not just + sequences (this is why this feature is called CUDA graph trees.) + +* Previously, if you executed graph A, some non-CUDA graph code, and then + graph B, after executing graph B, it was not safe to retain any references + to intermediates produced by A. With CUDA graph trees, we track if any +outputs of graph A are still live by the time graph B is run, and make + sure graph B doesn't clobber there memory when reusing the CUDA graphs + pool. You'll get a separate recording of B depending on what tensors + stay live or dead. + +CUDA graph trees are flexible enough to be used in Dynamo across graph breaks, +which is their primary use case. + +The ability to switch from replay to record is fairly nontrivial: remember that +when you replay a CUDA graph, you only replay CUDA operations; no CPU side state +is updated. In particular, the CPU-side book-keeping for the allocator is not +reconstructed. However, to record a new child CUDA graph, we must restore this +book-keeping. This is what checkpoint pool state is used for. +""" + +from __future__ import annotations + +import contextlib +import dataclasses +import functools +import gc +import itertools +import logging +import operator +import sys +import threading +import traceback +import warnings +import weakref +from collections import defaultdict +from contextlib import AbstractContextManager +from enum import auto, Enum +from typing import Any, cast, TYPE_CHECKING, TypeVar + +import torch.fx +from torch import Tensor +from torch._dynamo.callback import CallbackTrigger +from torch._dynamo.graph_bytecode_inputs import ( + CURRENT_STREAM_INDEX, + set_external_object_by_index, +) +from torch._dynamo.mutation_guard import GenerationTracker +from torch._dynamo.utils import counters, dynamo_timed, preserve_rng_state +from torch._higher_order_ops.cudagraph_conditional_nodes import ( + ControlFlowOpWarmupDispatchMode, + CUDAGraphCaptureControlFlowOpDispatchMode, +) +from torch._inductor.compile_fx import ( + align_inputs_from_check_idxs, + copy_misaligned_inputs, + get_expanded_dims, + get_input_idxs_to_check, + index_expanded_dims, + remove_unaligned_input_idxs, + static_input, +) +from torch._inductor.cudagraph_utils import ( + check_for_mutation, + CheckInvariantStatus, + collect_cuda_data_ptrs, + FunctionID, + log_cudagraph_skip_and_bump_counter, + log_data_ptr_mismatch, + maybe_warning_due_to_dynamic_shape, + ModelType, + OutputType, + PlaceholderInfo, + WrappedFunction, +) +from torch._library.opaque_object import is_opaque_value +from torch._opaque_base import OpaqueBase +from torch.multiprocessing.reductions import StorageWeakRef +from torch.storage import UntypedStorage +from torch.utils import _pytree as pytree +from torch.utils._ordered_set import OrderedSet +from torch.utils.weak import TensorWeakRef + + +if TYPE_CHECKING: + from collections.abc import Callable, Generator, Iterator, Sequence + + from torch._guards import CompileId + from torch._inductor.utils import InputType + from torch.cuda import _POOL_HANDLE + from torch.types import _bool + +StorageWeakRefPointer = int +StorageDataPtr = int +NBytes = int +S = TypeVar("S", bound="StorageWeakRefWrapper") + + +if torch.backends.cuda.is_built(): + from torch._C import ( + _cuda_CUDAAllocator_AllocatorState as AllocatorState, + _set_cached_tensors_enabled, + ) +else: + + class AllocatorState: # type: ignore[no-redef] + pass + + def _set_cached_tensors_enabled(enabled: _bool) -> None: + pass + + +log = torch._logging.getArtifactLogger(__name__, "cudagraphs") + + +def format_inputs_log(inputs: list[Any]) -> str: + def format_item(i: int, inp: Any) -> str: + if isinstance(inp, torch.Tensor): + return ( + f"[{i}]: Tensor(size={list(inp.size())}, stride={inp.stride()}, " + f"dtype={inp.dtype}, data_ptr=0x{inp.data_ptr():X})" + ) + elif inp is None: + return f"[{i}]: None" + else: + return f"[{i}]: {type(inp).__name__}({inp})" + + n = len(inputs) + if n == 0: + return "[]" + + # Only display the first 10 inputs if the input list is too long + max_items = 10 + parts = [format_item(i, inp) for i, inp in enumerate(inputs[:max_items])] + if n > max_items: + parts.append(f"... ({n - max_items} more items)") + + return ", ".join(parts) + + +from . import config + + +@dataclasses.dataclass(frozen=True) +class GraphID: + "Unique counter of a cuda graph recording" + + id: int + + +def clear_cublass_cache() -> None: + """ + Cublas keeps a persistent workspace allocation for running matmuls. This poses a problem for + doing warmup within a CUDAGraph private pool because we do not want persistent allocations from + one one run to the next. When we begin a new run of a cudagraphs path (generation), all tensors + from the previous generation are freed. This frees them the memory pool, but not elsewhere. + A tensor in the cublas workspace would continue to be in use the workspace but would also get allocated + in the next run. The memory would be in use in two places. + + To solve this, we clear cublas caches before and after warming up or recording. If a workspace is required + it will be allocated to the cudagraph private pool and accounted for in the allocator for the duration of the + program. There is no overhead to this on replay since cudagraphs removes allocation overhead. + """ + torch._C._cuda_clearCublasWorkspaces() + + +@contextlib.contextmanager +def clear_cublas_manager() -> Generator[None, None, None]: + "Context manager around clearing cublas caches that will clear on enter and exit" + clear_cublass_cache() + try: + yield + finally: + clear_cublass_cache() + + +@contextlib.contextmanager +def disable_conv_cache_emptying() -> Generator[None, None, None]: + prev = torch._C._cuda_get_conv_benchmark_empty_cache() + torch._C._cudnn_set_conv_benchmark_empty_cache(False) + try: + yield + finally: + torch._C._cudnn_set_conv_benchmark_empty_cache(prev) + + +@contextlib.contextmanager +def enable_history_recording() -> Generator[None, None, None]: + "Turns on history recording in the CUDA Caching Allocator" + enabled = torch._C._cuda_isHistoryEnabled() + try: + if not enabled: + torch.cuda.memory._record_memory_history() + yield + finally: + if not enabled: + torch.cuda.memory._record_memory_history(None) + + +def get_history_recording() -> AbstractContextManager[None]: + # TODO - remove, prevents cleanup + if not config.triton.cudagraph_trees_history_recording: + return contextlib.nullcontext() + return enable_history_recording() + + +class TreeManagerContainer: + """ + Manages the lifetime of the tree manager. Like `PrivatePool` in cuda caching allocator, + the tree and its corresponding memory pool should be kept alive as long as any outstanding + graph or tensor which is an output of a graph remains alive. + + There is a single tree manager container per device. + + The lifecycle of a tree_manager is: + - Is constructed, no graph, no fns, no tensors + - Tree manager is fetched, resulting in tree manager being allocated + - We generate a bunch of functions, calling add_strong_reference + - These functions die, calling finalize_reference + - When all the functions die, we finalize_tree_manager. + + TODO: in the future, we would like to do the following once storage weak refs land + - We look for all the live storages and add references to THOSE + - We count as storages die + - All the storages are dead, we deallocate the tree manager + """ + + def __init__(self, device_index: int) -> None: + # This class keeps a strong reference to tree_manager, + # but upon all other strong references to the tree_manager will reset it to None. + # We need a strong reference so that we can still access its attributes upon cleanup. + self.tree_manager: CUDAGraphTreeManager | None = None + + # Number of outstanding references to the current tree manager + self.live_cudagraphify_fns = 0 + + self.device_index = device_index + + # Following two objects are only set in the case that Tensor outputs outlive + # the cudagraphify_fns. Reference to the Graph is needed to keep the private pool from + # deallocation. + self.live_storages_count = 0 + self.graph: torch.cuda.CUDAGraph | None = None + + self.lock = threading.Lock() + + def _finalize_tensor(self) -> None: + with self.lock: + self.live_storages_count -= 1 + if self.live_storages_count == 0: + self.graph = None + + # manager was used again after existing cleanup, + # we shouldn't set it to None + if self.live_cudagraphify_fns == 0: + self.tree_manager = None + + def finalize_cudagraphify_fn(self) -> None: + with self.lock: + self.live_cudagraphify_fns -= 1 + if self.live_cudagraphify_fns == 0: + self._finalize_tree_manager() + + def _finalize_tree_manager(self) -> None: + assert self.lock.locked() + self.tree_manager = None + + # TODO - when issue #91395 is landed, we can set a weakref on + # storages and trigger a deallocation when all outputs of the + # cudagraph are dead. + + # live_storages = list( + # tree_manager.live_cudagraph_pool_storages_in_curr_execution() + # ) + + # # Maintain reference to graph to keep tensors alive + # assert len(tree_manager.roots) > 0, "expected at least one use" + # root = next(tree_manager.get_roots()) + # self.graph = root.graph + # seen_storages = set() + # for stor in live_storages: + # if stor in seen_storages: + # continue + # seen_storages.add(stor) + # self.live_storages_count += 1 + # . weakref.finalize(stor, self._finalize_tensor) + + def add_strong_reference(self, fn: Callable[..., Any]) -> None: + with self.lock: + self.live_cudagraphify_fns += 1 + + weakref.finalize(fn, self.finalize_cudagraphify_fn) + + def get_tree_manager(self) -> CUDAGraphTreeManager: + with self.lock: + if self.tree_manager is None: + self.tree_manager = CUDAGraphTreeManager(self.device_index) + return self.tree_manager + + +local = threading.local() + +# one tree manager per device +local.tree_manager_containers = {} +local.tree_manager_locks = defaultdict(threading.Lock) + + +# only incremented by user call of mark_step_begin +class MarkStepBox: + mark_step_counter = 0 + + +# We need to register this as an object that will be copied over as TLS when new +# threads are created in autograd +torch._C._stash_obj_in_tls("tree_manager_containers", local.tree_manager_containers) +torch._C._stash_obj_in_tls("tree_manager_locks", local.tree_manager_locks) + + +def mark_step_begin() -> None: + "Indicates that a new iteration of inference or training is about to begin." + + # iterate down to distinguish from GenerationTracking counter + MarkStepBox.mark_step_counter -= 1 + + +def reset_cudagraph_trees() -> None: + "Clear all cudagraph trees" + # see shutdown below for why this is necessary + # Note: get_obj may fail if called from a thread that wasn't spawned by + # autograd (e.g., test threads). In that case, there's nothing to reset. + if not torch._C._is_key_in_tls("tree_manager_containers"): + # TLS not set up for this thread, nothing to reset + return + container_dict = get_obj(local, "tree_manager_containers") + locks_dict = get_obj(local, "tree_manager_locks") + for device, lock in locks_dict.items(): + with lock: + container = container_dict.get(device) + if not container or not container.tree_manager: + continue + + container.tree_manager.shutdown() + + _set_cached_tensors_enabled(False) + container_dict.clear() + + MarkStepBox.mark_step_counter = 0 + + +def get_obj(local: Any, attr_name: str) -> Any: + if hasattr(local, attr_name): + return getattr(local, attr_name) + else: + assert torch._C._is_key_in_tls(attr_name) + return torch._C._get_obj_in_tls(attr_name) + + +def get_container(device_index: int) -> TreeManagerContainer: + container_dict = get_obj(local, "tree_manager_containers") + lock = get_obj(local, "tree_manager_locks")[device_index] + + with lock: + if device_index not in container_dict: + container_dict[device_index] = TreeManagerContainer(device_index) + + return container_dict[device_index] + + +def get_manager( + device_index: int, create_if_none_exists: bool = True +) -> CUDAGraphTreeManager | None: + if create_if_none_exists: + return get_container(device_index).get_tree_manager() + return get_container(device_index).tree_manager + + +def is_cudagraph_capture_sizes(int_key: int | tuple[int, ...]) -> bool: + """ + Returns true if all dynamic shapes should be captured or the dynamic shape + int_key should be captured. + """ + return ( + config.triton.cudagraph_capture_sizes is None + or int_key in config.triton.cudagraph_capture_sizes + ) + + +def cudagraphify_impl( + model: ModelType, + inputs: list[InputType], + static_input_idxs: Sequence[int], + *args: Any, + **kwargs: Any, +) -> ModelType: + fn_cache: dict[tuple[int, ...], Callable[..., Any]] = {} + + # Detect int inputs: we need to index on these + int_key = [i for i, v in enumerate(inputs) if isinstance(v, int)] + get_ints: Any = operator.itemgetter(*int_key) if int_key else lambda _: None + + has_warn = False + + del inputs + + def deferred_cudagraphify(inputs: list[InputType]) -> OutputType: + nonlocal has_warn + + int_key = get_ints(inputs) + + if not is_cudagraph_capture_sizes(int_key): + return model(inputs) + + fn = fn_cache.get(int_key) + if fn is not None: + return fn(inputs) + + if int_key is None: + log.info("Recording cudagraph tree for graph without symints") + else: + log.info("Recording cudagraph tree for symint key %s", int_key) + + if not has_warn: + has_warn = maybe_warning_due_to_dynamic_shape(fn_cache, int_key) + + # first get indices we need to check to align, then update our static inputs, + # and finally copy + check_input_idxs = get_input_idxs_to_check(inputs, static_input_idxs) + new_static_input_idxs = remove_unaligned_input_idxs(inputs, static_input_idxs) + copy_misaligned_inputs(inputs, check_input_idxs) + + fn, out = cudagraphify(model, inputs, new_static_input_idxs, *args, **kwargs) + # cudagraph will already clones input locally, no need to copy back + mutated_input_idxs: OrderedSet[int] = OrderedSet() + fn = align_inputs_from_check_idxs( + fn, inputs_to_check=check_input_idxs, mutated_input_idxs=mutated_input_idxs + ) + + fn_cache[int_key] = fn + + return out + + return deferred_cudagraphify + + +@contextlib.contextmanager +def dynamo_timed_cudagraph( + name: str, + compile_id: CompileId | None, + mode: CompilationMode | None, +) -> Generator[Any, None, None]: + """ + Makes usages of dynamo_timed in this file less verbose. NOTE: This CM sums + all durations into a single column in the dynamo_compile table. Use only if + you consider the timed region to be part of the runtime overhead associated + with the compiler. + """ + with dynamo_timed( + name, + log_pt2_compile_event=True, + compile_id=compile_id, + is_backward=mode == CompilationMode.BACKWARD, + dynamo_compile_column_us="runtime_cudagraphify_time_us", + ): + yield + + +def cudagraphify( + model: ModelType, + inputs: list[InputType], + static_input_idxs: Sequence[int] = (), + *, + device_index: int, + is_backward: bool, + is_inference: bool, + stack_traces: StackTraces | None = None, + constants: tuple[torch.Tensor, ...] = (), + placeholders: tuple[PlaceholderInfo, ...] = (), + mutated_input_idxs: tuple[int, ...] = (), + compile_id: CompileId | None = None, +) -> tuple[ModelType, OutputType]: + assert not (is_backward and is_inference) + mode = ( + CompilationMode.BACKWARD + if is_backward + else (CompilationMode.INFERENCE if is_inference else CompilationMode.FORWARD) + ) + + with dynamo_timed_cudagraph("cudagraphify.get_container", compile_id, mode): + manager = get_container(device_index).get_tree_manager() + + return manager.add_function( + model, + inputs, + static_input_idxs, + stack_traces, + mode, + constants, + placeholders, + mutated_input_idxs, + compile_id, + ) + + +class StorageWeakRefWrapper: + """ + Wrapper around a storage weak ref. Will deallocate it upon expiration if invoked. + """ + + __slots__ = ["ref", "_data_ptr", "extra_ref_check"] + + storage_ref: StorageWeakRef | None + + def __init__( + self, + inp: Tensor | UntypedStorage, + extra_ref_check: Callable[[], bool] | None = None, + ) -> None: + """ + extra_ref_check is an additional check we need to run to check if the + weak ref has expired. in checking storage use count we assume extra_ref_check + will hold an additional reference to the storage. + """ + if isinstance(inp, Tensor): + stor = inp.untyped_storage() + else: + assert isinstance(inp, UntypedStorage) + stor = inp + self.ref = StorageWeakRef(stor) + self._data_ptr = stor.data_ptr() + self.extra_ref_check = extra_ref_check + + @classmethod + def from_weakref_and_data_ptr( + cls: type[StorageWeakRefWrapper], + cdata: Any, + data_ptr: int, + extra_ref_check: Callable[[], bool] | None = None, + ) -> StorageWeakRefWrapper: + instance = cls.__new__(cls) + instance._data_ptr = data_ptr + instance.ref = StorageWeakRef.from_weakref(cdata) + instance.extra_ref_check = extra_ref_check + return instance + + def __call__(self) -> StorageWeakRefPointer | None: + if self.expired(): + return None + + return self.ref.cdata + + def swap_weakref(self, cdata: Any) -> None: + self.ref.__del__() + self.ref.cdata = cdata + + def data_ptr(self) -> int: + "NB: returns the data ptr even if the storage has expired" + return self._data_ptr + + def remove_extra_reference(self) -> None: + self.extra_ref_check = None + + def expired(self) -> bool: + if self.extra_ref_check is not None and not self.extra_ref_check(): + return False + + stor_count = torch._C._storage_Use_Count(self.ref.cdata) + if self.extra_ref_check is not None: + # if extra_ref_check is not None we expect two additional references: + # - one from the Python storage object + # - one from the cached Tensor + stor_count -= 2 + assert stor_count >= 0 + return stor_count == 0 + + def __repr__(self) -> str: + if self.ref is None or self.ref.expired(): + return f"StorageWeakRefWrapper to {self.data_ptr()}; dead" + else: + return f"StorageWeakRefWrapper to {self.data_ptr()}; alive" + + +def is_live(weak_ref: StorageWeakRefWrapper | None) -> bool: + return maybe_deref(weak_ref) is not None + + +def maybe_deref( + weak_ref: StorageWeakRefWrapper | None, +) -> tuple[StorageWeakRefPointer, int] | None: + if weak_ref is None: + return None + r = weak_ref() + if r is None: + return None + # NB: r.data_ptr() does not necessarily equal weak_ref.data_ptr() + return r, weak_ref.data_ptr() + + +@contextlib.contextmanager +def _use_cuda_memory_pool_manager( + device: int, mem_pool: tuple[int, int], stream: torch.cuda.Stream +) -> Generator[None, None, None]: + """ + Context manager to use cuda graph pool for new allocations. If you use this manager + all cudagraph tensors in use should be reflected in the allocator or they will be overwritten. + existing_graph should already have been used in a capture, and the mem_pool must already exist, + because this manager will not preserve a reference to the pool which keeps it alive. + """ + torch.cuda.synchronize() + stream.wait_stream(torch.cuda.current_stream()) + + with torch.cuda.stream(stream), torch.device(device): + # Begin allocate to mem pool for all memory allocation on the current thread. + # This is thread safe since a thread can only warmup or record 1 cudagraph + # at the same time. + torch._C._cuda_beginAllocateCurrentThreadToPool(device, mem_pool) + try: + yield + finally: + torch._C._cuda_endAllocateToPool(device, mem_pool) + torch._C._cuda_releasePool(device, mem_pool) + + torch.cuda.current_stream().wait_stream(stream) + + +@contextlib.contextmanager +def _update_current_stream_external_object() -> Generator[None, None, None]: + """Update the external object registry so custom ops see the capture stream. + + During cudagraph recording/warmup the current stream differs from the + trace-time default stream. The external object at CURRENT_STREAM_INDEX + must reflect the actual current stream so that custom ops (e.g. event + record/wait) executed during capture use the right stream. + """ + set_external_object_by_index(CURRENT_STREAM_INDEX, torch.cuda.current_stream()) + yield + + +def map_to_ref(t: Tensor | None) -> StorageWeakRefWrapper | None: + if not isinstance(t, torch.Tensor): + assert t is None + return None + return StorageWeakRefWrapper(t) + + +# A path index of (depth, offset) indices into a graph that is `depth`` number of nodes from the root +# at graph output offset +PathOutputIndex = tuple[int, int] + +# For each node in the path, for each output, is the output alive +PathLiveness = list[list[bool]] + +StackTraces = list[str | None] + + +class CUDAWarmupNode: + """ + Simplified Wrapper around A CUDA Model that wraps outputs in storage refs and exposes + apis to get the live storages in the current chain of warmup. + + A CUDAWarmupNode may have either CUDAGraphNode or CUDAWarmupNode as a parent, but may only have + CUDAWarmupNode as children, because we cannot record or execute with tensors which do not have stable + memory addresses. + + CUDAWarmupNode and CUDAGraphNode have a number of differences that make it easier to use separate classes. + - Much of the CUDAGraphNode logic & initialization is based on the tensor properties of first recording. In the + first instance of warmup, these are not finalized yet. + - All Inputs to the RecordedFunction must be copied over to the cuda graph memory pool, this is unnecessary in warmup. + - CUDAWarmup is only used once and so does not need to optimize as much bookkeeping. It is much simpler. + + NB: this class and CUDAGraphNode need to expose `path_live_weakrefs`, `all_outputs_are_dead`, and + `self.outputs_weakrefs`, `stack_traces`, and `tensor_weakrefs` for compatibility. + """ + + def __init__( + self, + wrapped_function: WrappedFunction, + parent: CUDAGraphNode | CUDAWarmupNode | None, + cuda_graphs_pool: tuple[int, int], + existing_cuda_graph: torch.cuda.CUDAGraph | None, + device_index: int, + stack_traces: StackTraces | None, + stream: torch.cuda.Stream, + already_warm: bool, + id: GraphID, + ) -> None: + self.wrapped_function = wrapped_function + self.parent: CUDAGraphNode | CUDAWarmupNode | None = parent + self.cuda_graphs_pool = cuda_graphs_pool + self.outputs_weakrefs: list[StorageWeakRefWrapper | None] = [] + self.tensor_weakrefs: list[TensorWeakRef | None] = [] + self.existing_cuda_graph = existing_cuda_graph + self.has_run = False + self.device_index = device_index + self.stack_traces = stack_traces + self.stream = stream + self.already_warm = already_warm + self.id = id + + def run(self, new_inputs: Any) -> OutputType: + assert not self.has_run, "Wrapped function should never be run twice" + + # See: output_is_alias_of_persistent_static_inputs below. We should only be returning freshly created + # storages in path_live_weakrefs. + existing_path_data_ptrs = OrderedSet( + [t.data_ptr() for t in self.path_live_weakrefs() if t()] + ) + + def get_non_cudagraph_inps() -> list[weakref.ReferenceType[UntypedStorage]]: + non_cudagraph_inps = [ + weakref.ref(t.untyped_storage()) + for t in itertools.chain(new_inputs, self.wrapped_function.constants) + if isinstance(t, torch.Tensor) + and t.untyped_storage().data_ptr() not in existing_path_data_ptrs + ] + return non_cudagraph_inps + + non_cudagraph_inps_storages = get_non_cudagraph_inps() + + if config.triton.slow_path_cudagraph_asserts and not self.already_warm: + refs = list(self.path_live_weakrefs()) + check_memory_pool(self.device_index, self.cuda_graphs_pool, refs) + + with ( + torch.cuda.device(self.device_index), + disable_conv_cache_emptying(), + clear_cublas_manager(), + _use_cuda_memory_pool_manager( + self.device_index, self.cuda_graphs_pool, self.stream + ), + # NB: must go after _use_cuda_memory_pool_manager which switches the stream + _update_current_stream_external_object(), + ControlFlowOpWarmupDispatchMode(), + get_history_recording(), + ): + out = self.wrapped_function.model(new_inputs) + + # We need to know which outputs are allocated within the cudagraph pool + # so that we can deallocate them at the beginning of the next cudagraph step, + # and set their access to error. + # We use a weakref to the inputs storage, in case a block which was previously + # allocated to the general caching allocator pool gets reallocated to a private pool. + + non_cudagraph_inps_storage_ptrs = OrderedSet[Any]() + for storage in non_cudagraph_inps_storages: + s = storage() + if s is not None: + non_cudagraph_inps_storage_ptrs.add(s._cdata) + + assert len(new_inputs) == 0 + + # sdpa returns cpu tensors when not recording cuda graph + def add_ref(o: Any) -> bool: + return ( + isinstance(o, torch.Tensor) + and o.is_cuda + and o.untyped_storage()._cdata not in non_cudagraph_inps_storage_ptrs + and o.untyped_storage().data_ptr() != 0 + ) + + self.outputs_weakrefs.extend( + [map_to_ref(o) if add_ref(o) else None for o in out] + ) + self.tensor_weakrefs.extend( + [TensorWeakRef(o) if add_ref(o) else None for o in out] + ) + + if config.triton.slow_path_cudagraph_asserts and not self.already_warm: + out_refs = list(self.path_live_weakrefs()) + check_memory_pool(self.device_index, self.cuda_graphs_pool, out_refs) + + return out + + @property + def _path_from_root( + self, + ) -> Generator[CUDAGraphNode | CUDAWarmupNode, None, None]: + nodes = [] + node: CUDAGraphNode | CUDAWarmupNode = self + while node: + nodes.append(node) + node = node.parent # type: ignore[assignment] + + yield from reversed(nodes) + + def path_live_weakrefs(self) -> Iterator[StorageWeakRefWrapper]: + "Returns all live storages weakrefs that created by nodes in this path" + for node in self._path_from_root: + for output in node.outputs_weakrefs: + if is_live(output): + yield output # type: ignore[misc] + + def all_outputs_are_dead(self) -> bool: + return not list(self.path_live_weakrefs()) + + def _is_cuda_graph_recorded_tensor(self, t: torch.Tensor) -> bool: + for storage_weak_ref in self.path_live_weakrefs(): + if t.untyped_storage().data_ptr() == storage_weak_ref.data_ptr(): + return True + return False + + +# Aliases for List that say what the indices denote +InputList = list # input indexes +OutputList = list # output indexes +LevelList = list # levels (distance from root of tree) + + +class OutputAliasInfo: + __slots__ = [] + + +class _UnaliasedStorage(OutputAliasInfo): + "Singleton to mark that the graph output constructs a new alias or is None" + + +UnaliasedStorage = _UnaliasedStorage() + + +class AliasesPriorGraphOutput(OutputAliasInfo): + "Marks that the graph output aliases an output of a prior graph" + + __slots__ = ["index"] + + index: PathOutputIndex + + def __init__(self, index: PathOutputIndex) -> None: + assert isinstance(index, tuple) + self.index = index + + +class AliasesNewOutput(OutputAliasInfo): + "Marks that the graph output aliases an index in the new, returned outputs" + + __slots__ = ["index"] + + index: int + + def __init__(self, index: int) -> None: + assert isinstance(index, int) + self.index = index + + +class CUDAGraphNode: + """ + A single recording of a function into a CUDA Graph. Recordings of CUDA Graphs share a single memory pool + and are structured into a tree, where there is a single recording that can precede it (parent) and multiple + subsequent recordings that may follow (children). A node will have no parent if it is the first recording + in a tree; i.e., when it is first recorded, there are no live tensors from a previous recording which + would force a dependency. + + On first recording, all of the live tensors in the current CUDA Graph Node path will be + reflected in the corresponding private pool. On subsequent executions, the caching allocator + is unaffected when the graph is replayed. + + In order to support recording a subsequent cuda graph recording after execution of this graph, + we checkpoint the state of the memory pool so that it may later be resumed. + + WrappedFunction should have already been warmed up prior to invocation. + + See [setCheckpointPoolState] for further explanation, as well as + https://user-images.githubusercontent.com/13564/222815509-374f3400-f83d-4f7d-8fa6-4a092b3250bb.png + """ + + def __init__( + self, + wrapped_function: WrappedFunction, + id: GraphID, + parent: CUDAGraphNode | None, + inputs: list[InputType], + cuda_graphs_pool: _POOL_HANDLE, + device_index: int, + stack_traces: StackTraces | None, + stream: torch.cuda.Stream, + mode: CompilationMode | None, + compile_id: CompileId | None, + ) -> None: + assert isinstance(inputs, (list, tuple)) + + self.wrapped_function = wrapped_function + self.id = id + self.device = device_index + self.stack_traces = stack_traces + self.stream = stream + + # Enable re-record a cudagraph when static tensor address changed. + # if not we should error when it changed. + self.rerecord_if_static_inputs_change = True + + # if this is a root parent will be None. use weakref to prevent reference cycle + self._parent = weakref.ref(parent) if parent is not None else None + # reference to the shared memory pool for the entire cuda graphs tree + self.cuda_graphs_pool = cuda_graphs_pool + + # A single wrapped function may be recorded multiple times if memory patterns or + # invariants change from one execution to the next + self.children: dict[FunctionID, list[CUDAGraphNode]] = defaultdict(list) + + # StorageWeakRef maintains whether the Storage C++ object remains allocated, + # not whether the corresponding memory has been deallocated. In order + # to use them to track memory deallocations we must maintain a single StorageWeakRef + # for all Storages that reference that memory (even if we are constructing Storages + # that do not have a deallocator function). We maintain one single storage_cache + # as we execute any tree path. When we retrieve a storage from the cache we + # check that it is still alive, and we hash based on observed recording data ptr + # and storage cdata. + + # we preserve a single reference to executed outputs that is then referenced + # in children to avoid children having to chase parent pointers in the hot path + # DO NOT reassign output_weakrefs, only call `clear()` + # Path is a series of nodes from root to the current node + self.outputs_weakrefs: OutputList[StorageWeakRefWrapper | None] = [] + self.path_weakrefs: LevelList[OutputList[StorageWeakRefWrapper | None]] = [ + node.outputs_weakrefs for node in self._path_from_root + ] + self.path_stacktraces: LevelList[StackTraces | None] = [ + node.stack_traces for node in self._path_from_root + ] + self.tensor_weakrefs: OutputList[TensorWeakRef | None] = [] + + # tensors which are outputs of previous graphs in the tree + self.cudagraph_managed_idxs: list[int] = [ + idx + for idx, t in enumerate(inputs) + if isinstance(t, torch.Tensor) and self._is_cuda_graph_recorded_tensor(t) + ] + + # (depth, offset) of live tensors which are alias of previous graph outputs + self.live_cudagraph_managed_path_refs: InputList[PathOutputIndex | None] = [ + ( + self._is_alias_of_live_recorded_tensor(t) + if isinstance(t, torch.Tensor) + else None + ) + for t in inputs + ] + + # when replay, preserve the liveness of an input if it AliasesPriorGraphOutput + # and also aliases an output of the current CUDAGraphNode + self.preserved_aliased_inputs: InputList[bool] = [False] * len(inputs) + + # Opaque values (e.g. DeviceMesh, ProcessGroup) are non-tensor + # inputs that cannot be copied like tensors. We include them in + # static_input_idxs to keep them out of non_static_input_idx + # (which drives the tensor-copy path during replay). "Static" + # here just means "don't try to copy this as a tensor" — it + # does NOT mean the object is semantically immutable. + # + # Opaque indices must also be excluded from any list passed to + # _tensors_data_ptrs_at_indices_equal (the C++ data-pointer + # stability check), because opaque objects have no data_ptr. + # That is why tensor_static_input_idxs and + # non_managed_static_input_idxs filter them out below. + opaque_input_idxs = OrderedSet( + i for i, inp in enumerate(inputs) if is_opaque_value(inp) + ) + static_input_idxs = OrderedSet(wrapped_function.static_input_idxs) + cudagraph_managed_idxs = OrderedSet(self.cudagraph_managed_idxs) + + self.static_input_idxs: list[int] = list( + static_input_idxs | cudagraph_managed_idxs | opaque_input_idxs + ) + + self.non_static_input_idx: LevelList[int] = [ + i for i in range(len(inputs)) if i not in self.static_input_idxs + ] + + counters["inductor"]["cudagraph_recorded_non_static_inputs"] += len( + self.non_static_input_idx + ) + + self.non_managed_static_input_idxs: LevelList[int] = LevelList( + static_input_idxs - cudagraph_managed_idxs - opaque_input_idxs + ) + + self.tensor_static_input_idxs: list[int] = list( + static_input_idxs | cudagraph_managed_idxs + ) + + def maybe_get_static_data_ptr( + idx: int, + inputs: list[InputType], + static_input_idxs: list[int], + ) -> int | None: + inp = inputs[idx] + if isinstance(inp, torch.Tensor) and idx in static_input_idxs: + return inp.data_ptr() + return None + + self.static_input_data_ptrs: InputList[int | None] = [ + maybe_get_static_data_ptr(i, inputs, self.static_input_idxs) + for i in range(len(inputs)) + ] + + # When we checkpoint, and free generations, we will be manually freeing the outputs + # of CUDAGraphNodes. We should not be freeing parameters, not do we need to account for + # their liveness (they are static), so we need to compute which outputs are aliases of + # parameters. Some static inputs are saved tensors from the forward that die in the backward. + # Their locations are static but lifetimes are not. We only include the persistent static + # data ptrs below because the non persistent data ptrs may be outputs of this record and + # fresh allocations. + + # precompute expanded dims to avoid computing in the hot path + self.expanded_dims: list[list[int]] = [ + get_expanded_dims(x) + if isinstance(x, torch.Tensor) and idx not in self.static_input_idxs + else [] + for idx, x in enumerate(inputs) + ] + + # For each node in path, which outputs were observed to be live + # before invoking graph recording, and after graph recording + self.recorded_liveness_before_graph: LevelList[OutputList[bool]] = [] + self.recorded_liveness_after_graph: LevelList[OutputList[bool]] = [] + + # List of tuples of (depth, output_index) that index into node at depth + # number of nodes from root and output_index of outputs. Will index into + # path_weakrefs. + self.expected_dead_indices_before_graph: list[PathOutputIndex] = [] + self.expected_dead_indices_after_graph: list[PathOutputIndex] = [] + + # all live indices after graph recording + self.live_indices_after_graph: list[PathOutputIndex] = [] + + if self.parent is not None: + previous_liveness = self.parent.recorded_liveness_after_graph + curr_liveness = self._get_liveness(self.path_weakrefs) + + different_indices = self._get_different_indices( + previous_liveness, curr_liveness + ) + + self.recorded_liveness_before_graph = curr_liveness + self.expected_dead_indices_before_graph = different_indices + + rng_states = [inp for inp in inputs if isinstance(inp, torch.Generator)] + + recording_inputs = self._allocate_and_copy_recording_inputs(inputs) + # recording inputs will copy over memory, so we can free non recording inputs + + inputs.clear() + del inputs + + # graph used for recording model invocation + self.graph: torch.cuda.CUDAGraph | None = torch.cuda.CUDAGraph() + + # TODO: register_generator_state should potentially take explicit device + with torch.cuda.device(self.device): + for rng_state in rng_states: + self.graph.register_generator_state(rng_state) + + # we allocate non-static inputs within the same memory pool as the CUDAGraph + # which we will record the model with. For memory efficiency, it is important + # to reclaim the input memory when the inputs are no longer live. To accomplish this, + # we reconstruct tensors at the correct data pointers of our inputs which are + # non owning and do not prevent deallocation. On subsequent executions, input values + # will be copied over to these tensors. + self.reconstructed_inputs: list[InputType] = [ + self._reconstruct_from_tensor_metadata(self._tensor_metadata(x)) + if isinstance(x, torch.Tensor) + else x + for x in recording_inputs + ] + + # DO THE RECORDING!!! + # We record the CUDA graph in the constructor of CUDAGraphNode, which + # gives you what the CPU side compute of the function would do. We + # don't throw the recording outputs away: their memory is + # correctly accounted for in the CUDAGraphs caching allocator. This + # means on the very FIRST run of the CUDA graph node, we can directly + # do more recording, because we have a valid caching allocator state. + # NB: This relies on run() being called immediately after the + # constructor, otherwise this optimization would not be valid. + + # initialized below in _record + + self.checkpointed_caching_state: AllocatorState | None = None + + # Output Storage Alias information, can be: + # - A new, unaliased storage, or the output is None + # - An alias of an output of a prior graph + # - An alias of an output already created in the reconstructed outputs + # This is None if the output in question is an int + self.output_storage_alias: OutputList[OutputAliasInfo | None] = [] + + # is the output Storage unaliased in subsequent outputs, of all subsequent paths + # if it is, we cached the output tensor and adjust storage liveness tracking to also + # check if the output tensor does not have an additional python reference. + # If a descendent node discovers it has an alias of a prior output, then the output + # will no longer be cached in the ancestor. + # The large majority of tensors are unaliased, and preserving aliased output tensors would add + # significant additional complexity with marginal gains + # The cached tensor outputs are added on the first execution, and cleared whenever we need + # to do subsequent recording + self.unaliased_in_all_paths: OutputList[bool] = [] + self.cached_tensor_outputs: OutputList[Tensor | None] = [] + + # if an output aliases a static, persistent input then the corresponding Tensor will + # be set here. These are different than cached tensors, because they are tensors that + # are aliases of parameters that are always live. + self.static_output_tensors: OutputList[Tensor | None] = [] + + # Cleared after recording + with dynamo_timed_cudagraph("CUDAGraphNode.record", compile_id, mode): + self.recording_outputs: OutputType | None = self._record( + wrapped_function.model, recording_inputs + ) + self.outputs_metadata: OutputList[dict[str, Any] | int | None] = [] + + # As with inputs, we do not want to keep the outputs permanently alive because that would prevent + # their memory being reclaimed in subsequent cuda graph recordings. We record the tensor metadata + # needed to reconstruct instead. + assert self.recording_outputs is not None + for out in self.recording_outputs: + if isinstance(out, torch.Tensor): + self.outputs_metadata.append( + self._tensor_metadata(out, ignore_storage_offset=False) + ) + else: + assert isinstance(out, (int, type(None))), type(out) + self.outputs_metadata.append(out) + + self.graph.replay() + + def _copy_inputs_and_remove_from_src( + self, dsts: list[InputType], srcs: list[InputType] + ) -> None: + dst_tensors = [] + src_tensors = [] + for idx in self.non_static_input_idx: + if not isinstance(srcs[idx], torch.Tensor): + continue + expanded_dims = self.expanded_dims[idx] + dst_tensors.append(index_expanded_dims(dsts[idx], expanded_dims)) # type: ignore[arg-type] + src_tensors.append(index_expanded_dims(srcs[idx], expanded_dims)) # type: ignore[arg-type] + srcs[idx] = None # type: ignore[call-overload] + # Fails on empty lists + if dst_tensors: + torch._foreach_copy_(dst_tensors, src_tensors) + + def check_static_inputs_are_stable(self, new_inputs: list[InputType]) -> None: + # avoid checking managed tensor static points since we already checked those in check_invariants + if ( + not self.rerecord_if_static_inputs_change + and not torch._C._tensors_data_ptrs_at_indices_equal( + new_inputs, # type: ignore[arg-type] + self.static_input_data_ptrs, + self.non_managed_static_input_idxs, + ) + ): + # this should error + error_msg = log_data_ptr_mismatch( + self.wrapped_function.placeholders, + new_inputs, + self.static_input_data_ptrs, + self.non_managed_static_input_idxs, + CheckInvariantStatus.StaticInputIdxMismatch, + ) + torch._check(False, lambda: error_msg) + + def run_first_inputs(self, new_inputs: list[InputType]) -> OutputType: + if config.triton.fast_path_cudagraph_asserts: + self.debug_check_invariants_before_invocation() + + # graph is already invoked in the __init__ + # inputs are copied over in _allocate_recording_inputs and subsequently cleared + assert len(new_inputs) == 0 + outputs = self.recording_outputs + self.recording_outputs = None + assert outputs is not None + return outputs + + def run(self, new_inputs: list[InputType]) -> OutputType: + self.check_static_inputs_are_stable(new_inputs) + + self._copy_inputs_and_remove_from_src(self.reconstructed_inputs, new_inputs) + + self.run_graph() + + outputs = self.reconstruct_outputs() + new_inputs.clear() + + if config.triton.fast_path_cudagraph_asserts: + self.debug_check_invariants_after_invocation() + + if config.triton.force_cudagraph_sync: + torch.cuda.synchronize() + + # Reset this to run the check in the future + self.static_inputs_stable = False + + return outputs + + def reconstruct_outputs(self) -> OutputType: + "Reconstruct output tensors according to their saved metadata and alias information" + + # Cached tensors will not yet be set on the first execution + # They are also cleared in checkpointing, so if we checkpoint this node + # and then execute it again we will need to repopulate cached tensors + if not self.cached_tensor_outputs: + self._initialize_cached_tensors() + + outputs: OutputType = [] + + for i, (storage_info, metadata) in enumerate( + zip(self.output_storage_alias, self.outputs_metadata) + ): + if not isinstance(metadata, dict): # tensor metadata + assert isinstance(metadata, (int, type(None))) + outputs.append(metadata) + continue + + cached_t = self.cached_tensor_outputs[i] + if cached_t is not None: + # this output represents a fresh allocated tensor. + # We return the same TensorImpl from run to run to avoid overhead. + # autograd.Function will reset the Autograd meta of output tensors + # as part of aot_autograd, but _backward_hooks are stored on tensors separately, + # so we need to manually reset hooks. + if cached_t._backward_hooks is not None: + cached_t._backward_hooks = None + + # No need to update weakrefs, already correctly initialized + outputs.append(cached_t) + continue + + static_t = self.static_output_tensors[i] + if static_t is not None: + assert self.outputs_weakrefs[i] is None + outputs.append(static_t) + continue + + storage = self.prepare_alias_info_for_tensor_construction( + storage_info, metadata + ) + + if isinstance(storage, UntypedStorage) or storage is None: + out = self._reconstruct_from_tensor_metadata(metadata, storage) + else: + assert isinstance(storage, int) + out = self._reconstruct_from_tensor_metadata( + metadata, cast(torch.Tensor, outputs[storage]).untyped_storage() + ) + + outputs.append(out) + w = self.outputs_weakrefs[i] + assert w is not None + w.swap_weakref(out.untyped_storage()._weak_ref()) + + return outputs + + def prepare_alias_info_for_tensor_construction( + self, + out_alias_info: OutputAliasInfo | None, + metadata: dict[str, Any] | int | None, + ) -> UntypedStorage | None | int: + if ( + isinstance(metadata, (int, type(None))) + or out_alias_info is UnaliasedStorage + ): + return None + + if isinstance(out_alias_info, AliasesPriorGraphOutput): + depth, existing_output_index = out_alias_info.index + ref = self.path_weakrefs[depth][existing_output_index] + assert ref is not None + return torch.UntypedStorage._new_with_weak_ptr(ref()) + + assert isinstance(out_alias_info, AliasesNewOutput) + return out_alias_info.index + + def prepare_storages_for_construction( + self, + ) -> list[UntypedStorage | None | int]: + output_storages = [] + for output_storage_alias, metadata in zip( + self.output_storage_alias, self.outputs_metadata + ): + output_storages.append( + self.prepare_alias_info_for_tensor_construction( + output_storage_alias, metadata + ) + ) + + return output_storages + + def run_graph(self) -> None: + assert self.graph is not None + self.graph.replay() + + def all_outputs_are_dead(self) -> bool: + "All outputs of the path from this node to its root are dead" + for depth, output_index in self.live_indices_after_graph: + if is_live(self.path_weakrefs[depth][output_index]): + return False + return True + + def _record(self, model: ModelType, inputs: list[InputType]) -> OutputType: + "Record the model" + assert self.graph is not None + + def static_input_iter() -> Generator[torch.Tensor, None, None]: + for i in self.wrapped_function.static_input_idxs: + _inp = inputs[i] + if isinstance( + _inp, torch.Tensor + ) and not self._is_cuda_graph_recorded_tensor(_inp): + yield _inp + + # see: output_is_alias_of_persistent_static_inputs above + static_input_persistent_storage_ptrs: dict[int, StorageWeakRefWrapper] = { + inp.untyped_storage().data_ptr(): StorageWeakRefWrapper(inp) + for inp in itertools.chain( + static_input_iter(), self.wrapped_function.constants + ) + } + + if config.triton.slow_path_cudagraph_asserts: + # need to use parent live weakrefs because live_indices isn't set yet + memory = ( + [] if self.parent is None else list(self.parent.path_live_weakrefs()) + ) + memory += [ + StorageWeakRefWrapper(elem) + for i, elem in enumerate(inputs) + if isinstance(elem, torch.Tensor) + and i not in self.wrapped_function.static_input_idxs + and elem.untyped_storage().data_ptr() != 0 + ] + check_memory_pool(self.device, self.cuda_graphs_pool, memory) + + with ( + preserve_rng_state(), + torch.cuda.device(self.device), + clear_cublas_manager(), + torch.cuda.graph( + self.graph, + stream=self.stream, + pool=self.cuda_graphs_pool, + capture_error_mode="thread_local", + ), + # NB: must go after torch.cuda.graph which switches the stream + _update_current_stream_external_object(), + CUDAGraphCaptureControlFlowOpDispatchMode(), + get_history_recording(), + ): + static_outputs = model(inputs) + + # running model should reclaim memory + assert len(inputs) == 0 + + if not isinstance(static_outputs, (list, tuple)): + static_outputs = (static_outputs,) + + # pyrefly: ignore [bad-argument-type] + self._add_first_outputs(static_outputs, static_input_persistent_storage_ptrs) + + # pyrefly: ignore [bad-return] + return static_outputs + + def _add_first_outputs( + self, + outputs: OutputType, + static_input_persistent_storage_ptrs: dict[int, StorageWeakRefWrapper], + ) -> None: + "Add the outputs from the first invocation of the node and set up metadata" + + # getting liveness before we have added the outputs to path, so the length + # of the two lists is equal + prev_liveness = self.recorded_liveness_before_graph + curr_liveness = self._get_liveness(self.path_weakrefs) + + delta = self._get_different_indices(prev_liveness, curr_liveness) + self.expected_dead_indices_after_graph = delta + + assert len(self.outputs_weakrefs) == 0 + # index from data pointer to index in outputs + output_new_storages_index: dict[StorageDataPtr, int] = {} + + self.unaliased_in_all_paths = [False for _ in range(len(outputs))] + self.static_output_tensors = [None for _ in range(len(outputs))] + + for i, o in enumerate(outputs): + if o is None or not isinstance(o, torch.Tensor): + self.output_storage_alias.append(UnaliasedStorage) + continue + + torch._check( + o.is_cuda or o.untyped_storage().data_ptr() == 0, + lambda: ( + "Expected all cuda outputs in cuda graph recording. Non cuda output " + f"from {self.stack_traces[i] if self.stack_traces else '(unknown)'}" + ), + ) + + ref = static_input_persistent_storage_ptrs.get( + o.untyped_storage().data_ptr() + ) + # also treat empty storages as static outputs because we do not need to manage their lifetime + # and they should not participate in checkpointing + is_empty_storage = o.untyped_storage().data_ptr() == 0 + if (ref and ref() is not None) or is_empty_storage: + self.output_storage_alias.append(None) + self.static_output_tensors[i] = o + continue + + path_ref = self._is_alias_of_live_recorded_tensor(o) + if path_ref is not None: + self._mark_prior_graph_output_as_aliased(path_ref) + + for idx, inp_path_ref in enumerate( + self.live_cudagraph_managed_path_refs + ): + if path_ref == inp_path_ref: + self.preserved_aliased_inputs[idx] = True + self.output_storage_alias.append(AliasesPriorGraphOutput(path_ref)) + continue + + if o.untyped_storage().data_ptr() in output_new_storages_index: + index = output_new_storages_index[o.untyped_storage().data_ptr()] + self.unaliased_in_all_paths[index] = False + self.output_storage_alias.append(AliasesNewOutput(index)) + continue + + output_new_storages_index[o.untyped_storage().data_ptr()] = i + self.output_storage_alias.append(UnaliasedStorage) + self.unaliased_in_all_paths[i] = True + + if self.stack_traces is None: + self.stack_traces = [None for _ in range(len(outputs))] + else: + assert len(self.stack_traces) == len(outputs), ( + "Wrong number of stack traces passed in" + ) + + assert not self.outputs_weakrefs + for out, static_output_tensor in zip(outputs, self.static_output_tensors): + if not isinstance(out, torch.Tensor) or static_output_tensor is not None: + self.outputs_weakrefs.append(None) + self.tensor_weakrefs.append(None) + else: + self.outputs_weakrefs.append(StorageWeakRefWrapper(out)) + self.tensor_weakrefs.append(TensorWeakRef(out)) + + self.recorded_liveness_after_graph = self._get_liveness(self.path_weakrefs) + self.checkpointed_caching_state = torch._C._cuda_getCheckpointState( + self.device, self.cuda_graphs_pool + ) + + # now, get liveness with outputs added + for depth in range(len(self.path_weakrefs)): + for output_index in range(len(self.path_weakrefs[depth])): + if is_live(self.path_weakrefs[depth][output_index]): + self.live_indices_after_graph.append((depth, output_index)) + + self.debug_check_invariants_after_invocation() + if config.triton.slow_path_cudagraph_asserts: + check_memory_pool( + self.device, self.cuda_graphs_pool, list(self.path_live_weakrefs()) + ) + + def _mark_prior_graph_output_as_aliased(self, index: PathOutputIndex) -> None: + "Remove a graph output from the unaliased, cached tensors in an ancestor node" + depth, output_index = index + node = list(self._path_from_root)[depth] + node.unaliased_in_all_paths[output_index] = False + x = self.path_weakrefs[depth][output_index] + assert x is not None + x.remove_extra_reference() + + def _initialize_cached_tensors(self) -> None: + # we should not be clearing output_weakrefs, and they should be set in the first + # record run + assert len(self.outputs_weakrefs) == len(self.outputs_metadata) + + for i, (storage_info, metadata, make_cached) in enumerate( + zip( + self.output_storage_alias, + self.outputs_metadata, + self.unaliased_in_all_paths, + ) + ): + if not make_cached: + self.cached_tensor_outputs.append(None) + continue + + assert storage_info is UnaliasedStorage + assert isinstance(metadata, dict) + s = self.create_storage(metadata) + out = self._reconstruct_from_tensor_metadata(metadata, storage=s) # type: ignore[arg-type] + + # XXX: let autograd know that there will be an additional reference to the tensor + # that can be ignored when deciding whether to do gradient buffer inplacing. + # Otherwise, inplacing could differ between tracing and subsequent execution. + # For some models we tested this led to inputs no longer being in cudagraph pools, + # leading to spurious re-recordings. + # It also tells AMP cache that even though the tensor impls cannot be cached + # in dtype conversions. + + torch._C._add_cached_tensor(out) + + self_ref = weakref.ref(self) + + # one reference in our array, and calling sys.getrefcount bumps the refcount by one + def check_refcount(i: int) -> bool: + self_loc = self_ref() + if self_loc is None: + return False + refcount = self_loc.get_output_refcount(i) + # pyrefly: ignore + if self_loc.cached_tensor_outputs[i]._use_count() > 1: + # c10::Tensor may also holds one reference count + assert refcount >= 3 + return refcount == 3 + else: + assert refcount >= 2 + return refcount == 2 + + check = functools.partial(check_refcount, i=i) + + self.outputs_weakrefs[i] = StorageWeakRefWrapper(out, extra_ref_check=check) + self.cached_tensor_outputs.append(out) + + def get_output_refcount(self, index: int) -> int: + return sys.getrefcount(self.cached_tensor_outputs[index]) + + @property + def parent(self) -> CUDAGraphNode | None: + "unwraps the weakref to _parent" + return self._parent() if self._parent is not None else None + + @property + def _path_to_root(self) -> Generator[CUDAGraphNode, None, None]: + "Returns all nodes in the path starting at self and ending at root" + node = self + # pyrefly: ignore [bad-assignment] + while node: + yield node + node = node.parent # type: ignore[assignment] + + @property + def _path_from_root(self) -> Generator[CUDAGraphNode, None, None]: + "Returns all nodes in the path starting at the root and ending at self" + nodes = reversed(list(self._path_to_root)) + yield from nodes + + def _is_cuda_graph_recorded_tensor(self, t: torch.Tensor) -> bool: + "Is this tensor an output of a node in this path" + for output_refs in self.path_weakrefs: + for storage_weak_ref in output_refs: + if storage_weak_ref is None: + continue + # don't need to check liveness of storage since the cuda graph managed + # memory is never released. + data_ptr = storage_weak_ref.data_ptr() + if t.untyped_storage().data_ptr() == data_ptr: + return True + + return False + + def _is_alias_of_live_recorded_tensor( + self, t: torch.Tensor + ) -> PathOutputIndex | None: + for depth, output_refs in enumerate(self.path_weakrefs): + for output_index, storage_ref in enumerate(output_refs): + if (storage_and_ptr := maybe_deref(storage_ref)) is not None: + _storage, ptr = storage_and_ptr + if ptr == t.untyped_storage().data_ptr(): + return (depth, output_index) + + return None + + @staticmethod + def _check_liveness( + indices: list[PathOutputIndex], + output_refs: list[list[StorageWeakRefWrapper | None]], + ) -> bool: + "Check that all of the indices specified are dead references" + for depth, output_index in indices: + w = output_refs[depth][output_index] + assert w is not None + if w() is not None: + return False + return True + + def add_child(self, function_id: FunctionID, node: CUDAGraphNode) -> None: + "Adds node as a a child of self" + self.children[function_id].append(node) + + @staticmethod + def _get_different_indices( + prev: list[list[bool]], curr: list[list[bool]] + ) -> list[PathOutputIndex]: + "Find indices where the two lists differ." + dead_indices = [] + assert len(prev) <= len(curr) + for i, (outputs1, outputs2) in enumerate(zip(prev, curr)): + assert len(outputs1) == len(outputs2) + for j, (output1, output2) in enumerate(zip(outputs1, outputs2)): + if output1 != output2: + dead_indices.append((i, j)) + + return dead_indices + + @staticmethod + def _get_liveness( + weakrefs: list[list[StorageWeakRefWrapper | None]], + ) -> list[list[bool]]: + "Maps weakrefs to true if the reference is alive and false otherwise" + if len(weakrefs) == 0: + return [] + + return [pytree.tree_map(is_live, outputs) for outputs in weakrefs] + + def debug_assert_invariants( + self, expected_liveness: list[list[bool]], newly_dead: list[PathOutputIndex] + ) -> None: + if not config.triton.fast_path_cudagraph_asserts: + return + + for i, node in enumerate(self._path_from_root): + assert self.path_weakrefs[i] is node.outputs_weakrefs + + nodes = list(self._path_from_root) + + live_blocks = get_block_addrs(self.cuda_graphs_pool) + + live_storage_data_ptrs = OrderedSet[Any]() + live_storage_weak_ptrs = OrderedSet[Any]() + + for depth, outputs_liveness in enumerate(expected_liveness): + for output_idx, output_liveness in enumerate(outputs_liveness): + # tensor can die early, but it can't be alive when it should be dead + w = self.path_weakrefs[depth][output_idx] + if (stor_weak_ptr_and_data_ptr := maybe_deref(w)) is not None: + assert output_liveness + stor_weak_ptr, stor_data_ptr = stor_weak_ptr_and_data_ptr + assert (stor_data_ptr in live_storage_data_ptrs) == ( + stor_weak_ptr in live_storage_weak_ptrs + ) + live_storage_data_ptrs.add(stor_data_ptr) + live_storage_weak_ptrs.add(stor_weak_ptr) + + is_persistent_alias = ( + nodes[depth].static_output_tensors[output_idx] is not None + ) + + if is_persistent_alias: + assert stor_data_ptr not in live_blocks + + for depth, output_index in newly_dead: + assert not is_live(self.path_weakrefs[depth][output_index]) + + def debug_check_invariants_before_invocation(self) -> None: + self.debug_assert_invariants( + self.recorded_liveness_before_graph, self.expected_dead_indices_before_graph + ) + + def debug_check_invariants_after_invocation(self) -> None: + self.debug_assert_invariants( + self.recorded_liveness_before_graph, self.expected_dead_indices_after_graph + ) + + def data_ptrs_dead_since_invocation(self) -> list[int]: + """ + Since this node was invoked, return data ptrs of all tensor outputs that have died + in the current executing tree path. + """ + curr_liveness = self._get_liveness(self.path_weakrefs) + _get_different_indices = self._get_different_indices( + self.recorded_liveness_after_graph, curr_liveness + ) + + path = list(self._path_from_root) + ptrs_to_deallocate = [] + for depth, output_index in _get_different_indices: + ptrs_to_deallocate.append( + path[depth].outputs_metadata[output_index]["data_ptr"] # type: ignore[index] + ) + + return ptrs_to_deallocate + + def path_live_weakrefs(self) -> Iterator[StorageWeakRefWrapper]: + for i, j in self.live_indices_after_graph: + out = self.path_weakrefs[i][j] + if out is not None and is_live(out): + yield out + + def remove_node_cached_tensors(self) -> None: + for t in self.cached_tensor_outputs: + if t is not None: + torch._C._remove_cached_tensor(t) + self.cached_tensor_outputs.clear() + + for i, unaliased in enumerate(self.unaliased_in_all_paths): + if unaliased: + n = self.outputs_weakrefs[i] + assert n is not None + n.remove_extra_reference() + + def remove_path_cached_tensors(self) -> None: + for node in self._path_from_root: + node.remove_node_cached_tensors() + + def clear_path_state(self) -> None: + "Clear the path state in this current executing node" + # this doesn't actually do anything right now, leaving it as placeholder + + @staticmethod + def _tensor_metadata( + x: torch.Tensor, ignore_storage_offset: bool = True + ) -> dict[str, Any]: + assert isinstance(x, torch.Tensor) + # We ignore the storage offset for inputs, but not for outputs + # TODO: - should we make the storage resizable ? + return { + "nbytes": x.untyped_storage().nbytes(), + "data_ptr": x.untyped_storage().data_ptr(), + "size": x.shape, + "stride": x.stride(), + "dtype": x.dtype, + "device": x.device, + "storage_offset": x.storage_offset() if not ignore_storage_offset else 0, + } + + def _reconstruct_from_tensor_metadata( + self, metadata: dict[str, Any], storage: UntypedStorage | None = None + ) -> Tensor: + s = self.create_storage(metadata) if storage is None else storage + return torch._C._construct_CUDA_Tensor_From_Storage_And_Metadata(metadata, s) # type: ignore[arg-type] + + def create_storage(self, metadata: dict[str, Any]) -> torch.types.Storage: + return torch._C._construct_storage_from_data_pointer( + metadata["data_ptr"], metadata["device"], metadata["nbytes"] + ) + + def _allocate_and_copy_recording_inputs( + self, inputs: list[InputType] + ) -> list[InputType]: + """ + Allocate inputs for non static, non cudagraph managed tensors in the memory pool + and copy over the tensor values. + """ + + torch.cuda.synchronize() + self.stream.wait_stream(torch.cuda.current_stream()) + recording_inputs: list[InputType] = [] + + with ( + warnings.catch_warnings(record=True), + torch.cuda.device(self.device), + _use_cuda_memory_pool_manager( + self.device, + mem_pool=self.cuda_graphs_pool, + stream=self.stream, + ), + ): + for i, inp in enumerate(inputs): + if not isinstance(inp, torch.Tensor): + assert isinstance(inp, (int, torch.Generator, OpaqueBase)) + + recording_inputs.append(inp) + elif i not in self.static_input_idxs: + # static_input does an allocation! + recording_inputs.append(static_input(inp)) + else: + recording_inputs.append(inp) + + self._copy_inputs_and_remove_from_src(recording_inputs, inputs) + + return recording_inputs + + def check_invariants( + self, inputs: list[InputType] + ) -> tuple[CheckInvariantStatus, Callable[..., str]]: + """ + Checks if this node can be run. The same pattern of tensor liveness, static inputs, + and tensors managed in the cudagraph private pool must remain stable. + """ + + _logger = functools.partial( + log_data_ptr_mismatch, + self.wrapped_function.placeholders, + inputs, + self.static_input_data_ptrs, + ) + + # previously managed data pointers remain stable + # this is on the hot path so moved to C++. equivalent to: + # return all(t.data_ptr() == data_ptr for (t, data_ptr) in zip(tensors, data_ptrs)) + if not torch._C._tensors_data_ptrs_at_indices_equal( + inputs, # type: ignore[arg-type] + self.static_input_data_ptrs, + self.cudagraph_managed_idxs, + ): + status = CheckInvariantStatus.CudagraphManagedIdxMismatch + _logger = functools.partial( + _logger, + self.cudagraph_managed_idxs, + status, + ) + return status, _logger + + if not self._check_liveness( + self.expected_dead_indices_before_graph, self.path_weakrefs + ): + status = CheckInvariantStatus.ExpectedDeadIndicesBeforeGraphMismatch + return status, lambda: f"{status}" + + # static input data pointers should remain stable + # if we are inlining builtin nn modules we re-record in this case + # if we are not inlining builtin nn modules, we check this in check_static_inputs_are_stable + # and error if they are not stable + if ( + self.rerecord_if_static_inputs_change + and not torch._C._tensors_data_ptrs_at_indices_equal( + inputs, # type: ignore[arg-type] + self.static_input_data_ptrs, + self.tensor_static_input_idxs, + ) + ): + status = CheckInvariantStatus.StaticInputIdxMismatch + _logger = functools.partial( + _logger, + self.tensor_static_input_idxs, + status, + ) + return status, _logger + + # the cudagraph managed tensors which died upon recording must also die upon + # this invocation. it is too late to check after we've replayed the graph, + # because we would have already written over their memory. + for idx in self.cudagraph_managed_idxs: + if not self.preserved_aliased_inputs[idx]: + inputs[idx] = None # type: ignore[call-overload] + + torch._check( + self._check_liveness( + self.expected_dead_indices_after_graph, self.path_weakrefs + ), + lambda: "TODO: graph recording observed an input tensor deallocate during graph " + " recording that did not occur during replay. Please file an issue.", + ) + return CheckInvariantStatus.SUCCESS, lambda: f"{CheckInvariantStatus.SUCCESS}" + + def num_descendants(self) -> int: + "Total number of descendents of this node" + num_desc = 0 + for children in self.children.values(): + for child in children: + num_desc += 1 + num_desc += child.num_descendants() + return num_desc + + +def get_cudagraph_segments(pool_id: tuple[int, int]) -> Any: + segments = torch.cuda.memory_snapshot() + return [segment for segment in segments if segment["segment_pool_id"] == pool_id] + + +def get_block_addrs(pool_id: tuple[int, int], live_only: bool = True) -> list[int]: + blocks = [] + + for segment in get_cudagraph_segments(pool_id): + addr = segment["address"] + for block in segment["blocks"]: + if block["state"] == "active_allocated" or not live_only: + blocks.append(addr) + + addr += block["size"] + + return blocks + + +def format_tb(frames: list[Any]) -> str: + formatted_traceback = [ + traceback.FrameSummary(entry["filename"], entry["line"], entry["name"]) + for entry in frames + ] + + return "".join(traceback.format_list(formatted_traceback)) + + +def check_memory_pool( + device: int, + pool_id: tuple[int, int], + live_storages_ptrs: list[StorageWeakRefWrapper], +) -> None: + """Validate cudagraph pool allocations against tracked live storages and surface leaks.""" + assert all(isinstance(elem, StorageWeakRefWrapper) for elem in live_storages_ptrs) # noqa: C419 + unique_storages = {stor.data_ptr() for stor in live_storages_ptrs if stor()} # noqa: set_linter + + # check if there is a divergence first, then do the expensive snapshot call after + # we know it will error + if torch._C._cuda_checkPoolLiveAllocations(device, pool_id, unique_storages): + return + + # at this point we are past the fast-path. we have seen rare cases where a dead tensor is dead, + # but hasn't been gc'd yet, and gives false positive for allocated_not_in_live_storages + gc.collect() + torch.cuda.synchronize() + + segments = get_cudagraph_segments(pool_id) + + allocated_not_in_live_storages = {} + + for segment in segments: + addr = segment["address"] + for block in segment["blocks"]: + if block["state"] == "active_allocated": + if addr not in unique_storages: + allocated_not_in_live_storages[addr] = block + else: + unique_storages.remove(addr) + + addr += block["size"] + + torch._check( + len(unique_storages) == 0, + lambda: ( + f"These storage data ptrs are not allocated in pool {pool_id} but should be: {unique_storages}. " + f"This could be a bug in inductor aliasing tracking or in a custom op's meta function. Please file an issue." + ), + ) + + if len(allocated_not_in_live_storages) != 0: + formatted = [] + for dp, block in allocated_not_in_live_storages.items(): + trace = format_tb(block.get("frames", [])) + # pyrefly: ignore [bad-argument-type] + formatted.append(f"Data Pointer: {dp}, history: \n{trace}") + formatted_s = "\n".join(formatted) + + history_hint = "" + if not config.triton.cudagraph_trees_history_recording: + history_hint = "- Set torch._inductor.config.triton.cudagraph_trees_history_recording = True for allocation origins\n" + + objgraph_hint = ( + ( + "- Objgraph backrefs disabled; set torch._inductor.config.triton.cudagraph_trees_objgraph = True " + "(refs_live_tensor_{index}.svg)\n" + ) + if not config.triton.cudagraph_trees_objgraph + else "" + ) + objgraph_files_hint = "" + + if config.triton.cudagraph_trees_objgraph: + # pyrefly: ignore # import-error + import objgraph + + generated_files: list[str] = [] + + tensors = objgraph.by_type("torch.Tensor") + for index, bad_dp in enumerate(allocated_not_in_live_storages): + bad_tensor = next( + (t for t in tensors if bad_dp in collect_cuda_data_ptrs(t)), None + ) + if bad_tensor is None: + continue + filename = f"refs_live_tensor_{index}.svg" + objgraph.show_backrefs(bad_tensor, filename=filename, max_depth=5) + generated_files.append(filename) + if generated_files: + objgraph_files_hint = ( + f"- Objgraph backrefs written: {', '.join(generated_files)}\n" + ) + + dangling_addrs = OrderedSet(allocated_not_in_live_storages.keys()) + msg = ( + f"Detected {len(allocated_not_in_live_storages)} tensor(s) in the cudagraph pool not tracked as outputs. " + f"All live allocations must be tracked for correctness.\n" + f"Debugging:\n" + f"{history_hint}" + f"{objgraph_hint}" + f"- Search gc.get_objects() for tensors with data_ptr() in {dangling_addrs}\n" + f"- Use refcycle to find what is preventing cleanup\n" + f"{objgraph_files_hint}" + f"Allocations:\n{formatted_s}" + ) + raise RuntimeError(msg) + + +class ExecutionState(Enum): + """ + Represents the state of the CUDAGraph Tree. Will be None if there is no live current memory allocated + in the cuda graph pool. Otherwise will reflect the state of the most recently executed node. + """ + + NONE = auto() + WARMUP = auto() + RECORDING = auto() + EXECUTION = auto() + + +class CompilationMode(Enum): + FORWARD = auto() + BACKWARD = auto() + INFERENCE = auto() + + +class CUDAGraphTreeManager: + """ + Groups individual recordings or executions of cuda graphs into a tree of recordings, + and checks required invariants, and manages warmups of graphs. + + When graphs are recorded in the same tree, it enforces subsequent execution + to follow the same order and have the same output tensor livespans. To remove + unnecessary coupling of cuda graphs (and additional imposed invariants), + the tree manager will end a currently recording tree whenever it is valid - when + the memory pool no longer has any live allocations. + + We ignore outputs from a previous generation that correspond to prior model outputs. + Currently this is hardcoded `GenerationTracker.generation` tracked in torch dynamo. + # TODO: make generation increment configurable, warn on overwrite. + + We run graph warmups in the cudagraph memory pool and return the result on the first invocation + of a function. For many models it is important to reclaim activations as you run the backward. + If we were to warm up the model and keep an extra copy of the inputs around to subsequently + use for recording, we would incur a memory penalty. Additionally, if we are part way through training + your model and need to recompile, memory will be allocated to the cuda graph pool, so we run this + warmup run in the cuda graph memory pool. As for recording, warm up needs the state of live tensors + to be accurately reflected so we checkpoint the allocator state if we need to warm up following graph + replay. + """ + + def __init__(self, device_index: int) -> None: + # roots are functions which have no dependencies on an other node. I.e., + # when they are first invoked, none of their inputs are outputs are outputs + # of another node, nor are there any live outputs of another node whose + # liveness would create a dependency. + self.roots: dict[FunctionID, list[CUDAGraphNode]] = defaultdict(list) + + # mapping from function id to wrapped function + self.ids_to_funcs: dict[FunctionID, WrappedFunction] = {} + + self.ids_to_stack_traces: dict[FunctionID, StackTraces | None] = {} + + self.warmed_up_functions: OrderedSet[FunctionID] = OrderedSet() + # if we fail to increment generation, and are stuck warming up, + # only warn on each function once + self.warned_functions: OrderedSet[FunctionID] = OrderedSet() + torch._C._set_cached_tensors_enabled(True) + + # warn only once if a function mutates inputs + self.warned_mutation: OrderedSet[FunctionID] = OrderedSet() + + # NB: cuda caching allocator will remember the stream a segment is allocated to + # and only allocate that segment to the same stream. we need to use a single stream + # for all allocations to the memory pool, otherwise the allocations to separate streams + # will not be reused; separate recordings would have use the same memory pool, but not + # the same memory. + + with torch.cuda.device(device_index): + torch.cuda.synchronize() + self.stream = torch.cuda.Stream() + self.stream.wait_stream(torch.cuda.current_stream()) + + # Keeps Memory Pool Alive + self.graph: torch.cuda.CUDAGraph | None = torch.cuda.CUDAGraph() + self.cuda_graphs_thread_pool = torch.cuda.graph_pool_handle() + + with ( + warnings.catch_warnings(record=True), + torch.cuda.graph( + self.graph, + pool=self.cuda_graphs_thread_pool, + stream=self.stream, + capture_error_mode="thread_local", + ), + ): + pass + + self.graph_counter = itertools.count(0) + self.func_counter = itertools.count(0) + + # mapping from graph_id to (function id to mutation type hint) since we are + # specializing on a particular combination of Parent Node -> Function ID. + self.non_cudagraph_managed_mutation_hint: dict[ + GraphID | None, dict[FunctionID, bool] + ] = defaultdict(dict) + self.warmup_node_counter = itertools.count(start=-1, step=-1) + + # mapping from graph_id to (function id to re-record count). We fall back to + # eager function if a function is re-recorded frequently on a node. + self.num_rerecord: dict[GraphID | None, dict[FunctionID, int]] = defaultdict( + lambda: defaultdict(lambda: 0) + ) + + # whether we the current node is in a state of warmup, recording, execution. If + # there is no current node the state will be ExecutionState.None. + self.path_state = ExecutionState.NONE + self.device_index = device_index + + # the most recently invoked cudagraph wrapping of a function. Will be None + # when there is no output from a previous recording or execution whose memory + # we need to respect in the cuda caching allocation. If you incremented generation, + # this will also be none, as ignore those allocations. + self.current_node: CUDAGraphNode | CUDAWarmupNode | None = None + + # current generation of cudagraph invocations. when torch.compile is run + # we increment the current generation. are willing to ignore live outputs + # of a previous generation in checking liveness. + self.current_gen: int = -1 + + # number of instances we are in execution and failed to match to an + # existing child + self.debug_fail_counter = 0 + # number of instances we had to checkpoint the function + self.debug_checkpointing_counter = 0 + + self.id_to_mode: dict[FunctionID, CompilationMode] = {} + self.id_to_compile_id: dict[FunctionID, CompileId | None] = {} + + # Note: [Backward Generation Handling] + # We generally perform a sequence of forward executions followed by backward executions. + # If multiple torch.compile wrapped forwards are executed with their backwards pending, + # we should not disregard the outputs from a prior torch.compile since the entire training + # loop hasn't completed. Occasionally, a backward pass corresponding to a forward pass may + # not be executed, so we cannot wait for all pending forward pass backward completions, so + # we cannot wait for all backwards to have been invoked. Instead we wait for a single backward + # invocation. Triggering a backward pass typically doesn't lead to another torch.compile + # invocation, making it less likely for the generation to increase between multiple + # backward calls. The following use case is covered by this approach: + # mod1 = torch.compile(...) + # mod2 = torch.compile(...) + # mod2(mod1(x)).sum().backward() + + self.running_forwards_with_pending_backwards = False + self.mode: CompilationMode | None = None + + self.disable_invalidate_aliases = ( + False + if not torch._environment.is_fbcode() + else torch._utils_internal.justknobs_check( + "pytorch/inductor:disable_cudagraph_alias_invalidation" + ) + ) + + def run(self, new_inputs: list[InputType], function_id: FunctionID) -> OutputType: + assert self.graph is not None, "Running CUDAGraph after shutdown" + self.mode = self.id_to_mode[function_id] + self.compile_id = self.id_to_compile_id[function_id] + out = self._run(new_inputs, function_id) + + # The forwards are only pending following invocation, not before + if self.mode == CompilationMode.FORWARD: + self.running_forwards_with_pending_backwards = True + elif self.mode == CompilationMode.BACKWARD: + self.running_forwards_with_pending_backwards = False + + return out + + def set_to_running_backward(self) -> None: + self.running_forwards_with_pending_backwards = False + self.mode = CompilationMode.BACKWARD + + def _get_cuda_graph_recorded_tensor_checker(self) -> Callable[[Tensor], bool]: + return ( + self.current_node._is_cuda_graph_recorded_tensor + if isinstance(self.current_node, (CUDAGraphNode, CUDAWarmupNode)) + else lambda _: False + ) + + def new_warmup_node_id(self) -> GraphID: + return GraphID(next(self.warmup_node_counter)) + + def _update_non_cudagraph_managed_mutation( + self, function_id: FunctionID, inputs: list[InputType] + ) -> None: + node_id = self._get_node_id() + if maybe_mutation_str := check_for_mutation( + self.ids_to_funcs[function_id], + inputs, + self._get_cuda_graph_recorded_tensor_checker(), + ): + self.non_cudagraph_managed_mutation_hint[node_id][function_id] = True + # warn once per function_id + if function_id in self.warned_mutation: + return + self.warned_mutation.add(function_id) + log_cudagraph_skip_and_bump_counter(maybe_mutation_str) + else: + self.non_cudagraph_managed_mutation_hint[node_id][function_id] = False + + def _get_node_id(self) -> GraphID | None: + if self.current_node is None: + return None + elif isinstance(self.current_node, (CUDAGraphNode, CUDAWarmupNode)): + return self.current_node.id + else: + raise RuntimeError(f"Unknown node type {type(self.current_node)}") + + def exceed_rerecord_limit( + self, node_id: GraphID | None, function_id: FunctionID + ) -> bool: + return False + + def _run(self, new_inputs: list[InputType], function_id: FunctionID) -> OutputType: + # we will try to end the current execution lazily, since + # we dont want to do unnecessary checking of the existing outputs + # on the hot path, but both recording and warmup only happen once + # so we check up front + if self.in_recording: + self.try_end_curr_recording(function_id) + + if self.in_warmup: + self.try_end_curr_warmup(function_id) + + node_id = self._get_node_id() + if function_id not in self.non_cudagraph_managed_mutation_hint[node_id]: + self._update_non_cudagraph_managed_mutation(function_id, new_inputs) + + # Early exit if the function mutates inputs which are neither parameters/buffers nor + # cudagraph recorded tensors. This check should happen after `try_end_curr_recording` + # and `try_end_curr_warmup` which may change self.current_node. + if self.non_cudagraph_managed_mutation_hint[node_id][ + function_id + ] or self.exceed_rerecord_limit(node_id, function_id): + return self.ids_to_funcs[function_id].model(new_inputs) + + # warming up a function and subsequentally recording may use different memory addresses + # because both depend on the state of the caching allocator. if we warm up graph A, + # then warm up graph B and make more allocations, the subsequent recording of A will not + # necessarily use the same addresses as in the warm up. Thus any warm up of a node can only + # be followed by warm up runs. + if ( + ( + not ( + function_id in self.warmed_up_functions + or config.triton.skip_cudagraph_warmup + ) + ) + or self.in_warmup + or config.triton.force_cudagraphs_warmup + ): + # If we are in the middle of executing cuda graphs, then we need to checkpoint memory state. + # Both Recording and Warmup will be reflected in the allocator and dont need changes + if self.path_state == ExecutionState.EXECUTION: + self.apply_checkpoint_execution_state_in_allocator() + + return self.run_eager(new_inputs, function_id) + + assert not isinstance(self.current_node, CUDAWarmupNode) + child_nodes = ( + self.roots if self.current_node is None else self.current_node.children + ) + + if not self.in_recording: + unexpected_rerecord = False + unexpected_rerecord_reason = None + for child in child_nodes[function_id]: + # here we are checking memory consistency between recording and execution, + # as well as things like stability of tensor locations, etc + # and other + status, status_logger = child.check_invariants(new_inputs) + if status == CheckInvariantStatus.SUCCESS: + return self.execute_node(child, new_inputs) + + if ( + status == CheckInvariantStatus.StaticInputIdxMismatch + or status == CheckInvariantStatus.CudagraphManagedIdxMismatch + ): + unexpected_rerecord = True + # Only compute detailed reason when debug logging is enabled + if log.isEnabledFor(logging.DEBUG): + unexpected_rerecord_reason = status_logger() + log.debug( + "[%s] Re-recording function=%s, mode=%s, reason=%s", + self.compile_id, + self.get_func_name(function_id), + self.id_to_mode[function_id].name, + unexpected_rerecord_reason, + ) + else: + # Defer reason computation until needed (for exceed_rerecord_limit) + unexpected_rerecord_reason = status_logger + + # now that we know the new function can't be run as a child of the + # current node, if it is a root, try to end the current execution. + # as noted above, we want to do this lazily to avoid having to + # check all existing outputs + if self.current_node is not None and function_id in self.roots: + self.try_end_curr_execution() + + # run again to hit the root matching case which must succeed + if self.current_node is None: + return self.run(new_inputs, function_id) + + if len(self.ids_to_funcs[function_id].mutated_input_idxs) > 0: + self._update_non_cudagraph_managed_mutation(function_id, new_inputs) + if self.non_cudagraph_managed_mutation_hint[self._get_node_id()][ + function_id + ]: + return self.ids_to_funcs[function_id].model(new_inputs) + + # nb: run before checkpointing because checkpointing is slow, and we will + # be using the eager caching allocator pool which does not require live + # accounting of tensors in cudagraph allocator + if unexpected_rerecord: + curr_node_id = self._get_node_id() + self.num_rerecord[curr_node_id][function_id] += 1 + if self.exceed_rerecord_limit(curr_node_id, function_id): + _id = curr_node_id.id if curr_node_id else None + # unexpected_rerecord_reason is either a string (if debug was enabled) + # or a callable (if debug was disabled) + assert unexpected_rerecord_reason is not None + reason = ( + unexpected_rerecord_reason + if isinstance(unexpected_rerecord_reason, str) + else unexpected_rerecord_reason() + ) + log_cudagraph_skip_and_bump_counter( + f"skipping cudagraph due to function {function_id.id} exceeding max " + f"re-recording limit " + f"(={torch._inductor.config.triton.cudagraph_unexpected_rerecord_limit}) " + f"on cudagraph node {_id} due to {reason}." + ) + return self.ids_to_funcs[function_id].model(new_inputs) + + # at this point, we necessarily will do a new recording + self.debug_fail_counter += 1 + + self.try_end_curr_execution() + if self.current_node is not None: + self.apply_checkpoint_execution_state_in_allocator() + + # now, we are in a recording state ! + return self.record_function(new_inputs, function_id) + + def shutdown(self) -> None: + """ + Remove all cached tensors in all nodes. Because cached tensors can hold gradients which in turn + might reference a backward which invokes a CUDA Graph Node, we have to manually clear them on shutdown + to avoid a reference cycle. + """ + nodes = [] + for roots in self.roots.values(): + nodes.extend(roots) + + while nodes: + node = nodes.pop() + for children in node.children.values(): + nodes.extend(children) + node.remove_node_cached_tensors() + node.graph = None + + self.graph = None + self.roots = None # type: ignore[assignment] + self.current_node = None + + def record_function( + self, new_inputs: list[InputType], function_id: FunctionID + ) -> OutputType: + assert not isinstance(self.current_node, CUDAWarmupNode) + with torch._dynamo.callback_handler.install_callbacks( + CallbackTrigger.CUDAGRAPH_RECORDING, str(self.compile_id) + ): + graph_id = self.new_graph_id() + log.debug( + "[%s] Recording function=%s, mode=%s, cuda_graph_id=%d, inputs: %s", + self.compile_id, + self.get_func_name(function_id), + self.id_to_mode[function_id].name, + graph_id.id, + format_inputs_log(new_inputs), + ) + torch.cuda.synchronize() + node = CUDAGraphNode( + self.ids_to_funcs[function_id], + graph_id, + self.current_node, + new_inputs, + self.cuda_graphs_thread_pool, + self.device_index, + self.ids_to_stack_traces[function_id], + self.stream, + self.mode, + self.compile_id, + ) + if self.current_node is None: + self.roots[function_id].append(node) + else: + self.current_node.add_child(function_id, node) + self.current_node = node + self.path_state = ExecutionState.RECORDING + self.update_generation() + torch.cuda.synchronize() + return node.run_first_inputs(new_inputs) + + def execute_node( + self, node: CUDAGraphNode, new_inputs: list[InputType] + ) -> OutputType: + self.current_node = node + self.path_state = ExecutionState.EXECUTION + self.update_generation() + return node.run(new_inputs) + + def run_eager( + self, new_inputs: list[InputType], function_id: FunctionID + ) -> OutputType: + # this is only stored on current node, because when we start a new path, + # we will deallocate it + already_warm = function_id in self.warmed_up_functions + func_name = self.get_func_name(function_id) + if not already_warm: + log.debug( + "[%s] Running warmup function=%s, mode=%s", + self.compile_id, + func_name, + self.id_to_mode[function_id].name, + ) + else: + log.debug( + "[%s] Running eager function=%s", + self.compile_id, + func_name, + ) + self.warmed_up_functions.add(function_id) + node = CUDAWarmupNode( + self.ids_to_funcs[function_id], + self.current_node, + self.cuda_graphs_thread_pool, + self.graph, + self.device_index, + self.ids_to_stack_traces[function_id], + self.stream, + already_warm, + self.new_warmup_node_id(), + ) + self.current_node = node + self.path_state = ExecutionState.WARMUP + self.update_generation() + return node.run(new_inputs) + + def new_graph_id(self) -> GraphID: + return GraphID(next(self.graph_counter)) + + def new_func_id(self) -> FunctionID: + return FunctionID(next(self.func_counter)) + + def get_func_name(self, function_id: FunctionID) -> str: + return getattr(self.ids_to_funcs[function_id].model, "__name__", "unknown") + + def add_function( + self, + model: ModelType, + inputs: list[InputType], + static_input_idxs: Sequence[int], + stack_traces: StackTraces | None, + mode: CompilationMode, + constants: tuple[torch.Tensor, ...], + placeholders: tuple[PlaceholderInfo, ...], + mutated_input_idxs: tuple[int, ...], + compile_id: CompileId | None, + ) -> tuple[ + ModelType, + OutputType, + ]: + id = self.new_func_id() + self.ids_to_stack_traces[id] = stack_traces + self.ids_to_funcs[id] = WrappedFunction( + model, + list(static_input_idxs), + id, + tuple(t for t in constants if isinstance(t, torch.Tensor) and t.is_cuda), + placeholders, + mutated_input_idxs, + ) + self.id_to_mode[id] = mode + self.id_to_compile_id[id] = compile_id + fn = functools.partial(self.run, function_id=id) + + # container needs to set clean up when fn dies + get_container(self.device_index).add_strong_reference(fn) + return fn, fn(inputs) + + @property + def in_recording(self) -> bool: + return self.path_state == ExecutionState.RECORDING + + @property + def in_warmup(self) -> bool: + return self.path_state == ExecutionState.WARMUP + + def get_roots(self) -> Iterator[CUDAGraphNode]: + for nodes in self.roots.values(): + yield from nodes + + @property + def current_node(self) -> CUDAGraphNode | CUDAWarmupNode | None: + return self._current_node + + @current_node.setter + def current_node(self, value: CUDAGraphNode | CUDAWarmupNode | None) -> None: + self._current_node = value + if value is None: + self.path_state = ExecutionState.NONE + + def update_generation(self) -> None: + self.current_gen = self.get_curr_generation() + + @staticmethod + def get_curr_generation() -> int: + if MarkStepBox.mark_step_counter != 0: + return MarkStepBox.mark_step_counter + + return GenerationTracker.generation + + @staticmethod + def user_invoked_mark_step() -> bool: + return MarkStepBox.mark_step_counter != 0 + + def can_start_new_generation(self) -> bool: + if not self.in_new_torch_compile_invocation(): + return False + + if self.user_invoked_mark_step(): + return True + + return not self.running_forwards_with_pending_backwards + + def in_new_torch_compile_invocation(self) -> bool: + return self.current_gen != self.get_curr_generation() + + def try_end_curr_recording(self, function_id: FunctionID) -> None: + """ + Check if the current recording can be terminated, either because all outputs of the + previously recorded node are dead or because it was executed in a different + generation. Will set current_node to None and in_recording to False if successful. + """ + assert self.in_recording + assert self.current_node is not None + + # multiple invocations, allow overwriting the previous generation + if self.can_start_new_generation(): + self.dealloc_current_path_weakrefs() + self.clear_current_path_state_and_set_to_none() + return + + if self.current_node.all_outputs_are_dead(): + self.clear_current_path_state_and_set_to_none() + return + + self.check_warn_on_unable_to_start_executing(function_id) + + def try_end_curr_execution(self) -> None: + """ + Check if the current executing node can be terminated, either because all outputs of the + previously executed node are dead or because it was executed in a different generation. + Will set current_node to None if successful. + """ + + assert not self.in_recording + if self.current_node is None: + return + + if self.can_start_new_generation(): + self.clear_current_path_state_and_set_to_none() + return + + if self.current_node.all_outputs_are_dead(): + self.clear_current_path_state_and_set_to_none() + + def try_end_curr_warmup(self, function_id: FunctionID) -> None: + if self.can_start_new_generation(): + self.dealloc_current_path_weakrefs() + self.current_node = None + return + + assert self.current_node is not None + if self.current_node.all_outputs_are_dead(): + self.current_node = None + return + + self.check_warn_on_unable_to_start_executing(function_id) + + def check_warn_on_unable_to_start_executing(self, function_id: FunctionID) -> None: + "Warn if we in a potential loop where we are unable to hit fast path" + if ( + function_id in self.warned_functions + or not self.in_new_torch_compile_invocation() + ): + return + + assert self.current_node is not None + existing_nodes = [ + node + for node in self.current_node._path_from_root + if node.wrapped_function.id == function_id + ] + + if len(existing_nodes) <= 1: + return + + # repeated same pattern + parents = OrderedSet( + [ + n.parent.wrapped_function.id + for n in itertools.chain(existing_nodes, (self.current_node,)) + if n.parent is not None + ] + ) + if len(parents) == len(existing_nodes): + return + + self.warned_functions.add(function_id) + warnings.warn( + "Unable to hit fast path of CUDAGraphs because outputs from a previous step " + "still require backward. Ensure backward() is invoked or detach outputs. " + "You may also call torch.compiler.cudagraph_mark_step_begin() before each model invocation." + ) + + @staticmethod + def format_dealloc_msg(stack_trace: str | None) -> str: + stack_trace = ( + stack_trace.strip() if stack_trace else "[Could not find stack trace]" + ) + return ( + "Error: accessing tensor output of CUDAGraphs that has been overwritten by a subsequent run. " + f"Stack trace: {stack_trace}. " + "To prevent overwriting, clone the tensor outside of torch.compile() " + "or call torch.compiler.cudagraph_mark_step_begin() before each model invocation." + ) + + def dealloc_current_path_weakrefs(self) -> None: + assert self.current_node is not None + # TODO: we could also allow the these weak refs to continue to be allocated, + # but that adds some complications. + + stor_stack_trace: dict[int, str | None] = {} + for node in self.current_node._path_from_root: + assert node.stack_traces is not None + assert len(node.tensor_weakrefs) == len(node.stack_traces) + for t, stack_trace in zip(node.tensor_weakrefs, node.stack_traces): + ten = None if t is None else t() + if ten is None: + continue + + torch._C._set_storage_access_error_msg( + ten, self.format_dealloc_msg(stack_trace) + ) + + # we would to enable the following assertion, but an internal model failed with a command + # that does not repro. len(node.outputs_weakrefs) == len(node.stack_traces) + # so, pessimistically assume that they might differ by doing the debug info + # loop separately from the dealloc loop + if self.disable_invalidate_aliases: + continue + + for storage_ref, stack_trace in zip( + node.outputs_weakrefs, node.stack_traces + ): + if not storage_ref: + continue + + stor_stack_trace[storage_ref.data_ptr()] = stack_trace + + deleted = OrderedSet[Any]() + for storage_ref in self.current_node.path_live_weakrefs(): + _storage_deref = storage_ref() + if _storage_deref and storage_ref.data_ptr() not in deleted: + deleted.add(storage_ref.data_ptr()) + + msg = self.format_dealloc_msg( + stor_stack_trace.get(storage_ref.data_ptr()) + ) + torch._C._free_And_Remove_DeleterFn(_storage_deref) + + if self.disable_invalidate_aliases: + continue + + torch._C._set_storage_data_ptr_access_error_msg(_storage_deref, msg) + + def clear_current_path_state_and_set_to_none(self) -> None: + assert isinstance(self.current_node, CUDAGraphNode) + self.current_node.clear_path_state() + self.current_node = None + + def apply_checkpoint_execution_state_in_allocator(self) -> None: + """ + Checkpoint the current execution state in the caching allocator so that + additional cudagraph recordings can be made respecting existent live storages. + """ + assert isinstance(self.current_node, CUDAGraphNode) + self.debug_checkpointing_counter += 1 + log.debug( + "Checkpointing cuda caching allocator state. Number of checkpoints %d", + self.debug_checkpointing_counter, + ) + + state = self.current_node.checkpointed_caching_state + device = self.current_node.device + assert state is not None and device is not None + + # currently we deallocate on instead of allowing stale recordings + stale_storages: list[int] = [] + + # remove cached tensors, otherwise they would prevent memory from being + # reclaimed in subsequent recordings + self.current_node.remove_path_cached_tensors() + live_storages_wrappers = list(self.current_node.path_live_weakrefs()) + + # path_live_weakrefs guarantees that t() will not be None + live_storages_weak_refs: list[int] = [t() for t in live_storages_wrappers] # type: ignore[misc] + ptrs_to_deallocate = self.current_node.data_ptrs_dead_since_invocation() + torch._C._cuda_setCheckpointPoolState( + device, + # pyrefly: ignore [bad-argument-type] + state, + stale_storages, + live_storages_weak_refs, + ) + + # NB: deduplicate aliased outputs + for ptr in OrderedSet(ptrs_to_deallocate): + torch._C._cuda_cudaCachingAllocator_raw_delete(ptr) + + # Now the live blocks should be exactly equal to the live storages in private pool + if config.triton.slow_path_cudagraph_asserts: + check_memory_pool( + self.device_index, self.cuda_graphs_thread_pool, live_storages_wrappers + ) + for wrapper in live_storages_wrappers: + storage_ptr = wrapper() + assert storage_ptr is not None + assert torch._C._has_Standard_Deleter(storage_ptr) + assert wrapper.data_ptr() not in ptrs_to_deallocate + + def live_cudagraph_pool_storages_in_curr_execution( + self, + ) -> list[StorageWeakRefPointer]: + if self.current_node is None: + return [] + # explicitly ignoring previous recorded outputs from past path + # path_live_weakrefs() guarantees that t() will not be None + return [t() for t in self.current_node.path_live_weakrefs()] # type: ignore[misc] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/cudagraph_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/cudagraph_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..d2c443c50cec24de9d9db72b99f6c07debb92b76 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/cudagraph_utils.py @@ -0,0 +1,533 @@ +# mypy: disallow-untyped-defs +from __future__ import annotations + +import dataclasses +from collections.abc import Callable +from enum import Enum +from typing import Any, TYPE_CHECKING, TypeVar + +import torch +from torch._dynamo.utils import counters, get_metrics_context +from torch._inductor.utils import GraphPartitionMap, InputType +from torch._subclasses.fake_tensor import get_plain_tensors, is_fake +from torch.utils._ordered_set import OrderedSet + +from .utils import is_using_cudagraph_partition + + +if TYPE_CHECKING: + from collections.abc import Sequence, Set as AbstractSet + + from torch._inductor.output_code import OutputCode + +_OC = TypeVar("_OC", bound="OutputCode") + + +cudagraphs_log = torch._logging.getArtifactLogger(__name__, "cudagraphs") +static_inputs_log = torch._logging.getArtifactLogger( + __name__, "cudagraph_static_inputs" +) + + +OutputType = list[int | torch.Tensor | None] +ModelType = Callable[[list[InputType]], OutputType] + + +class CUDAGraphPolicy: + """Pluggable policy controlling CUDA graph wrapping in Inductor's post_compile. + + Override methods to customize: + - HOW compiled functions are cudagraph-wrapped (cudagraphify) + - WHETHER inner CompiledFxGraphs should be wrapped (should_wrap) + - OUTER wrapping of compound outputs like RegionalOutputCode (wrap_output) + + Set via ``torch._inductor.config.cudagraph_policy``. When ``None`` + (the default), the existing built-in behaviour is used unchanged. + + Example usage:: + + class MyCUDAGraphPolicy(CUDAGraphPolicy): + def cudagraphify(self, model, example_inputs, static_input_idxs, **kwargs): + return my_custom_wrapper(model, example_inputs, static_input_idxs) + + + with torch._inductor.config.patch("cudagraph_policy", MyCUDAGraphPolicy()): + compiled_fn = deserialize_artifacts(...) + """ + + def cudagraphify( + self, + model: Callable[..., Any], + example_inputs: Sequence[InputType], + static_input_idxs: Sequence[int], + *, + device_index: int, + is_backward: bool, + is_inference: bool, + **kwargs: Any, + ) -> Callable[..., Any]: + """Wrap a single compiled callable with CUDA graph capture/replay. + + Called by ``cudagraph_post_compile`` for each ``CompiledFxGraph``. + The default delegates to ``compile_fx.cudagraphify`` (cudagraph_trees). + + ``example_inputs`` are the example inputs at post_compile time. + The default implementation does not forward them because + ``compile_fx.cudagraphify`` defers graph recording to the first + real call via an inner closure. Subclasses that need the + example inputs for warmup or static-input detection may use them. + + When ``config.graph_partition=True``, setting a CUDAGraphPolicy + bypasses ``cudagraph_partition_post_compile`` (which wraps each + partition individually) and routes through ``cudagraph_post_compile`` + instead, so this method wraps the *entire* callable, not individual + partitions. Subclasses that need per-partition control should + handle partitioning internally. + """ + from torch._inductor.compile_fx import cudagraphify + + return cudagraphify( + model, + static_input_idxs, + device_index=device_index, + is_backward=is_backward, + is_inference=is_inference, + **kwargs, + ) + + def should_wrap(self, compiled_graph: OutputCode) -> bool: + """Whether to apply cudagraph wrapping to this CompiledFxGraph. + + Called for each inner ``CompiledFxGraph`` during ``post_compile``. + Return ``False`` to skip wrapping (e.g. when wrapping at the outer + level via ``wrap_output`` instead). + + Default: ``True`` (wrap everything, same as current behaviour). + """ + return True + + def wrap_output(self, output_code: _OC) -> _OC: + """Optional outer-level wrapping after inner post_compile completes. + + Called by ``_compile_fx_inner``, ``BundledOutputCodeLoadable.post_compile``, + and ``FxGraphCacheLoadable.post_compile`` on the ``OutputCode`` returned + from ``post_compile``. Subclasses that only want to wrap specific + output types should check ``isinstance`` and return the input + unchanged for types they don't handle. + + Default: identity (no outer wrapping). + """ + return output_code + + +@dataclasses.dataclass(frozen=True, slots=True) +class FunctionID: + "Unique counter of a function wrapped in cudagraphify_impl" + + id: int + + +@dataclasses.dataclass(frozen=True, slots=True) +class PlaceholderInfo: + """ + A serializable version of torch.fx.Node that contains information + pertinent to placeholder stack traces. We use these in logging and error messages + related to cudagraphs, and will cache these results. + """ + + name: str + stack_trace: str | None + # This field is recursive, but never cyclic (since a node never uses itself) + users: list[PlaceholderInfo] + mutating_use_stack_trace: str | None + + +@dataclasses.dataclass(frozen=True, slots=True) +class WrappedFunction: + """ + Represents a function that you want to record for CUDA graph replay, + with a little more metadata so we can identify if we have an applicable + CUDA graph in our CUDA graph tree for it. + """ + + model: Callable[..., Any] + static_input_idxs: Sequence[int] + id: FunctionID + constants: tuple[torch.Tensor, ...] + placeholders: Sequence[PlaceholderInfo] + mutated_input_idxs: Sequence[int] + + +def get_mutating_use_stack_trace_from_node( + placeholder_node: torch.fx.Node, +) -> str | None: + # reinplaced uses might have a single, non-copy_ use + if len(placeholder_node.users) == 1: + return next(iter(placeholder_node.users)).meta.get("stack_trace", None) + + for use in placeholder_node.users: + if use.target is torch.ops.aten.copy_.default: + if stack_trace := use.meta.get("stack_trace", None): + return stack_trace + + return None + + +def get_mutating_use_stack_trace(placeholder_info: PlaceholderInfo) -> str | None: + return placeholder_info.mutating_use_stack_trace + + +def to_placeholder_info(placeholder_node: torch.fx.Node) -> PlaceholderInfo: + name = placeholder_node.name + stack_trace = placeholder_node.meta.get("stack_trace", None) + users = [] + mutating_use_stack_trace = None + # Only recurse to users once, since we only care about user's stack traces + if placeholder_node.op == "placeholder": + users = [to_placeholder_info(i) for i in placeholder_node.users] + mutating_use_stack_trace = get_mutating_use_stack_trace_from_node( + placeholder_node + ) + + return PlaceholderInfo(name, stack_trace, users, mutating_use_stack_trace) + + +def get_placeholder_info(graph: torch.fx.Graph) -> list[PlaceholderInfo]: + return [ + to_placeholder_info(node) for node in graph.nodes if node.op == "placeholder" + ] + + +def format_default_skip_message(reason: str) -> str: + return f"skipping cudagraphs due to {reason}" + + +def get_mutation_stack_trace( + placeholders: Sequence[PlaceholderInfo], + mutation_indices: AbstractSet[int] | Sequence[int], +) -> str: + stack_trace: str | None = "" + + for idx in mutation_indices: + placeholder = placeholders[idx] + if stack_trace := get_mutating_use_stack_trace(placeholder): + break + + msg = format_default_skip_message( + f"mutated inputs ({len(mutation_indices)} instances)" + ) + if stack_trace: + return f"{msg}. Found from : \n {stack_trace}" + + return msg + + +def check_for_mutation( + func: WrappedFunction, + inputs: list[InputType], + is_cuda_graph_recorded_tensor: Callable[[torch.Tensor], bool], +) -> str | None: + # doesn't work for non-trees because the warmup run would apply mutation twice + if torch._inductor.config.triton.cudagraph_trees: + # checking if mutation is only on parameters/static inputs + mutation_indices: Sequence[int] = [ + idx + for idx in func.mutated_input_idxs + if not ( + idx in func.static_input_idxs + or is_cuda_graph_recorded_tensor(inputs[idx]) # type: ignore[arg-type] + ) + ] + else: + mutation_indices = func.mutated_input_idxs + + static_inputs_log.debug( + "check mutation static input indices: %s", func.static_input_idxs + ) + static_inputs_log.debug("check mutation mutation indices: %s", mutation_indices) + + return ( + get_mutation_stack_trace(func.placeholders, mutation_indices) + if mutation_indices + else None + ) + + +def _get_use_stack_trace(node: torch.fx.Node) -> str | None: + for use in node.users: + if stack_trace := use.meta.get("stack_trace", None): + return stack_trace + return None + + +def check_multiple_devices_or_any_cpu_nodes( + device_node_mapping: dict[torch.device, torch.fx.Node], +) -> str | None: + # meta tensors are supported since there is no compute + device_node_mapping.pop(torch.device("meta"), None) + + # dynamo cudagraph does not support graph partition + if is_using_cudagraph_partition(): + # graph partition supports splitting on cpu op. So we can ignore cpu nodes. + device_node_mapping.pop(torch.device("cpu"), None) + + if cpu_node := device_node_mapping.get(torch.device("cpu")): + msg = f"cpu device ({cpu_node.name})" + if stack_trace := _get_use_stack_trace(cpu_node): + return format_default_skip_message(f"{msg}. Found from : \n {stack_trace}") + + return format_default_skip_message(msg) + + if ( + len(device_node_mapping) == 1 + and next(iter(device_node_mapping.keys())).type == "cuda" + ): + return None + + keys_repr = (repr(key) for key in device_node_mapping) + return format_default_skip_message(f"multiple devices: {', '.join(keys_repr)}") + + +def check_lowering_disable_cudagraph( + device_node_mapping: dict[torch.device, torch.fx.Node], +) -> str | None: + return check_multiple_devices_or_any_cpu_nodes(device_node_mapping) + + +def log_cudagraph_skip_and_bump_counter(msg: str) -> None: + cudagraphs_log.warning(msg) + counters["inductor"]["cudagraph_skips"] += 1 + + if torch._inductor.config.triton.cudagraph_or_error: + raise RuntimeError(msg) + + metrics_context = get_metrics_context() + if metrics_context.in_progress(): + metrics_context.set("cudagraph_skip_reason", msg, overwrite=True) + + +@dataclasses.dataclass +class BoxedDeviceIndex: + value: int | None + + def set(self, device_idx: int | None) -> None: + assert device_idx is None or isinstance(device_idx, int) + self.value = device_idx + + +def check_for_mutation_ignore_cuda_graph_managed_tensor( + gm: torch.fx.GraphModule, + mutated_inputs: OrderedSet[str], + mutated_input_idxs: OrderedSet[int], + static_input_idxs: Sequence[int], +) -> str | None: + default_msg = format_default_skip_message("mutated inputs") + + # doesn't work for non-trees because the warmup run would apply mutation twice + if torch._inductor.config.triton.cudagraph_trees: + unique_idxs = OrderedSet(static_input_idxs) + # checking if mutation is only on parameters/static inputs + mutation_indices = [idx for idx in mutated_input_idxs if idx not in unique_idxs] + has_mutation = len(mutation_indices) != 0 + if not has_mutation: + return None + placeholders = get_placeholder_info(gm.graph) + return get_mutation_stack_trace(placeholders, mutation_indices) + + else: + has_mutation = len(mutated_inputs) != 0 + return None if not has_mutation else default_msg + + +def get_placeholder_stack_trace(placeholder: PlaceholderInfo) -> str | None: + """ + Gets the first non-empty stack trace of a placeholder or its users. + """ + if placeholder.stack_trace: + return placeholder.stack_trace + + for user in placeholder.users: + if user.stack_trace: + return user.stack_trace + + return None + + +class CheckInvariantStatus(Enum): + # Check invariant succeeded + SUCCESS = 1 + + # Previously managed data pointers are not stable + CudagraphManagedIdxMismatch = 2 + + # Static tensor input addresses are not stable + StaticInputIdxMismatch = 3 + + # Expected dead indices before graph are live + ExpectedDeadIndicesBeforeGraphMismatch = 4 + + def __str__(self) -> str: + if self.name == "CudagraphManagedIdxMismatch": + return "cudagraph managed tensor data pointer changed" + elif self.name == "StaticInputIdxMismatch": + return "static input data pointer changed" + elif self.name == "ExpectedDeadIndicesBeforeGraphMismatch": + return "expected dead indices before graph are live" + else: + return f"{self.name}: {self.value}" + + +def log_data_ptr_mismatch( + placeholders: Sequence[PlaceholderInfo], + inputs: list[InputType], + recorded_data_ptr: Sequence[int | None], + target_idxs: Sequence[int], + mismatch: CheckInvariantStatus, +) -> str: + """ + Logs the mismatch between input data pointers and recorded data pointers. + This checks only idxs in target_idxs. + """ + assert len(inputs) == len(recorded_data_ptr) and len(inputs) == len(placeholders), ( + "length mismatch between inputs, recorded_data_ptr, and placeholders" + ) + + t_tensors = [inputs[i] for i in target_idxs] + t_data_ptrs = [recorded_data_ptr[i] for i in target_idxs] + error_msg = f"{mismatch}.\n" + for i, (tensor, data_ptr) in enumerate(zip(t_tensors, t_data_ptrs)): + assert isinstance(tensor, torch.Tensor) + index = target_idxs[i] + if tensor.data_ptr() != data_ptr: + placeholder = placeholders[index] + error_msg = ( + f"{error_msg}input name: {placeholder.name}. " + f"data pointer changed from {data_ptr} to {tensor.data_ptr()}. " + f"input stack trace: {get_placeholder_stack_trace(placeholder)}\n" + ) + return error_msg + + +def maybe_warning_due_to_dynamic_shape( + fn_cache: dict[tuple[int, ...], Callable[..., Any]], + new_int_key: Any, +) -> bool: + num_cudagraphs = len(fn_cache.keys()) + 1 + + def warn_msg() -> str: + return ( + "CUDAGraph supports dynamic shapes by recording a new graph for each " + "distinct input size. Recording too many CUDAGraphs may lead to " + f"extra overhead. We have observed {num_cudagraphs} distinct sizes. " + "Please consider the following options for better performance: " + "a) padding inputs to a few fixed number of shapes; or b) set " + "torch._inductor.config.triton.cudagraph_skip_dynamic_graphs=True. " + "Set torch._inductor.config.triton.cudagraph_dynamic_shape_warn_limit=None " + "to silence this warning." + ) + + if ( + torch._inductor.config.triton.cudagraph_dynamic_shape_warn_limit + and num_cudagraphs + > torch._inductor.config.triton.cudagraph_dynamic_shape_warn_limit + ): + cudagraphs_log.warning(warn_msg()) + return True + + return False + + +@dataclasses.dataclass(frozen=True) +class CudagraphCachedInfo: + """ + Info needed to realign inputs + """ + + placeholders: Sequence[PlaceholderInfo] + stack_traces: list[str | None] + cudagraph_fail_reasons: list[str] + + +@dataclasses.dataclass(frozen=True) +class CudagraphMetadata: + """ + Metadata for recording a CUDA graph. + """ + + placeholders: Sequence[PlaceholderInfo] + static_input_idxs: OrderedSet[int] + mutated_input_idxs: OrderedSet[int] + stack_traces: list[str | None] + constants: dict[str, torch.Tensor] + + +def get_partition_cudagraph_metadata( + partition_map: GraphPartitionMap, + metadata: CudagraphMetadata, +) -> CudagraphMetadata: + """ + Convert the cudagraph metadata at the graph level to the graph partition level, + given the graph partition info (i.e., mapping from partition input/output index + to graph input/output index). + """ + + partition_placeholders = [] + partition_static_input_idxs: OrderedSet[int] = OrderedSet() + partition_mutated_input_idxs: OrderedSet[int] = OrderedSet() + for partition_input_idx, graph_input_idx in enumerate( + partition_map.input_index_mapping + ): + if graph_input_idx in metadata.static_input_idxs: + partition_static_input_idxs.add(partition_input_idx) + + if graph_input_idx in metadata.mutated_input_idxs: + partition_mutated_input_idxs.add(partition_input_idx) + + if graph_input_idx is not None: + placeholder = metadata.placeholders[graph_input_idx] + else: + # create a dummy placeholder info since this partition input is not a graph input + placeholder = PlaceholderInfo( + name=f"partition_{partition_map.id}_placeholder_{partition_input_idx}", + stack_trace=None, + users=[], + mutating_use_stack_trace=None, + ) + partition_placeholders.append(placeholder) + + partition_stack_traces = [] + for graph_output_idx in partition_map.output_index_mapping: + if graph_output_idx is not None: + partition_stack_traces.append(metadata.stack_traces[graph_output_idx]) + else: + partition_stack_traces.append(None) + + partition_constants = { + name: metadata.constants[name] for name in partition_map.constant_names + } + + return CudagraphMetadata( + partition_placeholders, + partition_static_input_idxs, + partition_mutated_input_idxs, + partition_stack_traces, + partition_constants, + ) + + +def collect_cuda_data_ptrs(obj: object) -> OrderedSet[int]: + """Debug helper that collects the data pointers of all CUDA tensors in the object.""" + if not isinstance(obj, torch.Tensor): + return OrderedSet() + + ptrs: OrderedSet[int] = OrderedSet() + for base in get_plain_tensors(obj, out=[]): + if type(base) is not torch.Tensor: + continue + if is_fake(base) or base.is_meta or base.device.type != "cuda": + continue + try: + ptrs.add(base.data_ptr()) + except Exception: + pass + return ptrs diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/custom_graph_pass.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/custom_graph_pass.py new file mode 100644 index 0000000000000000000000000000000000000000..31b842133dc867b575cdce19dffdfca3121ec0a3 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/custom_graph_pass.py @@ -0,0 +1,285 @@ +import hashlib +from abc import ABC, abstractmethod +from collections.abc import Callable, Sequence +from functools import lru_cache +from typing import Any, TYPE_CHECKING, TypeAlias + +import torch.fx.graph + + +if TYPE_CHECKING: + from torch._functorch.partitioners import NodeInfo + + +class CustomGraphPass(ABC): + """ + Implement this interface for custom Graph passes: + + 1) The __call__() method contains the implementation of the custom pass. + + 2) The uuid() method enables inductor to cache compiled graphs when your custom + passes are applied. This method can return any identifier as long as it uniquely + identifies your implementation (and can be pickled). The caching logic includes this + identifier in its key calculation, i.e., any new value will effectively invalidate + existing entries. We expect custom passes would typically depend purely on the + textual representation of the implementation. In that case, we recommend using the + 'get_hash_for_files' helper below to compute a unique hash from the contents of a + static list of source files, i.e., the source(s) containing the custom pass + implementation. That approach ensures that any change to the implementation will + mean a new uuid. + + ** IMPORTANT ** If your custom pass's behavior depends on some external state, then + you'll need to implement something more complicated (or disable caching). + + EXAMPLE: + + class MyCustomGraphPass(CustomGraphPass): + def __call__(self, graph: torch.fx.graph.Graph) -> None: + # my custom graph optimization pass + # ... + + def uuid(self) -> Optional[Any]: + return get_hash_for_files((__file__,)) + + """ + + @abstractmethod + def __call__(self, graph: torch.fx.graph.Graph) -> None: + """ + Implementation of the custom pass. + """ + + @abstractmethod + def uuid(self) -> Any | None: + """ + Return an ID to uniquely identify your custom pass implementation. Return None + to skip inductor code caching entirely. + """ + + +class CustomGraphModulePass(ABC): + """ + Implement this interface for custom Graph passes: + + 1) The __call__() method contains the implementation of the custom pass. + + 2) The uuid() method enables inductor to cache compiled graphs when your custom + passes are applied. This method can return any identifier as long as it uniquely + identifies your implementation (and can be pickled). The caching logic includes this + identifier in its key calculation, i.e., any new value will effectively invalidate + existing entries. We expect custom passes would typically depend purely on the + textual representation of the implementation. In that case, we recommend using the + 'get_hash_for_files' helper below to compute a unique hash from the contents of a + static list of source files, i.e., the source(s) containing the custom pass + implementation. That approach ensures that any change to the implementation will + mean a new uuid. + """ + + @abstractmethod + def __call__(self, gm: torch.fx.GraphModule) -> None: + """ + Implementation of the custom pass. + """ + + @abstractmethod + def uuid(self) -> Any | None: + """ + Return an ID to uniquely identify your custom pass implementation. Return None + to skip inductor code caching entirely. + """ + + +class CustomInferenceAwareGraphPass(CustomGraphPass): + """ + Implement this interface for custom inference aware Graph passes. + + """ + + @abstractmethod + def __call__(self, graph: torch.fx.graph.Graph, is_inference: bool) -> None: + """ + Implementation of the custom pass. + """ + + +CustomGraphPassType: TypeAlias = ( + CustomGraphPass | Callable[[torch.fx.graph.Graph], None] | None +) + + +@lru_cache(1) +def get_hash_for_files(paths: tuple[str, ...], extra: str = "") -> bytes: + """ + Helper to compute a unique string by hashing the contents of a list of files. + """ + hasher = hashlib.sha256() + hasher.update(extra.encode("utf-8")) + for path in paths: + with open(path, "rb") as f: + hasher.update(f.read()) + return hasher.digest() + + +class CustomPartitionerFn(ABC): + """ + Implement this interface for custom partitioner: + + 1) The __call__() method contains the implementation of the custom partitioner. + + 2) The uuid() method enables inductor to cache compiled graphs when your custom + partitioner are applied. This method can return any identifier as long as it uniquely + identifies your implementation (and can be pickled). The caching logic includes this + identifier in its key calculation, i.e., any new value will effectively invalidate + existing entries. We expect custom partitioner would typically depend purely on the + textual representation of the implementation. In that case, we recommend using the + 'get_hash_for_files' helper below to compute a unique hash from the contents of a + static list of source files, i.e., the source(s) containing the custom partitioner + implementation. That approach ensures that any change to the implementation will + mean a new uuid. + + EXAMPLE: + + from torch._inductor.custom_graph_pass import get_hash_for_files + + class MyCustomPartitionerFn(CustomPartitionerFn): + def __call__( + self, + gm: torch.fx.GraphModule, + joint_inputs: Sequence[object], + **kwargs: Any + ) -> tuple[torch.fx.GraphModule, torch.fx.GraphModule]: + # my custom partitioner implementation + # ... + + def uuid(self) -> Optional[Any]: + return get_hash_for_files((__file__,)) + + """ + + @abstractmethod + def __call__( + self, gm: torch.fx.GraphModule, joint_inputs: Sequence[object], **kwargs: Any + ) -> tuple[torch.fx.GraphModule, torch.fx.GraphModule]: + """ + Implementation of the custom partitioner. + """ + + @abstractmethod + def uuid(self) -> Any | None: + """ + Return an ID to uniquely identify your custom partitioner implementation. + Return None to skip inductor code caching entirely. + """ + + +CustomPartitionerFnType: TypeAlias = CustomPartitionerFn | None + + +class CustomRuntimeEstimator(ABC): + """ + Implement this interface for custom runtime estimators: + + 1) The __call__() method contains the implementation of the runtime estimation. + + 2) The uuid() method enables AOTAutograd to cache compiled graphs when your custom + runtime estimator is used. This method can return any identifier as long as it uniquely + identifies your implementation (and can be pickled). The caching logic includes this + identifier in its key calculation, i.e., any new value will effectively invalidate + existing entries. We expect custom runtime estimators would typically depend purely on the + textual representation of the implementation. In that case, we recommend using the + 'get_hash_for_files' helper below to compute a unique hash from the contents of a + static list of source files, i.e., the source(s) containing the custom runtime estimator + implementation. That approach ensures that any change to the implementation will + mean a new uuid. + + ** IMPORTANT ** If your custom runtime estimator's behavior depends on some external state, + then you'll need to implement something more complicated (or disable caching). + + EXAMPLE: + + class MyCustomRuntimeEstimator(CustomRuntimeEstimator): + def __call__(self, node: fx.Node) -> float: + # my custom runtime estimation logic + return estimated_runtime + + def uuid(self) -> Optional[Any]: + return get_hash_for_files((__file__,)) + """ + + @abstractmethod + def __call__(self, node: "torch.fx.Node") -> float: + """ + Implementation of the custom runtime estimator. + + Args: + node: An fx.Node object whose runtime is to be estimated. + + Returns: + float: The estimated runtime for the node. + """ + + @abstractmethod + def uuid(self) -> Any | None: + """ + Return an ID to uniquely identify your custom runtime estimator implementation. + Return None to skip AOTAutograd caching entirely. + """ + + +class CustomKnapsackSolver(ABC): + """ + Implement this interface for custom knapsack solvers: + + 1) The __call__() method contains the implementation of the knapsack solver. + + 2) The uuid() method enables AOTAutograd to cache compiled graphs when your custom + knapsack solver is used. This method can return any identifier as long as it uniquely + identifies your implementation (and can be pickled). The caching logic includes this + identifier in its key calculation, i.e., any new value will effectively invalidate + existing entries. We expect custom knapsack solvers would typically depend purely on the + textual representation of the implementation. In that case, we recommend using the + 'get_hash_for_files' helper below to compute a unique hash from the contents of a + static list of source files, i.e., the source(s) containing the custom knapsack solver + implementation. That approach ensures that any change to the implementation will + mean a new uuid. + + ** IMPORTANT ** If your custom knapsack solver's behavior depends on some external state, + then you'll need to implement something more complicated (or disable caching). + + EXAMPLE: + + class MyCustomKnapsackSolver(CustomKnapsackSolver): + def __call__( + self, + memory: list[float], + joint_graph: fx.Graph, + max_memory: float, + node_info: NodeInfo, + all_recomputable_banned_nodes: list[fx.Node], + ) -> tuple[list[int], list[int]]: + # my custom knapsack solver logic + return saved_node_idx, recomp_node_idx + + def uuid(self) -> Optional[Any]: + return get_hash_for_files((__file__,)) + """ + + @abstractmethod + def __call__( + self, + memory: list[float], + joint_graph: "torch.fx.Graph", + max_memory: float, + node_info: "NodeInfo", + all_recomputable_banned_nodes: list["torch.fx.Node"], + ) -> tuple[list[int], list[int]]: + """ + Implementation of the custom knapsack solver. + """ + + @abstractmethod + def uuid(self) -> Any | None: + """ + Return an ID to uniquely identify your custom knapsack solver implementation. + Return None to skip AOTAutograd caching entirely. + """ diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/debug.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/debug.py new file mode 100644 index 0000000000000000000000000000000000000000..3187cbe8c802356c20bb5c144ceb9dd24e788b32 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/debug.py @@ -0,0 +1,1344 @@ +import collections +import contextlib +import copy +import dataclasses +import functools +import io +import itertools +import json +import logging +import os +import os.path +import pickle +import pstats +import shutil +import tempfile +import traceback +from collections.abc import Callable, Iterator, Sequence +from typing import Any, IO +from unittest.mock import patch + +import torch +from functorch.compile import draw_graph, get_aot_graph_name, get_graph_being_compiled +from torch import fx +from torch._dynamo.repro.after_aot import save_graph_repro +from torch._dynamo.utils import get_debug_dir +from torch._inductor import utils +from torch._logging import getArtifactLogger +from torch._logging._internal import trace_structured +from torch._utils_internal import signpost_event +from torch.fx.graph_module import GraphModule +from torch.fx.passes.shape_prop import _extract_tensor_metadata, TensorMetadata +from torch.fx.passes.tools_common import legalize_graph +from torch.types import FileLike +from torch.utils._ordered_set import OrderedSet +from torch.utils._pytree import tree_map + +from . import config, ir # noqa: F811, this is needed +from .ir import ExternKernel +from .scheduler import ( + BaseSchedulerNode, + FusedSchedulerNode, + NopKernelSchedulerNode, + OutputNode, + SchedulerNode, +) +from .virtualized import V + + +log = logging.getLogger(__name__) + +# Graph execution tracking for debugging +GRAPH_EXECUTION_ORDER: list[dict[str, object]] | None = None +RECORD_GRAPH_EXECUTION: bool = False +GRAPH_COMPILE_IDS: dict[int, str | None] | None = None + +ir_pre_fusion_log = getArtifactLogger(__name__, "ir_pre_fusion") +ir_post_fusion_log = getArtifactLogger(__name__, "ir_post_fusion") +SchedulerNodeList = list[Any] +BufMeta = collections.namedtuple("BufMeta", ["name", "n_origin"]) +GRAPHVIZ_COMMAND_SCALABLE = ["dot", "-Gnslimit=2", "-Gnslimit1=2", "-Gmaxiter=5000"] + + +@functools.cache +def has_dot() -> bool: + return shutil.which("dot") is not None + + +def draw_buffers( + nodes: list[BaseSchedulerNode], + print_graph: bool = False, + fname: str | None = None, +) -> None: + """ + Draw a graph in fname.svg. + """ + if not has_dot(): + log.warning("draw_buffers() requires `graphviz` package") + return + + if fname is None: + fname = get_graph_being_compiled() + + graph = create_fx_from_snodes(nodes) + + for node in graph.nodes: + if "fusion_meta" not in node.meta: + continue + group = node.meta["fusion_meta"].group + if isinstance(group, tuple): + if isinstance(group[1], int): + group = (group[1],) + else: + group = group[1] + elif isinstance(group, str): + # extern / template / nop nodes store a string like "extern" + # as the group instead of a shape tuple. Extract the real + # output shape from the scheduler node's first output buffer. + try: + snode = node.meta["fusion_meta"].snode + size = snode.get_outputs()[0].node.maybe_get_size() + group = tuple(size) if size else () + except Exception: + group = () + + # gather meta data + dtype = None + if isinstance(node, ir.ComputedBuffer): + dtype = node.data.dtype + + metadata = TensorMetadata(group, dtype, None, None, None, None, None) # type: ignore[arg-type] + + node.meta["tensor_meta"] = metadata + + if print_graph: + print(graph) + + gm = GraphModule({}, graph) + legalize_graph(gm) + gm.graph.lint() + draw_graph( + gm, fname, clear_meta=False, dot_graph_shape=config.trace.dot_graph_shape + ) + + +def create_fx_from_snodes(snodes: list[BaseSchedulerNode]) -> fx.Graph: + """ + Creates a FX Graph from a list of SchedulerNode objects. + """ + + def get_fake_func(name: str) -> Callable[..., int]: + def func1(*args: Any) -> int: + return 0 + + func1.__name__ = name + return func1 + + FusionMeta = collections.namedtuple("FusionMeta", ["group", "snode", "type"]) + + buf_to_fx_node = {} + node_to_fx_node = {} + graph = torch.fx.Graph() + first_node = None + + outputs = [] + group: Any = None + # create call_function node for each Buffer and Kernel + # pyrefly: ignore [bad-assignment] + for snode in snodes: + if snode.is_extern(): + node_type = "extern" + group = node_type + elif snode.is_template(): + node_type = "template" + group = node_type + elif isinstance(snode, NopKernelSchedulerNode): + node_type = "nop" + group = node_type + elif isinstance(snode, SchedulerNode): + node_type = "compute" + group = snode.group + elif isinstance(snode, FusedSchedulerNode): + node_type = "fused" + group = snode.group + else: + raise RuntimeError("Unknown node type") + + fused_name = torch._inductor.utils.get_fused_kernel_name( + snode.get_nodes(), "original_aten" + ) + func_name = f"{node_type}: {fused_name}" + node_func = get_fake_func(func_name) + kwargs = {} + if hasattr(snode, "get_device"): + kwargs = {"device": snode.get_device()} + fx_node = graph.call_function(node_func, args=(), kwargs=kwargs) # type: ignore[arg-type] + + def in_output(snode: BaseSchedulerNode | FusedSchedulerNode) -> bool: + if isinstance(snode, FusedSchedulerNode): + return any(in_output(x) for x in snode.snodes) + return any( + isinstance(user.node, OutputNode) + for buf in snode.get_outputs() + for user in buf.users + ) + + if in_output(snode): + outputs.append(fx_node) + name = snode.get_name() + fx_node.name = name + + fx_node.meta["fusion_meta"] = FusionMeta(group, snode, node_type) + + node_to_fx_node[name] = fx_node + for buf in snode.get_outputs(): + buf_to_fx_node[buf.get_name()] = fx_node + + if first_node is None: + first_node = fx_node + + # create edges between nodes + for snode in snodes: + name = snode.get_name() + deps = snode.read_writes.reads + + fx_node = node_to_fx_node[name] + new_args = [] + for dep in deps: + if dep.name in buf_to_fx_node: + dep_node = buf_to_fx_node[dep.name] + else: + with graph.inserting_before(first_node): + dep_node = graph.placeholder(dep.name) + buf_to_fx_node[dep.name] = dep_node + if dep_node == fx_node: # to avoid cycles + continue + new_args.append(dep_node) + + fx_node.args = tuple(new_args) + + graph.output(outputs[0] if len(outputs) == 1 else tuple(outputs)) + return graph + + +def update_orig_fx_node_name_to_buf_name( + nodes: SchedulerNodeList | None, + node_name_to_buf_name: dict[str, str], + parent_buf_name: str | None = None, + n_origins: int = 0, +) -> None: + if nodes is None: + return + for node in nodes: + # for FusedSchedulerNode, traverse recursively into get_nodes() + buf_name = node.get_name() + children_nodes = node.get_nodes() + if children_nodes is not None and len(children_nodes) > 1: + update_orig_fx_node_name_to_buf_name( + children_nodes, + node_name_to_buf_name, + buf_name if parent_buf_name is None else parent_buf_name, + ) + continue + else: + # pyrefly: ignore [bad-argument-type, unsupported-operation] + assert len(children_nodes) == 1 and children_nodes[0] == node + + ir_node = node.node + if ir_node is None or ir_node.origins is None: + continue + for origin in ir_node.origins: + node_name = origin.name + # when buf1 and buf2 both have origin=node1 + # we draw node1 according to buf1 + if node_name not in node_name_to_buf_name: + node_name_to_buf_name[node_name] = ( + buf_name if parent_buf_name is None else parent_buf_name + ) + + +def get_node_name_to_buf_meta( + node_name_to_buf_name: dict[str, str], +) -> dict[str, BufMeta]: + buf_name_to_n_node = {} + for node_name, buf_name in node_name_to_buf_name.items(): + if buf_name not in buf_name_to_n_node: + buf_name_to_n_node[buf_name] = OrderedSet([node_name]) + else: + # pyrefly: ignore [missing-attribute] + buf_name_to_n_node[buf_name].add(node_name) + + node_name_to_buf_meta = {} + for node_name, buf_name in node_name_to_buf_name.items(): + n_node = len(buf_name_to_n_node[buf_name]) + node_name_to_buf_meta[node_name] = BufMeta(buf_name, n_node) + return node_name_to_buf_meta + + +def annotate_orig_fx_with_snodes( + gm: torch.fx.GraphModule, + snodes: SchedulerNodeList, +) -> None: + """ + Creates a FX Graph from a list of SchedulerNode objects. + """ + node_name_to_buf_name: dict[str, str] = {} + update_orig_fx_node_name_to_buf_name(snodes, node_name_to_buf_name) + if node_name_to_buf_name is None: + return + node_name_to_buf_meta = get_node_name_to_buf_meta(node_name_to_buf_name) + for node in gm.graph.nodes: + if node.name in node_name_to_buf_meta: + node.meta["buf_meta"] = node_name_to_buf_meta.get(node.name) + + +@contextlib.contextmanager +def enable_aot_logging() -> Iterator[None]: + compile_debug = os.environ.get("TORCH_COMPILE_DEBUG", "0") == "1" + + import torch._functorch.aot_autograd + + log = logging.getLogger(torch._functorch.aot_autograd.__name__) + + stack = contextlib.ExitStack() + if not compile_debug: + try: + yield + finally: + stack.close() + return + + # Enable all graphs to be logged to a file by setting the flags to True + # and the log level of the file logger to DEBUG + stack.enter_context(patch("functorch.compile.config.debug_partitioner", True)) + + path = os.path.join(get_debug_dir(), "torchinductor") + os.makedirs(path, exist_ok=True) + + fh = logging.FileHandler( + os.path.join( + path, + f"aot_{get_aot_graph_name()}_debug.log", + ) + ) + fh.setLevel(logging.DEBUG) + fh.setFormatter( + logging.Formatter("[%(filename)s:%(lineno)d %(levelname)s] %(message)s") + ) + log.addHandler(fh) + try: + yield + finally: + log.removeHandler(fh) + stack.close() + + +# Used for provenance tracking +# They are not stored in DebugContext because they are not set in +# _inductor_triton_kernel_to_post_grad_node_info's Debug Context +_inductor_post_to_pre_grad_nodes: dict[str, dict[str, list[str]]] = {} +_inductor_triton_kernel_to_post_grad_node_info: dict[str, list[str]] = {} +_pre_grad_graph_id: int | None = None +_inductor_pre_grad_node_stack_trace: dict[str, str] = {} +_inductor_kernel_stack_trace: dict[str, list[str]] = {} +_inductor_kernel_provenance_debug_handle: int = 0 + + +def reset_inductor_kernel_provenance_debug_handle() -> None: + global _inductor_kernel_provenance_debug_handle + _inductor_kernel_provenance_debug_handle = 0 + + +@contextlib.contextmanager +def reset_provenance_globals() -> Iterator[None]: + """Context manager that resets provenance tracking globals upon entering + and restores their original values when exiting.""" + global _pre_grad_graph_id + global _inductor_post_to_pre_grad_nodes + global _inductor_triton_kernel_to_post_grad_node_info + global _inductor_pre_grad_node_stack_trace + global _inductor_kernel_stack_trace + global _inductor_kernel_provenance_debug_handle + + # Store original values + original_pre_grad_graph_id = _pre_grad_graph_id + original_post_to_pre_grad_nodes = _inductor_post_to_pre_grad_nodes.copy() + original_triton_kernel_to_post_grad_node_info = ( + _inductor_triton_kernel_to_post_grad_node_info.copy() + ) + original_inductor_pre_grad_node_stack_trace = ( + _inductor_pre_grad_node_stack_trace.copy() + ) + original_inductor_kernel_stack_trace = _inductor_kernel_stack_trace.copy() + original_inductor_kernel_provenance_debug_handle = ( + _inductor_kernel_provenance_debug_handle + ) + + # Reset to default values + _pre_grad_graph_id = -1 + _inductor_post_to_pre_grad_nodes = {} + _inductor_triton_kernel_to_post_grad_node_info = {} + _inductor_pre_grad_node_stack_trace = {} + _inductor_kernel_stack_trace = {} + _inductor_kernel_provenance_debug_handle = 0 + + try: + yield + finally: + # Restore original values + _pre_grad_graph_id = original_pre_grad_graph_id + _inductor_post_to_pre_grad_nodes = original_post_to_pre_grad_nodes + _inductor_triton_kernel_to_post_grad_node_info = ( + original_triton_kernel_to_post_grad_node_info + ) + _inductor_kernel_stack_trace = original_inductor_kernel_stack_trace + _inductor_pre_grad_node_stack_trace = ( + original_inductor_pre_grad_node_stack_trace + ) + _inductor_kernel_provenance_debug_handle = ( + original_inductor_kernel_provenance_debug_handle + ) + + +class DebugContext: + _counter = itertools.count() + + @staticmethod + def create_debug_dir(folder_name: str) -> str | None: + debug_dir = config.trace.debug_dir or get_debug_dir() + for n in DebugContext._counter: + dirname = os.path.join( + debug_dir, + "torchinductor", + f"{folder_name}.{n}", + ) + if not os.path.exists(dirname): + os.makedirs(dirname) + return dirname + return None + + def __init__(self) -> None: + self._prof = None + self._path = None + self._stack = contextlib.ExitStack() + + def copy(self, new_path: str) -> None: + if not self._path: + return + assert new_path.endswith(".debug"), new_path + from filelock import FileLock + + try: + with FileLock(f"{new_path}.lock"): + if os.path.exists(new_path): + shutil.rmtree(new_path) + shutil.copytree(self._path, new_path) + except OSError: + log.warning( + "Failed to copy debug files from %s to %s", self._path, new_path + ) + + def fopen( + self, + filename: str, + write_mode: str = "w", + *args: Any, + **kwargs: Any, + ) -> IO[Any]: + assert self._path + return open(os.path.join(self._path, filename), write_mode, *args, **kwargs) + + @contextlib.contextmanager + def fopen_context( + self, + filename: str, + write_mode: str = "w", + *args: Any, + **kwargs: Any, + ) -> Iterator[IO[Any]]: + assert self._path + with open(os.path.join(self._path, filename), write_mode, *args, **kwargs) as f: + yield f + + def filename(self, suffix: str) -> str: + assert self._path + return os.path.join(self._path, suffix) + + def upload_tar(self) -> None: + if config.trace.upload_tar is not None: + import tarfile + + assert self._path + tar_file = os.path.join( + self._path, f"{os.path.basename(self._path)}.tar.gz" + ) + with tarfile.open(tar_file, "w:gz") as tar: + tar.add(self._path, arcname=os.path.basename(self._path)) + config.trace.upload_tar(tar_file) + + def __enter__(self) -> None: + if config.debug: + log = logging.getLogger("torch._dynamo") + prev_level = log.level + log.setLevel(logging.DEBUG) + + def reset_log_level(level: Any) -> None: + log.setLevel(level) + + self._stack.callback(reset_log_level, prev_level) + + self._stack.enter_context(V.set_debug_handler(self)) + + if not config.trace.enabled: + return + + self._path = self.create_debug_dir(get_aot_graph_name()) # type: ignore[assignment] + + if config.trace.debug_log: + self._setup_log_capture("debug.log", logging.DEBUG) + if config.trace.info_log: + self._setup_log_capture("info.log", logging.INFO) + + def _setup_log_capture( + self, + filename: str, + level: int, + ) -> None: + log = logging.getLogger("torch._inductor") + fd = self._stack.enter_context(self.fopen(filename)) + ch = logging.StreamHandler(fd) + ch.setLevel(level) + ch.setFormatter( + logging.Formatter("[%(filename)s:%(lineno)d %(levelname)s] %(message)s") + ) + log.addHandler(ch) + log.setLevel(min(log.level, level)) + self._stack.callback(log.removeHandler, ch) + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: Any | None, + ) -> None: + if self._prof: + self._prof.disable() + self._save_profile_data() + + if self._path: + self.upload_tar() + log.warning("%s debug trace: %s", get_graph_being_compiled(), self._path) + self._stack.close() + + def _save_profile_data(self) -> None: + assert self._prof + self._prof.dump_stats(self.filename("compile.prof")) + with self.fopen("compile.stats") as fd: + stats = pstats.Stats(self._prof, stream=fd) + stats.strip_dirs() + stats.sort_stats("cumtime") + stats.print_stats(100) + stats.sort_stats("tottime") + stats.print_stats(100) + + def __getattr__(self, name: str) -> Callable[..., None] | None: + if config.trace.enabled and getattr(config.trace, name): + try: + return getattr(DebugFormatter(self), name) + except Exception: + log.warning("Ignoring exception in debug code", exc_info=True) + return None + else: + + def ignored(*args: Any, **kwargs: Any) -> None: + pass + + return ignored + + +class DebugFormatter: + def __init__(self, handler: DebugContext) -> None: + self.fopen = handler.fopen + self.fopen_context = handler.fopen_context + self.filename = handler.filename + self.handler = handler + + def fx_graph( + self, + gm: torch.fx.GraphModule, + inputs: list[torch.Tensor], + ) -> None: + with self.fopen("fx_graph_runnable.py") as fd: + save_dir = None + if torch._inductor.config.trace.save_real_tensors: + inputs = torch._subclasses.fake_utils.try_convert_fake_to_real(inputs) + save_dir = os.path.dirname(fd.name) + + # dont try to use stable hash torchinductor compilation if saving real tensors + # and avoid recursively trying to save real tensors inside of the inductor compilation + # regardless + stable_hash = torch._inductor.config.trace.save_real_tensors + with torch._inductor.config.patch( + {"trace.enabled": False, "trace.save_real_tensors": False} + ): + save_graph_repro( + fd, + gm, + inputs, + "inductor", + save_dir=save_dir, + stable_hash=stable_hash, + ) + + with self.fopen("fx_graph_readable.py") as fd: + fd.write(gm.print_readable(print_output=False)) + + def fx_graph_transformed( + self, + gm: torch.fx.GraphModule, + inputs: list[torch.Tensor], + ) -> None: + with self.fopen("fx_graph_transformed.py") as fd: + fd.write(gm.print_readable(print_output=False)) + + def ir_pre_fusion(self, nodes: SchedulerNodeList) -> None: + with self.fopen("ir_pre_fusion.txt") as fd: + fd.write(self._write_ir(nodes)) + + def ir_post_fusion(self, nodes: SchedulerNodeList) -> None: + with self.fopen("ir_post_fusion.txt") as fd: + fd.write(self._write_ir(nodes)) + + @staticmethod + def _write_ir(nodes: SchedulerNodeList) -> str: + buf = io.StringIO() + for node in nodes: + buf.write(node.debug_str()) + buf.write("\n\n\n") + return buf.getvalue() + + def graph_diagram(self, nodes: SchedulerNodeList) -> None: + draw_buffers(nodes, fname=self.filename("graph_diagram.svg")) + + def draw_orig_fx_graph( + self, + gm: torch.fx.GraphModule, + nodes: SchedulerNodeList, + ) -> None: + annotate_orig_fx_with_snodes(gm, nodes) + draw_graph( + gm, + fname=self.filename("orig_fx_graph_diagram.svg"), + clear_meta=False, + prog=GRAPHVIZ_COMMAND_SCALABLE, + parse_stack_trace=True, + dot_graph_shape=config.trace.dot_graph_shape, + ) + + def output_code(self, filename: str, extension: str = "py") -> None: + shutil.copy(filename, self.filename(f"output_code.{extension}")) + + def log_autotuning_results( + self, + name: str, + input_nodes: list[ir.IRNode], + timings: dict["ChoiceCaller", float], # type: ignore[name-defined] # noqa: F821 + elapse: float, + precompile_elapse: float, + prescreening_elapse: float | None, + ) -> None: + from .ir import FixedLayout + + def build_node_info(node: ir.IRNode) -> dict[str, str]: + if hasattr(node, "name"): + node_name = node.name + else: + node_name = "" + node_info = { + "name": node_name, + "type": type(node).__name__, + } + try: + layout = node.get_output_spec() + if isinstance(layout, FixedLayout): + static_layout = FixedLayout( + layout.device, + dtype=layout.dtype, + size=V.graph.sizevars.optimization_hints(layout.size), + stride=V.graph.sizevars.optimization_hints(layout.stride), + offset=V.graph.sizevars.optimization_hint( + layout.offset, fallback=0 + ), + ) + node_info["layout"] = str(static_layout) + else: + node_info["layout"] = str(layout) + except Exception: + pass + try: + node_info["dtype"] = str(node.get_dtype()) + except Exception: + pass + try: + node_info["device"] = str(node.get_device()) + except Exception: + pass + try: + node_info["stride"] = str( + V.graph.sizevars.optimization_hints(node.get_stride()) + ) + except Exception: + pass + try: + node_info["size"] = str( + V.graph.sizevars.optimization_hints(node.get_size()) + ) # type: ignore[arg-type] + except Exception: + pass + try: + node_info["numel"] = str( + V.graph.sizevars.optimization_hint(node.get_numel()) + ) + except Exception: + pass + if hasattr(node, "data") and isinstance(node.data, ir.IRNode): + node_info["data"] = build_node_info(node.data) + return node_info + + general_properties = { + "op_name": name, + "cuda_device_name": torch.cuda.get_device_name(), + "cuda_device_count": torch.cuda.device_count(), + "input_nodes": [build_node_info(node) for node in input_nodes], + "autotuning_time": elapse, + "precompile_time": precompile_elapse, + "prescreening_time": prescreening_elapse, + } + with self.fopen_context( + "autotuning_result_json_list.txt", "at", encoding="utf-8" + ) as fd: + for caller, time in timings.items(): + info_dict = dict(caller.info_dict()) + info_dict.update(general_properties) + info_dict["benchmark_result"] = time + json.dump(info_dict, fd) + fd.write("\n") + + +def log_ir_pre_fusion(nodes: SchedulerNodeList) -> None: + if ir_pre_fusion_log.isEnabledFor(logging.INFO): + ir_pre_fusion_log.info("BEFORE FUSION\n%s", DebugFormatter._write_ir(nodes)) + + V.debug.ir_pre_fusion(nodes) + + +def log_ir_post_fusion(nodes: SchedulerNodeList) -> None: + if ir_post_fusion_log.isEnabledFor(logging.INFO): + ir_post_fusion_log.info("AFTER FUSION\n%s", DebugFormatter._write_ir(nodes)) + + V.debug.ir_post_fusion(nodes) + + +def _dump_collective_schedule(schedule: list[str | None]) -> None: + try: + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "inductor_collective_schedule", + "encoding": "json", + }, + payload_fn=lambda: schedule, + ) + except Exception: + log.debug( + "Failed to log inductor_collective_schedule via structured logging", + exc_info=True, + ) + + +def log_collective_schedule(nodes: Sequence[BaseSchedulerNode]) -> None: + schedule = [ + getattr(op, "python_kernel_name", None) + for node in nodes + if isinstance(op := getattr(node, "node", None), ir._CollectiveKernel) + ] + + # Only log when there is at least one collective op + if schedule: + _dump_collective_schedule(schedule) + + +def log_runtime_and_tensor_meta(node_runtimes: Sequence[tuple[Any, float]]) -> None: + """Log per-op runtime estimates and output tensor metadata for TLParse.""" + + try: + to_optimization_hints = V.graph.sizevars.optimization_hints + + def to_list(x: Sequence[Any] | None) -> list[Any]: + return list(to_optimization_hints(x)) if x is not None else [] + + def dtype_to_str(dtype: Any) -> str | None: + if dtype is None: + return None + s = str(dtype) + s = s.removeprefix("torch.") + return s + + ops: list[dict[str, Any]] = [] + for s, runtime_ns in node_runtimes: + name = getattr(s.node, "python_kernel_name", s.get_name()) + op_type = "collective" if utils.is_collective(s.node) else "compute" + + # Build outputs metadata if available + outputs: list[dict[str, Any]] = [] + try: + for buf in s.get_outputs(): + irnode = buf.node + shape = irnode.maybe_get_size() + stride = ( + irnode.get_stride() + if isinstance(irnode.layout, ir.Layout) + else None + ) + dtype = irnode.maybe_get_dtype() + outputs.append( + { + "shape": to_list(shape), + "stride": to_list(stride), + "dtype": dtype_to_str(dtype), + } + ) + except Exception: + pass + + ops.append( + { + "name": name, + "type": op_type, + "estimated_runtime_ns": runtime_ns, + "outputs": outputs, + } + ) + + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "inductor_runtime_and_tensor_meta", + "encoding": "json", + }, + payload_fn=lambda: {"ops": ops}, + ) + except Exception: + log.debug("Failed to log inductor_runtime_and_tensor_meta", exc_info=True) + + +def log_graph_execution() -> None: + """Emit a structured artifact with the graph execution order.""" + if not GRAPH_EXECUTION_ORDER: + return + try: + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "graph_execution", + "encoding": "json", + }, + payload_fn=lambda: {"graph_execution_order": GRAPH_EXECUTION_ORDER}, + ) + except Exception: + log.debug("Failed to log graph_execution", exc_info=True) + + +@contextlib.contextmanager +def record_and_log_graph_execution_order() -> Iterator[None]: + """Record graph execution order and log it once on exit.""" + global RECORD_GRAPH_EXECUTION, GRAPH_EXECUTION_ORDER, GRAPH_COMPILE_IDS + GRAPH_EXECUTION_ORDER = [] + GRAPH_COMPILE_IDS = {} + RECORD_GRAPH_EXECUTION = True + try: + yield + finally: + log_graph_execution() + RECORD_GRAPH_EXECUTION = False + GRAPH_EXECUTION_ORDER = None + GRAPH_COMPILE_IDS = None + + +@dataclasses.dataclass +class TensorMetadataHolder: + tensor_metadata: TensorMetadata + device: torch.device + + +save_args_cnt = itertools.count() + + +def create_mapping_pre_post_grad_nodes( + pre_grad_graph_id: int | None, + post_to_pre_grad_nodes_json: dict[str, Any], +) -> dict[str, dict[str, list[str]]]: + """ + Create bidirectional mappings between pre_grad graph nodes + and post_grad graph code nodes, and vice versa. + """ + # return a dummy dict if there's any error + empty_return: dict[str, dict[str, list[str]]] = { + "preToPost": {}, + "postToPre": {}, + } + + if not isinstance(post_to_pre_grad_nodes_json, dict): + log.error("Provenance tacking error: post_to_pre_grad_nodes_json is not a dict") + return empty_return + + if not isinstance(pre_grad_graph_id, int): + # pre_grad_graph_id may be empty if there's no pre_grad graph + # and there's only a backward graph from backward pass engine + return empty_return + + pre_to_post: dict[str, Any] = collections.defaultdict(OrderedSet) + post_to_pre: dict[str, Any] = collections.defaultdict(OrderedSet) + + try: + + def check_format(node: dict[str, Any]) -> bool: + if not isinstance(node, dict): + log.error( + "Provenance tacking error: node provenance in post_to_pre_grad_nodes_json is not a dict" + ) + return False + if "graph_id" not in node or "name" not in node or "from_node" not in node: + log.error( + "Provenance tacking error: node provenance in post_to_pre_grad_nodes_json has wrong format" + ) + return False + return True + + for outer_key, node_array in post_to_pre_grad_nodes_json.items(): + if not isinstance(node_array, list): + log.error( + "Provenance tacking error: post_to_pre_grad_nodes_json value is not a list" + ) + return empty_return + for node in node_array: + if not check_format(node): + return empty_return + # Check the current node first + if node.get("graph_id") == pre_grad_graph_id: + pre_to_post[node["name"]].add(outer_key) + post_to_pre[outer_key].add(node["name"]) + + # Check nested from_node array recursively, add node with the right graph_id to the map + stack = [(n, outer_key) for n in node.get("from_node", [])] + while stack: + current_node, parent_key = stack.pop() + if not check_format(current_node): + return empty_return + if current_node.get("graph_id") == pre_grad_graph_id: + pre_to_post[current_node["name"]].add(parent_key) + post_to_pre[parent_key].add(current_node["name"]) + stack.extend( + (n, parent_key) for n in current_node.get("from_node", []) + ) + + def convert_sets_to_lists(d: dict[str, Any]) -> None: + for key in d: + d[key] = list(d[key]) + d = dict(d) + + # convert to list because set is not JSON serializable + convert_sets_to_lists(pre_to_post) + convert_sets_to_lists(post_to_pre) + return { + "preToPost": pre_to_post, + "postToPre": post_to_pre, + } + except Exception as e: + # Since this is just logging code, it should never interfere with regular + # program execution, so we use this try-except to guard against any error + signpost_event( + "inductor", + "provenance_tracking_error", + { + "function": "create_mapping_pre_post_grad_nodes", + "error_msg": str(e), + "stack_trace": traceback.format_exc(), + }, + ) + log.error("post_to_pre_grad_nodes_json: %s", post_to_pre_grad_nodes_json) + log.error("pre_grad_graph_id: %s", pre_grad_graph_id) + return empty_return + + +def create_node_mapping_kernel_to_post_grad( + triton_kernel_to_post_grad_json: dict[str, Any], +) -> dict[str, dict[str, Any]]: + """Create bidirectional mappings between triton kernel name and post_grad + graph code nodes, and vice versa. + """ + + # return a dummy dict if there's any error + empty_return: dict[str, dict[str, Any]] = { + "cppCodeToPost": {}, + "postToCppCode": {}, + } + + if not isinstance(triton_kernel_to_post_grad_json, dict): + log.error( + "Provenance tacking error: triton_kernel_to_post_grad_json is not a dict" + ) + return empty_return + + post_to_cpp_code: dict[str, Any] = collections.defaultdict(OrderedSet) + + try: + for outer_key, node_array in triton_kernel_to_post_grad_json.items(): + if not isinstance(node_array, list): + log.error( + "Provenance tacking error: triton_kernel_to_post_grad_json value is not a list" + ) + return empty_return + for curr_node in node_array: + post_to_cpp_code[curr_node].add(outer_key) + + def convert_sets_to_lists(d: dict[str, Any]) -> None: + for key in d: + d[key] = list(d[key]) + d = dict(d) + + # convert to list because set is not JSON serializable + convert_sets_to_lists(post_to_cpp_code) + return { + "cppCodeToPost": triton_kernel_to_post_grad_json, + "postToCppCode": post_to_cpp_code, + } + except Exception as e: + # Since this is just logging code, it should never interfere with regular + # program execution, so we use this try-except to guard against any error + signpost_event( + "inductor", + "provenance_tracking_error", + { + "function": "create_mapping_kernel_to_post_grad", + "error_msg": str(e), + "stack_trace": traceback.format_exc(), + }, + ) + log.error( + "triton_kernel_to_post_grad_json: %s", triton_kernel_to_post_grad_json + ) + return empty_return + + +def dump_inductor_provenance_info() -> dict[str, Any]: + try: + global _pre_grad_graph_id + global _inductor_post_to_pre_grad_nodes + global _inductor_triton_kernel_to_post_grad_node_info + node_mapping: dict[str, Any] = {} + if _pre_grad_graph_id: + node_mapping_kernel = create_node_mapping_kernel_to_post_grad( + _inductor_triton_kernel_to_post_grad_node_info + ) + node_mapping = { + **_inductor_post_to_pre_grad_nodes, + **node_mapping_kernel, + } + if config.trace.enabled: + with V.debug.fopen( + "inductor_provenance_tracking_node_mappings.json", "w" + ) as fd: + json.dump(node_mapping, fd) + # we need to update the node mapping version when node mapping format changes + # so the tlparse tool knows which node mapping version it is looking at + node_mapping["version"] = 2.0 + return node_mapping + except Exception as e: + # Since this is just debugging, it should never interfere with regular + # program execution, so we use this try-except to guard against any error + signpost_event( + "inductor", + "provenance_tracking_error", + { + "function": "dump_inductor_provenance_info", + "error_msg": str(e), + "stack_trace": traceback.format_exc(), + }, + ) + return {} + + +def create_kernel_information_json() -> dict[str, dict[str, list[str]]]: + """Create kernel information JSON""" + try: + global _inductor_post_to_pre_grad_nodes + global _inductor_kernel_stack_trace + global _inductor_triton_kernel_to_post_grad_node_info + + post_to_pre = _inductor_post_to_pre_grad_nodes.get("postToPre", {}) + all_kernels = OrderedSet(_inductor_kernel_stack_trace.keys()) | OrderedSet( + _inductor_triton_kernel_to_post_grad_node_info.keys() + ) + + result = {} + for kernel_name in all_kernels: + post_grad_nodes = _inductor_triton_kernel_to_post_grad_node_info.get( + kernel_name, [] + ) + + pre_grad_nodes: OrderedSet[str] = OrderedSet() + for post_node in post_grad_nodes: + pre_grad_nodes.update(post_to_pre.get(post_node, [])) + + result[kernel_name] = { + "stack_traces": _inductor_kernel_stack_trace.get(kernel_name, []), + "post_grad_nodes": post_grad_nodes, + "pre_grad_nodes": list(pre_grad_nodes), + } + + return result + except Exception as e: + signpost_event( + "inductor", + "provenance_tracking_error", + { + "function": "create_kernel_information_json", + "error_msg": str(e), + "stack_trace": traceback.format_exc(), + }, + ) + return {} + + +def set_kernel_post_grad_provenance_tracing( + node_schedule: Sequence[BaseSchedulerNode] | ExternKernel, + kernel_name: str, + is_extern: bool = False, +) -> int | None: + """ + Set the mapping between `kernel_name` and the post_grad nodes in `node_schedule`. + + Returns a unique int debug handler for each call to this function. + """ + + if config.trace.provenance_tracking_level == 0: + return None + + try: + from .codegen.simd_kernel_features import DisableReduction, EnableReduction + + global _inductor_triton_kernel_to_post_grad_node_info + global _inductor_kernel_stack_trace + global _inductor_kernel_provenance_debug_handle + + _inductor_kernel_provenance_debug_handle += 1 + stack_traces: list[str] = [] + kernel_name = f"{kernel_name}:{_inductor_kernel_provenance_debug_handle}" + if is_extern: + assert isinstance(node_schedule, ExternKernel) + curr_node_info = _inductor_triton_kernel_to_post_grad_node_info.setdefault( + kernel_name, [] + ) + # 'origins' on IR nodes gives what FX IR nodes contributed to any given fused kernel. + # "origin_node" is more precise and says that the contents of this node corresponds + # EXACTLY to the output of a particular FX node, but it's not always available + if node_schedule.origin_node: + origin_node_name = node_schedule.origin_node.name + if origin_node_name not in curr_node_info: + curr_node_info.append(origin_node_name) + else: + curr_node_info.extend( + origin.name + for origin in node_schedule.origins + if origin.name not in curr_node_info + ) + stack_traces = list(node_schedule.get_stack_traces()) + else: + assert isinstance(node_schedule, list) + stack_traces_set: OrderedSet[str] = OrderedSet() + for snode in node_schedule: + if snode not in (EnableReduction, DisableReduction): + if snode.node is not None: + curr_node_info = ( + _inductor_triton_kernel_to_post_grad_node_info.setdefault( + kernel_name, [] + ) + ) + # pyrefly: ignore [missing-attribute] + stack_traces_set.update(snode.node.get_stack_traces()) + curr_node_info.extend( + origin.name + # pyrefly: ignore [missing-attribute] + for origin in snode.node.origins + if origin.name not in curr_node_info + ) + stack_traces = list(stack_traces_set) + _inductor_kernel_stack_trace.setdefault(kernel_name, []).extend(stack_traces) + return _inductor_kernel_provenance_debug_handle + except Exception as e: + # Since this is just debugging, it should never interfere with regular + # program execution, so we use this try-except to guard against any error + signpost_event( + "inductor", + "provenance_tracking_error", + { + "function": "set_kernel_post_grad_provenance_tracing", + "error_msg": str(e), + "stack_trace": traceback.format_exc(), + }, + ) + return None + + +def save_args_for_compile_fx_inner(*args: Any, **kwargs: Any) -> None: + """ + This function is used to save arguments for a compile_fx_inner function call + to the file system. Later on one can replay the compile_fx_inner call + with the saved arguments using load_args_and_run_compile_fx_inner. + """ + + folder = os.path.join(tempfile.gettempdir(), "inductor_saved_args") + if not os.path.exists(folder): + os.mkdir(folder) + + def handle_tensor(x: Any) -> Any: + """ + Pickle FakeTensor will result in error: + AttributeError: Can't pickle local object 'WeakValueDictionary.__init__..remove' + + Convert all Tensor to metadata. This may also makes pickle faster. + """ + if isinstance(x, torch.Tensor): + return TensorMetadataHolder(_extract_tensor_metadata(x), x.device) + else: + return x + + args_to_save, kwargs_to_save = tree_map(handle_tensor, (args, kwargs)) + + fn_name = "compile_fx_inner" + path = f"{folder}/{fn_name}_{next(save_args_cnt)}.pkl" + with open(path, "wb") as f: + pickle.dump((args_to_save, kwargs_to_save), f) + + if log.isEnabledFor(logging.DEBUG): + message = f""" +Arguments for a compile_fx_inner call is saved to {path}. To replay the call, +run the following: + +from torch._inductor.debug import load_args_and_run_compile_fx_inner +load_args_and_run_compile_fx_inner({path!r}) + """ + # call print rather than log.debug. log.debug will print message + # prefix for each line which makes the code snippet harder to be + # copied. + # Not a big deal since the code is already been guarded by checking + # the log level. + print(message) + + +def load_args_and_run_compile_fx_inner(path: str) -> Any: + from torch._inductor.compile_fx import compile_fx_inner + + with open(path, "rb") as f: + args, kwargs = pickle.load(f) + + def handle_tensor(x: Any) -> Any: + if isinstance(x, TensorMetadataHolder): + return torch._dynamo.testing.rand_strided( + x.tensor_metadata.shape, + x.tensor_metadata.stride, + x.tensor_metadata.dtype, + x.device, + ) + else: + return x + + fake_mode = torch._subclasses.FakeTensorMode(allow_non_fake_inputs=True) + with fake_mode, config.patch("save_args", False): + args, kwargs = tree_map(handle_tensor, (args, kwargs)) + return compile_fx_inner(*args, **kwargs) + + +def aot_inductor_minifier_wrapper( + func: Callable[..., str], + exported_program: torch.export.ExportedProgram, + *, + inductor_configs: dict[str, Any], + package_path: FileLike | None = None, +) -> str: + from torch._dynamo.debug_utils import AccuracyError + from torch._dynamo.repro.aoti import dump_to_minify + from torch._inductor import config + from torch._inductor.compile_fx import _aoti_flatten_inputs + + use_minifier = config.aot_inductor.dump_aoti_minifier + + gm = exported_program.module(check_guards=False) + assert isinstance(gm, torch.fx.GraphModule) + + args, kwargs = exported_program.example_inputs + + try: + if use_minifier and config.aot_inductor.repro_level == 3: + # Always dump the original module in case we have segfaults + dump_to_minify( + exported_program, + "aot_inductor", + options=inductor_configs, + ) + if use_minifier and config.aot_inductor.repro_level == 4: + # Check for accuracy + # We will first flatten the inputs before compiling and checking for accuracy. + # This is ok because we will flatten the inputs in the minifier anyway. + gm_copy = copy.deepcopy(gm) + example_inputs_copy = copy.deepcopy(exported_program.example_inputs) + config_copy = copy.deepcopy(inductor_configs) + flat_example_inputs, config_copy = _aoti_flatten_inputs( + gm_copy, + example_inputs_copy[0], + example_inputs_copy[1], + options=config_copy, + ) + tuple_inputs = tuple(flat_example_inputs) + flattened_ep = torch.export.export(gm_copy, tuple_inputs, strict=False) + func( + flattened_ep.module(check_guards=False), + tuple_inputs, + inductor_configs=config_copy, + package_path=package_path, + load_and_run=True, + check_accuracy="accuracy", + ) + + return func( + gm, + args, + kwargs, + inductor_configs=inductor_configs, + package_path=package_path, + load_and_run=use_minifier, + ) + except AccuracyError as e: + dump_to_minify( + exported_program, + "aot_inductor_accuracy", + command="minify", + options=inductor_configs, + ) + log.warning("Accuracy failed") + raise e + except Exception as e: + if use_minifier: + command = "minify" + + if config.aot_inductor.repro_level == 1: + command = "run" + + dump_to_minify( + exported_program, + "aot_inductor", + command=command, + options=inductor_configs, + ) + raise e diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/decomposition.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/decomposition.py new file mode 100644 index 0000000000000000000000000000000000000000..5c90e4c070238cb532f312a1b24955503c710c7c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/decomposition.py @@ -0,0 +1,1335 @@ +# mypy: allow-untyped-decorators +import functools +import logging +import math +import operator +import sys +from collections.abc import Callable +from typing import Any, TypeAlias, TypeVar +from typing_extensions import ParamSpec + +import torch +import torch._decomp as decomp +import torch._prims_common as utils +import torch.ao.quantization.fx._decomposed +from torch._decomp import ( + core_aten_decompositions, + get_decompositions, + remove_decompositions, +) +from torch._decomp.decompositions import ( + _grid_sampler_2d as decomp_grid_sampler_2d, + _index_add, + embedding_dense_backward as decomp_embedding_dense_backward, + pw_cast_for_opmath, + pw_cast_for_opmath_non_tensor_args, +) +from torch._decomp.decompositions_for_rng import extra_random_decomps +from torch._dynamo.utils import counters +from torch._environment import is_fbcode +from torch._higher_order_ops.out_dtype import out_dtype +from torch._inductor.utils import pad_listlike +from torch._prims_common import ( + elementwise_dtypes, + ELEMENTWISE_TYPE_PROMOTION_KIND, + suggest_memory_format, + type_to_dtype, +) +from torch._refs import native_layer_norm as decomp_native_layer_norm +from torch.fx.experimental.symbolic_shapes import guard_or_false, statically_known_true + +from . import config, inductor_prims +from .utils import ( + is_gpu, + needs_fallback_due_to_atomic_add_limitations, + use_scatter_fallback, +) + + +_T = TypeVar("_T") +_P = ParamSpec("_P") + +_GenericOperator: TypeAlias = torch._ops.OperatorBase | torch._ops.OpOverloadPacket + +log = logging.getLogger(__name__) +aten = torch.ops.aten +prims = torch.ops.prims +quantized = torch.ops.quantized +_quantized = torch.ops._quantized +quantized_decomposed = torch.ops.quantized_decomposed + +inductor_decompositions = get_decompositions( + [ + aten._adaptive_avg_pool2d_backward, + aten.index_select, + aten.addmv, + aten.arange, + aten.bitwise_and_, + aten.bitwise_or_, + aten.clamp_min_, + aten.dist, + aten.elu, + aten.empty_like, + aten.flip, + aten.gelu, + aten.hardtanh, + aten.lcm, + aten.leaky_relu, + aten.linalg_vector_norm, + aten._log_softmax, + aten.max_pool2d_with_indices_backward, + aten._native_batch_norm_legit, + aten._native_batch_norm_legit_functional, + aten._native_batch_norm_legit_no_training, + aten._batch_norm_with_update, + aten._batch_norm_with_update_functional, + aten._batch_norm_no_update, + aten.batch_norm_backward, + aten.native_batch_norm, + aten.native_group_norm, + aten.native_layer_norm, + aten.nll_loss2d_backward, + aten.permute_copy, + aten.rrelu_with_noise_backward, + aten._softmax, + aten.sin_, + aten.sqrt_, + out_dtype, + aten._to_copy, + aten.tril_indices, + aten.triu_indices, + aten.unbind_copy.int, + aten.upsample_bilinear2d.vec, + aten.hann_window, + quantized.linear_dynamic_fp16_unpacked_weight, + _quantized.wrapped_quantized_linear, + ] +) +decompositions = {**core_aten_decompositions(), **inductor_decompositions} + +# Remove unwanted decompositions included via the core ATen decompositions from +# the Inductor decomp table. +decomps_to_exclude: list[torch._ops.OpOverload | torch._ops.OpOverloadPacket] = [ + aten._unsafe_index, + aten._unsafe_masked_index, + aten._unsafe_masked_index_put_accumulate, + aten._scaled_dot_product_flash_attention_for_cpu.default, # See comments in torch/_decomp/decompositions.py + aten._softmax_backward_data, + aten.clamp_max, + aten.clamp_min, + aten.embedding_dense_backward, # we fall back on xpu + aten.native_layer_norm, # we fall back on mtia + aten.index_add, # we conditionally call this decomp + aten.glu, # inductor lowers this directly + aten.select_scatter, # need to be in the ATen graph in order for it to work with the re-inplacing pass + aten.slice_scatter, # need to be in the ATen graph in order for it to work with the re-inplacing pass + aten.silu, # inductor uses exact eager decomposition + aten.split.Tensor, # inductor lowers this directly + aten.squeeze, # inductor lowers this directly + aten.sum, # inductor lowers this directly + aten.unbind, # inductor lowers this directly + aten.baddbmm, # upcasts to fp32, perf issue + # FMA ops - we have lowerings that use FMA to match eager CUDA behavior + aten.addcmul, + aten.addcmul_, + aten._foreach_addcmul.Scalar, + aten._foreach_addcmul_, + aten.addcdiv, + aten.addcdiv_, + aten._foreach_addcdiv.Scalar, + aten._foreach_addcdiv_, + aten.lerp, + aten.lerp_, +] + +remove_decompositions(decompositions, decomps_to_exclude) + + +def register_decomposition( + ops: _GenericOperator | list[_GenericOperator], +) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]: + for op in ops if isinstance(ops, list) else [ops]: + if op in decompositions: + log.warning("duplicate decomp: %s", ops) + return decomp.register_decomposition(ops, decompositions) + + +@register_decomposition([aten.lerp.Scalar]) +def _lerp_scalar(start: torch.Tensor, end: torch.Tensor, weight: float) -> torch.Tensor: + # Decompose into sub + add(alpha=weight) so that the add lowering emits FMA, + # matching eager CUDA's dual-formula (see aten/src/ATen/native/Lerp.h). + # Convert end to start's memory format so the output preserves start's layout, + # matching eager TensorIterator behavior. + fmt = suggest_memory_format(start) + if fmt != torch.contiguous_format: + end = end.contiguous(memory_format=fmt) + diff = end - start + if weight >= 0.5 or weight <= -0.5: + return torch.add(end, diff, alpha=-(1.0 - weight)) + return torch.add(start, diff, alpha=weight) + + +@register_decomposition([aten.lerp.Tensor]) +def _lerp_tensor( + start: torch.Tensor, end: torch.Tensor, weight: torch.Tensor +) -> torch.Tensor: + # Same dual-formula as foreach_lerp polyfill: decompose into sub + addcmul. + # Convert end to start's memory format so the output preserves start's layout. + fmt = suggest_memory_format(start) + if fmt != torch.contiguous_format: + end = end.contiguous(memory_format=fmt) + diff = end - start + mask = weight.abs() >= 0.5 + neg_omw = -(1.0 - weight) + w = torch.where(mask, neg_omw, weight) + base = torch.where(mask, end, start) + return torch.addcmul(base, w, diff, value=1) + + +@register_decomposition([aten.embedding_dense_backward]) +def _embedding_dense_backward( + grad_output: torch.Tensor, + indices: torch.Tensor, + num_weights: int, + padding_idx: int, + scale_grad_by_freq: bool, +) -> torch.Tensor: + # TODO: check if XE4 still need this fallback + # check torch.xpu.get_device_properties(grad_output.device).architecture + if grad_output.is_xpu: + return NotImplemented + # We can write a util function to update decomp table if we have more ops to fallback. + return decomp_embedding_dense_backward( + grad_output, indices, num_weights, padding_idx, scale_grad_by_freq + ) + + +@register_decomposition(aten.native_layer_norm) +def _native_layer_norm( + input: torch.Tensor, + normalized_shape: utils.ShapeType, + weight: torch.Tensor | None, + bias: torch.Tensor | None, + eps: float, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + if input.is_mtia: + return NotImplemented + # We can write a util function to update decomp table if we have more ops to fallback. + return decomp_native_layer_norm(input, normalized_shape, weight, bias, eps) + + +@register_decomposition([aten.sym_constrain_range_for_size.default]) +def sym_constrain_range_for_size( + symbol: torch.SymInt, + *, + min: torch.types.Number | None = None, + max: torch.types.Number | None = None, +) -> None: + return + + +@register_decomposition([aten.clamp]) +@pw_cast_for_opmath_non_tensor_args +def clamp( + x: torch.Tensor, + min: torch.types.Number | None = None, + max: torch.types.Number | None = None, +) -> torch.Tensor: + if min is not None: + x = x.clamp_min(min) + if max is not None: + x = x.clamp_max(max) + return x + + +# Inductor-specific SiLU decomposition for exact eager matching. +# The core decomposition uses x * sigmoid(x), but this form +# x / (1 + exp(-x)) matches eager execution more precisely. +@register_decomposition([aten.silu]) +@pw_cast_for_opmath +def silu(x: torch.Tensor) -> torch.Tensor: + return x / (1 + x.neg().exp()) + + +@register_decomposition([aten.full]) +def full( + size: list[int | torch.SymInt], + fill_value: torch.types.Number, + **kwargs: Any, +) -> torch.Tensor: + dtype = kwargs.get("dtype") + if dtype is None: + kwargs["dtype"] = type_to_dtype(type(fill_value)) + return torch.full(size, fill_value, **kwargs) + return NotImplemented + + +@register_decomposition([aten.index_add]) +def index_add( + x: torch.Tensor, + dim: int, + index: torch.Tensor, + tensor: torch.Tensor, + *, + alpha: torch.types.Number = 1, +) -> torch.Tensor: + # If we are not in fbcode and dtype is bfloat16 + # fallback to index_add kernel + # see https://github.com/pytorch/pytorch/issues/137425 for details + if not is_fbcode() and x.dtype == torch.bfloat16: + return NotImplemented + else: + return _index_add(x, dim, index, tensor, inplace=False, alpha=alpha) + + +# Not really sure how to put this into the main library. PrimTorch wants +# empty_permuted to go to the prim, and typically users don't really want +# to decompose to empty_strided (but inductor is OK with it, because we are +# cool with strides and everything goes to empty_strided) +@register_decomposition([aten.empty_permuted.default]) +def empty_permuted( + size: list[int | torch.SymInt], + physical_layout: list[int], + **kwargs: Any, +) -> torch.Tensor: + is_identity = list(physical_layout) == list(range(len(physical_layout))) + + if is_identity: + return torch.empty(size, **kwargs) + else: + perm = [0] * len(size) + for p, l in enumerate(physical_layout): + perm[l] = p + return torch.empty([size[l] for l in physical_layout], **kwargs).permute(perm) + + +@register_decomposition([aten.convolution_backward]) +def convolution_backward( + grad_output: torch.Tensor, + input: torch.Tensor, + weight: torch.Tensor, + bias_sizes: list[int], + stride: int | list[int], + padding: int | list[int], + dilation: int | list[int], + transposed: bool, + output_padding: list[int], + groups: int, + output_mask: list[bool], +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + if not output_mask[2] or not is_gpu(grad_output.device.type): + return NotImplemented + grad_bias = aten.sum(grad_output, [0] + list(range(2, grad_output.dim()))) + grad_inp, grad_weight, _ = aten.convolution_backward( + grad_output, + input, + weight, + bias_sizes, + stride, + padding, + dilation, + transposed, + output_padding, + groups, + [output_mask[0], output_mask[1], False], + ) + return (grad_inp, grad_weight, grad_bias) + + +@register_decomposition([aten.round.decimals]) +def round_dec(x: torch.Tensor, decimals: int = 0) -> torch.Tensor: + ten_pow_decimals = 10.0**decimals + return aten.round(x * ten_pow_decimals) * (1.0 / ten_pow_decimals) + + +@register_decomposition([aten.bmm]) +@pw_cast_for_opmath +def bmm( + self: torch.Tensor, + batch2: torch.Tensor, + out_dtype: torch.dtype | None = None, +) -> torch.Tensor: + # Outer-product specialization: [B, M, 1] x [B, 1, N] -> [B, M, N]. + # This avoids introducing a reduction and maps directly to broadcasted mul. + if statically_known_true(self.shape[2] == 1) or statically_known_true( + batch2.shape[1] == 1 + ): + return (self * batch2).contiguous() + + # TODO: Re-enable for mps once our reductions are performant enough + # (https://github.com/pytorch/pytorch/issues/150121) + if config.coordinate_descent_tuning and self.device.type not in ["cpu", "mps"]: + if statically_known_true(self.shape[1] == 1) or statically_known_true( + batch2.shape[2] == 1 + ): + out = (self.unsqueeze(-1) * batch2.unsqueeze(1)).sum(dim=2) + return out + if self.device.type == "cpu": + if statically_known_true(self.size(1) == 1) and statically_known_true( + batch2.size(-1) == 1 + ): + counters["inductor"]["decompose_bmm"] += 1 + return torch.sum( + self.squeeze(1) * batch2.squeeze(-1), dim=1, keepdim=True + ).unsqueeze(1) + return NotImplemented + + +@register_decomposition([aten.addmm]) +@pw_cast_for_opmath +def addmm( + self: torch.Tensor, + mat1: torch.Tensor, + mat2: torch.Tensor, + out_dtype: torch.dtype | None = None, + beta: torch.types.Number = 1, + alpha: torch.types.Number = 1, +) -> torch.Tensor: + if mat1.device.type not in ["cpu", "mps"]: + if ( + statically_known_true(mat1.size(-1) == 1) + and statically_known_true(mat1.size(0) != 1) + and statically_known_true(mat2.size(1) != 1) + ): + counters["inductor"]["decompose_addmm"] += 1 + out = mat1 * mat2 + return alpha * out + beta * self + + if self.device.type == "cpu": + if statically_known_true(mat1.size(0) == 1) and statically_known_true( + mat2.size(-1) == 1 + ): + counters["inductor"]["decompose_addmm"] += 1 + out = torch.sum( + mat1.squeeze(0) * mat2.squeeze(-1), dim=0, keepdim=True + ).unsqueeze(0) + return alpha * out + beta * self + if ( + statically_known_true(mat1.size(0) == 1) + and guard_or_false(mat2.size(0) <= 16) + and guard_or_false(mat2.size(1) <= 16) + ): + counters["inductor"]["decompose_addmm"] += 1 + out = (mat1.T * mat2).sum(dim=0, keepdim=True) + return alpha * out + beta * self + return NotImplemented + + +@register_decomposition([aten.mm]) +@pw_cast_for_opmath +def mm( + self: torch.Tensor, + input2: torch.Tensor, + out_dtype: torch.dtype | None = None, +) -> torch.Tensor: + # Our matrix vector multiplies only achieve peak bandwidth with coordinate descent tuning. + # todo: Look into why and fix it (hopefully) + + # TODO: Re-enable for mps once our reductions are performant enough + # (https://github.com/pytorch/pytorch/issues/150121) + if config.coordinate_descent_tuning and self.device.type not in ["cpu", "mps"]: + if statically_known_true(self.shape[0] == 1) or statically_known_true( + input2.shape[1] == 1 + ): + return (self.unsqueeze(2) * input2.unsqueeze(0)).sum(dim=1) + # Non-CPU/MPS: always decompose. CPU: only for small tensors. + if ( + statically_known_true(self.size(-1) == 1) + and statically_known_true(self.size(0) != 1) + and statically_known_true(input2.size(1) != 1) + ): + if self.device.type not in ["cpu", "mps"] or ( + self.device.type == "cpu" + and statically_known_true(self.size(0) > 0) + and statically_known_true(input2.size(0) == 1) + and (self.dtype == input2.dtype) + and guard_or_false((torch.numel(self) + torch.numel(input2)) <= 32) + ): + counters["inductor"]["decompose_mm"] += 1 + return self * input2 + if self.device.type == "cpu": + if statically_known_true(self.size(0) == 1) and statically_known_true( + input2.size(-1) == 1 + ): + counters["inductor"]["decompose_mm"] += 1 + return torch.sum( + self.squeeze(0) * input2.squeeze(-1), dim=0, keepdim=True + ).unsqueeze(0) + return NotImplemented + + +# This pass does two things: +# - Eliminate cat when there is only one tensor input +# - Normalize cat calls, so that legacy empty 1-D tensors are removed (NB: we +# don't remove ALL empty tensors, only the naughty ones) +@register_decomposition([aten.cat.default]) +def cat( + tensors: list[torch.Tensor], + dim: int = 0, +) -> torch.Tensor: + def non_empty_tensor(x: torch.Tensor) -> bool: + # For better or worse, this is a valid cat: + # + # torch.cat([torch.randn(2, 2, 4), torch.randn(0), torch.randn(3, 2, 4)]) + # + # We'd like to eliminate naughtiness like this for downstream passes + # like split_cat. The easiest way is to just drop such inputs + # (guarding that they are non-zero). + # + # Is it permissible for this filtering to be size-oblivious? A case + # where this could matter is cat([(2, 2), (u0,)], dim=0); if u0 + # happened to be zero, we would have liked to have filtered it out. + # But actually, the ONLY way this could have passed is if u0 == 0, + # so by the time we get here we have already installed a deferred + # runtime assert forcing u0 to be zero. So if this hasn't happened, + # we know that the unbacked SymInt has appropriate size and there are + # no problems. + if len(x.shape) == 1 and guard_or_false(x.shape[0] == 0): + return False + + if dim < len(x.shape) and guard_or_false(x.shape[dim] == 0): + return False + + return True + + filtered_tensors = list(filter(non_empty_tensor, tensors)) + + if len(filtered_tensors) == 1: + # check dtype promotion + promoted_dtype = elementwise_dtypes( + *tensors, + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.DEFAULT, + )[1] + filtered_t = filtered_tensors[0] + return ( + filtered_t.clone() + if promoted_dtype == filtered_t.dtype + else filtered_t.to(dtype=promoted_dtype) + ) + elif 1 < len(filtered_tensors) < len(tensors): + # on the first call, when we remove empty tensors, we redispatch recursively + return aten.cat.default(filtered_tensors, dim) + + # optimization, avoid concat for single, repeated input + if len(filtered_tensors) > 1 and all( + t is filtered_tensors[0] for t in filtered_tensors + ): + inp = filtered_tensors[0] + shape = list(inp.shape) + dim = dim + len(inp.shape) if dim < 0 else dim + shape.insert(dim, len(filtered_tensors)) + return inp.unsqueeze(dim).expand(*shape).flatten(dim, dim + 1).clone() + + # when no 'filtering' has occurred, we raise to prevent infinite recursion (no more decomposition needed) + return NotImplemented + + +@register_decomposition([aten.angle]) +def angle(x: torch.Tensor) -> torch.Tensor: + if x.is_complex(): + return torch.where( + torch.isnan(x.real), float("nan"), torch.atan2(x.imag, x.real) + ) + + # when x is real number + # if x >= 0, return 0 + # if x < 0, return pi + # if x is nan, return nan + _, dtype = elementwise_dtypes( + x, + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + ) + pi = torch.scalar_tensor(math.pi, dtype=dtype, device=x.device) + ret = torch.where(x < 0, pi, 0.0) + return torch.where(torch.isnan(x), float("nan"), ret) + + +@register_decomposition([aten.add]) +def add( + x: torch.Tensor, + y: torch.Tensor, + *, + alpha: torch.types.Number | None = None, +) -> torch.Tensor: + # Require both x and y to be complex tensors. + x_is_complex_tensor = torch.is_tensor(x) and x.is_complex() + y_is_complex_tensor = torch.is_tensor(y) and y.is_complex() + if not x_is_complex_tensor or not y_is_complex_tensor: + return NotImplemented + + def _requires_fallback(tensor: torch.Tensor) -> bool: + if tensor.ndim == 0: + return False + # Viewing complex tensors as their real dtype requires the last stride to be 1. + return tensor.stride()[-1] != 1 + + output_size_zero = False + if x.ndim == 0 and y.ndim == 0: + output_size_zero = True + + if x.ndim == 0: + x = x.reshape(1) + if y.ndim == 0: + y = y.reshape(1) + + z = y + if alpha is not None: + z = alpha * y + complex_type = torch.promote_types(x.dtype, y.dtype) + + if _requires_fallback(x) or _requires_fallback(z): + return NotImplemented + + # For complex typed `x`, `x.view(x.real.dtype)` doubles the last dimension and can cause problem + # when broadcasting the add. + def reshape_tensor_complex(tensor: torch.Tensor) -> torch.Tensor: + """Reshape tensor from [*initial_dims, last_dim] to *initial_dims, last_dim/2, 2]""" + # Get the current shape of the tensor + *initial_dims, last_dim = tensor.shape + + # Check if the last dimension is even. We should never reach here since `x.view(x.real.dtype)` + # doubles the last dimension for complex numbers. + if last_dim % 2 != 0: + raise AssertionError( + "The size of the last dimension must be even to reshape it to [..., last_dim/2, 2]" + ) + + # Reshape the tensor + new_shape = (*initial_dims, last_dim // 2, 2) + reshaped_tensor = tensor.view(new_shape) + return reshaped_tensor + + # Manually resolve complex tensors, as .is_conj() is unreliable after cloning during compilation. + x = x + 0 + z = z + 0 + + x_reshaped = reshape_tensor_complex(x.view(x.real.dtype)) + z_reshaped = reshape_tensor_complex(z.view(y.real.dtype)) + result = torch.flatten(x_reshaped + z_reshaped, start_dim=-2).view(complex_type) + + if output_size_zero: + return result[0] + return result + + +@register_decomposition([aten.conj_physical]) +def conj_physical(self: torch.Tensor) -> torch.Tensor: + if self.is_complex(): + return NotImplemented + return self + + +@register_decomposition([aten.lift, aten.detach_]) +def lift(self: torch.Tensor) -> torch.Tensor: + return self + + +@register_decomposition([aten.fmin, prims.fmin]) +def fmin(self: torch.Tensor, other: torch.Tensor) -> torch.Tensor: + return torch.where(torch.isnan(other) | (other > self), self, other) + + +@register_decomposition([aten.fmax, prims.fmax]) +def fmax(self: torch.Tensor, other: torch.Tensor) -> torch.Tensor: + return torch.where(torch.isnan(other) | (other < self), self, other) + + +@register_decomposition(aten.amax) +def amax( + self: torch.Tensor, + dim: int | None = None, + keepdim: bool = False, +) -> torch.Tensor: + if self.dtype == torch.bool: + return torch.any(self, dim=dim, keepdim=keepdim) + return NotImplemented + + +@register_decomposition(aten.amin) +def amin( + self: torch.Tensor, + dim: int | None = None, + keepdim: bool = False, +) -> torch.Tensor: + if self.dtype == torch.bool: + return torch.all(self, dim=dim, keepdim=keepdim) + return NotImplemented + + +@register_decomposition([aten.narrow_copy]) +def narrow_copy( + self: torch.Tensor, + dim: int, + start: int, + length: int, +) -> torch.Tensor: + # Use memory_format=torch.contiguous_format to ensure correct strides. + # For empty tensors, a plain clone() preserves the input view's strides. + return torch.narrow(self, dim, start, length).clone( + memory_format=torch.contiguous_format + ) + + +@register_decomposition([aten.view_copy.default]) +def view_copy_default( + self: torch.Tensor, + size: list[int | torch.SymInt], +) -> torch.Tensor: + return aten.view(self, size).clone() + + +@register_decomposition([aten.view_copy.dtype]) +def view_copy_dtype( + self: torch.Tensor, + dtype: torch.dtype, +) -> torch.Tensor: + return self.clone().view(dtype) + + +def _get_shape_permutation_like( + self: torch.Tensor, +) -> tuple[utils.ShapeType, utils.StrideType]: + physical_layout, _ = utils.compute_elementwise_output_logical_to_physical_perm(self) + shape = [self.shape[l] for l in physical_layout] + + permutation = [0] * len(shape) + for p, l in enumerate(physical_layout): + permutation[l] = p + + return (shape, permutation) + + +@register_decomposition(aten.full_like) +def full_like( + self: torch.Tensor, + fill_value: int | float, + *, + dtype: torch.dtype | None = None, + layout: torch.layout | None = None, + device: torch.device | None = None, + pin_memory: bool = False, + requires_grad: bool = False, + memory_format: torch.memory_format = torch.preserve_format, +) -> torch.Tensor: + dtype = self.dtype if dtype is None else dtype + layout = self.layout if layout is None else layout + device = self.device if device is None else device + + if memory_format != torch.preserve_format: + result = torch.full( + self.shape, + fill_value, + dtype=dtype, + layout=layout, + device=device, + pin_memory=pin_memory, + requires_grad=requires_grad, + ) + return result.to(memory_format=memory_format) + + else: + assert layout == torch.strided + shape, permutation = _get_shape_permutation_like(self) + result = torch.full( + shape, + fill_value, + dtype=dtype, + layout=layout, + device=device, + pin_memory=pin_memory, + requires_grad=requires_grad, + ) + if permutation == list(range(len(permutation))): + return result + return result.permute(permutation).clone() + + +def _rand_like( + rand_fn: Callable[..., torch.Tensor], + self: torch.Tensor, + *, + dtype: torch.dtype | None = None, + device: torch.device | None = None, + memory_format: torch.memory_format = torch.preserve_format, + **kwargs: Any, +) -> torch.Tensor: + dtype = self.dtype if dtype is None else dtype + device = self.device if device is None else device + + if memory_format != torch.preserve_format: + return rand_fn( + self.shape, + dtype=dtype, + device=device, + **kwargs, + ).to(memory_format=memory_format) + + shape, permutation = _get_shape_permutation_like(self) + result = rand_fn( + shape, + dtype=dtype, + device=device, + **kwargs, + ) + if permutation == list(range(len(permutation))): + return result + return result.permute(permutation).clone() + + +@decomp.register_decomposition([aten.rand_like], extra_random_decomps) +def rand_like(self: torch.Tensor, **kwargs: Any) -> torch.Tensor: + return _rand_like(torch.rand, self, **kwargs) + + +@decomp.register_decomposition([aten.randn_like], extra_random_decomps) +def randn_like(self: torch.Tensor, **kwargs: Any) -> torch.Tensor: + return _rand_like(torch.randn, self, **kwargs) + + +@decomp.register_decomposition([aten.randint_like.default], extra_random_decomps) +def randint_like(self: torch.Tensor, high: int, **kwargs: Any) -> torch.Tensor: + return _rand_like(functools.partial(aten.randint.low, 0, high), self, **kwargs) + + +@decomp.register_decomposition([aten.randint_like.low_dtype], extra_random_decomps) +def randint_like_low( + self: torch.Tensor, low: int, high: int, **kwargs: Any +) -> torch.Tensor: + return _rand_like(functools.partial(aten.randint.low, low, high), self, **kwargs) + + +@register_decomposition(aten.randint.default) +def randint( + high: int, + size: list[int | torch.SymInt], + **kwargs: Any, +) -> torch.Tensor: + return aten.randint.low(0, high, size, **kwargs) + + +@register_decomposition(quantized.linear_dynamic_fp16_unpacked_weight.default) +def linear_dynamic_fp16_unpacked_weight( + input: torch.Tensor, + weight: torch.Tensor, + bias: torch.Tensor | None = None, +) -> torch.Tensor: + packed_weight = torch.ops._quantized.wrapped_fbgemm_pack_gemm_matrix_fp16(weight) + return torch.ops._quantized.wrapped_fbgemm_linear_fp16_weight( + input, packed_weight, bias, weight.size()[0] + ) + + +@register_decomposition(_quantized.wrapped_quantized_linear.default) +def wrapped_quantized_linear( + input: torch.Tensor, + input_scale: torch.Tensor, + input_zero_point: torch.Tensor, + weight: torch.Tensor, + weight_scale: torch.Tensor, + weight_zero_point: torch.Tensor, + bias: torch.Tensor, + out_scale: torch.Tensor, + out_zero_point: torch.Tensor, + out_channel: int, +) -> torch.Tensor: + packed_weight = torch.ops._quantized._wrapped_linear_prepack( + weight, weight_scale, weight_zero_point, bias + ) + return torch.ops._quantized._wrapped_quantized_linear_prepacked( + input, + input_scale, + input_zero_point, + packed_weight, + out_scale, + out_zero_point, + out_channel, + ) + + +@register_decomposition(torch.ops.quantized.embedding_bag_byte_unpack) +def q_embedding_bag_byte_unpack_decomp(packed: torch.Tensor) -> torch.Tensor: + def bitcast_u8_to_f32(u8: torch.Tensor) -> torch.Tensor: + x, y, z, w = (u8[..., n].to(torch.int32) for n in (0, 1, 2, 3)) + if sys.byteorder == "little": + return (x + (y << 8) + (z << 16) + (w << 24)).view(torch.float32)[..., None] + else: + return ((x << 24) + (y << 16) + (z << 8) + w).view(torch.float32)[..., None] + + scales = bitcast_u8_to_f32(packed[..., -8:-4]) + offsets = bitcast_u8_to_f32(packed[..., -4:]) + return packed[..., :-8].to(torch.float32) * scales + offsets + + +@register_decomposition([aten.grid_sampler_2d]) +@pw_cast_for_opmath +def grid_sampler_2d( + a: torch.Tensor, + grid: torch.Tensor, + interpolation_mode: int = 0, + padding_mode: int = 0, + align_corners: bool = False, +) -> torch.Tensor: + # We do not expand the grid (_expand_grid=False) on cpu for performance reasons + # Experimenting locally it was found that compiled CUDA code is accelerated by ~5x + # and CPU code by ~2x on bicubic mode, if we expand the grid from (N, H, W, 2) into (N, C, H, W, 2) + # However, this leads to a slowdown around ~0.8x on CPU bilinear mode, channels first. + # Thus we apply this hack to not expand the grid for this case. + _expand_grid = not ( + a.device == torch.device("cpu") + and interpolation_mode == 0 + and a.is_contiguous(memory_format=torch.contiguous_format) + ) + + output = decomp_grid_sampler_2d( + a, + grid=grid, + interpolation_mode=interpolation_mode, + padding_mode=padding_mode, + align_corners=align_corners, + _expand_grid=_expand_grid, + ) + return output + + # _foreach_addcmul and _foreach_addcdiv decompositions removed — + # the inductor lowering in lowering.py handles these with FMA. + + +@register_decomposition(aten._foreach_lerp.Scalar) +def _foreach_lerp_scalar( + start_tensors: list[torch.Tensor], + end_tensors: list[torch.Tensor], + weight: torch.types.Number, +) -> list[torch.Tensor]: + return aten._foreach_add.List( + start_tensors, + aten._foreach_mul.Scalar( + aten._foreach_sub.List(end_tensors, start_tensors), weight + ), + ) + + +@register_decomposition(aten._foreach_lerp.ScalarList) +def _foreach_lerp_scalarlist( + start_tensors: list[torch.Tensor], + end_tensors: list[torch.Tensor], + scalars: list[torch.types.Number], +) -> list[torch.Tensor]: + return aten._foreach_add.List( + start_tensors, + aten._foreach_mul.ScalarList( + aten._foreach_sub.List(end_tensors, start_tensors), scalars + ), + ) + + +@aten.miopen_batch_norm.default.py_impl(torch._C.DispatchKey.Autograd) +@register_decomposition(aten.miopen_batch_norm) +def miopen_batch_norm( + input: torch.Tensor, + weight: torch.Tensor, + bias: torch.Tensor | None, + running_mean: torch.Tensor | None, + running_var: torch.Tensor | None, + training: bool, + exponential_average_factor: float, + epsilon: float, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + a, b, c = aten.native_batch_norm( + input, + weight, + bias, + running_mean, + running_var, + training, + exponential_average_factor, + epsilon, + ) + + if training: + return (a, b, c) + return ( + a, + weight.new_zeros((0,)), + weight.new_zeros((0,)), + ) + + +@functools.cache +def fast_random_decomps() -> dict[Any, Callable[..., Any]]: + return {**decompositions, **extra_random_decomps} + + +# TODO(aakhundov): replace this (and the above) Any by more +# specific type and fix all the cascading mypy errors +def select_decomp_table() -> dict[Any, Callable[..., Any]]: + """decomps can change based on config""" + if config.fallback_random: + return decompositions + if config.fallback_embedding_bag_byte_unpack: + # remove q_embedding_bag_byte_unpack_decomp from decompositions + decompositions.pop(torch.ops.quantized.embedding_bag_byte_unpack.default, None) + return decompositions + result = fast_random_decomps() + return result + + +@register_decomposition(aten.masked_scatter) +def masked_scatter( + self: torch.Tensor, + mask: torch.Tensor, + source: torch.Tensor, +) -> torch.Tensor: + from .codegen.common import BackendFeature, has_backend_feature + + if has_backend_feature(self.device, BackendFeature.MASKED_SCATTER_WITH_INDEX): + # This two-step algorithm is the same as eager CUDA, for eager CPU we + # use a 1-shot serial iteration. + self, mask = aten.broadcast_tensors([self, mask]) + source_idx = mask.reshape(-1).cumsum(0) - 1 + self_flat, mask_flat, source_flat = (x.flatten() for x in (self, mask, source)) + result = aten._unsafe_masked_index(source_flat, mask_flat, [source_idx], 0) + return torch.where(mask_flat, result, self_flat).view(self.shape) + return NotImplemented + + +@register_decomposition(quantized_decomposed.choose_qparams.tensor) +def choose_qparams_tensor( + input: torch.Tensor, + quant_min: int, + quant_max: int, + eps: float, + dtype: torch.dtype, +) -> tuple[torch.Tensor, torch.Tensor]: + min_val, max_val = torch.aminmax(input) + scale = (max_val - min_val) / float(quant_max - quant_min) + scale = torch.max(scale, torch.Tensor([eps])) + zero_point = quant_min - torch.round(min_val / scale).to(torch.int) + zero_point = torch.clamp(zero_point, quant_min, quant_max) + return scale.to(torch.float64), zero_point.to(torch.int64) + + +@register_decomposition(aten.put) +def put( + self: torch.Tensor, + index: torch.Tensor, + source: torch.Tensor, + accumulate: bool = False, +) -> torch.Tensor: + flattened = self.flatten() + flattened = torch.index_put( + flattened, [index], source.reshape(index.shape), accumulate + ) + return flattened.reshape(self.shape) + + +@register_decomposition(aten.put_) +def put_( + self: torch.Tensor, + index: torch.Tensor, + source: torch.Tensor, + accumulate: bool = False, +) -> torch.Tensor: + out = aten.put(self, index, source, accumulate=accumulate) + return self.copy_(out) + + +@register_decomposition(aten._softmax_backward_data.default) +@pw_cast_for_opmath +def _softmax_backward_data( + grad_output: torch.Tensor, + output: torch.Tensor, + dim: int, + input_dtype: torch.dtype, +) -> torch.Tensor: + new_grad_output = grad_output * output + sum_new_grad = torch.sum(new_grad_output, dim=dim, keepdim=True) + # grad_input = new_grad_output - output * sum_new_grad + grad_input = inductor_prims.fma(-output, sum_new_grad, new_grad_output) + + # CPU kernel doesn't respect input_dtype, but following check doesn't work for meta tensor + # if grad_output.device == torch.device("cpu"): + # return grad_input.contiguous() + + if grad_output.dtype != input_dtype: + grad_input = grad_input.to(input_dtype) + return grad_input.contiguous() + + +@register_decomposition(aten.index_reduce) +def index_reduce( + self: torch.Tensor, + dim: int, + index: torch.Tensor, + src: torch.Tensor, + reduction_type: str, + *, + include_self: bool = True, +) -> torch.Tensor: + if reduction_type == "mean" and not needs_fallback_due_to_atomic_add_limitations( + self.dtype + ): + true_division = self.dtype.is_floating_point or self.dtype.is_complex + ones = torch.ones_like(src) + if include_self: + out = self + counts = torch.ones_like(self).index_add(dim, index, ones) + else: + out = self.index_fill(dim, index, 0) + counts = torch.zeros_like(self).index_add(dim, index, ones) + counts = counts.masked_fill(counts < 1, 1) + out = out.index_add(dim, index, src) + return out / counts if true_division else out // counts + + if use_scatter_fallback( + aten.scatter_reduce_.two, + reduction_type, + self.dtype, + src.dtype, + src.device.type, + True, + ): + return NotImplemented + + # pyrefly: ignore [missing-attribute] + repeats = self.shape[dim + 1 :].numel() * self.shape[:dim].numel() + index_shape = (index.numel(), *self.shape[dim + 1 :], *self.shape[:dim]) + perm = (*range(self.ndim - dim, self.ndim), 0, *range(1, self.ndim - dim)) + scatter_index = ( + index.to(torch.int64) + .repeat_interleave(repeats) + .reshape(index_shape) + .permute(perm) + ) + return self.scatter_reduce( + dim, + scatter_index, + src, + reduction_type, + include_self=include_self, + ) + + +def _max_pool_with_indices( + x: torch.Tensor, + kernel_size: list[int], + stride: int | list[int] | None, + padding: int | list[int], + dilation: int | list[int], + ceil_mode: bool, + dim: int, +) -> tuple[torch.Tensor, torch.Tensor]: + if dilation == 1: + dilation = [1] * dim + + if padding == 0: + padding = [0] * dim + + if not stride: + stride = kernel_size + + # pyrefly: ignore [bad-assignment] + kernel_size = pad_listlike(kernel_size, dim) + # pyrefly: ignore [bad-assignment] + dilation = pad_listlike(dilation, dim) + # pyrefly: ignore [bad-assignment] + padding = pad_listlike(padding, dim) + # pyrefly: ignore [bad-assignment] + stride = pad_listlike(stride, dim) + + window_size = functools.reduce(operator.mul, kernel_size) + # We fallback when using non-default dilation or when the window size is too large + if ( + torch._inductor.lowering.should_fallback_max_pool_with_indices( + kernel_size, n_dim=dim + ) + or window_size > torch.iinfo(torch.int8).max + ): + return NotImplemented + + vals, offsets = prims._low_memory_max_pool_with_offsets( + x, + kernel_size, + stride, + padding, + dilation, + ceil_mode, + ) + indices = prims._low_memory_max_pool_offsets_to_indices( + offsets, + kernel_size, + x.shape[-dim:], + stride, + padding, + dilation, + ) + return vals, indices + + +@register_decomposition(aten.max_pool2d_with_indices) +def max_pool2d_with_indices( + x: torch.Tensor, + kernel_size: list[int], + stride: int | list[int] | None = None, + padding: int | list[int] = 0, + dilation: int | list[int] = 1, + ceil_mode: bool = False, +) -> tuple[torch.Tensor, torch.Tensor]: + return _max_pool_with_indices( + x, kernel_size, stride, padding, dilation, ceil_mode, dim=2 + ) + + +@register_decomposition(aten.max_pool3d_with_indices) +def max_pool3d_with_indices( + x: torch.Tensor, + kernel_size: list[int], + stride: int | list[int] | None = None, + padding: int | list[int] = 0, + dilation: int | list[int] = 1, + ceil_mode: bool = False, +) -> tuple[torch.Tensor, torch.Tensor]: + return _max_pool_with_indices( + x, kernel_size, stride, padding, dilation, ceil_mode, dim=3 + ) + + +@register_decomposition(aten.adaptive_max_pool2d) +def adaptive_max_pool2d( + x: torch.Tensor, output_size: list[int] +) -> tuple[torch.Tensor, torch.Tensor]: + *batch, h_in, w_in = x.shape + h_out, w_out = output_size + + if h_out == 0 or w_out == 0: + o_size = [*batch, h_out, w_out] + return x.new_empty(o_size), x.new_empty(o_size, dtype=torch.int64) + + if h_in % h_out == 0 and w_in % w_out == 0: + kernel_size = [h_in // h_out, w_in // w_out] + return aten.max_pool2d_with_indices(x, kernel_size) + + return NotImplemented + + +@register_decomposition(aten.searchsorted.Scalar) +def searchsorted_scalar( + sorted_sequence: torch.Tensor, + self: torch.types.Number, + *, + out_int32: bool = False, + right: bool = False, + side: str | None = None, + sorter: torch.Tensor | None = None, +) -> torch.Tensor: + return aten.searchsorted( + sorted_sequence, + torch.tensor([self], device=sorted_sequence.device), + out_int32=out_int32, + right=right, + side=side, + sorter=sorter, + )[0] + + +@register_decomposition(aten.bucketize.Scalar) +def bucketize_scalar( + self: torch.types.Number, + boundaries: torch.Tensor, + *, + out_int32: bool = False, + right: bool = False, +) -> torch.Tensor: + return aten.bucketize( + torch.tensor([self], device=boundaries.device), + boundaries, + out_int32=out_int32, + right=right, + ).squeeze(0) + + +@register_decomposition(aten.rrelu_with_noise_functional) +def rrelu_with_noise_functional( + self: torch.Tensor, + noise: torch.Tensor, + lower: float = 0.125, + upper: float = 0.3333333333333333, + training: bool = False, + generator: torch.Generator | None = None, +) -> tuple[torch.Tensor, torch.Tensor]: + if training: + not_positive = self <= 0 + r = aten.uniform(self, lower, upper, generator=generator) + output = torch.where(not_positive, self * r, self) + noise_out = torch.where(not_positive, r, 1) + return output, noise_out + else: + negative_slope = (lower + upper) / 2 + return aten.leaky_relu(self, negative_slope), torch.Tensor() + + +@register_decomposition(aten.repeat_interleave.Tensor) +def repeat_interleave_Tensor( + repeat: torch.Tensor, + output_size: int | None = None, +) -> torch.Tensor: + if config.triton.autotune_at_compile_time: + # We can't compile-time auto-tune this because + # it expects specific data in `repeat` + return NotImplemented + if output_size is None or type(output_size) is not int: + return NotImplemented + if repeat.device.type == "mps": + return NotImplemented + assert repeat.dtype in [torch.int32, torch.int64] + assert repeat.ndim == 1 + cumsum = repeat.cumsum(0) + pos = torch.arange(output_size, device=repeat.device) + indices = torch.searchsorted( + cumsum, pos, out_int32=(repeat.dtype == torch.int32), right=True + ) + return torch.clamp(indices, max=repeat.size(0) - 1) + + +# intentionally not regiestered +def conv1d_to_conv2d( + input: torch.Tensor, + weight: torch.Tensor, + bias: torch.Tensor | None = None, + stride: tuple[int] = (1,), + padding: tuple[int] = (0,), + dilation: tuple[int] = (1,), + groups: int = 1, +) -> torch.Tensor: + # Shapes: + # input: (N, C_in, L_in) + # weight: (C_out, C_in // groups, K) + # bias: (C_out,) + assert input.dim() == 3 and weight.dim() == 3, ( + "Expect (N,C_in,L) and (C_out,C_in//groups,K)" + ) + + # pyrefly: ignore [bad-assignment] + stride = stride[0] + # pyrefly: ignore [bad-assignment] + padding = padding[0] + # pyrefly: ignore [bad-assignment] + dilation = dilation[0] + + # Unsqueeze to make input 2D: (N,C,L) -> (N,C,L,1) + input_2d = input.unsqueeze(-1) + # Unsqueeze kernel: (C_out,C_in/groups,K) -> (C_out,C_in/groups,K,1) + weight_2d = weight.unsqueeze(-1) + + # Call conv2d with adjusted args + out_2d = aten.conv2d.default( + input_2d, + weight_2d, + bias, + stride=(stride, 1), + padding=(padding, 0), + dilation=(dilation, 1), + groups=groups, + ) + + # Squeeze dummy dimension back out: (N,C_out,L_out,1) -> (N,C_out,L_out) + return out_2d.squeeze(-1) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/dependencies.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/dependencies.py new file mode 100644 index 0000000000000000000000000000000000000000..893cf4b6a62e1cf879d601eaf7c0454890aa1919 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/dependencies.py @@ -0,0 +1,903 @@ +import abc +import dataclasses +import itertools +import logging +import re +from collections.abc import Callable, Iterable, Sequence +from typing import Any, TypeVar +from typing_extensions import Self +from unittest.mock import patch + +import sympy + +import torch +from torch._inductor.utils import get_free_symbols +from torch.fx.experimental.symbolic_shapes import free_symbols, free_unbacked_symbols +from torch.utils._ordered_set import OrderedSet + +from ..utils._sympy.symbol import make_symbol, SymT +from .codegen.common import index_prevent_reordering +from .ops_handler import DefaultHandler +from .utils import ( + get_dtype_size, + reduction_num_outputs, + sympy_index_symbol, + sympy_subs, + VarRanges, +) +from .virtualized import ReductionType, V + + +T = TypeVar("T") + +log = logging.getLogger(__name__) +is_indirect = re.compile(r"indirect|tmp").search + + +class Dep(abc.ABC): + name: str + index: sympy.Expr + + @abc.abstractmethod + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + pass + + @abc.abstractmethod + def rename(self, renames: dict[str, str]) -> Self: + pass + + @abc.abstractmethod + def get_numel(self) -> sympy.Expr: + pass + + @abc.abstractmethod + def numbytes_hint(self) -> int: + pass + + @abc.abstractmethod + def numel_hint(self) -> int: + pass + + @abc.abstractmethod + def has_unbacked_symbols(self) -> bool: + pass + + @abc.abstractmethod + def is_contiguous(self) -> bool: + pass + + def normalize_with_stride_order(self, prefix: str = "t") -> Self: + return self + + +@dataclasses.dataclass(frozen=True) +class MemoryDep(Dep): + # pyrefly: ignore [bad-override] + name: str + # pyrefly: ignore [bad-override] + index: sympy.Expr + var_names: tuple[sympy.Symbol, ...] + size: tuple[sympy.Expr, ...] + mode: str | None = None + + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + return ( + get_free_symbols(self.index, unbacked_only) + | get_free_symbols(self.size, unbacked_only) + | get_free_symbols(self.var_names, unbacked_only) + ) + + def __repr__(self) -> str: + maybe_mode = "" + if self.mode is not None: + maybe_mode = f", {self.mode}" + return f"MemoryDep({self.name!r}, {self.index}, {self.ranges}{maybe_mode})" + + @property + def num_vars(self) -> int: + return len(self.var_names) + + def decide_loop_order_to_match(self, other: "MemoryDep") -> list[int] | None: + """ + Can return None if not able to decide loop orders. + """ + assert self.num_vars == other.num_vars + + # ignore broadcast for now since broadcast causes extra 0 strides + # which makes it hard to decide the correct loop orders. + if self.num_vars != len(self.index.free_symbols): + return None + if other.num_vars != len(other.index.free_symbols): + return None + + # bail out if any size is 0 or 1 + # For size == 0, it's an empty tensor, any strides for that dimension + # are equivalent. Skip for simplicity and it may not matter that much. + # + # For size == 1, it cause cause tie for strides of different dimensions. + # Also when we first time create LoopBody in ComputedBuffer.simplify_and_reorder + # we can dependencies.index_vars_squeeze which should already sqeeuze + # the size == 1 dimensions. + if any(s == 0 or s == 1 for s in itertools.chain(self.size, other.size)): + return None + + # Extract strides for both expression + self_strides = V.graph.sizevars.stride_hints(self.index, self.var_names) + other_strides = V.graph.sizevars.stride_hints(other.index, other.var_names) + + # Even if the shape contains no 0/1, some complex index expression may + # still have duplicate stride values. Here is an example: + # https://gist.github.com/shunting314/511a7e1ec88aa2e1a8ec85d8445ab129 + # We don't reorder the loop for these cases for now, but in theory + # we could improve the algorithm to detect the correct loop orders. + if len(OrderedSet(self_strides)) != len(self_strides) or len( + OrderedSet(other_strides) + ) != len(other_strides): + log.debug( + "unable to decide loop order. self_dep=%s v.s. other_dep=%s, self_strides=%s v.s. other_strides=%s", + self, + other, + self_strides, + other_strides, + ) + return None + + # May happen if self and other are as follows + # MemoryDep('addmm_6', 393216*d0 + 768*d1 + d2, {d0: 16, d1: 512, d2: 768}, None) + # MemoryDep('addmm_6', 98304*d0 + d1 + 768*d2, {d0: 64, d1: 768, d2: 128}, None) + if OrderedSet(self_strides) != OrderedSet(other_strides): + return None + + stride_to_index = {s: i for i, s in enumerate(self_strides)} + order = [stride_to_index[s] for s in other_strides] + + assert OrderedSet(order) == OrderedSet(range(self.num_vars)) + return order + + def get_offset(self) -> sympy.Expr: + """ + Return the offset by setting every variable to be 0. + """ + return sympy_subs(self.index, dict.fromkeys(self.var_names, 0)) + + def normalize(self) -> "MemoryDep": + """ + Normalize by merging loops. The different to normalize_with_stride_order is, + this method does not reorder loops while normalize_with_stride_order reorder + loops based on stride order. + """ + return MemoryDep( + self.name, + *_RecordLoadStoreInner._normalize(self.index, self.ranges), # type: ignore[arg-type] + self.mode, + ) + + def normalize_with_stride_order(self, prefix: str = "t") -> "MemoryDep": + r""" + Used to decide if two MemoryDep does not equal due to different loop orders. + More specifically, when dep1 and dep2 are not equal, we can normalize + both and check if they are equal after that. If yes, then the mismatch is + caused by different loop orders. + """ + # import here to avoid circular import + from torch._inductor import ir + + strides = V.graph.sizevars.stride_hints(self.index, self.var_names) + + # pick a loop order with stride ordered decreasingly + order = sorted(range(len(strides)), key=strides.__getitem__, reverse=True) + stride_reorder = ir.same_reorder(order) + sizes = self.size + var_names = self.var_names + + new_reordered_sizes = stride_reorder(sizes) + new_reordered_var_names = stride_reorder(var_names) + + new_simplified_sizes, reindex, _prune = V.graph.sizevars._simplify_loops( + new_reordered_var_names, + new_reordered_sizes, + index_prevent_reordering( + [self.index], new_reordered_var_names, new_reordered_sizes + ), + ) + + # now let's create new symbols with the passed in prefix + var_ranges, add_var = var_builder(prefix) + replacement = dict( + zip( + new_reordered_var_names, + reindex([add_var(x) for x in new_simplified_sizes]), + ) + ) + new_index = sympy_subs(sympy.expand(self.index), replacement) # type: ignore[arg-type] # next PR + + out = MemoryDep( + self.name, new_index, tuple(var_ranges.keys()), tuple(var_ranges.values()) + ) # type: ignore[arg-type] + return out + + @property + def ranges(self) -> dict[sympy.Symbol, sympy.Expr]: + """{c0: 128, c1: 512, ...}""" + return dict(zip(self.var_names, self.size)) + + def simplify_with_ranges(self) -> "MemoryDep": + return MemoryDep( + name=self.name, + index=V.graph.sizevars.simplify_with_ranges(self.index, self.ranges), + var_names=self.var_names, + size=self.size, + mode=self.mode, + ) + + def get_numel(self) -> sympy.Expr: + if self.is_indirect(): + numel = V.graph.get_numel(self.name) + else: + vars: OrderedSet[sympy.Basic] = OrderedSet(self.index.free_symbols) + numel = sympy.S.One + for var, size in zip(self.var_names, self.size): + if var in vars: + numel = numel * size + return numel # type: ignore[return-value] + + def rename(self, renames: dict[str, str]) -> "MemoryDep": + if self.name in renames: + return MemoryDep( + renames[self.name], + self.index, + var_names=self.var_names, + size=self.size, + mode=self.mode, + ) + return self + + def numbytes_hint(self) -> int: + try: + return V.graph.sizevars.optimization_hint( + self.get_numel(), fallback=0 + ) * get_dtype_size(V.graph.get_dtype(self.name)) + except NotImplementedError: # NoneLayout + return 0 + + def numel_hint(self) -> int: + try: + return V.graph.sizevars.optimization_hint(self.get_numel(), fallback=0) + except NotImplementedError: # NoneLayout + return 0 + + def has_unbacked_symbols(self) -> bool: + return len(free_unbacked_symbols(self.get_numel())) > 0 + + def is_contiguous(self) -> bool: + if isinstance(self.index, sympy.Integer): + return True + return isinstance(self.index, sympy.Symbol) and self.index in self.var_names + + def stride1_for_last_dim(self, result_for_complex_expression: bool = True) -> bool: + """ + Whether the stride for the last dimension is 1. + """ + # python test/inductor/test_torchinductor_opinfo.py -k test_comprehensive_masked_scatter_cuda_float16 + # will exercise thru this corner case. + if len(self.var_names) == 0: + return True + + terms = self.index.args if isinstance(self.index, sympy.Add) else [self.index] + + last_sym = self.var_names[-1] + for term in terms: + if term == last_sym: + return True + + # Having a >1 stride for the last dimension is bad for perf + # return False. + if ( + isinstance(term, sympy.Mul) + and len(term.args) == 2 + and term.args[1] == last_sym + and isinstance(term.args[0], (int, sympy.Integer)) + and term.args[0] > 1 + ): + return False + + return result_for_complex_expression + + def is_scalar(self) -> bool: + if isinstance(self.index, sympy.Symbol): + return self.index not in self.var_names and not self.is_indirect() + return isinstance(self.index, (int, sympy.Integer)) + + def is_indirect(self) -> bool: + return any(is_indirect(v.name) for v in self.index.free_symbols) # type: ignore[attr-defined] + + +@dataclasses.dataclass(frozen=True) +class StarDep(Dep): + # pyrefly: ignore [bad-override] + name: str + mode: str | None = None + + # depends on the entire buffer + @property + # pyrefly: ignore [bad-override] + def index(self) -> sympy.Expr: + raise NotImplementedError("StarDep does not have an index") + + def get_numel(self) -> sympy.Expr: + return V.graph.get_numel(self.name) # type: ignore[return-value] + + def rename(self, renames: dict[str, str]) -> "StarDep": + if self.name in renames: + return StarDep(renames[self.name], self.mode) + return self + + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + return OrderedSet() + + def numbytes_hint(self) -> int: + try: + return V.graph.sizevars.optimization_hint( + self.get_numel(), fallback=0 + ) * get_dtype_size(V.graph.get_dtype(self.name)) + except NotImplementedError: + return 0 # NoneLayout, MultiOutputLayout, etc + + def numel_hint(self) -> int: + try: + return V.graph.sizevars.optimization_hint(self.get_numel(), fallback=0) + except NotImplementedError: + return 0 # NoneLayout, MultiOutputLayout, etc + + def has_unbacked_symbols(self) -> bool: + return len(free_unbacked_symbols(self.get_numel())) > 0 + + def is_contiguous(self) -> bool: + return False + + def is_scalar(self) -> bool: + return False + + def is_indirect(self) -> bool: + return False + + +# Used for tracking mutation ordering +# if A reads a buffer and B mutates it +# B must be ordered after A +# +# This is useful for a variety of reasons. +# For example, if A's read is never actually used, we can eliminate it. +# Another case is if A's buffer ends up being fused away, we never need to +# materialize that buffer +@dataclasses.dataclass(frozen=True) +class WeakDep(Dep): + # Fake dependency on unused buffer + # pyrefly: ignore [bad-override] + name: str + # Buffer that is doing the mutation + mutating_buf: str + # WeakDep's are also used to add dependencies to prevent some specific reordering, + # E.g. collectives global ordering. + # But if other pass guarantees proper ordering by its logic, + # This additional "fake" deps will be holding optimizations. + # This flag is used to identify those additional deps. + is_fake: bool = False + + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + return OrderedSet() + + @property + # pyrefly: ignore [bad-override] + def index(self) -> sympy.Expr: + raise NotImplementedError("WeakDep does not have an index") + + def get_numel(self) -> sympy.Expr: + return sympy.S.One + + def rename(self, renames: dict[str, str]) -> "WeakDep": + if self.name in renames: + return WeakDep(renames[self.name], self.mutating_buf, self.is_fake) + return self + + def numbytes_hint(self) -> int: + return 1 # Purely inserted for ordering, not an actual dep + + def numel_hint(self) -> int: + return 1 # Purely inserted for ordering, not an actual dep + + def has_unbacked_symbols(self) -> bool: + return False + + def is_contiguous(self) -> bool: + return False + + +@dataclasses.dataclass(frozen=True) +class IndexExprDep: + index: sympy.Expr # type: ignore[assignment] + var_names: tuple[sympy.Symbol, ...] + size: tuple[sympy.Expr, ...] + + +@dataclasses.dataclass +class ReadWrites: + reads: OrderedSet[Dep] + writes: OrderedSet[Dep] + index_exprs: OrderedSet[IndexExprDep] + range_vars: list[sympy.Expr] | None = None + var_ranges: VarRanges | None = None + + def rename(self, renames: dict[str, str]) -> "ReadWrites": + return ReadWrites( + OrderedSet(dep.rename(renames) for dep in self.reads), + OrderedSet(dep.rename(renames) for dep in self.writes), + self.index_exprs, + self.range_vars, + self.var_ranges, + ) + + def with_read(self, dep: Dep | OrderedSet[Dep]) -> "ReadWrites": + assert isinstance(dep, (WeakDep, StarDep, OrderedSet)) + if not isinstance(dep, OrderedSet): + dep = OrderedSet([dep]) + return ReadWrites( + OrderedSet.union(self.reads, dep), + self.writes, + self.index_exprs, + self.range_vars, + self.var_ranges, + ) + + def merge(self, other: "ReadWrites") -> "ReadWrites": + reads = OrderedSet.union(self.reads, other.reads) + writes = OrderedSet.union(self.writes, other.writes) + index_exprs = OrderedSet.union(self.index_exprs, other.index_exprs) + return ReadWrites(reads - writes, writes, index_exprs) + + @staticmethod + def merge_list(read_writes: list["ReadWrites"]) -> "ReadWrites": + all_writes = OrderedSet.union(*[rw.writes for rw in read_writes]) + all_reads = OrderedSet.union(*[rw.reads for rw in read_writes]) - all_writes + all_index_exprs = OrderedSet.union(*[rw.index_exprs for rw in read_writes]) + return ReadWrites(all_reads, all_writes, all_index_exprs) + + def remove_reads(self, rem_reads: OrderedSet[Dep]) -> "ReadWrites": + return ReadWrites( + self.reads - rem_reads, + self.writes, + self.index_exprs, + self.range_vars, + self.var_ranges, + ) + + def reads_and_writes(self) -> Iterable[Dep]: + return itertools.chain(self.reads, self.writes) + + def buffer_names(self, ignore_integer_index: bool = True) -> OrderedSet[str]: + """ + Integer index is used for load_seed. + """ + names: OrderedSet[str] = OrderedSet() + for dep in self.reads_and_writes(): + if not isinstance(dep, MemoryDep): + continue + if not ignore_integer_index or not isinstance( + dep.index, (int, sympy.Integer) + ): + names.add(dep.name) + return names + + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + result: OrderedSet[sympy.Symbol] = OrderedSet() + + for dep in self.reads_and_writes(): + result |= dep.get_free_symbol_uses(unbacked_only) + return result + + +class _RecordLoadStoreInner(V.MockHandler): # type: ignore[name-defined] + def __init__(self, var_ranges: VarRanges, normalize: bool) -> None: + super().__init__() + self._reads: OrderedSet[Dep] = OrderedSet() + self._writes: OrderedSet[MemoryDep] = OrderedSet() + self._index_exprs: OrderedSet[IndexExprDep] = OrderedSet() + self._var_ranges: VarRanges = var_ranges + self._should_normalize: bool = normalize + + @staticmethod + def drop_unused_symbols( + index: int | sympy.Expr, + var_names: list[sympy.Expr], + sizes: list[sympy.Expr], + ) -> None: + """ + Reduction has last (reduced) dim in its sizes, but + downstream users won't. Normalize this away. + """ + if not isinstance(index, sympy.Expr): + # index can be an int + return + free_symbols = index.free_symbols + while var_names and var_names[-1] not in free_symbols: + var_names.pop() + sizes.pop() + + @classmethod + def _normalize( + cls, index: sympy.Expr, var_ranges: VarRanges + ) -> tuple[sympy.Expr, tuple[sympy.Symbol, ...], tuple[sympy.Expr, ...]]: + # Try to further simplify the indexes even if simplify_loops didn't + # convert it to the simplest form because of the interference from + # different indexing formulas. + index_vars = [*var_ranges.keys()] + sizes = tuple(var_ranges.values()) # type: ignore[assignment] + new_sizes, reindex, _prune = V.graph.sizevars._simplify_loops( + index_vars, + sizes, + index_prevent_reordering([index], index_vars, sizes), + ) + + # assign new variables each dimension to deal with numbering mismatches + # d0, d1, d2 could become d0, d2 -- which won't match d0, d1 + new_vars, add_var = var_builder(canonicalization_prefix()) + replacement = dict(zip(index_vars, reindex([add_var(x) for x in new_sizes]))) + index = sympy_subs(sympy.expand(index), replacement) + + new_vars = [*new_vars.keys()] + new_sizes = [*new_sizes] + cls.drop_unused_symbols(index, new_vars, new_sizes) + return index, tuple(new_vars), tuple(new_sizes) # type: ignore[arg-type] + + def canonicalize( + self, index: sympy.Expr + ) -> tuple[sympy.Expr, tuple[sympy.Symbol, ...], tuple[sympy.Expr, ...]]: + if not self._should_normalize: + sizes = [V.graph.sizevars.simplify(x) for x in self._var_ranges.values()] + var_names = [k for k, v in zip(self._var_ranges.keys(), sizes) if v != 1] + sizes = [v for v in sizes if v != 1] + + self.drop_unused_symbols(index, var_names, sizes) + + return index, tuple(var_names), tuple(sizes) # type: ignore[return-value, arg-type] + var_ranges = { + k: V.graph.sizevars.simplify(v) + for k, v in self._var_ranges.items() + # TODO(jansel): explore this further normalization + # if k in free_symbols + } + return self._normalize(index, var_ranges) + + def load(self, name: str, index: sympy.Expr) -> None: + self._reads.add(MemoryDep(name, *self.canonicalize(index))) + + def load_seed(self, name: str, index: int) -> None: + assert isinstance(index, int) + self.load(name, sympy.Integer(index)) + + def store( + self, name: str, index: sympy.Expr, value: str, mode: str | None = None + ) -> None: + self._writes.add(MemoryDep(name, *self.canonicalize(index), mode=mode)) + + def store_reduction(self, name: str, index: sympy.Expr, value: str) -> None: + self.store(name, index, f"store_reduction({value})") + + def index_expr(self, index: sympy.Expr, dtype: torch.dtype | None) -> None: + self._index_exprs.add(IndexExprDep(*self.canonicalize(index))) + + def bucketize( + self, + values: T, + boundaries: tuple[str, sympy.Expr, sympy.Expr, sympy.Expr], + boundary_indices: T, + indexing_dtype: torch.dtype, + right: bool, + sorter: tuple[str, sympy.Expr] | None = None, + sorter_indices: T | None = None, + ) -> None: + """Records the names of the buffers that bucketize will read from.""" + self._reads.add(StarDep(boundaries[0])) + if sorter is not None: + self._reads.add(StarDep(sorter[0])) + + +class RecordLoadStore(V.KernelFormatterHandler): # type: ignore[name-defined] + def __init__(self, var_ranges: VarRanges, normalize: bool) -> None: + parent_handler = _RecordLoadStoreInner( + var_ranges=var_ranges, normalize=normalize + ) + super().__init__(parent_handler=parent_handler) + + +# TODO: check call sites +def var_builder(prefix: str) -> tuple[VarRanges, Callable[[sympy.Expr], sympy.Symbol]]: + cnt = itertools.count() + var_ranges: VarRanges = {} + + def add_var(length: sympy.Expr) -> sympy.Symbol: + v = sympy_index_symbol(f"{prefix}{next(cnt)}") + var_ranges[v] = length + return v + + return var_ranges, add_var + + +def index_vars_no_squeeze( + *argsizes: Sequence[sympy.Expr], prefix: str +) -> tuple[list[list[sympy.Symbol]], VarRanges]: + var_ranges, add_var = var_builder(prefix) + args: list[list[sympy.Symbol]] = [list(map(add_var, size)) for size in argsizes] + return args, var_ranges + + +def index_vars_squeeze( + *argsizes: Sequence[sympy.Expr], prefix: str = "d" +) -> tuple[list[Sequence[sympy.Expr]], VarRanges]: + from .ir import SqueezeView + + var_ranges, add_var = var_builder(prefix) + args: list[Sequence[sympy.Expr]] = [] + new_sizes: list[Sequence[sympy.Expr]] = [] + for size in argsizes: + new_size, reindex = SqueezeView.squeezer(size) + new_sizes.append(new_size) + args.append(reindex(list(map(add_var, new_size)))) + return args, var_ranges + + +def extract_read_writes( + fn: Callable[..., Any], + *argsizes: Sequence[sympy.Expr], + normalize: bool = False, + prefix: str = "d", + hidden_args: Sequence[list[sympy.Expr]] = (), +) -> ReadWrites: + args, var_ranges = index_vars_squeeze(*argsizes, prefix=prefix) + + from .loop_body import LoopBody + + if isinstance(fn, LoopBody): + inner = extract_loop_body_with_args( + fn, + [*args, *hidden_args], # type: ignore[list-item] + var_ranges, + normalize, + ) + else: + # Slow path tracing the function + rw = RecordLoadStore(var_ranges, normalize=normalize) + with V.set_ops_handler(rw): + fn(*args, *hidden_args) + inner = rw.parent_handler + + if normalize: + range_vars = [] # Number of vars could differ due to normalization + else: + range_vars = [*itertools.chain.from_iterable(args)] + + return ReadWrites( + # pyrefly: ignore [missing-attribute] + OrderedSet(inner._reads), + # pyrefly: ignore [missing-attribute] + OrderedSet(inner._writes), + # pyrefly: ignore [missing-attribute] + inner._index_exprs, + range_vars, + var_ranges, + ) + + +def extract_loop_body_with_args( + fn: Any, + args: list[list[sympy.Expr]], + var_ranges: VarRanges, + normalize: bool = False, +) -> _RecordLoadStoreInner: + from .loop_body import MemoryUsageType + + # Fast path to avoid tracing when we already have a LoopBody + inner = _RecordLoadStoreInner(var_ranges=var_ranges, normalize=normalize) + name_to_index = fn.indexing_from_args(args) + if fn.indirect_vars: + # mimic the `tmpX` naming tracing gives us + repl = {v: make_symbol(SymT.TMP, i) for i, v in enumerate(fn.indirect_vars)} + name_to_index = {k: sympy_subs(v, repl) for k, v in name_to_index.items()} # type: ignore[arg-type] + for entry in fn.memory_usage[MemoryUsageType.LOAD]: + inner.load(entry.buffer_name, name_to_index[entry.index_name]) # type: ignore[arg-type] + for entry in fn.memory_usage[MemoryUsageType.LOAD_SEED]: + inner.load_seed(entry.buffer_name, int(name_to_index[entry.index_name])) # type: ignore[arg-type] + for entry in fn.memory_usage[MemoryUsageType.STORE]: + inner.store( + entry.buffer_name, + name_to_index[entry.index_name], + None, # type: ignore[arg-type] + entry.mode, + ) + for entry in fn.memory_usage[MemoryUsageType.STORE_REDUCTION]: + inner.store_reduction( + entry.buffer_name, + name_to_index[entry.index_name], + None, # type: ignore[arg-type] + ) + for entry in fn.memory_usage[MemoryUsageType.INDEX_EXPR]: + inner.index_expr(name_to_index[entry.index_name], None) + for entry in fn.memory_usage[MemoryUsageType.BUCKETIZE]: + # All that matters is that we record the buffer name, so place it in the + # "boundaries" name position to ensure that it's recorded. + inner.bucketize( + None, + (entry.buffer_name, None, None, None), + None, + None, # type: ignore[arg-type] + None, # type: ignore[arg-type] + ) + # fn.memory_usage[MemoryUsageType.CHECK_BOUNDS] intentionally skipped + return inner + + +def extract_input_node_reduction_ranges( + input_node: "torch._inductor.ir.IRNode", +) -> tuple[list[sympy.Expr] | None, list[sympy.Expr] | None]: + """ + Returns the size and reduction size of all inputs, if the sizes and reduction_sizes (if exist) are all the same. + It's possible that a node has multiple inputs, some are Reduction nodes and others are Pointwise nodes. + In this case, reduction_sizes of the Reduction nodes need to be the same. + Otherwise returns (None, None). + """ + + from .ir import ComputedBuffer, ExternKernel, Loops + + size: list[sympy.Expr] | None + reduction_size: list[sympy.Expr] | None + + if isinstance(input_node.get_defining_op(), ComputedBuffer): + # Input node has already been realized. Return its size and reduction_size. + size = [*input_node.get_size()] + reduction_size = [*input_node.get_reduction_size()] + if len(reduction_size) > 0: + return (size, reduction_size) + else: + return (None, None) + + if not isinstance(input_node.data.data, Loops): # type: ignore[attr-defined] + # Other IRNodes do not have reduction_ranges. + return (None, None) + + # There is one issue: what if there are views / permutations between the input node and its dependent realized nodes? + # The current method still uses reduction ranges from the dependent realized node, which is not ideal. + # Is there a way to check whether there are permutations in between? + reads = input_node.get_reads() + reduction_size: list[sympy.Expr] | None = None + size: list[sympy.Expr] | None = None + while reduction_size is None and len(reads) > 0: + seen: OrderedSet[str] = OrderedSet() + new_reads: list[Dep] = [] + for read in reads: + if not isinstance(read, MemoryDep): + continue + if read.name in seen: + continue + seen.add(read.name) + buffer = V.graph.try_get_buffer(read.name) + if buffer is None: + continue + op = buffer.get_defining_op() + if op is None or isinstance(op, ExternKernel): + continue + + if isinstance(op, ComputedBuffer) and len(op.get_reduction_size()) > 0: + if reduction_size is None: + reduction_size = [*op.get_reduction_size()] + size = [*op.get_size()] + elif reduction_size != [*op.get_reduction_size()] or size != [ + *op.get_size() + ]: + return (None, None) + else: + new_reads.extend(op.get_reads()) + if reads == new_reads: + return (size, reduction_size) + else: + reads = OrderedSet(new_reads) + return (size, reduction_size) + + +def canonicalization_prefix() -> str: + return "c" + + +# ops handler which computes all the free symbols for an IR +class FreeSymbolsOpsHandler(DefaultHandler): + symbols: OrderedSet[sympy.Symbol] + + def __init__(self, unbacked_only: bool = True) -> None: + self.symbols = OrderedSet() + self.get_symbols = free_unbacked_symbols if unbacked_only else free_symbols + + def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: + for a in itertools.chain(args, kwargs.values()): + if isinstance(a, (sympy.Expr, sympy.logic.boolalg.Boolean)): + self.symbols |= self.get_symbols(a) + + def indirect_indexing( + self, + index_var: Any, + size: int | sympy.Expr, + check: bool = True, + wrap_neg: bool = True, + ) -> sympy.Symbol: + assert not isinstance(index_var, (sympy.Expr, sympy.logic.boolalg.Boolean)) + self.symbols |= self.get_symbols(size) + return sympy_index_symbol(f"({str(index_var)})") + + def frexp(self, x: Any) -> tuple[None, ...]: + return (None,) * 2 + + def scan( + self, dtypes: Any, combine_fn: Any, values: Sequence[Any] + ) -> tuple[None, ...]: + return (None,) * len(values) + + def sort( + self, dtypes: Any, values: Sequence[Any], stable: Any, descending: Any + ) -> tuple[None, ...]: + return (None,) * len(values) + + def reduction( + self, + dtype: torch.dtype, + src_dtype: torch.dtype, + reduction_type: ReductionType, + value: None | tuple[None, ...], + ) -> None | tuple[None, ...]: + num_values = reduction_num_outputs(reduction_type) + return (None,) * num_values if num_values > 1 else None + + def masked(self, mask: Any, body: Callable[..., Any], other: Any) -> None: + assert callable(body), "masked body must always be callable." + # The body can make additional calls, for e.g. ops.indirect_indexing + body() + + +def extract_free_symbols( + fn: Callable[..., Any], + index: Sequence[sympy.Expr], + rindex: Sequence[sympy.Expr] | None = None, + unbacked_only: bool = True, +) -> OrderedSet[sympy.Symbol]: + from .ir import FlexibleLayout + + args = [index, rindex] if rindex is not None else [index] + handler = FreeSymbolsOpsHandler(unbacked_only) + # NB: I cargo culted the allow_indexing patch here, I don't understand why + # people do this all over + with ( + V.set_ops_handler(handler), + patch.object(FlexibleLayout, "allow_indexing", True), + ): + fn(*args) + return handler.symbols + + +class SymbolUsageCollectorOpsHandler(DefaultHandler): + usages: OrderedSet[str] + + def __init__(self, symbol: sympy.Symbol) -> None: + self.symbol = symbol + self.usages = OrderedSet() + + def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: + used_here = self.symbol in args or self.symbol in kwargs.values() + if used_here: + self.usages.add(name) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/distributed_autotune.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/distributed_autotune.py new file mode 100644 index 0000000000000000000000000000000000000000..d066244a863443d305d781dfc12f0f8ead474e10 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/distributed_autotune.py @@ -0,0 +1,385 @@ +from __future__ import annotations + +import contextlib +import dataclasses +from typing import Any, TYPE_CHECKING +from unittest.mock import patch + +import sympy + +import torch._logging +import torch.distributed as dist +import torch.fx +from torch.utils._ordered_set import OrderedSet + +from . import config, select_algorithm +from .ir import ( + Buffer, + ChoiceCaller, + Layout, + MultiTemplateBuffer, + OperationBuffer, + StorageBox, + TensorBox, +) +from .kernel_inputs import KernelInputs, MMKernelInputs +from .scheduler import SchedulerNode +from .virtualized import NullHandler, V + + +if TYPE_CHECKING: + from collections.abc import Generator, Sequence + + +_DISTRIBUTED_AUTOTUNE_KEY = "distributed_autotune" + +_AUTOTUNE_PG: dist.ProcessGroup | None = None + + +@dataclasses.dataclass +class _DistributedAutotuneState: + """ + State used to track autotuning during a graph_context() + """ + + # This is the next operator index. Used to figure out which rank should do + # the autotuning. + autotuned_index: int = 0 + + # For debugging - used to make sure that we autotune the same number of + # local operators that we expected to. + autotuned_local_count: int = 0 + + +@dataclasses.dataclass +class _DistributedAutotuneInfo: + index: int + local: bool + + +def get_autotune_pg() -> dist.ProcessGroup | None: + if dist.is_available() and dist.is_initialized(): + global _AUTOTUNE_PG + if _AUTOTUNE_PG is None: + _AUTOTUNE_PG = dist.distributed_c10d._new_group_with_tag( + pg_tag="pt2_distributed_autotune_pg" + ) + return _AUTOTUNE_PG + + return None + + +def schedule(scheduler: torch._inductor.scheduler.Scheduler) -> None: + """ + Finish the distributed autotuning by propagating the autotuning results + between the ranks and then replacing the placeholder with the real Buffer. + """ + assert config.distributed_max_autotune_gemm + autotune_results = _autotune_local_nodes(scheduler) + choices_by_index = _sync(autotune_results) + _autotune_remote_nodes(scheduler, choices_by_index) + + +@contextlib.contextmanager +def graph_context() -> Generator[None, None, None]: + """ + Wrapped around processing a graph, sets up figuring out which ranks tune + which shapes. + """ + assert not isinstance( + V.get_distributed_autotune_state(check_poisoned=False), # type: ignore[call-arg] + _DistributedAutotuneState, + ) + V.set_distributed_autotune_state(_DistributedAutotuneState()) + try: + yield + finally: + V.set_distributed_autotune_state(NullHandler()) + + +def maybe_autotune_remote( + name: str, choices: list[ChoiceCaller], inputs: list[Buffer], layout: Layout +) -> TensorBox | None: + """ + Used by an op (like `mm`) to determine if the op should be autotuned + locally (returns None) or remotely (returns a placeholder Buffer). + """ + if not config.distributed_max_autotune_gemm: + return None + + if not (autotune_pg := get_autotune_pg()): + return None + + if len(choices) <= 1: + return None + + state = V.distributed_autotune_state + index = state.autotuned_index + state.autotuned_index += 1 + local = index % autotune_pg.size() == autotune_pg.rank() + + V.current_node.meta[_DISTRIBUTED_AUTOTUNE_KEY] = _DistributedAutotuneInfo( + index, local + ) + if local: + state.autotuned_local_count += 1 + return None + + return torch._inductor.ir.TensorBox.create( + _DistributedAutotuneBuffer(name, inputs, layout) + ) + + +class _DistributedAutotuneBuffer(MultiTemplateBuffer): + """ + A MultiTemplateBuffer which represents a kernel being autotuned on a + different rank. When `schedule` is called this will be replaced by the + "real" buffer. + """ + + # Name of the kernel being autotuned. + _kernel_name: str + + def __init__( + self, + kernel_name: str, + inputs: list[Buffer], + layout: Layout, + ) -> None: + super().__init__( + layout, + inputs, + choice_timings_fn=self._dummy_choice_timings, + unfiltered_choices=[], + allowed_prologue_inps=OrderedSet({}), + ) + + self._kernel_name = kernel_name + + def _dummy_choice_timings( + self, _hint_override: int | None + ) -> dict[ChoiceCaller, float]: + # This should never get called. It means that a remote autotune was + # scheduled but never filled in. + raise NotImplementedError + + def autotune(self, ser_choice: _SerializedChoice) -> TensorBox: + """ + Given a _SerializedChoice (autotune results from another rank) + compute the final TensorBox. + """ + + from .select_algorithm import autotune_select_algorithm + + with patch.object(V.graph, "scheduler", None): + kernel_inputs = MMKernelInputs([*self.original_inputs]) + assert isinstance(self.layout, Layout) + choice = ser_choice.get_choice(self.layout, kernel_inputs) + buffer, _ = autotune_select_algorithm( + self._kernel_name, + [choice], + kernel_inputs.nodes(), + self.layout, + ) + assert isinstance(buffer, TensorBox) + return buffer + + +# Can we make this async? +def _sync(autotune_results: list[_SerializedChoice]) -> Sequence[_SerializedChoice]: + """ + Perform the all_gather to collect the autotune results from all the ranks. + """ + + autotune_pg = get_autotune_pg() + assert autotune_pg + + # Perform allgather + all_states: list[list[_SerializedChoice]] = [None] * autotune_pg.size() # type: ignore[list-item] + torch.distributed.all_gather_object(all_states, autotune_results, group=autotune_pg) + + node_count = sum(len(x) for x in all_states) + # It's faster to briefly lie about the type than to unzip the results and append. + choices_by_index: list[_SerializedChoice] = [None] * node_count # type: ignore[list-item] + + check_count = 0 + for other_results in all_states: + for choice in other_results: + assert isinstance(choice, _SerializedChoice) + assert choices_by_index[choice.index] is None + choices_by_index[choice.index] = choice + check_count += 1 + + assert node_count == check_count, f"count mismatch: {node_count} != {check_count}" + return choices_by_index + + +class _SerializedChoice: + """ + This is a serializer for the autotune choice. KernelTemplateChoice can't + be serialized directly (the template and inputs prevent this) so we need to + serialize it by parts and reconstruct later on. + """ + + def __init__(self, index: int, choice: ChoiceCaller) -> None: + self.index = index + self.template_uid = _SerializedChoice._template_uid_from_choice(choice) + self.kwargs = self._compute_kwargs(choice.description) + + def get_choice(self, layout: Layout, inputs: KernelInputs) -> ChoiceCaller | None: + """ + Deserialize the ChoiceCaller and return it. + """ + + template = self._template_from_uid() + + kwargs = {**self.kwargs} + if "BLOCK_K" in kwargs: + # TODO: Do we really need to externally compute this value? If it's + # needed I'm surprised it's not just part of the original template + # description. + # This needs the actual 'k' to figure out the value. + k = inputs.nodes()[0].get_size()[1] + kwargs["EVEN_K"] = sympy.gcd(k, kwargs["BLOCK_K"]) == kwargs["BLOCK_K"] + + extra_kwargs: dict[str, Any] = {} + from .kernel_template_choice import ( + DictKernelTemplateParams, + KernelTemplateChoice, + ) + + params = DictKernelTemplateParams(kwargs) + ktc = KernelTemplateChoice(template, params, extra_kwargs, layout, inputs) + return ktc.choice + + @staticmethod + def _compute_kwargs(description: str) -> dict[str, int | str | bool]: + """ + Given a template description turn it into input kwargs. + """ + if not description: + return {} + + # TODO: It seems like it would be better if the template could provide + # this directly instead of having to parse a string. + kwargs: dict[str, int | str | bool] = {} + for cfg in description.split(","): + key, val = cfg.split("=", 1) + key, val = key.strip(), val.strip() + if val == "True": + kwargs[key] = True + elif val == "False": + kwargs[key] = False + elif val.isdigit(): + kwargs[key] = int(val) + else: + assert val.startswith("'") and val.endswith("'") + kwargs[key] = val[1:-1] + return kwargs + + @staticmethod + def _template_uid_from_choice(choice: ChoiceCaller) -> str: + """ + Given a ChoiceCaller figure out which template represents it. This + is reversed by _template_from_uid(). + """ + + # We need a better way to do this - right now we need to add each + # supported template directly. + if isinstance(choice, select_algorithm.ExternKernelCaller): + if choice.choice.name == "mm": + return "torch._inductor.kernel.mm.aten_mm" + else: + raise RuntimeError(f"TODO: kernel {choice.choice.name!r}") + elif isinstance(choice, select_algorithm.TritonTemplateCaller): + return "torch._inductor.kernel.mm.mm_template" + else: + raise RuntimeError(f"TODO: {type(choice)}") + + def _template_from_uid(self) -> Any: + """ + See _template_uid_from_choice(). + """ + parts = self.template_uid.split(".") + obj = globals()[parts[0]] + for k in parts[1:]: + obj = getattr(obj, k) + return obj + + +def _autotune_local_nodes( + scheduler: torch._inductor.scheduler.Scheduler, +) -> list[_SerializedChoice]: + """ + Go through the nodes in the scheduler and autotune the kernels which + should be autotuned by this rank. + """ + + autotune_results: list[_SerializedChoice] = [] + + for node in scheduler.nodes: + if not isinstance(node, SchedulerNode): + continue + + if (inner_node := node.node) is None: + continue + + if isinstance(inner_node, _DistributedAutotuneBuffer): + # This is marked for remote autotuning. + continue + + if not isinstance(inner_node, MultiTemplateBuffer): + continue + + if (origin_node := inner_node.origin_node) is None: + continue + + if (meta := origin_node.meta) is None: + continue + + info = meta.get(_DISTRIBUTED_AUTOTUNE_KEY) + if info is None: + continue + + assert info.local + + # We force autotuning here + # Still takes advantage of async precompile + # We need all the configs before fusion + min_choice, _ = inner_node.get_min_choice() + + choice = _SerializedChoice(info.index, min_choice) + autotune_results.append(choice) + + state = V.distributed_autotune_state + assert len(autotune_results) == state.autotuned_local_count, ( + f"incorrect local autotuned nodes found ({len(autotune_results)} != {state.autotuned_local_count})" + ) + return autotune_results + + +def _autotune_remote_nodes( + scheduler: torch._inductor.scheduler.Scheduler, + choices_by_index: Sequence[_SerializedChoice], +) -> None: + """ + Go through the nodes in the scheduler and autotune the nodes that were + autotuned on remote ranks. + """ + + for i, node in enumerate(scheduler.nodes): + if isinstance(node, SchedulerNode) and isinstance( + (dist_node := node.node), _DistributedAutotuneBuffer + ): + assert dist_node.origin_node is not None + info = dist_node.origin_node.meta[_DISTRIBUTED_AUTOTUNE_KEY] + out_tensorbox = dist_node.autotune(choices_by_index[info.index]) + + out_storage = out_tensorbox.data + assert isinstance(out_storage, StorageBox) + out_buffer = out_storage.data + assert isinstance(out_buffer, OperationBuffer) + + assert out_buffer.layout == dist_node.layout + + scheduler._replace_node(out_buffer, dist_node, i, node) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/dtype_propagation.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/dtype_propagation.py new file mode 100644 index 0000000000000000000000000000000000000000..312ab76341e3c193debc725651958af9677a2a85 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/dtype_propagation.py @@ -0,0 +1,489 @@ +# mypy: allow-untyped-defs +import functools +from collections.abc import Callable, Sequence +from typing import Any, Optional, Protocol, TYPE_CHECKING, TypeVar + +import sympy + +import torch +from torch._prims_common import ( + ELEMENTWISE_TYPE_PROMOTION_KIND, + is_integer_dtype, + type_to_dtype, +) +from torch.utils._ordered_set import OrderedSet + +from .ops_handler import OP_NAMES, OpsHandler +from .utils import upcast_compute_type +from .virtualized import OpsValue, V + + +T = TypeVar("T") +_MISSING_SHAPE = object() +_UNSIGNED_INT_DTYPES: frozenset[torch.dtype] = frozenset( + ( + torch.uint8, + torch.uint16, + torch.uint32, + torch.uint64, + ) +) + + +class DTypeVar(Protocol): + @property + def dtype(self) -> torch.dtype: ... + + +DTypeArg = DTypeVar | torch.types.Number | str | OpsValue + + +# Inputs need to be cacheable (e.g., not a CSEVar) in order for the cache to be effective +# So first decompose CSEVars -> tuple before calling this + + +@functools.cache +def get_promoted_dtype( + *args: Sequence[tuple[torch.dtype, bool]], + type_promotion_kind: ELEMENTWISE_TYPE_PROMOTION_KIND | None = None, +): + def construct_input(inp): + if inp[1]: + return torch.empty([], dtype=inp[0]) + else: + return torch.empty([1], dtype=inp[0]) + + inps = [construct_input(arg) for arg in args] + _, dtype = torch._prims_common.elementwise_dtypes( + *inps, + type_promotion_kind=( + type_promotion_kind + if type_promotion_kind + else ELEMENTWISE_TYPE_PROMOTION_KIND.DEFAULT + ), + ) + return dtype + + +def promote_types( + args: Sequence[DTypeArg], + type_promotion_kind: ELEMENTWISE_TYPE_PROMOTION_KIND | None = None, +): + dtype_prop_candidates = [] + + for arg in args: + assert not isinstance(arg, str) + if isinstance(arg, OpsValue): + arg = arg.value + assert isinstance(arg, torch._prims_common.Number) or hasattr(arg, "dtype") + + if isinstance(arg, torch._prims_common.Number): + dtype_prop_candidates.append((type_to_dtype(type(arg)), True)) + continue + + # pyrefly: ignore [missing-attribute] + dtype_prop_candidates.append((arg.dtype, getattr(arg, "is_scalar", False))) + + dtype = get_promoted_dtype( + *dtype_prop_candidates, + type_promotion_kind=type_promotion_kind, + ) + + return dtype + + +def _unwrap_dtype_arg(arg: DTypeArg) -> DTypeVar | torch.types.Number | str: + if isinstance(arg, OpsValue): + return arg.value + return arg + + +def _is_scalar_dtype_arg(arg: DTypeArg) -> bool: + arg = _unwrap_dtype_arg(arg) + if isinstance(arg, torch._prims_common.Number): + return True + + is_scalar = getattr(arg, "is_scalar", False) + if callable(is_scalar): + if is_scalar(): + return True + elif is_scalar: + return True + + shape = getattr(arg, "shape", _MISSING_SHAPE) + if shape is _MISSING_SHAPE: + return False + + if shape is None: + return True + if not isinstance(shape, Sequence): + return False + + return len(shape) == 0 + + +def _has_known_nonnegative_scalar_int_value(arg: DTypeArg) -> bool: + arg = _unwrap_dtype_arg(arg) + + if isinstance(arg, bool): + return True + if isinstance(arg, int): + return arg >= 0 + + dtype = getattr(arg, "dtype", None) + if dtype is None or not is_integer_dtype(dtype): + return False + if dtype in _UNSIGNED_INT_DTYPES: + return True + + lower = getattr(getattr(arg, "bounds", None), "lower", None) + if lower is None: + return False + if isinstance(lower, sympy.Expr): + return lower.is_nonnegative is True + return lower >= 0 + + +class DtypePropagationOpsHandler: + """ + Propagate dtype from args to output + """ + + # Singleton DtypePropagationOpsHandler, because we meta program over a number of op rules. + # Those are only defined after other inductor state has run. + + _instance: Optional["DtypePropagationOpsHandler"] = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self) -> None: + for op, rule in torch._inductor.utils.op_dtype_propagation_rules.items(): + fn = ( + functools.partial(self.return_dtype, dtype=rule.override_return_dtype) + if rule.override_return_dtype + else functools.partial( + self.op_dtype_rule, type_promotion_kind=rule.type_promotion_kind + ) + ) + setattr(self, op, fn) + + # Set pointwise operation rules + for op in torch._inductor.codegen.common.pointwise_overrides_data.values(): + if not hasattr(self, op.name): + setattr( + self, + op.name, + functools.partial( + self.op_dtype_rule, type_promotion_kind=op.type_promotion_kind + ), + ) + + # Set boolean operation rules + for op in torch._inductor.utils.boolean_ops(): + if not hasattr(self, op): + setattr( + self, op, functools.partial(self.return_dtype, dtype=torch.bool) + ) + + unimplemented_ops = OP_NAMES - OrderedSet(dir(self)) + torch._check( + len(unimplemented_ops) == 0, + lambda: f"Unimplemented dtype rule for ops: {unimplemented_ops}", + ) + + # metaprogrammed in __init__ + + @staticmethod + def op_dtype_rule( + *args: DTypeArg, type_promotion_kind: ELEMENTWISE_TYPE_PROMOTION_KIND + ) -> torch.dtype: + return promote_types(args, type_promotion_kind=type_promotion_kind) + + @staticmethod + def return_dtype(*args: DTypeArg, dtype: torch.dtype) -> torch.dtype: + return dtype + + # op rules + + @staticmethod + def constant(value: torch.types.Number, dtype: torch.dtype) -> torch.dtype: + return upcast_compute_type(dtype) + + @staticmethod + def load_seed(name: str, offset: int) -> torch.dtype: + return upcast_compute_type(V.graph.get_dtype(name)) + + @staticmethod + def randint64(seed: int, offset: int, low: int, high: int) -> torch.dtype: + return torch.int64 + + @staticmethod + def masked( + mask: DTypeArg, body: Callable[[], DTypeArg], other: DTypeArg + ) -> torch.dtype: + from .loop_body import LoopBodyBlock + + assert isinstance(body, LoopBodyBlock), "body must be a LoopBodyBlock" + # TODO - we avoid calling this in codegen, needs work for non codegen use cases + loads = body.graph.find_nodes(op="call_method", target="load") + if len(loads) <= 1: + return promote_types([other]) + + return upcast_compute_type(V.graph.get_dtype(loads[-1].args[1])) + + @staticmethod + def where(a: DTypeArg, b: DTypeArg, c: DTypeArg) -> torch.dtype: + return promote_types([b, c]) + + @staticmethod + def index_expr(expr: sympy.Expr, dtype: torch.dtype) -> torch.dtype: + # TODO - TODO - rationalize index_expr. The dtype is not always used and we are inconsistent about int32 or int64 + # in lowerings. cpp just uses the dtype + if dtype not in (torch.int32, torch.int64) or not hasattr( + V.kernel, "index_dtype" + ): + return upcast_compute_type(dtype) + + return V.kernel.get_index_dtype_as_torch_dtype() + + @staticmethod + def to_dtype( + x: DTypeArg, + dtype: torch.dtype, + src_dtype: torch.dtype | None = None, + use_compute_types=True, + ) -> torch.dtype: + return upcast_compute_type(dtype) if use_compute_types else dtype + + @staticmethod + def to_dtype_bitcast( + x: DTypeArg, dtype: torch.dtype, src_dtype: torch.dtype + ) -> torch.dtype: + return upcast_compute_type(dtype) + + @staticmethod + def gelu(x: DTypeArg) -> torch.dtype: + return promote_types([x]) + + @staticmethod + def mul(a: DTypeArg, b: DTypeArg) -> torch.dtype: + return promote_types([a, b]) + + @staticmethod + def truediv(a: DTypeArg, b: DTypeArg) -> torch.dtype: + return promote_types([a, b]) + + @staticmethod + def div_rn(a: DTypeArg, b: DTypeArg) -> torch.dtype: + return promote_types([a, b]) + + @staticmethod + def pow(a: DTypeArg, b: DTypeArg) -> torch.dtype: + dtype = promote_types([a, b]) + if ( + is_integer_dtype(dtype) + and _is_scalar_dtype_arg(a) + and _is_scalar_dtype_arg(b) + and not _has_known_nonnegative_scalar_int_value(b) + ): + # Scalar integer pow follows Python semantics: negative exponents + # produce a floating result, even though tensor integer pow stays + # integral or errors out in separate lowering paths. + return torch.float64 + return dtype + + @staticmethod + def mod(a: DTypeArg, b: DTypeArg) -> torch.dtype: + return promote_types([a, b]) + + @staticmethod + def indirect_indexing( + x: DTypeArg, size: int, check: bool = True, wrap_neg: bool = True + ) -> torch.dtype: + return torch.int64 + + @staticmethod + def randn(seed: int, offset: int) -> torch.dtype: + return torch.float + + @staticmethod + def rand(seed: int, offset: int) -> torch.dtype: + return torch.float + + @staticmethod + def rand_eager(seed, offset, threads_per_round, tid, vec) -> torch.dtype: + return torch.float + + @staticmethod + def store_reduction(name: str, index, value: DTypeArg) -> None: + return None + + @staticmethod + def reduction( + dtype: torch.dtype, src_dtype: torch.dtype, reduction_type: str, value: DTypeArg + ) -> torch.dtype: + return dtype + + @staticmethod + def store(name: str, index, value: DTypeArg, mode: str | None = None) -> None: + return None + + @staticmethod + def partial_accumulate( + name: str, + reduction_type: str, + value: DTypeArg, + extra_meta: dict[str, Any], + ) -> None: + return None + + @staticmethod + def load(name: str, index) -> torch.dtype: + return upcast_compute_type(V.graph.get_dtype(name)) + + @staticmethod + def floor(x: DTypeArg) -> torch.dtype: + return promote_types( + [x], type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT + ) + + @staticmethod + def ceil_to_int(x: DTypeArg, dtype: torch.dtype) -> torch.dtype: + return dtype + + @staticmethod + def int_truediv(x: DTypeArg, y: DTypeArg) -> torch.dtype: + return promote_types( + [x, y], type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT + ) + + @staticmethod + def scan( + dtypes: tuple[torch.dtype, ...], + combine_fn: Callable[[tuple[T, ...], tuple[T, ...]], tuple[T, ...]], + values: tuple[T, ...], + ) -> tuple[torch.dtype, ...]: + return dtypes + + @staticmethod + def fmod(x: DTypeArg, y: DTypeArg) -> torch.dtype: + return promote_types([x, y]) + + @staticmethod + def round_to_int(x: DTypeArg, dtype: torch.dtype) -> torch.dtype: + return dtype + + @staticmethod + def identity(x: DTypeArg) -> torch.dtype: + return promote_types([x]) + + @staticmethod + def frexp(x: DTypeArg) -> tuple[torch.dtype, torch.dtype]: + # TODO - need to handle multiple outputs + return (promote_types([x]), torch.int32) + + @staticmethod + def sort( + dtypes: tuple[torch.dtype, ...], + values: tuple[T, ...], + stable: bool, + descending: bool, + ) -> tuple[torch.dtype, ...]: + return dtypes + + @staticmethod + def trunc(x: DTypeArg) -> torch.dtype: + return promote_types([x]) + + @staticmethod + def bucketize( + values: DTypeArg, + boundaries: tuple[str, sympy.Expr, sympy.Expr, sympy.Expr], + boundary_indices: DTypeArg, + indexing_dtype: torch.dtype, + right: bool, + sorter: tuple[str, sympy.Expr] | None = None, + sorter_indices: T | None = None, + ) -> torch.dtype: + return indexing_dtype + + @staticmethod + def rshift(x: DTypeArg, y: DTypeArg) -> torch.dtype: + return promote_types([x]) + + @staticmethod + def round(x: DTypeArg) -> torch.dtype: + return promote_types( + [x], type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT + ) + + @staticmethod + def trunc_to_int(x: DTypeArg, dtype: torch.dtype) -> torch.dtype: + return dtype + + @staticmethod + def floor_to_int(x: DTypeArg, dtype: torch.dtype) -> torch.dtype: + return dtype + + @staticmethod + def truncdiv(x: DTypeArg, y: DTypeArg) -> torch.dtype: + return promote_types([x, y]) + + @staticmethod + def floordiv(x: DTypeArg, y: DTypeArg) -> torch.dtype: + return promote_types([x, y]) + + @staticmethod + def halide_clamp(value, size, check): + # TODO - way of registering dtype for op in backend + return torch.int32 + + @staticmethod + def dot(x: DTypeArg, y: DTypeArg) -> torch.dtype: + # triton tl.dot out_dtype is tl.float32 by default. + return torch.float32 + + @staticmethod + def inline_asm_elementwise( + *inputs, + asm, + constraints=None, + dtype=torch.float32, + is_pure=True, + pack=1, + input_dtypes=None, + ): + return dtype + + @staticmethod + def lshift(x: DTypeArg, y: DTypeArg) -> torch.dtype: + return promote_types([x]) + + @staticmethod + def check_bounds( + expr: sympy.Expr, size: sympy.Expr, lower: bool, upper: bool + ) -> None: + return None + + def output(self, *args: DTypeArg) -> None: + raise AssertionError( + f"{type(self).__name__}: ops.output should not appear here" + ) + + def placeholder(self, index: int) -> torch.dtype: + raise AssertionError( + f"{type(self).__name__}: ops.placeholder should not appear here" + ) + + @staticmethod + def device_assert_async(cond, msg: str) -> None: + return None + + +if TYPE_CHECKING: + # pyrefly: ignore [inconsistent-inheritance] + class _typecheck_DtypePropagation(DtypePropagationOpsHandler, OpsHandler[Any]): + pass # mypy will error if we got any of the signatures wrong diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/exc.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/exc.py new file mode 100644 index 0000000000000000000000000000000000000000..90fbb70a4740d57479ff6e2eba1c1257babe5876 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/exc.py @@ -0,0 +1,166 @@ +from __future__ import annotations + +import os +import tempfile +import textwrap +from functools import lru_cache +from typing import Any, TYPE_CHECKING + +from torch._dynamo.exc import BackendCompilerFailed, ShortenTraceback + + +if TYPE_CHECKING: + import types + + from torch.cuda import _CudaDeviceProperties + +if os.environ.get("TORCHINDUCTOR_WRITE_MISSING_OPS") == "1": + + @lru_cache(None) + def _record_missing_op(target: Any) -> None: + with open(f"{tempfile.gettempdir()}/missing_ops.txt", "a") as fd: + fd.write(str(target) + "\n") + +else: + + def _record_missing_op(target: Any) -> None: # type: ignore[misc] + pass + + +class OperatorIssue(RuntimeError): + @staticmethod + def operator_str(target: Any, args: list[Any], kwargs: dict[str, Any]) -> str: + lines = [f"target: {target}"] + [ + f"args[{i}]: {arg}" for i, arg in enumerate(args) + ] + if kwargs: + lines.append(f"kwargs: {kwargs}") + return textwrap.indent("\n".join(lines), " ") + + +class MissingOperatorWithoutDecomp(OperatorIssue): + def __init__(self, target: Any, args: list[Any], kwargs: dict[str, Any]) -> None: + _record_missing_op(target) + super().__init__(f"missing lowering\n{self.operator_str(target, args, kwargs)}") + + +class MissingOperatorWithDecomp(OperatorIssue): + def __init__(self, target: Any, args: list[Any], kwargs: dict[str, Any]) -> None: + _record_missing_op(target) + super().__init__( + f"missing decomposition\n{self.operator_str(target, args, kwargs)}" + + textwrap.dedent( + f""" + + There is a decomposition available for {target} in + torch._decomp.get_decompositions(). Please add this operator to the + `decompositions` list in torch._inductor.decomposition + """ + ) + ) + + +class LoweringException(OperatorIssue): + def __init__( + self, + exc: Exception, + target: Any, + args: list[Any], + kwargs: dict[str, Any], + stack_trace: str | None = None, + ) -> None: + msg = f"{type(exc).__name__}: {exc}\n{self.operator_str(target, args, kwargs)}" + if stack_trace: + msg += f"{msg}\nFound from : \n {stack_trace}" + super().__init__(msg) + + +class SubgraphLoweringException(RuntimeError): + pass + + +class InvalidCxxCompiler(RuntimeError): + def __init__(self) -> None: + from . import config + + super().__init__( + f"No working C++ compiler found in {config.__name__}.cpp.cxx: {config.cpp.cxx}" + ) + + +class CppWrapperCodegenError(RuntimeError): + def __init__(self, msg: str) -> None: + super().__init__(f"C++ wrapper codegen error: {msg}") + + +class CppCompileError(RuntimeError): + def __init__(self, cmd: list[str], output: str) -> None: + if isinstance(output, bytes): + output = output.decode("utf-8") + + self.cmd = cmd + self.output = output + + super().__init__( + textwrap.dedent( + """ + C++ compile error + + Command: + {cmd} + + Output: + {output} + """ + ) + .strip() + .format(cmd=" ".join(cmd), output=output) + ) + + def __reduce__(self) -> tuple[type, tuple[list[str], str]]: + return (self.__class__, (self.cmd, self.output)) + + +class CUDACompileError(CppCompileError): + pass + + +class TritonMissing(ShortenTraceback): + def __init__(self, first_useful_frame: types.FrameType | None) -> None: + super().__init__( + "Cannot find a working triton installation. " + "Either the package is not installed or it is too old. " + "More information on installing Triton can be found at: https://github.com/triton-lang/triton", + first_useful_frame=first_useful_frame, + ) + + +class GPUTooOldForTriton(ShortenTraceback): + def __init__( + self, + # pyrefly: ignore [not-a-type] + device_props: _CudaDeviceProperties, + first_useful_frame: types.FrameType | None, + ) -> None: + super().__init__( + f"Found {device_props.name} which is too old to be supported by the triton GPU compiler, " + "which is used as the backend. Triton only supports devices of CUDA Capability >= 7.0, " + f"but your device is of CUDA capability {device_props.major}.{device_props.minor}", + first_useful_frame=first_useful_frame, + ) + + +class InductorError(BackendCompilerFailed): + backend_name = "inductor" + + def __init__( + self, + inner_exception: Exception, + first_useful_frame: types.FrameType | None, + ) -> None: + self.inner_exception = inner_exception + ShortenTraceback.__init__( + self, + f"{type(inner_exception).__name__}: {inner_exception}", + first_useful_frame=first_useful_frame, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/extern_node_serializer.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/extern_node_serializer.py new file mode 100644 index 0000000000000000000000000000000000000000..0e5f42e7309e85035a8db51e1f5acc782336ddb0 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/extern_node_serializer.py @@ -0,0 +1,24 @@ +import json + +from torch._export.serde.schema import ExternKernelNode, ExternKernelNodes, Node +from torch._export.serde.serialize import _dataclass_to_dict, EnumEncoder +from torch._inductor.ir import ExternKernelNode as inductor_ExternKernelNode + + +def serialize_extern_kernel_node( + extern_kernel_node: inductor_ExternKernelNode, +) -> ExternKernelNode: + assert isinstance(extern_kernel_node.node, Node) + return ExternKernelNode( + name=extern_kernel_node.name, + node=extern_kernel_node.node, + ) + + +def extern_node_json_serializer( + extern_kernel_nodes: list[inductor_ExternKernelNode], +) -> str: + serialized_nodes = ExternKernelNodes( + nodes=[serialize_extern_kernel_node(node) for node in extern_kernel_nodes] + ) + return json.dumps(_dataclass_to_dict(serialized_nodes), cls=EnumEncoder) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/freezing.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/freezing.py new file mode 100644 index 0000000000000000000000000000000000000000..d8545a69bf0c7905abf12e39306c39a23a15882b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/freezing.py @@ -0,0 +1,294 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import itertools +import logging +import weakref +from typing import Any + +import torch +import torch.utils._pytree as pytree +from torch._dynamo.utils import dynamo_timed, lazy_format_graph_code +from torch._functorch.aot_autograd import MutationType +from torch._functorch.compile_utils import fx_graph_cse +from torch._inductor.constant_folding import constant_fold, replace_node_with_constant +from torch._inductor.freezing_utils import enter_freezing, record_has_frozen_params +from torch._inductor.fx_passes.freezing_patterns import freezing_passes +from torch._inductor.fx_passes.post_grad import view_to_reshape + +from . import config + + +aten = torch.ops.aten +prims = torch.ops.prims + +log = logging.getLogger(__name__) + + +def replace_params_with_constants( + gm: torch.fx.GraphModule, + flat_params: list[Any], + fw_metadata: torch._functorch.aot_autograd.ViewAndMutationMeta, +) -> list[int]: + """ + Replaces the parameters of a PyTorch GraphModule with constants wherever possible. + Returns a list of indices representing the input parameters that were not converted to constants. + """ + params = gm.graph.find_nodes(op="placeholder") + fake_inp_nodes = params[: len(params)] + preserved_arg_indices = [] + aliased_input_args = [ + out_info.base_idx + for out_info in fw_metadata.output_info + if out_info.base_idx is not None + ] + + # TODO (tmanlaibaatar) figure out why this is different + # from mutated_inp_runtime_indices + mutated_inps = [ + i + for i, m in enumerate(fw_metadata.input_info) + if m.mutation_type + in (MutationType.MUTATED_IN_GRAPH, MutationType.MUTATED_OUT_GRAPH) + ] + + static_indices_new = [] + static_indices_offset = 0 + for i, (real_input, node) in enumerate(zip(flat_params, fake_inp_nodes)): + if i in mutated_inps or i in aliased_input_args: + preserved_arg_indices.append(i) + if i in fw_metadata.static_input_indices: + new_static_index = i - static_indices_offset + static_indices_new.append(new_static_index) + else: + replace_node_with_constant(gm, node, real_input) + static_indices_offset += 1 + # add on non param inputs + preserved_arg_indices.extend(range(len(flat_params), len(params))) + # is this necessary ? + fw_metadata.static_input_indices = static_indices_new + gm.recompile() + return preserved_arg_indices + + +def freeze( + dynamo_gm: torch.fx.GraphModule, + aot_autograd_gm: torch.fx.GraphModule, + example_inputs: list[torch._subclasses.FakeTensor], +) -> tuple[torch.fx.GraphModule, list[int]]: + """ + Inlines parameters that are not mutated into constants and optimizes the graph through constant propagation + and other techniques. If enabled, the function also discards the original parameters of the module for memory efficiency. + + Assumes that this function is run in dynamo tracing post aot_autograd. + + Args: + dynamo_gm (torch.fx.GraphModule): The Dynamo constructed GraphModule. + aot_autograd_gm (torch.fx.GraphModule): The aot_autograd constructed GraphModule to be frozen. + example_inputs (List[torch.Tensor]): A list of example input tensors to be used in the freezing process. + + Returns: + Tuple[torch.fx.GraphModule, List[int]]: A tuple containing the frozen GraphModule and a list of indices + of the inputs that were preserved (not turned into constants). + """ + with enter_freezing(): + return _freeze(dynamo_gm, aot_autograd_gm, example_inputs) + + +def _freeze( + dynamo_gm: torch.fx.GraphModule, + aot_autograd_gm: torch.fx.GraphModule, + example_inputs: list[torch._subclasses.FakeTensor], +) -> tuple[torch.fx.GraphModule, list[int]]: + # We have convert conv's weight to channels last which may meet error for .view + # when doing fake_tensor_prop. So we need to convert view to reshape first. + # See the details in fx_codegen_and_compile of compile_fx.py. + view_to_reshape(aot_autograd_gm) + + if tracing_context := torch._guards.TracingContext.try_get(): + fw_metadata = tracing_context.fw_metadata + assert tracing_context.params_flat_unwrap_subclasses is not None + params_flat = tracing_context.params_flat_unwrap_subclasses + assert fw_metadata is not None and params_flat is not None + + preserved_arg_indices = replace_params_with_constants( + aot_autograd_gm, params_flat, fw_metadata + ) + else: + inputs = aot_autograd_gm.graph.find_nodes(op="placeholder") + preserved_arg_indices = list(range(len(inputs))) + + # TODO - further restrict cse ? right now needed to dedup aliasing ops + cse_graph = fx_graph_cse(aot_autograd_gm.graph) + aot_autograd_gm.graph = cse_graph + aot_autograd_gm.recompile() + + aot_example_inputs = [example_inputs[ind] for ind in preserved_arg_indices] + freezing_passes(aot_autograd_gm, aot_example_inputs) + + constant_fold(aot_autograd_gm) + # invalidate nn Modules + if config.freezing_discard_parameters: + invalidate_eager_modules() + discard_traced_gm_params(dynamo_gm) + + log.debug( + "%s", lazy_format_graph_code("FROZEN GRAPH", aot_autograd_gm, colored=True) + ) + + record_has_frozen_params(aot_autograd_gm) + return aot_autograd_gm, preserved_arg_indices + + +class ErasedTensor(torch.Tensor): + @staticmethod + def __new__(cls, elem, name, owning_mod): + return super().__new__(cls, elem.to(device="meta")) + + def __init__(self, elem, name: str | None, mod) -> None: + self.erased_name = name + self.owning_mod_ref = weakref.ref(mod) + + @classmethod + def __torch_dispatch__(cls, func, types, args=(), kwargs=None): # type: ignore[override] + erased_tensors = [ + e + # pyrefly: ignore [bad-unpacking] + for e in pytree.arg_tree_leaves(*args, **kwargs) + if isinstance(e, ErasedTensor) + ] + assert len(erased_tensors) > 0 + e = erased_tensors[0] + + raise RuntimeError( + f"Trying to run Pytorch Eager Module after Dynamo Freezing. " + "The original parameters have been discarded for memory efficiency. " + f"Found in op {func} for erased parameter {e.erased_name} of {e.owning_mod_ref()}" + ) + + +def invalidate_eager_modules(): + with torch.utils._python_dispatch._disable_current_modes(): + for ( + mod + ) in torch._guards.TracingContext.get().module_context.nn_modules.values(): + if not isinstance(mod, torch.nn.Module): + continue + + for attr_name, tensor in list( + itertools.chain( + mod.named_parameters(recurse=False), + # pyrefly: ignore [bad-argument-type] + mod.named_buffers(recurse=False), + ) + ): + with torch._dispatch.python.no_python_dispatcher(): + e_t = ErasedTensor(tensor, attr_name, mod) + if isinstance(tensor, torch.nn.Parameter): + e_t.requires_grad_(True) + e_t._is_param = True + setattr(mod, attr_name, e_t) + + +def discard_traced_gm_params(mod: torch.fx.GraphModule): + with torch.utils._python_dispatch._disable_current_modes(): + for attr_name, tensor in list( + itertools.chain( + mod.named_parameters(recurse=False), + # pyrefly: ignore [bad-argument-type] + mod.named_buffers(recurse=False), + ) + ): + with torch._dispatch.python.no_python_dispatcher(): + e_t = ErasedTensor(tensor, attr_name, mod) + if isinstance(tensor, torch.nn.Parameter): + e_t.requires_grad_(True) + e_t._is_param = True + setattr(mod, attr_name, e_t) + + +def enforce_output_layout(gm: torch.fx.GraphModule): + """ + Make sure the output node's layout does not change due to compiler optimizations + by adding aten.as_strided nodes with the expected strides. + + Only used for inference so we can assume all graph outputs are model outputs. + """ + *_, output_node = gm.graph.nodes + out_list = output_node.args[0] + with gm.graph.inserting_before(output_node): + for n in out_list: + if not isinstance( + n.meta["val"], torch.Tensor + ) or not torch._prims_common.is_non_overlapping_and_dense_or_false( + n.meta["val"] + ): + continue + + # add a node to enforce eager layout + ft = n.meta["val"] + new_node = gm.graph.call_function( + prims.inductor_force_stride_order.default, (n, ft.stride()) + ) + + # can not call + # n.replace_all_uses_with(new_node) + # since it will replace the usage of n in new_node itself. + output_node.replace_input_with(n, new_node) + + gm.graph.lint() + gm.recompile() + + +def enforce_as_strided_input_layout(gm: torch.fx.GraphModule): + """ + Make sure the as_strided node's input's layout does not change due to compiler + optimizations, because the as_strided strides info depends on input tensor stride info. + """ + + as_strided_ops = [ + torch.ops.aten.as_strided.default, + torch.ops.aten.as_strided_.default, + torch.ops.aten.as_strided_scatter.default, + ] + strided_nodes = [n for n in gm.graph.nodes if n.target in as_strided_ops] + for n in strided_nodes: + with gm.graph.inserting_before(n): + # add a node to enforce eager layout + ft = n.args[0].meta["val"] + new_node = gm.graph.call_function( + prims.inductor_force_stride_order.default, (n.args[0], ft.stride()) + ) + n.replace_input_with(n.args[0], new_node) + + gm.graph.lint() + gm.recompile() + + +def convert_conv_weights_to_channels_last(gm: torch.fx.GraphModule): + """ + Convert 4d convolution weight tensor to channels last format. + + This pass is performed before freezing so the added nodes can be constant + folded by freezing. + """ + with dynamo_timed("convert_conv_weights_to_channels_last"): + convs = [n for n in gm.graph.nodes if n.target is aten.convolution.default] + for conv in convs: + weight_node = conv.args[1] + if len(weight_node.meta["val"].size()) != 4 or weight_node.meta[ + "val" + ].is_contiguous(memory_format=torch.channels_last): + # not a 4d tensor or already channels last, skip + continue + + with gm.graph.inserting_before(conv): + new_node = gm.graph.call_function( + aten.clone.default, + (weight_node,), + {"memory_format": torch.channels_last}, + ) + conv.replace_input_with(weight_node, new_node) + + enforce_as_strided_input_layout(gm) + enforce_output_layout(gm) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/freezing_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/freezing_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..8a14890aacbd76acd0e49726d9eba99c590e83c8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/freezing_utils.py @@ -0,0 +1,55 @@ +import contextlib +import threading +from collections.abc import Generator +from typing import Any + +import torch + + +_TLS = threading.local() + + +def _freezing_active() -> bool: + return getattr(_TLS, "freezing_active", False) + + +@contextlib.contextmanager +def enter_freezing() -> Generator[Any, None, None]: + """ + Context manager to designate when freezing is active. + """ + prev = _freezing_active() + _TLS.freezing_active = True + try: + yield + finally: + _TLS.freezing_active = prev + + +def record_has_frozen_params(gm: torch.fx.GraphModule) -> None: + """ + Mark the gm as having frozen params. + """ + gm._has_frozen_params = True # type: ignore[assignment] + + +def has_frozen_params(gm: torch.fx.GraphModule) -> bool: + """ + Return True if the gm has frozen parameters. + """ + return getattr(gm, "_has_frozen_params", False) + + +def maybe_set_is_frozen_param(t: torch.Tensor) -> None: + """ + Mark the provided tensor as a frozen param if freezing is active. + """ + if _freezing_active(): + t._is_frozen_param = True # type: ignore[attr-defined] + + +def is_frozen_param(t: torch.Tensor) -> bool: + """ + Return True if the tensor is a frozen param. + """ + return getattr(t, "_is_frozen_param", False) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fuzzer.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fuzzer.py new file mode 100644 index 0000000000000000000000000000000000000000..e9ba9f575e7b1feca77a854fcb4f413152c9e23f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fuzzer.py @@ -0,0 +1,1019 @@ +import importlib +import itertools +import logging +import pickle +import random +import signal +import string +import traceback +import types +from collections.abc import Callable, KeysView, Sequence +from enum import Enum +from functools import partial, wraps +from types import FrameType +from typing import Any, get_args, get_origin, Literal, TypeVar, Union + +import torch +from functorch.compile import min_cut_rematerialization_partition +from torch._inductor.custom_graph_pass import CustomGraphPass, CustomPartitionerFn +from torch._inductor.scheduler import BaseSchedulerNode +from torch.utils._config_module import _ConfigEntry, ConfigModule +from torch.utils._ordered_set import OrderedSet + + +log = logging.getLogger(__name__) + + +def is_type(type_hint, comp_type) -> bool: # type: ignore[no-untyped-def] + """ + Determines if type_hint is comp_type. There are some type annotations that this doesn't work for. + I think it's because some Type annotations are Type Objects and some are Special Forms, but not sure. + There's definite room for improvement to make this more general for someone who deeply understands + Python types. + """ + return type_hint is comp_type or get_origin(type_hint) is comp_type + + +def is_optional_type(type_hint) -> bool: # type: ignore[no-untyped-def] + """ + Special case of is_type. + """ + origin = get_origin(type_hint) + + if origin is Union: + args = get_args(type_hint) + return type(None) in args + + return False + + +def is_callable_type(type_hint) -> bool: # type: ignore[no-untyped-def] + """ + Special Case of is_type. + """ + return type_hint.__name__ == "Callable" + + +class DummyPass(CustomGraphPass): + """ + A Dummy pass to be used by ConfigFuzzer + """ + + def __call__(self, graph: torch.fx.graph.Graph) -> None: + return None + + def uuid(self) -> Any | None: + return None + + +class DummyPartitionerFn(CustomPartitionerFn): + """ + A Dummy partitioner function to be used by ConfigFuzzer + """ + + def __call__( + self, gm: torch.fx.GraphModule, joint_inputs: Sequence[object], **kwargs: Any + ) -> tuple[torch.fx.GraphModule, torch.fx.GraphModule]: + return min_cut_rematerialization_partition(gm, joint_inputs, **kwargs) + + def uuid(self) -> Any | None: + return None + + +T = TypeVar("T") + + +class TypeExemplars: + """ + This class returns examples of a Type, given its class name. + """ + + TYPE_EXEMPLARS: dict[str, Any] = { + CustomGraphPass.__name__: DummyPass(), + CustomPartitionerFn.__name__: DummyPartitionerFn(), + torch.fx.graph.Graph.__name__: torch.fx.graph.Graph(), + BaseSchedulerNode.__name__: BaseSchedulerNode(None), # type: ignore[arg-type] + } + + @staticmethod + def example(t: type[T]) -> T | None: + """ + Return an example of a class. + """ + + return TypeExemplars.TYPE_EXEMPLARS.get(t.__name__, None) + + @staticmethod + def contains(t: type[T]) -> bool: + return t.__name__ in TypeExemplars.TYPE_EXEMPLARS + + +def check_halide_import() -> bool: + """checks if we have halide available""" + try: + importlib.import_module("halide") + return True + except ModuleNotFoundError: + return False + + +if check_halide_import(): + CUDA_BACKEND = ["triton", "halide"] +else: + CUDA_BACKEND = ["triton"] + + +class Status(Enum): + """ + The Status return value enum for Config Fuzzer + """ + + # ConfigFuzzer skipped the test + SKIPPED = "skipped" + # ConfigFuzzer compiled and ran the test and function it passed. + PASSED = "passed" + # ConfigFuzzer failed to compile the test function + FAILED_COMPILE = "failed_compile" + # ConfigFuzzer compiled the test function and running it raised an exception + FAILED_RUN_COMPILE_EXCEPTION = "failed_run_compile_exception" + # ConfigFuzzer ran eager and it raised an exception + FAILED_RUN_EAGER_EXCEPTION = "failed_run_eager_exception" + # ConfigFuzzer compiled the test function, but the return value indicated that the compiled value didn't match the + # value from eager (or however else you set up the comparison in the test function) + FAILED_RUN_RETURN = "failed_run_return" + + def failing(self) -> bool: + """ + Convenience method to check whether these status represent failure. + """ + return ( + self == Status.FAILED_COMPILE + or self == Status.FAILED_RUN_EAGER_EXCEPTION + or self == Status.FAILED_RUN_COMPILE_EXCEPTION + or self == Status.FAILED_RUN_RETURN + ) + + +# Sometime the types of configs aren't expressive enough to be captured by python type system, so the options can be +# manually specified here: +# TODO this needs to be indexed to the module, like inductor or dynamo, for name collisions +TYPE_OVERRIDES: dict[str, list[Any]] = { + "cuda_backend": CUDA_BACKEND, + "post_grad_fusion_options": [ + { + "batch_linear_post_grad": { + "shape_broadcast_batch_linear": True, + "fuse_nodes_with_same_users": True, + }, + "batch_aten_mul": {"fuse_nodes_with_same_parent": False}, + "batch_aten_sigmoid": {"fuse_nodes_with_same_parent": True}, + "batch_aten_add": {"fuse_nodes_with_same_parent": True}, + "normalization_aten_pass": {}, + "unbind_stack_aten_pass": {}, + }, + { + "batch_aten_add": {}, + "batch_aten_mul": {}, + "batch_aten_sub": {}, + "batch_aten_div": {}, + "group_linear": {"require_fbgemm": True}, + }, + ], + "autoheuristic_collect": [ + {"pad_mm": True, "mixed_mm": True}, + {"pad_mm": True, "mixed_mm": False}, + {"pad_mm": False, "mixed_mm": True}, + {"pad_mm": False, "mixed_mm": False}, + ], + "autoheuristic_use": [ + {"pad_mm": True, "mixed_mm": True}, + {"pad_mm": True, "mixed_mm": False}, + {"pad_mm": False, "mixed_mm": True}, + {"pad_mm": False, "mixed_mm": False}, + ], + "traceable_tensor_subclasses": [OrderedSet()], + "nontraceable_tensor_subclasses": [OrderedSet()], +} +SamplingType = Callable[[str, type[Any], Any], Any] + + +class SamplingMethod(Enum): + """ + This class handles the process of assigning concrete values to type annotations. So a type annotation of + ```python + foo: Optional[int] = None + ``` + Will be assigned an int if the dispatch function gets TOGGLE, or a 50/50 split between an int and None if it gets + RANDOM. + """ + + TOGGLE = "TOGGLE" # toggle to the opposite value + RANDOM = "RANDOM" # randomly choose an option + + @staticmethod + def _generate_value_for_type( + random_sample: bool, field_name: str, type_hint: type[Any], default: Any + ) -> Any: + """ + Generates a value of a type based on the setting. + """ + # look for name in type overrides + if field_name in TYPE_OVERRIDES: + return random.choice(TYPE_OVERRIDES[field_name]) + + if type_hint is bool: + return random.choice([True, False]) if random_sample else not default + elif type_hint is int: + # NOTE initially tried to use negation of the value, but it doesn't work because most types are ints + # when they should be natural numbers + zero. Python types to cover these values aren't super convenient. + return random.randint(0, 1000) + elif type_hint is float: + return random.uniform(0, 1000) + elif type_hint is str: + characters = string.ascii_letters + string.digits + string.punctuation + return "".join( + random.choice(characters) for _ in range(random.randint(1, 20)) + ) + elif is_type(type_hint, list): + elem_type = getattr( + type_hint, + "__args__", + [type(default[0])] if default and len(default) else [type(None)], + )[0] + new_default = default[0] if default and len(default) > 0 else None + return [ + SamplingMethod._generate_value_for_type( + random_sample, field_name, elem_type, new_default + ) + for _ in range(random.randint(1, 3)) + ] + elif is_type(type_hint, set): # noqa: set_linter + indexable = list(default) + elem_type = getattr( + type_hint, + "__args__", + [type(indexable[0])] if default and len(default) else [type(None)], + )[0] + new_default = indexable[0] if default and len(default) > 0 else None + return { # noqa: set_linter + SamplingMethod._generate_value_for_type( + random_sample, field_name, elem_type, new_default + ) + for _ in range(random.randint(1, 3)) + } + elif is_type(type_hint, OrderedSet): + indexable = list(default) + elem_type = getattr( + type_hint, + "__args__", + [type(indexable[0])] if default and len(default) else [type(None)], + )[0] + new_default = indexable[0] if default and len(default) > 0 else None + return OrderedSet( + [ + SamplingMethod._generate_value_for_type( + random_sample, field_name, elem_type, new_default + ) + for _ in range(random.randint(1, 3)) + ] + ) + elif is_type(type_hint, dict): + key_type, value_type = getattr( + type_hint, + "__args__", + map(type, next(iter(default.items()))) + if (default is not None and len(default)) + else (type(None), type(None)), + ) + if default is not None and len(default.items()) > 0: + default_key, default_val = next(iter(default.items())) + else: + default_key, default_val = None, None + return { + SamplingMethod._generate_value_for_type( + random_sample, field_name, key_type, default_key + ): SamplingMethod._generate_value_for_type( + random_sample, field_name, value_type, default_val + ) + for _ in range(random.randint(0, 3)) + } + elif is_type(type_hint, Union) or is_type(type_hint, types.UnionType): + # do whatever is not the type of default + try: + assert len(type_hint.__args__) > 1 + except AttributeError as err: + raise ValueError("Union type with no args") from err + if random_sample: + new_type = random.choice(type_hint.__args__) + else: + new_type = random.choice( + [t for t in type_hint.__args__ if t is not type(default)] + ) + try: + new_default = new_type() + except Exception: + # if default constructor doesn't work, try None + new_default = None + + return SamplingMethod._generate_value_for_type( + random_sample, field_name, new_type, new_default + ) + elif is_type(type_hint, tuple): + args = getattr( + type_hint, + "__args__", + tuple(map(type, default)), + ) + zipped = zip(args, default) + return tuple( + map( # noqa: C417 + lambda x: SamplingMethod._generate_value_for_type( + random_sample, field_name, x[0], x[1] + ), + zipped, + ) + ) + elif is_type(type_hint, Literal): + try: + if random_sample: + return random.choice(type_hint.__args__) + else: + choices = [t for t in type_hint.__args__ if t != default] + if choices: + return random.choice(choices) + else: + return default + except AttributeError as err: + raise ValueError("Literal type with no args") from err + elif is_optional_type(type_hint): + try: + elem_type = type_hint.__args__[0] + except AttributeError as err: + raise ValueError("Optional type with no args") from err + if random_sample: + return random.choice( + [ + None, + SamplingMethod._generate_value_for_type( + random_sample, field_name, elem_type, default + ), + ] + ) + else: + if default is None: + return SamplingMethod._generate_value_for_type( + random_sample, field_name, elem_type, None + ) + else: + return None + elif type_hint is type(None): + return None + elif is_callable_type(type_hint): + try: + return_type = list(type_hint.__args__)[-1] + except AttributeError as err: + raise ValueError("Callable type with no args") from err + + @wraps(lambda *args, **kwargs: None) + def dummy_function(*args, **kwargs): # type: ignore[no-untyped-def] + return SamplingMethod._generate_value_for_type( + random_sample, field_name, return_type, None + ) + + return dummy_function + elif type_hint == torch._ops.OpOverload: + return torch.ops.aten.add.default + elif TypeExemplars.contains(type_hint): + return TypeExemplars.example(type_hint) + elif type_hint == Any: + return 1 if default != 1 else 2 + else: + raise ValueError(f"Unable to process type {type_hint}. PRs welcome :)") + + @staticmethod + def dispatch(sm: "SamplingMethod") -> SamplingType: + """ + Returns a function that will generate values from a type, based on the SamplingMethod passed in. + """ + if sm == SamplingMethod.RANDOM: + return partial(SamplingMethod._generate_value_for_type, True) + elif sm == SamplingMethod.TOGGLE: + return partial(SamplingMethod._generate_value_for_type, False) + else: + raise ValueError(f"malformed sampling method: {sm}") + + +class Default: + """ + Singleton default object that will cause the ConfigFuzzer to always use the default value set in the config. + """ + + +DEFAULT = Default() + +# The combination of config settings being set (based on their strings) +ComboType = tuple[str, ...] + + +class ResultType: + """ + The mapping of the combo strings to the result status after running the config fuzzer. + """ + + _vals: dict[ComboType, Status] + + def __repr__(self) -> str: + return f"ResultType[{self._vals}]" + + def __init__(self) -> None: + self._vals = {} + + def __len__(self) -> int: + return len(self._vals) + + def num_ran(self) -> int: + """ + Returns how many combos actually ran (weren't skipped). + """ + ret = len(self._vals) + for status in self._vals.values(): + if status == Status.SKIPPED: + ret -= 1 + return ret + + def set(self, combo: ComboType, status: Status) -> None: + combo = tuple(sorted(combo)) + self._vals[combo] = status + + def lookup(self, combo: ComboType) -> Status | None: + combo = tuple(sorted(combo)) + return self._vals.get(combo, None) + + def keys(self) -> KeysView[ComboType]: + return self._vals.keys() + + +# Type that maps config strings to their default value +ConfigType = dict[str, Any] +# Callable that returns a bool +FactoryOutputType = Callable[[], bool] +# input function factory +FactoryType = Callable[[], FactoryOutputType] + +# Why are some configs disabled by default? Because if we don't the fuzzer produces uninteresting results. +# It will always hone-in on these failures, even with the most basic model, making it useless for +# debugging more complex models. +# +# More explicit explanations are below: +# Out of Scope: We can't fuzz, say, the cuda version because that comes from the environment and will +# produce a failure if not aligned with env. +# Known Failure: Disabled due to known failure. Hopefully re-enable. Known failures are listed in the +# docstring of this file. +# Required: Required for the fuzzer to operate (removing caching, etc.) +# FSDP: Flag meant for FSDP that fails in non FSDP envs. Re-enable these if you're testing FSDP. +# Typing: disabled because the type annotation of the config isn't constrained enough to produce +# meaningful fuzz values. These could be improved. +# Timing: These take too long to compile, feel free to enable. +MODULE_DEFAULTS: dict[str, ConfigType] = { + "torch._inductor.config": { + "force_disable_caches": True, # Required + "cpp.cxx": DEFAULT, # Out of Scope + "TYPE_CHECKING": DEFAULT, # Not a config + "max_autotune_pointwise": DEFAULT, # Timing + "max_autotune_gemm": DEFAULT, # Timing, re-enable when autotune speed improvements merged. + "max_autotune_gemm_backends": DEFAULT, # Timing + "max_autotune_conv_backends": DEFAULT, # Timing + "max_autotune_gemm_search_space": DEFAULT, # Timing + "max_autotune_subproc_result_timeout_seconds": DEFAULT, # Timing + "max_autotune_subproc_graceful_timeout_seconds": DEFAULT, # Timing + "max_autotune_subproc_terminate_timeout_seconds": DEFAULT, # Timing + "aot_inductor.presets": DEFAULT, # Typing + "cuda.arch": DEFAULT, # Out of Scope + "cuda.version": DEFAULT, # Out of Scope + "cuda.cutlass_dir": DEFAULT, # Out of Scope + "cuda.cuda_cxx": DEFAULT, # Out of Scope + "rocm.arch": DEFAULT, # Out of Scope + "rocm.ck_supported_arch": DEFAULT, # Out of Scope + "rocm.ck_dir": DEFAULT, # Out of Scope + "rocm.rocm_home": DEFAULT, # Out of Scope + "check_stack_no_cycles_TESTING_ONLY": DEFAULT, # Testing + "sleep_sec_TESTING_ONLY": DEFAULT, # Testing + "triton.inject_relu_bug_TESTING_ONLY": DEFAULT, # Testing + "reorder_for_compute_comm_overlap": DEFAULT, # FSDP + "enabled_metric_tables": DEFAULT, # Typing + "triton.debug_sync_graph": DEFAULT, # Known Failure + "triton.debug_sync_kernel": DEFAULT, # Known Failure + "profile_bandwidth_regex": DEFAULT, # Known Failure + "disable_cpp_codegen": DEFAULT, # Known Failure + "trace.save_real_tensors": DEFAULT, # Known Failure + "pre_grad_fusion_options": DEFAULT, # Typing + "external_matmul": DEFAULT, # Typing, need to add this to type overrides or type exemplars. + "test_configs.autotune_choice_name_regex": DEFAULT, # Typing + "test_configs.autotune_choice_desc_regex": DEFAULT, # Typing + "cpp.enable_floating_point_contract_flag": DEFAULT, # Typing + "post_grad_custom_pre_pass": DEFAULT, # Typing + "post_grad_custom_post_pass": DEFAULT, # Typing + "reorder_for_compute_comm_overlap_passes": DEFAULT, # Typing + "joint_custom_post_pass": DEFAULT, # Typing + "joint_custom_pre_pass": DEFAULT, # Typing + "pre_grad_custom_pass": DEFAULT, # Typing + "custom_partitioner_fn": DEFAULT, # Typing + "inductor_choices_class": DEFAULT, # Typing + "cudagraph_policy": DEFAULT, # Typing + }, + "torch._dynamo.config": { + "traceable_tensor_subclasses": DEFAULT, # Typing + "nontraceable_tensor_subclasses": DEFAULT, # Typing + "compiled_autograd_kwargs_override": DEFAULT, # Typing + "fail_on_recompile_limit_hit": DEFAULT, # fails in combo with suppress_errors + "suppress_errors": DEFAULT, + "caching_precompile": False, # Required + }, +} + + +class ConfigFuzzer: + """ + This tool makes it easy to search through config state-space with a minimal reproduction or test, either for + debugging or just bug hunting. + It has two entry points: + - bisect, which randomly flips configs and tries to find the minimal reproduction upon failure. + - fuzz_n_tuple, which tries every combination of n configs. This grows quickly as a function of n, so beware. + bisect is recommended, but fuzz_n_tuple can give you peace of mind that a new config will compose with + every other config. + + The main interface is a function factory that will return Callables to be torch.compiled. This function factory + should return a test function when it's called. Said test function returns a boolean, which determines whether + the ConfigFuzzer considers it a successful run or not. Throwing an exception from within the function will be + considered a failure as well. + + # Example usage: + + ```python + import torch._inductor.config as cfg + + + def create_simple_test_model_gpu() -> FactoryOutputType: + batch_size = 32 + seq_length = 50 + hidden_size = 768 + + def test_fn() -> bool: + inp = torch.randn(batch_size, seq_length, hidden_size, device="cuda") + weight = torch.randn(hidden_size, hidden_size, device="cuda") + matmul_output = inp @ weight + final_output = torch.nn.LayerNorm(hidden_size, device="cuda")(matmul_output) + return True + + return test_fn + + + fuzzer = ConfigFuzzer(cfg, create_simple_test_model_gpu, seed=2) + + # Test every pair of configs: + results = fuzzer.fuzz_n_tuple(n, max_combinations=10000000) + + visualize_results(n, results) + + # Test random configs with bisection: + ret = fuzzer.bisect(num_attempts=10) + + # reproduce a failing config + fuzzer.reproduce( + [{"triton.autotune_pointwise": ..., "coordinate_descent_tuning": ...}] + ) + ``` + + The list of known failures on inductor config are: + cpp_wrapper, triton_debug_sync_graph + cpp_wrapper, triton_debug_sync_kernel + cpp_wrapper, disable_cpp_codegen + combo_kernels, benchmark_combo_kernel, profile_bandwidth, profile_bandwidth_regex + trace.enabled, trace.save_real_tensors + """ + + sample: SamplingType + default: ConfigType + + def __init__( + self, + config_module: ConfigModule, + test_model_fn_factory: FactoryType, + seed: int, + default: ConfigType | None = None, + sm: SamplingMethod = SamplingMethod.TOGGLE, + test_timeout: int = 3600, + ): + """ + Args: + config_module: The module containing the configs to fuzz + test_model_fn_factory: Function that returns a test model, which runs and returns True if successful, or + the outputs if they should be compared with eager + seed: Randomness seed. + default: Default values for the config. Inductor has preset based on know failures. + sm: How type value samples are generated, default TOGGLE. + test_timeout: max time a test can take. + """ + self.seed = seed + self.test_timeout = test_timeout + self.detailed_results: dict[ComboType, dict[str, Any]] = {} + self.config_module = config_module + self.test_model_fn_factory = test_model_fn_factory + self.fields: dict[str, _ConfigEntry] = self.config_module._config + self.sample = SamplingMethod.dispatch(sm) + + if default is None: + if self.config_module.__name__ in MODULE_DEFAULTS: + self.default = MODULE_DEFAULTS[self.config_module.__name__] + else: + raise ValueError("No default passed to ConfigFuzzer.") + else: + self.default = default + + def __repr__(self) -> str: + return ( + f"ConfigFuzzer(config_module={self.config_module}, " + f"test_model_fn_factor={self.test_model_fn_factory}, seed={self.seed}, default={self.default})" + ) + + def _set_config(self, field_name: str, value: Any) -> None: + """Set a config value in the module.""" + setattr(self.config_module, field_name, value) + + def _reset_configs(self) -> None: + """Reset all configs to their default values.""" + for field_name, field_obj in self.fields.items(): + self._set_config(field_name, field_obj.default) + + def new_config(self) -> ConfigType: + """creates a new config from the default""" + ret = { + name: val if val != DEFAULT else self.fields[name].default + for name, val in self.default.items() + } + return ret + + def reproduce(self, configs: Sequence[ConfigType]) -> ResultType: + """entrypoint to reproduce any failure""" + results = ResultType() + for conf in configs: + self._reproduce_single_helper(conf, results) + return results + + def _reproduce_single_helper(self, conf: ConfigType, results: ResultType) -> None: + print(f"Starting repro of {conf}") + new_config = self.new_config() + new_config.update(conf) + self.test_config(results, new_config) + print(f"Status of {conf}:\n{results.lookup(tuple(conf.keys()))}") + + def reproduce_single(self, config: ConfigType) -> ResultType: + results = ResultType() + self._reproduce_single_helper(config, results) + return results + + def _fuzz_helper(self, results: ResultType, combo: ComboType) -> Status: + print(combo) + if st := results.lookup(combo): + # we already processed this config + return st + + config = self.new_config() + + skip = False + for field_name in combo: + if field_name in config: + # don't break here because we need to build the config dict + skip = True + if field_name.startswith("_"): + skip = True + field = self.fields[field_name] + value = self.sample(field_name, field.value_type, field.default) + config[field_name] = value + if skip: + results.set(combo, Status.SKIPPED) + return Status.SKIPPED + + return self.test_config(results, config) + + def fuzz_n_tuple(self, n: int, max_combinations: int = 1000) -> ResultType: + """ + Test every combination of n configs. + + returns a dict of this shape: {(config-1, config-2... config-n): status} + """ + results = ResultType() + print(f"Starting {n}-tuple testing with seed {self.seed}") + random.seed(self.seed) + + for combo in itertools.combinations(self.fields, n): + st = self._fuzz_helper(results, combo) + if st != Status.SKIPPED: + max_combinations -= 1 + if max_combinations <= 0: + print("Reached maximum combinations limit") + break + + return results + + def save_state(self, filename: str = "fuzzer_state.pkl") -> None: + """Save the current fuzzer state to a file""" + with open(filename, "wb") as f: + pickle.dump( + {"results": self.results, "detailed_results": self.detailed_results}, f + ) + + def load_state(self, filename: str = "fuzzer_state.pkl") -> None: + """Load fuzzer state from a file""" + with open(filename, "rb") as f: + state = pickle.load(f) + self.results = state["results"] + self.detailed_results = state.get("detailed_results", {}) + + def timeout_handler(self, signum: int, frame: FrameType | None) -> None: + raise TimeoutError("Test execution timed out") + + def test_config(self, results: ResultType, config: ConfigType) -> Status: + """ + Tests a config by calling the function produced by the factory function. + """ + original_handler = signal.signal(signal.SIGALRM, self.timeout_handler) + signal.alarm(self.test_timeout) + print(f"Testing config {config}") + config_tuple = tuple(config.keys()) + if ret := results.lookup(config_tuple): + signal.signal(signal.SIGALRM, original_handler) + return ret + + def print_config() -> None: + for field, value in config.items(): + print(f"{field} = {value}") + + def get_error_info(exc: Exception) -> dict[str, Any]: + return { + "exception": str(exc), + "traceback": traceback.format_exc(), + "config": config.copy(), + } + + def handle_return( + message: str, + return_status: Status, + print_traceback: bool, + exc: Exception | None, + ) -> Status: + signal.signal(signal.SIGALRM, original_handler) + print(f"{message} with config combination:") + print_config() + if exc: + self.detailed_results[config_tuple] = get_error_info(exc) + if print_traceback: + traceback.print_exc() + results.set(config_tuple, return_status) + return return_status + + # reset config + torch._dynamo.reset() + self._reset_configs() + for name, value in config.items(): + self._set_config(name, value) + + # try running eager + test_model_fn = self.test_model_fn_factory() + try: + test_model_fn() + except Exception as exc: + return handle_return( + "Eager exception", Status.FAILED_RUN_EAGER_EXCEPTION, True, exc + ) + + # try compilation + try: + test_model_fn2 = self.test_model_fn_factory() + comp = torch.compile(test_model_fn2, backend="inductor") + except Exception as exc: + return handle_return( + "Exception compiling", Status.FAILED_COMPILE, True, exc + ) + + # try running compiled + try: + compile_result = comp() + except Exception as exc: + return handle_return( + "Exception running compiled", + Status.FAILED_RUN_COMPILE_EXCEPTION, + True, + exc, + ) + + # bool return value means don't compare with eager + if not compile_result: + return handle_return( + "Function returned False", Status.FAILED_RUN_RETURN, False, None + ) + else: + return handle_return("Function succeeded", Status.PASSED, False, None) + + def bisect(self, num_attempts: int = 100, p: float = 0.5) -> list[ConfigType]: + """ + Test configs and bisect to minimal failing configuration. + """ + print(f"Starting random testing with bisection, seed {self.seed}, and p {p}") + random.seed(self.seed) + self._reset_configs() + results = ResultType() + ret: list[ConfigType] = [] + + for attempt in range(num_attempts): + print(f"Random attempt {attempt + 1}/{num_attempts}") + + config = self.new_config() + + for field_name, config_entry in self.fields.items(): + if ( + field_name not in config + and not field_name.startswith("_") + and "TESTING_ONLY" not in field_name + and random.random() < p + ): + value = self.sample( + field_name, config_entry.value_type, config_entry.default + ) + config[field_name] = value + + status = self.test_config(results, config) + if status not in OrderedSet([Status.PASSED, Status.SKIPPED]): + if minimal_failing_config := self._bisect_failing_config( + results, config + ): + print(f"Minimum failing config: {minimal_failing_config}") + ret.append(minimal_failing_config) + + return ret + + def _bisect_failing_config( + self, results: ResultType, failing_config: ConfigType + ) -> ConfigType | None: + return self._bisect_failing_config_helper(results, list(failing_config.items())) + + def _bisect_failing_config_helper( + self, results: ResultType, failing_config: list[tuple[str, Any]] + ) -> ConfigType | None: + """ + Bisect a failing configuration to find minimal set of configs that cause failure. + + Splits it into halves, then fourths, then tries dropping configs one-by-one. + """ + print(f"bisecting config: {failing_config}") + + if not failing_config: + return None + + def test(x: list[tuple[str, Any]]) -> Status: + d = dict(x) + result = self.test_config(results, d) + return result + + if len(failing_config) <= 1: + return dict(failing_config) if test(failing_config).failing() else None + + random.shuffle(failing_config) + + mid = len(failing_config) // 2 + first_half = failing_config[:mid] + second_half = failing_config[mid:] + if test(first_half).failing(): + return self._bisect_failing_config_helper(results, first_half) + if test(second_half).failing(): + return self._bisect_failing_config_helper(results, second_half) + + if len(failing_config) >= 8: + low = len(failing_config) // 4 + high = mid + low + quart1 = failing_config[low:] + if test(quart1).failing(): + return self._bisect_failing_config_helper(results, quart1) + quart2 = failing_config[:low] + second_half + if test(quart2).failing(): + return self._bisect_failing_config_helper(results, quart2) + quart3 = first_half + failing_config[:high] + if test(quart3).failing(): + return self._bisect_failing_config_helper(results, quart3) + quart4 = failing_config[high:] + if test(quart4).failing(): + return self._bisect_failing_config_helper(results, quart4) + # try dropping one value at a time + for i in range(len(failing_config)): + new_list = [x for j, x in enumerate(failing_config) if j != i] + if test(new_list).failing(): + return self._bisect_failing_config_helper(results, new_list) + # we have the minimal set + return dict(failing_config) + + +def visualize_results( + n: int, results: ResultType, filename: str = "results.html" +) -> None: + """ + Creates an HTML document representing the results of running the fuzzer with fuzz_n_tuple, with n = 2. + """ + # TODO support more dimensions + assert n == 2 + assert len(results) > 0 + + input_set: OrderedSet[str] = OrderedSet({}) + for key in results.keys(): # noqa: SIM118 + input_set.add(key[0]) + input_set.add(key[1]) + input_list = sorted(input_set) + + # Start the HTML content + html_content = """ + + + + + + Fuzzer Visualization + + + +

Fuzzer Visualization

+ + + """ + + html_content += "" + for col_name in input_list: + col = "
".join(col_name) + html_content += f"" + html_content += "" + + # Add table rows + for row_name in input_list: + html_content += f"" + for col_name in input_list: + # Determine the status class for the cell + status_enum = results.lookup((row_name, col_name)) + status_class = "" + status_val = "" + if status_enum == Status.SKIPPED: + status_class = "skipped" + status_val = "-" + elif status_enum == Status.PASSED: + status_class = "passed" + status_val = "O" + elif status_enum == Status.FAILED_RUN_EAGER_EXCEPTION: + status_class = "failed" + status_val = "e" + elif status_enum == Status.FAILED_RUN_COMPILE_EXCEPTION: + status_class = "failed" + status_val = "E" + elif status_enum == Status.FAILED_RUN_RETURN: + status_class = "failed" + status_val = "R" + elif status_enum == Status.FAILED_COMPILE: + status_class = "failed" + status_val = "C" + else: + status_class = "skipped" + status_val = "-" + + html_content += f'' + html_content += "" + + html_content += """ + +
\\{col}
{row_name}{status_val}
+ + + """ + + with open(filename, "w") as file: + file.write(html_content) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/apply_gumbel_max_trick.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/apply_gumbel_max_trick.py new file mode 100644 index 0000000000000000000000000000000000000000..bbe2a8cb8b540e4c178c17e954d444a8d658a2f2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/apply_gumbel_max_trick.py @@ -0,0 +1,68 @@ +import operator + +import torch +from torch._dynamo.utils import counters + +from ..pattern_matcher import CallFunction, KeywordArg, Match, register_graph_pattern +from .pre_grad import apply_gumbel_max_trick_pass + + +@register_graph_pattern( + CallFunction( + torch.argmax, + CallFunction( + operator.truediv, + # we don't rely on PatternMatcher to match softmax + # and exponential_ due to the mutation op + KeywordArg("softmax"), + KeywordArg("rand_exp"), + ), + dim=-1, + keepdim=True, + ), + # pyrefly: ignore [bad-argument-type] + pass_dict=apply_gumbel_max_trick_pass, +) +def apply_gumbel_max_trick(match: Match, softmax, rand_exp): + if not torch._inductor.config.apply_gumbel_max_trick: + return + + if ( + rand_exp.op != "call_method" + or rand_exp.target != "exponential_" + or len(rand_exp.users) != 1 + ): + return + + empty_node, rate = rand_exp.args + if rate != 1.0: + return + + if len(empty_node.users) != 1: + return + + if ( + softmax.op != "call_function" + or softmax.target != torch.nn.functional.softmax + or len(softmax.users) != 1 + ): + return + logits = softmax.args[0] + + truediv, argmax = match.nodes + nodes_to_erase = [truediv, softmax] + + graph = match.graph + + with graph.inserting_before(argmax): + log = graph.call_function(torch.log, (rand_exp,)) + gumbel_noise = graph.call_function(operator.neg, (log,)) + argmax_input = graph.call_function(operator.add, (logits, gumbel_noise)) + # pyrefly: ignore [missing-attribute] + argmax.args[0].replace_all_uses_with(argmax_input) + + # erase nodes + for n in nodes_to_erase: + match.graph.erase_node(n) + + counters["inductor"]["apply_gumbel_max_trick"] += 1 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/auto_chunker/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/auto_chunker/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2781691a50e4d3ca339e475b2a4b8a72d12b2932 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/auto_chunker/__init__.py @@ -0,0 +1,81 @@ +import dataclasses +import functools +import itertools +import logging +import operator +import os +from collections.abc import Sequence +from typing import Optional + +import torch +from torch._dynamo.utils import detect_fake_mode +from torch._inductor import config, metrics +from torch._inductor.utils import cache_on_self +from torch.fx import GraphModule +from torch.fx.passes.fake_tensor_prop import FakeTensorProp +from torch.utils import _pytree +from torch.utils._ordered_set import OrderedSet + +from .applier import ChunkingApplier +from .common import CantChunk +from .core import find_amplifier_node +from .propagator import propagate +from .utils import get_tangent_nodes, tangent_has_chunking_meta + + +aten = torch.ops.aten +prims = torch.ops.prims +log = torch._logging.getArtifactLogger(__name__, "auto_chunker") + + +def decide_num_chunks(gm: GraphModule) -> int: + # TODO: this is just a placeholder for now. + # We should either create heuristics or do autotuning to + # decide the number of chunks. + return 8 + + +def chunk(gm: GraphModule) -> GraphModule: + """ + Chunk input tensors for operations that amplify the tensor size significantly. + The chunking operation is propagated thru the fx graph until a point we should + re-generate non-chunked tensors. + + Only chunk across the batch dimension of the tensor for now. + """ + graph = gm.graph + + if torch._inductor.config.cpp_wrapper: + raise CantChunk("cpp wrapper does not support codegening invoke_subgraph") + + if gm.meta.get("produced_by_chunker", False): + # Don't chunk a graph produced by the chunker + return gm + + if len(get_tangent_nodes(gm.graph)) == 0: + # no tangents. Can be the optimizer graph. Skip chunking + return gm + + if log.isEnabledFor(logging.DEBUG): + log.debug("Joint graph before chunking:\n%s", gm.print_readable(False)) + + amplifier_node = find_amplifier_node(graph) + if amplifier_node is None: + raise CantChunk("Skip chunking due to no amplifier node found") + + if amplifier_node.meta["val"]._has_symbolic_sizes_strides: + raise CantChunk("Can't chunk due to dynamic shape") + + propagate(amplifier_node) + if not tangent_has_chunking_meta(gm): + raise CantChunk( + "Skip chunking either because the graph is for inference only or " + "because the chunking metadata does not propagate to the backward " + "(e.g. due to too trivial loss function)" + ) + + num_chunks = config.auto_chunker.num_chunk or decide_num_chunks(gm) + out_gm = ChunkingApplier(gm, num_chunks).apply() + metrics.num_auto_chunking += 1 + log.debug("AutoChunker being applied with %s chunks", num_chunks) + return out_gm diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/auto_chunker/applier.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/auto_chunker/applier.py new file mode 100644 index 0000000000000000000000000000000000000000..37582297d6d767a1d686c854fd6de78c4c54a31e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/auto_chunker/applier.py @@ -0,0 +1,441 @@ +import copy +import logging +import operator +from typing import Any + +import torch +from torch import Tensor +from torch._dynamo.utils import detect_fake_mode +from torch.fx import Graph, GraphModule, Node +from torch.fx.passes.fake_tensor_prop import FakeTensorProp + +from .core import get_chunking_meta, reorder_nodes +from .utils import get_args_of_node_type, get_fake_tensor_from_node_arg, is_tangent_node + + +log = torch._logging.getArtifactLogger(__name__, "auto_chunker") +aten = torch.ops.aten +prims = torch.ops.prims + + +def _factory_args(fake_tensor: Tensor) -> dict[str, Any]: + return { + "device": fake_tensor.device, + "dtype": fake_tensor.dtype, + } + + +def fake_tensor_prop(gm: GraphModule) -> None: + inputs = [] + for node in gm.graph.find_nodes(op="placeholder", sort=False): + fake_tensor = get_fake_tensor_from_node_arg(node) + if fake_tensor is not None: + inputs.append(fake_tensor) + else: + inputs.append(node) + + fake_mode = detect_fake_mode(inputs) + FakeTensorProp(gm, mode=fake_mode).propagate_dont_convert_inputs(*inputs) + + +def is_chunking_subgraph_input(node: Node) -> bool: + meta = get_chunking_meta(node) + if meta is None or is_tangent_node(node): + return False + arg_nodes = get_args_of_node_type(node) + arg_nodes_no_meta = [node for node in arg_nodes if get_chunking_meta(node) is None] + return len(arg_nodes_no_meta) > 0 or node.op == "placeholder" + + +class ChunkingApplier: + """ + A class that chunks the graph assuming chunking metadata has already + been attached to the nodes in the chunking subgraph. + """ + + def __init__(self, parent_gm: GraphModule, num_chunk: int): + self.gm = parent_gm + self.parent_graph = reorder_nodes(self.gm.graph) + # From this point on self.parent_graph is not equal to self.gm.graph + # due to reordering. We create a new copy of the graph so it's + # easier to fallback if chunking fails. + self.num_chunk = num_chunk + + # tangent node to the all-one tensor + self.overriden_tangent: dict[Node, Node | None] = {} + + self.subgraph_input: list[Node] = [] + self.subgraph_body: list[Node] = [] + self.subgraph_output: list[Node] = [] + self._categorize_subgraph_nodes() + + self.chunk_sizes: list[int] | None = None + + # First index is node index, + # Second index is chunk index. + # Node index may be different to the index for self.subgraph_input + # since not every subgraph_input may be chunked. + # check self.chunk_subgraph_input for more details + self.chunked_subgraph_input: list[list[Node]] = [] + + self.accumulators: dict[Node, Node | None] = {} + self.chunks_for_recovering: dict[Node, list[Node]] = {} + for node in self.subgraph_output: + meta = get_chunking_meta(node) + assert meta + if meta.chunk_dim is not None: + self.chunks_for_recovering[node] = [] + else: + # the accumulator nodes is created later + self.accumulators[node] = None + + self.chunk_size_to_gm_attr: dict[int, str] = {} + + def _categorize_subgraph_nodes(self) -> None: + """ + For each chunked node, decide if it's a input/body/output node of the + chunking subgraph. + """ + for node in self.parent_graph.nodes: + meta = get_chunking_meta(node) + + # The node does not have chunking metadata, skip + if meta is None: + continue + + # skip tangent nodes since they are overridden as 1 and + # reapplied later in the bwd graph + if is_tangent_node(node): + self.overriden_tangent[node] = None # will update the value later + continue + + # To check if a node is a placeholder for the chunking + # subgraph there are 2 alternatives + # 1. decide it's a placeholder if it's using any non-chunked node as argument + # 2. decide it's a placeholder if it's not using any chunked nodes as argument + # These 2 rules are almost equivalent since we don't mix + # chunked and non-chunked arguments for a fx.Node. + # But one subtle difference is for aten.full node. + # We don't need to pass in aten.full as a placeholder since + # we can just add the chunked aten.full in the subgraph + # directly. + # Alternative 2 does not work for this case, so we implement + # alternative 1. + + user_nodes = node.users + user_nodes_no_meta = [ + node for node in user_nodes if get_chunking_meta(node) is None + ] + + if is_chunking_subgraph_input(node): + # None of the node's arguments are chunked. It's a placeholder + self.subgraph_input.append(node) + elif len(user_nodes_no_meta) > 0: + self.subgraph_output.append(node) + else: + self.subgraph_body.append(node) + + assert len(self.subgraph_body) > 0 + + def replace_tangent_to_one(self) -> None: + for idx, node in enumerate(self.overriden_tangent): + assert is_tangent_node(node) + + fake_tensor = node.meta["val"] + one = self.parent_graph.call_function( + aten.full.default, (fake_tensor.shape, 1), _factory_args(fake_tensor) + ) + + name = "tangent_overriden_as_one_{idx}" + one._rename(name) + one.meta = copy.copy(node.meta) + node.replace_all_uses_with(one) + self.overriden_tangent[node] = one + + def chunk_subgraph_input(self) -> None: + """ + Chunk subgraph inputs if necessary. Note that not every subgraph + input needs to be chunked. + E.g. the weight for matmul does not need to be chunked. + """ + for node_idx, subgraph_input in enumerate(self.subgraph_input): + meta = get_chunking_meta(subgraph_input) + assert meta is not None + + # not chunked + if meta.chunk_dim is None: + continue + + chunk_node = self.parent_graph.call_function( + aten.chunk.default, (subgraph_input, self.num_chunk, meta.chunk_dim) + ) + chunks = [ + self.parent_graph.call_function(operator.getitem, (chunk_node, i)) + for i in range(self.num_chunk) + ] + + self.chunked_subgraph_input.append(chunks) + + # let's do a small meta propagation to get the size of each chunk + if self.chunk_sizes is None: + input_tensor = subgraph_input.meta["val"] + tensors = torch.chunk(input_tensor, self.num_chunk) + self.chunk_sizes = [t.size(0) for t in tensors] + + def create_accumulators(self) -> None: + for node in self.accumulators: + # use fp32 for accumulators + fake_tensor = node.meta["val"] + if fake_tensor.numel() == 1: + # TODO(shunting) revisit + # This tensor may be fused with mm and becomes a addmm + # if we upcast here, addmm may fail due to incompatible + # dtypes for input arguments. + override_dtype = torch.float32 + else: + override_dtype = fake_tensor.dtype + + kwargs = _factory_args(fake_tensor) + kwargs["dtype"] = override_dtype + accum = self.parent_graph.call_function( + aten.full.default, (fake_tensor.shape, 0), kwargs + ) + # reuse the meta['val'] + accum.meta = {"val": fake_tensor.to(override_dtype)} + self.accumulators[node] = accum + + def build_subgraph(self, chunk_size: int) -> GraphModule: + """ + Build a subgraph for the given chunk size. + The last chunk can be smaller and a new subgraph will be created + to avoid involving dynamic shapes. + """ + new_graph = Graph() + env: dict[Node, Node] = {} + + def _create_placeholder_node(input_node: Node) -> Node: + new_node = new_graph.placeholder(input_node.name) + fake_tensor = input_node.meta["val"] + chunking_meta = get_chunking_meta(input_node) + if chunking_meta is not None and chunking_meta.chunk_dim is not None: + # the node is chunked and we need update the + # fake tensor + # TODO any better way to do this? + new_tensor = aten.slice.Tensor( + fake_tensor, chunking_meta.chunk_dim, 0, chunk_size + ) + fake_tensor = new_tensor + new_node.meta = {"val": fake_tensor} + return new_node + + for node_idx, input_node in enumerate(self.subgraph_input): + env[input_node] = _create_placeholder_node(input_node) + + for overriden_tangent_node in self.overriden_tangent.values(): + assert overriden_tangent_node is not None + env[overriden_tangent_node] = _create_placeholder_node( + overriden_tangent_node + ) + + for accum in self.accumulators.values(): + assert accum is not None + env[accum] = _create_placeholder_node(accum) + + for original_node in self.subgraph_body + self.subgraph_output: + assert original_node.op != "placeholder" + + # Chunk aten.full + if ( + original_node.target == aten.full.default + and (meta := get_chunking_meta(original_node)) is not None + and meta.chunk_dim is not None + ): + shape = list(original_node.args[0]) # type: ignore[arg-type] + # pyrefly: ignore [unsupported-operation] + shape[meta.chunk_dim] = chunk_size + env[original_node] = new_graph.call_function( + aten.full.default, + (shape, original_node.args[1]), + original_node.kwargs, + ) + continue + # Chunk aten.expand: adjust the target shape at the chunk dimension + if ( + original_node.target == aten.expand.default + and isinstance(original_node.args[0], torch.fx.Node) + and (meta := get_chunking_meta(original_node)) is not None + and meta.chunk_dim is not None + ): + shape = list(original_node.args[1]) # type: ignore[arg-type] + # pyrefly: ignore [unsupported-operation] + shape[meta.chunk_dim] = chunk_size + env[original_node] = new_graph.call_function( + aten.expand.default, + (env.get(original_node.args[0], original_node.args[0]), shape), # type: ignore[arg-type] + original_node.kwargs, + ) + continue + + # Chunk aten.view: adjust the target shape at the chunk dimension + if ( + original_node.target == aten.view.default + and isinstance(original_node.args[0], torch.fx.Node) + and (meta := get_chunking_meta(original_node)) is not None + and meta.chunk_dim is not None + ): + shape = list(original_node.args[1]) # type: ignore[arg-type] + # pyrefly: ignore [unsupported-operation] + shape[meta.chunk_dim] = chunk_size + env[original_node] = new_graph.call_function( + aten.view.default, + (env[original_node.args[0]], shape), # type: ignore[arg-type] + original_node.kwargs, + ) + continue + + # create the node with chunked inputs + env[original_node] = new_graph.node_copy(original_node, lambda x: env[x]) + + # Do the accumulation inside this subgraph + for node, accum in self.accumulators.items(): + lhs = env[node] + assert accum is not None + rhs = env[accum] + + # add `addend` and `accum` + add_out = new_graph.call_function(aten.add.Tensor, (lhs, rhs)) + + # override the chunk value + env[node] = add_out + + out_values = [] + for node in self.subgraph_output: + out_values.append(env[node]) + + new_graph.output(tuple(out_values)) + new_graph.eliminate_dead_code() + new_graph.lint() + + sub_gm = torch.fx._lazy_graph_module._make_graph_module(self.gm, new_graph) + fake_tensor_prop(sub_gm) + return sub_gm + + def build_subgraphs(self) -> None: + assert self.chunk_sizes is not None + for chunk_size in self.chunk_sizes: + if chunk_size in self.chunk_size_to_gm_attr: + continue + + sub_gm = self.build_subgraph(chunk_size) + gm_attr = f"chunking_subgraph_{len(self.chunk_size_to_gm_attr)}" + self.chunk_size_to_gm_attr[chunk_size] = gm_attr + setattr(self.gm, gm_attr, sub_gm) + + # Mark this sub graph module so we don't recursively chunking + # it. + sub_gm.meta["produced_by_chunker"] = True + + def call_subgraph_for_each_chunk(self) -> None: + for chunk_id in range(self.num_chunk): + assert self.chunk_sizes is not None + chunk_size = self.chunk_sizes[chunk_id] + subgraph_id = self.chunk_size_to_gm_attr[chunk_size] + sub_gm = self.parent_graph.get_attr(subgraph_id) + + args = [] + chunks_iter = iter(self.chunked_subgraph_input) + for node in self.subgraph_input: + chunking_meta = get_chunking_meta(node) + assert chunking_meta is not None + if chunking_meta.chunk_dim is not None: + args.append(next(chunks_iter)[chunk_id]) + else: + # not chunked + args.append(node) + + args += list(self.overriden_tangent.values()) # type: ignore[arg-type] + + for accum in self.accumulators.values(): + assert accum is not None + args.append(accum) + + output_node = self.parent_graph.call_function( + torch.ops.higher_order.invoke_subgraph, (sub_gm, subgraph_id, *args), {} + ) + + output_node_dict = {} + for i, orig_node in enumerate(self.subgraph_output): + output_node_dict[orig_node] = self.parent_graph.call_function( + operator.getitem, (output_node, i) + ) + + for orig_node, node_list in self.chunks_for_recovering.items(): + chunk = output_node_dict[orig_node] + node_list.append(chunk) + + for orig_node in self.accumulators: + self.accumulators[orig_node] = output_node_dict[orig_node] + + def recover_to_unchunked_nodes(self) -> None: + """ + Recover the node from chunks and do the replacement. + """ + for node in self.subgraph_output: + meta = get_chunking_meta(node) + assert meta is not None + + recovered: torch.fx.Node = node + + if meta.chunk_dim is not None: + chunks = self.chunks_for_recovering[node] + recovered = self.parent_graph.call_function( + aten.cat.default, (chunks, meta.chunk_dim) + ) + elif meta.need_sum: + recovered = self.accumulators[node] # type: ignore[assignment] + + # do scaling last + if meta.scale_by is not None: + recovered = self.parent_graph.call_function( + aten.mul.Tensor, (recovered, meta.scale_by) + ) + + # convert back to the original dtype + if meta.need_sum: + original_dtype = node.meta["val"].dtype + # TODO(shunting): do we always uses a fp32 accumulator? + if original_dtype != torch.float32: + recovered = self.parent_graph.call_function( + prims.convert_element_type.default, (recovered, original_dtype) + ) + + assert recovered is not node + node.replace_all_uses_with(recovered) + + def erase_original_nodes(self) -> None: + # Traverse reversely to erase user first + for node in reversed(tuple(self.subgraph_body + self.subgraph_output)): + if node.op == "placeholder": + continue + + self.parent_graph.erase_node(node) + + def apply(self) -> GraphModule: + with self.parent_graph.inserting_before(self.subgraph_body[0]): + self.replace_tangent_to_one() + self.chunk_subgraph_input() + self.create_accumulators() + self.build_subgraphs() + self.call_subgraph_for_each_chunk() + self.recover_to_unchunked_nodes() + self.erase_original_nodes() + + newgm = torch.fx._lazy_graph_module._make_graph_module( + self.gm, + self.parent_graph, + ) + fake_tensor_prop(newgm) + if log.isEnabledFor(logging.DEBUG): + print("Graph module after chunking:") + newgm.print_readable() + return newgm diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/auto_chunker/common.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/auto_chunker/common.py new file mode 100644 index 0000000000000000000000000000000000000000..b2a221c598f5176626ffea7ac75a9792b2564626 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/auto_chunker/common.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import dataclasses +from typing import Any, TYPE_CHECKING + + +if TYPE_CHECKING: + from torch.fx import Node + + +class CantChunk(RuntimeError): + pass + + +@dataclasses.dataclass +class ChunkingMeta: + # The value of the current node having this ChunkingMeta should be scaled by the specified scalar + # tensor in the `scale_by` field. Need this since we pretend tangent to be 1 first and + # scale the affected tensor later. Need propagate such information + # to downstream. + scale_by: Node | None = None + + # The dimension of the current tensor that get chunked. + # Can be None if the current tensor is not chunked. E.g., when + # the current tensor is a scalar tensor generated by summing a chunked tensor. + # + # To recover a tensor with non-None chunk_dim, we need concat each + # chunk at the 'chunk_dim' dimension. + chunk_dim: int | None = None + + # The original tensor is the sum of each tensor in the chunking subgraph. + # `chunk_dim` and `need_sum` are exclusive! A node can not have both a non + # None chunk_dim and a need_sum with True value. + # + # Note for some special cases like the tangent placeholder node, both + # chunk_dim can be None and need_sum can be false, but scale_by + # in that case is the tangent node itself. + need_sum: bool = False + + def __post_init__(self) -> None: + assert self.chunk_dim is None or not self.need_sum, ( + f"Can not have both a non-None chunk_dim and a true need_sum: {self.chunk_dim}" + ) + + def copy(self, **kwargs: Any) -> ChunkingMeta: + meta = ChunkingMeta(**self.__dict__) + for k, v in kwargs.items(): + setattr(meta, k, v) + return meta + + @staticmethod + def equal( + lhs_meta: ChunkingMeta, rhs_meta: ChunkingMeta, skip_scale_by: bool = False + ) -> bool: + if skip_scale_by: + lhs_meta = lhs_meta.copy(scale_by=None) + rhs_meta = rhs_meta.copy(scale_by=None) + return lhs_meta == rhs_meta + + def chunked_by_dim(self, dim: int) -> bool: + return self.chunk_dim == dim + + @staticmethod + def is_nop(meta: ChunkingMeta | None) -> bool: + return meta is None or meta == ChunkingMeta() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/auto_chunker/core.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/auto_chunker/core.py new file mode 100644 index 0000000000000000000000000000000000000000..13622253aafd9a699666cb826219ca0003ba6539 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/auto_chunker/core.py @@ -0,0 +1,219 @@ +import logging +from collections.abc import Callable, Sequence +from typing import Any + +import torch +from torch._inductor import config +from torch.fx import Graph, Node +from torch.utils._ordered_set import OrderedSet + +from .common import ChunkingMeta +from .utils import ( + compute_tensor_size, + get_args_of_node_type, + get_fake_tensor_from_node_arg, + use_tangent, +) + + +aten = torch.ops.aten +log = torch._logging.getArtifactLogger(__name__, "auto_chunker") + + +def set_chunking_meta( + node: Node, meta: ChunkingMeta | None = None, **kwargs: Any +) -> bool: + """ + kwargs can override fields in the passed in `meta` + """ + if meta is None: + meta = ChunkingMeta(**kwargs) + else: + # make a copy to avoid override the passed in instance + meta = meta.copy() + for k, v in kwargs.items(): + setattr(meta, k, v) + + old_meta = get_chunking_meta(node) + node.meta["chunking"] = meta + return old_meta is None or old_meta != meta + + +def update_chunking_meta(node: Node, **kwargs: Any) -> bool: + """ + Unlike set_chunking_mete, this function keeps the existing chunking + metadata if it's not overridden. + """ + changed = False + meta = get_chunking_meta(node) + if meta is None: + meta = ChunkingMeta() + changed = True + for k, v in kwargs.items(): + if getattr(meta, k, None) != v: + changed = True + setattr(meta, k, v) + + node.meta["chunking"] = meta + return changed + + +def set_chunking_meta_if_none( + nodes: Sequence[Node], + meta: ChunkingMeta, + filter_for_nop: Callable[[Node], bool] | None = None, +) -> bool: + """ + If filter_fop_nop returns true for a node, we set the chunking + meta to nop instead. + """ + changed = False + for node in nodes: + if get_chunking_meta(node) is None: + changed = True + if filter_for_nop and filter_for_nop(node): + set_chunking_meta(node) + else: + set_chunking_meta(node, meta) + return changed + + +def copy_chunking_meta(dst_node: Node, src_node: Node | ChunkingMeta) -> bool: + if isinstance(src_node, torch.fx.Node): + src_meta = get_chunking_meta(src_node) + else: + assert isinstance(src_node, ChunkingMeta) + src_meta = src_node + assert src_meta + return set_chunking_meta(dst_node, src_meta) + + +def get_chunking_meta(node: Node) -> ChunkingMeta | None: + return node.meta.get("chunking") + + +def has_nop_chunking_meta(node: Node) -> bool: + return ChunkingMeta.is_nop(get_chunking_meta(node)) + + +def get_chunking_metas( + nodes: Sequence[Node], skip_none: bool = False +) -> Sequence[ChunkingMeta | None]: + return [ + get_chunking_meta(node) + for node in nodes + if not skip_none or get_chunking_meta(node) is not None + ] + + +eligible_amplifier_node = OrderedSet( + [ + aten.mm.default, + aten.addmm.default, + ] +) + + +def find_amplifier_node(graph: Graph) -> Node | None: + r""" + Find the 'amplifier' node which is a node that generates large + output with small/medium input. + + If there are multiple amplifier nodes, return the one with the largest + amplification ratio. + """ + + amplifier_nodes_ratio = [] + + for node in graph.nodes: + if use_tangent(node): + # enter backward part of the graph + break + + # Only trigger chunking for a small set of nodes like matmul for now + if node.op != "call_function" or node.target not in eligible_amplifier_node: + continue + + input_size = compute_tensor_size(node.args, node.kwargs) + output_size = compute_tensor_size(node) + + if input_size == 0: + continue + + ratio = output_size / input_size + if ( + output_size > config.auto_chunker.output_size_threshold + and ratio > config.auto_chunker.amplify_ratio_threshold + ): + amplifier_nodes_ratio.append((node, ratio)) + + amplifier_nodes_ratio = sorted( + amplifier_nodes_ratio, key=lambda x: x[1], reverse=True + ) + return amplifier_nodes_ratio[0][0] if len(amplifier_nodes_ratio) > 0 else None + + +def reorder_nodes(graph: Graph) -> Graph: + """ + Create a new graph to be like: + 1. all nodes run before `chunking_subgraph_nodes` + 2. all nodes in `chunking_subgraph_nodes` + 3. all nodes run after `chunking_subgraph_nodes` + + Return a new graph so it's easier to fallback. + """ + from .applier import is_chunking_subgraph_input + + # `pre_chunking_nodes` are all nodes that only depends on + # nodes inside `pre_chuning_nodes` + pre_chunking_nodes: OrderedSet[Node] = OrderedSet() + + for node in graph.nodes: + if node.op == "placeholder" or is_chunking_subgraph_input(node): + # these nodes have chunking meta but they should be placed + # before we do chunking + pre_chunking_nodes.add(node) + + if get_chunking_meta(node) is not None: + continue + if all(arg in pre_chunking_nodes for arg in get_args_of_node_type(node)): + pre_chunking_nodes.add(node) + + post_chunking_nodes = [] + + def _copy_node(typestr: str, node: Node) -> None: + if log.isEnabledFor(logging.DEBUG): + fake_tensor = get_fake_tensor_from_node_arg(node) + shape = list(fake_tensor.shape) if fake_tensor is not None else "?" + log.debug(" - %s: %s %s", typestr, shape, node.format_node()) + env[node] = new_graph.node_copy(node, lambda x: env[x]) + + # add pre_chunking_nodes + new_graph = Graph() + env: dict[Node, Node] = {} + for node in pre_chunking_nodes: + _copy_node("prechunking", node) + + # add nodes in the chunking subgraph + for node in graph.nodes: + if node in pre_chunking_nodes: + continue + elif get_chunking_meta(node): + _copy_node("chunking", node) + else: + post_chunking_nodes.append(node) + + for node in post_chunking_nodes: + _copy_node("postchuking", node) + + assert graph._len == new_graph._len + new_graph.eliminate_dead_code() + new_graph.lint() + + # Need replace the scale_by node in the chunking meta with the new node + for node in new_graph.nodes: + meta = get_chunking_meta(node) + if meta and meta.scale_by is not None: + meta.scale_by = env[meta.scale_by] + + return new_graph diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/auto_chunker/propagate_scale_by.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/auto_chunker/propagate_scale_by.py new file mode 100644 index 0000000000000000000000000000000000000000..22848193e52a4044f37546b629ee6662dcd1cc69 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/auto_chunker/propagate_scale_by.py @@ -0,0 +1,248 @@ +import functools +import logging +from collections.abc import Callable, Sequence +from typing import TypeAlias + +import torch +from torch.fx import Node + +from .common import CantChunk +from .core import get_chunking_meta, get_chunking_metas, log, update_chunking_meta +from .utils import ( + format_node_with_chunking_meta, + get_args_of_node_type, + get_scale_by_from_metas, + get_scale_by_from_node, +) + + +aten = torch.ops.aten +prims = torch.ops.prims + +_HandlerType: TypeAlias = Callable[[Node], bool] +propagate_rules: dict[torch._ops.OpOverload, _HandlerType] = {} + + +def _register_propagate_rule( + aten_op: torch._ops.OpOverload | Sequence[torch._ops.OpOverload], + handler: _HandlerType, +) -> _HandlerType: + if not isinstance(aten_op, (list, tuple)): + aten_op = [aten_op] # type: ignore[assignment, list-item] + + assert isinstance(aten_op, (list, tuple)), f"{type(aten_op)=}" + for op in aten_op: + assert isinstance(op, torch._ops.OpOverload) + propagate_rules[op] = handler + return handler + + +def register_propagate_rule( + aten_op: torch._ops.OpOverload | Sequence[torch._ops.OpOverload], +) -> Callable[[_HandlerType], _HandlerType]: + return functools.partial(_register_propagate_rule, aten_op) + + +def propagate_scale_by(nodes_with_chunking_meta: Sequence[Node]) -> None: + """ + The input is a list of nodes that have chunking metadata. + The nodes are already sorted in topological order. + """ + for node in nodes_with_chunking_meta: + arg_nodes = get_args_of_node_type(node) + arg_metas = get_chunking_metas(arg_nodes) + + if all(arg_meta is None for arg_meta in arg_metas): + # should be graph input of the chunking subgraph + continue + + if log.isEnabledFor(logging.DEBUG): + print("Propagate scale_by:") + format_node_with_chunking_meta(node, True) + + assert all(arg_meta is not None for arg_meta in arg_metas), node.format_node() + + # None of the input has scale_by set + if all(arg_meta.scale_by is None for arg_meta in arg_metas): # type: ignore[union-attr] + continue + + target = node.target + if ( + not isinstance(target, torch._ops.OpOverload) + or target not in propagate_rules + ): + raise CantChunk( + f"Missing scale_by propagation rule for target {target}: {node.format_node()}" + ) + + if not propagate_rules[target](node): + raise CantChunk( + f"scale_by propagate rule for {target} fail: {node.format_node()}" + ) + + +@register_propagate_rule( + [ + aten.div.Tensor, + ] +) +def propagate_div(div_node: Node) -> bool: + lhs_node, rhs_node = div_node.args[:2] + assert isinstance(lhs_node, Node) + lhs_scale_by = get_scale_by_from_node(lhs_node) + + # When gradient accumulation is enabled, rhs_node can be a constant + # representing the gradient accumulation steps + rhs_scale_by = ( + get_scale_by_from_node(rhs_node) if isinstance(rhs_node, Node) else None + ) + if lhs_scale_by and rhs_scale_by is None: + update_chunking_meta(div_node, scale_by=lhs_scale_by) + return True + return False + + +@register_propagate_rule( + [ + aten.where.self, + ] +) +def propagate_where(where_node: Node) -> bool: + cond_node, true_node, false_node = where_node.args + assert isinstance(cond_node, Node) + assert isinstance(true_node, Node) + assert isinstance(false_node, Node) + cond_meta, true_meta, false_meta = get_chunking_metas( + [cond_node, true_node, false_node] + ) + out_meta = get_chunking_meta(where_node) + + assert true_meta is not None + assert false_meta is not None + if true_meta.scale_by and not false_meta.scale_by: + # the false_node must be all zero + if false_node.target != aten.full.default: + return False + if false_node.args[1] != 0.0: + return False + assert out_meta is not None + out_meta.scale_by = true_meta.scale_by + return True + return False + + +@register_propagate_rule( + [ + aten.exp.default, + aten.log.default, + aten.tanh.default, + aten.eq.Tensor, + ] +) +def propagate_requires_no_scaling(out_node: Node) -> bool: + """ + For nonlinear ops (exp, log, tanh) scale_by cannot be propagated + through since f(S*x) != S*f(x). For boolean-output ops (eq) scale_by + is meaningless. These ops only appear in the chunking subgraph when + scale_by is None (e.g. the final gradient is 1). + """ + args_node = get_args_of_node_type(out_node) + args_meta = get_chunking_metas(args_node) + out_meta = get_chunking_meta(out_node) + + scale_by = get_scale_by_from_metas(*args_meta) # type: ignore[arg-type] + assert scale_by is None, ( + f"Nonlinear op {out_node.target} requires scale_by=None, got {scale_by}" + ) + assert out_meta is not None + out_meta.scale_by = None + return True + + +@register_propagate_rule( + [ + aten.mul.Tensor, + prims.convert_element_type.default, + aten.neg.default, + aten.sum.dim_IntList, + aten.sum.default, # sum to scalar + aten.amax.default, + aten.mm.default, + aten.permute.default, + aten.expand.default, + aten.squeeze.dim, + aten.unsqueeze.default, + aten.gather.default, + aten.scatter_add.default, + aten.view.default, + ] +) +def propagate_general_copy(out_node: Node) -> bool: + """ + A rule that holds for multiple ops: the scale_by of the output is + set to the only scale_by of input nodes or None if no input has scale_by + set. + """ + args_node = get_args_of_node_type(out_node) + args_meta = get_chunking_metas(args_node) + out_meta = get_chunking_meta(out_node) + + scale_by = get_scale_by_from_metas(*args_meta) # type: ignore[arg-type] + assert out_meta is not None + out_meta.scale_by = scale_by + return True + + +@register_propagate_rule(aten.scatter.value) +def propagate_scatter_value(out_node: Node) -> bool: + # The backward of scatter.value always has value=0 (gradient of a constant), + # so S * scatter(x, idx, 0) = scatter(S*x, idx, 0) holds. + value = out_node.args[3] + if value != 0: + return False + return propagate_general_copy(out_node) + + +@register_propagate_rule( + [ + aten.add.Tensor, + aten.sub.Tensor, + ] +) +def propagate_add_sub(out_node: Node) -> bool: + """ + The scale_by node of the two arguments must be the same. + """ + lhs_node, rhs_node = get_args_of_node_type(out_node) + assert isinstance(lhs_node, Node) + assert isinstance(rhs_node, Node) + lhs_meta, rhs_meta = get_chunking_metas([lhs_node, rhs_node]) + assert lhs_meta is not None + assert rhs_meta is not None + if lhs_meta.scale_by is rhs_meta.scale_by: + update_chunking_meta(out_node, scale_by=lhs_meta.scale_by) + return True + return False + + +@register_propagate_rule( + [ + prims.fma.default, + ] +) +def propagate_fma(out_node: Node) -> bool: + mul_lhs, mul_rhs, add_rhs = out_node.args[:3] + assert isinstance(mul_lhs, Node) + assert isinstance(mul_rhs, Node) + assert isinstance(add_rhs, Node) + mul_lhs_meta, mul_rhs_meta, add_rhs_meta = get_chunking_metas( + [mul_lhs, mul_rhs, add_rhs] + ) + assert mul_lhs_meta is not None + assert mul_rhs_meta is not None + add_lhs_scale_by = get_scale_by_from_metas(mul_lhs_meta, mul_rhs_meta) + assert add_rhs_meta is not None + if add_lhs_scale_by is add_rhs_meta.scale_by: + update_chunking_meta(out_node, scale_by=add_lhs_scale_by) + return True + return False diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/auto_chunker/propagator.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/auto_chunker/propagator.py new file mode 100644 index 0000000000000000000000000000000000000000..6f5f3915cef5121284e635a46a693b4ceafe94a2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/auto_chunker/propagator.py @@ -0,0 +1,850 @@ +import functools +import logging +import math +from collections.abc import Callable, Sequence +from enum import Enum +from queue import Queue +from typing import Any, TypeAlias + +import torch +from torch.fx import Graph, Node + +from .common import CantChunk +from .core import ( + ChunkingMeta, + copy_chunking_meta, + get_chunking_meta, + has_nop_chunking_meta, + set_chunking_meta, + set_chunking_meta_if_none, + update_chunking_meta, +) +from .propagate_scale_by import propagate_scale_by +from .utils import ( + format_node_with_chunking_meta, + get_args_of_node_type, + get_fake_tensor_from_node_arg, + get_first_chunking_meta, + get_node_is_scalar, + get_node_ndim, + get_nodes_with_chunking_meta, + has_any_chunking_meta, + is_chunked_by_dim, + is_tangent_node, +) + + +log = torch._logging.getArtifactLogger(__name__, "auto_chunker") +aten = torch.ops.aten +prims = torch.ops.prims + + +""" +NOTE [Why we need both fwd and bwd chunking metadata propagation?] +The starting point of chunking is we found an op that creates much larger outputs +than the inputs size. We attach chunking metadata upon the op and propagate it forward. + +But for backward rules like NLLLossBackward, we do a scatter upon a zero matrix. That +zero matrix is created by torch.full. We will only know we should chunk that tensor +by propagating chunking metadata backward. +""" + +""" +NOTE [Why we need a separate pass to propagate ChunkingMeta.scale_by?] + +ChunkingMeta.scale_by only need to be propagated forward from the tangent placeholder nodes. +If we do this together with propagating other metadata, we can not fully control the propagating +order and end up with cases like: + out = aten.sub(lhs, rhs) +where `lhs` has scale_by set, while `rhs` and `out` don't. +For sub op, we could propagate `scale_by` to `rhs` and `out` since that's the +only way to make sense. But overall this is unsafe since maybe this is a case +that chunking does not work and we should bail out. +Another example is, we can not really propagate `scale_by` backward for +aten.mul since we don't know which of the input should have this `scale_by` metadata. + +But it's safer that we only propagate `scale_by` metadata in the topological order. + +Have the `scale_by` handled in a separate pass also makes the fwd/bwd +chunking metadata propagation much simpler. We don't need special rules +for mul/div/where etc due to the special handling of scale_by: +https://gist.github.com/shunting314/324e57881f168009784991300acab852 +""" + + +class PropagateStatus(Enum): + SUCCEED_NO_CHANGE = 0 + SUCCEED_WITH_CHANGE = 1 + FAIL = 2 + + +_HandlerRetType: TypeAlias = PropagateStatus | tuple[PropagateStatus, PropagateStatus] + +_HandlerType: TypeAlias = Callable[[Node], _HandlerRetType] + +# Rules to propagate chunking metadata from inputs to the current node +# or from the current node back to its inputs +propagate_rules: dict[torch._ops.OpOverload, _HandlerType] = {} + + +def _register_propagate_rule( + aten_op: torch._ops.OpOverload | Sequence[torch._ops.OpOverload], + handler: _HandlerType, +) -> _HandlerType: + if not isinstance(aten_op, (list, tuple)): + aten_op = [aten_op] # type: ignore[assignment, list-item] + + @functools.wraps(handler) + def wrapper(node: Node, *args: Any, **kwargs: Any) -> PropagateStatus: + fwd_bwd_status = handler(node, *args, **kwargs) + if isinstance(fwd_bwd_status, PropagateStatus): + return fwd_bwd_status + assert isinstance(fwd_bwd_status, (list, tuple)) and len(fwd_bwd_status) == 2 + fwd_status, bwd_status = fwd_bwd_status + log.debug( + "Chunking metadata propagation for %s: Fwd status %s, bwd status %s", + node, + fwd_status, + bwd_status, + ) + if fwd_status == PropagateStatus.FAIL or bwd_status == PropagateStatus.FAIL: + return PropagateStatus.FAIL + if ( + fwd_status == PropagateStatus.SUCCEED_WITH_CHANGE + or bwd_status == PropagateStatus.SUCCEED_WITH_CHANGE + ): + return PropagateStatus.SUCCEED_WITH_CHANGE + return PropagateStatus.SUCCEED_NO_CHANGE + + assert isinstance(aten_op, (list, tuple)), f"{type(aten_op)=}" + for op in aten_op: + assert isinstance(op, torch._ops.OpOverload) + propagate_rules[op] = wrapper + return wrapper + + +def register_propagate_rule( + aten_op: torch._ops.OpOverload | Sequence[torch._ops.OpOverload], +) -> Callable[[_HandlerType], _HandlerType]: + return functools.partial(_register_propagate_rule, aten_op) + + +def _is_success(*statuslist: PropagateStatus) -> bool: + return not any(status == PropagateStatus.FAIL for status in statuslist) + + +def _enqueue(queue: Queue, item: Node) -> None: # type: ignore[type-arg] + """ + Have a function to make it easier to do debug logging in a central place + """ + queue.put(item) + log.debug("Enqueue: %s", item) + + +def can_reach_amplified_node( + graph: Graph, amplifier_node: Node, is_fwd: bool +) -> dict[Node, bool]: + """ + A amplified node means a node with the same numel as `amplifier_node` + """ + filter_obj: dict[Node, bool] = {} + nodelist = reversed(graph.nodes) if is_fwd else graph.nodes + target_numel = get_fake_tensor_from_node_arg(amplifier_node).numel() # type: ignore[union-attr] + + for node in nodelist: + reach = False + if node.op == "output": + # output node does not have a meta['val'] + reach = False + + elif get_fake_tensor_from_node_arg(node) is None: + reach = False + + # for the back propagation, we should continue propagate if we can + # reach a tangent node + elif get_fake_tensor_from_node_arg(node).numel() == target_numel or ( # type: ignore[union-attr] + not is_fwd and is_tangent_node(node) + ): + reach = True + else: + neighbors = node.users if is_fwd else get_args_of_node_type(node) + reach = any(filter_obj[neighbor] for neighbor in neighbors) + filter_obj[node] = reach + return filter_obj + + +def propagate(amplifier_node: Node) -> None: + log.debug("amplifier_node is %s", amplifier_node.format_node()) + # Chunk the batch dimension (dim 0) of the amplifier_node + graph = amplifier_node.graph + + fwd_filter = can_reach_amplified_node(graph, amplifier_node, True) + bwd_filter = can_reach_amplified_node(graph, amplifier_node, False) + + log.debug("fwd_filter %s", fwd_filter) + log.debug("bwd_filter %s", bwd_filter) + + assert len(get_nodes_with_chunking_meta(graph)) == 0, ( + "Expect no nodes with chunking meta yet" + ) + + set_chunking_meta(amplifier_node, chunk_dim=0) + + queue: Queue[Node] = Queue() + _enqueue(queue, amplifier_node) + + while not queue.empty(): + propagate_single_node(queue, fwd_filter, bwd_filter, queue.get()) + + nodes_with_chunking_meta = get_nodes_with_chunking_meta(graph) + propagate_scale_by(nodes_with_chunking_meta) + + if log.isEnabledFor(logging.DEBUG): + print("All nodes with chunking metadata set:") + for node in nodes_with_chunking_meta: + format_node_with_chunking_meta(node) + + +def propagate_single_node( + queue: Queue, fwd_filter: dict[Node, bool], bwd_filter: dict[Node, bool], node: Node +) -> None: # type: ignore[type-arg] + log.debug("Propagate_single_node: %s", node.format_node()) + + if node.op != "call_function": + raise CantChunk( + "Chunker can only propagate chunking metadata thru call_function nodes" + ) + + target = node.target + if log.isEnabledFor(logging.DEBUG): + log.debug("Before propagation, the node has the following chunking meta:") + format_node_with_chunking_meta(node, True) + + if not isinstance(target, torch._ops.OpOverload) or target not in propagate_rules: + raise CantChunk( + f"Missing propagation rule for target {target}: {node.format_node()}" + ) + + status = propagate_rules[target](node) + + if log.isEnabledFor(logging.DEBUG): + log.debug("After propagation, the node has the following chunking meta:") + format_node_with_chunking_meta(node, True) + + if status == PropagateStatus.FAIL: + raise CantChunk(f"Propagate rule for {target} fail: {node.format_node()}") + elif status == PropagateStatus.SUCCEED_WITH_CHANGE: + # propagate to used nodes + for arg in get_args_of_node_type(node): + # don't propagate back thru a placeholder node + if arg.op == "placeholder": + if "tangent" in arg.target: # type: ignore[operator] + # we have a separate pass to propagate scale_by information fwd. + set_chunking_meta(arg, scale_by=arg) + elif bwd_filter[arg]: + _enqueue(queue, arg) + + # propagate to user nodes + if fwd_filter[node]: + for user in node.users: + _enqueue(queue, user) + else: + assert status == PropagateStatus.SUCCEED_NO_CHANGE, ( + f"status type {type(status)}, value {status}" + ) + + +def _bool_to_status(changed: bool) -> PropagateStatus: + """ + Return the variant of the success flag depending on whether there is any change. + """ + return ( + PropagateStatus.SUCCEED_WITH_CHANGE + if changed + else PropagateStatus.SUCCEED_NO_CHANGE + ) + + +@register_propagate_rule(aten.addmm.default) +def propagate_addmm(addmm_node: Node) -> _HandlerRetType: + bias_node, input_node, weight_node = addmm_node.args + + def propagate_addmm_fwd() -> PropagateStatus: + assert isinstance(bias_node, Node) + assert isinstance(input_node, Node) + assert isinstance(weight_node, Node) + if not has_any_chunking_meta(bias_node, input_node, weight_node): + return PropagateStatus.SUCCEED_NO_CHANGE + + # only input is chunked by dim 0 + if ( + has_nop_chunking_meta(bias_node) + and has_nop_chunking_meta(weight_node) + and is_chunked_by_dim(input_node, 0) + ): + # set a nop chunking metadata on bias_node & weight_node + # to indicate that they should be a part of the chunking + # subgraph. (i.e. we pass in them as placeholder node) + return _bool_to_status( + copy_chunking_meta(addmm_node, input_node) + | set_chunking_meta(bias_node) + | set_chunking_meta(weight_node) + ) + return PropagateStatus.FAIL + + def propagate_addmm_bwd() -> PropagateStatus: + assert isinstance(bias_node, Node) + assert isinstance(input_node, Node) + assert isinstance(weight_node, Node) + + if not (meta := get_chunking_meta(addmm_node)): + return PropagateStatus.SUCCEED_NO_CHANGE + + if meta.chunked_by_dim(0): + # if the output is chunked by the batch dimension, then + # bias and input should as well + changed = set_chunking_meta(input_node, meta) | set_chunking_meta( + weight_node + ) + + # We should chunk the bias only if it's not broadcasted + bias_node_ft = get_fake_tensor_from_node_arg(bias_node) + input_node_ft = get_fake_tensor_from_node_arg(input_node) + assert bias_node_ft is not None + assert input_node_ft is not None + if bias_node_ft.ndim < input_node_ft.ndim: + changed |= set_chunking_meta(bias_node) + else: + changed |= set_chunking_meta(bias_node, meta) + return _bool_to_status(changed) + + return PropagateStatus.FAIL + + return propagate_addmm_fwd(), propagate_addmm_bwd() + + +@register_propagate_rule(aten.mm.default) +def propagate_mm(mm_node: Node) -> _HandlerRetType: + lhs_node, rhs_node = mm_node.args[:2] + + def fwd() -> PropagateStatus: + assert isinstance(lhs_node, Node) + assert isinstance(rhs_node, Node) + lhs_meta = get_chunking_meta(lhs_node) + rhs_meta = get_chunking_meta(rhs_node) + + if has_nop_chunking_meta(lhs_node) and has_nop_chunking_meta(rhs_node): + return _bool_to_status(False) + + # only lhs is chunked + if not has_nop_chunking_meta(lhs_node) and has_nop_chunking_meta(rhs_node): + assert lhs_meta is not None + return _bool_to_status( + copy_chunking_meta(mm_node, lhs_meta) | set_chunking_meta(rhs_node) + ) + + # either lhs or rhs is chunked at the reduction dimension + if (lhs_meta is not None and lhs_meta.chunk_dim == 1) or ( + rhs_meta is not None and rhs_meta.chunk_dim == 0 + ): + # The output is not chunked, but need to be sum'ed up! + return _bool_to_status( + set_chunking_meta(mm_node, chunk_dim=None, need_sum=True) + | update_chunking_meta(lhs_node, chunk_dim=1) + | update_chunking_meta(rhs_node, chunk_dim=0) + ) + + return PropagateStatus.FAIL + + def bwd() -> PropagateStatus: + assert isinstance(lhs_node, Node) + assert isinstance(rhs_node, Node) + out_meta = get_chunking_meta(mm_node) + if out_meta is None: + return _bool_to_status(False) + + # first dim of a 2D output is chunked + ft = get_fake_tensor_from_node_arg(mm_node) + assert ft is not None + if ft.ndim == 2 and out_meta.chunk_dim == 0: + rhs_meta = get_chunking_meta(rhs_node) + assert ChunkingMeta.is_nop(rhs_meta) + return _bool_to_status( + copy_chunking_meta(lhs_node, mm_node) | set_chunking_meta(rhs_node) + ) + + if out_meta.need_sum: + changed = set_chunking_meta(lhs_node, chunk_dim=1) | set_chunking_meta( + rhs_node, chunk_dim=0 + ) + return _bool_to_status(changed) + + return PropagateStatus.FAIL + + return fwd(), bwd() + + +@register_propagate_rule( + [ + prims.convert_element_type.default, + aten.exp.default, + aten.log.default, + aten.tanh.default, + aten.add.Tensor, + aten.sub.Tensor, + aten.div.Tensor, + aten.mul.Tensor, + prims.fma.default, + aten.where.self, + aten.neg.default, + aten.eq.Tensor, + ] +) +def propagate_general_copy_metadata( + out_node: Node, ignore_broadcast: bool = False +) -> _HandlerRetType: + """ + A general propagation rules that basically copy around the chunking + metadata. + """ + node_args = get_args_of_node_type(out_node) + node_is_scalar = get_node_is_scalar(node_args) + node_ndim = get_node_ndim(node_args) + + scalar_args = [node for node in node_args if node_is_scalar[node]] + non_scalar_args = [node for node in node_args if not node_is_scalar[node]] + + out_ndim = out_node.meta["val"].ndim + + # This general rule only allow scalar tensors without chunking meta + if scalar_args and not all( + ChunkingMeta.is_nop(get_chunking_meta(arg)) for arg in scalar_args + ): + return PropagateStatus.FAIL + + def _chunk_broadcasted_tensor(chunk_dim: int) -> bool: + for node in non_scalar_args: + if node_ndim[node] != out_ndim and chunk_dim >= out_ndim - node_ndim[node]: + return True + return False + + def propagate_fwd() -> PropagateStatus: + if len(node_args) == 0: + return PropagateStatus.FAIL + + first_meta = get_first_chunking_meta(*non_scalar_args) + if first_meta is None: + return _bool_to_status(False) + + need_handle_broadcast = ( + not ignore_broadcast and first_meta.chunk_dim is not None + ) + if ( + need_handle_broadcast + and first_meta.chunk_dim is not None + and _chunk_broadcasted_tensor(first_meta.chunk_dim) + ): + # We don't chunking a broadcasted tensor for now. + # Can add the rule if such a use case come up + return PropagateStatus.FAIL + + changed = set_chunking_meta_if_none( + non_scalar_args, first_meta, lambda node: node_ndim[node] != out_ndim + ) + + for other_node in non_scalar_args: + other_meta = get_chunking_meta(other_node) + + if need_handle_broadcast and node_ndim[other_node] != out_ndim: + if not ChunkingMeta.is_nop(other_meta): + return PropagateStatus.FAIL + else: + if other_meta != first_meta: + return PropagateStatus.FAIL + + changed |= copy_chunking_meta(out_node, first_meta) + return _bool_to_status(changed) + + def propagate_bwd() -> PropagateStatus: + if not (meta := get_chunking_meta(out_node)): + return PropagateStatus.SUCCEED_NO_CHANGE + + need_handle_broadcast = not ignore_broadcast and meta.chunk_dim is not None + if ( + need_handle_broadcast + and meta.chunk_dim is not None + and _chunk_broadcasted_tensor(meta.chunk_dim) + ): + return PropagateStatus.FAIL + + # apply any to a list to avoid short-circuit + changed = any( # noqa: C419 + [ # noqa: C419 + copy_chunking_meta(node, meta) + if not need_handle_broadcast or node_ndim[node] == out_ndim + else set_chunking_meta(node) + for node in non_scalar_args + ] + ) + + # [NOTE: NOP Chunking metadata] + # For scalar node arguments, we add a nop ChunkingMeta so the + # propagation continues. This is mainly needed to reach the point + # where we attach chunking metadata to tangents that need to be + # included in the chunking subgraph. + # This is different to having a None ChunkingMeta + changed |= any( # noqa: C419 + [ # noqa: C419 + set_chunking_meta(node) + for node in scalar_args + if get_chunking_meta(node) is None + ] + ) + + return _bool_to_status(changed) + + return propagate_fwd(), propagate_bwd() + + +@register_propagate_rule( + [ + aten.squeeze.dim, + aten.gather.default, + aten.scatter.value, + aten.scatter_add.default, + ] +) +def propagate_general_copy_metadata_ignore_broadcast(out_node: Node) -> _HandlerRetType: + return propagate_general_copy_metadata(out_node, ignore_broadcast=True) # type: ignore[call-arg] + + +@register_propagate_rule( + [ + aten.amax.default, + aten.sum.dim_IntList, + ] +) +def propagate_reduce(reduce_node: Node) -> _HandlerRetType: + arg_node, reduce_dims = reduce_node.args[0:2] + + def propagate_fwd() -> PropagateStatus: + assert isinstance(arg_node, Node) + assert isinstance(reduce_dims, (tuple, list)) + arg_meta = get_chunking_meta(arg_node) + if arg_meta is None: + return PropagateStatus.SUCCEED_NO_CHANGE + if arg_meta.chunk_dim not in reduce_dims: + # Reduce across the non chunked dimension + return _bool_to_status(copy_chunking_meta(reduce_node, arg_meta)) + + # sum across the chunked dimension. E.g. happens for computing + # the gradient of bias for an addmm + if reduce_node.target == aten.sum.dim_IntList and list(reduce_dims) == [ + arg_meta.chunk_dim + ]: + return _bool_to_status( + set_chunking_meta(reduce_node, arg_meta, chunk_dim=None, need_sum=True) + ) + + return PropagateStatus.FAIL + + def propagate_bwd() -> PropagateStatus: + assert isinstance(arg_node, Node) + assert isinstance(reduce_dims, (tuple, list)) + out_meta = get_chunking_meta(reduce_node) + if out_meta is None: + return PropagateStatus.SUCCEED_NO_CHANGE + if out_meta.chunk_dim is not None: + assert out_meta.chunk_dim not in reduce_dims + return _bool_to_status(copy_chunking_meta(arg_node, out_meta)) + + if out_meta.chunk_dim is None and out_meta.need_sum and len(reduce_dims) == 1: + assert reduce_node.target == aten.sum.dim_IntList + return _bool_to_status( + set_chunking_meta( + arg_node, out_meta, chunk_dim=reduce_dims[0], need_sum=False + ) + ) + + return PropagateStatus.FAIL + + return propagate_fwd(), propagate_bwd() + + +@register_propagate_rule(aten.permute.default) +def propagate_permute(permute_node: Node) -> _HandlerRetType: + input_node, order = permute_node.args[:2] + + def propagate_fwd() -> PropagateStatus: + assert isinstance(input_node, Node) + assert isinstance(order, (tuple, list)) + input_meta = get_chunking_meta(input_node) + output_meta = get_chunking_meta(permute_node) + if input_meta is None: + return _bool_to_status(False) + + if input_meta.chunk_dim is None: + return PropagateStatus.FAIL + + orig_chunk_dim = input_meta.chunk_dim + # pyrefly: ignore [bad-argument-type, bad-assignment] + reverse_lookup: dict[int, int] = {v: k for k, v in enumerate(order)} + new_chunk_dim = reverse_lookup[orig_chunk_dim] + + # sanity check + if output_meta is not None: + assert output_meta.chunk_dim == new_chunk_dim + return _bool_to_status( + set_chunking_meta(permute_node, meta=input_meta, chunk_dim=new_chunk_dim) + ) + + def propagate_bwd() -> PropagateStatus: + assert isinstance(input_node, Node) + assert isinstance(order, (tuple, list)) + + input_meta = get_chunking_meta(input_node) + output_meta = get_chunking_meta(permute_node) + if output_meta is None: + return _bool_to_status(False) + + if output_meta.chunk_dim is None: + return PropagateStatus.FAIL + + orig_chunk_dim = output_meta.chunk_dim + + new_chunk_dim = order[orig_chunk_dim] + + # sanity check + if input_meta is not None: + assert input_meta.chunk_dim == new_chunk_dim + return _bool_to_status( + set_chunking_meta(input_node, meta=output_meta, chunk_dim=new_chunk_dim) + ) + + return propagate_fwd(), propagate_bwd() + + +@register_propagate_rule( + [ + aten.full.default, # nop since there is no inputs for fwd/bwd metadata propagation + ] +) +def propagate_nop(node: Node) -> _HandlerRetType: + def fwd() -> PropagateStatus: + return _bool_to_status(False) + + def bwd() -> PropagateStatus: + return _bool_to_status(False) + + return fwd(), bwd() + + +@register_propagate_rule(aten.unsqueeze.default) +def propagate_unsqueeze(unsqueeze_node: Node) -> _HandlerRetType: + input_node, unsqueeze_dim = unsqueeze_node.args[:2] + assert isinstance(input_node, Node) + assert isinstance(unsqueeze_dim, int) + input_ndim = get_fake_tensor_from_node_arg(input_node).ndim # type: ignore[union-attr] + # Normalize negative dim: unsqueeze valid range is [-(ndim+1), ndim] + normalized_dim = ( + unsqueeze_dim + input_ndim + 1 if unsqueeze_dim < 0 else unsqueeze_dim + ) + + def fwd() -> PropagateStatus: + assert isinstance(input_node, Node) + input_meta = get_chunking_meta(input_node) + if input_meta is None: + return _bool_to_status(False) + if input_meta.chunk_dim is None: + return _bool_to_status(copy_chunking_meta(unsqueeze_node, input_meta)) + + # pyrefly: ignore[unsupported-operation] + new_dim = input_meta.chunk_dim + ( + 1 if input_meta.chunk_dim >= normalized_dim else 0 + ) + return _bool_to_status( + set_chunking_meta(unsqueeze_node, meta=input_meta, chunk_dim=new_dim) + ) + + def bwd() -> PropagateStatus: + assert isinstance(input_node, Node) + output_meta = get_chunking_meta(unsqueeze_node) + if output_meta is None: + return _bool_to_status(False) + if output_meta.chunk_dim is None: + return _bool_to_status(copy_chunking_meta(input_node, output_meta)) + # pyrefly: ignore[unsupported-operation] + new_dim = output_meta.chunk_dim - ( + 1 if output_meta.chunk_dim > normalized_dim else 0 + ) + return _bool_to_status( + set_chunking_meta(input_node, meta=output_meta, chunk_dim=new_dim) + ) + + return fwd(), bwd() + + +def _find_chunk_dim_after_reshape( + old_shape: Sequence[int], new_shape: Sequence[int], chunk_dim: int +) -> int | None: + """ + Find the equivalent chunk_dim position after a reshape by matching + the prefix product (number of elements before the dimension) and + the dimension size. Returns None if the chunk dimension is merged + or split by the reshape, making it unsafe to propagate. + + Examples: + [M, N] -> [M, N, 1], chunk_dim=0: returns 0 (trailing dim added) + [M] -> [M, 1], chunk_dim=0: returns 0 + [M, N] -> [M1, M2, N] where M1*M2=M, chunk_dim=0: returns None (split) + [M, N] -> [M*N], chunk_dim=0: returns None (merged) + """ + chunk_size = old_shape[chunk_dim] + old_offset = math.prod(old_shape[:chunk_dim]) + new_offset = 1 + for new_dim in range(len(new_shape)): + if new_offset == old_offset and new_shape[new_dim] == chunk_size: + return new_dim + new_offset *= new_shape[new_dim] + return None + + +@register_propagate_rule(aten.view.default) +def propagate_view(view_node: Node) -> _HandlerRetType: + input_node = view_node.args[0] + assert isinstance(input_node, Node) + input_shape = list(get_fake_tensor_from_node_arg(input_node).shape) # type: ignore[union-attr] + output_shape = list(get_fake_tensor_from_node_arg(view_node).shape) # type: ignore[union-attr] + + def fwd() -> PropagateStatus: + assert isinstance(input_node, Node) + input_meta = get_chunking_meta(input_node) + if input_meta is None: + return _bool_to_status(False) + if input_meta.chunk_dim is None: + return _bool_to_status(copy_chunking_meta(view_node, input_meta)) + new_dim = _find_chunk_dim_after_reshape( + input_shape, output_shape, input_meta.chunk_dim + ) + if new_dim is None: + return PropagateStatus.FAIL + return _bool_to_status( + set_chunking_meta(view_node, meta=input_meta, chunk_dim=new_dim) + ) + + def bwd() -> PropagateStatus: + assert isinstance(input_node, Node) + output_meta = get_chunking_meta(view_node) + if output_meta is None: + return _bool_to_status(False) + if output_meta.chunk_dim is None: + return _bool_to_status(copy_chunking_meta(input_node, output_meta)) + new_dim = _find_chunk_dim_after_reshape( + output_shape, input_shape, output_meta.chunk_dim + ) + if new_dim is None: + return PropagateStatus.FAIL + return _bool_to_status( + set_chunking_meta(input_node, meta=output_meta, chunk_dim=new_dim) + ) + + return fwd(), bwd() + + +@register_propagate_rule( + [ + aten.expand.default, + ] +) +def propagate_expand(expand_node: Node) -> _HandlerRetType: + input_node = expand_node.args[0] + assert isinstance(input_node, Node) + + input_ft = get_fake_tensor_from_node_arg(input_node) + assert input_ft is not None + output_ft = get_fake_tensor_from_node_arg(expand_node) + assert output_ft is not None + input_shape = list(input_ft.shape) + output_shape = list(output_ft.shape) + + if input_ft.numel() == 1: + # Scalar input: combined fwd/bwd rule + output_meta = get_chunking_meta(expand_node) + if output_meta is None: + return _bool_to_status(False) + return _bool_to_status(set_chunking_meta(input_node)) + + # How many leading dims are added by expand + dim_offset = len(output_shape) - len(input_shape) + + def is_expand_dim(out_dim: int) -> bool: + """Check if out_dim is a broadcast dimension (newly added or size 1 in input).""" + return out_dim < dim_offset or input_shape[out_dim - dim_offset] == 1 + + def fwd() -> PropagateStatus: + assert isinstance(input_node, Node) + input_meta = get_chunking_meta(input_node) + if input_meta is None: + return _bool_to_status(False) + # Fail if chunk_dim is an expand dimension (input size 1 broadcast to larger size) + if input_meta.chunk_dim is not None and is_expand_dim( + input_meta.chunk_dim + dim_offset + ): + return PropagateStatus.FAIL + return _bool_to_status(copy_chunking_meta(expand_node, input_meta)) + + def bwd() -> PropagateStatus: + assert isinstance(input_node, Node) + output_meta = get_chunking_meta(expand_node) + if output_meta is None: + return _bool_to_status(False) + # Fail if chunk_dim is an expand dimension (input size 1 broadcast to larger size) + if output_meta.chunk_dim is not None and is_expand_dim(output_meta.chunk_dim): + return PropagateStatus.FAIL + return _bool_to_status(copy_chunking_meta(input_node, output_meta)) + + return fwd(), bwd() + + +@register_propagate_rule( + [ + aten.sum.default, + ] +) +def propagate_sum_to_scalar(sum_node: Node) -> _HandlerRetType: + input_node = sum_node.args[0] + assert isinstance(input_node, Node) + + def fwd() -> PropagateStatus: + input_meta = get_chunking_meta(input_node) # pyrefly: ignore[bad-argument-type] + if has_nop_chunking_meta(input_node): # pyrefly: ignore[bad-argument-type] + return _bool_to_status(False) + + assert input_meta is not None + if input_meta.chunk_dim is not None: + changed = update_chunking_meta(sum_node, need_sum=True, chunk_by=None) + return _bool_to_status(changed) + return PropagateStatus.FAIL + + def bwd() -> PropagateStatus: + input_meta = get_chunking_meta(input_node) # pyrefly: ignore[bad-argument-type] + if has_nop_chunking_meta(sum_node): + return _bool_to_status(False) + + # We won't know how the input is chunked if sum_node.meta.need_sum is True. + # On the other hand, sum_node.meta.need_sum is True can only happen + # by propagating from input_node. Doing sanity check here is fine + # since the input_node.meta should have already been properly + # setup. + output_meta = get_chunking_meta(sum_node) + if ( + output_meta is not None + and output_meta.need_sum + and input_meta is not None + and input_meta.chunk_dim is not None + ): + return _bool_to_status(False) + + return PropagateStatus.FAIL + + return fwd(), bwd() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/auto_chunker/utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/auto_chunker/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..d37b32e61e5df728743fec2edcbc14f8360a7b4a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/auto_chunker/utils.py @@ -0,0 +1,183 @@ +from collections.abc import Sequence +from typing import Any + +import torch +from torch.fx import Graph, GraphModule, Node +from torch.utils._pytree import tree_flatten + +from .common import CantChunk, ChunkingMeta + + +def is_tangent_node(node: Node) -> bool: + return node.op == "placeholder" and "tangent" in node.name + + +def get_args_of_node_type(node: Node) -> Sequence[Node]: + return [x for x in tree_flatten((node.args, node.kwargs))[0] if isinstance(x, Node)] + + +def use_tangent(node: Node) -> bool: + """ + Whether the fx node uses tangent input. + """ + + return any( + is_tangent_node(arg) # type: ignore[operator] + for arg in get_args_of_node_type(node) + ) + + +def compute_tensor_size(*args: Any, count_bytes: bool = True, **kwargs: Any) -> int: + """ + Compute total tensor sizes from fx.Node in args & kwargs. + """ + flat_args, _ = tree_flatten((args, kwargs)) + tot = 0 + for arg in flat_args: + if (fake_tensor := get_fake_tensor_from_node_arg(arg)) is None: + continue + tot += fake_tensor.numel() * (fake_tensor.dtype.itemsize if count_bytes else 1) + return tot + + +def get_fake_tensor_from_node_arg( + node: torch.fx.node.Argument, +) -> torch.Tensor | None: + if ( + not hasattr(node, "meta") + or ("val" not in node.meta) # type: ignore[union-attr] + or not isinstance(node.meta["val"], torch.Tensor) # type: ignore[union-attr] + ): + return None + return node.meta["val"] # type: ignore[union-attr] + + +def get_nodes_with_chunking_meta(graph: torch.fx.Graph) -> Sequence[Node]: + from .core import get_chunking_meta + + output = [] + for node in graph.nodes: + if get_chunking_meta(node): + output.append(node) + return output + + +def format_node_with_chunking_meta( + node: torch.fx.Node, include_args: bool = False +) -> None: + """ + Print the node with chunking metadata for the current node if exists. + + If include_args is True, also print chuning metadata for Node arguments. + """ + from torch._inductor.runtime.runtime_utils import green_text + + from .core import get_chunking_meta + + fake_tensor = get_fake_tensor_from_node_arg(node) + shape = list(fake_tensor.shape) if fake_tensor is not None else "?" + print(f" {shape} {node.format_node()}") + + if meta := get_chunking_meta(node): + print(f" {green_text(str(meta))}") + + if include_args: + for arg in get_args_of_node_type(node): + if arg_meta := get_chunking_meta(arg): + print(f" {arg}: {green_text(str(arg_meta))}") + + +def has_any_chunking_meta(*node_list: Node) -> bool: + from .core import get_chunking_meta + + return any(get_chunking_meta(node) for node in node_list) + + +def get_first_chunking_meta(*node_list: Node) -> ChunkingMeta | None: + """ + Get the first non-none chunking metadata if there is any. + """ + from .core import get_chunking_meta + + for node in node_list: + if (meta := get_chunking_meta(node)) is not None: + return meta + + return None + + +def get_scale_by_from_metas(*metas: ChunkingMeta) -> Node | None: + """ + If there are multiple ChunkingMeta having the scale_by field, + raise a CantChunk exception. + + If no ChunkingMeta has scale_by field, return None. + Other wise return the only scale_by field. + """ + + scale_by_list = [] + + # don't do dedup on the scale_by field on purpose for this API + for meta in metas: + if meta.scale_by is not None: + scale_by_list.append(meta.scale_by) + + if len(scale_by_list) > 1: + raise CantChunk("Multiple scale_by") + + return scale_by_list[0] if len(scale_by_list) == 1 else None + + +def get_scale_by_from_node(node: Node) -> Node | None: + from .core import get_chunking_meta + + meta = get_chunking_meta(node) + return meta.scale_by if meta is not None else None + + +def get_node_is_scalar(nodes: Sequence[Node]) -> dict[Node, bool]: + """ + Returns a dict map a node to 'is_scalar'. + """ + node_is_scalar = {} + for node in nodes: + ft = get_fake_tensor_from_node_arg(node) + assert ft is not None + node_is_scalar[node] = ft.numel() == 1 + return node_is_scalar + + +def get_node_ndim(nodes: Sequence[Node]) -> dict[Node, int]: + """ + Returns a dict map a node to 'ndim'. + """ + node_ndim = {} + for node in nodes: + ft = get_fake_tensor_from_node_arg(node) + assert ft is not None + node_ndim[node] = ft.ndim + return node_ndim + + +def is_chunked_by_dim(node: Node, dim: int) -> bool: + from .core import get_chunking_meta + + meta = get_chunking_meta(node) + return meta is not None and meta.chunked_by_dim(dim) + + +def tangent_has_chunking_meta(gm: GraphModule) -> bool: + from .core import get_chunking_meta + + return any( + is_tangent_node(node) and get_chunking_meta(node) is not None + for node in gm.graph.find_nodes(op="placeholder", sort=False) + ) + + +def get_tangent_nodes(graph: Graph) -> Sequence[Node]: + tangents = [] + for node in graph.find_nodes(op="placeholder", sort=False): + if is_tangent_node(node): + tangents.append(node) + return tangents diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/b2b_gemm.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/b2b_gemm.py new file mode 100644 index 0000000000000000000000000000000000000000..a2e203fcfa0b64ae425517cd1c5590a34cd5046f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/b2b_gemm.py @@ -0,0 +1,774 @@ +# mypy: allow-untyped-defs +import functools +from collections import deque + +import torch +from torch.utils._ordered_set import OrderedSet +from torch.utils._pytree import tree_map + +from ..._dynamo.utils import counters +from ..ir import ( + ComputedBuffer, + FixedLayout, + FlexibleLayout, + InputBuffer, + StorageBox, + Subgraph, + TensorBox, +) +from ..lowering import lowerings +from ..pattern_matcher import ( + Arg, + CallFunction, + Match, + PatternMatcherPass, + register_graph_pattern, +) +from ..select_algorithm import ( + autotune_select_algorithm, + ExternKernelChoice, + SymbolicGridFn, + TritonTemplate, + TritonTemplateCaller, +) +from ..utils import ceildiv + + +B2B_GEMM_PASS = PatternMatcherPass( + pass_name="b2b_gemm_pass", +) + + +@SymbolicGridFn +def b2b_gemm_grid(M, P, meta, *, cdiv): + return (cdiv(M, meta["BLOCK_SIZE_M"]) * cdiv(P, meta["BLOCK_SIZE_P"]), 1, 1) + + +b2b_gemm_left_template = TritonTemplate( + name="b2b_gemm_left", + grid=b2b_gemm_grid, + debug=False, + source=r""" +{{def_kernel("A", "B", "C")}} + + + # B2B_GEMM_LEFT_TRITON_ENTRANCE + + # dynamic shapes + M = {{size("A", 0)}} + N = {{size("A", 1)}} + O = {{size("C", 0)}} + P = {{size("C", 1)}} + + # dynamic strides + stride_am = {{stride("A", 0)}} + stride_an = {{stride("A", 1)}} + stride_bn = {{stride("B", 0)}} + stride_bo = {{stride("B", 1)}} + stride_co = {{stride("C", 0)}} + stride_cp = {{stride("C", 1)}} + + # output block counts + num_m_block = tl.cdiv(M, BLOCK_SIZE_M) + num_p_block = tl.cdiv(P, BLOCK_SIZE_P) + + # internal block counts + num_n_block = tl.cdiv(N, BLOCK_SIZE_N) + num_o_block = tl.cdiv(O, BLOCK_SIZE_O) + + # output block ids + pid = tl.program_id(axis=0) + m_block_id = pid // num_p_block + p_block_id = pid % num_p_block + + # accumulator + acc = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_P), dtype=tl.float32) + + # main loop + offs_m = (m_block_id * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)) + offs_p = (p_block_id * BLOCK_SIZE_P + tl.arange(0, BLOCK_SIZE_P)) + # (subgraph(A @ B) @ C) + offs_o = tl.arange(0, BLOCK_SIZE_O) + for _ in range(num_o_block): + c_mask = (offs_o[:, None] < O) & (offs_p[None, :] < P) + c_ptrs = C + (offs_o[:, None] * stride_co + offs_p[None, :] * stride_cp) + c = tl.load(c_ptrs, mask=c_mask, other=0.0).to(tl.float32) # BLOCK_SIZE_O * BLOCK_SIZE_P + acc_ab = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_O), dtype=tl.float32) + offs_n = tl.arange(0, BLOCK_SIZE_N) + for __ in range(num_n_block): + a_mask = (offs_m[:, None] < M) & (offs_n[None, :] < N) + a_ptrs = A + (offs_m[:, None] * stride_am + offs_n[None, :] * stride_an) + a = tl.load(a_ptrs, mask=a_mask, other=0.0).to(tl.float32) # BLOCK_SIZE_M * BLOCK_SIZE_N + b_mask = (offs_n[:, None] < N) & (offs_o[None, :] < O) + b_ptrs = B + (offs_n[:, None] * stride_bn + offs_o[None, :] * stride_bo) + b = tl.load(b_ptrs, mask=b_mask, other=0.0).to(tl.float32) # BLOCK_SIZE_N * BLOCK_SIZE_O + acc_ab += tl.dot(a, b, out_dtype=tl.float32) + offs_n += BLOCK_SIZE_N + # apply the subgraph + {{ modification( + subgraph_number=0, + output_name="post_subgraph_acc_ab", + inner_mm="acc_ab" + ) | indent_except_first(2) }} + acc += tl.dot(post_subgraph_acc_ab, c, out_dtype=tl.float32) + offs_o += BLOCK_SIZE_O + + # type conversion + acc = acc.to(tl.float16) + + # store preparation + idx_m = offs_m[:, None] + idx_p = offs_p[None, :] + out_mask = (idx_m < M) & (idx_p < P) + + {{store_output(("idx_m", "idx_p"), "acc", "out_mask", val_shape=("BLOCK_SIZE_M", "BLOCK_SIZE_P"))}} +""", +) + + +b2b_gemm_right_template = TritonTemplate( + name="b2b_gemm_right", + grid=b2b_gemm_grid, + debug=False, + source=r""" +{{def_kernel("A", "B", "C")}} + + + # B2B_GEMM_RIGHT_TRITON_ENTRANCE + + # dynamic shapes + M = {{size("A", 0)}} + N = {{size("A", 1)}} + O = {{size("C", 0)}} + P = {{size("C", 1)}} + + # dynamic strides + stride_am = {{stride("A", 0)}} + stride_an = {{stride("A", 1)}} + stride_bn = {{stride("B", 0)}} + stride_bo = {{stride("B", 1)}} + stride_co = {{stride("C", 0)}} + stride_cp = {{stride("C", 1)}} + + # output block counts + num_m_block = tl.cdiv(M, BLOCK_SIZE_M) + num_p_block = tl.cdiv(P, BLOCK_SIZE_P) + + # internal block counts + num_n_block = tl.cdiv(N, BLOCK_SIZE_N) + num_o_block = tl.cdiv(O, BLOCK_SIZE_O) + + # output block ids + pid = tl.program_id(axis=0) + m_block_id = pid // num_p_block + p_block_id = pid % num_p_block + + # accumulator + acc = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_P), dtype=tl.float32) + + # main loop (two cases) + offs_m = (m_block_id * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)) + offs_p = (p_block_id * BLOCK_SIZE_P + tl.arange(0, BLOCK_SIZE_P)) + # (A @ subgraph(B @ C)) + offs_n = tl.arange(0, BLOCK_SIZE_N) + for _ in range(num_n_block): + a_mask = (offs_m[:, None] < M) & (offs_n[None, :] < N) + a_ptrs = A + (offs_m[:, None] * stride_am + offs_n[None, :] * stride_an) + a = tl.load(a_ptrs, mask=a_mask, other=0.0).to(tl.float32) # BLOCK_SIZE_M * BLOCK_SIZE_N + acc_bc = tl.zeros((BLOCK_SIZE_N, BLOCK_SIZE_P), dtype=tl.float32) + offs_o = tl.arange(0, BLOCK_SIZE_O) + for __ in range(num_o_block): + b_mask = (offs_n[:, None] < N) & (offs_o[None, :] < O) + b_ptrs = B + (offs_n[:, None] * stride_bn + offs_o[None, :] * stride_bo) + b = tl.load(b_ptrs, mask=b_mask, other=0.0).to(tl.float32) # BLOCK_SIZE_N * BLOCK_SIZE_O + c_mask = (offs_o[:, None] < O) & (offs_p[None, :] < P) + c_ptrs = C + (offs_o[:, None] * stride_co + offs_p[None, :] * stride_cp) + c = tl.load(c_ptrs, mask=c_mask, other=0.0).to(tl.float32) # BLOCK_SIZE_O * BLOCK_SIZE_P + acc_bc += tl.dot(b, c, out_dtype=tl.float32) + offs_o += BLOCK_SIZE_O + # apply the subgraph + {{ modification( + subgraph_number=0, + output_name="post_subgraph_acc_bc", + inner_mm="acc_bc" + ) | indent_except_first(2) }} + acc += tl.dot(a, post_subgraph_acc_bc, out_dtype=tl.float32) + offs_n += BLOCK_SIZE_N + + # type conversion + acc = acc.to(tl.float16) + + # store preparation + idx_m = offs_m[:, None] + idx_p = offs_p[None, :] + out_mask = (idx_m < M) & (idx_p < P) + + {{store_output(("idx_m", "idx_p"), "acc", "out_mask", val_shape=("BLOCK_SIZE_M", "BLOCK_SIZE_P"))}} +""", +) + + +# Note: load_ratio_left and load_ratio_right are only calculating numbers +# in the trivial subgraph case; i.e. (A @ (B @ C)) or ((A @ B) @ C) + + +def load_ratio_left( + M: int, N: int, O: int, P: int, m: int, n: int, o: int, p: int +) -> float: + """ + compute the ratio of estimated numbers of loads in baseline and b2bgemm + M, N, O, P are matrix sizes + m, n, o, p are block sizes + | | baseline (lower bound) | b2bgemm + | load | M * N + N * O + M * O + O * P | M / m * P / p * O / o * (o * p + N / n * (m * n + n * o)) + | store | M * O + M * P | M * P + b2bgemm is always better on stores, but for loads we need to find out beneficial cases using this function + """ + base = M * N + N * O + M * O + O * P + gemm = ( + ceildiv(M, m) + * ceildiv(P, p) + * ceildiv(O, o) + * (o * p + ceildiv(N, n) * (m * n + n * o)) + ) + return base / gemm + + +def load_ratio_right( + M: int, N: int, O: int, P: int, m: int, n: int, o: int, p: int +) -> float: + """ + compute the ratio of estimated numbers of loads in baseline and b2bgemm + M, N, O, P are matrix sizes + m, n, o, p are block sizes + | | baseline (lower bound) | b2bgemm + | load | N * O + O * P + M * N + N * P | M / m * P / p * N / n * (m * n + O / o * (n * o + o * p)) + | store | N * P + M * P | M * P + b2bgemm is always better on stores, but for loads we need to find out beneficial cases using this function + """ + base = N * O + O * P + M * N + N * P + gemm = ( + ceildiv(M, m) + * ceildiv(P, p) + * ceildiv(N, n) + * (m * n + ceildiv(O, o) * (n * o + o * p)) + ) + return base / gemm + + +# the block sizes are limited by hardware (the shared memory) +# intuitively, the optimization works when the intermediate matrix is large +# and we assign large block sizes to large dimensions +b2b_gemm_configs = [ + { + "BLOCK_SIZE_M": 128, + "BLOCK_SIZE_N": 16, + "BLOCK_SIZE_O": 16, + "BLOCK_SIZE_P": 16, + "num_stages": 4, + "num_warps": 8, + }, + { + "BLOCK_SIZE_M": 128, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_O": 32, + "BLOCK_SIZE_P": 32, + "num_stages": 2, + "num_warps": 4, + }, + { + "BLOCK_SIZE_M": 128, + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_O": 64, + "BLOCK_SIZE_P": 64, + "num_stages": 2, + "num_warps": 4, + }, + { + "BLOCK_SIZE_M": 128, + "BLOCK_SIZE_N": 16, + "BLOCK_SIZE_O": 128, + "BLOCK_SIZE_P": 16, + "num_stages": 4, + "num_warps": 8, + }, + { + "BLOCK_SIZE_M": 128, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_O": 128, + "BLOCK_SIZE_P": 32, + "num_stages": 2, + "num_warps": 4, + }, + { + "BLOCK_SIZE_M": 128, + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_O": 128, + "BLOCK_SIZE_P": 64, + "num_stages": 2, + "num_warps": 4, + }, + { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 16, + "BLOCK_SIZE_O": 16, + "BLOCK_SIZE_P": 128, + "num_stages": 4, + "num_warps": 8, + }, + { + "BLOCK_SIZE_M": 32, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_O": 32, + "BLOCK_SIZE_P": 128, + "num_stages": 2, + "num_warps": 4, + }, + { + "BLOCK_SIZE_M": 64, + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_O": 64, + "BLOCK_SIZE_P": 128, + "num_stages": 2, + "num_warps": 4, + }, + { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 128, + "BLOCK_SIZE_O": 16, + "BLOCK_SIZE_P": 128, + "num_stages": 4, + "num_warps": 8, + }, + { + "BLOCK_SIZE_M": 32, + "BLOCK_SIZE_N": 128, + "BLOCK_SIZE_O": 32, + "BLOCK_SIZE_P": 128, + "num_stages": 2, + "num_warps": 4, + }, + { + "BLOCK_SIZE_M": 64, + "BLOCK_SIZE_N": 128, + "BLOCK_SIZE_O": 64, + "BLOCK_SIZE_P": 128, + "num_stages": 2, + "num_warps": 4, + }, +] + + +def is_b2b_gemm_good_on( + is_left_assoc: bool, + A_node: torch.fx.Node, + B_node: torch.fx.Node, + C_node: torch.fx.Node, +) -> bool: + """ + checks whether the sizes are good for b2b_gemm + """ + # basic checks + if not all(["val" in A_node.meta, "val" in B_node.meta, "val" in C_node.meta]): + return False + fake_tensors = ( + A_node.meta["val"], + B_node.meta["val"], + C_node.meta["val"], + ) # torch._subclasses.fake_tensor.FakeTensor + + A, B, C = fake_tensors + + def check_all_attr_true(objects, attr): + return all(hasattr(obj, attr) and getattr(obj, attr) for obj in objects) + + if not check_all_attr_true(fake_tensors, "is_cuda") and not check_all_attr_true( + fake_tensors, "is_xpu" + ): + return False + if not all([len(A.shape) == 2, len(B.shape) == 2, len(C.shape) == 2]): + return False + if not ((A.shape[1] == B.shape[0]) and (B.shape[1] == C.shape[0])): + return False + # size checks: we only dispatch to B2B-GEMM when the average load ratio is > 1 + M, N = A.shape + O, P = C.shape + ratios = [] + if is_left_assoc: + for config in b2b_gemm_configs: + ratio = load_ratio_left( + M, + N, + O, + P, + config["BLOCK_SIZE_M"], + config["BLOCK_SIZE_N"], + config["BLOCK_SIZE_O"], + config["BLOCK_SIZE_P"], + ) + ratios.append(ratio) + else: + for config in b2b_gemm_configs: + ratio = load_ratio_right( + M, + N, + O, + P, + config["BLOCK_SIZE_M"], + config["BLOCK_SIZE_N"], + config["BLOCK_SIZE_O"], + config["BLOCK_SIZE_P"], + ) + ratios.append(ratio) + ratios.sort(reverse=True) + average_ratio = 1.0 + for r in ratios[:3]: # top 3 choices + average_ratio *= r + average_ratio = average_ratio ** (1 / 3) + return ( + average_ratio > 1 + ) # even if average_ratio is close to 1, the number of stores is always better + + +def unoptimized_b2b_gemm( + is_left_assoc: bool, + subgraph: Subgraph, + A: torch.Tensor, + B: torch.Tensor, + C: torch.Tensor, + *, + out: torch.Tensor, +) -> torch.Tensor: + """ + The unoptimized version is used as a fallback when the b2b_gemm kernel is not beneficial. + """ + if is_left_assoc: + torch.mm(subgraph.graph_module(torch.mm(A, B)), C, out=out) + else: + torch.mm(A, subgraph.graph_module(torch.mm(B, C)), out=out) + return out + + +unoptimized_choice = ExternKernelChoice(unoptimized_b2b_gemm) + + +def build_subgraph_buffer( + args: list[TensorBox], + subgraph: Subgraph, +): + """ + This function is adapted from ../kernel/flex_attention.py. + The goal is to take in the required args and produce the subgraph buffer + The subgraph buffer is a ComputedBuffer that will be inlined into the triton template + + Args: + args: The args that are passed into the subgraph + subgraph: The Subgraph ir for which to produce the output node + """ + cnt = 0 + env = {} + for node in subgraph.graph_module.graph.nodes: + if node.op == "placeholder": + env[node] = args[cnt] + cnt += 1 + elif node.op == "call_function": + # For call_function we use the default lowerings and pass in the + # already created TensorBoxes as args + args, kwargs = tree_map(lambda x: env.get(x, x), (node.args, node.kwargs)) + env[node] = lowerings[node.target](*args, **kwargs) + elif node.op == "output": + + def convert_output_node_to_buffer(output): + if output is None: + return None + output_node = output + output_buffer = env[output_node] + assert isinstance(output_buffer, TensorBox), ( + "The output node for B2B-GEMM's subgraph must be a TensorBox, but got: ", + type(output_buffer), + ) + assert isinstance(output_buffer.data, StorageBox), ( + "The output node for B2B-GEMM's subgraph must be a StorageBox, but got: ", + type(output_buffer), + ) + device = output_buffer.data.get_device() + assert device is not None + subgraph_buffer = ComputedBuffer( + name=None, + layout=FlexibleLayout( + device=device, + dtype=output_buffer.data.get_dtype(), + size=output_buffer.data.get_size(), + ), + data=output_buffer.data.data, # type: ignore[arg-type] + ) + return subgraph_buffer + + # node.args[0] should be a single element representing the output of the subgraph + return tree_map(convert_output_node_to_buffer, node.args[0]) + + raise ValueError("B2B-GEMM was passed a subgraph with no output node!") + + +def create_placeholder( + name: str, dtype: torch.dtype, device: torch.device +) -> TensorBox: + """ + Creates a placeholder input buffers for producing subgraph_output + """ + input_buffer = InputBuffer(name=name, layout=FixedLayout(device, dtype, [], [])) + return TensorBox.create(input_buffer) + + +def tuned_b2b_gemm( + is_left_assoc: bool, + subgraph: Subgraph, + A: torch._inductor.ir.TensorBox, + B: torch._inductor.ir.TensorBox, + C: torch._inductor.ir.TensorBox, + *, + layout=None, +) -> torch._inductor.ir.TensorBox: + # call .realize() to get rid of Pointwise + A.realize() + B.realize() + C.realize() + layout = FixedLayout( + A.get_device_or_error(), + A.get_dtype(), + [A.shape[0], C.shape[1]], # type: ignore[index] + ) + placeholders = [ + create_placeholder("inner_mm", A.get_dtype(), A.get_device_or_error()) + ] + subgraph_buffer = build_subgraph_buffer( + placeholders, # type: ignore[arg-type, list-item] + subgraph, + ) + choices: list[TritonTemplateCaller] = [] + for config in b2b_gemm_configs: + if is_left_assoc: + b2b_gemm_left_template.maybe_append_choice( + choices, + input_nodes=(A, B, C), + layout=layout, + subgraphs=[subgraph_buffer], + **config, + ) + else: + b2b_gemm_right_template.maybe_append_choice( + choices, + input_nodes=(A, B, C), + layout=layout, + subgraphs=[subgraph_buffer], + **config, + ) + # add the unoptimized choice to mitigate performance degradation + choices.append( + unoptimized_choice.bind( + (A, B, C), layout, is_left_assoc=is_left_assoc, subgraph=subgraph + ) + ) + # autotune + node, _ = autotune_select_algorithm("b2b_gemm", choices, [A, B, C], layout) + return node + + +# match the inner mm of a potential b2b_gemm +@register_graph_pattern( + CallFunction(torch.ops.aten.mm, Arg(), Arg()), + # pyrefly: ignore [bad-argument-type] + pass_dict=B2B_GEMM_PASS, +) +def b2b_gemm_handler(match: Match, mat1: torch.fx.Node, mat2: torch.fx.Node) -> None: + # match.args: list[torch.fx.Node] + + def is_pointwise_node(node: torch.fx.Node) -> bool: + return ( + node.op == "call_function" + and isinstance(node.target, torch._ops.OpOverload) + and (torch.Tag.pointwise in node.target.tags) + ) + + def is_mm(node: torch.fx.Node) -> bool: + return node.target is torch.ops.aten.mm.default + + # the inner MM + inner_mm = match.nodes[-1] + + # find the (candidate) outer MM, which will be re-checked below to ensure every path reaches it + # In a real (A @ f(B @ C)), every path starting from (B @ C) must reach (A @ _). + outer_mm = None + node = inner_mm + while len(node.users) > 0: + node = next(iter(node.users)) + if is_mm(node): + outer_mm = node + break + elif is_pointwise_node(node): + continue + else: + break + if not outer_mm: + return + + # find the unique input node for outer_mm representing f(B @ C) in (A @ f(B @ C)) + # we call it the "f_node" + # when the pattern is simply (A @ (B @ C)), f_node is just inner_mm + f_node = inner_mm + while next(iter(f_node.users)) is not outer_mm: + f_node = next(iter(f_node.users)) + + def all_reach_via_pointwise_with_no_other_inputs( + src: torch.fx.Node, + dst: torch.fx.Node, + ) -> tuple[bool, OrderedSet[torch.fx.Node]]: + """ + check whether every user path from src reaches dst via pointwise nodes, + with no other input nodes for the intermediates and dst; + return + (1) the Boolean value + (2) the subgraph node set including src and dst (which only makes sense when the Boolean value is True) + """ + visited = OrderedSet[torch.fx.Node]() + input_counter: dict[torch.fx.Node, int] = {} + + all_reachable = True + queue = deque([src]) + while queue: + node = queue.popleft() + if node not in visited: + if node is dst: + visited.add(node) + elif (node is src) or is_pointwise_node(node): + for user in node.users: + # for nodes other than dst, bookkeep their users' input counts + if user not in input_counter: + input_counter[user] = len(user.all_input_nodes) + input_counter[user] -= 1 + # continue BFS + queue.append(user) + visited.add(node) + else: + all_reachable = False + break + + return ( + all_reachable and all(count == 0 for count in input_counter.values()), + visited, + ) + + # check inner_mm reaches f_node on every user path via pointwise nodes with no outside input_nodes + ok, subgraph_node_set = all_reach_via_pointwise_with_no_other_inputs( + inner_mm, f_node + ) + if not ok: + return + + # check inner_mm's inputs and f_node's outputs + if not (len(inner_mm.all_input_nodes) == 2 and len(f_node.users) == 1): + return + + # at this point, the nodes between inner_mm and f_node (both included) + # are all used internally inside (A @ subgraph(B @ C)) + # i.e. they neither have other users nor have other inputs + + # original graph and module + graph, module = inner_mm.graph, inner_mm.graph.owning_module + + # construct the new (sub)graph + subgraph_node_list: list[ + torch.fx.Node + ] = [] # ordered list of nodes used for node removal later + new_graph: torch.fx.Graph = torch.fx.Graph() + node_remapping: dict[torch.fx.Node, torch.fx.Node] = {} + new_input_anchor: torch.fx.Node # inner_mm, to be changed to an input node + new_output_anchor: torch.fx.Node # f_node, to be used to construct an output node + new_input_node: torch.fx.Node + new_output_node: torch.fx.Node + for node in graph.nodes: # preserve the order of nodes + if node in subgraph_node_set: + subgraph_node_list.append(node) + new_node = new_graph.node_copy(node, lambda x: node_remapping.get(x, x)) + node_remapping[node] = new_node + if node is inner_mm: + new_input_anchor = new_node + if node is f_node: + new_output_anchor = new_node + # pyrefly: ignore [unbound-name] + if new_input_anchor is not new_output_anchor: # subgraph is non-trivial + # update the input node + # pyrefly: ignore [unbound-name] + with new_graph.inserting_before(new_input_anchor): + new_input_node = new_graph.placeholder(name="subgraph_input") + # pyrefly: ignore [unbound-name] + new_input_node.meta.update(new_input_anchor.meta) + # pyrefly: ignore [unbound-name] + new_input_anchor.replace_all_uses_with(new_input_node) + # pyrefly: ignore [unbound-name] + new_graph.erase_node(new_input_anchor) + # add the output node + # pyrefly: ignore [unbound-name] + new_output_node = new_graph.output(new_output_anchor) + # pyrefly: ignore [unbound-name] + new_output_node.meta.update(new_output_anchor.meta) + else: # subgraph is trivial, e.g. (A @ (B @ C)) + # update the input node + # pyrefly: ignore [unbound-name] + with new_graph.inserting_before(new_input_anchor): + new_input_node = new_graph.placeholder(name="subgraph_input") + # pyrefly: ignore [unbound-name] + new_input_node.meta.update(new_input_anchor.meta) + # pyrefly: ignore [unbound-name] + new_input_anchor.replace_all_uses_with(new_input_node) + # pyrefly: ignore [unbound-name] + new_graph.erase_node(new_input_anchor) + # update the output node (don't use new_output_anchor since it has been erased) + new_output_node = new_graph.output(new_input_node) + new_output_node.meta.update(new_input_node.meta) + new_graph.lint() + + # construct the subgraph + subgraph = Subgraph( + name="subgraph", graph_module=torch.fx.GraphModule(module, new_graph) + ) + + # two cases + # (1) (subgraph(A @ B) @ C), called "left_assoc" + # (2) (A @ subgraph(B @ C)), called "right_assoc" + is_left_assoc = outer_mm.args[0] is f_node + + # find the nodes A, B, C and check the sizes + A: torch.fx.Node + B: torch.fx.Node + C: torch.fx.Node + if is_left_assoc: + A = inner_mm.args[0] # type: ignore[assignment] + B = inner_mm.args[1] # type: ignore[assignment] + C = outer_mm.args[1] # type: ignore[assignment] + else: + A = outer_mm.args[0] # type: ignore[assignment] + B = inner_mm.args[0] # type: ignore[assignment] + C = inner_mm.args[1] # type: ignore[assignment] + if not is_b2b_gemm_good_on(is_left_assoc, A, B, C): + return + + # finally update the original graph + counters["inductor"]["b2b_gemm"] += 1 + graph = match.graph + with graph.inserting_before(outer_mm): + function = functools.partial(tuned_b2b_gemm, is_left_assoc, subgraph) + function.__name__ = tuned_b2b_gemm.__name__ # type: ignore[attr-defined] + function._inductor_lowering_function = True # type: ignore[attr-defined] + replacement: torch.fx.Node = graph.call_function( + function, + (A, B, C), + match.kwargs, + ) + replacement.meta.update(outer_mm.meta) + outer_mm.replace_all_uses_with(replacement) + # erase unnecessary nodes + graph.erase_node(outer_mm) + for node in reversed(subgraph_node_list): + graph.erase_node(node) + graph.lint() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/binary_folding.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/binary_folding.py new file mode 100644 index 0000000000000000000000000000000000000000..2f9bce1a8a2d599da6e8fa1f9b5a9442d6cbb954 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/binary_folding.py @@ -0,0 +1,503 @@ +# mypy: allow-untyped-defs +import functools +import itertools + +import torch + +from ..._dynamo.utils import counters +from .. import config +from ..pattern_matcher import Arg, CallFunction, KeywordArg +from .freezing_patterns import register_binary_folding_pattern + + +aten = torch.ops.aten +prims = torch.ops.prims + + +def mark_mixed_dtype(computation_node): + computation_node_dtype = computation_node.meta["val"].dtype + if computation_node_dtype not in (torch.float16, torch.bfloat16): + return + + if len(computation_node.users) != 1: + return + + computation_node_user = next(iter(computation_node.users.keys())) + if not isinstance(computation_node_user.meta["val"], torch.Tensor): + return + + if computation_node_user.meta["val"].dtype != torch.float32: + return + + while computation_node_user.target in _binary_ops: + if len(computation_node_user.users) != 1: + return + + computation_node_user = next(iter(computation_node_user.users.keys())) + + if computation_node_user.target != prims.convert_element_type.default: + return + + computation_node.meta["_allow_mixed_dtype_folding"] = computation_node_dtype + + +def mark_mixed_dtype_allowed_computation_ops(gm): + """ + Mark convolutions/linear which we will binary fold even with mixed precision constants. We constant fold in the higher precision + for better accuracy and then recover the original precision after. + """ + for target in [aten.convolution.default, aten.addmm.default, aten.mm.default]: + for node in gm.graph.find_nodes(op="call_function", target=target): + mark_mixed_dtype(node) + + +def recover_original_precision_folded_computation_ops(gm): + """ + After binary folding conv/linear weights and biases to a higher dtype, recover the original precision they were in. + """ + graph = gm.graph + for target, idx in ( + (aten.convolution.default, (1, 2)), + (aten.addmm.default, (0, 2)), + (aten.mm.default, (1,)), + ): + for node in graph.find_nodes(op="call_function", target=target): + orig_dtype = node.meta.get("_allow_mixed_dtype_folding", None) + if orig_dtype is None: + continue + + with graph.inserting_before(node): + for i in idx: + old_input = node.args[i] + if old_input is None: + continue + + new_input = graph.create_node( + "call_function", + prims.convert_element_type.default, + (old_input, orig_dtype), + ) + node.replace_input_with(old_input, new_input) + + +_binary_ops = [aten.add.Tensor, aten.sub.Tensor, aten.mul.Tensor, aten.div.Tensor] + + +@functools.cache +def binary_folding_init(): + _conv_args = [Arg() for _ in range(9)] + _addmm_args = [Arg() for _ in range(3)] + _mm_args = [Arg() for _ in range(2)] + _computation_ops = [aten.convolution.default, aten.addmm.default, aten.mm.default] + _computation_calls = [ + CallFunction(aten.convolution.default, *_conv_args, _users=1), + CallFunction(aten.addmm.default, *_addmm_args, _users=1), + CallFunction( + aten.reshape.default, + CallFunction(aten.addmm.default, *_addmm_args, _users=1), + Arg(), + _users=1, + ), + CallFunction(aten.mm.default, *_mm_args, _users=1), + CallFunction( + aten.reshape.default, + CallFunction(aten.mm.default, *_mm_args, _users=1), + Arg(), + _users=1, + ), + ] + + """ + In order to fuse add/sub/mul/div with conv/linear, the dimensions of its + constant tensor must satisfy the following: + - with resizing, broadcast to w/ weight/bias tensor shape + - broadcast to the conv/linear output shape + It needs to have a shape that can resize to weight/bias + tensor shape because we need to run the op with the conv/linear + weights/bias without changing their sizes. + It needs to broadcast to the conv/linear output shape so that we do + accidentally change the shape of op output by pre-fusing it + compared to eager. + The only dimension value shared by weight, bias, and conv/linear output + is they all contain a dim with value = channels-out. In the + conv/linear output tensor, this is in the second dimension, + so the pointwise op tensor may have a second dimension of + value == channels-out, but all the other dimensions have to be 1 + """ + + def _op_not_broadcasting_with_conv(weight_tensor, other_tensor): + # According to opDoesNotBroadCastWithConv of frozen_conv_folding.cpp + weight_shape = weight_tensor.shape + other_shape = other_tensor.shape + if len(weight_shape) < len(other_shape): + return False + if len(weight_shape) == len(other_shape) + 1: + # weight shape is [o, i, *], other_shape is [o, 1...]. + for i in reversed(range(len(other_shape))): + if i == 0 and weight_shape[0] == other_shape[i]: + continue + if other_shape[i] != 1: + return False + else: + # weight shape is [o, i, *], other_shape is [1, i, *] + for i in reversed(range(len(other_shape))): + if i == 1 and weight_shape[0] == other_shape[i]: + continue + if other_shape[i] != 1: + return False + return True + + def _op_not_broadcasting_with_linear(weight_tensor, other_tensor, has_reshape): + weight_shape = weight_tensor.shape + other_shape = other_tensor.shape + other_shapes = [ + torch.Size( + [ + weight_shape[1], + ] + ), + torch.Size([1, weight_shape[1]]), + torch.Size( + [ + 1, + ] + ), + torch.Size([1, 1]), + ] + if has_reshape: + other_shapes.extend( + [ + torch.Size([1, 1, weight_shape[1]]), + torch.Size([1, 1, 1]), + ] + ) + return other_shape in other_shapes + + def _check_conv_and_broadcast_op(conv_node, other): + # According to checkConvAndBroadcastingOpPreConditions of frozen_conv_folding.cpp. + # conv.weight + if conv_node.args[1].op != "get_attr": + return False + # conv.bias + if conv_node.args[1] is not None and conv_node.args[1].op != "get_attr": + return False + if ( + not isinstance(other, int) + and not isinstance(other, float) + and other.op != "get_attr" + ): + return False + + if len(conv_node.args[1].users) != 1: + return False + + weight_meta_value = conv_node.args[1].meta.get("val") + if weight_meta_value is None: + return False + # Avoid fusing op that causes type promotion + # restricting to float avoids int/float difficulties with scalar overload + if not weight_meta_value.is_floating_point(): + return False + if isinstance(other, torch.fx.Node) and other.op == "get_attr": + other_meta_value = other.meta.get("val") + if not other_meta_value.is_floating_point(): # type: ignore[union-attr] + return False + if ( + torch.promote_types(other_meta_value.dtype, weight_meta_value.dtype) # type: ignore[union-attr] + != weight_meta_value.dtype + ): + if not conv_node.meta.get("_allow_mixed_dtype_folding", False): + return False + + if ( + other_meta_value.dtype != torch.float # type: ignore[union-attr] + and weight_meta_value.dtype not in (torch.float16, torch.bfloat16) + ): + return False + + if not _op_not_broadcasting_with_conv(weight_meta_value, other_meta_value): + return False + elif not isinstance(other, float): + return False + + return True + + def _check_linear_and_broadcast_op(linear_node, other, has_reshape): + weight_node = ( + linear_node.args[2] + if linear_node.target is aten.addmm.default + else linear_node.args[1] + ) + bias_node = ( + linear_node.args[0] if linear_node.target is aten.addmm.default else None + ) + if weight_node.op != "get_attr": + return False + if bias_node is not None and bias_node.op != "get_attr": + return False + if ( + not isinstance(other, int) + and not isinstance(other, float) + and other.op != "get_attr" + ): + return False + + if len(weight_node.users) != 1: + return False + + weight_meta_value = weight_node.meta.get("val") + if weight_meta_value is None: + return False + # Avoid fusing op that causes type promotion + # restricting to float avoids int/float difficulties with scalar overload + if not weight_meta_value.is_floating_point(): + return False + if isinstance(other, torch.fx.Node) and other.op == "get_attr": + other_meta_value = other.meta.get("val") + if not other_meta_value.is_floating_point(): # type: ignore[union-attr] + return False + if ( + torch.promote_types(other_meta_value.dtype, weight_meta_value.dtype) # type: ignore[union-attr] + != weight_meta_value.dtype + ): + if not linear_node.meta.get("_allow_mixed_dtype_folding", False): + return False + + if ( + other_meta_value.dtype != torch.float # type: ignore[union-attr] + and weight_meta_value.dtype not in (torch.float16, torch.bfloat16) + ): + return False + + if not _op_not_broadcasting_with_linear( + weight_meta_value, other_meta_value, has_reshape + ): + return False + elif not isinstance(other, float): + return False + + return True + + def _is_foldable_pattern(match): + binary_node = match.output_node() + has_reshape = False + if binary_node.args[0].target in _computation_ops: + computation_node = binary_node.args[0] + other = binary_node.args[1] + elif binary_node.args[0].target is aten.reshape.default: + computation_node = binary_node.args[0].args[0] + other = binary_node.args[1] + has_reshape = True + elif binary_node.args[1].target in _computation_ops: + computation_node = binary_node.args[1] + other = binary_node.args[0] + else: + computation_node = binary_node.args[1].args[0] + other = binary_node.args[0] + has_reshape = False + if computation_node.target is aten.convolution.default: + return _check_conv_and_broadcast_op(computation_node, other) + elif computation_node.target in [aten.addmm.default, aten.mm.default]: + return ( + config.enable_linear_binary_folding + and _check_linear_and_broadcast_op(computation_node, other, has_reshape) + ) + + return False + + def resize_scalar_or_tensor_to_shape(graph, other, shape, weight): + if isinstance(other, float): + with torch.utils._python_dispatch._disable_current_modes(): + other_tensor = torch.tensor( + other, dtype=weight.dtype, device=weight.device + ) + graph.owning_module.register_buffer("other_tensor", other_tensor) + res = graph.create_node("get_attr", "other_tensor") + res = graph.create_node( + "call_function", + aten.reshape.default, + (res, (1,)), + ) + res = graph.create_node( + "call_function", + aten.expand.default, + (res, shape), + ) + elif other.meta.get("val").numel() == 1: + # expand errors if the shape input has less # dims than the tensor input + res = graph.create_node( + "call_function", + aten.reshape.default, + (other, (1,)), + ) + res = graph.create_node( + "call_function", + aten.expand.default, + (res, shape), + ) + else: + res = graph.create_node( + "call_function", + aten.reshape.default, + (other, shape), + ) + return res + + def _create_new_conv_node(graph, conv_node, binary_node, other): + assert conv_node.target is aten.convolution.default + conv_args = list(conv_node.args) + weight_meta_value = conv_node.args[1].meta.get("val") + bias = conv_args[2] + if binary_node.target in [aten.add.Tensor, aten.sub.Tensor]: + other_reshape = resize_scalar_or_tensor_to_shape( + graph, + other, + (weight_meta_value.size(0),), + weight_meta_value, + ) + new_bias = graph.create_node( + "call_function", + binary_node.target, + (0 if bias is None else bias, other_reshape), + ) + conv_args[2] = new_bias + else: + assert binary_node.target in [aten.mul.Tensor, aten.div.Tensor] + weight_broadcast_shape = [1 for _ in range(len(weight_meta_value.shape))] + weight_broadcast_shape[0] = weight_meta_value.size(0) + other_reshape1 = resize_scalar_or_tensor_to_shape( + graph, + other, + tuple(weight_broadcast_shape), + weight_meta_value, + ) + new_weight = graph.create_node( + "call_function", binary_node.target, (conv_args[1], other_reshape1) + ) + new_weight.meta.update(conv_args[1].meta) + conv_args[1] = new_weight + if bias is not None: + other_reshape = resize_scalar_or_tensor_to_shape( + graph, + other, + (weight_meta_value.size(0),), + weight_meta_value, + ) + new_bias = graph.create_node( + "call_function", binary_node.target, (bias, other_reshape) + ) + new_bias.meta.update(bias.meta) + conv_args[2] = new_bias + return graph.create_node("call_function", conv_node.target, tuple(conv_args)) + + def _create_new_linear_node(graph, linear_node, binary_node, other): + assert linear_node.target in [aten.addmm.default, aten.mm.default] + input_node = ( + linear_node.args[1] + if linear_node.target is aten.addmm.default + else linear_node.args[0] + ) + weight_node = ( + linear_node.args[2] + if linear_node.target is aten.addmm.default + else linear_node.args[1] + ) + bias_node = ( + linear_node.args[0] if linear_node.target is aten.addmm.default else None + ) + weight_meta_value = weight_node.meta.get("val") + if binary_node.target in [aten.add.Tensor, aten.sub.Tensor]: + other_reshape = resize_scalar_or_tensor_to_shape( + graph, + other, + (weight_meta_value.size(1),), + weight_meta_value, + ) + new_bias_node = graph.create_node( + "call_function", + binary_node.target, + (0 if bias_node is None else bias_node, other_reshape), + ) + return graph.create_node( + "call_function", + aten.addmm.default, + (new_bias_node, input_node, weight_node), + ) + else: + assert binary_node.target in [aten.mul.Tensor, aten.div.Tensor] + weight_broadcast_shape = [1, weight_meta_value.size(1)] + other_reshape1 = resize_scalar_or_tensor_to_shape( + graph, + other, + tuple(weight_broadcast_shape), + weight_meta_value, + ) + new_weight_node = graph.create_node( + "call_function", binary_node.target, (weight_node, other_reshape1) + ) + new_weight_node.meta.update(weight_node.meta) + if bias_node is not None: + other_reshape = resize_scalar_or_tensor_to_shape( + graph, + other, + (weight_meta_value.size(1),), + weight_meta_value, + ) + new_bias_node = graph.create_node( + "call_function", binary_node.target, (bias_node, other_reshape) + ) + new_bias_node.meta.update(bias_node.meta) + return graph.create_node( + "call_function", + linear_node.target, + (new_bias_node, input_node, new_weight_node), + ) + else: + return graph.create_node( + "call_function", linear_node.target, (input_node, new_weight_node) + ) + + for _computation_call, binary_op in itertools.product( + _computation_calls, _binary_ops + ): + + @register_binary_folding_pattern( + CallFunction(binary_op, _computation_call, KeywordArg("other")), + extra_check=_is_foldable_pattern, + ) + def folded_op(match, *args, **kwargs): + counters["inductor"]["binary_folding"] += 1 + other = kwargs.get("other") + binary_node = match.output_node() + reshape_node = None + if binary_node.args[0].target in _computation_ops: + computation_node = binary_node.args[0] + elif binary_node.args[0].target is aten.reshape.default: + computation_node = binary_node.args[0].args[0] + reshape_node = binary_node.args[0] + elif binary_node.args[1].target in _computation_ops: + computation_node = binary_node.args[1] + else: + computation_node = binary_node.args[1].args[0] + reshape_node = binary_node.args[1] + graph = match.graph + with graph.inserting_before(reshape_node if reshape_node else binary_node): + assert computation_node.target in _computation_ops + if computation_node.target is aten.convolution.default: + counters["inductor"]["binary_folding_conv"] += 1 + new_computation_node = _create_new_conv_node( + graph, computation_node, binary_node, other + ) + else: + new_computation_node = _create_new_linear_node( + graph, computation_node, binary_node, other + ) + new_computation_node.meta.update(computation_node.meta) + if reshape_node: + assert reshape_node.target is aten.reshape.default + computation_node.replace_all_uses_with(new_computation_node) + binary_node.replace_all_uses_with(reshape_node) + else: + binary_node.replace_all_uses_with(new_computation_node) + graph.erase_node(binary_node) + graph.erase_node(computation_node) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/bucketing.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/bucketing.py new file mode 100644 index 0000000000000000000000000000000000000000..a98793040a1946e9a97672f3e4bf0041736fc51c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/bucketing.py @@ -0,0 +1,1310 @@ +import collections +import logging +import operator +from collections import defaultdict +from collections.abc import Callable +from typing import Any, Literal, TypeAlias + +import torch +import torch.distributed as dist +import torch.utils._pytree as pytree +from torch._dispatch.python import enable_python_dispatcher +from torch._dynamo.utils import detect_fake_mode +from torch._inductor.comm_analysis import ( + get_collective_type_from_kernel_name, + NCCL_COLL, +) +from torch._inductor.runtime.runtime_utils import dynamo_timed +from torch._logging import trace_structured +from torch.fx.experimental.proxy_tensor import make_fx +from torch.fx.traceback import NodeSource, NodeSourceAction +from torch.utils._ordered_set import OrderedSet + + +logger: logging.Logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +overlap_log = torch._logging.getArtifactLogger(__name__, "overlap") + +BucketMode: TypeAlias = Literal[ + "default", "custom_ops", "custom_ops_multidtype", "coalesced" +] + + +def _default_bucket_mode() -> BucketMode: + from torch._inductor import config + + return config.aten_distributed_optimizations.bucket_mode or "default" + + +# Helper functions moved to top for better organization +def _ag_group_key(node: torch.fx.Node) -> tuple[str, torch.dtype]: # type: ignore[name-defined] + _, group_size, group_name = node.args + dtype = node.meta["val"].dtype + assert isinstance(group_name, str) + return (group_name, dtype) + + +def _ag_group_key_multidtype(node: torch.fx.Node) -> tuple[str]: + _, group_size, group_name = node.args + assert isinstance(group_name, str) + return (group_name,) + + +def _rs_group_key(node: torch.fx.Node) -> tuple[str, str, torch.dtype]: # type: ignore[name-defined] + _, reduce_op, group_size, group_name = node.args + dtype = node.meta["val"].dtype + assert isinstance(group_name, str) + assert isinstance(reduce_op, str) + return (group_name, reduce_op, dtype) + + +def _ar_group_key(node: torch.fx.Node) -> tuple[str, str, torch.dtype]: + _, reduce_op, group_name = node.args + dtype = node.meta["val"].dtype + assert isinstance(group_name, str) + assert isinstance(reduce_op, str) + return (group_name, reduce_op, dtype) + + +def _compute_foreach_groups( + ag_ins: list[torch.Tensor], + out_dtypes: list[torch.dtype], # type: ignore[name-defined] +) -> list[int] | None: + """ + Compute groups of indices that have the same src/dst dtype and shape. + + Groups tensors by (src_dtype, dst_dtype, shape) to avoid falling back to the foreach slow path. + + Returns a flat list with -1 as group delimiter, or None if only one group exists. + For example, groups [[0, 2], [1]] would be encoded as [0, 2, -1, 1]. + """ + from torch.fx.experimental.symbolic_shapes import guarding_hint_or_throw + + groups: defaultdict[tuple[torch.dtype, torch.dtype, tuple[int, ...]], list[int]] = ( + defaultdict(list) + ) + for i, (ag_in, out_dtype) in enumerate(zip(ag_ins, out_dtypes)): + shape = tuple(guarding_hint_or_throw(s) for s in ag_in.shape) + key = (ag_in.dtype, out_dtype, shape) + groups[key].append(i) + + if len(groups) <= 1: + return None + + # Encode as flat list with -1 as delimiter + result: list[int] = [] + for i, group_indices in enumerate(groups.values()): + result.extend(group_indices) + if i < len(groups) - 1: + result.append(-1) + + return result + + +def _get_collective_node_from_wait(node: torch.fx.Node) -> torch.fx.Node | None: + """Given a wait node, return the collective it waits on. + + Handles both standard (wait -> collective) and coalesced + (wait -> getitem -> coalesced_collective) patterns. + Returns None if the node is not a wait on a recognized NCCL collective. + """ + if not is_wait_tensor(node): + return None + arg = node.args[0] + assert isinstance(arg, torch.fx.Node) + if arg.op != "call_function": + return None + if arg.target is operator.getitem: + assert isinstance(arg.args[0], torch.fx.Node) + arg = arg.args[0] + if arg.op != "call_function": + return None + if not isinstance(arg.target, Callable): + return None + # pyrefly: ignore [missing-attribute] + coll: NCCL_COLL = get_collective_type_from_kernel_name(arg.target.name()) + if coll == NCCL_COLL.UNSUPPORTED: + return None + return arg + + +def _schedulable_wait_node(node: torch.fx.Node) -> bool: + """Check if this wait node is schedulable (waits on a recognized NCCL collective).""" + return _get_collective_node_from_wait(node) is not None + + +def _populate_node_meta( + bucket_nodes: list[torch.fx.Node], new_nodes: list[torch.fx.Node] +): + if bucket_nodes: + for n in new_nodes: + # For the following keys, we only store the information of the first node so + # gm.print_readable shows some information + # Full information are stored in "bucketing_{key}_sources" + for key, default in [ + ("nn_module_stack", ""), + ("fwd_nn_module_stack", ""), + ("stack_trace", ""), + ("custom", {}), + ]: + n.meta[key] = bucket_nodes[0].meta.get(key, default) + + # Collect sources from all bucket nodes for this metadata key, for debugging purposes only + bucketing_sources_key = f"bucketing_{key}_sources" + # Use set to remove duplicates + if key == "stack_trace": + sources = OrderedSet( + [ + node.meta.get(key, default) + for node in bucket_nodes + if node.meta.get(key, default) + ] + ) + else: + # type might not be hashable + sources = [ + node.meta.get(key, default) + for node in bucket_nodes + if node.meta.get(key, default) + ] + n.meta[bucketing_sources_key] = sources + + # used by inductor provenance tracking + n.meta["from_node"] = [ + NodeSource( + original_node, + "bucketing_pass", + [NodeSourceAction.CREATE, NodeSourceAction.REPLACE], + ) + for original_node in bucket_nodes + ] + + +def bucket_key(node: torch.fx.Node, mode: BucketMode | None = None) -> object | None: + if is_all_gather_into_tensor(node): + group_key_fn = ( + _ag_group_key_multidtype if mode and "multidtype" in mode else _ag_group_key + ) + return group_key_fn(node) + elif is_reduce_scatter_tensor(node): + return _rs_group_key(node) + elif is_all_reduce_tensor(node): + return _ar_group_key(node) + else: + return None + + +def pick_bucket_dtype(dtypes: list[torch.dtype]) -> torch.dtype: # type: ignore[name-defined] + assert len(dtypes) > 0 + return min(dtypes, key=operator.attrgetter("itemsize")) + + +def bucket_cap_mb_by_bucket_idx_default(bucket_id: int) -> float: + """ + Determine the size of a bucket based on its ID. + + Args: + bucket_id (int): The ID of the bucket. + + Returns: + float: The size of the bucket. + """ + return 2000.0 + + +def bucket_all_gather( + gm: torch.fx.GraphModule, + bucket_cap_mb_by_bucket_idx: Callable[[int], float] | None = None, + mode: BucketMode | None = None, +) -> None: + mode = mode or _default_bucket_mode() + if bucket_cap_mb_by_bucket_idx is None: + from torch._inductor.fx_passes.bucketing import ( + bucket_cap_mb_by_bucket_idx_default, # pyrefly: ignore [missing-module-attribute] + ) + + bucket_cap_mb_by_bucket_idx = bucket_cap_mb_by_bucket_idx_default + ag_buckets = bucket_all_gather_by_mb(gm, bucket_cap_mb_by_bucket_idx, None, mode) + if len(ag_buckets) == 0: + return + merge_all_gather(gm, ag_buckets, mode) + + +def bucket_reduce_scatter( + gm: torch.fx.GraphModule, + bucket_cap_mb_by_bucket_idx: Callable[[int], float] | None = None, + mode: BucketMode | None = None, +) -> None: + mode = mode or _default_bucket_mode() + if bucket_cap_mb_by_bucket_idx is None: + from torch._inductor.fx_passes.bucketing import ( + bucket_cap_mb_by_bucket_idx_default, # pyrefly: ignore [missing-module-attribute] + ) + + bucket_cap_mb_by_bucket_idx = bucket_cap_mb_by_bucket_idx_default + rs_buckets = bucket_reduce_scatter_by_mb( + gm, bucket_cap_mb_by_bucket_idx, None, mode + ) + if len(rs_buckets) == 0: + return + merge_reduce_scatter(gm, rs_buckets, mode) + + +def is_all_gather_into_tensor(node: torch.fx.Node) -> bool: # type: ignore[arg-type] + return node.op == "call_function" and ( + node.target == torch.ops._c10d_functional.all_gather_into_tensor.default + or node.target == torch.ops._c10d_functional.all_gather_into_tensor_out.default + ) + + +def is_reduce_scatter_tensor(node: torch.fx.Node) -> bool: + return ( + node.op == "call_function" + and node.target is torch.ops._c10d_functional.reduce_scatter_tensor.default + ) + + +def is_wait_tensor(node: torch.fx.Node) -> bool: + return ( + node.op == "call_function" + and node.target is torch.ops._c10d_functional.wait_tensor.default + ) + + +def is_all_reduce_tensor(node: torch.fx.Node) -> bool: + return ( + node.op == "call_function" + and node.target is torch.ops._c10d_functional.all_reduce.default + ) + + +def is_all_to_all_tensor(node: torch.fx.Node) -> bool: + return ( + node.op == "call_function" + and node.target is torch.ops._c10d_functional.all_to_all_single.default + ) + + +def get_collective_type(node: torch.fx.Node) -> str: + """Get the collective type name for a node.""" + if is_all_gather_into_tensor(node): + return "all_gather" + elif is_reduce_scatter_tensor(node): + return "reduce_scatter" + elif is_all_reduce_tensor(node): + return "all_reduce" + return "" + + +def get_full_bucket_key( + node: torch.fx.Node, bucket_mode: BucketMode | None +) -> tuple[str, Any]: + """Get the full bucket key including collective type and bucket key.""" + return (get_collective_type(node), bucket_key(node, mode=bucket_mode)) + + +def is_wait_tensor_from_all_gather_into_tensor(node: torch.fx.Node) -> bool: + return is_wait_tensor(node) and is_all_gather_into_tensor(node.args[0]) # type: ignore[arg-type] + + +def is_fsdp_all_gather( + node: torch.fx.Node, + all_node_ancestors: dict[torch.fx.Node, OrderedSet[torch.fx.Node]], +) -> bool: + """ + Check if the node is a FSDP-related all_gather by its recursive ancestors. + On the path from the all-gather to its originate placeholder, there should not be any compute node + So there should be ONLY ONE placeholder in its recursive ancestors. + """ + if not is_all_gather_into_tensor(node): + return False + + seen_placeholders = 0 + for ancestor in all_node_ancestors[node]: + if ancestor.op == "placeholder": + seen_placeholders += 1 + + return seen_placeholders == 1 + + +def is_fsdp_reduce_scatter(node: torch.fx.Node) -> bool: + """ + Check if a reduce_scatter node is FSDP-related by verifying its output flows + directly to graph outputs through only unary ops (e.g., to_copy, wait). + """ + if not is_reduce_scatter_tensor(node): + return False + + visited: OrderedSet[torch.fx.Node] = OrderedSet() + stack = [node] + + while stack: + curr = stack.pop() + if curr in visited: + continue + visited.add(curr) + + for user in curr.users: + if user.op == "output": + continue + # Non-unary op means computation with external data + if len(user.all_input_nodes) != 1: + return False + stack.append(user) + + return True + + +def collect_node_descendants( + graph: torch.fx.Graph, +) -> dict[torch.fx.Node, OrderedSet[torch.fx.Node]]: + """ + Collects the descendants of each node in the graph. + Args: + graph (torch.fx.Graph): The graph to collect descendants from. + Returns: + dict[torch.fx.Node, OrderedSet[torch.fx.Node]]: A dictionary mapping each node to its descendants. + """ + node_descendants: dict[torch.fx.Node, OrderedSet[torch.fx.Node]] = ( + collections.defaultdict(OrderedSet) + ) + outdegree = collections.defaultdict(int) + queue = [] + + for node in graph.nodes: + n_outdegree = len(node.users) + if n_outdegree == 0: + queue.append(node) + else: + outdegree[node] = len(node.users) + + while queue: + node = queue.pop() + for input_node in node.all_input_nodes: + node_descendants[input_node] |= node_descendants[node] + node_descendants[input_node].add(node) + outdegree[input_node] -= 1 + + if outdegree[input_node] == 0: + queue.append(input_node) + + return node_descendants + + +def greedy_bucket_collective_by_mb( + gm: torch.fx.GraphModule, + bucket_cap_mb_by_bucket_idx: Callable[[int], float], + filter_node: Callable[[torch.fx.Node], bool], + node_group_key: Callable[[torch.fx.Node], Any], + filter_wait_node: Callable[[torch.fx.Node], bool] | None = None, +) -> list[list[torch.fx.Node]]: + """ + Bucketing adjacent collectives with equal node_group_key. + We can not bucket non adjacent collectives, + as this will effectively change the order of collectives. + Reordering can lead to different order on different ranks. + """ + g = gm.graph + found_candidates = False + for node in g.nodes: + if filter_node(node): + found_candidates = True + break + if not found_candidates: + return [] + + # TODO: pearce kelly algorithm for detecting cycles + node_descendents = collect_node_descendants(gm.graph) + + nodes_groups: list[list[torch.fx.Node]] = [] + cur_group: list[torch.fx.Node] = [] + cur_group_key = None + + for node in g.nodes: + if is_wait_tensor(node) and filter_node(node.args[0]): + if (filter_wait_node is None) or filter_wait_node(node): + coll_node = node.args[0] + group_key = node_group_key(coll_node) + if group_key == cur_group_key: + cur_group.append(coll_node) + else: + if len(cur_group) > 1: + nodes_groups.append(cur_group) + cur_group = [coll_node] + cur_group_key = group_key + + if len(cur_group) > 1: + nodes_groups.append(cur_group) + + buckets: list[list[torch.fx.Node]] = [] + for nodes in nodes_groups: + cur_bucket: list[torch.fx.Node] = [] + cur_bucket_descendents: OrderedSet[torch.fx.Node] = OrderedSet() + cur_bucket_size_bytes: int = 0 + cur_bucket_id: int = 0 + bucket_size_bytes = int( + bucket_cap_mb_by_bucket_idx(cur_bucket_id) * 1024 * 1024 + ) + for node in nodes: + if node in cur_bucket_descendents: + # if there is a path from node to the current bucket, we cannot horizontally fuse (bucket) + continue + assert "val" in node.meta + n_val = node.meta["val"] + out_size_bytes = n_val.numel() * n_val.element_size() + n_input_val = node.all_input_nodes[0].meta["val"] + in_size_bytes = n_input_val.numel() * n_input_val.element_size() + size_bytes = max(out_size_bytes, in_size_bytes) + if cur_bucket_size_bytes + size_bytes > bucket_size_bytes and cur_bucket: + # Current bucket is full, create new bucket + if len(cur_bucket) > 1: + buckets.append(cur_bucket) + cur_bucket = [] + cur_bucket_size_bytes = 0 + cur_bucket_id += 1 + cur_bucket_descendents = OrderedSet() + cur_bucket_size_bytes += size_bytes + cur_bucket.append(node) + cur_bucket_descendents |= node_descendents[node] + if len(cur_bucket) > 1: + buckets.append(cur_bucket) + return buckets + + +def bucket_all_gather_by_mb( + gm: torch.fx.GraphModule, + bucket_cap_mb_by_bucket_idx: Callable[[int], float], + filter_wait_node: Callable[[torch.fx.Node], bool] | None = None, + mode: BucketMode | None = None, +) -> list[list[torch.fx.Node]]: + """ + Identifies all all_gather nodes and groups them into buckets, + based on size limit `bucket_cap_mb_by_bucket_idx`. + + Args: + gm (torch.fx.GraphModule): GraphModule where to bucket all_gathers. + bucket_cap_mb_by_bucket_idx (Callable[[int], float]): Callable to specify cap of the bucket + in megabytes by bucket idx. The idea of `bucket_cap_mb_by_bucket_idx` is to allow + to specify different sizes of the buckets at the start, + as first all_gather is usually exposed. Interface of bucket_cap_mb_by_bucket_idx + is `bucket_cap_mb_by_bucket_idx_default` function that is default value for `bucket_cap_mb_by_bucket_idx`. + filter_wait_node (Callable[[torch.fx.Node], bool] | None): If specified, + only all_gather nodes with wait_node that satisfy `filter_wait_node` will be bucketed. + + Returns: + list[list[torch.fx.Node]]: List of buckets, where each bucket is a list of all_gather nodes. + """ + mode = mode or _default_bucket_mode() + + group_key_fn = ( + _ag_group_key_multidtype if mode and "multidtype" in mode else _ag_group_key + ) + + return greedy_bucket_collective_by_mb( + gm, + bucket_cap_mb_by_bucket_idx, + is_all_gather_into_tensor, + group_key_fn, + filter_wait_node, + ) + + +def bucket_reduce_scatter_by_mb( + gm: torch.fx.GraphModule, + bucket_cap_mb_by_bucket_idx: Callable[[int], float], + filter_wait_node: Callable[[torch.fx.Node], bool] | None = None, + mode: BucketMode | None = None, +) -> list[list[torch.fx.Node]]: + """ + Identifies all reduce_scatter nodes and groups them into buckets, + based on size limit `bucket_cap_mb_by_bucket_idx`. + + Args: + gm (torch.fx.GraphModule): GraphModule where to bucket reduce_scatters. + bucket_cap_mb_by_bucket_idx (Callable[[int], float]): Callable to specify cap of the bucket + in megabytes by bucket idx. The idea of `bucket_cap_mb_by_bucket_idx` is to allow + to specify different sizes of the buckets. + filter_wait_node (Callable[[torch.fx.Node], bool] | None): If specified, + only reduce_scatter nodes with wait_node that satisfy `filter_wait_node` will be bucketed. + + Returns: + list[list[torch.fx.Node]]: List of buckets, where each bucket is a list of reduce_scatter nodes. + """ + mode = mode or _default_bucket_mode() + + assert mode is None or "multidtype" not in mode, ( + "reduce scatter bucketing does not support multidtype" + ) + + return greedy_bucket_collective_by_mb( + gm, + bucket_cap_mb_by_bucket_idx, + is_reduce_scatter_tensor, + _rs_group_key, + filter_wait_node, + ) + + +def bucket_all_reduce_by_mb( + gm: torch.fx.GraphModule, + bucket_cap_mb_by_bucket_idx: Callable[[int], float], + filter_wait_node: Callable[[torch.fx.Node], bool] | None = None, +) -> list[list[torch.fx.Node]]: + return greedy_bucket_collective_by_mb( + gm, + bucket_cap_mb_by_bucket_idx, + is_all_reduce_tensor, + _ar_group_key, + filter_wait_node, + ) + + +def bucket_all_reduce( + gm: torch.fx.GraphModule, + bucket_cap_mb_by_bucket_idx: Callable[[int], float] | None = None, + mode: str | None = None, +) -> None: + if bucket_cap_mb_by_bucket_idx is None: + from torch._inductor.fx_passes.bucketing import ( + bucket_cap_mb_by_bucket_idx_default, # pyrefly: ignore [missing-module-attribute] + ) + + bucket_cap_mb_by_bucket_idx = bucket_cap_mb_by_bucket_idx_default + ar_buckets = bucket_all_reduce_by_mb(gm, bucket_cap_mb_by_bucket_idx) + if len(ar_buckets) == 0: + return + for bucket in ar_buckets: + merge_all_reduce_bucket(gm.graph, bucket, mode) + + +@torch.library.custom_op("bucketing::_pre_bucket_reduce_scatter", mutates_args={}) +def _pre_bucket_reduce_scatter( + rs_ins: list[torch.Tensor], + group_size: int, +) -> torch.Tensor: + rs_ins_flattened = [x.view(group_size, -1) for x in rs_ins] + new_rs_in = torch.cat(rs_ins_flattened, dim=1).flatten() + return new_rs_in + + +def _pre_bucket_reduce_scatter_fake( + rs_ins: list[torch.Tensor], + group_size: int, +) -> torch.Tensor: + out_numel = sum(rs_in.numel() for rs_in in rs_ins) + return torch.empty((out_numel,), device=rs_ins[0].device, dtype=rs_ins[0].dtype) + + +_pre_bucket_reduce_scatter.register_fake(_pre_bucket_reduce_scatter_fake) + + +def reduce_scatter_merge_fn_to_trace_custom_ops( + rs_ins: list[torch.Tensor], + group_size: int, + group_name: str, + reduce_op: str, + reduce_dtype: torch.dtype, # type: ignore[name-defined] + device: torch.device, # type: ignore[name-defined] +) -> list[torch.Tensor]: # type: ignore[no-untyped-def] + new_out_sizes = [(x.shape[0] // group_size,) + x.shape[1:] for x in rs_ins] + new_out_numels = [x.numel() // group_size for x in rs_ins] + + new_rs_in = torch.ops.bucketing._pre_bucket_reduce_scatter(rs_ins, group_size) + + # TODO - either use torch.cat or make sure inductor foreach codegen + # fires more reliably + new_rs_out = torch.ops.c10d_functional.wait_tensor( + torch.ops._c10d_functional.reduce_scatter_tensor.default( + new_rs_in, reduce_op, group_size, group_name + ) + ) + new_out_flat = new_rs_out.split(new_out_numels, 0) + new_outs = [x.view(s) for x, s in zip(new_out_flat, new_out_sizes)] + return new_outs + + +def reduce_scatter_merge_fn_to_trace( + rs_ins: list[torch.Tensor], + group_size: int, + group_name: str, + reduce_op: str, + reduce_dtype: torch.dtype, # type: ignore[name-defined] + device: torch.device, # type: ignore[name-defined] +) -> list[torch.Tensor]: # type: ignore[no-untyped-def] + rs_ins_flattened = [x.view(group_size, -1) for x in rs_ins] + + new_out_sizes = [(x.shape[0] // group_size,) + x.shape[1:] for x in rs_ins] + new_out_numels = [x.numel() // group_size for x in rs_ins] + + new_rs_in = torch.cat(rs_ins_flattened, dim=1).flatten() + + new_rs_out = torch.ops.c10d_functional.wait_tensor( + torch.ops._c10d_functional.reduce_scatter_tensor.default( + new_rs_in, reduce_op, group_size, group_name + ) + ) + new_out_flat = new_rs_out.split(new_out_numels, 0) + new_outs = [x.view(s) for x, s in zip(new_out_flat, new_out_sizes)] + return new_outs + + +def reduce_scatter_merge_fn_coalesced( + rs_ins: list[torch.Tensor], + group_size: int, + group_name: str, + reduce_op: str, + reduce_dtype: torch.dtype, + device: torch.device, +) -> list[torch.Tensor]: + """Bucketed RS via NCCL's coalesced API (ncclGroupStart/End). + + Avoids cat-ing inputs into one buffer; instead passes the tensor list + directly to reduce_scatter_tensor_coalesced for zero-copy batching. + """ + rs_ins_flat = [x.view(-1) for x in rs_ins] + new_out_sizes = [(x.shape[0] // group_size,) + x.shape[1:] for x in rs_ins] + + rs_outs = torch.ops._c10d_functional.reduce_scatter_tensor_coalesced( + rs_ins_flat, reduce_op, group_size, group_name + ) + rs_outs = [torch.ops.c10d_functional.wait_tensor(o) for o in rs_outs] + return [o.view(s) for o, s in zip(rs_outs, new_out_sizes)] + + +def all_reduce_merge_fn_to_trace( + ar_ins: list[torch.Tensor], + group_name: str, + reduce_op: str, + reduce_dtype: torch.dtype, # type: ignore[name-defined] + device: torch.device, # type: ignore[name-defined] +) -> list[torch.Tensor]: # type: ignore[no-untyped-def] + ar_ins_flattened = [x.view(-1) for x in ar_ins] + new_ar_in = torch.cat(ar_ins_flattened) + new_ar_out = torch.ops.c10d_functional.wait_tensor( + torch.ops._c10d_functional.all_reduce.default(new_ar_in, reduce_op, group_name) + ) + split_sizes = [x.numel() for x in ar_ins] + new_outs_flat = new_ar_out.split(split_sizes) + new_outs = [x.view(ar_in.shape) for x, ar_in in zip(new_outs_flat, ar_ins)] + return new_outs + + +# List of all torch dtypes for serialization through custom ops +# TODO: custom ops support list[dtype] input +_ALL_DTYPES = tuple( + [ + getattr(torch, attr) + for attr in dir(torch) + if isinstance(getattr(torch, attr), torch.dtype) + ] +) + + +@torch.library.custom_op("bucketing::_pre_bucket_all_gather", mutates_args={}) +def _pre_bucket_all_gather( + ag_ins: list[torch.Tensor], + group_size: int, + group_name: str, + dtype: torch.dtype, # type: ignore[name-defined] + out_dtype_ints: list[ + int + ], # dtype enum values, that inputs are converted to before all_gather + rank: int, + foreach_group_indices: list[int] | None = None, +) -> torch.Tensor: + """ + Pre-bucket all gather operation. + + Args: + ag_ins: Input tensors to gather + group_size: Size of the process group + group_name: Name of the process group + dtype: Target dtype for the bucket + out_dtype_ints: Dtype enum values for each input + rank: Current rank + foreach_group_indices: Optional flat list of grouped indices with -1 as delimiter. + E.g., [0, 2, -1, 1] means groups [[0, 2], [1]]. + """ + # Convert int indices back to torch.dtype + out_dtypes = [_ALL_DTYPES[d] for d in out_dtype_ints] + ins_split_sizes_bytes = [ + ag_in.numel() * out_dtype.itemsize + for ag_in, out_dtype in zip(ag_ins, out_dtypes, strict=True) + ] + bucket_dtype_size_bytes = dtype.itemsize + ins_split_sizes = [ + _bytes // bucket_dtype_size_bytes for _bytes in ins_split_sizes_bytes + ] + ag_input_numel = sum(ins_split_sizes) + device = ag_ins[0].device + new_ag_out = torch.empty(ag_input_numel * group_size, dtype=dtype, device=device) + new_ag_in = new_ag_out.narrow(0, ag_input_numel * rank, ag_input_numel) + foreach_copy_dsts = torch.split(new_ag_in, ins_split_sizes) + # View each destination slice as its output dtype, then copy + # The copy operation handles dtype conversion from input dtype to output dtype + foreach_copy_dsts_typed = [ + dst.view(out_dtype) + for dst, out_dtype in zip(foreach_copy_dsts, out_dtypes, strict=True) + ] + ag_ins_flattened = [ag_in.reshape(-1) for ag_in in ag_ins] + + # Parse pre-computed groups from flat list with -1 delimiters + if foreach_group_indices is not None: + groups_list: list[list[int]] = [] + current_group: list[int] = [] + for idx in foreach_group_indices: + if idx == -1: + if current_group: + groups_list.append(current_group) + current_group = [] + else: + current_group.append(idx) + # Add last group if not empty + if current_group: + groups_list.append(current_group) + + # Call foreach_copy_ per group + for group_indices in groups_list: + group_dsts = [foreach_copy_dsts_typed[idx] for idx in group_indices] + group_srcs = [ag_ins_flattened[idx] for idx in group_indices] + torch._foreach_copy_(group_dsts, group_srcs) + else: + # No grouping provided - single foreach_copy_ call + torch._foreach_copy_(foreach_copy_dsts_typed, ag_ins_flattened) + return new_ag_out + + +def _pre_bucket_all_gather_fake( + ag_ins: list[torch.Tensor], + group_size: int, + group_name: str, + dtype: torch.dtype, # type: ignore[name-defined] + out_dtype_ints: list[int], + rank: int, + foreach_group_indices: list[int] | None = None, +) -> torch.Tensor: + out_dtypes = [_ALL_DTYPES[d] for d in out_dtype_ints] + ins_split_sizes_bytes = [ + ag_in.numel() * out_dtype.itemsize + for ag_in, out_dtype in zip(ag_ins, out_dtypes, strict=True) + ] + bucket_dtype_size_bytes = dtype.itemsize + ins_split_sizes = [ + _bytes // bucket_dtype_size_bytes for _bytes in ins_split_sizes_bytes + ] + ag_input_numel = sum(ins_split_sizes) + device = ag_ins[0].device + new_ag_out = torch.empty(ag_input_numel * group_size, dtype=dtype, device=device) + return new_ag_out + + +_pre_bucket_all_gather.register_fake(_pre_bucket_all_gather_fake) + + +def all_gather_merge_fn_to_trace_custom_ops( + _ag_ins: list[torch.Tensor], + group_size: int, + group_name: str, + dtype: torch.dtype, # type: ignore[name-defined] + out_dtypes: list[torch.dtype], # type: ignore[name-defined] + rank: int, +) -> list[torch.Tensor]: + # Don't create convert_element_type ops - _pre_bucket_all_gather handles conversion + # by viewing destination slices as output dtypes and letting copy do the conversion + ag_ins = _ag_ins + ins_sizes = [ag_in.shape for ag_in in ag_ins] + ins_split_sizes_bytes = [ + ag_in.numel() * out_dtype.itemsize + for ag_in, out_dtype in zip(ag_ins, out_dtypes) + ] + bucket_dtype_size_bytes = dtype.itemsize + ins_split_sizes = [ + _bytes // bucket_dtype_size_bytes for _bytes in ins_split_sizes_bytes + ] + ag_input_numel = sum(ins_split_sizes) + + # Convert out_dtypes to indices for custom_op + # TODO: custom ops support list[dtype] input + out_dtype_ints = [_ALL_DTYPES.index(dt) for dt in out_dtypes] + + # Pre-compute foreach groups for better foreach_copy_ performance + foreach_group_indices = _compute_foreach_groups(ag_ins, out_dtypes) + + new_ag_out = torch.ops.bucketing._pre_bucket_all_gather( + ag_ins, + group_size, + group_name, + dtype, + out_dtype_ints, + rank, + foreach_group_indices, + ) + new_ag_in = new_ag_out.narrow(0, ag_input_numel * rank, ag_input_numel) + wait_tensor = torch.ops.c10d_functional.wait_tensor( + torch.ops._c10d_functional.all_gather_into_tensor_out.default( + new_ag_in, group_size, group_name, out=new_ag_out + ) + ) + new_ag_out_reshaped = wait_tensor.reshape(group_size, -1) + outs_bucket_dtype = torch.split_with_sizes( + new_ag_out_reshaped, + ins_split_sizes, + dim=1, + ) + outs_reshaped = [ + o.view(out_dtype).reshape((shape[0] * group_size,) + shape[1:]) + for o, shape, out_dtype in zip(outs_bucket_dtype, ins_sizes, out_dtypes) + ] + return outs_reshaped + + +def all_gather_merge_fn_to_trace( + ag_ins: list[torch.Tensor], + group_size: int, + group_name: str, + dtype: torch.dtype, # type: ignore[name-defined] + out_dtypes: list[torch.dtype], # type: ignore[name-defined] + rank: int, +) -> list[torch.Tensor]: + ins_sizes = [ag_in.shape for ag_in in ag_ins] + ins_split_sizes = [ag_in.numel() for ag_in in ag_ins] + ag_input_numel = sum(ins_split_sizes) + device = ag_ins[0].device + new_ag_out = torch.empty(ag_input_numel * group_size, dtype=dtype, device=device) + new_ag_in = new_ag_out.narrow(0, ag_input_numel * rank, ag_input_numel) + ag_ins_flattened = [ag_in.reshape(-1) for ag_in in ag_ins] + # Inductor fuses copy_(cat(...)) into 1 Triton kernel with no allocation for cat. + # _foreach_copy_(..., ag_ins_flattened) emits separate kernel per item, + # resulting in large number of small triton kernels to launch. + new_ag_in.copy_(torch.cat(ag_ins_flattened)) + wait_tensor = torch.ops.c10d_functional.wait_tensor( + torch.ops._c10d_functional.all_gather_into_tensor_out.default( + new_ag_in, group_size, group_name, out=new_ag_out + ) + ) + new_ag_out_reshaped = wait_tensor.reshape(group_size, -1) + outs = torch.split_with_sizes( + new_ag_out_reshaped, + ins_split_sizes, + dim=1, + ) + outs_reshaped = [ + o.reshape((shape[0] * group_size,) + shape[1:]) + for o, shape in zip(outs, ins_sizes) + ] + return outs_reshaped + + +def all_gather_merge_fn_to_trace_functional( + ag_ins: list[torch.Tensor], + group_size: int, + group_name: str, + dtype: torch.dtype, # type: ignore[name-defined] + out_dtypes: list[torch.dtype], # type: ignore[name-defined] + rank: int, + use_fsdp_ag_copy_in: bool = False, +) -> list[torch.Tensor]: + # Implementation that is functional in graph, + # but uses custom op torch.ops.fsdp.all_gather_copy_in. + ins_sizes = [ag_in.shape for ag_in in ag_ins] + ins_split_sizes = [ag_in.numel() for ag_in in ag_ins] + ag_input_numel = sum(ins_split_sizes) + device = ag_ins[0].device + new_ag_out = torch.empty(ag_input_numel * group_size, dtype=dtype, device=device) + ag_ins_flattened = [ag_in.reshape(-1) for ag_in in ag_ins] + if use_fsdp_ag_copy_in: + new_ag_in, new_ag_out = torch.ops.fsdp.all_gather_copy_in( + ag_ins_flattened, new_ag_out, ins_split_sizes, ag_input_numel, rank + ) + else: + new_ag_in = torch.cat(ag_ins_flattened, dim=0) + wait_tensor = torch.ops.c10d_functional.wait_tensor( + torch.ops._c10d_functional.all_gather_into_tensor_out.default( + new_ag_in, group_size, group_name, out=new_ag_out + ) + ) + new_ag_out_reshaped = wait_tensor.reshape(group_size, -1) + outs = torch.split_with_sizes( + new_ag_out_reshaped, + ins_split_sizes, + dim=1, + ) + outs_reshaped = [ + o.reshape((shape[0] * group_size,) + shape[1:]) + for o, shape in zip(outs, ins_sizes) + ] + return outs_reshaped + + +def _trace(fn, inps) -> torch.fx.GraphModule: # type: ignore[no-untyped-def] + with dynamo_timed("fx.bucketing._trace", log_pt2_compile_event=True): + fake_mode = detect_fake_mode(inps) + assert fake_mode is not None + with fake_mode, enable_python_dispatcher(): + out = make_fx(fn)(*inps) + for node in out.graph.find_nodes( + op="call_function", target=torch.ops.aten.detach.default + ): + node.replace_all_uses_with(node.args[0]) + out.graph.erase_node(node) + return out + + +def _insert_fn_trace_before_node( # type: ignore[no-untyped-def] + g: torch.fx.Graph, + fn_to_trace, + inps, + insert_before_node: torch.fx.Node, + g_fn_inps: list[torch.fx.Node], + g_fn_outs: list[torch.fx.Node], +) -> tuple[dict[torch.fx.Node, torch.fx.Node], list[torch.fx.Node]]: # type: ignore[no-untyped-def] + """ + Helper function that traces :attr:`fn_to_trace` with inputs + :attr:`inps`. + The result function graph will be inserted before :attr:`insert_before_node`, + using :attr:`g_fn_inps` nodes of original graph as inputs of function graph, + function graph outputs will replace :attr:`g_fn_outs` in original graph. + + Returns: + (replacements, new_nodes): Dictionary mapping old to new nodes, and list of all newly inserted nodes + """ + with dynamo_timed( + "fx.bucketing._insert_fn_trace_before_node", log_pt2_compile_event=True + ): + fn_gm = _trace( + fn_to_trace, + inps, + ) + fn_g = fn_gm.graph + fn_g_ins = fn_g.find_nodes(op="placeholder") + env = {fn_g_ins[idx]: g_fn_inps[idx] for idx in range(len(g_fn_inps))} + g_fn_new_outs: list[torch.fx.Node] = [] + new_nodes: list[torch.fx.Node] = [] # Track all newly inserted nodes + + with g.inserting_before(insert_before_node): + for _n in fn_g.nodes: + if _n.op == "placeholder": + continue + _new_n = g.node_copy(_n, lambda x: env[x]) + env[_n] = _new_n + if _n.op == "output": + g_fn_new_outs = _new_n.args[0] # type: ignore[assignment] + g.erase_node(_new_n) + else: + new_nodes.append(_new_n) # Track non-output nodes + + replacements = { # noqa: C416 + orig_out: new_out for orig_out, new_out in zip(g_fn_outs, g_fn_new_outs) + } + for orig_out, new_out in zip(g_fn_outs, g_fn_new_outs): + orig_out.replace_all_uses_with(new_out) + + return replacements, new_nodes + + +def has_mergeable_all_gather_convert_dtype(n: torch.fx.Node) -> bool: + node_in = n.args[0] + return ( + is_all_gather_into_tensor(n) + and isinstance(node_in, torch.fx.Node) + and node_in.op == "call_function" + and ( + node_in.target is torch.ops.prims.convert_element_type.default + or node_in.target is torch.ops.aten._to_copy.default + ) + and len(node_in.users) == 1 + ) + + +def process_collective_bucket( + g: torch.fx.Graph, + bucket_nodes: list[torch.fx.Node], + fn_to_trace: Callable[..., list[torch.Tensor]], + trace_args_fn: Callable[[list[torch.fx.Node]], tuple[Any, ...]], + insert_before: torch.fx.Node | None = None, + wait_insertion_point: torch.fx.Node | None = None, +) -> tuple[list[torch.fx.Node], dict[torch.fx.Node, torch.fx.Node]]: + """ + Process a single bucket of collective operation nodes with flexible insertion control. + + Args: + g: The graph to modify + bucket_nodes: Nodes in the current bucket to process + fn_to_trace: Function to trace and insert + trace_args_fn: Function to create trace arguments from inputs + insert_before: Where to insert the traced function (default: after last bucket node) + wait_insertion_point: If provided, move all nodes from wait() onwards to before this node + + Returns: + new_nodes: List of all newly inserted nodes + replacements: Dictionary mapping old wait nodes to new output nodes + """ + # Collect inputs and waits from current bucket + bucket_ins: list[torch.fx.Node] = [] + bucket_waits: list[torch.fx.Node] = [] + ag_node_to_pre_nodes: dict[torch.fx.Node, list[torch.fx.Node]] = defaultdict(list) + + for n in bucket_nodes: + assert len(n.users) == 1, f"Expected single user for {n}, got {n.users}" + wait_n = next(iter(n.users)) + + # Handle convert_element_type operations (for all_gather) + node_in = n.args[0] + if has_mergeable_all_gather_convert_dtype(n): + # pyrefly: ignore [bad-argument-type] + ag_node_to_pre_nodes[n].append(node_in) + # pyrefly: ignore [missing-attribute] + node_in = node_in.args[0] + + assert isinstance(node_in, torch.fx.Node) # Ensure node_in is a Node + bucket_ins.append(node_in) + bucket_waits.append(wait_n) + + # Create trace arguments + trace_args = trace_args_fn(bucket_ins) + + # Determine insertion point + if insert_before is None: + insert_before = bucket_nodes[-1].next + + # Insert traced function and get replacements + new nodes + replacements, new_nodes = _insert_fn_trace_before_node( + g, + fn_to_trace, + trace_args, + insert_before, + bucket_ins, + bucket_waits, + ) + + # If requested, move wait nodes and everything after to specified location + if wait_insertion_point is not None: + # Find the first wait node in new_nodes + wait_start_idx = None + for i, node in enumerate(new_nodes): + if is_wait_tensor(node): + wait_start_idx = i + break + + # Move all nodes from wait onwards (including the wait) + if wait_start_idx is not None: + nodes_to_move = new_nodes[wait_start_idx:] + for node in nodes_to_move: + wait_insertion_point.prepend(node) + + # Preserve metadata from original collective nodes to new bucketed nodes + if bucket_nodes: + overlap_log.debug( + "Bucketing nodes: %s, New nodes: %s", + ",".join([n.name for n in bucket_nodes]), + ",".join([n.name for n in new_nodes]), + ) + _populate_node_meta(bucket_nodes, new_nodes) + + # Erase old nodes + for node, wait_n in zip(bucket_nodes, bucket_waits): + g.erase_node(wait_n) + g.erase_node(node) + # Erase any convert_element_type nodes we tracked + for pre_node in reversed(ag_node_to_pre_nodes[node]): + g.erase_node(pre_node) + + return new_nodes, replacements + + +def merge_reduce_scatter_bucket( + g: torch.fx.Graph, + rs_nodes: list[torch.fx.Node], + mode: BucketMode | None = None, + insert_before: torch.fx.Node | None = None, + wait_insertion_point: torch.fx.Node | None = None, +) -> tuple[list[torch.fx.Node], dict[torch.fx.Node, torch.fx.Node]]: + mode = mode or _default_bucket_mode() + # Validate bucket consistency + rs0 = rs_nodes[0] + rs0_val = rs0.meta["val"] + _, reduce_op, group_size, group_name = rs0.args + reduce_dtype = rs0_val.dtype + device = rs0_val.device + + for n in rs_nodes: + rs_val = n.meta["val"] + assert ( + n.args[1] == reduce_op + and n.args[2] == group_size + and n.args[3] == group_name + and rs_val.device == device + and rs_val.dtype == reduce_dtype + ) + + # Choose merge function based on mode + rs_merge_fn = reduce_scatter_merge_fn_to_trace + if mode == "coalesced": + rs_merge_fn = reduce_scatter_merge_fn_coalesced + elif mode and "custom_ops" in mode: + rs_merge_fn = reduce_scatter_merge_fn_to_trace_custom_ops + + # Process bucket with lazy input collection + def create_trace_args(bucket_ins: list[torch.fx.Node]) -> tuple[Any, ...]: + return ( + pytree.tree_map(lambda node: node.meta["val"], bucket_ins), + group_size, + group_name, + reduce_op, + reduce_dtype, + device, + ) + + return process_collective_bucket( + g, + rs_nodes, + rs_merge_fn, + create_trace_args, + insert_before=insert_before, + wait_insertion_point=wait_insertion_point, + ) + + +def merge_all_reduce_bucket( + g: torch.fx.Graph, + ar_nodes: list[torch.fx.Node], + mode: str | None = None, + insert_before: torch.fx.Node | None = None, + wait_insertion_point: torch.fx.Node | None = None, +) -> tuple[list[torch.fx.Node], dict[torch.fx.Node, torch.fx.Node]]: + ar0 = ar_nodes[0] + ar0_val = ar0.meta["val"] + _, reduce_op, group_name = ar0.args + reduce_dtype = ar0_val.dtype + device = ar0_val.device + + for n in ar_nodes: + ar_val = n.meta["val"] + assert ( + n.args[1] == reduce_op + and n.args[2] == group_name + and ar_val.device == device + and ar_val.dtype == reduce_dtype + ) + + ar_merge_fn = all_reduce_merge_fn_to_trace + + def create_trace_args(bucket_ins: list[torch.fx.Node]) -> tuple[Any, ...]: + return ( + pytree.tree_map(lambda node: node.meta["val"], bucket_ins), + group_name, + reduce_op, + reduce_dtype, + device, + ) + + return process_collective_bucket( + g, + ar_nodes, + ar_merge_fn, + create_trace_args, + insert_before=insert_before, + wait_insertion_point=wait_insertion_point, + ) + + +def merge_all_gather_bucket( + g: torch.fx.Graph, + ag_nodes: list[torch.fx.Node], + mode: BucketMode | None = None, + insert_before: torch.fx.Node | None = None, + wait_insertion_point: torch.fx.Node | None = None, +) -> tuple[list[torch.fx.Node], dict[torch.fx.Node, torch.fx.Node]]: + mode = mode or _default_bucket_mode() + from torch.distributed.distributed_c10d import _resolve_process_group + + ag0 = ag_nodes[0] + _, group_size, group_name = ag0.args + assert isinstance(group_name, str) + _ag_dtypes: list[torch.dtype] = [] # type: ignore[name-defined] + + for n in ag_nodes: + assert n.args[1] == group_size and n.args[2] == group_name + _ag_dtypes.append(n.meta["val"].dtype) + + bucket_dtype = pick_bucket_dtype(_ag_dtypes) + + # Choose merge function based on mode + ag_merge_fn = all_gather_merge_fn_to_trace + if mode == "coalesced": + logger.info("coalesced bucket_mode not supported for all_gather, using default") + elif mode and "custom_ops" in mode: + ag_merge_fn = all_gather_merge_fn_to_trace_custom_ops # type: ignore[assignment] + + # Process bucket with lazy input collection + # pyrefly: ignore [bad-argument-type] + rank: int = dist.get_rank(_resolve_process_group(group_name)) + + def create_trace_args(bucket_ins: list[torch.fx.Node]) -> tuple[Any, ...]: + return ( + pytree.tree_map(lambda node: node.meta["val"], bucket_ins), + group_size, + group_name, + bucket_dtype, + _ag_dtypes, + rank, + ) + + return process_collective_bucket( + g, + ag_nodes, + ag_merge_fn, + create_trace_args, + wait_insertion_point=wait_insertion_point, + ) + + +def merge_reduce_scatter( + gm: torch.fx.GraphModule, + rs_buckets: list[list[torch.fx.Node]], + mode: BucketMode | None = None, +) -> None: + """ + Merges specified buckets of reduce_scatter to joint reduce_scatter. + """ + mode = mode or _default_bucket_mode() + with dynamo_timed("fx.bucketing.merge_reduce_scatter", log_pt2_compile_event=True): + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "fx_bucketing_passes_reduce_scatter_buckets", + "encoding": "string", + }, + payload_fn=lambda: str(rs_buckets), + ) + + g = gm.graph + + for rs_nodes in rs_buckets: + merge_reduce_scatter_bucket(g, rs_nodes, mode) + + +def merge_all_gather( + gm: torch.fx.GraphModule, + ag_buckets: list[list[torch.fx.Node]], + mode: BucketMode | None = None, +) -> None: + """ + Merges specified buckets of all_gather to joint all_gather. + """ + mode = mode or _default_bucket_mode() + with dynamo_timed("fx.bucketing.merge_all_gather", log_pt2_compile_event=True): + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "fx_bucketing_passes_all_gather_buckets", + "encoding": "string", + }, + payload_fn=lambda: str(ag_buckets), + ) + + g = gm.graph + + for ag_nodes in ag_buckets: + merge_all_gather_bucket(g, ag_nodes, mode) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/control_dependencies.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/control_dependencies.py new file mode 100644 index 0000000000000000000000000000000000000000..d50a034665b2b4f2c8a806601564555242f7e970 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/control_dependencies.py @@ -0,0 +1,280 @@ +# mypy: allow-untyped-defs +""" +Effect ordering pass for inductor. + +This pass adds ordering dependencies to FX graphs using the control_deps HOP +for precise control over scheduling constraints. When you need exact ordering between +operations (e.g., collective_start -> mm -> wait), this pass wraps operations +with control_deps to make dependencies explicit. +""" + +from typing import Any + +import torch.fx as fx +import torch.utils._pytree as pytree +from torch._C import DispatchKey +from torch._higher_order_ops.utils import register_fake +from torch._ops import HigherOrderOperator +from torch.utils._ordered_set import OrderedSet + + +class ControlDeps(HigherOrderOperator): + """ + Higher-order operator that enforces ordering by making dependencies explicit. + + Schema: control_deps(additional_deps, target, *args, **kwargs) -> result + where: + - additional_deps: tuple of tensors that must be computed before this op + - subgraph: GraphModule containing the exact operation to execute + - args/kwargs: arguments for the target function + + This ensures all tensors in additional_deps are computed before the target + executes, creating explicit scheduling dependencies. + """ + + def __init__(self) -> None: + super().__init__("control_deps") + + def __call__(self, additional_deps, subgraph, *args, **kwargs): + """Call the operator with dependencies and subgraph. + + Args: + additional_deps: Tuple of tensors that must be computed first + subgraph: GraphModule containing the exact operation to execute + *args: Arguments to pass to the subgraph + """ + if not isinstance(additional_deps, (tuple, list)): + raise TypeError( + f"additional_deps must be tuple/list, got {type(additional_deps).__name__}" + ) + if not (isinstance(subgraph, fx.GraphModule) or callable(subgraph)): + raise TypeError( + f"subgraph must be GraphModule or callable, got {type(subgraph).__name__}" + ) + # pyrefly: ignore [missing-attribute] + return super().__call__(additional_deps, subgraph, *args, **kwargs) + + +control_deps = ControlDeps() + +# control_deps wraps side-effecting ops (e.g. record_event, wait_event) +# and must not be eliminated by DCE even when its outputs are unused. +from torch.fx.node import has_side_effect + + +has_side_effect(control_deps) + + +# Register fake implementation for tracing +@register_fake(control_deps) +def _(additional_deps, subgraph, *args, **kwargs): + """Fake tensor implementation - execute the subgraph.""" + return subgraph(*args, **kwargs) + + +# Register eager execution implementation +@control_deps.py_impl(DispatchKey.CompositeExplicitAutograd) +def control_deps_eager(additional_deps, subgraph, *args, **kwargs): + """Eager implementation - just execute the subgraph.""" + return subgraph(*args, **kwargs) + + +# Autograd impl needed because additional_deps tensors may have autograd state, +# causing dispatch through AutogradCUDA even in post-autograd graphs. +@control_deps.py_impl(DispatchKey.Autograd) +def control_deps_autograd(additional_deps, subgraph, *args, **kwargs): + return subgraph(*args, **kwargs) + + +def get_subgraph_name(gm: fx.GraphModule, name): + name = f"subgraph_{name}" + + if not hasattr(gm, name): + return name + + i = 0 + while hasattr(gm, f"{name}_{i}"): + i += 1 + + return f"{name}_{i}" + + +def _extract_unique_nodes( + args: tuple[Any, ...], kwargs: dict[str, Any] +) -> tuple[list[fx.Node], list[Any], Any]: + """Extract unique fx.Node instances from args/kwargs using pytree. + + Args: + args: The positional arguments (may contain nested structures with fx.Node) + kwargs: The keyword arguments (may contain nested structures with fx.Node) + + Returns: + - Ordered list of unique fx.Node instances (preserves first occurrence order) + - Flattened list of all items from args/kwargs + - The pytree spec for reconstructing the original structure + """ + flat_args_kwargs, spec = pytree.tree_flatten((args, kwargs)) + unique_nodes: list[fx.Node] = [] + seen: OrderedSet[fx.Node] = OrderedSet() + for item in flat_args_kwargs: + if isinstance(item, fx.Node) and item not in seen: + unique_nodes.append(item) + seen.add(item) + return unique_nodes, flat_args_kwargs, spec + + +def preserve_node_ordering( + graph: fx.Graph, + additional_deps_map: dict[fx.Node, OrderedSet[fx.Node]], + verbose: bool = False, +) -> None: + """ + Preserve node ordering using control_deps HOP with subgraph. + + This function wraps operations with control_deps that: + 1. Makes additional dependencies explicit (first argument) + 2. Creates a subgraph internally to preserve the exact original operation + 3. Preserves the original node names + + Args: + graph: The FX graph to modify + additional_deps_map: Mapping from dependent nodes to their dependencies + verbose: If True, print debug information + """ + if not additional_deps_map: + return + + # Track replacements so we can update dependencies + replacements: dict[fx.Node, fx.Node] = {} + + # Process each node that needs additional dependencies + for dependent_node, dep_nodes in additional_deps_map.items(): + assert dependent_node.op == "call_function", dependent_node.op + + original_name = dependent_node.name + original_args = dependent_node.args + original_kwargs = dependent_node.kwargs + original_meta = dependent_node.meta.copy() + + updated_dep_nodes = [replacements.get(dep, dep) for dep in dep_nodes] + + # Create a subgraph that preserves the exact original operation + subgraph_module = _create_subgraph_for_node(graph, dependent_node) + + owning_mod = graph.owning_module + assert owning_mod is not None + subgraph_attr_name = get_subgraph_name(owning_mod, original_name) + setattr(graph.owning_module, subgraph_attr_name, subgraph_module) + + # Create control_deps call with: + # 1. Additional dependencies as first arg (explicit) + # 2. Subgraph via get_attr (like b2b gemm pass) + # 3. Original arguments (only fx.Node args and kwargs are passed) + with graph.inserting_before(dependent_node): + # Create get_attr node for the subgraph + get_subgraph = graph.get_attr(subgraph_attr_name) + + # Extract unique nodes from nested args/kwargs + node_args, _, _ = _extract_unique_nodes(original_args, original_kwargs) + + # Create with temporary name first + ordered_node = graph.call_function( + control_deps, + args=( + tuple(updated_dep_nodes), # additional_deps + get_subgraph, # subgraph via get_attr (like b2b gemm) + *node_args, # original node arguments (from both args and kwargs) + ), + kwargs={}, + name=f"__temp_{original_name}", # Temporary name to avoid conflict + ) + + # Copy metadata from original node + ordered_node.meta = original_meta + # this will be constrained on the target node in subgraph if it exists + ordered_node.meta.pop("eager_input_vals", None) + + # Replace all uses of the original node with the ordered version + dependent_node.replace_all_uses_with(ordered_node) + + # Remove the original node from the graph + graph.erase_node(dependent_node) + + # Now rename the ordered node to the original name + ordered_node.name = original_name # PRESERVE ORIGINAL NAME + + # Track the replacement for future dependencies + replacements[dependent_node] = ordered_node + + +def _create_subgraph_for_node( + graph: fx.Graph, node: fx.Node, additional_deps=None +) -> fx.GraphModule: + """ + Create a subgraph that exactly recreates a node's operation optionally passing through additional dependencies. + + The subgraph takes only the fx.Node arguments and recreates the operation + with the exact target, args structure, and kwargs. + + Args: + graph: The parent graph + node: The node to wrap in a subgraph + additional_deps: Additional dependencies to pass through the subgraph + + Returns: + A GraphModule containing the subgraph + """ + # Get the owning module + owning_module = graph.owning_module + + # Create a new graph for the subgraph + subgraph = fx.Graph(owning_module) + + # Extract unique nodes and get flattened structure + spec + unique_nodes, flat_args_kwargs, spec = _extract_unique_nodes(node.args, node.kwargs) + + # Create placeholders for each unique node + node_to_placeholder: dict[fx.Node, fx.Node] = {} + for idx, orig_node in enumerate(unique_nodes): + placeholder = subgraph.placeholder(f"arg_{idx}") + if "val" in orig_node.meta: + placeholder.meta.update(orig_node.meta) + node_to_placeholder[orig_node] = placeholder + + # Replace fx.Node instances with their placeholders + def replace_nodes(item: Any) -> Any: + if isinstance(item, fx.Node): + return node_to_placeholder[item] + return item + + additional_deps_placeholders = [] + for idx, dep in enumerate(additional_deps or ()): + placeholder = subgraph.placeholder(f"dep_{idx}") + if "val" in dep.meta: + placeholder.meta.update(dep.meta) + additional_deps_placeholders.append(placeholder) + + new_flat = [replace_nodes(item) for item in flat_args_kwargs] + new_args, new_kwargs = pytree.tree_unflatten(new_flat, spec) + + # Recreate the exact original operation in the subgraph + assert callable(node.target) + result = subgraph.call_function( + node.target, + tuple(new_args), + new_kwargs, # type: ignore[arg-type] + ) + + # Copy metadata from the original node + result.meta.update(node.meta) + + if additional_deps_placeholders: + outputs = tuple([result] + additional_deps_placeholders) + out = subgraph.output(outputs) + out.meta["val"] = tuple(output.meta.get("val") for output in outputs) + else: + out = subgraph.output(result) + if "val" in result.meta: + out.meta["val"] = result.meta["val"] + + return fx.GraphModule(owning_module, subgraph) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/ddp_fusion.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/ddp_fusion.py new file mode 100644 index 0000000000000000000000000000000000000000..21fe1d6ecf0d93c9922808de834e77ee6369a92c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/ddp_fusion.py @@ -0,0 +1,589 @@ +# Owner(s): ["oncall: distributed"] +import collections +import inspect +import logging +import math +import operator +from collections.abc import Callable, Generator +from dataclasses import dataclass +from functools import partial +from typing import Any, cast + +import torch +import torch.fx as fx +from torch._dynamo.utils import counters +from torch.fx.passes.graph_transform_observer import GraphTransformObserver +from torch.fx.passes.shape_prop import _extract_tensor_metadata, TensorMetadata +from torch.utils._ordered_set import OrderedSet +from torch.utils._pytree import tree_flatten, tree_map, tree_unflatten + +from ..fx_utils import get_fake_args_kwargs +from ..virtualized import V + + +aten = torch.ops.aten +logger: logging.Logger = logging.getLogger("comm_fusion") + + +def move_block_after(block: list[fx.Node], target_node: fx.Node) -> None: + for node in block: + target_node.append(node) + target_node = node + + +def move_block_before(block: list[fx.Node], target_node: fx.Node) -> None: + for node in block: + target_node.prepend(node) + target_node = node + + +def call_function( + graph: fx.Graph, + target: str | Callable[..., Any], + args: tuple[fx.node.Argument, ...] | None = None, + kwargs: dict[str, fx.node.Argument] | None = None, +) -> fx.Node: + # We accept target as a str to avoid typing error as the type of + # a node.target is str | Callable[..., Any]. + # This also allows us to avoid writing check for every call. + if isinstance(target, str): + raise RuntimeError(f"Call function should not get a str target {target=}") + node = graph.call_function(target, args, kwargs) + _, args, kwargs = get_fake_args_kwargs(node) + with V.fake_mode: + node.meta["val"] = target(*args, **kwargs) + # node.meta["val"] may be a container. So we use tree_map here + # to recursively extract the tensor metadata. + node.meta["tensor_meta"] = tree_map( + _extract_tensor_metadata, (node.meta["val"],) + )[0] + return node + + +@dataclass(unsafe_hash=True) +class CommBlock: + shape: torch.Size | list[torch.Size] + node_list: list[fx.Node] + inputs: list[fx.Node] + wait_nodes: list[fx.Node] + comm_node: fx.Node + outputs: OrderedSet[fx.Node] + + +def get_comm_block(comm_node: fx.Node) -> CommBlock | None: + """ + Given a collective node (e.g., allreduce), find out all the nodes belong to + this communication. + + Args: + comm_node(fx.Node): The target communication/collective node. + Returns: + The CommBlock that encapsulates the related nodes (e.g., wait_node) of + the given comm_node. + """ + node_list = [] + wait_nodes = [] + inputs, _ = tree_flatten((comm_node.args, comm_node.kwargs)) + input_nodes = [inp for inp in inputs if isinstance(inp, fx.Node)] + # If the users of the wait node are following items, we consinder them + # to be a part of the output. + intermediate_outputs = ("split", "reshape", "getitem", "detach", "alias") + + first_user = next(iter(comm_node.users)) + if ( + len(comm_node.users) == 1 + and first_user.target is torch.ops._c10d_functional.wait_tensor.default + ): + # Collective with only one output + node_list = [comm_node, first_user] + wait_nodes.append(first_user) + elif len(comm_node.users) > 1 and first_user.target is operator.getitem: + # Collective with only more than one output + node_list.append(comm_node) + for user in comm_node.users: + if user.target != operator.getitem: + return None + if len(user.users) != 1: + return None + wait_node = next(iter(user.users)) + if wait_node.target != torch.ops._c10d_functional.wait_tensor.default: + return None + wait_nodes.append(wait_node) + node_list.append(user) + node_list.extend(wait_nodes) + else: + return None + + # Identify all the outputs of this collective block. + outputs = OrderedSet[fx.Node]() + nodes = collections.deque(wait_nodes) + while nodes: + node = nodes.popleft() + for user in node.users: + if isinstance(user, fx.Node) and user.name.startswith(intermediate_outputs): + nodes.append(user) + node_list.append(user) + else: + outputs.add(node) + break + + tensor_meta = input_nodes[0].meta["tensor_meta"] + shape: torch.Size | list[torch.Size] + if isinstance(tensor_meta, TensorMetadata): + shape = tensor_meta.shape + elif isinstance(tensor_meta, (list, tuple)): + shape = [tm.shape for tm in tensor_meta] + else: + logger.warning("Unexpected type of tensor_meta %s", type(tensor_meta)) + return None + + return CommBlock( + shape=shape, + node_list=node_list, + wait_nodes=wait_nodes, + comm_node=comm_node, + inputs=input_nodes, + outputs=outputs, + ) + + +def get_all_comm_blocks( + graph: fx.Graph, + comm_ops: tuple[torch._ops.OpOverload, ...], + comm_filter: Callable[..., bool] | None = None, +) -> list[CommBlock]: + if comm_filter is None: + + def always_true(comm_block: CommBlock) -> bool: + return True + + comm_filter = always_true + + blocks = [] + for node in graph.nodes: + if node.target not in comm_ops: + continue + comm_block = get_comm_block(node) + if comm_block is not None and comm_filter(comm_block): + blocks.append(comm_block) + return blocks + + +def _fuse_allreduce_by_concat( + graph: fx.Graph, + last_input_node: fx.Node, + all_input_nodes: list[fx.Node], + last_comm_block: CommBlock, +) -> CommBlock: + """Given a list of inputs in order, create a fused allreduce using concat.""" + # Flatten all the inputs to the all_reduce nodes. + with graph.inserting_after(last_input_node): + cat_inputs = [] + for input_node in all_input_nodes: + assert isinstance(input_node.args[0], fx.Node) + input_node = input_node.args[0] + cat_inputs.append( + call_function(graph, aten.flatten.using_ints, (input_node,)) + ) + + # Concat all the flattened nodes. + with graph.inserting_after(cat_inputs[0]): + cat_node = call_function(graph, aten.cat, (cat_inputs,)) + + # Insert the fused div node and remove the input div nodes. + # This is an optimization and is not mandatory for fusion. + divisors = [div.args[1] for div in all_input_nodes] + assert all(divisor == divisors[0] for divisor in divisors) + with graph.inserting_after(cat_node): + div_node = call_function(graph, last_input_node.target, (cat_node, divisors[0])) + + # Create a new Comm/all_reduce node. + last_comm_node = last_comm_block.comm_node + last_wait_node = last_comm_block.wait_nodes[0] + with graph.inserting_after(div_node): + flatten_args, spec = tree_flatten((last_comm_node.args, last_comm_node.kwargs)) + flatten_args[0] = div_node + args, kwargs = tree_unflatten(flatten_args, spec) + fused_comm_node = call_function(graph, last_comm_node.target, args, kwargs) + + # Create a new Wait node. + with graph.inserting_after(fused_comm_node): + flatten_args, spec = tree_flatten((last_wait_node.args, last_wait_node.kwargs)) + flatten_args[0] = fused_comm_node + args, kwargs = tree_unflatten(flatten_args, spec) + fused_wait_node = call_function(graph, last_wait_node.target, args, kwargs) + + # Move the fused all_reduce and its args to right after the input node + nodes_to_move = cat_inputs + [cat_node, div_node, fused_comm_node, fused_wait_node] + # pyrefly: ignore [bad-argument-type] + move_block_after(nodes_to_move, last_input_node) + + return CommBlock( + shape=cast(TensorMetadata, cat_node.meta.get("tensor_meta")).shape, + node_list=[fused_comm_node, fused_wait_node], + wait_nodes=[fused_wait_node], + comm_node=fused_comm_node, + inputs=[div_node], + outputs=OrderedSet([fused_wait_node]), + ) + + +def _fuse_with_coalesced_op( + graph: fx.Graph, + last_input_node: fx.Node, + all_input_nodes: list[fx.Node], + last_comm_block: CommBlock, +) -> CommBlock: + """Given a list of inputs in order, create a fused allreduce by coalesced.""" + last_comm_node = last_comm_block.comm_node + last_wait_node = last_comm_block.wait_nodes[0] + + # Insert the fused div node and remove the input div nodes. + # This is an optimization and is not mandatory for fusion. + dividends = [div.args[0] for div in all_input_nodes] + divisors = [div.args[1] for div in all_input_nodes] + assert all(divisor == divisors[0] for divisor in divisors) + with graph.inserting_before(last_input_node): + last_input_node = call_function( + graph, aten._foreach_div.Scalar, (dividends, divisors[0]) + ) + input_node = last_input_node + + # Create a new Comm/all_reduce_coalesced node. + with graph.inserting_after(last_comm_node): + flatten_args, spec = tree_flatten((last_comm_node.args, last_comm_node.kwargs)) + flatten_args[0] = input_node + args, kwargs = tree_unflatten(flatten_args, spec) + fused_comm_node = call_function( + graph, torch.ops._c10d_functional.all_reduce_coalesced.default, args, kwargs + ) + + # Create a new wait node. + getitem_nodes = [] + wait_nodes = [] + flatten_args, spec = tree_flatten((last_wait_node.args, last_wait_node.kwargs)) + for idx in range(len(all_input_nodes)): + with graph.inserting_after(fused_comm_node): + gi_node = call_function(graph, operator.getitem, (fused_comm_node, idx)) + getitem_nodes.append(gi_node) + flatten_args[0] = gi_node + args, kwargs = tree_unflatten(flatten_args, spec) + with graph.inserting_after(gi_node): + wait_nodes.append(call_function(graph, last_wait_node.target, args, kwargs)) + + # Move the new all_reduce_coalesced and its args to right after the input node + nodes_to_move = [fused_comm_node] + getitem_nodes + wait_nodes + move_block_after(nodes_to_move, last_input_node) + + return CommBlock( + shape=[ + tm.shape + for tm in cast( + list[TensorMetadata], fused_comm_node.meta.get("tensor_meta") + ) + ], + node_list=[fused_comm_node] + getitem_nodes + wait_nodes, + wait_nodes=wait_nodes, + comm_node=fused_comm_node, + inputs=[input_node], + outputs=OrderedSet(wait_nodes), + ) + + +def _scatter_fused_allreduce_waits( + graph: fx.Graph, + fused_comm_block: CommBlock, + orig_comm_blocks: list[CommBlock], + node_indices: dict[fx.Node, int], + split_and_reshape: bool = True, +) -> None: + """ + Scatters the result of the fused communication node to the original users. + If the fused method is concat splitting the output and reshape will be inserted, + before inserting getitem. Otherwise getitem will be used as the users of the + wait node. + """ + + # Before we mass up the order, we need to get the index of the last wait node + # in orig_comm_blocks. This index will be later used to determine what users + # nodes need to be move to maintain a correct topological sort order. + last_wait_node_idx = 0 + + for node in graph.nodes: + last_wait_node_idx = max( + node_indices.get(node, last_wait_node_idx), last_wait_node_idx + ) + if node == orig_comm_blocks[-1].wait_nodes[0]: + break + + if split_and_reshape: + fused_wait_node = fused_comm_block.wait_nodes[0] + with graph.inserting_after(fused_wait_node): + split_node = call_function( + graph, + aten.split, + ( + fused_wait_node, + [math.prod(cast(list[int], cb.shape)) for cb in orig_comm_blocks], + ), + ) + with graph.inserting_after(split_node): + fused_outputs = [] + for idx, comm_block in enumerate(orig_comm_blocks): + split_idx_node = call_function( + graph, operator.getitem, (split_node, idx) + ) + with graph.inserting_after(split_idx_node): + fused_outputs.append( + call_function( + graph, aten.reshape, (split_idx_node, comm_block.shape) + ) + ) + else: + fused_outputs = fused_comm_block.wait_nodes + + # Scatter the fused outputs. + incorrect_order_nodes = [] + for comm_block, fused_output in zip(orig_comm_blocks, fused_outputs): + # Some descendant users of the orig_comm_blocks may be scheduled before + # the fused all_reduce. For example, the user nodes of the very first + # all_reduce may be scheduled before the second all_reduce. Since the + # fused all_reduce is inserted right after the last all_reduce, the + # order can be wrong. + # `incorrect_order_nodes` records these nodes. + + orig_wait = comm_block.wait_nodes[0] + nodes = collections.deque(list(orig_wait.users)) + while nodes: + user_node = nodes.popleft() + if not isinstance(user_node, fx.Node): + continue + + if node_indices[user_node] < last_wait_node_idx: + incorrect_order_nodes.append(user_node) + nodes.extend(list(user_node.users)) + + orig_wait.replace_all_uses_with(fused_output) + + last_fused_result = fused_outputs[0] + fused_outputs_set = OrderedSet(fused_outputs) + for node in graph.nodes: + if node in fused_outputs_set: + last_fused_result = node + + # Move the incorrect_order_nodes to right after the last fused_result. + incorrect_order_nodes = sorted( + incorrect_order_nodes, key=lambda node: node_indices[node] + ) + move_block_after(incorrect_order_nodes, last_fused_result) + + +def _fuse_allreduce( + graph: fx.Graph, + comm_blocks: list[CommBlock], + node_indices: dict[fx.Node, int], + use_concat: bool, +) -> CommBlock: + """Given a list of allreduce CommBlock, fuse the CommBlocks into one CommBlock.""" + + if len(comm_blocks) == 1: + return comm_blocks[0] + + # Find the last input node of all the CommBlocks. This node will be served + # as the inserting point of the new collective op. + last_input_node = comm_blocks[0].inputs[0] + last_input_index = -1 + all_input_nodes = [] + for comm_block in comm_blocks: + input_node = comm_block.inputs[0] + all_input_nodes.append(input_node) + index = node_indices[input_node] + if index >= last_input_index: + assert index != last_input_index + last_input_node = input_node + last_input_index = index + + if use_concat: + fused_comm_block = _fuse_allreduce_by_concat( + graph, last_input_node, all_input_nodes, comm_blocks[-1] + ) + else: + fused_comm_block = _fuse_with_coalesced_op( + graph, last_input_node, all_input_nodes, comm_blocks[-1] + ) + + _scatter_fused_allreduce_waits( + graph, fused_comm_block, comm_blocks, node_indices, split_and_reshape=use_concat + ) + + for comm_block in comm_blocks: + for wait in comm_block.wait_nodes: + graph.erase_node(wait) + graph.erase_node(comm_block.comm_node) + graph.eliminate_dead_code() + + return fused_comm_block + + +def _bucket_size_fusion( + graph: fx.Graph, comm_blocks: list[CommBlock], bucket_size_mb: int +) -> Generator[list[CommBlock], None, None]: + MB = 1024**2 + bucket_size = 1 * MB + bucket_cap_size = bucket_size_mb * MB + curr_size = 0 + curr_blocks = [] + + count = 0 + fuse_count = 0 + for i, block in enumerate(comm_blocks): + curr_blocks.append(block) + itemsize = block.comm_node.meta["tensor_meta"].dtype.itemsize + curr_size += cast(torch.Size, block.shape).numel() * itemsize + count += 1 + if curr_size < bucket_size and i != len(comm_blocks) - 1: + continue + + fuse_count += 1 + if torch.distributed.get_rank() == 0: + logger.info( + "DDP bucketing: block%d, count=%d, curr_size=%d, bucket_size=%d", + fuse_count, + count, + curr_size, + bucket_size, + ) + + # Set the debug counters + counters["inductor"]["ddp_buckets"] = fuse_count + yield curr_blocks + + bucket_size = bucket_cap_size + curr_blocks = [] + curr_size = 0 + count = 0 + + +def _fuse_ddp_communication( + graph: fx.Graph, algorithm_fn: Callable[..., Any], fusion_fn: Callable[..., Any] +) -> None: + for output in reversed(graph.nodes): + if output.op == "output": + break + + def ddp_reducer_filter(block: CommBlock) -> bool: + if ( + not isinstance(block.comm_node.args[0], fx.Node) + or block.comm_node.args[0].target != aten.div.Tensor + ): + return False + + if len(block.wait_nodes[0].users) != 1: + # gradient/wait node should only be used by one user + return False + + # Two cases: + # 1. gradient/wait node should be directly used by the output + # if gradient is None before bwd. + # 2. gradient/wait node should be directly used by copy_. + if ( + output not in block.wait_nodes[0].users + and next(iter(block.wait_nodes[0].users)).target != aten.copy_.default + ): + return False + + return True + + ops = ( + torch.ops._c10d_functional.all_reduce_.default, + torch.ops._c10d_functional.all_reduce.default, + ) + comm_blocks = get_all_comm_blocks(graph, ops, comm_filter=ddp_reducer_filter) + node_indices = {node: i for i, node in enumerate(graph.nodes)} + + for block in algorithm_fn(graph, comm_blocks): + fusion_fn(graph, block, node_indices) + + +def fuse_ddp_with_coalesced_op(graph: fx.Graph, bucket_size_mb: int) -> None: + _fuse_ddp_communication( + graph, + partial(_bucket_size_fusion, bucket_size_mb=bucket_size_mb), + partial(_fuse_allreduce, use_concat=False), + ) + + +def fuse_ddp_with_concat_op(graph: fx.Graph, bucket_size_mb: int) -> None: + _fuse_ddp_communication( + graph, + partial(_bucket_size_fusion, bucket_size_mb=bucket_size_mb), + partial(_fuse_allreduce, use_concat=True), + ) + + +def schedule_comm_wait(graph: fx.Graph) -> None: + """ + Delay the execution of wait tensors of allreduce until its first user. + + This algorithm considers the intermediate users, like split, getitem, + of the wait node and schedule those intermediate users as well. + This will result in a better overlapping result. + """ + ops = ( + torch.ops._c10d_functional.all_reduce_.default, + torch.ops._c10d_functional.all_reduce.default, + torch.ops._c10d_functional.all_reduce_coalesced.default, + torch.ops._c10d_functional.all_reduce_coalesced_.default, + ) + comm_blocks = get_all_comm_blocks(graph, ops) + if not comm_blocks: + return + + # Find all the end users. + allreduce_users = OrderedSet[fx.Node]() + for allreduce in comm_blocks: + for output in allreduce.outputs: + allreduce_users.update(output.users) + + node_indices = {node: i for i, node in enumerate(graph.nodes)} + for allreduce in comm_blocks: + # Find the earliest/first user -- target_node. + assert len(allreduce.outputs) >= 1, ( + f"Found a allreduce that has zero outputs/users -- {allreduce}." + ) + # Initialize the target node to avoid typing issues. + target_node = next(iter(next(iter(allreduce.outputs)).users)) + target_node_index = 2**31 + for user in (user for output in allreduce.outputs for user in output.users): + index = node_indices[user] + if index < target_node_index: + target_node = user + target_node_index = index + + # Move wait nodes and all the subsequent nodes in the comm_block to + # before the first user -- target_node. + wait_idx = -1 + for wait_idx, node in enumerate(allreduce.node_list): + if node == allreduce.wait_nodes[0]: + break + assert wait_idx >= 0 + move_block_before(allreduce.node_list[wait_idx:], target_node) + + +def fuse_ddp_communication( + graph: fx.Graph, passes: list[Callable[..., None] | str], bucket_size_mb: int +) -> None: + for i, pa in enumerate(passes): + with GraphTransformObserver( + graph.owning_module, f"fuse_ddp_communication_pass_{i}" + ): + if isinstance(pa, str): + func = globals()[pa] + else: + func = pa + if "bucket_size_mb" in OrderedSet( + v.name for v in inspect.signature(func).parameters.values() + ): + func(graph, bucket_size_mb=bucket_size_mb) + else: + func(graph) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/decompose_mem_bound_mm.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/decompose_mem_bound_mm.py new file mode 100644 index 0000000000000000000000000000000000000000..3613ab1ed17b5e35815d1bca359b94b29b511abc --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/decompose_mem_bound_mm.py @@ -0,0 +1,285 @@ +# mypy: allow-untyped-defs +import logging + +import torch +from torch import Tensor +from torch._dynamo.utils import counters, is_node_meta_valid +from torch.fx.experimental.symbolic_shapes import ( + statically_known_false, + statically_known_true, +) + +from .. import config +from ..pattern_matcher import Arg, CallFunction, Match, register_graph_pattern +from .split_cat import construct_pattern_matcher_pass + + +aten = torch.ops.aten +log = logging.getLogger(__name__) + +# TODO: need a better strategy for decomposing mm +# The following two constants are for CUDA device only +MIN_FIRST_DIMENSION_DECOMPOSITION = 10240 +MAX_OTHER_DIMENSION_DECOMPOSITION = 32 +# The following two constants are for CPU device only +CPU_MAX_FIRST_DIMENSION_DECOMPOSITION = 1 +CPU_MAX_OTHER_DIMENSION_DECOMPOSITION = 2048 + +min_first_dimension_decomposition = MIN_FIRST_DIMENSION_DECOMPOSITION +max_other_dimension_decomposition = MAX_OTHER_DIMENSION_DECOMPOSITION +cpu_max_first_dimension_decomposition = CPU_MAX_FIRST_DIMENSION_DECOMPOSITION +cpu_max_other_dimension_decomposition = CPU_MAX_OTHER_DIMENSION_DECOMPOSITION +if "decompose_mm_pass" in config.post_grad_fusion_options: + min_first_dimension_decomposition = config.post_grad_fusion_options[ + "decompose_mm_pass" + ].get("min_first_dimension_decomposition", MIN_FIRST_DIMENSION_DECOMPOSITION) + max_other_dimension_decomposition = config.post_grad_fusion_options[ + "decompose_mm_pass" + ].get("max_other_dimension_decomposition", MAX_OTHER_DIMENSION_DECOMPOSITION) + cpu_max_first_dimension_decomposition = config.post_grad_fusion_options[ + "decompose_mm_pass" + ].get( + "cpu_max_first_dimension_decomposition", CPU_MAX_FIRST_DIMENSION_DECOMPOSITION + ) + cpu_max_other_dimension_decomposition = config.post_grad_fusion_options[ + "decompose_mm_pass" + ].get( + "cpu_max_other_dimension_decomposition", CPU_MAX_OTHER_DIMENSION_DECOMPOSITION + ) + + +def check_device(a: Tensor, b: Tensor, device="cuda") -> bool: + return (a.device.type == b.device.type) and (b.device.type == device) + + +def realize_inputs(inputs: list[torch.fx.Node]): + for inp in inputs: + if isinstance(inp, torch.fx.node.Node): + inp.meta["inductor_realize_to_strides"] = True + + +def should_decompose_bmm(mat1, mat2) -> bool: + if is_node_meta_valid(mat1) and is_node_meta_valid(mat2): + mat1 = mat1.meta["val"] + mat2 = mat2.meta["val"] + else: + return False + if len(mat1.shape) != 3 or len(mat2.shape) != 3: + return False + if check_device(mat1, mat2, device="cuda") or check_device( + mat1, mat2, device="xpu" + ): + if mat1.shape[0] < min_first_dimension_decomposition: + return False + # 2 of m, n, k must be <= MAX_OTHER_DIMENSION_DECOMPOSITION + # use bool() to deal with BooleanAtom type + if ( + bool(mat1.shape[1] < max_other_dimension_decomposition) + + bool(mat1.shape[2] < max_other_dimension_decomposition) + + bool(mat2.shape[2] < max_other_dimension_decomposition) + < 2 + ): + return False + return True + elif check_device(mat1, mat2, device="cpu"): + if ( + mat1.shape[0] <= cpu_max_first_dimension_decomposition + and mat2.shape[0] <= cpu_max_first_dimension_decomposition + ): + return True + return False + + +def should_decompose_mm(mat1, mat2) -> bool: + """ + Determines whether matrix multiplication (mm) should be decomposed into pointwise operations + based on the input matrices' metadata, shapes, device placement, and configuration options. + Args: + mat1: The first matrix operand. Expected to be an object with a `.meta` attribute containing + a "val" key, or a tensor-like object with a `.shape` attribute. + mat2: The second matrix operand. Same requirements as `mat1`. + Returns: + bool: True if the matrix multiplication should be decomposed according to the following logic: + - Both inputs must have valid node metadata. + - Both matrices must be 2-dimensional. + - If the configuration option `skip_dynamic_shape_dim_check` is False: + - Decomposition is only considered for statically-shaped matrices. + - For CUDA devices: `mat1.shape[0]` must be at least `min_first_dimension_decomposition`, + and both dimensions of `mat2` must be less than `max_other_dimension_decomposition`. + - For CPU devices: All relevant dimensions must be less than or equal to their respective + CPU decomposition thresholds. + - If `skip_dynamic_shape_dim_check` is True: + - Decomposition is considered for dynamic shapes as well, using a combination of + `statically_known_true` and `statically_known_false` checks to handle uncertainty. + - The same dimension and device checks apply, but allow for dynamic/static uncertainty. + - Returns False if any of the above conditions are not met. + Notes: + - Relies on helper functions such as `is_node_meta_valid`, `check_device`, `statically_known_true`, + and `statically_known_false`, as well as configuration values like + `min_first_dimension_decomposition`, `max_other_dimension_decomposition`, etc. + - Designed for use in graph optimization or fusion passes where decomposing large or dynamic + matrix multiplications can improve performance or memory usage. + """ + if is_node_meta_valid(mat1) and is_node_meta_valid(mat2): + mat1 = mat1.meta["val"] + mat2 = mat2.meta["val"] + else: + return False + if len(mat1.shape) != 2 or len(mat2.shape) != 2: + return False + # case 1: we skip decompose mm if the input is dynamic shape + if not config.post_grad_fusion_options["decompose_mm_pass"].get( + "skip_dynamic_shape_dim_check", False + ): + return ( + ( + check_device(mat1, mat2, device="cuda") + or check_device(mat1, mat2, device="xpu") + ) + and statically_known_true( + mat1.shape[0] >= min_first_dimension_decomposition + ) + and statically_known_true(mat2.shape[0] < max_other_dimension_decomposition) + and statically_known_true(mat2.shape[1] < max_other_dimension_decomposition) + ) or ( + check_device(mat1, mat2, device="cpu") + and statically_known_true( + mat1.shape[0] <= cpu_max_first_dimension_decomposition + ) + and statically_known_true( + mat2.shape[0] <= cpu_max_other_dimension_decomposition + ) + and statically_known_true( + mat2.shape[1] <= cpu_max_other_dimension_decomposition + ) + ) + # case 2: we decompose mm if the input is dynamic shape + else: + return ( + ( + check_device(mat1, mat2, device="cuda") + or check_device(mat1, mat2, device="xpu") + ) + and ( + statically_known_true( + mat1.shape[0] >= min_first_dimension_decomposition + ) + or not statically_known_false( + mat1.shape[0] >= min_first_dimension_decomposition + ) + ) + and ( + statically_known_true(mat2.shape[0] < max_other_dimension_decomposition) + or not statically_known_false( + mat2.shape[0] < max_other_dimension_decomposition + ) + ) + and ( + statically_known_true(mat2.shape[1] < max_other_dimension_decomposition) + or not statically_known_false( + mat2.shape[1] < max_other_dimension_decomposition + ) + ) + ) or ( + check_device(mat1, mat2, device="cpu") + and ( + statically_known_true( + mat1.shape[0] <= cpu_max_first_dimension_decomposition + ) + or not statically_known_false( + mat1.shape[0] <= cpu_max_first_dimension_decomposition + ) + ) + and ( + statically_known_true( + mat2.shape[0] <= cpu_max_other_dimension_decomposition + ) + or not statically_known_false( + mat2.shape[0] <= cpu_max_other_dimension_decomposition + ) + ) + and ( + statically_known_true( + mat2.shape[1] <= cpu_max_other_dimension_decomposition + ) + or not statically_known_false( + mat2.shape[1] <= cpu_max_other_dimension_decomposition + ) + ) + ) + + +def print_decompose_pattern(match: Match, inputs: list[torch.fx.Node]): + node = match.nodes[-1] + log.debug( + "Decompose %s with input shape: %s", + node.target, + ", ".join( + str(input.meta["val"].shape) if "val" in input.meta else "None" + for input in inputs + ), + ) + + +@register_graph_pattern( + CallFunction(aten.bmm, Arg(), Arg()), + pass_dict=construct_pattern_matcher_pass("decompose_mm_pass"), +) +def decompose_bmm(match: Match, mat1: torch.fx.Node, mat2: torch.fx.Node): + def repl(mat1, mat2): + return torch.sum(mat1[:, :, :, None] * mat2[:, None, :, :], dim=-2).to( + mat1.dtype + ) + + if should_decompose_bmm(mat1, mat2): + counters["inductor"]["decompose_bmm"] += 1 + # pyrefly: ignore [bad-argument-type] + match.replace_by_example(repl, [mat1, mat2]) + print_decompose_pattern(match, [mat1, mat2]) + realize_inputs([mat1, mat2]) + return + + +@register_graph_pattern( + CallFunction(aten.addmm, Arg(), Arg(), Arg()), + pass_dict=construct_pattern_matcher_pass("decompose_mm_pass"), +) +def decompose_addmm( + match: Match, + mat1: torch.fx.Node, + mat2: torch.fx.Node, + mat3: torch.fx.Node, +): + def repl(mat1, mat2, mat3): + return ( + torch.sum(mat2[:, :, None] * mat3[None, :, :], dim=-2).to(mat2.dtype) + mat1 + ) + + if should_decompose_mm(mat2, mat3): + counters["inductor"]["decompose_addmm"] += 1 + # pyrefly: ignore [bad-argument-type] + match.replace_by_example(repl, [mat1, mat2, mat3]) + print_decompose_pattern(match, [mat1, mat2, mat3]) + realize_inputs([mat1, mat2, mat3]) + return + + +@register_graph_pattern( + CallFunction(aten.mm, Arg(), Arg()), + pass_dict=construct_pattern_matcher_pass("decompose_mm_pass"), +) +def decompose_mm( + match: Match, + mat1: torch.fx.Node, + mat2: torch.fx.Node, +): + def repl(mat1, mat2): + return torch.sum(mat1[:, :, None] * mat2[None, :, :], dim=-2).to(mat1.dtype) + + if should_decompose_mm(mat1, mat2): + counters["inductor"]["decompose_mm"] += 1 + # pyrefly: ignore [bad-argument-type] + match.replace_by_example(repl, [mat1, mat2]) + print_decompose_pattern(match, [mat1, mat2]) + realize_inputs([mat1, mat2]) + return diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/dedupe_symint_uses.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/dedupe_symint_uses.py new file mode 100644 index 0000000000000000000000000000000000000000..7b431c2f17117ae0c9e570072759a72417711562 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/dedupe_symint_uses.py @@ -0,0 +1,81 @@ +# mypy: allow-untyped-defs +from dataclasses import dataclass +from typing import Any + +import torch +from torch import SymBool, SymFloat, SymInt +from torch.types import py_sym_types +from torch.utils._ordered_set import OrderedSet + + +@dataclass +class _SymExprHash: + """ + Hash for a py_sym_types that will use the underlying sympy expression + """ + + sym_obj: SymInt | SymFloat | SymBool + + def __hash__(self) -> int: + return hash((type(self.sym_obj), self.sym_obj.node.expr)) + + def __eq__(self, value) -> bool: + if not isinstance(value, _SymExprHash): + return False + return self.sym_obj.node.expr == value.sym_obj.node.expr + + +class _SymHashingDict: + """ + Wrapper around a dictionary that will convert sym types to hash with _SymExprHash and reuse + existing sym proxies. + + SymPy hash is not always reliable so optimistically hash sympy expression, and if those fail, + fallback to symnodes. + """ + + def __init__(self): + self.sym_hash_dict = {} + + def __setitem__(self, key, value): + self.sym_hash_dict.__setitem__(self._wrap_to_sym_expr_hash(key), value) + + def __getitem__(self, key): + return self.sym_hash_dict[self._wrap_to_sym_expr_hash(key)] + + def __contains__(self, key): + return self._wrap_to_sym_expr_hash(key) in self.sym_hash_dict + + def get(self, key, default=None): + return self.sym_hash_dict.get(self._wrap_to_sym_expr_hash(key), default) + + def _wrap_to_sym_expr_hash(self, key): + return _SymExprHash(key) if isinstance(key, py_sym_types) else key + + +def dedupe_symints(graph: torch.fx.Graph): + """ + Dedupes sym ints in the graph to nodes are resolvable to symint graph inputs. + + We only dedupe from graph inputs to avoid adding a potential dependency in the forward + from the backward. + + """ + + sym_dict = _SymHashingDict() + resolvable_from_input_symints = OrderedSet[Any]() + + for node in graph.nodes: + val = node.meta.get("val", None) + if val is None or not isinstance(val, py_sym_types): + continue + + if node.op == "placeholder": + resolvable_from_input_symints.add(node) + sym_dict[val] = node + elif existing_node := sym_dict.get(val): + node.replace_all_uses_with(existing_node) + graph.erase_node(node) + elif all(n in resolvable_from_input_symints for n in node.all_input_nodes): + sym_dict[val] = node + resolvable_from_input_symints.add(node) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/efficient_conv_bn_eval.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/efficient_conv_bn_eval.py new file mode 100644 index 0000000000000000000000000000000000000000..4260a04d6d1cecd165bd4dac490670ac54d4b399 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/efficient_conv_bn_eval.py @@ -0,0 +1,408 @@ +# mypy: allow-untyped-defs +import torch +import torch.nn as nn +from torch._dynamo.utils import counters +from torch._inductor import config as inductor_config +from torch.func import functional_call + +from ..pattern_matcher import ( + CallFunctionVarArgs, + CallModuleVarArgs, + Match, + register_graph_pattern, +) +from .pre_grad import efficient_conv_bn_eval_pass + + +def efficient_conv_bn_eval( + bn: nn.modules.batchnorm._BatchNorm, conv: nn.modules.conv._ConvNd, x: torch.Tensor +): + """ + Implementation based on https://arxiv.org/abs/2305.11624 + "Efficient ConvBN Blocks for Transfer Learning and Beyond" + It leverages the associative law between convolution and affine transform, + i.e., normalize (weight conv feature) = (normalize weight) conv feature. + It works for Eval mode of ConvBN blocks during validation, and can be used + for **training** as well, but only if one sets `bn.training=False`. It + reduces memory footprint and computation cost, at the cost of slightly + reduced numerical stability. + Args: + bn (nn.modules.batchnorm._BatchNorm): a BatchNorm module. + conv (nn.modules.conv._ConvNd): a conv module + x (torch.Tensor): Input feature map. + """ + + assert bn.running_var is not None + assert bn.running_mean is not None + + # These lines of code are designed to deal with various cases + # like bn without affine transform, and conv without bias + weight_on_the_fly = conv.weight + if conv.bias is not None: + bias_on_the_fly = conv.bias + else: + bias_on_the_fly = torch.zeros_like(bn.running_var) + + if bn.weight is not None: + bn_weight = bn.weight + else: + bn_weight = torch.ones_like(bn.running_var) + + if bn.bias is not None: + bn_bias = bn.bias + else: + bn_bias = torch.zeros_like(bn.running_var) + + # shape of [C_out, 1, 1, 1] in Conv2d + target_shape = [-1] + [1] * (conv.weight.ndim - 1) + if isinstance(conv, nn.modules.conv._ConvTransposeNd): + # for transposed conv, the C_out dimension should at index 1. + target_shape[:2] = [target_shape[1], target_shape[0]] + weight_coeff = torch.rsqrt(bn.running_var + bn.eps).reshape(target_shape) + # shape of [C_out, 1, 1, 1] in Conv2d + coefff_on_the_fly = bn_weight.view_as(weight_coeff) * weight_coeff + + # shape of [C_out, C_in, k, k] in Conv2d + weight_on_the_fly = weight_on_the_fly * coefff_on_the_fly + # shape of [C_out] in Conv2d + bias_on_the_fly = bn_bias + coefff_on_the_fly.flatten() * ( + bias_on_the_fly - bn.running_mean + ) + + input = x + params = {"weight": weight_on_the_fly, "bias": bias_on_the_fly} + output = functional_call(conv, params, input) + return output + + +def efficient_conv_bn_eval_decomposed( + bn_weight, + bn_bias, + bn_running_mean, + bn_running_var, + bn_eps, + conv: torch._ops.OpOverload, + conv_weight, + conv_bias, + x, + conv_remaining_args, +): + """ + Implementation based on https://arxiv.org/abs/2305.11624 + "Efficient ConvBN Blocks for Transfer Learning and Beyond" + It leverages the associative law between convolution and affine transform, + i.e., normalize (weight conv feature) = (normalize weight) conv feature. + It works for Eval mode of ConvBN blocks during validation, and can be used + for **training** as well, but only if one sets `bn.training=False`. It + reduces memory footprint and computation cost, at the cost of slightly + reduced numerical stability. + Args: + """ + assert bn_running_var is not None + + # These lines of code are designed to deal with various cases + # like bn without affine transform, and conv without bias + weight_on_the_fly = conv_weight + if conv_bias is not None: + bias_on_the_fly = conv_bias + else: + bias_on_the_fly = torch.zeros_like(bn_running_var) + + if bn_weight is None: + bn_weight = torch.ones_like(bn_running_var) + + if bn_bias is None: + bn_bias = torch.zeros_like(bn_running_var) + + # shape of [C_out, 1, 1, 1] in Conv2d + target_shape = [-1] + [1] * (conv_weight.ndim - 1) + if "conv_transpose" in conv.__str__(): + # for transposed conv, the C_out dimension should at index 1. + target_shape[:2] = [target_shape[1], target_shape[0]] + weight_coeff = torch.rsqrt(bn_running_var + bn_eps).reshape(target_shape) + # shape of [C_out, 1, 1, 1] in Conv2d + coefff_on_the_fly = bn_weight.view_as(weight_coeff) * weight_coeff + + # shape of [C_out, C_in, k, k] in Conv2d + weight_on_the_fly = weight_on_the_fly * coefff_on_the_fly + # shape of [C_out] in Conv2d + bias_on_the_fly = bn_bias + coefff_on_the_fly.flatten() * ( + bias_on_the_fly - bn_running_mean + ) + + input = x + return conv(*((input, weight_on_the_fly, bias_on_the_fly) + conv_remaining_args)) + + +@register_graph_pattern( + CallFunctionVarArgs( + [ + torch.nn.functional.batch_norm, + ] + ), + # pyrefly: ignore [bad-argument-type] + pass_dict=efficient_conv_bn_eval_pass, + extra_check=lambda match: not inductor_config.freezing + and inductor_config.efficient_conv_bn_eval_fx_passes, +) +def efficient_conv_bn_eval_graph_transform_inlined(match: Match, *args, **kwargs): + bn_node = match.nodes[0] + graph = match.graph + assert len(bn_node.args) == 8 + + # We can only use efficient conv-bn for eval mode with track_running_stats + # bn_node.args is `training` + if bn_node.args[-3]: + return + + # Check if the input is Conv + input_node = bn_node.args[0] + + if input_node.op != "call_function": # type: ignore[union-attr] + return + + input_fn = input_node.target # type: ignore[arg-type, union-attr] + supported_convs = [ + torch._C._nn.linear, + torch.conv1d, + torch.conv2d, + torch.conv3d, + torch.conv_transpose1d, + torch.conv_transpose2d, + torch.conv_transpose3d, + ] + + if not any(input_fn is cls for cls in supported_convs): + return + + conv_node = input_node + # Output of conv is used by other nodes, cannot optimize + if len(conv_node.users) > 1: # type: ignore[union-attr] + return + + counters["inductor"]["efficient_conv_bn_eval"] += 1 + + with graph.inserting_before(bn_node): + # prepare args for the fused function + bn_running_mean = bn_node.args[1] + bn_running_var = bn_node.args[2] + bn_weight = bn_node.args[3] + bn_bias = bn_node.args[4] + bn_eps = bn_node.args[7] + assert len(conv_node.args) >= 2 # type: ignore[union-attr] + conv_input = conv_node.args[0] # type: ignore[union-attr] + conv_weight = conv_node.args[1] # type: ignore[union-attr] + conv_bias = conv_node.args[2] if len(conv_node.args) >= 3 else None # type: ignore[union-attr] + conv_remaining_args = conv_node.args[3:] # type: ignore[union-attr] + args = ( + bn_weight, + bn_bias, + bn_running_mean, + bn_running_var, + bn_eps, + conv_node.target, # type: ignore[union-attr] + conv_weight, + conv_bias, + conv_input, + conv_remaining_args, + ) + + # create a new node + new_node = graph.create_node( + op="call_function", + target=efficient_conv_bn_eval_decomposed, + args=args, # type: ignore[arg-type] + name="efficient_conv_bn_eval", + ) + + # this node replaces the original conv + bn, and therefore + # should replace the uses of bn_node + bn_node.replace_all_uses_with(new_node) + # take care of the deletion order: + # delete bn_node first, and then conv_node + graph.erase_node(bn_node) + graph.erase_node(conv_node) # type: ignore[arg-type] + + return + + +@register_graph_pattern( + CallFunctionVarArgs( + [ + torch.ops.aten.batch_norm.default, + ] + ), + # pyrefly: ignore [bad-argument-type] + pass_dict=efficient_conv_bn_eval_pass, + extra_check=lambda match: not inductor_config.freezing + and inductor_config.efficient_conv_bn_eval_fx_passes, +) +def efficient_conv_bn_eval_graph_transform_decomposed(match: Match, *args, **kwargs): + bn_node = match.nodes[0] + graph = match.graph + assert len(bn_node.args) == 9 + + # We can only use efficient conv-bn for eval mode with track_running_stats + # bn_node.args is `training` + if bn_node.args[-4]: + return + + # Check if the input is Conv + input_node = bn_node.args[0] + + if input_node.op != "call_function": # type: ignore[union-attr] + return + + input_fn = input_node.target # type: ignore[arg-type, union-attr] + supported_convs = [ + torch.ops.aten.linear.default, + torch.ops.aten.conv1d.default, + torch.ops.aten.conv2d.default, + torch.ops.aten.conv3d.default, + torch.ops.aten.conv_transpose1d.default, + torch.ops.aten.conv_transpose2d.input, + torch.ops.aten.conv_transpose3d.input, + ] + + if not any(input_fn is cls for cls in supported_convs): + return + + conv_node = input_node + # Output of conv is used by other nodes, cannot optimize + if len(conv_node.users) > 1: # type: ignore[union-attr] + return + + counters["inductor"]["efficient_conv_bn_eval"] += 1 + + with graph.inserting_before(bn_node): + # prepare args for the fused function + bn_weight = bn_node.args[1] + bn_bias = bn_node.args[2] + bn_running_mean = bn_node.args[3] + bn_running_var = bn_node.args[4] + bn_eps = bn_node.args[7] + assert len(conv_node.args) >= 2 # type: ignore[union-attr] + conv_input = conv_node.args[0] # type: ignore[union-attr] + conv_weight = conv_node.args[1] # type: ignore[union-attr] + conv_bias = conv_node.args[2] if len(conv_node.args) >= 3 else None # type: ignore[union-attr] + conv_remaining_args = conv_node.args[3:] # type: ignore[union-attr] + args = ( + bn_weight, + bn_bias, + bn_running_mean, + bn_running_var, + bn_eps, + conv_node.target, # type: ignore[union-attr] + conv_weight, + conv_bias, + conv_input, + conv_remaining_args, + ) + + # create a new node + new_node = graph.create_node( + op="call_function", + target=efficient_conv_bn_eval_decomposed, + args=args, # type: ignore[arg-type] + name="efficient_conv_bn_eval", + ) + + # this node replaces the original conv + bn, and therefore + # should replace the uses of bn_node + bn_node.replace_all_uses_with(new_node) + # take care of the deletion order: + # delete bn_node first, and then conv_node + graph.erase_node(bn_node) + graph.erase_node(conv_node) # type: ignore[arg-type] + + return + + +@register_graph_pattern( + CallModuleVarArgs( + [ + nn.modules.batchnorm._BatchNorm, + nn.BatchNorm1d, + nn.BatchNorm2d, + nn.BatchNorm3d, + nn.SyncBatchNorm, + ], + ), + # pyrefly: ignore [bad-argument-type] + pass_dict=efficient_conv_bn_eval_pass, + extra_check=lambda match: not inductor_config.freezing + and inductor_config.efficient_conv_bn_eval_fx_passes, +) +def efficient_conv_bn_eval_graph_transform(match: Match, *args, **kwargs): + # We matched a BN node + bn_node = match.nodes[0] + graph = match.graph + gm = graph.owning_module + bn_mod = getattr(gm, bn_node.target) # type: ignore[arg-type] + + # We can only use efficient conv-bn for eval mode with track_running_stats + if not bn_mod.track_running_stats or bn_mod.training: + return + + # Check if the input is Conv + if bn_node.args: + input_node = bn_node.args[0] + else: + input_node = bn_node.kwargs["input"] + if input_node.op != "call_module": # type: ignore[union-attr] + return + if not hasattr(gm, input_node.target): # type: ignore[arg-type, union-attr] + return + input_mod = getattr(gm, input_node.target) # type: ignore[arg-type, union-attr] + supported_convs = [ + nn.Linear, + nn.Conv1d, + nn.Conv2d, + nn.Conv3d, + nn.ConvTranspose1d, + nn.ConvTranspose2d, + nn.ConvTranspose3d, + ] + if not any(isinstance(input_mod, cls) for cls in supported_convs): + return + conv_node = input_node + # Output of conv is used by other nodes, cannot optimize + if len(conv_node.users) > 1: # type: ignore[union-attr] + return + + # Find a pair of conv and bn computation nodes to optimize. + counters["inductor"]["efficient_conv_bn_eval"] += 1 + + with graph.inserting_before(conv_node): # type: ignore[arg-type] + # create `get_attr` node to access modules + # note that we directly call `create_node` to fill the `name` + # argument. `graph.get_attr` and + # `graph.call_function` does not allow the `name` argument. + conv_get_node = graph.create_node( + op="get_attr", + target=conv_node.target, # type: ignore[union-attr] + name="get_conv", + ) + bn_get_node = graph.create_node( + op="get_attr", target=bn_node.target, name="get_bn" + ) + if conv_node.args: # type: ignore[union-attr] + conv_input = conv_node.args[0] # type: ignore[union-attr] + else: + conv_input = conv_node.kwargs["input"] # type: ignore[union-attr] + # prepare args for the fused function + args = (bn_get_node, conv_get_node, conv_input) + # create a new node + new_node = graph.create_node( + op="call_function", + target=efficient_conv_bn_eval, + args=args, + name="efficient_conv_bn_eval", + ) + # this node replaces the original conv + bn, and therefore + # should replace the uses of bn_node + bn_node.replace_all_uses_with(new_node) + # take care of the deletion order: + # delete bn_node first, and then conv_node + graph.erase_node(bn_node) + graph.erase_node(conv_node) # type: ignore[arg-type] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/freezing_patterns.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/freezing_patterns.py new file mode 100644 index 0000000000000000000000000000000000000000..48a940ae625fe31a4f6cb1d8b620befcdc22da4c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/freezing_patterns.py @@ -0,0 +1,313 @@ +# mypy: allow-untyped-defs +import functools + +import torch +from torch._inductor.compile_fx import fake_tensor_prop +from torch._inductor.utils import GPU_TYPES + +from ..._dynamo.utils import counters +from .. import config +from ..pattern_matcher import ( + _return_true, + CallFunction, + fwd_only, + Ignored, + init_once_fakemode, + KeywordArg, + Match, + PatternMatcherPass, + register_graph_pattern, + register_replacement, + stable_topological_sort, +) + + +aten = torch.ops.aten + +# First pass_patterns[0] are applied, then [1], then [2] +pass_patterns = [ + PatternMatcherPass(), + PatternMatcherPass(), + PatternMatcherPass(), +] + +binary_folding_pass = PatternMatcherPass() + + +def freezing_passes(gm: torch.fx.GraphModule, aot_example_inputs): + """ + Passes that are applied to the graph to freeze pass. + """ + + from ..freezing import constant_fold + + lazy_init() + # We need a few rounds of binary folding to get rid of all the + # unnecessary nodes, but may need a good method to chose the rounds number. + # works like: conv+binary+binary. + binary_folding = counters["inductor"]["binary_folding"] + fake_tensor_prop(gm, aot_example_inputs, True) + + torch._inductor.fx_passes.binary_folding.mark_mixed_dtype_allowed_computation_ops( + gm + ) + for _ in range(4): + constant_fold(gm) + # Make sure meta['val'] is properly set for all nodes + fake_tensor_prop(gm, aot_example_inputs, True) + binary_folding_pass.apply(gm.graph) # type: ignore[arg-type] + # If we don't have binary folding, we don't need to run the pass again. + # TODO: remove the need to run fake_tensor_prop on the whole model. + if counters["inductor"]["binary_folding"] == binary_folding: + break + binary_folding = counters["inductor"]["binary_folding"] + + torch._inductor.fx_passes.binary_folding.recover_original_precision_folded_computation_ops( + gm + ) + + constant_fold(gm) + fake_tensor_prop(gm, aot_example_inputs, True) + + for pattern in pass_patterns: + pattern.apply(gm.graph) # type: ignore[arg-type] + + # The CPU weight packing always assume the conv's weight is channels last, + # So make sure the layout_optimization is on when doing it. + if ( + torch._C._has_mkldnn + and config.cpp.weight_prepack + and config.layout_optimization + ): + from .mkldnn_fusion import _eliminate_duplicate_packed_nodes + + _eliminate_duplicate_packed_nodes(gm) + + stable_topological_sort(gm.graph) + gm.recompile() + gm.graph.lint() + + +@init_once_fakemode +def lazy_init(input_device: torch.device | None = None): + if torch._C._has_mkldnn and config.cpp.weight_prepack: + from .mkldnn_fusion import _mkldnn_weight_pack_init + + _mkldnn_weight_pack_init() + + from .binary_folding import binary_folding_init + + addmm_patterns_init() + binary_folding_init() + + +def register_freezing_graph_pattern(pattern, extra_check=_return_true, pass_number=0): + while pass_number > len(pass_patterns) - 1: + pass_patterns.append(PatternMatcherPass()) + return register_graph_pattern( + pattern, + extra_check=extra_check, + # pyrefly: ignore [bad-argument-type] + pass_dict=pass_patterns[pass_number], + ) + + +def register_binary_folding_pattern(pattern, extra_check=_return_true): + return register_graph_pattern( + pattern, + extra_check=extra_check, + # pyrefly: ignore [bad-argument-type] + pass_dict=binary_folding_pass, + ) + + +@functools.cache +def addmm_patterns_init(): + """ + addmm related patterns. + To avoid duplication, also includes int8 WoQ GEMM pattern without bias. + """ + device = next( + (gpu for gpu in GPU_TYPES if getattr(torch, gpu).is_available()), "cpu" + ) + val = functools.partial(torch.empty, (10, 10), device=device, requires_grad=False) + scale = functools.partial(torch.empty, (10,), device=device, requires_grad=False) + + def check_int8_woq_concat_linear_weights(match): + is_cpu = match.kwargs["inp"].meta["val"].is_cpu + if not is_cpu or not config.cpp.enable_concat_linear: + # Currently, this pattern is only supported on CPU + return False + + weight_inputs = ["w1", "w2"] + if "w3" in match.kwargs: + weight_inputs.append("w3") + + if not all( + match.kwargs[wgt].target is torch.ops.prims.convert_element_type.default + for wgt in weight_inputs + ): + return False + + if not all( + next(iter(match.kwargs[wgt]._input_nodes.keys())).meta["val"].dtype + is torch.int8 + for wgt in weight_inputs + ): + return False + + if not all( + match.kwargs[wgt].meta["val"].dtype is torch.bfloat16 + for wgt in weight_inputs + ): + return False + + return True + + def check_concat_weights(match): + is_cpu = match.kwargs["inp"].meta["val"].is_cpu + if is_cpu and not config.cpp.enable_concat_linear: + return False + + weight_inputs = ["w1", "w2"] + if "w3" in match.kwargs: + weight_inputs.append("w3") + + equal_shape_inputs = [weight_inputs] + + if "b1" in match.kwargs: + bias_inputs = ["b1", "b2"] + if "b3" in match.kwargs: + bias_inputs.append("b3") + + equal_shape_inputs.append(bias_inputs) + + for equal_shape_group in equal_shape_inputs: + inps = [match.kwargs[name] for name in equal_shape_group] + + if not all( + inp.op == "get_attr" + and inp.meta["val"].shape[:-1] == inps[0].meta["val"].shape[:-1] + for inp in inps + ): + return False + return True + + def int8_woq_fusion_pattern(inp, w1, w2, w3, s1, s2, s3): + return ((inp @ w1) * s1, (inp @ w2) * s2, (inp @ w3) * s3) + + def int8_woq_fusion_replacement(inp, w1, w2, w3, s1, s2, s3): + cat_w = torch.cat((w1, w2, w3), dim=1) + cat_s = torch.cat((s1, s2, s3), dim=0) + mm = (inp @ cat_w).mul(cat_s) + n1, n2 = w1.size(1), w2.size(1) + return mm.tensor_split([n1, n1 + n2], dim=-1) + + register_replacement( + # pyrefly: ignore [bad-argument-type] + int8_woq_fusion_pattern, + # pyrefly: ignore [bad-argument-type] + int8_woq_fusion_replacement, + [val(), val(), val(), val(), scale(), scale(), scale()], + # pyrefly: ignore [bad-argument-type] + fwd_only, + # pyrefly: ignore [bad-argument-type] + pass_patterns[0], + extra_check=check_int8_woq_concat_linear_weights, + exclusive_arg_names=("w1", "w2", "w3", "s1", "s2", "s3"), + ) + + def matmul_fuse_pattern(inp, w1, w2, w3): + return (inp @ w1, inp @ w2, inp @ w3) + + def matmul_replacement(inp, w1, w2, w3): + weights = (w1, w2, w3) + cat_t = torch.cat(weights, dim=1) + mm = inp @ cat_t + return mm.split([w.size(1) for w in weights], dim=1) + + register_replacement( + # pyrefly: ignore [bad-argument-type] + matmul_fuse_pattern, + # pyrefly: ignore [bad-argument-type] + matmul_replacement, + [val(), val(), val(), val()], + # pyrefly: ignore [bad-argument-type] + fwd_only, + # pyrefly: ignore [bad-argument-type] + pass_patterns[0], + extra_check=check_concat_weights, + exclusive_arg_names=("w1", "w2", "w3"), + ) + + def matmul_fuse_pattern_two(inp, w1, w2): + return (inp @ w1, inp @ w2) + + def matmul_replacement_two(inp, w1, w2): + weights = (w1, w2) + cat_t = torch.cat(weights, dim=1) + mm = inp @ cat_t + return mm.split([w.size(1) for w in weights], dim=1) + + register_replacement( + # pyrefly: ignore [bad-argument-type] + matmul_fuse_pattern_two, + # pyrefly: ignore [bad-argument-type] + matmul_replacement_two, + [val(), val(), val()], + # pyrefly: ignore [bad-argument-type] + fwd_only, + # pyrefly: ignore [bad-argument-type] + pass_patterns[0], + extra_check=check_concat_weights, + exclusive_arg_names=("w1", "w2"), + ) + + def addmm_fuse_pattern_second(inp, w1, w2, w3, b1, b2, b3): + return ( + aten.addmm(b1, inp, w1), + aten.addmm(b2, inp, w2), + aten.addmm(b3, inp, w3), + ) + + def addmm_fuse_replacement_second(inp, w1, w2, w3, b1, b2, b3): + weights = (w1, w2, w3) + cat_w = torch.cat(weights, dim=1) + cat_b = torch.cat((b1, b2, b3)) + return aten.addmm(cat_b, inp, cat_w).split([w.size(1) for w in weights], dim=1) + + register_replacement( + # pyrefly: ignore [bad-argument-type] + addmm_fuse_pattern_second, + # pyrefly: ignore [bad-argument-type] + addmm_fuse_replacement_second, + [val() for _ in range(7)], + # pyrefly: ignore [bad-argument-type] + fwd_only, + # pyrefly: ignore [bad-argument-type] + pass_patterns[0], + extra_check=check_concat_weights, + exclusive_arg_names=("w1", "w2", "w3", "b1", "b2", "b3"), + ) + + +def same_dtype(match): + return match.output_node().args[0].meta["val"].dtype == match.kwargs["dtype"] + + +@register_graph_pattern( + CallFunction( + torch.ops.prims.convert_element_type.default, + Ignored(), + KeywordArg("dtype"), + ), + # pyrefly: ignore [bad-argument-type] + pass_dict=pass_patterns[0], + extra_check=same_dtype, +) +def unnecessary_dtype_convert(match: Match, **kwargs): + """Remove unnecessary dtype conversion op, probably left as a result of Conv-Bn folding""" + graph = match.graph + node = match.output_node() + node.replace_all_uses_with(node.args[0]) # type: ignore[arg-type] + graph.erase_node(node) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/fsdp.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/fsdp.py new file mode 100644 index 0000000000000000000000000000000000000000..655d81621f3cb1acb520fc39e3d2122d06bb0eb7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/fsdp.py @@ -0,0 +1,117 @@ +import logging +from collections.abc import Callable + +import torch +from torch._inductor.fx_passes.bucketing import ( + bucket_all_gather_by_mb, + bucket_reduce_scatter_by_mb, + BucketMode, + is_all_gather_into_tensor as is_all_gather, + merge_all_gather, + merge_reduce_scatter, +) + + +logger: logging.Logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +def is_graph_input(node: torch.fx.Node) -> bool: + return node.op == "placeholder" + + +def is_fsdp_all_gather(n): + assert is_all_gather(n) + while len(n.all_input_nodes) == 1: + n = n.all_input_nodes[0] + if n.op == "placeholder": + return True + return False + + +def is_fsdp_all_gather_wait(wait: torch.fx.Node) -> bool: + # Assume all_gather_into_tensor input is either graph input + # or dtype conversion of graph input + ag_node = wait.args[0] # type: ignore[arg-type, union-attr] + return is_fsdp_all_gather(ag_node) + + +def is_graph_output(node: torch.fx.Node) -> bool: + return all(user.op == "output" for user in node.users) + + +def is_fsdp_reduce_scatter_wait(wait: torch.fx.Node) -> bool: + if is_graph_output(wait): + return True + + if len(wait.users) == 1: + user = next(iter(wait.users)) + assert user is not None + return ( + is_graph_output(user) + and user.op == "call_function" + and user.target is torch.ops.prims.convert_element_type.default + ) + + return False + + +def bucket_fsdp_all_gather( + gm: torch.fx.GraphModule, + bucket_cap_mb_by_bucket_idx: Callable[[int], float] | None = None, + mode: BucketMode = "default", +) -> None: + """ + Bucketing pass for SimpleFSDP all_gather ops. + + Attributes: + gm (torch.fx.GraphModule): Graph module of the graph. + bucket_cap_mb_by_bucket_idx (Callable[[int], float] | None): callback function that + takes in bucket id and returns size of a bucket in megabytes. + """ + if bucket_cap_mb_by_bucket_idx is None: + from torch._inductor.fx_passes.bucketing import ( + bucket_cap_mb_by_bucket_idx_default, + ) + + bucket_cap_mb_by_bucket_idx = bucket_cap_mb_by_bucket_idx_default + assert bucket_cap_mb_by_bucket_idx is not None + ag_buckets = bucket_all_gather_by_mb( + gm, + bucket_cap_mb_by_bucket_idx, + filter_wait_node=is_fsdp_all_gather_wait, + ) + if len(ag_buckets) == 0: + return + merge_all_gather(gm, ag_buckets, mode) + + +def bucket_fsdp_reduce_scatter( + gm: torch.fx.GraphModule, + bucket_cap_mb_by_bucket_idx: Callable[[int], float] | None = None, + mode: BucketMode = "default", +) -> None: + """ + Bucketing pass for SimpleFSDP reduce_scatter ops. + + Attributes: + gm (torch.fx.GraphModule): Graph module of the graph. + bucket_cap_mb_by_bucket_idx (Callable[[int], float] | None): callback function that + takes in bucket idx and returns size of a bucket in megabytes. By default + torch._inductor.fx_passes.bucketing.bucket_cap_mb_by_bucket_idx_default is used. + + """ + if bucket_cap_mb_by_bucket_idx is None: + from torch._inductor.fx_passes.bucketing import ( + bucket_cap_mb_by_bucket_idx_default, + ) + + bucket_cap_mb_by_bucket_idx = bucket_cap_mb_by_bucket_idx_default + rs_buckets = bucket_reduce_scatter_by_mb( + gm, + bucket_cap_mb_by_bucket_idx, + filter_wait_node=is_fsdp_reduce_scatter_wait, + ) + if len(rs_buckets) == 0: + return + merge_reduce_scatter(gm, rs_buckets, mode) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/fuse_attention.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/fuse_attention.py new file mode 100644 index 0000000000000000000000000000000000000000..4bb2a9945557287469eca2556f2b71111171cef2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/fuse_attention.py @@ -0,0 +1,1371 @@ +# mypy: allow-untyped-defs +import functools +import inspect +import logging +import warnings + +import torch + +from ..._dynamo.utils import counters +from ..pattern_matcher import ( + filter_nodes, + fwd_only, + gen_register_replacement, + joint_fwd_bwd, +) + + +log = logging.getLogger(__name__) +aten = torch.ops.aten + +_scaled_dot_product_attention = aten.scaled_dot_product_attention + + +def _sfdp_pattern_1(query, key, value, inv_scale): + return ( + torch.matmul(query, key.transpose(-2, -1)) + .div(inv_scale) + .softmax(dim=-1) + .matmul(value) + ) + + +def _sfdp_replacement_1(query, key, value, inv_scale): + counters["inductor"]["fuse_attention"] += 1 + return _scaled_dot_product_attention( + query, + key, + value, + attn_mask=None, + dropout_p=0.0, + is_causal=False, + scale=1.0 / inv_scale, + ) + + +def _sfdp_pattern_2(query, key, value, scale_factor): + return ( + torch.matmul(query, key.transpose(-2, -1)) + .mul(scale_factor) + .softmax(dim=-1) + .matmul(value) + ) + + +def _sfdp_replacement_2(query, key, value, scale_factor): + counters["inductor"]["fuse_attention"] += 1 + return _scaled_dot_product_attention( + query, + key, + value, + attn_mask=None, + dropout_p=0.0, + is_causal=False, + scale=scale_factor, + ) + + +def _sfdp_pattern_3(query, key, value, inv_scale_factor, dropout_p): + return torch.nn.functional.dropout( + torch.matmul(query, key.transpose(-2, -1)) + .div(inv_scale_factor) + .softmax(dim=-1), + p=dropout_p, + ).matmul(value) + + +def _sfdp_replacement_3(query, key, value, inv_scale_factor, dropout_p): + counters["inductor"]["fuse_attention"] += 1 + return _scaled_dot_product_attention( + query, + key, + value, + attn_mask=None, + dropout_p=dropout_p, + is_causal=False, + scale=1.0 / inv_scale_factor, + ) + + +def _sfdp_pattern_4(query, key, value, scale_factor, dropout_p): + return torch.nn.functional.dropout( + torch.matmul(query, key.transpose(-2, -1)).mul(scale_factor).softmax(dim=-1), + p=dropout_p, + ).matmul(value) + + +def _sfdp_replacement_4(query, key, value, scale_factor, dropout_p): + counters["inductor"]["fuse_attention"] += 1 + return _scaled_dot_product_attention( + query, + key, + value, + attn_mask=None, + dropout_p=dropout_p, + is_causal=False, + scale=scale_factor, + ) + + +def _sfdp_pattern_5(query, key, value, attn_mask, inv_scale): + attn_weight = torch.softmax( + (query @ key.transpose(-2, -1) / (inv_scale)) + attn_mask, dim=-1 + ) + # attn_weight = torch.dropout(attn_weight, dropout_p) + return attn_weight @ value + + +def _sfdp_replacement_5(query, key, value, attn_mask, inv_scale): + counters["inductor"]["fuse_attention"] += 1 + return _scaled_dot_product_attention( + query, + key, + value, + attn_mask=attn_mask.to(dtype=query.dtype), + dropout_p=0.0, + scale=1.0 / inv_scale, + is_causal=False, + ) + + +def _sfdp_pattern_6(query, key, value, attn_mask, inv_scale, dropout_p): + attn_weight = torch.softmax( + (query @ key.transpose(-2, -1) / inv_scale) + attn_mask, dim=-1 + ) + attn_weight = torch.dropout(attn_weight, dropout_p, True) + return attn_weight @ value + + +def _sfdp_replacement_6(query, key, value, attn_mask, inv_scale, dropout_p): + counters["inductor"]["fuse_attention"] += 1 + return _scaled_dot_product_attention( + query, + key, + value, + attn_mask=attn_mask.to(dtype=query.dtype), + dropout_p=dropout_p, + scale=1.0 / inv_scale, + is_causal=False, + ) + + +def _sfdp_pattern_7(query, key, value, inv_scale, dropout_p): + # in real workloads inputs to matmul are permuted + # causing matmul to expand to a series of expand and clone calls + # we want the same to happen during pattern tracing + q = query.permute(0, 2, 1, 3) + k = key.permute(0, 2, 1, 3) + v = value.permute(0, 2, 1, 3) + div = q @ k.transpose(-2, -1) / inv_scale + div = div.to(torch.float32) + attn_weight = torch.softmax(div, dim=-1) + attn_weight = torch.dropout(attn_weight, dropout_p, True) + attn_weight = attn_weight.to(torch.float16) + v = v.to(attn_weight.dtype) + return attn_weight @ v + + +def _sfdp_replacement_7(query, key, value, inv_scale, dropout_p): + # sdpa prefers inputs in permuted format + # it makes a copy to put them in this format + # if they aren't already + # to make replacement efficient ensure that inputs to sdpa + # are in required order + counters["inductor"]["fuse_attention"] += 1 + q = query.permute(0, 2, 1, 3) + k = key.permute(0, 2, 1, 3) + v = value.permute(0, 2, 1, 3) + return _scaled_dot_product_attention( + q, + k, + v, + attn_mask=None, # attn_mask, + dropout_p=dropout_p, + scale=1.0 / inv_scale, + is_causal=False, + ) + + +def _sfdp_pattern_8(query, key, value, inv_scale): + # no dropout version of pattern 7 + q = query.permute(0, 2, 1, 3) + k = key.permute(0, 2, 1, 3) + v = value.permute(0, 2, 1, 3) + div = q @ k.transpose(-2, -1) / inv_scale + div = div.to(torch.float32) + attn_weight = torch.softmax(div, dim=-1) + attn_weight = attn_weight.to(torch.float16) + v = v.to(attn_weight.dtype) + return attn_weight @ v + + +def _sfdp_replacement_8(query, key, value, inv_scale): + counters["inductor"]["fuse_attention"] += 1 + q = query.permute(0, 2, 1, 3) + k = key.permute(0, 2, 1, 3) + v = value.permute(0, 2, 1, 3) + return _scaled_dot_product_attention( + q, + k, + v, + attn_mask=None, # attn_mask, + dropout_p=0.0, + scale=1.0 / inv_scale, + is_causal=False, + ) + + +def _sfdp_pattern_9(query, key, value, inv_scale, dropout_p): + q = query.permute(0, 2, 1, 3) + k = key.permute(0, 2, 1, 3) + v = value.permute(0, 2, 1, 3) + q = q / inv_scale + div = q @ k.transpose(-2, -1) + div = div.to(torch.float32) + attn_weight = torch.softmax(div, dim=-1) + attn_weight = torch.dropout(attn_weight, dropout_p, True) + attn_weight = attn_weight.to(torch.float16) + v = v.to(attn_weight.dtype) + return attn_weight @ v + + +def _sfdp_replacement_9(query, key, value, inv_scale, dropout_p): + counters["inductor"]["fuse_attention"] += 1 + q = query.permute(0, 2, 1, 3) + k = key.permute(0, 2, 1, 3) + v = value.permute(0, 2, 1, 3) + return _scaled_dot_product_attention( + q, + k, + v, + attn_mask=None, # attn_mask, + dropout_p=dropout_p, + scale=1.0 / inv_scale, + is_causal=False, + ) + + +def _sfdp_pattern_10(query, key, value, inv_scale): + # no dropout version of 9 + q = query.permute(0, 2, 1, 3) + k = key.permute(0, 2, 1, 3) + v = value.permute(0, 2, 1, 3) + q = q / inv_scale + div = q @ k.transpose(-2, -1) + div = div.to(torch.float32) + attn_weight = torch.softmax(div, dim=-1) + attn_weight = attn_weight.to(torch.float16) + v = v.to(attn_weight.dtype) + return attn_weight @ v + + +def _sfdp_replacement_10(query, key, value, inv_scale): + counters["inductor"]["fuse_attention"] += 1 + q = query.permute(0, 2, 1, 3) + k = key.permute(0, 2, 1, 3) + v = value.permute(0, 2, 1, 3) + return _scaled_dot_product_attention( + q, + k, + v, + attn_mask=None, # attn_mask, + dropout_p=0.0, + scale=1.0 / inv_scale, + is_causal=False, + ) + + +def _sfdp_pattern_11(query, key, value, inv_scale): + # Mainly for huggingface models + q = query.permute(0, 2, 1, 3) + k = key.permute(0, 2, 1, 3) + v = value.permute(0, 2, 1, 3) + return torch.matmul(q, k.transpose(-2, -1)).div(inv_scale).softmax(dim=-1).matmul(v) + + +def _sfdp_replacement_11(query, key, value, inv_scale): + counters["inductor"]["fuse_attention"] += 1 + return _scaled_dot_product_attention( + query.transpose(1, 2), + key.transpose(1, 2), + value.transpose(1, 2), + attn_mask=None, + dropout_p=0.0, + is_causal=False, + scale=1.0 / inv_scale, + ) + + +def _sfdp_pattern_12(query, key, value, inv_scale_factor, dropout_p): + q = query.permute(0, 2, 1, 3) + k = key.permute(0, 2, 1, 3) + v = value.permute(0, 2, 1, 3) + return torch.nn.functional.dropout( + torch.matmul(q, k.transpose(-2, -1)).div(inv_scale_factor).softmax(dim=-1), + p=dropout_p, + ).matmul(v) + + +def _sfdp_replacement_12(query, key, value, inv_scale_factor, dropout_p): + counters["inductor"]["fuse_attention"] += 1 + return _scaled_dot_product_attention( + query.transpose(1, 2), + key.transpose(1, 2), + value.transpose(1, 2), + attn_mask=None, + dropout_p=dropout_p, + is_causal=False, + scale=1.0 / inv_scale_factor, + ) + + +def _sfdp_pattern_13(query, key, value, dropout_p): + attn_weight = torch.bmm(query, key.transpose(1, 2)).softmax(dim=-1) + attn_weight = torch.nn.functional.dropout(attn_weight, p=dropout_p) + return torch.bmm(attn_weight, value) + + +def _sfdp_replacement_13(query, key, value, dropout_p): + counters["inductor"]["fuse_attention"] += 1 + return _scaled_dot_product_attention( + query.unsqueeze(0), + key.unsqueeze(0), + value.unsqueeze(0), + dropout_p=dropout_p, + scale=1.0, + ).squeeze(0) + + +def _sfdp_pattern_14(query, key, value, attn_mask, inv_scale): + # for BertLarge + # Permutations are needed to create clones in graph. + q = query.permute([0, 2, 1, 3]) + k = key.permute([0, 2, 1, 3]) + v = value.permute([0, 2, 1, 3]) + return ( + (torch.matmul(q, k.transpose(-2, -1)).div(inv_scale) + attn_mask) + .softmax(dim=-1) + .matmul(v) + ) + + +def _sfdp_replacement_14(query, key, value, attn_mask, inv_scale): + counters["inductor"]["fuse_attention"] += 1 + return _scaled_dot_product_attention( + query.transpose(1, 2), + key.transpose(1, 2), + value.transpose(1, 2), + attn_mask=attn_mask.to(dtype=query.dtype), + dropout_p=0.0, + is_causal=False, + scale=1.0 / inv_scale, + ) + + +def _sfdp_pattern_15(query, key, value, attn_mask, inv_scale): + # for DistilBert + # Permutations are needed to create clones in graph. + # Ref: https://github.com/pytorch/pytorch/issues/119911 + q = query.permute([0, 2, 1, 3]) + k = key.permute([0, 2, 1, 3]) + v = value.permute([0, 2, 1, 3]) + bs = q.size(0) + k_len = k.size(-2) + scores = q @ k.transpose(-2, -1) + scores = scores.div(inv_scale) + fill_value = torch.full((), -float("inf"), dtype=query.dtype, device=query.device) + attn_mask = (attn_mask == 0).view((bs, 1, 1, k_len)).expand_as(scores) + return torch.softmax(scores.masked_fill(attn_mask, fill_value), dim=-1) @ v + + +def _sfdp_replacement_15(query, key, value, attn_mask, inv_scale): + counters["inductor"]["fuse_attention"] += 1 + bs = query.size(0) + n_head = query.size(2) + q_len = query.size(1) + k_len = key.size(1) + # do attn_mask->logical_not() in _scaled_dot_product_attention + attn_mask = ( + (attn_mask == 1).view((bs, 1, 1, k_len)).expand((bs, n_head, q_len, k_len)) + ) + return _scaled_dot_product_attention( + query.transpose(1, 2), + key.transpose(1, 2), + value.transpose(1, 2), + attn_mask=attn_mask.to(dtype=torch.bool), + dropout_p=0.0, + is_causal=False, + scale=1.0 / inv_scale, + ) + + +def _sfdp_pattern_16(query, key, value, attn_mask, inv_scale, dropout_p): + # for BertLarge with dropout + q = query.permute([0, 2, 1, 3]) + k = key.permute([0, 2, 1, 3]) + v = value.permute([0, 2, 1, 3]) + return ( + torch.nn.functional.dropout( + (torch.matmul(q, k.transpose(-2, -1)).div(inv_scale) + attn_mask).softmax( + dim=-1 + ), + dropout_p, + ) + .to(dtype=query.dtype) + .matmul(v) + ) + + +def _sfdp_replacement_16(query, key, value, attn_mask, inv_scale, dropout_p): + counters["inductor"]["fuse_attention"] += 1 + return _scaled_dot_product_attention( + query.transpose(1, 2), + key.transpose(1, 2), + value.transpose(1, 2), + attn_mask=attn_mask.to(dtype=query.dtype), + dropout_p=dropout_p, + is_causal=False, + scale=1.0 / inv_scale, + ) + + +def _sfdp_pattern_17(query, key, value, attn_mask, inv_scale, dropout_p): + # for DistilBert with dropout + q = query.permute([0, 2, 1, 3]) + k = key.permute([0, 2, 1, 3]) + v = value.permute([0, 2, 1, 3]) + bs = q.size(0) + k_len = k.size(-2) + scores = q @ k.transpose(-2, -1) + scores = scores.div(inv_scale) + fill_value = torch.full((), -float("inf"), dtype=query.dtype, device=query.device) + attn_mask = (attn_mask == 0).view((bs, 1, 1, k_len)).expand_as(scores) + return ( + torch.nn.functional.dropout( + torch.softmax(scores.masked_fill(attn_mask, fill_value), dim=-1), dropout_p + ) + @ v + ) + + +def _sfdp_replacement_17(query, key, value, attn_mask, inv_scale, dropout_p): + counters["inductor"]["fuse_attention"] += 1 + bs = query.size(0) + n_head = query.size(2) + q_len = query.size(1) + k_len = key.size(1) + # do attn_mask->logical_not() in _scaled_dot_product_attention + attn_mask = ( + (attn_mask == 1).view((bs, 1, 1, k_len)).expand((bs, n_head, q_len, k_len)) + ) + return _scaled_dot_product_attention( + query.transpose(1, 2), + key.transpose(1, 2), + value.transpose(1, 2), + attn_mask=attn_mask.to(dtype=torch.bool), + dropout_p=dropout_p, + is_causal=False, + scale=1.0 / inv_scale, + ) + + +def _sfdp_pattern_18(query, key, value, causal_mask, inv_scale, dropout_p): + # for hf_GPT2 with dropout (introduces clone node) for inference + # it also returns permuted key & value + query = query.permute([0, 2, 1, 3]) + key = key.permute([0, 2, 1, 3]) + value = value.permute([0, 2, 1, 3]) + attn_weights = torch.matmul(query, key.permute(0, 1, 3, 2)) + inv_scale = torch.full( + [], + inv_scale, + dtype=attn_weights.dtype, + device=attn_weights.device, + ) + attn_weights = attn_weights.div(inv_scale) + causal_mask_value = torch.full( + (), torch.finfo(query.dtype).min, dtype=query.dtype, device=query.device + ) + attn_weights = torch.where(causal_mask, attn_weights, causal_mask_value) + return ( + ( + torch.nn.functional.dropout(attn_weights.softmax(dim=-1), dropout_p).matmul( + value + ) + ), + key, + value, + ) + + +def _sfdp_replacement_18(query, key, value, causal_mask, inv_scale, dropout_p): + counters["inductor"]["fuse_attention"] += 1 + permuted_key = key.transpose(1, 2) + permuted_value = value.transpose(1, 2) + return ( + _scaled_dot_product_attention( + query.transpose(1, 2), + permuted_key, + permuted_value, + attn_mask=causal_mask, + dropout_p=dropout_p, + is_causal=False, + scale=1.0 / inv_scale, + ), + permuted_key, + permuted_value, + ) + + +def _sfdp_pattern_19(query, key, value, causal_mask, attn_mask, inv_scale, dropout_p): + # for token-classification+gpt2 / text-generation+gpt2 + attn_weights = torch.matmul(query, key.permute(0, 1, 3, 2)) + inv_scale = torch.full( + [], + inv_scale, + dtype=attn_weights.dtype, + device=attn_weights.device, + ) + attn_weights = attn_weights.div(inv_scale) + causal_mask_value = torch.full( + (), torch.finfo(query.dtype).min, dtype=query.dtype, device=query.device + ) + attn_weights = torch.where(causal_mask, attn_weights, causal_mask_value) + attn_weights = attn_weights + attn_mask + attn_weights = attn_weights.softmax(dim=-1).type(value.dtype) + return torch.nn.functional.dropout(attn_weights, dropout_p).matmul(value) + + +def _sfdp_replacement_19( + query, key, value, causal_mask, attn_mask, inv_scale, dropout_p +): + counters["inductor"]["fuse_attention"] += 1 + fill_value = torch.full((), -float("inf"), dtype=query.dtype, device=query.device) + attn_mask = torch.where(causal_mask, attn_mask, fill_value) + return _scaled_dot_product_attention( + query, + key, + value, + attn_mask=attn_mask, + dropout_p=dropout_p, + is_causal=False, + scale=1.0 / inv_scale, + ) + + +def _sfdp_pattern_20(query, key, value, attn_mask, inv_scale, dropout_p): + # for DistilBert with dropout transformers==4.44.2 + q = query.permute([0, 2, 1, 3]) + k = key.permute([0, 2, 1, 3]) + v = value.permute([0, 2, 1, 3]) + bs = q.size(0) + k_len = k.size(-2) + q = q.div(inv_scale) + scores = q @ k.transpose(-2, -1) + fill_value = torch.full((), -float("inf"), dtype=query.dtype, device=query.device) + attn_mask = (attn_mask == 0).view((bs, 1, 1, k_len)).expand_as(scores) + return ( + torch.nn.functional.dropout( + torch.softmax(scores.masked_fill(attn_mask, fill_value), dim=-1), dropout_p + ) + @ v + ) + + +def _sfdp_replacement_20(query, key, value, attn_mask, inv_scale, dropout_p): + counters["inductor"]["fuse_attention"] += 1 + bs = query.size(0) + n_head = query.size(2) + q_len = query.size(1) + k_len = key.size(1) + # do attn_mask->logical_not() in _scaled_dot_product_attention + attn_mask = ( + (attn_mask == 1).view((bs, 1, 1, k_len)).expand((bs, n_head, q_len, k_len)) + ) + return _scaled_dot_product_attention( + query.transpose(1, 2), + key.transpose(1, 2), + value.transpose(1, 2), + attn_mask=attn_mask.to(dtype=torch.bool), + dropout_p=dropout_p, + is_causal=False, + scale=1.0 / inv_scale, + ) + + +def _sfdp_pattern_21(query, key, value, attn_mask): + # for T5 with inplace add + query = query.permute([0, 2, 1, 3]) + key = key.permute([0, 2, 1, 3]) + value = value.permute([0, 2, 1, 3]) + score = torch.matmul(query, key.permute(0, 1, 3, 2)) + masked_score = score + attn_mask + score = masked_score.type_as(query) + return score.float().softmax(dim=-1).type_as(query).matmul(value) + + +def _sfdp_replacement_21(query, key, value, attn_mask): + counters["inductor"]["fuse_attention"] += 1 + query = query.permute(0, 2, 1, 3) + key = key.permute(0, 2, 1, 3) + value = value.permute(0, 2, 1, 3) + return _scaled_dot_product_attention( + query, + key, + value, + attn_mask=attn_mask.to(dtype=query.dtype), + is_causal=False, + scale=1.0, + ) + + +def _sfdp_pattern_22(query, key, value, attn_mask): + # for T5 with inplace add and return key and value + query = query.permute([0, 2, 1, 3]) + key = key.permute([0, 2, 1, 3]) + value = value.permute([0, 2, 1, 3]) + score = torch.matmul(query, key.permute(0, 1, 3, 2)) + masked_score = score + attn_mask + score = masked_score.type_as(query) + return score.float().softmax(dim=-1).type_as(query).matmul(value), key, value + + +def _sfdp_replacement_22(query, key, value, attn_mask): + counters["inductor"]["fuse_attention"] += 1 + query = query.permute(0, 2, 1, 3) + key = key.permute(0, 2, 1, 3) + value = value.permute(0, 2, 1, 3) + return ( + _scaled_dot_product_attention( + query, + key, + value, + attn_mask=attn_mask.to(dtype=query.dtype), + is_causal=False, + scale=1.0, + ), + key, + value, + ) + + +def _sfdp_pattern_23(query, key, value): + # for T5 with inplace add and + # return key and value and + # attn_mask is generated by atem.full(..., 0) + query = query.permute([0, 2, 1, 3]) + key = key.permute([0, 2, 1, 3]) + value = value.permute([0, 2, 1, 3]) + score = torch.matmul(query, key.permute(0, 1, 3, 2)) + fp32_score = score.float() + score = fp32_score.type_as(query) + return score.float().softmax(dim=-1).type_as(query).matmul(value), key, value + + +def _sfdp_replacement_23(query, key, value): + counters["inductor"]["fuse_attention"] += 1 + query = query.permute(0, 2, 1, 3) + key = key.permute(0, 2, 1, 3) + value = value.permute(0, 2, 1, 3) + return ( + _scaled_dot_product_attention( + query, + key, + value, + attn_mask=None, + is_causal=False, + scale=1.0, + ), + key, + value, + ) + + +def _sfdp_pattern_24(query, key, value, attention_mask): + """ + this pattern is for MBartForCausalLM/PLBartForCausalLM. + attn_mask has a different dtype with QKV. + there is no scale in sdpa. + """ + bs = query.size(0) + n_head = query.size(1) + seq_len = query.size(2) + head_size = query.size(3) + q = query.view(bs * n_head, -1, head_size) + k = key.reshape(bs * n_head, -1, head_size) + v = value.reshape(bs * n_head, -1, head_size) + attn_weights = torch.bmm(q, k.transpose(1, 2)) + attn_weights = attn_weights.view(bs, n_head, seq_len, -1) + attention_mask + attn_weights = attn_weights.view(bs * n_head, seq_len, -1) + attn_weights = torch.nn.functional.softmax(attn_weights, dim=-1) + if query.dtype == torch.half: + attn_weights = attn_weights.to(torch.half) + attn_output = torch.bmm(attn_weights, v) + attn_output = attn_output.view(bs, n_head, seq_len, head_size) + return attn_output + + +def _sfdp_replacement_24(query, key, value, attention_mask): + counters["inductor"]["fuse_attention"] += 1 + return _scaled_dot_product_attention( + query, + key, + value, + attn_mask=attention_mask.to(dtype=query.dtype), + is_causal=False, + scale=1, + ) + + +def _sfdp_pattern_25(query, key, value, attn_mask, dropout_p): + # for T5 with inplace add + query = query.permute([0, 2, 1, 3]) + key = key.permute([0, 2, 1, 3]) + value = value.permute([0, 2, 1, 3]) + score = torch.matmul(query, key.permute(0, 1, 3, 2)) + masked_score = score + attn_mask + return torch.nn.functional.dropout( + masked_score.float().softmax(dim=-1).type_as(query), dropout_p + ).matmul(value) + + +def _sfdp_replacement_25(query, key, value, attn_mask, dropout_p): + counters["inductor"]["fuse_attention"] += 1 + query = query.permute(0, 2, 1, 3) + key = key.permute(0, 2, 1, 3) + value = value.permute(0, 2, 1, 3) + if attn_mask.device.type == "xpu": + attn_mask = attn_mask.contiguous() + return _scaled_dot_product_attention( + query, + key, + value, + attn_mask=attn_mask, + dropout_p=dropout_p, + is_causal=False, + scale=1.0, + ) + + +def _sfdp_pattern_26(query, key, value, attn_mask, dropout_p): + # for T5 with inplace add and return key and value + query = query.permute([0, 2, 1, 3]) + key = key.permute([0, 2, 1, 3]) + value = value.permute([0, 2, 1, 3]) + score = torch.matmul(query, key.permute(0, 1, 3, 2)) + masked_score = score + attn_mask + return ( + torch.nn.functional.dropout( + masked_score.float().softmax(dim=-1).type_as(query), dropout_p + ).matmul(value), + key, + value, + ) + + +def _sfdp_replacement_26(query, key, value, attn_mask, dropout_p): + counters["inductor"]["fuse_attention"] += 1 + query = query.permute(0, 2, 1, 3) + key = key.permute(0, 2, 1, 3) + value = value.permute(0, 2, 1, 3) + if attn_mask.device.type == "xpu": + attn_mask = attn_mask.contiguous() + return ( + _scaled_dot_product_attention( + query, + key, + value, + attn_mask=attn_mask, + dropout_p=dropout_p, + is_causal=False, + scale=1.0, + ), + key, + value, + ) + + +def _sfdp_pattern_27(query, key, value, dropout_p): + # for T5 with inplace add and + # return key and value and + # attn_mask is generated by atem.full(..., 0) + query = query.permute([0, 2, 1, 3]) + key = key.permute([0, 2, 1, 3]) + value = value.permute([0, 2, 1, 3]) + score = torch.matmul(query, key.permute(0, 1, 3, 2)) + return ( + torch.nn.functional.dropout( + score.float().softmax(dim=-1).type_as(query), dropout_p + ).matmul(value), + key, + value, + ) + + +def _sfdp_replacement_27(query, key, value, dropout_p): + counters["inductor"]["fuse_attention"] += 1 + query = query.permute(0, 2, 1, 3) + key = key.permute(0, 2, 1, 3) + value = value.permute(0, 2, 1, 3) + return ( + _scaled_dot_product_attention( + query, + key, + value, + attn_mask=None, + dropout_p=dropout_p, + is_causal=False, + scale=1.0, + ), + key, + value, + ) + + +def _sfdp_pattern_28(query, key, value, scale_factor, dropout_p): + # Visformer pattern + # same as pattern 4 but non-contiguous q/k/v + return _sfdp_pattern_4(query, key, value, scale_factor, dropout_p) + + +def _sfdp_replacement_28(query, key, value, scale_factor, dropout_p): + counters["inductor"]["fuse_attention"] += 1 + return _scaled_dot_product_attention( + query.contiguous(), + key.contiguous(), + value.contiguous(), + attn_mask=None, + dropout_p=dropout_p, + is_causal=False, + scale=scale_factor, + ) + + +@functools.lru_cache(None) +def _warn_tf32_disabled() -> None: + if ( + torch.cuda.is_available() + and torch.backends.cuda.matmul.fp32_precision != "tf32" + and torch.cuda.get_device_capability() >= (8, 0) + ): + warnings.warn( + "TensorFloat32 tensor cores for float32 matrix multiplication available but not enabled. " + "Skipping pattern matching to fused flash-attention. " + "Consider setting `torch.set_float32_matmul_precision('high')` for better performance." + ) + + +def _sfdp_params_check(match): + assert all(k in match.kwargs for k in ("query", "key", "value")) + query = match.kwargs["query"].meta["val"] + key = match.kwargs["key"].meta["val"] + value = match.kwargs["value"].meta["val"] + if not (query.dtype == key.dtype == value.dtype) or not ( + query.device == key.device == value.device + ): + return False + # fused kernels use tf32 + if ( + query.device.type == "cuda" + and query.dtype == torch.float32 + and torch.backends.cuda.matmul.fp32_precision != "tf32" + ): + _warn_tf32_disabled() + return False + + add_mask_node = filter_nodes(match.nodes, aten.add.Tensor) + # Has attn_mask add. + if len(add_mask_node) > 0: + attn_mask_node = add_mask_node[0].args[1] + # attn_mask_node may be a float/int number. + if not hasattr(attn_mask_node, "meta"): + return False + attn_mask = attn_mask_node.meta["val"] # type: ignore[union-attr] + # Make sure attn_mask.dtype == query.dtype or attn_mask.dtype == torch.bool + # attn_mask.dtype == torch.float for models like albert. + if ( + not isinstance(attn_mask, torch.Tensor) + or not ( + attn_mask.dtype == query.dtype + or attn_mask.dtype == torch.bool + or attn_mask.dtype == torch.float + ) + or query.device != attn_mask.device + # When we tensorify floats we end up turning floats + # into 0d scalar tensors. It doesn't make any sense + # to have a 0d scalar tensor attention mask so + # conveniently we can insert this check to get + # tests that erroneously passing in a float + # attention mask to fail as expected. + or attn_mask.dim() == 0 + ): + return False + return True + + +def _sfdp_extra_check(scale_factor_op=None, disable_cuda=False): + def fn(match): + if ( + disable_cuda + and "query" in match.kwargs + and "cuda" in str(match.kwargs["query"].meta["val"].device) + ): + return False + if scale_factor_op is not None: + scale_factor_node = filter_nodes(match.nodes, scale_factor_op)[0] + # Note: args[1] of the scale_factor_node is always the scale_factor for the current patterns. + scale_factor = scale_factor_node.args[1] + # make sure the scale_factor a float/int. SymInt? + if not isinstance(scale_factor, (float, int)): + return False + return _sfdp_params_check(match) + + return fn + + +def partialize_and_update_signature(func, **kwargs): + """ + Equivalent to functools.partial but also updates the signature on returned function + """ + original_sig = inspect.signature(func) + parameters = original_sig.parameters + + new_parameters = { + key: value for key, value in parameters.items() if key not in kwargs + } + new_sig = inspect.Signature(parameters=list(new_parameters.values())) + + partial_func = functools.partial(func, **kwargs) + + def wrapper(*args, **kwargs): + return partial_func(*args, **kwargs) + + wrapper.__signature__ = new_sig # type: ignore[attr-defined] + wrapper.__name__ = func.__name__ + + return wrapper + + +def _get_sfdp_patterns(input_device: torch.device | None = None): + from .joint_graph import patterns + + if input_device: + device = str(input_device) + else: + if torch.cuda.is_available(): + # workaround https://github.com/pytorch/pytorch/issues/97894 + device = "cuda" + elif torch.xpu.is_available(): + device = "xpu" + else: + device = "cpu" + + # sizes/values don't actually matter for initial trace + # once we get a possible match we re-trace with the actual values and verify the match still holds + g_inp = functools.partial( + torch.empty, (2, 4, 8, 16), device=device, requires_grad=True + ) + # non-contiguous input to cover more patterns. + gn_inp = functools.partial( + torch.empty_strided, + (2, 6, 16, 8), + (2304, 128, 1, 16), + device=device, + requires_grad=True, + ) + # attn_mask + b_inp = functools.partial(torch.empty, (1, 1, 8, 8), device=device) + m_inp = functools.partial(torch.empty, (2, 1, 1, 4), device=device) + # need 2d attn_mask to generate patterns with view op + m_inp_2d = functools.partial(torch.empty, (2, 4), device=device) + # inv_scale + c_inp = functools.partial(torch.tensor, 2.0, device=device) + # workaround https://github.com/pytorch/pytorch/issues/97894 + # 0.113377 is a "magic" value that lets us recover the lost input arg relationship + d = {"dropout_p": 0.113377} + s = {"inv_scale": 0.66666} + sd = {"inv_scale": 0.66666, "dropout_p": 0.113377} + + # we could also generate all these patterns in 3d.. TODO + g_3d_inp = functools.partial( + torch.empty, (1024, 128, 128), device=device, requires_grad=True + ) + + # reshape in matmul decomposition generates a clone when batch_size>1 due to the memory layout change. + # however when batch_size=1, reshape does not change the memory layout, so clone would not be generated. + # here we need to trace with input of batch_size=1 to generate a pattern graph without clone. + g_bs1_inp = functools.partial( + torch.empty, (1, 4, 8, 16), device=device, requires_grad=True + ) + m_bs1_inp = functools.partial(torch.empty, (1, 1, 1, 4), device=device) + + # softmax will generate a dtype conversion on inputs if they are in half, + # but will not in float, so we generate a pattern for both + for dtype in [torch.float, torch.half]: + g = functools.partial(g_inp, dtype=dtype) + gn = functools.partial(gn_inp, dtype=dtype) + b = functools.partial(b_inp, dtype=dtype) + b_float = functools.partial(b_inp, dtype=torch.float) + b_bool = functools.partial(b_inp, dtype=torch.bool) + m = functools.partial(m_inp, dtype=dtype) + m_float = functools.partial(m_inp, dtype=torch.float) + m_bool = functools.partial(m_inp, dtype=torch.bool) + m_2d = functools.partial(m_inp_2d, dtype=dtype) + c = functools.partial(c_inp, dtype=dtype) + g_3d = functools.partial(g_3d_inp, dtype=dtype) + g_bs1 = functools.partial(g_bs1_inp, dtype=dtype) + m_bs1 = functools.partial(m_bs1_inp, dtype=dtype) + m_bs1_float = functools.partial(m_bs1_inp, dtype=torch.float) + m_bs1_bool = functools.partial(m_bs1_inp, dtype=torch.bool) + + candidates = [ + ( + _sfdp_pattern_1, + _sfdp_replacement_1, + [g(), g(), g(), c()], + {}, + _sfdp_extra_check(aten.div.Tensor), + ), + ( + _sfdp_pattern_2, + _sfdp_replacement_2, + [g(), g(), g(), c()], + {}, + _sfdp_extra_check(aten.mul.Tensor), + ), + ( + _sfdp_pattern_3, + _sfdp_replacement_3, + [g(), g(), g(), c()], + d, + _sfdp_extra_check(aten.div.Tensor), + ), + ( + _sfdp_pattern_4, + _sfdp_replacement_4, + [g(), g(), g(), c()], + d, + _sfdp_extra_check(aten.mul.Tensor), + ), + ( + _sfdp_pattern_5, + _sfdp_replacement_5, + [g(), g(), g(), b()], + s, + _sfdp_params_check, + ), + ( + _sfdp_pattern_6, + _sfdp_replacement_6, + [g(), g(), g(), b()], + sd, + _sfdp_params_check, + ), + ( + _sfdp_pattern_7, + _sfdp_replacement_7, + [g(), g(), g()], + sd, + _sfdp_params_check, + ), + ( + _sfdp_pattern_8, + _sfdp_replacement_8, + [g(), g(), g()], + s, + _sfdp_params_check, + ), + ( + _sfdp_pattern_9, + _sfdp_replacement_9, + [g(), g(), g()], + sd, + _sfdp_params_check, + ), + ( + _sfdp_pattern_10, + _sfdp_replacement_10, + [g(), g(), g()], + s, + _sfdp_params_check, + ), + ( + _sfdp_pattern_11, + _sfdp_replacement_11, + [g(), g(), g(), c()], + {}, + _sfdp_extra_check(aten.div.Tensor), + ), + ( + _sfdp_pattern_12, + _sfdp_replacement_12, + [g(), g(), g(), c()], + d, + _sfdp_extra_check(aten.div.Tensor), + ), + ( + _sfdp_pattern_13, + _sfdp_replacement_13, + [g_3d(), g_3d(), g_3d()], + d, + _sfdp_params_check, + ), + ( + _sfdp_pattern_14, + _sfdp_replacement_14, + [g(), g(), g(), m(), c()], + {}, + _sfdp_extra_check(aten.div.Tensor), + ), + ( + _sfdp_pattern_15, + _sfdp_replacement_15, + [g(), g(), g(), m_2d(), c()], + {}, + _sfdp_extra_check(aten.div.Tensor), + ), + # disable_cuda only for NVIDIA CUDA (not ROCm) due to Bert accuracy issue + ( + _sfdp_pattern_16, + _sfdp_replacement_16, + [g(), g(), g(), m(), c()], + d, + _sfdp_extra_check( + aten.div.Tensor, disable_cuda=torch.version.hip is None + ), + ), + ( + _sfdp_pattern_16, + _sfdp_replacement_16, + [g_bs1(), g_bs1(), g_bs1(), m_bs1(), c()], + d, + _sfdp_extra_check( + aten.div.Tensor, disable_cuda=torch.version.hip is None + ), + ), + ( + _sfdp_pattern_17, + _sfdp_replacement_17, + [g(), g(), g(), m_2d(), c()], + d, + _sfdp_extra_check(aten.div.Tensor), + ), + ( + _sfdp_pattern_18, + _sfdp_replacement_18, + [g(), g(), g(), m_bool()], + sd, + _sfdp_params_check, + ), + ( + _sfdp_pattern_18, + _sfdp_replacement_18, + [g_bs1(), g_bs1(), g_bs1(), m_bs1_bool()], + sd, + _sfdp_params_check, + ), + ( + _sfdp_pattern_19, + _sfdp_replacement_19, + [g(), g(), g(), b_bool(), b_float()], + sd, + _sfdp_params_check, + ), + ( + _sfdp_pattern_20, + _sfdp_replacement_20, + [g(), g(), g(), m_2d()], + sd, + _sfdp_params_check, + ), + ( + _sfdp_pattern_21, + _sfdp_replacement_21, + [g(), g(), g(), m_float()], + {}, + _sfdp_params_check, + ), + ( + _sfdp_pattern_21, + _sfdp_replacement_21, + [g_bs1(), g_bs1(), g_bs1(), m_bs1_float()], + {}, + _sfdp_params_check, + ), + ( + _sfdp_pattern_22, + _sfdp_replacement_22, + [g(), g(), g(), m_float()], + {}, + _sfdp_params_check, + ), + ( + _sfdp_pattern_22, + _sfdp_replacement_22, + [g_bs1(), g_bs1(), g_bs1(), m_bs1_float()], + {}, + _sfdp_params_check, + ), + ( + _sfdp_pattern_23, + _sfdp_replacement_23, + [g(), g(), g()], + {}, + _sfdp_params_check, + ), + ( + _sfdp_pattern_23, + _sfdp_replacement_23, + [g_bs1(), g_bs1(), g_bs1()], + {}, + _sfdp_params_check, + ), + ( + _sfdp_pattern_24, + _sfdp_replacement_24, + [g(), g(), g(), b_float()], + {}, + _sfdp_extra_check, + ), + ( + _sfdp_pattern_25, + _sfdp_replacement_25, + [g(), g(), g(), m()], + d, + _sfdp_extra_check(disable_cuda=True), + ), + ( + _sfdp_pattern_25, + _sfdp_replacement_25, + [g_bs1(), g_bs1(), g_bs1(), m_bs1()], + d, + _sfdp_extra_check(disable_cuda=True), + ), + ( + _sfdp_pattern_26, + _sfdp_replacement_26, + [g(), g(), g(), m()], + d, + _sfdp_extra_check(disable_cuda=True), + ), + ( + _sfdp_pattern_26, + _sfdp_replacement_26, + [g_bs1(), g_bs1(), g_bs1(), m_bs1()], + d, + _sfdp_extra_check(disable_cuda=True), + ), + ( + _sfdp_pattern_27, + _sfdp_replacement_27, + [g(), g(), g()], + d, + _sfdp_extra_check(disable_cuda=True), + ), + ( + _sfdp_pattern_27, + _sfdp_replacement_27, + [g_bs1(), g_bs1(), g_bs1()], + d, + _sfdp_extra_check(disable_cuda=True), + ), + ( + _sfdp_pattern_28, + _sfdp_replacement_28, + [gn(), gn(), gn(), c()], + d, + _sfdp_extra_check(aten.mul.Tensor), + ), + ] + mask_fp32_patterns = ["pattern_16"] + if dtype == torch.half: + # Add inputs of bf16 q/k/v and fp32 mask, for models like albert. + candidates.append( + ( + _sfdp_pattern_16, + _sfdp_replacement_16, + [g(), g(), g(), m_float(), c()], + d, + # disable_cuda only for NVIDIA CUDA (not ROCm) due to Bert accuracy issue + _sfdp_extra_check( + aten.div.Tensor, disable_cuda=torch.version.hip is None + ), + ) + ) + candidates.append( + ( + _sfdp_pattern_16, + _sfdp_replacement_16, + [g_bs1(), g_bs1(), g_bs1(), m_bs1_float(), c()], + d, + # disable_cuda only for NVIDIA CUDA (not ROCm) due to Bert accuracy issue + _sfdp_extra_check( + aten.div.Tensor, disable_cuda=torch.version.hip is None + ), + ) + ) + + for pattern, replacement, args, workaround, extra_check in candidates: + # XXX: when adding a new pattern, re-run `gen_attention_patterns` so the pattern + # gets serialized to a python file and does not require tracing at runtime. + assert isinstance(workaround, dict) + name = pattern.__name__ + + if dtype != torch.float: + name += "_half" + if ( + any(p in name for p in mask_fp32_patterns) + and args[3].dtype == torch.float32 + ): + name += "_mask_fp32" + if args[0].size(0) == 1: + name += "_bs1" + + training_name = name + "_training" + yield ( + training_name, + { + "search_fn": pattern, + "replace_fn": replacement, + "example_inputs": args, + "trace_fn": joint_fwd_bwd, + "pass_dicts": patterns, + "extra_check": extra_check, + "scalar_workaround": workaround, + "skip_duplicates": True, + }, + ) + inference_workaround = {} + if workaround: + assert len(workaround) <= 2 + if "inv_scale" in workaround: + inference_workaround["inv_scale"] = workaround["inv_scale"] + if "dropout_p" in workaround: + # functools.partial insufficient because we look at signature downstream + pattern = partialize_and_update_signature(pattern, dropout_p=0.0) + replacement = partialize_and_update_signature( + replacement, dropout_p=0.0 + ) + + inference_name = name + "_inference" + yield ( + inference_name, + { + "search_fn": pattern, + "replace_fn": replacement, + "example_inputs": args, + "trace_fn": fwd_only, + "pass_dicts": patterns, + "extra_check": extra_check, + "scalar_workaround": inference_workaround, + # with dropout turned into clone, we end up with a number of + # semantically identical graphs + "skip_duplicates": True, + }, + ) + + +@functools.cache +def _sfdp_init(input_device: torch.device | None = None): + for key, register_replacement_kwargs in _get_sfdp_patterns(input_device): + gen_register_replacement(key, **register_replacement_kwargs) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/fusion_regions.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/fusion_regions.py new file mode 100644 index 0000000000000000000000000000000000000000..e5f4505dbb1cd38abaaf33910a9175e15203d9c5 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/fusion_regions.py @@ -0,0 +1,340 @@ +"""Detect fusion regions for overlap scheduling.""" + +import operator +from dataclasses import dataclass, field + +import torch +import torch.fx as fx +from torch._logging import trace_structured +from torch.utils._ordered_set import OrderedSet +from torch.utils._runtime_estimation import get_num_bytes + + +@dataclass +class FusionRegion: + """Represents a connected set of fusible operations that will fuse together.""" + + subgraph_node: fx.Node # The call_module node for this fusion + subgraph_module: fx.GraphModule # The subgraph module + total_bytes: int = field(default=0, init=False) # Total input + output bytes + cost_ms: float = field(default=0.0, init=False) # Estimated cost in milliseconds + + def __post_init__(self) -> None: + """Compute cost based on subgraph's placeholder inputs and output node.""" + self.total_bytes, self.cost_ms = self._compute_cost() + + def _compute_cost(self) -> tuple[int, float]: + from torch.utils._pytree import tree_flatten + from torch.utils._runtime_estimation import get_transfer_time + + subgraph = self.subgraph_module + input_vals = [ + n.meta.get("val") for n in subgraph.graph.find_nodes(op="placeholder") + ] + output_vals = [ + n.meta.get("val") + for n in torch._inductor.utils.output_node(subgraph).all_input_nodes + ] + flat_inputs, _ = tree_flatten(input_vals) + flat_outputs, _ = tree_flatten(output_vals) + + transfer_time_ns = get_transfer_time(flat_inputs, flat_outputs) + total_bytes = sum( + get_num_bytes(t) + for t in flat_inputs + flat_outputs + if isinstance(t, torch.Tensor) + ) + return total_bytes, transfer_time_ns / 1e6 + + +def is_view_node(n: fx.Node) -> bool: + """Check if a node is a view operation (zero cost, no memory allocation).""" + + return isinstance(n.target, torch._ops.OpOverload) and ( + n.target.is_view and n.target.namespace in ("aten", "prims") + ) + + +def is_fusible_node(n: fx.Node) -> bool: + """Check if a node is fusible based on whether it has an inductor lowering. + + A node is fusible if: + - It has a lowering in torch._inductor.lowering.lowerings + - It does NOT have a flop counter (expensive compute ops like mm/conv) + - It is NOT a registered fallback (ops that fall back to eager) + - It is NOT a collective or wait op + - For aten.cat, it must have <= max_pointwise_cat_inputs inputs + """ + if n.op != "call_function": + return False + + target = n.target + if not isinstance(target, torch._ops.OpOverload): + return False + + # Exclude collectives and waits (they have their own scheduling) + if target.namespace == "_c10d_functional": + return False + + from torch._inductor.lowering import fallbacks, lowerings + from torch.utils.flop_counter import flop_registry + + # Must have a lowering + if target not in lowerings: + return False + + # Exclude fallbacks (ops that fall back to eager execution) + if target in fallbacks: + return False + + # Exclude ops with flop counters (expensive compute ops like mm, conv, etc.) + overload_packet = target.overloadpacket + if overload_packet in flop_registry: + return False + + # Special case: cat is only fusible if it has few enough inputs + if target == torch.ops.aten.cat.default: + inputs = n.args[0] if n.args else [] + if isinstance(inputs, (list, tuple)): + import torch._inductor.config as inductor_config + + if len(inputs) > inductor_config.max_pointwise_cat_inputs: + return False + + return True + + +def _get_contiguous_fusible_spans(gm: fx.GraphModule) -> list[list[fx.Node]]: + """Get contiguous spans of fusible nodes from the graph. + + Walks the graph in topological order and groups consecutive fusible + nodes into spans. Non-fusible nodes act as span boundaries. + """ + spans: list[list[fx.Node]] = [] + current_span: list[fx.Node] = [] + + for node in gm.graph.nodes: + if is_fusible_node(node): + current_span.append(node) + else: + # Non-fusible node ends the current span + if current_span: + spans.append(current_span) + current_span = [] + + if current_span: + spans.append(current_span) + + return spans + + +def _find_connected_components(span: list[fx.Node]) -> list[list[fx.Node]]: + """Find connected components within a span of fusible nodes. + + Two nodes are connected if one is an input to the other (direct data dependency). + """ + if not span: + return [] + + from torch.fx.experimental.optimization import UnionFind + + span_set = OrderedSet(span) + node_to_idx = {n: i for i, n in enumerate(span)} + + uf = UnionFind(len(span)) + for i in range(len(span)): + uf.make_set(i) + + # Union nodes based on input edges + for node in span: + node_idx = node_to_idx[node] + for inp in node.all_input_nodes: + if inp in span_set: + uf.join(node_idx, node_to_idx[inp]) + + # Group by root + root_to_nodes: dict[int, list[fx.Node]] = {} + for node in span: + root = uf.find(node_to_idx[node]) + if root not in root_to_nodes: + root_to_nodes[root] = [] + root_to_nodes[root].append(node) + + return list(root_to_nodes.values()) + + +def build_fusion_regions( + gm: fx.GraphModule, +) -> dict[fx.Node, OrderedSet[fx.Node]]: + """Build fusion regions from contiguous spans of fusible nodes. + + 1. Identify contiguous spans of fusible nodes (separated by non-fusible nodes) + 2. Find connected components within each span + 3. Return regions that have 2+ non-view nodes + + This ensures fusion regions are strictly local - no reordering across + non-fusible node boundaries. + + Returns a dict mapping each node to its fusion group (OrderedSet of nodes). + """ + # Build node -> topo index map for sorting + node_to_idx: dict[fx.Node, int] = {n: i for i, n in enumerate(gm.graph.nodes)} + + # Step 1: Get contiguous spans of fusible nodes + spans = _get_contiguous_fusible_spans(gm) + + # Step 2: Find connected components within each span + region_of: dict[fx.Node, OrderedSet[fx.Node]] = {} + + for span in spans: + if len(span) < 2: + continue + + components = _find_connected_components(span) + + for component in components: + # Skip regions with fewer than 2 non-view nodes (views have no cost) + non_view_count = sum(1 for n in component if not is_view_node(n)) + if non_view_count < 2: + continue + + # Sort nodes in topological order to preserve original ordering + sorted_component = sorted(component, key=lambda n: node_to_idx[n]) + node_set = OrderedSet(sorted_component) + + for node in sorted_component: + region_of[node] = node_set + + return region_of + + +def collapse_fusion_regions( + gm: fx.GraphModule, + region_of: dict[fx.Node, OrderedSet[fx.Node]], +) -> dict[fx.Node, FusionRegion]: + """ + Collapse fusion regions into call_module nodes using fuse_by_partitions. + Returns new_region_of mapping module nodes to FusionRegions. + """ + from torch.fx.passes.utils.fuser_utils import fuse_by_partitions + + if not region_of: + return {} + + # Get unique node sets (regions with <2 nodes already filtered in build_fusion_regions) + unique_regions: list[tuple[OrderedSet[fx.Node], int]] = [] + seen_region_ids: OrderedSet[int] = OrderedSet() + for node_set in region_of.values(): + region_id = id(node_set) + if region_id not in seen_region_ids: + seen_region_ids.add(region_id) + unique_regions.append((node_set, region_id)) + + if not unique_regions: + return {} + + # Log graph before fusion + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "fusion_regions_before", + "encoding": "string", + }, + payload_fn=lambda: gm.print_readable(print_output=False), + ) + + # Build partitions list for fuse_by_partitions + partitions = [dict.fromkeys(nodes) for nodes, _ in unique_regions] + + # Fuse all partitions at once + fuse_by_partitions(gm, partitions, prefix="_fusion_region_") + + # Log graph after fusion + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "fusion_regions_after", + "encoding": "string", + }, + payload_fn=lambda: gm.print_readable(print_output=False), + ) + + # Build new_region_of by finding the call_module nodes + new_region_of: dict[fx.Node, FusionRegion] = {} + + for region_idx in range(len(unique_regions)): + subgraph_name = f"_fusion_region_{region_idx}" + + # Find the call_module node + module_nodes = list(gm.graph.find_nodes(op="call_module", target=subgraph_name)) + assert len(module_nodes) == 1, ( + f"Expected 1 call_module for {subgraph_name}, got {len(module_nodes)}" + ) + module_node = module_nodes[0] + + subgraph_module = getattr(gm, subgraph_name) + + # Create FusionRegion with all required info + region = FusionRegion( + subgraph_node=module_node, + subgraph_module=subgraph_module, + ) + + new_region_of[module_node] = region + + return new_region_of + + +def expand_fusion_regions( + gm: fx.GraphModule, + region_of: dict[fx.Node, FusionRegion], +) -> dict[fx.Node, fx.Node | None]: + """ + Expand call_module nodes back to their original nodes using _inline_module. + + Returns a mapping from erased module nodes to their replacement (last inlined node). + This is used with transfer_erased_node_deps to update dependencies. + """ + from torch.fx.experimental.const_fold import _inline_module + + result: dict[fx.Node, fx.Node | None] = {} + + if not region_of: + return result + + for module_node, region in list(region_of.items()): + if module_node.op != "call_module": + continue + + subgraph_name = module_node.target + assert isinstance(subgraph_name, str) + assert hasattr(gm, subgraph_name), ( + f"Expected submodule {subgraph_name} to exist" + ) + + # Users of module_node are get_items that will be removed from the graph + for user in module_node.users: + if user.op == "call_function" and user.target == operator.getitem: + result[user] = None + + # Get the output arg from the subgraph to determine what will replace module_node + output_arg = torch._inductor.utils.output_node(region.subgraph_module).args[0] + + # Inline the module and get the mapping from subgraph nodes to new nodes. + # Skip DCE since the graph may not be in a topo ordered state + subgraph_to_new = _inline_module(gm, subgraph_name, run_dce=False) + + # Map module_node to the replacement for the output arg + # For multi-output (tuple), use the last element (latest in topo order) + # so dependencies are only satisfied after all outputs are computed + if isinstance(output_arg, (list, tuple)): + if output_arg: + last_arg = output_arg[-1] + assert isinstance(last_arg, fx.Node) + result[module_node] = subgraph_to_new[last_arg] + elif isinstance(output_arg, fx.Node) and output_arg in subgraph_to_new: + result[module_node] = subgraph_to_new[output_arg] + + delattr(gm, subgraph_name) + + return result diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/graph_view.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/graph_view.py new file mode 100644 index 0000000000000000000000000000000000000000..58927d7ad4e12d52c512584a48c6731f9f6654a3 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/graph_view.py @@ -0,0 +1,238 @@ +from __future__ import annotations + +import itertools +import re +from typing import Any, TYPE_CHECKING + +import torch.fx as fx # noqa: TC001 +from torch.utils._ordered_set import OrderedSet + + +if TYPE_CHECKING: + from collections.abc import Callable + + +def _get_module_stack(node: fx.Node) -> list[tuple[str, type[Any]]]: + nn_stack = node.meta.get("nn_module_stack", "") + if nn_stack: + return list(nn_stack.values()) + + fwd_nn_stack = node.meta.get("fwd_nn_module_stack", "") + if fwd_nn_stack: + return list(fwd_nn_stack.values()) + + return [] + + +def _addindent(s_: str, num_spaces: int) -> str: + s: list[str] = s_.split("\n") + # don't do anything for single-line stuff + if len(s) == 1: + return s_ + first: str = s.pop(0) + s: list[str] = [(num_spaces * " ") + line for line in s] + joint_s: str = "\n".join(s) + joint_s = first + "\n" + joint_s + return joint_s + + +class GraphView: + """ + A hierarchical class for organizing and managing torch.fx nodes by their module stack. + + This class provides a tree-like structure where each node in the hierarchy corresponds + to a module or submodule in a traced FX graph. Each `GraphView` instance can hold a list + of FX nodes (`self.data`) belonging to that module scope, maintain a unique set of nodes + (`self.unique_nodes`), and manage its child containers (`self.children`). + + Attributes: + name (str): The name of the module or container scope. + klass (type[Any]): The class type associated with this module/container. + data (list[fx.Node]): A list of FX graph nodes belonging to this module. + unique_nodes (OrderedSet[fx.Node]): A deduplicated set of nodes to ensure no duplicates. + children (dict[str, GraphView]): A mapping of child module names to their corresponding GraphView instances. + """ + + def __init__(self, name: str, klass: type[Any]) -> None: + self.name: str = name + self.klass: type[Any] = klass + self.data: list[fx.Node] = [] + self.unique_nodes: OrderedSet[fx.Node] = OrderedSet() + self.children: dict[str, GraphView] = {} + + def add(self, data: fx.Node) -> None: + if data not in self.unique_nodes: + self.data.append(data) + self.unique_nodes.add(data) + + def get_child(self, module_stack: str, klass: type[Any] | None = None) -> GraphView: + if module_stack not in self.children: + new_stack = GraphView(module_stack, klass or self.klass) + self.children[module_stack] = new_stack + return self.children[module_stack] + + def __getitem__(self, name: str) -> GraphView: + return self.children[name] + + def __getattr__(self, name: str) -> GraphView: + return self.children[name] + + def __repr__(self) -> str: + child_lines: list[str] = [] + for name, child in self.children.items(): + mod_str = repr(child) + mod_str = _addindent(mod_str, 2) + child_lines.append(f"({name}): {mod_str}") + main_str = f"{self.klass.__name__}(" + if child_lines: + main_str += "\n " + "\n ".join(child_lines) + "\n" + main_str += ")" + return main_str + + +def _clean_stack_name(stack_name: str) -> str: + """ + Clean up FX node's nn_module_stack metadata string to match the module name hierarchies + + Example: + Input: "L['self']._modules['layers']['0']._modules['attention']" + Output: "layers.0.attention" + """ + cleaned = re.sub(r"^L\['self'\]\.?", "", stack_name) + parts = re.findall(r"\['([^']+)'\]", cleaned) + return ".".join(parts) if parts else cleaned + + +def _is_root(stack: str) -> bool: + return stack == "" + + +def make_graph_view( + graph: fx.Graph, + module_stack_fn: Callable[[fx.Node], list[tuple[str, type[Any]]]] | None = None, +) -> GraphView | None: + """ + Code from: https://github.com/meta-pytorch/autoparallel/pull/158 + + Make a graph view from the fx.Graph. This is a tree structure that + represents the module hierarchy of the graph, and enables us to + easily find the nodes that belong to each module, and gives a slightly + easier way of visualize different parts of the graph by extracting + subgraphs that belong to a particular module FQN. + + For example, if we have the following model with module hierarchy: + + Transformer( + (tok_embeddings): Embedding(128256, 4096) + (layers): ModuleDict( + (0): TransformerBlock( + (attention): Attention( + (wq): Linear(in_features=4096, out_features=4096, bias=False) + (wk): Linear(in_features=4096, out_features=1024, bias=False) + (wv): Linear(in_features=4096, out_features=1024, bias=False) + (wo): Linear(in_features=4096, out_features=4096, bias=False) + (sdpa): ScaledDotProductAttention() + ) + (feed_forward): FeedForward( + (w1): Linear(in_features=4096, out_features=14336, bias=False) + (w2): Linear(in_features=14336, out_features=4096, bias=False) + (w3): Linear(in_features=4096, out_features=14336, bias=False) + ) + (attention_norm): RMSNorm((4096,), eps=1e-05, elementwise_affine=True) + (ffn_norm): RMSNorm((4096,), eps=1e-05, elementwise_affine=True) + ) + ) + (norm): RMSNorm((4096,), eps=1e-05, elementwise_affine=True) + (output): Linear(in_features=4096, out_features=128256, bias=False) + ) + + Then we can get a GraphView for the fx.Graph that enables us to do + + graph_view = make_graph_view(graph) + subgraph = get_subgraph_by_path(graph_view, "layers.0") + + where subgraph contains all the nodes that belong to this region + + module_stack_fn: Optional callable for extracting module hierarchy information from nodes. + + Signature: Callable[[fx.Node], list[tuple[str, type[Any]]]] + + Takes an FX node and returns a list of (module_path, module_class) tuples representing + the nested module hierarchy for that node, ordered from outermost to innermost scope. + + - module_path (str): Dot-separated path identifying the module in the hierarchy + (e.g., "layers.0.attention.wq") + - module_class (type): The Python class type of the module + + This enables custom logic for determining module membership, useful for: + - Graphs without standard nn_module_stack metadata + - Filtering or grouping nodes by custom criteria + + Example of getting the module stack from annotation: + + def module_stack_fn(node): + module_stack = node.meta.get("custom", {}).get("module_path", "") + return [(module_stack, torch.nn.Module)] + + If None, defaults to extracting from node.meta["nn_module_stack"] or + node.meta["fwd_nn_module_stack"]. + """ + + def nn_module_stack_meta(node: fx.Node) -> list[tuple[str, type[Any]]]: + result = [] + for module_stack, module_class in _get_module_stack(node): + module_stack = _clean_stack_name(module_stack) + result.append((module_stack, module_class)) + return result + + if module_stack_fn is None: + module_stack_fn = nn_module_stack_meta + nodes: list[fx.Node] = list(graph.nodes) + nodes_by_module_stack_root: GraphView | None = None + for node in nodes: + for module_stack, module_class in module_stack_fn(node): + nodes_by_module_stack: GraphView | None = nodes_by_module_stack_root + for name in module_stack.split("."): + if nodes_by_module_stack is None: + nodes_by_module_stack = GraphView(name, module_class) + nodes_by_module_stack_root = nodes_by_module_stack + if _is_root(module_stack): + new_stack: GraphView = nodes_by_module_stack + else: + new_stack = nodes_by_module_stack.get_child(name, module_class) + nodes_by_module_stack = new_stack + nodes_by_module_stack.add(node) + + return nodes_by_module_stack_root + + +def get_subgraph_by_path( + graph_view: GraphView, paths: str | list[str] +) -> list[fx.Node]: + """ + Get subgraph by path(s). + Args: + graph_view (object): Root graph view object. + paths (str or list of str): Path(s) to subgraph. + Returns: + list[fx.Node]: fx nodes belong to the subgraph + """ + + def get_node_by_path(node: GraphView, path: str) -> GraphView: + for p in path.split("."): + if p in node.children: + node = node.children[p] + else: + return GraphView("", object) + return node + + if isinstance(paths, list): + nodes = list( + itertools.chain.from_iterable( + get_node_by_path(graph_view, p).data for p in paths + ) + ) + return nodes + else: + node = get_node_by_path(graph_view, paths) + return node.data diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/group_batch_fusion.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/group_batch_fusion.py new file mode 100644 index 0000000000000000000000000000000000000000..8782a5402538e1e7183843ec5689146ddf0736e2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/group_batch_fusion.py @@ -0,0 +1,1441 @@ +# mypy: allow-untyped-defs +import collections +import logging +import operator +from collections import OrderedDict +from collections.abc import Iterable, Iterator +from typing import Any + +import torch +from torch._dynamo.utils import counters, is_node_meta_valid +from torch._logging import trace_structured +from torch.fx.passes.graph_transform_observer import GraphTransformObserver +from torch.utils._ordered_set import OrderedSet + +from .. import config +from ..pattern_matcher import ( + CallFunctionVarArgs, + get_arg_value, + stable_topological_sort, +) +from ..utils import OPTIMUS_EXCLUDE_POST_GRAD + + +try: + # importing this will register fbgemm lowerings for inductor + import deeplearning.fbgemm.fbgemm_gpu.fb.inductor_lowerings # noqa: F401 + + has_fbgemm = True +except Exception: + has_fbgemm = False + +aten = torch.ops.aten + +log = logging.getLogger(__name__) + +DEFAULT_BETA = 1 +DEFAULT_ALPHA = 1 + +MIN_FUSE_SET_SIZE = 5 +MAX_FUSE_SET_SIZE = 300 +MAX_FUSE_SEARCH_DEPTH = 5 +# The maximum tensor size that can go into the fusion group +MAX_FUSE_TENSOR_SIZE_GROUP_LINEAR = 4096 +# Whether we only fuse nodes with same parent node +FUSE_NODES_WITH_SAME_PARENT = False +# Whether we enable the add broadcast in batch linear +SHAPE_BROADCAST_BATCH_LINEAR = False +# Whether we enable the fuse nodes with same users +Fuse_NODES_WITH_SAME_USERS = False + +# exclude these nodes from BFS +# excluding get item improves optimizer compilation time by 60s +SEARCH_EXCLUSIONS = OrderedSet([operator.getitem]) + + +default_graph_search_options = { + "min_fuse_set_size": MIN_FUSE_SET_SIZE, + "max_fuse_set_size": MAX_FUSE_SET_SIZE, + "max_fuse_search_depth": MAX_FUSE_SEARCH_DEPTH, + "max_fuse_tensor_size_group_linear": MAX_FUSE_TENSOR_SIZE_GROUP_LINEAR, + "fuse_nodes_with_same_parent": FUSE_NODES_WITH_SAME_PARENT, + "shape_broadcast_batch_linear": SHAPE_BROADCAST_BATCH_LINEAR, + "fuse_nodes_with_same_users": Fuse_NODES_WITH_SAME_USERS, +} + +graph_search_options = default_graph_search_options + + +def update_stack_example_value(node, metadata, dim=0, op=torch.stack): + """ + Update the example value of the node in the graph to enable followup split cat opt. + """ + if node is not None and hasattr(node, "meta"): + if op is torch.stack: + example_value = torch.stack(metadata, dim=dim) + elif op is torch.unbind: + example_value = torch.unbind(metadata, dim=dim) # type: ignore[assignment] + else: + return + node.meta["example_value"] = example_value + + +def update_pointwise_example_value(pointwise_node, input, other, op): + """ + Update the example value of the add node in the graph to enable followup split cat opt. + """ + if pointwise_node is not None and hasattr(pointwise_node, "meta"): + if op is torch.add: + example_value = torch.add(input, other) + elif op is torch.mul: + example_value = torch.mul(input, other) + else: + return + pointwise_node.meta["example_value"] = example_value + + +class GroupBatchFusionBase: + def __init__(self, **kwargs) -> None: + self.graph_search_options = kwargs.pop( + "graph_search_options", default_graph_search_options + ) + + def match(self, node): + raise NotImplementedError("match called on base") + + def fuse(self, graph, subset): + raise NotImplementedError("fuse called on base") + + +PRE_GRAD_FUSIONS: dict[str, GroupBatchFusionBase] = {} +POST_GRAD_FUSIONS: dict[str, GroupBatchFusionBase] = {} + + +def register_fusion(name: str, pre_grad=True): + def decorator(fusion_cls: GroupBatchFusionBase): + if pre_grad: + PRE_GRAD_FUSIONS[name] = fusion_cls + else: + POST_GRAD_FUSIONS[name] = fusion_cls + return fusion_cls + + return decorator + + +def list_group_batch_fusions(pre_grad=True) -> list[str]: + if pre_grad: + return list(PRE_GRAD_FUSIONS.keys()) + else: + return list(POST_GRAD_FUSIONS.keys()) + + +def decompose_stack(graph: torch.fx.GraphModule, input_tensors: list[Any]) -> Any: + unsqueezed_inputs = [] + unsqueezed_inputs_meta = [] + for input_tensor in input_tensors: + unsqueezed_input = graph.call_function( # type: ignore[operator] + aten.unsqueeze, args=(input_tensor,), kwargs={"dim": 0} + ) + unsqueezed_inputs.append(unsqueezed_input) + unsqueezed_input.meta["val"] = aten.unsqueeze(input_tensor.meta["val"], dim=0) # type: ignore[assignment] + unsqueezed_inputs_meta.append(unsqueezed_input.meta["val"]) + stacked_inputs = graph.call_function( # type: ignore[operator] + aten.cat, args=(unsqueezed_inputs,), kwargs={"dim": 0} + ) + stacked_inputs.meta["val"] = aten.cat(unsqueezed_inputs_meta, dim=0) # type: ignore[assignment] + return stacked_inputs + + +class GroupFusion(GroupBatchFusionBase): + """ + Fuse ops in a group way, e.g, fuse mm/addmm of arbitrary input shapes with fbgemm.gmm. + """ + + +class BatchFusion(GroupBatchFusionBase): + """ + Fuse ops in a batch way, e.g, fuse mm/addmm of same input shapes with bmm. + """ + + +class BatchPointwiseOpsFusionFactory(BatchFusion): + def __init__(self, op, **kwargs) -> None: + super().__init__(**kwargs) + self.op = op + + +@register_fusion("batch_linear_post_grad", pre_grad=False) +class PostGradBatchLinearFusion(BatchFusion): + """ + Fuse ops in a batch way in post grad (aten level). + """ + + def _addmm_node_can_be_fused(self, node: torch.fx.Node) -> bool: + # pyre-fixme[7]: Incompatible return type + return ( + # pyrefly: ignore [bad-return] + node.kwargs.get("beta", DEFAULT_BETA) == DEFAULT_BETA + and node.kwargs.get("alpha", DEFAULT_ALPHA) == DEFAULT_ALPHA # type: ignore[return-value] + ) + + def _is_input_2d(self, input: torch.fx.Node) -> bool: + input_shapes = input.meta["val"].shape + return ( + len(input_shapes) == 2 + and isinstance(input_shapes[0], int) + and isinstance(input_shapes[1], int) + ) + + def match(self, node: torch.fx.Node) -> tuple[str, int, int, int, bool, str] | None: + if CallFunctionVarArgs(aten.mm).match(node): + input_m, weight_m = node.args + bias_m = None + + elif CallFunctionVarArgs(aten.addmm.default).match( + node + ) and self._addmm_node_can_be_fused(node): + bias_m, input_m, weight_m = node.args + else: + return None + # get the user of the node + if self.graph_search_options.get("fuse_nodes_with_same_users", False): + users = [user.target for user in node.users] + else: + users = "" # type: ignore[assignment] + # only handle the cases where inputs are 2D tensors + if not self._is_input_2d(input_m) or not self._is_input_2d(weight_m): # type: ignore[arg-type] + return None + m, k = input_m.meta["val"].shape # type: ignore[union-attr] + n = weight_m.meta["val"].shape[1] # type: ignore[union-attr] + batch_key = ("batch_linear_post_grad", m, k, n, bias_m is not None, str(users)) + return batch_key + + def fuse(self, graph: torch.fx.GraphModule, subset: list[torch.fx.Node]): + batch_inputs = [] + batch_weights = [] + batch_biases = [] + batch_nodes = [] + batch_inputs_meta = [] + batch_weights_meta = [] + batch_biases_meta = [] + + for node in subset: + if CallFunctionVarArgs(aten.addmm.default).match(node): + bias, input, weight = node.args + elif CallFunctionVarArgs(aten.mm.default).match(node): + input, weight = node.args + bias = None + batch_nodes.append(node) + batch_inputs.append(input) # type: ignore[possibly-undefined] + batch_weights.append(weight) # type: ignore[possibly-undefined] + batch_biases.append(bias) # type: ignore[possibly-undefined] + batch_inputs_meta.append(input.meta) # type: ignore[possibly-undefined, union-attr] + batch_weights_meta.append(weight.meta) # type: ignore[possibly-undefined, union-attr] + if bias is not None: # type: ignore[possibly-undefined] + batch_biases_meta.append(bias.meta) # type: ignore[possibly-undefined, union-attr] + else: + batch_biases_meta.append(None) + + with graph.inserting_before(subset[-1]): # type: ignore[operator] + fused_inputs = decompose_stack(graph, batch_inputs) + fused_weights = decompose_stack(graph, batch_weights) + fused_inputs_meta_val = torch.stack( + [input["val"] for input in batch_inputs_meta] + ) + fused_weights_meta_val = torch.stack( + [weight["val"] for weight in batch_weights_meta] + ) + fused_bmm = graph.call_function( # type: ignore[operator] + aten.bmm, + args=(fused_inputs, fused_weights), + ) + fused_bmm.meta["val"] = aten.bmm( + fused_inputs_meta_val, fused_weights_meta_val + ) + for i, original_mm in enumerate(batch_nodes): + has_bias = False + with graph.inserting_after(fused_bmm): # type: ignore[operator] + new_mm = graph.call_function(aten.select, args=((fused_bmm, 0, i))) # type: ignore[operator] + new_mm.meta["val"] = aten.select(fused_bmm.meta["val"], 0, i) + if batch_biases[i]: + has_bias = True + # broadcast the bias to the same shape as the mm output + if self.graph_search_options.get( + "shape_broadcast_batch_linear", False + ): + broadcast_shape = torch.broadcast_shapes( + batch_biases_meta[i]["val"].shape, new_mm.meta["val"].shape + ) + broadcast_bias = graph.call_function( # type: ignore[operator] + aten.broadcast_to.default, + args=(batch_biases[i],), + kwargs={"size": broadcast_shape}, + ) + broadcast_bias.meta["val"] = aten.broadcast_to( + batch_biases_meta[i]["val"], broadcast_shape + ) # type: ignore[assignment] + new_bias_add = graph.call_function( # type: ignore[operator] + aten.add.Tensor, args=((broadcast_bias, new_mm)) + ) + new_bias_add.meta["val"] = aten.add.Tensor( + broadcast_bias.meta["val"], new_mm.meta["val"] + ) + else: + new_bias_add = graph.call_function( # type: ignore[operator] + aten.add, args=((batch_biases[i], new_mm)) + ) + new_bias_add.meta["val"] = aten.add.Tensor( + batch_biases_meta[i]["val"], new_mm.meta["val"] + ) + new_mm_cont = new_bias_add if has_bias else new_mm # type: ignore[possibly-undefined] + original_mm.replace_all_uses_with(new_mm_cont) + new_mm_cont.meta.update(original_mm.meta) + graph.erase_node(original_mm) # type: ignore[operator] + counters["inductor"]["batch_linear_post_grad"] += 1 + + +@register_fusion("group_linear", pre_grad=False) +class GroupLinearFusion(GroupFusion): + def _addmm_node_can_be_fused(self, node: torch.fx.Node): + input_shape = node.args[1].meta["val"].shape # type: ignore[union-attr] + weight_shape = node.args[2].meta["val"].shape # type: ignore[union-attr] + return ( + node.kwargs.get("beta", DEFAULT_BETA) == DEFAULT_BETA + and node.kwargs.get("alpha", DEFAULT_ALPHA) == DEFAULT_ALPHA + and len(input_shape) == 2 + and len(weight_shape) == 2 + and all(x % 2 == 0 for x in input_shape + weight_shape) + and all( + shape <= self.graph_search_options["max_fuse_tensor_size_group_linear"] + for shape in input_shape + weight_shape + ) + ) + + def _mm_node_can_be_fused(self, node: torch.fx.Node): + input_shape = node.args[0].meta["val"].shape # type: ignore[union-attr] + weight_shape = node.args[1].meta["val"].shape # type: ignore[union-attr] + return ( + len(input_shape) == 2 + and len(weight_shape) == 2 + and all(x % 2 == 0 for x in input_shape + weight_shape) + and all( + shape <= self.graph_search_options["max_fuse_tensor_size_group_linear"] + for shape in input_shape + weight_shape + ) + ) + + def match(self, node: torch.fx.Node) -> tuple[str, bool] | None: + if CallFunctionVarArgs(aten.mm.default).match( + node + ) and self._mm_node_can_be_fused(node): + group_key = ("group_linear", True) + elif CallFunctionVarArgs(aten.addmm.default).match( + node + ) and self._addmm_node_can_be_fused(node): + bias = node.args[0] + group_key = ("group_linear", bias is None) + else: + group_key = None + return group_key + + def fuse(self, graph: torch.fx.GraphModule, subset: list[torch.fx.Node]): + group_inputs = [] + group_weights = [] + group_biases = [] + group_nodes = [] + for node in subset: + if CallFunctionVarArgs(aten.addmm.default).match(node): + bias, input, weight = node.args + else: + assert CallFunctionVarArgs(aten.mm.default).match(node) + input, weight = node.args + bias = None + + group_nodes.append(node) + group_inputs.append(input) + group_weights.append(weight) + group_biases.append(bias) + + if all(bias is None for bias in group_biases): + group_biases = None # type: ignore[assignment] + + with graph.inserting_before(subset[0]): # type: ignore[operator] + fused_mm = graph.call_function( # type: ignore[operator] + torch.ops.fbgemm.gmm.default, + args=(group_inputs, group_weights, group_biases), + kwargs={"smart_fused": True}, + ) + + for i, original_mm in enumerate(group_nodes): + with graph.inserting_after(fused_mm): # type: ignore[operator] + new_mm = graph.call_function(operator.getitem, args=(fused_mm, i)) # type: ignore[operator] + original_mm.replace_all_uses_with(new_mm) + new_mm.meta.update(original_mm.meta) + graph.erase_node(original_mm) # type: ignore[operator] + counters["inductor"]["group_linear"] += 1 + + +class BatchPointwiseMathOpsPostGradFusion(BatchPointwiseOpsFusionFactory): + """ + Batch pointwise math operator (e.g., add, mul) in post grad pass. + """ + + def __init__(self, op, **kwargs) -> None: + super().__init__(op, **kwargs) + self.op = op + + def _pointwise_node_can_be_fused(self, node: torch.fx.Node): + # note: we only consider the case where the inputs are tensors + # for mixed precision training, we need to make sure the inputs + # of the aten.cat when do the stack should be the same dtype + # otherwise, the output of the aten.cat may be not the same as + # its inputs, and cause dtype not same error in mm or addmm + input, other = node.args + return ( + input.meta["val"].shape == other.meta["val"].shape # type: ignore[union-attr] + # input and other can be scalars, where they have no attribute 'meta' + if hasattr(input, "meta") + and hasattr(other, "meta") + and is_node_meta_valid(input) # type: ignore[arg-type, union-attr] + and is_node_meta_valid(other) # type: ignore[arg-type, union-attr] + # torch.SymInt or torch.SymFloat object has no attribute 'shape' + and isinstance(input.meta["val"], torch.Tensor) # type: ignore[union-attr] + and isinstance(other.meta["val"], torch.Tensor) # type: ignore[union-attr] + else False + ) + + def match(self, node: torch.fx.Node): + if CallFunctionVarArgs(self.op).match( + node + ) and self._pointwise_node_can_be_fused(node): + alpha = node.kwargs.get("alpha", DEFAULT_ALPHA) + rounding_mode = node.kwargs.get("rounding_mode", None) + input, other = node.args + shape = list(input.meta["val"].shape) # type: ignore[union-attr] + if self.graph_search_options.get("fuse_nodes_with_same_parent", False): + # only consider the linear case so far + # pyre-fixme[16] + if input.target is aten.select or other.target is aten.select: # type: ignore[union-attr] + parent = ( + # pyre-fixme[16] + input.args[0] # type: ignore[union-attr] + # pyre-fixme[16] + if input.target is aten.select # type: ignore[union-attr] + else other.args[0] # type: ignore[union-attr] + ) + else: + parent = "" + else: + parent = "" + group_key = ( + "batch_aten_" + self.op.__name__.lower().split(".")[0], + str(shape), + str(input.meta["val"].dtype), # type: ignore[union-attr] + str(other.meta["val"].dtype), # type: ignore[union-attr] + str(alpha), + str(rounding_mode), + str(parent), + ) + else: + group_key = None + return group_key + + def fuse(self, graph: torch.fx.GraphModule, subset: list[torch.fx.Node]): + batch_inputs, batch_others = [], [] + alpha = subset[0].kwargs.get("alpha", DEFAULT_ALPHA) + batch_inputs_meta, batch_others_meta = [], [] + + for node in subset: + input, other = node.args + batch_inputs.append(input) + batch_others.append(other) + batch_inputs_meta.append(input.meta) # type: ignore[possibly-undefined, union-attr] + batch_others_meta.append(other.meta) # type: ignore[possibly-undefined, union-attr] + + with graph.inserting_before(subset[0]): # type: ignore[operator] + stack_inputs = decompose_stack(graph, batch_inputs) + stack_others = decompose_stack(graph, batch_others) + stack_inputs_meta = torch.stack( + [input["val"] for input in batch_inputs_meta] + ) + stack_others_meta = torch.stack( + [other["val"] for other in batch_others_meta] + ) + + batch_op = graph.call_function( # type: ignore[operator] + self.op, + args=(stack_inputs, stack_others), + kwargs={"alpha": alpha} if self.op == aten.add.Tensor else {}, + ) + batch_op.meta["val"] = self.op(stack_inputs_meta, stack_others_meta) + for i, original_add in enumerate(subset): + with graph.inserting_after(batch_op): # type: ignore[operator] + new_add = graph.call_function( # type: ignore[operator] + torch.ops.aten.select, args=((batch_op, 0, i)) + ) + original_add.replace_all_uses_with(new_add) + new_add.meta.update(original_add.meta) + graph.erase_node(original_add) # type: ignore[operator] + counters["inductor"][ + "batch_aten_" + self.op.__name__.lower().split(".")[0] + ] += 1 + + +@register_fusion("batch_linear_lhs") +class BatchLinearLHSFusion(BatchFusion): + """ + Batch linear left-hand side fusion. This pass tries to fuse the following patterns: + + torch.nn.functional.linear(x, w1), linear(x, w2),... * linear(x, wn) + -> torch.mm(x, torch.cat([w1, w2,... * wn]).transpose(0, 1)) + + We have a separate pass to eliminate contiguous transpose in a generic way. + """ + + def match(self, node: torch.fx.Node) -> tuple[str, bool, Any] | None: + if CallFunctionVarArgs(torch.nn.functional.linear).match( + node + ) and is_linear_node_can_be_fused(node): + input = get_arg_value(node, 0, "input") + bias = get_arg_value(node, 2, "bias") + group_key = ("batch_linear_lhs", bias is None, input) + else: + group_key = None + return group_key + + def fuse(self, graph: torch.fx.GraphModule, subset: list[torch.fx.Node]): + batch_nodes = [] + batch_input = None + batch_weights, batch_weights_meta = [], [] + batch_biases, batch_biases_meta = [], [] + split_sections = [] + for node in subset: + input = get_arg_value(node, 0, "input") + weight = get_arg_value(node, 1, "weight") + bias = get_arg_value(node, 2, "bias") + batch_nodes.append(node) + if batch_input is None: + batch_input = input + else: + assert batch_input is input + batch_weights.append(weight) + batch_weights_meta.append(weight.meta["example_value"]) + if bias: + batch_biases.append(bias) + batch_biases_meta.append(bias.meta["example_value"]) + split_sections.append(weight.meta["example_value"].shape[0]) + + with graph.inserting_before(subset[0]): # type: ignore[operator] + cat_weights = graph.call_function( # type: ignore[operator] + torch.cat, args=(batch_weights,), kwargs={"dim": 0} + ) + cat_weights.meta["example_value"] = torch.cat(batch_weights_meta, dim=0) + transposed_weights = graph.call_function( # type: ignore[operator] + torch.transpose, args=(cat_weights, 0, 1) + ) + transposed_weights.meta["example_value"] = torch.transpose( + cat_weights.meta["example_value"], 0, 1 + ) + if len(batch_biases) > 0: + cat_biases = graph.call_function( # type: ignore[operator] + torch.cat, args=(batch_biases,), kwargs={"dim": 0} + ) + cat_biases.meta["example_value"] = torch.cat(batch_biases_meta, dim=0) + fused_lhs = graph.call_function( # type: ignore[operator] + torch.addmm, + args=(cat_biases, batch_input, transposed_weights), + ) + fused_lhs.meta["example_value"] = torch.addmm( + cat_biases.meta["example_value"], + batch_input.meta["example_value"], # type: ignore[union-attr] + transposed_weights.meta["example_value"], + ) + else: + fused_lhs = graph.call_function( # type: ignore[operator] + torch.mm, + args=(batch_input, transposed_weights), + ) + fused_lhs.meta["example_value"] = torch.mm( + batch_input.meta["example_value"], # type: ignore[union-attr] + transposed_weights.meta["example_value"], + ) + fused_lhs_list = graph.call_function( # type: ignore[operator] + torch.split, args=(fused_lhs, split_sections), kwargs={"dim": 1} + ) + + for i, node in enumerate(batch_nodes): + with graph.inserting_after(fused_lhs_list): # type: ignore[operator] + new_node = graph.call_function( # type: ignore[operator] + operator.getitem, args=(fused_lhs_list, i) + ) + node.replace_all_uses_with(new_node) + new_node.meta.update(node.meta) + graph.erase_node(node) # type: ignore[operator] + counters["inductor"]["batch_linear_lhs"] += 1 + + +# Poor person's check for if a node in the graph mutates its input. +# (the graph is torch IR, so we will see torch fns and python operators) +def _is_mutable_node(tgt): + if str(tgt).endswith("_"): + # e.g. torch.mul_, torch.Tensor.mul_ + return True + if ( + hasattr(tgt, "__module__") + and tgt.__module__ == "_operator" + and tgt.__name__.startswith("i") + ): + # e.g. operator.iand, operator.imul + return True + return False + + +def is_linear_node_can_be_fused(node: torch.fx.Node): + input = get_arg_value(node, 0, "input") + weight = get_arg_value(node, 1, "weight") + return ( + is_node_meta_valid(node) + and is_node_meta_valid(input) + and is_node_meta_valid(weight) + and len(input.meta["example_value"].shape) == 2 + and len(weight.meta["example_value"].shape) == 2 + # the mm -> bmm transform adds an unbind() op, + # which is not safe for autograd when the output of the mm is mutated. + # don't pattern match if any users of the mm mutate the input. + and not any(_is_mutable_node(user.target) for user in node.users) + ) + + +@register_fusion("batch_linear") +class PreGradBatchLinearFusion(BatchFusion): + """ + Batch linear fusion in pre grad pass. + Fuse linear with same size with torch.baddmm + """ + + def _getitem_args(self, getitem_node: torch.fx.Node): + if getitem_node.target != operator.__getitem__ or ( + getitem_node.op != "call_function" + ): + return None + return getitem_node.args[0] + + def match(self, node: torch.fx.Node): + if CallFunctionVarArgs(torch.nn.functional.linear).match( + node + ) and is_linear_node_can_be_fused(node): + input = get_arg_value(node, 0, "input") + weight = get_arg_value(node, 1, "weight") + bias = get_arg_value(node, 2, "bias") + if self.graph_search_options.get("fuse_nodes_with_same_users", False): + users = [user.target for user in node.users] + else: + users = "" # type: ignore[assignment] + group_key = ( + "batch_linear", + self._getitem_args(input), + str(input.meta["example_value"].shape), + str(weight.meta["example_value"].shape), + bias is None, + str(users), + ) + else: + group_key = None + return group_key + + def fuse(self, graph: torch.fx.GraphModule, subset: list[torch.fx.Node]): + batch_nodes = [] + batch_inputs = [] + batch_weights = [] + batch_biases = [] + batch_inputs_metadata = [] + batch_weights_metadata = [] + batch_biases_metadata = [] + for node in subset: + batch_nodes.append(node) + input = get_arg_value(node, 0, "input") + batch_inputs.append(input) + batch_inputs_metadata.append(input.meta["example_value"]) + weight = get_arg_value(node, 1, "weight") + batch_weights.append(weight) + batch_weights_metadata.append(weight.meta["example_value"]) + bias = get_arg_value(node, 2, "bias") + batch_biases.append(bias) + if bias is not None and hasattr(bias, "meta"): + batch_biases_metadata.append(bias.meta["example_value"]) + + with graph.inserting_before(subset[0]): # type: ignore[operator] + stack_inputs = graph.call_function( # type: ignore[operator] + torch.stack, args=(batch_inputs,), kwargs={"dim": 0} + ) + update_stack_example_value(stack_inputs, batch_inputs_metadata) + stack_weights = graph.call_function( # type: ignore[operator] + torch.stack, args=(batch_weights,), kwargs={"dim": 0} + ) + update_stack_example_value(stack_weights, batch_weights_metadata) + transpose_weight = graph.call_function( # type: ignore[operator] + torch.transpose, args=(stack_weights, 1, 2) + ) + transpose_weight.meta["example_value"] = torch.transpose( + stack_weights.meta["example_value"], 1, 2 + ) + if all(bias is None for bias in batch_biases): + bmm = graph.call_function( # type: ignore[operator] + torch.bmm, + args=(stack_inputs, transpose_weight), + ) + bmm.meta["example_value"] = torch.bmm( + stack_inputs.meta["example_value"], + transpose_weight.meta["example_value"], + ) + bmm_meta = bmm.meta["example_value"] + else: + stack_biases = graph.call_function( # type: ignore[operator] + torch.stack, args=(batch_biases,), kwargs={"dim": 0} + ) + update_stack_example_value(stack_biases, batch_biases_metadata) + unsqueeze_biases = graph.call_function( # type: ignore[operator] + torch.unsqueeze, args=(stack_biases, 1) + ) + unsqueeze_biases.meta["example_value"] = torch.unsqueeze( + stack_biases.meta["example_value"], 1 + ) + bmm = graph.call_function( # type: ignore[operator] + torch.baddbmm, + args=(unsqueeze_biases, stack_inputs, transpose_weight), + ) + try: + # it will have runtime error to broadcast when it has dynamic shape included + # in the meta data, so we need to skip the update meta data + bmm.meta["example_value"] = torch.baddbmm( + unsqueeze_biases.meta["example_value"], + stack_inputs.meta["example_value"], + transpose_weight.meta["example_value"], + ) + bmm_meta = bmm.meta["example_value"] + except Exception as e: + log.debug( + f" exception when update bmm meta data with stack error tracekey {e}" # noqa: G004 + ) + bmm_meta = None + + bmm = graph.call_function(torch.unbind, args=(bmm,), kwargs={"dim": 0}) # type: ignore[operator] + if bmm_meta is not None: + bmm.meta["example_value"] = torch.unbind(bmm_meta, dim=0) + for i, linear in enumerate(batch_nodes): + with graph.inserting_after(bmm): # type: ignore[operator] + getitem = graph.call_function(operator.getitem, args=(bmm, i)) # type: ignore[operator] + linear.replace_all_uses_with(getitem) + getitem.meta.update(linear.meta) + graph.erase_node(linear) # type: ignore[operator] + counters["inductor"]["batch_linear"] += 1 + + +@register_fusion("batch_layernorm") +class BatchLayernormFusion(BatchFusion): + """ + Batch layer norm fusion in pre grad pass + """ + + def match(self, node: torch.fx.Node): + if CallFunctionVarArgs(torch.nn.functional.layer_norm).match(node): + input = get_arg_value(node, 0, "input") + weight = get_arg_value(node, 2, "weight") + bias = get_arg_value(node, 3, "bias") + if self.graph_search_options.get("fuse_nodes_with_same_users", False): + users = [user.target for user in node.users] + else: + users = "" # type: ignore[assignment] + group_key = ( + ( + "batch_layernorm", + str(input.meta["example_value"].shape), + str(weight.meta["example_value"].shape) + if weight is not None + else "", + str(bias.meta["example_value"].shape) if bias is not None else "", + str(get_arg_value(node, 1, "normalized_shape")), + str(get_arg_value(node, 4, "eps")), + str(users), + ) + if "example_value" in input.meta + and is_node_meta_valid(weight) + and is_node_meta_valid(bias) + else None + ) + else: + group_key = None + return group_key + + def fuse(self, graph: torch.fx.GraphModule, subset: list[torch.fx.Node]): + group_inputs = [] + group_shapes = [] + group_weights = [] + group_biases = [] + group_epss = [] + group_nodes = [] + group_inputs_metadata = [] + group_biases_metadata = [] + group_weights_metadata = [] + for node in subset: + group_nodes.append(node) + input = get_arg_value(node, 0, "input") + group_inputs.append(input) + group_inputs_metadata.append(input.meta["example_value"]) + group_shapes.append(get_arg_value(node, 1, "normalized_shape")) + weight = get_arg_value(node, 2, "weight") + group_weights.append(weight) + if weight is not None and hasattr(weight, "meta"): + group_weights_metadata.append(weight.meta["example_value"]) + bias = get_arg_value(node, 3, "bias") + group_biases.append(bias) + if bias is not None and hasattr(bias, "meta"): + group_biases_metadata.append(bias.meta["example_value"]) + eps = get_arg_value(node, 4, "eps") + if eps is None: + eps = 1e-5 + group_epss.append(eps) + stack_dim = -1 - len(group_shapes[-1]) + + if all(bias is None for bias in group_biases): + group_biases = None # type: ignore[assignment] + if all(weight is None for weight in group_weights): + group_weights = None # type: ignore[assignment] + assert all(eps == group_epss[0] for eps in group_epss), ( + "all epsilon values must be equal" + ) + + with graph.inserting_before(subset[0]): # type: ignore[operator] + stack_input = graph.call_function( # type: ignore[operator] + torch.stack, args=(group_inputs,), kwargs={"dim": stack_dim} + ) + update_stack_example_value(stack_input, group_inputs_metadata, stack_dim) + if group_weights is not None: + stack_weight = graph.call_function( # type: ignore[operator] + torch.stack, args=(group_weights,), kwargs={"dim": 0} + ) + update_stack_example_value(stack_weight, group_weights_metadata) + else: + stack_weight = None + if group_biases is not None: + stack_bias = graph.call_function( # type: ignore[operator] + torch.stack, args=(group_biases,), kwargs={"dim": 0} + ) + update_stack_example_value(stack_bias, group_biases_metadata) + else: + stack_bias = None + + batch_layer_norm = graph.call_function( # type: ignore[operator] + torch.nn.functional.layer_norm, + args=(stack_input, group_shapes[-1]), + kwargs={"eps": group_epss[-1]}, + ) + batch_layer_norm.meta["example_value"] = stack_input.meta["example_value"] + + if group_weights is not None and group_biases is not None: + previous_batch_layer_norm_meta = batch_layer_norm.meta["example_value"] + batch_layer_norm = graph.call_function( # type: ignore[operator] + torch.mul, args=(stack_weight, batch_layer_norm) + ) + update_pointwise_example_value( + batch_layer_norm, + # pyrefly: ignore [missing-attribute] + stack_weight.meta["example_value"], + previous_batch_layer_norm_meta, + torch.mul, + ) + previous_batch_layer_norm_meta = batch_layer_norm.meta["example_value"] + batch_layer_norm = graph.call_function( # type: ignore[operator] + torch.add, args=(stack_bias, batch_layer_norm) + ) + update_pointwise_example_value( + batch_layer_norm, + # pyrefly: ignore [missing-attribute] + stack_bias.meta["example_value"], + previous_batch_layer_norm_meta, + torch.add, + ) + elif group_weights is not None and group_biases is None: + previous_batch_layer_norm_meta = batch_layer_norm.meta["example_value"] + # pyrefly: ignore [not-callable] + batch_layer_norm = graph.call_function( + torch.mul, args=(stack_weight, batch_layer_norm) + ) + update_pointwise_example_value( + batch_layer_norm, + # pyrefly: ignore [missing-attribute] + stack_weight.meta["example_value"], + previous_batch_layer_norm_meta, + torch.mul, + ) + elif group_weights is None and group_biases is not None: + previous_batch_layer_norm_meta = batch_layer_norm.meta["example_value"] + # pyrefly: ignore [not-callable] + batch_layer_norm = graph.call_function( + torch.add, args=(stack_bias, batch_layer_norm) + ) + update_pointwise_example_value( + batch_layer_norm, + # pyrefly: ignore [missing-attribute] + stack_bias.meta["example_value"], + previous_batch_layer_norm_meta, + torch.add, + ) + + batch_layer_norm_unbind = graph.call_function( # type: ignore[operator] + torch.unbind, + args=(batch_layer_norm,), + kwargs={"dim": stack_dim}, + ) + update_stack_example_value( + batch_layer_norm_unbind, + batch_layer_norm.meta["example_value"], + op=torch.unbind, + dim=stack_dim, + ) + + for i, node in enumerate(group_nodes): + with graph.inserting_after(batch_layer_norm_unbind): # type: ignore[operator] + new_node = graph.call_function( # type: ignore[operator] + operator.getitem, args=(batch_layer_norm_unbind, i) + ) + node.replace_all_uses_with(new_node) + new_node.meta.update(node.meta) + graph.erase_node(node) # type: ignore[operator] + counters["inductor"]["batch_layernorm"] += 1 + + +class BatchPointwiseOpsPreGradFusion(BatchPointwiseOpsFusionFactory): + """ + Batch pointwise ops (e.g., sigmoid, relu, tanh) fusion in pre grad pass. + We fuse it in random place, and the introduced stack node may be merged in split cat. + """ + + def __init__(self, op, **kwargs) -> None: + super().__init__(op, **kwargs) + self.op = op + + def match(self, node: torch.fx.Node): + input = get_arg_value(node, 0, "input") + if CallFunctionVarArgs(self.op).match(node) and is_node_meta_valid(node): + if self.graph_search_options.get("fuse_nodes_with_same_parent", False): + # pyre-fixme[16] + parent = node.args[0] + parent = parent.target if parent is not None else "" # type: ignore[union-attr] + else: + parent = "" + # for relu op, we also use the inplace to construct the key + group_key = ( + "batch_" + self.op.__name__.lower().split(".")[0], + str(input.meta["example_value"].shape), + str(node.kwargs.get("inplace", False)), + str(parent), + ) + else: + group_key = None + return group_key + + def fuse(self, graph: torch.fx.GraphModule, subset: list[torch.fx.Node]): + batch_nodes = [] + batch_inputs = [] + batch_inputs_metadata = [] + + for node in subset: + batch_nodes.append(node) + input = get_arg_value(node, 0, "input") + batch_inputs.append(input) + batch_inputs_metadata.append(input.meta["example_value"]) + + with graph.inserting_before(subset[0]): # type: ignore[operator] + stack_inputs = graph.call_function( # type: ignore[operator] + torch.stack, args=(batch_inputs,), kwargs={"dim": 0} + ) + update_stack_example_value(stack_inputs, batch_inputs_metadata) + if self.op is torch.nn.functional.relu: + batch_op = graph.call_function( # type: ignore[operator] + self.op, + args=(stack_inputs,), + kwargs={"inplace": subset[0].kwargs.get("inplace", False)}, + ) + batch_op.meta["example_value"] = self.op( + stack_inputs.meta["example_value"], + # pyrefly: ignore [bad-argument-type] + inplace=subset[0].kwargs.get("inplace", False), + ) + else: + batch_op = graph.call_function( # type: ignore[operator] + self.op, + args=(stack_inputs,), + ) + batch_op.meta["example_value"] = self.op( + stack_inputs.meta["example_value"] + ) + unbind_op = graph.call_function( # type: ignore[operator] + torch.unbind, args=(batch_op,), kwargs={"dim": 0} + ) + unbind_op.meta["example_value"] = torch.unbind( + batch_op.meta["example_value"], dim=0 + ) + for i, node in enumerate(batch_nodes): + with graph.inserting_after(unbind_op): # type: ignore[operator] + getitem = graph.call_function(operator.getitem, args=(unbind_op, i)) # type: ignore[operator] + node.replace_all_uses_with(getitem) + getitem.meta.update(node.meta) + graph.erase_node(node) # type: ignore[operator] + counters["inductor"]["batch_" + self.op.__name__.lower().split(".")[0]] += 1 + + +class BatchPointwiseOpsPostGradFusion(BatchPointwiseOpsFusionFactory): + """ + Batch pointwise ops (e.g., sigmoid, relu, tanh) fusion in post grad pass. + The introduced stack node may be merged in split cat. + """ + + def __init__(self, op, **kwargs) -> None: + super().__init__(op, **kwargs) + self.op = op + + def match(self, node: torch.fx.Node): + input = get_arg_value(node, 0, "input") + if CallFunctionVarArgs(self.op).match(node) and is_node_meta_valid(node): + # for relu op, we also use the inplace to construct the key + # we batch the ops with same parent to enable followup split cat + parent = node.args[0] + parent = ( + parent.target # type: ignore[union-attr] + if self.graph_search_options.get("fuse_nodes_with_same_parent", False) + else "" + ) + group_key = ( + "batch_aten_" + self.op.__name__.lower().split(".")[0], + str(input.meta["val"].shape), + str(node.kwargs.get("inplace", False)), + # pyre-fixme[16] + str(parent), + ) + else: + group_key = None + return group_key + + def fuse(self, graph: torch.fx.GraphModule, subset: list[torch.fx.Node]): + batch_nodes = [] + batch_inputs = [] + batch_inputs_metadata = [] + + for node in subset: + batch_nodes.append(node) + input = get_arg_value(node, 0, "input") + batch_inputs.append(input) + batch_inputs_metadata.append(input.meta["val"]) + + with graph.inserting_before(subset[0]): # type: ignore[operator] + stack_inputs = decompose_stack(graph, batch_inputs) + update_stack_example_value(stack_inputs, batch_inputs_metadata) + batch_op = graph.call_function( # type: ignore[operator] + self.op, + args=(stack_inputs,), + ) + for i, node in enumerate(batch_nodes): + with graph.inserting_after(batch_op): # type: ignore[operator] + getitem = graph.call_function(aten.select, args=(batch_op, 0, i)) # type: ignore[operator] + node.replace_all_uses_with(getitem) + getitem.meta.update(node.meta) + graph.erase_node(node) # type: ignore[operator] + counters["inductor"][ + "batch_aten_" + self.op.__name__.lower().split(".")[0] + ] += 1 + + +class BatchMathOpsPreGradFusion(BatchPointwiseOpsFusionFactory): + """ + Batch simple match related ops such as nan_to_num in pre grad pass. + """ + + def __init__(self, op, **kwargs): + super().__init__(op, **kwargs) + self.op = op + + def match(self, node: torch.fx.Node): + input = get_arg_value(node, 0, "input") + if CallFunctionVarArgs(self.op).match(node) and is_node_meta_valid(node): + # check the input has the same shape and its users have the same target + # check all clamp operators have the same min and max values, and + # nan_to_num operators use the same default value. + child = next(iter(node.users.keys())) + group_key = ( + str(input.meta["example_value"].shape) + + str(node.kwargs) + + str(child.target) + ) + else: + group_key = None + return group_key + + def fuse(self, graph: torch.fx.GraphModule, subset: list[torch.fx.Node]): + batch_nodes = [] + batch_inputs = [] + batch_inputs_metadata = [] + kwargs = subset[0].kwargs + + for node in subset: + batch_nodes.append(node) + input = get_arg_value(node, 0, "input") + batch_inputs.append(input) + batch_inputs_metadata.append(input.meta["example_value"]) + + with graph.inserting_before(subset[0]): # type: ignore[operator] + stack_inputs = graph.call_function( # type: ignore[operator] + torch.stack, args=(batch_inputs,), kwargs={"dim": 0} + ) + update_stack_example_value(stack_inputs, batch_inputs_metadata) + batch_op = graph.call_function( # type: ignore[operator] + self.op, + args=(stack_inputs,), + kwargs=kwargs, + ) + batch_op.meta["example_value"] = self.op( + stack_inputs.meta["example_value"], **kwargs + ) + unbind_op = graph.call_function( # type: ignore[operator] + torch.unbind, args=(batch_op,), kwargs={"dim": 0} + ) + unbind_op.meta["example_value"] = torch.unbind( + batch_op.meta["example_value"], dim=0 + ) + for i, node in enumerate(batch_nodes): + with graph.inserting_after(unbind_op): # type: ignore[operator] + getitem = graph.call_function(operator.getitem, args=(unbind_op, i)) # type: ignore[operator] + node.replace_all_uses_with(getitem) + getitem.meta.update(node.meta) + graph.erase_node(node) # type: ignore[operator] + counters["inductor"]["batch_" + self.op.__name__.lower().split(".")[0]] += 1 + + +@register_fusion("batch_tanh") +class BatchTanhPreGradFusion(BatchPointwiseOpsPreGradFusion): + def __init__(self, **kwargs) -> None: + super().__init__(torch.tanh, **kwargs) + + +@register_fusion("batch_sigmoid") +class BatchSigmoidPreGradFusion(BatchPointwiseOpsPreGradFusion): + def __init__(self, **kwargs) -> None: + super().__init__(torch.sigmoid, **kwargs) + + +@register_fusion("batch_relu") +class BatchReLuPreGradFusion(BatchPointwiseOpsPreGradFusion): + def __init__(self, **kwargs) -> None: + super().__init__(torch.nn.functional.relu, **kwargs) + + +@register_fusion("batch_detach") +class BatchDetachPreGradFusion(BatchMathOpsPreGradFusion): + def __init__(self, **kwargs): + super().__init__(torch.detach, **kwargs) + + +@register_fusion("batch_nan_to_num") +class BatchNanToNumPreGradFusion(BatchMathOpsPreGradFusion): + def __init__(self, **kwargs): + super().__init__(torch.nan_to_num, **kwargs) + + +@register_fusion("batch_clamp") +class BatchClampPreGradFusion(BatchMathOpsPreGradFusion): + def __init__(self, **kwargs): + super().__init__(torch.clamp, **kwargs) + + +@register_fusion("batch_dropout") +class BatchDropoutPreGradFusion(BatchMathOpsPreGradFusion): + def __init__(self, **kwargs): + super().__init__(torch.nn.functional.dropout, **kwargs) + + +@register_fusion("batch_aten_tanh", pre_grad=False) +class BatchTanhPostGradFusion(BatchPointwiseOpsPostGradFusion): + def __init__(self, **kwargs) -> None: + super().__init__(aten.tanh.default, **kwargs) + + +@register_fusion("batch_aten_sigmoid", pre_grad=False) +class BatchSigmoidPostGradFusion(BatchPointwiseOpsPostGradFusion): + def __init__(self, **kwargs) -> None: + super().__init__(aten.sigmoid.default, **kwargs) + + +@register_fusion("batch_aten_relu", pre_grad=False) +class BatchReLuPostGradFusion(BatchPointwiseOpsPostGradFusion): + def __init__(self, **kwargs) -> None: + super().__init__(aten.relu.default, **kwargs) + + +@register_fusion("batch_aten_add", pre_grad=False) +class BatchAddPostGradFusion(BatchPointwiseMathOpsPostGradFusion): + def __init__(self, **kwargs) -> None: + super().__init__(aten.add.Tensor, **kwargs) + + +@register_fusion("batch_aten_sub", pre_grad=False) +class BatchSubPostGradFusion(BatchPointwiseMathOpsPostGradFusion): + def __init__(self, **kwargs) -> None: + super().__init__(aten.sub.Tensor, **kwargs) + + +@register_fusion("batch_aten_div", pre_grad=False) +class BatchDivPostGradFusion(BatchPointwiseMathOpsPostGradFusion): + def __init__(self, **kwargs) -> None: + super().__init__(aten.div.Tensor, **kwargs) + + +@register_fusion("batch_aten_mul", pre_grad=False) +class BatchMulPostGradFusion(BatchPointwiseMathOpsPostGradFusion): + def __init__(self, **kwargs) -> None: + super().__init__(aten.mul.Tensor, **kwargs) + + +class _OrderedSet: + def __init__(self, param=None) -> None: + if param: + self.rep = OrderedDict(dict.fromkeys(param)) + else: + self.rep = OrderedDict() + + def __contains__(self, o) -> bool: + return o in self.rep + + def __len__(self) -> int: + return self.rep.__len__() + + def append(self, o): + self.rep[o] = None + + def __iter__(self): + return self.rep.keys().__iter__() + + +def find_independent_subset_greedy( + node_list: Iterable[torch.fx.Node], + graph_search_options: dict[str, Any], +) -> Iterator[Iterable[torch.fx.Node]]: + """ + Yields a list of subsets of `node_list` where no element in the subset + depends on any other element in the subset. This results in a set of + independent nodes which can be fused together. + + The order of `node_list` is preserved within each subset so we can benefit + from split-cat elimination in later passes. + + During iteration it is only safe to mutate the graph by changing the nodes + that have been returned. + + graph_search_options: + - min_fuse_set_size: Minimum size of the subset to consider. Subsets below + this size will be ignored. + - max_fuse_set_size: Maximum size of the subset to consider. Subsets will + be broken to be at most this size. + """ + + # Compute all the children of `node` which are members of + # `interesting_nodes`. + def find_dependent_nodes(node, interesting_nodes): + visited_node_set = OrderedSet[torch.fx.Node]() + dep_set = OrderedSet[torch.fx.Node]() + + work = [node] + while work: + node = work.pop() + for input_node in node.all_input_nodes: + if input_node in interesting_nodes: + dep_set.add(input_node) + + if input_node not in visited_node_set: + visited_node_set.add(input_node) + work.append(input_node) + + return dep_set + + min_fuse_set_size = graph_search_options["min_fuse_set_size"] + max_fuse_set_size = graph_search_options["max_fuse_set_size"] + + # node_list needs to be a set because we only track the nodes that are left + # in it (and we want to do the `in` on a set, not a list). But we want to + # keep the correct order. + node_list = _OrderedSet(node_list) + + cache: dict[torch.fx.Node, OrderedSet[torch.fx.Node]] = {} + while node_list: + subset: list[torch.fx.Node] = [] + subset_deps = OrderedSet[torch.fx.Node]() + + next_round_node_list = _OrderedSet() + for node in node_list: + if len(subset) >= max_fuse_set_size or node in subset_deps: + next_round_node_list.append(node) + continue + + dep_set = cache.pop(node, None) + if dep_set is None: + dep_set = find_dependent_nodes(node, node_list) + + if not dep_set.intersection(subset): + subset.append(node) + subset_deps.update(dep_set) + else: + next_round_node_list.append(node) + cache[node] = dep_set + + if len(subset) >= min_fuse_set_size: + # Careful here - the caller uses the subsets to fuse nodes together + # so we need to clear any cache entry that contains one of the + # returned nodes because the dependency list could be different + # (larger) after the merge. + cache = {k: v for k, v in cache.items() if v.isdisjoint(subset)} + yield subset + + node_list = next_round_node_list + + +def get_fusion_candidates( + rule: GroupBatchFusionBase, + root_node: torch.fx.Node, + fused_set: OrderedSet[torch.fx.Node], +) -> collections.defaultdict[Any, list[torch.fx.Node]]: + """ + Search fusion candidates for a specific rule using BFS starting from the root node. + We only search the subgraph within graph_search_options["max_fuse_search_depth"]. + """ + q: collections.deque[tuple[int, torch.fx.Node]] = collections.deque() + + candidate_dict: collections.defaultdict[Any, list[torch.fx.Node]] = ( + collections.defaultdict(list) + ) + + if root_node.target in SEARCH_EXCLUSIONS: + return candidate_dict + + visited_set = OrderedSet[torch.fx.Node]() + + for next_node in root_node.all_input_nodes: + q.append((1, next_node)) + visited_set.add(next_node) + + while len(q) > 0: + depth, node = q.popleft() + + if node in fused_set: + continue + + key = rule.match(node) + if key is not None: + candidate_nodes = candidate_dict[key] + if node not in candidate_nodes: + candidate_nodes.append(node) + else: + if depth < rule.graph_search_options["max_fuse_search_depth"]: + for next_node in node.all_input_nodes: + if next_node not in visited_set: + visited_set.add(next_node) + q.append((depth + 1, next_node)) + + return candidate_dict + + +def apply_group_batch_fusion(graph: torch.fx.GraphModule, rule: GroupBatchFusionBase): + stable_topological_sort(graph) # type: ignore[arg-type] + fused_set = OrderedSet[torch.fx.Node]() + log_to_scuba = False + + for node in reversed(graph.nodes): # type: ignore[arg-type] + candidates = get_fusion_candidates(rule, node, fused_set) + + for key, candidate_nodes in candidates.items(): + if len(candidate_nodes) < rule.graph_search_options["min_fuse_set_size"]: + continue + + for subset in find_independent_subset_greedy( + candidate_nodes, rule.graph_search_options + ): + rule.fuse(graph, subset) + fused_set.update(subset) + log.debug( + f"{rule.__class__.__name__}: key = {key}; subset size = {len(list(subset))}" # noqa: G004 + ) + log_to_scuba = True + if log_to_scuba: + from torch.fx._lazy_graph_module import _LazyGraphModule + + # Force graph to re-compile otherwise the output python code may be broken + gm = graph._owning_module + if isinstance(gm, _LazyGraphModule): + _LazyGraphModule.recompile() + else: + assert isinstance(gm, torch.fx.GraphModule) + gm.recompile() + graph_str = gm.print_readable( + print_output=False, include_stride=True, include_device=True + ) + + name = f"optimus_{str(rule.__class__.__name__)}" + if "MTIA" in name: + name = f"cff_{str(rule.__class__.__name__)}" + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": name, + "encoding": "string", + }, + payload_fn=lambda: graph_str, + ) + + +def generate_fusion_from_config(config_options: dict[str, Any], pre_grad=True): + fusions: list[GroupBatchFusionBase] = [] + for name, options in config_options.items(): + # we skip all patterns from pattern_matcher passes (e.g., split_cat) + if name not in PRE_GRAD_FUSIONS and name not in POST_GRAD_FUSIONS: + continue + fusion_cls = PRE_GRAD_FUSIONS[name] if pre_grad else POST_GRAD_FUSIONS[name] + _options = graph_search_options.copy() + _options.update(options) + fusions.append(fusion_cls(graph_search_options=_options)) # type: ignore[operator] + return fusions + + +def group_batch_fusion_passes(graph: torch.fx.Graph, pre_grad=True): + fusions: list[GroupBatchFusionBase] = [] + # we keep all current pre grad fusions to keep + # current implementation, will remove this later + if pre_grad: + fusions += generate_fusion_from_config( + config.pre_grad_fusion_options, pre_grad=True + ) + else: + fbgemm_fusion_keys = [ + x + for x in config.post_grad_fusion_options + if ( + x not in OPTIMUS_EXCLUDE_POST_GRAD + and config.post_grad_fusion_options[x].get("require_fbgemm", False) + ) + ] + fbgemm_fusions = { + fusion: config.post_grad_fusion_options[fusion] + for fusion in fbgemm_fusion_keys + } + non_fbgemm_fusions = { + fusion: config.post_grad_fusion_options[fusion] + for fusion in config.post_grad_fusion_options + if fusion not in fbgemm_fusion_keys + } + fusions += generate_fusion_from_config(non_fbgemm_fusions, pre_grad=False) + if has_fbgemm: + fusions += generate_fusion_from_config(fbgemm_fusions, pre_grad=False) + + for i, rule in enumerate(fusions): + with GraphTransformObserver( + graph.owning_module, + f"group_batch_fusion_{i}", + ): + apply_group_batch_fusion(graph, rule) # type: ignore[arg-type] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/joint_graph.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/joint_graph.py new file mode 100644 index 0000000000000000000000000000000000000000..16919f540f34bea7ea36a1db3cafce10e8058eff --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/joint_graph.py @@ -0,0 +1,1119 @@ +# mypy: allow-untyped-defs +import functools +import itertools +import logging +import operator +import typing +from collections import Counter +from collections.abc import Sequence +from typing import Any + +import torch +import torch._guards +import torch.utils._pytree as pytree +from torch._dynamo.utils import counters +from torch._inductor.constant_folding import ConstantFolder +from torch._inductor.fx_passes.dedupe_symint_uses import _SymHashingDict +from torch._inductor.utils import get_gpu_type +from torch.fx.experimental.symbolic_shapes import ( + guard_or_false, + guard_or_true, + statically_known_true, +) +from torch.multiprocessing.reductions import StorageWeakRef +from torch.utils._ordered_set import OrderedSet + +from .. import config +from ..pattern_matcher import ( + Arg, + CallFunction, + init_once_fakemode, + KeywordArg, + Match, + MULTIPLE, + PatternMatcherPass as PatternMatcherPassBase, + register_graph_pattern, + stable_topological_sort, +) +from .decompose_mem_bound_mm import check_device +from .replace_random import replace_random_passes + + +PatternMatcherPass = functools.partial( + PatternMatcherPassBase, subsystem="joint_graph_passes" +) + +log = logging.getLogger(__name__) +early_patterns = PatternMatcherPass() +patterns = PatternMatcherPass() +aten = torch.ops.aten +prims = torch.ops.prims + +pass_patterns = [ + patterns, + PatternMatcherPass(), +] + + +@init_once_fakemode +def lazy_init(input_device: torch.device | None = None): + from .fuse_attention import _sfdp_init + from .misc_patterns import _misc_patterns_init + from .pad_mm import _pad_mm_init + + _pad_mm_init(input_device) + _sfdp_init(input_device) + _misc_patterns_init(input_device) + + +def remove_no_ops( + gm: torch.fx.GraphModule, + zeros: OrderedSet[torch.fx.Node], + ones: OrderedSet[torch.fx.Node], +): + """Remove identity arithmetic operations: (+ 0, - 0, * 1, / 1).""" + with torch.utils._python_dispatch._disable_current_modes(): + graph = gm.graph + + def fake_tensors_eq(t1, t2, fields=("shape", "dtype", "device")): + if any(not isinstance(t, torch.Tensor) for t in (t1, t2)): + return False + for field in fields: + if getattr(t1, field) != getattr(t2, field): + return False + return True + + def is_mutated(n): + """Check if a node is mutated by any in-place operation.""" + for user in n.users: + if user.op != "call_function" or not hasattr(user.target, "_schema"): + continue + for i, arg in enumerate(user.args): + if arg is n: + schema_arg = user.target._schema.arguments[i] + if schema_arg.alias_info and schema_arg.alias_info.is_write: + return True + return False + + def replace_no_op(node, replace_input_index): + replacement = node.args[replace_input_index] + + # https://github.com/pytorch/pytorch/issues/86128 causes + # non-Tensor inputs even for ops with only Tensor inputs. + # TODO - decompose/type promote to avoid this + if not all(isinstance(arg, torch.fx.Node) for arg in node.args): + return + + # https://github.com/pytorch/pytorch/issues/174187 + # Don't replace if the replacement value is mutated in-place. + # The original node acts as an implicit copy; removing it would + # cause users to observe the post-mutation value instead. + if is_mutated(replacement): + return + + if not fake_tensors_eq(node.meta["val"], replacement.meta["val"]): + if fake_tensors_eq( + node.meta["val"], + replacement.meta["val"], + ("shape", "device"), + ): + with graph.inserting_after(node): + replacement = graph.call_function( + torch.ops.prims.convert_element_type.default, + args=(replacement, node.meta["val"].dtype), + ) + else: + return + + node.replace_all_uses_with(replacement) + replacement.meta.update(node.meta) + graph.erase_node(node) + + for node in graph.find_nodes(op="call_function", target=aten.add.Tensor): + # TODO handle Tensor-Scalar adds, it's a different schema + if len(node.args) == 2: + if ( + not any(e in zeros for e in node.args) + or node.kwargs.get("alpha", 1) != 1 + ): + continue + + replace_index = 1 if node.args[0] in zeros else 0 + replace_no_op(node, replace_index) + + for node in graph.find_nodes(op="call_function", target=aten.sub.Tensor): + if len(node.args) == 2: + if node.args[1] not in zeros or node.kwargs.get("alpha", 1) != 1: + continue + + replace_no_op(node, 0) + + for node in graph.find_nodes(op="call_function", target=aten.mul.Tensor): + if len(node.args) == 2: + if not any(e in ones for e in node.args): + continue + + replace_input_index = 1 if node.args[0] in ones else 0 + replace_no_op(node, replace_input_index) + + for node in graph.find_nodes(op="call_function", target=aten.div.Tensor): + if len(node.args) == 2 and node.args[1] in ones: + replace_no_op(node, 0) + + # meta tensors returned from the graph have no data and can be replaced with empty_strided + for output_node in graph.find_nodes(op="output"): + had_meta_return = False + + def visit(n): + nonlocal had_meta_return + val = n.meta.get("val") + if isinstance(val, torch.Tensor) and val.device.type == "meta": + with graph.inserting_before(output_node): + n.replace_all_uses_with( + graph.call_function( + torch.ops.aten.empty_strided.default, + args=(val.size(), val.stride()), + kwargs={"dtype": val.dtype, "device": val.device}, + ) + ) + had_meta_return = True + + torch.fx.map_arg(output_node.args, visit) + if had_meta_return: + graph.eliminate_dead_code() + + +def remove_redundant_views(gm: torch.fx.GraphModule): + """ + Removes redundant views by reusing existing ones. + """ + with torch.utils._python_dispatch._disable_current_modes(): + # A dictionary mapping a tensor to all aliased views. + views: dict[torch.fx.Node, dict[torch.dtype, torch.fx.Node]] = {} + graph = gm.graph + + for node in graph.find_nodes( + op="call_function", target=torch.ops.aten.view.dtype + ): + src = node.args[0] + to_type = node.args[1] + existing_views = views.get(src) + is_needed = True + + if existing_views: + # Replace the view with the an existing view if available. + alias = existing_views.get(to_type) + if alias: + is_needed = False + node.replace_all_uses_with(alias) + alias.meta.update(node.meta) + graph.erase_node(node) + else: + from_type = src.meta["val"].dtype + existing_views = {from_type: src} + views[src] = existing_views + + if is_needed: + # Save the new alias but do not replace existing one. + existing_views.setdefault(to_type, node) + views[node] = existing_views + + # Clean up unused views. + while True: + unused_views = [alias for alias in views if not alias.users] + if len(unused_views) == 0: + break + for unused in unused_views: + views.pop(unused) + graph.erase_node(unused) + + +class UniformValueConstantFolder(ConstantFolder): + """ + Runs constant folding and replaces tensors that have a uniform value + with a tensor constructor call: aten.full([shape], value, ...) + """ + + def __init__(self, gm, skip_constructors=False) -> None: + super().__init__(gm, skip_constructors) + self.node_storages_ptrs: dict[torch.fx.Node, int] = {} + self.constant_data_ptrs: dict[torch.fx.Node, StorageWeakRef] = {} + # we may constant fold a tensor which in the graph has a sym size + # see: [constant folding refining of symints] + self.node_replacements_shapes: dict[torch.fx.Node, list[int]] = {} + + # initialize symint -> node mapping so that we can + # use symint nodes in full constructors + self.symint_nodes = _SymHashingDict() + for n in self.module.graph.nodes: # type: ignore[union-attr] + if "val" in n.meta and isinstance(n.meta["val"], torch.SymInt): + if n.meta["val"] not in self.symint_nodes: + self.symint_nodes[n.meta["val"]] = n + + # reference from torch/_funtorch/partitioners.py:get_default_op_list + self.view_op_packets = [ + aten.squeeze, + aten.unsqueeze, + aten.alias, + aten.view, + aten.slice, + aten.t, + prims.broadcast_in_dim, + aten.expand, + aten.as_strided, + aten.permute, + ] + + self.indexing_op_packets = OrderedSet( + [ + aten.slice, + ] + ) + + self._add_peephole_patterns() + + def _add_peephole_patterns(self) -> None: + """ + Add peephole patterns for nodes where we can infer constant value even if some inputs + of the node are unknown. + """ + for op in itertools.chain( + self.module.graph.find_nodes( # type: ignore[operator, union-attr] + op="call_function", target=torch.ops.aten.mul.Tensor + ), + self.module.graph.find_nodes( # type: ignore[operator, union-attr] + op="call_function", target=torch.ops.aten.mul.Scalar + ), + ): + tensor_val = op.meta.get("val", None) + if not isinstance(tensor_val, torch.Tensor): + continue + + def is_zero_int(arg: Any) -> bool: + return isinstance(arg, int) and arg == 0 + + if not any(is_zero_int(a) for a in op.args): + continue + + t = torch.full( + [1], # shape + 0, # value + dtype=tensor_val.dtype, + device=tensor_val.device, + pin_memory=False, + ) + self.add_node_replacement(op, t) + + def _support_dynamic_shape(self): + return True + + def insertable_tensor_check(self, t: torch.Tensor) -> bool: + return True + + def add_node_replacement(self, node: torch.fx.Node, tensor: torch.Tensor) -> None: + self.node_replacements[node] = tensor.flatten()[0].item() + self.node_replacements_shapes[node] = node.meta["val"].shape + self.constant_data_ptrs[node] = StorageWeakRef(tensor.untyped_storage()) + + def insert_placerholder_values(self, env: dict[torch.fx.Node, Any]) -> None: + for n in self.module.graph.find_nodes(op="placeholder"): # type: ignore[operator, union-attr] + if "val" in n.meta and isinstance(n.meta["val"], torch.SymInt): + env[n] = n.meta["val"] + else: + env[n] = self.unknown_value + + def _deduce_value(self, node: torch.fx.Node): + # deduce value for full-like nodes + # 1. for constructors, substitute value is a tensor of size [1] + # 2. for view ops/indexing, substitute value is the same as the input + # 3. for pointwise ops, run node to get the substitute value + # 4. deal with some special ops + # otherwise, stop deduce value and return unknown value + + # TODO: cat, more indexing + # TODO - do on cpu to avoid syncs + + # single-elem attrs + if node.op == "get_attr" or ( + node.op == "call_function" + and node.target is torch.ops.aten.lift_fresh_copy.default + ): + out = super(ConstantFolder, self).run_node(node) + if isinstance(out, torch.Tensor) and out.numel() == 1: + return out + + # handle device_put op + if node.target == prims.device_put.default: + return super(ConstantFolder, self).run_node(node) + + # constructors ops + if ( + node.op == "call_function" + and node.target is aten.full.default + and len(node.args) == 2 + ): + args, kwargs = self.fetch_args_kwargs_from_env(node) + value = args[1] + # Don't specialize symbolic value. + if not isinstance(value, (torch.SymInt, torch.SymFloat, torch.SymBool)): + new_args = [[1], value] + return aten.full.default(*new_args, **node.kwargs) + + # handle before view ops because this changes value + if node.target is aten.view.dtype: + (input_tensor, output_dtype), kwargs = self.fetch_args_kwargs_from_env(node) + # view.dtype with different element sizes changes element count + # (e.g., complex64 [1+0j] viewed as float32 becomes [1.0, 0.0]), + # making uniform values non-uniform. Also crashes on 0-d tensors. + if input_tensor.element_size() != output_dtype.itemsize: + return self.unknown_value + return super(ConstantFolder, self).run_node(node) + + # view ops, return input tensor, the first argument + if hasattr(node.target, "overloadpacket") and ( + node.target.overloadpacket in self.view_op_packets + or node.target.overloadpacket in self.indexing_op_packets + ): + assert isinstance(node.args[0], torch.fx.Node) + return self.env[node.args[0]] + + # we don't want to return unknown value for symints so that we can + # still constant fold through their use in constructors or views + # if we see them in a pointwise node (e.g., tensor * symint) + # we will bail + if "val" in node.meta and isinstance(node.meta["val"], torch.SymInt): + return node.meta["val"] + + # pointwise ops + if isinstance(node.target, torch._ops.OpOverload) and ( + torch.Tag.pointwise in node.target.tags + or node.target is torch.ops.aten.scalar_tensor.default + ): + args, kwargs = self.fetch_args_kwargs_from_env(node) + flattened_inputs = pytree.arg_tree_leaves(*args, **kwargs) + + if any(isinstance(inp, torch.SymInt) for inp in flattened_inputs): + return self.unknown_value + + # we run the ops with dim 1, so remove memory_format to avoid error + kwargs = dict(kwargs) + kwargs.pop("memory_format", None) + + return node.target(*args, **kwargs) + + return self.unknown_value + + +def _has_self_referential_shape( + shapes: list[int | torch.fx.Node], node: torch.fx.Node +) -> bool: + """ + Check if any shape in `shapes` depends on `node`. + + This is used to detect cycles when constant_fold_uniform_value creates a + replacement full() node whose shape includes a sym_size computed from the + original tensor being replaced. + + Checks direct args only - shape nodes typically come from sym_size(tensor, dim) + where tensor is a direct arg. + """ + for shape_node in shapes: + if isinstance(shape_node, torch.fx.Node): + if node in shape_node.args: + return True + return False + + +def constant_fold_uniform_value(gm: torch.fx.GraphModule): + """Runs constant folding and replaces constants which can be constructed with a single `full` call. Calls into remove_no_ops.""" + with torch.utils._python_dispatch._disable_current_modes(): + aten = torch.ops.aten + + # Constant folding can leak memory, especially with repeated compilation, so we are only going to + # remove constants which can be replaced with a constructor. + cf = UniformValueConstantFolder(gm) + cf.run() + + node_replacements = cf.node_replacements + + # note: [constant folding refining of symints] + # constant folding will partially evaluate a graph such that values which have dependencies which + # are entirely known at compile time may also become compile time constants. in some cases, + # this will include symints which we had not yet previously deduced are guaranteed a + # constant value and is then deduced in constant folding. an example is: + # unbacked_symint_eq_11 = torch.full((), 11).item() + # torch.full((unbacked_symint_eq_11,), 0) + node_replacements_shapes = cf.node_replacements_shapes + + graph = gm.graph + + zeros = OrderedSet[Any]() + ones = OrderedSet[Any]() + + # Got failures in `test_is_set_to_cuda` if we change aliasing on constants, + # so just constant-ify if a Tensor is unaliased + constant_data_ptr_count: typing.Counter[StorageWeakRef] = Counter() + + for node in cf.node_replacements: + constant_data_ptr_count[cf.constant_data_ptrs[node]] += 1 + + for node, value in node_replacements.items(): + # we dont have a functional way right now of instantiating a non-contiguous tensor with full/zeros/ones right now + # hasn't shown up to be important yet + if "val" not in node.meta: + # This can only happen in AOTI + continue + + fake_tensor = node.meta["val"] + if not fake_tensor.is_contiguous(memory_format=torch.contiguous_format): + continue + + # TODO - not sure about lossy uint->python value->uint conversions + if fake_tensor.dtype in ( + torch.uint8, + torch.uint16, + torch.uint32, + torch.uint64, + ): + continue + + if constant_data_ptr_count[cf.constant_data_ptrs[node]] > 1: + continue + + with graph.inserting_after(node): + # the conversion from tensor and back to value can be lossy, just use the original full ctor value + if ( + node.op == "call_function" + and node.target is aten.full.default + and len(node.args) == 2 + ): + value = node.args[1] + + # refines symints, see [constant folding refining of symints] above + for runtime_size, compile_time_size in zip( + node_replacements_shapes[node], fake_tensor.shape + ): + torch._check(runtime_size == compile_time_size) + + # replace SymInt as Node before creating a new full node + # e.g. (1, s0) -> (1, arg0_1) + node_shape = node_replacements_shapes[node] + if not all( + not isinstance(s, torch.SymInt) or s in cf.symint_nodes + for s in node_shape + ): + continue + + shapes = [ + cf.symint_nodes[s] if isinstance(s, torch.SymInt) else s + for s in node_replacements_shapes[node] + ] + + # Check if any shape depends on a symint that was computed from + # the node being replaced - this would create a cycle + if _has_self_referential_shape(shapes, node): + continue + + # zeros and ones just get traced into full, so we insert those + new_node = graph.call_function( + aten.full.default, + args=(shapes, value), + kwargs={ + "dtype": fake_tensor.dtype, + "layout": torch.strided, + "device": fake_tensor.device, + "pin_memory": False, + }, + ) + + new_node.meta.update(node.meta) + node.replace_all_uses_with(new_node) + graph.erase_node(node) + + if value == 0: + zeros.add(new_node) + elif value == 1: + ones.add(new_node) + + remove_no_ops(gm, zeros, ones) + remove_redundant_views(gm) + + +def canonicalize_quant_mapping(gm: torch.fx.GraphModule): + """ + + + torch.ops.higher_order.invoke_quant_packed(repeated_subgraph0, 'quant_invoke_0_0', (arg0_1, arg1_1)); + -> + torch.ops.higher_order.invoke_quant(repeated_subgraph0, arg0_1, arg1_1, scheme = 'nf4'); + """ + graph = gm.graph + invoke_quant_invocations = graph.find_nodes( + op="call_function", target=torch.ops.higher_order.invoke_quant_packed + ) + for invoke_quant in invoke_quant_invocations: + kwargs = dict(invoke_quant.kwargs) + + quant_options_node = kwargs.pop("quant_options", None) + if quant_options_node is not None: + assert isinstance(quant_options_node, torch.fx.Node) + quant_options = torch._higher_order_ops.InvokeQuant( + *invoke_quant.kwargs["quant_options"].args, + **invoke_quant.kwargs["quant_options"].kwargs, + ) + else: + quant_options = torch._higher_order_ops.InvokeQuant() + + subgraph, *args = invoke_quant.args + with gm.graph.inserting_before(invoke_quant): + invoke_quant_replacement = graph.call_function( + torch._higher_order_ops.invoke_quant, + (subgraph, *args), + # pyrefly: ignore [bad-argument-type] + kwargs, + ) + invoke_quant_replacement.meta.update(subgraph.meta) + invoke_quant_replacement.meta["quant_options"] = quant_options + + invoke_quant.replace_all_uses_with(invoke_quant_replacement) + graph.erase_node(invoke_quant) + + if quant_options_node and len(quant_options_node.users) == 0: + graph.erase_node(quant_options_node) + + first_user = next(iter(invoke_quant_replacement.users)) + + if ( + len(invoke_quant_replacement.users) == 1 + and len(subgraph.users) == 1 + and first_user.target is operator.getitem + and first_user.args[1] == 0 + ): + subgraph_graph = getattr(gm, subgraph.target) + output_node = torch._inductor.utils.output_node(subgraph_graph) + assert ( + isinstance(output_node.args[0], (list, tuple)) + and len(output_node.args[0]) == 1 + ) + + unpacked_output = output_node.args[0][0] + # pyrefly: ignore [bad-argument-type] + output_node.args = (unpacked_output,) + if "val" in output_node.meta: + output_node.meta["val"] = output_node.meta["val"][0] + subgraph_graph.recompile() + + invoke_quant_replacement.meta.update(first_user.meta) + first_user.replace_all_uses_with(invoke_quant_replacement) + graph.erase_node(first_user) + + +def canonicalize_aten_ir_passes(gm: torch.fx.GraphModule): + """ + Canonicalization passes that will run immediately after aot autograd + tracing. Thsis must be run before all other graph passes. + """ + canonicalize_quant_mapping(gm) + + +def joint_graph_passes( + graph: torch.fx.GraphModule, + input_device: torch.device | None = None, +): + """ + Run FX transformations on the joint forwards+backwards graph. + """ + GraphTransformObserver = functools.partial( + torch.fx.passes.graph_transform_observer.GraphTransformObserver, + subsystem="joint_graph_passes", + ) + + lazy_init(input_device) + count = 0 + + # must occur before other passes + canonicalize_aten_ir_passes(graph) + + if config.joint_custom_pre_pass is not None: + GraphTransformObserver(graph, "joint_custom_pre_pass").apply_graph_pass( + config.joint_custom_pre_pass + ) + count += 1 + + from .post_grad import remove_noop_ops + + GraphTransformObserver(graph, "remove_noop_ops").apply_graph_pass(remove_noop_ops) + + if config.joint_graph_constant_folding: + GraphTransformObserver(graph, "constant_fold_uniform_value").apply_gm_pass( + constant_fold_uniform_value + ) + + if config.pattern_matcher: + count += early_patterns.apply(graph.graph) + + # Make sure AutoChunker happens before pad_mm so we don't need + # to handle padding when searching for chunking patterns. + if config.auto_chunker.enable: + from .auto_chunker import CantChunk, chunk + + try: + graph = chunk(graph) + except CantChunk: + auto_chunker_log = torch._logging.getArtifactLogger( + __name__, "auto_chunker" + ) + auto_chunker_log.debug("AutoChunker fail.", exc_info=True) + + if config.pattern_matcher: + for i, patterns in enumerate(pass_patterns): + maybe_count = GraphTransformObserver( + graph, f"pass_pattern_{i}" + ).apply_graph_pass(patterns.apply) + count += maybe_count if maybe_count is not None else 0 + + if not config.fallback_random: + # not trying into the bisector because decomps may have already affected rng reproducibility + # we'll instead explicitly turn off the config + count += replace_random_passes(graph) + + if config.joint_custom_post_pass is not None: + GraphTransformObserver(graph, "joint_custom_post_pass").apply_graph_pass( + config.joint_custom_post_pass + ) + count += 1 + + if count: + stable_topological_sort(graph.graph) + graph.graph.lint() + graph.recompile() + return graph + + +@register_graph_pattern( + CallFunction( + torch.ops.prims.iota.default, + KeywordArg("length"), + start=KeywordArg("start"), + step=KeywordArg("step"), + dtype=KeywordArg("dtype"), + device=KeywordArg("device"), + requires_grad=KeywordArg("requires_grad"), + ), + # pyrefly: ignore [bad-argument-type] + pass_dict=patterns, +) +def fix_iota_device(match: Match, length, start, step, dtype, device, requires_grad): + """ + Eager supports: + + aten.index(cuda_tensor, torch.arange(..., device="cpu")) + + But this results in an implicit host-device-copy and breaks cudagraphs. + Rewrite the arange to use CUDA. + """ + (node,) = match.nodes + user_devices = OrderedSet[torch.device]() + for user in node.users: + if ( + user.op == "call_function" + and user.target in (aten.index.Tensor, aten.index_put.default) + and hasattr(user.meta.get("val"), "device") + ): + user_devices.add(user.meta["val"].device) # type: ignore[union-attr] + else: + return # bail out + + if len(user_devices) == 1 and "val" in node.meta: + (user_device,) = user_devices + if device.type != user_device.type: + repl = match.graph.call_function( + torch.ops.prims.iota.default, + (length,), + { + "start": start, + "step": step, + "dtype": dtype, + "device": user_device, + "requires_grad": requires_grad, + }, + ) + repl.meta.update(node.meta) + repl.meta["val"] = repl.meta["val"].to(user_device) + node.replace_all_uses_with(repl) + match.erase_nodes() + + +@register_graph_pattern( + CallFunction( + torch.ops.prims.convert_element_type.default, + CallFunction( + torch.ops.prims.convert_element_type.default, + KeywordArg("arg"), + KeywordArg("dtype1"), + ), + KeywordArg("dtype2"), + ), + # pyrefly: ignore [bad-argument-type] + pass_dict=patterns, +) +def pointless_convert(match: Match, arg, dtype1: torch.dtype, dtype2: torch.dtype): + """Remove chain of dtype conversions often created by AMP""" + graph = match.graph + node = match.output_node() + allowed = torch.float16, torch.bfloat16, torch.float32, torch.float64 + if dtype1 in allowed and dtype2 in allowed: + repl = graph.call_function( + torch.ops.prims.convert_element_type.default, (arg, dtype2) + ) + repl.meta.update(node.meta) + node.replace_all_uses_with(repl) + match.erase_nodes() + + +def definitely_equal( + old_sizes: Sequence[torch.SymInt | int], + new_sizes: Sequence[torch.SymInt | torch.fx.Node | int], +) -> bool: + """ + Leverage guard_or_true/false to compare if two lists of int/symint are equal. + Useful to compare sizes, strides etc. + + Can handle -1 in new_sizes which happens in the size arguments of a + view op. old_sizes is supposed to be the tensor shape and should not + contain -1. + + new_sizes can contains fx.Node when dynamic shape is enabled. In that + case new_sizes[i].meta['val'] contains the real torch.SymInt. + """ + + num_neg1 = 0 + + if len(old_sizes) != len(new_sizes): + return False + + for lhs_item, rhs_item in zip(old_sizes, new_sizes): + if isinstance(rhs_item, torch.fx.Node): + rhs_item = rhs_item.meta["val"] + + assert isinstance(lhs_item, (int, torch.SymInt)), type(lhs_item) + assert isinstance(rhs_item, (int, torch.SymInt)), type(rhs_item) + + # It still makes sense to call guard_or_true/false since lhs_item + # rhs_item are torch.SymInt rather than sympy expressions when + # dynamic shape is enabled. + if guard_or_false(lhs_item == rhs_item): + continue + + if guard_or_true(rhs_item != -1): + return False + + num_neg1 += 1 + + if num_neg1 > 1: + return False + return True + + +@register_graph_pattern( + CallFunction(torch.ops.aten.view.default, KeywordArg("arg"), KeywordArg("size")), + # pyrefly: ignore [bad-argument-type] + pass_dict=early_patterns, +) +def pointless_view(match: Match, arg, size): + """Remove no-op view""" + node = match.output_node() + arg_size = list(node.args[0].meta["val"].shape) # type: ignore[union-attr] + if definitely_equal(arg_size, size): + node.replace_all_uses_with(node.args[0]) # type: ignore[arg-type] + match.erase_nodes() + + +@register_graph_pattern( + CallFunction( + aten.view.default, + CallFunction(aten.view.default, KeywordArg("arg"), KeywordArg("size1")), + KeywordArg("size2"), + ), + # pyrefly: ignore [bad-argument-type] + pass_dict=early_patterns, +) +def pointless_view_pair(match: Match, arg, size1, size2): + """ + Remove a pair of views that are pointless. + """ + node = match.output_node() + arg_size = list(arg.meta["val"].shape) + if definitely_equal(arg_size, size2): + node.replace_all_uses_with(arg) + match.erase_nodes() + counters["inductor"]["removed_pointless_view_pair"] += 1 + + +@register_graph_pattern( + CallFunction( + aten.permute.default, + CallFunction(aten.permute.default, KeywordArg("arg"), KeywordArg("perm1")), + KeywordArg("perm2"), + ), + # pyrefly: ignore [bad-argument-type] + pass_dict=early_patterns, +) +def pointless_permute_pair(match: Match, arg, perm1, perm2): + rank = len(perm1) + assert len(perm2) == rank + + for i in range(rank): + if perm1[perm2[i]] != i: + return # bail out + node = match.output_node() + node.replace_all_uses_with(arg) + match.erase_nodes() + + +@register_graph_pattern( + CallFunction( + aten.bmm, + Arg(), + Arg(), + ), + # pyrefly: ignore [bad-argument-type] + pass_dict=patterns, +) +def bmm_to_mm(match: Match, mat1: torch.fx.Node, mat2: torch.fx.Node): + """Convert bmm to mm when batch size is 1""" + + def repl(a, b): + return torch.mm(a.squeeze(0), b.squeeze(0)).unsqueeze(0) + + if ( + check_device(mat1.meta["val"], mat2.meta["val"], get_gpu_type()) + and statically_known_true(mat1.meta["val"].shape[0] == 1) + and statically_known_true(mat2.meta["val"].shape[0] == 1) + ): + # pyrefly: ignore [bad-argument-type] + match.replace_by_example(repl, [mat1, mat2]) + + +# When softmax is used with temperature or other scaling, we get the pattern +# +# scale(x) - scale(x).amax(dim, keepdim=True) +# +# which is expected to be at most zero, but we may end up with numerical +# discrepancies # between the recomputed values of scale(x) inside and out +# of the reduction, # depending on compiler optimizations, e.g. use of fma +# instructions. +# +# Here we replace it with the mathematically equivalent, +# +# scale(x - x.amax(dim, keepdim=True)) +# +# which is more stable as we only compute the scaling once. +# +# NOTE: This pattern must come after fused attention matching! + + +def _partial_softmax_pattern(linear_func, reverse=False, to_dtype=False): + # Allow matching inp * other and other * input + if reverse: + scaled = CallFunction( + linear_func, KeywordArg("other"), KeywordArg("inp"), _users=MULTIPLE + ) + else: + scaled = CallFunction( + linear_func, KeywordArg("inp"), KeywordArg("other"), _users=MULTIPLE + ) + if to_dtype: + scaled = CallFunction( + prims.convert_element_type, scaled, KeywordArg("dtype"), _users=MULTIPLE + ) + amax = CallFunction( + aten.amax.default, scaled, KeywordArg("dim"), KeywordArg("keepdim") + ) + return CallFunction(aten.sub.Tensor, scaled, amax) + + +def _other_is_broadcasted_in_dim(match): + # Check that the scaling factor is constant across the reduction dim, + # so scaling doesn't change which index corresponds to the maximum value + other = match.kwargs["other"] + if isinstance(other, (int, float)): + return True + + inp = match.kwargs["inp"] + if not all(isinstance(x, torch.fx.Node) for x in (inp, other)): + return False + + inp_example = inp.meta["val"] + other_example = other.meta["val"] + if isinstance(other_example, (torch.SymInt, torch.SymFloat)): + return True + + if not all(isinstance(x, torch.Tensor) for x in (inp_example, other_example)): + return False + + inp_ndim = inp_example.ndim + other_shape = other_example.shape + if inp_ndim < len(other_shape): + return False + + # Pad other_shape to the same ndim as inp + other_shape = [1] * (inp_ndim - len(other_shape)) + list(other_shape) + + dim = match.kwargs["dim"] + if isinstance(dim, int): + dim = (dim,) + + if any(d >= len(other_shape) for d in dim): + return False + + return all(statically_known_true(other_shape[d] == 1) for d in dim) + + +def mul_softmax_pattern(match: Match, *, inp, other, dim, keepdim, dtype=None): + def repl(inp, other): + if dtype is not None: + inp = inp.to(dtype) + + sign: int | float | torch.Tensor + if isinstance(other, (int, float, torch.SymInt, torch.SymFloat)): + sign = 1 if other >= 0 else -1 + else: + one = torch.scalar_tensor(1, dtype=inp.dtype, device=inp.device) + sign = torch.where(other >= 0, one, -one) + + inp = inp * sign + max_ = torch.amax(inp, dim=dim, keepdim=keepdim) + + return (inp - max_) * (sign * other) + + # pyrefly: ignore [bad-argument-type] + match.replace_by_example(repl, [inp, other]) + + +for reverse, to_dtype in itertools.product((False, True), repeat=2): + register_graph_pattern( + _partial_softmax_pattern(aten.mul.Tensor, reverse=reverse, to_dtype=to_dtype), + # pyrefly: ignore [bad-argument-type] + pass_dict=pass_patterns[1], + extra_check=_other_is_broadcasted_in_dim, + )(mul_softmax_pattern) + + +def div_softmax_pattern(match: Match, *, inp, other, dim, keepdim, dtype=None): + def repl(inp, other): + if dtype is not None: + inp = inp.to(dtype) + + sign: int | float | torch.Tensor + if isinstance(other, (int, float, torch.SymInt, torch.SymFloat)): + sign = 1 if other >= 0 else -1 + else: + one = torch.scalar_tensor(1, dtype=inp.dtype, device=inp.device) + sign = torch.where(other >= 0, one, -one) + + inp = inp * sign + max_ = torch.amax(inp, dim=dim, keepdim=keepdim) + + return (inp - max_) / (sign * other) + + # pyrefly: ignore [bad-argument-type] + match.replace_by_example(repl, [inp, other]) + + +for to_dtype in (False, True): + register_graph_pattern( + _partial_softmax_pattern(aten.div.Tensor, to_dtype=to_dtype), + # pyrefly: ignore [bad-argument-type] + pass_dict=pass_patterns[1], + extra_check=_other_is_broadcasted_in_dim, + )(div_softmax_pattern) + + +def scatter_upon_const_tensor_extra_check(m): + if not config.optimize_scatter_upon_const_tensor: + return False + full_shape = m.kwargs["shape"] + selector = m.kwargs["selector"] + dim = m.kwargs["dim"] + if dim < 0: + dim += len(full_shape) + + selector_ft = selector.meta["val"] + assert selector_ft.dim() == len(full_shape) + + for idx, select_sz, full_sz in zip( + itertools.count(), selector_ft.shape, full_shape + ): + if idx == dim: + continue + + # TODO: the pattern can be updated to support the case that index tensor + # is shorter. But that will need a more complex condition expression + # especially for multi-dimensional tensors. + # Skip it for now. + if isinstance(full_sz, torch.fx.Node): + full_sz = full_sz.meta["val"] + if select_sz < full_sz: + return False + + # Actually we can support small size larger than 1. It would be a bit + # tedious. E.g., we load all the index values (not many) and compare + # them with the position in tensor to decide what value to return. + return selector_ft.size(dim) == 1 + + +@register_graph_pattern( + CallFunction( + aten.scatter.value, + CallFunction( + aten.full, + KeywordArg("shape"), + KeywordArg("background_val"), + dtype=KeywordArg("dtype"), + ), + KeywordArg("dim"), + KeywordArg("selector"), + KeywordArg("val"), # scalar value + ), + # pyrefly: ignore [bad-argument-type] + pass_dict=patterns, + extra_check=scatter_upon_const_tensor_extra_check, +) +def scatter_upon_const_tensor( + match: Match, shape, background_val, dtype, dim, selector, val +): + """ + Match the pattern of full+scatter into a pointwise operation in joint graph. + + TODO: Right now the scatter value must be a scalar. But we could support it + when it is a tensor as well. + """ + from torch._inductor import metrics + + # pyrefly: ignore # bad-assignment + metrics.num_matches_for_scatter_upon_const_tensor += 1 + + # Create a replacement that uses torch.where for the pointwise operation + def repl_fn(shape, background_val, dim, selector, val): + # Create a tensor of indices for the scatter dimension + length = shape[dim] + indices = torch.arange(length, device=selector.device, dtype=torch.int64) + + # Reshape indices to have size 'length' at dim, then broadcast + view_shape = [1] * len(shape) + view_shape[dim] = length + indices_view = indices.view(*view_shape) + + # Broadcast selector to match full tensor shape + selector_expanded = selector.expand(shape) + + # Create a mask for where to scatter + mask = selector_expanded == indices_view + + # Use torch.where to implement the scatter pointwise operation + return torch.where(mask, val, background_val) + + # replace the scatter operation with pointwise equivalent + # pyrefly: ignore [bad-argument-type] + match.replace_by_example(repl_fn, [shape, background_val, dim, selector, val]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/memory_estimator.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/memory_estimator.py new file mode 100644 index 0000000000000000000000000000000000000000..c939621679e091bb9cb57fda8bc15c1b275030e6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/memory_estimator.py @@ -0,0 +1,453 @@ +import itertools +import logging +from collections import defaultdict +from collections.abc import Callable +from dataclasses import dataclass + +import torch +import torch.fx as fx +from torch.fx.experimental.symbolic_shapes import optimization_hint +from torch.utils._ordered_set import OrderedSet +from torch.utils._pytree import tree_map_only + + +log = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class StorageKey: + storage: torch.UntypedStorage + device: torch.device + + def __hash__(self) -> int: + return self.storage._cdata + + def __eq__(self, other: object) -> bool: + if not isinstance(other, StorageKey): + return False + return ( + self.storage._cdata == other.storage._cdata and self.device == other.device + ) + + +class GraphAliasTracker: + """ + Tracks storage allocation and usage relationships in an FX graph. + + Differentiates between: + - Fresh allocations: nodes that allocate new storage (not views/aliases) + - Uses: nodes that use a storage as input + """ + + def __init__(self, nodes: list[fx.Node]): + # Map from node to the fresh storages it allocates (not views/aliases) + self.node_to_fresh_allocations: dict[fx.Node, OrderedSet[StorageKey]] = {} + + # Map from storage to the node that originally allocated it + self.storage_to_allocator: dict[StorageKey, fx.Node] = {} + + # Map from node to all storages it uses as inputs + self.node_to_storage_uses: dict[fx.Node, OrderedSet[StorageKey]] = {} + + # Map from storage to all nodes that use it + self.storage_to_uses: dict[StorageKey, OrderedSet[fx.Node]] = defaultdict( + OrderedSet + ) + + # Map from storage to the last node that uses it + self.storage_to_last_user: dict[StorageKey, fx.Node] = {} + + # Map from node to storages that have their last use at that node + self.node_to_storages_last_used: dict[fx.Node, OrderedSet[StorageKey]] = ( + defaultdict(OrderedSet) + ) + + # Track all output storages for each node (for building usage graph) + self.node_to_output_storages: dict[fx.Node, OrderedSet[StorageKey]] = {} + + # First pass: build storage allocations and track uses + for node in nodes: + # Get output storages + output_storages = self._get_output_storages(node) + self.node_to_output_storages[node] = output_storages + + # Track fresh allocations + fresh_allocations: OrderedSet[StorageKey] = OrderedSet() + for storage_key in output_storages: + if storage_key not in self.storage_to_allocator: + self.storage_to_allocator[storage_key] = node + fresh_allocations.add(storage_key) + self.node_to_fresh_allocations[node] = fresh_allocations + + # Track input storage uses (safe because inputs were already processed) + input_storages = self._get_input_storages(node) + self.node_to_storage_uses[node] = input_storages + for storage_key in input_storages: + self.storage_to_uses[storage_key].add(node) + + # Second pass: find last users (iterate in reverse) + for node in reversed(nodes): + input_storages = self.node_to_storage_uses[node] + for storage_key in input_storages: + if storage_key not in self.storage_to_last_user: + self.storage_to_last_user[storage_key] = node + self.node_to_storages_last_used[node].add(storage_key) + + @staticmethod + def _get_output_storages(node: fx.Node) -> OrderedSet[StorageKey]: + """ + Get all storages from a node's outputs. + + Uses pytree to handle arbitrary nested structures. + """ + val = node.meta.get("val") + if val is None: + return OrderedSet() + + storages: OrderedSet[StorageKey] = OrderedSet() + + def collect_storage(tensor: torch._subclasses.FakeTensor) -> None: + storages.add(StorageKey(tensor.untyped_storage(), tensor.device)) + + # Use tree_map_only to handle FakeTensors in nested structures + tree_map_only(torch._subclasses.FakeTensor, collect_storage, val) + + return storages + + def _get_input_storages(self, node: fx.Node) -> OrderedSet[StorageKey]: + """ + Get all storages from a node's inputs. + """ + input_storages: OrderedSet[StorageKey] = OrderedSet() + + for input_node in node.all_input_nodes: + input_storages.update(self.node_to_output_storages[input_node]) + + return input_storages + + def get_fresh_allocations(self, node: fx.Node) -> OrderedSet[StorageKey]: + """Get all fresh storage allocations by this node (not views/aliases).""" + return self.node_to_fresh_allocations[node] + + def get_storage_uses(self, node: fx.Node) -> OrderedSet[StorageKey]: + """Get all storages that this node uses as inputs.""" + return self.node_to_storage_uses[node] + + def get_storages_last_used( + self, + node: fx.Node, + ) -> OrderedSet[StorageKey]: + """ + Get storages whose last use is at this node. + """ + return self.node_to_storages_last_used[node] + + +def _size_of_default(num_bytes: int | torch.SymInt) -> int: + return optimization_hint(num_bytes) + + +def device_filter(device: torch.device) -> bool: + return device.type != "cpu" + + +def build_memory_profile( + graph: fx.Graph, + is_releasable: Callable[[fx.Node], bool], + size_of: Callable[[int | torch.SymInt], int] | None = None, +) -> list[int]: + """ + Function to estimate the memory profile of an input FX graph. + + Args: + - graph (fx.Graph): The input FX graph for which the memory profile + is to be estimated. + - is_releasable (Callable[[fx.Node], bool]): A function that + determines if a node's memory can be released (e.g. primal nodes + cannot be released). + - size_of (Callable[[int | torch.SymInt], int]): A function that converts + byte counts (possibly symbolic) to concrete integers. + + Returns: + - List[int]: A list representing the memory profile over the execution + of the graph, where each entry corresponds to the memory usage at + a particular point in the execution. + """ + + size_of = size_of or _size_of_default + nodes = list(graph.nodes) + alias_info = GraphAliasTracker(nodes) + + # Build memory profile + current_memory = 0 + + for node in itertools.chain( + graph.find_nodes(op="placeholder"), graph.find_nodes(op="get_attr") + ): + for storage_key in alias_info.get_fresh_allocations(node): + if device_filter(storage_key.device): + current_memory += size_of(storage_key.storage.nbytes()) + + memory_profile = [current_memory] + + for node in nodes: + if node.op in ("placeholder", "get_attr", "output"): + continue + + # Process allocations + for storage_key in alias_info.get_fresh_allocations(node): + if device_filter(storage_key.device): + current_memory += size_of(storage_key.storage.nbytes()) + + memory_profile.append(current_memory) + + # Process deallocations + # pyrefly: ignore [bad-assignment] + for storage_key in alias_info.get_storages_last_used(node): + allocator = alias_info.storage_to_allocator[storage_key] + if is_releasable(allocator): + if device_filter(storage_key.device): + current_memory -= size_of(storage_key.storage.nbytes()) + + memory_profile.append(current_memory) + + return memory_profile + + +def get_fwd_bwd_interactions( + fwd_graph: fx.Graph, + bwd_graph: fx.Graph, + size_of: Callable[[int | torch.SymInt], int] | None = None, +) -> tuple[int, OrderedSet[str]]: + """ + Analyze the interactions between the forward (fwd) and backward (bwd) graphs + to determine memory usage characteristics. + + Args: + - fwd_graph (fx.Graph): The forward graph representing the forward pass. + - bwd_graph (fx.Graph): The backward graph representing the backward pass. + - size_of (Callable[[int | torch.SymInt], int]): A function that converts + byte counts (possibly symbolic) to concrete integers. + + Returns: + - tuple[int, OrderedSet[str]]: A tuple containing: + 1. The baseline memory usage during the backward pass, accounting for + storages that persist from the forward pass (i.e., in fwd output but + not in bwd input). + 2. A set of node names whose storage cannot be released during the bwd pass. + These include nodes that use storage from primals or are in bwd input + but not in fwd output. + """ + + size_of = size_of or _size_of_default + + # Build alias info for forward graph + fwd_nodes = list(fwd_graph.nodes) + fwd_alias_info = GraphAliasTracker(fwd_nodes) + + # Identify storages allocated by primal placeholder nodes + primal_storages: OrderedSet[StorageKey] = OrderedSet() + for node in fwd_graph.find_nodes(op="placeholder"): + if node.name.startswith("primals"): + primal_storages.update(fwd_alias_info.get_fresh_allocations(node)) + + # Get storages in forward output + fwd_output_node = next(iter(reversed(fwd_graph.nodes)))[-1] + assert fwd_output_node.op == "output" + fwd_output_storages = fwd_alias_info.get_storage_uses(fwd_output_node) + + # Node names that should not be deleted during memory profile estimation of bwd_graph + do_not_delete: OrderedSet[str] = OrderedSet() + + # Collect all storages in backward inputs and identify nodes to not delete + bwd_input_storages: OrderedSet[StorageKey] = OrderedSet() + for node in bwd_graph.find_nodes(op="placeholder"): + node_storages = GraphAliasTracker._get_output_storages(node) + bwd_input_storages.update(node_storages) + + # Check if this node uses primal storage + if node_storages & primal_storages: + do_not_delete.add(node.name) + + # Check if this node's storages are not in forward outputs + # (meaning it's an external input to backward pass) + if not (node_storages & fwd_output_storages): + do_not_delete.add(node.name) + + # Calculate baseline memory: storages in fwd output but not in bwd input + # These storages persist throughout the backward pass + baseline_storages = fwd_output_storages - bwd_input_storages + bwd_baseline_memory = 0 + for storage_key in baseline_storages: + if storage_key.device.type != "cpu": + bwd_baseline_memory += size_of(storage_key.storage.nbytes()) + + return bwd_baseline_memory, do_not_delete + + +def _is_releasable(n: fx.Node) -> bool: + # Storages of primals cannot be released during fwd or bwd pass. + return not n.name.startswith("primals") + + +def get_peak_memory( + fwd_graph: fx.Graph, + bwd_graph: fx.Graph, +) -> int: + fwd_peak_memory = max(build_memory_profile(fwd_graph, _is_releasable)) + + bwd_baseline_memory, bwd_do_not_delete = get_fwd_bwd_interactions( + fwd_graph, + bwd_graph, + ) + + def _is_bwd_releasable(n: fx.Node) -> bool: + # Storages of nodes in bwd_do_not_delete cannot be released + # during the bwd pass. + return _is_releasable(n) and n.name not in bwd_do_not_delete + + bwd_peak_memory = bwd_baseline_memory + max( + build_memory_profile(bwd_graph, _is_bwd_releasable) + ) + return max( + fwd_peak_memory, + bwd_peak_memory, + ) + + +class MemoryTracker: + """ + Tracks memory usage for alternative scheduling orders of an FX graph. + + This class enables tracking memory usage as nodes are scheduled in a different + order than the original graph. + """ + + def __init__( + self, + graph: fx.Graph, + is_releasable: Callable[[fx.Node], bool] | None = None, + device_filter: Callable[[torch.device], bool] | None = None, + ): + """ + Initialize memory tracker for alternative scheduling of the given graph. + + Args: + graph: FX graph to track memory for under alternative scheduling + is_releaseable: do we consider this input to the graph to release memory + upon final use, or is allocated for the duration of the graph ? + by default, we assume all nodes but those that start with "primals" to be releasable + device_filter: Function to determine which devices to track (default: non-CPU) + """ + + self.graph = graph + self.nodes = list(graph.nodes) + self.device_filter = device_filter or (lambda device: device.type != "cpu") + self.scheduled: OrderedSet[fx.Node] = OrderedSet() + + # Memory tracking using GraphAliasTracker + self.alias_tracker = GraphAliasTracker(self.nodes) + self.current_live_storages: OrderedSet[StorageKey] = OrderedSet() + self.current_memory_bytes = 0 + self.is_releasable = _is_releasable if is_releasable is None else is_releasable + + # Initialize live storages with placeholders and get_attr nodes + for node in self.nodes: + if node.op in ("placeholder", "get_attr"): + fresh_allocations = self.alias_tracker.get_fresh_allocations(node) + for storage_key in fresh_allocations: + if self.device_filter(storage_key.device): + self.current_live_storages.add(storage_key) + self.current_memory_bytes += self._get_storage_size(storage_key) + + self.peak_memory = self.current_memory_bytes + + log.debug( + "Memory tracker initialized with initial memory: %d MB", + self.current_memory_bytes // (1024 * 1024), + ) + + def schedule_node(self, node: fx.Node) -> None: + """ + Schedule a node and update memory tracking for the new scheduling order. + + Args: + node: The node being scheduled (potentially out of original order) + """ + assert node not in self.scheduled, "should not schedule node twice" + self.scheduled.add(node) + self._update_memory_for_node(node) + + def get_current_memory_bytes(self) -> int: + """Get current live memory in bytes under the current scheduling.""" + return self.current_memory_bytes + + def _get_storage_size(self, storage_key: StorageKey) -> int: + """Get the size of a storage in bytes, handling symbolic shapes.""" + size_bytes = storage_key.storage.nbytes() + return optimization_hint(size_bytes) + + def _get_storages_freed_by_node(self, node: fx.Node) -> OrderedSet[StorageKey]: + """Get storages that would be freed if we schedule this node.""" + freed_storages: OrderedSet[StorageKey] = OrderedSet() + + input_storages = self.alias_tracker.get_storage_uses(node) + for storage_key in input_storages: + if not self.device_filter(storage_key.device): + continue + + # Invariant: if a node uses a storage, it must be live + assert storage_key in self.current_live_storages, ( + "all input storages should be currently allocated" + ) + + if not self.is_releasable( + self.alias_tracker.storage_to_allocator[storage_key] + ): + continue + + all_uses = self.alias_tracker.storage_to_uses[storage_key] + + # If no more unscheduled uses remain, the storage can be freed + if all(u in self.scheduled for u in all_uses): + freed_storages.add(storage_key) + + return freed_storages + + def _update_memory_for_node(self, node: fx.Node) -> None: + """Update memory tracking when a node is scheduled.""" + if node.op in ("placeholder", "get_attr", "output"): + return + + # Add fresh allocations + fresh_allocations = self.alias_tracker.get_fresh_allocations(node) + alloc_bytes = 0 + for storage_key in fresh_allocations: + if ( + self.device_filter(storage_key.device) + and storage_key not in self.current_live_storages + ): + size = self._get_storage_size(storage_key) + self.current_live_storages.add(storage_key) + self.current_memory_bytes += size + alloc_bytes += size + + self.peak_memory = max(self.current_memory_bytes, self.peak_memory) + + # Remove storages that are no longer used + storages_to_free = self._get_storages_freed_by_node(node) + freed_bytes = 0 + for storage_key in storages_to_free: + if storage_key in self.current_live_storages: + size = self._get_storage_size(storage_key) + self.current_live_storages.remove(storage_key) + self.current_memory_bytes -= size + freed_bytes += size + + log.debug( + "Scheduled %s: memory change %d allocs, %d frees, current memory: %d MB", + node.name, + len(fresh_allocations), + len(storages_to_free), + self.current_memory_bytes // (1024 * 1024), + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/micro_pipeline_tp.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/micro_pipeline_tp.py new file mode 100644 index 0000000000000000000000000000000000000000..6cc5503d4815b6a37d1e0daa9c5ffaad4498539f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/micro_pipeline_tp.py @@ -0,0 +1,1114 @@ +# mypy: allow-untyped-defs +import logging +import operator +from collections import defaultdict +from dataclasses import dataclass, field +from math import prod +from typing import Any, cast + +import torch +from torch.utils._ordered_set import OrderedSet + +from .. import config, inductor_prims +from ..pattern_matcher import ( + CallFunction, + Ignored, + KeywordArg, + ListOf, + Match, + MULTIPLE, + PatternExpr, + PatternMatcherPass, +) + + +log = logging.getLogger(__name__) +aten = torch.ops.aten +patterns = PatternMatcherPass() + + +def _is_last_dim(t: torch.Tensor, dim: int) -> bool: + return dim == t.ndim - 1 or dim == -1 + + +def _is_backward(graph: torch.fx.Graph) -> bool: + placeholders = [] + for node in graph.nodes: + if node.op != "placeholder": + break + placeholders.append(node) + return not all(node.name.startswith("primal") for node in placeholders) + + +def _compute_mm_arithmetic_intensity(M: int, N: int, K: int) -> float: + return M * N * K / (M * K + N * K + M * N) + + +def _filter_nodes_by_target(nodes: list[torch.fx.Node], target) -> list[torch.fx.Node]: + return [x for x in nodes if x.target == target] + + +def _find_ancestors(node: torch.fx.Node) -> OrderedSet[torch.fx.Node]: + ancestors = OrderedSet[torch.fx.Node]() + ancestors.add(node) + cur_nodes = [node] + while len(cur_nodes) > 0: + new_nodes = [] + for node in cur_nodes: + for inp in node.all_input_nodes: + if inp not in ancestors: + ancestors.add(inp) + new_nodes.append(inp) + cur_nodes = new_nodes + return OrderedSet(node for node in ancestors if node.op != "placeholder") + + +def _get_tensor(node: torch.fx.Node) -> torch.Tensor: + val = node.meta["val"] + assert isinstance(val, torch.Tensor) + return val + + +@dataclass +class _AllGatherMatch: + match: Match + shard_node: torch.fx.Node + ag_node: torch.fx.Node + res_node: torch.fx.Node + gather_dim: int + group_name: "torch.distributed.distributed_c10d.GroupName" + + def replace_with(self, new_node: torch.fx.Node) -> None: + self.res_node.replace_all_uses_with(new_node) + + def erase(self) -> None: + for node in reversed(self.match.nodes): + if len(node.users) == 0: + node.graph.erase_node(node) + + +def find_all_gather_patterns(graph: torch.fx.Graph): + c10d = torch.ops._c10d_functional + + def make_zero_dim_all_gather_pattern(shard): + return CallFunction( + c10d.wait_tensor.default, + CallFunction( + c10d.all_gather_into_tensor.default, + shard, + Ignored(), + KeywordArg("group_name"), + ), + ) + + # Matches funcol.all_gather_tensor with gather_dim == 0 + zero_dim_all_gather_pattern = make_zero_dim_all_gather_pattern(KeywordArg("shard")) + + def make_all_gather_split_pattern(shard): + return CallFunction( + operator.getitem, + CallFunction( + aten.split.Tensor, + make_zero_dim_all_gather_pattern(shard), + Ignored(), + _users=MULTIPLE, + ), + Ignored(), + ) + + def make_cat_pattern(splits): + return CallFunction( + aten.cat.default, + ListOf(splits), + KeywordArg("gather_dim"), + ) + + # Matches funcol.all_gather_tensor with gather_dim > 0 + non_zero_dim_all_gather_pattern = make_cat_pattern( + make_all_gather_split_pattern(KeywordArg("shard")), + ) + + # Match a zero-dim all-gather in which the data is transferred as uint8 and + # viewed back as the original dtype. + zero_dim_type_erased_all_gather_pattern = CallFunction( + aten.view.dtype, + make_zero_dim_all_gather_pattern( + KeywordArg("shard"), + ), + Ignored(), + ) + + # Match a non-zero dim all-gather in which the data is transferred as uint8 + # and viewed back as the original dtype. + non_zero_dim_type_erased_all_gather_pattern = CallFunction( + aten.view.dtype, + make_cat_pattern( + CallFunction( + aten.view.dtype, + make_all_gather_split_pattern( + KeywordArg("shard"), + ), + Ignored(), + ), + ), + Ignored(), + ) + + # If two patterns with the same res_node_target have the same suffix, the + # longer pattern should appear first in the list. + # e.g. supposed we have (1) A -> B -> C -> D and (2) B -> C -> D, (1) + # should appear before (2) in the list. + res_node_target_to_patterns = { + aten.cat.default: [ + (non_zero_dim_all_gather_pattern, 0), + ], + aten.view.dtype: [ + (non_zero_dim_type_erased_all_gather_pattern, 0), + (zero_dim_type_erased_all_gather_pattern, 0), + ], + c10d.wait_tensor.default: [ + (zero_dim_all_gather_pattern, 0), + ], + } + + # Match in reverse to ensure longer patterns is prioritized + all_gathers = [] + visited_ag_nodes = OrderedSet[torch.fx.Node]() + for node in reversed(graph.nodes): + for target, patterns in res_node_target_to_patterns.items(): + if node.target != target: + continue + for pattern, ag_node_idx in patterns: + match = pattern.match(node) + if not match: + continue + + assert isinstance(match, Match) + ag_node = match.nodes[ag_node_idx] + assert ag_node.target == c10d.all_gather_into_tensor.default + + if ag_node in visited_ag_nodes: + continue + visited_ag_nodes.add(ag_node) + + ag_match = _AllGatherMatch( + match=match, + shard_node=match.kwargs["shard"], + ag_node=ag_node, + res_node=node, + gather_dim=match.kwargs.get("gather_dim", 0), + group_name=match.kwargs["group_name"], + ) + all_gathers.append(ag_match) + + return list(reversed(all_gathers)) + + +@dataclass +class _ReduceScatterMatch: + match: Match + input_node: torch.fx.Node + reduce_scatter_node: torch.fx.Node + wait_tensor_node: torch.fx.Node + reduce_op: str + scatter_dim: int + group_name: "torch.distributed.distributed_c10d.GroupName" + + def replace_with(self, new_node: torch.fx.Node) -> None: + # Replace all uses of the result node (wait_tensor) with the fused node. + self.wait_tensor_node.replace_all_uses_with(new_node) + + # If the reduce-scatter result is saved for backward, save the fused node for backward instead. + self._update_save_for_backward(new_node) + + def _update_save_for_backward(self, new_node: torch.fx.Node) -> None: + """ + If the output node is a user of the reduce_scatter node (indicating the reduce_scatter + result is saved for backward), this method will update the output node to use the fused node instead. + """ + output_node = None + for user in self.reduce_scatter_node.users: + if user.target == "output": + output_node = user + break + if output_node is not None: + output_node.replace_input_with(self.reduce_scatter_node, new_node) + + # Assert that now the reduce scatter node has only one user (the wait_tensor) and it's not + # saved for backward anymore. + assert len(self.reduce_scatter_node.users) == 1, ( + "Reduce scatter node has multiple users, this is not expected" + ) + + def erase(self) -> None: + for node in reversed(self.match.nodes): + if len(node.users) == 0: + node.graph.erase_node(node) + + +def find_reduce_scatter_patterns(graph: torch.fx.Graph): + c10d = torch.ops._c10d_functional + + def reduce_scatter_template(inp: PatternExpr, users: int): + return CallFunction( + c10d.wait_tensor.default, + CallFunction( + c10d.reduce_scatter_tensor.default, + inp, + KeywordArg("reduce_op"), + Ignored(), + KeywordArg("group_name"), + _users=users, + ), + ) + + # Matches funcol.reduce_scatter_tensor with scatter_dim == 0 + zero_dim_reduce_scatter_pattern_single_user = reduce_scatter_template( + KeywordArg("input"), users=1 + ) + + # Two users will occur when the reduce-scatter result is saved for backward + zero_dim_reduce_scatter_pattern_multi_user = reduce_scatter_template( + KeywordArg("input"), users=2 + ) + + # Matches funcol.reduce_scatter_tensor with scatter_dim > 0 + non_zero_dim_reduce_scatter_pattern_single_user = reduce_scatter_template( + CallFunction( + aten.cat.default, + ListOf( + CallFunction( + operator.getitem, + CallFunction( + aten.split.Tensor, + KeywordArg("input"), + Ignored(), + KeywordArg("scatter_dim"), + _users=MULTIPLE, + ), + Ignored(), + ) + ), + ), + users=1, + ) + + # Two users will occur when the reduce-scatter result is saved for backward + non_zero_dim_reduce_scatter_pattern_multi_user = reduce_scatter_template( + CallFunction( + aten.cat.default, + ListOf( + CallFunction( + operator.getitem, + CallFunction( + aten.split.Tensor, + KeywordArg("input"), + Ignored(), + KeywordArg("scatter_dim"), + _users=MULTIPLE, + ), + Ignored(), + ) + ), + ), + users=2, + ) + + reduce_scatters = [] + for node in reversed(graph.nodes): + if node.target == c10d.wait_tensor.default: + if match := non_zero_dim_reduce_scatter_pattern_single_user.match(node): + assert isinstance(match, Match) + reduce_scatters.append( + _ReduceScatterMatch( + match=match, + input_node=match.kwargs["input"], + reduce_scatter_node=match.nodes[-2], + wait_tensor_node=node, + reduce_op=match.kwargs["reduce_op"], + scatter_dim=match.kwargs["scatter_dim"], + group_name=match.kwargs["group_name"], + ) + ) + elif match := zero_dim_reduce_scatter_pattern_single_user.match(node): + assert isinstance(match, Match) + reduce_scatters.append( + _ReduceScatterMatch( + match=match, + input_node=match.kwargs["input"], + reduce_scatter_node=match.nodes[0], + wait_tensor_node=node, + reduce_op=match.kwargs["reduce_op"], + scatter_dim=0, + group_name=match.kwargs["group_name"], + ) + ) + elif match := non_zero_dim_reduce_scatter_pattern_multi_user.match(node): + assert isinstance(match, Match) + reduce_scatters.append( + _ReduceScatterMatch( + match=match, + input_node=match.kwargs["input"], + reduce_scatter_node=match.nodes[-2], + wait_tensor_node=node, + reduce_op=match.kwargs["reduce_op"], + scatter_dim=match.kwargs["scatter_dim"], + group_name=match.kwargs["group_name"], + ) + ) + elif match := zero_dim_reduce_scatter_pattern_multi_user.match(node): + assert isinstance(match, Match) + reduce_scatters.append( + _ReduceScatterMatch( + match=match, + input_node=match.kwargs["input"], + reduce_scatter_node=match.nodes[0], + wait_tensor_node=node, + reduce_op=match.kwargs["reduce_op"], + scatter_dim=0, + group_name=match.kwargs["group_name"], + ) + ) + return list(reversed(reduce_scatters)) + + +@dataclass +class _Matmul: + nodes: list[torch.fx.Node] + arg_ancestor_nodes: OrderedSet[torch.fx.Node] = field(init=False) + A_node: torch.fx.Node + B_node: torch.fx.Node + pre_mm_reshape: torch.fx.Node | None + post_mm_reshape: torch.fx.Node | None + + def __post_init__(self): + assert len(self.nodes) in (1, 3) + if len(self.nodes) == 1: + assert self.nodes[0].target in (aten.mm.default, aten._scaled_mm.default) + else: + assert self.nodes[0].target is aten.reshape.default + assert self.nodes[1].target in (aten.mm.default, aten._scaled_mm.default) + assert self.nodes[2].target is aten.reshape.default + self.arg_ancestor_nodes = _find_ancestors(self.B_node) + + def replace_with(self, new_node: torch.fx.Node) -> None: + """ + Replace the matmul with the new node. + """ + graph = new_node.graph + + # For 2D-matmuls, we simply replace the mm node with `new_node`. + if len(self.nodes) == 1: + mm_node = self.nodes[0] + assert mm_node.target in (aten.mm.default, aten._scaled_mm.default) + mm_node.replace_all_uses_with(new_node) + graph.erase_node(mm_node) + return + + # An ND-matmul is reshape -> mm -> reshape sequence. We first replace + # the second reshape node with `new_node`. Then, we ensure that the + # original mm node in the sequence ends up with zero users by replacing + # it with a reverse reshape of `new_node`. + graph = new_node.graph + assert len(self.nodes) == 3 + mm_node = self.nodes[1] + output_reshape_node = self.nodes[2] + + assert mm_node.target in (aten.mm.default, aten._scaled_mm.default) + assert output_reshape_node.target is aten.reshape.default + + output_reshape_node.replace_all_uses_with(new_node) + if len(mm_node.users) > 1: + with graph.inserting_after(new_node): + new_mm_node = graph.call_function( + aten.reshape.default, + args=(new_node, list(_get_tensor(mm_node).shape)), + ) + mm_node.replace_all_uses_with(new_mm_node) + + def erase(self) -> None: + for node in reversed(self.nodes): + if len(node.users) == 0: + node.graph.erase_node(node) + + @classmethod + def from_match(cls, match: list[torch.fx.Node]) -> "_Matmul": + assert len(match) in (1, 3) + assert match[0].target in ( + aten.mm.default, + aten.reshape.default, + ) + mm_node = match[0] if len(match) == 1 else match[1] + return _Matmul( + nodes=match, + A_node=cast("torch.fx.Node", match[0].args[0]), + B_node=cast("torch.fx.Node", mm_node.args[1]), + # _Matmul handles reshapes via custom graph manipulation logic, see `replace_with()` method. + # TODO: explore unifying the _Matmul and _ScaledMatmul approaches to handling reshapes. + pre_mm_reshape=None, + post_mm_reshape=None, + ) + + +@dataclass +class _ScaledMatmul(_Matmul): + A_scale_node: torch.fx.Node + B_scale_node: torch.fx.Node + bias_node: torch.fx.Node | None + result_scale_node: torch.fx.Node | None + out_dtype: torch.dtype | None + use_fast_accum: bool + pre_mm_reshape: torch.fx.Node | None + post_mm_reshape: torch.fx.Node | None + + def __post_init__(self): + super().__post_init__() + self.arg_ancestor_nodes |= _find_ancestors(self.A_scale_node) + self.arg_ancestor_nodes |= _find_ancestors(self.B_scale_node) + + @classmethod + def from_match(cls, match: list[torch.fx.Node]) -> "_ScaledMatmul": + assert len(match) in (1, 3) + assert match[0].target in ( + aten._scaled_mm.default, + aten.reshape.default, + ) + + def get_arg(node: torch.fx.Node, idx: int, default: Any) -> Any: + if idx >= len(node.args): + return default + return node.args[idx] + + # Use mm_node with 2D args for both A and B, even if this is a "reshape -> mm -> reshape" pattern. + # We will store the reshapes in pre_mm_reshape and post_mm_reshape, to be referenced later to + # produce the correct output shapes, reduce-scatter along the correct dimensions, etc. + is_reshape_mm_reshape_pattern = match[0].target is aten.reshape.default + mm_node = match[1] if is_reshape_mm_reshape_pattern else match[0] + pre_mm_reshape = match[0] if is_reshape_mm_reshape_pattern else None + post_mm_reshape = match[-1] if is_reshape_mm_reshape_pattern else None + A_node = cast("torch.fx.Node", mm_node.args[0]) + B_node = cast("torch.fx.Node", mm_node.args[1]) + A_scale_node = cast("torch.fx.Node", mm_node.args[2]) + B_scale_node = cast("torch.fx.Node", mm_node.args[3]) + + return _ScaledMatmul( + nodes=match, + A_node=A_node, + B_node=B_node, + A_scale_node=A_scale_node, + B_scale_node=B_scale_node, + bias_node=get_arg(mm_node, 4, None), + result_scale_node=get_arg(mm_node, 5, None), + out_dtype=get_arg(mm_node, 6, None), + use_fast_accum=get_arg(mm_node, 7, False), + pre_mm_reshape=pre_mm_reshape, + post_mm_reshape=post_mm_reshape, + ) + + +def _find_reshape_mm_reshape(node: torch.fx.Node) -> list[_Matmul]: + if node.target != aten.reshape.default: + return [] + + matches = [] + for mm_node in node.users: + if mm_node.target not in (aten.mm.default, aten._scaled_mm.default): + continue + for reshape_node in mm_node.users: + if reshape_node.target != aten.reshape.default: + continue + + # Since the reshape -> mm -> reshape pattern would be subsumed into + # the fused op, we only match the patterns where the shape of the + # second reshape is matches the mm result produced by the fused op. + matmul_input_node = cast("torch.fx.Node", node.args[0]) + B_node = cast("torch.fx.Node", mm_node.args[1]) + matmul_out_shape = torch.Size( + [ + *_get_tensor(matmul_input_node).shape[:-1], + _get_tensor(B_node).shape[-1], + ] + ) + if _get_tensor(reshape_node).shape != matmul_out_shape: + continue + matches.append([node, mm_node, reshape_node]) + # If for some rare reason mm_node is being reshaped by two + # different reshape nodes, we only include mm_node once in the + # parsing result. + break + + matmuls = [] + for match in matches: + mm_node = match[1] + if mm_node.target is aten.mm.default: + matmul = _Matmul.from_match(match) + matmuls.append(matmul) + elif mm_node.target is aten._scaled_mm.default: + matmul = _ScaledMatmul.from_match(match) + matmuls.append(matmul) + else: + raise AssertionError( + "Expect the node's target to be either aten.mm.default or " + f"aten._scaled_mm.default. Got {mm_node.target}." + ) + return matmuls + + +def _find_consumer_matmuls(node: torch.fx.Node) -> list[_Matmul]: + """ + Find the matmuls that use `node` as the lhs argument. + """ + matmuls = [] + for user in node.users: + # ND matmuls + if user.target is aten.reshape.default: + matmuls.extend(_find_reshape_mm_reshape(user)) + # 2D matmuls + elif user.target is aten.mm.default: + matmul = _Matmul.from_match(match=[user]) + matmuls.append(matmul) + elif user.target is aten._scaled_mm.default: + matmul = _ScaledMatmul.from_match([user]) + matmuls.append(matmul) + return matmuls + + +def _insert_fused_all_gather_matmul( + graph: torch.fx.Graph, + matmuls: list[_Matmul], + shard_node: torch.fx.Node, + gather_dim: int, + group_name: "torch.distributed.distributed_c10d.GroupName", +) -> torch.fx.Node: + mm_types = OrderedSet(map(type, matmuls)) + assert len(mm_types) == 1 + mm_type = next(iter(mm_types)) + if mm_type == _Matmul: + B_nodes = [matmul.B_node for matmul in matmuls] + return graph.call_function( + torch.ops.symm_mem.fused_all_gather_matmul.default, + args=(shard_node, B_nodes, gather_dim, group_name), + kwargs={"return_A": True}, + ) + elif mm_type == _ScaledMatmul: + scaled_matmuls = cast("list[_ScaledMatmul]", matmuls) + return graph.call_function( + torch.ops.symm_mem.fused_all_gather_scaled_matmul.default, + args=( + shard_node, + [matmul.B_node for matmul in scaled_matmuls], + scaled_matmuls[0].A_scale_node, + [matmul.B_scale_node for matmul in scaled_matmuls], + gather_dim, + group_name, + [matmul.bias_node for matmul in scaled_matmuls], + [matmul.result_scale_node for matmul in scaled_matmuls], + [matmul.out_dtype for matmul in scaled_matmuls], + [matmul.use_fast_accum for matmul in scaled_matmuls], + ), + ) + else: + raise AssertionError(f"Unexpected matmul match type: {mm_type}") + + +def fuse_all_gather_matmul(all_gather: _AllGatherMatch) -> None: + """ + Fused the pattern + + A = all_gather_tensor(A_shard, gather_dim, group_name) + C_0 = torch.matmul(A, B_0) + C_1 = torch.matmul(A, B_1) + C_2 = torch.matmul(A, B_2) + ... + + into + + A, Cs = torch.ops.symm_mem.fused_all_gather_matmul( + A_shard, [B_0, B_1, B_2, ...], gather_dim, group_name, + ) + """ + if ( + not torch.distributed.is_available() + or not torch.distributed.is_nccl_available() + ): + return + + from torch.distributed._symmetric_memory import ( + is_symm_mem_enabled_for_group, + restride_A_shard_for_fused_all_gather_matmul, + ) + + shard_node, ag_node, ag_res_node, gather_dim, group_name = ( + all_gather.shard_node, + all_gather.ag_node, + all_gather.res_node, + all_gather.gather_dim, + all_gather.group_name, + ) + + if not is_symm_mem_enabled_for_group(group_name): + return + + filter_matmul = None + if _is_last_dim(_get_tensor(shard_node), gather_dim): + # Decomposed mms should not be too small + if _get_tensor(shard_node).shape[-1] < 1024: + return + + # scaled_mm is not supported yet for last dim + def _filter_out_scaled_matmul(matmul: _Matmul): + return not isinstance(matmul, _ScaledMatmul) + + filter_matmul = _filter_out_scaled_matmul + + # Find consumer matmuls + matmuls = _find_consumer_matmuls(ag_res_node) + + # The matmuls are only fusible if non-A args don't depend on the all-gather + # result node + matmuls = [ + matmul + for matmul in matmuls + if all_gather.res_node not in matmul.arg_ancestor_nodes + ] + + if len(matmuls) == 0 or len(OrderedSet(map(type, matmuls))) != 1: + return + + if _is_last_dim(_get_tensor(shard_node), gather_dim) and len( + all_gather.res_node.users + ) > len(matmuls): + # The result of ag-split-cat is used not only in matmuls. + # Then it has to be materialized, which can have overhead. + return + + if filter_matmul and not filter_matmul(matmuls[0]): + return + + # Fuse the all_gather_tensor with the eligible matmuls + graph = ag_node.graph + with graph.inserting_before(ag_node): + if not _is_last_dim(_get_tensor(shard_node), gather_dim): + if "val" in shard_node.meta: + restrided = restride_A_shard_for_fused_all_gather_matmul( + _get_tensor(shard_node), + gather_dim, + ) + shard_node = graph.call_function( + inductor_prims.force_stride_order, + args=(shard_node, restrided.stride()), + ) + + fused_node = _insert_fused_all_gather_matmul( + graph, matmuls, shard_node, gather_dim, group_name + ) + new_ag_node = graph.call_function( + operator.getitem, + args=(fused_node, 0), + ) + new_out_nodes = graph.call_function( + operator.getitem, + args=(fused_node, 1), + ) + for idx, matmul in enumerate(matmuls): + new_out_node = graph.call_function( + operator.getitem, + args=(new_out_nodes, idx), + ) + matmul.replace_with(new_out_node) + matmul.erase() + all_gather.replace_with(new_ag_node) + all_gather.erase() + + # If the new_ag_node has no users, we tell the fused op to not return + # it. This creates more optimization opportunities. + if len(new_ag_node.users) == 0: + graph.erase_node(new_ag_node) + kwargs = dict(fused_node.kwargs) + if "return_A" in kwargs: + kwargs["return_A"] = False + fused_node.kwargs = kwargs + + # Raise ancestors of non-A args that are topologically ordered between + # ag_res_node and the matmul above fused_node. + order = {node: idx for idx, node in enumerate(graph.nodes)} + nodes_to_raise = sorted( + OrderedSet(x for matmul in matmuls for x in matmul.arg_ancestor_nodes), + key=lambda x: order[x], + ) + for node in nodes_to_raise: + if order[node] > order[fused_node]: + fused_node.prepend(node) + + +def _scatter_dim_after_reshape( + reshape_node: torch.fx.Node, orig_scatter_dim: int +) -> int: + """ + Given a reshape node and the original scatter dim for the target tensor, + returns the new scatter dim for the reshaped tensor. + """ + # if there was no pre-mm reshape, scatter dim will not change. + if not reshape_node: + return orig_scatter_dim + + reshape_op_output_tensor = _get_tensor(reshape_node) + assert reshape_op_output_tensor.ndim == 2, ( + "reshape must produce 2D tensor for scaled_mm" + ) + + assert len(reshape_node.args) >= 1, "reshape node must have at least 1 arg" + input_tensor_node = cast(torch.fx.Node, reshape_node.args[0]) + reshape_op_input_tensor = _get_tensor(input_tensor_node) + assert reshape_op_input_tensor.ndim > reshape_op_output_tensor.ndim, ( + "reshape must be from 3D+ to 2D" + ) + + # Note: for a N-D tensor to be reshaped into 2D, either the leading dims or ending dims must + # be collapsed to a single dim. First determine which of these happened. + input_shape = reshape_op_input_tensor.shape + output_shape = reshape_op_output_tensor.shape + leading_dims_collapsed = output_shape[0] == prod(input_shape[:-1]) + + # Case 1: scatter dim 0 always maps to 0 after any reshape from 3D+ to 2D, regardless if + # leading dims or ending dims were collapsed. + if orig_scatter_dim == 0: + return 0 + + # Case 2: scatter dim "ndim-1" always maps to 1 after any reshape from 3D+ to 2D, regardless if + # leading dims or ending dims were collapsed. + if orig_scatter_dim == reshape_op_input_tensor.ndim - 1: + return 1 + + # Case 3: scatter dim was one of the middle dims (between 0 and ndim-1). + # if the leading dims were collapsed, the new scatter dim will be 0. + # if the ending dims were collapsed, the new scatter dim will be 1. + return 0 if leading_dims_collapsed else 1 + + +def _find_producer_matmul(node: torch.fx.Node) -> _Matmul | None: + """ + Returns producer matmul node if found, otherwise returns None. + """ + if node.target is aten.mm.default: + return _Matmul.from_match(match=[node]) + elif node.target is aten._scaled_mm.default: + return _ScaledMatmul.from_match(match=[node]) + elif node.target is aten.reshape.default: + reshape_node_1 = node + + mm_node = reshape_node_1.args[0] + assert isinstance(mm_node, torch.fx.Node) + if mm_node.target not in (aten.mm.default, aten._scaled_mm.default): + return None + + reshape_node_0 = mm_node.args[0] + assert isinstance(reshape_node_0, torch.fx.Node) + if reshape_node_0.target != aten.reshape.default: + return None + + if mm_node.target is aten.mm.default: + return _Matmul.from_match(match=[reshape_node_0, mm_node, reshape_node_1]) + elif mm_node.target is aten._scaled_mm.default: + return _ScaledMatmul.from_match( + match=[reshape_node_0, mm_node, reshape_node_1] + ) + return None + + +def _insert_fused_matmul_reduce_scatter( + graph: torch.fx.Graph, + matmul: _Matmul, + reduce_op: str, + orig_scatter_dim: int, + group_name: "torch.distributed.distributed_c10d.GroupName", + scatter_dim_after_reshape: int, # only used for reshape -> scaled_mm -> reshape pattern + output_shape: list[int], # only used for reshape -> scaled_mm -> reshape pattern +) -> torch.fx.Node: + if type(matmul) is _Matmul: + return graph.call_function( + torch.ops.symm_mem.fused_matmul_reduce_scatter.default, + args=( + matmul.A_node, + matmul.B_node, + reduce_op, + orig_scatter_dim, + group_name, + ), + ) + elif type(matmul) is _ScaledMatmul: + return graph.call_function( + torch.ops.symm_mem.fused_scaled_matmul_reduce_scatter.default, + args=( + matmul.A_node, + matmul.B_node, + matmul.A_scale_node, + matmul.B_scale_node, + reduce_op, + orig_scatter_dim, + scatter_dim_after_reshape, + group_name, + output_shape, + matmul.bias_node, + matmul.result_scale_node, + matmul.out_dtype, + matmul.use_fast_accum, + ), + ) + else: + raise AssertionError(f"Unexpected matmul match type: {type(matmul)}") + + +def fuse_matmul_reduce_scatter(reduce_scatter: _ReduceScatterMatch) -> None: + """ + Fused the pattern + + reduce_scatter_tensor(A @ B, scatter_dim, group_name) + + into + + torch.ops.symm_mem.fused_matmul_reduce_scatter( + A, B, scatter_dim, group_name, + ) + + Returns boolean indicating if fusion was successful or not. + """ + if ( + not torch.distributed.is_available() + or not torch.distributed.is_nccl_available() + ): + return + + from torch.distributed._symmetric_memory import ( + is_symm_mem_enabled_for_group, + restride_A_for_fused_matmul_reduce_scatter, + ) + + ( + input_node, + _reduce_scatter_node, + rs_wait_tensor_node, + reduce_op, + orig_scatter_dim, + group_name, + ) = ( + reduce_scatter.input_node, + reduce_scatter.reduce_scatter_node, + reduce_scatter.wait_tensor_node, + reduce_scatter.reduce_op, + reduce_scatter.scatter_dim, + reduce_scatter.group_name, + ) + + if not is_symm_mem_enabled_for_group(group_name): + return + + filter_matmul = None + if _is_last_dim(_get_tensor(input_node), orig_scatter_dim): + # scaled_mm is not supported yet for last dim mm+rs + def _filter_out_scaled_matmul(matmul: _Matmul): + return not isinstance(matmul, _ScaledMatmul) + + filter_matmul = _filter_out_scaled_matmul + + # Currently fused_matmul_reduce_scatter doesn't return the matmul result, + # so we can't apply the fusion if the matmul result is used by multiple + # users. This is not a fundamental limitation of the fused op and can be + # addressed if needed. + if len(input_node.users) != 1: + log.warning( + "matmul result has more than one user, skipping fused_matmul_reduce_scatter fusion." + ) + return + + matmul = _find_producer_matmul(input_node) + + if matmul is None: + log.warning( + "no producer matmul found for reduce scatter, skipping fuse_matmul_reduce_scatter fusion" + ) + return + + if filter_matmul and not filter_matmul(matmul): + return + + if rs_wait_tensor_node in matmul.arg_ancestor_nodes: + log.warning( + "reduce-scatter result node is an ancestor of matmul, skipping fuse_matmul_reduce_scatter fusion" + ) + return + + # We need to track 3 values for the fused scaled mm reduce scatter implementation: + # 1. The scatter dim before the reshape, which was assigned using the original (a,b,c) @ (c,d) = (a,b,d) dims. + # 2. The scatter dim after the reshape, to use when we are doing the 2D (a*b,c) @ (c,d) = (a,b,d) scaled mm op. + # 3. Store expected potentially 3D+ mm output shape, so we can reshape the 2D mm output to the intended + # 3D+ shape before applying reduce-scatter, and to prevent shape errors with subsequent ops. + + # If 'A' was reshaped from 3D+ -> 2D for the mm, we need to determine the new scattter dim after the reshape + # for the fused matmul reduce scatter implementation to use. + if matmul.pre_mm_reshape: + scatter_dim_after_maybe_reshape = _scatter_dim_after_reshape( + matmul.pre_mm_reshape, orig_scatter_dim + ) + else: + scatter_dim_after_maybe_reshape = orig_scatter_dim + + # If the 2D mm output was reshaped from 2D -> 3D+, we need to store the intended output shape for the + # fused matmul reduce scatter implementation to use. + if matmul.post_mm_reshape: + output_shape = list(_get_tensor(matmul.post_mm_reshape).shape) + else: + A_orig_shape = list(_get_tensor(matmul.A_node).shape) + B_shape = list(_get_tensor(matmul.B_node).shape) + output_shape = [*A_orig_shape[:-1], B_shape[-1]] + + graph = rs_wait_tensor_node.graph + with graph.inserting_before(rs_wait_tensor_node): + # Restride A tensor before fused op, for optimal perf in fused matmul reduce scatter + if "val" in matmul.A_node.meta: + restrided = restride_A_for_fused_matmul_reduce_scatter( + _get_tensor(matmul.A_node), + scatter_dim_after_maybe_reshape, + ) + matmul.A_node = graph.call_function( + inductor_prims.force_stride_order, + args=(matmul.A_node, restrided.stride()), + ) + + # Replace matched subgraph with fused matmul reduce scatter node + fused_node = _insert_fused_matmul_reduce_scatter( + graph, + matmul, + reduce_op, + orig_scatter_dim, + group_name, + scatter_dim_after_maybe_reshape, + output_shape, + ) + reduce_scatter.replace_with(fused_node) + reduce_scatter.erase() + matmul.erase() + + order = {node: idx for idx, node in enumerate(graph.nodes)} + nodes_to_raise = sorted( + matmul.arg_ancestor_nodes, + key=lambda x: order[x], + ) + for node in nodes_to_raise: + if order[node] > order[fused_node]: + fused_node.prepend(node) + + log.debug("successfully fused matmul reduce scatter") + + +def _get_node_to_ancestors( + graph: torch.fx.Graph, +) -> dict[torch.fx.Node, OrderedSet[torch.fx.Node]]: + """ + Compute the ancestors for all nodes in a graph. + """ + node_to_ancestors = defaultdict(OrderedSet[torch.fx.Node]) # type: ignore[var-annotated] + for node in graph.nodes: + node_to_ancestors[node] = OrderedSet(node.all_input_nodes) + for dep in node.all_input_nodes: + node_to_ancestors[node] |= node_to_ancestors[dep] + + return node_to_ancestors + + +def _get_collective_to_overlappable_nodes( + graph: torch.fx.Graph, +) -> dict[torch.fx.Node, list[torch.fx.Node]]: + """ + For each collective in the graph, find nodes that are neither ancestors nor + descendants of the collective. + """ + + def is_collective(node) -> bool: + # Only consider all-gather and reduce-scatter in the context of + # micro-pipeline TP. + return node.target in [ + torch.ops._c10d_functional.all_gather_into_tensor.default, + torch.ops._c10d_functional.reduce_scatter_tensor.default, + ] + + node_to_ancestors = _get_node_to_ancestors(graph) + collective_to_overlappable_nodes = defaultdict(list) + for node in graph.nodes: + if not is_collective(node): + continue + for x in graph.nodes: + if ( + node not in node_to_ancestors[x] + and x not in node_to_ancestors[node] + and x.op == "call_function" + ): + collective_to_overlappable_nodes[node].append(x) + + return collective_to_overlappable_nodes + + +def _get_unexposed_collectives(graph: torch.fx.Graph) -> list[torch.fx.Node]: + """ + Find all unexposed collectives in the graph. + + Because we don't have the runtime estimate, this function is a rough + estimation using the following strong/hand-wavy assumptions: + + - Only a predefined set of "compute intensive" operation can hide a collective. + - Any "compute intensive" operation can hide exactly one collective. + """ + + def _is_compute_intensive(node: torch.fx.Node) -> bool: + return node.target is torch.ops.aten.mm.default + + collective_to_overlapping_candidates = defaultdict(list) + available_nodes = OrderedSet[torch.fx.Node]() + collective_to_overlappable_nodes = _get_collective_to_overlappable_nodes(graph) + for collective, overlappable_nodes in collective_to_overlappable_nodes.items(): + candidates = [x for x in overlappable_nodes if _is_compute_intensive(x)] + collective_to_overlapping_candidates[collective] = candidates + available_nodes.update(candidates) + + unexposed_collectives = [] + for ( + collective, + overlapping_candidates, + ) in collective_to_overlapping_candidates.items(): + # Each collective consumes exactly one overlapping candidate + for x in overlapping_candidates: + if x in available_nodes: + unexposed_collectives.append(collective) + available_nodes.remove(x) + break + return unexposed_collectives + + +def micro_pipeline_tp_pass(graph: torch.fx.Graph): + all_gathers = find_all_gather_patterns(graph) + reduce_scatters = find_reduce_scatter_patterns(graph) + + # When a collective can be hidden through either simple overlapping or + # micro-pipeline TP, we prefer simple overlapping to avoid the overhead + # associated with decomposition. If reorder_for_compute_comm_overlap is + # enabled, we identify collectives that can be hidden through simple + # overlapping and exclude them from micro-pipeline TP candidates. + if config.reorder_for_compute_comm_overlap: + unexposed_collectives = _get_unexposed_collectives(graph) + all_gathers = [x for x in all_gathers if x.ag_node not in unexposed_collectives] + reduce_scatters = [ + x + for x in reduce_scatters + if x.reduce_scatter_node not in unexposed_collectives + ] + + if not all_gathers and not reduce_scatters: + log.warning( + "async TP found no matching all-gather/reduce-scatter patterns for fusion" + ) + + for all_gather in all_gathers: + fuse_all_gather_matmul(all_gather) + + for reduce_scatter in reduce_scatters: + fuse_matmul_reduce_scatter(reduce_scatter) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/misc_patterns.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/misc_patterns.py new file mode 100644 index 0000000000000000000000000000000000000000..4179e0441f36dfb623b07e6eabbe08ca8eece2de --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/misc_patterns.py @@ -0,0 +1,219 @@ +# mypy: allow-untyped-defs +import functools + +import torch +from torch._dynamo.utils import counters +from torch._ops import OpOverload, OpOverloadPacket +from torch.utils._ordered_set import OrderedSet + +from ..pattern_matcher import fwd_only, register_replacement + + +aten = torch.ops.aten + + +@functools.cache +def _misc_patterns_init(input_device: torch.device | None = None): + from .joint_graph import patterns as joint_graph_patterns + from .post_grad import pass_patterns as post_grad_patterns_all + + post_grad_patterns = post_grad_patterns_all[1] # medium priority + + if input_device: + device = str(input_device) + else: + if torch.cuda.is_available(): + # workaround https://github.com/pytorch/pytorch/issues/97894 + device = "cuda" + else: + device = "cpu" + + # These patterns do 2 things + # 1. Since we know that index is completely unique, we can codegen it using + # stores instead of atomic adds, which is quite a bit faster. + # 2. Also, since we are guaranteed that they are completely within bounds, + # we can use unsafe indexing and skip debug asserts + def randperm_index_add_pattern(x, y): + index = torch.randperm(x.shape[0], device=x.device)[: y.shape[0]] + return torch.index_add(x, dim=0, source=y, index=index), index + + def randperm_index_add_replacement(x, y): + index = torch.randperm(x.shape[0], device=x.device)[: y.shape[0]] + return ( + torch.ops.aten._unsafe_index_put( + x, (index,), aten._unsafe_index(x, (index,)) + y, accumulate=False + ), + index, + ) + + register_replacement( + # pyrefly: ignore [bad-argument-type] + randperm_index_add_pattern, + # pyrefly: ignore [bad-argument-type] + randperm_index_add_replacement, + [torch.empty(4, 8, device=device), torch.empty(2, 8, device=device)], + # pyrefly: ignore [bad-argument-type] + fwd_only, + # pyrefly: ignore [bad-argument-type] + [post_grad_patterns, joint_graph_patterns], + skip_duplicates=True, + ) + + def randperm_index_pattern(x, slice_shape): + index = torch.randperm(x.shape[0], device=x.device)[:slice_shape] + return torch.ops.aten.index(x, (index,)), index + + def randperm_index_replacement(x, slice_shape): + index = torch.randperm(x.shape[0], device=x.device)[:slice_shape] + return torch.ops.aten._unsafe_index(x, (index,)), index + + register_replacement( + # pyrefly: ignore [bad-argument-type] + randperm_index_pattern, + # pyrefly: ignore [bad-argument-type] + randperm_index_replacement, + [torch.empty(4, 8, device=device)], + # pyrefly: ignore [bad-argument-type] + fwd_only, + # pyrefly: ignore [bad-argument-type] + [post_grad_patterns, joint_graph_patterns], + scalar_workaround={"slice_shape": 42}, + skip_duplicates=True, + ) + + # Pattern: e8m0 extraction with ceiling rounding (for MX format scaling) + # Only register on SM100+ where the PTX instruction is available + if device == "cuda" and torch.cuda.get_device_capability() >= (10, 0): + from .. import inductor_prims + + # Pattern 1: Bit manipulation approach + def e8m0_rceil_pattern(inp): + inp_bits = inp.view(torch.int32) + biased_exp = (inp_bits >> 23) & 0xFF + mantissa = inp_bits & 0x7FFFFF + needs_round_up = mantissa != 0 + e8m0_biased = biased_exp + needs_round_up.to(torch.int32) + e8m0_biased = torch.clamp(e8m0_biased, 0, 255) + return e8m0_biased.to(torch.uint8) + + def e8m0_rceil_replacement(inp): + return inductor_prims.cvt_e8m0_rceil(inp) + + def e8m0_extra_check(match): + inp = match.kwargs.get("inp") + if inp is None: + return False + inp_val = inp.meta.get("val") + return ( + inp_val is not None + and inp_val.device.type == "cuda" + and inp_val.dtype == torch.float32 + ) + + register_replacement( + # pyrefly: ignore [bad-argument-type] + e8m0_rceil_pattern, + # pyrefly: ignore [bad-argument-type] + e8m0_rceil_replacement, + [torch.randn(32, device="cuda", dtype=torch.float32)], + # pyrefly: ignore [bad-argument-type] + fwd_only, + # pyrefly: ignore [bad-argument-type] + [post_grad_patterns], + extra_check=e8m0_extra_check, + ) + + # Pattern 2: log2 + ceil approach (used by torchao MX formats) + # Matches: (clamp(ceil(log2(x)), -127, 127) + 127).to(uint8) + E8M0_BIAS = 127 + + def e8m0_rceil_log2_pattern(inp): + log2_val = torch.log2(inp) + ceil_val = torch.ceil(log2_val) + clamped = torch.clamp(ceil_val, min=-E8M0_BIAS, max=E8M0_BIAS) + biased = clamped + E8M0_BIAS + return biased.to(torch.uint8) + + def e8m0_rceil_log2_replacement(inp): + # The PTX instruction expects the raw float value, not log2 + # So we need to convert: if inp is log2(x), then 2^inp is x + # But actually our pattern matches on the value before log2 + return inductor_prims.cvt_e8m0_rceil(inp) + + register_replacement( + # pyrefly: ignore [bad-argument-type] + e8m0_rceil_log2_pattern, + # pyrefly: ignore [bad-argument-type] + e8m0_rceil_log2_replacement, + [torch.randn(32, device="cuda", dtype=torch.float32).abs() + 1e-10], + # pyrefly: ignore [bad-argument-type] + fwd_only, + # pyrefly: ignore [bad-argument-type] + [post_grad_patterns], + extra_check=e8m0_extra_check, + ) + + # TODO: Add pattern for cvt.rn.bf16x2.ue8m0x2 (e8m0 -> bf16 conversion) + # This is the inverse operation for MX format dequantization + + +class NumpyCompatNormalization: + numpy_compat: dict[str, tuple[str, ...]] = { + "dim": ("axis",), + "keepdim": ("keepdims",), + "input": ("x", "a", "x1"), + "other": ("x2",), + } + inverse_mapping: dict[str, str] + cache: dict["torch.fx.graph.Target", OrderedSet[str]] + + def __init__(self) -> None: + self.cache = {} # callable -> tuple of replaceable args e.g. ["axis"] + self.inverse_mapping = {} + for actual_kwarg, numpy_kwargs in self.numpy_compat.items(): + for numpy_kwarg in numpy_kwargs: + assert numpy_kwarg not in self.inverse_mapping + self.inverse_mapping[numpy_kwarg] = actual_kwarg + + def __call__(self, graph: torch.fx.Graph): + for node in graph.nodes: + if node.op != "call_function": + continue + if isinstance(node.target, (OpOverload, OpOverloadPacket)): + # only applies to torch ops; e.g. torch.stack(axis=1) works, torch.ops.aten.stack(axis=1) doesn't. + continue + kwargs = node.kwargs + + if node.target in self.cache: + replaceable_kwargs = self.cache[node.target] + else: + signatures = torch.fx.operator_schemas.get_signature_for_torch_op( + node.target + ) + signatures = () if signatures is None else signatures + replaceable_kwargs = OrderedSet() + for sig in signatures: + for param_name in sig.parameters: + if param_name in self.numpy_compat: + replaceable_kwargs.update(self.numpy_compat[param_name]) + + self.cache[node.target] = replaceable_kwargs + + if not replaceable_kwargs: + continue + + new_kwargs = {} + kwargs_changed = False + for k, v in kwargs.items(): + if k in replaceable_kwargs: + kwargs_changed = True + new_kwargs[self.inverse_mapping[k]] = v + else: + new_kwargs[k] = v + + if kwargs_changed: + node.kwargs = torch.fx.immutable_collections.immutable_dict(new_kwargs) + counters["inductor"]["numpy_compat_normalization"] += 1 + + +numpy_compat_normalization = NumpyCompatNormalization() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/mkldnn_fusion.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/mkldnn_fusion.py new file mode 100644 index 0000000000000000000000000000000000000000..c1f0a7284484e7b25c07923f7ca71613a630a460 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/mkldnn_fusion.py @@ -0,0 +1,1591 @@ +# mypy: allow-untyped-defs +import functools +import operator +from functools import reduce +from typing import Any, TYPE_CHECKING + +import torch +from torch._dynamo.utils import counters +from torch.fx.experimental.symbolic_shapes import has_free_symbols, optimization_hint +from torch.utils._ordered_set import OrderedSet + +from .. import ir, mkldnn_ir +from ..lowering import lowerings as L +from ..pattern_matcher import ( + Arg, + CallFunction, + filter_nodes, + get_arg_value, + KeywordArg, + MULTIPLE, +) +from ..utils import ( + is_mkldnn_bf16_supported, + is_mkldnn_fp16_supported, + SUPPORTED_MKLDNN_DEVICES, +) +from ..virtualized import ops, V +from .freezing_patterns import register_freezing_graph_pattern +from .post_grad import register_lowering_pattern +from .quantization import ( + _register_int8_woq_concat_linear_pattern, + _register_quantization_lowerings, + _register_quantization_weight_pack_pass, + _register_woq_lowerings, +) + + +if TYPE_CHECKING: + from collections.abc import Callable + + +if torch._C._has_mkldnn: + aten = torch.ops.aten + mkldnn = torch.ops.mkldnn + prims = torch.ops.prims + + _conv_args = [Arg() for _ in range(10)] + _linear_args = [Arg() for _ in range(6)] + _conv_transpose_args = [Arg() for _ in range(11)] + + class MkldnnDeviceOpBase: + def get_linear_transpose_weight(self, weight_node): + raise NotImplementedError + + def pack_conv_weight( + self, + graph, + is_transposed, + weight, + constant_args, + input_size, + ): + raise NotImplementedError + + def pack_linear_weight( + self, graph, is_lp_weight, transpose_weight_node, batch_size + ): + raise NotImplementedError + + def pack_linear( + self, graph, is_lp_weight, batch_size, input, packed_weight_node, bias + ): + raise NotImplementedError + + class CpuMkldnnDeviceOp(MkldnnDeviceOpBase): + def get_linear_transpose_weight(self, weight_node): + packed_weight_node = weight_node + assert packed_weight_node.target == mkldnn._reorder_linear_weight + transpose_weight_node = packed_weight_node.args[0] + assert transpose_weight_node.target is aten.permute.default + return transpose_weight_node + + def pack_conv_weight( + self, + graph, + is_transposed, + weight, + constant_args, + input_size, + ): + packed_weight_op = mkldnn._reorder_convolution_weight + if is_transposed: + packed_weight_op = mkldnn._reorder_convolution_transpose_weight + + # mkldnn_reorder_conv_weight(self, padding, stride, dilation, groups, input_size) + packed_weight_inputs = (weight,) + tuple(constant_args) + (input_size,) + return graph.create_node( + "call_function", packed_weight_op, args=packed_weight_inputs + ) + + def pack_linear_weight( + self, graph, is_lp_weight, transpose_weight_node, batch_size + ): + # For bfloat16 dynamic shape path, using input size hint to pack weight for a better performance. + packed_weight_inputs = ( + transpose_weight_node, + optimization_hint(batch_size) + if has_free_symbols(batch_size) + else batch_size, + ) + + # MKL packed matrix can't be copied to a different address because the internal implementation + # depends on the alignment of internally-stored metadata. + # In aot mode, we need to firstly save the packed weight, when loading it, + # it will be in a different address which doesn't work. + # Disable MKL prepack linear in AOT mode. + # Disable MKL prepack linear when batch_size has free symbols. + packed_weight_op = ( + mkldnn._reorder_linear_weight + if ( + is_lp_weight + or mkldnn._is_mkldnn_acl_supported() + or V.aot_compilation + or has_free_symbols(batch_size) + ) + else torch.ops.mkl._mkl_reorder_linear_weight + ) + return graph.create_node( + "call_function", packed_weight_op, args=packed_weight_inputs + ) + + def pack_linear( + self, graph, is_lp_weight, batch_size, input, packed_weight_node, bias + ): + packed_linear_inputs: tuple[Any, ...] = (input, packed_weight_node) + transpose_weight_node = packed_weight_node.args[0] + if ( + is_lp_weight + or mkldnn._is_mkldnn_acl_supported() + or V.aot_compilation + or has_free_symbols(batch_size) + ): + packed_linear_inputs += (bias, "none", [], "") + packed_linear_op: Callable[..., Any] = mkldnn._linear_pointwise.default + else: + packed_linear_inputs += (transpose_weight_node, bias, batch_size) + packed_linear_op = torch.ops.mkl._mkl_linear + + return graph.create_node( + "call_function", packed_linear_op, packed_linear_inputs + ) + + class XpuMkldnnDeviceOp(MkldnnDeviceOpBase): + def pack_conv_weight( + self, + graph, + is_transposed, + weight, + constant_args, + input_size, + ): + assert not is_transposed, ( + "'mkldnn::_convolution_transpose_pointwise' is not currently implemented for the XPU device." + ) + return weight + + def _get_mkldnn_device_op(device_type: str) -> MkldnnDeviceOpBase: + """ + Returns the MKLDNN device operation class based on the current device type. + """ + if device_type == "cpu": + return CpuMkldnnDeviceOp() + elif device_type == "xpu": + return XpuMkldnnDeviceOp() + else: + raise RuntimeError(f"MKLDNN is not supported on {device_type} device.") + + def _is_valid_grouped_gemm_fusion(computation_nodes): + """ + Here we check: + 1. More than 1 GEMM nodes has been found. + 2. All the GEMM nodes share the same activation. + 3. All the GEMM nodes have same weight size but different wgt node. + """ + computation_op = mkldnn._linear_pointwise.default + act = computation_nodes[0].args[0] + wgt = computation_nodes[0].args[1] + wgt_size = wgt.meta.get("val").size() # type: ignore[union-attr] + return len(computation_nodes) >= 2 and all( + ( + node.target == computation_op + and node.args[0] == act + and (node.args[1].meta.get("val").size() == wgt_size) + and (node.args[1] != wgt or gemm_idx == 0) + ) + for gemm_idx, node in enumerate(computation_nodes) + ) + + def grouped_gemm_pass(graph: torch.fx.Graph): + """ + Group GEMM has multi output nodes which is complicated to define a Pattern. + Use below way to connect the pattern to the lowering. + TODO: Use MultiOutputPattern, current limitation is the pattern requires + fixed number of output nodes. Extend to support Group GEMM for pattern matcher. + """ + computation_op = mkldnn._linear_pointwise.default + from ..mkldnn_lowerings import grouped_gemm_lowering + + for node in graph.find_nodes(op="call_function", target=computation_op): + if ( + not node._erased + and isinstance(node.meta.get("val"), torch.Tensor) + and node.meta["val"].device.type == "cpu" + ): + act = node.args[0] + users = list(act.users) + if _is_valid_grouped_gemm_fusion(users): + with graph.inserting_before(node): + grouped_gemm_node = graph.create_node( + "call_function", + grouped_gemm_lowering, + ( + act, + [user.args[1] for user in users], + [user.args[2] for user in users], + ), + ) + grouped_gemm_node.meta["val"] = [ + user.meta["val"] for user in users + ] + with graph.inserting_after(grouped_gemm_node): + for gemm_idx, user in enumerate(users): + assert user.target == computation_op + get_item = graph.create_node( + "call_function", + operator.getitem, + ( + grouped_gemm_node, + gemm_idx, + ), + ) + user.replace_all_uses_with(get_item) + graph.erase_node(user) + return + + def _conv_call(users=1): + return CallFunction( + mkldnn._convolution_pointwise.default, *_conv_args, _users=users + ) + + def _linear_call(users=1): + return CallFunction( + mkldnn._linear_pointwise.default, *_linear_args, _users=users + ) + + def _conv_transpose_call(users=1): + return CallFunction( + mkldnn._convolution_transpose_pointwise.default, + *_conv_transpose_args, + _users=users, + ) + + def _to_float(input_call, users=1): + return CallFunction( + prims.convert_element_type.default, + input_call, + KeywordArg("to_float"), + _users=users, + ) + + def _to_bf16(input_call): + return CallFunction( + prims.convert_element_type.default, + input_call, + KeywordArg("to_bf16"), + _users=1, + ) + + def _to_fp16(input_call): + return CallFunction( + prims.convert_element_type.default, + input_call, + KeywordArg("to_fp16"), + _users=1, + ) + + def _unary_fusion_pattern(unary_fusion, call_fn, users, lowp_dtype): + # only insert to_dtype if lowp_dtype is True + computation_call = ( + _to_float(call_fn(), users=users) if lowp_dtype else call_fn(users=users) + ) + out = unary_fusion(computation_call) + if lowp_dtype == torch.bfloat16: + return _to_bf16(out) + elif lowp_dtype == torch.float16: + return _to_fp16(out) + else: + return out + + def _gelu_fusion_1(computation_call): + return CallFunction( + aten.mul, + CallFunction(aten.mul, computation_call, 0.5), + CallFunction( + aten.add, + CallFunction( + aten.erf, + CallFunction(aten.mul, computation_call, 0.7071067811865476), + ), + 1, + ), + ) + + def _gelu_fusion_2(computation_call): + return CallFunction( + aten.mul, + CallFunction(aten.mul, computation_call, 0.5), + CallFunction( + aten.add, + CallFunction( + aten.tanh, + CallFunction( + aten.mul, + CallFunction( + aten.add, + computation_call, + CallFunction( + aten.mul, + CallFunction( + aten.mul, + CallFunction( + aten.mul, computation_call, computation_call + ), + computation_call, + ), + 0.044715, + ), + ), + 0.7978845608028654, + ), + ), + 1, + ), + ) + + def _hardswish_fusion(computation_call): + return CallFunction( + aten.div, + CallFunction( + aten.mul, + computation_call, + CallFunction( + aten.clamp_max, + CallFunction( + aten.clamp_min, CallFunction(aten.add, computation_call, 3), 0 + ), + 6, + ), + ), + 6, + ) + + def _silu_fusion(computation_call): + # Match: x / (exp(-x) + 1) pattern used by inductor's silu decomposition + return CallFunction( + aten.div, + computation_call, + CallFunction( + aten.add, + CallFunction(aten.exp, CallFunction(aten.neg, computation_call)), + 1, + ), + ) + + def _hardsigmoid_fusion(computation_call): + return CallFunction( + aten.div, + CallFunction( + aten.clamp_max, + CallFunction( + aten.clamp_min, CallFunction(aten.add, computation_call, 3), 0 + ), + 6, + ), + 6, + ) + + def _leaky_relu_fusion(computation_call): + return CallFunction( + aten.where, + CallFunction(aten.gt, computation_call, 0), + computation_call, + CallFunction(aten.mul, computation_call, KeywordArg("negative_slope")), + ) + + def _hardtanh_fusion(computation_call): + return CallFunction( + aten.clamp_max, + CallFunction(aten.clamp_min, computation_call, KeywordArg("min_value")), + KeywordArg("max_value"), + ) + + def _combined_fusion(computation_call, elementwise_op): + return CallFunction(elementwise_op, computation_call) + + # binary_op(other, computation_op) + def _binary_fusion_v1(computation_call, binary_fn): + return CallFunction(binary_fn, KeywordArg("other"), computation_call) + + # binary_op(computation_op, other) + def _binary_fusion_v2(computation_call, binary_fn): + return CallFunction(binary_fn, computation_call, KeywordArg("other")) + + def _is_single_computation_op(computation_op, lowp_dtype=None): + def fn(match): + computation_nodes = filter_nodes(match.nodes, computation_op) + + if lowp_dtype: + output_node_meta = match.output_node().meta.get("val") + if output_node_meta.dtype != lowp_dtype: + return False + + if len(computation_nodes) < 1: + return False + if any(n.args[-3] != "none" for n in computation_nodes): + return False + return True + + return fn + + def _is_valid_computation_unary_fusion(computation_op, lowp_dtype=None): + def fn(match): + matched = _is_single_computation_op(computation_op, lowp_dtype)(match) + computation_node = filter_nodes(match.nodes, computation_op)[0] + if lowp_dtype: + conversion_dtype_nodes = filter_nodes( + match.nodes, prims.convert_element_type.default + ) + if len(conversion_dtype_nodes) != 2: + return False + # fusion pattern is always in the form of computation_op + to_float32 + unary_op + to_bfloat16 + if computation_node == conversion_dtype_nodes[0].args[0]: + to_float = conversion_dtype_nodes[0].args[1] + to_lp = conversion_dtype_nodes[1].args[1] + else: + to_float = conversion_dtype_nodes[1].args[1] + to_lp = conversion_dtype_nodes[0].args[1] + matched = matched and to_float == torch.float and to_lp == lowp_dtype + return matched + + return fn + + def _register_unary_fusion_lowering( + pattern, unary_attr, computation_op, lowp_dtype=None + ): + @register_lowering_pattern( + pattern, + extra_check=_is_valid_computation_unary_fusion(computation_op, lowp_dtype), + ) + def fn(match, *args, **kwargs): + computation_args = list(args)[:-3] + [ + unary_attr.op_name, + unary_attr.scalars_attr, + unary_attr.algorithm_attr, + ] + counters["inductor"]["mkldnn_unary_fusion_matcher_count"] += 1 + counters["inductor"]["mkldnn_unary_fusion_matcher_nodes"] += len( + match.nodes + ) + return L[computation_op](*computation_args) + + return fn + + def _register_leaky_relu_fusion_lowering(pattern, computation_op, lowp_dtype=None): + @register_lowering_pattern( + pattern, extra_check=_is_single_computation_op(computation_op, lowp_dtype) + ) + def fn(match, *args, **kwargs): + negative_slope = kwargs.get("negative_slope") + if isinstance(negative_slope, ir.TensorBox): + matched = False + else: # inp is a Number + matched = True + if lowp_dtype: + dtype1 = kwargs.get("to_float") + dtype2 = ( + kwargs.get("to_bf16") + if lowp_dtype == torch.bfloat16 + else kwargs.get("to_fp16") + ) + matched = matched and dtype1 == torch.float and dtype2 == lowp_dtype + computation_args = list(args) + counters["inductor"]["mkldnn_unary_fusion_matcher_count"] += 1 + counters["inductor"]["mkldnn_unary_fusion_matcher_nodes"] += len( + match.nodes + ) + if matched: + computation_args = computation_args[:-3] + [ + "leaky_relu", + [negative_slope], + "", + ] + return L[computation_op](*computation_args) + else: + # computation_args += ["none", [], ""] + out = L[computation_op](*computation_args) + if lowp_dtype: + out = L[prims.convert_element_type.default](out, dtype=torch.float) + out = L[aten.where]( + L[aten.gt](out, 0), + out, + L[aten.mul](out, negative_slope), + ) + if lowp_dtype: + out = L[prims.convert_element_type.default](out, dtype=dtype2) # type: ignore[possibly-undefined] + return out + + return fn + + def _register_hardtanh_fusion_lowering(pattern, computation_op, lowp_dtype=None): + @register_lowering_pattern( + pattern, extra_check=_is_single_computation_op(computation_op, lowp_dtype) + ) + def fn(match, *args, **kwargs): + min_value = kwargs.get("min_value") + max_value = kwargs.get("max_value") + if isinstance(min_value, ir.TensorBox) or isinstance( + max_value, ir.TensorBox + ): + matched = False + else: # inp is a Number + assert max_value is not None + matched = min_value <= max_value + if lowp_dtype: + dtype1 = kwargs.get("to_float") + dtype2 = ( + kwargs.get("to_bf16") + if lowp_dtype == torch.bfloat16 + else kwargs.get("to_fp16") + ) + matched = matched and dtype1 == torch.float and dtype2 == lowp_dtype + computation_args = list(args) + counters["inductor"]["mkldnn_unary_fusion_matcher_count"] += 1 + counters["inductor"]["mkldnn_unary_fusion_matcher_nodes"] += len( + match.nodes + ) + if matched: + computation_args = computation_args[:-3] + [ + "hardtanh", + [min_value, max_value], + "", + ] + return L[computation_op](*computation_args) + else: + out = L[computation_op](*computation_args) + if lowp_dtype: + out = L[prims.convert_element_type.default](out, dtype=torch.float) + out = L[aten.clamp_max](L[aten.clamp_min](out, min_value), max_value) + if lowp_dtype: + out = L[prims.convert_element_type.default](out, dtype=dtype2) # type: ignore[possibly-undefined] + return out + + return fn + + _binary_attr = { + aten.add: "add", + ops.add: "add", + aten.sub: "sub", + ops.sub: "sub", + } + + def _is_valid_binary(match, computation_op, binary_op): + binary_nodes = filter_nodes(match.nodes, binary_op) + if len(binary_nodes) < 1: + return False + + def get_meta_value(argument: torch.fx.node.Argument): + # Only torch.fx.Node is expected to have meta. + if isinstance(argument, torch.fx.Node): + return argument.meta.get("val", None) + return None + + if any( + not isinstance(get_meta_value(n.args[0]), torch.Tensor) + or not isinstance(get_meta_value(n.args[1]), torch.Tensor) + for n in binary_nodes + ): + return False + # check alpha is one. + if any( + get_arg_value(n, 2, kwarg_name="alpha") != 1.0 + and get_arg_value(n, 2, kwarg_name="alpha") is not None + for n in binary_nodes + ): + return False + + def _check_input_sizes(n, computation_op): + # Check if the tensor shape of the 'other' node is the same as or + # can be broadcasted to the tensor shape of the computation node. + computation_node = ( + n.args[0] if n.args[1] is match.kwargs["other"] else n.args[1] + ) + assert computation_node.target == computation_op + computation_node_size = get_meta_value(computation_node).size() + if computation_op is mkldnn._linear_pointwise.default: + broadcast_sizes = [] + if len(computation_node_size) >= 2: + broadcast_sizes = [ + torch.Size( + [1 for _ in range(len(computation_node_size) - 1)] + + [computation_node_size[-1]] + ), + ] + else: + assert len(computation_node_size) > 2 + broadcast_sizes = [ + torch.Size( + [computation_node_size[0], computation_node_size[1]] + + [1 for _ in range(len(computation_node_size) - 2)] + ), + torch.Size( + [1, computation_node_size[1]] + + [1 for _ in range(len(computation_node_size) - 2)] + ), + torch.Size([1 for _ in range(len(computation_node_size))]), + ] + return ( + get_meta_value(match.kwargs["other"]).size() + in [ + computation_node_size, + ] + + broadcast_sizes + ) + + if any( + not _check_input_sizes(n, computation_op) + or get_meta_value(n.args[0]).device != get_meta_value(n.args[1]).device + or get_meta_value(n.args[0]).dtype != get_meta_value(n.args[1]).dtype + for n in binary_nodes + ): + return False + # check args[0] and args[1] is not same + if any(n.args[0] == n.args[1] for n in binary_nodes): + return False + return True + + def _is_valid_computation_binary(computation_op, binary_op, other_index=None): + def fn(match): + if not _is_single_computation_op(computation_op)(match): + return False + if not _is_valid_binary(match, computation_op, binary_op): + return False + return True + + return fn + + def _get_remaining_users(extra_input_node, compute_node): + # Think about this pattern: + # ReLU + # / \ + # Conv1 + # / \ + # Conv2 + # \ / + # Add + # Although, the extra input node (ReLU) has more than 1 users: Conv1 and Add. + # The Conv1 is the ancestor node of the current compute node (Conv2). + # This indicates that the buffer of ReLU has completed all its usage, + # So we can safely make changes to it now by doing Conv2->Add inplace fusion. + # Take above case as example: + # * extra_input_node: ReLU + # * compute_node: Conv2 + # _get_remaining_users will return the users of extra_input_node which are not + # ancestor node of compute_node. + def _is_ancestor_node(_current_node, _ancestor_node): + # Check whether _ancestor_node is the ancestor node of _current_node + _node_list = [_current_node] + _visited_nodes = OrderedSet[torch.fx.Node]() + while len(_node_list) != 0: + _current_node = _node_list.pop(0) + if _current_node not in _visited_nodes: + _visited_nodes.add(_current_node) + if _current_node == _ancestor_node: + return True + elif isinstance( + _current_node, torch.fx.Node + ) and _current_node.op not in ["placeholder", "output", "get_attr"]: + for input in _current_node.all_input_nodes: + _node_list.append(input) # noqa: PERF402 + return False + + return [ + user + for user in list(extra_input_node.users) + if not _is_ancestor_node(compute_node, user) + ] + + def _is_valid_computation_binary_inplace(computation_op, binary_op, other_index): + def fn(match): + if not _is_valid_computation_binary(computation_op, binary_op)(match): + return False + binary_nodes = filter_nodes(match.nodes, binary_op) + + def _get_compute_node(_binary_node, _other_index): + assert len(_binary_node.all_input_nodes) == 2, ( + "Binary node should have 2 input nodes." + ) + _compute_index = 1 if (_other_index == 0) else 0 + return _binary_node.args[_compute_index] + + def _other_input_not_inplaceable(_binary_node, _other_index): + _compute_node = _get_compute_node(_binary_node, _other_index) + return ( + len( + _get_remaining_users( + _binary_node.args[_other_index], _compute_node + ) + ) + > 1 + or _binary_node.args[_other_index] == _compute_node.args[0] + ) + + if any(_other_input_not_inplaceable(n, other_index) for n in binary_nodes): + return False + if any( + n.args[other_index].op in ["placeholder", "output"] + for n in binary_nodes + ): + return False + return True + + return fn + + def _register_binary_unary_fusion_lowering( + pattern, + computation_op, + binary_op, + fusion_op, + unary_attr=None, + ): + @register_lowering_pattern( + pattern, extra_check=_is_valid_computation_binary(computation_op, binary_op) + ) + def fn(match, *args, **kwargs): + other = kwargs.get("other") + assert isinstance(other, ir.TensorBox) + binary_attr = _binary_attr[binary_op] + args_list = list(args) + computation_args = [args_list[0], other] + args_list[1:-3] + [binary_attr] + if len(args_list) > 6: + if unary_attr is not None: + computation_args += [ + 1.0, + unary_attr.op_name, + unary_attr.scalars_attr, + unary_attr.algorithm_attr, + ] + else: + computation_args += [1.0, None, [], None] + counters["inductor"]["mkldnn_conv_binary_unary_fusion_matcher_count"] += 1 + counters["inductor"]["mkldnn_conv_binary_unary_fusion_matcher_nodes"] += ( + len(match.nodes) + ) + return L[fusion_op](*computation_args) + + return fn + + def _can_be_inplace(_other): + return not ( + isinstance(_other.data, ir.BaseView) + or len(_other.get_inputs_that_alias_output()) > 0 + ) + + def _qlinear_binary_can_be_inplace(_other): + if isinstance(_other.data, ir.BaseView): + + def unwrap_buffer(data): + if isinstance(data, ir.StorageBox): + return data.data + return data + + data = _other.data.unwrap_view() + if isinstance(unwrap_buffer(data), ir.CppTemplateBuffer): + # It can be inplaced when _other is the 2D to 3D view of + # a CppTemplateBuffer because if there is a view of CppTemplateBuffer, + # CppTemplateBuffer will not be used directly but the view. + return True + else: + # The case of QLinearPointwiseBinaryPT2E(sum) -> QLinearPointwiseBinaryPT2E(sum) + # is similar to CppTemplateBuffer above. + # The output of previous QLinearPointwiseBinaryPT2E is + # the input x2 of current QLinearPointwiseBinaryPT2E. + # Use V.graph.operations to check if _other is a view of the output + # of previous QLinearPointwiseBinaryPT2E (the inputs[6]). + for op in V.graph.operations: + if ( + isinstance(op, mkldnn_ir.QLinearPointwiseBinaryPT2E) + and unwrap_buffer(data) == op.inputs[6] # type: ignore[attr-defined] + ): + return True + return False + elif len(_other.get_inputs_that_alias_output()) > 0: + return False + else: + return True + + def _register_binary_unary_maybe_inplace_fusion_lowering( + pattern, + computation_op, + binary_op, + inplace_fusion_op, + outplace_fusion_op, + unary_attr=None, + other_index=None, + ): + @register_lowering_pattern( + pattern, + extra_check=_is_valid_computation_binary_inplace( + computation_op, binary_op, other_index + ), + ) + def fn(match, *args, **kwargs): + other = kwargs.get("other") + assert isinstance(other, ir.TensorBox) + binary_attr = _binary_attr[binary_op] + args_list = list(args) + computation_args = [args_list[0], other] + args_list[1:-3] + [binary_attr] + if len(args_list) > 6: + if unary_attr is not None: + computation_args += [ + 1.0, + unary_attr.op_name, + unary_attr.scalars_attr, + unary_attr.algorithm_attr, + ] + else: + computation_args += [1.0, None, [], None] + counters["inductor"]["mkldnn_conv_binary_unary_fusion_matcher_count"] += 1 + counters["inductor"]["mkldnn_conv_binary_unary_fusion_matcher_nodes"] += ( + len(match.nodes) + ) + # Make sure the other is not an alias or mutation(fx side doesn't has such info). + other.realize() + if not _can_be_inplace(other) or other.data.shape != list( + match.nodes[0].meta["val"].size() + ): + return L[outplace_fusion_op](*computation_args) + return L[inplace_fusion_op](*computation_args) + + return fn + + computation_ops = [ + mkldnn._convolution_pointwise.default, + mkldnn._linear_pointwise.default, + mkldnn._convolution_transpose_pointwise.default, + ] + + class UnaryAttr: + def __init__( + self, op_name: str, scalars_attr=None, algorithm_attr=None + ) -> None: + self.op_name = op_name + self.scalars_attr = scalars_attr if scalars_attr else [] + self.algorithm_attr = algorithm_attr if algorithm_attr else "" + + def _register_unary_fusion(): + computation_call_fns = [_conv_call, _linear_call, _conv_transpose_call] + + def _unary_fusion_patterns(lowp_dtype): + replacement_unary_fusion_patterns = { + UnaryAttr("gelu", algorithm_attr="tanh"): [ + _unary_fusion_pattern(_gelu_fusion_2, call_fn, 4, lowp_dtype) + for call_fn in computation_call_fns + ], + UnaryAttr("gelu", algorithm_attr="none"): [ + _unary_fusion_pattern(_gelu_fusion_1, call_fn, 2, lowp_dtype) + for call_fn in computation_call_fns + ], + UnaryAttr("hardswish"): [ + _unary_fusion_pattern(_hardswish_fusion, call_fn, 2, lowp_dtype) + for call_fn in computation_call_fns + ], + UnaryAttr("hardsigmoid"): [ + _unary_fusion_pattern(_hardsigmoid_fusion, call_fn, 1, lowp_dtype) + for call_fn in computation_call_fns + ], + UnaryAttr("swish"): [ + _unary_fusion_pattern(_silu_fusion, call_fn, 2, lowp_dtype) + for call_fn in computation_call_fns + ], + } + if not lowp_dtype: + call_user1 = [call_fn(users=1) for call_fn in computation_call_fns] + replacement_unary_fusion_patterns.update( + { + UnaryAttr("relu"): [ + _combined_fusion(u, aten.relu) for u in call_user1 + ], + UnaryAttr("sigmoid"): [ + _combined_fusion(u, aten.sigmoid) for u in call_user1 + ], + UnaryAttr("tanh"): [ + _combined_fusion(u, aten.tanh) for u in call_user1 + ], + } + ) + + return replacement_unary_fusion_patterns + + for lowp_dtype in [torch.bfloat16, torch.float16, None]: + replace_patterns = _unary_fusion_patterns(lowp_dtype) + for unary_attr, patterns in replace_patterns.items(): + _register_unary_fusion_lowering( + patterns[0], unary_attr, computation_ops[0], lowp_dtype + ) + _register_unary_fusion_lowering( + patterns[1], unary_attr, computation_ops[1], lowp_dtype + ) + _register_unary_fusion_lowering( + patterns[2], unary_attr, computation_ops[2], lowp_dtype + ) + _leaky_relu_patterns = [ + _unary_fusion_pattern(_leaky_relu_fusion, call_fn, 3, lowp_dtype) + for call_fn in computation_call_fns + ] + for pattern, computation_op in zip(_leaky_relu_patterns, computation_ops): + _register_leaky_relu_fusion_lowering( + pattern, computation_op, lowp_dtype + ) + hardtanh_patterns = [ + _unary_fusion_pattern(_hardtanh_fusion, call_fn, 1, lowp_dtype) + for call_fn in computation_call_fns + ] + for pattern, computation_op in zip(hardtanh_patterns, computation_ops): + _register_hardtanh_fusion_lowering(pattern, computation_op, lowp_dtype) + + def _register_inplace_fusion(): + binary_ops = [aten.add, ops.add] + inplace_fusion_op = mkldnn._convolution_pointwise_.binary + outplace_fusion_op = mkldnn._convolution_pointwise.binary + conv_call = _conv_call(users=1) + conv_op = computation_ops[0] + for binary_op in binary_ops: + binary_v1 = _binary_fusion_v1(conv_call, binary_op) + binary_unary_v1 = _combined_fusion(binary_v1, aten.relu) + _register_binary_unary_maybe_inplace_fusion_lowering( + binary_unary_v1, + conv_op, + binary_op, + inplace_fusion_op, + outplace_fusion_op, + other_index=0, + unary_attr=UnaryAttr("relu"), + ) + _register_binary_unary_maybe_inplace_fusion_lowering( + binary_v1, + conv_op, + binary_op, + inplace_fusion_op, + outplace_fusion_op, + other_index=0, + ) + binary_v2 = _binary_fusion_v2(conv_call, binary_op) + binary_unary_v2 = _combined_fusion(binary_v2, aten.relu) + _register_binary_unary_maybe_inplace_fusion_lowering( + binary_unary_v2, + conv_op, + binary_op, + inplace_fusion_op, + outplace_fusion_op, + other_index=1, + unary_attr=UnaryAttr("relu"), + ) + _register_binary_unary_maybe_inplace_fusion_lowering( + binary_v2, + conv_op, + binary_op, + inplace_fusion_op, + outplace_fusion_op, + other_index=1, + ) + + def _register_binary_fusion(): + binary_ops = [aten.add, ops.add, aten.sub, ops.sub] + fusion_ops = [ + mkldnn._convolution_pointwise.binary, + mkldnn._linear_pointwise.binary, + ] + _computation_user_1 = [_conv_call(users=1), _linear_call(users=1)] + for computation_call, computation_op, fusion_op in zip( + _computation_user_1, computation_ops[:-1], fusion_ops + ): + for binary_op in binary_ops: + pattern = _binary_fusion_v2(computation_call, binary_op) + _register_binary_unary_fusion_lowering( + pattern, computation_op, binary_op, fusion_op + ) + + for binary_op in [aten.add, ops.add]: + pattern = _binary_fusion_v1(computation_call, binary_op) + _register_binary_unary_fusion_lowering( + pattern, computation_op, binary_op, fusion_op + ) + + def _register_binary_unary_fusion(): + binary_ops = [aten.add, ops.add, aten.sub, ops.sub] + fusion_ops = [mkldnn._convolution_pointwise.binary] + _computation_user_1 = [_conv_call(users=1)] + for computation_call, computation_op, fusion_op in zip( + _computation_user_1, computation_ops[:-1], fusion_ops + ): + for binary_op in binary_ops: + pattern_v1 = _combined_fusion( + _binary_fusion_v2(computation_call, binary_op), aten.relu + ) + _register_binary_unary_fusion_lowering( + pattern_v1, + computation_op, + binary_op, + fusion_op, + unary_attr=UnaryAttr("relu"), + ) + for binary_op in [aten.add, ops.add]: + pattern_v2 = _combined_fusion( + _binary_fusion_v1(computation_call, binary_op), aten.relu + ) + _register_binary_unary_fusion_lowering( + pattern_v2, + computation_op, + binary_op, + fusion_op, + unary_attr=UnaryAttr("relu"), + ) + + def _recover_linear(): + # convert reshape+linear+reshape to a single linear for applying fusion path. + # concat_linear (pass_number=0) -> mkldnn_linear_pack (pass_number=1) -> _recover_linear(pass_number=2) + @register_freezing_graph_pattern( + CallFunction( + aten.reshape.default, + CallFunction( + mkldnn._linear_pointwise.default, + CallFunction( + aten.reshape.default, + Arg(), + KeywordArg("reshape_1"), + _users=MULTIPLE, + ), + Arg(), + Arg(), + Arg(), + Arg(), + Arg(), + ), + KeywordArg("reshape_2"), + ), + pass_number=2, + ) + def reshape_linear_reshape_pattern(match, *args, **kwargs): + def get_val(val): + return val if isinstance(val, int) else val.meta.get("val") + + reshape_1 = kwargs.get("reshape_1") + reshape_2 = kwargs.get("reshape_2") + assert isinstance(reshape_1, list) + assert isinstance(reshape_2, list) + assert len(reshape_1) == 2 + + graph = match.graph + reshape_2_node = match.output_node() + linear_input_node = reshape_2_node.args[0].args[0].args[0] + # check linear's input's shape[:-1] == reshape_2[:-1] + # and check product(reshape_2[:-1]) == reshape_1[0] + can_remove_reshape = linear_input_node.meta.get("val").shape[ + :-1 + ] == torch.Size([get_val(val) for val in reshape_2[:-1]]) + can_remove_reshape = can_remove_reshape and ( + reduce( + operator.mul, + [get_val(val) for val in reshape_2[:-1]], + ) + == get_val(reshape_1[0]) + ) + + if can_remove_reshape: + repl = graph.call_function(mkldnn._linear_pointwise.default, args) + repl.meta.update(reshape_2_node.meta) + reshape_2_node.replace_all_uses_with(repl) + old_linear_node = reshape_2_node.args[0] + reshape_1_node = old_linear_node.args[0] + graph.erase_node(reshape_2_node) + graph.erase_node(old_linear_node) + if len(reshape_1_node.users) == 0: + graph.erase_node(reshape_1_node) + counters["inductor"]["mkldnn_reshape_linear_reshape_matcher_count"] += 1 + counters["inductor"]["mkldnn_reshape_linear_reshape_matcher_nodes"] += len( + match.nodes + ) + + def is_linear_add_bias(match): + add_node = match.output_node() + linear_node = add_node.args[0] + device_type = add_node.meta.get("val").device.type + mkldnn_device_op = _get_mkldnn_device_op(device_type) + transpose_weight_node = mkldnn_device_op.get_linear_transpose_weight( + linear_node.args[1] + ) + weight_meta = transpose_weight_node.args[0].meta.get("val") + bias_node = add_node.args[1] + if isinstance(bias_node, int): + # we only folding bias if it is a constant + return False + bias_meta = add_node.args[1].meta.get("val") + if weight_meta is None or bias_meta is None: + return False + + if bias_meta.dtype != weight_meta.dtype: + return False + return ( + linear_node.args[2] is None + and bias_meta.dim() == 1 + and bias_meta.size(0) == weight_meta.size(1) + ) + + # convert linear+bias to a single linear for applying fusion path. + @register_freezing_graph_pattern( + CallFunction( + aten.add.Tensor, + CallFunction(mkldnn._linear_pointwise.default, *_linear_args), + Arg(), + ), + pass_number=2, + extra_check=is_linear_add_bias, + ) + def linear_bias_pattern(match, *args): + graph = match.graph + add_node = match.output_node() + linear_node = add_node.args[0] + new_args = list(linear_node.args) + new_args[2] = add_node.args[1] + repl = graph.call_function( + mkldnn._linear_pointwise.default, tuple(new_args) + ) + repl.meta.update(add_node.meta) + add_node.replace_all_uses_with(repl) + match.erase_nodes() + counters["inductor"]["mkldnn_linear_bias_matcher_count"] += 1 + counters["inductor"]["mkldnn_linear_bias_matcher_nodes"] += len(match.nodes) + + def _is_packable_mkldnn_rnn_layer(match): + lstm_node = match.output_node() + POS_WEIGHTS = [1, 2] + POS_INPUTS = [0, 5, 6] + POS_ARGS = POS_WEIGHTS + POS_INPUTS + # Weights should be Constant + if any( + lstm_node.args[POS_WEIGHT].op != "get_attr" for POS_WEIGHT in POS_WEIGHTS + ): + return False + + # Meta info for weights and inputs should be available + if any(lstm_node.args[POS_ARG].meta.get("val") is None for POS_ARG in POS_ARGS): + return False + + # Check device + if any( + lstm_node.args[POS_ARG].meta.get("val").device.type != "cpu" + for POS_ARG in POS_ARGS + ): + return False + + # Check dtype + if any( + lstm_node.args[POS_ARG].meta.get("val").dtype == torch.bfloat16 + and not is_mkldnn_bf16_supported("cpu") + for POS_ARG in POS_ARGS + ): + return False + if any( + lstm_node.args[POS_ARG].meta.get("val").dtype == torch.float16 + and not is_mkldnn_fp16_supported("cpu") + for POS_ARG in POS_ARGS + ): + return False + + return True + + def _is_packable_convolution(match): + """ + Check if the node is supported for MKLDNN convolution. + """ + conv_node = match.output_node() + device_type = conv_node.meta.get("val").device.type + # The operator 'mkldnn::_convolution_transpose_pointwise' is not currently implemented for the XPU device. + if match.kwargs["is_transposed"] and device_type == "xpu": + return False + + input_meta_value = conv_node.args[0].meta.get("val") + weight_meta_value = conv_node.args[1].meta.get("val") + if input_meta_value is None or weight_meta_value is None: + return False + input_size = input_meta_value.shape + if conv_node.args[1].op != "get_attr": + return False + for meta_value in [input_meta_value, weight_meta_value]: + if ( + meta_value is None + or meta_value.device.type not in SUPPORTED_MKLDNN_DEVICES + or (meta_value.dim() != 4 and meta_value.dim() != 5) + ): + return False + + if ( + input_meta_value.dtype == torch.bfloat16 + or weight_meta_value.dtype == torch.bfloat16 + ): + if not is_mkldnn_bf16_supported(device_type): + return False + if ( + input_meta_value.dtype == torch.float16 + or weight_meta_value.dtype == torch.float16 + ): + if not is_mkldnn_fp16_supported(device_type): + return False + is_transposed = conv_node.args[-3] + if is_transposed: + # TODO: Support dynamic shape case for MKLDNN conv transpose. + if has_free_symbols(input_size): + return False + groups = conv_node.args[-1] + in_channels = weight_meta_value.size(0) + # doesn't support group_depthwise_conv_transpose. + if groups > 1 and groups == in_channels: + return False + # Port from: aten/src/ATen/native/Convolution.cpp:is_output_padding_big + output_paddings = conv_node.args[-2] + strides = conv_node.args[3] + if any( + output_padding >= stride + for output_padding, stride in zip(output_paddings, strides) + ): + return False + return True + + def _is_packable_linear(match): + """ + Check if the node is supported for MKLDNN linear. + """ + + def is_const_or_cat_by_const(weight): + if weight.op == "get_attr": + return True + if weight.target != aten.cat.default: + return False + return all(arg.op == "get_attr" for arg in weight.args[0]) + + linear_node = match.output_node() + # mkldnn linear only supports beta=1or0 and alpha=1 + if linear_node.target is aten.addmm.default: + alpha = linear_node.kwargs.get("alpha", 1.0) + beta = linear_node.kwargs.get("beta", 1.0) + if (beta != 0.0 and beta != 1.0) or alpha != 1.0: + return False + # weight_idx is 1 for aten.mm and is 2 for aten.addmm + weight_idx = 2 if linear_node.target is aten.addmm.default else 1 + if not is_const_or_cat_by_const(linear_node.args[weight_idx]): + return False + input_meta_value = linear_node.args[weight_idx - 1].meta.get("val") + weight_meta_value = linear_node.args[weight_idx].meta.get("val") + if input_meta_value is None or weight_meta_value is None: + return False + if ( + input_meta_value.dtype == torch.float64 + or weight_meta_value.dtype == torch.float64 + ): + return False + is_lp_weight = weight_meta_value.dtype in ( + torch.bfloat16, + torch.float16, + ) + reduced_f32_matmul_enabled = torch.backends.mkldnn.matmul.fp32_precision in [ # type: ignore[attr-defined] + "bf16", + "tf32", + ] + use_reduced_f32_for_fp32_weight = ( + reduced_f32_matmul_enabled and weight_meta_value.dtype == torch.float32 + ) + compute_with_lp = is_lp_weight or use_reduced_f32_for_fp32_weight + # on x86, for fp32, mkl should be enabled. + # on aarch64, use mkldnn op for fp32 as well if acl is enabled + if ( + not compute_with_lp + and not mkldnn._is_mkldnn_acl_supported() + and not torch._C.has_mkl + ): + return False + for meta_value in [input_meta_value, weight_meta_value]: + if ( + meta_value is None + or meta_value.device.type != "cpu" + or meta_value.dim() != 2 + ): + return False + if weight_idx == 2: + bias_meta_value = linear_node.args[0].meta.get("val") + if ( + bias_meta_value is None + or meta_value.device.type != "cpu" + or bias_meta_value.dim() != 1 + or bias_meta_value.size(0) != weight_meta_value.size(1) + ): + return False + + device_type = input_meta_value.device.type + if ( + input_meta_value.dtype == torch.bfloat16 + or weight_meta_value.dtype == torch.bfloat16 + ): + if not is_mkldnn_bf16_supported(device_type): + return False + if ( + input_meta_value.dtype == torch.float16 + or weight_meta_value.dtype == torch.float16 + ): + if not is_mkldnn_fp16_supported(device_type): + return False + return True + + _aten_conv_args = ( + Arg(), + Arg(), + Arg(), + Arg(), + Arg(), + Arg(), + KeywordArg("is_transposed"), + Arg(), + Arg(), + ) + + _aten_mkldnn_rnn_layer_args = ( + Arg(), # input + Arg(), # weight0 + Arg(), # weight1 + Arg(), # weight2 + Arg(), # weight3 + Arg(), # hx_ + Arg(), # cx_ + KeywordArg("reverse"), # reverse + Arg(), # batch_sizes + Arg(), # mode + Arg(), # hidden_size + Arg(), # num_layers + Arg(), # has_biases + Arg(), # bidirectional + Arg(), # batch_first + Arg(), # train + ) + + def _register_weight_pack_pass(): + @register_freezing_graph_pattern( + CallFunction(aten.convolution.default, *_aten_conv_args), + extra_check=_is_packable_convolution, + ) + def convolution(match, *args, **kwargs): + is_transposed = kwargs.get("is_transposed") + assert isinstance(is_transposed, bool) + graph = match.graph + conv_node = match.output_node() + device_type = conv_node.args[0].meta.get("val").device.type + mkldnn_device_op = _get_mkldnn_device_op(device_type) + input_size = conv_node.args[0].meta.get("val").shape + with graph.inserting_before(conv_node): + constant_args = [args[4], args[3], args[5], args[-1]] + packed_conv_op = mkldnn._convolution_pointwise.default + if is_transposed: + constant_args.insert(1, args[-2]) # output_padding + packed_conv_op = mkldnn._convolution_transpose_pointwise.default + + if not has_free_symbols(input_size): + packed_weight_node = mkldnn_device_op.pack_conv_weight( + graph, + is_transposed, + args[1], + constant_args, + input_size, + ) + else: + assert not is_transposed + # For dynamic shape case, we need to pack weight in runtime. + packed_weight_node = args[1] + + packed_conv_inputs = ( + (args[0], packed_weight_node, args[2]) + + tuple(constant_args) + + ("none", [], "") + ) + packed_conv_node = graph.create_node( + "call_function", packed_conv_op, tuple(packed_conv_inputs) + ) + conv_node.replace_all_uses_with(packed_conv_node) + packed_conv_node.meta.update(conv_node.meta) + graph.erase_node(conv_node) + counters["inductor"]["mkldnn_conv_weight_pack_matcher_count"] += 1 + counters["inductor"]["mkldnn_conv_weight_pack_matcher_nodes"] += len( + match.nodes + ) + + @register_freezing_graph_pattern( + CallFunction(aten.mkldnn_rnn_layer.default, *_aten_mkldnn_rnn_layer_args), + extra_check=_is_packable_mkldnn_rnn_layer, + ) + def mkldnn_rnn_layer(match, *args, **kwargs): + def get_item(graph, node, index): + return graph.call_function(operator.getitem, (node, index)) + + graph = match.graph + lstm_node = match.output_node() + weight0, weight1 = args[1:3] + reverse = kwargs.get("reverse") + packed_lstm_op = aten.mkldnn_rnn_layer.default + hidden_size = args[9] + has_biases = args[11] + batch_first = args[13] + with graph.inserting_before(lstm_node): + packed_weight_op = mkldnn._reorder_mkldnn_rnn_layer_weight.default + packed_weight_inputs = ( + weight0, + weight1, + hidden_size, + reverse, + has_biases, + batch_first, + ) + packed_weight_node = graph.create_node( + "call_function", packed_weight_op, packed_weight_inputs, {}, "name" + ) + packed_weight_items = [ + get_item(graph, packed_weight_node, i) for i in range(2) + ] + pack_lstm_inputs = ( + args[0], + *packed_weight_items, + args[3], + args[4], + args[5], + args[6], + reverse, + *args[7:], + ) + + packed_lstm_node = graph.create_node( + "call_function", packed_lstm_op, args=pack_lstm_inputs + ) + lstm_node.replace_all_uses_with(packed_lstm_node) + packed_lstm_node.meta.update(lstm_node.meta) + graph.erase_node(lstm_node) + counters["inductor"]["mkldnn_rnn_weight_pack_matcher_count"] += 1 + counters["inductor"]["mkldnn_rnn_weight_pack_matcher_nodes"] += len( + match.nodes + ) + + @register_freezing_graph_pattern( + CallFunction( + aten.addmm.default, + Arg(), + Arg(), + Arg(), + beta=KeywordArg("beta"), + alpha=KeywordArg("alpha"), + ), + extra_check=_is_packable_linear, + pass_number=1, + ) + @register_freezing_graph_pattern( + CallFunction(aten.mm.default, Arg(), Arg()), + extra_check=_is_packable_linear, + pass_number=1, + ) + def linear(match, *args, **kwargs): + graph = match.graph + linear_node = match.output_node() + input = args[0] if linear_node.target is aten.mm.default else args[1] + bias = ( + None + if linear_node.target is aten.mm.default + or ( + linear_node.target is aten.addmm.default + and linear_node.kwargs.get("beta", 1.0) == 0.0 + ) + else args[0] + ) + weight = args[1] if linear_node.target is aten.mm.default else args[2] + device_type = input.meta.get("val").device.type + mkldnn_device_op = _get_mkldnn_device_op(device_type) + with graph.inserting_before(linear_node): + transpose_weight_node = graph.create_node( + "call_function", aten.permute.default, (weight, (1, 0)) + ) + weight_dtype = weight.meta.get("val").dtype + is_lp_weight = weight_dtype in ( + torch.bfloat16, + torch.float16, + ) + reduced_f32_matmul_enabled = ( + torch.backends.mkldnn.matmul.fp32_precision in ["bf16", "tf32"] # type: ignore[attr-defined] + ) + use_reduced_f32_for_fp32_weight = ( + reduced_f32_matmul_enabled and weight_dtype == torch.float32 + ) + compute_with_lp = is_lp_weight or use_reduced_f32_for_fp32_weight + batch_size = input.meta.get("val").shape[0] + packed_weight_node = mkldnn_device_op.pack_linear_weight( + graph, compute_with_lp, transpose_weight_node, batch_size + ) + packed_linear_node = mkldnn_device_op.pack_linear( + graph, compute_with_lp, batch_size, input, packed_weight_node, bias + ) + + linear_node.replace_all_uses_with(packed_linear_node) + packed_linear_node.meta.update(linear_node.meta) + graph.erase_node(linear_node) + counters["inductor"]["mkldnn_linear_weight_pack_matcher_count"] += 1 + counters["inductor"]["mkldnn_linear_weight_pack_matcher_nodes"] += len( + match.nodes + ) + + def _eliminate_duplicate_packed_nodes(gm): + """ + Combine packed weight nodes with the same inputs to reduce memory usage. + for example: + class Model(nn.Module): + def __init__(self) -> None: + super().__init__() + self.linear = nn.Linear(32, 32, bias=True) + + def forward(self, x): + return self.linear(self.linear(x)) + + the above's packed weight nodes are duplicate if two linear calls have same input size. + """ + if not (torch.backends.mkldnn.enabled and torch.backends.mkldnn.is_available()): + return gm + + packed_weight_ops = [ + torch._C._nn.mkldnn_reorder_conv2d_weight, + torch._C._nn.mkldnn_reorder_conv3d_weight, + mkldnn._reorder_convolution_transpose_weight, + mkldnn._reorder_linear_weight, + mkldnn._reorder_mkldnn_rnn_layer_weight, + ] + if torch._C.has_mkl: + packed_weight_ops.append(torch.ops.mkl._mkl_reorder_linear_weight) + + for node in gm.graph.nodes: + if node.target in packed_weight_ops and len(node.args[0].users) > 1: + for user_node in list(node.args[0].users.keys()): + if ( + user_node.target == node.target + and user_node != node + and user_node.args == node.args + ): + user_node.replace_all_uses_with(node) + gm.graph.erase_node(user_node) + + @functools.cache + def _mkldnn_fusion_init(): + # TODO: aarch64: enable op fusion for acl once it supports fused operators. Disabling it for now. + # Otherwise even the matmul or innerproduct can not be accelerated with acl + if ( + not torch.backends.mkldnn.enabled + or not torch.backends.mkldnn.is_available() + ): + return + + if not torch.ops.mkldnn._is_mkldnn_acl_supported(): + _register_unary_fusion() + _register_inplace_fusion() + _register_binary_unary_fusion() + _register_binary_fusion() + _register_quantization_lowerings() + + _register_woq_lowerings() + + @functools.cache + def _mkldnn_weight_pack_init(): + if torch.backends.mkldnn.enabled and torch.backends.mkldnn.is_available(): + _register_weight_pack_pass() + _recover_linear() + _register_quantization_weight_pack_pass() + _register_int8_woq_concat_linear_pattern() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/node_runtime_estimation.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/node_runtime_estimation.py new file mode 100644 index 0000000000000000000000000000000000000000..4d7201d8a8188150f8b9e5f52e6102bc9371d8ae --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/node_runtime_estimation.py @@ -0,0 +1,428 @@ +""" +Collective runtime estimation using CUDA events and power-of-2 rounding. +""" + +from __future__ import annotations + +import functools +import itertools +import operator +from functools import lru_cache +from typing import Any + +import torch +import torch.fx as fx +from torch._inductor.fx_passes.bucketing import _schedulable_wait_node +from torch._inductor.utils import clear_on_fresh_cache +from torch._logging import getArtifactLogger, trace_structured +from torch.fx.operator_schemas import normalize_function + + +def _format_csv(headers: list[str], rows: list[list[str]]) -> str: + """Format data as CSV. Human-readable and easily parseable.""" + lines = [",".join(headers)] + for row in rows: + lines.append(",".join(str(v) for v in row)) + return "\n".join(lines) + + +def _get_collective_key(coll_node: fx.Node) -> str: + """Extract a unique key for a collective node including group info and tensor size.""" + from torch._inductor import fx_utils + + opt_args_kwargs = normalize_function( + coll_node.target, # type: ignore[arg-type] + args=coll_node.args, + kwargs=coll_node.kwargs, + normalize_to_only_use_kwargs=True, + ) + assert opt_args_kwargs is not None + _, kwargs = opt_args_kwargs + group_name = kwargs.get("group_name", None) + group_size = kwargs.get("group_size", None) + + tensor_bytes: int | None = None + success, args, kw = fx_utils.get_fake_args_kwargs(coll_node) + if success: + + def extract_first_tensor_bytes(t: torch.Tensor) -> torch.Tensor: + nonlocal tensor_bytes + if tensor_bytes is None: + shape = [get_hint(dim) for dim in t.shape] + if all(s is not None for s in shape): + numel = functools.reduce(operator.mul, shape, 1) + tensor_bytes = numel * t.dtype.itemsize + return t + + torch.utils._pytree.tree_map_only( + torch.Tensor, extract_first_tensor_bytes, (args, kw) + ) + + return f"{coll_node.target} group_size:{group_size} group_name:{group_name} input_bytes:{tensor_bytes}" + + +def _get_collective_estimations(coll_node: fx.Node) -> tuple[float, float]: + """Get NCCL and Inductor analytical estimations for a collective node. + + Returns: (nccl_ms, inductor_ms) + """ + nccl_ms = ( + torch._inductor.comm_analysis.estimate_nccl_collective_runtime_from_fx_node( + coll_node, None, use_nccl_estimator=True + ) + ) + inductor_ms = ( + torch._inductor.comm_analysis.estimate_nccl_collective_runtime_from_fx_node( + coll_node, None, use_nccl_estimator=False + ) + ) + return nccl_ms, inductor_ms + + +# Setup logger for artifact logging +log = getArtifactLogger(__name__, "node_runtime_estimation") + + +# TODO: Consider using a distributed-aware cache or rank-local disk cache +# not using local cache because different ranks might write to it concurrently. +# solvable in future, potentially with workflow to seed cache +@clear_on_fresh_cache +@lru_cache +def _get_collective_cache() -> dict[str, float]: + """Get process-local cache for collective benchmarks.""" + return {} + + +def get_cached_runtime(key: str) -> float | None: + """Get cached runtime from process-local cache.""" + return _get_collective_cache().get(key) + + +def set_cached_runtime(key: str, value: float) -> None: + """Set cached runtime in process-local cache.""" + _get_collective_cache()[key] = value + + +def get_hint(x: int | torch.SymInt) -> int | None: + if isinstance(x, int): + return x + assert isinstance(x, torch.SymInt) + return x.node.hint if x.node.has_hint() else None + + +def can_benchmark_collective() -> bool: + """Check if we can benchmark collectives (not fake process group).""" + import torch.distributed as c10d + + if not c10d.is_initialized(): + return False + + pg = c10d.distributed_c10d._get_default_group() + if ( + torch.distributed.distributed_c10d.get_backend(pg) + == torch.distributed.distributed_c10d.Backend.FAKE + ): + return False + + return True + + +def _median(lst): + assert len(lst) > 0 + return torch.median(torch.tensor(lst)).item() + + +def _benchmark_collective_with_cuda_events_impl( + n: torch.fx.Node, + args: tuple[Any, ...], + kwargs: dict[str, Any], + nruns: int, +) -> float | None: + """ + Core benchmarking logic using CUDA events and barriers. + Returns runtime in ms or None on failure. + """ + from torch._dynamo.testing import rand_strided + + # Convert FakeTensors to real tensors before benchmarking + def to_real(t: torch.Tensor) -> torch.Tensor: + shape = [get_hint(dim) for dim in t.shape] + stride = [get_hint(s) for s in t.stride()] + + if any(s is None for s in itertools.chain(shape, stride)): + # This should not happen, as can_benhcmark_collective checks for unbacked + raise ValueError("Cannot convert tensor with symbolic dimensions") + + return rand_strided(shape, stride, device=t.device, dtype=t.dtype) # type: ignore[arg-type] + + args, kwargs = torch.utils._pytree.tree_map_only( + torch.Tensor, + to_real, + (args, kwargs), + ) + + # Warmup: call collective once and wait + torch.cuda.synchronize() + result = n.target(*args, **kwargs) # type: ignore[operator] + torch.ops._c10d_functional.wait_tensor(result) + torch.cuda.synchronize() + + # Benchmark with CUDA events + comm_times = [] + for _ in range(nruns): + start_evt = torch.cuda.Event(enable_timing=True) + end_evt = torch.cuda.Event(enable_timing=True) + + start_evt.record() + result = n.target(*args, **kwargs) # type: ignore[operator] + torch.ops._c10d_functional.wait_tensor(result) + end_evt.record() + end_evt.synchronize() + + comm_times.append(start_evt.elapsed_time(end_evt)) + + return _median(comm_times) + + +def benchmark_collective_with_cuda_events( + n: torch.fx.Node, + nruns: int = 2, +) -> tuple[float | None, str]: + """ + Benchmark collective with CUDA events. Returns (runtime_ms, cache_key) or (None, "") on failure. + """ + # context manager not allowed with profiler. + with torch.utils._python_dispatch._disable_current_modes(): + return benchmark_collective_with_cuda_events_impl(n, nruns) + + +def benchmark_collective_with_cuda_events_impl( + n: torch.fx.Node, + nruns: int = 3, +) -> tuple[float | None, str]: + """ + Benchmark collective with CUDA events. Returns (runtime_ms, cache_key) or (None, "") on failure. + """ + from torch._inductor import fx_utils + from torch.distributed.distributed_c10d import _get_group_size_by_name + + # Early check: can we actually run collectives? + if not can_benchmark_collective(): + return None, "" + + success, args, kwargs = fx_utils.get_fake_args_kwargs(n) + + opt_args_kwargs = normalize_function( + n.target, # type: ignore[arg-type] + args=n.args, + kwargs=n.kwargs, + normalize_to_only_use_kwargs=True, + ) + assert opt_args_kwargs is not None + group_name = opt_args_kwargs[1]["group_name"] + group_size = _get_group_size_by_name(group_name) + + if not success: + return None, "" + + # Extract actual input size in BYTES (first tensor argument) + actual_bytes: int | None = None + + def extract_tensor_info(t: torch.Tensor) -> torch.Tensor: + nonlocal actual_bytes + if actual_bytes is None: + shape = [get_hint(dim) for dim in t.shape] + if any(s is None for s in shape): + return t + + total_elems = 1 + for dim in shape: + assert dim is not None + total_elems *= dim + + actual_bytes = total_elems * t.dtype.itemsize + else: + # out-variants (e.g. all_gather_into_tensor_out) can have multiple tensors + pass + return t + + torch.utils._pytree.tree_map_only(torch.Tensor, extract_tensor_info, (args, kwargs)) + + if actual_bytes is None: + return None, "" + + # Cache key by BYTES (dtype-agnostic) + key = f"{n.target}: ({group_size} group size, {actual_bytes} bytes)" + + # Check cache + if (cached := get_cached_runtime(key)) is not None: + return cached, key + + # Benchmark using CUDA events with actual args/kwargs + runtime = _benchmark_collective_with_cuda_events_impl(n, args, kwargs, nruns) + + if runtime is None: + return None, key + + # Cache the result + set_cached_runtime(key, runtime) + return runtime, key + + +def _log_compute_estimations( + compute_nodes: list[fx.Node], + benchmarked_estimations: list[float], + analytical_estimations: list[float], +) -> None: + """Log compute node runtime estimations comparing benchmarked vs analytical.""" + import torch.utils._pytree as pytree + from torch._inductor.fx_utils import count_flops_fx + from torch.utils._dtype_abbrs import dtype_abbrs + + def _node_summary(n: fx.Node) -> str: + ret = str(n) + for arg in pytree.arg_tree_leaves(n.args, n.kwargs): + if not isinstance(arg, torch.fx.Node): + continue + if "val" in arg.meta: + t = arg.meta["val"] + ret += f" {dtype_abbrs[t.dtype]}{tuple(t.shape)}" + return ret + + headers = [ + "Node", + "Benchmarked Est(us)", + "Analytical Est(us)", + "Diff(ratio)", + "Diff(us)", + "Flops", + ] + + rows = [ + [ + _node_summary(node), + f"{est_b * 1e3:.4f}", + f"{est_a * 1e3:.4f}", + f"{(est_a / est_b) if est_b > 0 else 0:.4f}", + f"{(est_a - est_b) * 1e3:.4f}", + str(count_flops_fx(node)), + ] + for node, est_b, est_a in zip( + compute_nodes, benchmarked_estimations, analytical_estimations + ) + ] + + log_str = _format_csv(headers, rows) + + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "fx_compute_nodes_runtime_estimation", + "encoding": "string", + }, + payload_fn=lambda: log_str, + ) + + +def _log_graph_collective_benchmarks(gm: fx.GraphModule, artifact_name: str) -> None: + collective_nodes = [] + collective_keys = [] + benchmarked = [] + + for node in gm.graph.nodes: + if _schedulable_wait_node(node): + start = node.args[0] + collective_nodes.append(start) + collective_keys.append(_get_collective_key(start)) + benchmarked_ms, _ = benchmark_collective_with_cuda_events(start, nruns=5) + benchmarked.append(benchmarked_ms if benchmarked_ms else 0.0) + + if collective_nodes: + world_size = ( + torch.distributed.get_world_size() + if torch.distributed.is_initialized() + else 1 + ) + _log_collective_benchmarks( + collective_nodes, + collective_keys, + benchmarked, + world_size, + artifact_name, + ) + + +def _log_collective_benchmarks( + collective_nodes: list[fx.Node], + collective_keys: list[str] | None = None, + benchmarked_medians: list[float] | None = None, + world_size: int | None = None, + artifact_name: str = "fx_collectives_analytical_estimation", +) -> None: + """Log collective estimations for tlparse. Includes benchmarks if provided.""" + if world_size is None: + world_size = ( + torch.distributed.get_world_size() + if torch.distributed.is_initialized() + else 1 + ) + + has_benchmarks = benchmarked_medians is not None + + if has_benchmarks: + headers = [ + "Collective Key", + "Benchmarked(ms)", + "NCCL Est(ms)", + "Inductor Est(ms)", + "NCCL Diff(ratio)", + "Inductor Diff(ratio)", + ] + else: + headers = [ + "Collective Key", + "NCCL Est(ms)", + "Inductor Est(ms)", + ] + + rows = [] + for i, coll_node in enumerate(collective_nodes): + key = collective_keys[i] if collective_keys else _get_collective_key(coll_node) + nccl_ms, inductor_ms = _get_collective_estimations(coll_node) + + if benchmarked_medians is not None: + benchmarked_ms = benchmarked_medians[i] + nccl_diff_pct = (nccl_ms / benchmarked_ms) if benchmarked_ms > 0 else 0 + inductor_diff_pct = ( + (inductor_ms / benchmarked_ms) if benchmarked_ms > 0 else 0 + ) + rows.append( + [ + key, + f"{benchmarked_ms:.4f}", + f"{nccl_ms:.4f}", + f"{inductor_ms:.4f}", + f"{nccl_diff_pct:.2f}", + f"{inductor_diff_pct:.2f}", + ] + ) + else: + rows.append( + [ + key, + f"{nccl_ms:.4f}", + f"{inductor_ms:.4f}", + ] + ) + + log_str = f"# World size: {world_size}\n" + log_str += _format_csv(headers, rows) + + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": artifact_name, + "encoding": "string", + }, + payload_fn=lambda: log_str, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/numeric_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/numeric_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..d1db82f21f7ec6a37e1f260b02d2fcd77622c058 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/numeric_utils.py @@ -0,0 +1,213 @@ +# mypy: allow-untyped-defs +import gc +import logging +import os +import random +import traceback + +import numpy + +import torch +import torch.optim as optim +from torch.utils._ordered_set import OrderedSet + +from .. import config + + +logger: logging.Logger = logging.getLogger(__name__) + +MAIN_RANDOM_SEED = 1337 + +# Set the CUBLAS_WORKSPACE_CONFIG environment variable +os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8" + + +# If the two forward functions involve any non-deterministic operations, +# such as certain types of parallelism or asynchronous execution, +# this can also lead to different outputs. +def set_deterministic() -> None: + """Make torch manual seed deterministic.""" + + torch.manual_seed(MAIN_RANDOM_SEED) + random.seed(MAIN_RANDOM_SEED) + numpy.random.seed(MAIN_RANDOM_SEED) + torch.use_deterministic_algorithms(True) + + +def clean_memory() -> None: + """Clean memory to avoid OOM.""" + gc.collect() + torch.cuda.empty_cache() + + +# We compare the numerical results before and after pre/post grad fx passes +# transformation to make sure the numerical results are the same. +def compare_dict_tensors(dict_base, dict_control, precision): + if len(OrderedSet(dict_base.keys())) != len(OrderedSet(dict_control.keys())): + logger.warning("Mismatch keys found before and after pre/post grad fx passes.") + logger.debug("keys before pre/post grad fx passes %s", dict_base.keys()) + logger.debug("keys after pre/post grad fx passes %s", dict_control.keys()) + return False + is_allclose = True + for key in dict_base: + if key not in dict_control: + logger.warning( + "Mismatch parameter name %s does not exist after pre/post grad fx passes", + key, + ) + # Some parameters have `None`, and not every param has a valid .grad field, we skip them + if dict_base[key] is None or dict_control[key] is None: + continue + if not torch.allclose( + dict_base[key], + dict_control[key], + rtol=precision, + atol=precision, + equal_nan=True, + ): + logger.warning( + "Mismatch parameter values found before and after pre/post grad fx passes." + ) + logger.debug("value before pre/post grad fx passes %s", dict_base[key]) + logger.debug("value after pre/post grad fx passes %s", dict_control[key]) + is_allclose = False + return is_allclose + + +def compare_tuple_tensors(tuple_base, tuple_control, precision): + if len(tuple_base) != len(tuple_control): + logger.warning( + "Mismatch fw output length. before transformation: %s, after transformation: %s", + len(tuple_base), + len(tuple_control), + ) + return False + is_allclose = True + for i in range(len(tuple_base)): + # Some parameters have `None`, we skip them + if tuple_base[i] is None or tuple_control[i] is None: + continue + if not torch.allclose( + tuple_base[i], + tuple_control[i], + rtol=precision, + atol=precision, + equal_nan=True, + ): + logger.debug( + "forward output before pre/post grad fx passes %s", tuple_base[i] + ) + logger.debug( + "forward output after pre/post grad fx passes %s", tuple_control[i] + ) + is_allclose = False + return is_allclose + + +def compare_parameters(model_base, model_control, precision): + return compare_dict_tensors( + dict(model_base.named_parameters()), + dict(model_control.named_parameters()), + precision, + ) + + +def compare_forward_output(pred_base, pred_control, precision): + return compare_tuple_tensors( + pred_base, + pred_control, + precision, + ) + + +def compare_gradients(model_base, model_control, precision): + grad_base = {key: param.grad for key, param in model_base.named_parameters()} + grad_pt2 = {key: param.grad for key, param in model_control.named_parameters()} + return compare_dict_tensors( + grad_base, + grad_pt2, + precision, + ) + + +def run_model( + model_base, model_control, model_input, num_iterations=10, precision=1e-4 +): + clean_memory() + for i in range(num_iterations): + logger.info("start %s iteration", i) + set_deterministic() + pred_base = model_base(*model_input) + set_deterministic() + pred_control = model_control(*model_input) + + res = compare_parameters(model_base, model_control, precision) + logger.info("compare parameters. Numerical result : %s", res) + + res = compare_forward_output(pred_base, pred_control, precision) + logger.info("compare loss/predict. Numerical result : %s", res) + # tensor may not have a grad_fn + try: + _ = pred_base[0].sum().backward(retain_graph=True) + _ = pred_control[0].sum().backward(retain_graph=True) + res = compare_gradients(model_base, model_control, precision) + logger.info("compare param grad. Numerical result : %s", res) + except Exception: + logger.exception("Exception when comparing gradients") + traceback.print_exc() + + if config.fx_passes_numeric_check["requires_optimizer"]: + try: + optimizer_base = optim.SGD( + [param for name, param in model_base.named_parameters()], lr=0.01 + ) + optimizer_base.step() + + optimizer_control = optim.SGD( + [param for name, param in model_control.named_parameters()], lr=0.01 + ) + optimizer_control.step() + + res = compare_parameters(model_base, model_control, precision) + logger.info( + "compare parameters with optimizer added. Numerical result : %s", + res, + ) + except Exception: + logger.exception( + "Exception when optimizer is added to check parameter names" + ) + traceback.print_exc() + else: + logger.warning( + "no parameter with optimizer to compare with length %s before transformation" + " and the length %s after transformation", + len(dict(model_base.named_parameters())), + len(dict(model_control.named_parameters())), + ) + + +def numeric_check_if_enabled( + gm_before_fx_passes, + gm_after_fx_passes, + example_inputs, + num_iterations, + precision, +): + # need to topo-sort graphmodule before we run the model, + # otherwise it may fail as refer before def + # fail silently in order not to block the model run + try: + with torch.autograd.set_detect_anomaly(True): + run_model( + gm_before_fx_passes, + gm_after_fx_passes, + example_inputs, + num_iterations=num_iterations, + precision=precision, + ) + except Exception as e: + logger.warning( # noqa: G200 + "Runtime numeric check failed in pre grad fx passes with error: %s", e + ) + traceback.print_exc() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/overlap_manual_scheduling.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/overlap_manual_scheduling.py new file mode 100644 index 0000000000000000000000000000000000000000..f8d4cd619779742f5401019ee17a291e95ba5b92 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/overlap_manual_scheduling.py @@ -0,0 +1,398 @@ +from __future__ import annotations + +import heapq +from collections import Counter, defaultdict +from typing import Any, TYPE_CHECKING + +import torch # noqa: TC001 +import torch.fx as fx # noqa: TC001 +from torch._dynamo.graph_deduplication import _stable_topological_sort +from torch._inductor.fx_passes.bucketing import ( + _get_collective_node_from_wait, + _schedulable_wait_node, + BucketMode, + is_all_gather_into_tensor as is_all_gather, + is_fsdp_all_gather, + is_fsdp_reduce_scatter, + is_reduce_scatter_tensor as is_reduce_scatter, + merge_all_gather_bucket, + merge_reduce_scatter_bucket, +) +from torch._inductor.fx_passes.overlap_preserving_bucketer import ( + get_full_bucket_key, + OverlapPreservingBucketer, +) +from torch._inductor.fx_passes.overlap_scheduling import ( + CollectiveInfo, + is_compute_node, + OverlapScheduler, +) +from torch.utils._ordered_set import OrderedSet + +from .graph_view import get_subgraph_by_path, GraphView, make_graph_view + + +if TYPE_CHECKING: + from collections.abc import Callable + +import logging + + +logger = logging.getLogger(__name__) + + +class ManualOverlapPreservingBucketer(OverlapPreservingBucketer): + """ + Buckets collective operations based on user specifications. + The actual bucket happens in bucket_collectives, where all-gathers/reduce-scatters in + `nodes` will be buckted one single all-gather/reduce-scatter. + """ + + def __init__( + self, + *args: Any, + **kwargs: Any, + ): + super().__init__(*args, **kwargs) + self.node_to_wait_map: dict[fx.Node, fx.Node] = defaultdict() + + def _bucket_group(self, coll_nodes: list[fx.Node]) -> None: + assert len(coll_nodes) > 0, "bucketed coll_nodes should have nonzero node" + + waits = [self.collective_info[n].wait_node for n in coll_nodes] + # Use earliest wait insertion point + first_wait = min(waits, key=lambda w: self.node_idx[w]) + # Find insertion location + first = coll_nodes[0] + next_node = first + while next_node in coll_nodes: + next_node = next_node.next + + if is_all_gather(first): + new_nodes, replacements = merge_all_gather_bucket( + self.graph, + coll_nodes, + wait_insertion_point=first_wait, + insert_before=next_node, + mode=self.bucket_mode, + ) + elif is_reduce_scatter(first): + new_nodes, replacements = merge_reduce_scatter_bucket( + self.graph, + coll_nodes, + wait_insertion_point=first_wait, + insert_before=next_node, + mode=self.bucket_mode, + ) + else: + raise ValueError( + "bucket non all_gather/reduce_scatter node is not supported" + ) + + logger.debug(f"bucketing nodes: {coll_nodes} into {new_nodes}") # noqa: G004 + + # Identify the new wait(s) and their collective start in a single pass + wait_to_start = { + n: start + for n in new_nodes + if (start := _get_collective_node_from_wait(n)) is not None + } + assert len(wait_to_start) >= 1, ( + f"Expected at least one new wait, got none in {new_nodes}" + ) + new_waits = list(wait_to_start) + new_start: fx.Node = wait_to_start[new_waits[0]] + # Use last wait as the canonical wait for scheduling (same node when len == 1) + new_wait = new_waits[-1] + + # Set manual bucketing-specific metadata (generic metadata is preserved in bucketing.py) + node_type = ( + "bucketed_all_gather" if is_all_gather(first) else "bucketed_reduce_scatter" + ) + wait_set = OrderedSet(new_waits) + for n in new_nodes: + if n in wait_set: + n.meta["manual_bucket_node_type"] = node_type + "_wait" + self.node_to_wait_map[n] = new_wait + elif n is new_start: + n.meta["manual_bucket_node_type"] = node_type + + def manual_bucket_collectives(self, nodes: list[fx.Node]) -> None: + """ + Bucket all all-gather/reduce-scatter nodes from nodes into one all-gather/reduce-scatter. + """ + # Filter out valid collectives + collectives = [n for n in nodes if n in self.collective_info] + if collectives == []: + return + grouped_collectives: dict[object, OrderedSet[fx.Node]] = defaultdict(OrderedSet) + for node in collectives: + if not ( + is_fsdp_all_gather(node, self.node_ancestors) + or is_fsdp_reduce_scatter(node) + ): + continue + key = get_full_bucket_key(node, "custom_ops") + if key is not None: + grouped_collectives[key].add(node) + + for key, nodes in grouped_collectives.items(): # type: ignore[arg-type] + self._bucket_group(list(nodes)) + + +class ManualOverlapScheduler(OverlapScheduler): + """ + Scheduler that manual buckets and reorders collective nodes based on module_bucket_plans + """ + + def __init__( + self, + gm: fx.GraphModule, + module_bucket_plans: list[list[str] | str], + insert_overlap_deps: bool, + module_stack_fn: Callable[[fx.Node], list[tuple[str, type[Any]]]] | None = None, + bucket_mode: BucketMode | None = None, + ): + # Manual overlap historically used "custom_ops" mode for bucketing + bucket_mode = bucket_mode or "custom_ops" + super().__init__( + gm, + max_in_flight_gb=0.0, + max_compute_pre_fetch=0, + collective_bucketing=True, + insert_overlap_deps=insert_overlap_deps, + compute_overlap_multipler=0.0, + max_coll_distance=0, + custom_runtime_estimation=None, + collective_estimator="analytical", + max_memory_increase_gb=None, + max_memory_increase_ratio=None, + bucket_mode=bucket_mode, + ) + self.module_bucket_plans = module_bucket_plans + self.nodes_in_subgraph: list[list[fx.Node]] = [] + + self.bucketer = ManualOverlapPreservingBucketer( + graph=self.graph, + collective_info=self.collective_info, + scheduled=OrderedSet(self.graph.nodes), + bucket_mode=bucket_mode, + ) + self.insert_overlap_deps = insert_overlap_deps + + self.module_stack_fn = module_stack_fn + + def _identify_collectives(self) -> None: + """Identify all collective operations.""" + for node in self.nodes: + if _schedulable_wait_node(node): + start = node.args[0] + info = CollectiveInfo( + start_node=start, + wait_node=node, + size_bytes=0, + estimated_time_ms=0, + exposed_time_ms=0, + ) + self.collective_info[start] = info + self.wait_to_start[node] = start + self.unscheduled_collectives.add(start) + + def _add_to_ready_queue(self, node: fx.Node) -> None: + """Manual scheduling uses single queue ordered by original node index.""" + heapq.heappush(self.on_path_ready, (self.node_idx[node], node)) + + def run(self) -> torch.fx.GraphModule: + """Entry point to run the manual bucket algorithm""" + # Bucket collectives in each bucket_module + self._manual_bucket_collectives() + + # Reorder collectives with last/next bucket_module + self._manual_reorder_graph() + + return self.gm + + def _manual_reorder_graph(self) -> None: + """ + Reorder nodes in the FX graph to enforce manual overlap dependencies. + + forward graph (all-gathers only): + modules are processed in order: module 0, 1, 2, ... + + before reordering: + ag_start_0 -> ag_wait_0 -> compute_0 -> ag_start_1 -> ag_wait_1 -> compute_1 -> ... + + Reordering prefetches module i+1's parameters while computing module i + It adds dependencies: ag_wait_i should depend on ag_start_(i+1) + This enforces ag_start_(i+1) to happen before ag_wait_i so it overlaps with module i's compute + + after reordering: + ag_start_0 -> ag_start_1 -> ag_wait_0 -> compute_0 -> ag_wait_1 -> compute_1 -> ... + + backward graph (all-gathers and reduce-scatters): + modules are processed in reverse order: module N, N-1, N-2, ... + + before reordering: + ag_start_N -> ag_wait_N -> compute_N -> rs_start_N -> rs_wait_N -> ... + + For all-gathers, prefetch module i-1's parameters while computing module i + Adds dependencies: ag_wait_i should depend on ag_start_(i-1) + So ag_start_(i-1) overlaps with module i's compute + + For reduce-scatters, defer rs_wait_i to happen after rs_start_(i-1) + Adds dependencies: rs_wait_i should depend on rs_start_(i-1) + So rs_start_i overlaps with module i-1's compute + + """ + delayed_rs_wait_nodes: list[fx.Node] = [] + current_rs_start_nodes: list[fx.Node] = [] + overlap_deps: dict[fx.Node, OrderedSet[fx.Node]] = defaultdict(OrderedSet) + + # Re-initialize after graph modification in _manual_bucket_collectives + self.node_idx = {n: i for i, n in enumerate(self.nodes)} + self.on_path_ready = [] + self.scheduled = OrderedSet() + for node in self.nodes: + if self.in_degree[node] == 0: + self._add_to_ready_queue(node) + + # schedule reduce scatter normally in self._schedule + while self.on_path_ready: + _, node = heapq.heappop(self.on_path_ready) + node_type = node.meta.get("manual_bucket_node_type", "") + + if node in self.scheduled: + continue + + if node_type == "bucketed_reduce_scatter": + # Collect reduce scatter start nodes (pre_bucket_rs and rs) + current_rs_start_nodes.append(node) + + elif node_type == "bucketed_reduce_scatter_wait": + # When we see a wait node from a new RS, flush delayed waits + # with dependencies on previously collected RS start nodes + if current_rs_start_nodes: + for delayed in delayed_rs_wait_nodes: + for rs_start in current_rs_start_nodes: + overlap_deps[delayed].add(rs_start) + delayed_rs_wait_nodes.clear() + current_rs_start_nodes.clear() + delayed_rs_wait_nodes.append(node) + + self._schedule(node) + + self.scheduled = OrderedSet(reversed(list(self.scheduled))) + picked_ag: list[fx.Node] = [] + last_compute: fx.Node | None = None + + for node in self.scheduled: + node_type = node.meta.get("manual_bucket_node_type", "") + if node_type == "bucketed_all_gather": + picked_ag.append(node) + continue + + if node_type == "bucketed_all_gather_wait": + # Connect corresponding all_gather_wait -> all_gather edges + if picked_ag: + for ag in picked_ag: + overlap_deps[self.bucketer.node_to_wait_map[node]].add(ag) + picked_ag.clear() + if is_compute_node(node): + last_compute = node + + if last_compute is not None and not bool( + OrderedSet(picked_ag) & OrderedSet(self.node_ancestors[last_compute]) + ): + for ag in picked_ag: + overlap_deps[last_compute].add(ag) + + _stable_topological_sort(self.graph, overlap_deps) + self.graph.lint() + + if self.insert_overlap_deps: + from torch._inductor.fx_passes.control_dependencies import ( + preserve_node_ordering, + ) + + preserve_node_ordering(self.graph, overlap_deps) + + def _manual_bucket_collectives(self) -> None: + """Bucket nodes in each module_bucket from module_bucket_plans.""" + self._obtain_nodes_in_subgraph() + for i, nodes in enumerate(self.nodes_in_subgraph): + self.bucketer.manual_bucket_collectives(nodes=nodes) + + _stable_topological_sort(self.graph, {}) + self.graph.lint() + self.nodes = list(self.graph.nodes) + self.in_degree = Counter(user for node in self.nodes for user in node.users) + + def _schedule(self, node: fx.Node) -> None: + """Schedule a node.""" + assert node not in self.scheduled + assert all(n in self.scheduled for n in node.all_input_nodes) + self.scheduled.add(node) + for user in node.users: + self.in_degree[user] -= 1 + if self.in_degree[user] == 0: + self._add_to_ready_queue(user) + + def _obtain_nodes_in_subgraph(self) -> None: + """ + Obtain nodes in each subgraph from module_bucket_plans + """ + graph_view: GraphView | None = make_graph_view(self.graph, self.module_stack_fn) + if graph_view is None: + return + + for module in self.module_bucket_plans: + subgraph_view = get_subgraph_by_path(graph_view, module) + self.nodes_in_subgraph.append(subgraph_view) + + all_subgraph_nodes = [ + node for sublist in self.nodes_in_subgraph for node in sublist + ] + unique_subgraph_nodes = list(OrderedSet(all_subgraph_nodes)) + assert len(all_subgraph_nodes) <= len(unique_subgraph_nodes), ( + f"Overlapping FX nodes detected across subgraphs in `module_bucket_plans`. " + f"Expected disjoint node sets but found " + f"{len(all_subgraph_nodes) - len(unique_subgraph_nodes)} duplicated node(s)." + ) + + +def manual_overlap_bucketing( + gm: torch.fx.GraphModule, + module_bucket_plans: list[list[str] | str], + insert_overlap_deps: bool = False, + module_stack_fn: Callable[[fx.Node], list[tuple[str, type[Any]]]] | None = None, + bucket_mode: BucketMode | None = None, +) -> torch.fx.GraphModule: + """Schedule nodes based on user specifications in module_bucket_plans + The manual overlapping consists of two steps: + Step 1: bucket all-gather/reduce-scatter in each module in module_bucket_plans + Step 2: reorder all-gather to overlap with last module_bucket & + reorder reduce-scatter to overlap with next module_bucket + TODO(ruisizhang123): allow users to explicitly specify which + module_bucket they want to overlap. + + Args: + gm: input graph module to optimize. + module_bucket_plans: user specified FQNs + module_stack_fn: Optional callable for extracting module hierarchy from nodes. + Used to construct a GraphView for identifying nodes in module_bucket_plans. + The module_class component of the returned tuples is not used by this pass. + + See the `module_stack_fn` parameter in `make_graph_view` (graph_view.py) for + detailed documentation on signature, return format, and usage examples. + bucket_mode: Bucket mode for collective bucketing. None uses default. + """ + # decode abbreviated FQNs to actual FQNs + overlapped_gm = ManualOverlapScheduler( + gm, + module_bucket_plans, + insert_overlap_deps, + module_stack_fn, + bucket_mode=bucket_mode, + ).run() + overlapped_gm.recompile() + return overlapped_gm diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/overlap_preserving_bucketer.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/overlap_preserving_bucketer.py new file mode 100644 index 0000000000000000000000000000000000000000..88a6c6d05f8fe5f2768a33ab21fdb39b002864f2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/overlap_preserving_bucketer.py @@ -0,0 +1,1117 @@ +import itertools +import logging +from collections import defaultdict +from dataclasses import dataclass +from typing import Any, Literal, Optional + +import torch +import torch.fx as fx +from torch._dynamo.utils import counters +from torch._inductor.augmented_graph_helper import AugmentedGraphHelper +from torch._inductor.fx_passes.bucketing import ( + _default_bucket_mode, + _get_collective_node_from_wait, + _schedulable_wait_node, + BucketMode, + get_full_bucket_key, + has_mergeable_all_gather_convert_dtype, + is_all_gather_into_tensor as is_all_gather, + is_reduce_scatter_tensor as is_reduce_scatter, +) +from torch._inductor.fx_passes.fsdp import is_fsdp_all_gather +from torch._inductor.fx_passes.overlap_scheduling import ( + CollBucket, + CollectiveInfo, + get_group_name, + is_compute_node, + log as overlap_scheduling_log, +) +from torch._logging import trace_structured +from torch.utils._ordered_set import OrderedSet + + +bucket_log = logging.getLogger(__name__) + + +@dataclass +class WhyNoBucket: + name1: str + name2: str + reason: str + args: tuple[Any, ...] + + def __init__(self, node1: fx.Node, node2: fx.Node) -> None: + self.name1 = node1.name + self.name2 = node2.name + self.reason = "" + self.args = () + + def __call__(self, reason: str, *args: Any) -> None: + if bucket_log.isEnabledFor(logging.DEBUG): + bucket_log.debug( + "cannot bucket %s with %s: " + reason, # noqa: G003 + self.name1, + self.name2, + *args, + ) + + +def is_collective_or_wait(n: fx.Node) -> bool: + """Check if node is a collective start or wait.""" + if _schedulable_wait_node(n): + return True + # Collective starts have exactly one use: the wait_tensor + if len(n.users) == 1: + user = next(iter(n.users.keys())) + if _schedulable_wait_node(user): + return True + return False + + +@dataclass +class PGEvent: + """ + Represents an important event in a process group timeline. Either + a collective start, wait, or hiding compute. Each node is linked + to its prev and next and these dependencies are reflected + in the augmented graph. + + We want to enforce a sequential ordering of collective starts and waits + because NCCL collectives on the same process group execute on the same CUDA + stream, creating implicit dependencies between all operations on that PG. + + A wait of a particular collective will implicitly force realization of all collectives + enqueued prior to that collective. + """ + + node: fx.Node + event_type: Literal["compute", "starts", "waits"] + position: int + prev: Optional["PGEvent"] = None + next: Optional["PGEvent"] = None + + @property + def is_start(self) -> bool: + return self.event_type == "starts" + + @property + def is_wait(self) -> bool: + return self.event_type == "waits" + + @property + def is_compute(self) -> bool: + return self.event_type == "compute" + + def unlink(self) -> tuple[Optional["PGEvent"], Optional["PGEvent"]]: + """Remove this event from the linked list, return (prev, next).""" + prev_event, next_event = self.prev, self.next + if self.prev: + self.prev.next = self.next + if self.next: + self.next.prev = self.prev + self.prev = None + self.next = None + return prev_event, next_event + + def insert_between( + self, prev_event: Optional["PGEvent"], next_event: Optional["PGEvent"] + ) -> None: + """Insert this event between prev_event and next_event in the linked list.""" + if prev_event: + prev_event.next = self + self.prev = prev_event + + if next_event: + next_event.prev = self + self.next = next_event + + +class OverlapPreservingBucketer: + """ + Buckets collective operations while preserving compute-collective overlap relationships. + Uses an augmented graph to track dependencies between compute and collective operations. + """ + + def __init__( + self, + graph: fx.Graph, + collective_info: dict[fx.Node, CollectiveInfo], + scheduled: OrderedSet[fx.Node], + max_bucket_memory_gb: float = 1.0, + max_coll_distance: int = 1000, + insert_overlap_deps: bool = False, + collective_bucketing: bool = True, + bucket_mode: BucketMode | None = None, + bucket_exposed_first: bool | None = None, + region_of: dict[fx.Node, Any] | None = None, + bucket_only_internode_comms: bool = False, + ): + self.graph = graph + self.collective_info = collective_info + self.scheduled = scheduled + self.max_bucket_memory_gb = max_bucket_memory_gb + self.node_idx = {n: i for i, n in enumerate(scheduled)} + self.max_coll_distance = max_coll_distance + self.insert_overlap_deps = insert_overlap_deps + self.bucket_exposed_first = bucket_exposed_first + self.bucket_only_internode_comms = bucket_only_internode_comms + self.bucket_mode = bucket_mode or _default_bucket_mode() + self.collective_bucketing = collective_bucketing + self.region_of: dict[fx.Node, Any] = region_of or {} + self.node_to_event: dict[fx.Node, PGEvent] = {} + self.all_hiding_nodes: OrderedSet[fx.Node] = OrderedSet() + + # Compute ancestors including original graph edges and hiding interval dependencies + self.node_ancestors = self._compute_node_ancestors() + self.aug_graph = AugmentedGraphHelper(self.graph, self.node_ancestors) + + # Build timelines and add constraints to aug_graph + self.pg_to_timeline_head: dict[str, PGEvent | None] = self.build_timelines() + self._add_hiding_interval_constraints() + + def _compute_node_ancestors(self) -> dict[fx.Node, OrderedSet[fx.Node]]: + """ + Compute ancestor sets for all nodes including: + 1. Original graph edges + 2. Hiding interval deps: collective_start -> hiding_node -> wait + """ + augmented_inputs: dict[fx.Node, OrderedSet[fx.Node]] = defaultdict(OrderedSet) + for start, info in self.collective_info.items(): + if info.is_exposed: + continue + for hiding_node in info.hiding_nodes: + augmented_inputs[hiding_node].add(start) + augmented_inputs[info.wait_node].add(hiding_node) + + node_ancestors: dict[fx.Node, OrderedSet[fx.Node]] = defaultdict(OrderedSet) + for node in self.scheduled: + for input_node in itertools.chain( + augmented_inputs[node], node.all_input_nodes + ): + node_ancestors[node].add(input_node) + node_ancestors[node] |= node_ancestors[input_node] + + return node_ancestors + + def build_timelines(self) -> dict[str, PGEvent | None]: + "Construct each process groups ordered series of event" + all_pgs: OrderedSet[str] = OrderedSet() + for start in self.collective_info: + pg = get_group_name(start) + all_pgs.add(pg) + + pg_timeline: dict[str, PGEvent | None] = {} + for pg in all_pgs: + pg_timeline[pg] = self.build_timeline(pg) + + return pg_timeline + + def build_timeline(self, pg: str) -> PGEvent | None: + """ + Build a timeline of important events (starts, waits, hiding compute) for this process group + and constrain this ordering in the augmented graph. + + Sequential dependencies are added between all events because NCCL collectives on the same + process group execute on the same CUDA stream, enforcing LIFO semantics where later-issued + collectives must complete before earlier ones can finish. + """ + + head = None + prev_event = None + position = 0 + hiding_nodes = OrderedSet() + + for node in self.scheduled: + node_type = None + + # Determine if this node is relevant for this PG + if node in self.collective_info and get_group_name(node) == pg: + node_type = "starts" + hiding_nodes |= self.collective_info[node].hiding_nodes + elif _schedulable_wait_node(node): + wait_coll = _get_collective_node_from_wait(node) + if isinstance(wait_coll, fx.Node) and get_group_name(wait_coll) == pg: + node_type = "waits" + # Wait for a different PG but hiding a collective on this PG + elif node in hiding_nodes: + node_type = "compute" + elif is_compute_node(node) or node in hiding_nodes: + node_type = "compute" + + if node_type is None: + continue + + event = PGEvent(node=node, event_type=node_type, position=position) # type: ignore[arg-type] + + event.insert_between(prev_event, None) + + # Add sequential dependency to augmented graph + if prev_event: + self.aug_graph.add_extra_dep(n=event.node, dep=prev_event.node) + else: + head = event + + prev_event = event + position += 1 + + return head + + def _populate_node_to_event(self, pg: str) -> None: + """Populate node_to_event mapping for a specific PG's timeline.""" + self.node_to_event.clear() + head = self.pg_to_timeline_head[pg] + curr = head + while curr is not None: + self.node_to_event[curr.node] = curr + curr = curr.next + + def _add_hiding_interval_constraints(self) -> None: + """ + Add hiding interval constraints: start -> compute -> wait. + """ + for start, info in self.collective_info.items(): + if info.is_exposed: + continue + for hn in info.hiding_nodes: + # Enforce: start -> compute -> wait + self.aug_graph.add_extra_dep(n=hn, dep=start) + self.aug_graph.add_extra_dep(n=info.wait_node, dep=hn) + + self.all_hiding_nodes |= info.hiding_nodes + + def identify_internode_group_names(self) -> OrderedSet[str]: + # Identify internode comm groups. + # Temporary uses FSDP pattern as heuristic for outer groups. + checked_pgs = OrderedSet() + internode_pgs = OrderedSet() + for start in self.collective_info: + pg = get_group_name(start) + if is_all_gather(start) and pg not in checked_pgs: + if is_fsdp_all_gather(start): + internode_pgs.add(pg) + checked_pgs.add(pg) + return internode_pgs + + def _bucket_collectives_impl(self) -> list[CollBucket]: + """Find and apply bucket transformations for collectives.""" + pg_collectives: dict[str, OrderedSet[fx.Node]] = defaultdict(OrderedSet) + internode_pgs = self.identify_internode_group_names() + + for start in self.collective_info: + pg = get_group_name(start) + pg_collectives[pg].add(start) + + all_buckets: list[CollBucket] = [] + for pg, collectives in pg_collectives.items(): + if self.bucket_only_internode_comms and pg not in internode_pgs: + continue + + # Populate node_to_event for this PG's timeline + self._populate_node_to_event(pg) + + grouped_collectives: dict[object, OrderedSet[fx.Node]] = defaultdict( + OrderedSet + ) + for start in collectives: + key = get_full_bucket_key(start, self.bucket_mode) + if key[1] is not None: + grouped_collectives[key].add(start) + + for key, collective_group in grouped_collectives.items(): + bucket_log.debug( + "bucketing collective group with key %s: %s", + key, + [n.name for n in collective_group], + ) + buckets = self._find_buckets(collective_group, internode_pgs) + all_buckets.extend(buckets) + + for coll_bucket in all_buckets: + if len(coll_bucket.collectives) <= 1: + continue + + counters["inductor"]["collective_buckets"] += 1 + self._apply_bucket(coll_bucket) + return all_buckets + + def _apply_deps_and_effect_tokens(self) -> None: + """Apply topological sort and effect tokens to preserve overlap.""" + from torch._dynamo.graph_deduplication import _stable_topological_sort + + # Clean up any remaining erased node references and cycles + self.aug_graph.remove_erased_extra_deps() + autofix = torch._inductor.config.aten_distributed_optimizations.overlap_scheduling_autofix_cycles + self.aug_graph.check_and_maybe_autofix_cyclic_extra_deps(autofix=autofix) + additional_deps = self.aug_graph.get_all_extra_deps() + + for n, deps in additional_deps.items(): + torch._check( + not n._erased, lambda: f"Erased node deps not transferred: {n}" + ) + for d in deps: + torch._check( + not d._erased, lambda: f"Erased node deps not transferred: {d}" + ) + + _stable_topological_sort(self.graph, additional_deps) + + if self.insert_overlap_deps: + # Filter out collective-to-collective deps (handled by NCCL stream ordering) + filtered_deps: dict[fx.Node, OrderedSet[fx.Node]] = {} + for node, deps in additional_deps.items(): + filtered_node_deps: OrderedSet[fx.Node] = OrderedSet() + for dep in deps: + if not (is_collective_or_wait(node) and is_collective_or_wait(dep)): + filtered_node_deps.add(dep) + if filtered_node_deps: + filtered_deps[node] = filtered_node_deps + + if filtered_deps: + from torch._inductor.fx_passes.control_dependencies import ( + preserve_node_ordering, + ) + + preserve_node_ordering(self.graph, filtered_deps) + + def bucket_collectives(self) -> None: + """Run the full bucketing and dep application flow. + + Order is important: + 1. Bucketing - merge collectives into buckets + 2. Inline fusions - expand call_module back to original nodes + 3. Transfer deps - move deps from erased nodes to their replacements + 4. Add control deps - apply effect tokens and topo sort + + Steps 2-3 MUST happen before step 4, because control deps need to + reference the final inlined nodes, not the erased fusion modules. + """ + # Step 1: Bucket collectives + all_buckets: list[CollBucket] | None = None + if self.collective_bucketing: + all_buckets = self._bucket_collectives_impl() + + # Step 2: Inline fusion regions (expand call_module -> original nodes) + replaced: dict[fx.Node, fx.Node | None] = {} + if self.region_of: + from torch._inductor.fx_passes.fusion_regions import expand_fusion_regions + + gm = self.graph.owning_module + replaced = expand_fusion_regions(gm, self.region_of) + + # Step 3: Transfer deps from erased fusion modules to inlined nodes + if replaced: + self.aug_graph.transfer_erased_node_deps(replaced) + + # Step 4: Add control deps (MUST be after inline + transfer) + self._apply_deps_and_effect_tokens() + self.graph.lint() + + if ( + overlap_scheduling_log.isEnabledFor(logging.DEBUG) + and all_buckets is not None + ): + log_strs: list[str] = [] + stats_num_buckets_per_key = defaultdict(int) + stats_num_bucketed_collectives_per_key = defaultdict(int) + stats_num_total_collectives_per_key = defaultdict(int) + + def _bucket_key(node): + return get_full_bucket_key(node, self.bucket_mode) + + for start, info in self.collective_info.items(): + stats_num_total_collectives_per_key[_bucket_key(start)] += 1 + + for i, bucket in enumerate(all_buckets): + bucket_n = len(bucket.collectives) + if bucket_n == 0: + continue + node = bucket.collectives[0] + key = _bucket_key(node) + stats_num_buckets_per_key[key] += 1 + stats_num_bucketed_collectives_per_key[key] += bucket_n + log_strs.append(f"bucket[{i}] key:{key} len:{bucket_n}:{bucket}") + for coll in bucket.collectives: + info = self.collective_info[coll] + hns = info.hiding_nodes + log_strs.append(f"coll:{coll} hiding_nodes:{hns}") + + bucket_log_strs: list[str] = [] + for key, num_buckets in stats_num_buckets_per_key.items(): + num_colls = stats_num_bucketed_collectives_per_key[key] + bucket_log_strs.append( + f"bucket key stats {key}: {num_colls} in {num_buckets}" + f" buckets of total:{stats_num_total_collectives_per_key[key]}" + ) + bucket_log_strs.append("") + # Add stats to the beginning + log_strs[:0] = bucket_log_strs + bucket_logs = "\n".join(log_strs) + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "inductor_fx_passes_overlap_bucketing", + "encoding": "string", + }, + payload_fn=lambda: bucket_logs, + ) + + def _compute_overlap_ratio(self, node: fx.Node) -> float: + """ + Compute what fraction of the collective's time is hidden by compute. + Returns 0.0 for fully exposed, 1.0 for fully hidden. + """ + info = self.collective_info[node] + if info.estimated_time_ms <= 0: + return 0.0 + hidden_time = info.estimated_time_ms - info.exposed_time_ms + return hidden_time / info.estimated_time_ms + + def _should_bucket_exposed_first( + self, + collective_group: OrderedSet[fx.Node], + current_pg: str, + internode_pgs: OrderedSet[str], + ) -> bool: + """Determine whether to bucket exposed collectives first.""" + if self.bucket_exposed_first is None: + return current_pg in internode_pgs + return self.bucket_exposed_first + + def _find_buckets( + self, + collective_group: OrderedSet[fx.Node], + internode_pgs: OrderedSet[str], + ) -> list[CollBucket]: + """Find valid buckets within a group of similar collectives.""" + max_bucket_bytes = int(self.max_bucket_memory_gb * 1024 * 1024 * 1024) + buckets = [] + processed: OrderedSet[fx.Node] = OrderedSet() + if len(collective_group) == 0: + return [] + + current_pg = get_group_name(next(iter(collective_group))) + + bucket_exposed_first = self._should_bucket_exposed_first( + collective_group, current_pg, internode_pgs + ) + if bucket_exposed_first: + # Sort by overlap ratio (ascending) to bucket least hidden collectives first. + # Exposed collectives benefit most from bucketing since their latency is on the + # critical path. Prioritizing them also preserves hiding relationships for + # already-hidden collectives, which have less to gain from bucketing. + sorted_collectives = sorted( + collective_group, + key=lambda n: (self._compute_overlap_ratio(n), self.node_idx[n]), + ) + else: + sorted_collectives = sorted( + collective_group, + key=lambda n: self.node_idx[n], + ) + + for i, start_node in enumerate(sorted_collectives): + if start_node in processed: + continue + + if ( + start_node in self.all_hiding_nodes + or self.collective_info[start_node].wait_node in self.all_hiding_nodes + ): + continue + + # Initialize bucket with first collective + bucket_info = CollBucket( + collectives=[start_node], + total_bytes=self.collective_info[start_node].size_bytes, + ) + processed.add(start_node) + + # Greedy optimization: stop after consecutive failures + consecutive_failures = 0 + max_consecutive_failures = 20 + start_node_idx = self.node_idx[start_node] + + # Check candidates in sorted order, break when beyond max distance + for candidate in sorted_collectives[i + 1 : i + 1 + self.max_coll_distance]: + candidate_bytes = self.collective_info[candidate].size_bytes + # proxy on memory use, if we see a too large bucket, + # dont look for another, later bucket + if bucket_info.total_bytes + candidate_bytes > max_bucket_bytes: + break + + if candidate in processed: + continue + + candidate_node_idx = self.node_idx[candidate] + if ( + bucket_exposed_first + and abs(candidate_node_idx - start_node_idx) + > max_consecutive_failures + ): + # Since collectives are sorted by overlap ratio rather than graph + # position, skip candidates too far apart in the graph to avoid + # creating buckets that block future bucketing opportunities. + continue + + if self._can_add_to_bucket(bucket_info, candidate): + bucket_info.collectives.append(candidate) + bucket_info.total_bytes += candidate_bytes + processed.add(candidate) + consecutive_failures = 0 # Reset on success + else: + consecutive_failures += 1 + if consecutive_failures >= max_consecutive_failures: + break + + if len(bucket_info.collectives) > 1: + buckets.append(bucket_info) + + return buckets + + def _ancestor_dep(self, n1: fx.Node, n2: fx.Node) -> bool: + """Check if there's an ancestor relationship between two nodes.""" + return n1 in self.node_ancestors[n2] or n2 in self.node_ancestors[n1] + + def _get_intervals( + self, event: PGEvent + ) -> tuple[tuple[int, int] | None, list[tuple[int, int]]]: + """Get (execution_interval, hiding_intervals) for a collective event. + + Returns: + (execution_interval, hiding_intervals) where: + - execution_interval is (start_pos, wait_pos) or None + - hiding_intervals is a list of (start_pos, compute_pos) tuples, one for each hiding node + + Works for both start and wait events by looking up the collective info. + """ + # For start events, directly use the node + if event.is_start: + coll = event.node + # For wait events, look up the start node from the event's args + elif event.is_wait: + coll = _get_collective_node_from_wait(event.node) + if coll is None: + return None, [] + else: + return None, [] + + if coll not in self.collective_info: + return None, [] + + info = self.collective_info[coll] + start_event = self.node_to_event[coll] + wait_event = self.node_to_event[info.wait_node] + + execution_interval = (start_event.position, wait_event.position) + + hiding_intervals = [] + if info.hiding_nodes: + for hiding_node in info.hiding_nodes: + hiding_intervals.append( + ( + start_event.position, + self.node_to_event[hiding_node].position, + ) + ) + + return execution_interval, hiding_intervals + + def _preserves_hiding_intervals( + self, + bucket_info: CollBucket, + candidate: fx.Node, + start_pos: fx.Node, + wait_pos: fx.Node, + why: WhyNoBucket, + ) -> bool: + """ + Check that (start_pos, wait_pos) doesn't violate any hiding intervals or collectives. + + Collects all execution and hiding intervals in the affected timeline regions, + then checks: + 1. All bucket hiding compute stays between new start/wait + 2. No other collective's compute interval is enclosed by bucket execution interval + 3. No other collective's execution interval encloses bucket compute intervals + """ + # Collect all collectives being bucketed + all_bucketed_colls = [candidate] + list(bucket_info.collectives) + all_bucketed_waits = [ + self.collective_info[coll].wait_node for coll in all_bucketed_colls + ] + + # Collect hiding compute positions for the bucket + bucket_hiding_compute_positions = [] + for coll in all_bucketed_colls: + for coll_hiding_node in self.collective_info[coll].hiding_nodes: + bucket_hiding_compute_positions.append( + self.node_to_event[coll_hiding_node].position + ) + + # Get new positions + new_start_event = self.node_to_event[start_pos] + new_wait_event = self.node_to_event[wait_pos] + + # Check 1: All bucket hiding compute must be between new start and wait + for compute_pos in bucket_hiding_compute_positions: + if not (new_start_event.position < compute_pos < new_wait_event.position): + why( + "hiding compute at pos %d not between start %d and wait %d", + compute_pos, + new_start_event.position, + new_wait_event.position, + ) + return False + + def get_wait(n: fx.Node) -> fx.Node: + return self.collective_info[n].wait_node + + def get_pos(n: fx.Node) -> int: + return self.node_to_event[n].position + + latest_start_pos = max(get_pos(candidate), get_pos(bucket_info.collectives[0])) + earliest_wait_pos = min( + get_pos(get_wait(candidate)), get_pos(get_wait(bucket_info.collectives[0])) + ) + + # Bucket execution interval + bucket_execution_interval = (new_start_event.position, new_wait_event.position) + + # Because collectives on the same PG operate under LIFO semantics, + # it's only possible for us to force an early realization of an unrelated collective + # by delaying a start or raising a wait. + # We search in the interval from old_start -> new_start, to see if would be + # forcing another collective to be realized prior to its hiding nodes. + # Similarly, we search from old_wait -> new_wait, in the reverse direction, + # to check the same thing. + + execution_intervals = [bucket_execution_interval] + hiding_intervals = [ + (bucket_execution_interval[0], pos) + for pos in bucket_hiding_compute_positions + ] + + curr_event = new_start_event.next + while curr_event is not None and curr_event.position < latest_start_pos: + if ( + curr_event.node not in all_bucketed_colls + and curr_event.node not in all_bucketed_waits + ): + exec_interval, hiding_interval_list = self._get_intervals(curr_event) + if exec_interval: + execution_intervals.append(exec_interval) + hiding_intervals.extend(hiding_interval_list) + curr_event = curr_event.next + + curr_event = new_wait_event.prev + while curr_event is not None and curr_event.position > earliest_wait_pos: + if ( + curr_event.node not in all_bucketed_colls + and curr_event.node not in all_bucketed_waits + ): + exec_interval, hiding_interval_list = self._get_intervals(curr_event) + if exec_interval: + execution_intervals.append(exec_interval) + hiding_intervals.extend(hiding_interval_list) + curr_event = curr_event.prev + + # Check: no hiding interval should be enclosed by any execution interval + def enclosed_interval(inner: tuple[int, int], outer: tuple[int, int]) -> bool: + return outer[0] < inner[0] and inner[1] < outer[1] + + for hiding_interval in hiding_intervals: + for execution_interval in execution_intervals: + if enclosed_interval(hiding_interval, execution_interval): + why( + "hiding interval %s enclosed by execution interval %s", + hiding_interval, + execution_interval, + ) + return False + + return True + + def remove_from_event(self, node: fx.Node) -> tuple[PGEvent | None, PGEvent | None]: + """Remove node from timeline and return (prev_event, next_event).""" + event = self.node_to_event[node] + assert not event.is_compute, "Cannot remove compute events from timeline" + + prev_event, next_event = event.unlink() + + # Remove augmented graph dependency + if prev_event: + self.aug_graph.remove_extra_dep(n=node, dep=prev_event.node) + if next_event: + self.aug_graph.remove_extra_dep(n=next_event.node, dep=node) + + # Add bypass dependency + if prev_event and next_event: + self.aug_graph.add_extra_dep(n=next_event.node, dep=prev_event.node) + + return prev_event, next_event + + def restore_to_event( + self, + node: fx.Node, + prev_event: PGEvent | None, + next_event: PGEvent | None, + ) -> None: + """Restore node to timeline after failed merge attempt.""" + event = self.node_to_event[node] + + # Reinsert into linked list + event.insert_between(prev_event, next_event) + if prev_event: + self.aug_graph.add_extra_dep(n=node, dep=prev_event.node) + if next_event and not prev_event: + self.aug_graph.add_extra_dep(n=next_event.node, dep=node) + + # Remove bypass dependency + if prev_event and next_event: + self.aug_graph.remove_extra_dep(n=next_event.node, dep=prev_event.node) + + def _try_timeline_position( + self, + bucket_info: CollBucket, + candidate: fx.Node, + start_pos: fx.Node, + wait_pos: fx.Node, + why: WhyNoBucket, + ) -> bool: + """ + Try a specific timeline position for the candidate. + Returns True if valid and merges are successful. + """ + candidate_info = self.collective_info[candidate] + candidate_wait = candidate_info.wait_node + + # Quick check: does this violate hiding intervals? + if not self._preserves_hiding_intervals( + bucket_info, candidate, start_pos, wait_pos, why + ): + return False + + # Determine which start needs to move + existing_coll = bucket_info.collectives[0] + if start_pos == existing_coll: + start_to_move = candidate + else: + assert start_pos == candidate + start_to_move = existing_coll + + # Remove start from timeline + start_prev, start_next = self.remove_from_event(start_to_move) + + # Check if starts can be merged + if self.aug_graph.has_path(existing_coll, candidate) or self.aug_graph.has_path( + candidate, existing_coll + ): + # Restore start constraints + self.restore_to_event(start_to_move, start_prev, start_next) + why("path exists between starts") + return False + + # Merge starts + self.aug_graph.merge_to_set(existing_coll, candidate) + + # Determine which wait needs to move + existing_wait = self.collective_info[existing_coll].wait_node + candidate_wait = self.collective_info[candidate].wait_node + + if wait_pos == existing_wait: + wait_to_move = candidate_wait + else: + wait_to_move = existing_wait + + # Remove wait from timeline + wait_prev, wait_next = self.remove_from_event(wait_to_move) + + # Check if waits can be merged + if self.aug_graph.has_path( + existing_wait, candidate_wait + ) or self.aug_graph.has_path(candidate_wait, existing_wait): + # Restore wait constraints + self.restore_to_event(wait_to_move, wait_prev, wait_next) + # Unmerge the start we just merged + self.aug_graph.unmerge_node(candidate) + # Restore start constraints + self.restore_to_event(start_to_move, start_prev, start_next) + why("path exists between waits") + return False + + # Merge waits - success! + self.aug_graph.merge_to_set(existing_wait, candidate_wait) + + # Update node_to_event for moved nodes + target_start_event = self.node_to_event[start_pos] + target_wait_event = self.node_to_event[wait_pos] + + self.node_to_event[candidate] = target_start_event + self.node_to_event[candidate_wait] = target_wait_event + self.node_to_event[existing_coll] = target_start_event + self.node_to_event[existing_wait] = target_wait_event + + return True + + def _has_ancestor_conflicts( + self, bucket_info: CollBucket, candidate: fx.Node + ) -> bool: + """ + Check if candidate has ancestor conflicts with bucket collectives. + Returns True if there are conflicts. + """ + candidate_info = self.collective_info[candidate] + candidate_wait = candidate_info.wait_node + + for coll in bucket_info.collectives: + if ( + coll in self.node_ancestors[candidate] + or candidate in self.node_ancestors[coll] + ): + return True + + # Check if waits are ancestors of each other + coll_wait = self.collective_info[coll].wait_node + if ( + coll_wait in self.node_ancestors[candidate_wait] + or candidate_wait in self.node_ancestors[coll_wait] + ): + return True + + # Check if existing hiding node conflicts with candidate wait + for old_hiding_node in self.collective_info[coll].hiding_nodes: + if candidate_wait in self.node_ancestors[old_hiding_node]: + return True + + # Check if candidate hiding node conflicts with existing wait + for new_hiding_node in candidate_info.hiding_nodes: + if coll_wait in self.node_ancestors[new_hiding_node]: + return True + + return False + + def _can_add_to_bucket( + self, + bucket_info: CollBucket, + candidate: fx.Node, + ) -> bool: + """ + Check if candidate can be added to bucket without breaking comm/compute overlap. + + Strategy: Try all timeline positions - combinations of [existing_start, candidate_start] + x [existing_wait, candidate_wait]. For each position, verify: + 1. Hiding intervals preserved - for any (start, hiding_compute, wait) interval, no other + collective's (start, wait) pair falls between start and hiding_compute, which would + force realization and break overlap due to LIFO semantics + 2. Topologically valid (no dependency cycles) + + Return True if any timeline position satisfies both constraints. + """ + existing_coll = bucket_info.collectives[0] + why = WhyNoBucket(existing_coll, candidate) + + candidate_info = self.collective_info[candidate] + + if ( + candidate in self.all_hiding_nodes + or candidate_info.wait_node in self.all_hiding_nodes + ): + why("nyi: bucketing collective used for overlap") + return False + + # Step 1: Quick check using precomputed ancestors + # These ancestors are computed prior to adding augmented dependencies and not updated, + # so if any of these checks fail then the merge will not be topologically valid + # even ignoring comm/compute overlap + if self._has_ancestor_conflicts(bucket_info, candidate): + why("has ancestor conflicts") + return False + + # Step 2: Try different rail positions + existing_wait = self.collective_info[existing_coll].wait_node + + candidate_start = candidate + candidate_wait = candidate_info.wait_node + + # Try combinations in order of likelihood to succeed + # (early start, later wait is most likely to work) + combinations = [ + ( + existing_coll, + candidate_wait, + ), # Move candidate start early, keep wait late + ( + existing_coll, + existing_wait, + ), # Move candidate start early, move wait early + (candidate_start, candidate_wait), # Keep both in place + (candidate_start, existing_wait), # Keep start in place, move wait early + ] + + for i, (start_pos, wait_pos) in enumerate(combinations): + if self._try_timeline_position( + bucket_info, candidate, start_pos, wait_pos, why + ): + bucket_log.debug( + "bucketed %s with %s using timeline position %d: (start=%s, wait=%s)", + candidate.name, + existing_coll.name, + i + 1, + start_pos.name, + wait_pos.name, + ) + return True + + why("all timeline positions failed") + return False + + def _apply_bucket(self, bucket_info: CollBucket) -> None: + """ + Apply bucketing transformation. + + Dependencies are added to aug_graph.extra_deps and transferred from old nodes. + """ + + from torch._inductor.fx_passes.bucketing import ( + is_all_reduce_tensor, + merge_all_gather_bucket, + merge_all_reduce_bucket, + merge_reduce_scatter_bucket, + ) + + bucket = bucket_info.collectives + + # Collect old nodes BEFORE they're erased + old_starts = list(bucket) + old_waits = [self.collective_info[n].wait_node for n in bucket] + + fused_convert_dtypes = [] + for n in old_starts: + if has_mergeable_all_gather_convert_dtype(n): + fused_convert_dtypes.append(n.args[0]) + + # Find where to place the bucketed operations + next_node = bucket[0] + while next_node in bucket: + next_node = next_node.next + + # Don't use wait_insertion_point - let merge functions place waits naturally + # The wait_insertion_point feature tries to move waits to a specific location, + # but this can cause issues when that location is one of the nodes being erased + # Create bucketed collective (this will erase old nodes) + if is_all_gather(bucket[0]): + new_nodes, replacements = merge_all_gather_bucket( + self.graph, + bucket, + insert_before=next_node, + mode=self.bucket_mode, + ) + elif is_all_reduce_tensor(bucket[0]): + new_nodes, replacements = merge_all_reduce_bucket( + self.graph, + bucket, + mode=self.bucket_mode, + insert_before=next_node, + ) + else: + assert is_reduce_scatter(bucket[0]) + new_nodes, replacements = merge_reduce_scatter_bucket( + self.graph, + bucket, + insert_before=next_node, + mode=self.bucket_mode, + ) + + # Identify the new wait(s) and their collective start in a single pass + wait_to_start = { + n: start + for n in new_nodes + if (start := _get_collective_node_from_wait(n)) is not None + } + new_waits = list(wait_to_start) + + # Create mapping of all erased nodes to their replacements + erased_to_new: dict[fx.Node, fx.Node | None] = {} + new_start = wait_to_start[new_waits[0]] + if len(new_waits) == 1: + # Standard bucketing: single start + single wait + new_wait = new_waits[0] + for old_start in old_starts: + erased_to_new[old_start] = new_start + for old_wait in old_waits: + erased_to_new[old_wait] = new_wait + else: + # Coalesced bucketing: single start + N waits (one per original tensor) + assert len(new_waits) == len(old_waits) + for old_start in old_starts: + erased_to_new[old_start] = new_start + erased_to_new.update(dict(zip(old_waits, new_waits))) + + # Handle convert_element_type nodes that were fused and erased + # The bucketed operation may have a _pre_bucket op that handles dtype conversion + if fused_convert_dtypes: + # In custom_ops mode, the _pre_bucket_all_gather node handles dtype conversion + # In default mode, convert nodes are just erased — map them to new_start + new_convert_dtypes_node = new_start.kwargs.get("out") + if ( + isinstance(new_convert_dtypes_node, fx.Node) + and new_convert_dtypes_node.target + == torch.ops.bucketing._pre_bucket_all_gather.default + ): + replacement = new_convert_dtypes_node + else: + replacement = new_start + + for n in fused_convert_dtypes: + erased_to_new[n] = replacement + + # Transfer all dependencies from old nodes to new nodes + self.aug_graph.transfer_erased_node_deps(erased_to_new) + + +def finalize_overlap_scheduling( + gm: fx.GraphModule, + collective_info: dict[fx.Node, CollectiveInfo], + scheduled: OrderedSet[fx.Node], + *, + collective_bucketing: bool = False, + insert_overlap_deps: bool = False, + max_bucket_memory_gb: float = 2.0, + max_coll_distance: int = 1000, + region_of: dict[fx.Node, Any] | None = None, + bucket_exposed_first: bool | None = None, + bucket_only_internode_comms: bool = False, + bucket_mode: BucketMode | None = None, +) -> None: + """ + Finalize overlap scheduling by applying deps, inlining fusions, and optionally bucketing. + + This is the main entry point for post-scheduling graph transformations: + 1. Bucket collectives (if collective_bucketing=True) + 2. Inline fusion regions back to original nodes + 3. Transfer deps from erased nodes to replacements + 4. Apply topological sort and effect tokens + + Args: + gm: The graph module to modify + collective_info: Dict mapping collective start nodes to their CollectiveInfo + scheduled: Ordered set of scheduled nodes + collective_bucketing: Whether to bucket collectives + insert_overlap_deps: Whether to insert effect tokens for overlap deps + max_bucket_memory_gb: Maximum memory for a bucket in GB + max_coll_distance: Maximum distance for bucketing candidates + region_of: Optional dict mapping module nodes to FusionRegions + """ + bucketer = OverlapPreservingBucketer( + graph=gm.graph, + collective_info=collective_info, + scheduled=scheduled, + max_bucket_memory_gb=max_bucket_memory_gb, + max_coll_distance=max_coll_distance, + insert_overlap_deps=insert_overlap_deps, + collective_bucketing=collective_bucketing, + bucket_exposed_first=bucket_exposed_first, + bucket_only_internode_comms=bucket_only_internode_comms, + region_of=region_of, + bucket_mode=bucket_mode, + ) + bucketer.bucket_collectives() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/overlap_scheduling.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/overlap_scheduling.py new file mode 100644 index 0000000000000000000000000000000000000000..08b7853c667abf78f3a0e7a5522fda5762a186ee --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/overlap_scheduling.py @@ -0,0 +1,1869 @@ +import functools +import heapq +import itertools +import logging +import sys +from collections import Counter, defaultdict +from collections.abc import Callable, Iterable +from dataclasses import dataclass, field +from typing import Any, Literal + +import torch +import torch.fx as fx +from torch._dynamo.utils import counters, dynamo_timed +from torch._inductor import config +from torch._inductor.comm_analysis import estimate_fx_collective_memory_footprint +from torch._inductor.fx_passes.bucketing import ( + _default_bucket_mode, + _get_collective_node_from_wait, + _schedulable_wait_node, + bucket_key, + BucketMode, + get_full_bucket_key, + is_wait_tensor, +) +from torch._inductor.fx_passes.memory_estimator import MemoryTracker +from torch._logging import trace_structured +from torch.fx.operator_schemas import normalize_function +from torch.utils._ordered_set import OrderedSet +from torch.utils._python_dispatch import _disable_current_modes + + +log = logging.getLogger(__name__) + +from torch._inductor.pattern_matcher import stable_topological_sort + + +def make_all_device_put_sync(gm: torch.fx.GraphModule) -> int: + """ + Convert all non_blocking=True device_put operations to non_blocking=False. + + Only performs the conversion if at least one non_blocking=True device_put + exists in the graph. + + Returns: + The number of device_put operations converted to sync. + """ + g = gm.graph + device_put_nodes = list( + g.find_nodes(op="call_function", target=torch.ops.prims.device_put.default) + ) + + # Check if any non_blocking=True device_put exists + has_async_device_put = False + for n in device_put_nodes: + opt_args_kwargs = normalize_function( + n.target, + args=n.args, + kwargs=n.kwargs, + normalize_to_only_use_kwargs=True, + ) + if opt_args_kwargs is not None: + _, kwargs = opt_args_kwargs + if kwargs.get("non_blocking", False): + has_async_device_put = True + break + + if not has_async_device_put: + return 0 + + # Convert all non_blocking=True to non_blocking=False + count = 0 + for n in device_put_nodes: + opt_args_kwargs = normalize_function( + n.target, + args=n.args, + kwargs=n.kwargs, + normalize_to_only_use_kwargs=True, + ) + if opt_args_kwargs is not None: + _, kwargs = opt_args_kwargs + if kwargs.get("non_blocking", False): + kwargs["non_blocking"] = False + n.args = n.args[0], kwargs["device"], kwargs["non_blocking"] + n.kwargs = {} + count += 1 + + return count + + +@dataclass +class WhyNoOverlap: + """Track reasons why a collective cannot overlap with compute.""" + + compute_name: str + collective_name: str + + def __init__(self, compute_node: fx.Node, collective_node: fx.Node) -> None: + self.compute_name = compute_node.name + self.collective_name = collective_node.name + + def __call__(self, reason: str, *args: Any) -> None: + if log.isEnabledFor(logging.DEBUG): + log.debug( + "cannot overlap %s with %s: " + reason, # noqa: G003 + self.collective_name, + self.compute_name, + *args, + ) + + +def get_group_name(n: fx.Node) -> str: + """Extract the group name from a collective operation node.""" + opt_args_kwargs = normalize_function( + n.target, # type: ignore[arg-type] + args=n.args, + kwargs=n.kwargs, + normalize_to_only_use_kwargs=True, + ) + assert opt_args_kwargs is not None + _, kwargs = opt_args_kwargs + return kwargs["group_name"] + + +def get_custom_estimation( + n: fx.Node, + custom_runtime_estimation: Callable[[fx.Node, int | None], float | None] + | None = None, + override_size: int | None = None, +) -> float | None: + if custom_runtime_estimation is None: + return None + + return custom_runtime_estimation(n, override_size) + + +def estimate_collective_time( + n: fx.Node, + override_size: int | None = None, + custom_runtime_estimation: Callable[[fx.Node, int | None], float | None] + | None = None, + collective_estimator: Literal["analytical", "benchmark"] = "analytical", +) -> float: + """Estimate the runtime of a collective operation, optionally with an overridden size.""" + if ( + est := get_custom_estimation(n, custom_runtime_estimation, override_size) + ) is not None: + return est + + if collective_estimator == "benchmark": + from torch._inductor.fx_passes.node_runtime_estimation import ( + benchmark_collective_with_cuda_events, + ) + + cuda_val, _ = benchmark_collective_with_cuda_events(n, nruns=5) + if cuda_val is not None: + return cuda_val + + # Analytical model (also fallback when benchmark returns None) + return torch._inductor.comm_analysis.estimate_nccl_collective_runtime_from_fx_node( + n, override_size + ) + + +def is_compute_node(n: fx.Node) -> bool: + """ + Should we consider this node computationally expensive ? + Currently uses flop registration, but we could expand more generally. + """ + return ( + getattr(n.target, "overloadpacket", None) + in torch.utils.flop_counter.flop_registry + ) + + +def estimate_roofline_runtime_ms(node: fx.Node) -> float: + """Estimate runtime using roofline model (max of compute and memory bound). + + Uses FLOPs for compute-bound estimate if op is in flop_registry, + and memory bandwidth for memory-bound estimate. + Returns 0 for view nodes (no cost). + """ + from torch._inductor.fx_passes.fusion_regions import is_view_node + from torch.utils._pytree import tree_flatten, tree_map + from torch.utils._runtime_estimation import get_compute_time, get_transfer_time + from torch.utils.flop_counter import flop_registry + + if is_view_node(node): + return 0.0 + + def _get_val(n: Any) -> Any: + if isinstance(n, fx.Node): + return n.meta.get("val") + return n + + args = tree_map(_get_val, node.args) + kwargs = tree_map(_get_val, node.kwargs) + out = _get_val(node) + + if out is None: + return 0.0 + + flat_args_kwargs, _ = tree_flatten((args, kwargs)) + flat_outs, _ = tree_flatten(out) + out_dtypes = OrderedSet([t.dtype for t in flat_outs if isinstance(t, torch.Tensor)]) + + # Compute time (FLOPs-based, only if op is in flop_registry) + # May return SymFloat if shapes are symbolic (after flop division) + compute_ns: float = 0.0 + func_packet = getattr(node.target, "overloadpacket", None) + if func_packet in flop_registry and len(out_dtypes) == 1: + compute_ns = get_compute_time(func_packet, args, kwargs, out, out_dtypes.copy()) + # Extract hint from symbolic value if needed + if isinstance(compute_ns, (torch.SymInt, torch.SymFloat)): + compute_ns = compute_ns.node.hint if compute_ns.node.has_hint() else 0.0 + + # Transfer time (memory bandwidth-based, uses size_hint internally) + transfer_ns = get_transfer_time(flat_args_kwargs, flat_outs) + + # Roofline: max of compute and transfer, convert ns to ms + return max(float(compute_ns), float(transfer_ns)) / 1e6 + + +def get_hint(x: int | torch.SymInt) -> int | None: + if isinstance(x, int): + return x + assert isinstance(x, torch.SymInt) + if not x.node.has_hint(): + return None + return x.node.hint + + +def get_collective_do_bench() -> Callable[[Callable[[], Any]], float]: + with dynamo_timed("collective_compute_do_bench"): + return functools.partial( + # pyrefly: ignore [bad-argument-type] + torch._inductor.runtime.benchmarking.benchmarker.benchmark_gpu, + warmup=5, + ) + + +def benchmark_node_with_cache_key( + n: fx.Node, + custom_runtime_estimation: Callable[[fx.Node, int | None], float | None] + | None = None, +) -> tuple[float, str | None]: + """Benchmark a compute node and return (runtime, cache_key).""" + assert is_compute_node(n) + + from torch._dynamo.testing import rand_strided + + # todo - skip unbacked, symbolic + success, args, kwargs = torch._inductor.fx_utils.get_fake_args_kwargs(n) + + if not success: + return 0, None + + unbacked_tensor = False + + key = f"{str(n.target)}: " + + def to_real(t: torch.Tensor) -> torch.Tensor | None: + shape = [get_hint(dim) for dim in t.shape] + stride = [get_hint(s) for s in t.stride()] + + if any(s is None for s in itertools.chain(shape, stride)): + nonlocal unbacked_tensor + unbacked_tensor = True + return None + + nonlocal key + key += f"T: {shape, stride, t.dtype} " + return rand_strided(shape, stride, device=t.device, dtype=t.dtype) # type: ignore[arg-type] + + with _disable_current_modes(): + args, kwargs = torch.utils._pytree.tree_map_only( + torch.Tensor, + lambda t: to_real(t), + (args, kwargs), + ) + + if val := get_cached_node_time(key): + return val, key + + if unbacked_tensor: + return 0, key + + if ( + est := get_custom_estimation(n, custom_runtime_estimation, None) + ) is not None: + set_cached_node_time(key, est) + return est, key + + bench = get_collective_do_bench() + out = bench(lambda: n.target(*args, **kwargs)) # type: ignore[operator] + set_cached_node_time(key, out) + return out, key + + +def benchmark_node( + n: fx.Node, + custom_runtime_estimation: Callable[[fx.Node, int | None], float | None] + | None = None, +) -> float: + return benchmark_node_with_cache_key(n, custom_runtime_estimation)[0] + + +@functools.cache +def get_benchmark_cache() -> torch._inductor.codecache.LocalCache: + return torch._inductor.codecache.LocalCache() + + +def get_cached_node_time(key: str) -> float: + return get_benchmark_cache().lookup(key) # type: ignore[return-value] + + +def set_cached_node_time(key: str, value: float) -> None: + return get_benchmark_cache().set_value(key, value=value) + + +@dataclass +class CollectiveInfo: + """Track info about a collective operation""" + + start_node: fx.Node + wait_node: fx.Node + size_bytes: int + estimated_time_ms: float + exposed_time_ms: float # How much of this collective is still exposed + hiding_nodes: OrderedSet[fx.Node] = field(default_factory=OrderedSet) + + @property + def is_exposed(self) -> bool: + return self.exposed_time_ms != 0 + + +@dataclass +class CollBucket: + """Track information about a bucket of collectives.""" + + collectives: list[fx.Node] # Original collective starts + bucketed_start: fx.Node | None = None # After bucketing + bucketed_wait: fx.Node | None = None # After bucketing + total_bytes: int = 0 + + +def gb_to_bytes(gb: float) -> int: + """Convert gigabytes to bytes.""" + return int(gb * 1024 * 1024 * 1024) + + +class OverlapScheduler: + """ + Scheduler that reorders operations to maximize compute-collective overlap. + + The reordering is done as a scheduling pass. We maintain a priority queue of + schedulable nodes. The nodes are ranked by: + + 1) the compute node index they dominate. this allows reordering locally, such as with + parallel mms, and also allows overlapping reduce scatter nodes outputs in the backward + with compute by deferring their waits. + + 2) whether the current node is a collective or wait that is currently exposed but has a compute + node which it could be overlapped with. + + 3) original order in the graph for stability. + + When we schedule compute nodes, we first overlap exposed in-flight collectives, then look for unscheduled + collectives that can be scheduled concurrently. + + TODO: + - experiment with other priority scores / allow other mechanisms of reorder / more strict adherence to original graph + - memory limit for deferred scheduling of reduce_scatter nodes. + """ + + def __init__( + self, + gm: torch.fx.GraphModule, + max_in_flight_gb: float, + max_compute_pre_fetch: int, + collective_bucketing: bool, + insert_overlap_deps: bool, + compute_overlap_multipler: float, + max_coll_distance: int, + custom_runtime_estimation: Callable[[fx.Node, int | None], float | None] + | None = None, + collective_estimator: Literal["analytical", "benchmark"] = "analytical", + compute_estimator: Literal["analytical", "benchmark"] = "benchmark", + max_memory_increase_gb: float | None = 1.0, + max_memory_increase_ratio: float | None = 0.05, + log_final_collectives_estimations: bool = False, + bucket_exposed_first: bool | None = None, + enable_fusion_regions: bool = False, + bucket_only_internode_comms: bool = False, + bucket_mode: BucketMode | None = None, + max_off_bucket_gb: float | None = 0.5, + prioritize_bucketing_during_scheduling: bool = True, + ): + self.gm = gm + self.graph = gm.graph + self.compute_overlap_multipler = compute_overlap_multipler + self.max_node_distance = max_coll_distance + self.max_in_flight_bytes: int = gb_to_bytes(max_in_flight_gb) + self.custom_runtime_estimation = custom_runtime_estimation + self.collective_bucketing = collective_bucketing + self.insert_overlap_deps = insert_overlap_deps + self.max_compute_pre_fetch = max_compute_pre_fetch + # In deterministic mode, force analytical estimation to avoid GPU sync + if config.deterministic: + self.collective_estimator = "analytical" + self.compute_estimator = "analytical" + else: + self.collective_estimator = collective_estimator + self.compute_estimator = compute_estimator + self.log_final_collectives_estimations = log_final_collectives_estimations + self.bucket_exposed_first = bucket_exposed_first + self.bucket_only_internode_comms = bucket_only_internode_comms + self.bucket_mode = bucket_mode or _default_bucket_mode() + self.max_off_bucket_bytes: int | None = ( + gb_to_bytes(max_off_bucket_gb) if max_off_bucket_gb is not None else None + ) + self.prioritize_bucketing_during_scheduling = ( + prioritize_bucketing_during_scheduling + ) + + # Make all to(device) non_blocking=False, + # They can be implicitly depending by user logic on other to(device) non_blocking=True. + # OverlapScheduler can put reads of non_blocking device_put before blocking one. + # This results in dirty reads. + num_device_put_converted = make_all_device_put_sync(gm) + if num_device_put_converted > 0: + log.warning( + "overlap_scheduling converted %d device_put operations from " + "non_blocking=True to non_blocking=False. This may affect performance.", + num_device_put_converted, + ) + + # Build fusion regions (mutates gm.graph) and compute initial node runtime + # estimates. Compute nodes use roofline model here; the alignment step in + # run() replaces them with benchmarked + cross-rank-aligned values. + self.node_estimations, self.region_of = gather_node_runtime_estimations( + gm, + custom_runtime_estimation, + enable_fusion_regions=enable_fusion_regions, + log_estimations=True, + ) + if self.region_of: + # fuse_by_partitions replaces gm.graph, so we need to update our reference + self.graph = gm.graph + + # Build structures + stable_topological_sort(self.graph) + self.nodes = list(self.graph.nodes) + self.node_idx = {n: i for i, n in enumerate(self.nodes)} + self.node_ancestors: dict[fx.Node, OrderedSet[fx.Node]] = ( + self._collect_node_ancestors() + ) + + # Identify collectives and compute nodes + self.collective_info: dict[fx.Node, CollectiveInfo] = {} + self.unscheduled_collectives: OrderedSet[fx.Node] = OrderedSet() + + # Identify compute nodes early (needed for baseline memory computation) + self.compute_nodes = [n for n in self.nodes if is_compute_node(n)] + self.current_compute_index = 0 + + # Compute baseline memory profile from original schedule + self.original_mem_before_compute_index: list[int] = [] + self.original_peak_memory = self._compute_baseline_memory() + + # Maximum allowed peak memory = baseline + max(absolute, ratio * baseline) + # When both limits are specified, use the more permissive one + memory_increase_bytes = None + if max_memory_increase_gb is not None: + memory_increase_bytes = gb_to_bytes(max_memory_increase_gb) + if max_memory_increase_ratio is not None: + ratio_increase = int(self.original_peak_memory * max_memory_increase_ratio) + memory_increase_bytes = ( + max(memory_increase_bytes, ratio_increase) + if memory_increase_bytes is not None + else ratio_increase + ) + if memory_increase_bytes is None: + memory_increase_bytes = 0 + + self.allowed_peak_memory_bytes = ( + self.original_peak_memory + memory_increase_bytes + ) + + # Track cumulative prefetch memory at each compute index + # When we prefetch a collective at compute index i that will be used at index j, + # it adds memory from i to j, so we need to track this cumulative effect + self.cumulative_prefetch_mem_by_compute_index: list[int] = [ + 0 for _ in range(len(self.compute_nodes)) + ] + + self.memory_tracker = MemoryTracker(self.graph) + + self.wait_to_start: dict[fx.Node, fx.Node] = {} + self._identify_collectives() + self.wasted_compute = 0.0 + + # Calculate domination indices for both compute and reduce_scatter nodes + self.reduce_scatter_nodes = self.graph.find_nodes( + op="call_function", + target=torch.ops._c10d_functional.reduce_scatter_tensor.default, + ) + self.compute_index_domination = self._calculate_domination_index( + self.compute_nodes + ) + self.reduce_scatter_domination = self._calculate_domination_index( + self.reduce_scatter_nodes + ) + + # Scheduling state + self.potentially_hidden_collectives = ( + self.compute_potential_hidden_collectives() + ) + self.potentially_hidden_waits = self.compute_potential_hidden_waits() + self.in_degree = Counter(user for node in self.nodes for user in node.users) + + # Two separate queues: on-path (domination-based) and off-path (node_idx-based) + self.on_path_ready: list[tuple[object, fx.Node]] = [] + self.off_path_ready: list[tuple[object, fx.Node]] = [] + # Track potential bucket sizes for off-path collectives (for batch scheduling) + self.off_path_ready_potential_buckets: dict[object, int] = defaultdict(int) + + for node in self.nodes: + if self.in_degree[node] == 0: + self._add_to_ready_queue(node) + + self.in_flight: dict[fx.Node, CollectiveInfo] = {} # start -> info + self.in_flight_bytes = 0 + self.scheduled: OrderedSet[fx.Node] = OrderedSet() + self.max_compute_pre_fetch = max_compute_pre_fetch + + self.last_on_path_node_idx = -1 + + def _add_to_ready_queue(self, node: fx.Node) -> None: + if self.off_compute_path(node): + score = self._compute_off_path_score(node) + heapq.heappush(self.off_path_ready, (score, node)) + # Track potential bucket sizes for off-path ready collectives + if node in self.collective_info: + bucket_key = get_full_bucket_key(node, self.bucket_mode) + node_bytes = self.collective_info[node].size_bytes + self.off_path_ready_potential_buckets[bucket_key] += node_bytes + else: + score = self._compute_on_path_score(node) + heapq.heappush(self.on_path_ready, (score, node)) + + def _collect_node_ancestors(self) -> dict[fx.Node, OrderedSet[fx.Node]]: + """Collect all ancestors for each node.""" + ancestors: dict[fx.Node, OrderedSet[fx.Node]] = defaultdict(OrderedSet) + for node in self.nodes: + for input_node in node.all_input_nodes: + ancestors[node].add(input_node) + ancestors[node] |= ancestors[input_node] + + return ancestors + + def _compute_baseline_memory(self) -> int: + """ + Simulate the original schedule to compute baseline memory profile. + Returns the peak memory observed during simulation. + """ + baseline_tracker = MemoryTracker(self.graph) + + last_compute_max_memory = 0 + peak_memory = 0 + + for node in self.nodes: + baseline_tracker.schedule_node(node) + current_mem = baseline_tracker.current_memory_bytes + + # Record the max memory between this and previous compute node + last_compute_max_memory = max(last_compute_max_memory, current_mem) + + if is_compute_node(node): + self.original_mem_before_compute_index.append(last_compute_max_memory) + last_compute_max_memory = current_mem + + peak_memory = max(peak_memory, current_mem) + + return peak_memory + + def _prefetch_would_exceed_memory_budget(self, start_node: fx.Node) -> bool: + """ + Check if prefetching this collective would exceed memory budget at ANY compute node + between now and when it's used. + """ + info = self.collective_info[start_node] + size = info.size_bytes + + domination_index = self.compute_index_domination[start_node] + + # If off-path, assume it doesn't increase memory + if domination_index == sys.maxsize: + return False + + # check current mem + if ( + self.memory_tracker.current_memory_bytes + size + > self.allowed_peak_memory_bytes + ): + return True + + start_index = self.current_compute_index + + # then, check future mem + for compute_idx in range(start_index, domination_index): + cumulative_prefetch = self.cumulative_prefetch_mem_by_compute_index[ + compute_idx + ] + + # Check 1: Would cumulative prefetch exceed in-flight limit? + if (cumulative_prefetch + size) > self.max_in_flight_bytes: + return True + + # Check 2: Would total memory (baseline + cumulative prefetch) exceed budget? + baseline_mem = self.original_mem_before_compute_index[compute_idx] + projected = baseline_mem + cumulative_prefetch + size + + if projected > self.allowed_peak_memory_bytes: + return True + + return False + + def _update_cumulative_prefetch_memory( + self, collective: fx.Node, info: CollectiveInfo + ) -> None: + """ + Update cumulative prefetch memory for all compute indices this collective will be live. + """ + domination_index = self.compute_index_domination[collective] + if domination_index == sys.maxsize: + return + + for compute_idx in range(self.current_compute_index, domination_index): + self.cumulative_prefetch_mem_by_compute_index[compute_idx] += ( + info.size_bytes + ) + + def off_compute_path(self, n: fx.Node) -> bool: + """Check if a node is off the compute path (doesn't block any compute).""" + return self.compute_index_domination[n] == sys.maxsize + + def dominates_reduce_scatter(self, n: fx.Node) -> bool: + """Check if a node dominates (blocks) any reduce_scatter.""" + return self.reduce_scatter_domination[n] != sys.maxsize + + def _identify_collectives(self) -> None: + """Identify all collective operations and process groups.""" + self.all_pgs: OrderedSet[str] = OrderedSet() + + for node in self.nodes: + if _schedulable_wait_node(node): + start = _get_collective_node_from_wait(node) + assert start is not None + assert start in self.node_estimations, ( + f"Missing estimation for collective {start.name}. " + f"Ensure custom_runtime_estimation returns a value for this node." + ) + self.wait_to_start[node] = start + # For coalesced collectives, multiple waits share the same + # start node. Only register the first wait as the representative. + if start in self.collective_info: + continue + coll_time_ms = self.node_estimations[start] + + info = CollectiveInfo( + start_node=start, + wait_node=node, + size_bytes=estimate_fx_collective_memory_footprint(start), + estimated_time_ms=coll_time_ms, + exposed_time_ms=coll_time_ms, # Initially fully exposed + ) + self.collective_info[start] = info + self.unscheduled_collectives.add(start) + self.all_pgs.add(get_group_name(start)) + + def _calculate_domination_index( + self, target_nodes: list[fx.Node] + ) -> dict[fx.Node, int]: + """ + Calculate the topological index of the earliest target node each node dominates. + + target_nodes are assigned indices based on their topological order (0, 1, 2, ...). + For each node, returns the minimum index of target nodes it blocks/dominates. + Returns sys.maxsize if the node doesn't block any target nodes. + """ + target_node_index: dict[fx.Node, int] = {} + for node in self.graph.nodes: + if node in target_nodes: + target_node_index[node] = len(target_node_index) + + domination_index: dict[fx.Node, int] = {} + for node in reversed(self.graph.nodes): + if node in target_node_index: + domination_index[node] = target_node_index[node] + else: + domination_index[node] = min( + (domination_index[succ] for succ in node.users), default=sys.maxsize + ) + + return domination_index + + def _align_compute_nodes_runtime_estimations_across_all_distributed_ranks( + self, + ) -> None: + """Align runtime estimations across ranks (compute + collectives).""" + log.info( + "Overlap scheduling: Aligning runtime estimations across all distributed ranks" + ) + + # Benchmark compute nodes + runtime_estimations_keys: list[str | None] = [] + runtime_estimations: list[float] = [] + compute_key_count = 0 + + # Also collect analytical estimations for logging + runtime_estimations_analytical: list[float] = [] + + for n in self.compute_nodes: + # Compute analytical estimation using roofline model + val_analytical = estimate_roofline_runtime_ms(n) + runtime_estimations_analytical.append(val_analytical) + + if self.compute_estimator == "benchmark": + val, key = benchmark_node_with_cache_key( + n, self.custom_runtime_estimation + ) + else: + # Use analytical estimation + val, key = val_analytical, None + + runtime_estimations.append(val) + runtime_estimations_keys.append(key) + compute_key_count += 1 + + # Log compute estimations + from torch._inductor.fx_passes.node_runtime_estimation import ( + _log_compute_estimations, + ) + + _log_compute_estimations( + self.compute_nodes, + runtime_estimations, + runtime_estimations_analytical, + ) + + # Benchmark collectives if enabled (only CUDA events - others are deterministic) + # Skip if custom estimation is provided for collectives + collective_nodes: list[fx.Node] = [] + benchmarked_collective_nodes: list[ + fx.Node + ] = [] # Track which were actually benchmarked + if self.collective_estimator == "benchmark": + from torch._inductor.fx_passes.node_runtime_estimation import ( + benchmark_collective_with_cuda_events, + ) + + collective_nodes = [ + info.start_node for info in self.collective_info.values() + ] + + # Benchmark CUDA events (non-deterministic, needs alignment) + # Skip collectives with custom estimation + for n in collective_nodes: + if ( + get_custom_estimation(n, self.custom_runtime_estimation, None) + is not None + ): + continue + + # Benchmark actual size + cuda_val, cuda_key = benchmark_collective_with_cuda_events(n, nruns=5) + if cuda_val is not None: + runtime_estimations.append(cuda_val) + runtime_estimations_keys.append(cuda_key) + benchmarked_collective_nodes.append(n) + + # When both estimators are analytical, estimates are deterministic across ranks + # (same shapes = same estimates), so skip the all_gather to avoid sync. + import torch.distributed as dist + + world_size = dist.get_world_size() + + if ( + self.compute_estimator == "analytical" + and self.collective_estimator == "analytical" + ): + median_runtime_estimations = runtime_estimations + else: + # Single all_gather and compute medians + from torch._subclasses.fake_tensor import unset_fake_temporarily + from torch.distributed.distributed_c10d import _get_default_group + + pg = _get_default_group() + with unset_fake_temporarily(): + gathered_runtime_estimations: list[list[float]] = [ + [] for _ in range(world_size) + ] + dist.all_gather_object( + gathered_runtime_estimations, runtime_estimations, pg + ) + median_runtime_estimations = torch.median( + torch.tensor(gathered_runtime_estimations), dim=0 + ).values.tolist() + + # Cache medians + collective_keys = [] + collective_medians = [] + for idx, (key, median_runtime_estimation) in enumerate( + zip(runtime_estimations_keys, median_runtime_estimations) + ): + if key is None: + continue + if idx < compute_key_count: + # Compute node + self.node_estimations[self.compute_nodes[idx]] = ( + median_runtime_estimation + ) + set_cached_node_time(key, median_runtime_estimation) + else: + # Collective CUDA event benchmark + from torch._inductor.fx_passes.node_runtime_estimation import ( + set_cached_runtime, + ) + + set_cached_runtime(key, median_runtime_estimation) + + # Update CollectiveInfo with aligned benchmark + coll_idx = idx - compute_key_count + coll_node = benchmarked_collective_nodes[coll_idx] + info = self.collective_info[coll_node] + info.estimated_time_ms = median_runtime_estimation + info.exposed_time_ms = median_runtime_estimation + self.node_estimations[coll_node] = median_runtime_estimation + + collective_keys.append(key) + collective_medians.append(median_runtime_estimation) + + # Log benchmarks with analytical comparisons + if collective_keys: + from torch._inductor.fx_passes.node_runtime_estimation import ( + _log_collective_benchmarks, + ) + + _log_collective_benchmarks( + benchmarked_collective_nodes, + collective_keys, + collective_medians, + world_size, + "fx_collectives_node_runtime_estimation", + ) + else: + # No benchmarking - log analytical estimations for all collectives + from torch._inductor.fx_passes.node_runtime_estimation import ( + _log_collective_benchmarks, + ) + + all_collective_nodes = [ + info.start_node for info in self.collective_info.values() + ] + if all_collective_nodes: + _log_collective_benchmarks( + all_collective_nodes, + artifact_name="fx_collectives_analytical_estimation", + ) + + log.info("Overlap scheduling: Runtime estimations aligned") + + def _get_next_nodes(self) -> list[fx.Node]: + """ + Get next node(s) to schedule. + + When max_off_bucket_bytes is set, off-path collectives of the same type + (same bucket_key) are batched together to enable bucketing them in + overlap_preserving_bucketer. Bucket size is limited by max_off_bucket_bytes. + """ + if self.off_path_ready: + _, node = self.off_path_ready[0] + + should_schedule = False + if not self.on_path_ready or node in self.scheduled: + should_schedule = True + elif _schedulable_wait_node(node): + # Defer exposed waits until hidden or over memory budget + info = self.collective_info[self.wait_to_start[node]] + over_budget = ( + self.memory_tracker.current_memory_bytes + > self.allowed_peak_memory_bytes + ) + should_schedule = not info.is_exposed or over_budget + elif self.max_off_bucket_bytes is not None and node in self.collective_info: + # Batch off-path collectives: schedule when bucket threshold is reached + bucket_key = get_full_bucket_key(node, self.bucket_mode) + bucket_size = self.off_path_ready_potential_buckets[bucket_key] + should_schedule = bucket_size >= self.max_off_bucket_bytes + elif self.dominates_reduce_scatter(node): + # Only schedule off-path nodes that dominate reduce_scatters after original position + should_schedule = self.node_idx[node] <= self.last_on_path_node_idx + + if should_schedule: + heapq.heappop(self.off_path_ready) + + # If batching enabled and this is a collective, gather same-type collectives + if ( + self.max_off_bucket_bytes is not None + and node in self.collective_info + ): + node_key = get_full_bucket_key(node, self.bucket_mode) + if node_key is not None: + same_type_nodes = [node] + total_bytes = self.collective_info[node].size_bytes + indices_to_remove = [] + + # Scan the off_path_ready queue for same-key collectives + for i, (_, candidate) in enumerate(self.off_path_ready): + if candidate in self.scheduled: + continue + if candidate not in self.collective_info: + continue + candidate_key = get_full_bucket_key( + candidate, self.bucket_mode + ) + if candidate_key == node_key: + candidate_bytes = self.collective_info[ + candidate + ].size_bytes + # Check bucket size limit before adding + if ( + total_bytes + candidate_bytes + > self.max_off_bucket_bytes + ): + continue # Skip but keep looking for smaller ones + same_type_nodes.append(candidate) + total_bytes += candidate_bytes + indices_to_remove.append(i) + + # Remove collected nodes from heap (reverse order to preserve indices) + for i in reversed(indices_to_remove): + self.off_path_ready.pop(i) + if indices_to_remove: + heapq.heapify(self.off_path_ready) + + return same_type_nodes + + return [node] + + return [heapq.heappop(self.on_path_ready)[1]] + + def run(self) -> torch.fx.GraphModule: + """Run the scheduling algorithm.""" + # All ranks must make identical decisions on overlap reordering, + # Thus we must have identical runtime estimations across ranks. + # For now we do benchmarking only for compute nodes. + self._align_compute_nodes_runtime_estimations_across_all_distributed_ranks() + + while self.on_path_ready or self.off_path_ready: + if self._should_force_wait_for_memory(): + self._force_oldest_wait() + continue + + nodes = self._get_next_nodes() + + for node in nodes: + # we don't always remove nodes from the heap when we schedule them + if node in self.scheduled: + continue + + if node.op == "placeholder": + self._schedule(node) + elif node in self.collective_info: + self._handle_collective_start(node) + elif _schedulable_wait_node(node): + self._handle_wait(node) + else: + self._handle_compute_or_other(node) + + # Track progress for off-path scheduling - only for nodes from main queue + if not self.off_compute_path(node): + self.last_on_path_node_idx = max( + self.last_on_path_node_idx, self.node_idx[node] + ) + else: + # Decrement off-path bucket bytes when scheduling + if node in self.collective_info: + bucket_key = get_full_bucket_key(node, self.bucket_mode) + node_bytes = self.collective_info[node].size_bytes + self.off_path_ready_potential_buckets[bucket_key] -= node_bytes + + self._reorder_graph() + + # Finalize: bucket collectives (if enabled), inline fusions, apply deps + from torch._inductor.fx_passes.overlap_preserving_bucketer import ( + finalize_overlap_scheduling, + ) + + finalize_overlap_scheduling( + gm=self.gm, + collective_info=self.collective_info, + scheduled=self.scheduled, + collective_bucketing=self.collective_bucketing, + insert_overlap_deps=self.insert_overlap_deps, + max_bucket_memory_gb=2.0, + max_coll_distance=self.max_node_distance, + region_of=self.region_of, + bucket_exposed_first=self.bucket_exposed_first, + bucket_only_internode_comms=self.bucket_only_internode_comms, + bucket_mode=self.bucket_mode, + ) + + if self.log_final_collectives_estimations: + from torch._inductor.fx_passes.node_runtime_estimation import ( + _log_graph_collective_benchmarks, + ) + + _log_graph_collective_benchmarks( + self.gm, "fx_collectives_estimations_after_overlap_bucketing" + ) + + return self.gm + + def _reduce_exposed_time_of_in_flight_collectives( + self, + node: fx.Node, + available_compute: float, + exclude_pg: str | None = None, + ) -> dict[str, float]: + """ + Reduce exposed time of in-flight collectives using available compute time. + + Collectives on different process groups can overlap simultaneously with the same + compute, so we track remaining time separately per PG. + """ + # Initialize all PGs with full available compute (except excluded) + remaining_time_per_pg: dict[str, float] = { + pg: available_compute for pg in self.all_pgs if pg != exclude_pg + } + + for start_node, info in self.in_flight.items(): + if info.exposed_time_ms == 0: + continue + + pg_name = get_group_name(start_node) + if pg_name == exclude_pg: + continue + + pg_remaining = remaining_time_per_pg[pg_name] + if pg_remaining <= 0: + continue + + overlap_amount = min(info.exposed_time_ms, pg_remaining) + info.exposed_time_ms -= overlap_amount + remaining_time_per_pg[pg_name] -= overlap_amount + info.hiding_nodes.add(node) + + return remaining_time_per_pg + + def _handle_compute_or_other(self, node: fx.Node) -> None: + """Handle scheduling compute or other nodes and attempt to overlap with collectives.""" + runtime_estimate = self.node_estimations.get(node) + + # TODO: we could consider skipping overlapping for overlapable, unary chains to collectives. + # using these nodes for overlap prevents bucketing. potentially if chain time < latency + if runtime_estimate is None: + assert not is_compute_node(node), "should have estimate for compute nodes" + self._schedule(node) + return + + available_compute = runtime_estimate * self.compute_overlap_multipler + + # First, reduce exposed time of in-flight collectives (per PG) + remaining_time_per_pg = self._reduce_exposed_time_of_in_flight_collectives( + node, available_compute + ) + # Then, schedule new collectives for overlap + self._schedule_collectives_for_overlap(node, remaining_time_per_pg) + self._schedule(node) + + if is_compute_node(node): + self.current_compute_index += 1 + + def _schedule(self, node: fx.Node) -> None: + """Schedule a node.""" + assert node not in self.scheduled + assert all(n in self.scheduled for n in node.all_input_nodes) + self.scheduled.add(node) + self.memory_tracker.schedule_node(node) + + log.debug( + "Scheduled node %s: current_memory=%d bytes, total_scheduled=%d", + node.name, + self.memory_tracker.get_current_memory_bytes(), + len(self.scheduled), + ) + + for user in node.users: + self.in_degree[user] -= 1 + if self.in_degree[user] == 0: + self._add_to_ready_queue(user) + + def _compute_on_path_score(self, node: fx.Node) -> object: + """Compute priority score for on-path nodes (domination-based).""" + if _schedulable_wait_node(node): + info = self.collective_info[self.wait_to_start[node]] + # defer waits locally if they are exposed. + compute_local_priority = int(info.is_exposed) + else: + # if we're scheduling this collective via its queue, then it was not + # pre-fetched. we might as well maximize overlap for the + # local, non-mm nodes prior to the next compute node. + if self.in_overlappable_collective_unary_chain(node): + compute_local_priority = -1 + else: + compute_local_priority = 0 + + return ( + self.compute_index_domination[node], # what index compute it blocks + compute_local_priority, # collective_start=-1, wait=1, or neither=0 + self.node_idx[node], # Original order for stability + ) + + def _compute_off_path_score(self, node: fx.Node) -> object: + """ + Off-path priority scoring. + + Nodes that dominate reduce_scatters are prioritized (lower score = higher priority) + to ensure they get scheduled eagerly for potential overlap. + """ + dominates_rs = 0 if self.dominates_reduce_scatter(node) else 1 + return (dominates_rs, self.node_idx[node]) + + @staticmethod + def is_cheap_fn(node: fx.Node) -> bool: + return getattr(node.target, "is_view", False) or torch.Tag.pointwise in getattr( + node.target, "tags", () + ) + + def in_overlappable_collective_unary_chain(self, curr: fx.Node) -> bool: + while True: + if len(curr.users) != 1: + return False + + user = next(iter(curr.users)) + if len(user.all_input_nodes) != 1: + return False + + if user in self.unscheduled_collectives: + return True + + if not self.is_cheap_fn(user): + return False + + curr = user + + return False + + def _should_force_wait_for_memory(self) -> bool: + """Check if we need to force a wait due to memory pressure""" + if not self.in_flight: + return False + + return self.in_flight_bytes >= self.max_in_flight_bytes + + def _force_oldest_wait(self) -> None: + """Schedule the oldest in flight wait""" + self._handle_wait(self._get_oldest_wait()) + + def _handle_collective_start(self, node: fx.Node) -> None: + """Handle scheduling a collective start.""" + info = self.collective_info[node] + + if self.should_assume_bucketed(node): + latency = estimate_collective_time( + node, 0, custom_runtime_estimation=self.custom_runtime_estimation + ) + assert latency <= info.exposed_time_ms + info.exposed_time_ms = info.exposed_time_ms - latency + + self.in_flight[node] = info + self.in_flight_bytes += info.size_bytes + self.unscheduled_collectives.discard(node) + self._schedule(node) + + def _handle_wait(self, node: fx.Node) -> None: + """Handle scheduling a wait.""" + assert node in self.wait_to_start + coll_start = self.wait_to_start[node] + # For coalesced collectives, multiple waits share the same start node. + # The first wait completes the collective; subsequent waits just schedule. + if coll_start not in self.in_flight: + self._schedule(node) + return + + # Scheduling a wait of a collective also forces the wait + # of every node enqueued prior to the collective on the + # same process group + group_name = get_group_name(coll_start) + to_schedule: list[fx.Node] = [] + for in_flight_coll in self.in_flight: + if in_flight_coll == coll_start: + break + if get_group_name(in_flight_coll) == group_name: + to_schedule.append(in_flight_coll) + + for coll_to_schedule in to_schedule: + self._handle_wait(self.collective_info[coll_to_schedule].wait_node) + + # If we are waiting on an exposed collective, use this time to + # overlap on other PGs. + info = self.collective_info[coll_start] + if info.exposed_time_ms > 0: + exposed_time = info.exposed_time_ms + exclude_pg = group_name + + remaining_time_per_pg = self._reduce_exposed_time_of_in_flight_collectives( + node, exposed_time, exclude_pg=exclude_pg + ) + self._schedule_collectives_for_overlap( + node, remaining_time_per_pg, exclude_pg=exclude_pg + ) + + self.in_flight_bytes -= self.in_flight[coll_start].size_bytes + del self.in_flight[coll_start] + self._schedule(node) + + def _schedule_collectives_for_overlap( + self, + overlap_node: fx.Node, + remaining_time_per_pg: dict[str, float], + exclude_pg: str | None = None, + ) -> None: + """Opportunistically schedule collectives that can be hidden by available overlap time.""" + if not remaining_time_per_pg or all( + t <= 0 for t in remaining_time_per_pg.values() + ): + return + + overlap_node_ancestors = self.node_ancestors[overlap_node] + + # Compile candidates - limit by distance to bound compile time + candidates = [] + for i, collective in enumerate(self.unscheduled_collectives): + if i > self.max_node_distance: + break + + pg_name = get_group_name(collective) + if pg_name == exclude_pg: + continue + + if ( + not self.off_compute_path(collective) + and self.compute_index_domination[collective] + - self.current_compute_index + > self.max_compute_pre_fetch + ): + continue + + candidates.append(collective) + + def get_priority(n: fx.Node) -> int: + dominates_next_compute = ( + self.compute_index_domination[n] == self.current_compute_index + 1 + ) + if dominates_next_compute: + return 0 # Dominates next compute layer - most urgent + elif self.off_compute_path(n) and self.dominates_reduce_scatter(n): + return 1 # Off-path but blocks reduce_scatter + elif not self.off_compute_path(n): + return 2 # On-path but not immediate + else: + return 3 # Off-path, doesn't block reduce_scatter + + candidates.sort( + key=lambda n: ( + get_priority(n), + self.compute_index_domination[n], + self.node_idx[n], + ), + ) + + if self.prioritize_bucketing_during_scheduling: + # group candidates by bucket key first so same-bucket + # collectives are scheduled together, maximizing bucketing opportunities + bucket_groups: dict[object, list[fx.Node]] = defaultdict(list) + for coll in candidates: + key = get_full_bucket_key(coll, self.bucket_mode) + bucket_groups[key].append(coll) + + # Sort bucket groups by minimum domination index, larger groups first as tiebreaker + sorted_bucket_keys = sorted( + bucket_groups.keys(), + key=lambda k: ( + min(self.compute_index_domination[c] for c in bucket_groups[k]), + -len(bucket_groups[k]), + ), + ) + + # Flatten back to ordered candidate list + candidates = [] + for b_key in sorted_bucket_keys: + group = bucket_groups[b_key] + group.sort( + key=lambda n: (self.compute_index_domination[n], self.node_idx[n]) + ) + candidates.extend(group) + + for collective in candidates: + pg_name = get_group_name(collective) + pg_available_time = remaining_time_per_pg[pg_name] + + if pg_available_time <= 0: + continue + + why = WhyNoOverlap(overlap_node, collective) + info = self.collective_info[collective] + + if ( + collective in overlap_node_ancestors + or overlap_node in self.node_ancestors[collective] + ): + why("dependency conflict") + continue + + # Check if prefetching would exceed memory budget + if self._prefetch_would_exceed_memory_budget(collective): + why("prefetch would exceed memory budget") + continue + + # Try to free memory by forcing hidden waits + while ( + self.in_flight + and (self.max_in_flight_bytes - self.in_flight_bytes) < info.size_bytes + and self._wait_is_hidden(self._get_oldest_wait(), overlap_node) + ): + self._force_oldest_wait() + + if (self.max_in_flight_bytes - self.in_flight_bytes) < info.size_bytes: + why("in-flight memory limit") + continue + + # Check if we can reach this collective without scheduling compute, other collectives, or waits + path = self._find_schedulable_path(collective, overlap_node, why) + if path is None: + continue + + log.debug( + "Overlapping collective %s with node %s: coll_domination=%d, current_depth=%d", + collective.name, + overlap_node.name, + self.compute_index_domination[collective], + self.current_compute_index, + ) + + # TODO: We previously tracked path compute time and added it back to available + # overlap time. With per-PG tracking this is complex: if there were in-flight + # collectives on one PG but not another, we can't add path time back to the PG + # that wasn't in-flight + + # Schedule path and collective + self._schedule_path_to_collective(path, overlap_node) + self._handle_collective_start(collective) + self._update_cumulative_prefetch_memory(collective, info) + + # Update exposed time for this collective + overlap_amount = min(pg_available_time, info.exposed_time_ms) + info.exposed_time_ms -= overlap_amount + info.hiding_nodes.add(overlap_node) + + # Update available time for this PG + remaining_time_per_pg[pg_name] -= overlap_amount + + if sum(remaining_time_per_pg.values()) == 0: + break + + if remaining_time_per_pg: + self.wasted_compute += min(remaining_time_per_pg.values()) + + def _find_schedulable_path( + self, target: fx.Node, curr_overlap_node: fx.Node | None, why: WhyNoOverlap + ) -> OrderedSet[fx.Node] | None: + """Find path to target by collecting unscheduled dependencies.""" + # Get unscheduled ancestors + unscheduled_ancestors = self.node_ancestors[target] - self.scheduled + + # only schedule non distributed, non compute nodes + for node in unscheduled_ancestors: + if is_compute_node(node): + why("path blocked by compute node %s", node.name) + return None + + if node in self.unscheduled_collectives: + why("path blocked by unscheduled collective %s", node.name) + return None + + # if we schedule a wait tensor whose start collective is hidden by the + # current compute node we are scheduling, then we are effectively exposing it. + # similarly, dont schedule a wait of a collective that could be otherwise hidden, + # thus forcing it to be exposed. + # however, if it is already hidden it's fine to schedule it + if _schedulable_wait_node(node): + info = self.collective_info[self.wait_to_start[node]] + if (not info.is_exposed) and ( + curr_overlap_node not in info.hiding_nodes + ): + continue + + why( + "path blocked by wait node %s (exposed=%s, hidden_by_curr_overlap=%s)", + node.name, + info.is_exposed, + curr_overlap_node in info.hiding_nodes, + ) + return None + + # Skip c10 ops and dtensor shard ops - they should be scheduled via main loop + target_str = str(node.target) + if "c10" in target_str or "_dtensor" in target_str: + log.debug( + "Skipping c10/dtensor op %s in path to collective", + node.name, + ) + return None + + return unscheduled_ancestors + + def should_assume_bucketed(self, node: fx.Node) -> bool: + """ + Check if there's an in-flight collective that can be bucketed with the given node. If so, assume they will bucket. + This is a optimistic heuristic to account for latency reduction with bucketing. The two nodes may not get bucketed. + """ + if not torch._inductor.config.test_configs.assume_bucketing_reduces_latency: + return False + + key = bucket_key(node, mode=self.bucket_mode) + if key is None: + return False + + for in_flight_coll in self.in_flight: + if bucket_key(in_flight_coll, mode=self.bucket_mode) == key: + return True + + return False + + def _get_oldest_wait(self) -> fx.Node: + oldest_start = next(iter(self.in_flight)) + return self.collective_info[oldest_start].wait_node + + def _wait_is_hidden( + self, wait_node: fx.Node, overlap_node: fx.Node | None = None + ) -> bool: + assert is_wait_tensor(wait_node) + info = self.collective_info[self.wait_to_start[wait_node]] + return not info.is_exposed and overlap_node not in info.hiding_nodes + + def _schedule_path_to_collective( + self, path: OrderedSet[fx.Node], curr_overlap_node: fx.Node + ) -> None: + """Schedule all nodes needed to reach a collective.""" + + assert all(n not in self.scheduled for n in path) + for node in sorted(path, key=lambda n: self.node_idx[n]): + assert not (is_compute_node(node) or node in self.unscheduled_collectives) + if _schedulable_wait_node(node): + # When we schedule wait tensors, we also force realization of all + # collectives enqueued prior to their corresponding collective. + # It's possible the scheduling of one wait tensor here has forced + # another in the path. If so, skip scheduling it. + if node in self.scheduled: + continue + + info = self.collective_info[self.wait_to_start[node]] + assert curr_overlap_node not in info.hiding_nodes + self._handle_wait(node) + continue + + self._schedule(node) + + def reorder_graph(self) -> None: + output_node = self.graph.output_node() + for node in self.scheduled: + if node.op == "placeholder": + continue + output_node.prepend(node) + self.graph.lint() + + def _reorder_graph(self) -> None: + """Reorder graph based on schedule.""" + exposed = [ + c + for c in self.collective_info.values() + if c.exposed_time_ms == c.estimated_time_ms + ] + + potentially_hidden_collectives = self.compute_potential_hidden_collectives() + bad_exposed = [ + c for c in exposed if c.start_node in potentially_hidden_collectives + ] + + # Compute total exposed and potential exposed time + total_exposed = sum(c.exposed_time_ms for c in self.collective_info.values()) + hideable_exposed_ms = sum( + self.collective_info[c].exposed_time_ms + for c in potentially_hidden_collectives + ) + total_potential_exposed = sum( + c.estimated_time_ms for c in self.collective_info.values() + ) + + counters["inductor"]["overlap_scheduling_exposed"] += len(exposed) + counters["inductor"]["overlap_scheduling_bad_exposed"] += len(bad_exposed) + counters["inductor"]["overlap_scheduling_potentially_hidden"] += len( + potentially_hidden_collectives + ) + counters["inductor"]["overlap_original_mem"] = self.original_peak_memory + counters["inductor"]["rescheduled_mem"] = self.memory_tracker.peak_memory + + log.info( + "Overlap scheduling results: exposed=%d, bad_exposed=%d, potentially_hidden=%d, " + "original_peak_memory=%d bytes, rescheduled_peak_memory=%d bytes, " + "total_exposed_ms=%.2f, hideable_exposed_ms=%.2f, total_potential_exposed_ms=%.2f, " + "wasted_compute_ms=%.2f", + len(exposed), + len(bad_exposed), + len(potentially_hidden_collectives), + self.original_peak_memory, + self.memory_tracker.peak_memory, + total_exposed, + hideable_exposed_ms, + total_potential_exposed, + self.wasted_compute, + ) + + self.reorder_graph() + + def _bucket_collectives(self) -> None: + from torch._inductor.fx_passes.overlap_preserving_bucketer import ( + OverlapPreservingBucketer, + ) + + bucketer = OverlapPreservingBucketer( + graph=self.graph, + collective_info=self.collective_info, + scheduled=self.scheduled, + max_bucket_memory_gb=2.0, # Could make this configurable + max_coll_distance=self.max_node_distance, + insert_overlap_deps=self.insert_overlap_deps, + bucket_mode=self.bucket_mode, + bucket_exposed_first=self.bucket_exposed_first, + bucket_only_internode_comms=self.bucket_only_internode_comms, + ) + bucketer.bucket_collectives() + + def compute_potential_hidden_nodes( + self, nodes_to_check: Iterable[fx.Node] + ) -> dict[fx.Node, fx.Node]: + """ + Returns a dict containing a mapping of nodes which could potentially be hidden to their hiding node + """ + + def could_be_hidden(start: fx.Node) -> fx.Node | None: + for compute_node in self.compute_nodes: + if ( + start not in self.node_ancestors[compute_node] + and compute_node not in self.node_ancestors[start] + ): + return compute_node + + return None + + # TODO: We could potentially limit compute nodes per overlap time, + # today, this is optimistic, and just serves to avoid deferring + # collectives/waits that have no possible overlap as well as for analysis of how + # successfully we hid compute + potentially_hidden = {} + for node in nodes_to_check: + if mm := could_be_hidden(node): + potentially_hidden[node] = mm + + return potentially_hidden + + def compute_potential_hidden_collectives(self) -> dict[fx.Node, fx.Node]: + """Compute which collective operations could be hidden by compute.""" + return self.compute_potential_hidden_nodes(self.collective_info.keys()) + + def compute_potential_hidden_waits(self) -> dict[fx.Node, fx.Node]: + """Compute which wait operations could be hidden by compte.""" + wait_nodes = [info.wait_node for info in self.collective_info.values()] + return self.compute_potential_hidden_nodes(wait_nodes) + + +def gather_node_runtime_estimations( + gm: torch.fx.GraphModule, + custom_runtime_estimation: Callable[[fx.Node, int | None], float | None] + | None = None, + collective_estimator: Literal["analytical", "benchmark"] = "analytical", + enable_fusion_regions: bool = False, + log_estimations: bool = False, +) -> tuple[dict[fx.Node, float], dict[fx.Node, Any]]: + """Gather initial runtime estimations for all nodes without scheduling. + + Uses analytical models (roofline) for compute nodes — the alignment step + in OverlapScheduler.run() replaces these with benchmarked + cross-rank-aligned + values. Collectives use bandwidth formulas or CUDA events depending on + collective_estimator. + + When enable_fusion_regions is True, builds and collapses fusion regions + (mutating gm's graph), then includes their costs in the estimations. + + Args: + collective_estimator: "analytical" uses bandwidth formulas, + "benchmark" uses CUDA events for collectives. + log_estimations: When True, log compute and collective estimations + via trace_structured for tlparse. + + Returns (estimations, fusion_region_of) where estimations maps fx.Node to + runtime in ms, and fusion_region_of maps call_module nodes to FusionRegion + objects (empty dict if fusion regions are disabled). + """ + # Build and collapse fusion regions first (mutates gm) + fusion_region_of: dict[fx.Node, Any] = {} + if enable_fusion_regions: + from torch._inductor.fx_passes.fusion_regions import ( + build_fusion_regions, + collapse_fusion_regions, + ) + + fusion_region_of = build_fusion_regions(gm) + if fusion_region_of: + fusion_region_of = collapse_fusion_regions(gm, fusion_region_of) + + estimations: dict[fx.Node, float] = {} + nodes = list(gm.graph.nodes) + + # Collectives + collective_nodes: list[fx.Node] = [] + for node in nodes: + if _schedulable_wait_node(node): + start = _get_collective_node_from_wait(node) + assert start is not None + if start in estimations: + continue + estimations[start] = estimate_collective_time( + start, + custom_runtime_estimation=custom_runtime_estimation, + collective_estimator=collective_estimator, + ) + collective_nodes.append(start) + + # Compute nodes (matmul, bmm, etc.) — analytical estimates only. + # The alignment step in run() replaces these with benchmarked + aligned values. + compute_nodes: list[fx.Node] = [] + compute_analytical: list[float] = [] + + for node in nodes: + if is_compute_node(node): + est = estimate_roofline_runtime_ms(node) + if custom_runtime_estimation is not None: + custom_est = custom_runtime_estimation(node, None) + if custom_est is not None: + est = custom_est + estimations[node] = est + compute_nodes.append(node) + compute_analytical.append(est) + elif node.op == "call_function" and node not in estimations: + if custom_runtime_estimation is not None: + est = custom_runtime_estimation(node, None) + if est is not None: + estimations[node] = est + else: + est = estimate_roofline_runtime_ms(node) + if est > 0: + estimations[node] = est + + # Fusion region costs (call_module nodes from collapse_fusion_regions) + for node, region in fusion_region_of.items(): + estimations[node] = region.cost_ms # pyrefly: ignore[missing-attribute] + + # Logging + if log_estimations and compute_nodes: + from torch._inductor.fx_passes.node_runtime_estimation import ( + _log_compute_estimations, + ) + + _log_compute_estimations( + compute_nodes, + compute_analytical, + compute_analytical, + ) + + if log_estimations and collective_nodes: + from torch._inductor.fx_passes.node_runtime_estimation import ( + _log_collective_benchmarks, + ) + + _log_collective_benchmarks( + collective_nodes, + artifact_name="fx_collectives_analytical_estimation", + ) + + return estimations, fusion_region_of + + +def align_estimations_across_ranks( + estimations: dict[fx.Node, float], +) -> dict[fx.Node, float]: + """Align runtime estimations across distributed ranks using median. + + All ranks must make identical scheduling decisions, so we gather each + rank's values and take the median. All nodes in estimations are aligned. + + Returns a new estimations dict with aligned values. + """ + import torch.distributed as dist + from torch._subclasses.fake_tensor import unset_fake_temporarily + from torch.distributed.distributed_c10d import _get_default_group + + nodes = list(estimations.keys()) + if not nodes: + return {} + + local_values = [estimations[n] for n in nodes] + + world_size = dist.get_world_size() + pg = _get_default_group() + + with unset_fake_temporarily(): + gathered: list[list[float]] = [[] for _ in range(world_size)] + dist.all_gather_object(gathered, local_values, pg) + medians = torch.median(torch.tensor(gathered), dim=0).values.tolist() + + return dict(zip(nodes, medians)) + + +def schedule_overlap_bucketing( + gm: torch.fx.GraphModule, + max_in_flight_gb: float = 5, + max_compute_pre_fetch: int = 200, + collective_bucketing: bool = False, + insert_overlap_deps: bool = False, + compute_overlap_multipler: float = 1.0, + max_coll_distance: int = 200, + custom_runtime_estimation: Callable[[fx.Node, int | None], float | None] + | None = None, + collective_estimator: Literal["analytical", "benchmark"] = "analytical", + compute_estimator: Literal["analytical", "benchmark"] = "benchmark", + max_memory_increase_gb: float | None = 1.0, + max_memory_increase_ratio: float | None = 0.05, + log_final_collectives_estimations: bool = False, + bucket_exposed_first: bool | None = None, + enable_fusion_regions: bool = False, + bucket_only_internode_comms=False, + prioritize_bucketing_during_scheduling: bool = True, + max_off_bucket_gb: float | None = 0.5, + bucket_mode: BucketMode | None = None, +) -> torch.fx.GraphModule: + """Schedule nodes to maximize compute-collective overlap. + + Args: + gm: Input graph module to optimize. + max_in_flight_gb: Maximum GB of concurrent collective data. Too much in flight memory + can cause memory fragmentation within the CUDA Caching Allocator. + max_compute_pre_fetch: Maximum mm nodes to pre fetch. Note: should already be limited by max_in_flight_gb and + max_memory_increase_gb + collective_bucketing: Enable overlap-preserving collective bucketing. + insert_overlap_deps: Insert overlap dependencies using control deps operator. This should only be used if + compiling with inductor, or for subsequent passes before removing the ops prior to execution. + compute_overlap_multipler: Scale factor for compute time used to hide collectives. This can be used + to address over or under aggressive overlapping. + max_coll_distance: Maximum pre fetch or bucketing candidates. Mainly intended for compile time + custom_runtime_estimation: Override runtime estimation for specific nodes. Called as + custom_runtime_estimation(node, override_size) -> float | None. To pass pre-computed + estimations, wrap a dict: lambda node, _: estimations.get(node). + collective_estimator: Method for estimating collective runtime. "analytical" uses bandwidth formulas, + "benchmark" uses CUDA events with power-of-2 rounding and interpolation. + compute_estimator: Method for estimating compute (ATen op) runtime. "analytical" uses roofline model + estimates (deterministic, no GPU sync), "benchmark" uses GPU benchmarking (more accurate). + max_memory_increase_gb: Maximum GB increase above baseline memory (absolute cap). If None, no absolute limit. + max_memory_increase_ratio: Maximum increase as ratio of baseline peak memory. If None, no ratio limit. + Uses minimum of absolute and ratio limits when both are specified. + enable_fusion_regions: Enable fusion region detection and cost estimation for fusible ops. + bucket_mode: Bucketing mode for grouping collectives. + """ + if not any(is_wait_tensor(n) for n in gm.graph.nodes): + return gm + + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "overlap_scheduling_graph_before", + "encoding": "string", + }, + payload_fn=lambda: gm.print_readable(False), + ) + ret = OverlapScheduler( + gm, + compute_overlap_multipler=compute_overlap_multipler, + max_in_flight_gb=max_in_flight_gb, + max_coll_distance=max_coll_distance, + max_compute_pre_fetch=max_compute_pre_fetch, + custom_runtime_estimation=custom_runtime_estimation, + collective_bucketing=collective_bucketing, + insert_overlap_deps=insert_overlap_deps, + collective_estimator=collective_estimator, + compute_estimator=compute_estimator, + max_memory_increase_gb=max_memory_increase_gb, + max_memory_increase_ratio=max_memory_increase_ratio, + log_final_collectives_estimations=log_final_collectives_estimations, + bucket_exposed_first=bucket_exposed_first, + enable_fusion_regions=enable_fusion_regions, + bucket_only_internode_comms=bucket_only_internode_comms, + prioritize_bucketing_during_scheduling=prioritize_bucketing_during_scheduling, + max_off_bucket_gb=max_off_bucket_gb, + bucket_mode=bucket_mode, + ).run() + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "overlap_scheduling_graph_after", + "encoding": "string", + }, + payload_fn=lambda: ret.print_readable(False), + ) + return ret + + +def schedule_overlap_bucketing_from_inductor_configs( + gm: torch.fx.GraphModule, +) -> torch.fx.GraphModule: + """Schedule nodes to maximize compute-collective overlap using inductor configs. + + Reads configuration from torch._inductor.config.aten_distributed_optimizations + and calls schedule_overlap_bucketing with those settings. + """ + if not any(is_wait_tensor(n) for n in gm.graph.nodes): + return gm + + from torch._inductor import config + + dist_opts = config.aten_distributed_optimizations + + kwargs: dict[str, object] = {} + + config_keys = ( + "collective_bucketing", + "max_compute_pre_fetch", + "custom_runtime_estimation", + "insert_overlap_deps", + "collective_estimator", + "compute_estimator", + "max_memory_increase_gb", + "max_memory_increase_ratio", + "compute_overlap_multipler", + "max_in_flight_gb", + "max_coll_distance", + "log_final_collectives_estimations", + "bucket_exposed_first", + "bucket_only_internode_comms", + "enable_fusion_regions", + "prioritize_bucketing_during_scheduling", + "bucket_mode", + ) + for key in config_keys: + if (val := getattr(dist_opts, key, None)) is not None: + kwargs[key] = val + + return schedule_overlap_bucketing(gm, **kwargs) # type: ignore[arg-type] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/pad_mm.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/pad_mm.py new file mode 100644 index 0000000000000000000000000000000000000000..5a0deb68090f82990d7e32592604c430c4d11e86 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/pad_mm.py @@ -0,0 +1,1001 @@ +import functools +import itertools +import operator +import typing +from collections.abc import Callable, Sequence +from typing import Any + +import torch +import torch._inductor.runtime.runtime_utils +from torch import Tensor +from torch._dynamo.utils import counters +from torch._inductor import utils +from torch._inductor.autoheuristic.autoheuristic import ( + AHContext, + AutoHeuristic, + LocalFeedback, +) +from torch._inductor.autoheuristic.autoheuristic_utils import ( + context_add_strides, + context_add_using_tf32, + pad_mm_operations, + pad_mm_precondition, +) +from torch._inductor.runtime.caching import encoders, memoizers +from torch._subclasses.fake_tensor import FakeTensor +from torch.utils._mode_utils import no_dispatch + +from ...utils._triton import has_triton +from ..pattern_matcher import ( + fwd_only, + gen_register_replacement, + joint_fwd_bwd, + Match, + ReplaceFn, + SearchFn, +) + + +aten = torch.ops.aten + + +# This flag is only used for testing purpose. +# Changing it to True will ignore comparing do_bench times +# between original pattern and padded one. +_skip_do_bench_times = False + + +def fetch_fake_tensors(match: Match, kwarg_names: Sequence[str]) -> list[Tensor]: + kwargs = match.kwargs + return [kwargs[name].meta["val"] for name in kwarg_names] + + +def unwrap_fake_args( + *arg_names: str, +) -> Callable[[Callable[..., Any]], Callable[[Match], Any]]: + def decorator(func: Callable[..., Any]) -> Callable[[Match], Any]: + def wrapper(match: Match) -> Any: + fake_tensors = fetch_fake_tensors(match, arg_names) + return func(*fake_tensors) + + return wrapper + + return decorator + + +def get_alignment_size(x: Tensor) -> int: + return get_alignment_size_dtype(x.dtype) + + +def get_alignment_size_dtype(dtype: torch.dtype) -> int: + if dtype == torch.float16 or dtype == torch.half or dtype == torch.bfloat16: + return 8 + elif dtype == torch.float32 or dtype == torch.float: + return 4 + else: + return 0 + + +def check_device(a: Tensor, b: Tensor) -> bool: + return (a.is_cuda and b.is_cuda) or (a.is_xpu and b.is_xpu) + + +def check_dtype(a: Tensor, b: Tensor) -> bool: + return a.is_floating_point() and b.is_floating_point() + + +def hint_symbols( + ds: Sequence[int | torch.SymInt], +) -> list[int]: + """Helper to convert symbolic dimensions to their concrete hint values.""" + from torch.fx.experimental.symbolic_shapes import optimization_hint + + return [optimization_hint(d) for d in ds] + + +def can_pad( + mat1: Tensor, + mat2: Tensor, + op: torch._ops.OpOverloadPacket, + input: Tensor | None = None, +) -> bool: + """ + Determines if an operation CAN be padded (safety checks). + All logic related to whether it's safe to pad should be here. + """ + + # Can't pad if there is no static dims, we pad static dims only. + def has_one_static_dim(t: Tensor) -> bool: + """Return False if all dimensions are symbolic — nothing concrete to pad.""" + for x in t.size(): + if isinstance(x, int): + return True + elif not isinstance(x, torch.SymInt): + raise RuntimeError("not expected size") + return False + + # Basic safety checks + if not torch._inductor.config.shape_padding: + return False + + if not check_device(mat1, mat2): + return False + + if not check_dtype(mat1, mat2): + return False + + # For padding to be vaible each tensor should have at least one static dim. + tensors = [t for t in (mat1, mat2, input) if t is not None] + if not all(has_one_static_dim(t) for t in tensors): + return False + + # Skip zero-sized dimensions — padding would be wasteful (mm on empty tensors) + from torch.fx.experimental.symbolic_shapes import optimization_hint + + if any( + optimization_hint(dim) == 0 for dim in itertools.chain(mat1.shape, mat2.shape) + ): + return False + + # Calculate padding lengths to check if padding is needed + with no_dispatch(): + if op is torch.ops.aten.mm or op is torch.ops.aten.addmm: + m = mat1.shape[0] + k = mat1.shape[1] + n = mat2.shape[1] + elif op is torch.ops.aten.bmm: + m = mat1.shape[1] + k = mat1.shape[2] + n = mat2.shape[2] + else: + return False + + k_padded_length = get_padded_length(k, get_alignment_size(mat1)) + n_padded_length = get_padded_length(n, get_alignment_size(mat2)) + m_padded_length = get_padded_length(m, get_alignment_size(mat1)) + + # No padding needed - can't pad if there's nothing to pad + if m_padded_length == k_padded_length == n_padded_length == 0: + return False + + # In deterministic mode, we can't safely benchmark - disallow padding + # Check this after other basic checks so force_shape_pad/autoheuristic can override + if ( + torch._inductor.config.deterministic + and not torch._inductor.config.force_shape_pad + and not torch._inductor.config.use_autoheuristic("pad_mm") + ): + return False + + # Triton availability check - required for padding to work + if not has_triton(): + return False + + return True + + +def get_padded_length(x: int | torch.SymInt, alignment_size: int) -> int: + # we don't pad x if it is symbolic + if isinstance(x, torch.SymInt) or alignment_size == 0 or x % alignment_size == 0: + return 0 + + # ignore dim that can be squeezed away + if x == 1: + return 0 + + return int((x // alignment_size + 1) * alignment_size) - x + + +def pad_dim(x: Tensor, padded_length: int, dim: int) -> Tensor: + if padded_length == 0: + return x + pad = x.new_zeros(*x.shape[:dim], padded_length, *x.shape[dim + 1 :]) + return torch.cat([x, pad], dim=dim) + + +def addmm_pattern( + input: Tensor, mat1: Tensor, mat2: Tensor, beta: float, alpha: float +) -> Tensor: + return aten.addmm(input, mat1, mat2, beta=beta, alpha=alpha) + + +def should_pad_addmm(match: Match) -> bool: + mat1, mat2, input = fetch_fake_tensors(match, ("mat1", "mat2", "input")) + return should_pad(match, mat1, mat2, torch.ops.aten.addmm, input=input) + + +def pad_addmm( + input: Tensor | None, + mat1: Tensor, + mat2: Tensor, + m_padded_length: int, + k_padded_length: int, + n_padded_length: int, + beta: float = 1.0, + alpha: float = 1.0, + mat1_pre_padded: bool = False, + mat2_pre_padded: bool = False, +) -> Tensor: + # for paddings, dim order is reversed for some reasons + # and for every dim, we need to specify left and right padding + if not mat1_pre_padded: + mat1 = pad_mat1( + mat1, m_padded_length=m_padded_length, k_padded_length=k_padded_length + ) + if not mat2_pre_padded: + mat2 = pad_mat2( + mat2, k_padded_length=k_padded_length, n_padded_length=n_padded_length + ) + + # the add broadcasts, so we only pad if the dimension != 1 + if input is not None: + if n_padded_length != 0: + if input.dim() == 2 and input.shape[1] != 1: + input = pad_dim(input, n_padded_length, 1) + elif input.dim() == 1 and input.shape[0] != 1: + input = pad_dim(input, n_padded_length, 0) + if m_padded_length != 0 and input.dim() == 2 and input.shape[0] != 1: + input = pad_dim(input, m_padded_length, 0) + + res = aten.addmm(input, mat1, mat2, beta=beta, alpha=alpha) + + if m_padded_length != 0: + res = res[:-m_padded_length, :] + if n_padded_length != 0: + res = res[:, :-n_padded_length] + return res + + +def addmm_replace( + input: Tensor | None, + mat1: Tensor, + mat2: Tensor, + beta: float = 1.0, + alpha: float = 1.0, +) -> Tensor: + k_padded_length = get_padded_length(mat1.shape[1], get_alignment_size(mat1)) + n_padded_length = get_padded_length(mat2.shape[1], get_alignment_size(mat2)) + m_padded_length = get_padded_length(mat1.shape[0], get_alignment_size(mat1)) + return pad_addmm( + input, + mat1, + mat2, + m_padded_length, + k_padded_length, + n_padded_length, + beta, + alpha, + ) + + +def is_mm_compute_bound(M: int, K: int, N: int, dtype: torch.dtype) -> bool: + denominator = M * K + N * K + M * N + if denominator == 0: + return False + arithmetic_intensity = (M * N * K) / denominator + + # we have experienced some large perf hits in this case, even in bandwidth bound regimes + if ( + dtype is torch.bfloat16 + and K > M + and K > N + and (torch.xpu.is_available() or torch.cuda.get_device_capability() < (9, 0)) + ): # doesn't repro on h100s: + return True + + # Fails with AMD + try: + machine_balance = ( + 1000 * utils.get_device_tflops(dtype) + ) / utils.get_gpu_dram_gbps() + except Exception: + return True + + # dram_gbps might be underestimating bandwidth because of cache. + # if we estimate machine balance too low we might miss some speedups, + # if we estimate too high there will be unnecessary compilation time increase. + # TODO - finetune coefficient here. As a reference point, Triton mm model assumes + # 80% of reads are in cache and cache is 4x faster than dram_gbps + machine_balance = machine_balance * 0.5 + + return arithmetic_intensity > machine_balance + + +@functools.cache +def get_pad_cache() -> torch._inductor.codecache.LocalCache: + return torch._inductor.codecache.LocalCache() + + +def get_cached_should_pad(key: str) -> bool: + return get_pad_cache().lookup(key) # type: ignore[return-value] + + +def set_cached_should_pad(key: str, value: bool) -> None: + return get_pad_cache().set_value(key, value=value) + + +def get_cached_base_mm_benchmark_time(key: str) -> float: + return get_pad_cache().lookup(key) # type: ignore[return-value] + + +def set_cached_base_mm_benchmark_time(key: str, value: float) -> None: + return get_pad_cache().set_value(key, value=value) + + +def should_pad_bench_key( + match: Match, + mat1: Tensor, + mat2: Tensor, + op: torch._ops.OpOverloadPacket, + input: Tensor | None = None, + is_base_time_key: bool = False, +) -> str: + def tensor_key(t: Tensor) -> tuple[torch.Size, tuple[int, ...], torch.dtype]: + return (t.shape, t.stride(), t.dtype) + + tf32_key = ( + None + if mat1.dtype != torch.float32 + else torch.backends.cuda.matmul.fp32_precision == "tf32" + or torch.backends.mkldnn.fp32_precision == "tf32" + ) + + def fmt_pad(name: str) -> str | None: + if is_base_time_key: + return None + return f"exclude_pad:{should_exclude_padding_time(match, name)}" + + key = ( + tensor_key(mat1), + tensor_key(mat2), + fmt_pad("mat1"), + fmt_pad("mat2"), + op, + input if input is None else tensor_key(input), + tf32_key, + ) + + key = str(key) + if is_base_time_key: + key = f"base mm time: {key}" + return key + + +def get_non_view_def(node: torch.fx.Node) -> torch.fx.Node: + if node.op is operator.getitem: + return get_non_view_def(node.args[0]) # type: ignore[arg-type] + + if ( + node.op == "call_function" + and isinstance(node.target, torch._ops.OpOverload) + and utils.is_view(node.target) + ): + return get_non_view_def(node.all_input_nodes[0]) + + return node + + +def should_exclude_padding_time(match: Match, arg_name: str) -> bool: + from torch._prims_common import is_contiguous_or_false + + node_def = get_non_view_def(match.kwargs[arg_name]) + + # constant padding converts tensors to contiguous so even if the input tensor + # can be planned layout transform is not free. TODO - way to pad and preserve layout ? + # Use is_contiguous_or_false to avoid guarding on data-dependent expressions + # with unbacked symints - returns False instead of raising an error. + if not is_contiguous_or_false(fetch_fake_tensors(match, (arg_name,))[0]): + return False + + # TODO - see issue https://github.com/pytorch/pytorch/issues/128889 + # We would only able to completely plan these out if we were only doing + # first dimension padding. non-first we would still need a copy + # because these outputs are fixed dense. + cannot_plan_output = [ + aten.mm.default, + aten.convolution.default, + aten.convolution_backward.default, + aten.bmm.default, + aten.addmm.default, + aten._scaled_dot_product_flash_attention.default, + aten._scaled_dot_product_efficient_attention.default, + ] + + if node_def.target in cannot_plan_output: + return False + + if ( + node_def.target is aten.cat.default + and len(node_def.all_input_nodes) + > torch._inductor.config.max_pointwise_cat_inputs + ): + return False + + # optimistically assume we should be able to memory plan away + # all non inputs + return node_def.op != "placeholder" + + +def is_padded_faster(key: str, ori_time: float, pad_time: float) -> bool: + """ + Determines if padding is beneficial by comparing benchmark times. + Helper function that applies a multiplier to account for memory ops overhead. + """ + multiplier = 1.1 + # Shape padding introduces additional memory ops. Based on microbenchmarks, 1.1x represents a reasonable + # tradeoff between performance improvement from shape padding and overhead from additional memory ops + # TODO: Build a learned model which would be better than this heuristic + if "shape_padding_multiplier" in torch._inductor.config.post_grad_fusion_options: + multiplier = torch._inductor.config.post_grad_fusion_options[ + "shape_padding_multiplier" + ].get("value", 1.1) + counters["inductor"]["shape_padding_multiplier"] += 1 + padded_is_faster = _skip_do_bench_times or ori_time > pad_time * multiplier + set_cached_should_pad(key, padded_is_faster) + return padded_is_faster + + +def should_pad_mm_bf16(dtype: torch.dtype, M: int, N: int, K: int) -> bool: + # always force pad for mm with bf16 when the following are satisfied to avoid perf regression + large_k_threshold_to_pad = torch._inductor.config.post_grad_fusion_options[ + "pad_aten_mm_pass" + ].get("k_threshold_to_pad", 8388608) + if ( + dtype is torch.bfloat16 + and K > M + and K > N + and N % 2 == 1 + and K >= large_k_threshold_to_pad + and (torch.xpu.is_available() or torch.cuda.get_device_capability() < (9, 0)) + ): # doesn't repro on h100s: + return True + return False + + +def should_pad( + match: Match, + mat1: Tensor, + mat2: Tensor, + op: torch._ops.OpOverloadPacket, + input: Tensor | None = None, +) -> bool: + _can_pad = can_pad(mat1, mat2, op, input) + # Note that if you're tempted to insert a dynamo_timed call here, this function can + # be called enough that the dynamo_timed overhead is not negligible. + return _can_pad and _should_pad(match, mat1, mat2, op, input) + + +def get_do_bench() -> Callable[[Callable[[], Any]], float]: + return functools.partial( + # pyrefly: ignore [bad-argument-type] + torch._inductor.runtime.benchmarking.benchmarker.benchmark_gpu, + warmup=5, + ) + + +@memoizers.should_pad_memoizer.memoize( + custom_params_encoder=encoders.should_pad_params_encoder +) +def _should_pad( + match: Match, + mat1: Tensor, + mat2: Tensor, + op: torch._ops.OpOverloadPacket, + input: Tensor | None = None, +) -> bool: + """ + Determines if an operation SHOULD be padded (performance checks). + All logic related to whether padding would be performant should be here. + """ + do_bench = get_do_bench() + + with no_dispatch(): + if op is torch.ops.aten.mm or op is torch.ops.aten.addmm: + m = mat1.shape[0] + k = mat1.shape[1] + n = mat2.shape[1] + k_padded_length = get_padded_length(k, get_alignment_size(mat1)) + n_padded_length = get_padded_length(n, get_alignment_size(mat2)) + m_padded_length = get_padded_length(m, get_alignment_size(mat1)) + elif op is torch.ops.aten.bmm: + m = mat1.shape[1] + k = mat1.shape[2] + n = mat2.shape[2] + k_padded_length = get_padded_length(k, get_alignment_size(mat1)) + m_padded_length = get_padded_length(m, get_alignment_size(mat1)) + n_padded_length = get_padded_length(n, get_alignment_size(mat2)) + else: + return False + + # Force padding when explicitly requested - performance override + if torch._inductor.config.force_shape_pad: + return True + + # Resolve symbolic dims to concrete hints for heuristic checks below. + # These are performance decisions, not correctness — optimization_hint is safe. + m_concrete, k_concrete, n_concrete = hint_symbols((m, k, n)) + + # Performance heuristic for bf16 large K scenarios + if ( + "pad_aten_mm_pass" in torch._inductor.config.post_grad_fusion_options + and should_pad_mm_bf16(mat1.dtype, m_concrete, n_concrete, k_concrete) + ): + return True + + # Check if operation is compute bound (performance check) + if not is_mm_compute_bound(m_concrete, k_concrete, n_concrete, mat1.dtype): + return False + + # We don't want to look up the cache for cases that are trivially false + # since it does file io + key = should_pad_bench_key(match, mat1, mat2, op, input) + + cached_pad = get_cached_should_pad(key) + if cached_pad is not None: + return cached_pad + + def realize_tensor(t): + if isinstance(t, FakeTensor): + size_hints = hint_symbols(t.size()) + # pyrefly: ignore [bad-argument-type] + stride_hint = hint_symbols(t.stride()) + real_size = ( + sum((d - 1) * s for d, s in zip(size_hints, stride_hint)) + 1 + ) + real_t = torch.randn(real_size, dtype=t.dtype, device=t.device) + return torch.as_strided(real_t, size_hints, stride_hint) + else: + return torch.randn_like(t) + + mat1 = realize_tensor(mat1) + mat2 = realize_tensor(mat2) + + # since we key on whether or not the inputs can be memory planned, set cache for the + # original time which is unaffected by whether or not the input can be planned + ori_time_key = should_pad_bench_key( + match, mat1, mat2, op, input, is_base_time_key=True + ) + ori_time = get_cached_base_mm_benchmark_time(ori_time_key) + if ori_time is None and op is torch.ops.aten.addmm and input is not None: + # realize bias for addmm + input = realize_tensor(input) + + mat1_pad = mat1 + mat2_pad = mat2 + + is_bmm = op is torch.ops.aten.bmm + + mat1_pre_padded = should_exclude_padding_time(match, "mat1") + fns = [] + if mat1_pre_padded and (m_padded_length or k_padded_length): + mat1_pad = pad_mat1( + mat1_pad, + m_padded_length=m_padded_length, + k_padded_length=k_padded_length, + is_bmm=is_bmm, + ) + + def write_pad(): + if is_bmm: + mat1_pad[:, -m_padded_length:, -k_padded_length:].fill_(0) + else: + mat1_pad[-m_padded_length:, -k_padded_length:].fill_(0) + + fns.append(write_pad) + + mat2_pre_padded = should_exclude_padding_time(match, "mat2") + if mat2_pre_padded and (k_padded_length or n_padded_length): + mat2_pad = pad_mat2( + mat2_pad, + k_padded_length=k_padded_length, + n_padded_length=n_padded_length, + is_bmm=is_bmm, + ) + + def write_pad(): + if is_bmm: + mat2_pad[:, -k_padded_length:, -n_padded_length:].fill_(0) + else: + mat2_pad[-k_padded_length:, -n_padded_length:].fill_(0) + + fns.append(write_pad) + + if op is torch.ops.aten.addmm: + input_pad = None + if input is not None and (input.is_cuda or input.is_xpu): + input_pad = torch.randn_like(input) + fns.append( + lambda: pad_addmm( + input_pad, + mat1_pad, + mat2_pad, + m_padded_length, + k_padded_length, + n_padded_length, + mat1_pre_padded=mat1_pre_padded, + mat2_pre_padded=mat2_pre_padded, + ) + ) + elif op is torch.ops.aten.mm: + fns.append( + lambda: pad_mm( + mat1_pad, + mat2_pad, + m_padded_length, + k_padded_length, + n_padded_length, + mat1_pre_padded=mat1_pre_padded, + mat2_pre_padded=mat2_pre_padded, + ) + ) + else: + fns.append( + lambda: pad_bmm( + mat1_pad, + mat2_pad, + m_padded_length, + k_padded_length, + n_padded_length, + mat1_pre_padded=mat1_pre_padded, + mat2_pre_padded=mat2_pre_padded, + ) + ) + + def orig_bench_fn(): + if op is torch.ops.aten.bmm or op is torch.ops.aten.mm: + op(mat1, mat2) + else: + op(input, mat1, mat2) + + def pad_bench_fn(): + for fn in fns: + fn() + + if ( + torch._inductor.config.run_autoheuristic("pad_mm") + and op is torch.ops.aten.mm + ): + ah_should_pad = run_autoheuristic( + mat1, + mat2, + orig_bench_fn, + pad_bench_fn, + m_padded_length, + k_padded_length, + n_padded_length, + do_bench, + mat1_pre_padded, + mat2_pre_padded, + ori_time, + ori_time_key, + key, + ) + if ah_should_pad is not None: + return ah_should_pad + + # AH didn't make a decision, so if we're in deterministic mode, we should return false + if torch._inductor.config.deterministic: + return False + + if ori_time is None: + ori_time = do_bench(orig_bench_fn) + set_cached_base_mm_benchmark_time(ori_time_key, ori_time) + + pad_time = do_bench(pad_bench_fn) + + counters["inductor"]["pad_mm_bench"] += 1 + return is_padded_faster(key, ori_time, pad_time) + + +def get_context( + mat1: Tensor, + mat2: Tensor, + mat1_pre_padded: bool, + mat2_pre_padded: bool, + m_padded_length: int, + k_padded_length: int, + n_padded_length: int, +) -> AHContext: + context = AHContext() + + context.add_feature("m", mat1.shape[0]) + context.add_feature("k", mat1.shape[1]) + context.add_feature("n", mat2.shape[1]) + + context_add_strides(context, "mat1", mat1.stride()) + context_add_strides(context, "mat2", mat2.stride()) + + context.add_feature("m_padded_length", m_padded_length) + context.add_feature("k_padded_length", k_padded_length) + context.add_feature("n_padded_length", n_padded_length) + + context.add_feature("mat1_align_size", get_alignment_size(mat1)) + context.add_feature("mat2_align_size", get_alignment_size(mat2)) + + context.add_feature("mat1_dtype", mat1.dtype, is_categorical=True) + context.add_feature("mat2_dtype", mat2.dtype, is_categorical=True) + + context.add_feature("prepadded_mat1", mat1_pre_padded, is_categorical=True) + context.add_feature("prepadded_mat2", mat2_pre_padded, is_categorical=True) + + context_add_using_tf32(context, mat1.dtype) + return context + + +def run_autoheuristic( + mat1: Tensor, + mat2: Tensor, + orig_bench_fn: Callable[[], None], + pad_bench_fn: Callable[[], None], + m_padded_length: int, + k_padded_length: int, + n_padded_length: int, + do_bench: Callable[[Callable[[], Any]], float], + mat1_pre_padded: bool, + mat2_pre_padded: bool, + ori_time: float, + ori_time_key: str, + key: str, +) -> bool | None: + def feedback_fn( + choice: str, + ) -> float | None: + if choice == orig_choice: + return do_bench(orig_bench_fn) + elif choice == pad_choice: + return do_bench(pad_bench_fn) + return None + + def fallback() -> str: + return "autotune" + + orig_choice = "orig" + pad_choice = "pad" + choices = [orig_choice, pad_choice] + feedback = LocalFeedback(feedback_fn) # type: ignore[arg-type] + context = get_context( + mat1, + mat2, + mat1_pre_padded, + mat2_pre_padded, + m_padded_length, + k_padded_length, + n_padded_length, + ) + name = "pad_mm" + autoheuristic = AutoHeuristic( + fallback=fallback, + choices=choices, + feedback=feedback, + context=context, + name=name, + augment_context=pad_mm_operations(), + precondition=pad_mm_precondition, + ) + choice = autoheuristic.get_choice() + choice2should_pad = {orig_choice: False, pad_choice: True, "autotune": None} + ah_should_pad = choice2should_pad.get(choice) + + if torch._inductor.config.collect_autoheuristic(name): + ah_ori_time = autoheuristic.get_collected_feedback(orig_choice) + ah_pad_time = autoheuristic.get_collected_feedback(pad_choice) + + # if precondition is not satisfied, autoheuristic does not collect data + if ah_ori_time is not None and ah_pad_time is not None: + if ori_time is None: + set_cached_base_mm_benchmark_time(ori_time_key, ah_ori_time) + return is_padded_faster(key, ah_ori_time, ah_pad_time) + if ah_should_pad is not None: + set_cached_should_pad(key, ah_should_pad) + return ah_should_pad + + +def mm_pattern(mat1: Tensor, mat2: Tensor) -> Tensor: + return aten.mm(mat1, mat2) + + +def should_pad_mm(match: Match) -> bool: + mat1, mat2 = fetch_fake_tensors(match, ("mat1", "mat2")) + return should_pad(match, mat1, mat2, torch.ops.aten.mm) + + +def pad_mat1( + mat1: Tensor, *, m_padded_length: int, k_padded_length: int, is_bmm: bool = False +) -> Tensor: + if k_padded_length != 0 or m_padded_length != 0: + # dim order is reversed for constant_pad_nd, for every dim we specify right and left padding + pad_arg = [0, k_padded_length, 0, m_padded_length] + if is_bmm: + pad_arg.extend((0, 0)) + return aten.constant_pad_nd(mat1, pad_arg) + else: + return mat1 + + +def pad_mat2( + mat2: Tensor, *, k_padded_length: int, n_padded_length: int, is_bmm: bool = False +) -> Tensor: + if k_padded_length != 0 or n_padded_length != 0: + # dim order is reversed for constant_pad_nd, for every dim we specify right and left padding + pad_arg = [0, n_padded_length, 0, k_padded_length] + if is_bmm: + pad_arg.extend((0, 0)) + return aten.constant_pad_nd(mat2, pad_arg) + else: + return mat2 + + +def pad_mm( + mat1: Tensor, + mat2: Tensor, + m_padded_length: int, + k_padded_length: int, + n_padded_length: int, + mat1_pre_padded: bool = False, + mat2_pre_padded: bool = False, +) -> Tensor: + if not mat1_pre_padded: + mat1 = pad_mat1( + mat1, m_padded_length=m_padded_length, k_padded_length=k_padded_length + ) + if not mat2_pre_padded: + mat2 = pad_mat2( + mat2, k_padded_length=k_padded_length, n_padded_length=n_padded_length + ) + res = aten.mm(mat1, mat2) + if m_padded_length != 0: + res = res[:-m_padded_length, :] + if n_padded_length != 0: + res = res[:, :-n_padded_length] + return res + + +def mm_replace(mat1: Tensor, mat2: Tensor) -> Tensor: + k_padded_length = get_padded_length(mat1.shape[1], get_alignment_size(mat1)) + m_padded_length = get_padded_length(mat1.shape[0], get_alignment_size(mat1)) + n_padded_length = get_padded_length(mat2.shape[1], get_alignment_size(mat2)) + return pad_mm( + mat1, + mat2, + m_padded_length, + k_padded_length, + n_padded_length, + ) + + +def bmm_pattern(mat1: Tensor, mat2: Tensor) -> Tensor: + return aten.bmm(mat1, mat2) + + +def should_pad_bmm(match: Match) -> bool: + mat1, mat2 = fetch_fake_tensors(match, ("mat1", "mat2")) + return should_pad(match, mat1, mat2, torch.ops.aten.bmm) + + +def pad_bmm( + mat1: Tensor, + mat2: Tensor, + m_padded_length: int, + k_padded_length: int, + n_padded_length: int, + mat1_pre_padded: bool = False, + mat2_pre_padded: bool = False, +) -> Tensor: + if not mat1_pre_padded: + mat1 = pad_mat1( + mat1, + m_padded_length=m_padded_length, + k_padded_length=k_padded_length, + is_bmm=True, + ) + if not mat2_pre_padded: + mat2 = pad_mat2( + mat2, + k_padded_length=k_padded_length, + n_padded_length=n_padded_length, + is_bmm=True, + ) + res = aten.bmm(mat1, mat2) + if m_padded_length != 0: + res = res[:, :-m_padded_length, :] + if n_padded_length != 0: + res = res[:, :, :-n_padded_length] + return res + + +def bmm_replace(mat1: Tensor, mat2: Tensor) -> Tensor: + k_padded_length = get_padded_length(mat1.shape[2], get_alignment_size(mat1)) + n_padded_length = get_padded_length(mat2.shape[2], get_alignment_size(mat2)) + m_padded_length = get_padded_length(mat1.shape[1], get_alignment_size(mat1)) + return pad_bmm( + mat1, + mat2, + m_padded_length, + k_padded_length, + n_padded_length, + ) + + +@functools.cache +def _pad_mm_init(input_device: torch.device | None = None) -> None: + from .joint_graph import patterns + + if input_device: + device = str(input_device) + else: + if torch.cuda.is_available(): + # workaround https://github.com/pytorch/pytorch/issues/97894 + device = "cuda" + elif torch.xpu.is_available(): + device = "xpu" + else: + device = "cpu" + + # sizes/values dont actually matter for initial trace + # once we get a possible match we re-trace with the actual values and verify the match still holds + + dim2a = functools.partial(torch.empty, (4, 4), device=device, requires_grad=True) + dim2b = functools.partial(torch.empty, (4, 4), device=device, requires_grad=True) + + dim3a = functools.partial(torch.empty, (4, 4, 4), device=device, requires_grad=True) + dim3b = functools.partial(torch.empty, (4, 4, 4), device=device, requires_grad=True) + + dim1a = functools.partial(torch.empty, (4), device=device, requires_grad=True) + + # workaround https://github.com/pytorch/pytorch/issues/97894 + # 0.113377 is a "magic" value that lets us recover the lost input arg relationship + rep = {"beta": 0.213377, "alpha": 0.113377} + + for pattern, replacement, args, workaround, extra_check in [ + ( + typing.cast(SearchFn, mm_pattern), + typing.cast(ReplaceFn, mm_replace), + [dim2a(), dim2b()], + {}, + should_pad_mm, + ), + ( + typing.cast(SearchFn, bmm_pattern), + typing.cast(ReplaceFn, bmm_replace), + [dim3a(), dim3b()], + {}, + should_pad_bmm, + ), + ( + typing.cast(SearchFn, addmm_pattern), + typing.cast(ReplaceFn, addmm_replace), + [dim1a(), dim2a(), dim2b()], + rep, + should_pad_addmm, + ), + ]: + assert isinstance(workaround, dict) # mypy is unable to infer the type properly + name = pattern.__name__ + + gen_register_replacement( + f"{name}_training", + pattern, + replacement, + args, + # pyrefly: ignore [bad-argument-type] + joint_fwd_bwd, + # pyrefly: ignore [bad-argument-type] + patterns, + extra_check=extra_check, + scalar_workaround=workaround, + skip_duplicates=True, + ) + + gen_register_replacement( + f"{name}_inference", + pattern, + replacement, + args, + # pyrefly: ignore [bad-argument-type] + fwd_only, + # pyrefly: ignore [bad-argument-type] + patterns, + extra_check=extra_check, + scalar_workaround=workaround, + skip_duplicates=True, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/post_grad.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/post_grad.py new file mode 100644 index 0000000000000000000000000000000000000000..02ec541caf7a905a8d5922b9dba6f9cf4eeb5656 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/post_grad.py @@ -0,0 +1,2041 @@ +# mypy: allow-untyped-decorators +# mypy: allow-untyped-defs +import functools +import itertools +import logging +import operator +from collections import Counter, defaultdict +from collections.abc import Callable +from typing import Any, TypeVar +from typing_extensions import ParamSpec + +import torch +import torch._inductor as inductor +import torch.utils._pytree as pytree +from torch import fx +from torch._decomp import register_decomposition +from torch._dynamo.utils import counters +from torch._inductor import comms +from torch._inductor.custom_graph_pass import CustomInferenceAwareGraphPass +from torch._inductor.virtualized import ops # noqa: F401 +from torch._logging import trace_structured +from torch._prims_common import is_boolean_dtype, is_expandable_to, is_integer_dtype +from torch.fx.experimental.symbolic_shapes import statically_known_true, sym_eq +from torch.utils._ordered_set import OrderedSet + +from .. import config, ir, pattern_matcher # noqa: F401 +from ..codegen.common import custom_backend_passes +from ..comms import remove_fsdp2_unsharded_param_graph_input_usage +from ..fx_utils import FakeTensorUpdater, get_fake_args_kwargs, get_node_storage +from ..lowering import lowerings as L +from ..pattern_matcher import ( + _return_true, + Arg, + CallFunction, + CallFunctionVarArgs, + filter_nodes, + fwd_only, + get_arg_value, + get_mutation_region_id, + Ignored, + init_once_fakemode, + KeywordArg, + ListOf, + Match, + MultiOutputPattern, + MULTIPLE, + PatternMatcherPass as PatternMatcherPassBase, + register_graph_pattern, + register_replacement, + stable_topological_sort, +) +from ..utils import ( + decode_device, + get_all_devices, + get_gpu_type, + is_gpu, + is_pointwise_use, + OPTIMUS_EXCLUDE_POST_GRAD, +) +from ..virtualized import V +from .b2b_gemm import B2B_GEMM_PASS +from .ddp_fusion import fuse_ddp_communication +from .group_batch_fusion import group_batch_fusion_passes, POST_GRAD_FUSIONS +from .micro_pipeline_tp import micro_pipeline_tp_pass +from .pre_grad import is_same_dict, save_inductor_dict +from .reduced_atomic_contention import partitioned_scatter_optimization_pass +from .reinplace import reinplace_inplaceable_ops +from .split_cat import POST_GRAD_PATTERNS + + +_T = TypeVar("_T") +_P = ParamSpec("_P") + +PatternMatcherPass = functools.partial( + PatternMatcherPassBase, subsystem="post_grad_passes" +) + +log = logging.getLogger(__name__) +aten = torch.ops.aten +prims = torch.ops.prims + +# First pass_patterns[0] are applied, then [1], then [2] +pass_patterns = [ + PatternMatcherPass(), + PatternMatcherPass(), + PatternMatcherPass(), +] + + +def _remove_profiler_ops(graph: torch.fx.Graph) -> None: + """ + Remove profiler ops (record_function) from the graph. + These ops are side-effectful but don't affect computation, + and we don't want them to block fusion. + """ + profiler_ops = OrderedSet( + [ + torch.ops.profiler._record_function_enter.default, + torch.ops.profiler._record_function_enter_new.default, + torch.ops.profiler._record_function_exit._RecordFunction, + ] + ) + + nodes_to_remove = [] + + for node in graph.nodes: + if node.op == "call_function" and node.target in profiler_ops: + nodes_to_remove.append(node) + + for node in reversed(nodes_to_remove): + graph.erase_node(node) + + +def post_grad_passes(gm: torch.fx.GraphModule, is_inference: bool): + """ + Passes that run on after grad. This is called once on the forwards + graph and once on the backwards graph. + + The IR here has been normalized and functionalized. + """ + GraphTransformObserver = functools.partial( + torch.fx.passes.graph_transform_observer.GraphTransformObserver, + subsystem="post_grad_passes", + ) + + if not torch._dynamo.config.skip_fsdp_hooks: + remove_fsdp2_unsharded_param_graph_input_usage(gm.graph) + + if config.dce: + # has some issues with mutation in inference mode + gm.graph.eliminate_dead_code() + + if is_inference and config.reorder_for_locality: + GraphTransformObserver(gm, "reorder_for_locality").apply_graph_pass( + reorder_for_locality + ) + + fake_tensor_updater = FakeTensorUpdater(gm.graph) + + if post_grad_custom_pre_pass := config.post_grad_custom_pre_pass: + if isinstance(post_grad_custom_pre_pass, CustomInferenceAwareGraphPass): + post_grad_custom_pre_pass = functools.partial( + post_grad_custom_pre_pass, is_inference=is_inference + ) + GraphTransformObserver(gm, "post_grad_custom_pre_pass").apply_graph_pass( + post_grad_custom_pre_pass + ) + + if torch._C._has_mkldnn: + if ( + config.cpp.enable_grouped_gemm_template + and config.max_autotune + and "CPP" in config.max_autotune_gemm_backends + ): + from .mkldnn_fusion import grouped_gemm_pass + + grouped_gemm_pass(gm.graph) + + if config.cpp.enable_concat_linear: + from .quantization import concat_linear_woq_int4 + + # Concat linear optimization for WOQ int4 + concat_linear_woq_int4(gm) + + # Remove profiler ops (record_function) to prevent them blocking fusion + GraphTransformObserver(gm, "remove_profiler_ops").apply_graph_pass( + _remove_profiler_ops + ) + + if config.pattern_matcher: + lazy_init() + GraphTransformObserver(gm, "post_grad_custom_pre_pass").apply_graph_pass( + functools.partial(group_batch_fusion_passes, pre_grad=False) + ) + GraphTransformObserver(gm, "remove_noop_ops").apply_graph_pass(remove_noop_ops) + GraphTransformObserver(gm, "remove_assert_ops").apply_graph_pass( + remove_assert_ops + ) + for i, patterns in enumerate(pass_patterns): + GraphTransformObserver(gm, f"pass_pattern_{i}").apply_graph_pass( + patterns.apply + ) + if config.partitioned_scatter_enabled: + GraphTransformObserver( + gm, "partitioned_scatter_optimization" + ).apply_graph_pass(partitioned_scatter_optimization_pass) + for pass_name in config.post_grad_fusion_options: + # skip all patterns for group batch fusions or quantization patterns + if pass_name in POST_GRAD_FUSIONS or pass_name in OPTIMUS_EXCLUDE_POST_GRAD: + continue + pattern_matcher_pass = POST_GRAD_PATTERNS[pass_name] + inductor_before_change = save_inductor_dict( + [pattern_matcher_pass.pass_name] + ) + GraphTransformObserver(gm, pass_name).apply_graph_pass( + pattern_matcher_pass.apply + ) + if not is_same_dict(counters["inductor"], inductor_before_change): + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": f"{pattern_matcher_pass.pass_name}_post_grad", + "encoding": "string", + }, + payload_fn=lambda: gm.print_readable( + print_output=False, include_stride=True, include_device=True + ), + ) + if config.b2b_gemm_pass: + B2B_GEMM_PASS.apply(gm.graph) # type: ignore[arg-type] + + if config._micro_pipeline_tp: + micro_pipeline_tp_pass(gm.graph) + + if config._fuse_ddp_communication: + GraphTransformObserver(gm, "fuse_ddp_communication").apply_graph_pass( + lambda graph: fuse_ddp_communication( + graph, + config._fuse_ddp_communication_passes, + config._fuse_ddp_bucket_size, + ) + ) + + if post_grad_custom_post_pass := config.post_grad_custom_post_pass: + if isinstance(post_grad_custom_post_pass, CustomInferenceAwareGraphPass): + post_grad_custom_post_pass = functools.partial( + post_grad_custom_post_pass, is_inference=is_inference + ) + GraphTransformObserver(gm, "post_grad_custom_post_pass").apply_graph_pass( + post_grad_custom_post_pass + ) + + GraphTransformObserver(gm, "stable_sort").apply_graph_pass(stable_topological_sort) + + GraphTransformObserver(gm, "move_constructors_to_cuda").apply_graph_pass( + move_constructors_to_gpu + ) + + fake_tensor_updater.incremental_update() + + for device, custom_backend_pass in custom_backend_passes.items(): + if custom_backend_pass is not None: + gm_devices = [d.type for d in get_all_devices(gm)] + if device in gm_devices: + pass_name = "custom_backend_passes_" + device + GraphTransformObserver(gm, pass_name).apply_gm_pass(custom_backend_pass) + + # SPMD verification — before collective reordering passes. + if ( + config.aten_distributed_optimizations.spmd_check + and _needs_spmd_graph_preservation() + ): + from torch._inductor.fx_passes.spmd_check import spmd_check + + spmd_check(gm) + + collectives_bucketing: bool = False + + if config.bucket_reduce_scatters_fx != "none": + from torch._inductor.fx_passes.bucketing import bucket_reduce_scatter + from torch._inductor.fx_passes.fsdp import bucket_fsdp_reduce_scatter + + p = ( + bucket_fsdp_reduce_scatter + if "fsdp" in config.bucket_reduce_scatters_fx + else bucket_reduce_scatter + ) + GraphTransformObserver(gm, "bucket_reduce_scatters").apply_graph_pass( + lambda graph: p( + graph.owning_module, + config.bucket_reduce_scatters_fx_bucket_size_determinator, + config.bucket_reduce_scatters_bucket_mode, # type: ignore[arg-type] + ) + ) + collectives_bucketing = True + + if config.bucket_all_reduces_fx != "none": + from torch._inductor.fx_passes.bucketing import bucket_all_reduce + + GraphTransformObserver(gm, "bucket_all_reduce").apply_graph_pass( + lambda graph: bucket_all_reduce( + graph.owning_module, + config.bucket_all_reduces_fx_bucket_size_determinator, + config.bucket_all_reduces_fx, # type: ignore[arg-type] + ) + ) + collectives_bucketing = True + + # Fx all_gather bucketing introduces mutation op + # Keeping it in the end to keep invariant of functional graph for previous passes. + if config.bucket_all_gathers_fx != "none": + from torch._inductor.fx_passes.bucketing import bucket_all_gather + from torch._inductor.fx_passes.fsdp import bucket_fsdp_all_gather + + p = ( + bucket_fsdp_all_gather # type: ignore[assignment] + if "fsdp" in config.bucket_all_gathers_fx + else bucket_all_gather + ) + GraphTransformObserver(gm, "bucket_all_gathers").apply_graph_pass( + lambda graph: p( + graph.owning_module, + config.bucket_all_gathers_fx_bucket_size_determinator, + config.bucket_all_gathers_bucket_mode, # type: ignore[arg-type] + ) + ) + collectives_bucketing = True + + if collectives_bucketing: + # Fx collectives bucketing passes require topological sort for the cases: + # when bucketed collectives have users before the last collective in the bucket + # AND when inputs of bucketed collective have ancestors after the first collective in the bucket. + # + # In this case we can not manually pick the place for bucketed collective insertion. + # But we are guaranteed by the bucketing (independent collectives in the bucket), + # that it is possible to reorder nodes to satisfy all ordering requirements. + # + # --- before bucketing --- + # in0 = ... + # wait_ag0 = ag(in0) + # user0(wait_ag0) + # ... + # pre_in1 = ... + # in1 = transform(pre_in1) + # wait_ag1 = ag(in1) + # user1(wait_ag1) + # + # --- after bucketing --- + # + # in0 = ... + # user(wait_ag0) <--- wait_ag0 is defined only after bucketed collective. + # + # pre_in1 = ... + # in1 = transform(pre_in1) + # ag_bucket(in0+in1) + # wait_bucket + # wait_ag0 = wait_bucket[0] + # wait_ag1 = wait_bucket[1] + # user1(wait_ag1) + stable_topological_sort(gm.graph) + + # Apply overlap scheduling if enabled + if config.aten_distributed_optimizations.enable_overlap_scheduling: + from torch._inductor.fx_passes.overlap_scheduling import ( + schedule_overlap_bucketing_from_inductor_configs, + ) + + overlap_deps = config.aten_distributed_optimizations.insert_overlap_deps + fusion_regions = config.aten_distributed_optimizations.enable_fusion_regions + + # by default, insert overlap deps and enable fusion regions within inductor + with config.patch( + { + "aten_distributed_optimizations.insert_overlap_deps": ( + True if overlap_deps is None else overlap_deps + ), + "aten_distributed_optimizations.enable_fusion_regions": ( + True if fusion_regions is None else fusion_regions + ), + } + ): + GraphTransformObserver(gm, "overlap_scheduling").apply_graph_pass( + lambda graph: schedule_overlap_bucketing_from_inductor_configs( + graph.owning_module, + ) + ) + + # Keep these last, since they introduce mutation. Look at + # ./fx_passes/README.md for a discussion of mutation invariants. + GraphTransformObserver(gm, "reinplace_inplaceable_ops").apply_graph_pass( + functools.partial(reinplace_inplaceable_ops, fake_tensor_updater), + ) + GraphTransformObserver( + gm, "decompose_triton_kernel_wrapper_functional" + ).apply_graph_pass(decompose_triton_kernel_wrapper_functional) + GraphTransformObserver(gm, "decompose_auto_functionalized").apply_graph_pass( + decompose_auto_functionalized + ) + if not torch._dynamo.config.skip_fsdp_hooks: + GraphTransformObserver(gm, "reinplace_fsdp_all_gather").apply_graph_pass( + comms.reinplace_fsdp_all_gather + ) + GraphTransformObserver(gm, "decompose_scan_to_while_loop").apply_gm_pass( + decompose_scan_to_while_loop + ) + GraphTransformObserver(gm, "decompose_map_to_while_loop").apply_gm_pass( + decompose_map_to_while_loop + ) + + gm.recompile() + gm.graph.lint() + + +def prepare_softmax_pattern(x, dim): + xmax = x.amax(dim=dim, keepdim=True) + xsub = x - xmax + xexp = xsub.exp() + xsum = xexp.sum(dim=dim, keepdim=True) + return xmax, xsum, xsub, xexp + + +def prepare_softmax_replacement(x, dim): + """ + Return xsub since otherwise log-softmax can not be matched + due to a use of this intermediate node. Same reason to return + xsub.exp() for softmax. + """ + from torch._inductor.inductor_prims import prepare_softmax_online + + xmax, xsum = prepare_softmax_online(x, dim) + xsub = x - xmax + return xmax, xsum, xsub, xsub.exp() + + +def prepare_softmax_extra_check(match): + """ + We only have triton online softmax kernels currently. + """ + device_type = match.kwargs["x"].meta["val"].device.type + return ( + config.online_softmax + and device_type in ["cuda", "xpu"] + and getattr(config, f"{device_type}_backend") == "triton" + ) + + +def decompose_map_to_while_loop(gm: torch.fx.GraphModule): + """This is similar to decompose_scan_to_while_loop.""" + graph_pass = PatternMatcherPass() + + @register_graph_pattern( + CallFunctionVarArgs(torch.ops.higher_order.map_impl), + # pyrefly: ignore [bad-argument-type] + pass_dict=graph_pass, + ) + def _(match: Match, *args, **kwargs): + assert len(kwargs) == 0, ( + "kwargs of map are not merged into args before entering decompose_map_to_while_loop_pass" + ) + subgraph, fx_xs, fx_additional_inputs = args + sub_gm: torch.fx.GraphModule = getattr(gm, subgraph.target) + cur_node = match.nodes[0] + mapped_outputs = cur_node.meta["val"] + + def lower_to_while_loop(*args, **kwargs): + assert len(kwargs) == 0 + xs, additional_inputs = pytree.tree_unflatten(args, tree_spec) + assert isinstance(xs, (tuple, list)) and isinstance( + additional_inputs, (tuple, list) + ), (xs, additional_inputs) + map_length = xs[0].size(0) + loop_idx = torch.zeros([], dtype=torch.int64, device=torch.device("cpu")) + + # Similar to NOTE [Pre-allocate scan's output buffer] + bound_symbols = { + arg.node.expr: arg + for arg in pytree.tree_leaves((args, map_length)) + if isinstance(arg, torch.SymInt) + } + out_buffers = [ + torch.empty_strided( + resolve_shape_to_proxy(out.size(), bound_symbols), + resolve_shape_to_proxy(out.stride(), bound_symbols), + device=out.device, + dtype=out.dtype, + layout=out.layout, + requires_grad=out.requires_grad, + ) + for out in mapped_outputs + ] + + while_loop_operands = (loop_idx, out_buffers, xs) + while_loop_flat_operands, operands_spec = pytree.tree_flatten( + while_loop_operands + ) + while_loop_additional_inputs = additional_inputs + _, operands_and_additional_inputs_spec = pytree.tree_flatten( + (*while_loop_operands, additional_inputs) + ) + + def cond_fn(*flat_args): + loop_idx, _, _, _ = pytree.tree_unflatten( + flat_args, + operands_and_additional_inputs_spec, + ) + return loop_idx < map_length + + def body_fn(*flat_args): + loop_idx, out_bufs, xs, additional_inputs = pytree.tree_unflatten( + flat_args, + operands_and_additional_inputs_spec, + ) + + idx_int = loop_idx.item() + torch.ops.aten._assert_scalar.default(idx_int >= 0, "") + torch.ops.aten._assert_scalar.default(idx_int < map_length, "") + sub_xs = [torch.ops.aten.select.int(x, 0, idx_int) for x in xs] + outs = sub_gm(*sub_xs, *additional_inputs) + + for out, buffer in zip(outs, out_bufs): + buffer_slice = torch.ops.aten.select.int(buffer, 0, idx_int) + buffer_slice.copy_(out) + return loop_idx + 1, *out_bufs, *xs + + _, final_out, _ = pytree.tree_unflatten( + torch.ops.higher_order.while_loop( + cond_fn, + body_fn, + tuple(while_loop_flat_operands), + tuple(while_loop_additional_inputs), + ), + operands_spec, + ) + return (final_out,) + + lower_to_while_loop_args, tree_spec = pytree.tree_flatten( + (fx_xs, fx_additional_inputs) + ) + match.replace_by_example( + lower_to_while_loop, lower_to_while_loop_args, run_functional_passes=False + ) + + graph_pass.apply(gm) + + for _node in gm.graph.find_nodes( + op="call_function", target=torch.ops.higher_order.map_impl + ): + raise AssertionError("map is not lowered to while_loop") + + +def resolve_shape_to_proxy( + shape: list[int | torch.SymInt], bound_symbols: dict[Any, Any] +): + """ + Given a list of symints/ints, this function returns a calculated expression of bound_symbols' values. + When we trace this function, we'll get a graph with call_function nodes that describes how the shape expr is + computed from bound_symbols' values. + + Suppose shape = (s1*s2, s1+s2) and bound_symbols = {s1: arg0, s2: arg1}, the result will be + (arg0 * arg1, arg0 + arg1). + """ + from torch.utils._sympy.interp import sympy_interp + from torch.utils._sympy.reference import PythonReferenceAnalysis + + ret = [] + for s in shape: + if isinstance(s, torch.SymInt): + ret.append( + sympy_interp( + PythonReferenceAnalysis, + bound_symbols, + s.node.expr, + ), + ) + else: + assert isinstance(s, int) + ret.append(s) + return ret + + +def decompose_scan_to_while_loop(gm: torch.fx.GraphModule): + """ + NOTE [decompose scan to while_loop] + This pass decomposes `scan` to `while_loop` by replacing the scan fx_node with a while_loop hop. + + Suppose we have a function f: + + def f(): + init = torch.zeros([]) + xs = torch.arange(4) + ys = [] + for i in range(xs.size(0)): + init = xs[i] + init + ys.append(init) + + # Return the final carry and stack the intermediates + return init, torch.stack(ys) + + We could rewrite it with a scan with the benefits of reducing compilation time/binary size, reducing + memory usage, supporting loops over unbacked shapes and cudagraph etc. + + def g(): + def step_fn(init: torch.Tensor, x: torch.Tensor): + next_init = x + init + return next_init, next_init + + init = torch.zeros([]) + xs = torch.arange(4) + final_carry, ys = torch._higher_order.scan(step_fn, init, xs) + return final_carry, ys + + This pass will rewrite scan into: + + def k(): + init = torch.zeros([]) + xs = torch.arange(4) + + # we create a loop_idx and loop through xs.shape[0] + loop_idx = torch.zeros([]) + ys = torch.empty_strided(_shape_stride_of_ys) + def cond_fn(loop_idx, ys, init, xs): + return loop_idx < xs.shape[0] + + # we pre-allocate the output buffer ys and inplace + # copy the y of each intermediate into a slice. + # NOTE [Pre-allocate scan's output buffer]. + def body_fn(loop_idx, ys, init, xs): + int_idx = loop_idx.item() + next_init, y = step_fn(init, xs[int_idx]) + ys[int_idx].copy_(y) + return loop_idx + 1, ys, next_init, xs + + final_carry, _, _, ys = torch._higher_order.while_loop(cond_fn, body_fn, (loop_idx, ys, init, xs)) + return final_carry, ys + """ + + graph_pass = PatternMatcherPass() + + @register_graph_pattern( + CallFunctionVarArgs(torch.ops.higher_order.scan), + # pyrefly: ignore [bad-argument-type] + pass_dict=graph_pass, + ) + def _(match: Match, *args, **kwargs): + from torch._higher_order_ops.scan import _extract_carry_and_out + + assert len(kwargs) == 0, ( + "kwargs of scan are not merged into args before entering decompose_scan_to_while_loop_pass" + ) + + combine_subgraph, fx_init, fx_xs, fx_additional_inputs = args + assert combine_subgraph.op == "get_attr", "first arg is not combine_subgraph" + sub_gm: torch.fx.GraphModule = getattr(gm, combine_subgraph.target) + cur_node = match.nodes[0] + num_init_leaves = len(fx_init) + _, ys_outputs = _extract_carry_and_out(cur_node.meta["val"], num_init_leaves) + + def lower_to_while_loop(*args, **kwargs): + """ + The traced graph of this function will be used to replace the original scan fx_node. + """ + assert len(kwargs) == 0 + + # Step 1: construct necessary inputs to while_loop based on scan's input. + ( + init, + xs, + additional_inputs, + ) = pytree.tree_unflatten(args, tree_spec) + scan_length = xs[0].size(0) + loop_idx = torch.zeros([], dtype=torch.int64, device=torch.device("cpu")) + + # NOTE [Pre-allocate scan's output buffer] + # In order to pre-allocate the output buffer for ys, we rely on the meta of scan's fx_node. + # However, the meta consists of concrete symints, we need to bind those symints with + # proxies in order to trace the torch.empty_strided call correctly. + # + # Also note that basic free symbols of tensor's shapes are guaranteed to be lifted as subgraph inputs + # in dynamo so we can always re-construct the sym expression from placeholders. + # See Note [Auto lift basic free symbols when create_graph_input] for how this is done. + bound_symbols = { + arg.node.expr: arg + for arg in pytree.tree_leaves((args, scan_length)) + if isinstance(arg, torch.SymInt) + } + ys_outs = [ + torch.empty_strided( + resolve_shape_to_proxy(ys_out.size(), bound_symbols), + resolve_shape_to_proxy(ys_out.stride(), bound_symbols), + device=ys_out.device, + dtype=ys_out.dtype, + layout=ys_out.layout, + requires_grad=ys_out.requires_grad, + ) + for ys_out in ys_outputs + ] + + while_loop_operands = (loop_idx, ys_outs, init, xs) + flat_operands, operands_spec = pytree.tree_flatten(while_loop_operands) + _, operands_and_additional_inputs_spec = pytree.tree_flatten( + (*while_loop_operands, additional_inputs) + ) + + # Step 2: create the cond_fn and body_fn for while_loop + def cond_fn(*flat_args): + loop_idx, _, _, _, _ = pytree.tree_unflatten( + flat_args, operands_and_additional_inputs_spec + ) # type: ignore[has-type] + return loop_idx < scan_length # type: ignore[has-type] + + def body_fn(*flat_args): + loop_idx, ys_outs, carry, xs, additional_inputs = pytree.tree_unflatten( + flat_args, + operands_and_additional_inputs_spec, # type: ignore[has-type] + ) + + idx_int = loop_idx.item() + torch.ops.aten._assert_scalar.default(idx_int >= 0, "") + torch.ops.aten._assert_scalar.default(idx_int < scan_length, "") + sub_xs = [torch.ops.aten.select.int(x, 0, idx_int) for x in xs] + next_carry, ys = _extract_carry_and_out( + sub_gm(*(list(carry) + sub_xs + list(additional_inputs))), + num_init_leaves, + ) + for y, y_out in zip(ys, ys_outs): + y_out_slice = torch.ops.aten.select.int(y_out, 0, idx_int) + y_out_slice.copy_(y) + return loop_idx + 1, *ys_outs, *next_carry, *xs + + # Step 3: call the while_loop operator + _, ys_outs, last_carry, _ = pytree.tree_unflatten( + torch.ops.higher_order.while_loop( + cond_fn, + body_fn, + tuple(flat_operands), + tuple(additional_inputs), + ), + operands_spec, + ) + return list(last_carry) + list(ys_outs) + + lower_to_while_loop_args, tree_spec = pytree.tree_flatten( + ( + fx_init, + fx_xs, + fx_additional_inputs, + ) + ) + match.replace_by_example( + lower_to_while_loop, + lower_to_while_loop_args, + run_functional_passes=False, + ) + + graph_pass.apply(gm) + + for _node in gm.graph.find_nodes( + op="call_function", target=torch.ops.higher_order.scan + ): + raise AssertionError("scan is not lowered to while_loop") + + +@init_once_fakemode +def lazy_init(input_device: torch.device | None = None): + if torch._C._has_mkldnn: + from . import decompose_mem_bound_mm # noqa: F401 + from .mkldnn_fusion import _mkldnn_fusion_init + + _mkldnn_fusion_init() + else: + from .quantization import _register_woq_lowerings + + _register_woq_lowerings() + + # Put this patterns in post-grad pass rather than joint-graph + # pass since otherwise there will be perf/peak-memory regression: + # https://github.com/pytorch/pytorch/issues/148141 + register_replacement( + # pyrefly: ignore [bad-argument-type] + prepare_softmax_pattern, + # pyrefly: ignore [bad-argument-type] + prepare_softmax_replacement, + [torch.empty(4, 8)], + scalar_workaround=dict(dim=-1), + # pyrefly: ignore [bad-argument-type] + trace_fn=fwd_only, + # pyrefly: ignore [bad-argument-type] + pass_dicts=pass_patterns[1], + extra_check=prepare_softmax_extra_check, + ) + + +def reorder_for_locality(graph: torch.fx.Graph): + if torch.distributed.is_available(): + + def check(): + # This is a wait node, and `other_node`` is some collective node. + # Eager semantics allow waits to be issued in a different order than + # the collectives. Reordering this wait node might reorder collectives + # which cause hangs. Once we have SPMD mode, we can safely reorder them. + # However, increasing the locality between a collective and its wait node + # is generally worse for performance. + return node.target != torch.ops._c10d_functional.wait_tensor.default + else: + + def check(): + return True + + def consumes_rng_state(node: torch.fx.Node) -> bool: + return ( + node.op == "call_function" + and isinstance(node.target, torch._ops.OpOverload) + and torch.Tag.nondeterministic_seeded in node.target.tags + ) + + def visit(other_node): + if ( + other_node.op == "call_function" + and other_node.target != operator.getitem + and all((n in seen_nodes) for n in other_node.users) + and get_mutation_region_id(graph, node) + == get_mutation_region_id(graph, other_node) + and check() + ): + # Ops that consume RNG state are order-sensitive and must not be + # reordered during locality optimization. + if consumes_rng_state(other_node): + return + + # move node's producers right before it + node.prepend(other_node) + + seen_nodes = OrderedSet[torch.fx.Node]() + + # only reorder nodes before the first copy_ in the graph. + # copy_ will appear at the end of functionalized graphs when there is mutation on inputs, + # and this reordering doesn't work well with mutation + first_copy = next( + iter(graph.find_nodes(op="call_function", target=torch.ops.aten.copy_.default)), + None, + ) + past_mutating_epilogue = first_copy is None + + for node in reversed(graph.nodes): + seen_nodes.add(node) + if not past_mutating_epilogue: + past_mutating_epilogue = node is first_copy + continue + + torch.fx.map_arg((node.args, node.kwargs), visit) + + +def register_lowering_pattern( + pattern, extra_check=_return_true, pass_number=1 +) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]: + """ + Register an aten to inductor IR replacement pattern + """ + return pattern_matcher.register_lowering_pattern( + pattern, + extra_check, + # pyrefly: ignore [bad-argument-type] + pass_dict=pass_patterns[pass_number], + ) + + +################################################################################ +# Actual patterns below this point. +# Priority of patterns is: +# - later output nodes first +# - order patterns are defined in +################################################################################ + + +def is_valid_mm_plus_mm(match: Match): + if not (config.max_autotune or config.max_autotune_gemm): + return False + + # Check if all required values exist + mat1_val = match.kwargs["mat1"].meta.get("val") + mat2_val = match.kwargs["mat2"].meta.get("val") + mat3_val = match.kwargs["mat3"].meta.get("val") + mat4_val = match.kwargs["mat4"].meta.get("val") + + if mat1_val is None or mat2_val is None or mat3_val is None or mat4_val is None: + return False + + *_b1, m1, k1 = mat1_val.shape + *_b2, k2, n1 = mat2_val.shape + if k1 != k2: + return False + + *_b1, m2, k3 = mat3_val.shape + *_b2, k4, n2 = mat4_val.shape + if k3 != k4: + return False + + if m1 != m2 or n1 != n2: + return False + + return True + + +@register_lowering_pattern( + CallFunction( + aten.add, + CallFunction(aten.mm, KeywordArg("mat1"), KeywordArg("mat2")), + CallFunction(aten.mm, KeywordArg("mat3"), KeywordArg("mat4")), + ), + extra_check=is_valid_mm_plus_mm, +) +def mm_plus_mm(match: Match, mat1, mat2, mat3, mat4): + return inductor.kernel.mm_plus_mm.tuned_mm_plus_mm(mat1, mat2, mat3, mat4) + + +@register_graph_pattern( + CallFunction( + aten.cumsum.default, + CallFunction( + torch.ops.aten.full.default, + KeywordArg("shape"), + KeywordArg("fill_value"), + dtype=KeywordArg("dtype"), + layout=Ignored(), + device=KeywordArg("device"), + pin_memory=False, + _users=MULTIPLE, + ), + KeywordArg("dim"), + _users=MULTIPLE, + ), + # pyrefly: ignore [bad-argument-type] + pass_dict=pass_patterns[1], +) +def pointless_cumsum_replacement(match: Match, shape, fill_value, device, dtype, dim): + """Based on a pattern in OPTForCausalLM""" + + if is_integer_dtype(dtype) or is_boolean_dtype(dtype): + # cumsum promotes all integral types to int64 + dtype = torch.int64 + + def repl(*shape): + dim_size = shape[dim] + idx = torch.arange(1, dim_size + 1, device=device, dtype=dtype) + + inter_shape = [1] * len(shape) + inter_shape[dim] = dim_size + return (idx * fill_value).view(inter_shape).expand(shape) + + # only replace the output node, not all nodes + match.nodes = [match.output_node()] + # pyrefly: ignore [bad-argument-type] + match.replace_by_example(repl, list(shape)) + + +_cat_1 = CallFunction(aten.cat, Arg(), 1, _users=2) + + +@register_lowering_pattern( + CallFunction( + aten.cat, + [ + _cat_1, + CallFunction( + aten.slice, + _cat_1, + 1, + 0, + KeywordArg("size"), + ), + ], + 1, + ) +) +def cat_slice_cat(match, cat_input, size, dim=1): + """ + This is an example of a more complex pattern where cat_1 is used + multiple times inside the pattern. We fold 2 calls to cat into one. + + Matches: + cat_1: f32[1024, 4077] = torch.ops.aten.cat.default([add_26, primals_217], 1) + slice_1: f32[1024, 4077] = torch.ops.aten.slice.Tensor(cat_1, 0, 0, 9223372036854775807) + slice_2: f32[1024, 19] = torch.ops.aten.slice.Tensor(slice_1, 1, 0, 19) + cat_2: f32[1024, 4096] = torch.ops.aten.cat.default([cat_1, slice_2], 1) + + + Rewrite to: + slice_2 = torch.ops.aten.slice.Tensor(add_26, 1, 0, 19) + cat_2 = torch.ops.aten.cat.default([add_26, primals_217, slice2], 1) + """ + first, *rest = cat_input + # Optimization is optional, because we can just not fold the cat + # size should be within first.get_size()[dim] such that the optimization is valid. + # For negative `end`, we currently fallback to not optimizing. + if size >= 0 and V.graph.sizevars.statically_known_leq(size, first.get_size()[dim]): + # fold 2 cats into 1 cat + return L[aten.cat]( + [ + first, + *rest, + L[aten.slice](first, dim, 0, size), + ], + dim, + ) + else: + # don't expect to hit this case, just fall back + tmp = L[aten.cat](cat_input, dim) + return L[aten.cat]( + [ + tmp, + L[aten.slice](tmp, dim, 0, size), + ], + dim, + ) + + +def is_valid_splitwithsizes_cat(match): + split_nodes = filter_nodes(match.nodes, aten.split_with_sizes) + cat_nodes = filter_nodes(match.nodes, aten.cat) + get_item_nodes = filter_nodes(match.nodes, operator.getitem) + if len(split_nodes) != 1 or len(cat_nodes) != 1: + return False + split_node, cat_node = split_nodes[0], cat_nodes[0] + # The dim of split and cat should match for passthrough + if get_arg_value(split_node, 2, "dim") != get_arg_value(cat_node, 1, "dim"): + return False + get_item_args = OrderedSet( + get_arg_value(get_item_node, 1) for get_item_node in get_item_nodes + ) + assert None not in get_item_args + split_sizes = get_arg_value(split_node, 1, "split_sizes") + # All parts of split should be included in the cat + if get_item_args != OrderedSet(range(len(split_sizes))): + return False + # The order of get_item_args should same with cat_node used. + # For example, if the split_node like split_with_sizes(input, [2, 2, 3], 1), + # the cat node should be like cat([get_item(0), get_item(1), get_item(2)], 1). + cat_items_args_order = [ + get_arg_value(item_node, 1) for item_node in get_arg_value(cat_node, 0) + ] + if cat_items_args_order != list(range(len(split_sizes))): + return False + + return True + + +def same_meta(node1: torch.fx.Node, node2: torch.fx.Node): + """True if two nodes have the same metadata""" + val1 = node1.meta.get("val") + val2 = node2.meta.get("val") + return ( + val1 is not None + and val2 is not None + and statically_known_true(sym_eq(val1.size(), val2.size())) + and val1.layout == val2.layout + and val1.dtype == val2.dtype + and val1.device == val2.device + and ( + val1.layout != torch.strided + or statically_known_true(sym_eq(val1.stride(), val2.stride())) + ) + # Check conjugate and negative bits - a clone that resolves these is not a no-op + and val1.is_conj() == val2.is_conj() + and val1.is_neg() == val2.is_neg() + ) + + +noop_registry: dict[Any, Any] = {} + + +def register_noop_decomp(targets, nop_arg=0): + def register_fun(cond): + register_decomposition(targets, registry=noop_registry, unsafe=True)( + (cond, nop_arg) # type: ignore[arg-type] + ) + return cond + + return register_fun + + +def _needs_spmd_graph_preservation() -> bool: + """Check if SPMD graph preservation is needed for distributed overlap.""" + return ( + config.aten_distributed_optimizations.enable_overlap_scheduling + or config.reorder_for_compute_comm_overlap + ) + + +@register_noop_decomp(aten.slice) +def slice_noop(self, dim=0, start=None, end=None, step=1): + if _needs_spmd_graph_preservation(): + # Keep no-op slices so all ranks produce identical FX graphs (SPMD) + # with matching op counts and runtime estimations. + return False + if start is None or end is None: + return False + + slice_dim_size = self.shape[dim] + if ( + statically_known_true(sym_eq(start, 0)) + and ( + statically_known_true(end >= 2**63 - 1) + or statically_known_true(end >= slice_dim_size) + ) + and statically_known_true(sym_eq(step, 1)) + ): + return True + return False + + +@register_noop_decomp(aten.slice_scatter, 1) +def slice_scatter_noop(self, src, dim=0, start=None, end=None, step=1): + if start is None: + start = 0 + if end is None: + end = 2**63 - 1 + slice_scatter_dim_size = self.shape[dim] + if ( + self.shape == src.shape + and start == 0 + and ( + statically_known_true(end >= 2**63 - 1) + or statically_known_true(end >= slice_scatter_dim_size) + ) + and step == 1 + ): + return True + return False + + +@register_noop_decomp(aten.repeat) +def repeat_noop(self, repeats): + return all(r == 1 for r in repeats) + + +@register_noop_decomp(aten.constant_pad_nd) +def constant_pad_nd(x, padding, fill_value=0): + if _needs_spmd_graph_preservation(): + # Keep no-op pads so all ranks produce identical FX graphs (SPMD) + # with matching op counts and runtime estimations. + return False + return all(p == 0 for p in padding) + + +@register_noop_decomp(torch.ops.prims.convert_element_type) +def convert_element_type_noop(x, dtype: torch.dtype): + return x.dtype == dtype + + +@register_noop_decomp(torch.ops.prims.device_put) +def device_put_noop(x, device, non_blocking=True): + return x.device == decode_device(device) + + +@register_noop_decomp([aten.ceil, aten.floor, aten.round, aten.trunc]) +def int_noop(x): + return is_integer_dtype(x.dtype) + + +@register_noop_decomp([aten.pow]) +def pow_noop(a, b): + return isinstance(b, int) and b == 1 + + +@register_noop_decomp([aten.cat], lambda args: args[0][0]) +def cat_noop(inputs, dim=0): + return len(inputs) == 1 + + +@register_noop_decomp(aten.view.default) +def view_default_noop(arg, size): + return statically_known_true(sym_eq(arg.shape, tuple(size))) + + +@register_noop_decomp(aten.view.dtype) +def view_dtype_noop(arg, dtype): + return arg.dtype == dtype + + +# Note, we also always have a check for identical metadata, which is why these +# are safe +@register_noop_decomp([aten.copy], nop_arg=1) +@register_noop_decomp([aten.alias, aten.clone]) +def true_noop(*args, **kwargs): + return True + + +def remove_noop_ops(graph: torch.fx.Graph): + """ + Removes both operations that are essentially aten.clone and operations that are essentially aten.alias from the graph. + """ + inputs = OrderedSet[torch.fx.Node]() + input_storages = OrderedSet[int | None]() + output_storages = OrderedSet[int | None]() + + for node in graph.find_nodes(op="placeholder"): + inputs.add(node) + input_storages.add(get_node_storage(node)) + + output_node = next(iter(reversed(graph.nodes))) + assert output_node.op == "output" + outputs = output_node.args[0] + if not isinstance(outputs, (list, tuple)): + # nested subgraphs can have singleton outputs + outputs = (outputs,) + for out in outputs: + if isinstance(out, torch.fx.Node): + output_storages.add(get_node_storage(out)) + + for node in graph.nodes: + if node.target in noop_registry: + cond, src_index = noop_registry[node.target] + if isinstance(src_index, int): + src = node.args[src_index] + else: + src = src_index(node.args) + if not isinstance(src, torch.fx.Node): + continue + # Don't introduce new aliasing between inputs and outputs. + # See fx_passes/README.md for a discussion of why this is + # necessary. + node_storage = get_node_storage(node) + src_storage = get_node_storage(src) + node_is_view = node_storage == src_storage + if ( + not node_is_view + and node_storage in output_storages + and (src_storage in input_storages or src_storage in output_storages) + ): + continue + + # Even if input and outputs are expected to alias, + # don't make "node is src" True + if ( + node_is_view + and node in output_node.args + and (src in inputs or src in output_node.args) + ): + continue + + is_valid, args, kwargs = get_fake_args_kwargs(node) + if not is_valid: + continue + if same_meta(node, src) and cond(*args, **kwargs): + node.replace_all_uses_with(src) + graph.erase_node(node) + + +def remove_assert_ops(graph: torch.fx.Graph): + """ + Removes aten._assert_tensor_metadata.default op because + 1) it will be lowered to a no-op in inductor + 2) it can block fusion, such as unfuse_bias_add_to_pointwise fusion. + + This op could come from aten.to functionalization in export. + + For example, if we have a graph like below + + %addmm = aten.addmm.default(%linear_bias, %arg3_1, %permute) + %_assert_tensor_metadata = aten._assert_tensor_metadata.default(%addmm, None, None, torch.float16) + %convert_element_type_3 = prims.convert_element_type.default(%addmm, torch.float32) + %pow_1 = aten.pow.Tensor_Scalar(%convert_element_type_3, 2) + + We still want to fuse add from addmm with pow, instead of fusing add with mm, according to unfuse_bias_add_to_pointwise fusion. + + However, aten._assert_tensor_metadata.default is not a pointwise op, and would fail the should_prefer_unfused_addmm check. + + We remove this op so it doesn't block fusion decisions. It's safe because this op is lowered to a no-op with @register_lowering. + + """ + for node in graph.find_nodes( + op="call_function", target=torch.ops.aten._assert_tensor_metadata.default + ): + graph.erase_node(node) + + +def apply_pass_to_subgraphs(pass_fn: Callable[[fx.Graph], None], graph: fx.Graph): + """Recursively apply a pass function to all subgraphs referenced by get_attr nodes.""" + gm = graph.owning_module + if gm is None: + return + subgraph_names: OrderedSet[str] = OrderedSet( + x.target for x in graph.find_nodes(op="get_attr") + ) + for child_name, child_mod in gm.named_children(): + if child_name in subgraph_names and isinstance(child_mod, torch.fx.GraphModule): + pass_fn(child_mod.graph) + + +def decompose_triton_kernel_wrapper_functional(graph): + """Decomposes triton_kernel_wrapper_functional nodes into clones and the underlying + mutation node. + + We assume that the reinplacing pass runs before this; the reinplacing pass + tells us (via rewriting the arguments or .meta to those nodes) which + Tensors we should clone and which Tensors are safe to reinplace. + """ + # First, recursively process any subgraphs + apply_pass_to_subgraphs(decompose_triton_kernel_wrapper_functional, graph) + + graph_pass = PatternMatcherPass() + + @register_graph_pattern( + CallFunctionVarArgs(torch.ops.higher_order.triton_kernel_wrapper_functional), + # pyrefly: ignore [bad-argument-type] + pass_dict=graph_pass, + ) + def _(match: Match, *args, **kwargs): + from torch._higher_order_ops.triton_kernel_wrap import ( + triton_kernel_wrapper_functional_dense, + ) + + flat_args, spec = pytree.tree_flatten((args, kwargs)) + + # NB: we combine (args, kwargs) into flat args for replacing. + # This is replace_by_example uses make_fx which does not support + # tracing a function with kwargs. + def decomp(*flat_args): + args, kwargs = pytree.tree_unflatten(flat_args, spec) + return (triton_kernel_wrapper_functional_dense(*args, **kwargs),) + + # pyrefly: ignore [bad-argument-type] + match.replace_by_example(decomp, flat_args, run_functional_passes=False) + + graph_pass.apply(graph) + + for _ in graph.find_nodes( + op="call_function", + target=torch.ops.higher_order.triton_kernel_wrapper_functional, + ): + raise AssertionError("triton_kernel_wrapper_functional was not removed") + + +def decompose_auto_functionalized(graph): + """Decomposes auto_functionalized nodes into clones and the underlying + mutation node. + + We assume that the reinplacing pass runs before this; the reinplacing pass + tells us (via rewriting the arguments or .meta to those nodes) which + Tensors we should clone and which Tensors are safe to reinplace. + """ + graph_pass = PatternMatcherPass() + + @register_graph_pattern( + CallFunctionVarArgs(torch.ops.higher_order.auto_functionalized), + # pyrefly: ignore [bad-argument-type] + pass_dict=graph_pass, + ) + def _(match: Match, *args, **kwargs): + from torch._higher_order_ops.auto_functionalize import auto_functionalized_dense + + only_clone_these_tensors = tuple( + match.nodes[0].meta.get("only_clone_these_tensors", []) + ) + + flat_args, spec = pytree.tree_flatten((args, kwargs)) + + # NB: we combine (args, kwargs) into flat args for replacing. + # This is replace_by_example uses make_fx which does not support + # tracing a function with kwargs. + def decomp(*flat_args): + args, kwargs = pytree.tree_unflatten(flat_args, spec) + assert len(args) == 1 + mode = args[0] + return auto_functionalized_dense(mode, only_clone_these_tensors, **kwargs) + + # pyrefly: ignore [bad-argument-type] + match.replace_by_example(decomp, flat_args, run_functional_passes=False) + + @register_graph_pattern( + CallFunctionVarArgs(torch.ops.higher_order.auto_functionalized_v2), + # pyrefly: ignore [bad-argument-type] + pass_dict=graph_pass, + ) + def _(match: Match, *args, **kwargs): + from torch._higher_order_ops.auto_functionalize import ( + auto_functionalized_v2_dense, + ) + + only_clone_these_bases = tuple( + match.nodes[0].meta.get("only_clone_these_tensors", []) + ) + + flat_args, spec = pytree.tree_flatten((args, kwargs)) + + def _maybe_resolve_constant_get_attr(node): + # Resolve getattr node to its value because they don't always have meta["val"] + if ( + isinstance(node, torch.fx.Node) + and node.op == "get_attr" + and "val" not in node.meta + ): + const_attr = getattr(graph.owning_module, node.target) # type: ignore[arg-type] + assert isinstance( + const_attr, (torch.fx.GraphModule, pytree.TreeSpec) + ), (type(const_attr), const_attr) + return const_attr + return node + + flat_args = [_maybe_resolve_constant_get_attr(arg) for arg in flat_args] + + # NB: we combine (args, kwargs) into flat args for replacing. + # This is replace_by_example uses make_fx which does not support + # tracing a function with kwargs. + def decomp(*flat_args): + args, kwargs = pytree.tree_unflatten(flat_args, spec) + assert len(args) == 1 + mutable_op = args[0] + return auto_functionalized_v2_dense( + mutable_op, only_clone_these_bases, **kwargs + ) + + # pyrefly: ignore [bad-argument-type] + match.replace_by_example(decomp, flat_args, run_functional_passes=False) + + graph_pass.apply(graph) + + # Remove unused get_attr nodes and their corresponding attributes from the graph module. + # When auto_functionalizing a hop, we need to clean up get_attr nodes for _constant_schema + # and the auto_functionalized graph module that are no longer referenced. + unused_get_attr_nodes = [] + removable_attrs: OrderedSet[torch.fx.node.Target] = OrderedSet() + protected_attrs: OrderedSet[torch.fx.node.Target] = OrderedSet() + + # First pass: identify unused get_attr nodes and track attribute usage + for node in graph.nodes: + if node.op != "get_attr": + continue + + if len(node.users) == 0: + # Node is unused, mark for removal + unused_get_attr_nodes.append(node) + + # Check if the attribute can be removed from the module + if ( + hasattr(graph.owning_module, node.target) + and isinstance( + getattr(graph.owning_module, node.target), torch.fx.GraphModule + ) + and node.target not in protected_attrs + ): + removable_attrs.add(node.target) + else: + # Node is used, protect its attribute from removal + if node.target in removable_attrs: + removable_attrs.remove(node.target) + protected_attrs.add(node.target) + + # Second pass: clean up unused nodes and attributes + for node in unused_get_attr_nodes: + graph.erase_node(node) + + for attr_name in removable_attrs: + assert isinstance(attr_name, str) + delattr(graph.owning_module, attr_name) + + graph.lint() + + for _ in graph.find_nodes( + op="call_function", target=torch.ops.higher_order.auto_functionalized + ): + raise AssertionError("auto_functionalized was not removed") + + for _ in graph.find_nodes( + op="call_function", target=torch.ops.higher_order.auto_functionalized_v2 + ): + raise AssertionError("auto_functionalized_v2 was not removed") + + +@register_lowering_pattern( + CallFunction( + aten.cat, + ListOf( + CallFunction( + operator.getitem, + CallFunction( + aten.split_with_sizes, + KeywordArg("input_"), + Ignored(), + Ignored(), + _users=MULTIPLE, + ), + Ignored(), + ), + ), + Ignored(), + ), + pass_number=2, + extra_check=is_valid_splitwithsizes_cat, +) +def splitwithsizes_cat_replace(match, input_): + return input_ + + +def is_valid_cat_splitwithsizes(match): + cat_nodes = filter_nodes(match.nodes, aten.cat) + split_nodes = filter_nodes(match.nodes, aten.split_with_sizes) + if len(split_nodes) != 1 or len(cat_nodes) != 1: + return False + split_node, cat_node = split_nodes[0], cat_nodes[0] + + # the cat node has other users: can't eliminate + if len(cat_node.users) > 1: + return False + + # the dim of the cat and split should match + dim = get_arg_value(split_node, 2, "dim") + if dim != get_arg_value(cat_node, 1, "dim"): + return False + + cat_inputs = list(get_arg_value(cat_node, 0)) + split_sizes = get_arg_value(split_node, 1, "split_sizes") + # the number of input tensors in cat and the + # length of the split sizes should match + if len(cat_inputs) != len(split_sizes): + return False + + for cat_input, split_size in zip(cat_inputs, split_sizes): + # each cat input tensor's size along dim + # should match the corresponding split size + if "val" not in cat_input.meta: + return False + cat_input_size = cat_input.meta["val"].size(dim) + if cat_input_size != split_size: + return False + + return True + + +@register_lowering_pattern( + CallFunction( + aten.split_with_sizes, + CallFunction( + aten.cat, + KeywordArg("input_"), + Ignored(), + _users=MULTIPLE, + ), + Ignored(), + Ignored(), + ), + pass_number=2, + extra_check=is_valid_cat_splitwithsizes, +) +def cat_splitwithsizes_replace(match, input_): + return input_ + + +def view_to_reshape(gm): + """ + Replace view ops in the GraphModule to reshape ops. + """ + + def _view_to_reshape_graph(graph): + for nd in graph.find_nodes( + op="call_function", target=torch.ops.aten.view.default + ): + nd.target = torch.ops.aten.reshape.default + + def _recursive_view_to_reshape(graph): + apply_pass_to_subgraphs(_recursive_view_to_reshape, graph) + _view_to_reshape_graph(graph) + + _recursive_view_to_reshape(gm.graph) + + +def should_prefer_unfused_addmm(match): + inp = match.kwargs["inp"] + if not is_gpu(inp.meta["val"].device.type): + return False + + output = match.output_node() + return all(is_pointwise_use(use) for use in output.users) + + +@register_graph_pattern( + CallFunction( + aten.addmm, + KeywordArg("inp"), + Arg(), + Arg(), + beta=KeywordArg("beta"), + alpha=KeywordArg("alpha"), + ), + # pyrefly: ignore [bad-argument-type] + pass_dict=pass_patterns[2], + extra_check=should_prefer_unfused_addmm, +) +def unfuse_bias_add_to_pointwise(match: Match, mat1, mat2, *, inp, alpha, beta): + if config.keep_addmm_fused_for_half_dtypes and inp.meta["val"].dtype in ( + torch.bfloat16, + torch.float16, + ): + return + + def repl(inp, x1, x2, alpha, beta): + mm_result = x1 @ x2 + if alpha != 1: + mm_result = alpha * mm_result + if beta != 1: + inp = beta * inp + return inp + mm_result + + # pyrefly: ignore [bad-argument-type] + match.replace_by_example(repl, [inp, mat1, mat2, alpha, beta]) + + +def is_valid_addmm_fusion(match): + mat1, mat2 = match.args + inp = match.kwargs["inp"] + + if not ( + isinstance(inp, torch.fx.Node) and isinstance(inp.meta["val"], torch.Tensor) + ): + return False # Input is a number + + in_shape = inp.meta["val"].shape + mm_shape = mat1.meta["val"].shape[0], mat2.meta["val"].shape[1] + matched = is_expandable_to(in_shape, mm_shape) + if not matched: + return False # Shape mismatch + + inp_dtype = inp.meta["val"].dtype + + # aten cublas integration assumes equal dtypes + if inp_dtype != mat1.meta["val"].dtype or inp_dtype != mat2.meta["val"].dtype: + return False + + return not should_prefer_unfused_addmm(match) + + +@register_graph_pattern( + CallFunction( + aten.add, + CallFunction(aten.mm, Arg(), Arg()), + KeywordArg("inp"), + ), + # pyrefly: ignore [bad-argument-type] + pass_dict=pass_patterns[2], + extra_check=is_valid_addmm_fusion, +) +@register_graph_pattern( + CallFunction( + aten.add, + KeywordArg("inp"), + CallFunction(aten.mm, Arg(), Arg()), + ), + # pyrefly: ignore [bad-argument-type] + pass_dict=pass_patterns[2], + extra_check=is_valid_addmm_fusion, +) +def addmm(match, mat1, mat2, *, inp): + def repl(inp, mat1, mat2): + return aten.addmm(inp, mat1, mat2) + + match.replace_by_example(repl, [inp, mat1, mat2]) + + +def register_partial_reduction_pattern(): + "Reuse partial reductions in complete reductions" + + # post grad equivalents + equiv_red = { + aten.amax.default: aten.max.default, + aten.amin.default: aten.min.default, + } + + # TODO: to support other reductions like sum, would need to skip + # lower precision reductions since partial output would need to be kept at fp32. + for red_op in (aten.amax.default, aten.amin.default): + inp = KeywordArg("input") + partial_reduc = CallFunction( + red_op, inp, KeywordArg("reduced_dims"), KeywordArg("keepdim") + ) + full_reduc = CallFunction([red_op, equiv_red[red_op]], inp) + + @register_graph_pattern( + MultiOutputPattern([partial_reduc, full_reduc]), + # pyrefly: ignore [bad-argument-type] + pass_dict=pass_patterns[2], + ) + def reuse_partial(match, input, reduced_dims, keepdim): + partial_red, full_red = match.output_nodes() + + # if they're small, reuse not worth it + if not statically_known_true(input.meta["val"].numel() >= 4096): + return True + + def replacement(inp: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + partial = partial_red.target(inp, reduced_dims, keepdim) + complete = full_red.target(partial) + return (partial, complete) + + counters["inductor"]["partial_reduction_reuse"] += 1 + match.replace_by_example(replacement, [input]) + + +register_partial_reduction_pattern() + + +def check_shape_cuda_and_fused_int_mm_mul_enabled(match): + return ( + config.force_fuse_int_mm_with_mul + and len(getattr(match.args[2].meta.get("val"), "shape", [])) == 2 + and getattr(match.args[2].meta.get("val"), "is_cuda", False) + ) + + +def is_index_put_and_requires_h2d_sync_for_gpu_value(node): + from torch.fx.operator_schemas import normalize_function + + if node.target not in [ + torch.ops.aten.index_put.default, + torch.ops.aten.index_put_.default, + ]: + return False + # Inductor falls back to aten.index_put_. + # index_put_ will will call nonzero() and perform a H2D sync if + # any of its indices are bool/byte tensors + # However, it will short-circuit this H2D sync and run mask_fill_ + # if the value we are putting is a cpu scalar. + # Therefore, when inductor sees an index_put_ with byte tensor indices, + # it should *not* convert the cpu scalar value into a gpu tensor. + args_, _kwargs = normalize_function(node.target, node.args, node.kwargs) # type: ignore[misc] + any_byte_bool_indices = False + indices = args_[1] + for i in indices: + if i is not None and i.meta["val"].dtype in [torch.bool, torch.int8]: + any_byte_bool_indices = True + + val = args_[2].meta["val"] + val_is_cpu_scalar = val.device.type == "cpu" and val.numel() == 1 + # If both these conditions hold, then converting the val + # to a gpu tensor will incur a H2D sync when inductor calls aten.index_put_ + return any_byte_bool_indices and val_is_cpu_scalar + + +class ConstructorMoverPass: + def __init__( + self, target: str, allow_outputs: bool = False, allow_inputs: bool = False + ) -> None: + """ + Move constructors from cpu to the target_device. + + Sweeps through the module, looking for constructor nodes that can be moved + to the target_device. + + A constructor node can be moved to the target_device iff all of its users + can also be moved (tested by cannot_be_moved). Otherwise, all dependent + constructor nodes won't be moved. + + - target: target device type + - allow_outputs: allow outputs to be moved + - allow_inputs: allow inputs to be moved + """ + + self.target = target + self.allow_inputs = allow_inputs + self.allow_outputs = allow_outputs + + assert isinstance(target, str), ( + "target should be a string representing the device type. " + f"Got: {type(target).__name__}" + ) + + def allow_cpu_device(self, node: fx.Node) -> bool: + """ + Returns whether a node that returns a tensor on the target device may have + cpu tensors as input. + """ + return node.target in ( + torch.ops.aten.index.Tensor, + torch.ops.aten.index_put.default, + torch.ops.aten.index_put_.default, + torch.ops.aten.copy.default, + torch.ops.aten.copy_.default, + torch.ops.aten.slice_scatter.default, + ) + + def is_on_target_device(self, node: fx.Node) -> bool: + """ + Returns whether a node is on the target device. + """ + node_device = self.get_node_device(node) + return node_device is not None and node_device.type == self.target + + def is_cpu_scalar_tensor(self, node: fx.Node) -> bool: + """ + Returns whether a node is a cpu scalar tensor. + """ + device = self.get_node_device(node) + is_cpu = device is not None and device.type == "cpu" + ten = node.meta.get("val") + is_scalar = isinstance(ten, torch.Tensor) and len(ten.size()) == 0 + return is_cpu and is_scalar + + def all_inputs_are_cpu_scalar_or_on_target_device(self, node: fx.Node) -> bool: + """ + Returns whether a node's inputs are either cpu scalar tensors or + on the target device. + """ + inputs = ( + inp + for inp in itertools.chain(node.args, node.kwargs.values()) + if isinstance(inp, fx.Node) + ) + return all( + self.is_cpu_scalar_tensor(inp) or self.is_on_target_device(inp) + for inp in inputs + ) + + def cannot_be_moved(self, node: fx.Node) -> bool: + """ + Returns whether a node can be moved to the target device. + + If this function returns False, it means that this node and all of its users + won't be moved into the target device. + """ + if node.target == "output": + return not self.allow_outputs + + if not ( + isinstance(node.target, torch._ops.OpOverload) + and node.target.namespace in ("prims", "aten") + ): + return True + + if is_index_put_and_requires_h2d_sync_for_gpu_value(node): + return True + + return False + + def get_node_device(self, node: fx.Node) -> torch.device | None: + """ + Get the device of a node. + """ + ten = node.meta.get("val") + return None if not isinstance(ten, torch.Tensor) else ten.device + + def get_cpu_indeg_count(self, graph: fx.Graph) -> dict[fx.Node, int]: + """ + Get the number of cpu inputs to a node + """ + cpu_indeg: dict[fx.Node, int] = Counter() + + for node in graph.nodes: + cpu_count = 0 + + def add_cpu_inp(node): + nonlocal cpu_count + device = self.get_node_device(node) + cpu_count += device is not None and device.type == "cpu" + + pytree.tree_map_only(fx.Node, add_cpu_inp, (node.args, node.kwargs)) + + # pyrefly: ignore [redundant-condition] + if cpu_count: + cpu_indeg[node] = cpu_count + + return cpu_indeg + + def __call__(self, graph: fx.Graph) -> None: + target_devices = OrderedSet[torch.device]() + constructors = [] + cpu_placeholders: OrderedSet[fx.Node] = OrderedSet() + + for node in graph.nodes: + device = self.get_node_device(node) + if device and device.type == self.target: + target_devices.add(device) + + if ( + self.allow_inputs + and node.op == "placeholder" + and self.is_cpu_scalar_tensor(node) + ): + cpu_placeholders.add(node) + constructors.append(node) + continue + + if not ( + isinstance(node.target, torch._ops.OpOverload) + and node.target.namespace in ("prims", "aten") + ): + continue + + if not torch._subclasses.fake_tensor._is_tensor_constructor(node.target): + continue + + if node.kwargs.get("device") != torch.device("cpu"): + continue + + constructors.append(node) + + # not handling multiple target devices initially + if not constructors or len(target_devices) != 1: + return + + movable_constructors = self.find_movable_constructors(graph, constructors) + + target_device = next(iter(target_devices)) + movable_cpu_placeholders = movable_constructors & cpu_placeholders + if movable_cpu_placeholders: + node = next(iter(reversed(movable_cpu_placeholders))) + last_node = node + unsqueezed_nodes = [] + for elem in movable_cpu_placeholders: + with graph.inserting_after(last_node): + unsqueezed_nodes.append( + graph.call_function(torch.ops.aten.unsqueeze.default, (elem, 0)) + ) + last_node = unsqueezed_nodes[-1] + with graph.inserting_after(last_node): + cpu_concat = graph.call_function( + torch.ops.aten.cat.default, (unsqueezed_nodes,) + ) + last_node = cpu_concat + with graph.inserting_after(last_node): + gpu_concat = graph.call_function( + torch.ops.prims.device_put.default, + (cpu_concat, target_device, True), + ) + last_node = gpu_concat + with graph.inserting_after(last_node): + gpu_split = graph.call_function( + torch.ops.aten.unbind.int, (gpu_concat,) + ) + last_node = gpu_split + for idx, node in enumerate(movable_cpu_placeholders): + with graph.inserting_after(last_node): + gpu_node = graph.call_function(operator.getitem, (gpu_split, idx)) + node.replace_all_uses_with( + gpu_node, + lambda x: x + not in [cpu_concat, gpu_concat, gpu_split, gpu_node] + + unsqueezed_nodes + and x.target != torch.ops.aten.copy_.default + and x.target != "output", + ) + last_node = gpu_node + + # noop elimination if there are other device_put for gpu_node to + # target device. Alternatively, we could just move the other device_put + # earlier in the graph, but that is not supported in fx graph yet. + noop_device_puts = [ + user + for user in gpu_node.users + if user.target is torch.ops.prims.device_put.default + and user.args[1] == target_device + ] + for noop in noop_device_puts: + noop.replace_all_uses_with(gpu_node) + graph.erase_node(noop) + + movable_constructors -= movable_cpu_placeholders + for node in movable_constructors: + kwargs = node.kwargs.copy() + kwargs["device"] = target_device + node.kwargs = kwargs + + def find_movable_constructors( + self, graph: fx.Graph, constructors: list[fx.Node] + ) -> OrderedSet[fx.Node]: + """ + Starting from the cpu constructors, iterate through the graph and test that all of their + downstream uses can safely be moved to cpu. + """ + cpu_indeg: dict[fx.Node, int] = self.get_cpu_indeg_count(graph) + + # which constructors cannot be moved to gpu + cannot_move_to_gpu = OrderedSet[fx.Node]() + + # For any node in the graph, which constructors does it have a dependency on + constructor_dependencies: dict[fx.Node, OrderedSet[fx.Node]] = defaultdict( + OrderedSet + ) + + # if a cpu node has a dependency on two different cpu constructors, + # then if either constructor cannot be moved to gpu, the other cannot as well. + # In this case any node with a dependency on one will have a dependency on the other + equal_constructor_sets: dict[fx.Node, OrderedSet[fx.Node]] = { + c: OrderedSet([c]) for c in constructors + } + + def make_dependencies_equivalent( + set1: OrderedSet[fx.Node], set2: OrderedSet[fx.Node] + ) -> OrderedSet[fx.Node]: + # could use union find but not worth complexity here + set1.update(set2) + for obj in set1: + equal_constructor_sets[obj] = set1 + return set1 + + queue: list[fx.Node] = list(constructors) + + for c in queue: + constructor_dependencies[c].add(c) + + while queue: + node = queue.pop() + dependencies = constructor_dependencies[node] + + for user in node.users: + if self.cannot_be_moved(user): + cannot_move_to_gpu.update(dependencies) + break + + # this node was used on a op which takes in multiple devices and output a gpu + # tensor. we can convert its cpu input to gpu without making further changes + if self.allow_cpu_device(user) and self.is_on_target_device(user): + del cpu_indeg[user] + elif ( + self.allow_inputs + and self.is_on_target_device(user) + and self.all_inputs_are_cpu_scalar_or_on_target_device(user) + ): + # this node takes only cpu scalar tensors or gpu tensors as inputs + # and outputs a gpu tensor. we can convert its cpu scalar inputs to gpu + # without making further changes + del cpu_indeg[user] + else: + # otherwise, we should continue look at its downstream uses + cpu_indeg[user] -= 1 + if cpu_indeg[user] == 0: + del cpu_indeg[user] + queue.append(user) + + unioned_set = make_dependencies_equivalent( + dependencies, constructor_dependencies[user] + ) + constructor_dependencies[user] = unioned_set + + for node in cpu_indeg: + if constructor_dependencies[node]: + cannot_move_to_gpu.update(constructor_dependencies[node]) + + all_cannot_move_to_gpu = cannot_move_to_gpu.copy() + for constructor in cannot_move_to_gpu: + all_cannot_move_to_gpu.update(equal_constructor_sets[constructor]) + + return OrderedSet(constructors) - all_cannot_move_to_gpu + + +def move_constructors_to_gpu(graph: fx.Graph) -> None: + """ + Moves intermediary tensors which are constructed on the cpu to gpu when safe + """ + + # cudagraph does not support cpu tensors. In this pass, we update the graph + # by explicitly moving cpu scalar tensors to gpu when profitable, relying on + # graph partition to split off this data copy, and cudagraphifying + # the remaining gpu ops. + allow_inputs_outputs = bool( + torch._inductor.config.triton.cudagraphs + and torch._inductor.config.graph_partition + ) + ConstructorMoverPass( + get_gpu_type(), + allow_inputs=allow_inputs_outputs, + allow_outputs=allow_inputs_outputs, + )(graph) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/pre_grad.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/pre_grad.py new file mode 100644 index 0000000000000000000000000000000000000000..efc953f97068f034cacd9c71acecef0f47452fda --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/pre_grad.py @@ -0,0 +1,897 @@ +# mypy: allow-untyped-defs +import copy +import functools +import itertools +import logging +import types +from collections.abc import Sequence + +import torch +import torch.nn as nn +from torch._dynamo.utils import counters, detect_fake_mode +from torch._logging import trace_structured +from torch.fx.experimental.optimization import ( + matches_module_pattern, + replace_node_module, +) +from torch.fx.passes.graph_transform_observer import ( + GraphTransformObserver as GraphTransformObserverBase, +) +from torch.fx.passes.shape_prop import ShapeProp +from torch.nn import functional as F +from torch.nn.utils.fusion import fuse_conv_bn_eval, fuse_conv_bn_weights + +from .. import config +from ..fx_utils import matches_module_function_pattern +from ..pattern_matcher import ( + init_once_fakemode, + PatternMatcherPass as PatternMatcherPassBase, + stable_topological_sort, +) +from ..utils import is_cpu_device, pass_execution_and_save +from .group_batch_fusion import group_batch_fusion_passes, PRE_GRAD_FUSIONS +from .misc_patterns import numpy_compat_normalization +from .split_cat import PRE_GRAD_PATTERNS + + +PatternMatcherPass = functools.partial( + PatternMatcherPassBase, subsystem="pre_grad_passes" +) +GraphTransformObserver = functools.partial( + GraphTransformObserverBase, subsystem="pre_grad_passes" +) + +log = logging.getLogger(__name__) + +apply_gumbel_max_trick_pass = PatternMatcherPass( + pass_name="apply_gumbel_max_trick_pass" +) + +efficient_conv_bn_eval_pass = PatternMatcherPass( + pass_name="efficient_conv_bn_eval_pass" +) + +fuse_split_linear_add_pass = PatternMatcherPass( + pass_name="fuse_split_linear_add_pass", +) +fuse_chunk_squeeze_cat_pass = PatternMatcherPass( + pass_name="fuse_chunk_squeeze_cat_pass", +) +remove_reshape_pass = PatternMatcherPass( + pass_name="remove_reshape_pass", +) + +# based on predispatch aten IR +normalization_pass_aten = PatternMatcherPass(pass_name="normalization_pass_aten") +merge_splits_pass_aten = PatternMatcherPass(pass_name="merge_splits_pass_aten") +split_cat_pass_aten = PatternMatcherPass(pass_name="split_cat_pass_aten") +unbind_stack_pass_aten = PatternMatcherPass(pass_name="unbind_stack_pass_aten") +merge_getitem_cat_pass_aten = PatternMatcherPass( + pass_name="merge_getitem_cat_pass_aten" +) +merge_stack_tahn_unbind_pass_aten = PatternMatcherPass( + pass_name="merge_stack_tahn_unbind_pass_aten" +) +mutate_cat_pass_aten = PatternMatcherPass(pass_name="mutate_cat_pass_aten") +remove_split_with_size_one_pass_aten = PatternMatcherPass( + pass_name="remove_split_with_size_one_pass_aten" +) + + +def save_inductor_dict(pass_to_compare=None): + if not pass_to_compare: + pass_to_compare = list(config.pre_grad_fusion_options.keys()) + list( + config.post_grad_fusion_options.keys() + ) + return {p: dict(counters["inductor"]).get(p, 0) for p in pass_to_compare} + + +def is_same_dict(inductor_dict, optimus_dict): + for pass_name, count in optimus_dict.items(): + if count != dict(inductor_dict).get(pass_name, 0): + return False + return True + + +def shape_prop(mod) -> None: + return None + + +def normalize_node_kwargs_pass(graph): + return None + + +def fuse_parallel_linear_pass(graph): + return None + + +def remove_split_ops(graph, shape_prop): + return None + + +def remove_split_ops_pass(graph): + remove_split_ops(graph.owning_module, shape_prop) + + +def fuse_chunk_reshape_unsqueeze_concat_pass(graph): + return None + + +def fuse_chunk_reshape_concat_pass(graph): + return None + + +def remove_noop_pass(graph): + return None + + +def stack_to_unsqueeze_pass(graph): + return None + + +def merge_concats_pass(graph): + return None + + +def relu_nan_to_num(graph): + return None + + +def fuse_split_getitem_squeeze_cat(graph): + return None + + +def use_triton_dot_compress(graph): + return None + + +def use_triton_lce_replace_simple_LCE_helper(gm, shape_prop): + return None + + +def use_triton_lce_replace_simple_LCE(graph): + return use_triton_lce_replace_simple_LCE_helper(graph.owning_module, shape_prop) + + +def use_triton_lce_replace_normal_LCE_helper(gm, shape_prop): + return None + + +def use_triton_lce_replace_normal_LCE(graph): + return use_triton_lce_replace_simple_LCE_helper(graph.owning_module, shape_prop) + + +def use_matmul_lce_replace_normal_LCE(graph): + return None + + +def use_matmul_fuse_lce_replace_first_LCE(graph): + return None + + +@init_once_fakemode +def lazy_init(input_device: torch.device | None = None): + from . import ( # noqa: F401 # noqa: F401 + apply_gumbel_max_trick, + efficient_conv_bn_eval, + split_cat, + ) + + if config.is_fbcode(): + from . import fb # type: ignore[attr-defined] # noqa: F401 + + +def _get_pass_name_func(p): + if isinstance(p, PatternMatcherPassBase): + pass_name = p.pass_name + pass_func = p.apply + elif isinstance(p, types.FunctionType): + pass_name = p.__name__.lstrip("_") + pass_func = p + else: + pass_name = None + pass_func = None + + return pass_name, pass_func + + +def _run_pre_dispatch_passes( + gm: torch.fx.GraphModule, + example_inputs: Sequence[object] = (), + add_passes: str | None = None, + remove_passes: str | None = None, +) -> None: + # order matters + default_pass_list = [ + # normalize passes, must be called as the first passes + normalization_pass_aten, + normalize_node_kwargs_pass, + remove_noop_pass, + relu_nan_to_num, + fuse_chunk_reshape_concat_pass, + group_batch_fusion_passes, + normalize_node_kwargs_pass, + fuse_chunk_squeeze_cat_pass, + merge_concats_pass, + fuse_split_linear_add_pass, + remove_reshape_pass, + fuse_parallel_linear_pass, + remove_split_ops_pass, + stack_to_unsqueeze_pass, # run before fuse_chunk_reshape_unsqueeze_concat_pass + fuse_chunk_reshape_unsqueeze_concat_pass, + ] + + full_pass_list = default_pass_list + [ + fuse_split_getitem_squeeze_cat, + use_triton_dot_compress, + use_triton_lce_replace_simple_LCE, + use_triton_lce_replace_normal_LCE, + use_matmul_fuse_lce_replace_first_LCE, + use_matmul_lce_replace_normal_LCE, + ] + + log.info( + f"pre_grad_passes: add_passes: {add_passes}, remove_pass: {remove_passes}" # noqa: G004 + ) + add_passes_list = [] + remove_passes_list = [] + if add_passes: + add_passes_list = add_passes.split(",") + if remove_passes: + remove_passes_list = remove_passes.split(",") + + shape_prop = lambda mod: ShapeProp( # noqa: E731 + gm=mod, + # pyre-fixme[16]: Module `torch._dynamo.utils` has no attribute `detect_fake_mode` + fake_mode=detect_fake_mode(example_inputs), + ).propagate(*tuple(example_inputs)) + + for p in default_pass_list: + pass_name, pass_func = _get_pass_name_func(p) + # should not happen + if pass_name is None or pass_func is None: + continue + if pass_name in remove_passes_list: + continue + pass_execution_and_save( + pass_func, + gm, + example_inputs, + f"[Pre grad(predispatch IR)] Apply {pass_name} pass", + ) + + for p in full_pass_list: + pass_name, pass_func = _get_pass_name_func(p) + if pass_name is None or pass_func is None: + continue + if pass_name in add_passes_list: + pass_execution_and_save( + pass_func, + gm, + example_inputs, + f"[Pre grad(predispatch IR)] Apply {pass_name} pass", + ) + + if "remove_noop" not in remove_passes_list: + # Remove noops at the end, which may be generated other passes. + pass_execution_and_save( + remove_noop_pass, + gm, + example_inputs, + "[Pre grad(predispatch IR)]Apply remove_noop pass", + ) + shape_prop(gm) + + +def pre_grad_passes( + gm: torch.fx.GraphModule, + example_inputs: Sequence[object] = (), + add_passes: str | None = None, + remove_passes: str | None = None, +) -> torch.fx.GraphModule: + """ + Apply passes on the input FX graph using Torch IR. + + WARNING: + The IR before grad is not functional or normalized, so it is harder + to write passes on this IR. Passes must be safe with respect to + aliasing and mutation and need to handle all possible arg schemas. + + Consider adding a new pass to post_grad.py or joint_graph.py which + are after functionalization and normalization. + """ + if config.pattern_matcher: + lazy_init() + if hasattr( + config, "fx_passes_numeric_check" + ) and config.fx_passes_numeric_check.get("pre_grad", False): + gm_before_fx_passes = gm.__copy__() + # explicitly run with predispatch atenIR based passes + if config.is_predispatch: + _run_pre_dispatch_passes(gm, example_inputs, add_passes, remove_passes) + else: + # We only log the graph with changes to avoid the excessive compilation time + # https://fb.workplace.com/groups/257735836456307/permalink/633533465543207/ + if example_inputs is not None: + gm = fuse_fx(gm, example_inputs) + numpy_compat_normalization(gm.graph) + # We should always do the normalization_pass first + if "normalization_pass" in config.pre_grad_fusion_options: + pattern_matcher_pass = PRE_GRAD_PATTERNS["normalization_pass"] + pattern_matcher_pass.apply(gm.graph) # type: ignore[arg-type] + GraphTransformObserver(gm, "group_batch_fusion_passes").apply_graph_pass( + lambda graph: group_batch_fusion_passes(graph, pre_grad=True) + ) + for pass_name in config.pre_grad_fusion_options: + # skip all patterns for group batch fusions + if pass_name in PRE_GRAD_FUSIONS or pass_name == "normalization_pass": + continue + pattern_matcher_pass = PRE_GRAD_PATTERNS[pass_name] + inductor_before_change = save_inductor_dict( + [pattern_matcher_pass.pass_name] + ) + # we support run same pattern multiple times, the default is to run only once + counter = config.pre_grad_fusion_options[pass_name].get("counter", 1) + for _ in range(counter): + pattern_matcher_pass.apply(gm.graph) # type: ignore[arg-type] + if not is_same_dict(counters["inductor"], inductor_before_change): + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": f"{pattern_matcher_pass.pass_name}_pre_grad", + "encoding": "string", + }, + payload_fn=lambda: gm.print_readable( + print_output=False, include_stride=True, include_device=True + ), + ) + # TODO: move efficient_conv_bn_eval_pass to the fusions dict too. + GraphTransformObserver(gm, "efficient_conv_bn_eval_pass").apply_graph_pass( + efficient_conv_bn_eval_pass.apply + ) + GraphTransformObserver(gm, "apply_gumbel_max_trick_pass").apply_graph_pass( + apply_gumbel_max_trick_pass.apply + ) + + if config.pre_grad_custom_pass is not None: + GraphTransformObserver(gm, "pre_grad_custom_pass").apply_graph_pass( + config.pre_grad_custom_pass + ) + + stable_topological_sort(gm.graph) + + from .quantization import quant_lift_up + + quant_lift_up(gm) + + gm.graph.lint() + gm.recompile() + + if ( + config.pattern_matcher + and hasattr(config, "fx_passes_numeric_check") + and config.fx_passes_numeric_check.get("pre_grad", False) + and example_inputs is not None + ): + from .numeric_utils import numeric_check_if_enabled + + gm_after_fx_passes = gm.__copy__() + numeric_check_if_enabled( + gm_before_fx_passes, # type: ignore[possibly-undefined] + gm_after_fx_passes, + example_inputs, + config.fx_passes_numeric_check.get("num_iterations", 1), + config.fx_passes_numeric_check.get("precision", 1e-4), + ) + + return gm + + +def fuse_fx(gm: torch.fx.GraphModule, example_inputs) -> torch.fx.GraphModule: + is_cpu = is_cpu_device(example_inputs) + # pyre-fixme[16]: Module `torch._dynamo.utils` has no attribute `detect_fake_mode` + fake_mode = detect_fake_mode(example_inputs) + + gm = sink_cat_after_pointwise(gm) + if config.permute_fusion and not is_cpu: + # For linear permute fusion, we need to check input info to identify + # and perform proper permutation/transpose + ShapeProp(gm, fake_mode=fake_mode).propagate(*example_inputs) + with GraphTransformObserver(gm, "linear_permute_fusion"): + gm = linear_permute_fusion(gm) + with GraphTransformObserver(gm, "permute_linear_fusion"): + gm = permute_linear_fusion(gm) + with GraphTransformObserver(gm, "permute_matmul_fusion"): + gm = permute_matmul_fusion(gm) + + # make sure the autograd is disabled. + if torch.is_grad_enabled() or not is_cpu: + return gm + if config.freezing: + with GraphTransformObserver(gm, "remove_identity"): + gm = remove_identity(gm) + with GraphTransformObserver(gm, "fuse_conv_bn"): + gm = fuse_conv_bn(gm) + return gm + + +def fetch_attr(target: str, mod): + target_atoms = target.split(".") + attr_itr = mod + for i, atom in enumerate(target_atoms): + if not hasattr(attr_itr, atom): + raise RuntimeError( + f"Node referenced nonexistent target {'.'.join(target_atoms[:i])}" + ) + attr_itr = getattr(attr_itr, atom) + return attr_itr + + +def remove_identity(gm: torch.fx.GraphModule) -> torch.fx.GraphModule: + """ + Removes all identity layers from the module. + """ + graph = gm.graph + work_done = False + for module_name, module in gm.named_modules(): + if type(module) is nn.Identity: + for node in list(graph.find_nodes(op="call_module", target=module_name)): + assert len(node.args) == 1 + input_node = node.args[0] + node.replace_all_uses_with(input_node) + graph.erase_node(node) + work_done = True + if work_done: + graph.lint() + gm.recompile() + return gm + + +def fuse_conv_bn(gm: torch.fx.GraphModule, inplace=False) -> torch.fx.GraphModule: + """ + Fuses Convolution/BN layers for inference purposes. + """ + modules_patterns = [ + (torch.nn.Conv1d, torch.nn.BatchNorm1d), + (torch.nn.Conv2d, torch.nn.BatchNorm2d), + (torch.nn.Conv3d, torch.nn.BatchNorm3d), + ] + module_function_patterns = [ + (torch.nn.Conv1d, F.batch_norm), + (torch.nn.Conv2d, F.batch_norm), + (torch.nn.Conv3d, F.batch_norm), + ] + modules = dict(gm.named_modules()) + + class ConvBNFusion: + def __init__( + self, + bn_node, + conv_module, + bn_module=None, # For BN Module + bn_running_mean=None, # For Functional BN + bn_running_var=None, + bn_eps=None, + bn_weight=None, + bn_bias=None, + ) -> None: + self.bn_nodes = [ + bn_node, + ] + self.conv_module = conv_module + self.bn_module = bn_module + self.bn_running_mean = bn_running_mean + self.bn_running_var = bn_running_var + self.bn_eps = bn_eps + self.bn_weight = bn_weight + self.bn_bias = bn_bias + self.fusion_enabled = True + + def add_bn_node(self, bn_node): + self.bn_nodes.append(bn_node) + + def disable_fusion(self): + self.fusion_enabled = False + + def is_fusion_enabled(self): + return self.fusion_enabled + + conv_bn_to_fuse: dict[int, ConvBNFusion] = {} + for pattern in modules_patterns: + conv_bn_to_fuse.clear() + for node in gm.graph.nodes: + if matches_module_pattern(pattern, node, modules): + if len(node.args[0].users) > 1: # Output of conv is used by other nodes + continue + conv = modules[node.args[0].target] + bn = modules[node.target] + eval_mode = all(not n.training for n in [conv, bn]) + if not eval_mode: + continue + if not bn.track_running_stats: + continue + + # Do hash based on the module name of conv + hash_id = hash(node.args[0].target) + if hash_id not in conv_bn_to_fuse: + conv_bn_to_fuse[hash_id] = ConvBNFusion(node, conv, bn) + else: + if bn == conv_bn_to_fuse[hash_id].bn_module: + # Do fusion if same bn module + conv_bn_to_fuse[hash_id].add_bn_node(node) + else: + # Disable the conv bn folding if conv shared by different bn + conv_bn_to_fuse[hash_id].disable_fusion() + + for conv_bn_fusion in conv_bn_to_fuse.values(): + if conv_bn_fusion.is_fusion_enabled(): + bn_nodes = conv_bn_fusion.bn_nodes + conv = conv_bn_fusion.conv_module + bn = conv_bn_fusion.bn_module + + # pyrefly: ignore [bad-argument-type] + fused_conv = fuse_conv_bn_eval(conv, bn) + for bn_node in bn_nodes: + replace_node_module(bn_node.args[0], modules, fused_conv) + bn_node.replace_all_uses_with(bn_node.args[0]) + gm.graph.erase_node(bn_node) + + gm.graph.lint() + for pattern in module_function_patterns: + conv_bn_to_fuse.clear() + for node in gm.graph.nodes: + if matches_module_function_pattern(pattern, node, modules): + # TODO: support kwargs. + if len(node.args) != 8: + continue + conv = modules[node.args[0].target] + bn_training = node.args[5] + bn_eps = node.args[7] + if conv.training or bn_training: + continue + if type(bn_eps) is not float: + continue + + def _used_by_same_conv_module(users): + conv_module_name = users[0].args[0].target + return all( + conv_module_name == user.args[0].target for user in users + ) + + bn_args_is_constant = all( + n.op == "get_attr" + and (len(n.users) == 1 or _used_by_same_conv_module(list(n.users))) + for n in node.args[1:5] + ) + if not bn_args_is_constant: + continue + bn_running_mean = fetch_attr(node.args[1].target, gm) + bn_running_var = fetch_attr(node.args[2].target, gm) + bn_weight = fetch_attr(node.args[3].target, gm) + bn_bias = fetch_attr(node.args[4].target, gm) + if bn_running_mean is None or bn_running_var is None: + continue + + # Do hash based on the module name of conv + hash_id = hash(node.args[0].target) + if hash_id not in conv_bn_to_fuse: + conv_bn_to_fuse[hash_id] = ConvBNFusion( + node, + conv, + bn_running_mean=bn_running_mean, + bn_running_var=bn_running_var, + bn_eps=bn_eps, + bn_weight=bn_weight, + bn_bias=bn_bias, + ) + else: + if ( + hash(bn_running_mean) + == hash(conv_bn_to_fuse[hash_id].bn_running_mean) + and hash(bn_running_var) + == hash(conv_bn_to_fuse[hash_id].bn_running_var) + and torch.allclose( + torch.tensor(bn_eps), + torch.tensor(conv_bn_to_fuse[hash_id].bn_eps), + ) + and hash(bn_weight) == hash(conv_bn_to_fuse[hash_id].bn_weight) + and hash(bn_bias) == hash(conv_bn_to_fuse[hash_id].bn_bias) + ): + # Do fusion if same functional bn + conv_bn_to_fuse[hash_id].add_bn_node(node) + else: + # Disable the conv bn folding if conv shared by different bn + conv_bn_to_fuse[hash_id].disable_fusion() + + for conv_bn_fusion in conv_bn_to_fuse.values(): + if conv_bn_fusion.is_fusion_enabled(): + bn_nodes = conv_bn_fusion.bn_nodes + conv = conv_bn_fusion.conv_module + bn_running_mean = conv_bn_fusion.bn_running_mean + bn_running_var = conv_bn_fusion.bn_running_var + bn_eps = conv_bn_fusion.bn_eps + bn_weight = conv_bn_fusion.bn_weight + bn_bias = conv_bn_fusion.bn_bias + + fused_conv = copy.deepcopy(conv) + fused_conv.weight, fused_conv.bias = fuse_conv_bn_weights( + fused_conv.weight, + fused_conv.bias, + # pyrefly: ignore [bad-argument-type] + bn_running_mean, + # pyrefly: ignore [bad-argument-type] + bn_running_var, + # pyrefly: ignore [bad-argument-type] + bn_eps, + bn_weight, + bn_bias, + ) + for bn_node in bn_nodes: + replace_node_module(bn_node.args[0], modules, fused_conv) + bn_node.replace_all_uses_with(bn_node.args[0]) + gm.graph.erase_node(bn_node) + gm.graph.lint() + gm.recompile() + + return gm + + +class NormalizedLinearNode: + def __init__(self, node: torch.fx.Node) -> None: + assert node.op == "call_function" + assert node.target is torch.nn.functional.linear + self.node: torch.fx.Node = node + + def get_input(self) -> torch.fx.Node: + if len(self.node.args) > 0: + return self.node.args[0] # type: ignore[return-value] + else: + return self.node.kwargs["input"] # type: ignore[return-value] + + def get_weight(self) -> torch.fx.Node: + if len(self.node.args) > 1: + return self.node.args[1] # type: ignore[return-value] + else: + return self.node.kwargs["weight"] # type: ignore[return-value] + + def get_bias(self) -> torch.fx.Node: + if len(self.node.args) > 2: + return self.node.args[2] # type: ignore[return-value] + else: + return self.node.kwargs.get("bias", None) # type: ignore[return-value] + + +class NormalizedMatmulNode: + def __init__(self, node: torch.fx.Node) -> None: + assert node.op == "call_function" + assert node.target in [torch.bmm, torch.matmul] + self.node: torch.fx.Node = node + + def get_input(self) -> torch.fx.Node: + if len(self.node.args) > 0: + return self.node.args[0] # type: ignore[return-value] + else: + return self.node.kwargs["input"] # type: ignore[return-value] + + def get_other(self) -> torch.fx.Node: + if len(self.node.args) > 1: + return self.node.args[1] # type: ignore[return-value] + else: + return self.node.kwargs["other"] # type: ignore[return-value] + + +def check_permute(node: torch.fx.Node) -> bool: + ranks = len(node.meta["tensor_meta"].shape) + if len(node.args) > 3: + permutation = [node.args[i] % ranks for i in range(1, ranks + 1)] # type: ignore[operator] + elif ( + "permutation" in node.kwargs + and node.kwargs["permutation"] is not None + and len(node.kwargs["permutation"]) > 2 # type: ignore[arg-type] + ): + permutation = [i % ranks for i in node.kwargs["permutation"]] # type: ignore[operator, union-attr] + else: + return False + allowed_permutation = list(range(ranks)) + allowed_permutation[-1] = ranks - 2 + allowed_permutation[-2] = ranks - 1 + return permutation == allowed_permutation + + +def sink_cat_after_pointwise(module: torch.fx.GraphModule) -> torch.fx.GraphModule: + def one_user(node): + users = list(node.users) + return users[0] if len(users) == 1 else None + + def is_view(node): + return node.op == "call_method" and node.target == "view" + + def is_pointwise_unary(node): + ops = "call_function", "call_method" + pointwise = torch.relu, torch.tanh, "relu", "tanh" + return node.op in ops and node.target in pointwise + + g = module.graph + for node in g.nodes: + if node.op != "call_function" or node.target != torch.cat: + continue + + cat_or_view = node + while True: + user = one_user(cat_or_view) + if not user or not is_view(user): + break + cat_or_view = user + + if user and is_pointwise_unary(user): + with g.inserting_before(node): + + def cat_args(tensors, dim=0): + return tensors, dim + + tensors, dim = cat_args(*node.args, **node.kwargs) + new_kwargs = { + name: val for name, val in user.kwargs.items() if name != "input" + } + new_tensors = [ + g.create_node(user.op, user.target, args=(arg,), kwargs=new_kwargs) + for arg in tensors + ] + new_cat = g.create_node( + "call_function", torch.cat, args=(new_tensors, dim) + ) + user.replace_all_uses_with(cat_or_view) + node.replace_all_uses_with(new_cat) + g.erase_node(user) + g.erase_node(node) + g.lint() + module.recompile() + return module + + +def linear_permute_fusion(module: torch.fx.GraphModule) -> torch.fx.GraphModule: + for node in module.graph.find_nodes(op="call_method", target="permute"): + if check_permute(node): + if len(node.args) > 0: + input_node = node.args[0] + else: + input_node = node.kwargs["input"] + if ( + input_node.op == "call_function" + and input_node.target is torch.nn.functional.linear + ): + normalized = NormalizedLinearNode(input_node) + input = normalized.get_input() + weight = normalized.get_weight() + bias = normalized.get_bias() + with module.graph.inserting_before(node): + fused_node = module.graph.call_function( + linear_transpose, args=(input, weight, bias) + ) + node.replace_all_uses_with(fused_node) + module.graph.erase_node(node) + if len(input_node.users) == 0: + module.graph.erase_node(input_node) + + module.graph.lint() + module.recompile() + return module + + +# Y1 = X * W^T + bias +# Y2 = Y1.permute(0, 2, 1) +# ----> +# Y2 = (W * X^T + bias.unsqueeze(-1))^T +def linear_transpose( + input: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor | None +) -> torch.Tensor: + if bias is None: + return torch.matmul(weight, input.transpose(-1, -2)) + return torch.matmul(weight, input.transpose(-1, -2)) + bias.unsqueeze(-1) + + +def permute_linear_fusion(module: torch.fx.GraphModule) -> torch.fx.GraphModule: + for node in module.graph.find_nodes( + op="call_function", target=torch.nn.functional.linear + ): + if len(node.args) > 0: + input_node = node.args[0] + else: + input_node = node.kwargs["input"] + if ( + input_node.op == "call_method" + and input_node.target == "permute" + and check_permute(input_node) + ): + normalized = NormalizedLinearNode(node) + if len(input_node.args) > 0: + input = input_node.args[0] + else: + input = input_node.kwargs["input"] + weight = normalized.get_weight() + bias = normalized.get_bias() + with module.graph.inserting_before(node): + fused_node = module.graph.call_function( + transpose_linear, args=(input, weight, bias) + ) + node.replace_all_uses_with(fused_node) + module.graph.erase_node(node) + if len(input_node.users) == 0: + module.graph.erase_node(input_node) + + module.graph.lint() + module.recompile() + return module + + +def permute_matmul_fusion(module: torch.fx.GraphModule) -> torch.fx.GraphModule: + for node in itertools.chain( + module.graph.find_nodes(op="call_function", target=torch.bmm), + module.graph.find_nodes(op="call_function", target=torch.matmul), + ): + normalized = NormalizedMatmulNode(node) + input_A_node = normalized.get_input() + input_B_node = normalized.get_other() + input_A = input_A_node + input_B = input_B_node + Atrans = Btrans = False + if ( + input_A_node.op == "call_method" + and input_A_node.target == "permute" + and check_permute(input_A_node) + ): + Atrans = True + if len(input_A_node.args) > 0: + input_A = input_A_node.args[0] # type: ignore[assignment] + else: + input_A = input_A_node.kwargs["input"] # type: ignore[assignment] + + if ( + input_B_node.op == "call_method" + and input_B_node.target == "permute" + and check_permute(input_B_node) + ): + Btrans = True + if len(input_B_node.args) > 0: + input_B = input_B_node.args[0] # type: ignore[assignment] + else: + input_B = input_B_node.kwargs["input"] # type: ignore[assignment] + + if Atrans or Btrans: + with module.graph.inserting_before(node): + fused_node = module.graph.call_function( + transpose_matmul, + args=(input_A, input_B, Atrans, Btrans), + ) + node.replace_all_uses_with(fused_node) + module.graph.erase_node(node) + if Atrans and len(input_A_node.users) == 0: + module.graph.erase_node(input_A_node) + if Btrans and len(input_B_node.users) == 0: + module.graph.erase_node(input_B_node) + + module.graph.lint() + module.recompile() + return module + + +# X1 = X.permute(0, 2, 1) +# Y1 = X1 * W1^T + bias1 +# ----> +# Y2 = X1.transpose(-1, -2) * W1^T + bias1 +def transpose_linear( + input: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor | None +) -> torch.Tensor: + if bias is None: + return torch.matmul(input.transpose(-1, -2), weight.t()) + return torch.matmul(input.transpose(-1, -2), weight.t()) + bias + + +def transpose_matmul( + A: torch.Tensor, B: torch.Tensor, Atrans: bool, Btrans: bool +) -> torch.Tensor: + if Atrans: + A = A.transpose(-1, -2) + if Btrans: + B = B.transpose(-1, -2) + return torch.matmul(A, B) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/quantization.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/quantization.py new file mode 100644 index 0000000000000000000000000000000000000000..bde5c94351931b7dbff20bb3d1b29babf277f153 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/quantization.py @@ -0,0 +1,3969 @@ +# mypy: allow-untyped-decorators +# mypy: allow-untyped-defs +import copy +import functools +import itertools +import math +import operator +from typing import Any + +import torch +from torch._dynamo.utils import counters +from torch.fx.experimental.symbolic_shapes import has_free_symbols +from torch.fx.node import map_arg + +from .. import config +from ..lowering import lowerings as L, require_channels_last +from ..pattern_matcher import ( + Arg, + CallFunction, + filter_nodes, + KeywordArg, + ListOf, + Match, + stable_topological_sort, +) +from ..utils import pad_listlike +from .freezing_patterns import register_freezing_graph_pattern +from .post_grad import register_lowering_pattern + + +aten = torch.ops.aten +prims = torch.ops.prims +quantized_decomposed = torch.ops.quantized_decomposed +quantized = torch.ops.quantized + +# Only for per tensor quant since permute may changes the channel idx +_PER_TENSOR_QUANTIZE_OPS = [ + quantized_decomposed.quantize_per_tensor.default, + quantized_decomposed.quantize_per_tensor.tensor, +] + +_VIEW_OPS = [ + aten.transpose.int, + aten.permute.default, + aten.view.default, + aten.reshape.default, +] + +""" +The quantization.py file primarily incorporates passes related to quantization fusion +in inductor, includes: +1. Dequant Promotion; +2. Conv/GEMM weight prepack with oneDNN Library; +3. Conv/GEMM quantization fusion with output quant node (if have); +4. Other pointwise operators' quantization fusion like: qmaxpool2d, qcat and more; + +It also involves int8-mixed-fp32 and int8-mixed-bf16 quantization. The main difference +of patterns for int8-mixed-bf16, comparing with int8-mixed-fp32, is +1. There is to(dtype=torch.bfloat16) node at the inputs of activation and weight for Conv/GEMM. +2. There is to(dtype=torch.float32) node at the outputs of Conv/GEMM before inputs to next quant node. +Refer to: https://github.com/pytorch/pytorch/issues/111640 for detail design of int8-mixed-bf16 +quantization. +""" + + +def _get_pattern_output_dtype(match: Match): + """ + Get the pattern's output dtype from node's meta + Assume only 1 output node in this matched pattern. + """ + pattern_output_nodes = match.output_nodes() + assert len(pattern_output_nodes) == 1 + output_node = pattern_output_nodes[0] + assert isinstance(output_node, torch.fx.Node) + output_dtype = output_node.meta["val"].dtype + assert output_dtype in [ + torch.int8, + torch.uint8, + torch.float32, + torch.bfloat16, + torch.float8_e4m3fn, + ] + return output_dtype + + +def _may_generate_pattern_with_dtype_convert( + pattern, dtype=Arg(), with_dtype_convert=True, users=1 +): + if with_dtype_convert: + return CallFunction( + prims.convert_element_type.default, + pattern, + dtype, + _users=users, + ) + else: + return pattern + + +def _may_generate_pattern_with_reshape(pattern, reshape_size=Arg(), with_reshape=True): + if with_reshape: + return CallFunction( + torch.ops.aten.reshape.default, + pattern, + reshape_size, + ) + else: + return pattern + + +def _generate_linear_t_pattern( + _dequant_per_channel_pattern, + dtype, +): + assert dtype in [torch.float32, torch.bfloat16] + t_pattern = CallFunction( + aten.permute.default, + _may_generate_pattern_with_dtype_convert( + _dequant_per_channel_pattern, + KeywordArg("autocast_wgt_dtype"), + dtype == torch.bfloat16, + ), + KeywordArg("permute_axes"), + ) + return t_pattern + + +def _unary_fusion_pattern(unary_fusion, call_fn, users, is_bf16): + # only insert to_dtype if is_bf16 is True + computation_call = _may_generate_pattern_with_dtype_convert( + call_fn, dtype=KeywordArg("to_float"), with_dtype_convert=is_bf16, users=users + ) + return unary_fusion(computation_call) + + +def get_dequantize_per_tensor_activation_pattern(is_tensor_overload=False): + dequantize_per_tensor_activation_pattern = CallFunction( + quantized_decomposed.dequantize_per_tensor.tensor + if is_tensor_overload + else quantized_decomposed.dequantize_per_tensor.default, + KeywordArg("x"), + KeywordArg("x_scale"), + KeywordArg("x_zp"), + KeywordArg("x_quant_min"), + KeywordArg("x_quant_max"), + KeywordArg("x_dq_dtype"), + ) + return dequantize_per_tensor_activation_pattern + + +dequantize_per_channel_weight_pattern = CallFunction( + quantized_decomposed.dequantize_per_channel.default, + KeywordArg("q_weight"), + KeywordArg("w_scale"), + KeywordArg("w_zp"), + KeywordArg("w_axis"), + KeywordArg("w_quant_min"), + KeywordArg("w_quant_max"), + KeywordArg("w_dtype"), +) + +dequantize_per_channel_to_bf16_weight_pattern = ( + _may_generate_pattern_with_dtype_convert( + dequantize_per_channel_weight_pattern, + KeywordArg("autocast_wgt_dtype"), + ) +) + +dequantize_per_channel_clone_weight_pattern = CallFunction( + aten.clone.default, + dequantize_per_channel_weight_pattern, + memory_format=KeywordArg("memory_format"), +) + +dequantize_per_channel_to_bf16_clone_weight_pattern = CallFunction( + aten.clone.default, + dequantize_per_channel_to_bf16_weight_pattern, + memory_format=KeywordArg("memory_format"), +) + + +def get_qconv_pt2e_pattern(x_scale_zp_are_tensors=False, users=1): + qconv_op = ( + torch.ops.onednn.qconv_pointwise.tensor + if x_scale_zp_are_tensors + else torch.ops.onednn.qconv_pointwise.default + ) + return CallFunction( + qconv_op, + KeywordArg("x"), + KeywordArg("x_scale"), + KeywordArg("x_zp"), + KeywordArg("packed_weight"), + KeywordArg("w_scale"), + KeywordArg("w_zp"), + KeywordArg("b"), + KeywordArg("stride"), + KeywordArg("padding"), + KeywordArg("dilation"), + KeywordArg("groups"), + KeywordArg("output_scale"), + KeywordArg("output_zero_point"), + KeywordArg("output_dtype"), + KeywordArg("postop_name"), + KeywordArg("postop_args"), + KeywordArg("postop_algorithm"), + _users=users, + ) + + +def get_qconv2d_binary_pt2e_pattern(x_scale_zp_are_tensors=False, users=1): + qconv_op = ( + torch.ops.onednn.qconv2d_pointwise.binary_tensor + if x_scale_zp_are_tensors + else torch.ops.onednn.qconv2d_pointwise.binary + ) + return CallFunction( + qconv_op, + KeywordArg("x"), + KeywordArg("x_scale"), + KeywordArg("x_zp"), + KeywordArg("packed_weight"), + KeywordArg("w_scale"), + KeywordArg("w_zp"), + KeywordArg("accum"), + KeywordArg("b"), + KeywordArg("stride"), + KeywordArg("padding"), + KeywordArg("dilation"), + KeywordArg("groups"), + KeywordArg("output_scale"), + KeywordArg("output_zero_point"), + KeywordArg("output_dtype"), + KeywordArg("accum_scale"), + KeywordArg("accum_zero_point"), + KeywordArg("binary_op_name"), + KeywordArg("alpha"), + KeywordArg("unary_op_name"), + KeywordArg("unary_op_args"), + KeywordArg("unary_op_algorithm"), + _users=users, + ) + + +def get_qlinear_pt2e_pattern(x_scale_zp_are_tensors, users=1): + qlinear_op = ( + torch.ops.onednn.qlinear_pointwise.tensor + if x_scale_zp_are_tensors + else torch.ops.onednn.qlinear_pointwise.default + ) + return CallFunction( + qlinear_op, + KeywordArg("x"), + KeywordArg("x_scale"), + KeywordArg("x_zp"), + KeywordArg("packed_weight"), + KeywordArg("w_scale"), + KeywordArg("w_zp"), + KeywordArg("b"), + KeywordArg("output_scale"), + KeywordArg("output_zero_point"), + KeywordArg("output_dtype"), + KeywordArg("postop_name"), + KeywordArg("postop_args"), + KeywordArg("postop_algorithm"), + _users=users, + ) + + +def get_qlinear_binary_pt2e_pattern(x_scale_zp_are_tensors, users=1): + qlinear_op = ( + torch.ops.onednn.qlinear_pointwise.binary_tensor + if x_scale_zp_are_tensors + else torch.ops.onednn.qlinear_pointwise.binary + ) + return CallFunction( + qlinear_op, + KeywordArg("x"), + KeywordArg("x_scale"), + KeywordArg("x_zp"), + KeywordArg("packed_weight"), + KeywordArg("w_scale"), + KeywordArg("w_zp"), + KeywordArg("x_2"), + KeywordArg("b"), + KeywordArg("output_scale"), + KeywordArg("output_zero_point"), + KeywordArg("output_dtype"), + KeywordArg("x2_scale"), + KeywordArg("x2_zp"), + KeywordArg("binary_op_name"), + KeywordArg("alpha"), + KeywordArg("unary_op_name"), + KeywordArg("unary_op_args"), + KeywordArg("unary_op_algorithm"), + _users=users, + ) + + +dequantize_accum_pattern = CallFunction( + quantized_decomposed.dequantize_per_tensor.default, + KeywordArg("accum"), + KeywordArg("accum_scale"), + KeywordArg("accum_zp"), + Arg(), + Arg(), + KeywordArg("accum_dq_dtype"), +) + + +def generate_pattern_with_binary( + binary_post_op, + computation_call, + extra_input_pattern, + dtype_convert=False, + swap_inputs=False, +): + binary_pattern = ( + CallFunction( + binary_post_op, + extra_input_pattern, + computation_call, + ) + if swap_inputs + else CallFunction( + binary_post_op, + computation_call, + extra_input_pattern, + ) + ) + return _may_generate_pattern_with_dtype_convert( + binary_pattern, + KeywordArg("convert_dtype_after_inplace_add"), + dtype_convert, + ) + + +def generate_pattern_with_unary(computation_call, unary_post_op): + if unary_post_op is not None: + return CallFunction( + unary_post_op, + computation_call, + ) + return computation_call + + +def generate_pattern_with_output_quant(computation_call, with_dtype_convert=False): + quantized_op_output_pattern_pt2e = CallFunction( + quantized_decomposed.quantize_per_tensor.default, + _may_generate_pattern_with_dtype_convert( + computation_call, + Arg(), + with_dtype_convert, + ), + KeywordArg("o_inv_scale"), + KeywordArg("o_zp"), + KeywordArg("o_qmin"), + KeywordArg("o_qmax"), + KeywordArg("o_dtype"), + ) + return quantized_op_output_pattern_pt2e + + +def _check_node_kwarg_arg_value(check_node, kwarg_name, args_index, expected_value): + if kwarg_name in check_node.kwargs: + actual_value = check_node.kwargs[kwarg_name] + return actual_value == expected_value + else: + assert len(check_node.args) >= (args_index + 1) + actual_value = check_node.args[args_index] + return actual_value == expected_value + + +def _is_valid_quantized_conv_optimization_pattern(): + def fn(match): + output_dtype = _get_pattern_output_dtype(match) + if output_dtype in [torch.float32, torch.bfloat16]: + # Only keep matched pattern with same output_dtype + qconv_node_after_weight_prepack = filter_nodes( + match.nodes, torch.ops.onednn.qconv_pointwise + )[0] + return _check_node_kwarg_arg_value( + qconv_node_after_weight_prepack, "output_dtype", 13, output_dtype + ) + return True + + return fn + + +def _is_valid_qconv_post_op_fusion_pattern(has_binary_post_op=False): + return ( + _is_valid_qconv_binary_optimization_pattern() + if has_binary_post_op + else _is_valid_quantized_conv_optimization_pattern() + ) + + +def _is_valid_qconv_lowering_pattern(): + def fn(match): + if len(match.nodes) != 1: + return False + return match.nodes[0].target in ( + torch.ops.onednn.qconv_pointwise.default, + torch.ops.onednn.qconv_pointwise.tensor, + torch.ops.onednn.qconv2d_pointwise.binary, + torch.ops.onednn.qconv2d_pointwise.binary_tensor, + ) + + return fn + + +def _register_quantized_conv_lowering( + pattern, + pass_number, + computation_op, +): + @register_lowering_pattern( + pattern, + extra_check=_is_valid_qconv_lowering_pattern(), + pass_number=pass_number, + ) + def qconv(match: Match, *args, **kwargs): + # Activation QParams + x, x_scale, x_zp = ( + kwargs["x"], + kwargs["x_scale"], + kwargs["x_zp"], + ) + # Weight QParams + packed_weight, w_scale, w_zp = ( + kwargs["packed_weight"], + kwargs["w_scale"], + kwargs["w_zp"], + ) + # Conv Params + b, stride, padding, dilation, groups = ( + kwargs["b"], + kwargs["stride"], + kwargs["padding"], + kwargs["dilation"], + kwargs["groups"], + ) + output_dtype = _get_pattern_output_dtype(match) + assert output_dtype in [ + torch.int8, + torch.uint8, + torch.float8_e4m3fn, + torch.float32, + torch.bfloat16, + ] + # Output QParams + o_inv_scale = kwargs["output_scale"] + o_zero_point = kwargs["output_zero_point"] + output_dtype = kwargs["output_dtype"] + # post op + postop_name = kwargs["postop_name"] + postop_args = kwargs["postop_args"] + postop_algorithm = kwargs["postop_algorithm"] + + computation_args = ( + x, + x_scale, + x_zp, + packed_weight, + w_scale, + w_zp, + b, + stride, + padding, + dilation, + groups, + o_inv_scale, + o_zero_point, + output_dtype, + postop_name, + postop_args, + postop_algorithm, + ) + counters["inductor"]["qconv_unary_lower_count"] += 1 + counters["inductor"]["qconv_unary_lower_nodes"] += len(match.nodes) + return L[computation_op](*computation_args) + + return qconv + + +def _is_valid_quantized_linear_optimization_pattern(): + def fn(match): + output_dtype = _get_pattern_output_dtype(match) + if output_dtype in [torch.float32, torch.bfloat16]: + # Only keep matched pattern with same output_dtype + qlinear_node_after_weight_prepack = filter_nodes( + match.nodes, torch.ops.onednn.qlinear_pointwise + )[0] + return _check_node_kwarg_arg_value( + qlinear_node_after_weight_prepack, "output_dtype", 9, output_dtype + ) + return True + + return fn + + +def _is_valid_qlinear_post_op_fusion_pattern(has_binary_post_op=False): + return ( + _is_valid_qlinear_binary_optimization_pattern() + if has_binary_post_op + else _is_valid_quantized_linear_optimization_pattern() + ) + + +def _is_valid_qlinear_lowering_pattern(): + def fn(match): + if len(match.nodes) != 1: + return False + return match.nodes[0].target in ( + torch.ops.onednn.qlinear_pointwise.default, + torch.ops.onednn.qlinear_pointwise.tensor, + torch.ops.onednn.qlinear_pointwise.binary, + torch.ops.onednn.qlinear_pointwise.binary_tensor, + ) + + return fn + + +def _register_quantized_linear_unary_lowering( + pattern, + pass_number, + computation_op, +): + @register_lowering_pattern( + pattern, + extra_check=_is_valid_qlinear_lowering_pattern(), + pass_number=pass_number, + ) + def qlinear(match: Match, *args, **kwargs): + output_dtype = _get_pattern_output_dtype(match) + # Activation QParams + x, x_scale, x_zp = ( + kwargs["x"], + kwargs["x_scale"], + kwargs["x_zp"], + ) + # Weight QParams + packed_weight, w_scale, w_zp = ( + kwargs["packed_weight"], + kwargs["w_scale"], + kwargs["w_zp"], + ) + + # bias + b = kwargs.get("b") + + # Output QParams + o_inv_scale = kwargs["output_scale"] + o_zero_point = kwargs["output_zero_point"] + + # post op + postop_name = kwargs["postop_name"] + postop_args = kwargs["postop_args"] + postop_algorithm = kwargs["postop_algorithm"] + + computation_args = ( + x, + x_scale, + x_zp, + packed_weight, + w_scale, + w_zp, + b, + o_inv_scale, + o_zero_point, + output_dtype, + postop_name, + postop_args, + postop_algorithm, + ) + counters["inductor"]["qlinear_unary_lower_count"] += 1 + counters["inductor"]["qlinear_unary_lower_nodes"] += len(match.nodes) + return L[computation_op](*computation_args) + + return qlinear + + +def _register_quantized_linear_binary_lowering( + pattern, + pass_number, + computation_op, +): + @register_lowering_pattern( + pattern, + extra_check=_is_valid_qlinear_lowering_pattern(), + pass_number=pass_number, + ) + def qlinear_binary(match: Match, *args, **kwargs): + output_dtype = _get_pattern_output_dtype(match) + assert output_dtype is not None + # Activation QParams + x, x_scale, x_zp = ( + kwargs["x"], + kwargs["x_scale"], + kwargs["x_zp"], + ) + x2 = kwargs["x_2"] + x2_scale = kwargs["x2_scale"] + x2_zp = kwargs["x2_zp"] + # Weight QParams + packed_weight, w_scale, w_zp = ( + kwargs["packed_weight"], + kwargs["w_scale"], + kwargs["w_zp"], + ) + # bias + b = kwargs.get("b") + # Output QParams + o_inv_scale = kwargs["output_scale"] + o_zero_point = kwargs["output_zero_point"] + + x2.realize() + from .mkldnn_fusion import _qlinear_binary_can_be_inplace + + binary_op_name = kwargs["binary_op_name"] + alpha = kwargs["alpha"] + unary_op_name = kwargs["unary_op_name"] + unary_op_args = kwargs["unary_op_args"] + unary_op_algorithm = kwargs["unary_op_algorithm"] + if ( + # TODO Ensure sum is safe and remove such check, i.e., + # x2 is not used by other operations + # or current qlinear sum is the last user of x2. + # This needs to be ensured when registering + # the lowering pattern of quantized_linear_binary. + binary_op_name == "sum" and (not _qlinear_binary_can_be_inplace(x2)) + ): + binary_op_name = "add" + + computation_args = ( + x, + x_scale, + x_zp, + packed_weight, + w_scale, + w_zp, + x2, + b, + o_inv_scale, + o_zero_point, + output_dtype, + x2_scale, + x2_zp, + binary_op_name, + alpha, + unary_op_name, + unary_op_args, + unary_op_algorithm, + ) + counters["inductor"]["qlinear_binary_lower_count"] += 1 + counters["inductor"]["qlinear_binary_lower_nodes"] += len(match.nodes) + return L[computation_op](*computation_args) + + return qlinear_binary + + +def _is_valid_qconv_binary_optimization_pattern(): + return _is_valid_quantized_op_binary_optimization_pattern( + torch.ops.onednn.qconv_pointwise + ) + + +def _is_valid_qlinear_binary_optimization_pattern(): + return _is_valid_quantized_op_binary_optimization_pattern( + torch.ops.onednn.qlinear_pointwise, + # we don't insert q-dq for extra input due to accuracy issues + extra_input_from_dequant=False, + ) + + +def _is_valid_quantized_op_binary_optimization_pattern( + qop, extra_input_from_dequant=True +): + # Check if it's a valid Binary Pattern for qconv2d and qlinear: + # * qop_pointwise should only has one users + # * If extra_input_from_dequant is True, extra input of binary node should come from dequant pattern + # * the two inputs of binary node should have attribute "meta" and should be tensors + # * the two inputs of binary node should have the same shape + # * All users of the extra input in this pattern should be + # ancestor nodes of the compute node, except for the binary node + # connected to the compute node. + def fn(match): + output_dtype = _get_pattern_output_dtype(match) + compute_node = filter_nodes(match.nodes, qop)[0] + # qop_pointwise should only have one user + if len(compute_node.users) != 1: + return False + binary_node_inputs = next(iter(compute_node.users)).args + assert len(binary_node_inputs) == 2, "Expects binary node with 2 inputs" + if output_dtype in [torch.float32, torch.bfloat16]: + extra_input_of_binary_node = None + for arg in binary_node_inputs: + if arg != compute_node: + extra_input_of_binary_node = arg + break + assert extra_input_of_binary_node is not None + # Extra input of binary node comes from dequant pattern + if extra_input_from_dequant and ( + (not isinstance(extra_input_of_binary_node, torch.fx.Node)) + or ( + extra_input_of_binary_node.target + != quantized_decomposed.dequantize_per_tensor.default + ) + ): + return False + + # the two inputs of binary node should have attribute "meta" and should be tensors + if not ( + hasattr(binary_node_inputs[0], "meta") + and isinstance(binary_node_inputs[0].meta.get("val", None), torch.Tensor) # type: ignore[union-attr] + ) or not ( + hasattr(binary_node_inputs[1], "meta") + and isinstance(binary_node_inputs[1].meta.get("val", None), torch.Tensor) # type: ignore[union-attr] + ): + return False + # the two inputs of binary node should have the same shape + if ( + binary_node_inputs[0].meta["val"].size() # type: ignore[union-attr] + != binary_node_inputs[1].meta["val"].size() # type: ignore[union-attr] + ): + return False + + # All users of the extra input in this pattern should be + # ancestor nodes of the compute node, except for the binary node + # connected to the compute node. + + from .mkldnn_fusion import _get_remaining_users + + extra_input_of_pattern = ( + match.kwargs["other"] + if "other" in match.kwargs + else ( + match.kwargs["accum"] + if (output_dtype in [torch.uint8, torch.int8]) + or (not extra_input_from_dequant) + else match.kwargs["accum_after_dequant"] + ) + ) + if ( + len(_get_remaining_users(extra_input_of_pattern, compute_node)) > 1 + or extra_input_of_pattern == compute_node.args[0] + ): + return False + return True + + return fn + + +def _register_quantized_conv_binary_lowering( + pattern, + pass_number, + computation_op, +): + @register_lowering_pattern( + pattern, + extra_check=_is_valid_qconv_lowering_pattern(), + pass_number=pass_number, + ) + def qconv_binary(match: Match, *args, **kwargs): + output_dtype = _get_pattern_output_dtype(match) + assert output_dtype is not None + x, x_scale, x_zp = kwargs["x"], kwargs["x_scale"], kwargs["x_zp"] + accum = kwargs["accum"] + accum_scale = kwargs["accum_scale"] + accum_zp = kwargs["accum_zero_point"] + packed_weight, w_scale, w_zp = ( + kwargs["packed_weight"], + kwargs["w_scale"], + kwargs["w_zp"], + ) + b, stride, padding, dilation, groups = ( + kwargs["b"], + kwargs["stride"], + kwargs["padding"], + kwargs["dilation"], + kwargs["groups"], + ) + # Output QParams + output_scale = kwargs["output_scale"] + output_zero_point = kwargs["output_zero_point"] + + # post ops + binary_op_name = kwargs["binary_op_name"] + alpha = kwargs["alpha"] + unary_op_name = kwargs["unary_op_name"] + unary_op_args = kwargs["unary_op_args"] + unary_op_algorithm = kwargs["unary_op_algorithm"] + + accum.realize() + from .mkldnn_fusion import _can_be_inplace + + assert _can_be_inplace(accum), ( + "QConv Binary Inplace Fusion requires accum is not an alias or mutation." + ) + + computation_args = ( + x, + x_scale, + x_zp, + packed_weight, + w_scale, + w_zp, + accum, + b, + stride, + padding, + dilation, + groups, + output_scale, + output_zero_point, + output_dtype, + accum_scale, + accum_zp, + binary_op_name, + alpha, + unary_op_name, + unary_op_args, + unary_op_algorithm, + ) + counters["inductor"]["qconv2d_binary_lower_count"] += 1 + counters["inductor"]["qconv2d_binary_lower_nodes"] += len(match.nodes) + return L[computation_op](*computation_args) + + return qconv_binary + + +def _register_quantization_unary_lowering(): + # QConv2d + for x_scale_zp_are_tensors, users in itertools.product([False, True], [1, 2]): + qconv_pattern = get_qconv_pt2e_pattern(x_scale_zp_are_tensors, users) + computation_op = ( + torch.ops.onednn.qconv_pointwise.tensor + if x_scale_zp_are_tensors + else torch.ops.onednn.qconv_pointwise.default + ) + _register_quantized_conv_lowering( + qconv_pattern, + 2, # pass_number + computation_op, + ) + + # QLinear + for x_scale_zp_are_tensors in (False, True): + qlinear_pattern = get_qlinear_pt2e_pattern(x_scale_zp_are_tensors) + computation_op = ( + torch.ops.onednn.qlinear_pointwise.tensor + if x_scale_zp_are_tensors + else torch.ops.onednn.qlinear_pointwise.default + ) + _register_quantized_linear_unary_lowering( + qlinear_pattern, + 2, # pass_number + computation_op, + ) + + +def _register_quantization_binary_lowering(): + # QConv2d + for x_scale_zp_are_tensors, users in itertools.product([False, True], [1, 2]): + qconv_pattern = get_qconv2d_binary_pt2e_pattern(x_scale_zp_are_tensors, users) + computation_op = ( + torch.ops.onednn.qconv2d_pointwise.binary_tensor + if x_scale_zp_are_tensors + else torch.ops.onednn.qconv2d_pointwise.binary + ) + _register_quantized_conv_binary_lowering( + qconv_pattern, + 2, # pass_number + computation_op, + ) + + # QLinear + for x_scale_zp_are_tensors in (False, True): + qlinear_pattern = get_qlinear_binary_pt2e_pattern(x_scale_zp_are_tensors) + computation_op = ( + torch.ops.onednn.qlinear_pointwise.binary_tensor + if x_scale_zp_are_tensors + else torch.ops.onednn.qlinear_pointwise.binary + ) + _register_quantized_linear_binary_lowering( + qlinear_pattern, + 2, # pass_number + computation_op, + ) + + +def _is_valid_quantized_maxpool2d_optimization_pattern(): + def fn(match): + # Only match the pattern which max_pool2d_with_indices returns value + # instead of indices. + get_item_node = filter_nodes(match.nodes, operator.getitem)[0] + return get_item_node.args[1] == 0 + + return fn + + +def _register_quantized_maxpool2d_lowering( + pattern, + computation_op, +): + @register_lowering_pattern( + pattern, + extra_check=_is_valid_quantized_maxpool2d_optimization_pattern(), + ) + def qmaxpool2d(match: Match, *args, **kwargs): + x = kwargs["x"] + kernel_size = kwargs["kernel_size"] + stride = kwargs.get("stride") + padding = kwargs.get("padding", 0) + dilation = kwargs.get("dilation", 1) + ceil_mode = kwargs.get("ceil_mode", False) + + if padding == 0: + padding = [0, 0] + if dilation == 1: + dilation = [1, 1] + if not stride: + stride = kernel_size + kernel_size = pad_listlike(kernel_size, 2) + stride = pad_listlike(stride, 2) + padding = pad_listlike(padding, 2) + dilation = pad_listlike(dilation, 2) + + assert len(kernel_size) == 2 + assert len(stride) == 2 + assert len(padding) == 2 + assert len(dilation) == 2 + + computation_args = ( + x, + kernel_size, + stride, + padding, + dilation, + ceil_mode, + ) + computation_args, _ = require_channels_last(computation_op, *computation_args) + counters["inductor"]["qmaxpool2d_matcher_count"] += 1 + counters["inductor"]["qmaxpool2d_matcher_nodes"] += len(match.nodes) + return L[computation_op](*computation_args) + + return qmaxpool2d + + +def _register_quantization_maxpool2d(): + # Currently, the default parameters are not in FX Graph generated by Dynamo export. + # So, if user defines nn.MaxPool2d with different assignment of default parameter, + # it will generate graph with different number of input nodes and hence + # different pattern to be matched. + # Refer to the issue: https://github.com/pytorch/pytorch/issues/105901 + max_pool2d_args_list = [ + [ + KeywordArg("stride"), + ], + [ + KeywordArg("stride"), + KeywordArg("padding"), + ], + [ + KeywordArg("stride"), + KeywordArg("padding"), + KeywordArg("dilation"), + ], + [ + KeywordArg("stride"), + KeywordArg("padding"), + KeywordArg("dilation"), + KeywordArg("ceil_mode"), + ], + ] + for max_pool2d_args in max_pool2d_args_list: + dequantize_maxpool2d_pattern = CallFunction( + aten.max_pool2d_with_indices.default, + get_dequantize_per_tensor_activation_pattern(), + KeywordArg("kernel_size"), + *max_pool2d_args, + ) + dequantize_lowmem_maxpool2d_pattern = CallFunction( + prims._low_memory_max_pool_with_offsets.default, + get_dequantize_per_tensor_activation_pattern(), + KeywordArg("kernel_size"), + *max_pool2d_args, + KeywordArg("offset_dtype"), + ) + dequantize_maxpool2d_get_item_pattern = CallFunction( + operator.getitem, + dequantize_maxpool2d_pattern, + Arg(), + ) + dequantize_lowmem_maxpool2d_get_item_pattern = CallFunction( + operator.getitem, + dequantize_lowmem_maxpool2d_pattern, + Arg(), + ) + _register_quantized_maxpool2d_lowering( + generate_pattern_with_output_quant(dequantize_maxpool2d_get_item_pattern), + quantized.max_pool2d.default, + ) + _register_quantized_maxpool2d_lowering( + generate_pattern_with_output_quant( + dequantize_lowmem_maxpool2d_get_item_pattern + ), + quantized.max_pool2d.default, + ) + + +def _is_input_output_same_scale_zp(check_node): + def fn(match): + # Ensure all the inputs and output has same scale and zero point + # Step 1: Check inputs/output zero point + # Get dequant nodes at input + dequant_nodes = filter_nodes( + match.nodes, quantized_decomposed.dequantize_per_tensor.default + ) + zero_points = [node.args[2] for node in dequant_nodes] + # Get quant nodes at output + quant_nodes = filter_nodes( + match.nodes, quantized_decomposed.quantize_per_tensor.default + ) + assert len(quant_nodes) == 1, "expect only 1 add node at output quant pattern" + zero_points.append(quant_nodes[0].args[2]) + if not all(zero_point == zero_points[0] for zero_point in zero_points): + return False + + # Step 2: Check inputs/output scale + scales = [node.args[1] for node in dequant_nodes] + scales.append(quant_nodes[0].args[1]) + if not all(math.isclose(scale, scales[0], rel_tol=1e-5) for scale in scales): # type: ignore[arg-type] + return False + + return True + + return fn + + +def _register_quantized_cat_lowering( + pattern, + computation_op, +): + @register_lowering_pattern( + pattern, + extra_check=_is_input_output_same_scale_zp(aten.cat.default), + ) + def qcat(match: Match, inputs, dim, **kwargs): + # inputs is with format: [[x1, x1_dq_dtype, x1_zp, x1_scale], ...] + uint8_inputs = [input[0] for input in inputs] + counters["inductor"]["qcat_matcher_count"] += 1 + counters["inductor"]["qcat_matcher_nodes"] += len(match.nodes) + return L[computation_op](uint8_inputs, dim) + + return qcat + + +_raw_dequantize_per_tensor_activation_pattern = CallFunction( + quantized_decomposed.dequantize_per_tensor.default, + Arg(), + Arg(), + Arg(), + Arg(), + Arg(), + Arg(), +) + + +def _register_quantization_cat(): + dequantize_cat_pattern = CallFunction( + aten.cat.default, + ListOf(_raw_dequantize_per_tensor_activation_pattern), + KeywordArg("dim"), + ) + _register_quantized_cat_lowering( + generate_pattern_with_output_quant(dequantize_cat_pattern), + aten.cat, + ) + + +def _register_quantized_reshape_lowering( + pattern, + computation_op, +): + @register_lowering_pattern( + pattern, + extra_check=_is_input_output_same_scale_zp(aten.reshape.default), + ) + def qreshape(match: Match, *args, **kwargs): + qx = kwargs["x"] + shape = kwargs["shape"] + counters["inductor"]["qreshape_matcher_count"] += 1 + counters["inductor"]["qreshape_matcher_nodes"] += len(match.nodes) + return L[computation_op](qx, shape) + + return qreshape + + +def _register_quantization_reshape(): + dequantize_reshape_pattern = CallFunction( + torch.ops.aten.reshape.default, + get_dequantize_per_tensor_activation_pattern(), + KeywordArg("shape"), + ) + _register_quantized_reshape_lowering( + generate_pattern_with_output_quant(dequantize_reshape_pattern), + aten.reshape, + ) + + +def _is_valid_concat_linear_int8_woq_optimization_pattern(): + def fn(match): + if not config.cpp.enable_concat_linear: + return False + assert all(k in match.kwargs for k in ("x", "w1", "w2", "w3", "scales")) + if not all( + hasattr(match.kwargs[key], "meta") + for key in ["x", "w1", "w2", "w3", "scales"] + ): + return False + x = match.kwargs["x"].meta["val"] + w1 = match.kwargs["w1"].meta["val"] + w2 = match.kwargs["w2"].meta["val"] + w3 = match.kwargs["w3"].meta["val"] + scales = match.kwargs["scales"].meta["val"] + if len(match.kwargs["scales"].meta["val"].size()) > 1: + return False + num_scales = match.kwargs["scales"].meta["val"].numel() + w1_cols = match.kwargs["w1"].meta["val"].size()[0] + w2_cols = match.kwargs["w2"].meta["val"].size()[0] + w3_cols = match.kwargs["w3"].meta["val"].size()[0] + return ( + # For now, we only support woq mm kernels + # with x.type=bfloat16 and w.type=int8 + x.dtype == torch.bfloat16 + and w1.dtype == torch.int8 + and w2.dtype == torch.int8 + and w3.dtype == torch.int8 + and scales.dtype == torch.bfloat16 + and x.device.type in ("cpu", "cuda") + and x.device == w1.device + and w1.device == w2.device + and w2.device == w3.device + and x.device == scales.device + and num_scales == w1_cols + w2_cols + w3_cols + ) + + return fn + + +def _is_valid_woq_optimization_pattern(): + def fn(match): + assert all(k in match.kwargs for k in ("x", "weight", "scales")) + if not all( + hasattr(match.kwargs[key], "meta") for key in ["x", "weight", "scales"] + ): + return False + x = match.kwargs["x"].meta["val"] + weight = match.kwargs["weight"].meta["val"] + scales = match.kwargs["scales"].meta["val"] + return ( + # For now, we only support woq mm kernels + # with x.type=bfloat16 and w.type=int8 + x.dtype == torch.bfloat16 + and weight.dtype == torch.int8 + and scales.dtype == torch.bfloat16 + and x.device.type in ("cpu", "cuda", "xpu") + and x.device == weight.device + and x.device == scales.device + ) + + return fn + + +def _register_concat_linear_int8_woq_lowering( + pattern, computation_woq, computation_reshape +): + @register_freezing_graph_pattern( + pattern, + extra_check=_is_valid_concat_linear_int8_woq_optimization_pattern(), + pass_number=4, + ) + def woq_int8(match: Match, *args, **kwargs): + x = kwargs["x"] + w1 = kwargs["w1"] + w2 = kwargs["w2"] + w3 = kwargs["w3"] + scales = kwargs["scales"] + counters["inductor"]["woq_matcher_count"] += 1 + counters["inductor"]["woq_matcher_nodes"] += len(match.nodes) + out_features = ( + w1.meta["val"].size()[0] + + w2.meta["val"].size()[0] + + w3.meta["val"].size()[0] + ) + origin_x_size = tuple(x.meta["val"].size()) + x_shape = [-1, origin_x_size[-1]] + out_shape = list(origin_x_size[:-1] + (out_features,)) + mm_node_of_x = None + for candidate in iter(x.users.keys()): + if ( + candidate.target is aten.mm.default + and list(candidate._input_nodes)[1].target is aten.cat.default + ): + mm_node_of_x = candidate + break + assert mm_node_of_x is not None, "unable to find mm node" + _, cat_wgt_node = mm_node_of_x._input_nodes + scaling_node = next(iter(mm_node_of_x.users.keys())) + user_of_scaling_node = next(iter(scaling_node.users.keys())) + # Some other pass is making some changes that entails + # adding a node before it's used, but it can only be found when + # lint is run. stable_topological_sort() is being run before lint, + # so that error was not being being discovered. + # We call stable_topological_sort here as a workaround. + stable_topological_sort(match.graph) + with match.graph.inserting_before(user_of_scaling_node): + new_cat_node = match.graph.call_function( + aten.cat.default, + args=([w1, w2, w3], 0), + ) + x_reshape_node = match.graph.call_function( + computation_reshape, args=(x, x_shape) + ) + new_woq_node = match.graph.call_function( + computation_woq, + args=(x_reshape_node, new_cat_node, scales), + ) + new_woq_node.meta = copy.copy(x.meta) + output_reshape_node = match.graph.call_function( + computation_reshape, args=(new_woq_node, out_shape) + ) + scaling_node.replace_all_uses_with(output_reshape_node) + match.graph.erase_node(scaling_node) + match.graph.erase_node(mm_node_of_x) + match.graph.erase_node(cat_wgt_node) + match.graph.lint() + + return woq_int8 + + +def _register_woq_lowering(pattern, computation_woq, computation_reshape): + @register_lowering_pattern( + pattern, + extra_check=_is_valid_woq_optimization_pattern(), + ) + def woq_int8(match: Match, *args, **kwargs): + x = kwargs["x"] + weight = kwargs["weight"] + scales = kwargs["scales"] + counters["inductor"]["woq_matcher_count"] += 1 + counters["inductor"]["woq_matcher_nodes"] += len(match.nodes) + out_features = weight.get_size()[0] + origin_x_size = x.get_size() + x_shape = [-1, origin_x_size[-1]] + out_shape = origin_x_size[:-1] + [ + out_features, + ] + func1 = L[computation_reshape](x, x_shape) + func2 = L[computation_woq](func1, weight, scales) + return L[computation_reshape](func2, out_shape) + + return woq_int8 + + +def _register_woq_mm_int8_pattern1(): + # F.linear(x, weight.to(dtype=x.dtype)) * scales + # case of dispatching to mm, with x reshape + _woq_pattern = CallFunction( + aten.mul.Tensor, + CallFunction( + aten.reshape.default, + CallFunction( + aten.mm.default, + CallFunction(aten.reshape.default, KeywordArg("x"), Arg()), + CallFunction( + aten.permute.default, + CallFunction( + prims.convert_element_type.default, KeywordArg("weight"), Arg() + ), + Arg(), + ), + ), + Arg(), + ), + KeywordArg("scales"), + ) + _register_woq_lowering(_woq_pattern, aten._weight_int8pack_mm.default, aten.reshape) + + +def _register_woq_mm_int8_pattern2(): + # F.linear(x, weight.to(dtype=x.dtype)) * scales + # case of dispatching to mm, w/o x reshape + _woq_pattern = CallFunction( + aten.mul.Tensor, + CallFunction( + aten.reshape.default, + CallFunction( + aten.mm.default, + KeywordArg("x"), + CallFunction( + aten.permute.default, + CallFunction( + prims.convert_element_type.default, KeywordArg("weight"), Arg() + ), + Arg(), + ), + ), + Arg(), + ), + KeywordArg("scales"), + ) + _register_woq_lowering(_woq_pattern, aten._weight_int8pack_mm.default, aten.reshape) + + +def _register_woq_mm_int8_pattern3(): + # F.linear(x, weight.to(dtype=x.dtype)) * scales + # case of dispatching to bmm + _woq_pattern = CallFunction( + aten.mul.Tensor, + CallFunction( + aten.bmm.default, + CallFunction(aten.expand.default, KeywordArg("x"), Arg()), + CallFunction( + aten.expand.default, + CallFunction( + aten.permute.default, + CallFunction( + prims.convert_element_type.default, KeywordArg("weight"), Arg() + ), + Arg(), + ), + Arg(), + ), + ), + KeywordArg("scales"), + ) + _register_woq_lowering(_woq_pattern, aten._weight_int8pack_mm.default, aten.reshape) + + +def _register_woq_mm_int8_pattern4(): + _woq_pattern = CallFunction( + aten.mul.Tensor, + CallFunction( + aten.mm.default, + KeywordArg("x"), + CallFunction( + prims.convert_element_type.default, + CallFunction( + aten.permute.default, + KeywordArg("weight"), + Arg(), + ), + Arg(), + ), + ), + KeywordArg("scales"), + ) + _register_woq_lowering(_woq_pattern, aten._weight_int8pack_mm.default, aten.reshape) + + +def _register_int8_woq_concat_linear_pattern(): + def _create_wgt_node(wgt_node_name: str): + return CallFunction( + prims.convert_element_type.default, + CallFunction( + aten.permute.default, + KeywordArg(wgt_node_name), + Arg(), + ), + Arg(), + ) + + cat_wgt = CallFunction( + aten.cat.default, [_create_wgt_node(wgt) for wgt in ["w1", "w2", "w3"]], 1 + ) + + _woq_pattern = CallFunction( + aten.mul.Tensor, + CallFunction(aten.mm.default, KeywordArg("x"), cat_wgt), + KeywordArg("scales"), + ) + _register_concat_linear_int8_woq_lowering( + _woq_pattern, aten._weight_int8pack_mm.default, aten.reshape + ) + + +def _register_quantization_lowerings(): + _register_quantization_unary_lowering() + _register_quantization_binary_lowering() + _register_quantization_maxpool2d() + _register_quantization_cat() + _register_quantization_reshape() + + +def _register_woq_lowerings(): + _register_woq_mm_int8_pattern1() + _register_woq_mm_int8_pattern2() + _register_woq_mm_int8_pattern3() + _register_woq_mm_int8_pattern4() + + +def _is_valid_dequant_promotion_pattern(dtype=torch.float32): + def _inner(match): + assert dtype in [torch.float32, torch.bfloat16] + dequant_pattern_end_node = match.output_node() + if dequant_pattern_end_node.target not in [ + quantized_decomposed.dequantize_per_tensor.default, + quantized_decomposed.dequantize_per_tensor.tensor, + prims.convert_element_type.default, + aten.reshape.default, + ]: + return False + + if dequant_pattern_end_node.target is aten.reshape.default: + dequant_node = ( + dequant_pattern_end_node.args[ + 0 + ] # pattern: linear <- reshape <- dequant + if dtype == torch.float32 + else dequant_pattern_end_node.args[0].args[ + 0 + ] # pattern: linear <- reshape <- to_bf16 <- dequant + ) + else: + dequant_node = ( + dequant_pattern_end_node # pattern: linear <- dequant + if dtype == torch.float32 + else dequant_pattern_end_node.args[ + 0 + ] # pattern: linear <- to_bf16 <- dequant + ) + + if ( + dequant_node.target + in [ + quantized_decomposed.dequantize_per_tensor.default, + quantized_decomposed.dequantize_per_tensor.tensor, + ] + and len(list(dequant_pattern_end_node.users)) > 1 + ): + # If dequant pattern has more than 1 users, then do dequant promoted + return True + return False + + return _inner + + +def _register_dequant_promotion_pass(pattern, pass_number, dtype=torch.float32): + @register_freezing_graph_pattern( + pattern, + extra_check=_is_valid_dequant_promotion_pattern(dtype), + pass_number=pass_number, + ) + def dequant_promotion(match: Match, *args, **kwargs): + # Dequant_promotion will transform + # graph 1: + # quant + # + - - - | - - - + + # | dequant | + # | / \ | + # | node1 node2 | + # + - | - - - | - + + # quant quant + # into: + # graph 2: + # quant + # + - - / - \ - - + + # |dequant dequant| + # | | | | + # | node1 node2 | + # + - | - - - | - + + # quant quant + # In graph 1, the dequant node is shared by node1 and node2, + # as a result, neither node1 nor node2 could form an int8 + # fusion pattern. + # After this transformation, the graph 2 could hit the int8 + # fusion pattern: dequant-node-quant, respectively for + # node1 and node2. + assert dtype in [torch.float32, torch.bfloat16] + + def clone_to_new_node(graph, source_node, user_node): + # Clone the source_node to a new node + # Replace user_node's input from source_node to new_node + assert source_node.op == "call_function", ( + "clone_to_new_node only support node.op call_function" + ) + with graph.inserting_before(user_node): + new_node = graph.call_function( + source_node.target, + args=source_node.args, + kwargs=source_node.kwargs, + ) + new_node.meta = copy.copy(source_node.meta) + user_node.replace_input_with(source_node, new_node) + return new_node + + # Find the start node and end node of a dequant pattern + # * End node should be the match.output_node() + # * Start node should be the node of dequantize_per_tensor + dequant_pattern_end_node = match.output_node() + assert dequant_pattern_end_node.target in [ + quantized_decomposed.dequantize_per_tensor.default, + quantized_decomposed.dequantize_per_tensor.tensor, + prims.convert_element_type.default, + aten.reshape.default, + ] + + # For a dequant pattern, we should expect see the node list as: + # * OPT(aten.reshape.default) + # * OPT(prims.convert_element_type.default) (to_bf16) + # * dequantize_per_tensor + def _find_first_node_in_dequant_pattern(_node): + if _node.target in [ + quantized_decomposed.dequantize_per_tensor.default, + quantized_decomposed.dequantize_per_tensor.tensor, + ]: + # For a dequant pattern, we expect the start node is a dequantize_per_tensor node + return _node + else: + assert len(_node.args) >= 1, ( + "In in dequant pattern, each node should have more than 1 arg." + ) + return _find_first_node_in_dequant_pattern(_node.args[0]) + + dequant_pattern_start_node = _find_first_node_in_dequant_pattern( + dequant_pattern_end_node + ) + + assert dequant_pattern_start_node.target in [ + quantized_decomposed.dequantize_per_tensor.default, + quantized_decomposed.dequantize_per_tensor.tensor, + ] + + # Clone the dequant pattern for each user node + graph = match.graph + user_node_list = list(dequant_pattern_end_node.users) + for user_node in user_node_list[1:]: + _source_node = dequant_pattern_end_node + _user_node = user_node + # pyrefly: ignore [bad-assignment, non-convergent-recursion] + while _source_node != dequant_pattern_start_node.args[0]: + _user_node = clone_to_new_node(graph, _source_node, _user_node) + _source_node = _source_node.args[0] # type: ignore[assignment] + + counters["inductor"]["dequant_promotion_matcher_count"] += 1 + counters["inductor"]["dequant_promotion_matcher_nodes"] += len(match.nodes) + + +def _is_valid_dequant_conv_pattern(dtype, with_dtype_convert): + def _inner(match): + # Here we do some further check to ensure: + # 1. It's a conv2d node with dim of 4, since we only support lowering of conv2d now. + # 2. The dequant pattern has only 1 user of conv2d node. + # If these conditions don't meet, we will not + # insert weight prepack node into the matched pattern. + conv_node = match.output_node() + assert conv_node.target is aten.convolution.default + input_meta_value = conv_node.args[0].meta.get("val") + weight_meta_value = conv_node.args[1].meta.get("val") + for meta_value in [input_meta_value, weight_meta_value]: + if ( + meta_value is None + or (meta_value.device.type != "cpu" and meta_value.device.type != "xpu") + or meta_value.dim() not in [3, 4] + ): + # Only support conv1d/2d now + return False + + assert dtype in [torch.float32, torch.bfloat16] + + if not with_dtype_convert: + dequant_node = conv_node.args[0] + else: + convert_to_bf16 = conv_node.args[0] + dequant_node = convert_to_bf16.args[0] + + if len(list(dequant_node.users)) != 1: + # Ensure the dequant pattern only has 1 user + # since we will delete the dequant pattern here + return False + return True + + return _inner + + +def _register_qconv_weight_prepack_pass( + pattern, pass_number, dtype=torch.float32, with_dtype_convert=False +): + @register_freezing_graph_pattern( + pattern, + extra_check=_is_valid_dequant_conv_pattern(dtype, with_dtype_convert), + pass_number=pass_number, + ) + def qconv_weight_prepack(match: Match, *args, **kwargs): + """ + Match the pattern: + int8 activation + | + dequant_per_tensor + | + Conv2d <- optional(aten.clone.default) <- dequant_per_channel <- int8_weight + + Insert weight prepack node and change the pattern to: + int8 activation + | + onednn.qconv_pointwise <- onednn.qconv_prepack <- int8_weight + """ + assert dtype in [torch.float32, torch.bfloat16] + conv_node = match.output_node() + assert conv_node.target is aten.convolution.default + if not with_dtype_convert: + dequant_node = conv_node.args[0] + else: + convert_to_bf16 = conv_node.args[0] + dequant_node = convert_to_bf16.args[0] # type: ignore[union-attr] + has_clone_to_channel_last_node_in_pattern = ( + conv_node.args[1].target is aten.clone.default # type: ignore[union-attr] + ) + clone_node = ( + conv_node.args[1] if has_clone_to_channel_last_node_in_pattern else None + ) + + if dtype == torch.float32: + dequant_per_channel = ( + clone_node.args[0] # type: ignore[union-attr] + if has_clone_to_channel_last_node_in_pattern + else conv_node.args[1] + ) + else: + weight_to_bf16_node = ( + clone_node.args[0] # type: ignore[union-attr] + if has_clone_to_channel_last_node_in_pattern + else conv_node.args[1] + ) + dequant_per_channel = weight_to_bf16_node.args[0] # type: ignore[union-attr] + + assert ( + dequant_per_channel.target # type: ignore[union-attr] + is quantized_decomposed.dequantize_per_channel.default + ) + + # Activation QParams + qx, x_zp, x_scale = ( + kwargs["x"], + kwargs["x_zp"], + kwargs["x_scale"], + ) + + # Weight QParams + qw, w_scale, w_zp = ( + kwargs["q_weight"], + kwargs["w_scale"], + kwargs["w_zp"], + ) + + # Conv Params + bias, stride, padding, dilation, groups = ( + kwargs["b"], + kwargs["stride"], + kwargs["padding"], + kwargs["dilation"], + kwargs["groups"], + ) + + x_shape = qx.meta.get("tensor_meta").shape + if has_free_symbols(x_shape): + # For dynamic shape case, we can't get activation shape ahead of runtime. + x_shape = None + graph = match.graph + with graph.inserting_before(conv_node): + # Insert weight prepack node and the QConv node + packed_weight_inputs = ( + qw, + w_scale, + x_scale, + x_zp, + stride, + padding, + dilation, + groups, + x_shape, + ) + packed_weight_op = torch.ops.onednn.qconv_prepack + prepack_weight_node = graph.call_function( + packed_weight_op, args=packed_weight_inputs + ) + + new_args: tuple[Any, ...] = ( + qx, + x_scale, + x_zp, + prepack_weight_node, + w_scale, + w_zp, + bias, + stride, + padding, + dilation, + groups, + 1.0, # output_scale + 0, # output_zero_point + dtype, # output_dtype + "none", # attr + [], # scalars + "", # algorithm + ) + new_conv_node = graph.call_function( + torch.ops.onednn.qconv_pointwise.default, args=new_args + ) + conv_node.replace_all_uses_with(new_conv_node) + new_conv_node.meta.update(conv_node.meta) + + # Erase the original conv node + graph.erase_node(conv_node) + # Erase the dequant pattern + if with_dtype_convert: + graph.erase_node(convert_to_bf16) # type: ignore[possibly-undefined, arg-type] + graph.erase_node(dequant_node) # type: ignore[arg-type] + # Erase the dequant per channel pattern + if clone_node is not None: + graph.erase_node(clone_node) # type: ignore[arg-type] + if dtype == torch.bfloat16: + graph.erase_node(weight_to_bf16_node) # type: ignore[possibly-undefined, arg-type] + graph.erase_node(dequant_per_channel) # type: ignore[arg-type] + counters["inductor"]["qconv_weight_prepack_matcher_count"] += 1 + counters["inductor"]["qconv_weight_prepack_matcher_nodes"] += len( + match.nodes + ) + + +def _generate_dequant_convolution_node_pattern( + _dequant_per_channel_pattern, dtype=torch.float32, with_dtype_convert=False +): + assert dtype in [torch.float32, torch.bfloat16] + dequant_convolution_node_pattern = CallFunction( + aten.convolution.default, + _may_generate_pattern_with_dtype_convert( + get_dequantize_per_tensor_activation_pattern(), + KeywordArg("autocast_act_dtype"), + with_dtype_convert, + ), + _dequant_per_channel_pattern, + KeywordArg("b"), + KeywordArg("stride"), + KeywordArg("padding"), + KeywordArg("dilation"), + KeywordArg("is_transposed"), + KeywordArg("out_padding"), + KeywordArg("groups"), + ) + return dequant_convolution_node_pattern + + +def _generate_qconv_weight_prepack_patterns( + dtype=torch.float32, with_dtype_convert=False +): + assert dtype in [torch.float32, torch.bfloat16] + return ( + _generate_dequant_convolution_node_pattern( + dequantize_per_channel_weight_pattern + if dtype == torch.float32 + else dequantize_per_channel_to_bf16_weight_pattern, + dtype, + with_dtype_convert, + ), + # There is another pattern due to the pass of convert_conv_weights_to_channels_last + # https://github.com/pytorch/pytorch/blob/07107919297db3f8ab37f11c12666b6d6d5f692e/torch/_inductor/freezing.py#L338-L362. + # Depend on some heuristics, it may or may not insert to(channel_last) node + # between convolution and dequant_per_channel node + _generate_dequant_convolution_node_pattern( + dequantize_per_channel_clone_weight_pattern + if dtype == torch.float32 + else dequantize_per_channel_to_bf16_clone_weight_pattern, + dtype, + with_dtype_convert, + ), + ) + + +def _get_linear_node(match, input_dim_exceeds_two, input_contiguous): + output_reshape_node = None + if input_dim_exceeds_two: + if input_contiguous: + output_reshape_node = match.output_node() + assert output_reshape_node.target is aten.reshape.default + linear_node = output_reshape_node.args[0] + else: + linear_nodes = filter_nodes(match.nodes, aten.bmm.default) + assert len(linear_nodes) == 1 + linear_node = linear_nodes[0] + else: + linear_node = match.output_node() + + assert linear_node.target in ( + aten.addmm.default, + aten.mm.default, + aten.bmm.default, + ) + return linear_node, output_reshape_node + + +def _get_linear_dq_node( + linear_node, + input_index, + input_dim_exceeds_two, + input_contiguous, + with_dtype_convert, +): + act_reshape_node = None + activation_to_bf16_node = None + act_expand_node = None + if input_dim_exceeds_two: + if input_contiguous: + act_reshape_node = linear_node.args[input_index] + assert act_reshape_node.target is aten.reshape.default + if not with_dtype_convert: + # pattern: linear -> reshape -> dequant + dequant_node = act_reshape_node.args[0] + else: + # pattern: linear -> reshape -> to_bf16 -> dequant + activation_to_bf16_node = act_reshape_node.args[0] + dequant_node = activation_to_bf16_node.args[0] + else: + # bmm pattern decomposed from linear when input dim exceeds 2 and not contiguous + act_expand_node = linear_node.args[input_index] + assert act_expand_node.target is aten.expand.default + if not with_dtype_convert: + dequant_node = act_expand_node.args[0] + else: + activation_to_bf16_node = act_expand_node.args[0] + dequant_node = activation_to_bf16_node.args[0] + else: + if not with_dtype_convert: + # pattern: linear -> dequant + dequant_node = linear_node.args[input_index] + else: + # pattern: linear -> to_bf16 -> dequant + activation_to_bf16_node = linear_node.args[input_index] + dequant_node = activation_to_bf16_node.args[0] + return dequant_node, act_reshape_node, activation_to_bf16_node, act_expand_node + + +def _is_valid_dequant_linear_pattern( + dtype, input_dim_exceeds_two, input_contiguous, with_dtype_convert +): + def _inner(match): + # Check dequant pattern has only 1 user. + ( + linear_node, + _, + ) = _get_linear_node(match, input_dim_exceeds_two, input_contiguous) + + input_index = 1 if linear_node.target is aten.addmm.default else 0 + assert dtype in [torch.float32, torch.bfloat16] + ( + dequant_node, + _, + _, + _, + ) = _get_linear_dq_node( + linear_node, + input_index, + input_dim_exceeds_two, + input_contiguous, + with_dtype_convert, + ) + + assert dequant_node.target in [ + quantized_decomposed.dequantize_per_tensor.default, + quantized_decomposed.dequantize_per_tensor.tensor, + ] + + if len(list(dequant_node.users)) != 1: + # Ensure the dequant pattern only has 1 user + # since we will delete the dequant pattern here + return False + + # Extra check for bmm pattern + if input_dim_exceeds_two and not input_contiguous: + # Check for act + # Act expand size should be exactly same as act size + act_expand_size = match.kwargs["act_expand_size"] + act_node = match.kwargs["x"] + if not ( + hasattr(act_node, "meta") + and isinstance(act_node.meta.get("val", None), torch.Tensor) + and (act_node.meta["val"].size() == torch.Size(act_expand_size)) + ): + return False + + # Check for wgt + # wgt permute dims should be [1, 0] + wgt_permute_dims = match.kwargs["permute_axes"] + if wgt_permute_dims != [1, 0]: + return False + + # Check below wgt size items: + # wgt before expand should with dim 2 + # Expand size should with dim 3 + # Expand size[0] should same as act size[0] + # Expand size[1] should same as wgt size[1] + # Expand size[2] should same as wgt size[0] + qweight_node = match.kwargs["q_weight"] + wgt_expand_size = match.kwargs["wgt_expand_size"] + if not ( + hasattr(qweight_node, "meta") + and isinstance(qweight_node.meta.get("val", None), torch.Tensor) + and len(qweight_node.meta["val"].size()) == 2 + and len(wgt_expand_size) == 3 + and wgt_expand_size[0] == act_node.meta["val"].size()[0] + and wgt_expand_size[1] == qweight_node.meta["val"].size()[1] + and wgt_expand_size[2] == qweight_node.meta["val"].size()[0] + ): + return False + + return True + + return _inner + + +def _register_qlinear_weight_prepack_pass( + pattern, + pass_number, + dtype=torch.float32, + input_dim_exceeds_two=False, + input_contiguous=True, + with_dtype_convert=False, +): + @register_freezing_graph_pattern( + pattern, + extra_check=_is_valid_dequant_linear_pattern( + dtype, input_dim_exceeds_two, input_contiguous, with_dtype_convert + ), + pass_number=pass_number, + ) + def qlinear_weight_prepack(match: Match, *args, **kwargs): + """ + Match the pattern: + int8 activation + | + dequant_per_tensor + | + mm/addmm <- t <- dequant_per_channel <- int8_weight + + Insert weight prepack node and change the pattern to: + int8 activation + | + onednn.qlinear_pointwise <- onednn.qlinear_prepack <- int8_weight + """ + assert dtype in [torch.float32, torch.bfloat16] + ( + linear_node, + output_reshape_node, + ) = _get_linear_node(match, input_dim_exceeds_two, input_contiguous) + input_index = 1 if linear_node.target is aten.addmm.default else 0 + weight_index = input_index + 1 + + ( + dequant_node, + act_reshape_node, + activation_to_bf16_node, + act_expand_node, + ) = _get_linear_dq_node( + linear_node, + input_index, + input_dim_exceeds_two, + input_contiguous, + with_dtype_convert, + ) + + if input_dim_exceeds_two and not input_contiguous: + wgt_expand_node = linear_node.args[weight_index] + assert wgt_expand_node.target is aten.expand.default + t_node = wgt_expand_node.args[0] + else: + t_node = linear_node.args[weight_index] + + if dtype == torch.float32: + dequant_per_channel = t_node.args[0] + else: + weight_to_bf16_node = t_node.args[0] + dequant_per_channel = weight_to_bf16_node.args[0] + assert ( + dequant_per_channel.target + is quantized_decomposed.dequantize_per_channel.default + ) + + # Activation QParams + qx, x_zp, x_scale = ( + kwargs["x"], + kwargs["x_zp"], + kwargs["x_scale"], + ) + + # Weight QParams + qw, w_scale, w_zp = ( + kwargs["q_weight"], + kwargs["w_scale"], + kwargs["w_zp"], + ) + + # Params + bias = kwargs.get("b") + + x_shape = qx.meta.get("tensor_meta").shape + if has_free_symbols(x_shape): + # For dynamic shape case, we can't get activation shape ahead of runtime. + x_shape = None + graph = match.graph + with graph.inserting_before(linear_node): + # Insert weight prepack node and the qlinear node + packed_weight_inputs = ( + qw, + x_shape, + ) + packed_weight_op = torch.ops.onednn.qlinear_prepack + prepack_weight_node = graph.call_function( + packed_weight_op, args=packed_weight_inputs + ) + + new_args: tuple[Any, ...] = ( + qx, + x_scale, + x_zp, + prepack_weight_node, + w_scale, + w_zp, + bias, + 1.0, # output_scale + 0, # output_zero_point + dtype, # output_dtype + "none", # post op name + [], # post op args + "", # post op algorithm + ) + Node = torch.fx.node.Node + if isinstance(x_scale, Node) and isinstance(x_zp, Node): + new_linear_node = graph.call_function( + torch.ops.onednn.qlinear_pointwise.tensor, args=new_args + ) + else: + new_linear_node = graph.call_function( + torch.ops.onednn.qlinear_pointwise.default, args=new_args + ) + if input_dim_exceeds_two: + if input_contiguous: + output_reshape_node.replace_all_uses_with(new_linear_node) + new_linear_node.meta.update(output_reshape_node.meta) + else: + if bias: + output_add_node_for_bias = match.output_node() + assert output_add_node_for_bias.target is aten.add.Tensor + output_add_node_for_bias.replace_all_uses_with(new_linear_node) + new_linear_node.meta.update(output_add_node_for_bias.meta) + else: + linear_node.replace_all_uses_with(new_linear_node) + new_linear_node.meta.update(linear_node.meta) + else: + linear_node.replace_all_uses_with(new_linear_node) + new_linear_node.meta.update(linear_node.meta) + + # Erase the original linear node + if input_dim_exceeds_two: + if input_contiguous: + graph.erase_node(output_reshape_node) + elif not input_contiguous and bias: + graph.erase_node(output_add_node_for_bias) # type: ignore[possibly-undefined] + graph.erase_node(linear_node) + if input_dim_exceeds_two: + if input_contiguous: + graph.erase_node(act_reshape_node) + else: + graph.erase_node(act_expand_node) + graph.erase_node(wgt_expand_node) # type: ignore[possibly-undefined] + if with_dtype_convert: + graph.erase_node(activation_to_bf16_node) + # Erase the dequant pattern + graph.erase_node(dequant_node) + # Erase the dequant per channel pattern + graph.erase_node(t_node) + if dtype == torch.bfloat16: + graph.erase_node(weight_to_bf16_node) # type: ignore[possibly-undefined] + graph.erase_node(dequant_per_channel) + + counters["inductor"]["qlinear_weight_prepack_matcher_count"] += 1 + counters["inductor"]["qlinear_weight_prepack_matcher_nodes"] += len( + match.nodes + ) + + +def _generate_dequant_linear_node_pattern( + _dequant_per_channel_pattern, + dtype=torch.float32, + input_dim_exceeds_two=False, + is_tensor_overload=False, + with_dtype_convert=False, +): + assert dtype in [torch.float32, torch.bfloat16] + t_pattern = _generate_linear_t_pattern(_dequant_per_channel_pattern, dtype) + dequant_linear_bias_pattern = _may_generate_pattern_with_reshape( + CallFunction( + aten.addmm.default, + KeywordArg("b"), + _may_generate_pattern_with_reshape( + _may_generate_pattern_with_dtype_convert( + get_dequantize_per_tensor_activation_pattern(is_tensor_overload), + KeywordArg("autocast_act_dtype"), + with_dtype_convert, + ), + KeywordArg("act_reshape_size"), + input_dim_exceeds_two, + ), + t_pattern, + ), + KeywordArg("output_reshape_size"), + input_dim_exceeds_two, + ) + dequant_linear_no_bias_pattern = _may_generate_pattern_with_reshape( + CallFunction( + aten.mm.default, + _may_generate_pattern_with_reshape( + _may_generate_pattern_with_dtype_convert( + get_dequantize_per_tensor_activation_pattern(is_tensor_overload), + KeywordArg("autocast_act_dtype"), + with_dtype_convert, + ), + KeywordArg("act_reshape_size"), + input_dim_exceeds_two, + ), + t_pattern, + ), + KeywordArg("output_reshape_size"), + input_dim_exceeds_two, + ) + return dequant_linear_bias_pattern, dequant_linear_no_bias_pattern + + +def _generate_dequant_bmm_node_pattern( + _dequant_per_channel_pattern, + dtype=torch.float32, + with_bias=False, + is_tensor_overload=False, + with_dtype_convert=False, +): + # When activation of linear dim exceed 2 and not contiguous + t_pattern = _generate_linear_t_pattern(_dequant_per_channel_pattern, dtype) + + assert dtype in [torch.float32, torch.bfloat16] + dequant_bmm_pattern = CallFunction( + aten.bmm.default, + CallFunction( + aten.expand.default, + _may_generate_pattern_with_dtype_convert( + get_dequantize_per_tensor_activation_pattern(is_tensor_overload), + KeywordArg("autocast_act_dtype"), + with_dtype_convert, + ), + KeywordArg("act_expand_size"), + ), + CallFunction( + aten.expand.default, + t_pattern, + KeywordArg("wgt_expand_size"), + ), + ) + + def _generate_pattern_with_output_add(_dequant_bmm_pattern, _with_bias): + if _with_bias: + return CallFunction( + aten.add.Tensor, + _dequant_bmm_pattern, + KeywordArg("b"), + ) + else: + return _dequant_bmm_pattern + + return _generate_pattern_with_output_add(dequant_bmm_pattern, with_bias) + + +def _generate_qlinear_weight_prepack_patterns( + dtype=torch.float32, + input_dim_exceeds_two=False, + input_contiguous=True, + with_bias=False, + is_tensor_overload=False, + with_dtype_convert=False, +): + if input_dim_exceeds_two and not input_contiguous: + return _generate_dequant_bmm_node_pattern( + dequantize_per_channel_weight_pattern, + dtype, + with_bias, + is_tensor_overload, + with_dtype_convert, + ) + else: + return _generate_dequant_linear_node_pattern( + dequantize_per_channel_weight_pattern, + dtype, + input_dim_exceeds_two, + is_tensor_overload, + with_dtype_convert, + ) + + +def _generate_linear_dynamic_fp16_pattern( + _dequant_weight_pattern, + input_dim_exceeds_two=False, + input_contiguous=True, + relu_fused=False, +): + dtype = torch.float32 + t_pattern = _generate_linear_t_pattern(_dequant_weight_pattern, dtype) + + if input_dim_exceeds_two and not input_contiguous: + # pattern is + # x -> expand -> bmm (-> add) (-> relu) + # w -> dequant -> permute -> expand / + pattern_no_bias = CallFunction( + aten.bmm.default, + CallFunction( + aten.expand.default, + KeywordArg("x"), + KeywordArg("act_expand_size"), + ), + CallFunction( + aten.expand.default, + t_pattern, + KeywordArg("wgt_expand_size"), + ), + ) + pattern_with_bias = CallFunction( + aten.add.Tensor, + pattern_no_bias, + KeywordArg("b"), + ) + if relu_fused: + pattern_with_bias = CallFunction(aten.relu.default, pattern_with_bias) + pattern_no_bias = CallFunction(aten.relu.default, pattern_no_bias) + return pattern_with_bias, pattern_no_bias + + x_pattern_with_reshape = _may_generate_pattern_with_reshape( + KeywordArg("x"), + KeywordArg("act_reshape_size"), + input_dim_exceeds_two, + ) + dequant_linear_bias_pattern = generate_pattern_with_unary( + _may_generate_pattern_with_reshape( + CallFunction( + aten.addmm.default, + KeywordArg("b"), + x_pattern_with_reshape, + t_pattern, + ), + KeywordArg("output_reshape_size"), + input_dim_exceeds_two, + ), + aten.relu.default if relu_fused else None, + ) + dequant_linear_no_bias_pattern = generate_pattern_with_unary( + _may_generate_pattern_with_reshape( + CallFunction( + aten.mm.default, + x_pattern_with_reshape, + t_pattern, + ), + KeywordArg("output_reshape_size"), + input_dim_exceeds_two, + ), + aten.relu.default if relu_fused else None, + ) + return dequant_linear_bias_pattern, dequant_linear_no_bias_pattern + + +def _register_dequant_promotion(): + dequant_pattern_cases = itertools.product( + [torch.float32, torch.bfloat16], [True, False], [True, False] + ) + for dtype, input_dim_exceeds_two, is_tensor_overload in dequant_pattern_cases: + # 4 dequantization patterns will be matched based on the dtype and input dimension size. + # Case 1: int8-mixed-fp32, input dim size is 2 + # Case 2: int8-mixed-fp32, input dim size exceeds 2 + # Case 3: int8-mixed-bf16, input dim size is 2 + # Case 4: int8-mixed-bf16, input dim size exceeds 2 + # quant + # + - - - - | - - - - + + # | dequant | + # | | | + # | OPT(to_bf16) | + # | | | + # | OPT(reshape) | + # | / \ | + # | node1 node2 | + # + - - | - - - | - - + + # OPT(reshape) OPT(reshape) + # + - - | - - - | - - + + # OPT(to_fp32) OPT(to_fp32) + # + - - | - - - | - - + + # quant quant + _register_dequant_promotion_pass( + _may_generate_pattern_with_reshape( + _may_generate_pattern_with_dtype_convert( + get_dequantize_per_tensor_activation_pattern( + is_tensor_overload=is_tensor_overload + ), + KeywordArg("autocast_act_dtype"), + dtype == torch.bfloat16, + ), + KeywordArg("act_reshape_size"), + with_reshape=input_dim_exceeds_two, + ), + pass_number=0, + dtype=dtype, + ) # pass_number=0 to run before weight prepack + + +def _register_qconv_weight_prepack(): + for dtype, with_dtype_convert in itertools.product( + [torch.float32, torch.bfloat16], [True, False] + ): + if dtype == torch.float32 and with_dtype_convert: + continue + weight_prepack_patterns = _generate_qconv_weight_prepack_patterns( + dtype, with_dtype_convert + ) + for weight_prepack_pattern in weight_prepack_patterns: + # Register to pass_number 1, so we can do dequant promotion in pass_number 0. + _register_qconv_weight_prepack_pass( + weight_prepack_pattern, + pass_number=1, + dtype=dtype, + with_dtype_convert=with_dtype_convert, + ) + + +def _register_qlinear_weight_prepack(): + # 6 Linear related patterns will be matched based on the dtype, input dimension size and input contiguous. + # Then convert the pattern into a QLinear node with int8_fp32/bf16. + # Case 1: int8-mixed-fp32, input dim size is 2 + # Case 2: int8-mixed-fp32, input dim size exceeds 2 and contiguous + # Case 3: int8-mixed-bf16, input dim size is 2 + # Case 4: int8-mixed-bf16, input dim size exceeds 2 and contiguous + + # + - - - - | - - - - - - | - - - - - + + # | dq_per_tensor dq_per_channel | + # | | | | + # | OPT(to_bf16) OPT(to_bf16) | + # | | | | + # | OPT(reshape) permute | + # | \ / | + # | addmm/mm | + # | | | + # | OPT(reshape) | + + # Case 5: int8-mixed-fp32, input dim size exceeds 2 and not contiguous + # Case 6: int8-mixed-bf16, input dim size exceeds 2 and not contiguous + + # + - - - - | - - - - - - | - - - - - + + # | dq_per_tensor dq_per_channel | + # | | | | + # | OPT(to_bf16) OPT(to_bf16) | + # | | | | + # | expand permute | + # | \ | | + # | expand | + # | / | + # | bmm | + # | | | + # | OPT(add) | + + linear_weight_prepack_cases = itertools.product( + [torch.float32, torch.bfloat16], [True, False], [True, False], [True, False] + ) + + # Step 1: register patterns from mm and addmm + for ( + dtype, + input_dim_exceeds_two, + is_tensor_overload, + with_dtype_convert, + ) in linear_weight_prepack_cases: + if dtype == torch.float32 and with_dtype_convert: + continue + weight_prepack_patterns = _generate_qlinear_weight_prepack_patterns( + dtype, + input_dim_exceeds_two, + is_tensor_overload=is_tensor_overload, + with_dtype_convert=with_dtype_convert, + ) + for weight_prepack_pattern in weight_prepack_patterns: + # Register to pass_number 1, so we can do dequant promotion in pass_number 0. + _register_qlinear_weight_prepack_pass( + weight_prepack_pattern, + pass_number=1, + dtype=dtype, + input_dim_exceeds_two=input_dim_exceeds_two, + with_dtype_convert=with_dtype_convert, + ) + + # Step 2: register patterns from bmm + # Linear might be decomposed into bmm when input dim exceeds 2 and not contiguous + # refer to: + # https://github.com/pytorch/pytorch/blob/80c07df659362a95da7cd4f3ec367abfdace38c4/torch/_decomp/decompositions.py#L3965-L3968 + # in this case, we can convert it back to qlinear + for ( + dtype, + with_bias, + is_tensor_overload, + with_dtype_convert, + ) in itertools.product( + [torch.float32, torch.bfloat16], [True, False], [True, False], [True, False] + ): + if dtype == torch.float32 and with_dtype_convert: + continue + bmm_pattern = _generate_qlinear_weight_prepack_patterns( + dtype=dtype, + input_dim_exceeds_two=True, + input_contiguous=False, + with_bias=with_bias, + is_tensor_overload=is_tensor_overload, + with_dtype_convert=with_dtype_convert, + ) + _register_qlinear_weight_prepack_pass( + bmm_pattern, + pass_number=1 + if with_bias + else 2, # if with_bias, there is an output add, so we should try to match it firstly + dtype=dtype, + input_dim_exceeds_two=True, + input_contiguous=False, + with_dtype_convert=with_dtype_convert, + ) + + +def _register_linear_dynamic_fp16_weight_prepack_pass( + pattern, + pass_number, + input_dim_exceeds_two=False, + input_contiguous=True, + relu_fused=False, +): + def _extra_check_fn(match: Match): + return match.kwargs["dtype_fp16"] == torch.float16 + + @register_freezing_graph_pattern( + pattern, + extra_check=_extra_check_fn, + pass_number=pass_number, + ) + def linear_dynamic_fp16_weight_prepack(match: Match, *args, **kwargs): + """ + Match the pattern: + fp32 activation + | + mm/addmm <- t <- to_fp32 <- to_fp16 <- weight + | + (reshape) <- (relu) + + OR + + fp32 activation + | + expand + | + bmm <- expand <- t <- to_fp32 <- to_fp16 <- weight + | + (add) <- (relu) + + Insert weight prepack node and change the pattern to: + fp32 activation + | + onednn.linear_dynamic_fp16 <- onednn.linear_prepack_fp16 <- weight + (or onednn.linear_relu_dynamic_fp16) + """ + # find params + x = kwargs["x"] + w = kwargs["w"] + bias = kwargs.get("b") + + # find linear node + nodes_to_find = [aten.addmm.default, aten.mm.default, aten.bmm.default] + linear_nodes = [] + for node in nodes_to_find: + linear_nodes.extend(filter_nodes(match.nodes, node)) + assert len(linear_nodes) == 1 + linear_node = linear_nodes[0] + assert isinstance(linear_node, torch.fx.node.Node) + input_index = 1 if linear_node.target is aten.addmm.default else 0 + weight_index = input_index + 1 + + # find relu node + relu_node = None + if relu_fused: + relu_node = match.output_node() + assert isinstance(relu_node, torch.fx.node.Node) + + # find reshape node, expand node and add node + ( + act_reshape_node, + output_reshape_node, + expand_x_node, + expand_w_node, + add_bias_node, + ) = (None, None, None, None, None) + t_node = None + if input_dim_exceeds_two: + if input_contiguous: + act_reshape_node = linear_node.args[input_index] + t_node = linear_node.args[weight_index] + output_reshape_node = next(iter(linear_node.users)) + assert output_reshape_node.target is aten.reshape.default + else: + expand_x_node = linear_node.args[input_index] + expand_w_node = linear_node.args[weight_index] + assert isinstance(expand_w_node, torch.fx.node.Node) + t_node = expand_w_node.args[0] + if bias: + add_bias_node = next(iter(linear_node.users)) + assert add_bias_node.target is aten.add.Tensor + else: + t_node = linear_node.args[weight_index] + assert isinstance(t_node, torch.fx.node.Node) + + w_to_fp32_node = t_node.args[0] + assert ( + isinstance(w_to_fp32_node, torch.fx.node.Node) + and w_to_fp32_node.target + is quantized_decomposed.convert_element_type.no_fuse + ) + w_to_fp16_node = w_to_fp32_node.args[0] + assert ( + isinstance(w_to_fp16_node, torch.fx.node.Node) + and w_to_fp16_node.target + is quantized_decomposed.convert_element_type.no_fuse + ) + + x_shape = x.meta.get("tensor_meta").shape + if has_free_symbols(x_shape): + # For dynamic shape case, we can't get activation shape ahead of runtime. + x_shape = None + graph = match.graph + with graph.inserting_before(linear_node): + # Insert weight prepack node and the qlinear node + packed_weight_inputs = ( + w, + x_shape, + ) + packed_weight_op = torch.ops.onednn.linear_prepack_fp16 + prepack_weight_node = graph.call_function( + packed_weight_op, args=packed_weight_inputs + ) + + # create new linear node and insert on graph + new_args: tuple[Any, ...] = ( + x, + prepack_weight_node, + bias, + ) + linear_op = ( + torch.ops.onednn.linear_relu_dynamic_fp16.default + if relu_fused + else torch.ops.onednn.linear_dynamic_fp16.default + ) + new_linear_node = graph.call_function(linear_op, args=new_args) + out_node = match.output_node() + out_node.replace_all_uses_with(new_linear_node) + + # Erase the original nodes in the reverse order + new_linear_node.meta.update(out_node.meta) + if relu_node is not None: + graph.erase_node(relu_node) + if output_reshape_node is not None: + graph.erase_node(output_reshape_node) + if add_bias_node is not None: + graph.erase_node(add_bias_node) + graph.erase_node(linear_node) + if act_reshape_node is not None: + assert isinstance(act_reshape_node, torch.fx.node.Node) + graph.erase_node(act_reshape_node) + if expand_x_node is not None: + assert isinstance(expand_x_node, torch.fx.node.Node) + graph.erase_node(expand_x_node) + if expand_w_node is not None: + assert isinstance(expand_w_node, torch.fx.node.Node) + graph.erase_node(expand_w_node) + graph.erase_node(t_node) + graph.erase_node(w_to_fp32_node) + graph.erase_node(w_to_fp16_node) + + counters["inductor"]["qlinear_weight_prepack_matcher_count"] += 1 + counters["inductor"]["qlinear_weight_prepack_matcher_nodes"] += len( + match.nodes + ) + + +def _register_linear_dynamic_fp16_weight_prepack(): + to_dtype_op = torch.ops.quantized_decomposed.convert_element_type.no_fuse + weight_pattern = CallFunction( + to_dtype_op, + CallFunction( + to_dtype_op, + KeywordArg("w"), + KeywordArg("dtype_fp16"), + ), + KeywordArg("dtype_fp32"), + ) + cases = itertools.product( + [False, True], # input_dim_exceeds_two + [True, False], # input_contiguous + [False, True], # relu fused + ) + for input_dim_exceeds_two, input_contiguous, relu_fused in cases: + patterns = _generate_linear_dynamic_fp16_pattern( + weight_pattern, + input_dim_exceeds_two, + input_contiguous, + relu_fused, + ) + for pattern in patterns: + _register_linear_dynamic_fp16_weight_prepack_pass( + pattern, + pass_number=0 if relu_fused else 1, + input_dim_exceeds_two=input_dim_exceeds_two, + input_contiguous=input_contiguous, + relu_fused=relu_fused, + ) + + +def _register_smooth_quant_int_mm_pattern(): + """ + The pattern is: + (no bias) reshape -> _int_mm -> convert_element_type -> (expand ->) mul -> mul -> reshape + or + (with bias) pattern_no_bias -> add (-> reshape -> reshape) + """ + + # When torch.compile'ing with dynamic=True, the expand node and the two tailing reshape nodes exist + # When torch.compile'ing with dynamic=False, they don't exist + def get_pattern_no_bias(expand_a_scale: bool, reshape_a: bool = True): + return CallFunction( + aten.mul.Tensor, + CallFunction( + aten.mul.Tensor, + CallFunction( + prims.convert_element_type.default, + CallFunction( + aten._int_mm.default, + CallFunction( + aten.reshape.default, + KeywordArg("a"), + KeywordArg("in_shape"), + ) + if reshape_a + else KeywordArg("a"), + KeywordArg("b"), + ), + KeywordArg("dtype"), + ), + ( + CallFunction( + aten.expand.default, + KeywordArg("x_scale"), + Arg(), + ) + if expand_a_scale + else KeywordArg("x_scale") + ), + ), + KeywordArg("w_scale"), + ) + + def _with_outer_reshape(pattern): + return CallFunction( + aten.reshape.default, pattern, KeywordArg("out_shape_no_bias") + ) + + # for torch.compile(dynamic=False) + pattern_no_bias_1 = _with_outer_reshape(get_pattern_no_bias(expand_a_scale=False)) + pattern_with_bias_1 = CallFunction( + aten.add.Tensor, + pattern_no_bias_1, + KeywordArg("bias"), + ) + # for torch.compile(dynamic=True) + pattern_no_bias_2 = _with_outer_reshape(get_pattern_no_bias(expand_a_scale=True)) + pattern_with_bias_2 = CallFunction( + aten.reshape.default, + CallFunction( + aten.reshape.default, + CallFunction( + aten.add.Tensor, + pattern_no_bias_2, + KeywordArg("bias"), + ), + Arg(), + ), + KeywordArg("out_shape_with_bias"), + ) + + # The following patterns are for torchao int8_dynamic_activation_int8_weight linear, + # when both activation and weights are symmetrically quantized. + # In practice, though, they may also match smooth-quant pattern when a 2D input shape would be used. + # Since add is not currently being used as a oneDNN post-op, but is unfused, we don't need these patterns with bias. + # Ideally, we should add mul + add post-op support in ATen int8 oneDNN linear op. + pattern1_with_no_outer_or_act_reshape = get_pattern_no_bias( + expand_a_scale=False, reshape_a=False + ) + pattern2_with_no_outer_or_act_reshape = get_pattern_no_bias( + expand_a_scale=True, reshape_a=False + ) + + def _validate_pattern(match: Match): + if len(match.nodes) not in [4, 5, 6, 7, 10]: + return False + # Make sure weight is a constant + aten_int_mm_node = filter_nodes(match.nodes, aten._int_mm.default)[0] + if not isinstance(aten_int_mm_node.args[1], torch.fx.node.Node): + return False + if aten_int_mm_node.args[1].op != "get_attr": + return False + + if len(match.nodes) == 10: + # Check the two tailing reshape nodes can be fused + if match.nodes[9].args[1] != match.nodes[6].args[1]: + return False + if len(match.nodes) == 10 or ( + len(match.nodes) == 7 and match.nodes[6].target is aten.add.Tensor + ): + bias_idx = 7 if len(match.nodes) == 10 else 6 + # Check bias shape + bias_node = match.nodes[bias_idx].args[1] + if not isinstance(bias_node, torch.fx.node.Node): + return False + if len(bias_node.meta.get("tensor_meta").shape) != 1: # type: ignore[union-attr] + return False + return True + + pattern_to_pass_number = { + pattern_no_bias_2: 0, + pattern_with_bias_2: 0, + pattern_no_bias_1: 1, + pattern_with_bias_1: 1, + pattern1_with_no_outer_or_act_reshape: 2, + pattern2_with_no_outer_or_act_reshape: 2, + } + for pattern, pass_number in pattern_to_pass_number.items(): + + @register_freezing_graph_pattern( + pattern, + extra_check=_validate_pattern, + pass_number=pass_number, + ) + def _int_mm_weight_prepack(match: Match, *args, **kwargs): + bias = kwargs.get("bias") + x = kwargs["a"] + weight = kwargs["b"] + dtype = kwargs["dtype"] + x_scale = kwargs["x_scale"] + w_scale = kwargs["w_scale"] + x_shape = x.meta.get("tensor_meta").shape + if has_free_symbols(x_shape): + # For dynamic shape case, we can't get activation shape ahead of runtime. + x_shape = None + + out_node = match.output_node() + with match.graph.inserting_before(out_node): + transpose_node = match.graph.call_function( + aten.permute.default, args=(weight, [1, 0]) + ) + contig_node = match.graph.call_function( + aten.contiguous.default, args=(transpose_node,) + ) + packed_weight_inputs = ( + contig_node, + x_shape, + ) + packed_weight_op = torch.ops.onednn.qlinear_prepack + prepack_weight_node = match.graph.call_function( + packed_weight_op, args=packed_weight_inputs + ) + + dummy_zp = None + w_scale = match.graph.call_function( + prims.convert_element_type.default, args=(w_scale, torch.float32) + ) + + x_scale_shape = x_scale.meta.get("tensor_meta").shape + x_scale_is_scalar = False + if not has_free_symbols(x_scale_shape): + prod = 1 + for d in x_scale_shape: + prod *= d + x_scale_is_scalar = prod == 1 + + new_args: tuple[Any, ...] + if x_scale_is_scalar: + # in this case, we can call onednn.qlinear directly + new_args = ( + x, + x_scale, + dummy_zp, # x_zp + prepack_weight_node, + w_scale, + dummy_zp, # w_zp + bias, + 1.0, # output_scale + 0, # output_zero_point + dtype, # output_dtype + "none", # post op name + [], # post op args + "", # post op algorithm + ) + new_linear_node = match.graph.call_function( + torch.ops.onednn.qlinear_pointwise.tensor, args=new_args + ) + out_node.replace_all_uses_with(new_linear_node) + new_linear_node.meta.update(out_node.meta) + else: + # onednn.qlinear does not support per-channel quantization of x + # so in this case, we have to apply x scale and add bias ourselves after qlinear + in_shape = kwargs.get("in_shape") + if in_shape is None: + x_reshaped = x + else: + x_reshaped = match.graph.call_function( + aten.reshape.default, args=(x, in_shape) + ) + new_args = ( + x_reshaped, + 1.0, # x_scale + 0, # x_zp + prepack_weight_node, + w_scale, + dummy_zp, # w_zp + None, # bias + 1.0, # output_scale + 0, # output_zero_point + dtype, # output_dtype + "none", # post op name + [], # post op args + "", # post op algorithm + ) + new_linear_node = match.graph.call_function( + torch.ops.onednn.qlinear_pointwise, args=new_args + ) + # apply x scale + new_out_node = match.graph.call_function( + aten.mul.Tensor, args=(new_linear_node, x_scale) + ) + + # Add bias and reshape + has_outer_reshape = ( + kwargs.get("out_shape_with_bias") is not None + or kwargs.get("out_shape_no_bias") is not None + ) + + if has_outer_reshape: + out_shape = kwargs.get( + "out_shape_with_bias", kwargs["out_shape_no_bias"] + ) + if bias is not None: + new_out_node = match.graph.call_function( + aten.add.Tensor, args=(new_out_node, bias) + ) + if has_outer_reshape: + new_out_node = match.graph.call_function( + aten.reshape.default, + args=(new_out_node, out_shape), # type: ignore[possibly-undefined] + ) + else: + if has_outer_reshape: + new_out_node = match.graph.call_function( + aten.reshape.default, + args=(new_out_node, out_shape), # type: ignore[possibly-undefined] + ) + out_node.replace_all_uses_with(new_out_node) + new_out_node.meta.update(out_node.meta) + for node in reversed(match.nodes): + match.graph.erase_node(node) + counters["inductor"]["qlinear_weight_prepack_matcher_count"] += 1 + counters["inductor"]["qlinear_weight_prepack_matcher_nodes"] += len( + match.nodes + ) + + +class PostOpAttr: + def __init__( + self, + binary_op_name: str = "none", + alpha=None, + unary_op_name: str = "none", + scalars_attr=None, + algorithm_attr=None, + ) -> None: + self.binary_op_name = binary_op_name + self.alpha = alpha if alpha else 1.0 + self.unary_op_name = unary_op_name + self.scalars_attr = scalars_attr if scalars_attr else [] + self.algorithm_attr = algorithm_attr if algorithm_attr else "" + + +def _register_qconv_post_op_fusion_pass( + pattern, + pass_number, + computation_op, + post_op_attr, +): + has_binary_post_op = post_op_attr.binary_op_name != "none" + + @register_freezing_graph_pattern( + pattern, + extra_check=_is_valid_qconv_post_op_fusion_pattern(has_binary_post_op), + pass_number=pass_number, + ) + def qconv(match: Match, *args, **kwargs): + # Activation QParams + x, x_scale, x_zp = ( + kwargs["x"], + kwargs["x_scale"], + kwargs["x_zp"], + ) + # Weight QParams + packed_weight, w_scale, w_zp = ( + kwargs["packed_weight"], + kwargs["w_scale"], + kwargs["w_zp"], + ) + # Conv Params + b, stride, padding, dilation, groups = ( + kwargs["b"], + kwargs["stride"], + kwargs["padding"], + kwargs["dilation"], + kwargs["groups"], + ) + output_dtype = _get_pattern_output_dtype(match) + assert output_dtype in [torch.int8, torch.uint8, torch.float32, torch.bfloat16] + # Output QParams + o_inv_scale = ( + kwargs["o_inv_scale"] + if (output_dtype == torch.uint8 or output_dtype == torch.int8) + else 1.0 + ) + o_zero_point = ( + kwargs["o_zp"] + if (output_dtype == torch.uint8 or output_dtype == torch.int8) + else 0 + ) + assert ( + kwargs["postop_name"] == "none" + ) # Expected no post op fused in weight prepack phase + if post_op_attr.unary_op_name == "hardtanh": + min_value = kwargs.get("min_value") + max_value = kwargs.get("max_value") + post_op_attr.scalars_attr = [min_value, max_value] + + out_node = match.output_node() + with match.graph.inserting_before(out_node): + if not has_binary_post_op: + computation_args: tuple[Any, ...] = ( + x, + x_scale, + x_zp, + packed_weight, + w_scale, + w_zp, + b, + stride, + padding, + dilation, + groups, + o_inv_scale, + o_zero_point, + output_dtype, + post_op_attr.unary_op_name, + post_op_attr.scalars_attr, + post_op_attr.algorithm_attr, + ) + else: + accum = ( + kwargs["accum"] + if output_dtype in [torch.uint8, torch.int8] + else kwargs["accum_after_dequant"] + ) + accum_scale = ( + kwargs["accum_scale"] + if output_dtype in [torch.uint8, torch.int8] + else 1.0 + ) + accum_zp = ( + kwargs["accum_zp"] + if output_dtype in [torch.uint8, torch.int8] + else 0 + ) + computation_args = ( + x, + x_scale, + x_zp, + packed_weight, + w_scale, + w_zp, + accum, + b, + stride, + padding, + dilation, + groups, + o_inv_scale, + o_zero_point, + output_dtype, + accum_scale, + accum_zp, + post_op_attr.binary_op_name, + post_op_attr.alpha, + post_op_attr.unary_op_name, + post_op_attr.scalars_attr, + post_op_attr.algorithm_attr, + ) + new_conv_node = match.graph.call_function( + computation_op, args=computation_args + ) + out_node.replace_all_uses_with(new_conv_node) + new_conv_node.meta.update(out_node.meta) + for node in reversed(match.nodes): + match.graph.erase_node(node) + count_key = ( + "qconv2d_binary_matcher_count" + if has_binary_post_op + else "qconv_unary_matcher_count" + ) + nodes_key = ( + "qconv2d_binary_matcher_nodes" + if has_binary_post_op + else "qconv_unary_matcher_nodes" + ) + counters["inductor"][count_key] += 1 + counters["inductor"][nodes_key] += len(match.nodes) + + return qconv + + +def _register_qconv_unary_fusion(): + from .mkldnn_fusion import _hardswish_fusion, _hardtanh_fusion, _silu_fusion + + for original_pattern_output_dtype in [torch.float32, torch.bfloat16]: + # Priority 1 to match: QConv2d Unary pattern with int8 output + # If a pattern1 is a sub-set of pattern2, we should try to match pattern2 firstly. + # For example: pattern1 is qconv_fp32 -> relu, pattern2 is qconv_fp32 -> relu -> quant + is_bf16 = original_pattern_output_dtype == torch.bfloat16 + conv_unary_replace_patterns = { + PostOpAttr( + "none", None, "none", [], "" + ): generate_pattern_with_output_quant( + get_qconv_pt2e_pattern(users=1), + ), + PostOpAttr( + "none", None, "relu", [], "" + ): generate_pattern_with_output_quant( + generate_pattern_with_unary( + get_qconv_pt2e_pattern(users=1), aten.relu.default + ), + ), + PostOpAttr( + "none", None, "hardtanh", [], "" + ): generate_pattern_with_output_quant( + _unary_fusion_pattern( + _hardtanh_fusion, + get_qconv_pt2e_pattern(users=1), + 1, + is_bf16, + ), + with_dtype_convert=is_bf16, + ), + PostOpAttr( + "none", None, "hardswish", [], "" + ): generate_pattern_with_output_quant( + _unary_fusion_pattern( + _hardswish_fusion, + get_qconv_pt2e_pattern(users=1 if is_bf16 else 2), + 2, + is_bf16, + ), + with_dtype_convert=is_bf16, + ), + PostOpAttr( + "none", None, "swish", [], "" + ): generate_pattern_with_output_quant( + _unary_fusion_pattern( + _silu_fusion, + get_qconv_pt2e_pattern(users=1 if is_bf16 else 2), + 2, + is_bf16, + ), + with_dtype_convert=is_bf16, + ), + } + + for unary_attr, patterns in conv_unary_replace_patterns.items(): + # Register qconv2d pattern for ExternKernel Lowering + _register_qconv_post_op_fusion_pass( + patterns, + 3, # pass_number + torch.ops.onednn.qconv_pointwise.default, # computation_op + unary_attr, # unary_attr + ) + + # Priority 2 to match: QConv2d Unary pattern with fp32/bfloat16 output + conv_unary_replace_float_out_patterns = { + PostOpAttr("none", None, "relu", [], ""): generate_pattern_with_unary( + get_qconv_pt2e_pattern(users=1), aten.relu.default + ), + PostOpAttr( + "none", None, "hardtanh", [], "" + ): _may_generate_pattern_with_dtype_convert( + _unary_fusion_pattern( + _hardtanh_fusion, + get_qconv_pt2e_pattern(users=1), + 1, + is_bf16, + ), + Arg(), + is_bf16, + ), + PostOpAttr( + "none", None, "hardswish", [], "" + ): _may_generate_pattern_with_dtype_convert( + _unary_fusion_pattern( + _hardswish_fusion, + get_qconv_pt2e_pattern(users=1 if is_bf16 else 2), + 2, + is_bf16, + ), + Arg(), + is_bf16, + ), + PostOpAttr( + "none", None, "swish", [], "" + ): _may_generate_pattern_with_dtype_convert( + _unary_fusion_pattern( + _silu_fusion, + get_qconv_pt2e_pattern(users=1 if is_bf16 else 2), + 2, + is_bf16, + ), + Arg(), + is_bf16, + ), + } + + for unary_attr, patterns in conv_unary_replace_float_out_patterns.items(): + # Register qconv2d pattern for ExternKernel Lowering + _register_qconv_post_op_fusion_pass( + patterns, + 4, # pass_number + torch.ops.onednn.qconv_pointwise.default, # computation_op + unary_attr, # unary_attr + ) + + +def _register_qconv_binary_fusion(): + for int8_mixed_bf16_with_inplace_add in [False, True]: + # Priority 1 to match: QConv2d Binary or Binary-Unary pattern with int8 output + swap_binary_inputs_list = [False, True] + binary_replace_patterns = {} + for swap_inputs in swap_binary_inputs_list: + binary_replace_patterns.update( + { + PostOpAttr( + "sum", 1.0, "none", [], "" + ): generate_pattern_with_output_quant( + generate_pattern_with_binary( + aten.add.Tensor, + get_qconv_pt2e_pattern(users=1), + dequantize_accum_pattern, + int8_mixed_bf16_with_inplace_add, + swap_inputs=swap_inputs, + ), + ), + PostOpAttr( + "sum", 1.0, "relu", [], "" + ): generate_pattern_with_output_quant( + generate_pattern_with_unary( + generate_pattern_with_binary( + aten.add.Tensor, + get_qconv_pt2e_pattern(users=1), + dequantize_accum_pattern, + int8_mixed_bf16_with_inplace_add, + swap_inputs=swap_inputs, + ), + aten.relu.default, + ), + ), + } + ) + + for binary_unary_attr, patterns in binary_replace_patterns.items(): + _register_qconv_post_op_fusion_pass( + patterns, + 3, # pass_number + torch.ops.onednn.qconv2d_pointwise.binary, # computation_op + binary_unary_attr, # binary_unary_attr + ) + + # Priority 2 to match: QConv2d Binary-Unary pattern with fp32/bfloat16 output + binary_replace_float_out_patterns = {} + for swap_inputs in swap_binary_inputs_list: + binary_replace_float_out_patterns.update( + { + PostOpAttr("sum", 1.0, "relu", [], ""): generate_pattern_with_unary( + generate_pattern_with_binary( + aten.add.Tensor, + get_qconv_pt2e_pattern(users=1), + KeywordArg("accum_after_dequant"), + int8_mixed_bf16_with_inplace_add, + swap_inputs=swap_inputs, + ), + aten.relu.default, + ) + } + ) + + for ( + binary_unary_attr, + patterns, + ) in binary_replace_float_out_patterns.items(): + if int8_mixed_bf16_with_inplace_add: + _register_qconv_post_op_fusion_pass( + patterns, + 3, # pass_number + torch.ops.onednn.qconv2d_pointwise.binary, # computation_op + binary_unary_attr, # binary_unary_attr + ) + else: + _register_qconv_post_op_fusion_pass( + patterns, + 4, # pass_number + torch.ops.onednn.qconv2d_pointwise.binary, # computation_op + binary_unary_attr, # binary_unary_attr + ) + + # Priority 3: QConv2d Binary pattern with fp32/bfloat16 output + binary_replace_float_out_patterns = {} + for swap_inputs in swap_binary_inputs_list: + binary_replace_float_out_patterns.update( + { + PostOpAttr( + "sum", 1.0, "none", [], "" + ): generate_pattern_with_binary( + aten.add.Tensor, + get_qconv_pt2e_pattern(users=1), + KeywordArg("accum_after_dequant"), + int8_mixed_bf16_with_inplace_add, + swap_inputs=swap_inputs, + ), + } + ) + + for ( + binary_unary_attr, + patterns, + ) in binary_replace_float_out_patterns.items(): + _register_qconv_post_op_fusion_pass( + patterns, + 4 if int8_mixed_bf16_with_inplace_add else 5, # pass_number + torch.ops.onednn.qconv2d_pointwise.binary, # computation_op + binary_unary_attr, # binary_unary_attr + ) + + +def _register_qlinear_post_op_fusion_pass( + pattern, + pass_number, + computation_op, + post_op_attr, +): + has_binary_post_op = post_op_attr.binary_op_name != "none" + + @register_freezing_graph_pattern( + pattern, + extra_check=_is_valid_qlinear_post_op_fusion_pattern(has_binary_post_op), + pass_number=pass_number, + ) + def qlinear_post_op_fusion(match: Match, *args, **kwargs): + """ + Match the pattern: + qlinear - post op + """ + output_dtype = _get_pattern_output_dtype(match) + # Activation QParams + x, x_scale, x_zp = ( + kwargs["x"], + kwargs["x_scale"], + kwargs["x_zp"], + ) + # Weight QParams + packed_weight, w_scale, w_zp = ( + kwargs["packed_weight"], + kwargs["w_scale"], + kwargs["w_zp"], + ) + + # bias + b = kwargs.get("b") + + # Output QParams + o_inv_scale = ( + kwargs["o_inv_scale"] + if (output_dtype in [torch.uint8, torch.int8]) + else 1.0 + ) + o_zero_point = ( + kwargs["o_zp"] if (output_dtype in [torch.uint8, torch.int8]) else 0 + ) + assert ( + kwargs["postop_name"] == "none" + ) # Expected no post op fused in weight prepack phase + + out_node = match.output_node() + with match.graph.inserting_before(out_node): + if not has_binary_post_op: + computation_args: tuple[Any, ...] = ( + x, + x_scale, + x_zp, + packed_weight, + w_scale, + w_zp, + b, + o_inv_scale, + o_zero_point, + output_dtype, + post_op_attr.unary_op_name, + post_op_attr.scalars_attr, + post_op_attr.algorithm_attr, + ) + else: + other = kwargs["other"] if "other" in kwargs else kwargs["accum"] + x2_scale = 1.0 + x2_zp = 0 + computation_args = ( + x, + x_scale, + x_zp, + packed_weight, + w_scale, + w_zp, + other, + b, + o_inv_scale, + o_zero_point, + output_dtype, + x2_scale, + x2_zp, + post_op_attr.binary_op_name, + post_op_attr.alpha, + post_op_attr.unary_op_name, + post_op_attr.scalars_attr, + post_op_attr.algorithm_attr, + ) + new_linear_node = match.graph.call_function( + computation_op, args=computation_args + ) + out_node.replace_all_uses_with(new_linear_node) + new_linear_node.meta.update(out_node.meta) + for node in reversed(match.nodes): + match.graph.erase_node(node) + count_key = ( + "qlinear_binary_matcher_count" + if has_binary_post_op + else "qlinear_unary_matcher_count" + ) + nodes_key = ( + "qlinear_binary_matcher_nodes" + if has_binary_post_op + else "qlinear_unary_matcher_nodes" + ) + counters["inductor"][count_key] += 1 + counters["inductor"][nodes_key] += len(match.nodes) + + +def _register_qlinear_unary_fusion(): + from .mkldnn_fusion import ( + _gelu_fusion_1 as _gelu_fusion_erf, + _gelu_fusion_2 as _gelu_fusion_tanh, + ) + + for original_pattern_output_dtype in [torch.float32, torch.bfloat16]: + is_bf16 = original_pattern_output_dtype == torch.bfloat16 + for x_scale_zp_are_tensors in (False, True): + qlinear_pattern = get_qlinear_pt2e_pattern(x_scale_zp_are_tensors) + computation_op = ( + torch.ops.onednn.qlinear_pointwise.tensor + if x_scale_zp_are_tensors + else torch.ops.onednn.qlinear_pointwise.default + ) + # Priority 1 to match: QLinear Unary pattern with int8 output + linear_unary_replace_patterns = { + PostOpAttr( + "none", None, "none", [], "" + ): generate_pattern_with_output_quant( + qlinear_pattern, + ), + PostOpAttr( + "none", None, "relu", [], "" + ): generate_pattern_with_output_quant( + generate_pattern_with_unary(qlinear_pattern, aten.relu.default), + ), + PostOpAttr( + "none", None, "gelu", [], "none" + ): generate_pattern_with_output_quant( + _unary_fusion_pattern( + _gelu_fusion_erf, + get_qlinear_pt2e_pattern( + x_scale_zp_are_tensors, 1 if is_bf16 else 2 + ), + 2, + is_bf16, + ), + with_dtype_convert=is_bf16, + ), + PostOpAttr( + "none", None, "gelu", [], "tanh" + ): generate_pattern_with_output_quant( + _unary_fusion_pattern( + _gelu_fusion_tanh, + get_qlinear_pt2e_pattern( + x_scale_zp_are_tensors, 1 if is_bf16 else 4 + ), + 4, + is_bf16, + ), + with_dtype_convert=is_bf16, + ), + } + + for unary_attr, patterns in linear_unary_replace_patterns.items(): + _register_qlinear_post_op_fusion_pass( + patterns, + 3, # pass_number + computation_op, + unary_attr, # unary_attr + ) + + # Priority 2 to match: QLinear Unary pattern with FP32/BF16 output + linear_unary_replace_float_out_patterns = { + PostOpAttr("none", None, "relu", [], ""): generate_pattern_with_unary( + qlinear_pattern, aten.relu.default + ), + PostOpAttr( + "none", None, "gelu", [], "none" + ): _may_generate_pattern_with_dtype_convert( + _unary_fusion_pattern( + _gelu_fusion_erf, + get_qlinear_pt2e_pattern( + x_scale_zp_are_tensors, 1 if is_bf16 else 2 + ), + 2, + is_bf16, + ), + Arg(), + is_bf16, + ), + PostOpAttr( + "none", None, "gelu", [], "tanh" + ): _may_generate_pattern_with_dtype_convert( + _unary_fusion_pattern( + _gelu_fusion_tanh, + get_qlinear_pt2e_pattern( + x_scale_zp_are_tensors, 1 if is_bf16 else 4 + ), + 4, + is_bf16, + ), + Arg(), + is_bf16, + ), + } + + for unary_attr, patterns in linear_unary_replace_float_out_patterns.items(): + _register_qlinear_post_op_fusion_pass( + patterns, + 4, # pass_number + computation_op, + unary_attr, # unary_attr + ) + + +def _register_qlinear_binary_fusion(): + r""" + Supported linear-binary(-unary) patterns + + linear(X) extra input + \ / + Add + | + Optional(relu) + | + Y + + 1. int8-mixed-fp32 + +---+---------------+-----------+------------------------------+---------+ + | # | Add type | Quant out | Pattern | Post op | + +---+---------------+-----------+------------------------------+---------+ + | 1 | In-/out-place | Yes | linear + fp32 -> (relu) -> q | add | + +---+---------------+-----------+------------------------------+---------+ + | 2 | In-/out-place | No | linear + fp32 -> (relu) | sum | + +---+---------------+-----------+------------------------------+---------+ + + 2. int8-mixed-bf16 + +---+----------+---------------+-----------+-----------------------------------------+---------+ + | # | X2 dtype | Add type | Quant out | Pattern | Post op | + +---+----------+---------------+-----------+-----------------------------------------+---------+ + | 1 | BF16 | In-/out-place | Yes | linear + bf16 -> (relu) -> q | add | + +---+----------+---------------+-----------+-----------------------------------------+---------+ + | 2 | BF16 | In-/out-place | No | linear + bf16 -> (relu) | sum | + +---+----------+---------------+-----------+-----------------------------------------+---------+ + | 3 | FP32 | Out-place | Yes | linear + fp32 -> (relu) -> q | add | + | | | In-place right| | | | + +---+----------+---------------+-----------+-----------------------------------------+---------+ + | 4 | FP32 | Out-place | No | linear + fp32 -> (relu) | sum | + | | | In-place right| | | | + +---+----------+---------------+-----------+-----------------------------------------+---------+ + | 5 | FP32 | In-place left | Yes | linear + fp32 -> to_bf16 -> (relu) -> q | add | + +---+----------+---------------+-----------+-----------------------------------------+---------+ + | 6 | FP32 | In-place left | No | linear + fp32 -> to_bf16 -> (relu) | add | + +---+----------+---------------+-----------+-----------------------------------------+---------+ + + Note + (1) The positions of linear and the extra input can be swapped. + (2) we don't insert q-dq before the extra input of linear-add by recipe. But if q-dq is found at the + extra input, we don't match that pattern because we cannot match all these patterns in 3 passes. + """ + for x_scale_zp_are_tensors in (False, True): + qlinear_binary_op = ( + torch.ops.onednn.qlinear_pointwise.binary_tensor + if x_scale_zp_are_tensors + else torch.ops.onednn.qlinear_pointwise.binary + ) + unary_postop_list = ["none", "relu"] + unary_postop_dict = { + "none": None, + "relu": aten.relu.default, + } + convert_dtype_after_binary_list = [False, True] + + # Priority 1 to match: QLinear Binary or Binary-Unary pattern with int8 output + # Covers case (1) of int8-mixed-fp32 and case (1)(3)(5) of int8-mixed-bf16, + # totally 3 patterns (2 are identical) + swap_binary_inputs_list = [False, True] + int8_mixed_bf16_list = [False, True] + combinations = itertools.product( + unary_postop_list, + int8_mixed_bf16_list, + swap_binary_inputs_list, + convert_dtype_after_binary_list, + ) + qlinear_binary_replace_patterns = {} + for unary_op, int8_mixed_bf16, swap_inputs, cvt_dtype_binary in combinations: + if not int8_mixed_bf16 and cvt_dtype_binary: + # No convert node after binary node if dtypes are all fp32 + continue + qlinear_binary_replace_patterns.update( + { + PostOpAttr( + "add", 1.0, unary_op, [], "" + ): generate_pattern_with_output_quant( + generate_pattern_with_unary( + generate_pattern_with_binary( + aten.add.Tensor, + get_qlinear_pt2e_pattern(x_scale_zp_are_tensors), + KeywordArg("other"), + # If fp32 extra input is inplace added to bf16 linear output, + # a to_bf16 node is inserted after binary + dtype_convert=cvt_dtype_binary, + swap_inputs=swap_inputs, + ), + unary_postop_dict[unary_op], + ), + ) + } + ) + for binary_unary_attr, patterns in qlinear_binary_replace_patterns.items(): + _register_qlinear_post_op_fusion_pass( + patterns, + 3, # pass_number + qlinear_binary_op, # computation_op + binary_unary_attr, + ) + + # Priority 2.1 to match: QLinear Binary-Unary pattern with fp32/bfloat16 output + # Covers case (2) of int8-mixed-fp32 and case (2)(4) of int8-mixed-bf16, + # totally 2 patterns (2 are identical) + binary_replace_float_out_patterns = {} + for swap_binary_inputs in swap_binary_inputs_list: + binary_replace_float_out_patterns.update( + { + PostOpAttr("sum", 1.0, "relu", [], ""): generate_pattern_with_unary( + generate_pattern_with_binary( + aten.add.Tensor, + get_qlinear_pt2e_pattern(x_scale_zp_are_tensors), + KeywordArg("accum"), + dtype_convert=False, + swap_inputs=swap_binary_inputs, + ), + aten.relu.default, + ), + } + ) + for ( + binary_unary_attr, + patterns, + ) in binary_replace_float_out_patterns.items(): + _register_qlinear_post_op_fusion_pass( + patterns, + 4, # pass_number + qlinear_binary_op, # computation_op + binary_unary_attr, + ) + # Priority 2.2 to match: QLinear Binary-Unary pattern with fp32/bfloat16 output + # Covers case (6) of int8-mixed-bf16 + binary_replace_float_out_patterns = {} + for swap_binary_inputs in swap_binary_inputs_list: + binary_replace_float_out_patterns.update( + { + PostOpAttr("add", 1.0, "relu", [], ""): generate_pattern_with_unary( + generate_pattern_with_binary( + aten.add.Tensor, + get_qlinear_pt2e_pattern(x_scale_zp_are_tensors), + KeywordArg("other"), + dtype_convert=True, + swap_inputs=swap_binary_inputs, + ), + aten.relu.default, + ), + } + ) + for ( + binary_unary_attr, + patterns, + ) in binary_replace_float_out_patterns.items(): + _register_qlinear_post_op_fusion_pass( + patterns, + 4, # pass_number + qlinear_binary_op, # computation_op + binary_unary_attr, + ) + + # Priority 3.1: QLinear Binary pattern with fp32/bfloat16 output + # Covers case (2) of int8-mixed-fp32 and case (2)(4) of int8-mixed-bf16, + # totally 2 patterns (2 are identical) + binary_replace_float_out_patterns = {} + for swap_binary_inputs in swap_binary_inputs_list: + binary_replace_float_out_patterns.update( + { + PostOpAttr( + "sum", 1.0, "none", [], "" + ): generate_pattern_with_binary( + aten.add.Tensor, + get_qlinear_pt2e_pattern(x_scale_zp_are_tensors), + KeywordArg("accum"), + dtype_convert=False, + swap_inputs=swap_binary_inputs, + ), + } + ) + for ( + binary_unary_attr, + patterns, + ) in binary_replace_float_out_patterns.items(): + _register_qlinear_post_op_fusion_pass( + patterns, + 5, # pass_number + qlinear_binary_op, # computation_op + binary_unary_attr, + ) + # Priority 3.2: QLinear Binary pattern with fp32/bfloat16 output + # Covers (6) of int8-mixed-bf16 + binary_replace_float_out_patterns = {} + for swap_binary_inputs in swap_binary_inputs_list: + binary_replace_float_out_patterns.update( + { + PostOpAttr( + "add", 1.0, "none", [], "" + ): generate_pattern_with_binary( + aten.add.Tensor, + get_qlinear_pt2e_pattern(x_scale_zp_are_tensors), + KeywordArg("other"), + dtype_convert=True, + swap_inputs=swap_binary_inputs, + ), + } + ) + for ( + binary_unary_attr, + patterns, + ) in binary_replace_float_out_patterns.items(): + _register_qlinear_post_op_fusion_pass( + patterns, + 5, # pass_number + qlinear_binary_op, # computation_op + binary_unary_attr, + ) + + +@functools.cache +def _register_quantization_weight_pack_pass(): + # Step 1: Dequant promotion for int8-mixed-fp32/bf16 + _register_dequant_promotion() + + # Step 2: QConv weight prepack + _register_qconv_weight_prepack() + + # Step 3: QLinear weight prepack + _register_qlinear_weight_prepack() + _register_linear_dynamic_fp16_weight_prepack() + + # Step 4: weight prepack for SmoothQuant from Torchao + _register_smooth_quant_int_mm_pattern() + + # Step 5: QLinear post op Fusion + if not torch.ops.mkldnn._is_mkldnn_acl_supported(): + # skip fusion on ARM + _register_qconv_unary_fusion() + _register_qconv_binary_fusion() + _register_qlinear_unary_fusion() + _register_qlinear_binary_fusion() + + +def _is_valid_concat_linear_woq_int4_fusion(computation_nodes): + computation_op = torch.ops.aten._weight_int4pack_mm_for_cpu.default + act = computation_nodes[0].args[0] + wgt = computation_nodes[0].args[1] + in_feature_size = wgt.meta.get("val").size(1) # type: ignore[union-attr] + group_size = computation_nodes[0].args[2] + return len(computation_nodes) >= 2 and all( + ( + node.target == computation_op + and node.args[0] == act # share same activation + and ( + node.args[1].meta.get("val").size(1) == in_feature_size + ) # same in feature size + and (node.args[1] != wgt or gemm_idx == 0) + and node.args[1].op == "get_attr" # wgt are all constants + and node.args[2] == group_size # same group size + ) + for gemm_idx, node in enumerate(computation_nodes) + ) + + +def concat_linear_woq_int4(gm: torch.fx.GraphModule): + """ + Concat Linear optimization pass for WOQ int4 + This pass fuses the original pattern: + def ... + return (woq_int4(x, w1, group_size, scale_zp1), woq_int4(x, w2, group_size, scale_zp1) ...) + into a single operation: + def ... + concat_res = woq_int4(x, concat_w, group_size, concat_scale_zp) + return split(concat_res, split_size_list) + """ + + def concat_wgt(packed_wgts, scale_zps, group_size, act_dtype): + # Concat the wgts and scale_zps, and repack the wgt + unpacked_wgts = [] + for packed_wgt in packed_wgts: + # Get the unpacked weight list + # Same as https://github.com/pytorch/pytorch/pull/156174 + K = packed_wgt.size(1) * 2 + N = packed_wgt.size(0) + x = torch.eye(K).to(dtype=act_dtype) + qscales_and_zeros = ( + torch.tensor([1.0, 8.0]) + .to(dtype=act_dtype) + .expand(K // group_size, N, 2) + .contiguous() + ) + unpacked_wgts.append( + torch.ops.aten._weight_int4pack_mm_for_cpu( + x, + packed_wgt, + group_size, + qscales_and_zeros, + ) + .t() + .contiguous() + .to(torch.int32) # N, K + ) + concat_unpacked_wgt = torch.cat(unpacked_wgts, dim=0) + repack_w = torch.ops.aten._convert_weight_to_int4pack_for_cpu( + concat_unpacked_wgt, 1 + ) + concat_scale_zp = torch.cat(scale_zps, dim=1).contiguous() + return repack_w, concat_scale_zp + + graph = gm.graph + computation_op = torch.ops.aten._weight_int4pack_mm_for_cpu.default + for node in graph.find_nodes(op="call_function", target=computation_op): + if ( + not node._erased + and isinstance(node.meta.get("val"), torch.Tensor) + and node.meta["val"].device.type == "cpu" + ): + act = node.args[0] + users = list(act.users) + if _is_valid_concat_linear_woq_int4_fusion(users): + with graph.inserting_before(node): + assert all(user.args[1].op == "get_attr" for user in users) + computation_node_0 = users[0] + packed_wgts = [getattr(gm, user.args[1].target) for user in users] + group_size = computation_node_0.args[2] + scale_zps = [getattr(gm, user.args[3].target) for user in users] + out_feature_size_list = [ + packed_wgt.size(0) for packed_wgt in packed_wgts + ] + repack_w, concat_scale_zp = concat_wgt( + packed_wgts, scale_zps, group_size, act.meta.get("val").dtype + ) + repack_w_node_name = computation_node_0.args[1].target + "_concat" + concat_scale_zp_node_name = ( + computation_node_0.args[3].target + "_concat" + ) + gm.register_buffer(repack_w_node_name, repack_w) + setattr(gm, repack_w_node_name, repack_w) + gm.register_buffer(concat_scale_zp_node_name, concat_scale_zp) + setattr(gm, concat_scale_zp_node_name, concat_scale_zp) + + repack_w_node = graph.create_node( + "get_attr", repack_w_node_name, (), {} + ) + with graph.inserting_after(repack_w_node): + concat_scale_zp_node = graph.create_node( + "get_attr", concat_scale_zp_node_name, (), {} + ) + + with graph.inserting_after(concat_scale_zp_node): + concat_int4_gemm_node = graph.create_node( + "call_function", + computation_op, + ( + act, + repack_w_node, + group_size, + concat_scale_zp_node, + ), + ) + with graph.inserting_after(concat_int4_gemm_node): + split_node = graph.create_node( + "call_function", + torch.ops.aten.split_with_sizes.default, + ( + concat_int4_gemm_node, + out_feature_size_list, + 1, # split dim + ), + ) + with graph.inserting_after(split_node): + for gemm_idx, user in enumerate(users): + assert user.target == computation_op + get_item = graph.create_node( + "call_function", + operator.getitem, + ( + split_node, + gemm_idx, + ), + ) + with graph.inserting_after(get_item): + clone_node = graph.create_node( + "call_function", + torch.ops.aten.clone.default, + (get_item,), + {"memory_format": torch.contiguous_format}, + ) + user.replace_all_uses_with(clone_node) + graph.erase_node(user) + + +def quant_lift_up(graph_module: torch.fx.GraphModule): + """ + Lift up the quant node before view like nodes. It can benefit performance + of Attention like block. For example, we have the pattern as: + + DQ + DQ LINEAR + LINEAR VIEW + VIEW PERMUTE + PERMUTE TRANSPOSE + Q Q + DQ DQ + Matmul + DIV + ADD + SOFTMAX + + We want to lift up the quant nodes from matmul before view like nodes + as the output of Linear node. + + DQ + DQ LINEAR + LINEAR Q + Q VIEW + VIEW PERMUTE + PERMUTE TRANSPOSE + DQ DQ + Matmul + DIV + ADD + SOFTMAX + + It produces a DQ->LINEAR->Q pattern which can be fused by backend. + """ + + def is_view_op(node): + return node.op == "call_function" and node.target in _VIEW_OPS + + for node in graph_module.graph.nodes: + # Leslie: Here we verify that the quant node has exactly + # one input FX node, with constant scalar value for scale and zero point. + # For the case input of quant node has more than one input FX nodes, + # extend the implementation to lift up all the connected nodes + # before the view nodes to keep the topological order. + if ( + node.op == "call_function" + and node.target in _PER_TENSOR_QUANTIZE_OPS + and len(node.all_input_nodes) == 1 + and is_view_op(node.all_input_nodes[0]) + ): + quant_node = node + input_node_of_quant = quant_node.args[0] + + # Check the nodes along lift up path has only 1 user node + # Propagate view like node to find where to insert the new quant node + could_lift_up = True + current_node = quant_node + input_node = current_node.args[0] + while is_view_op(input_node): + if len(input_node.users) != 1: + could_lift_up = False + break + current_node = input_node + input_node = current_node.args[0] + + # Further check the input node of the first view node has only 1 user node + if could_lift_up and len(input_node.users) == 1: + counters["inductor"]["quant_lift_up_count"] += 1 + # Replace dequant's input from quant to quant's input + quant_node.replace_all_uses_with(input_node_of_quant) + # Insert the new quant node + with graph_module.graph.inserting_before(current_node): + new_quant_node = graph_module.graph.node_copy(quant_node) + input_node.replace_all_uses_with(new_quant_node) + + # Update inputs of new_quant_node + def maybe_replace_node(n: torch.fx.Node) -> torch.fx.Node: + if n == input_node_of_quant: + return input_node + else: + return n + + new_args = map_arg(new_quant_node.args, maybe_replace_node) + new_kwargs = map_arg(new_quant_node.kwargs, maybe_replace_node) + new_quant_node.args = new_args # type: ignore[assignment] + new_quant_node.kwargs = new_kwargs # type: ignore[assignment] + graph_module.graph.erase_node(quant_node) + + graph_module.graph.lint() + graph_module.recompile() diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/reduced_atomic_contention.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/reduced_atomic_contention.py new file mode 100644 index 0000000000000000000000000000000000000000..a4961b148190ce29d6cfcbf0566ca7498d2f0606 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/reduced_atomic_contention.py @@ -0,0 +1,397 @@ +# mypy: allow-untyped-defs +""" +Partitioned Scatter Optimization for Reduced Atomic Contention. + +This pass transforms high-contention index_put operations by distributing +writes across multiple partitions, reducing atomic contention. +""" + +import logging +import math +from typing import Any + +import torch +import torch.fx as fx +from torch._dynamo.utils import counters +from torch._inductor import config +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + Match, + PatternMatcherPass, + register_graph_pattern, +) + + +log = logging.getLogger(__name__) +aten = torch.ops.aten +prims = torch.ops.prims + + +def _get_min_partitions() -> int: + """Get minimum partitions from config.""" + return getattr(config, "partitioned_scatter_min_partitions", 2) + + +def _get_max_partitions() -> int: + """Get maximum partitions from config.""" + return getattr(config, "partitioned_scatter_max_partitions", 128) + + +def _get_memory_budget_fraction() -> float: + """Get memory budget fraction from config.""" + return getattr(config, "partitioned_scatter_memory_budget", 0.10) + + +partitioned_scatter_patterns = PatternMatcherPass( + pass_name="partitioned_scatter_optimization" +) + + +def partitioned_scatter_optimization_pass(graph: fx.Graph) -> fx.Graph: + """ + Apply partitioned scatter optimization to high-contention index_put operations. + + Reduces atomic contention by distributing writes across multiple buffers. + Controlled by: config.partitioned_scatter_enabled + """ + if not getattr(config, "partitioned_scatter_enabled", False): + return graph + + num_matches = partitioned_scatter_patterns.apply(graph) + + if num_matches > 0: + log.info( + "partitioned_scatter_optimization: applied to %d operation(s)", + num_matches, + ) + graph.lint() + + return graph + + +def validate_match(match: Match) -> bool: + """Check if pattern match should be optimized for scatter.""" + output_node = match.output_node() + if not output_node or not hasattr(output_node, "args") or len(output_node.args) < 4: + return False + + # Only apply when accumulating + if output_node.args[3] is not True: + log.debug("Skipping: accumulate=False") + return False + + # Extract metadata + input_node = output_node.args[0] + indices_arg = output_node.args[1] + + # Validate input_node is an FX Node + if not isinstance(input_node, fx.Node): + return False + + scatter_dim, index_node = _extract_scatter_dim_and_index(indices_arg) + if scatter_dim is None or index_node is None: + return False + + # Get tensor shapes and validate + input_meta = _get_tensor_meta(input_node) + index_meta = _get_tensor_meta(index_node) + if not input_meta or not index_meta: + return False + + # Skip unsupported cases + if isinstance(input_meta["numel"], torch.SymInt) or isinstance( + index_meta["numel"], torch.SymInt + ): + log.debug("Skipping: dynamic shapes not supported") + return False + + if input_meta["dtype"] == torch.bool or index_meta["dtype"] == torch.bool: + log.debug("Skipping: bool dtype not supported") + return False + + if scatter_dim >= len(input_meta["shape"]): + log.debug("Skipping: scatter dim %d out of bounds", scatter_dim) + return False + + # Calculate optimal partitions and check memory + output_size = input_meta["numel"] + index_size = index_meta["numel"] + + # Safety check (also done in _estimate_optimal_partitions) + if output_size == 0 or index_size == 0: + return False + + contention_ratio = index_size / output_size + + # Check minimum index size threshold + min_index_size = getattr(config, "partitioned_scatter_min_index_size", 4096) + if index_size < min_index_size: + log.debug( + "Skipping: index size %d below threshold %d", index_size, min_index_size + ) + return False + + # Get optimal partitions and adjust for memory constraints + num_partitions = _estimate_optimal_partitions(output_size, index_size) + num_partitions = _fit_to_memory_budget( + output_size, num_partitions, input_meta["dtype"] + ) + + # If reduced to < min partitions, optimization not worthwhile + if num_partitions < _get_min_partitions(): + log.debug("Skipping: insufficient memory for minimum partitions") + return False + + # Store optimization parameters for replacement + match._num_partitions = num_partitions # type: ignore[attr-defined] + match._scatter_dim = scatter_dim # type: ignore[attr-defined] + match._index_node = index_node # type: ignore[attr-defined] + + log.debug( + "Applying optimization: %d partitions, dim=%d, contention=%.2f, " + "output_size=%d, index_size=%d", + num_partitions, + scatter_dim, + contention_ratio, + output_size, + index_size, + ) + + return True + + +@register_graph_pattern( + CallFunction(aten.index_put.default, Arg(), Arg(), Arg(), True), + pass_dict=partitioned_scatter_patterns, # type: ignore[arg-type] + extra_check=validate_match, +) +@register_graph_pattern( + CallFunction(aten.index_put_.default, Arg(), Arg(), Arg(), True), + pass_dict=partitioned_scatter_patterns, # type: ignore[arg-type] + extra_check=validate_match, +) +def create_replacement(match: Match, input_tensor, indices, values) -> None: + """Replace high-contention index_put with partitioned scatter.""" + # Get optimization parameters (set in validate_match) + num_partitions: int = match._num_partitions # type: ignore[attr-defined] + scatter_dim: int = match._scatter_dim # type: ignore[attr-defined] + index_node = match._index_node # type: ignore[attr-defined] + + def repl(input_tensor, index_node, values): + """Partitioned scatter implementation that will be traced.""" + dim_size = input_tensor.shape[scatter_dim] + num_operations = index_node.numel() + + # Flatten if needed + if len(index_node.shape) > 1: + flat_index = index_node.reshape(num_operations) + values_ndim = len(index_node.shape) + flat_values = values.reshape( + [num_operations] + list(values.shape[values_ndim:]) + ) + else: + flat_index = index_node + flat_values = values + + # Generate operation IDs and assign to partitions + operation_ids = torch.ops.prims.iota.default( + num_operations, + start=0, + step=1, + dtype=flat_index.dtype, + device=flat_index.device, + requires_grad=False, + ) + partition_ids = torch.ops.aten.bitwise_and.Scalar( + operation_ids, num_partitions - 1 + ) + + # Create expanded buffer + expanded_shape = list(input_tensor.shape) + expanded_shape[scatter_dim] *= num_partitions + expanded_buffer = torch.ops.aten.full.default( + expanded_shape, + 0, + dtype=flat_values.dtype, + layout=torch.strided, + device=flat_values.device, + pin_memory=False, + ) + + # Adjust indices for partitioning + partition_offsets = partition_ids * dim_size + adjusted_index = flat_index + partition_offsets + + # Reconstruct indices list for scatter + if isinstance(indices, (list, tuple)): + adjusted_indices = [ + adjusted_index if i == scatter_dim else idx + for i, idx in enumerate(indices) + ] + else: + adjusted_indices = [adjusted_index] + + # Scatter with reduced contention + scattered_buffer = torch.ops.aten.index_put.default( + expanded_buffer, adjusted_indices, flat_values, True + ) + + # Reshape for reduction + reduce_shape = list(expanded_shape) + reduce_shape[scatter_dim] = num_partitions + reduce_shape.insert(scatter_dim + 1, dim_size) + reshaped = torch.ops.aten.view.default(scattered_buffer, reduce_shape) + + # Sum across partitions (preserve dtype for int types) + if flat_values.dtype in [torch.int8, torch.int16, torch.int32, torch.uint8]: + reduced = torch.ops.aten.sum.dim_IntList( + reshaped, [scatter_dim], dtype=flat_values.dtype + ) + else: + reduced = torch.ops.aten.sum.dim_IntList(reshaped, [scatter_dim]) + + # Add to original input + return input_tensor + reduced + + counters["inductor"]["partitioned_scatter_applied"] += 1 + # pyrefly: ignore [bad-argument-type] + match.replace_by_example(repl, [input_tensor, index_node, values]) + + +def _get_max_partitions_for_size(output_size: int) -> int: + """ + Get maximum partitions based on output tensor size. + + Larger tensors use fewer partitions to limit memory overhead. + """ + if output_size >= 100_000_000: # >= 100M elements + return 4 + elif output_size >= 10_000_000: # >= 10M elements + return 8 + elif output_size >= 1_000_000: # >= 1M elements + return 16 + else: # < 1M elements + return _get_max_partitions() + + +def _estimate_optimal_partitions(output_size: int, index_size: int) -> int: + """Estimate optimal number of partitions based on contention ratio.""" + # Safety check for edge cases + if output_size == 0 or index_size == 0: + return _get_min_partitions() + + contention_ratio = index_size / output_size + + # Size-aware partition limits (larger tensors = fewer partitions to limit memory) + max_partitions_for_size = _get_max_partitions_for_size(output_size) + + # Contention-based calculation - square root scaling + # Use max to ensure we never go below min_partitions for the base calculation + base_partitions = max(_get_min_partitions(), int(math.sqrt(contention_ratio) * 16)) + + # Round to power of 2 and apply limits + partitions = 2 ** math.ceil(math.log2(base_partitions)) + return min(partitions, max_partitions_for_size, _get_max_partitions()) + + +def _fit_to_memory_budget( + output_size: int, num_partitions: int, dtype: torch.dtype +) -> int: + """ + Reduce partitions to fit memory budget if needed. + + Returns the maximum number of partitions that fit in memory budget. + Returns input num_partitions if it fits, or a reduced count, or 0 if + even min_partitions doesn't fit. + """ + if not torch.cuda.is_available(): + return num_partitions + + try: + _, total_memory = torch.cuda.mem_get_info() + element_bytes = dtype.itemsize if hasattr(dtype, "itemsize") else 4 + budget = total_memory * _get_memory_budget_fraction() + + # Try reducing partitions (must be power of 2) until we fit + current_partitions = num_partitions + min_partitions = _get_min_partitions() + while current_partitions >= min_partitions: + overhead = output_size * element_bytes * (current_partitions - 1) + + if overhead <= budget: + # Only format debug string if debug logging is enabled + if current_partitions < num_partitions and log.isEnabledFor( + logging.DEBUG + ): + log.debug( + "Reduced partitions from %d to %d to fit memory budget " + "(%.2fGB / %.2fGB)", + num_partitions, + current_partitions, + overhead / 1e9, + budget / 1e9, + ) + return current_partitions + + # Reduce by half (maintain power of 2) + current_partitions //= 2 + + # If min_partitions doesn't fit in memory, return 0 + if log.isEnabledFor(logging.DEBUG): + overhead = output_size * element_bytes * (min_partitions - 1) + log.debug( + "Insufficient memory even for %d partitions: %.2fGB > %.2fGB", + min_partitions, + overhead / 1e9, + budget / 1e9, + ) + return 0 + + except Exception: + log.debug("Memory check failed, proceeding with %s", num_partitions) + return num_partitions # Assume we have enough memory if we can't check + + +def _extract_scatter_dim_and_index( + indices_arg: Any, +) -> tuple[int | None, fx.Node | None]: + """Extract scatter dimension and index node from indices argument.""" + # Case 1: Single index → dim=0 + if not isinstance(indices_arg, (list, tuple)): + return 0, indices_arg + + # List with Nones → position of non-None is dim + index_node = None + scatter_dim = None + + # Case 2 -> Find the first non-None index as the scatter dimension + for dim, idx in enumerate(indices_arg): + if idx is not None: + if index_node is not None: + # Multiple indices not supported + return None, None + index_node = idx + scatter_dim = dim + + return scatter_dim, index_node + + +def _get_tensor_meta(node: fx.Node) -> dict[str, Any] | None: + """Extract tensor metadata from FX node.""" + if not hasattr(node, "meta") or "val" not in node.meta: + return None + + val = node.meta["val"] + if not isinstance(val, (torch.Tensor, type(val))) or not hasattr(val, "shape"): + return None + + return { + "shape": tuple(val.shape), + "dtype": val.dtype, + "device": val.device, + "numel": val.numel(), + } + + +__all__ = ["partitioned_scatter_optimization_pass"] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/reinplace.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/reinplace.py new file mode 100644 index 0000000000000000000000000000000000000000..2ca7429aabe51094ba75f88a943572a045979959 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/reinplace.py @@ -0,0 +1,936 @@ +# mypy: allow-untyped-defs +import itertools +import logging +import operator +from collections import defaultdict +from collections.abc import Callable, Sequence +from contextlib import nullcontext +from dataclasses import dataclass +from typing import Any, cast + +import torch +import torch.fx.node +from torch._C._dynamo.guards import compute_overlapping_tensors +from torch._dispatch.python import enable_python_dispatcher +from torch._dynamo.utils import ReinplaceCounters, ReInplaceTrigger +from torch._guards import detect_fake_mode +from torch._higher_order_ops.triton_kernel_wrap import ( + kernel_side_table, + triton_kernel_wrapper_functional, +) +from torch._inductor import config, inductor_prims +from torch._inductor.fx_utils import get_node_storage, is_node_realized +from torch._inductor.lowering import ( + inplaceable_foreach_ops as inplaceable_foreach_ops_lowerings, +) +from torch._inductor.virtualized import V +from torch.fx.experimental.symbolic_shapes import ( + compute_unbacked_bindings, + GuardOnDataDependentSymNode, +) +from torch.fx.immutable_collections import immutable_dict, immutable_list +from torch.fx.passes.reinplace import _is_view_op +from torch.utils import _pytree as pytree +from torch.utils._ordered_set import OrderedSet + + +log = logging.getLogger(__name__) +aten = torch.ops.aten + + +@dataclass(frozen=True) +class InplaceableOp: + inplace_op: Callable[..., Any] + mutated_arg: int | tuple[int, ...] # Single index or tuple of indices + extra_check: Callable[[torch.fx.Node], bool] = lambda node: True + + @property + def mutated_args(self) -> tuple[int, ...]: + """Return mutated_arg as a tuple for uniform handling.""" + if isinstance(self.mutated_arg, int): + return (self.mutated_arg,) + return self.mutated_arg + + +_SCATTER_OP_TO_VIEW = { + torch.ops.aten.diagonal_scatter.default: torch.ops.aten.diagonal.default, + torch.ops.aten.select_scatter.default: torch.ops.aten.select.int, + torch.ops.aten.slice_scatter.default: torch.ops.aten.slice.Tensor, + torch.ops.aten.as_strided_scatter.default: torch.ops.aten.as_strided.default, +} +_VIEW_OP_TO_SCATTER = {v: k for k, v in _SCATTER_OP_TO_VIEW.items()} + + +def graph_call_function(graph: torch.fx.Graph, fn, *args, **kwargs): + fake_args, fake_kwargs = pytree.tree_map( + lambda node: node.meta["val"] if isinstance(node, torch.fx.Node) else node, + (args, kwargs), + ) + with V.fake_mode: + fake_result = fn(*fake_args, **fake_kwargs) + + node = graph.call_function(fn, args, kwargs) + + node.meta["val"] = fake_result + + return node + + +@dataclass +class ViewOp: + target: torch._ops.OpOverload + args: tuple[Any, ...] + kwargs: dict[str, Any] + + +def _inplace_generalized_scatter( + inp: torch.Tensor, src: torch.Tensor, view_ops: list[ViewOp] +) -> torch.Tensor: + tmp = inp + for view in view_ops: + fake_args, fake_kwargs = pytree.tree_map( + lambda node: node.meta["val"] if isinstance(node, torch.fx.Node) else node, + (view.args, view.kwargs), + ) + # slice and select can allocate new unbacked symints, but those won't be reflected + # in the output of this function, hence shall be ignored. + fake_mode = detect_fake_mode(fake_args) + with ( + fake_mode.shape_env.ignore_fresh_unbacked_symbols() + if fake_mode and fake_mode.shape_env + else nullcontext() + ): + tmp = view.target(tmp, *fake_args, **fake_kwargs) + try: + tmp.copy_(src) + except RuntimeError as e: + raise RuntimeError( + f"shape error in scatter op, can not broadcast {src.shape} to {tmp.shape}" + ) from e + return inp + + +def _generalized_scatter( + inp: torch.Tensor, src: torch.Tensor, view_ops: list[ViewOp] +) -> torch.Tensor: + out = inp.clone() + return _inplace_generalized_scatter(out, src, view_ops) + + +def _decompose_scatter_functional_helper( + graph: torch.fx.Graph, + inp: torch.Tensor, + src: torch.Tensor, + view_ops: list[ViewOp], +) -> torch.fx.Node: + view_op, view_ops_tail = view_ops[0], view_ops[1:] + + if view_ops_tail: + view = graph_call_function( + graph, view_op.target, inp, *view_op.args, **view_op.kwargs + ) + src = _decompose_scatter_functional_helper(graph, view, src, view_ops[1:]) # type: ignore[assignment] + + return graph_call_function( + graph, + _VIEW_OP_TO_SCATTER[view_op.target], + inp, + src, + *view_op.args, + **view_op.kwargs, + ) + + +def _decompose_scatter_functional( + graph: torch.fx.Graph, node: torch.fx.Node +) -> torch.fx.Node: + """Decompose _generalized_scatter to a sequence of view_scatter operations + + e.g. _generalized_scatter(inp, src, [(aten.slice, 0, 0, 10), (aten.slice, 1, 10, -10)]) + + will become + + view = aten.slice(inp, 0, 0, 10) + view_updated = aten.slice_scatter(view, src, 1, 10, -10) + inp_updated = aten.slice_scatter(inp, view_updated, 0, 0, 10) + """ + assert node.target is _generalized_scatter + return _decompose_scatter_functional_helper(graph, *node.args) # type: ignore[arg-type] + + +def _decompose_scatter_mutating( + graph: torch.fx.Graph, node: torch.fx.Node +) -> torch.fx.Node: + """Decompose _generalized_scatter using mutations + + e.g. _generalized_scatter(inp, src, [(aten.slice, 0, 0, 10), (aten.slice, 1, 10, -10)]) + + will become + + inp_updated = aten.clone(inp) + slice1 = aten.slice(inp_updated, 0, 0, 10) + slice2 = aten.slice(slice1, 1, 10, -10) + slice2.copy_(src) + + """ + assert node.target in (_generalized_scatter, _inplace_generalized_scatter) + inp, src, view_ops = node.args + assert not node.kwargs + + if node.target is _generalized_scatter: + inp = graph_call_function(graph, aten.clone, inp) + + tmp = inp + for view in view_ops: # type: ignore[union-attr] + tmp = graph_call_function(graph, view.target, tmp, *view.args, **view.kwargs) # type: ignore[union-attr] + # we need to set unbacked bindings that could have been created in the view ops. + if (V.fake_mode.shape_env) and ( + symbol_to_path := compute_unbacked_bindings( + V.fake_mode.shape_env, tmp.meta["val"] + ) + ): + tmp.meta["unbacked_bindings"] = symbol_to_path + + graph_call_function(graph, aten.copy_.default, tmp, src) + return inp # type: ignore[return-value] + + +# View ops whose view_scatter op is lowered into mutations anyway, +# so is never a pessimisation to decompose. +_ALWAYS_MUTATING_SCATTER_OPS = OrderedSet( + [ + aten.as_strided.default, + aten.diagonal.default, + ] +) + + +def scatter_always_uses_mutation(node: torch.fx.Node) -> bool: + _, _, view_ops = node.args + view_ops = cast(Sequence[torch.fx.node.Argument], view_ops) + return any( + target in _ALWAYS_MUTATING_SCATTER_OPS + for view in view_ops + if isinstance(target := getattr(view, "target", None), torch._ops.OpOverload) + ) + + +def should_reinplace_scatter(node: torch.fx.Node) -> bool: + """Choose between mutating and functional scatter decompositions + + Reinplacing view scatter ops can be pessimising as it blocks fusion with the + input or output tensor computations. However, it is still profitable if the + input and output would have been realized anyway. + + """ + inp, _src, _view_ops = node.args + + # Mutating scatter ops unconditionally realize input and output + if scatter_always_uses_mutation(node): + return True + + if is_node_realized(inp) and is_node_realized(node): # type: ignore[arg-type] + return True + + # If the output is copied back into the input, this forces both to be + # realized as the output is a user of the input + if inp.op in ("placeholder", "get_attr") and any( # type: ignore[union-attr] + user.target is aten.copy_.default and user.args[0] is inp for user in node.users + ): + return True + + # Otherwise, assume fusions will make functional variants profitable + return False + + +def decompose_generalized_scatter(graph: torch.fx.Graph) -> None: + """Replace _generalized_scatter with normal aten ops""" + for node in itertools.chain( + graph.find_nodes(op="call_function", target=_generalized_scatter), + graph.find_nodes(op="call_function", target=_inplace_generalized_scatter), + ): + use_mutation = ( + node.target is _inplace_generalized_scatter + or scatter_always_uses_mutation(node) + ) + + with graph.inserting_before(node): + if use_mutation: + new_node = _decompose_scatter_mutating(graph, node) + else: + new_node = _decompose_scatter_functional(graph, node) + + node.replace_all_uses_with(new_node) + graph.erase_node(node) + + +def canonicalize_view_scatter_ops(graph: torch.fx.Graph) -> None: + """ + This canonicalizes view scatter ops into a generalized form, defined as: + def scatter(inp, src, views): + tmp = inp.clone() + for view in views: + tmp = view(tmp) + tmp.copy_(src) + + We also fuse consecutive view scatter ops of the form + a = scatter(view2(self), src, [view1]) + b = scatter(self, a, [view2]) + which can be rewritten as + b = scatter(self, src, [view2, view1]) + a = view2(b) + + This is both more efficient as we only do a single scatter, and also + easier to reinplace since there is only one use of `self` + """ + + node_to_view_base: dict[torch.fx.Node, torch.fx.Node] = {} + node_to_view_op: dict[torch.fx.Node, list[ViewOp]] = defaultdict(list) + + def handle_views(node: torch.fx.Node): + inp = node.args[0] + node_to_view_base[node] = node_to_view_base.get(inp, inp) # type: ignore[arg-type, assignment] + node_to_view_op[node] = [ + *node_to_view_op[inp], # type: ignore[index] + ViewOp( + node.target, # type: ignore[arg-type] + args=node.args[1:], + kwargs=node.kwargs, + ), + ] + + def handle_view_scatter(node: torch.fx.Node): + assert len(node.args) >= 2 + inp, src = node.args[:2] + + assert isinstance(node.target, torch._ops.OpOverload) + scatter_view_op = ViewOp( + _SCATTER_OP_TO_VIEW[node.target], + args=node.args[2:], + kwargs=node.kwargs, + ) + + def can_fuse(): + if src.target is not _generalized_scatter: # type: ignore[union-attr] + return False + src_inp, _src_src, _src_scatter_view_op = src.args # type: ignore[union-attr] + + inp_base = node_to_view_base.get(inp, inp) # type: ignore[arg-type] + src_base = node_to_view_base.get(src_inp, src_inp) # type: ignore[arg-type] + return inp_base is src_base and node_to_view_op[src_inp] == [ # type: ignore[index] + *node_to_view_op[inp], # type: ignore[index] + scatter_view_op, + ] + + if not can_fuse(): + with graph.inserting_before(node): + new_node = graph_call_function( + graph, + _generalized_scatter, + inp, + src, + [scatter_view_op], + ) + node.replace_all_uses_with(new_node) + graph.erase_node(node) + return + + _src_inp, src_src, src_scatter_view_op = src.args # type: ignore[union-attr] + with graph.inserting_before(src): # type: ignore[arg-type] + new_node = graph_call_function( + graph, + _generalized_scatter, + inp, + src_src, + [scatter_view_op, *src_scatter_view_op], # type: ignore[misc] + ) + node.replace_all_uses_with(new_node) + graph.erase_node(node) + + if src.users: # type: ignore[union-attr] + new_src = graph_call_function( + graph, + _SCATTER_OP_TO_VIEW[node.target], + new_node, + *node.args[2:], + **node.kwargs, + ) + + handle_views(new_src) + src.replace_all_uses_with(new_src) # type: ignore[union-attr] + + graph.erase_node(src) # type: ignore[arg-type] + + for node in graph.nodes: + if _is_view_op(node.target): + handle_views(node) + elif node.target in _SCATTER_OP_TO_VIEW: + handle_view_scatter(node) + + +inplaceable_ops: dict[Callable[..., Any], InplaceableOp] = { + aten.index_put.default: InplaceableOp(aten.index_put_.default, 0), + aten._unsafe_index_put.default: InplaceableOp(inductor_prims._unsafe_index_put_, 0), + _generalized_scatter: InplaceableOp( + _inplace_generalized_scatter, + 0, + extra_check=should_reinplace_scatter, + ), +} + +try: + c10d_functional = torch.ops._c10d_functional + inplaceable_collective_ops: dict[Callable[..., Any], InplaceableOp] = { + c10d_functional.all_reduce.default: InplaceableOp( + c10d_functional.all_reduce_.default, 0 + ), + c10d_functional.all_reduce_coalesced.default: InplaceableOp( + c10d_functional.all_reduce_coalesced_.default, 0 + ), + } + inplaceable_ops.update(inplaceable_collective_ops) +except AttributeError: + # _c10d_functional ops are only available when torch + # is built with USE_DISTRIBUTED=1. + pass + +inplaceable_foreach_ops: dict[torch._ops.OpOverload, InplaceableOp] = {} +for outplace_op, inplace_op in inplaceable_foreach_ops_lowerings.items(): + inplaceable_foreach_ops[outplace_op] = InplaceableOp(inplace_op, 0) + + +inplaceable_triton_ops = OrderedSet([triton_kernel_wrapper_functional]) + + +# Operators that don't depend on the tensor data +META_ONLY_OPS = OrderedSet( + [ + aten.sym_size.int, + aten.sym_stride.int, + aten.sym_numel.default, + aten.sym_storage_offset.default, + ] +) + + +def reinplace_inplaceable_ops_core(graph: torch.fx.Graph) -> None: + """ + Reinplaces in-placeable operations. + If there are no uses of a view of the mutated arg after the current node, + it is possible to inplace the op. + This above algorithm could be justified by observing side effects. While + we traverse the graph in forwards direction, only latter nodes could view + side effects of the current node. If the current node is not used later as + well as no view of this node is used later in the graph, then it is safe to + inplace as there would be no way to observe the side effects. + This condition is slightly different for graph inputs where they can only + be inplaced if the above condition is true and there's a copy_ in the + epilogue that signals that the caller wants to observe the mutation. + + Unlike JIT Inductor, AOTInductor currently unlifts weights and buffers from + input args, so instead of checking mutation on placeholder, AOTInductor + checks mutation on get_attr. This is subject to change in future. + """ + + copy_args_to_copy_nodes = {} + # maps argument to the first copy_ node that mutates it. + copy_nodes = {} + mutated_inputs = OrderedSet[Any]() + storage_to_nodes = defaultdict(list) + node_order: dict[Any, int] = {} + for i, node in enumerate(reversed(graph.nodes)): + node_order[node] = len(graph.nodes) - i - 1 + storage_to_nodes[get_node_storage(node)].append(node) + if node.target is aten.copy_.default and node.args[0].op in ( + "placeholder", + "get_attr", + ): + dst = node.args[0] + src = node.args[1] + # If the target is a getitem and it indexes a possible clone, + # then skip over it + if src.target is operator.getitem and ( + ( + src.args[0].target == triton_kernel_wrapper_functional + and src.args[0].kwargs["kwargs"][src.args[1]] == node.args[0] + ) + or (src.args[0].target in inplaceable_foreach_ops) + or (src.args[0].target is torch.ops.higher_order.auto_functionalized) + ): + src = src.args[0] + + copy_args_to_copy_nodes[(dst, src)] = node + copy_nodes[dst] = node + + mutated_inputs.add(node.args[0]) + + def any_use_of_views_after_node(node, shared_view_nodes, *, copy_node, mutated_arg): + node_loc = node_order[node] + copy_node_loc = node_order[copy_node] if copy_node is not None else None + + def is_meta_only_user(node): + if _is_view_op(node.target): + return all(is_meta_only_user(u) for u in node.users) + return node.target in META_ONLY_OPS + + for view in shared_view_nodes: + for user in view.users: + user_loc = node_order[user] + # Skip all users before node + if user_loc <= node_loc: + continue + # Ignore uses after the copy_ epilogue node, where the input + # has already been mutated anyway + if copy_node_loc is not None and copy_node_loc <= user_loc: + continue + # Reinplacing does not change shape metadata + if is_meta_only_user(user): + continue + # If our graph looks like: + # foo(mutated_arg) + # mutated_arg.copy_(other) + # then it's safe for us to reinplace foo because mutated_arg + # will get overwritten anyways. + if ( + user.target is torch.ops.aten.copy_.default + and mutated_arg is user.args[0] + ): + continue + return True + return False + + def can_inplace(node, mutated_arg): + # ls should be a list of tensors that all shares the same storage. + def _overlap(ls) -> bool: + try: + return len(compute_overlapping_tensors(ls)) != 0 + except GuardOnDataDependentSymNode: + # If we fail with data dependent error we assume they all overlap. + return True + + if isinstance(mutated_arg, (list, tuple)): + # TODO Using _overlap here causes a several issues. + unique_storages = OrderedSet(get_node_storage(arg) for arg in mutated_arg) + if len(unique_storages) != len(mutated_arg): + # At least two Tensors in mutated_arg alias each other, so we can't reinplace it. + # We can probably do better (that is, reinplace one of them and clone the other) + # but that requires more work and mutable List[Tensor] are not that common. + return False + return all(can_inplace(node, arg) for arg in mutated_arg) + + if get_node_storage(mutated_arg) is None: + return False + + shared_view_nodes = storage_to_nodes[get_node_storage(mutated_arg)] + + # Only keep tensor that might overlap with mutated_arg. + shared_view_nodes = [ + v + for v in shared_view_nodes + if _overlap([mutated_arg.meta["val"], v.meta["val"]]) + ] + + if mutated_arg.op in ("placeholder", "get_attr"): + # Get the first copy_ node that mutates the mutated_arg. + copy_node = copy_nodes.get(mutated_arg) + if copy_node is None: + # There is no copy_ back to the candidate mutated_arg (which is a graph input). + # Therefore the semantics of the program are that it does not mutate + # mutated_arg, so we cannot re-inplace it. + return False + if any_use_of_views_after_node( + node, shared_view_nodes, copy_node=copy_node, mutated_arg=mutated_arg + ): + return False + + return True + elif any(view.op in ("placeholder", "get_attr") for view in shared_view_nodes): + # This should never happen in auto_functionalize_v2 non-inference mode, + # since all mutated_arg are bases. + + # If mutated arg is view of any of the inputs of the graph, + # do not allow for inplacing. + # This would require more sophisticated algorithm to handle + return False + else: + return not any_use_of_views_after_node( + node, shared_view_nodes, copy_node=None, mutated_arg=mutated_arg + ) + + def all_can_inplace(node, mutated_args): + return all(can_inplace(node, arg) for arg in mutated_args) + + def log_inplace_results( + node_name, + old_tensors_to_clone, + tensors_to_clone, + missed_args, + missed_nodes, + trigger, + ): + # Total size of possibly_missed_reinplacing_opportunities for tensors with static shapes. + missed_bytes = 0 + + def bytes(node): + t = node.meta.get("val", None) + if ( + t is not None + and isinstance(t.element_size(), int) + and isinstance(t.numel(), int) + ): + return t.element_size() * t.numel() + else: + return 0 + + for node in missed_nodes: + if isinstance(node, (list, tuple)): + for n in node: + missed_bytes += bytes(n) + else: + missed_bytes += bytes(node) + + log.info( + "For node %s, attempted to reinplace %s. We were unable to reinplace %s; " + "%s (if non-empty) are possible missed reinplacing opportunities that may be bad for " + "memory usage and performance. Total size of missed opportunities with static shapes is" + " : %s bytes.", + node_name, + old_tensors_to_clone, + tensors_to_clone, + missed_args, + missed_bytes, + ) + + ReinplaceCounters.add_missed_opportunities(trigger, len(missed_args)) + ReinplaceCounters.add_missed_bytes(trigger, missed_bytes) + + replace_dict: dict[torch.fx.Node, torch.fx.Node] = {} + + def reinplace_and_refine_tensors_to_clone( + old_tensors_to_clone, kwargs, node_name, trigger + ): + tensors_to_clone: list[str] = [] + storage_of_reinplaced_args = OrderedSet[int | None]() + + # Those used to count possibly_missed_reinplacing_opportunities + missed_nodes = [] + missed_args = [] + + # TODO this logic can be made more precise using _overlap + def tensor_with_same_storage_already_reinplaced(arg): + if isinstance(arg, (list, tuple)): + return any( + get_node_storage(a) in storage_of_reinplaced_args for a in arg + ) + return get_node_storage(mutated_arg) in storage_of_reinplaced_args + + for arg in old_tensors_to_clone: + assert arg in kwargs + + mutated_arg = kwargs[arg] + + # Let's say we have: + # - op(x, y) that mutates both x and y + # - new_x, new_y = functional_op(x, y) is the functional variant + # If we are presented with functional_op(x, x), we must not reinplace + # this into op(x, x), because then it would be writing to the same Tensor. + # Instead, it's OK to reinplace one of them and to clone the other: + # >>> y = x.clone() + # >>> op(x, y) + # This also applies if we have views: functional_op(x, x[0]) + # should not reinplace into op(x, x[0]). + should_attempt_reinplace = not tensor_with_same_storage_already_reinplaced( + mutated_arg + ) + if should_attempt_reinplace and can_inplace(node, mutated_arg): + # In general, we probably do not need those optimizations. + copy_node = copy_args_to_copy_nodes.get((mutated_arg, node)) + if copy_node is not None: + replace_dict[copy_node] = copy_node.args[0] + if trigger != ReInplaceTrigger.AUTO_FUNC_V2: + for user in node.users: + # For auto_functionalize_v2, arg is the index of the base, where base at index i corresponds to + # output atindex size(out)+i. + # This used to compare string with integers before for auto_functionalize_v2. Not sure + # if it was needed for inplaceable_triton_ops? + if user.target is operator.getitem and user.args[1] == arg: + replace_dict[user] = mutated_arg + + if isinstance(mutated_arg, (list, tuple)): + for a in mutated_arg: + storage_of_reinplaced_args.add(get_node_storage(a)) + else: + storage_of_reinplaced_args.add(get_node_storage(mutated_arg)) + else: + if should_attempt_reinplace: + missed_args.append(arg) + missed_nodes.append(mutated_arg) + + tensors_to_clone.append(arg) + + log_inplace_results( + node_name, + old_tensors_to_clone, + tensors_to_clone, + missed_args, + missed_nodes, + trigger, + ) + return tensors_to_clone + + for node in graph.nodes: + if (inplaceable_op := inplaceable_ops.get(node.target)) is not None: + # Check if ALL mutated args can be inplaced + # Only convert if we don't need to clone any tensor + mutated_args = [node.args[idx] for idx in inplaceable_op.mutated_args] + if all_can_inplace(node, mutated_args) and inplaceable_op.extra_check(node): + for mutated_arg in mutated_args: + copy_node = copy_args_to_copy_nodes.get((mutated_arg, node)) + if copy_node is not None: + replace_dict[copy_node] = copy_node.args[0] + node.target = inplaceable_op.inplace_op + elif node.target is torch.ops.higher_order.auto_functionalized_v2: + _mutable_op = node.args[0] + kwargs = node.kwargs + + all_bases = kwargs["_all_bases"] + bases_to_clone = range(len(all_bases)) + base_tensors_dct = dict(enumerate(all_bases)) + new_bases_to_clone: list[int] = reinplace_and_refine_tensors_to_clone( + bases_to_clone, + base_tensors_dct, + node.target, + ReInplaceTrigger.AUTO_FUNC_V2, + ) + # Stash the metadata. There is a pass later on where we decompose + # auto_functionalized into clones + a mutable op; this metadata + # tells the decomp to only clone the following inputs + node.meta["only_clone_these_tensors"] = new_bases_to_clone + elif node.target is torch.ops.higher_order.auto_functionalized: + _mutable_op = node.args[0] + from torch._higher_order_ops.auto_functionalize import get_mutable_args + + tensors_to_clone, _ = get_mutable_args(_mutable_op) + # Don't try to reinplace Tensor | None args that are None. + tensors_to_clone = [ + t for t in tensors_to_clone if node.kwargs[t] is not None + ] + tensors_to_clone = reinplace_and_refine_tensors_to_clone( + tensors_to_clone, + node.kwargs, + _mutable_op._name, + ReInplaceTrigger.AUTO_FUNC_V1, + ) + + # Stash the metadata. There is a pass later on where we decompose + # auto_functionalized into clones + a mutable op; this metadata + # tells the decomp to only clone the following inputs + node.meta["only_clone_these_tensors"] = tensors_to_clone + elif node.target is torch.ops.higher_order.with_effects: + # Handle effectful ops wrapped with with_effects + # args[0] is the token, args[1] is the inner op, args[2:] are the op's args + inner_op = node.args[1] + log.debug( + "reinplace: checking with_effects node with inner_op=%s", inner_op + ) + if (inplaceable_op := inplaceable_ops.get(inner_op)) is not None: + log.debug("reinplace: found inplaceable_op for %s", inner_op) + # Get the mutated arg indices, offset by 2 (token + op) + mutated_arg_indices = inplaceable_op.mutated_args + + # Build flat list of tensors for can_inplace check + # and a mapping of output index -> replacement tensor(s) + mutated_tensors_flat = [] + output_idx_to_replacement: dict[int, Any] = {} + + for position, idx in enumerate(mutated_arg_indices): + actual_idx = idx + 2 # offset for token and op + assert actual_idx < len(node.args), ( + f"mutated arg idx {actual_idx} out of range {len(node.args)}" + ) + arg = node.args[actual_idx] + + # Output index is position + 1 (index 0 is the token) + output_idx = position + 1 + output_idx_to_replacement[output_idx] = arg + + # Flatten for can_inplace check + if isinstance(arg, (list, tuple)): + mutated_tensors_flat.extend(arg) + else: + mutated_tensors_flat.append(arg) + + # Check if all mutated args can be inplaced + can_inplace_all = all_can_inplace(node, mutated_tensors_flat) + + log.debug( + "reinplace with_effects: mutated_tensors=%s, can_inplace_all=%s", + [str(a) for a in mutated_tensors_flat], + can_inplace_all, + ) + + if can_inplace_all and inplaceable_op.extra_check(node): + log.debug( + "reinplace with_effects: converting %s -> %s", + inner_op, + inplaceable_op.inplace_op, + ) + # Update the inner op to inplace version + node.update_arg(1, inplaceable_op.inplace_op) + + # The output structure changes: functional returns (token, tensors), + # inplace returns (token, None). We need to redirect tensor uses + # to the input tensors. + + def get_index_from_node(n): + """Extract the index from a getitem node.""" + if n.target is operator.getitem: + return n.args[1] + return None + + def is_getitem_node(n, parent): + """Check if node n is a getitem indexing into parent.""" + return n.target is operator.getitem and n.args[0] is parent + + def replace_and_collect(current_node, replacement_tensors): + """ + Collect replacements for getitem nodes into replace_dict. + Nodes are added in child-first order so children are erased before parents. + """ + # Find all users that are getitem nodes indexing into current_node + getitem_users = [ + u + for u in current_node.users + if is_getitem_node(u, current_node) + ] + + if not getitem_users: + # Leaf node - add to replace_dict with actual replacement + if isinstance(replacement_tensors, (list, tuple)): + if len(replacement_tensors) == 1: + replace_dict[current_node] = replacement_tensors[0] + return True + else: + # Multiple tensors but no indexing - can't replace + return False + else: + replace_dict[current_node] = replacement_tensors + return True + + # Process children first (so they're added to replace_dict before parent) + all_children_replaced = True + first_replacement = None + for getitem_user in getitem_users: + idx = get_index_from_node(getitem_user) + if idx is None or not isinstance(idx, int): + all_children_replaced = False + continue + + if not isinstance(replacement_tensors, (list, tuple)): + all_children_replaced = False + continue + + if idx >= len(replacement_tensors): + all_children_replaced = False + continue + + if first_replacement is None: + first_replacement = replacement_tensors[idx] + + if not replace_and_collect( + getitem_user, replacement_tensors[idx] + ): + all_children_replaced = False + + # Add this node to replace_dict after children (even if it has non-getitem users) + # Non-getitem users will have their input replaced via replace_all_uses_with + if all_children_replaced and first_replacement is not None: + replace_dict[current_node] = first_replacement + + return all_children_replaced + + # Find getitem nodes that extract tensor results + # Use the output_idx_to_replacement mapping built above + for user in list(node.users): + if not is_getitem_node(user, node): + continue + idx = get_index_from_node(user) + if idx is None or idx not in output_idx_to_replacement: + continue + replacement = output_idx_to_replacement[idx] + replace_and_collect(user, replacement) + elif node.target in inplaceable_triton_ops: + kernel_idx = node.kwargs["kernel_idx"] + kernel = kernel_side_table.get_kernel(kernel_idx) + from triton.runtime.autotuner import Autotuner + from triton.runtime.jit import JITFunction + + if isinstance(kernel, JITFunction): + kernel_name = kernel.fn.__name__ + elif isinstance(kernel, Autotuner): + if config.is_fbcode(): + # Autotuner has different implementations for AMD and NV + if torch.version.hip is None: + kernel_name = kernel.base_fn.__name__ + else: + kernel_name = kernel.fn.__name__ + else: + kernel_name = kernel.base_fn.__name__ + else: + raise AssertionError("Unknown triton kernel type") + + # inplaceable_triton_ops take an additional argument called + # tensors_to_clone which contain a list of tensors to clone + # This pass iterates over them and sees which ones are safe + # to eliminate (i.e. no longer need the clones) + tensors_to_clone = reinplace_and_refine_tensors_to_clone( + node.kwargs["tensors_to_clone"], + node.kwargs["kwargs"], + kernel_name, + ReInplaceTrigger.TRITON_OPS, + ) + + kwargs = dict(node.kwargs) + kwargs["tensors_to_clone"] = tensors_to_clone + node.kwargs = immutable_dict(kwargs) + if "eager_input_vals" in node.meta: + # We changed the kwargs, so we need to update eager_input_vals + # to something sane. + args, kwargs = node.meta["eager_input_vals"] + new_kwargs = {**kwargs} + new_kwargs["tensors_to_clone"] = immutable_list(tensors_to_clone) + new_kwargs = immutable_dict(new_kwargs) + node.meta["eager_input_vals"] = (args, new_kwargs) + elif (inplaceable_op := inplaceable_foreach_ops.get(node.target)) is not None: + mutated_args = node.args[inplaceable_op.mutated_arg] + + if not all((arg, node) in copy_args_to_copy_nodes for arg in mutated_args): + continue + + if can_inplace(node, mutated_args): + for arg in mutated_args: + copy_node = copy_args_to_copy_nodes[(arg, node)] + replace_dict[copy_node] = copy_node.args[0] + + node.target = inplaceable_op.inplace_op + for node, replacement in replace_dict.items(): + while replacement in replace_dict: + replacement = replace_dict[replacement] + replace_dict[node] = replacement + + node.replace_all_uses_with(replacement) + graph.erase_node(node) + + +def reinplace_inplaceable_ops( + fake_tensor_updater: torch._inductor.fx_utils.FakeTensorUpdater, + graph: torch.fx.Graph, +) -> None: + with enable_python_dispatcher(): + canonicalize_view_scatter_ops(graph) + # canonicalize_view_scatter_ops adds new operations to the graph. + # We run fake_tensor_updater to update the alias information. + # Correct alias information is required for `reinplace_inplaceable_ops_core`. + fake_tensor_updater.incremental_update() + reinplace_inplaceable_ops_core(graph) + decompose_generalized_scatter(graph) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/replace_random.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/replace_random.py new file mode 100644 index 0000000000000000000000000000000000000000..7689b64ed5334f066c8cdf3de0988826d5aeb76e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/replace_random.py @@ -0,0 +1,258 @@ +# mypy: allow-untyped-defs +import collections +import logging + +import torch +from torch.fx.experimental.symbolic_shapes import guard_or_false, statically_known_true +from torch.fx.passes.graph_transform_observer import GraphTransformObserver +from torch.fx.passes.shape_prop import _extract_tensor_metadata + +from .. import config, inductor_prims +from ..pattern_matcher import ( + CallFunctionVarArgs, + Match, + PatternMatcherPass, + register_graph_pattern, +) +from ..virtualized import V + + +log = logging.getLogger(__name__) +patterns = PatternMatcherPass(subsystem="joint_graph_passes") +aten = torch.ops.aten + + +def _shape_to_offset(shape, device: torch.device): + # Modified from torch/_prims/rng_prims.py:philox_rand_offset + nelem = 1 + for s in shape: + nelem *= s + + # Empty tensor: no random numbers are generated/consumed. + is_empty = nelem == 0 + if statically_known_true(is_empty) or guard_or_false(is_empty): + return 0 + + if device is None: + device = torch.device("cpu") + elif isinstance(device, str): + device = torch.device(device) + + if device.type != "cuda": + return 0 + + block_size = 256 + unroll = 4 + curand4_engine_calls = 4 + + device_property = torch.cuda.get_device_properties(device) + + blocks_per_sm = device_property.max_threads_per_multi_processor // block_size + max_grid = device_property.multi_processor_count * blocks_per_sm + grid_size = (nelem + block_size - 1) // block_size + grid_size = -torch.sym_min(-grid_size, -1) + grid_size = torch.sym_min(grid_size, max_grid) + + return ((nelem - 1) // (block_size * grid_size * unroll) + 1) * curand4_engine_calls + + +def replace_random_passes(gm: torch.fx.GraphModule): + """Modify the given FX graph to use backend-native random ops""" + if config.fallback_random: + return 0 + + count = patterns.apply(gm) + with GraphTransformObserver(gm, "fuse_seed_creation_pass", "joint_graph_passes"): + count += fuse_seed_creation_pass(gm.graph) + if config.align_random_eager: + with GraphTransformObserver(gm, "fuse_offset_creation_pass"): + count += fuse_offset_creation_pass(gm.graph) + + return count + + +def fuse_offset_creation_pass(graph: torch.fx.Graph) -> int: + """ + Here offset node means seed << 32 + offset, will unpacked in lowering.py:inductor_random() + Horizontally fuse all the seed generation on each device + a = inductor_prims.rand_eager_offset(offset, dev) + b = inductor_prims.rand_eager_offset(offset, dev) + Becomes: + offsets = inductor_prims.rand_eager_offsets([offset1, offset2...], dev) + a = torch.ops.aten.select.int(offsets, 0, 0) + b = torch.ops.aten.select.int(offsets, 0, 1) + We do this because seed creation is entirely launch overhead bound. + """ + device_offsets = collections.defaultdict(list) + for node in graph.nodes: + if CallFunctionVarArgs(inductor_prims.rand_eager_offset).match(node): + device_offsets[node.args[1]].append(node) + + if not device_offsets: + return 0 + + for device, offsets in device_offsets.items(): + with graph.inserting_before(offsets[0]): + offs = [n.args[0] for n in offsets] + combined = graph.call_function( + inductor_prims.rand_eager_offsets, (offs, device) + ) + with V.fake_mode: + combined.meta["val"] = torch.empty( + [len(offsets), 2], device=device, dtype=torch.int64 + ) + combined.meta["tensor_meta"] = _extract_tensor_metadata( + combined.meta["val"] + ) + + for idx, offset in enumerate(offsets): + with graph.inserting_before(offset): + new_state = graph.call_function( + torch.ops.aten.select.int, (combined, 0, idx) + ) + offset.replace_all_uses_with(new_state) + new_state.meta.update(offset.meta) + graph.erase_node(offset) + + return len(device_offsets) + + +def fuse_seed_creation_pass(graph: torch.fx.Graph): + """ + Horizontally fuse all the seed generation on each device + + a = inductor_seed(dev) + b = inductor_seed(dev) + + Becomes: + seeds = inductor_seeds(2, dev) + a = inductor_lookup_seed(seeds, 0) + b = inductor_lookup_seed(seeds, 1) + + We do this because seed creation is entirely launch overhead bound. + """ + device_seeds = collections.defaultdict(list) + for node in graph.nodes: + if CallFunctionVarArgs(inductor_prims.seed).match(node): + device_seeds[node.args[0]].append(node) + + if not device_seeds: + return 0 + + for device, seeds in device_seeds.items(): + with graph.inserting_before(seeds[0]): + combined = graph.call_function(inductor_prims.seeds, (len(seeds), device)) + with V.fake_mode: + combined.meta["val"] = torch.empty( + [len(seeds)], device=device, dtype=torch.int64 + ) + combined.meta["tensor_meta"] = _extract_tensor_metadata( + combined.meta["val"] + ) + + for idx, seed in enumerate(seeds): + with graph.inserting_before(seed): + new_seed = graph.call_function( + inductor_prims.lookup_seed, (combined, idx) + ) + seed.replace_all_uses_with(new_seed) + new_seed.meta.update(seed.meta) + graph.erase_node(seed) + + return len(device_seeds) + + +def default_kwargs(device): + return {} + + +def get_device(device): + if device is not None: + return device + return torch.empty([]).device # default device + + +# pyrefly: ignore [bad-argument-type] +@register_graph_pattern(CallFunctionVarArgs(aten.rand.default), pass_dict=patterns) +# pyrefly: ignore [bad-argument-type] +@register_graph_pattern(CallFunctionVarArgs(aten.rand.generator), pass_dict=patterns) +# pyrefly: ignore [bad-argument-type] +@register_graph_pattern(CallFunctionVarArgs(aten.randn.default), pass_dict=patterns) +# pyrefly: ignore [bad-argument-type] +@register_graph_pattern(CallFunctionVarArgs(aten.randn.generator), pass_dict=patterns) +def replace_random( + match: Match, + size, + *, + generator=None, + dtype=None, + device=None, + layout=None, + pin_memory=None, +): + if generator is not None: + return + + def replacement(size): + result = inductor_prims.random( + size, inductor_prims.seed(device), mode, **default_kwargs(device) + ) + if dtype is not None: + result = result.to(dtype) + return result + + mode = { + aten.rand: "rand", + aten.randn: "randn", + }[ + match.output_node().target.overloadpacket # type: ignore[union-attr] + ] # type: ignore[union-attr] + device = get_device(device) + replacement_fn = replacement + + if mode == "rand" and config.align_random_eager and device.type == "cuda": + # Only enable when align_random_eager is on. + def replacement_align(size): + offset = _shape_to_offset(size, device) + + align_dtype = dtype + if isinstance(align_dtype, (tuple, list)): + align_dtype = align_dtype[0] if len(align_dtype) else None + + result = inductor_prims.random( + size, + inductor_prims.rand_eager_offset(offset, device), + mode, + **default_kwargs(device), + align_dtype=align_dtype, + ) + if dtype is not None: + result = result.to(dtype) + return result + + replacement_fn = replacement_align + + # pyrefly: ignore [bad-argument-type] + match.replace_by_example(replacement_fn, [size]) + + +# pyrefly: ignore [bad-argument-type] +@register_graph_pattern(CallFunctionVarArgs(aten.randint.low), pass_dict=patterns) +def replace_randint( + match: Match, + low, + high, + size, + *, + dtype=torch.int64, + device=None, + layout=None, + pin_memory=None, +): + def replacement(low, high, size): + result = inductor_prims.randint(low, high, size, inductor_prims.seed(device)) + return result.to(dtype) + + device = get_device(device) + # pyrefly: ignore [bad-argument-type] + match.replace_by_example(replacement, [low, high, size]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_1.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_1.py new file mode 100644 index 0000000000000000000000000000000000000000..6c8e6de3ff3cba5f0ebcec729c33061b04319d6a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_1.py @@ -0,0 +1,174 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale'), _users=2) +amax_default = CallFunction(aten.amax.default, div_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, div_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_1) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_7, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +div_Tensor_2 = CallFunction(aten.div.Tensor, fma_default, KeywordArg('inv_scale')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_2) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_3, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_4 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_5 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_5, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +_sfdp_pattern_1_training = MultiOutputPattern([view_default_5, + view_default_9, + permute_default_4, + view_default_11, + None +]) + + +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale'), _users=2) +amax_default = CallFunction(aten.amax.default, div_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, div_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_1_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored(), _users=2) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +neg_default = CallFunction(aten.neg.default, convert_element_type_default_2) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_1) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, convert_element_type_default_3, convert_element_type_default_2, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +div_Tensor_2 = CallFunction(aten.div.Tensor, convert_element_type_default_4, KeywordArg('inv_scale')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_2) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_3, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_4 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_5 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_5, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +_sfdp_pattern_1_half_training = MultiOutputPattern([view_default_5, + view_default_9, + permute_default_4, + view_default_11, + None +]) + + +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_1_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_10.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_10.py new file mode 100644 index 0000000000000000000000000000000000000000..9e6e4a9ec945c64bdd3a267d665c8d18d22a8d55 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_10.py @@ -0,0 +1,209 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, permute_default, KeywordArg('inv_scale')) +expand_default = CallFunction(aten.expand.default, div_Tensor, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, view_default_2, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, view_default_2, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, permute_default_3, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, convert_element_type_default_2, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +view_default_8 = CallFunction(aten.view.default, fma_default, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +div_Tensor_2 = CallFunction(aten.div.Tensor, view_default_9, KeywordArg('inv_scale')) +permute_default_6 = CallFunction(aten.permute.default, div_Tensor_2, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, view_default_11, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, convert_element_type_default_3, Ignored()) +_sfdp_pattern_10_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, permute_default, KeywordArg('inv_scale')) +expand_default = CallFunction(aten.expand.default, div_Tensor, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, view_default_2, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, view_default_2, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, permute_default_3, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_10_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, permute_default, KeywordArg('inv_scale')) +expand_default = CallFunction(aten.expand.default, div_Tensor, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, view_default_2, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, convert_element_type_default_2, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +view_default_8 = CallFunction(aten.view.default, convert_element_type_default_3, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +div_Tensor_2 = CallFunction(aten.div.Tensor, view_default_9, KeywordArg('inv_scale')) +permute_default_6 = CallFunction(aten.permute.default, div_Tensor_2, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_10_half_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, permute_default, KeywordArg('inv_scale')) +expand_default = CallFunction(aten.expand.default, div_Tensor, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, view_default_2, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_10_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_11.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_11.py new file mode 100644 index 0000000000000000000000000000000000000000..6aa39474c67dd677008c8e7e9266cc875a153196 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_11.py @@ -0,0 +1,204 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale'), _users=2) +amax_default = CallFunction(aten.amax.default, div_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, div_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_7, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +div_Tensor_2 = CallFunction(aten.div.Tensor, fma_default, KeywordArg('inv_scale')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_11_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale'), _users=2) +amax_default = CallFunction(aten.amax.default, div_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, div_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_11_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored(), _users=2) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +neg_default = CallFunction(aten.neg.default, convert_element_type_default_2) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, convert_element_type_default_3, convert_element_type_default_2, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +div_Tensor_2 = CallFunction(aten.div.Tensor, convert_element_type_default_4, KeywordArg('inv_scale')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_11_half_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_11_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_12.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_12.py new file mode 100644 index 0000000000000000000000000000000000000000..87302d1bab3694a33eac14e263dca86c9f702c75 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_12.py @@ -0,0 +1,220 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale_factor'), _users=2) +amax_default = CallFunction(aten.amax.default, div_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, div_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +div_Tensor_2 = CallFunction(aten.div.Tensor, fma_default, KeywordArg('inv_scale_factor')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_12_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale_factor'), _users=2) +amax_default = CallFunction(aten.amax.default, div_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, div_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_12_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale_factor')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored(), _users=2) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, convert_element_type_default_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +neg_default = CallFunction(aten.neg.default, convert_element_type_default_2) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, mul_Tensor_3, Ignored()) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, convert_element_type_default_4, convert_element_type_default_2, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +convert_element_type_default_5 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +div_Tensor_2 = CallFunction(aten.div.Tensor, convert_element_type_default_5, KeywordArg('inv_scale_factor')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_12_half_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale_factor')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_12_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_13.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_13.py new file mode 100644 index 0000000000000000000000000000000000000000..d465c1cb4e22b14bfbbdd35d5ed28a43af891523 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_13.py @@ -0,0 +1,130 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, KeywordArg('query'), permute_default, _users=2) +amax_default = CallFunction(aten.amax.default, bmm_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, bmm_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, mul_Tensor_1, KeywordArg('value')) +neg_default = CallFunction(aten.neg.default, div_Tensor) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, KeywordArg('tangents_1'), permute_default_1) +convert_element_type_default = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, bmm_default_2, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4, _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, fma_default, permute_default_2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_3, fma_default) +permute_default_4 = CallFunction(aten.permute.default, bmm_default_4, Ignored()) +permute_default_5 = CallFunction(aten.permute.default, mul_Tensor_1, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_5, KeywordArg('tangents_1')) +_sfdp_pattern_13_training = MultiOutputPattern([bmm_default_1, + bmm_default_3, + permute_default_4, + bmm_default_5, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +bmm_default = CallFunction(aten.bmm.default, KeywordArg('query'), permute_default, _users=2) +amax_default = CallFunction(aten.amax.default, bmm_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, bmm_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +_sfdp_pattern_13_inference = CallFunction(aten.bmm.default, div_Tensor, KeywordArg('value'), _users=0) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, KeywordArg('query'), permute_default) +convert_element_type_default = CallFunction(prims.convert_element_type.default, bmm_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored(), _users=2) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, convert_element_type_default_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, mul_Tensor_1, KeywordArg('value')) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +neg_default = CallFunction(aten.neg.default, convert_element_type_default_2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, KeywordArg('tangents_1'), permute_default_1) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, bmm_default_2, mul_Tensor_2) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, mul_Tensor_3, Ignored()) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, convert_element_type_default_4, convert_element_type_default_2, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +convert_element_type_default_5 = CallFunction(prims.convert_element_type.default, fma_default, Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, convert_element_type_default_5, permute_default_2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_3, convert_element_type_default_5) +permute_default_4 = CallFunction(aten.permute.default, bmm_default_4, Ignored()) +permute_default_5 = CallFunction(aten.permute.default, mul_Tensor_1, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_5, KeywordArg('tangents_1')) +_sfdp_pattern_13_half_training = MultiOutputPattern([bmm_default_1, + bmm_default_3, + permute_default_4, + bmm_default_5, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +bmm_default = CallFunction(aten.bmm.default, KeywordArg('query'), permute_default) +convert_element_type_default = CallFunction(prims.convert_element_type.default, bmm_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +_sfdp_pattern_13_half_inference = CallFunction(aten.bmm.default, convert_element_type_default_1, KeywordArg('value'), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_14.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_14.py new file mode 100644 index 0000000000000000000000000000000000000000..f102038e82c6d5858b8b334e956d87c7e86a9d22 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_14.py @@ -0,0 +1,210 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_7, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +div_Tensor_2 = CallFunction(aten.div.Tensor, fma_default, KeywordArg('inv_scale')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_14_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_14_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored(), _users=2) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +neg_default = CallFunction(aten.neg.default, convert_element_type_default_2) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, convert_element_type_default_3, convert_element_type_default_2, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +div_Tensor_2 = CallFunction(aten.div.Tensor, convert_element_type_default_4, KeywordArg('inv_scale')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_14_half_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_14_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_15.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_15.py new file mode 100644 index 0000000000000000000000000000000000000000..e1cbb0df340bab14188ec9d5f04a29035dba8d84 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_15.py @@ -0,0 +1,230 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +eq_Scalar = CallFunction(aten.eq.Scalar, KeywordArg('attn_mask'), Ignored()) +view_default = CallFunction(aten.view.default, eq_Scalar, Ignored()) +expand_default = CallFunction(aten.expand.default, view_default, Ignored(), _users=2) +full_default = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_2, memory_format=torch.contiguous_format) +view_default_2 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default_1, view_default_2) +view_default_3 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_3, KeywordArg('inv_scale')) +where_self = CallFunction(aten.where.self, expand_default, full_default, div_Tensor, _users=2) +amax_default = CallFunction(aten.amax.default, where_self, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, where_self, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +expand_default_3 = CallFunction(aten.expand.default, div_Tensor_1, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_4 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_4, memory_format=torch.contiguous_format) +view_default_5 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_4, view_default_5) +view_default_6 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +scalar_tensor_default = CallFunction(aten.scalar_tensor.default, Ignored(), dtype=Ignored(), layout=torch.strided, device=Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_7 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_5, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_7, permute_default_4) +view_default_8 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_8, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +where_self_1 = CallFunction(aten.where.self, expand_default, scalar_tensor_default, fma_default) +div_Tensor_2 = CallFunction(aten.div.Tensor, where_self_1, KeywordArg('inv_scale')) +view_default_9 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_2, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_9, permute_default_5) +view_default_10 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_9) +view_default_11 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_11, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_7) +view_default_12 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_12, Ignored()) +_sfdp_pattern_15_training = MultiOutputPattern([view_default_6, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None +]) + + +eq_Scalar = CallFunction(aten.eq.Scalar, KeywordArg('attn_mask'), Ignored()) +view_default = CallFunction(aten.view.default, eq_Scalar, Ignored()) +expand_default = CallFunction(aten.expand.default, view_default, Ignored()) +full_default = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_2, memory_format=torch.contiguous_format) +view_default_2 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default_1, view_default_2) +view_default_3 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_3, KeywordArg('inv_scale')) +where_self = CallFunction(aten.where.self, expand_default, full_default, div_Tensor, _users=2) +amax_default = CallFunction(aten.amax.default, where_self, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, where_self, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_3 = CallFunction(aten.expand.default, div_Tensor_1, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_4 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_4, memory_format=torch.contiguous_format) +view_default_5 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_4, view_default_5) +_sfdp_pattern_15_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +eq_Scalar = CallFunction(aten.eq.Scalar, KeywordArg('attn_mask'), Ignored()) +view_default = CallFunction(aten.view.default, eq_Scalar, Ignored()) +expand_default = CallFunction(aten.expand.default, view_default, Ignored(), _users=2) +full_default = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_2, memory_format=torch.contiguous_format) +view_default_2 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default_1, view_default_2) +view_default_3 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_3, KeywordArg('inv_scale')) +where_self = CallFunction(aten.where.self, expand_default, full_default, div_Tensor) +convert_element_type_default = CallFunction(prims.convert_element_type.default, where_self, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_4 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_4, memory_format=torch.contiguous_format) +view_default_5 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_4, view_default_5) +view_default_6 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +scalar_tensor_default = CallFunction(aten.scalar_tensor.default, Ignored(), dtype=Ignored(), layout=torch.strided, device=Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +neg_default = CallFunction(aten.neg.default, convert_element_type_default_2) +view_default_7 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_5, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_7, permute_default_4) +view_default_8 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, view_default_8, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, convert_element_type_default_3, convert_element_type_default_2, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +where_self_1 = CallFunction(aten.where.self, expand_default, scalar_tensor_default, convert_element_type_default_4) +div_Tensor_2 = CallFunction(aten.div.Tensor, where_self_1, KeywordArg('inv_scale')) +view_default_9 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_2, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_9, permute_default_5) +view_default_10 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_9) +view_default_11 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_11, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_7) +view_default_12 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_12, Ignored()) +_sfdp_pattern_15_half_training = MultiOutputPattern([view_default_6, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None +]) + + +eq_Scalar = CallFunction(aten.eq.Scalar, KeywordArg('attn_mask'), Ignored()) +view_default = CallFunction(aten.view.default, eq_Scalar, Ignored()) +expand_default = CallFunction(aten.expand.default, view_default, Ignored()) +full_default = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_2, memory_format=torch.contiguous_format) +view_default_2 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default_1, view_default_2) +view_default_3 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_3, KeywordArg('inv_scale')) +where_self = CallFunction(aten.where.self, expand_default, full_default, div_Tensor) +convert_element_type_default = CallFunction(prims.convert_element_type.default, where_self, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_4 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_4, memory_format=torch.contiguous_format) +view_default_5 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_4, view_default_5) +_sfdp_pattern_15_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_16.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_16.py new file mode 100644 index 0000000000000000000000000000000000000000..3a15abb9088ff5ffe8cd9af43df11ccc0d5bc143 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_16.py @@ -0,0 +1,599 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +div_Tensor_2 = CallFunction(aten.div.Tensor, fma_default, KeywordArg('inv_scale')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_16_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_16_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +div_Tensor_2 = CallFunction(aten.div.Tensor, fma_default, KeywordArg('inv_scale')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_16_bs1_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_16_bs1_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored(), _users=2) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, convert_element_type_default_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +neg_default = CallFunction(aten.neg.default, convert_element_type_default_2) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, mul_Tensor_3, Ignored()) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, convert_element_type_default_4, convert_element_type_default_2, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +convert_element_type_default_5 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +div_Tensor_2 = CallFunction(aten.div.Tensor, convert_element_type_default_5, KeywordArg('inv_scale')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_16_half_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_16_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored(), _users=2) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, convert_element_type_default_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +neg_default = CallFunction(aten.neg.default, convert_element_type_default_2) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, mul_Tensor_3, Ignored()) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, convert_element_type_default_4, convert_element_type_default_2, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +convert_element_type_default_5 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +div_Tensor_2 = CallFunction(aten.div.Tensor, convert_element_type_default_5, KeywordArg('inv_scale')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_16_half_bs1_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_16_half_bs1_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, mul_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_2, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, convert_element_type_default_1, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +div_Tensor_2 = CallFunction(aten.div.Tensor, convert_element_type_default_3, KeywordArg('inv_scale')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_16_half_mask_fp32_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_16_half_mask_fp32_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, mul_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_2, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, convert_element_type_default_1, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +div_Tensor_2 = CallFunction(aten.div.Tensor, convert_element_type_default_3, KeywordArg('inv_scale')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_16_half_mask_fp32_bs1_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_16_half_mask_fp32_bs1_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_17.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_17.py new file mode 100644 index 0000000000000000000000000000000000000000..812708907b3414e2c864ed36b98f2199e63ae5d2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_17.py @@ -0,0 +1,246 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +eq_Scalar = CallFunction(aten.eq.Scalar, KeywordArg('attn_mask'), Ignored()) +view_default = CallFunction(aten.view.default, eq_Scalar, Ignored()) +expand_default = CallFunction(aten.expand.default, view_default, Ignored(), _users=2) +full_default = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_2, memory_format=torch.contiguous_format) +view_default_2 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default_1, view_default_2) +view_default_3 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_3, KeywordArg('inv_scale')) +where_self = CallFunction(aten.where.self, expand_default, full_default, div_Tensor, _users=2) +amax_default = CallFunction(aten.amax.default, where_self, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, where_self, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_4 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_4, memory_format=torch.contiguous_format) +view_default_5 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_4, view_default_5) +view_default_6 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +scalar_tensor_default = CallFunction(aten.scalar_tensor.default, Ignored(), dtype=Ignored(), layout=torch.strided, device=Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_7 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_5, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_7, permute_default_4) +view_default_8 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_8, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +where_self_1 = CallFunction(aten.where.self, expand_default, scalar_tensor_default, fma_default) +div_Tensor_2 = CallFunction(aten.div.Tensor, where_self_1, KeywordArg('inv_scale')) +view_default_9 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_2, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_9, permute_default_5) +view_default_10 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_9) +view_default_11 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_11, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_7) +view_default_12 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_12, Ignored()) +_sfdp_pattern_17_training = MultiOutputPattern([view_default_6, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None, + None +]) + + +eq_Scalar = CallFunction(aten.eq.Scalar, KeywordArg('attn_mask'), Ignored()) +view_default = CallFunction(aten.view.default, eq_Scalar, Ignored()) +expand_default = CallFunction(aten.expand.default, view_default, Ignored()) +full_default = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_2, memory_format=torch.contiguous_format) +view_default_2 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default_1, view_default_2) +view_default_3 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_3, KeywordArg('inv_scale')) +where_self = CallFunction(aten.where.self, expand_default, full_default, div_Tensor, _users=2) +amax_default = CallFunction(aten.amax.default, where_self, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, where_self, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_3 = CallFunction(aten.expand.default, div_Tensor_1, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_4 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_4, memory_format=torch.contiguous_format) +view_default_5 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_4, view_default_5) +_sfdp_pattern_17_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +eq_Scalar = CallFunction(aten.eq.Scalar, KeywordArg('attn_mask'), Ignored()) +view_default = CallFunction(aten.view.default, eq_Scalar, Ignored()) +expand_default = CallFunction(aten.expand.default, view_default, Ignored(), _users=2) +full_default = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_2, memory_format=torch.contiguous_format) +view_default_2 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default_1, view_default_2) +view_default_3 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_3, KeywordArg('inv_scale')) +where_self = CallFunction(aten.where.self, expand_default, full_default, div_Tensor) +convert_element_type_default = CallFunction(prims.convert_element_type.default, where_self, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored(), _users=2) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, convert_element_type_default_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_4 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_4, memory_format=torch.contiguous_format) +view_default_5 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_4, view_default_5) +view_default_6 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +scalar_tensor_default = CallFunction(aten.scalar_tensor.default, Ignored(), dtype=Ignored(), layout=torch.strided, device=Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +neg_default = CallFunction(aten.neg.default, convert_element_type_default_2) +view_default_7 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_5, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_7, permute_default_4) +view_default_8 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_8, mul_Tensor_2) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, mul_Tensor_3, Ignored()) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, convert_element_type_default_4, convert_element_type_default_2, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +convert_element_type_default_5 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +where_self_1 = CallFunction(aten.where.self, expand_default, scalar_tensor_default, convert_element_type_default_5) +div_Tensor_2 = CallFunction(aten.div.Tensor, where_self_1, KeywordArg('inv_scale')) +view_default_9 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_2, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_9, permute_default_5) +view_default_10 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_9) +view_default_11 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_11, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_7) +view_default_12 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_12, Ignored()) +_sfdp_pattern_17_half_training = MultiOutputPattern([view_default_6, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None, + None +]) + + +eq_Scalar = CallFunction(aten.eq.Scalar, KeywordArg('attn_mask'), Ignored()) +view_default = CallFunction(aten.view.default, eq_Scalar, Ignored()) +expand_default = CallFunction(aten.expand.default, view_default, Ignored()) +full_default = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_2, memory_format=torch.contiguous_format) +view_default_2 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default_1, view_default_2) +view_default_3 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_3, KeywordArg('inv_scale')) +where_self = CallFunction(aten.where.self, expand_default, full_default, div_Tensor) +convert_element_type_default = CallFunction(prims.convert_element_type.default, where_self, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_4 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_4, memory_format=torch.contiguous_format) +view_default_5 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_4, view_default_5) +_sfdp_pattern_17_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_18.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_18.py new file mode 100644 index 0000000000000000000000000000000000000000..252cdb3d147ef700d1d6f4f3bb33edc4f1617ea5 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_18.py @@ -0,0 +1,457 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +full_default = CallFunction(aten.full.default, [], KeywordArg('inv_scale'), dtype=Ignored(), device=Ignored(), pin_memory=False, _users=2) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, full_default) +full_default_1 = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +where_self = CallFunction(aten.where.self, KeywordArg('causal_mask'), div_Tensor, full_default_1, _users=2) +amax_default = CallFunction(aten.amax.default, where_self, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, where_self, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +scalar_tensor_default = CallFunction(aten.scalar_tensor.default, Ignored(), dtype=Ignored(), layout=torch.strided, device=Ignored()) +where_self_1 = CallFunction(aten.where.self, KeywordArg('causal_mask'), fma_default, scalar_tensor_default) +div_Tensor_2 = CallFunction(aten.div.Tensor, where_self_1, full_default) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_18_training = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +full_default = CallFunction(aten.full.default, [], KeywordArg('inv_scale'), dtype=Ignored(), device=Ignored(), pin_memory=False) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, full_default) +full_default_1 = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +where_self = CallFunction(aten.where.self, KeywordArg('causal_mask'), div_Tensor, full_default_1, _users=2) +amax_default = CallFunction(aten.amax.default, where_self, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, where_self, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +_sfdp_pattern_18_inference = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3 +]) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +full_default = CallFunction(aten.full.default, [], KeywordArg('inv_scale'), dtype=Ignored(), device=Ignored(), pin_memory=False, _users=2) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, full_default) +full_default_1 = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +where_self = CallFunction(aten.where.self, KeywordArg('causal_mask'), div_Tensor, full_default_1, _users=2) +amax_default = CallFunction(aten.amax.default, where_self, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, where_self, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +scalar_tensor_default = CallFunction(aten.scalar_tensor.default, Ignored(), dtype=Ignored(), layout=torch.strided, device=Ignored()) +where_self_1 = CallFunction(aten.where.self, KeywordArg('causal_mask'), fma_default, scalar_tensor_default) +div_Tensor_2 = CallFunction(aten.div.Tensor, where_self_1, full_default) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_18_bs1_training = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +full_default = CallFunction(aten.full.default, [], KeywordArg('inv_scale'), dtype=Ignored(), device=Ignored(), pin_memory=False) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, full_default) +full_default_1 = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +where_self = CallFunction(aten.where.self, KeywordArg('causal_mask'), div_Tensor, full_default_1, _users=2) +amax_default = CallFunction(aten.amax.default, where_self, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, where_self, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +_sfdp_pattern_18_bs1_inference = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3 +]) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +full_default = CallFunction(aten.full.default, [], KeywordArg('inv_scale'), dtype=Ignored(), device=Ignored(), pin_memory=False, _users=2) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, full_default) +full_default_1 = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +where_self = CallFunction(aten.where.self, KeywordArg('causal_mask'), div_Tensor, full_default_1) +convert_element_type_default = CallFunction(prims.convert_element_type.default, where_self, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored(), _users=2) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, convert_element_type_default_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +neg_default = CallFunction(aten.neg.default, convert_element_type_default_2) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, mul_Tensor_3, Ignored()) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, convert_element_type_default_4, convert_element_type_default_2, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +convert_element_type_default_5 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +scalar_tensor_default = CallFunction(aten.scalar_tensor.default, Ignored(), dtype=Ignored(), layout=torch.strided, device=Ignored()) +where_self_1 = CallFunction(aten.where.self, KeywordArg('causal_mask'), convert_element_type_default_5, scalar_tensor_default) +div_Tensor_2 = CallFunction(aten.div.Tensor, where_self_1, full_default) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_18_half_training = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +full_default = CallFunction(aten.full.default, [], KeywordArg('inv_scale'), dtype=Ignored(), device=Ignored(), pin_memory=False) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, full_default) +full_default_1 = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +where_self = CallFunction(aten.where.self, KeywordArg('causal_mask'), div_Tensor, full_default_1) +convert_element_type_default = CallFunction(prims.convert_element_type.default, where_self, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +_sfdp_pattern_18_half_inference = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3 +]) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +full_default = CallFunction(aten.full.default, [], KeywordArg('inv_scale'), dtype=Ignored(), device=Ignored(), pin_memory=False, _users=2) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, full_default) +full_default_1 = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +where_self = CallFunction(aten.where.self, KeywordArg('causal_mask'), div_Tensor, full_default_1) +convert_element_type_default = CallFunction(prims.convert_element_type.default, where_self, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored(), _users=2) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, convert_element_type_default_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +neg_default = CallFunction(aten.neg.default, convert_element_type_default_2) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, mul_Tensor_3, Ignored()) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, convert_element_type_default_4, convert_element_type_default_2, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +convert_element_type_default_5 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +scalar_tensor_default = CallFunction(aten.scalar_tensor.default, Ignored(), dtype=Ignored(), layout=torch.strided, device=Ignored()) +where_self_1 = CallFunction(aten.where.self, KeywordArg('causal_mask'), convert_element_type_default_5, scalar_tensor_default) +div_Tensor_2 = CallFunction(aten.div.Tensor, where_self_1, full_default) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_18_half_bs1_training = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +full_default = CallFunction(aten.full.default, [], KeywordArg('inv_scale'), dtype=Ignored(), device=Ignored(), pin_memory=False) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, full_default) +full_default_1 = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +where_self = CallFunction(aten.where.self, KeywordArg('causal_mask'), div_Tensor, full_default_1) +convert_element_type_default = CallFunction(prims.convert_element_type.default, where_self, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +_sfdp_pattern_18_half_bs1_inference = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3 +]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_19.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_19.py new file mode 100644 index 0000000000000000000000000000000000000000..66baf24230d19aa0bb928340b3c91b4662dfad74 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_19.py @@ -0,0 +1,211 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +full_default = CallFunction(aten.full.default, [], KeywordArg('inv_scale'), dtype=Ignored(), device=Ignored(), pin_memory=False, _users=2) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, full_default) +full_default_1 = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +where_self = CallFunction(aten.where.self, KeywordArg('causal_mask'), div_Tensor, full_default_1) +add_Tensor = CallFunction(aten.add.Tensor, where_self, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_1) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +scalar_tensor_default = CallFunction(aten.scalar_tensor.default, Ignored(), dtype=Ignored(), layout=torch.strided, device=Ignored()) +where_self_1 = CallFunction(aten.where.self, KeywordArg('causal_mask'), fma_default, scalar_tensor_default) +div_Tensor_2 = CallFunction(aten.div.Tensor, where_self_1, full_default) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_2) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_3, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_4 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_5 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_5, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +_sfdp_pattern_19_training = MultiOutputPattern([view_default_5, + view_default_9, + permute_default_4, + view_default_11, + None, + None, + None, + None +]) + + +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +full_default = CallFunction(aten.full.default, [], KeywordArg('inv_scale'), dtype=Ignored(), device=Ignored(), pin_memory=False) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, full_default) +full_default_1 = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +where_self = CallFunction(aten.where.self, KeywordArg('causal_mask'), div_Tensor, full_default_1) +add_Tensor = CallFunction(aten.add.Tensor, where_self, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_19_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +full_default = CallFunction(aten.full.default, [], KeywordArg('inv_scale'), dtype=Ignored(), device=Ignored(), pin_memory=False, _users=2) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, full_default) +full_default_1 = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +where_self = CallFunction(aten.where.self, KeywordArg('causal_mask'), div_Tensor, full_default_1) +add_Tensor = CallFunction(aten.add.Tensor, where_self, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, convert_element_type_default) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_1) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_1, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, mul_Tensor_3, Ignored()) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, convert_element_type_default_2, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +scalar_tensor_default = CallFunction(aten.scalar_tensor.default, Ignored(), dtype=Ignored(), layout=torch.strided, device=Ignored()) +where_self_1 = CallFunction(aten.where.self, KeywordArg('causal_mask'), convert_element_type_default_3, scalar_tensor_default) +div_Tensor_2 = CallFunction(aten.div.Tensor, where_self_1, full_default) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_2) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_3, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_4 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_5 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_5, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +_sfdp_pattern_19_half_training = MultiOutputPattern([view_default_5, + view_default_9, + permute_default_4, + view_default_11, + None, + None, + None, + None +]) + + +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +full_default = CallFunction(aten.full.default, [], KeywordArg('inv_scale'), dtype=Ignored(), device=Ignored(), pin_memory=False) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, full_default) +full_default_1 = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +where_self = CallFunction(aten.where.self, KeywordArg('causal_mask'), div_Tensor, full_default_1) +add_Tensor = CallFunction(aten.add.Tensor, where_self, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_19_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_2.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_2.py new file mode 100644 index 0000000000000000000000000000000000000000..f28da434ef0c85ca3d80095e68c052e8dc19dd2d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_2.py @@ -0,0 +1,174 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_2, KeywordArg('scale_factor'), _users=2) +amax_default = CallFunction(aten.amax.default, mul_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, mul_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_1) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, view_default_7, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_1, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_1) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, fma_default, KeywordArg('scale_factor')) +view_default_8 = CallFunction(aten.view.default, mul_Tensor_2, Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_2) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_3, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_4 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_5 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_5, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +_sfdp_pattern_2_training = MultiOutputPattern([view_default_5, + view_default_9, + permute_default_4, + view_default_11, + None +]) + + +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_2, KeywordArg('scale_factor'), _users=2) +amax_default = CallFunction(aten.amax.default, mul_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, mul_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_2_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_2, KeywordArg('scale_factor')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, mul_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored(), _users=2) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +neg_default = CallFunction(aten.neg.default, convert_element_type_default_2) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_1) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, convert_element_type_default_2, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_1, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_1) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_4, KeywordArg('scale_factor')) +view_default_8 = CallFunction(aten.view.default, mul_Tensor_2, Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_2) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_3, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_4 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_5 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_5, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +_sfdp_pattern_2_half_training = MultiOutputPattern([view_default_5, + view_default_9, + permute_default_4, + view_default_11, + None +]) + + +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_2, KeywordArg('scale_factor')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, mul_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_2_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_20.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_20.py new file mode 100644 index 0000000000000000000000000000000000000000..207cb76b49b92a8163689a0d55ba736c8355e3cb --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_20.py @@ -0,0 +1,246 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +eq_Scalar = CallFunction(aten.eq.Scalar, KeywordArg('attn_mask'), Ignored()) +view_default = CallFunction(aten.view.default, eq_Scalar, Ignored()) +expand_default = CallFunction(aten.expand.default, view_default, Ignored(), _users=2) +full_default = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, permute_default, KeywordArg('inv_scale')) +expand_default_1 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_2, memory_format=torch.contiguous_format) +view_default_2 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default_1, view_default_2) +view_default_3 = CallFunction(aten.view.default, bmm_default, Ignored()) +where_self = CallFunction(aten.where.self, expand_default, full_default, view_default_3, _users=2) +amax_default = CallFunction(aten.amax.default, where_self, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, where_self, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_4 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_4, memory_format=torch.contiguous_format) +view_default_5 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_4, view_default_5) +view_default_6 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +scalar_tensor_default = CallFunction(aten.scalar_tensor.default, Ignored(), dtype=Ignored(), layout=torch.strided, device=Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_7 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_5, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_7, permute_default_4) +view_default_8 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_8, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +where_self_1 = CallFunction(aten.where.self, expand_default, scalar_tensor_default, fma_default) +view_default_9 = CallFunction(aten.view.default, where_self_1, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_2, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_9, permute_default_5) +view_default_10 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +div_Tensor_2 = CallFunction(aten.div.Tensor, view_default_10, KeywordArg('inv_scale')) +permute_default_6 = CallFunction(aten.permute.default, div_Tensor_2, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_9) +view_default_11 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_11, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_7) +view_default_12 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_12, Ignored()) +_sfdp_pattern_20_training = MultiOutputPattern([view_default_6, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None, + None +]) + + +eq_Scalar = CallFunction(aten.eq.Scalar, KeywordArg('attn_mask'), Ignored()) +view_default = CallFunction(aten.view.default, eq_Scalar, Ignored()) +expand_default = CallFunction(aten.expand.default, view_default, Ignored()) +full_default = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, permute_default, KeywordArg('inv_scale')) +expand_default_1 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_2, memory_format=torch.contiguous_format) +view_default_2 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default_1, view_default_2) +view_default_3 = CallFunction(aten.view.default, bmm_default, Ignored()) +where_self = CallFunction(aten.where.self, expand_default, full_default, view_default_3, _users=2) +amax_default = CallFunction(aten.amax.default, where_self, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, where_self, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_3 = CallFunction(aten.expand.default, div_Tensor_1, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_4 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_4, memory_format=torch.contiguous_format) +view_default_5 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_4, view_default_5) +_sfdp_pattern_20_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +eq_Scalar = CallFunction(aten.eq.Scalar, KeywordArg('attn_mask'), Ignored()) +view_default = CallFunction(aten.view.default, eq_Scalar, Ignored()) +expand_default = CallFunction(aten.expand.default, view_default, Ignored(), _users=2) +full_default = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, permute_default, KeywordArg('inv_scale')) +expand_default_1 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_2, memory_format=torch.contiguous_format) +view_default_2 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default_1, view_default_2) +view_default_3 = CallFunction(aten.view.default, bmm_default, Ignored()) +where_self = CallFunction(aten.where.self, expand_default, full_default, view_default_3) +convert_element_type_default = CallFunction(prims.convert_element_type.default, where_self, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored(), _users=2) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, convert_element_type_default_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_4 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_4, memory_format=torch.contiguous_format) +view_default_5 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_4, view_default_5) +view_default_6 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +scalar_tensor_default = CallFunction(aten.scalar_tensor.default, Ignored(), dtype=Ignored(), layout=torch.strided, device=Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +neg_default = CallFunction(aten.neg.default, convert_element_type_default_2) +view_default_7 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_5, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_7, permute_default_4) +view_default_8 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_8, mul_Tensor_2) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, mul_Tensor_3, Ignored()) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, convert_element_type_default_4, convert_element_type_default_2, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +convert_element_type_default_5 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +where_self_1 = CallFunction(aten.where.self, expand_default, scalar_tensor_default, convert_element_type_default_5) +view_default_9 = CallFunction(aten.view.default, where_self_1, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_2, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_9, permute_default_5) +view_default_10 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +div_Tensor_2 = CallFunction(aten.div.Tensor, view_default_10, KeywordArg('inv_scale')) +permute_default_6 = CallFunction(aten.permute.default, div_Tensor_2, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_9) +view_default_11 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_11, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_7) +view_default_12 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_12, Ignored()) +_sfdp_pattern_20_half_training = MultiOutputPattern([view_default_6, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None, + None +]) + + +eq_Scalar = CallFunction(aten.eq.Scalar, KeywordArg('attn_mask'), Ignored()) +view_default = CallFunction(aten.view.default, eq_Scalar, Ignored()) +expand_default = CallFunction(aten.expand.default, view_default, Ignored()) +full_default = CallFunction(aten.full.default, [], Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, permute_default, KeywordArg('inv_scale')) +expand_default_1 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_2, memory_format=torch.contiguous_format) +view_default_2 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default_1, view_default_2) +view_default_3 = CallFunction(aten.view.default, bmm_default, Ignored()) +where_self = CallFunction(aten.where.self, expand_default, full_default, view_default_3) +convert_element_type_default = CallFunction(prims.convert_element_type.default, where_self, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_4 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_4, memory_format=torch.contiguous_format) +view_default_5 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_4, view_default_5) +_sfdp_pattern_20_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_21.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_21.py new file mode 100644 index 0000000000000000000000000000000000000000..159e2117f315ac58221ea4500598a4bcc6f9a29f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_21.py @@ -0,0 +1,367 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_7, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +view_default_8 = CallFunction(aten.view.default, fma_default, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_21_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_21_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_7, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +view_default_8 = CallFunction(aten.view.default, fma_default, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_21_bs1_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_21_bs1_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, convert_element_type_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default_1, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default_1, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_2, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, convert_element_type_default_3, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +convert_element_type_default_5 = CallFunction(prims.convert_element_type.default, convert_element_type_default_4, Ignored()) +convert_element_type_default_6 = CallFunction(prims.convert_element_type.default, convert_element_type_default_5, Ignored()) +view_default_8 = CallFunction(aten.view.default, convert_element_type_default_6, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_21_half_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, convert_element_type_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default_1, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default_1, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_2, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_21_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, convert_element_type_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default_1, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default_1, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_2, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, convert_element_type_default_3, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +convert_element_type_default_5 = CallFunction(prims.convert_element_type.default, convert_element_type_default_4, Ignored()) +convert_element_type_default_6 = CallFunction(prims.convert_element_type.default, convert_element_type_default_5, Ignored()) +view_default_8 = CallFunction(aten.view.default, convert_element_type_default_6, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_21_half_bs1_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, convert_element_type_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default_1, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default_1, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_2, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_21_half_bs1_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_22.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_22.py new file mode 100644 index 0000000000000000000000000000000000000000..2d27ca992a770608642dc6b10ddfa0e86104051e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_22.py @@ -0,0 +1,391 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_7, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +view_default_8 = CallFunction(aten.view.default, fma_default, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_22_training = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3, + permute_default_6, + permute_default_9, + permute_default_11, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +_sfdp_pattern_22_inference = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3 +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_7, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +view_default_8 = CallFunction(aten.view.default, fma_default, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_22_bs1_training = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3, + permute_default_6, + permute_default_9, + permute_default_11, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +_sfdp_pattern_22_bs1_inference = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3 +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, convert_element_type_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default_1, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default_1, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_2, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, convert_element_type_default_3, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +convert_element_type_default_5 = CallFunction(prims.convert_element_type.default, convert_element_type_default_4, Ignored()) +convert_element_type_default_6 = CallFunction(prims.convert_element_type.default, convert_element_type_default_5, Ignored()) +view_default_8 = CallFunction(aten.view.default, convert_element_type_default_6, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_22_half_training = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3, + permute_default_6, + permute_default_9, + permute_default_11, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, convert_element_type_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default_1, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default_1, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_2, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +_sfdp_pattern_22_half_inference = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3 +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, convert_element_type_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default_1, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default_1, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_2, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, convert_element_type_default_3, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +convert_element_type_default_5 = CallFunction(prims.convert_element_type.default, convert_element_type_default_4, Ignored()) +convert_element_type_default_6 = CallFunction(prims.convert_element_type.default, convert_element_type_default_5, Ignored()) +view_default_8 = CallFunction(aten.view.default, convert_element_type_default_6, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_22_half_bs1_training = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3, + permute_default_6, + permute_default_9, + permute_default_11, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, convert_element_type_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default_1, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default_1, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_2, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +_sfdp_pattern_22_half_bs1_inference = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3 +]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_23.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_23.py new file mode 100644 index 0000000000000000000000000000000000000000..321a816cb6d72ddb1eb5877a1437aebd28ec9ee8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_23.py @@ -0,0 +1,383 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, view_default_2, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, view_default_2, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_7, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +view_default_8 = CallFunction(aten.view.default, fma_default, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_23_training = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3, + permute_default_6, + permute_default_9, + permute_default_11 +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, view_default_2, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, view_default_2, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +_sfdp_pattern_23_inference = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3 +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, view_default_2, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, view_default_2, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_7, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +view_default_8 = CallFunction(aten.view.default, fma_default, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_23_bs1_training = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3, + permute_default_6, + permute_default_9, + permute_default_11 +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, view_default_2, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, view_default_2, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +_sfdp_pattern_23_bs1_inference = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3 +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, view_default_2, Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, convert_element_type_default, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default_2, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default_2, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_3, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, convert_element_type_default_4, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +convert_element_type_default_5 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +convert_element_type_default_6 = CallFunction(prims.convert_element_type.default, convert_element_type_default_5, Ignored()) +convert_element_type_default_7 = CallFunction(prims.convert_element_type.default, convert_element_type_default_6, Ignored()) +view_default_8 = CallFunction(aten.view.default, convert_element_type_default_7, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_23_half_training = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3, + permute_default_6, + permute_default_9, + permute_default_11 +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, view_default_2, Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, convert_element_type_default, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default_2, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default_2, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_3, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +_sfdp_pattern_23_half_inference = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3 +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, view_default_2, Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, convert_element_type_default, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default_2, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default_2, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_3, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, convert_element_type_default_4, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +convert_element_type_default_5 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +convert_element_type_default_6 = CallFunction(prims.convert_element_type.default, convert_element_type_default_5, Ignored()) +convert_element_type_default_7 = CallFunction(prims.convert_element_type.default, convert_element_type_default_6, Ignored()) +view_default_8 = CallFunction(aten.view.default, convert_element_type_default_7, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_23_half_bs1_training = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3, + permute_default_6, + permute_default_9, + permute_default_11 +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, view_default_2, Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, convert_element_type_default, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default_2, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default_2, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_3, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +_sfdp_pattern_23_half_bs1_inference = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3 +]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_24.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_24.py new file mode 100644 index 0000000000000000000000000000000000000000..32b991561108f6b62fe702a9959b8311df2c6377 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_24.py @@ -0,0 +1,151 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +view_default = CallFunction(aten.view.default, KeywordArg('query'), Ignored(), _users=2) +view_default_1 = CallFunction(aten.view.default, KeywordArg('key'), Ignored()) +permute_default = CallFunction(aten.permute.default, view_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, permute_default) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attention_mask')) +view_default_3 = CallFunction(aten.view.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, view_default_3, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, view_default_3, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=4) +view_default_4 = CallFunction(aten.view.default, KeywordArg('value'), Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, div_Tensor, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_1) +mul_Tensor = CallFunction(aten.mul.Tensor, bmm_default_2, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor, _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, fma_default, permute_default_2) +view_default_7 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_3, fma_default) +permute_default_4 = CallFunction(aten.permute.default, bmm_default_4, Ignored()) +view_default_8 = CallFunction(aten.view.default, permute_default_4, Ignored()) +permute_default_5 = CallFunction(aten.permute.default, div_Tensor, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_5, view_default_6) +view_default_9 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +_sfdp_pattern_24_training = MultiOutputPattern([view_default_5, + view_default_7, + view_default_8, + view_default_9, + None +]) + + +view_default = CallFunction(aten.view.default, KeywordArg('query'), Ignored()) +view_default_1 = CallFunction(aten.view.default, KeywordArg('key'), Ignored()) +permute_default = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, permute_default) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attention_mask')) +view_default_3 = CallFunction(aten.view.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, view_default_3, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, view_default_3, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +view_default_4 = CallFunction(aten.view.default, KeywordArg('value'), Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, div_Tensor, view_default_4) +_sfdp_pattern_24_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +view_default = CallFunction(aten.view.default, KeywordArg('query'), Ignored(), _users=2) +view_default_1 = CallFunction(aten.view.default, KeywordArg('key'), Ignored()) +permute_default = CallFunction(aten.permute.default, view_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, permute_default) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attention_mask')) +view_default_3 = CallFunction(aten.view.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, view_default_3, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, view_default_3, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored(), _users=2) +view_default_4 = CallFunction(aten.view.default, KeywordArg('value'), Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, convert_element_type_default, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_1) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, bmm_default_2, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, convert_element_type_default_1, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +view_default_7 = CallFunction(aten.view.default, fma_default, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +view_default_8 = CallFunction(aten.view.default, convert_element_type_default_2, Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_2) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_3, view_default_8) +permute_default_4 = CallFunction(aten.permute.default, bmm_default_4, Ignored()) +view_default_10 = CallFunction(aten.view.default, permute_default_4, Ignored()) +permute_default_5 = CallFunction(aten.permute.default, convert_element_type_default, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_5, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +_sfdp_pattern_24_half_training = MultiOutputPattern([view_default_5, + view_default_9, + view_default_10, + view_default_11, + None +]) + + +view_default = CallFunction(aten.view.default, KeywordArg('query'), Ignored()) +view_default_1 = CallFunction(aten.view.default, KeywordArg('key'), Ignored()) +permute_default = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, permute_default) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attention_mask')) +view_default_3 = CallFunction(aten.view.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, view_default_3, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, view_default_3, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +view_default_4 = CallFunction(aten.view.default, KeywordArg('value'), Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, convert_element_type_default, view_default_4) +_sfdp_pattern_24_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_25.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_25.py new file mode 100644 index 0000000000000000000000000000000000000000..143146dcee4c16ca5618f42127fe90450c90d12e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_25.py @@ -0,0 +1,391 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +view_default_8 = CallFunction(aten.view.default, fma_default, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_25_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_25_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +view_default_8 = CallFunction(aten.view.default, fma_default, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_25_bs1_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_25_bs1_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, convert_element_type_default_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_2, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, mul_Tensor_3, Ignored()) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +view_default_8 = CallFunction(aten.view.default, convert_element_type_default_4, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_25_half_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_25_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, convert_element_type_default_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_2, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, mul_Tensor_3, Ignored()) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +view_default_8 = CallFunction(aten.view.default, convert_element_type_default_4, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_25_half_bs1_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_25_half_bs1_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_26.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_26.py new file mode 100644 index 0000000000000000000000000000000000000000..5aedda2ea97aff37932a8a02c42a73561d670d22 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_26.py @@ -0,0 +1,415 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +view_default_8 = CallFunction(aten.view.default, fma_default, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_26_training = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +_sfdp_pattern_26_inference = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3 +]) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +view_default_8 = CallFunction(aten.view.default, fma_default, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_26_bs1_training = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +_sfdp_pattern_26_bs1_inference = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3 +]) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, convert_element_type_default_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_2, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, mul_Tensor_3, Ignored()) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +view_default_8 = CallFunction(aten.view.default, convert_element_type_default_4, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_26_half_training = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +_sfdp_pattern_26_half_inference = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3 +]) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, convert_element_type_default_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_2, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, mul_Tensor_3, Ignored()) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +view_default_8 = CallFunction(aten.view.default, convert_element_type_default_4, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_26_half_bs1_training = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +add_Tensor = CallFunction(aten.add.Tensor, view_default_2, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +_sfdp_pattern_26_half_bs1_inference = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3 +]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_27.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_27.py new file mode 100644 index 0000000000000000000000000000000000000000..677e538c8313b7143198125a6006a00979d1e89d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_27.py @@ -0,0 +1,403 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, view_default_2, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, view_default_2, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +view_default_8 = CallFunction(aten.view.default, fma_default, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_27_training = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3, + permute_default_6, + permute_default_9, + permute_default_11, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, view_default_2, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, view_default_2, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +_sfdp_pattern_27_inference = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3 +]) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, view_default_2, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, view_default_2, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +view_default_8 = CallFunction(aten.view.default, fma_default, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_27_bs1_training = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3, + permute_default_6, + permute_default_9, + permute_default_11, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, view_default_2, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, view_default_2, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +_sfdp_pattern_27_bs1_inference = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3 +]) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, view_default_2, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, convert_element_type_default_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_2, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, mul_Tensor_3, Ignored()) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +view_default_8 = CallFunction(aten.view.default, convert_element_type_default_4, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_27_half_training = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3, + permute_default_6, + permute_default_9, + permute_default_11, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, view_default_2, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +_sfdp_pattern_27_half_inference = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3 +]) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, view_default_2, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, convert_element_type_default_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_2, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, mul_Tensor_3, Ignored()) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +view_default_8 = CallFunction(aten.view.default, convert_element_type_default_4, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_27_half_bs1_training = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3, + permute_default_6, + permute_default_9, + permute_default_11, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, view_default_2, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +_sfdp_pattern_27_half_bs1_inference = MultiOutputPattern([view_default_5, + permute_default_1, + permute_default_3 +]) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_28.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_28.py new file mode 100644 index 0000000000000000000000000000000000000000..f414823adcebfc3e21f7ba23b454e5bd57493957 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_28.py @@ -0,0 +1,202 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_2, KeywordArg('scale_factor'), _users=2) +amax_default = CallFunction(aten.amax.default, mul_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, mul_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, mul_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_2, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_1) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, convert_element_type_default, Ignored()) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_3) +mul_Tensor_5 = CallFunction(aten.mul.Tensor, mul_Tensor_4, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_5, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_5) +mul_Tensor_6 = CallFunction(aten.mul.Tensor, fma_default, KeywordArg('scale_factor')) +view_default_8 = CallFunction(aten.view.default, mul_Tensor_6, Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_2) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_3, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_4 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_5 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_5, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +_sfdp_pattern_28_training = MultiOutputPattern([view_default_5, + view_default_9, + permute_default_4, + view_default_11, + None, + None +]) + + +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_2, KeywordArg('scale_factor'), _users=2) +amax_default = CallFunction(aten.amax.default, mul_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, mul_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_28_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_2, KeywordArg('scale_factor')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, mul_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored(), _users=2) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, gt_Scalar, convert_element_type_default_1) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, mul_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_2, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +neg_default = CallFunction(aten.neg.default, convert_element_type_default_2) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_1) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, Ignored()) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_3) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, mul_Tensor_4, Ignored()) +mul_Tensor_5 = CallFunction(aten.mul.Tensor, convert_element_type_default_4, convert_element_type_default_2, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_5, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_5) +convert_element_type_default_5 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +mul_Tensor_6 = CallFunction(aten.mul.Tensor, convert_element_type_default_5, KeywordArg('scale_factor')) +view_default_8 = CallFunction(aten.view.default, mul_Tensor_6, Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_2) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_3, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_4 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_5 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_5, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +_sfdp_pattern_28_half_training = MultiOutputPattern([view_default_5, + view_default_9, + permute_default_4, + view_default_11, + None, + None +]) + + +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_2, KeywordArg('scale_factor')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, mul_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_28_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_3.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_3.py new file mode 100644 index 0000000000000000000000000000000000000000..2c7f7519ad0570d2c2f700d4081c9b7253d16657 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_3.py @@ -0,0 +1,190 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale_factor'), _users=2) +amax_default = CallFunction(aten.amax.default, div_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, div_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_1) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +div_Tensor_2 = CallFunction(aten.div.Tensor, fma_default, KeywordArg('inv_scale_factor')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_2) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_3, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_4 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_5 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_5, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +_sfdp_pattern_3_training = MultiOutputPattern([view_default_5, + view_default_9, + permute_default_4, + view_default_11, + None, + None +]) + + +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale_factor'), _users=2) +amax_default = CallFunction(aten.amax.default, div_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, div_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_3_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale_factor')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored(), _users=2) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, convert_element_type_default_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +neg_default = CallFunction(aten.neg.default, convert_element_type_default_2) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_1) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, mul_Tensor_3, Ignored()) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, convert_element_type_default_4, convert_element_type_default_2, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +convert_element_type_default_5 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +div_Tensor_2 = CallFunction(aten.div.Tensor, convert_element_type_default_5, KeywordArg('inv_scale_factor')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_2) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_3, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_4 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_5 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_5, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +_sfdp_pattern_3_half_training = MultiOutputPattern([view_default_5, + view_default_9, + permute_default_4, + view_default_11, + None, + None +]) + + +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale_factor')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_3_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_4.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_4.py new file mode 100644 index 0000000000000000000000000000000000000000..dc9cfd506f950415f4f90b49edf83815432a641c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_4.py @@ -0,0 +1,190 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_2, KeywordArg('scale_factor'), _users=2) +amax_default = CallFunction(aten.amax.default, mul_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, mul_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, mul_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_2, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_1) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, convert_element_type_default, Ignored()) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_3) +mul_Tensor_5 = CallFunction(aten.mul.Tensor, mul_Tensor_4, div_Tensor, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_5, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_5) +mul_Tensor_6 = CallFunction(aten.mul.Tensor, fma_default, KeywordArg('scale_factor')) +view_default_8 = CallFunction(aten.view.default, mul_Tensor_6, Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_2) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_3, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_4 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_5 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_5, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +_sfdp_pattern_4_training = MultiOutputPattern([view_default_5, + view_default_9, + permute_default_4, + view_default_11, + None, + None +]) + + +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_2, KeywordArg('scale_factor'), _users=2) +amax_default = CallFunction(aten.amax.default, mul_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, mul_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_4_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_2, KeywordArg('scale_factor')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, mul_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored(), _users=2) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, gt_Scalar, convert_element_type_default_1) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, mul_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_2, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +neg_default = CallFunction(aten.neg.default, convert_element_type_default_2) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_1) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, Ignored()) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_3) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, mul_Tensor_4, Ignored()) +mul_Tensor_5 = CallFunction(aten.mul.Tensor, convert_element_type_default_4, convert_element_type_default_2, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_5, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_5) +convert_element_type_default_5 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +mul_Tensor_6 = CallFunction(aten.mul.Tensor, convert_element_type_default_5, KeywordArg('scale_factor')) +view_default_8 = CallFunction(aten.view.default, mul_Tensor_6, Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_2) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_3, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_4 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_5 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_5, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +_sfdp_pattern_4_half_training = MultiOutputPattern([view_default_5, + view_default_9, + permute_default_4, + view_default_11, + None, + None +]) + + +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_2, KeywordArg('scale_factor')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, mul_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_4_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_5.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_5.py new file mode 100644 index 0000000000000000000000000000000000000000..a36ff3ae712fb101102a070e9787463da9d506f8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_5.py @@ -0,0 +1,180 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_1) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, view_default_7, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +div_Tensor_2 = CallFunction(aten.div.Tensor, fma_default, KeywordArg('inv_scale')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_2) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_3, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_4 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_5 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_5, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +_sfdp_pattern_5_training = MultiOutputPattern([view_default_5, + view_default_9, + permute_default_4, + view_default_11, + None, + None +]) + + +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_5_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored(), _users=2) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +neg_default = CallFunction(aten.neg.default, convert_element_type_default_2) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_1) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, convert_element_type_default_3, convert_element_type_default_2, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +div_Tensor_2 = CallFunction(aten.div.Tensor, convert_element_type_default_4, KeywordArg('inv_scale')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_2) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_3, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_4 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_5 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_5, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +_sfdp_pattern_5_half_training = MultiOutputPattern([view_default_5, + view_default_9, + permute_default_4, + view_default_11, + None, + None +]) + + +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_5_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_6.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_6.py new file mode 100644 index 0000000000000000000000000000000000000000..48e3b33dfecc818cc00a71fb18130fdbf79555f7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_6.py @@ -0,0 +1,196 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_1) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +div_Tensor_2 = CallFunction(aten.div.Tensor, fma_default, KeywordArg('inv_scale')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_2) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_3, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_4 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_5 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_5, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +_sfdp_pattern_6_training = MultiOutputPattern([view_default_5, + view_default_9, + permute_default_4, + view_default_11, + None, + None, + None +]) + + +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask'), _users=2) +amax_default = CallFunction(aten.amax.default, add_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, add_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +expand_default_2 = CallFunction(aten.expand.default, div_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_6_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored(), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored(), _users=2) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, convert_element_type_default_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, mul_Tensor_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, convert_element_type_default_1, Ignored(), _users=2) +neg_default = CallFunction(aten.neg.default, convert_element_type_default_2) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_1) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, view_default_7, mul_Tensor_2) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, mul_Tensor_3, Ignored()) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, convert_element_type_default_4, convert_element_type_default_2, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +convert_element_type_default_5 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +div_Tensor_2 = CallFunction(aten.div.Tensor, convert_element_type_default_5, KeywordArg('inv_scale')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_2 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_2) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_3, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_4 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_5 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_5, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +_sfdp_pattern_6_half_training = MultiOutputPattern([view_default_5, + view_default_9, + permute_default_4, + view_default_11, + None, + None, + None +]) + + +expand_default = CallFunction(aten.expand.default, KeywordArg('query'), Ignored()) +view_default = CallFunction(aten.view.default, expand_default, Ignored()) +permute_default = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default, Ignored()) +view_default_1 = CallFunction(aten.view.default, expand_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +add_Tensor = CallFunction(aten.add.Tensor, div_Tensor, KeywordArg('attn_mask')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, add_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, KeywordArg('value'), Ignored()) +view_default_4 = CallFunction(aten.view.default, expand_default_3, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_6_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_7.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_7.py new file mode 100644 index 0000000000000000000000000000000000000000..4f42972a0d383b8ee7988439a3fa4aff12fd24ef --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_7.py @@ -0,0 +1,225 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale'), _users=2) +amax_default = CallFunction(aten.amax.default, div_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, div_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, mul_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, permute_default_3, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, convert_element_type_default_2, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +div_Tensor_2 = CallFunction(aten.div.Tensor, fma_default, KeywordArg('inv_scale')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, view_default_11, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, convert_element_type_default_4, Ignored()) +_sfdp_pattern_7_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale'), _users=2) +amax_default = CallFunction(aten.amax.default, div_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, div_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, permute_default_3, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_7_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, mul_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, convert_element_type_default_2, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +div_Tensor_2 = CallFunction(aten.div.Tensor, convert_element_type_default_4, KeywordArg('inv_scale')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_7_half_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_7_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_8.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_8.py new file mode 100644 index 0000000000000000000000000000000000000000..711f68cc6189e156161f6331fb3da71067bfd6f8 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_8.py @@ -0,0 +1,209 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale'), _users=2) +amax_default = CallFunction(aten.amax.default, div_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, div_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, permute_default_3, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, convert_element_type_default_2, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +div_Tensor_2 = CallFunction(aten.div.Tensor, fma_default, KeywordArg('inv_scale')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, view_default_11, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, convert_element_type_default_3, Ignored()) +_sfdp_pattern_8_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale'), _users=2) +amax_default = CallFunction(aten.amax.default, div_Tensor, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, div_Tensor, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, permute_default_3, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_8_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +mul_Tensor = CallFunction(aten.mul.Tensor, convert_element_type_default_2, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +div_Tensor_2 = CallFunction(aten.div.Tensor, convert_element_type_default_3, KeywordArg('inv_scale')) +view_default_8 = CallFunction(aten.view.default, div_Tensor_2, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +permute_default_6 = CallFunction(aten.permute.default, view_default_9, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_8_half_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +expand_default = CallFunction(aten.expand.default, permute_default, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, view_default_2, KeywordArg('inv_scale')) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_8_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_9.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_9.py new file mode 100644 index 0000000000000000000000000000000000000000..599851e7af012cc3ae6302664a4317c011216484 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/_sfdp_pattern_9.py @@ -0,0 +1,225 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, permute_default, KeywordArg('inv_scale')) +expand_default = CallFunction(aten.expand.default, div_Tensor, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, view_default_2, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, view_default_2, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, mul_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, permute_default_3, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, convert_element_type_default_2, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +view_default_8 = CallFunction(aten.view.default, fma_default, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +div_Tensor_2 = CallFunction(aten.div.Tensor, view_default_9, KeywordArg('inv_scale')) +permute_default_6 = CallFunction(aten.permute.default, div_Tensor_2, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, view_default_11, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, convert_element_type_default_4, Ignored()) +_sfdp_pattern_9_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, permute_default, KeywordArg('inv_scale')) +expand_default = CallFunction(aten.expand.default, div_Tensor, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, view_default_2, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, view_default_2, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, permute_default_3, Ignored()) +expand_default_3 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_9_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) + + +rand_default = CallFunction(aten.rand.default, Ignored(), dtype=Ignored(), device=Ignored(), pin_memory=False) +gt_Scalar = CallFunction(aten.gt.Scalar, rand_default, KeywordArg('dropout_p'), _users=2) +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, permute_default, KeywordArg('inv_scale')) +expand_default = CallFunction(aten.expand.default, div_Tensor, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored(), _users=2) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored(), _users=2) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, view_default_2, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList, _users=3) +mul_Tensor = CallFunction(aten.mul.Tensor, gt_Scalar, div_Tensor_1) +mul_Tensor_1 = CallFunction(aten.mul.Tensor, mul_Tensor, Ignored()) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, mul_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored(), _users=2) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored(), _users=2) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +view_default_5 = CallFunction(aten.view.default, bmm_default_1, Ignored()) +neg_default = CallFunction(aten.neg.default, div_Tensor_1) +view_default_6 = CallFunction(aten.view.default, KeywordArg('tangents_1'), Ignored(), _users=2) +permute_default_4 = CallFunction(aten.permute.default, view_default_4, Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, view_default_6, permute_default_4) +view_default_7 = CallFunction(aten.view.default, bmm_default_2, Ignored()) +convert_element_type_default_2 = CallFunction(prims.convert_element_type.default, view_default_7, Ignored()) +convert_element_type_default_3 = CallFunction(prims.convert_element_type.default, gt_Scalar, Ignored()) +mul_Tensor_2 = CallFunction(aten.mul.Tensor, convert_element_type_default_3, Ignored()) +mul_Tensor_3 = CallFunction(aten.mul.Tensor, convert_element_type_default_2, mul_Tensor_2) +mul_Tensor_4 = CallFunction(aten.mul.Tensor, mul_Tensor_3, div_Tensor_1, _users=2) +sum_dim_IntList_1 = CallFunction(aten.sum.dim_IntList, mul_Tensor_4, Ignored(), True) +fma_default = CallFunction(prims.fma.default, neg_default, sum_dim_IntList_1, mul_Tensor_4) +convert_element_type_default_4 = CallFunction(prims.convert_element_type.default, fma_default, Ignored()) +view_default_8 = CallFunction(aten.view.default, convert_element_type_default_4, Ignored(), _users=2) +permute_default_5 = CallFunction(aten.permute.default, view_default_1, Ignored()) +bmm_default_3 = CallFunction(aten.bmm.default, view_default_8, permute_default_5) +view_default_9 = CallFunction(aten.view.default, bmm_default_3, Ignored()) +div_Tensor_2 = CallFunction(aten.div.Tensor, view_default_9, KeywordArg('inv_scale')) +permute_default_6 = CallFunction(aten.permute.default, div_Tensor_2, Ignored()) +permute_default_7 = CallFunction(aten.permute.default, view_default, Ignored()) +bmm_default_4 = CallFunction(aten.bmm.default, permute_default_7, view_default_8) +view_default_10 = CallFunction(aten.view.default, bmm_default_4, Ignored()) +permute_default_8 = CallFunction(aten.permute.default, view_default_10, Ignored()) +permute_default_9 = CallFunction(aten.permute.default, permute_default_8, Ignored()) +permute_default_10 = CallFunction(aten.permute.default, view_default_3, Ignored()) +bmm_default_5 = CallFunction(aten.bmm.default, permute_default_10, view_default_6) +view_default_11 = CallFunction(aten.view.default, bmm_default_5, Ignored()) +permute_default_11 = CallFunction(aten.permute.default, view_default_11, Ignored()) +_sfdp_pattern_9_half_training = MultiOutputPattern([view_default_5, + permute_default_6, + permute_default_9, + permute_default_11, + None, + None +]) + + +permute_default = CallFunction(aten.permute.default, KeywordArg('query'), Ignored()) +div_Tensor = CallFunction(aten.div.Tensor, permute_default, KeywordArg('inv_scale')) +expand_default = CallFunction(aten.expand.default, div_Tensor, Ignored()) +clone_default = CallFunction(aten.clone.default, expand_default, memory_format=torch.contiguous_format) +view_default = CallFunction(aten.view.default, clone_default, Ignored()) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('key'), Ignored()) +permute_default_2 = CallFunction(aten.permute.default, permute_default_1, Ignored()) +expand_default_1 = CallFunction(aten.expand.default, permute_default_2, Ignored()) +clone_default_1 = CallFunction(aten.clone.default, expand_default_1, memory_format=torch.contiguous_format) +view_default_1 = CallFunction(aten.view.default, clone_default_1, Ignored()) +bmm_default = CallFunction(aten.bmm.default, view_default, view_default_1) +view_default_2 = CallFunction(aten.view.default, bmm_default, Ignored()) +convert_element_type_default = CallFunction(prims.convert_element_type.default, view_default_2, Ignored(), _users=2) +amax_default = CallFunction(aten.amax.default, convert_element_type_default, Ignored(), True) +sub_Tensor = CallFunction(aten.sub.Tensor, convert_element_type_default, amax_default) +exp_default = CallFunction(aten.exp.default, sub_Tensor, _users=2) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, exp_default, Ignored(), True) +div_Tensor_1 = CallFunction(aten.div.Tensor, exp_default, sum_dim_IntList) +convert_element_type_default_1 = CallFunction(prims.convert_element_type.default, div_Tensor_1, Ignored()) +expand_default_2 = CallFunction(aten.expand.default, convert_element_type_default_1, Ignored()) +view_default_3 = CallFunction(aten.view.default, expand_default_2, Ignored()) +permute_default_3 = CallFunction(aten.permute.default, KeywordArg('value'), Ignored()) +expand_default_3 = CallFunction(aten.expand.default, permute_default_3, Ignored()) +clone_default_2 = CallFunction(aten.clone.default, expand_default_3, memory_format=torch.contiguous_format) +view_default_4 = CallFunction(aten.view.default, clone_default_2, Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, view_default_3, view_default_4) +_sfdp_pattern_9_half_inference = CallFunction(aten.view.default, bmm_default_1, Ignored(), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/addmm_pattern.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/addmm_pattern.py new file mode 100644 index 0000000000000000000000000000000000000000..70d672442170905a411de63187a5b579b286bf73 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/addmm_pattern.py @@ -0,0 +1,53 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +addmm_default = CallFunction(aten.addmm.default, KeywordArg('input'), KeywordArg('mat1'), KeywordArg('mat2'), beta=KeywordArg('beta'), alpha=KeywordArg('alpha')) +mul_Scalar = CallFunction(aten.mul.Scalar, KeywordArg('tangents_1'), KeywordArg('beta')) +sum_dim_IntList = CallFunction(aten.sum.dim_IntList, mul_Scalar, Ignored(), True) +view_default = CallFunction(aten.view.default, sum_dim_IntList, Ignored()) +permute_default = CallFunction(aten.permute.default, KeywordArg('mat2'), Ignored()) +mm_default = CallFunction(aten.mm.default, KeywordArg('tangents_1'), permute_default) +mul_Scalar_1 = CallFunction(aten.mul.Scalar, mm_default, KeywordArg('alpha')) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('mat1'), Ignored()) +mm_default_1 = CallFunction(aten.mm.default, permute_default_1, KeywordArg('tangents_1')) +mul_Scalar_2 = CallFunction(aten.mul.Scalar, mm_default_1, KeywordArg('alpha')) +addmm_pattern_training = MultiOutputPattern([addmm_default, + view_default, + mul_Scalar_1, + mul_Scalar_2, + None, + None +]) + + +addmm_pattern_inference = CallFunction(aten.addmm.default, KeywordArg('input'), KeywordArg('mat1'), KeywordArg('mat2'), beta=KeywordArg('beta'), alpha=KeywordArg('alpha'), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/bmm_pattern.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/bmm_pattern.py new file mode 100644 index 0000000000000000000000000000000000000000..7b5ac59d6f06c97523e071e9b3ea78516ff09c0e --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/bmm_pattern.py @@ -0,0 +1,45 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +bmm_default = CallFunction(aten.bmm.default, KeywordArg('mat1'), KeywordArg('mat2')) +permute_default = CallFunction(aten.permute.default, KeywordArg('mat2'), Ignored()) +bmm_default_1 = CallFunction(aten.bmm.default, KeywordArg('tangents_1'), permute_default) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('mat1'), Ignored()) +bmm_default_2 = CallFunction(aten.bmm.default, permute_default_1, KeywordArg('tangents_1')) +bmm_pattern_training = MultiOutputPattern([bmm_default, + bmm_default_1, + bmm_default_2 +]) + + +bmm_pattern_inference = CallFunction(aten.bmm.default, KeywordArg('mat1'), KeywordArg('mat2'), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/mm_pattern.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/mm_pattern.py new file mode 100644 index 0000000000000000000000000000000000000000..058a2f881e3a52cb147cfd3fa0ef2bbd0a25945a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/serialized_patterns/mm_pattern.py @@ -0,0 +1,45 @@ +# mypy: ignore-errors + +# noqa: F401, E501 +# This is an auto-generated file. Please do not modify it by hand. +# To re-generate, run: +# cd ~/pytorch && python torchgen/fuse/gen_patterns.py + +import torch +import torch._inductor +import operator + +aten = torch.ops.aten +prims = torch.ops.prims + +from torch._inductor.pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethod, + CallMethodVarArgs, + CallModule, + CallModuleVarArgs, + ExclusiveKeywordArg, + Ignored, + KeywordArg, + ListOf, + MultiOutputPattern, + PatternExpr, + RepeatedExpr, + _TargetArgsExpr, + _TargetExpr, + _TargetExprVarArgs, +) +mm_default = CallFunction(aten.mm.default, KeywordArg('mat1'), KeywordArg('mat2')) +permute_default = CallFunction(aten.permute.default, KeywordArg('mat2'), Ignored()) +mm_default_1 = CallFunction(aten.mm.default, KeywordArg('tangents_1'), permute_default) +permute_default_1 = CallFunction(aten.permute.default, KeywordArg('mat1'), Ignored()) +mm_default_2 = CallFunction(aten.mm.default, permute_default_1, KeywordArg('tangents_1')) +mm_pattern_training = MultiOutputPattern([mm_default, + mm_default_1, + mm_default_2 +]) + + +mm_pattern_inference = CallFunction(aten.mm.default, KeywordArg('mat1'), KeywordArg('mat2'), _users=0) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/split_cat.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/split_cat.py new file mode 100644 index 0000000000000000000000000000000000000000..9f548846d14639e5b34662beb5752feb5b673b57 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/split_cat.py @@ -0,0 +1,3045 @@ +# mypy: allow-untyped-defs +import itertools +import logging +import operator +import os +from collections import defaultdict +from collections.abc import Callable, Sequence +from typing import Any, TypeAlias + +import torch +from torch._dynamo.utils import counters +from torch.fx.experimental.symbolic_shapes import free_symbols, guard_or_false +from torch.utils._ordered_set import OrderedSet + +from ..pattern_matcher import ( + Arg, + CallFunction, + CallFunctionVarArgs, + CallMethodVarArgs, + FailedMatch, + get_arg_value, + Ignored, + KeywordArg, + ListOf, + Match, + MatchContext, + MULTIPLE, + PatternExpr, + PatternMatcherPass, + register_graph_pattern, + RepeatedExpr, +) +from .group_batch_fusion import is_node_meta_valid, POST_GRAD_FUSIONS, PRE_GRAD_FUSIONS + + +log = logging.getLogger(__name__) + +_Arguments: TypeAlias = tuple[torch.fx.node.Argument, ...] +_TransformParam: TypeAlias = tuple[ + _Arguments | None, + _Arguments | None, + _Arguments | None, + _Arguments | None, +] +_Range: TypeAlias = tuple[int, int] + + +PRE_GRAD_PATTERNS: dict[str, PatternMatcherPass] = {} +POST_GRAD_PATTERNS: dict[str, PatternMatcherPass] = {} + +pre_grad_pass_names = [ + "normalization_pass", + "remove_split_with_size_one_pass", + "merge_getitem_cat_pass", + "merge_stack_tahn_unbind_pass", + "merge_splits_pass", + "mutate_cat_pass", + "split_cat_pass", + "unbind_stack_pass", + "split_cat_to_slices_pass", + "unbind_cat_to_view_pass", + "split_stack_to_cats_pass", + "unbind_stack_to_slices_pass", + "move_reshape_out_of_split_stack_pass", + "einsum_to_pointwise_pass", +] + +post_grad_pass_names = [ + "normalization_aten_pass", + "decompose_mm_pass", + "unbind_stack_aten_pass", + "shape_padding_multiplier", + "pad_aten_mm_pass", + "split_cat_aten_pass", + "select_cat_aten_pass", + "move_view_after_cat_aten_pass", +] + +backend = os.environ.get("TORCHINDUCTOR_PATTERN_MATCH_BACKEND", "inductor") + +for pass_name in pre_grad_pass_names: + # exclude all passes from the group batch fusion + # they do not use pattern matcher + if pass_name in PRE_GRAD_FUSIONS: + continue + PRE_GRAD_PATTERNS[pass_name] = PatternMatcherPass( + pass_name=pass_name, + ) + +for pass_name in post_grad_pass_names: + # exclude all passes from the group batch fusion + # they do not use pattern matcher + if pass_name in POST_GRAD_FUSIONS: + continue + POST_GRAD_PATTERNS[pass_name] = PatternMatcherPass( + pass_name=pass_name, + ) + + +def construct_pattern_matcher_pass(pass_name: str): + """ + Return the specific pattern_matcher_pass given the pass name. + """ + if pass_name in PRE_GRAD_PATTERNS: + return PRE_GRAD_PATTERNS[pass_name] + else: + return POST_GRAD_PATTERNS[pass_name] + + +def _get_split_args_default(split_node): + input_kwarg = "tensor" + split_size_kwarg = "split_size_or_sections" + dim_kwarg = "dim" + default_dim_value = 0 + if split_node.op == "call_method": + split_size_kwarg = "split_size" + return ( + get_arg_value(split_node, 0, input_kwarg), + get_arg_value(split_node, 1, split_size_kwarg), + get_arg_value(split_node, 2, dim_kwarg) or default_dim_value, + ) + + +def _get_dim(node: Any): + assert isinstance(node, torch.fx.Node) + if "dim" in node.kwargs: + assert isinstance(node.kwargs["dim"], int) + return node.kwargs["dim"] + if node.target is torch.unbind: + if len(node.args) == 2: + assert isinstance(node.args[-1], int) + return node.args[-1] + return 0 # defaults to dim=0 + if node.target is torch.split: + if len(node.args) == 3: + assert isinstance(node.args[-1], int) + return node.args[-1] + return 0 # defaults to dim=0 + raise AssertionError( + f"Can't extract `dim` from {node.target} {node.args} {node.kwargs}" + ) + + +# noqa: W605 +# ############The pattern to be optimized is######### +# unbind (dim=0) +# / ... \ +# getitem getitem -> user=1 +# | | +# split split -> dim=1, user=1, split_section_size=1 +# | | +# getitem getitem -> user=1 +# \ / +# cat (dim=1) -> user=1 +# | + +# ################After transformation############# +# unbind (dim=0) +# / ... \ +# getitem getitem -> user=1 +# \ / +# cat (dim=1) -> user=1 +# | + + +def normalize_split_base( + match: Match, + _get_split_args: Callable[ + [torch.fx.Node], tuple[torch.fx.Node | None, Any | None, int | None] + ], +): + """ + Normalize split with split_size into split_with_sizes, so that we only deal with one type of split in + subsequent optimizations + """ + split_node = match.nodes[0] + graph = match.graph + split_input, split_size, split_dim = _get_split_args(split_node) + if split_input is None or split_dim is None or split_size is None: + log.debug("couldn't find split args") + return + if not is_node_meta_valid(split_node): + log.debug("example value absent for node: %s", split_node) + return + assert isinstance(split_node.meta["example_value"], (list, tuple)) + split_sections = [t.size()[split_dim] for t in split_node.meta["example_value"]] + + if any(isinstance(section, torch.SymInt) for section in split_sections): + # TODO dynamic_shapes with assume_static_by_default=False fails while AOT Autograd tracing. + return + if split_dim < 0: # Normalize split dim + split_dim += split_input.meta["example_value"].dim() + + new_args = (split_input, split_sections) + new_kwargs = {"dim": split_dim} + if ( + split_node.args == new_args + and split_node.kwargs == new_kwargs + and split_node.op == "call_function" + ): + return + + with graph.inserting_after(split_node): + new_split_node = graph.call_function( + torch.split, + args=new_args, + kwargs=new_kwargs, # type: ignore[arg-type] + ) + split_node.replace_all_uses_with(new_split_node) + new_split_node.meta.update(split_node.meta) + graph.erase_node(split_node) + counters[backend]["normalization_pass"] += 1 + + +@register_graph_pattern( + CallFunctionVarArgs(torch.split, users=MULTIPLE), + pass_dict=construct_pattern_matcher_pass("normalization_pass"), +) +@register_graph_pattern( + CallMethodVarArgs("split", users=MULTIPLE), + pass_dict=construct_pattern_matcher_pass("normalization_pass"), +) +def normalize_split_default(match: Match, *args, **kwargs): + return normalize_split_base(match, _get_split_args_default) + + +@register_graph_pattern( + CallFunctionVarArgs(torch.split, users=MULTIPLE), + pass_dict=construct_pattern_matcher_pass("remove_split_with_size_one_pass"), +) +@register_graph_pattern( + CallMethodVarArgs("split", users=MULTIPLE), + pass_dict=construct_pattern_matcher_pass("remove_split_with_size_one_pass"), +) +def remove_split_with_size_one(match: Match, *args, **kwargs): + graph = match.graph + split_node = match.nodes[0] + split_input, split_size, split_dim = _get_split_args_default(split_node) + if split_input is None or split_dim is None or split_size is None: + log.debug("couldn't find split args") + return + if not is_node_meta_valid(split_node): + log.debug("example value absent for node: %s", split_node) + return + assert isinstance(split_node.meta["example_value"], (list, tuple)) + split_sections = [t.size()[split_dim] for t in split_node.meta["example_value"]] + + if any(isinstance(section, torch.SymInt) for section in split_sections): + # TODO dynamic_shapes with assume_static_by_default=False fails while AOT Autograd tracing. + return + # remove the dummy split whose split sections size is one + # theoretically nodes with no users should be removed, but we have seen the corner case + # thus we add its users check to walk around the StopIteration error. + if len(split_sections) == 1 and len(split_node.users.keys()) > 0: + # find the grand children of the split_node + next_users = find_next_users(split_node) + user = next(iter(split_node.users.keys())) + # replace the users of grand child node with the input node + for next_user in next_users: + next_user.replace_input_with(user, split_input) + # erase the split node and its child + graph.erase_node(user) + graph.erase_node(split_node) + counters[backend]["remove_split_with_size_one_pass"] += 1 + + +@register_graph_pattern( + CallFunctionVarArgs(torch.unbind, users=MULTIPLE), + pass_dict=construct_pattern_matcher_pass("normalization_pass"), +) +@register_graph_pattern( + CallMethodVarArgs("unbind", users=MULTIPLE), + pass_dict=construct_pattern_matcher_pass("normalization_pass"), +) +def normalize_unbind_default(match: Match, *args, **kwargs): + node = match.nodes[0] + graph = match.graph + input = get_arg_value(node, 0, "input") + dim = get_arg_value(node, 1, "dim") + if dim is None: + axis = node.kwargs.get("axis") + if axis is not None: + dim = axis + else: + dim = 0 + if input is None: + log.debug("couldn't find unbind args") + return + if not is_node_meta_valid(input): + log.debug("example value absent for node: %s", input) + return + ndim = input.meta["example_value"].ndim + + if dim < 0: # Normalize unbind dim + dim += ndim + with graph.inserting_after(node): + new_node = graph.call_function( + torch.unbind, + args=(input,), + kwargs={"dim": dim}, + ) + node.replace_all_uses_with(new_node) + new_node.meta.update(node.meta) + graph.erase_node(node) + counters[backend]["normalization_pass"] += 1 + + +@register_graph_pattern( + CallFunctionVarArgs([torch.cat, torch.concat], users=MULTIPLE), + pass_dict=construct_pattern_matcher_pass("normalization_pass"), +) +def normalize_cat_default(match: Match, *args, **kwargs): + cat_node = match.nodes[0] + graph = match.graph + tensors = get_arg_value(cat_node, 0, "tensors") + cat_dim = get_arg_value(cat_node, 1, "dim") + if cat_dim is None: + cat_axis = cat_node.kwargs.get("axis") + if cat_axis is not None: + cat_dim = cat_axis + else: + cat_dim = 0 + if tensors is None or cat_dim is None: + log.debug("couldn't find cat args") + return + assert isinstance(tensors, (list, tuple)) + for tensor in itertools.chain([cat_node], tensors): + if not is_node_meta_valid(tensor): + log.debug("example value absent for node: %s", tensor) + return + + ndim = cat_node.meta["example_value"].dim() + + def is_empty_tensor(x): + # special case where torch.cat supports cat'ing with an empty tensor + x_shape = x.meta["example_value"].shape + return len(x_shape) == 1 and guard_or_false(x_shape[0] == 0) + + assert all( + ndim == x.meta["example_value"].dim() or is_empty_tensor(x) for x in tensors + ) + + if cat_dim < 0: # Normalize cat dim + cat_dim += ndim + + new_args = (tensors,) + new_kwargs = {"dim": cat_dim} + if ( + cat_node.args == new_args + and cat_node.kwargs == new_kwargs + and cat_node.op == "call_function" + and cat_node.target is torch.cat + ): + return + + with graph.inserting_after(cat_node): + new_cat_node = graph.call_function( + torch.cat, + args=new_args, + kwargs=new_kwargs, + ) + cat_node.replace_all_uses_with(new_cat_node) + new_cat_node.meta.update(cat_node.meta) + graph.erase_node(cat_node) + counters[backend]["normalization_pass"] += 1 + + +@register_graph_pattern( + CallFunctionVarArgs(torch.stack, users=MULTIPLE), + pass_dict=construct_pattern_matcher_pass("normalization_pass"), +) +def normalize_stack_default(match: Match, *args, **kwargs): + node = match.nodes[0] + graph = match.graph + tensors = get_arg_value(node, 0, "tensors") + dim = get_arg_value(node, 1, "dim") or 0 + if tensors is None or dim is None: + log.debug("couldn't find stack args") + return + assert isinstance(tensors, (list, tuple)) + + # A bug in pytorch, some nodes miss the example_value metadata + for tensor in itertools.chain([node], tensors): + if not is_node_meta_valid(tensor): + log.debug("example value absent for node: %s", tensor) + return + + ndim = node.meta["example_value"].dim() + if dim < 0: # Normalize dim + dim += ndim + + with graph.inserting_after(node): + new_node = graph.call_function( + node.target, # type: ignore[arg-type] + args=(tensors,), + kwargs={"dim": dim}, + ) + node.replace_all_uses_with(new_node) + new_node.meta.update(node.meta) + graph.erase_node(node) + counters[backend]["normalization_pass"] += 1 + + +def find_next_users(split_node: torch.fx.Node) -> list[torch.fx.Node]: + next_users = [] + for getitem_node in split_node.users: + for getitem_user in getitem_node.users: + if getitem_user not in next_users: + next_users.append(getitem_user) + return next_users + + +@register_graph_pattern( + CallMethodVarArgs("squeeze", users=MULTIPLE), + pass_dict=construct_pattern_matcher_pass("normalization_pass"), +) +def normalize_squeeze_default(match: Match, *args, **kwargs): + squeeze_node = match.nodes[0] + squeeze_input = get_arg_value(squeeze_node, 0) + + if "dim" in squeeze_node.kwargs: + assert len(squeeze_node.args) == 1 + dim = squeeze_node.kwargs["dim"] + elif len(squeeze_node.args) == 1: + # squeeze(Tensor) + dim = None + elif len(squeeze_node.args) == 2: + # squeeze(Tensor self, int dim) + # squeeze(Tensor self, int[] dim) + dim = squeeze_node.args[1] + else: + # squeeze(Tensor self, int[] dim) (called with varargs) + dim = squeeze_node.args[1:] + + if isinstance(dim, Sequence) and len(dim) == 1: + dim = dim[0] + + with match.graph.inserting_after(squeeze_node): + if dim is None: + new_squeeze_node = match.graph.call_function( + torch.squeeze, args=(squeeze_input,) + ) + else: + new_squeeze_node = match.graph.call_function( + torch.squeeze, args=(squeeze_input,), kwargs={"dim": dim} + ) + squeeze_node.replace_all_uses_with(new_squeeze_node) + new_squeeze_node.meta.update(squeeze_node.meta) + match.graph.erase_node(squeeze_node) + + +@register_graph_pattern( + CallMethodVarArgs("reshape", users=MULTIPLE), + pass_dict=construct_pattern_matcher_pass("normalization_pass"), +) +def normalize_reshape_default(match: Match, *args, **kwargs): + reshape_node = match.nodes[0] + if not is_node_meta_valid(reshape_node): + log.debug("example value absent for node: %s", reshape_node) + return + reshape_input = get_arg_value(reshape_node, 0) + + if free_symbols(reshape_node.meta["example_value"].shape): + log.debug("dynamic shape not supported: %s", reshape_node) + return + + with match.graph.inserting_after(reshape_node): + new_reshape_node = match.graph.call_function( + torch.reshape, + args=(reshape_input, tuple(reshape_node.meta["example_value"].shape)), + ) + reshape_node.replace_all_uses_with(new_reshape_node) + new_reshape_node.meta.update(reshape_node.meta) + match.graph.erase_node(reshape_node) + + +@register_graph_pattern( + CallMethodVarArgs("clamp", users=MULTIPLE), + pass_dict=construct_pattern_matcher_pass("normalization_pass"), +) +@register_graph_pattern( + CallFunctionVarArgs(torch.clamp, users=MULTIPLE), + pass_dict=construct_pattern_matcher_pass("normalization_pass"), +) +def normalize_clamp_default(match: Match, *args, **kwargs): + clamp_node = match.nodes[0] + if not is_node_meta_valid(clamp_node): + log.debug("example value absent for node: %s", clamp_node) + return + + if free_symbols(clamp_node.meta["example_value"].shape): + log.debug("dynamic shape not supported: %s", clamp_node) + return + if len(clamp_node.args) > 1: + args = (get_arg_value(clamp_node, 0),) + kwargs = { + "min": get_arg_value(clamp_node, 1, kwarg_name="min"), + "max": get_arg_value(clamp_node, 2, kwarg_name="max"), + } + else: + args = clamp_node.args + kwargs = clamp_node.kwargs + with match.graph.inserting_after(clamp_node): + new_clamp_node = match.graph.call_function( + torch.clamp, + args=args, + kwargs=kwargs, + ) + clamp_node.replace_all_uses_with(new_clamp_node) + new_clamp_node.meta.update(clamp_node.meta) + match.graph.erase_node(clamp_node) + + +@register_graph_pattern( + CallMethodVarArgs("detach", users=MULTIPLE), + pass_dict=construct_pattern_matcher_pass("normalization_pass"), +) +def normalize_detach_default(match: Match, *args, **kwargs): + detach_node = match.nodes[0] + if not is_node_meta_valid(detach_node): + log.debug("example value absent for node: %s", detach_node) + return + + if free_symbols(detach_node.meta["example_value"].shape): + log.debug("dynamic shape not supported: %s", detach_node) + return + + with match.graph.inserting_after(detach_node): + new_detach_node = match.graph.call_function( + torch.detach, + args=detach_node.args, + ) + detach_node.replace_all_uses_with(new_detach_node) + new_detach_node.meta.update(detach_node.meta) + match.graph.erase_node(detach_node) + + +class TorchSplit(CallFunction): + """ + Matches a call to torch.split if it is in a normalized form. Ensures that all users of + splits are unique getitems. + """ + + def __init__(self, arg, sizes, func=torch.split) -> None: + # using KeywordArg("dim") for `dim` checks they all match + super().__init__(func, arg, sizes, _users=MULTIPLE, dim=KeywordArg("dim")) + + def _match(self, node: torch.fx.Node, ctx: MatchContext): + m = super()._match(node, ctx) + if not m: + return m + split_sections = node.args[1] + if not isinstance(split_sections, (list, tuple)): + return FailedMatch("split not normalized") + # check users are all unique getitems + seen_idxs = OrderedSet[int]() + for user in node.users: + if not CallFunction(operator.getitem, Arg(), Arg()).match(user): + # This should ideally never happen. Split user should always be a getitem + return FailedMatch(f"user of split not a getitem: {user}") + if not isinstance(user.args[1], int): + return FailedMatch("only integer getitems are handled") + if user.args[1] in seen_idxs: + return FailedMatch(f"duplicate getitem {user.args[1]}") + if user.args[-1] < 0: # type: ignore[operator] + # This shouldn't ideally happen as dynamo normalizes indexes to positive + return FailedMatch("negative index") + seen_idxs.add(user.args[1]) + return m + + +@register_graph_pattern( + TorchSplit( + CallFunction( + operator.getitem, + TorchSplit( + KeywordArg("first_split_input"), + KeywordArg("first_split_sections"), + ), + Ignored(), + ), + KeywordArg("next_split_sections"), + ), + pass_dict=construct_pattern_matcher_pass("merge_splits_pass"), +) +def merge_splits( + match: Match, + first_split_input: torch.fx.Node, + first_split_sections: list[int], + next_split_sections: list[int], + # Note: dim is implicitly passed by TorchSplit, as it internally uses a pattern with dim + dim: int, +): + node = match.output_node() + # it is possible that the split has no users, + # we check the corner case and skip the pattern + if len(node.users.keys()) == 0: + return + graph = match.graph + first_split = node.args[0].args[0] # type: ignore[union-attr] + next_split_index = node.args[0].args[1] # type: ignore[union-attr] + + new_split_sections = list(first_split_sections) + new_split_sections[next_split_index : next_split_index + 1] = next_split_sections # type: ignore[operator, misc] + + first_split_dim = _get_dim(first_split) + + to_remove = [] + + with graph.inserting_before(first_split): # type: ignore[arg-type] + # Add the new split node + new_split = graph.call_function( + torch.split, + args=(first_split_input, new_split_sections), + kwargs={"dim": first_split_dim}, + ) + if is_node_meta_valid(first_split_input): + new_split.meta["example_value"] = torch.split( + first_split_input.meta["example_value"], + new_split_sections, + dim=first_split_dim, + ) + first_split_num_to_user = { + user.args[1]: user + for user in first_split.users # type: ignore[union-attr] + } + + new_split_num = 0 + for split_num in range(len(first_split_sections)): + if split_num not in first_split_num_to_user: + new_split_num += 1 + continue + old_getitem = first_split_num_to_user[split_num] + if split_num != next_split_index: + old_getitem.update_arg(0, new_split) + old_getitem.update_arg(1, new_split_num) + new_split_num += 1 + else: + next_split_num_to_user = {user.args[1]: user for user in node.users} + # It is not necessary all getitems from the split node are used. + for next_split_num in range(len(next_split_sections)): + with graph.inserting_after(new_split): + new_getitem = graph.call_function( + operator.getitem, args=(new_split, new_split_num) + ) + new_split_num += 1 + if next_split_num not in next_split_num_to_user: + continue + next_getitem = next_split_num_to_user[next_split_num] + new_getitem.meta.update(next_getitem.meta) + next_getitem.replace_all_uses_with(new_getitem) + to_remove.append(next_getitem) + to_remove.append(node) + to_remove.append(old_getitem) + + to_remove.append(first_split) # type: ignore[arg-type] + for node in to_remove: + graph.erase_node(node) + + counters[backend]["merge_splits_pass"] += 1 + + +class SplitCatSimplifier: + """ + Helper class to simplify split-cat pattern. In simple cases, both split and cat node can be removed in a "split->cat" + pattern. However, there are various cases where they can't and we need to simplify split/ add transforms before cat. + Some such cases are: + 1. Final node has additional args (not coming from the initial split) + 2. Shuffling of args between split/cat + 3. Some final nodes are non-(cat/stack) + 4. Split-dim != cat-dim (but equal split) + + Note that any combination of the above cases can happen. + + To deal with 1, 2, & 3 - we iterate over all users of split. And figure out common "ranges" that can be merged. + Then, we simplify the split accordingly. In the best case, split can be entirely removed. + + To deal with 4, we add some transformations (unflatten + movedim) (See `get_transform_params`). + + Finally, depending on final node being cat or stack, unsqueeze/flatten needs to be added. + + """ + + def simplify( + self, + graph: torch.fx.Graph, + split_node: torch.fx.Node, + split_sections: list[int], + ): + # Find the next users (i.e. users after the getitem) + next_users = find_next_users(split_node) + # Gather inputs of the next users. When inputs come from `split_node`, they are instead represented by + # a tuple indicating the split ranges. See `get_user_input_list` for more details + user_inputs_list = self.get_user_input_list(split_node, next_users) + # Simplify the split_sections based on user_inputs_list. In simpler cases, len(simplified_split_ranges) == 1 and + # we can simply replace the split node. Otherwise, we simplify it. + simplified_split_ranges = self.get_simplified_split_ranges( + split_sections, next_users, user_inputs_list + ) + if not simplified_split_ranges: # Simplification not possible + return + transform_params_list = self.get_transform_params( + split_node, next_users, user_inputs_list + ) + if not transform_params_list: + return + + # Start actual replacement + user_inputs_list_new = self.replace_split( + graph, split_node, split_sections, user_inputs_list, simplified_split_ranges + ) + self.replace_cat( + graph, + split_node, + next_users, + user_inputs_list_new, + transform_params_list, # type: ignore[arg-type] + ) + self.erase_old_nodes(graph, split_node, next_users) # type: ignore[arg-type] + counters[backend]["unbind_stack_pass"] += 1 + + def get_user_input_list( + self, split_node: torch.fx.Node, next_users: list[torch.fx.Node] + ) -> list[list[torch.fx.Node | _Range]]: + """ + Returns list of inputs to the following user nodes, in order. The outer list represents the user node. The inner + list represents the inputs to that particular node. This list can either contain + - a tuple representing the ranges of get_items that should go into the cat (closed interval) + - torch.fx.Node representing "other" inputs (which are not coming from our split) + """ + user_inputs_list: list[list[torch.fx.Node | _Range]] = [] + for user in next_users: + if user.target in (torch.cat, torch.stack): + user_inputs_list.append(self.get_merged_user_inputs(split_node, user)) + else: + user_inputs_list.append(self.get_non_cat_node_input(split_node, user)) # type: ignore[arg-type] + return user_inputs_list + + def get_merged_user_inputs( + self, split_node: torch.fx.Node, cat_node: torch.fx.Node + ) -> list[torch.fx.Node | _Range]: + user_inputs = get_arg_value(cat_node, 0, "tensors") + simplified_user_inputs = [] + split_users = OrderedSet(split_node.users.keys()) + for user_input in user_inputs: + if user_input not in split_users: + simplified_user_inputs.append(user_input) + else: + # Add which "getitem" cat depends on + simplified_user_inputs.append(user_input.args[1]) + return self.merge_consecutive_inputs(simplified_user_inputs) + + def get_non_cat_node_input( + self, split_node: torch.fx.Node, node: torch.fx.Node + ) -> list[_Range]: + """ + Get input for a non cat node in the same format as `get_merged_user_inputs` + """ + node_input = [] + split_users = OrderedSet(split_node.users.keys()) + for node_arg in node.all_input_nodes: + if node_arg in split_users: + getitem_num = get_arg_value(node_arg, 1) + node_input.append((getitem_num, getitem_num)) + return node_input + + def merge_consecutive_inputs( + self, inputs: list[torch.fx.Node | int] + ) -> list[torch.fx.Node | _Range]: + """ + Merge consecutive inputs going into a user node. + + For e.g. + [arg0, 0, 1, 2, arg1] -> [arg0, (0, 2), arg1] + """ + merged_ranges = [] + cur_range = None + # pyrefly: ignore [bad-assignment] + for input_ in inputs: + if isinstance(input_, int): + if not cur_range: + cur_range = [input_, input_] + elif input_ == cur_range[1] + 1: + cur_range[1] += 1 + else: + merged_ranges.append(tuple(cur_range)) + cur_range = [input_, input_] + else: + if cur_range: + merged_ranges.append(tuple(cur_range)) + cur_range = None + merged_ranges.append(input_) # type: ignore[arg-type] + if cur_range: + merged_ranges.append(tuple(cur_range)) + return merged_ranges # type: ignore[return-value] + + def get_simplified_split_ranges( + self, + split_sections, + next_users, + user_inputs_list: list[list[torch.fx.Node | _Range]], + ) -> list[_Range] | None: + ranges = OrderedSet[Any]() + for user_inputs in user_inputs_list: + ranges.update(u for u in user_inputs if isinstance(u, tuple)) + + cumulative_sizes = [0] + torch.cumsum(torch.tensor(split_sections), 0).tolist() + split_ranges = sorted( + [(cumulative_sizes[r[0]], cumulative_sizes[r[1] + 1]) for r in ranges] + ) + + if not self.has_non_overlapping_ranges( + split_ranges, + ): # This need not be a strict condition + # However, we keep it now for simplicity. + return None + split_ranges = self.fill_gaps(split_ranges, 0, cumulative_sizes[-1]) + if len(split_sections) == len(split_ranges): # Simplification not possible + return None + counters[backend]["scmerge_split_sections_removed"] = len(split_sections) - len( + split_ranges + ) + return split_ranges + + def has_non_overlapping_ranges(self, ranges: list[_Range]) -> bool: + for range_, next_range in itertools.pairwise(ranges): + if range_[1] > next_range[0]: + return False + return True + + def fill_gaps(self, ranges: list[_Range], min_: int, max_: int) -> list[_Range]: + cur = min_ + filled_ranges = [] + for a, b in ranges: + if cur < a: + filled_ranges.append((cur, a)) + filled_ranges.append((a, b)) + cur = b + if filled_ranges[-1][1] < max_: + filled_ranges.append((filled_ranges[-1][1], max_)) + return filled_ranges + + def get_transform_params( + self, + split_node: torch.fx.Node, + next_users: list[torch.fx.Node], + user_inputs_list: list[list[torch.fx.Node | _Range]], + ) -> list[list[_TransformParam]] | None: + """ + Figure out what transforms are needed for each input to each cat node. + + We replace a split node with an unflatten followed by a movedim + """ + split_dim = _get_dim(split_node) + split_sections = split_node.args[1] + transform_params_list: list[list[_TransformParam]] = [] + + for user_node, user_inputs in zip(next_users, user_inputs_list): + if user_node.target not in (torch.cat, torch.stack): + transform_params_list.append([]) + continue + + cat_dim = get_arg_value(user_node, 1, "dim") + transform_params: list[_TransformParam] = [] + for user_input in user_inputs: + if split_dim == cat_dim and user_node.target is torch.cat: + # No transform needed + transform_params.append((None, None, None, None)) + elif isinstance(user_input, tuple): # Split being simplified + # Verify equal split + subset_split_sections = split_sections[ # type: ignore[index] + # pyrefly: ignore [bad-index] + user_input[0] : user_input[1] + + 1 # type: ignore[index] + ] + # All sections should be equal + if len(OrderedSet(subset_split_sections)) != 1: # type: ignore[arg-type] + return None + + num_splits = len(subset_split_sections) # type: ignore[arg-type] + unflatten_params = (split_dim, (num_splits, -1)) + movedim_params = ( + (split_dim, cat_dim) if split_dim != cat_dim else None + ) + transform_params.append( + (unflatten_params, movedim_params, None, None) + ) + elif ( + user_node.target is torch.stack or split_dim != cat_dim + ): # We need to unsqueeze inputs not coming through split + transform_params.append((None, None, (cat_dim,), None)) + else: # Non-split inputs + transform_params.append((None, None, None, None)) + transform_params_list.append(transform_params) + return transform_params_list + + def replace_split( + self, + graph: torch.fx.Graph, + split_node: torch.fx.Node, + split_sections: list[int], + user_inputs_list: list[list[torch.fx.Node | _Range]], + split_ranges: list[_Range], + ) -> list[list[torch.fx.Node]]: + """ + Replace the split node. It can either remove the split node if len(split_ranges) == 1, or simplify it + into a split with lesser sections if len(split_ranges) > 1. + + Returns the new `user_inputs_list`, with tuples replaced with new getitems from the newer split node. + """ + split_input = split_node.args[0] + split_dim = _get_dim(split_node) + if len(split_ranges) == 1: # We can completely eliminate the split node + split_items = [split_input] + else: + with graph.inserting_after(split_node): + new_split = graph.call_function( + torch.split, + args=( + split_input, + [r[1] - r[0] for r in split_ranges], + ), + kwargs={"dim": split_dim}, + ) + if is_node_meta_valid(split_input): # type: ignore[arg-type, union-attr] + new_split.meta["example_value"] = torch.split( + split_input.meta["example_value"], # type: ignore[union-attr] + [r[1] - r[0] for r in split_ranges], + dim=split_dim, + ) + counters[backend]["scmerge_split_added"] += 1 + split_items = [] + with graph.inserting_after(new_split): + for i in range(len(split_ranges)): + getitem = graph.call_function(operator.getitem, args=(new_split, i)) + if is_node_meta_valid(new_split): + getitem.meta["example_value"] = new_split.meta["example_value"][ + i + ] + split_items.append(getitem) + # Now assign the right getitem to the right input + cumulative_sizes = [0] + torch.cumsum(torch.tensor(split_sections), 0).tolist() + new_user_inputs_list = [] + for user_inputs in user_inputs_list: + new_user_inputs = [] + for user_input in user_inputs: + if isinstance(user_input, tuple): + # Find the correct new getitem (present in split_items) + new_user_inputs.append( + # pyrefly: ignore [bad-argument-type] + split_items[ + split_ranges.index( + ( + cumulative_sizes[user_input[0]], + cumulative_sizes[user_input[1] + 1], + ) + ) + ] + ) + else: + new_user_inputs.append(user_input) + new_user_inputs_list.append(new_user_inputs) + return new_user_inputs_list # type: ignore[return-value] + + def replace_cat( + self, + graph: torch.fx.Graph, + split_node: torch.fx.Node, + next_users: list[torch.fx.Node], + user_inputs_list_new, + transform_params_list: list[list[_TransformParam]], + ): + split_dim = _get_dim(split_node) + split_users = split_node.users.keys() + new_cats = [] + for user_node, user_inputs_new, transform_params in zip( + next_users, user_inputs_list_new, transform_params_list + ): + if user_node.target not in (torch.cat, torch.stack): + # Change the args and kwargs of non-cat/stack nodes. Replace old getitems (belonging to + # the original split node) with the newer getitems + next_cat_input = 0 + for input_node in user_node.all_input_nodes: + if input_node in split_users: + user_node.replace_input_with( + input_node, user_inputs_new[next_cat_input] + ) + next_cat_input += 1 + continue + + # Handle cat/stack user nodes + cat_dim = get_arg_value(user_node, 1, "dim") + user_inputs_new_transformed, user_inputs_new_transformed_meta = [], [] + # For `unsqueeze` transform, we will combine consecutive inputs with the same unsqueeze params, and stack them + to_stack, to_stack_meta = [], [] + stack_dim = None + with graph.inserting_before(user_node): + # pyrefly: ignore [bad-assignment] + for user_input_new, transform_param in zip( + user_inputs_new, transform_params + ): + # pyrefly: ignore [bad-argument-type] + if not is_node_meta_valid(user_input_new): + log.debug("example value absent for node: %s", user_input_new) + return + # Apply transforms + ( + unflatten_params, + movedim_params, + unsqueeze_params, + flatten_params, + ) = transform_param + if unsqueeze_params and ( + stack_dim is None or stack_dim == unsqueeze_params[0] + ): + to_stack.append(user_input_new) + # pyrefly: ignore [missing-attribute] + to_stack_meta.append(user_input_new.meta["example_value"]) + stack_dim = unsqueeze_params[0] + continue + elif to_stack: + stacked_input = graph.call_function( + torch.stack, args=(to_stack,), kwargs={"dim": stack_dim} + ) + stacked_input.meta["example_value"] = torch.stack( # type: ignore[arg-type] + to_stack_meta, + dim=stack_dim, # type: ignore[arg-type] + ) + to_stack, to_stack_meta = [], [] + stack_dim = None + user_inputs_new_transformed.append(stacked_input) + user_inputs_new_transformed_meta.append( + stacked_input.meta["example_value"] + ) + if unsqueeze_params: + to_stack.append(user_input_new) + stack_dim = unsqueeze_params[0] + # pyrefly: ignore [missing-attribute] + to_stack_meta.append(user_input_new.meta["example_value"]) + continue + + if unflatten_params: + # pyrefly: ignore [missing-attribute] + user_input_new_meta = user_input_new.meta["example_value"] + user_input_new = graph.call_function( + torch.unflatten, args=(user_input_new, *unflatten_params) + ) + user_input_new.meta["example_value"] = torch.unflatten( # type: ignore[arg-type] + user_input_new_meta, # type: ignore[arg-type] + *unflatten_params, # type: ignore[arg-type] + ) + if movedim_params: + # pyrefly: ignore [missing-attribute] + user_input_new_meta = user_input_new.meta["example_value"] + user_input_new = graph.call_function( + torch.movedim, args=(user_input_new, *movedim_params) + ) + user_input_new.meta["example_value"] = torch.movedim( # type: ignore[arg-type] + user_input_new_meta, # type: ignore[arg-type] + *movedim_params, # type: ignore[arg-type] + ) + if flatten_params: + # pyrefly: ignore [missing-attribute] + user_input_new_meta = user_input_new.meta["example_value"] + user_input_new = graph.call_function( + torch.flatten, args=(user_input_new, *flatten_params) + ) + user_input_new.meta["example_value"] = torch.flatten( # type: ignore[arg-type] + user_input_new_meta, + *flatten_params, # type: ignore[arg-type] + ) + user_inputs_new_transformed.append(user_input_new) + user_inputs_new_transformed_meta.append( + # pyrefly: ignore [missing-attribute] + user_input_new.meta["example_value"] + ) + if to_stack: + stacked_input = graph.call_function( + torch.stack, args=(to_stack,), kwargs={"dim": stack_dim} + ) + stacked_input.meta["example_value"] = torch.stack( # type: ignore[arg-type] + to_stack_meta, + dim=stack_dim, # type: ignore[arg-type] + ) + user_inputs_new_transformed.append(stacked_input) + user_inputs_new_transformed_meta.append( + stacked_input.meta["example_value"] + ) + + with graph.inserting_after(user_node): + if len(user_inputs_new_transformed) > 1: + new_cat_node = graph.call_function( + torch.cat, + args=(user_inputs_new_transformed,), + kwargs={"dim": cat_dim}, + ) + new_cat_node.meta["example_value"] = torch.cat( + user_inputs_new_transformed_meta, + dim=cat_dim, + ) + counters[backend]["scmerge_cat_added"] += 1 + else: + new_cat_node = user_inputs_new_transformed[-1] + new_cat_node.meta["example_value"] = ( + user_inputs_new_transformed_meta[-1] + ) + + if ( + user_node.target is torch.cat + and split_dim != cat_dim + and split_node.target is torch.split + ): + with graph.inserting_after(new_cat_node): + new_cat_node_meta = new_cat_node.meta["example_value"] + new_cat_node = graph.call_function( + torch.flatten, args=(new_cat_node, cat_dim, cat_dim + 1) + ) + new_cat_node.meta["example_value"] = torch.flatten( + new_cat_node_meta, + cat_dim, + cat_dim + 1, + ) + user_node.replace_all_uses_with(new_cat_node) + new_cats.append(new_cat_node) + + def erase_old_nodes( + self, + graph: torch.fx.Graph, + split_node: torch.fx.Node, + next_users: list[torch.fx.Node], + ): + to_remove = [split_node] + counters[backend]["scmerge_split_removed"] += 1 + to_remove.extend(split_node.users.keys()) + for next_user in next_users: + if next_user.target not in (torch.cat, torch.stack): + continue + counters[backend]["scmerge_cat_removed"] += 1 + to_remove.append(next_user) + for node in reversed(to_remove): + if len(node.users.keys()) == 0: + graph.erase_node(node) + + +class UnbindCatRemover(SplitCatSimplifier): + """ + Helper class to merge Unbind->Cat/Stack. Many of the cases are similar to SplitCatSimplifier. + + Unbind can't be simplified like splits. So, we can only remove the unbind node. Other than this, + other cases like multiple users, additional args, dim mismatch are similar to `SplitCatSimplifier`, + hence we extend that class. + """ + + def remove_unbind( + self, + graph: torch.fx.Graph, + unbind_node: torch.fx.Node, + ): + if not is_node_meta_valid(unbind_node): + return + # we need to check if the getitem indices from unbind are consecutive and all go to the same cat node + # before we do the unbind remove, otherwise it will hit the error when we unbind part of them + getitem_indices = [getitem_node.args[1] for getitem_node in unbind_node.users] + if not is_sorted_and_consecutive(getitem_indices) or len( # type: ignore[arg-type] + getitem_indices + ) != len(unbind_node.meta["example_value"]): + return + num_unbind = len(getitem_indices) + split_sections = [1 for _ in range(num_unbind)] # type: ignore[operator, arg-type] + + super().simplify(graph, unbind_node, split_sections) + + def get_simplified_split_ranges( + self, + split_sections: list[int], + next_users: list[torch.fx.Node], + user_inputs_list: list[list[torch.fx.Node | _Range]], + ) -> list[_Range] | None: + simplified_split_ranges = super().get_simplified_split_ranges( + split_sections, next_users, user_inputs_list + ) + if not simplified_split_ranges or len(simplified_split_ranges) != 1: + return None + return simplified_split_ranges + + def get_transform_params( + self, + split_node: torch.fx.Node, + next_users: list[torch.fx.Node], + user_inputs_list: list[list[torch.fx.Node | _Range]], + ) -> list[list[_TransformParam]] | None: + """ + Figure out what transforms are needed for each input to each cat node. + + Here is the rough transforms we apply: + + x -> unbind -> stack => x -> movedim + + x -> unbind -> cat => x -> movedim -> flatten + + When cat/stack nodes have additional args: + + addn ---| addn -> unsqueeze ---| + x -> unbind -> stack => x -> movedim -> cat + + addn ---| addn ---| + x -> unbind -> cat => x -> movedim -> flatten -> cat + + (Note application of these depends on the dims as well) + + + """ + split_dim = _get_dim(split_node) + transform_params_list: list[list[_TransformParam]] = [] + for user_node, user_inputs in zip(next_users, user_inputs_list): + cat_dim = get_arg_value(user_node, 1, "dim") or 0 + transform_params: list[_TransformParam] = [] + for user_input in user_inputs: + if isinstance(user_input, tuple): + # User input is coming from unbind + movedim_params = ( + (split_dim, cat_dim) if split_dim != cat_dim else None + ) + flatten_params = None + if user_node.target is torch.cat: + flatten_params = (cat_dim, cat_dim + 1) + transform_params.append( + (None, movedim_params, None, flatten_params) + ) + elif ( + user_node.target is torch.stack + ): # We need to unsqueeze inputs not coming through unbind into cat + transform_params.append((None, None, (cat_dim,), None)) + else: # Non-unbind inputs + transform_params.append((None, None, None, None)) + transform_params_list.append(transform_params) + return transform_params_list + + +class GetItem(CallFunction): + def __init__(self, arg, index, _users=1) -> None: + super().__init__(operator.getitem, arg, index, _users=_users) + + def find_anchor_nodes(self, ctx: MatchContext, searched: OrderedSet[torch.fx.Node]): + # We generally match GetItem with arg being an Arg(). So, we never return the anchor + # nodes as the stored node in ctx.pattern_to_node is returned. Here we override find_anchor_nodes + # to not use ctx.pattern_to_node + for pattern in self.flat_args_kwargs[0]: + if isinstance(pattern, PatternExpr): + for other_node in pattern.find_anchor_nodes(ctx, searched): + if not isinstance(other_node, torch.fx.Node): + continue + for node in other_node.users: + if node not in searched: + if self._match_fns(node): + yield node + searched.add(node) + + +@register_graph_pattern( + RepeatedExpr( + CallFunction( + torch.squeeze, + GetItem( + TorchSplit( + KeywordArg("split_input"), + KeywordArg("split_sizes"), + ), + Ignored(), + ), + KeywordArg("dim"), + _users=MULTIPLE, + ), + ), + pass_dict=construct_pattern_matcher_pass("split_cat_pass"), +) +@register_graph_pattern( + RepeatedExpr( + CallFunction( + torch.squeeze, + GetItem( + TorchSplit( + KeywordArg("split_input"), + KeywordArg("split_sizes"), + ), + Ignored(), + ), + dim=KeywordArg("dim"), + _users=MULTIPLE, + ) + ), + pass_dict=construct_pattern_matcher_pass("split_cat_pass"), +) +def merge_split_squeeze( + match: Match, split_input: torch.fx.Node, split_sizes: list[int], dim: int +): + graph = match.graph + split = next(node for node in match.nodes if node.target is torch.split) + if not all(s == 1 for s in split_sizes): + return + if isinstance(dim, Sequence): + return + next_users = find_next_users(split) + if not all(node.target is torch.squeeze for node in next_users): + return + with graph.inserting_before(match.output_node()): + unbind = graph.call_function( + torch.unbind, args=(split_input,), kwargs={"dim": dim} + ) + if is_node_meta_valid(split_input): + unbind.meta["example_value"] = torch.unbind( + split_input.meta["example_value"], dim=dim + ) + for item_index, getitem_node in sorted( + [(getitem_node.args[1], getitem_node) for getitem_node in split.users] + ): + squeeze = next(iter(getitem_node.users.keys())) + new_get_item = graph.call_function( + operator.getitem, args=(unbind, item_index) + ) + squeeze.replace_all_uses_with(new_get_item) + new_get_item.meta.update(squeeze.meta) + graph.erase_node(squeeze) + graph.erase_node(getitem_node) + graph.erase_node(split) + counters[backend]["split_cat_pass"] += 1 + + +getitem_unbind = ListOf( + GetItem( + CallFunction( + torch.unbind, + KeywordArg("unbind_input"), + dim=KeywordArg("dim"), + _users=MULTIPLE, + ), + Ignored(), + _users=MULTIPLE, + ), + partial=True, +) + + +@register_graph_pattern( + CallFunction([torch.stack, torch.cat], getitem_unbind, Ignored(), _users=MULTIPLE), + pass_dict=construct_pattern_matcher_pass("unbind_stack_pass"), +) +@register_graph_pattern( + CallFunction( + [torch.stack, torch.cat], getitem_unbind, dim=Ignored(), _users=MULTIPLE + ), + pass_dict=construct_pattern_matcher_pass("unbind_stack_pass"), +) +@register_graph_pattern( + CallFunction( + [torch.stack, torch.cat], tensors=getitem_unbind, dim=Ignored(), _users=MULTIPLE + ), + pass_dict=construct_pattern_matcher_pass("unbind_stack_pass"), +) +def merge_unbind_stack(match: Match, unbind_input: torch.fx.Node, dim: int): + unbind_node = next(node for node in match.nodes if node.target is torch.unbind) + UnbindCatRemover().remove_unbind(match.graph, unbind_node) + + +getitem_split = ListOf( + CallFunction( + operator.getitem, + TorchSplit( + Ignored(), + KeywordArg("split_sections"), + ), + Ignored(), + _users=MULTIPLE, + ), + partial=True, +) + + +reshape_getitem_split = ListOf( + CallFunction( + torch.reshape, + CallFunction( + operator.getitem, + TorchSplit( + Ignored(), + KeywordArg("split_sections"), + ), + Ignored(), + _users=MULTIPLE, + ), + Arg(), + _users=MULTIPLE, + ), + partial=True, +) + + +@register_graph_pattern( + CallFunction( + [torch.stack, torch.cat], + tensors=getitem_split, + dim=Ignored(), + _users=MULTIPLE, + ), + pass_dict=construct_pattern_matcher_pass("split_cat_pass"), +) +@register_graph_pattern( + CallFunction( + [torch.stack, torch.cat], + getitem_split, + dim=Ignored(), + _users=MULTIPLE, + ), + pass_dict=construct_pattern_matcher_pass("split_cat_pass"), +) +@register_graph_pattern( + CallFunction( + [torch.stack, torch.cat], + getitem_split, + Ignored(), + _users=MULTIPLE, + ), + pass_dict=construct_pattern_matcher_pass("split_cat_pass"), +) +def simplify_split_cat(match: Match, split_sections: list[int], dim: int): + if not isinstance(split_sections, (list, tuple)): # Unnormalized split + return + split_node = next(node for node in match.nodes if node.target is torch.split) + + SplitCatSimplifier().simplify(match.graph, split_node, split_sections) + + +# noqa: W605 +# ############pattern to be optimized is######### + +# split_node(dim=1) +# / \ ... / \ +# getitem getitem getitem getitem -> user=1 +# \ / \ / +# cat (user=mul, dim=1) cat(user=mul, dim=1) +# | \ | \ + +# ################after transformation############# + +# split_node(dim=1) +# / ... \ +# getitem getitem +# | \ | \ + + +def has_same_parent_node(node: torch.fx.Node): + # the input nodes of the node should come from the same parent + prev_node = None + for getitem in node.args[0]: # type: ignore[union-attr] + if getitem.target != operator.getitem: # type: ignore[union-attr] + return False + if prev_node is None: + prev_node = getitem.args[0] # type: ignore[union-attr] + else: + if getitem.args[0] != prev_node: # type: ignore[union-attr] + return False + return True + + +def remove_zeros(split_sections: list[int]): + """ + Remove zeros from the list and get the index mapping dict from getitem + in split node to getitem in new split node + """ + new_split_sections, index_mapping = [], {} + idx = 0 + for i in range(len(split_sections)): + if split_sections[i] > 0: + new_split_sections.append(split_sections[i]) + index_mapping[i] = idx + idx += 1 + + return new_split_sections, index_mapping + + +def is_sorted_and_consecutive(arr: list[int]) -> bool: + # check if the array is sorted + if arr == sorted(arr): + # check if the differences between adjacent elements are all 1 + return all(x[1] - x[0] == 1 for x in itertools.pairwise(arr)) + else: + return False + + +def calculate_fused_tensor_size(split_node: torch.fx.Node, indices: list[int]) -> int: + """ + Calculate the fused tensor size in the indices + """ + fused_tensor_size = 0 + for i in range(len(split_node.args[1])): # type: ignore[arg-type] + if i in indices: + fused_tensor_size += split_node.args[1][i] # type: ignore[operator, assignment, index] + # pyrefly: ignore [bad-return] + return fused_tensor_size + + +@register_graph_pattern( + CallFunction( + torch.cat, + getitem_split, + dim=Ignored(), + _users=MULTIPLE, + ), + pass_dict=construct_pattern_matcher_pass("merge_getitem_cat_pass"), +) +def merge_getitem_cat(match: Match, split_sections: list[int], dim: int): + if not isinstance(split_sections, (list, tuple)): # Unnormalized split + return + graph = match.graph + split_node = next(node for node in match.nodes if node.target is torch.split) + split_input, _split_size, split_dim = _get_split_args_default(split_node) + # if the cat and split have different dims, return + # Find the next users (i.e. users after the getitem) + next_users = find_next_users(split_node) + # 'immutable_list' object does not support mutation. Create a new copy of it + split_sections = list(split_sections) + for cat_user in next_users: + if cat_user.target is torch.cat: + cat_dim = get_arg_value(cat_user, 1, "dim") + # check the all getitems in the cat_user from the same node + # check the input of the cat has all getitem from the split + # check all getitem only has one single user + if ( + split_dim != cat_dim + or not has_same_parent_node(cat_user) + or not all(len(arg.users) == 1 for arg in cat_user.args[0]) # type: ignore[union-attr] + ): + continue + # find the index of getitems to be cated/stacked + # type: ignore[union-attr] + indices = [arg.args[1] for arg in cat_user.args[0]] # type: ignore[union-attr] + # the getitems to be merged must be consecutive, otherwise + # returned sliced tensor could be wrong + if not is_sorted_and_consecutive(indices): # type: ignore[arg-type] + continue + # update the arg of cat user, only keep the first getitem + cat_user.update_arg(0, cat_user.args[0][0]) # type: ignore[index] + # calculate the fused tensor sizes in the indices + fused_tensor_size = 0 + for i in range(len(split_node.args[1])): # type: ignore[arg-type] + if i in indices: + fused_tensor_size += split_node.args[1][i] # type: ignore[operator, assignment, index] + # update the split sections + split_sections[indices[0]] = calculate_fused_tensor_size( # type: ignore[index] + split_node, + indices, # type: ignore[arg-type] + ) + # padding others with zeros to keep the same dict size + for i in indices[1:]: + split_sections[i] = 0 # type: ignore[index] + # remove all unused indexes in the split_node + new_split_sections, index_mapping = remove_zeros(split_sections) + with graph.inserting_after(split_node): + new_split_node = graph.call_function( + torch.split, + args=(split_input, split_sections), + kwargs={"dim": split_dim}, + ) + split_node.replace_all_uses_with(new_split_node) + new_split_node.meta.update(split_node.meta) + # remove all unused getitem nodes + to_remove = [cat_user] + # dictionary keys changed during iteration + new_split_getitem_nodes = list(new_split_node.users.keys()) + for getitem_node in new_split_getitem_nodes: + if getitem_node.args[1] in indices[1:]: + to_remove.append(getitem_node) + # update meta data of getitem + elif getitem_node.args[1] == indices[0]: + cat_user.replace_all_uses_with(getitem_node) + getitem_node.meta.update(cat_user.meta) + else: + # update getitem index for new split node + getitem_node.update_arg(1, index_mapping[getitem_node.args[1]]) + graph.erase_node(split_node) + for getitem_node in to_remove: + graph.erase_node(getitem_node) + # update the split sections of new split node + new_split_node.update_arg(1, new_split_sections) + split_node = new_split_node + split_sections = new_split_sections + + counters[backend]["merge_getitem_cat_pass"] += 1 + + +# ############pattern to be optimized is######### + +# split_node(dim=1) -> user=multiple +# / \ ... / \ +# getitem getitem getitem getitem -> user=multiple +# \ \ / \ +# other_op /cat(user=mul, dim=1) other_op +# | + +# ################after transformation############# + +# split_node(dim=1) -> -> user=multiple +# / \ ... / \ +# getitem getitem getitem getitem -> user=multiple +# \ \ / \ +# other_op + + +@register_graph_pattern( + CallFunction( + torch.cat, + getitem_split, + dim=Ignored(), + _users=MULTIPLE, + ), + pass_dict=construct_pattern_matcher_pass("mutate_cat_pass"), +) +def mutate_cat_node(match: Match, split_sections: list[int], dim: int): + if not isinstance(split_sections, (list, tuple)): # Unnormalized split + return + graph = match.graph + split_node = next(node for node in match.nodes if node.target is torch.split) + _split_input, _split_size, split_dim = _get_split_args_default(split_node) + # if the cat and split have different dims, return + # Find the next users (i.e. users after the getitem) + next_users = find_next_users(split_node) + for cat_user in next_users: + if cat_user.target is torch.cat: + cat_dim = get_arg_value(cat_user, 1, "dim") or 0 + # check that all getitems in the cat_user from the same node + # check the input of the cat has all getitem from the split + if split_dim != cat_dim or not has_same_parent_node(cat_user): + continue + # find the index of getitems to be cat + indices, idx_to_getitem = [], {} + for getitem in cat_user.args[0]: # type: ignore[union-attr] + indices.append(getitem.args[1]) # type: ignore[union-attr] + idx_to_getitem[getitem.args[1]] = getitem # type: ignore[union-attr] + # the getitems to be merged must be consecutive, otherwise + # returned sliced tensor could be wrong + if not is_sorted_and_consecutive(indices): # type: ignore[arg-type] + continue + # case 1: the cat uses all getitems from the split + if len(split_sections) == len(cat_user.args[0]): # type: ignore[arg-type] + # replace the users of the cat node to be the input of the split node + cat_user.replace_all_uses_with(split_node.args[0]) # type: ignore[arg-type] + # remove the cat node + graph.erase_node(cat_user) + counters[backend]["mutate_cat_pass"] += 1 + # case 2: the cat uses some getitems from the split + elif is_node_meta_valid(split_node.args[0]): # type: ignore[arg-type] + # check the split dim, and construct the slice tuple + start_fused_size = calculate_fused_tensor_size( + split_node, + list(range(indices[0])), # type: ignore[arg-type] + ) + end_fused_size = start_fused_size + calculate_fused_tensor_size( + split_node, + indices, # type: ignore[arg-type] + ) + slice_list = [] + for i in range(len(split_node.args[0].meta["example_value"].shape)): # type: ignore[union-attr] + if i != split_dim: + slice_list.append(slice(None, None, None)) + else: + slice_list.append(slice(start_fused_size, end_fused_size, None)) + with graph.inserting_after(split_node): + slice_node = graph.call_function( + operator.getitem, + args=(split_node.args[0], tuple(slice_list)), + ) + cat_user.replace_all_uses_with(slice_node) + slice_node.meta.update(cat_user.meta) + + # remove the cat node + graph.erase_node(cat_user) + counters[backend]["mutate_cat_pass"] += 1 + + +getitem_split_aten = ListOf( + CallFunction( + operator.getitem, + CallFunctionVarArgs([torch.ops.aten.split_with_sizes.default], users=MULTIPLE), + Ignored(), + _users=MULTIPLE, + ), + partial=True, +) + + +@register_graph_pattern( + CallFunctionVarArgs(torch.ops.aten.split.Tensor, users=MULTIPLE), + pass_dict=construct_pattern_matcher_pass("normalization_aten_pass"), +) +def normalize_split_default_aten(match: Match, *args, **kwargs): + split_node = match.nodes[0] + graph = match.graph + split_input, split_size, split_dim = _get_split_args_default(split_node) + if split_input is None or split_dim is None or split_size is None: + log.debug("couldn't find split args") + return + if not is_node_meta_valid(split_node): + log.debug("val absent for node: %s", split_node) + return + assert isinstance(split_node.meta["val"], (list, tuple)) + split_sections = [t.size()[split_dim] for t in split_node.meta["val"]] + if any(isinstance(section, torch.SymInt) for section in split_sections): + # TODO dynamic_shapes with assume_static_by_default=False fails while AOT Autograd tracing. + return + if split_dim < 0: # Normalize split dim + split_dim += split_input.meta["val"].dim() + # we also need to check the input of the split_node + # primals =torch.randn(4096, 300) + # split = torch.ops.aten.split.Tensor(primals, 320, 1) -> truncate to 300 automatically + # split_2 = torch.ops.aten.split_with_sizes.default(primals, [320], dim = 1) -> runtime error + split_input_size = split_input.meta["val"].shape[split_dim] + split_size = min(split_size, split_input_size) + split_section_list = [split_size] * (len(split_node.meta["val"])) + new_args = (split_input, split_section_list) + new_kwargs = {"dim": split_dim} + if ( + split_node.args == new_args + and split_node.kwargs == new_kwargs + and split_node.op == "call_function" + ): + return + + with graph.inserting_after(split_node): + new_split_node = graph.call_function( + torch.ops.aten.split_with_sizes.default, + args=new_args, + kwargs=new_kwargs, # type: ignore[arg-type] + ) + split_node.replace_all_uses_with(new_split_node) + new_split_node.meta.update(split_node.meta) + graph.erase_node(split_node) + counters[backend]["normalization_aten_pass"] += 1 + + +@register_graph_pattern( + CallFunctionVarArgs(torch.ops.aten.split_with_sizes.default, users=MULTIPLE), + pass_dict=construct_pattern_matcher_pass("normalization_aten_pass"), +) +def normalize_split_with_size_default_aten(match: Match, *args, **kwargs): + split_node = match.nodes[0] + graph = match.graph + split_input, split_sections, split_dim = _get_split_args_default(split_node) + if split_input is None or split_dim is None or split_sections is None: + log.debug("couldn't find split args") + return + if not is_node_meta_valid(split_node): + log.debug("val absent for node: %s", split_node) + return + if any(isinstance(section, torch.SymInt) for section in split_sections): + # TODO dynamic_shapes with assume_static_by_default=False fails while AOT Autograd tracing. + return + if split_dim < 0: # Normalize split dim + split_dim += split_input.meta["val"].dim() + + new_args = (split_input, split_sections) + new_kwargs = {"dim": split_dim} + if ( + split_node.args == new_args + and split_node.kwargs == new_kwargs + and split_node.op == "call_function" + ): + return + + with graph.inserting_after(split_node): + new_split_node = graph.call_function( + torch.ops.aten.split_with_sizes.default, + args=new_args, + kwargs=new_kwargs, # type: ignore[arg-type] + ) + split_node.replace_all_uses_with(new_split_node) + new_split_node.meta.update(split_node.meta) + graph.erase_node(split_node) + counters[backend]["normalization_aten_pass"] += 1 + + +@register_graph_pattern( + CallFunction( + torch.ops.aten.cat.default, + getitem_split_aten, + dim=Ignored(), + _users=MULTIPLE, + ), + pass_dict=construct_pattern_matcher_pass("split_cat_aten_pass"), +) +def merge_split_cat_aten(match: Match, *args, **kwargs): + graph = match.graph + split_node = match.nodes[0] + threshold_to_cat = torch._inductor.config.post_grad_fusion_options[ + "split_cat_aten_pass" + ].get("threshold_to_cat", 10) + # get the getitem nodes from the split node + getitem_nodes = list(split_node.users.keys()) + for cat_node in list(getitem_nodes[0].users.keys()): + cat_dim = get_arg_value(cat_node, 1, "dim") + cat_inputs = get_arg_value(cat_node, 0, "tensors") + try: + cat_input_len = len(cat_inputs) + except TypeError: + continue + if cat_input_len < threshold_to_cat: + continue + # check split node and cat node has same dim, and all getitem nodes have same parent node + parent_to_indices = defaultdict(list) # type: ignore[var-annotated] + parent_to_getitems = defaultdict(list) # type: ignore[var-annotated] + for cat_input in cat_inputs: + # skip all non-getitem cat input + if cat_input.target != operator.getitem: + continue + current_getitem_parent = cat_input.args[0] + split_dim = get_arg_value(current_getitem_parent, 2, "dim") + if split_dim != cat_dim: + break + getitem_idx = cat_input.args[1] + if ( + current_getitem_parent not in parent_to_indices + ) or getitem_idx != parent_to_indices[current_getitem_parent][-1][-1] + 1: + parent_to_indices[current_getitem_parent].append([getitem_idx]) + parent_to_getitems[current_getitem_parent].append([cat_input]) + else: + parent_to_getitems[current_getitem_parent][-1].append(cat_input) + parent_to_indices[current_getitem_parent][-1].append(getitem_idx) + + cat_inputs_list = list(cat_inputs) + update_cat_arg = [] + # iterate through the indices to construct the slice nodes + for parent, indices in parent_to_indices.items(): + for idx, indice in enumerate(indices): + start, end = indice[0], indice[-1] + split_sections = list(parent.args[1]) + input_of_current_getitem_parent = parent.args[0] + if len(indice) >= threshold_to_cat or len(indice) == len( + split_sections + ): + if len(indice) != len(split_sections): + # get the start and end slicing indices + slice_node = graph.call_function( + torch.ops.aten.slice.Tensor, + args=( + input_of_current_getitem_parent, + split_dim, # type: ignore[possibly-undefined] + sum(split_sections[:start]), + sum(split_sections[: end + 1]), + ), + ) + else: + slice_node = input_of_current_getitem_parent + # find the index in the cat_inputs_list given the getitem node + update_cat_arg.append( + ( + slice_node, + cat_inputs_list.index(parent_to_getitems[parent][idx][0]), + cat_inputs_list.index(parent_to_getitems[parent][idx][-1]), + ) + ) + + result = [] + i = 0 + for slice_tensor, start, end in update_cat_arg: + while i < start: + result.append(cat_inputs_list[i]) + i += 1 + result.append(slice_tensor) + i = end + 1 + while i < len(cat_inputs_list): + result.append(cat_inputs_list[i]) + i += 1 + + cat_node.update_arg(0, result) + for getitem_node in getitem_nodes: + if len(getitem_node.users) == 0: + graph.erase_node(getitem_node) + if len(split_node.users) == 0: + graph.erase_node(split_node) + counters[backend]["split_cat_aten_pass"] += 1 + + +@register_graph_pattern( + CallFunction( + torch.ops.aten.cat.default, + ListOf( + CallFunctionVarArgs(torch.ops.aten.select.int, users=MULTIPLE), + partial=True, + ), + dim=Ignored(), + _users=MULTIPLE, + ), + pass_dict=construct_pattern_matcher_pass("select_cat_aten_pass"), +) +def merge_select_cat_aten(match: Match, *args, **kwargs): + graph = match.graph + node = match.nodes[0] + node_input = get_arg_value(node, 0, "tensors") + # get the select nodes from the node + select_nodes = list(node_input.users.keys()) + for cat_node in list(node.users.keys()): + if cat_node.target is torch.ops.aten.cat.default: + cat_dim = get_arg_value(cat_node, 1, "dim") + cat_inputs = get_arg_value(cat_node, 0, "tensors") + # check all select nodes has same slice dim + if not all( + select_node.args[1] == select_nodes[0].args[1] + for select_node in select_nodes + ): + continue + # We only consider the case where selece slice dim and cat node has same dim + if select_nodes[0].args[1] != cat_dim: + continue + if not is_node_meta_valid(cat_node): + continue + # check the cat node has consecutive indices + indices = [select.args[2] for select in cat_node.args[0]] # type: ignore[union-attr] + if ( + not is_sorted_and_consecutive(indices) # type: ignore[arg-type] + or len(select_nodes) != len(cat_inputs) + ): + continue + # check all the select nodes can be merged to the cat node input + if len(indices) != select_nodes[0].args[0].meta["val"].shape[cat_dim]: # type: ignore[union-attr] + continue + # reshape the node input to be the same shape as the cat node + with graph.inserting_before(node): + view_node = graph.call_function( + torch.ops.aten.view.default, + args=(node_input, cat_node.meta["val"].shape), + ) + # replace the node input with the new node + cat_node.replace_all_uses_with(view_node) + view_node.meta.update(cat_node.meta) + # remove the cat node + graph.erase_node(cat_node) + for select_node in select_nodes: + if len(select_node.users) == 0: + graph.erase_node(select_node) + counters[backend]["select_cat_aten_pass"] += 1 + + +@register_graph_pattern( + CallFunctionVarArgs(torch.ops.aten.cat.default, users=MULTIPLE), + pass_dict=construct_pattern_matcher_pass("normalization_aten_pass"), +) +def normalize_cat_default_aten(match: Match, *args, **kwargs): + cat_node = match.nodes[0] + graph = match.graph + tensors = get_arg_value(cat_node, 0, "tensors") + cat_dim = get_arg_value(cat_node, 1, "dim") + if cat_dim is None: + cat_axis = cat_node.kwargs.get("axis") + if cat_axis is not None: + cat_dim = cat_axis + else: + cat_dim = 0 + if tensors is None or cat_dim is None: + log.debug("couldn't find cat args") + return + assert isinstance(tensors, (list, tuple)) + for tensor in itertools.chain([cat_node], tensors): + if "val" not in tensor.meta: + log.debug("val absent for node: %s", tensor) + return + + ndim = cat_node.meta["val"].dim() + + def is_empty_tensor(x: torch.fx.Node) -> bool: + # special case where torch.ops.aten.cat.default supports cat'ing with an empty tensor + x_shape = x.meta["val"].shape + return len(x_shape) == 1 and x_shape[0] == 0 + + assert all(ndim == x.meta["val"].dim() or is_empty_tensor(x) for x in tensors) + + if cat_dim < 0: # Normalize cat dim + cat_dim += ndim + + with graph.inserting_after(cat_node): + new_cat_node = graph.call_function( + torch.ops.aten.cat.default, + args=(tensors,), + kwargs={"dim": cat_dim}, + ) + cat_node.replace_all_uses_with(new_cat_node) + new_cat_node.meta.update(cat_node.meta) + graph.erase_node(cat_node) + counters[backend]["normalization_aten_pass"] += 1 + + +@register_graph_pattern( + CallFunction( + torch.ops.aten.cat, + ListOf(CallFunctionVarArgs(torch.ops.aten.unsqueeze)), + _users=MULTIPLE, + ), + pass_dict=construct_pattern_matcher_pass("unbind_stack_aten_pass"), +) +def merge_unbind_stack_aten(match: Match, *args, **kwargs): + node = match.nodes[-1] + graph = match.graph + # pyre-fixme[6] + unsqueeze_nodes = list(node.args[0]) # type: ignore[arg-type] + cat_dim = get_arg_value(node, 1, "dim") + # check the unsqueeze nodes come from the select nodes + if not all( + # pyrefly: ignore [bad-argument-type] + get_arg_value(unsqueeze_node, 0, "input").target is torch.ops.aten.select + for unsqueeze_node in unsqueeze_nodes + ): + return + select_nodes = [ + # pyrefly: ignore [bad-argument-type] + get_arg_value(unsqueeze_node, 0, "input") + for unsqueeze_node in unsqueeze_nodes + ] + parent_of_select_node = get_arg_value(select_nodes[0], 0, "input") + # check the target of select_nodes are the same + if not all( + select_node.target is torch.ops.aten.select for select_node in select_nodes + ): + return + # check the select nodes come from the same parent node + if not all( + get_arg_value(select_node, 0, "input") == parent_of_select_node + for select_node in select_nodes + ): + return + if len(unsqueeze_nodes) != len(select_nodes): + return + # check the select nodes have the same dim + if not all( + get_arg_value(select_node, 1, "dim") == cat_dim for select_node in select_nodes + ): + return + # check the select nodes have consecutive indices starting from 0 + if get_arg_value(select_nodes[0], 2, "index") != 0 or not is_sorted_and_consecutive( + [get_arg_value(select_node, 2, "index") for select_node in select_nodes] + ): + return + # check the users of parent of select node only from unsqueeze nodes that go to the cat node + # we simply check the number of users of the parent of select node + if len(parent_of_select_node.users.keys()) != len(node.args[0]): # type: ignore[arg-type] + return + node.replace_all_uses_with(parent_of_select_node) + graph.erase_node(node) + for unsqueeze_node in unsqueeze_nodes: + # pyrefly: ignore [bad-argument-type] + graph.erase_node(unsqueeze_node) + for select_node in select_nodes: + if len(select_node.users) == 0: + graph.erase_node(select_node) + counters[backend]["unbind_stack_aten_pass"] += 1 + + +def divide_into_consecutive_sublists(indices: list[int]) -> list[list[int]]: + n = len(indices) + if n <= 1: + return [indices] + + # Initialize the list of sublists + sublists = [] + + # Iterate over the indices + i = 0 + while i < n: + # Initialize the current sublist + sublist = [indices[i]] + + # Iterate over the remaining indices + j = i + 1 + while j < n and indices[j] == indices[j - 1] + 1: + # Add the next index to the current sublist + sublist.append(indices[j]) + j += 1 + + # Add the current sublist to the list of sublists + sublists.append(sublist) + # Move to the next index + i = j + + return sublists + + +def update_args_from_split_getitem( + graph: torch.fx.Graph, + node: torch.fx.Node, + getitem_indices: list[int], + parents_seen: list[torch.fx.Node], + new_cat_args: list[torch.fx.Node], + new_cat_args_meta: list[torch.fx.Node], + idx_to_getitems: dict[int, torch.fx.Node], + threshold_to_cat: int = 2, +): + split_input, split_size, split_dim = _get_split_args_default(parents_seen[-1]) + # case 1: the number of getitems is the same as the split size, eliminate the split + if len(split_size) == len(getitem_indices) and is_sorted_and_consecutive( + getitem_indices + ): + # we can merge the getitems from the previous parent + new_cat_args.append(split_input) + new_cat_args_meta.append(split_input.meta["example_value"]) + else: + if len(getitem_indices) > 0: + # case 2: the number of getitems is smaller than the split size but larger than the threshold, and + # the indices of getitems are not all consecutive, we need to divide the indices into multiple groups + geitem_indices_sublist = divide_into_consecutive_sublists(getitem_indices) + for sublist in geitem_indices_sublist: + if len(sublist) >= threshold_to_cat: + # case 2: the number of getitems is smaller than the split size but larger than the threshold + # we need to slice the input of parent + start_fused_size = sum(split_size[: sublist[0]]) + end_fused_size = sum(split_size[: sublist[-1] + 1]) + slice_list = [] + for i in range(len(split_input.meta["example_value"].shape)): # type: ignore[union-attr] + if i != split_dim: + slice_list.append(slice(None, None, None)) + else: + slice_list.append( + slice(start_fused_size, end_fused_size, None) + ) + with graph.inserting_after(node): + slice_node = graph.call_function( + operator.getitem, + args=(split_input, tuple(slice_list)), + ) + slice_node.meta["example_value"] = split_input.meta[ + "example_value" + ][tuple(slice_list)] + new_cat_args.append(slice_node) + new_cat_args_meta.append(slice_node.meta["example_value"]) + else: + # case 3: the number of getitems is smaller than the threshold, no merge is done + # get the getitems based on the indexes + for i in sublist: + new_cat_args.append(idx_to_getitems[i]) + new_cat_args_meta.append( + idx_to_getitems[i].meta["example_value"] + ) + + +def reshape_cat_node( + graph: torch.fx.Graph, + cat_node: torch.fx.Node, + unbind_input: torch.fx.Node, + cat_dim: int, + unbind_dim: int, + cat_shape: torch.Size, +) -> torch.fx.Node: + if cat_dim != unbind_dim: + # construct the permute node args, which has the same shape as the slice node + # then it has the same dim as the unbind_input, i.e., shape of cat + 1 + with graph.inserting_after(cat_node): + permute_list = list(range(len(cat_shape) + 1)) + permute_list[unbind_dim], permute_list[cat_dim] = ( + permute_list[cat_dim], + permute_list[unbind_dim], + ) + permute_node = graph.call_function( + torch.permute, + args=(unbind_input, permute_list), + ) + permute_node.meta["example_value"] = torch.permute( + unbind_input.meta["example_value"], permute_list + ) # type: ignore[arg-type] + else: + permute_node = unbind_input + with graph.inserting_after(permute_node): + reshape_node = graph.call_function( + torch.reshape, args=(permute_node, tuple(cat_shape)) + ) + reshape_node.meta["example_value"] = torch.reshape( + permute_node.meta["example_value"], tuple(cat_shape) + ) # type: ignore[arg-type] + return reshape_node + + +def update_args_from_unbind_getitem( + graph: torch.fx.Graph, + node: torch.fx.Node, # cat or stack node + getitem_indices: list[int], + parents_seen: list[torch.fx.Node], + new_cat_args: list[torch.fx.Node], + new_cat_args_meta: list[torch.fx.Node], + idx_to_getitems: dict[int, torch.fx.Node], + threshold_to_cat: int = 2, +): + unbind_input = get_arg_value(parents_seen[-1], 0, "input") # split or unbind input + unbind_dim = get_arg_value(parents_seen[-1], 1, "dim") # split or unbind dim + cat_dim = get_arg_value(node, 1, "dim") # cat or stack dim + # case 1: the number of getitems is the same as the split size, eliminate the split + size = list(unbind_input.meta["example_value"].shape)[unbind_dim] + if size == len(getitem_indices): + cat_shape = torch.cat( + [idx_to_getitems[i].meta["example_value"] for i in getitem_indices], + dim=cat_dim, + ).shape + # we can merge the getitems from the previous parent + reshape_node = reshape_cat_node( + graph, node, unbind_input, cat_dim, unbind_dim, cat_shape + ) + new_cat_args.append(reshape_node) + new_cat_args_meta.append(reshape_node.meta["example_value"]) + elif len(getitem_indices) >= threshold_to_cat and is_sorted_and_consecutive( + getitem_indices + ): + # case 2: the number of getitems is smaller than the split size but larger than the threshold + # we need to slice the input of parent + cat_shape = torch.cat( + [idx_to_getitems[i].meta["example_value"] for i in getitem_indices], + dim=cat_dim, + ).shape + slice_list = [] + for i in range(len(cat_shape) + 1): + if i != unbind_dim: + slice_list.append(slice(None, None, None)) # start, end, step + else: + slice_list.append( + slice(getitem_indices[0], getitem_indices[-1] + 1, None) + ) + with graph.inserting_after(node): + slice_node = graph.call_function( + operator.getitem, + args=(unbind_input, tuple(slice_list)), + ) + slice_node.meta["example_value"] = torch.narrow( + unbind_input.meta["example_value"], + unbind_dim, + getitem_indices[0], + getitem_indices[-1] - getitem_indices[0] + 1, + ) + reshape_node = reshape_cat_node( + graph, node, slice_node, cat_dim, unbind_dim, cat_shape + ) + new_cat_args.append(reshape_node) + new_cat_args_meta.append(reshape_node.meta["example_value"]) + else: + # case 3: the number of getitems is smaller than the threshold, no merge is done + # get the getitems based on the indexes + for i in getitem_indices: + new_cat_args.append(idx_to_getitems[i]) + new_cat_args_meta.append(idx_to_getitems[i].meta["example_value"]) + + +def construct_cat_args( + graph: torch.fx.Graph, + cat_or_stack_node: torch.fx.Node, + inputs: list[torch.fx.Node], + split_or_unbind_node: torch.fx.Node, + threshold_to_cat: int = 2, + run_update_func: Callable = update_args_from_split_getitem, # type: ignore[type-arg] +) -> tuple[list[torch.fx.Node], list[torch.Tensor]]: + new_cat_args, parents_seen, getitem_indices, idx_to_getitems = [], [], [], {} # type: ignore[var-annotated] + new_cat_args_meta = [] # type: ignore[var-annotated] + for input in inputs: + if input.target != operator.getitem: + # update the last arg based on getitem_indices and parents_seens + if len(parents_seen) > 0: + run_update_func( # type: ignore[arg-type, union-attr] + graph, + cat_or_stack_node, + getitem_indices, + parents_seen, + new_cat_args, + new_cat_args_meta, + idx_to_getitems, # type: ignore[arg-type, union-attr] + threshold_to_cat, + ) + new_cat_args.append(input) + new_cat_args_meta.append(input.meta["example_value"]) + # reset the indices array + getitem_indices, idx_to_getitems = [], {} + else: + # get the parent node of the getitem input + parent, idx = input.args[0], input.args[1] # type: ignore[union-attr] + if parent.target != split_or_unbind_node.target: # type: ignore[union-attr] + new_cat_args.append(input) + new_cat_args_meta.append(input.meta["example_value"]) + continue + # cannot use parents_seen to check since the first item could be non getitem node + if len(parents_seen) == 0: + parents_seen.append(parent) + idx_to_getitems[idx] = input + getitem_indices.append(idx) + # case: we only have one getitem input, and it is in the last position + if input == inputs[-1]: + new_cat_args.append(input) + new_cat_args_meta.append(input.meta["example_value"]) + continue + # if it is the last input in the tensors, we also check if it can be optimized + if parent != parents_seen[-1] or input == inputs[-1]: + if input == inputs[-1]: + getitem_indices.append(idx) + idx_to_getitems[idx] = input + run_update_func( # type: ignore[arg-type, union-attr] + graph, + cat_or_stack_node, + getitem_indices, + parents_seen, + new_cat_args, + new_cat_args_meta, + idx_to_getitems, # type: ignore[arg-type, union-attr] + threshold_to_cat, + ) + # reset the indices array for the next parent + # remember to add the last element since it is the first + # item in this round of parent + # add the parent to the list of seen parents + parents_seen.append(parent) + getitem_indices, idx_to_getitems = [idx], {idx: input} + else: + getitem_indices.append(idx) + idx_to_getitems[idx] = input + return new_cat_args, new_cat_args_meta + + +def remove_split_unbind_children(graph: torch.fx.Graph, inputs: list[torch.fx.Node]): + nodes = OrderedSet[Any]() + for input in inputs: + if input.target is operator.getitem: + nodes.add(input.args[0]) # type: ignore[union-attr] + if len(input.users.keys()) == 0: + graph.erase_node(input) + # check the split node to remove if it has no users + for node in nodes: + if len(node.users.keys()) == 0: # type: ignore[union-attr] + graph.erase_node(node) # type: ignore[arg-type] + + +# ############pattern to be optimized is######### + +# split_node(dim=1) -> user=multiple +# / \ ... / \ +# other inputs getitem getitem getitem -> user=multiple +# \ / \ +# cat(user=mul, dim=1) other_op +# | + +# ################after transformation############# + +# split_node(dim=1) other inputs -> -> user=multiple +# / \ +# cat (user=mul, dim=1, split_node) + + +@register_graph_pattern( + CallFunction( + torch.cat, + getitem_split, + dim=Ignored(), + _users=MULTIPLE, + ), + pass_dict=construct_pattern_matcher_pass("split_cat_to_slices_pass"), +) +def split_cat_to_slices(match: Match, split_sections: list[int], dim: int): + if not isinstance(split_sections, (list, tuple)): # Unnormalized split + return + split_nodes = [node for node in match.nodes if node.target is torch.split] + if split_nodes: + split_node = next(node for node in split_nodes) + else: + # Handle the case where there are no nodes with a target of torch.split + return + split_dim = get_arg_value(split_node, 2, "dim") or 0 + graph = match.graph + threshold_to_cat = torch._inductor.config.pre_grad_fusion_options[ + "split_cat_to_slices_pass" + ].get("threshold_to_cat", 10) + # get the cat_node and check its inputs and meta data + next_users = find_next_users(split_node) + for cat_node in next_users: + if cat_node.target != torch.cat or not is_node_meta_valid(cat_node): + continue + cat_inputs = get_arg_value(cat_node, 0, "tensors") # type: ignore[union-attr] + new_cat_args, _ = construct_cat_args( + graph, + cat_node, + cat_inputs, + split_node, + threshold_to_cat, + update_args_from_split_getitem, + ) + # At least one node would be in the returned new_cat_args + # case 1: if new cat args has length 1, we can remove the cat node + if len(new_cat_args) == 1: + cat_node.replace_all_uses_with(new_cat_args[0]) + # remove inputs of cat_node if they have no users + cat_inputs = cat_node.args[0] # type: ignore[union-attr] + graph.erase_node(cat_node) + remove_split_unbind_children(graph, cat_inputs) # type: ignore[arg-type] + counters[backend]["split_cat_to_slices_pass"] += 1 + continue + if len(new_cat_args) > 1 and len(new_cat_args) < len(cat_inputs): + new_args = (new_cat_args,) + with graph.inserting_after(cat_node): + new_cat_node = graph.call_function( + torch.cat, + args=new_args, + # split and cat have the same dim + kwargs={"dim": split_dim}, + ) + cat_node.replace_all_uses_with(new_cat_node) + new_cat_node.meta.update(cat_node.meta) + # remove the cat node + graph.erase_node(cat_node) + remove_split_unbind_children(graph, cat_inputs) + counters[backend]["split_cat_to_slices_pass"] += 1 + + +# ############pattern to be optimized is######### + +# unbind(dim=0) -> user=multiple +# / \ ... / \ +# getitem getitem getitem getitem -> user=multiple +# \ / \ +# cat(user=mul, dim=1) other_op +# | + +# ################after transformation############# + +# input_of_unbind +# | \ +# slice +# | +# view +# | + + +@register_graph_pattern( + CallFunction( + torch.cat, + getitem_unbind, + dim=Ignored(), + _users=MULTIPLE, + ), + pass_dict=construct_pattern_matcher_pass("unbind_cat_to_view_pass"), +) +def unbind_cat_to_view(match: Match, unbind_input: torch.fx.Node, dim: int): + unbind_node = next(node for node in match.nodes if node.target is torch.unbind) + graph = match.graph + # get the cat_node and check its inputs and meta data + next_users = find_next_users(unbind_node) + threshold_to_cat = torch._inductor.config.pre_grad_fusion_options[ + "unbind_cat_to_view_pass" + ].get("threshold_to_cat", 10) + # get the cat_node and check its inputs and meta data + for cat_node in next_users: + if cat_node.target != torch.cat or not is_node_meta_valid(cat_node): + continue + inputs = get_arg_value(cat_node, 0, "tensors") # type: ignore[union-attr] + new_cat_args, new_cat_args_meta = construct_cat_args( + graph, + cat_node, + inputs, + unbind_node, + threshold_to_cat, + update_args_from_unbind_getitem, + ) + # get the view shape + # At least one node would be in the returned new_cat_args + # case 1: only one node in the new cat args, don't need to cat + if len(new_cat_args) == 1: + cat_node.replace_all_uses_with(new_cat_args[0]) + # remove inputs of cat_node if they have no users + cat_inputs = cat_node.args[0] # type: ignore[union-attr] + graph.erase_node(cat_node) + remove_split_unbind_children(graph, cat_inputs) # type: ignore[arg-type] + counters[backend]["unbind_cat_to_view_pass"] += 1 + continue + if len(new_cat_args) > 1 and len(new_cat_args) < len(inputs): + # get the view shape + cat_dim = get_arg_value(cat_node, 1, "dim") + with graph.inserting_after(cat_node): + new_cat_node = graph.call_function( + torch.cat, + args=(new_cat_args,), + kwargs={"dim": cat_dim}, + ) + new_cat_node.meta["example_value"] = torch.cat( + new_cat_args_meta, dim=cat_dim + ) # type: ignore[arg-type] + cat_node.replace_all_uses_with(new_cat_node) + new_cat_node.meta.update(cat_node.meta) + # remove inputs of cat_node if they have no users + cat_inputs = cat_node.args[0] # type: ignore[union-attr] + graph.erase_node(cat_node) + remove_split_unbind_children(graph, cat_inputs) # type: ignore[arg-type] + counters[backend]["unbind_cat_to_view_pass"] += 1 + + +def reshape_cat_node_to_stack( + graph: torch.fx.Graph, + cat_node: torch.fx.Node, + stack_node: torch.fx.Node, + split_or_unbind_dim: int, +) -> None: + # reshape the cat node to the stack node shape + stack_shape = stack_node.meta["example_value"].shape + stack_dim = _get_dim(stack_node) + if stack_dim != split_or_unbind_dim: + # case 1: the stack dim is not the same as the split dim + # we need to reshape the split input before we do the reshape + reshape_list = list(stack_shape) + reshape_list[stack_dim], reshape_list[split_or_unbind_dim] = ( + reshape_list[split_or_unbind_dim], + reshape_list[stack_dim], + ) + reshape_node = graph.call_function( + torch.reshape, + args=(cat_node, tuple(reshape_list)), + ) + reshape_node.meta["example_value"] = torch.reshape( + cat_node.meta["example_value"], + tuple(reshape_list), # pyrefly: ignore [bad-argument-type] + ) + permute_list = list(range(len(stack_shape))) + permute_list[stack_dim], permute_list[split_or_unbind_dim] = ( + permute_list[split_or_unbind_dim], + permute_list[stack_dim], + ) + permute_node = graph.call_function( + torch.permute, + args=(reshape_node, permute_list), + ) + permute_node.meta["example_value"] = torch.permute( + reshape_node.meta["example_value"], permute_list + ) + else: + # case 2: the stack dim is the same as the split dim + # we can directly reshape the split input + permute_node = cat_node + reshape_node = graph.call_function( + torch.Tensor.view, + args=(permute_node, *stack_shape), # type: ignore[arg-type] + ) + stack_node.replace_all_uses_with(reshape_node) + reshape_node.meta.update(stack_node.meta) + stack_inputs = stack_node.args[0] # type: ignore[union-attr] + # remove stack node + graph.erase_node(stack_node) + # check the input of stack node, and remove nodes that have no users + remove_split_unbind_children(graph, stack_inputs) # type: ignore[arg-type] + + +def convert_reshape_cat_arg_to_stack( + graph: torch.fx.Graph, + cat_node: torch.fx.Node, + stack_node: torch.fx.Node, + stack_node_shape: torch.Size, + stack_dim: int, + split_dim: int, +) -> torch.fx.Node: + # reshape the cat node to the stack node shape + cat_shape = cat_node.meta["example_value"].shape + if stack_dim != split_dim: + permute_list = list(range(len(cat_shape))) + permute_list[stack_dim], permute_list[split_dim] = ( + permute_list[split_dim], + permute_list[stack_dim], + ) + permute_node = graph.call_function( + torch.permute, + args=(cat_node, permute_list), + ) + permute_node.meta["example_value"] = torch.permute( + cat_node.meta["example_value"], permute_list + ) + else: + permute_node = cat_node + reshape_node = graph.call_function( + torch.Tensor.view, + args=(permute_node, tuple(stack_node_shape)), # type: ignore[arg-type] + ) + reshape_node.meta["example_value"] = torch.Tensor.view( + permute_node.meta["example_value"], + tuple(stack_node_shape), # type: ignore[arg-type] + ) + return reshape_node + + +# ############pattern to be optimized is######### +# | | +# split split (dim=1) +# / \ / \ +# getitem ... getitem other ops +# \ | / / +# stack(user=mul, dim=1 or 2) -> can be different dim +# | + +# ################after transformation############# + +# / \ ... / \ +# getitem getitem getitem getitem -> user=multiple +# \ / +# cat(user=mul, dim=1) cat_other_opts +# \ / +# cat +# | +# view +# | + + +@register_graph_pattern( + CallFunction( + torch.stack, + getitem_split, + dim=Ignored(), + _users=MULTIPLE, + ), + pass_dict=construct_pattern_matcher_pass("split_stack_to_cats_pass"), +) +def split_stack_to_cats(match: Match, split_sections: list[int], dim: int): + if not isinstance(split_sections, (list, tuple)): # Unnormalized split + return + split_node = next(node for node in match.nodes if node.target is torch.split) + split_dim = get_arg_value(split_node, 2, "dim") or 0 + graph = match.graph + threshold_to_cat = torch._inductor.config.pre_grad_fusion_options[ + "split_stack_to_cats_pass" + ].get("threshold_to_cat", 10) + # get the stack_node and check its inputs and meta data + next_users = find_next_users(split_node) + for stack_node in next_users: + if stack_node.target != torch.stack or not is_node_meta_valid(stack_node): + continue + inputs = get_arg_value(stack_node, 0, "tensors") # type: ignore[union-attr] + new_cat_args, new_cat_args_meta = construct_cat_args( + graph, + stack_node, + inputs, + split_node, + threshold_to_cat, + update_args_from_split_getitem, + ) + # At least one node would be in the returned new_cat_args + # case 1: only one node in the new cat args, don't need to cat + if len(new_cat_args) == 1: + reshape_cat_node_to_stack(graph, new_cat_args[0], stack_node, split_dim) + counters[backend]["split_stack_to_cats_pass"] += 1 + continue + if len(new_cat_args) > 1 and len(new_cat_args) < len(inputs): + with graph.inserting_after(stack_node): + cat_node = graph.call_function( + torch.cat, + args=(new_cat_args,), + kwargs={"dim": split_dim}, + ) + cat_node.meta["example_value"] = torch.cat( # type: ignore[arg-type] + new_cat_args_meta, dim=split_dim + ) + reshape_cat_node_to_stack(graph, cat_node, stack_node, split_dim) + counters[backend]["split_stack_to_cats_pass"] += 1 + + +# ############pattern to be optimized is######### + +# unbind(dim=1) -> user=multiple +# \ ... / \ +# others getitem getitem getitem -> user=multiple +# \ \ / \ +# stack(user=mul, dim=1) other_op +# | + +# ################after transformation############# + +# input_of_unbind +# | \ +# slice +# | +# view others +# | / +# stack +# | + + +@register_graph_pattern( + CallFunction( + torch.stack, + getitem_unbind, + dim=Ignored(), + _users=MULTIPLE, + ), + pass_dict=construct_pattern_matcher_pass("unbind_stack_to_slices_pass"), +) +def unbind_stack_to_slices(match: Match, unbind_input: torch.fx.Node, dim: int): + unbind_node = next(node for node in match.nodes if node.target is torch.unbind) + graph = match.graph + # get the cat_node and check its inputs and meta data + next_users = find_next_users(unbind_node) + threshold_to_cat = torch._inductor.config.pre_grad_fusion_options[ + "unbind_stack_to_slices_pass" + ].get("threshold_to_cat", 10) + # get the cat_node and check its inputs and meta data + for stack_node in next_users: + if stack_node.target != torch.stack or not is_node_meta_valid(stack_node): + continue + inputs = get_arg_value(stack_node, 0, "tensors") # type: ignore[union-attr] + new_cat_args, new_cat_args_meta = construct_cat_args( + graph, + stack_node, + inputs, + unbind_node, + threshold_to_cat, + update_args_from_unbind_getitem, + ) + unbind_dim = get_arg_value(unbind_node, 1, "dim") or 0 + # At least one node would be in the returned new_cat_args + # case 1: only one node in the new cat args, don't need to cat + if len(new_cat_args) == 1: + reshape_cat_node_to_stack(graph, new_cat_args[0], stack_node, unbind_dim) + counters[backend]["unbind_stack_to_slices_pass"] += 1 + continue + if len(new_cat_args) > 1 and len(new_cat_args) < len(inputs): + # get the view shape + cat_dim = get_arg_value(stack_node, 1, "dim") + with graph.inserting_after(stack_node): + new_cat_node = graph.call_function( + torch.cat, + args=(new_cat_args,), + kwargs={"dim": cat_dim}, + ) + new_cat_node.meta["example_value"] = torch.cat( + new_cat_args_meta, dim=cat_dim + ) + reshape_cat_node_to_stack(graph, new_cat_node, stack_node, unbind_dim) + counters[backend]["unbind_stack_to_slices_pass"] += 1 + + +# ############pattern to be optimized is######### +# input +# | +# split(dim=1) -> user=multiple +# \ \ +# others getitem getitem +# \ \ / +# reshape reshape reshape other_op +# \ \ / / +# stack(user=mul, dim=0) +# | + +# ################after transformation############# +# input +# | +# permute +# | +# reshape others +# | / +# cat (dim=0) +# | + + +def get_view_shape_list(cat_arg: torch.fx.Node, stack_dim: int) -> list[int]: + # cat_arg must be the split input + view_shape_list = [] + for user in cat_arg.users: + if user.target is torch.split: + for getitem in user.users: + if getitem.target is operator.getitem: + reshape_user = [ + user for user in getitem.users if user.target is torch.reshape + ] + if len(reshape_user) > 0: + view_shape_list = list( + reshape_user[0] + .meta["example_value"] + .unsqueeze(stack_dim) + .shape + ) + view_shape_list[stack_dim] = -1 + return view_shape_list + return view_shape_list + + +@register_graph_pattern( + CallFunction( + torch.stack, + reshape_getitem_split, + dim=Ignored(), + _users=MULTIPLE, + ), + pass_dict=construct_pattern_matcher_pass("move_reshape_out_of_split_stack_pass"), +) +def move_reshape_out_of_split_stack(match: Match, *args, **kwargs): + split_node = next(node for node in match.nodes if node.target is torch.split) + split_dim = _get_dim(split_node) + split_users = list(split_node.users.keys()) + stack_nodes = [node for node in match.nodes if node.target is torch.stack] + graph = match.graph + threshold_to_cat = torch._inductor.config.pre_grad_fusion_options[ + "move_reshape_out_of_split_stack_pass" + ].get("threshold_to_cat", 10) + for stack_node in stack_nodes: + if not is_node_meta_valid(stack_node): + log.debug("example value absent for node: %s", stack_node) + continue + stack_dim = _get_dim(stack_node) + stack_inputs = get_arg_value(stack_node, 0, "tensors") # type: ignore[union-attr] + inputs = [] + for stack_input in stack_inputs: + if stack_input.target != torch.reshape: + inputs.append(stack_input) + else: + inputs.append(stack_input.args[0]) # type: ignore[union-attr] + new_cat_args, _new_cat_args_meta = construct_cat_args( + graph, + stack_node, + inputs, + split_node, + threshold_to_cat, + update_args_from_split_getitem, + ) + # At least one node would be in the returned new_cat_args + # case 1: only one node in the new cat args, don't need to cat + if len(new_cat_args) == 1: + reshape_node = convert_reshape_cat_arg_to_stack( + graph, + new_cat_args[0], + stack_node, + stack_node.meta["example_value"].shape, + stack_dim, + split_dim, + ) + stack_node.replace_all_uses_with(reshape_node) + # remove stack node + graph.erase_node(stack_node) + # check the input of stack node, and remove nodes that have no users + remove_split_unbind_children(graph, stack_inputs) # type: ignore[arg-type] + remove_split_unbind_children(graph, split_users) # type: ignore[arg-type] + counters[backend]["move_reshape_out_of_split_stack_pass"] += 1 + continue + if len(new_cat_args) > 1 and len(new_cat_args) < len(inputs): + # decompose the cat args into multiple stack nodes, i.e., we stack + # all the nodes exist in the stack inputs and reshape the rest followed by a cat + stack_node_input, stack_node_input_meta, cat_inputs = [], [], [] # type: ignore[var-annotated] + for cat_arg in new_cat_args: + if cat_arg not in stack_inputs: + if len(stack_node_input) > 0: + with graph.inserting_after(stack_node): + decomposed_stack_node = graph.call_function( + torch.stack, + args=(stack_node_input,), + kwargs={"dim": stack_dim}, + ) + decomposed_stack_node.meta["example_value"] = torch.stack( + stack_node_input_meta, dim=stack_dim + ) + cat_inputs.append(decomposed_stack_node) + # cat_arg must be the split input + view_shape_list = get_view_shape_list(cat_arg, stack_dim) + stack_node_shape = torch.reshape( + cat_arg.meta["example_value"], tuple(view_shape_list) + ).shape # type: ignore[union-attr] + cat_inputs.append( + convert_reshape_cat_arg_to_stack( + graph, + cat_arg, + stack_node, + stack_node_shape, + stack_dim, + split_dim, + ) + ) + stack_node_input, stack_node_input_meta = [], [] + else: + stack_node_input.append(cat_arg) + stack_node_input_meta.append(cat_arg.meta["example_value"]) + + if len(stack_node_input) > 0: + with graph.inserting_after(stack_node): + decomposed_stack_node = graph.call_function( + torch.stack, + args=(stack_node_input,), + kwargs={"dim": stack_dim}, + ) + decomposed_stack_node.meta["example_value"] = torch.stack( + stack_node_input_meta, dim=stack_dim + ) + cat_inputs.append(decomposed_stack_node) + + with graph.inserting_after(stack_node): + cat_node = graph.call_function( + torch.cat, + args=(cat_inputs,), + kwargs={"dim": stack_dim}, + ) + stack_node.replace_all_uses_with(cat_node) + cat_node.meta.update(stack_node.meta) + graph.erase_node(stack_node) + remove_split_unbind_children(graph, stack_inputs) # type: ignore[arg-type] + remove_split_unbind_children(graph, split_users) # type: ignore[arg-type] + counters[backend]["move_reshape_out_of_split_stack_pass"] += 1 + + +view_getitem_split_aten = ListOf( + CallFunction( + [torch.ops.aten.reshape.default], + CallFunction( + operator.getitem, + CallFunctionVarArgs( + torch.ops.aten.split_with_sizes.default, users=MULTIPLE + ), + Ignored(), + _users=MULTIPLE, + ), + Arg(), + _users=MULTIPLE, + ), + partial=True, +) + + +@register_graph_pattern( + CallFunction( + torch.ops.aten.cat.default, + view_getitem_split_aten, + dim=Ignored(), + _users=MULTIPLE, + ), + pass_dict=construct_pattern_matcher_pass("move_view_after_cat_aten_pass"), +) +def move_view_after_cat(match: Match, *args, **kwargs): + split_node = next( + node + for node in match.nodes + if node.target is torch.ops.aten.split_with_sizes.default + ) + split_input, split_section, split_dim = _get_split_args_default(split_node) + split_users = list(split_node.users.keys()) + getitem_indices = [ + getitem.args[1] for getitem in split_users if getitem.target is operator.getitem + ] + if not is_sorted_and_consecutive(getitem_indices): # type: ignore[arg-type] + return + cat_nodes = [ + node for node in match.nodes if node.target is torch.ops.aten.cat.default + ] + graph = match.graph + for cat_node in cat_nodes: + if not is_node_meta_valid(cat_node): + log.debug("example value absent for node: %s", cat_node) + continue + cat_dim = _get_dim(cat_node) + cat_inputs = get_arg_value(cat_node, 0, "tensors") # type: ignore[union-attr] + # we only consider the following special case + if len(cat_inputs) != len(split_section): + continue + # check if the cat inputs are all the view nodes + if not all( + view_node.target is torch.ops.aten.reshape.default + for view_node in cat_inputs + ): + continue + # check if the view nodes are all from getitem nodes + if not all( + view_node.args[0].target is operator.getitem for view_node in cat_inputs + ): + continue + view_indices = [view.args[0].args[1] for view in cat_inputs] + if not is_sorted_and_consecutive(view_indices): # type: ignore[arg-type] + continue + if cat_dim != split_dim: + # construct permute node + permute_list = list(range(len(cat_node.meta["val"].shape) + 1)) + permute_list[split_dim], permute_list[cat_dim] = ( + permute_list[cat_dim], + permute_list[split_dim], + ) + permute_node = graph.call_function( + torch.ops.aten.permute.default, + args=(split_input, permute_list), + ) + else: + permute_node = split_input + + with graph.inserting_before(cat_node): + view_node = graph.call_function( + torch.ops.aten.reshape.default, + args=(permute_node, list(cat_node.meta["val"].shape)), + ) + cat_node.replace_all_uses_with(view_node) + view_node.meta.update(cat_node.meta) + graph.erase_node(cat_node) + counters[backend]["move_view_after_cat_aten_pass"] += 1 + + +def match_einsum_strings(s: str) -> bool: + """ + This function takes a string s as input, where s is in the format "3 letter string, + 4 letter string -> 3 letter string". + It checks if the strings match the rule and returns True if they do, False otherwise. + + The rule is: + - The three strings have the same first two characters. + - The first two strings have the same third character. + - The second and third strings have the same last character. + """ + + # Split the input string into parts + parts = s.replace("->", ",").split(",") + + # Strip leading/trailing whitespaces from each part + parts = [part.strip() for part in parts] + + # Check if we have exactly three parts + if len(parts) != 3: + return False + + # Extract the strings + s1, s2, s3 = parts + + # Check if the strings have the correct lengths + if len(s1) != 3 or len(s2) != 4 or len(s3) != 3: + return False + + # Check the rule + return s1[:2] == s2[:2] == s3[:2] and s1[2] == s2[2] and s2[3] == s3[2] + + +@register_graph_pattern( + CallFunctionVarArgs(torch.functional.einsum, users=MULTIPLE), + pass_dict=construct_pattern_matcher_pass("einsum_to_pointwise_pass"), +) +def replace_einsum_to_pointwise(match: Match, *args, **kwargs): + def repl(input, weights): + return (input.unsqueeze(-1) * weights).sum(-2) + + def should_replace_einsum(einsum_node) -> bool: + equation = get_arg_value(einsum_node, 0) + users = einsum_node.users.keys() + # for now, we only consider the case of two operands + return ( + len(einsum_node.args) == 3 + and is_node_meta_valid(input) + and is_node_meta_valid(weights) + and any( + user.target == "add" or user.target is operator.add for user in users + ) + and match_einsum_strings(equation) + ) + + einsum_node = match.nodes[0] + input, weights = get_arg_value(einsum_node, 1), get_arg_value(einsum_node, 2) + if should_replace_einsum(einsum_node): + # pyrefly: ignore [bad-argument-type] + match.replace_by_example(repl, [input, weights]) + counters[backend]["einsum_to_pointwise_pass"] += 1 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/spmd_check.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/spmd_check.py new file mode 100644 index 0000000000000000000000000000000000000000..b51004107a10723f996fa5b78fca7ef6df0ddbda --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_passes/spmd_check.py @@ -0,0 +1,228 @@ +"""SPMD graph verification for overlap scheduling. + +Verifies all ranks have identical FX graph structure before collective +reordering passes. Non-SPMD graphs cause NCCL collective ordering +mismatches and hangs. +""" + +import hashlib +import logging +from collections import Counter + +import torch +from torch._inductor import config +from torch._logging import trace_structured + + +log = logging.getLogger(__name__) + + +def _compute_hash(gm: torch.fx.GraphModule) -> int | None: + """Compute a structural hash of the graph including tensor metadata. + + Uses FxGraphCachePickler(device_id_agnostic=True) to serialize + (target, val) per call_function node, capturing op targets and + FakeTensor metadata (dtype, shape, stride, etc.) with device indices + normalized to 0. + + Returns None if the graph contains unpicklable objects. + """ + from torch._inductor.codecache import BypassFxGraphCache, FxGraphCachePickler + + try: + pickler = FxGraphCachePickler(gm, device_id_agnostic=True) + data = pickler.dumps( + tuple( + (str(n.target), n.meta.get("val")) + for n in gm.graph.nodes + if n.op == "call_function" + ) + ) + digest = hashlib.blake2b(data, digest_size=8).digest() + return int.from_bytes(digest, "big", signed=True) + except BypassFxGraphCache: + # FxGraphCachePickler can't serialize certain objects: + # mkldnn tensors, BackwardState, torchbind objects, or general + # pickle failures. Skip the SPMD check gracefully. + log.warning("SPMD check: skipping, unpicklable graph objects", exc_info=True) + return None + + +def _build_diag_fingerprint( + gm: torch.fx.GraphModule, +) -> tuple[tuple[str, str | None], ...]: + """Build human-readable fingerprint for mismatch diagnostics. + + Only called on the rare mismatch path. + """ + from torch._inductor.codecache import extract_tensor_metadata_for_cache_key + + entries: list[tuple[str, str | None]] = [] + for n in gm.graph.nodes: + if n.op != "call_function": + continue + target_str = str(n.target) + val = n.meta.get("val") + entries.append( + ( + target_str, + _format_val_metadata(val, extract_tensor_metadata_for_cache_key), + ) + ) + return tuple(entries) + + +def _format_val_metadata(val: object, extract_fn: object) -> str | None: + """Format node val metadata for human-readable diagnostics.""" + if val is None: + return None + if isinstance(val, torch.Tensor): + return str(extract_fn(val)) # type: ignore[operator] + if isinstance(val, (tuple, list)): + parts = [] + for v in val: + if isinstance(v, torch.Tensor): + parts.append(str(extract_fn(v))) # type: ignore[operator] + else: + parts.append(str(type(v).__name__)) + return f"({', '.join(parts)})" + return str(type(val).__name__) + + +def spmd_check(gm: torch.fx.GraphModule) -> bool: + """Verify all ranks have identical FX graph structure (SPMD). + + Computes a structural hash (op targets + tensor metadata including + shapes, dtypes, strides) and compares across ranks. + On mismatch, emits a diagnostic report to stdout, logging, and + trace_structured. + + Returns True if graphs match (SPMD), False on mismatch. + """ + import torch.distributed as dist + + if not dist.is_initialized() or dist.get_world_size() <= 1: + return True + + structure_hash = _compute_hash(gm) + if structure_hash is None: + return True + + from torch._subclasses.fake_tensor import unset_fake_temporarily + from torch.distributed.distributed_c10d import _get_default_group + + pg = _get_default_group() + world_size = dist.get_world_size() + rank = dist.get_rank() + + with unset_fake_temporarily(): + all_hashes: list[int] = [0] * world_size + dist.all_gather_object(all_hashes, structure_hash, pg) + + if all(h == all_hashes[0] for h in all_hashes): + return True + + # Mismatch detected — build and gather diagnostic fingerprints + fingerprint = _build_diag_fingerprint(gm) + with unset_fake_temporarily(): + all_fingerprints: list[tuple[object, ...]] = [() for _ in range(world_size)] + dist.all_gather_object(all_fingerprints, fingerprint, pg) + + report = _build_mismatch_report(all_fingerprints, rank, world_size) + + print(report, flush=True) + log.warning("\n%s", report) + + trace_structured( + "artifact", + metadata_fn=lambda: { + "name": "inductor_spmd_graph_mismatch", + "encoding": "string", + }, + payload_fn=lambda: report, + ) + + if config.aten_distributed_optimizations.spmd_mismatch == "error": + raise RuntimeError( + "SPMD graph verification failed. " + 'Set aten_distributed_optimizations.spmd_mismatch="warn" ' + "to warn instead of fail.\n" + report + ) + + return False + + +def _entry_target(entry: object) -> str: + """Extract the target string from a fingerprint entry.""" + if isinstance(entry, tuple): + return str(entry[0]) + return str(entry) + + +def _entry_metadata(entry: object) -> str: + """Format metadata from a fingerprint entry, if present.""" + if isinstance(entry, tuple) and len(entry) >= 2: + meta = entry[1] + if meta is not None: + return f" meta={meta}" + return "" + + +def _build_mismatch_report( + all_fingerprints: list[tuple[object, ...]], + rank: int, + world_size: int, +) -> str: + """Build diagnostic report for SPMD graph mismatch.""" + lines = [ + "=" * 80, + f"SPMD GRAPH MISMATCH — rank {rank}, world_size={world_size}", + "=" * 80, + ] + + # Node count per rank + counts = [len(t) for t in all_fingerprints] + lines.append("NODE COUNTS PER RANK:") + for r in range(world_size): + marker = " <--" if counts[r] != counts[0] else "" + lines.append(f" rank {r}: {counts[r]} call_function nodes{marker}") + lines.append("") + + # Find entries that differ + ref = all_fingerprints[0] + for r in range(1, world_size): + other = all_fingerprints[r] + if other == ref: + continue + lines.append(f"DIFFS rank 0 vs rank {r}:") + + # Show first few positional differences + max_diffs = 10 + shown = 0 + for i, (a, b) in enumerate(zip(ref, other)): + if a != b and shown < max_diffs: + lines.append(f" node {i}:") + lines.append(f" rank 0: {_entry_target(a)}{_entry_metadata(a)}") + lines.append(f" rank {r}: {_entry_target(b)}{_entry_metadata(b)}") + shown += 1 + + # Also show count-based diffs for op targets + ref_targets = [_entry_target(e) for e in ref] + other_targets = [_entry_target(e) for e in other] + + ref_counts = Counter(ref_targets) + other_counts = Counter(other_targets) + only_ref = ref_counts - other_counts + only_other = other_counts - ref_counts + if only_ref: + lines.append(" Only on rank 0:") + for op, cnt in only_ref.most_common(10): + lines.append(f" {op} (x{cnt})") + if only_other: + lines.append(f" Only on rank {r}:") + for op, cnt in only_other.most_common(10): + lines.append(f" {op} (x{cnt})") + lines.append("") + + lines.append("=" * 80) + return "\n".join(lines) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_utils.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..c520deb369a0a4527887cdb7ba6b28e619827da6 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/fx_utils.py @@ -0,0 +1,346 @@ +# mypy: allow-untyped-defs +import contextlib +import operator +from collections import defaultdict +from collections.abc import Callable +from typing import Any + +import sympy + +import torch +import torch.fx +from torch._dispatch.python import enable_python_dispatcher +from torch._subclasses.fake_tensor import FakeTensorMode +from torch.fx.experimental.symbolic_shapes import ( + compute_unbacked_bindings, + rebind_unbacked, + statically_known_true, + sym_eq, +) +from torch.utils import _pytree as pytree +from torch.utils._ordered_set import OrderedSet +from torch.utils._pytree import tree_map +from torch.utils.flop_counter import flop_registry + +from .virtualized import V + + +# Check the pattern: (nn.module, F.function/torch.Tensor.method) matched. +# Works for length 2 patterns with 1 module and 1 function/method. +def matches_module_function_pattern( + pattern: tuple[type[torch.nn.modules.Module], Callable[..., Any]], + node: torch.fx.node.Node, + modules: dict[str, torch.nn.modules.Module], +) -> bool: + if len(node.args) == 0: + return False + if not isinstance(node.args[0], torch.fx.Node) or not isinstance( + node, torch.fx.Node + ): + return False + # the first node is call_module + if node.args[0].op != "call_module": + return False + if not isinstance(node.args[0].target, str): + return False + if node.args[0].target not in modules: + return False + if type(modules[node.args[0].target]) is not pattern[0]: + return False + # the second node is call_function or call_method + if node.op != "call_function" and node.op != "call_method": + return False + if node.target != pattern[1]: + return False + # make sure node.args[0] output is only used by current node. + if len(node.args[0].users) > 1: + return False + return True + + +class FakeTensorUpdater: + """ + The main idea here is that it's difficult to maintain accurate fake + tensors (our primary form of metadata) for each node in our graph as we + transform it. + + The most reliable way to obtain this information is by rerunning + faketensor propagation. However, in general, faketensor propagation is + fairly expensive. So, instead we'd like to only rerun faketensor + propagation on nodes that have changed. + + In order to detect which nodes have changed, we first hash its node, + target, and argument lists (which are immutable in FX). + + Then, whenever we call incremental_update, we check which FX nodes have a + new hash, and recompute the faketensor metadata for that node. Then, we + continue to recursively compute the faketensors for all users until the + fake tensors stop changing. + """ + + def __init__(self, graph: torch.fx.Graph) -> None: + self.processed_hashes = OrderedSet[Any]() + self.graph = graph + + for node in self.graph.nodes: + self.processed_hashes.add(self.hash_node(node)) + + def hash_node(self, node: torch.fx.Node): + # todo(chilli): Not a great hash function + return (node, node.target, id(node.args), id(node.kwargs)) + + def incremental_update(self): + """Update FakeTensors on self.graph. We will try to do the minimum amount of work.""" + existing_storages: defaultdict[int | None, int] = defaultdict(int) + for node in self.graph.nodes: + existing_storages[get_node_storage(node)] += 1 + + def is_intlist_same(new, old): + return statically_known_true(sym_eq(new, old)) + + def is_fake_tensor_same(new, old, *, node): + if type(new) is not type(old): + return False + if isinstance(new, (list, tuple)): + if len(new) != len(old): + return False + return all( + is_fake_tensor_same(new_i, old_i, node=node) + for new_i, old_i in zip(new, old) + ) + if new is None: + return old is None + if not isinstance(new, torch.Tensor): + assert isinstance(new, (torch.SymInt, torch.SymBool, torch.SymFloat)), ( + f"Unknown type {type(new)} in {self.graph}" + ) + return ( + new.node.shape_env._maybe_evaluate_static( + sympy.Eq(new.node.expr, old.node.expr) + ) + == sympy.true + ) + if not is_intlist_same(new.shape, old.shape) or new.layout != old.layout: + return False + if new.layout == torch.strided and ( + not is_intlist_same(new.stride(), old.stride()) + or not statically_known_true( + new.storage_offset() == old.storage_offset() + ) + ): + return False + + if new.device != old.device: + return False + + if get_storage(new) == get_storage(old): + return True + + def any_user_may_alias(node): + if not isinstance(node.meta["val"], torch.Tensor): + # analysis too complicated on lists, can support in the future + return True + for user in node.users: + if not ( + isinstance( + user.target, + (torch._ops.OpOverload, torch._ops.HigherOrderOperator), + ) + or user.target + is torch._inductor.fx_passes.reinplace._generalized_scatter + ): + return True + if isinstance(user.target, torch._ops.HigherOrderOperator): + # HOPs that survive until inductor are all non-aliasing HOPs. + # We will likely never support HOPs that are aliasing. + continue + # Strategy: do a FakeTensor prop, see if the storage aliases. + # If Inductor ever gets tighter invariants on OpOverloads + # (that is, we ban things like torch.ops.aten.reshape calls in the graph), + # Then this could just be a fast schema lookup. + is_valid, args, kwargs = get_fake_args_kwargs(user) + if not is_valid: + return True + with ( + V.fake_mode, + enable_python_dispatcher(), + contextlib.ExitStack() as stack, + ): + # Ignore unbacked symbols (if they exist): we're making + # this FakeTensor and then throwing it away. + shape_env = V.fake_mode.shape_env + if shape_env is not None: + stack.enter_context( + shape_env.ignore_fresh_unbacked_symbols() + ) + new_fake_tensor = user.target(*args, **kwargs) + if not isinstance(new_fake_tensor, torch.Tensor): + # analysis too complicated on lists, can support in the future + return True + if get_storage(new_fake_tensor) == get_storage(node.meta["val"]): + return True + return False + + # This is the case where it returns a completely fresh storage that's used nowhere else. + # If the FakeTensor's storage is fresh and none of the node's users can alias it, then + # we don't need to update this node. + if ( + existing_storages[get_storage(old)] == 1 + and get_storage(new) not in existing_storages + and not any_user_may_alias(node) + ): + return True + + return False + + def should_process_node(node): + # node.target for nodes returning true from this function + # are called under fake mode and does not work for inductor + # lowerings. We check if the node.target is an aten operator + # or operator.getitem which is used when returning multiple + # tensors from an op. + return node.op == "call_function" and ( + isinstance(node.target, torch._ops.OpOverload) + or node.target is operator.getitem + or node.target + is torch._inductor.fx_passes.reinplace._generalized_scatter + ) + + to_process = OrderedSet[int]() + for node in self.graph.nodes: + # NB: Be very careful about skipping nodes (via continues) here + # and ask for a careful review when changing this code. The + # consequence for incorrect FakeTensor metadata is difficult-to-debug + # silent incorrectness. + if ( + self.hash_node(node) in self.processed_hashes + and id(node) not in to_process + ): + continue + + if not should_process_node(node): + continue + + is_valid, args, kwargs = get_fake_args_kwargs(node) + if not is_valid: + continue + with V.fake_mode, enable_python_dispatcher(): + new_fake_tensor = node.target(*args, **kwargs) + + if "val" in node.meta and is_fake_tensor_same( + new_fake_tensor, node.meta["val"], node=node + ): + continue + + rebind_unbacked(V.fake_mode.shape_env, node, new_fake_tensor) + + node.meta["val"] = new_fake_tensor + if (shape_env := V.fake_mode.shape_env) and ( + symbol_to_path := compute_unbacked_bindings(shape_env, new_fake_tensor) + ): + # Refresh the bindings to the new symbols + + node.meta["unbacked_bindings"] = symbol_to_path + + existing_storages[get_node_storage(node)] += 1 + + to_process.update([id(user) for user in node.users]) + + self.processed_hashes.add(self.hash_node(node)) + + +def get_storage(t: torch.Tensor) -> int: + return t.untyped_storage()._cdata + + +def get_node_storage(node: torch.fx.Node) -> int | None: + if "val" not in node.meta: + return None + if not isinstance(node.meta["val"], torch.Tensor): + return None + if not torch._C._has_storage(node.meta["val"]): + return None + return get_storage(node.meta["val"]) + + +def get_fake(x): + if isinstance(x, torch.fx.Node): + if "val" not in x.meta: + return x + return x.meta["val"] + return x + + +def get_fake_args_kwargs(x: torch.fx.Node) -> tuple[bool, tuple[Any], dict[str, Any]]: + """ + First value returns a boolean if any of the input nodes don't have a faketensor. + """ + args, kwargs = tree_map(get_fake, (x.args, x.kwargs)) + if any( + isinstance(a, torch.fx.Node) for a in pytree.arg_tree_leaves(*args, **kwargs) + ): + return False, args, kwargs + return True, args, kwargs + + +def is_node_realized(node: torch.fx.Node) -> bool: + """Returns true if a node is always realized when lowered to inductor IR. + + NOTE: This may return some false negatives. e.g. it doesn't + handle buffers realized heuristically during lowering, or + buffers realized indirectly through view ops. + """ + from torch._inductor.lowering import fallbacks, needs_realized_inputs + + def is_buffer(node: torch.fx.Node) -> bool: + if node.op == "call_function" and node.target is operator.getitem: + # For nodes with multiple outputs, we get the fx graph: + # foo = torch.ops.aten.foo(...) + # getitem = foo[0] + # getitem_1 = foo[1] + # where we need to check if foo is a fallback kernel + return is_buffer(node.args[0]) # type: ignore[arg-type] + return node.op in ("placeholder", "output") or node.target in fallbacks + + if is_buffer(node): + return True + + def realizes_inputs(node: torch.fx.Node) -> bool: + return node.op == "output" or node.target in needs_realized_inputs + + if any(realizes_inputs(user) for user in node.users): + return True + + # Otherwise, assume node isn't realized + return False + + +def count_flops_fx(node: torch.fx.Node) -> int | None: + if not countable_fx(node) or isinstance(node.target, str): + return None + with FakeTensorMode(allow_non_fake_inputs=True): + success, args, kwargs = get_fake_args_kwargs(node) + + if success: + with torch.utils.flop_counter.FlopCounterMode( + display=False + ) as flop_counter_mode: + node.target(*args, **kwargs) + + counted_flops = flop_counter_mode.get_total_flops() + return counted_flops + return None + + +def countable_fx(node: torch.fx.Node) -> bool: + """ + Whether or not we can count the flops of an FX node. + """ + assert isinstance(node, torch.fx.Node) + if not hasattr(node, "target"): + return False + target = node.target + if not hasattr(target, "overloadpacket"): + return target in flop_registry + packet = target.overloadpacket + return packet in flop_registry diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/graph.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/graph.py new file mode 100644 index 0000000000000000000000000000000000000000..c7747724945452aabdb102f79b7568c16164614d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/graph.py @@ -0,0 +1,2767 @@ +from __future__ import annotations + +import contextlib +import functools +import itertools +import logging +import operator +import os +import re +import sys +import time +import typing_extensions +from collections import defaultdict +from contextlib import contextmanager +from typing import Any, NoReturn, TYPE_CHECKING + +import sympy +from sympy import Expr + +import torch +import torch._logging +import torch.fx +from torch import device, Tensor +from torch._decomp import get_decompositions +from torch._dynamo.utils import defake, dynamo_timed +from torch._library.fake_class_registry import FakeScriptObject +from torch._library.opaque_object import ( + is_opaque_reference_type, + is_opaque_type, + is_opaque_value_type, +) +from torch._library.utils import get_layout_constraint_tag +from torch._logging import LazyString, trace_structured +from torch._prims_common import ( + compute_required_storage_length, + make_channels_last_strides_for, +) +from torch._subclasses.fake_tensor import FakeTensor +from torch._utils_internal import full_aoti_runtime_assert +from torch.fx.experimental._backward_state import BackwardState +from torch.fx.experimental.sym_node import magic_methods, method_to_operator +from torch.fx.experimental.symbolic_shapes import ( + _get_placeholder_expr, + free_unbacked_symbols, + has_free_symbols, + resolve_unbacked_bindings, + RuntimeAssert, + ShapeEnv, + SympyBoolean, + SymTypes, +) +from torch.fx.node import Node +from torch.fx.passes.reinplace import _is_view_op +from torch.utils._mode_utils import no_dispatch +from torch.utils._ordered_set import OrderedSet +from torch.utils._sympy.numbers import int_oo + +from . import config, ir, metrics +from .codegen.common import ( + BackendFeature, + DeviceOpOverrides, + FileBackedGraphModule, + get_backend_features, + get_device_op_overrides, + get_wrapper_codegen_for_device, + init_backend_registration, + WorkspaceArg, +) +from .exc import ( + CppWrapperCodegenError, + LoweringException, + MissingOperatorWithDecomp, + MissingOperatorWithoutDecomp, +) +from .fx_utils import count_flops_fx +from .ir import ( + assign_origin_node, + Constant, + DonatedBuffer, + FixedLayout, + get_device_type, + GraphPartitionSignature, + InputBuffer, + Pointwise, + Reduction, + ShapeAsConstantBuffer, + StorageBox, + TensorBox, + TorchBindObject, +) +from .lowering import ( + constrain_to_fake_tensors, + constrain_to_fx_strides, + FALLBACK_ALLOW_LIST, + fallback_handler, + fallback_node_due_to_unsupported_type, + lowerings, + make_fallback, + maybe_layout_constraints, + needs_realized_inputs, + require_contiguous, + tag_to_layout_constraint, + unsupported_output_tensor, + user_lowerings, +) +from .runtime import autotune_cache +from .runtime.autotune_cache import AutotuneCacheBundler +from .sizevars import SizeVarAllocator +from .utils import ( + convert_shape_to_inductor, + gather_origins, + get_cloned_parameter_buffer_name, + get_donated_idxs, + get_sympy_Expr_dtype, + GraphPartitionMap, + is_same_tensor, + maybe_get_suppress_shape_guards_ctx, + normalize_name, + should_assume_input_aligned, + should_fallback_by_default, + SUPPORTED_MKLDNN_DEVICES, + ValueWithLineMap, +) +from .virtualized import NullHandler, V + + +if TYPE_CHECKING: + from collections.abc import Callable, Iterable, Iterator, Sequence + from types import ModuleType + + from torch._higher_order_ops.effects import _EffectType + from torch.fx import GraphModule + from torch.fx.graph import Graph + + from .codegen.wrapper import PythonWrapperCodegen + from .dependencies import Dep + from .scheduler import BaseSchedulerNode + + CompiledModule = ModuleType | FileBackedGraphModule + +from torch._inductor.codecache import output_code_log + + +log = logging.getLogger(__name__) +perf_hint_log = torch._logging.getArtifactLogger(__name__, "perf_hints") + +aten = torch.ops.aten + +_post_grad_graph_counter = itertools.count() + +if config.is_fbcode(): + from torch._inductor.fb.triton_kernel_metadata import ( + save_triton_kernel_perf_artifact, + ) + from torch._inductor.fb.utils import log_module_code +else: + + def log_module_code(*args: Any, **kwargs: Any) -> None: + pass + + def save_triton_kernel_perf_artifact(*args: Any, **kwargs: Any) -> None: + pass + + +def may_get_constant_buffer_dtype(constant_buffer: sympy.Expr) -> torch.dtype | None: + assert isinstance( + constant_buffer, (sympy.Symbol, sympy.Expr, sympy.core.numbers.Integer) + ), ( + "get_constant_buffer_dtype only supports input of sympy.Symbol, sympy.Expr or sympy.core.numbers.Integer" + ) + if isinstance(constant_buffer, sympy.core.numbers.Integer): + return torch.int64 + + if isinstance(constant_buffer, sympy.Expr): + return get_sympy_Expr_dtype(constant_buffer) + + if constant_buffer.is_integer: + return torch.int64 + elif constant_buffer.is_float: + return torch.float32 + else: + return None + + +def is_magic_method(op: Any) -> bool: + magic_ops = OrderedSet(method_to_operator(m) for m in magic_methods) + return op in magic_ops + + +def getattr_recursive( + obj: GraphModule, target: str +) -> Tensor | torch._C.ScriptObject | GraphModule: + target_atoms = target.split(".") + attr_itr = obj + for i, atom in enumerate(target_atoms): + if not hasattr(attr_itr, atom): + raise RuntimeError( + f"Node referenced nonexistent target {'.'.join(target_atoms[:i])}" + ) + attr_itr = getattr(attr_itr, atom) + return attr_itr + + +def get_user_visible_output_strides(g: Graph) -> dict[Node, tuple[int, ...]]: + ret: dict[Node, tuple[int, ...]] = {} + output_node = g.find_nodes(op="output")[0] + + if "user_visible_output_idxs" not in output_node.meta: + return ret + + if not isinstance(output_node.args[0], torch.fx.Node): + output_node_args = output_node.args[0] + else: + output_node_args = output_node.args + + for idx, node in enumerate(output_node_args): + if idx in output_node.meta["user_visible_output_idxs"]: + ret[node] = output_node.meta["original_output_strides"][idx] + return ret + + +def extend_user_visible_output_strides( + user_visible_outputs: dict[Node, tuple[int, ...]], +) -> dict[Node, object]: + """ + Extend user_visible_output_strides to include view ops that lead to user-visible outputs. + """ + result: dict[Node, object] = {**user_visible_outputs} + queue = [*result.keys()] + visited = OrderedSet([*queue]) + while queue: + current = queue.pop() + if ( + _is_view_op(current.target) + and current.args + and isinstance(current.args[0], torch.fx.Node) + ): + base = current.args[0] + if base not in visited: + result.setdefault(base, None) + visited.add(base) + queue.append(base) + return result + + +def mark_nodes_dislike_padding( + g: Graph, user_visible_output_strides: dict[Node, tuple[int, ...]] +) -> None: + """ + Nodes like convolution/convolution_backward want its input to be dense. + If we pad their inputs, we result in extra calls to copy kernels! On the other hand, padding usually helps reduction. + + The pass finds nodes that dislike padding. These are nodes that can be reached + from a convolution/convolution_backward in the backward direction without + going thru a reduction. + """ + if not config.comprehensive_padding: + return + + extended_user_visible_nodes = extend_user_visible_output_strides( + user_visible_output_strides + ) + ops_dislike_padding = OrderedSet( + [ + aten.convolution, + aten.convolution_backward, + aten._scaled_mm, + ] + ) + # what's a better way to collect the reduction ops? + ops_like_padding = OrderedSet( + [ + aten.var_mean, + aten.sum, + aten.mean, + aten.prod, + aten.any, + aten.amin, + aten.amax, + aten.min, + aten.max, + aten.argmin, + aten.argmax, + aten.scatter_reduce, + ] + ) + + def _get_overload_packet( + node: torch.fx.Node, + ) -> torch._ops.OpOverloadPacket | None: + return ( + node.target._overloadpacket + if node.op == "call_function" + # hasattr on OpOverloadPacket is slow, do isinstance first + and isinstance(node.target, torch._ops.OpOverload) + and hasattr(node.target, "_overloadpacket") + else None + ) + + for cur in reversed(g.nodes): + if isinstance( + cur.target, + torch._higher_order_ops.triton_kernel_wrap.TritonKernelWrapperMutation, + ): + cur.meta["dislike_padding"] = True + continue + + if ( + isinstance(cur.target, torch._ops.OpOverload) + and get_layout_constraint_tag(cur.target) + == torch._C.Tag.needs_exact_strides + ): + cur.meta["dislike_padding"] = True + continue + + op = _get_overload_packet(cur) + if not op: + continue + if op in ops_dislike_padding: + cur.meta["dislike_padding"] = True + + if cur.meta.get("dislike_padding", False): + # propagate + for prior in cur.all_input_nodes: + prior_op = _get_overload_packet(prior) + if not prior_op: + continue + if prior_op not in ops_like_padding: + prior.meta["dislike_padding"] = True + # We only want to mark output nodes. So, move it after the above prior nodes process. + if not config.pad_outputs and cur in extended_user_visible_nodes: + cur.meta["dislike_padding"] = True + + +def is_mkldnn_conv(node: Node) -> bool: + # When mkldnn_fusion is enabled, conv will be replaced by the lowering pattern function. + # See _register_unary_fusion_lowering in torch/_inductor/fx_passes/mkldnn_fusion.py. + if ( + getattr(torch.ops, "mkldnn", None) is not None + and getattr(torch.ops.mkldnn, "_convolution_pointwise", None) is not None + and isinstance(node.target, functools.partial) + and len(node.target.args) > 0 + and hasattr(node.target.args[0], "targets") + ): + for target in node.target.args[0].targets: + if target.fns[0] in [ + torch.ops.mkldnn._convolution_pointwise.default, + torch.ops.mkldnn._convolution_pointwise.binary, + torch.ops.mkldnn._convolution_pointwise_.binary, + ]: + return True + + return False + + +class GraphLowering(torch.fx.Interpreter): + graph_outputs: list[ir.IRNode] + + def __init__( + self, + gm: torch.fx.GraphModule, + example_inputs: Sequence[object] | None = None, + shape_env: ShapeEnv | None = None, + graph_id: int | None = None, + cpp_wrapper: bool = False, + aot_mode: bool = False, + layout_opt: bool | None = None, + extern_node_serializer: Callable[[list[ir.ExternKernelNode]], Any] + | None = None, + is_inference: bool = False, + is_backward: bool = False, + is_const_graph: bool = False, + const_output_index: dict[str, int] | None = None, + const_wrapper_code: str | None = None, + const_kernel_code: str | None = None, + const_module: GraphLowering | None = None, + name: str | None = None, + inputs_to_check: Sequence[int] | None = None, + fx_wrapper: bool = False, + get_decomp_fn: Callable[..., dict[Any, Callable[..., Any]]] | None = None, + ) -> None: + super().__init__(gm) + self.get_decomp_fn = get_decomp_fn + self.example_inputs = example_inputs + self.layout_opt = ( + layout_opt + if layout_opt is not None + else self.decide_layout_opt(gm, is_inference=is_inference) + ) + self.num_channels_last_conv = 0 + self.is_inference = is_inference + self.is_backward = is_backward + self.is_const_graph = is_const_graph + self.const_wrapper_code = const_wrapper_code + self.const_kernel_code = const_kernel_code + self.const_module = const_module + self.inputs_to_check = inputs_to_check + self._defers_input_alignment = False + + self.extra_traceback = False # we do our own error wrapping + if shape_env is None: + shape_env = ShapeEnv() + self.reuse_shape_env = False + else: + self.reuse_shape_env = True + self._shape_env = shape_env + # We're going to mutate ras_by_symbol as we finish generating them + self.ras_by_symbol: dict[sympy.Symbol | None, list[RuntimeAssert]] = ( + shape_env.deferred_runtime_asserts.copy() + ) + self.bound_unbacked_symbols = OrderedSet[sympy.Symbol]() + + self.sizevars = SizeVarAllocator(shape_env) + self.graph_input_names: list[str] = [] + self.graph_inputs: dict[str, TensorBox | TorchBindObject | sympy.Expr] = {} + self.graph_inputs_original: dict[str, InputBuffer] = {} + self.partition_maps: list[GraphPartitionMap] | None = None + self.zero_dim_cpu_tensor_list: OrderedSet[str] = OrderedSet() + self.device_types: OrderedSet[str] = ( + const_module.device_types if const_module else OrderedSet() + ) + self.device_idxs: OrderedSet[int] = ( + const_module.device_idxs if const_module else OrderedSet() + ) + self.device_type = "cpu" + self.additional_buffer_deps: dict[str, OrderedSet[str]] = defaultdict( + OrderedSet + ) + self.additional_star_deps: dict[str, OrderedSet[str]] = defaultdict(OrderedSet) + + # Inplace padding may require Inductor to allocate slightly larger + # tensor for padding. + self.buffer_to_padded_size: dict[str, list[int]] = {} + + self.buffers: list[ir.Buffer] = [] + self.operations: list[ir.Operation] = [] + self.const_output_index: dict[str, int] = ( + const_output_index if const_output_index else {} + ) + self.folded_constants: OrderedSet[str] = ( + OrderedSet(const_output_index.keys()) + if const_output_index + else OrderedSet() + ) + self.constants: dict[str, torch.Tensor] = ( + const_module.constants if const_module else {} + ) + self.named_buffers: dict[str, torch.Tensor] = ( + const_module.named_buffers if const_module else {} + ) + self.mutated_named_buffers: OrderedSet[torch.Tensor] = gm.meta.get( + "mutated_named_buffers", OrderedSet() + ) + self.named_parameters: dict[str, torch.Tensor] = ( + const_module.named_parameters if const_module else {} + ) + self.torchbind_constants: dict[ + str, torch._C.ScriptObject | FakeScriptObject + ] = {} + self.opaque_value_type_classes: dict[str, type] = {} + self.seen_subgraphs: dict[str, ir.Subgraph] = {} + self.constant_reprs: dict[str, str] = {} + self.removed_operations: OrderedSet[str] = OrderedSet() + self.removed_buffers: OrderedSet[str] = OrderedSet() + self.removed_inplace_buffers: OrderedSet[str] = OrderedSet() + self.mutated_buffers: OrderedSet[str] = OrderedSet() + self.sdpa_constraint_cache: dict[tuple, ir.IRNode] = {} + self.never_reuse_buffers: OrderedSet[str] = OrderedSet() + self.inplaced_to_remove: OrderedSet[str] = OrderedSet() + self.device_ops: DeviceOpOverrides = None # type: ignore[assignment] + self.wrapper_code: PythonWrapperCodegen = None # type: ignore[assignment] + + from torch._inductor.extern_node_serializer import extern_node_json_serializer + + self.extern_node_serializer: Callable[[list[ir.ExternKernelNode]], Any] = ( + extern_node_serializer + if config.is_fbcode() and extern_node_serializer + else extern_node_json_serializer + ) + + self.current_node: torch.fx.Node = None # type: ignore[assignment] + self.lists: dict[str, list[str]] = {} + self.mutated_inputs: OrderedSet[str] = OrderedSet() + self.mutated_input_idxs: list[int] = [] + self.name_to_buffer: dict[str, ir.Buffer] = {} + self.name_to_users: defaultdict[str, list[ir.IRNode]] = defaultdict(list) + self.name_to_op: dict[str, ir.Operation] = {} + self.creation_time = time.time() + self.name = name # type: ignore[assignment] + self.cpp_wrapper = cpp_wrapper + self.fx_wrapper = fx_wrapper + + # record multi_kernel choice for cpp_wrapper so the second pass knows + # which sub-kernel is picked. Copy cpp_wrapper to another variable + # since cpp_wrapper flag is OrderedSet to false for the first pass of codegen. + self.record_multi_kernel_choice = cpp_wrapper + self.multi_kernel_to_choice: dict[str, str] = {} + + self.aot_mode = aot_mode + self.graph_id = graph_id + self.post_grad_graph_id = next(_post_grad_graph_counter) + self.scheduler: torch._inductor.scheduler.Scheduler = None # type: ignore[assignment] + + # record intermediate results for input of UsedDefinedTritonKernels + # This will be used if autotuning is done in one pass. + self.autotuning_inputs: list[torch.Tensor] | None = None + self.autotuning_mapping: dict[str, dict[str, int]] | None = None + self.autotuning_grids: dict[str, Any] | None = None + + # current_device is set only during codegen of a device-specific kernel + # a graph can have many devices + self.current_device: torch.device | None = None + + self.nodes_prefer_channels_last = ( + self.find_nodes_prefer_channels_last() if self.layout_opt else OrderedSet() + ) + self._warned_fallback = OrderedSet(["aten.convolution_backward"]) + self.user_visible_output_strides = get_user_visible_output_strides(gm.graph) + mark_nodes_dislike_padding(gm.graph, self.user_visible_output_strides) + self.cache_key: str = "" # This is the cache key for the compiled artifact + self.cache_path: str = "" # This is the path in the filesystem where the compiled artifact is stored + self.cache_linemap: list[ + tuple[int, str] + ] = [] # This is the linemap used by the profiler to mark custom compiled kernels getting run + # Used if lowering encounters cases where cudagraphs are not supported + self.disable_cudagraphs_reason: str | None = None + + # only keeping one node per device for stack trace purposes + self.device_node_mapping: dict[torch.device, torch.fx.Node] = {} + self.orig_gm: torch.fx.GraphModule = gm.__copy__() + for k, v in self.orig_gm.named_buffers(): + self.named_buffers[k] = v + for k, v in self.orig_gm.named_parameters(): + self.named_parameters[k] = v + self.dynamo_flat_name_to_original_fqn = self.module.meta.get( # type: ignore[operator, union-attr] + "dynamo_flat_name_to_original_fqn", {} + ) + self.allocated_constant_name: dict[str, str] = ( + const_module.allocated_constant_name if const_module is not None else {} + ) + init_backend_registration() + self.get_backend_features = functools.lru_cache(None)(get_backend_features) + + self.effectful_ops: dict[_EffectType, ir.Buffer] = {} + # Track the buffers that we know is unaligned + # This can either be a graph input or the output of fallback + # kernels. + self.unaligned_buffers: OrderedSet[str] = OrderedSet() + self.no_fuse_buffer_names: OrderedSet[str] = OrderedSet() + + # Layout constraints for Triton template buffers. + # Maps buffer name -> expected FixedLayout (computed speculatively without freezing) + self.buffer_layout_constraints: dict[str, ir.FixedLayout] = {} + + self.low_precision_codegen_ops: OrderedSet[str] = OrderedSet() + # more aggressive prologue fusion + self.invoke_quant_ops: OrderedSet[str] = OrderedSet() + + # Below field is related to printing debug intermediate tensor values info for debugging + self.all_codegen_kernel_names: OrderedSet[str] = OrderedSet() + + # state used by for KernelArgs.workspace + self.workspace_id = itertools.count() + + # track the current placeholder index that we are processing + self.placeholder_idx = -1 + + self.bw_donated_idxs = get_donated_idxs() + + # Cache for dep size hints to avoid expensive recomputation + self.dep_size_hint_cache: dict[tuple[Dep, bool], int] = {} + + def freeze_runtime_asserts(self) -> None: + self._shape_env.freeze_runtime_asserts() + + def symbolic_sizes_strides( + self, ex: torch.Tensor + ) -> tuple[Sequence[int | Expr], Sequence[int | Expr]]: + """ + Support dynamic shapes and dynamic strides by assigning variables + to each dimension. We duck-shape tensors, so if two tensors + have the same size they get assigned the same symbolic variable. + """ + if self.reuse_shape_env: + return convert_shape_to_inductor(ex.size()), convert_shape_to_inductor( + ex.stride() + ) + else: + from torch._dynamo.source import ConstantSource + + # TODO: this should not be needed once #93059 lands + # https://github.com/pytorch/pytorch/pull/94031#discussion_r1096044816 + # TODO: make a dedicated UnknownSource for this? + # NB: This is using the legacy default behavior from + # create_symbolic_sizes_strides_storage_offset but we hope we can + # just delete this entirely + source = ConstantSource( + f"__inductor_unknown_tensor_{len(self._shape_env.backed_var_to_val)}" + ) + ( + size, + stride, + _, + ) = self._shape_env.create_symbolic_sizes_strides_storage_offset( + ex, + source, + ) + + r_size = [i.node.expr if isinstance(i, torch.SymInt) else i for i in size] + r_stride = [i.node.expr if isinstance(i, torch.SymInt) else i for i in stride] + return r_size, r_stride + + def static_sizes_strides( + self, ex: torch.Tensor + ) -> tuple[list[sympy.Expr], list[sympy.Expr]]: + """ + Primarily used to weights + """ + size = [sympy.Integer(i) for i in ex.size()] + stride = [sympy.Integer(i) for i in ex.stride()] + return size, stride + + def get_allocation_size( + self, + node: ir.TensorBox + | ir.StorageBox + | ir.Buffer + | WorkspaceArg + | ir.TorchBindObject, + ) -> Sequence[Expr]: + if isinstance(node, ir.TensorBox): + node = node.data # type: ignore[assignment] + if isinstance(node, ir.StorageBox): + node = node.data # type: ignore[assignment] + if ( + isinstance(node, ir.ComputedBuffer) + and node.name in self.buffer_to_padded_size + ): + # pyrefly: ignore [bad-index, index-error] + return self.buffer_to_padded_size[node.name] + else: + return node.get_size() + + def get_allocation_storage_size( + self, node: ir.Buffer | WorkspaceArg | ir.TorchBindObject + ) -> Expr: + layout = node.get_layout() + size = self.get_allocation_size(node) # consider inplace padding + stride = layout.stride + offset = layout.offset + return compute_required_storage_length(size, stride, offset) # type: ignore[arg-type] + + def has_feature( + self, + device: torch._inductor.ir.IRNode | device | None, + feature: BackendFeature, + ) -> bool: + assert isinstance(feature, BackendFeature), feature + return feature in self.get_backend_features(get_device_type(device)) + + def get_dep_size_hint(self, dep: Dep, count_bytes: bool = True) -> int: + """ + Get the size hint for a dependency with caching to avoid expensive recomputation. + """ + if (dep, count_bytes) not in self.dep_size_hint_cache: + res = 0 + # Non-tensor graph inputs (TorchBindObject, OpaqueObjectState) + # have no meaningful size — skip the size computation entirely. + inp = self.graph_inputs.get(dep.name) + if isinstance(inp, ir.NonTensorObj): + self.dep_size_hint_cache[(dep, count_bytes)] = 0 + return 0 + try: + if ( + not dep.has_unbacked_symbols() + or self.sizevars.all_unbacked_explicitly_hinted(dep.get_numel()) + ): + if count_bytes: + res = dep.numbytes_hint() + else: + res = dep.numel_hint() + except KeyError: + # In at least one test (test/inductor/test_torchbind.py) we + # create a StarDep that doesn't exist in the graph and calling + # `has_unbacked_symbols()` throws an error. + pass + self.dep_size_hint_cache[(dep, count_bytes)] = res + return self.dep_size_hint_cache[(dep, count_bytes)] + + def get_current_device_or_throw(self) -> torch.device: + if device := self.current_device: + return device + else: + raise RuntimeError("No current device") + + @contextlib.contextmanager + def set_current_device(self, device: torch.device) -> Iterator[None]: + prior = self.current_device + self.current_device = device + try: + yield + finally: + self.current_device = prior + + def get_training_phase(self) -> str: + if self.is_inference: + return "inference" + if self.is_backward: + return "backward" + return "forward" + + @staticmethod + def decide_layout_opt(gm: GraphModule, *, is_inference: bool) -> bool: + """ + Decide if we should enable layout optimization for this graph based on + heuristics. + """ + if not config.layout_optimization: + return False + + if config.force_layout_optimization: + return True + + conv_nodes = [ + n for n in gm.graph.nodes if n.target is torch.ops.aten.convolution.default + ] + + for n in gm.graph.nodes: + if is_mkldnn_conv(n): + conv_nodes.append(n) + + nconv = len(conv_nodes) + + if nconv == 0: + return False + + # For cpu backend and mkldnn enabled, we always use channels_last for better performance. + if ( + torch.backends.mkldnn.enabled # pyrefly: ignore [unbound-name] + and torch.backends.mkldnn.is_available() # pyrefly: ignore [unbound-name] + and all( + n.args[idx].meta["val"].device.type in SUPPORTED_MKLDNN_DEVICES + for n in conv_nodes + for idx in [0, 1] + ) + ): + return True + + # Following models are skipped due to this: + # jx_nest_base + # volo_d1_224 + if len(list(gm.graph.nodes)) >= 300 * nconv: + log.debug("Skipped layout opt because only a few conv") + return False + + if any( + has_free_symbols(n.args[idx].meta["val"]) + for n in conv_nodes + for idx in [0, 1] + ): + log.debug( + "See perf regression with dynamic shape. Follow up in https://github.com/pytorch/pytorch/issues/102670" + ) + return False + + def is_grouped(n: Any) -> bool: + meta_val = n.args[1].meta["val"] # type: ignore[union-attr, operator] + assert isinstance(meta_val, torch.Tensor) + return n.args[-1] > 1 and meta_val.size(1) > 1 # type: ignore[union-attr, operator] + + def is_in_out_channel(n: torch.fx.Node) -> bool: + return ( + n.args[1].meta["val"].size(0) * 2 <= n.args[1].meta["val"].size(1) # type: ignore[union-attr, operator] + and n.args[1].meta["val"].size(2) > 1 # type: ignore[union-attr, operator] + ) + + def is_small_channel(n: torch.fx.Node) -> bool: + return ( + n.args[1].meta["val"].size(0) <= 64 # type: ignore[union-attr, operator] + and n.args[1].meta["val"].size(1) <= 64 # type: ignore[union-attr, operator] + ) + + # only grouped convolutions benchmarked as slower in conv samples for inference only + if is_inference: + flop_counts: dict[str, float] = defaultdict(float) + for node in conv_nodes: + counted_flops = count_flops_fx(node) + if counted_flops is None: + continue + + if is_grouped(node): + node_type = "grouped" + elif is_small_channel(node): + node_type = "small" + elif is_in_out_channel(node): + node_type = "in_out" + else: + node_type = "default" + + flop_counts[node_type] += counted_flops + else: + log.debug("Conv inputs meta not found") + + # average benchmarked channels last speedup / slowdown, < 1 is speedup. + # taken from the set of convolution inputs in benchmarks/dynamo/microbenchmarks/operator_inp_logs/torchbench_train/ + # To regenerate these numbers follow https://gist.github.com/eellison/55d7a6ed6f39829d68ac56f95f4df5bb + GROUPED_MULTIPLIER = 1.358 + DEFAULT_MULTIPLIER = 0.823 + IN_OUT_MULTIPLIER = 0.725 + SMALL_MULTIPLIER = 0.783 + + total_flops = sum(flop_counts.values()) + # TODO - get different values per hardware + weighted_flops = ( + flop_counts["grouped"] * GROUPED_MULTIPLIER + + flop_counts["small"] * SMALL_MULTIPLIER + + flop_counts["in_out"] * IN_OUT_MULTIPLIER + + flop_counts["default"] * DEFAULT_MULTIPLIER + ) + do_layout_opt = weighted_flops <= total_flops + if not do_layout_opt: + log.debug( + "Skipped layout opt in inference because weighted flops indicate slowdown, default: %d, channels last: %d", + total_flops, + weighted_flops, + ) + return do_layout_opt + + # Channels last layout can dramatically hurt grouped conv perf. E.g. + # Conv with arguments like + # {"input_shape": [32, 224, 112, 112], "weight_shape": [224, 112, 3, 3], + # "stride": [2, 2], "padding": [1, 1], "groups": 2} + # slows down 31x using channels last.. + + # But a lot of timm models use depthwise separable convolution which will + # result in grouped convolution with in-channel size == 1. + # For those grouped convolution, channels last still helps a lot. + # E.g. + # Conv with arguments + # {"input_shape": [128, 58, 56, 56], "weight_shape": [58, 1, 3, 3], + # "stride": [2, 2], "padding": [1, 1], "groups": 58} + # get 1.86x speedup with channels last layout. + # + # The following heuristics skip using channels-last if the model contains + # grouped convolution with in-channels > 1. + if any(map(is_grouped, conv_nodes)): + log.debug( + "Skip layout opt because found grouped convolution with >1 in_channels!" + ) + return False + + # For some models that contain convolution with larger in-channel than out-channel, applying + # channels last hurts performance. + # Following models are skipped due to this: + # - pytorch_unet + # - phlippe_densenet (slightly worse) + # - Background_Matting (1.22x -> 0.821x) + # - pytorch_CycleGAN_and_pix2pix (1.597x -> 1.294x) + if any(map(is_in_out_channel, conv_nodes)): + log.debug( + "Skip layout opt because some convolutions have smaller out_channel" + ) + return False + + # Following models are skipped due to this: + # - functorch_maml_omniglot + if all(map(is_small_channel, conv_nodes)): + log.debug("Skip layout opt because all convolution channels are too small") + return False + + return True + + def qualify_name(self, name: str) -> str: + """Prepend the given name with the graph name if any.""" + if self.name is not None: + return f"{self.name}_{name}" + return name + + def make_subgraph( + self, + gm: torch.fx.GraphModule, + example_inputs: list[torch.Tensor], + subgraph_name: str, + ) -> SubgraphLowering: + """ + Make a subgraph of the current graph with all inherited parts, except + the graph module (`gm`) and `example_inputs`. The subgraphs are lowered + separately and lifted into a separate function in the parent output + wrapper code. The subgraph name is qualified by the parent graph's + name. Note that the lifting of subgraph is supported for python wrapper + only. For cpp wrapper, we inline the subgraphs in the parent wrapper. + """ + return SubgraphLowering( + parent=self, + gm=gm, + example_inputs=example_inputs, + shape_env=self._shape_env, + cpp_wrapper=self.cpp_wrapper, + aot_mode=self.aot_mode, + extern_node_serializer=self.extern_node_serializer, + is_inference=self.is_inference, + is_backward=self.is_backward, + name=self.qualify_name(subgraph_name), + ) + + def find_nodes_prefer_channels_last(self) -> OrderedSet[Node]: + """ + The rule to decide if an node prefer channels last is simple. + 1. if it's input/output of a convolution + 2. if one of its user prefers channels last + + We have rule 1 because cudnn runs a faster convolution kernel for channels last inputs; + Rule 2 is also important. It makes sure that indirect inputs to convolution also prefers + channels last. + + Consider the scenario: conv -> batch-norm -> relu -> conv + Without rule 2, batch-norm output may use a contiguous layout. That will cause 2 extra copies: + 1. the output of batch-norm should be channels last initially since its input is a conv's output. + Forcing the batch-norm's output to be contiguous results in the first copy + 2. The second conv's input is initially contiguous. This layout is propagated from the batch-norm's output. + We need convert it to channels last layout which results in the second copy. + With rule 2, we makes sure all the tensors in the chain uses channels last layout. So both copies + can be saved. + """ + last_conv = None + nodes_cannot_propagate = [torch.ops.aten.bmm.default] + output_set = OrderedSet[Node]() + for n in reversed(self.module.graph.nodes): # type: ignore[arg-type, union-attr] + if n.target is torch.ops.aten.convolution.default: + output_set.add(n) + if last_conv is None: + last_conv = n + continue + if n.target in nodes_cannot_propagate: + continue + if is_mkldnn_conv(n): + output_set.add(n) + continue + for user in n.users: + if user in output_set: + output_set.add(n) + break + + # need a second pass to add downstream nodes of those channel last nodes to the sets. + # This pass is especially needed to avoid mix-layout kernel inputs in backward pass. + # + # Let's say a conv-batchnorm 's output is passed to relu whose output is in turn returned + # from the fwd graph. Without this second pass, we will force relu's output to be contiguous. + # Then in the kernel in backward pass, the contiguous output of relu may be mix with other channels last + # tensors and passed to a kernel. + # + # This pass improve yolov3 training speedup from 1.116x (worse than disabling layout optimization speedup 1.196x) to 1.457x. + # It also improves dla102 training speedup from 1.240x (worse than disabling layout optimization speedup 1.523x) to 1.835x . + # This also helps the following models: + # - res2net101_26w_4s + # - res2net50_14w_8s + # - sebotnet33ts_256 + for n in self.module.graph.nodes: # type: ignore[union-attr] + # layout propagation ends at last conv node, which will benefit vison transformers. + if last_conv is not None and n == last_conv: + break + if n in output_set: + for user in n.users: + if user.target in nodes_cannot_propagate: + continue + output_set.add(user) + + return output_set + + def warn_fallback(self, name: str) -> None: + if name not in self._warned_fallback: + self._warned_fallback.add(name) + perf_hint_log.info("Using FallbackKernel: %s", name) + + def add_device_info(self, device: torch.device) -> None: + self.device_types.add(device.type) + if device.index is not None: + self.device_idxs.add(device.index) + if V.graph.current_node and device not in self.device_node_mapping: + self.device_node_mapping[device] = V.graph.current_node + + @property + def fake_mode(self) -> torch._subclasses.fake_tensor.FakeTensorMode: + return V.fake_mode + + def try_get_buffer( + self, buffer_name: str + ) -> ir.TensorBox | ir.Buffer | ir.TorchBindObject | None: + if buffer_name in self.name_to_buffer: + return self.name_to_buffer[buffer_name] + if buffer_name in self.graph_inputs: + return self.graph_inputs[buffer_name] + if buffer_name in self.constants: + data = V.graph.constants[buffer_name] + return ir.ConstantBuffer( + name=buffer_name, + layout=ir.FixedLayout( + data.device, data.dtype, *V.graph.static_sizes_strides(data) + ), + ) + + return None + + def add_symbol_graph_input(self, symbol: sympy.Expr) -> None: + raise RuntimeError("Should not be called for the main graph") + + def get_buffer( + self, buffer_name: str + ) -> ir.TensorBox | ir.Buffer | ir.TorchBindObject: + buf = self.try_get_buffer(buffer_name) + if buf is not None: + return buf + raise RuntimeError(f"Failed to find buffer matching name {buffer_name}") + + def get_dtype(self, buffer_name: str) -> torch.dtype: + if buffer_name in self.constants: + return self.constants[buffer_name].dtype + # For a mutation op we should return the dtype of the buffer being mutated + if ( + hasattr(self.scheduler, "mutation_real_name") + and buffer_name in self.scheduler.mutation_real_name + ): + mutated_buf = self.scheduler.mutation_real_name[buffer_name] + if mutated_buf in self.name_to_buffer: + return self.name_to_buffer[mutated_buf].get_dtype() + if mutated_buf in self.graph_inputs: + return self.graph_inputs[mutated_buf].get_dtype() + if buffer_name in self.name_to_buffer: + return self.name_to_buffer[buffer_name].get_dtype() + if buffer_name in self.graph_inputs: + return self.graph_inputs[buffer_name].get_dtype() + m = re.match(r"(as_strided|reinterpret_tensor)\(([a-zA-Z0-9_]+),", buffer_name) + if m: + return self.get_dtype(m.group(1)) + raise KeyError(f"could not find {buffer_name}") + + def get_numel(self, buffer_name: str) -> int | Expr: + if buffer_name in self.constants: + return self.constants[buffer_name].numel() + if buffer_name in self.name_to_buffer: + buf = self.name_to_buffer[buffer_name] + if not buf.has_tensor_output(): + return 1 + return buf.get_numel() + if buffer_name in self.graph_inputs: + return self.graph_inputs[buffer_name].get_numel() + raise KeyError(f"could not find {buffer_name}") + + def run(self, *args: Any) -> Any: # type: ignore[override] + with dynamo_timed("GraphLowering.run"): + return super().run(*args) + + def register_operation(self, op: ir.Operation) -> str: + assert op.operation_name is None, f"Operation registered twice: {op}" + assert isinstance(op, ir.Operation) + name = self.qualify_name(f"op{len(self.operations)}") + self.operations.append(op) + self.name_to_op[name] = op + op.operation_name = name + return name + + def register_buffer(self, buffer: ir.Buffer, *, set_name: bool = False) -> str: + name = self.qualify_name(f"buf{len(self.buffers)}") + self.buffers.append(buffer) + self.name_to_buffer[name] = buffer + device = buffer.get_device() + if ( + # Skip empty CPU tensor so that CUDA graphs can succeed, see https://github.com/pytorch/pytorch/pull/114144 + device is not None + and not ( + isinstance(buffer, ir.ComputedBuffer) + and buffer.is_zero_elements() + and device == torch.device("cpu") + ) + ): + self.add_device_info(device) + + if set_name: + buffer.name = name + return name + + def register_operation_list(self, operation_names: list[str]) -> str: + name = self.qualify_name("list_" + "_".join(operation_names)) + self.lists[name] = operation_names + return name + + def register_users_of(self, node_output: Iterable[ir.IRNode] | ir.IRNode) -> None: + def register(value: Iterable[ir.IRNode] | ir.IRNode) -> None: + if isinstance(value, (list, tuple)): + for x in value: + register(x) + if isinstance(value, ir.TensorBox): + for read_name in value.get_read_names(): + self.name_to_users[read_name].append(value) + + register(node_output) + + def mark_buffer_mutated(self, name: str) -> None: + """ + When a buffer is mutated we need to make sure all the reads to + the old version are realized before the mutation happens. + """ + assert isinstance(name, str) + self.mutated_buffers.add(name) + + if name not in self.name_to_users: + return + + for user in self.name_to_users[name]: + user.realize() + + def get_original_value_of_constant(self, name: str) -> torch.Tensor: + """ + In AOTI, module buffers may have been mutated during the tracing and compilation. + Thus we need to read from previously stored original buffers, to make sure the + generated model.so uses correct initial values. + """ + assert name in self.allocated_constant_name and name in self.constants, ( + "Can not find the original value for " + name + ) + orig_name = get_cloned_parameter_buffer_name(self.allocated_constant_name[name]) + return ( + self.module.meta[orig_name] # type: ignore[index] + if orig_name in self.module.meta # type: ignore[operator] + else self.constants[name] + ) + + def allocate_non_dup_const_name(self, name: str | None, data: Tensor) -> str: + if not config.aot_inductor.use_runtime_constant_folding: + for constant_name, value in self.constants.items(): + if is_same_tensor(data, value): + return constant_name + + if name is None: + name = f"constant{len(self.constants)}" + orig_name = name + if name[0].isdigit(): + name = f"constant_{name}" + name = self.qualify_name(name) + # We may generate a var name for each constant in the codegen. + # Let's only keep sane characters. + prefix = normalize_name(name) + name = prefix + cnt = 0 + while name in self.constants: + name = f"{prefix}_{cnt}" + cnt += 1 + self.constants[name] = data + self.constant_reprs[name] = ( + f"{data.device!r} {data.dtype!r} " + f"{tuple(data.size())!r} {tuple(data.stride())!r} " + f"{hash(data):x}" + ) + self.allocated_constant_name[name] = orig_name # type: ignore[assignment] + return name + + def add_tensor_constant(self, data: Tensor, name: str | None = None) -> TensorBox: + new_name = self.allocate_non_dup_const_name(name, data) + return TensorBox.create( + ir.ConstantBuffer( + name=new_name, + layout=FixedLayout( + data.device, data.dtype, *self.static_sizes_strides(data) + ), + ) + ) + + def constant_name(self, name: str, device_override: torch.device | None) -> str: + """ + We AOT copy constants to the devices they are needed on. + If device_override doesn't match the constant's device, then + copy it and return a different name. + """ + if self.constants[name].device == device_override or device_override is None: + return name + with torch.utils._python_dispatch._disable_current_modes(): + # caller might have OrderedSet fake tensor mode which will create a fake tensor + # when calling .to, so unset modes here + non_dup_const_name = self.allocate_non_dup_const_name( + f"{name}_{device_override.type}{device_override.index or 0}", + self.constants[name].to(device_override), + ) + + assert non_dup_const_name in self.constants, ( + f"{non_dup_const_name} should be in V.graph.constants already" + ) + + # register device-copied buffers and parameters to graph as well + # to codegen correct torch::aot_inductor::ConstantType for them rather than `Unknown` + if any( + name == normalize_name(buffer_name) + for buffer_name in self.named_buffers + ): + self.named_buffers[non_dup_const_name] = self.constants[ + non_dup_const_name + ] + + if any( + name == normalize_name(param_name) + for param_name in self.named_parameters + ): + self.named_parameters[non_dup_const_name] = self.constants[ + non_dup_const_name + ] + + return non_dup_const_name + + # pyrefly: ignore [bad-override] + def placeholder( + self, + target: str, # type: ignore[override] + args: tuple[object], # type: ignore[override] + kwargs: dict[str, object], + ) -> Expr | TensorBox | None: + self.placeholder_idx += 1 + example = super().placeholder(target, args, kwargs) # type: ignore[arg-type] + target = self.qualify_name(target) + if isinstance(example, SymTypes): + # TODO fix partitioning issue and re-enable for backward + # https://github.com/pytorch/pytorch/issues/155468. + if not V.graph.is_backward: + expr = _get_placeholder_expr(example.node) + else: + expr = example.node.expr + self.graph_inputs[target] = expr + self.graph_input_names.append(target) + return expr + elif isinstance(example, (int, bool, float)): + expr = sympy.sympify(example) + self.graph_inputs[target] = expr + self.graph_input_names.append(target) + return expr + elif isinstance(example, FakeScriptObject): + obj = TorchBindObject(name=target, value=example) + self.graph_inputs[target] = obj + self.graph_input_names.append(target) + return obj + elif example is None: + self.graph_input_names.append(target) + return None + if isinstance(example, BackwardState): + # Ignored arg, must be unused + # Alternately we could filter this out in AotAutograd + self.graph_input_names.append(target) + return None + # See note: Note: [Generator arguments in AOTDispatcher] + elif isinstance(example, torch.Generator): + assert len(V.graph.current_node.users) == 1 and next( + iter(V.graph.current_node.users) + ).target in ( + torch._prims.rng_prims.graphsafe_run_with_rng_state, + torch.ops.higher_order.invoke_subgraph, + ) + gen = ir.GeneratorState(name=target, device=example.device) + self.graph_inputs[target] = gen # type: ignore[assignment] + self.graph_input_names.append(target) + return gen + elif is_opaque_reference_type(type(example)): + opaque_obj = ir.OpaqueObjectState(name=target, value=example) + self.graph_inputs[target] = opaque_obj # type: ignore[assignment] + self.graph_input_names.append(target) + return opaque_obj + + assert isinstance(example, torch.Tensor), example + # todo(chilli): We can remove the last check once we turn buffers into + # static shape tensors. That's a hack to workaround Inductor believing + # the buffer should be static but us passing in a fake tensor with + # symbolic shapes. + if not example._has_symbolic_sizes_strides: + # the first N inputs are weights + sizes, strides = self.static_sizes_strides(example) + else: + sizes, strides = self.symbolic_sizes_strides(example) # type: ignore[assignment] + + if ( + self.is_backward + and self.bw_donated_idxs + and self.placeholder_idx in self.bw_donated_idxs + ): + tensor = TensorBox.create( + DonatedBuffer( + name=target, + layout=FixedLayout(example.device, example.dtype, sizes, strides), + ) + ) + else: + # TODO(jansel): handle input aliasing + tensor = TensorBox.create( + InputBuffer( + name=target, + layout=FixedLayout(example.device, example.dtype, sizes, strides), + ) + ) + + self.graph_inputs[target] = tensor + self.graph_input_names.append(target) + self.graph_inputs_original[target] = tensor.data.data # type: ignore[union-attr] + if self.current_node.users: # cudagraphs should work with an unused CPU input + self.add_device_info(example.device) + + # Note: [Input Alignment handling in Inductor] + # Alignment matters for generating efficient code. Some operations, + # e.g. vectorized loads, can only be performed on aligned inputs. + # + # But if we codegen assuming aligned inputs and then get unaligned + # inputs at runtime, then we are forced to clone - which is bad for + # both perf and memory usage. + # + # One option would be to guard on storage_offset%ALIGNMENT, and then + # codegen based on this. But storage_offset guards turned out to be + # expensive and cause recompiles; Instead, we're generating code + # based on the alignment of the example input without guarding. + with maybe_get_suppress_shape_guards_ctx(): + if not should_assume_input_aligned(example): + self.unaligned_buffers.add(target) + return tensor + + @typing_extensions.override + def call_function(self, target: Callable, args: Any, kwargs: dict[str, Any]) -> Any: # type: ignore[type-arg] + if target is operator.getitem and isinstance(args[0], (list, tuple, dict)): + return super().call_function(target, args, kwargs) + + # hasattr on OpOverloadPacket is slow, check isinstance first + if not isinstance(target, torch._ops.OpOverloadPacket) and hasattr( + target, "_inductor_lowering_function" + ): + # passthrough lowerings from .pattern_matcher + return target(*args, **kwargs) + + if target not in lowerings: + assert isinstance(target, torch._ops.OpOverload), ( + f"{target} is not an OpOverload" + ) + base_name = target.name().split(".")[0] + if base_name in FALLBACK_ALLOW_LIST: + make_fallback( + target, + warn=False, + get_decomp_fn=self.get_decomp_fn, + override_decomp=True, + ) + elif config.implicit_fallbacks: + error = ( + MissingOperatorWithDecomp + if get_decompositions([target]) + else MissingOperatorWithoutDecomp + ) + log.info( + "Creating implicit fallback for:\n%s", + error.operator_str(target, args, kwargs), + ) + + tag: torch._C.Tag | None = get_layout_constraint_tag( + target, with_default=False + ) + if ( + tag is None + and torch._library.utils.is_builtin(target) + and self.is_backward + ): + # for implicit fallback ATen ops during backward, if there + # is no layout constraint tag, we conservatively require contiguous + # input since some eager kernels do not + # support non-contiguous inputs. Otherwise they may silently cause + # accuracy problems. Check https://github.com/pytorch/pytorch/issues/140452 + # We only do this For ATen ops and for backward. + # + # TODO: should really switch to "needs_fixed_stride" constraint on these + # and identify them one by one. + decided_constraint: Callable[..., tuple[Any, Any]] | None = ( + require_contiguous + ) + else: + default_tag: torch._C.Tag = get_layout_constraint_tag( + target, with_default=True + ) + decided_constraint = tag_to_layout_constraint(default_tag) + + make_fallback( + target, + layout_constraint=decided_constraint, + get_decomp_fn=self.get_decomp_fn, + ) + + elif get_decompositions([target]): + # There isn't a good way to dynamically patch this in + # since AOT Autograd already ran. The error message tells + # the user how to fix it. + raise MissingOperatorWithDecomp(target, args, kwargs) + else: + raise MissingOperatorWithoutDecomp(target, args, kwargs) + + try: + log.debug(" via %s", lowerings[target]) # type: ignore[index] + + n = self.current_node + layout_constraints = maybe_layout_constraints(target) + if layout_constraints: + old_args, old_kwargs = args, kwargs + if layout_constraints is constrain_to_fake_tensors: + # only constrain_to_fake_tensor if this exists. + # otherwise, no constraints at all: the implication is + # that this operator was inserted by a custom pass + # so we'll give them the freedom. + if "eager_input_vals" in n.meta: + fake_args, fake_kwargs = n.meta["eager_input_vals"] + + # (fake_args, fake_kwargs) might not align with (args, kwargs). + # we need to normalize them based on the schema + assert isinstance(target, torch._ops.OpOverload) + + def normalize(args: Any, kwargs: Any) -> tuple[Any, Any]: + result = torch.fx.operator_schemas.normalize_function( + target, args, kwargs + ) + assert result is not None + return result[0], result[1] + + fake_args, fake_kwargs = normalize(fake_args, fake_kwargs) + args, kwargs = normalize(args, kwargs) + old_args, old_kwargs = normalize(old_args, old_kwargs) + + args, kwargs = constrain_to_fake_tensors( + args, kwargs, fake_args, fake_kwargs + ) + else: + args, kwargs = layout_constraints(n, *args, **kwargs) + + if "should_fallback" in n.meta: + out = fallback_handler(target, add_to_fallback_set=False)( + *args, **kwargs + ) + else: + out = None + + if ( + target in user_lowerings + and target not in V.active_user_lowering_ops + ): + # User-registered lowering takes priority, with recursion guard + V.active_user_lowering_ops.add(target) + try: + # pyrefly: ignore[bad-index] + out = user_lowerings[target](*args, **kwargs) + finally: + V.active_user_lowering_ops.discard(target) + + # If no user_lowering, or it returned None fall back to normal lowering + if out is None: + if target in lowerings: + out = lowerings[target](*args, **kwargs) + else: + # Fallback for ops not in lowerings (e.g., custom ops during recursion) + out = fallback_handler(target, add_to_fallback_set=False)( + *args, **kwargs + ) + + if layout_constraints: + # layout_constraints are allowed to make new copies of the inputs. + # if they do, and if the target is mutable, then we need to + # write the new values back into the original inputs. + self.propagate_mutation(n, old_args, old_kwargs, args, kwargs) # type: ignore[possibly-undefined] + + return out + except Exception as e: + stack_trace = None + if ( + hasattr(self, "current_node") + and self.current_node is not None + and hasattr(self.current_node, "meta") + and self.current_node.meta is not None + ): + stack_trace = self.current_node.meta.get("stack_trace", None) + raise LoweringException( + e, target, args, kwargs, stack_trace=stack_trace + ).with_traceback(e.__traceback__) from None + + @staticmethod + def can_inline_constant(t: torch.Tensor) -> bool: + """ + True if this is a small constant attr that will be inlined. + """ + return len(t.shape) == 1 and t.shape[0] <= 8 + + # pyrefly: ignore [bad-override] + def get_attr( + self, + target: str, # type: ignore[override] + args: tuple[()], # type: ignore[override] + kwargs: dict[str, object], + ) -> Constant | TensorBox | ShapeAsConstantBuffer | ir.Subgraph | TorchBindObject: + # this is a constant + value = getattr_recursive(self.module, target) # type: ignore[arg-type] + + if isinstance(value, torch.fx.GraphModule): + # Reuse the existing subgraph if we have seen it before already. + if target in self.seen_subgraphs: + return self.seen_subgraphs[target] + + out = ir.Subgraph(name=target, graph_module=value) + self.seen_subgraphs[target] = out + return out + + if isinstance(value, torch._C.ScriptObject): + self.torchbind_constants[target] = value + self.constant_reprs[target] = "" + return TorchBindObject(name=target, value=value) + elif isinstance(value, FakeScriptObject): + self.torchbind_constants[target] = value + self.constant_reprs[target] = "" + return TorchBindObject(name=target, value=value) + elif is_opaque_type(type(value)): + self.torchbind_constants[target] = value # type: ignore[arg-type] + self.constant_reprs[target] = "" + return TorchBindObject(name=target, value=value) # type: ignore[arg-type] + + assert isinstance(value, torch.Tensor) + if ( + config.aot_inductor.use_runtime_constant_folding + or config.always_keep_tensor_constants + or unsupported_output_tensor(value) + or target in self.mutated_named_buffers + ): + return self.add_tensor_constant(value, target) + + with no_dispatch(): + if value.shape == (): + return Constant( + value=value.item(), dtype=value.dtype, device=value.device + ) + if self.can_inline_constant(value): + log.debug("Inlining constant: %s ", target) + # tensor lowering has constant inlining logic + from .lowering import tensor + + return tensor(value.tolist(), dtype=value.dtype, device=value.device) + + return self.add_tensor_constant(value, target) + + def call_module(self, target: Any, args: Any, kwargs: Any) -> NoReturn: + raise AssertionError + + def call_method(self, target: Any, args: Any, kwargs: Any) -> NoReturn: + raise AssertionError + + @typing_extensions.override + def output( + self, + target: torch.fx.node.Target, + args: tuple[torch.fx.node.Argument, ...], + kwargs: dict[str, object], + ) -> None: + result = super().output(target, args, kwargs) # type: ignore[arg-type] + if not isinstance(result, (tuple, list)): + # nested subgraphs can have singleton outputs + result = (result,) + assert isinstance(result, (tuple, list)), type(result) + result = [ + ir.OpaqueValueTypeConstant(value=x) if is_opaque_value_type(type(x)) else x + for x in result + ] + assert all( + isinstance( + x, + ( + TensorBox, + ir.Constant, + type(None), + ir.ConstantBuffer, + sympy.Expr, + sympy.logic.boolalg.Boolean, + int, + ir.EffectfulKernel, + ir.ShapeAsConstantBuffer, + TorchBindObject, + ir.OpaqueMultiOutput, + ir.OpaqueValueTypeConstant, + ir.OpaqueObjectState, + ), + ) + for x in result + ), result + + fx_node_args = V.graph.current_node.args[0] # type: ignore[arg-type] + if not isinstance(fx_node_args, (tuple, list)): + # nested subgraphs can have singleton outputs + fx_node_args = (fx_node_args,) + result = [ir.ExternKernel.realize_input(x) for x in result] + result_correct_strides = [] + + assert len(fx_node_args) == len(result) + for r, fx_node in zip(result, fx_node_args): + if not isinstance(r, (ir.TensorBox, ir.BaseView)): + result_correct_strides.append(r) + elif isinstance(r.get_output_spec(), ir.CommBufferLayout): + # Active references to persistent comm buffers are not allowed + # outside of graphs + result_correct_strides.append(ir.ExternKernel.copy_input(r)) + else: + # AOT Autograd tries to detect stride divergence of inductor from output metadata. + # Here, we try to avoid spurious divergence by matching insignificant strides such as + + # should have already been realized + assert torch._inductor.ir.is_storage_and_layout(r) + meta_strides = [ + s.node.expr if isinstance(s, torch.SymInt) else s + # pyrefly: ignore [missing-attribute] + for s in fx_node.meta["val"].stride() + ] + result_correct_strides.append( + ir.try_match_insignificant_strides(r, meta_strides) + ) + + self.graph_outputs = result_correct_strides + value: ir.IRNode + for name, value in self.graph_inputs.items(): + if isinstance( + value, + ( + TorchBindObject, + sympy.Basic, + torch._inductor.ir.GeneratorState, + torch._inductor.ir.OpaqueObjectState, + ), + ): + continue + assert isinstance(value, TensorBox), ( + f"Unsupported inductor graph input type: {type(value)}" + ) + value.realize() + assert isinstance(value, TensorBox) + value = value.data + assert isinstance(value, ir.StorageBox) + value_storage_box = value + value = value.data + if not isinstance(value, InputBuffer) or value.get_name() != name: + # one of our inputs was mutated, need to turn that into a copy + ir.MutationLayoutSHOULDREMOVE.realize_into( + value, self.graph_inputs_original[name] + ) + # replace output with mutated input + try: + ind = self.graph_outputs.index(value_storage_box) + self.graph_outputs[ind] = self.graph_inputs_original[name] + except ValueError: + pass + + self.finalize() + log.debug( + "Force channels last inputs for %d conv for the current graph with id %d", + self.num_channels_last_conv, + self.graph_id if self.graph_id is not None else -1, + ) + + def finalize(self) -> None: + for buf in self.buffers: + buf.decide_layout() + + @contextmanager + def set_current_node(self, node: torch.fx.Node): # type: ignore[no-untyped-def] + old = self.current_node + try: + self.current_node = node + yield + finally: + self.current_node = old + + @contextmanager + def set_current_wrapper_code(self) -> Iterator[None]: + old = self.wrapper_code + try: + yield + finally: + self.wrapper_code = old + + def propagate_mutation( + self, + fx_node: torch.fx.Node, + old_args: tuple[Any], + old_kwargs: dict[str, Any], + new_args: tuple[Any], + new_kwargs: dict[str, Any], + ) -> None: + """Propagate mutations on new_args/new_kwargs back to old_args/old_kwargs. + + Assumes we may have cloned old_args/old_kwargs into new_args/new_kwargs + and then called fx_node(*new_args, **new_kwargs). + + If fx_node mutates any of new_args/new_kwargs, and they are different from + old_args/old_kwargs, then we need to update the original tensor. + """ + assert len(old_args) == len(new_args) + assert len(old_kwargs) == len(new_kwargs) + + if fx_node.target is torch.ops.higher_order.triton_kernel_wrapper_mutation: + kwargs = fx_node.kwargs["kwargs"] + assert isinstance(kwargs, dict) + mutated = torch._higher_order_ops.triton_kernel_wrap.get_mutated_tensors( + old_kwargs["kernel_idx"], + old_kwargs["constant_args_idx"], + { + k: v.meta["val"] if isinstance(v, torch.fx.Node) else v + for k, v in kwargs.items() + }, + old_kwargs["tma_descriptor_metadata"], + ) + for name in mutated: + old_arg = old_kwargs["kwargs"][name] + new_arg = new_kwargs["kwargs"][name] + if old_arg is new_arg: + continue + + self.call_function(torch.ops.aten.copy_.default, (old_arg, new_arg), {}) + return + + assert isinstance(fx_node.target, torch._ops.OpOverload) + + def maybe_propagate( + schema_arg: torch._C.Argument, old_arg: ir.IRNode, new_arg: ir.IRNode + ) -> None: + if old_arg is new_arg: + return + if schema_arg.alias_info is not None and schema_arg.alias_info.is_write: + # The lowering for copy_ is smart enough to "replace" old_arg with + # new_arg in all future uses so a copy_ kernel never gets emitted. + # old_arg, new_arg may be immutable_list + if isinstance(old_arg, ir.IRNode): + old_arg = (old_arg,) # type: ignore[assignment] + new_arg = (new_arg,) # type: ignore[assignment] + + for old_arg_item, new_arg_item in zip(old_arg, new_arg): # type: ignore[call-overload] + if old_arg_item is new_arg_item: + continue + self.call_function( + torch.ops.aten.copy_.default, (old_arg_item, new_arg_item), {} + ) + + schema = fx_node.target._schema + for idx, (old_arg, new_arg) in enumerate(zip(old_args, new_args)): + schema_arg = schema.arguments[idx] + maybe_propagate(schema_arg, old_arg, new_arg) + + schema_kwargs = {arg.name: arg for arg in schema.arguments} + + for key in old_kwargs: + old_arg = old_kwargs[key] + new_arg = new_kwargs[key] + schema_arg = schema_kwargs[key] + maybe_propagate(schema_arg, old_arg, new_arg) + + @staticmethod + def _get_node_stream(n: torch.fx.Node) -> int | None: + """Get the user-annotated stream index from FX node metadata.""" + return n.meta.get("custom", {}).get("stream") + + def _realize_inputs_at_stream_boundaries(self, n: torch.fx.Node) -> None: + """Realize IR inputs that are on a different stream. + + Without this, pointwise ops across stream boundaries would be inlined + into each other during lowering, making it impossible for the scheduler + to split them into separate kernels. + + None means the default stream, so it is compared like any other value. + """ + node_stream = self._get_node_stream(n) + for input_node in n.all_input_nodes: + input_stream = self._get_node_stream(input_node) + if input_stream == node_stream: + continue + ir_value = self.env.get(input_node) + if isinstance(ir_value, ir.TensorBox): + ir_value.realize() + + def run_node(self, n: torch.fx.Node) -> object: + """Lower and execute a single FX node into Inductor IR.""" + + def debug(msg: str) -> None: + log.debug("lowering %s %s", LazyString(n.format_node), msg) # type: ignore[arg-type] + + # Use channels-last stride order for certain + # dense 4D intermediates when layout optimization determines a + # downstream consumer (typically conv) prefers channels-last. + def maybe_apply_channels_last_stride_order( + result: ir.IRNode, n: torch.fx.Node + ) -> ir.IRNode: + dense = torch._prims_common.is_non_overlapping_and_dense_or_false( + n.meta["val"] + ) + strides = n.meta["val"].stride() + unbacked_symbols_in_strides = len(free_unbacked_symbols(strides)) > 0 + if ( + not unbacked_symbols_in_strides + and dense + and len(result.get_size()) == 4 + and n in self.nodes_prefer_channels_last + and not is_user_visible + and not is_input_for_as_strided + ): + result = ir.ExternKernel.require_stride_order( + result, + ir.get_stride_order( + make_channels_last_strides_for(n.meta["val"].shape) + ), + ) + return result + + from torch._inductor.compiler_bisector import CompilerBisector + + buffer_watermark = len(self.buffers) + operation_watermark = len(self.operations) + + # origins: OrderedSet[Union[Node, ir.IRNode]] = OrderedSet([n]) + origins: OrderedSet[Any] = OrderedSet([n]) + is_call_function = n.op == "call_function" + if is_call_function: + args, kwargs = self.fetch_args_kwargs_from_env(n) + origins |= gather_origins(args, kwargs) + self._realize_inputs_at_stream_boundaries(n) + with ( + ir.IRNode.current_origins(origins), + ir.IRNode.current_stream_idx(self._get_node_stream(n)), + self.set_current_node(n), + V.set_current_node(n), + ): + if ( + n.op == "call_function" + # this path only for built-in operators + and n.target + and isinstance(n.target, torch._ops.OpOverload) + and torch._library.utils.is_builtin(n.target) + and ( + fallback_node_due_to_unsupported_type(n) + or CompilerBisector.disable_subsystem( + "inductor", "lowerings", lambda: repr(n) + ) + ) + ): + debug("fallback_handler") + result = fallback_handler(n.target, add_to_fallback_set=False)( + *args, # type: ignore[possibly-undefined] + **kwargs, # type: ignore[possibly-undefined] + ) + elif ( + n.op == "call_function" + and isinstance( + n.target, (torch._ops.OpOverload, torch._ops.HigherOrderOperator) + ) + and should_fallback_by_default(n) + ): + # this path supports fallback due to inductor lite mode. It supports + # both OpOverload and HOPs (e.g., triton_kernel_wrapper_functional). + debug("fallback_handler") + result = fallback_handler(n.target, add_to_fallback_set=False)( + *args, # type: ignore[possibly-undefined] + **kwargs, # type: ignore[possibly-undefined] + ) + elif ( + n.op == "call_function" + and n.target is torch.ops.higher_order.triton_kernel_wrapper_mutation + and config.triton_kernel_default_layout_constraint != "flexible_layout" + ): + debug("user_defined_triton_kernel_layout_constraints") + if ( + config.triton_kernel_default_layout_constraint + == "needs_fixed_stride_order" + ): + old_args = args # type: ignore[possibly-undefined] + old_kwargs = kwargs # type: ignore[possibly-undefined] + + if eager_input_vals := n.meta.get("eager_input_vals"): + inp_args = eager_input_vals[0] + inp_kwargs = eager_input_vals[1] + args, kwargs = constrain_to_fake_tensors( + # pyrefly: ignore [unbound-name] + args, + # pyrefly: ignore [unbound-name] + kwargs, + inp_args, + inp_kwargs, + ) + else: + args, kwargs = constrain_to_fx_strides(n, *args, **kwargs) # type: ignore[index] + result = self.call_function(n.target, args, kwargs) # type: ignore[arg-type] + self.propagate_mutation(n, old_args, old_kwargs, args, kwargs) # type: ignore[possibly-undefined] + else: + raise RuntimeError( + f"Unknown triton_kernel_default_layout_constraint: {config.triton_kernel_default_layout_constraint}" + ) + elif is_magic_method(n.target): + # TODO: this is sus, it probably should be handled in the + # lowerings themselves similarly to sym_size/sym-stride + # https://github.com/pytorch/pytorch/issues/127789 + debug("is_magic_method") + if isinstance( + n.meta["val"], (torch.SymInt, torch.SymFloat, torch.SymBool) + ): + result = n.meta["val"].node.expr + else: + result = super().run_node(n) + else: + debug("") + result = super().run_node(n) + + # require the same stride order for dense outputs, + # 1. user-land view() will not throw because inductor + # output different strides than eager + # long term the solution is to make view() always succeed + # with infallible strides. + # 2: as_strided ops, we need make sure its input has same size/stride with + # eager model to align with eager behavior. + as_strided_ops = [ + torch.ops.aten.as_strided.default, + torch.ops.aten.as_strided_.default, + torch.ops.aten.as_strided_scatter.default, + torch.ops.aten.resize.default, + torch.ops.aten.resize_as.default, + ] + is_output = any(user.op == "output" for user in n.users) + is_user_visible = n in self.user_visible_output_strides + is_input_for_as_strided = any( + user.target in as_strided_ops for user in n.users + ) + + if n.meta.get("inductor_realize_to_strides", False) and isinstance( + result, TensorBox + ): + result.realize() + strides = n.meta["val"].stride() + sym_strides = torch._inductor.utils.any_is_symbolic(*strides) + if result.maybe_get_stride() != strides and not sym_strides: + stride_order = ir.get_stride_order(strides) + result = ir.ExternKernel.require_stride_order(result, stride_order) + if ( + is_output + and isinstance(result, TensorBox) + and isinstance(result.data, ir.BaseView) + ): + # Realize so that outputs are correctly aliased + result.realize() + + if (is_output or is_input_for_as_strided) and isinstance( + n.meta.get("val"), torch.Tensor + ): + if is_user_visible: + strides = self.user_visible_output_strides.get(n) + else: + strides = n.meta["val"].stride() + + if strides is not None and len(strides) > 0: + allow_padding = ( + config.pad_outputs or not is_user_visible + ) and not is_input_for_as_strided + dense = torch._prims_common.is_non_overlapping_and_dense_or_false( + n.meta["val"] + ) + unbacked_symbols_in_strides = ( + len(free_unbacked_symbols(strides)) > 0 + ) + if ( + not unbacked_symbols_in_strides + and dense + and len(result.get_size()) == 4 + and n in self.nodes_prefer_channels_last + and not is_user_visible + and not is_input_for_as_strided + ): + strides = ir.FlexibleLayout.stride_ordered_for_memory_format( + result.get_size(), torch.channels_last + ) + if not unbacked_symbols_in_strides and len(strides): + # To avoid converting possible view ops to a copy kernel, we use the previous + # require_exact_strides to handle views. But ultimately it's better to require + # the right strides at the tensor definition. + if n.meta["val"]._is_view() or isinstance( + result.data, + ir.BaseView, + ): + result = ir.ExternKernel.require_stride_order( + result, + ir.get_stride_order(strides), + allow_padding=allow_padding, + ) + else: + # Fix for 0-d tensors: if result size is empty, + # strides should also be empty + if len(result.get_size()) == 0 and len(strides) > 0: + strides = [] + result = ir.ExternKernel.require_exact_strides( + result, strides, allow_padding=allow_padding + ) + + # Realize if (1) any user need inputs realized, or (2) there is + # already too many reads and rematerializing can be bad. + num_users = len(OrderedSet(n.users)) + if num_users > 1 and isinstance(result, TensorBox): + for user in n.users: + if user.target in needs_realized_inputs: + result.realize_hint() + # This inclusion is somewhat controversial (from + # discussion between Horace, Natalia, and Elias). + # Currently, it's not very clear why this is helpful. + # The general idea here is that even though a node may + # have FlexibleLayout, we still often *treat* it as if + # it was contiguous. This appears to sometimes result in + # suboptimal behavior. + # + # When we do a better job selecting layout, we should + # revisit this. + need_fixed_layout = [ + torch.ops.aten.convolution_backward.default, + torch.ops.aten.mm.default, + torch.ops.aten._int_mm.default, + ] + need_fixed_channels_last_layout = [] + if not self.layout_opt: + need_fixed_layout.append(torch.ops.aten.convolution.default) + if torch._C._has_mkldnn: + need_fixed_layout += [ + torch.ops.mkldnn._linear_pointwise.default, + torch.ops.mkldnn._linear_pointwise.binary, + torch.ops.aten.mkldnn_rnn_layer.default, + torch.ops.onednn.qlinear_pointwise.default, + torch.ops.onednn.qlinear_pointwise.tensor, + torch.ops.onednn.qlinear_pointwise.binary, + torch.ops.onednn.qlinear_pointwise.binary_tensor, + ] + need_fixed_channels_last_layout += [ + torch.ops.mkldnn._convolution_pointwise.default, + torch.ops.mkldnn._convolution_pointwise.binary, + torch.ops.mkldnn._convolution_pointwise_.binary, + torch.ops.mkldnn._convolution_transpose_pointwise.default, + torch.ops.onednn.qconv_pointwise.default, + torch.ops.onednn.qconv2d_pointwise.binary, + ] + if torch._C.has_mkl: + need_fixed_layout += [torch.ops.mkl._mkl_linear.default] + if user.target in need_fixed_layout: + result = ir.ExternKernel.require_stride_order( + result, + ir.get_stride_order(n.meta["val"].stride()), + allow_padding=True, + ) + if ( + user.target in need_fixed_channels_last_layout + and n is user.args[0] + ): + result = ir.ExternKernel.require_stride_order( + result, + ir.get_stride_order( + make_channels_last_strides_for(n.meta["val"].shape) + ), + ) + if user.op == "output": + # pyrefly: ignore [missing-attribute] + if isinstance(result.data.data, (Pointwise, Reduction)): + # Cheap-to-recompute nodes (0 buffer reads, e.g. + # index arithmetic or constant fills) can be + # deferred to realize_input at output processing. + # This prevents cascade materialization where + # shared constants inflate downstream read counts. + if ( + config.delay_realize_cheap_outputs + # pyrefly: ignore [missing-attribute] + and result.data.num_reads() == 0 + # pyrefly: ignore [missing-attribute] + and not result.data.has_large_inner_fn() + ): + continue + result.realize() + + _data = result.data # type: ignore[attr-defined] + while not isinstance(_data, StorageBox) and isinstance( + _data, (ir.BaseView, ir.MutableBox) + ): + _data = _data.data + + if isinstance(_data, StorageBox) and _data.should_realize_on_reuse( + len(n.users) + ): + result = maybe_apply_channels_last_stride_order(result, n) + + # TODO(jansel): introduce a store vs inline choice + result.mark_reuse(len(n.users)) + + # Realize if the IRNode already has accumulated lots of reads + if isinstance(result, TensorBox) and result.has_exceeded_max_reads(): + # Prevent excessive accumulation in a computed buffer, when + # there are multiple branches each with small number of memory + # reads, but they converge to a user. + result = maybe_apply_channels_last_stride_order(result, n) + result.realize_hint() + + # Realize if a Pointwise has too much stuff to be inlined. + # As this may cause RecursionError during Inductor's evaluation. + if isinstance(result, TensorBox) and isinstance(result.data, StorageBox): + curr = result.data.data + if isinstance(curr, Pointwise): + # Use inner fn as a rough proxy. Good enough. + if curr.has_large_inner_fn(threshold=100): + result.realize() + + assign_origin_node(result, n) + self.register_users_of(result) + + new_unbacked_defs = OrderedSet[sympy.Symbol]() + for buf in self.buffers[buffer_watermark:]: + new_unbacked_defs |= buf.get_unbacked_symbol_defs() + for op in self.operations[operation_watermark:]: + new_unbacked_defs |= op.get_unbacked_symbol_defs() + + shape_env = V.graph.sizevars.shape_env + + # An input can be unbacked symint i.e.: when mark_unbacked is used. + # in that case add it to new_unbacked_defs. + if ( + n.op == "placeholder" + and isinstance(result, sympy.Symbol) + and shape_env.is_unbacked_symint(result) + ): + new_unbacked_defs.add(result) + + def format_new_defs() -> str: + r = [ + f"unbacked_symbol_defs={buf.get_unbacked_symbol_defs()} in:\n{buf}\n" + for buf in self.buffers[buffer_watermark:] + ] + r.extend( + f"unbacked_symbol_defs={op.get_unbacked_symbol_defs()} in:\n{op}\n" + for op in self.operations[operation_watermark:] + ) + return "***\n".join(r) + + # We do not skip unbacked symints that are input for backward see the note below. + if V.graph.is_backward and n.op == "placeholder": + return result + + # Note [Backwards runtime asserts] + # Backwards poses an interesting problem for deferred runtime + # asserts. In the easy case, we may solely close over data + # dependent sized tensors, and there are no binding sites for + # unbacked SymInts. In this case, we can just drop all the + # runtime asserts on the floor: no non-placeholder bindings, no + # problem. + # + # However, it is *possible* for a fresh runtime assert to show up + # between forwards and backwards. Right now, the freezing process + # that happens when we lower forwards means that we will freeze + # runtime asserts, and then the moment the backwards lowering + # process attempts to add a new deferred runtime assert, we will + # fail. Let's say you remove that assert. Now when we get here, + # we need to make sure we actually emit these asserts (because we + # can't emit them in forwards, we already compiled it). So we + # have to do something here. But we don't want to reemit ALL + # deferred runtime asserts, we only want to emit the NEW ones. + # Therefore needing some sort of stratification in the ShapeEnv. + # This is all doable, it just hasn't been done yet. + + unbacked_bindings = resolve_unbacked_bindings( + V.graph.sizevars.shape_env, n.meta.get("unbacked_bindings", {}) + ) + assert unbacked_bindings is not None + # When we do lowering, it is possible we reallocate unbacked SymInts. + # So we need to line up the unbacked SymInts when performing the test + # here + # + # In principle, we could permit lowering to introduce MORE unbacked + # SymInts: as long as all the old unbacked ones are accounted for, + # it's fine for inductor to introduce extra calls to item()/unbacked() + # whatever. This actually happens in practice when an unbacked SymInt + # gets memoized away; naively, when Inductor reprocesses a kernel, it + # doesn't know that the memo still applies, and ends up allocating a + # new symbol. However, this is generally a bad thing: we may still + # end up needing to test equalities on the symbols, and a fresh + # symbol is likely to hit lots of GuardOnDataDependent errors that + # we already know facts for. + renamed_unbacked_bindings = OrderedSet( + V.fake_mode.shape_env.unbacked_renamings.get(s, s) + for s in unbacked_bindings + ) + + assert new_unbacked_defs >= renamed_unbacked_bindings, ( + f"failed {new_unbacked_defs} >= {renamed_unbacked_bindings} (inductor >= fx)\n" + f"fx node is: {n.format_node()}\n" + f"new operations are:\n\n{format_new_defs()}" + ) + self.create_deferred_runtime_asserts(n, new_unbacked_defs) + return result + + def create_deferred_runtime_asserts( + self, n: torch.fx.Node, new_unbacked_defs: OrderedSet[sympy.Symbol] + ) -> None: + if config.do_not_emit_runtime_assertions: + return + # [NOTE] Codegen runtime asserts in Inductor + # + # We need to generate runtime asserts directly in Inductor instead + # of just reusing the asserts from input graphs because we reuse the + # same ShapeEnv as before. In particular, on subsequent graph passes, + # we would immediately turn all of these assertions into noops, + # because when we evaluated their expressions, we would see that + # because we had a deferred runtime assert in the ShapeEnv, we + # know "oh, of course this expression is True" already. + # One example is below: + # + # class Model(torch.nn.Module): + # def forward(self, a, b, c): + # nz = torch.nonzero(a) + # ones = a.new_ones([nz.size(0), b.size(0)]) + # torch._check(ones.size(0) >= 1) + # equals = torch.add(ones, c) + # return equals + # torch._dynamo.mark_dynamic(c, 0) + # When we reuse the ShapeEnv in Inductor lowering, the check that checks + # a and nonzero have the same shape would be evaluated to True after we resolve + # unbacked bindings using the ShapeEnv. + # See test_unbacked_equals_input_size_runtime_assertion in test_aot_inductor. + # + # + # In addition to the Inductor generated runtime asserts, we also + # need the runtime asserts from the input graph, because some derived + # runtime asserts on backed symints are not generated in Inductor. One example is + # this: `y = x.reshape(100, -1).clone()`. x.shape[0] needs to be a multiple of 100. + # See test_aoti_runtime_asserts_backed_symint in test_aot_inductor. + + def make_assert(expr: SympyBoolean, msg: str) -> None: + assert_op = ir.AssertScalar(expr, msg) + self.register_buffer(assert_op, set_name=True) + self.register_operation(assert_op) + + if ( + full_aoti_runtime_assert() + and n.target is torch.ops.aten._assert_scalar.default + and self.aot_mode + ): + node_args, _ = self.fetch_args_kwargs_from_env(n) + if node_args[0] != True: # noqa: E712 + make_assert(node_args[0], f"{node_args[0]} to be True") + else: + # bound_unbacked_symbols tracks the symbols that are created so far, + # we use it to make sure that runtime assertions are added after all + # symbols used in them are defined. + self.bound_unbacked_symbols |= new_unbacked_defs + + shape_env = V.graph.sizevars.shape_env + + # Emit code for runtime asserts that can be inserted at this point. + for i0 in new_unbacked_defs: + ras = self.ras_by_symbol.pop(i0, []) + # NB: size-like not needed, we won't retrace + vr = shape_env.var_to_range[i0] + if not shape_env._default_unspecified_value_range().issubset(vr): + + def is_convertible(s: Expr) -> bool: + if s in (int_oo, -int_oo): + return False + try: + int(s) + return True + except TypeError: + return False + + if is_convertible(vr.lower): + make_assert(i0 >= vr.lower, f"{i0} >= {vr.lower}") + if is_convertible(vr.upper): + make_assert(i0 <= vr.upper, f"{i0} <= {vr.upper}") + + for ra in ras: + fvs = free_unbacked_symbols(ra.expr) + missing = fvs - self.bound_unbacked_symbols + if missing: + i1 = min(missing, key=str) + self.ras_by_symbol.setdefault(i1, []).append(ra) + else: + make_assert(ra.expr, f"{ra.expr}") + + def validate_can_generate_cpp_wrapper(self) -> None: + if config.disable_cpp_codegen: + raise CppWrapperCodegenError("C++ codegen is disabled") + + if sys.platform not in ("linux", "darwin", "win32"): + raise CppWrapperCodegenError(f"Unsupported platform {sys.platform}") + + def init_wrapper_code( + self, + is_subgraph: bool = False, + subgraph_name: str | None = None, + parent_wrapper_code: PythonWrapperCodegen | None = None, + partition_signatures: GraphPartitionSignature | None = None, + ) -> None: + device_types = self.device_types.copy() + device_types.discard("cpu") + device_types.discard("meta") + # TODO(Eikan): Only support mixing cpu and other device now. + assert len(device_types) <= 1, "Does not support mixing {}".format( + "+".join(device_types) + ) + only_cpu = len(device_types) == 0 + self.device_type = "cpu" if only_cpu else device_types.pop() + + if self.cpp_wrapper: + self.validate_can_generate_cpp_wrapper() + + self.device_ops = get_device_op_overrides(self.device_type) + wrapper_code_gen_cls = get_wrapper_codegen_for_device( + self.device_type, self.cpp_wrapper, self.fx_wrapper + ) + assert wrapper_code_gen_cls is not None, ( + f"Device {self.device_type} not supported" + ) + self.wrapper_code = wrapper_code_gen_cls.create( + is_subgraph, + subgraph_name, + parent_wrapper_code, + partition_signatures, + ) + + if self.const_module: + self.wrapper_code._names_iter = self.const_module.wrapper_code._names_iter + + def extract_autotune_inputs( + self, example_inputs: list[int | float | torch.Tensor] + ) -> None: + import copy + + cloned_gm = copy.deepcopy(self.orig_gm) + example_inputs = copy.deepcopy(example_inputs) + triton_nodes = [] + for node in cloned_gm.graph.nodes: + if ( + node.op == "call_function" + and node.target is torch.ops.higher_order.triton_kernel_wrapper_mutation + ): + triton_nodes.append(node) + + # Store grid related nodes + grid_inputs: list[torch.fx.Node] = [] + visited_grids: dict[torch.fx.Node, int] = {} + # Store kwargs related nodes + triton_inputs: dict[str, Any] = {} + kwargs_inputs: list[torch.fx.Node] = [] + visited_kwargs: dict[Any, int] = {} + for node in triton_nodes: + # first check whether we have fx node in grid settings. + for grid in node.kwargs["grid"]: + for val in grid: + if val in visited_grids: + continue + + if isinstance(val, torch.fx.Node): + visited_grids[val] = len(grid_inputs) + grid_inputs.append(val) + + kwargs = node.kwargs["kwargs"] + # identify which args might be mutated, those should be cloned. + mutated = torch._higher_order_ops.triton_kernel_wrap.get_mutated_tensors( + node.kwargs["kernel_idx"], + node.kwargs["constant_args_idx"], + { + k: v.meta["val"] if isinstance(v, torch.fx.Node) else v + for k, v in kwargs.items() + }, + node.kwargs["tma_descriptor_metadata"], + ) + + new_kwargs: dict[str, int] = {} + with cloned_gm.graph.inserting_before(node): + for k, v in kwargs.items(): + if k in mutated: + new_node = cloned_gm.graph.call_function(torch.clone, args=(v,)) + new_kwargs[k] = len(kwargs_inputs) + kwargs_inputs.append(new_node) + continue + + if v in visited_kwargs: + new_kwargs[k] = visited_kwargs[v] + continue + visited_kwargs[v] = len(kwargs_inputs) + kwargs_inputs.append(v) + new_kwargs[k] = visited_kwargs[v] + triton_inputs[node.name] = new_kwargs + + new_outputs = kwargs_inputs + grid_inputs + for node in cloned_gm.graph.nodes: + if node.op == "output": + node.args = (tuple(new_outputs),) + break + + cloned_gm.recompile() + runner = torch.fx.Interpreter(cloned_gm) + returned_outputs = runner.run(example_inputs) + # Extract and store the grid for autotuning + if len(grid_inputs) > 0: + grid_outputs = returned_outputs[len(kwargs_inputs) :] + self.autotuning_grids = {} + for node in triton_nodes: + dynamic_grid = False + new_grids: list[tuple[Any]] = [] + for grid in node.kwargs["grid"]: + new_grid = [] + for val in grid: + if not isinstance(val, torch.fx.Node): + new_grid.append(val) + continue + dynamic_grid = True + new_grid.append(grid_outputs[visited_grids[val]]) + # pyrefly: ignore [bad-argument-type] + new_grids.append(tuple(new_grid)) + + if dynamic_grid: + self.autotuning_grids[node.name] = new_grids + # Store the kwargs input for autotuning + self.autotuning_inputs = returned_outputs[: len(kwargs_inputs)] + self.autotuning_mapping = triton_inputs + + def codegen_with_cpp_wrapper( + self, + ) -> tuple[ValueWithLineMap, ValueWithLineMap]: + """ + For GPU, Triton kernels are autotuned and stored as cubin files + """ + if any(device in self.device_types for device in ["cuda", "xpu"]): + + def extract_real_inputs() -> list[int | float | torch.Tensor]: + def materialize( + x: torch.SymInt | torch.SymFloat | torch.Tensor, + ) -> int | float | torch.Tensor: + if x is None: + # pyrefly: ignore [bad-return] + return None + elif isinstance(x, (torch.SymInt, torch.SymFloat)): + # Need concrete value to run dynamic shapes and tune the result + return x.node.hint + elif isinstance(x, FakeTensor): + return defake(x) + else: + assert isinstance(x, torch.Tensor), ( + "Unknown type when creating real inputs" + str(type(x)) + ) + return x + + tracing_context = torch._guards.TracingContext.try_get() + if tracing_context is not None and not isinstance( + V.real_inputs, NullHandler + ): + if tracing_context.output_strides: + tracing_context.output_strides.clear() + + params_flat = [ + param + for param in tracing_context.params_flat # type: ignore[union-attr] + if param is not None + ] + real_inputs = [ + materialize(x) + for x in itertools.chain(params_flat, V.real_inputs) + ] + else: + # In the backward pass, V.real_inputs is not OrderedSet. + # Generating random inputs based on self.example_inputs sometimes can be problematic, + # e.g. illegal memory access. A comprehensive fix is to autotune in a separate process. + real_inputs = [ + materialize(x) # type:ignore[arg-type] + for x in ( + self.example_inputs # type:ignore[union-attr] + if isinstance(V.real_inputs, NullHandler) + else V.real_inputs + ) + ] + + if self.mutated_inputs: + from .compile_fx import clone_preserve_strides + + mutated_input_idxs = [ + idx + for idx, name in enumerate(self.graph_inputs) + if name in self.mutated_inputs + and isinstance(real_inputs[idx], torch.Tensor) + ] + for idx in mutated_input_idxs: + # clone mutated Tensor inputs to avoid mutating them in + # the first pass of the CPP wrapper-based compilation, as + # this will lead to a side effect on the example inputs: + # e.g. if torch.compile(f)(x) if called on input-mutating + # f, the inputs x will be mutated twice in the process: + # once here, and again when running the compiled model; + # this will also lead to a numerically incorrect output + mutated_inp = real_inputs[idx] + assert isinstance(mutated_inp, torch.Tensor) + real_inputs[idx] = clone_preserve_strides(mutated_inp) + del mutated_inp + return real_inputs + + if config.triton.autotune_at_compile_time: + # If autotune_at_compile_time is True, we can do the codegen in one-pass + # We will construct the autotuning values if user defined kernel exists. + if config.triton.autotune_with_sample_inputs: + user_defined_kernels = False + for op in self.operations: + if isinstance(op, ir.UserDefinedTritonKernel): + user_defined_kernels = True + break + if user_defined_kernels: + real_inputs = extract_real_inputs() + self.extract_autotune_inputs(real_inputs) + save_triton_kernel_perf_artifact(self) + return self.codegen() + else: + if not self.aot_mode: + # Lazy kernel compilation does not require two passes + # TODO: need to consolidate the logic between AOT and JIT + return self.codegen() + + # first pass + self.cpp_wrapper = False + compiled = self.compile_to_module().call + + real_inputs = extract_real_inputs() + with torch.utils._python_dispatch._disable_current_modes(): + compiled(real_inputs) + del real_inputs + + # second pass + self.cpp_wrapper = True + self.removed_buffers.clear() + self.removed_operations.clear() + self.inplaced_to_remove.clear() + V.graph.sizevars.precomputed_replacements.clear() + V.graph.sizevars.inv_precomputed_replacements.clear() + metrics.reset() + with config.patch({"triton.autotune_at_compile_time": False}): + return self.codegen() + else: + # cpu + return self.codegen() + + def _update_scheduler(self) -> None: + """ + (Re)initializes the scheduler member. When initializing the scheduler, no CUBIN + files should be generated (to avoid biasing any benchmarks and pessimizing + fusion decisions). + """ + from .scheduler import Scheduler + + with config.patch("triton.store_cubin", False): + self.scheduler = Scheduler(self.operations) + + def codegen(self) -> tuple[ValueWithLineMap, ValueWithLineMap]: + with dynamo_timed("GraphLowering.codegen", log_pt2_compile_event=True): + self.init_wrapper_code() + + self._update_scheduler() + V.debug.draw_orig_fx_graph(self.orig_gm, self.scheduler.nodes) + + self.wrapper_code.push_codegened_graph(self) + self.scheduler.codegen() + + log.debug( + "Finished codegen for all nodes. The list of kernel names available: %s", + V.graph.all_codegen_kernel_names, + ) + + result = self.wrapper_code.generate(self.is_inference) + self.wrapper_code.pop_codegened_graph() + return result + + def codegen_subgraph(self, parent_graph: GraphLowering) -> None: + """ + This is a more compact version of the `codegen()` above + where we codegen this graph as a subgraph of some parent + graph. The parent graph is passed as an argument: the + intention is to inline codegening of the subgraph in + the parent graph's wrapper code (including the generated + kernels). The wrapper code is not finalized (via `.generate()` + call), as this will be done in the parent graph's `codegen()`. + """ + with dynamo_timed("GraphLowering.codegen_subgraph", log_pt2_compile_event=True): + self.wrapper_code = parent_graph.wrapper_code + self.device_ops = parent_graph.device_ops + self.cpp_wrapper = parent_graph.cpp_wrapper + self.device_types = parent_graph.device_types + self.device_idxs = parent_graph.device_idxs + self.device_type = parent_graph.device_type + + self._update_scheduler() + self.scheduler.codegen() + + def count_bytes( + self, + ) -> tuple[ + int, list[tuple[BaseSchedulerNode, int]], list[tuple[BaseSchedulerNode, float]] + ]: + total_bytes = 0 + node_counts = [] + node_runtimes = [] + for node in self.scheduler.nodes: + num_bytes = node.get_read_write_buffers_sizes() + total_bytes += num_bytes + node_counts.append((node, num_bytes // 4)) + node_runtimes.append((node, node.get_estimated_runtime())) + + return total_bytes, node_counts, node_runtimes + + # No-op to be patched for unit tests + save_output_code: Callable[[str], None] | None = None + + def compile_to_module(self) -> CompiledModule: + with dynamo_timed( + "GraphLowering.compile_to_module", + phase_name="code_gen", + log_pt2_compile_event=True, + dynamo_compile_column_us="inductor_code_gen_cumulative_compile_time_us", + ): + return self._compile_to_module() + + def _compile_to_module(self) -> CompiledModule: + # If we're here, we don't have to worry about the kernel code, which is only + # returned separately in AOTInductor mode. + wrapper_code, _ = ( + self.codegen_with_cpp_wrapper() if self.cpp_wrapper else self.codegen() + ) + + if isinstance(wrapper_code, ValueWithLineMap): + mod = self._compile_to_module_lines(wrapper_code) + elif isinstance(wrapper_code, FileBackedGraphModule): + mod = wrapper_code + else: + raise NotImplementedError( + f"Unrecognized wrapper code type: {type(wrapper_code)}" + ) + + # Logged twice as per https://github.com/pytorch/pytorch/pull/99038#discussion_r1167826029 + # TODO. Revisit this once the logging API is more mature + assert mod.__file__ is not None + + log_module_code(mod.__file__) + log.debug("Output code written to: %s", mod.__file__) + output_code_log.info("Output code written to: %s", mod.__file__) + if config.benchmark_kernel: + print(f"Compiled module path: {mod.__file__}", file=sys.stderr) + if isinstance(wrapper_code, FileBackedGraphModule): + V.debug.output_code(mod.__file__) + V.debug.copy(os.path.splitext(mod.__file__)[0] + ".debug") + + return mod + + def _compile_to_module_lines( + self, wrapper_code: ValueWithLineMap + ) -> CompiledModule: + from .codecache import PyCodeCache + + if config.triton.autotune_at_compile_time: + # sanitize docstrings in kernel defs (#155006) + kernel_autotune_defs = self.wrapper_code.kernel_autotune_defs.getvalue() + kernel_autotune_defs = kernel_autotune_defs.replace('"""', '\\"\\"\\"') + + tuning_code = ( + 'r"""\n' + + "Compile-time auto-tuning block: \n" + + kernel_autotune_defs + + self.wrapper_code.kernel_autotune_calls.getvalue() + + '"""\n' + ) + wrapper_code.value = tuning_code + wrapper_code.value + if GraphLowering.save_output_code is not None: + GraphLowering.save_output_code(wrapper_code.value) + output_code_log.debug("Output code: \n%s", wrapper_code.value) + + inductor_meta = autotune_cache.inductor_meta_from_config() + AutotuneCacheBundler.begin_compile(inductor_meta, code=wrapper_code.value) + + try: + linemap = [ + (line_no, node.stack_trace) # type: ignore[attr-defined] + for line_no, node in wrapper_code.line_map + ] + key, path = PyCodeCache.write(wrapper_code.value) + output_code_log.debug("Output code written to: %s", path) + + V.debug.output_code(path) + V.debug.copy(os.path.splitext(path)[0] + ".debug") + except Exception: + trace_structured( + "inductor_output_code", + # Just omit the filename, I still want the code though! + payload_fn=lambda: wrapper_code.value, + ) + raise + else: + trace_structured( + "inductor_output_code", + lambda: { + "filename": path, + "file_path": os.path.abspath(path), + }, + payload_fn=lambda: wrapper_code.value, + ) + with dynamo_timed("PyCodeCache.load_by_key_path", log_pt2_compile_event=True): + mod = PyCodeCache.load_by_key_path( + key, + path, + linemap=linemap, # type: ignore[arg-type] + attrs={ + **self.constants, + **self.torchbind_constants, + **self.opaque_value_type_classes, + }, + ) + self.cache_key = key + self.cache_path = path + self.cache_linemap = linemap # type: ignore[assignment] + + if config.benchmark_harness and config.profile_bandwidth_output: + # run the inputs code gen to get the bandwidth info + args = mod.get_args() + mod.benchmark_compiled_module(args, times=1, repeat=1) + + return mod + + def _get_output_names(self, graph_outputs: list[ir.IRNode]) -> list[str]: + names = [] + shape_counter = itertools.count(0) + none_counter = itertools.count(0) + for node in graph_outputs: + if isinstance(node, ir.NoneAsConstantBuffer): + names.append(f"{self.name}_none{next(none_counter)}") + elif isinstance(node, ir.ShapeAsConstantBuffer): + names.append(f"{self.name}_shape{next(shape_counter)}") + else: + names.append(node.get_name()) + return names + + def get_output_names(self) -> list[str]: + return self._get_output_names(self.graph_outputs) + + def is_unspec_arg(self, name: str) -> bool: + # dynamo wraps unspec variable as 0d CPU tensor, + # need to convert to scalar during codegen (triton only) + return ( + name in self.graph_inputs + and self.graph_inputs[name].get_numel() == 1 + and len(self.graph_inputs[name].get_size()) == 0 + and get_device_type(self.graph_inputs[name]) == "cpu" + ) or name in self.zero_dim_cpu_tensor_list + + +class SubgraphLowering(GraphLowering): + """ + Mostly a helper class for the subgraph lowering. The main goal is to call + init_wrapper_code with the subgraph related arguments. + """ + + def __init__(self, parent: GraphLowering, *args: Any, **kwargs: Any) -> None: + self.parent = parent + super().__init__(*args, **kwargs) + + def init_wrapper_code( + self, + is_subgraph: bool = False, + subgraph_name: str | None = None, + parent_wrapper_code: PythonWrapperCodegen | None = None, + partition_signatures: GraphPartitionSignature | None = None, + ) -> None: + super().init_wrapper_code( + is_subgraph=True, + subgraph_name=self.name, + parent_wrapper_code=self.parent.wrapper_code, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/hooks.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/hooks.py new file mode 100644 index 0000000000000000000000000000000000000000..72a935fb5d272adc39e9bf5116f452d66addccdd --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/hooks.py @@ -0,0 +1,31 @@ +# mypy: allow-untyped-defs +import contextlib +from collections.abc import Callable +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + import torch + +# Executed in the order they're registered +INTERMEDIATE_HOOKS: list[Callable[[str, "torch.Tensor"], None]] = [] + + +@contextlib.contextmanager +def intermediate_hook(fn): + INTERMEDIATE_HOOKS.append(fn) + try: + yield + finally: + INTERMEDIATE_HOOKS.pop() + + +def run_intermediate_hooks(name, val): + global INTERMEDIATE_HOOKS + hooks = INTERMEDIATE_HOOKS + INTERMEDIATE_HOOKS = [] + try: + for hook in hooks: + hook(name, val) + finally: + INTERMEDIATE_HOOKS = hooks diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/index_propagation.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/index_propagation.py new file mode 100644 index 0000000000000000000000000000000000000000..8ae0435437dbd08e841fc725b56198b1ded84eaa --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/index_propagation.py @@ -0,0 +1,381 @@ +# mypy: allow-untyped-defs +"""This file implements the IndexPropagation ops handler, which wraps an +underlying handler to add a limited form of constant propagation, as well as +propagation of sympy expressions downstream of ops.index_expr calls. + +For example, say we have the IR: + + tmp0 = ops.index_expr(x, torch.int32) + tmp1 = ops.constant(2, torch.int32) + tmp2 = ops.mul(tmp0, tmp1) + tmp3 = ops.indirect_indexing(tmp2, x_size) + tmp4 = ops.load("buf0", tmp3) + +The underlying handler would just see: + + ops.load("buf0", x * 2) + +This is limited by the set of operators handled in the sympy expression +printers. So simple operations like minimum and maximum cannot be translated to +SymPy expressions yet, despite sympy.Min and sympy.Max existing. + +""" + +import itertools +from collections.abc import Sequence +from dataclasses import dataclass +from typing import Any, Literal, overload, TypeAlias + +import sympy + +import torch +from torch._prims_common import dtype_to_type, is_integer_dtype +from torch.utils._sympy.functions import FloorDiv, ModularIndexing, Where +from torch.utils._sympy.value_ranges import bound_sympy, ValueRanges + +from .ops_handler import DefaultHandler +from .sizevars import statically_known_true +from .utils import generate_assert +from .virtualized import V + + +_ExprType = sympy.Expr | float | int | bool + + +def _is_constant(val: _ExprType): + if isinstance(val, sympy.Basic): + return val.is_number + return isinstance(val, (int, float, bool)) + + +def upper_bound(val: _ExprType): + return bound_sympy(val).upper if isinstance(val, sympy.Expr) else val + + +@dataclass +class TypedExpr: + """A SymPy expression with associated type""" + + expr: _ExprType + dtype: torch.dtype + + def is_constant(self): + return _is_constant(self.expr) + + def __post_init__(self): + if _is_constant(self.expr): + expr = self.expr + if isinstance(expr, sympy.Expr): + expr = expr.expand(identity=True) + expr = dtype_to_type(self.dtype)(expr) + if is_integer_dtype(self.dtype): + bits = torch.iinfo(self.dtype).bits + if self.dtype.is_signed: + expr = expr + 2 ** (bits - 1) + expr = expr % 2**bits + if self.dtype.is_signed: + expr = expr - 2 ** (bits - 1) + self.expr = expr + + +class SymPyOps: + """An ops handler where all IR values are SymPy expressions + + When a value cannot be represented as a SymPy expression, the method is + either not defined, or returns NotImplemented + + """ + + @staticmethod + def identity(value: Any) -> Any: + return value + + @staticmethod + def constant(value: int | float | bool, dtype: torch.dtype) -> TypedExpr: + return TypedExpr(value, dtype) + + @staticmethod + def index_expr(value: sympy.Expr | int, dtype: torch.dtype) -> TypedExpr: + return TypedExpr(value, dtype) + + @staticmethod + def to_dtype( + value: TypedExpr, + dtype: torch.dtype, + src_dtype: torch.dtype | None = None, + use_compute_types: bool = False, + ) -> TypedExpr: + return TypedExpr(value.expr, dtype) + + @staticmethod + def abs(x: TypedExpr) -> TypedExpr: + return TypedExpr(abs(x.expr), x.dtype) # type: ignore[arg-type] + + @staticmethod + def square(x: TypedExpr) -> TypedExpr: + return TypedExpr(x.expr * x.expr, x.dtype) + + @staticmethod + def add(x: TypedExpr, y: TypedExpr) -> TypedExpr: + result_type = torch.promote_types(x.dtype, y.dtype) + return TypedExpr(x.expr + y.expr, result_type) + + @staticmethod + def sub(x: TypedExpr, y: TypedExpr) -> TypedExpr: + result_type = torch.promote_types(x.dtype, y.dtype) + return TypedExpr(x.expr - y.expr, result_type) + + @staticmethod + def mul(x: TypedExpr, y: TypedExpr) -> TypedExpr: + result_type = torch.promote_types(x.dtype, y.dtype) + return TypedExpr(x.expr * y.expr, result_type) + + @staticmethod + def neg(x: TypedExpr) -> TypedExpr: + return TypedExpr(-x.expr, x.dtype) + + @staticmethod + def floordiv(x: TypedExpr, y: TypedExpr) -> TypedExpr: + result_type = torch.promote_types(x.dtype, y.dtype) + if not is_integer_dtype(result_type): + return NotImplemented + + return TypedExpr(FloorDiv(x.expr, y.expr), result_type) + + @staticmethod + def mod(x: TypedExpr, y: TypedExpr) -> TypedExpr | None: + result_type = torch.promote_types(x.dtype, y.dtype) + if not is_integer_dtype(result_type): + return NotImplemented + + result_expr = ModularIndexing(x.expr, sympy.S.One, y.expr) + return TypedExpr(result_expr, result_type) + + @staticmethod + def remainder(x: TypedExpr, y: TypedExpr) -> TypedExpr | None: + result_type = torch.promote_types(x.dtype, y.dtype) + if not is_integer_dtype(result_type): + return NotImplemented + + x_expr = sympy.sympify(x.expr) + y_expr = sympy.sympify(y.expr) + # In these cases, remainder in Python == remainder in C++, so this transformation + # is sound + if ( + x_expr.is_nonnegative is not None + and x_expr.is_nonnegative == y_expr.is_positive + ): + result_expr = ModularIndexing(x.expr, sympy.S.One, y.expr) + return TypedExpr(result_expr, result_type) + return NotImplemented + + @staticmethod + def minimum(x: TypedExpr, y: TypedExpr) -> TypedExpr: + result_type = torch.promote_types(x.dtype, y.dtype) + return TypedExpr(sympy.Min(x.expr, y.expr), result_type) + + @staticmethod + def maximum(x: TypedExpr, y: TypedExpr) -> TypedExpr: + result_type = torch.promote_types(x.dtype, y.dtype) + return TypedExpr(sympy.Max(x.expr, y.expr), result_type) + + +@dataclass +class IndexPropVar: + value: Any # Either an IR value, or TypedExpr if is_symbolic is true + is_symbolic: bool = False + + @staticmethod + def new_symbolic(expr: TypedExpr) -> "IndexPropVar": + return IndexPropVar(expr, is_symbolic=True) + + def __post_init__(self): + assert not self.is_symbolic or isinstance(self.value, TypedExpr), ( + "Symbolic IndexPropVar must contain a TypedExpr" + ) + + +IndexPropResult: TypeAlias = IndexPropVar | tuple["IndexPropResult", ...] + + +class IndexPropagation(DefaultHandler): + """Ops wrapper that tries to propagate constant and index_expr values through the computation. + + This aims to maximize the compile time simplification possible, and convert + indirect indexing from arange into normal static indexing. + + """ + + def __init__( + self, + inner: Any, + iter_ranges: dict[sympy.Symbol, sympy.Expr], + indirect_var_ranges: dict[sympy.Symbol, sympy.Expr], + ) -> None: + self._inner = inner + self.shape_env = V.graph.sizevars.shape_env + + var_to_range = { + k: ValueRanges(0, upper_bound(v) - 1) for k, v in iter_ranges.items() + } + self.var_to_range = tuple( + itertools.chain(self.shape_env.var_to_range.items(), var_to_range.items()) + ) + # NOTE: this is intentionally kept as a reference so the caller can + # update it in-place + self.indirect_var_ranges = indirect_var_ranges + + axioms = [] + for x, s in iter_ranges.items(): + axioms.append(0 <= x) + axioms.append(x < s) + self.axioms = tuple(axioms) + self.shape_env.get_axioms() + + def materialize_expr(self, expr: sympy.Expr, dtype: torch.dtype) -> Any: + # Construct a new constant/index_expr from the SymPy expression + if _is_constant(expr): + val = dtype_to_type(dtype)(expr) + return self._inner.constant(val, dtype) + return self._inner.index_expr(expr, dtype) + + def unwrap(self, a: Any | IndexPropVar) -> Any: + if isinstance(a, (list, tuple)): + return tuple(self.unwrap(v) for v in a) + + if not isinstance(a, IndexPropVar): + return a + + # Prefer the sympy representation if possible + if a.is_symbolic: + return self.materialize_expr(a.value.expr, a.value.dtype) + + return a.value + + def wrap(self, a) -> IndexPropResult: + if isinstance(a, (list, tuple)): + return tuple(self.wrap(v) for v in a) + return IndexPropVar(a) + + @overload + def fallback( + self, + name: Literal["indirect_indexing"], + args: Sequence[Any], + kwargs: dict[str, Any], + ) -> IndexPropVar: ... + + @overload + def fallback( + self, name: str, args: Sequence[Any], kwargs: dict[str, Any] + ) -> IndexPropResult: ... + + def fallback( + self, name: str, args: Sequence[Any], kwargs: dict[str, Any] + ) -> IndexPropResult: + # Fallback to the wrapped handler + new_args = [self.unwrap(a) for a in args] + new_kwargs = {k: self.unwrap(v) for k, v in kwargs.items()} + return self.wrap(getattr(self._inner, name)(*new_args, **new_kwargs)) + + def propagate_sympy( + self, name: str, args: Sequence[Any], kwargs: dict[str, Any] + ) -> IndexPropResult: + # Build a new SymPy expression from this ops call + def unwrap(a: Any | IndexPropVar) -> Any: + if not isinstance(a, IndexPropVar): + return a + return a.value + + new_args = [unwrap(a) for a in args] + new_kwargs = {k: unwrap(v) for k, v in kwargs.items()} + new_expr = getattr(SymPyOps, name)(*new_args, **new_kwargs) + is_valid_expr = new_expr is not NotImplemented and ( + # Inductor doesn't expect floating point in sympy expressions, but + # allow floating point constants to be propagated + new_expr.is_constant() or new_expr.expr.is_integer + ) + if not is_valid_expr: + return self.fallback(name, args, kwargs) + return IndexPropVar.new_symbolic(new_expr) + + def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: + if not hasattr(SymPyOps, name): + return self.fallback(name, args, kwargs) + + var_arguments = [ + a + for a in itertools.chain(args, kwargs.values()) + if isinstance(a, IndexPropVar) + ] + if not all(v.is_symbolic for v in var_arguments): + return self.fallback(name, args, kwargs) + + return self.propagate_sympy(name, args, kwargs) + + def statically_true(self, e): + """ + Given some iter_ranges, return a function that given an expression, returns whether + it is true or false using value ranges, guard knowledge and runtime_asserts. + + FIXME I think this may not be entirely right, as we may not be able to use all runtime_asserts + If this is an issue, just use guards in `self.axioms`. + + The proper way of handling this would be to have a global shape_env that adds + runtime_asserts as they happen in the code. Then, it should be used in SimplifyIndexing + to perform wrap_expr and in CSEProxy.check_bounds to elide upper / lower bounds also + for indirect_indexing + """ + var_to_range = ( + *self.var_to_range, + *( + (k, ValueRanges(0, upper_bound(v) - 1)) + for k, v in self.indirect_var_ranges.items() + ), + ) + # pyrefly: ignore [bad-argument-type] + return statically_known_true(self.shape_env, e, self.axioms, var_to_range) + + def indirect_indexing( + self, + index: Any | IndexPropVar, + size: Any, + check: bool = True, + wrap_neg=True, + ) -> Any: + if isinstance(index, IndexPropVar) and index.is_symbolic: + # If we find something we can convert into a direct indexing we do so + # We still need to (perhaps) wrap the expression and add bound checks + # We want to do this "constant folding", as we don't allow to fuse + # kernels into indirect indexing + + expr = sympy.sympify(index.value.expr) + + # TODO Perhaps move this logic to the simplify indexing pass + def wrap_expr(expr): + # Positive, negative, mixed + if self.statically_true(0 <= expr): + return expr + elif self.statically_true(expr < 0): + return expr + size + else: + return Where(expr < 0, expr + size, expr) + + # Sometimes it's easier to prove 0 <= expr than the weaker -size <= expr + can_prove_lower = self.statically_true(0 <= expr) or self.statically_true( + -size <= expr + ) + can_prove_upper = self.statically_true(expr < size) + if wrap_neg: + expr = wrap_expr(expr) + if generate_assert(check): + self.fallback( + "check_bounds", + (expr, size), + dict(lower=not can_prove_lower, upper=not can_prove_upper), + ) + return expr + + indirect_var = self.fallback( + "indirect_indexing", (index, size, check, wrap_neg), {} + ).value + return indirect_var diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/inductor_prims.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/inductor_prims.py new file mode 100644 index 0000000000000000000000000000000000000000..785b5755962950ad58b7dd8f0ac749e49b9e2f03 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/inductor_prims.py @@ -0,0 +1,363 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import functools +import logging +import operator +from typing import TYPE_CHECKING + +import torch +from torch import _prims, Tensor +from torch._utils import _get_device_index + + +if TYPE_CHECKING: + from collections.abc import Sequence + + +log = logging.getLogger(__name__) + + +def make_prim( + schema: str, + impl_aten, + return_type=_prims.RETURN_TYPE.NEW, + doc: str = "", + tags: Sequence[torch.Tag] | None = None, +): + if isinstance(return_type, tuple): + + def meta(*args, **kwargs): + return tuple(_prims.TensorMeta(o) for o in impl_aten(*args, **kwargs)) + + else: + + def meta(*args, **kwargs): + return _prims.TensorMeta(impl_aten(*args, **kwargs)) + + return _prims._make_prim( + schema=schema, + return_type=return_type, + meta=meta, + impl_aten=impl_aten, + doc=doc, + tags=tags, + ) + + +def eager_force_stride(input_tensor: Tensor, stride) -> Tensor: + if input_tensor.stride() == stride: + return input_tensor + new_tensor = input_tensor.clone().as_strided( + input_tensor.shape, + stride, + ) + new_tensor.copy_(input_tensor) + return new_tensor + + +def eager_prepare_softmax(x: Tensor, dim: int) -> tuple[Tensor, Tensor]: + amax = torch.amax(x, dim, keepdim=True) + return amax, torch.sum(torch.exp(x - amax), dim, keepdim=True) + + +# Custom prims used for handling randomness +seed = make_prim( + "inductor_seed(Device device) -> Tensor", + lambda device: torch.randint(2**63 - 1, [], device=device), + doc="create a fresh seed (one per call) for use with inductor_rand", + tags=(torch.Tag.nondeterministic_seeded,), +) +seeds = make_prim( + "inductor_seeds(int count, Device device) -> Tensor", + lambda count, device: torch.randint(2**63 - 1, [count], device=device), + doc="Horizontal fusion of many inductor_seed() calls", + tags=(torch.Tag.nondeterministic_seeded,), +) +lookup_seed = make_prim( + # if inductor_lookup_seed changes, update partitioners.py + "inductor_lookup_seed(Tensor seeds, int index) -> Tensor", + lambda seeds, index: seeds[index].clone(), + doc="Extract a single seed from the result of inductor_seeds()", +) +# inductor_random() doesn't accept a dtype. +# instead, its lowering always burns in float32, and conversions to a different type +# are explicit in the graph. We therefore need this impl (used during tracing) to hardcoded +# the dtype, so it always faithfully produces a float32 tensor during tracing, +# even if the default dtype is set to something else. +random = make_prim( + "inductor_random(SymInt[] size, Tensor seed, str mode, *, ScalarType? align_dtype=None) -> Tensor", + lambda size, seed, mode, *, align_dtype=None: getattr(torch, mode)( + size, device=seed.device, dtype=torch.float32 + ), + doc="torch.rand()/torch.randn() using backend-specific RNG that can be fused", +) +randint = make_prim( + "inductor_randint(SymInt low, SymInt high, SymInt[] size, Tensor seed) -> Tensor", + lambda low, high, size, seed: torch.randint(low, high, size, device=seed.device), + doc="torch.randint() using backend-specific RNG that can be fused", +) + + +def _reserve_rng_state(device: torch.device, used_offset): + """ + Reserve `used_offset` 32-bit Philox samples on the given CUDA device and + return (seed, base), where base is in Philox-4x32 units. + + This mirrors how Inductor accounts for Philox consumption so compiled + dropout kernels can reconstruct eager RNG state. + """ + dev = device if isinstance(device, torch.device) else torch.device(device) + if dev.type != "cuda": + # Only CUDA devices have Philox-based CUDAGenerator. For non-CUDA + # devices this prim should be dead code and never actually run. + return 0, 0 + + dev_index = _get_device_index(dev, optional=True) + if dev_index is None: + dev_index = torch.cuda.current_device() + + gen = torch.cuda.default_generators[dev_index] + seed_t, off_t, intra_t = torch.ops.inductor_prims.inductor_reserve_rng_state( + gen, used_offset + ) + + # NOTE: for correctness in eager, intra_t should be 0. + # Keep everything as tensor math to avoid host sync. + if intra_t.device.type != off_t.device.type: + intra = int(intra_t.item()) + base = torch.div(off_t + intra, 4, rounding_mode="floor") + else: + base = torch.div(off_t + intra_t, 4, rounding_mode="floor") + return seed_t, base + + +def _rand_eager_offset_impl(offset, device: torch.device) -> Tensor: + """ + Reserve `offset` 32-bit Philox samples and return a 1-element int64 tensor + Place-holder: will be replaced by rand_eager_offsets + In fx_passes/replace_random.py + fuse_offset_creation_pass() + """ + return torch.empty(2, dtype=torch.int64, device=device) + + +def _rand_eager_offsets_impl(offsets, device: torch.device) -> Tensor: + """ + Batched version of _rand_eager_offset_impl. For each entry in `offsets`, + reserve that many 32-bit Philox samples and return a 1D int64 tensor + containing the packed (seed, base) values for each reservation. + """ + states = [_reserve_rng_state(device, int(off)) for off in offsets] + seeds = [s for s, _ in states] + bases = [b for _, b in states] + + def _to_i64(x): + if isinstance(x, torch.Tensor): + return x + return torch.as_tensor(x, device=device, dtype=torch.int64) + + seeds_tensor = torch.stack([_to_i64(x) for x in seeds]).view(-1) + bases_tensor = torch.stack([_to_i64(x) for x in bases]).view(-1) + packed = torch.stack([seeds_tensor, bases_tensor], dim=1) + return packed + + +def _rand_eager_offsets_meta(offsets, device: torch.device): + return torch.empty((len(offsets), 2), dtype=torch.int64, device=device) + + +rand_eager_offset = make_prim( + "inductor_rand_eager_offset(SymInt offset, Device device) -> Tensor", + _rand_eager_offset_impl, + doc=( + "Reserve `offset` 32-bit Philox samples on `device` and return a " + "1-element int64 tensor containing packed (seed, base)." + ), + tags=(torch.Tag.nondeterministic_seeded,), +) + + +rand_eager_offsets = _prims._make_prim( + schema="inductor_rand_eager_offsets(SymInt[] offsets, Device device) -> Tensor", + return_type=_prims.RETURN_TYPE.NEW, + meta=_rand_eager_offsets_meta, + impl_aten=_rand_eager_offsets_impl, + doc=( + "Batched version of inductor_rand_eager_offset. For each entry in " + "`offsets`, reserves that many 64-bit Philox samples and returns " + "packed (seed, base) values." + ), + tags=(torch.Tag.nondeterministic_seeded,), +) + + +force_stride_order = make_prim( + "inductor_force_stride_order(Tensor input, SymInt[] stride) -> Tensor", + eager_force_stride, + doc="Force the stride order for input tensor. No-op if the input tensor already has the stride. Do a copy otherwise", +) +_unsafe_index_put_ = make_prim( + "_unsafe_index_put_(Tensor(a!) self, Tensor?[] indices, Tensor values, bool accumulate=False) -> Tensor(a!)", + lambda self, indices, values, accumulate=False: torch.ops.aten.index_put_( + self, indices, values, accumulate + ), + doc="Unsafe index_put_ (doesn't issue device asserts)", +) +fma = make_prim( + "fma(Tensor a, Tensor b, Tensor c) -> Tensor", + lambda a, b, c: (a * b) + c, + doc="Fused multiply add: fma(a, b, c) -> (a * b) + c without rounding after the multiplication", + tags=(torch.Tag.pointwise,), +) + +# Register DTensor sharding strategies for inductor prims ops. +# This must happen here (not in _pointwise_ops.py) because the ops +# don't exist until make_prim() is called above. +if torch.distributed.is_available(): + from torch.distributed.tensor._ops._pointwise_ops import register_inductor_prims + + register_inductor_prims() + +prepare_softmax_online = make_prim( + "prepare_softmax_online(Tensor a, int dim) -> (Tensor, Tensor)", + eager_prepare_softmax, + return_type=(_prims.RETURN_TYPE.NEW, _prims.RETURN_TYPE.NEW), + doc="Prepare the softmax by computing the max and sum.", +) + + +def _flattened_index_to_nd(indices, width): + import sympy + + from torch.utils._sympy.functions import FloorDiv + + dim = len(width) + + if dim == 1: + return [indices] + elif dim >= 2: + m = functools.reduce(operator.mul, width[1:]) + if isinstance(indices, sympy.Expr) or isinstance(m, sympy.Expr): + ih = FloorDiv(indices, m) + else: + ih = indices // m + indices_new = indices - (ih * m) + return [ih, *_flattened_index_to_nd(indices_new, width[1:])] + else: + raise ValueError(f"Unknown dim: {dim}") + + +def _flatten_index(indices, width): + result = indices[0] + for d in range(1, len(indices)): + result = width[d] * result + indices[d] + return result + + +def _low_memory_max_pool_with_offsets_aten( + self, + kernel_size, + stride, + padding, + dilation, + ceil_mode, +): + dim = len(kernel_size) + if dim == 2: + vals, indices = torch.ops.aten.max_pool2d_with_indices( + self, kernel_size, stride, padding, dilation, ceil_mode + ) + else: + vals, indices = torch.ops.aten.max_pool3d_with_indices( + self, kernel_size, stride, padding, dilation, ceil_mode + ) + + idhw = _flattened_index_to_nd(indices, self.shape[-dim:]) + + dhw_inc = [] + + for d in range(dim): + bh_shape = [1] * self.ndim + bh_shape[-dim + d] = -1 + bh = torch.arange( + indices.shape[-dim + d], dtype=torch.int64, device=self.device + ).view(bh_shape) + hbase = bh * stride[d] - padding[d] + h_inc = (idhw[d] - hbase) // dilation[d] + dhw_inc.append(h_inc) + + offsets = _flatten_index(dhw_inc, kernel_size) + + return vals, offsets.to(torch.int8) + + +def _low_memory_max_pool_offsets_to_indices_aten( + offsets, + kernel_size, + input_size, + stride, + padding, + dilation, +): + dim = len(kernel_size) + offsets = offsets.to(torch.int64) + dhw_inc = _flattened_index_to_nd(offsets, kernel_size) + + idhw = [] + for d in range(dim): + bh_shape = [1] * offsets.ndim + bh_shape[-dim + d] = -1 + bh = torch.arange( + offsets.shape[-dim + d], dtype=torch.int64, device=offsets.device + ).view(bh_shape) + hbase = bh * stride[d] - padding[d] + idhw.append(hbase + dhw_inc[d] * dilation[d]) + + return _flatten_index(idhw, input_size) + + +_low_memory_max_pool_with_offsets = make_prim( + "_low_memory_max_pool_with_offsets(Tensor self, SymInt[] kernel_size, SymInt[] stride, SymInt[] padding, SymInt[] dilation, bool ceil_mode) -> (Tensor, Tensor)", # noqa: B950 + _low_memory_max_pool_with_offsets_aten, + return_type=(_prims.RETURN_TYPE.NEW, _prims.RETURN_TYPE.NEW), + doc="Instead of returning indices, returns indices offsets.", +) + +_low_memory_max_pool_offsets_to_indices = make_prim( + "_low_memory_max_pool_offsets_to_indices(Tensor self, SymInt[] kernel_size, SymInt[] input_size, SymInt[] stride, SymInt[] padding, SymInt[] dilation) -> Tensor", # noqa: B950 + _low_memory_max_pool_offsets_to_indices_aten, + doc="Convert small int offsets to regular indices.", +) + + +def _cvt_e8m0_rceil_aten(inp: Tensor) -> Tensor: + """ + Convert float to e8m0 format with ceiling rounding and satfinite semantics. + + e8m0 format: 8-bit biased exponent (bias=127), no mantissa. + For MX format scaling, this extracts the exponent with ceiling rounding. + Uses satfinite semantics: inf is saturated to 254 (max finite e8m0). + Accepts float32, float16, or bfloat16 (upcasted to float32 internally). + """ + if inp.dtype not in (torch.float32, torch.float16, torch.bfloat16): + raise ValueError( + f"cvt_e8m0_rceil requires float32, float16, or bfloat16 input, got {inp.dtype}" + ) + if inp.dtype != torch.float32: + inp = inp.to(torch.float32) + inp_bits = inp.view(torch.int32) + biased_exp = (inp_bits >> 23) & 0xFF + mantissa = inp_bits & 0x7FFFFF + needs_round_up = mantissa != 0 + e8m0_biased = biased_exp + needs_round_up.to(torch.int32) + # satfinite: clamp to max finite e8m0 value (254), not 255 (inf/nan) + e8m0_biased = torch.clamp(e8m0_biased, 0, 254) + return e8m0_biased.to(torch.uint8) + + +cvt_e8m0_rceil = make_prim( + "inductor_cvt_e8m0_rceil(Tensor input) -> Tensor", + _cvt_e8m0_rceil_aten, + doc="Convert float to e8m0 with ceiling rounding. Uses PTX cvt.rp.satfinite.ue8m0x2.f32 on SM100+.", +) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/invert_expr_analysis.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/invert_expr_analysis.py new file mode 100644 index 0000000000000000000000000000000000000000..904f073f4509049dd4ca3756df8db1feff930657 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/invert_expr_analysis.py @@ -0,0 +1,205 @@ +from dataclasses import dataclass + +import sympy + +from torch._inductor.utils import _IntLike, argsort_sym +from torch.utils._sympy.functions import FloorDiv, ModularIndexing + +from .virtualized import V + + +def static_eq(a: _IntLike, b: _IntLike) -> bool: + return V.graph.sizevars.statically_known_equals(a, b) + + +@dataclass +class Term: + coefficient: _IntLike + range: _IntLike | None # None for unbounded + original_expr: sympy.Expr + reconstruction_multiplier: _IntLike # The multiplier needed for reconstruction + + +def generate_inverse_formula(expr: sympy.Expr, var: sympy.Symbol) -> sympy.Expr | None: + """ + Analyze an expression to see if it matches a specific invertible pattern that we + know how to reverse. + + We're looking for expressions that are sums of terms where each term extracts a + distinct bounded range from the input variable, like: + + y = c₀*a₀ + c₁*a₁ + c₂*a₂ + ... + cₙ*aₙ + + where each aᵢ must be one of these specific patterns: + - ModularIndexing(var, divisor, modulo) + - FloorDiv(ModularIndexing(var, 1, modulo), divisor) + - FloorDiv(var, divisor) + - var (the variable itself) + + The key pattern we need is: + - Coefficients are strictly decreasing: c₀ > c₁ > c₂ > ... > cₙ + - Each coefficient matches the product of ranges of later terms (mixed-radix property) + - Each term extracts a bounded range, creating non-overlapping "slots" + + If we find this pattern, we can generate the reconstruction transformation that + decomposes the variable and rebuilds it using the correct multipliers. + + EXAMPLE: + Input: 100*((p//100)) + 10*((p%100)//10) + (p%10) + + Returns the reconstruction expression: + remainder₀ = p + component₀ = remainder₀ // 100 # hundreds digit + remainder₁ = remainder₀ % 100 + component₁ = remainder₁ // 10 # tens digit + remainder₂ = remainder₁ % 10 + component₂ = remainder₂ # ones digit + result = component₀*100 + component₁*10 + component₂*1 + + This decomposes p into its components and rebuilds it using the original + multipliers, which should equal the input expression. + + Args: + expr: Expression to analyze (sum of terms with ModularIndexing, FloorDiv, etc.) + var: The variable being decomposed + + Returns: + None if not invertible, or the reconstruction expression + + References: + Mixed-radix systems: https://en.wikipedia.org/wiki/Mixed_radix + """ + # Step 1: Parse all terms + terms = parse_terms(expr, var) + if not terms: + return None + + # Step 2: Sort by coefficient (descending) + coeffs = [t.coefficient for t in terms] + idxs = reversed(argsort_sym(V.graph.sizevars.shape_env, coeffs)) + terms = [terms[i] for i in idxs] + + # Step 3: Check invertibility conditions + if not check_invertibility(terms): + return None + + return generate_reconstruction_expr(terms, var) + + +def parse_terms(expr: sympy.Expr, var: sympy.Symbol) -> list[Term] | None: + """Parse expression into terms.""" + if not isinstance(expr, sympy.Add): + # Single term + term = parse_single_term(expr, var) + return [term] if term else [] + + terms = [] + for arg in expr.args: + term = parse_single_term(arg, var) + if term: + terms.append(term) + else: + return None # If any term fails to parse, fail completely + + return terms + + +def parse_single_term(term: sympy.Expr, var: sympy.Symbol) -> Term | None: + """Parse a single term and extract coefficient, range, and reconstruction multiplier.""" + # Extract coefficient and expression parts + coefficient, expr_parts = term.as_coeff_mul() + + if len(expr_parts) == 0: + # Pure constant term + return Term( + coefficient=coefficient, + range=1, + original_expr=1, + reconstruction_multiplier=0, + ) + elif len(expr_parts) == 1: + expr = expr_parts[0] + else: + # Multiple non-constant factors, too complex + return None + + # Now determine the range and reconstruction multiplier + range_val, reconstruction_multiplier = analyze_expression_properties(expr, var) + if reconstruction_multiplier is None: + return None + + return Term( + coefficient=coefficient, + range=range_val, + original_expr=expr, + reconstruction_multiplier=reconstruction_multiplier, + ) + + +def analyze_expression_properties( + expr: sympy.Expr, var: sympy.Symbol +) -> tuple[_IntLike | None, _IntLike | None]: + """Analyze an expression to determine its range and reconstruction multiplier.""" + # ModularIndexing(var, divisor, modulo) = (var // divisor) % modulo + if isinstance(expr, ModularIndexing): + x, div, mod = expr.args + if static_eq(x, var): + return mod, div # Range is mod, multiplier is div + + # FloorDiv cases + if isinstance(expr, FloorDiv): + base, divisor = expr.args + + # FloorDiv(ModularIndexing(var, 1, mod), div) = (var % mod) // div + if isinstance(base, ModularIndexing): + x, inner_div, mod = base.args + if static_eq(x, var) and static_eq(inner_div, 1): + range_val = FloorDiv(mod, divisor) + return range_val, divisor # Range is mod//div, multiplier is div + + # FloorDiv(var, divisor) = var // divisor (unbounded) + elif static_eq(base, var): + return None, divisor # Unbounded range, multiplier is div + + return None, None + + +def check_invertibility(terms: list[Term]) -> bool: + """Check if the terms represent an invertible transformation.""" + if not terms: + return False + + # Coefficients must be strictly decreasing + coeffs = [t.coefficient for t in terms] + if argsort_sym(V.graph.sizevars.shape_env, coeffs) != list( + reversed(range(len(coeffs))) + ): + return False + + # Check mixed-radix property: each coeff[i] = coeff[i+1] * range[i+1] + expected_coeff = 1 + for term in reversed(terms): + if not static_eq(term.coefficient, expected_coeff): + return False + if term.range is not None: + expected_coeff *= term.range + + return True + + +def generate_reconstruction_expr(terms: list[Term], var: sympy.Symbol) -> sympy.Expr: + y = var + reconstruction = sympy.S.Zero + remainder = y + + for i, term in enumerate(terms): + if i < len(terms) - 1: + component = FloorDiv(remainder, term.coefficient) + remainder = ModularIndexing(remainder, 1, term.coefficient) + else: + # Last term should also divide by its coefficient + component = FloorDiv(remainder, term.coefficient) + + reconstruction += component * term.reconstruction_multiplier + + return reconstruction diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/ir.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/ir.py new file mode 100644 index 0000000000000000000000000000000000000000..435e301860d53458b0da29d23683c91838ecd904 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/ir.py @@ -0,0 +1,10808 @@ +from __future__ import annotations + +import contextlib +import copy +import dataclasses +import functools +import itertools +import logging +import operator +import textwrap +import traceback +from collections.abc import Callable, Generator, Iterable, Iterator, Sequence +from contextlib import AbstractContextManager, nullcontext +from enum import Enum +from functools import partial +from typing import ( + Any, + cast, + ClassVar, + Literal, + overload, + SupportsFloat, + SupportsInt, + TYPE_CHECKING, + TypeAlias, + TypeVar, + Union, +) +from typing_extensions import assert_never, Never, override, ParamSpec, Self, TypeIs +from unittest.mock import patch + +import sympy +from sympy import Expr, Integer, Symbol + +import torch._export.serde.schema as export_schema +import torch._library.utils as library_utils +import torch._logging +import torch.fx +import torch.utils._pytree as pytree +from torch._dynamo.utils import identity +from torch._export.serde.serialize import GraphModuleSerializer +from torch._higher_order_ops.auto_functionalize import can_auto_functionalize +from torch._inductor import metrics +from torch._inductor.utils import get_free_symbols +from torch._library.fake_class_registry import FakeScriptObject +from torch._library.opaque_object import get_opaque_obj_repr, is_opaque_value +from torch._prims_common import ( + compute_required_storage_length, + is_boolean_dtype, + is_contiguous_for_memory_format_or_false, + is_float_dtype, + make_channels_last_strides_for, + StrideType, +) +from torch.fx.experimental.symbolic_shapes import ( + _remove_effect_token_unbacked_bindings, + compute_unbacked_bindings, + free_symbols, + free_unbacked_symbols, + GuardOnDataDependentSymNode, + has_free_unbacked_symbols, + IterateExprs, + rebind_unbacked, + resolve_unbacked_bindings, + ShapeEnv, + SymTypes, +) +from torch.fx.node import Node +from torch.utils._ordered_set import OrderedSet +from torch.utils._python_dispatch import _disable_current_modes +from torch.utils._sympy.functions import CleanDiv, FloorDiv, Mod, ModularIndexing +from torch.utils._sympy.symbol import SymT + +from . import config, dependencies +from .codegen.common import ( + BackendFeature, + CodegenSymbol, + get_scheduling_for_device, + index_prevent_reordering, + Kernel, +) +from .dependencies import ( + Dep, + extract_free_symbols, + extract_input_node_reduction_ranges, + extract_read_writes, + SymbolUsageCollectorOpsHandler, + var_builder, +) +from .loop_body import LoopBody +from .ops_handler import OpCounterCSE, OpCountResult, ReductionType, StoreMode +from .runtime.benchmarking import benchmarker +from .runtime.hints import DeviceProperties, ReductionHint +from .utils import ( + argsort, + argsort_sym, + cache_on_self, + cache_on_self_and_args, + ceildiv, + convert_shape_to_inductor, + convert_shape_to_symint, + developer_warning, + do_bench_using_profiling, + dtype_from_size, + get_dtype_size, + get_kernel_metadata, + GPU_ALIGN_BYTES, + ir_dataclass, + is_dynamic, + is_gpu, + sympy_dot, + sympy_index_symbol, + sympy_index_symbol_with_prefix, + sympy_product, + sympy_subs, + tensor_is_aligned, +) +from .virtualized import ops, OpsValue, V + + +if TYPE_CHECKING: + from torch.fx.experimental.symbolic_shapes import SympyBoolean + from torch.fx.node import Argument + from torch.types import IntLikeType + + from .codegen.cutlass.template import CUTLASSTemplate + from .codegen.wrapper import PythonWrapperCodegen + from .graph import GraphLowering + from .utils import IndentedBuffer + +else: + CUTLASSTemplate: TypeAlias = object + + +try: + import triton + + triton_version = triton.__version__ + has_triton = True +except ImportError: + triton_version = None + has_triton = False + + +_P = ParamSpec("_P") +_T = TypeVar("_T") +_U = TypeVar("_U") +_V = TypeVar("_V") + +_IntLike: TypeAlias = int | Expr +_NumLike: TypeAlias = int | float | Expr + +_OpOverloads: TypeAlias = torch._ops.OpOverload | torch._ops.HigherOrderOperator + +log = logging.getLogger(__name__) +indent = functools.partial(textwrap.indent, prefix=" ") +aten = torch.ops.aten + +""" [Note: Inductor IR] + +Inductor's IR is produced by executing 'lowering' code (see lowering.py). Each +lowering is registered to a particular aten operator, and expects inputs that +correspond to the aten schema. However, in place of torch Tensor inputs, lowerings +expect Inductor TensorBox inputs. + +TensorBox IR represents torch tensors. Tensors are sometimes single objects owning +storage, and sometimes views of another Tensor's storage. Mutating tensor operations +(such as add_()) affect the underlying storage and any associated views. Other operations +(such as .t_()) update metadata about the current view but don't modify the underlying storage. + +To model this in Inductor, the IR distinguishes between TensorBox, View, StorageBox and Buffer. + +TensorBox is the top level IR construct that any lowering should produce and maps to a torch.Tensor +output from an operation. But just as torch.Tensors take different forms, TensorBox IR can +reference View IR or directly reference StorageBox IRs. + +Some Inductor lowerings produce new sets of 'Box'es, while others (such as .t() or other view ops) +may take an existing TensorBox and point it to a new underlying View IR. + +Tensors that directly own storage are represented as a chain of: +TensorBox -> StorageBox -> Buffer +where Buffer is a simple (1D) allocation, and StorageBox introduces the concept of a Layout. + +If you mutate the data of such a tensor, we swing the StorageBox pointer to point to a new buffer +(leaving the old buffer unmodified and functionalizing the operation). + +Tensors backed by views add one more indirection to the IR. +TensorBox -> View -> StorageBox -> Buffer +In these cases, the underlying StorageBox/Buffer will be shared with the pre-view TensorBox. + +Computation is represented by Operation nodes, with each operation producing 1 +or more output Buffers. In the case of mutations, these will be new Buffers that have the +mutated buffer listed in its get_mutation_names(). + +It is also possible to have an InputBuffer for which there is no corresponding Operation, +e.g. it may be a graph input or compile time constant. + +""" + + +_NodeOrNodes: TypeAlias = Union[ + int, + "TensorBox", + dict[str, "TensorBox"], + "Symbol", + "IRNode", + Sequence[ + Union[int, dict[str, "TensorBox"], "TensorBox", "Symbol", "IRNode"] | None + ], +] + + +def _is_static(x: object) -> TypeIs[int | Integer]: + return isinstance(x, (int, Integer)) + + +@dataclasses.dataclass(frozen=True) +class GraphPartitionSignature: + # symbol inputs that are necessary for codegen + symbol_inputs: OrderedSet[sympy.Symbol] + + # mapping from partition input name to IRNode or Expr. Need the name str since + # we cannot get name from Expr. + input_nodes: dict[str, IRNode | sympy.Expr | TorchBindObject] + output_nodes: list[IRNode] + + # mapping from partition input name to a boolean for whether deallocating it + # in the partition function + input_deallocation: dict[str, bool] + skip_cudagraph: bool + + # name of constants read/written by the graph partition + constant_names: list[str] + + +def validate_ir(node_or_nodes: _NodeOrNodes | None) -> None: + def _check_tensorbox(nodes: _NodeOrNodes | None) -> None: + # Could expand this to check deeper properties + # (e.g. TensorBox points to View or StorageBox) + if nodes is None: + pass + elif isinstance(nodes, (list, tuple)): + for node in nodes: + _check_tensorbox(node) + elif isinstance(nodes, dict): + for node in nodes.values(): + _check_tensorbox(node) + else: + assert isinstance( + nodes, + ( + ExpandView, + DynamicScalar, + AssertScalar, + TensorBox, + sympy.logic.boolalg.Boolean, + Expr, + int, + EffectfulKernel, + ShapeAsConstantBuffer, + OpaqueMultiOutput, + ), + ), ( + f"Found {type(nodes)}, which is not a supported top level IR node. See [Note: Inductor IR]" + ) + + # Be picky about the accepted data structure (don't use pytree here) + _check_tensorbox(node_or_nodes) + + +def ops_wrapper(name: str) -> Callable[..., OpsValue]: + assert isinstance(name, str), type(name) + + def fn(*args: object, **kwargs: object) -> OpsValue: + return getattr(ops, name)(*args, **kwargs) + + return fn + + +def inverse_reorder(order: Sequence[int]) -> Callable[[Sequence[_T]], Sequence[_T]]: + inv_order = dict(zip(order, range(len(order)))) + + def reindex(index: Sequence[_T]) -> Sequence[_T]: + assert len(index) == len(inv_order) + return [index[inv_order[i]] for i in range(len(index))] + + return reindex + + +def same_reorder(order: Sequence[int]) -> Callable[[Sequence[_T]], Sequence[_T]]: + def reindex(index: Sequence[_T]) -> Sequence[_T]: + assert len(index) == len(order) + return [index[order[i]] for i in range(len(index))] + + return reindex + + +def fuse_reindexing( + reindex1: Callable[[Sequence[_U]], Sequence[_V]], + reindex2: Callable[[Sequence[_T]], Sequence[_U]], +) -> Callable[[Sequence[_T]], Sequence[_V]]: + def reindex(index: Sequence[_T]) -> Sequence[_V]: + return reindex1(reindex2(index)) + + return reindex + + +NHWC_STRIDE_ORDER = [3, 0, 2, 1] +NHWDC_STRIDE_ORDER = [4, 0, 3, 2, 1] + + +def get_fill_order( + seq: Sequence[int | torch.SymInt | Expr], shape_env: ShapeEnv | None = None +) -> Sequence[int]: + """ + Convert strides to fill order (argsort) + """ + if shape_env is None or all(isinstance(s, (int, sympy.Integer)) for s in seq): + sorted_idx: Sequence[int] = argsort(seq) + else: + # argsort_sym handles unbacked symints (with the help of the shape_env) + sorted_idx = argsort_sym(shape_env, seq) + return sorted_idx + + +def stride_order2fill_order(order: Sequence[int | Integer]) -> Sequence[int]: + """ + Convert stride order to fill order + For channel last format, + + stride order = [3, 0, 2, 1] and fill order = [1, 3, 2, 0] + """ + lookup = {pos: idx for idx, pos in enumerate(order)} + fill_order = [lookup[i] for i in range(len(order))] + return fill_order + + +def get_stride_order( + seq: Sequence[int | torch.SymInt | Expr], shape_env: ShapeEnv | None = None +) -> Sequence[int]: + """ + Convert strides to stride order + """ + sorted_idx: Sequence[int] = get_fill_order(seq, shape_env) + out = [0 for _ in range(len(seq))] + for i, elem in enumerate(sorted_idx): + out[elem] = i + return out + + +@overload +def ir_node_to_tensor(x: None, replace_symbols_with_hints: bool = False) -> None: ... + + +@overload +def ir_node_to_tensor( + x: IRNode, replace_symbols_with_hints: bool = False +) -> torch.Tensor: ... + + +def ir_node_to_tensor( + x: IRNode | None, replace_symbols_with_hints: bool = False +) -> torch.Tensor | None: + # When replace_symbols_with_hints=False (default), sizes/strides remain as + # symbolic expressions, so downstream operations on the resulting tensor (e.g., + # shape comparisons inside a kernel's meta function) may install guards. When + # True, symbolic expressions are replaced with concrete integer hints via + # size_hint, preventing any downstream guards. + if x is None: + return None + + shape_fn: Callable[[int | Expr], int | Expr] + if replace_symbols_with_hints: + shape_fn = V.graph.sizevars.optimization_hint + else: + shape_fn = identity + size = [shape_fn(s) for s in x.get_size()] + stride: StrideType + if is_storage_and_layout(x): + stride = [shape_fn(s) for s in x.get_layout().stride] + else: + stride = FlexibleLayout.contiguous_strides(size) + dtype = x.get_dtype() + device = x.get_device() + size = convert_shape_to_symint(size) + # pyrefly: ignore [bad-assignment] + stride = convert_shape_to_symint(stride) + with V.graph.sizevars.shape_env.suppress_guards(): + t = torch.empty_strided( + size=size, stride=stride, dtype=dtype, device=device + ).zero_() + return t + + +def may_convert_to_optional( + value: Sequence[_T] | None, +) -> Sequence[_T | None] | None: + if isinstance(value, list) and not value: + # [None] makes sure the cpp wrapper codegen will generate something like + # {std::nullopt} instead of {} + return [None] + return value + + +def get_device_type( + x: IRNode | OutputSpec | torch.device | None | str, +) -> str | None: + if isinstance(x, str) or x is None: + return x + elif isinstance(x, torch.device): + return x.type + elif isinstance(x, (IRNode, OutputSpec)): + return get_device_type(x.get_device()) + # pyrefly: ignore [bad-argument-type] + assert_never(f"get_device_type({x}: {type(x).__name__})") + + +def is_triton(x: IRNode | torch.device | None | str) -> bool: + device = get_device_type(x) + # Special case cpu and cuda as using the method below + # to determine if the scheduler is a triton scheduler subclass + # requires instantiating a scheduler for them + if device in ["cpu", "cuda", "xpu"]: + if getattr(config, f"{device}_backend") == "triton": + return True + return False + if ( + device is None + or (device_scheduling := get_scheduling_for_device(device)) is None + ): + return False + from .codegen.triton import TritonScheduling + + assert isinstance(device_scheduling, type), type(device_scheduling) + return issubclass(device_scheduling, TritonScheduling) + + +def is_cpu(x: IRNode | torch.device | None | str) -> bool: + return get_device_type(x) == "cpu" + + +def is_aligned_realized_tensor(x: Buffer | TensorBox, alignment: int) -> bool: + if ( + not isinstance(x, IRNode) + or x.maybe_get_stride() is None + or free_unbacked_symbols(x.get_stride()) + or free_unbacked_symbols(x.get_size()) + ): + return False + + aligned_strides = sympy.And( + *(sympy.Eq(Mod(s, alignment), 0) for s in x.get_stride()[:-1]) + ) + aligned_last_dim = sympy.Or( + sympy.Eq(x.get_stride()[-1], 1), sympy.Le(x.get_size()[-1], 1) + ) + is_aligned = sympy.And(aligned_strides, aligned_last_dim) + + # Make sure to guard to recompile when necessary. + return V.graph.sizevars.guard_or_false(is_aligned) + + +def significant_strides_equal( + strides1: Sequence[_IntLike], + strides2: Sequence[_IntLike], + shape: Sequence[_IntLike], +) -> bool: + """ + Returns true if the strides are equal, ignoring dimensions of size 1 . + """ + assert len(shape) == len(strides1) and len(strides1) == len(strides2) + for dim, s1, s2 in zip(shape, strides1, strides2): + if V.graph.sizevars.statically_known_leq(dim, 1): + continue + + if not V.graph.sizevars.guard_or_false(sympy.Eq(s1, s2)): + return False + return True + + +def try_match_insignificant_strides( + tensor: IRNode, + strides: Sequence[int | torch.SymInt], +) -> IRNode: + """ + Tries to match the strides of the tensor to those in the meta_strides. Strides of insignificant + dimensions - size 0 or 1 - will be updated. + + If there are real stride differences (NHWC vs NCHW), or the tensor is not realized, then the input will be returned + """ + if not is_storage_and_layout(tensor): + return tensor + + if all( + V.graph.sizevars.statically_known_equals(s1, s2) + for s1, s2 in zip(strides, tensor.get_stride()) + ): + return tensor + + if not significant_strides_equal(strides, tensor.get_stride(), tensor.get_size()): + return tensor + + storage, old_layout = as_storage_and_layout(tensor) + new_stride = [*old_layout.stride] + for i, s in enumerate(tensor.get_size()): + if V.graph.sizevars.statically_known_leq(s, 1): + new_stride[i] = strides[i] + + new_layout = FixedLayout( + old_layout.device, + old_layout.dtype, + old_layout.size, + new_stride, + old_layout.offset, + old_layout.is_pinned, + ) + return TensorBox(ReinterpretView(data=storage, layout=new_layout)) + + +def gm_original_output_strides(gm: torch.fx.GraphModule) -> None: + output_node = gm.graph.find_nodes(op="output")[0] + output_node.meta["user_visible_output_idxs"] = [ + idx for idx, _ in enumerate(output_node.args) + ] + from torch._inductor.compile_fx import record_original_output_strides + + record_original_output_strides(gm) + + +def get_symbolic_inputs(inputs: Sequence[IRNode]) -> list[Expr]: + sym_vars: OrderedSet[Expr] = OrderedSet() + for inp in inputs: + sym_vars |= get_free_symbols(inp.get_size(), unbacked_only=False) + sym_vars |= get_free_symbols(inp.get_stride(), unbacked_only=False) + + return list(sym_vars) + + +def try_get_name(x): + if isinstance(x, TensorBox): + x = x.data + if isinstance(x, BaseView): + x = x.unwrap_view() + if isinstance(x, StorageBox): + x = x.data + return x.get_name() if isinstance(x, Buffer) else None + + +class IRNode: + """Base class for all intermediate representation (IR) nodes in TorchInductor. + + Note: + This is an abstract base class. Most methods raise NotImplementedError + and must be overridden by concrete subclasses. + """ + + _current_origins: ClassVar[OrderedSet[Any]] = OrderedSet() + _current_stream_idx: ClassVar[int | None] = None + + # NB: These are kinda weird, + origins: OrderedSet[Any] = dataclasses.field(init=False) + # traces back to where the IRNode is created in Inductor + traceback: list[str] | None = dataclasses.field(init=False) + origin_node: torch.fx.Node | None = dataclasses.field(init=False) + # Annotations dict for storing metadata (e.g., KernelTemplateChoice) + annotations: dict[str, Any] = dataclasses.field(init=False) + # User-annotated stream index from FX node metadata (set during lowering) + stream_idx: int | None = dataclasses.field(init=False) + + @staticmethod + @contextlib.contextmanager + def current_origins(origins: OrderedSet[Node]) -> Generator[None, None, None]: + old = IRNode._current_origins + IRNode._current_origins = old | origins + try: + yield + finally: + IRNode._current_origins = old + + @staticmethod + @contextlib.contextmanager + def current_stream_idx( + stream_idx: int | None, + ) -> Generator[None, None, None]: + old = IRNode._current_stream_idx + IRNode._current_stream_idx = stream_idx + try: + yield + finally: + IRNode._current_stream_idx = old + + @staticmethod + def is_realized_node(node: IRNode) -> bool: + return isinstance( + node, + ( + ComputedBuffer, + InputsKernel, + InputBuffer, + ReinterpretView, + TemplateBuffer, + ), + ) + + def wrap_for_lowering(self) -> IRNode: + return TensorBox.create(self) + + def _post_init_setattr(self, attr: str, value: Any) -> None: + # Intended for use in __post_init__ for enforcing an invariant on a dataclass + # If you must, can also be used for setting provenance info + # We would like to try and minimize these usages though + object.__setattr__(self, attr, value) + + def __post_init__(self) -> None: + origins = OrderedSet(self._current_origins) + self._post_init_setattr("origins", origins) + self._post_init_setattr( + "traceback", traceback.format_stack() if config.debug_ir_traceback else None + ) + self._post_init_setattr("origin_node", None) + # Annotations dict for storing metadata (e.g., KernelTemplateChoice) + self._post_init_setattr("annotations", {}) + self._post_init_setattr("stream_idx", self._current_stream_idx) + + def get_read_names(self) -> OrderedSet[str]: + return OrderedSet(dep.name for dep in self.get_reads()) + + def get_traceback(self) -> list[str] | None: + return self.traceback + + def get_origin_node(self) -> torch.fx.Node | None: + return self.origin_node + + def get_defining_op(self) -> Operation | None: + return None + + def get_subgraphs(self) -> list[Subgraph]: + """Return subgraphs contained in this node""" + return [] + + def get_stack_traces(self) -> OrderedSet[str]: + # Return stack traces to user model code + # A single IRNode could correspond to multiple lines of code + stack_traces: OrderedSet[str] = OrderedSet() + origins = self.origins + if isinstance(self, ExternKernel): + origin_node = self.get_origin_node() + if self.origin_node: + origins = OrderedSet([origin_node]) + for node in origins: + if hasattr(node, "stack_trace") and node.stack_trace: + # nodes in the backward graph don't have mapping to pre_grad_graph + stack_traces.add(node.stack_trace) + else: + pre_grad_nodes = ( + torch._inductor.debug._inductor_post_to_pre_grad_nodes.get( + "postToPre", + {}, + # pyrefly: ignore [missing-attribute] + ).get(node.name, []) + ) + if not isinstance(pre_grad_nodes, list): + continue + for node_name in pre_grad_nodes: + stack_trace = ( + torch._inductor.debug._inductor_pre_grad_node_stack_trace.get( + node_name, None + ) + ) + if stack_trace: + stack_traces.add(stack_trace) + return stack_traces + + def common_repr(self, shorten: bool = True) -> Sequence[str]: + origins = f"origins={getattr(self, 'origins', '')}" + if shorten and len(origins) > 64: + # this can get *very* long + origins = f"{origins[:61]}..." + if not self.get_stack_traces(): + return [origins] + + stack_trace_str = [] + for stack_trace in self.get_stack_traces(): + stack_trace_str.append("stack_traces = {") + stack_trace_str += stack_trace.split("\n") + stack_trace_str.append("}") + return [origins] + stack_trace_str + + def str_helper( + self, lines: Sequence[object], shorten: bool = True, multiline: bool = True + ) -> str: + lines = list(lines) + list(self.common_repr(shorten)) + lines = list(map(str, lines)) + if multiline: + # pyrefly: ignore [no-matching-overload] + new_lines = indent(",\n".join(lines)) + return f"{type(self).__name__}(\n{new_lines}\n)" + else: + return f"{type(self).__name__}({lines})" + + def get_dtype(self) -> torch.dtype: + return self.dtype + + def maybe_get_dtype(self) -> torch.dtype | None: + try: + return self.get_dtype() + except NotImplementedError: + return None + + def get_layout(self) -> Layout: + raise NotImplementedError(f"get_layout() is not implemented by {type(self)}!") + + def maybe_get_layout(self) -> Layout | None: + try: + return self.get_layout() + except NotImplementedError: + return None + + def get_output_spec(self) -> OutputSpec: + return self.get_layout() + + def maybe_get_output_spec(self) -> OutputSpec | None: + try: + return self.get_output_spec() + except NotImplementedError: + return None + + def has_tensor_output(self) -> bool: + """True for single tensor output (excludes MultiOutput)""" + return isinstance(self.maybe_get_output_spec(), Layout) + + def get_size(self) -> Sequence[Expr]: + raise NotImplementedError(f"get_size() is not implemented by {type(self)}!") + + def maybe_get_size(self) -> Sequence[_IntLike] | None: + try: + return self.get_size() + except NotImplementedError: + return None + + @property + def shape(self) -> _IntLike | sympy.Rel | Sequence[_IntLike]: + return self.get_size() + + def get_numel(self) -> Expr: + return sympy_product(self.get_size()) + + def is_zero_elements(self) -> bool: + return V.graph.sizevars.statically_known_true(sympy.Eq(self.get_numel(), 0)) + + def realize(self) -> str | None: + """ + If the IRNode refers to data which has not been materialized (e.g., + it is a Pointwise/Reduction that could potentially have more + compute fused into it), realize the IRNode into physical memory, + ending the possibility of fusing into it, but allowing, e.g., multiple + users to access the data without having to recompute. + + Check StorageBox.realize for a particularly notable implementation. + + TODO(ezyang): I think, in principle, every IRNode should have an + implementation of this, and most of the time no-op is OK, but you + really do have to audit each IRNode for this, so for now, raise + an error if it's not implemented. Note that some code in graph.py + will catch this thrown error and suppress it with a warning. + """ + raise NotImplementedError(f"realize NYI on {type(self)}") + + def codegen_reference(self, writer: IndentedBuffer | None = None) -> str: + raise NotImplementedError(f"codegen_reference NYI on {type(self)}") + + def get_device(self) -> torch.device | None: + return None + + def get_device_or_error(self) -> torch.device: + device = self.get_device() + assert device is not None + return device + + def has_exceeded_max_reads(self) -> bool: + return False + + def make_loader(self) -> Callable[[Sequence[Expr]], OpsValue]: + raise NotImplementedError(type(self).__name__) + + def make_indexer(self) -> Callable[[Sequence[Expr]], Expr]: + raise NotImplementedError(type(self).__name__) + + def get_stride(self) -> Sequence[_IntLike]: + raise NotImplementedError(type(self).__name__) + + def maybe_get_stride(self) -> Sequence[_IntLike] | None: + try: + return self.get_stride() + except NotImplementedError: + return None + + def get_name(self) -> str: + raise NotImplementedError(type(self).__name__) + + def maybe_get_name(self) -> str | None: + try: + return self.get_name() + except NotImplementedError: + return None + + def is_input_buffer(self) -> bool: + try: + return self.get_name() in V.graph.graph_inputs + except NotImplementedError: + return False + + def has_large_inner_fn(self, threshold: int | None = None) -> bool: + return False + + def mark_reuse(self, users: int) -> None: + pass + + def realize_hint(self) -> None: + pass + + def unwrap_view(self) -> IRNode: + raise NotImplementedError(type(self).__name__) + + def freeze_layout(self) -> None: + raise NotImplementedError(type(self).__name__) + + def freeze_layout_with_stride_order( + self, order: Sequence[int], allow_padding: bool = False + ) -> None: + raise NotImplementedError(type(self).__name__) + + def freeze_layout_with_fill_order(self, order: Sequence[int]) -> None: + raise NotImplementedError(type(self).__name__) + + def freeze_layout_with_same_order(self, stride: Sequence[_IntLike]) -> None: + raise NotImplementedError(type(self).__name__) + + def freeze_layout_with_exact_strides( + self, exact_strides: Sequence[_IntLike], allow_padding: bool = False + ) -> None: + raise NotImplementedError(type(self).__name__) + + def get_read_writes(self) -> dependencies.ReadWrites: + raise NotImplementedError(type(self).__name__) + + def get_reads(self) -> OrderedSet[Dep]: + return self.get_read_writes().reads + + def num_reads(self) -> int: + return len(self.get_reads()) + + def get_storage_numel(self) -> _IntLike: + raise NotImplementedError(type(self).__name__) + + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + raise NotImplementedError(type(self).__name__) + + def get_reduction_type(self) -> str | None: + raise NotImplementedError(type(self).__name__) + + def get_reduction_size(self) -> Sequence[Expr]: + raise NotImplementedError(type(self).__name__) + + def is_extern(self) -> bool: + return False + + def is_no_op(self) -> bool: + return False + + def constant_to_device(self, device: torch.device) -> IRNode: + raise NotImplementedError(type(self).__name__) + + def get_mutation_names(self) -> Sequence[str]: + raise NotImplementedError(type(self).__name__) + + def get_operation_name(self) -> str: + raise NotImplementedError(type(self).__name__) + + def get_inputs_that_alias_output(self) -> Sequence[str]: + raise NotImplementedError(type(self).__name__) + + if TYPE_CHECKING: + + @property + def dtype(self) -> torch.dtype: ... + + +@ir_dataclass(frozen=False) +class Operation: + def __post_init__(self) -> None: + self.operation_name: str | None = None + self._config_patches: dict[str, Any] = {} + + def get_device(self) -> torch.device | None: + raise NotImplementedError + + def get_origin_node(self) -> torch.fx.Node | None: + assert hasattr(self, "origin_node") + return self.origin_node + + def get_origins(self) -> OrderedSet[Any]: + assert hasattr(self, "origins") + return self.origins + + def get_stream_idx(self) -> int | None: + assert hasattr(self, "stream_idx") + return self.stream_idx + + def get_operation_name(self) -> str: + assert self.operation_name is not None + return self.operation_name + + def get_config_patches(self) -> dict[str, Any]: + """Get config patches for this operation (e.g., coordinate_descent_tuning).""" + return self._config_patches + + def set_config_patches(self, patches: dict[str, Any]) -> None: + """Set config patches for this operation.""" + self._config_patches = patches + + def is_extern(self) -> bool: + return False + + def is_no_op(self) -> bool: + return False + + def get_read_writes(self) -> dependencies.ReadWrites: + raise NotImplementedError + + def is_user_of(self, name: str) -> bool: + return name in self.get_read_names() + + def get_read_names(self) -> OrderedSet[str]: + return OrderedSet(dep.name for dep in self.get_reads()) + + def get_reads(self) -> OrderedSet[Dep]: + return self.get_read_writes().reads + + def get_outputs(self) -> list[Buffer]: + raise NotImplementedError + + def get_unbacked_symbol_defs(self) -> OrderedSet[sympy.Symbol]: + return OrderedSet() + + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + """ + When unbacked_only=True: + Returns the unbacked symbols which are required to be in scope in + order to successfully perform codegen for this buffer. For example, + a buffer that corresponds to an extern kernel call that takes i0 as + an argument would return {i0} here. This is used to generate necessary + dependencies that ensure we actually bind i0 in codegen before you + try to use it. + + Note that this is NOT transitive; in particular, if this buffer takes + in as input another buffer with dynamic shape (e.g., (i0,)), we will + not report it here, because you will already have a dependency + on that buffer, which will eventually have a dependency on i0 if + necessary. + + When unbacked_only=False: + Similar to `unbacked_only=True` but including all free symbols + instead of only free unbacked symbols. + """ + return OrderedSet() + + def get_workspace_size(self) -> int: + """ + Gets extra global memory size needed by this buffer. + Some algorithms (e.g. group gemm) may require extra global memory in the generated code. + """ + return 0 + + +@ir_dataclass +class Loops(IRNode): + device: torch.device + dtype: torch.dtype + inner_fn: Callable[..., Any] + ranges: Sequence[_IntLike] + + @cache_on_self_and_args("Loops") + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + return OrderedSet().union( + *(get_free_symbols(e, unbacked_only) for e in self.ranges), + self.inner_fn_free_symbols(unbacked_only), + ) + + def _to_str(self, names: Sequence[str]) -> str: + return self.str_helper( + [ + f"'{self.device.type}'", + str(self.dtype), + self.inner_fn_str(), + ] + + [f"{name}={getattr(self, name)}" for name in names] + + [f"origin_node={self.origin_node!r}"] + ) + + def __str__(self) -> str: + return self._to_str(("ranges",)) + + __repr__ = __str__ + + def get_device(self) -> torch.device | None: + return self.device + + def get_origin_node(self) -> torch.fx.Node | None: + return self.origin_node + + def get_size(self) -> Sequence[Expr]: + return self.ranges + + def get_pointwise_size(self) -> Sequence[Expr]: + return self.ranges + + @classmethod + def create(cls, *args: Any, **kwargs: Any) -> TensorBox: + origin_node = kwargs.pop("origin_node", None) + tb = kwargs.pop("traceback", None) + r = cls(*args, **kwargs) + # Need to explicitly set origin_node here to propagate it down. + # todo(chilli): I think it would be better for IRNode to directly set + # origin_node + r._post_init_setattr("origin_node", origin_node) + r._post_init_setattr("traceback", tb or r.traceback) + return TensorBox.create(r) + + @staticmethod + def _index(ranges: Sequence[_IntLike], prefix: SymT = SymT.INDEX) -> Sequence[Expr]: + return [ + sympy.S.Zero if s == 1 else sympy_index_symbol_with_prefix(prefix, n) + for n, s in enumerate(ranges) + ] + + @cache_on_self + def inner_fn_opcount(self) -> OpCountResult: + opcounter = OpCounterCSE(V.MockHandler()) + with ( + V.set_ops_handler(opcounter), + patch.object(FlexibleLayout, "allow_indexing", True), + ): + self.inner_fn(*self.inner_fn_args()) + return opcounter.getvalue() + + def inner_fn_args(self) -> Sequence[Sequence[_IntLike]]: + return (self._index(self.ranges),) + + @cache_on_self + def inner_fn_str(self) -> str: + return V.KernelFormatterHandler.ir_to_string( + self.inner_fn, *self.inner_fn_args() + ) + + def has_large_inner_fn(self, threshold: int | None = None) -> bool: + if threshold is None: + threshold = 0 + threshold = max(threshold, config.realize_opcount_threshold) + return self.inner_fn_opcount().num_ops > threshold + + def inner_fn_free_symbols(self, unbacked_only: bool = False) -> OrderedSet[Symbol]: + index = self._index(self.ranges) + return extract_free_symbols(self.inner_fn, index, unbacked_only=unbacked_only) + + # returns a list of op names which references the symbol + def collect_inner_fn_symbol_usage(self, symbol: Symbol) -> OrderedSet[str]: + index = self._index(self.ranges) + handler = SymbolUsageCollectorOpsHandler(symbol) + with ( + V.set_ops_handler(handler), + ): + self.inner_fn(index) + return handler.usages + + def get_reads(self) -> OrderedSet[Dep]: + with patch.object(FlexibleLayout, "allow_indexing", True): + if self.get_reduction_type(): + return extract_read_writes( + self.make_loader(), + self.get_size(), + self.get_reduction_size(), + ).reads + else: + return extract_read_writes( + self.make_loader(), + self.get_size(), + ).reads + + def get_read_names(self) -> OrderedSet[str]: + return OrderedSet(self.inner_fn_opcount().read_buffers) + + def num_reads(self) -> int: + return len(self.inner_fn_opcount().read_buffers) + + def get_reduction_size(self) -> Sequence[Expr]: + raise NotImplementedError( + f"get_reduction_size() is not implemented by {type(self)}!" + ) + + def get_reduction_type(self) -> str | None: + raise NotImplementedError( + f"get_reduction_type() is not implemented by {type(self)}!" + ) + + def constant_to_device(self, device: torch.device) -> IRNode: + raise NotImplementedError( + f"constant_to_device() is not implemented by {type(self)}!" + ) + + +def nop_loader_fn(idx: Expr | Sequence[Expr], *, dtype: torch.dtype) -> OpsValue: + if dtype.is_floating_point: + return ops.constant(float("nan"), dtype) + else: + return ops.constant(0, dtype) + + +@ir_dataclass +class Pointwise(Loops): + def make_loader(self) -> Callable[[Sequence[Expr]], OpsValue]: + # Make zero-element loops into a no-op + if self.is_zero_elements(): + return partial(nop_loader_fn, dtype=self.dtype) + + return self.inner_fn + + def __str__(self) -> str: + return self._to_str(("ranges",)) + + __repr__ = __str__ + + def get_reduction_size(self) -> Sequence[sympy.Expr]: + return [] + + def get_reduction_type(self) -> str | None: + return None + + def store_output( + self, + output_name: str | None, + indexer: Callable[[Sequence[Expr]], Never], + vars: Sequence[Expr], + ) -> None: + loader = self.make_loader() + return ops.store(output_name or "unnamed", indexer(vars), loader(vars)) + + def constant_to_device(self, device: torch.device) -> IRNode: + """Move this to a given device. Requires that all reads are to constants.""" + loader = self.make_loader() + loader = patch.object(ConstantBuffer, "override_device", device)(loader) + return Pointwise( + device=device, + dtype=self.dtype, + inner_fn=loader, + ranges=self.ranges, + ) + + +@ir_dataclass +class Scatter(Pointwise): + output_indexer: Callable[[Sequence[Expr]], Expr] + scatter_mode: StoreMode = None + + def constant_to_device(self, device: torch.device) -> IRNode: + """Move this to a given device. Requires that all reads are to constants.""" + loader = self.make_loader() + loader = patch.object(ConstantBuffer, "override_device", device)(loader) + return Scatter( + device=device, + dtype=self.dtype, + inner_fn=loader, + ranges=self.ranges, + output_indexer=self.output_indexer, + scatter_mode=self.scatter_mode, + ) + + def store_output( + self, + output_name: str | None, + indexer: Callable[[Sequence[Expr]], Never], + vars: Sequence[Expr], + ) -> Any: + loader = self.make_loader() + if output_name is None: + output_name = "unnamed" + return ops.store( + output_name, + indexer(self.output_indexer(vars)), + loader(vars), + mode=self.scatter_mode, + ) + + +REDUCTION_COMBINE_FN: dict[str, Callable[..., OpsValue]] = { + "any": ops_wrapper("logical_or"), + "max": ops_wrapper("maximum"), + "min": ops_wrapper("minimum"), + "prod": ops_wrapper("mul"), + "sum": ops_wrapper("add"), + "dot": ops_wrapper("add"), + "xor_sum": ops_wrapper("bitwise_xor"), +} + + +def get_reduction_combine_fn( + reduction_type: str, dtype: torch.dtype, arg_break_ties_left: bool = True +) -> Callable[..., object]: + if reduction_type in REDUCTION_COMBINE_FN: + return REDUCTION_COMBINE_FN[reduction_type] + + elif reduction_type in ("argmax", "argmin"): + + def argmax_combine_fn( + a: tuple[object, object], b: tuple[object, object] + ) -> tuple[OpsValue, OpsValue]: + a_value, a_index = a + b_value, b_index = b + + if reduction_type == "argmin": + mask = ops.lt(a_value, b_value) + else: + mask = ops.gt(a_value, b_value) + + equal = ops.eq(a_value, b_value) + if is_float_dtype(dtype): + a_isnan = ops.ne(a_value, a_value) + b_isnan = ops.ne(b_value, b_value) + mask = ops.logical_or(mask, ops.gt(a_isnan, b_isnan)) + equal = ops.logical_or(equal, ops.logical_and(a_isnan, b_isnan)) + + tie = ( + ops.lt(a_index, b_index) + if arg_break_ties_left + else ops.gt(a_index, b_index) + ) + mask = ops.logical_or(mask, ops.logical_and(equal, tie)) + return ( + ops.where(mask, a_value, b_value), + ops.where(mask, a_index, b_index), + ) + + return argmax_combine_fn + + elif reduction_type == "welford_combine": + + def welford_combine_fn( + a: tuple[OpsValue, OpsValue, OpsValue], + b: tuple[OpsValue, OpsValue, OpsValue], + ) -> tuple[OpsValue, OpsValue, OpsValue]: + a_mean, a_m2, a_weight = a + b_mean, b_m2, b_weight = b + + delta = b_mean - a_mean + new_weight = a_weight + b_weight + w2_over_w = b_weight / new_weight + return ( + a_mean + delta * w2_over_w, + a_m2 + b_m2 + delta * delta * a_weight * w2_over_w, + new_weight, + ) + + return welford_combine_fn + + else: + raise NotImplementedError(f"unknown reduction_type={reduction_type}") + + +@ir_dataclass +class Reduction(Loops): + reduction_ranges: Sequence[_IntLike] + reduction_type: ReductionType + # self.dtype represents the dst dtype + src_dtype: torch.dtype + reduction_hint: ReductionHint + + def __str__(self) -> str: + return self._to_str(("ranges", "reduction_ranges", "reduction_type")) + + __repr__ = __str__ + + @cache_on_self_and_args("Reduction") + def get_free_symbol_uses(self, unbacked_only: bool = False) -> OrderedSet[Symbol]: + return super().get_free_symbol_uses(unbacked_only) | OrderedSet().union( + *(get_free_symbols(e, unbacked_only) for e in self.reduction_ranges) + ) + + def get_reduction_size(self) -> Sequence[Expr]: + return self.reduction_ranges + + def get_reduction_type(self) -> str | None: + return self.reduction_type + + def store_reduction( + self, + output_name: str | None, + indexer: Callable[[Sequence[Expr]], Never], + vars: Sequence[Expr], + reduction_vars: Sequence[Symbol], + ) -> None: + value = ops.reduction( + self.dtype, + self.src_dtype, + self.reduction_type, + self.inner_fn(vars, reduction_vars), + ) + ops.store_reduction(output_name or "unnamed", indexer(vars), value) + + def index_length(self) -> int: + return len(self.ranges) + len(self.reduction_ranges) + + def inner_fn_args(self) -> Sequence[Sequence[Expr]]: + index = self._index(self.ranges) + rindex = self._index(self.reduction_ranges, SymT.R0_INDEX) + return (index, rindex) + + def inner_fn_free_symbols(self, unbacked_only: bool = False) -> OrderedSet[Symbol]: + index = self._index(self.ranges) + rindex = self._index(self.reduction_ranges, SymT.R0_INDEX) + return extract_free_symbols( + self.inner_fn, index, rindex, unbacked_only=unbacked_only + ) + + def constant_to_device(self, device: torch.device) -> IRNode: + """Move this to a given device. Requires that all reads are to constants.""" + loader = self.make_loader() + loader = patch.object(ConstantBuffer, "override_device", device)(loader) + return Reduction( + device=device, + dtype=self.dtype, + inner_fn=loader, + ranges=self.ranges, + reduction_ranges=self.reduction_ranges, + reduction_type=self.reduction_type, + src_dtype=self.src_dtype, + reduction_hint=ReductionHint.DEFAULT, + ) + + @staticmethod + def num_splits( + device: torch.device, + dst_dtype: torch.dtype, + src_dtype: torch.dtype, + inner_fn: Callable[_P, OpsValue], + ranges: Sequence[_IntLike], + reduction_ranges: Sequence[_IntLike], + reduction_type: ReductionType | Literal["scan"], + reduction_numel: Expr, + input_node: IRNode | None = None, + ) -> tuple[ReductionHint, _IntLike]: + # Use optimization_hint when all unbacked symbols have explicit hints, + # otherwise fall back conservatively. + exprs = [reduction_numel, sympy_product(ranges)] + if not V.graph.sizevars.all_unbacked_explicitly_hinted(exprs): + return ReductionHint.DEFAULT, 1 + reduction_numel_hint = V.graph.sizevars.optimization_hint(reduction_numel) + numel_hint = V.graph.sizevars.optimization_hint(sympy_product(ranges)) + + should_split = reduction_type == "scan" or ( + not V.graph.has_feature(device, BackendFeature.REDUCE_TO_SINGLE_ELEMENT) + and reduction_type + not in ( + "argmax", + "argmin", + ) + and config.split_reductions + ) + + if reduction_type == "dot": + # Don't split when doing native matmul + return ReductionHint.DEFAULT, 1 + + props = DeviceProperties.create(device) + num_sm = props.multi_processor_count + min_elements_per_thread = 32 + if should_split: + inner_reduction_splits: Callable[[int, int], int] = functools.partial( + V.choices.reduction_split_factor, device, inner_reduction=True + ) + outer_reduction_splits: Callable[[int, int], int] = functools.partial( + V.choices.reduction_split_factor, device, inner_reduction=False + ) + else: + + def inner_reduction_splits( + reduction_numel_hint: int, + numel_hint: int, + ) -> int: + return 1 + + outer_reduction_splits = inner_reduction_splits + + # easy cases + if numel_hint == 1: + split = inner_reduction_splits(reduction_numel_hint, numel_hint) + if split == 1: + # No need to split. + return ReductionHint.INNER, split + if input_node is not None and isinstance(input_node, TensorBox): + with patch.object(FlexibleLayout, "allow_indexing", True): + ( + new_ranges, + new_reduction_ranges, + ) = extract_input_node_reduction_ranges(input_node) + if new_ranges is not None and new_reduction_ranges is not None: + extracted_numel_hint = ( + V.graph.sizevars.replace_backed_symbols_with_hints( + sympy_product(new_ranges + new_reduction_ranges) + ) + ) + if reduction_numel_hint == extracted_numel_hint: + log.debug( + "Use previous IRNode's range and reduction_ranges instead of split. " + "current ranges: %s, current reduction ranges: %s, current split: %d, " + "new ranges: %s, new reduction ranges: %s", + ranges, + reduction_ranges, + split, + new_ranges, + new_reduction_ranges, + ) + # If the input_node or its dependent nodes are also Reduction nodes, + # use reduction_sizes of this node or its dependent nodes directly. + return ReductionHint.INNER, -1 + return ReductionHint.INNER, split + if ( + reduction_numel_hint <= min_elements_per_thread + or numel_hint >= num_sm * 2 * 32 + ): + return ReductionHint.DEFAULT, 1 + + r = Reduction( + device=device, + dtype=dst_dtype, + inner_fn=inner_fn, + ranges=ranges, + reduction_ranges=reduction_ranges, + reduction_type=reduction_type if reduction_type != "scan" else "sum", + src_dtype=src_dtype, + reduction_hint=ReductionHint.DEFAULT, + ) + + def get_read_indices(r: Reduction) -> tuple[Sequence[Expr], bool]: + device = r.get_device() + assert device is not None + cb = ComputedBuffer( + name=None, + layout=FlexibleLayout( + device=device, + dtype=r.get_dtype(), + size=r.get_size(), + ), + data=r, + ) + read_writes = cb.get_read_writes() + # try finding the full size producer + # TODO this will fail for something like ((1, N) * (N, 1)).sum() + # this would also possibly be wrong for producers with the different contiguity but we hope those cases are rare + assert read_writes.range_vars is not None + range_vars = [ + r + for r in read_writes.range_vars + if isinstance(r, Expr) and not isinstance(r, sympy.Number) + ] + indices = [] + changed = False + for md in sorted(read_writes.reads, key=lambda x: x.name): + if all(r in md.index.free_symbols for r in range_vars): + indices.append(md.index) + if md.name in V.graph.name_to_buffer: + buf = V.graph.name_to_buffer[md.name] + original_stride = getattr(buf.layout, "stride", None) + buf.decide_layout() + if getattr(buf.layout, "stride", None) != original_stride: + changed = True + return indices, changed + + indices, changed = get_read_indices(r) + if changed: + indices, _ = get_read_indices(r) + + if len(indices) == 0: + # TODO determine splits when all inputs are broadcast + return ReductionHint.DEFAULT, 1 + + (_, reduction_vars), ranges1 = dependencies.index_vars_squeeze( + r.get_size(), r.get_reduction_size() + ) + num_outer = 0 + num_inner = 0 + for i in indices: + j = V.graph.sizevars.simplify_with_ranges(i, ranges1) + strides = V.graph.sizevars.stride_hints( + j, reduction_vars, list(ranges1.keys()) + ) + # A 0 stride does not make a reduction contiguous. + # This can happen when the reduction ranges contains a 1. + outer = all(s == 0 or s > 1 for s in strides) + if outer: + num_outer += 1 + else: + num_inner += 1 + if num_inner > num_outer: + return ReductionHint.INNER, inner_reduction_splits( + reduction_numel_hint, numel_hint + ) + else: + return ReductionHint.OUTER, outer_reduction_splits( + reduction_numel_hint, numel_hint + ) + + @staticmethod + def _unroll_reduction_fn( + inner_fn: Callable[[Sequence[_IntLike], Sequence[_IntLike]], OpsValue], + reduction_ranges: Sequence[_IntLike], + reduction_type: str, + src_dtype: torch.dtype, + ) -> Callable[[Sequence[_IntLike]], OpsValue]: + """Convert inner_fn from a reduction to an pointwise""" + reduction_ranges = V.graph.sizevars.guard_int_seq(reduction_ranges) + + combine_fn = get_reduction_combine_fn(reduction_type, src_dtype) + + def fn(index: Sequence[_IntLike]) -> Any: + return functools.reduce( + combine_fn, + ( + value_fn(index, rindex) + for rindex in itertools.product( + *[range(x) for x in reduction_ranges] + ) + ), + ) + + value_fn: Callable[[Sequence[_IntLike], Sequence[_IntLike]], Any] + if reduction_type in ("argmin", "argmax"): + flatten_index = _fixed_indexer( + reduction_ranges, + FlexibleLayout.contiguous_strides(reduction_ranges), + ) + + def value_fn( + index: Sequence[_IntLike], rindex: Sequence[_IntLike] + ) -> tuple[OpsValue, OpsValue]: + rindex = [sympy.expand(i) for i in rindex] + return ( + inner_fn(index, rindex), + ops.index_expr(flatten_index(rindex), torch.int64), + ) + + return lambda index: fn(index)[1] + else: + value_fn = inner_fn + return fn + + @classmethod + # pyrefly: ignore [bad-override] + def create( + cls, + device: torch.device, + dst_dtype: torch.dtype, + src_dtype: torch.dtype, + inner_fn: Callable[..., Any], + ranges: Sequence[Expr], + reduction_ranges: Sequence[Expr], + reduction_type: ReductionType, + reduction_hint: ReductionHint = ReductionHint.DEFAULT, + input_node: IRNode | None = None, + ) -> TensorBox: + """ + Create a reduction node. May split the reduction to multiple layers to expose + more parallelism. + """ + reduction_numel = V.graph.sizevars.simplify(sympy_product(reduction_ranges)) + + if reduction_numel == 0: + # N.B. This is a hack to generate the literal of the given type + # Ideally, we should be fixing `def constant` in triton.py + # but it breaks due to hardcoded dtypes in other places + def py_cnst(val: object) -> bool | float | int: + if dst_dtype == torch.bool: + return bool(val) + elif dst_dtype.is_floating_point: + assert isinstance(val, SupportsFloat), type(val) + return float(val) + else: + assert isinstance(val, SupportsInt), type(val) + return int(val) + + rtypes_to_inits = { + "sum": py_cnst(0), + "xor_sum": py_cnst(0), + "prod": py_cnst(1), + "any": py_cnst(0), + # "all" is desugared to `!any(!val)` + } + + assert reduction_type in rtypes_to_inits, ( + f"{reduction_type} not supported for zero-dimension tensors!" + ) + + def const_fn(index: int) -> OpsValue: + return ops.constant(rtypes_to_inits[reduction_type], dst_dtype) + + return Pointwise.create( + device=device, + dtype=src_dtype, + inner_fn=const_fn, + ranges=list(ranges), + ) + + if reduction_numel == 1: + # this reduction is actually a pointwise op + if reduction_type in ("argmin", "argmax"): + + def fn(index: int) -> OpsValue: + return ops.constant(0, dst_dtype) + + else: + + def fn(index: int) -> OpsValue: + reduction_index = [sympy.S.Zero for _ in reduction_ranges] + return inner_fn(index, reduction_index) + + return Pointwise.create( + device=device, dtype=dst_dtype, inner_fn=fn, ranges=ranges + ) + + if ( + isinstance(reduction_numel, Integer) + and int(reduction_numel) < config.unroll_reductions_threshold + and (sympy_product(ranges) != 1 or is_gpu(device.type)) + and reduction_type != "dot" + ): + # When native matmul, don't unroll the dot reduction. + + # NB: This works around https://github.com/pytorch/pytorch/issues/140457 + # since turning reductions into pointwise ops can exacerbate this problem + return Pointwise.create( + device=device, + dtype=dst_dtype, + inner_fn=cls._unroll_reduction_fn( + inner_fn, reduction_ranges, reduction_type, src_dtype + ), + ranges=ranges, + ) + + # triton doesn't support reduce to single element well, so break it up + hint, split = cls.num_splits( + device, + dst_dtype, + src_dtype, + inner_fn, + ranges, + reduction_ranges, + reduction_type, + reduction_numel, + input_node, + ) + + def _maybe_increase_split(split: int) -> int: + # don't apply min_num_split constraint for static shape case. + if _is_static(reduction_numel): + return split + if split > 1: + return max(split, config.min_num_split) + else: + return split + + split = _maybe_increase_split(split) + + # intermediate reduction in split can contain complex indexing, + # and num_splits will fail to correctly set the hint + # reuse the passed hint if available + if reduction_hint == ReductionHint.DEFAULT: + reduction_hint = hint + if split == -1: + assert input_node is not None + with patch.object(FlexibleLayout, "allow_indexing", True): + new_ranges, new_reduction_ranges = extract_input_node_reduction_ranges( + input_node + ) + assert new_ranges is not None + assert new_reduction_ranges is not None + return cls.create_multilayer_existing_ranges( + device, + dst_dtype, + src_dtype, + inner_fn, + ranges, + reduction_ranges, + new_ranges, + new_reduction_ranges, + reduction_type, + reduction_hint, + ) + elif split > 1: + # triton doesn't support reduce to single element well, so break it up + out = cls.create_multilayer( + device, + dst_dtype, + src_dtype, + inner_fn, + ranges, + reduction_ranges, + reduction_type, + split, + reduction_hint, + input_node, + ) + + # Find the reduction that get split + split_reduction = None + if config.triton.mix_order_reduction and isinstance(out, TensorBox): + + def _find_split_reduction( + cur_node: TensorBox, + ) -> ComputedBuffer | None: + read_names = cur_node.get_read_names() + if len(read_names) != 1: + return None + + bufname = next(iter(read_names)) + if bufname not in V.graph.name_to_buffer: + return None + buf = V.graph.name_to_buffer[bufname] + if not isinstance(buf, ComputedBuffer): + return None + + assert buf.data.get_reduction_type() is not None + + return buf + + split_reduction = _find_split_reduction(out) + + if split_reduction: + # If a reduction is split to more than 2 layers, + # say there are 3 layers, + # we always have the correct setting for layer1 (top layer). + # The setting on layer2 may be incorrect but it's fine + # since they are never get used. + # TODO: should we skip setting these fields for layer2 + assert isinstance(split_reduction.data, Reduction), ( + f"{type(split_reduction.data)}" + ) + split_reduction._split_size = split_reduction.data.reduction_ranges[0] + split_reduction._original_inner_fn = inner_fn + split_reduction._original_ranges = ranges + split_reduction._original_reduction_ranges = reduction_ranges + return out + + out = TensorBox.create( + Reduction( + device=device, + dtype=dst_dtype, + inner_fn=inner_fn, + ranges=ranges, + reduction_ranges=reduction_ranges, + reduction_type=reduction_type, + src_dtype=src_dtype, + reduction_hint=reduction_hint, + ) + ) + return out + + @staticmethod + def default_accumulator( + reduction_type: str, dtype: torch.dtype + ) -> _NumLike | Sequence[_NumLike]: + if reduction_type in ("max", "argmax"): + if is_float_dtype(dtype): + return float("-inf") + elif is_boolean_dtype(dtype): + return False + else: + return torch.iinfo(dtype).min + if reduction_type in ("min", "argmin"): + if is_float_dtype(dtype): + return float("inf") + elif is_boolean_dtype(dtype): + return True + else: + return torch.iinfo(dtype).max + + zero = False if is_boolean_dtype(dtype) else 0 + one = True if is_boolean_dtype(dtype) else 1 + return { + "sum": zero, + "prod": one, + "dot": zero, + "xor_sum": zero, + "any": zero, + "welford_reduce": (zero, zero, zero), + "welford_combine": (zero, zero, zero), + "online_softmax_reduce": (float("-inf"), zero), + }[reduction_type] + + @staticmethod + def default_value( + reduction_type: str, dtype: torch.dtype + ) -> _NumLike | Sequence[_NumLike]: + if reduction_type == "welford_reduce": + return 0 + return Reduction.default_accumulator(reduction_type, dtype) + + @staticmethod + def _multilayer_second_step_hint( + split: _IntLike, numel_hint: int, reduction_hint: ReductionHint + ) -> ReductionHint: + if split == -1: + return reduction_hint + if split <= 512 and numel_hint <= 512 and reduction_hint == ReductionHint.OUTER: + return ReductionHint.OUTER_TINY + if ( + split <= 1024 + and numel_hint <= 256 + and reduction_hint == ReductionHint.OUTER + ): + return ReductionHint.OUTER_TINY + + return reduction_hint + + @classmethod + def check_for_split_dense_dim_reindexing( + cls, reduction_numel: _IntLike, input_node: IRNode | None + ) -> int | None: + """ + If we are reducing over the full tensor, and it is non-dense in the last dimension, + reindex so we reduce over the dense dimension. initially just handle complete + reduction case + """ + if input_node is None: + return None + + if not V.graph.sizevars.statically_known_equals( + input_node.get_numel(), reduction_numel + ): + return None + + input_node.realize() + try: + # finalize layout + as_storage_and_layout(input_node) + except NotImplementedError: + return None + + strides = input_node.get_stride() + + for i, s in enumerate(strides[:-1]): + if V.graph.sizevars.statically_known_equals(s, 1): + return i + + return None + + @classmethod + def _multilayer_wrap_loader( + cls, + loader: Callable[..., OpsValue], + reduction_ranges: Sequence[_IntLike], + reduction_numel: _IntLike, + split: _IntLike, + block_size: _IntLike, + default: _NumLike | Sequence[_NumLike], + input_node: IRNode | None = None, + ) -> Callable[..., object]: + dense_index = cls.check_for_split_dense_dim_reindexing( + reduction_numel, input_node + ) + reindex = View.dynamic_reshape_indexer( + reduction_ranges, [reduction_numel], dense_index + ) + need_mask = not V.graph.sizevars.statically_known_true( + sympy.Eq(Mod(reduction_numel, split), 0) + ) + + def wrapper_fn( + index: Sequence[Symbol], reduction_index: Sequence[Symbol] + ) -> OpsValue: + (reduction_index,) = reduction_index + *new_index, reduction_block = index + indices = block_size * reduction_block + reduction_index + + def body() -> OpsValue: + return loader(new_index, reindex([indices])) + + if need_mask: + index_dtype = dtype_from_size(reduction_numel) + mask = ops.lt( + ops.index_expr(indices, index_dtype), + ops.index_expr(reduction_numel, index_dtype), + ) + return ops.masked(mask, body, default) + else: + return body() + + return wrapper_fn + + @classmethod + def _multilayer_wrap_loader_existing_ranges( + cls, + loader: Callable[[Sequence[Expr], Sequence[Expr]], OpsValue], + original_ranges: Sequence[Expr], + original_reduction_ranges: Sequence[Expr], + new_ranges: Sequence[Integer], + new_reduction_ranges: Sequence[Integer], + ) -> Callable[[Sequence[sympy.Expr], Sequence[sympy.Expr]], OpsValue]: + assert all(r == 1 for r in original_ranges), ( + f"Only enabled for numel_hint == 1, found {original_ranges=}" + ) + reindex = View.dynamic_reshape_indexer( + original_reduction_ranges, tuple(new_ranges) + tuple(new_reduction_ranges) + ) + + def wrapper_fn( + merged_index: Sequence[Expr], + new_reduction_index: Sequence[Expr], + ) -> OpsValue: + original_idx = merged_index[: len(original_ranges)] + new_index = merged_index[len(original_ranges) :] + return loader( + original_idx, + reindex(tuple(new_index) + tuple(new_reduction_index)), + ) + + return wrapper_fn + + @classmethod + def create_multilayer_helper( + cls, + device: torch.device, + dst_dtype: torch.dtype, + src_dtype: torch.dtype, + wrapper_fn: Callable[..., Any], + original_ranges: Sequence[Expr], + original_reduction_ranges: Sequence[Expr], + new_ranges: list[Expr], + new_reduction_ranges: list[Integer], + reduction_type: ReductionType, + split: _IntLike, + reduction_hint: ReductionHint, + ) -> TensorBox: + """ + Break a large reduction up into multiple smaller reductions + recursively + """ + # triton will automatically compute reductions in fp32 if reducing over fp16/bf16 + # within the kernel. keep the intermediate in fp32 so as to keep the whole reduction + # in fp32 and not reduce precision by breaking up the kernel into multiple layers + intermediate_dtype = ( + dst_dtype + if dst_dtype not in (torch.float16, torch.bfloat16) + else torch.float + ) + intermediate = Reduction.create( + device, + intermediate_dtype, + src_dtype, + wrapper_fn, + new_ranges, + new_reduction_ranges, + reduction_type, + reduction_hint, + ) + intermediate.realize() + intermediate_loader = intermediate.make_loader() + + def intermediate_fn( + index: Sequence[_IntLike], reduction_index: Sequence[_IntLike] + ) -> OpsValue: + return intermediate_loader([*index, *reduction_index]) + + numel_hint = V.graph.sizevars.optimization_hint(sympy_product(original_ranges)) + reduction_hint = cls._multilayer_second_step_hint( + split, numel_hint, reduction_hint + ) + + assert original_ranges == new_ranges[: len(original_ranges)] + return TensorBox.create( + Reduction( + device=device, + dtype=dst_dtype, + inner_fn=intermediate_fn, + ranges=original_ranges, + reduction_ranges=new_ranges[len(original_ranges) :], + reduction_type=reduction_type, + src_dtype=src_dtype, + reduction_hint=reduction_hint, + ) + ) + + @classmethod + def create_multilayer( + cls, + device: torch.device, + dst_dtype: torch.dtype, + src_dtype: torch.dtype, + inner_fn: Callable[..., Any], + ranges: Sequence[Expr], + reduction_ranges: Sequence[Expr], + reduction_type: ReductionType, + split: _IntLike, + reduction_hint: ReductionHint, + input_node: IRNode | None = None, + ) -> TensorBox: + """ + Break a large reduction up into multiple smaller reductions + recursively + """ + # TODO(jansel): realize the reduction so we can do dynamic indexing + reduction_numel = sympy_product(reduction_ranges) + block_size = FloorDiv(reduction_numel + (split - 1), split) + default = cls.default_value(reduction_type, dst_dtype) + wrapper_fn = cls._multilayer_wrap_loader( + inner_fn, + reduction_ranges, + reduction_numel, + split, + block_size, + default, + input_node, + ) + + return cls.create_multilayer_helper( + device, + dst_dtype, + src_dtype, + wrapper_fn, + ranges, + reduction_ranges, + [*ranges, split], + [block_size], + reduction_type, + split, + reduction_hint, + ) + + @classmethod + def create_multilayer_existing_ranges( + cls, + device: torch.device, + dst_dtype: torch.dtype, + src_dtype: torch.dtype, + inner_fn: Callable[..., Any], + original_ranges: Sequence[Expr], + original_reduction_ranges: Sequence[Expr], + new_ranges: list[Integer], + new_reduction_ranges: list[Integer], + reduction_type: ReductionType, + reduction_hint: ReductionHint, + ) -> TensorBox: + """ + Break a large reduction up into multiple smaller reductions + recursively + """ + wrapper_fn = cls._multilayer_wrap_loader_existing_ranges( + inner_fn, + original_ranges, + original_reduction_ranges, + new_ranges, + new_reduction_ranges, + ) + return cls.create_multilayer_helper( + device, + dst_dtype, + src_dtype, + wrapper_fn, + original_ranges, + original_reduction_ranges, + [*original_ranges, *new_ranges], + new_reduction_ranges, + reduction_type, + -1, + reduction_hint, + ) + + +def _fixed_indexer( + size: Sequence[int], + stride: Sequence[int] | None = None, + offset: Expr = Integer(0), +) -> Callable[[Sequence[Expr]], Expr]: + """A closure containing math to read a given element""" + + def indexer(index: Sequence[int]) -> int: + assert stride is not None and len(index) == len(stride) + assert len(index) == len(size) + result = offset + for idx, st, sz in zip(index, stride, size): + if sz != 1: + result = result + idx * st + return result + + return indexer + + +INNER_FN_TY: TypeAlias = Callable[[Sequence[Expr], Sequence[Expr]], OpsValue] + + +class MultiOutputReduction(Reduction): + output_index: int + + def __init__( + self, + device: torch.device, + dst_dtype: torch.dtype, + inner_fns: INNER_FN_TY | Sequence[INNER_FN_TY], + ranges: Sequence[Integer], + reduction_ranges: Sequence[Integer], + reduction_type: ReductionType, + src_dtype: torch.dtype, + reduction_hint: ReductionHint, + output_index: int, + ): + if callable(inner_fns): + inner_fns = (inner_fns,) + + loader: Callable[[Sequence[Expr], Sequence[Expr]], Any] + if len(inner_fns) == 1: + loader = inner_fns[0] + else: + + def loader( + idx: Sequence[Expr], reduction_idx: Sequence[Expr] + ) -> tuple[OpsValue, ...]: + return tuple(fn(idx, reduction_idx) for fn in inner_fns) + + super().__init__( + device=device, + dtype=dst_dtype, + inner_fn=loader, + ranges=ranges, + reduction_ranges=reduction_ranges, + reduction_type=reduction_type, + src_dtype=src_dtype, + reduction_hint=reduction_hint, + ) + self.output_index = output_index + + def store_reduction( + self, + output_name: str | None, + indexer: Callable[[Sequence[Expr]], Never], + vars: Sequence[Expr], + reduction_vars: Sequence[Symbol], + ) -> Any: + values = ops.reduction( + self.dtype, + self.src_dtype, + self.reduction_type, + self.inner_fn(vars, reduction_vars), + ) + assert isinstance(values, (tuple, list)), type(values) + value = values[self.output_index] + return ops.store_reduction(output_name or "unnamed", indexer(vars), value) + + +class OnlineSoftmaxReduction(MultiOutputReduction): + @classmethod + def create( # type: ignore[override] + cls, + device: torch.device, + dst_dtype: torch.dtype, + src_dtype: torch.dtype, + inner_fn: Callable[..., Any], + ranges: Sequence[Expr], + reduction_ranges: Sequence[Expr], + num_output: int, + reduction_hint: ReductionHint = ReductionHint.DEFAULT, + input_node: IRNode | None = None, + ) -> Sequence[TensorBox]: + """ + Create the reduction disregarding splitting. + """ + results = tuple( + TensorBox.create( + MultiOutputReduction( + device, + dst_dtype, + inner_fn, + ranges, + reduction_ranges, + "online_softmax_reduce", + src_dtype, + reduction_hint, + output_idx, + ) + ) + for output_idx in range(num_output) + ) + for t in results: + t.realize() + return results + + +class WelfordReduction(MultiOutputReduction): + @classmethod + def create( # type: ignore[override] + cls, + device: torch.device, + dtype: torch.dtype, + inner_fns: Sequence[Callable[..., Any]], + ranges: list[Integer], + reduction_ranges: list[Integer], + reduction_type: ReductionType, + reduction_hint: ReductionHint = ReductionHint.DEFAULT, + ) -> Sequence[TensorBox]: + assert reduction_type in ("welford_reduce", "welford_combine") + assert not config.mtia.disable_welford_reduction, ( + "welford reduction usage is explicitly disabled, please check you config" + ) + + reduction_numel = V.graph.sizevars.simplify(sympy_product(reduction_ranges)) + + def const(val: int) -> TensorBox: + def inner_fn(idx: Sequence[Expr]) -> OpsValue: + return ops.constant( + val, + dtype, + ) + + return Pointwise.create( + device=device, + dtype=dtype, + inner_fn=inner_fn, + ranges=list(ranges), + ) + + if reduction_numel == 0: + mean = const(0) + m2 = const(0) + weight = const(0) + return mean, m2, weight + + if reduction_numel == 1: + + def copy( + loader: Callable[[Sequence[Expr], Sequence[Expr]], OpsValue], + ) -> TensorBox: + def inner_fn(idx: Sequence[Expr]) -> OpsValue: + reduction_index = [sympy.S.Zero for _ in reduction_ranges] + return loader(idx, reduction_index) + + return Pointwise.create( + device=device, + dtype=dtype, + inner_fn=inner_fn, + ranges=list(ranges), + ) + + if reduction_type == "welford_reduce": + return copy(inner_fns[0]), const(0), const(1) + else: + return tuple(copy(fn) for fn in inner_fns) + + # TODO: Unrolled reduction + # if ( + # isinstance(reduction_numel, Integer) + # and int(reduction_numel) + # < config.unroll_reductions_threshold + # and sympy_product(ranges) != 1 + # ): + # return Pointwise.create( + # device, + # dst_dtype, + # cls._unroll_reduction_fn( + # inner_fn, reduction_ranges, reduction_type, src_dtype, + # ), + # ranges, + # ) + + # triton doesn't support reduce to single element well, so break it up + hint, split = Reduction.num_splits( + device, + dtype, + dtype, + inner_fns[0], + ranges, + reduction_ranges, + reduction_type=reduction_type, + reduction_numel=reduction_numel, + ) + # intermediate reduction in split can contain complex indexing, + # and num_splits will fail to correctly set the hint + # reuse the passed hint if available + if reduction_hint == ReductionHint.DEFAULT: + reduction_hint = hint + if split > 1: + # triton doesn't support reduce to single element well, so break it up + return cls.create_multilayer( + device, + dtype, + inner_fns, + ranges, + reduction_ranges, + reduction_type, + split, + reduction_hint, + ) + + results = [ + TensorBox.create( + WelfordReduction( + device, + dtype, + inner_fns, + ranges, + reduction_ranges, + reduction_type, + dtype, + reduction_hint, + output_idx, + ) + ) + for output_idx in range(3) + ] + for t in results: + t.realize() + return results + + @staticmethod + def default_value( + reduction_type: str, dtype: torch.dtype + ) -> _NumLike | Sequence[_NumLike]: + return (0, 0, 0) + + @classmethod + def create_multilayer( # type: ignore[override] + cls, + device: torch.device, + dtype: torch.dtype, + inner_fns: Sequence[Callable[..., Any]], + ranges: list[Integer], + reduction_ranges: list[Integer], + reduction_type: ReductionType, + split: _IntLike, + reduction_hint: ReductionHint, + ) -> Sequence[TensorBox]: + """ + Break a large reduction up into multiple smaller reductions + recursively + """ + reduction_numel = sympy_product(reduction_ranges) + need_mask = not V.graph.sizevars.statically_known_true( + sympy.Eq(Mod(reduction_numel, split), 0) + ) + + if need_mask and reduction_type != "welford_combine": + # If we need mask, then "welford_reduce" doesn't work because + # masked inputs shouldn't count towards the welford weight + + def constant( + idx: Sequence[Expr], reduction_idx: Sequence[Expr], value: int + ) -> OpsValue: + return ops.constant(value, dtype) + + return cls.create_multilayer( + device=device, + dtype=dtype, + inner_fns=( + inner_fns[0], + partial(constant, value=0), + partial(constant, value=1), + ), + ranges=ranges, + reduction_ranges=reduction_ranges, + reduction_type="welford_combine", + split=split, + reduction_hint=reduction_hint, + ) + + block_size = FloorDiv(reduction_numel + (split - 1), split) + intermediates = WelfordReduction.create( + device, + dtype, + tuple( + cls._multilayer_wrap_loader( + loader, + reduction_ranges, + reduction_numel, + split, + block_size, + default=0, + ) + for loader in inner_fns + ), + [*ranges, split], + [block_size], + reduction_type, + reduction_hint, + ) + for i in intermediates: + i.realize() + + def intermediate_loader_fn( + index: Sequence[Expr], + reduction_index: Sequence[Expr], + loader: Callable[[Sequence[Expr]], OpsValue], + ) -> OpsValue: + return loader([*index, *reduction_index]) + + # numel_hint is only used to choose between ReductionHint.OUTER vs + # OUTER_TINY, which is a performance heuristic, not a correctness decision. + numel_hint = V.graph.sizevars.optimization_hint(sympy_product(ranges)) + reduction_hint = cls._multilayer_second_step_hint( + split, numel_hint, reduction_hint + ) + return WelfordReduction.create( + device, + dtype, + tuple( + partial(intermediate_loader_fn, loader=i.make_loader()) + for i in intermediates + ), + ranges, + [split], + # welford_reduce turns one input into three outputs, which are combined with welford_combine + "welford_combine", + reduction_hint, + ) + + +@ir_dataclass +class Scan(Loops): + scan_ranges: list[Integer] + size: list[Integer] + combine_fn: Callable[[tuple[Any, ...], tuple[Any, ...]], tuple[Any, ...]] + reindex: Callable[[Sequence[_IntLike], Sequence[_IntLike]], Sequence[_IntLike]] + reduction_hint: ReductionHint + output_index: int + # output_index indexes the following tuples + dtypes: tuple[torch.dtype, ...] + inner_fns: tuple[Callable[..., Any], ...] + + # HACK we mimic reduction + + @cache_on_self_and_args("Scan") + def get_free_symbol_uses(self, unbacked_only: bool = False) -> OrderedSet[Symbol]: + # TODO: Can combine_fn/reindex close over unbacked symbols? If so, we + # need to explicitly represent the closure so we can pull out unbacked + # symbols here + return ( + super().get_free_symbol_uses(unbacked_only) + | OrderedSet().union( + *(get_free_symbols(e, unbacked_only) for e in self.scan_ranges) + ) + | OrderedSet().union( + *(get_free_symbols(e, unbacked_only) for e in self.size) + ) + ) + + def __post_init__(self) -> None: + assert len(self.ranges) + len(self.scan_ranges) == len(self.size) + super().__post_init__() + + def store_reduction( + self, + output_name: str | None, + indexer: Callable[[Sequence[_IntLike]], Never], + vars: Sequence[Expr], + scan_vars: Sequence[Symbol], + ) -> Any: + idx = self.reindex(vars, scan_vars) + values = tuple(inner_fn(idx) for inner_fn in self.inner_fns) + result = ops.scan(self.dtypes, self.combine_fn, values) + return ops.store( + output_name or "unnamed", indexer(idx), result[self.output_index] + ) + + def get_reduction_type(self) -> str | None: + # return self.scan_op + return "custom" + + def get_reduction_size(self) -> Sequence[Expr]: + return self.scan_ranges + + def get_size(self) -> Sequence[Expr]: + return self.size + + def get_pointwise_size(self) -> Sequence[Expr]: + return self.ranges + + def index_length(self) -> int: + return len(self.ranges) + len(self.scan_ranges) + + def inner_fn_args(self) -> Sequence[Sequence[_IntLike]]: + index = self._index(self.ranges) + rindex = self._index(self.scan_ranges, SymT.R0_INDEX) + idx = self.reindex(index, rindex) + return (idx,) + + def inner_fn_free_symbols(self, unbacked_only: bool = False) -> OrderedSet[Symbol]: + index = self._index(self.ranges) + rindex = self._index(self.scan_ranges, SymT.R0_INDEX) + idx = self.reindex(index, rindex) + return extract_free_symbols(self.inner_fn, idx, unbacked_only=unbacked_only) + + @classmethod + def create( # type: ignore[override] + cls, + device: torch.device, + dtypes: tuple[torch.dtype, ...], + inner_fns: tuple[Callable[[Sequence[Expr]], Any], ...], + size: list[Integer], + axis: int, + combine_fn: Callable[[tuple[Any, ...], tuple[Any, ...]], tuple[Any, ...]], + reduction_hint: ReductionHint = ReductionHint.DEFAULT, + *, + # Whether we have the option to fallback to aten + can_fallback_to_aten: bool = True, + **kwargs: Any, + ) -> Sequence[TensorBox | None]: + pointwise_ranges = [*size[:axis], *size[axis + 1 :]] + scan_ranges = [size[axis]] + + if not V.graph.has_feature(device, BackendFeature.SCAN): + return [None] * len(dtypes) + + if len(dtypes) > 1 and not V.graph.has_feature( + device, BackendFeature.TUPLE_REDUCTION + ): + return [None] * len(dtypes) + + sizevars = V.graph.sizevars + scan_numel = sizevars.simplify(sympy_product(scan_ranges)) + + assert len(dtypes) == len(inner_fns) + + # Scan with a single element is just a copy + if sizevars.statically_known_true(sympy.Le(scan_numel, 1)): + return [ + Pointwise.create( + device=device, + dtype=dtypes[output_index], + inner_fn=inner_fns[output_index], + ranges=size, + ) + for output_index in range(len(dtypes)) + ] + + reduction_hint, num_splits = cls.num_splits( + device=device, + dtype=dtypes[0], + inner_fn=inner_fns[0], + axis=axis, + pointwise_ranges=pointwise_ranges, + scan_ranges=scan_ranges, + combine_fn=combine_fn, + scan_numel=scan_numel, + ) + scan_type = Scan + if num_splits > 1: + supports_split = ( + # pyrefly: ignore [unsupported-operation] + torch.version.hip is None or (has_triton and triton_version >= "3.3.0") + ) and (len(dtypes) == 1) + if not supports_split: + if can_fallback_to_aten: + # Fallback to ATen + return [None] * len(dtypes) + else: + num_splits = 1 + else: + scan_type = SplitScan + + def reindex(index: Sequence[Expr], scan_index: Sequence[Expr]) -> list[Expr]: + assert len(scan_index) == len(scan_ranges) + assert len(index) == len(pointwise_ranges) + return [*index[:axis], *scan_index, *index[axis:]] + + results = [ + TensorBox.create( + scan_type( + device=device, + dtype=dtypes[output_index], + dtypes=dtypes, + inner_fn=inner_fns[output_index], + inner_fns=inner_fns, + size=size, + ranges=pointwise_ranges, + scan_ranges=scan_ranges, + combine_fn=combine_fn, + reindex=reindex, + reduction_hint=reduction_hint, + output_index=output_index, + **kwargs, + ) + ) + for output_index in range(len(dtypes)) + ] + + for result in results: + result.realize() + + return results + + @classmethod + def num_splits( + cls, + device: torch.device, + dtype: torch.dtype, + inner_fn: Callable[[Sequence[Expr]], OpsValue], + axis: int, + pointwise_ranges: list[Integer], + scan_ranges: list[Integer], + combine_fn: Callable[[tuple[Any, ...], tuple[Any, ...]], tuple[Any, ...]], + scan_numel: Expr, + ) -> tuple[ReductionHint, _IntLike]: + # TODO: custom splitting heuristic for scan + def wrapper_fn(idx: Sequence[Expr], reduction_idx: Sequence[Expr]) -> OpsValue: + return inner_fn([*idx[:axis], *reduction_idx, *idx[axis:]]) + + return Reduction.num_splits( + device=device, + dst_dtype=dtype, + src_dtype=dtype, + inner_fn=wrapper_fn, + ranges=pointwise_ranges, + reduction_ranges=scan_ranges, + reduction_type="scan", + reduction_numel=scan_numel, + ) + + +# This signifies a scan op that should go through TritonSplitScanKernel codegen on CUDA. +@ir_dataclass +class SplitScan(Scan): + pass + + +@ir_dataclass +class Sort(Loops): + # Sorts a tuple of key, value pairs + sort_ranges: list[Integer] + size: list[Integer] + reindex: Callable[[Sequence[Expr], Sequence[Expr]], Sequence[Expr]] + reduction_hint: ReductionHint + output_index: int + # output_index indexes the following tuples + dtypes: tuple[torch.dtype, ...] + inner_fns: tuple[Callable[..., Any], ...] + + stable: bool + descending: bool + + # HACK we mimic reduction + + @cache_on_self_and_args("Sort") + def get_free_symbol_uses(self, unbacked_only: bool = False) -> OrderedSet[Symbol]: + return ( + super().get_free_symbol_uses(unbacked_only) + | OrderedSet().union( + *(get_free_symbols(e, unbacked_only) for e in self.sort_ranges) + ) + | OrderedSet().union( + *(get_free_symbols(e, unbacked_only) for e in self.size) + ) + ) + + def __post_init__(self) -> None: + assert len(self.ranges) + len(self.sort_ranges) == len(self.size) + super().__post_init__() + + def store_reduction( + self, + output_name: str | None, + indexer: Callable[[Sequence[Expr]], Expr], + vars: Sequence[Expr], + reduction_vars: Sequence[Expr], + ) -> Any: + idx = self.reindex(vars, reduction_vars) + values = tuple(inner_fn(idx) for inner_fn in self.inner_fns) + result = ops.sort(self.dtypes, values, self.stable, self.descending) + return ops.store( + output_name or "unnamed", indexer(idx), result[self.output_index] + ) + + def get_reduction_type(self) -> str | None: + return "sort" + + def get_reduction_size(self) -> Sequence[Expr]: + return self.sort_ranges + + def get_size(self) -> Sequence[Expr]: + return self.size + + def get_pointwise_size(self) -> Sequence[Expr]: + return self.ranges + + def index_length(self) -> int: + return len(self.ranges) + len(self.sort_ranges) + + def inner_fn_args(self) -> Sequence[Sequence[Expr]]: + index = self._index(self.ranges) + rindex = self._index(self.sort_ranges, SymT.R0_INDEX) + idx = self.reindex(index, rindex) + return (idx,) + + def inner_fn_free_symbols(self, unbacked_only: bool = False) -> OrderedSet[Symbol]: + index = self._index(self.ranges) + rindex = self._index(self.sort_ranges, SymT.R0_INDEX) + idx = self.reindex(index, rindex) + return extract_free_symbols(self.inner_fn, idx, unbacked_only=unbacked_only) + + @classmethod + def create( # type: ignore[override] + cls, + device: torch.device, + dtypes: tuple[torch.dtype, ...], + inner_fns: tuple[Callable[[list[Expr]], Any], ...], + size: list[Integer], + axis: int, + stable: bool, + descending: bool, + reduction_hint: ReductionHint = ReductionHint.DEFAULT, + **kwargs: Any, + ) -> Sequence[TensorBox | None]: + pointwise_ranges = [*size[:axis], *size[axis + 1 :]] + sort_ranges = [size[axis]] + + if not V.graph.has_feature(device, BackendFeature.SORT): + return [None] * len(dtypes) + + sizevars = V.graph.sizevars + sort_numel = sizevars.simplify(sympy_product(sort_ranges)) + + # Heuristic, smallest rblock where triton usually outperforms aten.sort. + # It also isn't bandwidth bound so fusion is unlikely to help. + # When decompose_sort_ops is enabled, skip the size limit to always + # attempt Triton sort (index dtype is widened to int32 in lowering). + if config.triton.decompose_sort_ops: + is_persistent_kernel = config.triton.persistent_reductions + else: + max_rblock = 512 + is_persistent_kernel = ( + config.triton.persistent_reductions + and sizevars.statically_known_true(sympy.Le(sort_numel, max_rblock)) + ) + if not is_persistent_kernel: + # We only support persistent triton kernels + return [None] * len(dtypes) + + assert len(dtypes) == len(inner_fns) + + # Sort with a single element is just a copy + if sizevars.statically_known_true(sympy.Le(sort_numel, 1)): + return [ + Pointwise.create( + device=device, + dtype=dtypes[output_index], + inner_fn=inner_fns[output_index], + ranges=size, + ) + for output_index in range(len(dtypes)) + ] + + def reindex(index: Sequence[Expr], sort_index: Sequence[Expr]) -> list[Expr]: + assert len(sort_index) == len(sort_ranges) + assert len(index) == len(pointwise_ranges) + return [*index[:axis], *sort_index, *index[axis:]] + + results = [ + TensorBox.create( + Sort( + device=device, + dtype=dtypes[output_index], + dtypes=dtypes, + inner_fn=inner_fns[output_index], + inner_fns=inner_fns, + size=size, + ranges=pointwise_ranges, + sort_ranges=sort_ranges, + reindex=reindex, + reduction_hint=reduction_hint, + output_index=output_index, + stable=stable, + descending=descending, + **kwargs, + ) + ) + for output_index in range(len(dtypes)) + ] + + for result in results: + result.realize() + + return results + + +def is_storage_and_layout(x: IRNode) -> bool: + try: + as_storage_and_layout(x, freeze=False) + return True + except NotImplementedError: + return False + + +def is_contiguous_storage_and_layout(x: IRNode) -> bool: + try: + _buffer, layout = as_storage_and_layout(x, freeze=False) + # pad the stride here so we will NOT claim an tensor as contiguous + # if a padding is gonna happen. + if layout.should_pad_strides(): + layout.pad_strides() + return layout.is_contiguous() + except NotImplementedError: + return False + + +def as_storage_and_layout( + x: IRNode, + freeze: bool = True, + want_contiguous: bool = False, + stride_order: Sequence[int | Integer] | None = None, + allow_padding: bool = False, + exact_strides: Sequence[int | Integer] | None = None, +) -> tuple[StorageBox, Layout]: + """ + Try to simplify x into a StorageBox and a Layout. + + allow_padding only affect how we apply stride_order. When allow_padding + is True, we have the freedom to add padding when applying the stride_order. + """ + if isinstance(x, TensorBox): + return as_storage_and_layout( + x.data, + freeze=freeze, + want_contiguous=want_contiguous, + stride_order=stride_order, + allow_padding=allow_padding, + exact_strides=exact_strides, + ) + if isinstance(x, StorageBox): + _, layout = as_storage_and_layout( + x.data, + freeze=freeze, + want_contiguous=want_contiguous, + stride_order=stride_order, + allow_padding=allow_padding, + exact_strides=exact_strides, + ) + return x, x.data.get_layout() + if isinstance(x, Buffer): + if freeze: + if want_contiguous: + x.freeze_layout() + assert x.get_layout().is_contiguous() + elif stride_order is not None: + x.freeze_layout_with_stride_order( + stride_order, allow_padding=allow_padding + ) + elif exact_strides is not None: + x.freeze_layout_with_exact_strides( + exact_strides, allow_padding=allow_padding + ) + else: + x.decide_layout() + return StorageBox(x), x.get_layout() + if isinstance(x, ReinterpretView): + # making the base of x contiguous or stride_ordered will not necessarily make + # the ReinterpretView either, so don't pass along those arguments + buffer, _ = as_storage_and_layout( + x.data, + freeze=freeze, + ) + return buffer, x.layout + raise NotImplementedError + + +def is_stride_order_storage_and_layout( + x: IRNode, stride_order: Sequence[int | Integer] +) -> bool: + try: + _buffer, layout = as_storage_and_layout(x, freeze=False) + return layout.is_stride_ordered(stride_order) + except NotImplementedError: + return False + + +def is_unaligned(node: IRNode) -> bool: + if isinstance(node, (TensorBox, StorageBox)): + return is_unaligned(node.data) + + if isinstance(node, ReinterpretView): + layout = node.layout + has_unaligned_layout = not V.graph.sizevars.statically_known_multiple_of( + layout.offset * get_dtype_size(layout.dtype), GPU_ALIGN_BYTES + ) + return is_unaligned(node.data) or has_unaligned_layout + + if isinstance(node, Buffer): + return node.get_name() in V.graph.unaligned_buffers + + # assume to be aligned otherwise + return False + + +@ir_dataclass +class BaseView(IRNode): + data: IRNode + + @cache_on_self_and_args("BaseView") + def get_free_symbol_uses(self, unbacked_only: bool = False) -> OrderedSet[Symbol]: + return self.data.get_free_symbol_uses(unbacked_only) + + def make_reindexer(self) -> Callable[[Sequence[Expr]], Sequence[Expr]]: + raise NotImplementedError(f"make_reindexer NYI on {self}") + + def make_indexer(self) -> Callable[[Sequence[Expr]], Expr]: + inner = self.data.make_indexer() + reindex = self.make_reindexer() + + def indexer(idx: Sequence[Expr]) -> Expr: + return inner(reindex(idx)) + + return indexer + + def make_loader(self) -> Callable[[Sequence[Expr]], OpsValue]: + inner = self.data.make_loader() + reindex = self.make_reindexer() + + def loader(idx: Sequence[Expr]) -> OpsValue: + return inner(reindex(idx)) + + return loader + + @property + def dtype(self) -> torch.dtype: + return self.data.get_dtype() + + def get_layout(self) -> Layout: + return self.data.get_layout() + + def get_device(self) -> torch.device | None: + return self.data.get_device() + + def get_origin_node(self) -> torch.fx.Node | None: + return None + + def get_name(self) -> str: + return self.data.get_name() + + def get_pointwise_size(self) -> Sequence[Expr]: + return self.get_size() + + def mark_reuse(self, users: int) -> None: + return self.data.mark_reuse(users) + + def has_exceeded_max_reads(self) -> bool: + return self.data.has_exceeded_max_reads() + + def realize(self) -> str | None: + return self.data.realize() + + def realize_hint(self) -> None: + self.data.realize_hint() + + def get_storage_numel(self) -> _IntLike: + return self.data.get_storage_numel() + + def is_extern(self) -> bool: + return self.data.is_extern() + + def is_module_buffer(self) -> bool: + assert isinstance(self.data, BaseView), type(self.data) + return self.data.is_module_buffer() + + def get_read_names(self) -> OrderedSet[str]: + return self.data.get_read_names() + + def get_reads(self) -> OrderedSet[Dep]: + with patch.object(FlexibleLayout, "allow_indexing", True): + return extract_read_writes( + self.make_loader(), + self.get_size(), + ).reads + + def unwrap_view(self) -> IRNode: + x: IRNode = self + while isinstance(x, BaseView): + x = x.data + return x + + def constant_to_device(self, device: torch.device) -> IRNode: + """Move this to a given device. Requires that all reads are to constants.""" + loader = self.make_loader() + loader = patch.object(ConstantBuffer, "override_device", device)(loader) + return Pointwise( + device=device, + dtype=self.get_dtype(), + inner_fn=loader, + ranges=self.get_size(), + ) + + +@ir_dataclass +class ExpandView(BaseView): + size: Sequence[Expr] + + @staticmethod + def _normalize_size(x: IRNode, new_size: Sequence[_IntLike]) -> Sequence[_IntLike]: + """Replace `-1` with correct sizes""" + sizevars = V.graph.sizevars + new_size = [sympy.expand(s) for s in new_size] + old_size = x.get_size() + old_size = [None] * (len(new_size) - len(old_size)) + list(old_size) + assert len(new_size) == len(old_size) + for i in range(len(new_size)): + if new_size[i] == -1: + assert old_size[i] is not None + new_size[i] = old_size[i] + elif old_size[i] is None or V.graph.sizevars.is_size_one_or_false( + old_size[i] + ): + pass + elif not has_free_unbacked_symbols( + old_size + ) and not has_free_unbacked_symbols(new_size): + # Sanity check: Expect broadcast compatibility + # + # NB: new_size[i] == old_size[i] is expected to already be + # guarded because the meta formula was expected to have taught + # us this equality. + v1 = new_size[i] + v2 = old_size[i] + assert v1 is not None + assert v2 is not None + diff = v1 - v2 + assert ( + sizevars.optimization_hint( + diff, + fallback=0, + ) + == 0 + ), ( + f"Broadcast failed in ExpandView({x.get_size()}, {new_size}) on dimension {i}" + ) + return new_size + + @classmethod + def create(cls, x: IRNode, new_size: Sequence[_IntLike]) -> BaseView: + new_size = cls._normalize_size(x, new_size) + + if is_storage_and_layout(x): + storage, old_layout = as_storage_and_layout(x) + skip = len(new_size) - len(old_layout.size) + assert skip >= 0 + new_stride = [sympy.S.Zero] * skip + for stride, size in zip(old_layout.stride, old_layout.size): + new_stride.append( + stride + if not V.graph.sizevars.is_size_one_or_false(size) + else sympy.S.Zero + ) + new_layout = FixedLayout( + old_layout.device, + old_layout.dtype, + list(new_size), + new_stride, + old_layout.offset, + old_layout.is_pinned, + ) + return ReinterpretView(data=storage, layout=new_layout) + + return ExpandView(data=x, size=new_size) + + def get_size(self) -> Sequence[Expr]: + return self.size + + def make_reindexer( + self, + ) -> Callable[[Sequence[Expr]], Sequence[Expr]]: + target = self.get_size() + actual = self.data.get_size() + skip = len(target) - len(actual) + + def reindex( + index: Sequence[Expr], + ) -> Sequence[Expr]: + index = list(index[skip:]) + assert len(index) == len(actual) + for i in range(len(actual)): + if actual[i] == 1: + # zero out broadcast dimension + index[i] = sympy.S.Zero + return index + + return reindex + + +@ir_dataclass +class PermuteView(BaseView): + dims: list[Expr] + + @classmethod + def create(cls, x: IRNode, dims: Sequence[int]) -> BaseView: + dims = cls._map_neg_dims(dims) + assert OrderedSet(dims) == OrderedSet(range(len(dims))) + + if is_storage_and_layout(x): + storage, old_layout = as_storage_and_layout(x) + new_layout = FixedLayout( + old_layout.device, + old_layout.dtype, + [old_layout.size[i] for i in dims], + [old_layout.stride[i] for i in dims], + old_layout.offset, + old_layout.is_pinned, + ) + return ReinterpretView(data=storage, layout=new_layout) + + return PermuteView(data=x, dims=dims) + + @classmethod + def _map_neg_dims(cls, dims: Sequence[int]) -> list[int]: + return [dim if dim >= 0 else len(dims) + dim for dim in dims] + + def get_size(self) -> Sequence[Expr]: + assert OrderedSet(self._map_neg_dims(self.dims)) == OrderedSet( + range(len(self.dims)) + ) + size = self.data.get_size() + return [size[i] for i in self.dims] + + def make_reindexer( + self, + ) -> Callable[[Sequence[Expr]], Sequence[Expr]]: + inv = {j: i for i, j in enumerate(self.dims)} + inv = [inv[i] for i in range(len(self.dims))] + assert OrderedSet(inv) == OrderedSet(range(len(self.dims))) + + def reindex( + index: Sequence[Expr], + ) -> Sequence[Expr]: + return [index[i] for i in inv] + + return reindex + + +@ir_dataclass +class SqueezeView(BaseView): + @classmethod + def create(cls, x: IRNode, *, dim: int | None = None) -> IRNode: + if is_storage_and_layout(x): + storage, old_layout = as_storage_and_layout(x) + new_size = [] + new_stride = [] + if dim is not None: + assert isinstance(dim, int), type(dim) + assert 0 <= dim and dim < len(old_layout.size) + + for i, (size, stride) in enumerate(zip(old_layout.size, old_layout.stride)): + if dim is None: + # Only append if dim is not squeezed out + if not V.graph.sizevars.is_size_one_or_false(size): + new_size.append(size) + new_stride.append(stride) + else: + if i != dim: + new_size.append(size) + new_stride.append(stride) + else: + assert size == 1, "expected squeezed size to be 1" + + new_layout = FixedLayout( + old_layout.device, + old_layout.dtype, + new_size, + new_stride, + old_layout.offset, + old_layout.is_pinned, + ) + return ReinterpretView(data=storage, layout=new_layout) + + if dim is None: + return View.create( + x, + [ + s + for s in x.get_size() + if not V.graph.sizevars.is_size_one_or_false(s) + ], + ) + else: + assert x.get_size()[dim] == 1 + return View.create(x, [s for i, s in enumerate(x.get_size()) if i != dim]) + + @staticmethod + def squeezer( + size: Sequence[Expr], + ) -> tuple[list[int], Callable[[Sequence[Expr]], tuple[Expr, ...]]]: + new_size = [s for s in size if s != 1] + not_one = [i for i, s in enumerate(size) if s != 1] + length = len(size) + + def reindex(index: Sequence[Expr]) -> tuple[Expr, ...]: + assert len(index) == len(not_one), f"{index} {not_one}" + new_index: list[Expr] = [sympy.S.Zero] * length + for idx, s in zip(not_one, index): + new_index[idx] = s + return tuple(new_index) + + return new_size, reindex + + def __init__(self, data: Any) -> None: + raise AssertionError("use SqueezeView.create()") + + +@ir_dataclass +class GenericView(BaseView): + size: Sequence[Expr] + reindex: Callable[[Sequence[Expr]], Sequence[Expr]] + + def make_reindexer( + self, + ) -> Callable[[Sequence[Expr]], Sequence[Expr]]: + return self.reindex + + def reindex_str(self) -> str: + index_old = [ + sympy_index_symbol_with_prefix(SymT.INDEX, n) for n in range(len(self.size)) + ] + index_new = list(self.reindex(index_old)) + return f"lambda {', '.join(map(str, index_old))}: {index_new}" + + def __str__(self) -> str: + return self.str_helper( + [self.data, f"size={self.size}", f"reindex={self.reindex_str()}"] + ) + + __repr__ = __str__ + + @classmethod + def create( + cls, + x: IRNode, + new_size: Sequence[Expr], + reindex: Callable[[Sequence[Expr]], Sequence[Expr]], + ) -> BaseView: + return cls(data=x, size=list(new_size), reindex=reindex) + + def get_size(self) -> Sequence[Expr]: + return self.size + + +@ir_dataclass +class View(GenericView): + """ + This class handles tensor reshaping by computing appropriate index transformations + to map the new shape back to the original storage layout. + """ + + @staticmethod + def handle_negative_index(idx: Expr, size: Expr) -> Expr: + idx = sympy.expand(idx) + size = sympy.expand(size) + evaluate_expr = V.graph.sizevars.shape_env.evaluate_expr + if evaluate_expr(sympy.Lt(idx, 0)): + idx = idx + size + return idx + + @classmethod + @override + def create(cls, x: IRNode, new_size: Sequence[Expr]) -> IRNode: # type: ignore[override] + assert isinstance(new_size, Sequence), type(new_size) + old_size, new_size = cls.resolve_negative_size(x.get_size(), new_size) + + # Skip pointless views + if V.graph.sizevars.statically_known_list_equals(old_size, new_size): + return x + + unbacked_symbols_in_sizes = ( + len(free_unbacked_symbols(old_size)) > 0 + or len(free_unbacked_symbols(new_size)) > 0 + ) + is_contiguous = is_contiguous_storage_and_layout(x) + + def create_reinterpret_view( + inp: IRNode, new_size: Sequence[Expr], new_stride: Sequence[Expr] + ) -> ReinterpretView: + storage, old_layout = as_storage_and_layout(inp, want_contiguous=True) + new_layout = FixedLayout( + old_layout.device, + old_layout.dtype, + new_size, + new_stride, + old_layout.offset, + old_layout.is_pinned, + ) + return ReinterpretView(data=storage, layout=new_layout) + + def handle_unbacked_or_dynamic_reshape( + x: IRNode, + ) -> IRNode: + """ + Handle the case where view is not possible with current strides. + Try dynamic_reshape_indexer first; if it fails with unbacked + symbols (guard_or_false can't resolve comparisons), fall back + to making the tensor contiguous. + """ + nonlocal old_size, new_size + try: + reindex = cls.dynamic_reshape_indexer(old_size, new_size) + return cls(data=x, size=list(new_size), reindex=reindex) + except GuardOnDataDependentSymNode: + # dynamic_reshape_indexer cannot handle unbacked SymInts + # because guard_or_false can't resolve size comparisons. + # https://github.com/pytorch/pytorch/issues/145561 + x = ExternKernel.require_contiguous(x) + return create_reinterpret_view( + x, new_size, FlexibleLayout.contiguous_strides(new_size) + ) + + if 0 in new_size: + + def fake_reindex(index: Any) -> tuple[int, ...]: + return tuple([0] * len(old_size)) + + return cls(data=x, size=list(new_size), reindex=fake_reindex) + + # TODO: a new class for FixedTransferLayout that output layout is constrained by input layout + elif is_contiguous: + # Input is contiguous, output can use contiguous strides + return create_reinterpret_view( + x, new_size, FlexibleLayout.contiguous_strides(new_size) + ) + + # Input is non-contiguous. Check if we can get storage/layout. + if not is_storage_and_layout(x): + # Can't get storage/layout (e.g., for Pointwise nodes) + return handle_unbacked_or_dynamic_reshape(x) + + # Try to compute valid output strides. + storage, old_layout = as_storage_and_layout(x, freeze=False) + + old_stride = old_layout.stride + + # Convert sympy exprs to SymInt for _compute_stride, then convert back + old_size_symint = V.graph.sizevars.to_symints_or_ints(old_size) + old_stride_symint = V.graph.sizevars.to_symints_or_ints(old_stride) + new_size_symint = V.graph.sizevars.to_symints_or_ints(new_size) + + from torch._subclasses.fake_impls import _compute_stride + + # Use size_oblivious=True for unbacked symbols to avoid DDE errors + new_stride_symint = _compute_stride( + old_size_symint, + old_stride_symint, + new_size_symint, + size_oblivious=unbacked_symbols_in_sizes, + ) + + if new_stride_symint is not None: + # Convert SymInt back to sympy expressions + new_stride = [ + s.node.expr if hasattr(s, "node") else sympy.Integer(s) + for s in new_stride_symint + ] + # View is possible with computed strides + new_layout = FixedLayout( + old_layout.device, + old_layout.dtype, + new_size, + new_stride, + old_layout.offset, + old_layout.is_pinned, + ) + return ReinterpretView(data=storage, layout=new_layout) + + # View not possible with current strides + return handle_unbacked_or_dynamic_reshape(x) + + @staticmethod + def resolve_negative_size( + old_size: Sequence[Expr], new_size: Sequence[Expr] + ) -> tuple[list[Expr], list[Expr]]: + new_size = [V.graph.sizevars.simplify(x) for x in new_size] + old_size = [V.graph.sizevars.simplify(x) for x in old_size] + + new_size = list(new_size) + for i in range(len(new_size)): + if new_size[i] == -1: + new_size[i] = sympy.S.One + new_size[i] = CleanDiv(sympy_product(old_size), sympy_product(new_size)) + break + + V.graph.sizevars.check_equals(sympy_product(old_size), sympy_product(new_size)) + return old_size, new_size + + @classmethod + def dynamic_reshape_indexer( + cls, + old_size: Sequence[_IntLike], + new_size: Sequence[_IntLike], + dense_dim: int | None = None, + ) -> Callable[[Sequence[_T]], Sequence[_V]]: + try: + reindex = cls._dynamic_reshape_indexer(old_size, new_size, dense_dim) + except (AssertionError, GuardOnDataDependentSymNode, IndexError): + # optimistic algorithm failed, lets do a fallback + flat = [sympy_product(old_size)] + reindex1 = cls._dynamic_reshape_indexer(old_size, flat) + reindex2 = cls._dynamic_reshape_indexer(flat, new_size) + reindex = fuse_reindexing(reindex1, reindex2) + return reindex + + @staticmethod + def _dynamic_reshape_indexer( + old_size: Sequence[Expr], + new_size: Sequence[Expr], + dense_dim: int | None = None, + ) -> Callable[[Sequence[Expr]], Sequence[Expr]]: + """ + Perform a reshape entirely by modifying indexing math + """ + guard_or_false = V.graph.sizevars.guard_or_false + + def compare_sizes(a: Expr, b: Expr) -> int: + """ + Compare two symbolic sizes, returning -1 if a < b, 0 if a == b, 1 if a > b. + + For unbacked symbols, guard_or_false returns False, so we fall back + to divisibility checks. + """ + if guard_or_false(sympy.Eq(a, b)): + return 0 + if guard_or_false(sympy.Lt(a, b)): + return -1 + if guard_or_false(sympy.Gt(a, b)): + return 1 + + # Divisibility fallback for unbacked symbols: + # e.g. comparing u0 vs u0*u1, statically_known_multiple_of(u0*u1, u0) + # returns True so we take the merge path (return -1). + # The merge reindex for old=[u0, u1] -> new=[u0*u1] is: + # for k in range(u0 * u1): + # i = k // u1 + # j = k % u1 + # z[k] = x[i, j] + 1 + # Two cases where this could seem wrong but is still safe: + # u1=1: the merge reindex is correct since the extra dim is + # size 1 (a no-op). e.g. k // 1 = k, k % 1 = 0. + # u0=0: loop is range(0), no kernel runs, result doesn't matter. + + if V.graph.sizevars.statically_known_multiple_of(b, a): + return -1 + if V.graph.sizevars.statically_known_multiple_of(a, b): + return 1 + + raise GuardOnDataDependentSymNode(sympy.Eq(a, b)) + + # TODO: These symbols may not escape, if they don't assert so and + # treat them as temporary + vars = [ + sympy_index_symbol_with_prefix(SymT.VIEW, i) for i in range(len(new_size)) + ] + + stack_new = list(zip(vars, new_size)) + stack_old = list(old_size) + + # process the dense dim first + reordering_dense_dim = ( + dense_dim is not None + and dense_dim != len(stack_old) - 1 + and len(new_size) == 1 + ) + if reordering_dense_dim: + assert dense_dim is not None # mypy + old_dim = stack_old.pop(dense_dim) + stack_old.append(old_dim) + + view_expr = [] + while stack_new and stack_old: + size_old = stack_old.pop() + var, size_new = stack_new.pop() + if size_old == 1: + view_expr.append(sympy.S.Zero) + stack_new.append((var, size_new)) # re-add + elif size_new == 1: + stack_old.append(size_old) # re-add + elif compare_sizes(size_new, size_old) == 0: + view_expr.append(var) + elif compare_sizes(size_new, size_old) < 0: + while compare_sizes(size_new, size_old) < 0: + var2, size_new2 = stack_new.pop() + var = var2 * size_new + var + size_new = size_new * size_new2 + view_expr.append(var) + V.graph.sizevars.check_equals(size_new, size_old) + elif compare_sizes(size_new, size_old) > 0: + divisor = sympy.S.One + modulus = size_old + view_expr.append(ModularIndexing(var, divisor, modulus)) + divisor = divisor * modulus + while compare_sizes(size_new, size_old) > 0: + modulus = stack_old.pop() + view_expr.append(ModularIndexing(var, divisor, modulus)) + divisor = divisor * modulus + size_old = size_old * modulus + V.graph.sizevars.check_equals(size_new, size_old) + else: + raise AssertionError + + while stack_old: + size_old = stack_old.pop() + V.graph.sizevars.check_equals(size_old, 1) + view_expr.append(sympy.S.Zero) + + while stack_new: + var, size_new = stack_new.pop() + V.graph.sizevars.check_equals(size_new, 1) + + if dense_dim is not None and len(new_size) == 1: + view_expr.reverse() + # Move the last expression (dense dim) to its original position + dense_expr = view_expr.pop() + view_expr.insert(dense_dim, dense_expr) + else: + view_expr.reverse() + + assert len(view_expr) == len(old_size) + + def reindex( + index: Sequence[Expr], + ) -> Sequence[Expr]: + assert len(index) == len(vars), (len(index), len(vars)) + replacements = dict(zip(vars, index)) + return tuple(sympy_subs(x, replacements) for x in view_expr) + + return reindex + + +@ir_dataclass +class ReinterpretView(BaseView): + """Pretend our storage has a different layout""" + + layout: Layout + + def __post_init__(self) -> None: + super().__post_init__() + if isinstance(self.data, BaseView): + object.__setattr__(self, "data", self.data.unwrap_view()) + + def __str__(self) -> str: + return self.str_helper( + [ + self.data, + self.layout, + ] + ) + + __repr__ = __str__ + + def get_name(self) -> str: + return self.data.get_name() + + def get_device(self) -> torch.device | None: + return self.layout.device + + def get_origin_node(self) -> torch.fx.Node | None: + return None + + @property + def dtype(self) -> torch.dtype: + return self.layout.dtype + + def get_size(self) -> Sequence[Expr]: + return list(self.layout.size) + + def get_stride(self) -> Sequence[Expr]: + return list(self.layout.stride) + + def make_loader(self) -> Callable[[Sequence[Expr]], OpsValue]: + def loader(index: Sequence[Expr]) -> OpsValue: + indexer = self.layout.make_indexer() + tmp_loader = ops.load(self.get_name(), indexer(index)) + if self.layout.dtype != self.data.dtype: + return ops.to_dtype_bitcast(tmp_loader, self.dtype, self.data.dtype) + else: + return tmp_loader + + return loader + + def make_indexer(self) -> Callable[[Sequence[Expr]], Expr]: + return self.layout.make_indexer() + + def get_layout(self) -> Layout: + return self.layout + + def freeze_layout(self) -> None: + pass + + @cache_on_self_and_args("ReinterpretView") + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + return ( + get_free_symbols(self.layout.size, unbacked_only) + | get_free_symbols(self.layout.stride, unbacked_only) + | get_free_symbols(self.layout.offset, unbacked_only) + ) + + def codegen_reference(self, writer: IndentedBuffer | None = None) -> str: + # reinterpret_tensor is similar to as_strided except: + # - offset is added to the existing offset (rather than replacing it) + # - view tracking is disabled similar to unsafe_view + return V.graph.wrapper_code.codegen_reinterpret_view( + self.data, + self.layout.size, + self.layout.stride, + self.layout.offset, + writer.writeline if writer is not None else V.graph.wrapper_code.writeline, + dtype=self.layout.dtype, + ) + + def num_reads(self) -> int: + return 1 + + +@ir_dataclass +class DtypeView(BaseView): + """Pretend our storage has a different type""" + + target_dtype: torch.dtype + + @classmethod + def create(cls, x: IRNode, new_dtype: torch.dtype) -> BaseView: + if is_storage_and_layout(x): + storage, old_layout = as_storage_and_layout(x) + new_layout = FixedLayout( + old_layout.device, + new_dtype, + old_layout.size, + old_layout.stride, + old_layout.offset, + old_layout.is_pinned, + ) + return ReinterpretView(data=storage, layout=new_layout) + return DtypeView(data=x, target_dtype=new_dtype) + + def __str__(self) -> str: + return self.str_helper([self.data, self.target_dtype]) + + __repr__ = __str__ + + @property + def dtype(self) -> torch.dtype: + return self.target_dtype + + def get_size(self) -> Sequence[Expr]: + return self.data.get_size() + + def make_loader(self) -> Callable[[Sequence[Expr]], OpsValue]: + inner = self.data.make_loader() + + def loader(idx: Sequence[Expr]) -> OpsValue: + return ops.to_dtype_bitcast(inner(idx), self.target_dtype, self.data.dtype) + + return loader + + +class SliceView(View): + """View that represents a slice along a single dimension. + + Corresponds to tensor[..., start:end:step, ...]. + """ + + @classmethod + def normalize_start_end( + cls, x: IRNode, dim: int, start: int, end: int + ) -> tuple[int, int]: + """ + Normalize start and end such that both are in the range + [0, x.get_size()[dim]] and start <= end. + """ + sizevars = V.graph.sizevars + dim_size = x.get_size()[dim] + + if any(free_unbacked_symbols(x) for x in (start, end, dim_size)): + min_func = sympy.Min + max_func = sympy.Max + elif any( + # Only needed when backed_size_oblivious is on. + x.has(sympy.Min, sympy.Max) + for x in (start, end, dim_size) + if isinstance(x, Expr) + ): + min_func = sympy.Min + max_func = sympy.Max + else: + min_func = sizevars.evaluate_min + max_func = sizevars.evaluate_max + + def clamp(x: Expr, lower: int, upper: int) -> Expr: + clamped_lower = ( + x if sizevars.statically_known_geq(x, lower) else max_func(x, lower) + ) + clamped_full = ( + clamped_lower + if sizevars.statically_known_leq(clamped_lower, upper) + else min_func(clamped_lower, upper) + ) + return clamped_full + + def clamp_wrap( + val: int | None, lower: int, upper: int, default: Expr | int + ) -> Expr | int: + if val is None: + # TODO(rec): can this really happen? + return default + val = cls.handle_negative_index(val, dim_size) + return clamp(val, lower, upper) + + start = clamp_wrap(start, 0, dim_size, 0) + end = clamp_wrap(end, start, dim_size, dim_size) + return start, end + + @classmethod + def create( # type: ignore[override] + cls, + x: IRNode, + dim: int, + start: int, + end: int, + step: int = 1, + clamp: bool = True, + ) -> IRNode: + step = sympy.expand(step) + assert isinstance(step, Expr) or step > 0, step + try: + if start == 0 and end >= 2**63 - 1 and step == 1: + return x + except TypeError: + pass + + new_size = list(x.get_size()) + + # NB: Ordinarily we default to clamping. + # We only don't clamp for split_with_sizes. For split_with_sizes, sizes should be already valid + # failing in this situation is ok, since invalid sizes could trigger silent errors. + if clamp: + start, end = cls.normalize_start_end(x, dim, start, end) + + new_size[dim] = FloorDiv(end - start + (step - 1), step) + + if is_storage_and_layout(x): + # Fast path + storage, old_layout = as_storage_and_layout(x) + new_stride = list(old_layout.stride) + new_stride[dim] = new_stride[dim] * step + new_layout = FixedLayout( + old_layout.device, + old_layout.dtype, + new_size, + new_stride, + old_layout.offset + old_layout.stride[dim] * start, + old_layout.is_pinned, + ) + return ReinterpretView(data=storage, layout=new_layout) + + def reindex( + index: Sequence[Expr], + ) -> Sequence[Expr]: + assert len(index) == len(new_size), f"wrong ndim {index} {new_size}" + index = list(index) + index[dim] = index[dim] * step + start + return index + + # redirect to a generic view + return SliceView(data=x, size=new_size, reindex=reindex) + + +@ir_dataclass +class BaseConstant(IRNode): + dtype: torch.dtype + device: torch.device + + def get_size(self) -> Sequence[Expr]: + return () + + def get_device(self) -> torch.device | None: + return self.device + + def get_origin_node(self) -> torch.fx.Node | None: + return None + + def get_reads(self) -> OrderedSet[Dep]: + return OrderedSet() + + +@ir_dataclass +class Constant(BaseConstant): + value: Any + dtype: torch.dtype + device: torch.device + + def make_loader(self) -> Callable[[Sequence[Expr]], OpsValue]: + def loader(index: Sequence[Expr]) -> OpsValue: + return ops.constant(self.value, self.dtype) + + return loader + + def realize(self) -> str | None: + pass + + def constant_to_device(self, device: torch.device) -> IRNode: + return Constant(value=self.value, dtype=self.dtype, device=device) + + +@ir_dataclass +class IndexingConstant(BaseConstant): + index: Any + dtype: torch.dtype + device: torch.device + + def make_loader(self) -> Callable[[Sequence[Expr]], OpsValue]: + def loader(index: Sequence[Expr]) -> OpsValue: + return ops.index_expr(self.index, self.dtype) + + return loader + + def constant_to_device(self, device: torch.device) -> IRNode: + return IndexingConstant(index=self.index, dtype=self.dtype, device=device) + + +def is_contiguous_strides_for_shape( + stride: Sequence[_IntLike], shape: Sequence[_IntLike] +) -> bool: + expected_stride = 1 + expected_stride_max = 1 + for x, y in reversed(tuple(zip(shape, stride))): + if x == 1: + continue + + if not V.graph.sizevars.statically_known_equals( + y, expected_stride + ) and not V.graph.sizevars.statically_known_equals(y, expected_stride_max): + return False + + expected_stride_max *= sympy.Max(1, x) + expected_stride *= x + + return True + + +def get_align_for_dtype(dtype: torch.dtype) -> int: + return config.padding_alignment_bytes // dtype.itemsize + + +class OutputSpec: + """Abstract base for Layout, MultiOutputLayout, NoneLayout. + Represents the memory layout of the output of an Operation.""" + + def get_device(self) -> torch.device | None: + raise NotImplementedError(type(self).__name__) + + def storage_size(self) -> int: + raise NotImplementedError(type(self).__name__) + + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + raise NotImplementedError(type(self).__name__) + + +@ir_dataclass +class Layout(OutputSpec): + """ + Layout base class + + Carries tensor meta-information including offset and + whether it is pinned. + """ + + def __init__( + self, + device: torch.device, + dtype: torch.dtype, + size: Sequence[Expr], + stride: Sequence[Expr] | None = None, + offset: Expr = Integer(0), + is_pinned: bool = False, + ) -> None: + if stride is None: + stride = FlexibleLayout.contiguous_strides(size) + # pyrefly: ignore [read-only] + self.device = device + self.dtype = dtype + assert len(size) == len(stride), f"size={size}, stride={stride}" + assert all(isinstance(s, (Expr, int)) for s in size) + self._size = size + self._stride = stride + self._offset = offset + self.is_pinned = is_pinned + # is_pinned implies cpu + assert (not self.is_pinned) or (self.device.type == "cpu"), ( + "Only CPU tensors can be pinned" + ) + + @property + def size(self) -> Sequence[Expr]: + return self._size + + @size.setter + def size(self, value: Sequence[Expr]) -> None: + self._size = value + + @property + def stride(self) -> Sequence[Expr]: + return self._stride + + @stride.setter + def stride(self, value: Sequence[Expr]) -> None: + self._stride = value + + @property + def offset(self) -> Expr: + return self._offset + + @offset.setter + def offset(self, value: Expr) -> None: + self._offset = value + + def __str__(self) -> str: + offset = "" + if self.offset != 0: + offset = f", offset={self.offset}" + + device_index_str = "" if self.device.index is None else f":{self.device.index}" + is_pinned_str = "" + if self.is_pinned: + is_pinned_str = f", is_pinned={self.is_pinned}" + return ( + f"{type(self).__name__}('{self.device.type}{device_index_str}', {self.dtype}, " + f"size={self.size}, stride={self.stride}{offset}{is_pinned_str})" + ) + + __repr__ = __str__ + + def get_device(self) -> torch.device: + return self.device + + def get_example(self) -> torch.Tensor: + with V.fake_mode: + return torch.empty_strided( + convert_shape_to_symint(self.size), + convert_shape_to_symint(self.stride), + dtype=self.dtype, + device=self.device, + pin_memory=self.is_pinned, + ) + + def is_contiguous(self) -> bool: + return is_contiguous_strides_for_shape(self.stride, self.size) + + @staticmethod + def is_channels_last_contiguous( + shape: Sequence[_IntLike], strides: Sequence[_IntLike] + ) -> bool: + ndim = len(shape) + if ndim not in [4, 5] or shape[1] == 1: + return False + for left, right, size in zip( + # pyrefly: ignore [bad-specialization] + strides, + # pyrefly: ignore [bad-specialization] + make_channels_last_strides_for(shape), + shape, + ): + if size != 1 and left != right: + return False + return True + + def is_transposed(self) -> bool: + for left, right, size in zip( + self.stride, + reversed(FlexibleLayout.contiguous_strides(list(reversed(self.size)))), + self.size, + ): + if size != 1 and left != right: + return False + return True + + def is_stride_ordered(self, order: Sequence[int]) -> bool: + assert len(self.stride) == len(order) + + # ignore dimensions of size 1, they dont affect layout + non_1_indices = [ + i + for i, dim in enumerate(self.size) + if V.graph.sizevars.optimization_hint(dim, fallback=2) != 1 + ] + + stride = [self.stride[i] for i in non_1_indices] + order: Sequence[int] = [order[i] for i in non_1_indices] + + def sorted_indices(arr: Sequence[int]) -> Sequence[int]: + sorted_arr = sorted(arr) + return [sorted_arr.index(element) for element in arr] + + # since we may have removed dimensions, need to re-sort & re-index order + order = sorted_indices(order) + + # reorder the stride given order + stride_ordered = [-1] * len(order) + for i in range(len(order)): + stride_ordered[order[i]] = stride[i] + # check if it is in ascending order + for i in range(len(order) - 1): + expr = stride_ordered[i] > stride_ordered[i + 1] + if not isinstance(expr, bool): + expr = V.graph._shape_env.evaluate_expr( + stride_ordered[i] > stride_ordered[i + 1], size_oblivious=True + ) + if expr: + return False + return True + + def is_channels_last_stride_ordered(self) -> bool: + # create channels_last order(NCHW, NCDHW, the C is the first order). + order = [0] + list(reversed(range(1, len(self.stride) - 1))) + order = [len(order)] + order + return self.is_stride_ordered(order) + + @staticmethod + def _pad_strides( + in_strides: Sequence[int], size: Sequence[Expr], dtype: torch.dtype + ) -> Sequence[int]: + """ + The padding does not change stride order but makes sure all strides larger + than the threshold are multiple of align. + """ + align = get_align_for_dtype(dtype) + if len(in_strides) == 0: + return in_strides + + if not config.pad_channels_last and Layout.is_channels_last_contiguous( + size, in_strides + ): + return in_strides + + current_fx_node = V.get_current_node() + if hasattr(current_fx_node, "meta") and current_fx_node.meta.get( + "dislike_padding", False + ): + return in_strides + + # Skip padding the strides for dynamic shapes based on config.pad_dynamic_shape + # Checking both shape and strides, as there are cases where only one is dynamic + is_dynamic = not all( + isinstance(s, (int, sympy.Integer)) + for s in itertools.chain(in_strides, size) + ) + if not config.pad_dynamic_shapes and is_dynamic: + return in_strides + + shape_env = V.graph._shape_env if hasattr(V.graph, "_shape_env") else None + + def contains_unbacked_symints(expr: sympy.Expr | int) -> bool: + if shape_env is None: + return False + if not isinstance(expr, sympy.Expr): + return False + return any(shape_env.is_unbacked_symint(s) for s in expr.free_symbols) + + # Skip padding the strides when it contains unbacked symints for now. + if shape_env and any(contains_unbacked_symints(s) for s in in_strides): + return in_strides + + stride_order = get_stride_order(in_strides, shape_env) + fill_order = stride_order2fill_order(stride_order) + + new_strides = [0 for _ in range(len(in_strides))] + # since we pad when the layout is flexible, we can decide the + # smallest stride to be 1. + new_strides[fill_order[0]] = 1 + + padded = False + for rank, idx in enumerate(fill_order[1:], start=1): + prev_idx = fill_order[rank - 1] + stride = new_strides[prev_idx] * size[prev_idx] + # Static stride and meets padding conditions OR + # Dynamic stride and config.pad_dynamic_shape=True + require_padding = ( + isinstance(stride, (int, sympy.Integer)) + and stride > config.padding_stride_threshold + and stride % align != 0 + ) or (isinstance(stride, sympy.Expr) and config.pad_dynamic_shapes) + new_strides[idx] = stride + if require_padding: + new_strides[idx] = ceildiv(stride, align) * align + padded = True + + if not padded: + # Consider a tensor with shape [256, 1, 5, 5] + # Avoid strides like [25, 5, 5, 1] being padded to equivalent strides + # [25, 25, 5, 1]. + return in_strides + + # pyrefly: ignore [bad-assignment] + metrics.num_comprehensive_padding += 1 + return new_strides + + def pad_strides(self) -> None: + assert isinstance(self, FlexibleLayout), type(self) + assert self.stride is not None + self.stride = self._pad_strides(self.stride, self.size, self.dtype) + + def should_pad_strides(self) -> bool: + return config.comprehensive_padding and isinstance(self, FlexibleLayout) + + def as_fixed(self) -> FixedLayout: + if isinstance(self, FixedLayout): + return self + + if self.should_pad_strides(): + self.pad_strides() + return FixedLayout( + self.device, + self.dtype, + self.size, + self.stride, + self.offset, + self.is_pinned, + ) + + def make_indexer(self) -> Callable[[Sequence[Expr]], Expr]: + assert FlexibleLayout.allow_indexing, ( + f"convert {type(self).__name__} to FixedLayout first" + ) + return self.as_fixed().make_indexer() + + def __eq__(self, other: object) -> bool: + return ( + isinstance(other, Layout) + and self.device == other.device + and self.dtype == other.dtype + and self.size == other.size + and self.stride == other.stride + and self.offset == other.offset + and self.is_pinned == other.is_pinned + ) + + def storage_size(self) -> Expr: + return compute_required_storage_length(self.size, self.stride, self.offset) # type: ignore[arg-type] + + @cache_on_self_and_args("Layout") + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + return ( + get_free_symbols(self.size, unbacked_only) + | get_free_symbols(self.stride, unbacked_only) + | get_free_symbols(self.offset, unbacked_only) + ) + + +class FixedLayout(Layout): + """A Tensor layout we cannot change""" + + def make_indexer(self) -> Callable[[Sequence[Expr]], Expr]: + """A closure containing math to read a given element""" + return _fixed_indexer(self.size, self.stride, self.offset) + + +class FlexibleLayout(Layout): + """ + A Tensor layout that we are allowed to change + + Assumption: layout change should NOT add or remove free symbols + """ + + allow_indexing = False + + def get_fixed_layout_without_freezing(self) -> FixedLayout: + """ + Compute what the strides would be if this layout were frozen, + without actually modifying the layout. This is used for speculative + stride computation during Triton template code generation. + """ + # Create a temporary copy and use as_fixed to keep freezing path in sync + return copy.deepcopy(self).as_fixed() + + # WARNING! This doesn't handle zero size tensors correctly + @staticmethod + def contiguous_strides(sizes: Sequence[int]) -> list[Expr]: + if len(sizes) == 0: + return [] + reversed_strides = [sympy.S.One] + for size in reversed(sizes[1:]): + reversed_strides.append(size * reversed_strides[-1]) + return list(reversed(reversed_strides)) + + @staticmethod + def fill_ordered(sizes: Sequence[int], order: Sequence[int]) -> list[Expr]: + """ + Create a stride based on the order the dimensions should be filled in. + + In this format, channels last would be: + [1, 3, 2, 0] + """ + assert OrderedSet(range(len(sizes))) == OrderedSet(order), (sizes, order) + next_stride = sympy.S.One + strides = [None] * len(order) + + for i in order: + strides[i] = next_stride + next_stride = next_stride * sizes[i] + return strides + + @staticmethod + def stride_ordered(sizes: Sequence[int], order: Sequence[int]) -> Sequence[Expr]: + """ + Create a stride based on the sorted order of a permuted range. + + In this format, channels last would be: + [3, 0, 2, 1] + """ + assert OrderedSet(range(len(sizes))) == OrderedSet(order) + fill_order = stride_order2fill_order(order) + return FlexibleLayout.fill_ordered(sizes, fill_order) + + @staticmethod + def stride_ordered_for_memory_format( + sizes: Sequence[int], memory_format: torch.memory_format + ) -> Sequence[Expr]: + """ + Create a stride based on a memory format. + + Memory format is translasted into a stride order, + so channels_last is the same as: + FlexibleLayout.stride_ordered(sizes, [3, 0, 2, 1]) + + This interface does not support memory_format `torch.preserve_format` + which should be used to deduce a format from another source + """ + if memory_format == torch.channels_last: + return FlexibleLayout.stride_ordered(sizes, NHWC_STRIDE_ORDER) + elif memory_format == torch.channels_last_3d: + return FlexibleLayout.stride_ordered(sizes, NHWDC_STRIDE_ORDER) + elif memory_format == torch.contiguous_format: + return FlexibleLayout.contiguous_strides(sizes) + else: + log.debug( + "stride_ordered_for_memory_format, unsuppored memory_format: %s", + memory_format, + ) + raise NotImplementedError + + @staticmethod + def same_ordered( + sizes: Sequence[int], stride: Sequence[_IntLike] + ) -> Sequence[Expr]: + """ + Create a stride that has the same stride order as given stride + + For example, if given stride is [1000, 1, 100, 10], + the fill order should be [1, 3, 2, 0] + """ + assert len(sizes) == len(stride) + stride = V.graph.sizevars.guarding_hints_or_throw(stride) + fill_order = sorted(range(len(stride)), key=stride.__getitem__) + return FlexibleLayout.fill_ordered(sizes, fill_order) + + @property + def size(self) -> Sequence[Expr]: + return self._size + + @size.setter + def size(self, value: Sequence[Expr]) -> None: + self.assert_free_symbol_uses_unchanged("size", value) + self._size = value + + @property + def stride(self) -> Sequence[Expr]: + return self._stride + + @stride.setter + def stride(self, value: Sequence[Expr]) -> None: + self.assert_free_symbol_uses_unchanged("stride", value) + self._stride = value + + @property + def offset(self) -> Expr: + return self._offset + + @offset.setter + def offset(self, value: Expr) -> None: + self.assert_free_symbol_uses_unchanged("offset", value) + self._offset = value + + def as_stride_order( + self, order: Sequence[int], allow_padding: bool = False + ) -> FixedLayout: + new_stride = self.stride_ordered(self.size, order) + if self.should_pad_strides() and allow_padding: + new_stride = self._pad_strides(new_stride, self.size, self.dtype) + + return FixedLayout( + self.device, + self.dtype, + self.size, + new_stride, + self.offset, + self.is_pinned, + ) + + def as_exact_strides( + self, exact_strides: Sequence[_IntLike], allow_padding: bool = False + ) -> FixedLayout: + new_stride = exact_strides + if self.should_pad_strides() and allow_padding: + new_stride = self._pad_strides(new_stride, self.size, self.dtype) + + return FixedLayout( + self.device, + self.dtype, + self.size, + new_stride, + self.offset, + self.is_pinned, + ) + + def as_fill_order(self, order: Sequence[int]) -> FixedLayout: + new_stride: Sequence[int] = self.fill_ordered(self.size, order) + if self.should_pad_strides(): + new_stride = self._pad_strides(new_stride, self.size, self.dtype) + return FixedLayout( + self.device, + self.dtype, + self.size, + new_stride, + self.offset, + self.is_pinned, + ) + + def as_same_order(self, stride: Sequence[_IntLike]) -> FixedLayout: + new_stride = self.same_ordered(self.size, stride) + if self.should_pad_strides(): + new_stride = self._pad_strides(new_stride, self.size, self.dtype) + return FixedLayout( + self.device, + self.dtype, + self.size, + new_stride, + self.offset, + self.is_pinned, + ) + + def get_initial_free_symbol_uses(self) -> dict[tuple[str, bool], sympy.Symbol]: + initial_free_symbols = {} + for name in ["size", "stride", "offset"]: + for unbacked_only in [True, False]: + key = (name, unbacked_only) + initial_free_symbols[key] = OrderedSet( + get_free_symbols(getattr(self, name), unbacked_only) + ) + + return initial_free_symbols + + def assert_free_symbol_uses_unchanged(self, name: str, value: IterateExprs) -> None: + for unbacked_only in [True, False]: + old_free_symbols = self.initial_free_symbols[(name, unbacked_only)] + new_free_symbols = OrderedSet(get_free_symbols(value, unbacked_only)) + assert new_free_symbols == old_free_symbols, ( + f"Expected free symbols unchanged, but got {new_free_symbols} vs {old_free_symbols}" + ) + + def __init__( + self, + device: torch.device, + dtype: torch.dtype, + size: Sequence[Expr], + stride_order: Sequence[int | Integer] | None = None, + is_pinned: bool = False, + ) -> None: + if stride_order: + strides = FlexibleLayout.fill_ordered(size, stride_order) + else: + strides = FlexibleLayout.contiguous_strides(size) + super().__init__(device, dtype, size, strides, is_pinned=is_pinned) + + # record the initial free symbols to check that we do not add new free symbols + # later when modifying sizes, strides, and offsets. + self.initial_free_symbols = self.get_initial_free_symbol_uses() + + +class NonOwningLayout(Layout): + """Is a view into the storage of another tensor""" + + def __init__(self, view: BaseView | TensorBox) -> None: + layout = view.get_layout() + super().__init__( + layout.device, + layout.dtype, + layout.size, + layout.stride, + ) + self.view = view + + def make_indexer(self) -> Callable[[Sequence[Expr]], Expr]: + return self.as_fixed().make_indexer() + + def maybe_guard_aligned(self) -> bool: + offset = self.view.get_layout().offset + if offset == 0: + return True + from .utils import ALIGNMENT + + return V.graph.sizevars.statically_known_multiple_of(offset, ALIGNMENT) + + @cache_on_self_and_args("NonOwningLayout") + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + assert isinstance(self.view, ReinterpretView) + box = self.view.data + assert isinstance(box, StorageBox), type(box) + input_buffer = box.data + assert isinstance(input_buffer, Buffer), type(box) + return input_buffer.layout.get_free_symbol_uses(unbacked_only) + + +class CommBufferType(Enum): + SYMM_MEM = "symm_mem" + + +class CommBufferLayout(FixedLayout): + """ + A layout that signifies the buffer is a comm buffer. + In terms of striding, the layout is identical to `FixedLayout`. + + Buffers with this layout do not participate in in-place reuse - it can be + neither the source nor the target for in-place reuse. + + For detailed motivation and usage of this layout, see + NOTE [lowering-time collective optimization]. + """ + + comm_buffer_type: CommBufferType + group_name: str + + def __init__( + self, + layout: FlexibleLayout | FixedLayout, + comm_buffer_type: CommBufferType, + group_name: str, + ): + fixed = layout.as_fixed() if isinstance(layout, FlexibleLayout) else layout + super().__init__( + device=fixed.device, + dtype=fixed.dtype, + size=fixed.size, + stride=fixed.stride, + offset=fixed.offset, + is_pinned=fixed.is_pinned, + ) + self.comm_buffer_type = comm_buffer_type + self.group_name = group_name + + +@ir_dataclass +class NoneLayout(OutputSpec): + # This is janky, I figured out what fields to populate by just running + # the model I was interested in and adding properties/methods as needed. + # This doesn't inherit from Layout because Layout assumes you have stuff + # like sizes, but I don't really have anything here. + # + # If you have an ir.Node with NoneLayout, you probably need to setup + # dependencies manually in scheduler + + device: torch.device | None + size: list[int] = dataclasses.field(default_factory=lambda: [0]) + stride: list[int] = dataclasses.field(default_factory=lambda: [0]) + + def storage_size(self) -> int: + return 0 + + def as_fixed(self) -> OutputSpec: + return self + + def get_device(self) -> torch.device | None: + return self.device + + +class MutationLayoutSHOULDREMOVE(Layout): + def __init__(self, target: IRNode) -> None: + super().__init__( + target.get_device_or_error(), + target.get_dtype(), + target.get_size(), + None, + ) + self.target = target + name = self.get_buffer().get_name() + V.graph.mark_buffer_mutated(name) + + @property + def stride(self) -> Sequence[Expr]: # type: ignore[override] + return self.real_layout().stride + + @stride.setter # type: ignore[override] + def stride(self, value: Never) -> None: + pass # ignore setting of stride + + def storage_size(self) -> Expr: + return self.real_layout().storage_size() + + def get_buffer(self) -> Buffer: + def unwrap_views(target: Any) -> Any: + if isinstance(target, MutationLayoutSHOULDREMOVE): + return unwrap_views(target.target) + if isinstance(target, BaseView): + return unwrap_views(target.unwrap_view()) + if isinstance(target, MutableBox): + return unwrap_views(target.data) + return target + + result = unwrap_views(self.target) + assert isinstance(result, Buffer), type(result) + return result + + def real_layout(self) -> Layout: + layout = self.get_buffer().layout + assert isinstance(layout, Layout) + return layout + + @classmethod + def realize_into( + cls, src: IRNode, dst: IRNode, unsafe_alias: bool = False + ) -> IRNode: + dst.realize() + # NOTE: We must realize users of `dst` before we realize `src`, since + # realization order determines scheduling order. Otherwise, src's + # mutation would be scheduled before the existing users of dst! + V.graph.mark_buffer_mutated(dst.get_name()) + + if isinstance(src, TensorBox): + src = src.data + + # We copy the contents of src into dst. In most cases this should + # be fused into a single kernel by the scheduler. + # NOTE: We cannot change src's layout to mutate dst directly as this + # would alias src to dst, which is not correct as further mutations to + # dst would effect users of src. However if there are no more users of + # dst, we can alias src to dst. + src.realize_hint() + + if not unsafe_alias: + node = Pointwise.create( + device=src.get_device(), + dtype=src.get_dtype(), + inner_fn=src.make_loader(), + ranges=[ + V.graph.sizevars.check_equals_and_simplify(a, b) + for a, b in zip(src.get_size(), dst.get_size()) + ], + ) + assert isinstance(node, (BaseView, MutableBox)) + src = node.data + + src.realize() + assert hasattr(src, "data"), src + assert isinstance(src.data.layout, FlexibleLayout), type(src.data.layout) + src.data.layout = MutationLayoutSHOULDREMOVE(dst) + return src.data + + def as_fixed(self) -> Self: # type: ignore[override] + return self + + def make_indexer(self) -> Callable[[Sequence[Expr]], Expr]: + return self.target.make_indexer() + + +@ir_dataclass(frozen=False) +class Buffer(IRNode, CodegenSymbol): + # Name is sometimes None; e.g., ForceInPlace, where there isn't + # a meaningful name + name: str | None + layout: OutputSpec + + # Multi-output buffers will define 'outputs: List[Buffer]'. Confusingly, + # MultiOutput does NOT define this! + + def __post_init__(self) -> None: + super().__post_init__() + self._post_init_setattr("origin_node", None) + + def make_indexer(self) -> Callable[[Sequence[Expr]], Expr]: + return self.get_layout().make_indexer() + + def get_name(self) -> str: + assert self.name, self + return self.name + + def get_example(self) -> torch.Tensor | torch.SymInt: + if isinstance(self.layout, Layout): + return self.layout.get_example() + raise NotImplementedError(type(self.layout).__name__) + + def get_device(self) -> torch.device | None: + return self.get_output_spec().get_device() + + def get_defining_op(self) -> Operation | None: + return None + + @property + def dtype(self) -> torch.dtype: + return self.get_layout().dtype + + def get_size(self) -> Sequence[Expr]: + return [*self.get_layout().size] + + def get_stride(self) -> list[Expr]: + return [*self.get_layout().stride] + + def get_offset(self) -> Expr: + return self.get_layout().offset + + def get_layout(self) -> Layout: + if isinstance(self.layout, Layout): + return self.layout + raise NotImplementedError(type(self.layout).__name__) + + def get_output_spec(self) -> OutputSpec: + return self.layout + + def get_storage_numel(self) -> int: + return self.get_numel() + + def get_is_pinned(self) -> bool: + return self.get_layout().is_pinned + + def freeze_layout(self) -> None: + if isinstance(self.layout, Layout) and not isinstance( + self.layout, NonOwningLayout + ): + self.layout = self.layout.as_fixed() + + def freeze_layout_with_stride_order( + self, order: Sequence[int], allow_padding: bool = False + ) -> None: + assert isinstance(self.layout, FlexibleLayout), type(self.layout) + self.layout = self.layout.as_stride_order(order, allow_padding=allow_padding) + + def freeze_layout_with_fill_order(self, order: Sequence[int]) -> None: + assert isinstance(self.layout, FlexibleLayout), type(self.layout) + self.layout = self.layout.as_fill_order(order) + + def freeze_layout_with_same_order(self, stride: Sequence[int]) -> None: + assert isinstance(self.layout, FlexibleLayout), type(self.layout) + self.layout = self.layout.as_same_order(stride) + + def freeze_layout_with_exact_strides( + self, exact_strides: Sequence[int], allow_padding: bool = False + ) -> None: + assert isinstance(self.layout, FlexibleLayout), type(self.layout) + self.layout = self.layout.as_exact_strides( + exact_strides, allow_padding=allow_padding + ) + + def is_zero_elements(self) -> bool: + return V.graph.sizevars.statically_known_true(sympy.Eq(self.get_numel(), 0)) + + def make_loader(self) -> Callable[[Sequence[Expr]], OpsValue]: + # Loading from a zero-element buffer is a no-op + if self.is_zero_elements(): + return partial(nop_loader_fn, dtype=self.get_dtype()) + + def loader(index: Sequence[Expr]) -> OpsValue: + indexer = self.make_indexer() + return ops.load(self.name or "unnamed", indexer(index)) + + return loader + + def codegen_reference(self, writer: IndentedBuffer | None = None) -> str: + return self.get_name() + + def decide_layout(self) -> None: + pass + + def get_inputs_that_alias_output(self) -> Sequence[str]: + if isinstance(self.layout, NonOwningLayout): + return [self.layout.view.get_name()] + return () + + def get_mutation_names(self) -> Sequence[str]: + if isinstance(self.layout, MutationLayoutSHOULDREMOVE): + return [self.layout.target.get_name()] + return () + + def get_read_names(self) -> OrderedSet[str]: + return OrderedSet([self.get_name()]) + + @cache_on_self_and_args("Buffer") + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + return OrderedSet() + + def get_unbacked_symbol_defs(self) -> OrderedSet[sympy.Symbol]: + return OrderedSet() + + def realize(self) -> str | None: + pass + + def should_allocate(self) -> bool: + # Returns False by default. + return False + + +@ir_dataclass(frozen=False) +class OperationBuffer(Buffer, Operation): + # An operation that produces a single output buffer + def get_outputs(self) -> list[Buffer]: + return [self] + + def get_defining_op(self) -> Operation: + return self + + # Skip implementation in Buffer + get_operation_name = Operation.get_operation_name + + def __post_init__(self) -> None: + Buffer.__post_init__(self) + Operation.__post_init__(self) + + +class InputBuffer(Buffer): + def num_reads(self) -> int: + return 1 + + +class DonatedBuffer(InputBuffer): + """ + Represents a donated buffer which is a saved tensor that is not alias to any + fwd inputs, fwd user outputs, and bwd outputs. We generally cannot inplace + reuse the input tensor memory during backward since it might be used in another + function. However, donated buffer can be inplace reused during backward + to save memory. + """ + + +class ConstantBuffer(InputBuffer): + override_device: torch.device | None = None + + def make_loader(self) -> Callable[[Sequence[Expr]], OpsValue]: + def loader(index: Sequence[Expr]) -> OpsValue: + indexer = self.get_layout().make_indexer() + return ops.load( + V.graph.constant_name(self.get_name(), self.override_device), + indexer(index), + ) + + return loader + + def constant_to_device(self, device: torch.device) -> IRNode: + return ConstantBuffer( + name=V.graph.constant_name(self.get_name(), device), layout=self.layout + ) + + +@ir_dataclass +class NoneAsConstantBuffer(IRNode): + def get_reads(self) -> OrderedSet[Dep]: + return OrderedSet() + + @cache_on_self_and_args("NoneAsConstantBuffer") + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + return OrderedSet() + + def codegen_reference(self, writer: IndentedBuffer | None = None) -> str: + return V.graph.wrapper_code.none_str + + def get_output_spec(self) -> OutputSpec: + return NoneLayout(device=None) + + def has_tensor_output(self) -> bool: + return False + + +@ir_dataclass +class ShapeAsConstantBuffer(IRNode): + expr: Expr + + @cache_on_self_and_args("ShapeAsConstantBuffer") + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + return get_free_symbols(self.expr, unbacked_only) + + def codegen_reference(self, writer: IndentedBuffer | None = None) -> str: + return V.graph.wrapper_code.codegen_sizevar(self.expr) + + def has_tensor_output(self) -> bool: + return False + + +@ir_dataclass(frozen=False) +class ComputedBuffer(OperationBuffer): + """ + Represents a buffer that is computed during kernel execution rather than being an input. + """ + + data: Loops + _force_realize: ClassVar[bool] = False + + # fields for split reduction + _split_size: int | None = None + _original_inner_fn: Callable[..., Any] | None = None + _original_ranges: Sequence[_IntLike] | None = None + _original_reduction_ranges: Sequence[_IntLike] | None = None + + @contextlib.contextmanager + def with_original_inner_fn(self) -> Iterator[None]: + assert self._split_size is not None + assert self._original_inner_fn is not None + assert self._original_ranges is not None + assert self._original_reduction_ranges is not None + + assert isinstance(self.data, Reduction), f"{type(self.data)}" + old_data = self.data + old_layout = self.layout + try: + new_data = Reduction( + device=old_data.device, + dtype=old_data.dtype, + inner_fn=self._original_inner_fn, + ranges=self._original_ranges, + reduction_ranges=self._original_reduction_ranges, + reduction_type=old_data.reduction_type, + src_dtype=old_data.src_dtype, + reduction_hint=old_data.reduction_hint, + ) + self.data = new_data + # this layout does not matter since we skip tl.store + # later + self.layout = FixedLayout( + old_data.device, + old_data.dtype, + self._original_ranges, + ) + self.get_default_sizes_body.clear_cache(self) + yield + finally: + self.data = old_data + self.layout = old_layout + + @staticmethod + @contextlib.contextmanager + def force_realize() -> Iterator[None]: + old_value = ComputedBuffer._force_realize + try: + ComputedBuffer._force_realize = True + yield + finally: + ComputedBuffer._force_realize = old_value + + def get_computed_buffer_name(self) -> str | None: + """ + Returns self.name if it exists, otherwise returns the name of the data node if that exists. + If neither exist, returns None. + """ + if self.name is not None: + return self.name + if hasattr(self.data, "name"): + return self.data.name + return None + + def num_reads(self) -> int: + return self.data.num_reads() + + def get_reads(self) -> OrderedSet[Dep]: + return self.data.get_reads() + + def get_read_names(self) -> OrderedSet[str]: + return self.data.get_read_names() + + def get_read_writes(self) -> dependencies.ReadWrites: + if not isinstance(self.data, (Reduction, Scan, Sort, Pointwise)): + return dependencies.ReadWrites( + reads=OrderedSet(), + writes=OrderedSet(), + index_exprs=OrderedSet(), + ) + + with patch.object(FlexibleLayout, "allow_indexing", True): + if self.data.get_reduction_type(): + return extract_read_writes( + self.get_store_function(), + self.data.get_pointwise_size(), + self.data.get_reduction_size(), + ) + else: + return extract_read_writes( + self.get_store_function(), + self.data.get_size(), + ) + + @cache_on_self_and_args("ComputedBuffer") + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + # Ordinarily, we'd like to just peek at the arguments list, + # but ComputedBuffers have no argument list. + # + # Morally, this logic needs to be synchronized with the + # KernelArgs.size calls, which are responsible for making symbols make + # there way as kernel arguments (and it is precisely passing in one of + # those symbols that establishes a dependency). However, we haven't + # started codegen yet so we can't directly reuse that logic. + # + # One thing you might wonder is if this is enough for a ComputedBuffer + # denoting a reduction over i0. Empirically, it is enough, but for an + # unusual reason: we only need accurate dependencies for item() call, + # but it's impossible to end up with a reduction over i0 from an + # item() call without a regular non-reduction buffer first. + result = self.layout.get_free_symbol_uses( + unbacked_only + ) | self.data.get_free_symbol_uses(unbacked_only) + + if self.has_store_function(): + result |= self.get_read_writes().get_free_symbol_uses(unbacked_only) + return result + + def make_loader(self) -> Callable[[Sequence[Expr]], OpsValue]: + if ( + not self.get_reduction_type() + and self.name not in V.graph.mutated_buffers + and self.num_reads() == 0 + and not self._force_realize + ): + # inline this op rather than generating ops.load() + return self.data.make_loader() + return super().make_loader() + + def has_store_function(self) -> bool: + return isinstance(self.data, (Reduction, Scan, Sort, Pointwise)) + + def get_store_function(self) -> Callable[..., None]: + indexer = self.get_layout().as_fixed().make_indexer() + if isinstance(self.data, (Reduction, Scan, Sort)): + return partial(self.data.store_reduction, self.name, indexer) + else: + assert isinstance(self.data, Pointwise), type(self.data) + return partial(self.data.store_output, self.name, indexer) + + def get_fill_order(self) -> list[int] | None: + """ + If our layout is still flexible, try to determine the stride order based on stride orders of reads. + + TODO(jansel): A better algorithm here would look at downstream consumers of this + value and try to do global graph-level layout optimization. + This is also something just begging to be autotuned. + """ + if isinstance(self.layout, FlexibleLayout): + (index_vars, reduction_vars), _ = dependencies.index_vars_squeeze( + self.data.get_pointwise_size(), self.data.get_reduction_size() + ) + reads = self.get_read_writes().reads + # only consider reads to buffer of same size + # ignore StarDeps because they don't contribute stride information + assert all( + isinstance(r, (dependencies.StarDep, dependencies.MemoryDep)) + for r in reads + ) + reads = [ + sympy_subs(r.index, {v: sympy.S.Zero for v in reduction_vars if v != 0}) + for r in reads + if isinstance(r, dependencies.MemoryDep) + ] + + if reads: + if isinstance(self.data, (Scan, Sort)): + indices = self.data.reindex(index_vars, reduction_vars) + else: + indices = index_vars + stride_lengths = [ + V.graph.sizevars.stride_hints(expr, indices) for expr in reads + ] + from .scheduler import pick_loop_order + + return pick_loop_order(stride_lengths, self.get_size()) + + return None + + def decide_layout(self) -> None: + if isinstance(self.layout, FlexibleLayout): + order = self.get_fill_order() + if order: + self.freeze_layout_with_fill_order(order) + else: + self.freeze_layout() + + @cache_on_self + def get_default_sizes_body( + self, + ) -> tuple[ + tuple[list[Expr], list[Expr]], + LoopBody, + tuple[list[Expr], list[Expr]], + ]: + args, var_ranges = dependencies.index_vars_squeeze( + self.get_pointwise_size(), self.get_reduction_size(), prefix="q" + ) + with patch.object(ConstantBuffer, "override_device", self.get_device()): + body = LoopBody( + self.get_store_function(), + (args if self.get_reduction_type() else args[:1]), + var_ranges, + *args, + ) + index_vars = [] + reduce_vars: list[Any] = [] + index_size = [] + reduce_size = [] + for v, s in var_ranges.items(): + if v in args[0]: + assert not reduce_vars + index_vars.append(v) + index_size.append(s) + else: + assert v in args[1] + reduce_vars.append(v) + reduce_size.append(s) + return (index_size, reduce_size), body, (index_vars, reduce_vars) + + def simplify_and_reorder( + self, + extra_indexing_constraints: tuple[dict[Any, Any], list[Any]] | None = None, + recompute_sizes_body_func: Callable[..., Any] | None = None, + ) -> tuple[tuple[list[Expr], list[Expr]], LoopBody | None]: + """ + This is a main place where we do loop transformations in a + backend-agnostic way. + + Here we: + 1) Remove any 1 dimensions + 2) Fuse contiguous dimensions together + 3) Reorder dimensions based on stride orders + + Optional argument extra_indexing_constraints can be used to append additional + indexing expressions to existing ones derived from buffer's body. This can be useful + to fuse scheduler nodes with compatible ranges, e.g. (s0*s1*...,) and (s0, s1, s2, ...) + on CPU by preventing indexing simplifications and obtaining index/reduce ranges for + the scheduler node compatible with other nodes. + Optional argument recompute_sizes_body_func can be used to recompute sizes and body + on the default body. This can be useful to append additional loop transformations. + """ + ( + (index_size, reduce_size), + body, + (index_vars, reduce_vars), + ) = self.get_default_sizes_body() + + if recompute_sizes_body_func: + ( + (index_size, reduce_size), + body, + (index_vars, reduce_vars), + ) = recompute_sizes_body_func( + (index_size, reduce_size), body, (index_vars, reduce_vars) + ) + + index_formulas = [*body.indexing_exprs.values()] + if extra_indexing_constraints is not None: + assert ( + isinstance(extra_indexing_constraints, tuple) + and len(extra_indexing_constraints) == 2 + ) + extra_indexing_ranges, extra_indexing_expr = extra_indexing_constraints + assert isinstance(extra_indexing_ranges, dict), type(extra_indexing_ranges) + assert isinstance(extra_indexing_expr, list), type(extra_indexing_expr) + assert all(isinstance(f, Expr) for f in extra_indexing_expr) + + expected_var_ranges = body.var_ranges + assert expected_var_ranges == extra_indexing_ranges, ( + expected_var_ranges, + extra_indexing_ranges, + ) + # remove already existing expressions + extra_indexing_expr = [ + e for e in extra_indexing_expr if e not in index_formulas + ] + index_formulas += extra_indexing_expr + + memory_addrs = [*body.get_write_exprs()] + if not V.graph.has_feature(self, BackendFeature.PREFER_STORE_LOOP_ORDER): + memory_addrs.extend(body.get_read_exprs()) + + def simplify_and_reorder( + x_vars: Sequence[sympy.Symbol], + support_vars: Sequence[sympy.Symbol], + sizes: Sequence[int], + simplify_loops: bool, + ) -> tuple[ + list[int], + Callable[[Sequence[int]], Sequence[int]], + Callable[[Sequence[int]], Sequence[int]], + ]: + newsizes, reindex0, reindex1 = self._apply_loop_reordering( + x_vars, support_vars, sizes, memory_addrs + ) + + # When using native matmul, the codegen assumes the following loop order, + # regardless of the stride of A and B: + # + # for z -> y -> x -> r: C[z, y, x] += A[z, y, r] * B[z, r, x] + # or + # for z -> x -> y -> r: C[z, y, x] += A[z, y, r] * B[z, r, x] + # + # The critical point is the position of the "z" (batch) axis in bmm. + # It is fine to swap the y and x axes (e.g., (z, y, x, r) or (z, x, y, r)), + # but reordering the z axis (e.g., (y, x, z, r)) breaks codegen. + # + # Therefore, if loop reordering changes the "z" location in bmm, + # it should be reverted to the default. + # This may not always produce the optimal loop order when strides + # do not align with the default assumption. + # + # TODO: Consider extending tl.dot codegen to support arbitrary loop orders. + if self.get_reduction_type() == "dot" and len(sizes) == 3: + order = list(range(len(sizes))) # default order + + # if z axis is not the outermost, use the default reorder. + if reindex0(order)[0] != 0: + newsizes = [sizes[i] for i in order] + reindex0 = same_reorder(order) + reindex1 = inverse_reorder(order) + + # for NHWC: reindex0([0,1,2,3]) = [0,2,3,1], reindex1([0,1,2,3]) = [0,3,2,1] + x_vars = reindex0(x_vars) + + if simplify_loops: + newsizes, reindex2, _prune = V.graph.sizevars._simplify_loops( + x_vars, + newsizes, + index_prevent_reordering(index_formulas, x_vars, newsizes), + ) + reindex = fuse_reindexing(reindex1, reindex2) + else: + reindex = reindex1 + return newsizes, reindex, reindex1 + + support_vars = index_vars + reduce_vars + should_merge_loops = ( + not is_gpu(get_device_type(self)) or not config.loop_ordering_after_fusion + ) + iter_ranges, iter_reindex, _ = simplify_and_reorder( + index_vars, + support_vars, + index_size, + should_merge_loops, + ) + + # Like iteration dimensions, we may also want to delay merging reduction dimensions. + # E.g., if we reduce a tensor [M, N, K] for its M and N dimensions followed by a pointwise + # kernel, merging M and N dimension too early makes it hard to decide what loop order + # we should pick for the piontwise kernel so that it is fusible with the reduction. + reduce_ranges, reduce_reindex, _ = simplify_and_reorder( + reduce_vars, support_vars, reduce_size, should_merge_loops + ) + + # retrace the loop body with simplification and reordering applied + (iter_vars, reduce_vars), var_ranges = dependencies.index_vars_no_squeeze( + iter_ranges, + reduce_ranges, + prefix="p", + ) + body = LoopBody( + body, + [iter_reindex(iter_vars), reduce_reindex(reduce_vars)], + var_ranges, + iter_vars, + reduce_vars, + ) + return (iter_ranges, reduce_ranges), body + + @staticmethod + def _apply_loop_reordering( + index_vars: Sequence[sympy.Symbol], + support_vars: Sequence[sympy.Symbol], + sizes: Sequence[int], + memory_addrs: list[sympy.Expr], + priority_idx: list[int] | None = None, + ) -> tuple[ + list[int], + Callable[[Sequence[int]], Sequence[int]], + Callable[[Sequence[int]], Sequence[int]], + ]: + """ + Shuffle the order of loops around to hopefully improve performance. + """ + from .scheduler import pick_loop_order + + if priority_idx is None: + priority_idx = [] + + try: + strides = [ + V.graph.sizevars.stride_hints(expr, index_vars, support_vars) + for expr in memory_addrs + ] + assert len(strides) == len(memory_addrs) and len(strides[0]) == len( + index_vars + ) + order = list(reversed(pick_loop_order(strides, sizes, priority_idx))) + except Exception: + if config.debug: + log.warning( + "Did not simplify complex index:\n%s\n%s", + dict(zip(index_vars, sizes)), + memory_addrs, + ) + order = list(range(len(sizes))) + sizes = [sizes[i] for i in order] + return sizes, same_reorder(order), inverse_reorder(order) + + def get_pointwise_size(self) -> Sequence[Expr]: + return self.data.get_pointwise_size() + + def get_reduction_size(self) -> Sequence[Expr]: + return self.data.get_reduction_size() + + def get_reduction_type(self) -> str | None: + return self.data.get_reduction_type() + + def is_no_op(self) -> bool: + return self.data.is_zero_elements() + + def should_allocate(self) -> bool: + return True + + def constant_to_device(self, device: torch.device) -> IRNode: + """Move this to a given device. Requires that all reads are to constants.""" + return self.data.constant_to_device(device) + + +@dataclasses.dataclass(frozen=True) +class FinalizeCodegenResult: + """Structured result from TemplateBuffer._finalize_codegen for external backends.""" + + source: str + imports: list[str] + call_preamble: list[str] + call_args: list[str] + + +class TemplateBuffer(OperationBuffer): + """ + Base class for template operators that support epilogue and prologue fusion. + Subclasses: TritonTemplateBuffer (built-in Triton templates), + HelionTemplateBuffer (Helion kernels), etc. + """ + + def __init__( + self, + layout: OutputSpec, + inputs: Sequence[IRNode], + make_kernel_render: Callable[..., Any] | None, + mutated_inputs: Iterable[IRNode] | None = None, + allowed_prologue_inps: OrderedSet[str] | None = None, + named_inputs: dict[str, IRNode] | None = None, + ) -> None: + super().__init__(name=None, layout=layout) + self.inputs = InputsKernel.unwrap_storage(inputs) + self.make_kernel_render = make_kernel_render + self.name = V.graph.register_buffer(self) + V.graph.register_operation(self) + # Annotations dict for storing metadata (e.g., KernelTemplateChoice) + self.annotations: dict[str, Any] = {} + + # Output buffer names eligible for epilogue fusion. + # Maps buffer name → kernel parameter name (e.g. "buf3" → "result"). + self.epilogue_fusable_outputs: dict[str, str] = {} + # For multi-output kernels: maps child buffer name → MultiOutput + # node. Used by call_kernel to emit tuple-unpacking lines. + self._multi_output_children: dict[str, MultiOutput] = {} + # Maps kernel parameter name → IRNode for each tensor input. + # Used by ExternalTritonTemplateKernel to set up prologue fusion and + # by HelionTemplateBuffer to resolve call arguments. + self._named_inputs: dict[str, IRNode] = ( + dict(named_inputs) if named_inputs else {} + ) + + # Inputs that the kernel mutates in-place + self.mutated_inputs = mutated_inputs + self.mutation_outputs: list[MutationOutput] = [] + if mutated_inputs is not None: + first_input = self.inputs[0] + assert isinstance(first_input, IRNode), type(first_input) + device = first_input.get_device() + self.mutation_outputs = [ + MutationOutput(NoneLayout(device=device), buf, self) + for buf in mutated_inputs + ] + # Input buffer names eligible for prologue fusion. + self.allowed_prologue_inps: OrderedSet[str] = ( + allowed_prologue_inps or OrderedSet() + ) + # Per-template fusion overrides. None means fall back to global + # config.epilogue_fusion / config.prologue_fusion. + self.allow_epilogue_fusion: bool | None = None + self.allow_prologue_fusion: bool | None = None + + @property + def dtype(self) -> torch.dtype: + if isinstance(self.layout, MultiOutputLayout): + raise NotImplementedError( + "Multi-output templates do not have a single dtype" + ) + return self.get_layout().dtype + + def get_read_writes(self) -> dependencies.ReadWrites: + return self.extract_read_writes(normalize=True) + + def _read_deps_from_inputs(self, normalize: bool) -> OrderedSet[dependencies.Dep]: + """Build read dependencies from all inputs.""" + reads: OrderedSet[dependencies.Dep] = OrderedSet() + for inp_raw in self.inputs: + assert isinstance(inp_raw, (ReinterpretView, Buffer)), type(inp_raw) + inp: ReinterpretView | Buffer = inp_raw + assert isinstance(inp.layout, Layout), type(inp.layout) + inp_indexer = inp.layout.make_indexer() + + def dummy(index: Sequence[Any], rindex: Sequence[Any]) -> Any: + assert len(rindex) == 0 + return ops.load(inp.get_name(), inp_indexer(index)) + + reads |= dependencies.extract_read_writes( + dummy, inp.get_size(), (), normalize=normalize + ).reads + return reads + + def extract_read_writes(self, normalize: bool = False) -> dependencies.ReadWrites: + """Extract read/write dependencies for this TemplateBuffer. + + When the layout is MultiOutputLayout (multi-output templates), the + buffer itself has no data layout, so we cannot build an indexer. + Instead, synthesize a trivial write dep and derive read deps from + the named tensor inputs (``_named_inputs``). For single-output + templates with a concrete layout, fall through to the standard path. + """ + if isinstance(self.layout, MultiOutputLayout): + writes: OrderedSet[dependencies.Dep] = OrderedSet( + [ + dependencies.MemoryDep( + self.get_name(), sympy.Integer(0), var_names=(), size=() + ), + ] + ) + return dependencies.ReadWrites( + reads=self._read_deps_from_inputs(normalize), + writes=writes, + index_exprs=OrderedSet(), + range_vars=None, + var_ranges=None, + ) + + name = self.get_name() + indexer = self.get_layout().make_indexer() + + def dummy(index: Sequence[Any], rindex: Sequence[Any]) -> Any: + assert len(rindex) == 0 + return ops.store(name, indexer(index), "fake") + + deps = dependencies.extract_read_writes( + dummy, self.get_size(), (), normalize=normalize + ) + deps.reads |= self._read_deps_from_inputs(normalize) + return deps + + def get_reduction_size(self) -> Sequence[Expr]: + return sympy.S.One + + def get_reduction_type(self) -> str | None: + return None + + def should_allocate(self) -> bool: + return True + + def simplify_and_reorder( + self, + extra_indexing_constraints: tuple[dict[Any, Any], list[Any]] | None = None, + recompute_sizes_body_func: Callable[..., Any] | None = None, + ) -> tuple[tuple[Sequence[Expr], list[Expr]], LoopBody | None]: + return ( + ( + self.get_size(), + [], + ), + None, + ) + + def is_multi_outputs_template(self) -> bool: + """Whether this template produces multiple outputs via MultiOutputLayout.""" + return isinstance(self.layout, MultiOutputLayout) + + def get_allowed_prologue_inps(self) -> OrderedSet[str]: + return self.allowed_prologue_inps + + def _finalize_codegen( + self, hook_outputs: dict[str, str] + ) -> FinalizeCodegenResult | None: + """Called after epilogue/prologue subgraph codegen with rendered hook outputs. + + ``hook_outputs`` maps placeholder keys (e.g. ````, + ````) to the Triton code generated by Inductor for + each fused subgraph. + + Return a ``FinalizeCodegenResult`` to provide custom source code and + call metadata, or ``None`` to use the default codegen path. + """ + return None + + @classmethod + def realize_template_input(cls, tb: TensorBox) -> IRNode: + """Realize a TensorBox, preserving MultiOutput layout (unlike ExternKernel.realize_input).""" + if isinstance(tb, TensorBox) and isinstance(tb.data, MultiOutput): + return tb.data + result = ExternKernel.realize_input(tb) + if isinstance(result, StorageBox): + result = result.data + if isinstance(result.layout, FlexibleLayout): # type: ignore[union-attr] + result.freeze_layout() + return result + + @classmethod + def build_multi_outputs( + cls, + template_buf: TemplateBuffer, + structured: object, + *, + direct_alias_at_leaf: dict[int, IRNode] | None = None, + on_tensor_leaf: Callable[[str, MultiOutput, list[tuple[type, int]], int], None] + | None = None, + on_non_tensor_leaf: Callable[[int], None] | None = None, + ) -> tuple[TensorBox, ...]: + """Walk a structured output tree, creating MultiOutput nodes for tensor leaves.""" + seen_outputs: dict[int, TensorBox] = {} + leaf_counter = itertools.count() + + def walk(output: object, indices: list[tuple[type, int]]) -> list[TensorBox]: + if isinstance(output, (list, tuple)): + results: list[TensorBox] = [] + for i, item in enumerate(output): + results.extend(walk(item, [*indices, (type(output), i)])) + return results + leaf_idx = next(leaf_counter) + if isinstance(output, torch.Tensor): + if direct_alias_at_leaf and leaf_idx in direct_alias_at_leaf: + return [TensorBox.create(direct_alias_at_leaf[leaf_idx])] + tid = id(output) + if tid in seen_outputs: + return [seen_outputs[tid]] + mo = MultiOutput( + FallbackKernel.tensor_to_layout(output), template_buf, indices + ) + template_buf._multi_output_children[mo.get_name()] = mo + if on_tensor_leaf is not None: + on_tensor_leaf(mo.get_name(), mo, indices, leaf_idx) + tb = TensorBox(mo) + seen_outputs[tid] = tb + return [tb] + # Non-tensor leaf (int, SymInt, None, etc.) + if on_non_tensor_leaf is not None: + on_non_tensor_leaf(leaf_idx) + return [] + + return tuple(walk(structured, [])) + + +class TritonTemplateBuffer(TemplateBuffer): + def __init__( + self, + layout: Layout, + inputs: Sequence[IRNode], + make_kernel_render: Callable[_P, _T] | None, + mutated_inputs: Iterable[IRNode] | None = None, + allowed_prologue_inps: OrderedSet[str] | None = None, + ) -> None: + """ + NOTE:[TritonTemplates with multiple outputs] + We want the ability for TritonTemplates to output multiple tensors. Triton + kernels have no notion of outputs and this is done by creating tensors that + are then mutated by the kernel. Currently our STORE_OUTPUT codegen doesn't + support creating multinode outputs for triton templates. + We work around this by creating an extra input buffer during the lowering + and we mark them as mutated inputs. + """ + super().__init__( + layout, + inputs, + make_kernel_render, + mutated_inputs=mutated_inputs, + allowed_prologue_inps=allowed_prologue_inps, + ) + assert self.name is not None + self.epilogue_fusable_outputs = {self.name: self.name} + + self.subgraph_inps: list[IRNode | sympy.Expr | None] | None = None + self.subgraph_outs: list[IRNode | None] | None = None + + @cache_on_self_and_args("TritonTemplateBuffer") + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + res = super().get_free_symbol_uses(unbacked_only) + subgraph_outs = self.subgraph_outs if self.subgraph_outs else [] + subgraph_inps = self.subgraph_inps if self.subgraph_inps else [] + + for inp in subgraph_inps: + if isinstance(inp, sympy.Expr): + res.update(get_free_symbols(inp, unbacked_only)) + elif isinstance(inp, IRNode): + res.update(inp.get_free_symbol_uses(unbacked_only)) + else: + assert inp is None + + for out in subgraph_outs: + if isinstance(out, IRNode): + res.update(out.get_free_symbol_uses(unbacked_only)) + else: + assert out is None + + return res + + def get_outputs(self) -> list[Buffer]: + return [self, *self.mutation_outputs] + + def __str__(self) -> str: + out = f"TritonTemplateBuffer(layout={self.layout})" + return out + + +PrimitiveInfoType = int | float | bool | str | list[int | str | float | bool] + + +class ChoiceCaller: + """ + Represents a possible choice used in autotune_process.py. + During autotuning, self.benchmark() is first called to get benchmark result, + and if this choice is selected, self.output_node() is called to get the output_node. + + Children classes: TritonTemplateCaller, CUTLASSTemplateCaller. + """ + + def __init__( + self, + name: str, + input_nodes: list[Buffer], + layout: Layout, + description: str, + ) -> None: + super().__init__() + self.name = name + self.layout = layout + self.input_nodes = input_nodes + # An additional description used to describe the choice (useful for + # knowing what autotuning is choosing) + self.description = description + self.failed: bool = False + # When True, benchmark using CUDA graph capture/replay + self._benchmark_with_cudagraphs: bool = False + # A place to store annotations that can be read post benchmarking + # Use this to shuttle information between ChoieCaller generation + # and the end of benchmarking + self.annotations: dict[Any, Any] = {} + # Subclass-overridden attributes for subgraph-based choices + self.gm: torch.fx.GraphModule | None = None + self.decomposition: Callable[..., Any] | None = None + self.decomposition_kwargs: dict[str, Any] = {} + self.config_patches: dict[str, Any] = {} + + def benchmark(self, *args: Any, out: torch.Tensor) -> float: + algo = self.to_callable() + if self._benchmark_with_cudagraphs: + return benchmarker.benchmark_gpu_with_cuda_graph(lambda: algo(*args)) + if config.profile_bandwidth_with_do_bench_using_profiling: + return do_bench_using_profiling(lambda: algo(*args)) # type: ignore[arg-type] + return benchmarker.benchmark(algo, args, {"out": out}, device=None) + + def call_name(self) -> str: + raise NotImplementedError + + def to_callable(self) -> Callable[..., Any]: + raise NotImplementedError + + def kernel_hash_key(self) -> str: + """ + Hash key for the underlying kernel. By default, we assume there are no + runtime params, so kernel hash key defaults to choice caller's hash key. + """ + return self.hash_key() + + def hash_key(self) -> str: + raise NotImplementedError + + def output_node(self) -> TensorBox: + raise NotImplementedError + + def info_dict(self) -> dict[str, PrimitiveInfoType | list[PrimitiveInfoType]]: + """Information returned here is logged to the autotune log file when that is enabled.""" + return {} + + def autoheuristic_id(self) -> str: + return "unsupported_choice" + + def mark_failed(self) -> None: + """ + Mark the choice as failed so that it can be + removed later. Useful for when we decouple + compilation and tuning. + """ + self.failed = True + + +class TritonTemplateCallerBase(ChoiceCaller): + def get_make_kernel_render(self) -> Any: + raise NotImplementedError + + +class MultiTemplateBuffer(TritonTemplateBuffer): + """ + Represents a Buffer with multiple backing implementation choices. + + Choices can be TritonTemplates or ExternKernels. During scheduling if there is a potential + epilogue we will benchmark each of the choices with the epilogue to determine an implementation. + Otherwise, the fastest base choice will be chosen. + """ + + def __init__( + self, + layout: Layout, + inputs: Sequence[IRNode], + choice_timings_fn: Callable[[int | None], dict[ChoiceCaller, float]], + unfiltered_choices: list[ChoiceCaller], + allowed_prologue_inps: OrderedSet[str], + ) -> None: + super().__init__( + layout=layout, + inputs=inputs, + make_kernel_render=None, + allowed_prologue_inps=allowed_prologue_inps, + ) + self._choice_timings_fn = choice_timings_fn + self._choice_timings: dict[int | None, dict[ChoiceCaller, float]] = {} + self._choices: list[ChoiceCaller] = unfiltered_choices + self.original_inputs = inputs + self._output_plannable = all( + isinstance(choice, TritonTemplateCallerBase) + or ( + isinstance(choice, torch._inductor.select_algorithm.ExternKernelCaller) + and choice.has_out_variant + ) + for choice in unfiltered_choices + ) + self._make_kernel_renders: dict[int | None, Any] = {} + + @property + def output_plannable(self) -> bool: + """ + Are all possible choices TritonTemplates or Extern Kernels with out variants + """ + return self._output_plannable + + @property + def choices(self) -> list[ChoiceCaller]: + return self._choices + + def choice_timings( + self, hint_override: int | None = None + ) -> dict[ChoiceCaller, float]: + if hint_override not in self._choice_timings: + self._choice_timings[hint_override] = self._choice_timings_fn(hint_override) + return self._choice_timings[hint_override] + + @contextlib.contextmanager + def swap_as_triton_caller(self, caller: TritonTemplateCallerBase) -> Iterator[None]: + assert isinstance( + caller, torch._inductor.select_algorithm.TritonTemplateCaller + ), type(caller) + assert self.layout == caller.layout + + render = self.make_kernel_render + self.make_kernel_render = caller.get_make_kernel_render() + try: + yield + finally: + self.make_kernel_render = render + + def finalize_as_triton_caller(self, caller: TritonTemplateCallerBase) -> None: + assert isinstance( + caller, torch._inductor.select_algorithm.TritonTemplateCaller + ), type(caller) + assert self.get_size() == caller.layout.size + assert self.get_stride() == caller.layout.stride + self.make_kernel_render = caller.get_make_kernel_render() + + def get_min_choice( + self, hint_override: int | None = None + ) -> tuple[ChoiceCaller, float]: + timings = self.choice_timings(hint_override=hint_override) + min_choice = min(timings, key=timings.get) # type: ignore[arg-type] + return (min_choice, timings[min_choice]) + + def finalize_as_triton_callers( + self, callers: dict[int | None, TritonTemplateCallerBase] + ) -> None: + """Finalize with multiple callers for different hint overrides""" + for hint_override, caller in callers.items(): + self._make_kernel_renders[hint_override] = caller.get_make_kernel_render() + + # Set the default to be the one without hint override + self.make_kernel_render = self._make_kernel_renders[None] + + +class CUTLASSTemplateBuffer(TemplateBuffer): + def __init__( + self, + layout: Layout, + inputs: Sequence[IRNode], + make_kernel_render: Callable[_P, _T], + workspace_size: int, + template: CUTLASSTemplate, + supports_epilogue_fusion: bool, + ) -> None: + super().__init__(layout, inputs, make_kernel_render) + # Global memory (in bytes) needed for this template. + self.workspace_size = workspace_size + self.template = template + self.supports_epilogue_fusion = supports_epilogue_fusion + + def get_workspace_size(self) -> int: + return self.workspace_size if self.workspace_size is not None else 0 + + def emulate_store_fn(self) -> None: + for output in self.get_outputs(): + ops.store(output.get_name(), None, None) + + +class CppTemplateBuffer(TemplateBuffer): + def __init__( + self, + layout: Layout, + inputs: Sequence[IRNode], + make_kernel_render: Callable[_P, _T], + template: CUTLASSTemplate, + choice: Any, + ) -> None: + super().__init__(layout, inputs, make_kernel_render) + self.template = template + self.choice = choice + self.outputs: list[Buffer] | None = None + + def get_layout(self) -> Layout: + if isinstance(self.layout, MultiOutputLayout): + assert isinstance(self.outputs, Iterable), type(self.outputs) + + first_output = self.outputs[0] + assert isinstance(first_output, Buffer), type(first_output) + layout = first_output.layout + assert isinstance(layout, Layout), type(layout) + return layout + else: + return super().get_layout() + + +class CuteDSLTemplateBuffer(TemplateBuffer): + """ + Buffer for CuteDSL (CUTLASS Python DSL) template kernels. + Similar to other template buffers but specialized for CuteDSL operations. + """ + + def __init__( + self, + layout: Layout, + inputs: Sequence[IRNode], + make_kernel_render: Callable[_P, _T], + template: Any, + mutated_inputs: Iterable[IRNode] | None = None, + ) -> None: + super().__init__(layout, inputs, make_kernel_render) + self.template = template + self.mutated_inputs = mutated_inputs + self.outputs: list[Buffer] = [self] + + if mutated_inputs is not None: + assert isinstance(self.inputs[0], IRNode), type(self.inputs[0]) + device = self.inputs[0].get_device() + self.outputs += [ + MutationOutput(NoneLayout(device=device), buf, self) + for buf in mutated_inputs + ] + + def get_outputs(self) -> list[Buffer]: + return self.outputs + + +class NVUniversalGemmBuffer(TemplateBuffer): + """ + Buffer for NVIDIA Universal GEMM kernels. + + Unlike CuteDSL templates which use Jinja templates, this generates + simpler Python code that directly calls the cutlass_api library. + """ + + def __init__( + self, + layout: Layout, + inputs: Sequence[IRNode], + kernel: Any, + accumulator_type: Any, + variant: Any, # GemmVariant, use Any to avoid circular import + workspace_size: int = 0, + scale_type_a: Any | None = None, + scale_type_b: Any | None = None, + swizzle_type_a: Any | None = None, + swizzle_type_b: Any | None = None, + ) -> None: + # We pass None initially, then override with our method below + super().__init__(layout, inputs, make_kernel_render=None) + self.kernel = kernel + self.accumulator_type = accumulator_type + self.outputs: list[Buffer] = [self] + self.workspace_size = workspace_size + self.variant = variant + self.scale_type_a = scale_type_a + self.scale_type_b = scale_type_b + self.swizzle_type_a = swizzle_type_a + self.swizzle_type_b = swizzle_type_b + # Store kernel metadata for code generation since kernels aren't serializeable yet + self.kernel_metadata = { + "kernel_name": kernel.metadata.kernel_name, + "min_cc": kernel.metadata.min_cc, + } + # Override the instance attribute set by parent with our method + # This is necessary because TemplateBuffer stores make_kernel_render as instance attr + self.make_kernel_render = self._make_kernel_render + + def get_workspace_size(self) -> int: + """Return the workspace size in bytes.""" + return self.workspace_size + + def get_outputs(self) -> list[Buffer]: + return self.outputs + + def _make_kernel_render( + self, out_node: Any, hint_override: int | None = None + ) -> tuple[Any, Any]: + """ + Create a kernel renderer for code generation. + + Returns (kernel, render) tuple where: + - kernel: NVUniversalGemmKernel object with call_kernel() method + - render: function that returns source code string + """ + from torch._inductor.codegen.nv_universal_gemm.nv_universal_gemm_kernel import ( + NVUniversalGemmKernel, + ) + from torch._inductor.utils import Placeholder + + input_nodes: list[Any] = [] + for inp in self.inputs: + if isinstance(inp, TensorBox): + inp = inp.data + if isinstance(inp, StorageBox): + inp = inp.data + input_nodes.append(inp) + + kernel_name = str(Placeholder.KERNEL_NAME) + + render_kernel = NVUniversalGemmKernel( + kernel_name=kernel_name, + input_nodes=input_nodes, + output_node=out_node, + kernel_metadata=self.kernel_metadata, + accumulator_type=self.accumulator_type, + workspace_size=self.workspace_size, + variant=self.variant, + scale_type_a=self.scale_type_a, + scale_type_b=self.scale_type_b, + swizzle_type_a=self.swizzle_type_a, + swizzle_type_b=self.swizzle_type_b, + ) + + def render(): + return render_kernel.render() + + return render_kernel, render + + +def is_node_sequence( + nodes: Sequence[IRNode | Sequence[IRNode]], +) -> TypeIs[Sequence[IRNode]]: + return all(isinstance(n, IRNode) for n in nodes) + + +@ir_dataclass(frozen=False) +class InputsKernel(OperationBuffer): + inputs: Sequence[IRNode | Sequence[IRNode]] + + def input_name(self, i: int) -> str: + input = self.inputs[i] + assert isinstance(input, IRNode) + return input.get_name() + + def get_read_writes(self) -> dependencies.ReadWrites: + reads = OrderedSet[dependencies.Dep]() + StarDep = dependencies.StarDep + for input in self.inputs: + if isinstance(input, Sequence): + reads.update(StarDep(x.get_name()) for x in input) + elif isinstance(input, ShapeAsConstantBuffer): + # Skip creating dependency for symbolics as they're visible globally + continue + else: + reads.add(StarDep(input.get_name())) + + writes = OrderedSet[dependencies.Dep]( + StarDep(buf.get_name()) for buf in self.get_outputs() + ) + + return dependencies.ReadWrites( + reads=reads, + writes=writes, + index_exprs=OrderedSet(), + ) + + def get_reads(self) -> OrderedSet[Dep]: + return self.get_read_writes().reads + + @classmethod + def unwrap_storage_for_input(cls, x: IRNode) -> IRNode: + if isinstance(x, TensorBox): + x = x.data + if isinstance(x, StorageBox): + x = x.data + if isinstance(x, BaseView) and not isinstance(x, ReinterpretView): + x = ExternKernel.realize_input(x) + if isinstance(x, TensorBox): + # when converting to ReinterpretView fails in the + # realize_input call above, the result will be wrapped + # into TensorBox / StorageBox pair as a result of the + # cls.copy_input call; so we should unwrap recursively + return cls.unwrap_storage_for_input(x) + if isinstance(x, TorchBindObject): + return x + assert isinstance(x, (Buffer, ReinterpretView)), type(x) + return x + + @staticmethod + def unwrap_storage( + inputs: Sequence[IRNode | Sequence[IRNode]], + ) -> list[IRNode | Sequence[IRNode]]: + inputs_new: list[IRNode | Sequence[IRNode]] = [] + for x in inputs: + if isinstance(x, Sequence): + x = [InputsKernel.unwrap_storage_for_input(i) for i in x] + else: + x = InputsKernel.unwrap_storage_for_input(x) + inputs_new.append(x) + return inputs_new + + def is_extern(self) -> bool: + return True + + def num_reads(self) -> int: + return 1 + + @cache_on_self_and_args("InputsKernel") + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + r = OrderedSet[sympy.Symbol]() + for inp in self.inputs: + if isinstance(inp, IRNode): + r |= inp.get_free_symbol_uses(unbacked_only) + else: + for inner_inp in inp: + r |= inner_inp.get_free_symbol_uses(unbacked_only) + return r + + +class NopKernel(InputsKernel): + def is_no_op(self) -> bool: + return True + + def get_reads(self) -> OrderedSet[Dep]: + return OrderedSet() + + +class ConcatKernel(NopKernel): + """ + There isn't actually a real kernel for concat, we just change the + storage for the upstream data. + """ + + @classmethod + def create(cls, inputs: Sequence[IRNode], dim: int) -> StorageBox: + """ + Create the concat kernel from inputs + """ + device = inputs[0].get_device() + dtype = inputs[0].get_dtype() + new_size = list(inputs[0].get_size()) + offsets_start = [0] + offsets_end = [new_size[dim]] + assert 0 <= dim < len(new_size) + for i in range(1, len(inputs)): + input_size = inputs[i].get_size() + offsets_start.append(new_size[dim]) + assert len(input_size) == len(new_size) + assert inputs[i].get_dtype() == dtype + assert inputs[i].get_device() == device + for j in range(len(new_size)): + if j == dim: + new_size[j] = new_size[j] + input_size[j] + else: + new_size[j] = V.graph.sizevars.check_equals_and_simplify( + new_size[j], input_size[j] + ) + offsets_end.append(new_size[dim]) + + output_stride: Sequence[int] = FlexibleLayout.contiguous_strides(new_size) + if config.comprehensive_padding: + # Ensure the output stride matches the alignment requirements + output_stride = Layout._pad_strides( + output_stride, new_size, inputs[0].dtype + ) + + # If any of the inputs is in CL format, use CL format for the output + for i in range(len(inputs)): + x = inputs[i] + if is_storage_and_layout(x): + layout = x.get_layout() + if isinstance( + layout, FixedLayout + ) and Layout.is_channels_last_contiguous(layout.size, layout.stride): + # use CL stride for the output + output_stride = make_channels_last_strides_for(new_size) + break + any_input_is_storage_and_layout = any(is_storage_and_layout(x) for x in inputs) + fx_node_args = V.graph.current_node.args[0] + # If any of the inputs has meta tensor and the meta tensor is in CL format, use CL format for the output + # Skip this check when fx_node_args is not a list (e.g., called from _pad_as_cat). + if ( + any_input_is_storage_and_layout is False + and isinstance(fx_node_args, list) + and any( + # pyrefly: ignore [missing-attribute] + "val" in arg.meta + and ( + # pyrefly: ignore [missing-attribute] + arg.meta["val"].is_contiguous(memory_format=torch.channels_last) + # pyrefly: ignore [missing-attribute] + or arg.meta["val"].is_contiguous( + memory_format=torch.channels_last_3d + ) + ) + for arg in fx_node_args + ) + ): + output_stride = make_channels_last_strides_for(new_size) + + is_pinned = all( + is_storage_and_layout(x) and x.get_layout().is_pinned for x in inputs + ) + + assert device is not None + concat_kernel = ConcatKernel( + name=None, + layout=FixedLayout( + device=device, + dtype=dtype, + size=new_size, + stride=output_stride, + is_pinned=is_pinned, + ), + inputs=[], + ) + kernel = StorageBox(concat_kernel) + op_names = [] + for i, inp in enumerate(inputs): + assert isinstance(inp, (BaseView, MutableBox)), type(inp) + input_buffer = cls.realize_into( + inp, + SliceView.create( + kernel, dim, offsets_start[i], offsets_end[i], clamp=False + ), + ) + assert isinstance(input_buffer, Buffer), type(input_buffer) + assert isinstance(concat_kernel.inputs, list), type(concat_kernel.inputs) + concat_kernel.inputs.append(input_buffer) + + if isinstance(inp.data, BaseView): + input_unwrapped = inp.data.unwrap_view() + else: + input_unwrapped = inp.data + + if ( + isinstance(input_unwrapped, StorageBox) + and input_unwrapped.is_input_buffer() + and (dev := inp.get_device()) is not None + and is_gpu(dev.type) + and not is_dynamic(input_buffer) + ): + op_names.append(input_buffer.get_operation_name()) + + if len(op_names) > 1 and V.graph.has_feature(device, BackendFeature.FOREACH): + V.graph.register_operation_list(op_names) + + concat_kernel.name = V.graph.register_buffer(concat_kernel) + concat_kernel.inputs = cls.unwrap_storage(concat_kernel.inputs) + V.graph.register_operation(concat_kernel) + + return kernel + + @classmethod + def can_realize_into_without_copy( + cls, src: IRNode, dst: IRNode | None = None + ) -> bool: + if isinstance(src, TensorBox): + # unwrap a TensorBox + return cls.can_realize_into_without_copy(src.data, dst) + + assert isinstance(src, (BaseView, StorageBox)), type(src) + if isinstance(src.data, MultiTemplateBuffer): + if ( + not isinstance(src.data.layout, FixedLayout) + or not src.data.output_plannable + ): + return False + + # we call can_realize_into_without_copy in cat lowering before we've decided + # on output format, optimistically assume layout matches + if dst is None: + return True + + # otherwise, check equality of layouts + if len(src.get_stride()) != len(dst.get_stride()): + return False + + return all( + V.graph.sizevars.statically_known_equals(s1, s2) + for s1, s2 in zip(src.get_stride(), dst.get_stride()) + ) + + return ( + hasattr(src.data, "layout") + and isinstance(src.data.layout, FlexibleLayout) + and not isinstance(src.data, ExternKernelAlloc) + ) + + @cache_on_self_and_args("ConcatKernel") + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + return NopKernel.get_free_symbol_uses(self, unbacked_only) + + @classmethod + def realize_into(cls, src: IRNode, dst: IRNode) -> IRNode: + # Attempt to turn this into a ReinterpretView rather than assert. + # This has concessions around layout, as as_storage_and_layout + # can cause us to go from flexible to fixed layout. + if not isinstance(dst, ReinterpretView): + if is_storage_and_layout(dst): + storage, layout = as_storage_and_layout(dst) + dst = ReinterpretView(data=storage, layout=layout) + assert isinstance(dst, ReinterpretView), type(dst) + if isinstance(src, TensorBox): + # unwrap a TensorBox + return cls.realize_into(src.data, dst) + + if isinstance(src, StorageBox): + src.realize() + # ExternKernelAlloc has specific requirements for output layout, should create a copy + assert hasattr(src.data, "layout") + if cls.can_realize_into_without_copy(src, dst): + # pyrefly: ignore [missing-attribute] + src.data.layout = NonOwningLayout(dst) + return src.data + # introduce a copy + pw = Pointwise.create( + device=src.get_device(), + dtype=src.get_dtype(), + inner_fn=src.make_loader(), + ranges=[ + V.graph.sizevars.check_equals_and_simplify(a, b) + for a, b in zip(src.get_size(), dst.get_size()) + ], + ) + return cls.realize_into(pw, dst) + + def should_allocate(self) -> bool: + return True + + +@ir_dataclass(frozen=False) +class ExternKernel(InputsKernel): + """ + A class that represents Kernels which are not directly lowered to Inductor + Loop Level IR, such as custom operators, or aten operators which we fallback to. + """ + + constant_args: Sequence[Any] = () + kwargs: dict[str, Any] = dataclasses.field(default_factory=dict) + output_view: ReinterpretView | None = None + python_kernel_name: str | None = None + cpp_kernel_name: str | None = None + # FIXME: in some cases we sill need to explicitly pass in ordered_kwargs_for_cpp_kernel + # We shouldn't need to do this since the information can be retrieved from op_overload._schema. + ordered_kwargs_for_cpp_kernel: Iterable[str] = dataclasses.field( + default_factory=list + ) + op_overload: _OpOverloads | None = None + arg_properties: list[dict[str, Any]] | None = None + allarg_properties: dict[str, dict[str, Any]] = dataclasses.field( + default_factory=dict + ) + kwarg_properties: dict[str, dict[str, Any]] | None = None + unbacked_bindings: dict[sympy.Symbol, pytree.KeyPath] = dataclasses.field( + default_factory=dict + ) + mutation_outputs: list[MutationOutput] = dataclasses.field(default_factory=list) + + def __init__( + self, + name: str | None, + layout: OutputSpec, + inputs: Sequence[IRNode | Sequence[IRNode]], + constant_args: Sequence[Any] = (), + kwargs: dict[str, Any] | None = None, + output_view: ReinterpretView | None = None, + python_kernel_name: str | None = None, + cpp_kernel_name: str | None = None, + ordered_kwargs_for_cpp_kernel: Iterable[str] = (), + op_overload: _OpOverloads | None = None, + ) -> None: + super().__init__( + name=name, + layout=layout, + inputs=inputs, + ) + self.constant_args = constant_args + self.kwargs = kwargs if kwargs else {} + self.output_view = output_view + self.op_overload = op_overload + self.set_cpp_kernel_name(cpp_kernel_name) + self.set_python_kernel_name(python_kernel_name) + self.ordered_kwargs_for_cpp_kernel = ordered_kwargs_for_cpp_kernel + self.collect_arg_kwarg_properties() + self.unbacked_bindings = {} + self.mutation_outputs = [] + self.fx_node = V.graph.current_node + # Annotations dict for storing metadata (e.g., KernelTemplateChoice) + self.annotations: dict[str, Any] = {} + + def get_outputs(self) -> list[Buffer]: + return [self, *self.mutation_outputs] + + def get_unbacked_symbol_defs(self) -> OrderedSet[sympy.Symbol]: + return OrderedSet() + + def collect_arg_kwarg_properties(self) -> None: + # if self.op_overload is torch._ops.OpOverload, we can use its schema to collect additional + # information for args and kwargs, e.g. type and default value, to help with the cpp wrapper codegen + self.arg_properties = ( + [ + { + "name": x.name, + "type": x.real_type, + "default_value": x.default_value, + } + for x in self.op_overload._schema.arguments + if not x.kwarg_only + ] + if isinstance(self.op_overload, torch._ops.OpOverload) + else [{} for i in range(len(self.inputs))] + ) + self.allarg_properties = ( + { + x.name: {"type": x.real_type, "default_value": x.default_value} + for x in self.op_overload._schema.arguments + } + if isinstance(self.op_overload, torch._ops.OpOverload) + else {} + ) + # FIXME: self.kwargs does not always match kwargs defined in schema, so sometimes + # ordered_kwargs_for_cpp_kernel is explicitly passed in. + if isinstance(self.op_overload, torch._ops.OpOverload): + if not self.ordered_kwargs_for_cpp_kernel: + self.ordered_kwargs_for_cpp_kernel = [ + x.name for x in self.op_overload._schema.arguments if x.kwarg_only + ] + self.schema_kwargs = [ + x for x in self.op_overload._schema.arguments if x.kwarg_only + ] + else: + self.schema_kwargs = [] + + def decide_layout(self) -> None: + if isinstance(self.layout, FlexibleLayout): + self.apply_constraint() + self.freeze_layout() + + def codegen_comment( + self, wrapper: PythonWrapperCodegen, kernel_name: str | None = None + ) -> None: + origin_str, _detailed_origin_str = get_kernel_metadata(self, wrapper) + if origin_str: + wrapper.make_comment(origin_str) + + if not kernel_name: + kernel_name = self.try_get_kernel_name() + if kernel_name: + from .debug import set_kernel_post_grad_provenance_tracing + + debug_handle = set_kernel_post_grad_provenance_tracing( + self, kernel_name, is_extern=True + ) + wrapper.write_provenance_debug_handle(kernel_name, debug_handle) + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + raise NotImplementedError + + def set_cpp_kernel_name(self, cpp_kernel_name: str | None = None) -> None: + self.cpp_kernel_name = cpp_kernel_name + if not V.graph.cpp_wrapper or not isinstance( + self.op_overload, torch._ops.OpOverload + ): + return + + kernel = self.op_overload + if self.cpp_kernel_name is None: + # Try to construct cpp_kernel_name from op_overload + if kernel.namespace == "aten": + # Calling with the default kernel name can lead to ambiguous behavior like the following example. + # repeat_interleave(const at::Tensor & repeats, std::optional output_size=std::nullopt) + # repeat_interleave(const at::Tensor & self, int64_t repeats, + # std::optional dim=std::nullopt, std::optional output_size=std::nullopt) + opname = ( + kernel.__name__.split(".")[0] + if kernel._overloadname == "default" + else kernel.__name__.replace(".", "_") + ) + # If the op has a versioned c_shim entry, call the latest _v{N} + # variant so new AOTI artifacts match the current op schema. The + # unversioned shim is retained solely for BC with existing + # already-compiled artifacts. + from torchgen.aoti.fallback_ops import inductor_fallback_ops + + version_info = inductor_fallback_ops.get(f"aten.{kernel.__name__}", {}) + latest_version = max( + (int(v[1:]) for v in version_info if v.startswith("v")), + default=1, + ) + if latest_version > 1: + opname = f"{opname}_v{latest_version}" + self.cpp_kernel_name = f"at::_ops::{opname}::call" + else: + self.cpp_kernel_name = kernel._schema.name + + def set_python_kernel_name(self, python_kernel_name: str | None) -> None: + self.python_kernel_name = python_kernel_name + if python_kernel_name is not None: + return + + kernel = self.op_overload + if kernel is None: + pass + elif isinstance(kernel, torch._ops.HigherOrderOperator): + self.python_kernel_name = f"torch.ops.higher_order.{kernel.__name__}" + else: + self.python_kernel_name = ( + f"{kernel.__module__.replace('._ops.', '.ops.')}.{kernel.__name__}" + ) + + def try_get_kernel_name(self) -> str | None: + from .codegen.cpp_wrapper_cpu import CppWrapperCpu + + device = d.type if (d := self.get_device()) else V.graph.device_type + if V.graph.fx_wrapper: + return self.python_kernel_name + elif V.graph.cpp_wrapper: + assert isinstance(V.graph.wrapper_code, CppWrapperCpu), type( + V.graph.wrapper_code + ) + if self.cpp_kernel_name is None: + return None + return V.graph.wrapper_code.get_c_shim_func_name( + self.cpp_kernel_name, device + ) + else: + return self.python_kernel_name + + def get_kernel_name(self) -> str: + name = self.try_get_kernel_name() + assert name is not None + return name + + @staticmethod + def copy_input(x: IRNode) -> TensorBox: + pw = Pointwise.create( + device=x.get_device(), + dtype=x.get_dtype(), + inner_fn=x.make_loader(), + ranges=x.get_size(), + origin_node=x.get_origin_node(), + traceback=x.get_traceback(), + ) + pw.realize() + return pw + + @classmethod + def process_kernel( + cls, kernel: _OpOverloads, *args: Any, **kwargs: Any + ) -> tuple[ + Any, + list[Any], + list[Any], + Callable[[Any, Any], Any], + dict[sympy.Symbol, pytree.KeyPath] | None, + ]: + """Partition kernel args into tensor and non-tensor, realize tensor inputs, + re-run fake tensor propagation with the realized strides, and return + (example_output, tensor_args, non_tensor_args, unflatten_args, unbacked_bindings). + + unflatten_args(new_tensor_args, new_non_tensor_args) reconstructs the + original (args, kwargs) tree from replacement lists. + """ + binded_args = {"args": args, "kwargs": kwargs} + + args_flat, args_spec = pytree.tree_flatten(binded_args) + + args_flat_is_tensor: list[bool] = [] + # tensor_args can be either tensor or torchbind objects + tensor_args: list[IRNode] = [] + non_tensor_args: list[object] = [] + real_non_tensor_args: list[ + FakeScriptObject + | torch._C.Generator + | torch._C.ScriptObject + | torch.Tensor + | IntLikeType + ] = [] + for arg in args_flat: + match arg: + case Expr(): + node = V.graph.sizevars.shape_env.create_symintnode(arg, hint=None) + args_flat_is_tensor.append(False) + non_tensor_args.append(node) + real_non_tensor_args.append(node) + + case GeneratorState(): + args_flat_is_tensor.append(False) + non_tensor_args.append(arg) + device_index = arg.device.index + assert arg.device.type == "cuda" and device_index is not None + real_non_tensor_args.append( + torch.cuda.default_generators[device_index].clone_state() + ) + + case OpaqueObjectState(): + args_flat_is_tensor.append(False) + non_tensor_args.append(arg) + real_non_tensor_args.append(arg.value) + + case IRNode(): + args_flat_is_tensor.append(True) + tensor_args.append(arg) + + case _: + args_flat_is_tensor.append(False) + non_tensor_args.append(arg) + real_non_tensor_args.append(arg) + + def unflatten_args( + new_tensor_args: Sequence[_T], new_non_tensor_args: Sequence[_T] + ) -> tuple[list[_T], dict[str, _T]]: + result = [] + it_tensors = iter(new_tensor_args) + it_non_tensors = iter(new_non_tensor_args) + for is_tensor in args_flat_is_tensor: + if is_tensor: + result.append(next(it_tensors)) + else: + result.append(next(it_non_tensors)) + r = pytree.tree_unflatten(result, args_spec) + return r.get("args", []), r.get("kwargs", {}) + + tensor_args = [cls.realize_input(x) for x in tensor_args] + + # freeze layout otherwise our output stride calculation might + # become incorrect + for x in tensor_args: + if is_storage_and_layout(x): + as_storage_and_layout(x, freeze=True) + + # Rerun fake tensor propagation, because Inductor may have changed the + # strides of inputs and we need to determine accurately what the + # output stride will be. + example_args: list[ + torch.Tensor | torch._C.ScriptObject | FakeScriptObject | torch.Generator + ] = [] + + # We need to retain the constant values of fake tensors that we originally + # propagated the graph with, because for some operators running without a + # constant would trigger an error / DataDependentException + for x in tensor_args: + # if x is a view of a constant, we need to realize the view + # (we can't pass the constant into the kernel directly) + if not isinstance(x, BaseView) and x.get_name() in V.graph.constants: + example_args.append(V.graph.constants[x.get_name()]) + elif ( + not isinstance(x, BaseView) + and x.get_name() in V.graph.torchbind_constants + ): + example_args.append(V.graph.torchbind_constants[x.get_name()]) + elif isinstance(x, TorchBindObject): + example_args.append(x.get_value()) + elif isinstance(x, OpaqueMultiOutput): + example_args.append(x.opaque_example_value) + elif isinstance(x, torch._inductor.ir.GeneratorState): + device_index = x.device.index + assert x.device.type == "cuda" and device_index is not None + example_args.append( + torch.cuda.default_generators[device_index].clone_state() + ) + else: + example_args.append(ir_node_to_tensor(x)) + + new_args, new_kwargs = unflatten_args(example_args, real_non_tensor_args) + example_output = kernel(*new_args, **new_kwargs) + + unbacked_bindings: dict[sympy.Symbol, pytree.KeyPath] | None = None + if shape_env := V.fake_mode.shape_env: + node_meta_val = V.current_node.meta.get("val") + ctx: AbstractContextManager[None] = nullcontext() + if V.current_node.target is torch._higher_order_ops.effects.with_effects: + # remove the first effect token in meta["val"] and meta["unbacked_bindings"] + node_meta_val = node_meta_val[1] + ctx = _remove_effect_token_unbacked_bindings(V.current_node) + + with ctx: + rebind_unbacked(shape_env, V.current_node, example_output) + unbacked_bindings = compute_unbacked_bindings( + shape_env, example_output, node_meta_val + ) + + example_out_li = ( + [example_output] + if not isinstance(example_output, (list, tuple)) + else example_output + ) + # When graph_partition is enabled, skip - partitioning handles sparse outputs + for t in example_out_li: + if ( + isinstance(t, torch.Tensor) + and t.is_sparse + and not config.graph_partition + ): + msg = "sparsity not handled. Please file issue for sparse inference weights." + if stack_trace := V.graph.current_node.meta.get("stack_trace", None): + msg = f"{msg} Found from : \n {stack_trace}" + V.graph.disable_cudagraphs_reason = msg + + return ( + example_output, + tensor_args, + non_tensor_args, + unflatten_args, + unbacked_bindings, + ) + + @classmethod + def convert_to_reinterpret_view(cls, x: IRNode) -> ReinterpretView: + """ + In order to pass this to an extern kernel we need a + ReinterpretView not a View. This allows us to avoid some + unneeded copies. + """ + assert isinstance(x, BaseView), type(x) + if isinstance(x, ReinterpretView): + return x + + # NOTE: Don't use extract_read_writes here as it fails when + # make_loader() inlines the computation + x_unwrap_view = x.unwrap_view() + buf = V.graph.get_buffer(x_unwrap_view.get_name()) + assert buf is not None + x_unwrap_view_fx_node = buf.get_origin_node() + # Prefer channels last format according to how the format is set from eager. + if ( + x_unwrap_view_fx_node is not None + and "val" in x_unwrap_view_fx_node.meta + and isinstance(x_unwrap_view, (ReinterpretView, Buffer, MutableBox)) + and isinstance(x_unwrap_view.layout, FlexibleLayout) + and ( + is_contiguous_for_memory_format_or_false( + x_unwrap_view_fx_node.meta["val"], + memory_format=torch.channels_last, + ) + or is_contiguous_for_memory_format_or_false( + x_unwrap_view_fx_node.meta["val"], + memory_format=torch.channels_last_3d, + ) + ) + ): + x_unwrap_view.freeze_layout_with_same_order( + make_channels_last_strides_for(x_unwrap_view.get_size()) + ) + else: + x_unwrap_view.freeze_layout() + + index_args, var_ranges = dependencies.index_vars_squeeze( + x.get_size(), prefix="r" + ) + range_vars = index_args[0] + index = x.make_indexer()(range_vars) + + index = V.graph.sizevars.simplify_with_ranges(index, var_ranges) + strides = V.graph.sizevars.stride_vars(index, range_vars) + offset = V.graph.sizevars.offset_var(index, range_vars) + expected = sympy_dot(range_vars, strides) + offset + + if index != expected: + log.debug( + "convert_to_reinterpret_view failed: stride=%s offset=%s index=%s", + strides, + offset, + index, + ) + raise NotImplementedError + + return ReinterpretView( + data=x.data, + layout=FixedLayout( + device=x.get_device_or_error(), + dtype=x.get_dtype(), + size=x.get_size(), + stride=strides, + offset=offset, + is_pinned=False, + ), + ) + + @classmethod + def realize_input(cls, x: IRNode) -> IRNode: + if x is None: + return NoneAsConstantBuffer() + if isinstance(x, (Expr, sympy.logic.boolalg.Boolean, int)): + return ShapeAsConstantBuffer(expr=x) + if isinstance(x, Constant): + # We need to unset fake mode, or else the torch.tensor() call will + # turn into a FakeTensor + with _disable_current_modes(): + return V.graph.add_tensor_constant( + torch.tensor(x.value, dtype=x.get_dtype(), device=x.get_device()) + ) + if isinstance(x, ConstantBuffer): + return x + if isinstance(x, TensorBox): + return cls.realize_input(x.data) + if isinstance(x, ReinterpretView): + return ReinterpretView( + data=cls.realize_input(x.data), layout=x.get_layout() + ) + if isinstance(x, BaseView): + x.realize() + if is_storage_and_layout(x.unwrap_view()): + try: + return cls.convert_to_reinterpret_view(x) + except NotImplementedError: + pass + if isinstance(x, StorageBox): + # TODO(jansel): impose layout preference on realized buffer + x.realize() + return x + if isinstance(x, (NonTensorObj, ShapeAsConstantBuffer, OpaqueMultiOutput)): + return x + return cls.copy_input(x) + + @classmethod + def require_stride1(cls, x: IRNode) -> IRNode: + if is_storage_and_layout(x): + if len(x.get_stride()) == 0: + return x + for stride in x.get_stride(): + if stride == 1: + return x + return cls.copy_input(x) + + @classmethod + def require_strides( + cls, + x: IRNode, + order: Sequence[int] | None = None, + exact_strides: Sequence[_IntLike] | None = None, + allow_padding: bool = False, + ) -> IRNode: + assert order is not None or exact_strides is not None + # Layout generally doesn't matter, but some consuming external ops might have requirements + if x.get_numel() in (0, 1) and not exact_strides: + return x + + # require x to have the layout + if is_storage_and_layout(x): + if isinstance(x.get_layout(), FlexibleLayout): + if order: + # If the FlexibleLayout already has the size and stride in the required order, + # freeze it to a FixedLayout by using its current size and stride. + # The behavior of using its current size and stride or the given order can be different + # if the size and stride has ambiguilty, for example for a 4D input where the iC = 1: + # size=[s0, 1, 28, 28], stride=[784, 784, 28, 1]. If the required order is [3, 0, 2, 1] (channels last), + # the current size and stride already satisfies this order. + # However by freezing it to the required order, the layout will be changed to: + # size=[s0, 1, 28, 28], stride=[784, 1, 28, 1]), which is not actually necessary. + use_current_stride_order = is_stride_order_storage_and_layout( + x, order + ) and not free_unbacked_symbols(x.get_layout().stride) + # fix flexiblelayout to be FixedLayout with stride_order + as_storage_and_layout( + x, + freeze=True, + want_contiguous=False, + stride_order=( + get_stride_order( + V.graph.sizevars.guarding_hints_or_throw( + x.get_layout().stride + ) + ) + if use_current_stride_order + else order + ), + allow_padding=allow_padding, + ) + return x + else: + # If the exact_strides is given, freeze the FlexibleLayout to a FixedLayout with the exact_strides. + as_storage_and_layout( + x, + freeze=True, + want_contiguous=False, + stride_order=None, + allow_padding=allow_padding, + exact_strides=exact_strides, + ) + return x + elif isinstance(x.get_layout(), (FixedLayout, NonOwningLayout)) and ( + (order and x.get_layout().is_stride_ordered(order)) + or ( + exact_strides + and significant_strides_equal( + exact_strides, x.get_layout().stride, x.get_size() + ) + ) + ): + return ( + try_match_insignificant_strides(x, exact_strides) + if exact_strides is not None + else x + ) + elif isinstance( + (mutation_layout := x.get_layout()), MutationLayoutSHOULDREMOVE + ): + if isinstance( + (real_layout := mutation_layout.real_layout()), FlexibleLayout + ): + raise AssertionError( + "the MutationLayoutSHOULDREMOVE's real layout shouldn't be FlexibleLayout" + ) + elif isinstance(real_layout, FixedLayout) and ( + (order and real_layout.is_stride_ordered(order)) + or ( + exact_strides + and significant_strides_equal( + exact_strides, real_layout.stride, x.get_size() + ) + ) + ): + return x + + # TODO - Storage to InputBuffer + if isinstance(x, InputBuffer) and ( + (order and x.get_layout().is_stride_ordered(order)) + or ( + exact_strides + and significant_strides_equal( + exact_strides, x.get_layout().stride, x.get_size() + ) + ) + ): + return x + if ( + isinstance(x, TensorBox) + and isinstance(x.data, BaseView) + and not isinstance(x.data, ReinterpretView) + and is_storage_and_layout(unwrap_view := x.unwrap_view()) + and hasattr(unwrap_view, "data") + and not isinstance(unwrap_view.data, ExternKernelAlloc) + ): + try: + x.data = cls.convert_to_reinterpret_view(x.data) + if order: + return cls.require_stride_order( + x, order, allow_padding=allow_padding + ) + elif exact_strides: + return cls.require_exact_strides( + x, exact_strides, allow_padding=allow_padding + ) + except NotImplementedError: + pass + + # Preserve ExpandView representation that would be lost during copy_input + # Without representation of the expand in inductor IR, in codegen we end up + # launching a grid for the full size tensor and doing redundant computation + # across expanded dims. + # TODO: could also be good to have a codegen fix to recognize overlapping elements + + expanded_dims: list[int] | None = None + orig_size = x.get_size() + if exact_strides is not None: + sizevars = V.graph.sizevars + expanded_dims = [ + i + for i in range(len(x.get_size())) + if sizevars.statically_known_equals(exact_strides[i], 0) + and sizevars.statically_known_geq(x.get_size()[i], 2) + ] + + for dim in expanded_dims: + x = torch._inductor.lowering.slice_(x, dim, 0, 1) + + # Although this is a clone, inductor is good about fusing clones into previous + # operations if they weren't realized and their layouts were flexible. + x = cls.copy_input(x) + + as_storage_and_layout( + x, + freeze=True, + want_contiguous=False, + stride_order=order, + allow_padding=allow_padding, + exact_strides=exact_strides, + ) + if order: + assert is_stride_order_storage_and_layout(x, order) + elif expanded_dims: + assert orig_size is not None and exact_strides is not None + x = torch._inductor.lowering.expand(x, orig_size) + # the expand will sometimes may change insignificant strides, so match them back + return try_match_insignificant_strides(x, exact_strides) + + return x + + @classmethod + def require_exact_strides( + cls, x: IRNode, exact_strides: Sequence[_IntLike], allow_padding: bool = False + ) -> IRNode: + return cls.require_strides( + x, + exact_strides=[ + s.node.expr if isinstance(s, torch.SymInt) else s for s in exact_strides + ], + allow_padding=allow_padding, + ) + + @classmethod + def require_stride_order( + cls, x: IRNode, order: Sequence[int], allow_padding: bool = False + ) -> IRNode: + return cls.require_strides(x, order=order, allow_padding=allow_padding) + + @classmethod + def require_channels_last(cls, x: IRNode) -> IRNode: + return cls.require_stride_order(x, NHWC_STRIDE_ORDER) + + @classmethod + def require_channels_last_3d(cls, x: IRNode) -> IRNode: + return cls.require_stride_order(x, NHWDC_STRIDE_ORDER) + + @classmethod + def require_contiguous(cls, x: IRNode) -> IRNode: + def is_mkldnn_tensor(x: IRNode) -> bool: + try: + name = x.get_name() + except (AttributeError, NotImplementedError): + return False + + return name in V.graph.constants and V.graph.constants[name].is_mkldnn + + # TODO move this to the more proper places + if is_mkldnn_tensor(x): + return x + else: + return cls.require_exact_strides( + x, FlexibleLayout.contiguous_strides(x.get_size()) + ) + + @classmethod + def require_contiguous_strides(cls, x: IRNode) -> IRNode: + # TODO: combine this with require_contiguous after + # https://github.com/pytorch/pytorch/pull/148235 lands. + return cls.require_exact_strides( + x, FlexibleLayout.contiguous_strides(x.get_size()) + ) + + def apply_constraint(self) -> None: + pass + + def fill_non_provided_args( + self, args: Sequence[Any], kwargs: dict[str, Any] + ) -> Sequence[Any]: + # Previously, we want to maintain forward-compatibility by skipping + # default args in the serialized artifacts in fbcode. However, + # some of our shim interfaces require default values being OrderedSet. + # Discussed with Sherlock offline and we decided to allow serializing + # default args into the C++ wrapper code for now. We will refine this + # part if we see real FC requirement. More details related to FC + # can be found at: + # https://docs.google.com/document/d/1FzWm-sHYwmRi3x_g036kOxd99KaYquUsA-L5JwOn8ys/edit?usp=sharing + assert isinstance(args, Sequence), type(args) + if not isinstance(args, list): + args = list(args) + assert self.arg_properties, "ExternKernel.arg_properties should not be empty" + + n_args = len(args) + n_pos_args = len(self.arg_properties) + # For cpp wrapper, if some positional args are not provided, we need to check + # if they're in the kwargs or use their default value + if n_args < n_pos_args: + log.debug( + "%s has %d unprovided positional arguments. " + "Will check if they are in the keyword arguments or will use default values.", + self.op_overload, + n_pos_args - n_args, + ) + for i in range(n_args, n_pos_args): + arg_name = self.arg_properties[i]["name"] + args.append( + kwargs[arg_name] + if arg_name in kwargs + else self.arg_properties[i]["default_value"] + ) + return args + + def codegen_const_args(self, names: list[str] | None = None) -> list[str]: + if V.graph.cpp_wrapper: + result = [] + # Aten ops follow the convention that tensor args are before non-tensor args, + # in which case the following 'len(self.inputs) + i' logic works. But this + # may not be true for other ops, and if that is the case, caller needs to + # pass in a list of const arg names for arg_properties lookup. + name_to_arg_properties = None + if names and self.arg_properties: + assert len(self.constant_args) == len(names), ( + "names passed to codegen_const_args does not match self.constant_args" + ) + name_to_arg_properties = { + arg.get("name"): arg for arg in self.arg_properties + } + + for i, x in enumerate(self.constant_args): + if name_to_arg_properties is not None: + assert names is not None + prop = name_to_arg_properties.get(names[i]) + type_ = prop.get("type") if prop else None + else: + idx = len(self.inputs) + i + type_ = ( + self.arg_properties[idx].get("type") + if self.arg_properties and idx < len(self.arg_properties) + else None + ) + result.append(V.graph.wrapper_code.val_to_arg_str(x, type_)) + return result + else: + return [V.graph.wrapper_code.val_to_arg_str(a) for a in self.constant_args] + + def codegen_args(self) -> list[str]: + if V.graph.cpp_wrapper and self.op_overload is not None: + # cpp wrapper needs special logic to fill in missing args with default values + inputs = self.fill_non_provided_args( + [*self.inputs, *self.constant_args], self.kwargs + ) + # fill_non_provided_args has handled constant args, so no need to codegen for that later + need_codegen_constant_args = False + else: + inputs = self.inputs + need_codegen_constant_args = True + + args = [] + for i, x in enumerate(inputs): + if V.graph.cpp_wrapper: + assert self.arg_properties and i < len(self.arg_properties), ( + "Invalid access to ExternKernel.arg_properties" + ) + type_ = self.arg_properties[i].get("type") + args.append(V.graph.wrapper_code.val_to_arg_str(x, type_)) + else: + args.append(V.graph.wrapper_code.val_to_arg_str(x)) + if need_codegen_constant_args: + args.extend(self.codegen_const_args()) + return args + + def get_kwargs_value(self, arg_name: str, **kwargs: Any) -> Any: + """Given an argument name, queries for values in (in order): + 1. any provided kwargs for this function. + 2. the class self.kwargs member. + 3. any available default arguments in self.allarg_properties.""" + if arg_name in kwargs: + return kwargs.get(arg_name) + if arg_name in self.kwargs: + return self.kwargs.get(arg_name) + if (arg := self.allarg_properties.get(arg_name)) is not None: + return arg.get("default_value") + raise AssertionError(f"{arg_name} not in self.allarg_properties") + + def codegen_kwargs(self, skip_out: bool = False) -> list[str]: + if V.graph.cpp_wrapper: + if self.op_overload is not None and len(self.schema_kwargs) == 0: + # All the args should have been generated by fill_non_provided_args in codegen_args + return [] + + kwargs = [] + for arg_name in self.ordered_kwargs_for_cpp_kernel: + if skip_out and arg_name == "out": + # ExternKernelOut has its own logic for inserting the out parameter + continue + + v = self.get_kwargs_value(arg_name) + if isinstance(v, Expr): + kwargs.append(v) + else: + assert self.allarg_properties is not None + type_ = self.allarg_properties.get(arg_name, {}).get("type") + kwargs.append(V.graph.wrapper_code.val_to_arg_str(v, type_)) + else: + kwargs = [ + f"{k}={V.graph.wrapper_code.val_to_arg_str(v)}" + for k, v in self.kwargs.items() + ] + return kwargs + + def get_op_name(self) -> str: + if self.fx_node is not None: + target = self.fx_node.target + op_namespace = getattr(target, "__module__", "unknown_namespace") + op_namespace = op_namespace.replace("._ops.", ".ops.") + op_namespace = op_namespace.rsplit(".", 1)[0] + op_name = f"{op_namespace}.{target}" + else: + op_name = "unknown_op" + return op_name + + def codegen_size_asserts(self, wrapper: PythonWrapperCodegen) -> None: + if config.size_asserts and not V.graph.cpp_wrapper: + # comparing strides for 0 size tensor is tricky. Ignore them for now. + if sympy_product(self.get_size()) == 0: + return + size = V.graph.wrapper_code.codegen_shape_tuple(self.get_size()) + stride = V.graph.wrapper_code.codegen_shape_tuple(self.get_stride()) + op_name = self.get_op_name() + wrapper.writeline( + f"assert_size_stride({self.get_name()}, {size}, {stride}, {op_name!r})" + ) + + def codegen_alignment_asserts(self, wrapper: PythonWrapperCodegen) -> None: + if config.alignment_asserts and not V.graph.cpp_wrapper: + name = self.get_name() + aligned = name not in V.graph.unaligned_buffers + op_name = self.get_op_name() + if aligned: + wrapper.writeline( + f"assert_alignment({name}, {GPU_ALIGN_BYTES}, {op_name!r})" + ) + else: + wrapper.writeline( + f"# buffer {name} (op: {op_name}) is assumed to be not aligned" + ) + + def codegen_memory_tracking(self, wrapper: PythonWrapperCodegen) -> None: + """ + Track outputs of fallback operators if config.test_configs.track_memory_lifecycle + """ + if not config.test_configs.track_memory_lifecycle or V.graph.cpp_wrapper: + return + + wrapper.write_memory_track_allocation_once() + name = self.get_name() + wrapper.writeline(f"track_tensor({name}, '{name}')") + + def get_group_stride(self) -> tuple[list[Sequence[Expr]], list[Expr]]: + """ + get output sizes and strides, for template_codegen + """ + _size = self.get_size() + _stride = self.get_stride() + # iter_ranges = _size of output tensor, reduce_range = [] because no reduction + return [_size, []], _stride + + def canonicalize(self) -> tuple[Expr, Sequence[Expr]]: + """ + Manually get canonicalization of the output index + """ + # manually generate index formula for conv + sizevars = V.graph.sizevars + sizes = self.get_size() + strides = self.get_stride() + # Stride hints are only used as sort keys to determine dimension + # ordering for canonicalization, not for correctness. + strides = [sizevars.optimization_hint(x) for x in strides] + # TODO: I can't tell if the symbols here are temporary + index_vars = [sympy_index_symbol(f"d{i}") for i in range(len(sizes))] + # reorder index vars according to stride + index_order = sorted(range(len(strides)), key=strides.__getitem__, reverse=True) + lookup = {pos: idx for idx, pos in enumerate(index_order)} + order = [lookup[i] for i in range(len(lookup))] + index_vars = [index_vars[i] for i in order] + indexer = self.make_indexer() + index = indexer(index_vars) + + new_sizes, reindex, _prune = V.graph.sizevars._simplify_loops( + index_vars, sizes, [index] + ) + + # assign new variables each dimension to deal with numbering mismatches + # d0, d1, d2 could become d0, d2 -- which won't match d0, d1 + _, add_var = var_builder("c") + replacement = dict(zip(index_vars, reindex([add_var(x) for x in new_sizes]))) + + index = sympy_subs(sympy.expand(index), replacement) + return index, tuple(new_sizes) + + @cache_on_self_and_args("ExternKernel") + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + # NB: It's not necessary to check regular inputs as we automatically + # have dependencies on them + maybe_get_symbols = ( + maybe_free_unbacked_symbols if unbacked_only else maybe_free_symbols + ) + r = InputsKernel.get_free_symbol_uses(self, unbacked_only) + for arg in self.constant_args: + r |= maybe_get_symbols(arg) + for arg in self.kwargs.values(): + r |= maybe_get_symbols(arg) + return r + + def __str__(self) -> str: + kernel_name = getattr(self, "python_kernel_name", None) + lines = [ + f"python_kernel_name={kernel_name!r}", + ] + lines += [ + f"{field.name}={getattr(self, field.name)}" + for field in dataclasses.fields(self) + ] + lines.append(f"origin_node={self.origin_node!r}") + return self.str_helper(lines) + + __repr__ = __str__ + + +@ir_dataclass(frozen=False) +class ExternKernelOut(ExternKernel): + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + wrapper.generate_extern_kernel_out(self) + + def __init__( + self, + layout: Layout, + inputs: Sequence[IRNode], + constant_args: Sequence[Any] = (), + kwargs: dict[str, Any] | None = None, + output_view: ReinterpretView | None = None, + python_kernel_name: str | None = None, + cpp_kernel_name: str | None = None, + ordered_kwargs_for_cpp_kernel: Sequence[Any] = (), + op_overload: _OpOverloads | None = None, + ) -> None: + unwrapped_inputs = self.unwrap_storage(inputs) + assert isinstance(unwrapped_inputs, Sequence), type(unwrapped_inputs) + super().__init__( + None, + layout, + unwrapped_inputs, + constant_args, + kwargs or {}, + None, + python_kernel_name, + cpp_kernel_name, + ordered_kwargs_for_cpp_kernel, + op_overload, + ) + self.name = V.graph.register_buffer(self) + V.graph.register_operation(self) + + def should_allocate(self) -> bool: + return True + + +class RandomSeeds(ExternKernelOut): + def __init__(self, count: int, device: torch.device) -> None: + limits = torch.iinfo(torch.int64) + super().__init__( + layout=FixedLayout( + device=device, + dtype=torch.int64, + size=[count], + ), + inputs=[], + constant_args=[limits.min, limits.max, [count]], + python_kernel_name="aten.randint.low_out", + # FIXME: Ideally we should only use at::_ops::randint_low_out::call here, + # but the signature is different from is at::randint_out. Again, + # we can simplify the code when only keeping an ABI-compatible version. + cpp_kernel_name="at::_ops::randint_low_out::call", + op_overload=aten.randint.low_out, + ) + + +class ExternKernelAlloc(ExternKernel): + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + wrapper.generate_extern_kernel_alloc(self) + + def __init__( + self, + layout: OutputSpec, + inputs: Sequence[IRNode], + constant_args: Sequence[Any] = (), + kwargs: dict[str, Any] | None = None, + python_kernel_name: str | None = None, + cpp_kernel_name: str | None = None, + ordered_kwargs_for_cpp_kernel: Sequence[Any] = (), + op_overload: _OpOverloads | None = None, + ) -> None: + unwrapped_inputs = self.unwrap_storage(inputs) + assert all(isinstance(i, IRNode) for i in unwrapped_inputs) + super().__init__( + None, + layout, + cast(Sequence[IRNode], unwrapped_inputs), + constant_args, + kwargs or {}, + None, + python_kernel_name, + cpp_kernel_name, + ordered_kwargs_for_cpp_kernel, + op_overload, + ) + # We need output buffers for generating kernel arguments in the + # abi-compatible mode, where we retrieve outputs by pass each individual + # output through the abi-compatible interface. + self.outputs: Sequence[Any] = [] + self.name = V.graph.register_buffer(self) + V.graph.register_operation(self) + + def should_allocate(self) -> bool: + return False + + def apply_constraint(self) -> None: + raise NotImplementedError + + +class MutationOutput(Buffer): + """ + An output buffer that represents the mutation of a pre-existing buffer + """ + + def __init__( + self, layout: OutputSpec, mutated_node: IRNode, mutating_node: Operation + ) -> None: + super().__init__(name=None, layout=layout) + mutated_node_name = mutated_node.get_name() + V.graph.mark_buffer_mutated(mutated_node_name) + self.mutation_names = [mutated_node_name] + self.mutating_node: Operation = mutating_node + self.name = V.graph.register_buffer(self) + + def get_defining_op(self) -> Operation: + return self.mutating_node + + def get_mutation_names(self) -> Sequence[str]: + return self.mutation_names + + def should_allocate(self) -> bool: + return False + + def get_mutation_buffers(self) -> Sequence[IRNode]: + mutation_names = self.get_mutation_names() + return [ + buf + for buf in (V.graph.try_get_buffer(name) for name in mutation_names) + if buf is not None + ] + + +class TMADescriptor(ExternKernel): + """ + An IR node representing a generic host-side TMA descriptor in the Triton API + Mostly useful for user-defined Triton kernels relying on host-side TMA; + but can, in principle, be used for Inductor's Triton templates, too. + + See TMADescriptorExperimental and TMADescriptorStable for the two implementations + (the old API and the new API) + """ + + # as TMA descriptors are immutable, + # we can dedup them by the input args + _CACHE: dict[Any, TMADescriptor] = {} + + @classmethod + def _create_impl( + cls, tensor: IRNode, tma_meta: tuple[str, tuple[Any, ...]] + ) -> TMADescriptor: + assert len(tma_meta) == 2 + if tma_meta[0] == "experimental": + return TMADescriptorExperimental(tensor, *tma_meta[1]) + else: + assert tma_meta[0] == "stable" + return TMADescriptorStable(tensor, *tma_meta[1]) + + @classmethod + def create( + cls, tensor: IRNode, tma_meta: tuple[str, tuple[Any, ...]] + ) -> TMADescriptor: + key = (id(tensor), tma_meta) + if key not in cls._CACHE: + cls._CACHE[key] = cls._create_impl(tensor, tma_meta) + return cls._CACHE[key] + + def __init__( + self, tensor: IRNode, inputs: Sequence[Any], constant_args: Sequence[Any] + ) -> None: + super().__init__( + None, + # link back to the underlying tensor in terms of ownership + # to avoid getting the underlying tensor deleted *before* + # the TMADescriptor node can be deleted. + NonOwningLayout( + ReinterpretView( + data=tensor, + layout=tensor.get_layout(), + ) + ), + cast(Sequence[Buffer], inputs), + tuple(constant_args), + None, + ) + + self.tensor = tensor + self.name = V.graph.register_buffer(self) + V.graph.register_operation(self) + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + wrapper.generate_tma_descriptor(self) + + def get_tensor(self) -> IRNode: + return self.tensor + + +class TMADescriptorExperimental(TMADescriptor): + """ + the new host-side TMA Descriptor API: + (the ones obtained via create_{1d,2d}_tma_descriptor calls). + + See also TMADescriptorStable for the new API. + """ + + def __init__( + self, + tensor: IRNode, + dims: list[int | torch.SymInt], + block_dims: list[int | torch.SymInt], + element_size: int | None = None, + ) -> None: + assert len(dims) in (1, 2) + assert len(dims) == len(block_dims) + + if element_size is None: + element_size = tensor.get_dtype().itemsize + + self.dims = dims + self.block_dims = block_dims + self.element_size = element_size + self.rank = len(self.dims) + + inputs = [tensor] + constant_args = [ + *self.dims, + *self.block_dims, + self.element_size, + ] + + super().__init__( + tensor=tensor, + inputs=inputs, + constant_args=constant_args, + ) + + +class TMADescriptorStable(TMADescriptor): + """ + the new host-side TMA descriptor API + (the ones obtained via TensorDescriptor.from_tensor). + + See also TMADescriptorExperimental for the old API. + """ + + def __init__(self, tensor: IRNode, block_shape: list[int | torch.SymInt]): + self.block_shape = block_shape + + super().__init__( + tensor=tensor, + inputs=[tensor], + constant_args=block_shape, + ) + + +class SubgraphBuffer(ExternKernel): + def __init__( + self, + layout: Layout, + input_nodes: list[Buffer], + gm: torch.fx.GraphModule, + example_inputs: list[Any], + subgraph_name: str, + config_patches: dict[str, Any] | None = None, + ): + super().__init__(None, layout, input_nodes) + self.gm = gm + self.example_inputs = example_inputs + self.name = V.graph.register_buffer(self) + V.graph.register_operation(self) + + self.subgraph = V.graph.make_subgraph(self.gm, example_inputs, subgraph_name) + + assert is_node_sequence(self.inputs) + sym_inputs = get_symbolic_inputs(self.inputs) + + for sym_inp in sym_inputs: + self.subgraph.graph_inputs[sym_inp.name] = sym_inp + self.subgraph.graph_input_names.append(sym_inp.name) + + self.sym_inputs = [sym_var.name for sym_var in sym_inputs] + + import torch._inductor.config as inductor_config + + with V.set_graph_handler(self.subgraph): + # Base config: don't autotune Triton, but allow other optimizations + base_patches = { + "max_autotune": False, + "max_autotune_gemm": False, + "max_autotune_gemm_backends": "ATEN", + } + # Merge with user config_patches (e.g., coordinate_descent_tuning) + merged_patches: dict[str, Any] = {**base_patches, **(config_patches or {})} + with inductor_config.patch(merged_patches): + self.subgraph.run(*self.example_inputs) + + # Tag all operations in subgraph with config_patches + # These will be applied during kernel codegen via SIMD scheduling + if config_patches: + for op in self.subgraph.operations: + op.set_config_patches(config_patches.copy()) + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + class CodegenGraph: + def __init__(self, graph: GraphLowering): + self.graph = graph + self.name = graph.name + + assert is_node_sequence(self.inputs) + outer_inputs = [t.codegen_reference() for t in self.inputs] + + wrapper.codegen_subgraph_with_flattened_outputs( + CodegenGraph(self.subgraph), + [*self.sym_inputs, *outer_inputs], + [self.name], + ) + + +class UserDefinedTritonKernel(ExternKernel): + """ + A user-defined triton kernel (e.g. via @triton.jit). + """ + + def get_kernel_and_metadata(self) -> tuple[Kernel, Any, list[str], list[str]]: + from triton.runtime.autotuner import Autotuner + + from torch._higher_order_ops.triton_kernel_wrap import kernel_side_table + + kernel = kernel_side_table.get_kernel(self.kernel_idx) + configs = [] + restore_value_args: list[str] = [] + reset_to_zero_args: list[str] = [] + if isinstance(kernel, Autotuner): + # https://github.com/triton-lang/triton/pull/5083 + # changes kernel.restore_idx to kernel.restore_value + if hasattr(kernel, "restore_idx"): + restore_value_args.extend( + kernel.fn.arg_names[i] for i in kernel.restore_idx + ) + else: + assert hasattr(kernel, "restore_value") + restore_value_args.extend(kernel.restore_value) + + if hasattr(kernel, "reset_idx"): + for i in kernel.reset_idx: + reset_to_zero_args.append(kernel.fn.arg_names[i]) + else: + assert hasattr(kernel, "reset_to_zero") + reset_to_zero_args.extend(kernel.reset_to_zero) + + configs = kernel.configs + kernel = kernel.fn + + return kernel, configs, restore_value_args, reset_to_zero_args + + def can_fuse_epilogue(self) -> bool: + """ + For kernels like + + @triton.jit + def add_kernel(in_ptr0, in_ptr1, out_ptr, n_elements, BLOCK_SIZE: tl.constexpr): + pid = tl.program_id(0) + offs = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) + mask = offs < n_elements + x = tl.load(in_ptr0 + offs, mask=mask) + y = tl.load(in_ptr1 + offs, mask=mask) + tl.store(out_ptr + offs, x + y, mask=mask) + + @torch.compile + def fn(a, b): + out = torch.empty_like(a) + grid = (triton.cdiv(a.numel(), 1024),) + add_kernel[grid](a, b, out, a.numel(), BLOCK_SIZE=1024) + return out.relu() + + We can potentially fuse the relu epilogue into the add_kernel. + We do this by pruning the `out` tensor allocation and directly writing the relu-output. + """ + + if not config.epilogue_fusion_user_defined_triton_kernel: + return False + + if not self.arg_accesses.can_fuse_epilogue: + return False + + # We achieve fusion by parsing the original src into a python AST, + # then identify the expr containing the original value written via tl.store(). + # We generate an expr for the value after the epilogue and replace that into the tl.store. + # So far we only support the simple case where there is a single tl.store in the kernel. + if len(self.kernel_stores.stores) != 1: + return False + + # Only fuse if the mutated arg is originally an "empty" tensor. + # This is because we don't know exactly which element of that tensor is being written to. + # If the kernel only writes to a subset of the tensor, then we only apply the epilogue to that subset. + # In these edge cases, our fusion is only correct if the original tensor is empty, + # where the semantics is that content values are UB, and we can rely on the fact that `epilogue(UB) == UB`. + assert len(self.mutable_args) == 1 + if not isinstance(self.mutable_args[0], TensorBox): + return False + if not isinstance(self.mutable_args[0].data, StorageBox): + return False + if not isinstance(self.mutable_args[0].data.data, ComputedBuffer): + return False + if not isinstance(self.mutable_args[0].data.data.data, Pointwise): + return False + if not all(r == 0 for r in self.mutable_args[0].data.data.data.ranges): + return False + + return True + + @override + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + return self._codegen(wrapper, epilogue_fusion=None) + + def codegen_with_epilogue_fusion( + self, wrapper: PythonWrapperCodegen, epilogue_fusion: tuple[ComputedBuffer, str] + ) -> None: + """ + epilogue_fusion: (fused epilogue node, modified kerel src code) + """ + return self._codegen(wrapper, epilogue_fusion) + + def _codegen( + self, + wrapper: PythonWrapperCodegen, + epilogue_fusion: tuple[ComputedBuffer, str] | None, + ) -> None: + """Overrides the parent member. + See https://github.com/pytorch/pytorch/issues/151692""" + + from torch._inductor.utils import triton_version_uses_attrs_dict + + ( + kernel, + configs, + restore_value_args, + reset_to_zero_args, + ) = self.get_kernel_and_metadata() + + # Definition of kernel + ( + new_name, + triton_meta, + inductor_meta, + extra_launch_args, + ) = wrapper.define_user_defined_triton_kernel( + kernel, + configs, + self.kwargs, + restore_value_args, + reset_to_zero_args, + self.grid, + epilogue_fusion, + ) + named_args = { + k: self.get_kwargs_value(k) for k in self.ordered_kwargs_for_cpp_kernel + } + + if epilogue_fusion: + assert len(self.arg_accesses.read_writes.writes) == 1 + mutable_arg_name = next(iter(self.arg_accesses.read_writes.writes)).name + assert mutable_arg_name in named_args + epilogue_computed_buffer, _ = epilogue_fusion + named_args[mutable_arg_name] = epilogue_computed_buffer + + arg_names = [p.name for p in kernel.params] # type: ignore[attr-defined] + constexprs = [p.num for p in kernel.params if p.is_constexpr] # type: ignore[attr-defined] + constexpr_names = OrderedSet(arg_names[i] for i in constexprs) + + args: list[Any] = [] + arg_types: list[Any] = [] + raw_keys_filtered: list[Any] = [] + raw_args_filtered: list[Any] = [] + for name, arg in itertools.chain( + named_args.items(), zip(itertools.repeat(""), extra_launch_args) + ): + if name in constexpr_names and triton_version_uses_attrs_dict(): + # see #160000 - we don't pass in constexpr args to speed up runtime. + continue + raw_keys_filtered.append(name) + raw_args_filtered.append(arg) + if isinstance(arg, IRNode): + args.append(arg.codegen_reference()) + arg_types.append(arg.get_dtype()) + elif isinstance(arg, (int, float, bool, sympy.Expr)): + args.append(arg) + arg_types.append(type(arg)) + elif name in constexpr_names: + # insert a dummy value for constexpr args of unsupported type + # constexprs will end up getting baked into the kernel at compile time + args.append(-1) + arg_types.append(int) + elif arg is None: + """ + Filter out None args. + + see https://github.com/pytorch/pytorch/issues/115344 + + Two cases for a None arg: + 1. The arg is already tl.constexpr, so leave it in + 2. The arg is not tl.constexpr so we have to remove it + """ + if triton_version_uses_attrs_dict(): + args.append(-1) + arg_types.append(int) + else: + raw_keys_filtered.pop() + raw_args_filtered.pop() + else: + raise NotImplementedError(f"Unsupported arg type: {type(arg)}: {arg}") + + self.codegen_comment(wrapper, new_name) + wrapper.generate_kernel_call( + new_name, + args, + arg_types=arg_types, + raw_args=raw_args_filtered, + raw_keys=raw_keys_filtered, + triton_meta=triton_meta, + inductor_meta=inductor_meta, + triton=True, + device=self.get_device(), + original_fxnode_name=self.fx_node.name, + ) + + @cache_on_self_and_args("UserDefinedTritonKernel") + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + # add unbacked symbols used in the grid to the ones used + # in the kwargs (the latter is generated by ExternKernel) + return super().get_free_symbol_uses(unbacked_only) | get_free_symbols( + self.grid, unbacked_only + ) + + def get_unbacked_symbol_defs(self) -> OrderedSet[sympy.Symbol]: + return OrderedSet() + + def __init__( + self, + *, + kernel_idx: int, + grid: Any, + tma_descriptor_metadata: dict[str, Any], + kernel_args: dict[str, Any], + ) -> None: + inputs: list[IRNode] = [] + kwargs: dict[str, IRNode] = {} + constant_args: list[IRNode] = [] + + for k, v in kernel_args.items(): + if isinstance(v, TensorBox): + t = InputsKernel.unwrap_storage_for_input(self.realize_input(v)) + if k in tma_descriptor_metadata: + t = TMADescriptor.create(t, tma_descriptor_metadata[k]) + inputs.append(t) + kwargs[k] = t + else: + constant_args.append(v) + kwargs[k] = v + + assert len(inputs) != 0 + self.device = inputs[0].get_device() + + assert isinstance(inputs, Sequence), type(inputs) + super().__init__( + None, + NoneLayout(device=self.device), + inputs, + tuple(constant_args), + kwargs, + ) + self.kernel_idx = kernel_idx + self.grid = grid + + kernel, configs, _, _ = self.get_kernel_and_metadata() + + # If we are autotuning, not all arguments will be passed + assert hasattr(kernel, "arg_names") + self.ordered_kwargs_for_cpp_kernel = [ + arg for arg in kernel.arg_names if arg in kernel_args + ] + + from torch._higher_order_ops.triton_kernel_wrap import ( + identify_accessed_tensors, + identify_triton_stores, + ) + + autotuned_kwargs = configs[0].kwargs if len(configs) > 0 else {} + + import ast + + # pyrefly: ignore [missing-attribute] + self.kernel_src = kernel.src + self.kernel_ast = ast.parse(self.kernel_src) + self.kernel_stores = identify_triton_stores(self.kernel_src) + self.kernel_args = kernel_args + # names in `arg_accesses.read_writes` are names of formal arguments in the kernel's prototype + self.arg_accesses = identify_accessed_tensors( + kernel, + {**kernel_args, **autotuned_kwargs}, + tma_descriptor_metadata, + ) + + # Filter to only tensor args: with Triton 3.7+, ordered_arg_names + # includes scalars, so writes may reference non-tensor args like SymInts. + self.mutable_args = [ + kernel_args[key.name] + for key in self.arg_accesses.read_writes.writes + if isinstance(kernel_args.get(key.name), TensorBox) + ] + + self.mutation_outputs = [ + MutationOutput(NoneLayout(device=self.device), buf, self) + for buf in self.mutable_args + ] + V.graph.register_operation(self) + + @override + def get_read_writes(self) -> dependencies.ReadWrites: + # Limit the new `get_read_writes` to `epilogue_fusion_user_defined_triton_kernel` + # to avoid potential regression to existing models. + if not config.epilogue_fusion_user_defined_triton_kernel: + return super().get_read_writes() + + # maps formal arg name to actual arg name + read_renames = { + formal_arg_dep.name: self.kernel_args[formal_arg_dep.name].get_name() + for formal_arg_dep in self.arg_accesses.read_writes.reads + } + + formal_arg_writes = list(self.arg_accesses.read_writes.writes) + write_renames = { + formal_arg_dep.name: mut_output.get_name() + for formal_arg_dep, mut_output in zip( + formal_arg_writes, self.mutation_outputs + ) + } + + read_writes = dependencies.ReadWrites( + reads=OrderedSet( + [ + dep.rename(read_renames) + for dep in self.arg_accesses.read_writes.reads + ] + ), + writes=OrderedSet( + [ + dep.rename(write_renames) + for dep in self.arg_accesses.read_writes.writes + ] + ), + index_exprs=OrderedSet(), + ) + return read_writes + + def get_outputs(self) -> list[Buffer]: + return list(self.mutation_outputs) + + def get_device(self) -> torch.device | None: + return self.device + + +class InplaceBernoulliFallback(ExternKernel): + """ + This needs to be a custom class to handle mutation properly + """ + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + assert all(isinstance(t, IRNode) for t in self.inputs) + (x,) = (cast(IRNode, t).codegen_reference() for t in self.inputs) + + if V.graph.cpp_wrapper: + # Inductor doesn't really support aten Generator, so the Generator kwarg is always NULL here, + # which needs to be explicitly generated for cpp wrapper + wrapper.writeline( + f"{self.get_kernel_name()}({x}, {', '.join(map(repr, self.constant_args))}, NULL){wrapper.ending}" + ) + else: + wrapper.writeline( + f"{self.get_kernel_name()}({x}, {', '.join(map(repr, self.constant_args))}){wrapper.ending}" + ) + + def should_allocate(self) -> bool: + return False + + def get_mutation_names(self) -> Sequence[str]: + return [self.input_name(0)] + + def get_unbacked_symbol_defs(self) -> OrderedSet[sympy.Symbol]: + return OrderedSet() + + def __init__( + self, op_overload: _OpOverloads, x: IRNode, *constant_args: Any + ) -> None: + super().__init__( + None, + NoneLayout(device=x.get_device()), + self.unwrap_storage([x]), + constant_args, + op_overload=op_overload, + ) + V.graph.mark_buffer_mutated(x.get_name()) + self.name = V.graph.register_buffer(self) + V.graph.register_operation(self) + + +# Used to deal with torch.complex types +class InplaceCopyFallback(ExternKernel): + """ + This needs to be a custom class to handle mutation properly + """ + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + (dst, src, non_blocking) = self.codegen_args() + wrapper.codegen_device_copy(src, dst, non_blocking) + + def should_allocate(self) -> bool: + return False + + def get_mutation_names(self) -> Sequence[str]: + return [self.input_name(0)] + + def get_unbacked_symbol_defs(self) -> OrderedSet[sympy.Symbol]: + return OrderedSet() + + def __init__( + self, + layout: OutputSpec, + inputs: Sequence[IRNode], + constant_args: Sequence[Any], + ) -> None: + super().__init__( + None, + layout, + inputs, + constant_args, + python_kernel_name="aten.copy_", + cpp_kernel_name="aoti_torch_copy_", + ) + V.graph.mark_buffer_mutated(inputs[0].get_name()) + self.name = V.graph.register_buffer(self) + V.graph.register_operation(self) + + @classmethod + def create( + cls, dst: IRNode, src: IRNode, non_blocking: bool = False + ) -> InplaceCopyFallback: + inputs = [cls.realize_input(t) for t in [dst, src]] + constant_args = (non_blocking,) + result = InplaceCopyFallback( + NoneLayout(device=dst.get_device()), + inputs, + constant_args, + ) + return result + + +class MutatingFirstArgExternKernel(ExternKernel): + """ + This needs to be a custom class to handle mutation properly + """ + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + assert is_node_sequence(self.inputs) + argrefs = [ + *(t.codegen_reference() for t in self.inputs), + *map(repr, self.constant_args), + ] + wrapper.writeline( + f"{self.get_kernel_name()}({', '.join(argrefs)}){wrapper.ending}" + ) + + def should_allocate(self) -> bool: + return False + + def get_mutation_names(self) -> Sequence[str]: + return [self.input_name(0)] + + def get_unbacked_symbol_defs(self) -> OrderedSet[sympy.Symbol]: + return OrderedSet() + + def has_side_effects(self) -> bool: + return True + + +class ResizeStorageBytes(MutatingFirstArgExternKernel): + def __init__(self, variable: IRNode, new_size: int) -> None: + assert isinstance(new_size, int), "TODO: dynamic shapes" + super().__init__( + None, + NoneLayout(device=variable.get_device()), + self.unwrap_storage([variable]), + constant_args=(new_size,), + ) + V.graph.mark_buffer_mutated(variable.get_name()) + self.name = V.graph.register_buffer(self) + V.graph.register_operation(self) + self.python_kernel_name = "inductor_ops.resize_storage_bytes_" + self.cpp_kernel_name = "torch::inductor::resize_storage_bytes_" + assert isinstance(variable, (BaseView, StorageBox, TensorBox)), type(variable) + V.graph.never_reuse_buffers.add(variable.data.get_name()) + + +class SetSourceTensorKernel(ExternKernelAlloc): + def __init__(self, self_tensor: IRNode, storage_tensor: IRNode) -> None: + storage_tensor.freeze_layout() + super().__init__( + storage_tensor.get_layout(), + [self_tensor, storage_tensor], + python_kernel_name="torch.ops.aten.set_.source_Tensor", + op_overload=torch.ops.aten.set_.source_Tensor, + ) + assert isinstance(self_tensor, (BaseView, StorageBox, TensorBox)), type( + self_tensor + ) + V.graph.never_reuse_buffers.add(self_tensor.data.get_name()) + V.graph.never_reuse_buffers.add(storage_tensor.get_name()) + V.graph.never_reuse_buffers.add(self.get_name()) + device = storage_tensor.get_device() + self.mutation_outputs = [ + MutationOutput(NoneLayout(device=device), self_tensor, self), + MutationOutput(NoneLayout(device=device), storage_tensor, self), + ] + + def get_inputs_that_alias_output(self) -> Sequence[str]: + return [self.input_name(0), self.input_name(1)] + + +class ScatterFallback(ExternKernel): + """ + This needs to be a custom class to handle mutation properly. + This class handles both aten.scatter_ and aten.scatter_reduce_. + It also handle the case `src` being a scalar properly. + """ + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + wrapper.generate_scatter_fallback(self) + + def should_allocate(self) -> bool: + return False + + def get_mutation_names(self) -> list[str]: + inp = self.inputs[0] + assert isinstance(inp, IRNode) + return [inp.get_name()] + + def get_unbacked_symbol_defs(self) -> OrderedSet[sympy.Symbol]: + return OrderedSet() + + def __init__( + self, + op_overload: _OpOverloads, + x: IRNode, + dim: int, + index: IRNode, + src: IRNode, + *, + reduce: str | None = None, + include_self: bool = True, + ) -> None: + self.src_is_tensor = isinstance(src, TensorBox) + + constant_args: tuple[Any, ...] + if self.src_is_tensor: + tensors = [self.realize_input(t) for t in [x, index, src]] + constant_args = (dim,) + else: + tensors = [self.realize_input(t) for t in [x, index]] + constant_args = (dim, src) + + super().__init__( + None, + NoneLayout(device=x.get_device()), + self.unwrap_storage(tensors), + constant_args, + {"reduce": reduce, "include_self": include_self}, + python_kernel_name=str(op_overload), + ordered_kwargs_for_cpp_kernel=["reduce", "include_self"], + op_overload=op_overload, + ) + V.graph.mark_buffer_mutated(x.get_name()) + self.name = V.graph.register_buffer(self) + V.graph.register_operation(self) + + +class IndexPutFallback(ExternKernel): + """ + This needs to be a custom class to handle mutation and indices properly + """ + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + wrapper.generate_index_put_fallback(self) + + def should_allocate(self) -> bool: + return False + + def get_mutation_names(self) -> Sequence[str]: + return [self.input_name(0)] + + def get_unbacked_symbol_defs(self) -> OrderedSet[sympy.Symbol]: + return OrderedSet() + + def __init__( + self, + op_overload: torch._ops.OpOverload, + x: IRNode, + indices: list[Any], + values: Sequence[Any], + accumulate: Any, + ) -> None: + self.indices = indices + valid_indices = [i for i in indices if i is not None] + # pyrefly: ignore [bad-argument-type] + tensors = [self.realize_input(x) for x in [x, values, *valid_indices]] + cpp_kernel_name = "aoti_torch_index_put_out" + super().__init__( + None, + NoneLayout(device=x.get_device()), + self.unwrap_storage(tensors), + (accumulate,), + python_kernel_name="aten.index_put_", + cpp_kernel_name=cpp_kernel_name, + op_overload=op_overload, + ) + V.graph.mark_buffer_mutated(self.input_name(0)) + self.name = V.graph.register_buffer(self) + V.graph.register_operation(self) + + +class DeviceCopy(ExternKernelOut): + @classmethod + def create(cls, x: IRNode, device: torch.device, non_blocking: bool) -> IRNode: + x_device = x.get_device() + assert x_device is not None + if ( + not x.is_extern() + # Can not apply this optimization if x has been mutated + and try_get_name(x) not in V.graph.mutated_buffers + and all(r in V.graph.constants for r in x.get_read_names()) + and not config.aot_inductor.use_runtime_constant_folding + ): + if V.graph.cpp_wrapper: + # Even if x is promoted to be a device constant, we still need to + # register device info to construct the correct CppWrapper class later + V.graph.add_device_info(device) + V.graph.add_device_info(x_device) + return x.constant_to_device(device) + + V.graph.add_device_info(device) + V.graph.add_device_info(x_device) + developer_warning("DeviceCopy in input program") + constant_args = (non_blocking,) + # Device Copy should keep the same layout as input + x = ExternKernel.require_contiguous(x) + stride = None + if x.get_size(): + # x.get_stride() may be unimplemented if x's size is empty + stride = x.get_stride() + is_destination_pinned = ( + is_gpu(x_device.type) and device.type == "cpu" and non_blocking + ) + is_source_pinned = ( + x_device.type == "cpu" and is_gpu(device.type) and non_blocking + ) + if is_source_pinned and is_storage_and_layout(x): + x.get_layout().is_pinned = True + return DeviceCopy( + FixedLayout( + device, + x.get_dtype(), + x.get_size(), + stride, + is_pinned=is_destination_pinned, + ), + [cls.realize_input(x)], + constant_args, + ) + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + args = self.codegen_args() + assert len(args) == 2 + if self.output_view: + wrapper.codegen_device_copy( + args[0], self.output_view.codegen_reference(), args[1] + ) + else: + wrapper.codegen_device_copy(args[0], self.codegen_reference(), args[1]) + + +class DynamicSelectStorageOffset(ExternKernel): + """ + The result of computing a dynamic selection index is determined as follows: when the index in the + select operation is unbacked, the actual index calculation is ambiguous for negative indices + (index + size) versus non-negative indices (just index). To resolve this, we allocate an unbacked + SymInt to represent the storage offset and decompose the select operation into a call to as_strided, + computing the storage offset at runtime with this node. + """ + + def get_reads(self) -> OrderedSet[Dep]: + return OrderedSet() + + def should_allocate(self) -> bool: + return False + + def __init__( + self, + unbacked_offset_symbol: sympy.Symbol, + index: sympy.Symbol, + base_offset: sympy.Symbol | int, + base_dim_stride: sympy.Symbol | int, + size: sympy.Symbol | int, + clamp: bool, + ) -> None: + super().__init__(None, NoneLayout(device=torch.device("cpu")), []) + # This node codegen the following: + # unbacked_offset_symbol = base_offset + base_dim_stride * (index if index >=0 else index + size) + self.unbacked_offset_symbol = unbacked_offset_symbol + self.index = index + self.base_offset = base_offset + self.base_dim_stride = base_dim_stride + self.size = size + self.clamp = clamp + + def get_unbacked_symbol_defs(self) -> OrderedSet[sympy.Symbol]: + return OrderedSet([self.unbacked_offset_symbol]) + + @cache_on_self_and_args("DynamicSelectStorageOffset") + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + return get_free_symbols(self.index, unbacked_only) + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + wrapper.codegen_dynamic_select_index(self, clamp=self.clamp) + + +class DynamicSliceSize(ExternKernel): + """ + Computes the output size of a slice call, handling the correct semantics in codegen. + We do this for flexible handling for unbacked indices (to not data-dependent error). + + Slicing has 4 semantics for indices, i.e. x[start:] could be: + 1) start < -x.size(0) -> x[0:] # negative out-of-bounds + 2) start in [-x.size(0), 0) -> x[x.size(0) + start:] # negative slicing + 3) start in [0, x.size(0)) -> x[start:] # standard slicing + 4) start >= x.size(0) -> empty slice # positive out-of-bounds + + If the appropriate semantics are known beforehand, the output size is computed based on + the start & end indices. If not (with unbacked indices), a new unbacked symbol is created + to represent the output size, and codegen handles computing the correct case. + """ + + def get_reads(self) -> OrderedSet[Dep]: + return OrderedSet() + + def should_allocate(self) -> bool: + return False + + def __init__( + self, + unbacked_size_symbol: sympy.Symbol, + start: sympy.Symbol | int, + end: sympy.Symbol | int, + step: sympy.Symbol | int, + size: sympy.Symbol | int, + ): + super().__init__(None, NoneLayout(device=torch.device("cpu")), []) + # This node codegen + self.unbacked_size_symbol = unbacked_size_symbol + self.start = start + self.end = end + self.step = step + self.size = size + + def get_unbacked_symbol_defs(self) -> OrderedSet[sympy.Symbol]: + return OrderedSet([self.unbacked_size_symbol]) + + @cache_on_self_and_args("DynamicSliceSize") + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + return get_free_symbols(self.start, unbacked_only).union( + get_free_symbols(self.end, unbacked_only) + ) + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + wrapper.codegen_dynamic_slice_size(self) + + +class DynamicScalar(ExternKernel): + """ + The result of a call to aten._local_scalar_dense. + """ + + def get_reads(self) -> OrderedSet[Dep]: + return OrderedSet() + + def should_allocate(self) -> bool: + return False + + def __init__( + self, sym: sympy.Symbol, keypath: pytree.KeyPath, data: IRNode + ) -> None: + data.realize() + super().__init__( + None, NoneLayout(device=torch.device("cpu")), self.unwrap_storage([data]) + ) + self.sym = sym + self.keypath = keypath + + def get_unbacked_symbol_defs(self) -> OrderedSet[sympy.Symbol]: + return OrderedSet([self.sym]) + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + wrapper.codegen_dynamic_scalar(self) + + +class AssertScalar(ExternKernel): + """ + The result of a call to aten._assert_scalar + """ + + def get_reads(self) -> OrderedSet[Dep]: + return OrderedSet() + + def should_allocate(self) -> bool: + return False + + def __init__(self, scalar: SympyBoolean, msg: str) -> None: + super().__init__( + # Buffer(name, layotu) + None, + NoneLayout(device=torch.device("cpu")), + # InputsKernel(inputs) + [], + ) + self.scalar = scalar + self.msg = msg + + def has_side_effects(self) -> bool: + return True + + @cache_on_self_and_args("AssertScalar") + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + return get_free_symbols(self.scalar, unbacked_only) + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + if not config.scalar_asserts: + return + # NB: It is EXTREMELY important not to simplify the scalar under assertion here, + # because simplify is done with respect to runtime asserts. So if you have + # "u0 == 0" in the runtime asserts, if you subsequently try to + # simplify(u0 == 0), you will get True (because we've already runtime assert'ed + # that it's true). But we're code generating the actual runtime assert here!! + symbol = next(iter(self.get_free_symbol_uses(unbacked_only=False))) + if V.graph.fx_wrapper: + # TODO fix + pass + elif V.graph.cpp_wrapper: + symbol_str = f"std::to_string({symbol})" + sizevar = V.graph.wrapper_code.codegen_cpp_sizevar( + self.scalar, simplify=False + ) + # TODO: when we start compiling in C++20, annotate with [[unlikely]]. + wrapper.writeline( + f'if (!({sizevar})) {{ throw std::runtime_error("Expected {self.msg} but received " + {symbol_str}); }}' + ) + else: + sizevar = V.graph.wrapper_code.codegen_python_sizevar( + self.scalar, simplify=False + ) + wrapper.writeline(f"if not ({sizevar}):") + wrapper.writeline(f" raise RuntimeError({repr(self.msg)})") + # No one should ever use this buffer, but for uniformity + # define the variable and assign it None + wrapper.writeline(f"{self.get_name()} = None") + + +@ir_dataclass(frozen=False) +class ExternKernelNode: + name: str + node: export_schema.Node + + +class FallbackKernel(ExternKernelAlloc): + """ + A class that represents a fallback kernel for handling operators that are not + directly support by inductor. It currently supports functional ops, view ops, + inplace aten ops, and mutating ops that are auto-functionalizable. + """ + + def __init__( + self, + layout: OutputSpec, + kernel: _OpOverloads, + tensor_args: Sequence[IRNode], + nontensor_args: Sequence[Any], + unflatten_args: Callable[..., Any], + kwargs: dict[str, Any] | None = None, + *, + unbacked_bindings: dict[sympy.Symbol, pytree.KeyPath] | None = None, + ) -> None: + super().__init__( + layout, + tuple(tensor_args), + tuple(nontensor_args), + op_overload=kernel, + ) + + self.use_runtime_dispatch = False + self.unbacked_bindings = unbacked_bindings or {} + + assert isinstance( + kernel, (torch._ops.OpOverload, torch._ops.HigherOrderOperator) + ), f"Fails to create FallbackKernel for {kernel}: {type(kernel)} not supported" + self.op_overload = kernel + self.unflatten_args = unflatten_args + self.kwargs = {} if kwargs is None else kwargs + assert self.python_kernel_name is not None + V.graph.warn_fallback(self.python_kernel_name) + + # args that are aliased + self.alias_names: list[str] = [] + # args that are mutated AND returned from the op + self.mutation_names: list[str] = [] + + if isinstance(self.op_overload, torch._ops.HigherOrderOperator): + # We assume here that HOPs with FallbackKernel are functional. + # This may not always be true! HOPs must individually opt-in to + # FallbackKernel, so please check this if you opt-in. + return + + if "_c10d_functional" in self.op_overload.name(): + # _c10d_functional kernels are lowered into _CollectiveKernel which + # derives from FallbackKernel for the cpp codegen. The kernels + # don't pass the can_auto_functionalize check, but their mutation + # is handled properly by _CollectiveKernel. + return + + schema = self.op_overload._schema + + # NOTE: [FallbackKernel supported operators] + # We only support three types of operators: + # - functional ops + # - view ops + # - inplace aten ops + # - mutating ops that are auto-functionalizable. That is, + # the operator may mutate any number of inputs, but its outputs + # may not alias any of the inputs. + # + # The unsupported cases usually do not show up here (because + # AOTAutograd functionalized them away); the only way for an in-place + # op to show up here is if a lowering or pass introduced it. + if torch._library.utils.mutates_and_returns_first_arg(self.op_overload): + self.mutation_names.append(tensor_args[0].get_name()) + return + + def has_functionalize_impl(op: torch._ops.OpOverload) -> bool: + return torch._C._dispatch_has_kernel_for_dispatch_key( + op.name(), torch._C.DispatchKey.Functionalize + ) or ( + hasattr(op, "py_kernels") + and torch._C.DispatchKey.Functionalize in op.py_kernels + ) + + if ( + schema.is_mutable + and not can_auto_functionalize(self.op_overload) + and not has_functionalize_impl(self.op_overload) + ): + raise NotImplementedError( + f"NYI: Can't generate FallbackKernel for {self.op_overload}" + ) + + args, kwargs = self.unflatten_args(self.inputs, self.constant_args) + + def handle_aliasing_and_mutation(info: torch._C.Argument, arg: Any) -> None: + # Assertions to make sure we didn't mismatch args + if isinstance(info.type, torch.ListType): + assert isinstance(arg, (list, tuple)), type(arg) + if library_utils.is_tensor_like_type(info.type): + # PyTorch also accepts None and scalar types for args marked as "Tensor". + # We're not going to check all of them here. + assert not isinstance(arg, (tuple, list)) + + if arg is None: + return + if info.alias_info is None: + return + + def add_alias(t: IRNode) -> None: + self.alias_names.append(t.get_name()) + assert info.alias_info is not None + if info.alias_info.is_write: + self.mutation_outputs.append( + MutationOutput(NoneLayout(device=t.get_device()), t, self) + ) + + if library_utils.is_tensorlist_like_type(info.type): + if arg is not None: + for optional_tensor_arg in arg: + add_alias(optional_tensor_arg) + else: + assert library_utils.is_tensor_like_type(info.type) + + add_alias(arg) + + for info, arg in torch._library.utils.zip_schema(schema, args, kwargs): + handle_aliasing_and_mutation(info, arg) + + def get_read_writes(self) -> dependencies.ReadWrites: + read_writes = super().get_read_writes() + + if self.op_overload is torch._prims.rng_prims.graphsafe_run_with_rng_state: + for arg in self.constant_args: + if isinstance(arg, GeneratorState): + read_writes = read_writes.with_read( + dependencies.StarDep(arg.get_name()) + ) + + return read_writes + + def codegen_unbacked_symbol_defs(self, wrapper: PythonWrapperCodegen) -> None: + return wrapper.codegen_unbacked_symbol_defs_for_outputs( + self.get_name(), self.outputs, getattr(self, "unbacked_bindings", None) + ) + + def get_unbacked_symbol_defs(self) -> OrderedSet[sympy.Symbol]: + if unbacked_bindings := getattr(self, "unbacked_bindings", None): + resolved = resolve_unbacked_bindings( + V.graph.sizevars.shape_env, unbacked_bindings + ) + assert resolved is not None + return OrderedSet(resolved.keys()) + else: + return OrderedSet() + + def codegen_args(self) -> list[str]: + @dataclasses.dataclass + class Shim: + ref: Any + + def __repr__(self) -> str: + return self.ref + + assert is_node_sequence(self.inputs) + tensor_args = [Shim(x.codegen_reference()) for x in self.inputs] + args, kwargs = self.unflatten_args(tensor_args, self.constant_args) + if V.graph.cpp_wrapper and isinstance(self.op_overload, torch._ops.OpOverload): + args = self.fill_non_provided_args(args, kwargs) + args = [ + V.graph.wrapper_code.val_to_arg_str(x, param.real_type) + for param, x in zip(self.op_overload._schema.arguments, args) + ] + else: + args = [V.graph.wrapper_code.val_to_arg_str(x) for x in args] + + # let self.codegen_kwargs handle kwargs + self.kwargs.update(kwargs) + return args + + @staticmethod + def find_device( + tensor_args: Sequence[torch.Tensor] | None, example_output: Sequence[Any] + ) -> Any: + non_torch_bind_tensor_args = ( + [t for t in tensor_args if not isinstance(t, TorchBindObject)] + if tensor_args + else None + ) + if non_torch_bind_tensor_args: + assert tensor_args + devices = [arg.get_device() for arg in tensor_args if arg.get_device()] + return devices[0] + if isinstance(example_output, torch.Tensor): + return example_output.device + if isinstance( + example_output, (torch._C.ScriptObject, FakeScriptObject) + ) or is_opaque_value(example_output): + return torch.device("cpu") + if isinstance(example_output, (list, tuple)): + device_set = OrderedSet( + # pyrefly: ignore [bad-argument-type] + FallbackKernel.find_device(None, x) + for x in example_output + ) + # Remove None + devices = [device for device in device_set if device] + if len(devices) == 1: + return devices[0] + if not devices: + return None + for device in devices: + assert isinstance(device, torch.device) + if is_gpu(device.type): + return device + return devices[0] + return None + + def has_side_effects(self) -> bool: + from torch._library.utils import is_impure + + # Note: We don't pass args/kwargs here because they're IRNodes, not actual values + # The check is done on the op_overload itself + return is_impure(self.op_overload) # pyrefly: ignore[bad-argument-type] + + def get_inputs_that_alias_output(self) -> Sequence[str]: + assert isinstance( + self.op_overload, (torch._ops.OpOverload, torch._ops.HigherOrderOperator) + ), ( + f"Fails to create FallbackKernel for {self.op_overload}: " + f"{type(self.op_overload)} not supported" + ) + + # See [Note: FallbackKernel supported operators]: for a mutating + # op that is auto-functionalizable, its outputs does NOT + # alias any of the inputs. + if ( + not isinstance(self.op_overload, torch._ops.HigherOrderOperator) + and "_c10d_functional" not in self.op_overload.name() + and self.op_overload._schema.is_mutable + and can_auto_functionalize(self.op_overload) + ): + return [] + else: + return self.alias_names + + def get_mutation_names(self) -> Sequence[str]: + assert len(self.mutation_names) <= 1 + return self.mutation_names + + def export_extern_kernel_node(self): # type: ignore[no-untyped-def] + """ + ProxyExecutor Design Note + We export the ExternFallbackNodes (for custom ops) into a serialized file + and run it with a host side proxy executor to address the ABI problem + This is currently only implemented for fbcode. Eventually, we will also make this work for OSS. + Detailed design doc can be found at + https://docs.google.com/document/d/1wC4DOZFaYym2t1Esz0X5yxlLI3RDnSiyRbUus3bkJ64/edit?usp=sharing + """ + log.debug( + "Extern kernel node added for node %s with target %s.", + self.get_name(), + self.op_overload, + ) + + assert isinstance(self, FallbackKernel), type(self) + args, kwargs = self.unflatten_args(self.inputs, self.constant_args) + args = self.fill_non_provided_args(args, kwargs) + ordered_kwargs = [ + self.get_kwargs_value(key, **kwargs) + for key in self.ordered_kwargs_for_cpp_kernel + ] + target = self.op_overload + + if not V.graph.aot_mode: + # No need to serialize in the cpp wrapper JIT mode + return [*args, *ordered_kwargs] + + serializer = GraphModuleSerializer(None, []) # type: ignore[arg-type] + named_arguments = serializer.serialize_inputs(target, args, kwargs) + + # serialize_outputs + def handle_single_output( + return_type: torch.TensorType | torch.ListType | torch.JitType, + output: IRNode | Sequence[IRNode], + ) -> export_schema.Argument: + if isinstance(return_type, (torch.TensorType, torch.NoneType)): + # For single Tensor or None + out = output + if isinstance(output, (list, tuple)): + assert len(output) == 1 + out = output[0] + if isinstance(return_type, torch.TensorType): + assert isinstance(out, IRNode) + return export_schema.Argument.create( + as_tensor=export_schema.TensorArgument(name=out.get_name()) + ) + else: # NoneType + assert out is None + return export_schema.Argument.create(as_none=True) + elif isinstance(return_type, torch.ListType) and isinstance( + return_type.getElementType(), torch.TensorType + ): + assert isinstance(output, Sequence), type(output) + # For single TensorList + return export_schema.Argument.create( + as_tensors=[ + export_schema.TensorArgument(name=out.get_name()) + for out in output + ] + ) + elif isinstance(return_type, torch.OptionalType) and isinstance( + return_type.getElementType(), torch.TensorType + ): + # For OptionalTensor + if output is None: + return export_schema.Argument.create( + as_optional_tensor=export_schema.OptionalTensorArgument.create( + as_none=True + ) + ) + else: + assert isinstance(output, IRNode) + return export_schema.Argument.create( + as_optional_tensor=export_schema.OptionalTensorArgument.create( + as_tensor=export_schema.TensorArgument( + name=output.get_name() + ) + ) + ) + elif isinstance(return_type, torch.IntType): + return export_schema.Argument.create(as_int=output) + else: + raise RuntimeError(f"Unsupported return type {type(return_type)}") + + if isinstance(target, torch._higher_order_ops.torchbind.CallTorchBind): + returns = target.schema(args[0], args[1]).returns + else: + returns = target._schema.returns # type: ignore[union-attr] + if len(returns) == 1: + # NOTE: [special handling of all_reduce_coalesced_'s return value] + # all_reduce_coalesced_ return a list of tensors via self.mutation_outputs + outputs = self.outputs if self.outputs else self.mutation_outputs + return_type = returns[0].real_type + output_arguments = [handle_single_output(return_type, outputs)] + else: + # For tuple returns, e.g "-> (Tensor, Tensor)" or "-> (Tensor, Tensor[])" + # Not generating output args for self.mutation_outputs + output_arguments = [ + handle_single_output( + return_schema.real_type, # type: ignore[attr-defined] + output, + ) + for return_schema, output in zip(returns, self.outputs) + ] + + assert self.op_overload is not None + node = ExternKernelNode( + name=self.get_name(), + node=export_schema.Node( + target=self.op_overload.name(), + inputs=named_arguments, + outputs=output_arguments, + metadata={}, + ), + ) + + V.extern_kernel_nodes.append(node) + + return [*args, *ordered_kwargs] + + @override + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + """Overrides the parent member. + See https://github.com/pytorch/pytorch/issues/151692""" + kernel = self.op_overload + assert kernel is not None + if kernel.namespace == "aten": + # Aten Fallback Ops + assert isinstance(kernel, torch._ops.OpOverload), type(kernel) + if V.graph.cpp_wrapper: + from torchgen.aoti.fallback_ops import inductor_fallback_ops + + if str(kernel) not in inductor_fallback_ops: + # C shim v2 is torchgen-ed, which should cover all aten ops. + # If you do hit a missed op, please update fallback_ops.py. + log.warning( + "%s is missing a c-shim implementation, using proxy executor as fallback", + kernel, + ) + self.use_runtime_dispatch = True + elif kernel.namespace == "_quantized": + # Internal Quantized Fallback Ops + assert isinstance(kernel, torch._ops.OpOverload), type(kernel) + elif V.graph.cpp_wrapper: + # For non-aten OpOverload, i.e. custom ops + # If the op is in custom_ops_to_c_shims, generate direct function call + self.use_runtime_dispatch = ( + kernel not in config.aot_inductor.custom_ops_to_c_shims + ) + + # Handle the special case where a complex number is input to a C-shim kernel for + # a scalar input. The torchgen'ed shim API will use type "double", which is + # incompatible with complex numbers, forcing a fallback to runtime dispatch. + if ( + V.graph.cpp_wrapper + and isinstance(kernel, torch._ops.OpOverload) + and not self.use_runtime_dispatch + ): + + def is_number(t: torch.JitType) -> bool: + if isinstance(t, torch.OptionalType): + return is_number(t.getElementType()) + return isinstance(t, torch.NumberType) + + # Using unflatten_args is a bit of a hack, but all the complex arguments we + # care about are in self.constant_args, and calling unflatten_args puts them + # in the correct order without triggering codegen. + args, kwargs = self.unflatten_args(self.inputs, self.constant_args) + # Append kwarg values to args. ordered_kwargs_for_cpp_kernel is guaranteed + # to be set, since this is an OpOverload kernel. + args_iter = itertools.chain( + args, + ( + self.get_kwargs_value(k, **kwargs) + for k in self.ordered_kwargs_for_cpp_kernel + ), + ) + self.use_runtime_dispatch = any( + isinstance(v, complex) and is_number(a.real_type) + for v, a in zip(args_iter, kernel._schema.arguments) + ) + + self.codegen_comment(wrapper) + if self.use_runtime_dispatch: + exported_args = self.export_extern_kernel_node() + assert self.python_kernel_name is not None + assert self.op_overload is not None + + wrapper.generate_fallback_kernel_with_runtime_lookup( + self.get_name(), + self.python_kernel_name, + lambda: [*self.codegen_args(), *self.codegen_kwargs()], + self.op_overload, + exported_args, + # NOTE: [special handling of all_reduce_coalesced_'s return value] + self.outputs if self.outputs else self.mutation_outputs, + ) + else: + wrapper.generate_fallback_kernel(self) + if isinstance(self.layout, Layout): + self.codegen_size_asserts(wrapper) + self.codegen_alignment_asserts(wrapper) + self.codegen_memory_tracking(wrapper) + + self.codegen_unbacked_symbol_defs(wrapper) + + @staticmethod + def tensor_to_layout(output: torch.Tensor) -> FixedLayout: + is_pinned = False + try: + is_pinned = output.is_pinned() + except RuntimeError: + # dispatch not implemented + pass + return FixedLayout( + output.device, + output.dtype, + convert_shape_to_inductor(output.size()), + convert_shape_to_inductor(output.stride()), + is_pinned=is_pinned, + ) + + @classmethod + def create(cls, kernel: _OpOverloads, *args: Any, **kwargs: Any) -> FallbackKernel: + """Create an instance of FallbackKernel from an _OpOverloads""" + fake_incorrect_kernels = (aten._fused_moving_avg_obs_fq_helper_functional,) + if kernel not in fake_incorrect_kernels: + context = cast(AbstractContextManager[None], V.graph.fake_mode) + else: + context = nullcontext() + + with context: + ( + example_output, + tensor_args, + non_tensor_args, + unflatten_args, + unbacked_bindings, + ) = cls.process_kernel(kernel, *args, **kwargs) + + # Try to lower single output functional custom ops to their out-variant. + if isinstance(kernel, torch._ops.OpOverload) and isinstance( + example_output, torch.Tensor + ): + from torch._library._out_variant import ( + _is_functional, + get_out_arg_names, + lookup_manual_out_variant, + to_out_variant, + ) + + out_op = None + if _is_functional(kernel._schema): + out_op = to_out_variant(kernel) + if out_op is None: + out_op = lookup_manual_out_variant(kernel) + + if out_op is not None and len(get_out_arg_names(out_op)) == 1: + layout = FixedLayout( + device=example_output.device, + dtype=example_output.dtype, + size=[*example_output.shape], + stride=[*example_output.stride()], + ) + return ExternKernelOut( # type: ignore[return-value] + layout=layout, + inputs=list(tensor_args), + constant_args=list(non_tensor_args), + kwargs=kwargs, + python_kernel_name=_make_out_variant_kernel_name(out_op), + op_overload=out_op, + ) + + # We need this extra check for input alignment since the example + # inputs we created are always aligned. + has_unaligned_input = any(is_unaligned(arg) for arg in tensor_args) + + device = cls.find_device(tensor_args, example_output) + + # Default to CPU for torchbind methods or HOPs that don't produce tensors + if not device and ( + isinstance(kernel, torch._higher_order_ops.torchbind.CallTorchBind) + or kernel is torch.ops.higher_order.print + ): + device = torch.device("cpu") + + # Try multi-output .out() lowering for ops with out_variant tag. + if ( + isinstance(kernel, torch._ops.OpOverload) + and not V.graph.cpp_wrapper + and device + ): + out_result = ExternKernelMultiOut.try_create( + kernel, + example_output, + device, + tensor_args, + non_tensor_args, + unflatten_args, + kwargs, + unbacked_bindings=unbacked_bindings, + has_unaligned_input=has_unaligned_input, + ) + if out_result is not None: + return out_result # type: ignore[return-value] + + if example_output is None: + packed = cls( + NoneLayout(device=device), + kernel, + tensor_args, + non_tensor_args, + unflatten_args, + kwargs=kwargs, + unbacked_bindings=unbacked_bindings, + ) + + else: + assert device, "Not sure where to find device info" + packed = cls( + MultiOutputLayout(device=device), + kernel, + tensor_args, + non_tensor_args, + unflatten_args, + kwargs=kwargs, + unbacked_bindings=unbacked_bindings, + ) + + def generate_output(output: Any, indices: list[tuple[Any, int]]) -> Any: + if isinstance(output, (list, tuple)): + return type(output)( + generate_output(output[i], indices + [(type(output), i)]) + for i in range(len(output)) + ) + elif isinstance(output, dict): + return { + key: generate_output(val, indices + [(type(output), key)]) + for key, val in output.items() + } + elif isinstance(output, torch.Tensor): + buf = MultiOutput( + cls.tensor_to_layout(output), + packed, + indices, + ) + if ( + config.assume_unaligned_fallback_output + or has_unaligned_input + or not tensor_is_aligned(output) + ): + V.graph.unaligned_buffers.add(buf.name) # type: ignore[arg-type] + return buf + elif isinstance(output, int): + return output + elif isinstance(output, torch.SymInt): + return output.node.expr + elif isinstance( + output, (torch._C.ScriptObject, FakeScriptObject) + ) or is_opaque_value(output): + return OpaqueMultiOutput( + NoneLayout(device=device), + packed, + indices, + output, + ) + else: + assert output is None, ( + f"FallbackKernel output type {type(output)} is not supported" + ) + return None + + outputs = generate_output(example_output, []) + if isinstance(outputs, (list, tuple)): + packed.outputs = outputs + elif isinstance(outputs, dict): + packed.outputs = tuple(outputs) + else: + packed.outputs = [outputs] + + return outputs + + +@ir_dataclass(frozen=False) +class ComplexView(FallbackKernel): + """View a complex number as two dtyped numbers or vice versa""" + + def should_allocate(self) -> bool: + return False + + def get_inputs_that_alias_output(self) -> Sequence[str]: + # Signal to codegen that our output buffer isn't safe to reuse + return [self.input_name(0)] + + def __init__( + self, + layout: OutputSpec, + kernel: _OpOverloads, + tensor_args: Sequence[IRNode], + nontensor_args: Sequence[Any], + unflatten_args: Callable[..., Any], + *, + kwargs: dict[str, Any] | None = None, + unbacked_bindings: dict[sympy.Symbol, pytree.KeyPath] | None = None, + ) -> None: + super().__init__( + layout, + kernel, + tensor_args, + nontensor_args, + unflatten_args, + kwargs=kwargs, + unbacked_bindings=unbacked_bindings, + ) + + +class MemoryCheckKernel(FallbackKernel): + """ + Custom kernel for memory checking that generates direct function calls + + TODO - the custom op was erroring with str inputs. should be able to custom op directly. + """ + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + """Override codegen to write direct function call""" + # Extract our arguments from nontensor_args + wrapper.write_memory_track_allocation_once() + alive_list, dead_list, is_final_step = self.constant_args + + alive_repr = repr(alive_list) + dead_repr = repr(dead_list) + if is_final_step: + wrapper.writeline( + "# note: dont currently distinguish between buffers returned and dealloc'd in last step" + ) + call = f"check_memory_step(allocated={alive_repr}, freed={dead_repr}, is_final_step={is_final_step})" + else: + call = f"check_memory_step(allocated={alive_repr}, freed={dead_repr})" + wrapper.writeline(call) + + +@ir_dataclass +class MultiOutputLayout(OutputSpec): + device: torch.device + + def get_device(self) -> torch.device | None: + return self.device + + +class MultiOutput(ExternKernel): + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + wrapper.codegen_multi_output(self) + if not self.skip_size_stride_alignment_checks: + self.codegen_size_asserts(wrapper) + self.codegen_alignment_asserts(wrapper) + + def __init__( + self, + layout: OutputSpec, + input: IRNode, + indices: list[tuple[Any, ...]], + skip_size_stride_alignment_checks: bool = False, + ) -> None: + super().__init__(None, layout, [input], ()) + self.name = V.graph.register_buffer(self) + V.graph.register_operation(self) + self.indices = indices + self.skip_size_stride_alignment_checks = skip_size_stride_alignment_checks + + @cache_on_self_and_args("MultiOutput") + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + input_node = self.inputs[0] + assert isinstance(input_node, IRNode), input_node + return input_node.get_free_symbol_uses(unbacked_only) + + def should_allocate(self) -> bool: + return len(self.inputs) == 1 and ( + isinstance(self.inputs[0], CppTemplateBuffer) # Grouped GEMM + ) + + def get_inputs_that_alias_output(self) -> Sequence[str]: + return [ + inp.get_name() + for inp in self.inputs + if isinstance(inp, FallbackKernel) + and len(inp.get_inputs_that_alias_output()) > 0 + ] + + def get_read_writes(self) -> dependencies.ReadWrites: + # Reads: StarDep on parent (we don't know which elements of the + # packed output we index into — conservative is correct). + reads: OrderedSet[dependencies.Dep] = OrderedSet() + for inp in self.inputs: + if isinstance(inp, IRNode): + reads.add(dependencies.StarDep(inp.get_name())) + + # Writes: build proper MemoryDep from our FixedLayout so the + # scheduler can match our write with downstream epilogue reads. + # Normalize using the same policy as SchedulerNode so that the + # index expressions are directly comparable during fusion checks. + name = self.get_name() + indexer = self.get_layout().make_indexer() + + def dummy(index: Sequence[Any], rindex: Sequence[Any]) -> Any: + assert len(rindex) == 0 + return ops.store(name, indexer(index), "fake") + + device = self.get_device() + should_normalize = ( + not config.loop_ordering_after_fusion + or device is None + or not is_gpu(device.type) + ) + write_rw = dependencies.extract_read_writes( + dummy, self.get_size(), (), normalize=should_normalize + ) + return dependencies.ReadWrites( + reads=reads, + writes=write_rw.writes, + index_exprs=OrderedSet(), + ) + + +class OpaqueMultiOutput(MultiOutput): + """MultiOutput for opaque objects.""" + + def __init__( + self, + layout: OutputSpec, + input: IRNode, + indices: list[tuple[Any, ...]], + opaque_value: Any, + ) -> None: + super().__init__(layout, input, indices, skip_size_stride_alignment_checks=True) + self.opaque_example_value = opaque_value + + @property # type: ignore[override] + def dtype(self) -> Never: + raise AttributeError("OpaqueMultiOutput has no dtype") + + def wrap_for_lowering(self) -> OpaqueMultiOutput: + return self + + def get_read_writes(self) -> dependencies.ReadWrites: + reads: OrderedSet[dependencies.Dep] = OrderedSet() + for inp in self.inputs: + if isinstance(inp, IRNode): + reads.add(dependencies.StarDep(inp.get_name())) + writes: OrderedSet[dependencies.Dep] = OrderedSet( + [dependencies.StarDep(self.get_name())] + ) + return dependencies.ReadWrites( + reads=reads, + writes=writes, + index_exprs=OrderedSet(), + ) + + +class AllocatingMultiOutput(MultiOutput): + """MultiOutput with Inductor-controlled allocation for .out() variant ops. + + Overrides should_allocate()=True so Inductor allocates the output buffer, + and skips tuple-indexing codegen since .out() writes directly into these buffers. + """ + + def should_allocate(self) -> bool: + return True + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + if not self.skip_size_stride_alignment_checks: + self.codegen_size_asserts(wrapper) + self.codegen_alignment_asserts(wrapper) + + +def _make_out_variant_kernel_name(out_op: torch._ops.OpOverload) -> str: + """Build fully-qualified kernel name for an out-variant op.""" + ns = out_op.namespace + op_name = out_op._schema.name.split("::")[1] + overload = out_op._overloadname + return f"torch.ops.{ns}.{op_name}.{overload}" + + +class ExternKernelMultiOut(FallbackKernel): + """Multi-output .out() variant lowering. + + Subclass of FallbackKernel that emits .out() calls with pre-allocated + output buffers. Uses AllocatingMultiOutput child nodes for each output. + """ + + out_arg_names: list[str] + out_variant_output_nodes: list[AllocatingMultiOutput] + + def __init__( + self, + *args: Any, + out_op: torch._ops.OpOverload, + out_arg_names: list[str], + **kwargs: Any, + ) -> None: + super().__init__(*args, **kwargs) + self.out_arg_names = out_arg_names + self.out_variant_output_nodes = [] + self.python_kernel_name = _make_out_variant_kernel_name(out_op) + self.op_overload = out_op + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + self.codegen_comment(wrapper) + wrapper.generate_extern_kernel_multi_out(self) + + @classmethod + def try_create( + cls, + kernel: torch._ops.OpOverload, + example_output: Any, + device: torch.device, + tensor_args: Sequence[IRNode], + non_tensor_args: Sequence[Any], + unflatten_args: Callable[..., Any], + kwargs: dict[str, Any] | None, + *, + unbacked_bindings: dict[sympy.Symbol, pytree.KeyPath] | None = None, + has_unaligned_input: bool = False, + ) -> Sequence[AllocatingMultiOutput] | None: + """Create an ExternKernelMultiOut if the op has a matching .out() variant.""" + from torch._library._out_variant import ( + _is_functional, + get_out_arg_names, + to_out_variant, + ) + + if not _is_functional(kernel._schema): + return None + + if not isinstance(example_output, (tuple, list)): + return None + + out_op = to_out_variant(kernel) + if out_op is None: + return None + + out_arg_names = get_out_arg_names(out_op) + if not all(isinstance(t, torch.Tensor) for t in example_output): + return None + if len(example_output) != len(out_arg_names): + return None + + packed = cls( + MultiOutputLayout(device=device), + kernel, + tensor_args, + non_tensor_args, + unflatten_args, + kwargs=kwargs, + unbacked_bindings=unbacked_bindings, + out_op=out_op, + out_arg_names=out_arg_names, + ) + + outputs: list[AllocatingMultiOutput] = [] + for i, tensor_out in enumerate(example_output): + layout = FixedLayout( + device=tensor_out.device, + dtype=tensor_out.dtype, + size=[*tensor_out.shape], + stride=[*tensor_out.stride()], + ) + multi_out = AllocatingMultiOutput( + layout=layout, + input=packed, + indices=[(type(example_output), i)], + ) + if ( + config.assume_unaligned_fallback_output + or has_unaligned_input + or not tensor_is_aligned(tensor_out) + ): + V.graph.unaligned_buffers.add(multi_out.name) # type: ignore[arg-type] + outputs.append(multi_out) + + packed.out_variant_output_nodes = outputs + packed.outputs = outputs + + if isinstance(example_output, tuple): + return tuple(outputs) # type: ignore[return-value] + return list(outputs) + + +# We just use a normal dataclass for MutableBox/TensorBox/StorageBox since +# they're mainly lowering-time constructs that we expect to mutate and such. +@dataclasses.dataclass +class MutableBox(IRNode): + """ + TensorBox / StorageBox allow in-place mutation of Tensors + """ + + data: IRNode + + def has_exceeded_max_reads(self) -> bool: + return self.data.has_exceeded_max_reads() + + def get_device(self) -> torch.device | None: + return self.data.get_device() + + def make_loader(self) -> Callable[[Sequence[Expr]], OpsValue]: + return self.data.make_loader() + + def make_indexer(self) -> Callable[[Sequence[Expr]], Expr]: + return self.data.make_indexer() + + def get_stride(self) -> Sequence[_IntLike]: + return self.data.get_stride() + + def get_name(self) -> str: + return self.data.get_name() + + def has_large_inner_fn(self, threshold: int | None = None) -> bool: + return self.data.has_large_inner_fn(threshold) + + def mark_reuse(self, users: int) -> None: + return self.data.mark_reuse(users) + + def realize_hint(self) -> None: + return self.data.realize_hint() + + def unwrap_view(self) -> IRNode: + return self.data.unwrap_view() + + def is_input_buffer(self) -> bool: + return self.data.is_input_buffer() + + def freeze_layout(self) -> None: + return self.data.freeze_layout() + + def freeze_layout_with_stride_order( + self, order: Sequence[int], allow_padding: bool = False + ) -> None: + return self.data.freeze_layout_with_stride_order(order, allow_padding) + + def freeze_layout_with_fill_order(self, order: Sequence[int]) -> None: + return self.data.freeze_layout_with_fill_order(order) + + def freeze_layout_with_same_order(self, stride: Sequence[_IntLike]) -> None: + return self.data.freeze_layout_with_same_order(stride) + + def freeze_layout_with_exact_strides( + self, exact_strides: Sequence[_IntLike], allow_padding: bool = False + ) -> None: + return self.data.freeze_layout_with_exact_strides(exact_strides, allow_padding) + + def get_read_writes(self) -> dependencies.ReadWrites: + return self.data.get_read_writes() + + def get_reads(self) -> OrderedSet[Dep]: + return self.data.get_reads() + + def num_reads(self) -> int: + return self.data.num_reads() + + def get_storage_numel(self) -> _IntLike: + return self.data.get_storage_numel() + + def get_reduction_type(self) -> str | None: + return self.data.get_reduction_type() + + def get_reduction_size(self) -> Sequence[Expr]: + return self.data.get_reduction_size() + + def is_extern(self) -> bool: + return self.data.is_extern() + + def is_no_op(self) -> bool: + return self.data.is_no_op() + + def constant_to_device(self, device: torch.device) -> IRNode: + return self.data.constant_to_device(device) + + def get_mutation_names(self) -> Sequence[str]: + return self.data.get_mutation_names() + + def get_operation_name(self) -> str: + return self.data.get_operation_name() + + def get_inputs_that_alias_output(self) -> Sequence[str]: + return self.data.get_inputs_that_alias_output() + + def realize(self) -> str | None: + return self.data.realize() + + @cache_on_self_and_args("MutableBox") + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + return self.data.get_free_symbol_uses(unbacked_only) + + def get_read_names(self) -> OrderedSet[str]: + return self.data.get_read_names() + + def get_defining_op(self) -> Operation | None: + return self.data.get_defining_op() + + def codegen_reference(self, writer: IndentedBuffer | None = None) -> str: + return self.data.codegen_reference(writer) + + @property + def layout(self) -> OutputSpec: + # we intentionally call get_output_spec (rather than get_layout) since Buffer.layout is an OutputSpec + return self.data.get_output_spec() + + def get_layout(self) -> Layout: + return self.data.get_layout() + + def get_output_spec(self) -> OutputSpec: + return self.data.get_output_spec() + + def get_size(self) -> Sequence[Expr]: + return self.data.get_size() + + @property + def dtype(self) -> torch.dtype: + return self.data.dtype + + def __str__(self) -> str: + if isinstance(self.data, MutableBox): + line0 = f"{type(self).__name__}({type(self.data).__name__}(" + endl = "))" + inner = self.data.data + else: + line0 = f"{type(self).__name__}(" + inner = self.data + endl = ")" + + lines = [ + line0, + indent(str(inner)), + endl, + ] + return "\n".join(lines) + + __repr__ = __str__ + + +class TensorBox(MutableBox): + @overload + @staticmethod + def create(data: ShapeAsConstantBuffer) -> ShapeAsConstantBuffer: ... + @overload + @staticmethod + def create(data: IRNode) -> TensorBox: ... + + @staticmethod + def create(data: IRNode): + if isinstance(data, ShapeAsConstantBuffer): + return data + return TensorBox(StorageBox(data)) + + +class StorageBox(MutableBox): + """ + StorageBox allow in-place mutation of Tensors + """ + + def is_input_buffer(self) -> bool: + if isinstance(self.data, (InputBuffer, ReinterpretView)): + return self.data.get_name() in V.graph.graph_inputs + return False + + def is_module_buffer(self) -> bool: + return ( + isinstance(self.data, (ConstantBuffer)) + and self.data.get_name() in V.graph.constants + ) + + def realize(self) -> str | None: + if IRNode.is_realized_node(self.data): + return self.data.get_name() + + assert isinstance(self.data, (Pointwise, Reduction, Scan, Sort)), type( + self.data + ) + origin_node = self.data.get_origin_node() + traceback = self.data.get_traceback() + device = self.data.get_device() + assert device is not None + + self.data = ComputedBuffer( + name=None, + layout=FlexibleLayout( + device=device, + dtype=self.data.get_dtype(), + size=self.data.get_size(), + is_pinned=False, + ), + data=self.data, + ) + self.data.name = V.graph.register_buffer(self.data) + V.graph.register_operation(self.data) + self.data.origins = self.origins + self.data.origin_node = origin_node + self.data.traceback = traceback + self.data.stream_idx = self.data.data.stream_idx + return self.data.name + + def realize_hint(self) -> None: + """ + Called on buffers we expect to be forced to realize later. + """ + if ( + isinstance(self.data, (Pointwise, Reduction)) + and self.data.inner_fn_opcount().nontrivial_read_count > 1 + ): + self.realize() + + def has_accumulated_enough_reads_by_size(self, threshold: int) -> bool: + from torch._inductor.utils import is_nonfreeable_buffers + + size_of_reads = [ + V.graph.get_dep_size_hint(dep) + for dep in self.get_reads() + if not is_nonfreeable_buffers(dep) + ] + if not size_of_reads: + return False + total_size = sum(size_of_reads) + max_size = max(size_of_reads) + min_size = min(size_of_reads) + return ( + total_size >= threshold + and total_size / max_size >= 2 + and max_size == min_size + ) + + def has_exceeded_max_reads(self) -> bool: + return isinstance(self.data, Pointwise) and ( + self.num_reads() > config.realize_acc_reads_threshold + or self.has_large_inner_fn() + or ( + config.realize_acc_reads_size_threshold is not None + and self.has_accumulated_enough_reads_by_size( + config.realize_acc_reads_size_threshold + ) + ) + ) + + def should_realize_on_reuse(self, users: int) -> bool: + """ + A heuristic to decide if we should realize a tensor + that is used multiple times. + """ + if users > 1 and isinstance(self.data, (Pointwise, Reduction)): + if is_cpu(self.data): + # Heuristic for realizing reused result of heavy ops on cpu + opcount = self.data.inner_fn_opcount() + heavy_ops = ["exp", "sigmoid"] # a list of heavy ops + if any(x in opcount.used_ops for x in heavy_ops): + return True + return ( + self.num_reads() > config.realize_reads_threshold + or self.has_large_inner_fn() + ) + return False + + def mark_reuse(self, users: int) -> None: + if self.should_realize_on_reuse(users): + self.realize() + + def num_reads(self) -> int: + return self.data.num_reads() + + +@ir_dataclass(frozen=False) +class Subgraph(IRNode): + name: str + graph_module: torch.fx.GraphModule + graph: GraphLowering | None = None + + +def _has_aliased_buffers(buffers: Sequence[IRNode]) -> bool: + buffers = [ + buffer.unwrap_view() if isinstance(buffer, ReinterpretView) else buffer + for buffer in buffers + ] + # assuming the same buffer is represented by the same IRNode object + return len(OrderedSet(id(buffer) for buffer in buffers)) < len(buffers) + + +@ir_dataclass(frozen=False) +class InvokeSubgraph(ExternKernel): + """ + Ir node for the invoke_subgraph HOP. + """ + + subgraph: Subgraph | None = None + operands: Sequence[IRNode] | None = None + outputs: Sequence[IRNode] | None = None + + def __init__( + self, subgraph: Subgraph, operands: Sequence[IRNode], layout: MultiOutputLayout + ) -> None: + super().__init__( + name=None, + layout=layout, + inputs=operands, + ) + self.subgraph = subgraph + self.name = V.graph.register_buffer(self) + V.graph.register_operation(self) + + def get_subgraphs(self) -> list[Subgraph]: + return [self.subgraph] if self.subgraph else [] + + @classmethod + def create( + cls, subgraph: Subgraph, *operands: IRNode + ) -> list[ShapeAsConstantBuffer | NoneAsConstantBuffer | MultiOutput]: + """For each operand, get a realized input, force it to have the same + strides as the subgraph inputs, then use an InvokeSubgraph""" + from .lowering import constrain_to_fake_tensor + + # TODO(anijain2305) - Support sym expr as operands in future. + current_node = V.graph.current_node + + fake_operands = None + if eager_input_vals := current_node.meta.get("eager_input_vals"): + # eager_input_vals is (args_values, kwargs_values). We need args for invoke_subgraph + offset = 2 + if current_node.target is torch.ops.higher_order.with_effects: + # Aruguments eagerly are (token, subgraph, identifier, *operands) + assert current_node.args[1] is torch.ops.higher_order.invoke_subgraph + offset = 3 + fake_operands = eager_input_vals[0][offset:] + else: + offset = 2 + if current_node.target is torch.ops.higher_order.with_effects: + # with_effects args: (token, invoke_subgraph, subgraph, identifier, *operands) + assert current_node.args[1] is torch.ops.higher_order.invoke_subgraph + offset = 4 + + # For the partitioned backward graph, we do not have + # eager_input_vals. Here, we rely on the recorded example values. + fx_operands = current_node.args[offset:] + fake_operands = [x.meta["val"] for x in fx_operands] # type: ignore[union-attr] + + # Realize the inputs. Also intermediates can have different strides than + # the inputs of the subgraph. So, force the intermediates to have same + # strides as that of subgraph inputs. + # pyrefly: ignore [annotation-mismatch, redefinition] + operands: list[IRNode] = [cls.realize_input(x) for x in operands] + new_operands: list[IRNode] = [] + + for idx, operand in enumerate(operands): + if isinstance( + operand, (ShapeAsConstantBuffer, GeneratorState, OpaqueObjectState) + ): + new_operands.append(operand) + else: + new_operands.append( + constrain_to_fake_tensor(operand, fake_operands[idx]) + ) + + # pyrefly: ignore [bad-assignment] + operands = new_operands + + if subgraph.graph is None: + # create and lower subgraphs + subgraph.graph = V.graph.make_subgraph( + gm=subgraph.graph_module, + example_inputs=fake_operands, + subgraph_name=subgraph.name, + ) + with V.set_graph_handler(subgraph.graph): + subgraph.graph.run(*fake_operands) + + outputs = subgraph.graph.graph_outputs + + # Find the device - operands could be integers from shapes, so we can't + # use operands[0] + device = None + for operand in operands: + if not isinstance(operand, ShapeAsConstantBuffer): + device = operand.get_device() + break + assert device is not None + invoke_subgraph = InvokeSubgraph( + subgraph=subgraph, + operands=operands, + layout=MultiOutputLayout(device=device), + ) + + def create_output( + output: IRNode, ind: int + ) -> ShapeAsConstantBuffer | NoneAsConstantBuffer | MultiOutput: + if isinstance(output, (ShapeAsConstantBuffer, NoneAsConstantBuffer)): + return output + else: + device = output.get_device() + assert device is not None + + return MultiOutput( + FixedLayout( + device=device, + dtype=output.get_dtype(), + size=output.get_size(), + stride=output.get_stride(), + offset=output.get_layout().offset, + is_pinned=output.get_layout().is_pinned, + ), + invoke_subgraph, # type: ignore[has-type] + [(list, ind)], + skip_size_stride_alignment_checks=True, + ) + + outs = [create_output(output, i) for i, output in enumerate(outputs)] + invoke_subgraph.outputs = outs # type: ignore[assignment] + return outs + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + wrapper.codegen_invoke_subgraph(self) + + +@ir_dataclass(frozen=False) +class Conditional(ExternKernel): + """ + IR node representing torch.cond + + Attributes: + predicate: A boolean scalar tensor determining which branch to execute. + operands: Input tensors passed to both true and false subgraphs. + true_subgraph: Subgraph executed when predicate is True. + false_subgraph: Subgraph executed when predicate is False. + outputs: MultiOutput nodes representing the conditional's outputs. + """ + + predicate: IRNode | None = None + operands: Sequence[IRNode] | None = None + true_subgraph: Subgraph | None = None + false_subgraph: Subgraph | None = None + outputs: Sequence[MultiOutput] | None = None + + def __init__( + self, + predicate: IRNode, + operands: Sequence[IRNode], + true_subgraph: Subgraph, + false_subgraph: Subgraph, + layout: MultiOutputLayout, + unbacked_bindings: dict[sympy.Symbol, pytree.KeyPath] | None, + ) -> None: + self.predicate = predicate + self.operands = operands + self.true_subgraph = true_subgraph + self.false_subgraph = false_subgraph + + sym_args, tensor_args = _split_by_sym_type([predicate, *operands]) + + super().__init__( + name=None, + layout=layout, + inputs=tensor_args, + constant_args=sym_args, + ) + if unbacked_bindings is not None: + self.unbacked_bindings = unbacked_bindings + + self.name = V.graph.register_buffer(self) + V.graph.register_operation(self) + + def get_subgraphs(self) -> list[Subgraph]: + subgraphs = [] + if self.true_subgraph: + subgraphs.append(self.true_subgraph) + if self.false_subgraph: + subgraphs.append(self.false_subgraph) + return subgraphs + + @staticmethod + def _maybe_expr(s: int | torch.SymInt) -> int | sympy.Expr: + if isinstance(s, int): + return s + return s.node.expr + + @classmethod + def create( + cls, + predicate: TensorBox, + true_fn: Subgraph, + false_fn: Subgraph, + operands: list[TensorBox], + ) -> list[MultiOutput]: + """Create a Sequence of IRNodes from a conditional statement (see .lowering.cond)""" + # pyrefly: ignore [bad-assignment] + predicate = cls.realize_input(predicate) + # pyrefly: ignore [bad-assignment] + operands = [cls.realize_input(x) for x in operands] + fx_operands: Argument = V.graph.current_node.args[-1] + + assert isinstance(fx_operands, Sequence), type(fx_operands) + # Build fake_operands from FX nodes' metadata + # For FX Nodes, get the fake tensor from meta["val"] + # For non-Nodes (e.g., symbolic integers from sym_size lowering), pass directly + fake_operands: list[Any] = [] + for fx_op in fx_operands: + if isinstance(fx_op, Node): + fake_operands.append(fx_op.meta["val"]) + else: + # Symbolic integer or constant - pass directly + fake_operands.append(fx_op) + fake_outputs = V.graph.current_node.meta["val"] + + def _require_exact_strides( + graph_outputs: Sequence[IRNode], + fake_tensors: Sequence[torch.Tensor], + ) -> list[IRNode]: + ret = [] + for output, fake in zip(graph_outputs, fake_tensors): + if isinstance(output, ShapeAsConstantBuffer): + ret.append(output) + else: + ret.append( + # pyrefly: ignore [bad-argument-type] + ExternKernel.require_exact_strides( + TensorBox(output), fake.stride(), allow_padding=False + ) + ) + # pyrefly: ignore [bad-return] + return ret + + for subgraph in (true_fn, false_fn): + if subgraph.graph is None: + # create and lower subgraphs + subgraph.graph = V.graph.make_subgraph( + gm=subgraph.graph_module, + example_inputs=fake_operands, + subgraph_name=subgraph.name, + ) + with V.set_graph_handler(subgraph.graph): + subgraph.graph.run(*fake_operands) + # Force subgraph outputs to have the expected strides from + # FakeTensor metadata. This ensures both branches produce + # outputs with consistent strides. + subgraph.graph.graph_outputs = _require_exact_strides( + subgraph.graph.graph_outputs, fake_outputs + ) + + assert true_fn.graph is not None + assert false_fn.graph is not None + true_outputs = true_fn.graph.graph_outputs + false_outputs = false_fn.graph.graph_outputs + + for name, outputs in (("true_fn", true_outputs), ("false_fn", false_outputs)): + if _has_aliased_buffers(true_outputs): + raise AssertionError( + "Output aliasing is currently not supported in compiled torch.cond. " + f"The outputs of the {name} subgraph of torch.cond are aliased: {outputs}" + ) + + # make sure true and false outputs are structurally equivalent + assert len(true_outputs) == len(false_outputs), (true_outputs, false_outputs) + for i, (t_o, f_o) in enumerate(zip(true_outputs, false_outputs)): + assert t_o.get_device() == f_o.get_device(), (i, t_o, f_o) + assert t_o.get_dtype() == f_o.get_dtype(), (i, t_o, f_o) + assert t_o.get_layout().offset == f_o.get_layout().offset, (i, t_o, f_o) + + # Determine device from operands and predicate + # The predicate can be on a different device (e.g., CPU for control flow) + # while the data operands and outputs should be on the compute device, so + # using predicate device as a fallback. + device = next( + o.get_device() + for o in operands + [predicate] + if not isinstance(o, ShapeAsConstantBuffer) + ) + unbacked_bindings = resolve_unbacked_bindings( + V.graph.sizevars.shape_env, + V.graph.current_node.meta.get("unbacked_bindings", None), + ) + assert device is not None, "cannot determine device" + conditional = Conditional( + predicate=predicate, + operands=operands, + true_subgraph=true_fn, + false_subgraph=false_fn, + layout=MultiOutputLayout(device=device), + unbacked_bindings=unbacked_bindings, + ) + + outputs = [ + MultiOutput( + FixedLayout( + # pyrefly: ignore [bad-argument-type] + device=output.get_device() + if output.get_device() is not None + else device, # type: ignore[arg-type] + dtype=output.get_dtype(), + size=[Conditional._maybe_expr(sz) for sz in merged_output.size()], + stride=[ + Conditional._maybe_expr(sz) for sz in merged_output.stride() + ], + offset=output.get_layout().offset, + is_pinned=output.get_layout().is_pinned, + ), + conditional, + [(list, i)], + ) + # as the true and false outputs are equivalent, + # we can use either of them here as a "template" + for i, (output, merged_output) in enumerate( + zip(true_outputs, V.graph.current_node.meta["val"]) + ) + ] + + conditional.outputs = outputs # type: ignore[assignment] + + from torch._higher_order_ops.utils import ( + check_input_alias_and_mutation_return_outputs, + ) + + (_, _, _, true_mutated_inputs, _) = ( + check_input_alias_and_mutation_return_outputs(true_fn.graph_module) + ) + (_, _, _, false_mutated_inputs, _) = ( + check_input_alias_and_mutation_return_outputs(false_fn.graph_module) + ) + + mutated_operand_indices = OrderedSet(true_mutated_inputs) | OrderedSet( + false_mutated_inputs + ) + + # Create MutationOutput for each mutated operand (for scheduler dependencies) + conditional.mutation_outputs = [ + MutationOutput(operands[idx].layout, operands[idx], conditional) # type: ignore[union-attr] + for idx in sorted(mutated_operand_indices) + ] + + return outputs + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + wrapper.codegen_conditional(self) + wrapper.codegen_unbacked_symbol_defs_for_outputs( + self.get_name(), self.outputs, getattr(self, "unbacked_bindings", {}) + ) + + def get_unbacked_symbol_defs(self) -> OrderedSet[sympy.Symbol]: + if unbacked_bindings := getattr(self, "unbacked_bindings", None): + resolved = resolve_unbacked_bindings( + V.graph.sizevars.shape_env, unbacked_bindings + ) + assert resolved is not None + return OrderedSet(resolved.keys()) + else: + return OrderedSet() + + +def _split_by_sym_type( + args: list[Any], +) -> tuple[list[ShapeAsConstantBuffer], list[Any]]: + non_sym_args = [] + sym_args = [] + for arg in args: + if isinstance(arg, ShapeAsConstantBuffer): + sym_args.append(arg.expr) + else: + non_sym_args.append(arg) + + return sym_args, non_sym_args + + +@ir_dataclass(frozen=False) +class WhileLoop(ExternKernel): + """The IR node for while_loop and while_loop_stack_output. It supports input mutation.""" + + carried_inputs: Sequence[IRNode] | None = None + additional_inputs: Sequence[IRNode] | None = None + cond_subgraph: Subgraph | None = None + body_subgraph: Subgraph | None = None + outputs: Sequence[MultiOutput] | None = None + + def __init__( + self, + carried_inputs: Sequence[IRNode], + additional_inputs: Sequence[IRNode], + cond_subgraph: Subgraph, + body_subgraph: Subgraph, + layout: MultiOutputLayout, + unbacked_bindings: dict[sympy.Symbol, pytree.KeyPath] | None, + stack_output: bool, + ) -> None: + self.carried_inputs = carried_inputs + self.additional_inputs = additional_inputs + self.cond_subgraph = cond_subgraph + self.body_subgraph = body_subgraph + + sym_args, tensor_args = _split_by_sym_type( + [*carried_inputs, *additional_inputs] + ) + super().__init__( + name=None, + layout=layout, + inputs=tensor_args, + constant_args=sym_args, + ) + if unbacked_bindings is not None: + self.unbacked_bindings = unbacked_bindings + self.stack_output = stack_output + + self.name = V.graph.register_buffer(self) + V.graph.register_operation(self) + + def get_subgraphs(self) -> list[Subgraph]: + subgraphs = [] + if self.cond_subgraph: + subgraphs.append(self.cond_subgraph) + if self.body_subgraph: + subgraphs.append(self.body_subgraph) + return subgraphs + + # Accidental aliasing can be created due to cse, where the empty buffers we + # allocated for backward to use gets csed into the same buffer in function fx_graph_cse. + # See test_scan_multiple_layers_gradient for a concrete example. + @staticmethod + def _clone_aliased_inputs(carried_inputs: Sequence[IRNode]) -> Sequence[IRNode]: + if not _has_aliased_buffers(carried_inputs): + return carried_inputs + + # Import clone from lowering module + + # Unwrap views to get the underlying buffers for comparison + unwrapped_buffers = [ + buffer.unwrap_view() if isinstance(buffer, ReinterpretView) else buffer + for buffer in carried_inputs + ] + + # Track which buffers we've seen and their indices + seen_buffers: OrderedSet[int] = OrderedSet() + result: list[IRNode | TensorBox] = [] + + for original_input, unwrapped_buffer in zip(carried_inputs, unwrapped_buffers): + if id(unwrapped_buffer) in seen_buffers: + result.append(ExternKernel.copy_input(original_input)) + else: + seen_buffers.add(id(unwrapped_buffer)) + result.append(original_input) + + return result + + @staticmethod + def _maybe_wrap_as_tensor_box(out: IRNode) -> IRNode: + if isinstance(out, TensorBox): + return out + elif isinstance(out, (StorageBox, ReinterpretView)): + return TensorBox(out) + elif isinstance(out, MultiOutput): + return TensorBox.create(out) + else: + raise RuntimeError(f"NYI unsupported output type: {type(out)}") + + @classmethod + def create( + cls, + cond_fn: Subgraph, + body_fn: Subgraph, + carried_inputs: Sequence[IRNode], + additional_inputs: Sequence[IRNode], + stack_output: bool, + ) -> IRNode | Sequence[IRNode]: + """create the while_loop IR node. stack_output controls whether it stack + each iterations' output, which is necessary for training. + """ + from torch._higher_order_ops.utils import check_input_alias_and_mutation + + def _require_exact_strides( + tensor_boxes: Sequence[IRNode], + fake_tensors: list[int | torch.SymInt | torch.Tensor], + ) -> list[IRNode]: + assert len(tensor_boxes) == len(fake_tensors) + ret = [] + for tb, fk in zip(tensor_boxes, fake_tensors): + if isinstance(fk, torch.Tensor): + # Subgraph lowering always return StorageBox as graph_outputs because + # it realizes the outputs. + # + # However, require_exact_strides is expecting TensorBox + # e.g. in require_exact_strides when an expand happens, + # the fake tensor's stride is (0, 0, 0) but the storage + # box might have a different stride so lowering.slice_ + # is used to make the stride consistent and it expects input to + # be TensorBox. + # + # So we wrap the inputs as tensor boxes if they're not yet. + new_tb = WhileLoop._maybe_wrap_as_tensor_box(tb) + ret.append( + ExternKernel.require_exact_strides( + new_tb, fk.stride(), allow_padding=False + ) + ) + else: + ret.append(tb) + return ret + + fx_carried_inputs = V.graph.current_node.args[-2] + fx_additional_inputs = V.graph.current_node.args[-1] + fx_all_inputs = fx_carried_inputs + fx_additional_inputs # type: ignore[operator] + fake_all_inputs = [x.meta["val"] for x in fx_all_inputs] # type: ignore[union-attr] + fake_carried_inputs = [x.meta["val"] for x in fx_carried_inputs] # type: ignore[union-attr] + fake_additional_inputs = [x.meta["val"] for x in fx_additional_inputs] # type: ignore[union-attr] + + carried_inputs_ = [cls.realize_input(x) for x in carried_inputs] + carried_inputs_ = WhileLoop._clone_aliased_inputs(carried_inputs_) + carried_inputs_ = _require_exact_strides(carried_inputs_, fake_carried_inputs) + additional_inputs_ = [cls.realize_input(x) for x in additional_inputs] + additional_inputs_ = _require_exact_strides( + additional_inputs_, fake_additional_inputs + ) + all_inputs = carried_inputs_ + additional_inputs_ + + for subgraph in (cond_fn, body_fn): + if subgraph.graph is None: + # create and lower subgraphs + assert isinstance(fx_all_inputs, Sequence), type(fx_all_inputs) + subgraph.graph = V.graph.make_subgraph( + gm=subgraph.graph_module, + example_inputs=fx_all_inputs, # type: ignore[arg-type] + subgraph_name=subgraph.name, + ) + with V.set_graph_handler(subgraph.graph): + subgraph.graph.run(*fake_all_inputs) + # For body_fn, we require its output to have the exact same stride + # as inputs because the previous output is the input of next iteration. + # + # This cannot be automatically done in graph lowering because body_fn's graph outputs + # are not user-facing so the special handling for strides of user-facing output in graph + # lowering is not applicable. + if subgraph is body_fn: + assert len(subgraph.graph.graph_outputs) == len( + fake_carried_inputs + ) + subgraph.graph.graph_outputs = _require_exact_strides( # type: ignore[assignment] + subgraph.graph.graph_outputs, + fake_carried_inputs, + ) + + assert cond_fn.graph and body_fn.graph + cond_outputs = cond_fn.graph.graph_outputs + body_outputs = body_fn.graph.graph_outputs + + if _has_aliased_buffers(body_outputs): + raise AssertionError( + "Output aliasing is currently not supported in compiled torch.while_loop. " + f"The outputs of the body_fn subgraph of torch.while_loop are aliased: {body_outputs}" + ) + + # make sure cond_fn returns a boolean scalar Tensor + assert len(cond_outputs) == 1, cond_outputs + p = cond_outputs[0] + if not isinstance(p, ShapeAsConstantBuffer): + assert p.get_dtype() == torch.bool, p + assert len(p.get_size()) == 0, p + + assert len(all_inputs) > 0, ( + "torch.while_loop is assumed to have at least one operand." + ) + + device = all_inputs[0].get_device() + + assert device is not None # to make linter happy + # make sure carried_inputs_ and body outputs are structurally equivalent + assert len(carried_inputs_) == len(body_outputs), ( + carried_inputs_, + body_outputs, + ) + for i, (op, bo) in enumerate(zip(carried_inputs_, body_outputs)): + + def _guard_list_equals( + lhs_exprs: Sequence[int | sympy.Expr], + rhs_exprs: Sequence[int | sympy.Expr], + ) -> None: + assert len(lhs_exprs) == len(rhs_exprs) + for lhs, rhs in zip(lhs_exprs, rhs_exprs): + V.graph.sizevars.check_equals(lhs, rhs) + + _guard_list_equals(op.get_size(), bo.get_size()) + _guard_list_equals(op.get_stride(), bo.get_stride()) + # assume all carried_inputs_ and outputs are on the same device + # as the MultiOutputLayout below requires single device + assert op.get_device() == bo.get_device(), (i, op, bo, device) + assert op.get_dtype() == bo.get_dtype(), (i, op, bo) + + assert device is not None + + unbacked_bindings = resolve_unbacked_bindings( + V.graph.sizevars.shape_env, + V.graph.current_node.meta.get("unbacked_bindings", None), + ) + + while_loop = WhileLoop( + carried_inputs=carried_inputs_, + additional_inputs=additional_inputs_, + cond_subgraph=cond_fn, + body_subgraph=body_fn, + # asserted above that there is at least one operand + layout=MultiOutputLayout(device=device), + unbacked_bindings=unbacked_bindings, + stack_output=stack_output, + ) + + assert body_fn.graph is not None and isinstance( + body_fn.graph.module, torch.fx.GraphModule + ) # to make linter happy + + # Handling input mutations + mutated_idxs = check_input_alias_and_mutation( + body_fn.graph.module, fake_all_inputs + )[3] + mutated_idx_set = OrderedSet(mutated_idxs) + mutated_inputs = [all_inputs[idx] for idx in mutated_idx_set] + + # Create all outputs first + mutated_inputs_iter = iter(mutated_inputs) + all_outputs: list[IRNode] = [] + while_loop.outputs = [] + while_loop.mutation_outputs = [] + if stack_output: + assert len(mutated_idx_set) == 0, ( + "NYI: while_loop_stack_output input mutations." + ) + for idx, output in enumerate(V.graph.current_node.meta["val"]): + # Create MultiOutput for regular outputs + multi_out = MultiOutput( + FixedLayout( + device=output.device, # type: ignore[arg-type] + dtype=output.dtype, + size=[Conditional._maybe_expr(sz) for sz in output.size()], + stride=[Conditional._maybe_expr(st) for st in output.stride()], + ), + while_loop, + [(list, idx)], + ) + while_loop.outputs.append(multi_out) + all_outputs.append(multi_out) + else: + for idx, output in enumerate(body_outputs): + if idx in mutated_idx_set: + assert idx < len(carried_inputs), "only carries can be mutated." + # Create MutationOutput for mutated inputs + mutated_input = next(mutated_inputs_iter) + while_loop.mutation_outputs.append( + MutationOutput(mutated_input.layout, mutated_input, while_loop) # type: ignore[attr-defined, union-attr] + ) + all_outputs.append(mutated_input) + else: + multi_out = MultiOutput( + FixedLayout( + device=output.get_device(), # type: ignore[arg-type] + dtype=output.get_dtype(), + size=output.get_size(), + stride=output.get_stride(), + offset=output.get_layout().offset, + ), + while_loop, + [(list, idx)], + ) + while_loop.outputs.append(multi_out) + all_outputs.append(multi_out) + + for inp, out in zip(carried_inputs, all_outputs): + if inp.get_name() in V.graph.graph_inputs: + # if a carried input of the while_loop is a graph input, + # it can be returned as is when the number of iterations + # is zero. due to this, we can't (generally) reuse the + # output buffers corresponding to the graph inputs, as + # the inputs may end up being mutated. + V.graph.never_reuse_buffers.add(out.get_name()) + return all_outputs + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + wrapper.codegen_while_loop(self, self.stack_output) + wrapper.codegen_unbacked_symbol_defs_for_outputs( + self.get_name(), self.outputs, getattr(self, "unbacked_bindings", {}) + ) + + def get_unbacked_symbol_defs(self) -> OrderedSet[sympy.Symbol]: + if unbacked_bindings := getattr(self, "unbacked_bindings", None): + resolved = resolve_unbacked_bindings( + V.graph.sizevars.shape_env, unbacked_bindings + ) + assert resolved is not None + return OrderedSet(resolved.keys()) + else: + return OrderedSet() + + +class EffectfulKernel(FallbackKernel): + def __init__( + self, + layout: OutputSpec, + kernel: _OpOverloads, + tensor_args: Sequence[IRNode], + nontensor_args: Sequence[Any], + unflatten_args: Callable[..., Any], + kwargs: dict[str, Any] | None = None, + *, + unbacked_bindings: dict[sympy.Symbol, pytree.KeyPath] | None = None, + ) -> None: + super().__init__( + layout, + kernel, + tensor_args, + nontensor_args, + unflatten_args, + kwargs=None, + unbacked_bindings=unbacked_bindings, + ) + + from torch._higher_order_ops.effects import _get_effect + + effect_type = _get_effect(kernel) + assert effect_type is not None + self.effect_type = effect_type + self.prev_effect_buffer = V.graph.effectful_ops.get(effect_type, None) + V.graph.effectful_ops[effect_type] = self + + def get_read_writes(self) -> dependencies.ReadWrites: + read_writes = super().get_read_writes() + + if self.prev_effect_buffer is not None: + read_writes.reads.add( + dependencies.StarDep(self.prev_effect_buffer.get_name()) + ) + + return read_writes + + def has_side_effects(self) -> bool: + return True + + +class NonTensorObj(IRNode): + @cache_on_self_and_args("NonTensorObj") + def get_free_symbol_uses( + self, unbacked_only: bool = False + ) -> OrderedSet[sympy.Symbol]: + return OrderedSet() + + +@ir_dataclass +class TorchBindObject(NonTensorObj): + name: str + value: FakeScriptObject | torch.ScriptObject + + def get_name(self) -> str: + return self.name + + def codegen_reference(self, writer: IndentedBuffer | None = None) -> str: + return self.name + + def get_value(self) -> FakeScriptObject | torch.ScriptObject: + return self.value + + def get_real_obj(self) -> torch.ScriptObject: + if isinstance(self.value, torch.ScriptObject): + return self.value + else: + return self.value.real_obj + + def get_buf_bytes(self) -> int: + # Returns the sum of all tensors in the flattened object + real_script_obj = self.get_real_obj() + + if is_opaque_value(real_script_obj): + return 0 + + assert hasattr(real_script_obj, "__obj_flatten__") + flat_dict = dict(real_script_obj.__obj_flatten__()) + flat_elems = pytree.tree_flatten(flat_dict)[0] + flat_sizes = [ + x.element_size() * x.numel() + for x in flat_elems + if isinstance(x, torch.Tensor) + ] + return functools.reduce(operator.add, flat_sizes, 0) + + +@ir_dataclass +class OpaqueValueTypeConstant(NonTensorObj): + """IR node for opaque value type constants that appear directly in graph outputs. + + Unlike TorchBindObject (which references named constants loaded at runtime), + this inlines the value's repr into the generated code since value types are + reconstructed from their repr. + """ + + value: Any + + def get_name(self) -> str: + return repr(self.value) + + def codegen_reference(self, writer: IndentedBuffer | None = None) -> str: + obj_repr, opaque_types = get_opaque_obj_repr(self.value) + for n, t in opaque_types.items(): + V.graph.opaque_value_type_classes[n] = t + return obj_repr + + +@ir_dataclass +class GeneratorState(NonTensorObj): + name: str + device: torch.device + + def get_name(self) -> str: + return self.name + + def codegen_reference(self, writer: IndentedBuffer | None = None) -> str: + return self.name + + +@ir_dataclass +class OpaqueObjectState(NonTensorObj): + """ + Represents an opaque object (e.g., ProcessGroup) that is passed through + as a graph input. Similar to GeneratorState, this wraps the object with + its placeholder name so codegen can reference it properly. + """ + + name: str + value: Any # The actual opaque object (for reference, not used in codegen) + + def get_name(self) -> str: + return self.name + + def codegen_reference(self, writer: IndentedBuffer | None = None) -> str: + return self.name + + +class _CollectiveKernel(FallbackKernel): + def should_allocate(self) -> bool: + return False + + def has_side_effects(self) -> bool: + return True + + # This is identical to FallbackKernel.set_cpp_kernel(), minus the + # part that checks against input aliasing and mutation. + def set_cpp_kernel_name(self, cpp_kernel_name: str | None = None) -> None: + assert type(self.op_overload) is torch._ops.OpOverload, ( + "Setting cpp kernel needs a valid op_overload" + ) + kernel = self.op_overload + if cpp_kernel_name is not None: + self.cpp_kernel_name = cpp_kernel_name + else: + self.cpp_kernel_name = kernel._schema.name + + self.ordered_kwargs_for_cpp_kernel = [ + x.name for x in kernel._schema.arguments if x.kwarg_only + ] + + # NOTE: [In-Place Collective Safety] + # Between the initiation and completion of an in-place collective, the + # input buffers are subject to both volatile reads and volatile writes. + # They must not be read, written to or reused by another kernel. To ensure + # the constraints, we model collective -> wait_tensor as as two-step + # mutation of the input buffers. + @classmethod + def create_inplace( + cls, + kernel: _OpOverloads, + inputs: IRNode | list[IRNode], + *args: Any, + **kwargs: Any, + ) -> None: + with V.graph.fake_mode: + ( + _example_output, + tensor_args, + non_tensor_args, + unflatten_args, + unbacked_bindings, + ) = cls.process_kernel(kernel, inputs, *args, **kwargs) + assert not unbacked_bindings, f"{kernel} {unbacked_bindings}" + for tensor_arg in tensor_args: + tensor_arg.realize() + V.graph.mark_buffer_mutated(tensor_arg.get_name()) + + device = tensor_args[0].get_device() + packed = cls( + NoneLayout(device=device), + kernel, + tensor_args, + non_tensor_args, + unflatten_args, + ) + + inps = pytree.tree_leaves(inputs) + packed.mutation_outputs.extend( + [MutationOutput(NoneLayout(device=device), buf, packed) for buf in inps] + ) + + # For inplace collective ops, the input is guaranteed to be alias of the returned value of op. + packed.alias_names.extend([inp.get_name() for inp in inps]) + if "out" in kwargs: + packed.mutation_outputs.append( + MutationOutput(NoneLayout(device=device), kwargs["out"], packed) + ) + # For out-variant collective ops, the `out=` arg is guaranteed to be alias of the returned value of op. + packed.alias_names.append(kwargs["out"].get_name()) + + # NOTE: [Out-of-Place Collective Safety] + # Between the initiation and completion of an out-of-place collective: + # + # Input buffers: + # - Are subject to volatile reads + # - Can be read by another kernel + # - Must not be written to or reused by another kernel + # + # Output buffers: + # - Are subject to volatile writes + # - Must not be read, written to or reused by another kernel + # + # To ensure the safety of input buffers without sacrificing read + # availability, we add input buffers as read deps of wait_tensor kernels. + # + # To ensure the safety of output buffers, we model wait_tensor as a + # mutation to the output buffer. Note we also assumes the user program being + # correct and the output buffer is not consumed by kernels other than + # wait_tensor. + # + # TODO(yifu): add a pre-grad pass to validate the correctness of collective + # usage in the user program. + @classmethod + def create_out_of_place( + cls, + kernel: _OpOverloads, + inputs: TensorBox | list[TensorBox], + *args: Any, + **kwargs: Any, + ) -> list[MultiOutput] | _CollectiveKernel: + with V.graph.fake_mode: + ( + example_output, + tensor_args, + non_tensor_args, + unflatten_args, + unbacked_bindings, + ) = cls.process_kernel(kernel, inputs, *args, **kwargs) + assert not unbacked_bindings, f"{kernel}, {unbacked_bindings}" + for tensor_arg in tensor_args: + if not isinstance(tensor_arg, TorchBindObject): + tensor_arg.realize() + + if isinstance(example_output, list): + device = cls.find_device(tensor_args, example_output) + assert device is not None + packed = cls( + MultiOutputLayout(device=device), + kernel, + tensor_args, + non_tensor_args, + unflatten_args, + ) + packed.outputs = [ + MultiOutput( + cls.tensor_to_layout(tensor), + packed, + [(list, i)], + ) + for i, tensor in enumerate(example_output) + ] + for buf, tensor in zip(packed.outputs, example_output): + if config.assume_unaligned_fallback_output or not tensor_is_aligned( + tensor + ): + V.graph.unaligned_buffers.add(buf.name) # type: ignore[arg-type] + return packed.outputs + else: + packed = cls( + cls.tensor_to_layout(example_output), + kernel, + tensor_args, + non_tensor_args, + unflatten_args, + ) + if config.assume_unaligned_fallback_output or not tensor_is_aligned( + example_output + ): + V.graph.unaligned_buffers.add(packed.name) # type: ignore[arg-type] + packed.outputs = [packed] + return packed + + +class _AllReduce_Kernel(_CollectiveKernel): + def __init__( + self, + layout: OutputSpec, + kernel: _OpOverloads, + tensor_args: Sequence[IRNode], + nontensor_args: Sequence[Any], + unflatten_args: Callable[..., Any], + kwargs: dict[str, Any] | None = None, + *, + unbacked_bindings: dict[sympy.Symbol, pytree.KeyPath] | None = None, + ) -> None: + super().__init__( + layout, + kernel, + tensor_args, + nontensor_args, + unflatten_args, + kwargs=None, + unbacked_bindings=unbacked_bindings, + ) + self.set_cpp_kernel_name("aoti_torch_cpu__c10d_functional_all_reduce_") + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + wrapper.include_extra_header("torch/csrc/inductor/aoti_torch/c/shim_cpu.h") + wrapper.generate_extern_kernel_alloc(self) + + if isinstance(self.layout, Layout): + self.codegen_size_asserts(wrapper) + + +class _AllReduceKernel(_CollectiveKernel): + def __init__( + self, + layout: OutputSpec, + kernel: _OpOverloads, + tensor_args: Sequence[IRNode], + nontensor_args: Sequence[Any], + unflatten_args: Callable[..., Any], + kwargs: dict[str, Any] | None = None, + *, + unbacked_bindings: dict[sympy.Symbol, pytree.KeyPath] | None = None, + ) -> None: + super().__init__( + layout, + kernel, + tensor_args, + nontensor_args, + unflatten_args, + kwargs=None, + unbacked_bindings=unbacked_bindings, + ) + self.set_cpp_kernel_name("aoti_torch_cpu__c10d_functional_all_reduce") + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + wrapper.include_extra_header("torch/csrc/inductor/aoti_torch/c/shim_cpu.h") + wrapper.generate_extern_kernel_alloc(self) + + if isinstance(self.layout, Layout): + self.codegen_size_asserts(wrapper) + + +class _WaitKernel(_CollectiveKernel): + def __init__( + self, + layout: OutputSpec, + kernel: _OpOverloads, + tensor_args: Sequence[IRNode], + nontensor_args: Sequence[Any], + unflatten_args: Callable[..., Any], + kwargs: dict[str, Any] | None = None, + *, + unbacked_bindings: dict[sympy.Symbol, pytree.KeyPath] | None = None, + ) -> None: + super().__init__( + layout, + kernel, + tensor_args, + nontensor_args, + unflatten_args, + kwargs=None, + unbacked_bindings=unbacked_bindings, + ) + self.set_cpp_kernel_name("aoti_torch_cpu__c10d_functional_wait_tensor") + + def codegen(self, wrapper: PythonWrapperCodegen) -> None: + wrapper.include_extra_header("torch/csrc/inductor/aoti_torch/c/shim_cpu.h") + wrapper.generate_extern_kernel_alloc(self) + + if isinstance(self.layout, Layout): + self.codegen_size_asserts(wrapper) + + def get_volatile_reads(self) -> Sequence[IRNode]: + inp = self.inputs[0] + assert isinstance(inp, IRNode) + if isinstance(inp, _CollectiveKernel): + # Out-of-place single-output + i = inp.inputs[0] + assert isinstance(i, IRNode), type(i) + return [i] + elif isinstance(inp, MultiOutput): + # This can be two things: + # 1. Out-of-place multi-output coll + # 2. In-place coll with inputs coming from another MultiOutput + coll = inp.inputs[0] + # Case 1 + if isinstance(coll, _CollectiveKernel): + _, idx = inp.indices[0] + + return [coll.inputs[idx]] + # Case 2 + return [] + else: + # In-place requires no additional deps handling for volatile + # reads since the inputs are mutated. + return [] + + @classmethod + def create_wait(cls, kernel: _OpOverloads, inp: TensorBox) -> None: + with V.graph.fake_mode: + ( + _example_output, + tensor_args, + non_tensor_args, + unflatten_args, + unbacked_bindings, + ) = cls.process_kernel(kernel, inp) + assert not unbacked_bindings, f"{kernel} {unbacked_bindings}" + packed = cls( + NoneLayout(device=inp.get_device()), + kernel, + tensor_args, + non_tensor_args, + unflatten_args, + ) + packed.mutation_outputs.append( + MutationOutput(NoneLayout(device=inp.get_device()), inp, packed) + ) + + def get_read_writes(self) -> dependencies.ReadWrites: + read_writes = super().get_read_writes() + # See [Out-of-Place Collective Safety]. + volatile_reads = self.get_volatile_reads() + for vr in volatile_reads: + read_writes.reads.add(dependencies.StarDep(vr.get_name())) + return read_writes + + +# NB: recursive structure here reflects val_to_arg_str, avoid +# calling free_unbacked_symbols on "exotic" types that don't get pexpr +# treatment +def maybe_free_unbacked_symbols(s: object) -> OrderedSet[Symbol]: + if isinstance(s, (SymTypes, Expr)): + # This branch should be impossible in return position + return free_unbacked_symbols(s) + elif isinstance(s, (tuple, list)): + r = OrderedSet[sympy.Symbol]() + for t in s: + r |= maybe_free_unbacked_symbols(t) + return r + elif isinstance(s, torch.Tensor): + # This branch is impossible in constant-args position + return free_unbacked_symbols(s) + else: + return OrderedSet() + + +def maybe_free_symbols(s: object) -> OrderedSet[Symbol]: + if isinstance(s, (SymTypes, Expr)): + # This branch should be impossible in return position + return free_symbols(s) + elif isinstance(s, (tuple, list)): + r = OrderedSet[sympy.Symbol]() + for t in s: + r |= maybe_free_symbols(t) + return r + elif isinstance(s, torch.Tensor): + # This branch is impossible in constant-args position + return free_symbols(s) + else: + return OrderedSet() + + +def assign_origin_node(result: Any, n: torch.fx.Node) -> None: + # This is not complete, but it doesn't have to be: origin_node + # tracking is best effort. The logic here critically relies on direct + # TensorBox -> StorageBox denoting a non-view; we don't bother trying + # to get views to work. Feel free to add any extra cases as needed. + # + # Note: we can't YOLO tree_map over this result, because if there are + # buffers or a view involved, we might not be able to validly assign + # the origin_node here. + if isinstance(result, TensorBox) and isinstance(result.data, StorageBox): + if isinstance(result.data.data, Loops): + result.data.data._post_init_setattr("origin_node", n) + elif isinstance(result.data.data, Buffer): + result.data.data._post_init_setattr("origin_node", n) + if isinstance(result.data.data, ComputedBuffer) and isinstance( + result.data.data.data, Loops + ): + result.data.data.data._post_init_setattr("origin_node", n) + # Not really multi-output, can straightforwardly recurse in + elif ( + isinstance(result.data.data, MultiOutput) + and not result.data.data.indices + ): + if isinstance(result.data.data.inputs[0], Buffer): + result.data.data.inputs[0]._post_init_setattr("origin_node", n) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/jagged_lowerings.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/jagged_lowerings.py new file mode 100644 index 0000000000000000000000000000000000000000..3a8e404133c4ea1ca2976c931fb96ce36e7bb991 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/jagged_lowerings.py @@ -0,0 +1,269 @@ +# mypy: allow-untyped-defs + +import sympy + +import torch + +from .ir import Pointwise, TensorBox +from .virtualized import ops + + +# pyre-ignore[2,3] +def dense_idx_to_jagged_idx(batch_idx, seq_idx, offsets_loader, jagged_len): + # jagged_len + 1 is used as the upper bound, + # because the last sequence length may be zero + begin_idx = ops.indirect_indexing( + offsets_loader([batch_idx]), + jagged_len + 1, + ) + end_idx = offsets_loader([batch_idx + 1]) + jagged_idx = begin_idx + seq_idx + return jagged_idx, end_idx + + +def get_inverse_offsets( + offsets: TensorBox, + jagged_len: int | sympy.Expr, + realize: bool = True, +) -> TensorBox: + """ + Returns "inverse_offsets" - the inverse of the offsets array. + offsets maps batch index (dense) to jagged index (i.e. offset into jagged tensor). + inverse_offsets maps jagged index to batch index. + + e.g. for offsets [0, 3, 4, 9, 10] this will return + inverse_offsets = [0, 0, 0, 1, 2, 2, 2, 2, 2, 3] + + For the given offsets, the computed inverse_offsets are cached + on the first call and reused in the further calls. + """ + + if hasattr(offsets, "inverse_offsets"): + # inverse_offsets are already computed + # for these offsets: can reuse + return offsets.inverse_offsets + + # ops.bucketize takes offsets.get_name() which doesn't exist on Pointwise + # kernels, i.e. we need to realize it before using. In other words, we need + # offsets to be in global memory so that we can binary search over the + # entire tensor + offsets.realize() + device: torch.device = offsets.get_device_or_error() + dtype: torch.dtype = offsets.get_dtype() + + # pyre-ignore[2,3] + def inner_fn(index): + idx = index[0] + bucket = ops.bucketize( + values=ops.index_expr(idx, dtype), + boundaries=( + offsets.get_name(), + offsets.get_size()[-1], + offsets.get_size()[0] * offsets.get_stride()[0], + offsets.get_stride()[-1], + ), + boundary_indices=0, + indexing_dtype=dtype, + right=True, + ) + # ops.bucketize above returns 1-based bucket indices, + # but we need 0-based, hence we subtract 1 from batch + return bucket - 1 + + inverse_offsets = Pointwise.create( + device=device, + dtype=dtype, + inner_fn=inner_fn, + ranges=[jagged_len], + ) + + if realize: + # "freeze" the node so that it doesn't get inlined downstream. + inverse_offsets.realize() + + # cache inverse_offsets for further reuse + offsets.inverse_offsets = inverse_offsets # type: ignore[attr-defined] + + return inverse_offsets + + +def jagged_idx_to_dense_idx( + jagged_idx, # pyre-ignore[2] + inverse_offsets_loader, # pyre-ignore[2] + offsets_loader, # pyre-ignore[2] + batch_size: int | sympy.Expr, + max_seq_len: int | sympy.Expr, + offsets_dtype: torch.dtype, +) -> tuple[sympy.Expr, sympy.Expr]: + batch_idx = ops.indirect_indexing( + inverse_offsets_loader([jagged_idx]), + batch_size + 1, + ) + batch_start = offsets_loader([batch_idx]) + seq = ops.index_expr(jagged_idx, offsets_dtype) - batch_start + # check=False because there may be sequences longer than max_seq_len + seq_idx = ops.indirect_indexing(seq, max_seq_len, check=False) + return batch_idx, seq_idx + + +def register_jagged_ops(): + # Avoid circular import by importing here + from .lowering import fallback_handler, is_integer_type, register_lowering + + # pyre-ignore[56] + @register_lowering(torch.ops.aten._jagged_to_padded_dense_forward.default) + def _jagged_to_padded_dense_forward( + jagged_values: TensorBox, + jagged_offsets: list[TensorBox], + max_lengths: list[int], # list of ints/SymInts + padding_value: float = 0.0, + ) -> TensorBox: + device = jagged_values.get_device_or_error() + dtype = jagged_values.get_dtype() + + jagged_values_size = jagged_values.get_size() + + # only handle the common case of a single jagged dimension + if ( + len(jagged_offsets) != 1 + or device.type != "cuda" + or device != jagged_offsets[0].get_device() + or len(jagged_values_size) != 2 + or len(jagged_offsets[0].get_size()) != 1 + or len(max_lengths) != len(jagged_offsets) + or not is_integer_type(jagged_offsets[0]) + ): + return fallback_handler( + torch.ops.aten._jagged_to_padded_dense_forward.default, + add_to_fallback_set=False, + )( + jagged_values, + jagged_offsets, + max_lengths, + padding_value, + ) + + offsets: TensorBox = jagged_offsets[0] # type: ignore[assignment] + offsets_len = offsets.get_size()[0] + offsets_dtype = offsets.get_dtype() + batch_size = offsets_len - 1 + max_seq_len = max_lengths[0] + embedding_len = jagged_values_size[1] + jagged_len = jagged_values_size[0] + + output_size = [batch_size, max_seq_len, embedding_len] + + values_loader = jagged_values.make_loader() + offsets_loader = offsets.make_loader() + + # pyre-ignore[2,3,53] + def inner_fn(index): + # dense tensor size: [B, N, D] + batch_idx, seq_idx, emb_idx = index + jagged_idx, end_idx = dense_idx_to_jagged_idx( + batch_idx=batch_idx, + seq_idx=seq_idx, + offsets_loader=offsets_loader, + jagged_len=jagged_len, + ) + return ops.masked( + ops.lt( + ops.index_expr(jagged_idx, offsets_dtype), + end_idx, + ), + lambda: values_loader([jagged_idx, emb_idx]), + padding_value, + ) + + return Pointwise.create( + device=device, + dtype=dtype, + inner_fn=inner_fn, + ranges=output_size, + ) + + def _dense_to_jagged_forward_impl( + fallback_op, # pyre-ignore[2] + dense: TensorBox, + jagged_offsets: list[TensorBox], + jagged_len: int | None = None, + ) -> TensorBox: + device = dense.get_device_or_error() + dtype = dense.get_dtype() + + dense_size = dense.get_size() + + # only handle the common case of a single jagged dimension + if ( + len(jagged_offsets) != 1 + or device.type != "cuda" + or device != jagged_offsets[0].get_device() + or len(jagged_offsets[0].get_size()) != 1 + or len(dense_size) != 3 + or jagged_len is None + or not is_integer_type(jagged_offsets[0]) + ): + return fallback_handler(fallback_op, add_to_fallback_set=False)( + dense, + jagged_offsets, + jagged_len, + ) + + offsets: TensorBox = jagged_offsets[0] # type: ignore[assignment] + offsets_dtype = offsets.get_dtype() + batch_size = dense_size[0] + max_seq_len = dense_size[1] + embedding_len = dense_size[-1] + + output_size = [jagged_len, embedding_len] + + dense_loader = dense.make_loader() + offsets_loader = offsets.make_loader() + + inverse_offsets = get_inverse_offsets( + offsets=offsets, + jagged_len=jagged_len, + ) + inverse_offsets_loader = inverse_offsets.make_loader() + + # pyre-ignore[2,3,53] + def inner_fn(index): + # jagged tensor size: [sum_B(N_B), D] + jagged_idx, emb_idx = index + batch_idx, seq_idx = jagged_idx_to_dense_idx( + jagged_idx=jagged_idx, + offsets_loader=offsets_loader, + inverse_offsets_loader=inverse_offsets_loader, + batch_size=batch_size, + max_seq_len=max_seq_len, + offsets_dtype=offsets_dtype, + ) + return ops.masked( + ops.lt( + ops.index_expr(seq_idx, offsets_dtype), + ops.index_expr(max_seq_len, offsets_dtype), + ), + lambda: dense_loader([batch_idx, seq_idx, emb_idx]), + 0.0, # jagged sequence longer than max_seq_len + ) + + return Pointwise.create( + device=device, + dtype=dtype, + inner_fn=inner_fn, + ranges=output_size, + ) + + # pyre-ignore[56] + @register_lowering(torch.ops.aten._padded_dense_to_jagged_forward) + def _dense_to_jagged_forward( + dense: TensorBox, + jagged_offsets: list[TensorBox], + jagged_len: int | None = None, + ) -> TensorBox: + return _dense_to_jagged_forward_impl( + fallback_op=torch.ops.aten._padded_dense_to_jagged_forward.default, + dense=dense, + jagged_offsets=jagged_offsets, + jagged_len=jagged_len, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9668f1b6c6e1d07c9a2744ab3894929826f39429 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/__init__.py @@ -0,0 +1 @@ +from . import flex, mm, mm_common, mm_plus_mm diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/bmm.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/bmm.py new file mode 100644 index 0000000000000000000000000000000000000000..55e6bcf82810b50ee05a2be775ede6f297e96245 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/bmm.py @@ -0,0 +1,294 @@ +# mypy: allow-untyped-defs +import logging +from typing import TYPE_CHECKING + +import torch +from torch._dynamo.utils import counters +from torch._inductor.codegen.rocm.ck_universal_gemm_template import CKGemmTemplate +from torch._inductor.kernel.mm_common import load_kernel_template + +from .. import config as inductor_config, ir, lowering as L +from ..kernel_inputs import MMKernelInputs +from ..lowering import lowerings, make_pointwise, make_reduction, transform_args +from ..runtime.runtime_utils import get_max_y_grid +from ..select_algorithm import ( + autotune_select_algorithm, + ExternKernelChoice, + SymbolicGridFn, + TritonTemplate, +) +from ..utils import ( + _use_cutlass_for_op, + use_aten_gemm_kernels, + use_ck_gemm_template, + use_cpp_bmm_template, + use_cutlass_template, + use_nv_universal_gemm_template, + use_triton_template, +) +from ..virtualized import ops, V +from .mm_common import ( + _is_static_problem, + is_batch_stride_largest_or_zero, + mm_args, + use_native_matmul, +) + + +if TYPE_CHECKING: + from ..ir import ChoiceCaller + from ..select_algorithm import KernelTemplate + +log = logging.getLogger(__name__) +aten = torch.ops.aten + + +@SymbolicGridFn +def bmm_grid(b, m, n, meta, *, cdiv, max): + tiles = cdiv(m, meta["BLOCK_M"]) * cdiv(n, meta["BLOCK_N"]) + # Split batch across grid_y and grid_z to avoid exceeding CUDA grid_y limit. + # When b <= max_y_grid, grid_z = 1 and behavior is identical to the original. + max_y_grid = get_max_y_grid() + grid_z = max(cdiv(b, max_y_grid), 1) + grid_y = cdiv(b, grid_z) + return (tiles, grid_y, grid_z) + + +# We define each template kernel in a separate file which is the name of the input to load_kernel_template +# (e.g. triton_bmm for templates/triton_bmm.py.jinja). +# If you are adding a new template, please follow that pattern and add a new file with your implementation in the templates folder. +bmm_template = TritonTemplate( + name="bmm", + grid=bmm_grid, + source=load_kernel_template("triton_bmm"), + cache_codegen_enabled_for_template=True, +) + +aten_bmm = ExternKernelChoice(torch.bmm, "at::bmm_out", op_overload=aten.bmm.out) +aten_bmm_dtype = ExternKernelChoice( + torch.bmm, + "at::_bmm_out_dtype_xpu" if torch.xpu._is_compiled() else "at::_bmm_out_dtype_cuda", + name="bmm_dtype", + op_overload=aten.bmm.dtype_out, +) +aten_baddbmm = ExternKernelChoice( + torch.baddbmm, "at::baddbmm_out", op_overload=aten.baddbmm.out +) + + +@L.register_lowering(aten.bmm) +def tuned_bmm(mat1, mat2, out_dtype=None, *, layout=None): + """ + Lowering for autotuning aten.bmm with different backends (Aten, Triton, CUTLASS, etc.) + """ + if all(x.get_device().type == "cpu" for x in [mat1, mat2]): + # decompose to small ops when memory bound + if mat1.get_size()[1] == 1 or mat2.get_size()[2] == 1: + mat1 = L.unsqueeze(mat1, -1) + mat2 = L.unsqueeze(mat2, 1) + return L.sum_(L.mul(mat1, mat2), axis=2) + + def is_valid_to_require_contiguous(t): + if not ir.is_storage_and_layout(t): + return True + _, layout = ir.as_storage_and_layout(t, freeze=False) + return isinstance(layout, ir.FlexibleLayout) + + def is_preferred_layout_as_bmm_input(sizes, strides): + # contiguous on one of the last two dims + return ( + strides[-1] == 1 and (sizes[-2] == 1 or strides[-2] >= sizes[-1]) + ) or (strides[-2] == 1 and (sizes[-1] == 1 or strides[-1] >= sizes[-2])) + + # Make the input of bmm contiguous + # if it is not contiguous on either of the last two dims, + # because bmm cpu implementation would do contiguous() if not. + # This is to avoid additional copies in bmm. + def may_require_contiguous(t, meta_t): + sizes = meta_t.meta["val"].size() + strides = meta_t.meta["val"].stride() + if not is_preferred_layout_as_bmm_input(sizes, strides): + t = ir.ExternKernel.require_contiguous(t) + return t + + if is_valid_to_require_contiguous(mat1): + meta_mat1 = V.graph.current_node.args[0] + mat1 = may_require_contiguous(mat1, meta_mat1) + if is_valid_to_require_contiguous(mat2): + meta_mat2 = V.graph.current_node.args[1] + mat2 = may_require_contiguous(mat2, meta_mat2) + + if use_native_matmul(mat1, mat2): + mat1 = lowerings[aten.unsqueeze](mat1, -1) + mat2 = lowerings[aten.unsqueeze](mat2, 1) + args, kwargs = transform_args( + args=[mat1, mat2], + kwargs={}, + broadcast=True, + type_promotion_kind=None, + convert_input_to_bool=False, + ) # Handles broadcasting the arguments + + if inductor_config.triton.codegen_upcast_to_fp32 and mat1.dtype in [ + torch.float16, + torch.bfloat16, + ]: + + def _to_dtype(x): + return ops.to_dtype(x, mat1.dtype, use_compute_types=False) + + args = [make_pointwise(_to_dtype)(x) for x in args] + + mul_pointwise = make_pointwise(ops.dot)(*args) + dot_reduction = make_reduction("dot")(mul_pointwise, 2) + + return dot_reduction + + # TODO(coconutruben): integrate into MMKernelInputs when all callsites use that + m, n, k, layout, mat1, mat2 = mm_args( + mat1, mat2, layout=layout, out_dtype=out_dtype + ) + name = "bmm" + + # Create MMKernelInputs for BMM at the top + kernel_inputs = MMKernelInputs([mat1, mat2], out_dtype=out_dtype) + + # below is for getting an overview logging info of inductor mms + batch_size = mat1.get_size()[0] # Extract batch dimension + counters["aten_mm_info"][f"aten.bmm_{batch_size}_{m}_{n}_{k}"] += 1 + log.info( + "Tuned aten.bmm: batch=%s, m=%s, n=%s, k=%s, mat1_dtype=%s, mat2_dtype=%s, output_layout=%s", + batch_size, + m, + n, + k, + mat1.get_dtype(), + mat2.get_dtype(), + layout, + ) + + aten_handler: ExternKernelChoice = aten_bmm + aten_extra_kwargs = {} + if out_dtype: + assert mat1.get_device().type in ("cuda", "xpu"), ( + "out_dtype is only supported for CUDA or XPU" + ) + aten_handler = aten_bmm_dtype + aten_extra_kwargs = {"out_dtype": out_dtype} + + choices: list[ChoiceCaller] = [] + + # Collect all templates for unified call + templates_to_use: list[ExternKernelChoice | KernelTemplate] = [] + kwarg_overrides = {} + + if use_aten_gemm_kernels(): + templates_to_use.append(aten_handler) + kwarg_overrides[aten_handler.uid] = aten_extra_kwargs + + if use_triton_template(layout, check_max_autotune=False): + templates_to_use.append(bmm_template) + + # Single unified call for all templates + choices.extend( + V.choices.get_template_configs( + kernel_inputs, + templates_to_use, + name, + kwarg_overrides=kwarg_overrides, + ) + ) + _, is_nonzero = _is_static_problem(layout) + batch_stride_largest_or_zero = is_batch_stride_largest_or_zero(mat1, mat2, layout) + if ( + batch_stride_largest_or_zero + and is_nonzero + and use_cutlass_template(layout, m, n, k) + and _use_cutlass_for_op(name) + ): + from ..codegen.cutlass.gemm_template import CUTLASS3xGemmTemplate + + CUTLASS3xGemmTemplate.add_cutlass_gemm_choices( + choices, layout, kernel_inputs.nodes() + ) # type: ignore[arg-type] + + if use_cpp_bmm_template(layout, mat1, mat2): + from ..codegen.cpp_bmm_template import CppBmmTemplate + + CppBmmTemplate.add_choices( + choices, + layout, + kernel_inputs.nodes(), + ) + + if use_ck_gemm_template(layout, m, n, k): + CKGemmTemplate.add_ck_gemm_choices(choices, layout, kernel_inputs.nodes()) + + if is_nonzero and use_nv_universal_gemm_template(layout, m, n, k, mat1, mat2): + from ..codegen.nv_universal_gemm import add_nv_universal_gemm_choices + + add_nv_universal_gemm_choices(choices, layout, kernel_inputs) + + node, _ = autotune_select_algorithm(name, choices, kernel_inputs.nodes(), layout) + return node + + +@L.register_lowering(aten.baddbmm) +def tuned_baddbmm(inp, mat1, mat2, *, alpha=1, beta=1, layout=None): + """ + Lowering for autotuning aten.mm with different backends (Aten, Triton, CUTLASS, etc.) + """ + if use_native_matmul(mat1, mat2): + if beta == 0: + arg1 = 0 + else: + arg1 = lowerings[aten.mul](beta, inp) + + if alpha == 0: + arg2 = 0 + else: + arg2 = lowerings[aten.mul](alpha, lowerings[aten.bmm](mat1, mat2)) + + return lowerings[aten.add](arg1, arg2) + + # TODO(coconutruben): integrate into MMKernelInputs when all callsites use that + m, n, k, layout, mat1, mat2, inp = mm_args(mat1, mat2, inp, layout=layout) + + # Create MMKernelInputs for BadDBMM at the top + kernel_inputs = MMKernelInputs( + [inp, mat1, mat2], scalars=dict(alpha=alpha, beta=beta) + ) + + # below is for getting an overview logging info of inductor mms + batch_size = mat1.get_size()[0] + counters["aten_mm_info"][f"aten.baddbmm_{batch_size}_{m}_{n}_{k}"] += 1 + log.info( + "Tuned aten.baddbmm: batch_size=%s, m=%s, n=%s, k=%s, mat1_dtype=%s, mat2_dtype=%s, inp=%s, output_layout=%s", + batch_size, + m, + n, + k, + mat1.get_dtype(), + mat2.get_dtype(), + inp.get_dtype(), + layout, + ) + name = "baddbmm" + # options to tune from + choices: list[ChoiceCaller] = [] + + # Collect all templates for unified call + templates_to_use: list[ExternKernelChoice | KernelTemplate] = [] + if use_aten_gemm_kernels(): + templates_to_use.append(aten_baddbmm) + + if use_triton_template(layout, check_max_autotune=False): + templates_to_use.append(bmm_template) + + # Single unified call for all templates + choices.extend( + V.choices.get_template_configs(kernel_inputs, templates_to_use, name) + ) + + node, _ = autotune_select_algorithm(name, choices, kernel_inputs.nodes(), layout) + return node diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/conv.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/conv.py new file mode 100644 index 0000000000000000000000000000000000000000..1fa9588a7bf633ae463241617e626d0e824c9d4a --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/conv.py @@ -0,0 +1,740 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, TypedDict + +import torch +from torch._inductor.codegen.rocm.ck_conv_template import CKGroupedConvFwdTemplate + +from .. import config, ir +from ..lowering import ( + add_layout_constraint, + constrain_to_fx_strides, + lowerings as L, + register_lowering, +) +from ..select_algorithm import ( + autotune_select_algorithm, + ExternKernelChoice, + SymbolicGridFn, + TritonTemplate, +) +from ..utils import ( + is_ones, + is_zeros, + pad_listlike, + sympy_product, + use_ck_conv_template, + use_triton_template, +) +from ..virtualized import V +from .mm_common import load_kernel_template + + +if TYPE_CHECKING: + from collections.abc import Sequence + + from ..ir import TensorBox + +log = logging.getLogger(__name__) + + +aten = torch.ops.aten + + +@SymbolicGridFn +def conv2d_grid(n, c, h, w, meta, *, cdiv): + return ( + cdiv(n * h * w, meta["BLOCK_M"]), + cdiv(c, meta["BLOCK_N"]), + meta["GROUPS"], + ) + + +@SymbolicGridFn +def conv3d_grid(n, c, d, h, w, meta, *, cdiv): + return ( + cdiv(n * d * h * w, meta["BLOCK_M"]), + cdiv(c, meta["BLOCK_N"]), + meta["GROUPS"], + ) + + +# ============================================================================= +# Depthwise conv1d (groups == in_channels == out_channels) +# Uses direct element-wise multiply-accumulate instead of implicit GEMM. +# Channels-last (NLC) layout with 3D tiling: BLOCK_N x BLOCK_L x BLOCK_C. +# ============================================================================= + + +@SymbolicGridFn +def depthwise_conv1d_grid(n, c, l, meta, *, cdiv): + return ( + cdiv(n, meta["BLOCK_N"]), + cdiv(l, meta["BLOCK_L"]), + cdiv(c, meta["BLOCK_C"]), + ) + + +depthwise_conv1d_template = TritonTemplate( + name="depthwise_conv1d", + grid=depthwise_conv1d_grid, + source=load_kernel_template("triton_depthwise_conv"), + cache_codegen_enabled_for_template=True, +) + +LOOP_BODY_2D = """ + idx_x_h = i - PADDING_H + idx_y_h * STRIDE_H + idx_x_w = j - PADDING_W + idx_y_w * STRIDE_W + idx_x_c = tl.arange(0, BLOCK_K) + k + + x_ptrs = x_base + ( + (idx_x_h * stride_xh)[:, None] + + (idx_x_w * stride_xw)[:, None] + + (idx_x_c * stride_xc)[None, :] + ) + mask_x = ( + (idx_n < BATCH)[:, None] + & (idx_x_h >= 0)[:, None] + & (idx_x_h < IN_H)[:, None] + & (idx_x_w >= 0)[:, None] + & (idx_x_w < IN_W)[:, None] + & (idx_x_c < GROUP_IN_C)[None, :] + ) + matrix_x = tl.load(x_ptrs, mask=mask_x, other=0.0) + + w_ptrs = w_base + ( + (idx_x_c * stride_wc_in)[:, None] + (i * stride_wh) + (j * stride_ww) + ) + mask_w = (idx_x_c[:, None] < GROUP_IN_C) & (idx_y_c[None, :] < GROUP_OUT_C) + matrix_w = tl.load(w_ptrs, mask=mask_w, other=0.0) + acc += tl.dot(matrix_x, matrix_w, allow_tf32=ALLOW_TF32) +""" + +""" +This is a relatively simple conv implementation that can likely be +improved. Many alternate conv versions can be found here: +https://github.com/pytorch/torchdynamo/pull/971 +""" +conv2d_template = TritonTemplate( + name="convolution2d", + grid=conv2d_grid, + source=r""" +{{def_kernel("X", "W")}} + # Tensor dimensions + BATCH = {{size("X", 0)}} + IN_C = {{size("X", 1)}} + IN_H = {{size("X", 2)}} + IN_W = {{size("X", 3)}} + OUT_C = {{size(None, 1)}} + OUT_H = {{size(None, 2)}} + OUT_W = {{size(None, 3)}} + + # Strides: + stride_xn = {{stride("X", 0)}} + stride_xc = {{stride("X", 1)}} + stride_xh = {{stride("X", 2)}} + stride_xw = {{stride("X", 3)}} + stride_wc_out = {{stride("W", 0)}} + stride_wc_in = {{stride("W", 1)}} + stride_wh = {{stride("W", 2)}} + stride_ww = {{stride("W", 3)}} + + nhw = tl.program_id(0).to(INDEX_DTYPE) * BLOCK_M + tl.arange(0, BLOCK_M) + idx_y_w = nhw % OUT_W + nh = nhw // OUT_W + idx_y_h = nh % OUT_H + idx_n = nh // OUT_H + idx_y_c = tl.program_id(1).to(INDEX_DTYPE) * BLOCK_N + tl.arange(0, BLOCK_N) + +{% if GROUPS == 1 %} + group = 0 + GROUP_IN_C = IN_C + GROUP_OUT_C = OUT_C +{% else %} + group = tl.program_id(2).to(INDEX_DTYPE) + GROUP_IN_C = IN_C // GROUPS + GROUP_OUT_C = OUT_C // GROUPS +{% endif %} + + x_base = X + (group * stride_xc * GROUP_IN_C + idx_n * stride_xn)[:, None] + w_base = ( + W + (group * stride_wc_out * GROUP_OUT_C + idx_y_c * stride_wc_out)[None, :] + ) + + acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32) + +{% if UNROLL %} +{% for i in range(KERNEL_H) %} +{% for j in range(KERNEL_W) %} + i = {{i}} + j = {{j}} + for k in range(0, GROUP_IN_C, BLOCK_K): + """ + + LOOP_BODY_2D + + """ +{% endfor %} +{% endfor %} +{% else %} + # Could be simplified, but slightly slower: + # for i in range(KERNEL_H): + # for j in range(KERNEL_W): + # for k in range(0, GROUP_IN_C, BLOCK_K): + BLOCK_K_COUNT = (GROUP_IN_C + BLOCK_K - 1) // BLOCK_K + for ijk in range(KERNEL_H * KERNEL_W * BLOCK_K_COUNT): + k = (ijk % BLOCK_K_COUNT) * BLOCK_K + ij = ijk // BLOCK_K_COUNT + i = ij // KERNEL_W + j = ij % KERNEL_W + """ + + LOOP_BODY_2D + + """ +{% endif %} + + mask = ( + (idx_n < BATCH)[:, None] + & (idx_y_h < OUT_H)[:, None] + & (idx_y_w < OUT_W)[:, None] + & (idx_y_c < GROUP_OUT_C)[None, :] + ) + idx_n = idx_n[:, None] + idx_c = idx_y_c[None, :] + group * GROUP_OUT_C + idx_h = idx_y_h[:, None] + idx_w = idx_y_w[:, None] + + # inductor generates a suffix + {{store_output(("idx_n", "idx_c", "idx_h", "idx_w"), "acc", "mask", val_shape=("BLOCK_M", "BLOCK_N"))}} +""", +) + +LOOP_BODY_3D = """ + idx_x_d = d - PADDING_D + idx_y_d * STRIDE_D + idx_x_h = i - PADDING_H + idx_y_h * STRIDE_H + idx_x_w = j - PADDING_W + idx_y_w * STRIDE_W + idx_x_c = tl.arange(0, BLOCK_K) + k + + x_ptrs = x_base + ( + (idx_x_d * stride_xd)[:, None] + + (idx_x_h * stride_xh)[:, None] + + (idx_x_w * stride_xw)[:, None] + + (idx_x_c * stride_xc)[None, :] + ) + mask_x = ( + (idx_n < BATCH)[:, None] + & (idx_x_d >= 0)[:, None] + & (idx_x_d < IN_D)[:, None] + & (idx_x_h >= 0)[:, None] + & (idx_x_h < IN_H)[:, None] + & (idx_x_w >= 0)[:, None] + & (idx_x_w < IN_W)[:, None] + & (idx_x_c < GROUP_IN_C)[None, :] + ) + matrix_x = tl.load(x_ptrs, mask=mask_x, other=0.0) + + w_ptrs = w_base + ( + (idx_x_c * stride_wc_in)[:, None] + + (d * stride_wd) + (i * stride_wh) + (j * stride_ww) + ) + mask_w = (idx_x_c[:, None] < GROUP_IN_C) & (idx_y_c[None, :] < GROUP_OUT_C) + matrix_w = tl.load(w_ptrs, mask=mask_w, other=0.0) + acc += tl.dot(matrix_x, matrix_w, allow_tf32=ALLOW_TF32) +""" + +conv3d_template = TritonTemplate( + name="convolution3d", + grid=conv3d_grid, + source=r""" +{{def_kernel("X", "W")}} + # Tensor dimensions + BATCH = {{size("X", 0)}} + IN_C = {{size("X", 1)}} + IN_D = {{size("X", 2)}} + IN_H = {{size("X", 3)}} + IN_W = {{size("X", 4)}} + OUT_C = {{size(None, 1)}} + OUT_D = {{size(None, 2)}} + OUT_H = {{size(None, 3)}} + OUT_W = {{size(None, 4)}} + + # Strides: + stride_xn = {{stride("X", 0)}} + stride_xc = {{stride("X", 1)}} + stride_xd = {{stride("X", 2)}} + stride_xh = {{stride("X", 3)}} + stride_xw = {{stride("X", 4)}} + stride_wc_out = {{stride("W", 0)}} + stride_wc_in = {{stride("W", 1)}} + stride_wd = {{stride("W", 2)}} + stride_wh = {{stride("W", 3)}} + stride_ww = {{stride("W", 4)}} + + ndhw = tl.program_id(0).to(INDEX_DTYPE) * BLOCK_M + tl.arange(0, BLOCK_M) + idx_y_w = ndhw % OUT_W + ndh = ndhw // OUT_W + idx_y_h = ndh % OUT_H + nd = ndh // OUT_H + idx_y_d = nd % OUT_D + idx_n = nd // OUT_D + idx_y_c = tl.program_id(1).to(INDEX_DTYPE) * BLOCK_N + tl.arange(0, BLOCK_N) + +{% if GROUPS == 1 %} + group = 0 + GROUP_IN_C = IN_C + GROUP_OUT_C = OUT_C +{% else %} + group = tl.program_id(2).to(INDEX_DTYPE) + GROUP_IN_C = IN_C // GROUPS + GROUP_OUT_C = OUT_C // GROUPS +{% endif %} + + x_base = X + (group * stride_xc * GROUP_IN_C + idx_n * stride_xn)[:, None] + w_base = ( + W + (group * stride_wc_out * GROUP_OUT_C + idx_y_c * stride_wc_out)[None, :] + ) + + acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32) + +{% if UNROLL %} +{% for d in range(KERNEL_D) %} +{% for i in range(KERNEL_H) %} +{% for j in range(KERNEL_W) %} + d = {{d}} + i = {{i}} + j = {{j}} + for k in range(0, GROUP_IN_C, BLOCK_K): + """ + + LOOP_BODY_3D + + """ +{% endfor %} +{% endfor %} +{% endfor %} +{% else %} + # Could be simplified, but slightly slower: + # for d in range(KERNEL_D): + # for i in range(KERNEL_H): + # for j in range(KERNEL_W): + # for k in range(0, GROUP_IN_C, BLOCK_K): + BLOCK_K_COUNT = (GROUP_IN_C + BLOCK_K - 1) // BLOCK_K + for dijk in range(KERNEL_D * KERNEL_H * KERNEL_W * BLOCK_K_COUNT): + k = (dijk % BLOCK_K_COUNT) * BLOCK_K + dij = dijk // BLOCK_K_COUNT + j = dij % KERNEL_W + di = dij // KERNEL_W + i = di % KERNEL_H + d = di // KERNEL_H + """ + + LOOP_BODY_3D + + """ +{% endif %} + + mask = ( + (idx_n < BATCH)[:, None] + & (idx_y_d < OUT_D)[:, None] + & (idx_y_h < OUT_H)[:, None] + & (idx_y_w < OUT_W)[:, None] + & (idx_y_c < GROUP_OUT_C)[None, :] + ) + idx_n = idx_n[:, None] + idx_c = idx_y_c[None, :] + group * GROUP_OUT_C + idx_d = idx_y_d[:, None] + idx_h = idx_y_h[:, None] + idx_w = idx_y_w[:, None] + + # inductor generates a suffix + {{store_output(("idx_n", "idx_c", "idx_d", "idx_h", "idx_w"), "acc", "mask", val_shape=("BLOCK_M", "BLOCK_N"))}} +""", +) + +aten_convolution = ExternKernelChoice( + torch.convolution, + "at::convolution", + has_out_variant=False, + op_overload=aten.convolution.default, +) + + +def conv1x1_via_mm(x, w, *, out): + w = torch.squeeze(torch.squeeze(w, -1), -1) + return torch.matmul( + x.permute(0, 2, 3, 1), w.permute(1, 0), out=out.permute(0, 2, 3, 1) + ) + + +aten_conv1x1_via_mm = ExternKernelChoice(conv1x1_via_mm, None) + + +class ConvLayoutParams(TypedDict): + stride: tuple[int, ...] + padding: tuple[int, ...] + dilation: tuple[int, ...] + transposed: bool + output_padding: tuple[int, ...] + groups: int + + +def conv_layout( + x: TensorBox, + weight: TensorBox, + bias: TensorBox | None, + stride: Sequence[int], + padding: tuple[int, ...], + dilation: tuple[int, ...], + transposed: bool, + output_padding: tuple[int, ...], + groups: int, +) -> ir.Layout: + """Determine output layout for a convolution""" + # We use guard_int_seq rather than size_hints because the output shape + # depends on these values — if they ever contained symbols, size_hints + # would silently substitute a hint that could be wrong, producing an + # incorrect layout. guard_int_seq will install a proper guard instead. + # Note: stride and padding are already guarded via guard_int_seq in + # convolution() above, but we guard all four here so conv_layout is + # self-contained and doesn't rely on callers. + guard = V.graph.sizevars.guard_int_seq + with V.graph.fake_mode: + output = torch.ops.aten.convolution( + ir.ir_node_to_tensor(x), + ir.ir_node_to_tensor(weight), + ir.ir_node_to_tensor(bias), + guard(stride), + guard(padding), + guard(dilation), + transposed, + guard(output_padding), + groups, + ) + sizes = ir.convert_shape_to_inductor(output.size()) + stride = ir.convert_shape_to_inductor(output.stride()) # type: ignore[assignment] + + return ir.FixedLayout( + x.get_device_or_error(), + x.get_dtype(), + sizes, + stride, + ) + + +def channels_last_order(rank): + order = list(reversed(range(rank))) + order.insert(1, order.pop(-1)) + return order + + +def convert_1x1_conv_to_mm(x, weight, bias): + # special case for 1x1 convolution, which is actually just a matmul + rank = len(weight.get_size()) + for _ in range(rank - 2): + weight = L[aten.squeeze](weight, dim=-1) + weight = L[aten.permute](weight, [1, 0]) + + x = ir.ExternKernel.require_stride_order(x, channels_last_order(rank)) + x_permute = list(range(rank)) + x_permute.append(x_permute.pop(1)) + x = L[aten.permute](x, x_permute) + *sizes, in_chan = x.get_size() + x = L[aten.reshape](x, [sympy_product(sizes), in_chan]) + if bias is None: + result = L[aten.mm](x, weight) + else: + result = L[aten.addmm](bias, x, weight) + result = L[aten.reshape](result, [*sizes, -1]) + result_permute = list(range(rank)) + result_permute.insert(1, result_permute.pop(-1)) + return L[aten.permute](result, result_permute) + + +@register_lowering(aten.convolution) +def convolution( + x: TensorBox, + weight: TensorBox, + bias: TensorBox | None, + stride: Sequence[int], + padding: Sequence[int], + dilation: Sequence[int], + transposed: bool, + output_padding: Sequence[int], + groups: int, +): + stride = tuple(stride) + padding = tuple(padding) + dilation = tuple(dilation) + output_padding = tuple(output_padding) + if not isinstance(groups, int): + groups = V.graph.sizevars.guard_int(groups) + assert isinstance(groups, int) + + # Need use hint for triton template since the template does not + # work with a dynamic shape. + # + # No need to guard_int for dilation and output_padding + # since the template is only used when dilation is 1 and output_padding + # is 0. + stride = tuple(V.graph.sizevars.guard_int_seq(stride)) + padding = tuple(V.graph.sizevars.guard_int_seq(padding)) + + kwargs: ConvLayoutParams = { + "stride": stride, + "padding": padding, + "dilation": dilation, + "transposed": transposed, + "output_padding": output_padding, + "groups": groups, + } + + device_type = ir.get_device_type(x) + + if len(x.get_size()) == len(weight.get_size()) - 1: + # add batch dimension to simplify rest of function + return L[aten.squeeze]( + convolution(L[aten.expand](x, [1, *x.get_size()]), weight, bias, **kwargs), + dim=0, + ) + + out_chan, in_chan, *kernel_shape = V.graph.sizevars.guard_int_seq(weight.get_size()) + + # Always convert conv1D to 2D for Intel GPU. + # Only conv2D can be converted to channel last layout, + # which have much better performance. + if len(x.get_size()) == 3 and len(kernel_shape) == 1 and device_type == "xpu": + kwargs.update( + { + "stride": (1,) + stride, + "padding": (0,) + padding, + "dilation": (1,) + dilation, + "output_padding": (0,) + output_padding, + } + ) + # (N, C, L) -> (N, C, 1, L) + x = L[aten.unsqueeze](x, dim=2) + weight = L[aten.unsqueeze](weight, dim=2) + + return L[aten.squeeze]( + convolution(x, weight, bias, **kwargs), + dim=2, + ) + + ndim = len(kernel_shape) + stride = pad_listlike(stride, ndim) + padding = pad_listlike(padding, ndim) + dilation = pad_listlike(dilation, ndim) + output_padding = pad_listlike(output_padding, ndim) + + def channels_last_conv(): + if V.graph.layout_opt and ndim == 2: + return True + + layout = conv_layout(x, weight, None, **kwargs) + # TODO: This does not guard on the stride order decision, + # shall we use optimization_hint to handle unbacked? + req_stride_order = ir.get_stride_order( + V.graph.sizevars.guarding_hints_or_throw(layout.stride) + ) + return req_stride_order == ir.NHWC_STRIDE_ORDER + + autotuning_gemm = config.max_autotune or config.max_autotune_gemm + + if ( + (config.conv_1x1_as_mm or (autotuning_gemm and channels_last_conv())) + and is_ones(kernel_shape) + and is_ones(stride) + and is_zeros(padding) + and is_ones(dilation) + and not transposed + and is_zeros(output_padding) + and groups == 1 + and V.graph.sizevars.statically_known_gt(sympy_product(x.get_size()), 0) + ): + return convert_1x1_conv_to_mm(x, weight, bias) + + if bias is not None and device_type != "cpu": + # peel off the bias, cudnn is slower with it + result = convolution(x, weight, None, **kwargs) + return L[aten.add]( + result, L[aten.view](bias, [result.get_size()[1]] + ndim * [1]) + ) + + x.realize() + weight.realize() + + # ndim can be 1 for convolution in models such as demucs + # TODO: check if it's beneficial to convert Conv1d to Conv2d and then + # apply channels last. + if V.graph.layout_opt and ndim == 2: + V.graph.num_channels_last_conv += 1 + x = ir.ExternKernel.require_channels_last(x) # type: ignore[assignment] + # TODO maybe we can convert weights to channels last just once before + # running the model. + weight = ir.ExternKernel.require_channels_last(weight) # type: ignore[assignment] + layout = conv_layout(x, weight, None, **kwargs) + else: + layout = conv_layout(x, weight, None, **kwargs) + # TODO: This does not guard on the stride order decision, + # shall we use optimization_hint to handle unbacked? + req_stride_order = ir.get_stride_order( + V.graph.sizevars.guarding_hints_or_throw(layout.stride) + ) + x = ir.ExternKernel.require_stride_order(x, req_stride_order) # type: ignore[assignment] + weight = ir.ExternKernel.require_stride_order(weight, req_stride_order) # type: ignore[assignment] + + ordered_kwargs_for_cpp_kernel = [ + "stride", + "padding", + "dilation", + "transposed", + "output_padding", + "groups", + ] + if bias is None: + args = [x, weight] + kwargs["bias"] = None # type: ignore[typeddict-unknown-key] + ordered_kwargs_for_cpp_kernel.insert(0, "bias") + else: + args = [x, weight, bias] + bias.realize() + bias.freeze_layout() + V.graph.sizevars.guard_int_seq(bias.get_size()) + + choices = [] + if torch._inductor.utils._use_conv_autotune_backend("ATEN"): + choices = [ + aten_convolution.bind( + args, + layout, + ordered_kwargs_for_cpp_kernel, + **kwargs, + ) + ] + + if ( + torch._inductor.utils._use_conv_autotune_backend("TRITON") + and use_triton_template(layout) + # templates only support these: + and is_ones(dilation) + and not transposed + and is_zeros(output_padding) + # there are some odd models where this check fails (e.g. shufflenet_v2_x1_0) + and V.graph.sizevars.statically_known_equals(in_chan * groups, x.get_size()[1]) # type: ignore[arg-type] + ): + if ( + is_ones(kernel_shape) + and is_ones(stride) + and is_zeros(padding) + and groups == 1 + ): + choices.append(aten_conv1x1_via_mm.bind(args, layout)) + + is_depthwise = groups > 1 and in_chan == 1 and out_chan == groups + if is_depthwise and ndim == 1: + depthwise_configs = V.choices.get_depthwise_conv_configs(device_type) + for cfg in depthwise_configs: + depthwise_conv1d_template.maybe_append_choice( + choices, + input_nodes=(x, weight), + layout=layout, + KERNEL_SIZE=kernel_shape[0], + CONV_STRIDE=stride[0], + PADDING=padding[0], + num_stages=cfg.num_stages, + num_warps=cfg.num_warps, + **cfg.kwargs, + ) + + conv_configs = V.choices.get_conv_configs(device_type) + + dtype_size = x.get_dtype().itemsize + for cfg in conv_configs( + sympy_product([x.get_size()[0], *x.get_size()[2:]]), + out_chan, + in_chan, + dtype_size=dtype_size, + ): + if ndim == 2: + conv2d_template.maybe_append_choice( + choices, + input_nodes=(x, weight), + layout=layout, + KERNEL_H=kernel_shape[0], + KERNEL_W=kernel_shape[1], + STRIDE_H=stride[0], + STRIDE_W=stride[1], + PADDING_H=padding[0], + PADDING_W=padding[1], + GROUPS=groups, + # TODO(jansel): try unroll for bigger kernels once fixed: + # https://github.com/triton-lang/triton/issues/1254 + UNROLL=is_ones(kernel_shape), + ALLOW_TF32=torch.backends.cudnn.fp32_precision == "tf32", + num_stages=cfg.num_stages, + num_warps=cfg.num_warps, + **cfg.kwargs, + ) + elif ndim == 3: + conv3d_template.maybe_append_choice( + choices, + input_nodes=(x, weight), + layout=layout, + KERNEL_D=kernel_shape[0], + KERNEL_H=kernel_shape[1], + KERNEL_W=kernel_shape[2], + STRIDE_D=stride[0], + STRIDE_H=stride[1], + STRIDE_W=stride[2], + PADDING_D=padding[0], + PADDING_H=padding[1], + PADDING_W=padding[2], + GROUPS=groups, + # TODO(jansel): try unroll for bigger kernels once fixed: + # https://github.com/triton-lang/triton/issues/1254 + UNROLL=is_ones(kernel_shape), + ALLOW_TF32=torch.backends.cudnn.fp32_precision == "tf32", + num_stages=cfg.num_stages, + num_warps=cfg.num_warps, + **cfg.kwargs, + ) + if use_ck_conv_template(layout): + CKGroupedConvFwdTemplate.add_ck_conv_choices( + choices, + layout, + input_nodes=(x, weight) + ((bias,) if bias is not None else tuple()), + stride=stride, + padding=padding, + dilation=dilation, + groups=groups, + n_spatial_dimensions=ndim, + ) + node, _ = autotune_select_algorithm("convolution", choices, args, layout) + return node + + +@register_lowering(aten._convolution) +def _convolution( + x, + weight, + bias, + stride, + padding, + dilation, + transposed, + output_padding, + groups, + benchmark, + deterministic, + cudnn_enabled, + allow_tf32, +): + return convolution( + x, weight, bias, stride, padding, dilation, transposed, output_padding, groups + ) + + +def constrain_conv_to_fx_strides(fx_node, *args, **kwargs): + assert fx_node.target is torch.ops.aten.convolution.default + if V.graph.layout_opt: + return args, kwargs + else: + return constrain_to_fx_strides(fx_node, *args, **kwargs) + + +add_layout_constraint(aten.convolution, constrain_conv_to_fx_strides) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/custom_op.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/custom_op.py new file mode 100644 index 0000000000000000000000000000000000000000..dbc10b26af44d950b737043fa50aa035f17ad868 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/custom_op.py @@ -0,0 +1,1317 @@ +# Owner(s): ["module: inductor"] + +import contextlib +import functools +import logging +from collections.abc import Callable +from dataclasses import dataclass, field +from typing import Any + +import torch +from torch._dynamo.utils import counters +from torch._inductor.codegen.subgraph import SubgraphTemplate +from torch._inductor.ir import ( + Buffer, + ChoiceCaller, + FixedLayout, + ir_node_to_tensor, + StorageBox, + TensorBox, +) +from torch._inductor.lowering import user_lowerings, validate_ir +from torch._inductor.select_algorithm import ( + autotune_select_algorithm, + ExternKernelChoice, +) +from torch._inductor.utils import convert_symint_to_expr +from torch._inductor.virtualized import V + + +log = logging.getLogger(__name__) + +DEFAULT_RANGE_UPPER_BOUND = 65536 + + +@dataclass(frozen=True) +class RangeBounds: + """Inclusive range [start, end] for dimension-based dispatch.""" + + start: int + end: int | float # float('inf') for unbounded + + def __post_init__(self) -> None: + if self.start < 1: + raise ValueError(f"Range start must be >= 1, got {self.start}") + if self.end != float("inf") and self.start > self.end: + raise ValueError(f"Invalid range: start={self.start} > end={self.end}") + + def contains(self, value: int) -> bool: + if self.end == float("inf"): + return value >= self.start + return self.start <= value <= int(self.end) + + def __str__(self) -> str: + end_str = "inf" if self.end == float("inf") else str(int(self.end)) + return f"[{self.start}, {end_str}]" + + +@dataclass(frozen=True) +class ImplConfig: + """Implementation config with semantic identity (name + kwargs) for hashing.""" + + impl_name: str + impl_func: Callable[..., Any] = field(compare=False, hash=False, repr=False) + kwargs: dict[str, Any] = field(default_factory=dict) + config_patches: dict[str, Any] = field( + default_factory=dict, compare=False, hash=False, repr=False + ) + + def __hash__(self) -> int: + return hash((self.impl_name, tuple(sorted(self.kwargs.items())))) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, ImplConfig): + return False + return self.impl_name == other.impl_name and self.kwargs == other.kwargs + + def __str__(self) -> str: + if self.kwargs: + kwargs_str = ", ".join(f"{k}={v}" for k, v in sorted(self.kwargs.items())) + return f"{self.impl_name}({kwargs_str})" + return self.impl_name + + +@dataclass +class RangeImplGroup: + """Groups non-adjacent ranges using the same implementation.""" + + impl_config: ImplConfig + ranges: list[RangeBounds] = field(default_factory=list) + + def add_range(self, range_bounds: RangeBounds) -> None: + self.ranges.append(range_bounds) + self.ranges.sort(key=lambda r: r.start) + + def __str__(self) -> str: + ranges_str = ", ".join(str(r) for r in self.ranges) + return f"{self.impl_config.impl_name}: {ranges_str}" + + @property + def impl_name(self) -> str: + return self.impl_config.impl_name + + @property + def impl_func(self) -> Callable[..., Any]: + return self.impl_config.impl_func + + @property + def impl_kwargs(self) -> dict[str, Any]: + return self.impl_config.kwargs + + @property + def config_patches(self) -> dict[str, Any]: + return self.impl_config.config_patches + + +def _detect_collective_ops(choices: list) -> bool: + """ + Detect if choices contain collective operations. + """ + from torch._inductor.utils import is_collective_op + + for choice in choices: + if not hasattr(choice, "gm") or choice.gm is None: + continue + + for node in choice.gm.graph.nodes: + if node.op == "call_function" and node.target is not None: + op_name = str(node.target) + + if is_collective_op(op_name) or is_collective_op( + f"torch.ops.{op_name}" + ): + return True + + return False + + +class CustomOpConfig: + """Config for custom op autotuning. + + Specifies optional decomposition function with parameter values. + Each config creates exactly one variant. + + Args: + decomposition: Optional functions to autotune. If not provided, default will be used. + config_patches: Optional dict of config patches to apply during kernel codegen + (e.g., {"coordinate_descent_tuning": True}) + **params: Parameters passed to the function + + Examples: + CustomOpConfig(attention_impl, head_dim=32, method='chunked') + CustomOpConfig(head_dim=32, method='chunked') + CustomOpConfig(decomposition, config_patches={"coordinate_descent_tuning": True}) + """ + + def __init__( + self, + decomposition: Callable[..., Any] | None = None, + config_patches: dict[str, Any] | None = None, + **params: Any, + ): + if decomposition is not None and not callable(decomposition): + raise TypeError( + f"decomposition must be callable, got {type(decomposition)}" + ) + + self.decomposition = decomposition + self.config_patches = config_patches or {} + self.params = params + + def get_decomposition( + self, default_impl: Callable[..., Any] | None = None + ) -> Callable[..., Any]: + """Return the decomposition function for this config. + When decomposition is not specified, return the default implementation. + """ + if self.decomposition is not None: + return self.decomposition + + if default_impl is not None and callable(default_impl): + return default_impl + + raise TypeError( + "No decomposition specified in config and no default implementation provided. " + "Please provide a decomposition function in CustomOpConfig." + ) + + def __repr__(self) -> str: + decomp_name = self.decomposition.__name__ if self.decomposition else "default" + if self.params: + params_str = ", ".join(f"{k}={v}" for k, v in self.params.items()) + return f"CustomOpConfig({decomp_name}, {params_str})" + return f"CustomOpConfig({decomp_name})" + + +__all__ = [ + "autotune_custom_op", + "register_custom_op_autotuning", + "CustomOpConfig", +] + + +def _extract_tensor_inputs( + args: tuple[Any, ...], + kwargs: dict[str, Any], + op_overload: torch._ops.OpOverload, +) -> tuple[list[Any], dict[str, Any]]: + """Extract tensor inputs from mixed args/kwargs. + Separates tensors (for autotuning input_nodes) from non-tensor parameters. + + Args: + args: Positional arguments (mix of tensors and scalars) + kwargs: Keyword arguments (mix of tensors and scalars) + op_overload: Custom Op overload to get parameter names from schema. + + Returns: + Tuple of (tensor_inputs_list, non_tensor_kwargs) + """ + tensor_inputs = [] + non_tensor_kwargs = {} + + # Get schema names and extend with fallback names for any extra args + schema_names = [arg.name for arg in op_overload._schema.arguments] + param_names = schema_names + [ + f"arg_{i}" for i in range(len(schema_names), len(args)) + ] + + for i, arg in enumerate(args): + if isinstance(arg, (TensorBox, Buffer, StorageBox)): + tensor_inputs.append(arg) + else: + non_tensor_kwargs[param_names[i]] = arg + + for key, value in kwargs.items(): + if isinstance(value, (TensorBox, Buffer, StorageBox)): + tensor_inputs.append(value) + else: + non_tensor_kwargs[key] = value + + return tensor_inputs, non_tensor_kwargs + + +def _merge_config_and_runtime_kwargs( + config_params: dict[str, Any], + runtime_kwargs: dict[str, Any], +) -> dict[str, Any]: + """Merge config parameters with runtime kwargs. Config params take precedence, + since they represent the values being autotuned. + + Args: + config_params: Parameters from CustomOpConfig (autotuning knobs) + runtime_kwargs: Runtime non-tensor kwargs from _extract_tensor_inputs + + Returns: + Merged kwargs dictionary with config values taking precedence + """ + merged_kwargs = runtime_kwargs.copy() + merged_kwargs.update(config_params) + return merged_kwargs + + +def _adapt_user_input_gen_fns( + inputs: list[Any], + op_overload: torch._ops.OpOverload, + user_input_gen_fns: dict[str, Callable[[torch.Tensor], torch.Tensor]], +) -> dict[int, Callable[[Any], torch.Tensor]]: + """Convert user input generators from name-based to index-based format. + Inductor autotune's input_gen_fns expects index of arg_names as key. + """ + arg_names = [arg.name for arg in op_overload._schema.arguments] + name_to_index = {name: i for i, name in enumerate(arg_names)} + index_based_fns = {} + + for name, gen_fn in user_input_gen_fns.items(): + if name in name_to_index: + index_based_fns[name_to_index[name]] = gen_fn + else: + raise ValueError( + f"Unknown argument name '{name}' in input_gen_fns. " + f"Available argument names: {list(name_to_index.keys())}" + ) + + def create_internal_input_gen_fn( + user_function: Callable[[torch.Tensor], torch.Tensor], arg_name: str + ) -> Callable[[Any], torch.Tensor]: + """Create internal input generator that converts IR buffer to user's fake tensor.""" + + def internal_input_gen_fn(ir_buffer: Any) -> torch.Tensor: + fake_tensor = ir_node_to_tensor(ir_buffer, replace_symbols_with_hints=True) + assert fake_tensor is not None, "ir_node_to_tensor returned None" + return user_function(fake_tensor) + + return internal_input_gen_fn + + return { + i: create_internal_input_gen_fn( + user_gen_fn, arg_names[i] if i < len(arg_names) else f"arg_{i}" + ) + for i, user_gen_fn in index_based_fns.items() + if i < len(inputs) + } + + +def _group_ranges_by_impl( + range_to_best_impl: dict[RangeBounds, ImplConfig], +) -> list[RangeImplGroup]: + """Group ranges by implementation using semantic identity (name + kwargs).""" + from torch._inductor import config + + if not range_to_best_impl: + return [] + + # Test mode: skip grouping to force torch.cond dispatch path + if config.test_configs.force_no_impl_grouping: + log.info("Test mode: skipping impl grouping, each range is separate group") + groups = [] + for range_bounds, impl_config in sorted( + range_to_best_impl.items(), key=lambda x: x[0].start + ): + group = RangeImplGroup(impl_config) + group.add_range(range_bounds) + groups.append(group) + return groups + + # Group ranges by impl_config (uses __hash__ and __eq__ based on semantic identity) + impl_to_group: dict[ImplConfig, RangeImplGroup] = {} + + for range_bounds, impl_config in range_to_best_impl.items(): + if impl_config not in impl_to_group: + impl_to_group[impl_config] = RangeImplGroup(impl_config) + impl_to_group[impl_config].add_range(range_bounds) + + # Sort groups by first range start for deterministic codegen + groups = sorted(impl_to_group.values(), key=lambda g: g.ranges[0].start) + + # Log grouping info + original_count = len(range_to_best_impl) + grouped_count = len(groups) + + if grouped_count < original_count: + log.info( + "Implementation grouping: reduced from %d ranges to %d impl groups", + original_count, + grouped_count, + ) + + return groups + + +def _create_ranges_from_split_points( + split_points: list[int], +) -> list[tuple[int, int] | tuple[int, float]]: + """Convert split points into ranges for autotuning dispatch. + + Example: + split_points=[512, 2048] + returns: + [(1, 512), (513, 2048), (2049, float('inf'))] + """ + ranges: list[tuple[int, int] | tuple[int, float]] = [] + start = 1 + + for split_point in split_points: + ranges.append((start, split_point)) + start = split_point + 1 + + ranges.append((start, float("inf"))) + + return ranges + + +def _create_range_input_gen_fn( + base_gen_fn: Callable[[torch.Tensor], torch.Tensor], + dim_index: int, + range_start: int, + range_end: int | float, + range_upper_bound: int, +) -> Callable[[torch.Tensor], torch.Tensor]: + """Create input generator that modifies target dimension to top of range. + range_upper_bound: Size to use for benchmarking when range_end is unbounded. + Default is DEFAULT_RANGE_UPPER_BOUND = 65536 + """ + from torch._inductor.ir import get_fill_order + from torch._inductor.kernel.flex.common import construct_strides + + target_dim = range_upper_bound if range_end == float("inf") else int(range_end) + + def constrained_gen_fn(fake_tensor: torch.Tensor) -> torch.Tensor: + result = base_gen_fn(fake_tensor) + shape = list(result.shape) + shape[dim_index] = target_dim + + # We modified the shape of the result, so we need to recalculate the strides + # TODO: Refine this to a better way to more directly preserve strides + fill_order = get_fill_order(result.stride(), shape_env=None) + new_stride = construct_strides(shape, fill_order) + + storage_size = sum((s - 1) * st for s, st in zip(shape, new_stride)) + 1 + storage = torch.randn(storage_size, dtype=result.dtype, device=result.device) + return storage.as_strided(shape, tuple(new_stride)) + + return constrained_gen_fn + + +def _default_input_gen_fn(fake_tensor: torch.Tensor) -> torch.Tensor: + """Default input generator that creates a real tensor matching the fake tensor's shape.""" + return torch.randn( + fake_tensor.shape, dtype=fake_tensor.dtype, device=fake_tensor.device + ) + + +def _create_fallback_choice( + op_overload: torch._ops.OpOverload, +) -> ExternKernelChoice: + """Create or reuse fallback choice that calls the op eagerly. + + Since kwargs are passed at bind time via maybe_append_choice rather than + baked into the kernel, the same ExternKernelChoice is reused across + compilations for the same op_overload. + """ + fallback_name = ( + f"{op_overload.name().replace('::', '_').replace('.', '_')}_fallback" + ) + + existing = ExternKernelChoice.lookup(fallback_name) + if existing is not None: + return existing + + return ExternKernelChoice( + kernel=op_overload, + name=fallback_name, + has_out_variant=False, + op_overload=op_overload, + use_fallback_kernel=True, + ) + + +def autotune_custom_op( + name: str, + decompositions: list[Callable[..., Any]], + inputs: list[torch.fx.Node], + non_tensor_args: list[dict[str, Any]], + op_overload: torch._ops.OpOverload, + user_input_gen_fns: dict[str, Callable[[torch.Tensor], torch.Tensor]] | None = None, + config_patches_list: list[dict[str, Any]] | None = None, + min_speedup_threshold: float = 1.0, + benchmark_with_cudagraphs: bool = False, +) -> tuple[TensorBox, ChoiceCaller]: + """Autotune custom operations by comparing multiple decomposition implementations. + + Currently supports SINGLE OUTPUT custom ops only. + TODO: Add support for multiple output custom ops (tuple/list returns). + + This function generates multiple implementation choices for a custom operation and + uses Inductor's autotuning system to select the best performing variant at runtime. + After selecting the best choice, applies inline fusion if the winning choice has a graph. + + Args: + name: Unique identifier for the autotuning operation + decompositions: List of alternative implementation functions to benchmark + inputs: Input tensor IR nodes from compilation (TensorBox/Buffer objects) + non_tensor_args: List of kwargs dicts, paired with corresponding decompositions arg + op_overload: OpOverload of the custom op, used as fallback implementation + user_input_gen_fns: Optional custom input generators for benchmarking. + Maps input indices to functions that take fake tensors + and return real tensors for performance measurement. + + Returns: + Tuple of (IR node representing the optimized operation result, winning ChoiceCaller) + + Raises: + TypeError: If decompositions is not a list/tuple + RuntimeError: If no inputs or no valid choices generated + """ + if not isinstance(decompositions, (list, tuple)): + raise TypeError( + f"decompositions must be a list or tuple of callables, got {type(decompositions)}" + ) + + if not inputs: + raise RuntimeError(f"Custom op '{name}' requires tensor inputs for autotuning") + + if len(decompositions) != len(non_tensor_args): + raise ValueError( + f"decompositions and non_tensor_args must have same length, " + f"got {len(decompositions)} decompositions and {len(non_tensor_args)} kwargs" + ) + + # Convert user input generation functions BEFORE creating choices + input_gen_fns: dict[int, Callable[[Any], torch.Tensor]] = {} + if user_input_gen_fns: + input_gen_fns = _adapt_user_input_gen_fns( + inputs, op_overload, user_input_gen_fns + ) + + template = SubgraphTemplate(name=name) + choices = template.generate_custom_op_choices( + name=name, + decompositions=decompositions, + # pyrefly: ignore [bad-argument-type, no-matching-overload] + input_nodes=list(inputs), + non_tensor_args=non_tensor_args, + input_gen_fns=input_gen_fns if input_gen_fns else None, + config_patches_list=config_patches_list, + ) + + # Add fallback choice that calls the op eagerly (not through inductor lowering) + # This provides a baseline to compare decompositions against + from torch._inductor import config + + fallback_kwargs = non_tensor_args[0] if non_tensor_args else {} + + with V.fake_mode: + # pyrefly: ignore [no-matching-overload] + fake_inputs = [ir_node_to_tensor(inp) for inp in inputs] + fake_output = op_overload(*fake_inputs, **fallback_kwargs) + + output_size = tuple(convert_symint_to_expr(s) for s in fake_output.shape) + output_stride = tuple(convert_symint_to_expr(s) for s in fake_output.stride()) + + fallback_choice = _create_fallback_choice(op_overload) + fallback_choice.maybe_append_choice( + choices=choices, + input_nodes=list(inputs), + layout=FixedLayout( + device=fake_output.device, + dtype=fake_output.dtype, + size=output_size, + stride=output_stride, + ), + **fallback_kwargs, + ) + + if not choices: + raise RuntimeError(f"No valid choices generated for {name}") + + is_collective = _detect_collective_ops(choices) + + # Run autotuning and get both result and winning choice + selected_result, winning_choice = autotune_select_algorithm( + name=name, + choices=choices, + input_nodes=list(inputs), + layout=choices[0].layout, + input_gen_fns=input_gen_fns, + is_collective=is_collective, + min_speedup_threshold=min_speedup_threshold, + benchmark_with_cudagraphs=benchmark_with_cudagraphs, + ) + + # Test mode: force specific choice to win + force_choice = config.test_configs.force_custom_op_decomposition + if force_choice is True and winning_choice.gm is None: + # Force decomposition: pick first choice with a graph + for choice in choices: + if choice.gm is not None: + log.info( + "Test mode: forcing decomposition %s over fallback", + getattr(choice, "name", type(choice).__name__), + ) + winning_choice = choice + selected_result = choice.output_node() + break + elif force_choice is False and winning_choice.gm is not None: + # Force fallback: pick first choice without a graph + for choice in choices: + if choice.gm is None: + log.info( + "Test mode: forcing fallback %s over decomposition", + getattr(choice, "name", type(choice).__name__), + ) + winning_choice = choice + selected_result = choice.output_node() + break + + # Always inline when winning_choice has a graph; callers extract choice metadata separately + if winning_choice.gm is not None: + log.debug( + "Inlining winning choice: %s (name=%s)", + getattr(winning_choice, "name", type(winning_choice).__name__), + name, + ) + from torch._inductor.codegen.subgraph import inline_subgraph_to_ir_nodes + + ops_before = len(V.graph.operations) + result = inline_subgraph_to_ir_nodes(winning_choice.gm, inputs, name) + + # Tag inlined operations with config_patches from the winning choice + config_patches = winning_choice.config_patches + if config_patches: + for op in V.graph.operations[ops_before:]: + op.set_config_patches(config_patches.copy()) + + return result, winning_choice + + log.debug( + "Winning choice does not support inlining: %s (name=%s)", + getattr(winning_choice, "name", type(winning_choice).__name__), + name, + ) + return selected_result, winning_choice + + +def _generate_dynamic_configs( + tensor_inputs: list[Buffer], + config_generator: Callable[[dict[str, torch.Tensor]], list[CustomOpConfig]], + op_overload: torch._ops.OpOverload, + operation_name: str, +) -> list[CustomOpConfig]: + """Generate configs dynamically based on input tensors at lowering time.""" + # Get parameter names from op schema instead of impl signature + schema = op_overload._schema + param_names = [arg.name for arg in schema.arguments if not arg.kwarg_only] + + with V.fake_mode: + fake_tensors = [ir_node_to_tensor(inp) for inp in tensor_inputs] + + fake_tensors_dict = dict(zip(param_names, fake_tensors)) + + configs = config_generator(fake_tensors_dict) + + if not isinstance(configs, (list, tuple)): + raise TypeError( + f"config_generator must return a list or tuple of CustomOpConfig, " + f"got {type(configs)}" + ) + if not configs: + log.info( + "config_generator returned empty list for %s, will use default lowering", + operation_name, + ) + return [] + + return list(configs) + + +def _prepare_configs_and_decompositions( + processed_configs: list[CustomOpConfig] | None, + config_generator: Callable[[dict[str, torch.Tensor]], list[CustomOpConfig]] | None, + tensor_inputs: list[Any], + default_impl: Callable[..., Any], + op_overload: torch._ops.OpOverload, + runtime_kwargs: dict[str, Any], + name: str, +) -> tuple[list[Callable], list[dict[str, Any]], list[dict[str, Any]]]: + """Prepare decompositions and merged kwargs from configs. + + Handles both static configs and dynamic config generation. + Merges config params with runtime kwargs (runtime takes precedence). + """ + # Get configs: either generate dynamically or use static configs + if config_generator is not None: + configs_to_use = _generate_dynamic_configs( + tensor_inputs, config_generator, op_overload, name + ) + else: + assert processed_configs is not None + configs_to_use = processed_configs + + # Prepare decompositions and kwargs for autotuning + decompositions = [] + non_tensor_args = [] + config_patches_list = [] + + for cfg in configs_to_use: + decomp = cfg.get_decomposition(default_impl=default_impl) + decompositions.append(decomp) + + # Merge config params with runtime kwargs (runtime takes precedence) + merged_kwargs = _merge_config_and_runtime_kwargs(cfg.params, runtime_kwargs) + non_tensor_args.append(merged_kwargs) + + # Collect config_patches for each config + config_patches_list.append(cfg.config_patches) + + return decompositions, non_tensor_args, config_patches_list + + +def _standard_lowering_fn( + processed_configs: list[CustomOpConfig], + default_impl: Callable[..., Any], + name: str, + op_overload: torch._ops.OpOverload, + input_gen_fns: dict[str, Callable[[torch.Tensor], torch.Tensor]] | None, + tensor_inputs: list[Any], + runtime_kwargs: dict[str, Any], + config_generator: Callable[[dict[str, torch.Tensor]], list[CustomOpConfig]] + | None = None, + min_speedup_threshold: float = 1.0, + benchmark_with_cudagraphs: bool = False, +) -> Any: + """Standard autotuning lowering function. + + Returns None if no configs/decompositions available, signaling caller to + use normal lowering. + """ + decompositions, non_tensor_args, config_patches_list = ( + _prepare_configs_and_decompositions( + processed_configs, + config_generator, + tensor_inputs, + default_impl, + op_overload, + runtime_kwargs, + name, + ) + ) + + # If no decompositions, signal caller to use normal lowering + if not decompositions: + return None + + result, _ = autotune_custom_op( + name=name, + decompositions=decompositions, + inputs=tensor_inputs, + non_tensor_args=non_tensor_args, + config_patches_list=config_patches_list, + op_overload=op_overload, + user_input_gen_fns=input_gen_fns, + min_speedup_threshold=min_speedup_threshold, + benchmark_with_cudagraphs=benchmark_with_cudagraphs, + ) + + validate_ir(result) + return result + + +def _apply_config_patches_recursive( + operations: list, + config_patches: dict[str, Any], +) -> None: + """Apply config_patches to operations, including those inside subgraphs.""" + for op in operations: + if hasattr(op, "set_config_patches"): + op.set_config_patches(config_patches.copy()) + + # Recurse into any subgraphs (Conditional, WhileLoop, InvokeSubgraph, etc.) + for subgraph in op.get_subgraphs(): + if subgraph.graph: + _apply_config_patches_recursive( + subgraph.graph.operations, config_patches + ) + + +def _lower_single_impl( + impl: Callable[..., Any], + impl_kwargs: dict[str, Any], + runtime_kwargs: dict[str, Any], + tensor_inputs: list[Any], + name: str, + config_patches: dict[str, Any] | None = None, +) -> Any: + """Lower a single implementation by tracing and inlining it. + + Uses error_on_new_guards() during tracing to detect if the impl adds guards. + Returns None if the impl adds guards, signaling caller to skip this choice. + """ + from torch._inductor.codegen.subgraph import inline_subgraph_to_ir_nodes + from torch.fx.experimental.proxy_tensor import make_fx + from torch.fx.experimental.symbolic_shapes import _ShapeEnvGuardError + + from ..decomposition import select_decomp_table + + merged_kwargs = _merge_config_and_runtime_kwargs(impl_kwargs, runtime_kwargs) + + def impl_wrapper(*tensors): + return impl(*tensors, **merged_kwargs) + + shape_env = V.fake_mode.shape_env + + with V.fake_mode: + fake_inputs = tuple(ir_node_to_tensor(inp) for inp in tensor_inputs) + decomposition_table = select_decomp_table() + + context = ( + shape_env.error_on_new_guards + if shape_env is not None + else contextlib.nullcontext + ) + try: + with context(): + impl_gm = make_fx( + impl_wrapper, + decomposition_table=decomposition_table, + tracing_mode="symbolic", + )(*fake_inputs) + except (_ShapeEnvGuardError, AssertionError) as e: + is_guard_error = isinstance(e, _ShapeEnvGuardError) or ( + isinstance(e, AssertionError) + and "Guard attempted while ShapeEnv guards are frozen" in str(e) + ) + if not is_guard_error: + raise + log.info( + "Implementation %s adds guards, skipping custom op lowering", + impl.__name__, + ) + counters["inductor"]["custom_op_decomp_guard_skips"] += 1 + return None + + log.info("Inlining implementation: %s", impl.__name__) + ops_before = len(V.graph.operations) + result = inline_subgraph_to_ir_nodes(impl_gm, tensor_inputs, name) + + if config_patches: + _apply_config_patches_recursive(V.graph.operations[ops_before:], config_patches) + + validate_ir(result) + return result + + +def _range_based_lowering_fn( + processed_configs: list[CustomOpConfig], + default_impl: Callable[..., Any], + name: str, + op_overload: torch._ops.OpOverload, + input_gen_fns: dict[str, Callable[[torch.Tensor], torch.Tensor]] | None, + tensor_name: str, + dim_index: int, + ranges: list[tuple[int, int | float]], + tensor_inputs: list[Any], + runtime_kwargs: dict[str, Any], + range_upper_bound: int, + config_generator: Callable[[dict[str, torch.Tensor]], list[CustomOpConfig]] + | None = None, + min_speedup_threshold: float = 1.0, + benchmark_with_cudagraphs: bool = False, +) -> Any: + """Range-based autotuning lowering function.""" + from torch._inductor.codegen.subgraph import inline_subgraph_to_ir_nodes + from torch.fx.experimental.proxy_tensor import make_fx + + from ..decomposition import select_decomp_table + + log.info("=== Range-based Autotuning for %s ===", name) + log.info("Dispatch on: %s[%d], Ranges: %s", tensor_name, dim_index, ranges) + + decompositions, non_tensor_args, config_patches_list = ( + _prepare_configs_and_decompositions( + processed_configs, + config_generator, + tensor_inputs, + default_impl, + op_overload, + runtime_kwargs, + name, + ) + ) + + range_to_best_impl_map: dict[RangeBounds, ImplConfig] = {} + + # Benchmark each range and collect winning implementations + for range_start, range_end in ranges: + if input_gen_fns and tensor_name in input_gen_fns: + base_gen_fn = input_gen_fns[tensor_name] + else: + base_gen_fn = _default_input_gen_fn + + range_gen_fn = _create_range_input_gen_fn( + base_gen_fn, dim_index, range_start, range_end, range_upper_bound + ) + range_input_gen_fns = {**(input_gen_fns or {}), tensor_name: range_gen_fn} + + range_name = f"{name}_range_{int(range_start)}_{int(range_end) if range_end != float('inf') else 'inf'}" + + # pyrefly: ignore [not-iterable] + autotuned_result, winning_choice = autotune_custom_op( + name=range_name, + decompositions=decompositions, + inputs=tensor_inputs, + non_tensor_args=non_tensor_args, + op_overload=op_overload, + user_input_gen_fns=range_input_gen_fns, + min_speedup_threshold=min_speedup_threshold, + benchmark_with_cudagraphs=benchmark_with_cudagraphs, + config_patches_list=config_patches_list, + ) + + if winning_choice.decomposition is not None: + winning_impl = winning_choice.decomposition + winning_kwargs = winning_choice.decomposition_kwargs + else: + # Fallback was selected (ExternKernelCaller) + winning_impl = default_impl + winning_kwargs = non_tensor_args[0] if non_tensor_args else {} + log.info( + " Range [%s, %s]: Fallback (default_impl) selected", + range_start, + range_end if range_end != float("inf") else "inf", + ) + + winning_config_patches = winning_choice.config_patches or {} + + # Create dataclass instances for cleaner code + range_bounds = RangeBounds(range_start, range_end) + impl_config = ImplConfig( + impl_name=winning_impl.__name__, + impl_func=winning_impl, + kwargs=winning_kwargs, + config_patches=winning_config_patches, + ) + range_to_best_impl_map[range_bounds] = impl_config + + log.info( + " Range %s -> %s", + range_bounds, + impl_config.impl_name, + ) + + # Group ranges by implementation + from torch.fx.experimental.symbolic_shapes import _ShapeEnvGuardError + + impl_groups = _group_ranges_by_impl(range_to_best_impl_map) + + log.info("After grouping by implementation: %d impl groups", len(impl_groups)) + for group in impl_groups: + log.info(" %s", group) + + # If only one impl group remains, just inline that implementation + if len(impl_groups) == 1: + group = impl_groups[0] + log.info("Only one implementation after grouping, directly inlining") + return _lower_single_impl( + group.impl_func, + group.impl_kwargs, + runtime_kwargs, + tensor_inputs, + name, + config_patches=group.config_patches, + ) + + def dispatch_fn(*fake_tensors): + """Build nested torch.cond dispatch: cond(pred1, impl1, cond(pred2, impl2, ...)).""" + num_impl_groups = len(impl_groups) + if num_impl_groups < 2: + raise RuntimeError( + f"dispatch_fn requires at least 2 impl groups, got {num_impl_groups}" + ) + + dim_value = fake_tensors[0].size(dim_index) + + def build_range_predicate(ranges_list: list[RangeBounds]) -> torch.Tensor: + predicates = [] + for rb in ranges_list: + end = int(rb.end) if rb.end != float("inf") else None + if end is None: + predicates.append(dim_value >= rb.start) + else: + predicates.append((dim_value >= rb.start) & (dim_value <= end)) + + result = predicates[0] + for pred in predicates[1:]: + result = result | pred + return result # pyrefly: ignore [bad-return] + + def build_nested_cond(idx: int): + if idx >= num_impl_groups: + raise RuntimeError(f"Invalid impl group index: {idx}") + + group = impl_groups[idx] + merged_kwargs = _merge_config_and_runtime_kwargs( + group.impl_kwargs, runtime_kwargs + ) + + @torch._dynamo.dont_skip_tracing + def group_fn(*ops): + return group.impl_func(*ops, **merged_kwargs) + + if idx == num_impl_groups - 1: + return group_fn + + next_fn = build_nested_cond(idx + 1) + + @torch._dynamo.dont_skip_tracing + def cond_wrapper(*ops, _ranges=group.ranges): + return torch.cond( + pred=build_range_predicate(_ranges), + true_fn=group_fn, + false_fn=next_fn, + operands=ops, + ) + + return cond_wrapper + + return build_nested_cond(0)(*fake_tensors) + + with V.fake_mode: + fake_inputs = tuple(ir_node_to_tensor(inp) for inp in tensor_inputs) + decomposition_table = select_decomp_table() + shape_env = V.fake_mode.shape_env + + log.info("Tracing torch.cond dispatch with symbolic shapes...") + + try: + context = ( + shape_env.error_on_new_guards + if shape_env is not None + else contextlib.nullcontext + ) + with context(): + dispatch_gm = make_fx( + dispatch_fn, + decomposition_table=decomposition_table, + tracing_mode="symbolic", + )(*fake_inputs) + + log.info("Successfully traced torch.cond dispatch") + log.info("Traced graph:\n%s", dispatch_gm.graph) + + except (_ShapeEnvGuardError, AssertionError) as e: + is_guard_error = isinstance(e, _ShapeEnvGuardError) or ( + isinstance(e, AssertionError) + and "Guard attempted while ShapeEnv guards are frozen" in str(e) + ) + if not is_guard_error: + raise + log.info("Dispatch function adds guards, skipping custom op lowering") + counters["inductor"]["custom_op_decomp_guard_skips"] += 1 + return None + + except Exception: + log.exception("make_fx tracing FAILED") + raise + + ops_before = len(V.graph.operations) + result = inline_subgraph_to_ir_nodes(dispatch_gm, tensor_inputs, f"{name}_dispatch") + + # Apply config_patches from all impl groups to inlined operations + # TODO - consider conflicting patches + merged_patches: dict[str, Any] = {} + for group in impl_groups: + merged_patches.update(group.config_patches) + if merged_patches: + _apply_config_patches_recursive(V.graph.operations[ops_before:], merged_patches) + + log.info( + "Successfully created torch.cond dispatch for %d impl groups", len(impl_groups) + ) + + validate_ir(result) + return result + + +def _create_autotuning_lowering( + processed_configs: list[CustomOpConfig], + default_impl: Callable[..., Any], + name: str, + op_overload: torch._ops.OpOverload, + input_gen_fns: dict[str, Callable[[torch.Tensor], torch.Tensor]] | None, + range_upper_bound: int, + is_range_based: bool = False, + config_generator: Callable[[dict[str, torch.Tensor]], list[CustomOpConfig]] + | None = None, + dispatch_on: tuple[str, int] | None = None, + split_points: list[int] | None = None, + min_speedup_threshold: float = 1.0, + benchmark_with_cudagraphs: bool = False, +) -> Callable[..., Any]: + """Create the lowering function for autotuning.""" + if not is_range_based: + # Standard autotuning path + @functools.wraps(op_overload) + def standard_lowering_wrapper(*args: Any, **kwargs: Any) -> Any: + tensor_inputs, runtime_kwargs = _extract_tensor_inputs( + args, kwargs, op_overload + ) + return _standard_lowering_fn( + processed_configs=processed_configs, + default_impl=default_impl, + name=name, + op_overload=op_overload, + input_gen_fns=input_gen_fns, + tensor_inputs=tensor_inputs, + runtime_kwargs=runtime_kwargs, + config_generator=config_generator, + min_speedup_threshold=min_speedup_threshold, + benchmark_with_cudagraphs=benchmark_with_cudagraphs, + ) + + return standard_lowering_wrapper + + # Range-based autotuning path + # pyrefly: ignore [not-iterable] + tensor_name, dim_index = dispatch_on + # pyrefly: ignore [bad-argument-type] + ranges = _create_ranges_from_split_points(split_points) + + @functools.wraps(op_overload) + def range_based_lowering_wrapper(*args: Any, **kwargs: Any) -> Any: + tensor_inputs, runtime_kwargs = _extract_tensor_inputs( + args, kwargs, op_overload + ) + return _range_based_lowering_fn( + processed_configs=processed_configs, + default_impl=default_impl, + name=name, + op_overload=op_overload, + input_gen_fns=input_gen_fns, + tensor_name=tensor_name, + dim_index=dim_index, + ranges=ranges, + tensor_inputs=tensor_inputs, + runtime_kwargs=runtime_kwargs, + range_upper_bound=range_upper_bound, + config_generator=config_generator, + min_speedup_threshold=min_speedup_threshold, + benchmark_with_cudagraphs=benchmark_with_cudagraphs, + ) + + return range_based_lowering_wrapper + + +def register_custom_op_autotuning( + custom_op: torch._library.custom_ops.CustomOpDef | torch._ops.OpOverload, + configs: list[CustomOpConfig] | list[Callable[..., Any]] | None = None, + config_generator: Callable[[dict[str, torch.Tensor]], list[CustomOpConfig]] + | None = None, + name: str | None = None, + input_gen_fns: dict[str, Callable[[torch.Tensor], torch.Tensor]] | None = None, + dispatch_on: dict[str, Any] | None = None, + split_points: list[int] | None = None, + min_speedup_threshold: float = 1.0, + benchmark_with_cudagraphs: bool = False, +) -> None: + """Register custom op for autotuning with custom_op configs where each config + specifies a decomposition implementation function with its parameter values. + It also supports Range-based autotuning to benchmark per range and generate + runtime dispatch. + + Args: + custom_op: Custom operation (CustomOpDef from @torch.library.custom_op) or + OpOverload (e.g., torch.ops.aten.mm.default) + configs: List of CustomOpConfig objects for static inputs. Mutually exclusive with config_generator. + config_generator: Dynamic config generator function that takes a dict mapping + parameter names to fake tensors, and returns list[CustomOpConfig] + based on input tensor properties. Mutually exclusive with configs. + name: Operation name (default: "{op_name}_autotuned") + input_gen_fns: Custom input generators for benchmarking + dispatch_on: Dict for range-based dispatch with keys: + - 'tensor_name': Name of tensor parameter to dispatch on + - 'dim': Dimension index to check size + - 'unbounded_size' (optional): Benchmark size for the unbounded (last) range, such + as [2048, inf] -> [2048, unbounded_size]. Set based on your expected workload size. + Default is DEFAULT_RANGE_UPPER_BOUND=65536. + split_points: List of range endpoints in ascending order for range-based autotuning + min_speedup_threshold: Only pick a non-fallback choice if it beats the fallback + by at least this ratio. Default is 1.0 (any speedup wins). Set to e.g. 1.1 + to require 10% speedup over fallback. + benchmark_with_cudagraphs: If True, benchmark the fallback kernel using CUDA graph + capture and replay for fair comparison with compiled kernels. Default is False. + + The default/fallback implementation is automatically derived: + - For CustomOpDef: Uses the decorated function + - For OpOverload: Traces the op call, which falls through to normal inductor lowering + + Examples: + # Static configs + @torch.library.custom_op("mylib::attention", mutates_args=()) + def my_attention(query, key, value, head_dim=32): + ... + + register_custom_op_autotuning( + my_attention, + configs=[ + CustomOpConfig(attention_impl, head_dim=32, method='chunked'), + CustomOpConfig(attention_impl, head_dim=64, method='tiled'), + CustomOpConfig(head_dim=128), # No decomposition specified, use default + ], + input_gen_fns={ + "query": lambda fake: torch.randn_like(fake, device='cuda'), + "key": lambda fake: torch.randn_like(fake, device='cuda'), + "value": lambda fake: torch.randn_like(fake, device='cuda'), + }, + ) + + # Dynamic config generation based on input tensor properties + def generate_k_split_configs(fake_tensors: dict[str, torch.Tensor]) -> list[CustomOpConfig]: + # Access tensor shapes, dtypes, devices, etc. + m, k = fake_tensors["mat1"].shape + _, n = fake_tensors["mat2"].shape + k_splits = ... # compute possible k splits based on tensor properties + return [CustomOpConfig(k_splits=k) for k in k_splits] + + register_custom_op_autotuning( + matmul_decomposeK_op, + config_generator=generate_k_split_configs, + input_gen_fns={...}, + ) + + Range-based Example: + register_custom_op_autotuning( + my_op, + configs=[CustomOpConfig(impl1), CustomOpConfig(impl2), CustomOpConfig(impl3)], + dispatch_on={ + # Dispatch based on x.shape[1] + "tensor_name": "x", + "dim": 1, + # Optional Benchmark size used for the unbounded range [2049, inf]. + # Since inf is not a concrete value, we use range_upper_bound as the benchmark size. + # Default value is 65536 (DEFAULT_RANGE_UPPER_BOUND) if not provided. + "range_upper_bound": 8192, + }, + split_points=[512, 2048], # Creates ranges: [1,512], [513,2048], [2049, 8192] + ) + """ + from torch._library.custom_ops import CustomOpDef + + # Handle both CustomOpDef and OpOverload - derive impl_fn automatically + # Both cases call through op_overload so fake handlers are used during tracing + if isinstance(custom_op, CustomOpDef): + op_overload = custom_op._opoverload + elif isinstance(custom_op, torch._ops.OpOverload): + op_overload = custom_op + else: + raise TypeError( + f"custom_op must be a CustomOpDef or OpOverload, got {type(custom_op)}." + ) + + # impl_fn calls through op_overload so fake handlers are used during tracing + def impl_fn(*args, **kwargs): + return op_overload(*args, **kwargs) + + # Validate configs and config_generator are mutually exclusive + if configs is not None and config_generator is not None: + raise ValueError( + "Cannot specify both 'configs' and 'config_generator'. " + "Use 'config_generator' for shape-dependent configs." + ) + + if configs is None and config_generator is None: + raise ValueError("Must specify either 'configs' or 'config_generator'") + + # Process and validate static configs at registration time + static_configs = None + if configs is not None: + if not isinstance(configs, (list, tuple)): + raise TypeError(f"configs must be a list or tuple, got {type(configs)}") + + static_configs = [] + for cfg in configs: + if isinstance(cfg, CustomOpConfig): + static_configs.append(cfg) + else: + raise TypeError( + f"Each config must be a CustomOpConfig object, got {type(cfg)}" + ) + + if not static_configs: + raise ValueError("At least one config must be provided") + + if name is None: + name = f"{op_overload._name}_autotuned" + + # Validate range-based parameters + is_range_based = dispatch_on is not None or split_points is not None + dispatch_on_tuple: tuple[str, int] | None = None + range_upper_bound = DEFAULT_RANGE_UPPER_BOUND + if is_range_based: + if dispatch_on is None or split_points is None: + raise ValueError( + "Both dispatch_on and split_points must be specified for range-based autotuning" + ) + if not isinstance(dispatch_on, dict): + raise ValueError( + "dispatch_on must be a dict with 'tensor_name' and 'dim' keys, " + f"e.g., {{'tensor_name': 'x', 'dim': 1}}. Got: {type(dispatch_on)}" + ) + if "tensor_name" not in dispatch_on or "dim" not in dispatch_on: + raise ValueError( + "dispatch_on must contain 'tensor_name' and 'dim' keys, " + f"e.g., {{'tensor_name': 'x', 'dim': 1}}. Got keys: {list(dispatch_on.keys())}" + ) + if not isinstance(dispatch_on["tensor_name"], str): + raise ValueError( + f"dispatch_on['tensor_name'] must be a string (tensor parameter name), " + f"got {type(dispatch_on['tensor_name'])}" + ) + if not isinstance(dispatch_on["dim"], int): + raise ValueError( + f"dispatch_on['dim'] must be an integer (dimension index), " + f"got {type(dispatch_on['dim'])}" + ) + dispatch_on_tuple = (dispatch_on["tensor_name"], dispatch_on["dim"]) + range_upper_bound = dispatch_on.get( + "range_upper_bound", DEFAULT_RANGE_UPPER_BOUND + ) + if not isinstance(range_upper_bound, int) or range_upper_bound <= 0: + raise ValueError( + f"dispatch_on['range_upper_bound'] must be a positive integer, " + f"got {range_upper_bound}" + ) + if not isinstance(split_points, list) or len(split_points) == 0: + raise ValueError("split_points must be a non-empty list of integers") + if sorted(split_points) != split_points: + raise ValueError("split_points must be sorted in ascending order") + + # Create and register the lowering function + lowering_fn = _create_autotuning_lowering( + # pyrefly: ignore [bad-argument-type] + processed_configs=static_configs, + default_impl=impl_fn, + name=name, + op_overload=op_overload, + input_gen_fns=input_gen_fns, + is_range_based=is_range_based, + config_generator=config_generator, + dispatch_on=dispatch_on_tuple, + split_points=split_points, + range_upper_bound=range_upper_bound, + min_speedup_threshold=min_speedup_threshold, + benchmark_with_cudagraphs=benchmark_with_cudagraphs, + ) + + # Register in user_lowerings which takes priority over built-in lowerings + # The dispatch in graph.py checks user_lowerings first with recursion guard + user_lowerings[op_overload] = lowering_fn diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..67a604adcb1e6057015f7fa1833d766b37d7c61b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/__init__.py @@ -0,0 +1,3 @@ +# mypy: allow-untyped-defs +# Import so here and then reimport above so that register_lowering gets triggered +from . import flex_attention, flex_decoding diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/common.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/common.py new file mode 100644 index 0000000000000000000000000000000000000000..1ad6e502391707b9a89d520eef4d0f618e8aecfa --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/common.py @@ -0,0 +1,364 @@ +# mypy: allow-untyped-defs +"""Common utilities and functions for flex attention kernels""" + +import math +from collections.abc import Sequence +from functools import partial +from pathlib import Path +from typing import Any, TYPE_CHECKING + +import sympy + +import torch +from torch._inductor.virtualized import V +from torch.utils._ordered_set import OrderedSet +from torch.utils._pytree import tree_map, tree_map_only + + +if TYPE_CHECKING: + from torch._inductor.codegen.cuda_combined_scheduling import _IntLike +else: + _IntLike = int | sympy.Expr + + +from ...ir import ( + ComputedBuffer, + ExternKernel, + FixedLayout, + FlexibleLayout, + get_fill_order, + InputBuffer, + IRNode, + MutationLayoutSHOULDREMOVE, + Scatter, + StorageBox, + Subgraph, + TensorBox, +) +from ...lowering import ( + _full, + check_and_broadcast_indices, + expand, + index_output_size_and_inner_fn, + to_dtype, +) +from ...select_algorithm import realize_inputs +from ...utils import load_template + + +SubgraphResults = list[ComputedBuffer | None] | ComputedBuffer | None + + +def zeros_and_scatter_lowering(shape: list[int], indices, values): + """To support backwards on captured buffers we register a specific lowering for our specific custom up""" + # Always accumulate into fp32 then cast + grad = _full(0, values.get_device(), torch.float32, shape) + assert isinstance(grad, TensorBox) + grad.realize() + x_size = grad.get_size() + values = to_dtype(values, grad.get_dtype()) + indices_loaders = [i.make_loader() if i is not None else None for i in indices] + indices, tensor_indices = check_and_broadcast_indices(indices, grad.get_device()) + # We can use the first one since they are all required to be the same size + tensor_size = list(indices[tensor_indices[0]].get_size()) + indexed_size = [x_size[i] for i in range(len(indices))] + + expected_vals_size, inner_fn = index_output_size_and_inner_fn( + x_size, + indices, + tensor_indices, + tensor_size, + indices_loaders, + indexed_size, + None, + check=True, + ) + + values = expand(values, expected_vals_size) + device = grad.get_device() + assert device is not None + scatter = Scatter( + device=device, + dtype=grad.get_dtype(), + inner_fn=values.make_loader(), + ranges=expected_vals_size, # iter_ranges, + output_indexer=inner_fn, + scatter_mode="atomic_add", + ) + + buffer = ComputedBuffer( + name=grad.data.data.name, # type: ignore[attr-defined] + layout=MutationLayoutSHOULDREMOVE(grad), + data=scatter, + ) + return buffer + + +def get_fwd_subgraph_outputs( + subgraph_buffer: SubgraphResults, mask_graph_buffer: SubgraphResults +) -> list[ComputedBuffer | None]: + subgraph_buffer = ( + subgraph_buffer if isinstance(subgraph_buffer, Sequence) else [subgraph_buffer] + ) + mask_graph_buffer = ( + mask_graph_buffer + if isinstance(mask_graph_buffer, Sequence) + else [mask_graph_buffer] + ) + + return [*subgraph_buffer, *mask_graph_buffer] + + +def build_subgraph_module_buffer( + args: list[TensorBox], + graph_module: torch.fx.GraphModule, +) -> SubgraphResults: + """This function's goal is to take in the required args and produce the subgraph buffer + The subgraph buffer is a ComputedBuffer that will be inlined into the triton template + + Args: + args: The args that are passed into the subgraph. Contains both fixed and lifted inputs. + subgraph: The Subgraph ir for which to produce the output node + """ + # This one we gotta keep lazy + from ...subgraph_lowering import PointwiseSubgraphLowering + + pw_subgraph = PointwiseSubgraphLowering( + graph_module, + root_graph_lowering=V.graph, + allowed_mutations=OrderedSet([torch.ops.flex_lib.zeros_and_scatter.default]), + additional_lowerings={ + torch.ops.flex_lib.zeros_and_scatter.default: zeros_and_scatter_lowering + }, + ) + with V.set_graph_handler(pw_subgraph): # type: ignore[arg-type] + pw_subgraph.run(*args) + + def convert_output_node_to_buffer(output_buffer) -> ComputedBuffer | None: + if output_buffer is None: + return None + if isinstance(output_buffer, ComputedBuffer): + # These nodes are coming from the output of zeros_and_scatter + return output_buffer + assert isinstance(output_buffer, TensorBox), ( + "The output node for flex attention's subgraph must be a TensorBox, but got: ", + type(output_buffer), + ) + assert isinstance(output_buffer.data, StorageBox), ( + "The output node for the flex attention subgraph must be a StorageBox, but got: ", + type(output_buffer), + ) + device = output_buffer.data.get_device() + assert device is not None + subgraph_buffer = ComputedBuffer( + name=None, + layout=FlexibleLayout( + device=device, + dtype=output_buffer.data.get_dtype(), + size=output_buffer.data.get_size(), + ), + data=output_buffer.data.data, # type: ignore[arg-type] + ) + return subgraph_buffer + + return tree_map(convert_output_node_to_buffer, pw_subgraph.graph_outputs) + + +def build_subgraph_buffer(args: list[TensorBox], subgraph: Subgraph) -> SubgraphResults: + return build_subgraph_module_buffer(args, subgraph.graph_module) + + +def maybe_realize(args: list[IRNode | None]): + """Accepts a list of optional IRNodes and returns a list of realized IRNodes""" + return tree_map( + lambda x: ( + realize_inputs(x) + if x is not None and not isinstance(x, sympy.Symbol) + else x + ), + args, + ) + + +def freeze_irnodes(tree: Any) -> Any: + """Freeze layouts for every IRNode contained in a pytree.""" + + if tree is None: + return None + + def _freeze(node: IRNode) -> IRNode: + try: + node.freeze_layout() + except NotImplementedError: + pass + return node + + return tree_map_only(IRNode, _freeze, tree) + + +def create_placeholder( + name: str, + dtype: torch.dtype, + device: torch.device, + size: list[int] | None = None, +) -> TensorBox: + """Creates a placeholder input buffers for producing subgraph_output.""" + input_buffer = InputBuffer( + name=name, + layout=FixedLayout( + device, + dtype, + size if size else [], + FlexibleLayout.contiguous_strides(size) if size else [], + ), + ) + return TensorBox.create(input_buffer) + + +def construct_strides( + sizes: Sequence[_IntLike], + fill_order: Sequence[int], +) -> Sequence[_IntLike]: + """From a list of sizes and a fill order, construct the strides of the permuted tensor.""" + # Initialize strides + assert len(sizes) == len(fill_order), ( + "Length of sizes must match the length of the fill order" + ) + strides: list[_IntLike] = [0] * len(sizes) + + # Start with stride 1 for the innermost dimension + current_stride: _IntLike = 1 + + # Iterate through the fill order populating strides + for dim in fill_order: + strides[dim] = current_stride + current_stride *= sizes[dim] + + return strides + + +def infer_dense_strides( + size: Sequence[_IntLike], + orig_strides: Sequence[_IntLike], +): + """This is a mirror of the same function in aten/src/ATen/ExpandUtils.cpp + + Args: + size: The size of the output tensor + orig_strides: The strides of the input tensor + Returns: + List[int]: Dense non-overlapping strides that preserve the input tensor's layout permutation. + The returned strides follow the same stride propagation rules as TensorIterator. This matches + The behavior of empty_like() + """ + fill_order = get_fill_order(orig_strides, V.graph.sizevars.shape_env) + strides = construct_strides(size, fill_order) + + # Attention kernels require stride[-1]=1 for efficient memory access. + # Ensure this by moving last dim to front of fill_order if needed. + if strides[-1] != 1: + last_dim = len(size) - 1 + fill_order = list(fill_order) + fill_order.remove(last_dim) + fill_order = [last_dim] + fill_order + strides = construct_strides(size, fill_order) + + return strides + + +def create_indices_fake(x) -> torch.Tensor: + """Create a fake indices that is used for autotuning.""" + size = V.graph.sizevars.optimization_hints(x.get_size()) + indices = torch.arange(0, size[-1], dtype=x.get_dtype(), device=x.get_device()) + indices = indices.expand(size).contiguous() + return indices + + +def create_num_blocks_fake_generator(sparse_indices): + """Create a fake num_blocks that is used for autotuning. + + The idea here is that we need to create a real tensor with real data + that's representative for benchmarking. + For example, returning all zeros for the `kv_num_blocks` input would mean + that we are computing 0 blocks for each row, which would provide bogus + autotuning results. + + In this case, we choose to use min(16, max_block) blocks, because I + (Horace) think it'll probably result in pretty representative performance. + If it's too short then prefetching won't help. If it's too long then + autotuning will take longer for no good reason. + """ + + def create_num_blocks_fake(x) -> torch.Tensor: + num_blocks_for_autotuning = V.graph.sizevars.optimization_hint( + sparse_indices.shape[-1] + ) + size = V.graph.sizevars.optimization_hints(x.get_size()) + return torch.full( + size, + num_blocks_for_autotuning, + dtype=x.get_dtype(), + device=x.get_device(), + ) + + return create_num_blocks_fake + + +def contiguous_last_dim(x): + """Ensure that realized IR node has a contiguous stride in the last dimension.""" + strides = x.maybe_get_stride() + if strides and strides[-1] != 1: + contiguous_stride_order = list(reversed(range(len(x.get_size())))) + return ExternKernel.require_stride_order(x, contiguous_stride_order) + return x + + +def set_head_dim_values( + kernel_options: dict[str, Any], qk_head_dim, v_head_dim, graph_sizevars +): + """ + Mutates kernel options, adding head dimension calculations. + + Args: + kernel_options: Dictionary to populate with options + qk_head_dim: Query/Key head dimension + v_head_dim: Value head dimension + graph_sizevars: Graph size variables object with guard_int method + + """ + # QK dimensions + qk_head_dim_static = graph_sizevars.guard_int(qk_head_dim) + kernel_options.setdefault("QK_HEAD_DIM", qk_head_dim_static) + kernel_options.setdefault( + "QK_HEAD_DIM_ROUNDED", next_power_of_two(qk_head_dim_static) + ) + + # V dimensions + v_head_dim_static = graph_sizevars.guard_int(v_head_dim) + kernel_options.setdefault("V_HEAD_DIM", v_head_dim_static) + kernel_options.setdefault( + "V_HEAD_DIM_ROUNDED", next_power_of_two(v_head_dim_static) + ) + + # Safety flag + kernel_options.setdefault( + "SAFE_HEAD_DIM", + is_power_of_2(qk_head_dim_static) and is_power_of_2(v_head_dim_static), + ) + + +def is_power_of_2(n): + return n != 0 and ((n & (n - 1)) == 0) + + +def next_power_of_two(n): + if n <= 0: + return 1 + return 2 ** math.ceil(math.log2(n)) + + +_FLEX_TEMPLATE_DIR = Path(__file__).parent / "templates" +load_flex_template = partial(load_template, template_dir=_FLEX_TEMPLATE_DIR) + + +# Template strings have been moved to templates/common.py.jinja diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/flex_attention.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/flex_attention.py new file mode 100644 index 0000000000000000000000000000000000000000..8c1523591c1ef5a2e226a063644b4d11d5689ebf --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/flex_attention.py @@ -0,0 +1,1090 @@ +# mypy: allow-untyped-defs +"""Triton Implementation of the flex_attention Kernel""" + +from __future__ import annotations + +import logging +import math +import warnings +from collections.abc import Sequence +from dataclasses import dataclass +from typing import Any, cast, TYPE_CHECKING + +import sympy + +import torch +from torch._inductor.virtualized import V +from torch.nn.attention.flex_attention import _Backend +from torch.utils._sympy.functions import FloorDiv, Mod + +from ...ir import ComputedBuffer, ExternKernel, FixedLayout, TensorBox +from ...lowering import empty, empty_strided, lowerings, register_lowering, to_dtype +from ...select_algorithm import ( + autotune_select_algorithm, + SymbolicGridFn, + TritonTemplate, +) +from ...utils import can_use_tma +from .common import ( + build_subgraph_buffer, + create_indices_fake, + create_num_blocks_fake_generator, + create_placeholder, + freeze_irnodes, + get_fwd_subgraph_outputs, + infer_dense_strides, + load_flex_template, + maybe_realize, + set_head_dim_values, + SubgraphResults, +) +from .flex_cpu import lower_cpu +from .flex_decoding import _use_flex_decoding, create_flex_decoding_kernel +from .flex_flash_attention import ( + _use_flex_flash_attention, + _use_flex_flash_attention_backward, + create_flex_flash_attention_backward_kernel, + create_flex_flash_attention_kernel, + is_trivial_mask_graph, + is_trivial_score_graph, +) + + +if TYPE_CHECKING: + from ...template_heuristics.triton import FlexBwDConfig, FlexConfig + + +log = logging.getLogger(__name__) +aten = torch.ops.aten +prims = torch.ops.prims +Expr = sympy.Expr + + +def _sanitize_kernel_options_for_triton( + kernel_options: dict[str, Any], +) -> tuple[dict[str, Any], _Backend]: + """We always strip quotes around str values, we only need this in lowering, so we pop it here + to avoid passing to triton constexpr dict + """ + sanitized = dict(kernel_options) + backend = cast(_Backend, sanitized.pop("BACKEND", "AUTO")) + return sanitized, backend + + +@SymbolicGridFn +def flex_attention_grid(batch_size, q_heads, num_queries, d_model, meta, *, cdiv): + """How is this kernel parallelized? + We create a grid of (ceil_div(n_queries, query_block_size), batch_size, num_heads) + Each block is responsible for iterating over blocks of keys and values calculating + the final attention output. + """ + return (cdiv(num_queries, meta["BLOCK_M"]), batch_size, q_heads) + + +def get_float32_precision(): + if ( + ( + torch.backends.cuda.matmul.fp32_precision == "ieee" + if torch.backends.cuda.matmul.fp32_precision != "none" + else torch.get_float32_matmul_precision() == "highest" + ) + or torch.version.hip + or torch.mtia.is_available() + ): + return "'ieee'" + else: + return "'tf32'" + + +flex_attention_template = TritonTemplate( + name="flex_attention", + grid=flex_attention_grid, + source=load_flex_template("flex_attention") + + load_flex_template("utilities") + + load_flex_template("common"), + always_freeze_layout=True, +) + + +@register_lowering(torch.ops.higher_order.flex_attention, type_promotion_kind=None) +def flex_attention( + query, + key, + value, + subgraph, + block_mask, + scale, + kernel_options: dict[str, Any], + score_mod_other_buffers, + mask_mod_other_buffers, +): + """The main lowering for the flex_attention hop + This can currently lower to one of 3 templates: + 1. Base Triton Template + 2. Flex Decode Triton Template + 3. Cpu specific CPP template + """ + if query.get_device().type == "cpu": + return lower_cpu( + query, + key, + value, + subgraph, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ) + # below is cuda path if device is not cpu + # tl.dot does not support embedding size less than 16 + small_dqk = V.graph.sizevars.evaluate_expr(sympy.Lt(query.get_size()[-1], 16)) + small_dv = V.graph.sizevars.evaluate_expr(sympy.Lt(value.get_size()[-1], 16)) + if small_dqk or small_dv: + raise NotImplementedError( + f"NYI: embedding dimension of the query, key, and value must be " + f"at least 16 but got E={query.get_size()[-1]} and Ev={value.get_size()[-1]}" + ) + + ( + _, # q_length + _, # kv_length + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + q_num_blocks, + q_indices, + full_q_num_blocks, + full_q_indices, + SPARSE_Q_BLOCK_SIZE, + SPARSE_KV_BLOCK_SIZE, + mask_graph, + ) = block_mask + + kernel_options, backend = _sanitize_kernel_options_for_triton(kernel_options) + + # Early check for FLASH backend: detect unsupported captured scalars before + # building subgraph buffers (which can trigger unbacked_bindings errors) + if backend == "FLASH": + from .flex_flash_attention import _has_unsupported_captured_scalars + + if _has_unsupported_captured_scalars( + score_mod_other_buffers, mask_mod_other_buffers + ): + raise RuntimeError( + "BACKEND='FLASH' but flash attention cannot be used: " + "NYI: score_mod or mask_mod captures a dynamic scalar (SymInt/SymFloat). " + "The FLASH backend cannot inline symbolic values into the CuteDSL template. " + "Workarounds: use BACKEND='TRITON', compile with dynamic=False, or pass the " + "value as a tensor on device instead of capturing a Python scalar." + ) + + placeholder_inps = [ + create_placeholder(name, dtype, query.get_device()) + for name, dtype in [ + ("score", query.get_dtype()), + ("b", torch.int32), + ("h", torch.int32), + ("m", torch.int32), + ("n", torch.int32), + ] + ] + subgraph_buffer = build_subgraph_buffer( + placeholder_inps + list(score_mod_other_buffers), subgraph + ) + freeze_irnodes(subgraph_buffer) + + mask_graph_placeholder_inps = [ + create_placeholder(name, dtype, query.get_device()) + for name, dtype in [ + ("b", torch.int32), + ("h", torch.int32), + ("m", torch.int32), + ("n", torch.int32), + ] + ] + mask_graph_buffer = build_subgraph_buffer( + mask_graph_placeholder_inps + list(mask_mod_other_buffers), mask_graph + ) + freeze_irnodes(mask_graph_buffer) + # Mark symbols in custom kernel options as static shapes and add guards. + kernel_options = { + k: V.graph.sizevars.guard_int(v) if isinstance(v, sympy.Symbol) else v + for k, v in kernel_options.items() + } + kernel_options.setdefault("FLOAT32_PRECISION", get_float32_precision()) + enable_gqa = V.graph.sizevars.evaluate_expr( + sympy.Ne(query.get_size()[1], key.get_size()[1]), + ) + + can_use_decode = _use_flex_decoding( + query, kv_indices, value, kernel_options, enable_gqa + ) + use_decode = (backend == "TRITON_DECODE") or (backend == "AUTO" and can_use_decode) + + if backend == "TRITON_DECODE" and not can_use_decode: + raise RuntimeError( + "BACKEND='TRITON_DECODE' was specified but flex_decoding cannot be used for this input. " + "flex_decoding is only available for short sequence lengths with specific configurations." + ) + + if use_decode: + return create_flex_decoding_kernel( + query, + key, + value, + block_mask, + scale, + kernel_options, + subgraph_buffer, + mask_graph_buffer, + score_mod_other_buffers, + mask_mod_other_buffers, + ) + + ( + query, + key, + value, + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + q_num_blocks, + q_indices, + full_q_num_blocks, + full_q_indices, + ) = maybe_realize( + [ + query, + key, + value, + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + q_num_blocks, + q_indices, + full_q_num_blocks, + full_q_indices, + ] + ) + + if _use_flex_flash_attention( + subgraph, + mask_graph, + kernel_options, + num_score_mod_placeholders=len(placeholder_inps), + backend=backend, + ): + return create_flex_flash_attention_kernel( + query, + key, + value, + block_mask, + scale, + kernel_options, + subgraph_buffer, + mask_graph_buffer, + score_mod_other_buffers, + mask_mod_other_buffers, + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + SPARSE_Q_BLOCK_SIZE, + SPARSE_KV_BLOCK_SIZE, + mask_graph=mask_graph, + subgraph=subgraph, + ) + + score_mod_other_buffers = maybe_realize(score_mod_other_buffers) + mask_mod_other_buffers = maybe_realize(mask_mod_other_buffers) + + freeze_irnodes(score_mod_other_buffers) + freeze_irnodes(mask_mod_other_buffers) + + Bq, Hq, seq_len_q, qk_head_dim = query.get_size() + Bkv, Hkv, seq_len_kv, v_head_dim = value.get_size() + assert V.graph.sizevars.evaluate_expr(sympy.Eq(Bq, Bkv) | sympy.Eq(Bkv, 1)), ( + f"Bq and Bkv must broadcastable. Got Bq={Bq} and Bkv={Bkv}" + ) + assert V.graph.sizevars.evaluate_expr(sympy.Gt(seq_len_q, 0)), ( + "Query length must be greater than 0" + ) + assert V.graph.sizevars.evaluate_expr(sympy.Gt(seq_len_kv, 0)), ( + "Key length must be greater than 0" + ) + + B = Bq + + seq_q_divisible = V.graph.sizevars.statically_known_true( + sympy.Eq(Mod(seq_len_q, 128), 0) + ) + seq_kv_divisible = V.graph.sizevars.statically_known_true( + sympy.Eq(Mod(seq_len_kv, 128), 0) + ) + if seq_q_divisible and seq_kv_divisible: + kernel_options.setdefault("IS_DIVISIBLE", True) + else: + kernel_options.setdefault("IS_DIVISIBLE", False) + + # NB it is okay that the v_head_dim is different + # We are using these to match fill order of the output. + q_strides = query.get_stride() + # Construct output layout with strides matching the query. + out_size = [B, Hq, seq_len_q, v_head_dim] + out_strides = infer_dense_strides(out_size, q_strides) + + layout = FixedLayout( + query.get_device(), + query.get_dtype(), + [B, Hq, seq_len_q, v_head_dim], + stride=[sympy.sympify(s) for s in out_strides], + ) + # see NOTE:[TritonTemplates with multiple outputs] + logsumexp_shape = [B, Hq, seq_len_q] + logsumexp = empty_strided( + logsumexp_shape, + None, + dtype=torch.float32, # The logsumexp is always stored in fp32 regardless of the input dtype + device=query.get_device(), + ) + max_scores = empty_strided( + logsumexp_shape, # Same shape as logsumexp + None, + dtype=torch.float32, # The max scores are always stored in fp32 regardless of the input dtype + device=query.get_device(), + ) + kernel_options.setdefault("SM_SCALE", scale) + + # Determine GQA broadcast factor. + gqa_shared_heads = FloorDiv(Hq, Hkv) + kernel_options.setdefault("GQA_SHARED_HEADS", gqa_shared_heads) + + # Inside of Triton kernel, only apply partial masking if partial blocks are computed. + # full_kv_num_blocks is None if partial blocks are not computed + has_full_blocks = full_kv_num_blocks is not None + kernel_options.setdefault("HAS_FULL_BLOCKS", has_full_blocks) + if not has_full_blocks: + full_kv_num_blocks, full_kv_indices = ( + empty(0, device=query.get_device()) for _ in range(2) + ) + + set_head_dim_values(kernel_options, qk_head_dim, v_head_dim, V.graph.sizevars) + + choices: list[Any] = [] + + dtype = query.get_dtype() + head_dim = V.graph.sizevars.guard_int(query.get_size()[-1]) + configs: list[FlexConfig] = V.choices.get_flex_attention_fwd_configs( + head_dim, dtype, query.get_device().type + ) + + # Mark SPARSE_KV_BLOCK_SIZE & SPARSE_Q_BLOCK_SIZE as static shapes and add guards. + SPARSE_KV_BLOCK_SIZE = V.graph.sizevars.guard_int(SPARSE_KV_BLOCK_SIZE) + SPARSE_Q_BLOCK_SIZE = V.graph.sizevars.guard_int(SPARSE_Q_BLOCK_SIZE) + + # Note, we don't need to pass in the captured buffers explicitly + # because they're implicitly added by the score_mod function + # We do need to explicitly pass it in for autotuning though. + original_kernel_options = kernel_options.copy() + # Default config for warp specialization + num_consumer_groups, num_buffers_warp_spec = 0, 0 + + for conf in configs: + cur_kernel_options = original_kernel_options.copy() + # Performance tuning + # Triton parameters + # Remove prefix for forward kernels options and delete backward kernel options. + for k in list(cur_kernel_options.keys()): + if k.startswith("fwd_"): + v = cur_kernel_options.pop(k) + cur_kernel_options[k[4:]] = v + if k.startswith("bwd_"): + cur_kernel_options.pop(k) + cur_kernel_options.setdefault("num_stages", conf.num_stages) + cur_kernel_options.setdefault("num_warps", conf.num_warps) + if cur_kernel_options.get("num_consumer_groups", False): + cur_kernel_options.setdefault("num_consumer_groups", num_consumer_groups) + cur_kernel_options.setdefault( + "num_buffers_warp_spec", num_buffers_warp_spec + ) + + # Intel GPU enables TMA by default + cur_kernel_options.setdefault("USE_TMA", bool(torch.xpu.is_available())) + + if cur_kernel_options["USE_TMA"] and not can_use_tma(query, key, value): + cur_kernel_options["USE_TMA"] = False + + cur_kernel_options.setdefault("BLOCK_M", conf.block_m) + cur_kernel_options.setdefault("BLOCK_N", conf.block_n) + # Blocksparse options + cur_kernel_options.setdefault("SPARSE_Q_BLOCK_SIZE", SPARSE_Q_BLOCK_SIZE) + cur_kernel_options.setdefault("SPARSE_KV_BLOCK_SIZE", SPARSE_KV_BLOCK_SIZE) + + if ( + cur_kernel_options["SPARSE_KV_BLOCK_SIZE"] % cur_kernel_options["BLOCK_N"] + != 0 + or cur_kernel_options["SPARSE_Q_BLOCK_SIZE"] % cur_kernel_options["BLOCK_M"] + != 0 + ): + if len(configs) == 1: + raise ValueError( + f"Q and KV block size must be divisible by BLOCK_M and BLOCK_N. We " + f"got Q_BLOCK_SIZE={cur_kernel_options['SPARSE_Q_BLOCK_SIZE']} and " + f"KV_BLOCK_SIZE={cur_kernel_options['SPARSE_KV_BLOCK_SIZE']}." + ) + continue + + # ROCm specific kernargs + for attrib in ["kpack", "matrix_instr_nonkdim", "waves_per_eu"]: + if hasattr(conf, attrib): + cur_kernel_options[attrib] = getattr(conf, attrib) + + error = flex_attention_template.maybe_append_choice( + choices=choices, + input_nodes=[ + query, + key, + value, + logsumexp, + max_scores, + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + ], + layout=layout, + subgraphs=[ + subgraph_buffer, + mask_graph_buffer, + ], + mutated_inputs=[ + logsumexp, + max_scores, + ], + call_sizes=query.get_size(), + **cur_kernel_options, + ) + if error is not None and len(configs) == 1: + raise error + inputs_for_autotuning = ( + [ + query, + key, + value, + logsumexp, + max_scores, + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + ] + + list(score_mod_other_buffers) + + list(mask_mod_other_buffers) + ) + input_gen_fns = { + 5: create_num_blocks_fake_generator(kv_indices), + 6: create_indices_fake, + 7: create_num_blocks_fake_generator(full_kv_indices), + 8: create_indices_fake, + } + + out, _ = autotune_select_algorithm( + "flex_attention", + choices, + # Need to filter out symbols since there is an invariant + # that all input_nodes are of type IRNode + [x for x in inputs_for_autotuning if isinstance(x, torch._inductor.ir.IRNode)], + layout, + input_gen_fns=input_gen_fns, + ) + + # need subgraph inputs and outputs to analyze all symints used in flex attention + out.data.data.subgraph_inps = list(score_mod_other_buffers) + list( + mask_mod_other_buffers + ) + out.data.data.subgraph_outs = get_fwd_subgraph_outputs( + subgraph_buffer, mask_graph_buffer + ) + + return (out, logsumexp, max_scores) + + +# ---------------------------- Backward HOP Implementation ---------------------------- + + +@SymbolicGridFn +def flex_attention_backward_grid( + batch_size, q_heads, num_queries, d_model, kv_heads, num_key_value, meta, *, cdiv +): + """How is this kernel parallelized? + We create a grid of (ceil_div(n_queries, query_block_size) * heads_ratio + ceil_div(n_kv, kv_block_size), batch_size, kv_heads) + Currently this is only parallelizing over batch* kv_heads, but we can, and want to + parallelize over ceil_div(q_heads//kv_heads * num_key_value, key_value_block_size). + To do this will either require atomic updates to some grad values or to have a two pass kernel design. + """ + return ( + cdiv(num_queries, meta["BLOCK_M2"]) * (q_heads // kv_heads) + + cdiv(num_key_value, meta["BLOCK_N1"]), + batch_size, + kv_heads, + ) + + +flex_attention_backward_template = TritonTemplate( + name="flex_attention_backward", + grid=flex_attention_backward_grid, + source=load_flex_template("flex_backwards") + load_flex_template("utilities"), + always_freeze_layout=True, +) + + +def validate_joint_graph(joint_graph: torch.fx.Graph): + """We do some pre lowering graph checks in order to raise nicer error messages""" + for node in joint_graph.nodes: + if ( + node.op == "call_function" + and node.target is torch.ops.flex_lib.zeros_and_scatter.default + ): + for user in node.users: + if user.op != "output": + raise NotImplementedError( + "Using multiple indexing operations on the same tensor that requires gradients " + "in a score_mod function is not currently supported. " + "This typically happens when indexing the same tensor multiple times, like:\n\n" + " def score_mod(score, b, h, q_idx, kv_idx):\n" + " return score + bias[q_idx] + bias[kv_idx] # bias used twice!\n\n" + "A valid workaround is to clone() the tensors that will be indexed multiple times. For example:\n\n" + " bias1 = bias.clone()\n" + " def score_mod(score, b, h, q_idx, kv_idx):\n" + " return score + bias[q_idx] + bias1[kv_idx]\n\n" + "Note that this solution will use additional memory." + ) + return + + +@dataclass(frozen=True) +class JointOutputResult: + """Results from processing joint outputs.""" + + grad_input: ComputedBuffer + captured_grads_compute: list[ComputedBuffer] + captured_grads: list[TensorBox | None] + mutated_grads: list[TensorBox] + + +def process_joint_outputs( + all_joint_outputs: SubgraphResults, num_placeholders: int +) -> JointOutputResult: + """Process joint outputs and extract various buffers needed for lowering + + Args: + all_joint_outputs: List of all the outputs from build_subgraphs + num_placeholders: The number of placeholder inputs, used to skip over unused backward compute buffers + + Returns: + JointOutputResult containing processed buffers and gradients + """ + assert isinstance(all_joint_outputs, list) + assert all_joint_outputs[0] is not None, ( + "joint_subgraph_buffer is None - this is a bug!" + ) + + joint_buffer = all_joint_outputs[0] + other_grads = all_joint_outputs[num_placeholders - 1 :] + + # outer_grads has the structure: Len(other_buffer_grads) if buffer doesn't require grad than it will be None + # We only grab the buffers that require grad for inlining into kernel + grads_compute = [buf for buf in other_grads if buf is not None] + + def get_out(buf): + if buf is None: + return None + assert isinstance(buf, ComputedBuffer) + assert buf.name is not None + return TensorBox.create(V.graph.get_buffer(buf.name)) + + grads_out = [get_out(x) for x in other_grads] + mutated_grads = [buf for buf in grads_out if buf is not None] + + return JointOutputResult( + grad_input=joint_buffer, + captured_grads_compute=grads_compute, + captured_grads=grads_out, + mutated_grads=mutated_grads, + ) + + +# TODO: We probably also need a layout constraint? +@register_lowering( + torch.ops.higher_order.flex_attention_backward, type_promotion_kind=None +) +def flex_attention_backward(*args, **kwargs): + """Lowering for the flex_attention_backward op in triton""" + ( + query, + key, + value, + out, + logsumexp, + grad_out, + grad_logsumexp, + fw_graph, + joint_graph, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ) = args + ( + _, # q_length + _, # kv_length + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + q_num_blocks, + q_indices, + full_q_num_blocks, + full_q_indices, + SPARSE_Q_BLOCK_SIZE, + SPARSE_KV_BLOCK_SIZE, + mask_graph, + ) = block_mask + + ( + query, + key, + value, + logsumexp, + grad_out, + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + q_num_blocks, + q_indices, + full_q_num_blocks, + full_q_indices, + ) = maybe_realize( + [ + query, + key, + value, + logsumexp, + grad_out, + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + q_num_blocks, + q_indices, + full_q_num_blocks, + full_q_indices, + ] + ) + + device = query.get_device() + dtype = query.get_dtype() + Bq, Hq, seq_len_q, qk_head_dim = query.get_size() + Bkv, Hkv, seq_len_kv, v_head_dim = value.get_size() + + assert V.graph.sizevars.evaluate_expr(sympy.Eq(Bq, Bkv) | sympy.Eq(Bkv, 1)), ( + f"Bq and Bkv must broadcastable. Got Bq={Bq} and Bkv={Bkv}" + ) + + kernel_options, backend = _sanitize_kernel_options_for_triton(kernel_options) + # Add check for mixed dtypes + if query.dtype != key.dtype or query.dtype != value.dtype: + raise ValueError( + f"Backward pass with mixed query, key, and value dtype is not supported, " + f"got query.dtype={query.dtype}, key.dtype={key.dtype}, " + f"and value.dtype={value.dtype}" + ) + # Mark symbols in custom kernel options as static shapes and add guards. + kernel_options = { + k: V.graph.sizevars.guard_int(v) if isinstance(v, sympy.Symbol) else v + for k, v in kernel_options.items() + } + kernel_options.setdefault("FLOAT32_PRECISION", get_float32_precision()) + kernel_options.setdefault("PRESCALE_QK", False) + kernel_options.setdefault("ROWS_GUARANTEED_SAFE", False) + kernel_options.setdefault("BLOCKS_ARE_CONTIGUOUS", False) + kernel_options.setdefault("WRITE_DQ", True) + seq_q_divisible = V.graph.sizevars.statically_known_true( + sympy.Eq(Mod(seq_len_q, 128), 0) + ) + seq_kv_divisible = V.graph.sizevars.statically_known_true( + sympy.Eq(Mod(seq_len_kv, 128), 0) + ) + if seq_q_divisible and seq_kv_divisible: + kernel_options.setdefault("IS_DIVISIBLE", True) + else: + kernel_options.setdefault("IS_DIVISIBLE", False) + + fwd_placeholder_inps = [ + create_placeholder(name, dtype, device) + for name, dtype in [ + ("score", dtype), + ("b", torch.int32), + ("h", torch.int32), + ("m", torch.int32), + ("n", torch.int32), + ] + ] + fw_subgraph_buffer = build_subgraph_buffer( + fwd_placeholder_inps + list(score_mod_other_buffers), fw_graph + ) + freeze_irnodes(fw_subgraph_buffer) + + joint_placeholder_inps = fwd_placeholder_inps + [ + create_placeholder("grad_score_mod", dtype, device) + ] + # Sometimes we have weird unused nodes here + joint_graph.graph_module.graph.eliminate_dead_code() + + # It is hard to raise nice errors for some joint graphs during subgraph lowering + # This lets us do some checks before attempting to lower + validate_joint_graph(joint_graph.graph_module.graph) + + all_joint_outputs = build_subgraph_buffer( + joint_placeholder_inps + list(score_mod_other_buffers), + joint_graph, + ) + + freeze_irnodes(all_joint_outputs) + + joint_outputs = process_joint_outputs( + all_joint_outputs, len(joint_placeholder_inps) + ) + + mask_graph_placeholder_inps = [ + create_placeholder(name, dtype, query.get_device()) + for name, dtype in [ + ("b", torch.int32), + ("h", torch.int32), + ("m", torch.int32), + ("n", torch.int32), + ] + ] + mask_graph_buffer = build_subgraph_buffer( + mask_graph_placeholder_inps + list(mask_mod_other_buffers), mask_graph + ) + freeze_irnodes(mask_graph_buffer) + + if _use_flex_flash_attention_backward( + fw_graph, + mask_graph, + backend=backend, + joint_outputs=joint_outputs, + score_mod_other_buffers=score_mod_other_buffers, + ): + needs_block_mask = not is_trivial_mask_graph(mask_graph.graph_module) + if ( + torch.are_deterministic_algorithms_enabled() + and not torch.is_deterministic_algorithms_warn_only_enabled() + and needs_block_mask + ): + raise NotImplementedError( + "Deterministic backward for flex_attention with block_mask using the FLASH backend " + "is not yet implemented. The TRITON backend supports deterministic backward." + ) + if torch.is_deterministic_algorithms_warn_only_enabled() and needs_block_mask: + warnings.warn( + "Deterministic backward for flex_attention with block_mask using the FLASH backend " + "is not yet implemented. Running non-deterministic backward.", + ) + # TODO: Implement dLSE support in flash-attention backward by folding + # grad_logsumexp into the dPsum preprocess step. + if grad_logsumexp is not None: + raise NotImplementedError( + "FLASH backend backward does not support differentiating through " + "logsumexp (dLSE). This happens when the loss depends on the LSE " + "output of flex_attention. " + "Use BACKEND='TRITON' or avoid differentiating through logsumexp." + ) + score_is_trivial = is_trivial_score_graph(fw_graph.graph_module) + return create_flex_flash_attention_backward_kernel( + query, + key, + value, + out, + logsumexp, + grad_out, + scale, + kernel_options, + SPARSE_Q_BLOCK_SIZE, + SPARSE_KV_BLOCK_SIZE, + fw_subgraph_buffer=None if score_is_trivial else fw_subgraph_buffer, + joint_subgraph_buffer=None + if score_is_trivial + else joint_outputs.grad_input, + score_mod_other_buffers=list(score_mod_other_buffers), + mask_graph_buffer=mask_graph_buffer if needs_block_mask else None, + q_num_blocks=q_num_blocks if needs_block_mask else None, + q_indices=q_indices if needs_block_mask else None, + full_q_num_blocks=full_q_num_blocks if needs_block_mask else None, + full_q_indices=full_q_indices if needs_block_mask else None, + ) + + # Construct layout with stride order matching K + key_size = [Bq, Hkv, seq_len_kv, qk_head_dim] + key_strides = infer_dense_strides(key_size, key.get_stride()) + + layout_broadcasted_k = FixedLayout( + key.get_device(), + key.get_dtype(), + key_size, + stride=[sympy.sympify(s) for s in key_strides], + ) + + # Create delta which will is needed for the bwd's kernel + mul_delta = lowerings[aten.mul](out, grad_out) + delta = lowerings[aten.sum](mul_delta, axis=-1) + delta = lowerings[prims.convert_element_type](delta, torch.float32) + if grad_logsumexp is not None: + grad_lse_exp2 = lowerings[aten.mul](grad_logsumexp, 1 / math.log(2)) + grad_lse_exp2 = ExternKernel.require_contiguous(grad_lse_exp2) + delta = lowerings[aten.sub](delta, grad_lse_exp2) + delta = ExternKernel.require_contiguous(delta) + delta, grad_lse_exp2 = maybe_realize([delta, grad_lse_exp2]) + else: + delta = ExternKernel.require_contiguous(delta) + (delta,) = maybe_realize([delta]) + + # # see NOTE:[TritonTemplates with multiple outputs] + query_size = [Bq, Hq, seq_len_q, qk_head_dim] + grad_query_strides = infer_dense_strides(query_size, query.get_stride()) + grad_query = empty_strided( + query_size, + stride=[sympy.sympify(s) for s in grad_query_strides], + dtype=query.get_dtype(), + device=query.get_device(), + ) + + # Construct output layout with stride order matching value + value_size = [Bq, Hkv, seq_len_kv, v_head_dim] + value_strides = infer_dense_strides(value_size, value.get_stride()) + + broadcasted_grad_value = empty_strided( + value_size, + stride=[sympy.sympify(s) for s in value_strides], + dtype=value.get_dtype(), + device=value.get_device(), + ) + + kernel_options.setdefault("SM_SCALE", scale) + + # Determine GQA factor + gqa_shared_heads = FloorDiv(Hq, Hkv) + kernel_options.setdefault("GQA_SHARED_HEADS", gqa_shared_heads) + + # Inside of Triton kernel, only apply partial masking if partial blocks are computed. + # full_kv_num_blocks is torch.zeros([1, 1, 1]) if partial blocks are not computed. + has_full_blocks = full_kv_num_blocks is not None + kernel_options.setdefault("HAS_FULL_BLOCKS", has_full_blocks) + if not has_full_blocks: + full_kv_num_blocks, full_kv_indices, full_q_num_blocks, full_q_indices = ( + empty(0, device=query.get_device()) for _ in range(4) + ) + + set_head_dim_values(kernel_options, qk_head_dim, v_head_dim, V.graph.sizevars) + + SPARSE_Q_BLOCK_SIZE = V.graph.sizevars.guard_int(SPARSE_Q_BLOCK_SIZE) + SPARSE_KV_BLOCK_SIZE = V.graph.sizevars.guard_int(SPARSE_KV_BLOCK_SIZE) + + choices: list[Any] = [] + + dtype = query.get_dtype() + head_dim = V.graph.sizevars.guard_int(query.get_size()[-1]) + configs: list[FlexBwDConfig] = V.choices.get_flex_attention_bwd_configs( + head_dim, dtype, query.get_device().type + ) + + # Default config for warp specialization + num_consumer_groups, num_buffers_warp_spec = 0, 0 + + original_kernel_options = kernel_options.copy() + + for conf in configs: + if ( + SPARSE_KV_BLOCK_SIZE % conf.block_n1 != 0 + or SPARSE_Q_BLOCK_SIZE % conf.block_m1 != 0 + or SPARSE_KV_BLOCK_SIZE % conf.block_n2 != 0 + or SPARSE_Q_BLOCK_SIZE % conf.block_m2 != 0 + ): + continue + + # Performance tuning + # Triton heuristics + cur_kernel_options = original_kernel_options.copy() + # Remove prefix for backward kernels options and delete forward kernel options. + for k in list(cur_kernel_options.keys()): + if k.startswith("bwd_"): + v = cur_kernel_options.pop(k) + cur_kernel_options[k[4:]] = v + if k.startswith("fwd_"): + cur_kernel_options.pop(k) + cur_kernel_options.setdefault("num_warps", conf.num_warps) + cur_kernel_options.setdefault("num_stages", conf.num_stages) + + if cur_kernel_options.get("num_consumer_groups", False): + cur_kernel_options.setdefault("num_consumer_groups", num_consumer_groups) + cur_kernel_options.setdefault( + "num_buffers_warp_spec", num_buffers_warp_spec + ) + + # Intel GPU enables TMA by default + cur_kernel_options.setdefault("USE_TMA", bool(torch.xpu.is_available())) + + if cur_kernel_options["USE_TMA"] and not can_use_tma(query, key, value): + cur_kernel_options["USE_TMA"] = False + + cur_kernel_options.setdefault("BLOCK_M1", conf.block_m1) + cur_kernel_options.setdefault("BLOCK_N1", conf.block_n1) + cur_kernel_options.setdefault("BLOCK_M2", conf.block_m2) + cur_kernel_options.setdefault("BLOCK_N2", conf.block_n2) + + # Blocksparse options + cur_kernel_options.setdefault("SPARSE_Q_BLOCK_SIZE", SPARSE_Q_BLOCK_SIZE) + cur_kernel_options.setdefault("SPARSE_KV_BLOCK_SIZE", SPARSE_KV_BLOCK_SIZE) + + # ROCm specific kernargs + for attrib in ["kpack", "matrix_instr_nonkdim", "waves_per_eu"]: + if hasattr(conf, attrib): + cur_kernel_options[attrib] = getattr(conf, attrib) + + flex_attention_backward_template.maybe_append_choice( + choices=choices, + input_nodes=[ + query, + key, + value, + logsumexp, + delta, + grad_out, + grad_query, + broadcasted_grad_value, + kv_num_blocks, + kv_indices, + q_num_blocks, + q_indices, + full_kv_num_blocks, + full_kv_indices, + full_q_num_blocks, + full_q_indices, + ], + layout=layout_broadcasted_k, # We use store_output only for grad_key + subgraphs=[ + fw_subgraph_buffer, + joint_outputs.grad_input, + mask_graph_buffer, + joint_outputs.captured_grads_compute, + ], + mutated_inputs=[ + grad_query, + broadcasted_grad_value, + *joint_outputs.mutated_grads, + ], + call_sizes=query.get_size() + key.get_size()[1:3], + **cur_kernel_options, + ) + inputs_for_autotuning = ( + [ + query, + key, + value, + logsumexp, + delta, + grad_out, + grad_query, + broadcasted_grad_value, + kv_num_blocks, + kv_indices, + q_num_blocks, + q_indices, + full_kv_num_blocks, + full_kv_indices, + full_q_num_blocks, + full_q_indices, + ] + + list(score_mod_other_buffers) + + list(mask_mod_other_buffers) + + joint_outputs.mutated_grads + ) + input_gen_fns = { + 8: create_num_blocks_fake_generator(kv_indices), # kv_num_blocks + 9: create_indices_fake, + 10: create_num_blocks_fake_generator(q_indices), # q_num_blocks + 11: create_indices_fake, + 12: create_num_blocks_fake_generator(full_kv_indices), # full_kv_num_blocks + 13: create_indices_fake, + 14: create_num_blocks_fake_generator(full_q_indices), # full_q_num_blocks + 15: create_indices_fake, + } + + broadcasted_grad_key, _ = autotune_select_algorithm( + "flex_attention_backward", + choices, + [x for x in inputs_for_autotuning if isinstance(x, torch._inductor.ir.IRNode)], + layout_broadcasted_k, + input_gen_fns=input_gen_fns, + ) # [Bq, Hkv, seq_len_kv, k_head_dim] + + # need subgraph inputs and outputs to analyze all symints used in flex attention + broadcasted_grad_key.data.data.subgraph_inps = list(score_mod_other_buffers) + list( + mask_mod_other_buffers + ) + broadcasted_grad_key.data.data.subgraph_outs = get_bwd_subgraph_outputs( + fw_subgraph_buffer, mask_graph_buffer, joint_outputs + ) + + if V.graph.sizevars.evaluate_expr(sympy.Eq(Bq, Bkv)): + grad_key = broadcasted_grad_key + grad_value = broadcasted_grad_value + else: + assert V.graph.sizevars.evaluate_expr(sympy.Gt(Bq, 1) & sympy.Eq(Bkv, 1)), ( + f"Bq and Bkv must broadcastable. " + f"Got Bq={V.graph.sizevars.evaluate_expr(Bq)} " + f"and Bkv={V.graph.sizevars.evaluate_expr(Bkv)}" + ) + grad_key = lowerings[aten.sum](broadcasted_grad_key, axis=0, keepdims=True) + grad_value = lowerings[aten.sum](broadcasted_grad_value, axis=0, keepdims=True) + + # Cast captured grads to match original buffer dtypes. Gradients are accumulated + # in fp32 for precision, then cast to the original dtype (e.g., bf16) here. + captured_grads = tuple( + to_dtype(g, orig.get_dtype()) + if g is not None and g.get_dtype() != orig.get_dtype() + else g + for g, orig in zip(joint_outputs.captured_grads, score_mod_other_buffers) + ) + + return (grad_query, grad_key, grad_value, captured_grads) + + +def get_bwd_subgraph_outputs( + subgraph_buffer: SubgraphResults, + mask_graph_buffer: SubgraphResults, + joint_outputs: JointOutputResult, +) -> list[ComputedBuffer | TensorBox | None]: + subgraph_buffer = ( + subgraph_buffer if isinstance(subgraph_buffer, Sequence) else [subgraph_buffer] + ) + mask_graph_buffer = ( + mask_graph_buffer + if isinstance(mask_graph_buffer, Sequence) + else [mask_graph_buffer] + ) + joint_output_buffers = [ + joint_outputs.grad_input, + *joint_outputs.captured_grads_compute, + *joint_outputs.captured_grads, + *joint_outputs.mutated_grads, + ] + + return [*subgraph_buffer, *mask_graph_buffer, *joint_output_buffers] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/flex_cpu.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/flex_cpu.py new file mode 100644 index 0000000000000000000000000000000000000000..b16975f7113ebe46695b726807fa2e554243ef79 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/flex_cpu.py @@ -0,0 +1,346 @@ +# mypy: allow-untyped-defs +"""CPU-specific implementations for flex attention""" + +import copy +import os +import sys +from typing import Any + +import sympy + +import torch +from torch._inductor.virtualized import V +from torch.utils._ordered_set import OrderedSet +from torch.utils._sympy.numbers import int_oo +from torch.utils._sympy.value_ranges import ValueRanges + +from ...codegen.cpp_flex_attention_template import CppFlexAttentionTemplate +from ...ir import Buffer, FixedLayout, TensorBox +from ...select_algorithm import autotune_select_algorithm +from .common import ( + build_subgraph_buffer, + build_subgraph_module_buffer, + contiguous_last_dim, + create_placeholder, + get_fwd_subgraph_outputs, + infer_dense_strides, + maybe_realize, +) + + +def check_cpu_supported(): + requires_avx2_on_cpu = ( + torch.cpu._is_avx2_supported() and os.getenv("ATEN_CPU_CAPABILITY") != "default" + ) + supported = ( + requires_avx2_on_cpu + and not torch.xpu.is_available() + and sys.platform != "darwin" + ) + return supported + + +def lower_cpu( + query, + key, + value, + subgraph, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, +): + """CPP based template for flex attention for x86 CPUs""" + ( + _, # q_length + _, # kv_length + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + q_num_blocks, + q_indices, + full_q_num_blocks, + full_q_indices, + SPARSE_Q_BLOCK_SIZE, + SPARSE_KV_BLOCK_SIZE, + mask_graph, + ) = block_mask + + if query.dtype != key.dtype or query.dtype != value.dtype: + raise ValueError( + f"Mixed query, key, and value dtype is not supported on this platform, " + f"got query.dtype: {query.dtype}, key.dtype: {key.dtype}, " + f"and value.dtype: {value.dtype}." + ) + + if kernel_options["OUTPUT_LOGSUMEXP"]: + raise NotImplementedError( + "torch.compile on CPU only supports inference and `return_lse` is not supported yet." + ) + if not check_cpu_supported(): + raise NotImplementedError( + "torch.compile on current platform is not supported for CPU." + ) + + fake_buffers: list[Buffer] = [] # noqa: F821 + + # [Note] Handle the case where the split sizes are not statically known. + # The value of cur_qSplitSize and cur_kvSplitSize are decided during runtime. + # We use symbols to represent them during the compilation here. + # They'll be replaced by the string "cur_qSplitSize" and "cur_kvSplitSize" in + # the modification function of the CppFlexAttentionTemplate class. + cur_qSplitSize = V.graph.sizevars.shape_env.create_unbacked_symint().node.expr + cur_kvSplitSize = V.graph.sizevars.shape_env.create_unbacked_symint().node.expr + shape_env = V.graph.sizevars.shape_env + + # We don't know the concrete value of cur_qSplitSize and cur_kvSplitSize during the compilation. + # Mark symbols > 1 to ensure broadcasting is always applied. + # This avoids treating them as equal when `eq(var, 1)` is evaluated in `broadcast_symbolic_shapes`. + shape_env.var_to_range[cur_qSplitSize] = ValueRanges(2, int_oo) + shape_env.var_to_range[cur_kvSplitSize] = ValueRanges(2, int_oo) + + score_dtype = torch.float + placeholder_inps = [ + create_placeholder(name, dtype, query.get_device(), size) + for name, dtype, size in [ + ("score", score_dtype, [cur_qSplitSize, cur_kvSplitSize]), + ("b", torch.int64, []), + ("h", torch.int64, []), + ("q_idx", torch.int64, [cur_qSplitSize, 1]), + ("kv_idx", torch.int64, [1, cur_kvSplitSize]), + ] + ] + subgraph_buffer = build_subgraph_buffer( + placeholder_inps + list(score_mod_other_buffers), subgraph + ) + if subgraph_buffer is not None: + if isinstance(subgraph_buffer, list): + for _buf in subgraph_buffer: + if _buf is not None: + _buf.freeze_layout() + else: + subgraph_buffer.freeze_layout() + mask_graph_placeholder_inps = [ + create_placeholder(name, dtype, query.get_device(), size) + for name, dtype, size in [ + ("score", score_dtype, [cur_qSplitSize, cur_kvSplitSize]), + ("b", torch.int64, []), + ("h", torch.int64, []), + ("q_idx", torch.int64, [cur_qSplitSize, 1]), + ("kv_idx", torch.int64, [1, cur_kvSplitSize]), + ] + ] + + # The original mask_graph works on a scalar and only includes + # the logic of calculating the mask value. + # We need to add the logic of applying the mark to the qk_data tensor + # into the graph for the later codegen of this part. + # Example: + # mask_graph: + # def mask_fn(b, h, q_idx, kv_idx): + # mask = q_idx >= kv_idx + # return mask + # The converted_mask_graph should be: + # def converted_mask_fn(qk_data, b, h, q_idx, kv_idx): + # mask = q_idx >= kv_idx + # qk_data = torch.where(mask, qk_data, torch.full_like(qk_data, -float("inf"))) + # return qk_data + def convert_mask_graph_module(mask_graph): + gm = copy.deepcopy(mask_graph.graph_module) + graph = gm.graph + # Add qk_data as the first input + with graph.inserting_before(next(iter(graph.nodes))): + qk_data_node = graph.placeholder("qk_data") + + # Find the node that returns the mask + output_node = None + for node in graph.nodes: + if node.op == "output": + output_node = node + break + + # Get the mask node + assert output_node is not None + mask_node = output_node.args[0] + + size_node = [cur_qSplitSize, cur_kvSplitSize] + # Create a new node for torch.full + with graph.inserting_after(mask_node): + full_node = graph.call_function( + torch.full, + args=(size_node, -float("inf")), + kwargs={"dtype": score_dtype}, + ) + + # Create a new node for torch.where + with graph.inserting_after(full_node): + where_node = graph.call_function( + torch.ops.aten.where, args=(mask_node, qk_data_node, full_node) + ) + + # Update the output node to return the result of torch.where + output_node.args = (where_node,) + + graph.lint() + converted = torch.fx.GraphModule(gm, graph) + return converted + + converted_mask_graph_module = convert_mask_graph_module(mask_graph) + + mask_graph_buffer = build_subgraph_module_buffer( + mask_graph_placeholder_inps + list(mask_mod_other_buffers), + converted_mask_graph_module, + ) + + # Clear the pending fresh unbacked symbols that are created for cur_qSplitSize and cur_kvSplitSize in the current kernel. + pending = V.graph.sizevars.shape_env.pending_fresh_unbacked_symbols + V.graph.sizevars.shape_env.pending_fresh_unbacked_symbols = [ + x for x in pending if x not in (cur_qSplitSize, cur_kvSplitSize) + ] + + buffer_list = ( + placeholder_inps + + list(score_mod_other_buffers) + + mask_graph_placeholder_inps + + list(mask_mod_other_buffers) + ) + for item in buffer_list: + if isinstance(item, TensorBox): + fake_buffers.append(item.data.data) # type: ignore[attr-defined] + + # CPU kernel requires last dim to be contiguous + query, key, value = map(contiguous_last_dim, [query, key, value]) + + ( + query, + key, + value, + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + q_num_blocks, + q_indices, + full_q_num_blocks, + full_q_indices, + ) = maybe_realize( + [ + query, + key, + value, + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + q_num_blocks, + q_indices, + full_q_num_blocks, + full_q_indices, + ] + ) + + if len(OrderedSet([query.get_name(), key.get_name(), value.get_name()])) != 3: + raise NotImplementedError( + "Unsupported for now if query, key, value are the same buffer." + ) + if query.get_dtype() not in [torch.float, torch.bfloat16, torch.float16]: + raise NotImplementedError( + "`torch.float` , `torch.float16` and `torch.bfloat16` are supported in FlexAttention for CPU device. " + f"Found input tensors are `{query.get_dtype()}`." + ) + score_mod_other_buffers = maybe_realize(score_mod_other_buffers) + mask_mod_other_buffers = maybe_realize(mask_mod_other_buffers) + Bq, Hq, seq_len_q, qk_head_dim = query.get_size() + Bkv, Hkv, seq_len_kv, v_head_dim = value.get_size() + B = Bq + + # Construct output layout with strides matching the query. + out_size = [B, Hq, seq_len_q, v_head_dim] + out_strides = infer_dense_strides(out_size, query.get_stride()) + + layout = FixedLayout( + query.get_device(), + query.get_dtype(), + [B, Hq, seq_len_q, v_head_dim], + stride=[sympy.sympify(s) for s in out_strides], + ) + _choices: list[Any] = [] + input_nodes = [query, key, value, kv_num_blocks, kv_indices] + if not full_kv_num_blocks: + no_full_kv_block = True + else: + no_full_kv_block = False + input_nodes += [full_kv_num_blocks] + input_nodes += [full_kv_indices] + has_other_buffer = False + kernel_input_name_to_buffer = {} + if score_mod_other_buffers or mask_mod_other_buffers: + has_other_buffer = True + + for prefix, buffers in [ + ("score_others", score_mod_other_buffers), + ("mask_others", mask_mod_other_buffers), + ]: + kernel_input_name_to_buffer.update( + {f"{prefix}_{i}": buf for i, buf in enumerate(buffers)} + ) + input_nodes += [ + value + for value in kernel_input_name_to_buffer.values() + if not isinstance(value, sympy.Symbol) + ] + + skip_mask_score = kernel_options.get("SKIP_MASK_SCORE", False) + # Mark SPARSE_KV_BLOCK_SIZE & SPARSE_Q_BLOCK_SIZE as static shapes and add guards. + SPARSE_KV_BLOCK_SIZE = V.graph.sizevars.guard_int(SPARSE_KV_BLOCK_SIZE) + SPARSE_Q_BLOCK_SIZE = V.graph.sizevars.guard_int(SPARSE_Q_BLOCK_SIZE) + assert V.graph.sizevars.evaluate_expr( + sympy.Le(seq_len_q, sympy.Mul(kv_indices.get_size()[-2], SPARSE_Q_BLOCK_SIZE)) + ), ( + "Q seqlen must be smaller than the block_mask size in the Q dimension, considering pass a larger block_mask." + ) + assert V.graph.sizevars.evaluate_expr( + sympy.Le(seq_len_kv, sympy.Mul(kv_indices.get_size()[-1], SPARSE_KV_BLOCK_SIZE)) + ), ( + "KV seqlen must be smaller than the block_mask size in the KV dimension, considering pass a larger block_mask." + ) + CppFlexAttentionTemplate.add_choices( + choices=_choices, + input_nodes=input_nodes, + layout=layout, + scale=scale, + score_mod=None if skip_mask_score else subgraph_buffer, + mask_mod=None if skip_mask_score else mask_graph_buffer, + kv_block_size=SPARSE_KV_BLOCK_SIZE, + q_block_size=SPARSE_Q_BLOCK_SIZE, + has_other_buffer=has_other_buffer, + no_full_kv_block=no_full_kv_block, + fake_buffers=fake_buffers, + len_score_other=len(score_mod_other_buffers), + len_mask_other=len(mask_mod_other_buffers), + kernel_input_name_to_buffer=kernel_input_name_to_buffer, + block_vars=(cur_qSplitSize, cur_kvSplitSize), + ) + inputs_for_autotuning = [ + query, + key, + value, + ] + res, _ = autotune_select_algorithm( + "flex_attention", + _choices, + inputs_for_autotuning, + layout, + ) + + # need subgraph inputs and outputs to analyze all symints used in flex attention + res.data.data.subgraph_inps = list(score_mod_other_buffers) + list( + mask_mod_other_buffers + ) + res.data.data.subgraph_outs = get_fwd_subgraph_outputs( + subgraph_buffer, mask_graph_buffer + ) + + return (res,) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/flex_decoding.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/flex_decoding.py new file mode 100644 index 0000000000000000000000000000000000000000..2cd25c5fa1063aae5920b894bf8df42efaf003fe --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/flex_decoding.py @@ -0,0 +1,460 @@ +# mypy: allow-untyped-defs +"""Triton Implementation of the flex_attention Kernel for short query length (FlexDecoding)""" + +import logging +from typing import Any + +import sympy + +import torch +from torch._inductor.virtualized import V +from torch.utils._sympy.functions import FloorDiv, Mod + +from ... import ir +from ...ir import FixedLayout, FlexibleLayout +from ...lowering import empty, empty_strided, lowerings +from ...runtime.runtime_utils import ceildiv, is_power_of_2, next_power_of_2 +from ...select_algorithm import ( + autotune_select_algorithm, + SymbolicGridFn, + TritonTemplate, +) +from ...utils import can_use_tma +from .common import ( + create_indices_fake, + create_num_blocks_fake_generator, + freeze_irnodes, + get_fwd_subgraph_outputs, + load_flex_template, + maybe_realize, + set_head_dim_values, +) + + +aten = torch.ops.aten +prims = torch.ops.prims + +log = logging.getLogger(__name__) + + +def _use_flex_decoding(query, kv_indices, value, kernel_options, enable_gqa) -> bool: + """Decide which kernel to use, return true if use flex decoding kernel. + Note: + Since the number of splits is calculated based of the number of batch and head dims + we need to ensure that the batch and head dims are statically known. Otherwise we just + use the main flex_attention kernel. + """ + force_flex = kernel_options.get("FORCE_USE_FLEX_ATTENTION", False) + + short_query_length = V.graph.sizevars.evaluate_expr( + sympy.Lt(query.get_size()[-2], 128) + ) + non_zero_length = V.graph.sizevars.evaluate_expr(sympy.Gt(query.get_size()[-2], 0)) + static_batch = isinstance(query.get_size()[0], (int, sympy.Integer)) + static_num_heads = isinstance(query.get_size()[1], (int, sympy.Integer)) + if enable_gqa: + # in the current flex decoding triton kernel, grouped query heads for the + # same kv head are handled by the same block. So it's hard to support different + # kv num blocks for grouped query heads. We just fall back to main flex_attention + # kernel where each query head is handled by a separate block. + valid_block_mask_num_heads = V.graph.sizevars.evaluate_expr( + sympy.Eq(kv_indices.get_size()[1], 1) + ) + else: + valid_block_mask_num_heads = V.graph.sizevars.evaluate_expr( + sympy.Or( + sympy.Eq(kv_indices.get_size()[1], 1), + sympy.Eq(kv_indices.get_size()[1], query.get_size()[1]), + ) + ) + + Hq = query.get_size()[1] + Hkv = value.get_size()[1] + ratio = FloorDiv(Hq, Hkv) + + pw_of_two = V.graph.sizevars.guard_or_false( + sympy.And(sympy.Gt(ratio, 0), sympy.Eq(ratio & (ratio - 1), 0)) + ) + + out = ( + not force_flex + and not kernel_options.get("OUTPUT_MAX", False) + and short_query_length + and static_batch + and static_num_heads + and non_zero_length + and valid_block_mask_num_heads + and pw_of_two + ) + log.debug( + "Use flex decoding %s, force_flex_attention=%s, short_query_length=%s, static_batch=%s, static_num_heads=%s", # noqa: B950 + out, + force_flex, + short_query_length, + static_batch, + static_num_heads, + ) + return out + + +@SymbolicGridFn +def flex_decoding_grid(batch_size, kv_heads, gqa_group_size, seq_len_q, d_model, meta): + """How is this kernel parallelized? + We create a grid of (batch_size * kv_heads, SPLIT_KV, 1) + Each block is responsible for iterating over blocks of keys and values calculating + the local output for their tile of keys and values over all full length of query. + groups of SPLIT_KV blocks then combine their output to produce the final result. + """ + + BLOCK_M = meta["BLOCK_M"] + num_block_m = ceildiv(seq_len_q * gqa_group_size, BLOCK_M) + return (num_block_m, batch_size * kv_heads, meta["SPLIT_KV"]) + + +flex_decoding_template = TritonTemplate( + name="flex_decoding", + grid=flex_decoding_grid, + source=load_flex_template("flex_decode") + + load_flex_template("utilities") + + load_flex_template("common"), + always_freeze_layout=True, +) + + +def get_split_k(B: int, H: int, Mk: int) -> int: + if torch.xpu.is_available(): + num_SM = torch.xpu.get_device_properties("xpu").gpu_subslice_count + else: + num_SM = torch.cuda.get_device_properties("cuda").multi_processor_count + bh = max(B * H, 1) # NOTE: Handle B*h=0 case + assert isinstance(bh, (int, sympy.Integer)), "B and H must be concrete integers" + split_k = num_SM // bh * 2 # Each SM should at least get one block. + # TODO: workload evening at runtime for splits fully masked out. + # Before we have runtime workload evening, assign 2 splits per SM. + split_k = max(split_k, 1) + + return split_k + + +def create_flex_decoding_kernel(*args, **kwargs): + """Flex decode lowering that is optimized for small Q_LEN and GQA packing""" + ( + query, + key, + value, + block_mask, + scale, + kernel_options, + score_mod_subgraph, + mask_mod_subgraph, + score_mod_other_buffers, + mask_mod_other_buffers, + ) = args + ( + _, # q_length + _, # kv_length + kv_num_blocks, + kv_indices, + full_kv_num_blocks, # full_kv_num_blocks, + full_kv_indices, # full_kv_indices, + _, # q_num_blocks + _, # q_indices + _, # full_q_num_blocks, + _, # full_q_indices, + SPARSE_Q_BLOCK_SIZE, + SPARSE_KV_BLOCK_SIZE, + _, + ) = block_mask + + Bq, Hq, seq_len_q, qk_head_dim = query.get_size() + Bkv, Hkv, seq_len_kv, v_head_dim = value.get_size() + + assert V.graph.sizevars.evaluate_expr(sympy.Eq(Bq, Bkv) | sympy.Eq(Bkv, 1)), ( + f"Bq and Bkv must broadcastable. Got Bq={Bq} and Bkv={Bkv}" + ) + + B = Bq + kernel_options = dict(kernel_options) + # Mark symbols in custom kernel options as static shapes and add guards. + kernel_options = { + k: V.graph.sizevars.guard_int(v) if isinstance(v, sympy.Symbol) else v + for k, v in kernel_options.items() + } + + seq_q_divisible = V.graph.sizevars.statically_known_true( + sympy.Eq(Mod(seq_len_q, 128), 0) + ) + seq_kv_divisible = V.graph.sizevars.statically_known_true( + sympy.Eq(Mod(seq_len_kv, 128), 0) + ) + if seq_q_divisible and seq_kv_divisible: + kernel_options.setdefault("IS_DIVISIBLE", True) + else: + kernel_options.setdefault("IS_DIVISIBLE", False) + + # Calculate GQA head sharing + gqa_shared_heads = FloorDiv(Hq, Hkv) + if not is_power_of_2(gqa_shared_heads): + raise ValueError( + "Number of shared query heads sharing the same KV head must be power of 2. " + ) + kernel_options.setdefault("GQA_SHARED_HEADS", gqa_shared_heads) + + # Determine if there are "full" blocks where we only need to apply score_mod, and can skip mask_mod + has_full_blocks = full_kv_num_blocks is not None + kernel_options.setdefault("HAS_FULL_BLOCKS", has_full_blocks) + if not has_full_blocks: + # Create a plackeholder full block list in case it is empty + full_kv_num_blocks, full_kv_indices = ( + empty(0, device=query.get_device()) for _ in range(2) + ) + + ( + query, + key, + value, + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + ) = maybe_realize( + [ + query, + key, + value, + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + ] + ) + score_mod_other_buffers = maybe_realize(score_mod_other_buffers) + mask_mod_other_buffers = maybe_realize(mask_mod_other_buffers) + + freeze_irnodes(score_mod_other_buffers) + freeze_irnodes(mask_mod_other_buffers) + + choices: list[Any] = [] + dtype = key.get_dtype() + head_dim = V.graph.sizevars.guard_int(key.get_size()[-1]) + configs = V.choices.get_flex_decode_configs( + head_dim, dtype, query.get_device().type + ) + + # TODO: fix autotuning. + + kernel_options.setdefault("SM_SCALE", scale) + kernel_options.setdefault("SPLIT_KV", get_split_k(B, Hkv, seq_len_kv)) + MAX_SPLIT_KV = kernel_options["SPLIT_KV"] + + # create config dependent intermediate buffers + buf_ACC_shape = [B, MAX_SPLIT_KV, Hq, seq_len_q, v_head_dim] + buf_ML_shape = buf_ACC_shape[:-1] + buf_M = empty_strided( + buf_ML_shape, + None, + dtype=torch.float32, # The rowmax is always stored in fp32 regardless of the input dtype + device=query.get_device(), + ) + buf_L = empty_strided( + buf_ML_shape, + None, + dtype=torch.float32, # The intermediate sumexp is always stored in fp32 regardless of the input dtype + device=query.get_device(), + ) + + layout_acc = FixedLayout( + query.get_device(), + torch.float32, + buf_ACC_shape, + FlexibleLayout.contiguous_strides(buf_ACC_shape), + ) + + set_head_dim_values(kernel_options, qk_head_dim, v_head_dim, V.graph.sizevars) + + kernel_options.setdefault( + "BLOCK_M", + ( + # m + # if V.graph.sizevars.evaluate_expr(sympy.Lt(query.get_size()[-2], 0)) + # else # Always use a BLOCK_M > 16 before Triton fix https://github.com/triton-lang/triton/pull/4061 is in pin + max( + next_power_of_2( + V.graph.sizevars.optimization_hint(seq_len_q) * gqa_shared_heads + ), + 1 if torch.xpu.is_available() else 16, + ) + ), + ) + + query = ir.ExternKernel.realize_input(query) + stride_b, stride_hq, stride_seq_len_q, stride_qk_head_dim = query.get_stride() + + # Reshape query for GQA: [B, Hq, Mq, D] -> [B, Hkv, G, Mq, D] + gqa_query_shape = (B, Hkv, gqa_shared_heads, seq_len_q, qk_head_dim) + gqa_query_stride = ( + stride_b, + stride_hq * gqa_shared_heads, + stride_hq, + stride_seq_len_q, + stride_qk_head_dim, + ) + query = lowerings[aten.as_strided](query, gqa_query_shape, gqa_query_stride) + + kernel_options.setdefault( + "SAFE_M_BOUNDARY", + Mod(seq_len_q * gqa_shared_heads, kernel_options["BLOCK_M"]) == 0, + ) + # TODO: This feels sketchy + kernel_options.setdefault("SAFE_N_BOUNDARY", True) + # Mark SPARSE_KV_BLOCK_SIZE as static shapes and add guards. + SPARSE_Q_BLOCK_SIZE = V.graph.sizevars.guard_int(SPARSE_Q_BLOCK_SIZE) + SPARSE_KV_BLOCK_SIZE = V.graph.sizevars.guard_int(SPARSE_KV_BLOCK_SIZE) + + original_kernel_options = kernel_options.copy() + # Note, we don't need to pass in the captured buffers explicitly + # because they're implicitly added by the score_mod function + # We do need to explicitly pass it in for autotuning though. + + # Default config for warp specialization + num_consumer_groups, num_buffers_warp_spec = 0, 0 + + for conf in configs: + if conf.block_n > SPARSE_KV_BLOCK_SIZE: + conf.block_n = SPARSE_KV_BLOCK_SIZE + + if SPARSE_Q_BLOCK_SIZE % kernel_options["BLOCK_M"] != 0: + continue + + if SPARSE_KV_BLOCK_SIZE % conf.block_n != 0: + continue + + cur_kernel_options = original_kernel_options.copy() + # Remove prefix for forward kernels options and delete backward kernel options. + for k in list(cur_kernel_options.keys()): + if k.startswith("fwd_"): + v = cur_kernel_options.pop(k) + cur_kernel_options[k[4:]] = v + if k.startswith("bwd_"): + cur_kernel_options.pop(k) + # Performance tuning + cur_kernel_options.setdefault("BLOCK_N", conf.block_n) + cur_kernel_options.setdefault("SPARSE_Q_BLOCK_SIZE", SPARSE_Q_BLOCK_SIZE) + cur_kernel_options.setdefault("SPARSE_KV_BLOCK_SIZE", SPARSE_KV_BLOCK_SIZE) + cur_kernel_options.setdefault("num_warps", conf.num_warps) + cur_kernel_options.setdefault("num_stages", conf.num_stages) + + if cur_kernel_options.get("num_consumer_groups", False): + cur_kernel_options.setdefault("num_consumer_groups", num_consumer_groups) + cur_kernel_options.setdefault( + "num_buffers_warp_spec", num_buffers_warp_spec + ) + + # Intel GPU enables TMA by default + cur_kernel_options.setdefault("USE_TMA", bool(torch.xpu.is_available())) + + if cur_kernel_options["USE_TMA"] and not can_use_tma(query, key, value): + cur_kernel_options["USE_TMA"] = False + + # Add ROCm-specific parameters if they exist in the config + for attrib in ["kpack", "matrix_instr_nonkdim", "waves_per_eu"]: + if hasattr(conf, attrib): + cur_kernel_options[attrib] = getattr(conf, attrib) + + flex_decoding_template.maybe_append_choice( + choices=choices, + input_nodes=[ + query, + key, + value, + buf_M, + buf_L, + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + ], + layout=layout_acc, + subgraphs=[ + score_mod_subgraph, + mask_mod_subgraph, + ], + mutated_inputs=[buf_M, buf_L], + call_sizes=query.get_size(), + **cur_kernel_options, + ) + + filtered_score_mod_buffers = [ + buf for buf in score_mod_other_buffers if not isinstance(buf, sympy.Symbol) + ] + filtered_mask_mod_buffers = [ + buf for buf in mask_mod_other_buffers if not isinstance(buf, sympy.Symbol) + ] + + inputs_for_flex_decoding = ( + [ + query, + key, + value, + buf_M, + buf_L, + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + ] + + filtered_score_mod_buffers + + filtered_mask_mod_buffers + ) + + input_gen_fns = { + 5: create_num_blocks_fake_generator(kv_indices), + 6: create_indices_fake, + 7: create_num_blocks_fake_generator(full_kv_indices), + 8: create_indices_fake, + } + + buf_ACC, _ = autotune_select_algorithm( + "flex_decoding", + choices, + inputs_for_flex_decoding, + layout_acc, + input_gen_fns=input_gen_fns, + ) + + # need subgraph inputs and outputs to analyze all symints used in flex attention + buf_ACC.data.data.subgraph_inps = list(score_mod_other_buffers) + list( + mask_mod_other_buffers + ) + buf_ACC.data.data.subgraph_outs = get_fwd_subgraph_outputs( + score_mod_subgraph, mask_mod_subgraph + ) + + # Reduction + + g_M = lowerings[aten.max](buf_M, dim=1, keepdim=True)[0] + # See [Note] Handle fully masked out rows: + # g_M Is the global max among split kv blocks. + masked_rows = lowerings[aten.eq](g_M, -float("inf")) + adj_M = lowerings[aten.sub](buf_M, g_M) + adj_M = lowerings[aten.where](masked_rows, 0, adj_M) + alpha = lowerings[aten.exp2](adj_M) + + buf_L = lowerings[aten.mul](buf_L, alpha) + g_L = lowerings[aten.sum](buf_L, axis=1) + masked_rows_squeezed = lowerings[aten.squeeze](masked_rows, dim=1) + g_L = lowerings[aten.where](masked_rows_squeezed, 1.0, g_L) + logsumexp = lowerings[aten.log2](g_L) + logsumexp = lowerings[aten.add](logsumexp, lowerings[aten.squeeze](g_M, dim=1)) + + alpha_unseq = lowerings[aten.unsqueeze](alpha, 4) + buf_ACC = lowerings[aten.mul](buf_ACC, alpha_unseq) + output = lowerings[aten.sum](buf_ACC, axis=1) + L_unseq = lowerings[aten.unsqueeze](g_L, 3) + output = lowerings[aten.div](output, L_unseq) + output = lowerings[prims.convert_element_type](output, query.get_dtype()) + + return ( + output, + logsumexp, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/flex_flash_attention.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/flex_flash_attention.py new file mode 100644 index 0000000000000000000000000000000000000000..9401da5155c81675d419cc3c4cd872c9965887c2 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/flex_flash_attention.py @@ -0,0 +1,689 @@ +# mypy: allow-untyped-defs +"""Call into flash-attention 4 for flexattention""" + +import dataclasses +import functools +import importlib +from collections.abc import Callable, Sequence +from contextlib import contextmanager +from typing import Any, cast, Literal + +import sympy +from sympy import Expr, Integer + +import torch +from torch.fx import GraphModule + +from ...ir import FixedLayout, ShapeAsConstantBuffer, Subgraph, TensorBox +from ...lowering import empty_strided +from ...select_algorithm import autotune_select_algorithm +from ...virtualized import V +from .common import ( + create_indices_fake, + create_num_blocks_fake_generator, + infer_dense_strides, + load_flex_template, + SubgraphResults, +) + + +@dataclasses.dataclass +class FlexFlashConfig: + """Autotuning configuration for CuteDSL flex flash attention kernels. + + score_mod_vec_size: Number of elements processed per thread in the score_mod + application loop. Maps to score_mod.__vec_size__ in CuTe flash attention. + None uses the kernel default. Only effective for forward; backward does + not currently support vectorized score_mod. + """ + + score_mod_vec_size: int | None = None + + +def _get_flex_flash_fwd_configs( + has_score_mod: bool, + has_aux_tensors: bool, +) -> list[FlexFlashConfig]: + if not has_score_mod or not torch._inductor.config.max_autotune: + return [FlexFlashConfig()] + if has_aux_tensors: + return [FlexFlashConfig(score_mod_vec_size=1)] + return [ + FlexFlashConfig(score_mod_vec_size=v) for v in (1, 2, 4, 8, 16, 32, 64, 128) + ] + + +def _get_flex_flash_bwd_configs() -> list[FlexFlashConfig]: + return [FlexFlashConfig()] + + +aten = torch.ops.aten +prims = torch.ops.prims + + +@functools.lru_cache(maxsize=1) +def ensure_flash_available() -> bool: + """Check if flash-attn is importable; cache the result for reuse. + + Call ensure_flash_available.cache_clear() after installing flash-attn + in the same interpreter to retry the import. + """ + try: + return importlib.util.find_spec("flash_attn.cute") is not None # type: ignore[attr-defined] + except ImportError: + return False + + +from ...codegen.cutedsl.cutedsl_template import CuteDSLTemplate + + +flash_attention_cutedsl_template = CuteDSLTemplate( + name="flash_attention_cutedsl", source=load_flex_template("flash_attention") +) +flash_attention_backward_cutedsl_template = CuteDSLTemplate( + name="flash_attention_backward_cutedsl", + source=load_flex_template("flash_attention_backward"), +) + + +class HierarchicalIndex(sympy.Function): + """ + Inert wrapper to carry an N-D index tuple through Inductor's SymPy-based IR. + + Inductor generally represents a tensor index as a single `sympy.Expr` (often a + flattened linear offset in memory). CuteDSL, however, wants structured coordinates so it + can emit `tensor[i, j, ...]` and handle strides internally. We therefore wrap + the per-dimension indices in a `sympy.Function` node: this keeps the value a + `sympy.Expr` for existing substitution/CSE machinery, while letting CuteDSL + codegen pattern-match and unpack the coordinates via `index.args`. + + `eval()` returns None to keep the node inert (no simplification/flattening). + + These nodes are intended to be short-lived wrappers and are only interpreted by + CuteDSL codegen (see `ModificationWrapperCuteDSL.load` in + `torch/_inductor/codegen/cutedsl/cutedsl_kernel.py`). + """ + + @classmethod + def eval(cls, *args): + return None + + +def _hierarchical_indexer_cute( + size: Sequence[int], + stride: Sequence[int] | None = None, + offset: Expr = Integer(0), +) -> Callable[[Sequence[Expr]], Expr]: + """Return an indexer that preserves multi-dimensional indices for CuteDSL.""" + + def indexer(indices: Sequence[Expr]) -> Expr: + assert offset == Integer(0), "Offset not supported for hierarchical indexing" + assert len(indices) == len(size), ( + f"Rank mismatch: got {len(indices)} indices for tensor of rank {len(size)}" + ) + if not indices: + return Integer(0) + if len(indices) == 1: + return indices[0] + return HierarchicalIndex(*indices) + + return indexer + + +@contextmanager +def patch_fixed_layout_indexer_for_cutedsl(): + """ + Temporarily swap FixedLayout.make_indexer so CuteDSL sees hierarchical indexing. + + Note [CuteDSL indexer patch]: + Flex flash attention only supports a limited set of IR ops (pointwise, reads, no stores), + so temporarily changing the indexing behavior is safe for the kernels we emit today. + TODO(dynamic shapes): Reconfirm once flex flash attention supports dynamic shapes. + """ + original_make_indexer = FixedLayout.make_indexer + + def cutedsl_make_indexer(self): + return _hierarchical_indexer_cute(self.size, self.stride, self.offset) + + FixedLayout.make_indexer = cutedsl_make_indexer # type: ignore[assignment] + try: + yield + finally: + FixedLayout.make_indexer = original_make_indexer # type: ignore[assignment] + + +def wrap_choice_render_with_cutedsl_indexer(choice: Any) -> None: + """ + Wrap a template choice's kernel render to apply CuteDSL indexer patching. + + See Note [CuteDSL indexer patch]: + CuteDSL handles tensor strides internally, so template rendering must use + hierarchical indexing. + """ + original_make_kernel_render = choice.make_kernel_render + + def make_kernel_render_with_patch(*args, **kwargs): + render_kernel, render = original_make_kernel_render(*args, **kwargs) + + def render_with_patch(): + with patch_fixed_layout_indexer_for_cutedsl(): + return render() + + return render_kernel, render_with_patch + + choice.make_kernel_render = make_kernel_render_with_patch + + +def input_buffers_require_grads(graph_module, num_score_mod_placeholders: int): + """Check if any of the input buffers (beyond the score mod placeholders) require gradients.""" + inputs = [] + for node in graph_module.graph.nodes: + if node.op == "placeholder": + inputs.append(node) + if len(inputs) <= num_score_mod_placeholders: + return False + + def requires_grad(n): + tensor_meta = n.meta.get("tensor_meta") + return tensor_meta.requires_grad if tensor_meta is not None else False + + return any(requires_grad(n) for n in inputs[num_score_mod_placeholders:]) + + +def is_trivial_score_graph(graph_module: GraphModule) -> bool: + """Backwards currently doesn't support score_mods, match against identity""" + graph = graph_module.graph + nodes = list(graph.nodes) + placeholders = [n for n in nodes if n.op == "placeholder"] + output = [n for n in nodes if n.op == "output"] + assert len(output) == 1, "Got graph w/ multiple outputs" + output_val = output[0].args[0] + # The identity graph just sends the score straight through + return output_val == placeholders[0] + + +def is_trivial_mask_graph(graph_module: GraphModule) -> bool: + """Mask graph is trivial when it only gates via the default full op.""" + graph = graph_module.graph + nodes = list(graph.nodes) + placeholders = [n for n in nodes if n.op == "placeholder"] + output = [n for n in nodes if n.op == "output"] + assert len(output) == 1, "Got graph w/ multiple outputs" + output_val = output[0].args[0] + + # mask mod graph is empty if we have 4 inputs and full_default output + return len(placeholders) == 4 and output_val.target is torch.ops.aten.full.default + + +@functools.lru_cache(maxsize=1) +def _is_symbol_from_tensor_shape(symbol: sympy.Symbol, shape_env: Any) -> bool: + """Check if a symbol originates from a tensor size/stride (TensorPropertySource).""" + from torch._dynamo.source import TensorPropertySource + + sources = shape_env.var_to_sources.get(symbol, []) + return any(isinstance(s, TensorPropertySource) for s in sources) + + +def _has_unsupported_captured_scalars( + score_mod_other_buffers: Sequence[Any], + mask_mod_other_buffers: Sequence[Any], +) -> bool: + """Check if any captured buffers are dynamic scalars that cannot be inlined. + + When compiling with dynamic=True, captured Python scalars in score_mod or + mask_mod may become: + - sympy symbols from LocalSource (captured ints) - NOT from tensor shapes + - 0-dim CPU tensors (captured floats) + + Symbols from TensorPropertySource (tensor size/stride) are fine because they + get resolved at runtime. + + The FLASH backend cannot inline captured scalar symbolic values into the CuteDSL template. + """ + from torch._inductor.virtualized import V + + shape_env = V.graph.sizevars.shape_env + + for buf in list(score_mod_other_buffers) + list(mask_mod_other_buffers): + # Captured int becomes sympy.Symbol - check if it's NOT from a tensor shape + if isinstance(buf, sympy.Expr): + for symbol in buf.free_symbols: + if not _is_symbol_from_tensor_shape(symbol, shape_env): + return True + # Captured float becomes 0-dim TensorBox on CPU + if isinstance(buf, TensorBox): + device = buf.get_device() + size = buf.get_size() + if device is not None and device.type == "cpu" and len(size) == 0: + # 0-dimensional CPU tensor (scalar) - can't be inlined into CUDA kernel + return True + return False + + +def _can_use_flex_flash_attention( + subgraph: Subgraph, + mask_graph: Subgraph, + num_score_mod_placeholders: int, +) -> tuple[bool, str]: + """Check if flex flash attention can be used for the given inputs. + + Returns: + tuple: (can_use, reason) where reason explains why it can't be used if can_use is False + """ + if not ensure_flash_available(): + return False, "CUTE flash attention library is not available" + + if input_buffers_require_grads(subgraph.graph_module, num_score_mod_placeholders): + return ( + False, + "Input buffers require gradients (not supported by flash attention)", + ) + + return True, "" + + +def _use_flex_flash_attention( + subgraph: Subgraph, + mask_graph: Subgraph, + kernel_options: dict[str, Any], + num_score_mod_placeholders: int, + backend: Literal["AUTO", "TRITON", "FLASH", "TRITON_DECODE"], +) -> bool: + """Determine if we should use flex flash attention for the given inputs. + + Args: + subgraph: The score modification subgraph + mask_graph: The mask modification subgraph + kernel_options: Kernel configuration options + num_score_mod_placeholders: Number of placeholders in score_mod + backend: Implementation selector (AUTO, TRITON, FLASH, TRITON_DECODE) + + Returns: + True if flash attention should be used, False otherwise + """ + # Flash is experimental and must be explicitly requested + if backend != "FLASH": + return False + + can_use, reason = _can_use_flex_flash_attention( + subgraph, + mask_graph, + num_score_mod_placeholders, + ) + + if not can_use: + raise RuntimeError( + f"BACKEND='FLASH' but flash attention cannot be used: {reason}" + ) + + return True + + +def create_flex_flash_attention_kernel( + query: TensorBox, + key: TensorBox, + value: TensorBox, + block_mask: tuple[Any, ...], + scale: float, + kernel_options: dict[str, Any], + subgraph_buffer: SubgraphResults, + mask_graph_buffer: SubgraphResults, + score_mod_other_buffers: list[TensorBox], + mask_mod_other_buffers: list[TensorBox], + kv_num_blocks: TensorBox | None, + kv_indices: TensorBox | None, + full_kv_num_blocks: TensorBox | None, + full_kv_indices: TensorBox | None, + sparse_q_block_size: int, + sparse_kv_block_size: int, + mask_graph: Subgraph, + subgraph: Subgraph | None = None, +) -> tuple[TensorBox, TensorBox]: + """Create a flex flash attention kernel using CuteDSL template.""" + if query.dtype != key.dtype or query.dtype != value.dtype: + raise ValueError( + f"Mixed query, key, and value dtype is not supported on this platform, " + f"got query.dtype: {query.dtype}, key.dtype: {key.dtype}, " + f"and value.dtype: {value.dtype}." + ) + if not ensure_flash_available(): + raise RuntimeError("CUTE flash attention not available") + + # Get dimensions + batch_size, num_heads, seq_len_q, head_dim = query.get_size() + v_head_dim = value.get_size()[-1] + device = query.get_device() + dtype = query.get_dtype() + assert device is not None, "Device must be specified" + + # Match stride pattern from query tensor + q_strides = query.get_stride() + out_size = [batch_size, num_heads, seq_len_q, v_head_dim] + out_strides = infer_dense_strides(out_size, q_strides) + + output = empty_strided( + size=out_size, + stride=out_strides, + dtype=dtype, + device=device, + ) + + lse = empty_strided( + size=[batch_size, num_heads, seq_len_q], + stride=None, # LSE can be contiguous + dtype=torch.float32, # LSE is always fp32 + device=device, + ) + + # Create layout for primary output + output_layout = FixedLayout( + device=device, + dtype=dtype, + size=[batch_size, num_heads, seq_len_q, v_head_dim], + stride=[sympy.sympify(s) for s in output.get_stride()], + ) + + sparse_q_block_size = V.graph.sizevars.guard_int(sparse_q_block_size) + sparse_kv_block_size = V.graph.sizevars.guard_int(sparse_kv_block_size) + + mask_graph_is_trivial = is_trivial_mask_graph(mask_graph.graph_module) + score_graph_is_trivial = subgraph is None or is_trivial_score_graph( + subgraph.graph_module + ) + + needs_block_mask = not mask_graph_is_trivial + has_score_mod = not score_graph_is_trivial + has_full_blocks = full_kv_num_blocks is not None + + choices: list[Any] = [] + assert flash_attention_cutedsl_template is not None + + input_nodes = [query, key, value, lse] + if has_full_blocks: + input_nodes.extend( + [kv_num_blocks, kv_indices, full_kv_num_blocks, full_kv_indices] + ) + + if needs_block_mask and not has_full_blocks: + raise NotImplementedError( + "Flash attention with block mask but without full blocks is not supported yet" + ) + + subgraphs = [] + if has_score_mod: + subgraphs.append(subgraph_buffer) + subgraphs.append(mask_graph_buffer) + + configs = _get_flex_flash_fwd_configs( + has_score_mod, len(score_mod_other_buffers) > 0 + ) + + error: NotImplementedError | None = None + for conf in configs: + with patch_fixed_layout_indexer_for_cutedsl(): + error = flash_attention_cutedsl_template.maybe_append_choice( + choices, + input_nodes=input_nodes, + layout=output_layout, + mutated_inputs=[lse], + subgraphs=subgraphs, + SM_SCALE=scale, + HAS_SCORE_MOD=has_score_mod, + SCORE_MOD_VEC_SIZE=conf.score_mod_vec_size, + NEEDS_BLOCK_MASK=needs_block_mask, + SPARSE_Q_BLOCK_SIZE=sparse_q_block_size, + SPARSE_KV_BLOCK_SIZE=sparse_kv_block_size, + ) + if error is not None and len(configs) == 1: + raise RuntimeError(f"CuteDSL template failed: {error}") + + for choice in choices: + wrap_choice_render_with_cutedsl_indexer(choice) + + if not choices: + raise RuntimeError(f"CuteDSL template failed: {error}") + + input_gen_fns: dict[int, Callable] | None = None + if has_full_blocks: + input_gen_fns = { + 4: create_num_blocks_fake_generator(kv_indices), + 5: create_indices_fake, + 6: create_num_blocks_fake_generator(full_kv_indices), + 7: create_indices_fake, + } + + template_output, _ = autotune_select_algorithm( + "flex_flash_attention", + choices, + input_nodes, + output_layout, + input_gen_fns=input_gen_fns, + return_multi_template=False, + ) + + return (template_output, lse) + + +def _can_use_flex_flash_attention_backward( + fw_subgraph: Subgraph, + mask_graph: Subgraph, + joint_outputs: Any | None = None, + score_mod_other_buffers: Sequence[TensorBox] | None = None, + num_score_mod_placeholders: int = 5, +) -> tuple[bool, str]: + if not ensure_flash_available(): + return False, "CUTE flash attention is not available" + + if input_buffers_require_grads( + fw_subgraph.graph_module, num_score_mod_placeholders + ): + return ( + False, + "Input buffers require gradients (not supported by flash attention backward)", + ) + + if joint_outputs is not None: + if joint_outputs.captured_grads_compute: + return ( + False, + "NYI: Flex Flash Attention bwd doesn't support captured grads yet.", + ) + if joint_outputs.mutated_grads: + return ( + False, + "NYI: Flex Flash Attention bwd doesn't support mutated grads yet.", + ) + + return True, "" + + +def _use_flex_flash_attention_backward( + fw_subgraph: Subgraph, + mask_graph: Subgraph, + backend: Literal["AUTO", "TRITON", "FLASH", "TRITON_DECODE"], + joint_outputs: Any | None = None, + score_mod_other_buffers: Sequence[TensorBox] | None = None, +) -> bool: + """Determine if we should use flex flash attention for the given inputs. + + Args: + fw_subgraph: The forward score modification subgraph + mask_graph: The mask modification subgraph + backend: Implementation selector (AUTO, TRITON, FLASH, TRITON_DECODE) + joint_outputs: Processed joint outputs (for PR1 constraint checking) + score_mod_other_buffers: Additional buffers used by score_mod + + Returns: + True if flash attention should be used, False otherwise + """ + # Flash is experimental and must be explicitly requested + if backend != "FLASH": + return False + + can_use, reason = _can_use_flex_flash_attention_backward( + fw_subgraph, + mask_graph, + joint_outputs, + score_mod_other_buffers, + ) + + if not can_use: + raise RuntimeError( + f"BACKEND='FLASH' but flash attention cannot be used: {reason}" + ) + + return True + + +def create_flex_flash_attention_backward_kernel( + query: TensorBox, + key: TensorBox, + value: TensorBox, + out: TensorBox, + logsumexp: TensorBox, + grad_out: TensorBox, + scale: float, + kernel_options: dict[str, Any], + sparse_q_block_size: int, + sparse_kv_block_size: int, + fw_subgraph_buffer: SubgraphResults | None = None, + joint_subgraph_buffer: Any | None = None, + score_mod_other_buffers: list[TensorBox] | None = None, + mask_graph_buffer: SubgraphResults | None = None, + q_num_blocks: TensorBox | None = None, + q_indices: TensorBox | None = None, + full_q_num_blocks: TensorBox | None = None, + full_q_indices: TensorBox | None = None, +) -> tuple[TensorBox | ShapeAsConstantBuffer, TensorBox, TensorBox, tuple]: + """Create a CuteDSL flash attention backward kernel for the default mod path.""" + if not ensure_flash_available(): + raise RuntimeError("CUTE flash attention not available") + + batch_size, num_heads, seq_len_q, head_dim = query.get_size() + _, num_heads_kv, seq_len_kv, v_head_dim = value.get_size() + device = query.get_device() + dtype = query.get_dtype() + assert device is not None + + grad_query_strides = infer_dense_strides( + [batch_size, num_heads, seq_len_q, head_dim], query.get_stride() + ) + grad_query = empty_strided( + size=[batch_size, num_heads, seq_len_q, head_dim], + stride=grad_query_strides, + dtype=dtype, + device=device, + ) + + grad_key_strides = infer_dense_strides( + [batch_size, num_heads_kv, seq_len_kv, head_dim], key.get_stride() + ) + grad_key = empty_strided( + size=[batch_size, num_heads_kv, seq_len_kv, head_dim], + stride=grad_key_strides, + dtype=dtype, + device=device, + ) + + grad_value_strides = infer_dense_strides( + [batch_size, num_heads_kv, seq_len_kv, v_head_dim], value.get_stride() + ) + grad_value = empty_strided( + size=[batch_size, num_heads_kv, seq_len_kv, v_head_dim], + stride=grad_value_strides, + dtype=dtype, + device=device, + ) + + # we use dq as the output layout + output_layout = FixedLayout( + device=device, + dtype=dtype, + size=[batch_size, num_heads, seq_len_q, head_dim], + stride=[sympy.sympify(s) for s in grad_query.get_stride()], + ) + + sparse_q_block_size = V.graph.sizevars.guard_int(sparse_q_block_size) + sparse_kv_block_size = V.graph.sizevars.guard_int(sparse_kv_block_size) + + choices: list[Any] = [] + + input_nodes: list[TensorBox] = [ + query, + key, + value, + out, + grad_out, + logsumexp, + grad_key, + grad_value, + ] + + has_block_mask = mask_graph_buffer is not None + if has_block_mask: + assert q_indices is not None + assert full_q_num_blocks is not None + assert full_q_indices is not None + input_nodes.extend( + [ + cast(TensorBox, q_num_blocks), + q_indices, + full_q_num_blocks, + full_q_indices, + ] + ) + + has_score_mod = fw_subgraph_buffer is not None and joint_subgraph_buffer is not None + subgraphs = [] + if has_score_mod: + subgraphs.append(fw_subgraph_buffer) + subgraphs.append(joint_subgraph_buffer) + if has_block_mask: + subgraphs.append(mask_graph_buffer) + + configs = _get_flex_flash_bwd_configs() + + error: NotImplementedError | None = None + for conf in configs: + with patch_fixed_layout_indexer_for_cutedsl(): + error = flash_attention_backward_cutedsl_template.maybe_append_choice( + choices, + input_nodes=input_nodes, + layout=output_layout, + mutated_inputs=[grad_key, grad_value], + subgraphs=subgraphs or None, + SM_SCALE=scale, + HAS_SCORE_MOD=has_score_mod, + SCORE_MOD_VEC_SIZE=conf.score_mod_vec_size, + HAS_BLOCK_MASK=has_block_mask, + SPARSE_Q_BLOCK_SIZE=sparse_q_block_size, + SPARSE_KV_BLOCK_SIZE=sparse_kv_block_size, + ) + if error is not None and len(configs) == 1: + raise RuntimeError(f"CuteDSL template failed: {error}") + + for choice in choices: + wrap_choice_render_with_cutedsl_indexer(choice) + + if not choices: + raise RuntimeError(f"CuteDSL template failed: {error}") + + input_gen_fns: dict[int, Callable] | None = None + if has_block_mask: + input_gen_fns = { + 8: create_num_blocks_fake_generator(q_indices), + 9: create_indices_fake, + 10: create_num_blocks_fake_generator(full_q_indices), + 11: create_indices_fake, + } + + template_output, _ = autotune_select_algorithm( + "flex_flash_attention_backward", + choices, + input_nodes, + output_layout, + input_gen_fns=input_gen_fns, + return_multi_template=False, + ) + + return (template_output, grad_key, grad_value, tuple()) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/templates/common.py.jinja b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/templates/common.py.jinja new file mode 100644 index 0000000000000000000000000000000000000000..1713fca53effb7c0ea18eeee62f8139bc26241cf --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/templates/common.py.jinja @@ -0,0 +1,205 @@ + + +# Common Imports +@triton.jit +def forward_block_mn( + {{gen_argdefs()}}, + q, K, V, desc_k, desc_v, Q_LEN, KV_LEN, + # accumulated values + acc, l_i, m_i, + # Offsets + off_z, off_h, offs_m, offs_n, + # Offsets needed for TMA loads + kv_start, + kv_offset, + MATMUL_PRECISION, RCP_LN2, + # Strides for K and V + stride_kk, stride_kn, stride_vn, stride_vk, + IS_FULL_BLOCKS, CHECK_BLOCK_BOUNDARY=False, + +): + # Redefines all kernel parameters (BLOCK_M, etc.) so we don't need to plumb them all through + {{gen_defines() | indent_except_first(1)}} + + # -- load k -- + # NB reversed order to since K is transposed + kv_base_offset = kv_start + kv_offset + {%- if USE_TMA %} + k = tl.load_tensor_descriptor( + desc_k, + [kv_base_offset, 0], + ) + {%- else %} + + # Load K as [BLOCK_N, QK_HEAD_DIM_ROUNDED] then transpose to [QK_HEAD_DIM_ROUNDED, BLOCK_N] + offs_k = tl.arange(0, QK_HEAD_DIM_ROUNDED) + offs_n_load = kv_base_offset + tl.arange(0, BLOCK_N) + k = load_checked_2d(K, offs_n_load, offs_k, stride_kn, stride_kk, IS_DIVISIBLE, SAFE_HEAD_DIM, KV_LEN, QK_HEAD_DIM) + {%- endif %} + + k = tl.trans(k) + k = k.to(q.dtype) + # -- compute qk --- + qk = tl.dot(q, k, input_precision=FLOAT32_PRECISION) # TODO: use cuda matmul when q_len <= 2. + if not PRESCALE_QK: + qk *= SM_SCALE + # ~~~~~~~~~~~~~~~~~~~ Apply score modification ~~~~~~~~~~~~~~~~~~~ + # If this is the last block of a non divisible seqlen, we still need to load [BLOCK_M, BLOCK_N] elements, + # which is larger than the actual number of elements. To avoid access memory out of bound, + # we need to mask out the elements that are out of Q_LEN & KV_LEN. + m = get_bounded_indices(offs_m, Q_LEN if CHECK_BLOCK_BOUNDARY else None) + n = get_bounded_indices(offs_n, KV_LEN if CHECK_BLOCK_BOUNDARY else None) + + {{ modification( + subgraph_number=0, + output_name="post_mod_scores", + score="qk", + b="off_z", + h="off_h", + m="m", + n="n", + out="qk" + ) | indent_except_first(1) }} + + if CHECK_BLOCK_BOUNDARY: + # Mask out the elements that are out of the KV_LEN for non divisible seqlen. + post_mod_scores = tl.where(offs_n < KV_LEN, post_mod_scores, float("-inf")) + + if not IS_FULL_BLOCKS: + {{ modification( + subgraph_number=1, + output_name="mask_mod_output", + score="qk", + b="off_z", + h="off_h", + m="m", + n="n", + ) | indent_except_first(2) }} + + if CHECK_BLOCK_BOUNDARY: + mask_mod_output = tl.where(offs_n < KV_LEN, mask_mod_output, False) + # apply mask for partially unmasked blocks + post_mod_scores = tl.where(mask_mod_output, post_mod_scores, float("-inf")) + + if not PRESCALE_QK: + post_mod_scores *= RCP_LN2 + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + # -- compute scaling constant --- + m_ij = tl.maximum(m_i, tl.max(post_mod_scores, 1)) + if not ROWS_GUARANTEED_SAFE: + masked_out_rows = (m_ij == float("-inf")) + m_ij_masked = tl.where(masked_out_rows, 0, m_ij) + else: + m_ij_masked = m_ij + + alpha = tl.math.exp2(m_i - m_ij_masked) + p = tl.math.exp2(post_mod_scores - m_ij_masked[:, None]) + + # NB: l_i update is pulled up here since it's a bit faster + # NB: For headdim=256, it's faster to move it back down to after m_i = + # m_ij + l_i = l_i * alpha + tl.sum(p, 1) + # # -- scale and update acc -- + acc = acc * alpha[:, None] + {%- if USE_TMA %} + v = tl.load_tensor_descriptor( + desc_v, + [kv_base_offset, 0], + ) + {%- else %} + # Calculate offsets for V loading - reuse kv_base_offset from K loading + offs_v = tl.arange(0, V_HEAD_DIM_ROUNDED) + v = load_checked_2d(V, offs_n_load, offs_v, stride_vn, stride_vk, IS_DIVISIBLE, SAFE_HEAD_DIM, KV_LEN, V_HEAD_DIM) + {%- endif %} + acc = tl.dot(p.to(MATMUL_PRECISION), v.to(q.dtype), acc, input_precision=FLOAT32_PRECISION) + + # -- update m_i + m_i = m_ij + + return acc, l_i, m_i + +@triton.jit +def forward_inner( + {{gen_argdefs()}}, + q, K, V, + desc_k, desc_v, Q_LEN, KV_LEN, + # accumulated values + acc, l_i, m_i, + # Offsets used as inputs to score_mod & mask_mod + # of size [BLOCK_M, BLOCK_N] or scalar. + off_z, off_h, offs_m, offs_n, + # Offsets needed for TMA loads + kv_start, + # blocksparse data + kv_indices, kv_num_blocks, + # start kv and end kv block + block_n_start, block_n_end, + MATMUL_PRECISION, + # Strides for K and V + stride_kk, stride_kn, stride_vn, stride_vk, + IS_FULL_BLOCKS, +): + # Redefines all kernel parameters (BLOCK_M, etc.) so we don't need to plumb them all through + {{gen_defines() | indent_except_first(1)}} + + SPARSE_KV_MULTIPLE: tl.constexpr = (SPARSE_KV_BLOCK_SIZE // BLOCK_N) + RCP_LN2: tl.constexpr = 1.44269504 + + if PRESCALE_QK: + q = (q * SM_SCALE * RCP_LN2).to(MATMUL_PRECISION) + + kv_offset = 0 + + # loop over k, v and update accumulator until block_n_end + for start_n in range(block_n_start, block_n_end): + # Here IS_DIVISIBLE acts are the start_n = tl.multiple_of(start_n, BLOCK_N) from triton_fused_attention. + if IS_DIVISIBLE: + acc, l_i, m_i = forward_block_mn( + {{gen_argdefs()}}, + q, K, V, desc_k, desc_v, Q_LEN, KV_LEN, + # accumulated values + acc, l_i, m_i, + # Offsets + off_z, off_h, offs_m, offs_n, + # Offsets needed for TMA loads + kv_start, + kv_offset, + MATMUL_PRECISION, RCP_LN2, + # Strides for K and V + stride_kk, stride_kn, stride_vn, stride_vk, + IS_FULL_BLOCKS, + ) + else: + # Benchmark shows even we applied mod & mask to each block for non divisible seqlen, + # it's on par or slightly faster than only applying to the last block in fwd. + # However, we choose different strategy for bwd, where we only apply mod & mask + # to the last block because it's faster a lot. + acc, l_i, m_i = forward_block_mn( + {{gen_argdefs()}}, + q, K, V, desc_k, desc_v, Q_LEN, KV_LEN, + # accumulated values + acc, l_i, m_i, + # Offsets + off_z, off_h, offs_m, offs_n, + # Offsets needed for TMA loads + kv_start, + kv_offset, + MATMUL_PRECISION, RCP_LN2, + # Strides for K and V + stride_kk, stride_kn, stride_vn, stride_vk, + IS_FULL_BLOCKS, CHECK_BLOCK_BOUNDARY=True, + ) + + + + offset = get_offset_for_next_block( + start_n, kv_indices, kv_num_blocks, + SPARSE_KV_BLOCK_SIZE, SPARSE_KV_MULTIPLE, BLOCK_N, BLOCKS_ARE_CONTIGUOUS + ) + + offs_n = offs_n + offset + kv_offset += offset + + + return acc, l_i, m_i diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/templates/flash_attention.py.jinja b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/templates/flash_attention.py.jinja new file mode 100644 index 0000000000000000000000000000000000000000..3061747f12a9c12d8746deff92d52c419cf03637 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/templates/flash_attention.py.jinja @@ -0,0 +1,85 @@ +{% if NEEDS_BLOCK_MASK %} +{{def_kernel("Q", "K", "V", "LOGSUMEXP", "KV_NUM_BLKS", "KV_IDX", "FULL_KV_NUM_BLKS", "FULL_KV_IDX")}} +{% else %} +{{def_kernel("Q", "K", "V", "LOGSUMEXP")}} +{% endif %} + from flash_attn.cute.interface import _flash_attn_fwd + from flash_attn.cute.block_sparsity import BlockSparseTensorsTorch + + # Transpose tensors for _flash_attn_fwd compatibility (B,H,M,D) -> (B,M,H,D) + q_transposed = Q.transpose(1, 2) + k_transposed = K.transpose(1, 2) + v_transposed = V.transpose(1, 2) + + {% if HAS_SCORE_MOD %} + @cute.jit + def score_mod(tSrS_ssa, b_idx, h_idx, q_idx, kv_idx, seqlen_info, aux_tensors): + {{unpack_buffers("aux_tensors", indent_width=8)}} + {{ modification( + subgraph_number=0, + output_name="tSrS_ssa", + score="tSrS_ssa", + b="b_idx", + h="h_idx", + m="q_idx", + n="kv_idx", + out="tSrS_ssa" + ) | indent_except_first(2) }} + return tSrS_ssa + {{ set_cute_hash("score_mod", "score") }} + {% if SCORE_MOD_VEC_SIZE %} + score_mod.__vec_size__ = {{SCORE_MOD_VEC_SIZE}} + {% endif %} + {% else %} + score_mod = None + {% endif %} + + # (B,M,H,D) -> (B,H,M,D) + output = {{get_output()}} + output_transposed = output.transpose(1, 2) + + {% if NEEDS_BLOCK_MASK %} + {# mask_mod is subgraph 1 when HAS_SCORE_MOD, else subgraph 0 #} + {% set mask_subgraph_idx = 1 if HAS_SCORE_MOD else 0 %} + @cute.jit + def mask_mod(b_idx, h_idx, q_idx, kv_idx, seqlen_info, aux_tensors): + {{unpack_buffers("aux_tensors", indent_width=8)}} + {{ modification( + subgraph_number=mask_subgraph_idx, + output_name="mask_mod_output", + b="b_idx", + h="h_idx", + m="q_idx", + n="kv_idx", + ) | indent_except_first(2) }} + return mask_mod_output + {{ set_cute_hash("mask_mod", "mask") }} + block_sparse_tensors = BlockSparseTensorsTorch(KV_NUM_BLKS, KV_IDX, FULL_KV_NUM_BLKS, FULL_KV_IDX, block_size=({{SPARSE_Q_BLOCK_SIZE}}, {{SPARSE_KV_BLOCK_SIZE}})) + {% else %} + block_sparse_tensors = None + mask_mod = None + {% endif %} + + # Collect any additional tensor buffers that were added during modifications + {% set tensor_buffers = get_tensor_buffers() -%} + {% if tensor_buffers -%} + buffers = [{% for buffer in tensor_buffers %}{{buffer}}{% if not loop.last %}, {% endif %}{% endfor %}] + buffers = list(buffers) + {% else -%} + buffers = None + {% endif -%} + + # Out and LSE filled inplace + _flash_attn_fwd( + q_transposed, + k_transposed, + v_transposed, + softmax_scale={{SM_SCALE}}, + return_lse=True, + score_mod=score_mod, + mask_mod=mask_mod, + out=output_transposed, + lse=LOGSUMEXP, + block_sparse_tensors=block_sparse_tensors, + aux_tensors=buffers + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/templates/flash_attention_backward.py.jinja b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/templates/flash_attention_backward.py.jinja new file mode 100644 index 0000000000000000000000000000000000000000..61a38cfa1a588d17eea9fef431551f272600a379 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/templates/flash_attention_backward.py.jinja @@ -0,0 +1,104 @@ +{% if HAS_BLOCK_MASK %} +{{def_kernel("Q", "K", "V", "OUT", "D_OUT", "LSE", "DK", "DV", "Q_NUM_BLKS", "Q_IDX", "FULL_Q_NUM_BLKS", "FULL_Q_IDX")}} +{% else %} +{{def_kernel("Q", "K", "V", "OUT", "D_OUT", "LSE", "DK", "DV")}} +{% endif %} + from flash_attn.cute.interface import _flash_attn_bwd + + q_transposed = Q.transpose(1, 2) + k_transposed = K.transpose(1, 2) + v_transposed = V.transpose(1, 2) + out_transposed = OUT.transpose(1, 2) + d_out_transposed = D_OUT.transpose(1, 2) + + dq_out = {{get_output()}} + dq_out_transposed = dq_out.transpose(1, 2) + dk_out_transposed = DK.transpose(1, 2) + dv_out_transposed = DV.transpose(1, 2) + + {% if HAS_SCORE_MOD %} + @cute.jit + def score_mod(tSrS_ssa, b_idx, h_idx, q_idx, kv_idx, seqlen_info, aux_tensors): + {{unpack_buffers("aux_tensors", indent_width=8)}} + {{ modification( + subgraph_number=0, + output_name="tSrS_ssa", + score="tSrS_ssa", + b="b_idx", + h="h_idx", + m="q_idx", + n="kv_idx", + out="tSrS_ssa" + ) | indent_except_first(2) }} + return tSrS_ssa + {{ set_cute_hash("score_mod", "score") }} + + @cute.jit + def score_mod_bwd(grad_score_mod_ssa, tSrS_ssa, b_idx, h_idx, q_idx, kv_idx, seqlen_info, aux_tensors): + {{unpack_buffers("aux_tensors", indent_width=8)}} + {{ modification( + subgraph_number=1, + output_name="grad_score_mod_ssa_out", + score="tSrS_ssa", + grad_score_mod="grad_score_mod_ssa", + b="b_idx", + h="h_idx", + m="q_idx", + n="kv_idx" + ) | indent_except_first(2) }} + return grad_score_mod_ssa_out + {{ set_cute_hash("score_mod_bwd", "score_bwd") }} + {% else %} + score_mod = None + score_mod_bwd = None + {% endif %} + + {% if HAS_BLOCK_MASK %} + from flash_attn.cute.block_sparsity import BlockSparseTensorsTorch + {# mask_mod is subgraph 2 when HAS_SCORE_MOD, else subgraph 0 #} + {% set mask_subgraph_idx = 2 if HAS_SCORE_MOD else 0 %} + @cute.jit + def mask_mod(b_idx, h_idx, q_idx, kv_idx, seqlen_info, aux_tensors): + {{unpack_buffers("aux_tensors", indent_width=8)}} + {{ modification( + subgraph_number=mask_subgraph_idx, + output_name="mask_mod_output", + b="b_idx", + h="h_idx", + m="q_idx", + n="kv_idx" + ) | indent_except_first(2) }} + return mask_mod_output + {{ set_cute_hash("mask_mod", "mask") }} + block_sparse_tensors = BlockSparseTensorsTorch(Q_NUM_BLKS, Q_IDX, FULL_Q_NUM_BLKS, FULL_Q_IDX, block_size=({{SPARSE_Q_BLOCK_SIZE}}, {{SPARSE_KV_BLOCK_SIZE}})) + {% else %} + mask_mod = None + block_sparse_tensors = None + {% endif %} + + {% set tensor_buffers = get_tensor_buffers() -%} + {% if tensor_buffers -%} + buffers = [{% for buffer in tensor_buffers %}{{buffer}}{% if not loop.last %}, {% endif %}{% endfor %}] + buffers = list(buffers) + {% else -%} + buffers = None + {% endif -%} + + _flash_attn_bwd( + q_transposed, + k_transposed, + v_transposed, + out_transposed, + d_out_transposed, + LSE, + softmax_scale={{SM_SCALE}}, + score_mod=score_mod, + score_mod_bwd=score_mod_bwd, + mask_mod=mask_mod, + aux_tensors=buffers, + dq=dq_out_transposed, + dk=dk_out_transposed, + dv=dv_out_transposed, + block_sparse_tensors=block_sparse_tensors, + deterministic=torch.are_deterministic_algorithms_enabled() and not torch.is_deterministic_algorithms_warn_only_enabled() + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/templates/flex_attention.py.jinja b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/templates/flex_attention.py.jinja new file mode 100644 index 0000000000000000000000000000000000000000..74fa146b134230b01a257bda03b69545cc046cfc --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/templates/flex_attention.py.jinja @@ -0,0 +1,224 @@ +{{def_kernel("Q", "K", "V", "LSE", "MAX", "KV_NUM_BLKS", "KV_IDX", "FULL_KV_NUM_BLKS", "FULL_KV_IDX")}} + # Sub notation for this kernel: + # + # Q: Query, K: Key, V: Value + # M: Number of queries, N: Number of keys/values, D: Model dimension + # QK_HEAD_DIM: The dimension of the query and key embeddings + # V_HEAD_DIM: The dimension of the value embeddings + # z: Batch size, h: Number of heads, m: Number of queries per head, k: Number of keys per head + # GQA_SHARED_HEADS: number of query heads sharing one kv head in GQA setups. + # + # The following FULL_* and PARTIAL_* is defined in the block sparse mask grid, rather than the thread block grid. + # KV_NUM_BLKS: The number of KV blocks (that may or may not require masking) for each query. + # KV_IDX: The indices of KV blocks (that may or may not require masking) for each query. + # FULL_KV_NUM_BLKS: The number of fully unmasked KV blocks (so we don't need masking) for each query. + # FULL_KV_IDX: The indices of fully unmasked KV blocks (so we don't need masking) for each query. + # + # OUTPUT_LOGSUMEXP: We only need to store the logsumexp if we require grad + # + # (Modifiable) Performance tuning options + # BLOCK_M: The thread block size across the seqlen dim of Q. + # BLOCK_N: Iterate over BLOCK_N across the seqlen dim of K/V in each thread block. + + # The below are kernel options that can be applied for certain score_mods, + # or involve a numerics vs. perf tradeoff + # PRESCALE_QK: Whether to pre-scale QK by 1/sqrt(d) and change of base. Has + # about 20% more numerical error, but slightly faster. + # ROWS_GUARANTEED_SAFE: Is it guaranteed that at least one value in each row + # is not masked out? If so, we can skip an extra safety check + # BLOCKS_ARE_CONTIGUOUS: Is it guaranteed that all blocks in the mask are + # contiguous? If so, we don't need to do an indirect jump for every block + + tl.static_assert(SPARSE_Q_BLOCK_SIZE >= BLOCK_M and SPARSE_Q_BLOCK_SIZE % BLOCK_M == 0) + tl.static_assert(SPARSE_KV_BLOCK_SIZE >= BLOCK_N and SPARSE_KV_BLOCK_SIZE % BLOCK_N == 0) + + # Define strides of inputs + stride_qz, stride_qh, stride_qm, stride_qk = {{stride("Q")}} + stride_kz, stride_kh, stride_kn, stride_kk = {{stride("K")}} + stride_vz, stride_vh, stride_vn, stride_vk = {{stride("V")}} + + ZQ = {{size("Q", 0)}} + HQ = {{size("Q", 1)}} + Q_LEN = {{size("Q", 2)}} + ZKV = {{size("K", 0)}} + KV_LEN = {{size("K", 2)}} + + MATMUL_PRECISION = Q.dtype.element_ty + + q_start = tl.program_id(0).to(INDEX_DTYPE) + off_zq = tl.program_id(1).to(INDEX_DTYPE) + off_hq = tl.program_id(2).to(INDEX_DTYPE) + + # We support two cases for batch dimension. a) (ZKV == ZQ) where off_zkv = off_zq. + # b) (ZKV == 1 and ZQ > 1) where KV is broadcasted along the batch dimension and off_zkv=0. + off_zkv = off_zq % ZKV + off_hkv = off_hq // GQA_SHARED_HEADS + off_g = off_hq % GQA_SHARED_HEADS + + q_offset = off_zq * stride_qz + off_hq * stride_qh + k_offset = off_zkv * stride_kz + off_hkv * stride_kh + v_offset = off_zkv * stride_vz + off_hkv * stride_vh + + Q = Q + q_offset + K = K + k_offset + V = V + v_offset + + # Setting up the TMA descriptors for Q, K, V + desc_q = None + desc_k = None + desc_v = None + {%- if USE_TMA %} + desc_q = tl.make_tensor_descriptor( + base=Q, + shape=[Q_LEN, QK_HEAD_DIM], + strides=[stride_qm, 1], + block_shape=[BLOCK_M, QK_HEAD_DIM_ROUNDED], + ) + + desc_k = tl.make_tensor_descriptor( + base=K, + shape=[KV_LEN, QK_HEAD_DIM], + strides=[stride_kn, 1], + block_shape=[BLOCK_N, QK_HEAD_DIM_ROUNDED], + ) + + desc_v = tl.make_tensor_descriptor( + base=V, + shape=[KV_LEN, V_HEAD_DIM], + strides=[stride_vn, 1], + block_shape=[BLOCK_N, V_HEAD_DIM_ROUNDED], + ) + {%- endif %} + + SPARSE_Z = {{size("KV_NUM_BLKS", 0)}} + SPARSE_HQ = {{size("KV_NUM_BLKS", 1)}} + + sparse_idx_z = off_zq % SPARSE_Z + sparse_idx_hq = off_hq % SPARSE_HQ + + SPARSE_Q_MULTIPLE: tl.constexpr = (SPARSE_Q_BLOCK_SIZE // BLOCK_M) + SPARSE_KV_MULTIPLE: tl.constexpr = (SPARSE_KV_BLOCK_SIZE // BLOCK_N) + + stride_kv_num_blks_h = {{stride("KV_NUM_BLKS", 1)}} + stride_kv_idx_h = {{stride("KV_IDX", 1)}} + stride_kv_idx_m = {{stride("KV_IDX", 2)}} + {%- if HAS_FULL_BLOCKS %} + stride_full_kv_num_blks_h = {{stride("FULL_KV_NUM_BLKS", 1)}} + stride_full_kv_idx_h = {{stride("FULL_KV_IDX", 1)}} + stride_full_kv_idx_m = {{stride("FULL_KV_IDX", 2)}} + {%- endif %} + + # initialize pointer to m and l + m_i = tl.zeros([BLOCK_M], dtype=tl.float32) - float("inf") + l_i = tl.zeros([BLOCK_M], dtype=tl.float32) + acc = tl.zeros([BLOCK_M, V_HEAD_DIM_ROUNDED], dtype=tl.float32) + + offs_m = q_start * BLOCK_M + tl.arange(0, BLOCK_M) + + # KV_IDX and KV_NUM_BLKS are always contiguous. + sparse_hz_offset = sparse_idx_z * SPARSE_HQ + sparse_idx_hq + sparse_kv_num_blks_offset = sparse_hz_offset * stride_kv_num_blks_h + q_start // SPARSE_Q_MULTIPLE + sparse_kv_idx_offset = sparse_hz_offset * stride_kv_idx_h + (q_start // SPARSE_Q_MULTIPLE) * stride_kv_idx_m # noqa: B950 + {%- if HAS_FULL_BLOCKS %} + sparse_full_kv_num_blks_offset = sparse_hz_offset * stride_full_kv_num_blks_h + q_start // SPARSE_Q_MULTIPLE + sparse_full_kv_idx_offset = sparse_hz_offset * stride_full_kv_idx_h + (q_start // SPARSE_Q_MULTIPLE) * stride_full_kv_idx_m # noqa: B950 + {%- endif %} + + {%- if USE_TMA %} + q = tl.load_tensor_descriptor( + desc_q, + [(q_start * BLOCK_M).to(tl.int32), 0], + ) + {%- else %} + offs_m = q_start * BLOCK_M + tl.arange(0, BLOCK_M) + offs_k = tl.arange(0, QK_HEAD_DIM_ROUNDED) + q = load_checked_2d(Q, offs_m, offs_k, stride_qm, stride_qk, IS_DIVISIBLE, SAFE_HEAD_DIM, Q_LEN, QK_HEAD_DIM) + {%- endif %} + + # ~~~~~~~~~~~~~~ normal blocks ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # We don't know anything "special" about these blocks, so we need to apply + # both score_mod and mask_mod to it + kv_indices = KV_IDX + sparse_kv_idx_offset + kv_start = tl.load(kv_indices) * SPARSE_KV_BLOCK_SIZE # first kv block we're loading + kv_num_blocks = tl.load(KV_NUM_BLKS + sparse_kv_num_blks_offset) + block_n_end = tl.minimum(kv_num_blocks * SPARSE_KV_MULTIPLE, tl.maximum(tl.cdiv(KV_LEN, BLOCK_N), 1)) + + + # K and V pointers will be passed directly to forward_inner + + offs_n = kv_start + tl.arange(0, BLOCK_N) + + + acc, l_i, m_i = forward_inner( + {{gen_argdefs()}}, + q, K, V, + desc_k, desc_v, Q_LEN, KV_LEN, + acc, l_i, m_i, + off_zq, off_hq, offs_m[:, None], offs_n[None, :], + kv_start, + kv_indices, kv_num_blocks, + 0, block_n_end, + MATMUL_PRECISION, + stride_kk, stride_kn, stride_vn, stride_vk, + IS_FULL_BLOCKS=False, + ) + + # ~~~~~~~~~~~~~~ "full" blocks ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # We know these blocks are guaranteed to be "full", so we don't need to + # apply mask_mod to them - only score_mod + if HAS_FULL_BLOCKS: + # FULL_KV_IDX and FULL_KV_NUM_BLKS are always contiguous. + kv_indices = FULL_KV_IDX + sparse_full_kv_idx_offset + kv_start = tl.load(kv_indices) * SPARSE_KV_BLOCK_SIZE # first kv block we're loading + kv_num_blocks = tl.load(FULL_KV_NUM_BLKS + sparse_full_kv_num_blks_offset) + block_n_end = tl.minimum(kv_num_blocks * SPARSE_KV_MULTIPLE, tl.maximum(tl.cdiv(KV_LEN, BLOCK_N), 1)) + # K and V pointers will be passed directly to forward_inner + offs_n = kv_start + tl.arange(0, BLOCK_N) + + acc, l_i, m_i = forward_inner( + {{gen_argdefs()}}, + q, K, V, + desc_k, desc_v, Q_LEN, KV_LEN, + acc, l_i, m_i, + off_zq, off_hq, offs_m[:, None], offs_n[None, :], + kv_start, + kv_indices, kv_num_blocks, + 0, block_n_end, + MATMUL_PRECISION, + stride_kk, stride_kn, stride_vn, stride_vk, + IS_FULL_BLOCKS=True, + ) + + + # [Note] Handle fully masked out rows: + # Li will be the sum(e^(-inf)) == 0.0 for masked out rows, mi will be -inf. + # We set Li to 1.0 which will result in lse/out = 0.0 | after the log(li) + mi(0.0) step + l_i = tl.where(l_i == 0.0, 1, l_i) + + acc = acc / l_i[:, None] + idx_zq = tl.program_id(1).to(INDEX_DTYPE) + idx_hq = tl.program_id(2).to(INDEX_DTYPE) + idx_m = offs_m[:, None].to(INDEX_DTYPE) + idx_d = tl.arange(0, V_HEAD_DIM_ROUNDED)[None, :].to(INDEX_DTYPE) + + mask = (idx_m < Q_LEN) & (idx_d < V_HEAD_DIM) + + tl.static_assert(acc.shape == [BLOCK_M, V_HEAD_DIM_ROUNDED]) + {{store_output(("idx_zq", "idx_hq", "idx_m", "idx_d"), "acc", "mask", val_shape=("BLOCK_M", "V_HEAD_DIM_ROUNDED"))}} + + if OUTPUT_LOGSUMEXP: + off_hz = off_zq * HQ + off_hq + l_ptrs = LSE + off_hz * Q_LEN + offs_m + lse = m_i + tl.math.log2(l_i) + if IS_DIVISIBLE: + tl.store(l_ptrs, lse) + else: + tl.store(l_ptrs, lse, mask=offs_m < Q_LEN) + + if OUTPUT_MAX: + off_hz = off_zq * HQ + off_hq + max_ptrs = MAX + off_hz * Q_LEN + offs_m + if IS_DIVISIBLE: + tl.store(max_ptrs, m_i) + else: + tl.store(max_ptrs, m_i, mask=offs_m < Q_LEN) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/templates/flex_backwards.py.jinja b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/templates/flex_backwards.py.jinja new file mode 100644 index 0000000000000000000000000000000000000000..41f4d8c8f1316fd278a90021190762d1e5f32358 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/templates/flex_backwards.py.jinja @@ -0,0 +1,751 @@ +{{def_kernel("Q", "K", "V", "LSE", "DELTA", "DO", "DQ", "DV", "KV_NUM_BLKS", "KV_IDX", "Q_NUM_BLKS", "Q_IDX", "FULL_KV_NUM_BLKS", "FULL_KV_IDX", "FULL_Q_NUM_BLKS", "FULL_Q_IDX")}} + # Sub notation for this kernel: + # + # Q: Query, K: Key, V: Value + # LSE: logsumexp (logsumexp is always stored in fp32 regardless of the input dtype) + # DELTA: Precomputed sum(OUT*DO, axis=-1) + # DO: Derivative of Output, DQ: Derivative of Query, DV: Derivative of Value + # DK: Derivative of Key, is the written to via the store_output call due to some limitations with + # inductor codegen + # M: Number of queries, N: Number of keys/values + # QK_HEAD_DIM: The dimension of the query and key embeddings + # V_HEAD_DIM: The dimension of the value embeddings + # z: Batch size, h: Number of heads, m: Number of queries or keys/values, d: Head dim + # GQA_SHARED_HEADS: number of query heads sharing one kv head in GQA setups. + # (Modifiable) Performance tuning options + # BLOCK_M1: when calculating DK & DV, iterate over BLOCK_M1 across the seqlen dim of Q in each thread block. + # BLOCK_N1: when calculating DK & DV, the thread block size across the seqlen dim of K/V. + # BLOCK_M2: when calculating DQ, the thread block size across the seqlen dim of Q. + # BLOCK_N2: when calculating DQ, iterate over BLOCK_N2 across the seqlen dim of K/V in each thread block. + # + # The following FULL_* and PARTIAL_* is defined in the block sparse mask grid, rather than the thread block grid. + # KV_NUM_BLKS: The number of KV blocks (that may or may not require masking) for each query. + # KV_IDX: The indices of KV blocks (that may or may not require masking) for each query. + # Q_NUM_BLKS: The number of Q blocks (that may or may not require masking) for each query. + # Q_IDX: The indices of Q blocks (that may or may not require masking) for each query. + # FULL_KV_NUM_BLKS: The number of fully unmasked KV blocks (so we don't need masking) for each query. + # FULL_KV_IDX: The indices of fully unmasked KV blocks (so we don't need masking) for each query. + # FULL_Q_NUM_BLKS: The number of fully unmasked Q blocks (so we don't need masking) for each query. + # FULL_Q_IDX: The indices of fully unmasked Q blocks (so we don't need masking) for each query. + + # The below are kernel options that can be applied for certain score_mods, + # or involve a numerics vs. perf tradeoff + # PRESCALE_QK: Whether to pre-scale QK by 1/sqrt(d) and change of base. Has + # about 20% more numerical error, but slightly faster. + + # Define strides of inputs + stride_qz, stride_qh, stride_qm, stride_qd = {{stride("Q")}} + stride_kz, stride_kh, stride_kn, stride_kd = {{stride("K")}} + stride_vz, stride_vh, stride_vn, stride_vd = {{stride("V")}} + stride_doz, stride_doh, stride_dom, stride_dod = {{stride("DO")}} + + stride_dqz, stride_dqh, stride_dqm, stride_dqd = {{stride("DQ")}} + stride_dvz, stride_dvh, stride_dvm, stride_dvd = {{stride("DV")}} + + ZQ = {{size("Q", 0)}} + HQ = {{size("Q", 1)}} + HKV = {{size("K", 1)}} + Q_LEN = {{size("Q", 2)}} + ZKV = {{size("K", 0)}} + KV_LEN = {{size("K", 2)}} + + MATMUL_PRECISION = Q.dtype.element_ty + + pid = tl.program_id(0).to(INDEX_DTYPE) + NUM_KV_BLOCKS = tl.cdiv(KV_LEN, BLOCK_N1) + NUM_Q_BLOCKS = tl.cdiv(Q_LEN, BLOCK_M2) + + off_zq = tl.program_id(1).to(INDEX_DTYPE) # q batch idx + off_hkv = tl.program_id(2).to(INDEX_DTYPE) # kv head idx + off_zkv = off_zq % ZKV # kv batch idx + + SPARSE_Z = {{size("KV_NUM_BLKS", 0)}} + SPARSE_HQ = {{size("KV_NUM_BLKS", 1)}} + + sparse_idx_z = off_zq % SPARSE_Z + + k_adj = (stride_kh * off_hkv + stride_kz * off_zkv).to(tl.int64) + v_adj = (stride_vh * off_hkv + stride_vz * off_zkv).to(tl.int64) + # first compute broadcasted dv of shape [Bq, Hkv, KV_LEN, V_HEAD_DIM] + # then reduce to dv of shape [Bkv, Hkv, KV_LEN, V_HEAD_DIM] + dv_adj = (stride_dvh * off_hkv + stride_dvz * off_zq).to(tl.int64) + + # offset K, V, DV pointers for batch/kv-head + K += k_adj + V += v_adj + DV += dv_adj + + RCP_LN2 = 1.44269504 + offs_k = tl.arange(0, QK_HEAD_DIM_ROUNDED) + offs_v = tl.arange(0, V_HEAD_DIM_ROUNDED) + + if pid >= NUM_KV_BLOCKS: + off_pid = pid - NUM_KV_BLOCKS + # THIS BLOCK DOES DQ + SPARSE_Q_MULTIPLE = (SPARSE_Q_BLOCK_SIZE // BLOCK_M2) + SPARSE_KV_MULTIPLE = (SPARSE_KV_BLOCK_SIZE // BLOCK_N2) + off_hq2 = off_pid // NUM_Q_BLOCKS + off_hkv * GQA_SHARED_HEADS + start_m2_block = off_pid % NUM_Q_BLOCKS + off_pid_mask = start_m2_block // SPARSE_Q_MULTIPLE + stride_kv_num_blks_h = {{stride("KV_NUM_BLKS", 1)}} + stride_kv_idx_h = {{stride("KV_IDX", 1)}} + stride_kv_idx_m = {{stride("KV_IDX", 2)}} + {%- if HAS_FULL_BLOCKS %} + stride_full_kv_num_blks_h = {{stride("FULL_KV_NUM_BLKS", 1)}} + stride_full_kv_idx_h = {{stride("FULL_KV_IDX", 1)}} + stride_full_kv_idx_m = {{stride("FULL_KV_IDX", 2)}} + {%- endif %} + + sparse_idx_hq2 = off_hq2 % SPARSE_HQ + sparse_hz_offset = sparse_idx_z * SPARSE_HQ + sparse_idx_hq2 + + sparse_kv_num_blks_offset = sparse_hz_offset * stride_kv_num_blks_h + off_pid_mask + sparse_kv_idx_offset = sparse_hz_offset * stride_kv_idx_h + off_pid_mask * stride_kv_idx_m # noqa: B950 + {%- if HAS_FULL_BLOCKS %} + sparse_full_kv_num_blks_offset = sparse_hz_offset * stride_full_kv_num_blks_h + off_pid_mask + sparse_full_kv_idx_offset = sparse_hz_offset * stride_full_kv_idx_h + off_pid_mask * stride_full_kv_idx_m # noqa: B950 + {%- endif %} + + # Offset Q, DQ, DO, DELTA & LSE. These inputs are offsetted by query heads. + q_adj2 = (stride_qh * off_hq2 + stride_qz * off_zq).to(tl.int64) + do_adj2 = (stride_doh * off_hq2 + stride_doz * off_zq).to(tl.int64) + dq_adj2 = (stride_dqh * off_hq2 + stride_dqz * off_zq).to(tl.int64) + off_chz2 = ((off_zq * HQ + off_hq2) * Q_LEN).to(tl.int64) + + Q2 = Q + q_adj2 + DO2 = DO + do_adj2 + # TODO: This does not work if DQ is not the same layout as Q (for example, + # if Q is broadcasted) + DQ2 = DQ + dq_adj2 + LSE2 = LSE + off_chz2 + DELTA2 = DELTA + off_chz2 + + dq = tl.zeros([BLOCK_M2, QK_HEAD_DIM_ROUNDED], dtype=tl.float32) + + start_m2 = start_m2_block * BLOCK_M2 + offs_m2 = start_m2 + tl.arange(0, BLOCK_M2) + + desc_q = None + desc_do = None + desc_k_dq = None + desc_v_dq = None + {%- if USE_TMA %} + desc_q = tl.make_tensor_descriptor( + base=Q2, + shape=(Q_LEN, QK_HEAD_DIM), + strides=(stride_qm, 1), + block_shape=[BLOCK_M2, QK_HEAD_DIM_ROUNDED], + ) + desc_do = tl.make_tensor_descriptor( + base=DO2, + shape=(Q_LEN, V_HEAD_DIM), + strides=(stride_dom, 1), + block_shape=[BLOCK_M2, V_HEAD_DIM_ROUNDED], + ) + desc_k_dq = tl.make_tensor_descriptor( + base=K, + shape=(KV_LEN, QK_HEAD_DIM), + strides=(stride_kn, 1), + block_shape=[BLOCK_N2, QK_HEAD_DIM_ROUNDED], + ) + desc_v_dq = tl.make_tensor_descriptor( + base=V, + shape=(KV_LEN, V_HEAD_DIM), + strides=(stride_vn, 1), + block_shape=[BLOCK_N2, V_HEAD_DIM_ROUNDED], + ) + q = tl.load_tensor_descriptor( + desc_q, + [start_m2.to(tl.int32), 0], + ) + do = tl.load_tensor_descriptor( + desc_do, + [start_m2.to(tl.int32), 0], + ) + {%- else %} + # load Q and do: they stay in SRAM throughout the inner loop. + q = load_checked_2d(Q2, offs_m2, offs_k, stride_qm, stride_qd, IS_DIVISIBLE, SAFE_HEAD_DIM, Q_LEN, QK_HEAD_DIM) + do = load_checked_2d(DO2, offs_m2, offs_v, stride_dom, stride_dod, IS_DIVISIBLE, SAFE_HEAD_DIM, Q_LEN, V_HEAD_DIM) + {%- endif %} + if PRESCALE_QK: + q = (q * SM_SCALE * RCP_LN2).to(MATMUL_PRECISION) + + if IS_DIVISIBLE: + Di = tl.load(DELTA2 + offs_m2) + lse = tl.load(LSE2 + offs_m2) + else: + Di = tl.load(DELTA2 + offs_m2, mask=offs_m2 < Q_LEN) + lse = tl.load(LSE2 + offs_m2, mask=offs_m2 < Q_LEN) + lse = tl.where(lse == -float("inf"), 0.0, lse) + lse = lse[:, None] + + # ~~~~~~~~~~~ fully unmasked blocks ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # KV_IDX and KV_NUM_BLKS are always contiguous. + kv_indices = KV_IDX + sparse_kv_idx_offset + kv_start = tl.load(kv_indices) * SPARSE_KV_BLOCK_SIZE # first kv block we're loading + sparse_kv_num_blocks = tl.load(KV_NUM_BLKS + sparse_kv_num_blks_offset) + + dq = bwd_dq_inner( + {{gen_argdefs()}}, + K, V, desc_k_dq, desc_v_dq, kv_start, start_m2, + dq, q, do, Di, lse, + off_zq, off_hq2, + stride_kn, stride_kd, stride_vn, stride_vd, + kv_indices, sparse_kv_num_blocks, + MATMUL_PRECISION, + IS_FULL_BLOCKS=False, + ) + + if HAS_FULL_BLOCKS: + # ~~~~~~~~~~~ partial unmasked blocks ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # FULL_KV_IDX and FULL_KV_NUM_BLKS are always contiguous. + kv_indices = FULL_KV_IDX + sparse_full_kv_idx_offset + kv_start = tl.load(kv_indices) * SPARSE_KV_BLOCK_SIZE # first kv block we're loading + sparse_kv_num_blocks = tl.load(FULL_KV_NUM_BLKS + sparse_full_kv_num_blks_offset) + + offs_n2 = kv_start + tl.arange(0, BLOCK_N2) + dq = bwd_dq_inner( + {{gen_argdefs()}}, + K, V, desc_k_dq, desc_v_dq, kv_start, start_m2, + dq, q, do, Di, lse, + off_zq, off_hq2, + stride_kn, stride_kd, stride_vn, stride_vd, + kv_indices, sparse_kv_num_blocks, + MATMUL_PRECISION, + IS_FULL_BLOCKS=True, + ) + + # Write back dQ. + dq_ptrs = DQ2 + offs_m2[:, None] * stride_dqm + offs_k[None, :] * stride_dqd + dq *= SM_SCALE + if IS_DIVISIBLE and SAFE_HEAD_DIM: + tl.store(dq_ptrs, dq) + else: + tl.store(dq_ptrs, dq, mask=(offs_m2[:, None] < Q_LEN) & (offs_k[None, :] < QK_HEAD_DIM)) + else: + # THIS BLOCK DOES DK & DV + SPARSE_Q_MULTIPLE = (SPARSE_Q_BLOCK_SIZE // BLOCK_M1) + SPARSE_KV_MULTIPLE = (SPARSE_KV_BLOCK_SIZE // BLOCK_N1) + + pid_mask = pid // SPARSE_KV_MULTIPLE + + stride_q_num_blks_h = {{stride("Q_NUM_BLKS", 1)}} + stride_q_idx_h = {{stride("Q_IDX", 1)}} + stride_q_idx_n = {{stride("Q_IDX", 2)}} + {%- if HAS_FULL_BLOCKS %} + stride_full_q_num_blks_h = {{stride("FULL_Q_NUM_BLKS", 1)}} + stride_full_q_idx_h = {{stride("FULL_Q_IDX", 1)}} + stride_full_q_idx_n = {{stride("FULL_Q_IDX", 2)}} + {%- endif %} + + + dv = tl.zeros([BLOCK_N1, V_HEAD_DIM_ROUNDED], dtype=tl.float32) + dk = tl.zeros([BLOCK_N1, QK_HEAD_DIM_ROUNDED], dtype=tl.float32) + + start_n1 = pid * BLOCK_N1 + offs_n1 = start_n1 + tl.arange(0, BLOCK_N1) + + {%- if USE_TMA %} + desc_k_dkdv = tl.make_tensor_descriptor( + base=K, + shape=(KV_LEN, QK_HEAD_DIM), + strides=(stride_kn, 1), + block_shape=[BLOCK_N1, QK_HEAD_DIM_ROUNDED], + ) + desc_v_dkdv = tl.make_tensor_descriptor( + base=V, + shape=(KV_LEN, V_HEAD_DIM), + strides=(stride_vn, 1), + block_shape=[BLOCK_N1, V_HEAD_DIM_ROUNDED], + ) + k = tl.load_tensor_descriptor( + desc_k_dkdv, + [start_n1.to(tl.int32), 0], + ) + v = tl.load_tensor_descriptor( + desc_v_dkdv, + [start_n1.to(tl.int32), 0], + ) + {%- else %} + # load K and V: they stay in SRAM throughout the inner loop. + k = load_checked_2d(K, offs_n1, offs_k, stride_kn, stride_kd, IS_DIVISIBLE, SAFE_HEAD_DIM, KV_LEN, QK_HEAD_DIM) + v = load_checked_2d(V, offs_n1, offs_v, stride_vn, stride_vd, IS_DIVISIBLE, SAFE_HEAD_DIM, KV_LEN, V_HEAD_DIM) + {%- endif %} + if PRESCALE_QK: + k = (k * SM_SCALE * RCP_LN2).to(MATMUL_PRECISION) + + for off_g in range(0, GQA_SHARED_HEADS): + off_hq1 = off_hkv * GQA_SHARED_HEADS + off_g + + # Offset Q, DQ, DO, DELTA & LSE. These inputs are offsetted by query heads. + q_adj1 = (stride_qh * off_hq1 + stride_qz * off_zq).to(tl.int64) + do_adj1 = (stride_doh * off_hq1 + stride_doz * off_zq).to(tl.int64) + dq_adj1 = (stride_dqh * off_hq1 + stride_dqz * off_zq).to(tl.int64) + off_chz1 = ((off_zq * HQ + off_hq1) * Q_LEN).to(tl.int64) + + Q1 = Q + q_adj1 + DO1 = DO + do_adj1 + # TODO: This does not work if DQ is not the same layout as Q (for example, + # if Q is broadcasted) + LSE1 = LSE + off_chz1 + DELTA1 = DELTA + off_chz1 + + sparse_idx_hq1 = off_hq1 % SPARSE_HQ + sparse_hz_offset = sparse_idx_z * SPARSE_HQ + sparse_idx_hq1 + + sparse_q_num_blks_offset = sparse_hz_offset * stride_q_num_blks_h + pid_mask + sparse_q_idx_offset = sparse_hz_offset * stride_q_idx_h + pid_mask * stride_q_idx_n # noqa: B950 + {%- if HAS_FULL_BLOCKS %} + sparse_full_q_num_blks_offset = sparse_hz_offset * stride_full_q_num_blks_h + pid_mask + sparse_full_q_idx_offset = sparse_hz_offset * stride_full_q_idx_h + pid_mask * stride_full_q_idx_n # noqa: B950 + {%- endif %} + + # ~~~~~~~~~~~~~~~ fully unmasked blocks ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Q_IDX and Q_NUM_BLKS are always contiguous. + q_indices = Q_IDX + sparse_q_idx_offset + q_start = tl.load(q_indices) * SPARSE_Q_BLOCK_SIZE # first q block we're loading + sparse_q_num_blocks = tl.load(Q_NUM_BLKS + sparse_q_num_blks_offset) + + offs_m1 = q_start + tl.arange(0, BLOCK_M1) + desc_q = None + desc_do = None + {%- if USE_TMA %} + desc_q = tl.make_tensor_descriptor( + base=Q1, + shape=(Q_LEN, QK_HEAD_DIM), + strides=(stride_qm, 1), + block_shape=[BLOCK_M1, QK_HEAD_DIM_ROUNDED], + ) + desc_do = tl.make_tensor_descriptor( + base=DO1, + shape=(Q_LEN, V_HEAD_DIM), + strides=(stride_dom, 1), + block_shape=[BLOCK_M1, V_HEAD_DIM_ROUNDED], + ) + {%- endif %} + dk, dv = bwd_dkdv_inner( + {{gen_argdefs()}}, + Q1, DO1, DELTA1, LSE1, + desc_q, desc_do, q_start, + dk, dv, k, v, + off_zq, off_hq1, offs_n1, offs_m1, + stride_qm, stride_qd, stride_dom, stride_dod, + q_indices, sparse_q_num_blocks, + MATMUL_PRECISION, + IS_FULL_BLOCKS=False, + ) + + + if HAS_FULL_BLOCKS: + # ~~~~~~~~~~~~~~~ fully unmasked blocks ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # FULL_Q_IDX and FULL_Q_NUM_BLKS are always contiguous. + q_indices = FULL_Q_IDX + sparse_full_q_idx_offset + q_start = tl.load(q_indices) * SPARSE_Q_BLOCK_SIZE # first q block we're loading + sparse_q_num_blocks = tl.load(FULL_Q_NUM_BLKS + sparse_full_q_num_blks_offset) + + offs_m1 = q_start + tl.arange(0, BLOCK_M1) + dk, dv = bwd_dkdv_inner( + {{gen_argdefs()}}, + Q1, DO1, DELTA1, LSE1, + desc_q, desc_do, q_start, + dk, dv, k, v, + off_zq, off_hq1, offs_n1, offs_m1, + stride_qm, stride_qd, stride_dom, stride_dod, + q_indices, sparse_q_num_blocks, + MATMUL_PRECISION, + IS_FULL_BLOCKS=True, + ) + + # Write back dV and dK. + dv_ptrs = DV + offs_n1[:, None] * stride_dvm + offs_v[None, :] * stride_dvd + + index_n = offs_n1[:, None] + index_k = offs_k[None, :] + index_v = offs_v[None, :] + + if IS_DIVISIBLE and SAFE_HEAD_DIM: + tl.store(dv_ptrs, dv) + else: + tl.store(dv_ptrs, dv, mask=(index_n < KV_LEN) & (index_v < V_HEAD_DIM)) + + dk *= SM_SCALE + + if SAFE_HEAD_DIM: + mask = index_n < KV_LEN + else: + mask = (index_n < KV_LEN) & (index_k < QK_HEAD_DIM) + + # first compute broadcasted dk of shape [Bq, Hkv, KV_LEN, V_HEAD_DIM] + # then reduce to dk of shape [Bkv, Hkv, KV_LEN, V_HEAD_DIM] + tl.static_assert(dk.shape == [BLOCK_N1, QK_HEAD_DIM_ROUNDED]) + {{store_output(("off_zq", "off_hkv", "index_n", "index_k"), "dk", "mask", indent_width=8, val_shape=("BLOCK_N1", "QK_HEAD_DIM_ROUNDED"))}} + +@triton.jit +def bwd_dq_inner( + {{gen_argdefs()}}, + K, V, desc_k, desc_v, kv_start, start_m2, + dq, q, do, Di, lse, + off_z, off_hq, + stride_kn, stride_kd, stride_vn, stride_vd, + kv_indices, sparse_kv_num_blocks, + MATMUL_PRECISION, + IS_FULL_BLOCKS, +): + + {{gen_defines() | indent_except_first(1) }} + SPARSE_KV_MULTIPLE: tl.constexpr = (SPARSE_KV_BLOCK_SIZE // BLOCK_N2) + RCP_LN2: tl.constexpr = 1.44269504 + Q_LEN = {{size("Q", 2)}} + KV_LEN = {{size("K", 2)}} + + offs_k = tl.arange(0, QK_HEAD_DIM_ROUNDED) + offs_v = tl.arange(0, V_HEAD_DIM_ROUNDED) + + # BLOCK_M2 must be a multiple of BLOCK_N2, otherwise the code wouldn't work. + tl.static_assert(BLOCK_M2 % BLOCK_N2 == 0) + + hi = tl.minimum(sparse_kv_num_blocks * SPARSE_KV_MULTIPLE, tl.maximum(tl.cdiv(KV_LEN, BLOCK_N2), 1)) + + kv_offset = 0 + for start_n in range(0, hi): + dq = bwd_dq_block_mn( + {{gen_argdefs()}}, + dq, q, K, V, desc_k, desc_v, kv_start, kv_offset, start_m2, + do, Di, lse, Q_LEN, KV_LEN, + off_z, off_hq, offs_k, offs_v, + stride_kn, stride_kd, stride_vn, stride_vd, + kv_indices, sparse_kv_num_blocks, + MATMUL_PRECISION, RCP_LN2, + IS_FULL_BLOCKS, + ) + + # Increment pointers. + offset = get_offset_for_next_block( + start_n, kv_indices, sparse_kv_num_blocks, + SPARSE_KV_BLOCK_SIZE, SPARSE_KV_MULTIPLE, BLOCK_N2, BLOCKS_ARE_CONTIGUOUS + ) + + kv_offset += offset + + return dq + + +@triton.jit +def bwd_dq_block_mn( + {{gen_argdefs()}}, + dq, q, K, V, desc_k, desc_v, kv_start, kv_offset, start_m2, + do, Di, lse, Q_LEN, KV_LEN, + off_z, off_hq, offs_k, offs_v, + stride_kn, stride_kd, stride_vn, stride_vd, + kv_indices, sparse_kv_num_blocks, + MATMUL_PRECISION, RCP_LN2, + IS_FULL_BLOCKS, +): + {{gen_defines() | indent_except_first(1)}} + + offs_n2 = kv_start + kv_offset + tl.arange(0, BLOCK_N2) + offs_m2 = start_m2 + tl.arange(0, BLOCK_M2) + {%- if USE_TMA %} + k = tl.load_tensor_descriptor( + desc_k, + [(kv_start + kv_offset).to(tl.int32), 0], + ) + kT = tl.trans(k) + {%- else %} + kT_ptrs = K + offs_n2[None, :] * stride_kn + offs_k[:, None] * stride_kd + vT_ptrs = V + offs_n2[None, :] * stride_vn + offs_v[:, None] * stride_vd + # NB reversed order to since K is transposed + kT = load_checked_2d(kT_ptrs, offs_k, offs_n2, None, None, SAFE_HEAD_DIM, IS_DIVISIBLE, QK_HEAD_DIM, KV_LEN) + {%- endif %} + qk = tl.dot(q, kT, input_precision=FLOAT32_PRECISION) + if not PRESCALE_QK: + qk *= SM_SCALE + # ~~~~~~~~~~~~~~~~~~~ Apply score modification ~~~~~~~~~~~~~~~~~~~ + pre_mod_scores = qk + n = get_bounded_indices(offs_n2[None, :], KV_LEN if not IS_DIVISIBLE else None) + # The boundary check is done for the outer loop, but here it's possible since we're iterating across N dim + # that the M reads out of bounds for the PIDS spanning the Q_LEN boundary + m = get_bounded_indices(offs_m2[:, None], Q_LEN if not IS_DIVISIBLE else None) + + {{ modification( + subgraph_number=0, + output_name="post_mod_scores", + score="qk", + b="off_z", + h="off_hq", + m="m", + n="n", + out="qk" + ) | indent_except_first(1) }} + + + {# Note: Selective masking DQ + We load elements beyond KV_LEN w/ zero, some score mods may convert this elements to NaN + Example: lambda x, *_: 1 / score, this NaN would propagate regardless of other masking + We only need to do this on the m1 dim since these elements take part in the final reduction + for DQ #} + if not IS_DIVISIBLE: + post_mod_scores = tl.where(offs_n2[None, :] < KV_LEN, post_mod_scores, float("-inf")) + + if not IS_FULL_BLOCKS: + {{ modification( + subgraph_number=2, + output_name="mask_mod_output", + score="qk", + b="off_z", + h="off_hq", + m="m", + n="n", + ) | indent_except_first(2) }} + + # apply mask for partial masked block + post_mod_scores = tl.where(mask_mod_output, post_mod_scores, float("-inf")) + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + if not PRESCALE_QK: + post_mod_scores *= RCP_LN2 + p = tl.math.exp2(post_mod_scores - lse) + # Compute dP and dS. + # NB reversed order to since V is transposed + {%- if USE_TMA %} + v = tl.load_tensor_descriptor( + desc_v, + [(kv_start + kv_offset).to(tl.int32), 0], + ) + vT = tl.trans(v) + {%- else %} + vT = load_checked_2d(vT_ptrs, offs_v, offs_n2, None, None, SAFE_HEAD_DIM, IS_DIVISIBLE, V_HEAD_DIM, KV_LEN) + {%- endif %} + dp = tl.dot(do, vT, input_precision=FLOAT32_PRECISION) + ds = p * (dp - Di[:, None]) + # ~~~~~~~~~~~~~~~~~~~ Apply joint modification ~~~~~~~~~~~~~~~~~~~ + {{ modification( + subgraph_number=1, + output_name = "grad_scores", + score="pre_mod_scores", + b="off_z", + h="off_hq", + m="m", + n="n", + grad_score_mod="ds" + ) | indent_except_first(1) }} + {# See Note Selective masking DQ #} + if not IS_DIVISIBLE: + grad_scores = tl.where(offs_n2[None, :] < KV_LEN, grad_scores, 0.0) + + # ~~~~~~~~~~~~~~~~~~~ Apply other buffer grad writes ~~~~~~~~~~~~~ + if WRITE_DQ: + scatter_mask = (offs_m2[:, None] < Q_LEN ) & (offs_n2[None, :] < KV_LEN) + {{ modification( + subgraph_number=3, + output_name=None, + mask="scatter_mask", + input_shapes={"m": ("BLOCK_M2", "1"), "n": ("1", "BLOCK_N2"), "b": ("1", "1"), "h": ("1", "1")}, + score="pre_mod_scores", + b="off_z", + h="off_hq", + m="m", + n="n", + grad_score_mod="ds" + ) | indent_except_first(2) }} + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ds = grad_scores + + if not IS_FULL_BLOCKS: + # (grads) apply mask for partially unmasked block + ds = tl.where(mask_mod_output, ds, 0.0) + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ds = ds.to(MATMUL_PRECISION) + # Compute dQ. + dq += tl.dot(ds, tl.trans(kT), input_precision=FLOAT32_PRECISION) + + return dq + + +@triton.jit +def bwd_dkdv_inner( + {{gen_argdefs()}}, + Q, DO, DELTA, LSE, # pointers + desc_q, desc_do, q_start, + dk, dv, k, v, + off_z, off_hq, offs_n1, offs_m1, + stride_qm, stride_qd, stride_dom, stride_dod, + q_indices, sparse_q_num_blocks, + MATMUL_PRECISION, + IS_FULL_BLOCKS, +): + {{gen_defines() | indent_except_first(1) }} + SPARSE_Q_MULTIPLE: tl.constexpr = (SPARSE_Q_BLOCK_SIZE // BLOCK_M1) + RCP_LN2: tl.constexpr = 1.44269504 + Q_LEN = {{size("Q", 2)}} + KV_LEN = {{size("K", 2)}} + + offs_k = tl.arange(0, QK_HEAD_DIM_ROUNDED) + offs_v = tl.arange(0, V_HEAD_DIM_ROUNDED) + + # BLOCK_N1 must be a multiple of BLOCK_M1, otherwise the code wouldn't work. + tl.static_assert(BLOCK_N1 % BLOCK_M1 == 0) + + # The minimum is needed to handle the case where we run with a super large + # SPARSE_BLOCK_SIZE (i.e. no block-mask!) + hi = tl.minimum(sparse_q_num_blocks * SPARSE_Q_MULTIPLE, tl.maximum(tl.cdiv(Q_LEN, BLOCK_M1), 1)) + + offset_block = 0 + for start_m in range(0, hi): + dk, dv = bwd_dkdv_block_mn( + {{gen_argdefs()}}, + dk, dv, Q, k, v, DO, DELTA, LSE, + desc_q, desc_do, q_start, offset_block, Q_LEN, KV_LEN, + off_z, off_hq, offs_n1, offs_m1, offs_k, offs_v, + stride_qm, stride_qd, stride_dom, stride_dod, + q_indices, sparse_q_num_blocks, + MATMUL_PRECISION, RCP_LN2, + IS_FULL_BLOCKS, + ) + # Increment pointers. + offset = get_offset_for_next_block( + start_m, q_indices, sparse_q_num_blocks, + SPARSE_Q_BLOCK_SIZE, SPARSE_Q_MULTIPLE, BLOCK_M1, BLOCKS_ARE_CONTIGUOUS + ) + offs_m1 += offset + offset_block += offset + + return dk, dv + + +@triton.jit +def bwd_dkdv_block_mn( + {{gen_argdefs()}}, + dk, dv, Q, k, v, DO, DELTA, LSE, + desc_q, desc_do, q_start, offset, Q_LEN, KV_LEN, + off_z, off_hq, offs_n1, offs_m1, offs_k, offs_v, + stride_qm, stride_qd, stride_dom, stride_dod, + q_indices, sparse_q_num_blocks, + MATMUL_PRECISION, RCP_LN2, + IS_FULL_BLOCKS, +): + {{gen_defines() | indent_except_first(1) }} + + {%- if USE_TMA %} + q = tl.load_tensor_descriptor( + desc_q, + [(q_start + offset).to(tl.int32), 0], + ) + qT = tl.trans(q) + {%- else %} + qT_ptrs = Q + offs_m1[None, :] * stride_qm + offs_k[:, None] * stride_qd + do_ptrs = DO + offs_m1[:, None] * stride_dom + offs_v[None, :] * stride_dod + # NB reversed order since Q is transposed + qT = load_checked_2d(qT_ptrs, offs_k, offs_m1, None, None, SAFE_HEAD_DIM, IS_DIVISIBLE, QK_HEAD_DIM, Q_LEN) + {%- endif %} + # Load LSE before computing qk to reduce pipeline stall. + if IS_DIVISIBLE: + lse = tl.load(LSE + offs_m1) + else: + lse = tl.load(LSE + offs_m1, mask=offs_m1 < Q_LEN) + lse = tl.where(lse == -float("inf"), 0.0, lse) + qkT = tl.dot(k, qT, input_precision=FLOAT32_PRECISION) + if not PRESCALE_QK: + qkT *= SM_SCALE + # ~~~~~~~~~~~~~~~~~~~ Apply score modification ~~~~~~~~~~~~~~~~~~~ + m = get_bounded_indices(offs_m1[None, :], Q_LEN if not IS_DIVISIBLE else None) + # The boundary check is done for the outer loop, but here it's possible since we're iterating across M dim + # that the n reads out of bounds for the PIDS spanning the KV_LEN boundary + n = get_bounded_indices(offs_n1[:, None], KV_LEN if not IS_DIVISIBLE else None) + + pre_mod_scores = qkT + {{ modification( + subgraph_number=0, + output_name="post_mod_scores", + score="qkT", + b="off_z", + h="off_hq", + m="m", + n="n", + out="qkT" + ) | indent_except_first(1) }} + + {# Note: Selective masking DK/DV + We load elements beyond Q_LEN w/ zero, some score mods may convert this elements to NaN + Example: lambda x, *_: 1 / score, this NaN would propagate regardless of other masking + We only need to do this on the m1 dim since these elements take part in the final reduction + for DK/DV #} + if not IS_DIVISIBLE: + post_mod_scores = tl.where(offs_m1[None, :] < Q_LEN, post_mod_scores, float("-inf")) + + if not IS_FULL_BLOCKS: + {{ modification( + subgraph_number=2, + output_name="mask_mod_output", + b="off_z", + h="off_hq", + m="m", + n="n", + ) | indent_except_first(2) }} + # (grads) apply mask for fully masked block + post_mod_scores = tl.where(mask_mod_output, post_mod_scores, float("-inf")) + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + if not PRESCALE_QK: + post_mod_scores *= RCP_LN2 + pT = tl.math.exp2(post_mod_scores - lse[None, :]) + {%- if USE_TMA %} + do = tl.load_tensor_descriptor( + desc_do, + [(q_start + offset).to(tl.int32), 0], + ) + {%- else %} + do = load_checked_2d(do_ptrs, offs_m1, offs_v, None, None, IS_DIVISIBLE, SAFE_HEAD_DIM, Q_LEN, V_HEAD_DIM) + {%- endif %} + # Compute dV. + ppT = pT + dv += tl.dot(ppT.to(MATMUL_PRECISION), do, input_precision=FLOAT32_PRECISION) + if IS_DIVISIBLE: + Di = tl.load(DELTA + offs_m1) + else: + Di = tl.load(DELTA + offs_m1, mask=offs_m1 < Q_LEN) + # Compute dP and dS. + dpT = tl.dot(v, tl.trans(do), input_precision=FLOAT32_PRECISION) + dsT = pT * (dpT - Di[None, :]) + # ~~~~~~~~~~~~~~~~~~~ Apply joint modification ~~~~~~~~~~~~~~~~~~~ + {{ modification( + subgraph_number=1, + output_name = "grad_scores", + score="pre_mod_scores", + b="off_z", + h="off_hq", + m="m", + n="n", + grad_score_mod="dsT" + ) | indent_except_first(1) }} + + {# See Note: Selective masking DK/DV#} + if not IS_DIVISIBLE: + grad_scores = tl.where(offs_m1[None, :] < Q_LEN, grad_scores, 0.0) + + # ~~~~~~~~~~~~~~~~~~~ Apply other buffer grad writes ~~~~~~~~~~~~~ + if not WRITE_DQ: + idx_b = off_z + idx_h = off_hq + idx_m = m + idx_n = n + scatter_mask = (offs_m1[None, :] < Q_LEN) & (offs_n1[:, None] < KV_LEN) + {{ modification( + subgraph_number=3, + output_name=None, + mask="scatter_mask", + input_shapes={"idx_m": ("1", "BLOCK_M1"), "idx_n": ("BLOCK_N1", "1"), "idx_b": ("1", "1"), "idx_h": ("1", "1")}, + score="pre_mod_scores", + b="idx_b", + h="idx_h", + m="idx_m", + n="idx_n", + grad_score_mod="dsT" + ) | indent_except_first(2) }} + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + dsT = grad_scores + if not IS_FULL_BLOCKS: + # (grads) apply mask for partially unmasked block + dsT = tl.where(mask_mod_output, dsT, 0.0) + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + dk += tl.dot(dsT.to(MATMUL_PRECISION), tl.trans(qT), input_precision=FLOAT32_PRECISION) + + return dk, dv diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/templates/flex_decode.py.jinja b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/templates/flex_decode.py.jinja new file mode 100644 index 0000000000000000000000000000000000000000..d425ca399532e658858127b0cccf9d54e42cc8d3 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/templates/flex_decode.py.jinja @@ -0,0 +1,254 @@ + {{def_kernel("Q", "K", "V", "M", "L", "KV_NUM_BLKS", "KV_IDX", "FULL_KV_NUM_BLKS", "FULL_KV_IDX")}} + # Sub notation for this kernel: + # Q: Query, K: Key, V: Value + # reduction buffers: M rowmax across local KV split, L local sumexp across local KV split + # M: Number of queries, N: Number of keys/values + # QK_HEAD_DIM: The dimension of the query and key embeddings + # V_HEAD_DIM: The dimension of the value embeddings + # BLOCK_M, QK_HEAD_DIM: M, and D dimemsion are always assigned to the same block + # z: Batch size, h: Number of heads, m: Number of queries per head, k: Number of keys per head t: Number of kv splits + # (Modifiable) Config options: + # SPLIT_KV: number of blocks K & V are split into + # TILE_KV: length of each local KV split + # BLOCK_M: block size that Q is padded along seqlen dim. + # BLOCK_N: block size of K & V along N dimension. + # GQA_SHARED_HEADS: number of query heads sharing one kv head in GQA setups. + # + # change of base out of the loop + # ROWS_GUARANTEED_SAFE: Is it guaranteed that at least one value in each row + # is not masked out? If so, we can skip an extra safety check + # SAFE_M_BOUNDARY: Is Q seqlen a multiple of BLOCK_M? If so, we can skip an extra boundary check for loading query. + # SAFE_N_BOUNDARY: Is KV seqlen a multiple of BLOCK_N? If so, we can skip an extra boundary check for loading key/value. + + # PRESCALE_QK: Whether to pre-scale QK by 1/sqrt(d) and change of base. + # + # SPARSE_KV_BLOCK_SIZE: sparse mask block size along KV seqlen dim. + # KV_NUM_BLKS: The number of KV blocks (that may or may not require masking) for each query. + # KV_IDX: The indices of KV blocks (that may or may not require masking) for each query. + # + # + # Output: ACC output accumulated across local KV split. + + tl.static_assert(SPARSE_KV_BLOCK_SIZE >= BLOCK_N and SPARSE_KV_BLOCK_SIZE % BLOCK_N == 0) + + # Define Q Strides + stride_qz, stride_qh, stride_qg, stride_qm, stride_qk = {{stride("Q")}} + stride_kz, stride_kh, stride_kn, stride_kk = {{stride("K")}} + stride_vz, stride_vh, stride_vn, stride_vk = {{stride("V")}} + stride_mz, stride_mt, stride_mh, stride_mm = {{stride("M")}} + stride_lz, stride_lt, stride_lh, stride_lm = {{stride("L")}} + + + Z = {{size("Q", 0)}} + ZKV = {{size("K", 0)}} + HKV = {{size("Q", 1)}} + G: tl.constexpr = GQA_SHARED_HEADS + HQ = HKV * G + Q_LEN = {{size("Q", 3)}} + KV_LEN = {{size("K", 2)}} + + MATMUL_PRECISION = Q.dtype.element_ty + + NUM_BLOCK_M = tl.cdiv(Q_LEN * G, BLOCK_M) + + off_block_m = tl.program_id(0).to(INDEX_DTYPE) + + off_z = tl.program_id(1).to(INDEX_DTYPE) // HKV + off_zkv = off_z % ZKV + off_hkv = tl.program_id(1).to(INDEX_DTYPE) % HKV + off_t = tl.program_id(2).to(INDEX_DTYPE) + + q_offset = off_z * stride_qz + off_hkv * stride_qh + k_offset = off_zkv * stride_kz + off_hkv * stride_kh + v_offset = off_zkv * stride_vz + off_hkv * stride_vh + + K = K + k_offset + V = V + v_offset + + SPARSE_Z = {{size("KV_NUM_BLKS", 0)}} + SPARSE_HQ = {{size("KV_NUM_BLKS", 1)}} + + sparse_idx_z = off_z % SPARSE_Z + sparse_idx_h = off_hkv % SPARSE_HQ + + SPARSE_KV_MULTIPLE: tl.constexpr = (SPARSE_KV_BLOCK_SIZE // BLOCK_N) + SPARSE_KV_BLOCK_CNT = tl.cdiv(KV_LEN, SPARSE_KV_BLOCK_SIZE) + + # initialize pointer to m and l + m_i = tl.zeros([BLOCK_M], dtype=tl.float32) - float("inf") + l_i = tl.zeros([BLOCK_M], dtype=tl.float32) + acc = tl.zeros([BLOCK_M, V_HEAD_DIM_ROUNDED], dtype=tl.float32) + + # initialize offsets + tl.device_assert(BLOCK_M % G == 0) + BLOCK_M_PER_HQ: tl.constexpr = BLOCK_M // G + off_g = tl.arange(0, G) # [G] + offs_g = tl.ravel(tl.broadcast_to(off_g[:, None], [G, BLOCK_M_PER_HQ])) # [BLOCK_M] + offs_hq = offs_g + off_hkv * G + off_m = tl.arange(0, BLOCK_M_PER_HQ) + off_block_m * BLOCK_M_PER_HQ + offs_m = tl.ravel(tl.broadcast_to(off_m[None, :], [G, BLOCK_M_PER_HQ])) # [BLOCK_M] + offs_d = tl.arange(0, QK_HEAD_DIM_ROUNDED) + offs_vd = tl.arange(0, V_HEAD_DIM_ROUNDED) + + # Get HZ offsets for KV_NUM_BLKS and KV_IDX + sparse_idx_m = off_block_m * BLOCK_M_PER_HQ // SPARSE_Q_BLOCK_SIZE + stride_block_z, stride_block_h, stride_block_row = {{stride("KV_NUM_BLKS")}} + sparse_block_zhm_offset = sparse_idx_z * stride_block_z + sparse_idx_h * stride_block_h + sparse_idx_m * stride_block_row + stride_kv_z, stride_kv_h, stride_kv_row, stride_kv_col = {{stride("KV_IDX")}} + sparse_idx_zhm_offset = sparse_idx_z * stride_kv_z + sparse_idx_h * stride_kv_h + sparse_idx_m * stride_kv_row + + q_range = stride_qg * off_g[:, None, None] + stride_qm * off_m[None, :, None] + stride_qk * offs_d[None, None, :] + + if not SAFE_M_BOUNDARY and not SAFE_HEAD_DIM: + q = tl.load(Q + q_offset + q_range, mask=(offs_d[None, None, :] < QK_HEAD_DIM) & (off_m[None, :, None] < Q_LEN)) + elif SAFE_M_BOUNDARY and not SAFE_HEAD_DIM: + q = tl.load(Q + q_offset + q_range, mask=offs_d[None, None, :] < QK_HEAD_DIM) + elif not SAFE_M_BOUNDARY and SAFE_HEAD_DIM: + q = tl.load(Q + q_offset + q_range, mask=off_m[None, :, None] < Q_LEN) + else: + q = tl.load(Q + q_offset + q_range) + + q = tl.reshape(q, [BLOCK_M, QK_HEAD_DIM_ROUNDED]) + + + # ~~~~~~~~~~~~~~ normal blocks ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # find first kv block we are loading and the number of blocks we are loading + # Offset the kv_indices tensor by the correct batch and head + kv_indices = KV_IDX + sparse_idx_zhm_offset + kv_num_blocks = tl.load(KV_NUM_BLKS + sparse_block_zhm_offset) + + # Make sure each split is a multiple of BLOCK_N + if HAS_FULL_BLOCKS: + # TODO do the a optimization as partial blocks once vllm is + # able to pass in full blocks: + # https://github.com/vllm-project/vllm/blob/61249b177de1566027fc74e9b51b45a4c973eb47/vllm/v1/attention/backends/flex_attention.py#L615-L616 + TILE_KV_OG = tl.cdiv(KV_LEN, SPLIT_KV) + else: + TILE_KV_OG = tl.cdiv(kv_num_blocks * SPARSE_KV_BLOCK_SIZE, SPLIT_KV) + TILE_KV = tl.cdiv(TILE_KV_OG, BLOCK_N) * BLOCK_N + TILE_KV_MULTIPLE: tl.constexpr = (TILE_KV // BLOCK_N) + + # Calculate KV blocks that belong this CTA. + block_n_start = off_t * TILE_KV_MULTIPLE # n_offset inside sparse block + block_n_end = block_n_start + TILE_KV_MULTIPLE # end BLOCK_N + + MAX_KV_IDX = {{size("KV_IDX", -1)}} + indices_idx = (block_n_start // SPARSE_KV_MULTIPLE) % (MAX_KV_IDX) + off_n_block_in_sparse = block_n_start % SPARSE_KV_MULTIPLE + off_n = tl.load(kv_indices + indices_idx) * SPARSE_KV_BLOCK_SIZE + off_n_block_in_sparse * BLOCK_N + # first kv block we're loading + + # last valid block according to sparse mask + block_n_last_valid = tl.minimum(kv_num_blocks * SPARSE_KV_MULTIPLE, tl.maximum(tl.cdiv(KV_LEN, BLOCK_N), 1)) + + offs_n = tl.arange(0, BLOCK_N) + off_n + + desc_k = None + desc_v = None + {%- if USE_TMA %} + desc_k = tl.make_tensor_descriptor( + base=K, + shape=[KV_LEN, QK_HEAD_DIM], + strides=[stride_kn, 1], + block_shape=[BLOCK_N, QK_HEAD_DIM_ROUNDED], + ) + + desc_v = tl.make_tensor_descriptor( + base=V, + shape=[KV_LEN, V_HEAD_DIM], + strides=[stride_vn, 1], + block_shape=[BLOCK_N, V_HEAD_DIM_ROUNDED], + ) + {%- endif %} + + acc, l_i, m_i = forward_inner( + {{gen_argdefs()}}, + q, K, V, desc_k, desc_v, Q_LEN, KV_LEN, + # accumulatd values + acc, l_i, m_i, + #offsets + off_z, offs_hq[:, None], offs_m[:, None], offs_n[None, :], + off_n, + #block sparse data + kv_indices, kv_num_blocks, + block_n_start, block_n_end if block_n_end <= block_n_last_valid else block_n_last_valid, + MATMUL_PRECISION, + stride_kk, stride_kn, stride_vn, stride_vk, + IS_FULL_BLOCKS=False, + ) + + + # ~~~~~~~~~~~~~~ "full" blocks ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # We know these blocks are guaranteed to be "full", so we don't need to + # apply mask_mod to them - only score_mod + if HAS_FULL_BLOCKS: + kv_indices = FULL_KV_IDX + sparse_idx_zhm_offset + kv_num_blocks = tl.load(FULL_KV_NUM_BLKS + sparse_block_zhm_offset) + # Assign full block in a reverse order for off_t. Prioritize the last CTA. + block_n_start = (SPLIT_KV - off_t - 1) * TILE_KV_MULTIPLE + block_n_end = block_n_start + TILE_KV_MULTIPLE + indices_idx = (block_n_start // SPARSE_KV_MULTIPLE) % (MAX_KV_IDX) + off_n_block_in_sparse = block_n_start % SPARSE_KV_MULTIPLE + off_n = tl.load(kv_indices + indices_idx) * SPARSE_KV_BLOCK_SIZE + off_n_block_in_sparse * BLOCK_N + + # last valid block according to sparse mask + block_n_last_valid = tl.minimum(kv_num_blocks * SPARSE_KV_MULTIPLE, tl.maximum(tl.cdiv(KV_LEN, BLOCK_N), 1)) + + offs_n = tl.arange(0, BLOCK_N) + off_n + + acc, l_i, m_i = forward_inner( + {{gen_argdefs()}}, + q, K, V, desc_k, desc_v, Q_LEN, KV_LEN, + # accumulatd values + acc, l_i, m_i, + #offsets + off_z, offs_hq[:, None], offs_m[:, None], offs_n[None, :], + off_n, + #block sparse data + kv_indices, kv_num_blocks, + block_n_start, block_n_end if block_n_end <= block_n_last_valid else block_n_last_valid, + MATMUL_PRECISION, + stride_kk, stride_kn, stride_vn, stride_vk, + IS_FULL_BLOCKS=True, + ) + + m_offset = off_t * stride_mt + off_z * stride_mz + l_offset = off_t * stride_lt + off_z * stride_lz + + M_block_ptr = tl.make_block_ptr( + base=M + m_offset, + shape=(G, Q_LEN), # (G, M) + strides=(stride_mh, stride_mm), + offsets=(off_hkv*G, off_block_m * BLOCK_M_PER_HQ), + block_shape=(G, BLOCK_M_PER_HQ), + order=(1, 0) + ) + L_block_ptr = tl.make_block_ptr( + base=L + l_offset, + shape=(G, Q_LEN), # (G, M) + strides=(stride_lh, stride_lm), + offsets=(off_hkv*G, off_block_m * BLOCK_M_PER_HQ), + block_shape=(G, BLOCK_M_PER_HQ), + order=(1, 0) + ) + + # Store output, logsumexp and rowmax for cross CTA reduction. (all in float32, even when input data are in fp16) + m_i = m_i.reshape(G, BLOCK_M_PER_HQ) + l_i = l_i.reshape(G, BLOCK_M_PER_HQ) + if SAFE_M_BOUNDARY: + tl.store(M_block_ptr, m_i) + tl.store(L_block_ptr, l_i) + else: + tl.store(M_block_ptr, m_i, boundary_check=(1,)) + tl.store(L_block_ptr, l_i, boundary_check=(1,)) + + # -- store output + idx_z = off_z + idx_t = off_t + idx_hq = off_hkv*G + off_g[:, None, None] + idx_m = off_m[None, :, None] + idx_d = offs_vd[None, None, :] + + mask = (idx_m < Q_LEN) & (idx_d < V_HEAD_DIM) + acc = acc.reshape(G, BLOCK_M_PER_HQ, V_HEAD_DIM_ROUNDED) + {{store_output(("idx_z", "idx_t", "idx_hq", "idx_m", "idx_d"), "acc", "mask", val_shape=("GQA_SHARED_HEADS", "BLOCK_M_PER_HQ", "V_HEAD_DIM_ROUNDED"))}} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/templates/utilities.py.jinja b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/templates/utilities.py.jinja new file mode 100644 index 0000000000000000000000000000000000000000..0c40b43277f8ae2da748487803758ff46c338ced --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/flex/templates/utilities.py.jinja @@ -0,0 +1,59 @@ + + +# Utility triton funcs +@triton.jit +def get_offset_for_next_block( + loop_iter, col_indices, total_blocks, + SPARSE_BLOCK, SPARSE_BLOCK_MULTIPLE, BLOCK, + BLOCKS_ARE_CONTIGUOUS: tl.constexpr +): + if BLOCKS_ARE_CONTIGUOUS: + return BLOCK + cur_block_idx = loop_iter // SPARSE_BLOCK_MULTIPLE + cur_block = tl.load(col_indices + cur_block_idx, eviction_policy="evict_last") + next_block = tl.load(col_indices + cur_block_idx + 1, eviction_policy="evict_last", mask=cur_block_idx + 1 < total_blocks) + needs_jump = (loop_iter + 1) % SPARSE_BLOCK_MULTIPLE == 0 + jump_to_block = (next_block - cur_block ) * SPARSE_BLOCK - (SPARSE_BLOCK_MULTIPLE - 1) * BLOCK + offset = jump_to_block * needs_jump + (1 - needs_jump) * BLOCK + return offset + +@triton.jit +def get_bounded_indices(indices, max_len=None): + return indices % max_len if max_len is not None else indices + +@triton.jit +def load_checked_block(block_ptr, IS_DIVISIBLE: tl.constexpr, SAFE_HEAD_DIM: tl.constexpr): + if IS_DIVISIBLE and SAFE_HEAD_DIM: + return tl.load(block_ptr) + elif IS_DIVISIBLE and not SAFE_HEAD_DIM: + return tl.load(block_ptr, boundary_check=(1,), padding_option="zero") + elif not IS_DIVISIBLE and SAFE_HEAD_DIM: + return tl.load(block_ptr, boundary_check=(0,), padding_option="zero") + else: + return tl.load(block_ptr, boundary_check=(0, 1), padding_option="zero") + +@triton.jit +def load_checked_2d( + ptr, + offs_m, + offs_n, + stride_m, + stride_n, + IS_DIVISIBLE_M: tl.constexpr, + IS_DIVISIBLE_N: tl.constexpr, + M_LEN: tl.constexpr, + N_LEN: tl.constexpr, +): + # Calculate final pointer if strides are provided + if stride_m is not None and stride_n is not None: + ptr = ptr + offs_m[:, None] * stride_m + offs_n[None, :] * stride_n + + # Handle all masking cases + if not IS_DIVISIBLE_M and not IS_DIVISIBLE_N: + return tl.load(ptr, mask=(offs_m[:, None] < M_LEN) & (offs_n[None, :] < N_LEN), other=0.0) + elif IS_DIVISIBLE_M and not IS_DIVISIBLE_N: + return tl.load(ptr, mask=(offs_n[None, :] < N_LEN), other=0.0) + elif not IS_DIVISIBLE_M and IS_DIVISIBLE_N: + return tl.load(ptr, mask=(offs_m[:, None] < M_LEN), other=0.0) + else: # Both divisible + return tl.load(ptr) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/mm.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/mm.py new file mode 100644 index 0000000000000000000000000000000000000000..13653d41e66d5fbc53dbddd648e0c2f39ee86d59 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/mm.py @@ -0,0 +1,1160 @@ +# mypy: allow-untyped-defs +import functools +import logging +from typing import Any + +import torch +from torch._dynamo.utils import counters +from torch._inductor.autoheuristic.autoheuristic import AutoHeuristicSelectAlgorithm +from torch._inductor.autoheuristic.autoheuristic_utils import ( + AHContext, + context_add_strides, + context_add_using_tf32, + mm_operations, +) +from torch._inductor.codegen.cpp_gemm_template import CppGemmTemplate +from torch._inductor.remote_gemm_autotune_cache import gen_best_config +from torch._inductor.virtualized import ops, V +from torch.fx.experimental.proxy_tensor import make_fx +from torch.nn.functional import ScalingType # type: ignore[attr-defined] +from torch.torch_version import TorchVersion + +from .. import config as inductor_config, distributed_autotune +from ..codegen.cutlass.gemm_template import CUTLASS2xGemmTemplate, CUTLASS3xGemmTemplate +from ..codegen.rocm.ck_tile_universal_gemm_template import CKTileGemmTemplate +from ..codegen.rocm.ck_universal_gemm_template import CKGemmTemplate +from ..codegen.subgraph import SubgraphChoiceCaller, SubgraphTemplate +from ..ir import Buffer, ChoiceCaller, is_triton, Layout +from ..kernel_inputs import MMKernelInputs +from ..lowering import ( + lowerings, + make_pointwise, + make_reduction, + register_lowering, + transform_args, +) +from ..select_algorithm import ( + autotune_select_algorithm, + ExternKernelChoice, + KernelTemplate, + realize_inputs, + TritonTemplate, +) +from ..utils import ( + _use_cutlass_for_op, + ceildiv, + use_aten_gemm_kernels, + use_ck_gemm_template, + use_ck_tile_gemm_template, + use_cpp_gemm_template, + use_cutlass_template, + use_decompose_k_choice, + use_nv_universal_gemm_template, + use_triton_blackwell_tma_template, + use_triton_scaling_template, + use_triton_template, + use_triton_tma_template, +) +from .mm_common import ( + _is_static_problem, + load_kernel_template, + mm_args, + mm_grid, + persistent_mm_grid, + use_native_matmul, +) + + +try: + import triton + + triton_version = TorchVersion(triton.__version__) + has_triton = True +except ImportError: + triton_version = TorchVersion("0.0.0") + has_triton = False + +log = logging.getLogger(__name__) +aten = torch.ops.aten +prims = torch.ops.prims + +# We define each template kernel in a separate file which is the name of the input to load_kernel_template +# (e.g. triton_mm for templates/triton_mm.py.jinja). +# If you are adding a new template, please follow that pattern and add a new file with your implementation in the templates folder. +mm_template = TritonTemplate( + name="mm", + grid=mm_grid, + source=load_kernel_template("triton_mm") + if (torch.version.hip is None) or triton_version >= "3.3.0" + # FIXME: To get around rocm failures like https://github.com/pytorch/pytorch/actions/runs/13123783322/job/36617154943 + # The only difference between the two templates is M >= BLOCK_M and N >= BLOCK_N checking. + # See more details in https://github.com/pytorch/pytorch/pull/146293 + else load_kernel_template("triton_mm_rocm"), + cache_codegen_enabled_for_template=True, + prologue_loads_all_inputs=True, +) + +persistent_tma_mm_template = TritonTemplate( + name="mm_persistent_tma", + grid=persistent_mm_grid, + source=load_kernel_template("triton_persistent_tma_mm"), +) + +# Non-TMA Triton template for persistent MM +# used on AMD +persistent_mm_template = TritonTemplate( + name="mm_persistent", + grid=persistent_mm_grid, + source=load_kernel_template("triton_persistent_mm"), +) + + +scaled_mm_device_tma_epilogue_scaling_template = TritonTemplate( + name="scaled_mm_device_tma_epilogue_scaling", + grid=persistent_mm_grid, + source=load_kernel_template("triton_epilogue_scaled_mm"), +) + + +scaled_mm_device_tma_main_loop_scaling_template = TritonTemplate( + name="scaled_mm_device_tma_main_loop_scaling", + grid=persistent_mm_grid, + source=load_kernel_template("triton_main_loop_scaled_mm"), +) + +blackwell_ws_persistent_device_tma_mm_template = TritonTemplate( + name="blackwell_ws_persistent_device_tma", + grid=persistent_mm_grid, + source=load_kernel_template("triton_blackwell_ws_persistent_device_tma_mm"), +) + + +# prevent duplication registration of extern functions +@functools.cache +def lazy_register_extern_choice(fn): + return ExternKernelChoice(fn) + + +aten_mm = ExternKernelChoice(torch.mm, "at::mm_out", op_overload=aten.mm.out) +aten_mm_dtype = ExternKernelChoice( + torch.mm, + "at::mm_dtype_out", + name="mm_dtype", + op_overload=aten.mm.dtype_out, +) + +aten_addmm = ExternKernelChoice( + torch.addmm, "at::addmm_out", op_overload=aten.addmm.out +) + +aten__int_mm = ExternKernelChoice( + torch._int_mm, "at::_int_mm_out", op_overload=aten._int_mm.out +) + +aten__sparse_semi_structured_mm = ExternKernelChoice( + torch._sparse_semi_structured_mm, + "at::_sparse_semi_structured_mm", + has_out_variant=False, + op_overload=aten._sparse_semi_structured_mm.default, +) + +aten__fp8_mm = ExternKernelChoice( + torch._scaled_mm, "at::_scaled_mm_out", op_overload=aten._scaled_mm.out +) + + +def _is_int8_mat(mat): + return mat.get_dtype() in (torch.int8, torch.uint8) + + +def bias_addmm(inp, mat1, mat2, *, out=None, alpha=1, beta=1): + """ + Giving torch.addmm a 1D tensor calls a different (faster) cublasLt + kernel under the hood. There are a few shapes where this is slower, + but they are rare. + """ + if (inp.stride(0) == 0 and inp.size(0) != 0) or inp.size(0) == 1: + return torch.addmm(inp[0], mat1, mat2, out=out, alpha=alpha, beta=beta) + return torch.addmm(inp, mat1, mat2, out=out, alpha=alpha, beta=beta) + + +def check_supported_striding(mat_a, mat_b) -> None: + def is_row_major(stride) -> bool: + return V.graph.sizevars.statically_known_equals(stride[1], 1) + + def is_col_major(stride) -> bool: + return V.graph.sizevars.statically_known_equals(stride[0], 1) + + def has_zero_dim(size) -> bool: + return bool( + V.graph.sizevars.statically_known_equals(size[0], 0) + or V.graph.sizevars.statically_known_equals(size[1], 0) + ) + + # Check mat_a (self) stride requirements + torch._check( + is_row_major(mat_a.get_stride()) or has_zero_dim(mat_a.get_size()), + lambda: f"mat_a must be row_major, got stride {mat_a.get_stride()}", + ) + + # Check mat_b stride requirements + torch._check( + is_col_major(mat_b.get_stride()) or has_zero_dim(mat_b.get_size()), + lambda: f"mat_b must be col_major, got stride {mat_b.get_stride()}", + ) + + +aten_bias_addmm = ExternKernelChoice(bias_addmm, None) + + +def decomposeK(a, b, k_splits): + m = a.shape[0] + n = b.shape[1] + k = a.shape[1] + + k_parts = k // k_splits + B = k_splits + a_reshaped = torch.permute(a.reshape(m, B, k_parts), (1, 0, 2)) + b_reshaped = b.reshape(B, k_parts, n) + result = torch.bmm(a_reshaped, b_reshaped, out_dtype=torch.float32) + reduced_buf = torch.sum(result, 0) + return reduced_buf.to(a.dtype) + + +class DecomposeKSugraphTemplate(SubgraphTemplate): + def __init__(self): + super().__init__( + name="decompose_k", + ) + + def generate( # type: ignore[override] + self, + input_nodes: list[Buffer], + layout: Layout, + k_split: int, + ) -> SubgraphChoiceCaller: + from torch._dispatch.python import enable_python_dispatcher + + from ..decomposition import select_decomp_table + + name = f"decompose_k_mm_{k_split}_split" + description = f"{k_split=}" + + with enable_python_dispatcher(): + decompositions = select_decomp_table() + fn = make_fx( + functools.partial(decomposeK, k_splits=k_split), + decompositions, + ) + + return super().generate( + name=name, + input_nodes=input_nodes, + layout=layout, + make_fx_graph=fn, + description=description, + ) + + +decompose_k_subgraph_template = DecomposeKSugraphTemplate() + + +class ContiguousTemplate(SubgraphTemplate): + def __init__(self, name: str, description: str, fn: Any): + self.name = name + self.description = description + self.fn = fn + super().__init__( + name=name, + ) + + def generate( # type: ignore[override] + self, + input_nodes: list[Buffer], + layout: Layout, + ) -> SubgraphChoiceCaller: + from torch._dispatch.python import enable_python_dispatcher + + from ..decomposition import select_decomp_table + + with enable_python_dispatcher(): + decompositions = select_decomp_table() + fn = make_fx( + self.fn, + decompositions, + ) + + return super().generate( + name=self.name, + input_nodes=input_nodes, + layout=layout, + make_fx_graph=fn, + description=self.description, + ) + + +def contiguous_mm(a, b): + return torch.mm(a, b.contiguous()) + + +def contiguous_addmm(inp, a, b): + return torch.addmm(inp, a, b.contiguous()) + + +mm_contiguous_subgraph_template = ContiguousTemplate( + "contiguous_mm", "contiguous mm", contiguous_mm +) +addmm_contiguous_subgraph_template = ContiguousTemplate( + "contiguous_addmm", "contiguous addmm", contiguous_addmm +) + + +@register_lowering(aten.mm, type_promotion_kind=None) +def tuned_mm(mat1, mat2, out_dtype=None, *, layout=None): + """ + Lowering for autotuning aten.mm with different backends (Aten, Triton, CUTLASS, etc.) + """ + if out_dtype is not None: + input_dtype = mat1.get_dtype() + torch._check( + mat2.get_dtype() == input_dtype, + lambda: "input dtypes must be the same", + ) + torch._check( + mat1.get_device().type in ("cuda", "xpu"), + lambda: "out_dtype is only supported for CUDA or XPU", + ) + torch._check( + out_dtype == input_dtype + or ( + out_dtype == torch.float32 + and input_dtype in (torch.float16, torch.bfloat16) + ), + lambda: "out_dtype must be the same as input dtype or fp32 for fp16/bf16 inputs", + ) + + # Lower matmul-related operations (e.g., torch.matmul / torch.bmm / torch.addmm) + # into native matmul IR using `ops.dot`. When we see a matmul pattern + # (C[y, x] = A[y, r] * B[r, x]), the core idea is to emulate a broadcasted + # multiply followed by a sum. + # + # For example, given `C = torch.matmul(A, B)`, this can be rewritten as: + # + # Prod = A.unsqueeze(-1) * B.unsqueeze(0) + # C = Prod.sum(dim=1) + # + # Instead of explicitly using `ops.mul` and `ops.reduction("sum")`, we lower + # these into `ops.dot` (pointwise) and `ops.reduction("dot")`. These IR nodes + # are semantically equivalent to the `ops.mul` + `ops.reduction("sum")` + # combination, but are lowered to `tl.dot` during the code generation phase. + if use_native_matmul(mat1, mat2): + mat1 = lowerings[aten.unsqueeze](mat1, -1) + mat2 = lowerings[aten.unsqueeze](mat2, 0) + args, kwargs = transform_args( + args=[mat1, mat2], + kwargs={}, + broadcast=True, + type_promotion_kind=None, + convert_input_to_bool=False, + ) # Handles broadcasting the arguments + + if inductor_config.triton.codegen_upcast_to_fp32 and mat1.dtype in [ + torch.float16, + torch.bfloat16, + ]: + + def _to_dtype(x): + return ops.to_dtype(x, mat1.dtype, use_compute_types=False) + + args = [make_pointwise(_to_dtype)(x) for x in args] + mul_pointwise = make_pointwise(ops.dot)(*args) + dot_reduction = make_reduction("dot")(mul_pointwise, 1) + + return dot_reduction + + # TODO(coconutruben): integrate into MMKernelInputs when all callsites use that + m, n, k, layout, mat1, mat2 = mm_args( + mat1, mat2, layout=layout, out_dtype=out_dtype + ) + static_shape, is_nonzero = _is_static_problem(layout) + name = "mm" + + # Create MMKernelInputs for standard MM at the top + kernel_inputs = MMKernelInputs([mat1, mat2], out_dtype=out_dtype) + + # below is for getting an overview logging info of inductor mms + counters["aten_mm_info"][f"aten.mm_{m}_{n}_{k}"] += 1 + log.info( + "Tuned aten.mm: m=%s, n=%s, k=%s, mat1_dtype=%s, mat2_dtype=%s, output_layout=%s", + m, + n, + k, + mat1.get_dtype(), + mat2.get_dtype(), + layout, + ) + + choices: list[ChoiceCaller] = [] + static_shape, is_nonzero = _is_static_problem(layout) + + aten_handler: ExternKernelChoice = aten_mm + aten_extra_kwargs: dict[str, Any] = {} + if out_dtype is not None: + aten_handler = aten_mm_dtype + aten_extra_kwargs = {"out_dtype": out_dtype} + + templates_to_use: list[ExternKernelChoice | KernelTemplate] = [] + kwarg_overrides: dict[str, dict[str, Any]] = {} + if use_aten_gemm_kernels(): + templates_to_use.append(aten_handler) + if aten_extra_kwargs: + kwarg_overrides[aten_handler.uid] = aten_extra_kwargs + + if ( + out_dtype is None + and is_nonzero + and use_triton_template(layout, check_max_autotune=True) + ): + if use_decompose_k_choice(m, n, k): + templates_to_use.append(decompose_k_subgraph_template) + # Triton Templates typically perform very poorly for large K. + # Its highly unlikely that if we want to use decompose_k, then + # Triton will ever win. + # + # To be conservative we increase this threshold for N/M by 2. + is_exhaustive = inductor_config.max_autotune_gemm_search_space == "exhaustive" + if is_exhaustive or not use_decompose_k_choice(m, n, k, threshold_multiple=2): + templates_to_use.append(mm_template) + + if use_triton_blackwell_tma_template( + mat1, mat2, output_layout=layout, add_guards=True + ): + templates_to_use.append(blackwell_ws_persistent_device_tma_mm_template) + elif use_triton_tma_template( + mat1, mat2, output_layout=layout, add_guards=True + ): + if torch.version.hip is None: + templates_to_use.append(persistent_tma_mm_template) + else: + templates_to_use.append(persistent_mm_template) + + templates_to_use.append(mm_contiguous_subgraph_template) + + choices.extend( + V.choices.get_template_configs( + kernel_inputs, + templates_to_use, + "mm", + kwarg_overrides=kwarg_overrides, + ) + ) + + if ( + out_dtype is None + and is_nonzero + and use_cutlass_template(layout, m, n, k) + and _use_cutlass_for_op("mm") + ): + CUTLASS3xGemmTemplate.add_cutlass_gemm_choices( + choices, layout, kernel_inputs.nodes() + ) + + if out_dtype is None and is_nonzero and use_ck_gemm_template(layout, m, n, k): + CKGemmTemplate.add_ck_gemm_choices(choices, layout, kernel_inputs.nodes()) + if out_dtype is None and is_nonzero and use_ck_tile_gemm_template(layout, m, n, k): + CKTileGemmTemplate.add_choices(choices, layout, kernel_inputs.nodes()) + + if ( + out_dtype is None + and is_nonzero + and use_nv_universal_gemm_template(layout, m, n, k, mat1, mat2) + ): + from ..codegen.nv_universal_gemm import add_nv_universal_gemm_choices + + add_nv_universal_gemm_choices(choices, layout, kernel_inputs) + + if out_dtype is None and use_cpp_gemm_template(layout, mat1, mat2): + CppGemmTemplate.add_choices( + choices, + layout, + kernel_inputs.nodes(), + ) + + input_nodes = [mat1, mat2] + if ( + out_dtype is None + and is_nonzero + and use_triton_template(layout) + and torch._inductor.config.run_autoheuristic(name) + and is_triton(mat1) + ): + always_included = [] + if use_aten_gemm_kernels(): + always_included.append("extern_mm") + num_choices_before_extra_configs = len(choices) + choices.extend( + V.choices.get_template_configs( + # TODO(coconutruben): remove once we deprecate ah + # mm-extra is a hack to keep the ah functionality alive + # while we transition to the unified kwargs retrieval + kernel_inputs, + [mm_template], + "mm-ah", + ) + ) + + # using AutoHeuristic for ranking + ah_choices = mm_autoheuristic( + mat1, + mat2, + m, + n, + k, + choices, + name, + input_nodes, + mm_operations(), + None, + top_k=10, + always_included=always_included, + ) + if not torch._inductor.config.collect_autoheuristic(name): + # if we are collecting data, we do not want to modify choices + if ah_choices is not None and len(ah_choices) > 0: + # the order in which autoheuristic returns choices is not the same as + # as the order of choices, which affects things like epilogue fusion. + # once epilogue fusion benchmarks choices in sorted order, I think we can + # just use the order returned by autoheuristic + choices = [choice for choice in choices if choice in ah_choices] + else: + choices = choices[:num_choices_before_extra_configs] + + if out_dtype is None: + for k in inductor_config.external_matmul: + choices.append( + lazy_register_extern_choice(k).bind(kernel_inputs.nodes(), layout) + ) + + best_config_future = None + if out_dtype is None and torch._inductor.config.remote_gemm_autotune_cache: + # Purposely not awaiting the future here - this kicks off the best config lookup at lowering time + # The future will be awaited at scheduling time in select_algorithm.py + best_config_future = gen_best_config(mat1, mat2) + + if box := distributed_autotune.maybe_autotune_remote( + name, choices, kernel_inputs.nodes(), layout + ): + return box + + node, _ = autotune_select_algorithm( + name, + choices, + kernel_inputs.nodes(), + layout, + best_config_future=best_config_future, + ) + return node + + +@register_lowering(aten._int_mm, type_promotion_kind=None) +def tuned_int_mm(mat1, mat2, *, layout=None): + # TODO(coconutruben): integrate into MMKernelInputs when all callsites use that + m, n, k, layout, mat1, mat2 = mm_args( + mat1, mat2, layout=layout, out_dtype=torch.int32 + ) + name = "int_mm" + # below is for getting an overview logging info of inductor mms + counters["aten_mm_info"][f"aten._int_mm_{m}_{n}_{k}"] += 1 + log.info( + "Tuned aten._int_mm: m=%s, n=%s, k=%s, mat1_dtype=%s, mat2_dtype=%s, output_layout=%s", + m, + n, + k, + mat1.get_dtype(), + mat2.get_dtype(), + layout, + ) + + static_shape, is_nonzero = _is_static_problem(layout) + use_cutlass = static_shape and is_nonzero and use_cutlass_template(layout, m, n, k) + choices: list[ChoiceCaller] = [] + + # Create MMKernelInputs for Int MM + kernel_inputs = MMKernelInputs([mat1, mat2], out_dtype=torch.int32) + + # Collect all templates for unified call + templates_to_use: list[ExternKernelChoice | KernelTemplate] = [] + if use_aten_gemm_kernels(): + templates_to_use.append(aten__int_mm) + + if is_nonzero and use_triton_template( + layout, enable_int32=True, check_max_autotune=False + ): + templates_to_use.append(mm_template) + + # Single unified call for all templates + choices.extend( + V.choices.get_template_configs(kernel_inputs, templates_to_use, name) + ) + + if use_cutlass and _use_cutlass_for_op(name): + CUTLASS3xGemmTemplate.add_cutlass_gemm_choices( + choices, layout, kernel_inputs.nodes(), fuseable=True, non_fuseable=True + ) + + node, _ = autotune_select_algorithm(name, choices, kernel_inputs.nodes(), layout) + return node + + +@register_lowering(aten.addmm, type_promotion_kind=None) +def tuned_addmm(inp, mat1, mat2, *, alpha=1, beta=1, layout=None): + """ + Lowering for autotuning aten.addmm with different backends (Aten, Triton, CUTLASS, etc.) + """ + if use_native_matmul(mat1, mat2): + if beta == 0: + arg1 = 0 + else: + arg1 = lowerings[aten.mul](beta, inp) + + if alpha == 0: + arg2 = 0 + else: + arg2 = lowerings[aten.mul](alpha, lowerings[aten.mm](mat1, mat2)) + + return lowerings[aten.add](arg1, arg2) + + # TODO(coconutruben): integrate into MMKernelInputs when all callsites use that + m, n, k, layout, mat1, mat2, inp_expanded = mm_args(mat1, mat2, inp, layout=layout) + static_shape, is_nonzero = _is_static_problem(layout) + name = "addmm" + + # Create MMKernelInputs for AddMM at the top + kernel_inputs = MMKernelInputs( + [inp_expanded, mat1, mat2], scalars=dict(alpha=alpha, beta=beta) + ) + kernel_inputs_aten = MMKernelInputs( + [inp, mat1, mat2], scalars=dict(alpha=alpha, beta=beta) + ) + + choices: list[ChoiceCaller] = [] + + # below is for getting an overview logging info of inductor mms + counters["aten_mm_info"][f"aten.addmm_{m}_{n}_{k}"] += 1 + log.info( + "Tuned aten.addmm: m=%s, n=%s, k=%s, mat1_dtype=%s, mat2_dtype=%s, output_layout=%s", + m, + n, + k, + mat1.get_dtype(), + mat2.get_dtype(), + layout, + ) + if (not is_nonzero) or ( + not (inductor_config.max_autotune or inductor_config.max_autotune_gemm) + ): + choices.extend( + V.choices.get_template_configs( + kernel_inputs_aten, + [aten_addmm], + name, + ) + ) + node, _ = autotune_select_algorithm( + name, choices, kernel_inputs.nodes(), layout + ) + return node + + templates_to_use: list[ExternKernelChoice | KernelTemplate] = [] + + if use_aten_gemm_kernels(): + aten_templates: list[ExternKernelChoice | KernelTemplate] = [aten_addmm] + if ( + inp.get_stride()[0] == 0 + and len(inp.get_size()) == 2 + and inductor_config.triton.autotune_cublasLt + and not V.graph.cpp_wrapper # bias_addmm only has a Python implementation + ): + aten_templates.append(aten_bias_addmm) + + # On ROCm, ATen choices use original bias input; non-ROCm keeps unified inputs. + choices.extend( + V.choices.get_template_configs(kernel_inputs_aten, aten_templates, name) + ) + + if is_nonzero and use_triton_template(layout, check_max_autotune=False): + templates_to_use.append(mm_template) + + if use_triton_blackwell_tma_template( + mat1, mat2, output_layout=layout, add_guards=True + ): + templates_to_use.append(blackwell_ws_persistent_device_tma_mm_template) + elif use_triton_tma_template(mat1, mat2, output_layout=layout, add_guards=True): + if torch.version.hip is None: + templates_to_use.append(persistent_tma_mm_template) + else: + templates_to_use.append(persistent_mm_template) + + templates_to_use.append(addmm_contiguous_subgraph_template) + + # Single unified call for all templates + choices.extend( + V.choices.get_template_configs(kernel_inputs, templates_to_use, name) + ) + + if ( + is_nonzero + and use_cutlass_template(layout, m, n, k) + and _use_cutlass_for_op(name) + ): + CUTLASS3xGemmTemplate.add_cutlass_gemm_choices( + choices, + layout, + # reorder here because CUTLASS expects (x, w, bias) but torch + # is bias, x, w + kernel_inputs.nodes(reorder=[1, 2, 0]), + alpha=alpha, + beta=beta, + input_reorder=[2, 0, 1], + ) + + if is_nonzero and use_ck_gemm_template(layout, m, n, k): + CKGemmTemplate.add_ck_gemm_choices( + choices, + layout, + # reorder here because CK expects (x, w, bias) but torch + # is bias, x, w + kernel_inputs.nodes(reorder=[1, 2, 0]), + alpha=alpha, + beta=beta, + input_reorder=[2, 0, 1], + ) + + if use_cpp_gemm_template(layout, mat1, mat2): + CppGemmTemplate.add_choices( + choices, + layout, + kernel_inputs.nodes(), + alpha=alpha, + beta=beta, + has_bias=True, + ) + + node, _ = autotune_select_algorithm(name, choices, kernel_inputs.nodes(), layout) + return node + + +@register_lowering(aten._sparse_semi_structured_mm, type_promotion_kind=None) +def tuned_sparse_semi_structured_mm( + mat1, mat1_meta, mat2, *, out_dtype=None, layout=None +): + from torch._inductor.select_algorithm import realize_inputs + + # TODO(coconturuben): support V.choices.get_mm_configs for sparse_semi_structured_mm + mat1, mat1_meta, mat2 = realize_inputs(mat1, mat1_meta, mat2) + m1, k1 = mat1.get_size() + m2, _ = mat1_meta.get_size() + k2, n = mat2.get_size() + m = V.graph.sizevars.check_equals_and_simplify(m1, m2) + k = V.graph.sizevars.check_equals_and_simplify(2 * k1, k2) + if layout is None: + from torch._inductor.ir import FixedLayout + + layout = FixedLayout( + mat2.get_device(), + out_dtype if out_dtype else mat2.get_dtype(), + [m, n], + [n, 1], + ) + else: + assert out_dtype is None, "out_dtype is ignored if layout is specified." + + choices = ( + [ + aten__sparse_semi_structured_mm.bind( + (mat1, mat1_meta, mat2), layout, out_dtype=out_dtype + ) + ] + if use_aten_gemm_kernels() + else [] + ) + + if ( + m * n != 0 + and use_cutlass_template(layout, m, n, k) + and _use_cutlass_for_op("sparse_semi_structured_mm") + ): + CUTLASS2xGemmTemplate.add_cutlass_gemm_choices( + choices, layout, [mat1, mat2, mat1_meta], fuseable=True, non_fuseable=True + ) + + node, _ = autotune_select_algorithm( + "sparse_semi_structured_mm", choices, (mat1, mat1_meta, mat2), layout + ) + return node + + +scaling_pairs = [ + (ScalingType.TensorWise, ScalingType.TensorWise), + (ScalingType.RowWise, ScalingType.RowWise), + (ScalingType.BlockWise1x128, ScalingType.BlockWise128x128), + (ScalingType.BlockWise1x128, ScalingType.BlockWise1x128), + (ScalingType.BlockWise128x128, ScalingType.BlockWise1x128), +] + + +epilogue_scaling_types = [ScalingType.TensorWise, ScalingType.RowWise] +main_loop_scaling_types = [ScalingType.BlockWise1x128, ScalingType.BlockWise128x128] + + +def _is_tensorwise_scaling(sz: Any) -> bool: + return (len(sz) == 0) or all( + V.graph.sizevars.statically_known_equals(d, 1) for d in sz + ) + + +def _is_rowwise_scaling(sz: Any, transpose: bool) -> bool: + idx = 0 if transpose else -1 + return V.graph.sizevars.statically_known_equals(sz[idx], 1) + + +def _is_blockwise1xTILESIZE_scaling( + sz: Any, tensor_sz: Any, tile_size: int, transpose: bool +) -> bool: + lhs = 1 if transpose else 0 + rhs = 0 if transpose else 1 + return V.graph.sizevars.statically_known_equals( + sz[lhs], tensor_sz[lhs] + ) and V.graph.sizevars.statically_known_equals( + sz[rhs], ceildiv(tensor_sz[rhs], tile_size) + ) + + +def _is_blockwise128x128_scaling(sz: Any, tensor_sz: Any) -> bool: + return V.graph.sizevars.statically_known_equals( + sz[0], ceildiv(tensor_sz[0], 128) + ) and V.graph.sizevars.statically_known_equals(sz[1], ceildiv(tensor_sz[1], 128)) + + +def is_desired_scaling( + t: Any, + scale_size: torch.Tensor, + scaling_type: ScalingType, + transpose: bool = False, +) -> bool: + match scaling_type: + case ScalingType.TensorWise: + return _is_tensorwise_scaling(scale_size) + case ScalingType.RowWise: + return _is_rowwise_scaling(scale_size, transpose) + case ScalingType.BlockWise1x128: + return _is_blockwise1xTILESIZE_scaling( + scale_size, t.get_size(), 128, transpose + ) + case ScalingType.BlockWise128x128: + return _is_blockwise128x128_scaling(scale_size, t.get_size()) + case _: + raise AssertionError(f"Unsupported scaling type {scaling_type}") + + +def get_tile_size(scale_option) -> int: + match scale_option: + case ScalingType.BlockWise128x128: + return 128 + case ScalingType.BlockWise1x128: + return 128 + case _: + raise AssertionError( + f"Unsupported scaling type {scale_option} in get_tile_size" + ) + + +def get_scaling_options( + mat_a: Any, + mat_b: Any, + scale_a_size: torch.Tensor, + scale_b_size: torch.Tensor, +) -> tuple[ScalingType, ScalingType]: + for scale_option_a, scale_option_b in scaling_pairs: + if is_desired_scaling( + mat_a, scale_a_size, scale_option_a + ) and is_desired_scaling(mat_b, scale_b_size, scale_option_b, transpose=True): + return scale_option_a, scale_option_b + + raise AssertionError( + f"Inductor Triton does not support scale_a.shape = {scale_a_size}, scale_b.shape = {scale_b_size}" + ) # verify that shapes are supported by at least one existing pairing + + +@register_lowering(aten._scaled_mm.default, type_promotion_kind=None) # type: ignore[misc] +def tuned_scaled_mm( + mat_a, + mat_b, + scale_a, + scale_b, + bias=None, + scale_result=None, + out_dtype=None, + use_fast_accum=False, + layout=None, +): + """ + Performs an optimized matrix multiplication where scaling factors are applied + to the inputs and/or output. + + Args: + mat1 (Tensor): First input matrix + mat2 (Tensor): Second input matrix + scale1 (Tensor): Scale factor applied to mat1 (supports broadcasting) + scale2 (Tensor): Scale factor applied to mat2 (supports broadcasting) + bias (Tensor, optional): Optional bias tensor to add to the result + layout: Layout hint for optimization + + Returns: + Tensor: The result of the scaled matrix multiplication + """ + # TODO(coconutruben): integrate into MMKernelInputs when all callsites use that + m, n, k, layout, mat_a, mat_b = mm_args( + mat_a, mat_b, layout=layout, out_dtype=out_dtype + ) + # below is for getting an overview logging info of inductor mms + counters["aten_mm_info"][f"aten._scaled_mm.default_{m}_{n}_{k}"] += 1 + log.info( + "Tuned aten._scaled_mm.default: m=%s, n=%s, k=%s, mat1_dtype=%s, mat2_dtype=%s, output_layout=%s", + m, + n, + k, + mat_a.get_dtype(), + mat_b.get_dtype(), + layout, + ) + name = "scaled_mm" + check_supported_striding(mat_a, mat_b) + + scale_a_real, scale_b_real = realize_inputs(scale_a, scale_b) + + input_nodes: list[Any] + + if not bias: + input_nodes = [mat_a, mat_b, scale_a_real, scale_b_real] + else: + bias_real = realize_inputs(bias) + input_nodes = [mat_a, mat_b, scale_a_real, scale_b_real, bias_real] + + # Create MMKernelInputs for Scaled MM (matrices are at indices 0, 1) + kernel_inputs = MMKernelInputs( + input_nodes, mat1_idx=0, mat2_idx=1, out_dtype=out_dtype + ) + + choices: list[ChoiceCaller] = [] + + # Collect all templates for unified call + templates_to_use: list[ExternKernelChoice | KernelTemplate] = [] + kwarg_overrides = {} + + if use_aten_gemm_kernels(): + templates_to_use.append(aten__fp8_mm) + kwarg_overrides[aten__fp8_mm.uid] = dict( + out_dtype=out_dtype, use_fast_accum=use_fast_accum + ) + + _, is_nonzero = _is_static_problem(layout) + + if ( + # We dont have triton lowerings for the MX variants yet + scale_a.dtype == torch.float32 + and is_nonzero + and use_triton_template(layout, enable_float8=True, check_max_autotune=False) + ): + overriders = dict(USE_FAST_ACCUM=use_fast_accum) + + scale_a_size, scale_b_size = scale_a_real.shape, scale_b_real.shape + + scale_option_a, scale_option_b = get_scaling_options( + mat_a, mat_b, scale_a_size, scale_b_size + ) + + # TODO (paulzhan): There is no template that exists for bias and TMA + # Don't run tma template currently if bias exist + if ( + use_triton_tma_template(mat_a, mat_b, output_layout=layout, add_guards=True) + and not bias + ): + overriders["SCALE_RECIPE_A"] = scale_option_a.value + overriders["SCALE_RECIPE_B"] = scale_option_b.value + + if use_triton_scaling_template( + scale_option_a, scale_option_b, epilogue_scaling_types + ): + templates_to_use.append(scaled_mm_device_tma_epilogue_scaling_template) + kwarg_overrides[scaled_mm_device_tma_epilogue_scaling_template.uid] = ( + overriders + ) + elif use_triton_scaling_template( + scale_option_a, scale_option_b, main_loop_scaling_types + ): + overriders["TILE_SIZE_A"] = get_tile_size(scale_option_a) + overriders["TILE_SIZE_B"] = get_tile_size(scale_option_b) + + templates_to_use.append(scaled_mm_device_tma_main_loop_scaling_template) + kwarg_overrides[scaled_mm_device_tma_main_loop_scaling_template.uid] = ( + overriders + ) + else: + raise AssertionError( + "Inductor Triton does not support scaling options that are present " + + "in both epilogue scaling and main loop scaling" + ) + + if ( + use_triton_blackwell_tma_template( + mat_a, mat_b, output_layout=layout, add_guards=True + ) + and not bias + ): + templates_to_use.append(blackwell_ws_persistent_device_tma_mm_template) + kwarg_overrides[blackwell_ws_persistent_device_tma_mm_template.uid] = ( + overriders + ) + + if use_triton_scaling_template( + scale_option_a, scale_option_b, epilogue_scaling_types + ): + templates_to_use.append(mm_template) + kwarg_overrides[mm_template.uid] = overriders + + # Single unified call for all templates + choices.extend( + V.choices.get_template_configs( + kernel_inputs, + templates_to_use, + name, + kwarg_overrides=kwarg_overrides, + ) + ) + + # NVGEMM get_kernels() will return empty if the scaling mode/dtype is unsupported + if is_nonzero and use_nv_universal_gemm_template(layout, m, n, k, mat_a, mat_b): + from ..codegen.nv_universal_gemm import add_nv_universal_scaled_gemm_choices + + add_nv_universal_scaled_gemm_choices( + choices, + layout, + input_nodes, + kernel_inputs=kernel_inputs, + ) + + # Early return for MX variants + if scale_a.dtype != torch.float32: + node, _ = autotune_select_algorithm(name, choices, input_nodes, layout) + return node + + if ( + is_nonzero + and use_cutlass_template(layout, m, n, k) + and _use_cutlass_for_op(name) + ): + CUTLASS3xGemmTemplate.add_cutlass_gemm_choices( + choices, + layout, + kernel_inputs.nodes(), # type: ignore[arg-type] + use_fast_accum=use_fast_accum, # type: ignore[arg-type] + ) + + if is_nonzero and use_ck_gemm_template(layout, m, n, k): + CKGemmTemplate.add_ck_gemm_choices(choices, layout, kernel_inputs.nodes()) + + node, _ = autotune_select_algorithm(name, choices, kernel_inputs.nodes(), layout) + return node + + +@functools.cache +def _is_sm7x_or_older_gpu(index: int | None) -> bool: + props = torch.cuda.get_device_properties(index or 0) + return props.major <= 7 + + +def dims_are_int(dims): + return all(isinstance(dim, int) for dim in dims) + + +def mm_autoheuristic( + mat1, + mat2, + m, + n, + k, + choices, + name, + input_nodes, + ops, + precondition, + top_k: int | None = None, + always_included=None, +): + m, n, k = get_size_hints(mat1, mat2, m, n, k) + if not dims_are_int([m, n, k]): + return None + mat1_stride, mat2_stride = get_size_hints_strides(mat1, mat2) + + def get_context(m, k, n, mat1, mat2, mat1_stride, mat2_stride): + context = AHContext() + context.add_feature("m", m) + context.add_feature("k", k) + context.add_feature("n", n) + context.add_feature("mat1_dtype", mat1.layout.dtype, is_categorical=True) + context.add_feature("mat2_dtype", mat2.layout.dtype, is_categorical=True) + context_add_strides(context, "mat1", mat1_stride) + context_add_strides(context, "mat2", mat2_stride) + context.add_feature( + "mat1_iscontig", mat1.layout.is_contiguous(), is_categorical=True + ) + context.add_feature( + "mat2_iscontig", mat2.layout.is_contiguous(), is_categorical=True + ) + if name == "mm": + context_add_using_tf32(context, mat1.layout.dtype) + return context + + def fallback(): + return None + + context = get_context(m, k, n, mat1, mat2, mat1_stride, mat2_stride) + autoheuristic = AutoHeuristicSelectAlgorithm( + fallback=fallback, + choices=choices, + input_nodes=input_nodes, + context=context, + name=name, + augment_context=ops, + precondition=precondition, + ) + + if top_k is not None: + # TODO: is there a cleaner way to ensure aten.mm is always included? + return autoheuristic.get_top_k_choices_caller( + top_k, always_included=always_included + ) + + return autoheuristic.get_choice_caller() + + +def get_size_hints(mat1, mat2, m, n, k): + if not isinstance(m, int) or not isinstance(k, int): + (m, k) = V.graph.sizevars.optimization_hints(mat1.get_size()) + + if not isinstance(n, int) or not isinstance(k, int): + (k, n) = V.graph.sizevars.optimization_hints(mat2.get_size()) + return m, n, k + + +def get_size_hints_strides(mat1, mat2): + mat1_stride = mat1.layout.stride + mat2_stride = mat2.layout.stride + strides = [mat1_stride, mat2_stride] + strides_hints = [] + for stride in strides: + if not isinstance(stride, int): + stride = V.graph.sizevars.optimization_hints(stride) + strides_hints.append(stride) + return strides_hints[0], strides_hints[1] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/mm_common.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/mm_common.py new file mode 100644 index 0000000000000000000000000000000000000000..37c33833a1b65164e0427076b40156c666a3bdc7 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/mm_common.py @@ -0,0 +1,266 @@ +# mypy: allow-untyped-defs +import logging +from collections.abc import Sequence +from functools import partial +from pathlib import Path +from typing import Any + +import torch +from torch._inductor.select_algorithm import realize_inputs, SymbolicGridFn +from torch._inductor.utils import get_current_backend, sympy_product +from torch._inductor.virtualized import V +from torch.fx.experimental.symbolic_shapes import has_free_unbacked_symbols + +from .. import config +from ..codegen.wrapper import PythonWrapperCodegen +from ..ir import _IntLike, Layout, TensorBox +from ..utils import load_template + + +log = logging.getLogger(__name__) + + +@SymbolicGridFn +def mm_grid(m, n, meta, *, cdiv): + """ + The CUDA grid size for matmul triton templates. + """ + return (cdiv(m, meta["BLOCK_M"]) * cdiv(n, meta["BLOCK_N"]), 1, 1) + + +@SymbolicGridFn +def persistent_mm_grid(M: int, N: int, meta: dict[str, Any], *, cdiv, min): + """Defines the grid for persistent kernels.""" + return ( + min(meta["NUM_SMS"], cdiv(M, meta["BLOCK_M"]) * cdiv(N, meta["BLOCK_N"])), + 1, + 1, + ) + + +@SymbolicGridFn +def persistent_grouped_mm_grid(*args): + meta = args[-1] + return (meta["NUM_SMS"], 1, 1) + + +def acc_type(dtype): + if dtype in (torch.float16, torch.bfloat16): + return "tl.float32" + return f"tl.{dtype}".replace("torch.", "") + + +def mm_args( + mat1, + mat2, + *others, + layout=None, + out_dtype=None, + use_4x2_dim=False, + mat2_transposed=False, +): + """ + Common arg processing for mm,bmm,addmm,etc + """ + mat1, mat2 = realize_inputs(mat1, mat2) + *b1, m, k1 = mat1.get_size() + if mat2_transposed: + *b2, n, k2 = mat2.get_size() + else: + *b2, k2, n = mat2.get_size() + b = [V.graph.sizevars.check_equals_and_simplify(a, b) for a, b in zip(b1, b2)] + if use_4x2_dim: + k2 = k2 * 2 + k = V.graph.sizevars.check_equals_and_simplify(k1, k2) + if layout is None: + from torch._inductor.ir import FixedLayout + + if out_dtype is None: + out_dtype = mat1.get_dtype() + + layout = FixedLayout( + mat1.get_device(), + out_dtype, + [*b, m, n], + ) + else: + assert out_dtype is None, "out_dtype is ignored if layout is specified." + from ..lowering import expand + + others = [realize_inputs(expand(x, layout.size)) for x in others] + + return [m, n, k, layout, mat1, mat2, *others] + + +def addmm_epilogue(dtype, alpha, beta): + def epilogue(acc, bias): + if alpha != 1: + acc = V.ops.mul(acc, V.ops.constant(alpha, dtype)) + if beta != 1: + bias = V.ops.mul(bias, V.ops.constant(beta, dtype)) + return V.ops.add(acc, bias) + + return epilogue + + +def scale_mm_epilogue(): + """ + Create an epilogue function that applies scaling to matrix multiplication result + using the given scale factors. + + Args: + dtype: The data type of the output + scale_a: Scale factor for matrix A + scale_b: Scale factor for matrix B + + Returns: + Epilogue function that takes the accumulator and applies scaling + """ + + def epilogue(acc, inv_a_scale, inv_b_scale, bias=None): + # The epilogue function receives the accumulator (result of mat1 @ mat2) + # and applies the scaling factors + # In the original scaled_mm, we use inverse scales, so we multiply by them + mul_scales = V.ops.mul(inv_a_scale, inv_b_scale) + mul_acc = V.ops.mul(acc, mul_scales) + if bias is not None: + return V.ops.add(mul_acc, bias) + else: + return mul_acc + + return epilogue + + +def use_native_matmul(mat1, mat2): + if not config.triton.native_matmul: + return False + + # If tma matmul is on, don't do native matmul + if ( + config.triton.enable_persistent_tma_matmul + and torch.utils._triton.has_triton_tma_device() + ): + raise AssertionError("native matmul doesn't support tma codegen yet") + + # Currently only enable native matmul for default indexing + # TODO : support block ptr + if config.triton.use_block_ptr: + raise AssertionError("native matmul doesn't support block_ptr codegen yet") + + # Currently only enable native matmul for triton on GPU. + device_type = mat1.get_device().type + if not ( + device_type in ("cuda", "xpu") and get_current_backend(device_type) == "triton" + ): + return False + + # Currently, tl.dot only supports following dtypes + triton_supported_dtype = [ + torch.int8, + torch.uint8, + torch.float16, + torch.bfloat16, + torch.float32, + ] + if mat1.dtype not in triton_supported_dtype: + return False + if mat2.dtype not in triton_supported_dtype: + return False + + # (..., M, K) @ (..., K, N) + m, k, n = mat1.get_size()[-2], mat1.get_size()[-1], mat2.get_size()[-1] + + # If the shape has unbacked symbols, don't do native matmul. + # This is related to the behavior of statically_known_multiple_of on unbacked symints. + # Since statically_known_multiple_of just returns False for unbacked symbols + # due to the expensive cost, codegen fails when there is a unbacked symbol. + # In particular, it fails at _split_iteration_ranges in codegen/simd.py. + # See this : https://github.com/pytorch/pytorch/pull/131649 + if any(map(has_free_unbacked_symbols, [m, k, n])): + return False + + # Consider the shape (m,k,n) > 1 + # TODO : support when size = 1 + if ( + V.graph.sizevars.statically_known_leq(m, 1) + or V.graph.sizevars.statically_known_leq(k, 1) + or V.graph.sizevars.statically_known_leq(n, 1) + ): + return False + + return True + + +def _is_static_problem(layout: Layout) -> tuple[bool, bool]: + """ + Check if input tensors and output layout have static shapes and non-zero sizes. + + Args: + layout: Output layout object with a 'size' attribute. + + Returns: + Tuple[bool, bool]: (is_static, is_nonzero) + is_static: True if all shapes are statically known + is_nonzero: True if all dimensions are non-zero + """ + static_shape = True + static_size = PythonWrapperCodegen.statically_known_list_of_ints_or_none( + layout.size + ) + if static_size is None: + nonzero = True + for s in layout.size: + sz = PythonWrapperCodegen.statically_known_int_or_none(s) + if sz is not None and sz == 0: + nonzero = False + break + return False, nonzero + numel = 1 + for dim in static_size: + numel *= dim + nonzero = numel > 0 + return static_shape, nonzero + + +def check_supported_striding(mat_a: TensorBox, mat_b: TensorBox) -> None: + def is_row_major(stride: Sequence[_IntLike]) -> bool: + return stride[-1] == 1 + + def is_col_major(stride: Sequence[_IntLike]) -> bool: + return stride[-2] == 1 + + def has_zero_dim(size: Sequence[_IntLike]) -> bool: + return bool(size[0] == 0 or size[1] == 0) + + # Check mat_a (self) stride requirements + torch._check( + is_row_major(mat_a.get_stride()) or has_zero_dim(mat_a.get_size()), + lambda: f"mat_a must be row_major, got stride {mat_a.get_stride()}", + ) + + # Check mat_b stride requirements + torch._check( + is_col_major(mat_b.get_stride()) or has_zero_dim(mat_b.get_size()), + lambda: f"mat_b must be col_major, got stride {mat_b.get_stride()}", + ) + + +def is_batch_stride_largest_or_zero(mat1, mat2, layout) -> bool: + """ + Checking if the batch stride is the largest in the stride. + """ + sizes = [mat1.get_size(), mat2.get_size(), layout.size] + strides = [mat1.get_stride(), mat2.get_stride(), layout.stride] + for size, stride in zip(sizes, strides): + assert len(size) == len(stride) == 3, "Expect 3D tensors" + if stride[0] != 0 and stride[0] != sympy_product(size[1:]): + return False + + return True + + +_KERNEL_TEMPLATE_DIR = Path(__file__).parent / "templates" +load_kernel_template = partial(load_template, template_dir=_KERNEL_TEMPLATE_DIR) + +_KERNEL_TEMPLATE_FB_DIR = Path(__file__).parent.parent / "fb" / "tlx_templates" +load_fb_kernel_template = partial(load_template, template_dir=_KERNEL_TEMPLATE_FB_DIR) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/mm_grouped.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/mm_grouped.py new file mode 100644 index 0000000000000000000000000000000000000000..3b35127745ea3385a720c1aaa3a6e26cac6eea88 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/mm_grouped.py @@ -0,0 +1,519 @@ +# mypy: allow-untyped-defs +import logging +from dataclasses import asdict, dataclass +from typing import Any + +import torch +from torch._dynamo.utils import counters +from torch._inductor.codegen.cutedsl.cutedsl_template import CuteDSLTemplate +from torch._inductor.runtime.triton_compat import tl +from torch._inductor.template_heuristics.cutedsl import get_groupgemm_configs +from torch._inductor.virtualized import V +from torch.utils._triton import has_triton + +from ..ir import ChoiceCaller, Layout, TensorBox +from ..lowering import register_lowering +from ..select_algorithm import ( + autotune_select_algorithm, + ExternKernelChoice, + realize_inputs, + TritonTemplate, +) +from ..utils import ( + get_gpu_shared_memory, + get_num_sms, + has_free_symbols, + use_aten_gemm_kernels, + use_blackwell_cutedsl_grouped_mm, + use_nv_universal_gemm_template, + use_triton_template, +) +from .mm_common import ( + _is_static_problem, + check_supported_striding, + load_kernel_template, + persistent_grouped_mm_grid, +) + + +log = logging.getLogger(__name__) +aten = torch.ops.aten + + +@dataclass +class Config: + kwargs: dict[str, int] + num_stages: int + num_warps: int + + +_NV_CONFIGS = [ + Config( + { + "BLOCK_M": block_size_m, + "BLOCK_N": block_size_n, + "BLOCK_K": block_size_k, + "NUM_CONSUMER_GROUPS": 1, + }, + num_stages=num_stages, + num_warps=num_warps, + ) + for block_size_m in [16, 32, 64, 128] + for block_size_n in [64, 128, 256] + for block_size_k in [64, 128, 256] + for num_stages in [3, 4] + for num_warps in [4, 8] +] + + +def grouped_mm_configs(): + return _NV_CONFIGS + + +def early_config_prune(g, m, dtsize, configs, named_args): + pruned_configs = [] + for config in configs: + kw = config.kwargs + BLOCK_M, BLOCK_N, BLOCK_K, num_stages, num_warps, num_consumer_groups = ( + kw["BLOCK_M"], + kw["BLOCK_N"], + kw["BLOCK_K"], + config.num_stages, + config.num_warps, + getattr(config, "num_consumer_groups", 0), + ) + + # 1. Prune NV configs depending on g and m. + if not has_free_symbols((g, m)): + a_is_2d, b_is_2d = named_args["A_IS_2D"], named_args["B_IS_2D"] + m_avg = m // g if a_is_2d and not b_is_2d else m + if m_avg <= 16: + if BLOCK_M > 32: + continue + elif m_avg <= 32: + if BLOCK_M > 64: + continue + elif m_avg <= 64: + if BLOCK_M <= 16: + continue + else: + if BLOCK_M <= 32: + continue + + # 2. make sure we have enough smem + max_shared_memory = get_gpu_shared_memory() + + required_shared_memory = (BLOCK_M + BLOCK_N) * BLOCK_K * num_stages * dtsize + if required_shared_memory > max_shared_memory: + continue + + use_warp_specialization = num_consumer_groups >= 1 + + # 3. make sure we can partition for ws + if use_warp_specialization: + if num_warps != 4: + continue + + # "tritongpu-warp-spec-data-partition" + m_slice = BLOCK_M // num_consumer_groups + n_slice = BLOCK_N // num_consumer_groups + if m_slice < 64 and n_slice < 256: + continue + + pruned_configs.append(config) + + return pruned_configs + + +triton_grouped_mm_template = TritonTemplate( + name="grouped_mm", + grid=persistent_grouped_mm_grid, + source=load_kernel_template("triton_mm_grouped"), +) + +triton_scaled_grouped_mm_template = TritonTemplate( + name="scaled_grouped_mm", + grid=persistent_grouped_mm_grid, + source=load_kernel_template("triton_mm_grouped"), +) + +cutedsl_grouped_mm_template = CuteDSLTemplate( + name="grouped_gemm_cutedsl", + source=load_kernel_template("cutedsl_mm_grouped"), +) + + +def grouped_mm_args( + mat1: TensorBox, + mat2: TensorBox, + offs: TensorBox | None, + layout=None, + out_dtype=None, +): + mat1, mat2 = realize_inputs(mat1, mat2) + if offs is not None: + realize_inputs(offs) + mat1_size = mat1.get_size() + mat2_size = mat2.get_size() + + m1dim, m2dim = len(mat1_size), len(mat2_size) + + assert m1dim == 2 or m1dim == 3 + assert m2dim == 2 or m2dim == 3 + + if layout is None: + from torch._inductor.ir import FixedLayout + + if out_dtype is None: + out_dtype = mat1.get_dtype() + alignment = 16 // out_dtype.itemsize + + if m1dim == 2: + if m2dim == 2: + assert offs is not None + out_size = [offs.get_size()[0], mat1_size[0], mat2_size[1]] + else: + out_size = [mat1_size[0], mat2_size[-1]] + else: + if m2dim == 2: + out_size = [mat1_size[1], mat2_size[1]] + else: + out_size = [mat1_size[0], mat1_size[1], mat2_size[-1]] + size_padded = (out_size[-1] + alignment - 1) // alignment * alignment + if len(out_size) == 2: + out_stride = [size_padded, 1] + else: + out_stride = [out_size[1] * size_padded, size_padded, 1] + + layout = FixedLayout( + mat1.get_device(), + out_dtype, + out_size, + out_stride, + ) + else: + assert out_dtype is None, "out_dtype is ignored if layout is specified." + + return (mat1_size, mat2_size, layout, mat1, mat2, offs) + + +aten__grouped_mm = ExternKernelChoice( + torch._grouped_mm, + "at::_grouped_mm", + op_overload=aten._grouped_mm.default, + has_out_variant=False, +) + + +aten__scaled_grouped_mm = ExternKernelChoice( + torch._scaled_grouped_mm, + "at::_scaled_grouped_mm", + op_overload=aten._scaled_grouped_mm.default, + has_out_variant=False, +) + + +def can_use_triton_kernel( + mat_a: TensorBox, + mat_b: TensorBox, + offs: TensorBox | None, + bias: TensorBox | None, + scale_result: TensorBox | None, +) -> bool: + if not ( + torch.cuda.is_available() + and torch.cuda.get_device_capability() >= (9, 0) + and not torch.version.hip + ): + return False + if not has_triton(): + return False + + # The _grouped_mm()/_scaled_grouped_mm() operator do not support + # bias nor scale_result yet. + if bias is not None: + return False + if scale_result is not None: + return False + + if len(mat_a.get_size()) == 2 or len(mat_b.get_size()) == 2: + return offs is not None + else: + return offs is None + + +def create_offsets(offs_box, m1_is_2d, m2_is_2d, m, n, k, alignment): + if m1_is_2d: + if m2_is_2d: + end = k + else: + end = m + else: + if m2_is_2d: + end = n + else: + return None + + end_hint = V.graph.sizevars.optimization_hint(end) + noffs_hint = V.graph.sizevars.optimization_hint(offs_box.get_size()[0]) + offs = torch.arange(1, noffs_hint + 1, dtype=torch.float32) * ( + end_hint / noffs_hint + ) + offs[:-1] = (offs[:-1] / alignment).round() * alignment + offs[-1] = end_hint + return offs.to(dtype=offs_box.get_dtype(), device=offs_box.get_device()) + + +def _tuned_grouped_mm_common( + operator_name: str, + algorithm_name: str, + extern_kernel_choice: ExternKernelChoice, + kernel_template: TritonTemplate, + mat_a: TensorBox, + mat_b: TensorBox, + scale_a: TensorBox | None = None, + scale_b: TensorBox | None = None, + offs: TensorBox | None = None, + bias: TensorBox | None = None, + scale_result: TensorBox | None = None, + out_dtype: torch.dtype | None = None, + use_fast_accum: bool | None = None, + layout: Layout | None = None, +) -> TensorBox: + assert (scale_a is None) == (scale_b is None) + assert scale_result is None or scale_a is not None + + m1_size, m2_size, layout, mat_a, mat_b, offs = grouped_mm_args( + mat_a, mat_b, offs, layout=layout, out_dtype=out_dtype + ) + counters["aten_mm_info"][operator_name] += 1 + log_message = f"Tuned {operator_name}: mat1_shape=%s, mat2_shape=%s, mat1_dtype=%s, mat2_dtype=%s, output_layout=%s" + log.info( + log_message, + m1_size, + m2_size, + mat_a.get_dtype(), + mat_b.get_dtype(), + layout, + ) + + if scale_a is not None and scale_b is not None: + check_supported_striding(mat_a, mat_b) + + # workaround for Inductor not supporting optional tensor input arguments + input_nodes: list[Any] = [mat_a, mat_b] + if scale_a is not None: + input_nodes.append(realize_inputs(scale_a)) + if scale_b is not None: + input_nodes.append(realize_inputs(scale_b)) + if offs is not None: + input_nodes.append(realize_inputs(offs)) + + if use_fast_accum is None: + aten_choice = extern_kernel_choice.bind( + input_nodes, + layout, + out_dtype=out_dtype, + ) + else: + aten_choice = extern_kernel_choice.bind( + input_nodes, + layout, + out_dtype=out_dtype, + use_fast_accum=use_fast_accum, + ) + if use_fast_accum is None: + use_fast_accum = False + + choices: list[ChoiceCaller] = [] + if use_aten_gemm_kernels(): + choices.append(aten_choice) + + _, is_nonzero = _is_static_problem(layout) + + # Checking only for the equality of corresponding dims of + # multiplicands here, relying on meta function checks for + # everything else. + if len(m1_size) == 2: + if len(m2_size) == 2: + m, k1 = m1_size + k2, n = m2_size + + g = offs.get_size()[0] + k = V.graph.sizevars.check_equals(k1, k2) + a_is_2d, b_is_2d = True, True + else: + g1 = offs.layout.size[0] + m, k1 = m1_size + g2, k2, n = m2_size + g = V.graph.sizevars.check_equals_and_simplify(g1, g2) + k = V.graph.sizevars.check_equals(k1, k2) + a_is_2d, b_is_2d = True, False + else: + if len(m2_size) == 2: + g1 = offs.layout.size[0] + g2, m, k1 = m1_size + k2, n = m2_size + g = V.graph.sizevars.check_equals_and_simplify(g1, g2) + k = V.graph.sizevars.check_equals(k1, k2) + a_is_2d, b_is_2d = False, True + else: + g1, m, k1 = m1_size + g2, k2, n = m2_size + g = V.graph.sizevars.check_equals_and_simplify(g1, g2) + k = V.graph.sizevars.check_equals(k1, k2) + a_is_2d, b_is_2d = False, False + + if ( + is_nonzero + and use_triton_template(layout) + and can_use_triton_kernel(mat_a, mat_b, offs, bias, scale_result) + ): + scaled = scale_a is not None + + a_is_k_major = mat_a.get_stride()[-1] == 1 + b_is_k_major = mat_b.get_stride()[-2] == 1 + + triton_has_make_tensor_descriptor = hasattr(tl, "make_tensor_descriptor") + triton_has_experimental_make_tensor_descriptor = hasattr( + tl, "_experimental_make_tensor_descriptor" + ) + use_tma_load = ( + triton_has_make_tensor_descriptor + or triton_has_experimental_make_tensor_descriptor + ) + kwargs = { + "SCALED": scaled, + "A_IS_2D": a_is_2d, + "B_IS_2D": b_is_2d, + "A_IS_K_MAJOR": a_is_k_major, + "B_IS_K_MAJOR": b_is_k_major, + "USE_FAST_ACCUM": use_fast_accum, + "NUM_SMS": get_num_sms(), + "USE_TMA_LOAD": use_tma_load, + "USE_EXPERIMENTAL_MAKE_TENSOR_DESCRIPTOR": triton_has_experimental_make_tensor_descriptor, + } + + for config in early_config_prune( + g, m, mat_a.dtype.itemsize, grouped_mm_configs(), kwargs + ): + kernel_template.maybe_append_choice( + choices, + input_nodes=input_nodes, + layout=layout, + num_stages=config.num_stages, + num_warps=config.num_warps, + **kwargs, + **config.kwargs, + ) + + if use_blackwell_cutedsl_grouped_mm( + mat_a, mat_b, layout, a_is_2d, b_is_2d, offs, bias, scale_result + ): + for config in get_groupgemm_configs(): + kwargs = dict( + ACC_DTYPE="cutlass.Float32", + ) + + cutedsl_grouped_mm_template.maybe_append_choice( + choices, + input_nodes=input_nodes, + layout=layout, + **kwargs, + **asdict(config), + ) + + if ( + is_nonzero + and a_is_2d + and not b_is_2d + and offs is not None + and use_nv_universal_gemm_template(layout, m, n, k, mat_a, mat_b, offs, g) + ): + from torch._inductor.codegen.nv_universal_gemm.nv_universal_gemm import ( + add_nv_universal_grouped_gemm_choices, + ) + + add_nv_universal_grouped_gemm_choices( + choices, + layout, + input_nodes, + accumulator_type=torch.float32, + ) + + input_gen_fns = {} + if offs is not None: + input_offs_idx = 2 if scale_a is None else 4 + alignment = 16 // mat_a.dtype.itemsize + input_gen_fns[input_offs_idx] = lambda x: create_offsets( + x, a_is_2d, b_is_2d, m, n, k, alignment + ) + node, _ = autotune_select_algorithm( + algorithm_name, choices, input_nodes, layout, input_gen_fns=input_gen_fns + ) + return node + + +@register_lowering(aten._grouped_mm.default, type_promotion_kind=None) +def tuned_grouped_mm( + mat_a: TensorBox, + mat_b: TensorBox, + offs: TensorBox | None = None, + bias: TensorBox | None = None, + out_dtype: torch.dtype | None = None, + layout: Layout | None = None, +) -> TensorBox: + """Auto-tuning for _grouped_mm() operator.""" + + return _tuned_grouped_mm_common( + "aten._grouped_mm.default", + "grouped_mm", + aten__grouped_mm, + triton_grouped_mm_template, + mat_a, + mat_b, + None, + None, + offs, + bias, + None, + out_dtype, + None, + layout, + ) + + +@register_lowering(aten._scaled_grouped_mm.default, type_promotion_kind=None) +def tuned_scaled_grouped_mm( + mat_a: TensorBox, + mat_b: TensorBox, + scale_a: TensorBox, + scale_b: TensorBox, + offs: TensorBox | None = None, + bias: TensorBox | None = None, + scale_result: TensorBox | None = None, + out_dtype: torch.dtype | None = None, + use_fast_accum: bool = False, + layout: Layout | None = None, +) -> TensorBox: + """Auto-tuning for _scaled_grouped_mm() operator.""" + + # matching _scaled_grouped_mm_cuda Blas.cpp implementation + out_dtype = out_dtype or torch.bfloat16 + + return _tuned_grouped_mm_common( + "aten._scaled_grouped_mm.default", + "scaled_grouped_mm", + aten__scaled_grouped_mm, + triton_scaled_grouped_mm_template, + mat_a, + mat_b, + scale_a, + scale_b, + offs, + bias, + scale_result, + out_dtype, + use_fast_accum, + layout, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/mm_plus_mm.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/mm_plus_mm.py new file mode 100644 index 0000000000000000000000000000000000000000..4bbd909d4702ffa3722161120c64349db15a5d88 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/mm_plus_mm.py @@ -0,0 +1,178 @@ +# mypy: allow-untyped-defs + +import logging +from typing import TYPE_CHECKING + +import torch + +from .. import config as inductor_config +from ..kernel_inputs import MMKernelInputs +from ..lowering import lowerings +from ..select_algorithm import ( + autotune_select_algorithm, + ExternKernelChoice, + TritonTemplate, +) +from ..utils import use_aten_gemm_kernels, use_triton_template +from ..virtualized import V +from .mm_common import mm_args, mm_grid + + +if TYPE_CHECKING: + from torch._inductor.ir import ChoiceCaller + from torch._inductor.select_algorithm import KernelTemplate + +log = logging.getLogger(__name__) + +aten = torch.ops.aten + +aten_mm_plus_mm = ExternKernelChoice( + torch.ops.inductor._mm_plus_mm, "torch::inductor::_mm_plus_mm" +) + +mm_plus_mm_template = TritonTemplate( + name="mm_plus_mm", + grid=mm_grid, + debug=False, + source=r""" +{{def_kernel("A", "B", "C", "D")}} + M = {{size("A", 0)}} + N = {{size("B", 1)}} + K1 = {{size("A", 1)}} + if M * N == 0: + # early exit due to zero-size input(s) + return + # K2 = {{size("C", 1)}} + stride_am = {{stride("A", 0)}} + stride_ak = {{stride("A", 1)}} + stride_bk = {{stride("B", 0)}} + stride_bn = {{stride("B", 1)}} + stride_cm = {{stride("C", 0)}} + stride_ck = {{stride("C", 1)}} + stride_dk = {{stride("D", 0)}} + stride_dn = {{stride("D", 1)}} + + # based on triton.ops.matmul + pid = tl.program_id(0).to(INDEX_DTYPE) + grid_m = (M + BLOCK_M - 1) // BLOCK_M + grid_n = (N + BLOCK_N - 1) // BLOCK_N + + # re-order program ID for better L2 performance + width = GROUP_M * grid_n + group_id = pid // width + group_size = min(grid_m - group_id * GROUP_M, GROUP_M) + pid_m = group_id * GROUP_M + (pid % group_size) + pid_n = (pid % width) // (group_size) + tl.assume(pid_m >= 0) + tl.assume(pid_n >= 0) + + rm = pid_m * BLOCK_M + tl.arange(0, BLOCK_M) + rn = pid_n * BLOCK_N + tl.arange(0, BLOCK_N) + + if (((stride_am == 1 and stride_ak == M) or (stride_am == K1 and stride_ak == 1)) + and ((stride_cm == 1 and stride_ck == M) or (stride_cm == K1 and stride_ck == 1))): + ram = tl.max_contiguous(tl.multiple_of(rm % M, BLOCK_M), BLOCK_M) + else: + ram = rm % M + + if (((stride_bk == 1 and stride_bn == K1) or (stride_bk == N and stride_bn == 1)) + and ((stride_dk == 1 and stride_dn == K1) or (stride_dk == N and stride_dn == 1))): + rbn = tl.max_contiguous(tl.multiple_of(rn % N, BLOCK_N), BLOCK_N) + else: + rbn = rn % N + + rk = tl.arange(0, BLOCK_K) + A = A + (ram[:, None] * stride_am + rk[None, :] * stride_ak) + B = B + (rk[:, None] * stride_bk + rbn[None, :] * stride_bn) + C = C + (ram[:, None] * stride_cm + rk[None, :] * stride_ck) + D = D + (rk[:, None] * stride_dk + rbn[None, :] * stride_dn) + + acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=ACC_TYPE) + for k1 in range(K1, 0, -BLOCK_K): + # First matmul with A @ B + if EVEN_K: + a = tl.load(A) + b = tl.load(B) + else: + a = tl.load(A, mask=rk[None, :] < k1, other=0.) + b = tl.load(B, mask=rk[:, None] < k1, other=0.) + acc += tl.dot(a, b, allow_tf32=ALLOW_TF32) + A += BLOCK_K * stride_ak + B += BLOCK_K * stride_bk + + for k2 in range(K1, 0, -BLOCK_K): + + # Second matmul with C @ D + if EVEN_K: + c = tl.load(C) + d = tl.load(D) + else: + c = tl.load(C, mask=rk[None, :] < k2, other=0.) + d = tl.load(D, mask=rk[:, None] < k2, other=0.) + acc += tl.dot(c, d, allow_tf32=ALLOW_TF32) + C += BLOCK_K * stride_ck + D += BLOCK_K * stride_dk + + + idx_m = rm[:, None] + idx_n = rn[None, :] + mask = (idx_m < M) & (idx_n < N) + + # inductor generates a suffix + {{store_output(("idx_m", "idx_n"), "acc", "mask", val_shape=("BLOCK_M", "BLOCK_N"))}} +""", + cache_codegen_enabled_for_template=True, +) + + +def tuned_mm_plus_mm(mat1, mat2, mat3, mat4, *, layout=None): + """ + Computes mm(mat1, mat2) + mm(mat3, mat4) + """ + # TODO(coconutruben): integrate into MMKernelInputs when all callsites use that + m1, n1, k1, layout1, mat1, mat2 = mm_args(mat1, mat2, layout=layout) + m2, n2, _, layout2, mat3, mat4 = mm_args(mat3, mat4, layout=layout) + + # Optimization is optional, because we can always just not do the fusion + if ( + m1 * n1 == 0 + or m2 * n2 == 0 + or not V.graph.sizevars.statically_known_list_equals( + mat1.get_size(), mat3.get_size() + ) + or not V.graph.sizevars.statically_known_list_equals( + mat2.get_size(), mat4.get_size() + ) + or inductor_config.triton.native_matmul + ): + # TODO(jansel): support different K values when this is fixed: + # https://github.com/triton-lang/triton/issues/967 + return lowerings[aten.add]( + lowerings[aten.mm](mat1, mat2), lowerings[aten.mm](mat3, mat4) + ) + + # Create MMKernelInputs for MM Plus MM (matrices are at indices 0, 1 for first pair) + # Note: This is a special case with 4 matrices, but we use the first pair for M, N, K extraction + kernel_inputs = MMKernelInputs([mat1, mat2, mat3, mat4], mat1_idx=0, mat2_idx=1) + + assert layout1 == layout2 + # options to tune from + choices: list[ChoiceCaller] = [] + + # Collect all templates for unified call + templates_to_use: list[ExternKernelChoice | KernelTemplate] = [] + if use_aten_gemm_kernels(): + templates_to_use.append(aten_mm_plus_mm) + + if use_triton_template(layout1, check_max_autotune=False): + templates_to_use.append(mm_plus_mm_template) + + # Single unified call for all templates + choices.extend( + V.choices.get_template_configs(kernel_inputs, templates_to_use, "mm_plus_mm") + ) + + node, _ = autotune_select_algorithm( + "mm_plus_mm", choices, kernel_inputs.nodes(), layout1 + ) + return node diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/cutedsl_mm_grouped.py.jinja b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/cutedsl_mm_grouped.py.jinja new file mode 100644 index 0000000000000000000000000000000000000000..02285013f165dbc8355b9c68d573cb37743efa49 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/cutedsl_mm_grouped.py.jinja @@ -0,0 +1,333 @@ +import functools +from torch._inductor.runtime.runtime_utils import ceildiv +from cutlass.utils import TensorMapUpdateMode +{{gen_defines()}} +# ---- Import GroupedGemm implementation, copied on PyTorch build from Cutlass repository: cutlass/examples/python/CuTeDSL/blackwell/grouped_gemm.py ---- +from torch._inductor.kernel.vendored_templates.cutedsl.kernels.cutedsl_grouped_gemm import ( + GroupedGemmKernel, +) + + +# Note about caching: +# Each instantiated CuTeDSL grouped GEMM kernel file generated by Inductor +# maintains its own local caching system. At this stage, all compile-time +# constexprs (e.g., TILE_M, TILE_N, CLUSTER_M/N, USE_2_CTA) and the kernel +# name itself ({{kernel_name}}) are permanently baked into the file, so they +# do not need to be included in any cache key. +# +# The caching mechanism is split into two levels: +# +# 1. prep_cache +# Caches the compiled executor for build_group_ptrs_from_bases(). This +# kernel depends only on the tensor shapes, strides, and dtypes of A/B/C, +# and can therefore be safely reused across runs with different group +# partitioning (`offs`). +# +# 2. gemm_cache +# Caches the compiled Grouped GEMM executor. Its key extends the prep +# cache key with hardware- and grid-specific parameters: +# (prep_cache_key, max_active_clusters, total_num_clusters). +# This is necessary because different `offs` tensors can change the +# per-group problem sizes and thus alter `total_num_clusters`, which in +# turn changes the grid shape and persistent scheduler configuration. +# Kernels compiled for one grid cannot be safely reused for another. +# +# +# Additionally, note the @lru_cache decorator on get_hardware_info(). Empirically, +# hw.get_max_active_clusters() triggers significant MLIR recompilation overhead, +# despite depending only on the GPU type. We cache this function to mitigate +# redundant recompiles even when shape/stride/dtype cache misses force kernel +# regeneration. A follow-up study will investigate the root cause. + +prep_cache = {} +gemm_cache = {} + + +@functools.lru_cache +def get_hardware_info(): + hw = cutlass.utils.HardwareInfo() + sm_count = hw.get_max_active_clusters(1) + max_active_clusters = hw.get_max_active_clusters(CLUSTER_M * CLUSTER_N) + + return (sm_count, max_active_clusters) + + +def get_prep_cache_key(input_a, input_b, output): + """ + Returns a tuple key for caching the preprocessing kernel executor based on kernel name, + shapes, strides, and dtypes of input/output tensors. + """ + return ( + tuple(input_a.shape), + tuple(input_a.stride()), + input_a.dtype, + tuple(input_b.shape), + tuple(input_b.stride()), + input_b.dtype, + tuple(output.shape), + tuple(output.stride()), + output.dtype, + ) + + +def get_gemm_cache_key(prep_cache_key, max_active_clusters, total_num_clusters): + """ + Returns a tuple key for caching the gemm kernel executor by extending the + prep cache key with hardware- and grid-specific parameters. + """ + return ( + prep_cache_key, + max_active_clusters, + total_num_clusters, + ) + + +@cute.kernel +def build_group_ptrs_from_bases_kernel( + base_A_u64: cutlass.Int64, # device addr of input_a (bytes) + base_B_u64: cutlass.Int64, # device addr of input_b (bytes) + base_C_u64: cutlass.Int64, # device addr of Output (bytes) + offs: cute.Tensor, # [G], cutlass.Int32/64 cumulative + K: cutlass.Constexpr, + N: cutlass.Constexpr, + sizeof_element: cutlass.Int32, # bytes + # -------- STRIDES (in ELEMENTS) -------- + stride_A_m_elems: cutlass.Constexpr, # A.stride(0) + stride_A_k_elems: cutlass.Constexpr, # A.stride(1) + stride_B0_elems: cutlass.Constexpr, # B.stride(0) + stride_Bk_elems: cutlass.Constexpr, # B.stride(1) + stride_Bn_elems: cutlass.Constexpr, # B.stride(2) + stride_C_m_elems: cutlass.Constexpr, # C.stride(0) + stride_C_n_elems: cutlass.Constexpr, # C.stride(1) + # -------- OUTPUTS -------- + out_ptrs: cute.Tensor, # [G,3] cutlass.Int64: (A_ptr, B_ptr, C_ptr) + out_problem: cute.Tensor, # [G,4] cutlass.Int32: (m_g, n, k, 1) + out_strides_abc: cute.Tensor, # [G,3,2] cutlass.Int32 [[A_m,A_k],[B_n,B_k],[C_m,C_n]] +): + tidx, _, _ = cute.arch.thread_idx() + g = tidx + + m_beg_i32 = 0 + if g > 0: + m_beg_i32 = offs[g - 1] + m_end_i32 = offs[g] + m_g_i32 = m_end_i32 - m_beg_i32 + + a_byte_off = ( + cutlass.Int64(m_beg_i32) * stride_A_m_elems * cutlass.Int64(sizeof_element) + ) + c_byte_off = ( + cutlass.Int64(m_beg_i32) * stride_C_m_elems * cutlass.Int64(sizeof_element) + ) + b_byte_off = cutlass.Int64(g) * stride_B0_elems * cutlass.Int64(sizeof_element) + + # ---- pointers ---- + out_ptrs[g, 0] = base_A_u64 + a_byte_off + out_ptrs[g, 1] = base_B_u64 + b_byte_off + out_ptrs[g, 2] = base_C_u64 + c_byte_off + + # ---- (m, n, k, 1) ---- + out_problem[g, 0] = m_g_i32 + out_problem[g, 1] = N + out_problem[g, 2] = K + out_problem[g, 3] = cutlass.Int32(1) + + # ---- strides ---- + out_strides_abc[g, 0, 0] = cutlass.Int32(stride_A_m_elems) + out_strides_abc[g, 0, 1] = cutlass.Int32(stride_A_k_elems) + out_strides_abc[g, 1, 0] = cutlass.Int32(stride_Bn_elems) + out_strides_abc[g, 1, 1] = cutlass.Int32(stride_Bk_elems) + out_strides_abc[g, 2, 0] = cutlass.Int32(stride_C_m_elems) + out_strides_abc[g, 2, 1] = cutlass.Int32(stride_C_n_elems) + + +@cute.jit +def launch_build_group_ptrs_from_bases( + base_A_u64: cutlass.Int64, + base_B_u64: cutlass.Int64, + base_C_u64: cutlass.Int64, + offs: cute.Tensor, + G: cutlass.Constexpr, + K: cutlass.Constexpr, + N: cutlass.Constexpr, + sizeof_element: cutlass.Constexpr, + stride_A_m_elems: cutlass.Constexpr, + stride_A_k_elems: cutlass.Constexpr, + stride_B0_elems: cutlass.Constexpr, + stride_Bk_elems: cutlass.Constexpr, + stride_Bn_elems: cutlass.Constexpr, + stride_C_m_elems: cutlass.Constexpr, + stride_C_n_elems: cutlass.Constexpr, + out_ptrs: cute.Tensor, # [G,3] cutlass.Int64 + out_problem: cute.Tensor, # [G,4] cutlass.Int32 + out_strides_abc: cute.Tensor, # [3,2] cutlass.Int32 + stream: cuda.CUstream, +): + build_group_ptrs_from_bases_kernel( + base_A_u64, + base_B_u64, + base_C_u64, + offs, + K, + N, + sizeof_element, + stride_A_m_elems, + stride_A_k_elems, + stride_B0_elems, + stride_Bk_elems, + stride_Bn_elems, + stride_C_m_elems, + stride_C_n_elems, + out_ptrs, + out_problem, + out_strides_abc, + ).launch(grid=(1, 1, 1), block=(G, 1, 1), stream=stream) + + +{{def_kernel("input_a", "input_b", "input_a_offs")}} + stream = cuda.CUstream(stream) + + input_b = input_b.transpose(1, 2) + + sumM, K = input_a.shape + G, N, Kb = input_b.shape + + dev = input_a.device + + base_A_u64 = int(input_a.data_ptr()) + base_B_u64 = int(input_b.data_ptr()) + base_C_u64 = int({{get_output()}}.data_ptr()) + + ptrs_t = torch.empty((G, 3), device=dev, dtype=torch.int64) + probs_t = torch.empty((G, 4), device=dev, dtype=torch.int32) + strides_t = torch.empty((G, 3, 2), device=dev, dtype=torch.int32) + ptrs = from_dlpack(ptrs_t) + probs = from_dlpack(probs_t) + strides = from_dlpack(strides_t) + + prep_cache_key = get_prep_cache_key(input_a, input_b, {{get_output()}}) + prep_executor = prep_cache.get(prep_cache_key) + + if prep_executor is None: + sizeof_element = int(input_a.element_size()) + sA_m, sA_k = map(int, input_a.stride()) + sB_0, sB_n, sB_k = map(int, input_b.stride()) + sC_m, sC_n = map(int, {{get_output()}}.stride()) + + prep_executor = cute.compile( + launch_build_group_ptrs_from_bases, + base_A_u64=base_A_u64, + base_B_u64=base_B_u64, + base_C_u64=base_C_u64, + offs=from_dlpack(input_a_offs), + G=int(G), + K=int(K), + N=int(N), + sizeof_element=sizeof_element, + stride_A_m_elems=sA_m, + stride_A_k_elems=sA_k, + stride_B0_elems=sB_0, + stride_Bk_elems=sB_k, + stride_Bn_elems=sB_n, + stride_C_m_elems=sC_m, + stride_C_n_elems=sC_n, + out_ptrs=ptrs, + out_problem=probs, + out_strides_abc=strides, + stream=stream, + ) + + prep_cache[prep_cache_key] = prep_executor + + prep_executor( + base_A_u64=base_A_u64, + base_B_u64=base_B_u64, + base_C_u64=base_C_u64, + offs=from_dlpack(input_a_offs), + out_ptrs=ptrs, + out_problem=probs, + out_strides_abc=strides, + stream=stream, + ) + + # --- Tensormap workspace per SM --- + num_tensormap_buffers, max_active_clusters = get_hardware_info() + tensormap_shape = ( + num_tensormap_buffers, + GroupedGemmKernel.num_tensormaps, + GroupedGemmKernel.bytes_per_tensormap // 8, + ) + tensormap_workspace_t = torch.empty(tensormap_shape, device=dev, dtype=torch.int64) + tensormap_workspace = from_dlpack(tensormap_workspace_t) + + # --- Total clusters --- + def compute_total_num_clusters( + problem_sizes_mnkl, + cluster_tile_shape_mn, + ): + total_num_clusters = 0 + for m, n, _, _ in problem_sizes_mnkl: + num_clusters_mn = tuple( + ceildiv(x, y) for x, y in zip((m, n), cluster_tile_shape_mn) + ) + total_num_clusters += functools.reduce(lambda x, y: x * y, num_clusters_mn) + return total_num_clusters + + # Compute cluster tile shape + def compute_cluster_tile_shape( + mma_tiler_mn, + cluster_shape_mn, + use_2cta_instrs, + ): + cta_tile_shape_mn = list(mma_tiler_mn) + if use_2cta_instrs: + cta_tile_shape_mn[0] = cta_tile_shape_mn[0] // 2 + return tuple(x * y for x, y in zip(cta_tile_shape_mn, cluster_shape_mn)) + + cluster_tile_shape_mn = compute_cluster_tile_shape( + (TILE_M, TILE_N), (CLUSTER_M, CLUSTER_N), bool(USE_2_CTA) + ) + + total_num_clusters = int(compute_total_num_clusters(probs_t, cluster_tile_shape_mn)) + + gemm_cache_key = get_gemm_cache_key( + prep_cache_key, max_active_clusters, total_num_clusters + ) + gemm_executor = gemm_cache.get(gemm_cache_key) + + if gemm_executor is None: + grouped_gemm = GroupedGemmKernel( + acc_dtype=ACC_DTYPE, + use_2cta_instrs=USE_2_CTA, + mma_tiler_mn=(TILE_M, TILE_N), + cluster_shape_mn=(CLUSTER_M, CLUSTER_N), + tensormap_update_mode=TENSORMAP_UPDATE_MODE, + ) + + gemm_executor = cute.compile( + grouped_gemm, + from_dlpack(input_a.unsqueeze(-1), assumed_align=16), + from_dlpack(input_b[0].unsqueeze(-1), assumed_align=16), + from_dlpack({{get_output()}}.unsqueeze(-1), assumed_align=16), + G, + probs, + strides, + ptrs, + total_num_clusters, + tensormap_workspace, + max_active_clusters, + stream, + ) + + gemm_cache[gemm_cache_key] = gemm_executor + + gemm_executor( + from_dlpack(input_a.unsqueeze(-1), assumed_align=16), + from_dlpack(input_b[0].unsqueeze(-1), assumed_align=16), + from_dlpack({{get_output()}}.unsqueeze(-1), assumed_align=16), + probs, + strides, + ptrs, + tensormap_workspace, + stream, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_blackwell_ws_persistent_device_tma_mm.py.jinja b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_blackwell_ws_persistent_device_tma_mm.py.jinja new file mode 100644 index 0000000000000000000000000000000000000000..f0a363725ae1354fc92f36bbae3af791e68b0932 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_blackwell_ws_persistent_device_tma_mm.py.jinja @@ -0,0 +1,136 @@ +{{def_kernel("A", "B")}} + M = {{size("A", 0)}} + N = {{size("B", 1)}} + K = {{size("A", 1)}} + if M * N == 0: + # early exit due to zero-size input(s) + return + start_pid = tl.program_id(0) + grid_m = tl.cdiv(M, BLOCK_M) + grid_n = tl.cdiv(N, BLOCK_N) + k_tiles = tl.cdiv(K, BLOCK_K) + num_tiles = grid_m * grid_n + + # Note: We require TMA_EXPERIMENTAL_API == False, which + # we will check before invoking this template. + stride_am = {{stride("A", 0)}} + stride_ak = {{stride("A", 1)}} + stride_bk = {{stride("B", 0)}} + stride_bn = {{stride("B", 1)}} + a_desc = triton.language.make_tensor_descriptor( + base=A, + shape=[M, K] if A_ROW_MAJOR else [K, M], + strides=[stride_am, 1] if A_ROW_MAJOR else [stride_ak, 1], + block_shape=[BLOCK_M, BLOCK_K] if A_ROW_MAJOR else [BLOCK_K, BLOCK_M], + ) + b_desc = triton.language.make_tensor_descriptor( + base=B, + shape=[K, N] if B_ROW_MAJOR else [N, K], + strides=[stride_bk, 1] if B_ROW_MAJOR else [stride_bn, 1], + block_shape=[BLOCK_K, BLOCK_N] if B_ROW_MAJOR else [BLOCK_N, BLOCK_K], + ) + + # tile_id_c is used in the epilogue to break the dependency between + # the prologue and the epilogue + tile_id_c = start_pid - NUM_SMS + num_pid_in_group = GROUP_M * grid_n + + for tile_id in tl.range( + start_pid, num_tiles, NUM_SMS, flatten=FLATTEN, warp_specialize=WARP_SPECIALIZE + ): + pid_m, pid_n = _compute_pid( + tile_id, num_pid_in_group, grid_m, GROUP_M, NUM_SMS + ) + offs_am = pid_m * BLOCK_M + offs_bn = pid_n * BLOCK_N + offs_am_desc = offs_am.to(tl.int32) + offs_bn_desc = offs_bn.to(tl.int32) + + accumulator = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32) + for ki in range(k_tiles): + offs_k = ki * BLOCK_K + offs_k_desc = offs_k.to(tl.int32) + a = tl.load_tensor_descriptor( + a_desc, + [offs_am_desc, offs_k_desc] + if A_ROW_MAJOR + else [offs_k_desc, offs_am_desc], + ) + b = tl.load_tensor_descriptor( + b_desc, + [offs_k_desc, offs_bn_desc] + if B_ROW_MAJOR + else [offs_bn_desc, offs_k_desc], + ) + accumulator += tl.dot( + a if A_ROW_MAJOR else a.T, + b if B_ROW_MAJOR else b.T, + allow_tf32=ALLOW_TF32, + ) + + tile_id_c += NUM_SMS + pid_m, pid_n = _compute_pid( + tile_id_c, num_pid_in_group, grid_m, GROUP_M, NUM_SMS + ) + offs_cm = pid_m * BLOCK_M + offs_cn = pid_n * BLOCK_N + subtiles = _subtile_accumulator(accumulator, BLOCK_M, BLOCK_N, EPILOGUE_SUBTILE) + for i in tl.static_range(EPILOGUE_SUBTILE): + subtile = subtiles[i] + offs_cn_i = offs_cn + i * (BLOCK_N // EPILOGUE_SUBTILE) + {{store_output( + ("offs_cm", "offs_cn_i"), + "subtile", + indent_width=12, + val_shape=("BLOCK_M", "BLOCK_N // EPILOGUE_SUBTILE"), + block_indexing=True + )}} + +@triton.jit +def _compute_pid(tile_id, num_pid_in_group, grid_m, GROUP_M: tl.constexpr, NUM_SMS: tl.constexpr): + group_id = tile_id // num_pid_in_group + first_pid_m = group_id * GROUP_M + GROUP_M = min(grid_m - first_pid_m, GROUP_M) + pid_m = first_pid_m + (tile_id % GROUP_M) + pid_n = (tile_id % num_pid_in_group) // GROUP_M + return pid_m, pid_n + + +@triton.jit +def _subtile_accumulator( + acc, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + SUBTILE_FACTOR: tl.constexpr, +): + """ + Recursively split an accumulator into SUBTILE_FACTOR pieces using + reshape + permute + split operations. + + Args: + acc: The accumulator tensor of shape (BLOCK_M, BLOCK_N) + BLOCK_M: The M dimension size (constexpr) + BLOCK_N: The N dimension size (constexpr) + SUBTILE_FACTOR: Must be a power of 2. The number of pieces to split into (constexpr) + + Returns: + A tuple of SUBTILE_FACTOR tensors, each of shape (BLOCK_M, BLOCK_N // SUBTILE_FACTOR) + """ + # SUBTILE_FACTOR must be a power of 2 + tl.static_assert(SUBTILE_FACTOR > 0, "SUBTILE_FACTOR must be positive") + tl.static_assert((SUBTILE_FACTOR & (SUBTILE_FACTOR - 1)) == 0, "SUBTILE_FACTOR must be a power of 2") + + if SUBTILE_FACTOR == 1: + # Base case: no subtiling needed, return the input as a single-element tuple + return (acc,) + else: + # Recursive case: split into 2 halves, then recursively split each half + tl.static_assert(BLOCK_N % 2 == 0) + acc = tl.reshape(acc, (BLOCK_M, 2, BLOCK_N // 2)) + acc = tl.permute(acc, (0, 2, 1)) + left, right = tl.split(acc) + # Recursively split each half + left_subtiles = _subtile_accumulator(left, BLOCK_M, BLOCK_N // 2, SUBTILE_FACTOR // 2) + right_subtiles = _subtile_accumulator(right, BLOCK_M, BLOCK_N // 2, SUBTILE_FACTOR // 2) + # Concatenate the tuples + return left_subtiles + right_subtiles diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_bmm.py.jinja b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_bmm.py.jinja new file mode 100644 index 0000000000000000000000000000000000000000..94cec7f8063012658e4f277393a9b77f4fa64c43 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_bmm.py.jinja @@ -0,0 +1,73 @@ +{{def_kernel("A", "B")}} + M = {{size("A", -2)}} + N = {{size("B", -1)}} + K = {{size("A", -1)}} + BATCH = {{size("A", 0)}} + + stride_aq = {{stride("A", 0)}} + stride_am = {{stride("A", 1)}} + stride_ak = {{stride("A", 2)}} + + stride_bq = {{stride("B", 0)}} + stride_bk = {{stride("B", 1)}} + stride_bn = {{stride("B", 2)}} + + # based on triton.ops.matmul + pid = tl.program_id(0).to(INDEX_DTYPE) + grid_m = (M + BLOCK_M - 1) // BLOCK_M + grid_n = (N + BLOCK_N - 1) // BLOCK_N + + # re-order program ID for better L2 performance + width = GROUP_M * grid_n + group_id = pid // width + group_size = min(grid_m - group_id * GROUP_M, GROUP_M) + pid_m = group_id * GROUP_M + (pid % group_size) + pid_n = (pid % width) // (group_size) + tl.assume(pid_m >= 0) + tl.assume(pid_n >= 0) + + rm = pid_m * BLOCK_M + tl.arange(0, BLOCK_M).to(INDEX_DTYPE) + rn = pid_n * BLOCK_N + tl.arange(0, BLOCK_N).to(INDEX_DTYPE) + if ((stride_am == 1 and stride_ak == M) or (stride_am == K and stride_ak == 1)) and (M >= BLOCK_M and K > 1): + ram = tl.max_contiguous(tl.multiple_of(rm % M, BLOCK_M), BLOCK_M) + else: + ram = rm % M + if ((stride_bk == 1 and stride_bn == K) or (stride_bk == N and stride_bn == 1)) and (N >= BLOCK_N and K > 1): + rbn = tl.max_contiguous(tl.multiple_of(rn % N, BLOCK_N), BLOCK_N) + else: + rbn = rn % N + + rk = tl.arange(0, BLOCK_K).to(INDEX_DTYPE) + + # Reconstruct batch index from grid_y/grid_z split (handles batch > 65535) + idx_q = (tl.program_id(1) + tl.program_id(2) * tl.num_programs(1)).to(INDEX_DTYPE) + # Clamp to valid range for safe pointer arithmetic; out-of-bounds CTAs are + # masked off at the store below. + idx_q_clamped = tl.minimum(idx_q, BATCH - 1) + A = A + (ram[:, None] * stride_am + rk[None, :] * stride_ak + idx_q_clamped*stride_aq) + B = B + (rk[:, None] * stride_bk + rbn[None, :] * stride_bn + idx_q_clamped*stride_bq) + + acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=ACC_TYPE) + for k in range(K, 0, -BLOCK_K): + if EVEN_K: + a = tl.load(A) + b = tl.load(B) + else: + a = tl.load(A, mask=rk[None, :] < k, other=0.) + b = tl.load(B, mask=rk[:, None] < k, other=0.) + acc += tl.dot(a, b, allow_tf32=ALLOW_TF32) + A += BLOCK_K * stride_ak + B += BLOCK_K * stride_bk + + # rematerialize rm and rn to save registers + rm = pid_m * BLOCK_M + tl.arange(0, BLOCK_M).to(INDEX_DTYPE) + rn = pid_n * BLOCK_N + tl.arange(0, BLOCK_N).to(INDEX_DTYPE) + idx_q = (tl.program_id(1) + tl.program_id(2) * tl.num_programs(1)).to(INDEX_DTYPE) + idx_m = rm[:, None] + idx_n = rn[None, :] + mask = (idx_m < M) & (idx_n < N) & (idx_q < BATCH) + + # cast accumulator to output dtype + acc = acc.to(OUT_DTYPE) + # inductor generates a suffix + {{store_output(("idx_q", "idx_m", "idx_n"), "acc", "mask", val_shape=("BLOCK_M", "BLOCK_N"))}} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_depthwise_conv.py.jinja b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_depthwise_conv.py.jinja new file mode 100644 index 0000000000000000000000000000000000000000..312177c49dda363c95345b128af6dbebb145ae72 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_depthwise_conv.py.jinja @@ -0,0 +1,64 @@ +{{def_kernel("X", "W")}} + # Depthwise conv1d - channels-last (NLC) layout + # Each group has exactly 1 input channel (groups == in_channels == out_channels) + # 3D tiling: BLOCK_N x BLOCK_L x BLOCK_C + # Matches the hand-written NLC kernel from depthwise_conv1d_benchmark.py + + BATCH = {{size("X", 0)}} + CHANNELS = {{size("X", 1)}} + IN_L = {{size("X", 2)}} + OUT_L = {{size(None, 2)}} + + stride_xn = {{stride("X", 0)}} + stride_xc = {{stride("X", 1)}} + stride_xl = {{stride("X", 2)}} + + stride_wc = {{stride("W", 0)}} + stride_wk = {{stride("W", 2)}} + + # Grid: (cdiv(BATCH, BLOCK_N), cdiv(OUT_L, BLOCK_L), cdiv(CHANNELS, BLOCK_C)) + pid_n = tl.program_id(0).to(INDEX_DTYPE) + pid_l = tl.program_id(1).to(INDEX_DTYPE) + pid_c = tl.program_id(2).to(INDEX_DTYPE) + + n_start = pid_n * BLOCK_N + l_start = pid_l * BLOCK_L + c_start = pid_c * BLOCK_C + + n_offs = n_start + tl.arange(0, BLOCK_N) + l_offs = l_start + tl.arange(0, BLOCK_L) + c_offs = c_start + tl.arange(0, BLOCK_C) + n_mask = n_offs < BATCH + l_mask = l_offs < OUT_L + c_mask = c_offs < CHANNELS + + n3 = n_offs[:, None, None] + nm3 = n_mask[:, None, None] + c3 = c_offs[None, None, :] + cm3 = c_mask[None, None, :] + + in_base = n3 * stride_xn + c3 * stride_xc + + acc = tl.zeros((BLOCK_N, BLOCK_L, BLOCK_C), dtype=tl.float32) + + for k in range(KERNEL_SIZE): + wk = tl.load( + W + c_offs * stride_wc + k * stride_wk, + mask=c_mask, + other=0.0, + ).to(tl.float32) + l_in = l_offs * CONV_STRIDE - PADDING + k + mask_in = (l_in >= 0) & (l_in < IN_L) + x_vals = tl.load( + X + in_base + l_in[None, :, None] * stride_xl, + mask=nm3 & mask_in[None, :, None] & cm3, + other=0.0, + ).to(tl.float32) + acc += x_vals * wk[None, None, :] + + mask = nm3 & l_mask[None, :, None] & cm3 + idx_n = n_offs[:, None, None] + idx_c = c_offs[None, None, :] + idx_l = l_offs[None, :, None] + + {{store_output(("idx_n", "idx_c", "idx_l"), "acc", "mask", val_shape=("BLOCK_N", "BLOCK_L", "BLOCK_C"))}} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_epilogue_scaled_mm.py.jinja b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_epilogue_scaled_mm.py.jinja new file mode 100644 index 0000000000000000000000000000000000000000..cd3a6bdd4913a222489f18b0dccfbd7f3a3ab490 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_epilogue_scaled_mm.py.jinja @@ -0,0 +1,203 @@ +{{def_kernel("A", "B", "A_inverse_scale", "B_inverse_scale")}} + M = {{size("A", 0)}} + N = {{size("B", 1)}} + K = {{size("A", 1)}} + if M * N == 0: + # early exit due to zero-size input(s) + return + + stride_am = {{stride("A", 0)}} + stride_ak = {{stride("A", 1)}} + stride_bk = {{stride("B", 0)}} + stride_bn = {{stride("B", 1)}} + + if SCALE_RECIPE_A == 1: # ScalingType.RowWise + stride_a_scale_m = 1 + else: + stride_a_scale_m = 0 + + if SCALE_RECIPE_B == 1: # ScalingType.RowWise + stride_b_scale_n = 1 + else: + stride_b_scale_n = 0 + + start_pid = tl.program_id(axis=0).to(INDEX_DTYPE) + num_pid_m = tl.cdiv(M, BLOCK_M) + num_pid_n = tl.cdiv(N, BLOCK_N) + k_tiles = tl.cdiv(K, BLOCK_K) + num_tiles = num_pid_m * num_pid_n + + {%- if TMA_EXPERIMENTAL_API %} + workspace_base = ws_ptr + start_pid * 2 * TMA_SIZE + a_desc_ptr = workspace_base + b_desc_ptr = workspace_base + TMA_SIZE + + triton.language.extra.cuda.experimental_device_tensormap_create2d( + desc_ptr=a_desc_ptr, + global_address=A, + load_size=[BLOCK_M, BLOCK_K], + global_size=[M, K], + element_ty=A.dtype.element_ty, + ) + triton.language.extra.cuda.experimental_device_tensormap_create2d( + desc_ptr=b_desc_ptr, + global_address=B, + load_size=[BLOCK_N, BLOCK_K], + global_size=[N, K], + element_ty=B.dtype.element_ty, + ) + + tl.extra.cuda.experimental_tensormap_fenceproxy_acquire(a_desc_ptr) + tl.extra.cuda.experimental_tensormap_fenceproxy_acquire(b_desc_ptr) + + {%- else %} + stride_am = {{stride("A", 0)}} + stride_bn = {{stride("B", 1)}} + a_desc = triton.language.make_tensor_descriptor( + base=A, + shape=[M, K], + strides=[stride_am, 1], + block_shape=[BLOCK_M, BLOCK_K], + ) + b_desc = triton.language.make_tensor_descriptor( + base=B, + shape=[N, K], + strides=[stride_bn, 1], + block_shape=[BLOCK_N, BLOCK_K], + ) + {%- endif %} + + tiles_per_SM = num_tiles // NUM_SMS + if start_pid < num_tiles % NUM_SMS: + tiles_per_SM += 1 + + tile_id = start_pid - NUM_SMS + ki = -1 + + pid_m = 0 + pid_n = 0 + offs_am = 0 + offs_bn = 0 + + num_pid_in_group = GROUP_M * num_pid_n + accumulator = tl.zeros((BLOCK_M, BLOCK_N), dtype=ACC_TYPE) + a_scale = load_scales(A_inverse_scale, SCALE_RECIPE_A) + b_scale = load_scales(B_inverse_scale, SCALE_RECIPE_B) + + for _ in range(0, k_tiles * tiles_per_SM): + ki = tl.where(ki == k_tiles - 1, 0, ki + 1) + if ki == 0: + tile_id += NUM_SMS + group_id = tile_id // num_pid_in_group + first_pid_m = group_id * GROUP_M + group_size_m = min(num_pid_m - first_pid_m, GROUP_M) + pid_m = first_pid_m + (tile_id % group_size_m) + pid_n = (tile_id % num_pid_in_group) // group_size_m + + offs_am = pid_m * BLOCK_M + offs_bn = pid_n * BLOCK_N + + offs_k = ki * BLOCK_K + offs_am_desc = offs_am.to(tl.int32) + offs_bn_desc = offs_bn.to(tl.int32) + offs_k_desc = offs_k.to(tl.int32) + + {%- if TMA_EXPERIMENTAL_API %} + a = tl._experimental_descriptor_load( + a_desc_ptr, + [offs_am_desc, offs_k_desc], + [BLOCK_M, BLOCK_K], + A.dtype.element_ty, + ) + b = tl._experimental_descriptor_load( + b_desc_ptr, + [offs_bn_desc, offs_k_desc], + [BLOCK_N, BLOCK_K], + B.dtype.element_ty, + ) + {%- else %} + a = tl.load_tensor_descriptor(a_desc, [offs_am_desc, offs_k_desc]) + b = tl.load_tensor_descriptor(b_desc, [offs_bn_desc, offs_k_desc]) + {%- endif %} + if USE_FAST_ACCUM: + accumulator = tl.dot(a, b.T, accumulator) + else: + accumulator += tl.dot(a, b.T) + + if ki == k_tiles - 1: + # Apply inverse scaling + offs_cm = offs_am + tl.arange(0, BLOCK_M) + offs_cn = offs_bn + tl.arange(0, BLOCK_N) + # Apply scaling + accumulator = apply_scaling( + accumulator, + a_scale, + b_scale, + SCALE_RECIPE_A, + SCALE_RECIPE_B, + offs_cm, + offs_cn, + M, + N, + stride_a_scale_m, + stride_b_scale_n, + ) + + # inductor generates a suffix + {%- if TMA_EXPERIMENTAL_API %} + idx_m = offs_cm[:, None] + idx_n = offs_cn[None, :] + mask = (idx_m < M) & (idx_n < N) + {{store_output(("idx_m", "idx_n"), "accumulator", "mask", indent_width=12, val_shape=("BLOCK_M", "BLOCK_N"))}} + {%- else %} + {{store_output( + ("offs_am", "offs_bn"), + "accumulator", + indent_width=12, + val_shape=("BLOCK_M", "BLOCK_N"), + block_indexing=True, + )}} + {%- endif %} + accumulator = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32) + + +@triton.jit +def load_scales(scale_ptr, SCALE_RECIPE: tl.constexpr): + if SCALE_RECIPE == 0: + return tl.load(scale_ptr) # For tensor-wise scaling, we'll load the scalar values + else: + return scale_ptr # For all other scaling recipes, we'll return the pointers + + +@triton.jit +def apply_scaling( + accumulator, + a_scale, + b_scale, + SCALE_RECIPE_A: tl.constexpr, + SCALE_RECIPE_B: tl.constexpr, + offs_cm, + offs_cn, + M, + N, + stride_a_scale_m, + stride_b_scale_n, +): + if SCALE_RECIPE_A == 1 and SCALE_RECIPE_B == 1: # (ScalingType.RowWise, ScalingType.RowWise) + # For row-wise scaling, we need to load the scales for each row/column + a_scales = tl.load( + a_scale + (offs_cm * stride_a_scale_m), + mask=offs_cm < M, + other=0.0, + ) + b_scales = tl.load( + b_scale + (offs_cn * stride_b_scale_n), + mask=offs_cn < N, + other=0.0, + ) + acc_scale = a_scales[:, None] * b_scales[None, :] + else: # (ScalingType.TensorWise, ScalingType.TensorWise) + # For per-tensor scaling, we can directly use the loaded scalar values + acc_scale = a_scale * b_scale + + return accumulator * acc_scale diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_main_loop_scaled_mm.py.jinja b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_main_loop_scaled_mm.py.jinja new file mode 100644 index 0000000000000000000000000000000000000000..0c2e51a1ec2f2d540e712e2119b6c1bba2a2eac5 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_main_loop_scaled_mm.py.jinja @@ -0,0 +1,190 @@ +{{def_kernel("A", "B", "A_inverse_scale", "B_inverse_scale")}} + M = {{size("A", 0)}} + N = {{size("B", 1)}} + K = {{size("A", 1)}} + if M * N == 0: + # early exit due to zero-size input(s) + return + + stride_am = {{stride("A", 0)}} + stride_bn = {{stride("B", 1)}} + + start_pid = tl.program_id(axis=0).to(INDEX_DTYPE) + num_pid_m = tl.cdiv(M, BLOCK_M) + num_pid_n = tl.cdiv(N, BLOCK_N) + k_tiles = tl.cdiv(K, BLOCK_K) + num_tiles = num_pid_m * num_pid_n + + a_desc = triton.language.make_tensor_descriptor( + base=A, + shape=[M, K], + strides=[stride_am, 1], + block_shape=[BLOCK_M, BLOCK_K], + ) + b_desc = triton.language.make_tensor_descriptor( + base=B, + shape=[N, K], + strides=[stride_bn, 1], + block_shape=[BLOCK_N, BLOCK_K], + ) + + + num_pid_in_group = GROUP_M * num_pid_n + + am_blocks = tl.cdiv(M, TILE_SIZE_A) + ak_blocks = tl.cdiv(K, TILE_SIZE_A) + bn_blocks = tl.cdiv(N, TILE_SIZE_B) + bk_blocks = tl.cdiv(K, TILE_SIZE_B) + + for tile_id in range(start_pid, num_tiles, NUM_SMS): + group_id = tile_id // num_pid_in_group + first_pid_m = group_id * GROUP_M + group_size_m = min(num_pid_m - first_pid_m, GROUP_M) + pid_m = first_pid_m + (tile_id % group_size_m) + pid_n = (tile_id % num_pid_in_group) // group_size_m + + offs_am = pid_m * BLOCK_M + offs_bn = pid_n * BLOCK_N + offs_am_desc = offs_am.to(tl.int32) + offs_bn_desc = offs_bn.to(tl.int32) + + accumulator = tl.zeros((BLOCK_M, BLOCK_N), dtype=ACC_TYPE) + for ki in range(0, k_tiles): + offs_k = ki * BLOCK_K + offs_k_desc = offs_k.to(tl.int32) + + a = tl.load_tensor_descriptor(a_desc, [offs_am_desc, offs_k_desc]) + b = tl.load_tensor_descriptor(b_desc, [offs_bn_desc, offs_k_desc]) + + {%- if SCALE_RECIPE_A == 5 %} # ScalingType.Blockwise128x128 + scale_a_block = blockwise128x128_scaling( + pid_m, + A_inverse_scale, + ki, + am_blocks, + ak_blocks, + BLOCK_M, + BLOCK_K, + MIN_BLOCK_TILE_AM, + MIN_BLOCK_TILE_AK, + ) + {%- else %} # ScalingType.Blockwise1xTILESIZE + scale_a_block = blockwise1xTILESIZE_scaling( + pid_m, + A_inverse_scale, + ki, + M, + am_blocks, + ak_blocks, + BLOCK_M, + BLOCK_K, + MIN_BLOCK_TILE_AK, + TILE_SIZE_A, + ) + {%- endif %} + + {%- if SCALE_RECIPE_B == 5 %} # ScalingType.Blockwise128x128 + scale_b_block = blockwise128x128_scaling( + pid_n, + B_inverse_scale, + ki, + bn_blocks, + bk_blocks, + BLOCK_N, + BLOCK_K, + MIN_BLOCK_TILE_BN, + MIN_BLOCK_TILE_BK, + ) + {%- else %} # ScalingType.Blockwise1xTILESIZE + scale_b_block = blockwise1xTILESIZE_scaling( + pid_n, + B_inverse_scale, + ki, + N, + bn_blocks, + bk_blocks, + BLOCK_N, + BLOCK_K, + MIN_BLOCK_TILE_BK, + TILE_SIZE_B, + ) + {%- endif %} + + a_scaled = a * scale_a_block + b_scaled = b * scale_b_block + accumulator = tl.dot(a_scaled, b_scaled.T, accumulator) + + offs_cm = offs_am + tl.arange(0, BLOCK_M) + offs_cn = offs_bn + tl.arange(0, BLOCK_N) + + # inductor generates a suffix + {{store_output( + ("offs_am", "offs_bn"), + "accumulator", + indent_width=8, + val_shape=("BLOCK_M", "BLOCK_N"), + block_indexing=True, + )}} + + +@triton.jit +def blockwise1xTILESIZE_scaling( + pid, + scale, + ki, + lhs_size, + lhs_blocks, + k_blocks, + BLOCK_lhs: tl.constexpr, + BLOCK_K: tl.constexpr, + MIN_BLOCK_TILE_K: tl.constexpr, + TILE_SIZE: tl.constexpr, +): + row_offs_scale = pid * BLOCK_lhs + tl.arange(0, BLOCK_lhs) + col_offs_scale = ki * tl.cdiv(BLOCK_K, TILE_SIZE) + tl.arange(0, (BLOCK_K + TILE_SIZE - 1) // TILE_SIZE) + ptrs = scale + row_offs_scale[:, None] * k_blocks + col_offs_scale[None, :] + mask = (row_offs_scale[:, None] < lhs_size) & (col_offs_scale[None, :] < k_blocks) + scale_block = tl.load(ptrs, mask=mask, other=1.0) + + scale_expanded = scale_block[:, :, None] + scale_expanded = tl.broadcast_to( + scale_expanded, + (BLOCK_lhs, (BLOCK_K + TILE_SIZE - 1) // TILE_SIZE, MIN_BLOCK_TILE_K) + ) + scale_expanded = scale_expanded.reshape( + BLOCK_lhs, + ((BLOCK_K + TILE_SIZE - 1) // TILE_SIZE) * MIN_BLOCK_TILE_K + ) + + return scale_expanded + + +@triton.jit +def blockwise128x128_scaling( + pid, + scale, + ki, + lhs_blocks, + k_blocks, + BLOCK_lhs: tl.constexpr, + BLOCK_K: tl.constexpr, + MIN_BLOCK_TILE_lhs: tl.constexpr, + MIN_BLOCK_TILE_K: tl.constexpr, +): + row_offs_scale = pid * tl.cdiv(BLOCK_lhs, 128) + tl.arange(0, (BLOCK_lhs + 128 - 1) // 128) + col_offs_scale = ki * tl.cdiv(BLOCK_K, 128) + tl.arange(0, (BLOCK_K + 128 - 1) // 128) + ptrs = scale + row_offs_scale[:, None] * k_blocks + col_offs_scale[None, :] + mask = (row_offs_scale[:, None] < lhs_blocks) & (col_offs_scale[None, :] < k_blocks) + scale_block = tl.load(ptrs, mask=mask, other=1.0) + + scale_expanded = scale_block[:, :, None, None] + scale_expanded = tl.broadcast_to( + scale_expanded, + ((BLOCK_lhs + 128 - 1) // 128, (BLOCK_K + 128 - 1) // 128, MIN_BLOCK_TILE_lhs, MIN_BLOCK_TILE_K) + ) + scale_expanded = scale_expanded.reshape( + ((BLOCK_lhs + 128 - 1) // 128) * MIN_BLOCK_TILE_lhs, + ((BLOCK_K + 128 - 1) // 128) * MIN_BLOCK_TILE_K + ) + + return scale_expanded diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_mm.py.jinja b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_mm.py.jinja new file mode 100644 index 0000000000000000000000000000000000000000..fcb22c94eada124b9c88f996cf3f707ff5436fb3 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_mm.py.jinja @@ -0,0 +1,72 @@ +{{def_kernel("A", "B")}} + M = {{size("A", 0)}} + N = {{size("B", 1)}} + K = {{size("A", 1)}} + if M * N == 0: + # early exit due to zero-size input(s) + return + stride_am = {{stride("A", 0)}} + stride_ak = {{stride("A", 1)}} + stride_bk = {{stride("B", 0)}} + stride_bn = {{stride("B", 1)}} + + # based on triton.ops.matmul + pid = tl.program_id(0).to(INDEX_DTYPE) + grid_m = (M + BLOCK_M - 1) // BLOCK_M + grid_n = (N + BLOCK_N - 1) // BLOCK_N + + # re-order program ID for better L2 performance + width = GROUP_M * grid_n + group_id = pid // width + group_size = min(grid_m - group_id * GROUP_M, GROUP_M) + pid_m = group_id * GROUP_M + (pid % group_size) + pid_n = (pid % width) // (group_size) + tl.assume(pid_m >= 0) + tl.assume(pid_n >= 0) + + rm = pid_m * BLOCK_M + tl.arange(0, BLOCK_M).to(INDEX_DTYPE) + rn = pid_n * BLOCK_N + tl.arange(0, BLOCK_N).to(INDEX_DTYPE) + if ((stride_am == 1 and stride_ak == M) or (stride_am == K and stride_ak == 1)) and (M >= BLOCK_M and K > 1): + offs_a_m = tl.max_contiguous(tl.multiple_of(rm % M, BLOCK_M), BLOCK_M) + else: + offs_a_m = rm % M + if ((stride_bk == 1 and stride_bn == K) or (stride_bk == N and stride_bn == 1)) and (N >= BLOCK_N and K > 1): + offs_b_n = tl.max_contiguous(tl.multiple_of(rn % N, BLOCK_N), BLOCK_N) + else: + offs_b_n = rn % N + offs_k = tl.arange(0, BLOCK_K).to(INDEX_DTYPE) + acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=ACC_TYPE) + + for k_idx in range(0, tl.cdiv(K, BLOCK_K)): + {% if not EVEN_K %} + a_mask = offs_k[None, :] < (K - k_idx * BLOCK_K) + b_mask = offs_k[:, None] < (K - k_idx * BLOCK_K) + {% endif %} + a_k_idx_vals = offs_k[None, :] + (k_idx * BLOCK_K) + b_k_idx_vals = offs_k[:, None] + (k_idx * BLOCK_K) + + idx_m = offs_a_m[:, None] + idx_n = a_k_idx_vals + {{load_input("A", "a", ("idx_m", "idx_n"), mask=None if EVEN_K else "a_mask", + indent_width=8, index_shape=("BLOCK_M", "BLOCK_K"))}} + + idx_m = b_k_idx_vals + idx_n = offs_b_n[None, :] + {{load_input("B", "b", ("idx_m", "idx_n"), mask=None if EVEN_K else "b_mask", + indent_width=8, index_shape=("BLOCK_K", "BLOCK_N"))}} + + {% if USE_FAST_ACCUM %} + acc = tl.dot(a, b, acc, allow_tf32=ALLOW_TF32, out_dtype=ACC_TYPE) + {% else %} + acc += tl.dot(a, b, allow_tf32=ALLOW_TF32, out_dtype=ACC_TYPE) + {% endif %} + + # rematerialize rm and rn to save registers + rm = pid_m * BLOCK_M + tl.arange(0, BLOCK_M).to(INDEX_DTYPE) + rn = pid_n * BLOCK_N + tl.arange(0, BLOCK_N).to(INDEX_DTYPE) + idx_m = rm[:, None] + idx_n = rn[None, :] + mask = (idx_m < M) & (idx_n < N) + + # inductor generates a suffix + {{store_output(("idx_m", "idx_n"), "acc", "mask", val_shape=("BLOCK_M", "BLOCK_N"))}} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_mm_grouped.py.jinja b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_mm_grouped.py.jinja new file mode 100644 index 0000000000000000000000000000000000000000..52049582bf21d2928b58d6ee9a664eae15e5e6b9 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_mm_grouped.py.jinja @@ -0,0 +1,379 @@ +{% macro assign_maybe_constexpr(name, value_expr) -%} + {%- set value_str = value_expr | string -%} + {%- set sentinel = "__NOT_A_NUMBER__" -%} + {%- set as_int = value_str | int(default=sentinel) -%} + {%- set as_float = value_str | float(default=sentinel) -%} + {%- set is_constexpr = (as_int != sentinel) or (as_float != sentinel) -%} + {{ name }}{{ ": tl.constexpr" if is_constexpr else "" }} = {{ value_expr }} +{%- endmacro %} + +import triton +import triton.language as tl + +@triton.jit +def do_tma_loads( + g, a_desc, b_desc, m_offset, n_offset, k_offset, + BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr, +): +{%- if A_IS_2D %} +{%- if A_IS_K_MAJOR %} + a = a_desc.load([m_offset, k_offset]) +{%- else %} + a = a_desc.load([k_offset, m_offset]) +{%- endif %} +{%- else %} +{%- if A_IS_K_MAJOR %} + a = a_desc.load([g, m_offset, k_offset]).reshape(BLOCK_M, BLOCK_K) +{%- else %} + a = a_desc.load([g, k_offset, m_offset]).reshape(BLOCK_K, BLOCK_M) +{%- endif %} +{%- endif %} +{%- if B_IS_2D %} +{%- if B_IS_K_MAJOR %} + b = b_desc.load([n_offset, k_offset]) +{%- else %} + b = b_desc.load([k_offset, n_offset]) +{%- endif %} +{%- else %} +{%- if B_IS_K_MAJOR %} + b = b_desc.load([g, n_offset, k_offset]).reshape(BLOCK_N, BLOCK_K) +{%- else %} + b = b_desc.load([g, k_offset, n_offset]).reshape(BLOCK_K, BLOCK_N) +{%- endif %} +{%- endif %} + + return (a, b) + + +@triton.jit +def do_mma(a, b, accumulator): +{%- if USE_FAST_ACCUM %} +{%- if A_IS_K_MAJOR and B_IS_K_MAJOR %} + accumulator = tl.dot(a, b.T, accumulator) +{%- elif A_IS_K_MAJOR and not B_IS_K_MAJOR %} + accumulator = tl.dot(a, b, accumulator) +{%- elif not A_IS_K_MAJOR and B_IS_K_MAJOR %} + accumulator = tl.dot(a.T, b.T, accumulator) +{%- else %} + accumulator = tl.dot(a.T, b, accumulator) +{%- endif %} +{%- else %} +{%- if A_IS_K_MAJOR and B_IS_K_MAJOR %} + accumulator += tl.dot(a, b.T) +{%- elif A_IS_K_MAJOR and not B_IS_K_MAJOR %} + accumulator += tl.dot(a, b) +{%- elif not A_IS_K_MAJOR and B_IS_K_MAJOR %} + accumulator += tl.dot(a.T, b.T) +{%- else %} + accumulator += tl.dot(a.T, b) +{%- endif %} +{%- endif %} + + return accumulator + + +{%- if SCALED %} +{%- if A_IS_2D or B_IS_2D %} +{{def_kernel("a_ptr", "b_ptr", "scale_a_ptr", "scale_b_ptr", "offsets_ptr")}} +{%- else %} +{{def_kernel("a_ptr", "b_ptr", "scale_a_ptr", "scale_b_ptr")}} +{%- endif %} +{%- else %} +{%- if A_IS_2D or B_IS_2D %} +{{def_kernel("a_ptr", "b_ptr", "offsets_ptr")}} +{%- else %} +{{def_kernel("a_ptr", "b_ptr")}} +{%- endif %} +{%- endif %} + tidx = tl.program_id(0).to(INDEX_DTYPE) + +{%- set M_IS_VARYING = A_IS_2D and not B_IS_2D %} +{%- set N_IS_VARYING = not A_IS_2D and B_IS_2D %} +{%- set K_IS_VARYING = A_IS_2D and B_IS_2D %} + +{%- if A_IS_2D %} +{%- if B_IS_2D %} + {{ assign_maybe_constexpr("G", size("offsets_ptr", 0)) }} +{%- else %} + {{ assign_maybe_constexpr("G", size("b_ptr", 0)) }} +{%- endif %} +{%- else %} +{%- if B_IS_2D %} + {{ assign_maybe_constexpr("G", size("a_ptr", 0)) }} +{%- else %} + {{ assign_maybe_constexpr("G", size("a_ptr", 0)) }} +{%- endif %} +{%- endif %} + + # the b_ptr tensor is given with its last two dims transposed, revert here + + {{ assign_maybe_constexpr("M", size("a_ptr", -2)) }} + {{ assign_maybe_constexpr("N", size("b_ptr", -1)) }} + {{ assign_maybe_constexpr("K", size("a_ptr", -1)) }} + + {{ assign_maybe_constexpr("A_STRIDE_M", stride("a_ptr", -2)) }} + {{ assign_maybe_constexpr("A_STRIDE_K", stride("a_ptr", -1)) }} +{%- if not A_IS_2D %} + {{ assign_maybe_constexpr("A_STRIDE_G", stride("a_ptr", 0)) }} +{%- if SCALED %} + {{ assign_maybe_constexpr("SCALE_A_STRIDE_G", stride("scale_a_ptr", 0)) }} +{%- endif %} +{%- endif %} + {{ assign_maybe_constexpr("B_STRIDE_N", stride("b_ptr", -1)) }} + {{ assign_maybe_constexpr("B_STRIDE_K", stride("b_ptr", -2)) }} +{%- if not B_IS_2D %} + {{ assign_maybe_constexpr("B_STRIDE_G", stride("b_ptr", 0)) }} + B_STRIDE_G = {{stride("b_ptr", 0)}} +{%- if SCALED %} + {{ assign_maybe_constexpr("SCALE_B_STRIDE_G", stride("scale_b_ptr", 0)) }} +{%- endif %} +{%- endif %} + +{%- if USE_TMA_LOAD %} +{%- if USE_EXPERIMENTAL_MAKE_TENSOR_DESCRIPTOR %} + a_desc = tl._experimental_make_tensor_descriptor( +{%- else %} + a_desc = tl.make_tensor_descriptor( +{%- endif %} + a_ptr, +{%- if A_IS_2D %} +{%- if A_IS_K_MAJOR %} + shape=[M, K], + strides=[A_STRIDE_M, A_STRIDE_K], + block_shape=[BLOCK_M, BLOCK_K], +{%- else %} + shape=[K, M], + strides=[A_STRIDE_K, A_STRIDE_M], + block_shape=[BLOCK_K, BLOCK_M], +{%- endif %} +{%- else %} +{%- if A_IS_K_MAJOR %} + shape=[G, M, K], + strides=[A_STRIDE_G, A_STRIDE_M, A_STRIDE_K], + block_shape=[1, BLOCK_M, BLOCK_K], +{%- else %} + shape=[G, K, M], + strides=[A_STRIDE_G, A_STRIDE_K, A_STRIDE_M], + block_shape=[1, BLOCK_K, BLOCK_M], +{%- endif %} +{%- endif %} + ) + +{%- if USE_EXPERIMENTAL_MAKE_TENSOR_DESCRIPTOR %} + b_desc = tl._experimental_make_tensor_descriptor( +{%- else %} + b_desc = tl.make_tensor_descriptor( +{%- endif %} + b_ptr, +{%- if B_IS_2D %} +{%- if B_IS_K_MAJOR %} + shape=[N, K], + strides=[B_STRIDE_N, B_STRIDE_K], + block_shape=[BLOCK_N, BLOCK_K], +{%- else %} + shape=[K, N], + strides=[B_STRIDE_K, B_STRIDE_N], + block_shape=[BLOCK_K, BLOCK_N], +{%- endif %} +{%- else %} +{%- if B_IS_K_MAJOR %} + shape=[G, N, K], + strides=[B_STRIDE_G, B_STRIDE_N, B_STRIDE_K], + block_shape=[1, BLOCK_N, BLOCK_K], +{%- else %} + shape=[G, K, N], + strides=[B_STRIDE_G, B_STRIDE_K, B_STRIDE_N], + block_shape=[1, BLOCK_K, BLOCK_N], +{%- endif %} +{%- endif %} + ) +{%- endif %} + +{%- if M_IS_VARYING %} + m_end_offset = 0 +{%- endif %} +{%- if N_IS_VARYING %} + n_end_offset = 0 +{%- endif %} +{%- if K_IS_VARYING %} + k_end_offset = 0 +{%- endif %} + iterated_tiles = 0 + for g in tl.range(G): +{%- if M_IS_VARYING %} + # Move across groups + m_start_offset = m_end_offset + m_end_offset = tl.load(offsets_ptr + g) + m_size = m_end_offset - m_start_offset +{%- if SCALED %} + m_scale_start_offset = m_start_offset +{%- endif %} +{%- else %} + m_start_offset = 0 + m_size = M +{%- if SCALED %} + m_scale_start_offset = g * M +{%- endif %} +{%- endif %} + +{%- if N_IS_VARYING %} + # Move across groups + n_start_offset = n_end_offset + n_end_offset = tl.load(offsets_ptr + g) + n_size = n_end_offset - n_start_offset +{%- if SCALED %} + n_scale_start_offset = n_start_offset +{%- endif %} +{%- else %} + n_start_offset = 0 + n_size = N +{%- if SCALED %} + n_scale_start_offset = g * N +{%- endif %} +{%- endif %} + + if m_size > 0 and n_size > 0: +{%- if K_IS_VARYING %} + # Move across groups + k_start_offset = k_end_offset + k_end_offset = tl.load(offsets_ptr + g) + k_size = k_end_offset - k_start_offset +{%- else %} + k_start_offset = 0 + k_size = K +{%- endif %} + + num_m_tiles = tl.cdiv(m_size, BLOCK_M) + num_n_tiles = tl.cdiv(n_size, BLOCK_N) + num_tiles = num_m_tiles * num_n_tiles + + # Move across tiles + while tidx >= iterated_tiles and tidx < iterated_tiles + num_tiles: + gidx = tidx - iterated_tiles + # Split M first and N second. + tile_m_idx = gidx % num_m_tiles + tile_n_idx = gidx // num_m_tiles + + accumulator = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32) + +{%- if USE_TMA_LOAD %} + m_tile_offset = tile_m_idx * BLOCK_M + n_tile_offset = tile_n_idx * BLOCK_N + m_offset = (m_start_offset + m_tile_offset).to(tl.int32) + n_offset = (n_start_offset + n_tile_offset).to(tl.int32) + + k_block_offset = 0 + for k in range(k_size // BLOCK_K): + k_offset = k_start_offset + k_block_offset + a, b = do_tma_loads( + g, a_desc, b_desc, m_offset, n_offset, k_offset, + BLOCK_M, BLOCK_N, BLOCK_K + ) + accumulator = do_mma(a, b, accumulator) + k_block_offset += BLOCK_K + + if k_size % BLOCK_K != 0: + k_offset = k_start_offset + k_block_offset + a, b = do_tma_loads( + g, a_desc, b_desc, m_offset, n_offset, k_offset, + BLOCK_M, BLOCK_N, BLOCK_K + ) +{%- if K_IS_VARYING %} + group_offs = k_block_offset + tl.arange(0, BLOCK_K) + k_mask = group_offs < k_size +{%- if A_IS_K_MAJOR %} + a = tl.where(k_mask[None, :], a, 0) +{%- else %} + a = tl.where(k_mask[:, None], a, 0) +{%- endif %} +{%- if B_IS_K_MAJOR %} + b = tl.where(k_mask[None, :], b, 0) +{%- else %} + b = tl.where(k_mask[:, None], b, 0) +{%- endif %} +{%- endif %} + accumulator = do_mma(a, b, accumulator) +{%- else %} + offs_am = tile_m_idx * BLOCK_M + tl.arange(0, BLOCK_M).to(INDEX_DTYPE) + offs_bn = tile_n_idx * BLOCK_N + tl.arange(0, BLOCK_N).to(INDEX_DTYPE) + for k_block_offset in range(0, k_size, BLOCK_K): + block_offs_k = k_block_offset + tl.arange(0, BLOCK_K).to(INDEX_DTYPE) + offs_k = block_offs_k + k_start_offset + a_ptrs = ( + a_ptr +{%- if not A_IS_2D %} + + g * A_STRIDE_G +{%- endif %} + + (m_start_offset + offs_am[:, None]) * A_STRIDE_M + + offs_k[None, :] * A_STRIDE_K + ) + b_ptrs = ( + b_ptr +{%- if not B_IS_2D %} + + g * B_STRIDE_G +{%- endif %} + + (n_start_offset + offs_bn[:, None]) * B_STRIDE_N + + offs_k[None, :] * B_STRIDE_K + ) + a_mask = (offs_am[:, None] < m_size) & (block_offs_k[None, :] < k_size) + b_mask = (offs_bn[:, None] < n_size) & (block_offs_k[None, :] < k_size) + a = tl.load(a_ptrs, mask=a_mask, other=tl.zeros((), dtype=a_ptrs.dtype.element_ty)) + b = tl.load(b_ptrs, mask=b_mask, other=tl.zeros((), dtype=b_ptrs.dtype.element_ty)) +{%- if USE_FAST_ACCUM %} + accumulator = tl.dot(a, b.T, accumulator) +{%- else %} + accumulator += tl.dot(a, b.T) +{%- endif %} + a_ptrs += BLOCK_K + b_ptrs += BLOCK_K +{%- endif %} + + offs_am = tile_m_idx * BLOCK_M + tl.arange(0, BLOCK_M).to(INDEX_DTYPE) + offs_bn = tile_n_idx * BLOCK_N + tl.arange(0, BLOCK_N).to(INDEX_DTYPE) +{%- if SCALED %} + scale_a = tl.load( + scale_a_ptr +{%- if A_IS_2D %} + + m_scale_start_offset +{%- else %} + + g * SCALE_A_STRIDE_G +{%- endif %} + + offs_am[:, None], + mask=offs_am[:, None] < m_size, + other=tl.zeros((), dtype=scale_a_ptr.dtype.element_ty), + ) + scale_b = tl.load( + scale_b_ptr +{%- if B_IS_2D %} + + n_scale_start_offset +{%- else %} + + g * SCALE_B_STRIDE_G +{%- endif %} + + offs_bn[None, :], + mask=offs_bn[None, :] < n_size, + other=tl.zeros((), dtype=scale_b_ptr.dtype.element_ty), + ) + c = accumulator.to(tl.float32) * scale_a * scale_b +{%- else %} + c = accumulator.to(tl.float32) +{%- endif %} + +{%- if M_IS_VARYING %} + idx_m = (m_start_offset + offs_am[:, None]) +{%- else %} + idx_m = offs_am[:, None] +{%- endif %} +{%- if N_IS_VARYING %} + idx_n = (n_start_offset + offs_bn[None, :]) +{%- else %} + idx_n = offs_bn[None, :] +{%- endif %} + mask = (offs_am[:, None] < m_size) & (offs_bn[None, :] < n_size) +{%- if M_IS_VARYING or N_IS_VARYING %} + {{store_output(("idx_m", "idx_n"), "c", "mask", indent_width=16, val_shape=("BLOCK_M", "BLOCK_N"))}} +{%- else %} + {{store_output(("g", "idx_m", "idx_n"), "c", "mask", indent_width=16, val_shape=("BLOCK_M", "BLOCK_N"))}} +{%- endif %} + tidx += NUM_SMS + + iterated_tiles += num_tiles diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_mm_rocm.py.jinja b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_mm_rocm.py.jinja new file mode 100644 index 0000000000000000000000000000000000000000..6ad00438cf66d428148a8a6c6c912aaaadde0291 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_mm_rocm.py.jinja @@ -0,0 +1,71 @@ +{{def_kernel("A", "B")}} + M = {{size("A", 0)}} + N = {{size("B", 1)}} + K = {{size("A", 1)}} + if M * N == 0: + # early exit due to zero-size input(s) + return + stride_am = {{stride("A", 0)}} + stride_ak = {{stride("A", 1)}} + stride_bk = {{stride("B", 0)}} + stride_bn = {{stride("B", 1)}} + + # based on triton.ops.matmul + pid = tl.program_id(0).to(INDEX_DTYPE) + grid_m = (M + BLOCK_M - 1) // BLOCK_M + grid_n = (N + BLOCK_N - 1) // BLOCK_N + + # re-order program ID for better L2 performance + width = GROUP_M * grid_n + group_id = pid // width + group_size = min(grid_m - group_id * GROUP_M, GROUP_M) + pid_m = group_id * GROUP_M + (pid % group_size) + pid_n = (pid % width) // (group_size) + tl.assume(pid_m >= 0) + tl.assume(pid_n >= 0) + + rm = pid_m * BLOCK_M + tl.arange(0, BLOCK_M).to(INDEX_DTYPE) + rn = pid_n * BLOCK_N + tl.arange(0, BLOCK_N).to(INDEX_DTYPE) + if (stride_am == 1 and stride_ak == M) or (stride_am == K and stride_ak == 1): + offs_a_m = tl.max_contiguous(tl.multiple_of(rm % M, BLOCK_M), BLOCK_M) + else: + offs_a_m = rm % M + if (stride_bk == 1 and stride_bn == K) or (stride_bk == N and stride_bn == 1): + offs_b_n = tl.max_contiguous(tl.multiple_of(rn % N, BLOCK_N), BLOCK_N) + else: + offs_b_n = rn % N + offs_k = tl.arange(0, BLOCK_K).to(INDEX_DTYPE) + acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=ACC_TYPE) + + for k_idx in range(0, tl.cdiv(K, BLOCK_K)): + {% if not EVEN_K %} + a_mask = offs_k[None, :] < (K - k_idx * BLOCK_K) + b_mask = offs_k[:, None] < (K - k_idx * BLOCK_K) + {% endif %} + a_k_idx_vals = offs_k[None, :] + (k_idx * BLOCK_K) + b_k_idx_vals = offs_k[:, None] + (k_idx * BLOCK_K) + + idx_m = offs_a_m[:, None] + idx_n = a_k_idx_vals + {{load_input("A", "a", ("idx_m", "idx_n"), mask=None if EVEN_K else "a_mask", + indent_width=8, index_shape=("BLOCK_M", "BLOCK_K"))}} + + idx_m = b_k_idx_vals + idx_n = offs_b_n[None, :] + {{load_input("B", "b", ("idx_m", "idx_n"), mask=None if EVEN_K else "b_mask", + indent_width=8, index_shape=("BLOCK_K", "BLOCK_N"))}} + {% if USE_FAST_ACCUM %} + acc = tl.dot(a, b, acc, allow_tf32=ALLOW_TF32, out_dtype=ACC_TYPE) + {% else %} + acc += tl.dot(a, b, allow_tf32=ALLOW_TF32, out_dtype=ACC_TYPE) + {% endif %} + + # rematerialize rm and rn to save registers + rm = pid_m * BLOCK_M + tl.arange(0, BLOCK_M).to(INDEX_DTYPE) + rn = pid_n * BLOCK_N + tl.arange(0, BLOCK_N).to(INDEX_DTYPE) + idx_m = rm[:, None] + idx_n = rn[None, :] + mask = (idx_m < M) & (idx_n < N) + + # inductor generates a suffix + {{store_output(("idx_m", "idx_n"), "acc", "mask", val_shape=("BLOCK_M", "BLOCK_N"))}} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_persistent_mm.py.jinja b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_persistent_mm.py.jinja new file mode 100644 index 0000000000000000000000000000000000000000..7450533be5adfa97632e4ccf3ef0ca966cbfef49 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_persistent_mm.py.jinja @@ -0,0 +1,75 @@ +{{def_kernel("A", "B")}} + M = {{size("A", 0)}} + N = {{size("B", 1)}} + K = {{size("A", 1)}} + if M * N == 0: + # early exit due to zero-size input(s) + return + stride_am = {{stride("A", 0)}} + stride_ak = {{stride("A", 1)}} + stride_bk = {{stride("B", 0)}} + stride_bn = {{stride("B", 1)}} + + # persistent kernel: each CTA processes multiple tiles + start_pid = tl.program_id(0).to(INDEX_DTYPE) + grid_m = tl.cdiv(M, BLOCK_M) + grid_n = tl.cdiv(N, BLOCK_N) + num_tiles = grid_m * grid_n + width = GROUP_M * grid_n + + for tile_id in tl.range(start_pid, num_tiles, NUM_SMS): + + # re-order program ID for better L2 performance + group_id = tile_id // width + group_size = min(grid_m - group_id * GROUP_M, GROUP_M) + pid_m = group_id * GROUP_M + (tile_id % group_size) + pid_n = (tile_id % width) // (group_size) + tl.assume(pid_m >= 0) + tl.assume(pid_n >= 0) + + rm = pid_m * BLOCK_M + tl.arange(0, BLOCK_M) + rn = pid_n * BLOCK_N + tl.arange(0, BLOCK_N) + if ((stride_am == 1 and stride_ak == M) or (stride_am == K and stride_ak == 1)) and (M >= BLOCK_M and K > 1): + offs_a_m = tl.max_contiguous(tl.multiple_of(rm % M, BLOCK_M), BLOCK_M) + else: + offs_a_m = rm % M + if ((stride_bk == 1 and stride_bn == K) or (stride_bk == N and stride_bn == 1)) and (N >= BLOCK_N and K > 1): + offs_b_n = tl.max_contiguous(tl.multiple_of(rn % N, BLOCK_N), BLOCK_N) + else: + offs_b_n = rn % N + offs_k = tl.arange(0, BLOCK_K) + acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=ACC_TYPE) + + for k_idx in range(0, tl.cdiv(K, BLOCK_K)): + {% if not EVEN_K %} + a_mask = offs_k[None, :] < (K - k_idx * BLOCK_K) + b_mask = offs_k[:, None] < (K - k_idx * BLOCK_K) + {% endif %} + a_k_idx_vals = offs_k[None, :] + (k_idx * BLOCK_K) + b_k_idx_vals = offs_k[:, None] + (k_idx * BLOCK_K) + + idx_m = offs_a_m[:, None] + idx_n = a_k_idx_vals + {{load_input("A", "a", ("idx_m", "idx_n"), mask=None if EVEN_K else "a_mask", + indent_width=12, index_shape=("BLOCK_M", "BLOCK_K"))}} + + idx_m = b_k_idx_vals + idx_n = offs_b_n[None, :] + {{load_input("B", "b", ("idx_m", "idx_n"), mask=None if EVEN_K else "b_mask", + indent_width=12, index_shape=("BLOCK_K", "BLOCK_N"))}} + + {% if USE_FAST_ACCUM %} + acc = tl.dot(a, b, acc, allow_tf32=ALLOW_TF32, out_dtype=ACC_TYPE) + {% else %} + acc += tl.dot(a, b, allow_tf32=ALLOW_TF32, out_dtype=ACC_TYPE) + {% endif %} + + # rematerialize rm and rn to save registers + rm = pid_m * BLOCK_M + tl.arange(0, BLOCK_M) + rn = pid_n * BLOCK_N + tl.arange(0, BLOCK_N) + idx_m = rm[:, None] + idx_n = rn[None, :] + mask = (idx_m < M) & (idx_n < N) + + # inductor generates a suffix + {{store_output(("idx_m", "idx_n"), "acc", "mask", indent_width=8, val_shape=("BLOCK_M", "BLOCK_N"))}} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_persistent_tma_mm.py.jinja b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_persistent_tma_mm.py.jinja new file mode 100644 index 0000000000000000000000000000000000000000..cb6b4a14408126350f76415ded20265ba4dc808f --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/templates/triton_persistent_tma_mm.py.jinja @@ -0,0 +1,127 @@ +{{def_kernel("A", "B")}} + M = {{size("A", 0)}} + N = {{size("B", 1)}} + K = {{size("A", 1)}} + + if (M == 0) or (N == 0): + # early exit due to zero-size input(s) + return + + start_pid = tl.program_id(0).to(INDEX_DTYPE) + grid_m = tl.cdiv(M, BLOCK_M) + grid_n = tl.cdiv(N, BLOCK_N) + num_tiles = grid_m * grid_n + width = GROUP_M * grid_n + + {%- if TMA_EXPERIMENTAL_API %} + workspace_base = ws_ptr + start_pid * 2 * TMA_SIZE + a_desc_ptr = workspace_base + b_desc_ptr = workspace_base + TMA_SIZE + + triton.language.extra.cuda.experimental_device_tensormap_create2d( + desc_ptr=a_desc_ptr, + global_address=A, + load_size=[BLOCK_M, BLOCK_K] if A_ROW_MAJOR else [BLOCK_K, BLOCK_M], + global_size=[M, K] if A_ROW_MAJOR else [K, M], + element_ty=A.dtype.element_ty, + ) + triton.language.extra.cuda.experimental_device_tensormap_create2d( + desc_ptr=b_desc_ptr, + global_address=B, + load_size=[BLOCK_K, BLOCK_N] if B_ROW_MAJOR else [BLOCK_N, BLOCK_K], + global_size=[K, N] if B_ROW_MAJOR else [N, K], + element_ty=B.dtype.element_ty, + ) + + tl.extra.cuda.experimental_tensormap_fenceproxy_acquire(a_desc_ptr) + tl.extra.cuda.experimental_tensormap_fenceproxy_acquire(b_desc_ptr) + + {%- else %} + stride_am = {{stride("A", 0)}} + stride_ak = {{stride("A", 1)}} + stride_bk = {{stride("B", 0)}} + stride_bn = {{stride("B", 1)}} + + a_desc = triton.language.make_tensor_descriptor( + base=A, + shape=[M, K] if A_ROW_MAJOR else [K, M], + strides=[stride_am, 1] if A_ROW_MAJOR else [stride_ak, 1], + block_shape=[BLOCK_M, BLOCK_K] if A_ROW_MAJOR else [BLOCK_K, BLOCK_M], + ) + b_desc = triton.language.make_tensor_descriptor( + base=B, + shape=[K, N] if B_ROW_MAJOR else [N, K], + strides=[stride_bk, 1] if B_ROW_MAJOR else [stride_bn, 1], + block_shape=[BLOCK_K, BLOCK_N] if B_ROW_MAJOR else [BLOCK_N, BLOCK_K], + ) + {%- endif %} + + for tile_id in tl.range(start_pid, num_tiles, NUM_SMS): + + # re-order program ID for better L2 performance + group_id = tile_id // width + group_size = min(grid_m - group_id * GROUP_M, GROUP_M) + pid_m = group_id * GROUP_M + (tile_id % group_size) + pid_n = (tile_id % width) // (group_size) + + rm = pid_m * BLOCK_M + rn = pid_n * BLOCK_N + rm_desc = rm.to(tl.int32) + rn_desc = rn.to(tl.int32) + acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=ACC_TYPE) + + for rk in tl.range(0, K, BLOCK_K): + rk_desc = rk.to(tl.int32) + + {%- if TMA_EXPERIMENTAL_API %} + a = tl._experimental_descriptor_load( + a_desc_ptr, + [rm_desc, rk_desc] if A_ROW_MAJOR else [rk_desc, rm_desc], + [BLOCK_M, BLOCK_K] if A_ROW_MAJOR else [BLOCK_K, BLOCK_M], + A.dtype.element_ty, + ) + b = tl._experimental_descriptor_load( + b_desc_ptr, + [rk_desc, rn_desc] if B_ROW_MAJOR else [rn_desc, rk_desc], + [BLOCK_K, BLOCK_N] if B_ROW_MAJOR else [BLOCK_N, BLOCK_K], + B.dtype.element_ty, + ) + {%- else %} + a = tl.load_tensor_descriptor( + a_desc, + [rm_desc, rk_desc] if A_ROW_MAJOR else [rk_desc, rm_desc], + ) + b = tl.load_tensor_descriptor( + b_desc, + [rk_desc, rn_desc] if B_ROW_MAJOR else [rn_desc, rk_desc], + ) + {%- endif %} + if USE_FAST_ACCUM: + acc = tl.dot( + a if A_ROW_MAJOR else a.T, + b if B_ROW_MAJOR else b.T, + acc, + allow_tf32=ALLOW_TF32, + ) + else: + acc += tl.dot( + a if A_ROW_MAJOR else a.T, + b if B_ROW_MAJOR else b.T, + allow_tf32=ALLOW_TF32, + ) + + # rematerialize rm and rn to save registers + rm = pid_m * BLOCK_M + rn = pid_n * BLOCK_N + + # inductor generates a suffix + {%- if TMA_EXPERIMENTAL_API %} + rcm = rm + tl.arange(0, BLOCK_M) + rcn = rn + tl.arange(0, BLOCK_N) + idx_m = rcm[:, None] + idx_n = rcn[None, :] + mask = (idx_m < M) & (idx_n < N) + {{store_output(("idx_m", "idx_n"), "acc", "mask", indent_width=8, val_shape=("BLOCK_M", "BLOCK_N"))}} + {%- else %} + {{store_output(("rm", "rn"), "acc", indent_width=8, val_shape=("BLOCK_M", "BLOCK_N"), block_indexing=True)}} + {%- endif %} diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/vendored_templates/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/vendored_templates/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/vendored_templates/cutedsl/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/vendored_templates/cutedsl/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d7dc51ed6f633ef7b4329b51d80c5dd4a1caa52c --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/vendored_templates/cutedsl/__init__.py @@ -0,0 +1 @@ +# CuTeDSL vendored kernels and wrappers for PyTorch Inductor. diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/vendored_templates/cutedsl/dense_blockscaled_gemm_persistent.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/vendored_templates/cutedsl/dense_blockscaled_gemm_persistent.py new file mode 100644 index 0000000000000000000000000000000000000000..91188fb97e107d625d1bac740c28feb9f6a88e7d --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/vendored_templates/cutedsl/dense_blockscaled_gemm_persistent.py @@ -0,0 +1,2156 @@ +# Copyright (c) 2025 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import argparse +from typing import Tuple, Type, Union + +import cuda.bindings.driver as cuda + +import cutlass +import cutlass.cute as cute +import cutlass.pipeline as pipeline +import cutlass.torch as cutlass_torch +import cutlass.utils as utils +import cutlass.utils.blackwell_helpers as sm100_utils +import cutlass.utils.blockscaled_layout as blockscaled_utils +from cutlass.cute.nvgpu import cpasync, tcgen05 +from cutlass.cute.runtime import from_dlpack +from cutlass.pipeline import pipeline_init_arrive, pipeline_init_wait + +import torch + + +""" +This example provides an experimental implementation of the SM100 batched dense blockscaled GEMM kernel, please note that the APIs and implementation details related to this kernel may change in future releases. + +A high-performance persistent batched dense blockscaled GEMM example for the NVIDIA Blackwell SM100 architecture +using CUTE DSL. +- Matrix A is MxKxL, L is batch dimension, A can be row-major("K") or column-major("M") for MXF8 input type and can only be row-major("K") for MXF4/NVF4 input type +- Matrix B is NxKxL, L is batch dimension, B can be row-major("N") or column-major("K") for MXF8 input type and can only be row-major("K") for MXF4/NVF4 input type +- Matrix C is MxNxL, L is batch dimension, C can be row-major("N") or column-major("M") +- Matrix SFA layout is filled internally according to A shape and BlockScaledBasicChunk, which has M×ceil_div(K, sf_vec_size)×L elements respectively +- Matrix SFB layout is filled internally according to B shape and BlockScaledBasicChunk, which has N×ceil_div(K, sf_vec_size)×L elements respectively + +This GEMM kernel supports the following features: + - Utilizes Tensor Memory Access (TMA) for efficient memory operations + - Utilizes Blackwell's tcgen05.mma for matrix multiply-accumulate (MMA) operations (including 2cta mma instructions) + - Implements TMA multicast with cluster to reduce L2 memory traffic + - Support persistent tile scheduling to better overlap memory load/store with mma between tiles + - Support warp specialization to avoid explicit pipelining between mainloop load and mma + +This GEMM works as follows: +1. DMA warp: Load A and B matrices from global memory (GMEM) to shared memory (SMEM) using TMA operations. +2. MMA warp: + - Load scale factor A/B from shared memory (SMEM) to tensor memory (TMEM) using tcgen05.cp instruction. + - Perform matrix multiply-accumulate (MMA) operations using tcgen05.mma instruction. +3. EPILOGUE warp: + - Load completed accumulator from tensor memory (TMEM) to registers (RMEM) using tcgen05.ld. + - Type convert C matrix to output type. + - Optionally store C matrix from registers (RMEM) to shared memory (SMEM) to global memory (GMEM) with TMA operations, + or directly store C matrix from registers (RMEM) to global memory (GMEM) without TMA operations. + - Optionally accept an elementwise lambda function epilogue_op to apply to the output tensor: + e.g., relu can set epilogue_op = lambda x: cute.where(x > 0, x, cute.full_like(x, 0)) + +SM100 tcgen05.mma.kind.block_scale instructions operate as follows: +- Read matrix A from SMEM +- Read matrix B from SMEM +- Read scalefactor A from TMEM +- Read scalefactor B from TMEM +- Write accumulator to TMEM +The accumulator in TMEM must then be loaded to registers before writing back to GMEM. + +Input arguments to this example is shown below: + +.. code-block:: bash + + python examples/blackwell/dense_blockscaled_gemm_persistent.py \ + --ab_dtype Float4E2M1FN --sf_dtype Float8E8M0FNU --sf_vec_size 16 \ + --c_dtype Float16 \ + --mma_tiler_mn 256,128 --cluster_shape_mn 2,1 \ + --mnkl 8192,8192,1024,1 + +To collect performance with NCU profiler: + +.. code-block:: bash + + ncu python examples/blackwell/dense_blockscaled_gemm_persistent.py \ + --ab_dtype Float4E2M1FN --sf_dtype Float8E8M0FNU --sf_vec_size 16 \ + --c_dtype Float16 \ + --mma_tiler_mn 256,128 --cluster_shape_mn 2,1 \ + --mnkl 8192,8192,1024,1 \ + --warmup_iterations 1 --iterations 10 --skip_ref_check + + +Constraints: +* Supported input data types: mxf8, mxf4, nvf4 + see detailed valid dtype combinations in below Sm100BlockScaledPersistentDenseGemmKernel class documentation +* A/B tensor must have the same data type, mixed data type is not supported (e.g., mxf8 x mxf4) +* Mma tiler M must be 128 or 256(use_2cta_instrs) +* Mma tiler N must be 64/128/192/256 +* Cluster shape M/N must be positive and power of 2, total cluster size <= 16 +* Cluster shape M must be multiple of 2 if Mma tiler M is 256(use_2cta_instrs) +* The contiguous dimension of A/B/C tensors must be at least 16 bytes aligned, + i.e, number of elements is a multiple of 16 and 32 for Float8 and Float4, respectively. +""" + + +class Sm100BlockScaledPersistentDenseGemmKernel: + """This class implements batched matrix multiplication (C = A x SFA x B x SFB) with support for various data types + and architectural features specific to Blackwell GPUs with persistent tile scheduling and warp specialization. + + :param sf_vec_size: Scalefactor vector size. + :type sf_vec_size: int + :param mma_tiler_mn: Shape of the Matrix Multiply-Accumulate (MMA) tile (M,N) + :type mma_tiler_mn: Tuple[int, int] + :param cluster_shape_mn: Cluster dimensions (M,N) for parallel processing + :type cluster_shape_mn: Tuple[int, int] + + :note: In current version, A and B tensor must have the same data type + - i.e., Float8E4M3FN for A and Float8E5M2 for B is not supported + + :note: Supported combinations of A/B data types, SF data typs and SF vector size: + - MXF8: A/B: Float8E5M2/Float8E4M3FN + SF: Float8E8M0FNU + sf_vec_size: 32 + - MXF4: A/B: Float4E2M1FN + SF: Float8E8M0FNU + sf_vec_size: 32 + - NVF4: A/B: Float4E2M1FN + SF: Float8E8M0FNU/Float8E4M3FN + sf_vec_size: 16 + + :note: Supported accumulator data types: + - Float32 + + :note: Supported C data types: + - Float32 + - Float16/BFloat16 + - Float8E4M3FN/Float8E5M2 + :note: Constraints: + - MMA tiler M must be 128 or 256 (use_2cta_instrs) + - MMA tiler N must be 64/128/192/256 + - Cluster shape M must be multiple of 2 if Mma tiler M is 256 + - Cluster shape M/N must be positive and power of 2, total cluster size <= 16 + - Also, Cluster shape M/N must be <= 4 for scale factor multicasts due to limited size of scale factors + + Example: + >>> # xdoctest: +SKIP(requires cutlass) + >>> gemm = Sm100BlockScaledPersistentDenseGemmKernel( + ... sf_vec_size=16, mma_tiler_mn=(256, 128), cluster_shape_mn=(2, 1) + ... ) + >>> gemm( + ... a_tensor, + ... b_tensor, + ... sfa_tensor, + ... sfb_tensor, + ... c_tensor, + ... max_active_clusters, + ... stream, + ... ) + """ + + def __init__( + self, + sf_vec_size: int, + mma_tiler_mn: Tuple[int, int], + cluster_shape_mn: Tuple[int, int], + ): + """Initializes the configuration for a Blackwell dense GEMM kernel. + + This configuration includes several key aspects: + + 1. MMA Instruction Settings (tcgen05): + - acc_dtype: Data types for MMA accumulator, always set to Float32 + - sf_vec_size: Scalefactor A/B vector size. + - mma_tiler_mn: The (M, N) shape of the MMA instruction tiler. + + 2. Cluster Shape: + - cluster_shape_mn: The (ClusterM, ClusterN) shape of the CTA cluster. + + :param sf_vec_size: Scalefactor vector size. + :type sf_vec_size: int + :param mma_tiler_mn: Tuple (M, N) shape of the MMA instruction. + :type mma_tiler_mn: Tuple[int, int] + :param cluster_shape_mn: Tuple (ClusterM, ClusterN) shape of the cluster. + :type cluster_shape_mn: Tuple[int, int] + """ + + self.acc_dtype = cutlass.Float32 + self.sf_vec_size = sf_vec_size + self.use_2cta_instrs = mma_tiler_mn[0] == 256 + self.cluster_shape_mn = cluster_shape_mn + # K dimension is deferred in _setup_attributes + self.mma_tiler = (*mma_tiler_mn, 1) + + self.cta_group = ( + tcgen05.CtaGroup.TWO if self.use_2cta_instrs else tcgen05.CtaGroup.ONE + ) + + self.occupancy = 1 + # Set specialized warp ids + self.epilog_warp_id = ( + 0, + 1, + 2, + 3, + ) + self.mma_warp_id = 4 + self.tma_warp_id = 5 + self.threads_per_cta = 32 * len( + (self.mma_warp_id, self.tma_warp_id, *self.epilog_warp_id) + ) + # Set barrier id for epilogue sync and tmem ptr sync + self.epilog_sync_barrier = pipeline.NamedBarrier( + barrier_id=1, + num_threads=32 * len(self.epilog_warp_id), + ) + self.tmem_alloc_barrier = pipeline.NamedBarrier( + barrier_id=2, + num_threads=32 * len((self.mma_warp_id, *self.epilog_warp_id)), + ) + self.smem_capacity = utils.get_smem_capacity_in_bytes("sm_100") + SM100_TMEM_CAPACITY_COLUMNS = 512 + self.num_tmem_alloc_cols = SM100_TMEM_CAPACITY_COLUMNS + + def _setup_attributes(self): + """Set up configurations that are dependent on GEMM inputs + + This method configures various attributes based on the input tensor properties + (data types, leading dimensions) and kernel settings: + - Configuring tiled MMA + - Computing MMA/cluster/tile shapes + - Computing cluster layout + - Computing multicast CTAs for A/B/SFA/SFB + - Computing epilogue subtile + - Setting up A/B/SFA/SFB/C stage counts in shared memory + - Computing A/B/SFA/SFB/C shared memory layout + """ + # Compute mma instruction shapes + # (MMA_Tile_Shape_M, MMA_Tile_Shape_N, MMA_Inst_Shape_K) + self.mma_inst_shape_mn = ( + self.mma_tiler[0], + self.mma_tiler[1], + ) + # (CTA_Tile_Shape_M, Round_Up(MMA_Tile_Shape_N, 128), MMA_Inst_Shape_K) + self.mma_inst_shape_mn_sfb = ( + self.mma_inst_shape_mn[0] // (2 if self.use_2cta_instrs else 1), + cute.round_up(self.mma_inst_shape_mn[1], 128), + ) + + tiled_mma = sm100_utils.make_blockscaled_trivial_tiled_mma( + self.a_dtype, + self.a_major_mode, + self.b_major_mode, + self.sf_dtype, + self.sf_vec_size, + self.cta_group, + self.mma_inst_shape_mn, + ) + + tiled_mma_sfb = sm100_utils.make_blockscaled_trivial_tiled_mma( + self.a_dtype, + self.a_major_mode, + self.b_major_mode, + self.sf_dtype, + self.sf_vec_size, + cute.nvgpu.tcgen05.CtaGroup.ONE, + self.mma_inst_shape_mn_sfb, + ) + + # Compute mma/cluster/tile shapes + mma_inst_shape_k = cute.size(tiled_mma.shape_mnk, mode=[2]) + mma_inst_tile_k = 4 + self.mma_tiler = ( + self.mma_inst_shape_mn[0], + self.mma_inst_shape_mn[1], + mma_inst_shape_k * mma_inst_tile_k, + ) + self.mma_tiler_sfb = ( + self.mma_inst_shape_mn_sfb[0], + self.mma_inst_shape_mn_sfb[1], + mma_inst_shape_k * mma_inst_tile_k, + ) + self.cta_tile_shape_mnk = ( + self.mma_tiler[0] // cute.size(tiled_mma.thr_id.shape), + self.mma_tiler[1], + self.mma_tiler[2], + ) + self.cta_tile_shape_mnk_sfb = ( + self.mma_tiler_sfb[0] // cute.size(tiled_mma.thr_id.shape), + self.mma_tiler_sfb[1], + self.mma_tiler_sfb[2], + ) + + # Compute cluster layout + self.cluster_layout_vmnk = cute.tiled_divide( + cute.make_layout((*self.cluster_shape_mn, 1)), + (tiled_mma.thr_id.shape,), + ) + self.cluster_layout_sfb_vmnk = cute.tiled_divide( + cute.make_layout((*self.cluster_shape_mn, 1)), + (tiled_mma_sfb.thr_id.shape,), + ) + + # Compute number of multicast CTAs for A/B + self.num_mcast_ctas_a = cute.size(self.cluster_layout_vmnk.shape[2]) + self.num_mcast_ctas_b = cute.size(self.cluster_layout_vmnk.shape[1]) + self.num_mcast_ctas_sfb = cute.size(self.cluster_layout_sfb_vmnk.shape[1]) + self.is_a_mcast = self.num_mcast_ctas_a > 1 + self.is_b_mcast = self.num_mcast_ctas_b > 1 + self.is_sfb_mcast = self.num_mcast_ctas_sfb > 1 + + # Compute epilogue subtile + self.epi_tile = sm100_utils.compute_epilogue_tile_shape( + self.cta_tile_shape_mnk, + self.use_2cta_instrs, + self.c_layout, + self.c_dtype, + ) + self.epi_tile_n = cute.size(self.epi_tile[1]) + + # Setup A/B/C stage count in shared memory and ACC stage count in tensor memory + self.num_acc_stage, self.num_ab_stage, self.num_c_stage = self._compute_stages( + tiled_mma, + self.mma_tiler, + self.a_dtype, + self.b_dtype, + self.epi_tile, + self.c_dtype, + self.c_layout, + self.sf_dtype, + self.sf_vec_size, + self.smem_capacity, + self.occupancy, + ) + + # Compute A/B/SFA/SFB/C shared memory layout + self.a_smem_layout_staged = sm100_utils.make_smem_layout_a( + tiled_mma, + self.mma_tiler, + self.a_dtype, + self.num_ab_stage, + ) + self.b_smem_layout_staged = sm100_utils.make_smem_layout_b( + tiled_mma, + self.mma_tiler, + self.b_dtype, + self.num_ab_stage, + ) + self.sfa_smem_layout_staged = blockscaled_utils.make_smem_layout_sfa( + tiled_mma, + self.mma_tiler, + self.sf_vec_size, + self.num_ab_stage, + ) + self.sfb_smem_layout_staged = blockscaled_utils.make_smem_layout_sfb( + tiled_mma, + self.mma_tiler, + self.sf_vec_size, + self.num_ab_stage, + ) + self.c_smem_layout_staged = sm100_utils.make_smem_layout_epi( + self.c_dtype, + self.c_layout, + self.epi_tile, + self.num_c_stage, + ) + + # Overlap and double buffer accumulator when num_acc_stage == 1 for cta_tile_n = 256 case + self.overlapping_accum = self.num_acc_stage == 1 + + # Compute number of TMEM columns for SFA/SFB/Accumulator + sf_atom_mn = 32 + self.num_sfa_tmem_cols = ( + self.cta_tile_shape_mnk[0] // sf_atom_mn + ) * mma_inst_tile_k + self.num_sfb_tmem_cols = ( + self.cta_tile_shape_mnk_sfb[1] // sf_atom_mn + ) * mma_inst_tile_k + self.num_sf_tmem_cols = self.num_sfa_tmem_cols + self.num_sfb_tmem_cols + self.num_accumulator_tmem_cols = ( + self.cta_tile_shape_mnk[1] * self.num_acc_stage + if not self.overlapping_accum + else self.cta_tile_shape_mnk[1] * 2 - self.num_sf_tmem_cols + ) + + # Only when overlapping_accum is enabled, we need to release accumulator buffer early in epilogue + self.iter_acc_early_release_in_epilogue = ( + self.num_sf_tmem_cols // self.epi_tile_n + ) + + @cute.jit + def __call__( + self, + a_tensor: cute.Tensor, + b_tensor: cute.Tensor, + sfa_tensor: cute.Tensor, + sfb_tensor: cute.Tensor, + c_tensor: cute.Tensor, + max_active_clusters: cutlass.Constexpr, + stream: cuda.CUstream, + epilogue_op: cutlass.Constexpr = lambda x: x, + ): + """Execute the GEMM operation in steps: + - Setup static attributes before smem/grid/tma computation + - Setup TMA load/store atoms and tensors + - Compute grid size with regard to hardware constraints + - Define shared storage for kernel + - Launch the kernel synchronously + + :param a_tensor: Input tensor A + :type a_tensor: cute.Tensor + :param b_tensor: Input tensor B + :type b_tensor: cute.Tensor + :param sfa_tensor: Scale factor tensor A + :type sfa_tensor: cute.Tensor + :param sfb_tensor: Scale factor tensor B + :type sfb_tensor: cute.Tensor + :param c_tensor: Output tensor C + :type c_tensor: cute.Tensor + :param max_active_clusters: Maximum number of active clusters + :type max_active_clusters: cutlass.Constexpr + :param stream: CUDA stream for asynchronous execution + :type stream: cuda.CUstream + :param epilogue_op: Optional elementwise lambda function to apply to the output tensor + :type epilogue_op: cutlass.Constexpr + :raises TypeError: If input data types are incompatible with the MMA instruction. + """ + + # Convert from torch convention to CuTe convention. + # CUTLASS API passes tensors as A:(L,M,K) B:(L,K,N) C:(L,M,N) + # but CuTe DSL kernels expect A:(M,K,L) B:(N,K,L) C:(M,N,L). + def add_batch_mode(tensor: cute.Tensor) -> cute.Tensor: + return cute.make_tensor( + tensor.iterator, + cute.prepend(tensor.layout, cute.make_layout(1), up_to_rank=3), + ) + + a_tensor = add_batch_mode(a_tensor) + b_tensor = add_batch_mode(b_tensor) + c_tensor = add_batch_mode(c_tensor) + + a_tensor = cute.make_tensor( + a_tensor.iterator, cute.select(a_tensor.layout, [1, 2, 0]) + ) + b_tensor = cute.make_tensor( + b_tensor.iterator, cute.select(b_tensor.layout, [2, 1, 0]) + ) + c_tensor = cute.make_tensor( + c_tensor.iterator, cute.select(c_tensor.layout, [1, 2, 0]) + ) + + # Setup static attributes before smem/grid/tma computation + self.a_dtype: Type[cutlass.Numeric] = a_tensor.element_type + self.b_dtype: Type[cutlass.Numeric] = b_tensor.element_type + self.sf_dtype: Type[cutlass.Numeric] = sfa_tensor.element_type + self.c_dtype: Type[cutlass.Numeric] = c_tensor.element_type + self.a_major_mode = utils.LayoutEnum.from_tensor(a_tensor).mma_major_mode() + self.b_major_mode = utils.LayoutEnum.from_tensor(b_tensor).mma_major_mode() + self.c_layout = utils.LayoutEnum.from_tensor(c_tensor) + + # Check if input data types are compatible with MMA instruction + if cutlass.const_expr(self.a_dtype != self.b_dtype): + raise TypeError(f"Type must match: {self.a_dtype} != {self.b_dtype}") + + # Setup attributes that dependent on gemm inputs + self._setup_attributes() + + # Setup sfa/sfb tensor by filling A/B tensor to scale factor atom layout + # ((Atom_M, Rest_M),(Atom_K, Rest_K),RestL) + sfa_layout = blockscaled_utils.tile_atom_to_shape_SF( + a_tensor.shape, self.sf_vec_size + ) + sfa_tensor = cute.make_tensor(sfa_tensor.iterator, sfa_layout) + + # ((Atom_N, Rest_N),(Atom_K, Rest_K),RestL) + sfb_layout = blockscaled_utils.tile_atom_to_shape_SF( + b_tensor.shape, self.sf_vec_size + ) + sfb_tensor = cute.make_tensor(sfb_tensor.iterator, sfb_layout) + + tiled_mma = sm100_utils.make_blockscaled_trivial_tiled_mma( + self.a_dtype, + self.a_major_mode, + self.b_major_mode, + self.sf_dtype, + self.sf_vec_size, + self.cta_group, + self.mma_inst_shape_mn, + ) + + tiled_mma_sfb = sm100_utils.make_blockscaled_trivial_tiled_mma( + self.a_dtype, + self.a_major_mode, + self.b_major_mode, + self.sf_dtype, + self.sf_vec_size, + cute.nvgpu.tcgen05.CtaGroup.ONE, + self.mma_inst_shape_mn_sfb, + ) + atom_thr_size = cute.size(tiled_mma.thr_id.shape) + + # Setup TMA load for A + a_op = sm100_utils.cluster_shape_to_tma_atom_A( + self.cluster_shape_mn, tiled_mma.thr_id + ) + a_smem_layout = cute.slice_(self.a_smem_layout_staged, (None, None, None, 0)) + tma_atom_a, tma_tensor_a = cute.nvgpu.make_tiled_tma_atom_A( + a_op, + a_tensor, + a_smem_layout, + self.mma_tiler, + tiled_mma, + self.cluster_layout_vmnk.shape, + ) + + # Setup TMA load for B + b_op = sm100_utils.cluster_shape_to_tma_atom_B( + self.cluster_shape_mn, tiled_mma.thr_id + ) + b_smem_layout = cute.slice_(self.b_smem_layout_staged, (None, None, None, 0)) + tma_atom_b, tma_tensor_b = cute.nvgpu.make_tiled_tma_atom_B( + b_op, + b_tensor, + b_smem_layout, + self.mma_tiler, + tiled_mma, + self.cluster_layout_vmnk.shape, + ) + + # Setup TMA load for SFA + sfa_op = sm100_utils.cluster_shape_to_tma_atom_A( + self.cluster_shape_mn, tiled_mma.thr_id + ) + sfa_smem_layout = cute.slice_( + self.sfa_smem_layout_staged, (None, None, None, 0) + ) + tma_atom_sfa, tma_tensor_sfa = cute.nvgpu.make_tiled_tma_atom_A( + sfa_op, + sfa_tensor, + sfa_smem_layout, + self.mma_tiler, + tiled_mma, + self.cluster_layout_vmnk.shape, + internal_type=cutlass.Int16, + ) + + # Setup TMA load for SFB + sfb_op = sm100_utils.cluster_shape_to_tma_atom_SFB( + self.cluster_shape_mn, tiled_mma.thr_id + ) + sfb_smem_layout = cute.slice_( + self.sfb_smem_layout_staged, (None, None, None, 0) + ) + tma_atom_sfb, tma_tensor_sfb = cute.nvgpu.make_tiled_tma_atom_B( + sfb_op, + sfb_tensor, + sfb_smem_layout, + self.mma_tiler_sfb, + tiled_mma_sfb, + self.cluster_layout_sfb_vmnk.shape, + internal_type=cutlass.Int16, + ) + + if cutlass.const_expr(self.cta_tile_shape_mnk[1] == 192): + x = tma_tensor_sfb.stride[0][1] + y = cute.ceil_div(tma_tensor_sfb.shape[0][1], 4) + + new_shape = ( + (tma_tensor_sfb.shape[0][0], ((2, 2), y)), + tma_tensor_sfb.shape[1], + tma_tensor_sfb.shape[2], + ) + # Use right multiplication for ScaledBasis (3 * x instead of x * 3) + x_times_3 = 3 * x + new_stride = ( + (tma_tensor_sfb.stride[0][0], ((x, x), x_times_3)), + tma_tensor_sfb.stride[1], + tma_tensor_sfb.stride[2], + ) + tma_tensor_sfb_new_layout = cute.make_layout(new_shape, stride=new_stride) + tma_tensor_sfb = cute.make_tensor( + tma_tensor_sfb.iterator, tma_tensor_sfb_new_layout + ) + + a_copy_size = cute.size_in_bytes(self.a_dtype, a_smem_layout) + b_copy_size = cute.size_in_bytes(self.b_dtype, b_smem_layout) + sfa_copy_size = cute.size_in_bytes(self.sf_dtype, sfa_smem_layout) + sfb_copy_size = cute.size_in_bytes(self.sf_dtype, sfb_smem_layout) + self.num_tma_load_bytes = ( + a_copy_size + b_copy_size + sfa_copy_size + sfb_copy_size + ) * atom_thr_size + + # Setup TMA store for C + epi_smem_layout = cute.slice_(self.c_smem_layout_staged, (None, None, 0)) + tma_atom_c, tma_tensor_c = cpasync.make_tiled_tma_atom( + cpasync.CopyBulkTensorTileS2GOp(), + c_tensor, + epi_smem_layout, + self.epi_tile, + ) + + # Compute grid size + self.tile_sched_params, grid = self._compute_grid( + c_tensor, + self.cta_tile_shape_mnk, + self.cluster_shape_mn, + max_active_clusters, + ) + + self.buffer_align_bytes = 1024 + + # Define shared storage for kernel + @cute.struct + class SharedStorage: + ab_full_mbar_ptr: cute.struct.MemRange[cutlass.Int64, self.num_ab_stage] + ab_empty_mbar_ptr: cute.struct.MemRange[cutlass.Int64, self.num_ab_stage] + acc_full_mbar_ptr: cute.struct.MemRange[cutlass.Int64, self.num_acc_stage] + acc_empty_mbar_ptr: cute.struct.MemRange[cutlass.Int64, self.num_acc_stage] + tmem_dealloc_mbar_ptr: cutlass.Int64 + tmem_holding_buf: cutlass.Int32 + # (EPI_TILE_M, EPI_TILE_N, STAGE) + sC: cute.struct.Align[ + cute.struct.MemRange[ + self.c_dtype, + cute.cosize(self.c_smem_layout_staged.outer), + ], + self.buffer_align_bytes, + ] + # (MMA, MMA_M, MMA_K, STAGE) + sA: cute.struct.Align[ + cute.struct.MemRange[ + self.a_dtype, cute.cosize(self.a_smem_layout_staged.outer) + ], + self.buffer_align_bytes, + ] + # (MMA, MMA_N, MMA_K, STAGE) + sB: cute.struct.Align[ + cute.struct.MemRange[ + self.b_dtype, cute.cosize(self.b_smem_layout_staged.outer) + ], + self.buffer_align_bytes, + ] + # (MMA, MMA_M, MMA_K, STAGE) + sSFA: cute.struct.Align[ + cute.struct.MemRange[ + self.sf_dtype, cute.cosize(self.sfa_smem_layout_staged) + ], + self.buffer_align_bytes, + ] + # (MMA, MMA_N, MMA_K, STAGE) + sSFB: cute.struct.Align[ + cute.struct.MemRange[ + self.sf_dtype, cute.cosize(self.sfb_smem_layout_staged) + ], + self.buffer_align_bytes, + ] + + self.shared_storage = SharedStorage + + # Launch the kernel synchronously + self.kernel( + tiled_mma, + tiled_mma_sfb, + tma_atom_a, + tma_tensor_a, + tma_atom_b, + tma_tensor_b, + tma_atom_sfa, + tma_tensor_sfa, + tma_atom_sfb, + tma_tensor_sfb, + tma_atom_c, + tma_tensor_c, + self.cluster_layout_vmnk, + self.cluster_layout_sfb_vmnk, + self.a_smem_layout_staged, + self.b_smem_layout_staged, + self.sfa_smem_layout_staged, + self.sfb_smem_layout_staged, + self.c_smem_layout_staged, + self.epi_tile, + self.tile_sched_params, + epilogue_op, + ).launch( + grid=grid, + block=[self.threads_per_cta, 1, 1], + cluster=(*self.cluster_shape_mn, 1), + stream=stream, + min_blocks_per_mp=1, + ) + return + + # GPU device kernel + @cute.kernel + def kernel( + self, + tiled_mma: cute.TiledMma, + tiled_mma_sfb: cute.TiledMma, + tma_atom_a: cute.CopyAtom, + mA_mkl: cute.Tensor, + tma_atom_b: cute.CopyAtom, + mB_nkl: cute.Tensor, + tma_atom_sfa: cute.CopyAtom, + mSFA_mkl: cute.Tensor, + tma_atom_sfb: cute.CopyAtom, + mSFB_nkl: cute.Tensor, + tma_atom_c: cute.CopyAtom, + mC_mnl: cute.Tensor, + cluster_layout_vmnk: cute.Layout, + cluster_layout_sfb_vmnk: cute.Layout, + a_smem_layout_staged: cute.ComposedLayout, + b_smem_layout_staged: cute.ComposedLayout, + sfa_smem_layout_staged: cute.Layout, + sfb_smem_layout_staged: cute.Layout, + c_smem_layout_staged: Union[cute.Layout, cute.ComposedLayout], + epi_tile: cute.Tile, + tile_sched_params: utils.PersistentTileSchedulerParams, + epilogue_op: cutlass.Constexpr, + ): + """ + GPU device kernel performing the Persistent batched GEMM computation. + """ + warp_idx = cute.arch.warp_idx() + warp_idx = cute.arch.make_warp_uniform(warp_idx) + + # + # Prefetch tma desc + # + if warp_idx == self.tma_warp_id: + cpasync.prefetch_descriptor(tma_atom_a) + cpasync.prefetch_descriptor(tma_atom_b) + cpasync.prefetch_descriptor(tma_atom_sfa) + cpasync.prefetch_descriptor(tma_atom_sfb) + cpasync.prefetch_descriptor(tma_atom_c) + + use_2cta_instrs = cute.size(tiled_mma.thr_id.shape) == 2 + + # + # Setup cta/thread coordinates + # + # Coords inside cluster + bidx, bidy, bidz = cute.arch.block_idx() + mma_tile_coord_v = bidx % cute.size(tiled_mma.thr_id.shape) + is_leader_cta = mma_tile_coord_v == 0 + cta_rank_in_cluster = cute.arch.make_warp_uniform( + cute.arch.block_idx_in_cluster() + ) + block_in_cluster_coord_vmnk = cluster_layout_vmnk.get_flat_coord( + cta_rank_in_cluster + ) + block_in_cluster_coord_sfb_vmnk = cluster_layout_sfb_vmnk.get_flat_coord( + cta_rank_in_cluster + ) + # Coord inside cta + tidx, _, _ = cute.arch.thread_idx() + + # + # Alloc and init: a+b full/empty, accumulator full/empty, tensor memory dealloc barrier + # + smem = utils.SmemAllocator() + storage = smem.allocate(self.shared_storage) + + # Initialize mainloop ab_pipeline (barrier) and states + ab_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread) + num_tma_producer = self.num_mcast_ctas_a + self.num_mcast_ctas_b - 1 + ab_pipeline_consumer_group = pipeline.CooperativeGroup( + pipeline.Agent.Thread, num_tma_producer + ) + ab_pipeline = pipeline.PipelineTmaUmma.create( + barrier_storage=storage.ab_full_mbar_ptr.data_ptr(), + num_stages=self.num_ab_stage, + producer_group=ab_pipeline_producer_group, + consumer_group=ab_pipeline_consumer_group, + tx_count=self.num_tma_load_bytes, + cta_layout_vmnk=cluster_layout_vmnk, + defer_sync=True, + ) + + # Initialize acc_pipeline (barrier) and states + acc_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread) + num_acc_consumer_threads = len(self.epilog_warp_id) * ( + 2 if use_2cta_instrs else 1 + ) + acc_pipeline_consumer_group = pipeline.CooperativeGroup( + pipeline.Agent.Thread, num_acc_consumer_threads + ) + acc_pipeline = pipeline.PipelineUmmaAsync.create( + barrier_storage=storage.acc_full_mbar_ptr.data_ptr(), + num_stages=self.num_acc_stage, + producer_group=acc_pipeline_producer_group, + consumer_group=acc_pipeline_consumer_group, + cta_layout_vmnk=cluster_layout_vmnk, + defer_sync=True, + ) + + # Tensor memory dealloc barrier init + tmem = utils.TmemAllocator( + storage.tmem_holding_buf, + barrier_for_retrieve=self.tmem_alloc_barrier, + allocator_warp_id=self.epilog_warp_id[0], + is_two_cta=use_2cta_instrs, + two_cta_tmem_dealloc_mbar_ptr=storage.tmem_dealloc_mbar_ptr, + ) + + # Cluster arrive after barrier init + pipeline_init_arrive(cluster_shape_mn=self.cluster_shape_mn, is_relaxed=True) + + # + # Setup smem tensor A/B/SFA/SFB/C + # + # (EPI_TILE_M, EPI_TILE_N, STAGE) + sC = storage.sC.get_tensor( + c_smem_layout_staged.outer, swizzle=c_smem_layout_staged.inner + ) + # (MMA, MMA_M, MMA_K, STAGE) + sA = storage.sA.get_tensor( + a_smem_layout_staged.outer, swizzle=a_smem_layout_staged.inner + ) + # (MMA, MMA_N, MMA_K, STAGE) + sB = storage.sB.get_tensor( + b_smem_layout_staged.outer, swizzle=b_smem_layout_staged.inner + ) + # (MMA, MMA_M, MMA_K, STAGE) + sSFA = storage.sSFA.get_tensor(sfa_smem_layout_staged) + # (MMA, MMA_N, MMA_K, STAGE) + sSFB = storage.sSFB.get_tensor(sfb_smem_layout_staged) + + # + # Compute multicast mask for A/B/SFA/SFB buffer full + # + a_full_mcast_mask = None + b_full_mcast_mask = None + sfa_full_mcast_mask = None + sfb_full_mcast_mask = None + if cutlass.const_expr(self.is_a_mcast or self.is_b_mcast or use_2cta_instrs): + a_full_mcast_mask = cpasync.create_tma_multicast_mask( + cluster_layout_vmnk, block_in_cluster_coord_vmnk, mcast_mode=2 + ) + b_full_mcast_mask = cpasync.create_tma_multicast_mask( + cluster_layout_vmnk, block_in_cluster_coord_vmnk, mcast_mode=1 + ) + sfa_full_mcast_mask = cpasync.create_tma_multicast_mask( + cluster_layout_vmnk, block_in_cluster_coord_vmnk, mcast_mode=2 + ) + sfb_full_mcast_mask = cpasync.create_tma_multicast_mask( + cluster_layout_sfb_vmnk, block_in_cluster_coord_sfb_vmnk, mcast_mode=1 + ) + + # + # Local_tile partition global tensors + # + # (bM, bK, RestM, RestK, RestL) + gA_mkl = cute.local_tile( + mA_mkl, cute.slice_(self.mma_tiler, (None, 0, None)), (None, None, None) + ) + # (bN, bK, RestN, RestK, RestL) + gB_nkl = cute.local_tile( + mB_nkl, cute.slice_(self.mma_tiler, (0, None, None)), (None, None, None) + ) + # (bM, bK, RestM, RestK, RestL) + gSFA_mkl = cute.local_tile( + mSFA_mkl, cute.slice_(self.mma_tiler, (None, 0, None)), (None, None, None) + ) + # (bN, bK, RestN, RestK, RestL) + gSFB_nkl = cute.local_tile( + mSFB_nkl, + cute.slice_(self.mma_tiler_sfb, (0, None, None)), + (None, None, None), + ) + # (bM, bN, RestM, RestN, RestL) + gC_mnl = cute.local_tile( + mC_mnl, cute.slice_(self.mma_tiler, (None, None, 0)), (None, None, None) + ) + k_tile_cnt = cute.size(gA_mkl, mode=[3]) + + # + # Partition global tensor for TiledMMA_A/B/C + # + thr_mma = tiled_mma.get_slice(mma_tile_coord_v) + thr_mma_sfb = tiled_mma_sfb.get_slice(mma_tile_coord_v) + # (MMA, MMA_M, MMA_K, RestM, RestK, RestL) + tCgA = thr_mma.partition_A(gA_mkl) + # (MMA, MMA_N, MMA_K, RestN, RestK, RestL) + tCgB = thr_mma.partition_B(gB_nkl) + # (MMA, MMA_M, MMA_K, RestM, RestK, RestL) + tCgSFA = thr_mma.partition_A(gSFA_mkl) + # (MMA, MMA_N, MMA_K, RestN, RestK, RestL) + tCgSFB = thr_mma_sfb.partition_B(gSFB_nkl) + # (MMA, MMA_M, MMA_N, RestM, RestN, RestL) + tCgC = thr_mma.partition_C(gC_mnl) + + # + # Partition global/shared tensor for TMA load A/B + # + # TMA load A partition_S/D + a_cta_layout = cute.make_layout( + cute.slice_(cluster_layout_vmnk, (0, 0, None, 0)).shape + ) + # ((atom_v, rest_v), STAGE) + # ((atom_v, rest_v), RestM, RestK, RestL) + tAsA, tAgA = cpasync.tma_partition( + tma_atom_a, + block_in_cluster_coord_vmnk[2], + a_cta_layout, + cute.group_modes(sA, 0, 3), + cute.group_modes(tCgA, 0, 3), + ) + # TMA load B partition_S/D + b_cta_layout = cute.make_layout( + cute.slice_(cluster_layout_vmnk, (0, None, 0, 0)).shape + ) + # ((atom_v, rest_v), STAGE) + # ((atom_v, rest_v), RestN, RestK, RestL) + tBsB, tBgB = cpasync.tma_partition( + tma_atom_b, + block_in_cluster_coord_vmnk[1], + b_cta_layout, + cute.group_modes(sB, 0, 3), + cute.group_modes(tCgB, 0, 3), + ) + + # TMA load SFA partition_S/D + sfa_cta_layout = a_cta_layout + # ((atom_v, rest_v), STAGE) + # ((atom_v, rest_v), RestM, RestK, RestL) + tAsSFA, tAgSFA = cute.nvgpu.cpasync.tma_partition( + tma_atom_sfa, + block_in_cluster_coord_vmnk[2], + sfa_cta_layout, + cute.group_modes(sSFA, 0, 3), + cute.group_modes(tCgSFA, 0, 3), + ) + tAsSFA = cute.filter_zeros(tAsSFA) + tAgSFA = cute.filter_zeros(tAgSFA) + + # TMA load SFB partition_S/D + sfb_cta_layout = cute.make_layout( + cute.slice_(cluster_layout_sfb_vmnk, (0, None, 0, 0)).shape + ) + # ((atom_v, rest_v), STAGE) + # ((atom_v, rest_v), RestN, RestK, RestL) + tBsSFB, tBgSFB = cute.nvgpu.cpasync.tma_partition( + tma_atom_sfb, + block_in_cluster_coord_sfb_vmnk[1], + sfb_cta_layout, + cute.group_modes(sSFB, 0, 3), + cute.group_modes(tCgSFB, 0, 3), + ) + tBsSFB = cute.filter_zeros(tBsSFB) + tBgSFB = cute.filter_zeros(tBgSFB) + + # + # Partition shared/tensor memory tensor for TiledMMA_A/B/C + # + # (MMA, MMA_M, MMA_K, STAGE) + tCrA = tiled_mma.make_fragment_A(sA) + # (MMA, MMA_N, MMA_K, STAGE) + tCrB = tiled_mma.make_fragment_B(sB) + # (MMA, MMA_M, MMA_N) + acc_shape = tiled_mma.partition_shape_C(self.mma_tiler[:2]) + if cutlass.const_expr(self.overlapping_accum): + num_acc_stage_overlapped = 2 + tCtAcc_fake = tiled_mma.make_fragment_C( + cute.append(acc_shape, num_acc_stage_overlapped) + ) + # (MMA, MMA_M, MMA_N, STAGE) + tCtAcc_fake = cute.make_tensor( + tCtAcc_fake.iterator, + cute.make_layout( + tCtAcc_fake.shape, + stride=( + tCtAcc_fake.stride[0], + tCtAcc_fake.stride[1], + tCtAcc_fake.stride[2], + (256 - self.num_sf_tmem_cols) * tCtAcc_fake.stride[0][1], + ), + ), + ) + else: + # (MMA, MMA_M, MMA_N, STAGE) + tCtAcc_fake = tiled_mma.make_fragment_C( + cute.append(acc_shape, self.num_acc_stage) + ) + + # + # Cluster wait before tensor memory alloc + # + pipeline_init_wait(cluster_shape_mn=self.cluster_shape_mn) + + # + # Specialized TMA load warp + # + if warp_idx == self.tma_warp_id: + # + # Persistent tile scheduling loop + # + tile_sched = utils.StaticPersistentTileScheduler.create( + tile_sched_params, cute.arch.block_idx(), cute.arch.grid_dim() + ) + work_tile = tile_sched.initial_work_tile_info() + + ab_producer_state = pipeline.make_pipeline_state( + pipeline.PipelineUserType.Producer, self.num_ab_stage + ) + + while work_tile.is_valid_tile: + # Get tile coord from tile scheduler + cur_tile_coord = work_tile.tile_idx + mma_tile_coord_mnl = ( + cur_tile_coord[0] // cute.size(tiled_mma.thr_id.shape), + cur_tile_coord[1], + cur_tile_coord[2], + ) + + # + # Slice to per mma tile index + # + # ((atom_v, rest_v), RestK) + tAgA_slice = tAgA[ + (None, mma_tile_coord_mnl[0], None, mma_tile_coord_mnl[2]) + ] + # ((atom_v, rest_v), RestK) + tBgB_slice = tBgB[ + (None, mma_tile_coord_mnl[1], None, mma_tile_coord_mnl[2]) + ] + + # ((atom_v, rest_v), RestK) + tAgSFA_slice = tAgSFA[ + (None, mma_tile_coord_mnl[0], None, mma_tile_coord_mnl[2]) + ] + + slice_n = mma_tile_coord_mnl[1] + if cutlass.const_expr(self.cta_tile_shape_mnk[1] == 64): + slice_n = mma_tile_coord_mnl[1] // 2 + # ((atom_v, rest_v), RestK) + tBgSFB_slice = tBgSFB[(None, slice_n, None, mma_tile_coord_mnl[2])] + + # Peek (try_wait) AB buffer empty for k_tile = prefetch_k_tile_cnt + ab_producer_state.reset_count() + peek_ab_empty_status = cutlass.Boolean(1) + if ab_producer_state.count < k_tile_cnt: + peek_ab_empty_status = ab_pipeline.producer_try_acquire( + ab_producer_state + ) + # + # Tma load loop + # + for k_tile in cutlass.range(0, k_tile_cnt, 1, unroll=1): + # Conditionally wait for AB buffer empty + ab_pipeline.producer_acquire( + ab_producer_state, peek_ab_empty_status + ) + + # TMA load A/B/SFA/SFB + cute.copy( + tma_atom_a, + tAgA_slice[(None, ab_producer_state.count)], + tAsA[(None, ab_producer_state.index)], + tma_bar_ptr=ab_pipeline.producer_get_barrier(ab_producer_state), + mcast_mask=a_full_mcast_mask, + ) + cute.copy( + tma_atom_b, + tBgB_slice[(None, ab_producer_state.count)], + tBsB[(None, ab_producer_state.index)], + tma_bar_ptr=ab_pipeline.producer_get_barrier(ab_producer_state), + mcast_mask=b_full_mcast_mask, + ) + cute.copy( + tma_atom_sfa, + tAgSFA_slice[(None, ab_producer_state.count)], + tAsSFA[(None, ab_producer_state.index)], + tma_bar_ptr=ab_pipeline.producer_get_barrier(ab_producer_state), + mcast_mask=sfa_full_mcast_mask, + ) + cute.copy( + tma_atom_sfb, + tBgSFB_slice[(None, ab_producer_state.count)], + tBsSFB[(None, ab_producer_state.index)], + tma_bar_ptr=ab_pipeline.producer_get_barrier(ab_producer_state), + mcast_mask=sfb_full_mcast_mask, + ) + + # Peek (try_wait) AB buffer empty for k_tile = prefetch_k_tile_cnt + k_tile + 1 + ab_producer_state.advance() + peek_ab_empty_status = cutlass.Boolean(1) + if ab_producer_state.count < k_tile_cnt: + peek_ab_empty_status = ab_pipeline.producer_try_acquire( + ab_producer_state + ) + + # + # Advance to next tile + # + tile_sched.advance_to_next_work() + work_tile = tile_sched.get_current_work() + + # + # Wait A/B buffer empty + # + ab_pipeline.producer_tail(ab_producer_state) + + # + # Specialized MMA warp + # + if warp_idx == self.mma_warp_id: + # + # Bar sync for retrieve tensor memory ptr from shared mem + # + tmem.wait_for_alloc() + + # + # Retrieving tensor memory ptr and make accumulator/SFA/SFB tensor + # + acc_tmem_ptr = tmem.retrieve_ptr(self.acc_dtype) + # Make accumulator tmem tensor + # (MMA, MMA_M, MMA_N, STAGE) + tCtAcc_base = cute.make_tensor(acc_tmem_ptr, tCtAcc_fake.layout) + + # Make SFA tmem tensor + sfa_tmem_ptr = cute.recast_ptr( + acc_tmem_ptr + self.num_accumulator_tmem_cols, + dtype=self.sf_dtype, + ) + # (MMA, MMA_M, MMA_K) + tCtSFA_layout = blockscaled_utils.make_tmem_layout_sfa( + tiled_mma, + self.mma_tiler, + self.sf_vec_size, + cute.slice_(sfa_smem_layout_staged, (None, None, None, 0)), + ) + tCtSFA = cute.make_tensor(sfa_tmem_ptr, tCtSFA_layout) + + # Make SFB tmem tensor + sfb_tmem_ptr = cute.recast_ptr( + acc_tmem_ptr + self.num_accumulator_tmem_cols + self.num_sfa_tmem_cols, + dtype=self.sf_dtype, + ) + # (MMA, MMA_N, MMA_K) + tCtSFB_layout = blockscaled_utils.make_tmem_layout_sfb( + tiled_mma, + self.mma_tiler, + self.sf_vec_size, + cute.slice_(sfb_smem_layout_staged, (None, None, None, 0)), + ) + tCtSFB = cute.make_tensor(sfb_tmem_ptr, tCtSFB_layout) + # + # Partition for S2T copy of SFA/SFB + # + ( + tiled_copy_s2t_sfa, + tCsSFA_compact_s2t, + tCtSFA_compact_s2t, + ) = self.mainloop_s2t_copy_and_partition(sSFA, tCtSFA) + ( + tiled_copy_s2t_sfb, + tCsSFB_compact_s2t, + tCtSFB_compact_s2t, + ) = self.mainloop_s2t_copy_and_partition(sSFB, tCtSFB) + + # + # Persistent tile scheduling loop + # + tile_sched = utils.StaticPersistentTileScheduler.create( + tile_sched_params, cute.arch.block_idx(), cute.arch.grid_dim() + ) + work_tile = tile_sched.initial_work_tile_info() + + ab_consumer_state = pipeline.make_pipeline_state( + pipeline.PipelineUserType.Consumer, self.num_ab_stage + ) + acc_producer_state = pipeline.make_pipeline_state( + pipeline.PipelineUserType.Producer, self.num_acc_stage + ) + + while work_tile.is_valid_tile: + # Get tile coord from tile scheduler + cur_tile_coord = work_tile.tile_idx + mma_tile_coord_mnl = ( + cur_tile_coord[0] // cute.size(tiled_mma.thr_id.shape), + cur_tile_coord[1], + cur_tile_coord[2], + ) + + # Get accumulator stage index + if cutlass.const_expr(self.overlapping_accum): + acc_stage_index = acc_producer_state.phase ^ 1 + else: + acc_stage_index = acc_producer_state.index + + # Set tensor memory buffer for current tile + # (MMA, MMA_M, MMA_N) + tCtAcc = tCtAcc_base[(None, None, None, acc_stage_index)] + + # Peek (try_wait) AB buffer full for k_tile = 0 + ab_consumer_state.reset_count() + peek_ab_full_status = cutlass.Boolean(1) + if ab_consumer_state.count < k_tile_cnt and is_leader_cta: + peek_ab_full_status = ab_pipeline.consumer_try_wait( + ab_consumer_state + ) + + # + # Wait for accumulator buffer empty + # + if is_leader_cta: + acc_pipeline.producer_acquire(acc_producer_state) + + tCtSFB_mma = tCtSFB + if cutlass.const_expr(self.cta_tile_shape_mnk[1] == 192): + # If this is an ODD tile, shift the TMEM start address for cta_tile_shape_n=192 case by two words (ignores first 64 columns of SFB) + offset = ( + cutlass.Int32(2) + if mma_tile_coord_mnl[1] % 2 == 1 + else cutlass.Int32(0) + ) + shifted_ptr = cute.recast_ptr( + acc_tmem_ptr + + self.num_accumulator_tmem_cols + + self.num_sfa_tmem_cols + + offset, + dtype=self.sf_dtype, + ) + tCtSFB_mma = cute.make_tensor(shifted_ptr, tCtSFB_layout) + elif cutlass.const_expr(self.cta_tile_shape_mnk[1] == 64): + # Move in increments of 64 columns of SFB + offset = cutlass.Int32((mma_tile_coord_mnl[1] % 2) * 2) + shifted_ptr = cute.recast_ptr( + acc_tmem_ptr + + self.num_accumulator_tmem_cols + + self.num_sfa_tmem_cols + + offset, + dtype=self.sf_dtype, + ) + tCtSFB_mma = cute.make_tensor(shifted_ptr, tCtSFB_layout) + + # + # Reset the ACCUMULATE field for each tile + # + tiled_mma.set(tcgen05.Field.ACCUMULATE, False) + + # + # Mma mainloop + # + for k_tile in range(k_tile_cnt): + if is_leader_cta: + # Conditionally wait for AB buffer full + ab_pipeline.consumer_wait( + ab_consumer_state, peek_ab_full_status + ) + + # Copy SFA/SFB from smem to tmem + s2t_stage_coord = ( + None, + None, + None, + None, + ab_consumer_state.index, + ) + tCsSFA_compact_s2t_staged = tCsSFA_compact_s2t[s2t_stage_coord] + tCsSFB_compact_s2t_staged = tCsSFB_compact_s2t[s2t_stage_coord] + cute.copy( + tiled_copy_s2t_sfa, + tCsSFA_compact_s2t_staged, + tCtSFA_compact_s2t, + ) + cute.copy( + tiled_copy_s2t_sfb, + tCsSFB_compact_s2t_staged, + tCtSFB_compact_s2t, + ) + + # tCtAcc += tCrA * tCrSFA * tCrB * tCrSFB + num_kblocks = cute.size(tCrA, mode=[2]) + for kblock_idx in cutlass.range(num_kblocks, unroll_full=True): + kblock_coord = ( + None, + None, + kblock_idx, + ab_consumer_state.index, + ) + + # Set SFA/SFB tensor to tiled_mma + sf_kblock_coord = (None, None, kblock_idx) + tiled_mma.set( + tcgen05.Field.SFA, + tCtSFA[sf_kblock_coord].iterator, + ) + tiled_mma.set( + tcgen05.Field.SFB, + tCtSFB_mma[sf_kblock_coord].iterator, + ) + + cute.gemm( + tiled_mma, + tCtAcc, + tCrA[kblock_coord], + tCrB[kblock_coord], + tCtAcc, + ) + + # Enable accumulate on tCtAcc after first kblock + tiled_mma.set(tcgen05.Field.ACCUMULATE, True) + + # Async arrive AB buffer empty + ab_pipeline.consumer_release(ab_consumer_state) + + # Peek (try_wait) AB buffer full for k_tile = k_tile + 1 + ab_consumer_state.advance() + peek_ab_full_status = cutlass.Boolean(1) + if ab_consumer_state.count < k_tile_cnt: + if is_leader_cta: + peek_ab_full_status = ab_pipeline.consumer_try_wait( + ab_consumer_state + ) + + # + # Async arrive accumulator buffer full + # + if is_leader_cta: + acc_pipeline.producer_commit(acc_producer_state) + acc_producer_state.advance() + + # + # Advance to next tile + # + tile_sched.advance_to_next_work() + work_tile = tile_sched.get_current_work() + + # + # Wait for accumulator buffer empty + # + acc_pipeline.producer_tail(acc_producer_state) + # + # Specialized epilogue warps + # + if warp_idx < self.mma_warp_id: + # + # Alloc tensor memory buffer + # + tmem.allocate(self.num_tmem_alloc_cols) + + # + # Bar sync for retrieve tensor memory ptr from shared memory + # + tmem.wait_for_alloc() + + # + # Retrieving tensor memory ptr and make accumulator tensor + # + acc_tmem_ptr = tmem.retrieve_ptr(self.acc_dtype) + # (MMA, MMA_M, MMA_N, STAGE) + tCtAcc_base = cute.make_tensor(acc_tmem_ptr, tCtAcc_fake.layout) + + # + # Partition for epilogue + # + epi_tidx = tidx + ( + tiled_copy_t2r, + tTR_tAcc_base, + tTR_rAcc, + ) = self.epilog_tmem_copy_and_partition( + epi_tidx, tCtAcc_base, tCgC, epi_tile, use_2cta_instrs + ) + + tTR_rC = cute.make_rmem_tensor(tTR_rAcc.shape, self.c_dtype) + tiled_copy_r2s, tRS_rC, tRS_sC = self.epilog_smem_copy_and_partition( + tiled_copy_t2r, tTR_rC, epi_tidx, sC + ) + ( + tma_atom_c, + bSG_sC, + bSG_gC_partitioned, + ) = self.epilog_gmem_copy_and_partition( + epi_tidx, tma_atom_c, tCgC, epi_tile, sC + ) + + # + # Persistent tile scheduling loop + # + tile_sched = utils.StaticPersistentTileScheduler.create( + tile_sched_params, cute.arch.block_idx(), cute.arch.grid_dim() + ) + work_tile = tile_sched.initial_work_tile_info() + + acc_consumer_state = pipeline.make_pipeline_state( + pipeline.PipelineUserType.Consumer, self.num_acc_stage + ) + + # Threads/warps participating in tma store pipeline + c_producer_group = pipeline.CooperativeGroup( + pipeline.Agent.Thread, + 32 * len(self.epilog_warp_id), + ) + c_pipeline = pipeline.PipelineTmaStore.create( + num_stages=self.num_c_stage, + producer_group=c_producer_group, + ) + + while work_tile.is_valid_tile: + # Get tile coord from tile scheduler + cur_tile_coord = work_tile.tile_idx + mma_tile_coord_mnl = ( + cur_tile_coord[0] // cute.size(tiled_mma.thr_id.shape), + cur_tile_coord[1], + cur_tile_coord[2], + ) + + # + # Slice to per mma tile index + # + # ((ATOM_V, REST_V), EPI_M, EPI_N) + bSG_gC = bSG_gC_partitioned[ + ( + None, + None, + None, + *mma_tile_coord_mnl, + ) + ] + + # Get accumulator stage index + if cutlass.const_expr(self.overlapping_accum): + acc_stage_index = acc_consumer_state.phase + reverse_subtile = ( + cutlass.Boolean(True) + if acc_stage_index == 0 + else cutlass.Boolean(False) + ) + else: + acc_stage_index = acc_consumer_state.index + + # Set tensor memory buffer for current tile + # (T2R, T2R_M, T2R_N, EPI_M, EPI_M) + tTR_tAcc = tTR_tAcc_base[ + (None, None, None, None, None, acc_stage_index) + ] + + # + # Wait for accumulator buffer full + # + acc_pipeline.consumer_wait(acc_consumer_state) + + tTR_tAcc = cute.group_modes(tTR_tAcc, 3, cute.rank(tTR_tAcc)) + bSG_gC = cute.group_modes(bSG_gC, 1, cute.rank(bSG_gC)) + + # + # Store accumulator to global memory in subtiles + # + subtile_cnt = cute.size(tTR_tAcc.shape, mode=[3]) + num_prev_subtiles = tile_sched.num_tiles_executed * subtile_cnt + for subtile_idx in cutlass.range(subtile_cnt): + real_subtile_idx = subtile_idx + if cutlass.const_expr(self.overlapping_accum): + if reverse_subtile: + real_subtile_idx = ( + self.cta_tile_shape_mnk[1] // self.epi_tile_n + - 1 + - subtile_idx + ) + # + # Load accumulator from tensor memory buffer to register + # + tTR_tAcc_mn = tTR_tAcc[(None, None, None, real_subtile_idx)] + cute.copy(tiled_copy_t2r, tTR_tAcc_mn, tTR_rAcc) + + # + # Async arrive accumulator buffer empty ealier when overlapping_accum is enabled + # + if cutlass.const_expr(self.overlapping_accum): + if subtile_idx == self.iter_acc_early_release_in_epilogue: + # Fence for TMEM load + cute.arch.fence_view_async_tmem_load() + with cute.arch.elect_one(): + acc_pipeline.consumer_release(acc_consumer_state) + acc_consumer_state.advance() + + # + # Convert to C type + # + acc_vec = tiled_copy_r2s.retile(tTR_rAcc).load() + acc_vec = epilogue_op(acc_vec.to(self.c_dtype)) + tRS_rC.store(acc_vec) + + # + # Store C to shared memory + # + c_buffer = (num_prev_subtiles + real_subtile_idx) % self.num_c_stage + cute.copy( + tiled_copy_r2s, + tRS_rC, + tRS_sC[(None, None, None, c_buffer)], + ) + # Fence and barrier to make sure shared memory store is visible to TMA store + cute.arch.fence_proxy( + cute.arch.ProxyKind.async_shared, + space=cute.arch.SharedSpace.shared_cta, + ) + self.epilog_sync_barrier.arrive_and_wait() + + # + # TMA store C to global memory + # + if warp_idx == self.epilog_warp_id[0]: + cute.copy( + tma_atom_c, + bSG_sC[(None, c_buffer)], + bSG_gC[(None, real_subtile_idx)], + ) + # Fence and barrier to make sure shared memory store is visible to TMA store + c_pipeline.producer_commit() + c_pipeline.producer_acquire() + self.epilog_sync_barrier.arrive_and_wait() + + # + # Async arrive accumulator buffer empty + # + if cutlass.const_expr(not self.overlapping_accum): + with cute.arch.elect_one(): + acc_pipeline.consumer_release(acc_consumer_state) + acc_consumer_state.advance() + + # + # Advance to next tile + # + tile_sched.advance_to_next_work() + work_tile = tile_sched.get_current_work() + + # + # Dealloc the tensor memory buffer + # + tmem.relinquish_alloc_permit() + self.epilog_sync_barrier.arrive_and_wait() + tmem.free(acc_tmem_ptr) + # + # Wait for C store complete + # + c_pipeline.producer_tail() + + def mainloop_s2t_copy_and_partition( + self, + sSF: cute.Tensor, + tSF: cute.Tensor, + ) -> Tuple[cute.TiledCopy, cute.Tensor, cute.Tensor]: + """ + Make tiledCopy for smem to tmem load for scale factor tensor, then use it to partition smem memory (source) and tensor memory (destination). + + :param sSF: The scale factor tensor in smem + :type sSF: cute.Tensor + :param tSF: The scale factor tensor in tmem + :type tSF: cute.Tensor + + :return: A tuple containing (tiled_copy_s2t, tCsSF_compact_s2t, tCtSF_compact_s2t) where: + - tiled_copy_s2t: The tiled copy operation for smem to tmem load for scale factor tensor(s2t) + - tCsSF_compact_s2t: The partitioned scale factor tensor in smem + - tSF_compact_s2t: The partitioned scale factor tensor in tmem + :rtype: Tuple[cute.TiledCopy, cute.Tensor, cute.Tensor] + """ + # (MMA, MMA_MN, MMA_K, STAGE) + tCsSF_compact = cute.filter_zeros(sSF) + # (MMA, MMA_MN, MMA_K) + tCtSF_compact = cute.filter_zeros(tSF) + + # Make S2T CopyAtom and tiledCopy + copy_atom_s2t = cute.make_copy_atom( + tcgen05.Cp4x32x128bOp(self.cta_group), + self.sf_dtype, + ) + tiled_copy_s2t = tcgen05.make_s2t_copy(copy_atom_s2t, tCtSF_compact) + thr_copy_s2t = tiled_copy_s2t.get_slice(0) + + # ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K, STAGE) + tCsSF_compact_s2t_ = thr_copy_s2t.partition_S(tCsSF_compact) + # ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K, STAGE) + tCsSF_compact_s2t = tcgen05.get_s2t_smem_desc_tensor( + tiled_copy_s2t, tCsSF_compact_s2t_ + ) + # ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K) + tCtSF_compact_s2t = thr_copy_s2t.partition_D(tCtSF_compact) + + return tiled_copy_s2t, tCsSF_compact_s2t, tCtSF_compact_s2t + + def epilog_tmem_copy_and_partition( + self, + tidx: cutlass.Int32, + tAcc: cute.Tensor, + gC_mnl: cute.Tensor, + epi_tile: cute.Tile, + use_2cta_instrs: Union[cutlass.Boolean, bool], + ) -> Tuple[cute.TiledCopy, cute.Tensor, cute.Tensor]: + """ + Make tiledCopy for tensor memory load, then use it to partition tensor memory (source) and register array (destination). + + :param tidx: The thread index in epilogue warp groups + :type tidx: cutlass.Int32 + :param tAcc: The accumulator tensor to be copied and partitioned + :type tAcc: cute.Tensor + :param gC_mnl: The global tensor C + :type gC_mnl: cute.Tensor + :param epi_tile: The epilogue tiler + :type epi_tile: cute.Tile + :param use_2cta_instrs: Whether use_2cta_instrs is enabled + :type use_2cta_instrs: bool + + :return: A tuple containing (tiled_copy_t2r, tTR_tAcc, tTR_rAcc) where: + - tiled_copy_t2r: The tiled copy operation for tmem to register copy(t2r) + - tTR_tAcc: The partitioned accumulator tensor + - tTR_rAcc: The accumulated tensor in register used to hold t2r results + :rtype: Tuple[cute.TiledCopy, cute.Tensor, cute.Tensor] + """ + # Make tiledCopy for tensor memory load + copy_atom_t2r = sm100_utils.get_tmem_load_op( + self.cta_tile_shape_mnk, + self.c_layout, + self.c_dtype, + self.acc_dtype, + epi_tile, + use_2cta_instrs, + ) + # (EPI_TILE_M, EPI_TILE_N, EPI_M, EPI_N, STAGE) + tAcc_epi = cute.flat_divide( + tAcc[((None, None), 0, 0, None)], + epi_tile, + ) + # (EPI_TILE_M, EPI_TILE_N) + tiled_copy_t2r = tcgen05.make_tmem_copy( + copy_atom_t2r, tAcc_epi[(None, None, 0, 0, 0)] + ) + + thr_copy_t2r = tiled_copy_t2r.get_slice(tidx) + # (T2R, T2R_M, T2R_N, EPI_M, EPI_M, STAGE) + tTR_tAcc = thr_copy_t2r.partition_S(tAcc_epi) + + # (EPI_TILE_M, EPI_TILE_N, EPI_M, EPI_N, RestM, RestN, RestL) + gC_mnl_epi = cute.flat_divide( + gC_mnl[((None, None), 0, 0, None, None, None)], epi_tile + ) + # (T2R, T2R_M, T2R_N, EPI_M, EPI_N, RestM, RestN, RestL) + tTR_gC = thr_copy_t2r.partition_D(gC_mnl_epi) + # (T2R, T2R_M, T2R_N) + tTR_rAcc = cute.make_rmem_tensor( + tTR_gC[(None, None, None, 0, 0, 0, 0, 0)].shape, self.acc_dtype + ) + return tiled_copy_t2r, tTR_tAcc, tTR_rAcc + + def epilog_smem_copy_and_partition( + self, + tiled_copy_t2r: cute.TiledCopy, + tTR_rC: cute.Tensor, + tidx: cutlass.Int32, + sC: cute.Tensor, + ) -> Tuple[cute.TiledCopy, cute.Tensor, cute.Tensor]: + """ + Make tiledCopy for shared memory store, then use it to partition register array (source) and shared memory (destination). + + :param tiled_copy_t2r: The tiled copy operation for tmem to register copy(t2r) + :type tiled_copy_t2r: cute.TiledCopy + :param tTR_rC: The partitioned accumulator tensor + :type tTR_rC: cute.Tensor + :param tidx: The thread index in epilogue warp groups + :type tidx: cutlass.Int32 + :param sC: The shared memory tensor to be copied and partitioned + :type sC: cute.Tensor + :type sepi: cute.Tensor + + :return: A tuple containing (tiled_copy_r2s, tRS_rC, tRS_sC) where: + - tiled_copy_r2s: The tiled copy operation for register to smem copy(r2s) + - tRS_rC: The partitioned tensor C (register source) + - tRS_sC: The partitioned tensor C (smem destination) + :rtype: Tuple[cute.TiledCopy, cute.Tensor, cute.Tensor] + """ + copy_atom_r2s = sm100_utils.get_smem_store_op( + self.c_layout, self.c_dtype, self.acc_dtype, tiled_copy_t2r + ) + tiled_copy_r2s = cute.make_tiled_copy_D(copy_atom_r2s, tiled_copy_t2r) + # (R2S, R2S_M, R2S_N, PIPE_D) + thr_copy_r2s = tiled_copy_r2s.get_slice(tidx) + tRS_sC = thr_copy_r2s.partition_D(sC) + # (R2S, R2S_M, R2S_N) + tRS_rC = tiled_copy_r2s.retile(tTR_rC) + return tiled_copy_r2s, tRS_rC, tRS_sC + + def epilog_gmem_copy_and_partition( + self, + tidx: cutlass.Int32, + atom: Union[cute.CopyAtom, cute.TiledCopy], + gC_mnl: cute.Tensor, + epi_tile: cute.Tile, + sC: cute.Tensor, + ) -> Tuple[cute.CopyAtom, cute.Tensor, cute.Tensor]: + """Make tiledCopy for global memory store, then use it to: + partition shared memory (source) and global memory (destination) for TMA store version. + + :param tidx: The thread index in epilogue warp groups + :type tidx: cutlass.Int32 + :param atom: The copy_atom_c to be used for TMA store version, or tiled_copy_t2r for none TMA store version + :type atom: cute.CopyAtom or cute.TiledCopy + :param gC_mnl: The global tensor C + :type gC_mnl: cute.Tensor + :param epi_tile: The epilogue tiler + :type epi_tile: cute.Tile + :param sC: The shared memory tensor to be copied and partitioned + :type sC: cute.Tensor + + :return: A tuple containing (tma_atom_c, bSG_sC, bSG_gC) where: + - tma_atom_c: The TMA copy atom + - bSG_sC: The partitioned shared memory tensor C + - bSG_gC: The partitioned global tensor C + :rtype: Tuple[cute.CopyAtom, cute.Tensor, cute.Tensor] + """ + # (EPI_TILE_M, EPI_TILE_N, EPI_M, EPI_N, RestM, RestN, RestL) + gC_epi = cute.flat_divide( + gC_mnl[((None, None), 0, 0, None, None, None)], epi_tile + ) + + tma_atom_c = atom + sC_for_tma_partition = cute.group_modes(sC, 0, 2) + gC_for_tma_partition = cute.group_modes(gC_epi, 0, 2) + # ((ATOM_V, REST_V), EPI_M, EPI_N) + # ((ATOM_V, REST_V), EPI_M, EPI_N, RestM, RestN, RestL) + bSG_sC, bSG_gC = cpasync.tma_partition( + tma_atom_c, + 0, + cute.make_layout(1), + sC_for_tma_partition, + gC_for_tma_partition, + ) + return tma_atom_c, bSG_sC, bSG_gC + + @staticmethod + def _compute_stages( + tiled_mma: cute.TiledMma, + mma_tiler_mnk: Tuple[int, int, int], + a_dtype: Type[cutlass.Numeric], + b_dtype: Type[cutlass.Numeric], + epi_tile: cute.Tile, + c_dtype: Type[cutlass.Numeric], + c_layout: utils.LayoutEnum, + sf_dtype: Type[cutlass.Numeric], + sf_vec_size: int, + smem_capacity: int, + occupancy: int, + ) -> Tuple[int, int, int]: + """Computes the number of stages for A/B/C operands based on heuristics. + + :param tiled_mma: The tiled MMA object defining the core computation. + :type tiled_mma: cute.TiledMma + :param mma_tiler_mnk: The shape (M, N, K) of the MMA tiler. + :type mma_tiler_mnk: tuple[int, int, int] + :param a_dtype: Data type of operand A. + :type a_dtype: type[cutlass.Numeric] + :param b_dtype: Data type of operand B. + :type b_dtype: type[cutlass.Numeric] + :param epi_tile: The epilogue tile shape. + :type epi_tile: cute.Tile + :param c_dtype: Data type of operand C (output). + :type c_dtype: type[cutlass.Numeric] + :param c_layout: Layout enum of operand C. + :type c_layout: utils.LayoutEnum + :param sf_dtype: Data type of Scale factor. + :type sf_dtype: type[cutlass.Numeric] + :param sf_vec_size: Scale factor vector size. + :type sf_vec_size: int + :param smem_capacity: Total available shared memory capacity in bytes. + :type smem_capacity: int + :param occupancy: Target number of CTAs per SM (occupancy). + :type occupancy: int + + :return: A tuple containing the computed number of stages for: + (ACC stages, A/B operand stages, C stages) + :rtype: tuple[int, int, int] + """ + # ACC stages + num_acc_stage = 1 if mma_tiler_mnk[1] == 256 else 2 + + # Default C stages + num_c_stage = 2 + + # Calculate smem layout and size for one stage of A, B, SFA, SFB and C + a_smem_layout_stage_one = sm100_utils.make_smem_layout_a( + tiled_mma, + mma_tiler_mnk, + a_dtype, + 1, # a tmp 1 stage is provided + ) + b_smem_layout_staged_one = sm100_utils.make_smem_layout_b( + tiled_mma, + mma_tiler_mnk, + b_dtype, + 1, # a tmp 1 stage is provided + ) + sfa_smem_layout_staged_one = blockscaled_utils.make_smem_layout_sfa( + tiled_mma, + mma_tiler_mnk, + sf_vec_size, + 1, # a tmp 1 stage is provided + ) + sfb_smem_layout_staged_one = blockscaled_utils.make_smem_layout_sfb( + tiled_mma, + mma_tiler_mnk, + sf_vec_size, + 1, # a tmp 1 stage is provided + ) + + c_smem_layout_staged_one = sm100_utils.make_smem_layout_epi( + c_dtype, + c_layout, + epi_tile, + 1, + ) + + ab_bytes_per_stage = ( + cute.size_in_bytes(a_dtype, a_smem_layout_stage_one) + + cute.size_in_bytes(b_dtype, b_smem_layout_staged_one) + + cute.size_in_bytes(sf_dtype, sfa_smem_layout_staged_one) + + cute.size_in_bytes(sf_dtype, sfb_smem_layout_staged_one) + ) + mbar_helpers_bytes = 1024 + c_bytes_per_stage = cute.size_in_bytes(c_dtype, c_smem_layout_staged_one) + c_bytes = c_bytes_per_stage * num_c_stage + + # Calculate A/B/SFA/SFB stages: + # Start with total smem per CTA (capacity / occupancy) + # Subtract reserved bytes and initial C stages bytes + # Divide remaining by bytes needed per A/B/SFA/SFB stage + num_ab_stage = ( + smem_capacity // occupancy - (mbar_helpers_bytes + c_bytes) + ) // ab_bytes_per_stage + + # Refine epilogue stages: + # Calculate remaining smem after allocating for A/B/SFA/SFB stages and reserved bytes + # Add remaining unused smem to epilogue + num_c_stage += ( + smem_capacity + - occupancy * ab_bytes_per_stage * num_ab_stage + - occupancy * (mbar_helpers_bytes + c_bytes) + ) // (occupancy * c_bytes_per_stage) + + return num_acc_stage, num_ab_stage, num_c_stage + + @staticmethod + def _compute_grid( + c: cute.Tensor, + cta_tile_shape_mnk: Tuple[int, int, int], + cluster_shape_mn: Tuple[int, int], + max_active_clusters: cutlass.Constexpr, + ) -> Tuple[utils.PersistentTileSchedulerParams, Tuple[int, int, int]]: + """Use persistent tile scheduler to compute the grid size for the output tensor C. + + :param c: The output tensor C + :type c: cute.Tensor + :param cta_tile_shape_mnk: The shape (M, N, K) of the CTA tile. + :type cta_tile_shape_mnk: tuple[int, int, int] + :param cluster_shape_mn: Shape of each cluster in M, N dimensions. + :type cluster_shape_mn: tuple[int, int] + :param max_active_clusters: Maximum number of active clusters. + :type max_active_clusters: cutlass.Constexpr + + :return: A tuple containing: + - tile_sched_params: Parameters for the persistent tile scheduler. + - grid: Grid shape for kernel launch. + :rtype: Tuple[utils.PersistentTileSchedulerParams, tuple[int, int, int]] + """ + c_shape = cute.slice_(cta_tile_shape_mnk, (None, None, 0)) + gc = cute.zipped_divide(c, tiler=c_shape) + num_ctas_mnl = gc[(0, (None, None, None))].shape + cluster_shape_mnl = (*cluster_shape_mn, 1) + + tile_sched_params = utils.PersistentTileSchedulerParams( + num_ctas_mnl, cluster_shape_mnl + ) + grid = utils.StaticPersistentTileScheduler.get_grid_shape( + tile_sched_params, max_active_clusters + ) + + return tile_sched_params, grid + + @staticmethod + def is_valid_dtypes_and_scale_factor_vec_size( + ab_dtype: Type[cutlass.Numeric], + sf_dtype: Type[cutlass.Numeric], + sf_vec_size: int, + c_dtype: Type[cutlass.Numeric], + ) -> bool: + """ + Check if the dtypes and sf_vec_size are valid combinations + + :param ab_dtype: The data type of the A and B operands + :type ab_dtype: Type[cutlass.Numeric] + :param sf_dtype: The data type of the scale factor + :type sf_dtype: Type[cutlass.Numeric] + :param sf_vec_size: The vector size of the scale factor + :type sf_vec_size: int + :param c_dtype: The data type of the output tensor + :type c_dtype: Type[cutlass.Numeric] + + :return: True if the dtypes and sf_vec_size are valid, False otherwise + :rtype: bool + """ + is_valid = True + + # Check valid ab_dtype + if ab_dtype not in { + cutlass.Float4E2M1FN, + cutlass.Float8E5M2, + cutlass.Float8E4M3FN, + }: + is_valid = False + + # Check valid sf_vec_size + if sf_vec_size not in {16, 32}: + is_valid = False + + # Check valid sf_dtype + if sf_dtype not in {cutlass.Float8E8M0FNU, cutlass.Float8E4M3FN}: + is_valid = False + + # Check valid sf_dtype and sf_vec_size combinations + if sf_dtype == cutlass.Float8E4M3FN and sf_vec_size == 32: + is_valid = False + if ab_dtype in {cutlass.Float8E5M2, cutlass.Float8E4M3FN} and sf_vec_size == 16: + is_valid = False + + # Check valid c_dtype + if c_dtype not in { + cutlass.Float32, + cutlass.Float16, + cutlass.BFloat16, + cutlass.Float8E5M2, + cutlass.Float8E4M3FN, + }: + is_valid = False + + return is_valid + + @staticmethod + def is_valid_layouts( + ab_dtype: Type[cutlass.Numeric], + c_dtype: Type[cutlass.Numeric], + a_major: str, + b_major: str, + c_major: str, + ) -> bool: + """ + Check if layouts and dtypes are valid combinations + + :param ab_dtype: The data type of the A and B operands + :type ab_dtype: Type[cutlass.Numeric] + :param c_dtype: The data type of the output tensor + :type c_dtype: Type[cutlass.Numeric] + :param a_major: The major dimension of the A tensor + :type a_major: str + :param b_major: The major dimension of the B tensor + :type b_major: str + :param c_major: The major dimension of the C tensor + :type c_major: str + + :return: True if the layouts are valid, False otherwise + :rtype: bool + """ + is_valid = True + + if ab_dtype is cutlass.Float4E2M1FN and not (a_major == "k" and b_major == "k"): + is_valid = False + return is_valid + + @staticmethod + def is_valid_mma_tiler_and_cluster_shape( + mma_tiler_mn: Tuple[int, int], + cluster_shape_mn: Tuple[int, int], + ) -> bool: + """ + Check if the mma tiler and cluster shape are valid + + :param mma_tiler_mn: The (M, N) shape of the MMA instruction tiler + :type mma_tiler_mn: Tuple[int, int] + :param cluster_shape_mn: The (ClusterM, ClusterN) shape of the CTA cluster + :type cluster_shape_mn: Tuple[int, int] + + :return: True if the mma tiler and cluster shape are valid, False otherwise + :rtype: bool + """ + is_valid = True + # Skip invalid mma tile shape + if mma_tiler_mn[0] not in [128, 256]: + is_valid = False + if mma_tiler_mn[1] not in [64, 128, 192, 256]: + is_valid = False + # Skip illegal cluster shape + if cluster_shape_mn[0] % (2 if mma_tiler_mn[0] == 256 else 1) != 0: + is_valid = False + # Skip invalid cluster shape + is_power_of_2 = lambda x: x > 0 and (x & (x - 1)) == 0 + if ( + cluster_shape_mn[0] * cluster_shape_mn[1] > 16 + or cluster_shape_mn[0] <= 0 + or cluster_shape_mn[1] <= 0 + # Special cluster shape check for scale factor multicasts. + # Due to limited size of scale factors, we can't multicast among more than 4 CTAs. + or cluster_shape_mn[0] > 4 + or cluster_shape_mn[1] > 4 + or not is_power_of_2(cluster_shape_mn[0]) + or not is_power_of_2(cluster_shape_mn[1]) + ): + is_valid = False + return is_valid + + @staticmethod + def is_valid_tensor_alignment( + m: int, + n: int, + k: int, + l: int, + ab_dtype: Type[cutlass.Numeric], + c_dtype: Type[cutlass.Numeric], + a_major: str, + b_major: str, + c_major: str, + ) -> bool: + """ + Check if the tensor alignment is valid + + :param m: The number of rows in the A tensor + :type m: int + :param n: The number of columns in the B tensor + :type n: int + :param k: The number of columns in the A tensor + :type k: int + :param l: The number of columns in the C tensor + :type l: int + :param ab_dtype: The data type of the A and B operands + :type ab_dtype: Type[cutlass.Numeric] + :param c_dtype: The data type of the output tensor + :type c_dtype: Type[cutlass.Numeric] + :param a_major: The major axis of the A tensor + :type a_major: str + :param b_major: The major axis of the B tensor + :type b_major: str + :param c_major: The major axis of the C tensor + :type c_major: str + + :return: True if the problem shape is valid, False otherwise + :rtype: bool + """ + is_valid = True + + def check_contigous_16B_alignment(dtype, is_mode0_major, tensor_shape): + major_mode_idx = 0 if is_mode0_major else 1 + num_major_elements = tensor_shape[major_mode_idx] + num_contiguous_elements = 16 * 8 // dtype.width + return num_major_elements % num_contiguous_elements == 0 + + if ( + not check_contigous_16B_alignment(ab_dtype, a_major == "m", (m, k, l)) + or not check_contigous_16B_alignment(ab_dtype, b_major == "n", (n, k, l)) + or not check_contigous_16B_alignment(c_dtype, c_major == "m", (m, n, l)) + ): + is_valid = False + return is_valid + + @staticmethod + def can_implement( + ab_dtype: Type[cutlass.Numeric], + sf_dtype: Type[cutlass.Numeric], + sf_vec_size: int, + c_dtype: Type[cutlass.Numeric], + mma_tiler_mn: Tuple[int, int], + cluster_shape_mn: Tuple[int, int], + m: int, + n: int, + k: int, + l: int, + a_major: str, + b_major: str, + c_major: str, + ) -> bool: + """ + Check if the gemm can be implemented + + :param ab_dtype: The data type of the A and B operands + :type ab_dtype: Type[cutlass.Numeric] + :param sf_dtype: The data type of the scale factor tensor + :type sf_dtype: Type[cutlass.Numeric] + :param sf_vec_size: The vector size + :type sf_vec_size: int + :param c_dtype: The data type of the output tensor + :type c_dtype: Type[cutlass.Numeric] + :param mma_tiler_mn: The (M, N) shape of the MMA instruction tiler + :type mma_tiler_mn: Tuple[int, int] + :param cluster_shape_mn: The (ClusterM, ClusterN) shape of the CTA cluster + :type cluster_shape_mn: Tuple[int, int] + :param m: The number of rows in the A tensor + :type m: int + :param n: The number of columns in the B tensor + :type n: int + :param k: The number of columns in the A tensor + :type k: int + :param l: The number of columns in the C tensor + :type l: int + :param a_major: The major axis of the A tensor + :type a_major: str + :param b_major: The major axis of the B tensor + :type b_major: str + :param c_major: The major axis of the C tensor + :type c_major: str + + :return: True if the gemm can be implemented, False otherwise + :rtype: bool + """ + can_implement = True + # Skip unsupported types + if not Sm100BlockScaledPersistentDenseGemmKernel.is_valid_dtypes_and_scale_factor_vec_size( + ab_dtype, sf_dtype, sf_vec_size, c_dtype + ): + can_implement = False + # Skip unsupported layouts + if not Sm100BlockScaledPersistentDenseGemmKernel.is_valid_layouts( + ab_dtype, c_dtype, a_major, b_major, c_major + ): + can_implement = False + # Skip invalid mma tile shape and cluster shape + if not Sm100BlockScaledPersistentDenseGemmKernel.is_valid_mma_tiler_and_cluster_shape( + mma_tiler_mn, cluster_shape_mn + ): + can_implement = False + # Skip illegal problem shape for load/store alignment + if not Sm100BlockScaledPersistentDenseGemmKernel.is_valid_tensor_alignment( + m, n, k, l, ab_dtype, c_dtype, a_major, b_major, c_major + ): + can_implement = False + return can_implement + + +@cute.jit +def cvt_sf_MKL_to_M32x4xrm_K4xrk_L( + sf_ref_tensor: cute.Tensor, + sf_mma_tensor: cute.Tensor, +): + """Convert scale factor tensor from MKL layout to mma specification M(32x4xrest_m)xK(4xrest_k)xL layout""" + # sf_mma_tensor has flatten shape (32, 4, rest_m, 4, rest_k, l) + # group to ((32, 4, rest_m), (4, rest_k), l) + sf_mma_tensor = cute.group_modes(sf_mma_tensor, 0, 3) + sf_mma_tensor = cute.group_modes(sf_mma_tensor, 1, 3) + for i in cutlass.range(cute.size(sf_ref_tensor)): + mkl_coord = sf_ref_tensor.layout.get_hier_coord(i) + sf_mma_tensor[mkl_coord] = sf_ref_tensor[mkl_coord] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/vendored_templates/cutedsl/kernels/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/vendored_templates/cutedsl/kernels/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/vendored_templates/cutedsl/kernels/cutedsl_grouped_gemm.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/vendored_templates/cutedsl/kernels/cutedsl_grouped_gemm.py new file mode 100644 index 0000000000000000000000000000000000000000..50c208f204f77fe622263d54c2ab69c03edc4c4b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/vendored_templates/cutedsl/kernels/cutedsl_grouped_gemm.py @@ -0,0 +1,2387 @@ +# Copyright (c) 2025 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: + +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. + +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import argparse +import functools +from typing import List, Type, Union +from inspect import isclass + +import torch +import cuda.bindings.driver as cuda + +import cutlass +import cutlass.cute as cute +import cutlass.cute.testing as testing +import cutlass.utils as utils +import cutlass.pipeline as pipeline +from cutlass.pipeline import pipeline_init_arrive, pipeline_init_wait +from cutlass.cute.nvgpu import cpasync, tcgen05 +import cutlass.utils.blackwell_helpers as sm100_utils +import cutlass.torch as cutlass_torch + +""" +A grouped GEMM example for the NVIDIA Blackwell SM100 architecture using CUTE DSL + +This example demonstrates an implementation of grouped GEMM using a TMA plus Blackwell SM100 TensorCore +warp-specialized persistent kernel. +The grouped GEMM workload computes a batch of GEMM operations with distinct problem sizes. Pointers to matrices +in global memory are passed to the kernel in an array (also held in global memory). Similarly, problem shapes and +strides are also stored in arrays in GMEM. + +This differs from "Batched Array" GEMM since the size of each GEMM problem in the grouped GEMM concept may be distinct. + +To run this example: + +.. code-block:: bash + + python examples/blackwell/grouped_gemm.py \ + --ab_dtype Float16 --c_dtype Float16 --acc_dtype Float32 \ + --mma_tiler_mn 128,64 --cluster_shape_mn 1,1 \ + --problem_sizes_mnkl "(8192,1280,32,1),(16,384,1536,1),(640,1280,16,1),(640,160,16,1)" \ + --num_groups 4 --tensormap_update_mode SMEM + +The above example command makes 4 groups of different m, n, k sizes. The Blackwell tcgen05 MMA tile shape +is specified as (128, 64) and the cluster shape is (1,1). The input, mma accumulator and output data type +are set as fp16, fp32 and fp16, respectively. + +To collect performance with NCU profiler: + +.. code-block:: bash + + ncu python examples/blackwell/grouped_gemm.py \ + --ab_dtype Float16 --c_dtype Float16 --acc_dtype Float32 \ + --mma_tiler_mn 128,64 --cluster_shape_mn 1,1 \ + --problem_sizes_mnkl "(8192,1280,32,1),(16,384,1536,1),(640,1280,16,1),(640,160,16,1)" \ + --num_groups 4 --tensormap_update_mode SMEM \ + --warmup_iterations 1 --iterations 10 --skip_ref_check + +There are some constrains for this example. Besides the constrains from the Balckwell dense GEMM persistent example, +there are also the following constrains: +* Only fp16 and bf16 data types are supported as inputs. +* Output data types could be fp16, bf16 or fp32. +* The contiguous dimension of each tensor must be at least 16 bytes aligned. +* The l mode(aka, batch size) for each group must be 1. +* The majorness for A, B and C must be the same across all groups. +""" + + +class GroupedGemmKernel: + def __init__( + self, + acc_dtype: type[cutlass.Numeric], + use_2cta_instrs: bool, + mma_tiler_mn: tuple[int, int], + cluster_shape_mn: tuple[int, int], + tensormap_update_mode: utils.TensorMapUpdateMode = utils.TensorMapUpdateMode.SMEM, + ): + """Initializes the configuration for a Blackwell grouped GEMM kernel. + + Besides configurations for dense persistent GEMM, there is an extra config specific to grouped GEMM: + + Tensormap Update Mode: + - tensormap_update_mode: Specifies whether the tensormap is + updated in global memory(GMEM) or shared memory(SMEM). + The 2 modes are functionally equivalent and the difference are: + - We buffer 3 tensormaps in SMEM for A, B, and C tensors (each TMA descriptor takes 128B) when TMA updates performed on SMEM. + - Performance varies between modes depending on problem size; optimal choice differs across workloads. + + :param acc_dtype: Data type of the accumulator. + :type acc_dtype: type[cutlass.Numeric] + :param use_2cta_instrs: Boolean, True to use cta_group=2 MMA variant. + :type use_2cta_instrs: bool + :param mma_tiler_mn: tuple (M, N) shape of the MMA instruction. + :type mma_tiler_mn: tuple[int, int] + :param cluster_shape_mn: tuple (ClusterM, ClusterN) shape of the cluster. + :type cluster_shape_mn: tuple[int, int] + :param tensormap_update_mode: Mode for updating the tensormap (GMEM or SMEM), defaults to SMEM. + :type tensormap_update_mode: utils.TensorMapUpdateMode, optional + """ + self.acc_dtype: Type[cutlass.Numeric] = acc_dtype + self.use_2cta_instrs = use_2cta_instrs + self.cluster_shape_mn = cluster_shape_mn + # K dimension is deferred in _setup_attributes + self.mma_tiler = (*mma_tiler_mn, 1) + self.cta_group = ( + tcgen05.CtaGroup.TWO if use_2cta_instrs else tcgen05.CtaGroup.ONE + ) + + self.tensormap_update_mode = tensormap_update_mode + # Delegate tensormap ab initialization to MMA warp when SMEM mode is used for better latency hiding + self.delegate_tensormap_ab_init = ( + tensormap_update_mode == utils.TensorMapUpdateMode.SMEM + ) + + self.num_mcast_ctas_a = 1 + self.num_mcast_ctas_b = 1 + self.is_a_mcast = False + self.is_b_mcast = False + + self.occupancy = 1 + # Set specialized warp ids + self.epilog_warp_id = ( + 0, + 1, + 2, + 3, + ) + self.mma_warp_id = 4 + self.tma_warp_id = 5 + self.threads_per_cta = 32 * len( + (self.mma_warp_id, self.tma_warp_id, *self.epilog_warp_id) + ) + # Set barrier for epilog sync, tmem ptr sync and tensormap update sync + self.epilog_sync_barrier = pipeline.NamedBarrier( + barrier_id=1, + num_threads=32 * len(self.epilog_warp_id), + ) + self.tmem_alloc_barrier = pipeline.NamedBarrier( + barrier_id=2, + num_threads=32 * len((self.mma_warp_id, *self.epilog_warp_id)), + ) + # Barrier used by MMA/TMA warps to signal A/B tensormap initialization completion + self.tensormap_ab_init_barrier = pipeline.NamedBarrier( + barrier_id=3, + num_threads=32 * (len(self.epilog_warp_id) + 1), + ) + self.smem_capacity = utils.get_smem_capacity_in_bytes("sm_100") + self.num_tma_load_bytes = 0 + + def _setup_attributes(self): + """Set up configurations that are dependent on GEMM inputs + + Most of the implementation follows standard dense GEMM patterns, + with the key difference being additional consideration for SMEM + buffer needed for tensormap updates. + """ + # Configure tiled mma + tiled_mma = sm100_utils.make_trivial_tiled_mma( + self.a_dtype, + self.a_major_mode, + self.b_major_mode, + self.acc_dtype, + self.cta_group, + self.mma_tiler[:2], + ) + + # Compute mma/cluster/tile shapes + mma_inst_shape_k = cute.size(tiled_mma.shape_mnk, mode=[2]) + mma_inst_tile_k = 4 + self.mma_tiler = ( + self.mma_tiler[0], + self.mma_tiler[1], + mma_inst_shape_k * mma_inst_tile_k, + ) + self.cta_tile_shape_mnk = ( + self.mma_tiler[0] // cute.size(tiled_mma.thr_id.shape), + self.mma_tiler[1], + self.mma_tiler[2], + ) + self.cluster_tile_shape_mnk = tuple( + x * y for x, y in zip(self.cta_tile_shape_mnk, (*self.cluster_shape_mn, 1)) + ) + + # Compute cluster layout + self.cluster_layout_vmnk = cute.tiled_divide( + cute.make_layout((*self.cluster_shape_mn, 1)), + (tiled_mma.thr_id.shape,), + ) + + # Compute number of multicast CTAs for A/B + self.num_mcast_ctas_a = cute.size(self.cluster_layout_vmnk.shape[2]) + self.num_mcast_ctas_b = cute.size(self.cluster_layout_vmnk.shape[1]) + self.is_a_mcast = self.num_mcast_ctas_a > 1 + self.is_b_mcast = self.num_mcast_ctas_b > 1 + + # Compute epilogue subtile + self.epi_tile = utils.compute_epilogue_tile_shape( + self.cta_tile_shape_mnk, + self.use_2cta_instrs, + self.c_layout, + self.c_dtype, + ) + + # Setup A/B/C stage count in shared memory and ACC stage count in tensor memory + ( + self.num_acc_stage, + self.num_ab_stage, + self.num_epi_stage, + ) = self._compute_stages( + tiled_mma, + self.mma_tiler, + self.a_dtype, + self.b_dtype, + self.epi_tile, + self.c_dtype, + self.c_layout, + self.smem_capacity, + self.occupancy, + ) + + self.a_smem_layout_staged = sm100_utils.make_smem_layout_a( + tiled_mma, + self.mma_tiler, + self.a_dtype, + self.num_ab_stage, + ) + self.b_smem_layout_staged = sm100_utils.make_smem_layout_b( + tiled_mma, + self.mma_tiler, + self.b_dtype, + self.num_ab_stage, + ) + self.epi_smem_layout_staged = sm100_utils.make_smem_layout_epi( + self.c_dtype, + self.c_layout, + self.epi_tile, + self.num_epi_stage, + ) + + mbar_smem_bytes = self._get_mbar_smem_bytes( + num_acc_stage=self.num_acc_stage, + num_ab_stage=self.num_ab_stage, + num_epi_stage=self.num_epi_stage, + ) + tensormap_smem_bytes = self._get_tensormap_smem_bytes( + self.tensormap_update_mode + ) + if ( + mbar_smem_bytes + + tensormap_smem_bytes + + GroupedGemmKernel.tensor_memory_management_bytes + > self.reserved_smem_bytes + ): + raise ValueError( + f"smem consumption for mbar and tensormap {mbar_smem_bytes + tensormap_smem_bytes} exceeds the " + f"reserved smem bytes {self.reserved_smem_bytes}" + ) + + # Compute the number of tensor memory allocation columns + self.num_tmem_alloc_cols = self._compute_num_tmem_alloc_cols( + tiled_mma, self.mma_tiler, self.num_acc_stage + ) + + @cute.jit + def __call__( + self, + initial_a: cute.Tensor, + initial_b: cute.Tensor, + initial_c: cute.Tensor, + group_count: cutlass.Constexpr[int], + problem_shape_mnkl: cute.Tensor, + strides_abc: cute.Tensor, + tensor_address_abc: cute.Tensor, + total_num_clusters: cutlass.Constexpr[int], + tensormap_cute_tensor: cute.Tensor, + max_active_clusters: cutlass.Constexpr[int], + stream: cuda.CUstream, + ): + """Execute the GEMM operation in steps: + - Setup static attributes before smem/grid/tma computation + - Setup TMA load/store atoms and tensors + - Compute grid size with regard to hardware constraints + - Define shared storage for kernel + - Launch the kernel synchronously + + For grouped GEMM, tensor shapes, tensor strides, and tensor address are all provided + by different tensors in global memory. The "initial" tensors only carry data type and + majorness information. + + :param initial_a: Initial tensor A, used for data type and majorness information. + :type initial_a: cute.Tensor + :param initial_b: Initial tensor B, used for data type and majorness information. + :type initial_b: cute.Tensor + :param initial_c: Initial tensor C, used for data type and majorness information. + :type initial_c: cute.Tensor + :param group_count: The number of GEMM groups. + :type group_count: cutlass.Constexpr[int] + :param problem_shape_mnkl: Tensor containing the (M, N, K, L) shape for each group. + :type problem_shape_mnkl: cute.Tensor + :param strides_abc: Tensor containing the strides for A, B, and C for each group. + :type strides_abc: cute.Tensor + :param tensor_address_abc: Tensor containing the base addresses for A, B, and C for each group. + :type tensor_address_abc: cute.Tensor + :param total_num_clusters: Total number of clusters needed for all groups. + :type total_num_clusters: cutlass.Constexpr[int] + :param tensormap_cute_tensor: Tensor for storing tensormaps. + :type tensormap_cute_tensor: cute.Tensor + :param max_active_clusters: Maximum number of active clusters. + :type max_active_clusters: cutlass.Constexpr[int] + :param stream: CUDA stream for asynchronous execution. + :type stream: cuda.CUstream + :raises TypeError: If A and B data types do not match. + """ + + self.a_dtype = initial_a.element_type + self.b_dtype = initial_b.element_type + self.c_dtype = initial_c.element_type + + self.a_major_mode = utils.LayoutEnum.from_tensor(initial_a).mma_major_mode() + self.b_major_mode = utils.LayoutEnum.from_tensor(initial_b).mma_major_mode() + self.c_layout = utils.LayoutEnum.from_tensor(initial_c) + if cutlass.const_expr(self.a_dtype != self.b_dtype): + raise TypeError(f"Type mismatch: {self.a_dtype} != {self.b_dtype}") + + # Setup attributes that dependent on gemm inputs + self._setup_attributes() + + tiled_mma = sm100_utils.make_trivial_tiled_mma( + self.a_dtype, + self.a_major_mode, + self.b_major_mode, + self.acc_dtype, + self.cta_group, + self.mma_tiler[:2], + ) + atom_thr_size = cute.size(tiled_mma.thr_id.shape) + + # Setup TMA load for A + a_op = sm100_utils.cluster_shape_to_tma_atom_A( + self.cluster_shape_mn, tiled_mma.thr_id + ) + a_smem_layout = cute.slice_(self.a_smem_layout_staged, (None, None, None, 0)) + tma_atom_a, tma_tensor_a = cute.nvgpu.make_tiled_tma_atom_A( + a_op, + initial_a, + a_smem_layout, + self.mma_tiler, + tiled_mma, + self.cluster_layout_vmnk.shape, + ) + + # Setup TMA load for B + b_op = sm100_utils.cluster_shape_to_tma_atom_B( + self.cluster_shape_mn, tiled_mma.thr_id + ) + b_smem_layout = cute.slice_(self.b_smem_layout_staged, (None, None, None, 0)) + tma_atom_b, tma_tensor_b = cute.nvgpu.make_tiled_tma_atom_B( + b_op, + initial_b, + b_smem_layout, + self.mma_tiler, + tiled_mma, + self.cluster_layout_vmnk.shape, + ) + + a_copy_size = cute.size_in_bytes(self.a_dtype, a_smem_layout) + b_copy_size = cute.size_in_bytes(self.b_dtype, b_smem_layout) + self.num_tma_load_bytes = (a_copy_size + b_copy_size) * atom_thr_size + + # Setup TMA store for C + tma_atom_c = None + tma_tensor_c = None + epi_smem_layout = cute.slice_(self.epi_smem_layout_staged, (None, None, 0)) + tma_atom_c, tma_tensor_c = cpasync.make_tiled_tma_atom( + cpasync.CopyBulkTensorTileS2GOp(), + initial_c, + epi_smem_layout, + self.epi_tile, + ) + + self.tile_sched_params, grid = self._compute_grid( + total_num_clusters, self.cluster_shape_mn, max_active_clusters + ) + + self.buffer_align_bytes = 1024 + self.size_tensormap_in_i64 = ( + 0 + if self.tensormap_update_mode == utils.TensorMapUpdateMode.GMEM + else GroupedGemmKernel.num_tensormaps + * GroupedGemmKernel.bytes_per_tensormap + // 8 + ) + + # Define shared storage for kernel + @cute.struct + class SharedStorage: + tensormap_buffer: cute.struct.MemRange[ + cutlass.Int64, self.size_tensormap_in_i64 + ] + ab_full_mbar_ptr: cute.struct.MemRange[cutlass.Int64, self.num_ab_stage] + ab_empty_mbar_ptr: cute.struct.MemRange[cutlass.Int64, self.num_ab_stage] + acc_full_mbar_ptr: cute.struct.MemRange[cutlass.Int64, self.num_acc_stage] + acc_empty_mbar_ptr: cute.struct.MemRange[cutlass.Int64, self.num_acc_stage] + tmem_dealloc_mbar_ptr: cutlass.Int64 + tmem_holding_buf: cutlass.Int32 + # (EPI_TILE_M, EPI_TILE_N, STAGE) + sC: cute.struct.Align[ + cute.struct.MemRange[ + self.c_dtype, + cute.cosize(self.epi_smem_layout_staged.outer), + ], + self.buffer_align_bytes, + ] + # (MMA, MMA_M, MMA_K, STAGE) + sA: cute.struct.Align[ + cute.struct.MemRange[ + self.a_dtype, cute.cosize(self.a_smem_layout_staged.outer) + ], + self.buffer_align_bytes, + ] + # (MMA, MMA_N, MMA_K, STAGE) + sB: cute.struct.Align[ + cute.struct.MemRange[ + self.b_dtype, cute.cosize(self.b_smem_layout_staged.outer) + ], + self.buffer_align_bytes, + ] + + self.shared_storage = SharedStorage + + # Launch the kernel synchronously + self.kernel( + tiled_mma, + tma_atom_a, + tma_tensor_a, + tma_atom_b, + tma_tensor_b, + tma_atom_c, + tma_tensor_c, + self.cluster_layout_vmnk, + self.a_smem_layout_staged, + self.b_smem_layout_staged, + self.epi_smem_layout_staged, + self.epi_tile, + self.tile_sched_params, + group_count, + problem_shape_mnkl, + strides_abc, + tensor_address_abc, + tensormap_cute_tensor, + ).launch( + grid=grid, + block=[self.threads_per_cta, 1, 1], + cluster=(*self.cluster_shape_mn, 1), + stream=stream, + min_blocks_per_mp=1, + ) + return + + # GPU device kernel + @cute.kernel + def kernel( + self, + tiled_mma: cute.TiledMma, + tma_atom_a: cute.CopyAtom, + mA_mkl: cute.Tensor, + tma_atom_b: cute.CopyAtom, + mB_nkl: cute.Tensor, + tma_atom_c: cute.CopyAtom, + mC_mnl: cute.Tensor, + cluster_layout_vmnk: cute.Layout, + a_smem_layout_staged: cute.ComposedLayout, + b_smem_layout_staged: cute.ComposedLayout, + epi_smem_layout_staged: Union[cute.Layout, cute.ComposedLayout], + epi_tile: cute.Tile, + tile_sched_params: utils.PersistentTileSchedulerParams, + group_count: cutlass.Constexpr[int], + problem_sizes_mnkl: cute.Tensor, + strides_abc: cute.Tensor, + ptrs_abc: cute.Tensor, + tensormaps: cute.Tensor, + ): + """ + GPU device kernel performing the grouped GEMM computation. + """ + warp_idx = cute.arch.warp_idx() + warp_idx = cute.arch.make_warp_uniform(warp_idx) + + # + # Prefetch tma desc + # + if warp_idx == self.tma_warp_id: + cpasync.prefetch_descriptor(tma_atom_a) + cpasync.prefetch_descriptor(tma_atom_b) + cpasync.prefetch_descriptor(tma_atom_c) + + use_2cta_instrs = cute.size(tiled_mma.thr_id.shape) == 2 + + # + # Setup cta/thread coordinates + # + # Coord inside cluster + bid = cute.arch.block_idx() + mma_tile_coord_v = bid[0] % cute.size(tiled_mma.thr_id.shape) + is_leader_cta = mma_tile_coord_v == 0 + cta_rank_in_cluster = cute.arch.make_warp_uniform( + cute.arch.block_idx_in_cluster() + ) + block_in_cluster_coord_vmnk = cluster_layout_vmnk.get_flat_coord( + cta_rank_in_cluster + ) + # Coord inside cta + tidx, _, _ = cute.arch.thread_idx() + + # + # Alloc and init: tensormap buffer, a+b full/empty, accumulator full/empty, tensor memory dealloc barrier + # + smem = utils.SmemAllocator() + storage = smem.allocate(self.shared_storage) + + tensormap_a_smem_ptr = None + tensormap_b_smem_ptr = None + tensormap_c_smem_ptr = None + if cutlass.const_expr( + self.tensormap_update_mode == utils.TensorMapUpdateMode.SMEM + ): + tensormap_smem_ptr = storage.tensormap_buffer.data_ptr() + tensormap_a_smem_ptr = tensormap_smem_ptr + tensormap_b_smem_ptr = ( + tensormap_a_smem_ptr + GroupedGemmKernel.bytes_per_tensormap // 8 + ) + tensormap_c_smem_ptr = ( + tensormap_b_smem_ptr + GroupedGemmKernel.bytes_per_tensormap // 8 + ) + + # init barrier for loading A, B with TMA + ab_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread) + num_tma_producer = self.num_mcast_ctas_a + self.num_mcast_ctas_b - 1 + ab_pipeline_consumer_group = pipeline.CooperativeGroup( + pipeline.Agent.Thread, num_tma_producer + ) + ab_pipeline = pipeline.PipelineTmaUmma.create( + barrier_storage=storage.ab_full_mbar_ptr.data_ptr(), + num_stages=self.num_ab_stage, + producer_group=ab_pipeline_producer_group, + consumer_group=ab_pipeline_consumer_group, + tx_count=self.num_tma_load_bytes, + cta_layout_vmnk=cluster_layout_vmnk, + defer_sync=True, + ) + # Accumulator barrier init + acc_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread) + num_acc_consumer_threads = len(self.epilog_warp_id) * ( + 2 if use_2cta_instrs else 1 + ) + acc_pipeline_consumer_group = pipeline.CooperativeGroup( + pipeline.Agent.Thread, num_acc_consumer_threads + ) + acc_pipeline = pipeline.PipelineUmmaAsync.create( + barrier_storage=storage.acc_full_mbar_ptr.data_ptr(), + num_stages=self.num_acc_stage, + producer_group=acc_pipeline_producer_group, + consumer_group=acc_pipeline_consumer_group, + cta_layout_vmnk=cluster_layout_vmnk, + defer_sync=True, + ) + # Tensor memory dealloc barrier init + tmem = utils.TmemAllocator( + storage.tmem_holding_buf, + barrier_for_retrieve=self.tmem_alloc_barrier, + allocator_warp_id=self.epilog_warp_id[0], + is_two_cta=use_2cta_instrs, + two_cta_tmem_dealloc_mbar_ptr=storage.tmem_dealloc_mbar_ptr, + ) + + # Cluster arrive after barrier init + pipeline_init_arrive(cluster_shape_mn=self.cluster_shape_mn, is_relaxed=True) + + # + # Setup smem tensor A/B/C + # + # (EPI_TILE_M, EPI_TILE_N, STAGE) + sC = storage.sC.get_tensor( + epi_smem_layout_staged.outer, swizzle=epi_smem_layout_staged.inner + ) + # (MMA, MMA_M, MMA_K, STAGE) + sA = storage.sA.get_tensor( + a_smem_layout_staged.outer, swizzle=a_smem_layout_staged.inner + ) + # (MMA, MMA_N, MMA_K, STAGE) + sB = storage.sB.get_tensor( + b_smem_layout_staged.outer, swizzle=b_smem_layout_staged.inner + ) + + # + # Compute multicast mask for A/B buffer full and empty + # + a_full_mcast_mask = None + b_full_mcast_mask = None + ab_empty_mcast_mask = None + if cutlass.const_expr(self.is_a_mcast or self.is_b_mcast or use_2cta_instrs): + a_full_mcast_mask = cpasync.create_tma_multicast_mask( + cluster_layout_vmnk, block_in_cluster_coord_vmnk, mcast_mode=2 + ) + b_full_mcast_mask = cpasync.create_tma_multicast_mask( + cluster_layout_vmnk, block_in_cluster_coord_vmnk, mcast_mode=1 + ) + ab_empty_mcast_mask = a_full_mcast_mask | b_full_mcast_mask + acc_full_mcast_mask = None + if cutlass.const_expr(use_2cta_instrs): + acc_full_mcast_mask = cute.make_layout_image_mask( + cluster_layout_vmnk, block_in_cluster_coord_vmnk, mode=0 + ) + block_in_cluster_coord_vmnk_peer = ( + block_in_cluster_coord_vmnk[0] ^ 1, + *block_in_cluster_coord_vmnk[1:], + ) + a_full_mcast_mask_peer = cpasync.create_tma_multicast_mask( + cluster_layout_vmnk, block_in_cluster_coord_vmnk_peer, mcast_mode=2 + ) + b_full_mcast_mask_peer = cpasync.create_tma_multicast_mask( + cluster_layout_vmnk, block_in_cluster_coord_vmnk_peer, mcast_mode=1 + ) + ab_empty_mcast_mask = ( + a_full_mcast_mask_peer + | b_full_mcast_mask_peer + | cutlass.Int16( + 0 if ab_empty_mcast_mask is None else ab_empty_mcast_mask + ) + ) + + # + # Local_tile partition global tensors + # + # (bM, bK, RestM, RestK, RestL) + gA_mkl = cute.local_tile( + mA_mkl, cute.slice_(self.mma_tiler, (None, 0, None)), (None, None, None) + ) + # (bN, bK, RestN, RestK, RestL) + gB_nkl = cute.local_tile( + mB_nkl, cute.slice_(self.mma_tiler, (0, None, None)), (None, None, None) + ) + # (bM, bN, RestM, RestN, RestL) + gC_mnl = cute.local_tile( + mC_mnl, cute.slice_(self.mma_tiler, (None, None, 0)), (None, None, None) + ) + + # + # Partition global tensor for TiledMMA_A/B/C + # + thr_mma = tiled_mma.get_slice(mma_tile_coord_v) + # (MMA, MMA_M, MMA_K, RestM, RestK, RestL) + tCgA = thr_mma.partition_A(gA_mkl) + # (MMA, MMA_N, MMA_K, RestN, RestK, RestL) + tCgB = thr_mma.partition_B(gB_nkl) + # (MMA, MMA_M, MMA_N, RestM, RestN, RestL) + tCgC = thr_mma.partition_C(gC_mnl) + + # + # Partition global/shared tensor for load A, B with TMA + # + a_cta_layout = cute.make_layout( + cute.slice_(cluster_layout_vmnk, (0, 0, None, 0)).shape + ) + # ((atom_v, rest_v), STAGE) + # ((atom_v, rest_v), RestM, RestK, RestL) + tAsA, tAgA = cpasync.tma_partition( + tma_atom_a, + block_in_cluster_coord_vmnk[2], + a_cta_layout, + cute.group_modes(sA, 0, 3), + cute.group_modes(tCgA, 0, 3), + ) + # TMA load B partition_S/D + b_cta_layout = cute.make_layout( + cute.slice_(cluster_layout_vmnk, (0, None, 0, 0)).shape + ) + # ((atom_v, rest_v), STAGE) + # ((atom_v, rest_v), RestM, RestK, RestL) + tBsB, tBgB = cpasync.tma_partition( + tma_atom_b, + block_in_cluster_coord_vmnk[1], + b_cta_layout, + cute.group_modes(sB, 0, 3), + cute.group_modes(tCgB, 0, 3), + ) + + # + # Partition shared/tensor memory tensor for TiledMMA_A/B/C + # + # (MMA, MMA_M, MMA_K, STAGE) + tCrA = tiled_mma.make_fragment_A(sA) + # (MMA, MMA_N, MMA_K, STAGE) + tCrB = tiled_mma.make_fragment_B(sB) + # (MMA, MMA_M, MMA_N) + acc_shape = tiled_mma.partition_shape_C(self.mma_tiler[:2]) + # (MMA, MMA_M, MMA_N, STAGE) + tCtAcc_fake = tiled_mma.make_fragment_C( + cute.append(acc_shape, self.num_acc_stage) + ) + + # + # Cluster wait before tensor memory alloc + # + pipeline_init_wait(cluster_shape_mn=self.cluster_shape_mn) + + # + # Get tensormap buffer address + # + grid_dim = cute.arch.grid_dim() + tensormap_workspace_idx = ( + bid[2] * grid_dim[1] * grid_dim[0] + bid[1] * grid_dim[0] + bid[0] + ) + + tensormap_manager = utils.TensorMapManager( + self.tensormap_update_mode, GroupedGemmKernel.bytes_per_tensormap + ) + tensormap_a_ptr = tensormap_manager.get_tensormap_ptr( + tensormaps[(tensormap_workspace_idx, 0, None)].iterator + ) + tensormap_b_ptr = tensormap_manager.get_tensormap_ptr( + tensormaps[(tensormap_workspace_idx, 1, None)].iterator + ) + tensormap_c_ptr = tensormap_manager.get_tensormap_ptr( + tensormaps[(tensormap_workspace_idx, 2, None)].iterator + ) + # Setup tensormap initialization pointer based on the mode + if cutlass.const_expr( + self.tensormap_update_mode == utils.TensorMapUpdateMode.SMEM + ): + tensormap_a_init_ptr = tensormap_a_smem_ptr + tensormap_b_init_ptr = tensormap_b_smem_ptr + tensormap_c_init_ptr = tensormap_c_smem_ptr + else: + tensormap_a_init_ptr = tensormap_a_ptr + tensormap_b_init_ptr = tensormap_b_ptr + tensormap_c_init_ptr = tensormap_c_ptr + + # + # Persistent tile scheduling loop + # + # When the problem shapes are on device, we launch one CTA per SM. + # The if condition later prevents the warps from extra CTAs from doing any work. + tile_sched = utils.StaticPersistentGroupTileScheduler.create( + tile_sched_params, + bid, + grid_dim, + self.cluster_tile_shape_mnk, + utils.create_initial_search_state(), + group_count, + problem_sizes_mnkl, + ) + initial_work_tile_info = tile_sched.initial_work_tile_info() + + # + # Specialized TMA load warp + # + if warp_idx == self.tma_warp_id and initial_work_tile_info.is_valid_tile: + # Initialize tensormaps for A, B + if cutlass.const_expr(self.delegate_tensormap_ab_init == False): + tensormap_manager.init_tensormap_from_atom( + tma_atom_a, tensormap_a_init_ptr, self.tma_warp_id + ) + tensormap_manager.init_tensormap_from_atom( + tma_atom_b, tensormap_b_init_ptr, self.tma_warp_id + ) + + tensormap_init_done = cutlass.Boolean(False) + # group index of last tile + last_group_idx = cutlass.Int32(-1) + + work_tile = initial_work_tile_info + ab_producer_state = pipeline.make_pipeline_state( + pipeline.PipelineUserType.Producer, self.num_ab_stage + ) + + while work_tile.is_valid_tile: + grouped_gemm_cta_tile_info = work_tile.group_search_result + + cur_k_tile_cnt = grouped_gemm_cta_tile_info.cta_tile_count_k + is_k_tile_cnt_zero = cur_k_tile_cnt == 0 + cur_group_idx = grouped_gemm_cta_tile_info.group_idx + # Do not load any data if cur_k_tile_cnt is 0 + if not is_k_tile_cnt_zero: + is_group_changed = cur_group_idx != last_group_idx + # skip tensormap update if we're working on the same group + if is_group_changed: + real_tensor_a = self.make_tensor_for_tensormap_update( + cur_group_idx, + self.a_dtype, + ( + grouped_gemm_cta_tile_info.problem_shape_m, + grouped_gemm_cta_tile_info.problem_shape_n, + grouped_gemm_cta_tile_info.problem_shape_k, + ), + strides_abc, + ptrs_abc, + 0, # 0 for tensor A + ) + real_tensor_b = self.make_tensor_for_tensormap_update( + cur_group_idx, + self.b_dtype, + ( + grouped_gemm_cta_tile_info.problem_shape_m, + grouped_gemm_cta_tile_info.problem_shape_n, + grouped_gemm_cta_tile_info.problem_shape_k, + ), + strides_abc, + ptrs_abc, + 1, # 1 for tensor B + ) + # wait tensormap initialization complete before update + if not tensormap_init_done: + if cutlass.const_expr(self.delegate_tensormap_ab_init): + self.tensormap_ab_init_barrier.arrive_and_wait() + tensormap_manager.fence_tensormap_initialization() + tensormap_init_done = True + + tensormap_manager.update_tensormap( + (real_tensor_a, real_tensor_b), + (tma_atom_a, tma_atom_b), + (tensormap_a_ptr, tensormap_b_ptr), + self.tma_warp_id, + (tensormap_a_smem_ptr, tensormap_b_smem_ptr), + ) + + mma_tile_coord_mnl = ( + grouped_gemm_cta_tile_info.cta_tile_idx_m + // cute.size(tiled_mma.thr_id.shape), + grouped_gemm_cta_tile_info.cta_tile_idx_n, + 0, + ) + + # + # Slice to per mma tile index + # + # ((atom_v, rest_v), RestK) + tAgA_slice = tAgA[ + (None, mma_tile_coord_mnl[0], None, mma_tile_coord_mnl[2]) + ] + # ((atom_v, rest_v), RestK) + tBgB_slice = tBgB[ + (None, mma_tile_coord_mnl[1], None, mma_tile_coord_mnl[2]) + ] + + # Peek (try_wait) AB buffer empty for k_tile = prefetch_k_tile_cnt + ab_producer_state.reset_count() + peek_ab_empty_status = cutlass.Boolean(1) + if ab_producer_state.count < cur_k_tile_cnt: + peek_ab_empty_status = ab_pipeline.producer_try_acquire( + ab_producer_state + ) + # ensure the update to tensormap has completed before using it + if is_group_changed: + tensormap_manager.fence_tensormap_update(tensormap_a_ptr) + tensormap_manager.fence_tensormap_update(tensormap_b_ptr) + # + # Tma load loop + # + for k_tile in cutlass.range(0, cur_k_tile_cnt, 1, unroll=1): + # Wait for AB buffer empty + ab_pipeline.producer_acquire( + ab_producer_state, peek_ab_empty_status + ) + + # Load A/B with TMA + cute.copy( + tma_atom_a, + tAgA_slice[(None, ab_producer_state.count)], + tAsA[(None, ab_producer_state.index)], + tma_bar_ptr=ab_pipeline.producer_get_barrier( + ab_producer_state + ), + mcast_mask=a_full_mcast_mask, + tma_desc_ptr=tensormap_manager.get_tensormap_ptr( + tensormap_a_ptr, + cute.AddressSpace.generic, + ), + ) + cute.copy( + tma_atom_b, + tBgB_slice[(None, ab_producer_state.count)], + tBsB[(None, ab_producer_state.index)], + tma_bar_ptr=ab_pipeline.producer_get_barrier( + ab_producer_state + ), + mcast_mask=b_full_mcast_mask, + tma_desc_ptr=tensormap_manager.get_tensormap_ptr( + tensormap_b_ptr, + cute.AddressSpace.generic, + ), + ) + + # Peek (try_wait) AB buffer empty for k_tile = prefetch_k_tile_cnt + k_tile + 1 + ab_producer_state.advance() + peek_ab_empty_status = cutlass.Boolean(1) + if ab_producer_state.count < cur_k_tile_cnt: + peek_ab_empty_status = ab_pipeline.producer_try_acquire( + ab_producer_state + ) + else: + # If tensormap initialization is not done, wait for it to complete + if not tensormap_init_done: + if cutlass.const_expr(self.delegate_tensormap_ab_init): + self.tensormap_ab_init_barrier.arrive_and_wait() + tensormap_manager.fence_tensormap_initialization() + tensormap_init_done = True + # Advance to next tile + tile_sched.advance_to_next_work() + work_tile = tile_sched.get_current_work() + last_group_idx = cur_group_idx + + # + # Wait A/B buffer empty + # + ab_pipeline.producer_tail(ab_producer_state) + + # + # Specialized MMA warp + # + if warp_idx == self.mma_warp_id and initial_work_tile_info.is_valid_tile: + # Bar sync for retrieve tmem ptr from shared mem + tmem.wait_for_alloc() + + # + # Retrieving tensor memory ptr and make accumulator tensor + # + tmem_ptr = tmem.retrieve_ptr(self.acc_dtype) + # (MMA, MMA_M, MMA_N, STAGE) + tCtAcc_base = cute.make_tensor(tmem_ptr, tCtAcc_fake.layout) + + # + # Persistent tile scheduling loop + # + work_tile = initial_work_tile_info + ab_consumer_state = pipeline.make_pipeline_state( + pipeline.PipelineUserType.Consumer, self.num_ab_stage + ) + acc_producer_state = pipeline.make_pipeline_state( + pipeline.PipelineUserType.Producer, self.num_acc_stage + ) + + # tile count we have searched + while work_tile.is_valid_tile: + cur_group_idx = work_tile.group_search_result.group_idx + problem_shape_k = work_tile.group_search_result.problem_shape_k + + # MMA warp is only interested in number of tiles along K dimension + cur_k_tile_cnt = ( + problem_shape_k + self.cluster_tile_shape_mnk[2] - 1 + ) // self.cluster_tile_shape_mnk[2] + is_k_tile_cnt_zero = cur_k_tile_cnt == 0 + + # (MMA, MMA_M, MMA_N) + tCtAcc = tCtAcc_base[(None, None, None, acc_producer_state.index)] + + # Peek (try_wait) AB buffer full for k_tile = 0 + ab_consumer_state.reset_count() + peek_ab_full_status = cutlass.Boolean(1) + if is_leader_cta: + if ab_consumer_state.count < cur_k_tile_cnt: + peek_ab_full_status = ab_pipeline.consumer_try_wait( + ab_consumer_state + ) + + # + # Wait for accumulator buffer empty + # + if not is_k_tile_cnt_zero: + acc_pipeline.producer_acquire(acc_producer_state) + + # + # Reset the ACCUMULATE field for each tile + # + tiled_mma.set(tcgen05.Field.ACCUMULATE, False) + + # + # Mma mainloop + # + for k_tile in cutlass.range(0, cur_k_tile_cnt, 1, unroll=1): + # Wait for AB buffer full + ab_pipeline.consumer_wait( + ab_consumer_state, peek_ab_full_status + ) + # tCtAcc += tCrA * tCrB + num_kblocks = cute.size(tCrA, mode=[2]) + for kblock_idx in cutlass.range(num_kblocks, unroll_full=True): + kblock_coord = ( + None, + None, + kblock_idx, + ab_consumer_state.index, + ) + + cute.gemm( + tiled_mma, + tCtAcc, + tCrA[kblock_coord], + tCrB[kblock_coord], + tCtAcc, + ) + # Enable accumulate on tCtAcc after first kblock + tiled_mma.set(tcgen05.Field.ACCUMULATE, True) + + # Async arrive AB buffer empty + ab_pipeline.consumer_release(ab_consumer_state) + + # Peek (try_wait) AB buffer full for k_tile = k_tile + 1 + ab_consumer_state.advance() + peek_ab_full_status = cutlass.Boolean(1) + if ab_consumer_state.count < cur_k_tile_cnt: + peek_ab_full_status = ab_pipeline.consumer_try_wait( + ab_consumer_state + ) + + # + # Async arrive accumulator buffer full + # + if not is_k_tile_cnt_zero: + acc_pipeline.producer_commit(acc_producer_state) + acc_producer_state.advance() + + # + # Advance to next tile + # + tile_sched.advance_to_next_work() + work_tile = tile_sched.get_current_work() + # + # Wait for accumulator buffer empty + # + acc_pipeline.producer_tail(acc_producer_state) + + # + # Specialized epilogue warps + # + if warp_idx < self.mma_warp_id and initial_work_tile_info.is_valid_tile: + # initialize tensormap A, B for TMA warp + if cutlass.const_expr(self.delegate_tensormap_ab_init): + tensormap_manager.init_tensormap_from_atom( + tma_atom_a, tensormap_a_init_ptr, self.epilog_warp_id[0] + ) + tensormap_manager.init_tensormap_from_atom( + tma_atom_b, tensormap_b_init_ptr, self.epilog_warp_id[0] + ) + # signal tensormap initialization has finished + self.tensormap_ab_init_barrier.arrive_and_wait() + # initialize tensorap for C + tensormap_manager.init_tensormap_from_atom( + tma_atom_c, + tensormap_c_init_ptr, + self.epilog_warp_id[0], + ) + # Alloc tensor memory buffer + tmem.allocate(self.num_tmem_alloc_cols) + + # + # Bar sync for retrieve tensor memory ptr from shared memory + # + tmem.wait_for_alloc() + + # + # Retrieving tensor memory ptr and make accumulator tensor + # + tmem_ptr = tmem.retrieve_ptr(self.acc_dtype) + # (MMA, MMA_M, MMA_N, STAGE) + tCtAcc_base = cute.make_tensor(tmem_ptr, tCtAcc_fake.layout) + + epi_tidx = tidx + # + # Partition for epilogue + # + ( + tiled_copy_t2r, + tTR_tAcc_base, + tTR_rAcc, + ) = self.epilog_tmem_copy_and_partition( + epi_tidx, tCtAcc_base, tCgC, epi_tile, use_2cta_instrs + ) + + tTR_rC = cute.make_rmem_tensor(tTR_rAcc.shape, self.c_dtype) + tiled_copy_r2s, tRS_rC, tRS_sC = self.epilog_smem_copy_and_partition( + tiled_copy_t2r, tTR_rC, epi_tidx, sC + ) + ( + tma_atom_c, + bSG_sC, + bSG_gC_partitioned, + ) = self.epilog_gmem_copy_and_partition(tma_atom_c, tCgC, epi_tile, sC) + + # + # Persistent tile scheduling loop + # + + work_tile = initial_work_tile_info + + # wait tensormap initialization complete before update + tensormap_manager.fence_tensormap_initialization() + acc_consumer_state = pipeline.make_pipeline_state( + pipeline.PipelineUserType.Consumer, self.num_acc_stage + ) + # Threads/warps participating in tma store pipeline + c_producer_group = pipeline.CooperativeGroup( + pipeline.Agent.Thread, + 32 * len(self.epilog_warp_id), + ) + c_pipeline = pipeline.PipelineTmaStore.create( + num_stages=self.num_epi_stage, + producer_group=c_producer_group, + ) + # group index of last tile + last_group_idx = cutlass.Int32(-1) + while work_tile.is_valid_tile: + grouped_gemm_cta_tile_info = work_tile.group_search_result + cur_group_idx = grouped_gemm_cta_tile_info.group_idx + cur_k_tile_cnt = grouped_gemm_cta_tile_info.cta_tile_count_k + is_k_tile_cnt_zero = cur_k_tile_cnt == 0 + is_group_changed = cur_group_idx != last_group_idx + # We still need to store 0s when k_tile_cnt is 0 + if is_group_changed: + # construct tensor C based on real address, shape and stride information + real_tensor_c = self.make_tensor_for_tensormap_update( + cur_group_idx, + self.c_dtype, + ( + grouped_gemm_cta_tile_info.problem_shape_m, + grouped_gemm_cta_tile_info.problem_shape_n, + grouped_gemm_cta_tile_info.problem_shape_k, + ), + strides_abc, + ptrs_abc, + 2, # 2 for tensor C + ) + tensormap_manager.update_tensormap( + ((real_tensor_c),), + ((tma_atom_c),), + ((tensormap_c_ptr),), + self.epilog_warp_id[0], + (tensormap_c_smem_ptr,), + ) + + mma_tile_coord_mnl = ( + grouped_gemm_cta_tile_info.cta_tile_idx_m + // cute.size(tiled_mma.thr_id.shape), + grouped_gemm_cta_tile_info.cta_tile_idx_n, + 0, + ) + + # + # Slice to per mma tile index + # + # ((ATOM_V, REST_V), EPI_M, EPI_N) + bSG_gC = bSG_gC_partitioned[ + ( + None, + None, + None, + *mma_tile_coord_mnl, + ) + ] + # (T2R, T2R_M, T2R_N, EPI_M, EPI_M) + tTR_tAcc = tTR_tAcc_base[ + (None, None, None, None, None, acc_consumer_state.index) + ] + # + # Wait for accumulator buffer full + # + # Not waiting for accumulator buffer full when k_tile_cnt is 0 + if not is_k_tile_cnt_zero: + acc_pipeline.consumer_wait(acc_consumer_state) + + tTR_tAcc = cute.group_modes(tTR_tAcc, 3, cute.rank(tTR_tAcc)) + bSG_gC = cute.group_modes(bSG_gC, 1, cute.rank(bSG_gC)) + # ensure the update to tensormap has completed before using it + if is_group_changed: + if warp_idx == self.epilog_warp_id[0]: + tensormap_manager.fence_tensormap_update(tensormap_c_ptr) + # + # Store accumulator to global memory in subtiles + # + subtile_cnt = cute.size(tTR_tAcc.shape, mode=[3]) + num_prev_subtiles = tile_sched.num_tiles_executed * subtile_cnt + for subtile_idx in range(subtile_cnt): + # + # Store C to shared memory + # + epi_buffer = (num_prev_subtiles + subtile_idx) % self.num_epi_stage + # + # Load accumulator from tensor memory buffer to register + # + tTR_tAcc_mn = tTR_tAcc[(None, None, None, subtile_idx)] + if not is_k_tile_cnt_zero: + cute.copy(tiled_copy_t2r, tTR_tAcc_mn, tTR_rAcc) + + # + # Convert to output type + # + acc_vec = tiled_copy_r2s.retile(tTR_rAcc).load() + tRS_rC.store(acc_vec.to(self.c_dtype)) + else: + tRS_rC.fill(0) + cute.copy( + tiled_copy_r2s, + tRS_rC, + tRS_sC[(None, None, None, epi_buffer)], + ) + # Fence and barrier to make sure shared memory store is visible to TMA store + cute.arch.fence_proxy( + "async.shared", + space="cta", + ) + self.epilog_sync_barrier.arrive_and_wait() + # + # store C to global memory with TMA + # + if warp_idx == self.epilog_warp_id[0]: + cute.copy( + tma_atom_c, + bSG_sC[(None, epi_buffer)], + bSG_gC[(None, subtile_idx)], + tma_desc_ptr=tensormap_manager.get_tensormap_ptr( + tensormap_c_ptr, + cute.AddressSpace.generic, + ), + ) + # Fence and barrier to make sure shared memory store is visible to TMA store + c_pipeline.producer_commit() + c_pipeline.producer_acquire() + self.epilog_sync_barrier.arrive_and_wait() + # + # Async arrive accumulator buffer empty + # + if not is_k_tile_cnt_zero: + with cute.arch.elect_one(): + acc_pipeline.consumer_release(acc_consumer_state) + acc_consumer_state.advance() + + # + # Advance to next tile + # + tile_sched.advance_to_next_work() + work_tile = tile_sched.get_current_work() + last_group_idx = cur_group_idx + + # + # Dealloc the tensor memory buffer + # + tmem.relinquish_alloc_permit() + self.epilog_sync_barrier.arrive_and_wait() + tmem.free(tmem_ptr) + + # + # Wait for C store complete + # + c_pipeline.producer_tail() + + @cute.jit + def make_tensor_for_tensormap_update( + self, + group_idx: cutlass.Int32, + dtype: Type[cutlass.Numeric], + problem_shape_mnk: tuple[cutlass.Int32, cutlass.Int32, cutlass.Int32], + strides_abc: cute.Tensor, + tensor_address_abc: cute.Tensor, + tensor_index: int, + ): + """Extract stride and tensor address for a given group and construct a global tensor. + + This function is used within the kernel to dynamically create a CUTE tensor + representing A, B, or C for the current group being processed, using the + group-specific address, shape, and stride information. + + :param group_idx: The index of the current group within the grouped GEMM. + :type group_idx: cutlass.Int32 + :param dtype: The data type of the tensor elements (e.g., cutlass.Float16). + :type dtype: Type[cutlass.Numeric] + :param problem_shape_mnk: The (M, N, K) problem shape for the current group. + :type problem_shape_mnk: tuple[cutlass.Int32, cutlass.Int32, cutlass.Int32] + :param strides_abc: Tensor containing strides for A, B, C for all groups. Layout: (group_count, 3, 2). + :type strides_abc: cute.Tensor + :param tensor_address_abc: Tensor containing global memory addresses for A, B, C for all groups. Layout: (group_count, 3). + :type tensor_address_abc: cute.Tensor + :param tensor_index: Specifies which tensor to create: 0 for A, 1 for B, 2 for C. + :type tensor_index: int + :return: A CUTE tensor representing the requested global memory tensor (A, B, or C) for the specified group. + :rtype: cute.Tensor + :raises TypeError: If the provided dtype is not a subclass of cutlass.Numeric. + """ + ptr_i64 = tensor_address_abc[(group_idx, tensor_index)] + if cutlass.const_expr( + not isclass(dtype) or not issubclass(dtype, cutlass.Numeric) + ): + raise TypeError( + f"dtype must be a type of cutlass.Numeric, got {type(dtype)}" + ) + tensor_gmem_ptr = cute.make_ptr( + dtype, ptr_i64, cute.AddressSpace.gmem, assumed_align=16 + ) + + strides_tensor_gmem = strides_abc[(group_idx, tensor_index, None)] + strides_tensor_reg = cute.make_rmem_tensor( + cute.make_layout(2), + strides_abc.element_type, + ) + cute.autovec_copy(strides_tensor_gmem, strides_tensor_reg) + stride_mn = strides_tensor_reg[0] + stride_k = strides_tensor_reg[1] + c1 = cutlass.Int32(1) + c0 = cutlass.Int32(0) + + if cutlass.const_expr(tensor_index == 0): # tensor A + m = problem_shape_mnk[0] + k = problem_shape_mnk[2] + return cute.make_tensor( + tensor_gmem_ptr, + cute.make_layout((m, k, c1), stride=(stride_mn, stride_k, c0)), + ) + elif cutlass.const_expr(tensor_index == 1): # tensor B + n = problem_shape_mnk[1] + k = problem_shape_mnk[2] + return cute.make_tensor( + tensor_gmem_ptr, + cute.make_layout((n, k, c1), stride=(stride_mn, stride_k, c0)), + ) + else: # tensor C + m = problem_shape_mnk[0] + n = problem_shape_mnk[1] + return cute.make_tensor( + tensor_gmem_ptr, + cute.make_layout((m, n, c1), stride=(stride_mn, stride_k, c0)), + ) + + def epilog_tmem_copy_and_partition( + self, + tidx: cutlass.Int32, + tAcc: cute.Tensor, + gC_mnl: cute.Tensor, + epi_tile: cute.Tile, + use_2cta_instrs: Union[cutlass.Boolean, bool], + ) -> tuple[cute.TiledCopy, cute.Tensor, cute.Tensor]: + """ + Make tiledCopy for tensor memory load, then use it to partition tensor memory (source) and register array (destination). + + :param tidx: The thread index in epilogue warp groups + :type tidx: cutlass.Int32 + :param tAcc: The accumulator tensor to be copied and partitioned + :type tAcc: cute.Tensor + :param gC_mnl: The global tensor C + :type gC_mnl: cute.Tensor + :param epi_tile: The epilogue tiler + :type epi_tile: cute.Tile + :param use_2cta_instrs: Whether use_2cta_instrs is enabled + :type use_2cta_instrs: bool + + :return: A tuple containing (tiled_copy_t2r, tTR_tAcc, tTR_rAcc) where: + - tiled_copy_t2r: The tiled copy operation for tmem to register copy(t2r) + - tTR_tAcc: The partitioned accumulator tensor + - tTR_rAcc: The accumulated tensor in register used to hold t2r results + :rtype: Tuple[cute.TiledCopy, cute.Tensor, cute.Tensor] + """ + # Make tiledCopy for tensor memory load(t2r) + copy_atom_t2r = sm100_utils.get_tmem_load_op( + self.cta_tile_shape_mnk, + self.c_layout, + self.c_dtype, + self.acc_dtype, + epi_tile, + use_2cta_instrs, + ) + # (EPI_TILE_M, EPI_TILE_N, EPI_M, EPI_N, STAGE) + tAcc_epi = cute.flat_divide( + tAcc[((None, None), 0, 0, None)], + epi_tile, + ) + # (EPI_TILE_M, EPI_TILE_N) + tiled_copy_t2r = tcgen05.make_tmem_copy( + copy_atom_t2r, tAcc_epi[(None, None, 0, 0, 0)] + ) + + thr_copy_t2r = tiled_copy_t2r.get_slice(tidx) + # (T2R, T2R_M, T2R_N, EPI_M, EPI_M, STAGE) + tTR_tAcc = thr_copy_t2r.partition_S(tAcc_epi) + + # (EPI_TILE_M, EPI_TILE_N, EPI_M, EPI_N, RestM, RestN, RestL) + gC_mnl_epi = cute.flat_divide( + gC_mnl[((None, None), 0, 0, None, None, None)], epi_tile + ) + # (T2R, T2R_M, T2R_N, EPI_M, EPI_N, RestM, RestN, RestL) + tTR_gC = thr_copy_t2r.partition_D(gC_mnl_epi) + # (T2R, T2R_M, T2R_N) + tTR_rAcc = cute.make_rmem_tensor( + tTR_gC[(None, None, None, 0, 0, 0, 0, 0)].shape, self.acc_dtype + ) + return tiled_copy_t2r, tTR_tAcc, tTR_rAcc + + def epilog_smem_copy_and_partition( + self, + tiled_copy_t2r: cute.TiledCopy, + tTR_rC: cute.Tensor, + tidx: cutlass.Int32, + sC: cute.Tensor, + ) -> tuple[cute.TiledCopy, cute.Tensor, cute.Tensor]: + """ + Make tiledCopy for shared memory store, then use it to partition register array (source) and shared memory (destination). + + :param tiled_copy_t2r: The tiled copy operation for tmem to register copy(t2r) + :type tiled_copy_t2r: cute.TiledCopy + :param tTR_rC: The partitioned accumulator tensor + :type tTR_rC: cute.Tensor + :param tidx: The thread index in epilogue warp groups + :type tidx: cutlass.Int32 + :param sC: The shared memory tensor to be copied and partitioned + :type sC: cute.Tensor + + :return: A tuple containing (tiled_copy_r2s, tRS_rC, tRS_sC) where: + - tiled_copy_r2s: The tiled copy operation for register to smem copy(r2s) + - tRS_rC: The partitioned tensor C (register source) + - tRS_sC: The partitioned tensor C (smem destination) + :rtype: Tuple[cute.TiledCopy, cute.Tensor, cute.Tensor] + """ + copy_atom_r2s = sm100_utils.get_smem_store_op( + self.c_layout, self.c_dtype, self.acc_dtype, tiled_copy_t2r + ) + tiled_copy_r2s = cute.make_tiled_copy_D(copy_atom_r2s, tiled_copy_t2r) + # (R2S, R2S_M, R2S_N, PIPE_D) + thr_copy_r2s = tiled_copy_r2s.get_slice(tidx) + tRS_sC = thr_copy_r2s.partition_D(sC) + # (R2S, R2S_M, R2S_N) + tRS_rC = tiled_copy_r2s.retile(tTR_rC) + return tiled_copy_r2s, tRS_rC, tRS_sC + + def epilog_gmem_copy_and_partition( + self, + tma_atom_c: cute.CopyAtom, + gC_mnl: cute.Tensor, + epi_tile: cute.Tile, + sC: cute.Tensor, + ) -> tuple[cute.CopyAtom, cute.Tensor, cute.Tensor]: + """Make tiledCopy for global memory store, then use it to partition + shared memory (source) and global memory (destination) for TMA store version. + + :param tma_atom_c: The TMA copy atom configured for storing tensor C. + :type tma_atom_c: cute.CopyAtom + :param gC_mnl: The global memory tensor C. + :type gC_mnl: cute.Tensor + :param epi_tile: The epilogue tiler defining the granularity of the operation. + :type epi_tile: cute.Tile + :param sC: The shared memory epilogue buffer tensor. + :type sC: cute.Tensor + :return: A tuple containing: + - tma_atom_c: The input TMA copy atom (passed through). + - bSG_sC: The source shared memory tensor partitioned for the TMA operation. + - tCgC: The destination global memory tensor partitioned for the TMA operation. + :rtype: tuple[cute.CopyAtom, cute.Tensor, cute.Tensor] + """ + # (EPI_TILE_M, EPI_TILE_N, EPI_M, EPI_N, RestM, RestN, RestL) + gC_epi = cute.flat_divide( + gC_mnl[((None, None), 0, 0, None, None, None)], epi_tile + ) + sC_for_tma_partition = cute.group_modes(sC, 0, 2) + gC_for_tma_partition = cute.group_modes(gC_epi, 0, 2) + # ((ATOM_V, REST_V), EPI_M, EPI_N) + # ((ATOM_V, REST_V), EPI_M, EPI_N, RestM, RestN, RestL) + bSG_sC, bSG_gC = cpasync.tma_partition( + tma_atom_c, + 0, + cute.make_layout(1), + sC_for_tma_partition, + gC_for_tma_partition, + ) + return tma_atom_c, bSG_sC, bSG_gC + + @staticmethod + def _compute_stages( + tiled_mma: cute.TiledMma, + mma_tiler_mnk: tuple[int, int, int], + a_dtype: type[cutlass.Numeric], + b_dtype: type[cutlass.Numeric], + epi_tile: cute.Tile, + c_dtype: type[cutlass.Numeric], + c_layout: utils.LayoutEnum, + smem_capacity: int, + occupancy: int, + ) -> tuple[int, int, int]: + """Computes the number of stages for accumulator, A/B operands, and epilogue based on heuristics. + + :param tiled_mma: The tiled MMA object defining the core computation. + :type tiled_mma: cute.TiledMma + :param mma_tiler_mnk: The shape (M, N, K) of the MMA tiler. + :type mma_tiler_mnk: tuple[int, int, int] + :param a_dtype: Data type of operand A. + :type a_dtype: type[cutlass.Numeric] + :param b_dtype: Data type of operand B. + :type b_dtype: type[cutlass.Numeric] + :param epi_tile: The epilogue tile shape. + :type epi_tile: cute.Tile + :param c_dtype: Data type of operand C (output). + :type c_dtype: type[cutlass.Numeric] + :param c_layout: Layout enum of operand C in global memory. + :type c_layout: utils.LayoutEnum + :param smem_capacity: Total available shared memory capacity in bytes. + :type smem_capacity: int + :param occupancy: Target number of CTAs per SM (occupancy). + :type occupancy: int + + :return: A tuple containing the computed number of stages for: + (accumulator stages, A/B operand stages, epilogue stages) + :rtype: tuple[int, int, int] + """ + # Default accumulator and epilogue stages + num_acc_stage = 2 + num_epi_stage = 2 + + # Calculate smem layout and size for one stage of A, B, and Epilogue + a_smem_layout_stage_one = sm100_utils.make_smem_layout_a( + tiled_mma, + mma_tiler_mnk, + a_dtype, + 1, # stage=1 + ) + b_smem_layout_staged_one = sm100_utils.make_smem_layout_b( + tiled_mma, + mma_tiler_mnk, + b_dtype, + 1, # stage=1 + ) + epi_smem_layout_staged_one = sm100_utils.make_smem_layout_epi( + c_dtype, + c_layout, + epi_tile, + 1, # stage=1 + ) + ab_bytes_per_stage = cute.size_in_bytes( + a_dtype, a_smem_layout_stage_one + ) + cute.size_in_bytes(b_dtype, b_smem_layout_staged_one) + + epi_bytes_per_stage = cute.size_in_bytes(c_dtype, epi_smem_layout_staged_one) + epi_bytes = epi_bytes_per_stage * num_epi_stage + + # Calculate A/B stages: + # Start with total smem per CTA (capacity / occupancy) + # Subtract reserved bytes and initial epilogue bytes + # Divide remaining by bytes needed per A/B stage + num_ab_stage = ( + smem_capacity // occupancy + - GroupedGemmKernel.reserved_smem_bytes + - epi_bytes + ) // ab_bytes_per_stage + + # Refine epilogue stages: + # Calculate remaining smem after allocating for A/B stages and reserved bytes + # Add remaining unused smem to epilogue + remaining_smem = ( + smem_capacity + - occupancy * ab_bytes_per_stage * num_ab_stage + - occupancy * (GroupedGemmKernel.reserved_smem_bytes + epi_bytes) + ) + num_epi_stage += remaining_smem // (occupancy * epi_bytes_per_stage) + return num_acc_stage, num_ab_stage, num_epi_stage + + @staticmethod + def _compute_grid( + total_num_clusters: int, + cluster_shape_mn: tuple[int, int], + max_active_clusters: cutlass.Constexpr[int], + ) -> tuple[utils.PersistentTileSchedulerParams, tuple[int, int, int]]: + """Compute tile scheduler parameters and grid shape for grouped GEMM operations. + + :param total_num_clusters: Total number of clusters to process across all groups. + :type total_num_clusters: int + :param cluster_shape_mn: Shape of each cluster in M, N dimensions. + :type cluster_shape_mn: tuple[int, int] + :param max_active_clusters: Maximum number of active clusters. + :type max_active_clusters: cutlass.Constexpr[int] + + :return: A tuple containing: + - tile_sched_params: Parameters for the persistent tile scheduler. + - grid: Grid shape for kernel launch. + :rtype: tuple[utils.PersistentTileSchedulerParams, tuple[int, ...]] + """ + # Create problem shape with M, N dimensions from cluster shape + # and L dimension representing the total number of clusters. + problem_shape_ntile_mnl = ( + cluster_shape_mn[0], + cluster_shape_mn[1], + cutlass.Int32(total_num_clusters), + ) + + tile_sched_params = utils.PersistentTileSchedulerParams( + problem_shape_ntile_mnl, (*cluster_shape_mn, 1) + ) + + grid = utils.StaticPersistentGroupTileScheduler.get_grid_shape( + tile_sched_params, max_active_clusters + ) + + return tile_sched_params, grid + + @staticmethod + def _get_mbar_smem_bytes(**kwargs_stages: int) -> int: + """Calculate shared memory consumption for memory barriers based on provided stages. + + Each stage requires 2 barriers, and each barrier consumes 8 bytes of shared memory. + The total consumption is the sum across all provided stages. This function calculates the total + shared memory needed for these barriers. + + :param kwargs_stages: Variable keyword arguments where each key is a stage name + (e.g., num_acc_stage, num_ab_stage) and each value is the + number of stages of that type. + :type kwargs_stages: int + :return: Total shared memory bytes required for all memory barriers. + :rtype: int + """ + num_barriers_per_stage = 2 + num_bytes_per_barrier = 8 + mbar_smem_consumption = sum( + [ + num_barriers_per_stage * num_bytes_per_barrier * stage + for stage in kwargs_stages.values() + ] + ) + return mbar_smem_consumption + + @staticmethod + def _get_tensormap_smem_bytes( + tensormap_update_mode: utils.TensorMapUpdateMode, + ) -> int: + """Get the SMEM consumption for the tensormap buffer based on the update mode. + + :param tensormap_update_mode: Specifies whether tensormaps are updated in GMEM or SMEM. + :type tensormap_update_mode: utils.TensorMapUpdateMode + :return: The shared memory bytes required for the tensormap buffer. Returns 0 if mode is GMEM. + :rtype: int + :raises ValueError: If an invalid tensormap update mode is provided. + """ + if tensormap_update_mode == utils.TensorMapUpdateMode.GMEM: + return 0 + elif tensormap_update_mode == utils.TensorMapUpdateMode.SMEM: + return ( + GroupedGemmKernel.bytes_per_tensormap * GroupedGemmKernel.num_tensormaps + ) + else: + raise ValueError(f"Invalid tensormap update mode: {tensormap_update_mode}") + + @staticmethod + def _compute_num_tmem_alloc_cols( + tiled_mma: cute.TiledMma, + mma_tiler: tuple[int, int, int], + num_acc_stage: int, + ) -> int: + """ + Compute the number of tensor memory allocation columns. + + :param tiled_mma: The tiled MMA object defining the core computation. + :type tiled_mma: cute.TiledMma + :param mma_tiler: The shape (M, N, K) of the MMA tile. + :type mma_tiler: tuple[int, int, int] + :param acc_stage: The stage of the accumulator tensor. + :type acc_stage: int + + :return: The number of tensor memory allocation columns. + :rtype: int + """ + acc_shape = tiled_mma.partition_shape_C(mma_tiler[:2]) + tCtAcc_fake = tiled_mma.make_fragment_C(cute.append(acc_shape, num_acc_stage)) + num_tmem_alloc_cols = utils.get_num_tmem_alloc_cols(tCtAcc_fake) + + return num_tmem_alloc_cols + + # Size of smem we reserved for mbarrier, tensor memory management and tensormap update + reserved_smem_bytes = 1024 + bytes_per_tensormap = 128 + num_tensormaps = 3 + # size of smem used for tensor memory management + tensor_memory_management_bytes = 12 + + +# Create tensor and return the pointer, tensor, and stride +def create_tensor_and_stride( + l: int, + mode0: int, + mode1: int, + is_mode0_major: bool, + dtype: type[cutlass.Numeric], + is_dynamic_layout: bool = True, + torch_tensor_cpu: torch.Tensor = None, +) -> tuple[int, torch.Tensor, cute.Tensor, torch.Tensor, tuple[int, int]]: + """Create GPU tensor from either a new or existing CPU tensor. + + :param torch_tensor_cpu: Optional existing CPU tensor to reuse. If None, creates a new one. + :type torch_tensor_cpu: torch.Tensor, optional + """ + if torch_tensor_cpu is None: + # Create new CPU tensor + torch_tensor_cpu = cutlass_torch.matrix(l, mode0, mode1, is_mode0_major, dtype) + + # Create GPU tensor from CPU tensor (new or existing) + cute_tensor, torch_tensor = cutlass_torch.cute_tensor_like( + torch_tensor_cpu, dtype, is_dynamic_layout, assumed_align=16 + ) + return ( + torch_tensor.data_ptr(), + torch_tensor, + cute_tensor, + torch_tensor_cpu, + torch_tensor.stride()[:-1], + ) + + +def create_tensors_for_all_groups( + problem_sizes_mnkl: List[tuple[int, int, int, int]], + ab_dtype: Type[cutlass.Numeric], + c_dtype: Type[cutlass.Numeric], + a_major: str, + b_major: str, + c_major: str, + torch_fp32_tensors_abc: List[List[torch.Tensor]] = None, +) -> tuple[ + List[List[int]], + List[List[torch.Tensor]], + List[tuple], + List[List[tuple]], + List[List[torch.Tensor]], +]: + if torch_fp32_tensors_abc is not None and len(torch_fp32_tensors_abc) != len( + problem_sizes_mnkl + ): + raise ValueError("torch_fp32_tensors_abc must have one entry per group") + + # Initialize lists to store tensors for all groups + new_torch_fp32_tensors_abc = ( + [] if torch_fp32_tensors_abc is None else torch_fp32_tensors_abc + ) + torch_tensors_abc = [] + cute_tensors_abc = [] + strides_abc = [] + ptrs_abc = [] + + # Iterate through all groups and create tensors for each group + for group_idx, (m, n, k, l) in enumerate(problem_sizes_mnkl): + # Get existing CPU tensors if available, otherwise None + existing_cpu_a = ( + torch_fp32_tensors_abc[group_idx][0] if torch_fp32_tensors_abc else None + ) + existing_cpu_b = ( + torch_fp32_tensors_abc[group_idx][1] if torch_fp32_tensors_abc else None + ) + existing_cpu_c = ( + torch_fp32_tensors_abc[group_idx][2] if torch_fp32_tensors_abc else None + ) + + # Create tensors (reusing CPU tensors if provided) + ( + ptr_a, + torch_tensor_a, + cute_tensor_a, + tensor_fp32_a, + stride_mk_a, + ) = create_tensor_and_stride( + l, m, k, a_major == "m", ab_dtype, torch_tensor_cpu=existing_cpu_a + ) + ( + ptr_b, + torch_tensor_b, + cute_tensor_b, + tensor_fp32_b, + stride_nk_b, + ) = create_tensor_and_stride( + l, n, k, b_major == "n", ab_dtype, torch_tensor_cpu=existing_cpu_b + ) + ( + ptr_c, + torch_tensor_c, + cute_tensor_c, + tensor_fp32_c, + stride_mn_c, + ) = create_tensor_and_stride( + l, m, n, c_major == "m", c_dtype, torch_tensor_cpu=existing_cpu_c + ) + + # Only append to new_torch_fp32_tensors_abc if we created new CPU tensors + if torch_fp32_tensors_abc is None: + new_torch_fp32_tensors_abc.append( + [tensor_fp32_a, tensor_fp32_b, tensor_fp32_c] + ) + + ptrs_abc.append([ptr_a, ptr_b, ptr_c]) + torch_tensors_abc.append([torch_tensor_a, torch_tensor_b, torch_tensor_c]) + strides_abc.append([stride_mk_a, stride_nk_b, stride_mn_c]) + cute_tensors_abc.append( + ( + cute_tensor_a, + cute_tensor_b, + cute_tensor_c, + ) + ) + + return ( + ptrs_abc, + torch_tensors_abc, + cute_tensors_abc, + strides_abc, + new_torch_fp32_tensors_abc, + ) + + +def run( + num_groups: int, + problem_sizes_mnkl: tuple[int, int, int, int], + host_problem_shape_available: bool, + ab_dtype: Type[cutlass.Numeric], + c_dtype: Type[cutlass.Numeric], + acc_dtype: Type[cutlass.Numeric], + a_major: str, + b_major: str, + c_major: str, + mma_tiler_mn: tuple[int, int], + cluster_shape_mn: tuple[int, int], + use_2cta_instrs: bool, + tensormap_update_mode: utils.TensorMapUpdateMode, + tolerance: float, + warmup_iterations: int, + iterations: int, + skip_ref_check: bool, + use_cold_l2: bool = False, + **kwargs, +): + """Run grouped GEMM example with specified configurations. + + :param use_cold_l2: Whether to use circular buffer strategy to ensure cold L2 cache, defaults to False + :type use_cold_l2: bool, optional + :return: Execution time of the GEMM kernel in microseconds + :rtype: float + """ + print("Running Blackwell Grouped GEMM test with:") + print(f"{num_groups} groups") + for i, (m, n, k, l) in enumerate(problem_sizes_mnkl): + print(f"Group {i}: {m}x{n}x{k}x{l}") + print(f"AB dtype: {ab_dtype}, C dtype: {c_dtype}, Acc dtype: {acc_dtype}") + print(f"Matrix majors - A: {a_major}, B: {b_major}, C: {c_major}") + print(f"Mma Tiler (M, N): {mma_tiler_mn}, Cluster Shape (M, N): {cluster_shape_mn}") + print(f"2CTA MMA instructions: {'True' if use_2cta_instrs else 'False'}") + print(f"Tensor map update mode: {tensormap_update_mode}") + print(f"Tolerance: {tolerance}") + print(f"Warmup iterations: {warmup_iterations}") + print(f"Iterations: {iterations}") + print(f"Skip reference checking: {skip_ref_check}") + print(f"Use cold L2: {'True' if use_cold_l2 else 'False'}") + + # Skip unsupported types + if ab_dtype not in { + cutlass.Float16, + cutlass.BFloat16, + }: + raise ValueError(f"Skip unsupported ab_dtype {ab_dtype}") + if c_dtype not in {cutlass.Float16, cutlass.BFloat16, cutlass.Float32}: + raise ValueError(f"Skip unsupported c_dtype {c_dtype}") + # Skip unsupported acc dtype + if acc_dtype not in {cutlass.Float32, cutlass.Float16}: + raise ValueError(f"Skip unsupported acc_dtype {acc_dtype}") + # Skip invalid ab_dtype and acc_dtype combination + if ab_dtype == cutlass.BFloat16 and acc_dtype == cutlass.Float16: + raise ValueError("Skip invalid ab_dtype and acc_dtype combination") + # Skip invalid mma tile shape + if not ( + (not use_2cta_instrs and mma_tiler_mn[0] in [64, 128]) + or (use_2cta_instrs and mma_tiler_mn[0] in [128, 256]) + ): + raise ValueError(f"Skip invalid mma tiler M {mma_tiler_mn[0]}") + if mma_tiler_mn[1] not in range(32, 257, 32): + raise ValueError(f"Skip invalid mma tiler N {mma_tiler_mn[1]}") + # Skip illegal cluster shape + if cluster_shape_mn[0] % (2 if use_2cta_instrs else 1) != 0: + raise ValueError( + f"cluster_shape_m need align with use_2cta_instrs config {cluster_shape_mn}" + ) + # Skip invalid cluster shape + is_power_of_2 = lambda x: x > 0 and (x & (x - 1)) == 0 + if ( + cluster_shape_mn[0] * cluster_shape_mn[1] > 16 + or cluster_shape_mn[0] <= 0 + or cluster_shape_mn[1] <= 0 + or not is_power_of_2(cluster_shape_mn[0]) + or not is_power_of_2(cluster_shape_mn[1]) + ): + raise ValueError(f"Skip invalid cluster shape {cluster_shape_mn}") + + # Skip illegal problem shape for load/store alignment + def check_contigous_16B_alignment(dtype, is_mode0_major, tensor_shape): + major_mode_idx = 0 if is_mode0_major else 1 + num_major_elements = tensor_shape[major_mode_idx] + num_contiguous_elements = 16 * 8 // dtype.width + return num_major_elements % num_contiguous_elements == 0 + + if ( + not check_contigous_16B_alignment(ab_dtype, a_major == "m", (m, k, l)) + or not check_contigous_16B_alignment(ab_dtype, b_major == "n", (n, k, l)) + or not check_contigous_16B_alignment(c_dtype, c_major == "m", (m, n, l)) + ): + raise ValueError("Skip invalid problem alignment") + if not torch.cuda.is_available(): + raise RuntimeError("GPU is required to run this example!") + + # Create tensors for all groups using the new function + ( + ptrs_abc, + torch_tensors_abc, + cute_tensors_abc, + strides_abc, + torch_fp32_tensors_abc, + ) = create_tensors_for_all_groups( + problem_sizes_mnkl, + ab_dtype, + c_dtype, + a_major, + b_major, + c_major, + ) + + # Setup inital tensors for TMA of A,B and C + alignment = 16 # 16 bytes aligned + min_ab_size = alignment * 8 // ab_dtype.width + min_c_size = alignment * 8 // c_dtype.width + initial_cute_tensors_abc = [ + create_tensor_and_stride(1, min_ab_size, min_ab_size, a_major == "m", ab_dtype)[ + 2 + ], + create_tensor_and_stride(1, min_ab_size, min_ab_size, b_major == "n", ab_dtype)[ + 2 + ], + create_tensor_and_stride(1, min_c_size, min_c_size, c_major == "m", c_dtype)[2], + ] + + hardware_info = utils.HardwareInfo() + sm_count = hardware_info.get_max_active_clusters(1) + max_active_clusters = hardware_info.get_max_active_clusters( + cluster_shape_mn[0] * cluster_shape_mn[1] + ) + + # Prepare tensormap buffer for each SM + num_tensormap_buffers = sm_count + tensormap_shape = ( + num_tensormap_buffers, + GroupedGemmKernel.num_tensormaps, + GroupedGemmKernel.bytes_per_tensormap // 8, + ) + tensor_of_tensormap, tensor_of_tensormap_torch = cutlass_torch.cute_tensor_like( + torch.empty(tensormap_shape, dtype=torch.int64), + cutlass.Int64, + is_dynamic_layout=False, + ) + + grouped_gemm = GroupedGemmKernel( + acc_dtype, + use_2cta_instrs, + mma_tiler_mn, + cluster_shape_mn, + tensormap_update_mode, + ) + + # layout (num_groups, 4):(4, 1) + ( + tensor_of_dim_size_mnkl, + tensor_of_dim_size_mnkl_torch, + ) = cutlass_torch.cute_tensor_like( + torch.tensor(problem_sizes_mnkl, dtype=torch.int32), + cutlass.Int32, + is_dynamic_layout=False, + assumed_align=16, + ) + + # layout (num_groups, 3, 2):(6, 2, 1) + tensor_of_strides_abc, tensor_of_strides_abc_torch = cutlass_torch.cute_tensor_like( + torch.tensor(strides_abc, dtype=torch.int32), + cutlass.Int32, + is_dynamic_layout=False, + assumed_align=16, + ) + + # layout (num_groups,3):(3, 1) + tensor_of_ptrs_abc, tensor_of_ptrs_abc_torch = cutlass_torch.cute_tensor_like( + torch.tensor(ptrs_abc, dtype=torch.int64), + cutlass.Int64, + is_dynamic_layout=False, + assumed_align=16, + ) + + # Compute total number of cluster tiles we need to compute for given grouped GEMM problem + def compute_total_num_clusters( + problem_sizes_mnkl: List[tuple[int, int, int, int]], + cluster_tile_shape_mn: tuple[int, int], + ) -> int: + total_num_clusters = 0 + for m, n, _, _ in problem_sizes_mnkl: + num_clusters_mn = tuple( + (x + y - 1) // y for x, y in zip((m, n), cluster_tile_shape_mn) + ) + total_num_clusters += functools.reduce(lambda x, y: x * y, num_clusters_mn) + return total_num_clusters + + # Compute cluster tile shape + def compute_cluster_tile_shape( + mma_tiler_mn: tuple[int, int], + cluster_shape_mn: tuple[int, int], + use_2cta_instrs: bool, + ) -> tuple[int, int]: + cta_tile_shape_mn = list(mma_tiler_mn) + if use_2cta_instrs: + cta_tile_shape_mn[0] = cta_tile_shape_mn[0] // 2 + return tuple(x * y for x, y in zip(cta_tile_shape_mn, cluster_shape_mn)) + + cluster_tile_shape_mn = compute_cluster_tile_shape( + mma_tiler_mn, cluster_shape_mn, use_2cta_instrs + ) + + # If the host problem shape is available, we will launch the grid with only + # the necessary clusters. The function compute_total_num_clusters() does that. + # If the problem shape only exists on device, we will need to launch all active + # clusters possible on a device. + if host_problem_shape_available: + print("Problem shapes available on host and device") + total_num_clusters = compute_total_num_clusters( + problem_sizes_mnkl, cluster_tile_shape_mn + ) + else: + print("Problem shapes available only on device") + total_num_clusters = max_active_clusters + + # Initialize Stream + current_stream = cutlass_torch.default_stream() + + # try to check CUDA version to decide the opt level + try: + from cutlass import CUDA_VERSION + + opt_level = ( + 3 + if CUDA_VERSION.major < 13 + or (CUDA_VERSION.major == 13 and CUDA_VERSION.minor < 1) + else 2 + ) + except ImportError: + opt_level = 3 + # Compile grouped GEMM kernel + compiled_grouped_gemm = cute.compile( + grouped_gemm, + initial_cute_tensors_abc[0], + initial_cute_tensors_abc[1], + initial_cute_tensors_abc[2], + num_groups, + tensor_of_dim_size_mnkl, + tensor_of_strides_abc, + tensor_of_ptrs_abc, + total_num_clusters, + tensor_of_tensormap, + max_active_clusters, + current_stream, + options=f"--opt-level {opt_level}", + ) + + if not skip_ref_check: + compiled_grouped_gemm( + initial_cute_tensors_abc[0], + initial_cute_tensors_abc[1], + initial_cute_tensors_abc[2], + tensor_of_dim_size_mnkl, + tensor_of_strides_abc, + tensor_of_ptrs_abc, + tensor_of_tensormap, + current_stream, + ) + + # Compute reference result + for i, (a, b, c) in enumerate(torch_tensors_abc): + ref = torch.einsum( + "mkl,nkl->mnl", + a.cpu().to(dtype=torch.float32), + b.cpu().to(dtype=torch.float32), + ) + print(f"checking group {i}") + torch.testing.assert_close( + c.cpu(), + ref.to(cutlass_torch.dtype(c_dtype)), + atol=tolerance, + rtol=1e-05, + ) + + if iterations <= 0: + return 0 + + def generate_tensors(): + # Reuse existing CPU tensors and create new GPU tensors from them + ( + ptrs_abc_workspace, + torch_tensors_abc_workspace, + cute_tensors_abc_workspace, + strides_abc_workspace, + _, + ) = create_tensors_for_all_groups( + problem_sizes_mnkl, + ab_dtype, + c_dtype, + a_major, + b_major, + c_major, + torch_fp32_tensors_abc, + ) + + initial_cute_tensors_abc_workspace = [ + create_tensor_and_stride( + 1, min_ab_size, min_ab_size, a_major == "m", ab_dtype + )[2], + create_tensor_and_stride( + 1, min_ab_size, min_ab_size, b_major == "n", ab_dtype + )[2], + create_tensor_and_stride( + 1, min_c_size, min_c_size, c_major == "m", c_dtype + )[2], + ] + + # Create new tensors for this workspace + tensor_of_strides_abc_workspace, _ = cutlass_torch.cute_tensor_like( + torch.tensor(strides_abc_workspace, dtype=torch.int32), + cutlass.Int32, + is_dynamic_layout=False, + assumed_align=16, + ) + + tensor_of_ptrs_abc_workspace, _ = cutlass_torch.cute_tensor_like( + torch.tensor(ptrs_abc_workspace, dtype=torch.int64), + cutlass.Int64, + is_dynamic_layout=False, + assumed_align=16, + ) + + tensormap_workspace, _ = cutlass_torch.cute_tensor_like( + torch.empty(tensormap_shape, dtype=torch.int64), + cutlass.Int64, + is_dynamic_layout=False, + ) + + args = testing.JitArguments( + initial_cute_tensors_abc_workspace[0], + initial_cute_tensors_abc_workspace[1], + initial_cute_tensors_abc_workspace[2], + tensor_of_dim_size_mnkl, + tensor_of_strides_abc_workspace, + tensor_of_ptrs_abc_workspace, + tensormap_workspace, + current_stream, + ) + args.add_to_scope([torch_tensors_abc_workspace]) + return args + + workspace_count = 1 + if use_cold_l2: + one_workspace_bytes = ( + sum( + [ + sum( + [ + torch_tensor.numel() * torch_tensor.element_size() + for torch_tensor in group_tensors + ] + ) + for group_tensors in torch_tensors_abc + ] + ) + + + # Add size of strides tensor + tensor_of_strides_abc_torch.numel() + * tensor_of_strides_abc_torch.element_size() + + + # Add size of ptrs tensor + tensor_of_ptrs_abc_torch.numel() * tensor_of_ptrs_abc_torch.element_size() + + + # Add size of tensormap tensor + tensor_of_tensormap_torch.numel() * tensor_of_tensormap_torch.element_size() + ) + workspace_count = testing.get_workspace_count( + one_workspace_bytes, warmup_iterations, iterations + ) + + exec_time = testing.benchmark( + compiled_grouped_gemm, + workspace_generator=generate_tensors, + workspace_count=workspace_count, + stream=current_stream, + warmup_iterations=warmup_iterations, + iterations=iterations, + ) + + runtime_s = exec_time / 1.0e6 + fmas = 0 + for group in range(num_groups): + [M, N, K, _] = problem_sizes_mnkl[group] + fmas += M * N * K + flop = 2 * fmas + gflop = flop / 1.0e9 + gflops = gflop / runtime_s + + print("Average Runtime : ", exec_time / 1000, "ms") + print("GFLOPS : ", gflops) + + return exec_time # Return execution time in microseconds + + +if __name__ == "__main__": + + def parse_comma_separated_ints(s: str) -> tuple[int, ...]: + try: + return tuple(int(x.strip()) for x in s.split(",")) + except ValueError: + raise argparse.ArgumentTypeError( + "Invalid format. Expected comma-separated integers." + ) + + def parse_comma_separated_tuples(s: str) -> List[tuple[int, ...]]: + if s.strip().startswith("("): + # Split on ),( to separate tuples + tuples = s.strip("()").split("),(") + result = [] + tuple_len = None + + for t in tuples: + # Parse individual tuple + nums = [int(x.strip()) for x in t.split(",")] + + # Validate tuple length consistency + if tuple_len is None: + tuple_len = len(nums) + elif len(nums) != tuple_len: + raise argparse.ArgumentTypeError( + "All tuples must have the same length" + ) + + result.append(tuple(nums)) + return result + + raise argparse.ArgumentTypeError( + "Invalid format. Expected comma-separated integers or list of tuples" + ) + + parser = argparse.ArgumentParser( + description="Example of Grouped GEMM on Blackwell." + ) + parser.add_argument( + "--num_groups", + type=int, + default=3, + help="Number of groups", + ) + parser.add_argument( + "--problem_sizes_mnkl", + type=parse_comma_separated_tuples, + default=((128, 128, 128, 1), (512, 128, 128, 1), (128, 256, 128, 1)), + help="a tuple of problem sizes for each group (comma-separated tuples)", + ) + parser.add_argument( + "--host_problem_shape_available", + action="store_true", + help="Enable the compute of grid based upon host problem shape", + ) + parser.add_argument( + "--mma_tiler_mn", + type=parse_comma_separated_ints, + default=(128, 128), + help="Mma tile shape (comma-separated)", + ) + parser.add_argument( + "--cluster_shape_mn", + type=parse_comma_separated_ints, + default=(1, 1), + help="Cluster shape (comma-separated)", + ) + parser.add_argument( + "--tensormap_update_mode", + type=str, + default="SMEM", + help="Tensor map update mode", + ) + parser.add_argument("--ab_dtype", type=cutlass.dtype, default=cutlass.Float16) + parser.add_argument("--c_dtype", type=cutlass.dtype, default=cutlass.Float16) + parser.add_argument("--acc_dtype", type=cutlass.dtype, default=cutlass.Float32) + parser.add_argument( + "--use_2cta_instrs", + action="store_true", + help="Enable 2CTA MMA instructions feature", + ) + parser.add_argument("--a_major", choices=["k", "m"], type=str, default="k") + parser.add_argument("--b_major", choices=["k", "n"], type=str, default="k") + parser.add_argument("--c_major", choices=["n", "m"], type=str, default="n") + parser.add_argument( + "--tolerance", type=float, default=1e-01, help="Tolerance for validation" + ) + parser.add_argument( + "--warmup_iterations", type=int, default=0, help="Warmup iterations" + ) + parser.add_argument( + "--iterations", + type=int, + default=1, + help="Number of iterations to run the kernel", + ) + parser.add_argument( + "--skip_ref_check", action="store_true", help="Skip reference checking" + ) + parser.add_argument( + "--use_cold_l2", + action="store_true", + default=False, + help="Use circular buffer tensor sets to ensure L2 cold cache", + ) + + args = parser.parse_args() + + if ( + len(args.problem_sizes_mnkl) != 0 + and len(args.problem_sizes_mnkl) != args.num_groups + ): + parser.error("--problem_sizes_mnkl must contain exactly num_groups tuples") + + # l mode must be 1 for all groups + for _, _, _, l in args.problem_sizes_mnkl: + if l != 1: + parser.error("l must be 1 for all groups") + + if len(args.mma_tiler_mn) != 2: + parser.error("--mma_tiler_mn must contain exactly 2 values") + + if len(args.cluster_shape_mn) != 2: + parser.error("--cluster_shape_mn must contain exactly 2 values") + + if args.tensormap_update_mode not in ["GMEM", "SMEM"]: + parser.error("--tensormap_update_mode must be GMEM or SMEM") + + if args.tensormap_update_mode == "GMEM": + tensormap_update_mode = utils.TensorMapUpdateMode.GMEM + else: + tensormap_update_mode = utils.TensorMapUpdateMode.SMEM + + torch.manual_seed(2025) + + run( + args.num_groups, + args.problem_sizes_mnkl, + args.host_problem_shape_available, + args.ab_dtype, + args.c_dtype, + args.acc_dtype, + args.a_major, + args.b_major, + args.c_major, + args.mma_tiler_mn, + args.cluster_shape_mn, + args.use_2cta_instrs, + tensormap_update_mode, + args.tolerance, + args.warmup_iterations, + args.iterations, + args.skip_ref_check, + args.use_cold_l2, + ) + print("PASS") diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/vendored_templates/cutedsl/wrappers/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/vendored_templates/cutedsl/wrappers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..583f1f04b7c97c115438dddd57e084d87dacdf7b --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/vendored_templates/cutedsl/wrappers/__init__.py @@ -0,0 +1,2 @@ +# CuTeDSL Cutlass API registrations for PyTorch Inductor. +from . import dense_blockscaled_gemm_kernel # noqa: F401 diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/vendored_templates/cutedsl/wrappers/dense_blockscaled_gemm_kernel.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/vendored_templates/cutedsl/wrappers/dense_blockscaled_gemm_kernel.py new file mode 100644 index 0000000000000000000000000000000000000000..91a7da1c8497e233f02141cff7af6b2f5b3621ae --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel/vendored_templates/cutedsl/wrappers/dense_blockscaled_gemm_kernel.py @@ -0,0 +1,449 @@ +"""Kernel wrapper for dense_blockscaled_gemm_persistent vendored template.""" + +from __future__ import annotations + +import itertools +import logging +from collections.abc import Callable, Generator # noqa: TC003 + +import cutlass_api +from cutlass_api.arguments import GemmArguments # noqa: TC002 +from cutlass_api.artifact import CompiledArtifact +from cutlass_api.library import ScaleMode, ScaleSwizzleMode +from cutlass_api.metadata import ( + DenseTensorAttributes, + GemmOperandsMetadata, + KernelMetadata, + ScaledTensorAttributes, + Sm100DesignMetadata, +) +from cutlass_api.providers.cutedsl.kernel import CuteDslKernel +from cutlass_api.providers.cutedsl.utils import get_max_active_clusters +from cutlass_api.status import Status +from cutlass_api.utils import ( + ceil_div, + round_up, + strides_to_layout_string, + to_cuda_stream, + tuple_to_string, +) + + +log = logging.getLogger(__name__) + + +try: + from ..dense_blockscaled_gemm_persistent import ( # pyrefly: ignore[missing-import] + Sm100BlockScaledPersistentDenseGemmKernel as BlockScaledGemmKernelImpl, + ) +except ImportError: + BlockScaledGemmKernelImpl = None # type: ignore[misc, assignment] + + +class VendoredDenseBlockScaledGemmKernel(CuteDslKernel): + """Wrapper for vendored dense blockscaled GEMM template for SM100 GPUs.""" + + def __init__(self, metadata: KernelMetadata): + super().__init__(metadata) + + self.sf_vec_size = metadata.operands.A.mode[-1] + mma_tiler_mn = (metadata.design.tile_shape[0], metadata.design.tile_shape[1]) + cluster_shape_mn = ( + metadata.design.cluster_shape[0], + metadata.design.cluster_shape[1], + ) + + self.impl = BlockScaledGemmKernelImpl( # pyrefly: ignore[not-callable] + self.sf_vec_size, + mma_tiler_mn, + cluster_shape_mn, + ) + self.cluster_shape_mn = cluster_shape_mn + + @staticmethod + def _major_modes(args): + """Extract major modes from arguments or operand metadata.""" + import cutlass.utils as utils + from cutlass.cute.nvgpu.tcgen05 import OperandMajorMode + + if args.A.stride[-2:].index(1) == 1: + a_major_mode = (OperandMajorMode.K, "k") + else: + a_major_mode = (OperandMajorMode.MN, "m") + + if args.B.stride[-2:].index(1) == 0: + b_major_mode = (OperandMajorMode.K, "k") + else: + b_major_mode = (OperandMajorMode.MN, "n") + + if args.out.stride[-2:].index(1) == 1: + out_layout = (utils.LayoutEnum.ROW_MAJOR, "n") + else: + out_layout = (utils.LayoutEnum.COL_MAJOR, "m") + + return a_major_mode, b_major_mode, out_layout + + def compile(self, args: GemmArguments, cc: int | None = None) -> CompiledArtifact: + import cutlass.cute as cute + + stream = cute.runtime.make_fake_stream() + max_active_clusters = get_max_active_clusters(self.cluster_shape_mn) + + # pyrefly: ignore[missing-attribute] + compiled_kernel = self.cute_compile( + self.impl, + args.A.tensor, + args.B.tensor, + args.A.scale.tensor, + args.B.scale.tensor, + args.out.tensor, + max_active_clusters, + stream, + ) + + return CompiledArtifact(compiled_kernel, self) + + def _run( + self, + args: GemmArguments, + compiled_artifact: CompiledArtifact, + stream, + workspace=None, + ) -> None: + import torch + + stream = to_cuda_stream(stream) + compiled_gemm = compiled_artifact.compiled_obj + + # TVM FFI needs a torch.cuda.Stream, not a raw int handle + if isinstance(stream, int): + stream = torch.cuda.ExternalStream(stream) + + self.cute_run( # pyrefly: ignore[missing-attribute] + compiled_gemm, + args.A.tensor, + args.B.tensor, + args.A.scale.tensor, + args.B.scale.tensor, + args.out.tensor, + stream, + ) + + def get_workspace_size(self, args: GemmArguments) -> int: + return 0 + + def _supports(self, args: GemmArguments) -> Status: + import cutlass + + m, n = args.out.shape[-2:] + k = args.A.shape[-1] + if args.A.element_type == cutlass.Float4E2M1FN: + k = k * 2 + l = args.A.shape[0] if len(args.A.shape) == 3 else 1 + + expected_sf_k = round_up(ceil_div(k, self.sf_vec_size), 4) + expected_sfa_elements = l * m * expected_sf_k + expected_sfb_elements = l * n * expected_sf_k + + if args.A.scale.numel() != expected_sfa_elements: + return Status.fail( + f"Scale factor A for tensor A of shape {args.A.shape} must have " + f"{expected_sfa_elements} elements, got {args.A.scale.numel()}." + ) + if args.B.scale.numel() != expected_sfb_elements: + return Status.fail( + f"Scale factor B for tensor B of shape {args.B.shape} must have " + f"{expected_sfb_elements} elements, got {args.B.scale.numel()}." + ) + + return Status.success() + + @staticmethod + def _is_valid_dtype_combo(ab_dtype, sf_dtype, sf_vec_size, out_dtype) -> bool: + """Validate dtype/scale-factor/vec-size combinations. + + Matches constraints from the upstream kernel: + MXF8: Float8E5M2/Float8E4M3FN + Float8E8M0FNU, sf_vec_size=32 + MXF4: Float4E2M1FN + Float8E8M0FNU, sf_vec_size=32 + NVF4: Float4E2M1FN + Float8E8M0FNU/Float8E4M3FN, sf_vec_size=16 + """ + import cutlass + + if ab_dtype not in { + cutlass.Float4E2M1FN, + cutlass.Float8E5M2, + cutlass.Float8E4M3FN, + }: + return False + if sf_vec_size not in {16, 32}: + return False + if sf_dtype not in {cutlass.Float8E8M0FNU, cutlass.Float8E4M3FN}: + return False + # Float8E4M3FN as SF only valid with sf_vec_size=16 (NVF4) + if sf_dtype == cutlass.Float8E4M3FN and sf_vec_size == 32: + return False + # Float8 AB types require sf_vec_size=32 (MXF8) + if ab_dtype in {cutlass.Float8E5M2, cutlass.Float8E4M3FN} and sf_vec_size == 16: + return False + if out_dtype not in { + cutlass.Float32, + cutlass.Float16, + cutlass.BFloat16, + cutlass.Float8E5M2, + cutlass.Float8E4M3FN, + }: + return False + return True + + @staticmethod + def _is_valid_layouts(ab_dtype, a_major, b_major) -> bool: + """Float4E2M1FN (MXF4/NVF4) requires row-major (K-major) A and B.""" + import cutlass + + if ab_dtype is cutlass.Float4E2M1FN and not (a_major == "k" and b_major == "k"): + return False + return True + + @staticmethod + def _valid_operands(operands: GemmOperandsMetadata, sf_vec_size: int) -> bool: + import cutlass + + if operands.A.dtype != operands.B.dtype: + return False + if operands.A.scale.dtype != operands.B.scale.dtype: + return False + if operands.accumulator_type != cutlass.Float32: + return False + + ab_dtype = operands.A.dtype + sf_dtype = operands.A.scale.dtype + out_dtype = operands.out.dtype + + if not VendoredDenseBlockScaledGemmKernel._is_valid_dtype_combo( + ab_dtype, sf_dtype, sf_vec_size, out_dtype + ): + return False + + (_, a_major), (_, b_major), _ = VendoredDenseBlockScaledGemmKernel._major_modes( + operands + ) + if not VendoredDenseBlockScaledGemmKernel._is_valid_layouts( + ab_dtype, a_major, b_major + ): + return False + + return True + + @staticmethod + def _metadata_operand_combinations() -> Generator[GemmOperandsMetadata, None, None]: + import cutlass + + ab_dtypes = [cutlass.Float8E5M2, cutlass.Float8E4M3FN, cutlass.Float4E2M1FN] + out_dtypes = [ + cutlass.Float32, + cutlass.Float16, + cutlass.BFloat16, + cutlass.Float8E4M3FN, + cutlass.Float8E5M2, + ] + sf_dtypes = [cutlass.Float8E8M0FNU, cutlass.Float8E4M3FN] + scale_modes = [ScaleMode.Blockwise1x16, ScaleMode.Blockwise1x32] + + row_major = (0, 0, 1) + col_major = (0, 1, 0) + alignment_bytes = 16 + + def major_str_a(stride): + return "k" if stride == row_major else "m" + + def major_str_b(stride): + return "n" if stride == row_major else "k" + + for ab_dtype, sf_dtype, scale_mode, out_dtype in itertools.product( + ab_dtypes, sf_dtypes, scale_modes, out_dtypes + ): + sf_vec_size = scale_mode[-1] + if not VendoredDenseBlockScaledGemmKernel._is_valid_dtype_combo( + ab_dtype, sf_dtype, sf_vec_size, out_dtype + ): + continue + + for stride_A, stride_B, stride_out in itertools.product( + [row_major, col_major], repeat=3 + ): + a_major = major_str_a(stride_A) + b_major = major_str_b(stride_B) + + if not VendoredDenseBlockScaledGemmKernel._is_valid_layouts( + ab_dtype, a_major, b_major + ): + continue + + ab_div = alignment_bytes * 8 // ab_dtype.width + out_div = alignment_bytes * 8 // out_dtype.width + sf_div = alignment_bytes * 8 // sf_dtype.width + + yield GemmOperandsMetadata( + A=ScaledTensorAttributes( + base=DenseTensorAttributes( + dtype=ab_dtype, + stride=stride_A, + divisibility=ab_div, + ), + scale=DenseTensorAttributes( + dtype=sf_dtype, + stride=None, + divisibility=sf_div, + ), + mode=scale_mode, + swizzle=ScaleSwizzleMode.Swizzle32x4x4, + ), + B=ScaledTensorAttributes( + base=DenseTensorAttributes( + dtype=ab_dtype, + stride=stride_B, + divisibility=ab_div, + ), + scale=DenseTensorAttributes( + dtype=sf_dtype, + stride=None, + divisibility=sf_div, + ), + mode=scale_mode, + swizzle=ScaleSwizzleMode.Swizzle32x4x4, + ), + out=DenseTensorAttributes( + dtype=out_dtype, + stride=stride_out, + divisibility=out_div, + ), + accumulator_type=cutlass.Float32, + ) + + @staticmethod + def _valid_metadata(metadata: KernelMetadata) -> bool: + scale_vec = metadata.operands.A.mode + + if len(scale_vec) > 1: + for i in range(len(scale_vec) - 1): + if scale_vec[i] != 1: + return False + + sf_vec_size = scale_vec[-1] + if not VendoredDenseBlockScaledGemmKernel._valid_operands( + metadata.operands, sf_vec_size + ): + return False + + design = metadata.design + if not isinstance(design, Sm100DesignMetadata): + return False + + cm, cn, _ = design.cluster_shape + if cm <= 0 or cn <= 0: + return False + if cm * cn > 16: + return False + if cm & (cm - 1) != 0 or cn & (cn - 1) != 0: + return False + # SF multicast constraint: cluster dims <=4 + if cm > 4 or cn > 4: + return False + + tile_m, tile_n, _ = design.tile_shape + if tile_m not in [128, 256]: + return False + if tile_n not in [64, 128, 192, 256]: + return False + use_2cta = tile_m == 256 + if use_2cta and cm % 2 != 0: + return False + + if metadata.epilogue is not None: + return False + + return True + + @staticmethod + def generate_kernels( + metadata_filter: Callable[[KernelMetadata], bool], + epilogue_args=None, + cc: int | None = None, + ) -> list[VendoredDenseBlockScaledGemmKernel]: + if cc is not None and cc not in [100, 101, 103]: + return [] + if epilogue_args is not None: + return [] + + design_params = { + "use_2cta_mma": [True], + "tile_shape": [ + (M, N, 256) for M in [128, 256] for N in [64, 128, 192, 256] + ], + "cluster_shape": [(M, N, 1) for M in [1, 2, 4] for N in [1, 2, 4]], + "use_tma_store": [True], + } + + param_names = list(design_params.keys()) + param_values = [design_params[name] for name in param_names] + + kernel_list = [] + + for ( + operands + ) in VendoredDenseBlockScaledGemmKernel._metadata_operand_combinations(): + # pyrefly: ignore[no-matching-overload] + for values in itertools.product(*param_values): + design = Sm100DesignMetadata(**dict(zip(param_names, values))) + + kernel_name = ( + "inductor_vendored.DenseBlockScaledGemmKernel_sm100_" + "{layout}_A{A}_B{B}_out{out}_SFA{SFA}_SFB{SFB}_" + "acc{acc}_scale{scale_mode}_swizzle{scale_swizzle}_" + "{num_cta}cta_cluster{cluster}_tile{tile}" + "{_tma_store}" + ).format( + layout=strides_to_layout_string( + operands.A.stride, + operands.B.stride, + operands.out.stride, + ), + A=operands.A.dtype, + B=operands.B.dtype, + out=operands.out.dtype, + SFA=operands.A.scale.dtype, + SFB=operands.B.scale.dtype, + acc=operands.accumulator_type, + scale_mode=operands.A.mode, + scale_swizzle=operands.A.swizzle, + num_cta="2" if design.use_2cta_mma else "1", + cluster=tuple_to_string(design.cluster_shape), + tile=tuple_to_string(design.tile_shape), + _tma_store="_tma_store" if design.use_tma_store else "", + ) + + metadata = KernelMetadata( + operands=operands, + design=design, + kernel_name=kernel_name, + kernel_class=VendoredDenseBlockScaledGemmKernel, + min_cc=100, + epilogue=None, + ) + + if VendoredDenseBlockScaledGemmKernel._valid_metadata(metadata): + if metadata_filter is None or metadata_filter(metadata): + kernel_list.append(VendoredDenseBlockScaledGemmKernel(metadata)) + + log.debug( + "Generated %d DenseBlockScaledGemmKernel configurations", + len(kernel_list), + ) + return kernel_list + + +# Only register if kernel implementation is available +if BlockScaledGemmKernelImpl is not None: + cutlass_api.providers.cutedsl.CuTeDSLProvider.register( + VendoredDenseBlockScaledGemmKernel + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel_inputs.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel_inputs.py new file mode 100644 index 0000000000000000000000000000000000000000..596d6d688d1a8164a510e97da355388a6a21c1cc --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel_inputs.py @@ -0,0 +1,347 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any, TYPE_CHECKING + +import torch +import torch._inductor.config +from torch._inductor import ir +from torch._inductor.virtualized import V + +from .ir import FixedLayout, FlexibleLayout, Layout + + +if TYPE_CHECKING: + from collections.abc import Sequence + + import sympy + + +class KernelInputs(ABC): + """ + Class to store and provide access to input nodes for kernels. + This class takes in a tuple of input nodes and provides methods to access + information about these nodes, such as their device type and device. + """ + + def __init__( + self, + input_nodes: list[Any], + scalars: dict[str, float | int] | None = None, + out_dtype: torch.dtype | None = None, + ): + """ + Initialize with a tuple of input nodes. + + Args: + input_nodes: A tuple of input nodes to store + out_dtype: Optional output dtype to store + """ + self._input_nodes = input_nodes + self._device_name: str | None = None + self._scalars = scalars if scalars is not None else {} + self._out_dtype = out_dtype + assert len(input_nodes) > 0, "Expected at least one input node" + + def nodes(self, reorder: Sequence[int] | None = None) -> list[Any]: + """ + Return the stored input nodes, optionally reordered. + + Args: + reorder: Optional sequence of indices to reorder the nodes. + For example, (2, 0, 1) would return nodes in that order. + + Returns: + The tuple of input nodes, optionally reordered + """ + if reorder is None: + return self._input_nodes + assert len(self._input_nodes) == len(reorder), ( + f"Reorder length mismatch: {len(self._input_nodes)} vs {len(reorder)}" + ) + return [self._input_nodes[i] for i in reorder] + + @property + def count(self) -> int: + """ + Get the number of input nodes. + + Returns: + The number of input nodes + """ + return len(self._input_nodes) + + @property + def device_type(self) -> str | None: + """ + Get the device type of the first node. + + Returns: + The device type (e.g., 'cuda', 'cpu') + """ + + return ir.get_device_type(self._input_nodes[0]) + + def device(self) -> torch.device: + """ + Get the device of the first node. + + Returns: + The device of the first node + """ + return self._input_nodes[0].get_device() + + def device_name(self) -> str | None: + """ + Get the device name information. + + Returns: + A tuple of (gpu_name, vendor, model) + """ + if self._device_name is None: + device = self.device() + if self.device_type == "cuda": + device_properties = torch.cuda.get_device_properties(device) + self._device_name = device_properties.gcnArchName + return self._device_name + + def shapes_symbolic(self) -> tuple[tuple[Any, ...], ...]: + """ + Get the symbolic shapes of all input nodes. + + Returns: + A tuple of shape tuples for each input node + """ + return tuple(node.get_size() for node in self._input_nodes) + + def shapes_hinted(self) -> tuple[tuple[int, ...], ...]: + """ + Get the size hints for shapes of all input nodes. + + Returns: + A tuple of shape tuples with integer hints for each input node + """ + return tuple( + V.graph.sizevars.optimization_hints(node.get_size()) + for node in self._input_nodes + ) + + def strides_symbolic(self) -> tuple[tuple[sympy.Integer, ...], ...]: + """ + Get the symbolic strides of all input nodes. + + Returns: + A tuple of stride tuples for each input node + """ + return tuple(node.get_stride() for node in self._input_nodes) + + def strides_hinted(self) -> tuple[tuple[int, ...], ...]: + """ + Get the size hints for strides of all input nodes. + + Returns: + A tuple of stride tuples with integer hints for each input node + """ + return tuple( + V.graph.sizevars.optimization_hints(node.get_stride()) + for node in self._input_nodes + ) + + def dtypes(self) -> tuple[torch.dtype, ...]: + """ + Get the dtypes of all input nodes. + + Returns: + A tuple of dtypes for each input node + """ + return tuple(node.get_dtype() for node in self._input_nodes) + + def dtype(self, idx: int = 0) -> torch.dtype: + """ + Get the dtype of a specific input node. + + Args: + idx: Index of the node to get the dtype from (default: 0) + + Returns: + The dtype of the specified input node + """ + return self._input_nodes[idx].get_dtype() + + @abstractmethod + def out_dtype(self) -> torch.dtype: + """ + Get the output dtype, whether passed in or inferred from the nodes + + Returns: + The output dtype + """ + + def get_scalar(self, name: str) -> float | int: + """ + Get the scalar value for a given name. + + Args: + name: Name of the scalar to get + + Returns: + The scalar value + """ + assert name in self._scalars, f"Scalar {name} not found, but required" + return self._scalars[name] + + @abstractmethod + def output_layout(self, flexible: bool = True) -> Layout: + """ + Abstract method to handle output layout generation. + + Args: + out_dtype: Optional output dtype. If not provided, infer from inputs + flexible: If True, return FlexibleLayout, otherwise FixedLayout + """ + + +class MMKernelInputs(KernelInputs): + """ + Specialized KernelInputs for matrix multiplication operations. + Provides additional methods to access M, N, K dimensions. + """ + + def __init__( + self, + input_nodes: list[Any], + scalars: dict[str, float | int] | None = None, + out_dtype: torch.dtype | None = None, + mat1_idx: int = -2, + mat2_idx: int = -1, + ): + """ + Initialize with a tuple of input nodes. + + By default, we assume the last 2 input nodes are mat1 and mat2, but + the caller can adjust when necessary + """ + super().__init__(input_nodes, scalars, out_dtype) + # for mm, we need at least 2 nodes, and we need to know which nodes + # are the main matrixes e.g. addmm is (bias, mat1, mat2) whereas others + # might be (mat1, mat2, scale), etc. + assert len(self._input_nodes) >= 2, "Expected at least 2 input nodes" + + # Adjust assertions to handle negative indices + m1_idx, m2_idx = mat1_idx, mat2_idx + if mat1_idx < 0: + m1_idx += len(input_nodes) + if mat2_idx < 0: + m2_idx += len(input_nodes) + + assert 0 <= m1_idx < len(input_nodes), f"Invalid mat1_idx: {mat1_idx}" + assert 0 <= m2_idx < len(input_nodes), f"Invalid mat2_idx: {mat2_idx}" + + self._mat1_idx = mat1_idx + self._mat2_idx = mat2_idx + + def mnk_symbolic( + self, + ) -> tuple[sympy.Integer, sympy.Integer, sympy.Integer]: + """ + Get the symbolic M, N, K dimensions for matrix multiplication. + Handles both 2D (MM) and 3D (BMM) tensors. + + M is extracted from the second-to-last dimension of the first operand (mat1). + N is extracted from the last dimension of the second operand (mat2). + K is extracted from the last dimension of the first operand (mat1). + + Returns: + A tuple of (M, N, K) dimensions + """ + mat1 = self.nodes()[self._mat1_idx] + mat2 = self.nodes()[self._mat2_idx] + + m = mat1.get_size()[-2] # M from second-to-last dimension of mat1 + k = mat1.get_size()[-1] # K from last dimension of mat1 + n = mat2.get_size()[-1] # N from last dimension of mat2 + + # Ensure K dimensions match between operands + k0 = mat2.get_size()[-2] # K from second-to-last dimension of mat2 + V.graph.sizevars.check_equals(k, k0) + return (m, n, k) + + def out_dtype(self) -> torch.dtype: + """ + Get the output dtype, whether passed in or inferred from the nodes + + Returns: + The output dtype + """ + if self._out_dtype is not None: + return self._out_dtype + return self.mat1mat2()[0].get_dtype() + + def output_layout(self, flexible: bool = True) -> Layout: + """ + Handle output layout generation for matrix multiplication. + + Args: + out_dtype: Optional output dtype. If not provided, infer from inputs + flexible: If True, return FlexibleLayout, otherwise FixedLayout + """ + mat1, mat2 = self.mat1mat2() + out_dtype = self.out_dtype() + # NOTE: taken from mm_common.mm_args + *b1, m, k1 = mat1.get_size() + *b2, k2, n = mat2.get_size() + b = [V.graph.sizevars.check_equals_and_simplify(a, b) for a, b in zip(b1, b2)] + size = [*b, m, n] + if flexible: + return FlexibleLayout(self.device(), out_dtype, size) + else: + return FixedLayout(self.device(), out_dtype, size) + + def mat1mat2(self) -> tuple[Any, Any]: + """ + Get the mat1 and mat2 nodes. + + Returns: + A tuple of (mat1, mat2) nodes + """ + nodes = self.nodes() + return nodes[self._mat1_idx], nodes[self._mat2_idx] + + def mnk_hinted(self) -> tuple[int, int, int]: + """ + Get the hinted M, N, K dimensions for matrix multiplication. + Handles both 2D (MM) and 3D (BMM) tensors. + + Uses shapes_hinted from the base class to get integer hints for dimensions. + + Returns: + A tuple of (M, N, K) dimensions as integers + """ + hinted_shapes = self.shapes_hinted() + mat1_shape = hinted_shapes[self._mat1_idx] + mat2_shape = hinted_shapes[self._mat2_idx] + + m = mat1_shape[-2] # M from second-to-last dimension of mat1 + k = mat1_shape[-1] # K from last dimension of mat1 + n = mat2_shape[-1] # N from last dimension of mat2 + + # Ensure K dimensions match between operands + k_check = mat2_shape[-2] # K from second-to-last dimension of mat2 + assert k == k_check, f"K dimensions don't match: {k} vs {k_check}" + + return (m, n, k) + + def batch_hinted(self) -> int: + """ + Get the hinted batch size for batched matrix multiplication. + Returns 1 for non-batched (2D) operations. + + Returns: + The batch size as an integer + """ + hinted_shapes = self.shapes_hinted() + mat1_shape = hinted_shapes[self._mat1_idx] + + if len(mat1_shape) >= 3: + return mat1_shape[-3] # Batch from third-to-last dimension + return 1 # Non-batched operation diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel_template_choice.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel_template_choice.py new file mode 100644 index 0000000000000000000000000000000000000000..cd68cfc62ab4c78343e0594e25a85241247664c9 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/kernel_template_choice.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +from typing import Any, TYPE_CHECKING + +from .template_heuristics.params import DictKernelTemplateParams + + +if TYPE_CHECKING: + from collections.abc import Generator + + from .codegen.common import KernelTemplate + from .ir import ChoiceCaller, Layout + from .kernel_inputs import KernelInputs + from .select_algorithm import ExternKernelChoice + from .template_heuristics.params import KernelTemplateParams + + +class KernelTemplateChoice: + """ + A class that encapsulates all the components needed to create a ChoiceCaller from a template. + + This class implements lazy evaluation for the choice property - the actual ChoiceCaller + is only created when first accessed via the choice property. + """ + + def __init__( + self, + template: KernelTemplate | ExternKernelChoice, + params: KernelTemplateParams, + extra_kwargs: dict[str, Any], + layout: Layout, + inputs: KernelInputs, + ): + self.template = template + self.params = params + self.extra_kwargs = extra_kwargs + self.layout = layout + self.inputs = inputs + self.annotations: dict[str, Any] = {"ktc": self} + + @property + def choice(self) -> ChoiceCaller | None: + """ + Lazily evaluate and return the ChoiceCaller for this template choice. + + On first access, calls template.choice_or_none() with the stored parameters. + If successful, caches and returns the ChoiceCaller. If it fails, caches + and returns None. Subsequent accesses return the cached value. + + Returns: + ChoiceCaller if the template choice succeeds, None otherwise + """ + if not hasattr(self, "_choice"): + # First time accessing choice - try to generate it + kwargs = self.params.to_kwargs() + self._choice = self.template.choice_or_none( + **kwargs, + **self.extra_kwargs, + layout=self.layout, + input_nodes=self.inputs.nodes(), + ) + if self._choice is not None: + self._choice.annotations = self.annotations + return self._choice + + +def make_ktc_generator( + template: KernelTemplate | ExternKernelChoice, + cs: Generator[KernelTemplateParams, None, None], + extra_kwargs: dict[str, Any], + overrides: dict[str, Any], + layout: Layout, + inputs: KernelInputs, +) -> Generator[KernelTemplateChoice, None, None]: + """ + Create a generator of KernelTemplateChoice objects for a given template. + + Args: + template: The template object (KernelTemplate or ExternKernelChoice) + cs: Generator of KernelTemplateParams from template heuristic + overrides: Override kwargs for the template + layout: Layout value for the template + inputs: KernelInputs for the op + + Yields: + KernelTemplateChoice objects + """ + for params in cs: + # Apply overrides to params + base_kwargs = params.to_kwargs() + final_kwargs = {**base_kwargs, **overrides} + final_params = DictKernelTemplateParams(final_kwargs) + yield KernelTemplateChoice( + template=template, + params=final_params, + extra_kwargs=extra_kwargs, + layout=layout, + inputs=inputs, + ) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/lookup_table/__init__.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/lookup_table/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0ebb1d5618bfa5eb44cfa9c4bc3921e887f54374 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/lookup_table/__init__.py @@ -0,0 +1,32 @@ +""" +Template lookup table system for PyTorch Inductor. + +This package provides functionality for: +- Loading pre-configured template choices from lookup tables +- Managing template configurations and choices + +All functionality is contained within the LookupTableChoices class. +You can customize any aspect by subclassing LookupTableChoices and overriding methods. + +Usage: + # Basic usage + choices = LookupTableChoices() + V.set_choices_handler(choices) + + # Custom usage + class MyCustomChoices(LookupTableChoices): + def _get_lookup_table(self): + return my_custom_table + + def make_lookup_key(self, kernel_inputs, op_name, include_device=False): + return f"custom_{op_name}_{hash(str(kernel_inputs))}" + + V.set_choices_handler(MyCustomChoices()) +""" + +from .choices import LookupTableChoices + + +__all__ = [ + "LookupTableChoices", +] diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/lookup_table/choices.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/lookup_table/choices.py new file mode 100644 index 0000000000000000000000000000000000000000..03fdab47e11e7f99578f52e4e972e15c239b5a64 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/lookup_table/choices.py @@ -0,0 +1,421 @@ +from __future__ import annotations + +import copy +import logging +from functools import lru_cache +from typing import Any, TYPE_CHECKING + +import torch +from torch._inductor import config +from torch._inductor.choices import InductorChoices +from torch._inductor.kernel_template_choice import KernelTemplateChoice +from torch._inductor.template_heuristics.params import DictKernelTemplateParams + + +log = logging.getLogger(__name__) + + +if TYPE_CHECKING: + from collections.abc import Generator + + from torch._inductor.codegen.common import KernelTemplate + from torch._inductor.kernel_inputs import KernelInputs + from torch._inductor.select_algorithm import ExternKernelChoice + + +class LookupTableChoices(InductorChoices): + """ + InductorChoices subclass that uses lookup table when available, otherwise falls back to parent. + All lookup functionality is contained within this class and can be customized by overriding methods. + """ + + def _get_lookup_table(self) -> dict[str, list[dict[str, Any]]]: + """ + Get the template lookup table from config. + Override this method to use custom lookup table sources (database, API, etc.). + """ + if not torch.cuda.is_available() or config.lookup_table.table is None: + return {} + return config.lookup_table.table + + @staticmethod + @lru_cache + def _get_device_key(device: torch.device) -> str | None: + """ + Generate a device key for lookup table indexing. + For CPU devices, returns None. + For CUDA devices, returns the props.gcnArchName string. + """ + if device.type != "cuda": + # only cuda devices are supported, this indicates that the system is not in use + # for this device + return None + + # Get CUDA device properties + props = torch.cuda.get_device_properties(device.index) + return props.gcnArchName + + @staticmethod + def _generate_kernel_inputs_key(kernel_inputs: KernelInputs) -> str: + """ + Generate a key based on input node properties and scalars. + The key includes dtype, size, and stride information for each input node, + plus scalar values as key=value pairs separated by & signs. + """ + # Get node information using existing methods + dtypes = kernel_inputs.dtypes() + shapes = kernel_inputs.shapes_hinted() + strides = kernel_inputs.strides_hinted() + + # Create tuple of (dtype, shape_list, stride_list) for each node + node_info = tuple( + (dtype, list(shape), list(stride)) + for dtype, shape, stride in zip(dtypes, shapes, strides) + ) + + # Create base key from node information + fmt_key = str(node_info) + # Add scalar information if present + if kernel_inputs._scalars: + # Sort scalars for consistent key generation and join with & + scalar_parts = [ + f"{key}={value}" + for key, value in sorted(kernel_inputs._scalars.items()) + ] + scalars_key = "&".join(scalar_parts) + fmt_key = f"{fmt_key}+{scalars_key}" + + return f"{fmt_key}" + + def make_lookup_key( + self, kernel_inputs: KernelInputs, op_name: str, include_device: bool = False + ) -> str | None: + """ + Create a flattened lookup key from kernel inputs and operation name. + Override this method to customize key generation. + + Args: + kernel_inputs: KernelInputs object containing input nodes and scalars + op_name: Operation name (e.g., "mm", "addmm") + include_device: Whether to include device key in the generated key + + Returns: + A string key combining device (optional), operation, and input information + """ + device = kernel_inputs.device() + dev_key = self._get_device_key(device) + if dev_key is None: + # The system does not run when dev_key is None, regardless of + # whether include_device is True or False + return None + if not include_device: + dev_key = None + + # Generate input key using our staticmethod + input_key = self._generate_kernel_inputs_key(kernel_inputs) + + # Create the flattened lookup key + if dev_key is not None: + key_parts = [dev_key, input_key, op_name] + else: + key_parts = [input_key, op_name] + + return "+".join(key_parts) + + def make_lookup_key_variants( + self, kernel_inputs: KernelInputs, op_name: str + ) -> tuple[str | None, str | None]: + """ + Generate both device-specific and device-agnostic lookup keys. + Override this method to customize key variant generation. + + Args: + kernel_inputs: KernelInputs object containing input nodes and scalars + op_name: Operation name (e.g., "mm", "addmm") + + Returns: + Tuple of (device_key, device_agnostic_key). Either may be None if generation fails. + """ + device_key = self.make_lookup_key(kernel_inputs, op_name, include_device=True) + device_agnostic_key = self.make_lookup_key( + kernel_inputs, op_name, include_device=False + ) + + return device_key, device_agnostic_key + + @staticmethod + def _entry_is_valid( + cfg: dict[str, Any], + template_id: str, + template_hash_map: dict[str, str | None] | None, + ) -> bool: + """ + Check if a config entry is valid based on template hash validation. + + Args: + cfg: Configuration dictionary that may contain a template_hash field + template_id: The template identifier + template_hash_map: Optional mapping from template_uid to src_hash for validation + + Returns: + True if the config is valid and should be kept, False if it should be filtered out + """ + # If hash checking is disabled or no hash map provided, keep the config + if not config.lookup_table.check_src_hash or not template_hash_map: + return True + + template_hash = template_hash_map.get(template_id) + config_hash = cfg.get("template_hash") + + # Both hashes present - validate they match + if template_hash is not None and config_hash is not None: + if config_hash != template_hash: + log.warning( + "Hash validation failed for template '%s': config_hash='%s' != template_hash='%s'. " + "Template code may have changed. Filtering out config: %s", + template_id, + config_hash, + template_hash, + {k: v for k, v in cfg.items() if k != "template_hash"}, + ) + return False + else: + log.debug( + "Hash validation passed for template '%s': hash='%s'", + template_id, + template_hash, + ) + return True + # Config has no hash - keep it + elif config_hash is None: + log.debug( + "Config for template '%s' has no hash - keeping it (template_hash='%s')", + template_id, + template_hash, + ) + return True + # Template has no hash - keep config + else: + log.debug( + "Template '%s' has no src_hash - keeping config with hash '%s'", + template_id, + config_hash, + ) + return True + + def lookup_template_configs( + self, + kernel_inputs: KernelInputs, + op_name: str, + template_uids: list[str], + template_hash_map: dict[str, str | None] | None = None, + ) -> dict[str, list[dict[str, Any]]]: + """ + Unified function to look up template configurations for multiple templates. + Override this method to customize lookup logic. + + Args: + kernel_inputs: KernelInputs object containing input nodes and scalars + op_name: Operation name (e.g., "mm", "addmm") + template_uids: List of template identifiers (e.g., ["mm", "tma", "decompose_k"]) + template_hash_map: Optional mapping from template_uid to src_hash for validation + + Returns: + {}: No lookup table in use, or no matches found for any template + {"template_uid1": [config1, config2], ...}: Matches found, filtered configurations + """ + lookup_table = self._get_lookup_table() + if not lookup_table: + log.debug("Lookup table: no table configured or CUDA unavailable") + return {} + + # Try both key variants: device-specific first, then device-agnostic + # If both exist, device-specific takes priority + device_key, device_agnostic_key = self.make_lookup_key_variants( + kernel_inputs, op_name + ) + + config_list = [] + + for key_type, key in [ + ("device-specific", device_key), + ("device-agnostic", device_agnostic_key), + ]: + if key is not None: + config_list = lookup_table.get(key, []) + if config_list: + log.debug( + "Lookup table: found %d configs using %s key '%s' for %s", + len(config_list), + key_type, + key, + op_name, + ) + break + else: + log.debug( + "Lookup table: no match for %s (tried keys: %s, %s) (table has %d keys)", + op_name, + device_key, + device_agnostic_key, + len(lookup_table), + ) + return {} + + log.debug( + "Lookup table: found %d configs for %s templates %s", + len(config_list), + op_name, + template_uids, + ) + # Group configs by template_id + configs_by_template: dict[str, list[dict[str, Any]]] = {} + for cfg in config_list: + if not isinstance(cfg, dict): + raise ValueError( + f"Config for {op_name} operation is not a dictionary: {cfg}" + ) + if "template_id" not in cfg: + raise ValueError( + f"Config for {op_name} operation missing required 'template_id' field: {cfg}" + ) + + template_id = cfg["template_id"] + if template_id in template_uids: + if template_id not in configs_by_template: + configs_by_template[template_id] = [] + configs_by_template[template_id].append(cfg) + + # Check template hashes and clean up template_id field + result = {} + for template_id, matching_configs in configs_by_template.items(): + filtered_configs = [] + for cfg in matching_configs: + # Check template hash using helper function + if not self._entry_is_valid(cfg, template_id, template_hash_map): + continue + + # Return a copy of the config, as we don't want to modify the original + cconfig = copy.deepcopy(cfg) + # Lastly, we have to throw out the template_id, as it's not a valid kwarg + # and just used to identify which template the entry belongs to + del cconfig["template_id"] + # Similarly, the template_hash is not a valid kwarg + cconfig.pop("template_hash", None) + filtered_configs.append(cconfig) + + if filtered_configs: + result[template_id] = filtered_configs + + return result + + def _finalize_template_configs( + self, + template_choices: dict[str, Generator[KernelTemplateChoice, None, None]], + kernel_inputs: KernelInputs, + templates: list[KernelTemplate | ExternKernelChoice], + op_name: str, + kwarg_overrides: dict[str, dict[str, Any]] | None = None, + ) -> list[KernelTemplateChoice]: + """Check lookup table for hits, use those if found, otherwise fall back to parent.""" + # 1. Collect template src_hashes for validation + template_uids = [template.uid for template in templates] + template_hash_map = {} + for template in templates: + src_hash = getattr(template, "src_hash", None) + template_hash_map[template.uid] = src_hash + + log.debug( + "Choices: attempting lookup for %s with %d templates", + op_name, + len(template_uids), + ) + + # 2. Single batch lookup for all templates + lookup_results = self.lookup_template_configs( + kernel_inputs, op_name, template_uids, template_hash_map + ) + + # 3. Early exit if no lookup table or no matches + if not lookup_results: # Empty dict + log.info("LookupChoices: lookup miss for %s, using fallback", op_name) + return self._fallback( + template_choices, + kernel_inputs, + templates, + op_name, + kwarg_overrides, + ) + + log.info( + "LookupChoices: lookup hit for %s - found %d/%d templates: %s", + op_name, + len(lookup_results), + len(template_uids), + list(lookup_results.keys()), + ) + + # 4. Create KTCs only for templates with lookup entries + return self._create_lookup_choices( + lookup_results, templates, kernel_inputs, op_name + ) + + def _fallback( + self, + template_choices: dict[str, Generator[KernelTemplateChoice, None, None]], + kernel_inputs: KernelInputs, + templates: list[KernelTemplate | ExternKernelChoice], + op_name: str, + kwarg_overrides: dict[str, dict[str, Any]] | None = None, + ) -> list[KernelTemplateChoice]: + """Fallback to parent if no lookup table or no matches.""" + # NOTE: this is broken out, so that subclasses are able to override this + # to handle explicitly the situations where the lookup take had a miss vs + # overriding the entire logic + return super()._finalize_template_configs( + template_choices, + kernel_inputs, + templates, + op_name, + kwarg_overrides, + ) + + def _create_lookup_choices( + self, + lookup_results: dict[str, list[dict[str, Any]]], + templates: list[KernelTemplate | ExternKernelChoice], + kernel_inputs: KernelInputs, + op_name: str, + ) -> list[KernelTemplateChoice]: + """Create KernelTemplateChoice objects from lookup results using parent's get_ktc method.""" + templates_by_uid = {template.uid: template for template in templates} + lookup_choices: list[KernelTemplateChoice] = [] + + for template_uid, configs in lookup_results.items(): + template = templates_by_uid[template_uid] + + # Use parent's get_ktc method to get a generator, then get the first base KTC + ktc_generator = self.get_ktc(kernel_inputs, template, op_name) + + try: + base_ktc = next(ktc_generator) + except StopIteration: + # No configs from heuristic, skip this template + continue + + # For each lookup config, create a KTC with the override kwargs + # Start from base params (which include derived options like OUT_DTYPE) + # and let the lookup config override them + base_kwargs = base_ktc.params.to_kwargs() + for c in configs: + merged = {**base_kwargs, **c} + lookup_ktc = KernelTemplateChoice( + template=base_ktc.template, + params=DictKernelTemplateParams(merged), + extra_kwargs=base_ktc.extra_kwargs, + layout=base_ktc.layout, + inputs=base_ktc.inputs, + ) + lookup_choices.append(lookup_ktc) + + return lookup_choices diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/loop_body.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/loop_body.py new file mode 100644 index 0000000000000000000000000000000000000000..1ed47c0c2a32dddca360a017881250d43be49996 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/loop_body.py @@ -0,0 +1,795 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import collections +import functools +import itertools +import re +from enum import auto, Enum +from typing import Any, NamedTuple, TYPE_CHECKING, TypeVar + +import sympy + +import torch.fx +from torch._dynamo.utils import identity +from torch.fx.proxy import Scope, TracerBase +from torch.utils._sympy.functions import Mod +from torch.utils._sympy.symbol import SymT + +from . import config, dependencies +from .codegen.common import index_prevent_reordering +from .ops_handler import DefaultHandler, OpsHandler, WrapperHandler +from .utils import ( + cache_on_self, + reduction_num_outputs, + sympy_index_symbol_with_prefix, + sympy_subs, +) +from .virtualized import ops, V + + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + + +T = TypeVar("T") + + +class InterpreterShim(torch.fx.Interpreter): + @staticmethod + @functools.cache + def _dummy_gm(): + return torch.fx.symbolic_trace(identity) + + def __init__(self, graph, submodules): + # call super() with a placeholder to avoid constructing a + # GraphModule which is very expensive (it does codegen). + super().__init__(self._dummy_gm(), garbage_collect_values=False) + self.module = self # type: ignore[assignment] + self.graph = graph + self.submodules = submodules + self.extra_traceback = False + self.fetch_attr = submodules.__getitem__ # type: ignore[method-assign] + self.current_node = None + + def run_node(self, n: torch.fx.Node) -> Any: + # pyrefly: ignore [bad-assignment] + self.current_node = n + return super().run_node(n) + + def run(self, *args, **kwargs): + with V.set_interpreter_handler(self): + return super().run(*args, **kwargs) + + +# We don't need the nn.Module and constant handling in Tracer +class LightTracer(TracerBase): + def __init__(self): + super().__init__() + self.graph = torch.fx.Graph(tracer_cls=self.__class__) # type: ignore[arg-type] + self.scope = Scope("", None) + self.module_stack = {} # type: ignore[assignment] + self.node_name_to_scope = {} + + +class MemoryEntry(NamedTuple): + index_name: str # LoopBody.indexing_exprs[index_name] + buffer_name: str | None + mode: str | None # V.ops.store(..., mode=mode) + + +class MemoryUsageType(Enum): + # These are 1:1 with the opcode generating the usage + LOAD = auto() + LOAD_SEED = auto() + STORE = auto() + STORE_REDUCTION = auto() + INDEX_EXPR = auto() + CHECK_BOUNDS = auto() + BUCKETIZE = auto() + + +class LoopBody: + """ + Captures the body of a Loops subclass into an FX graph. Persists any + indexing simplifications and makes it easier to analyze loop bodies. + """ + + indexing_exprs: dict[str, sympy.Expr] + submodules: dict[str, Any] + subblocks: dict[str, LoopBodyBlock] + indirect_vars: list[sympy.Symbol] + indirect_var_ranges: dict[sympy.Symbol, sympy.Expr] + root_block: LoopBodyBlock + memory_usage: dict[MemoryUsageType, list[MemoryEntry]] + op_counts: collections.Counter[str] + + # defined only temporarily + indexing_exprs_name: dict[sympy.Expr, str] + + def __init__( + self, + fn, + args, + var_ranges, + iter_vars, + reduce_vars, + allow_same_symbol_in_index=False, + ): + super().__init__() + + _flat_sizes = tuple(var_ranges.values()) + self.sizes = ( + _flat_sizes[: len(iter_vars)], + _flat_sizes[len(iter_vars) :], + ) + + self.iter_vars = iter_vars + self.reduce_vars = reduce_vars + self.var_ranges = var_ranges + + if isinstance(fn, LoopBody): + self._init_with_copy(fn, args, allow_same_symbol_in_index) + else: + self._init_with_tracing(fn, args) + + self.indexing = None + + def get_original_num_rdims(self) -> int: + assert self.has_partial_accumulate + node = self.root_block.graph.find_nodes( + op="call_method", target="partial_accumulate" + )[0] + meta = node.args[-1] + return meta["num_reduction_dims"] + + def extract_pw_from_reduction(self): + self.root_block = self.root_block.extract_pw_from_reduction() + self.has_partial_accumulate = True + self.iter_vars = self.iter_vars + self.reduce_vars + self.reduce_vars = [] + self.sizes = (self.sizes[0] + self.sizes[1], tuple()) + return self + + def _init_with_tracing(self, fn, args): + """Do an FX trace of an arbitrary callable to construct self""" + self.indexing_exprs = {} + self.indexing_exprs_name = {} + self.submodules = {"get_index": self.get_index} + self.subblocks = {} + self.indirect_vars = [] + self.indirect_var_ranges: dict[sympy.Symbol, sympy.Expr] = {} + self.memory_usage = {t: [] for t in MemoryUsageType} + self.op_counts = collections.Counter() + self.root_block = LoopBodyBlock(self, fn, args) # traces + self.has_partial_accumulate = self.root_block.graph.find_nodes( + op="call_method", target="partial_accumulate" + ) + del self.indexing_exprs_name # not used after _init_with_tracing + + def _init_with_copy(self, other: LoopBody, args, allow_same_symbol_in_index): + """ + _init_with_tracing() is slow, so this is a fast path in the case + where we are just reordering/merging/splitting the args of an + existing LoopBody. + """ + indexing_exprs = other.indexing_from_args(args, allow_same_symbol_in_index) + self.indexing_exprs = { + name: V.graph.sizevars.simplify_with_ranges(expr, self.var_ranges) + for name, expr in indexing_exprs.items() + } + self.subblocks = {k: v.clone(self) for k, v in other.subblocks.items()} + self.indirect_vars = other.indirect_vars + self.indirect_var_ranges = other.indirect_var_ranges + self.memory_usage = other.memory_usage + self.op_counts = other.op_counts + self.root_block = other.root_block.clone(self) + self.has_partial_accumulate = other.has_partial_accumulate + + submodules = {**other.submodules} + submodules.pop("get_index") + self.submodules = { + "get_index": self.get_index, + **{k: v.clone(self) for k, v in submodules.items()}, # type: ignore[attr-defined] + } + + def has_op(self, name: str): + return self.op_counts.get(name, 0) > 0 + + def merge_loops(self) -> LoopBody: + """ + Merge both iteration and reduction loops and return a new LoopBody. + """ + old_body = self + old_sizes = self.sizes + old_iter_vars, old_reduce_vars = old_body.vars + old_iter_sizes, old_reduce_sizes = old_sizes + + index_exprs = [*old_body.indexing_exprs.values()] + + iter_sizes, iter_reindex, _ = V.graph.sizevars._simplify_loops( + old_iter_vars, + old_iter_sizes, + index_prevent_reordering(index_exprs, old_iter_vars, old_iter_sizes), + ) + + reduce_sizes, reduce_reindex, _ = V.graph.sizevars._simplify_loops( + old_reduce_vars, + old_reduce_sizes, + index_prevent_reordering(index_exprs, old_reduce_vars, old_reduce_sizes), + ) + + if iter_sizes == old_iter_sizes and reduce_sizes == old_reduce_sizes: + return old_body + + ( + ( + iter_vars, + reduce_vars, + ), + var_ranges, + ) = dependencies.index_vars_no_squeeze(iter_sizes, reduce_sizes, prefix="p") + new_body = LoopBody( + old_body, + [iter_reindex(iter_vars), reduce_reindex(reduce_vars)], + var_ranges, + iter_vars, + reduce_vars, + allow_same_symbol_in_index=True, + ) + + return new_body + + def expand_dimension_for_pointwise_node( + self, dimension: int, new_range: int + ) -> LoopBody: + """ + Expand node on `dimension` to `new_range` and rely on index modular to avoid + out-of-boundary access. + """ + + old_body = self + old_sizes = self.sizes + + iter_size, reduce_size = old_sizes + original_range = iter_size[dimension] + new_iter_size = list(iter_size) + new_iter_size[dimension] = new_range + new_sizes = (new_iter_size, reduce_size) + + (iter_vars, reduce_vars), var_ranges = dependencies.index_vars_no_squeeze( + *new_sizes, + prefix="t", # type: ignore[arg-type] + ) + + def new_body(*indices: Sequence[sympy.Expr]) -> Any: + index = [*itertools.chain.from_iterable(indices)] + assert len(index) == len(iter_size) + len(reduce_size) + iter_idx = index[: len(iter_size)] + reduce_idx = index[len(iter_size) :] + + new_iter_idx = list(iter_idx) + new_iter_idx[dimension] = Mod(iter_idx[dimension], original_range) + + return old_body(new_iter_idx, reduce_idx) + + loop_body = LoopBody( + new_body, (iter_vars, reduce_vars), var_ranges, iter_vars, reduce_vars + ) + + # use the original symbol prefix so we can do multiple round of reordering + (iter_vars2, reduce_vars2), var_ranges2 = dependencies.index_vars_no_squeeze( + *new_sizes, + prefix="p", # type: ignore[arg-type] + ) + new_body = LoopBody( + loop_body, (iter_vars2, reduce_vars2), var_ranges2, iter_vars2, reduce_vars2 + ) + return new_body + + def reorder_iter_loops(self, new_order) -> LoopBody: + """ + Reorder iteration loops and return a new LoopBody. + """ + from .ir import same_reorder + + old_body = self + old_sizes = self.sizes + assert len(old_sizes[0]) == len(new_order) + reorder_fn = same_reorder(new_order) + + iter_size, reduce_size = old_sizes + new_iter_size = reorder_fn(iter_size) + + new_sizes = (new_iter_size, reduce_size) + + (iter_vars, reduce_vars), var_ranges = dependencies.index_vars_no_squeeze( + *new_sizes, + prefix="p", # type: ignore[arg-type] + ) + + inverse_order = {b: a for a, b in enumerate(new_order)} + inverse_order = [inverse_order[i] for i in range(len(new_order))] + + def new_body(*indices: Sequence[sympy.Expr]) -> Any: + index = [*itertools.chain.from_iterable(indices)] + assert len(index) == len(iter_size) + len(reduce_size) + iter_idx = index[: len(iter_size)] + reduce_idx = index[len(iter_size) :] + iter_idx = [iter_idx[i] for i in inverse_order] + return old_body(iter_idx, reduce_idx, allow_same_symbol_in_index=True) + + return LoopBody( + new_body, + (iter_vars, reduce_vars), + var_ranges, + iter_vars, + reduce_vars, + ) + + @property + def vars(self): + assert self.iter_vars is not None + assert self.reduce_vars is not None + return self.iter_vars, self.reduce_vars + + @cache_on_self + def get_nodes(self): + all_graphs = itertools.chain( + (self.root_block.graph,), + (block.graph for block in self.subblocks.values()), + ) + return [node for graph in all_graphs for node in graph.nodes] + + @cache_on_self + def bounds(self): + # Doing a local import to avoid dumping all the code here + from .bounds import BoundVars + + return BoundVars(self) + + def get_read_expr(self, buffer_name): + # reversed to match old behavior + for entry in reversed(self.memory_usage[MemoryUsageType.LOAD]): + if entry.buffer_name == buffer_name: + return self.indexing_exprs[entry.index_name] + raise KeyError(buffer_name) + + def get_write_expr(self, buffer_name): + for entry in itertools.chain( + self.memory_usage[MemoryUsageType.STORE], + self.memory_usage[MemoryUsageType.STORE_REDUCTION], + ): + if entry.buffer_name == buffer_name: + return self.indexing_exprs[entry.index_name] + raise KeyError(buffer_name) + + def get_read_exprs(self): + return [ + self.indexing_exprs[entry.index_name] + for entry in self.memory_usage[MemoryUsageType.LOAD] + ] + + def get_all_read_expr(self, buffer_name): + # reversed to match old behavior + out = [] + for entry in reversed(self.memory_usage[MemoryUsageType.LOAD]): + if entry.buffer_name == buffer_name: + out.append(self.indexing_exprs[entry.index_name]) + return out + + def get_write_exprs(self): + return [ + self.indexing_exprs[entry.index_name] + for entry in itertools.chain( + self.memory_usage[MemoryUsageType.STORE], + self.memory_usage[MemoryUsageType.STORE_REDUCTION], + ) + ] + + def get_all_write_expr(self, buffer_name): + out = [] + for entry in itertools.chain( + self.memory_usage[MemoryUsageType.STORE], + self.memory_usage[MemoryUsageType.STORE_REDUCTION], + ): + if entry.buffer_name == buffer_name: + out.append(self.indexing_exprs[entry.index_name]) + return out + + def debug_str(self): + lines = [f"var_ranges = {dict(self.var_ranges)}"] + lines.extend([f"{name} = {val}" for name, val in self.indexing_exprs.items()]) + lines.extend( + [ + block.debug_str(name) + for name, block in itertools.chain( + [("body", self.root_block)], self.subblocks.items() + ) + ] + ) + return "\n".join(lines) + + def is_memory_copy(self) -> bool: + """ + True of this contains only a single loads and store. + Note, this could involve a layout change. + """ + return ( + len(self.memory_usage[MemoryUsageType.LOAD]) == 1 + and len(self.memory_usage[MemoryUsageType.STORE]) == 1 + and len(self.submodules) == 1 # get_index + and self.root_block.contains_only_ops(("load", "store")) + ) + + __repr__ = debug_str + + def add_index_expr( + self, + expr: sympy.Expr, + mtype: MemoryUsageType, + buffer_name: str | None = None, + mode: str | None = None, + ): + name = self.indexing_exprs_name.get(expr) + if not name: + name = f"index{len(self.indexing_exprs)}" + self.indexing_exprs_name[expr] = name + self.indexing_exprs[name] = expr + self.memory_usage[mtype].append(MemoryEntry(name, buffer_name, mode)) + return name + + def add_submodule(self, block, prefix): + """Not actually for nn.Modules, but subblocks in generated code are mapped to FX call_module opcodes""" + if prefix[-1].isnumeric() and prefix not in self.submodules: + name = prefix + else: + name = f"{prefix}{len(self.submodules)}" + self.submodules[name] = block + return name + + def add_indirect(self, size): + var = sympy_index_symbol_with_prefix(SymT.INDIRECT, len(self.indirect_vars)) + assert var not in self.indirect_var_ranges + self.indirect_vars.append(var) + self.indirect_var_ranges[var] = size + return var + + def replace_indirect(self, old, new): + """Swap in a variable used in indirect indexing""" + if str(old) == str(new): + return + assert self.indexing is not None + # pyrefly: ignore [bad-assignment] + self.indexing = {k: sympy_subs(v, {old: new}) for k, v in self.indexing.items()} + + def get_index(self, name): + assert self.indexing is not None + return self.indexing[name] + + def indexing_from_args(self, indices, allow_same_symbol_in_index=False): + index = [*itertools.chain.from_iterable(indices)] + assert len(index) == len(self.var_ranges), (index, self.var_ranges) + assert allow_same_symbol_in_index or all( + v not in self.var_ranges for v in index + ), f"{self.var_ranges=}, {indices=}" + + replacements = dict(zip(self.var_ranges.keys(), index)) + return { + name: sympy_subs(expr, replacements) + for name, expr in self.indexing_exprs.items() + } + + def __call__(self, *indices, allow_same_symbol_in_index=False): + self.indexing = self.indexing_from_args(indices, allow_same_symbol_in_index) + result = self.root_block() + self.indexing = None + return result + + def bind_set_indirect_shim(self, var, size, check, wrap_neg): + def set_indirect(new_var): + self.replace_indirect( + var, V.ops.indirect_indexing(new_var, size, check, wrap_neg) + ) + + set_indirect.clone = functools.partial( # type: ignore[attr-defined] + LoopBody.bind_set_indirect_shim, + var=var, + size=size, + check=check, + wrap_neg=wrap_neg, + ) + return set_indirect + + def bind_scan_shim(self, combine_fn): + def shim(dtypes, values): + return V.ops.scan(dtypes, combine_fn, values) + + shim.clone = functools.partial(LoopBody.bind_scan_shim, combine_fn=combine_fn) # type: ignore[attr-defined] + return shim + + def bind_masked_shim(self, name): + def shim(mask, other): + return V.ops.masked(mask, self.subblocks[name], other) + + shim.clone = functools.partial(LoopBody.bind_masked_shim, name=name) # type: ignore[attr-defined] + return shim + + +class LoopBodyBlock: + """ + Captures the body of a Loops subclass into an FX graph. + In normal cases there will be a 1:1 mapping between LoopBody and + LoopBodyBlock, however in the case of ops.masked() the masked out + operations will manifest as an extra LoopBodyBlock. + """ + + def __init__(self, body: LoopBody, fn: Callable[..., Any], args: list[Any]): + self.body = body + + tracer = LightTracer() + proxy_ops = tracer.create_proxy("placeholder", "ops", (), {}) + + from .index_propagation import IndexPropagation + + handler: Any = CountOps( + CaptureIndexing( + # pyrefly: ignore[bad-argument-type] + proxy_ops, + body, + tracer, + ), + body.op_counts, + ) + if config.constant_and_index_propagation: + handler = IndexPropagation( + handler, self.body.var_ranges, self.body.indirect_var_ranges + ) + + with V.set_ops_handler(handler): + # This indirection is just a cute way to get IndexPropagation to + # unwrap the return value. + ops.output(fn(*args)) + self.graph = tracer.graph + + def extract_pw_from_reduction(self): + red = None + store = None + for node in self.graph.nodes: + if node.target == "reduction": + assert not red + red = node + if node.target == "store_reduction": + assert not store + store = node + assert red + assert store + reduction_type = red.args[-2] + red_arg = red.args[-1] + buf = store.args[1] + ops = store.args[0] + + extra_meta = { + "num_reduction_dims": len(self.body.reduce_vars), + } + with self.graph.inserting_after(store): + self.graph.call_method( + "partial_accumulate", (ops, buf, reduction_type, red_arg, extra_meta) + ) + self.graph.erase_node(store) + self.graph.erase_node(red) + return self + + def __call__(self): + graph = self.graph + submodules = self.body.submodules + + return InterpreterShim(graph, submodules).run(V.get_ops_handler()) + + def debug_str(self, name="block"): + code = torch.fx.GraphModule(self.body.submodules, self.graph).code + return re.sub( + # strip `; del var0` suffixes to make output prettier + r";[^\n]*", + "", + code.strip().replace("def forward(", f"def {name}("), + ) + + def contains_only_ops(self, allowed_ops) -> bool: + return all( + node.target in allowed_ops + for node in self.graph.find_nodes(op="call_method") + ) + + def clone(self, body: LoopBody): + """Shallow copy with a new parent LoopBody""" + copy = LoopBodyBlock.__new__(LoopBodyBlock) + copy.__dict__.update({**self.__dict__, "body": body}) + return copy + + +class CountOps(DefaultHandler): + def __init__(self, inner: OpsHandler[Any], counts: collections.Counter[str]): + self._inner = inner + self._counts = counts + + def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: + self._counts[name] += 1 + return getattr(self._inner, name)(*args, **kwargs) + + +class CaptureIndexing(WrapperHandler): + name = "CaptureIndexing" + + def __init__( + self, + inner: OpsHandler[Any], + body: LoopBody, + tracer: LightTracer, + ): + super().__init__(inner) + self.body = body + self.tracer = tracer + + def _add_index(self, expr: sympy.Expr, mtype: MemoryUsageType, **kwargs: Any): + return self.tracer.create_proxy( + "call_module", + "get_index", + (self.body.add_index_expr(expr, mtype, **kwargs),), + {}, + ) + + def _simplify(self, expr: sympy.Expr) -> sympy.Expr: + return V.graph.sizevars.simplify_with_ranges(expr, self.body.var_ranges) + + def load(self, name: str, index: sympy.Expr): + index = self._simplify(index) + index = self._add_index(index, MemoryUsageType.LOAD, buffer_name=name) + return self._inner.load(name, index) + + def load_seed(self, name: str, index: int): + assert isinstance(index, int) + self.body.add_index_expr( + sympy.Integer(index), MemoryUsageType.LOAD_SEED, buffer_name=name + ) + return self._inner.load_seed(name, index) + + def store(self, name, index, value, mode=None): + index = self._simplify(index) + index = self._add_index( + index, MemoryUsageType.STORE, buffer_name=name, mode=mode + ) + return self._inner.store(name, index, value, mode) + + def store_reduction(self, name, index, value): + index = self._simplify(index) + index = self._add_index( + index, MemoryUsageType.STORE_REDUCTION, buffer_name=name + ) + return self._inner.store_reduction(name, index, value) + + def reduction(self, dtype, src_dtype, reduction_type, value): + result = self._inner.reduction(dtype, src_dtype, reduction_type, value) + num_outputs = reduction_num_outputs(reduction_type) + if num_outputs > 1: + return tuple(result[i] for i in range(num_outputs)) + return result + + def index_expr(self, index, dtype): + index = self._simplify(index) + if isinstance(index, (int, sympy.Integer)): + return self._inner.constant(int(index), dtype) + index = self._add_index(index, MemoryUsageType.INDEX_EXPR) + return self._inner.index_expr(index, dtype) + + def check_bounds(self, index, size, lower, upper): + index = self._simplify(index) + index = self._add_index(index, MemoryUsageType.CHECK_BOUNDS) + size = self._add_index(size, MemoryUsageType.CHECK_BOUNDS) + return self._inner.check_bounds(index, size, lower, upper) + + def bucketize( + self, + values: T, + boundaries: tuple[str, sympy.Expr, sympy.Expr, sympy.Expr], + boundary_indices: T, + indexing_dtype: torch.dtype, + right: bool, + sorter: tuple[str, sympy.Expr] | None = None, + sorter_indices: T | None = None, + ) -> T: + """ + See [Note: Inductor bucketize op] + """ + boundaries = ( + boundaries[0], + self._add_index( + boundaries[1], + MemoryUsageType.BUCKETIZE, + buffer_name=boundaries[0], + ), + self._add_index( + boundaries[2], + MemoryUsageType.BUCKETIZE, + buffer_name=boundaries[0], + ), + self._add_index( + boundaries[3], + MemoryUsageType.BUCKETIZE, + buffer_name=boundaries[0], + ), + ) + if sorter is not None: + sorter = ( + sorter[0], + self._add_index( + sorter[1], MemoryUsageType.BUCKETIZE, buffer_name=sorter[0] + ), + ) + + return self._inner.bucketize( + values, + boundaries, + boundary_indices, + indexing_dtype, + right, + sorter, + sorter_indices, + ) + + def masked(self, mask_proxy, masked_body: Callable[..., Any], other_proxy): + """ + Recursively capture the masked out body in another LoopBodyBlock + """ + name = self.body.add_submodule(None, "masked_subblock") + self.body.submodules[name] = self.body.bind_masked_shim(name) + self.body.subblocks[name] = LoopBodyBlock(self.body, masked_body, []) + return self.tracer.create_proxy( + "call_module", name, (mask_proxy, other_proxy), {} + ) + + def scan( + self, + dtype_proxy, + combine_fn: Callable[[tuple[Any, ...], tuple[Any, ...]], tuple[Any, ...]], + value_proxy, + ): + shim = self.body.bind_scan_shim(combine_fn) + name = self.body.add_submodule(shim, "scan") + result = self.tracer.create_proxy( + "call_module", + name, + (dtype_proxy, value_proxy), + {}, + ) + # Proxies are iterable, but some methods expect tuples/lists + return tuple(result[i] for i in range(len(value_proxy))) + + def sort(self, dtypes, values, stable, descending): + result = self._inner.sort(dtypes, values, stable, descending) + # Proxies are iterable, but some methods expect tuples/lists + return tuple(result[i] for i in range(len(values))) + + def frexp(self, value_proxy): + result = self._inner.frexp(value_proxy) + # Proxies are iterable, but some methods expect tuples/lists + return (result[0], result[1]) + + def indirect_indexing(self, index_proxy, size, check=True, wrap_neg=True): + """ + Flow data from tensors into indexing formulas. + Introduce a call_module to update the indexing. + """ + + var = self.body.add_indirect(size) + set_indirect = self.body.bind_set_indirect_shim(var, size, check, wrap_neg) + self.tracer.create_proxy( + "call_module", + self.body.add_submodule(set_indirect, f"set_{var}"), + (index_proxy,), + {}, + ) + return var + + def output(self, *result): + self.tracer.create_proxy("output", "output", result, {}) diff --git a/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/lowering.py b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/lowering.py new file mode 100644 index 0000000000000000000000000000000000000000..0b1c3f35932044d5b586f3f2245be880cc291b43 --- /dev/null +++ b/outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/lowering.py @@ -0,0 +1,8649 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import contextlib +import dataclasses +import functools +import itertools +import logging +import math +import operator +import os +import sys +import warnings +from collections import defaultdict +from collections.abc import Callable, Collection, Iterable, Sequence +from typing import Any, cast, TYPE_CHECKING, TypeGuard, TypeVar +from typing_extensions import ParamSpec +from unittest.mock import patch + +import sympy + +import torch +import torch.ao.quantization.fx._decomposed +import torch.fx +import torch.utils._pytree as pytree +from torch._dynamo.utils import counters +from torch._higher_order_ops.associative_scan import associative_scan_op +from torch._higher_order_ops.triton_kernel_wrap import triton_kernel_wrapper_mutation +from torch._library.fake_class_registry import FakeScriptObject +from torch._library.opaque_object import is_opaque_value +from torch._library.utils import get_layout_constraint_tag +from torch._prims_common import ( + canonicalize_dim, + canonicalize_dims, + check, + dtype_to_type, + elementwise_dtypes, + ELEMENTWISE_TYPE_PROMOTION_KIND, + get_computation_dtype, + is_boolean_dtype, + is_float_dtype, + is_integer_dtype, + Number, +) +from torch.fx.experimental.sym_node import magic_methods, method_to_operator +from torch.fx.experimental.symbolic_shapes import ( + free_unbacked_symbols, + has_free_unbacked_symbols, + resolve_unbacked_bindings, +) +from torch.utils._ordered_set import OrderedSet +from torch.utils._sympy.functions import ( + CeilDiv, + FloorDiv, + Identity, + Mod, + ModularIndexing, +) + +from .._dynamo.utils import import_submodule +from . import config, inductor_prims, ir, test_operators # NOQA: F401 +from .decomposition import decompositions, get_decompositions +from .ir import ( + BaseView, + DtypeView, + ExpandView, + IndexingConstant, + IRNode, + is_triton, + MutableBox, + OnlineSoftmaxReduction, + ops_wrapper, + PermuteView, + Pointwise, + Reduction, + SqueezeView, + TensorBox, + validate_ir, + View, +) +from .utils import ( + ceildiv, + decode_device, + is_dynamic, + is_gpu, + is_pointwise_use, + is_view, + needs_fallback_due_to_atomic_add_limitations, + pad_listlike, + register_op_dtype_propagation_rules, + register_op_requires_libdevice_fp64, + sympy_product, + use_scatter_fallback, +) +from .virtualized import ops, V + + +if TYPE_CHECKING: + from .ops_handler import ReductionType + + +_T = TypeVar("_T") +_P = ParamSpec("_P") + +# TODO(jansel): we should implement decomps or lowerings for these +# https://github.com/pytorch/torchdynamo/issues/327 +FALLBACK_ALLOW_LIST = OrderedSet( + [ + "torchvision::roi_align", + "aten::index_add", + ] +) + +log = logging.getLogger(__name__) +lowerings: dict[Callable[..., Any] | str, Callable[..., Any]] = {} +# User-registered lowerings that take priority over built-in lowerings. +user_lowerings: dict[torch._ops.OpOverload, Callable[..., Any]] = {} +# Use maybe_layout_constraints to access this dict, we lazily register tag-based layout constraints +_maybe_layout_constraints: dict[torch._ops.OpOverload, Callable[..., Any] | None] = {} +fallbacks = OrderedSet[torch._ops.OpOverload]() +aten = torch.ops.aten +tr_c10d = torch.ops.tr_c10d +prims = torch.ops.prims +needs_realized_inputs = OrderedSet[torch._ops.OpOverload]() +foreach_ops = OrderedSet[torch._ops.OpOverload]( + [torch._higher_order_ops._foreach_map] # type: ignore[list-item] +) +# TODO(rec): torch._higher_order_ops._foreach_map is not an OpOverload +# so why is it in foreach_ops? +inplace_foreach_ops = OrderedSet[torch._ops.OpOverload]() +inplaceable_foreach_ops: dict[torch._ops.OpOverload, torch._ops.OpOverload] = {} +quantized_decomposed = torch.ops.quantized_decomposed + + +def cur_node_has_non_foreach_users() -> bool: + for node in V.graph.current_node.users: + for user in node.users: + if not (user.op == "call_function" and (user.target in foreach_ops)): + return True + + return False + + +# group by device, whether any of the inputs are dynamic +# note arg_pairs may or may not be a pair +# foreach_map for example just passes output buffers here +def group_foreach_args( + arg_pairs: Iterable[Any], +) -> defaultdict[tuple[Any, bool], list[tuple[int, Any]]]: + out = defaultdict(list) + unpack_args = False + for i, args in enumerate(arg_pairs): + if not isinstance(args, Iterable): + unpack_args = True + args = (args,) + use_foreach = ( + not is_dynamic(*args) or config.combo_kernel_foreach_dynamic_shapes + ) + device = None + for t in args: + if isinstance(t, TensorBox): + device = t.data.get_device() + break + assert device is not None, "foreach op should have at least one tensor arg" + if unpack_args: + (args,) = args + out[(device, use_foreach)].append((i, args)) + return out + + +def maybe_layout_constraints(fn: Callable[..., Any]) -> Callable[..., Any] | None: + """Get layout constraints. Returns None if there are no layout constraints.""" + if not isinstance(fn, torch._ops.OpOverload): + # Only OpOverloads have layout constraints. + return None + + if maybe_layout_tag := get_layout_constraint_tag(fn, with_default=False): + return tag_to_layout_constraint(maybe_layout_tag) + + if fn in _maybe_layout_constraints: + return _maybe_layout_constraints[fn] + return None + + +def tag_to_layout_constraint( + tag: torch._C.Tag, +) -> Callable[..., tuple[Any, Any]] | None: + if tag == torch._C.Tag.needs_exact_strides: + return constrain_to_fake_tensors + if tag == torch._C.Tag.needs_contiguous_strides: # type: ignore[attr-defined] + return require_contiguous_strides + if tag == torch._C.Tag.needs_fixed_stride_order: + return constrain_to_fx_strides + if tag == torch._C.Tag.flexible_layout: + return None + raise AssertionError(f"Unknown layout constraint tag: {tag}") + + +def assert_nyi(cond: bool, msg: str) -> None: + if not cond: + raise NotImplementedError(f"inductor does not support {msg}") + + +def add_needs_realized_inputs( + fn: Collection[torch._ops.OpOverload | torch._ops.OpOverloadPacket] + | torch._ops.OpOverload + | torch._ops.OpOverloadPacket, +) -> list[Any] | None: + if isinstance(fn, (list, set, tuple, OrderedSet)): # noqa: set_linter + # pyrefly: ignore [bad-argument-type] + return [add_needs_realized_inputs(x) for x in fn] + if isinstance(fn, torch._ops.OpOverload): + needs_realized_inputs.add(fn) + elif isinstance(fn, torch._ops.OpOverloadPacket): + needs_realized_inputs.update( + getattr(fn, overload) for overload in fn.overloads() + ) + return None + + +def add_layout_constraint( + fn: torch._ops.OpOverloadPacket | torch._ops.OpOverload, + constraint: Callable[..., tuple[Any, Any]], +) -> None: + if isinstance(fn, torch._ops.OpOverloadPacket): + for overload in fn.overloads(): + _maybe_layout_constraints[getattr(fn, overload)] = constraint + else: + _maybe_layout_constraints[fn] = constraint + + +add_needs_realized_inputs( + [ + aten.as_strided, + aten.as_strided_copy, + aten.avg_pool2d, + aten.avg_pool2d_backward, + aten.bmm, + aten.convolution, + aten.convolution_backward, + aten.max_pool2d_with_indices, + aten.max_pool3d_with_indices, + aten.max_pool2d_with_indices_backward, + aten.mm, + aten.upsample_nearest2d, + aten._upsample_nearest_exact2d, + aten._int_mm, + ] +) + +# TODO(jansel): ezyang says we won't need this in the future, try removing it +# based on https://github.com/pytorch/pytorch/blob/9e3eb329df8f701/c10/core/ScalarType.h#L28 +DTYPE_ID_LOOKUP = { + 0: torch.uint8, + 1: torch.int8, + 2: torch.int16, + 3: torch.int32, + 4: torch.int64, + 5: torch.float16, + 6: torch.float32, + 7: torch.float64, + 8: torch.complex32, + 9: torch.complex64, + 10: torch.complex32, + 11: torch.bool, + 15: torch.bfloat16, + # TODO(jansel): add quantized types? + # _(c10::qint8, QInt8) /* 12 */ + # _(c10::quint8, QUInt8) /* 13 */ + # _(c10::qint32, QInt32) /* 14 */ + # _(c10::quint4x2, QUInt4x2) /* 16 */ + # _(c10::quint2x4, QUInt2x4) /* 17 */ +} + + +def decode_dtype(dtype: int | torch.dtype) -> torch.dtype: + if not isinstance(dtype, int): + return dtype + assert dtype in DTYPE_ID_LOOKUP, f"id {dtype} missing from DTYPE_ID_LOOKUP" + + dtype = DTYPE_ID_LOOKUP[dtype] + return dtype + + +def is_integer_type(x: Any) -> TypeGuard[TensorBox | sympy.Expr | int]: + if isinstance(x, TensorBox): + return is_integer_dtype(x.get_dtype()) or is_boolean_dtype(x.get_dtype()) + elif isinstance(x, sympy.Expr): + return x.is_integer is True # type: ignore[attr-defined] + else: + return isinstance(x, int) + + +def is_boolean_type(x: Any) -> TypeGuard[TensorBox | bool]: + if isinstance(x, TensorBox): + return is_boolean_dtype(x.get_dtype()) + else: + return isinstance(x, bool) + + +def get_promoted_dtype( + *args: Any, + type_promotion_kind: ELEMENTWISE_TYPE_PROMOTION_KIND, + return_compute_dtype: bool = False, +) -> torch.dtype: + def construct_input(inp: Any) -> Any: + if isinstance(inp, (Number, sympy.Basic)): + return inp + else: + dim = len(inp.get_size()) + # construct a tmp tensor to feed into torch.result_type + return torch.zeros([1] * dim, dtype=inp.get_dtype()) + + inps = [construct_input(arg) for arg in args] + compute_dtype, result_dtype = elementwise_dtypes( + *inps, type_promotion_kind=type_promotion_kind + ) + return compute_dtype if return_compute_dtype else result_dtype + + +def get_overloads(aten_fn): + if not isinstance(aten_fn, (list, tuple)): + aten_fn = [aten_fn] + else: + aten_fn = list(aten_fn) + + for fn in list(aten_fn): + if isinstance(fn, torch._ops.OpOverloadPacket): + for overload in fn.overloads(): + other_fn = getattr(fn, overload) + if other_fn not in lowerings: + aten_fn.append(other_fn) + + return aten_fn + + +def in_namespace( + op: Any | torch._ops.OpOverloadPacket | torch._ops.OpOverload, namespace: str +) -> bool: + if isinstance(op, torch._ops.OpOverloadPacket): + return namespace in op._qualified_op_name + elif isinstance(op, torch._ops.OpOverload): + return namespace in op.name() + return False + + +def maybe_copy_cpu_scalar(x: TensorBox, device: torch.device) -> TensorBox: + """ + Copy cpu scalar if doesn't not match with given `device` + """ + if not isinstance(x.data, ir.ReinterpretView) or has_free_unbacked_symbols( + x.get_size() + ): + return x + size = V.graph.sizevars.guarding_hints_or_throw(x.get_size()) + cur_device = x.get_device() + if ( + cur_device is not None + and cur_device.type == "cpu" + and cur_device != device + and (len(size) == 0 or (len(size) == 1 and size[0] == 1)) + ): + return TensorBox(ir.StorageBox(ir.DeviceCopy.create(x, cur_device, False))) + return x + + +def transform_args( + args: list[Any], + kwargs: dict[str, Any], + broadcast: bool, + type_promotion_kind: ELEMENTWISE_TYPE_PROMOTION_KIND | None, + convert_input_to_bool: bool, +) -> tuple[list[Any], dict[str, Any]]: + """ + Transforms arguments for broadcasting and type promotion + """ + + args_indices = [i for i, x in enumerate(args) if isinstance(x, TensorBox)] + kwargs_indices = [k for k, v in kwargs.items() if isinstance(v, TensorBox)] + # check that there's something to transform + if not args_indices and not kwargs_indices: + return args, kwargs + + if type_promotion_kind or convert_input_to_bool: + if convert_input_to_bool: + dtype = torch.bool + else: + # FIXME this is a crude approximation for promoting args + promoting_args = [ + a + for a in args + if isinstance(a, (Number, sympy.Basic)) or hasattr(a, "dtype") + ] + # only consider tensor kwargs for promotion, for now + promoting_args.extend(a for a in kwargs.values() if hasattr(a, "dtype")) + dtype = get_promoted_dtype( + *promoting_args, + type_promotion_kind=type_promotion_kind, # type: ignore[arg-type] + ) + + device = ( + args[args_indices[0]] if args_indices else kwargs[kwargs_indices[0]] + ).get_device() + + for i in args_indices: + args[i] = maybe_copy_cpu_scalar(args[i], device) + + for k in kwargs_indices: + kwargs[k] = maybe_copy_cpu_scalar(kwargs[k], device) + + # sometimes args are an immutable list so we can't mutate them + def promote(arg: Any) -> Any: + if isinstance(arg, TensorBox): + return to_dtype(arg, dtype) + elif isinstance(arg, ir.Constant): + return ir.Constant(value=arg.value, dtype=dtype, device=device) + else: + return arg + + args = [promote(a) for a in args] + kwargs = {k: promote(v) for k, v in kwargs.items()} + + if broadcast: + broadcasted = broadcast_tensors( + *list( + itertools.chain( + (args[i] for i in args_indices), + (kwargs[k] for k in kwargs_indices), + ) + ) + ) + size = list(broadcasted[0].get_size()) + + for i, x in zip(args_indices, broadcasted[: len(args_indices)]): + args[i] = x + for k, x in zip(kwargs_indices, broadcasted[len(args_indices) :]): + kwargs[k] = x + + for i in range(len(args)): + if isinstance(args[i], ir.Constant): + args[i] = ExpandView.create(args[i], size) + for k in kwargs: + if isinstance(kwargs[k], ir.Constant): + kwargs[k] = ExpandView.create(kwargs[k], size) + + return args, kwargs + + +def _register_foreach_lowering( + aten_fn: torch._ops.OpOverload, decomp_fn: Callable[..., Any] +) -> Callable[..., Any]: + """ + Add a foreach lowering to lowerings dict. + + Arguments: + aten_fn: torch.ops.aten.* fn we are lowering + decomp_fn: alternate implementation on our IR + broadcast: True to apply broadcasting to tensor inputs + type_promotion_kind: kind of type promotion applied to tensor inputs, `None` means no type promotion + convert_input_to_bool: some logical ops require inputs are converted to bool + """ + + @functools.wraps(decomp_fn) + def wrapped(*args: Any, **kwargs: Any) -> Any: + out = decomp_fn(*args, **kwargs) + validate_ir(out) + return out + + aten_fns = get_overloads(aten_fn) + foreach_ops.update(aten_fns) + lowerings.update(dict.fromkeys(aten_fns, wrapped)) + return wrapped + + +def _register_lowering( + aten_fn, + decomp_fn: Callable[..., Any], + broadcast: bool, + type_promotion_kind: ELEMENTWISE_TYPE_PROMOTION_KIND | None, + convert_input_to_bool: bool, + lowering_dict: dict[Callable[..., Any] | str, Callable[..., Any]], +): + """ + Add a lowering to lowerings dict + + Arguments: + aten_fn: torch.ops.aten.* fn we are lowering + decomp_fn: alternate implementation on our IR + broadcast: True to apply broadcasting to tensor inputs + type_promotion_kind: kind of type promotion applied to tensor inputs, `None` means no type promotion + convert_input_to_bool: some logical ops require inputs are converted to bool + """ + + @functools.wraps(decomp_fn) + def wrapped(*args, **kwargs): + args: list[Any] = list(args) + kwargs: dict[str, Any] = dict(kwargs) + unpacked = False + # TODO maybe we need to use pytrees here + if len(args) == 1 and isinstance(args[0], (list, tuple)): + unpacked = True + args = list(args[0]) + + if not all( + (fn in fallbacks or in_namespace(fn, "_c10d_functional")) for fn in aten_fn + ): + # explicitly assert for "out=" ops for better error messages + assert not any(x == "out" for x in kwargs), "out= ops aren't yet supported" + + args, kwargs = transform_args( + args, kwargs, broadcast, type_promotion_kind, convert_input_to_bool + ) + + if unpacked: + args = [args] + + out = decomp_fn(*args, **kwargs) + validate_ir(out) + + return out + + aten_fn = get_overloads(aten_fn) + + lowering_dict.update(dict.fromkeys(aten_fn, wrapped)) + return wrapped + + +def register_lowering( + aten_fn, + broadcast=False, + type_promotion_kind: ELEMENTWISE_TYPE_PROMOTION_KIND + | None = ELEMENTWISE_TYPE_PROMOTION_KIND.DEFAULT, + convert_input_to_bool=False, + lowering_dict=lowerings, +) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]: + """ + Shim to support decorator syntax. + """ + return functools.partial( + _register_lowering, + aten_fn, + broadcast=broadcast, + type_promotion_kind=type_promotion_kind, + convert_input_to_bool=convert_input_to_bool, + lowering_dict=lowering_dict, + ) + + +def broadcast_symbolic_shapes(a, b): + """ + Broadcasting logic based on symbolic shapes. + + We give the shapes 0 and 1 concrete values, while all other shapes + are symbolic sympy formulas. + """ + b = tuple(b) + if not a or a == b: + return b + + output = [] + for x, y in itertools.zip_longest(reversed(a), reversed(b), fillvalue=sympy.S.One): + if V.graph.sizevars.is_size_one_or_false(y): + output.append(x) + elif V.graph.sizevars.is_size_one_or_false(x): + output.append(y) + else: + V.graph.sizevars.check_equals(x, y) + if len(sympy.expand(y).free_symbols) < len(sympy.expand(x).free_symbols): + output.append(y) # prefer shorter formula + else: + output.append(x) + return tuple(reversed(output)) + + +def promote_constants(inputs, override_return_dtype=None, type_promotion_kind=None): + assert override_return_dtype is None or type_promotion_kind is None, ( + "only one of override_return_dtype or type_promotion_kind may be given" + ) + + if override_return_dtype is None and type_promotion_kind is None: + type_promotion_kind = ELEMENTWISE_TYPE_PROMOTION_KIND.DEFAULT + + if not any(isinstance(x, (sympy.Basic, int, float)) for x in inputs): + return inputs + if all(isinstance(x, (int, float, sympy.Basic)) for x in inputs): + dtype = override_return_dtype or get_promoted_dtype( + *inputs, + # pyrefly: ignore [bad-argument-type] + type_promotion_kind=type_promotion_kind, + ) + + def const_func(x): + if isinstance(x, sympy.Basic): + return ir.IndexingConstant( + index=x, dtype=dtype, device=decode_device(None) + ) + else: + return ir.Constant(value=x, dtype=dtype, device=decode_device(None)) + + return [const_func(x) for x in inputs] + ex = next(x for x in inputs if isinstance(x, (TensorBox, ExpandView, ir.Constant))) + tensor_dtype = ex.get_dtype() + + # Round scalar to tensor's dtype for comparison ops to match eager + if override_return_dtype == torch.bool and tensor_dtype in ( + torch.bfloat16, + torch.float16, + ): + _round_scalar = lambda v: torch.tensor(v, dtype=tensor_dtype).item() # noqa: E731 + else: + _round_scalar = lambda v: v # noqa: E731 + + out = [] + for x in inputs: + if isinstance(x, (int, float)): + out.append( + ExpandView.create( + ir.Constant( + value=_round_scalar(x), + dtype=tensor_dtype, + device=ex.get_device_or_error(), + ), + list(ex.get_size()), + ) + ) + elif isinstance(x, sympy.Basic): + out.append( + ExpandView.create( + IndexingConstant( + index=x, dtype=tensor_dtype, device=ex.get_device_or_error() + ), + list(ex.get_size()), + ) + ) + else: + out.append(x) + + return out + + +def _add_with_alpha_fma(a, b, alpha): + """Compute a + alpha * b using FMA for CUDA floating-point precision.""" + dtype = get_promoted_dtype( + a, + b, + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.DEFAULT, + ) + a_loader = a.make_loader() + b_loader = b.make_loader() + + def inner_fn(idx): + a_val = a_loader(idx) + b_val = b_loader(idx) + if isinstance(alpha, sympy.Basic): + alpha_expr = ops.index_expr(alpha, dtype) + else: + alpha_expr = ops.constant(alpha, dtype) + return ops.fma(b_val, alpha_expr, a_val) + + return Pointwise.create( + device=a.get_device(), + dtype=dtype, + inner_fn=inner_fn, + ranges=a.get_size(), + ) + + +def make_pointwise( + fn, + override_return_dtype=None, + override_device=None, + override_fn_when_input_bool=None, + allow_alpha=False, + use_fma_for_alpha=False, + triton_fallback=None, +): + """Wraps a pointwise fn and returns a function representing the pointwise in + the define-by-run IR.""" + + def inner(*inputs: TensorBox, alpha=None): + if triton_fallback is not None and any( + isinstance(inp, IRNode) and is_triton(inp) for inp in inputs + ): + assert not allow_alpha # not implemented + return triton_fallback(*inputs) + + inputs = promote_constants(inputs, override_return_dtype) + if allow_alpha: + if alpha is not None and alpha != 1: + # Use FMA for add-with-alpha on CUDA floating-point. + # Eager CUDA computes a + alpha * b as fma(b, alpha, a). + if use_fma_for_alpha and isinstance(inputs[0], IRNode): + inp_device = inputs[0].get_device() + if ( + inputs[0].get_dtype().is_floating_point + and not torch.version.hip + and inp_device is not None + and inp_device.type == "cuda" + ): + return _add_with_alpha_fma(inputs[0], inputs[1], alpha) + + # pyrefly: ignore [bad-assignment] + inputs = list(inputs) + # pyrefly: ignore [unsupported-operation] + inputs[-1] = mul(inputs[-1], alpha) + else: + assert alpha is None + loaders = [x.make_loader() for x in inputs] + ranges = inputs[0].get_size() + dtype = override_return_dtype or inputs[0].get_dtype() + + for other in inputs[1:]: + assert isinstance(other, ir.BaseConstant) or len(ranges) == len( + other.get_size() + ), f"ndim mismatch {fn} {ranges} {other.get_size()}" + + # in tracing, we will annotate pointwise nodes that correspond to the output of + # a pointwise node that would have been run in eager. intermediary pointwise nodes + # during decompositions are not annotated. + low_pr_fp = (torch.bfloat16, torch.float16) + emulate_precision_casts = ( + V.graph is not None + and getattr(V.graph, "current_node", None) is not None + and V.graph.current_node.meta is not None + and V.graph.current_node.meta.get("low_precision_pointwise_barrier", False) + ) + emulate_output_cast = emulate_precision_casts and dtype in low_pr_fp + + def inner_fn(index): + assert len(index) == len(ranges), f"wrong ndim {index} {ranges}" + if dtype == torch.bool and override_fn_when_input_bool is not None: + return override_fn_when_input_bool(*[load(index) for load in loaders]) + else: + inputs_loaded = [] + for inp_index, load in enumerate(loaders): + out = load(index) + inp_dtype = inputs[inp_index].get_dtype() + if emulate_precision_casts and inp_dtype in low_pr_fp: + downcast = ops.to_dtype(out, inp_dtype, use_compute_types=False) + out = ops.to_dtype(downcast, inp_dtype) + inputs_loaded.append(out) + + out = fn(*inputs_loaded) + if emulate_output_cast: + # fp16/bf16 kernels are computed in fp32. Casting down to fp16/bf16 here, + # then upcasting again, to emulate casts that eager would do. + downcast = ops.to_dtype(out, dtype, use_compute_types=False) + return ops.to_dtype(downcast, dtype) + return out + + if not override_device: + device = None + for i in inputs: + if is_gpu(i.get_device().type): + device = i.get_device() + break + if not device: + device = inputs[0].get_device() + + # pyrefly: ignore [unbound-name] + device = override_device or device + + return Pointwise.create( + device=device, # type: ignore[arg-type] + dtype=dtype, + inner_fn=inner_fn, + ranges=ranges, + ) + + return inner + + +def make_foreach_pointwise(pw_fn, allow_alpha=False, scalar_kwarg="alpha"): + def inner(*inputs: list[list[TensorBox]], alpha=1, value=1): + # For ops like addcmul/addcdiv, the scalar `value` arrives as a + # positional arg (not keyword) due to the ATen schema. Extract it + # from the end of inputs if present. + # pyrefly: ignore [bad-assignment] + inputs = list(inputs) + if ( + scalar_kwarg == "value" + and inputs + and not isinstance(inputs[-1], (list, tuple)) + ): + # pyrefly: ignore [missing-attribute] + scalar_val = inputs.pop() + elif scalar_kwarg == "value": + scalar_val = value + else: + scalar_val = alpha + + realize_outputs = ( + len(V.graph.current_node.users) == 0 + or V.graph.current_node.target in inplace_foreach_ops + or cur_node_has_non_foreach_users() + ) + + a_list_input = None + for input in inputs: + if isinstance(input, (list, tuple)): + a_list_input = input + break + assert a_list_input is not None, ( + "at least one input must be a list to a foreach op" + ) + + # broadcast scalar inputs to match length of list inputs + broadcast_inputs = [] + for input in inputs: + if not isinstance(input, (list, tuple)): + broadcast_inputs.append([input] * len(a_list_input)) + else: + # pyrefly: ignore [bad-argument-type] + broadcast_inputs.append(input) + + groups = group_foreach_args(zip(*broadcast_inputs)) + + def apply_fn(args): + if allow_alpha: + return pw_fn(*args, **{scalar_kwarg: scalar_val}) + else: + return pw_fn(*args) + + return foreach_group_loop(groups, len(a_list_input), apply_fn, realize_outputs) + + return inner + + +def foreach_group_loop(groups, num_outputs, apply_fn, realize_outputs): + """ + Common loop over grouped foreach arguments. + + Args: + groups: Result of group_foreach_args - dict mapping (device, use_foreach) to groups + num_outputs: Number of outputs to produce + apply_fn: Function to apply to each set of args, returns the output + realize_outputs: Whether to realize outputs for foreach fusion + """ + outputs = [None] * num_outputs + for (device, use_foreach), group in groups.items(): + operation_list: list[str] = [] + for output_ind, args in group: + output = apply_fn(args) + outputs[output_ind] = output + + if ( + V.graph.has_feature(device, BackendFeature.FOREACH) + and use_foreach + and realize_outputs + ): + output.realize() + operation_list.append(output.get_operation_name()) + + if operation_list: + V.graph.register_operation_list(operation_list) + + assert all(x is not None for x in outputs) + return outputs + + +def to_dtype( + x: TensorBox, dtype: torch.dtype, copy: bool = False, use_compute_types: bool = True +): + src_dtype = x.get_dtype() + if src_dtype == dtype: + return clone(x) if copy else x + + def _to_dtype(x): + result = ops.to_dtype( + x, + dtype, + src_dtype=src_dtype, + use_compute_types=use_compute_types, + ) + low_pr_fp = (torch.bfloat16, torch.float16) + if not use_compute_types and dtype in low_pr_fp: + # Upcast back to compute type so fused consumers see a compute-type + # value. Without this, a raw low-precision value gets a redundant + # downcast from the consumer's input emulation. + result = ops.to_dtype(result, dtype) + return result + + return make_pointwise(_to_dtype, override_return_dtype=dtype)(x) + + +@register_lowering(torch._higher_order_ops._foreach_map, type_promotion_kind=None) +def _foreach_map(subgraph, *args, **kwargs): + """ + This lowers an invocation of foreach_map + The way this works is that an arbitrary N-arg func is provided by the user, looped over by the + polyfill with the same semantics as a foreach op (a loop applying an n-ary function to n args) + and then traced into a subgraph by dynamo. + This code allows us to inline the subgraph into the main graph lowering using the PontwiseSubgraphLowering. + The graph outputs represent the vertically fused sequence of ops, and then register_operation_list + below registers the buffers as horizontally fuseable in the scheduler. + """ + from .subgraph_lowering import PointwiseSubgraphLowering + + inputs = args + + gm = subgraph.graph_module + pw_subgraph = PointwiseSubgraphLowering(gm, root_graph_lowering=V.graph) + with V.set_graph_handler(pw_subgraph): # type: ignore[arg-type] + pw_subgraph.run(*inputs) + + sub_outputs = pw_subgraph.graph_outputs + # group outputs by device and register as foreach + assert sub_outputs # mypy lol + groups = group_foreach_args(sub_outputs) + + outputs = [None] * len(sub_outputs) + for (device, use_foreach), group in groups.items(): + operation_list: list[str] = [] + for ( + output_ind, + output, + ) in group: + outputs[output_ind] = output + + if V.graph.has_feature(device, BackendFeature.FOREACH) and use_foreach: + output.realize() + operation_list.append(output.get_operation_name()) + + if operation_list: + V.graph.register_operation_list(operation_list) + + assert all(x is not None for x in outputs) + return outputs + + +@register_lowering(prims.convert_element_type, type_promotion_kind=None) +def _convert_element_type(x: TensorBox, dtype: torch.dtype): + if dtype.is_complex or x.get_dtype().is_complex: + if x.get_size(): + # Decompose since aa aten fallback is more friendly for c++ codegen. + # This decomposition doesn't work for empty tensor, which needs more investigation. + dst = empty_like(x, dtype=dtype) + ir.InplaceCopyFallback.create(dst, x) + return dst + else: + return fallback_handler( + prims.convert_element_type.default, add_to_fallback_set=False + )(x, dtype) + src_dtype = x.get_dtype() + low_pr_fp = (torch.bfloat16, torch.float16) + use_compute_types = not ( + config.emulate_precision_casts + and (src_dtype in low_pr_fp or dtype in low_pr_fp) + ) + return to_dtype(x, dtype, copy=True, use_compute_types=use_compute_types) + + +def to_dtype_bitcast(x: TensorBox, dtype: torch.dtype, *, copy=False): + x_dtype = x.get_dtype() + if x_dtype == dtype: + return clone(x) if copy else x + + def _get_primitive_bitwidth(dtype): + if dtype.is_floating_point: + return torch.finfo(dtype).bits + elif dtype == torch.bool: + # torch.iinfo doesn't support bool; bools are stored as uint8 (8 bits) + return 8 + else: + return torch.iinfo(dtype).bits + + src_bits = _get_primitive_bitwidth(x_dtype) + dst_bits = _get_primitive_bitwidth(dtype) + if src_bits != dst_bits: + # fallback to aten eager implementation for differing bitwidths + return fallback_handler(aten.view.dtype)(x, dtype) + else: + return TensorBox(DtypeView.create(x, dtype)) + + +@register_lowering(aten.view.dtype, type_promotion_kind=None) +def _view_dtype(x: TensorBox, dtype: torch.dtype): + if dtype.is_complex or x.get_dtype().is_complex: + return TensorBox.create( + ir.ComplexView.create(torch.ops.aten.view.dtype, x, dtype) + ) + return to_dtype_bitcast(x, dtype) + + +def to_device(x: TensorBox, device: torch.device, *, copy=False, non_blocking=False): + device = decode_device(device) + if x.get_device() == device: + return clone(x) if copy else x + return TensorBox.create(ir.DeviceCopy.create(x, device, non_blocking)) + + +@register_lowering(prims.device_put, type_promotion_kind=None) +def _device_put(x: TensorBox, device: torch.device, non_blocking=False): + return to_device(x, device, copy=True, non_blocking=non_blocking) + + +def register_pointwise( + aten_fn, + name=None, + broadcast=True, + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.DEFAULT, + convert_input_to_bool=False, + override_return_dtype=None, + override_fn_when_input_bool=None, + allow_alpha=False, + use_fma_for_alpha=False, + triton_fallback=None, +): + """A pointwise function that maps ops.{name} to inputs""" + name = name or aten_fn.__name__ + fn = ops_wrapper(name) + + register_op_dtype_propagation_rules( + name, type_promotion_kind, override_return_dtype + ) + + if override_fn_when_input_bool is not None: + override_fn_when_input_bool = ops_wrapper(override_fn_when_input_bool) + + fn = make_pointwise( + fn, + override_return_dtype=override_return_dtype, + override_fn_when_input_bool=override_fn_when_input_bool, + allow_alpha=allow_alpha, + use_fma_for_alpha=use_fma_for_alpha, + triton_fallback=triton_fallback, + ) + fn = register_lowering( + aten_fn, + broadcast=broadcast, + type_promotion_kind=type_promotion_kind, + convert_input_to_bool=convert_input_to_bool, + )(fn) + + if hasattr(prims, name): + register_lowering( + getattr(prims, name), + type_promotion_kind=None, + convert_input_to_bool=convert_input_to_bool, + )(fn) + return fn + + +register_op_dtype_propagation_rules( + "ldexp", + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + override_return_dtype=None, +) + + +@register_lowering(aten.ldexp, broadcast=True, type_promotion_kind=None) +def ldexp_lowering(x: TensorBox, n: TensorBox): + ldexp_fn = ops_wrapper("ldexp") + + x_dtype = x.get_dtype() + n_dtype = n.get_dtype() + + x_is_float = x_dtype.is_floating_point + n_is_int = not n_dtype.is_floating_point and n_dtype != torch.bool + + if x_is_float and n_is_int: + # Use native ldexp + def compute_ldexp(x, n): + return ldexp_fn(x, n) + + return make_pointwise(compute_ldexp)(x, n) + else: + # Fall back to decomposition: x * pow(2, n) + out_dtype = torch.float32 if is_integer_type(x) else x_dtype + + def compute_fallback(x, n): + n_out_type = ops.to_dtype(n, out_dtype) + two = ops.constant(2.0, out_dtype) + pow_result = ops.pow(two, n_out_type) + return ops.mul(x, pow_result) + + return make_pointwise( + compute_fallback, + override_return_dtype=out_dtype, + )(x, n) + + +def register_frexp(): + """A pointwise function that maps ops.frexp to inputs""" + name = "frexp" + frexp = ops_wrapper("frexp") + + def frexp0(*args, **kwargs): + return frexp(*args, **kwargs)[0] # type: ignore[index] + + def frexp1(*args, **kwargs): + return frexp(*args, **kwargs)[1] # type: ignore[index] + + pw_fns = [ + make_pointwise(frexp0), + make_pointwise(frexp1, override_return_dtype=torch.int32), + ] + + def fn(*args, **kwargs): + return pw_fns[0](*args, **kwargs), pw_fns[1](*args, **kwargs) + + fn = register_lowering( + aten.frexp, + )(fn) + + if hasattr(prims, name): + register_lowering( + getattr(prims, name), + type_promotion_kind=None, + )(fn) + return fn + + +register_frexp() + + +def register_foreach_pointwise( + aten_fn, + pointwise_lowering_fn, + allow_alpha=False, + scalar_kwarg="alpha", +): + fn = make_foreach_pointwise( + pointwise_lowering_fn, allow_alpha=allow_alpha, scalar_kwarg=scalar_kwarg + ) + fn = _register_foreach_lowering(aten_fn, fn) + return fn + + +@register_lowering(aten.where, broadcast=False, type_promotion_kind=None) +def where(cond, a, b): + def fn(*args): + return ops.where(*args) + + if isinstance(a, (float, int)): + a = constant_like(a)(b) + if isinstance(b, (float, int)): + b = constant_like(b)(a) + + args = [cond, a, b] + dtype = get_promoted_dtype( + args[1], args[2], type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.DEFAULT + ) + indices = [i for i, x in enumerate(args) if isinstance(x, TensorBox)] + for i, x in zip(indices, broadcast_tensors(*[args[i] for i in indices])): + args[i] = x + for i in range(len(args)): + if isinstance(args[i], ir.Constant): + args[i] = ExpandView.create(args[i], list(args[indices[0]].get_size())) + return make_pointwise(fn, override_return_dtype=dtype)( + args[0], to_dtype(args[1], dtype), to_dtype(args[2], dtype) + ) + + +@register_lowering(aten.broadcast_tensors, broadcast=False, type_promotion_kind=None) +def broadcast_tensors(*inputs): + if len(inputs) == 1: + if isinstance(inputs[0], (list, tuple)): + return broadcast_tensors(*inputs[0]) + return inputs + target: list[sympy.Expr] = functools.reduce( + broadcast_symbolic_shapes, (x.get_size() for x in inputs), () + ) + outputs = [] + for x in inputs: + if (sizes := tuple(x.get_size())) == target: + pass + + elif len(sizes) != len(target) or any( + V.graph.sizevars.is_size_one_or_false(a) + != V.graph.sizevars.is_size_one_or_false(b) + for a, b in zip(sizes, target) + ): + x = expand(x, target) + outputs.append(x) + return outputs + + +@register_lowering([aten.alias, aten.detach, aten.detach_, aten.lift, prims.view_of]) +def nop(x): + return x # AOT autograd handles this for us + + +if hasattr(aten, "lift_fresh"): + register_lowering(aten.lift_fresh)(nop) + + +@register_lowering(aten.squeeze, type_promotion_kind=None) +def squeeze(x, dim=None): + assert isinstance(x, TensorBox) + if dim is None: + return TensorBox(SqueezeView.create(x.data)) + + dim = ( + V.graph.sizevars.guard_int(dim) + if isinstance(dim, (int, sympy.Expr)) + else tuple(V.graph.sizevars.guard_int(d) for d in dim) + ) + dim = canonicalize_dims(len(x.get_size()), dim) # type: ignore[call-overload] + dims = OrderedSet((dim,) if not isinstance(dim, tuple) else dim) + + new_shape = [] + for d, s in enumerate(x.get_size()): + if not (d in dims and V.graph.sizevars.guard_or_false(sympy.Eq(s, 1))): + new_shape.append(s) + + # squeeze does nothing if the size isn't 1 + return view(x, new_shape) if new_shape != x.get_size() else x + + +@register_lowering(aten.squeeze_copy, type_promotion_kind=None) +def squeeze_copy(x, dim=None): + return clone(squeeze(x, dim)) + + +@register_lowering([aten.squeeze_]) +def squeeze_(x, dim=None): + val = squeeze(x, dim) + assert isinstance(x, TensorBox) + assert isinstance(val, TensorBox) + x.data = val.data + return x + + +@register_lowering(aten.isinf) +def isinf(x): + if is_integer_type(x): + return full_like(x, False, dtype=torch.bool) + fn = ops_wrapper("isinf") + return make_pointwise(fn, override_return_dtype=torch.bool)(x) + + +@register_lowering(aten.isnan) +def isnan(x): + if is_integer_type(x): + return full_like(x, False, dtype=torch.bool) + fn = ops_wrapper("isnan") + return make_pointwise(fn, override_return_dtype=torch.bool)(x) + + +@register_lowering(aten.ceil) +def ceil(x): + if is_integer_type(x): + return clone(x) + fn = ops_wrapper("ceil") + return make_pointwise(fn)(x) + + +@register_lowering(aten.floor) +def floor(x): + if is_integer_type(x): + return clone(x) + fn = ops_wrapper("floor") + return make_pointwise(fn)(x) + + +@register_lowering(aten.round.default) +def round(x): + if is_integer_type(x): + return clone(x) + else: + fn = ops_wrapper("round") + return make_pointwise(fn)(x) + + +@register_lowering(aten.trunc) +def trunc(x): + if is_integer_type(x): + return clone(x) + fn = ops_wrapper("trunc") + return make_pointwise(fn)(x) + + +@register_lowering(aten.expand, type_promotion_kind=None) +def expand(x, sizes): + (x,) = promote_constants([x]) + if isinstance(x, ir.BaseConstant): + return ExpandView.create(x, tuple(sizes)) + assert isinstance(x, TensorBox) + assert isinstance(sizes, (list, tuple)) + if tuple(x.get_size()) == tuple(sizes): + return x + + if not free_unbacked_symbols(x.get_size()): + x_size_product = V.graph.sizevars.guarding_hint_or_throw( + sympy_product(x.get_size()) + ) + # TODO: It would be better to realize the input if any of its sizes + # are unbacked, because typically the size will be non-zero. However, + # this cannot be done directly as below as we'll choke on the size_hint + # here + if x_size_product > 0 and not free_unbacked_symbols(sizes): + # maybe realize input before broadcasting it + x.mark_reuse( + V.graph.sizevars.guarding_hint_or_throw(sympy_product(sizes)) + // x_size_product + ) + return TensorBox(ExpandView.create(x.data, tuple(sizes))) + + +@register_lowering(prims.broadcast_in_dim, type_promotion_kind=None) +def broadcast_in_dim(a, shape, broadcast_dimensions): + s = list(shape) + for broadcast_dimension in broadcast_dimensions: + s[broadcast_dimension] = -1 + + v = a + for idx, x in enumerate(s): + if x != -1: + v = unsqueeze(v, idx) + + return expand(v, shape) + + +@register_lowering(aten.expand_as, type_promotion_kind=None) +def expand_as(x, y): + return expand(x, y.get_size()) + + +@register_lowering(aten.repeat) +def repeat(x, repeats): + old_size = list(x.get_size()) + if len(repeats) > len(old_size): + old_size = [sympy.S.One] * (len(repeats) - len(old_size)) + old_size + x = view(x, list(old_size)) + assert len(repeats) == len(x.get_size()) + + new_size = list(x.get_size()) + + zero_tensor = False + for i in range(len(repeats)): + if repeats[i] == 0: + zero_tensor = True + new_size[i] = new_size[i] * repeats[i] + + if zero_tensor: + return empty(new_size, dtype=x.get_dtype(), device=x.get_device()) + if all((a == 1 or b == 1) for a, b in zip(repeats, old_size)): + return clone(expand(x, new_size)) + + x_loader: Callable[[Any], Any] + + def inner_fn(index): + assert len(index) == len(repeats) + index = list(index) + for i in range(len(repeats)): + if repeats[i] != 1: + if old_size[i] == 1: + index[i] = sympy.S.Zero + else: + index[i] = ModularIndexing(index[i], 1, old_size[i]) + return x_loader(index) + + # TODO Laith is there better check + if not free_unbacked_symbols(old_size) and not free_unbacked_symbols(new_size): + old_size_product = V.graph.sizevars.guarding_hint_or_throw( + sympy_product(old_size) + ) + if old_size_product > 0: + # maybe realize the input but skip for unbacked symints since it'll + # choke on the size hint. + x.mark_reuse( + V.graph.sizevars.guarding_hint_or_throw(sympy_product(new_size)) + // old_size_product + ) + + x_loader = x.make_loader() + return Pointwise.create( + device=x.get_device(), + dtype=x.get_dtype(), + inner_fn=inner_fn, + ranges=list(new_size), + ) + + +@register_lowering(aten._unsafe_view, type_promotion_kind=None) +@register_lowering(aten.view, type_promotion_kind=None) +@register_lowering(aten.reshape, type_promotion_kind=None) +def view(x: TensorBox, sizes: Sequence[sympy.Expr]) -> TensorBox: + return TensorBox(View.create(x.data, sizes)) + + +@register_lowering(aten.permute, type_promotion_kind=None) +def permute(x, dims): + assert isinstance(x, TensorBox) + assert isinstance(dims, (list, tuple)) + return TensorBox(PermuteView.create(x.data, tuple(dims))) + + +# Note: logic in this function need to be always synchronized with +# slice_forward in fake implementation. +@register_lowering(aten.slice, type_promotion_kind=None) +def slice_(x, dim=0, start=0, end=sys.maxsize, step=1, clamp=True): + """ + Lowers a slice call, creating ExternKernels for the output size & storage offset symbols, + if the indices are unbacked and appropriate semantics aren't known. + If they are known (indices are static/backed/unbacked with info), a SliceView is created. + """ + + from torch.fx.experimental.symbolic_shapes import ( + CallMethodKey, + resolve_unbacked_bindings, + ) + + assert isinstance(x, TensorBox) + dim = _validate_dim(x, dim, 0) + size = x.get_size()[dim] + step = sympy.expand(step) + assert isinstance(step, sympy.Expr) or step > 0, step + + # maybe apply slice optimization + try: + if ( + start == 0 + and V.graph.sizevars.statically_known_leq(size, end) + and step == 1 + ): + return x + except TypeError: + pass + + # try to avoid dynamic (unbacked) slice + def compute_slice_index(index, size, default=None): + if index is None: + return default + + fn = lambda x: V.graph.sizevars.guard_or_false(x) # noqa: E731 + index = sympy.expand(index) + size = sympy.expand(size) + if fn(sympy.And(sympy.Ge(index, 0), sympy.Le(index, size))): + return index + elif fn(sympy.And(sympy.Lt(index, 0), sympy.Ge(index, -size))): + return index + size + elif fn(sympy.Gt(index, size)): + return size + elif fn(sympy.Lt(index, -size)): + return 0 + elif fn(sympy.Ge(index, 0)): + # If index >= 0, the resolved index is at most min(index, size). + return sympy.Min(index, size) + elif fn(sympy.Lt(index, 0)): + # If index < 0, wrap and clamp: the resolved index is at least 0. + return sympy.Max(index + size, 0) + return None + + start_index, end_index = None, None + # ambiguous_slice=False means we know what semantics this slice call follows, + # and don't need to generate an extern kernel to represent the output size. + # This is assumed True for clamp=False + # (meant to follow standard indexing semantics: 0 <= index < size) + ambiguous_slice = clamp + if ambiguous_slice: + start_index = compute_slice_index(start, size, 0) + # Special case: if end is maxsize (unbounded), use size directly + # This matches the logic in fake_impls.py + if end is not None and V.graph.sizevars.statically_known_equals( + end, sys.maxsize + ): + end_index = size + else: + end_index = compute_slice_index(end, size, size) + if start_index is not None and end_index is not None: + start, end = start_index, end_index + ambiguous_slice = False + + if not ambiguous_slice: + # Even though the bounds are resolvable now, the FX node may have + # allocated unbacked symbols for the slice output size because dynamo + # couldn't prove the bounds at trace time (constraints may have been + # learned after tracing the slice). We still need to define those + # symbols so the assertion new_unbacked_defs >= renamed_unbacked_bindings + # passes. Register a DynamicSliceSize operation to define the size symbol. + # Note: storage_offset bindings should not appear here because + # a resolved start_index means the offset is computable directly + # (base_offset + start * stride), so dynamo wouldn't allocate an + # unbacked symbol for it. + # Note: current_node may be None when slice_ is called from template + # rendering (e.g. cpp_template_kernel.slice_nd) rather than FX graph + # lowering, so we handle that. + current_node = V.graph.current_node + node_unbacked_bindings = resolve_unbacked_bindings( + V.graph.sizevars.shape_env, + current_node.meta.get("unbacked_bindings", {}) + if current_node is not None + else {}, + ) + if node_unbacked_bindings: + for sym, keypath in node_unbacked_bindings.items(): + if keypath == (CallMethodKey("size"), pytree.SequenceKey(dim)): + b_size = ir.DynamicSliceSize(sym, start, end, step, size) + b_size.name = V.graph.register_buffer(b_size) + V.graph.register_operation(b_size) + elif keypath == (CallMethodKey("storage_offset"),): + # Not handled yet — would require materializing the + # tensor layout. Unlikely to be hit because a resolved + # start_index means the offset is computable directly. + raise AssertionError( + "Unexpected storage_offset unbacked binding when both " + "start and end indices are resolved" + ) + + return TensorBox( + ir.SliceView.create(x.data, dim, start, end, step, clamp=clamp) + ) # go to SliceView/ReinterpretView + + # unbacked territory: create DynamicSlice ExternKernel + # clamp is True, unbacked start / end + assert clamp + unbacked_bindings = resolve_unbacked_bindings( + V.graph.sizevars.shape_env, V.graph.current_node.meta["unbacked_bindings"] + ) + assert unbacked_bindings is not None + assert len(unbacked_bindings) <= 2, unbacked_bindings + sym_size, sym_storage = None, None + for sym, keypath in unbacked_bindings.items(): + if keypath == (CallMethodKey("size"), pytree.SequenceKey(dim)): + sym_size = sym + elif keypath == (CallMethodKey("storage_offset"),): + sym_storage = sym + + assert start_index is None or end_index is None + b_size = ir.DynamicSliceSize( + sym_size, + start, + end, + step, + x.get_size()[dim], + ) + b_size.name = V.graph.register_buffer(b_size) + V.graph.register_operation(b_size) + new_size = sym_size + + if x.maybe_get_layout() is None: + # realize tensor before accessing layout + x.realize() + + if start_index is not None: + # we shouldn't have allocated storage offset symbol if start index was determinable + assert sym_storage is None + new_storage_offset = x.get_layout().offset + start_index * x.get_stride()[dim] + else: + b_storage = ir.DynamicSelectStorageOffset( + sym_storage, + start, + x.get_layout().offset, + x.get_stride()[dim], + x.get_size()[dim], + clamp=True, + ) + b_storage.name = V.graph.register_buffer(b_storage) + V.graph.register_operation(b_storage) + new_storage_offset = sym_storage + + new_sizes = list(x.get_size()) + new_strides = list(x.get_stride()) + new_sizes[dim] = new_size + new_strides[dim] *= step + return as_strided(x, new_sizes, new_strides, new_storage_offset) + + +@register_lowering(aten.as_strided, type_promotion_kind=None) +def as_strided(x, size, stride, storage_offset=None): + new_device = None + new_dtype = None + if isinstance(x, TensorBox) and isinstance(x.data, ir.BaseView): + # Note: Merging views + # When we use as_strided, we can rewrite the size/stride/offset + # of the incoming buffer x. If x is a view, we would overwrite + # its metadata. Except for dtype, which we need to propagate. + + # Technically device is not needed because it is not possible + # to have a cross-device view today. + new_device = x.get_device() + new_dtype = x.dtype + x = x.data.unwrap_view() + x.realize() + if not ir.is_storage_and_layout(x): + raise NotImplementedError(f"unrealized as_strided({x}, ...)") + storage, old_layout = ir.as_storage_and_layout(x) + new_layout = ir.FixedLayout( + new_device if new_device else old_layout.device, + new_dtype if new_dtype else old_layout.dtype, + [sympy.expand(s) for s in size], + [sympy.expand(s) for s in stride], + sympy.expand(storage_offset or 0), + ) + return TensorBox(ir.ReinterpretView(data=storage, layout=new_layout)) + + +@register_lowering(aten.as_strided_, type_promotion_kind=None) +def as_strided_(x, size, stride, storage_offset=None): + assert isinstance(x, TensorBox) + x.data = as_strided(x, size, stride, storage_offset).data + return x + + +@register_lowering(aten.as_strided_copy, type_promotion_kind=None) +def as_strided_copy(x, size, stride, storage_offset=None): + result = as_strided(x, size, stride, storage_offset) + return clone(result) + + +def pointwise_cat(inputs, dim=0): + # (inclusive, exclusive) + inputs_ranges: list[tuple[sympy.Expr, sympy.Expr]] = [] + prev_end = 0 + for inp in inputs: + inputs_ranges.append((prev_end, prev_end + inp.get_size()[dim])) # type: ignore[arg-type] + prev_end = inputs_ranges[-1][-1] # type: ignore[assignment] + + inputs_loaders = [inp.make_loader() for inp in inputs] + + def inner_fn(idx): + idx_dim = ops.index_expr(idx[dim], torch.int64) + + masks = [] + masked_loads = [] + for i in range(len(inputs)): + start = ( + ops.constant(0, torch.int64) + if i == 0 + else ops.index_expr(inputs_ranges[i][0], torch.int64) + ) + end = ops.index_expr(inputs_ranges[i][1], torch.int64) + + start_cond = ops.ge(idx_dim, start) + end_cond = ops.lt(idx_dim, end) + if i == 0: + mask = end_cond + elif i == len(inputs) - 1: + mask = start_cond + else: + mask = ops.and_(start_cond, end_cond) + + masks.append(mask) + idx_load = list(idx) + + # if we're concatting [4], [2] + # when we index the second tensor for 5 we want to index 5 - 4 + # Use Identity to prevent expansion of index * stride to keep expression + # in same int bitwidth as shape + idx_load[dim] = Identity(idx_load[dim] - inputs_ranges[i][0]) + + masked_loads.append( + ops.masked( + mask, + lambda: inputs_loaders[i](idx_load), + 0.0, # this value should be unused + ), + ) + + next_val = masked_loads[-1] + for i in range((len(inputs)) - 2, -1, -1): + next_val = ops.where( + masks[i], + masked_loads[i], + next_val, + ) + return next_val + + new_size = list(inputs[0].get_size()) + new_size[dim] = inputs_ranges[-1][-1] + + return Pointwise.create( + device=inputs[0].get_device(), + dtype=inputs[0].get_dtype(), + inner_fn=inner_fn, + ranges=new_size, + ) + + +@register_lowering(quantized_decomposed.quantize_per_channel, type_promotion_kind=None) +def quantized_decomposed_quantize_per_channel( + input: TensorBox, + scales: TensorBox, + zero_points: TensorBox, + axis: int, + quant_min: int, + quant_max: int, + dtype: torch.dtype, +) -> TensorBox: + assert len(scales.get_size()) == 1, "expect scales 1 dim" + assert len(zero_points.get_size()) == 1, "expect zero_points 1 dim" + + if input.get_dtype() == torch.bfloat16: + input = to_dtype(input, torch.float32) + assert input.get_dtype() == torch.float32, ( + f"Expecting input to have dtype torch.float32, but got dtype: {input.get_dtype()}" + ) + assert axis < len(input.get_size()), ( + f"Expecting axis to be < {len(input.get_size())}" + ) + + input_loader = input.make_loader() + scales_loader = scales.make_loader() + zero_points_loader = zero_points.make_loader() + + def inner_fn(idx): + channel_idx = (idx[axis],) + + input = input_loader(idx) + scale = scales_loader(channel_idx) + zero_point = zero_points_loader(channel_idx) + qmin, qmax = _create_constants(quant_min, quant_max, dtype=torch.float32) + + if scales.dtype != torch.float32: + scale = ops.to_dtype(scale, torch.float32) + if zero_points.dtype != torch.int32: + zero_point = ops.to_dtype(zero_point, torch.int32) + inv_scale = ops.reciprocal(scale) + val = ops.round(input * inv_scale) + zero_point + clamped = ops.maximum(qmin, ops.minimum(qmax, val)) + return ops.to_dtype(clamped, dtype) + + return Pointwise.create( + device=input.get_device(), + dtype=dtype, + inner_fn=inner_fn, + ranges=input.get_size(), + ) + + +def _assert_async(cond, msg): + cond.realize() + cond = to_dtype(cond, torch.bool) + + def inner_fn(index): + with ir.ComputedBuffer.force_realize(): + return ops.device_assert_async(cond.make_loader()(index), msg) + + assertion_op = Pointwise.create( + device=cond.get_device(), + dtype=cond.get_dtype(), + inner_fn=inner_fn, + ranges=list(cond.get_size()), + ) + assertion_op.realize() + return assertion_op + + +@register_lowering(aten._assert_async.msg) +def lower_assert_async(cond, msg): + return _assert_async(cond, msg) + + +@register_lowering(aten._functional_assert_async.msg) +def lower_assert_functional_async(cond, msg): + return _assert_async(cond, msg) + + +@register_lowering( + quantized_decomposed.dequantize_per_channel, type_promotion_kind=None +) +def quantized_decomposed_dequantize_per_channel( + input: TensorBox, + scales: TensorBox, + zero_points: TensorBox, + axis: int, + quant_min: int, + quant_max: int, + dtype: torch.dtype, + *, + out_dtype: torch.dtype | None = None, +) -> TensorBox: + assert len(scales.get_size()) == 1, "expect scales 1 dim" + assert len(zero_points.get_size()) == 1, "expect zero_points 1 dim" + assert input.get_dtype() == dtype, ( + f"Expecting input to have dtype {dtype}, but got dtype: {input.get_dtype()}" + ) + assert axis < len(input.get_size()), ( + f"Expecting axis to be < {len(input.get_size())}" + ) + + if out_dtype is None: + out_dtype = torch.float32 + + input_loader = input.make_loader() + scales_loader = scales.make_loader() + zero_points_loader = zero_points.make_loader() + + def inner_fn(idx): + channel_idx = (idx[axis],) + + input = input_loader(idx) + scale = scales_loader(channel_idx) + zero_point = zero_points_loader(channel_idx) + + if scales.dtype != torch.float32: + scale = ops.to_dtype(scale, torch.float32) + if zero_points.dtype != torch.float32: + zero_point = ops.to_dtype(zero_point, torch.float32) + val = ops.sub(ops.to_dtype(input, torch.float32), zero_point) * scale + val = ops.to_dtype(val, out_dtype) + return val + + return Pointwise.create( + device=input.get_device(), + dtype=out_dtype, + inner_fn=inner_fn, + ranges=input.get_size(), + ) + + +@register_lowering( + quantized_decomposed.quantize_per_tensor.default, type_promotion_kind=None +) +def quantized_decomposed_quantize_per_tensor_default( + input: TensorBox, + scale: float, + zero_point: int, + quant_min: int, + quant_max: int, + dtype: torch.dtype, +) -> TensorBox: + if input.get_dtype() == torch.bfloat16: + input = to_dtype(input, torch.float32) + assert input.get_dtype() == torch.float32, ( + f"Expecting input to have dtype torch.float32, but got dtype: {input.get_dtype()}" + ) + + input_loader = input.make_loader() + + def inner_fn(idx, scale, zero_point): + input = input_loader(idx) + inv_scale, zero_point = _create_constants( + 1.0 / scale, zero_point, dtype=torch.float32 + ) + val = ops.round(input * inv_scale) + zero_point + qmin, qmax = _create_constants(quant_min, quant_max, dtype=torch.float32) + clamped = ops.minimum(ops.maximum(val, qmin), qmax) + return ops.to_dtype(clamped, dtype) + + return Pointwise.create( + device=input.get_device(), + dtype=dtype, + inner_fn=functools.partial( + inner_fn, scale=float(scale), zero_point=int(zero_point) + ), + ranges=input.get_size(), + ) + + +@register_lowering( + quantized_decomposed.dequantize_per_tensor.default, type_promotion_kind=None +) +def quantized_decomposed_dequantize_per_tensor_default( + input: TensorBox, + scale: float, + zero_point: int, + quant_min: int, + quant_max: int, + dtype: torch.dtype, + *, + out_dtype: torch.dtype | None = None, +) -> TensorBox: + assert input.get_dtype() == dtype, ( + f"Expecting input to have dtype {dtype}, but got dtype: {input.get_dtype()}" + ) + + if out_dtype is None: + out_dtype = torch.float32 + + input_loader = input.make_loader() + + def inner_fn(idx, scale, zero_point): + input = input_loader(idx) + scale, zero_point = _create_constants(scale, zero_point, dtype=torch.float32) + val = ops.sub(ops.to_dtype(input, torch.float32), zero_point) * scale + val = ops.to_dtype(val, out_dtype) + return val + + return Pointwise.create( + device=input.get_device(), + dtype=out_dtype, + inner_fn=functools.partial( + inner_fn, scale=float(scale), zero_point=int(zero_point) + ), + ranges=input.get_size(), + ) + + +@register_lowering( + quantized_decomposed.quantize_per_tensor.tensor, type_promotion_kind=None +) +def quantized_decomposed_quantize_per_tensor_tensor( + input: TensorBox, + scale: TensorBox, + zero_point: TensorBox, + quant_min: int, + quant_max: int, + dtype: torch.dtype, +) -> TensorBox: + if input.get_dtype() == torch.bfloat16: + input = to_dtype(input, torch.float32) + assert input.get_dtype() == torch.float32, ( + f"Expecting input to have dtype torch.float32, but got dtype: {input.get_dtype()}" + ) + assert len(scale.get_size()) == 0 or ( + len(scale.get_size()) == 1 and scale.get_size()[0] == 1 + ), "expect scale as scalar tensor" + assert len(zero_point.get_size()) == 0 or ( + len(zero_point.get_size()) == 1 and zero_point.get_size()[0] == 1 + ), "expect zero_point as scalar tensor" + + input_loader = input.make_loader() + scale_loader = scale.make_loader() + zero_point_loader = zero_point.make_loader() + + device = input.get_device() + + def inner_fn(idx): + input = input_loader(idx) + _scale = scale_loader((0,) if len(scale.get_size()) == 1 else ()) + _zero_point = zero_point_loader((0,) if len(scale.get_size()) == 1 else ()) + if scale.dtype != torch.float32: + _scale = ops.to_dtype(_scale, torch.float32) + if zero_point.dtype != torch.float32: + _zero_point = ops.to_dtype(_zero_point, torch.float32) + if device and device.type == "cpu": + val = ops.fma(input, ops.reciprocal(_scale), _zero_point) + return ops.round_to_int(val, dtype) + val = ops.round(input * ops.reciprocal(_scale)) + _zero_point + qmin, qmax = _create_constants(quant_min, quant_max, dtype=torch.float32) + clamped = ops.minimum(ops.maximum(val, qmin), qmax) + return ops.to_dtype(clamped, dtype) + + return Pointwise.create( + device=input.get_device(), + dtype=dtype, + inner_fn=inner_fn, + ranges=input.get_size(), + ) + + +@register_lowering( + quantized_decomposed.dequantize_per_tensor.tensor, type_promotion_kind=None +) +def quantized_decomposed_dequantize_per_tensor_tensor( + input: TensorBox, + scale: TensorBox, + zero_point: TensorBox, + quant_min: int, + quant_max: int, + dtype: torch.dtype, + *, + out_dtype: torch.dtype | None = None, +) -> TensorBox: + assert len(scale.get_size()) == 0 or ( + len(scale.get_size()) == 1 and scale.get_size()[0] == 1 + ), "expect scale as scalar tensor" + assert len(zero_point.get_size()) == 0 or ( + len(zero_point.get_size()) == 1 and zero_point.get_size()[0] == 1 + ), "expect zero_point as scalar tensor" + assert input.get_dtype() == dtype, ( + f"Expecting input to have dtype {dtype}, but got dtype: {input.get_dtype()}" + ) + + if out_dtype is None: + out_dtype = torch.float32 + + input_loader = input.make_loader() + scale_loader = scale.make_loader() + zero_point_loader = zero_point.make_loader() + + def inner_fn(idx): + input = input_loader(idx) + _scale = scale_loader((0,) if len(scale.get_size()) == 1 else ()) + _zero_point = zero_point_loader((0,) if len(scale.get_size()) == 1 else ()) + if scale.dtype != torch.float32: + _scale = ops.to_dtype(_scale, torch.float32) + if zero_point.dtype != torch.float32: + _zero_point = ops.to_dtype(_zero_point, torch.float32) + val = ops.sub(ops.to_dtype(input, torch.float32), _zero_point) * _scale + val = ops.to_dtype(val, out_dtype) + return val + + return Pointwise.create( + device=input.get_device(), + dtype=out_dtype, + inner_fn=inner_fn, + ranges=input.get_size(), + ) + + +@register_lowering(aten.cat) +def cat(inputs, dim=0): + """Lower aten.cat, choosing between pointwise_cat and ConcatKernel.""" + cpu_device = inputs[0].get_device().type == "cpu" + if cpu_device and all( + input.get_dtype() in [torch.int8, torch.uint8] for input in inputs + ): + # TODO Remove this fallback when we support vectorization + # code gen with uint8 data type directly. + for input in inputs: + input.realize() + if all(len(input.get_size()) == 4 for input in inputs): + inputs, _ = require_channels_last(aten.cat, *inputs) + return fallback_handler(aten.cat.default)(inputs, dim) + + if len(inputs) == 1: + return clone(inputs[0]) + + dim = _validate_dim(inputs[0], dim, 0) + dtype = get_promoted_dtype( + *inputs, type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.DEFAULT + ) + inputs = [to_dtype(inp, dtype) for inp in inputs] + + def unwrap_tensor(x: TensorBox | ir.StorageBox) -> ir.IRNode: + if isinstance(x, TensorBox): + if isinstance(x.data, ir.BaseView): + return x.data.unwrap_view() + else: + return x.data + + if isinstance(x, ir.StorageBox): + return x.data + + return x + + def is_reduction(t): + return isinstance(t, ir.ComputedBuffer) and isinstance(t.data, ir.Reduction) + + def can_fuse_reduction(t): + if isinstance(t, (TensorBox, ir.StorageBox)): + return can_fuse_reduction(unwrap_tensor(t)) + return ( + is_reduction(t) + or isinstance(t, ir.Pointwise) + and any( + can_fuse_reduction(V.graph.get_buffer(read)) + for read in t.get_read_names() + ) + ) + + # fusing reducutions into computed concat buffer can cause regressions. + fusable_reduction = any(can_fuse_reduction(t) for t in inputs) + + def should_lower_cat_input(x) -> bool: + # Unrealized inputs will not be storage and layouts, and we dont want to realize + # them in case we want to fuse + if ir.is_storage_and_layout(x): + storage, _ = ir.as_storage_and_layout(x, freeze=False) + return not ir.ConcatKernel.can_realize_into_without_copy(storage) + + if isinstance(x, (TensorBox, ir.StorageBox)): + return should_lower_cat_input(unwrap_tensor(x)) + + if isinstance(x, ir.Pointwise): + return True + + return False + + if config.force_pointwise_cat: + return pointwise_cat(inputs, dim) + + # TODO: We observed negative performance impact of pointwise_cat optimization on CPU so disabled it. + # We will revisit this later after enabling vectorization on index_expr. + if cpu_device: + return TensorBox(ir.ConcatKernel.create(inputs, dim)) + + def op_count(x): + if isinstance(x, (TensorBox, ir.StorageBox)): + return op_count(unwrap_tensor(x)) + + # this will correspond to a direct memory read + if not isinstance(x, ir.Pointwise): + return 0 + + count = x.inner_fn_opcount().num_ops + for read in x.get_read_names(): + count += op_count(V.graph.get_buffer(read)) + + return count + + # as of inputs increase, possibility for register spilling also increases + # past a certain threshold of inputs we only fuse if the if the input kernels + # are simple + # not sure if we want to expose to users via config since logic may change in future + MAX_COMPLEX_POINTWISE_CAT = 8 + MAX_SIMPLE_OP_COUNT = 2 + + def additional_pointwise_ops(op: torch._ops.OpOverload): + return op in (aten.cat.default, aten.constant_pad_nd.default) + + if len(inputs) <= MAX_COMPLEX_POINTWISE_CAT or ( + (len(inputs) <= config.max_pointwise_cat_inputs) + and all(op_count(t) <= MAX_SIMPLE_OP_COUNT for t in inputs) + ): + pointwise_uses = all( + is_pointwise_use(use, additional_pointwise_ops) + for use in V.current_node.users + ) + # fuse in case we will be used in a pointwise node, and there are any inputs we + # we can prevent materialization of. + fuse_pointwise_use = ( + any(should_lower_cat_input(inp) for inp in inputs) and pointwise_uses + ) + + # horizontal fuse in case all inputs will require a copy kernel anyway. + # only horizontally fuse pointwise kernels + + # Skip pointwise_cat when any cat input has a fusible (pointwise) + # multi-consumer — ConcatKernel + NonOwningLayout avoids redundant + # reads. Also skip when input is an unrealized Pointwise with + # multiple consumers to avoid recomputation (e.g. pad-as-cat). + def any_input_has_multi_consumers() -> bool: + current_node = V.current_node + if current_node is None: + return False + fx_args = current_node.args[0] + if isinstance(fx_args, (list, tuple)): + input_nodes = fx_args + elif isinstance(fx_args, torch.fx.Node): + input_nodes = [fx_args] + else: + return False + + def is_unrealized_pointwise(x): + if isinstance(x, (TensorBox, ir.StorageBox)): + return is_unrealized_pointwise(unwrap_tensor(x)) + return isinstance(x, ir.Pointwise) + + for arg, ir_input in zip(input_nodes, inputs): + if not hasattr(arg, "users") or len(arg.users) <= 1: + continue + # input will be computed multiple times because other consumers + # (eg. pointwise) will also inline it. So we should realize-in-place via ConcatKernel + if any(is_pointwise_use(u) for u in arg.users if u is not current_node): + return True + # If input is an unrealized Pointwise with multiple consumers, pointwise_cat + # will inline input without realizing it to memory, causing separate + # realization cost for input. So we should realize-in-place via ConcatKernel + if is_unrealized_pointwise(ir_input): + return True + return False + + has_multi_consumers = any_input_has_multi_consumers() + + horizontal_fuse_cat = all( + should_lower_cat_input(inp) for inp in inputs + ) and not any(can_fuse_reduction(t) for t in inputs) + if not has_multi_consumers and ( + fuse_pointwise_use or (horizontal_fuse_cat and not fusable_reduction) + ): + return pointwise_cat(inputs, dim) + + return TensorBox(ir.ConcatKernel.create(inputs, dim)) + + +@register_lowering(aten.diagonal, type_promotion_kind=None) +def diagonal(input, offset: int = 0, dim1: int = 0, dim2: int = 1): + original_shape = input.get_size() + num_dims = len(original_shape) + dim1 = canonicalize_dim(idx=dim1, rank=num_dims) + dim2 = canonicalize_dim(idx=dim2, rank=num_dims) + + check( + dim1 != dim2, lambda: f"diagonal dimensions cannot be identical {dim1}, {dim2}" + ) + + offset_negative = V.graph.sizevars.evaluate_expr(sympy.Lt(offset, 0)) + if offset_negative: + diag_size = V.graph.sizevars.evaluate_max( + V.graph.sizevars.evaluate_min( + original_shape[dim1] + offset, + original_shape[dim2], + ), + 0, # type: ignore[arg-type] + ) + else: + diag_size = V.graph.sizevars.evaluate_max( + V.graph.sizevars.evaluate_min( + original_shape[dim1], + original_shape[dim2] - offset, + ), + 0, # type: ignore[arg-type] + ) + + base_idx = (0, 0) + if offset_negative: + base_idx = (-offset, 0) + else: + base_idx = (0, offset) + + sizes = [s for i, s in enumerate(original_shape) if i not in (dim1, dim2)] + sizes.append(diag_size) + + def reindexer(idx): + diag_idx = idx[-1] + original_idx = [0] * len(original_shape) + cur_dim = 0 + for d in range(num_dims): + if d == dim1: + original_idx[d] = diag_idx + base_idx[0] + elif d == dim2: + original_idx[d] = diag_idx + base_idx[1] + else: + original_idx[d] = idx[cur_dim] + cur_dim += 1 + + assert cur_dim == len(original_shape) - 2 + return original_idx + + return TensorBox(ir.GenericView.create(input, sizes, reindexer)) + + +@register_lowering(aten.diagonal_copy, type_promotion_kind=None) +def diagonal_copy(input, offset: int = 0, dim1: int = 0, dim2: int = 1): + return clone(diagonal(input, offset, dim1, dim2)) + + +@register_lowering(aten.diagonal_scatter, type_promotion_kind=None) +def diagonal_scatter(input, src, offset: int = 0, dim1: int = 0, dim2: int = 1): + output = clone(input) + target = diagonal(output, offset, dim1, dim2) + mutate_to(target, src) + return output + + +@register_lowering(aten.select, type_promotion_kind=None) +def select(x, dim, idx): + idx = sympy.expand(idx) + size = sympy.expand(x.get_size()[dim]) + actual_index = None + + if V.graph.sizevars.guard_or_false(sympy.Lt(idx, 0)): + actual_index = idx + size + elif V.graph.sizevars.guard_or_false(sympy.Ge(idx, 0)): + actual_index = idx + + if actual_index is not None: + if has_free_unbacked_symbols(idx): + # Inductor could generate incorrect views for tensors with unbacked symbols here; + # Squeeze operations are translated to views, resulting in incorrect strides. + # Additionally, we want to avoid accidental unbacked unsqueeze semantics. To resolve this, + # we use as_strided instead. + # Removing this branch will cause test_unbacked_select_index_with_check to fail. + + # before accessing size, stride, and offset we need to realize. + x.realize() + new_size = x.get_size() + new_stride = x.get_stride() + new_storage_offset = x.get_layout().offset + new_stride[dim] * actual_index + + del new_size[dim] + del new_stride[dim] + return as_strided(x, new_size, new_stride, new_storage_offset) + else: + # no need to clamp, this function handles negative indexing itself + slice_result = slice_(x, dim, actual_index, actual_index + 1, clamp=False) + return squeeze(slice_result, dim) + + # Unbacked Semantics: + # When the index idx is unbacked (e.g., u0), we compute the index dynamically + # during the lowering of the select operation using DynamicSelectStorageOffset. + + unbacked_bindings = resolve_unbacked_bindings( + V.graph.sizevars.shape_env, V.graph.current_node.meta["unbacked_bindings"] + ) + assert unbacked_bindings is not None + assert len(unbacked_bindings) == 1, unbacked_bindings + unbacked_offset_sym, _ = next(iter(unbacked_bindings.items())) + + # before accessing size, stride, and offset we need to realize. + x.realize() + new_size = x.get_size() + new_stride = x.get_stride() + new_storage_offset = unbacked_offset_sym + buffer = ir.DynamicSelectStorageOffset( + unbacked_offset_sym, + idx, + x.get_layout().offset, + new_stride[dim], + x.get_size()[dim], + clamp=False, + ) + buffer.name = V.graph.register_buffer(buffer) + V.graph.register_operation(buffer) + + del new_size[dim] + del new_stride[dim] + return as_strided(x, new_size, new_stride, new_storage_offset) + + +@register_lowering(aten.split, type_promotion_kind=None) +def split(x, sizes, dim=0): + dim = _validate_dim(x, dim, 0) + sizes_ = sizes + + # If sizes is an integer (or a SymInt), we turn it into a list of sizes + # by computing what the actual size of each chunk should be. + if not isinstance(sizes, (list, tuple)): + x_size = x.get_size()[dim] + chunks = V.graph.sizevars.guard_int(FloorDiv(x_size + sizes - 1, sizes)) + sizes_ = [sizes] * chunks + # The last chunk might have a smaller size than the rest. + sizes_[-1] = x_size - (chunks - 1) * sizes + + # From this point, we assume that the sum of the sizes of all chunks + # equals the size of the base tensor. + result = [] + start = 0 + for size in sizes_: + end = start + size + # No need for clamping here, since we compute the exact + # start and end values. + result.append(slice_(x, dim, start, end, clamp=False)) + start = end + return result + + +@register_lowering(aten.split_with_sizes, type_promotion_kind=None) +def split_with_sizes(x, sizes, dim=0): + return split(x, sizes, dim) + + +@register_lowering(aten.unbind, type_promotion_kind=None) +def unbind(x, dim=0): + dim = _validate_dim(x, dim, 0) + x_size = V.graph.sizevars.guard_int(x.get_size()[dim]) + result = [select(x, dim, i) for i in range(x_size)] + return result + + +@register_lowering(aten.unfold, type_promotion_kind=None) +def unfold(x, dimension, size, step): + sizes = x.get_size() + ndim = len(sizes) + dim = canonicalize_dim(ndim, dimension) + + if ndim == 0: + return slice_(unsqueeze(x, 0), end=size, clamp=False) + + dim_size = sizes[dim] + sizevars = V.graph.sizevars + sizevars.check_leq(size, dim_size) + sizevars.check_lt(0, step) # type: ignore[arg-type] + + new_dim_size = FloorDiv(dim_size - size, step) + 1 + if sizevars.guarding_hint_or_throw(dim_size) > 0: + x.mark_reuse( + sizevars.guarding_hint_or_throw(CeilDiv(new_dim_size * size, dim_size)) + ) + + out_size = [*sizes[:dim], new_dim_size, *sizes[dim + 1 :], size] + + def reindexer(idx): + dim_idx = idx[-1] + idx[dim] * step + return (*idx[:dim], dim_idx, *idx[dim + 1 : -1]) + + return TensorBox(ir.GenericView.create(x, out_size, reindexer)) + + +@register_lowering(aten.unsqueeze, type_promotion_kind=None) +def unsqueeze(x, dim): + dim = _validate_dim(x, dim, 1) + new_shape = list(x.get_size()) + new_shape.insert(dim, sympy.S.One) + return view(x, new_shape) + + +@register_lowering(aten.unsqueeze_, type_promotion_kind=None) +def unsqueeze_(x, dim): + val = unsqueeze(x, dim) + assert isinstance(x, TensorBox) + assert isinstance(val, TensorBox) + x.data = val.data + return x + + +def _validate_dim(x, dim, offset=0): + dim = V.graph.sizevars.shape_env.evaluate_expr(sympy.sympify(dim)) + ndim = len(x.get_size()) + if dim < 0: + dim += ndim + offset + assert 0 <= dim < ndim + offset + return dim + + +@register_lowering(aten.glu) +def glu(x, dim=-1): + dim = _validate_dim(x, dim, 0) + # TODO: don't guard on static shape here + new_len = V.graph.sizevars.guard_int(x.get_size()[dim]) // 2 + # no need to clamp, index is int based on input size + a = slice_(x, dim, 0, new_len, clamp=False) + b = slice_(x, dim, new_len, new_len * 2, clamp=False) + return mul(a, sigmoid(b)) + + +def fallback_handler(kernel, add_to_fallback_set=True): + if add_to_fallback_set: + fallbacks.add(kernel) + + def handler(*args, **kwargs): + def wrap_tensors(x): + return x.wrap_for_lowering() if isinstance(x, ir.IRNode) else x + + return pytree.tree_map( + wrap_tensors, ir.FallbackKernel.create(kernel, *args, **kwargs) + ) + + # This lets us detect that a lowering is a fallback handler. + handler._is_fallback_handler = True # type: ignore[attr-defined] + + return handler + + +@functools.cache +def _warn_complex_not_supported(): + warnings.warn( + "Torchinductor does not support code generation for complex operators. Performance may be worse than eager." + ) + + +# There are some types (CPU) which we accept as input but not as +# output. +def unsupported_input_tensor(t: torch.Tensor, node=None): + "Do not support reading or writing to this tensor" + if t.is_complex(): + # Complex views are supported with IR ComplexView + _warn_complex_not_supported() + return True + + if t.is_meta: + return True + + if t.is_sparse: + return True + + if t.dtype == torch.float8_e8m0fnu: + if not node: + return True + + # allow bitcast, views, memory movement, but not arithmetic + # TODO: delete once triton adds native support + return not ( + isinstance(node.target, torch._ops.OpOverload) + and node.target + in ( + aten.view.dtype, + aten.cat.default, + aten.clone.default, + aten._scaled_mm.default, + ) + or (isinstance(node.target, torch._ops.OpOverload) and is_view(node.target)) + ) + + return False + + +def unsupported_output_tensor(t: torch.Tensor, node=None): + "Do not support writing tensor but can read from it" + supported_complex_views = ( + aten.view.dtype, + torch.ops.prims.convert_element_type.default, + ) + if node is not None and node.target in supported_complex_views and t.is_complex(): + return False + if unsupported_input_tensor(t, node): + return True + return t.is_cpu and config.disable_cpp_codegen + + +def fallback_node_due_to_unsupported_type(node: torch.fx.Node, allow_cpu_inputs=True): + # Custom fallback lowering + if node.target is aten.view_as_complex.default: + return False + + if node.op == "placeholder": + return False + + # We should be able to remove this special case once `disable_cpp_codegen` is killed. + if node.target is aten.lift_fresh_copy.default: + return False + + def check_skip_condition(inp_out_node, is_output): + if not isinstance(inp_out_node, torch.fx.Node): + return False + + if "val" not in inp_out_node.meta: + return False + + for meta in pytree.tree_leaves(inp_out_node.meta["val"]): + if not isinstance(meta, torch._subclasses.FakeTensor): + continue + + if is_output: + if unsupported_output_tensor(meta, node): + return True + else: + if unsupported_input_tensor(meta, node): + return True + + return False + + # only skip codegen if there is a cpu output, not input + for arg in pytree.arg_tree_leaves(*node.args, **node.kwargs): + if check_skip_condition(arg, is_output=False): + return True + + return check_skip_condition(node, is_output=True) + + +def make_fallback( + op, + layout_constraint=None, + warn=True, + override_decomp=False, + get_decomp_fn=None, +): + check_decomps = get_decomp_fn() if get_decomp_fn is not None else decompositions + assert op not in check_decomps or override_decomp, ( + f"both a fallback and a decomp for same op: {op}" + ) + if ( + warn + and bool(os.getenv("CI")) + and get_decompositions([op]) + # if fallback_random, we allow not decomposing random + and not ( + config.fallback_random + and op in torch._decomp.decompositions_for_rng.extra_random_decomps + ) + and not override_decomp + ): + # Note: 'warn' is holdover from when this was a warning, but for ops that previously + # set warn=False we do not want a CI error. + # Ignore the 'suppress errors' configs in CI, as this particular warning happens on startup anyway and is not + # likely to be triggered preferentially on one CI config over another. + if torch._dynamo.config.suppress_errors: + torch._dynamo.config.suppress_errors = False + log.warning( + "A make_fallback error occurred in suppress_errors config," + " and suppress_errors is being disabled to surface it." + ) + raise AssertionError( + f"make_fallback({op}): a decomposition exists, we should switch to it." + " To fix this error, either add a decomposition to core_aten_decompositions (preferred)" + " or inductor_decompositions, and delete the corresponding `make_fallback` line." + " Get help from the inductor team if unsure, don't pick arbitrarily to unblock yourself.", + ) + + def register_fallback(op_overload): + add_needs_realized_inputs(op_overload) + if layout_constraint is not None: + add_layout_constraint(op_overload, layout_constraint) + return register_lowering(op_overload, type_promotion_kind=None)( + fallback_handler(op_overload) + ) + + if isinstance(op, torch._ops.OpOverloadPacket): + for ol in op.overloads(): + op_overload = getattr(op, ol) + register_fallback(op_overload) + elif isinstance(op, (torch._ops.OpOverload, torch._ops.HigherOrderOperator)): + register_fallback(op) + else: + raise RuntimeError(f"Unsupported fallback {op} with type {type(op)}") + + +def philox_rand_offset(shape): + """ + TorchInductor offset calculation differs from PyTorch eager offset + calculation for random ops (tl.rand vs torch.rand). In future, we should + strive for same impl for tl.rand and torch.rand. + """ + numel = 1 + for s in shape: + numel = numel * s + return tensor(numel, dtype=torch.int64) + + +@register_lowering(torch.ops.rngprims.philox_rand, type_promotion_kind=None) +def philox_rand(size, seed, offset, stride, device, dtype): + # stride arg is optional and will be used in future for distributed random + # ops. Currently, its unused. + random_pos = ir.FixedLayout( + device, + dtype, + size, + ir.FlexibleLayout.contiguous_strides(size), + ).make_indexer() + seed_loader = seed.make_loader() + offset_loader = offset.make_loader() + + def inner_fn(index): + # Both seed and offset in the philox_rand op are tensors. + # torch seed and offsets are of type int64, but tl.rand accepts int32 + seed_index_expr = ops.to_dtype(seed_loader([]), torch.int32) + offset_index_expr = ops.to_dtype(offset_loader([]), torch.int32) + # Get the offset'd position + rand_index_expr = ops.add( + ops.index_expr(random_pos(index), torch.int32), offset_index_expr + ) + result = ops.rand( + seed_index_expr, + rand_index_expr, + ) + return ops.to_dtype(result, dtype) + + random_values_node = Pointwise.create( + device=device, + dtype=dtype, + inner_fn=inner_fn, + ranges=list(size), + ) + + offset_node = philox_rand_offset(size) + return random_values_node, offset_node + + +@register_lowering(aten.native_dropout, type_promotion_kind=None) +def native_dropout(x, p, train): + if config.fallback_random: + return pytree.tree_map( + TensorBox.create, + ir.FallbackKernel.create(aten.native_dropout.default, x, p, train), + ) + else: + raise AssertionError("should be handled in replace_random.py") + + +@register_lowering(aten.bernoulli_, type_promotion_kind=None) +def bernoulli_(x, *args): + assert config.fallback_random or x.get_device() == torch.device("cpu"), ( + "this should be handled in decomps unless config.fallback_random or the device is CPU" + ) + x.realize() + op_overload = ( + aten.bernoulli_.float + if len(args) == 0 or isinstance(args[0], float) + else aten.bernoulli_.Tensor + ) + ir.InplaceBernoulliFallback(op_overload, x, *args) + return x + + +@register_lowering(aten.bernoulli.p, type_promotion_kind=None) +def bernoulli_p(x, *args): + assert config.fallback_random or x.get_device() == torch.device("cpu"), ( + "this should be handled in decomps unless config.fallback_random or the device is CPU" + ) + return bernoulli_(clone(x), *args) + + +# This shouldn't be called in general +@register_lowering(aten._foobar) +def _foobar(_): + raise AssertionError + + +@functools.lru_cache(1) +def _warn_triton_random(salt): + log.info("using triton random, expect difference from eager") + + +def warn_triton_random(): + # only warn once per graph + _warn_triton_random(V.graph.creation_time) + + +fallback_rand_default = fallback_handler(aten.rand.default) +fallback_rand_generator = fallback_handler(aten.rand.generator) +fallback_randn_default = fallback_handler(aten.randn.default) +fallback_randn_generator = fallback_handler(aten.randn.generator) +make_fallback(aten.randint) +make_fallback(aten.rand_like, override_decomp=True) +make_fallback(aten.randn_like, override_decomp=True) +make_fallback(aten.randint_like, override_decomp=True) + +# TODO: mlazos reevaluate if we want to codegen something different +make_fallback(torch.ops.streams.record_event.default) +make_fallback(torch.ops.streams.wait_event.default) +make_fallback(torch.ops.streams.synchronize_event.default) +make_fallback(torch.ops.streams.synchronize_device.default) + + +@register_lowering(aten.rand) +def rand(*args, **kwargs): + if kwargs.get("generator") is not None: + return fallback_rand_generator(*args, **kwargs) + elif config.fallback_random: + kwargs.pop("generator", None) + return fallback_rand_default(*args, **kwargs) + raise AssertionError("should have been handled in replace_random.py") + + +@register_lowering(aten.randn) +def randn(*args, **kwargs): + if kwargs.get("generator") is not None: + return fallback_randn_generator(*args, **kwargs) + elif config.fallback_random: + kwargs.pop("generator", None) + return fallback_randn_default(*args, **kwargs) + raise AssertionError("should have been handled in replace_random.py") + + +@register_lowering(inductor_prims.force_stride_order, type_promotion_kind=None) +def inductor_force_stride_order(input_tensor, stride): + stride_order = ir.get_stride_order(stride) + return ir.ExternKernel.require_stride_order(input_tensor, stride_order) + + +@register_lowering(inductor_prims.seed, type_promotion_kind=None) +def inductor_seed(device: torch.device): + raise AssertionError("should be handled in fuse_seed_creation_pass()") + + +@register_lowering(inductor_prims.seeds, type_promotion_kind=None) +def inductor_seeds(count, device): + warn_triton_random() + return TensorBox.create(ir.RandomSeeds(count, decode_device(device))) + + +@register_lowering(inductor_prims.lookup_seed, type_promotion_kind=None) +def inductor_lookup_seed(seeds, index): + def inner_fn(_): + return ops.load_seed(seeds.get_name(), index) + + return Pointwise.create( + device=seeds.get_device(), + dtype=seeds.get_dtype(), + inner_fn=inner_fn, + ranges=[], + ) + + +def get_threads_per_round(device: torch.device): + if not isinstance(device, torch.device): + device = torch.device(device) + + if device.type == "cuda": + idx = device.index + if idx is None: + idx = torch.cuda.current_device() + + prop = torch.cuda.get_device_properties(idx) + threads_per_round = ( + prop.multi_processor_count * prop.max_threads_per_multi_processor + ) + else: + _CPU_GRAIN_SIZE = 32768 + threads_per_round = _CPU_GRAIN_SIZE + + return threads_per_round + + +@register_lowering(inductor_prims.random, type_promotion_kind=None) +def inductor_random( + size: list[int], + seed: TensorBox, + mode: str, + *, + offset: int = 0, + align_dtype: torch.dtype = torch.float32, +): + assert not config.fallback_random + assert mode in ("rand", "randn") + size = [*size] + dtype = torch.float32 + device = seed.get_device_or_error() + random_pos = ir.FixedLayout( + device, dtype, size, ir.FlexibleLayout.contiguous_strides(size), offset=offset + ).make_indexer() + seed_loader = seed.make_loader() + + if config.align_random_eager and device.type == "cuda": + threads_per_round = get_threads_per_round(device) + + def _vec_from_dtype(dt: torch.dtype) -> int: + if dt in (torch.float16, torch.bfloat16): + return 8 + return 4 + + vec = _vec_from_dtype(align_dtype) + + def inner_fn(index): + rng_seed = seed_loader([0]) + base_offset = seed_loader([1]) + return ops.rand_eager( + rng_seed, + base_offset, + threads_per_round, + ops.index_expr(random_pos(index), torch.int32), + vec=int(vec), + ) + else: + + def inner_fn(index): + return getattr(ops, mode)( + seed_loader([]), + ops.index_expr(random_pos(index), torch.int32), + ) + + result = Pointwise.create( + device=device, + dtype=dtype, + inner_fn=inner_fn, + ranges=[*size], + ) + result.realize() + return result + + +make_fallback(inductor_prims.rand_eager_offset) +make_fallback(inductor_prims.rand_eager_offsets) + + +@register_lowering(inductor_prims.randint, type_promotion_kind=None) +def inductor_randint( + low: int, high: int, size: list[int], seed: TensorBox, *, offset: int = 0 +): + assert not config.fallback_random + size = [*size] + dtype = torch.int64 + device = seed.get_device_or_error() + random_pos = ir.FixedLayout( + device, dtype, size, ir.FlexibleLayout.contiguous_strides(size), offset=offset + ).make_indexer() + seed_loader = seed.make_loader() + + def inner_fn(index): + return ops.randint64( + seed_loader([]), + ops.index_expr(random_pos(index), torch.int32), + ops.index_expr(low, torch.int64), + ops.index_expr(high, torch.int64), + ) + + return Pointwise.create( + device=device, + dtype=dtype, + inner_fn=inner_fn, + ranges=[*size], + ) + + +def _boundaries_helper(tb: TensorBox) -> tuple[str, sympy.Expr, sympy.Expr, sympy.Expr]: + # Calculate the maximum offset for the boundaries tensor + # For a strided tensor, this is sum((size[i] - 1) * stride[i]) + stride[-1] + # This ensures the mask check in bucketize_binary_search works correctly + # for both contiguous and non-contiguous tensors. + size = tb.get_size() + stride = tb.get_stride() + max_offset = sum((s - 1) * st for s, st in zip(size, stride)) + stride[-1] + return ( + tb.get_name(), + size[-1], + max_offset, + stride[-1], + ) + + +def _sorter_helper(tb: TensorBox) -> tuple[str, sympy.Expr]: + return tb.get_name(), tb.get_stride()[-1] + + +@register_lowering(aten.searchsorted.Tensor, type_promotion_kind=None) +def searchsorted( + sorted_sequence: TensorBox, + self: TensorBox, + *, + out_int32: bool = False, + right: bool = False, + side: str | None = None, + sorter: TensorBox | None = None, +) -> TensorBox: + validate_bucketize = lambda tb: V.graph.has_feature( # noqa: E731 + tb, BackendFeature.BUCKETIZE + ) + if ( + not validate_bucketize(sorted_sequence) + or not validate_bucketize(self) + or (sorter is not None and not validate_bucketize(sorter)) + ): + return fallback_handler(aten.searchsorted.Tensor, add_to_fallback_set=False)( + sorted_sequence, + self, + out_int32=out_int32, + right=right, + side=side, + sorter=sorter, + ) + + # If side is present, override the value of right if needed. This assumes that + # validation of the two options being non-contradictory is already done by the + # searchsorted meta-function. + if side is not None and side == "right": + right = True + + index_dtype = torch.int32 if out_int32 else torch.int64 + values_loader = self.make_loader() + + # The entire sorted_sequence tensor needs to be used by ops.bucketize, so we need to + # realize it into global memory; or in other words, we can't guarantee that + # sorted_sequence.get_name() (used below) will exist unless we call + # sorted_sequence.realize(). + sorted_sequence.realize() + + if sorter is not None: + sorter.realize() + + if len(sorted_sequence.get_size()) == 1: + + def inner_fn(idx): + val = values_loader(idx) + return ops.bucketize( + val, + _boundaries_helper(sorted_sequence), + 0, + index_dtype, + right, + sorter=None if sorter is None else _sorter_helper(sorter), + sorter_indices=None if sorter is None else 0, + ) + + else: + + def inner_fn(idx): + val = values_loader(idx) + + # Get index to the beginning of the sorted sequence within a flattened + # version of the array. + def get_flattened_index(tb: TensorBox): + strides = tb.get_stride() + return ops.index_expr( + functools.reduce( + operator.add, (s * i for s, i in zip(strides[:-1], idx[:-1])) + ), + index_dtype, + ) + + return ops.bucketize( + val, + _boundaries_helper(sorted_sequence), + get_flattened_index(sorted_sequence), + index_dtype, + right, + sorter=None if sorter is None else _sorter_helper(sorter), + sorter_indices=None if sorter is None else get_flattened_index(sorter), + ) + + device = self.get_device() + result = Pointwise.create( + device=device, + dtype=index_dtype, + inner_fn=inner_fn, + ranges=self.shape, + ) + # see [NOTE: inductor bucketize realize] + result.realize() + + return result + + +@register_lowering( + aten.bucketize.Tensor, type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.NO_OPMATH +) +def bucketize( + input: TensorBox, + boundaries: TensorBox, + *, + out_int32: bool = False, + right: bool = False, +): + assert len(boundaries.get_size()) == 1 + + if not ( + V.graph.has_feature(input, BackendFeature.BUCKETIZE) + and V.graph.has_feature(boundaries, BackendFeature.BUCKETIZE) + ): + return fallback_handler(aten.bucketize.Tensor, add_to_fallback_set=False)( + input, boundaries, out_int32=out_int32, right=right + ) + + # The entire boundaries tensor needs to be used by ops.bucketize, so we + # need to realize it into global memory; or in other words, we can't + # guarantee that boundaries.get_name() (used below) will exist unless + # we call boundaries.realize(). + boundaries.realize() + device = input.get_device() + input_loader = input.make_loader() + + index_dtype = torch.int32 if out_int32 else torch.int64 + + def inner_fn(index): + val = input_loader(index) + indices = ops.bucketize( + val, + _boundaries_helper(boundaries), + 0, + index_dtype, + right, + ) + + return indices + + result = Pointwise.create( + device=device, + dtype=index_dtype, + inner_fn=inner_fn, + ranges=input.get_size(), + ) + + # [NOTE: inductor bucketize realize] + # bucketize_binary_search is relatively expensive, so we don't want to re-compute + # it unnecessarily. If we run bucketize() and then broadcast the result, we don't + # want this to be fused into a large number of duplicate bucketize() computations + # for each of the elements in the result. + # + # If no broadcasting occurs, fusions can still occur in scheduler.py + result.realize() + + return result + + +def _is_tensor_irnode(x): + return isinstance(x, ir.IRNode) and not isinstance( + x, (ir.NonTensorObj, ir.OpaqueMultiOutput) + ) + + +def require_dense(_, *args, **kwargs): + args, kwargs = pytree.tree_map_only( + _is_tensor_irnode, ir.ExternKernel.require_stride1, (args, kwargs) + ) + return args, kwargs + + +def require_contiguous(_, *args, **kwargs): + args, kwargs = pytree.tree_map_only( + _is_tensor_irnode, + ir.ExternKernel.require_contiguous, + (args, kwargs), + ) + return args, kwargs + + +def require_contiguous_strides(_, *args, **kwargs): + # TODO: combine this with require_contiguous after + # https://github.com/pytorch/pytorch/pull/148235 lands. + args, kwargs = pytree.tree_map_only( + _is_tensor_irnode, + ir.ExternKernel.require_contiguous_strides, + (args, kwargs), + ) + return args, kwargs + + +def require_channels_last(_, *args, **kwargs): + args, kwargs = pytree.tree_map_only( + _is_tensor_irnode, + ir.ExternKernel.require_channels_last, + (args, kwargs), + ) + return args, kwargs + + +def constrain_to_fake_tensor(arg, fake_arg): + if fake_arg is None: + return arg + if isinstance(fake_arg, FakeScriptObject) or is_opaque_value(fake_arg): + return arg + if isinstance(arg, ir.IRNode): + return ir.ExternKernel.require_exact_strides(arg, fake_arg.stride()) + if isinstance(arg, dict): + return {key: constrain_to_fake_tensor(arg[key], fake_arg[key]) for key in arg} + elif isinstance(arg, (tuple, list)): + return type(arg)( + constrain_to_fake_tensor(a, f_a) for (a, f_a) in zip(arg, fake_arg) + ) + return arg + + +def constrain_to_fake_tensors(args, kwargs, fake_args, fake_kwargs): + args = tuple( + constrain_to_fake_tensor(arg, fake_arg) + for arg, fake_arg in zip(args, fake_args) + ) + kwargs = {k: constrain_to_fake_tensor(v, fake_kwargs[k]) for k, v in kwargs.items()} + return args, kwargs + + +def constrain_to_fx_strides(fx_node, *args, **kwargs): + def apply_constraint(arg, fx_arg): + if _is_tensor_irnode(arg): + fake_val = fx_arg.meta.get("val") + if not isinstance(fake_val, torch.Tensor): + return arg + stride_order = ir.get_stride_order( + fake_val.stride(), V.graph.sizevars.shape_env + ) + return ir.ExternKernel.require_stride_order(arg, stride_order) + if isinstance(arg, dict): + return {key: apply_constraint(arg[key], fx_arg[key]) for key in arg} + return arg + + args = tuple( + apply_constraint(arg, fx_arg) for arg, fx_arg in zip(args, fx_node.args) + ) + kwargs = {k: apply_constraint(v, fx_node.kwargs[k]) for k, v in kwargs.items()} + return args, kwargs + + +# native_dropout uses empty_like(input) internally, so bernoulli_ consumes +# RNG values in the input's stride order. Constrain input strides to match +# the FX graph (i.e. eager) so the dropout mask is identical. +add_layout_constraint(aten.native_dropout.default, constrain_to_fx_strides) + + +def sdpa_constraint(fx_node, *args, **kwargs): + """Apply stride constraints to SDPA inputs, ensuring dense last dimension.""" + + def apply_constraint(idx, arg, fx_arg): + if not _is_tensor_irnode(arg): + return arg + + meta_val = fx_arg.meta["val"] + meta_stride_expr = [ + s.node.expr if isinstance(s, torch.SymInt) else s for s in meta_val.stride() + ] + shape_env = V.graph.sizevars.shape_env + stride_order = ir.get_stride_order(meta_val.stride(), shape_env) + + if stride_order and stride_order[-1] != 0: + # contiguous stride order + stride_order = list(reversed(range(len(arg.get_size())))) + + if ( + fx_node.target + == aten._scaled_dot_product_efficient_attention_backward.default + and idx in (0, 5) + ): + assert len(stride_order) == 4 + # The 0 and 5th arguments for aten._scaled_dot_product_efficient_attention_backward.default + # are for out and gradient_out. They have to be in + # (3, 1, 2, 0) stride order. Otherwise the kernel will crash. + # Check https://github.com/pytorch/pytorch/issues/138772 + stride_order = (3, 1, 2, 0) + + # Cache keyed by (id(arg), arg_name, stride_order) to avoid + # duplicate copy_input when the same tensor feeds multiple SDPA + # positions (e.g., key=value). Including arg_name handles + # mutation: mark_buffer_mutated() renames the buffer in place, + # so a mutated tensor has the same id but a different name, + # causing a cache miss. + cache_key = None + if config.cache_sdpa_constraint: + arg_name = arg.maybe_get_name() + cache_key = ( + id(arg), + arg_name, + tuple(stride_order) if stride_order else None, + ) + if cache_key in V.graph.sdpa_constraint_cache: + return V.graph.sdpa_constraint_cache[cache_key] + + result = _apply_constraint_inner( + idx, arg, meta_val, meta_stride_expr, stride_order + ) + if cache_key is not None: + V.graph.sdpa_constraint_cache[cache_key] = result + return result + + def _apply_constraint_inner(idx, arg, meta_val, meta_stride_expr, stride_order): + if not (meta_val.is_cuda or meta_val.is_xpu): + return ir.ExternKernel.require_stride_order(arg, stride_order) + + # This is the minimum alignment required by SDPA kernels for attention_bias. + # This value can be found in pytorch/aten/src/ATen/native/transformers/attention.cpp preprocess_mask + ALIGNMENT = 8 + + # effn_attn_fwd does requires dense last dim, not just alignment + effn_attn_fwd_bias = ( + fx_node.target + == torch.ops.aten._scaled_dot_product_efficient_attention.default + and idx == 3 + ) + + assert isinstance(arg, TensorBox) + if len(arg.get_size()) not in (3, 4): + return arg + + is_aligned_tensor = ir.is_aligned_realized_tensor(arg, ALIGNMENT) + if is_aligned_tensor: + return ir.try_match_insignificant_strides( + ir.ExternKernel.realize_input(arg), meta_stride_expr + ) + + if ( + isinstance(arg, IRNode) + and arg.maybe_get_stride() is not None + and is_aligned_tensor + ): + return ir.try_match_insignificant_strides( + ir.ExternKernel.realize_input(arg), meta_stride_expr + ) + + if effn_attn_fwd_bias: + out_size = list(arg.get_size()) + + expanded_dims = [] + # We require a dense last dimension, but the other strides + # can be expanded, which results in a smaller tensor + maybe_stride = arg.maybe_get_stride() + for i in range(len(arg.get_size()) - 1): + if V.graph.sizevars.statically_known_equals(meta_stride_expr[i], 0) or ( + maybe_stride is not None + and V.graph.sizevars.statically_known_equals(maybe_stride[i], 0) + ): + expanded_dims.append(i) + + # Now, pad strides to alignment + out_strides = [-1] * len(out_size) + out_strides[-1] = 1 + stride = 1 + for i in range(len(out_size) - 2, -1, -1): + if out_strides[i + 1] != 0: + stride = stride * out_size[i + 1] + + # the expanded dims still need to be aligned, if they are, + # we can make them expanded by setting the stride equal to 0 + if i in expanded_dims: + if V.graph.sizevars.statically_known_equals( + Mod(out_strides[i + 1], ALIGNMENT), 0 + ): + out_strides[i] = 0 + continue + + if not V.graph.sizevars.statically_known_equals( + Mod(stride, ALIGNMENT), 0 + ): + stride = ceildiv(stride, ALIGNMENT) * ALIGNMENT + + out_strides[i] = stride + + return ir.ExternKernel.require_exact_strides(arg, out_strides) + + if is_aligned_tensor: + return ir.try_match_insignificant_strides( + ir.ExternKernel.realize_input(arg), meta_stride_expr + ) + + if ( + isinstance(arg, IRNode) + and arg.maybe_get_stride() is not None + and is_aligned_tensor + ): + return ir.try_match_insignificant_strides( + ir.ExternKernel.realize_input(arg), meta_stride_expr + ) + + def is_aligned(x): + return V.graph.sizevars.guard_or_false( + sympy.Eq(Mod(x.get_size()[-1], ALIGNMENT), 0) + ) + + if isinstance(arg.data, ir.BaseView): + if not is_aligned(arg): + if is_aligned(arg.unwrap_view()): + return ir.try_match_insignificant_strides( + ir.ExternKernel.realize_input(arg), meta_stride_expr + ) + + return ir.ExternKernel.require_stride_order(arg, stride_order) + + args = tuple( + apply_constraint(idx, arg, fx_arg) + for idx, (arg, fx_arg) in enumerate(zip(args, fx_node.args)) + ) + kwargs = {k: apply_constraint(-1, v, fx_node.kwargs[k]) for k, v in kwargs.items()} + return args, kwargs + + +# WIP +make_fallback(aten._adaptive_avg_pool3d) # @isuruf +make_fallback(aten.adaptive_max_pool3d) # @isuruf +make_fallback(aten._scaled_dot_product_attention_math_for_mps) # @malfet + + +# 1) Easy +make_fallback(aten.uniform, warn=False) +make_fallback(aten.exponential.default, warn=False) # (fails accuracy on test_torch.py) +make_fallback(aten._pdist_forward, require_contiguous) # Has decomp. Needs benchmarks +make_fallback(aten.soft_margin_loss_backward, warn=False) # py_impl? +make_fallback(aten._fused_rms_norm, warn=False) # (MPS-only and faster than decomp) +if torch.xpu._is_compiled(): + make_fallback( + aten.embedding_dense_backward, warn=False + ) # (XPU-only and faster than decomp) + +if torch.mtia._is_compiled(): + make_fallback( + aten.native_layer_norm, warn=False + ) # (MTIA-only and faster than decomp) + +# 1.5) Easy or Impossible +make_fallback(aten._cdist_forward) # p=2 should be feasible +make_fallback(aten._cdist_backward) + +# 2) Medium +make_fallback(aten._trilinear) + + +# 3) Difficult +# Scans +# See the discussion at +# https://dev-discuss.pytorch.org/t/pytorch-sparse-gnn-compiler-rfc/1644/19 +make_fallback(aten.segment_reduce.default) +make_fallback(aten._segment_reduce_backward.default) + +# Histogram (need to implement Histogram IR) +make_fallback(aten.histc) +make_fallback(aten.histogram.bin_ct) +make_fallback(aten._histogramdd_bin_edges.default) +make_fallback(aten._histogramdd_from_bin_cts.default) + +# Need templated kernel +make_fallback(aten.addbmm) +make_fallback(aten._addmm_activation, warn=False) + +make_fallback(aten._grouped_mm, require_dense) + +# Need templated kernel. Probably impossible to write efficiently +make_fallback(aten.convolution_backward, constrain_to_fx_strides) +make_fallback(aten._cudnn_rnn, require_dense) +make_fallback(aten._cudnn_rnn_backward, require_contiguous) +make_fallback(aten.miopen_rnn, require_dense) +make_fallback(aten.miopen_rnn_backward, require_contiguous) + +# Haven't checked but sound difficult / impossible +make_fallback(aten._embedding_bag, require_contiguous) +make_fallback(aten._embedding_bag_forward_only, require_contiguous) +make_fallback(aten._embedding_bag_backward) +make_fallback(aten._embedding_bag_per_sample_weights_backward) +make_fallback(aten._embedding_bag_per_sample_weights_backward) +make_fallback(aten._fused_moving_avg_obs_fq_helper) +make_fallback(aten._fused_moving_avg_obs_fq_helper_functional) + + +# 4) Backwards (try py_impl'ing them) when fwd is written as a decomp +make_fallback(aten.max_pool3d_with_indices_backward) +make_fallback(aten._adaptive_avg_pool2d_backward, require_dense) +make_fallback(aten._adaptive_avg_pool3d_backward) +make_fallback(aten.adaptive_max_pool2d_backward) +make_fallback(aten.adaptive_max_pool3d_backward) +make_fallback(aten.fractional_max_pool2d_backward) +make_fallback(aten.fractional_max_pool3d_backward) +make_fallback(aten.replication_pad1d_backward) +make_fallback(aten.replication_pad2d_backward) +make_fallback(aten.upsample_linear1d_backward) +make_fallback(aten.upsample_bicubic2d_backward, require_contiguous) +make_fallback(aten.upsample_trilinear3d_backward) +make_fallback(aten.grid_sampler_2d_backward) +make_fallback(aten._pdist_backward, require_contiguous) + + +# 5) Impossible (missing triton/CPU features) + +# Sorting / Sorting-like +make_fallback(aten.nanmedian) +make_fallback(aten.randperm) +# see: https://github.com/pytorch/pytorch/pull/121354 +make_fallback(aten.resize_) +make_fallback(aten.resize_as_) + +# Linalg +make_fallback(aten._linalg_det) +make_fallback(aten.linalg_householder_product) +make_fallback(aten.linalg_inv_ex) +make_fallback(aten.linalg_ldl_factor_ex) +make_fallback(aten.linalg_ldl_solve) +make_fallback(aten.linalg_lu) +make_fallback(aten.linalg_lu_factor_ex) +make_fallback(aten.linalg_lu_solve) +make_fallback(aten.linalg_matrix_exp) +make_fallback(aten.linalg_qr) +make_fallback(aten._linalg_slogdet) +make_fallback(aten._linalg_solve_ex) +make_fallback(aten.linalg_solve_triangular) +make_fallback(aten._linalg_svd) +make_fallback(aten.lu_unpack) +make_fallback(aten.ormqr) +make_fallback(aten._linalg_check_errors) +make_fallback(aten.linalg_pinv.atol_rtol_tensor) +make_fallback(aten._linalg_eigh) +make_fallback(aten.triangular_solve) +make_fallback(aten.linalg_cholesky_ex) +make_fallback(aten.cholesky_inverse) +make_fallback(aten.cholesky_solve) +make_fallback(aten.geqrf) +make_fallback(aten._fft_r2c) # needs complex as well + +# Data dependent (are these necessary?) +make_fallback(aten.nonzero.default) + +# Misc +make_fallback(aten.gcd.default, warn=False) +make_fallback(aten._thnn_fused_lstm_cell, require_dense) +make_fallback(torch._prims.rng_prims.run_and_save_rng_state) +make_fallback(torch._prims.rng_prims.run_with_rng_state) +make_fallback(torch._prims.rng_prims.graphsafe_run_with_rng_state) +make_fallback(torch._prims.rng_prims.run_dtensor_rng_op) + + +# Implemented / Half implemented +# Scans. Implemented for CUDA, missing CPU +make_fallback(aten.masked_scatter) +make_fallback(aten.masked_scatter_backward) + +# Complex number support +make_fallback(aten.view_as_complex, require_contiguous) +make_fallback(aten.angle) # needs complex + +# Needs efficentzerotensor +make_fallback(aten._efficientzerotensor) + +# Needs Sparse +make_fallback(aten._sparse_coo_tensor_with_dims_and_tensors) +make_fallback(aten.to_sparse) +make_fallback(aten._to_sparse) + +# Needs dimname support +make_fallback(aten.zeros.names) + +# 6) Pattern-matched +make_fallback( + aten._scaled_dot_product_efficient_attention.default, + sdpa_constraint, + warn=False, +) +make_fallback( + aten._scaled_dot_product_efficient_attention_backward.default, + sdpa_constraint, + warn=False, +) +make_fallback( + aten._scaled_dot_product_flash_attention.default, + sdpa_constraint, + warn=False, +) +make_fallback( + aten._scaled_dot_product_flash_attention.quantized, + warn=False, +) +make_fallback( + aten._scaled_dot_product_flash_attention_backward.default, + sdpa_constraint, + warn=False, +) +make_fallback( + aten._scaled_dot_product_cudnn_attention.default, + sdpa_constraint, + warn=False, +) +make_fallback( + aten._scaled_dot_product_cudnn_attention_backward.default, + sdpa_constraint, + warn=False, +) +make_fallback( + aten._scaled_dot_product_flash_attention_for_cpu.default, + sdpa_constraint, + warn=False, +) +make_fallback( + aten._scaled_dot_product_flash_attention_for_cpu_backward.default, + sdpa_constraint, + warn=False, +) +make_fallback( + aten._scaled_dot_product_fused_attention_overrideable.default, + sdpa_constraint, + warn=False, +) +make_fallback( + aten._scaled_dot_product_fused_attention_overrideable_backward.default, + sdpa_constraint, + warn=False, +) +make_fallback(aten._flash_attention_forward.default, sdpa_constraint) +make_fallback(aten._flash_attention_forward.quantized) +make_fallback(aten._flash_attention_backward.default, sdpa_constraint) +make_fallback(aten._efficient_attention_forward.default, sdpa_constraint) +make_fallback(aten._efficient_attention_backward.default, sdpa_constraint) + +# index_reduce requires fallback when use_scatter_fallback(...) returns True +make_fallback(aten.index_reduce) +make_fallback(aten.repeat_interleave.Tensor, override_decomp=True) + +make_fallback(aten._weight_norm_interface_backward.default, require_contiguous) + + +# Register with type_promotion_kind None. +# For example, fp16.copy_(fp32) should **not** promote the first input's dtype. +@register_lowering(aten.copy, type_promotion_kind=None) +def copy(self, src, non_blocking=False): + if not isinstance(src, ir.IRNode): + src = tensor(src, dtype=self.get_dtype(), device=self.get_device()) + x = src + if self.get_device() != src.get_device(): + x = to_device(x, self.get_device()) + if self.get_dtype() != src.get_dtype(): + x = to_dtype(x, self.get_dtype()) + + if self.get_size() != src.get_size(): + out = expand(x, self.get_size()) + return clone(out) + return clone(x) + + +@register_lowering(aten.clone) +def clone(x, *, memory_format=None): + # TODO(jansel): memory format + return Pointwise.create( + device=x.get_device(), + dtype=x.get_dtype(), + inner_fn=x.make_loader(), + ranges=list(x.get_size()), + ) + + +def clone_preserve_reinterpret_view(x): + reinterpret_view_layouts = [] + if isinstance(x, TensorBox) and isinstance(x.data, ir.ReinterpretView): + x = x.data # unwrap TensorBox + # pyrefly: ignore [bad-assignment] + while isinstance(x, ir.ReinterpretView): + reinterpret_view_layouts.append(x.get_layout()) + x = x.data + x = TensorBox(x) + + x = clone(x) + + if reinterpret_view_layouts: + x = x.data # unwrap TensorBox + for layout in reinterpret_view_layouts[::-1]: + x = ir.ReinterpretView(data=x, layout=layout) + x = TensorBox(x) + + return x + + +if hasattr(aten, "lift_fresh_copy"): + register_lowering(aten.lift_fresh_copy)(clone) + + +@register_lowering(prims.iota) +def iota( + length, + *, + start, + step, + dtype, + device, + requires_grad, +): + def fn(index): + return ops.index_expr(step * index[0] + start, dtype=dtype) + + return Pointwise.create( + device=decode_device(device), + dtype=dtype, + inner_fn=fn, + ranges=[length], + ) + + +@register_lowering(aten.arange.start_step, type_promotion_kind=None) +def arange_start_step( + start, + end, + step=1, + *, + dtype=None, + device=None, + layout=None, + pin_memory=None, + requires_grad=False, +): + assert dtype is not None + length = ceildiv(end - start, step) + return iota( + length, + start=start, + step=step, + dtype=dtype, + device=device if device is not None else "cpu", + requires_grad=requires_grad, + ) + + +@register_lowering(aten.select_scatter, type_promotion_kind=None) +def select_scatter(x, src, dim: int, index: int): + src = to_dtype(src, x.get_dtype()) + x_loader = x.make_loader() + dim = _validate_dim(x, dim, 0) + if V.graph.sizevars.guard_or_false(sympy.Lt(index, 0)): + index = index + x.get_size()[dim] + elif V.graph.sizevars.guard_or_false(sympy.Ge(index, 0)): + pass + else: + # unbacked index + return fallback_handler(aten.select_scatter.default)(x, src, dim, index) + + V.graph.sizevars.check_leq(0, index) # type: ignore[arg-type] + V.graph.sizevars.check_lt(index, x.get_size()[dim]) # type: ignore[arg-type] + src = expand(unsqueeze(src, dim), x.get_size()) + src_loader = src.make_loader() + + def inner_fn(idx): + return ops.where( + ops.eq( + ops.index_expr(idx[dim], torch.int32), + ops.index_expr(index, torch.int32), + ), + src_loader(idx), + x_loader(idx), + ) + + return Pointwise.create( + device=x.get_device(), + dtype=x.get_dtype(), + inner_fn=inner_fn, + ranges=list(x.get_size()), + ) + + +@register_lowering(aten.slice_scatter, type_promotion_kind=None) +def slice_scatter(x, src, dim=0, start=None, end=None, step=1): + src = to_dtype(src, x.get_dtype()) + x_loader = x.make_loader() + dim = _validate_dim(x, dim, 0) + dim_size = x.get_size()[dim] + + # pyrefly: ignore [bad-argument-type] + start, end = ir.SliceView.normalize_start_end(x, dim, start, end) + + src_size = list(x.get_size()) + src_size[dim] = FloorDiv(end - start + (step - 1), step) + src = expand(src, src_size) + src_loader = src.make_loader() + + def inner_fn(idx): + if start == 0 and end == dim_size and step == 1: + # selecting every element is the same as just src.clone() + return src_loader(idx) + + idx_dim = ops.index_expr(idx[dim], torch.int64) + src_idx = list(idx) + src_idx[dim] = FloorDiv(idx[dim] - start, step) + + mask = [] + if start != 0: + mask.append( + ops.ge( + idx_dim, + ops.index_expr(sympy.expand(start), torch.int64), + ) + ) + if end != dim_size: + mask.append( + ops.lt( + idx_dim, + ops.index_expr(sympy.expand(end), torch.int64), + ) + ) + if step != 1: + mask.append( + ops.eq( + ops.index_expr( + ModularIndexing(idx[dim] - start, 1, step), torch.int64 + ), + ops.constant(0, torch.int64), + ) + ) + assert mask + mask = functools.reduce(ops.and_, mask) + src_val = ops.masked( + mask, + lambda: src_loader(src_idx), + 0 if is_integer_type(x) else 0.0, + ) + return ops.where( + mask, + src_val, + x_loader(idx), + ) + + return Pointwise.create( + device=x.get_device(), + dtype=x.get_dtype(), + inner_fn=inner_fn, + ranges=list(x.get_size()), + ) + + +def _unwrap(x): + if isinstance(x, (list, tuple)) and len(x) > 0: + return _unwrap(x[0]) + return x + + +@register_lowering([torch.tensor, aten.scalar_tensor, prims.scalar_tensor]) +def tensor(data, *, dtype=None, device=None, layout=None, pin_memory=False): + assert_nyi(layout in (None, torch.strided), f"layout={layout}") + assert_nyi(not pin_memory, "pin_memory") + if isinstance(_unwrap(data), int): + dtype = dtype or torch.int64 + else: + dtype = dtype or torch.get_default_dtype() + + ranges: list[sympy.Expr] = [] + + if isinstance(data, sympy.Basic): + + def inner_fn(index): + return ops.index_expr(data, dtype) + + elif isinstance(data, (float, int)): + + def inner_fn(index): + return ops.constant(data, dtype) + + elif len(data) == 0 or isinstance(data[0], (float, int)) and len(data) <= 8: + # inline small tensors + ranges.append(sympy.Integer(len(data))) + + def inner_fn(index): + def binary_search(start, end): + assert start < end + if end - start == 1: + return ops.constant(data[start], dtype) + mid = (end - start) // 2 + start + return ops.where( + ops.lt( + ops.index_expr(index[0], torch.int64), + ops.constant(mid, torch.int64), + ), + binary_search(start, mid), + binary_search(mid, end), + ) + + if len(data) == 0: + return ops.constant(0, dtype) + return binary_search(0, len(data)) + + else: + return V.graph.add_tensor_constant( + torch.tensor(data, dtype=dtype, device=device) + ) + + return Pointwise.create( + device=decode_device(device), + dtype=dtype, + inner_fn=inner_fn, + ranges=ranges, + ) + + +@register_lowering(torch.as_tensor) +def as_tensor(data, dtype=None, device=None): + if isinstance(data, TensorBox): + if dtype is not None: + data = to_dtype(data, dtype) + if device is not None: + data = to_device(data, device) + return data + return tensor(data, dtype=dtype, device=device) + + +@register_lowering(torch.LongTensor) +def long_tensor(data): + return tensor(data, dtype=torch.int64) + + +@register_lowering(aten._local_scalar_dense) +def _local_scalar_dense(data): + # This is interesting! Most lowerings return tensors, so you can just + # return the buffer you allocated and it will get used (or not used, if + # it's dead.) But _local_scalar_dense (aka item) returns an int, + # not a Tensor, so you would have a type mismatch if you return a buffer; + # we are obligated to return a sympy expression instead. However, + # we need to actually codegen the .item() call somehow. We do this + # by registering a faux buffer for the DynamicScalar IR node, which is + # solely responsible for generating this .item(). The buffer is + # not used for anything (notice we discard it); at codegen time, + # the "buffer" just gets assigned None. + unbacked_bindings = resolve_unbacked_bindings( + V.graph.sizevars.shape_env, V.graph.current_node.meta["unbacked_bindings"] + ) + assert unbacked_bindings is not None + assert len(unbacked_bindings) == 1, unbacked_bindings + # NB: Have to be very careful here. V.graph.current_node.meta["val"] + # seemingly also contains a symbol which you want to do binding for, + # but it actually isn't. In particular, if we have later performed + # a deferred runtime assert saying that u0 == s0, you will actually + # see s0 from expr! This is bad because we need to actually generate + # the assert that says u0 == s0, so we need to know where to get u0 + # from (this call). In particular, we must use unbacked_bindings, which + # is guaranteed to have the original, unreplaced symbol in question. + # + # NB2: Another thing we have to be very careful about are symbol bindings + # that require nontrivial refinement, e.g., when you have a binding site + # x: Sym(u0 * 4) = y.item(). Here, the code generation must do a division + # in order to appropriately bind u0. This is communicated via the keypath + # in unbacked_bindings, and we need to hold onto it in order to generate + # code appropriately for this case. + binding_sym, keypath = next(iter(unbacked_bindings.items())) + buffer = ir.DynamicScalar(binding_sym, keypath, data) + buffer.name = V.graph.register_buffer(buffer) + V.graph.register_operation(buffer) + # NB: the replaced expr is OK to use directly downstream, we want + # simplifications in this case! + val = V.graph.current_node.meta["val"] + if isinstance(val, (torch.SymInt, torch.SymFloat, torch.SymBool)): + return val.node.expr + else: + return sympy.sympify(val) + + +@register_lowering(aten._assert_scalar) +def _assert_scalar(data, msg): + # NB: These will be handled at codegen time + # Not sure if we are guaranteed to be able to serve out truth from the + # deferred_runtime_asserts, TODO: try this assert out + # See [NOTE] Codegen runtime asserts in Inductor + # assert bool(data.scalar), data + return None + + +@register_lowering(aten._assert_tensor_metadata) +def _assert_tensor_metadata( + a, size=None, stride=None, dtype=None, *, device=None, layout=None +): + return None + + +def _full(fill_value, device, dtype, size): + value = fill_value + if not isinstance(fill_value, (int, float)) and hasattr(value, "value"): + value = value.value + + if isinstance(value, (int, float)): + + def inner_fn(index): + return ops.constant(value, dtype) + + elif isinstance(value, sympy.Basic): + + def inner_fn(index): + return ops.index_expr(value, dtype) + + else: + assert len(value.get_size()) == 0 + value_loader = value.make_loader() + + def inner_fn(index): + return value_loader([]) + + return Pointwise.create( + device=device, + dtype=dtype, + inner_fn=inner_fn, + ranges=list(size), + ) + + +def full_like(x, fill_value, **kwargs): + return create_tensor_like(tensor_constructor(fill_value))(x, **kwargs) + + +def tensor_constructor(fill_value): + # torch.zeros, torch.ones, etc + def inner( + *size, + names=None, + dtype=None, + device=None, + layout=None, + pin_memory=False, + memory_format=None, + ): + assert_nyi(names is None, "named tensors") + assert_nyi(layout in (None, torch.strided), f"layout={layout}") + assert_nyi(not pin_memory, "pin_memory") + device = decode_device(device) + dtype = dtype or torch.get_default_dtype() + if len(size) == 1 and isinstance(size[0], (list, tuple, torch.Size)): + size = tuple(size[0]) + # See https://github.com/pytorch/pytorch/issues/118102 + # All sizes at lowering time should be sympy.Symbol, not SymInt! + for s in size: + assert not isinstance(s, torch.SymInt) + size = [sympy.expand(s) for s in size] + return _full(fill_value, device, dtype, size) + + return inner + + +@register_lowering([torch.empty, aten.empty]) +def empty( + *size, + names=None, + dtype=None, + layout=None, + device=None, + pin_memory=None, + memory_format=None, +): + assert_nyi(names is None, "named tensors") + device = decode_device(device) + if len(size) == 1 and isinstance(size[0], (list, tuple, torch.Size)): + size = tuple(size[0]) + return empty_strided( + size, None, dtype=dtype, layout=layout, device=device, pin_memory=pin_memory + ) + + +def create_tensor_like(creation_fn): + """ + Shim to convert X_like(...) into X(...). For example zeros_like() into zeros(). + """ + + def _constant_like( + x, *, dtype=None, device=None, layout=None, pin_memory=False, memory_format=None + ): + assert_nyi(not pin_memory, "pin_memory") + assert_nyi(layout in (None, torch.strided), f"layout={layout}") + if dtype is None: + dtype = x.get_dtype() + else: + dtype = decode_dtype(dtype) + device = device or x.get_device() + size = list(x.get_size()) + return creation_fn( + size, dtype=dtype, device=device, layout=layout, pin_memory=pin_memory + ) + + return _constant_like + + +def constant_like(fill_value): + return create_tensor_like(tensor_constructor(fill_value)) + + +empty_like = register_lowering(aten.empty_like)(create_tensor_like(empty)) +ones_like = create_tensor_like(tensor_constructor(1)) +zeros_like = create_tensor_like(tensor_constructor(0)) + + +def new_constant(fill_value): + def _new_constant( + x, size, *, dtype=None, layout=None, device=None, pin_memory=None + ): + assert isinstance(size, (list, tuple)) + assert_nyi(not pin_memory, "pin_memory") + assert_nyi(layout in (None, torch.strided), f"layout={layout}") + # pyrefly: ignore [bad-argument-type] + dtype = decode_dtype(dtype) or x.get_dtype() + device = device or x.get_device() + size = [sympy.Integer(s) for s in size] + return _full(fill_value, decode_device(device), dtype, size) + + return _new_constant + + +@register_lowering(aten.new_empty) +def new_empty(x, size, *, dtype=None, layout=None, device=None, pin_memory=None): + if dtype is None: + dtype = x.get_dtype() + if device is None: + device = x.get_device() + return empty_strided( + size, + None, + dtype=dtype, + layout=layout, + device=decode_device(device), + pin_memory=pin_memory, + ) + + +@register_lowering(aten.empty_strided) +def empty_strided( + size, stride, *, dtype=None, layout=None, device=None, pin_memory=None +): + assert isinstance(size, (list, tuple)) + assert isinstance(stride, (list, tuple, type(None))) + assert_nyi(layout in (None, torch.strided), f"layout={layout}") + # pyrefly: ignore [bad-argument-type] + dtype = decode_dtype(dtype) or torch.get_default_dtype() + device = device or torch.tensor(0.0).device + device = decode_device(device) + pointwise = _full(fill_value=0, device=device, dtype=dtype, size=size) + pointwise.realize() + buffer = pointwise.data.data + # explicitly set ranges to zeros in order to make a NopKernelSchedulerNode + buffer.data = dataclasses.replace(buffer.data, ranges=[0] * len(size)) + assert isinstance(buffer, ir.ComputedBuffer) + size = [sympy.expand(s) for s in size] + stride = ( + [sympy.expand(s) for s in stride] + if stride + else ir.FlexibleLayout.contiguous_strides(size) + ) + buffer.layout = ir.FixedLayout( + device=device, + dtype=dtype, + size=size, + stride=stride, + is_pinned=pin_memory or False, + ) + return pointwise + + +@register_lowering(aten.new_empty_strided) +def new_empty_strided( + x, size, stride, *, dtype=None, layout=None, device=None, pin_memory=None +): + if dtype is None: + dtype = x.get_dtype() + if device is None: + device = x.get_device() + return empty_strided( + size, + stride, + dtype=dtype, + layout=layout, + device=decode_device(device), + pin_memory=pin_memory, + ) + + +@register_lowering(prims.copy_strided.default) +def copy_strided(x, stride): + stride = V.graph.sizevars.guarding_hints_or_throw(stride) + stride_order = sorted(range(len(stride)), key=stride.__getitem__) + return ir.ExternKernel.require_stride_order(x, stride_order) + + +@register_lowering([torch.full, aten.full]) +def full(size, fill_value, **kwargs): + assert kwargs.get("dtype") is not None, "dtype should be handled by decomposition" + return tensor_constructor(fill_value)(size, **kwargs) + + +@register_lowering(aten.gather, type_promotion_kind=None) +def gather(x, dim, index, sparse_grad=False): + # sparse_grad doesn't affect forward computation, + # and backward tracing is taken care of by AOT Autograd + assert isinstance(x, TensorBox) + if index.get_numel() == 0: + # Empty index case. Return an empty array with the same shape + return new_empty(x, index.get_size()) + + size = x.get_size() + offset = len(size) == 0 + dim = _validate_dim(x, dim, offset) + + if offset: + x = expand(x, [1]) + size = [1] + + x_loader = x.make_loader() + index_loader = index.make_loader() + + def fn(idx): + idx = list(idx) + gather_idx = ops.indirect_indexing(index_loader(idx), size[dim]) + if len(idx) == 0: + idx = [gather_idx] + else: + idx[dim] = gather_idx + return x_loader(idx) + + return Pointwise.create( + device=x.get_device(), + dtype=x.get_dtype(), + inner_fn=fn, + ranges=index.get_size(), + ) + + +@register_lowering(aten.embedding, type_promotion_kind=None) +def embedding(weight, indices, padding_idx=-1, scale_grad_by_freq=False, sparse=False): + if sparse: + return fallback_handler(aten.embedding.default)( + weight, indices, padding_idx, scale_grad_by_freq, sparse + ) + + assert not sparse + assert isinstance(weight, TensorBox) + assert isinstance(indices, TensorBox) + assert "int" in str(indices.get_dtype()) + + weight_loader = weight.make_loader() + indices_loader = indices.make_loader() + indices_ndim = len(indices.get_size()) + weight_size = weight.get_size() + new_size = [*indices.get_size(), *weight_size[1:]] + + def fn(idx): + assert len(idx) == len(new_size), f"{idx} != {new_size}" + var_index = indices_loader(idx[:indices_ndim]) + weight_idx = [ops.indirect_indexing(var_index, weight_size[0])] + [ + *idx[indices_ndim:] + ] + return weight_loader(weight_idx) + + return Pointwise.create( + device=weight.get_device(), + dtype=weight.get_dtype(), + inner_fn=fn, + ranges=new_size, + ) + + +def check_and_broadcast_indices(indices, device): + assert all( + i.get_dtype() in (torch.int64, torch.int32, torch.bool, torch.uint8) + for i in indices + if i is not None + ), ( + f"indices must be int64, byte or bool. Got {[i.get_dtype() for i in indices if i is not None]}" + ) + if any( + i.get_dtype() in (torch.bool, torch.uint8) for i in indices if i is not None + ): + raise NotImplementedError("Fallback for bool indices") + + valid_idxs = [i for i, x in enumerate(indices) if isinstance(x, TensorBox)] + assert len(valid_idxs) > 0, "requires at least 1 non-None index" + new_indices = [None] * len(indices) + for i, x in zip(valid_idxs, broadcast_tensors(*[indices[i] for i in valid_idxs])): + # Eager allows indices to be CPU tensor when running on CUDA + # FIXME: Calling to_device(x, device) should work but + # test_advancedindex_mixed_cpu_devices still fails + if x.get_device() != device: + raise NotImplementedError("Fallback when indices is on a different device") + new_indices[i] = x + return new_indices, valid_idxs + + +def index_output_size_and_inner_fn( + x_size, + indices, + tensor_indices, + tensor_size, + indices_loaders, + indexed_size, + x_loader, + check, + wrap_neg=True, +): + # Note that behavior of indexing differs when there are non consecutive + # tensors. In this case, the tensor index is pulled to the beginning. + # + # Suppose a = torch.arange(3 * 4 * 5 * 6 * 7).view(3, 4, 5, 6, 7) + # x = torch.tensor[1,2] + # Then, a[:,x,:,x,:] will have shape 2,3,5,7 as due to x,:,x then 2 will + # be pulled to the front. + non_consecutive_tensors = False + for previous, current in itertools.pairwise(tensor_indices): + if current - previous != 1: + non_consecutive_tensors = True + + output_size = [x_size[i] for i, val in enumerate(indices) if val is None] + output_size = [*output_size, *x_size[len(output_size) + len(tensor_indices) :]] + + first_tensor_index = tensor_indices[0] + if non_consecutive_tensors: + output_size = tensor_size + output_size + else: + output_size = ( + output_size[:first_tensor_index] + + tensor_size + + output_size[first_tensor_index:] + ) + + def fn(idx): + assert len(idx) == len(output_size) + assert len(indices_loaders) == len(indexed_size) + + rank = len(tensor_size) + new_index = [] + first_tensor_index = tensor_indices[0] + start_offset = 0 if non_consecutive_tensors else first_tensor_index + next_idx = 0 + for i in range(tensor_indices[-1] + 1): + if i == start_offset: + next_idx += rank + if indices[i] is None: + assert next_idx < len(idx) + new_index.append(idx[next_idx]) + next_idx += 1 + else: + loader = indices_loaders[i] + assert loader is not None + size = indexed_size[i] + new_index.append( + ops.indirect_indexing( + loader(idx[start_offset : start_offset + rank]), + size, + check=check, + wrap_neg=wrap_neg, + ) + ) + new_index = [ + *new_index, + *idx[next_idx:], + ] + return new_index if x_loader is None else x_loader(new_index) + + return output_size, fn + + +def index_impl(x, indices, check): + output_size, inner_fn, _ = index_impl_helper(x, indices, check) + + return Pointwise.create( + device=x.get_device(), + dtype=x.get_dtype(), + inner_fn=inner_fn, + ranges=output_size, + ) + + +def index_impl_helper(x, indices, check, wrap_neg=True): + assert isinstance(indices, (list, tuple)) + x_loader = x.make_loader() + indices, tensor_indices = check_and_broadcast_indices(indices, x.get_device()) + assert len(tensor_indices) > 0, "Must have at least one valid idx" + + indices_loaders = [i.make_loader() if i is not None else None for i in indices] + # no guards on output size, all the guards are set in broadcast_tensors + + # We can use the first one since they are all required to be the same size + tensor_size = list(indices[tensor_indices[0]].get_size()) + + x_size = x.get_size() + + indexed_size = [x_size[i] for i in range(len(indices)) if indices[i] is not None] + if check and 0 in indexed_size and 0 not in tensor_size: + raise IndexError("index is out of bounds for dimension with size 0") + + indexed_size = [x_size[i] for i in range(len(indices))] + output_size, index_inner_fn = index_output_size_and_inner_fn( + x_size, + indices, + tensor_indices, + tensor_size, + indices_loaders, + indexed_size, + None, + check=check, + wrap_neg=wrap_neg, + ) + + def inner_fn(idx): + return x_loader(index_inner_fn(idx)) + + return output_size, inner_fn, index_inner_fn + + +@register_lowering(aten.index, type_promotion_kind=None) +def index(x, indices): + try: + return index_impl(x, indices, check=True) + except NotImplementedError: + # Fallback to ATen for boolean indexing + x.realize() + return fallback_handler(aten.index.Tensor, add_to_fallback_set=False)( + x, indices + ) + + +@register_lowering(aten._unsafe_index, type_promotion_kind=None) +def _unsafe_index(x, indices): + return index_impl(x, indices, check=False) + + +# All the indexing decompositions are written in terms of index, index_put, and index_put_ +# We cannot have this lowering as a decomposition as it introduces +# mutation in the graph, which is bad for Aot Autograd. Aot Autograd runs dead +# code elimination and common subexpression elimination optimizations, which +# assume graphs to be side-effect free. More details at +# https://github.com/pytorch/torchdynamo/issues/1235 +# and +# https://github.com/pytorch/torchdynamo/issues/1863 +@register_lowering(aten.index_put, type_promotion_kind=None) +def index_put(x, indices, values, accumulate=False): + return index_put_impl_( + clone(x), indices, values, accumulate, check=True, may_realize=False + ) + + +@register_lowering(aten._unsafe_index_put) +def _unsafe_index_put(x, indices, values, accumulate=False): + return index_put_impl_( + clone(x), indices, values, accumulate, check=False, may_realize=False + ) + + +def index_put_as_masked_fill(self, indices, value, accumulate): + if value.get_device() != self.get_device(): + value = to_device(value, self.get_device()) + if accumulate: + value = add(self, value) + return mutate_to(self, where(indices[0], value, self)) + + +def index_put_fallback(self, indices, values, accumulate): + from .utils import _fx_node_is_input_dependent_cudagraph_unsafe + + op_overload = getattr(aten.index_put_, V.graph.current_node.target._overloadname) # type: ignore[union-attr] + + # Check if any index is a boolean tensor - if so, mark as cudagraph-unsafe + # because boolean indices trigger .nonzero() during CUDA graph capture + # When graph_partition is enabled, skip - partitioning handles this + fx_node = V.graph.current_node + if ( + not config.graph_partition + and fx_node is not None + and _fx_node_is_input_dependent_cudagraph_unsafe(fx_node) + ): + msg = "index_put_ fallback with boolean indexing is not compatible with CUDA graphs" + if stack_trace := fx_node.meta.get("stack_trace", None): + msg = f"{msg} Found from : \n {stack_trace}" + V.graph.disable_cudagraphs_reason = msg + + ir.IndexPutFallback(op_overload, self, indices, values, accumulate) + return self + + +@register_lowering(aten.index_put_, type_promotion_kind=None) +def index_put_(self, indices, values, accumulate=False): + return index_put_impl_( + self, indices, values, accumulate, check=True, may_realize=True + ) + + +@register_lowering(inductor_prims._unsafe_index_put_, type_promotion_kind=None) +def _unsafe_index_put_(self, indices, values, accumulate=False): + return index_put_impl_( + self, indices, values, accumulate, check=False, may_realize=True + ) + + +def index_put_impl_(self, indices, values, accumulate, check, may_realize=False): + if may_realize: + + def indice_slice_from_randperm(indice): + # Refer to: https://github.com/pytorch/pytorch/pull/139366#discussion_r1825424660 + # For this specific pattern, indices is unique as coming from torch.randperm. + # However, as the content of the indices is unknown, we have to check this specific pattern. + if isinstance(indice, TensorBox) and isinstance(indice.data, ir.BaseView): + indice = indice.data.unwrap_view() + return ( + isinstance(indice, ir.StorageBox) + and isinstance(indice.data, ir.ExternKernel) + and getattr(indice.data, "fx_node", None) + and indice.data.fx_node.target is torch.ops.aten.randperm.default + ) + return False + + if ir.try_get_name(self) in values.get_read_names() and not all( + indice_slice_from_randperm(indice) for indice in indices + ): + # Fix issue: https://github.com/pytorch/pytorch/issues/138908 + # When self and values have memory overlapping, indices may + # contain duplicate values, potentially causing incorrect results since + # the load of `values` might contain modified value from the store of `self`. + # To address this, store values in a temporary buffer in such cases. + values.realize() + + # Dispatch to masked fill for single boolean index with single value + if ( + values.get_numel() == 1 + and len(indices) == 1 + and indices[0].get_dtype() in (torch.bool, torch.uint8) + ): + mask = indices[0] + for _ in range(len(mask.get_size()), len(self.get_size())): + mask = unsqueeze(mask, -1) + return index_put_as_masked_fill(self, [mask], values, accumulate) + + # Fallback in torch deterministic mode + if torch.are_deterministic_algorithms_enabled(): + return index_put_fallback(self, indices, values, accumulate) + + # Fallback if there is a boolean index + for index in indices: + if index is not None and index.get_dtype() in (torch.bool, torch.uint8): + return index_put_fallback(self, indices, values, accumulate) + + x_size = self.get_size() + x_ndim = len(x_size) + + if accumulate and needs_fallback_due_to_atomic_add_limitations(self.get_dtype()): + # self is an scalar Tensor + if x_ndim == 0: + self = view(self, [1]) + self = index_put_fallback(self, indices, values, accumulate) + if x_ndim == 0: + self = view(self, []) + return self + + values = to_dtype(values, self.get_dtype()) + + try: + # Note that code will only get here when dtype is uint32 + indices, tensor_indices = check_and_broadcast_indices( + indices, self.get_device() + ) + except NotImplementedError: + return index_put_fallback(self, indices, values, accumulate) + + indices_loaders = [i.make_loader() if i is not None else None for i in indices] + + assert isinstance(self, TensorBox) + self.realize() + + # self is an scalar Tensor + if x_ndim == 0: + self = view(self, [1]) + + # We can use the first one since they are all required to be the same size + tensor_size = list(indices[tensor_indices[0]].get_size()) + indexed_size = [x_size[i] for i in range(len(indices))] + + expected_vals_size, inner_fn = index_output_size_and_inner_fn( + x_size, + indices, + tensor_indices, + tensor_size, + indices_loaders, + indexed_size, + None, + check=check, + ) + values = expand(values, expected_vals_size) + # all guards are set above during broadcast_tensors and expand + + device = self.get_device() + assert device is not None + scatter = ir.Scatter( + device=device, + dtype=self.get_dtype(), + inner_fn=values.make_loader(), + ranges=expected_vals_size, # iter_ranges, + output_indexer=inner_fn, + scatter_mode="atomic_add" if accumulate else None, + ) + buffer = ir.ComputedBuffer( + name=None, + layout=ir.MutationLayoutSHOULDREMOVE(self), + data=scatter, + ) + buffer.name = V.graph.register_buffer(buffer) + V.graph.register_operation(buffer) + + if x_ndim == 0: + self = view(self, []) + return self + + +fallback__unsafe_masked_index = fallback_handler( + aten._unsafe_masked_index.default, add_to_fallback_set=False +) + +fallback__unsafe_masked_index_put_accumulate = fallback_handler( + aten._unsafe_masked_index_put_accumulate.default, add_to_fallback_set=False +) + + +@register_lowering(aten._unsafe_masked_index, type_promotion_kind=None) +def _unsafe_masked_index(self, mask, indices, fill): + ranges, _, _unsafe_index_fn = index_impl_helper( + self, indices, check=False, wrap_neg=False + ) + mask_loader = mask.make_loader() + self_loader = self.make_loader() + + def inner_fn(idx): + if mask.dtype != torch.bool: + mask_val = ops.to_dtype(mask_loader(idx), torch.bool) + else: + mask_val = mask_loader(idx) + return ops.masked(mask_val, lambda: self_loader(_unsafe_index_fn(idx)), fill) + + return Pointwise.create( + device=self.get_device(), + dtype=self.get_dtype(), + inner_fn=inner_fn, + ranges=ranges, + ) + + +@register_lowering(aten._unsafe_masked_index_put_accumulate, type_promotion_kind=None) +def _unsafe_masked_index_put_accumulate(x, mask, indices, values): + masked_value = where(mask, values, 0) + shape = x.get_size() + clamped_indices = [ + clamp(indices[i], -shape[i], shape[i] - 1) if indices[i] else None + for i in range(len(indices)) + ] + # TODO: use a masked store for this. currently only triton + # supports masked stores and cpp backend does not. + return _unsafe_index_put(x, clamped_indices, masked_value, accumulate=True) + + +@make_pointwise +def clamp(a, min, max): + return ops.maximum(min, ops.minimum(max, a)) + + +@register_lowering(aten.as_strided_scatter, type_promotion_kind=None) +def as_strided_scatter(self, src, size, stride, storage_offset=None): + output = clone(self) + output_view = as_strided(output, size, stride, storage_offset) + copy_(output_view, src) + return output + + +@register_lowering(aten.scatter, type_promotion_kind=None) +def scatter(x, dim: int, index, src, **kwargs): + return scatter_(clone(x), dim, index, src, **kwargs) + + +def scatter_fallback( + op_overload: torch._ops.OpOverload, + self, + dim: int, + index, + src, + *, + reduce: str | None = None, + include_self: bool = True, +): + src_is_tensor = isinstance(src, TensorBox) + if use_scatter_fallback( + op_overload, + reduce, + self.get_dtype(), + cast(torch.dtype, src.get_dtype() if src_is_tensor else type(src)), + # pyrefly: ignore [missing-attribute] + src.get_device().type if src_is_tensor else "not impl", + src_is_tensor, + ): + ir.ScatterFallback( + op_overload, + self, + dim, + index, + src, + reduce=reduce, + include_self=include_self, + ) + return self + + return None + + +@register_lowering(aten.scatter_, type_promotion_kind=None) +def scatter_(self, dim: int, index, src, *, reduce: str | None = None): + assert reduce in (None, "add", "multiply") + if reduce is None: + op_overload = getattr(aten.scatter_, V.graph.current_node.target._overloadname) # type: ignore[union-attr] + fallback_result = scatter_fallback( + op_overload, self, dim, index, src, reduce=reduce + ) + if fallback_result is not None: + return fallback_result + + if reduce == "add": + reduce = "sum" + elif reduce == "multiply": + reduce = "prod" + return scatter_reduce_(self, dim, index, src, reduce) + + +@register_lowering(aten.scatter_add, type_promotion_kind=None) +def scatter_add(x, dim: int, index, src): + return scatter_add_(clone(x), dim, index, src) + + +@register_lowering(aten.scatter_add_, type_promotion_kind=None) +def scatter_add_(x, dim: int, index, src): + return scatter_reduce_(x, dim, index, src, "sum") + + +@register_lowering(aten.scatter_reduce, type_promotion_kind=None) +def scatter_reduce(x, dim: int, index, src, reduction_type, **kwargs): + return scatter_reduce_(clone(x), dim, index, src, reduction_type, **kwargs) + + +@register_lowering(aten.scatter_reduce_, type_promotion_kind=None) +def scatter_reduce_(self, dim: int, index, src, reduce, *, include_self: bool = True): + assert reduce in (None, "sum", "prod", "mean", "amax", "amin") + assert ( + len(aten.scatter_reduce_.overloads()) == 1 + and "two" in aten.scatter_reduce_.overloads() + ), "aten.scatter_reduce_.two is not the unique overload of aten.scatter_reduce_" + + if isinstance(src, Number): + src = full_like(self, src) + + fallback_result = scatter_fallback( + aten.scatter_reduce_.two, + self, + dim, + index, + src, + reduce=reduce, + include_self=include_self, + ) + + if fallback_result: + return fallback_result + + assert isinstance(self, TensorBox) + assert "int" in str(index.get_dtype()) + + ndim = len(self.get_size()) + if ndim == 0: + self = view(self, [1]) + + if isinstance(src, TensorBox) and len(src.get_size()) == 0: + src = view(src, [1]) + + if isinstance(index, TensorBox) and len(index.get_size()) == 0: + index = view(index, [1]) + + if index.get_numel() == 0: + return self + + dim = _validate_dim(self, dim) + + self.realize() + index_loader = index.make_loader() + src_loader = src.make_loader() if isinstance(src, TensorBox) else None + + def output_indexer(idx): + # self is captured from the end of the function, so it may have 0 dim + shape = self.get_size() + ndim = len(shape) + indirect_idx = list(idx) + indirect_idx[dim] = ops.indirect_indexing( + index_loader(idx), 1 if ndim == 0 else shape[dim], wrap_neg=False + ) + return indirect_idx + + def fn(idx): + if src_loader: + return src_loader(idx) + else: + # src is a scalar + # pyrefly: ignore [bad-argument-type] + return ops.constant(src, self.get_dtype()) + + def backend_reduce_str(reduce): + if reduce == "sum": + return "atomic_add" + else: + # TODO: Need to support more reduction type + assert reduce is None + return None + + device = self.get_device() + assert device is not None + + if not include_self: + # zero out the corresponding elements first + zero_out = ir.Scatter( + device=device, + dtype=self.get_dtype(), + inner_fn=lambda index: ops.constant(0, self.get_dtype()), + ranges=index.get_size(), + output_indexer=output_indexer, + scatter_mode=None, + ) + buffer = ir.ComputedBuffer( + name=None, + layout=ir.MutationLayoutSHOULDREMOVE(self), + data=zero_out, + ) + buffer.name = V.graph.register_buffer(buffer) + V.graph.register_operation(buffer) + + # self[index[i][j][k]][j][k] += src[i][j][k] # if dim == 0 + # self[i][index[i][j][k]][k] += src[i][j][k] # if dim == 1 + # self[i][j][index[i][j][k]] += src[i][j][k] # if dim == 2 + scatter = ir.Scatter( + device=device, + dtype=self.get_dtype(), + inner_fn=fn, + ranges=index.get_size(), + output_indexer=output_indexer, + scatter_mode=backend_reduce_str(reduce), + ) + buffer = ir.ComputedBuffer( + name=None, + layout=ir.MutationLayoutSHOULDREMOVE(self), + data=scatter, + ) + buffer.name = V.graph.register_buffer(buffer) + V.graph.register_operation(buffer) + + if ndim == 0: + self = view(self, []) + return self + + +def upsample_nearestnd( + x, + output_size, + scales_x: tuple[float | None, ...], + n: int = 2, + exact: bool = False, +): + x.realize_hint() # elements are reused + x_loader = x.make_loader() + i_sizes = x.get_size()[-n:] + batch = x.get_size()[:-n] + i_sizes = [V.graph.sizevars.guard_int(i) for i in i_sizes] + + assert len(scales_x) == n + o_sizes = output_size + + inv_scales = [i / o for i, o in zip(i_sizes, o_sizes)] + for i, scale in enumerate(scales_x): + if scale is not None: + inv_scales[i] = 1.0 / scale + + def scale_fn(x, scale, size): + # Nearest Exact: input_index = round(scale * (output_index + 0.5) - 0.5) + # = floor(scale * (output_index + 0.5)) + # Nearest: input_index = floor(scale * output_index) + x = ops.index_expr(x, torch.float32) + if exact: + x = ops.add(x, ops.constant(0.5, torch.float32)) + x = ops.mul(x, ops.constant(scale, torch.float32)) + x = ops.to_dtype(x, torch.int32) + return ops.indirect_indexing(x, size, check=False) + + def fn(idx): + x = idx[-n:] + b = idx[:-n] + return x_loader( + [*b, *[scale_fn(i, s, size) for i, s, size in zip(x, inv_scales, i_sizes)]] + ) + + return Pointwise.create( + device=x.get_device(), + dtype=x.get_dtype(), + inner_fn=fn, + ranges=[*batch, *o_sizes], + ) + + +@register_lowering(aten.upsample_nearest1d.default) +def upsample_nearest1d(x, output_size, scales: float | None = None): + return upsample_nearestnd(x, output_size, (scales,), n=1) + + +@register_lowering(aten._upsample_nearest_exact1d.default) +def _upsample_nearest_exact1d(x, output_size, scales: float | None = None): + return upsample_nearestnd(x, output_size, (scales,), n=1, exact=True) + + +@register_lowering(aten.upsample_nearest2d.default) +def upsample_nearest2d( + x, output_size, scales_h: float | None = None, scales_w: float | None = None +): + return upsample_nearestnd(x, output_size, (scales_h, scales_w), n=2) + + +@register_lowering(aten._upsample_nearest_exact2d.default) +def _upsample_nearest_exact2d( + x, output_size, scales_h: float | None = None, scales_w: float | None = None +): + return upsample_nearestnd(x, output_size, (scales_h, scales_w), n=2, exact=True) + + +@register_lowering(aten.upsample_nearest3d.default) +def upsample_nearest3d( + x, + output_size, + scales_d: float | None = None, + scales_h: float | None = None, + scales_w: float | None = None, +): + return upsample_nearestnd(x, output_size, (scales_d, scales_h, scales_w), n=3) + + +@register_lowering(aten._upsample_nearest_exact3d.default) +def _upsample_nearest_exact3d( + x, + output_size, + scales_d: float | None = None, + scales_h: float | None = None, + scales_w: float | None = None, +): + return upsample_nearestnd( + x, output_size, (scales_d, scales_h, scales_w), n=3, exact=True + ) + + +def _create_constants(*args, dtype): + return tuple(ops.constant(a, dtype) for a in args) + + +@register_lowering(prims.rev.default) +def rev(x, dims): + # note - dims pre-canonicalized + x_loader = x.make_loader() + sizes = x.get_size() + + def loader(idx): + idx = list(idx) + assert len(idx) == len(sizes) + for dim in dims: + idx[dim] = (sizes[dim] - 1) - idx[dim] + + return x_loader(idx) + + return Pointwise.create( + device=x.get_device(), + dtype=x.get_dtype(), + inner_fn=loader, + ranges=sizes, + ) + + +def inplace_constant_pad_nd( + x: TensorBox, padding: Sequence[int], fill_value: float +) -> TensorBox | None: + """ + This optimization changes the semantics of padding from 'clone' + style to 'view' style. + + Thanks to functionalization, this change can still maintain numerical + correctness. + """ + + def _padding_can_be_fused(): + """ + Conservatively check if padding can be fused with downstream op. + 1. if the downstream op is a sum, then there is little benefit to + do inplace padding + 2. if the downstream op is a matmul, doing inplace padding can + save membw. + """ + current_node = V.graph.current_node + if current_node is None: + return True # be conservative + users = tuple(current_node.users) + if len(users) == 1 and users[0].target in ( + aten.mm.default, + aten.addmm.default, + ): + return False + + return True # be conservative + + if _padding_can_be_fused(): + return None + + # Only handle 2D case for now + if len(padding) != 4 or len(x.get_size()) != 2: + return None + + # No harm to realize since we already know that + # the op can not be fused into the single user. + # It need to be realized later anyways. + x.realize() + + # If x is a view (e.g. a SliceView), realizing it just realizing the + # underlying storage. x itself is still a view. + if ( + not isinstance(x, ir.TensorBox) + or not isinstance(x.data, ir.StorageBox) + or not ( + isinstance(x.data.data, ir.ComputedBuffer) + or ( + config.can_inplace_pad_graph_input + and isinstance(x.data.data, ir.InputBuffer) + ) + ) + or not x.data.data.name + ): + return None + x.freeze_layout() + + _, layout = ir.as_storage_and_layout(x) + strides = layout.stride + if strides[1] != 1: + return None + + if padding[0] != 0 or padding[2] != 0 or padding[3] != 0: + return None + + npad = padding[1] + if npad == 0: + return None + + stride0 = strides[0] + rowsize = layout.size[1] + + if stride0 < rowsize + npad: + return None + + bufname = x.data.data.name + padded_size = [layout.size[0], layout.size[1] + npad] + V.graph.buffer_to_padded_size[bufname] = padded_size + resized_x = as_strided( + x, + padded_size, + layout.stride, + layout.offset, + ) + + sliced_x = slice_(resized_x, dim=1, start=rowsize, end=rowsize + npad, clamp=False) + fill_(sliced_x, fill_value) + + counters["inductor"]["inplace_padding"] += 1 + return resized_x + + +def _pad_as_cat( + x: TensorBox, padding: Sequence[int], fill_value: float +) -> TensorBox | None: + """Decompose right-pad into cat([x, fill], dim) and delegate to cat lowering. + + The cat lowering already has heuristics for choosing between pointwise_cat + (fusion) and ConcatKernel (memory planning / zero-copy). By routing through + cat() we reuse those heuristics rather than duplicating them here. + """ + # Bail out for symbolic padding, dynamic shapes + if not all(isinstance(p, int) for p in padding): + return None + + sizes = x.get_size() + ndim = len(sizes) + pad_pairs = list(zip(padding[::2], padding[1::2])) + + # Only support single-dimension right-pad + pad_dim = None + pad_amount = None + for i, (left, right) in enumerate(pad_pairs): + if left != 0: + return None + if right > 0: + if pad_dim is not None: + return None # multi-dim pad + pad_dim = ndim - 1 - i # padding format is reversed dim order + pad_amount = right + elif right < 0: + return None # trim, not pad + + if pad_dim is None: + return None + + # CPU cat always uses ConcatKernel (no pointwise_cat), which adds + # extra kernel launches for the fill. Skip pad-as-cat on CPU. + device = x.get_device() + if device is not None and device.type == "cpu": + return None + + # Build the fill tensor for the padding region + pad_shape = list(sizes) + pad_shape[pad_dim] = pad_amount + dtype = x.get_dtype() + fill_value_typed = dtype_to_type(dtype)(fill_value) + pad_tensor = tensor_constructor(fill_value_typed)( + pad_shape, dtype=dtype, device=device + ) + + counters["inductor"]["pad_rewritten_as_cat"] += 1 + return cat([x, pad_tensor], pad_dim) + + +@register_lowering(aten.constant_pad_nd, type_promotion_kind=None) +def constant_pad_nd(x, padding, fill_value=0): + assert (len(padding) % 2) == 0 + if all(p == 0 for p in padding): + return clone(x) + + if config.inplace_padding: + out = inplace_constant_pad_nd(x, padding, fill_value) + if out: + return out + # fall through if can not inplace the padding + + out = _pad_as_cat(x, padding, fill_value) + if out is not None: + return out + + sizes = x.get_size() + + bounds = list(reversed(list(zip(padding[::2], padding[1::2])))) + n = len(sizes) - len(bounds) + + # if padding is a complicated expression, hoist it + bounds_precomp: list[tuple[sympy.Symbol, Any]] = [] + for l, h in bounds: + bounds_precomp.append((V.graph.sizevars.lookup_precomputed_size(l), h)) # type: ignore[arg-type] + + output_size = list(sizes[:n]) + mask_sizes = [] + for (low, high), size in zip(bounds, sizes[n:]): + mask_sizes.append(size) + output_size.append(sympy.expand(size + low + high)) + assert len(output_size) == len(sizes) + fill_value = dtype_to_type(x.get_dtype())(fill_value) + + def mask(index): + mask = [] + for idx, (low, high), length in zip(index[n:], bounds, mask_sizes): + if low != 0: + mask.append(range_mask_low(idx, 0)) + if high != 0: + mask.append(range_mask_high(idx, length)) + mask = functools.reduce(ops.and_, mask) + return ops.masked(mask, lambda: x_loader(index), fill_value) + + def offset_fn(index): + new_index = list(index[:n]) + for idx, (low, _high) in zip(index[n:], bounds_precomp): + new_index.append(idx - low) + assert len(new_index) == len(index) + return mask(new_index) + + x_loader = x.make_loader() + return Pointwise.create( + device=x.get_device(), + dtype=x.get_dtype(), + inner_fn=offset_fn, + ranges=output_size, + ) + + +def range_mask_low(i: sympy.Expr, low: sympy.Expr | int): + return ops.ge( + ops.index_expr(i, torch.int64), + ops.index_expr(sympy.Integer(low), torch.int64), + ) + + +def range_mask_high(i: sympy.Expr, high: sympy.Expr): + return ops.lt( + ops.index_expr(i, torch.int64), + ops.index_expr(high, torch.int64), + ) + + +def range_mask(i: sympy.Expr, high: sympy.Expr, low: sympy.Expr): + return ops.and_( + range_mask_low(i, low), + range_mask_high(i, high), + ) + + +def constant_boundary_condition( + x, fill_value, padding=None, pad_fill_value=1.0, dim=None +): + h = x.get_size()[-dim:] + x_loader = x.make_loader() + # pyrefly: ignore [unsupported-operation] + padding_h = padding or [0] * dim + + def load(index): + prefix = index[:-dim] + ih = index[-dim:] + + mask = functools.reduce( + ops.and_, + # pyrefly: ignore [bad-argument-type, no-matching-overload] + [range_mask(ih[i], h[i] + padding_h[i], -padding_h[i]) for i in range(dim)], + ) + return ( + ops.masked( + mask, + lambda: constant_boundary_condition(x, pad_fill_value, dim=dim)( + [*prefix, *ih] + ), + fill_value, + ) + if padding + else ops.masked(mask, lambda: x_loader([*prefix, *ih]), fill_value) + ) + + return load + + +def pooling_size(x, i, kernel_size, stride, padding, ceil_mode, *, dilation=None): + if dilation is None: + dilation = [1] * len(padding) + + x_out = FloorDiv( + x + 2 * padding[i] - dilation[i] * (kernel_size[i] - 1) + (stride[i] - 1), + stride[i], + ) + + if ceil_mode: + x_alt = FloorDiv( + x + + 2 * padding[i] + - dilation[i] * (kernel_size[i] - 1) + + 2 * (stride[i] - 1), + stride[i], + ) + if V.graph.sizevars.guard_or_false( + sympy.Ge((x_alt - 1) * stride[i] - x - padding[i], 0) + ): + # Sliding windows must start within the input or left padding + x_alt -= 1 # type: ignore[assignment] + if V.graph.sizevars.guard_or_false(sympy.Eq(x_out, x_alt)): + # ceil mode is actually a no-op, lets guard on that + ceil_mode = False + else: + x_out = x_alt + return x_out, ceil_mode + + +def should_fallback_max_pool_with_indices(kernel_size, *, n_dim): + kernel_size = pad_listlike(kernel_size, n_dim) + window_size = functools.reduce(operator.mul, kernel_size) + return window_size > 25 + + +def max_pool_checks( + x, kernel_size, stride, padding, dilation, n_dim, *, assert_fallback=None +): + if padding == 0: + padding = [0] * n_dim + if dilation == 1: + dilation = [1] * n_dim + if not stride: + stride = kernel_size + + kernel_size = pad_listlike(kernel_size, n_dim) + stride = pad_listlike(stride, n_dim) + padding = pad_listlike(padding, n_dim) + dilation = pad_listlike(dilation, n_dim) + + assert isinstance(x, TensorBox) + assert len(kernel_size) == n_dim + assert len(stride) == n_dim + assert len(padding) == n_dim + assert len(dilation) == n_dim + assert len(x.get_size()) in (n_dim + 1, n_dim + 2) + + use_fallback = should_fallback_max_pool_with_indices(kernel_size, n_dim=n_dim) + if assert_fallback is not None: + assert use_fallback == assert_fallback + + return kernel_size, stride, padding, dilation, use_fallback + + +def _max_pool_with_offsets( + x, + kernel_size, + stride, + padding, + dilation, + ceil_mode, + *, + n_dim, +): + x.realize_hint() + batch = x.shape[:-n_dim] + dhw = x.shape[-n_dim:] + + dhw_out, ceil_mode = zip( + *[ + pooling_size( + dhw[d], d, kernel_size, stride, padding, ceil_mode, dilation=dilation + ) + for d in range(n_dim) + ] + ) + + dtype = x.dtype + min_value = ( + False + if dtype is torch.bool + else (float("-inf") if dtype.is_floating_point else torch.iinfo(dtype).min) + ) + + new_size = list(batch) + list(dhw_out) + if any(padding) or any(ceil_mode) or any(d > 1 for d in dilation): + x_loader = constant_boundary_condition(x, min_value, dim=n_dim) + else: + x_loader = x.make_loader() + + def fn_inner(idx, reduction_idx): + prefix = idx[:-n_dim] + bh = idx[-n_dim:] + ih = [ + (bh[i] * stride[i]) + (reduction_idx[i] * dilation[i]) - padding[i] + for i in range(n_dim) + ] + return x_loader([*prefix, *ih]) + + result = Reduction.create( + reduction_type="max", + input_node=x, + device=x.get_device(), + dst_dtype=dtype, + src_dtype=dtype, + inner_fn=fn_inner, + ranges=new_size, + reduction_ranges=kernel_size, + ) + offsets = Reduction.create( + reduction_type="argmax", + input_node=x, + device=x.get_device(), + dst_dtype=torch.int64, + src_dtype=dtype, + inner_fn=fn_inner, + ranges=new_size, + reduction_ranges=kernel_size, + ) + if isinstance(result.data.data, Reduction): # type: ignore[attr-defined, union-attr] + # Only realize if reduction isn't unrolled + result.realize() + if isinstance(offsets.data.data, Reduction): # type: ignore[attr-defined, union-attr] + # Only realize if reduction isn't unrolled + offsets.realize() + + return result, offsets + + +@register_lowering(prims._low_memory_max_pool_with_offsets, type_promotion_kind=None) +def _low_memory_max_pool_with_offsets( + x, + kernel_size, + stride, + padding, + dilation, + ceil_mode=False, +): + n_dim = len(kernel_size) + + # assert we are not on a fallback path, the inductor decomp should have guaranteed this + kernel_size, stride, padding, dilation, _ = max_pool_checks( + x, + kernel_size, + stride, + padding, + dilation, + n_dim, + assert_fallback=False, + ) + + with config.patch(unroll_reductions_threshold=25): + result, offsets = _max_pool_with_offsets( + x, + kernel_size, + stride, + padding, + dilation, + ceil_mode, + n_dim=n_dim, + ) + return result, to_dtype(offsets, torch.int8) + + +def _pool_offsets_to_indices( + offsets: TensorBox, + kernel_size: Sequence[int | torch.SymInt], + input_size: Sequence[int | torch.SymInt], + increments_to_index: Callable[ + [Sequence[int | torch.SymInt], Sequence[int | torch.SymInt]], + torch._inductor.virtualized.OpsValue, + ], +) -> TensorBox: + n_dim = len(kernel_size) + offsets_loader = offsets.make_loader() + window_size = sympy.sympify(functools.reduce(operator.mul, kernel_size)) + + def offsets_to_indices(idx): + offset = offsets_loader(idx) + offset_sympy = ops.indirect_indexing(offset, window_size) + reduction_idx = inductor_prims._flattened_index_to_nd(offset_sympy, kernel_size) + idhw = increments_to_index(idx, reduction_idx) + return ops.index_expr( + inductor_prims._flatten_index(idhw, input_size[-n_dim:]), torch.int64 + ) + + indices = Pointwise.create( + device=offsets.get_device(), + dtype=torch.int64, + inner_fn=offsets_to_indices, + ranges=offsets.get_size(), + ) + return indices + + +@register_lowering( + prims._low_memory_max_pool_offsets_to_indices, type_promotion_kind=None +) +def _low_memory_max_pool_offsets_to_indices( + offsets, kernel_size, input_size, stride, padding, dilation +): + # TODO: Generalize to other max pooling flavors + n_dim = len(kernel_size) + + def increments_to_index(idx, reduction_idx): + bh = idx[-n_dim:] + return [ + (bh[i] * stride[i]) + (reduction_idx[i] * dilation[i]) - padding[i] + for i in range(n_dim) + ] + + return _pool_offsets_to_indices( + offsets, kernel_size, input_size, increments_to_index + ) + + +def _max_pool_with_indices( + x, + kernel_size, + stride, + padding, + dilation, + ceil_mode, + n_dim, +): + kernel_size, stride, padding, dilation, _ = max_pool_checks( + x, kernel_size, stride, padding, dilation, n_dim=n_dim + ) + + out, offsets = _max_pool_with_offsets( + x, kernel_size, stride, padding, dilation, ceil_mode, n_dim=n_dim + ) + + indices = _low_memory_max_pool_offsets_to_indices( + offsets, + kernel_size, + x.shape[-n_dim:], + stride, + padding, + dilation, + ) + + return out, indices + + +# Fallback when we do not decompose to the low-memory path. +@register_lowering(aten.max_pool2d_with_indices, type_promotion_kind=None) +def max_pool2d_with_indices( + x, + kernel_size, + stride=None, + padding=0, + dilation=1, + ceil_mode=False, +): + return _max_pool_with_indices( + x, kernel_size, stride, padding, dilation, ceil_mode, n_dim=2 + ) + + +# Fallback when we do not decompose to the low-memory path. +@register_lowering(aten.max_pool3d_with_indices, type_promotion_kind=None) +def max_pool3d_with_indices( + x, + kernel_size, + stride=None, + padding=0, + dilation=1, + ceil_mode=False, +): + return _max_pool_with_indices( + x, kernel_size, stride, padding, dilation, ceil_mode, n_dim=3 + ) + + +fallback_max_pool2d_with_indices_backward = fallback_handler( + aten.max_pool2d_with_indices_backward.default, + add_to_fallback_set=False, +) + + +@register_lowering(aten.max_pool2d_with_indices_backward, type_promotion_kind=None) +def max_pool2d_with_indices_backward( + grad_output, x, kernel_size, stride, padding, dilation, ceil_mode, indices +): + if padding == 0: + padding = [0, 0] + if dilation == 1: + dilation = [1, 1] + if not stride: + stride = kernel_size + + assert isinstance(x, TensorBox) + assert len(kernel_size) == 2 + assert len(stride) == 2 + assert len(padding) == 2 + assert len(dilation) == 2 + assert len(x.get_size()) in (3, 4) + + # we will read this many times, so make sure it is computed + grad_output.realize_hint() + gO_stride = grad_output.maybe_get_stride() + x_stride: Sequence[Any] | None + if isinstance(x, TensorBox) and isinstance(x.data.data, Pointwise): # type: ignore[attr-defined] + data = x.data.data # type: ignore[attr-defined] + device = data.get_device() + assert device is not None + x_buffer = ir.ComputedBuffer( + name=None, + layout=ir.FlexibleLayout( + device=device, + dtype=data.get_dtype(), + size=data.get_size(), + ), + data=data, + ) + x_buffer.decide_layout() + x_stride = x_buffer.get_stride() + else: + x_stride = x.maybe_get_stride() + + is_channels_last = (x_stride is not None and x_stride[1] == 1) or ( + gO_stride is not None and gO_stride[1] == 1 + ) + if any(d != 1 for d in dilation): + # dilation NYI + return fallback_max_pool2d_with_indices_backward( + grad_output, x, kernel_size, stride, padding, dilation, ceil_mode, indices + ) + + *_batch, _height, width = x.get_size() + *_, pooled_height, pooled_width = grad_output.get_size() + + indices_loader = indices.make_loader() + grad_loader = grad_output.make_loader() + new_size = list(x.get_size()) + + h_window_size = max( + max(FloorDiv(h, stride[0]) - max(0, FloorDiv(h - kernel_size[0], stride[0])), 1) + for h in range(kernel_size[0] * 2) + ) + w_window_size = max( + max(FloorDiv(w, stride[1]) - max(0, FloorDiv(w - kernel_size[1], stride[1])), 1) + for w in range(kernel_size[1] * 2) + ) + + window_size = h_window_size * w_window_size + + if window_size > 25: + # Kernel size too big. Results in hard-to-optimize Triton code. Use fallback. + return fallback_max_pool2d_with_indices_backward( + grad_output, x, kernel_size, stride, padding, dilation, ceil_mode, indices + ) + + indices_size = indices.get_size() + + def fn(idx): + *prefix, h, w = idx + index_test = ops.index_expr(h * width + w, torch.int32) + h = h + padding[0] + w = w + padding[1] + phstart = ops.index_expr( + FloorDiv(h - kernel_size[0] + stride[0], stride[0]), torch.int32 + ) + pwstart = ops.index_expr( + FloorDiv(w - kernel_size[1] + stride[1], stride[1]), torch.int32 + ) + phend = ops.index_expr(FloorDiv(h, stride[0]) + 1, torch.int32) + pwend = ops.index_expr(FloorDiv(w, stride[1]) + 1, torch.int32) + + phstart = ops.maximum(phstart, ops.constant(0, torch.int32)) + pwstart = ops.maximum(pwstart, ops.constant(0, torch.int32)) + phend = ops.minimum(phend, ops.index_expr(pooled_height, torch.int32)) + pwend = ops.minimum(pwend, ops.index_expr(pooled_width, torch.int32)) + + gradient = None + for ph_ in range(h_window_size): + for pw_ in range(w_window_size): + ph = ops.add(phstart, ops.constant(ph_, torch.int32)) + pw = ops.add(pwstart, ops.constant(pw_, torch.int32)) + grad_index = [ + *prefix, + ops.indirect_indexing( + ops.minimum(ph, ops.sub(phend, ops.constant(1, torch.int32))), + indices_size[-2], + check=False, + ), + ops.indirect_indexing( + ops.minimum(pw, ops.sub(pwend, ops.constant(1, torch.int32))), + indices_size[-1], + check=False, + ), + ] + + index_actual = indices_loader(grad_index) + grad_part = grad_loader(grad_index) + check = ops.eq(index_actual, index_test) + + if gradient is None: + # don't need mask for 0, 0 + gradient = ops.where( + check, grad_part, ops.constant(0.0, torch.float32) + ) + else: + mask = ops.and_( + ops.and_( + ops.lt(ph, phend), + ops.lt(pw, pwend), + ), + check, + ) + gradient = ops.where(mask, ops.add(gradient, grad_part), gradient) + assert gradient is not None + return gradient + + out = Pointwise.create( + device=grad_output.get_device(), + dtype=grad_output.get_dtype(), + inner_fn=fn, + ranges=new_size, + ) + if is_channels_last: + return ir.ExternKernel.require_channels_last(out) + else: + return out + + +def pad_adaptive_loader(x, pad_val=0.0): + x_loader = x.make_loader() + + def load(prefix, increments, start_indices, end_indices): + ih, iw = increments + h_start_index, w_start_index = start_indices + h_end_index, w_end_index = end_indices + + mask = ops.and_( + ops.lt( + ops.index_expr(h_start_index + ih, torch.int64), + ops.index_expr(h_end_index, torch.int64), + ), + ops.lt( + ops.index_expr(w_start_index + iw, torch.int64), + ops.index_expr(w_end_index, torch.int64), + ), + ) + + return ops.masked( + mask, + lambda: x_loader([*prefix, h_start_index + ih, w_start_index + iw]), + pad_val, + ) + + return load + + +def compute_indices_adaptive_pooling(start_index, end_index, h_in, w_in, h_out, w_out): + h_start_index = functools.partial(start_index, out_dim=h_out, inp_dim=h_in) + h_end_index = functools.partial(end_index, out_dim=h_out, inp_dim=h_in) + + w_start_index = functools.partial(start_index, out_dim=w_out, inp_dim=w_in) + w_end_index = functools.partial(end_index, out_dim=w_out, inp_dim=w_in) + + return h_start_index, h_end_index, w_start_index, w_end_index + + +def _adaptive_pooling_fn( + start_index, end_index, kernel_maxes, in_sizes, out_sizes, pooling_fn +): + h_in, w_in = in_sizes + h_out, w_out = out_sizes + + ( + h_start_index_fn, + h_end_index_fn, + w_start_index_fn, + w_end_index_fn, + ) = compute_indices_adaptive_pooling( + start_index, end_index, h_in, w_in, h_out, w_out + ) + + def fn(idx, loader): + *prefix, bh, bw = idx + + h_start_index = h_start_index_fn(bh) + h_end_index = h_end_index_fn(bh) + + w_start_index = w_start_index_fn(bw) + w_end_index = w_end_index_fn(bw) + + result = None + for ih, iw in itertools.product(range(kernel_maxes[0]), range(kernel_maxes[1])): + val = loader( + prefix, + [ih, iw], + [h_start_index, w_start_index], + [h_end_index, w_end_index], + ) + if result is None: + result = val + else: + result = pooling_fn(val, result) + return result + + return fn + + +def _adaptive_pooling_fn_with_idx( + start_index, end_index, kernel_maxes, in_sizes, out_sizes, pooling_fn +): + h_in, w_in = in_sizes + h_out, w_out = out_sizes + + ( + h_start_index_fn, + h_end_index_fn, + w_start_index_fn, + w_end_index_fn, + ) = compute_indices_adaptive_pooling( + start_index, end_index, h_in, w_in, h_out, w_out + ) + + def fn(idx, loader): + *prefix, bh, bw = idx + + h_start_index = h_start_index_fn(bh) + h_end_index = h_end_index_fn(bh) + + w_start_index = w_start_index_fn(bw) + w_end_index = w_end_index_fn(bw) + + maxval = None + maxindex = None + for ih, iw in itertools.product(range(kernel_maxes[0]), range(kernel_maxes[1])): + val = loader( + prefix, + [ih, iw], + [h_start_index, w_start_index], + [h_end_index, w_end_index], + ) + + index = ops.index_expr( + (h_start_index + ih) * w_in + w_start_index + iw, torch.int64 + ) + + if maxindex is None: + maxindex = index + else: + maxindex = ops.where(ops.gt(val, maxval), index, maxindex) + + if maxval is None: + maxval = val + else: + maxval = pooling_fn(val, maxval) + + return maxindex + + return fn + + +fallback_adaptive_avg_pool2d = fallback_handler( + aten._adaptive_avg_pool2d.default, add_to_fallback_set=False +) + + +@register_lowering(aten._adaptive_avg_pool2d) +def _adaptive_avg_pool2d(x, output_size): + if x.get_dtype() == torch.int64: + # not supported in eager + raise RuntimeError("'adaptive_avg_pool2d' not implemented for 'Long'") + assert isinstance(x, TensorBox) + assert len(output_size) == 2 + x.realize_hint() + + *batch, h_in, w_in = x.get_size() + + h_in = V.graph.sizevars.guard_int(h_in) + w_in = V.graph.sizevars.guard_int(w_in) + + h_out, w_out = output_size + + # no-op if the same input and output + if h_in == h_out and w_in == w_out: + return clone(x) + + if h_out == 0 or w_out == 0: + o_size = [*batch, h_out, w_out] + return empty(o_size, dtype=x.get_dtype(), device=x.get_device()) + if h_in % h_out == 0 and w_in % w_out == 0: + kernel_size = [FloorDiv(h_in, h_out), FloorDiv(w_in, w_out)] + return avg_pool2d(x, kernel_size) + + h_kernel_max = ceildiv((h_in + h_out - 1), h_out) + w_kernel_max = ceildiv((w_in + w_out - 1), w_out) + + new_size = list(batch) + [h_out, w_out] + dtype = x.get_dtype() + + window_size = h_kernel_max * w_kernel_max + if window_size > 25: + # Kernel size too big. Results in hard-to-optimize Triton code. Use fallback. + return fallback_adaptive_avg_pool2d(x, output_size) + + def start_index(index, out_dim, inp_dim): + return FloorDiv((index * inp_dim), out_dim) + + def end_index(index, out_dim, inp_dim): + return FloorDiv((index + 1) * inp_dim + out_dim - 1, out_dim) + + fn_sum = _adaptive_pooling_fn( + start_index=start_index, + end_index=end_index, + kernel_maxes=[h_kernel_max, w_kernel_max], + in_sizes=[h_in, w_in], + out_sizes=[h_out, w_out], + pooling_fn=ops.add, + ) + + ones_loader = pad_adaptive_loader(ones_like(x)) + + def fn(idx): + return ops.truediv( + fn_sum(idx, pad_adaptive_loader(x)), fn_sum(idx, ones_loader) + ) + + rv = Pointwise.create( + device=x.get_device(), + dtype=dtype, + inner_fn=fn, + ranges=new_size, + ) + # TODO: should we force these to be realized? + return rv + + +fallback_adaptive_max_pool2d = fallback_handler( + aten.adaptive_max_pool2d.default, add_to_fallback_set=False +) + + +@register_lowering(aten.adaptive_max_pool2d) +def adaptive_max_pool2d(x, output_size): + if x.get_dtype() == torch.int64: + # not supported in eager + raise RuntimeError("adaptive_max_pool2d not implemented for Long") + assert isinstance(x, TensorBox) + assert len(output_size) == 2 + x.realize_hint() + + *batch, h_in, w_in = x.get_size() + + h_in = V.graph.sizevars.guard_int(h_in) + w_in = V.graph.sizevars.guard_int(w_in) + + h_out, w_out = output_size + + if h_out == 0 or w_out == 0: + o_size = [*batch, h_out, w_out] + return empty(o_size, dtype=x.get_dtype(), device=x.get_device()), empty( + o_size, dtype=torch.int64, device=x.get_device() + ) + + if h_in % h_out == 0 and w_in % w_out == 0: + # This is handled by a decomposition + raise ValueError + + h_kernel_max = ceildiv((h_in + h_out - 1), h_out) + w_kernel_max = ceildiv((w_in + w_out - 1), w_out) + + new_size = list(batch) + [h_out, w_out] + dtype = x.get_dtype() + + window_size = h_kernel_max * w_kernel_max + if window_size > 25: + # Kernel size too big. Results in hard-to-optimize Triton code. Use fallback. + return fallback_adaptive_max_pool2d(x, output_size) + + def start_index(index, out_dim, inp_dim): + return FloorDiv((index * inp_dim), out_dim) + + def end_index(index, out_dim, inp_dim): + return FloorDiv((index + 1) * inp_dim + out_dim - 1, out_dim) + + inner_func_max_val = _adaptive_pooling_fn( + start_index=start_index, + end_index=end_index, + kernel_maxes=[h_kernel_max, w_kernel_max], + in_sizes=[h_in, w_in], + out_sizes=[h_out, w_out], + pooling_fn=ops.maximum, + ) + + inner_func_max_idx = _adaptive_pooling_fn_with_idx( + start_index=start_index, + end_index=end_index, + kernel_maxes=[h_kernel_max, w_kernel_max], + in_sizes=[h_in, w_in], + out_sizes=[h_out, w_out], + pooling_fn=ops.maximum, + ) + + def inner_fn_max_val(idx): + return inner_func_max_val(idx, pad_adaptive_loader(x, float("-inf"))) + + def inner_fn_max_idx(idx): + return inner_func_max_idx(idx, pad_adaptive_loader(x, float("-inf"))) + + rv = Pointwise.create( + device=x.get_device(), + dtype=dtype, + inner_fn=inner_fn_max_val, + ranges=new_size, + ) + ri = Pointwise.create( + device=x.get_device(), + dtype=torch.int64, + inner_fn=inner_fn_max_idx, + ranges=new_size, + ) + return rv, ri + + +def _fractional_pooling_offsets(samples, in_sz, out_sz, kernel_sz, dim, ndims): + out_sz = out_sz[dim] + in_sz = in_sz[dim] + kernel_sz = kernel_sz[dim] + samples_loader = samples.make_loader() + + def load(prefix, i): + # Handle indexing for samples tensor correctly for different input dimensions + # samples tensor always has shape (N, C, 2) for fractional_max_pool2d where: + # - N=1 for 3D inputs (C,H,W), N=batch_size for 4D inputs (N,C,H,W) + # - C=num_channels + # - 2 for the two spatial dimensions (height, width) + samples_shape = samples.get_size() + + if len(samples_shape) == 3: # Expected: (N, C, 2) + if len(prefix) == 1: + # 3D input case: prefix=(channel,), samples=(1, C, 2) + # Access: samples[0, channel, dim] + sample = samples_loader([0, prefix[0], ndims - 1 - dim]) + elif len(prefix) >= 2: + # 4D+ input case: prefix=(batch, channel, ...), samples=(batch, C, 2) + # Access: samples[batch, channel, dim] + sample = samples_loader([prefix[0], prefix[1], ndims - 1 - dim]) + else: + # Edge case - shouldn't happen for valid fractional pooling + sample = samples_loader([0, 0, ndims - 1 - dim]) + else: + # Fallback for unexpected tensor shapes + sample = samples_loader([*prefix, ndims - 1 - dim]) + i_expr = ops.index_expr(i, samples.get_dtype()) + diff = ops.index_expr(in_sz - kernel_sz, torch.int64) + out_sz_expr = ops.index_expr(out_sz - 1, torch.int64) + alpha = ops.truediv( + ops.to_dtype(diff, torch.float64), ops.to_dtype(out_sz_expr, torch.float64) + ) + alpha = ops.where(ops.eq(out_sz_expr, 0), 0, alpha) + seq_i = ops.trunc((i_expr + sample) * alpha) - ops.trunc(sample * alpha) + seq_i = ops.to_dtype(seq_i, torch.int64) + mask = ops.lt(i_expr, out_sz_expr) + return ops.indirect_indexing(ops.where(mask, seq_i, diff), sympy.sympify(in_sz)) + + return load + + +@register_lowering(aten.fractional_max_pool2d) +def fractional_max_pool2d(x, kernel_size, output_size, random_samples): + return _fractional_max_pool(x, kernel_size, output_size, random_samples, n_dim=2) + + +@register_lowering(aten.fractional_max_pool3d) +def fractional_max_pool3d(x, kernel_size, output_size, random_samples): + return _fractional_max_pool(x, kernel_size, output_size, random_samples, n_dim=3) + + +def _fractional_max_pool(x, kernel_size, output_size, random_samples, n_dim): + x.realize_hint() + batch, inp_dhw = x.shape[:-n_dim], x.shape[-n_dim:] + + with config.patch(unroll_reductions_threshold=25): + dhw_index_fn = [ + _fractional_pooling_offsets( + samples=random_samples, + in_sz=inp_dhw, + out_sz=output_size, + kernel_sz=kernel_size, + ndims=n_dim, + dim=d, + ) + for d in range(n_dim) + ] + + x_loader = x.make_loader() + + def fn_inner(idx, reduction_idx): + prefix = idx[:-n_dim] + return x_loader([*prefix, *increments_to_index(idx, reduction_idx)]) + + def increments_to_index(idx, reduction_idx): + prefix = idx[:-n_dim] + bdhw = idx[-n_dim:] + return [ + dhw_index_fn[d](prefix, bdhw[d]) + reduction_idx[d] + for d in range(n_dim) + ] + + new_size = list(batch) + list(output_size) + dtype = x.get_dtype() + result = Reduction.create( + reduction_type="max", + input_node=x, + device=x.get_device(), + dst_dtype=dtype, + src_dtype=dtype, + inner_fn=fn_inner, + ranges=new_size, + reduction_ranges=kernel_size, + ) + offsets = Reduction.create( + reduction_type="argmax", + input_node=x, + device=x.get_device(), + dst_dtype=torch.int64, + src_dtype=dtype, + inner_fn=fn_inner, + ranges=new_size, + reduction_ranges=kernel_size, + ) + assert isinstance(result, TensorBox), result + if isinstance(result.data.data, Reduction): # type: ignore[attr-defined] + # Only realize if reduction isn't unrolled + result.realize() + assert isinstance(offsets, TensorBox), offsets + if isinstance(offsets.data.data, Reduction): # type: ignore[attr-defined] + # Only realize if reduction isn't unrolled + offsets.realize() + + indices = _pool_offsets_to_indices( + offsets, kernel_size, x.shape, increments_to_index + ) + return result, indices + + +@register_lowering(aten.upsample_nearest2d_backward.default) +def upsample_nearest2d_backward( + x, output_size=None, input_size=None, scales_h=None, scales_w=None +): + x.realize_hint() + + *_batch, inp_h, inp_w = x.get_size() + inp_h = V.graph.sizevars.guard_int(inp_h) + inp_w = V.graph.sizevars.guard_int(inp_w) + + # pyrefly: ignore [not-iterable] + *_batch, out_h, out_w = input_size + + if inp_h % out_h == 0 and inp_w % out_w == 0: + return avg_pool2d( + x, [FloorDiv(inp_h, out_h), FloorDiv(inp_w, out_w)], divisor_override=1 + ) + + h_kernel_max = ceildiv(inp_h, out_h) + w_kernel_max = ceildiv(inp_w, out_w) + + def start_index(index, out_dim, inp_dim): + return CeilDiv(index * inp_dim, sympy.sympify(out_dim)) + + def end_index(index, out_dim, inp_dim): + return start_index((index + 1), out_dim, inp_dim) + + fn_sum = _adaptive_pooling_fn( + start_index=start_index, + end_index=end_index, + kernel_maxes=[h_kernel_max, w_kernel_max], + in_sizes=[inp_h, inp_w], + out_sizes=[out_h, out_w], + pooling_fn=ops.add, + ) + + def fn(idx): + return fn_sum(idx, pad_adaptive_loader(x)) + + rv = Pointwise.create( + device=x.get_device(), + dtype=x.get_dtype(), + inner_fn=fn, + # pyrefly: ignore [bad-argument-type, no-matching-overload] + ranges=list(input_size), + ) + + return rv + + +@register_lowering(aten.avg_pool2d, type_promotion_kind=None) +def avg_pool2d( + x, + kernel_size, + stride=(), + padding=0, + ceil_mode=False, + count_include_pad=True, + divisor_override=None, +): + return _avg_poolnd( + x, + kernel_size, + stride, + padding, + ceil_mode, + count_include_pad, + divisor_override, + dim=2, + ) + + +@register_lowering(aten.avg_pool3d, type_promotion_kind=None) +def avg_pool3d( + x, + kernel_size, + stride=(), + padding=0, + ceil_mode=False, + count_include_pad=True, + divisor_override=None, +): + return _avg_poolnd( + x, + kernel_size, + stride, + padding, + ceil_mode, + count_include_pad, + divisor_override, + dim=3, + ) + + +fallbacks_avg_poolnd = [ + fallback_handler(aten.avg_pool1d.default, add_to_fallback_set=False), + fallback_handler(aten.avg_pool2d.default, add_to_fallback_set=False), + fallback_handler(aten.avg_pool3d.default, add_to_fallback_set=False), +] + + +def _avg_poolnd( + x, + kernel_size, + stride, + padding, + ceil_mode, + count_include_pad, + divisor_override, + dim, +): + if not stride: + stride = kernel_size + if not padding: + padding = [0] * dim + kernel_size = pad_listlike(kernel_size, dim) + stride = pad_listlike(stride, dim) + padding = pad_listlike(padding, dim) + + assert isinstance(x, TensorBox) + assert len(kernel_size) == dim + assert len(stride) == dim + assert len(padding) == dim + assert len(x.get_size()) in (dim + 1, dim + 2) + + x.realize_hint() + batch = x.get_size()[:-dim] + h = x.get_size()[-dim:] + + h_out, ceil_modes = zip( + *[ + pooling_size(h[i], i, kernel_size, stride, padding, ceil_mode) + for i in range(dim) + ] + ) + + if any(padding) or any(ceil_modes): + x_loader = constant_boundary_condition(x, 0.0, dim=dim) + had_padding = True + else: + x_loader = x.make_loader() + had_padding = False + + new_size = list(batch) + list(h_out) + dtype = x.get_dtype() + # compute in higher-precision until scaling + output_dtype = get_promoted_dtype( + x, + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.DEFAULT, + return_compute_dtype=True, + ) + + def fn_inner(idx, reduction_idx): + prefix = idx[:-dim] + bh = idx[-dim:] + ih = reduction_idx + ih = [bh[i] * stride[i] + ih[i] - padding[i] for i in range(dim)] + return x_loader([*prefix, *ih]) + + window_size = functools.reduce(operator.mul, kernel_size) + + if window_size > 25 and any( + V.graph.sizevars.statically_known_true(sympy.Ne(k, s)) + for k, s in zip(kernel_size, stride) + ): + fallback = fallbacks_avg_poolnd[dim - 1] + return fallback( + x, + kernel_size, + stride, + padding, + ceil_mode, + count_include_pad, + divisor_override, + ) + + # TODO: remove this when #100331 is merged. We only do this + # for window_size <=25 to avoid performance regressions compared + # to the previous algorithm which unrolled manually for <=25 + context = ( + config.patch(unroll_reductions_threshold=25) + if window_size <= 25 + else contextlib.nullcontext() + ) + + device = x.get_device() + assert device is not None + + with context: + rv = Reduction.create( + reduction_type="sum", + input_node=x, + device=device, + dst_dtype=output_dtype, + src_dtype=dtype, + inner_fn=fn_inner, + ranges=new_size, + reduction_ranges=kernel_size, + ) + if hasattr(rv.data, "data") and isinstance(rv.data.data, Reduction): + # Only realize if reduction isn't unrolled + rv.realize() + + if not had_padding or divisor_override: + divisor = divisor_override if divisor_override else window_size + result = div_prim(rv, divisor) + else: + + def fn_count(idx): + bh = idx[-dim:] + + divide_factors = [] + for i in range(dim): + hstart = bh[i] * stride[i] - padding[i] + hend = sympy.Min(hstart + kernel_size[i], h[i] + padding[i]) + if not count_include_pad: + hstart = sympy.Max(hstart, 0) + hend = sympy.Min(hend, h[i]) + factor = ops.index_expr(hend - hstart, torch.int32) + divide_factors.append(factor) + return functools.reduce(ops.mul, divide_factors) + + divide_factor = Pointwise.create( + device=x.get_device(), + dtype=dtype, + inner_fn=fn_count, + ranges=new_size, + ) + result = div_prim(rv, divide_factor) + + return to_dtype(result, dtype) + + +fallback_avg_pool2d_backward = fallback_handler( + aten.avg_pool2d_backward.default, add_to_fallback_set=False +) + + +@register_lowering(aten.avg_pool2d_backward, type_promotion_kind=None) +def avg_pool2d_backward( + grad_output, + x, + kernel_size, + stride, + padding, + ceil_mode, + count_include_pad, + divisor_override=None, +): + assert divisor_override is None or divisor_override != 0, "divisor must be not zero" + if not stride: + stride = kernel_size + if not padding: + padding = [0, 0] + + assert isinstance(grad_output, TensorBox) + assert isinstance(x, TensorBox) + assert len(kernel_size) == 2 + assert len(stride) == 2 + assert len(padding) == 2 + assert len(x.get_size()) in (3, 4) + + grad_output.realize_hint() # we will read this many times, so make sure it is computed + + *_, height, width = x.get_size() + + _h_out, ceil_mode1 = pooling_size( + height, 0, kernel_size, stride, padding, ceil_mode + ) + _w_out, ceil_mode2 = pooling_size(width, 1, kernel_size, stride, padding, ceil_mode) + + grad_loader = grad_output.make_loader() + + had_padding = padding[0] or padding[1] or ceil_mode1 or ceil_mode2 + + *_, pooled_height, pooled_width = grad_output.get_size() + new_size = list(x.get_size()) + dtype = x.get_dtype() + + h_window_size = max( + max(FloorDiv(h, stride[0]) - max(0, FloorDiv(h - kernel_size[0], stride[0])), 1) + for h in range(kernel_size[0] * 2) + ) + w_window_size = max( + max(FloorDiv(w, stride[1]) - max(0, FloorDiv(w - kernel_size[1], stride[1])), 1) + for w in range(kernel_size[1] * 2) + ) + + window_size = h_window_size * w_window_size + if window_size > 25: + # Kernel size too big. Results in hard-to-optimize Triton code. Use fallback. + return fallback_avg_pool2d_backward( + grad_output, + x, + kernel_size, + stride, + padding, + ceil_mode, + count_include_pad, + divisor_override, + ) + + def compute_pool_size_without_padding(ph, pw): + """ + This computes the scaling factor that we will divide an element + by when `count_include_pad=False` + """ + stride_h = ops.constant(stride[0], torch.int32) + stride_w = ops.constant(stride[1], torch.int32) + pad_h = ops.constant(padding[0], torch.int32) + pad_w = ops.constant(padding[1], torch.int32) + kernel_h = ops.constant(kernel_size[0], torch.int32) + kernel_w = ops.constant(kernel_size[1], torch.int32) + hstart = ops.sub(ops.mul(ph, stride_h), pad_h) + wstart = ops.sub(ops.mul(pw, stride_w), pad_w) + hend = ops.minimum( + ops.add(hstart, kernel_h), + ops.add(ops.index_expr(height, torch.int32), pad_h), + ) + wend = ops.minimum( + ops.add(wstart, kernel_w), + ops.add(ops.index_expr(width, torch.int32), pad_w), + ) + hstart = ops.maximum(hstart, ops.constant(0, torch.int32)) + wstart = ops.maximum(wstart, ops.constant(0, torch.int32)) + hend = ops.minimum(hend, ops.index_expr(height, torch.int32)) + wend = ops.minimum(wend, ops.index_expr(width, torch.int32)) + divide_factor = ops.mul(ops.sub(hend, hstart), ops.sub(wend, wstart)) + return divide_factor + + def fn(idx): + *prefix, h, w = idx + h = h + padding[0] + w = w + padding[1] + phstart = ops.index_expr( + FloorDiv(h - kernel_size[0] + stride[0], stride[0]), torch.int32 + ) + pwstart = ops.index_expr( + FloorDiv(w - kernel_size[1] + stride[1], stride[1]), torch.int32 + ) + phend = ops.index_expr(FloorDiv(h, stride[0]) + 1, torch.int32) + pwend = ops.index_expr(FloorDiv(w, stride[1]) + 1, torch.int32) + + phstart = ops.maximum(phstart, ops.constant(0, torch.int32)) + pwstart = ops.maximum(pwstart, ops.constant(0, torch.int32)) + phend = ops.minimum(phend, ops.index_expr(pooled_height, torch.int32)) + pwend = ops.minimum(pwend, ops.index_expr(pooled_width, torch.int32)) + + gradient = None + for ph_ in range(h_window_size): + for pw_ in range(w_window_size): + ph = ops.add(phstart, ops.constant(ph_, torch.int32)) + pw = ops.add(pwstart, ops.constant(pw_, torch.int32)) + + if divisor_override is not None: + scale = divisor_override + elif count_include_pad or not had_padding: + scale = kernel_size[0] * kernel_size[1] + else: + scale = compute_pool_size_without_padding(ph, pw) + + part = ops.truediv( + grad_loader( + [ + *prefix, + ops.indirect_indexing( + ops.minimum( + ph, ops.sub(phend, ops.constant(1, torch.int32)) + ), + pooled_height, + check=False, + ), + ops.indirect_indexing( + ops.minimum( + pw, ops.sub(pwend, ops.constant(1, torch.int32)) + ), + pooled_width, + check=False, + ), + ] + ), + scale, + ) + + mask = ops.and_( + ops.lt(ph, phend), + ops.lt(pw, pwend), + ) + if gradient is None: + gradient = ops.where(mask, part, ops.constant(0.0, torch.float32)) + else: + gradient = ops.where(mask, ops.add(gradient, part), gradient) + assert gradient is not None + return gradient + + rv = Pointwise.create( + device=grad_output.get_device(), + dtype=dtype, + inner_fn=fn, + ranges=new_size, + ) + return rv + + +fallback_avg_pool3d_backward = fallback_handler( + aten.avg_pool3d_backward.default, add_to_fallback_set=False +) + + +@register_lowering(aten.avg_pool3d_backward, type_promotion_kind=None) +def avg_pool3d_backward( + grad_output, + x, + kernel_size, + stride, + padding, + ceil_mode, + count_include_pad, + divisor_override=None, +): + assert divisor_override is None or divisor_override != 0, "divisor must be not zero" + if not stride: + stride = kernel_size + if not padding: + padding = [0, 0, 0] + + assert isinstance(grad_output, TensorBox) + assert isinstance(x, TensorBox) + assert len(kernel_size) == 3 + assert len(stride) == 3 + assert len(padding) == 3 + assert len(x.get_size()) in (4, 5) + + grad_output.realize_hint() + + *_batch, depth, height, width = x.get_size() + + _d_out, ceil_mode_d = pooling_size( + depth, 0, kernel_size, stride, padding, ceil_mode + ) + _h_out, ceil_mode_h = pooling_size( + height, 1, kernel_size, stride, padding, ceil_mode + ) + _w_out, ceil_mode_w = pooling_size( + width, 2, kernel_size, stride, padding, ceil_mode + ) + + grad_loader = grad_output.make_loader() + had_padding = any(padding) or ceil_mode_d or ceil_mode_h or ceil_mode_w + + *_, pooled_depth, pooled_height, pooled_width = grad_output.get_size() + new_size = list(x.get_size()) + dtype = x.get_dtype() + + d_window_size, h_window_size, w_window_size = ( + max( + max(d // stride[i] - max(0, (d - kernel_size[i]) // stride[i]), 1) + for d in range(kernel_size[i] * 2) + ) + for i in range(3) + ) + + window_size = d_window_size * h_window_size * w_window_size + if window_size > 125: + # Kernel size too big. Results in hard-to-optimize Triton code. + return fallback_avg_pool3d_backward( + grad_output, + x, + kernel_size, + stride, + padding, + ceil_mode, + count_include_pad, + divisor_override, + ) + + def compute_pool_size_without_padding(pd, ph, pw): + stride_d, stride_h, stride_w = (ops.constant(s, torch.int32) for s in stride) + pad_d, pad_h, pad_w = (ops.constant(p, torch.int32) for p in padding) + kernel_d, kernel_h, kernel_w = ( + ops.constant(k, torch.int32) for k in kernel_size + ) + + dstart, hstart, wstart = ( + ops.sub(ops.mul(p, s), pad) + for p, s, pad in zip( + [pd, ph, pw], [stride_d, stride_h, stride_w], [pad_d, pad_h, pad_w] + ) + ) + dend, hend, wend = ( + ops.minimum( + ops.add(start, k), ops.add(ops.index_expr(dim, torch.int32), pad) + ) + for start, k, dim, pad in zip( + [dstart, hstart, wstart], + [kernel_d, kernel_h, kernel_w], + [depth, height, width], + [pad_d, pad_h, pad_w], + ) + ) + dstart, hstart, wstart = ( + ops.maximum(start, ops.constant(0, torch.int32)) + for start in [dstart, hstart, wstart] + ) + dend, hend, wend = ( + ops.minimum(end, ops.index_expr(dim, torch.int32)) + for end, dim in zip([dend, hend, wend], [depth, height, width]) + ) + divide_factor = ops.mul( + ops.mul(ops.sub(dend, dstart), ops.sub(hend, hstart)), ops.sub(wend, wstart) + ) + return divide_factor + + def fn(idx): + *prefix, d, h, w = idx + d, h, w = (v + pad for v, pad in zip([d, h, w], padding)) + + pdstart, phstart, pwstart = ( + ops.index_expr(FloorDiv(v - k + s, s), torch.int32) + for v, k, s in zip([d, h, w], kernel_size, stride) + ) + + pdend, phend, pwend = ( + ops.index_expr(FloorDiv(v, s) + 1, torch.int32) + for v, s in zip([d, h, w], stride) + ) + + pdstart, phstart, pwstart = ( + ops.maximum(pstart, ops.constant(0, torch.int32)) + for pstart in [pdstart, phstart, pwstart] + ) + pdend, phend, pwend = ( + ops.minimum(pend, ops.index_expr(pooled_dim, torch.int32)) + for pend, pooled_dim in zip( + [pdend, phend, pwend], [pooled_depth, pooled_height, pooled_width] + ) + ) + + gradient = None + # Iterate over the 3D region to accumulate gradients + for pd_ in range(d_window_size): + for ph_ in range(h_window_size): + for pw_ in range(w_window_size): + pd, ph, pw = ( + ops.add(pstart, ops.constant(p_, torch.int32)) + for pstart, p_ in zip( + [pdstart, phstart, pwstart], [pd_, ph_, pw_] + ) + ) + + if divisor_override is not None: + scale = divisor_override + elif count_include_pad or not had_padding: + scale = kernel_size[0] * kernel_size[1] * kernel_size[2] + else: + scale = compute_pool_size_without_padding(pd, ph, pw) + + part = ops.truediv( + grad_loader( + [ + *prefix, + ops.indirect_indexing( + ops.minimum( + pd, ops.sub(pdend, ops.constant(1, torch.int32)) + ), + pooled_depth, + check=False, + ), + ops.indirect_indexing( + ops.minimum( + ph, ops.sub(phend, ops.constant(1, torch.int32)) + ), + pooled_height, + check=False, + ), + ops.indirect_indexing( + ops.minimum( + pw, ops.sub(pwend, ops.constant(1, torch.int32)) + ), + pooled_width, + check=False, + ), + ] + ), + scale, + ) + + mask = ops.and_( + ops.and_(ops.lt(pd, pdend), ops.lt(ph, phend)), + ops.lt(pw, pwend), + ) + if gradient is None: + gradient = ops.where( + mask, part, ops.constant(0.0, torch.float32) + ) + else: + gradient = ops.where(mask, ops.add(gradient, part), gradient) + assert gradient is not None + return gradient + + rv = Pointwise.create( + device=grad_output.get_device(), + dtype=dtype, + inner_fn=fn, + ranges=new_size, + ) + return rv + + +def _validate_reduction_axis(x, axis): + size = x.get_size() + if isinstance(axis, int): + axis = [axis] + elif not axis: + axis = range(len(size)) + if len(size) == 0: + assert tuple(axis) in [(), (0,), (-1,)], f"invalid axis: {axis}" + return [] + axis = list(axis) + for i in range(len(axis)): + if axis[i] < 0: + axis[i] += len(size) if len(size) else 1 + assert 0 <= axis[i] < len(size) or (len(size) == 0 and axis[i] == 0) + assert len(OrderedSet(axis)) == len(axis), "reduction axis not unique" + return axis + + +def _make_reduction_inner( + x, *, axis, keepdims, dtype, override_return_dtype, reduction_type=None +): + if dtype is not None: + x = to_dtype(x, dtype) + size = x.get_size() + axis = OrderedSet[int](_validate_reduction_axis(x, axis)) + + kept_sizes = [] + kept_idx = [] + reduced_sizes = [] + reduced_idx = [] + for i in range(len(size)): + if i in axis: + reduced_idx.append(i) + reduced_sizes.append(size[i]) + else: + kept_idx.append(i) + kept_sizes.append(size[i]) + + # For argmax/argmin compute logical indices when the tensor has non-contiguous layout. + should_compute_logical_index = False + if ( + reduction_type in ("argmax", "argmin") + and len(reduced_sizes) > 1 + and is_triton(x) + ): + if isinstance(x.data, PermuteView): + should_compute_logical_index = True + elif isinstance(x.data, ir.ReinterpretView) or ( + isinstance(x.data, ir.StorageBox) and isinstance(x.data.data, ir.Buffer) + ): + layout = x.get_layout() + should_compute_logical_index = ( + layout.is_transposed() or not layout.is_contiguous() + ) + + def loader(index, reduction_index): + assert len(reduction_index) == len(reduced_idx) + if keepdims: + assert len(index) == len(size) + index = [index[i] for i in kept_idx] + assert len(index) == len(kept_idx) + new_index = [None] * (len(index) + len(reduction_index)) + for idx, var in itertools.chain( + zip(kept_idx, index), zip(reduced_idx, reduction_index) + ): + new_index[idx] = var + value = inner_loader(new_index) + + # For argmax/argmin, return tuple with logical linear index if needed + if should_compute_logical_index: + rindex = [sympy.expand(i) for i in reduction_index] + + # Compute linear index in row-major order + # For reduction_ranges = [4, 6]: linear_index = r0 * 6 + r1 + linear_idx = rindex[0] + for i in range(1, len(rindex)): + linear_idx = linear_idx * reduced_sizes[i] + rindex[i] + + return (value, ops.index_expr(linear_idx, torch.int64)) + + return value + + if keepdims: + new_size = list(size) + for i in reduced_idx: + new_size[i] = sympy.S.One + else: + new_size = kept_sizes + + inner_loader = x.make_loader() + return dict( + device=x.get_device(), + dst_dtype=override_return_dtype or x.get_dtype(), + src_dtype=x.get_dtype(), + inner_fn=loader, + ranges=new_size, + reduction_ranges=reduced_sizes, + ) + + +def make_reduction(reduction_type: ReductionType, override_return_dtype=None): + def inner(x, axis=None, keepdims=False, *, dtype=None): + # For argmax/argmin on boolean tensors, cast to int32 first to ensure + # correct comparison in Triton. See https://github.com/pytorch/pytorch/issues/174069 + # Only apply on Triton backend; MPS handles bool comparisons natively. + if ( + reduction_type in ("argmax", "argmin") + and x.get_dtype() == torch.bool + and is_triton(x) + ): + x = to_dtype(x, torch.int32) + kwargs = _make_reduction_inner( + x, + axis=axis, + keepdims=keepdims, + dtype=dtype, + override_return_dtype=override_return_dtype, + reduction_type=reduction_type, + ) + result = Reduction.create(reduction_type=reduction_type, input_node=x, **kwargs) + if isinstance( + result.data.data, # type: ignore[attr-defined, attr-type, union-attr] + Reduction, + ): # Only realize if reduction isn't unrolled + result.realize() + return result + + return inner + + +def _make_scan_inner(x, *, axis, dtype): + if dtype is not None: + x = to_dtype(x, dtype) + axis = _validate_dim(x, axis) + + return dict( + device=x.get_device(), + dtypes=(x.get_dtype(),), + inner_fns=(x.make_loader(),), + size=x.get_size(), + axis=axis, + ) + + +@register_lowering(aten.mean) +def mean(x, axis=None, keepdim=False, *, dtype=None): + if dtype is not None: + x = to_dtype(x, dtype) + size = x.get_size() + axis = _validate_reduction_axis(x, axis) + # compute in higher-precision until end of mean lowering + output_dtype = x.get_dtype() + if output_dtype in (torch.float16, torch.bfloat16): + x = to_dtype(x, torch.float) + sum_result = sum_(x, axis, keepdim) + denom = sympy_product(size[i] for i in axis) + denom = ir.IndexingConstant(index=denom, dtype=x.get_dtype(), device=x.get_device()) + denom = ExpandView.create(denom, list(sum_result.get_size())) + return to_dtype(div(sum_result, denom), output_dtype) + + +def var_mean_sum_(x, axis, correction, keepdim, return_mean): + if correction is None: + correction = 1 + + size = x.get_size() + axis = _validate_reduction_axis(x, axis) + x_mean = mean(x, axis, keepdim=True) + if return_mean: + x_mean.realize() + + diffs = square(sub(x, x_mean)) + sum_result = sum_(diffs, axis, keepdim) + + denom = sympy_product(size[i] for i in axis) + if correction: + denom = sympy.Max(denom - correction, 0) + denom = ir.IndexingConstant(index=denom, dtype=x.get_dtype(), device=x.get_device()) + denom = ExpandView.create(denom, list(sum_result.get_size())) + x_var = div(sum_result, denom) + if not return_mean: + return (x_var,) + + x_mean = x_mean if keepdim else squeeze(x_mean, axis) + return x_var, x_mean + + +def use_two_step_variance(x, axis, keepdim): + # two-step algorithm can get better performance in small reductions size + # while it can accumulate more numerical error than Welford algorithm. + axis = _validate_reduction_axis(x, axis) + kwargs = _make_reduction_inner( + x, axis=axis, keepdims=keepdim, dtype=None, override_return_dtype=None + ) + + ranges = kwargs["ranges"] + reduction_numel = sympy_product(kwargs["reduction_ranges"]) + device = x.get_device() + if not (device and device.type == "cpu"): + threshold = config.unroll_reductions_threshold + else: + # 1024 is a default value to pass all the UTs about accuracy. + # A larger threshold can still get performance benefits. + threshold = config.cpp.use_two_step_variance_threshold + return ( + isinstance(reduction_numel, sympy.Integer) + and int(reduction_numel) <= threshold + and sympy_product(ranges) != 1 + ) + + +def var_mean_welford_(x, axis, *, correction, keepdim, return_mean): + if correction is None: + correction = 1 + + kwargs = _make_reduction_inner( + x, axis=axis, keepdims=keepdim, dtype=None, override_return_dtype=None + ) + loader = kwargs.pop("inner_fn") + kwargs.pop("dst_dtype") + kwargs.pop("src_dtype") + + mean, m2, _ = ir.WelfordReduction.create( + inner_fns=(loader,), + reduction_type="welford_reduce", + dtype=x.get_dtype(), + **kwargs, + ) + m2.realize() + + dtype = x.get_dtype() + size = x.get_size() + axis = _validate_reduction_axis(x, axis) + rnumel = sympy_product(size[i] for i in axis) + + def get_constant_or_index_expr(x, dtype): + if isinstance(x, sympy.Expr) and not x.is_number: + return ops.to_dtype(ops.index_expr(x, torch.int64), dtype) + return ops.constant(x, dtype) + + def scale_fn(data): + c = get_constant_or_index_expr(correction, dtype) + N = get_constant_or_index_expr(rnumel, dtype) + zero = ops.constant(0, dtype) + return data / ops.maximum(zero, N - c) + + var = make_pointwise(scale_fn)(m2) + + if return_mean: + mean.realize() + return var, mean + return (var,) + + +def var_mean_helper_(x, *, axis, correction, keepdim, return_mean): + out_dtype = x.get_dtype() + compute_dtype = get_computation_dtype(out_dtype) + x = to_dtype(x, compute_dtype, copy=False) + kwargs = dict( + x=x, + axis=axis, + correction=correction, + keepdim=keepdim, + return_mean=return_mean, + ) + output = ( + var_mean_sum_(**kwargs) + if ( + config.mtia.disable_welford_reduction + or use_two_step_variance(x, axis=axis, keepdim=keepdim) + ) + else var_mean_welford_(**kwargs) + ) + output = tuple(to_dtype(x, out_dtype, copy=False) for x in output) + return output[0] if not return_mean else output + + +@register_lowering([aten.var, prims.var]) +def var_(x, axis=None, *, correction=None, keepdim=False): + return var_mean_helper_( + x, axis=axis, correction=correction, keepdim=keepdim, return_mean=False + ) + + +@register_lowering(aten.var_mean) +def var_mean(x, axis=None, *, correction=None, keepdim=False): + return var_mean_helper_( + x, axis=axis, correction=correction, keepdim=keepdim, return_mean=True + ) + + +def pow_recursive(x, y, dtype): + if y < 0: + return pow_recursive(ops.reciprocal(x), -y, dtype) + if y == 0: + return ops.constant(1, dtype) + if y == 1: + return x + + result = pow_recursive(x, y // 2, dtype) + result = ops.mul(result, result) + if (y % 2) == 1: + result = ops.mul(result, x) + return result + + +@make_pointwise +def pow_native(a, b): + return ops.pow(a, b) + + +fallback_pow_tensor_tensor = fallback_handler( + aten.pow.Tensor_Tensor, add_to_fallback_set=False +) +fallback_pow_scalar = fallback_handler(aten.pow.Scalar, add_to_fallback_set=False) +fallback_pow_tensor_scalar = fallback_handler( + aten.pow.Tensor_Scalar, add_to_fallback_set=False +) + + +@register_lowering(aten.pow, broadcast=True) +def pow(a, b): + if isinstance(b, float) and b.is_integer(): + return pow(a, int(b)) + elif isinstance(b, float) and b == 0.5: + return sqrt(a) + elif isinstance(b, int) and b == 1: + return clone(a) + + # Type promotion ensures all tensor arguments have the same type + dtype = next(x.get_dtype() for x in (a, b) if isinstance(x, ir.TensorBox)) + is_integer_pow = is_integer_dtype(dtype) + + # Optimize away small fixed powers, or for integers avoid falling back to ATen + embed_exponent = isinstance(b, int) and ( + -32 < b < 32 or (is_integer_pow and b >= 0) + ) + if embed_exponent: + loader = a.make_loader() + + def fn(idx): + return pow_recursive(loader(idx), b, a.get_dtype()) + + return Pointwise.create( + device=a.get_device(), + dtype=a.get_dtype(), + inner_fn=fn, + ranges=a.get_size(), + ) + + if isinstance(a, Number): + if a == 1: + return full_like(b, 1) + + if a == 2 and is_float_dtype(b.get_dtype()): + return exp2(b) + + if is_integer_pow: + # ops.pow doesn't work for integers + if isinstance(a, Number): + return fallback_pow_scalar(a, b) + elif isinstance(b, Number): + return fallback_pow_tensor_scalar(a, b) + else: + return fallback_pow_tensor_tensor(a, b) + + return pow_native(a, b) + + +def mutate_to(changed, val, unsafe_alias=False): + if isinstance(changed, TensorBox): + changed_data = changed.data + else: + changed_data = changed + if isinstance(val, TensorBox): + val = val.data + + if not isinstance(val, ir.StorageBox): + # introduce a copy to handle views + node = Pointwise.create( + device=changed.get_device(), + dtype=changed.get_dtype(), + inner_fn=val.make_loader(), + ranges=changed.get_size(), + ) + assert isinstance(node, (BaseView, MutableBox)) + val = node.data + assert isinstance(val, ir.StorageBox) + + if isinstance(changed_data, ir.StorageBox) and not ( + changed_data.is_input_buffer() + # In AOTI, module parameters and buffers are not lifted as graph inputs + or changed_data.is_module_buffer() + or isinstance(changed_data.data, ir.NopKernel) + ): + # Fast path, just swing the data pointer + val.realize() + changed_data.data = val.data + return changed + + ir.MutationLayoutSHOULDREMOVE.realize_into( + val, changed_data, unsafe_alias=unsafe_alias + ) + return changed + + +@register_lowering(aten.fill_) +def fill_(x, fill_value): + return mutate_to(x, full_like(x, fill_value)) + + +@register_lowering(aten.copy_, type_promotion_kind=None) +def copy_(dst, src, non_blocking=False): + if dst is src: + # dst.copy_(dst) can happen from the reinplacing pass + return dst + src = to_device(src, dst.get_device()) + src = to_dtype(src, dst.get_dtype()) + src = expand(src, dst.get_size()) + return mutate_to(dst, src) + + +@make_pointwise +def floordiv(a, b): + return ops.floordiv(a, b) + + +@make_pointwise +def truncdiv(a, b): + return ops.truncdiv(a, b) + + +@make_pointwise +def _div_rn(a, b): + return ops.div_rn(a, b) + + +@register_lowering(aten.div, broadcast=True) +def div_mode(a, b, rounding_mode=None): + both_integer = is_integer_type(a) and is_integer_type(b) + both_boolean = is_boolean_type(a) and is_boolean_type(b) + + # floordiv and truncdiv need special handling for integer tensors on Triton, + # see the discussion at https://github.com/triton-lang/triton/issues/605 + if rounding_mode == "floor": + assert not both_boolean, "floordiv operands can not be boolean at the same time" + # Use div_rn (IEEE round-to-nearest) instead of truediv here because + # Triton's default division uses an approximate reciprocal, which can + # produce a result slightly below the true quotient and cause floor() + # to round down by one. + return floordiv(a, b) if both_integer else floor(_div_rn(a, b)) + if rounding_mode == "trunc": + assert not both_boolean, "truncdiv operands can not be boolean at the same time" + return truncdiv(a, b) if both_integer else trunc(div(a, b)) + return div(a, b) + + +@register_lowering([aten.mul], broadcast=True) +def mul(a, b): + both_bool = is_boolean_type(a) and is_boolean_type(b) + if both_bool: + return logical_and(a, b) + else: + fn = ops_wrapper(aten.mul.__name__) + return make_pointwise(fn)(a, b) + + +def get_constant_value(x: ir.IRNode) -> ir.Constant | None: + """Try convert an arbitrary IR node into an ir.Constant value""" + + # First try unwrapping the IRNode to see if it is already an ir.Constant + # Optional step, but avoids unnecessary inner_fn evaluation. + if isinstance(x, ir.MutableBox): + return get_constant_value(x.data) + if isinstance(x, ir.BaseView): + return get_constant_value(x.unwrap_view()) + if isinstance(x, ir.Constant): + return x + + # If the unwrapped node is not an ir.Constant, try evaluating inner_fn + # to see if the returned value is from an `ops.constant` call + if not isinstance(x, ir.Loops): + return None + + handler = torch._inductor.ops_handler.ExtractConstantsHandler(x.get_device()) + with ( + V.set_ops_handler(handler), + patch.object(ir.FlexibleLayout, "allow_indexing", True), + ): + out = x.inner_fn(*x.inner_fn_args()) + + assert isinstance(out, torch._inductor.virtualized.OpsValue) + if isinstance(out.value, ir.Constant): + return out.value + return None + + +# NOTE: prims.div maps to a / b in C, so performs truncation division on +# integer inputs and true division for floating and complex inputs. +@register_lowering([prims.div], broadcast=True) +def div_prim(a, b): + is_integral = all(is_boolean_type(x) or is_integer_type(x) for x in [a, b]) + + if is_integral: + return truncdiv(a, b) + + # Disable CPU optimization to avoid precision issues. + # see https://github.com/pytorch/pytorch/issues/157959 + if (divisor := get_constant_value(b)) is not None and a.get_device().type != "cpu": + # Replace divide by constant with multiply by reciprocal + + if divisor.value == 0: + reciprocal = math.copysign(float("inf"), divisor.value) + else: + reciprocal = 1.0 / divisor.value + return mul(a, reciprocal) + + def fn(*args): + return ops.truediv(*args) + + return make_pointwise(fn)(a, b) + + +@register_lowering( + [aten.true_divide, aten.div.Tensor], + broadcast=True, + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, +) +def div(a, b): + a, b = promote_constants( + (a, b), type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT + ) + return div_prim(a, b) + + +@register_lowering([aten.fmod, prims.fmod], broadcast=True) +def fmod(a, b): + is_integral = is_boolean_type(a) or is_integer_type(a) + + if is_integral: + + def fn(a, b): + return ops.mod(a, b) + + else: + + def fn(a, b): + return ops.fmod(a, b) + + return make_pointwise(fn)(a, b) + + +@register_lowering([aten.sum, prims.sum]) +def sum_(x, axis=None, keepdims=False, *, dtype=None): + if ( + is_integer_dtype(x.get_dtype()) or is_boolean_dtype(x.get_dtype()) + ) and dtype is None: + dtype = torch.int64 + + fn = make_reduction("sum", override_return_dtype=dtype) + return fn(x, axis, keepdims, dtype=dtype) + + +fallback_cumsum = fallback_handler(aten.cumsum.default) +fallback_cumprod = fallback_handler(aten.cumprod.default) +fallback_logcumsumexp = fallback_handler(aten.logcumsumexp.default) +fallback_cummax = fallback_handler(aten.cummax.default) +fallback_cummin = fallback_handler(aten.cummin.default) + + +@register_lowering(aten.cumsum) +def cumsum(x, axis=None, dtype=None): + if ( + is_integer_dtype(x.get_dtype()) or is_boolean_dtype(x.get_dtype()) + ) and dtype is None: + dtype = torch.int64 + + if len(x.get_size()) == 0: + assert axis in [0, -1] + dtype = dtype or x.get_dtype() + return to_dtype(x, dtype, copy=True) + + def combine_fn(a_tuple, b_tuple): + (a,) = a_tuple + (b,) = b_tuple + return (ops.add(a, b),) + + kwargs = _make_scan_inner(x, axis=axis, dtype=dtype) + (result,) = ir.Scan.create(**kwargs, combine_fn=combine_fn) + if result is None: + return fallback_cumsum(x, dim=axis, dtype=dtype) + return result + + +@register_lowering(aten.cumprod) +def cumprod(x, axis=None, dtype=None): + if ( + is_integer_dtype(x.get_dtype()) or is_boolean_dtype(x.get_dtype()) + ) and dtype is None: + dtype = torch.int64 + + if len(x.get_size()) == 0: + assert axis in [0, -1] + dtype = dtype or x.get_dtype() + return to_dtype(x, dtype, copy=True) + + def combine_fn(a_tuple, b_tuple): + (a,) = a_tuple + (b,) = b_tuple + return (ops.mul(a, b),) + + kwargs = _make_scan_inner(x, axis=axis, dtype=dtype) + (result,) = ir.Scan.create(**kwargs, combine_fn=combine_fn) + if result is None: + return fallback_cumprod(x, dim=axis, dtype=dtype) + return result + + +@register_lowering(aten.logcumsumexp) +def logcumsumexp(x, dim): + def log_add_exp_helper(a_tuple, b_tuple): + (a,) = a_tuple + (b,) = b_tuple + min_v = ops.minimum(a, b) + max_v = ops.maximum(a, b) + mask = (min_v != max_v) | (~ops.isinf(min_v)) + return (ops.where(mask, ops.log1p(ops.exp(min_v - max_v)) + max_v, a),) + + dtype = x.get_dtype() + if len(x.get_size()) == 0: + assert dim in [0, -1] + return clone(x) + + kwargs = _make_scan_inner(x, axis=dim, dtype=dtype) + (result,) = ir.Scan.create(**kwargs, combine_fn=log_add_exp_helper) + if result is None: + return fallback_logcumsumexp(x, dim=dim) + return result + + +@register_lowering(aten.cummax, type_promotion_kind=None) +def cummax(x, axis=None): + if len(x.get_size()) == 0: + assert axis in [0, -1] + return clone(x), empty_like(x, dtype=torch.int64) + + dtype = x.get_dtype() + combine_fn = ir.get_reduction_combine_fn( + "argmax", dtype=dtype, arg_break_ties_left=False + ) + + kwargs = _make_scan_inner(x, axis=axis, dtype=dtype) + kwargs["dtypes"] = (dtype, torch.int64) + kwargs["inner_fns"] = ( + x.make_loader(), + lambda idx: ops.index_expr(idx[axis], torch.int64), + ) + values, indices = ir.Scan.create(**kwargs, combine_fn=combine_fn) # type: ignore[arg-type] + if values is None: + return fallback_cummax(x, dim=axis) + return values, indices + + +@register_lowering(aten.cummin, type_promotion_kind=None) +def cummin(x, axis=None): + if len(x.get_size()) == 0: + assert axis in [0, -1] + return clone(x), empty_like(x, dtype=torch.int64) + + dtype = x.get_dtype() + combine_fn = ir.get_reduction_combine_fn( + "argmin", dtype=dtype, arg_break_ties_left=False + ) + + kwargs = _make_scan_inner(x, axis=axis, dtype=dtype) + kwargs["dtypes"] = (dtype, torch.int64) + kwargs["inner_fns"] = ( + x.make_loader(), + lambda idx: ops.index_expr(idx[axis], torch.int64), + ) + values, indices = ir.Scan.create(**kwargs, combine_fn=combine_fn) # type: ignore[arg-type] + if values is None: + return fallback_cummin(x, dim=axis) + return values, indices + + +@register_lowering(aten.prod) +def prod(x, axis=None, keepdims=False, *, dtype=None): + if ( + is_integer_dtype(x.get_dtype()) or is_boolean_dtype(x.get_dtype()) + ) and dtype is None: + dtype = torch.int64 + + fn = make_reduction("prod", override_return_dtype=dtype) + return fn(x, axis, keepdims, dtype=dtype) + + +@register_lowering(aten.any) +def reduce_any(x, dim=None, keepdim=False): + x = to_dtype(x, torch.bool) + return make_reduction("any")(x, axis=dim, keepdims=keepdim) + + +@register_lowering(aten.max, type_promotion_kind=None) +def reduce_max(x, dim=None, keepdim=False): + if dim is not None: + return ( + reduce_amax(x, axis=dim, keepdims=keepdim), + reduce_argmax(x, axis=dim, keepdims=keepdim), + ) + + return reduce_amax(x, axis=None, keepdims=keepdim) + + +@register_lowering(aten.min, type_promotion_kind=None) +def reduce_min(x, dim=None, keepdim=False): + if dim is not None: + return ( + reduce_amin(x, axis=dim, keepdims=keepdim), + reduce_argmin(x, axis=dim, keepdims=keepdim), + ) + + return reduce_amin(x, axis=None, keepdims=keepdim) + + +register_lowering(prims.xor_sum)(make_reduction("xor_sum")) +reduce_amax = register_lowering(aten.amax)(make_reduction("max")) +reduce_amin = register_lowering(aten.amin)(make_reduction("min")) +reduce_argmax = register_lowering(aten.argmax)( + make_reduction("argmax", override_return_dtype=torch.int64) +) +reduce_argmin = register_lowering(aten.argmin)( + make_reduction("argmin", override_return_dtype=torch.int64) +) + +add = register_pointwise( + aten.add, + allow_alpha=True, + use_fma_for_alpha=True, + override_fn_when_input_bool="logical_or", +) + +sort_fallback = fallback_handler(aten.sort.stable, add_to_fallback_set=False) + + +@register_lowering(aten.sort.stable, type_promotion_kind=None) +def sort_stable(x, *, stable=None, dim=-1, descending=False): + if stable is None: + stable = False + + shape = x.get_size() + device = x.get_device() + dim = canonicalize_dim(len(shape), dim) + if len(shape) == 0: + return clone(x), _full(0, device, torch.int64, shape) + + dim_size = shape[dim] if len(shape) else 1 + # Use int32 indices when decompose_sort_ops is enabled, allowing sort + # dimensions up to 2^31-1. Default int16 keeps register pressure low + # on GPU where the bitonic network holds all indices in-block. + if config.triton.decompose_sort_ops: + idx_dtype = torch.int32 + else: + idx_dtype = torch.int16 + if not V.graph.sizevars.statically_known_lt(dim_size, torch.iinfo(idx_dtype).max): + return sort_fallback(x, stable=stable, dim=dim, descending=descending) + + indices = iota( + dim_size, start=0, step=1, dtype=idx_dtype, device=device, requires_grad=False + ) + view_shape = [1] * len(shape) + if len(shape): + view_shape[dim] = dim_size + indices = view(indices, view_shape) + indices = expand(indices, shape) + + values, indices = ir.Sort.create( + device=device, + dtypes=(x.dtype, indices.dtype), + inner_fns=(x.make_loader(), indices.make_loader()), + size=shape, + axis=dim, + stable=stable, + descending=descending, + ) + if values is None: + return sort_fallback(x, stable=stable, dim=dim, descending=descending) + + assert indices is not None + return values, to_dtype(indices, torch.int64) + + +@register_lowering(aten.sort.default, type_promotion_kind=None) +def sort(x, dim=-1, descending=False): + return sort_stable(x, stable=False, dim=dim, descending=descending) + + +# Sort-based op lowerings +# When config.triton.decompose_sort_ops is enabled, decompose into sort-based +# ops so Inductor generates Triton kernels via ir.Sort. +# Otherwise, fall back to ATen eager. +topk_fallback = fallback_handler(aten.topk.default, add_to_fallback_set=False) +kthvalue_fallback = fallback_handler(aten.kthvalue.default, add_to_fallback_set=False) +median_fallback = fallback_handler(aten.median.default, add_to_fallback_set=False) +median_dim_fallback = fallback_handler(aten.median.dim, add_to_fallback_set=False) +mode_fallback = fallback_handler(aten.mode.default, add_to_fallback_set=False) + +# sort/sort.stable already have register_lowering above (sort_stable, sort). +# They use ir.Sort directly and fall back when the dimension is too large. +# When decompose_sort_ops is enabled, the size limit is lifted (int32 indices). + + +@register_lowering(aten.median.default, type_promotion_kind=None) +def median_default(self): + if not config.triton.decompose_sort_ops: + return median_fallback(self) + size = self.get_size() + numel = functools.reduce(operator.mul, size, sympy.Integer(1)) + flat = view(self, [numel]) + sorted_vals, _ = sort_stable(flat, dim=0) + k = (numel - 1) // 2 + return select(sorted_vals, 0, k) + + +@register_lowering(aten.median.dim, type_promotion_kind=None) +def median_dim(self, dim, keepdim=False): + if not config.triton.decompose_sort_ops: + return median_dim_fallback(self, dim, keepdim) + shape = self.get_size() + ndim = len(shape) + if ndim == 0: + return clone(self), _full(0, self.get_device(), torch.int64, shape) + dim = canonicalize_dim(ndim, dim) + sorted_vals, sorted_idxs = sort_stable(self, stable=True, dim=dim) + n = shape[dim] + k = (n - 1) // 2 + values = select(sorted_vals, dim, k) + indices = select(sorted_idxs, dim, k) + if keepdim: + values = unsqueeze(values, dim) + indices = unsqueeze(indices, dim) + return values, indices + + +@register_lowering(aten.mode.default, type_promotion_kind=None) +def mode_default(self, dim=-1, keepdim=False): + """Lower aten.mode via sort-based decomposition or fallback.""" + if not config.triton.decompose_sort_ops: + return mode_fallback(self, dim, keepdim) + shape = self.get_size() + ndim = len(shape) + device = self.get_device() + if ndim == 0: + return clone(self), _full(0, device, torch.int64, shape) + dim = canonicalize_dim(ndim, dim) + sorted_vals, sorted_idxs = sort_stable(self, stable=True, dim=dim) + n = shape[dim] + + # Position indices along dim: [0, 1, ..., n-1] + positions = iota( + n, start=0, step=1, dtype=torch.int64, device=device, requires_grad=False + ) + pos_view_shape = [sympy.Integer(1)] * ndim + pos_view_shape[dim] = n + positions = view(positions, pos_view_shape) + positions = expand(positions, shape) + + # Shift positions by -1, clamp to 0 for position 0 + positions_loader0 = positions.make_loader() + + def prev_pos_fn(idx): + return ops.maximum( + ops.sub(positions_loader0(idx), ops.constant(1, torch.int64)), + ops.constant(0, torch.int64), + ) + + prev_positions = Pointwise.create( + device=decode_device(device), + dtype=torch.int64, + inner_fn=prev_pos_fn, + ranges=shape, + ) + + # Gather shifted values and compare for run boundaries + shifted_vals = gather(sorted_vals, dim, prev_positions) + + sorted_loader = sorted_vals.make_loader() + shifted_loader = shifted_vals.make_loader() + positions_loader = positions.make_loader() + + # is_boundary = (sorted != shifted) | (position == 0) + def is_boundary_fn(idx): + return ops.or_( + ops.ne(sorted_loader(idx), shifted_loader(idx)), + ops.eq(positions_loader(idx), ops.constant(0, torch.int64)), + ) + + is_boundary = Pointwise.create( + device=decode_device(device), + dtype=torch.bool, + inner_fn=is_boundary_fn, + ranges=shape, + ) + + # boundary_pos = where(is_boundary, position, -1) + is_boundary_loader = is_boundary.make_loader() + positions_loader2 = positions.make_loader() + + def boundary_pos_fn(idx): + return ops.where( + is_boundary_loader(idx), + positions_loader2(idx), + ops.constant(-1, torch.int64), + ) + + boundary_pos = Pointwise.create( + device=decode_device(device), + dtype=torch.int64, + inner_fn=boundary_pos_fn, + ranges=shape, + ) + + # Propagate boundary positions forward with cummax + last_boundary, _ = cummax(boundary_pos, dim) + + # run_len = position - last_boundary + 1 + positions_loader3 = positions.make_loader() + last_boundary_loader = last_boundary.make_loader() + + def run_len_fn(idx): + return ops.add( + ops.sub(positions_loader3(idx), last_boundary_loader(idx)), + ops.constant(1, torch.int64), + ) + + run_len = Pointwise.create( + device=decode_device(device), + dtype=torch.int64, + inner_fn=run_len_fn, + ranges=shape, + ) + + # argmax returns first maximum -> end of leftmost longest run + max_pos = reduce_argmax(run_len, axis=dim, keepdims=True) + mode_vals = gather(sorted_vals, dim, max_pos) + mode_idxs = gather(sorted_idxs, dim, max_pos) + + if not keepdim: + mode_vals = squeeze(mode_vals, dim) + mode_idxs = squeeze(mode_idxs, dim) + + return mode_vals, mode_idxs + + +@register_lowering(aten.topk.default, type_promotion_kind=None) +def topk(self, k, dim=-1, largest=True, sorted=True): + if not config.triton.decompose_sort_ops: + return topk_fallback(self, k, dim, largest, sorted) + shape = self.get_size() + ndim = len(shape) + if ndim == 0: + return clone(self), _full(0, self.get_device(), torch.int64, shape) + dim = canonicalize_dim(ndim, dim) + sorted_vals, sorted_idxs = sort_stable( + self, stable=True, dim=dim, descending=largest + ) + values = slice_(sorted_vals, dim, 0, k) + indices = slice_(sorted_idxs, dim, 0, k) + return values, indices + + +@register_lowering(aten.kthvalue.default, type_promotion_kind=None) +def kthvalue(self, k, dim=-1, keepdim=False): + if not config.triton.decompose_sort_ops: + return kthvalue_fallback(self, k, dim, keepdim) + shape = self.get_size() + ndim = len(shape) + if ndim == 0: + return clone(self), _full(0, self.get_device(), torch.int64, shape) + dim = canonicalize_dim(ndim, dim) + sorted_vals, sorted_idxs = sort_stable(self, stable=True, dim=dim) + # k is 1-based + values = select(sorted_vals, dim, k - 1) + indices = select(sorted_idxs, dim, k - 1) + if keepdim: + values = unsqueeze(values, dim) + indices = unsqueeze(indices, dim) + return values, indices + + +def register_pointwise_numeric(op, name=None, triton_fallback=None): + return register_pointwise( + op, + name=name, + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + triton_fallback=triton_fallback, + ) + + +def register_pointwise_numeric_ldf64(op: torch._ops.OpOverloadPacket): + register_op_requires_libdevice_fp64(op.__name__) + return register_pointwise( + op, + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + ) + + +rsqrt = register_pointwise_numeric(aten.rsqrt) +exp = register_pointwise_numeric_ldf64(aten.exp) +exp2 = register_pointwise_numeric(aten.exp2) +expm1 = register_pointwise_numeric(aten.expm1) +relu = register_pointwise(aten.relu) +sigmoid = register_pointwise_numeric_ldf64(aten.sigmoid) +sqrt = register_pointwise_numeric_ldf64(aten.sqrt) +square = register_pointwise(aten.square) +sub = register_pointwise(aten.sub, allow_alpha=True) + + +@register_lowering(aten.addcmul, broadcast=True) +def addcmul(self, tensor1, tensor2, *, value=1): + """ + Computes self + value * tensor1 * tensor2 using FMA for better precision. + + Matches eager CUDA kernel order: self + value * (tensor1 * tensor2) + This is computed as: fma(value, tensor1 * tensor2, self) + + Note: FMA is only used for floating-point types on non-AMD GPUs. For integer types, + we fall back to regular arithmetic since FMA doesn't support integers. + + For floating-point types, we use mul_rn (round-to-nearest multiplication) + to force rounding of the product before the FMA. This prevents Triton's + compiler from fusing the multiplication with the FMA, matching eager's + rounding behavior. + """ + dtype = get_promoted_dtype( + self, + tensor1, + tensor2, + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.DEFAULT, + ) + + self_loader = self.make_loader() + t1_loader = tensor1.make_loader() + t2_loader = tensor2.make_loader() + + # FMA/mul_rn/div_rn are only available for floating-point types on CUDA (non-AMD) + device = self.get_device() + use_fma = ( + dtype.is_floating_point + and not torch.version.hip + and device is not None + and device.type in ["cuda", "xpu"] + ) + + def inner_fn(idx): + self_val = self_loader(idx) + t1_val = t1_loader(idx) + t2_val = t2_loader(idx) + + if value == 1 and use_fma: + return ops.fma(t1_val, t2_val, self_val) + + # Match eager order: self + value * (tensor1 * tensor2) + # Compute tensor1 * tensor2 first + if use_fma: + # Use mul_rn to force rounding of the product, preventing Triton + # from fusing t1*t2 with the subsequent FMA + t1_times_t2 = ops.mul_rn(t1_val, t2_val) + else: + t1_times_t2 = ops.mul(t1_val, t2_val) + + # Use index_expr for sympy expressions (e.g., from .item()), constant otherwise + if isinstance(value, sympy.Basic): + value_expr = ops.index_expr(value, dtype) + else: + value_expr = ops.constant(value, dtype) + + if use_fma: + # Use FMA for floating-point types for better precision + return ops.fma(value_expr, t1_times_t2, self_val) + else: + # Fall back to regular arithmetic for integer types + return ops.add(self_val, ops.mul(value_expr, t1_times_t2)) + + return Pointwise.create( + device=self.get_device(), + dtype=dtype, + inner_fn=inner_fn, + ranges=self.get_size(), + ) + + +@register_lowering(aten.addcdiv, broadcast=True) +def addcdiv(self, tensor1, tensor2, *, value=1): + """ + Computes self + value * (tensor1 / tensor2) using FMA for better precision. + + Matches eager CUDA kernel order: self + value * (tensor1 / tensor2) + This is computed as: fma(value, tensor1 / tensor2, self) + + For value=1: self + tensor1 / tensor2 (no FMA needed, just add the division) + For value!=1: fma(value, div_rn(tensor1, tensor2), self) + + Note: FMA is only used for floating-point types on non-AMD GPUs. For integer types, + we fall back to regular arithmetic since FMA doesn't support integers. + + We use div_rn (round-to-nearest division) to force proper rounding, preventing + Triton from fusing operations in ways that change the rounding behavior. + """ + dtype = get_promoted_dtype( + self, + tensor1, + tensor2, + type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT, + ) + + self_loader = self.make_loader() + t1_loader = tensor1.make_loader() + t2_loader = tensor2.make_loader() + + # FMA/mul_rn/div_rn are only available for floating-point types on CUDA (non-AMD) + device = self.get_device() + use_fma = ( + dtype.is_floating_point + and not torch.version.hip + and device is not None + and device.type in ["cuda", "xpu"] + ) + + def inner_fn(idx): + self_val = self_loader(idx) + t1_val = t1_loader(idx) + t2_val = t2_loader(idx) + + # Compute tensor1 / tensor2 first + # Use div_rn for round-to-nearest division on CUDA to match eager behavior + if use_fma: + t1_div_t2 = ops.div_rn(t1_val, t2_val) + else: + t1_div_t2 = ops.truediv(t1_val, t2_val) + + if value == 1: + # For value=1, just add the division result (no FMA needed) + return ops.add(self_val, t1_div_t2) + + # Use index_expr for sympy expressions (e.g., from .item()), constant otherwise + if isinstance(value, sympy.Basic): + value_expr = ops.index_expr(value, dtype) + else: + value_expr = ops.constant(value, dtype) + + if use_fma: + # Use FMA for floating-point types for better precision + return ops.fma(value_expr, t1_div_t2, self_val) + else: + # Fall back to regular arithmetic for integer types + return ops.add(self_val, ops.mul(value_expr, t1_div_t2)) + + return Pointwise.create( + device=self.get_device(), + dtype=dtype, + inner_fn=inner_fn, + ranges=self.get_size(), + ) + + +_foreach_addcmul_scalar = register_foreach_pointwise( + aten._foreach_addcmul.Scalar, addcmul, allow_alpha=True, scalar_kwarg="value" +) +_foreach_addcdiv_scalar = register_foreach_pointwise( + aten._foreach_addcdiv.Scalar, addcdiv, allow_alpha=True, scalar_kwarg="value" +) + + +register_pointwise_numeric_ldf64(aten.cos) +register_pointwise_numeric_ldf64(aten.sin) +abs = register_pointwise(aten.abs) +bitwise_and = register_pointwise(aten.bitwise_and) +bitwise_left_shift = register_pointwise(aten.bitwise_left_shift) +bitwise_not = register_pointwise( + aten.bitwise_not, override_fn_when_input_bool="logical_not" +) +bitwise_or = register_pointwise(aten.bitwise_or) +bitwise_right_shift = register_pointwise(aten.bitwise_right_shift) +bitwise_xor = register_pointwise(aten.bitwise_xor) +register_pointwise_numeric(aten.lgamma) +erf = register_pointwise_numeric(aten.erf) +register_lowering( + aten.special_erf, type_promotion_kind=ELEMENTWISE_TYPE_PROMOTION_KIND.INT_TO_FLOAT +)(erf) + +register_pointwise_numeric(aten.log1p) +register_pointwise_numeric(aten.tan) +register_pointwise_numeric(aten.tanh) +register_pointwise_numeric_ldf64(aten.log) +logical_and = register_pointwise( + aten.logical_and, + type_promotion_kind=None, + convert_input_to_bool=True, + override_return_dtype=torch.bool, +) +logical_not = register_pointwise( + aten.logical_not, + type_promotion_kind=None, + convert_input_to_bool=True, + override_return_dtype=torch.bool, +) +logical_or = register_pointwise( + aten.logical_or, + type_promotion_kind=None, + convert_input_to_bool=True, + override_return_dtype=torch.bool, +) +logical_xor = register_pointwise( + aten.logical_xor, + type_promotion_kind=None, + convert_input_to_bool=True, + override_return_dtype=torch.bool, +) +maximum = register_pointwise(aten.maximum) +minimum = register_pointwise(aten.minimum) +register_lowering(aten.clamp_min)(maximum) +register_lowering(aten.clamp_max)(minimum) +neg = register_pointwise(aten.neg) +abs = register_pointwise(aten.abs) +reciprocal = register_pointwise_numeric(aten.reciprocal) +register_pointwise(aten.remainder) +sign = register_pointwise(aten.sign, override_fn_when_input_bool="identity") +register_pointwise(aten.ceil) +register_pointwise(aten.signbit, override_return_dtype=torch.bool) + +register_lowering(aten._neg_view)(neg) + +register_pointwise(aten.le, override_return_dtype=torch.bool) +register_pointwise(aten.lt, override_return_dtype=torch.bool) +register_pointwise(aten.ge, override_return_dtype=torch.bool) +gt = register_pointwise(aten.gt, override_return_dtype=torch.bool) +register_pointwise(aten.eq, override_return_dtype=torch.bool) +register_pointwise(aten.ne, override_return_dtype=torch.bool) + +register_pointwise_numeric(aten.cosh) +register_pointwise_numeric(aten.sinh) +register_pointwise_numeric(aten.acos) +register_pointwise_numeric(aten.acosh) +register_pointwise_numeric(aten.asin) +register_pointwise_numeric(aten.asinh) +register_pointwise_numeric(aten.atan2) +register_pointwise_numeric(aten.atan) +register_pointwise_numeric(aten.atanh) +register_pointwise_numeric(aten.copysign) +register_pointwise_numeric(aten.erfc) +register_pointwise_numeric(aten.erfinv) +register_pointwise_numeric(aten.hypot) +register_pointwise_numeric(aten.log10) +register_pointwise_numeric(aten.log2) +register_pointwise_numeric(aten.nextafter) + +from .codegen.common import BackendFeature, pointwise_overrides_data + + +def _get_pointwise_overrides(ns, name): + data = pointwise_overrides_data[name] + op = getattr(ns, data.name, None) + if op is None: + return + + def make_triton_fallback(op): + if data.triton is None: + return fallback_handler(op) + + if isinstance(op, torch._ops.OpOverloadPacket): + for olname in op.overloads(): + ol = getattr(op, olname) + yield ol, data.type_promotion_kind, make_triton_fallback(ol) + else: + yield op, data.type_promotion_kind, make_triton_fallback(op) + + +for name in pointwise_overrides_data: + for op, type_promotion_kind, triton_fallback in _get_pointwise_overrides( + aten, name + ): + register_pointwise( + op, + name=name, + type_promotion_kind=type_promotion_kind, + triton_fallback=triton_fallback, + ) + + for op, type_promotion_kind, triton_fallback in _get_pointwise_overrides( + prims, name + ): + register_pointwise( + op, + name=name, + type_promotion_kind=type_promotion_kind, + triton_fallback=triton_fallback, + ) + + +foreach_add_list = register_foreach_pointwise( + aten._foreach_add.List, add, allow_alpha=True +) +foreach_add_scalar = register_foreach_pointwise( + aten._foreach_add.Scalar, add, allow_alpha=True +) +register_foreach_pointwise(aten._foreach_add.Tensor, add, allow_alpha=True) +foreach_mul_list = register_foreach_pointwise(aten._foreach_mul.List, mul) +register_foreach_pointwise(aten._foreach_mul.Tensor, mul) +foreach_mul_scalar = register_foreach_pointwise(aten._foreach_mul.Scalar, mul) +register_foreach_pointwise(aten._foreach_sub.List, sub) +register_foreach_pointwise(aten._foreach_sub.Scalar, sub) +register_foreach_pointwise(aten._foreach_neg.default, neg) +register_foreach_pointwise(aten._foreach_abs.default, abs) +register_foreach_pointwise(aten._foreach_pow.Scalar, pow) +register_foreach_pointwise(aten._foreach_pow.List, pow) +register_foreach_pointwise(aten._foreach_pow.ScalarAndTensor, pow) +foreach_div_list = register_foreach_pointwise(aten._foreach_div.List, div) +register_foreach_pointwise(aten._foreach_div.Tensor, div) +foreach_div_scalar = register_foreach_pointwise(aten._foreach_div.Scalar, div) +register_foreach_pointwise(aten._foreach_sqrt, sqrt) +register_foreach_pointwise(aten._foreach_rsqrt, rsqrt) +register_foreach_pointwise(aten._foreach_maximum.List, maximum) +register_foreach_pointwise(aten._foreach_maximum.Scalar, maximum) +register_foreach_pointwise(aten._foreach_minimum.List, minimum) +register_foreach_pointwise(aten._foreach_minimum.Scalar, minimum) +register_foreach_pointwise(aten._foreach_clamp_min.List, maximum) +register_foreach_pointwise(aten._foreach_clamp_min.Scalar, maximum) +register_foreach_pointwise(aten._foreach_clamp_max.List, minimum) +register_foreach_pointwise(aten._foreach_clamp_max.Scalar, minimum) +register_foreach_pointwise(aten._foreach_reciprocal, reciprocal) +register_foreach_pointwise(aten._foreach_sign, sign) +register_foreach_pointwise(aten._foreach_clone, clone) +foreach_copy = register_foreach_pointwise(aten._foreach_copy, copy) + + +# these are only encountered as outputs of the graph +# reinplacing epilogue copies improves compile time +# by removing extra buffers sent to the scheduler. +def register_foreach_inplace(aten_op, outplace_aten_op, outplace_op): + inplaceable_foreach_ops[outplace_aten_op] = aten_op + inplace_foreach_ops.add(aten_op) + + def fn(*args, **kwargs): + results = outplace_op(*args, **kwargs) + mut_results = [] + for arg, result in zip(args[0], results): + mut_results.append(mutate_to(arg, result, unsafe_alias=True)) + + return mut_results + + _register_foreach_lowering(aten_op, fn) + + +register_foreach_inplace( + aten._foreach_add_.List, aten._foreach_add.List, foreach_add_list +) +register_foreach_inplace( + aten._foreach_add_.Scalar, aten._foreach_add.Scalar, foreach_add_scalar +) +register_foreach_inplace( + aten._foreach_mul_.List, aten._foreach_mul.List, foreach_mul_list +) +register_foreach_inplace( + aten._foreach_mul_.Scalar, aten._foreach_mul.Scalar, foreach_mul_scalar +) +register_foreach_inplace( + aten._foreach_div_.List, aten._foreach_div.List, foreach_div_list +) +register_foreach_inplace( + aten._foreach_div_.Scalar, aten._foreach_div.Scalar, foreach_div_scalar +) +register_foreach_inplace( + aten._foreach_copy_.default, aten._foreach_copy.default, foreach_copy +) +register_foreach_inplace( + aten._foreach_addcmul_.Scalar, + aten._foreach_addcmul.Scalar, + _foreach_addcmul_scalar, +) +register_foreach_inplace( + aten._foreach_addcdiv_.Scalar, + aten._foreach_addcdiv.Scalar, + _foreach_addcdiv_scalar, +) + + +def register_inplace(aten_op, outplace_op): + @register_lowering(aten_op, type_promotion_kind=None) + def fn(*args, **kwargs): + result = outplace_op(*args, **kwargs) + result = to_dtype(result, args[0].get_dtype()) + return mutate_to(args[0], result) + + return fn + + +register_inplace(aten.add_, add) +register_inplace(aten.bitwise_and_, bitwise_and) +register_inplace(aten.bitwise_left_shift_, bitwise_left_shift) +register_inplace(aten.bitwise_not_, bitwise_not) +register_inplace(aten.bitwise_or_, bitwise_or) +register_inplace(aten.bitwise_right_shift_, bitwise_right_shift) +register_inplace(aten.bitwise_xor_, bitwise_xor) +register_inplace(aten.mul_, mul) +register_inplace(aten.div_.Tensor, div) +register_inplace(aten.div_.Tensor_mode, div_mode) +register_inplace(aten.logical_and_, logical_and) +register_inplace(aten.logical_not_, logical_not) +register_inplace(aten.logical_or_, logical_or) +register_inplace(aten.logical_xor_, logical_xor) +register_inplace(aten.sub_, sub) +register_inplace(aten.relu_, relu) +register_inplace(aten.sigmoid_, sigmoid) + + +register_lowering(aten.__and__)(bitwise_and) +register_lowering(aten.__lshift__)(bitwise_left_shift) +register_lowering(aten.__or__)(bitwise_or) +register_lowering(aten.__rshift__)(bitwise_right_shift) +register_lowering(aten.__xor__)(bitwise_xor) + +register_inplace(aten.__iand__, aten.__and__) +register_inplace(aten.__ilshift__, aten.__lshift__) +register_inplace(aten.__ior__, aten.__or__) +register_inplace(aten.__irshift__, aten.__rshift__) +register_inplace(aten.__ixor__, aten.__xor__) + + +@register_lowering(aten.sym_constrain_range) +def sym_constrain_range(a, min=None, max=None): + return None + + +@register_lowering(aten.sym_size.int) +def sym_size(a, dim): + val = V.graph.current_node.meta["val"] + if isinstance(val, torch.SymInt): + return val.node.expr + else: + return int(val) + + +@register_lowering(aten.sym_stride.int) +def sym_stride(a, dim): + val = V.graph.current_node.meta["val"] + if isinstance(val, torch.SymInt): + return val.node.expr + else: + return int(val) + + +@register_lowering(aten.sym_numel) +def sym_numel(a): + return a.get_numel() + + +for method, func in magic_methods.items(): + register_lowering(method_to_operator(method))(func) # type: ignore[arg-type] + + +@register_lowering(torch.sym_sum) +def sym_sum(*args): + # sym_sum can be called as sym_sum([a, b]) or sym_sum(a, b). + # Normalize to a flat list before summing. + if len(args) == 1 and isinstance(args[0], (list, tuple)): + args = args[0] + return sympy.Add(*args) + + +@register_lowering(aten._foobar) +def foobar(self, *args, **kwargs): + raise NotImplementedError("Helpful for debugging") + + +@register_lowering(torch.ops._inductor_test.realize) +def _realize(x): + x.realize() + return clone(x) + + +@register_lowering(torch.ops.inductor.resize_storage_bytes_) +def resize_storage_bytes_(variable, new_size): + variable.realize() + ir.ResizeStorageBytes(variable, new_size) + return variable + + +@register_lowering(torch.ops.aten.set_.source_Tensor) +def set__source_tensor(self, source_tensor): + self.realize() + source_tensor.realize() + return TensorBox.create(ir.SetSourceTensorKernel(self, source_tensor)) + + +if hasattr(torch.ops.fsdp, "copy_"): + + @register_lowering(torch.ops.fsdp.copy_.default) + def fsdp_copy_(dst, src): + if dst is src: + # dst.copy_(dst) can happen from the reinplacing pass + return dst + src = to_device(src, dst.get_device()) + src = to_dtype(src, dst.get_dtype()) + src = expand(src, dst.get_size()) + return mutate_to(dst, src) + + +@register_lowering(torch.ops.aten.resize) +def resize(x, size, *, memory_format=None): + assert isinstance(x, TensorBox) + assert isinstance(size, (list, tuple)) + + if memory_format is None: + memory_format = torch.contiguous_format + if memory_format == torch.preserve_format: + raise RuntimeError(f"unsupported memory format: {memory_format}") + + if memory_format == torch.channels_last: + assert len(size) == 4 + if memory_format == torch.channels_last_3d: + assert len(size) == 5 + + old_numel = x.get_numel() + dtype = x.get_dtype() + device = x.get_device_or_error() + + if ( + torch.are_deterministic_algorithms_enabled() + and torch.utils.deterministic.fill_uninitialized_memory # type: ignore[attr-defined] + ): + if is_float_dtype(dtype): + uninitialized_val = float("nan") + elif is_integer_dtype(dtype): + uninitialized_val = torch.iinfo(dtype).max + else: + uninitialized_val = True + else: + # using zero as that is what empty does + uninitialized_val = 0.0 + + if V.graph.sizevars.statically_known_equals(old_numel, 0): # type: ignore[arg-type] + return full(size, uninitialized_val, dtype=dtype, device=device) + + strides = x.maybe_get_stride() + has_overlapping = strides is not None and any( + V.graph.sizevars.statically_known_equals(s, 0) for s in strides + ) + if has_overlapping: + # overlapping: provide a contiguous logical view + x_flat = view(x, [old_numel]) + else: + # non-overlapping: keep storage order + if isinstance(x.data, ir.BaseView): + x.data = x.data.unwrap_view() + x_flat = as_strided(x, [old_numel], [1]) + flat_loader = x_flat.make_loader() + out_stride = ir.FlexibleLayout.stride_ordered_for_memory_format(size, memory_format) + out_indexer = ir.FixedLayout(device, dtype, size, out_stride).make_indexer() + + def inner_fn(idx): + flat_index = out_indexer(idx) + flat_index_expr = ops.index_expr(flat_index, torch.int64) + limit = ops.index_expr(old_numel, torch.int64) + mask = ops.lt(flat_index_expr, limit) + return ops.masked(mask, lambda: flat_loader([flat_index]), uninitialized_val) + + out = Pointwise.create( + device=device, dtype=dtype, inner_fn=inner_fn, ranges=list(size) + ) + return out + + +from torch._higher_order_ops.auto_functionalize import auto_functionalized + + +make_fallback(auto_functionalized) + + +@register_lowering(triton_kernel_wrapper_mutation) +def triton_kernel_wrap_( + *, + kernel_idx, + constant_args_idx, + grid, + tma_descriptor_metadata, + kwargs, +): + from torch._higher_order_ops.triton_kernel_wrap import kernel_side_table + + constant_args = kernel_side_table.get_constant_args(constant_args_idx) + ir.UserDefinedTritonKernel( + kernel_idx=kernel_idx, + grid=grid, + tma_descriptor_metadata=tma_descriptor_metadata, + kernel_args={**kwargs, **constant_args}, + ) + return {key: val for key, val in kwargs.items() if isinstance(val, TensorBox)} + + +@register_lowering(torch.ops.higher_order.cond, type_promotion_kind=None) +def cond( + pred, true_fn, false_fn, operands +) -> list[ir.TensorBox | ir.ShapeAsConstantBuffer]: + # TODO: when graph_partition is enabled, skip - partitioning handles control flow + # we run into memory cleanup issue + if any(isinstance(x, IRNode) and is_triton(x) for x in [pred, *operands]): + msg = "control flow operator: torch.cond." + if stack_trace := V.graph.current_node.meta.get("stack_trace", None): + msg = f"{msg} Found from : \n {stack_trace}" + V.graph.disable_cudagraphs_reason = msg + + result = ir.Conditional.create(pred, true_fn, false_fn, operands) + return list(map(TensorBox.create, result)) # pyrefly: ignore no-matching-overload + + +@register_lowering(torch.ops.higher_order.while_loop, type_promotion_kind=None) +def while_loop(cond_fn, body_fn, carried_inputs, additional_inputs, stack_output=False): + # TODO: when graph_partition is enabled, skip - partitioning handles control flow + # we run into memory cleanup issue + if not config.graph_partition and any( + isinstance(x, IRNode) and is_triton(x) + for x in carried_inputs + additional_inputs + ): + msg = "control flow operator: torch.while_loop." + if stack_trace := V.graph.current_node.meta.get("stack_trace", None): + msg = f"{msg} Found from : \n {stack_trace}" + V.graph.disable_cudagraphs_reason = msg + + result = ir.WhileLoop.create( + cond_fn, body_fn, carried_inputs, additional_inputs, stack_output + ) + assert isinstance(result, Sequence) + return list(map(ir.WhileLoop._maybe_wrap_as_tensor_box, result)) + + +register_lowering( + torch.ops.higher_order.while_loop_stack_output, type_promotion_kind=None +)(functools.partial(while_loop, stack_output=True)) + + +@register_lowering(torch.ops.higher_order.invoke_subgraph, type_promotion_kind=None) +def invoke_subgraph(subgraph_fn: ir.Subgraph, identifier: str, *operands): + result = ir.InvokeSubgraph.create(subgraph_fn, *operands) + return list(map(TensorBox.create, result)) # type: ignore[call-overload] + + +_MISSING = object() + + +def process_subgraph_nodes(graph_module: torch.fx.GraphModule, args: list[Any]): + """Process nodes from a FX graph by executing them through V.graph. + + This is a common pattern for executing a subgraph's nodes: + - Placeholder nodes are mapped to the provided args + - Output nodes return their result + - Other nodes are executed via V.graph.run_node + + """ + output = _MISSING + + for i, node in enumerate(graph_module.graph.nodes): + if node.op == "placeholder": + assert node not in V.graph.env + V.graph.env[node] = args[i] + continue + elif node.op == "output": + output_args, kwargs = V.graph.fetch_args_kwargs_from_env(node) + output = torch.fx.Interpreter.output(V.graph, node, output_args, kwargs) + else: + assert node not in V.graph.env + # Track current node for error diagnostics; restore after run_node to handle nested calls correctly + saved_current_node = V.graph.current_node + try: + V.graph.current_node = node + V.graph.env[node] = V.graph.run_node(node) + finally: + V.graph.current_node = saved_current_node + + if output is _MISSING: + raise RuntimeError("No output node found in graph") + + return output + + +# Import the control_deps_op HOP for lowering +from torch._inductor.fx_passes.control_dependencies import control_deps + + +@register_lowering(control_deps, type_promotion_kind=None) +def control_deps_op_lowering(additional_deps, subgraph_fn, *args): + """ + Lower control_deps_op by ensuring dependencies are realized and tracking them. + + The control_deps_op HOP makes dependencies explicit in the graph. During lowering: + 1. Realize all additional dependencies to ensure they're computed + 2. Execute the target operation normally + 3. Track the dependencies for the scheduler + """ + # Realize all additional dependencies + dep_names = [] + for dep in additional_deps: + if not isinstance(dep, IRNode): + continue + + dep.realize() + dep_names.append(dep.get_name()) + + original_args = V.graph.current_node.args + arg_offset = 2 # first two args (additional_deps, subgraph) + assert len(args) + arg_offset == len(original_args) + + operation_len = len(V.graph.operations) + assert len(subgraph_fn.graph_module.graph.find_nodes(op="placeholder")) == len(args) + + # Process subgraph nodes using the shared helper + output = process_subgraph_nodes(subgraph_fn.graph_module, list(args)) + + assert additional_deps + + # some operators, like wait_tensor, just return their input, + # so its more robust to add dep to the operation itself, + # otherwise you can have a cycle of + # a = coll + # b = control_deps(a, mm, ...) + # c = control_deps(b, wait, ...) + # if c == a, then you have a cycle. + for op in V.graph.operations[operation_len:]: + for dep_name in dep_names: + op_name = op.operation_name + assert op_name is not None + V.graph.additional_buffer_deps[op_name].add(dep_name) + + return output + + +@register_lowering(torch._higher_order_ops.invoke_quant, type_promotion_kind=None) +def invoke_quant_tracer(subgraph_fn: ir.Subgraph, *operands, scheme=None): + output = None + quant_options = V.graph.current_node.meta.get("quant_options", None) + assert quant_options is not None + + for i, node in enumerate(subgraph_fn.graph_module.graph.nodes): + if node.op == "placeholder": + V.graph.env[node] = operands[i] + continue + # todo getattr + elif node.op == "output": + args, kwargs = V.graph.fetch_args_kwargs_from_env(node) + + for v in itertools.chain(args, kwargs.values()): + v.realize() + + if quant_options.codegen_low_precision: + V.graph.low_precision_codegen_ops.add(v.get_operation_name()) + + V.graph.invoke_quant_ops.add(v.get_operation_name()) + + output = torch.fx.Interpreter.output(V.graph, node, args, kwargs) + else: + V.graph.env[node] = V.graph.run_node(node) + + return output + + +@register_lowering(associative_scan_op, type_promotion_kind=None) +def associative_scan( + combine_fn: ir.Subgraph, xs, additional_inputs: tuple[torch.Tensor] +): + from .subgraph_lowering import InputDescriptor, lower_pointwise_subgraph + + if len(additional_inputs) > 0: + raise RuntimeError( + "Unable to generate code for associative_scan op, because there are lifted arguments" + ) + + subgraph_inputs = [ + InputDescriptor(dtype=x.get_dtype(), device=x.get_device()) + for x in itertools.chain(xs, xs) + ] + lowered_combine_fn = lower_pointwise_subgraph(combine_fn, subgraph_inputs) # type: ignore[var-annotated] + + def wrapped_combine_fn(lhs, rhs): + return lowered_combine_fn( + *pytree.tree_leaves(lhs), + *pytree.tree_leaves(rhs), + ) + + kwargs = _make_scan_inner(xs[0], axis=0, dtype=None) + kwargs["dtypes"] = tuple(x.get_dtype() for x in xs) + kwargs["inner_fns"] = tuple(x.make_loader() for x in xs) + result = ir.Scan.create( + combine_fn=wrapped_combine_fn, + can_fallback_to_aten=False, + **kwargs, + ) + if result[0] is None: + raise RuntimeError("Unable to generate code for associative_scan op") + return result + + +@register_lowering(torch.ops.prims._sink_tokens.default) +def _sink_tokens(tokens): + return None + + +@register_lowering(torch.ops.prims._make_token.default) +def _make_token(): + return None + + +@register_lowering(torch.ops.higher_order.with_effects, type_promotion_kind=None) +def with_effects(token, op, *args, **kwargs): + """ + We lower the operator directly, and then we add StarDep dependencies to all + the newly created nodes in the graph. + """ + from torch._higher_order_ops.effects import _get_effect, _get_schema + + # Get effect type + effect_type = _get_effect(op) + if effect_type is None and op is torch.ops.higher_order.invoke_subgraph: + from torch._guards import InvokeSubgraphCache, TracingContext + + tracing_ctx = TracingContext.try_get() + if tracing_ctx: + invoke_subgraph_cache = tracing_ctx.hop_dispatch_set_cache.get_cache( + torch.ops.higher_order.invoke_subgraph + ) + if invoke_subgraph_cache: + assert isinstance(invoke_subgraph_cache, InvokeSubgraphCache) + # args[1] is identifier + effects = invoke_subgraph_cache.get_effects(args[1]) + if effects: + assert len(effects) == 1, "Multiple effects NYI" + effect_type = next(iter(effects)) + + # Track operations before + operation_len = len(V.graph.operations) + + # Lower the op + if op in lowerings: + result = lowerings[op](*args, **kwargs) + # Realize so that we can get the ops to show up in V.graph.operations + pytree.tree_map_only(TensorBox, lambda a: a.realize(), result) + else: + + def wrap_tensors(x): + return x.wrap_for_lowering() if isinstance(x, ir.IRNode) else x + + result = pytree.tree_map( + wrap_tensors, ir.FallbackKernel.create(op, *args, **kwargs) + ) + + # Get all the operations created during the lowering above, and add StarDeps + # to the previous node with the same effect + assert len(V.graph.operations[operation_len:]) > 0, ( + f"No operation nodes were generated when lowering effectful operator {op}." + ) + if effect_type: + prev_effect_buffer = V.graph.effectful_ops.get(effect_type) + for new_op in V.graph.operations[operation_len:]: + # Patch has_side_effects to return True + new_op.has_side_effects = lambda: True # pyrefly: ignore[missing-attribute] + if prev_effect_buffer: + op_name = new_op.get_name() # pyrefly: ignore[missing-attribute] + V.graph.additional_star_deps[op_name].add(prev_effect_buffer.get_name()) + # Update the effectful ops chain to point to the latest operation + V.graph.effectful_ops[effect_type] = ( + new_op # pyrefly: ignore[unsupported-operation] + ) + + try: + + def convert_ir_to_value(a): + if isinstance(a, ir.TorchBindObject): + return a.get_value() + elif isinstance(a, TensorBox): + # TensorBox wraps StorageBox, which wraps the actual buffer + # We need to get the example tensor from the inner buffer + try: + storage = a.data + if hasattr(storage, "data") and hasattr( + storage.data, "get_example" + ): + return storage.data.get_example() + except (AttributeError, NotImplementedError): + pass + # Fall back to returning the TensorBox itself if get_example fails + return a + return a + + schema_args, schema_kwargs = pytree.tree_map( + convert_ir_to_value, (args, kwargs) + ) + schema = _get_schema(op, schema_args, schema_kwargs) + except RuntimeError as e: + error_msg = str(e) + log.warning( + "Failed to get schema for %s: %s. Assuming list output", op, error_msg + ) + if isinstance(result, (tuple, list)): + return (token, *result) + else: + return (token, result) + + if len(schema.returns) == 0: + return (token, result) + elif len(schema.returns) == 1: + return (token, result) + else: + return (token, *result) + + +from .comm_lowering import register_comm_lowerings, register_symm_mem_lowerings + + +register_comm_lowerings() +register_symm_mem_lowerings() + + +@register_lowering(inductor_prims.prepare_softmax_online, type_promotion_kind=None) +def prepare_softmax_online(x, dim): + """ + Lowering inductor_prims.prepare_softmax_online to compute max/sum in one pass if no split is needed. + """ + kwargs = _make_reduction_inner( + x, axis=dim, keepdims=True, dtype=None, override_return_dtype=None + ) + + reduction_ranges = kwargs["reduction_ranges"] + rnumel = V.graph.sizevars.simplify(sympy_product(reduction_ranges)) + hint, num_split = ir.Reduction.num_splits( + **kwargs, + reduction_type="online_softmax_reduce", # type: ignore[arg-type] + reduction_numel=rnumel, + ) + + if num_split == 1 and V.graph.sizevars.statically_known_geq( + rnumel, config.unroll_reductions_threshold + ): + max_tensor, sum_tensor = OnlineSoftmaxReduction.create( + input_node=x, num_output=2, reduction_hint=hint, **kwargs + ) + return max_tensor, sum_tensor + else: + # Note: [Split online_softmax_reduce] + # We don't split reduction for online_softmax_reduce for now. + # On one hand, supporting split reduction makes things complex since + # the split out reuctions requires 2 inputs rather than one. + # On the other hand, during training the online_softmax_reduce should + # usually don't requires a split due to large batch size + # (more specifically batch size times sequence length). + # We should support split reduction if we find legit use cases to + # motivate the work. + # + # TODO: does inference need split online_softmax_reduce? + + log.debug( + "Online softmax is disabled on the fly since Inductor decides to split the reduction." + ) + amax = reduce_amax(x, dim, keepdims=True) + exp = lowerings[aten.exp](sub(x, amax)) + xsum = sum_(exp, dim, keepdims=True) + return amax, xsum + + +def _is_sm100_or_later(): + """Check if we're on SM100+ hardware (Blackwell).""" + return torch.cuda.is_available() and torch.cuda.get_device_capability() >= (10, 0) + + +@register_lowering(inductor_prims.cvt_e8m0_rceil, type_promotion_kind=None) +def cvt_e8m0_rceil_lowering(inp): + """ + Lowering for cvt_e8m0_rceil. Uses PTX cvt.rp.satfinite.ue8m0x2.f32 on SM100+. + + The PTX instruction takes 2 float32 and outputs 2 e8m0 packed in uint16. + Currently we pass 0.0 as the second input and only use the low byte result. + """ + # TODO: Optimize to process pairs (pack=2) by creating a custom Pointwise + # that loads adjacent elements, applies PTX to both, and uses a follow-up + # kernel to extract the packed uint16 results as uint8. + if not _is_sm100_or_later(): + raise NotImplementedError( + "cvt_e8m0_rceil requires SM100+ (Blackwell) for PTX instruction support" + ) + + dtype = inp.get_dtype() + if dtype not in (torch.float32, torch.float16, torch.bfloat16): + raise ValueError( + f"cvt_e8m0_rceil requires float32, float16, or bfloat16 input, got {dtype}" + ) + + # Upcast bf16/fp16 to float32 for PTX instruction + if dtype != torch.float32: + inp = to_dtype(inp, torch.float32) + + fn = functools.partial( + ops.inline_asm_elementwise, + asm="cvt.rp.satfinite.ue8m0x2.f32 $0, 0.0, $1;", + constraints="=h,r", + dtype=torch.uint16, + is_pure=True, + pack=1, + ) + result = make_pointwise(fn)(inp) + return to_dtype(result, torch.uint8) + + +@register_lowering( + torch._higher_order_ops.inline_asm_elementwise, type_promotion_kind=None +) +def lower_inline_asm_elementwise( + *inputs, asm_str, constraints, dtype, is_pure=True, pack=1 +): + inputs = broadcast_tensors(*inputs) + + input_dtypes = tuple(inp.get_dtype() for inp in inputs) + loaders = [inp.make_loader() for inp in inputs] + + def inner_fn(idx): + vals = tuple(loader(idx) for loader in loaders) + result = ops.inline_asm_elementwise( + *vals, + asm=asm_str, + constraints=constraints, + dtype=dtype, + is_pure=is_pure, + pack=pack, + input_dtypes=input_dtypes, + ) + # Inductor computes in fp32 for bf16/fp16. Upcast so fused downstream + # ops (reductions, etc.) see fp32 values. The Pointwise's storage dtype + # handles the final downcast on store. + if dtype in (torch.float16, torch.bfloat16): + result = ops.to_dtype(result, torch.float32) + return result + + return ir.Pointwise.create( + device=inputs[0].get_device(), + dtype=dtype, + inner_fn=inner_fn, + ranges=list(inputs[0].get_size()), + ) + + +# populate lowerings defined in kernel/* +from . import kernel + + +import_submodule(kernel) + +from . import quantized_lowerings + + +quantized_lowerings.register_quantized_ops() +quantized_lowerings.register_woq_mm_ops() + +from . import mkldnn_lowerings + + +mkldnn_lowerings.register_onednn_fusion_ops() + +from . import jagged_lowerings + + +jagged_lowerings.register_jagged_ops() + + +@contextlib.contextmanager +def force_fallback(op: torch._ops.OpOverload): + """ + A context manager to force fallback an op. Used in unit test + for FallbackKernel. + """ + assert isinstance(op, torch._ops.OpOverload), ( + "Only OpOverload to make the clean up easier" + ) + old_handler = lowerings.get(op) + try: + register_lowering(op)(fallback_handler(op)) + yield + finally: + if old_handler: + lowerings[op] = old_handler + else: + lowerings.pop(op)